diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index d3a68540d9b4..000000000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,50 +0,0 @@ -defaults: &defaults - docker : - - image: knqyf263/ci-trivy:latest - environment: - CGO_ENABLED: "0" - -jobs: - test: - <<: *defaults - steps: - - checkout - - run: - name: Test - command: go test ./... - release: - <<: *defaults - steps: - - checkout - - run: - name: Release - command: goreleaser --rm-dist - - run: - name: Clone trivy repository - command: git clone git@github.com:knqyf263/trivy-repo.git - - run: - name: Setup git settings - command: | - git config --global user.email "knqyf263@gmail.com" - git config --global user.name "Teppei Fukuda" - - run: - name: Create rpm repository - command: ci/deploy-rpm.sh - - run: - name: Import GPG key - command: echo -e "$GPG_KEY" | gpg --import - - run: - name: Create deb repository - command: ci/deploy-deb.sh - -workflows: - version: 2 - release: - jobs: - - test - - release: - filters: - branches: - ignore: /.*/ - tags: - only: /.*/ diff --git a/.dockerignore b/.dockerignore index 51549fa5a6e6..610fbae488ba 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,6 @@ +.git +.github +.cache +.circleci +integration imgs diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000000..1738163f9adf --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000000..a9291f958d11 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,22 @@ +# Global +* @knqyf263 + +# SBOM/Vulnerability scanning +pkg/dependency/ @knqyf263 @DmitriyLewen +pkg/fanal/ @knqyf263 @DmitriyLewen +pkg/sbom/ @knqyf263 @DmitriyLewen +pkg/scanner/ @knqyf263 @DmitriyLewen + +# Misconfiguration scanning +docs/guide/scanner/misconfiguration/ @simar7 @nikpivkin +docs/guide/target/aws.md @simar7 @nikpivkin +pkg/fanal/analyzer/config/ @simar7 @nikpivkin +pkg/config/aws/ @simar7 @nikpivkin +pkg/iac/ @simar7 @nikpivkin + +# Helm chart +helm/trivy/ @afdesk @simar7 + +# Kubernetes scanning +pkg/k8s/ @afdesk @simar7 +docs/guide/target/kubernetes.md @afdesk @simar7 diff --git a/.github/DISCUSSION_TEMPLATE/adopters.yml b/.github/DISCUSSION_TEMPLATE/adopters.yml new file mode 100644 index 000000000000..03008a4df4f0 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/adopters.yml @@ -0,0 +1,47 @@ +title: "" +labels: ["adopters"] +body: + - type: textarea + id: info + attributes: + label: "[Optional] How do you use Trivy?" + validations: + required: false + - type: textarea + id: info + attributes: + label: "[Optional] Can you provide us with a quote on your favourite part of Trivy? This may be used on the trivy.dev website, posted on Twitter (@AquaTrivy) or similar marketing material." + validations: + required: false + - type: checkboxes + attributes: + label: "[Optional] Which targets are you scanning with Trivy?" + options: + - label: "Container Image" + - label: "Filesystem" + - label: "Git Repository" + - label: "Virtual Machine Image" + - label: "Kubernetes" + - label: "AWS" + - label: "SBOM" + validations: + required: false + - type: checkboxes + attributes: + label: "[Optional] What kind of issues are scanning with Trivy?" + options: + - label: "Software Bill of Materials (SBOM)" + - label: "Known vulnerabilities (CVEs)" + - label: "IaC issues and misconfigurations" + - label: "Sensitive information and secrets" + - label: "Software licenses" + - type: markdown + attributes: + value: | + ## Get in touch + We are always looking for + * User feedback + * Collaboration with other companies and organisations + * Or just to have a chat with you about trivy. + If any of this interests you or your marketing team, please reach out at: oss@aquasec.com + We would love to hear from you! diff --git a/.github/DISCUSSION_TEMPLATE/bugs.yml b/.github/DISCUSSION_TEMPLATE/bugs.yml new file mode 100644 index 000000000000..2be717434b08 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/bugs.yml @@ -0,0 +1,124 @@ +labels: ["kind/bug"] +body: + - type: markdown + attributes: + value: | + #### Note + Feel free to raise a bug report if something doesn't work as expected. + Please ensure that you're not creating a duplicate report by searching the [issues](https://github.com/aquasecurity/trivy/issues)/[discussions](https://github.com/aquasecurity/trivy/discussions) beforehand. + If you see any false positives or false negatives, please file a ticket [here](https://github.com/aquasecurity/trivy/discussions/new?category=false-detection). + + **Do not open a GitHub issue, please.** Maintainers triage discussions and then create issues. + + Please also check [our contribution guidelines](https://trivy.dev/docs/latest/community/contribute/discussion/). + - type: textarea + attributes: + label: Description + description: Briefly describe the problem you are having in a few paragraphs. + validations: + required: true + - type: textarea + attributes: + label: Desired Behavior + description: What did you expect to happen? + validations: + required: true + - type: textarea + attributes: + label: Actual Behavior + description: What happened instead? + validations: + required: true + - type: textarea + attributes: + label: Reproduction Steps + description: How do you trigger this bug? Please walk us through it step by step. + value: | + 1. + 2. + 3. + ... + render: bash + validations: + required: true + - type: dropdown + attributes: + label: Target + description: Which target are you scanning? It is equal to which subcommand you are using. + options: + - Container Image + - Filesystem + - Git Repository + - Virtual Machine Image + - Kubernetes + - AWS + - SBOM + validations: + required: false + - type: dropdown + attributes: + label: Scanner + description: Which scanner are you using? + options: + - Vulnerability + - Misconfiguration + - Secret + - License + validations: + required: false + - type: dropdown + attributes: + label: Output Format + description: Which output format are you using? + options: + - Table + - JSON + - Template + - SARIF + - CycloneDX + - SPDX + validations: + required: false + - type: dropdown + attributes: + label: Mode + description: Which mode are you using? Specify "Standalone" if you are not using `trivy server`. + options: + - Standalone + - Client/Server + validations: + required: false + - type: textarea + attributes: + label: Debug Output + description: Output of run with `--debug` + placeholder: "$ trivy --debug" + render: bash + validations: + required: true + - type: input + attributes: + label: Operating System + description: On what operating system are you running Trivy? + placeholder: "e.g. macOS Big Sur" + validations: + required: true + - type: textarea + attributes: + label: Version + description: Output of `trivy --version` + placeholder: "$ trivy --version" + render: bash + validations: + required: true + - type: checkboxes + attributes: + label: Checklist + description: Have you tried the following? + options: + - label: Run `trivy clean --all` + - label: Read [the troubleshooting](https://trivy.dev/docs/latest/references/troubleshooting/) + - type: markdown + attributes: + value: | + We would be happy if you could share how you are using Trivy [here](https://github.com/aquasecurity/trivy/discussions/new?category=adopters). \ No newline at end of file diff --git a/.github/DISCUSSION_TEMPLATE/documentation.yml b/.github/DISCUSSION_TEMPLATE/documentation.yml new file mode 100644 index 000000000000..488caa24c336 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/documentation.yml @@ -0,0 +1,28 @@ +labels: ["kind/documentation"] +body: + - type: markdown + attributes: + value: | + #### Note + Feel free to create a docs report if something doesn't work as expected or is unclear in the documentation. + Please ensure that you're not creating a duplicate report by searching the [issues](https://github.com/aquasecurity/trivy/issues)/[discussions](https://github.com/aquasecurity/trivy/discussions) beforehand. + + Please also check [our contribution guidelines](https://trivy.dev/docs/latest/community/contribute/discussion/). + - type: textarea + attributes: + label: Description + description: Briefly describe the what has been unclear in the existing documentation + validations: + required: true + - type: textarea + attributes: + label: Link + description: Please provide a link to the current documentation or where you thought to find the information you were looking for + validations: + required: false + - type: textarea + attributes: + label: Suggestions + description: What would you like to have added or changed in the documentation? + validations: + required: true \ No newline at end of file diff --git a/.github/DISCUSSION_TEMPLATE/false-detection.yml b/.github/DISCUSSION_TEMPLATE/false-detection.yml new file mode 100644 index 000000000000..984514f3fad3 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/false-detection.yml @@ -0,0 +1,96 @@ +body: + - type: markdown + attributes: + value: | + #### Note + Feel free to raise a bug report if something doesn't work as expected. + Please ensure that you're not creating a duplicate report by searching the [issues](https://github.com/aquasecurity/trivy/issues)/[discussions](https://github.com/aquasecurity/trivy/discussions) beforehand. + + **Do not open a GitHub issue, please.** Maintainers triage discussions and then create issues. + + Please also check [our contribution guidelines](https://trivy.dev/docs/latest/community/contribute/discussion/). + - type: input + attributes: + label: IDs + description: List the IDs of vulnerabilities, misconfigurations, secrets, or licenses that are either not detected or mistakenly detected. + placeholder: "e.g. CVE-2021-44228, CVE-2022-22965" + validations: + required: true + - type: textarea + attributes: + label: Description + description: Describe the false detection. + validations: + required: true + - type: textarea + attributes: + label: Reproduction Steps + description: How do you trigger this bug? Please walk us through it step by step. + value: | + 1. + 2. + 3. + ... + render: bash + validations: + required: true + - type: dropdown + attributes: + label: Target + description: Which target are you scanning? It is equal to which subcommand you are using. + options: + - Container Image + - Filesystem + - Git Repository + - Virtual Machine Image + - Kubernetes + - AWS + - SBOM + validations: + required: true + - type: dropdown + attributes: + label: Scanner + description: Which scanner are you using? + options: + - Vulnerability + - Misconfiguration + - Secret + - License + validations: + required: true + - type: input + attributes: + label: Target OS + description: What operating system are you scanning? Fill in this field if the scanning target is an operating system. + placeholder: "Example: Ubuntu 22.04" + validations: + required: false + - type: textarea + attributes: + label: Debug Output + description: Output of run with `--debug` + placeholder: "$ trivy --debug" + render: bash + validations: + required: true + - type: textarea + attributes: + label: Version + description: Output of `trivy --version` + placeholder: "$ trivy --version" + render: bash + validations: + required: true + - type: checkboxes + attributes: + label: Checklist + options: + - label: Read [the documentation regarding wrong detection](https://trivy.dev/docs/latest/community/contribute/discussion/#false-detection) + - label: Ran Trivy with `-f json` that shows data sources and confirmed that the security advisory in data sources was correct + validations: + required: true + - type: markdown + attributes: + value: | + We would be happy if you could share how you are using Trivy [here](https://github.com/aquasecurity/trivy/discussions/new?category=adopters). \ No newline at end of file diff --git a/.github/DISCUSSION_TEMPLATE/ideas.yml b/.github/DISCUSSION_TEMPLATE/ideas.yml new file mode 100644 index 000000000000..461383ae2465 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/ideas.yml @@ -0,0 +1,47 @@ +labels: ["kind/feature"] +body: + - type: markdown + attributes: + value: | + #### Note + Feel free to share your idea. + Please ensure that you're not creating a duplicate ticket by searching the [issues](https://github.com/aquasecurity/trivy/issues)/[discussions](https://github.com/aquasecurity/trivy/discussions) beforehand. + + **Do not open a GitHub issue, please.** Maintainers triage discussions and then create issues. + + Please also check [our contribution guidelines](https://trivy.dev/docs/latest/community/contribute/discussion/). + - type: textarea + attributes: + label: Description + description: Describe your idea. + validations: + required: true + - type: dropdown + attributes: + label: Target + description: Which target is your idea related to? + options: + - Container Image + - Filesystem + - Git Repository + - Virtual Machine Image + - Kubernetes + - AWS + - SBOM + validations: + required: false + - type: dropdown + attributes: + label: Scanner + description: Which scanner is your idea related to? + options: + - Vulnerability + - Misconfiguration + - Secret + - License + validations: + required: false + - type: markdown + attributes: + value: | + We would be happy if you could share how you are using Trivy [here](https://github.com/aquasecurity/trivy/discussions/new?category=adopters). diff --git a/.github/DISCUSSION_TEMPLATE/q-a.yml b/.github/DISCUSSION_TEMPLATE/q-a.yml new file mode 100644 index 000000000000..504f3025ac24 --- /dev/null +++ b/.github/DISCUSSION_TEMPLATE/q-a.yml @@ -0,0 +1,84 @@ +labels: ["triage/support"] +body: + - type: markdown + attributes: + value: | + #### Note + If you have any troubles/questions, feel free to ask. + Please ensure that you're not asking a duplicate question by searching the [issues](https://github.com/aquasecurity/trivy/issues)/[discussions](https://github.com/aquasecurity/trivy/discussions) beforehand. + + **Do not open a GitHub issue, please.** Maintainers triage discussions and then create issues. + + Please also check [our contribution guidelines](https://trivy.dev/docs/latest/community/contribute/discussion/). + - type: textarea + attributes: + label: Question + description: What kind of problem are you facing? Or, what questions do you have? + validations: + required: true + - type: dropdown + attributes: + label: Target + description: Which target are you scanning? It is equal to which subcommand you are using. + options: + - Container Image + - Filesystem + - Git Repository + - Virtual Machine Image + - Kubernetes + - AWS + - SBOM + validations: + required: false + - type: dropdown + attributes: + label: Scanner + description: Which scanner are you using? + options: + - Vulnerability + - Misconfiguration + - Secret + - License + validations: + required: false + - type: dropdown + attributes: + label: Output Format + description: Which output format are you using? + options: + - Table + - JSON + - Template + - SARIF + - CycloneDX + - SPDX + validations: + required: false + - type: dropdown + attributes: + label: Mode + description: Which mode are you using? Specify "Standalone" if you are not using `trivy server`. + options: + - Standalone + - Client/Server + validations: + required: false + - type: input + attributes: + label: Operating System + description: What operating system are you using? + placeholder: "Example: macOS Big Sur" + validations: + required: false + - type: textarea + attributes: + label: Version + description: Output of `trivy --version` + placeholder: "$ trivy --version" + render: bash + validations: + required: false + - type: markdown + attributes: + value: | + We would be happy if you could share how you are using Trivy [here](https://github.com/aquasecurity/trivy/discussions/new?category=adopters. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000000..80284b7bd81d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,17 @@ +blank_issues_enabled: false +contact_links: + - name: Report a false detection + url: https://github.com/aquasecurity/trivy/discussions/new?category=false-detection + about: Report false positives/negatives + - name: Report a bug + url: https://github.com/aquasecurity/trivy/discussions/new?category=bugs + about: Report bugs + - name: Enhance documentation + url: https://github.com/aquasecurity/trivy/discussions/new?category=documentation + about: Make suggestions to the documentation + - name: Request a feature enhancement + url: https://github.com/aquasecurity/trivy/discussions/new?category=ideas + about: Share ideas for new features + - name: Ask the community for help + url: https://github.com/aquasecurity/trivy/discussions/new?category=q-a + about: Ask questions and discuss with other community members \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/maintainer.md b/.github/ISSUE_TEMPLATE/maintainer.md new file mode 100644 index 000000000000..ae0c27d2d00f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/maintainer.md @@ -0,0 +1,11 @@ +--- +name: Maintainer +about: Create an issue by maintainers +title: '' +labels: '' +assignees: '' + +--- + +## Are you a maintainer of the Trivy project? +If not, please open [a discussion](https://github.com/aquasecurity/trivy/discussions); if you are, please review [the guideline](https://trivy.dev/docs/latest/community/contribute/discussion/). diff --git a/.github/actions/setup-go/action.yaml b/.github/actions/setup-go/action.yaml new file mode 100644 index 000000000000..afecbc56e9a9 --- /dev/null +++ b/.github/actions/setup-go/action.yaml @@ -0,0 +1,31 @@ +name: Setup Go +description: > + Set up Go with the major.minor version extracted from go.mod. + This ensures check-latest works correctly even when go.mod contains a patch version. + cf. https://github.com/actions/setup-go/issues/713 + +inputs: + go-version-file: + description: Path to the go.mod file + required: false + default: go.mod + +runs: + using: composite + steps: + - name: Extract Go version from go.mod + id: go-version + shell: bash + run: | + # Extract the major.minor version from the go directive in go.mod + # e.g. 1.25.5 → 1.25 + go_version=$(grep -m1 '^go ' "${{ inputs.go-version-file }}" | awk '{print $2}') + major_minor=$(echo "$go_version" | cut -d. -f1,2) + echo "version=$major_minor" >> "$GITHUB_OUTPUT" + + - name: Set up Go + uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 + with: + go-version: ${{ steps.go-version.outputs.version }} + cache: false + check-latest: true diff --git a/.github/actions/trivy-triage/Makefile b/.github/actions/trivy-triage/Makefile new file mode 100644 index 000000000000..de557aa565f7 --- /dev/null +++ b/.github/actions/trivy-triage/Makefile @@ -0,0 +1,3 @@ +.PHONEY: test +test: helpers.js helpers.test.js + node --test helpers.test.js diff --git a/.github/actions/trivy-triage/action.yaml b/.github/actions/trivy-triage/action.yaml new file mode 100644 index 000000000000..f847c4333866 --- /dev/null +++ b/.github/actions/trivy-triage/action.yaml @@ -0,0 +1,29 @@ +name: 'trivy-discussion-triage' +description: 'automatic triage of Trivy discussions' +inputs: + discussion_num: + description: 'Discussion number to triage' + required: false +runs: + using: "composite" + steps: + - name: Conditionally label discussions based on category and content + env: + GH_TOKEN: ${{ github.token }} + uses: actions/github-script@v6 + with: + script: | + const {detectDiscussionLabels, fetchDiscussion, labelDiscussion } = require('${{ github.action_path }}/helpers.js'); + const config = require('${{ github.action_path }}/config.json'); + discussionNum = parseInt(${{ inputs.discussion_num }}); + let discussion; + if (discussionNum > 0) { + discussion = (await fetchDiscussion(github, context.repo.owner, context.repo.repo, discussionNum)).repository.discussion; + } else { + discussion = context.payload.discussion; + } + const labels = detectDiscussionLabels(discussion, config.discussionLabels); + if (labels.length > 0) { + console.log(`Adding labels ${labels} to discussion ${discussion.node_id}`); + labelDiscussion(github, discussion.node_id, labels); + } diff --git a/.github/actions/trivy-triage/config.json b/.github/actions/trivy-triage/config.json new file mode 100644 index 000000000000..a972fb024f58 --- /dev/null +++ b/.github/actions/trivy-triage/config.json @@ -0,0 +1,14 @@ +{ + "discussionLabels": { + "Container Image":"LA_kwDOCsUTCM75TTQU", + "Filesystem":"LA_kwDOCsUTCM75TTQX", + "Git Repository":"LA_kwDOCsUTCM75TTQk", + "Virtual Machine Image":"LA_kwDOCsUTCM8AAAABMpz1bw", + "Kubernetes":"LA_kwDOCsUTCM75TTQv", + "AWS":"LA_kwDOCsUTCM8AAAABMpz1aA", + "Vulnerability":"LA_kwDOCsUTCM75TTPa", + "Misconfiguration":"LA_kwDOCsUTCM75TTP8", + "License":"LA_kwDOCsUTCM77ztRR", + "Secret":"LA_kwDOCsUTCM75TTQL" + } +} \ No newline at end of file diff --git a/.github/actions/trivy-triage/helpers.js b/.github/actions/trivy-triage/helpers.js new file mode 100644 index 000000000000..cbd99c1aace8 --- /dev/null +++ b/.github/actions/trivy-triage/helpers.js @@ -0,0 +1,81 @@ +const patterns = { + Scanner: /### Scanner\r?\n\r?\n(.+)/, + Target: /### Target\r?\n\r?\n(.+)/, +}; + +module.exports = { + detectDiscussionLabels: (discussion, configDiscussionLabels) => { + const res = []; + const discussionId = discussion.id; + const category = discussion.category.name; + const body = discussion.body; + if (category !== "Ideas") { + console.log(`skipping discussion with category ${category} and body ${body}`); + return []; + } + + for (const key in patterns) { + const match = body.match(patterns[key]); + if (match && match.length > 1 && match[1] !== "None") { + const val = configDiscussionLabels[match[1]]; + if (val === undefined && match[1]) { + console.warn( + `Value for ${key.toLowerCase()} key "${ + match[1] + }" not found in configDiscussionLabels` + ); + } else { + res.push(val); + } + } + } + return res; + }, + fetchDiscussion: async (github, owner, repo, discussionNum) => { + const query = `query Discussion ($owner: String!, $repo: String!, $discussion_num: Int!){ + repository(name: $repo, owner: $owner) { + discussion(number: $discussion_num) { + number, + id, + body, + category { + id, + name + }, + labels(first: 100) { + edges { + node { + id, + name + } + } + } + } + } + }`; + const vars = { + owner: owner, + repo: repo, + discussion_num: discussionNum + }; + return github.graphql(query, vars); + }, + labelDiscussion: async (github, discussionId, labelIds) => { + const query = `mutation AddLabels($labelId: ID!, $labelableId:ID!) { + addLabelsToLabelable( + input: {labelIds: [$labelId], labelableId: $labelableId} + ) { + clientMutationId + } + }`; + // TODO: add all labels in one call + labelIds.forEach((labelId) => { + const vars = { + labelId: labelId, + labelableId: discussionId + }; + github.graphql(query, vars); + }); + } +}; + diff --git a/.github/actions/trivy-triage/helpers.test.js b/.github/actions/trivy-triage/helpers.test.js new file mode 100644 index 000000000000..5dd556584532 --- /dev/null +++ b/.github/actions/trivy-triage/helpers.test.js @@ -0,0 +1,108 @@ +const assert = require('node:assert/strict'); +const { describe, it } = require('node:test'); +const {detectDiscussionLabels} = require('./helpers.js'); + +const configDiscussionLabels = { + "Container Image":"ContainerImageLabel", + "Filesystem":"FilesystemLabel", + "Vulnerability":"VulnerabilityLabel", + "Misconfiguration":"MisconfigurationLabel", +}; + +describe('trivy-triage', async function() { + describe('detectDiscussionLabels', async function() { + it('detect scanner label', async function() { + const discussion = { + body: 'hello hello\nbla bla.\n### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.', + category: { + name: 'Ideas' + } + }; + const labels = detectDiscussionLabels(discussion, configDiscussionLabels); + assert(labels.includes('VulnerabilityLabel')); + }); + it('detect target label', async function() { + const discussion = { + body: 'hello hello\nbla bla.\n### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.', + category: { + name: 'Ideas' + } + }; + const labels = detectDiscussionLabels(discussion, configDiscussionLabels); + assert(labels.includes('ContainerImageLabel')); + }); + it('detect label when it is first', async function() { + const discussion = { + body: '### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.', + category: { + name: 'Ideas' + } + }; + const labels = detectDiscussionLabels(discussion, configDiscussionLabels); + assert(labels.includes('ContainerImageLabel')); + }); + it('detect label when it is last', async function() { + const discussion = { + body: '### Scanner\n\nVulnerability\n### Target\n\nContainer Image', + category: { + name: 'Ideas' + } + }; + const labels = detectDiscussionLabels(discussion, configDiscussionLabels); + assert(labels.includes('ContainerImageLabel')); + }); + it('detect scanner and target labels', async function() { + const discussion = { + body: 'hello hello\nbla bla.\n### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.', + category: { + name: 'Ideas' + } + }; + const labels = detectDiscussionLabels(discussion, configDiscussionLabels); + assert(labels.includes('ContainerImageLabel')); + assert(labels.includes('VulnerabilityLabel')); + }); + it('detect scanner and target labels on windows', async function() { + const discussion = { + body: 'hello hello\r\nbla bla.\r\n### Scanner\r\n\r\nVulnerability\r\n### Target\r\n\r\nContainer Image\r\nbye bye.', + category: { + name: 'Ideas' + } + }; + const labels = detectDiscussionLabels(discussion, configDiscussionLabels); + assert(labels.includes('ContainerImageLabel')); + assert(labels.includes('VulnerabilityLabel')); + }); + it('not detect other labels', async function() { + const discussion = { + body: 'hello hello\nbla bla.\n### Scanner\n\nVulnerability\n### Target\n\nContainer Image\nbye bye.', + category: { + name: 'Ideas' + } + }; + const labels = detectDiscussionLabels(discussion, configDiscussionLabels); + assert(!labels.includes('FilesystemLabel')); + assert(!labels.includes('MisconfigurationLabel')); + }); + it('ignores unmatched label values from body', async function() { + const discussion = { + body: '### Target\r\n\r\nNone\r\n\r\n### Scanner\r\n\r\nMisconfiguration', + category: { + name: 'Ideas' + } + }; + const labels = detectDiscussionLabels(discussion, configDiscussionLabels); + assert.deepStrictEqual(labels, ['MisconfigurationLabel']); + }); + it('process only relevant categories', async function() { + const discussion = { + body: 'hello world', + category: { + name: 'Announcements' + } + }; + const labels = detectDiscussionLabels(discussion, configDiscussionLabels); + assert(labels.length === 0); + }); + }); +}); diff --git a/.github/actions/trivy-triage/testutils/discussion-payload-sample.json b/.github/actions/trivy-triage/testutils/discussion-payload-sample.json new file mode 100644 index 000000000000..5615fddee804 --- /dev/null +++ b/.github/actions/trivy-triage/testutils/discussion-payload-sample.json @@ -0,0 +1,65 @@ +{ + "active_lock_reason": null, + "answer_chosen_at": null, + "answer_chosen_by": null, + "answer_html_url": null, + "author_association": "OWNER", + "body": "### Description\n\nlfdjs lfkdj dflsakjfd ';djk \r\nfadfd \r\nasdlkf \r\na;df \r\ndfsal;kfd ;akjl\n\n### Target\n\nContainer Image\n\n### Scanner\n\nMisconfiguration", + "category": { + "created_at": "2023-07-02T10:14:46.000+03:00", + "description": "Share ideas for new features", + "emoji": ":bulb:", + "id": 39743708, + "is_answerable": false, + "name": "Ideas", + "node_id": "DIC_kwDOE0GiPM4CXnDc", + "repository_id": 323068476, + "slug": "ideas", + "updated_at": "2023-07-02T10:14:46.000+03:00" + }, + "comments": 0, + "created_at": "2023-09-11T08:40:11Z", + "html_url": "https://github.com/itaysk/testactions/discussions/9", + "id": 5614504, + "locked": false, + "node_id": "D_kwDOE0GiPM4AVauo", + "number": 9, + "reactions": { + "+1": 0, + "-1": 0, + "confused": 0, + "eyes": 0, + "heart": 0, + "hooray": 0, + "laugh": 0, + "rocket": 0, + "total_count": 0, + "url": "https://api.github.com/repos/itaysk/testactions/discussions/9/reactions" + }, + "repository_url": "https://api.github.com/repos/itaysk/testactions", + "state": "open", + "state_reason": null, + "timeline_url": "https://api.github.com/repos/itaysk/testactions/discussions/9/timeline", + "title": "Title title", + "updated_at": "2023-09-11T08:40:11Z", + "user": { + "avatar_url": "https://avatars.githubusercontent.com/u/1161307?v=4", + "events_url": "https://api.github.com/users/itaysk/events{/privacy}", + "followers_url": "https://api.github.com/users/itaysk/followers", + "following_url": "https://api.github.com/users/itaysk/following{/other_user}", + "gists_url": "https://api.github.com/users/itaysk/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/itaysk", + "id": 1161307, + "login": "itaysk", + "node_id": "MDQ6VXNlcjExNjEzMDc=", + "organizations_url": "https://api.github.com/users/itaysk/orgs", + "received_events_url": "https://api.github.com/users/itaysk/received_events", + "repos_url": "https://api.github.com/users/itaysk/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/itaysk/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/itaysk/subscriptions", + "type": "User", + "url": "https://api.github.com/users/itaysk" + } +} \ No newline at end of file diff --git a/.github/actions/trivy-triage/testutils/fetchDiscussion.sh b/.github/actions/trivy-triage/testutils/fetchDiscussion.sh new file mode 100755 index 000000000000..9c213f948d91 --- /dev/null +++ b/.github/actions/trivy-triage/testutils/fetchDiscussion.sh @@ -0,0 +1,29 @@ +#! /bin/bash +# fetch discussion by discussion number +# requires authenticated gh cli, assumes repo but current git repository +# args: +# $1: discussion number, e.g 123, required + +discussion_num="$1" +gh api graphql -F discussion_num="$discussion_num" -F repo="{repo}" -F owner="{owner}" -f query=' + query Discussion ($owner: String!, $repo: String!, $discussion_num: Int!){ + repository(name: $repo, owner: $owner) { + discussion(number: $discussion_num) { + number, + id, + body, + category { + id, + name + }, + labels(first: 100) { + edges { + node { + id, + name + } + } + } + } + } + }' \ No newline at end of file diff --git a/.github/actions/trivy-triage/testutils/fetchLabels.sh b/.github/actions/trivy-triage/testutils/fetchLabels.sh new file mode 100755 index 000000000000..87736eefc1aa --- /dev/null +++ b/.github/actions/trivy-triage/testutils/fetchLabels.sh @@ -0,0 +1,16 @@ +#! /bin/bash +# fetch labels and their IDs +# requires authenticated gh cli, assumes repo but current git repository + +gh api graphql -F repo="{repo}" -F owner="{owner}" -f query=' + query GetLabelIds($owner: String!, $repo: String!) { + repository(name: $repo, owner: $owner) { + id + labels(first: 100) { + nodes { + id + name + } + } + } + }' \ No newline at end of file diff --git a/.github/actions/trivy-triage/testutils/labelDiscussion.sh b/.github/actions/trivy-triage/testutils/labelDiscussion.sh new file mode 100755 index 000000000000..e10d043f9f39 --- /dev/null +++ b/.github/actions/trivy-triage/testutils/labelDiscussion.sh @@ -0,0 +1,16 @@ +#! /bin/bash +# add a label to a discussion +# requires authenticated gh cli, assumes repo but current git repository +# args: +# $1: discussion ID (not number!), e.g DIC_kwDOE0GiPM4CXnDc, required +# $2: label ID, e.g. MDU6TGFiZWwzNjIzNjY0MjQ=, required +discussion_id="$1" +label_id="$2" +gh api graphql -F labelableId="$discussion_id" -F labelId="$label_id" -F repo="{repo}" -F owner="{owner}" -f query=' + mutation AddLabels($labelId: ID!, $labelableId:ID!) { + addLabelsToLabelable( + input: {labelIds: [$labelId], labelableId: $labelableId} + ) { + clientMutationId + } + }' \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..db6919ddb84b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,45 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directories: + - / + - /.github/actions/* + schedule: + interval: monthly + groups: + github-actions: + patterns: + - "*" + - package-ecosystem: docker + directory: / + schedule: + interval: monthly + groups: + docker: + patterns: + - "*" + - package-ecosystem: gomod + open-pull-requests-limit: 10 + directory: / + schedule: + interval: weekly + cooldown: + default-days: 3 + ignore: + - dependency-name: "github.com/aquasecurity/trivy-*" ## `trivy-*` dependencies are updated manually + groups: + aws: + patterns: + - "github.com/aws/*" + docker: + patterns: + - "github.com/docker/*" + - "github.com/moby/*" + testcontainers: + patterns: + - "github.com/testcontainers/*" + common: + exclude-patterns: + - "github.com/aquasecurity/trivy-*" + patterns: + - "*" \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000000..3c83e0c0446b --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,18 @@ +## Description + +## Related issues +- Close #XXX + +## Related PRs +- [ ] #XXX +- [ ] #YYY + +Remove this section if you don't have related PRs. + +## Checklist +- [ ] I've read the [guidelines for contributing](https://trivy.dev/docs/latest/community/contribute/pr/) to this repository. +- [ ] I've followed the [conventions](https://trivy.dev/docs/latest/community/contribute/pr/#title) in the PR title. +- [ ] I've added tests that prove my fix is effective or that my feature works. +- [ ] I've updated the [documentation](https://github.com/aquasecurity/trivy/blob/main/docs) with the relevant information (if needed). +- [ ] I've added usage information (if the PR introduces new options) +- [ ] I've included a "before" and "after" example to the description (if the PR is a user interface change). diff --git a/.github/workflows/apidiff.yaml b/.github/workflows/apidiff.yaml new file mode 100644 index 000000000000..7bb51ed37891 --- /dev/null +++ b/.github/workflows/apidiff.yaml @@ -0,0 +1,178 @@ +name: API Diff Check + +on: + # SECURITY: Using pull_request_target to support fork PRs with write permissions. + # PR code is checked out but only for static analysis - it is never executed. + # If modifying this workflow, ensure PR code is never executed and user inputs are not used unsafely. + pull_request_target: + types: [opened, synchronize] + paths: + - 'pkg/**/*.go' + - 'rpc/**/*.go' + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + apidiff: + runs-on: ubuntu-24.04 + name: API Diff Check + steps: + # Check if PR has conflicts. When conflicts exist, the merge commit becomes + # frozen at an old state and apidiff cannot run correctly. + - name: Check for merge conflicts + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + # pull_request_target and mergeability are processed asynchronously. + # As a result, it’s possible that we start the check before GitHub has finished calculating the mergeability. + # To handle this, a retry mechanism has been added — it waits for 2 seconds after each attempt. + # If mergeable_state isn’t obtained after 5 attempts, an error is returned. + run: | + MAX=5 + for i in $(seq 1 "$MAX"); do + state=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" --jq .mergeable_state) + echo "mergeable_state=$state" + + if [ "$state" = "dirty" ]; then + echo "::error::This PR has merge conflicts. Please resolve conflicts before running apidiff." + exit 1 + fi + + if [ -n "$state" ] && [ "$state" != "unknown" ] && [ "$state" != "null" ]; then + break + fi + + if [ "$i" -lt "$MAX" ] && { [ -z "$state" ] || [ "$state" = "unknown" ] || [ "$state" = "null" ]; }; then + echo "::error::Could not determine mergeability after $i tries." + exit 1 + fi + + sleep 2 + done + + # Checkout PR merge commit to compare against base branch + # This ensures we compare the actual merge result with the base branch, + # avoiding false positives when PR is not rebased with latest main + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: refs/pull/${{ github.event.pull_request.number }}/merge + + - name: Set up Go + uses: ./.github/actions/setup-go + + # Ensure the base commit exists locally for go-apidiff to compare against. + # Even though we checkout the merge commit, go-apidiff needs the base ref to exist. + # Use base.ref instead of base.sha, since base.sha is outdated (not updated after every commit). + # cf. https://github.com/orgs/community/discussions/59677 + - name: Fetch base commit + id: fetch_base + run: | + set -euo pipefail + BASE_REF="${{ github.event.pull_request.base.ref || github.event.merge_group.base_ref }}" + if [ -z "${BASE_REF:-}" ]; then + echo "::error::BASE_REF is empty (no base ref in event payload)"; exit 1 + fi + + git fetch --depth=1 origin "$BASE_REF" + + BASE_SHA="$(git rev-parse "origin/$BASE_REF")" + if [ -z "${BASE_SHA:-}" ]; then + echo "::error::BASE_SHA is empty (failed to resolve origin/$BASE_REF)"; exit 1 + fi + echo "base_sha=$BASE_SHA" >> "$GITHUB_OUTPUT" + + # NOTE: go-apidiff is not managed in go.mod because installing it via `go get -tool` + # would cause `mage tool:install` to attempt building it on Windows, which currently + # fails due to platform-specific issues. + - name: Run go-apidiff + id: apidiff + continue-on-error: true + uses: joelanford/go-apidiff@60c4206be8f84348ebda2a3e0c3ac9cb54b8f685 # v0.8.3 + with: + base-ref: ${{ steps.fetch_base.outputs.base_sha }} + version: v0.8.3 + + - name: Add apidiff label + if: ${{ steps.apidiff.outputs.semver-type == 'major' }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const label = 'apidiff'; + await github.rest.issues.addLabels({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + labels: [label], + }); + + - name: Comment API diff + if: ${{ steps.apidiff.outputs.semver-type == 'major' }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + APIDIFF_OUTPUT: ${{ steps.apidiff.outputs.output }} + SEMVER_TYPE: ${{ steps.apidiff.outputs.semver-type }} + with: + script: | + const header = '## 📊 API Changes Detected'; + const diff = process.env.APIDIFF_OUTPUT.trim(); + const semver = process.env.SEMVER_TYPE || 'unknown'; + const body = [ + header, + '', + `Semver impact: \`${semver}\``, + '', + '```', + diff, + '```', + ].join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }); + + const existing = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.startsWith(header), + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } + + # Attempt to request the premium reviewers; needs org-scoped token because GITHUB_TOKEN lacks read:org. + - name: Request trivy-premium review + if: ${{ steps.apidiff.outputs.semver-type == 'major' }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.ORG_REPO_TOKEN }} + script: | + try { + await github.rest.pulls.requestReviewers({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + team_reviewers: ['trivy-premium'], + }); + console.log('Requested review from aquasecurity/trivy-premium team'); + } catch (error) { + core.error(`Failed to request trivy-premium reviewers: ${error.message}`); + throw error; + } diff --git a/.github/workflows/auto-close-issue.yaml b/.github/workflows/auto-close-issue.yaml new file mode 100644 index 000000000000..758824d99636 --- /dev/null +++ b/.github/workflows/auto-close-issue.yaml @@ -0,0 +1,46 @@ +name: Auto-close issues + +on: + issues: + types: [opened] + +jobs: + close_issue: + runs-on: ubuntu-latest + steps: + - name: Close issue if user does not have write or admin permissions + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + // Get the issue creator's username + const issueCreator = context.payload.issue.user.login; + + // Check the user's permissions for the repository + const repoPermissions = await github.rest.repos.getCollaboratorPermissionLevel({ + owner: context.repo.owner, + repo: context.repo.repo, + username: issueCreator + }); + + const permission = repoPermissions.data.permission; + + // If the user does not have write or admin permissions, leave a comment and close the issue + if (permission !== 'write' && permission !== 'admin') { + const commentBody = "Please see https://trivy.dev/docs/latest/community/contribute/issue/"; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + body: commentBody + }); + + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.issue.number, + state: 'closed', + state_reason: 'not_planned' + }); + + console.log(`Issue #${context.payload.issue.number} closed because ${issueCreator} does not have sufficient permissions.`); + } \ No newline at end of file diff --git a/.github/workflows/auto-ready-for-review.yaml b/.github/workflows/auto-ready-for-review.yaml new file mode 100644 index 000000000000..44417fe7af5b --- /dev/null +++ b/.github/workflows/auto-ready-for-review.yaml @@ -0,0 +1,138 @@ +name: Auto Ready for Review + +on: + workflow_run: + workflows: ["Test", "Validate PR Title"] + types: [completed] + +jobs: + auto-ready-for-review: + runs-on: ubuntu-24.04 + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Get PR context + id: pr-context + env: + GH_TOKEN: ${{ github.token }} + PR_BRANCH: |- + ${{ + (github.event.workflow_run.head_repository.owner.login != github.event.workflow_run.repository.owner.login) + && format('{0}:{1}', github.event.workflow_run.head_repository.owner.login, github.event.workflow_run.head_branch) + || github.event.workflow_run.head_branch + }} + run: | + echo "[INFO] Searching for PR with branch: ${PR_BRANCH}" + if gh pr view --repo "${{ github.repository }}" "${PR_BRANCH}" --json 'number' --jq '"number=\(.number)"' >> "${GITHUB_OUTPUT}"; then + echo "[INFO] PR found successfully" + else + echo "[INFO] No PR found for branch ${PR_BRANCH}, skipping" + echo "skip=true" >> "${GITHUB_OUTPUT}" + fi + + - name: Check PR and all workflows status + if: steps.pr-context.outputs.skip != 'true' + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const prNumber = ${{ steps.pr-context.outputs.number }}; + console.log(`[INFO] Processing PR #${prNumber}`); + + // Get PR info + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + + console.log(`[INFO] PR #${prNumber} - Draft: ${pr.draft}, Labels: ${pr.labels.map(l => l.name).join(', ')}`); + + // Check if PR has autoready label and is draft + const hasAutoreadyLabel = pr.labels.some(label => label.name === 'autoready'); + + if (!pr.draft) { + console.log(`[INFO] PR #${prNumber} is not draft, skipping`); + return; + } + + if (!hasAutoreadyLabel) { + console.log(`[INFO] PR #${prNumber} doesn't have autoready label, skipping`); + return; + } + + // Get all workflow runs for this PR's head commit (head_sha) + const { data: workflowRuns } = await github.rest.actions.listWorkflowRunsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + head_sha: pr.head.sha, + per_page: 100 + }); + + console.log(`[INFO] Found ${workflowRuns.workflow_runs.length} workflow runs for PR #${prNumber}`); + + // Check workflow status + const runningWorkflows = workflowRuns.workflow_runs.filter(run => + run.status === 'in_progress' || run.status === 'queued' + ); + + const failedWorkflows = workflowRuns.workflow_runs.filter(run => + run.conclusion === 'failure' || run.conclusion === 'cancelled' + ); + + const successfulWorkflows = workflowRuns.workflow_runs.filter(run => + run.conclusion === 'success' + ); + + console.log(`[INFO] Workflow status - Running: ${runningWorkflows.length}, Failed: ${failedWorkflows.length}, Success: ${successfulWorkflows.length}`); + + if (runningWorkflows.length > 0) { + console.log(`[INFO] Some workflows are still running: ${runningWorkflows.map(w => w.name).join(', ')}`); + return; + } + + if (failedWorkflows.length > 0) { + console.log(`[INFO] Some workflows failed: ${failedWorkflows.map(w => w.name).join(', ')}`); + return; + } + + console.log(`[INFO] All workflows passed! Marking PR #${prNumber} as ready for review...`); + + // Mark PR as ready for review using GraphQL API + // Reference: https://github.com/orgs/community/discussions/70061 + try { + const mutation = ` + mutation MarkPullRequestReadyForReview($pullRequestId: ID!) { + markPullRequestReadyForReview(input: { pullRequestId: $pullRequestId }) { + pullRequest { + id + isDraft + number + } + } + } + `; + + const updateResult = await github.graphql(mutation, { + pullRequestId: pr.node_id + }); + + const isDraft = updateResult.markPullRequestReadyForReview.pullRequest.isDraft; + console.log(`[SUCCESS] PR #${prNumber} marked as ready for review. Draft status: ${isDraft}`); + } catch (error) { + console.log(`[ERROR] Failed to mark PR #${prNumber} as ready for review: ${error.message}`); + console.log(`[ERROR] Error details: ${JSON.stringify(error.response?.data || error, null, 2)}`); + return; + } + + // Remove autoready label + try { + const labelResult = await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: 'autoready' + }); + console.log(`[SUCCESS] autoready label removed from PR #${prNumber}. Status: ${labelResult.status}`); + } catch (error) { + console.log(`[WARNING] Could not remove autoready label from PR #${prNumber}: ${error.message}`); + console.log(`[WARNING] Error details: ${JSON.stringify(error.response?.data || error, null, 2)}`); + } \ No newline at end of file diff --git a/.github/workflows/auto-update-labels.yaml b/.github/workflows/auto-update-labels.yaml new file mode 100644 index 000000000000..ecfc834f2a80 --- /dev/null +++ b/.github/workflows/auto-update-labels.yaml @@ -0,0 +1,25 @@ +name: Auto-update labels +on: + push: + paths: + - 'misc/triage/labels.yaml' + branches: + - main +jobs: + deploy: + name: Auto-update labels + runs-on: ubuntu-latest + steps: + - name: Checkout main + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install Go tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: update labels + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: mage label \ No newline at end of file diff --git a/.github/workflows/backport.yaml b/.github/workflows/backport.yaml new file mode 100644 index 000000000000..e5d85a728936 --- /dev/null +++ b/.github/workflows/backport.yaml @@ -0,0 +1,65 @@ +name: Automatic Backporting + +on: + issue_comment: + types: [created] + +jobs: + check_permission: + name: Check comment author permissions + runs-on: ubuntu-latest + outputs: + is_maintainer: ${{ steps.check_permission.outputs.is_maintainer }} + steps: + - name: Check permission + id: check_permission + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PERMISSION=$(gh api /repos/$GITHUB_REPOSITORY/collaborators/$GITHUB_ACTOR/permission --jq '.permission') + if [ "$PERMISSION" == "admin" ] || [ "$PERMISSION" == "write" ]; then + echo "is_maintainer=true" >> $GITHUB_OUTPUT + else + echo "is_maintainer=false" >> $GITHUB_OUTPUT + fi + + + backport: + name: Backport PR + needs: check_permission # run this job after checking permissions + if: | + needs.check_permission.outputs.is_maintainer == 'true' && + github.event.issue.pull_request && + github.event.issue.pull_request.merged_at != null && + startsWith(github.event.comment.body, '@aqua-bot backport release/') + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + + - name: Extract branch name + env: + COMMENT_BODY: ${{ github.event.comment.body }} + run: | + BRANCH_NAME=$(echo "$COMMENT_BODY" | grep -oE '@aqua-bot backport\s+(\S+)' | awk '{print $3}') + if [[ -z "$BRANCH_NAME" || "$BRANCH_NAME" == *".."* || ! "$BRANCH_NAME" =~ ^[A-Za-z0-9._-]+(/[A-Za-z0-9._-]+)*$ ]]; then + echo "Error: Invalid branch name extracted (unsafe characters detected)." >&2 + exit 1 + fi + echo "BRANCH_NAME=$BRANCH_NAME" >> "$GITHUB_ENV" + + - name: Set up Git user + run: | + git config --global user.email "actions@github.com" + git config --global user.name "GitHub Actions" + + - name: Run backport script + env: + # Use ORG_REPO_TOKEN instead of GITHUB_TOKEN + # This allows the created PR to trigger tests and other workflows + GITHUB_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + run: ./misc/backport/backport.sh "$BRANCH_NAME" "$ISSUE_NUMBER" diff --git a/.github/workflows/bypass-cla.yaml b/.github/workflows/bypass-cla.yaml new file mode 100644 index 000000000000..93818d08e41b --- /dev/null +++ b/.github/workflows/bypass-cla.yaml @@ -0,0 +1,12 @@ +# This workflow is used to bypass the required status checks in merge queue. +# cf. https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/troubleshooting-required-status-checks +name: CLA +on: + merge_group: + +jobs: + cla: + name: license/cla + runs-on: ubuntu-latest + steps: + - run: 'echo "No test required"' \ No newline at end of file diff --git a/.github/workflows/bypass-test.yaml b/.github/workflows/bypass-test.yaml new file mode 100644 index 000000000000..93c5ba2869d3 --- /dev/null +++ b/.github/workflows/bypass-test.yaml @@ -0,0 +1,35 @@ +# This workflow is used to bypass the required status checks. +# cf. https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/troubleshooting-required-status-checks +name: Test +on: + push: + paths: + - '**.md' + - 'docs/**' + - 'mkdocs.yml' + - 'LICENSE' + - '.release-please-manifest.json' + - 'helm/trivy/Chart.yaml' + pull_request: + paths: + - '**.md' + - 'docs/**' + - 'mkdocs.yml' + - 'LICENSE' + - '.release-please-manifest.json' + - 'helm/trivy/Chart.yaml' +jobs: + test: + name: Test + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest, windows-latest, macos-latest] + steps: + - run: 'echo "No test required"' + + integration: + name: Integration Test + runs-on: ubuntu-latest + steps: + - run: 'echo "No test required"' \ No newline at end of file diff --git a/.github/workflows/cache-test-assets.yaml b/.github/workflows/cache-test-assets.yaml new file mode 100644 index 000000000000..f427725e9b94 --- /dev/null +++ b/.github/workflows/cache-test-assets.yaml @@ -0,0 +1,89 @@ +name: Cache test assets +# This workflow runs on the main branch to create caches that can be accessed by PRs. +# GitHub Actions cache isolation restricts access: +# - PRs can only restore caches from: current branch, base branch, and default branch (main) +# - PRs cannot restore caches from sibling branches or other PR branches +# - By creating caches on the main branch, all PRs can benefit from shared cache +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + test-images: + name: Cache test images + runs-on: ubuntu-latest + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install Go tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Generate image list digest + id: image-digest + run: | + source integration/testimages.ini + IMAGE_LIST=$(skopeo list-tags docker://$TEST_IMAGES) + DIGEST=$(echo "$IMAGE_LIST" | jq '.Tags += ["containerd"] | .Tags |= sort' | sha256sum | cut -d' ' -f1) + echo "digest=$DIGEST" >> $GITHUB_OUTPUT + + - name: Restore and save test images cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + with: + path: integration/testdata/fixtures/images + key: cache-test-images-${{ steps.image-digest.outputs.digest }} + + - name: Download test images + run: mage test:fixtureContainerImages + + test-vm-images: + name: Cache test VM images + runs-on: ubuntu-latest + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install Go tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Generate image list digest + id: image-digest + run: | + source integration/testimages.ini + IMAGE_LIST=$(skopeo list-tags docker://$TEST_VM_IMAGES) + DIGEST=$(echo "$IMAGE_LIST" | jq '.Tags |= sort' | sha256sum | cut -d' ' -f1) + echo "digest=$DIGEST" >> $GITHUB_OUTPUT + + - name: Restore and save test VM images cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + with: + path: integration/testdata/fixtures/vm-images + key: cache-test-vm-images-${{ steps.image-digest.outputs.digest }} + + - name: Download test VM images + run: mage test:fixtureVMImages + + lint-cache: + name: Cache lint results + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Run golangci-lint for caching + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 + with: + version: v2.4 + args: --verbose + env: + GOEXPERIMENT: jsonv2 diff --git a/.github/workflows/canary.yaml b/.github/workflows/canary.yaml new file mode 100644 index 000000000000..4c2ffa6d757d --- /dev/null +++ b/.github/workflows/canary.yaml @@ -0,0 +1,67 @@ +name: Canary build +on: + push: + branches: + - 'main' + paths: + - '**.go' + - 'go.mod' + - 'Dockerfile.canary' + - '.github/workflows/canary.yaml' + workflow_dispatch: + +jobs: + build-binaries: + name: Build binaries + uses: ./.github/workflows/reusable-release.yaml + with: + goreleaser_config: goreleaser-canary.yml + goreleaser_options: '--snapshot --clean --timeout 60m' # will not release + secrets: inherit + + upload-binaries: + name: Upload binaries + needs: build-binaries # run this job after 'build-binaries' job completes + runs-on: ubuntu-latest + steps: + - name: Restore Trivy binaries from cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + with: + path: dist/ + key: ${{ runner.os }}-bins-${{ github.workflow }}-${{ github.sha }} + + # Upload artifacts + - name: Upload artifacts (trivy_Linux-64bit) + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: trivy_Linux-64bit + path: dist/trivy_*_Linux-64bit.tar.gz + if-no-files-found: error + + - name: Upload artifacts (trivy_Linux-ARM64) + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: trivy_Linux-ARM64 + path: dist/trivy_*_Linux-ARM64.tar.gz + if-no-files-found: error + + - name: Upload artifacts (trivy_macOS-64bit) + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: trivy_macOS-64bit + path: dist/trivy_*_macOS-64bit.tar.gz + if-no-files-found: error + + - name: Upload artifacts (trivy_macOS-ARM64) + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: trivy_macOS-ARM64 + path: dist/trivy_*_macOS-ARM64.tar.gz + if-no-files-found: error + + - name: Delete cache after upload + run: | + gh cache delete "$CACHE_KEY" --repo "${{ github.repository }}" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CACHE_KEY: ${{ runner.os }}-bins-${{ github.workflow }}-${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/mkdocs-dev.yaml b/.github/workflows/mkdocs-dev.yaml new file mode 100644 index 000000000000..9df099e5f6df --- /dev/null +++ b/.github/workflows/mkdocs-dev.yaml @@ -0,0 +1,34 @@ +name: Deploy the dev documentation +on: + push: + paths: + - 'docs/**' + - mkdocs.yml + branches: + - main +jobs: + deploy: + name: Deploy the dev documentation + runs-on: ubuntu-22.04 + steps: + - name: Checkout main + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + persist-credentials: true + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: 3.x + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + pip install git+https://${GH_TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git@9.5.44-insiders-4.53.14 + pip install -r docs/build/requirements.txt + env: + GH_TOKEN: ${{ secrets.MKDOCS_AQUA_BOT }} + - name: Configure the git user + run: | + git config user.name "knqyf263" + git config user.email "knqyf263@gmail.com" + - name: Deploy the dev documents + run: mike deploy --push dev diff --git a/.github/workflows/mkdocs-latest.yaml b/.github/workflows/mkdocs-latest.yaml new file mode 100644 index 000000000000..23319c653774 --- /dev/null +++ b/.github/workflows/mkdocs-latest.yaml @@ -0,0 +1,58 @@ +name: Deploy the latest documentation +on: + workflow_dispatch: + inputs: + version: + description: Version to be deployed + required: true + push: + tags: + - "v*" +jobs: + deploy: + name: Deploy the latest documentation + runs-on: ubuntu-22.04 + steps: + - name: Checkout main + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + persist-credentials: true + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: 3.x + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + pip install git+https://${GH_TOKEN}@github.com/squidfunk/mkdocs-material-insiders.git@9.5.44-insiders-4.53.14 + pip install -r docs/build/requirements.txt + env: + GH_TOKEN: ${{ secrets.MKDOCS_AQUA_BOT }} + - name: Configure the git user + run: | + git config user.name "knqyf263" + git config user.email "knqyf263@gmail.com" + - name: Deploy the latest documents from new tag push + if: ${{ github.event.inputs.version == '' }} + run: | + VERSION=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g") + mike deploy --push --update-aliases ${VERSION%.*} latest + - name: Deploy the latest documents from manual trigger + if: ${{ github.event.inputs.version != '' }} + run: mike deploy --push --update-aliases ${{ github.event.inputs.version }} latest + + # This workflow is used to trigger the trivy-www deployment + trigger-trivy-www-deploy: + needs: deploy + runs-on: ubuntu-22.04 + steps: + - name: Trigger update_version workflow in trivy-telemetry + env: + # Use ORG_REPO_TOKEN instead of GITHUB_TOKEN + # This allows triggering workflows in other repositories + GH_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} + run: | + gh workflow run build-docs.yml \ + --repo ${{ github.repository_owner }}/trivy-www \ + --ref main \ + --field from_version=${{ github.ref_name }} \ No newline at end of file diff --git a/.github/workflows/publish-chart.yaml b/.github/workflows/publish-chart.yaml new file mode 100644 index 000000000000..0d5ce75cdf2d --- /dev/null +++ b/.github/workflows/publish-chart.yaml @@ -0,0 +1,96 @@ + +name: Publish Helm chart + +on: + workflow_dispatch: + pull_request: + types: + - opened + - synchronize + - reopened + - closed + branches: + - main + paths: + - 'helm/trivy/**' +env: + HELM_REP: helm-charts + GH_OWNER: aquasecurity + CHART_DIR: helm/trivy + KIND_VERSION: "v0.14.0" + KIND_IMAGE: "kindest/node:v1.23.6@sha256:b1fa224cc6c7ff32455e0b1fd9cbfd3d3bc87ecaa8fcb06961ed1afb3db0f9ae" +jobs: + # `test-chart` job starts if a PR with Helm Chart is created, merged etc. + test-chart: + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + - name: Install Helm + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4.3.1 + with: + version: v3.14.4 + - name: Set up python + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: '3.x' + check-latest: true + - name: Setup Chart Linting + id: lint + uses: helm/chart-testing-action@6ec842c01de15ebb84c8627d2744a0c2f2755c9f # v2.8.0 + with: + # v6.0.0 resolved the compatibility issue with Python > 3.13. may be removed after the action itself is updated + yamale_version: "6.0.0" + - name: Setup Kubernetes cluster (KIND) + uses: helm/kind-action@92086f6be054225fa813e0a4b13787fc9088faab # v1.13.0 + with: + version: ${{ env.KIND_VERSION }} + image: ${{ env.KIND_IMAGE }} + - name: Run chart-testing + run: ct lint-and-install --validate-maintainers=false --charts helm/trivy + - name: Run chart-testing (Ingress enabled) + run: | + sed -i -e '136s,false,'true',g' ./helm/trivy/values.yaml + ct lint-and-install --validate-maintainers=false --charts helm/trivy + + + # `publish-chart` job starts if a PR with a new Helm Chart is merged or manually + publish-chart: + if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' + needs: + - test-chart + runs-on: ubuntu-24.04 + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + - name: Install chart-releaser + run: | + wget https://github.com/helm/chart-releaser/releases/download/v1.3.0/chart-releaser_1.3.0_linux_amd64.tar.gz + echo "baed2315a9bb799efb71d512c5198a2a3b8dcd139d7f22f878777cffcd649a37 chart-releaser_1.3.0_linux_amd64.tar.gz" | sha256sum -c - + tar xzvf chart-releaser_1.3.0_linux_amd64.tar.gz cr + - name: Package helm chart + run: | + ./cr package ${{ env.CHART_DIR }} + - name: Upload helm chart + # Failed with upload the same version: https://github.com/helm/chart-releaser/issues/101 + continue-on-error: true + run: | + ./cr upload -o ${{ env.GH_OWNER }} -r ${{ env.HELM_REP }} --token ${{ secrets.ORG_REPO_TOKEN }} -p .cr-release-packages + - name: Index helm chart + run: | + ./cr index -o ${{ env.GH_OWNER }} -r ${{ env.HELM_REP }} -c https://${{ env.GH_OWNER }}.github.io/${{ env.HELM_REP }}/ -i index.yaml + - name: Push index file + uses: dmnemec/copy_file_to_another_repo_action@c93037aa10fa8893de271f19978c980d0c1a9b37 #v1.1.1 + env: + API_TOKEN_GITHUB: ${{ secrets.ORG_REPO_TOKEN }} + with: + source_file: 'index.yaml' + destination_repo: '${{ env.GH_OWNER }}/${{ env.HELM_REP }}' + destination_folder: '.' + destination_branch: 'gh-pages' + user_email: aqua-bot@users.noreply.github.com + user_name: 'aqua-bot' diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml new file mode 100644 index 000000000000..bb7525fb6a8e --- /dev/null +++ b/.github/workflows/release-please.yaml @@ -0,0 +1,111 @@ +name: Release Please + +on: + push: + branches: + - main + - 'release/v*' + workflow_dispatch: + inputs: + version: + required: true + description: 'Release version without the "v" prefix (e.g., 0.51.0)' + type: string + +jobs: + release-please: + runs-on: ubuntu-latest + if: ${{ !startsWith(github.event.head_commit.message, 'release:') && !github.event.inputs.version }} + steps: + - name: Release Please + id: release + uses: googleapis/release-please-action@16a9c90856f42705d54a6fda1823352bdc62cf38 # v4.4.0 + with: + token: ${{ secrets.ORG_REPO_TOKEN }} + target-branch: ${{ github.ref_name }} + + manual-release-please: + runs-on: ubuntu-latest + if: ${{ github.event.inputs.version }} + steps: + - name: Install Release Please CLI + run: npm install release-please -g + + - name: Release Please + run: | + release-please release-pr --repo-url=${{ github.server_url }}/${{ github.repository }} \ + --token=${{ secrets.ORG_REPO_TOKEN }} \ + --release-as=${{ github.event.inputs.version }} \ + --target-branch=${{ github.ref_name }} + + release-tag: + runs-on: ubuntu-latest + if: ${{ startsWith(github.event.head_commit.message, 'release:') }} + steps: + # Since skip-github-release is specified, the outputs of googleapis/release-please-action cannot be used. + # Therefore, we need to parse the version ourselves. + - name: Extract version and PR number from commit message + id: extract_info + shell: bash + env: + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + run: | + echo "version=$( echo "$COMMIT_MESSAGE" | sed 's/^release: v\([0-9]\+\.[0-9]\+\.[0-9]\+\).*$/\1/' )" >> $GITHUB_OUTPUT + echo "pr_number=$( echo "$COMMIT_MESSAGE" | sed 's/.*(\#\([0-9]\+\)).*$/\1/' )" >> $GITHUB_OUTPUT + echo "release_branch=release/v$( echo "$COMMIT_MESSAGE" | sed 's/^release: v\([0-9]\+\.[0-9]\+\).*$/\1/' )" >> $GITHUB_OUTPUT + + - name: Tag release + if: ${{ steps.extract_info.outputs.version }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.ORG_REPO_TOKEN }} # To trigger another workflow + script: | + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/tags/v${{ steps.extract_info.outputs.version }}`, + sha: context.sha + }); + + # When v0.50.0 is released, a release branch "release/v0.50" is created. + - name: Create release branch for patch versions + if: ${{ endsWith(steps.extract_info.outputs.version, '.0') }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} # Should not trigger the workflow again + script: | + const releaseBranch = '${{ steps.extract_info.outputs.release_branch }}'; + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/heads/${releaseBranch}`, + sha: context.sha + }); + + + # Add release branch to rulesets to enable merge queue + - name: Add release branch to rulesets + if: ${{ endsWith(steps.extract_info.outputs.version, '.0') }} + env: + GITHUB_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} + shell: bash + run: | + RULESET_ID=$(gh api /repos/${{ github.repository }}/rulesets --jq '.[] | select(.name=="release") | .id') + gh api /repos/${{ github.repository }}/rulesets/$RULESET_ID | jq '{conditions}' | jq '.conditions.ref_name.include += ["refs/heads/${{ steps.extract_info.outputs.release_branch }}"]' | gh api --method put --input - /repos/${{ github.repository }}/rulesets/$RULESET_ID + + # Since skip-github-release is specified, googleapis/release-please-action doesn't delete the label from PR. + # This label prevents the subsequent PRs from being created. Therefore, we need to delete it ourselves. + # cf. https://github.com/googleapis/release-please?tab=readme-ov-file#release-please-bot-does-not-create-a-release-pr-why + - name: Remove the label from PR + if: ${{ steps.extract_info.outputs.pr_number }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prNumber = parseInt('${{ steps.extract_info.outputs.pr_number }}', 10); + github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + name: 'autorelease: pending' + }); diff --git a/.github/workflows/release-pr-check.yaml b/.github/workflows/release-pr-check.yaml new file mode 100644 index 000000000000..fe4909cc3fac --- /dev/null +++ b/.github/workflows/release-pr-check.yaml @@ -0,0 +1,21 @@ +name: Backport PR Check + +on: + pull_request: + branches: + - 'release/v*' + +jobs: + check-pr-author: + runs-on: ubuntu-latest + steps: + - name: Check PR author + id: check_author + env: + PR_AUTHOR: ${{ github.event.pull_request.user.login }} + run: | + if [ "$PR_AUTHOR" != "aqua-bot" ]; then + echo "::error::This branch is intended for automated backporting by bot. Please refer to the documentation:" + echo "::error::https://trivy.dev/docs/latest/community/maintainer/backporting/" + exit 1 + fi \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000000..d39d81a73049 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,124 @@ +name: Release +on: + push: + tags: + - "v*" + +jobs: + release: + name: Release + uses: ./.github/workflows/reusable-release.yaml + with: + goreleaser_config: goreleaser.yml + goreleaser_options: '--clean --timeout 90m' + secrets: inherit + + deploy-packages: + name: Deploy rpm/dep packages + needs: release # run this job after 'release' job completes + runs-on: ubuntu-22.04 + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + + - name: Restore Trivy binaries from cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + with: + path: dist/ + key: ${{ runner.os }}-bins-${{github.workflow}}-${{github.sha}} + + - name: Install dependencies + run: | + sudo apt-get -y update + sudo apt-get -y install rpm reprepro createrepo-c distro-info + + - name: Checkout trivy-repo + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + repository: ${{ github.repository_owner }}/trivy-repo + path: trivy-repo + token: ${{ secrets.ORG_REPO_TOKEN }} + + - name: Setup git settings + run: | + git config --global user.email "knqyf263@gmail.com" + git config --global user.name "Teppei Fukuda" + + - name: Create rpm repository + run: ci/deploy-rpm.sh + + - name: Import GPG key + run: echo -e "${{ secrets.GPG_KEY }}" | gpg --import + + - name: Create deb repository + run: ci/deploy-deb.sh + + # `update-chart-version` creates a new PR for updating the helm chart + update-chart-version: + needs: deploy-packages + runs-on: ubuntu-22.04 + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + + - name: Set up Git user + run: | + git config --global user.email "actions@github.com" + git config --global user.name "GitHub Actions" + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install Go tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Create a PR with Trivy version + run: mage helm:updateVersion + env: + # Use ORG_REPO_TOKEN instead of GITHUB_TOKEN + # This allows the created PR to trigger tests and other workflows + GITHUB_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} + + # `trigger-version-update` triggers the `update_version` workflow in the `trivy-telemetry` repository + # and the trivy-downloads repository. + trigger-version-update: + needs: deploy-packages + runs-on: ubuntu-22.04 + steps: + - name: Trigger update_version workflow in trivy-telemetry + env: + # Use ORG_REPO_TOKEN instead of GITHUB_TOKEN + # This allows triggering workflows in other repositories + GH_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} + run: | + gh workflow run update_version.yml \ + --repo ${{ github.repository_owner }}/trivy-telemetry \ + --ref main \ + --field version=${{ github.ref_name }} + + - name: Trigger update_version workflow in trivy-downloads + env: + # Use ORG_REPO_TOKEN instead of GITHUB_TOKEN + # This allows triggering workflows in other repositories + GH_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} + run: | + gh workflow run update_version.yml \ + --repo ${{ github.repository_owner }}/trivy-downloads \ + --ref main \ + --field version=${{ github.ref_name }} \ + --field artifact=trivy + + - name: Trigger version update and release workflow in trivy-chocolatey + env: + # Use ORG_REPO_TOKEN instead of GITHUB_TOKEN + # This allows triggering workflows in other repositories + GH_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} + run: | + gh workflow run release.yml \ + --repo ${{ github.repository_owner }}/trivy-chocolatey \ + --ref main \ + --field version=${{ github.ref_name }} diff --git a/.github/workflows/reusable-release.yaml b/.github/workflows/reusable-release.yaml new file mode 100644 index 000000000000..96a185177745 --- /dev/null +++ b/.github/workflows/reusable-release.yaml @@ -0,0 +1,125 @@ +name: Reusable release +on: + workflow_call: + inputs: + goreleaser_config: + description: 'file path to GoReleaser config' + required: true + type: string + goreleaser_options: + description: 'GoReleaser options separated by spaces' + default: '' + required: false + type: string + +env: + GH_USER: "aqua-bot" + +jobs: + release: + name: Release + runs-on: ubuntu-latest-m + env: + DOCKER_CLI_EXPERIMENTAL: "enabled" + permissions: + id-token: write # For cosign + packages: write # For GHCR + contents: read # Not required for public repositories, but for clarity + steps: + - name: Cosign install + uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 + + - name: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + + - name: Show available Docker Buildx platforms + run: echo ${{ steps.buildx.outputs.platforms }} + + - name: Login to docker.io registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to ghcr.io registry + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: ghcr.io + username: ${{ env.GH_USER }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ECR + uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + with: + registry: public.ecr.aws + username: ${{ secrets.ECR_ACCESS_KEY_ID }} + password: ${{ secrets.ECR_SECRET_ACCESS_KEY }} + + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + + - name: Setup Go + uses: ./.github/actions/setup-go + + - name: Generate SBOM + uses: CycloneDX/gh-gomod-generate-sbom@efc74245d6802c8cefd925620515442756c70d8f # v2.0.0 + with: + args: mod -licenses -json -output bom.json + version: ^v1 + + - name: "save gpg key" + env: + GPG_KEY: ${{ secrets.GPG_KEY }} + run: | + echo "$GPG_KEY" > gpg.key + + # Create tmp dir for GoReleaser + - name: "create tmp dir" + run: | + mkdir tmp + + - name: GoReleaser + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + version: v2.1.0 + args: release -f=${{ inputs.goreleaser_config}} ${{ inputs.goreleaser_options}} + env: + GITHUB_TOKEN: ${{ secrets.ORG_REPO_TOKEN }} + NFPM_DEFAULT_RPM_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + GPG_FILE: "gpg.key" + TMPDIR: "tmp" + + - name: "remove gpg key" + run: | + rm gpg.key + + # Push images to registries (only for canary build) + # The custom Dockerfile.canary is necessary + # because GoReleaser Free doesn't support pushing images with the `--snapshot` flag. + - name: Build and push + if: ${{ inputs.goreleaser_config == 'goreleaser-canary.yml' }} + uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 + with: + platforms: linux/amd64, linux/arm64 + file: ./Dockerfile.canary # path to Dockerfile + context: . + push: true + tags: | + aquasec/trivy:canary + ghcr.io/aquasecurity/trivy:canary + public.ecr.aws/aquasecurity/trivy:canary + + - name: Cache Trivy binaries + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + with: + path: dist/ + # use 'github.sha' to create a unique cache folder for each run. + # use 'github.workflow' to create a unique cache folder if some runs have same commit sha. + # e.g. build and release runs + key: ${{ runner.os }}-bins-${{github.workflow}}-${{github.sha}} diff --git a/.github/workflows/roadmap.yaml b/.github/workflows/roadmap.yaml new file mode 100644 index 000000000000..8f274c14da36 --- /dev/null +++ b/.github/workflows/roadmap.yaml @@ -0,0 +1,79 @@ +name: Add issues to the roadmap project + +on: + issues: + types: + - labeled + +jobs: + add-issue-to-roadmap-project: + name: Add issue to the roadmap project + runs-on: ubuntu-latest + steps: + # 'kind/feature' AND 'priority/backlog' labels -> 'Backlog' column + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + with: + project-url: https://github.com/orgs/aquasecurity/projects/25 + github-token: ${{ secrets.ORG_PROJECT_TOKEN }} + labeled: kind/feature, priority/backlog + label-operator: AND + id: add-backlog-issue + - uses: titoportas/update-project-fields@421a54430b3cdc9eefd8f14f9ce0142ab7678751 # v0.1.0 + if: ${{ steps.add-backlog-issue.outputs.itemId }} + with: + project-url: https://github.com/orgs/aquasecurity/projects/25 + github-token: ${{ secrets.ORG_PROJECT_TOKEN }} + item-id: ${{ steps.add-backlog-issue.outputs.itemId }} # Use the item-id output of the previous step + field-keys: Priority + field-values: Backlog + + # 'kind/feature' AND 'priority/important-longterm' labels -> 'Important (long-term)' column + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + with: + project-url: https://github.com/orgs/aquasecurity/projects/25 + github-token: ${{ secrets.ORG_PROJECT_TOKEN }} + labeled: kind/feature, priority/important-longterm + label-operator: AND + id: add-longterm-issue + - uses: titoportas/update-project-fields@421a54430b3cdc9eefd8f14f9ce0142ab7678751 # v0.1.0 + if: ${{ steps.add-longterm-issue.outputs.itemId }} + with: + project-url: https://github.com/orgs/aquasecurity/projects/25 + github-token: ${{ secrets.ORG_PROJECT_TOKEN }} + item-id: ${{ steps.add-longterm-issue.outputs.itemId }} # Use the item-id output of the previous step + field-keys: Priority + field-values: Important (long-term) + + # 'kind/feature' AND 'priority/important-soon' labels -> 'Important (soon)' column + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + with: + project-url: https://github.com/orgs/aquasecurity/projects/25 + github-token: ${{ secrets.ORG_PROJECT_TOKEN }} + labeled: kind/feature, priority/important-soon + label-operator: AND + id: add-soon-issue + - uses: titoportas/update-project-fields@421a54430b3cdc9eefd8f14f9ce0142ab7678751 # v0.1.0 + if: ${{ steps.add-soon-issue.outputs.itemId }} + with: + project-url: https://github.com/orgs/aquasecurity/projects/25 + github-token: ${{ secrets.ORG_PROJECT_TOKEN }} + item-id: ${{ steps.add-soon-issue.outputs.itemId }} # Use the item-id output of the previous step + field-keys: Priority + field-values: Important (soon) + + # 'kind/feature' AND 'priority/critical-urgent' labels -> 'Urgent' column + - uses: actions/add-to-project@244f685bbc3b7adfa8466e08b698b5577571133e # v1.0.2 + with: + project-url: https://github.com/orgs/aquasecurity/projects/25 + github-token: ${{ secrets.ORG_PROJECT_TOKEN }} + labeled: kind/feature, priority/critical-urgent + label-operator: AND + id: add-urgent-issue + - uses: titoportas/update-project-fields@421a54430b3cdc9eefd8f14f9ce0142ab7678751 # v0.1.0 + if: ${{ steps.add-urgent-issue.outputs.itemId }} + with: + project-url: https://github.com/orgs/aquasecurity/projects/25 + github-token: ${{ secrets.ORG_PROJECT_TOKEN }} + item-id: ${{ steps.add-urgent-issue.outputs.itemId }} # Use the item-id output of the previous step + field-keys: Priority + field-values: Urgent \ No newline at end of file diff --git a/.github/workflows/scan.yaml b/.github/workflows/scan.yaml new file mode 100644 index 000000000000..1473d8cb5028 --- /dev/null +++ b/.github/workflows/scan.yaml @@ -0,0 +1,23 @@ +name: Scan vulnerabilities +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +jobs: + build: + name: Scan Go vulnerabilities + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Run Trivy vulnerability scanner and create GitHub issues + uses: knqyf263/trivy-issue-action@4466f52d1401b66dd2a2ab9e0c40cddc021829ec # v0.0.6 + with: + assignee: knqyf263 + severity: CRITICAL + skip-dirs: integration,examples,pkg + label: kind/security + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/semantic-pr.yaml b/.github/workflows/semantic-pr.yaml new file mode 100644 index 000000000000..ba66d38b03fb --- /dev/null +++ b/.github/workflows/semantic-pr.yaml @@ -0,0 +1,168 @@ +name: "Validate PR Title" + +on: + pull_request: + types: + - opened + - edited + - synchronize + +jobs: + validate: + name: Validate PR title + runs-on: ubuntu-latest + steps: + - name: Validate PR title + shell: bash + env: + PR_TITLE: ${{ github.event.pull_request.title }} + # Valid types + VALID_TYPES: | + feat + fix + docs + style + refactor + perf + test + build + ci + chore + revert + release + # Valid scopes categorized by area + VALID_SCOPES: | + # Scanners + vuln + misconf + secret + license + + # Targets + image + fs + repo + sbom + server + k8s + aws + vm + plugin + + # OS + activestate + alpine + wolfi + chainguard + redhat + alma + rocky + mariner + oracle + debian + ubuntu + amazon + suse + photon + echo + distroless + windows + minimos + rootio + seal + + # Languages + ruby + php + python + nodejs + rust + dotnet + java + go + c + c++ + elixir + dart + swift + bitnami + conda + julia + + # Package types + os + lang + + # IaC + kubernetes + dockerfile + terraform + cloudformation + + # Container + docker + podman + containerd + oci + + # SBOM + sbom + spdx + cyclonedx + + # Misc + cli + flag + purl + vex + helm + report + db + parser + deps + run: | + set -euo pipefail + + # Convert env vars to regex alternatives, excluding comments and empty lines + TYPES_REGEX=$(echo "$VALID_TYPES" | grep -v '^$' | paste -sd '|') + SCOPES_REGEX=$(echo "$VALID_SCOPES" | grep -v '^$' | grep -v '^#' | paste -sd '|') + + # Basic format check (should match: type(scope): description or type: description) + FORMAT_REGEX="^[a-z]+(\([a-z0-9+]+\))?!?: .+$" + if ! echo "$PR_TITLE" | grep -qE "$FORMAT_REGEX"; then + echo "Error: Invalid PR title format" + echo "Expected format: (): or : " + echo "Examples:" + echo " feat(vuln): add new vulnerability detection" + echo " fix: correct parsing logic" + echo " docs(kubernetes): update installation guide" + echo -e "\nCurrent title: $PR_TITLE" + exit 1 + fi + + # Extract type and scope for validation + TYPE=$(echo "$PR_TITLE" | sed -E 's/^([a-z]+)(\([a-z0-9+]+\))?!?: .+$/\1/') + SCOPE=$(echo "$PR_TITLE" | sed -E 's/^[a-z]+\(([a-z0-9+]+)\)!?: .+$/\1/; t; s/.*//') + + # Validate type + if ! echo "$VALID_TYPES" | grep -qx "$TYPE"; then + echo "Error: Invalid type '${TYPE}'" + echo -e "\nValid types:" + echo "$VALID_TYPES" | grep -v '^$' | sed 's/^/- /' + echo -e "\nCurrent title: $PR_TITLE" + exit 1 + fi + + # Validate scope if present + if [ -n "$SCOPE" ]; then + if ! echo "$VALID_SCOPES" | grep -v '^#' | grep -qx "$SCOPE"; then + echo "Error: Invalid scope '${SCOPE}'" + echo -e "\nValid scopes:" + echo "$VALID_SCOPES" | grep -v '^$' | grep -v '^#' | sed 's/^/- /' + echo -e "\nCurrent title: $PR_TITLE" + exit 1 + fi + fi + + echo "PR title validation passed ✅" + echo "Current title: $PR_TITLE" diff --git a/.github/workflows/spdx-cron.yaml b/.github/workflows/spdx-cron.yaml new file mode 100644 index 000000000000..e4fd8a1d0edb --- /dev/null +++ b/.github/workflows/spdx-cron.yaml @@ -0,0 +1,37 @@ +name: SPDX licenses cron +on: + schedule: + - cron: '0 0 * * 0' # every Sunday at 00:00 + workflow_dispatch: + +jobs: + build: + name: Check if SPDX exceptions + runs-on: ubuntu-24.04 + steps: + - name: Check out code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install Go tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Check if SPDX license IDs and exceptions are up-to-date + id: exceptions_check + run: | + mage spdx:updateLicenseEntries + if [ -n "$(git status --porcelain)" ]; then + echo "Run 'mage spdx:updateLicenseEntries' and push it" + echo "send_notify=true" >> $GITHUB_OUTPUT + fi + + - name: Microsoft Teams Notification + uses: Skitionek/notify-microsoft-teams@e7a2493ac87dad8aa7a62f079f295e54ff511d88 # main + if: steps.exceptions_check.outputs.send_notify == 'true' + with: + webhook_url: ${{ secrets.TRIVY_MSTEAMS_WEBHOOK }} + needs: ${{ toJson(needs) }} + job: ${{ toJson(job) }} + steps: ${{ toJson(steps) }} \ No newline at end of file diff --git a/.github/workflows/stale-issues.yaml b/.github/workflows/stale-issues.yaml new file mode 100644 index 000000000000..a09b5a7f0464 --- /dev/null +++ b/.github/workflows/stale-issues.yaml @@ -0,0 +1,19 @@ +name: "Stale PR's" +on: + schedule: + - cron: '0 0 * * *' +jobs: + stale: + timeout-minutes: 1 + runs-on: ubuntu-latest + steps: + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-pr-message: 'This PR is stale because it has been labeled with inactivity.' + exempt-pr-labels: 'lifecycle/active' + stale-pr-label: 'lifecycle/stale' + days-before-stale: 60 + days-before-issue-stale: '-1' + days-before-close: 20 + days-before-issue-close: '-1' diff --git a/.github/workflows/test-docs.yaml b/.github/workflows/test-docs.yaml new file mode 100644 index 000000000000..9ef46caf3621 --- /dev/null +++ b/.github/workflows/test-docs.yaml @@ -0,0 +1,29 @@ +name: Test docs +on: + pull_request: + paths: + - 'docs/**' + - 'mkdocs.yml' +jobs: + build-documents: + name: Documentation Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + fetch-depth: 0 + persist-credentials: true + - uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: 3.x + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools wheel + pip install -r docs/build/requirements.txt + - name: Configure the git user + run: | + git config user.name "knqyf263" + git config user.email "knqyf263@gmail.com" + - name: Deploy the dev documents + run: mike deploy test diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 000000000000..c5f32c9c4f8b --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,235 @@ +name: Test +on: + pull_request: + paths-ignore: + - '**.md' + - 'docs/**' + - 'mkdocs.yml' + - 'LICENSE' + - '.release-please-manifest.json' ## don't run tests for release-please PRs + - 'helm/trivy/Chart.yaml' + merge_group: + workflow_dispatch: + +jobs: + test: + name: Test + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: go mod tidy + run: | + go mod tidy + if [ -n "$(git status --porcelain)" ]; then + echo "Run 'go mod tidy' and push it" + exit 1 + fi + if: matrix.operating-system == 'ubuntu-latest' + + - name: Lint + id: lint + uses: golangci/golangci-lint-action@1e7e51e771db61008b38414a730f564565cf7c20 # v9.2.0 + with: + version: v2.4 + args: --verbose + skip-save-cache: true # Restore cache from main branch but don't save new cache + env: + GOEXPERIMENT: jsonv2 + if: matrix.operating-system == 'ubuntu-latest' + + - name: Check if linter failed + run: | + echo "Linter failed, running 'mage lint:fix' might help to correct some errors" + exit 1 + if: ${{ failure() && steps.lint.conclusion == 'failure' }} + + - name: Install tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Check if CLI references are up-to-date + run: | + mage docs:generate + if [ -n "$(git status --porcelain)" ]; then + echo "Run 'mage docs:generate' and push it" + exit 1 + fi + if: matrix.operating-system == 'ubuntu-latest' + + - name: Run unit tests + run: mage test:unit + + integration: + name: Integration Test + runs-on: ubuntu-latest + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install Go tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Generate image list digest + id: image-digest + run: | + source integration/testimages.ini + IMAGE_LIST=$(skopeo list-tags docker://$TEST_IMAGES) + DIGEST=$(echo "$IMAGE_LIST" | jq '.Tags += ["containerd"] | .Tags |= sort' | sha256sum | cut -d' ' -f1) + echo "digest=$DIGEST" >> $GITHUB_OUTPUT + + - name: Restore test images from cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + with: + path: integration/testdata/fixtures/images + key: cache-test-images-${{ steps.image-digest.outputs.digest }} + + - name: Run integration tests + run: mage test:integration + + k8s-integration: + name: K8s Integration Test + runs-on: ubuntu-latest + steps: + - name: Check out code into the Go module directory + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install Go tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Run k8s integration tests + run: mage test:k8s + + module-test: + name: Module Integration Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Generate image list digest + id: image-digest + run: | + source integration/testimages.ini + IMAGE_LIST=$(skopeo list-tags docker://$TEST_IMAGES) + DIGEST=$(echo "$IMAGE_LIST" | jq '.Tags += ["containerd"] | .Tags |= sort' | sha256sum | cut -d' ' -f1) + echo "digest=$DIGEST" >> $GITHUB_OUTPUT + + - name: Restore test images from cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + with: + path: integration/testdata/fixtures/images + key: cache-test-images-${{ steps.image-digest.outputs.digest }} + + - name: Run module integration tests + shell: bash + run: | + mage test:module + + vm-test: + name: VM Integration Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install Go tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Generate image list digest + id: image-digest + run: | + source integration/testimages.ini + IMAGE_LIST=$(skopeo list-tags docker://$TEST_VM_IMAGES) + DIGEST=$(echo "$IMAGE_LIST" | jq '.Tags |= sort' | sha256sum | cut -d' ' -f1) + echo "digest=$DIGEST" >> $GITHUB_OUTPUT + + - name: Restore test VM images from cache + uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 + with: + path: integration/testdata/fixtures/vm-images + key: cache-test-vm-images-${{ steps.image-digest.outputs.digest }} + + - name: Run vm integration tests + run: | + mage test:vm + + e2e-test: + name: E2E Test + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Install Go tools + run: go install tool # GOBIN is added to the PATH by the setup-go action + + - name: Run E2E tests + run: mage test:e2e + + build-test: + name: Build Test + runs-on: ${{ matrix.operating-system }} + strategy: + matrix: + operating-system: [ubuntu-latest, windows-latest, macos-latest] + env: + DOCKER_CLI_EXPERIMENTAL: "enabled" + steps: + # The go-build (GOCACHE env) directory requires a large amount of free disk space. + - name: Free up disk space + if: matrix.operating-system == 'ubuntu-latest' + run: | + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force + df -h + + - name: Checkout + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Set up Go + uses: ./.github/actions/setup-go + + - name: Determine GoReleaser ID + id: goreleaser_id + shell: bash + run: | + if [ "${{ matrix.operating-system }}" == "windows-latest" ]; then + echo "id=--id build-windows" >> $GITHUB_OUTPUT + elif [ "${{ matrix.operating-system }}" == "macos-latest" ]; then + echo "id=--id build-macos --id build-bsd" >> $GITHUB_OUTPUT + else + echo "id=--id build-linux" >> $GITHUB_OUTPUT + fi + + - name: Run GoReleaser + uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 + with: + version: v2.1.0 + args: build --snapshot --clean --timeout 90m ${{ steps.goreleaser_id.outputs.id }} diff --git a/.github/workflows/triage.yaml b/.github/workflows/triage.yaml new file mode 100644 index 000000000000..0c9812b513b3 --- /dev/null +++ b/.github/workflows/triage.yaml @@ -0,0 +1,16 @@ +name: Triage Discussion +on: + discussion: + types: [created] + workflow_dispatch: + inputs: + discussion_num: + required: true +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + - uses: ./.github/actions/trivy-triage + with: + discussion_num: ${{ github.event.inputs.discussion_num }} diff --git a/.gitignore b/.gitignore index 97cca67c6785..a2fc00ad08b6 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,10 @@ *.dll *.so *.dylib +/trivy + +## chart release +.cr-release-packages # Test binary, build with `go test -c` *.test @@ -12,3 +16,30 @@ *.out .idea +.vscode + +# Directory Cache Files +.DS_Store +thumbs.db + +# test fixtures +coverage.txt +integration/testdata/fixtures/images +integration/testdata/fixtures/vm-images +internal/gittest/testdata/test-repo + +# SBOMs generated during CI +/bom.json + +# goreleaser output +dist + +# WebAssembly +*.wasm + +# Signing +gpg.key +cmd/trivy/trivy + +# RPM +*.rpm diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 000000000000..944e6ad6b916 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,222 @@ +issues: + max-issues-per-linter: 0 + max-same-issues: 0 + +linters: + settings: + depguard: + rules: + main: + list-mode: lax + deny: + # Cannot use gomodguard, which examines go.mod, as "golang.org/x/exp/slices" is not a module and doesn't appear in go.mod. + - pkg: "golang.org/x/exp/slices" + desc: "Use 'slices' instead" + - pkg: "golang.org/x/exp/maps" + desc: "Use 'maps' or 'github.com/samber/lo' instead" + - pkg: "io/ioutil" + desc: "io/ioutil is deprecated. Use 'io' or 'os' instead" + dupl: + threshold: 100 + errcheck: + check-type-assertions: true + check-blank: true + goconst: + min-len: 3 + min-occurrences: 3 + gocritic: + disabled-checks: + - appendAssign + - commentedOutCode + - hugeParam + - importShadow # FIXME + - indexAlloc + - rangeValCopy + - regexpSimplify + - sloppyReassign + - unnamedResult + - whyNoLint + enabled-tags: + - diagnostic + - style + - performance + - experimental + - opinionated + settings: + ruleguard: + failOn: all + rules: "${base-path}/misc/lint/rules.go" + gocyclo: + # Next highest complexity candidate: ApplyLayers in pkg/fanal/applier/docker.go (score: 28) + min-complexity: 20 + gomodguard: + blocked: + modules: + - github.com/hashicorp/go-version: + recommendations: + - github.com/aquasecurity/go-version + reason: "`aquasecurity/go-version` is designed for our use-cases" + - github.com/Masterminds/semver: + recommendations: + - github.com/aquasecurity/go-version + reason: "`aquasecurity/go-version` is designed for our use-cases" + - github.com/liamg/memoryfs: + recommendations: + - github.com/aquasecurity/trivy/pkg/mapfs + gosec: + excludes: + - G101 + - G114 + - G115 + - G204 + - G304 + - G402 + govet: + disable: + - shadow + misspell: + locale: US + ignore-rules: + - behaviour + - licence + - optimise + - simmilar + perfsprint: + # Optimizes even if it requires an int or uint type cast. + int-conversion: true + # Optimizes into `err.Error()` even if it is only equivalent for non-nil errors. + err-error: true + # Optimizes `fmt.Errorf`. + errorf: true + # Optimizes `fmt.Sprintf` with only one argument. + sprintf1: false + # Optimizes into strings concatenation. + strconcat: false + revive: + max-open-files: 2048 + # https://github.com/mgechev/revive/blob/HEAD/RULES_DESCRIPTIONS.md + rules: + - name: bool-literal-in-expr + - name: context-as-argument + arguments: + - allowTypesBefore: "*testing.T" + - name: duplicated-imports + - name: early-return + arguments: + - preserve-scope + - name: if-return + - name: increment-decrement + - name: indent-error-flow + arguments: + - preserve-scope + - name: range + - name: range-val-address + - name: superfluous-else + arguments: + - preserve-scope + - name: time-equal + - name: unnecessary-stmt + - name: unused-parameter + - name: use-any + + staticcheck: + checks: + - all + - -QF1008 # Omit embedded fields from selector expression + - -S1007 # Simplify regular expression by using raw string literal + - -S1011 # Use a single append to concatenate two slices + - -S1023 # Omit redundant control flow + - -SA1019 # Using a deprecated function, variable, constant or field + - -SA1024 # A string cutset contains duplicate characters + - -SA4004 # The loop exits unconditionally after one iteration + - -SA4023 # Impossible comparison of interface value with untyped nil + - -SA4032 # Comparing runtime.GOOS or runtime.GOARCH against impossible value + - -SA5011 # Possible nil pointer dereference + - -ST1003 # Poorly chosen identifier + - -ST1012 # Poorly chosen name for error variable + + testifylint: + enable-all: true + + default: none + + enable: + - bodyclose + - depguard + - goconst + - gocritic + - gocyclo + - gomodguard + - gosec + - govet + - ineffassign + - misspell + - perfsprint + - revive + - staticcheck + - testifylint + - unconvert + - unused + - usestdlibvars + - usetesting + + exclusions: + generated: lax + paths: + - "pkg/iac/scanners/terraform/parser/funcs" # copies of Terraform functions + rules: + - path: ".*_test.go$" + linters: + - goconst + - gosec + - unused + - path: ".*_test.go$" + linters: + - govet + text: "copylocks:" + - path: ".*_test.go$" + linters: + - gocritic + text: "commentFormatting:" + - path: ".*_test.go$" + linters: + - gocritic + text: "exitAfterDefer:" + - path: ".*_test.go$" + linters: + - gocritic + text: "importShadow:" + - linters: + - goconst + text: "string `each` has 3 occurrences, make it a constant" # FIXME + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + warn-unused: true + +run: + go: "1.25" + timeout: 30m + +formatters: + enable: + - gci + - gofmt + + exclusions: + generated: lax + + settings: + gci: + sections: + - standard + - default + - prefix(github.com/aquasecurity/) + - blank + - dot + gofmt: + simplify: false + +version: "2" diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000000..fa58225ccb8e --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1 @@ +{".":"0.69.0"} diff --git a/.vex/oci.openvex.json b/.vex/oci.openvex.json new file mode 100644 index 000000000000..f1ec8a32df48 --- /dev/null +++ b/.vex/oci.openvex.json @@ -0,0 +1,244 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-8e30ed756ae8e4196af93bf43edf68360f396a98c0268787453a3443b26e7d6c", + "author": "Aqua Security", + "timestamp": "2024-07-10T12:17:44.60495+04:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2023-42363" + }, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=index.docker.io%2Faquasec%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=public.ecr.aws%2Faquasecurity%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_cannot_be_controlled_by_adversary", + "impact_statement": "awk is not used" + }, + { + "vulnerability": { + "name": "CVE-2023-42364" + }, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=index.docker.io%2Faquasec%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=public.ecr.aws%2Faquasecurity%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_cannot_be_controlled_by_adversary", + "impact_statement": "awk is not used" + }, + { + "vulnerability": { + "name": "CVE-2023-42365" + }, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=index.docker.io%2Faquasec%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=public.ecr.aws%2Faquasecurity%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_cannot_be_controlled_by_adversary", + "impact_statement": "awk is not used" + }, + { + "vulnerability": { + "name": "CVE-2023-42366" + }, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=index.docker.io%2Faquasec%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=public.ecr.aws%2Faquasecurity%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_cannot_be_controlled_by_adversary", + "impact_statement": "awk is not used" + }, + { + "vulnerability": { + "name": "CVE-2024-4741" + }, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=index.docker.io%2Faquasec%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/libcrypto3"}, + {"@id": "pkg:apk/alpine/libssl3"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=public.ecr.aws%2Faquasecurity%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/libcrypto3"}, + {"@id": "pkg:apk/alpine/libssl3"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/libcrypto3"}, + {"@id": "pkg:apk/alpine/libssl3"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_cannot_be_controlled_by_adversary", + "impact_statement": "openssl is not used" + }, + { + "vulnerability": { + "name": "CVE-2024-5535" + }, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=index.docker.io%2Faquasec%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/libcrypto3"}, + {"@id": "pkg:apk/alpine/libssl3"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=public.ecr.aws%2Faquasecurity%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/libcrypto3"}, + {"@id": "pkg:apk/alpine/libssl3"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/libcrypto3"}, + {"@id": "pkg:apk/alpine/libssl3"}, + {"@id": "pkg:apk/alpine/ssl_client"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_cannot_be_controlled_by_adversary", + "impact_statement": "openssl is not used" + }, + { + "vulnerability": { + "name": "CVE-2024-6119" + }, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=index.docker.io%2Faquasec%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/libcrypto3"}, + {"@id": "pkg:apk/alpine/libssl3"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=public.ecr.aws%2Faquasecurity%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/libcrypto3"}, + {"@id": "pkg:apk/alpine/libssl3"} + ] + }, + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/libcrypto3"}, + {"@id": "pkg:apk/alpine/libssl3"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_cannot_be_controlled_by_adversary", + "impact_statement": "openssl is not used" + } + ] +} diff --git a/.vex/trivy.openvex.json b/.vex/trivy.openvex.json new file mode 100644 index 000000000000..a7d05cf8a530 --- /dev/null +++ b/.vex/trivy.openvex.json @@ -0,0 +1,634 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "aquasecurity/trivy:613fd55abbc2857b5ca28b07a26f3cd4c8b0ddc4c8a97c57497a2d4c4880d7fc", + "author": "Aqua Security", + "timestamp": "2024-07-09T11:38:00.115697+04:00", + "version": 1, + "tooling": "https://github.com/aquasecurity/trivy/tree/main/magefiles/vex.go", + "statements": [ + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-2575", + "name": "GO-2024-2575", + "description": "Helm's Missing YAML Content Leads To Panic in helm.sh/helm/v3", + "aliases": [ + "CVE-2024-26147", + "GHSA-r53h-jv2g-vpx6" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/helm.sh/helm/v3", + "identifiers": { + "purl": "pkg:golang/helm.sh/helm/v3" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2023-1765", + "name": "GO-2023-1765", + "description": "Leaked shared secret and weak blinding in github.com/cloudflare/circl", + "aliases": [ + "CVE-2023-1732", + "GHSA-2q89-485c-9j2x" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/cloudflare/circl", + "identifiers": { + "purl": "pkg:golang/github.com/cloudflare/circl" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-2512", + "name": "GO-2024-2512", + "description": "Classic builder cache poisoning in github.com/docker/docker", + "aliases": [ + "CVE-2024-24557", + "GHSA-xw73-rw38-6vjc" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/docker/docker", + "identifiers": { + "purl": "pkg:golang/github.com/docker/docker" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-2453", + "name": "GO-2024-2453", + "description": "Timing side channel in github.com/cloudflare/circl", + "aliases": [ + "GHSA-9763-4f94-gfch" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/cloudflare/circl", + "identifiers": { + "purl": "pkg:golang/github.com/cloudflare/circl" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2023-2048", + "name": "GO-2023-2048", + "description": "Paths outside of the rootfs could be produced on Windows in github.com/cyphar/filepath-securejoin", + "aliases": [ + "GHSA-6xv5-86q9-7xr8" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/cyphar/filepath-securejoin", + "identifiers": { + "purl": "pkg:golang/github.com/cyphar/filepath-securejoin" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-2497", + "name": "GO-2024-2497", + "description": "Privilege escalation in github.com/moby/buildkit", + "aliases": [ + "CVE-2024-23653", + "GHSA-wr6v-9f75-vh2g" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/moby/buildkit", + "identifiers": { + "purl": "pkg:golang/github.com/moby/buildkit" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2023-2102", + "name": "GO-2023-2102", + "description": "HTTP/2 rapid reset can cause excessive work in net/http", + "aliases": [ + "CVE-2023-39325", + "GHSA-4374-p667-p6c8" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/golang.org/x/net", + "identifiers": { + "purl": "pkg:golang/golang.org/x/net" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-2493", + "name": "GO-2024-2493", + "description": "Host system file access in github.com/moby/buildkit", + "aliases": [ + "CVE-2024-23651", + "GHSA-m3r6-h7wv-7xxv" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/moby/buildkit", + "identifiers": { + "purl": "pkg:golang/github.com/moby/buildkit" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-2491", + "name": "GO-2024-2491", + "description": "Container breakout through process.cwd trickery and leaked fds in github.com/opencontainers/runc", + "aliases": [ + "CVE-2024-21626", + "GHSA-xr7r-f8xq-vfvv" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/opencontainers/runc", + "identifiers": { + "purl": "pkg:golang/github.com/opencontainers/runc" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-2494", + "name": "GO-2024-2494", + "description": "Host system modification in github.com/moby/buildkit", + "aliases": [ + "CVE-2024-23652", + "GHSA-4v98-7qmw-rqr8" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/moby/buildkit", + "identifiers": { + "purl": "pkg:golang/github.com/moby/buildkit" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2023-2412", + "name": "GO-2023-2412", + "description": "RAPL accessibility in github.com/containerd/containerd", + "aliases": [ + "GHSA-7ww5-4wqc-m92c" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/containerd/containerd", + "identifiers": { + "purl": "pkg:golang/github.com/containerd/containerd" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2023-1988", + "name": "GO-2023-1988", + "description": "Improper rendering of text nodes in golang.org/x/net/html", + "aliases": [ + "CVE-2023-3978", + "GHSA-2wrh-6pvc-2jm9" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/golang.org/x/net", + "identifiers": { + "purl": "pkg:golang/golang.org/x/net" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-2492", + "name": "GO-2024-2492", + "description": "Panic in github.com/moby/buildkit", + "aliases": [ + "CVE-2024-23650", + "GHSA-9p26-698r-w4hx" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/moby/buildkit", + "identifiers": { + "purl": "pkg:golang/github.com/moby/buildkit" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2022-0646", + "name": "GO-2022-0646", + "description": "Use of risky cryptographic algorithm in github.com/aws/aws-sdk-go", + "aliases": [ + "CVE-2020-8911", + "CVE-2020-8912", + "GHSA-7f33-f4f5-xwgw", + "GHSA-f5pg-7wfw-84q9" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/aws/aws-sdk-go", + "identifiers": { + "purl": "pkg:golang/github.com/aws/aws-sdk-go" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2023-2153", + "name": "GO-2023-2153", + "description": "Denial of service from HTTP/2 Rapid Reset in google.golang.org/grpc", + "aliases": [ + "GHSA-m425-mq94-257g" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/google.golang.org/grpc", + "identifiers": { + "purl": "pkg:golang/google.golang.org/grpc" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-3105", + "name": "GO-2024-3105", + "description": "Stack exhaustion in all Parse functions in go/parser", + "aliases": [ + "CVE-2024-34155" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/stdlib", + "identifiers": { + "purl": "pkg:golang/stdlib" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-3106", + "name": "GO-2024-3106", + "description": "Stack exhaustion in Decoder.Decode in encoding/gob", + "aliases": [ + "CVE-2024-34156" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/stdlib", + "identifiers": { + "purl": "pkg:golang/stdlib" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck incorrectly marks this vulnerability as affected. The vulnerable code isn't called. See https://github.com/aquasecurity/trivy/issues/7478" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-3107", + "name": "GO-2024-3107", + "description": "Stack exhaustion in Parse in go/build/constraint", + "aliases": [ + "CVE-2024-34158" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/stdlib", + "identifiers": { + "purl": "pkg:golang/stdlib" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-3321", + "name": "GO-2024-3321", + "description": "Misuse of ServerConfig.PublicKeyCallback may cause authorization bypass in golang.org/x/crypto", + "aliases": [ + "CVE-2024-45337", + "GHSA-v778-237x-gjrc" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/golang.org/x/crypto", + "identifiers": { + "purl": "pkg:golang/golang.org/x/crypto" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2024-3333", + "name": "GO-2024-3333", + "description": "Non-linear parsing of case-insensitive content in golang.org/x/net/html", + "aliases": [ + "CVE-2024-45338" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/golang.org/x/net", + "identifiers": { + "purl": "pkg:golang/golang.org/x/net" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + }, + { + "vulnerability": { + "@id": "https://pkg.go.dev/vuln/GO-2025-4192", + "name": "GO-2025-4192", + "description": "Sigstore Timestamp Authority allocates excessive memory during request parsing in github.com/sigstore/timestamp-authority", + "aliases": [ + "CVE-2025-66564", + "GHSA-4qg8-fj49-pxjh" + ] + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "identifiers": { + "purl": "pkg:golang/github.com/aquasecurity/trivy" + }, + "subcomponents": [ + { + "@id": "pkg:golang/github.com/sigstore/timestamp-authority@v1.2.2", + "identifiers": { + "purl": "pkg:golang/github.com/sigstore/timestamp-authority@v1.2.2" + } + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "Govulncheck determined that the vulnerable code isn't called" + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000000..9ad71362042e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,829 @@ +# Changelog + +## [0.69.0](https://github.com/aquasecurity/trivy/compare/v0.68.1...v0.69.0) (2026-01-30) + + +### ⚠ BREAKING CHANGES + +* **misconf:** use ID instead of AVDID for providers mapping ([#9752](https://github.com/aquasecurity/trivy/issues/9752)) + +### Features + +* **activestate:** add support ActiveState images ([#10081](https://github.com/aquasecurity/trivy/issues/10081)) ([676709d](https://github.com/aquasecurity/trivy/commit/676709de44639e6c4329e1c69ee34a5e2760555f)) +* add AnalyzedBy field to track which analyzer detected packages ([#10059](https://github.com/aquasecurity/trivy/issues/10059)) ([1953824](https://github.com/aquasecurity/trivy/commit/195382400fc9bf93cf303910453d44f6ba3b5dad)) +* **cloudformation:** add support for Fn::ForEach ([#9508](https://github.com/aquasecurity/trivy/issues/9508)) ([d65b504](https://github.com/aquasecurity/trivy/commit/d65b504cb24a4d71f054bfbd6a872b58385bb0d3)) +* **debian:** detect third-party packages using maintainer list ([#9917](https://github.com/aquasecurity/trivy/issues/9917)) ([effc1c0](https://github.com/aquasecurity/trivy/commit/effc1c0d4d46e01efa896f53ccc72eb87d175d91)) +* **flag:** add JSON Schema for trivy.yaml configuration file ([#9971](https://github.com/aquasecurity/trivy/issues/9971)) ([4caf731](https://github.com/aquasecurity/trivy/commit/4caf7312b63f93024c188f243cda957d97b30a88)) +* **helm:** add sslCertDir parameter ([#9697](https://github.com/aquasecurity/trivy/issues/9697)) ([879e4fc](https://github.com/aquasecurity/trivy/commit/879e4fca12b53010e3b6a22fcb9c6e2b8eefad96)) +* **julia:** enable vulnerability scanning for the Julia language ecosystem ([#9800](https://github.com/aquasecurity/trivy/issues/9800)) ([c2f82ad](https://github.com/aquasecurity/trivy/commit/c2f82add3a59a509f581cc245630ce29e801de6a)) +* **misconf:** add action block to Terraform schema ([#10035](https://github.com/aquasecurity/trivy/issues/10035)) ([b06ef6d](https://github.com/aquasecurity/trivy/commit/b06ef6d2706fddb5b9c5bc42c7d26598eb105f28)) +* **misconf:** initial ansible scanning support ([#9332](https://github.com/aquasecurity/trivy/issues/9332)) ([9275e15](https://github.com/aquasecurity/trivy/commit/9275e1532b11c5f1842f334660303a7ebf46a2c6)) +* **misconf:** support for ARM resources defined as an object ([#9959](https://github.com/aquasecurity/trivy/issues/9959)) ([92d3465](https://github.com/aquasecurity/trivy/commit/92d3465cee297cad6f6e1efbb4597676e081c275)) +* **misconf:** support for azurerm_*_web_app ([#9944](https://github.com/aquasecurity/trivy/issues/9944)) ([37b5da8](https://github.com/aquasecurity/trivy/commit/37b5da895bcac45a617fee00f9de2bececb3b26a)) +* **misconf:** Update Azure Database schema ([#9811](https://github.com/aquasecurity/trivy/issues/9811)) ([48dfede](https://github.com/aquasecurity/trivy/commit/48dfedeb1e13d1654e2b9239f403d847af353e2e)) +* **misconf:** use Terraform plan configuration to partially restore schema ([#9623](https://github.com/aquasecurity/trivy/issues/9623)) ([5fced3a](https://github.com/aquasecurity/trivy/commit/5fced3ae492281f0fe000e5ccf3c634425c008d8)) +* **nodejs:** parse licenses from `package-lock.json` file ([#9983](https://github.com/aquasecurity/trivy/issues/9983)) ([b64d5ad](https://github.com/aquasecurity/trivy/commit/b64d5adc6b90fde205e4eeaf7b58332bf10372d2)) +* **php:** add support for dev dependencies in Composer ([#9910](https://github.com/aquasecurity/trivy/issues/9910)) ([56b59e8](https://github.com/aquasecurity/trivy/commit/56b59e8abbb891564bf03608b8150bceaeb60ded)) +* **report:** add Trivy version to JSON output ([#10065](https://github.com/aquasecurity/trivy/issues/10065)) ([fe7d20a](https://github.com/aquasecurity/trivy/commit/fe7d20a31c02b7ce61d85d264883831208132002)) +* **rocky:** enable modular package vulnerability detection ([#10069](https://github.com/aquasecurity/trivy/issues/10069)) ([31c4780](https://github.com/aquasecurity/trivy/commit/31c4780f720c5adeb6488440406dc7fad7637dae)) +* **rootio:** Update trivy db to support usage of Severity from root.io feed ([#9930](https://github.com/aquasecurity/trivy/issues/9930)) ([d3096e7](https://github.com/aquasecurity/trivy/commit/d3096e7617a6b5d6bc5beb2abce1b18a70c05c08)) +* **sbom:** exclude PEP 770 SBOMs in .dist-info/sboms/ ([#10033](https://github.com/aquasecurity/trivy/issues/10033)) ([07ff788](https://github.com/aquasecurity/trivy/commit/07ff7885df5397531271b837d4d9402a7d58e8a3)) +* **secret:** add detection for Symfony default secret key ([#9892](https://github.com/aquasecurity/trivy/issues/9892)) ([34baef2](https://github.com/aquasecurity/trivy/commit/34baef28ecb31c7a9ba414a118b220da97b29634)) +* **vex:** support per-repo tls configuration ([#10030](https://github.com/aquasecurity/trivy/issues/10030)) ([f809066](https://github.com/aquasecurity/trivy/commit/f809066b0745769876be8006e00dcf44017ffe96)) +* **vuln:** skip vulnerability scanning for third-party packages in Debian/Ubuntu ([#9932](https://github.com/aquasecurity/trivy/issues/9932)) ([74819bf](https://github.com/aquasecurity/trivy/commit/74819bf45733eda0a8377c40d09a60daefe9210a)) + + +### Bug Fixes + +* **docker:** fix non-det scan results for images with embedded SBOM ([#9866](https://github.com/aquasecurity/trivy/issues/9866)) ([7f71b57](https://github.com/aquasecurity/trivy/commit/7f71b577a0e138fc2908f9a8c2932b11004041db)) +* **go:** use ldflags version for all pseudo-versions ([#10037](https://github.com/aquasecurity/trivy/issues/10037)) ([3c0ab97](https://github.com/aquasecurity/trivy/commit/3c0ab97e10dc451a7fb91ef6283a932c9c4db6bf)) +* **image:** race condition in image artifact inspection ([#9966](https://github.com/aquasecurity/trivy/issues/9966)) ([18acf4f](https://github.com/aquasecurity/trivy/commit/18acf4f71dfa0668b0fe8b0ffcafd960f88b46d1)) +* **java:** add hash of GAV+root pom file path for pkgID for packages from pom.xml files ([#9880](https://github.com/aquasecurity/trivy/issues/9880)) ([809db46](https://github.com/aquasecurity/trivy/commit/809db462317fb43317e2d4f7116a8e96c1b2a728)) +* **java:** correctly inherit properties from parent fields for pom.xml files ([#9111](https://github.com/aquasecurity/trivy/issues/9111)) ([2933b01](https://github.com/aquasecurity/trivy/commit/2933b01cd55bc73ab99e6e867532093f2d65c94f)) +* **java:** correctly propagate repositories from upper POMs to dependencies ([#10077](https://github.com/aquasecurity/trivy/issues/10077)) ([b9415a3](https://github.com/aquasecurity/trivy/commit/b9415a309cf6bd34505afcda05dc0b0ad8b78059)) +* **license:** normalize licenses for PostAnalyzers ([#9941](https://github.com/aquasecurity/trivy/issues/9941)) ([11dd3fa](https://github.com/aquasecurity/trivy/commit/11dd3fac3825b45e5d4c5dd473f18c0c76e18bf5)) +* **misconf:** correct typos in block and attribute names ([#9993](https://github.com/aquasecurity/trivy/issues/9993)) ([ac061f8](https://github.com/aquasecurity/trivy/commit/ac061f8e887ab5a415b8c13474493c0541a48317)) +* **misconf:** respect .yml files when Helm charts are detected ([#9912](https://github.com/aquasecurity/trivy/issues/9912)) ([18ecf75](https://github.com/aquasecurity/trivy/commit/18ecf75176a1cadcf20e088a991af899e96fe40b)) +* **misconf:** safely parse rotation_period in google_kms_crypto_key ([#9980](https://github.com/aquasecurity/trivy/issues/9980)) ([a0ecc8e](https://github.com/aquasecurity/trivy/commit/a0ecc8e9267cb63ee3d2cd28c82561e39533031d)) +* move enum into items for array-type fields in JSON Schema ([#10039](https://github.com/aquasecurity/trivy/issues/10039)) ([4e06c3d](https://github.com/aquasecurity/trivy/commit/4e06c3df5b77f92e5390dc7d3559036f1fdf6cad)) +* remove trailing tab in statefulset template ([#9889](https://github.com/aquasecurity/trivy/issues/9889)) ([9db123c](https://github.com/aquasecurity/trivy/commit/9db123ccf80e55b30bee53db7163b9fd9a38bd75)) +* **repo:** return a nil interface for gitAuth if missing ([#10097](https://github.com/aquasecurity/trivy/issues/10097)) ([036c05b](https://github.com/aquasecurity/trivy/commit/036c05b9a12eaccd7ebe8ee2f69d610fb7eea1fc)) +* **rust:** add cargo workspace members glob support ([#10032](https://github.com/aquasecurity/trivy/issues/10032)) ([d2dc46a](https://github.com/aquasecurity/trivy/commit/d2dc46ad60178371ff9e1099bc8cbe838e1190ad)) +* **rust:** implement version inheritance for Cargo mono repos ([#10011](https://github.com/aquasecurity/trivy/issues/10011)) ([47d3103](https://github.com/aquasecurity/trivy/commit/47d3103c50d455df02c299d0f3e954b61e77fc8e)) +* **secret:** improve word boundary detection for Hugging Face tokens ([#10046](https://github.com/aquasecurity/trivy/issues/10046)) ([cdb28ee](https://github.com/aquasecurity/trivy/commit/cdb28eeeb176684e5b62b1bafbaf52e4cea79d4a)) +* use canonical SPDX license IDs from embeded licenses.json ([#10053](https://github.com/aquasecurity/trivy/issues/10053)) ([c233735](https://github.com/aquasecurity/trivy/commit/c233735b023974c0a5c321c370c7f3ebe5f20eec)) +* **vex:** add CVE-2025-66564 as not_affected into Trivy VEX file ([#9924](https://github.com/aquasecurity/trivy/issues/9924)) ([335cc99](https://github.com/aquasecurity/trivy/commit/335cc993fa76cddf2e263cb8e09d0cbbb0b98a09)) +* **vuln:** skip vulns detection for CentOS Stream family without scan failure ([#9964](https://github.com/aquasecurity/trivy/issues/9964)) ([b46cde0](https://github.com/aquasecurity/trivy/commit/b46cde0ebe651cfad461fcde510cfe8b847391c3)) + + +### Performance Improvements + +* **misconf:** optimize string concatenation in azure scanner ([#9969](https://github.com/aquasecurity/trivy/issues/9969)) ([10a50a7](https://github.com/aquasecurity/trivy/commit/10a50a7429dd18377747c54db53fc68c376135d7)) + + +### Code Refactoring + +* **misconf:** use ID instead of AVDID for providers mapping ([#9752](https://github.com/aquasecurity/trivy/issues/9752)) ([6462dc8](https://github.com/aquasecurity/trivy/commit/6462dc8a5888003de0d08a5a2b2eb8ce464b6ae2)) + +## [0.68.1](https://github.com/aquasecurity/trivy/compare/v0.68.0...v0.68.1) (2025-12-03) + + +### Bug Fixes + +* update cosing settings for GoReleaser after bumping cosing to v3 ([#9863](https://github.com/aquasecurity/trivy/issues/9863)) ([c7accc8](https://github.com/aquasecurity/trivy/commit/c7accc85c66c27ec5c51b33acda97f4002cad584)) + +## [0.68.0](https://github.com/aquasecurity/trivy/compare/v0.67.0...v0.68.0) (2025-12-02) + + +### Features + +* add ArtifactID field to uniquely identify scan targets ([#9663](https://github.com/aquasecurity/trivy/issues/9663)) ([84a7d9a](https://github.com/aquasecurity/trivy/commit/84a7d9a5d6880ef4248ead6bcf2e580deed9107b)) +* add ReportID field to scan reports ([#9670](https://github.com/aquasecurity/trivy/issues/9670)) ([fc976be](https://github.com/aquasecurity/trivy/commit/fc976bea480599e52365d306ad8d6031718d4303)) +* allow ignoring findings by type in Rego ([#9578](https://github.com/aquasecurity/trivy/issues/9578)) ([c638fc6](https://github.com/aquasecurity/trivy/commit/c638fc646c3c0d56ea50c830a31609badb477c5e)) +* **aws:** Add support for dualstack ECR endpoints ([#9862](https://github.com/aquasecurity/trivy/issues/9862)) ([e74e2b1](https://github.com/aquasecurity/trivy/commit/e74e2b1b0a8ca124b1299969abc9196789f30e8b)) +* **cli:** Add trivy cloud suppport ([#9637](https://github.com/aquasecurity/trivy/issues/9637)) ([8e6a7ff](https://github.com/aquasecurity/trivy/commit/8e6a7ff670c64106d4dea6972ac3f6228f9c6269)) +* **db:** enable concurrent access to vulnerability database ([#9750](https://github.com/aquasecurity/trivy/issues/9750)) ([d70d994](https://github.com/aquasecurity/trivy/commit/d70d994d8882a6e7a8b0c9a9b08524a2cae32ea4)) +* **dotnet:** add dependency graph support for .deps.json files ([#9726](https://github.com/aquasecurity/trivy/issues/9726)) ([18c0ee8](https://github.com/aquasecurity/trivy/commit/18c0ee86f318c7d2b1dab979370f62dd00b73979)) +* **flag:** add `--cacert` flag ([#9781](https://github.com/aquasecurity/trivy/issues/9781)) ([6048173](https://github.com/aquasecurity/trivy/commit/604817326683cdf5628550540aef63b97affc3b0)) +* **fs:** change artifact type to repository when git info is detected ([#9613](https://github.com/aquasecurity/trivy/issues/9613)) ([cff91ac](https://github.com/aquasecurity/trivy/commit/cff91acdef91fbce22306a72000c43a26ac8d79b)) +* **image:** add RepoTags support for Docker archives ([#9690](https://github.com/aquasecurity/trivy/issues/9690)) ([a9a3031](https://github.com/aquasecurity/trivy/commit/a9a3031675150e70a32df8d55b4aec2c4a33084b)) +* **image:** add Sigstore bundle SBOM support ([#9516](https://github.com/aquasecurity/trivy/issues/9516)) ([e1f3f28](https://github.com/aquasecurity/trivy/commit/e1f3f28ae4b86dd7f518a261080dc8d24ac2cdad)) +* **image:** pass global context to docker/podman image save func ([#9733](https://github.com/aquasecurity/trivy/issues/9733)) ([2690ac9](https://github.com/aquasecurity/trivy/commit/2690ac99341dcdf06eb16316d3ca20e6070969b3)) +* include registry and repository in artifact ID calculation ([#9689](https://github.com/aquasecurity/trivy/issues/9689)) ([758f271](https://github.com/aquasecurity/trivy/commit/758f2710403d5f0e9b3e138a604f95cbcab6f275)) +* **java:** add support remote repositories from settings.xml files ([#9708](https://github.com/aquasecurity/trivy/issues/9708)) ([eff52eb](https://github.com/aquasecurity/trivy/commit/eff52eb2e60a700d831cbc3d260217162b38e45c)) +* **license:** use separate SPDX ids to ignore SPDX expressions ([#9087](https://github.com/aquasecurity/trivy/issues/9087)) ([012f3d7](https://github.com/aquasecurity/trivy/commit/012f3d75359e019df1eb2602460146d43cb59715)) +* **misconf:** add agentpools to azure container schema ([#9714](https://github.com/aquasecurity/trivy/issues/9714)) ([69f400c](https://github.com/aquasecurity/trivy/commit/69f400c1839cc16013f2af3d1942c86f496e7017)) +* **misconf:** Add RoleAssignments attribute ([#9396](https://github.com/aquasecurity/trivy/issues/9396)) ([3fb8703](https://github.com/aquasecurity/trivy/commit/3fb8703f8cd659a36d6a9affe0d2e20cd752a1e4)) +* **misconf:** Add support for configurable Rego error limit ([#9657](https://github.com/aquasecurity/trivy/issues/9657)) ([445cd2b](https://github.com/aquasecurity/trivy/commit/445cd2b6b4faf78349245bc6541176e2fbf88715)) +* **misconf:** include map key in manifest snippet for diagnostics ([#9681](https://github.com/aquasecurity/trivy/issues/9681)) ([197c9e1](https://github.com/aquasecurity/trivy/commit/197c9e1dce450737fc705184eed4c24fcfc1ecc1)) +* **misconf:** support https_traffic_only_enabled in Az storage account ([#9784](https://github.com/aquasecurity/trivy/issues/9784)) ([c8d5ab7](https://github.com/aquasecurity/trivy/commit/c8d5ab7690b63a0af14d648eacabc62b868fdfe9)) +* **misconf:** Update AppService schema ([#9792](https://github.com/aquasecurity/trivy/issues/9792)) ([c6d95d7](https://github.com/aquasecurity/trivy/commit/c6d95d7cd271c3d29ce147f1ad5983cafc1caf48)) +* **misconf:** Update Azure Compute schema ([#9675](https://github.com/aquasecurity/trivy/issues/9675)) ([cb58bf6](https://github.com/aquasecurity/trivy/commit/cb58bf639eaea0a94584b4afbefe19d0010eef38)) +* **misconf:** Update Azure Container Schema ([#9673](https://github.com/aquasecurity/trivy/issues/9673)) ([43a7546](https://github.com/aquasecurity/trivy/commit/43a7546d31f2cc8dd5f8d68f82822aff1bddc4d2)) +* **misconf:** Update Azure network schema for new checks ([#9791](https://github.com/aquasecurity/trivy/issues/9791)) ([ea2dc58](https://github.com/aquasecurity/trivy/commit/ea2dc586b83fec6eac1b5de04fd9e5a06db4e16a)) +* **misconf:** Update azure storage schema ([#9728](https://github.com/aquasecurity/trivy/issues/9728)) ([c3bfecf](https://github.com/aquasecurity/trivy/commit/c3bfecf3ef236f333ebf1ace7fa2f739fdcbdcca)) +* **misconf:** Update SecurityCenter schema ([#9674](https://github.com/aquasecurity/trivy/issues/9674)) ([58819c5](https://github.com/aquasecurity/trivy/commit/58819c5285520b55bc6a5ed30aab82826aee3065)) +* **report:** add fingerprint generation for vulnerabilities ([#9794](https://github.com/aquasecurity/trivy/issues/9794)) ([cbad9ca](https://github.com/aquasecurity/trivy/commit/cbad9ca3a888cb3fb6b8649e683efe4f7047a8ed)) +* **report:** add image reference to report metadata ([#9729](https://github.com/aquasecurity/trivy/issues/9729)) ([d020f26](https://github.com/aquasecurity/trivy/commit/d020f2690e58c328f96e3083ce57fe2b71f308f3)) +* **report:** switch ReportID from UUIDv4 to UUIDv7 ([#9749](https://github.com/aquasecurity/trivy/issues/9749)) ([6fb3fde](https://github.com/aquasecurity/trivy/commit/6fb3fde916f991ccca8f23e18ab4d46e0780e50d)) +* **sbom:** add support for SPDX attestations ([#9829](https://github.com/aquasecurity/trivy/issues/9829)) ([d8eaaeb](https://github.com/aquasecurity/trivy/commit/d8eaaeb611151f1da3583ec50100a7be09ce9bc5)) +* **sbom:** use SPDX license IDs list to validate SPDX IDs ([#9569](https://github.com/aquasecurity/trivy/issues/9569)) ([35db88c](https://github.com/aquasecurity/trivy/commit/35db88c81cc5cdb8ab25362aea455c586d2e1d32)) +* **suse:** Add new openSUSE, Micro and SLES releases end of life dates ([#9788](https://github.com/aquasecurity/trivy/issues/9788)) ([019af7f](https://github.com/aquasecurity/trivy/commit/019af7fefdc1da55610446ca07f13b2ea84348b5)) + + +### Bug Fixes + +* add `buildInfo` for `BlobInfo` in `rpc` package ([#9608](https://github.com/aquasecurity/trivy/issues/9608)) ([6def66e](https://github.com/aquasecurity/trivy/commit/6def66e002427eadcc6dbabe56b01c37c1eae075)) +* close all opened resources if an error occurs ([#9665](https://github.com/aquasecurity/trivy/issues/9665)) ([fa6f779](https://github.com/aquasecurity/trivy/commit/fa6f77902234f5bee70287a841e8cfa42ca4a505)) +* **flag:** remove viper.SetDefault to fix IsSet() for config-only flags ([#9732](https://github.com/aquasecurity/trivy/issues/9732)) ([bf43629](https://github.com/aquasecurity/trivy/commit/bf43629d320426b18e7177b4b6f05affb6f93374)) +* **java:** update order for resolving package fields from multiple demManagement ([#9575](https://github.com/aquasecurity/trivy/issues/9575)) ([e286c5e](https://github.com/aquasecurity/trivy/commit/e286c5e207b6d8a1ef01f3f634f874e2e3d4c0f0)) +* **java:** use `true` as default value for Repository Release|Snapshot Enabled in pom.xml and settings.xml files ([#9751](https://github.com/aquasecurity/trivy/issues/9751)) ([d87d9b9](https://github.com/aquasecurity/trivy/commit/d87d9b97d1a61d05a9b1742caaa7e0125c481e2c)) +* **license:** don't normalize `unlicensed` licenses into `unlicense` ([#9611](https://github.com/aquasecurity/trivy/issues/9611)) ([09162e5](https://github.com/aquasecurity/trivy/commit/09162e52ecf2a3e7a65dcf4ab2c2ea43ee6f5437)) +* **license:** handle SPDX WITH exceptions as single license in category detection ([#9380](https://github.com/aquasecurity/trivy/issues/9380)) ([212f078](https://github.com/aquasecurity/trivy/commit/212f0781c552cd0395791a4a0276e1e39579f491)) +* **misconf:** ensure boolean metadata values are correctly interpreted ([#9770](https://github.com/aquasecurity/trivy/issues/9770)) ([a6ceff7](https://github.com/aquasecurity/trivy/commit/a6ceff7e83121e2c9e618dcb0584aa62e7077bbf)) +* **misconf:** ensure value used as ignore marker is non-null and known ([#9835](https://github.com/aquasecurity/trivy/issues/9835)) ([7aca801](https://github.com/aquasecurity/trivy/commit/7aca80151c2073999aa43213ae03a86b3d13a54c)) +* **misconf:** handle unsupported experimental flags in Dockerfile ([#9769](https://github.com/aquasecurity/trivy/issues/9769)) ([08d51a8](https://github.com/aquasecurity/trivy/commit/08d51a8e08c1c7f159ca491810d0d08dc787f93e)) +* **misconf:** map healthcheck start period flag to --start-period instead of --startPeriod ([#9837](https://github.com/aquasecurity/trivy/issues/9837)) ([7b2b4d4](https://github.com/aquasecurity/trivy/commit/7b2b4d4b459358ed0f561aa30639282211c80cc1)) +* **nodejs:** fix npmjs parser.pkgNameFromPath() panic issue ([#9688](https://github.com/aquasecurity/trivy/issues/9688)) ([231492d](https://github.com/aquasecurity/trivy/commit/231492db52ce69c8d9186b039b038bf0153f8dfa)) +* **nodejs:** use the default ID format to match licenses in pnpm packages. ([#9661](https://github.com/aquasecurity/trivy/issues/9661)) ([804ea4a](https://github.com/aquasecurity/trivy/commit/804ea4aa575e486fd888f59c9ceb495857b57f8c)) +* **os:** Add photon 5.0 in supported OS ([#9724](https://github.com/aquasecurity/trivy/issues/9724)) ([29f0347](https://github.com/aquasecurity/trivy/commit/29f034796590bc6b7a17fa4fee8b43c822a77c13)) +* **report:** correct field order in SARIF license results ([#9712](https://github.com/aquasecurity/trivy/issues/9712)) ([d20216e](https://github.com/aquasecurity/trivy/commit/d20216edf6fdbd0281173b25b796880bc6b2b210)) +* restore compatibility for google.protobuf.Value ([#9559](https://github.com/aquasecurity/trivy/issues/9559)) ([aeeb2a1](https://github.com/aquasecurity/trivy/commit/aeeb2a1f842b56147996b600bd34db2cf05cd28e)) +* **sbom:** add `buildInfo` info as properties ([#9683](https://github.com/aquasecurity/trivy/issues/9683)) ([2c43425](https://github.com/aquasecurity/trivy/commit/2c43425e051d80d45169a2c675dba79caa91b1e7)) +* **sbom:** don’t panic on SBOM format if scanned CycloneDX file has empty metadata ([#9562](https://github.com/aquasecurity/trivy/issues/9562)) ([fb0593b](https://github.com/aquasecurity/trivy/commit/fb0593bee68a24b7ecddeb737e1d8e3c3a3c0364)) +* Trim the end-of-range suffix ([#9618](https://github.com/aquasecurity/trivy/issues/9618)) ([e18b038](https://github.com/aquasecurity/trivy/commit/e18b038ee2dce6c239246592fcef769853c11660)) +* update all documentation links ([#9777](https://github.com/aquasecurity/trivy/issues/9777)) ([738b2b4](https://github.com/aquasecurity/trivy/commit/738b2b474a8ca94386a558c66442d8632acdce3c)) +* Use `fetch-level: 1` to check out trivy-repo in the release workflow ([#9636](https://github.com/aquasecurity/trivy/issues/9636)) ([6e53686](https://github.com/aquasecurity/trivy/commit/6e53686526ef21e8a347fc07daa2f628e24eb9e5)) +* use context for analyzers ([#9538](https://github.com/aquasecurity/trivy/issues/9538)) ([b885d3a](https://github.com/aquasecurity/trivy/commit/b885d3a3693a62bd2f506aeb238025242735ef1d)) +* using SrcVersion instead of Version for echo detector ([#9552](https://github.com/aquasecurity/trivy/issues/9552)) ([66479f0](https://github.com/aquasecurity/trivy/commit/66479f050dc1f0faa314c5a4b9159f38bb1f146b)) +* validate backport branch name ([#9548](https://github.com/aquasecurity/trivy/issues/9548)) ([f0fd432](https://github.com/aquasecurity/trivy/commit/f0fd432a7aeced7cca1acab5a52d72cd960e7171)) +* **vex:** don't use reused BOM ([#9604](https://github.com/aquasecurity/trivy/issues/9604)) ([7422cc7](https://github.com/aquasecurity/trivy/commit/7422cc7168ab917ec96b75de784be72e9b6bdb2e)) +* **vex:** use a separate `visited` set for each DFS path ([#9760](https://github.com/aquasecurity/trivy/issues/9760)) ([c274f5b](https://github.com/aquasecurity/trivy/commit/c274f5b986afb82a805f1f6d3a79d44231a7edf6)) + +## [0.67.0](https://github.com/aquasecurity/trivy/compare/v0.66.0...v0.67.0) (2025-09-30) + + +### Features + +* add documentation URL for database lock errors ([#9531](https://github.com/aquasecurity/trivy/issues/9531)) ([eba48af](https://github.com/aquasecurity/trivy/commit/eba48afd583391cef346e45a176aa5a6d77b704f)) +* **cli:** change --list-all-pkgs default to true ([#9510](https://github.com/aquasecurity/trivy/issues/9510)) ([7b663d8](https://github.com/aquasecurity/trivy/commit/7b663d86ca65ee3eb332c857b77bfa18e6da56c4)) +* **cloudformation:** support default values and list results in Fn::FindInMap ([#9515](https://github.com/aquasecurity/trivy/issues/9515)) ([42b3bf3](https://github.com/aquasecurity/trivy/commit/42b3bf37bb7d39139911843297c8b8ab3551c31a)) +* **cyclonedx:** preserve SBOM structure when scanning SBOM files with vulnerability updates ([#9439](https://github.com/aquasecurity/trivy/issues/9439)) ([aff03eb](https://github.com/aquasecurity/trivy/commit/aff03ebab2e7874dd997e20b4ec9962a41eae7bb)) +* **redhat:** add os-release detection for RHEL-based images ([#9458](https://github.com/aquasecurity/trivy/issues/9458)) ([cb25a07](https://github.com/aquasecurity/trivy/commit/cb25a074501c5cf48050fdf6a0ae7c85c4f385ea)) +* **sbom:** added support for CoreOS ([#9448](https://github.com/aquasecurity/trivy/issues/9448)) ([6d562a3](https://github.com/aquasecurity/trivy/commit/6d562a3b48926b6efd508e067e1059564173b270)) +* **seal:** add seal support ([#9370](https://github.com/aquasecurity/trivy/issues/9370)) ([e4af279](https://github.com/aquasecurity/trivy/commit/e4af279b29ed5b77ed1d62e31b232b1f9b92ef4f)) + + +### Bug Fixes + +* **aws:** use `BuildableClient` insead of `xhttp.Client` ([#9436](https://github.com/aquasecurity/trivy/issues/9436)) ([fa6f1bf](https://github.com/aquasecurity/trivy/commit/fa6f1bfecfb68c29ad4684a6fb5d86948c7d6887)) +* close file descriptors and pipes on error paths ([#9536](https://github.com/aquasecurity/trivy/issues/9536)) ([a4cbd6a](https://github.com/aquasecurity/trivy/commit/a4cbd6a1380b7b4dc650a312ec4e5bc47501f674)) +* **db:** Dowload database when missing but metadata still exists ([#9393](https://github.com/aquasecurity/trivy/issues/9393)) ([92ebc7e](https://github.com/aquasecurity/trivy/commit/92ebc7e4d72424c17d93c54e5f24891710c85a60)) +* **k8s:** disable parallel traversal with fs cache for k8s images ([#9534](https://github.com/aquasecurity/trivy/issues/9534)) ([c0c7a6b](https://github.com/aquasecurity/trivy/commit/c0c7a6bf1b92c868ed44172b3cd15c51667b8a6e)) +* **misconf:** handle tofu files in module detection ([#9486](https://github.com/aquasecurity/trivy/issues/9486)) ([bfd2f6b](https://github.com/aquasecurity/trivy/commit/bfd2f6ba697c223d60a7378283293d8e1fc8a8fe)) +* **misconf:** strip build metadata suffixes from image history ([#9498](https://github.com/aquasecurity/trivy/issues/9498)) ([c938806](https://github.com/aquasecurity/trivy/commit/c9388069a4325a9f8bc53bc8a82ff46d84d06847)) +* **misconf:** unmark cty values before access ([#9495](https://github.com/aquasecurity/trivy/issues/9495)) ([8e40d27](https://github.com/aquasecurity/trivy/commit/8e40d27a43ecb96795a8a7d4a2444241fc7fce9a)) +* **misconf:** wrap legacy ENV values in quotes to preserve spaces ([#9497](https://github.com/aquasecurity/trivy/issues/9497)) ([267a970](https://github.com/aquasecurity/trivy/commit/267a9700fa233abe1a04eada8f3ea513f3ebacb3)) +* **nodejs:** parse workspaces as objects for package-lock.json files ([#9518](https://github.com/aquasecurity/trivy/issues/9518)) ([404abb3](https://github.com/aquasecurity/trivy/commit/404abb3d91cb3b1c1ee027169de5a40e32ba8b8a)) +* **nodejs:** use snapshot string as `Package.ID` for pnpm packages ([#9330](https://github.com/aquasecurity/trivy/issues/9330)) ([4517e8c](https://github.com/aquasecurity/trivy/commit/4517e8c0ef5e942b8e2e498729257374634ffbf8)) +* **vex:** don't suppress vulns for packages with infinity loop ([#9465](https://github.com/aquasecurity/trivy/issues/9465)) ([78f0d4a](https://github.com/aquasecurity/trivy/commit/78f0d4ae0378f81940a5faa6497e6905cb5d034a)) +* **vuln:** compare `nuget` package names in lower case ([#9456](https://github.com/aquasecurity/trivy/issues/9456)) ([1ff9ac7](https://github.com/aquasecurity/trivy/commit/1ff9ac79488e0d4deab4226f1a969676a9851cdb)) + +## [0.66.0](https://github.com/aquasecurity/trivy/compare/v0.65.0...v0.66.0) (2025-09-02) + + +### Features + +* add timeout handling for cache database operations ([#9307](https://github.com/aquasecurity/trivy/issues/9307)) ([235c24e](https://github.com/aquasecurity/trivy/commit/235c24e71a546b6196f7264fced2d02d836e3f85)) +* **misconf:** added audit config attribute ([#9249](https://github.com/aquasecurity/trivy/issues/9249)) ([4d4a244](https://github.com/aquasecurity/trivy/commit/4d4a2444b692512aca137dcbd367ff224fe25597)) +* **secret:** implement streaming secret scanner with byte offset tracking ([#9264](https://github.com/aquasecurity/trivy/issues/9264)) ([5a5e097](https://github.com/aquasecurity/trivy/commit/5a5e0972c72e629ddf2915ef066d632d58b8d3b0)) +* **terraform:** use .terraform cache for remote modules in plan scanning ([#9277](https://github.com/aquasecurity/trivy/issues/9277)) ([298a994](https://github.com/aquasecurity/trivy/commit/298a9941f098d2701b9524a703b9f9b1b9451785)) + + +### Bug Fixes + +* **conda:** memory leak by adding closure method for `package.json` file ([#9349](https://github.com/aquasecurity/trivy/issues/9349)) ([03d039f](https://github.com/aquasecurity/trivy/commit/03d039f17d94cf668152e83d0cf9dabf3b27d3dd)) +* create temp file under composite fs dir ([#9387](https://github.com/aquasecurity/trivy/issues/9387)) ([ce22f54](https://github.com/aquasecurity/trivy/commit/ce22f54a39a1abac08fa3ad540697c668792bf50)) +* **cyclonedx:** handle multiple license types ([#9378](https://github.com/aquasecurity/trivy/issues/9378)) ([46ab76a](https://github.com/aquasecurity/trivy/commit/46ab76a5af828c98cf93fc988ed6a405b7b07392)) +* **fs:** avoid shadowing errors in file.glob ([#9286](https://github.com/aquasecurity/trivy/issues/9286)) ([b51c789](https://github.com/aquasecurity/trivy/commit/b51c789330141d634a9b14bd10994c997862940f)) +* **image:** use standardized HTTP client for ECR authentication ([#9322](https://github.com/aquasecurity/trivy/issues/9322)) ([84fbf86](https://github.com/aquasecurity/trivy/commit/84fbf8674dfc0f91d8795a50bafa6041cce83ba2)) +* **misconf:** ensure ignore rules respect subdirectory chart paths ([#9324](https://github.com/aquasecurity/trivy/issues/9324)) ([d3cd101](https://github.com/aquasecurity/trivy/commit/d3cd101266eb7bf9b8ffe5899765efa7bd1abe30)) +* **misconf:** ensure module source is known ([#9404](https://github.com/aquasecurity/trivy/issues/9404)) ([81d9425](https://github.com/aquasecurity/trivy/commit/81d94253c8bc816ad932f7e0c0b8907e1cd759bb)) +* **misconf:** preserve original paths of remote submodules from .terraform ([#9294](https://github.com/aquasecurity/trivy/issues/9294)) ([1319d8d](https://github.com/aquasecurity/trivy/commit/1319d8dc7f4796177876af18f0e13ba1f7086348)) +* **misconf:** use correct field log_bucket instead of target_bucket in gcp bucket ([#9296](https://github.com/aquasecurity/trivy/issues/9296)) ([04ad0c4](https://github.com/aquasecurity/trivy/commit/04ad0c4fc2926a92e9e9ec11bb8eae826ed95827)) +* persistent flag option typo ([#9374](https://github.com/aquasecurity/trivy/issues/9374)) ([6e99dd3](https://github.com/aquasecurity/trivy/commit/6e99dd304c7fad8213489039e7ca42909383b5ff)) +* **plugin:** don't remove plugins when updating index.yaml file ([#9358](https://github.com/aquasecurity/trivy/issues/9358)) ([5f067ac](https://github.com/aquasecurity/trivy/commit/5f067ac15e5c609283bef26a211746a279b6b5d0)) +* **python:** impove package name normalization ([#9290](https://github.com/aquasecurity/trivy/issues/9290)) ([1473e88](https://github.com/aquasecurity/trivy/commit/1473e88b74ca269691de7827e045703612b90050)) +* **repo:** preserve RepoMetadata on FS cache hit ([#9389](https://github.com/aquasecurity/trivy/issues/9389)) ([4f2a44e](https://github.com/aquasecurity/trivy/commit/4f2a44ea45bed1e842bb2072077da67ec7e744ac)) +* **repo:** sanitize git repo URL before inserting into report metadata ([#9391](https://github.com/aquasecurity/trivy/issues/9391)) ([1ac9b1f](https://github.com/aquasecurity/trivy/commit/1ac9b1f07cea429cc122bf9721e8909c649549cf)) +* **sbom:** add support for `file` component type of `CycloneDX` ([#9372](https://github.com/aquasecurity/trivy/issues/9372)) ([aa7cf43](https://github.com/aquasecurity/trivy/commit/aa7cf4387c5e82c1f629ac14cd6a35b48fc95983)) +* suppress debug log for context cancellation errors ([#9298](https://github.com/aquasecurity/trivy/issues/9298)) ([2458d5e](https://github.com/aquasecurity/trivy/commit/2458d5e28a54da9adec0b36f6b1e6bd4f15a72ce)) + +## [0.65.0](https://github.com/aquasecurity/trivy/compare/v0.64.0...v0.65.0) (2025-07-30) + + +### Features + +* add graceful shutdown with signal handling ([#9242](https://github.com/aquasecurity/trivy/issues/9242)) ([2c05882](https://github.com/aquasecurity/trivy/commit/2c05882f45071928c14d8212ef6c4f0f7048245d)) +* add HTTP request/response tracing support ([#9125](https://github.com/aquasecurity/trivy/issues/9125)) ([aa5b32a](https://github.com/aquasecurity/trivy/commit/aa5b32a19f4d61d0df72c11fd314c5a0b7284202)) +* **alma:** add AlmaLinux 10 support ([#9207](https://github.com/aquasecurity/trivy/issues/9207)) ([861d51e](https://github.com/aquasecurity/trivy/commit/861d51e99a45ee448f86fe195dedcaefb811c919)) +* **flag:** add schema validation for `--server` flag ([#9270](https://github.com/aquasecurity/trivy/issues/9270)) ([ed4640e](https://github.com/aquasecurity/trivy/commit/ed4640ec27f2575a50d7e6d516c9e2e45a59bb7f)) +* **image:** add Docker context resolution ([#9166](https://github.com/aquasecurity/trivy/issues/9166)) ([99cd4e7](https://github.com/aquasecurity/trivy/commit/99cd4e776c0c6cc689126e53fa86ee6333ba6277)) +* **license:** observe pkg types option in license scanner ([#9091](https://github.com/aquasecurity/trivy/issues/9091)) ([d44af8c](https://github.com/aquasecurity/trivy/commit/d44af8cfa21a145d14ca6e5e1ed4742d892f2dc5)) +* **misconf:** add private ip google access attribute to subnetwork ([#9199](https://github.com/aquasecurity/trivy/issues/9199)) ([263845c](https://github.com/aquasecurity/trivy/commit/263845cfc1419401f24adc8bc6316f3ea0caacad)) +* **misconf:** added logging and versioning to the gcp storage bucket ([#9226](https://github.com/aquasecurity/trivy/issues/9226)) ([110f80e](https://github.com/aquasecurity/trivy/commit/110f80ea29951863997dd5a1c48fe14eb81e230b)) +* **repo:** add git repository metadata to reports ([#9252](https://github.com/aquasecurity/trivy/issues/9252)) ([f4b2cf1](https://github.com/aquasecurity/trivy/commit/f4b2cf10e917d58c0840f789e083bd3f268a8af1)) +* **report:** add CVSS vectors in sarif report ([#9157](https://github.com/aquasecurity/trivy/issues/9157)) ([60723e6](https://github.com/aquasecurity/trivy/commit/60723e6cfce82ede2863cf545a189c581246f4e9)) +* **sbom:** add SHA-512 hash support for CycloneDX SBOM ([#9126](https://github.com/aquasecurity/trivy/issues/9126)) ([12d6706](https://github.com/aquasecurity/trivy/commit/12d6706961423acb12430c8b3d986b4aa4671d04)) + + +### Bug Fixes + +* **alma:** parse epochs from rpmqa file ([#9101](https://github.com/aquasecurity/trivy/issues/9101)) ([82db2fc](https://github.com/aquasecurity/trivy/commit/82db2fcc8034c911cc7a67f5a82d2f081d9c1fdf)) +* also check `filepath` when removing duplicate packages ([#9142](https://github.com/aquasecurity/trivy/issues/9142)) ([4d10a81](https://github.com/aquasecurity/trivy/commit/4d10a815dde53f5e128366f1dd0837a1dc29c17b)) +* **aws:** update amazon linux 2 EOL date ([#9176](https://github.com/aquasecurity/trivy/issues/9176)) ([0ecfed6](https://github.com/aquasecurity/trivy/commit/0ecfed6ea75cfe33e0f436a9015ac72a679e754e)) +* **cli:** Add more non-sensitive flags to telemetry ([#9110](https://github.com/aquasecurity/trivy/issues/9110)) ([7041a39](https://github.com/aquasecurity/trivy/commit/7041a39bdcf21c5b3114137d9a931f529eac2566)) +* **cli:** ensure correct command is picked by telemetry ([#9260](https://github.com/aquasecurity/trivy/issues/9260)) ([b4ad00f](https://github.com/aquasecurity/trivy/commit/b4ad00f301a5fd7326060a567871c6f4a9711696)) +* **cli:** panic: attempt to get os.Args[1] when len(os.Args) < 2 ([#9206](https://github.com/aquasecurity/trivy/issues/9206)) ([adfa879](https://github.com/aquasecurity/trivy/commit/adfa879e4e8ab88f211222a13d2b89013ae9a853)) +* **license:** add missed `GFDL-NIV-1.1` and `GFDL-NIV-1.2` into Trivy mapping ([#9116](https://github.com/aquasecurity/trivy/issues/9116)) ([a692f29](https://github.com/aquasecurity/trivy/commit/a692f296d15f7241ba5ff082e4e69926b1c728a8)) +* **license:** handle WITH operator for `LaxSplitLicenses` ([#9232](https://github.com/aquasecurity/trivy/issues/9232)) ([b4193d0](https://github.com/aquasecurity/trivy/commit/b4193d0d31a167aafdcd9d9ccd89f3f124eef7ee)) +* migrate from `*.list` to `*.md5sums` files for `dpkg` ([#9131](https://github.com/aquasecurity/trivy/issues/9131)) ([f224de3](https://github.com/aquasecurity/trivy/commit/f224de3e39b08672212ec0f94660c36bef77bc30)) +* **misconf:** correctly adapt azure storage account ([#9138](https://github.com/aquasecurity/trivy/issues/9138)) ([51aa022](https://github.com/aquasecurity/trivy/commit/51aa0222604829706193eb2ff3a6886742bb42b4)) +* **misconf:** correctly parse empty port ranges in google_compute_firewall ([#9237](https://github.com/aquasecurity/trivy/issues/9237)) ([77bab7b](https://github.com/aquasecurity/trivy/commit/77bab7b6d25c712e2db7dc53956985c2721728e9)) +* **misconf:** fix log bucket in schema ([#9235](https://github.com/aquasecurity/trivy/issues/9235)) ([7ebc129](https://github.com/aquasecurity/trivy/commit/7ebc129ab726f3133d940708837b7edda2621105)) +* **misconf:** skip rewriting expr if attr is nil ([#9113](https://github.com/aquasecurity/trivy/issues/9113)) ([42ccd3d](https://github.com/aquasecurity/trivy/commit/42ccd3df9a7c838a99facb8248e1a68eaf47a999)) +* **nodejs:** don't use prerelease logic for compare npm constraints ([#9208](https://github.com/aquasecurity/trivy/issues/9208)) ([fe96436](https://github.com/aquasecurity/trivy/commit/fe96436b99bae3bbfc7498d2ad222d4acccdfcf1)) +* prevent graceful shutdown message on normal exit ([#9244](https://github.com/aquasecurity/trivy/issues/9244)) ([6095984](https://github.com/aquasecurity/trivy/commit/6095984d5340633740204a7a40f002a5643802b9)) +* **rootio:** check full version to detect `root.io` packages ([#9117](https://github.com/aquasecurity/trivy/issues/9117)) ([c2ddd44](https://github.com/aquasecurity/trivy/commit/c2ddd44d98594a2066cb5b5acbb9ad2aaad8fd96)) +* **rootio:** fix severity selection ([#9181](https://github.com/aquasecurity/trivy/issues/9181)) ([6fafbeb](https://github.com/aquasecurity/trivy/commit/6fafbeb60609a020b47266743250ea847234cbbd)) +* **sbom:** merge in-graph and out-of-graph OS packages in scan results ([#9194](https://github.com/aquasecurity/trivy/issues/9194)) ([aa944cc](https://github.com/aquasecurity/trivy/commit/aa944cc6da43e2035f74e9d842f487c0d2f993f4)) +* **sbom:** use correct field for licenses in CycloneDX reports ([#9057](https://github.com/aquasecurity/trivy/issues/9057)) ([143da88](https://github.com/aquasecurity/trivy/commit/143da88dd82dfbe204f4c2afe46af3b01701675d)) +* **secret:** add UTF-8 validation in secret scanner to prevent protobuf marshalling errors ([#9253](https://github.com/aquasecurity/trivy/issues/9253)) ([54832a7](https://github.com/aquasecurity/trivy/commit/54832a77b50e2da3a3ceacbb6ce1b13e45605cde)) +* **secret:** fix line numbers for multiple-line secrets ([#9104](https://github.com/aquasecurity/trivy/issues/9104)) ([e579746](https://github.com/aquasecurity/trivy/commit/e57974649e4a3a275b9cf02db191b3f6bf10340f)) +* **server:** add HTTP transport setup to server mode ([#9217](https://github.com/aquasecurity/trivy/issues/9217)) ([1163b04](https://github.com/aquasecurity/trivy/commit/1163b044c7e91a81bba3a862cc4a38e90182f0b4)) +* supporting .egg-info/METADATA in python.Packaging analyzer ([#9151](https://github.com/aquasecurity/trivy/issues/9151)) ([e306e2d](https://github.com/aquasecurity/trivy/commit/e306e2dc5275c0e75f056c8c7ee9ff9261c78e7f)) +* **terraform:** `for_each` on a map returns a resource for every key ([#9156](https://github.com/aquasecurity/trivy/issues/9156)) ([153318f](https://github.com/aquasecurity/trivy/commit/153318f65f7e5059bcc064bd2cd651cc720791a9)) + +## [0.64.0](https://github.com/aquasecurity/trivy/compare/v0.63.0...v0.64.0) (2025-06-30) + + +### Features + +* **cli:** add version constraints to annoucements ([#9023](https://github.com/aquasecurity/trivy/issues/9023)) ([19efa9f](https://github.com/aquasecurity/trivy/commit/19efa9fd372242d2ec582a248e9e6573d2caef00)) +* **java:** dereference all maven settings.xml env placeholders ([#9024](https://github.com/aquasecurity/trivy/issues/9024)) ([5aade69](https://github.com/aquasecurity/trivy/commit/5aade698c71450badf8db028be61e12ec85c6248)) +* **misconf:** add OpenTofu file extension support ([#8747](https://github.com/aquasecurity/trivy/issues/8747)) ([57801d0](https://github.com/aquasecurity/trivy/commit/57801d0324384d990889ba39d856c881e5b8b070)) +* **misconf:** normalize CreatedBy for buildah and legacy docker builder ([#8953](https://github.com/aquasecurity/trivy/issues/8953)) ([65e155f](https://github.com/aquasecurity/trivy/commit/65e155fdaf0ad02ec82f00a004427f126faf65ed)) +* **redhat:** Add EOL date for RHEL 10. ([#8910](https://github.com/aquasecurity/trivy/issues/8910)) ([48258a7](https://github.com/aquasecurity/trivy/commit/48258a701a7adb210c433310de52f48568ccee19)) +* reject unsupported artifact types in remote image retrieval ([#9052](https://github.com/aquasecurity/trivy/issues/9052)) ([1e1e1b5](https://github.com/aquasecurity/trivy/commit/1e1e1b5fa6a884da978fe1ed4c222d613d6eafbd)) +* **sbom:** add manufacturer field to CycloneDX tools metadata ([#9019](https://github.com/aquasecurity/trivy/issues/9019)) ([41d0f94](https://github.com/aquasecurity/trivy/commit/41d0f949c874609641c08fa2620fa10bf4ceef78)) +* **terraform:** add partial evaluation for policy templates ([#8967](https://github.com/aquasecurity/trivy/issues/8967)) ([a9f7dcd](https://github.com/aquasecurity/trivy/commit/a9f7dcdb9c5973746c3737f2bbc3306a74be5408)) +* **ubuntu:** add end of life date for Ubuntu 25.04 ([#9077](https://github.com/aquasecurity/trivy/issues/9077)) ([367564a](https://github.com/aquasecurity/trivy/commit/367564a3bec0c202566c59598dcff087bf50a23d)) +* **ubuntu:** add eol date for 20.04-ESM ([#8981](https://github.com/aquasecurity/trivy/issues/8981)) ([87118a0](https://github.com/aquasecurity/trivy/commit/87118a0ec4a6ae492523b7bac9834c2b93a14557)) +* **vuln:** add Root.io support for container image scanning ([#9073](https://github.com/aquasecurity/trivy/issues/9073)) ([3a0ec0f](https://github.com/aquasecurity/trivy/commit/3a0ec0f2acff6a13ed6ab348b6b220d49e14a298)) + + +### Bug Fixes + +* Add missing version check flags ([#8951](https://github.com/aquasecurity/trivy/issues/8951)) ([ef5f8de](https://github.com/aquasecurity/trivy/commit/ef5f8de8dadf5534a2c965aecca01c7067e5baca)) +* **cli:** add some values to the telemetry call ([#9056](https://github.com/aquasecurity/trivy/issues/9056)) ([fd2bc91](https://github.com/aquasecurity/trivy/commit/fd2bc91e133f846bc9f0910c19ac3be3fbfe4009)) +* Correctly check for semver versions for trivy version check ([#8948](https://github.com/aquasecurity/trivy/issues/8948)) ([b813527](https://github.com/aquasecurity/trivy/commit/b813527449c4604f5afad71ae82b13399bb48680)) +* don't show corrupted trivy-db warning for first run ([#8991](https://github.com/aquasecurity/trivy/issues/8991)) ([4ed78e3](https://github.com/aquasecurity/trivy/commit/4ed78e39afe57e81c12482fef9102dc3f85d1493)) +* **misconf:** .Config.User always takes precedence over USER in .History ([#9050](https://github.com/aquasecurity/trivy/issues/9050)) ([371b8cc](https://github.com/aquasecurity/trivy/commit/371b8cc02f2ffa3f42534a437ce8727519e7b9b9)) +* **misconf:** correct Azure value-to-time conversion in AsTimeValue ([#9015](https://github.com/aquasecurity/trivy/issues/9015)) ([40d017b](https://github.com/aquasecurity/trivy/commit/40d017b67da38131734eab90c42ad945ac3b5013)) +* **misconf:** move disabled checks filtering after analyzer scan ([#9002](https://github.com/aquasecurity/trivy/issues/9002)) ([a58c36d](https://github.com/aquasecurity/trivy/commit/a58c36de124cba7250e1a5ae0cc32d83018391fe)) +* **misconf:** reduce log noise on incompatible check ([#9029](https://github.com/aquasecurity/trivy/issues/9029)) ([99c5151](https://github.com/aquasecurity/trivy/commit/99c5151d6ea1dabe85cce75ff9bb91166532b11f)) +* **nodejs:** correctly parse `packages` array of `bun.lock` file ([#8998](https://github.com/aquasecurity/trivy/issues/8998)) ([875ec3a](https://github.com/aquasecurity/trivy/commit/875ec3a9d2568e15a6824c8f84ad6a59f03eb212)) +* **report:** don't panic when report contains vulns, but doesn't contain packages for `table` format ([#8549](https://github.com/aquasecurity/trivy/issues/8549)) ([87fda76](https://github.com/aquasecurity/trivy/commit/87fda76f38a3a6939a87828c3df0c5ac2cf7fce3)) +* **sbom:** remove unnecessary OS detection check in SBOM decoding ([#9034](https://github.com/aquasecurity/trivy/issues/9034)) ([198789a](https://github.com/aquasecurity/trivy/commit/198789a07b857b053c73f8fcd1f508902fac344d)) + +## [0.63.0](https://github.com/aquasecurity/trivy/compare/v0.62.0...v0.63.0) (2025-05-29) + + +### Features + +* add Bottlerocket OS package analyzer ([#8653](https://github.com/aquasecurity/trivy/issues/8653)) ([07ef63b](https://github.com/aquasecurity/trivy/commit/07ef63b4830f9f3d791a07433287a99118d7590a)) +* add JSONC support for comments and trailing commas ([#8862](https://github.com/aquasecurity/trivy/issues/8862)) ([0b0e406](https://github.com/aquasecurity/trivy/commit/0b0e4061ef955efc0f94280d2d390f11ff6e2409)) +* **alpine:** add maintainer field extraction for APK packages ([#8930](https://github.com/aquasecurity/trivy/issues/8930)) ([104bbc1](https://github.com/aquasecurity/trivy/commit/104bbc18ea85caec17125296dc4fe2dea9c49826)) +* **cli:** Add available version checking ([#8553](https://github.com/aquasecurity/trivy/issues/8553)) ([5a0bf9e](https://github.com/aquasecurity/trivy/commit/5a0bf9ed31ad34248895e69231da602935e66785)) +* **echo:** Add Echo Support ([#8833](https://github.com/aquasecurity/trivy/issues/8833)) ([c7b8cc3](https://github.com/aquasecurity/trivy/commit/c7b8cc392eb28eb63e10561cf1ff7991e5e3c548)) +* **go:** support license scanning in both GOPATH and vendor ([#8843](https://github.com/aquasecurity/trivy/issues/8843)) ([26437be](https://github.com/aquasecurity/trivy/commit/26437be083960d17bee8b1b37b8a6780eff07981)) +* **k8s:** get components from namespaced resources ([#8918](https://github.com/aquasecurity/trivy/issues/8918)) ([4f1ab23](https://github.com/aquasecurity/trivy/commit/4f1ab238693919772a65450de9fb9fb2f873c0d6)) +* **license:** improve work text licenses with custom classification ([#8888](https://github.com/aquasecurity/trivy/issues/8888)) ([ee52230](https://github.com/aquasecurity/trivy/commit/ee522300b73a2afc72829fc2fa7ff419712fc89a)) +* **license:** improve work with custom classification of licenses from config file ([#8861](https://github.com/aquasecurity/trivy/issues/8861)) ([c321fdf](https://github.com/aquasecurity/trivy/commit/c321fdfcdd58f34d076fc730e2b63fdd13e426a9)) +* **license:** scan vendor directory for license for go.mod files ([#8689](https://github.com/aquasecurity/trivy/issues/8689)) ([dd6a6e5](https://github.com/aquasecurity/trivy/commit/dd6a6e50a44b7b543fd9dba634da599a76650acb)) +* **license:** Support compound licenses (licenses using SPDX operators) ([#8816](https://github.com/aquasecurity/trivy/issues/8816)) ([39f9ed1](https://github.com/aquasecurity/trivy/commit/39f9ed128b2c0fb599ad9092a3cf5675106bffdc)) +* **minimos:** Add support for MinimOS ([#8792](https://github.com/aquasecurity/trivy/issues/8792)) ([c2dde33](https://github.com/aquasecurity/trivy/commit/c2dde33c3f19d499258a7089d7658a9f90722acf)) +* **misconf:** add misconfiguration location to junit template ([#8793](https://github.com/aquasecurity/trivy/issues/8793)) ([a516775](https://github.com/aquasecurity/trivy/commit/a516775da6fda92a55a62418a081561127a1d5ca)) +* **misconf:** Add support for `Minimum Trivy Version` ([#8880](https://github.com/aquasecurity/trivy/issues/8880)) ([3b2a397](https://github.com/aquasecurity/trivy/commit/3b2a3976ac7e7785828655903b132e84ebd9d727)) +* **misconf:** export raw Terraform data to Rego ([#8741](https://github.com/aquasecurity/trivy/issues/8741)) ([aaecc29](https://github.com/aquasecurity/trivy/commit/aaecc29e909db4d5dac03caa0daf223035bfb877)) +* **nodejs:** add a bun.lock analyzer ([#8897](https://github.com/aquasecurity/trivy/issues/8897)) ([7ca656d](https://github.com/aquasecurity/trivy/commit/7ca656d54b99346253fc6ac6422eecaca169514e)) +* **nodejs:** add bun.lock parser ([#8851](https://github.com/aquasecurity/trivy/issues/8851)) ([1dcf816](https://github.com/aquasecurity/trivy/commit/1dcf81666f1c814600702b9ab603b4070da0b940)) +* terraform parser option to set current working directory ([#8909](https://github.com/aquasecurity/trivy/issues/8909)) ([8939451](https://github.com/aquasecurity/trivy/commit/893945117464bf6e090a55e3822f8299825f26d4)) + + +### Bug Fixes + +* check post-analyzers for StaticPaths ([#8904](https://github.com/aquasecurity/trivy/issues/8904)) ([93e6680](https://github.com/aquasecurity/trivy/commit/93e6680b1c6bbb590157f521c667c0f611775143)) +* **cli:** disable `--skip-dir` and `--skip-files` flags for `sbom` command ([#8886](https://github.com/aquasecurity/trivy/issues/8886)) ([69a5fa1](https://github.com/aquasecurity/trivy/commit/69a5fa18ca86ff7e5206abacf98732d46c000c7a)) +* **cli:** don't use allow values for `--compliance` flag ([#8881](https://github.com/aquasecurity/trivy/issues/8881)) ([35e8889](https://github.com/aquasecurity/trivy/commit/35e88890c3c201b3eb11f95376172e57bf44df4b)) +* filter all files when processing files installed from package managers ([#8842](https://github.com/aquasecurity/trivy/issues/8842)) ([6ebde88](https://github.com/aquasecurity/trivy/commit/6ebde88dbcaf22f25932bad4844b3c9eaca90560)) +* **java:** exclude dev dependencies in gradle lockfile ([#8803](https://github.com/aquasecurity/trivy/issues/8803)) ([8995838](https://github.com/aquasecurity/trivy/commit/8995838e8d184ee9178d5b52d2d3fa9b4e403015)) +* julia parser panicing ([#8883](https://github.com/aquasecurity/trivy/issues/8883)) ([be8c7b7](https://github.com/aquasecurity/trivy/commit/be8c7b796dbe36d8dc3889e0bdea23336de9a1ab)) +* **julia:** add `Relationship` field support ([#8939](https://github.com/aquasecurity/trivy/issues/8939)) ([22f040f](https://github.com/aquasecurity/trivy/commit/22f040f94790060132c7b0a635f44c35d5a35fb6)) +* **k8s:** use in-memory cache backend during misconfig scanning ([#8873](https://github.com/aquasecurity/trivy/issues/8873)) ([fe12771](https://github.com/aquasecurity/trivy/commit/fe127715e505d753e0d878d52c5f280cdc326b76)) +* **misconf:** check if for-each is known when expanding dyn block ([#8808](https://github.com/aquasecurity/trivy/issues/8808)) ([5706603](https://github.com/aquasecurity/trivy/commit/570660314698472ab831a7e0d55044e0b1e9c6c0)) +* **misconf:** use argument value in WithIncludeDeprecatedChecks ([#8942](https://github.com/aquasecurity/trivy/issues/8942)) ([7e9a54c](https://github.com/aquasecurity/trivy/commit/7e9a54cd6bf4bc15e485c6233d140b389e432fe5)) +* more revive rules ([#8814](https://github.com/aquasecurity/trivy/issues/8814)) ([3ab459e](https://github.com/aquasecurity/trivy/commit/3ab459e3b674f319bf349d478917a531a69754c0)) +* octalLiteral from go-critic ([#8811](https://github.com/aquasecurity/trivy/issues/8811)) ([a19e0aa](https://github.com/aquasecurity/trivy/commit/a19e0aa1ba0350198c898fd57c9405fbf38fa432)) +* **redhat:** Also try to find buildinfo in root layer (layer 0) ([#8924](https://github.com/aquasecurity/trivy/issues/8924)) ([906b037](https://github.com/aquasecurity/trivy/commit/906b037cff97060267d20f8947f429e078419d66)) +* **redhat:** save contentSets for OS packages in fs/vm modes ([#8820](https://github.com/aquasecurity/trivy/issues/8820)) ([9256804](https://github.com/aquasecurity/trivy/commit/9256804df8577d8a746fb8b97c508c247ab82f8f)) +* **redhat:** trim invalid suffix from content_sets in manifest parsing ([#8818](https://github.com/aquasecurity/trivy/issues/8818)) ([fa1077b](https://github.com/aquasecurity/trivy/commit/fa1077bbf5863a519f6f180a600afe5e2d6180d8)) +* **server:** add missed Relationship field for `rpc` ([#8872](https://github.com/aquasecurity/trivy/issues/8872)) ([38f17c9](https://github.com/aquasecurity/trivy/commit/38f17c945e3ef7784607037c0457fb1e06a99959)) +* use-any from revive ([#8810](https://github.com/aquasecurity/trivy/issues/8810)) ([883c63b](https://github.com/aquasecurity/trivy/commit/883c63bf29568f0feab37e5d36ae1c417eef88f5)) +* **vex:** use `lo.IsNil` to check `VEX` from OCI artifact ([#8858](https://github.com/aquasecurity/trivy/issues/8858)) ([e97af98](https://github.com/aquasecurity/trivy/commit/e97af9806ab13e1ec8b792e0586b486c4982c170)) +* **wolfi:** support new APK database location ([#8937](https://github.com/aquasecurity/trivy/issues/8937)) ([b15d9a6](https://github.com/aquasecurity/trivy/commit/b15d9a60e6a3ed40811d5ca6387082266ae92ea7)) + + +### Performance Improvements + +* **secret:** only match secrets of meaningful length, allow example strings to not be matched ([#8602](https://github.com/aquasecurity/trivy/issues/8602)) ([60fef1b](https://github.com/aquasecurity/trivy/commit/60fef1b615a765248c5870b814ba0c4345220c0e)) + +## [0.62.0](https://github.com/aquasecurity/trivy/compare/v0.61.0...v0.62.0) (2025-04-30) + + +### Features + +* **image:** save layers metadata into report ([#8394](https://github.com/aquasecurity/trivy/issues/8394)) ([a95cab0](https://github.com/aquasecurity/trivy/commit/a95cab0eab0fcaab57eb554e74e17da71bc4809f)) +* **misconf:** add option to pass Rego scanner to IaC scanner ([#8369](https://github.com/aquasecurity/trivy/issues/8369)) ([890a360](https://github.com/aquasecurity/trivy/commit/890a3602444ad2e5320044c9b8cc79ca883d17ec)) +* **misconf:** convert AWS managed policy to document ([#8757](https://github.com/aquasecurity/trivy/issues/8757)) ([7abf5f0](https://github.com/aquasecurity/trivy/commit/7abf5f0199ec65c40056d4f9addc3d27e373725a)) +* **misconf:** support auto_provisioning_defaults in google_container_cluster ([#8705](https://github.com/aquasecurity/trivy/issues/8705)) ([9792611](https://github.com/aquasecurity/trivy/commit/9792611b36271efbf79f635deebae7e51f497b70)) +* **nodejs:** add root and workspace for `yarn` packages ([#8535](https://github.com/aquasecurity/trivy/issues/8535)) ([bf4cd4f](https://github.com/aquasecurity/trivy/commit/bf4cd4f2d2dda0bb3a7018606db9a6c1e56e4f38)) +* **rust:** add root and workspace relationships/package for `cargo` lock files ([#8676](https://github.com/aquasecurity/trivy/issues/8676)) ([93efe07](https://github.com/aquasecurity/trivy/commit/93efe0789ed9d9a71e04e93d87be63032ad9cae7)) + + +### Bug Fixes + +* early-return, indent-error-flow and superfluous-else rules from revive ([#8796](https://github.com/aquasecurity/trivy/issues/8796)) ([43350dd](https://github.com/aquasecurity/trivy/commit/43350dd9b487b39d7d19bd0875274c90262dbed9)) +* **k8s:** correct compare artifact versions ([#8682](https://github.com/aquasecurity/trivy/issues/8682)) ([cc47711](https://github.com/aquasecurity/trivy/commit/cc4771158b72b88258057fa379deba9f39190994)) +* **k8s:** remove using `last-applied-configuration` ([#8791](https://github.com/aquasecurity/trivy/issues/8791)) ([7a58ccb](https://github.com/aquasecurity/trivy/commit/7a58ccbc7fffdfb1e5ccff9fd4cb6ca08c03a9ea)) +* **k8s:** skip passed misconfigs for the summary report ([#8684](https://github.com/aquasecurity/trivy/issues/8684)) ([bff0e9b](https://github.com/aquasecurity/trivy/commit/bff0e9b034f39d0d1ca02457558b1f89847009ac)) +* **misconf:** add missing variable as unknown ([#8683](https://github.com/aquasecurity/trivy/issues/8683)) ([9dcd06f](https://github.com/aquasecurity/trivy/commit/9dcd06fda717347eab1ac8ef0710687a3bfd8588)) +* **misconf:** check if metadata is not nil ([#8647](https://github.com/aquasecurity/trivy/issues/8647)) ([b7dfd64](https://github.com/aquasecurity/trivy/commit/b7dfd64987b94b4bdd8b7c5a68ba2b8f1a0a9198)) +* **misconf:** filter null nodes when parsing json manifest ([#8785](https://github.com/aquasecurity/trivy/issues/8785)) ([e10929a](https://github.com/aquasecurity/trivy/commit/e10929a669f43861bae80652bdfc9f39fad7225f)) +* **misconf:** perform operations on attribute safely ([#8774](https://github.com/aquasecurity/trivy/issues/8774)) ([3ce7d59](https://github.com/aquasecurity/trivy/commit/3ce7d59bb16553ab487762a5a660a046bcd63334)) +* **misconf:** populate context correctly for module instances ([#8656](https://github.com/aquasecurity/trivy/issues/8656)) ([efd177b](https://github.com/aquasecurity/trivy/commit/efd177b300950d82e381992e1dea39308cc39bc3)) +* **report:** clean buffer after flushing ([#8725](https://github.com/aquasecurity/trivy/issues/8725)) ([9a5383e](https://github.com/aquasecurity/trivy/commit/9a5383e993222d919d63f8d9934729cf4e291c06)) +* **secret:** ignore .dist-info directories during secret scanning ([#8646](https://github.com/aquasecurity/trivy/issues/8646)) ([a032ad6](https://github.com/aquasecurity/trivy/commit/a032ad696aa58850b9576d889128559149282ad3)) +* **server:** fix redis key when trying to delete blob ([#8649](https://github.com/aquasecurity/trivy/issues/8649)) ([36f8d0f](https://github.com/aquasecurity/trivy/commit/36f8d0fd6705bb0da5b43507128c772b153dafec)) +* **terraform:** `evaluateStep` to correctly set `EvalContext` for multiple instances of blocks ([#8555](https://github.com/aquasecurity/trivy/issues/8555)) ([e25de25](https://github.com/aquasecurity/trivy/commit/e25de25262fd1cd559879dee07bb2db2747eedd4)) +* **terraform:** hcl object expressions to return references ([#8271](https://github.com/aquasecurity/trivy/issues/8271)) ([0d3efa5](https://github.com/aquasecurity/trivy/commit/0d3efa5dc150dba437d975a2f8335de8786f94d6)) +* testifylint last issues ([#8768](https://github.com/aquasecurity/trivy/issues/8768)) ([ee4f7dc](https://github.com/aquasecurity/trivy/commit/ee4f7dc6b4be437666e91383406bba8443eec199)) +* unused-parameter rule from revive ([#8794](https://github.com/aquasecurity/trivy/issues/8794)) ([6562082](https://github.com/aquasecurity/trivy/commit/6562082e280a9df6199892927f2e3f7dc8f0c8ce)) + +## [0.61.0](https://github.com/aquasecurity/trivy/compare/v0.60.0...v0.61.0) (2025-03-28) + + +### Features + +* **fs:** optimize scanning performance by direct file access for known paths ([#8525](https://github.com/aquasecurity/trivy/issues/8525)) ([8bf6caf](https://github.com/aquasecurity/trivy/commit/8bf6caf98e2b1eff7bd16987f6791122d827747c)) +* **k8s:** add support for controllers ([#8614](https://github.com/aquasecurity/trivy/issues/8614)) ([1bf0117](https://github.com/aquasecurity/trivy/commit/1bf0117f776953bbfe67cf32e4231360010fdf33)) +* **misconf:** adapt aws_default_security_group ([#8538](https://github.com/aquasecurity/trivy/issues/8538)) ([b57eccb](https://github.com/aquasecurity/trivy/commit/b57eccb09c33df4ad0423fb148ddeaa292028401)) +* **misconf:** adapt aws_opensearch_domain ([#8550](https://github.com/aquasecurity/trivy/issues/8550)) ([9913465](https://github.com/aquasecurity/trivy/commit/9913465a535c29b377bd2f2563163ccf7cbcd6a4)) +* **misconf:** adapt AWS::DynamoDB::Table ([#8529](https://github.com/aquasecurity/trivy/issues/8529)) ([8112cdf](https://github.com/aquasecurity/trivy/commit/8112cdf8d638fa2bf57e5687e32f54b704c7e6b7)) +* **misconf:** adapt AWS::EC2::VPC ([#8534](https://github.com/aquasecurity/trivy/issues/8534)) ([0d9865f](https://github.com/aquasecurity/trivy/commit/0d9865f48f46e85595af40140faa5ff6f02b9a02)) +* **misconf:** Add support for aws_ami ([#8499](https://github.com/aquasecurity/trivy/issues/8499)) ([573502e](https://github.com/aquasecurity/trivy/commit/573502e2e83ff18020d5e7dcad498468a548733e)) +* replace TinyGo with standard Go for WebAssembly modules ([#8496](https://github.com/aquasecurity/trivy/issues/8496)) ([529957e](https://github.com/aquasecurity/trivy/commit/529957eac1fc790c57fa3d93524a901ce842a9f5)) + + +### Bug Fixes + +* **debian:** don't include empty licenses for `dpkgs` ([#8623](https://github.com/aquasecurity/trivy/issues/8623)) ([346f5b3](https://github.com/aquasecurity/trivy/commit/346f5b3553b9247f99f89d859d4f835e955d34e9)) +* **fs:** check postAnalyzers for StaticPaths ([#8543](https://github.com/aquasecurity/trivy/issues/8543)) ([c228307](https://github.com/aquasecurity/trivy/commit/c22830766e8cf1532f20198864757161eed6fda4)) +* **k8s:** show report for `--report all` ([#8613](https://github.com/aquasecurity/trivy/issues/8613)) ([dbb6f28](https://github.com/aquasecurity/trivy/commit/dbb6f288712240ef5dec59952e33b73e3a6d5b06)) +* **misconf:** add ephemeral block type to config schema ([#8513](https://github.com/aquasecurity/trivy/issues/8513)) ([41512f8](https://github.com/aquasecurity/trivy/commit/41512f846e75bae73984138ad7b3d03284a53f19)) +* **misconf:** Check values wholly prior to evalution ([#8604](https://github.com/aquasecurity/trivy/issues/8604)) ([ad58cf4](https://github.com/aquasecurity/trivy/commit/ad58cf4457ebef80ff0bc4c113d4ab4c86a9fe56)) +* **misconf:** do not skip loading documents from subdirectories ([#8526](https://github.com/aquasecurity/trivy/issues/8526)) ([de7eb13](https://github.com/aquasecurity/trivy/commit/de7eb13938f2709983a27ab3f59dbfac3fb74651)) +* **misconf:** do not use cty.NilVal for non-nil values ([#8567](https://github.com/aquasecurity/trivy/issues/8567)) ([400a79c](https://github.com/aquasecurity/trivy/commit/400a79c2c693e462ad2e1cfc21305ef13d2ec224)) +* **misconf:** identify the chart file exactly by name ([#8590](https://github.com/aquasecurity/trivy/issues/8590)) ([ba77dbe](https://github.com/aquasecurity/trivy/commit/ba77dbe5f952d67bbbbc0f43543d5f34135bc280)) +* **misconf:** Improve logging for unsupported checks ([#8634](https://github.com/aquasecurity/trivy/issues/8634)) ([5b7704d](https://github.com/aquasecurity/trivy/commit/5b7704d1d091a12822df060ee7a679135185f2ae)) +* **misconf:** set default values for AWS::EKS::Cluster.ResourcesVpcConfig ([#8548](https://github.com/aquasecurity/trivy/issues/8548)) ([1f05b45](https://github.com/aquasecurity/trivy/commit/1f05b4545d8f1de3ee703de66a7b3df2baaa07a7)) +* **misconf:** skip Azure CreateUiDefinition ([#8503](https://github.com/aquasecurity/trivy/issues/8503)) ([c7814f1](https://github.com/aquasecurity/trivy/commit/c7814f1401b0cc66a557292fe07da24d0ea7b5cc)) +* **spdx:** save text licenses into `otherLicenses` without normalize ([#8502](https://github.com/aquasecurity/trivy/issues/8502)) ([e5072f1](https://github.com/aquasecurity/trivy/commit/e5072f1eef8f3a78f4db48b4ac3f7c48aeec5e92)) +* use `--file-patterns` flag for all post analyzers ([#7365](https://github.com/aquasecurity/trivy/issues/7365)) ([8b88238](https://github.com/aquasecurity/trivy/commit/8b88238f07e389cc32e2478f84aceaf860e421ef)) + + +### Performance Improvements + +* **misconf:** parse input for Rego once ([#8483](https://github.com/aquasecurity/trivy/issues/8483)) ([0e5e909](https://github.com/aquasecurity/trivy/commit/0e5e9097650f60bc54f47a21ecc937a66e66e225)) +* **misconf:** retrieve check metadata from annotations once ([#8478](https://github.com/aquasecurity/trivy/issues/8478)) ([7b96351](https://github.com/aquasecurity/trivy/commit/7b96351c32d264d136978fe8fd9e113ada69bb2b)) + +## [0.60.0](https://github.com/aquasecurity/trivy/compare/v0.59.0...v0.60.0) (2025-03-05) + + +### Features + +* add `--vuln-severity-source` flag ([#8269](https://github.com/aquasecurity/trivy/issues/8269)) ([d464807](https://github.com/aquasecurity/trivy/commit/d4648073211e8451d66e4c0399e9441250b60a76)) +* add report summary table ([#8177](https://github.com/aquasecurity/trivy/issues/8177)) ([dd54f80](https://github.com/aquasecurity/trivy/commit/dd54f80d3fda7821dba13553480e9893ba8b4cb3)) +* **cyclonedx:** Add initial support for loading external VEX files from SBOM references ([#8254](https://github.com/aquasecurity/trivy/issues/8254)) ([4820eb7](https://github.com/aquasecurity/trivy/commit/4820eb70fc926a35d759c373112dbbdca890fd46)) +* **go:** fix parsing main module version for go >= 1.24 ([#8433](https://github.com/aquasecurity/trivy/issues/8433)) ([e58dcfc](https://github.com/aquasecurity/trivy/commit/e58dcfcf9f102c12825d5343ebbcc12a2d6c05c5)) +* **misconf:** render causes for Terraform ([#8360](https://github.com/aquasecurity/trivy/issues/8360)) ([a99498c](https://github.com/aquasecurity/trivy/commit/a99498cdd9b7bdac000140af6654bfe30135242d)) + + +### Bug Fixes + +* **db:** fix case when 2 trivy-db were copied at the same time ([#8452](https://github.com/aquasecurity/trivy/issues/8452)) ([bb3cca6](https://github.com/aquasecurity/trivy/commit/bb3cca6018551e96fdd357563dc177215ca29bd4)) +* don't use `scope` for `trivy registry login` command ([#8393](https://github.com/aquasecurity/trivy/issues/8393)) ([8715e5d](https://github.com/aquasecurity/trivy/commit/8715e5d14a727667c2e62d6f7a4b5308a0323386)) +* **go:** merge nested flags into string for ldflags for Go binaries ([#8368](https://github.com/aquasecurity/trivy/issues/8368)) ([b675b06](https://github.com/aquasecurity/trivy/commit/b675b06e897aaf374e7b1262d4323060a8a62edb)) +* **image:** disable AVD-DS-0007 for history scanning ([#8366](https://github.com/aquasecurity/trivy/issues/8366)) ([a3cd693](https://github.com/aquasecurity/trivy/commit/a3cd693a5ea88def2f9057df6178b0c0e7a6bdb0)) +* **k8s:** add missed option `PkgRelationships` ([#8442](https://github.com/aquasecurity/trivy/issues/8442)) ([f987e41](https://github.com/aquasecurity/trivy/commit/f987e4157494434f6e4e4566fedfedda92167565)) +* **misconf:** do not log scanners when misconfig scanning is disabled ([#8345](https://github.com/aquasecurity/trivy/issues/8345)) ([5695eb2](https://github.com/aquasecurity/trivy/commit/5695eb22dfed672eafacb64a71da8e9bdfbaab87)) +* **misconf:** ecs include enhanced for container insights ([#8326](https://github.com/aquasecurity/trivy/issues/8326)) ([39789ff](https://github.com/aquasecurity/trivy/commit/39789fff438d11bc6eccd254b3b890beb68c240b)) +* **misconf:** fix incorrect k8s locations due to JSON to YAML conversion ([#8073](https://github.com/aquasecurity/trivy/issues/8073)) ([a994453](https://github.com/aquasecurity/trivy/commit/a994453a7d0f543fe30c4dc8adbc92ad0c21bcbc)) +* **os:** add mapping OS aliases ([#8466](https://github.com/aquasecurity/trivy/issues/8466)) ([6b4cebe](https://github.com/aquasecurity/trivy/commit/6b4cebe9592f3a06bd91aa58ba6d65869afebbee)) +* **python:** add `poetry` v2 support ([#8323](https://github.com/aquasecurity/trivy/issues/8323)) ([10cd98c](https://github.com/aquasecurity/trivy/commit/10cd98cf55263749cb2583063a2e9e9953c7371a)) +* **report:** remove html escaping for `shortDescription` and `fullDescription` fields for sarif reports ([#8344](https://github.com/aquasecurity/trivy/issues/8344)) ([3eb0b03](https://github.com/aquasecurity/trivy/commit/3eb0b03f7c9ee462daccfacb291b2c463d848ff5)) +* **sbom:** add SBOM file's filePath as Application FilePath if we can't detect its path ([#8346](https://github.com/aquasecurity/trivy/issues/8346)) ([ecc01bb](https://github.com/aquasecurity/trivy/commit/ecc01bb3fb876fd0cc503cb38efa23e4fb9484b4)) +* **sbom:** improve logic for binding direct dependency to parent component ([#8489](https://github.com/aquasecurity/trivy/issues/8489)) ([85cca8c](https://github.com/aquasecurity/trivy/commit/85cca8c07affee4ded5c232efb45b05dacf22242)) +* **sbom:** preserve OS packages from multiple SBOMs ([#8325](https://github.com/aquasecurity/trivy/issues/8325)) ([bd5baaf](https://github.com/aquasecurity/trivy/commit/bd5baaf93054d71223e0721c7547a0567dea3b02)) +* **server:** secrets inspectation for the config analyzer in client server mode ([#8418](https://github.com/aquasecurity/trivy/issues/8418)) ([a1c4bd7](https://github.com/aquasecurity/trivy/commit/a1c4bd746f5f901e2a8f09f48f58b973b9103165)) +* **spdx:** init `pkgFilePaths` map for all formats ([#8380](https://github.com/aquasecurity/trivy/issues/8380)) ([72ea4b0](https://github.com/aquasecurity/trivy/commit/72ea4b0632308bd6150aaf2f1549a3f10b60dc23)) +* **terraform:** apply parser options to submodule parsing ([#8377](https://github.com/aquasecurity/trivy/issues/8377)) ([398620b](https://github.com/aquasecurity/trivy/commit/398620b471c25e467018bc23df53a3a1c2aa661c)) +* update all documentation links ([#8045](https://github.com/aquasecurity/trivy/issues/8045)) ([49456ba](https://github.com/aquasecurity/trivy/commit/49456ba8410e0e4cc1756906ccea1fdd60006d2d)) + +## [0.59.0](https://github.com/aquasecurity/trivy/compare/v0.58.0...v0.59.0) (2025-01-30) + + +### Features + +* add `--distro` flag to manually specify OS distribution for vulnerability scanning ([#8070](https://github.com/aquasecurity/trivy/issues/8070)) ([da17dc7](https://github.com/aquasecurity/trivy/commit/da17dc72782cd68b5d2c4314a67936343462b75e)) +* add a examples field to check metadata ([#8068](https://github.com/aquasecurity/trivy/issues/8068)) ([6d84e0c](https://github.com/aquasecurity/trivy/commit/6d84e0cc0d48ae5c490cad868bb4e5e76392241c)) +* add support for registry mirrors ([#8244](https://github.com/aquasecurity/trivy/issues/8244)) ([4316bcb](https://github.com/aquasecurity/trivy/commit/4316bcbc5b9038eed21214a826981c49696bb27f)) +* **fs:** use git commit hash as cache key for clean repositories ([#8278](https://github.com/aquasecurity/trivy/issues/8278)) ([b5062f3](https://github.com/aquasecurity/trivy/commit/b5062f3ae20044d1452bf293f210a24cd1d419b3)) +* **image:** prevent scanning oversized container images ([#8178](https://github.com/aquasecurity/trivy/issues/8178)) ([509e030](https://github.com/aquasecurity/trivy/commit/509e03030c36d17f9427ab50a4e99fb1846ba65a)) +* **image:** return error early if total size of layers exceeds limit ([#8294](https://github.com/aquasecurity/trivy/issues/8294)) ([73bd20d](https://github.com/aquasecurity/trivy/commit/73bd20d6199a777d1ed7eb560e0184d8f1b4b550)) +* **k8s:** improve artifact selections for specific namespaces ([#8248](https://github.com/aquasecurity/trivy/issues/8248)) ([db9e57a](https://github.com/aquasecurity/trivy/commit/db9e57a34e460ac6934ee21dffaa2322db9fd56b)) +* **misconf:** generate placeholders for random provider resources ([#8051](https://github.com/aquasecurity/trivy/issues/8051)) ([ffe24e1](https://github.com/aquasecurity/trivy/commit/ffe24e18dc3dca816ec9ce5ccf66d5d7b5ea70d6)) +* **misconf:** support for ignoring by inline comments for Dockerfile ([#8115](https://github.com/aquasecurity/trivy/issues/8115)) ([c002327](https://github.com/aquasecurity/trivy/commit/c00232720a89df659c6cd0b56d99304d5ffea1a7)) +* **misconf:** support for ignoring by inline comments for Helm ([#8138](https://github.com/aquasecurity/trivy/issues/8138)) ([a0429f7](https://github.com/aquasecurity/trivy/commit/a0429f773b4f696fc613d91f1600cd0da38fb2c8)) +* **nodejs:** respect peer dependencies for dependency tree ([#7989](https://github.com/aquasecurity/trivy/issues/7989)) ([7389961](https://github.com/aquasecurity/trivy/commit/73899610e8eece670d2e5ddc1478fcc0a2a5760d)) +* **python:** add support for poetry dev dependencies ([#8152](https://github.com/aquasecurity/trivy/issues/8152)) ([774e04d](https://github.com/aquasecurity/trivy/commit/774e04d19dc2067725ac2e18ca871872f74082ab)) +* **python:** add support for uv ([#8080](https://github.com/aquasecurity/trivy/issues/8080)) ([c4a4a5f](https://github.com/aquasecurity/trivy/commit/c4a4a5fa971d73ae924afcf2259631f15e96e520)) +* **python:** add support for uv dev and optional dependencies ([#8134](https://github.com/aquasecurity/trivy/issues/8134)) ([49c54b4](https://github.com/aquasecurity/trivy/commit/49c54b49c6563590dd82007d52e425a7a4e07ac0)) + + +### Bug Fixes + +* CVE-2024-45337: Misuse of ServerConfig.PublicKeyCallback may cause authorization bypass ([#8088](https://github.com/aquasecurity/trivy/issues/8088)) ([d7ac286](https://github.com/aquasecurity/trivy/commit/d7ac286085077c969734225a789e6cc056d5c5f5)) +* CVE-2025-21613 and CVE-2025-21614 : go-git: argument injection via the URL field ([#8207](https://github.com/aquasecurity/trivy/issues/8207)) ([670fbf2](https://github.com/aquasecurity/trivy/commit/670fbf2d81ea20ea691a86e4ed25a7454baf08e5)) +* de-duplicate same `dpkg` packages with different filePaths from different layers ([#8298](https://github.com/aquasecurity/trivy/issues/8298)) ([846498d](https://github.com/aquasecurity/trivy/commit/846498dd23a80531881f803147077eee19004a50)) +* enable err-error and errorf rules from perfsprint linter ([#7859](https://github.com/aquasecurity/trivy/issues/7859)) ([156a2aa](https://github.com/aquasecurity/trivy/commit/156a2aa4c49386828c0446f8978473c8da7a8754)) +* **flag:** skip hidden flags for `--generate-default-config` command ([#8046](https://github.com/aquasecurity/trivy/issues/8046)) ([5e68bdc](https://github.com/aquasecurity/trivy/commit/5e68bdc9d08f96d22451d7b5dd93e79ca576eeb7)) +* **fs:** fix cache key generation to use UUID ([#8275](https://github.com/aquasecurity/trivy/issues/8275)) ([eafd810](https://github.com/aquasecurity/trivy/commit/eafd810d7cb366215efbd0ab3b72c4651d31c6a6)) +* handle `BLOW_UNKNOWN` error to download DBs ([#8060](https://github.com/aquasecurity/trivy/issues/8060)) ([51f2123](https://github.com/aquasecurity/trivy/commit/51f2123c5ccc4f7a37d1068830b6670b4ccf9ac8)) +* improve conversion of image config to Dockerfile ([#8308](https://github.com/aquasecurity/trivy/issues/8308)) ([2e8e38a](https://github.com/aquasecurity/trivy/commit/2e8e38a8c094f3392893693ab15a605ab0d378f9)) +* **java:** correctly overwrite version from depManagement if dependency uses `project.*` props ([#8050](https://github.com/aquasecurity/trivy/issues/8050)) ([9d9f80d](https://github.com/aquasecurity/trivy/commit/9d9f80d9791f38a0b4c727152166ae4d237a83a9)) +* **license:** always trim leading and trailing spaces for licenses ([#8095](https://github.com/aquasecurity/trivy/issues/8095)) ([f5e4291](https://github.com/aquasecurity/trivy/commit/f5e429179df1637de96962ab9c19e4336056bb5d)) +* **misconf:** allow null values only for tf variables ([#8112](https://github.com/aquasecurity/trivy/issues/8112)) ([23dc3a6](https://github.com/aquasecurity/trivy/commit/23dc3a67535b7458728b2939514a96bd3de3aa81)) +* **misconf:** correctly handle all YAML tags in K8S templates ([#8259](https://github.com/aquasecurity/trivy/issues/8259)) ([f12054e](https://github.com/aquasecurity/trivy/commit/f12054e669f9df93c6322ba2755036dbccacaa83)) +* **misconf:** disable git terminal prompt on tf module load ([#8026](https://github.com/aquasecurity/trivy/issues/8026)) ([bbc5a85](https://github.com/aquasecurity/trivy/commit/bbc5a85444ec86b7bb26d6db27803d199431a8e6)) +* **misconf:** handle heredocs in dockerfile instructions ([#8284](https://github.com/aquasecurity/trivy/issues/8284)) ([0a3887c](https://github.com/aquasecurity/trivy/commit/0a3887ca0350d7dabf5db7e08aaf8152201fdf0d)) +* **misconf:** use log instead of fmt for logging ([#8033](https://github.com/aquasecurity/trivy/issues/8033)) ([07b2d7f](https://github.com/aquasecurity/trivy/commit/07b2d7fbd7f8ef5473c2438c560fffc8bdadf913)) +* **oracle:** add architectures support for advisories ([#4809](https://github.com/aquasecurity/trivy/issues/4809)) ([90f1d8d](https://github.com/aquasecurity/trivy/commit/90f1d8d78aa20b47fafab2c8ecb07247f075ef45)) +* **python:** skip dev group's deps for poetry ([#8106](https://github.com/aquasecurity/trivy/issues/8106)) ([a034d26](https://github.com/aquasecurity/trivy/commit/a034d26443704601c1fe330a5cc1f019f6974524)) +* **redhat:** check `usr/share/buildinfo/` dir to detect content sets ([#8222](https://github.com/aquasecurity/trivy/issues/8222)) ([f352f6b](https://github.com/aquasecurity/trivy/commit/f352f6b66355fe3636c9e4e9f3edd089c551a81c)) +* **redhat:** correct rewriting of recommendations for the same vulnerability ([#8063](https://github.com/aquasecurity/trivy/issues/8063)) ([4202c4b](https://github.com/aquasecurity/trivy/commit/4202c4ba0d8fcff4b89499fe03050ef4efd37330)) +* respect GITHUB_TOKEN to download artifacts from GHCR ([#7580](https://github.com/aquasecurity/trivy/issues/7580)) ([21b68e1](https://github.com/aquasecurity/trivy/commit/21b68e18188f91935ac1055a78ee97a7f35a110d)) +* **sbom:** attach nested packages to Application ([#8144](https://github.com/aquasecurity/trivy/issues/8144)) ([735335f](https://github.com/aquasecurity/trivy/commit/735335f08f84936f3928cbbc3eb71af3a3a4918d)) +* **sbom:** fix wrong overwriting of applications obtained from different sbom files but having same app type ([#8052](https://github.com/aquasecurity/trivy/issues/8052)) ([fd07074](https://github.com/aquasecurity/trivy/commit/fd07074e8033530eee2732193b00e59f27c73096)) +* **sbom:** scan results of SBOMs generated from container images are missing layers ([#7635](https://github.com/aquasecurity/trivy/issues/7635)) ([f9fceb5](https://github.com/aquasecurity/trivy/commit/f9fceb58bf64657dee92302df1ed97e597e474c9)) +* **sbom:** use root package for `unknown` dependencies (if exists) ([#8104](https://github.com/aquasecurity/trivy/issues/8104)) ([7558df7](https://github.com/aquasecurity/trivy/commit/7558df7c227c769235e5441fbdd3f9f7efb1ff84)) +* **spdx:** use the `hasExtractedLicensingInfos` field for licenses that are not listed in the SPDX ([#8077](https://github.com/aquasecurity/trivy/issues/8077)) ([aec8885](https://github.com/aquasecurity/trivy/commit/aec8885bc7f7e3c5a2a68214dca9aff28accd122)) +* **suse:** SUSE - update OSType constants and references for compatility ([#8236](https://github.com/aquasecurity/trivy/issues/8236)) ([ae28398](https://github.com/aquasecurity/trivy/commit/ae283985c926ca828b25b69ad0338008be31e5fe)) +* Updated twitter icon ([#7772](https://github.com/aquasecurity/trivy/issues/7772)) ([2c41ac8](https://github.com/aquasecurity/trivy/commit/2c41ac83a95e9347605d36f483171a60ffce0fa2)) +* wasm module test ([#8099](https://github.com/aquasecurity/trivy/issues/8099)) ([2200f38](https://github.com/aquasecurity/trivy/commit/2200f3846d675c64ab9302af43224d663a67c944)) + + +### Performance Improvements + +* avoid heap allocation in applier findPackage ([#7883](https://github.com/aquasecurity/trivy/issues/7883)) ([9bd6ed7](https://github.com/aquasecurity/trivy/commit/9bd6ed73e5d49d52856c76124e84c268475c5456)) + +## [0.58.0](https://github.com/aquasecurity/trivy/compare/v0.57.0...v0.58.0) (2024-12-02) + + +### Features + +* add `workspaceRelationship` ([#7889](https://github.com/aquasecurity/trivy/issues/7889)) ([d622ca2](https://github.com/aquasecurity/trivy/commit/d622ca2b1fe40a0eb588478ba9e15d3bd8471a78)) +* add cvss v4 score and vector in scan response ([#7968](https://github.com/aquasecurity/trivy/issues/7968)) ([e0f2054](https://github.com/aquasecurity/trivy/commit/e0f2054f9d12dce87e8a0226350f6317f7167195)) +* **go:** construct dependencies in the parser ([#7973](https://github.com/aquasecurity/trivy/issues/7973)) ([bcdc0bb](https://github.com/aquasecurity/trivy/commit/bcdc0bbf1f63777ff79d3ecadb8d4f916f376b7d)) +* **go:** construct dependencies of `go.mod` main module in the parser ([#7977](https://github.com/aquasecurity/trivy/issues/7977)) ([5448ba2](https://github.com/aquasecurity/trivy/commit/5448ba2a5c1ee36cbcf74ee1c2e83409092c5715)) +* **k8s:** add default commands for unknown platform ([#7863](https://github.com/aquasecurity/trivy/issues/7863)) ([b1c7f55](https://github.com/aquasecurity/trivy/commit/b1c7f5516fc39c6cbb76cbeae5c8677ccc9ce5dd)) +* **misconf:** log causes of HCL file parsing errors ([#7634](https://github.com/aquasecurity/trivy/issues/7634)) ([e9a899a](https://github.com/aquasecurity/trivy/commit/e9a899a3cfe41a622202808a0241b7f40b54d338)) +* **oracle:** add `flavors` support ([#7858](https://github.com/aquasecurity/trivy/issues/7858)) ([b9b383e](https://github.com/aquasecurity/trivy/commit/b9b383eb2714e88357af75900c856db2900b83ec)) +* **secret:** Add built-in secrets rules for Private Packagist ([#7826](https://github.com/aquasecurity/trivy/issues/7826)) ([132d9df](https://github.com/aquasecurity/trivy/commit/132d9dfa19a8835c94f332c6939ab7f64641ee5f)) +* **suse:** Align SUSE/OpenSUSE OS Identifiers ([#7965](https://github.com/aquasecurity/trivy/issues/7965)) ([45d3b40](https://github.com/aquasecurity/trivy/commit/45d3b40044202dec91384847ce2b50a7271f5977)) +* Update registry fallbacks ([#7679](https://github.com/aquasecurity/trivy/issues/7679)) ([5ba9a83](https://github.com/aquasecurity/trivy/commit/5ba9a83a447c4f9e577ae6235c315df71f50b452)) + + +### Bug Fixes + +* **alpine:** add `UID` for removed packages ([#7887](https://github.com/aquasecurity/trivy/issues/7887)) ([07915da](https://github.com/aquasecurity/trivy/commit/07915da4816d4d9ec8a6c5e4cba17be2a0f4ad65)) +* **aws:** change CPU and Memory type of ContainerDefinition to a string ([#7995](https://github.com/aquasecurity/trivy/issues/7995)) ([aeeba70](https://github.com/aquasecurity/trivy/commit/aeeba70d15c11443d9fe7c26f90fc7d9dcc7f92c)) +* **cli:** Handle empty ignore files more gracefully ([#7962](https://github.com/aquasecurity/trivy/issues/7962)) ([4cfb2a9](https://github.com/aquasecurity/trivy/commit/4cfb2a97b27923182ab45c178544542ec65981d4)) +* **debian:** infinite loop ([#7928](https://github.com/aquasecurity/trivy/issues/7928)) ([d982e6a](https://github.com/aquasecurity/trivy/commit/d982e6ab89967629f71ec09100cdc61e30a27c63)) +* **fs:** add missing defered Cleanup() call to post analyzer fs ([#7882](https://github.com/aquasecurity/trivy/issues/7882)) ([ab32297](https://github.com/aquasecurity/trivy/commit/ab32297e0a8220a427fa330025f8625281e02275)) +* Improve version comparisons when build identifiers are present ([#7873](https://github.com/aquasecurity/trivy/issues/7873)) ([eda4d76](https://github.com/aquasecurity/trivy/commit/eda4d7660d8908705bc08a6edc55d8144d02806a)) +* **k8s:** check all results for vulnerabilities ([#7946](https://github.com/aquasecurity/trivy/issues/7946)) ([797b36f](https://github.com/aquasecurity/trivy/commit/797b36fbad90b8e7f04e16e2cf08d6bdc0255ac7)) +* **misconf:** do not erase variable type for child modules ([#7941](https://github.com/aquasecurity/trivy/issues/7941)) ([de3b7ea](https://github.com/aquasecurity/trivy/commit/de3b7ea24c282bce22ce9cacb49a43d8d90e2bde)) +* **misconf:** handle null properties in CloudFormation templates ([#7813](https://github.com/aquasecurity/trivy/issues/7813)) ([99b2db3](https://github.com/aquasecurity/trivy/commit/99b2db3978562689cef956a71281abb84ff0ce47)) +* **misconf:** load full Terraform module ([#7925](https://github.com/aquasecurity/trivy/issues/7925)) ([fbc42a0](https://github.com/aquasecurity/trivy/commit/fbc42a04ea24e2246f81491434a965846d55ed69)) +* **misconf:** properly resolve local Terraform cache ([#7983](https://github.com/aquasecurity/trivy/issues/7983)) ([fe3a897](https://github.com/aquasecurity/trivy/commit/fe3a8971b6697d896c1ec30b5326a10c20349d14)) +* **misconf:** Update trivy-checks default repo to `mirror.gcr.io` ([#7953](https://github.com/aquasecurity/trivy/issues/7953)) ([9988147](https://github.com/aquasecurity/trivy/commit/9988147b8b0e463464fe494122bfcc66ccdf04e0)) +* **misconf:** wrap AWS EnvVar to iac types ([#7407](https://github.com/aquasecurity/trivy/issues/7407)) ([54130dc](https://github.com/aquasecurity/trivy/commit/54130dcc1d775506d34b83a558952176fc549914)) +* **redhat:** don't return error if `root/buildinfo/content_manifests/` contains files that are not `contentSets` files ([#7912](https://github.com/aquasecurity/trivy/issues/7912)) ([38775a5](https://github.com/aquasecurity/trivy/commit/38775a5ed985eefe2b410e72407c454cdad3d075)) +* **report:** handle `git@github.com` schema for misconfigs in `sarif` report ([#7898](https://github.com/aquasecurity/trivy/issues/7898)) ([19aea4b](https://github.com/aquasecurity/trivy/commit/19aea4b01f3ce5a3cd05d5a1091da5b0b3ba4af6)) +* **sbom:** Fixes for Programming Language Vulnerabilities and SBOM Package Maintainer Details ([#7871](https://github.com/aquasecurity/trivy/issues/7871)) ([461a68a](https://github.com/aquasecurity/trivy/commit/461a68afd60b77dd67e91047b3b4d558fa5bd2ec)) +* **terraform:** set null value as fallback for missing variables ([#7669](https://github.com/aquasecurity/trivy/issues/7669)) ([611558e](https://github.com/aquasecurity/trivy/commit/611558e4ce61818330118684274534f26b1fda99)) + +## [0.57.0](https://github.com/aquasecurity/trivy/compare/v0.56.0...v0.57.0) (2024-10-31) + + +### ⚠ BREAKING CHANGES + +* **k8s:** support k8s multi container ([#7444](https://github.com/aquasecurity/trivy/issues/7444)) + +### Features + +* add end of life date for Ubuntu 24.10 ([#7787](https://github.com/aquasecurity/trivy/issues/7787)) ([ad3c09e](https://github.com/aquasecurity/trivy/commit/ad3c09e006e134f3c5b879ffc34ce9895a8c860f)) +* **cli:** add `trivy auth` ([#7664](https://github.com/aquasecurity/trivy/issues/7664)) ([27117f8](https://github.com/aquasecurity/trivy/commit/27117f81d52483c3ceec56fe56ac298e242fbc9a)) +* **cli:** error out when ignore file cannot be found ([#7624](https://github.com/aquasecurity/trivy/issues/7624)) ([cb0b3a9](https://github.com/aquasecurity/trivy/commit/cb0b3a9279b31810ecd686a385e5140e567ce86f)) +* **cli:** rename `trivy auth` to `trivy registry` ([#7727](https://github.com/aquasecurity/trivy/issues/7727)) ([633a7ab](https://github.com/aquasecurity/trivy/commit/633a7abeea4287899392a24f2705f96dfeb7e312)) +* **cyclonedx:** add file checksums to `CycloneDX` reports ([#7507](https://github.com/aquasecurity/trivy/issues/7507)) ([c225883](https://github.com/aquasecurity/trivy/commit/c225883649f58128a99fa2c1cef327d0e57940be)) +* **db:** append errors ([#7843](https://github.com/aquasecurity/trivy/issues/7843)) ([5e78b6c](https://github.com/aquasecurity/trivy/commit/5e78b6c12fb5740c12dedeea3d335d48ec2f752b)) +* **misconf:** export unresolvable field of IaC types to Rego ([#7765](https://github.com/aquasecurity/trivy/issues/7765)) ([9514148](https://github.com/aquasecurity/trivy/commit/9514148767865baddd73a49245385574927f7a74)) +* **misconf:** public network support for Azure Storage Account ([#7601](https://github.com/aquasecurity/trivy/issues/7601)) ([ad91412](https://github.com/aquasecurity/trivy/commit/ad914123c4d203af1e1da6b7e2d3e49d9d3831d8)) +* **misconf:** Show misconfig ID in output ([#7762](https://github.com/aquasecurity/trivy/issues/7762)) ([f75c0d1](https://github.com/aquasecurity/trivy/commit/f75c0d1f0069d4856cb4826d6049f32c5b9409d9)) +* **misconf:** ssl_mode support for GCP SQL DB instance ([#7564](https://github.com/aquasecurity/trivy/issues/7564)) ([2eaa17e](https://github.com/aquasecurity/trivy/commit/2eaa17e0717940b27a79050e2efd9213b71178c9)) +* **parser:** ignore white space in pom.xml files ([#7747](https://github.com/aquasecurity/trivy/issues/7747)) ([a7baa93](https://github.com/aquasecurity/trivy/commit/a7baa93b00b8636aa097e64cdb8eed97dbd68511)) +* **report:** update gitlab template to populate operating_system value ([#7735](https://github.com/aquasecurity/trivy/issues/7735)) ([c0d79fa](https://github.com/aquasecurity/trivy/commit/c0d79fa09e645f3a3dbff878e393b8631fb17b64)) + + +### Bug Fixes + +* **cli:** `clean --all` deletes only relevant dirs ([#7704](https://github.com/aquasecurity/trivy/issues/7704)) ([672e886](https://github.com/aquasecurity/trivy/commit/672e886aed152ae0f09a16941706746f3053ca94)) +* **cli:** add config name to skip-policy-update alias ([#7820](https://github.com/aquasecurity/trivy/issues/7820)) ([b661d68](https://github.com/aquasecurity/trivy/commit/b661d680ff0372c8e4beea0db13bf69d6a2203a8)) +* **db:** fix javadb downloading error handling ([#7642](https://github.com/aquasecurity/trivy/issues/7642)) ([2c87f0c](https://github.com/aquasecurity/trivy/commit/2c87f0cb794acd77446a273582ba1a45b9f18980)) +* enable usestdlibvars linter ([#7770](https://github.com/aquasecurity/trivy/issues/7770)) ([57e24aa](https://github.com/aquasecurity/trivy/commit/57e24aa85382f749df7f673e241caaf3fcbb45cb)) +* **go:** Do not trim v prefix from versions in Go Mod Analyzer ([#7733](https://github.com/aquasecurity/trivy/issues/7733)) ([e872ec0](https://github.com/aquasecurity/trivy/commit/e872ec006c0745a5a142728af0096c6d6bb9ddf3)) +* **helm:** properly handle multiple archived dependencies ([#7782](https://github.com/aquasecurity/trivy/issues/7782)) ([6fab88d](https://github.com/aquasecurity/trivy/commit/6fab88dd56c257ef2cc63b617c2a5decb1c4cf98)) +* **java:** correctly inherit `version` and `scope` from upper/root `depManagement` and `dependencies` into parents ([#7541](https://github.com/aquasecurity/trivy/issues/7541)) ([778df82](https://github.com/aquasecurity/trivy/commit/778df828eaad9827cb833c6285058a33aa2b83ca)) +* **k8s:** skip resources without misconfigs ([#7797](https://github.com/aquasecurity/trivy/issues/7797)) ([7882776](https://github.com/aquasecurity/trivy/commit/78827768a612ab305bf9c55409ce76d6774302a5)) +* **k8s:** support k8s multi container ([#7444](https://github.com/aquasecurity/trivy/issues/7444)) ([c434775](https://github.com/aquasecurity/trivy/commit/c4347759234dcb5f372b07f92fb4230ef391d710)) +* **k8s:** support kubernetes v1.31 ([#7810](https://github.com/aquasecurity/trivy/issues/7810)) ([7a4f4d8](https://github.com/aquasecurity/trivy/commit/7a4f4d8b12996687f3095a2042cdf2f5985332c9)) +* **license:** fix license normalization for Universal Permissive License ([#7766](https://github.com/aquasecurity/trivy/issues/7766)) ([f6acdf7](https://github.com/aquasecurity/trivy/commit/f6acdf713991f8ffdbe765178fcb8a9cde433cba)) +* **misconf:** change default ACL of digitalocean_spaces_bucket to private ([#7577](https://github.com/aquasecurity/trivy/issues/7577)) ([9da84f5](https://github.com/aquasecurity/trivy/commit/9da84f54fadbe6ad0d73983952e945ed63b666f3)) +* **misconf:** check if property is not nil before conversion ([#7578](https://github.com/aquasecurity/trivy/issues/7578)) ([c8c14d3](https://github.com/aquasecurity/trivy/commit/c8c14d36245623019f29d258f813d2325f7490f7)) +* **misconf:** fix for Azure Storage Account network acls adaptation ([#7602](https://github.com/aquasecurity/trivy/issues/7602)) ([35fd018](https://github.com/aquasecurity/trivy/commit/35fd018ae7ad86823f114f0ac2f1376726aee444)) +* **misconf:** properly expand dynamic blocks ([#7612](https://github.com/aquasecurity/trivy/issues/7612)) ([8d5dbc9](https://github.com/aquasecurity/trivy/commit/8d5dbc9fec3569b22ed81a03c40eaf732768718b)) +* **redhat:** include arch in PURL qualifiers ([#7654](https://github.com/aquasecurity/trivy/issues/7654)) ([a585e95](https://github.com/aquasecurity/trivy/commit/a585e95f3398631d9ad10505c5ff642fde21aef7)) +* **repo:** `git clone` output to Stderr ([#7561](https://github.com/aquasecurity/trivy/issues/7561)) ([fdf203c](https://github.com/aquasecurity/trivy/commit/fdf203cd209aeb40f454bd12d121a54d6ed7a542)) +* **report:** Fix invalid URI in SARIF report ([#7645](https://github.com/aquasecurity/trivy/issues/7645)) ([015bb88](https://github.com/aquasecurity/trivy/commit/015bb885ac414b91201fa9791eead395d878149c)) +* **sbom:** add options for DBs in private registries ([#7660](https://github.com/aquasecurity/trivy/issues/7660)) ([1f2e91b](https://github.com/aquasecurity/trivy/commit/1f2e91b02b3606dd11963002a8cfac7962f3478f)) +* **sbom:** use `Annotation` instead of `AttributionTexts` for `SPDX` formats ([#7811](https://github.com/aquasecurity/trivy/issues/7811)) ([f2bb9c6](https://github.com/aquasecurity/trivy/commit/f2bb9c6227743dd61f44eb591d4b15192fe110c6)) + +## [0.56.0](https://github.com/aquasecurity/trivy/compare/v0.55.0...v0.56.0) (2024-10-03) + + +### Features + +* **java:** add empty versions if `pom.xml` dependency versions can't be detected ([#7520](https://github.com/aquasecurity/trivy/issues/7520)) ([b836232](https://github.com/aquasecurity/trivy/commit/b8362321adb2af220830c5de31c29978423d47da)) +* **license:** improve license normalization ([#7131](https://github.com/aquasecurity/trivy/issues/7131)) ([6472e3c](https://github.com/aquasecurity/trivy/commit/6472e3c9da2a8e7ba41598a45c80df8f18e57d4c)) +* **misconf:** add ability to disable checks by ID ([#7536](https://github.com/aquasecurity/trivy/issues/7536)) ([ef0a27d](https://github.com/aquasecurity/trivy/commit/ef0a27d515ff80762bf1959d44a8bde017ae06ec)) +* **misconf:** Register checks only when needed ([#7435](https://github.com/aquasecurity/trivy/issues/7435)) ([f768d3a](https://github.com/aquasecurity/trivy/commit/f768d3a767a99a86b0372f19d9f49a2de35dbe59)) +* **misconf:** Support `--skip-*` for all included modules ([#7579](https://github.com/aquasecurity/trivy/issues/7579)) ([c0e8da3](https://github.com/aquasecurity/trivy/commit/c0e8da3828e9d3a0b30d1f6568037db8dc827765)) +* **secret:** enhance secret scanning for python binary files ([#7223](https://github.com/aquasecurity/trivy/issues/7223)) ([60725f8](https://github.com/aquasecurity/trivy/commit/60725f879ba014c5c57583db6afc290b78facae8)) +* support multiple DB repositories for vulnerability and Java DB ([#7605](https://github.com/aquasecurity/trivy/issues/7605)) ([3562529](https://github.com/aquasecurity/trivy/commit/3562529ddfb26d301311ed450c192e17011353df)) +* support RPM archives ([#7628](https://github.com/aquasecurity/trivy/issues/7628)) ([69bf7e0](https://github.com/aquasecurity/trivy/commit/69bf7e00ea5ab483692db830fdded26a31f03183)) +* **suse:** added SUSE Linux Enterprise Micro support ([#7294](https://github.com/aquasecurity/trivy/issues/7294)) ([efdb68d](https://github.com/aquasecurity/trivy/commit/efdb68d3b9ddf9dfaf45ea5855b31c43a4366bab)) + + +### Bug Fixes + +* allow access to '..' in mapfs ([#7575](https://github.com/aquasecurity/trivy/issues/7575)) ([a8fbe46](https://github.com/aquasecurity/trivy/commit/a8fbe46119adbd89f827a75c75b9e97d392f1842)) +* **db:** check `DownloadedAt` for `trivy-java-db` ([#7592](https://github.com/aquasecurity/trivy/issues/7592)) ([13ef3e7](https://github.com/aquasecurity/trivy/commit/13ef3e7d62ba2bcb3a04d7b44f79b1299674b480)) +* **java:** use `dependencyManagement` from root/child pom's for dependencies from parents ([#7497](https://github.com/aquasecurity/trivy/issues/7497)) ([5442949](https://github.com/aquasecurity/trivy/commit/54429497e7d6a87eac236771d4efb8a5a7faaac5)) +* **license:** stop spliting a long license text ([#7336](https://github.com/aquasecurity/trivy/issues/7336)) ([4926da7](https://github.com/aquasecurity/trivy/commit/4926da79de901fba73819d71845ec0355b68ae0f)) +* **misconf:** Disable deprecated checks by default ([#7632](https://github.com/aquasecurity/trivy/issues/7632)) ([82e2adc](https://github.com/aquasecurity/trivy/commit/82e2adc6f8e68d0cc0021031170c2adb60d213ba)) +* **misconf:** disable DS016 check for image history analyzer ([#7540](https://github.com/aquasecurity/trivy/issues/7540)) ([de40df9](https://github.com/aquasecurity/trivy/commit/de40df9408d6d856a3ad384ec9f086edce3aa382)) +* **misconf:** escape all special sequences ([#7558](https://github.com/aquasecurity/trivy/issues/7558)) ([ea0cf03](https://github.com/aquasecurity/trivy/commit/ea0cf0379aff0348fde87356dab37947800fc1b6)) +* **misconf:** Fix logging typo ([#7473](https://github.com/aquasecurity/trivy/issues/7473)) ([56db43c](https://github.com/aquasecurity/trivy/commit/56db43c24f4f6be92891be85faaf9492cad516ac)) +* **misconf:** Fixed scope for China Cloud ([#7560](https://github.com/aquasecurity/trivy/issues/7560)) ([37d549e](https://github.com/aquasecurity/trivy/commit/37d549e5b86a1c5dce6710fbfd2310aec9abe949)) +* **misconf:** not to warn about missing selectors of libraries ([#7638](https://github.com/aquasecurity/trivy/issues/7638)) ([fcaea74](https://github.com/aquasecurity/trivy/commit/fcaea740808d5784c120e5c5d65f5f94e1d931d4)) +* **oracle:** Update EOL date for Oracle 7 ([#7480](https://github.com/aquasecurity/trivy/issues/7480)) ([dd0a64a](https://github.com/aquasecurity/trivy/commit/dd0a64a1cf0cd76e6f81e3ff55fa6ccb95ce3c3d)) +* **report:** change a receiver of MarshalJSON ([#7483](https://github.com/aquasecurity/trivy/issues/7483)) ([927c6e0](https://github.com/aquasecurity/trivy/commit/927c6e0c9d4d4a3f1be00f0f661c1d18325d9440)) +* **report:** fix error with unmarshal of `ExperimentalModifiedFindings` ([#7463](https://github.com/aquasecurity/trivy/issues/7463)) ([7ff9aff](https://github.com/aquasecurity/trivy/commit/7ff9aff2739b2eee4a98175b98914795e4077060)) +* **sbom:** export bom-ref when converting a package to a component ([#7340](https://github.com/aquasecurity/trivy/issues/7340)) ([5dd94eb](https://github.com/aquasecurity/trivy/commit/5dd94ebc1ffe3f1df511dee6381f92a5daefadf2)) +* **sbom:** parse type `framework` as `library` when unmarshalling `CycloneDX` files ([#7527](https://github.com/aquasecurity/trivy/issues/7527)) ([aeb7039](https://github.com/aquasecurity/trivy/commit/aeb7039d7ce090e243d29f0bf16c9e4e24252a01)) +* **secret:** change grafana token regex to find them without unquoted ([#7627](https://github.com/aquasecurity/trivy/issues/7627)) ([3e1fa21](https://github.com/aquasecurity/trivy/commit/3e1fa2100074e840bacdd65947425b08750b7d9a)) + + +### Performance Improvements + +* **misconf:** use port ranges instead of enumeration ([#7549](https://github.com/aquasecurity/trivy/issues/7549)) ([1f9fc13](https://github.com/aquasecurity/trivy/commit/1f9fc13da4a1e7c76c978e4f8e119bfd61a0480e)) + + +### Reverts + +* **java:** stop supporting of `test` scope for `pom.xml` files ([#7488](https://github.com/aquasecurity/trivy/issues/7488)) ([b0222fe](https://github.com/aquasecurity/trivy/commit/b0222feeb586ec59904bb321fda8f3f22496d07b)) + +## [0.55.0](https://github.com/aquasecurity/trivy/compare/v0.54.0...v0.55.0) (2024-09-03) + + +### ⚠ BREAKING CHANGES + +* **cli:** delete deprecated SBOM flags ([#7266](https://github.com/aquasecurity/trivy/issues/7266)) + +### Features + +* **cli:** delete deprecated SBOM flags ([#7266](https://github.com/aquasecurity/trivy/issues/7266)) ([7024572](https://github.com/aquasecurity/trivy/commit/70245721372720027b7089bd61c693df48add865)) +* **go:** use `toolchain` as `stdlib` version for `go.mod` files ([#7163](https://github.com/aquasecurity/trivy/issues/7163)) ([2d80769](https://github.com/aquasecurity/trivy/commit/2d80769c34b118851640411fff9dac0b3e353e82)) +* **java:** add `test` scope support for `pom.xml` files ([#7414](https://github.com/aquasecurity/trivy/issues/7414)) ([2d97700](https://github.com/aquasecurity/trivy/commit/2d97700d10665142d2f66d7910202bec82116209)) +* **misconf:** Add support for using spec from on-disk bundle ([#7179](https://github.com/aquasecurity/trivy/issues/7179)) ([be86126](https://github.com/aquasecurity/trivy/commit/be861265cafc89787fda09c59b2ef175e3d04204)) +* **misconf:** ignore duplicate checks ([#7317](https://github.com/aquasecurity/trivy/issues/7317)) ([9ef05fc](https://github.com/aquasecurity/trivy/commit/9ef05fc6b171a264516a025b0b0bcbbc8cff10bc)) +* **misconf:** iterator argument support for dynamic blocks ([#7236](https://github.com/aquasecurity/trivy/issues/7236)) ([fe92072](https://github.com/aquasecurity/trivy/commit/fe9207255a4f7f984ec1447f8a9219ae60e560c4)) +* **misconf:** port and protocol support for EC2 networks ([#7146](https://github.com/aquasecurity/trivy/issues/7146)) ([98e136e](https://github.com/aquasecurity/trivy/commit/98e136eb7baa2b66f4233d96875c1490144e1594)) +* **misconf:** scanning support for YAML and JSON ([#7311](https://github.com/aquasecurity/trivy/issues/7311)) ([efdbd8f](https://github.com/aquasecurity/trivy/commit/efdbd8f19ab0ab0c3b48293d43e51c81b7b03b89)) +* **misconf:** support for ignore by nested attributes ([#7205](https://github.com/aquasecurity/trivy/issues/7205)) ([44e4686](https://github.com/aquasecurity/trivy/commit/44e468603d44b077cc4606327fb3e7d7ca435e05)) +* **misconf:** support for policy and bucket grants ([#7284](https://github.com/aquasecurity/trivy/issues/7284)) ([a817fae](https://github.com/aquasecurity/trivy/commit/a817fae85b7272b391b737ec86673a7cab722bae)) +* **misconf:** variable support for Terraform Plan ([#7228](https://github.com/aquasecurity/trivy/issues/7228)) ([db2c955](https://github.com/aquasecurity/trivy/commit/db2c95598da098ca610825089eb4ab63b789b215)) +* **python:** use minimum version for pip packages ([#7348](https://github.com/aquasecurity/trivy/issues/7348)) ([e9b43f8](https://github.com/aquasecurity/trivy/commit/e9b43f81e67789b067352fcb6aa55bc9478bc518)) +* **report:** export modified findings in JSON ([#7383](https://github.com/aquasecurity/trivy/issues/7383)) ([7aea79d](https://github.com/aquasecurity/trivy/commit/7aea79dd93cfb61453766dbbb2e3fc0fbd317852)) +* **sbom:** set User-Agent header on requests to Rekor ([#7396](https://github.com/aquasecurity/trivy/issues/7396)) ([af1d257](https://github.com/aquasecurity/trivy/commit/af1d257730422d238871beb674767f8f83c5d06a)) +* **server:** add internal `--path-prefix` flag for client/server mode ([#7321](https://github.com/aquasecurity/trivy/issues/7321)) ([24a4563](https://github.com/aquasecurity/trivy/commit/24a45636867b893ff54c5ce07197f3b5c6db1d9b)) +* **server:** Make Trivy Server Multiplexer Exported ([#7389](https://github.com/aquasecurity/trivy/issues/7389)) ([4c6e8ca](https://github.com/aquasecurity/trivy/commit/4c6e8ca9cc9591799907cc73075f2d740e303b8f)) +* **vm:** Support direct filesystem ([#7058](https://github.com/aquasecurity/trivy/issues/7058)) ([45b3f34](https://github.com/aquasecurity/trivy/commit/45b3f344042bcd90ca63ab696b69bff0e9ab4e36)) +* **vm:** support the Ext2/Ext3 filesystems ([#6983](https://github.com/aquasecurity/trivy/issues/6983)) ([35c60f0](https://github.com/aquasecurity/trivy/commit/35c60f030fa48de8d8e57958e5ba379814126831)) +* **vuln:** Add `--detection-priority` flag for accuracy tuning ([#7288](https://github.com/aquasecurity/trivy/issues/7288)) ([fd8348d](https://github.com/aquasecurity/trivy/commit/fd8348d610f20c6c33da81cd7b0e7d5504ce26be)) + + +### Bug Fixes + +* **aws:** handle ECR repositories in different regions ([#6217](https://github.com/aquasecurity/trivy/issues/6217)) ([feaef96](https://github.com/aquasecurity/trivy/commit/feaef9699df5d8ca399770e701a59d7c0ff979a3)) +* **flag:** incorrect behavior for deprected flag `--clear-cache` ([#7281](https://github.com/aquasecurity/trivy/issues/7281)) ([2a0e529](https://github.com/aquasecurity/trivy/commit/2a0e529c36057b572119815af59c28e4790034ca)) +* **helm:** explicitly define `kind` and `apiVersion` of `volumeClaimTemplate` element ([#7362](https://github.com/aquasecurity/trivy/issues/7362)) ([da4ebfa](https://github.com/aquasecurity/trivy/commit/da4ebfa1a741f3f8b0b43289b4028afe763f7d43)) +* **java:** Return error when trying to find a remote pom to avoid segfault ([#7275](https://github.com/aquasecurity/trivy/issues/7275)) ([49d5270](https://github.com/aquasecurity/trivy/commit/49d5270163e305f88fedcf50412973736e69dc69)) +* **license:** add license handling to JUnit template ([#7409](https://github.com/aquasecurity/trivy/issues/7409)) ([f80183c](https://github.com/aquasecurity/trivy/commit/f80183c1139b21bb95bc64e216358f4a76001a65)) +* logger initialization before flags parsing ([#7372](https://github.com/aquasecurity/trivy/issues/7372)) ([c929290](https://github.com/aquasecurity/trivy/commit/c929290c3c0e4e91337264d69e75ccb60522bc65)) +* **misconf:** change default TLS values for the Azure storage account ([#7345](https://github.com/aquasecurity/trivy/issues/7345)) ([aadb090](https://github.com/aquasecurity/trivy/commit/aadb09078843250c66087f46db9a2aa48094a118)) +* **misconf:** do not filter Terraform plan JSON by name ([#7406](https://github.com/aquasecurity/trivy/issues/7406)) ([9d7264a](https://github.com/aquasecurity/trivy/commit/9d7264af8e85bcc0dba600b8366d0470d455251c)) +* **misconf:** do not recreate filesystem map ([#7416](https://github.com/aquasecurity/trivy/issues/7416)) ([3a5d091](https://github.com/aquasecurity/trivy/commit/3a5d091759564496992a83fb2015a21c84a22213)) +* **misconf:** do not register Rego libs in checks registry ([#7420](https://github.com/aquasecurity/trivy/issues/7420)) ([a5aa63e](https://github.com/aquasecurity/trivy/commit/a5aa63eff7e229744090f9ad300c1bec3259397e)) +* **misconf:** do not set default value for default_cache_behavior ([#7234](https://github.com/aquasecurity/trivy/issues/7234)) ([f0ed5e4](https://github.com/aquasecurity/trivy/commit/f0ed5e4ced7e60af35c88d5d084aa4b7237f4973)) +* **misconf:** fix infer type for null value ([#7424](https://github.com/aquasecurity/trivy/issues/7424)) ([0cac3ac](https://github.com/aquasecurity/trivy/commit/0cac3ac7075017628a21a7990941df04cbc16dbe)) +* **misconf:** init frameworks before updating them ([#7376](https://github.com/aquasecurity/trivy/issues/7376)) ([b65b32d](https://github.com/aquasecurity/trivy/commit/b65b32ddfa6fc62ac81ad9fa580e1f5a327864f5)) +* **misconf:** load only submodule if it is specified in source ([#7112](https://github.com/aquasecurity/trivy/issues/7112)) ([a4180bd](https://github.com/aquasecurity/trivy/commit/a4180bddd43d86e479edf0afe0c362021d071482)) +* **misconf:** support deprecating for Go checks ([#7377](https://github.com/aquasecurity/trivy/issues/7377)) ([2a6c7ab](https://github.com/aquasecurity/trivy/commit/2a6c7ab3b338ce4a8f99d6ac3508c2531dcbe812)) +* **misconf:** use module to log when metadata retrieval fails ([#7405](https://github.com/aquasecurity/trivy/issues/7405)) ([0799770](https://github.com/aquasecurity/trivy/commit/0799770b8827a8276ad0d6d9ac7e0381c286757c)) +* **misconf:** wrap Azure PortRange in iac types ([#7357](https://github.com/aquasecurity/trivy/issues/7357)) ([c5c62d5](https://github.com/aquasecurity/trivy/commit/c5c62d5ff05420321f9cdbfb93e2591e0866a342)) +* **nodejs:** check all `importers` to detect dev deps from pnpm-lock.yaml file ([#7387](https://github.com/aquasecurity/trivy/issues/7387)) ([fd9ed3a](https://github.com/aquasecurity/trivy/commit/fd9ed3a330bc66e229bcbdc262dc296a3bf01f54)) +* **plugin:** do not call GitHub content API for releases and tags ([#7274](https://github.com/aquasecurity/trivy/issues/7274)) ([b3ee6da](https://github.com/aquasecurity/trivy/commit/b3ee6dac269bd7847674f3ce985a5ff7f8f0ba38)) +* **report:** escape `Message` field in `asff.tpl` template ([#7401](https://github.com/aquasecurity/trivy/issues/7401)) ([dd9733e](https://github.com/aquasecurity/trivy/commit/dd9733e950d3127aa2ac90c45ec7e2b88a2b47ca)) +* safely check if the directory exists ([#7353](https://github.com/aquasecurity/trivy/issues/7353)) ([05a8297](https://github.com/aquasecurity/trivy/commit/05a829715f99cd90b122c64cd2f40157854e467b)) +* **sbom:** use `NOASSERTION` for licenses fields in SPDX formats ([#7403](https://github.com/aquasecurity/trivy/issues/7403)) ([c96dcdd](https://github.com/aquasecurity/trivy/commit/c96dcdd440a14cdd1b01ac473b2c15e4698e387b)) +* **secret:** use `.eyJ` keyword for JWT secret ([#7410](https://github.com/aquasecurity/trivy/issues/7410)) ([bf64003](https://github.com/aquasecurity/trivy/commit/bf64003ac8b209f34b88f228918a96d4f9dac5e0)) +* **secret:** use only line with secret for long secret lines ([#7412](https://github.com/aquasecurity/trivy/issues/7412)) ([391448a](https://github.com/aquasecurity/trivy/commit/391448aba9fcb0a4138225e5ab305e4e6707c603)) +* **terraform:** add aws_region name to presets ([#7184](https://github.com/aquasecurity/trivy/issues/7184)) ([bb2e26a](https://github.com/aquasecurity/trivy/commit/bb2e26a0ab707b718f6a890cbc87e2492298b6e5)) + + +### Performance Improvements + +* **misconf:** do not convert contents of a YAML file to string ([#7292](https://github.com/aquasecurity/trivy/issues/7292)) ([85dadf5](https://github.com/aquasecurity/trivy/commit/85dadf56265647c000191561db10b08a4948c140)) +* **misconf:** optimize work with context ([#6968](https://github.com/aquasecurity/trivy/issues/6968)) ([2b6d8d9](https://github.com/aquasecurity/trivy/commit/2b6d8d9227fb6ecc9386a14333964c23c0370a52)) +* **misconf:** use json.Valid to check validity of JSON ([#7308](https://github.com/aquasecurity/trivy/issues/7308)) ([c766831](https://github.com/aquasecurity/trivy/commit/c766831069e188226efafeec184e41498685ed85)) + +## [0.54.0](https://github.com/aquasecurity/trivy/compare/v0.53.0...v0.54.0) (2024-07-30) + + +### Features + +* add `log.FilePath()` function for logger ([#7080](https://github.com/aquasecurity/trivy/issues/7080)) ([1f5f348](https://github.com/aquasecurity/trivy/commit/1f5f34895823fae81bf521fc939bee743a50e304)) +* add openSUSE tumbleweed detection and scanning ([#6965](https://github.com/aquasecurity/trivy/issues/6965)) ([17b5dbf](https://github.com/aquasecurity/trivy/commit/17b5dbfa12180414b87859c6c46bfe6cc5ecf7ba)) +* **cli:** rename `--vuln-type` flag to `--pkg-types` flag ([#7104](https://github.com/aquasecurity/trivy/issues/7104)) ([7cbdb0a](https://github.com/aquasecurity/trivy/commit/7cbdb0a0b5dff33e506e1c1f3119951fa241b432)) +* **mariner:** Add support for Azure Linux ([#7186](https://github.com/aquasecurity/trivy/issues/7186)) ([5cbc452](https://github.com/aquasecurity/trivy/commit/5cbc452a09822d1bf300ead88f0d613d4cf0349a)) +* **misconf:** enabled China configuration for ACRs ([#7156](https://github.com/aquasecurity/trivy/issues/7156)) ([d1ec89d](https://github.com/aquasecurity/trivy/commit/d1ec89d1db4b039f0e31076ccd1ca969fb15628e)) +* **nodejs:** add license parser to pnpm analyser ([#7036](https://github.com/aquasecurity/trivy/issues/7036)) ([03ac93d](https://github.com/aquasecurity/trivy/commit/03ac93dc208f1b40896f3fa11fa1d45293176dca)) +* **sbom:** add image labels into `SPDX` and `CycloneDX` reports ([#7257](https://github.com/aquasecurity/trivy/issues/7257)) ([4a2f492](https://github.com/aquasecurity/trivy/commit/4a2f492c6e685ff577fb96a7006cd0c43755baf4)) +* **sbom:** add vulnerability support for SPDX formats ([#7213](https://github.com/aquasecurity/trivy/issues/7213)) ([efb1f69](https://github.com/aquasecurity/trivy/commit/efb1f6938321eec3529ef4fea6608261f6771ae0)) +* share build-in rules ([#7207](https://github.com/aquasecurity/trivy/issues/7207)) ([bff317c](https://github.com/aquasecurity/trivy/commit/bff317c77bf4a5f615a80d9875d129213bd52f6d)) +* **vex:** retrieve VEX attestations from OCI registries ([#7249](https://github.com/aquasecurity/trivy/issues/7249)) ([c2fd2e0](https://github.com/aquasecurity/trivy/commit/c2fd2e0d89567a0ccd996dda8790f3c3305ea6f7)) +* **vex:** VEX Repository support ([#7206](https://github.com/aquasecurity/trivy/issues/7206)) ([88ba460](https://github.com/aquasecurity/trivy/commit/88ba46047c93e6046292523ae701de774dfdc4dc)) +* **vuln:** add `--pkg-relationships` ([#7237](https://github.com/aquasecurity/trivy/issues/7237)) ([5c37361](https://github.com/aquasecurity/trivy/commit/5c37361600d922db27dd594b2a80c010a19b3a6e)) + + +### Bug Fixes + +* Add dependencyManagement exclusions to the child exclusions ([#6969](https://github.com/aquasecurity/trivy/issues/6969)) ([dc68a66](https://github.com/aquasecurity/trivy/commit/dc68a662a701980d6529f61a65006f1e4728a3e5)) +* add missing platform and type to spec ([#7149](https://github.com/aquasecurity/trivy/issues/7149)) ([c8a7abd](https://github.com/aquasecurity/trivy/commit/c8a7abd3b508975fcf10c254d13d1a2cd42da657)) +* **cli:** error on missing config file ([#7154](https://github.com/aquasecurity/trivy/issues/7154)) ([7fa5e7d](https://github.com/aquasecurity/trivy/commit/7fa5e7d0ab67f20d434b2922725988695e32e6af)) +* close file when failed to open gzip ([#7164](https://github.com/aquasecurity/trivy/issues/7164)) ([2a577a7](https://github.com/aquasecurity/trivy/commit/2a577a7bae37e5731dceaea8740683573b6b70a5)) +* **dotnet:** don't include non-runtime libraries into report for `*.deps.json` files ([#7039](https://github.com/aquasecurity/trivy/issues/7039)) ([5bc662b](https://github.com/aquasecurity/trivy/commit/5bc662be9a8f072599f90abfd3b400c8ab055ed6)) +* **dotnet:** show `nuget package dir not found` log only when checking `nuget` packages ([#7194](https://github.com/aquasecurity/trivy/issues/7194)) ([d76feba](https://github.com/aquasecurity/trivy/commit/d76febaee107c645e864da0f4d74a8f6ae4ad232)) +* ignore nodes when listing permission is not allowed ([#7107](https://github.com/aquasecurity/trivy/issues/7107)) ([25f8143](https://github.com/aquasecurity/trivy/commit/25f8143f120965c636c5ea8386398b211b082398)) +* **java:** avoid panic if deps from `pom` in `it` dir are not found ([#7245](https://github.com/aquasecurity/trivy/issues/7245)) ([4e54a7e](https://github.com/aquasecurity/trivy/commit/4e54a7e84c33c1be80c52c6db78c634bc3911715)) +* **java:** use `go-mvn-version` to remove `Package` duplicates ([#7088](https://github.com/aquasecurity/trivy/issues/7088)) ([a7a304d](https://github.com/aquasecurity/trivy/commit/a7a304d53e1ce230f881c28c4f35885774cf3b9a)) +* **misconf:** do not evaluate TF when a load error occurs ([#7109](https://github.com/aquasecurity/trivy/issues/7109)) ([f27c236](https://github.com/aquasecurity/trivy/commit/f27c236d6e155cb366aeef619b6ea96d20fb93da)) +* **nodejs:** detect direct dependencies when using `latest` version for files `yarn.lock` + `package.json` ([#7110](https://github.com/aquasecurity/trivy/issues/7110)) ([54bb8bd](https://github.com/aquasecurity/trivy/commit/54bb8bdfb934d114b5570005853bf4bc0d40c609)) +* **report:** hide empty table when all secrets/license/misconfigs are ignored ([#7171](https://github.com/aquasecurity/trivy/issues/7171)) ([c3036de](https://github.com/aquasecurity/trivy/commit/c3036de6d7719323d306a9666ccc8d928d936f9a)) +* **secret:** skip regular strings contain secret patterns ([#7182](https://github.com/aquasecurity/trivy/issues/7182)) ([174b1e3](https://github.com/aquasecurity/trivy/commit/174b1e3515a6394cf8d523216d6267c1aefb820a)) +* **secret:** trim excessively long lines ([#7192](https://github.com/aquasecurity/trivy/issues/7192)) ([92b13be](https://github.com/aquasecurity/trivy/commit/92b13be668bd20f8e9dac2f0cb8e5a2708b9b3b5)) +* **secret:** update length of `hugging-face-access-token` ([#7216](https://github.com/aquasecurity/trivy/issues/7216)) ([8c87194](https://github.com/aquasecurity/trivy/commit/8c87194f0a6b194bc5d340c8a65bd99a3132d973)) +* **server:** pass license categories to options ([#7203](https://github.com/aquasecurity/trivy/issues/7203)) ([9d52018](https://github.com/aquasecurity/trivy/commit/9d5201808da89607ae43570bdf1f335b482a6b79)) + + +### Performance Improvements + +* **debian:** use `bytes.Index` in `emptyLineSplit` to cut allocation ([#7065](https://github.com/aquasecurity/trivy/issues/7065)) ([acbec05](https://github.com/aquasecurity/trivy/commit/acbec053c985388a26d899e73b4b7f5a6d1fa210)) + +## [0.53.0](https://github.com/aquasecurity/trivy/compare/v0.52.0...v0.53.0) (2024-07-01) + + +### ⚠ BREAKING CHANGES + +* **k8s:** node-collector dynamic commands support ([#6861](https://github.com/aquasecurity/trivy/issues/6861)) +* add clean subcommand ([#6993](https://github.com/aquasecurity/trivy/issues/6993)) +* **aws:** Remove aws subcommand ([#6995](https://github.com/aquasecurity/trivy/issues/6995)) + +### Features + +* add clean subcommand ([#6993](https://github.com/aquasecurity/trivy/issues/6993)) ([8d0ae1f](https://github.com/aquasecurity/trivy/commit/8d0ae1f5de72d92a043dcd6b7c164d30e51b6047)) +* Add local ImageID to SARIF metadata ([#6522](https://github.com/aquasecurity/trivy/issues/6522)) ([f144e91](https://github.com/aquasecurity/trivy/commit/f144e912d34234f00b5a13b7a11a0019fa978b27)) +* add memory cache backend ([#7048](https://github.com/aquasecurity/trivy/issues/7048)) ([55ccd06](https://github.com/aquasecurity/trivy/commit/55ccd06df43f6ff28685f46d215ccb70f55916d2)) +* **aws:** Remove aws subcommand ([#6995](https://github.com/aquasecurity/trivy/issues/6995)) ([979e118](https://github.com/aquasecurity/trivy/commit/979e118a9e0ca8943bef9143f492d7eb1fd4d863)) +* **conda:** add licenses support for `environment.yml` files ([#6953](https://github.com/aquasecurity/trivy/issues/6953)) ([654217a](https://github.com/aquasecurity/trivy/commit/654217a65485ca0a07771ea61071977894eb4920)) +* **dart:** use first version of constraint for dependencies using SDK version ([#6239](https://github.com/aquasecurity/trivy/issues/6239)) ([042d6b0](https://github.com/aquasecurity/trivy/commit/042d6b08c283105c258a3dda98983b345a5305c3)) +* **image:** Set User-Agent header for Trivy container registry requests ([#6868](https://github.com/aquasecurity/trivy/issues/6868)) ([9b31697](https://github.com/aquasecurity/trivy/commit/9b31697274c8743d6e5a8f7a1a05daf60cd15910)) +* **java:** add support for `maven-metadata.xml` files for remote snapshot repositories. ([#6950](https://github.com/aquasecurity/trivy/issues/6950)) ([1f8fca1](https://github.com/aquasecurity/trivy/commit/1f8fca1fc77b989bb4e3ba820b297464dbdd825f)) +* **java:** add support for sbt projects using sbt-dependency-lock ([#6882](https://github.com/aquasecurity/trivy/issues/6882)) ([f18d035](https://github.com/aquasecurity/trivy/commit/f18d035ae13b281c96aa4ed69ca32e507d336e66)) +* **k8s:** node-collector dynamic commands support ([#6861](https://github.com/aquasecurity/trivy/issues/6861)) ([8d618e4](https://github.com/aquasecurity/trivy/commit/8d618e48a2f1b60c2e4c49cdd9deb8eb45c972b0)) +* **misconf:** add metadata to Cloud schema ([#6831](https://github.com/aquasecurity/trivy/issues/6831)) ([02d5404](https://github.com/aquasecurity/trivy/commit/02d540478d495416b50d7e8b187ff9f5bba41f45)) +* **misconf:** add support for AWS::EC2::SecurityGroupIngress/Egress ([#6755](https://github.com/aquasecurity/trivy/issues/6755)) ([55fa610](https://github.com/aquasecurity/trivy/commit/55fa6109cd0463fd3221aae41ca7b1d8c44ad430)) +* **misconf:** API Gateway V1 support for CloudFormation ([#6874](https://github.com/aquasecurity/trivy/issues/6874)) ([8491469](https://github.com/aquasecurity/trivy/commit/8491469f0b35bd9df706a433669f5b62239d4ef3)) +* **misconf:** support of selectors for all providers for Rego ([#6905](https://github.com/aquasecurity/trivy/issues/6905)) ([bc3741a](https://github.com/aquasecurity/trivy/commit/bc3741ae2c68cdd00fc0aef7e51985568b2eb78a)) +* **php:** add installed.json file support ([#4865](https://github.com/aquasecurity/trivy/issues/4865)) ([edc556b](https://github.com/aquasecurity/trivy/commit/edc556b85e3554c31e19b1ece189effb9ba2be12)) +* **plugin:** add support for nested archives ([#6845](https://github.com/aquasecurity/trivy/issues/6845)) ([622c67b](https://github.com/aquasecurity/trivy/commit/622c67b7647f94d0a0ca3acf711d8f847cdd8d98)) +* **sbom:** migrate to `CycloneDX v1.6` ([#6903](https://github.com/aquasecurity/trivy/issues/6903)) ([09e50ce](https://github.com/aquasecurity/trivy/commit/09e50ce6a82073ba62f1732d5aa0cd2701578693)) + + +### Bug Fixes + +* **c:** don't skip conan files from `file-patterns` and scan `.conan2` cache dir ([#6949](https://github.com/aquasecurity/trivy/issues/6949)) ([38b35dd](https://github.com/aquasecurity/trivy/commit/38b35dd3c804027e7a6e6a9d3c87b7ac333896c5)) +* **cli:** show info message only when --scanners is available ([#7032](https://github.com/aquasecurity/trivy/issues/7032)) ([e9fc3e3](https://github.com/aquasecurity/trivy/commit/e9fc3e3397564512038ddeca2adce0efcb3f93c5)) +* **cyclonedx:** trim non-URL info for `advisory.url` ([#6952](https://github.com/aquasecurity/trivy/issues/6952)) ([417212e](https://github.com/aquasecurity/trivy/commit/417212e0930aa52a27ebdc1b9370d2943ce0f8fa)) +* **debian:** take installed files from the origin layer ([#6849](https://github.com/aquasecurity/trivy/issues/6849)) ([089b953](https://github.com/aquasecurity/trivy/commit/089b953462260f01c40bdf588b2568ae0ef658bc)) +* **image:** parse `image.inspect.Created` field only for non-empty values ([#6948](https://github.com/aquasecurity/trivy/issues/6948)) ([0af5730](https://github.com/aquasecurity/trivy/commit/0af5730cbe56686417389c2fad643c1bdbb33999)) +* **license:** return license separation using separators `,`, `or`, etc. ([#6916](https://github.com/aquasecurity/trivy/issues/6916)) ([52f7aa5](https://github.com/aquasecurity/trivy/commit/52f7aa54b520a90a19736703f8ea63cc20fab104)) +* **misconf:** fix caching of modules in subdirectories ([#6814](https://github.com/aquasecurity/trivy/issues/6814)) ([0bcfedb](https://github.com/aquasecurity/trivy/commit/0bcfedbcaa9bbe30ee5ecade5b98e9ce3cc54c9b)) +* **misconf:** fix parsing of engine links and frameworks ([#6937](https://github.com/aquasecurity/trivy/issues/6937)) ([ec68c9a](https://github.com/aquasecurity/trivy/commit/ec68c9ab4580d057720179173d58734402c92af4)) +* **misconf:** handle source prefix to ignore ([#6945](https://github.com/aquasecurity/trivy/issues/6945)) ([c3192f0](https://github.com/aquasecurity/trivy/commit/c3192f061d7e84eaf38df8df7c879dc00b4ca137)) +* **misconf:** parsing numbers without fraction as int ([#6834](https://github.com/aquasecurity/trivy/issues/6834)) ([8141a13](https://github.com/aquasecurity/trivy/commit/8141a137ba50b553a9da877d95c7ccb491d041c6)) +* **nodejs:** fix infinite loop when package link from `package-lock.json` file is broken ([#6858](https://github.com/aquasecurity/trivy/issues/6858)) ([cf5aa33](https://github.com/aquasecurity/trivy/commit/cf5aa336e660e4c98481ebf8d15dd4e54c38581e)) +* **nodejs:** fix infinity loops for `pnpm` with cyclic imports ([#6857](https://github.com/aquasecurity/trivy/issues/6857)) ([7d083bc](https://github.com/aquasecurity/trivy/commit/7d083bc890eccc3bf32765c6d7e922cab2e2ef94)) +* **plugin:** respect `--insecure` ([#7022](https://github.com/aquasecurity/trivy/issues/7022)) ([3d02a31](https://github.com/aquasecurity/trivy/commit/3d02a31b44924f9e2495aae087f7ca9de3314db4)) +* **purl:** add missed os types ([#6955](https://github.com/aquasecurity/trivy/issues/6955)) ([2d85a00](https://github.com/aquasecurity/trivy/commit/2d85a003b22298d1101f84559f7c6b470f2b3909)) +* **python:** compare pkg names from `poetry.lock` and `pyproject.toml` in lowercase ([#6852](https://github.com/aquasecurity/trivy/issues/6852)) ([faa9d92](https://github.com/aquasecurity/trivy/commit/faa9d92cfeb8d924deda2dac583b6c97099c08d9)) +* **sbom:** don't overwrite `srcEpoch` when decoding SBOM files ([#6866](https://github.com/aquasecurity/trivy/issues/6866)) ([04af59c](https://github.com/aquasecurity/trivy/commit/04af59c2906bcfc7f7970b4e8f45a90f04313170)) +* **sbom:** fix panic when scanning SBOM file without root component into SBOM format ([#7051](https://github.com/aquasecurity/trivy/issues/7051)) ([3d4ae8b](https://github.com/aquasecurity/trivy/commit/3d4ae8b5be94cd9b00badeece8d86c2258b2cd90)) +* **sbom:** take pkg name from `purl` for maven pkgs ([#7008](https://github.com/aquasecurity/trivy/issues/7008)) ([a76e328](https://github.com/aquasecurity/trivy/commit/a76e3286c413de3dec55394fb41dd627dfee37ae)) +* **sbom:** use `purl` for `bitnami` pkg names ([#6982](https://github.com/aquasecurity/trivy/issues/6982)) ([7eabb92](https://github.com/aquasecurity/trivy/commit/7eabb92ec2e617300433445718be07ac74956454)) +* **sbom:** use package UIDs for uniqueness ([#7042](https://github.com/aquasecurity/trivy/issues/7042)) ([14d71ba](https://github.com/aquasecurity/trivy/commit/14d71ba63c39e51dd4179ba2d6002b46e1816e90)) +* **secret:** `Asymmetric Private Key` shouldn't start with space ([#6867](https://github.com/aquasecurity/trivy/issues/6867)) ([bb26445](https://github.com/aquasecurity/trivy/commit/bb26445e3df198df77930329f532ac5ab7a67af2)) +* **suse:** Add SLES 15.6 and Leap 15.6 ([#6964](https://github.com/aquasecurity/trivy/issues/6964)) ([5ee4e9d](https://github.com/aquasecurity/trivy/commit/5ee4e9d30ea814f60fd5705361cabf2e83a47a78)) +* use embedded when command path not found ([#7037](https://github.com/aquasecurity/trivy/issues/7037)) ([137c916](https://github.com/aquasecurity/trivy/commit/137c9164238ffd989a0c5ed24f23a55bbf341f6e)) + +## [0.52.0](https://github.com/aquasecurity/trivy/compare/v0.51.1...v0.52.0) (2024-06-03) + + +### Features + +* Add Julia language analyzer support ([#5635](https://github.com/aquasecurity/trivy/issues/5635)) ([fecafb1](https://github.com/aquasecurity/trivy/commit/fecafb1fc5bb129c7485342a0775f0dd8bedd28e)) +* add support for plugin index ([#6674](https://github.com/aquasecurity/trivy/issues/6674)) ([26faf8f](https://github.com/aquasecurity/trivy/commit/26faf8f3f04b1c5f9f81c03ffc6b2008732207e2)) +* **misconf:** Add support for deprecating a check ([#6664](https://github.com/aquasecurity/trivy/issues/6664)) ([88702cf](https://github.com/aquasecurity/trivy/commit/88702cfd5918b093defc5b5580f7cbf16f5f2417)) +* **misconf:** add Terraform 'removed' block to schema ([#6640](https://github.com/aquasecurity/trivy/issues/6640)) ([b7a0a13](https://github.com/aquasecurity/trivy/commit/b7a0a131a03ed49c08d3b0d481bc9284934fd6e1)) +* **misconf:** register builtin Rego funcs from trivy-checks ([#6616](https://github.com/aquasecurity/trivy/issues/6616)) ([7c22ee3](https://github.com/aquasecurity/trivy/commit/7c22ee3df5ee51beb90e44428a99541b3d19ab98)) +* **misconf:** resolve tf module from OpenTofu compatible registry ([#6743](https://github.com/aquasecurity/trivy/issues/6743)) ([ac74520](https://github.com/aquasecurity/trivy/commit/ac7452009bf7ca0fa8ee1de8807c792eabad405a)) +* **misconf:** support for VPC resources for inbound/outbound rules ([#6779](https://github.com/aquasecurity/trivy/issues/6779)) ([349caf9](https://github.com/aquasecurity/trivy/commit/349caf96bc3dd81551d488044f1adfdb947f39fb)) +* **misconf:** support symlinks inside of Helm archives ([#6621](https://github.com/aquasecurity/trivy/issues/6621)) ([4eae37c](https://github.com/aquasecurity/trivy/commit/4eae37c52b035b3576361c12f70d3d9517d0a73c)) +* **nodejs:** add v9 pnpm lock file support ([#6617](https://github.com/aquasecurity/trivy/issues/6617)) ([1e08648](https://github.com/aquasecurity/trivy/commit/1e0864842e32a709941d4b4e8f521602bcee684d)) +* **plugin:** specify plugin version ([#6683](https://github.com/aquasecurity/trivy/issues/6683)) ([d6dc567](https://github.com/aquasecurity/trivy/commit/d6dc56732babbc9d7f788c280a768d8648aa093d)) +* **python:** add license support for `requirement.txt` files ([#6782](https://github.com/aquasecurity/trivy/issues/6782)) ([29615be](https://github.com/aquasecurity/trivy/commit/29615be85e8bfeaf5a0cd51829b1898c55fa4274)) +* **python:** add line number support for `requirement.txt` files ([#6729](https://github.com/aquasecurity/trivy/issues/6729)) ([2bc54ad](https://github.com/aquasecurity/trivy/commit/2bc54ad2752aba5de4380cb92c13b09c0abefd73)) +* **report:** Include licenses and secrets filtered by rego to ModifiedFindings ([#6483](https://github.com/aquasecurity/trivy/issues/6483)) ([fa3cf99](https://github.com/aquasecurity/trivy/commit/fa3cf993eace4be793f85907b42365269c597b91)) +* **vex:** improve relationship support in CSAF VEX ([#6735](https://github.com/aquasecurity/trivy/issues/6735)) ([a447f6b](https://github.com/aquasecurity/trivy/commit/a447f6ba94b6f8b14177dc5e4369a788e2020d90)) +* **vex:** support non-root components for products in OpenVEX ([#6728](https://github.com/aquasecurity/trivy/issues/6728)) ([9515695](https://github.com/aquasecurity/trivy/commit/9515695d45e9b5c20890e27e21e3ab45bfd4ce5f)) + + +### Bug Fixes + +* clean up golangci lint configuration ([#6797](https://github.com/aquasecurity/trivy/issues/6797)) ([62de6f3](https://github.com/aquasecurity/trivy/commit/62de6f3feba6e4c56ad3922441d5b0f150c3d6b7)) +* **cli:** always output fatal errors to stderr ([#6827](https://github.com/aquasecurity/trivy/issues/6827)) ([c2b9132](https://github.com/aquasecurity/trivy/commit/c2b9132a7e933a68df4cc0eb86aab23719ded1b5)) +* close APKINDEX archive file ([#6672](https://github.com/aquasecurity/trivy/issues/6672)) ([5caf437](https://github.com/aquasecurity/trivy/commit/5caf4377f3a7fcb1f6e1a84c67136ae62d100be3)) +* close settings.xml ([#6768](https://github.com/aquasecurity/trivy/issues/6768)) ([9c3e895](https://github.com/aquasecurity/trivy/commit/9c3e895fcb0852c00ac03ed21338768f76b5273b)) +* close testfile ([#6830](https://github.com/aquasecurity/trivy/issues/6830)) ([aa0c413](https://github.com/aquasecurity/trivy/commit/aa0c413814e8915b38d2285c6a8ba5bc3f0705b4)) +* **conda:** add support `pip` deps for `environment.yml` files ([#6675](https://github.com/aquasecurity/trivy/issues/6675)) ([150a773](https://github.com/aquasecurity/trivy/commit/150a77313e980cd63797a89a03afcbc97b285f38)) +* **go:** add only non-empty root modules for `gobinaries` ([#6710](https://github.com/aquasecurity/trivy/issues/6710)) ([c96f2a5](https://github.com/aquasecurity/trivy/commit/c96f2a5b3de820da37e14594dd537c3b0949ae9c)) +* **go:** include only `.version`|`.ver` (no prefixes) ldflags for `gobinaries` ([#6705](https://github.com/aquasecurity/trivy/issues/6705)) ([afb4f9d](https://github.com/aquasecurity/trivy/commit/afb4f9dc4730671ba004e1734fa66422c4c86dad)) +* Golang version parsing from binaries w/GOEXPERIMENT ([#6696](https://github.com/aquasecurity/trivy/issues/6696)) ([696f2ae](https://github.com/aquasecurity/trivy/commit/696f2ae0ecdd4f90303f41249924a09ace70dd78)) +* include packages unless it is not needed ([#6765](https://github.com/aquasecurity/trivy/issues/6765)) ([56dbe1f](https://github.com/aquasecurity/trivy/commit/56dbe1f6768fe67fbc1153b74fde0f83eaa1b281)) +* **misconf:** don't shift ignore rule related to code ([#6708](https://github.com/aquasecurity/trivy/issues/6708)) ([39a746c](https://github.com/aquasecurity/trivy/commit/39a746c77837f873e87b81be40676818030f44c5)) +* **misconf:** skip Rego errors with a nil location ([#6638](https://github.com/aquasecurity/trivy/issues/6638)) ([a2c522d](https://github.com/aquasecurity/trivy/commit/a2c522ddb229f049999c4ce74ef75a0e0f9fdc62)) +* **misconf:** skip Rego errors with a nil location ([#6666](https://github.com/aquasecurity/trivy/issues/6666)) ([a126e10](https://github.com/aquasecurity/trivy/commit/a126e1075a44ef0e40c0dc1e214d1c5955f80242)) +* node-collector high and critical cves ([#6707](https://github.com/aquasecurity/trivy/issues/6707)) ([ff32deb](https://github.com/aquasecurity/trivy/commit/ff32deb7bf9163c06963f557228260b3b8c161ed)) +* **plugin:** initialize logger ([#6836](https://github.com/aquasecurity/trivy/issues/6836)) ([728e77a](https://github.com/aquasecurity/trivy/commit/728e77a7261dc3fcda1e61e79be066c789bbba0c)) +* **python:** add package name and version validation for `requirements.txt` files. ([#6804](https://github.com/aquasecurity/trivy/issues/6804)) ([ea3a124](https://github.com/aquasecurity/trivy/commit/ea3a124fc7162c30c7f1a59bdb28db0b3c8bb86d)) +* **report:** hide empty tables if all vulns has been filtered ([#6352](https://github.com/aquasecurity/trivy/issues/6352)) ([3d388d8](https://github.com/aquasecurity/trivy/commit/3d388d8552ef42d4d54176309a38c1879008527b)) +* **sbom:** fix panic for `convert` mode when scanning json file derived from sbom file ([#6808](https://github.com/aquasecurity/trivy/issues/6808)) ([f92ea09](https://github.com/aquasecurity/trivy/commit/f92ea096856c7c262b05bd4d31c62689ebafac82)) +* use of specified context to obtain cluster name ([#6645](https://github.com/aquasecurity/trivy/issues/6645)) ([39ebed4](https://github.com/aquasecurity/trivy/commit/39ebed45f8c218509d264bd3f3ca548fc33d2b3a)) + + +### Performance Improvements + +* **misconf:** parse rego input once ([#6615](https://github.com/aquasecurity/trivy/issues/6615)) ([67c6b1d](https://github.com/aquasecurity/trivy/commit/67c6b1d473999003d682bdb42657bbf3a4a69a9c)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000000..b31eff4a97e3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +See [Issues](https://trivy.dev/docs/latest/community/contribute/issue/) and [Pull Requests](https://trivy.dev/docs/latest/community/contribute/pr/) \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2aa304aac953..40468c7653e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,14 +1,5 @@ -FROM golang:1.12-alpine AS builder -ADD go.mod go.sum /app/ -WORKDIR /app/ -RUN apk --no-cache add git -RUN go mod download -ADD . /app/ -RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -o /trivy cmd/trivy/main.go - -FROM alpine:3.9 +FROM alpine:3.23.3 RUN apk --no-cache add ca-certificates git -COPY --from=builder /trivy /usr/local/bin/trivy -RUN chmod +x /usr/local/bin/trivy - -CMD ["trivy"] +COPY trivy /usr/local/bin/trivy +COPY contrib/*.tpl contrib/ +ENTRYPOINT ["trivy"] diff --git a/Dockerfile.canary b/Dockerfile.canary new file mode 100644 index 000000000000..2ad7394eb2f0 --- /dev/null +++ b/Dockerfile.canary @@ -0,0 +1,11 @@ +FROM alpine:3.23.3 +RUN apk --no-cache add ca-certificates git + +# binaries were created with GoReleaser +# need to copy binaries from folder with correct architecture +# example architecture folder: dist/trivy_canary_build_linux_arm64/trivy +# GoReleaser adds _v* to the folder name, but only when GOARCH is amd64 +ARG TARGETARCH +COPY "dist/trivy_canary_build_linux_${TARGETARCH}*/trivy" /usr/local/bin/trivy +COPY contrib/*.tpl contrib/ +ENTRYPOINT ["trivy"] diff --git a/LICENSE b/LICENSE index 7ce066ea3308..261eeb9e9f8b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,201 @@ -MIT License - -Copyright (c) 2019 Teppei Fukuda - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000000..3fe97bf7d4b0 --- /dev/null +++ b/NOTICE @@ -0,0 +1,4 @@ +Trivy +Copyright 2019-2020 Aqua Security Software Ltd. + +This product includes software developed by Aqua Security (https://aquasec.com). diff --git a/README.md b/README.md index d629d3ed4a8c..756d7d600e83 100644 --- a/README.md +++ b/README.md @@ -1,255 +1,147 @@ - +
+ -[![GitHub release](https://img.shields.io/github/release/knqyf263/trivy.svg)](https://github.com/knqyf263/trivy/releases/latest) -[![CircleCI](https://circleci.com/gh/knqyf263/trivy.svg?style=svg)](https://circleci.com/gh/knqyf263/trivy) -[![Go Report Card](https://goreportcard.com/badge/github.com/knqyf263/trivy)](https://goreportcard.com/report/github.com/knqyf263/trivy) -[![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](https://github.com/knqyf263/trivy/blob/master/LICENSE) +[![GitHub Release][release-img]][release] +[![Test][test-img]][test] +[![Go Report Card][go-report-img]][go-report] +[![License: Apache-2.0][license-img]][license] +[![GitHub Downloads][github-downloads-img]][release] +![Docker Pulls][docker-pulls] -A Simple and Comprehensive Vulnerability Scanner for Containers, Compatible with CI +[📖 Documentation][docs] +
-# Abstract -`Trivy` is a simple and comprehensive vulnerability scanner for containers. -`Trivy` detects vulnerabilities of OS packages (Alpine, RHEL, CentOS, etc.) and application dependencies (Bundler, Composer, npm, etc.). -`Trivy` is easy to use. Just install the binary and you're ready to scan. It can be scanned just by specifying a container image name. +Trivy ([pronunciation][pronunciation]) is a comprehensive and versatile security scanner. +Trivy has *scanners* that look for security issues, and *targets* where it can find those issues. -It is considered to be used in CI. Before pushing to a container registry, you can scan your local container image easily. -See [here](#continuous-integration-ci) for details. +Targets (what Trivy can scan): +- Container Image +- Filesystem +- Git Repository (remote) +- Virtual Machine Image +- Kubernetes -# Features -- Detect comprehensive vulnerabilities - - OS packages (Alpine, Red Hat Enterprise Linux, CentOS, Debian, Ubuntu) - - **Application dependencies** (Bundler, Composer, Pipenv, npm) -- Simple - - Specify only an image name -- Easy installation - - **No need for prerequirements** such as installation of DB, libraries, etc. - - `apt-get install`, `yum install` and `brew install` is possible (See [Installation](#installation)) -- High accuracy - - Especially Alpine -- **Compatible with CI** - - See [CI Example](#continuous-integration-ci) +Scanners (what Trivy can find there): +- OS packages and software dependencies in use (SBOM) +- Known vulnerabilities (CVEs) +- IaC issues and misconfigurations +- Sensitive information and secrets +- Software licenses -# Installation +Trivy supports most popular programming languages, operating systems, and platforms. For a complete list, see the [Scanning Coverage] page. -## RHEL/CentOS +To learn more, go to the [Trivy homepage][homepage] for feature highlights, or to the [Documentation site][docs] for detailed information. -Add repository setting to `/etc/yum.repos.d`. +## Quick Start -``` -$ sudo vim /etc/yum.repos.d/trivy.repo -[trivy] -name=Trivy repository -baseurl=https://knqyf263.github.io/trivy-repo/rpm/releases/$releasever/$basearch/ -gpgcheck=0 -enabled=1 -$ sudo yum -y update -$ sudo yum -y install trivy -``` +### Get Trivy -or - -``` -$ rpm -ivh https://github.com/knqyf263/trivy/releases/download/v0.0.3/trivy_0.0.3_Linux-64bit.rpm -``` +Trivy is available in most common distribution channels. The full list of installation options is available in the [Installation] page. Here are a few popular examples: -## Debian/Ubuntu +- `brew install trivy` +- `docker run aquasec/trivy` +- Download binary from +- See [Installation] for more -Replace `[CODE_NAME]` with your code name +Trivy is integrated with many popular platforms and applications. The complete list of integrations is available in the [Ecosystem] page. Here are a few popular examples: -CODE_NAME: wheezy, jessie, stretch, buster, trusty, xenial, bionic +- [GitHub Actions](https://github.com/aquasecurity/trivy-action) +- [Kubernetes operator](https://github.com/aquasecurity/trivy-operator) +- [VS Code plugin](https://github.com/aquasecurity/trivy-vscode-extension) +- See [Ecosystem] for more -``` -$ sudo apt-get install apt-transport-https gnupg -$ wget -qO - https://knqyf263.github.io/trivy-repo/deb/public.key | sudo apt-key add - -$ echo deb https://knqyf263.github.io/trivy-repo/deb [CODE_NAME] main | sudo tee -a /etc/apt/sources.list -$ sudo apt-get update -$ sudo apt-get install trivy -``` +### Canary builds +There are canary builds ([Docker Hub](https://hub.docker.com/r/aquasec/trivy/tags?page=1&name=canary), [GitHub](https://github.com/aquasecurity/trivy/pkgs/container/trivy/75776514?tag=canary), [ECR](https://gallery.ecr.aws/aquasecurity/trivy#canary) images and [binaries](https://github.com/aquasecurity/trivy/actions/workflows/canary.yaml)) generated with every push to the main branch. -or +Please be aware: canary builds might have critical bugs, so they are not recommended for use in production. -``` -$ sudo apt-get install rpm -$ wget https://github.com/knqyf263/trivy/releases/download/v0.0.3/trivy_0.0.3_Linux-64bit.deb -$ sudo dpkg -i trivy_0.0.3_Linux-64bit.deb -``` +### General usage -## Mac OS X / Homebrew -You can use homebrew on OS X. -``` -$ brew tap knqyf263/trivy -$ brew install knqyf263/trivy/trivy +```bash +trivy [--scanners ] ``` -## Binary (Including Windows) -Go to [the releases page](https://github.com/knqyf263/trivy/releases), find the version you want, and download the zip file. Unpack the zip file, and put the binary to somewhere you want (on UNIX-y systems, /usr/local/bin or the like). Make sure it has execution bits turned on. +Examples: -## From source - -```sh -$ go get -u github.com/knqyf263/trivy +```bash +trivy image python:3.4-alpine ``` -# Examples -## Continuous Integration (CI) -Scan your image built in Travis CI/CircleCI. The test will fail if a vulnerability is found. When you don't want to fail the test, specify `--exit-code 0` . - -**Note**: The first time take a while (faster by cache after the second time) -### Travis CI +
+Result -``` -$ cat .travis.yml -services: - - docker - -before_install: - - docker build -t trivy-ci-test:latest . - - wget https://github.com/knqyf263/trivy/releases/download/v0.0.3/trivy_0.0.3_Linux-64bit.tar.gz - - tar zxvf trivy_0.0.3_Linux-64bit.tar.gz -script: - - ./trivy --exit-code 1 --quiet trivy-ci-test:latest -cache: - directories: - - $HOME/.cache/trivy -``` +https://user-images.githubusercontent.com/1161307/171013513-95f18734-233d-45d3-aaf5-d6aec687db0e.mov -example: https://travis-ci.org/knqyf263/trivy-ci-test -repository: https://github.com/knqyf263/trivy-ci-test +
-### Circle CI - -``` -$ cat .circleci/config.yml -jobs: - build: - docker: - - image: docker:18.09-git - steps: - - checkout - - setup_remote_docker - - restore_cache: - key: vulnerability-db - - run: - name: Build image - command: docker build -t trivy-ci-test:latest . - - run: - name: Install trivy - command: | - wget https://github.com/knqyf263/trivy/releases/download/v0.0.4/trivy_0.0.4_Linux-64bit.tar.gz - tar zxvf trivy_0.0.4_Linux-64bit.tar.gz - mv trivy /usr/local/bin - - run: - name: Scan the local image with trivy - command: trivy --exit-code 1 --quiet trivy-ci-test:latest - - save_cache: - key: vulnerability-db - paths: - - $HOME/.cache/trivy -workflows: - version: 2 - release: - jobs: - - build +```bash +trivy fs --scanners vuln,secret,misconfig myproject/ ``` -example: https://circleci.com/gh/knqyf263/trivy-ci-test -repository: https://github.com/knqyf263/trivy-ci-test +
+Result -# Usage +https://user-images.githubusercontent.com/1161307/171013917-b1f37810-f434-465c-b01a-22de036bd9b3.mov -``` -NAME: - trivy - A simple and comprehensive vulnerability scanner for containers -USAGE: - trivy [options] image_name -VERSION: - 0.0.3 -OPTIONS: - --format value, -f value format (table, json) (default: "table") - --input value, -i value input file path instead of image name - --severity value, -s value severities of vulnerabilities to be displayed (comma separated) (default: "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL") - --output value, -o value output file name - --exit-code value Exit code when vulnerabilities were found (default: 0) - --skip-update skip db update - --clean, -c clean all cache - --quiet, -q suppress progress bar - --ignore-unfixed display only fixed vulnerabilities - --refresh refresh DB (usually used after version update of trivy - --debug, -d debug mode - --help, -h show help - --version, -v print the version -``` +
-# Q&A -## Homebrew -### Error: Your macOS keychain GitHub credentials do not have sufficient scope! - -``` -$ brew tap knqyf263/trivy -Error: Your macOS keychain GitHub credentials do not have sufficient scope! -Scopes they need: none -Scopes they have: -Create a personal access token: - https://github.com/settings/tokens/new?scopes=gist,public_repo&description=Homebrew -echo 'export HOMEBREW_GITHUB_API_TOKEN=your_token_here' >> ~/.zshrc +```bash +trivy k8s --report summary cluster ``` -Try: -``` -$ printf "protocol=https\nhost=github.com\n" | git credential-osxkeychain erase -``` +
+Result -### Error: knqyf263/trivy/trivy 64 already installed +![k8s summary](docs/imgs/trivy-k8s.png) -``` -$ brew upgrade -... -Error: knqyf263/trivy/trivy 64 already installed -``` +
-Try: +## FAQ -``` -$ brew unlink trivy && brew uninstall trivy -($ rm -rf /usr/local/Cellar/trivy/64) -$ brew install knqyf263/trivy/trivy -``` +### How to pronounce the name "Trivy"? -## Others -### Detected version update of trivy. Please try again with --refresh option -Try again with `--refresh` option +`tri` is pronounced like **tri**gger, `vy` is pronounced like en**vy**. -``` -$ trivy --refresh alpine:3.9 -``` +## Want more? Check out Aqua -### Unknown error -Try again with `--clean` option - -``` -$ trivy --clean -``` +If you liked Trivy, you will love Aqua which builds on top of Trivy to provide even more enhanced capabilities for a complete security management offering. +You can find a high level comparison table specific to Trivy users [here](https://trivy.dev/docs/latest/commercial/compare/). +In addition check out the website for more information about our products and services. +If you'd like to contact Aqua or request a demo, please use this form: -# Contribute +## Community -1. fork a repository: github.com/knqyf263/trivy to github.com/you/repo -2. get original code: `go get github.com/knqyf263/trivy` -3. work on original code -4. add remote to your repo: git remote add myfork https://github.com/you/repo.git -5. push your changes: git push myfork -6. create a new Pull Request +Trivy is an [Aqua Security][aquasec] open source project. +Learn about our open source work and portfolio [here][oss]. +Contact us about any matter by opening a GitHub Discussion [here][discussions] -- see [GitHub and Go: forking, pull requests, and go-getting](http://blog.campoy.cat/2014/03/github-and-go-forking-pull-requests-and.html) +Please ensure to abide by our [Code of Conduct][code-of-conduct] during all interactions. ----- +[test]: https://github.com/aquasecurity/trivy/actions/workflows/test.yaml +[test-img]: https://github.com/aquasecurity/trivy/actions/workflows/test.yaml/badge.svg +[go-report]: https://goreportcard.com/report/github.com/aquasecurity/trivy +[go-report-img]: https://goreportcard.com/badge/github.com/aquasecurity/trivy +[release]: https://github.com/aquasecurity/trivy/releases +[release-img]: https://img.shields.io/github/release/aquasecurity/trivy.svg?logo=github +[github-downloads-img]: https://img.shields.io/github/downloads/aquasecurity/trivy/total?logo=github +[docker-pulls]: https://img.shields.io/docker/pulls/aquasec/trivy?logo=docker&label=docker%20pulls%20%2F%20trivy +[license]: https://github.com/aquasecurity/trivy/blob/main/LICENSE +[license-img]: https://img.shields.io/badge/License-Apache%202.0-blue.svg +[homepage]: https://trivy.dev +[docs]: https://trivy.dev/docs/latest/ +[pronunciation]: #how-to-pronounce-the-name-trivy +[code-of-conduct]: https://github.com/aquasecurity/community/blob/main/CODE_OF_CONDUCT.md -# Credits -Special thanks to [Tomoya Amachi](https://github.com/tomoyamachi) +[Installation]:https://trivy.dev/docs/latest/getting-started/installation/ +[Ecosystem]: https://trivy.dev/docs/latest/ecosystem/ +[Scanning Coverage]: https://trivy.dev/docs/latest/coverage/ -# License -MIT +[alpine]: https://ariadne.space/2021/06/08/the-vulnerability-remediation-lifecycle-of-alpine-containers/ +[rego]: https://www.openpolicyagent.org/docs/latest/#rego +[sigstore]: https://www.sigstore.dev/ -# Author -Teppei Fukuda (knqyf263) +[aquasec]: https://aquasec.com +[oss]: https://www.aquasec.com/products/open-source-projects/ +[discussions]: https://github.com/aquasecurity/trivy/discussions diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000000..3d20210454f4 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +# Security Policy + +## Supported Versions + +This is an open source project that is provided as-is without warranty or liability. +As such, there is no supportability commitment. The maintainers will do the best they can to address any report promptly and responsibly. + +## Reporting a Vulnerability + +Please use the "Private vulnerability reporting" feature in the GitHub repository (under the "Security" tab). + +⚠️ **Important:** +This policy is intended for vulnerabilities in **Trivy itself** (e.g., core functionality, scanning logic, or security features). + +If you discover a vulnerability in a **dependency module** (e.g., a third-party library used by Trivy), please **do not report it here**. +Instead, open a ticket in [GitHub Discussions](https://github.com/aquasecurity/trivy/discussions) so that the maintainers and community can evaluate and address it appropriately. + diff --git a/brand/Trivy-OSS-Logo-Color-Horizontal-RGB.png b/brand/Trivy-OSS-Logo-Color-Horizontal-RGB.png new file mode 100644 index 000000000000..04ae7cd4e9ad Binary files /dev/null and b/brand/Trivy-OSS-Logo-Color-Horizontal-RGB.png differ diff --git a/brand/Trivy-OSS-Logo-Color-Horizontal-RGB.svg b/brand/Trivy-OSS-Logo-Color-Horizontal-RGB.svg new file mode 100644 index 000000000000..9cdd1b594dd9 --- /dev/null +++ b/brand/Trivy-OSS-Logo-Color-Horizontal-RGB.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/brand/Trivy-OSS-Logo-Color-Stacked-RGB.png b/brand/Trivy-OSS-Logo-Color-Stacked-RGB.png new file mode 100644 index 000000000000..de36d4fcd7d3 Binary files /dev/null and b/brand/Trivy-OSS-Logo-Color-Stacked-RGB.png differ diff --git a/brand/Trivy-OSS-Logo-Color-Stacked-RGB.svg b/brand/Trivy-OSS-Logo-Color-Stacked-RGB.svg new file mode 100644 index 000000000000..fad8b53fbab5 --- /dev/null +++ b/brand/Trivy-OSS-Logo-Color-Stacked-RGB.svg @@ -0,0 +1,3206 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + v82+dTLO1RDGbbAO/HPo4KImk2Xnuvyid29h9OyZ22RhLmhaMpe2sjFul4m5sNVjWbebLNv2ybJn +fX6RM5txvnU2jvfu7yTi2TJ8azXP415t1On1RJaGPYl+/+cR0BtpHtpFnYVeSFOw4xze9Rc7NO1L +3V1G43WjgSwB+48l3++Z3Kt1FPloI1DD3sSK+JNGDe8eR77f05h343zrPg54zq4hfPNEmYL3E+si +Dpjux/Cp0S5wdhkNYFy9dOq9E3hwtIcmCW2bQDs/0ydXy/St1S5pfutyxt1m28y4OuI7u4ZQrsfo +sc0xe210TJ7bLMMHN/Mw6vkeyrz+Mocm8y5rs2tdWSaPrc/wwXkZPLc6Bm+NnuFj+zByaXVW7csu +3ZnrsjZvZQLZaB1FPH9TeEfzNO55n0c+ewcx79YR13Vl9No8zd+bHUT5ZxdVHn4iTEJ7qfR7D10S +/hvDu18TGPdtCul8jeBctymk8zpgPJuJ9HtTmZi0iyoPbRrAN28zOPeLPAf9DqLeh8FLm3HAczbR +5aC99Bm+mUDBXaPNQlvp83sndYLtHkg9G+ewzt5B1LNvvnN+pg+unvGDq4s2D7sSqLc7JCnYDYoM +vHG+dbYMYNvsAtfLzjhweNG7uTOEbnTWrZdxLQYNfMOEL77FoIHfZWBmZWEwf2famTAbF2ZPbZuN ++2M8bawDN9OGxQSyjdkkxnFtwHTyyx1aWYwemzb7dpPxPraBk2HQwMu8LvAaDENWjiuWxeuKucjN +Yrxj2MA1QJjAx7y+sng2WUzfmlaGsI0rU9jGnTFsm2UO2bYub1wt/2FgXHsBA/dhGlzaxjq4qJWJ +uaDJY9k03yHrRpNl3Wgyl7VtB0532MA7WPji9AYNNhvqnC0trSosJgdoe2gXmLCYqJiqmLiqmKqY +sJi4qJio3momKiYrLSYurSqmm0qKiYqJiunniNVMYb2DBTY1uD62Cw1yMtRMVUwJonLy2Pb00LQO +7qayrjgwUWVpYU11YWltSeXQMVFlaW0xVWlt7aGRMa3VfeXIOW05HKiupqiwsLCyrqa6uqy0sqa6 +rLqs6KqsrKysrLaytKiytq6quKawqLq6uKawsqi2sriupq6ouK6otq6usKistKauurSyrKqourSs +rqimrKqorKy6tLS4qLq6sLI4bHFtZW1xaXFRdWVdWXFRcXFpWU1tbVFtbW1VUVlVXV1tYV1hdWld +TW1pWWVddV1tcbDa0urasrra0rLSssq6utLS4rqikhXktOVYqMq60sLSstLq6mrLsXA1tbVVpVWl +tcWlNVWldaW2tWXFRWd1gamqb11xUW1tbXFdVW1VbW1ldVVtWW1tVVVtcW1RXWlNWW1tTVltcU1t +WW1tbW1dSW1tUU1hbW1hbWFdVWVtZWVdaV1xdWFhdXVtTWlpbW1tbW1VWV1RVXFtYU1tbW1dXWVR +aVlZbVlxUU1tbW1hdWFVTW1pZV1hbVVpSU1lbUlNZWVtaWFtbV1taV1tbW1dSW1pTVFtVU1pdU11 +aXVpdU1xbWFxTWVpaXV1TWlpaW1RbU1paWlpaW1laXVVZWVpbW1NZWlpTWVhYWVhaW1pTWldaVlN +XVFVTV1paXVZWVm4srrK6urCutLaotLaupK60rLS0pqyyrKy0sqy0tri0sKqwpqy0tqy0tLK4qK6 +4sLi2rLi0sqqstrSotKi0srqyurqysrKyqKyyuKi6srK4uLKyqq62sqqmsrayrLCysqa0srSusra +yprSyurC4srq6prK4rqaysrayuLamsLK0prCyqLKysrK2rq66rLK0rLK6trK4rriysqq0uri6uLa +quLK2uLCwtqisrrCusLS2rqSwrLKwqrCypqqwtrKqsLCwqrCyqLC4kCFdcU1RYXVlcV1hYU1lcV1 +tbVVdVWV1VXFpdWFNbWlRaWVlTWldZXFZTWVFZF6WAJAGvgkAcs7KmPJPiZMQBWFbHIiAjnFNPSB +A9OLo4amN0cBmoc9YGwi9ljFLPRRQ/OwRwvORBysm+IdqZrhHB80xTdQNxV1hIJKEln0FBEFLhJK +FVuxSuwPb1niIalc+ZUxspgFjgOgrRsnQJZhHIshaUuoF04BfxReLs8k8LKtBpzpx4kHvTBxID7E +c/gOr+E2vIbj8Buew4NgiLiTEBM/QGSAN2By4nBaClBIPMEcjFeAZSEgxQaqBDyAMfGQDBMycdiB +RzUQzAeEoYapAHjQMDZWAoaIpQByaHiJJaVfkySVf7FGPC+XEIKvYKH/AiBF1nxxJa2T0Qem5yIP +UE3EHSE0wTpGeCrqIBWtHII4GjmE8VMRtrMTMQhppiGPF5eDsDcwDXvowDTUgcNTrMNVk4eUszsE +dPi1sY6ZzwBTuH5MpMODcQwNQUIaK/A7siw+IUECjoVkgE/9MHEoGiTOBEPEjbI8XOglhxfxH17E +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 +Lzi8h7eJOcAw9rEhcLzAdm6MhayPpXBoG2sRg+xhgJ1ZCERK52EPJZvfGy00vTpebHp3oND04pjB +KaaRYlN8Y5WTMcfnZiKOVM5EHKydiDRoeHIeAfz8NPLH6aEJXGYBTdQmh0DpKlqR0t/GWsQUjEN8 +BTl5pMDnJCzgUDRBnIbbcBgew124S5a/uLjkcB++w4F4Dc954TKccxdOw294EA4T5xDhCqkG5wW6 +8QDGMr1MMCn5ohABpVxy+LhkAD2ySQD8ZusTuc3EHp+aXh4lMg9zsNAE6xgBSgkk0dKFFbTPSSHi +S8MdMjALfczYROThuhnWwYrpvYFjU2zD9fNDE7lIJlhstvER7eF4i+w31qIVIqJ4ga9lTXEGPUQ8 +iOdwnawM1+EzvMdlhufwGU7DYbhMlnPOOc/yF84555xzzrkLh+EyvIbf8BqOw2P4DM/yFz7DbzgO +5+E+3IhHFUXxOLMIYmMqGjq9sZ2Wfk5sWAIyoeHZ2cQPfEAAZMckCHhHKQDooZFDGjEJYXVseneg +0Pz2IKH5zVEiE0yjReb3hgrN740Ym4g2TGyKb6xuJuJI5WTE8bm5yKNz0/FGp6bjDc1NyAI+NSNl +aIZ3YPELxiQuZCHnB/xbUfEEOUoch+NwGB7DXzgMl+Ez3IbfcBxew2F4DIfhMDzLXfgLf+E1PIbD +cBgOw2P4DKfhOpyHMy09vQjCfQGxhsGEXhM3ZPHQvniUFAPOBIWAW3BQ5RJoR/ipIbaQNc5xQW48 +ADFMUylzAVtMZTl1OXTO25yWVA6d1tZUVZXWltSWVhcXl5qV1hZbF5vUVppalxRbGxeWFFUXG5fU +mhbb2pWVmpoW2laXFVYVFduaWpZUWpVVl1QX1xqW1NpVFpdUlRUWW1rbldbV1hYTlhUeh6osrS2m +qqwsJrOnnAdob3dzXl1oaWprV1ZrXWpoVVJoV1tpUldYbVhSWVxUbVJYVFtqaGpVaUxzaFhWU1Qy +2h7aBT80LZsbGt0GJiyrDV5sa3ZqcHVyA6YEVVVpXVRUWFVqWVxqUlVaVF1SWVlXWFJobWhrUm1V +aVVrbVxVURc2yHt5XmZUVRu8opgeMCUw0zKjMoticsB0YYPMoUPdQCwnj21PD02roc5B20O7sFb3 +9fQ0bqDC/1D6JQgB+bNkbNZPIRZl/NZmGT83GkGIyTmsSWvvEOW1i0U5wZUA5VULFDnYY+7ciDQJ +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 +D5qxKhdwFiKOUEo8iBUaqHXX7xn8M4huthBo/CT+txWLjsVPlVYOmUy4hiySvqOdAK1QAzaGSlAK +0LK+shG1gxOGaO04Wkf7aFfE+N4H5w8PMjSJ8DCqGrimxGHh8refOczAAXlt9w6i9D8WpmftKIz3 +gaseXEWXnLo2a7NJEH3KY7LaIjS3oqFhbKoKdhuiWxkjHMo9hkaPRfRkalVaFphCb3hS9qPTc1UP +PoEIHXFBsNILEC3NtV+W+BRFIWOwIssM1oCgD50M0IrSXVbxR3qqHKN5irsUPBCESAu2M+ZYsJCL +nYZZX382grKeTR8VPwe1Cap5sZ4EV0N/vfKLW2az/319BQH1X9AcrfmkaJkjihiW7C6C0OYAioi6 +pYSkrvjdV6Bnk0csnhOD5B5KYweTCOCAVyZ9fY4FIsOu+k+YToyJhYXy9YpHSfUyIfpKGH2w8Nzw +t4Wg0+so867Ypye6kHDBGi56IbiMqGh1Dto8icYm8XIFLwFLL0Jv2uOcjiyS5O6NHKP1Qf2pMIL4 +imxfS1qnHGjhyvN2CUYZ6b+THDhO/srSBcrDop793RBOCSklm3e2wwA7ptdSwTd9tGHmS46l48ec +feBmoY/Wg3k2szqUVXjDhyo2pWQ/4saMMLZLCcM8CSiniWSwfvcvPH7OtolT6omFJJDxIdaEY/CM +kReskVFioGCyygNPhhw/H3m0AEA5oHaGt+ds6ODUU4xe+t3I01x/mYa6fLW1oiOX3XCbUGy/2WfM +a8t4ID0EPJ0aBS5uFxaqZnvXME9YQr0GRNJ51BltC8zQpXetBrQMOXTd6A4QjneTFn4CySCSmDgB +7AkAdApfMmFkYzkwNTUwLWVjZTAtNDEzNy05MWY0LTE3YzQxNWZlYjZlZWQ1OWI4NTBkLTU2MGUt +NDM2OC1iYTc1LWI4NjJlMGZiZDc0NCA2MzguMjUyMzcyM2RlMGI1MC1hZDYxLTQ3NWYtYjQxNi00 +OTc0MzBiYTFmODA5MjQ5YTM5NS00MGJiLTRiNWEtOTFkMy1iYzMzY2E0OWEwZTIzLjQ2MTI1NTAx +My4gQQXLISI8r/YSnv3mdJAGUK1mGlirkeZ5oLkj57eZqjkitM2pBN9OEOgGpA054K/AulA5gxCC +WMNY55qfmjGYhgfc2g3aBbImasdKejSmHzz45bdz9JjgAO58D+w6GTbu1AaONVeA5zenKTyYNaPx +30QE6gYWpgHyAwunYdB/c88RLoEI9OYbWEuP1vSDZxDRBTZcjABkO1IxODNlNDNmODQtNGZjZC00 +NDUyLThlM2YtOGRhMTdlY2I2MjM4MmVkYjg2NWQtODUzYy00OThhLWFmMzItYTY3NmUxZTM5ZmU2 +bWwxMF9TVkdGaWx0ZXINLyA6DS9YTUxOb2RlIDoNKGZ4bWxub2RlLW5vZGVuYW12YWx1MSAvSW50 +dHlwL0FycmF5ZVR1cmJ1bGVuYztjaGlsZHJlbi9yZXN1bHQodHVyYjJhdHRyaWJ1dGU7ICxzdGl0 +Y2hUaWxlcyhub1NiYXNlRnJlcXVlbmN5MC4wNW51bU9jdGF2MnRmZUNvbXBvc2l0b3BlcmF0b3Io +aW5pblNvdXJjZUdyYXBoaWN4MCV4aDEweXd3aWQpQUlfX2lkb2JqZWN0L0RlZiA7NGZyYWN0YWxO +b2lzNEdhdXNzaWFuQmx1MWIyZERldmlmZU9mZnNlb2RkZFNwZWN1bGFyTGlnaHRpbmdQb2ludEwt +NTAwLXotMnpzcGVjT3VzdXJmYWNldHlsbDp3aEV4cG9uZW50KDFDb25zdGFsaXRQYWlhcml0aG1l +dGtrNGszazMxMDEyMTJNZXJnTm9kLTIxNHk0QmV2ZWxTaGFkb3dNb3JwaG9sb2d5YWRpbGFyYWRp +dTEuYmJuYi1kbmIybjVEaXNwbGFjZW1lbnRNYXAoYm5zM3hDaGFubmVsU2VsZWNSeUFDb2xvck1h +dHJpNDQxbWFuaW1hY2N1bXUobm9uY2FsY00obGluZWFkNWZyb210bzV0b3Jlc3RhcmFsd2F5Zmls +bGZyZWV6ZU5kZGl0cmViZWcwczU1NG5jYzhjY2NjY2NjOGNjYzFjY2NuYigtNTQxQ29vbEJEXzY2 +ZXJFcm9kNjY0Xyg3cmVwZWF0RChpbmRlZmluc3BsaTFyZW1vdlIxIDE7MjAgMTU7MjAwIDIwMDsg +MTUgMjA7MSAxIGNuUGl4ZWxQbGF5NTAgNTsyMCAyMDtEaWZmdXNlNXllbGxvdztncmVlbjtibHVl +O2luZGlnbzt2aW9sZXQ7cmVkO29yYW5EaWF6aW11ZWxldjZkMTFsMXJlZDUwMTAxMDEyMm5yZWQ2 +ODgtMTM0MjAuMC4xNW50YTh4NTRkZG9uRmxvb2Zsb29kYmxhY2s7IG9wYWNpdHk6c0NuMzVuMW4w +MTAxR3JheSgwT3gtQ29tcEJsdXJUMW5lbnRUcmFuc2ZGdW5jdGFibGVWMkZ1bmNHLjcgMEIxQ29t +cFhmZXJGaXJlQS01eTVXb29kZ3JhhPeoUzyaqSGRJClIjQ7DEUggQAxYHBSNR5sqevITwMBBgcBg +JBAJA2RwGBwEggADgUAwEBAEh0JhICgwDMQoBmIQHB2axs04ZQPGMjFactKYcY0fRyvtCH2jvLQz +zvucbbvxwPNlZhwqURIWTGWyaQFoKguWgYNaHJF9nIbSqwL3ZfAxngw5Slace97Los/e1wlG5pwL ++BP6C/0JsjIbzkncx5UKMoGQJtVhvjxgaU/IEjfqBDk9EoTPYGLj6+a5ieDcLHdkkHPsFo0KXxYc +Qh2Szg90CYvtjEpe4xzq+PEUKWP8c0kP1x7RUi87C66fPRTmXvGC60q4N41mHc10f2FV3fWSDV2X +z2XQjAkZNr8MghMFK2opKLGqa7XyR2EU1YSgswRuAuLeIsuc8xttrtTarEjQgWUdxVV2Mcs+hZQi +gHob1FMI+gMI9oMiIDC0BFc3KA64i0BKI3HIQplG+pin1llaEYPRdSItkDiwbqohcS+7aP6ESZTh +N6aXLBVJS5IkgeXF5gvQRTysIOfkLFlMh2P0rSO5XOy0KmBIY4RYRvpNV0ZlbQAmIdkSepjyK3+V +Xk4ghk+izvsl+z69Wx5RDBOt4ufDYC4DEk67Bbw3UzJYxLJafTjkes/4HC38XEiJkE6asqeCWXt8 +j6H2PXIcgKQ7wOLloucdRhGckkkzx+/xvVb9kJncCqsRowkqccYqRjlp1KkuC/CDb6wCVRzUWNj4 +0SDtOoBRBBT4NmJmBdII++myhcFoN5WvzvbRRFyw4YCSSoUOU4b/yKKmVuBUAHEDPssW/yONUv6I +t9IUHs+cMPPCGCqYeIfeSt18z1FEQWbcr9xKtyAr4rs4Q4VANPFWRl55sFHmeOwMoBHmImIdShtA +ka47KEavnJFBBgeAY82rUBKKon6GAUam14osYPhIR5/SMIewl+PoExhnkZrdN9aszsFA5tBC0LVD +l3e/iwNqqwRMalaaZ09PhxNNXQIODi7b2wRDN8UKq/A+QEI2GvD20/0b5XHpj4ra/RFq+bsH1Zt+ +LfSWQkyG0i20YGb/TBvGE3WyukgWAJFaNHL2jjol93SewzQxDkuIvICNtVpCdmkTxGxBlN+KRWrI +almXpETmLNjo7pKbT8koDwLX/qPw3RCup2pWLQVxoWEXJBQsdx1pvCTvyiT56DNyCUK9+I6TAU64 +5IlrijzrGDZYlROpxm1CgndpIURyYusyntnGbCkpckQiPRZda8Xoqvgt5OBG48fsZWoHrUj8uYK5 +8GGB7mnoTF1K6HzRb3SFVbgmKpaTykSzvCHMqWFfIeuiQY0VnYCdvpziSep2Em3ght4xPYQxylIx +g8QsEwxpOpf7IBdi3Ho7UZk8sZYqwN/K40/ICp+bSfgGcj0nx4ieRTrJ87TeD89YI6pqK0YDPR58 +vMxeIABA07M75lPiJmx3PwIZiel14ijmCgGo4VgchW8k3KolvhAVdCC2KnkuCnueqFXJjxDH51fb +xJfGYIi1Zuaj4YVAyDQi3qkRuns5CMsSkQe3qA33rKMowYnKbUUuyXhiRZHfw83LOJ8Yhk2szdR6 +HPiivOAHyWx25K7jsUQleCUoO6Q5f+geFTTmeib04XKCBH30ZzOR8Lw5v5JotJb0bhfsqx2LO91v +dQCZo6HRyVbYA6LNKR4OLE8eGEc3M2zB09Q5mgAWFOjeNNCRM4J0Z1fsUNQYYLQqcQ== + + + juJfmnGhVMHG0fIFGdRq9NluUxFL8b01BwNYMeOIcB8uLV4C5Z21c0gyBqleX43rsQQBCUI/SIjM +f0I6rBSS//n+JGJufFTnEDNEa3DLKMN2ihxB+L0j/fZNnE7DP0VP6R2pUF/NBZKW4gmDivrok7MW +8siEVIzHj5AGYHqaFGgqKa2RBXjX1ZFpJ+ep2Xy9a1paDLWFGD4FJvJ37HApZNakTvlfRULKXZfC +wAUfKOgnJi0WUugC9REGBt+ksTTbnmxoX35hF6nSzlxOEEH9TmTzXs4QULH6/IDV0KWbHx8bK+DG +92J4+Sjlbk9NzC6YOjV7oDVZ59/zPVQ4HQLwUCQ2FLESYdgaw8QgptPMxSlSJcPV8m6QiFthlKKR +B6LXozuE/UU2yCE24ykgPQUtsD/2Cv/N+pAJhaU6pj6+RcJNUnd/1VgP+fYeRaiJTMYiJ1SneAQq +U9N4PnXBfjdR0j6q3c98d/6JHyhQ4EAkXnOp0/qLokHHM6mTFm1l98LFfXS1eJyi9krYGjftNnNd +r41UWpPYI7YRgiSVM10WDweW3TTNISkX2un7tALMWfb6Lo7XtNMXKCLcyOs9JpZT23YREhd+vmf9 +bRRwgL7X+G5QFwZUm7vBwmH+9hg8IzjcSRV5JZsZ9ELdysFbxMF5dgOAkPrSwAJxI0arE6qcUzBQ +0YRov1oa3B6RU9SBzjYIgcAG7JhTmSAYRNQXSUTpFpmSEpAq6HBQUarkSJUIYtMkk1p+vDjCPBfr +sIt8sIWAn9Qq7N62VB3cTKptoJKHRTDA1B44B9OqK/71ElQwhODUJM1ZBEodhqfk3TMzzOA3jHRm +LrvKIqAzVBtU8qnNwMCeRL4yzGhcElD+H4zanvtShUeYH6kCMQy/Fuas9m0xoKqeQyqyxLni6S1v +Zajr88jDxSJkhe5AxTsOBRBIJKL4yMDiz0ZKgHEmehExSbZukDI1nMwdiODICSefyAI30DUtOH3m +AhZVb+t7kJJyUltDF1kiSQvE/qmGQ78W/Q5ednCWPU7/QBZXWkTkxrnciwreQTWJ6VPQGxIinB3R +fOUC706pvPht//gizqCunBCjHuS1H7QIqxl+bJbA5pQoZnxANEMn7Qd8kmt0URpYcIpk4ELcYYhu +pAn7v+XiYWDZq4mhz1ahMWB5HO3I1MBeiZBVZfLDHYbr6qL2ssY1HY+in3iedKMgTi8aTLTe1zL3 +h5jtu9FF2cVbdSSxHt+P5soilWdASXZSxuIuExmni6dtl4FbK9+ooPxYK/5H70ppU2c40Eajv4L6 +FMHaOTV/8WjzpQe/XxkSAxPzC3pZjY2LtzBoIyKUjg+SU5q/z2oVr3Jmm73nxtE2LifKchnHf3gm +NTrcWP2S0R4OKshGmH87T5J485bZ8uQq7HCqHEHPpUCxN9HVaosj7s1FG2l04wIPTVjmAYWWMxGZ +ttCnvLkYIQoU2jgRPBs8JUbdf3dSyK7ZT0DYhRQV2XeKgRDmciLUCj3W4jLGwVaY7qMmYDp0q0Zg +FlDxBgUoumlE8AefxpxdpcQL1mG0QZWeCywkXBCZwjpwTDJS/llETQbmkJRiBVP7pBsrCJPHoWdH +qYAm1aIP86laU3TTFbdtTjQYUqUiVQx54wmRzKc/RjnIQlrJfu2aNl5InrgQjfEDa2eIVqYMRlQR +JMoSh5KfhQWELLkK+CcFdOdy0gQuGtbh0Inht74YI7Rs4t/OBsSiEGVs+wDNWep3J3oVDGA4sxzA +yZv2Kxhc8j9JnFCMITFhDDQzGk1KZngwC8BbA0H/utWy2XhGVBtl43YRvd6SXA3vaxReGYR2r0/Z +5BMNBzDn3ls5Aa3a0vhHjxuC1SXYtp6XcfKmVxIbKRVDxODIrWTcVgC0ryxaQGk/VhhZVkYBqKIQ +cfiL3AZQIglF/RnGMMnalDBa2LIZGrcRSn0oiOIaFZSWIRXHKUhKpQVUl1AMSsmsv8JmJ6dgZ9DJ +jTJTpQzLT0IVQonJWKxcKuyIL6TCaG0fRw4eWh8i5ARYElyosT6Hk+JT19sfWpMjRAtVBDs/Fy6p +asocLLjJmGc2HjoX8rl9e+yDhbjUvGPFnO8cLBOmE4MwC4clSUiGqpqGAVatqUL3ZZPPiGbGKiuc +F/yzpr5IIL+9ByUjXdBR2vnQBgG3IgtO2A5TEHGEWyzecU7zbekOk7yHTNno6wBZMK5cc1ZlRx4J +ekt26/QpCVkLC2flYn+zSWXgoXHsUXPUyiSIFs1Cd8uzbuVSKKoDFwyYOBdiZi9Wm5f79NicZ9e7 +zGIKb2JINr6Wvz9/Fudomo0KMgCAYa5d1DzH7XhKu0KTIivKbmRJMQu11it709M12l9nYW08iBGb +LtB9YNn4ah2Fwo+m6hrkaqOA9438Jd0POV67S4AJMoMJpU4e1DLDYjH0rM1w7JuescxVlgH6UeBz +FAwZEaOaV6ThFthtLNblzGKGh6N5HwtRW6R3qVukoMQxqc7AIp8A38KKxwbrqRsEqE2AeqclEREv +1LCXh5pMiLTqyw6DRunCe2A9RtraVZ92PGG7BSYWzHBamTRwUQPWUvTLdGXlE969eTjgQbOXsaUH +R3RQCTtlN6D54E43tcEK+ORoNDgcGg3GjDVL/MHUidR6LCHPPIU03kLdZykXUPeInrpaQdEypHPo +QaeZeIxP6UhcCObwD8DUZqhRdhB5rblm9ufOZVc+dZHQ+pLU6MX3hFnnd2nFEb1wHPJ3FUTLMFGj +MKd/0/WfaYi3Q2rO2Poc2ZdcEGGjNmwXQxwcuB9hC/sOi3WNgrF++TeyMBuPj7nb89oUJyg/Kt8Z +eDaZZvWC7dvEEKMMP949uCW55kupBH3Efvz3mabusw36t0nJkaezAuMA494fs9DByuPOuvHpAsX/ +yHNPJT8YxhK6Bk0QLpIuPMzqjDTQ03cvxoCkc5zQvAbQEhUR0cHXnN7dbxIt8riVCAmnHyDM136z +LBoNJWjb9I0SkDdbe3QonSIZUo0DErK6tPvIT9RuXY7OAXRFA/r70oxDQCUMcQ5zroqiKIqiKIqi +KIriQg1vjJaYSEY4RnS/m7Ng3iII1cY0EKRrfPvZNsSTaSmllFKmpJdFVYtKhA53wPG+Gt4HxgcD +CR5c883SaZw5y9mT5VfmmGsVfbqwzLNOem0V6+3KYtkv7XRNaScSlnkaTddkWfkx34z92pazulO2 +pdmp7GjnjJNWU8jldPkZSxqvV1Hep5ed2PGUfa3MkVbHLWe2hT4tNIUkvbJOoVxZpyGnnv1jnXVe +eqmVGM/s8kZqba2SejwaE3vXaYtO6VeJ71f5q+mqLnrn+63S/ZL3uiXV6/6kPe3EUya2g3lZj/2j +evxZYflTqKomq7o256b2XoktW/Gd/U5f0vfL/nUrKynaKr77Y49vZ/UR3eJPpPg//bPb9+rKjal5 +VvG7vxtXibviKv7nmeeMjy+O7ZOUZzWFxK6iexVxnq7Jijf2lXhOV+yrSnslnlX186eLVHsKiU39 +PQ6vy6h0eqySZrd1yq9IFOdZ8e3pyo+riO3Tl5Ri+ZPim/uraH+6LoyraC/Hw1PHTbFX0fO0cIWr +C1cVjjbaiON1eSPGUya2NV2TFW3MMedoJyvmmKtcxWzlzVWct4p2IsVsMTKw26zPlnFYRtebabVT +3uxO+yWtJq6+q3vhn0bT1Thj6Zb+t/TqsfSc35/OebNtLP3xvKhXcc7p8n8iTVmrOHuSOorHl3VZ +Fsm6rL6uD/Whpuu6PtR1fajJ+lAY94f6UFPpATQaYd5YpjhUdfZY5VmmlYvsrbDpNstY5nhUKc5w +1zrCKhddrMpAmVazgrIqgzLatLEBAAAAAJgXW9sz5o8KorWTdU3WdE3WZV3WZV3Vb73wvVfmLPPF +12TN+bJ9utrd3W2ersmq1FYs7bzf8k7XnS/vS4zxdE1WT5bTOV0t6WTVavOUFc+nVHa/P+mvybLV +V9Pl+PGUX00WxXlKd1yvt/Sruj1dGVdZq8m6tLaVdFaap6QkSavRhV3mimKLcTVZ18pL8knlxaPp +sibrPdJ7p7Uuf8r36qKzrVzhxtn7/+Ls3tht47f0W3a01s6fLTP4iZBVfESZgy8DQRha7MLSJXYu +kw+FwWw+1If6UB/qQ1H6oSYQ5kN9qBAM5/D4n+x52maNcNpF8fhQIyBmESMEIAClTcyj2VlkX1Qt +rq7NuHKR0woUmqHq2tDEoUwnINszlOGMXVFX1BurRqFLK1xzVnUdPahOO/lQFB4f6kMJQAAAAAAA +ACAAAXz7B3fsGRVUCD5UBOzjfKgP9flQU8niEX6ZA66TbSAM1DIjIGZR2UwhjM21P7f0/PN+9Wqt +7Env/adu7Y1P8z/1+1TiSe9tfOvjnl6lraJXi9/dXnn72r49r73xvk9/S2m9T2W1fael0l7P+T3b +eiOul1J3i/3Sz7Wf/pTVp/Xc9fbs2U17Zp8X12qxrBdbW78pro+dXvvVJbX3/tvZFF+Jn9L5Fkv3 +LDEJV6NrevVyuk+fQmI/kDZtM7m2GXu65kRyWj/3e71d68T9M/uc2X5H69UzndMznT/tvXNi/C6/ +Le739qb/j7O8/taVLenBLSaEubMdZ/fOE8lj2hszbXurOD/f3N3XZOHpwlW08ubK5toucTWRMMZg +4k8gzCTyiB7bejX5ZFuE0BN+l0/uXD4BPtQEQfq60UbnYtm+dlrpDRFFmejrQplG2F1U4mRNV74m +q3o1XS1vNVn2Voozlba6SFXa6NW9S+zSg07kdV9mToqh1944r8XUr1dxehX93p4sYvt0pxoe0SfM +I2aXtFJ3POutc9ZKa6WUzumKs06k7v/u/8a0ipf2/0Sad/ac7rO7aXfP6T7b+1ax53Sftq+ddc5a +MddOD1qlV28/0o+2shRj22m1VcRVpNm6nVWk1XRFryKttu2slFZbMVW6rZJW8VZ7I6VevYpexWrr +dMVaRVtVsU4WkVp7o432Tle0VbSVrS6WbSet9d0vrbezbTovxo/zzJM2rm3z/bdeJRVtmbbJQizj +HueMmEbrfU1WtNNVZa9I+atqurJHKy8pWo/5p0xsK/5XNv6M/5MV/6P9Kv7Hn9Kr+PPzV9NVTST7 +L/owK4aaKPusGbHHtpHWkTCUjb6sulOKHVtbxf5QbK+qLbZb7hXLjtknjocJg4k9jZpiQph4cf0q +PskjXlvF/yr6FIKJHjtXsX0CYZxeG9u7qtqreKvos3J6bc5Z5tyxPX/0OZHijT6nTOzRZOW+NE9X +vFX8+DPW6D2vyYruEX8Vc84df8aLM+6XVTVd+CdSdK/wTyGx26fSw5c56D5cF4YlTPh1HgSNSPs4 +IZymbZ78oaanLyu2+f83RFqJk20gjNNM7w/1oXC907OVPV1+G0v36cqSVqRYM7Yubaw/YR6Y9qUz +2s+R67jLJPrEfagP9aE+1If6UB/qQ32oD/WhPtSH+lAf6kN9qA/1oT7U9aE+1ET6UA== + + + dhJ5xI49cUyjyZoz93uWXaVPV09WfDpb1o+1Us/SRptb0mj/1tovf7LoR1yvjbOarsn6Wfms6leT +tsReXfeW1yt8q0iryYrzq0rrlPerKqftlncKVZfVY51VtD6F+nrua2XP6fKMbRVpvlN6vZJWsqel +Ocv80xWtrWgVcf5aRVtnldTWmek1WT9rlfTnYyqp/8yS+hTy2bllO56uOl3Rzr/SvaqwrFX0l/9V +zFWW7dUUqj+/sl7Nr65Xcn6m3bK/+mq64sVuZa7itJTK+tVT2VXEs4rUrVcTacI84ve1LmdujHG+ +Pf9OpSzUvs4Btoki0WUybR2IFIF650KJOA3FBZTW3E4npu3YvXtSt7azpXbavtctntTeiadsmqv9 +txZM9GvhxIl/+qRv2+KaHX+m9fHEt+mlXZ22nbnO+njethdbMDFnCydWn7Lz11rtt52PacY/L+2b +28p2i2nTvz2r7JgvvU5pV3cLJtLpFk6c1WmVtTa2mHb1Wf/Wvu4+s60X5zlxzh3xP/2nNec5L8a5 +c5724qeP8b//nHNiOnHG9s7+t/Nx35zfK223NnvGF3/FTtteW3Oe87N7tmBiWzgRV5/1LW7q8+I5 +G/ed2b77Y7futFJMJ/VafTq27d1en06f1H96U4ztW+r2c8bY3qZzugUTK7Vw4rfLvnninxVj/N9t +cVv889baleLs+XOtTWult9stmNif++nMFHf/zLjd/qyWUgsnWtq16fRM2/5tfzrvbDwrtVk2vbfe +ebPnjLFbMHG6/bdgol+3aExudQsmZmvnW0vvN/XZ+VqKvf6tVPatlXrPe/G704n/fn+mV96fb2+t +Fkyk3Tg3lR3v09r251tcZ57fjrsx7Ynxz87Z0nf77d72qfU67e23f5taLOm1YKLjOr9f4u5qb85y +0vbPnqe19FoLJuLsFk5s6nfi7qa1TveeOdu+bj3jiuvsrvSvu7ut1rPftxdjl3O+hRP/Z84S/33H +nzH26TRXCyba6xX3W9nx2re21r8430pttbRiOntaMPFSt3DipW/RmGR5vRu33/+2OMuO2R/f7p72 +7fTvbtnZwolOr//tf+9uOmtbnDH995yv7GinBRPxWzjxzq/z3TPO9ud3xbi76WfcTudnv9deLPu7 +ftv8XakFE/O0cOLMbtGYNM0X/+e/tmVXTCumj+9neu289+b7j//O6xlbx0IuC8EsanUsO77t7Lb9 +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 +vizkOqEuE2IUW+aZxAAQsjD0bFtp07mMyJbUJ1kIQQPGjNO2sNSNYnvnkoVCOO1BpnVgkRBNIwth +m3CUXVTjOLCQUhw9O45e6qxtyyzPtmXWpOC6AGhfR/HX3p+YcJRdOOOqEY6yKxxll799ztd12+bi +Sj8i7eNsmYPsihFCbcNKBWIIp1l83dZZYed5Pi8CZhYxdy4elrywhBFz59Izhl+mIJJKmiizQDDx +uoyYhWClEA67olcKM08Eo0j7QmLMnUtIDEvdSKjjRrINFLMQ2zpQi9EdFEkizJugtmFdSGqe6H6o +zgMfP/2qgfHLPMyLuXPpL3rEYp612lDbNBAWaiJNDCFkGzH7MqJ759KZeNoHIzDLdELY9mUhp/mH +eSCHfWFbdNxvFCZZGGIk+pyLjDAL+bAwhNPCMPMEQCNpoddl3PgnptRGiECzbfo/a5aTYggQwlF2 +kbow7EAirtRhspC2+YMRGOo2D+E0EaeBMs+HOXAQAX6YAwcRH4DUhWAp9AE5hgm3kSBgIZbJNo+E +gREOwDDzZBsI0z7MgWMhJsLCjJQ/1IcSKUw+1IfqCEfZJcqyDYNl38XrPiuAg50v2w== + + + Shoal4zH7R/qQ1mIPqwDhd+lsc8wLHQbdENVYyFQ9SNLq0kvrTDsE5vfkw/FdVqogV2GRrexZRoR +sfKhsojTOHg4nxDULkzGe8Z+//Hn3H0vru50Wvr3Z9fGTuv//WnBxPzutH3epvk/Z5xnfmpzvf3v +fumdGU9MPf98W7NTi6230582gjIDk+j0LZj49C2Y2D1ddrQ2f/aK39quf+vFbWe7bc+Xer2fM8WN +4UTan++0lebP17s67ew2V3otzZ5vvn7v9Wy929576dvG2C2uXbPtWXH9WbHPOjF26vQ6vfTWp+/v +Fo3Jf3vN1Pvmx30nnX5vV/wT34srdfd7c73Xgon3WjjxXptrtnP+12rtlB0xtj0f21qr2+t4Zm9c +72N7cbVgoleLxsSCifbfZVc6H/c//Zrnz2qtxfSppd237VswsUMyokcHLyoIZUw/iWAcIox4MJg4 +W/b/ndRx457X32K/E8t+fKvfeem81nrPSy2caPOs+N7auPbN8zadtzqV7i+70jnbUkx9ZotrbWzt +/VqnbZ94us244p85T2ybzp90Tnrn9aZv52O3cKK1Tn9i774Z5+9bndLaTi+emXqmt86m1WduW3G+ +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 + + + diff --git a/brand/Trivy-OSS-Logo-White-Horizontal-RGB.png b/brand/Trivy-OSS-Logo-White-Horizontal-RGB.png new file mode 100644 index 000000000000..06587180294a Binary files /dev/null and b/brand/Trivy-OSS-Logo-White-Horizontal-RGB.png differ diff --git a/brand/Trivy-OSS-Logo-White-Horizontal-RGB.svg b/brand/Trivy-OSS-Logo-White-Horizontal-RGB.svg new file mode 100644 index 000000000000..b3183574dde0 --- /dev/null +++ b/brand/Trivy-OSS-Logo-White-Horizontal-RGB.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/brand/Trivy-OSS-Logo-White-Stacked-RGB.png b/brand/Trivy-OSS-Logo-White-Stacked-RGB.png new file mode 100644 index 000000000000..0ece65b88a0a Binary files /dev/null and b/brand/Trivy-OSS-Logo-White-Stacked-RGB.png differ diff --git a/brand/Trivy-OSS-Logo-White-Stacked-RGB.svg b/brand/Trivy-OSS-Logo-White-Stacked-RGB.svg new file mode 100644 index 000000000000..52def3a62ddc --- /dev/null +++ b/brand/Trivy-OSS-Logo-White-Stacked-RGB.svg @@ -0,0 +1,3179 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 +qt4aktqds6qqLW8s9lK9PkuNT9Lag/WVrsxIzq7VtW5ke0qRm/kpIuNhDZqYXGjf0aruJslrwymz +Exyjvcwnz+LYVO3oI/GLYmOE4FId7f/OzszkuuUQ1eEV3Szf1BnN/WTqxDIeq05nW+vkLCmZcsRH +iZl2PKK4Cm/nex3JJb9rKp3Oc1p7c1ValS8p9y7tPd2n8zqeUjquqiSXLgah0G4khT0b1iT2SCxd +7zSZ0qsy32Bl7H6GQzWasl/t0izVNIdGN3cOIZhHau5TelSZaYVVNg/VJVHdf5V582FV1pUObUfL +y73IamIQDAXrqxvRcm3ttPqRq+xGLZv0XW1Tdlem1Ch7V2ltrpX3dPFKTyQjMrP7zzpZLssibOnN +29RVk5p1dzV99xlZ0b9DovSk67OzNc5m3V67NSl/eXOFFzwsnNZY2n6W3RVrvb01OtWmPGsv0Uc2 +NjOyqskP1p/FI7IlkhnW8ExpLsm8B1a1vesZ4S9vabcrMrrCjk2h5WX+TndlxyKOueyq+ezr3Cyr +fuEvCyUnpy9eonOuEJ/To3Qjq2fdVbVgHsXLl0csDyw20z8jcR6t25Na8yFDI06PeqYrZ6ZaVVnP +eEIQWmIzIt9dTm08DjXnprs5O/zcp7CkDM/1qV7OPgz/z91OkrKcehY7dyvzkdhMrxJJXe3omLf0 +SHY0dkevH8qYj8OXzz6WtWtO0oumYu4M2mCNPyxEX1H9PIlXuvV61BFi1U09yVbSeqCn5iyHsVx3 +o03qujKZw4Tkujowx3U2Bznnl3M0Uj6OkIhM7MCjOd9L9KNw8l5yc9+VVBVPdVMdql1mTySURmJU +H6WdUc/SXJk1I3QgXW/3fIUOH1kzj04d3pVMiBAeq5uqvIdlDomPjh7aZNUI/ubjHesu55aZHVba +W8vjHYnWSbmjbYodxxplb4k4qjzsYFEqsYhplVb81KANni9/bz91PSP02Ky/MvpkV9urkMq+K7rj +dO9omv5aT3yNquhopHRTrPparfZaL6xK6arXKlWLdTobZ4YohaZIViMsp+taxaSksik+acgyeKfP +JUpO/Zx2ZFp3uzIx0e7nlDfvlIm211Wyks9d0bN1q44M0dR1ato8iY2d9EkyqTIhoSW+VT+oQlY2 +NaI3b5el8ezicz0pxee6e06j1zSGdCupU+HHaFj7U/Syp8z1vrGJyPLoLOuosHdCdy6SzCOT3qI5 +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 + + + qyGM22Ad+OfQwUVNJsvOdflF797C6Nkzt8nCXNC0ZC5tZWPcLhNzYavHsm43Wbbtk2XP+vwiZzbj +fOtsHO/d30nEs2X41mqex73aqNPriSwNexL9/s8joDfSPLSLOgu9kKZgxzm86y92aNqXuruMxutG +A1kC9h9Lvt8zuVfrKPLRRqCGvYkV8SeNGt49jny/pzHvxvnWfRzwnF1D+OaJMgXvJ9ZFHDDdj+FT +o13g7DIawLh66dR7J/DgaA9NEto2gXZ+pk+ululbq13S/NbljLvNtplxdcR3dg2hXI/RY5tj9tro +mDy3WYYPbuZh1PM9lHn9ZQ5N5l3WZte6skweW5/hg/MyeG51DN4aPcPH9mHk0uqs2pddujPXZW3e +ygSy0TqKeP6m8I7madzzPo989g5i3q0jruvK6LV5mr83O4jyzy6qPPxEmIT2Uun3Hrok/DeGd78m +MO7bFNL5GsG5blNI53XAeDYT6femMjFpF1Ue2jSAb95mcO4XeQ76HUS9D4OXNuOA52yiy0F76TN8 +M4GCu0abhbbS5/dO6gTbPZB6Ns5hnb2DqGfffOf8TB9cPeMHVxdtHnYlUG93SFKwGxQZeON862wZ +wLbZBa6XnXHg8KJ3c2cI3eisWy/jWgwa+IYJX3yLQQO/y8DMysJg/s60M2E2Lsye2jYb98d42lgH +bqYNiwlkG7NJjOPagOnklzu0shg9Nm327SbjfWwDJ8OggZd5XeA1GIasHFcsi9cVc5GbxXjHsIFr +gDCBj3l9ZfFsspi+Na0MYRtXprCNO2PYNsscsm1d3rha/sPAuPYCBu7DNLi0jXVwUSsTc0GTx7Jp +vkPWjSbLutFkLmvbDpzusIF3sPDF6Q0abDbUOVtaWlVYTA7Q9tAuMGExUTFVMXFVMVUxYTFxUTFR +vdVMVExWWkxcWlVMN5UUExUTFdPPEauZwnoHC2xqcH1sFxrkZKiZqpgSROXkse3poWkd3E1lXXFg +osrSwprqwtLaksqhY6LK0tpiqtLa2kMjY1qr+8qRc9pyOFBdTVFhYWFlXU11dVlpZU11WXVZ0VVZ +WVlZWW1laVFlbV1VcU1hUXV1cU1hZVFtZXFdTV1RcV1RbV1dYVFZaU1ddWllWVVRdWlZXVFNWVVR +WVl1aWlxUXV1YWVx2OLaytri0uKi6sq6suKi4uLSspra2qLa2tqqorKqurrawrrC6tK6mtrSssq6 +6rra4mC1pdW1ZXW1pWWlZZV1daWlxXVFJSvIacuxUJV1pYWlZaXV1dWWY+FqamurSqtKa4tLa6pK +60pta8sKi+4CU1XfuuKi2tra4rqq2qra2srqqtqy2tqqqtri2qK60pqy2tqastrimtqy2tra2rqS +2tqimsLa2sLawrqqytrKyrrSuuLqwsLq6tqa0tLa2tra2qqyuqKq4trCmtra2rq6yqLSsrLasuKi +mtra2sLqwqqa2tLKusLaqtKSmsrakprKytrSwtrautrSutra2rqS2tKaotqqmtLqmurS6tLqmuLa +wuKaytLS6uqa0tLS2qLamtLS0tLS2srS6qrKytLa2prK0tKaysLCyrrC0trSmtK60rKauqKqmrrS +0uqysrJwZXWV1dWFdaW1RaW1dSV1pWWlpTVllWVlpZVlpbXFpYVVhTVlpbVlpaWVxUV1xYXFtWXF +pZVVZbWlRaVFpZXVldXVlZWVlUVllcVF1ZWVxcWVlVV1tZVVNZW1lWWFlZU1pZWldZW1lTWlldWF +xZXV1TWVxXU1lZW1lcW1NYWVpTWFlUWVlZWVtXV11WWVpWWV1bWVxXXFlZVVpdXF1cW1VcWVtcWF +hbVFZXWFdYWltXUlhWWVhVWFlTVVhbWVVYWFhVWFlUWFxYEK64prigqrK4vrCgtrKovramur6qoq +q6uKS6sLa2pLi0orK2tK6yqLiymRelgCQBr4JAHLOypjyT4mTEAVhWxyIgI5xTT0gQPTi6OGpjdH +AZqHPWBsIvZYxSz0UUPzsEcLzkQcrJviHama4RwfNMU3UDcVdYSCShJZ9BQRBS4SShVbsUrsD29Z +4iGpXPmVMbKYBY4DoK0bJ0CWYRyLIWlLqBdOAX8UXi7PJPCyrQac6ceJB70wcSA+xHP4Dq/hNryG +4/AbnsODYIi4kxATP0BkgDdgcuJwWgpQSDzBHIxXgGUhIMUGqgQ8gDHxkAwTMnHYgUc1EMwHhKGG +qQB40DA2VgKGiKUAcmh4iSWlX5MklX+xRjwvlxCCr2Ch/wIgRdZ8cSWtk9EHpuciD1BNxB0hNME6 +Rngq6iAVrRyCOBo5hPFTEbazEzEIaaYhjxeXg7A3MA176MA01IHDU6zDVZOHlLM7BHT4tbGOmc8A +U7h+TKTDg3EMDUFCGivwO7IsPiFBAo6FZIBP/TBxKBokzgRDxI2yPFzoJYcX8R9exI84Ug8UpyE4 +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 +sSFwvMB2boyFrI+lcGgbaxGD7GGAnVkIRErnYQ8lm98bLTS9Ol5seneg0PTimMEpppFiU3xjlZMx +x+dmIo5UzkQcrJ2INGh4ch4B/Pw08sfpoQlcZgFN1CaHQOkqWpHS38ZaxBSMQ3wFOXmkwOckLOBQ +NEGchttwGB7DXbhLlr+4uORwH77DgXgNz3nhMpxzF07Db3gQDhPnEOEKqQbnBbrxAMYyvUwwKfmi +EAGlXHL4uGQAPbJJAPxm6xO5zcQen5peHiUyD3Ow0ATrGAFKCSTR0oUVtM9JIeJLwx0yMAt9zNhE +5OG6GdbBium9gWNTbMP180MTuUgmWGy28RHt4XiL7DfWohUioniBr2VNcQY9RDyI53CdrAzX4TO8 +x2WG5/AZTsNhuEyWc845z/IXzjnnnHPOuQuH4TK8ht/wGo7DY/gMz/IXPsNvOA7n4T7ciEcVRfE4 +swhiYyoaOr2xnZZ+TmxYAjKh4dnZxA98QABkxyQIeEcpAOihkUMaMQlhdWx6d6DQ/PYgofnNUSIT +TKNF5veGCs3vjRibiDZMbIpvrG4m4kjlZMTxubnIo3PT8UanpuMNzU3IAj41I2Vohndg8QvGJC5k +IecH/FtR8QQ5ShyH43AYHsNfOAyX4TPcht9wHF7DYXgMh+EwPMtd+At/4TU8hsNwGA7DY/gMp+E6 +nIczLT29CMJ9AbGGwYReEzdk8dC+eJQUA84EhYBbcFDlEmhH+KkhtpA1znFBbjwAMUxTKXMBW0xl +OXU5dM7bnJZUDp3W1lRVldaW1JZWFxeXmpXWFlsXm9RWmlqXFFsbF5YUVRcbl9SaFtvalZWamhba +VpcVVhUV25pallRalVWXVBfXGpbU2lUWl1SVFRZbWtuV1tXWFhOWFR6HqiytLaaqrCwms6ecB2hv +d3NeXWhpamtXVmtdamhVUmhXW2lSV1htWFJZXFRtUlhUW2poalVpTHNoWFZTVDLaHtoFPzQtmxsa +3QYmLKsNXmxrdmpwdXIDpgRVVWldVFRYVWpZXGpSVVpUXVJZWVdYUmhtaGtSbVVpVWttXFVRFzbI +e3leZlRVG7yimB4wJTDTMqMyi2JywHRhg8yhQ91ALCePbU8PTauhzkHbQ7uwVvf19DRuoML/UPol +CAH5s2Rs1k8hFmX81mYZPzcaQYjJOaxJa+8Q5bWLRTnBlQDlVQsUOdhj7tyINAktDEGlkUC/QJl/ +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 +TkKEEUqJB7GFBmrd1T2DfgaRzRYCjZ/E/7Zi0WPxU6GVA5MJ15BF0ne0G6BVasDFUEmVArSor2yi +dnDCFq2dR+toH+2KGL/0wdngQY4mEQ5GVQPXFDksXPn2sw8zOCDv7d7Bl//HyvSsGYXxkOCqE0fQ +JWfXZtAsCaJNeYyjtghPrWhoGltXwe646FbGCJdyj6nRYxE9Sa1K1wJT0FuflP3s9FxVg08goCMm +CHZ6AaIFc+2fJTJFUWQMVmSZwRqQ9FEnAxSjdNdV/G1PlWs0T3GXgicEIcKC7Yg5FlRysdMwy+vP +TlDWs+lbxc9hbYI2L9aT4Gror1d+c8ts9r+XryCo/gs1T2s+ES1zRBHPkt1BEJocQBFRYykhqRO/ +eyr0bPKIRXJikNxDaUTCEgEc8MKkr8+xQGzYVf8JsxNjsLBwvt7xKKleJkRfCaMPFp4b/oYQdDod +Yd4V//REFxIuWMNFLwSHERWtzkGTJ1HYJF6u4CVg6QX0pn6c05FF0ty9lWO0IKi/FUaQXZENasnr +lDMtXHneLtGoI/13kwPHyV9Zu4A8LOq7vxvCqSFlZPPOdhhwxnS1VHBNH22d+cKxdPyas0/cLvTR +92CezayHsgpv+FTFppbsR9yEEcbtUsIwTwDlLpEMzu3+hc3P2WrilD6xkAUyPsSKcAyeMfqCNTJK +DCyYrHjgqZHjdyOPBgCoB9TO8PaczQ5OcYrRT78be5rrLzNQl6++VnToshs2jvf9ZjnGjKaMJ1FD +4GrWaGFLuyZTNYd3A3vCMOpNQOzCo45oWyANcXpJaELLmNHVrzyrJIt1sZssp5PmEewJAHQKnzJh +ZGM5MDU1MC1lY2UwLTQxMzctOTFmNC0xN2M0MTVmZWI2ZWVkNTliODUwZC01NjBlLTQzNjgtYmE3 +NS1iODYyZTBmYmQ3NDQgNjM4LjI1MjM3MjNkZTBiNTAtYWQ2MS00NzVmLWI0MTYtNDk3NDMwYmEx +ZjgwOTI0OWEzOTUtNDBiYi00YjVhLTkxZDMtYmMzM2NhNDlhMGUyMy40NjEyNTUwMTMuIEEFyyEi +PK/2G5795nSQBlCtZhpYq5HmeaC5I+e3mao5IrTNqQTfThDoBqQNOeCvwLpQOYMQgljDWOean5ox +mIYH3NoN2gWyJmrHSno0ph88+OW3c/SY4ADufA/sOhk27tQGjjVXgOc3pyk8mDWj8d9EBOoGFqYB +8gMLp2HQf3PPES6BCPTmG1hLj9b0g9cP0QU2TIwARDuBMTgzZTQzZjg0LTRmY2QtNDQ1Mi04ZTNm +LThkYTE3ZWNiNjIzODJlZGI4NjVkLTg1M2MtNDk4YS1hZjMyLWE2NzZlMWUzOWZlNm1sMTBfU1ZH +RmlsdGVyDS8gOg0vWE1MTm9kZSA6DShmeG1sbm9kZS1ub2RlbmFtdmFsdTEgL0ludHR5cC9BcnJh +eWVUdXJidWxlbmM7Y2hpbGRyZW4vcmVzdWx0KHR1cmIyYXR0cmlidXRlOyAsc3RpdGNoVGlsZXMo +bm9TYmFzZUZyZXF1ZW5jeTAuMDVudW1PY3RhdjJ0ZmVDb21wb3NpdG9wZXJhdG9yKGluaW5Tb3Vy +Y2VHcmFwaGljeDAleGgxMHl3d2lkKUFJX19pZG9iamVjdC9EZWYgOzRmcmFjdGFsTm9pczRHYXVz +c2lhbkJsdTFiMmREZXZpZmVPZmZzZW9kZGRTcGVjdWxhckxpZ2h0aW5nUG9pbnRMLTUwMC16LTJ6 +c3BlY091c3VyZmFjZXR5bGw6RXhwb25lbnQoMUNvbnN0YWxpdFBhaWFyaXRobWV0a2s0azNrMzEw +MTIxMk1lcmdOb2QtMjE0eTRCZXZlbFNoYWRvd01vcnBob2xvZ3lhZGlsYXJhZGl1MS5iYm5iLWRu +YjJuNURpc3BsYWNlbWVudE1hcChibnMzeENoYW5uZWxTZWxlY1J5QUNvbG9yTWF0cmk0NDFtYW5p +bWFjY3VtdShub25jYWxjTShsaW5lYWQ1ZnJvbXRvNXRvcmVzdGFyYWx3YXlmaWxsZnJlZXplTmRk +aXRyZWJlZzBzNTU0bmNjOGNjY2NjY2M4Y2NjMWNjY25iKC01NDFDb29sQkRfNjZlckVyb2Q2NjRf +KDdyZXBlYXREKGluZGVmaW5zcGxpMXJlbW92UjEgMTsyMCAxNTsyMDAgMjAwOyAxNSAyMDsxIDEg +Y25QaXhlbFBsYXk1MCA1OzIwIDIwO0RpZmZ1c2U1eWVsbG93O2dyZWVuO2JsdWU7aW5kaWdvO3Zp +b2xldDtyZWQ7b3JhbkRpYXppbXVlbGV2NmQxMWwxcmVkNTAxMDEwMTIybnJlZDY4OC0xMzQyMC4w +LjE1bnRhOHg1NGRkb25GbG9vZmxvb2RibGFjazsgb3BhY2l0eTpzQ24zNW4xbjAxMDFHcmF5KDBP +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 +lWLD723rnSEnUzAhO5/X/n8zszVTw8iilFJKKVOmFqkfq/b/4Jg73lcD3wfDBxcJJKEPyzy45pul +0zhzlrMny6/MMdcq+nRhmWed9Noq1tuVxbJf2uma0k4kLPM0mq7JsvJjvhn7tS1ndadsS7NT2dHO +GSetppDL6fIzljRer6K8Ty87seMp+1qZI62OW85sC31aaApJemWdQrmyTkNOPfvHOuu89FIrMZ7Z +5Y3U2lol9Xg2Jvau0xad0q8S36/yV9NVXfTO91ul+yXvdUuq1/1Je9qJJ01sBvOyHjtI9fizwvKn +UFVNVnVtzk3tvRJbtuI7+52+pO+X/etWVlK0VXz3xx7fzuojusWfSPF/+me379WVO1PzrOJ3fzeu +EnfFVfzPM88ZH18c2ycpz2oaiV1F9yriPF2TFW/sK/GcrthXlfZKPKvq508XKfY0Epv6ex1ep1Hp +9FglzW7rlF+RKM6z4tvTlR9XEdunLynF8ifFN/dX0f50XRhX0d4Oh6eOm9pbYa+i52nhClcXrioc +bbQRx+vyRownTWxruiYr2phjztFOVswxV7mK2cqbqzhvFe1EitlmZGC3WZ8t4zCNrjfTaqe82Z32 +S1pNXH1X98I/jaarccbSLf1v6dVj6Tm/P53zZttY+uN5Ua/inNPl/0SaslZx9iR1E48v67IsknVZ +fV0f6kNN13V9qOv6UJP1oTDuD/WhppIDaDTCvLFMcajq7LHKs0wrF9lbYdNtlrHM8ahSnOGudYRV +LrpYlYEyrWYFZVUGZbRpYwMAAAAAMC+2tmfMHxFEayfrmqzpmqzLuqzLuqrfeuF7r8xZ5ouvyZrz +Zft0tbu72zxdk1WprVjaeb/lna6XGOPpmqyeLKdzulrSyarV5ikrnk+p7H5/0l+TZauvpsvx4ym/ +miyK85TuuF5v6Vd1e7oyrrJWk3VpbSvprDRPSUmSVqMLu8wVxRbjarKulZfkk8qLR9NlTdZ7pPdO +a13+lO/VRWdbucKNs/f/xdm9sdvGb+m37GitnT9bZvDzILP4iDIGXwaCMrTYhSVMpMBQPlQmw/lQ +H+pDfagPRemHmkCZD/WhRjK8w+N/sudpm1XCaVfl40OVgNhFfBCAAJQ4MY9mZ5F9UbW4ujbjykVO +K1BohqprQxOHMqWAbM9QhjN2RV1Rb6wahS6tcM1Z1XX0oDot5UNNfHyoDyUAAQAAAAAAgAAE8O0f +3LFnRFCFfKgH2Nf5UB/q86Gm0uUj/DIGHEW2gTBQ05SA2IWFM4UyNtf+3NLzz/vVq7WyJ733n7q1 +Nz7N/9TvU4knvbfxrY97epW2il4tfnd75e1r+/a89sb7Pv0tpfU+ldX2nZZKez3n92zrjbheSt0t +9ks/1376U1af1nPX27NnN+2ZfV5cq8WyXmxt/aa4PnZ67VeX1N77b2dTfCV+SudbLN2zxCRcja7p +1cvpPn0aie1A2jTOtc3Y0zUnktP6ud/r7Von7p/Z58z2O1qvnumcnun8ae+dE+N3+W1xv7c3/X+c +5fW3rmxJmUz8CZSZRB/RY3+y7UHoCT/MJ1NgPgE+1PRA+roRBwXGeoiiTPR1oWwj7DAs0cFtJpS5 +sx1n984T6WPaGzNte6s4P9/c3bavnVZ6x74mC08XrqKVN1c213aJq4mEsfXJmq58TVb1arpa3mqy +7K0UZyptdZGqtNGre5jYpQadyOs+zZw0Q6+9cV6LqV+v4vQq+r09XcT26U43PqJP2EfMLmml7njW +W+esldZKKZ3TFWedSN3/3f+NaRUv7f+JNO/sOd1nd9PuntN9tvetYs/pPm1fO+uctWaunR+0Sq/e +fqQfbWVpxrbTaquIq0izdTurSKvpil5FWm3bWSmttmaqdFslreKt9kZKvXoVvYrV1umKtYq2qmKd +LiK19kYb7Z2uaKtoK1tdLNtOWuu7X1pvZ9t0Xowf55knbVzb5vtvvUoq2jKNk4WYxj3OGTGN1vua +rGinq8pekfJX1XRlj1ZeUrQe80+a2Fb8r2z8Gf8nK/5H+1X8jz+lV/Hn56+mq5pI9l/0YVYMNVH2 +WTNij20jrSVhKBt9WXWnFDu2tor9odheVVtst9wrlh2zTx0OUyYTexo1zYQy8eL6VXzSR7y2iv9V +9GkkEz12rmL7BMo4vTa2d1W1V/FW0Wfl9Nqcs8y5Y3v+6HMixRt9TprYo8nKfWmernir+PFnrNF7 +XpMV3SP+KuacZfvLHH/GizPur6rpwj+RonuFfxqJ3T6VHL6MQffhujAsZcKv8xxsRNrXGeE0bfPk +DzU9fVmxzf/PIdJKnWwDYdxmen+oD4XrnZ6t7Ony21i6T1eWtCK5tLH+hH1g2pfOaD9HruMwlOgT +96E+1If6UB/qQ32oD/WhPtSH+lAf6kN9qA/1oT7Uh/pQ14f6UBPpQ9lJ9BE79tQxjQ== + + + JmvO3O9ZdpU+XT1Z8elsWT/WSj1LG21uSaP9W2u//MmiH3G9Ns5quibrZ+Wzql9N2hJ7dd1bXq/w +rSKtJivOryqtU96vqpy2W94pVF1Wj3VW0foU6uu5r5U9p8sztlWk+U7p9UpayZ6W5izzT1e0tqJV +xPlrFW2dVVJbZ6bXZP2sVdKfj6mk/jNL6lPIZ+eW7Xi66nRFO/9K96rCslbRX/5XMVdZtldTqP78 +yno1v7peyfmZdsv+6qvpihe7lbmK01Iq61dPZVcRzypSt15NpAn7iN/Xupy5Mcb59vw7lbJQ+zoG +GCeKRBjKtHUg0gPqFBhKxG0oUd59M87ftzqltZ1ePDP1TG+dTavP3LbifCulNbfTiWk7du+e1K3t +bKmdtu91iye1d+Ipm+Zq/+0lE/1eOnHinz7p27a4ZsefaX088W16aVenbWeusz6et+3Fl0zM+dKJ +1afs/LVW+23nY5rxz0v75ray3WLa9G/PKjvmS69T2tX9kol0+qUTZ3VaZa2NLaZdfda/ta+7z2zr +xXlOnHNH/E//ac15zotx7pynvfjpY/zvP+ecmE6csb2z/+183Dfn90rbrc2e8cVfsdO219ac5/zs +ni+Z2JdOxNVnfYub+rx4zsZ9Z7bv/titO60U00m9Vp+ObXu316fTJ/Wf3hRj+5a6/ZwxtrfpnH7J +xEovnfjtsm+e+GfFGP93W9wW/7y1dqU4e/5ca9Na6e32Syb25346M8XdPzNutz+rpfTSiZZelx27 +Np2eadu/7U/nnY1npTbLpvfWO2/2nDH2SyZOt/+XTPTrlw3KrX7JxGztfGvp/aY+O19Lsde/lcq+ +tVLveePF704n/vv9mV55f769tV4ykXbj3FR2vE9r259vcZ15fjvuxrQnxj87Z0vf7bd726fW67S3 +3/5tarGk95KJjuv8fom7q705y0nbP3ue1tJrL5mIs186sanfibub1jrde+Zs+7r1jCuus7vSv+7u +tlrPft9ejF3O+ZdO/J85S/z3HX/G2KfTXC+ZaK9X3NPKjte+tbX+xflWaqulFdPZ85KJl/qlEy/9 +ywYly+vduP3+t8VZdsz++Hb3tG+nf3fLzpdOdHr9b/97d9NZ2+KM6b/nfGVHOy+ZiP/SiXd+ne+e +cbY/vyvG3U0/43Y6P/u99mLZ3/Xb5u9KL5mY56UTZ/bLBqVpvvg//7Utu2JaMX18P9Nr570333/8 +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 +kpdtbBio2zoZUZZtmVF2YdqGRS3kMBuGNE22gTCOjIRhbFiou0ANxZeFHEWo04TYxJZ5KDGAgywM +PdtW4lBgSmRL6pQsdLABY8ZpW1jqRrGdApOFRjitQaZ5YJIRbSMLYZxwlF1U4zqwkFIcPTuOXuqs +bcssz7Zl1pzgugBoXzfx196fmXCUXTjjuhGOsiscZZe//c7XdRvn4ko/Iu3rbBmD7IoPQm3DSgTi +CKddfN3WWWHneT5PAmYXMVNgPCx5YSkjZgpMzxh+GYJIKmmi7CKE4nUaMRvBSiMcdkWvFGaeCEaR +9o3EmCkwIzEsdSWhjivJNlDMQmzrQC1Gd1AkiTBvgtqGdSGpeaL7oToPfPz0qwbGL/MwL2YKTH/R +IxbzrNWG2qaBsFATbWLoIOOI2acR3SkwFBNP22AEZhqKELZ9Wchp/mEeyGFf2Bcd95wEShaGGIl+ +B0MjzEY+LBzhtDDMPAHQSFroddh6rg8e3H+Lp6yOHTgIR9lF6sKwA4m4UpfJQtrmDUZgqNt8hNNE +nAbKPB/GgAEE+GEMGEB8AFIXgqXQB+QWKNxGcoCFmCbbPBIGQjAAw8yTbSBM+zAGjg== + + + hZgICzNS/lAfSpRA+VAfyiMcZZcoy7YMln0Yr/usAA5SfNlW2tjAaDxu/1Af6kL0YR0o/DCNfYZh +odugG6oaC4GqH1laTXpphWGf2PymfCiu00IN7DQ2Oo4t24CIlg+VRdymwdP5hKB2ZTTeM/b7jz/n +7ntxdafT0r8/uzZ2Wv/vz0sm5nen7fM2zf854zzzU5vr7X/3S+/MeGLq+efbmp1abL2d/rQRlBWg +RKd/ycSnf8nE7umyo7X5s1f81nb9Wy9uO9tte77U6/2cKe5MJ9L+fKetNH++3tVpZ7e50mtp9nzz +9XuvZ+vd9t5L3zbGbnHtmm3PiuvPin3WibFTp9fppbc+fX+/bFD+22um3jc/7jvp9Hu74p/4Xlyp +u9+b672XTLz30on32lyznfO/Voux7fnY1lrdXscze+N6H9uL6yUTvV42KJdMtP8uu9L5uP/p1zx/ +Vmstpk8t7b5t/5KJPaIRPTZ4EUFIY/qJJNPwoMSTycTZsv/vpI5p457X32K/E8t+fKvfeem81nrP +Sy+daPOs+N7auPbN8zadtzqV7i+70jnbUkx9ZotrbWzt/VqnbZ94us244p85T2ybzp90Tnrn9aZv +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 + + + diff --git a/brand/readme.md b/brand/readme.md new file mode 100644 index 000000000000..cf9460c0a0a5 --- /dev/null +++ b/brand/readme.md @@ -0,0 +1,2 @@ +This directory contains media assets, such as the Trivy logo. +Assets under this directory are provided under the Creative Commons - BY 4.0 License. For more details, see here: \ No newline at end of file diff --git a/buf.gen.yaml b/buf.gen.yaml new file mode 100644 index 000000000000..34f2690cd3f7 --- /dev/null +++ b/buf.gen.yaml @@ -0,0 +1,13 @@ +version: v2 +plugins: + - remote: buf.build/protocolbuffers/go:v1.34.0 + out: . + opt: + - paths=source_relative + # Using local protoc-gen-twirp since the remote twirp plugin is not available on buf.build + - local: protoc-gen-twirp + out: . + opt: + - paths=source_relative +inputs: + - directory: . \ No newline at end of file diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 000000000000..6828fe2c4ab9 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,10 @@ +version: v2 +modules: + - path: . + name: buf.build/aquasecurity/trivy +lint: + use: + - STANDARD +breaking: + use: + - FILE \ No newline at end of file diff --git a/ci/Dockerfile b/ci/Dockerfile deleted file mode 100644 index fba3d17e6888..000000000000 --- a/ci/Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM bepsays/ci-goreleaser:1.12-2 - -RUN apt-get -y update \ - && apt-get -y install vim rpm reprepro createrepo \ - && wget https://dl.bintray.com/homebrew/mirror/berkeley-db-18.1.32.tar.gz \ - - # Berkeley DB - && tar zxvf berkeley-db-18.1.32.tar.gz \ - && cd db-18.1.32/build_unix \ - - # Linux - && ../dist/configure --prefix=/usr/local --host=x86_64-linux \ - && make \ - && make install \ - - # Darwin - && make clean \ - && ../dist/configure --prefix=/usr/local --host=x86_64-apple-darwin15 \ - && make \ - && make install diff --git a/ci/deploy-deb.sh b/ci/deploy-deb.sh index 9516c2467dc1..a9e78dcd9465 100755 --- a/ci/deploy-deb.sh +++ b/ci/deploy-deb.sh @@ -1,17 +1,24 @@ #!/bin/bash -RELEASES=(wheezy jessie stretch buster trusty xenial bionic) +DEBIAN_RELEASES=$(debian-distro-info --supported) +UBUNTU_RELEASES=$(sort -u <(ubuntu-distro-info --supported-esm) <(ubuntu-distro-info --supported)) cd trivy-repo/deb -for release in ${RELEASES[@]}; do - echo "Adding deb package to $release" +for release in generic ${DEBIAN_RELEASES[@]} ${UBUNTU_RELEASES[@]}; do + echo "Removing deb package of $release" reprepro -A i386 remove $release trivy reprepro -A amd64 remove $release trivy - reprepro includedeb $release ../../dist/*Linux-64bit.deb + reprepro -A arm64 remove $release trivy +done + +for release in generic ${DEBIAN_RELEASES[@]} ${UBUNTU_RELEASES[@]}; do + echo "Adding deb package to $release" reprepro includedeb $release ../../dist/*Linux-32bit.deb + reprepro includedeb $release ../../dist/*Linux-64bit.deb + reprepro includedeb $release ../../dist/*Linux-ARM64.deb done git add . git commit -m "Update deb packages" -git push origin master +git push origin main diff --git a/ci/deploy-rpm.sh b/ci/deploy-rpm.sh index 4a0c6f094e44..5936505083de 100755 --- a/ci/deploy-rpm.sh +++ b/ci/deploy-rpm.sh @@ -1,20 +1,51 @@ -#!/bin/sh +#!/bin/bash -RPM_EL6=$(find dist/ -type f -name "*64bit.rpm" -printf "%f\n" | head -n1 | sed -e 's/_/-/g' -e 's/-Linux/.el6/' -e 's/-64bit/.x86_64/') -RPM_EL7=$(find dist/ -type f -name "*64bit.rpm" -printf "%f\n" | head -n1 | sed -e 's/_/-/g' -e 's/-Linux/.el7/' -e 's/-64bit/.x86_64/') +TRIVY_VERSION=$(find dist/ -type f -name "*64bit.rpm" -printf "%f\n" | head -n1 | sed -nre 's/^[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p') + +function create_common_rpm_repo () { + rpm_path=$1 + + ARCHES=("x86_64" "aarch64") + for arch in ${ARCHES[@]}; do + prefix=$arch + if [ "$arch" == "x86_64" ]; then + prefix="64bit" + elif [ "$arch" == "aarch64" ]; then + prefix="ARM64" + fi + + mkdir -p $rpm_path/$arch + cp ../dist/*${prefix}.rpm ${rpm_path}/$arch/ + createrepo_c -u https://get.trivy.dev/rpm/ --location-prefix="v"$TRIVY_VERSION --update $rpm_path/$arch + rm ${rpm_path}/$arch/*${prefix}.rpm + done +} + +function create_rpm_repo () { + version=$1 + rpm_path=rpm/releases/${version}/x86_64 + + mkdir -p $rpm_path + cp ../dist/*64bit.rpm ${rpm_path}/ + + createrepo_c -u https://get.trivy.dev/rpm/ --location-prefix="v"$TRIVY_VERSION --update $rpm_path + + rm ${rpm_path}/*64bit.rpm +} + +echo "Create RPM releases for Trivy v$TRIVY_VERSION" cd trivy-repo -mkdir -p rpm/releases/6/x86_64 -mkdir -p rpm/releases/7/x86_64 -cd rpm -cp ../../dist/*64bit.rpm releases/6/x86_64/${RPM_EL6} -cp ../../dist/*64bit.rpm releases/7/x86_64/${RPM_EL7} +echo "Processing common repository for RHEL/CentOS..." +create_common_rpm_repo rpm/releases -createrepo --update releases/6/x86_64/ -createrepo --update releases/7/x86_64/ +VERSIONS=(5 6 7 8 9) +for version in ${VERSIONS[@]}; do + echo "Processing RHEL/CentOS $version..." + create_rpm_repo $version +done git add . -git commit -m "Update rpm packages" -git push origin master - +git commit -m "Update rpm packages for Trivy v$TRIVY_VERSION" +git push origin main diff --git a/cmd/remic/main.go b/cmd/remic/main.go deleted file mode 100644 index b6c2c96b5b20..000000000000 --- a/cmd/remic/main.go +++ /dev/null @@ -1,67 +0,0 @@ -package main - -import ( - "os" - "strings" - - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/remic" - "github.com/urfave/cli" - - "github.com/knqyf263/trivy/pkg/log" -) - -func main() { - cli.AppHelpTemplate = `NAME: - {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} -USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} -VERSION: - {{.Version}}{{end}}{{end}}{{if .Description}} -DESCRIPTION: - {{.Description}}{{end}}{{if len .Authors}} -AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: - {{range $index, $author := .Authors}}{{if $index}} - {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} -OPTIONS: - {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}} -` - app := cli.NewApp() - app.Name = "remic" - app.Version = "0.0.1" - app.ArgsUsage = "file" - - app.Usage = "A simple and fast tool for detecting vulnerabilities in application dependencies" - - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "format, f", - Value: "table", - Usage: "format (table, json)", - }, - cli.StringFlag{ - Name: "severity, s", - Value: strings.Join(vulnerability.SeverityNames, ","), - Usage: "severity of vulnerabilities to be displayed", - }, - cli.StringFlag{ - Name: "output, o", - Usage: "output file name", - }, - cli.BoolFlag{ - Name: "debug, d", - Usage: "debug mode", - }, - } - - app.Action = func(c *cli.Context) error { - return remic.Run(c) - } - - err := app.Run(os.Args) - if err != nil { - log.Logger.Fatal(err) - } -} diff --git a/cmd/trivy/main.go b/cmd/trivy/main.go index 8162fd890273..33bd25416566 100644 --- a/cmd/trivy/main.go +++ b/cmd/trivy/main.go @@ -1,105 +1,51 @@ package main import ( - l "log" + "context" + "errors" "os" - "strings" - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" + "golang.org/x/xerrors" - "github.com/urfave/cli" + "github.com/aquasecurity/trivy/pkg/commands" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/plugin" + "github.com/aquasecurity/trivy/pkg/types" - "github.com/knqyf263/trivy/pkg" - "github.com/knqyf263/trivy/pkg/log" -) - -var ( - version = "dev" + _ "modernc.org/sqlite" // sqlite driver for RPM DB and Java DB ) func main() { - cli.AppHelpTemplate = `NAME: - {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} -USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} -VERSION: - {{.Version}}{{end}}{{end}}{{if .Description}} -DESCRIPTION: - {{.Description}}{{end}}{{if len .Authors}} -AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: - {{range $index, $author := .Authors}}{{if $index}} - {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} -OPTIONS: - {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}} -` - app := cli.NewApp() - app.Name = "trivy" - app.Version = version - app.ArgsUsage = "image_name" - - app.Usage = "A simple and comprehensive vulnerability scanner for containers" + if err := run(); err != nil { + var exitError *types.ExitError + if errors.As(err, &exitError) { + os.Exit(exitError.Code) + } - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "format, f", - Value: "table", - Usage: "format (table, json)", - }, - cli.StringFlag{ - Name: "input, i", - Value: "", - Usage: "input file path instead of image name", - }, - cli.StringFlag{ - Name: "severity, s", - Value: strings.Join(vulnerability.SeverityNames, ","), - Usage: "severities of vulnerabilities to be displayed (comma separated)", - }, - cli.StringFlag{ - Name: "output, o", - Usage: "output file name", - }, - cli.IntFlag{ - Name: "exit-code", - Usage: "Exit code when vulnerabilities were found", - Value: 0, - }, - cli.BoolFlag{ - Name: "skip-update", - Usage: "skip db update", - }, - cli.BoolFlag{ - Name: "clean, c", - Usage: "clean all cache", - }, - cli.BoolFlag{ - Name: "quiet, q", - Usage: "suppress progress bar", - }, - cli.BoolFlag{ - Name: "ignore-unfixed", - Usage: "display only fixed vulnerabilities", - }, - cli.BoolFlag{ - Name: "refresh", - Usage: "refresh DB (usually used after version update of trivy)", - }, - cli.BoolFlag{ - Name: "debug, d", - Usage: "debug mode", - }, - } + var userErr *types.UserError + if errors.As(err, &userErr) { + log.Fatal("Error", log.Err(userErr)) + } - app.Action = func(c *cli.Context) error { - return pkg.Run(c) + log.Fatal("Fatal error", log.Err(err)) } +} - err := app.Run(os.Args) - if err != nil { - if log.Logger != nil { - log.Logger.Fatal(err) +func run() error { + // Trivy behaves as the specified plugin. + if runAsPlugin := os.Getenv("TRIVY_RUN_AS_PLUGIN"); runAsPlugin != "" { + log.InitLogger(false, false) + if err := plugin.Run(context.Background(), runAsPlugin, plugin.Options{Args: os.Args[1:]}); err != nil { + return xerrors.Errorf("plugin error: %w", err) } - l.Fatal(err) + return nil } + + // Ensure cleanup on exit + defer commands.Cleanup() + + // Set up signal handling for graceful shutdown + ctx := commands.NotifyContext(context.Background()) + + return commands.Run(ctx) } diff --git a/contrib/Trivy.gitlab-ci.yml b/contrib/Trivy.gitlab-ci.yml new file mode 100644 index 000000000000..f91588240346 --- /dev/null +++ b/contrib/Trivy.gitlab-ci.yml @@ -0,0 +1,29 @@ +Trivy_container_scanning: + stage: test + image: + name: alpine:3.11 + variables: + # Override the GIT_STRATEGY variable in your `.gitlab-ci.yml` file and set it to `fetch` if you want to provide a `clair-whitelist.yml` + # file. See https://docs.gitlab.com/ee/user/application_security/container_scanning/index.html#overriding-the-container-scanning-template + # for details + GIT_STRATEGY: none + IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" + allow_failure: true + before_script: + - export TRIVY_VERSION=${TRIVY_VERSION:-v0.19.2} + - apk add --no-cache curl docker-cli + - curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin ${TRIVY_VERSION} + - curl -sSL -o /tmp/trivy-gitlab.tpl https://github.com/aquasecurity/trivy/raw/${TRIVY_VERSION}/contrib/gitlab.tpl + - trivy registry login --username "$CI_REGISTRY_USER" --password "$CI_REGISTRY_PASSWORD" $CI_REGISTRY + script: + - trivy --exit-code 0 --cache-dir .trivycache/ --no-progress --format template --template "@/tmp/trivy-gitlab.tpl" -o gl-container-scanning-report.json $IMAGE + cache: + paths: + - .trivycache/ + artifacts: + reports: + container_scanning: gl-container-scanning-report.json + dependencies: [] + only: + refs: + - branches diff --git a/contrib/asff.tpl b/contrib/asff.tpl new file mode 100644 index 000000000000..3cadbb03ce79 --- /dev/null +++ b/contrib/asff.tpl @@ -0,0 +1,161 @@ +{ + "Findings": [ + {{- $t_first := true -}} + {{- range . -}} + {{- $target := .Target -}} + {{- $image := .Target -}} + {{- if gt (len $image) 127 -}} + {{- $image = $image | regexFind ".{124}$" | printf "...%v" -}} + {{- end}} + {{- range .Vulnerabilities -}} + {{- if $t_first -}} + {{- $t_first = false -}} + {{- else -}} + , + {{- end -}} + {{- $severity := .Severity -}} + {{- if eq $severity "UNKNOWN" -}} + {{- $severity = "INFORMATIONAL" -}} + {{- end -}} + {{- $description := .Description -}} + {{- if gt (len $description ) 512 -}} + {{- $description = (substr 0 512 $description) | printf "%v .." -}} + {{- end}} + { + "SchemaVersion": "2018-10-08", + "Id": "{{ $target }}/{{ .VulnerabilityID }}", + "ProductArn": "arn:aws:securityhub:{{ env "AWS_REGION" }}::product/aquasecurity/aquasecurity", + "GeneratorId": "Trivy/{{ .VulnerabilityID }}", + "AwsAccountId": "{{ env "AWS_ACCOUNT_ID" }}", + "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], + "CreatedAt": "{{ now | date "2006-01-02T15:04:05.999999999Z07:00" }}", + "UpdatedAt": "{{ now | date "2006-01-02T15:04:05.999999999Z07:00" }}", + "Severity": { + "Label": "{{ $severity }}" + }, + "Title": "Trivy found a vulnerability to {{ .VulnerabilityID }} in container {{ $target }}, related to {{ .PkgName }}", + "Description": {{ escapeString $description | printf "%q" }}, + {{ if not (empty .PrimaryURL) -}} + "Remediation": { + "Recommendation": { + "Text": "More information on this vulnerability is provided in the hyperlink", + "Url": "{{ .PrimaryURL }}" + } + }, + {{ end -}} + "ProductFields": { "Product Name": "Trivy" }, + "Resources": [ + { + "Type": "Container", + "Id": "{{ $target }}", + "Partition": "aws", + "Region": "{{ env "AWS_REGION" }}", + "Details": { + "Container": { "ImageName": "{{ $image }}" }, + "Other": { + "CVE ID": "{{ .VulnerabilityID }}", + "CVE Title": {{ .Title | printf "%q" }}, + "PkgName": "{{ .PkgName }}", + "Installed Package": "{{ .InstalledVersion }}", + "Patched Package": "{{ .FixedVersion }}", + "NvdCvssScoreV3": "{{ (index .CVSS (sourceID "nvd")).V3Score }}", + "NvdCvssVectorV3": "{{ (index .CVSS (sourceID "nvd")).V3Vector }}", + "NvdCvssScoreV2": "{{ (index .CVSS (sourceID "nvd")).V2Score }}", + "NvdCvssVectorV2": "{{ (index .CVSS (sourceID "nvd")).V2Vector }}" + } + } + } + ], + "RecordState": "ACTIVE" + } + {{- end -}} + {{- range .Misconfigurations -}} + {{- if $t_first -}}{{- $t_first = false -}}{{- else -}},{{- end -}} + {{- $severity := .Severity -}} + {{- if eq $severity "UNKNOWN" -}} + {{- $severity = "INFORMATIONAL" -}} + {{- end -}} + {{- $description := .Description -}} + {{- if gt (len $description ) 512 -}} + {{- $description = (substr 0 512 $description) | printf "%v .." -}} + {{- end}} + { + "SchemaVersion": "2018-10-08", + "Id": "{{ $target }}/{{ .ID }}", + "ProductArn": "arn:aws:securityhub:{{ env "AWS_REGION" }}::product/aquasecurity/aquasecurity", + "GeneratorId": "Trivy/{{ .ID }}", + "AwsAccountId": "{{ env "AWS_ACCOUNT_ID" }}", + "Types": [ "Software and Configuration Checks" ], + "CreatedAt": "{{ now | date "2006-01-02T15:04:05.999999999Z07:00" }}", + "UpdatedAt": "{{ now | date "2006-01-02T15:04:05.999999999Z07:00" }}", + "Severity": { + "Label": "{{ $severity }}" + }, + "Title": "Trivy found a misconfiguration in {{ $target }}: {{ escapeString .Title }}", + "Description": {{ escapeString $description | printf "%q" }}, + "Remediation": { + "Recommendation": { + "Text": "{{ .Resolution }}", + "Url": "{{ .PrimaryURL }}" + } + }, + "ProductFields": { "Product Name": "Trivy" }, + "Resources": [ + { + "Type": "Other", + "Id": "{{ $target }}", + "Partition": "aws", + "Region": "{{ env "AWS_REGION" }}", + "Details": { + "Other": { + "Message": "{{ escapeString .Message }}", + "Filename": "{{ $target }}", + "StartLine": "{{ .CauseMetadata.StartLine }}", + "EndLine": "{{ .CauseMetadata.EndLine }}" + } + } + } + ], + "RecordState": "ACTIVE" + } + {{- end -}} + {{- range .Secrets -}} + {{- if $t_first -}}{{- $t_first = false -}}{{- else -}},{{- end -}} + {{- $severity := .Severity -}} + {{- if eq $severity "UNKNOWN" -}} + {{- $severity = "INFORMATIONAL" -}} + {{- end -}} + { + "SchemaVersion": "2018-10-08", + "Id": "{{ $target }}", + "ProductArn": "arn:aws:securityhub:{{ env "AWS_REGION" }}::product/aquasecurity/aquasecurity", + "GeneratorId": "Trivy", + "AwsAccountId": "{{ env "AWS_ACCOUNT_ID" }}", + "Types": [ "Sensitive Data Identifications" ], + "CreatedAt": "{{ now | date "2006-01-02T15:04:05.999999999Z07:00" }}", + "UpdatedAt": "{{ now | date "2006-01-02T15:04:05.999999999Z07:00" }}", + "Severity": { + "Label": "{{ $severity }}" + }, + "Title": "Trivy found a secret in {{ $target }}: {{ .Title }}", + "Description": "Trivy found a secret in {{ $target }}: {{ .Title }}", + "ProductFields": { "Product Name": "Trivy" }, + "Resources": [ + { + "Type": "Other", + "Id": "{{ $target }}", + "Partition": "aws", + "Region": "{{ env "AWS_REGION" }}", + "Details": { + "Other": { + "Filename": "{{ $target }}" + } + } + } + ], + "RecordState": "ACTIVE" + } + {{- end -}} + {{- end }} + ] +} diff --git a/contrib/gitlab-codequality.tpl b/contrib/gitlab-codequality.tpl new file mode 100644 index 000000000000..5beec257e078 --- /dev/null +++ b/contrib/gitlab-codequality.tpl @@ -0,0 +1,103 @@ +{{- /* Template based on https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types */ -}} +[ + {{- $t_first := true }} + {{- range . }} + {{- $target := .Target }} + {{- range .Vulnerabilities -}} + {{- if $t_first -}} + {{- $t_first = false -}} + {{ else -}} + , + {{- end }} + { + "type": "issue", + "check_name": "container_scanning", + "categories": [ "Security" ], + "description": {{ list .VulnerabilityID .PkgName .InstalledVersion .Title | join " - " | printf "%q" }}, + "fingerprint": "{{ list .VulnerabilityID .PkgName .InstalledVersion $target | join "" | sha1sum }}", + "content": {{ .Description | printf "%q" }}, + "severity": {{ if eq .Severity "LOW" -}} + "info" + {{- else if eq .Severity "MEDIUM" -}} + "minor" + {{- else if eq .Severity "HIGH" -}} + "major" + {{- else if eq .Severity "CRITICAL" -}} + "critical" + {{- else -}} + "info" + {{- end }}, + "location": { + "path": "{{ $target }}", + "lines": { + "begin": 0 + } + } + } + {{- end -}} + {{- range .Misconfigurations -}} + {{- if $t_first -}} + {{- $t_first = false -}} + {{ else -}} + , + {{- end }} + { + "type": "issue", + "check_name": "container_scanning", + "categories": [ "Security" ], + "description": {{ list "Misconfig" .ID .Title | join " - " | printf "%q" }}, + "fingerprint": "{{ list .ID .Title $target | join "" | sha1sum }}", + "content": {{ .Description | printf "%q" }}, + "severity": {{ if eq .Severity "LOW" -}} + "info" + {{- else if eq .Severity "MEDIUM" -}} + "minor" + {{- else if eq .Severity "HIGH" -}} + "major" + {{- else if eq .Severity "CRITICAL" -}} + "critical" + {{- else -}} + "info" + {{- end }}, + "location": { + "path": "{{ $target }}", + "lines": { + "begin": {{ .CauseMetadata.StartLine }} + } + } + } + {{- end -}} + {{- range .Secrets -}} + {{- if $t_first -}} + {{- $t_first = false -}} + {{ else -}} + , + {{- end }} + { + "type": "issue", + "check_name": "container_scanning", + "categories": [ "Security" ], + "description": {{ list "Secret" .RuleID .Title | join " - " | printf "%q" }}, + "fingerprint": "{{ list .RuleID .Title $target | join "" | sha1sum }}", + "content": {{ .Title | printf "%q" }}, + "severity": {{ if eq .Severity "LOW" -}} + "info" + {{- else if eq .Severity "MEDIUM" -}} + "minor" + {{- else if eq .Severity "HIGH" -}} + "major" + {{- else if eq .Severity "CRITICAL" -}} + "critical" + {{- else -}} + "info" + {{- end }}, + "location": { + "path": "{{ $target }}", + "lines": { + "begin": {{ .StartLine }} + } + } + } + {{- end -}} + {{- end }} +] diff --git a/contrib/gitlab.tpl b/contrib/gitlab.tpl new file mode 100644 index 000000000000..7ff8abd8bfec --- /dev/null +++ b/contrib/gitlab.tpl @@ -0,0 +1,112 @@ +{{- /* Template based on https://docs.gitlab.com/ee/user/application_security/container_scanning/#reports-json-format */ -}} +{ + "version": "15.0.7", + "scan": { + "analyzer": { + "id": "trivy", + "name": "Trivy", + "vendor": { + "name": "Aqua Security" + }, + "version": "{{ appVersion }}" + }, + "end_time": "{{ now | date "2006-01-02T15:04:05" }}", + "scanner": { + "id": "trivy", + "name": "Trivy", + "url": "https://github.com/aquasecurity/trivy/", + "vendor": { + "name": "Aqua Security" + }, + "version": "{{ appVersion }}" + }, + "start_time": "{{ now | date "2006-01-02T15:04:05" }}", + "status": "success", + "type": "container_scanning" + }, + {{- $image := "Unknown" -}} + {{- $os := "Unknown" -}} + {{- range . }} + {{- if eq .Class "os-pkgs" -}} + {{- $target := .Target }} + {{- $image = $target | regexFind "[^\\s]+" }} + {{- $os = $target | splitList "(" | last | trimSuffix ")" }} + {{- end }} + {{- end }} + "vulnerabilities": [ + {{- $t_first := true }} + {{- range . }} + {{- range .Vulnerabilities -}} + {{- if $t_first -}} + {{- $t_first = false -}} + {{ else -}} + , + {{- end }} + { + "id": "{{ .VulnerabilityID }}", + "name": {{ .Title | printf "%q" }}, + "description": {{ .Description | printf "%q" }}, + "severity": {{ if eq .Severity "UNKNOWN" -}} + "Unknown" + {{- else if eq .Severity "LOW" -}} + "Low" + {{- else if eq .Severity "MEDIUM" -}} + "Medium" + {{- else if eq .Severity "HIGH" -}} + "High" + {{- else if eq .Severity "CRITICAL" -}} + "Critical" + {{- else -}} + "{{ .Severity }}" + {{- end }}, + "solution": {{ if .FixedVersion -}} + "Upgrade {{ .PkgName }} to {{ .FixedVersion }}" + {{- else -}} + "No solution provided" + {{- end }}, + "location": { + "dependency": { + "package": { + "name": "{{ .PkgName }}" + }, + "version": "{{ .InstalledVersion }}" + }, + {{- /* TODO: No mapping available - https://github.com/aquasecurity/trivy/issues/332 */}} + "operating_system": "{{ $os }}", + "image": "{{ $image }}" + }, + "identifiers": [ + { + {{- /* TODO: Type not extractable - https://github.com/aquasecurity/trivy-db/pull/24 */}} + "type": "cve", + "name": "{{ .VulnerabilityID }}", + "value": "{{ .VulnerabilityID }}" + {{- /* cf. https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/e3d280d7f0862ca66a1555ea8b24016a004bb914/dist/container-scanning-report-format.json#L157-179 */}} + {{- if .PrimaryURL | regexMatch "^(https?|ftp)://.+" -}}, + "url": "{{ .PrimaryURL }}" + {{- end }} + } + ], + "links": [ + {{- $l_first := true -}} + {{- range .References -}} + {{- if $l_first -}} + {{- $l_first = false }} + {{- else -}} + , + {{- end -}} + {{- if . | regexMatch "^(https?|ftp)://.+" -}} + { + "url": "{{ . }}" + } + {{- else -}} + {{- $l_first = true }} + {{- end -}} + {{- end }} + ] + } + {{- end -}} + {{- end }} + ], + "remediations": [] +} diff --git a/contrib/html.tpl b/contrib/html.tpl new file mode 100644 index 000000000000..e92b1b1cf7f5 --- /dev/null +++ b/contrib/html.tpl @@ -0,0 +1,148 @@ + + + + +{{- if . }} + + {{- escapeXML ( index . 0 ).Target }} - Trivy Report - {{ now }} + + + +

{{- escapeXML ( index . 0 ).Target }} - Trivy Report - {{ now }}

+ + {{- range . }} + + {{- if (eq (len .Vulnerabilities) 0) }} + + {{- else }} + + + + + + + + + {{- range .Vulnerabilities }} + + + + + + + + + {{- end }} + {{- end }} + {{- if (eq (len .Misconfigurations ) 0) }} + + {{- else }} + + + + + + + + {{- range .Misconfigurations }} + + + + + + + + {{- end }} + {{- end }} + {{- end }} +
{{ .Type | toString | escapeXML }}
No Vulnerabilities found
PackageVulnerability IDSeverityInstalled VersionFixed VersionLinks
{{ escapeXML .PkgName }}{{ escapeXML .VulnerabilityID }}{{ escapeXML .Vulnerability.Severity }}{{ escapeXML .InstalledVersion }}{{ escapeXML .FixedVersion }}
No Misconfigurations found
TypeMisconf IDCheckSeverityMessage
{{ escapeXML .Type }}{{ escapeXML .ID }}{{ escapeXML .Title }}{{ escapeXML .Severity }}
+{{- else }} + + +

Trivy Returned Empty Report

+{{- end }} + + diff --git a/contrib/install.sh b/contrib/install.sh new file mode 100755 index 000000000000..0024c0f2bf05 --- /dev/null +++ b/contrib/install.sh @@ -0,0 +1,397 @@ +#!/bin/sh +set -e +# Code generated by godownloader on 2020-01-14T10:03:29Z. DO NOT EDIT. +# + +usage() { + this=$1 + cat </dev/null +} +echoerr() { + echo "$@" 1>&2 +} +log_prefix() { + echo "$0" +} +_logp=6 +log_set_priority() { + _logp="$1" +} +log_priority() { + if test -z "$1"; then + echo "$_logp" + return + fi + [ "$1" -le "$_logp" ] +} +log_tag() { + case $1 in + 0) echo "emerg" ;; + 1) echo "alert" ;; + 2) echo "crit" ;; + 3) echo "err" ;; + 4) echo "warning" ;; + 5) echo "notice" ;; + 6) echo "info" ;; + 7) echo "debug" ;; + *) echo "$1" ;; + esac +} +log_debug() { + log_priority 7 || return 0 + echo "$(log_prefix)" "$(log_tag 7)" "$@" +} +log_info() { + log_priority 6 || return 0 + echo "$(log_prefix)" "$(log_tag 6)" "$@" +} +log_err() { + log_priority 3 || return 0 + echoerr "$(log_prefix)" "$(log_tag 3)" "$@" +} +log_crit() { + log_priority 2 || return 0 + echoerr "$(log_prefix)" "$(log_tag 2)" "$@" +} +uname_os() { + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$os" in + cygwin_nt*) os="windows" ;; + mingw*) os="windows" ;; + msys_nt*) os="windows" ;; + esac + echo "$os" +} +uname_arch() { + arch=$(uname -m) + case $arch in + x86_64) arch="amd64" ;; + x86) arch="386" ;; + i686) arch="386" ;; + i386) arch="386" ;; + ppc64le) arch="ppc64le" ;; + aarch64) arch="arm64" ;; + armv5*) arch="armv5" ;; + armv6*) arch="armv6" ;; + armv7*) arch="armv7" ;; + s390*) arch="s390x" ;; + esac + echo ${arch} +} +uname_os_check() { + os=$(uname_os) + case "$os" in + darwin) return 0 ;; + dragonfly) return 0 ;; + freebsd) return 0 ;; + linux) return 0 ;; + android) return 0 ;; + nacl) return 0 ;; + netbsd) return 0 ;; + openbsd) return 0 ;; + plan9) return 0 ;; + solaris) return 0 ;; + windows) return 0 ;; + esac + log_crit "uname_os_check '$(uname -s)' got converted to '$os' which is not a GOOS value. Please file bug at https://github.com/client9/shlib" + return 1 +} +uname_arch_check() { + arch=$(uname_arch) + case "$arch" in + 386) return 0 ;; + amd64) return 0 ;; + arm64) return 0 ;; + armv5) return 0 ;; + armv6) return 0 ;; + armv7) return 0 ;; + ppc64) return 0 ;; + ppc64le) return 0 ;; + mips) return 0 ;; + mipsle) return 0 ;; + mips64) return 0 ;; + mips64le) return 0 ;; + s390x) return 0 ;; + amd64p32) return 0 ;; + esac + log_crit "uname_arch_check '$(uname -m)' got converted to '$arch' which is not a GOARCH value. Please file bug report at https://github.com/client9/shlib" + return 1 +} +untar() { + tarball=$1 + case "${tarball}" in + *.tar.gz | *.tgz) tar --no-same-owner -xzf "${tarball}" ;; + *.tar) tar --no-same-owner -xf "${tarball}" ;; + *.zip) unzip "${tarball}" ;; + *) + log_err "untar unknown archive format for ${tarball}" + return 1 + ;; + esac +} +http_download_curl() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + code=$(curl -w '%{http_code}' -sL -o "$local_file" "$source_url") + else + code=$(curl -w '%{http_code}' -sL -H "$header" -o "$local_file" "$source_url") + fi + if [ "$code" != "200" ]; then + log_debug "http_download_curl received HTTP status $code" + return 1 + fi + return 0 +} +http_download_wget() { + local_file=$1 + source_url=$2 + header=$3 + if [ -z "$header" ]; then + wget -q -O "$local_file" "$source_url" + else + wget -q --header "$header" -O "$local_file" "$source_url" + fi +} +http_download() { + log_debug "http_download $2" + if is_command curl; then + http_download_curl "$@" + return + elif is_command wget; then + http_download_wget "$@" + return + fi + log_crit "http_download unable to find wget or curl" + return 1 +} +http_copy() { + tmp=$(mktemp) + http_download "${tmp}" "$1" "$2" || return 1 + body=$(cat "$tmp") + rm -f "${tmp}" + echo "$body" +} +github_release() { + owner_repo=$1 + version=$2 + test -z "$version" && version="latest" + giturl="https://github.com/${owner_repo}/releases/${version}" + json=$(http_copy "$giturl" "Accept:application/json") + test -z "$json" && return 1 + version=$(echo "$json" | tr -s '\n' ' ' | sed 's/.*"tag_name":"//' | sed 's/".*//') + test -z "$version" && return 1 + echo "$version" +} +hash_sha256() { + TARGET=${1:-/dev/stdin} + if is_command gsha256sum; then + hash=$(gsha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command sha256sum; then + hash=$(sha256sum "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command shasum; then + hash=$(shasum -a 256 "$TARGET" 2>/dev/null) || return 1 + echo "$hash" | cut -d ' ' -f 1 + elif is_command openssl; then + hash=$(openssl -dst openssl dgst -sha256 "$TARGET") || return 1 + echo "$hash" | cut -d ' ' -f a + else + log_crit "hash_sha256 unable to find command to compute sha-256 hash" + return 1 + fi +} +hash_sha256_verify() { + TARGET=$1 + checksums=$2 + if [ -z "$checksums" ]; then + log_err "hash_sha256_verify checksum file not specified in arg2" + return 1 + fi + BASENAME=${TARGET##*/} + want=$(grep "${BASENAME}" "${checksums}" 2>/dev/null | tr '\t' ' ' | cut -d ' ' -f 1) + if [ -z "$want" ]; then + log_err "hash_sha256_verify unable to find checksum for '${TARGET}' in '${checksums}'" + return 1 + fi + got=$(hash_sha256 "$TARGET") + if [ "$want" != "$got" ]; then + log_err "hash_sha256_verify checksum for '$TARGET' did not verify ${want} vs $got" + return 1 + fi +} +cat /dev/null < + +{{- range . -}} +{{- $failures := len .Vulnerabilities }} + + {{- if not (eq .Type "") }} + + + + {{- end -}} + {{ range .Vulnerabilities }} + + {{ escapeXML .Description }} + + {{- end }} + + +{{- $target := .Target }} +{{- if .MisconfSummary }} + +{{- else }} + +{{- end }} + {{- if not (eq .Type "") }} + + + + {{- end -}} + {{ range .Misconfigurations }} + + {{- if (eq .Status "FAIL") }} + + {{- $target }}: + {{- with .CauseMetadata }} + {{- .StartLine }} + {{- if lt .StartLine .EndLine }}:{{ .EndLine }}{{ end }}: Occurrences: + {{- range $i := .Occurrences -}} + via {{ .Filename }}: + {{- .Location.StartLine }} + {{- if lt .Location.StartLine .Location.EndLine }}:{{ .Location.EndLine }}{{ end }} ({{ .Resource }}) + {{- end -}} + Code: + {{- range .Code.Lines }} + {{- if .IsCause }}{{ escapeXML .Content }} {{- end }} + {{- end }} + {{- end }} + {{- escapeXML .Description }} + + {{- end }} + + {{- end }} + + +{{- if .Licenses }} + {{- $licenses := len .Licenses }} + {{ range .Licenses }} + + + + {{- end }} + +{{- end }} + +{{- if .Secrets }} + {{- $secrets := len .Secrets }} + {{ range .Secrets }} + + {{ escapeXML .Match }} + + {{- end }} + +{{- end }} + +{{- end }} + diff --git a/docs/build/Dockerfile b/docs/build/Dockerfile new file mode 100644 index 000000000000..22ab95afddca --- /dev/null +++ b/docs/build/Dockerfile @@ -0,0 +1,6 @@ +FROM squidfunk/mkdocs-material:9.5.44 + +# https://squidfunk.github.io/mkdocs-material/getting-started/?h=macros#with-docker-material-for-mkdocs + +COPY requirements.txt . +RUN pip install -r requirements.txt diff --git a/docs/build/requirements.in b/docs/build/requirements.in new file mode 100644 index 000000000000..c4124a27c7fd --- /dev/null +++ b/docs/build/requirements.in @@ -0,0 +1,3 @@ +mkdocs-material==9.5.44 +mkdocs-macros-plugin +mike diff --git a/docs/build/requirements.txt b/docs/build/requirements.txt new file mode 100644 index 000000000000..2dea909675a8 --- /dev/null +++ b/docs/build/requirements.txt @@ -0,0 +1,114 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --output-file=docs/build/requirements.txt docs/build/requirements.in +# +babel==2.16.0 + # via mkdocs-material +certifi==2024.8.30 + # via requests +charset-normalizer==3.4.0 + # via requests +click==8.1.7 + # via mkdocs +colorama==0.4.6 + # via mkdocs-material +ghp-import==2.1.0 + # via mkdocs +hjson==3.1.0 + # via + # mkdocs-macros-plugin + # super-collections +idna==3.10 + # via requests +importlib-metadata==8.5.0 + # via mike +importlib-resources==6.4.5 + # via mike +jinja2==3.1.4 + # via + # mike + # mkdocs + # mkdocs-macros-plugin + # mkdocs-material +markdown==3.7 + # via + # mkdocs + # mkdocs-material + # pymdown-extensions +markupsafe==3.0.2 + # via + # jinja2 + # mkdocs +mergedeep==1.3.4 + # via + # mkdocs + # mkdocs-get-deps +mike==2.1.3 + # via -r docs/build/requirements.in +mkdocs==1.6.1 + # via + # mike + # mkdocs-macros-plugin + # mkdocs-material +mkdocs-get-deps==0.2.0 + # via mkdocs +mkdocs-macros-plugin==1.3.7 + # via -r docs/build/requirements.in +mkdocs-material==9.5.44 + # via -r docs/build/requirements.in +mkdocs-material-extensions==1.3.1 + # via mkdocs-material +packaging==24.2 + # via + # mkdocs + # mkdocs-macros-plugin +paginate==0.5.7 + # via mkdocs-material +pathspec==0.12.1 + # via + # mkdocs + # mkdocs-macros-plugin +platformdirs==4.3.6 + # via mkdocs-get-deps +pygments==2.19.2 + # via mkdocs-material +pymdown-extensions==10.12 + # via mkdocs-material +pyparsing==3.2.0 + # via mike +python-dateutil==2.9.0.post0 + # via + # ghp-import + # mkdocs-macros-plugin +pyyaml==6.0.2 + # via + # mike + # mkdocs + # mkdocs-get-deps + # mkdocs-macros-plugin + # pymdown-extensions + # pyyaml-env-tag +pyyaml-env-tag==0.1 + # via + # mike + # mkdocs +regex==2024.11.6 + # via mkdocs-material +requests==2.32.3 + # via mkdocs-material +six==1.16.0 + # via python-dateutil +super-collections==0.5.3 + # via mkdocs-macros-plugin +termcolor==2.5.0 + # via mkdocs-macros-plugin +urllib3==2.2.3 + # via requests +verspec==0.1.0 + # via mike +watchdog==6.0.0 + # via mkdocs +zipp==3.21.0 + # via importlib-metadata diff --git a/docs/commercial/compare.md b/docs/commercial/compare.md new file mode 100644 index 000000000000..32e0f9f9d53b --- /dev/null +++ b/docs/commercial/compare.md @@ -0,0 +1,86 @@ +# Aqua Security is the home of Trivy + +Trivy is proudly maintained by [Aqua Security](https://aquasec.com). +If you liked Trivy, you will love Aqua which builds on top of Trivy to provide even more enhanced capabilities for a complete security management offering. +In this page you can find a high level comparison between Trivy Open Source and Aqua's commercial product. +If you'd like to learn more or request a demo, [click here to contact us](./contact.md). + +## User experience + +| Feature | Trivy OSS | Aqua | +| --- | --- | --- | +| Interface | CLI tool | CLI tool
Enterprise-grade web application
SaaS or on-prem | +| Search & Discover | - | Easily search for security issues across all workloads and infrastructure in your organization
Visually discover risks across your organization | +| User management | - | Multi account
Granular permissions (RBAC)
Single Sign On (SSO) | +| Support | Some skills required for setup and integration
Best effort community support | Personal onboarding by Aqua Customer Success
SLA backed professional support | +| Scalability & Availability | Single scan at a time | Centralized scanning service supports concurrent scans efficiently
Highly available production grade architecture | +| Rate limiting | Assets hosted on public free infrastructure and could be rate limited | Assets hosted on Aqua infrastructure and does not have limitations | + +## Vulnerability scanning + +| Feature | Trivy OSS | Aqua | +| --- | --- | --- | +| Vulnerabilities sources | Based on open source vulnerability feeds | Based on open source and commercial vulnerability feeds | +| New Vulnerabilities SLA | No SLA | Commercial level SLA | +| Package managers | Find packages in lock files | Find packages in lock files or reconstructed lock files | +| Vulnerability management | Manually ignore specific vulnerabilities by ID or property | Advanced vulnerability management solution
Vulnerability tracking and suppression
Incident lifecycle management | +| Vulnerability prioritization | Manually triage by severity | Multiple prioritization tools:
Accessibility of the affected resources
Exploitability of the vulnerability
Open Source packages health and trustworthiness score
Affected image layers | +| Reachability analysis | - | Analyze source code to eliminate vulnerabilities of unused dependencies | +| Contextual vulnerabilities | - | Reduce irrelevant vulnerabilities based on environmental factors (e.g. Spring4Shell not relevant due to JDK version) | +| Compiled binaries | Find embedded dependencies in Go and Rust binaries
Find SBOM by hash in public Sigstore | In addition, identify popular applications | + +## Container scanning + +| Feature | Trivy OSS | Aqua | +| --- | --- | --- | +| Windows containers | - | Support scanning windows containers | +| Scan container registries | - | Connect to any container registries and automatically scan it | +| Private registries | Standard registry authenticationCloud authentication with ECR, GCR, ACR | Supports registry specific authentication schemes | +| Layer cache | Local cache directory | Scalable Cloud cache | + +## Advanced scanning + +| Feature | Trivy OSS | Aqua | +| --- | --- | --- | +| Malware scanning | - | Scan container images for malware | +| Sandbox scanning | - | Use DTA (Dynamic threat analysis) to run and test container images' behavior to detect sophisticated threats | +| SAST (code scanning) | - | Analyze source code for security issues and vulnerabilities | + +## Policy and enforcement + +| Feature | Trivy OSS | Aqua | +| --- | --- | --- | +| Kubernetes admission | - | Validating Kubernetes Admission based on automatic or user defined policy | +| CI/CD policies | Can fail the entire build on any finding | Granular policies to fail builds based on custom criteria | +| Container engine | - | Block incompliant images from running at container engine level | +| Block vulnerable packages | - | vShield – monitor and block usage of vulnerable packages | + +## Secrets scanning + +| Feature | Trivy OSS | Aqua | +| --- | --- | --- | +| Detected patterns | Basic patterns | Advanced patterns | +| Leaked secrets validation | - | Automatically checks if leaked secrets are valid and usable | + +## IaC/CSPM scanning + +| Feature | Trivy OSS | Aqua | +| --- | --- | --- | +| Infrastructure as Code (IaC) | Many popular languages as detailed [here](https://trivy.dev/docs/latest/scanner/misconfiguration/check/builtin/) | In addition, Build Pipeline configuration scanning | +| Checks customization | Create custom checks with Rego | Create custom checks in no-code interface
Customize existing checks with organizational preferences | +| Cloud scanning | AWS (subset of services) | AWS, Azure, GCP, Alibaba Cloud, Oracle Cloud | +| Compliance frameworks | CIS, NSA, vendor guides | More than 25 compliance programs | +| Custom compliance | Create in YAML | Create in a web UI | +| Remediation advice | Basic | AI powered specialized remediation guides | + +## Kubernetes scanning + +| Feature | Trivy OSS | Aqua | +| --- | --- | --- | +Scan initiation | CLI / Kubernetes Operator | Kubernetes Operator / Management web application | +Results consumption | kubectl / CRD / Prometheus exporter | In addition, Advanced UI dashboards, Automatic notifications and incident management flows | +Cluster discovery | Kubeconfig | Automatic discovery thorough cloud onboarding | +Workload image scanning | Scanning in cluster, requires capacity planning | Scanning offloaded to Aqua service, little impact on scanned clusters | +| Cluster scanning | CIS, NSA, PSS | More than 25 compliance programs | +| Scope | Single cluster | Multi cluster, Cloud relationship | +| Scalability | Reports limited by in-cluster etcd storage (size and number of reports) | Cloud-based storage (unlimited scalability) | diff --git a/docs/commercial/contact.md b/docs/commercial/contact.md new file mode 100644 index 000000000000..7dbc29ef5407 --- /dev/null +++ b/docs/commercial/contact.md @@ -0,0 +1,17 @@ + + + diff --git a/docs/community/contribute/checks/overview.md b/docs/community/contribute/checks/overview.md new file mode 100644 index 000000000000..5c6e73522e64 --- /dev/null +++ b/docs/community/contribute/checks/overview.md @@ -0,0 +1,130 @@ +# Contribute Rego Checks + +The following guide provides an overview of contributing checks to the default checks in Trivy. + +All of the checks in Trivy can be found in the [trivy-checks](https://github.com/aquasecurity/trivy-checks/tree/main) repository on GitHub. Before you begin writing a check, ensure: + +1. The check does not already exist as part of the default checks in the [trivy-checks](https://github.com/aquasecurity/trivy-checks/tree/main) repository. +2. The pull requests in the [trivy-checks](https://github.com/aquasecurity/trivy-checks/pulls) repository to see whether someone else is already contributing the check that you wanted to add. +3. The [issues in Trivy](https://github.com/aquasecurity/trivy/issues) to see whether any specific checks are missing in Trivy that you can contribute. + +If anything is unclear, please [start a discussion](https://github.com/aquasecurity/trivy/discussions/new) and we will do our best to help. + +## Check structure + +Checks are written in Rego and follow a particular structure in Trivy. Below is an example check for AWS: + +```rego +# METADATA +# title: "RDS IAM Database Authentication Disabled" +# description: "Ensure IAM Database Authentication is enabled for RDS database instances to manage database access" +# scope: package +# schemas: +# - input: schema["aws"] +# related_resources: +# - https://docs.aws.amazon.com/neptune/latest/userguide/iam-auth.html +# custom: +# id: AVD-AWS-0176 +# avd_id: AVD-AWS-0176 +# provider: aws +# service: rds +# severity: MEDIUM +# short_code: enable-iam-auth +# recommended_action: "Modify the PostgreSQL and MySQL type RDS instances to enable IAM database authentication." +# input: +# selector: +# - type: cloud +# subtypes: +# - service: rds +# provider: aws + +package builtin.aws.rds.aws0176 + +deny[res] { + instance := input.aws.rds.instances[_] + instance.engine.value == ["postgres", "mysql"][_] + not instance.iamauthenabled.value + res := result.new("Instance does not have IAM Authentication enabled", instance.iamauthenabled) +} +``` + +## Verify the provider and service exists + +Every check for a cloud service references a cloud provider. The list of providers are found in the [Trivy](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/providers) repository. + +Before writing a new check for a cloud provider, you need to verify if the cloud provider or resource type that your check targets is supported by Trivy. If it's not, you'll need to add support for it. Additionally, if the provider that you want to target exists, you need to check whether the service your policy will target is supported. As a reference you can take a look at the AWS provider [here](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/providers/aws/aws.go). + +???+ note + New Kubernetes and Dockerfile checks do not require any additional provider definitions. You can find an example of a Dockerfile check [here](https://github.com/aquasecurity/trivy-checks/blob/main/checks/docker/add_instead_of_copy.rego) and a Kubernetes check [here](https://github.com/aquasecurity/trivy-checks/blob/main/checks/kubernetes/general/CPU_not_limited.rego). + + +### Add Support for a New Service in an existing Provider + +[Please reference the documentation on adding Support for a New Service](./service-support.md). + +This guide also showcases how to add new properties for an existing Service. + +## Create a new .rego file + +The following directory in the trivy-checks repository contains all of our custom checks. Depending on what type of check you want to create, you will need to nest a new `.rego` file in either of the [subdirectories](https://github.com/aquasecurity/trivy-checks/tree/main/checks): + +* cloud: All checks related to cloud providers and their services +* docker: Docker specific checks +* kubernetes: Kubernetes specific checks + +## Check Package name + +Have a look at the existing package names in the [built in checks](https://github.com/aquasecurity/trivy-checks/tree/main/checks). + +The package name should be in the format `builtin.PROVIDER.SERVICE.ID`, e.g. `builtin.aws.rds.aws0176`. + +## Generating an ID + +Every check has a custom ID that is referenced throughout the metadata of the check to uniquely identify the check. If you plan to contribute your check back into the [trivy-checks](https://github.com/aquasecurity/trivy-checks) repository, it will require a valid ID. + +Running `make id` in the root of the trivy-checks repository will provide you with the next available _ID_ for your rule. + +## Check Schemas + +Rego Checks for Trivy can utilise Schemas to map the input to specific objects. The schemas available are listed [here.](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/rego/schemas). + +More information on using the builtin schemas is provided in the [main documentation.](../../../guide/scanner/misconfiguration/custom/schema.md) + +## Check Metadata + +The metadata is the top section that starts with `# METADATA`, and has to be placed on top of the check. You can copy and paste from another check as a starting point. This format is effectively _yaml_ within a Rego comment, and is [defined as part of Rego itself](https://www.openpolicyagent.org/docs/latest/policy-language/#metadata). + +For detailed information on each component of the Check Metadata, please refer to the [main documentation.](../../../guide/scanner/misconfiguration/custom/index.md) + +Note that while the Metadata is optional in your own custom checks for Trivy, if you are contributing your check to the Trivy builtin checks, the Metadata section will be required. + + +## Writing Rego Rules + +Rules are defined using _OPA Rego_. You can find a number of examples in the `checks` directory ([Link](https://github.com/aquasecurity/trivy-checks/tree/main/checks)). The [OPA documentation](https://www.openpolicyagent.org/docs/latest/policy-language/) is a great place to start learning Rego. You can also check out the [Rego Playground](https://play.openpolicyagent.org/) to experiment with Rego, and [join the OPA Slack](https://slack.openpolicyagent.org/). + + +```rego +deny[res] { + instance := input.aws.rds.instances[_] + instance.engine.value == ["postgres", "mysql"][_] + not instance.iamauthenabled.value + res := result.new("Instance does not have IAM Authentication enabled", instance.iamauthenabled) +} +``` + +The rule should return a result, which can be created using `result.new`. This function does not need to be imported, it is defined internally and provided at runtime. The first argument is the message to display and the second argument is the resource that the issue was detected on. + +It is possible to pass any rego variable that references a field of the input document. + +## Generate docs + +Finally, you'll want to generate documentation for your newly added rule. Please run `make docs` in the [trivy-checks](https://github.com/aquasecurity/trivy-checks) directory to generate the documentation for your new policy and submit a PR for us to take a look at. + +## Adding Tests + +All Rego checks need to have tests. There are many examples of these in the `checks` directory for each check ([Link](https://github.com/aquasecurity/trivy-checks/tree/main/checks)). More information on how to write tests for Rego checks is provided in the [custom misconfiguration](../../../guide/scanner/misconfiguration/custom/testing.md) section of the docs. + +## Example PR + +You can see a full example PR for a new rule being added here: [https://github.com/aquasecurity/defsec/pull/1000](https://github.com/aquasecurity/defsec/pull/1000). diff --git a/docs/community/contribute/checks/service-support.md b/docs/community/contribute/checks/service-support.md new file mode 100644 index 000000000000..ff600db578fb --- /dev/null +++ b/docs/community/contribute/checks/service-support.md @@ -0,0 +1,69 @@ +# Add Service Support + +A service refers to a service by a cloud provider. This section details how to add a new service to an existing provider. All contributions need to be made to the [trivy repository](https://github.com/aquasecurity/trivy/). + +## Prerequisites + +Before you begin, verify that the [provider](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/providers) does not already have the service that you plan to add. + +## Adding a new service to an existing provider + +Adding a new service involves two steps. The service will need a data structure to store information about the required resources that will be scanned. Additionally, the service will require one or more adapters to convert the scan targetes as input(s) into the aforementioned data structure. + +### Create a new file in the provider directory + +In this example, we are adding the CodeBuild service to the AWS provider. + +First, create a new directory and file for your new service under the provider directory: e.g. [aws/codebuild/codebuild.go](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/providers/aws/codebuild/codebuild.go) + +The CodeBuild service will require a structure `struct` to hold the information on the input that is scanned. The input is the CodeBuild resource that a user configured and wants to scan for misconfiguration. + +``` +type CodeBuild struct { + Projects []Project +} +``` + +The CodeBuild service manages `Project` resources. The `Project` struct has been added to hold information about each Project resources; `Project` Resources in turn manage `ArtifactSettings`: + +``` +type Project struct { + Metadata iacTypes.Metadata + ArtifactSettings ArtifactSettings + SecondaryArtifactSettings []ArtifactSettings +} + +type ArtifactSettings struct { + Metadata iacTypes.Metadata + EncryptionEnabled iacTypes.BoolValue +} +``` + +The `iacTypes.Metadata` struct is embedded in all of the Trivy types and provides a common set of metadata for all resources. This includes the file and line number where the resource was defined and the name of the resource. + +A resource in this example `Project` can have a name and can optionally be encrypted. Instead of using raw string and bool types respectively, we use the trivy types `iacTypes.Metadata` and `iacTypes.BoolValue`. These types wrap the raw values and provide additional metadata about the value. For instance, whether it was set by the user and the file and line number where the resource was defined. + +Have a look at the other providers and services in the [`iac/providers`](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/providers) directory in Trivy. + +Next you'll need to add a reference to your new service struct in the [provider struct](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/providers/aws/aws.go) at `pkg/iac/providers/aws/aws.go`: + +``` +type AWS struct { + ... + CodeBuild codebuild.CodeBuild + ... +} +``` + +### Update Adapters + +Now you'll need to update all of the [adapters](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/adapters) which populate the struct of the provider that you have been using. Following the example above, if you want to add support for CodeBuild in Terraform, you'll need to update the Terraform AWS adapter as shown here: [`trivy/pkg/iac/adapters/terraform/aws/codebuild/adapt.go`](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/adapters/terraform/aws/codebuild/adapt.go). + +Another example for updating the adapters is provided in the [following PR.](https://github.com/aquasecurity/defsec/pull/1000/files) Additionally, please refer to the respective Terraform documentation on the provider to which you are adding the service. For instance, the Terraform documentation for AWS CodeBuild is provided [here.](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codebuild_project) + + +## Create a new Schema for your provider + +Once the new service has been added to the provider, you need to create the schema for the service as part of the provider schema. + +This process has been automated with mage commands. In the Trivy root directory run `mage schema:generate` to generate the schema for your new service and `mage schema:verify`. diff --git a/docs/community/contribute/discussion.md b/docs/community/contribute/discussion.md new file mode 100644 index 000000000000..d5590e9cf5fa --- /dev/null +++ b/docs/community/contribute/discussion.md @@ -0,0 +1,49 @@ +# Discussions +Thank you for taking interest in contributing to Trivy! + +Trivy uses [GitHub Discussion](https://github.com/aquasecurity/trivy/discussions) for bug reports, feature requests, and questions. +If maintainers decide to accept a new feature or confirm that it is a bug, they will close the discussion and create a [GitHub Issue](https://github.com/aquasecurity/trivy/issues) associated with that discussion. + +- Feel free to open discussions for any reason. When you open a new discussion, you'll have to select a discussion category as described below. +- Please spend a small amount of time giving due diligence to the issue/discussion tracker. Your discussion might be a duplicate. If it is, please add your comment to the existing issue/discussion. +- Remember that users might search for your issue/discussion in the future, so please give it a meaningful title to help others. +- The issue should clearly explain the reason for opening, the proposal if you have any, and any relevant technical information. + +There are 4 categories: + +- 💡 [Ideas](https://github.com/aquasecurity/trivy/discussions/categories/ideas) + - Share ideas for new features +- 🔎 [False Detection](https://github.com/aquasecurity/trivy/discussions/categories/false-detection) + - Report false positives/negatives +- 🐛 [Bugs](https://github.com/aquasecurity/trivy/discussions/categories/bugs) + - Report something that is not working as expected +- 🙏 [Q&A](https://github.com/aquasecurity/trivy/discussions/categories/q-a) + - Ask the community for help + +!!! note + If you find any false positives or false negatives, please make sure to report them under the "False Detection" category, not "Bugs". + +## False detection +Trivy depends on [multiple data sources](https://trivy.dev/docs/latest/scanner/vulnerability/#data-sources). +Sometime these databases contain mistakes. + +If Trivy can't detect any CVE-IDs or shows false positive result, at first please follow the next steps: + +1. Run Trivy with `-f json` that shows data sources. +2. According to the shown data source, make sure that the security advisory in the data source is correct. + +If the data source is correct and Trivy shows wrong results, please raise an issue on Trivy. + +### GitHub Advisory Database +Visit [here](https://github.com/advisories) and search CVE-ID. + +If you find a problem, it'll be nice to fix it: [How to contribute to a GitHub security advisory](https://github.blog/2022-02-22-github-advisory-database-now-open-to-community-contributions/) + +### GitLab Advisory Database +Visit [here](https://advisories.gitlab.com/) and search CVE-ID. + +If you find a problem, it'll be nice to fix it: [Create an issue to GitLab Advisory Database](https://gitlab.com/gitlab-org/security-products/gemnasium-db/-/issues) + +### Red Hat CVE Database +Visit [here](https://access.redhat.com/security/security-updates/?cwe=476#/cve) and search CVE-ID. + diff --git a/docs/community/contribute/issue.md b/docs/community/contribute/issue.md new file mode 100644 index 000000000000..c402f73b9cfc --- /dev/null +++ b/docs/community/contribute/issue.md @@ -0,0 +1,7 @@ +# Issues +Thank you for taking interest in contributing to Trivy! + +Trivy uses [GitHub Discussion](./discussion.md) for bug reports, feature requests, and questions. + +!!! warning + Issues created by non-maintainers will be immediately closed. \ No newline at end of file diff --git a/docs/community/contribute/pr.md b/docs/community/contribute/pr.md new file mode 100644 index 000000000000..97e64a2dbf4f --- /dev/null +++ b/docs/community/contribute/pr.md @@ -0,0 +1,242 @@ +Thank you for taking interest in contributing to Trivy! + +1. Every Pull Request should have an associated GitHub issue link in the PR description. Note that issues are created by Trivy maintainers based on feedback provided in a GitHub discussion. Please refer to the [issue](./issue.md) and [discussion](./discussion.md) pages for explanation about this process. If you think your change is trivial enough, you can skip the issue and instead add justification and explanation in the PR description. +1. Your PR is more likely to be accepted if it focuses on just one change. +1. There's no need to add or tag reviewers. +1. If a reviewer commented on your code or asked for changes, please remember to respond with a comment. Do not mark the discussion as resolved. It's up to the reviewer to mark it resolved (in case the suggested fix addresses the problem properly). PRs with unresolved issues should not be merged (even if the comment is unclear or requires no action from your side). +1. Please include a comment with the results before and after your change. +1. Your PR is more likely to be accepted if it includes tests (We have not historically been very strict about tests, but we would like to improve this!). +1. If your PR affects the user experience in some way, please update the README.md and the CLI help accordingly. + +## Development +Install the necessary tools for development by following their respective installation instructions. + +- [Go](https://go.dev/doc/install) +- [Mage](https://magefile.org/) + +### Build +After making changes to the Go source code, build the project with the following command: + +```shell +$ mage build +$ ./trivy -h +``` + +### Lint +You must pass the linter checks: + +```shell +$ mage lint:run +``` + +Additionally, you need to have run `go mod tidy`, so execute the following command as well: + +```shell +$ mage tidy +``` + +To autofix linters use the following command: +```shell +$ mage lint:fix +``` + +### Unit tests +Your PR must pass all the unit tests. You can test it as below. + +``` +$ mage test:unit +``` + +### Integration tests +Your PR must pass all the integration tests. You can test it as below. + +``` +$ mage test:integration +``` + +### Protocol Buffers +If you update protobuf files (`.proto`), you need to regenerate the Go code: + +```shell +$ mage protoc:generate +``` + +You can also format and lint protobuf files: + +```shell +$ mage protoc:fmt # Format protobuf files +$ mage protoc:lint # Lint protobuf files +$ mage protoc:breaking # Check for breaking changes against main branch +``` + +### Documentation +If you update CLI flags, you need to generate the CLI references. +The test will fail if they are not up-to-date. + +```shell +$ mage docs:generate +``` + +You can build the documents as below and view it at http://localhost:8000. + +``` +$ mage docs:serve +``` + +## Title +It is not that strict, but we use the title conventions in this repository. +Each commit message doesn't have to follow the conventions as long as it is clear and descriptive since it will be squashed and merged. + +### Format of the title + +``` +(): +``` + +The `type` and `scope` should always be lowercase as shown below. + +**Allowed `` values:** + +- **feat** for a new feature for the user, not a new feature for build script. Such commit will trigger a release bumping a MINOR version. +- **fix** for a bug fix for the user, not a fix to a build script. Such commit will trigger a release bumping a PATCH version. +- **perf** for performance improvements. Such commit will trigger a release bumping a PATCH version. +- **docs** for changes to the documentation. +- **style** for formatting changes, missing semicolons, etc. +- **refactor** for refactoring production code, e.g. renaming a variable. +- **test** for adding missing tests, refactoring tests; no production code change. +- **build** for updating build configuration, development tools or other changes irrelevant to the user. +- **chore** for updates that do not apply to the above, such as dependency updates. +- **ci** for changes to CI configuration files and scripts +- **revert** for revert to a previous commit + +**Allowed `` values:** + +checks: + +- vuln +- misconf +- secret +- license + +mode: + +- image +- fs +- repo +- sbom +- k8s +- server +- aws +- vm +- plugin + +os: + +- alpine +- redhat +- alma +- rocky +- azure +- oracle +- debian +- ubuntu +- amazon +- suse +- photon +- distroless + +language: + +- ruby +- php +- python +- nodejs +- rust +- dotnet +- java +- go +- elixir +- dart +- julia + +vuln: + +- os +- lang + +config: + +- kubernetes +- dockerfile +- terraform +- cloudformation + +container + +- docker +- podman +- containerd +- oci + +cli: + +- cli +- flag + +SBOM: + +- cyclonedx +- spdx +- purl + +others: + +- helm +- report +- db +- parser +- deps + +The `` can be empty (e.g. if the change is a global or difficult to assign to a single component), in which case the parentheses are omitted. + +**Breaking changes** + +A PR, introducing a breaking API change, needs to append a `!` after the type/scope. + +### Example titles + +``` +feat(alma): add support for AlmaLinux +``` + +``` +feat(vuln)!: delete the existing CLI flag +``` + +``` +fix(oracle): handle advisories with ksplice versions +``` + +``` +docs(misconf): add comparison with Conftest and TFsec +``` + +``` +chore(deps): bump go.uber.org/zap from 1.19.1 to 1.20.0 +``` + +**NOTE**: please do not use `chore(deps): update fanal` and something like that if you add new features or fix bugs in Trivy-related projects. +The PR title should describe what the PR adds or fixes even though it just updates the dependency in Trivy. + +## Commits + + +## Understand where your pull request belongs + +Trivy is composed of several repositories that work together: + +- [Trivy](https://github.com/aquasecurity/trivy) is the client-side, user-facing, command line tool. +- [vuln-list](https://github.com/aquasecurity/vuln-list) is a vulnerability database, aggregated from different sources, and normalized for easy consumption. Think of this as the "server" side of the trivy command line tool. **There should be no pull requests to this repo** +- [vuln-list-update](https://github.com/aquasecurity/vuln-list-update) is the code that maintains the vuln-list database. +- [trivy-db](https://github.com/aquasecurity/trivy-db) maintains the vulnerability database pulled by Trivy CLI. +- [go-dep-parser](https://github.com/aquasecurity/go-dep-parser) is a library for parsing lock files such as package-lock.json and Gemfile.lock. diff --git a/docs/community/contribute/vulnerability-database/add-vulnerability-source.md b/docs/community/contribute/vulnerability-database/add-vulnerability-source.md new file mode 100644 index 000000000000..c560ed3fc543 --- /dev/null +++ b/docs/community/contribute/vulnerability-database/add-vulnerability-source.md @@ -0,0 +1,144 @@ +# Add Vulnerability Advisory Source + +This guide walks through the process of adding a new vulnerability advisory source to Trivy. + +!!! info + For an overview of how Trivy's vulnerability database works, see the [Overview](overview.md) page. + +## Prerequisites + +Before starting, ensure you have: + +1. Identified the upstream advisory source and its API/format +2. Checked that the data source doesn't already exist in Trivy +3. Created a GitHub discussion or issue to discuss the addition + +## Required Changes + +To add a new vulnerability advisory source, you'll need to make changes across three repositories. Below we'll use the Echo OS support as an example. + +### Step 1: Add Fetcher Script (vuln-list-update) + +!!! note + Skip this step if your advisory source is already managed in a Git repository (e.g., GitHub, GitLab). + +Create a fetcher script in [vuln-list-update] to collect advisories from the upstream source. + +**Key tasks:** + +- Fetch advisories from the upstream API or source +- Validate the advisory format and data +- Save advisories as JSON files in the [vuln-list] directory structure +- **Store original data as-is where possible**: Avoid preprocessing or modifying advisory fields. Save the raw data exactly as provided by the upstream source (format conversion like YAML to JSON is acceptable for consistency) +- Include all necessary metadata (CVE ID, affected versions, severity, etc.) + +**Example PR:** + +- [feat(echo): Add Echo Support (vuln-list-update#350)](https://github.com/aquasecurity/vuln-list-update/pull/350) + +### Step 2: Add Parser (trivy-db) + +Create a parser in [trivy-db] to transform raw advisories into Trivy's database format. + +**Key tasks:** + +- Create a new vulnerability source in `pkg/vulnsrc/` +- Implement the advisory parsing logic +- Map advisory fields to Trivy's vulnerability schema +- Handle version ranges and affected packages correctly +- Store CVE mappings if available +- Add unit tests for the parser + +**Example PR:** + +- [feat(echo): Add Echo Support (trivy-db#528)](https://github.com/aquasecurity/trivy-db/pull/528) + +### Step 3: Add OS/Ecosystem Support (Trivy) + +Update [trivy] to support the new operating system or package ecosystem. + +**Key tasks:** + +- Add OS analyzer in `pkg/fanal/analyzer/os/` to detect the OS +- Implement vulnerability detection logic if special handling is needed +- Add integration tests with test data +- Update documentation to include the new data source + +**Example PR:** + +- [feat(echo): Add Echo Support (trivy#8833)](https://github.com/aquasecurity/trivy/pull/8833) + +## Complete Example: Echo OS Support + +The Echo OS support was added through three coordinated PRs: + +1. **vuln-list-update**: Fetches Echo advisories from `https://advisory.echohq.com/data.json` + - PR: https://github.com/aquasecurity/vuln-list-update/pull/350 +2. **trivy-db**: Parses Echo advisories and stores them in the database + - PR: https://github.com/aquasecurity/trivy-db/pull/528 +3. **Trivy**: Detects Echo OS and scans for vulnerabilities + - PR: https://github.com/aquasecurity/trivy/pull/8833 + +## Testing Your Changes + +### Test vuln-list-update + +First, fetch all existing advisories (required for building the database): + +```bash +cd vuln-list-update +go run main.go -vuln-list-dir /path/to/vuln-list +``` + +Then, test your new data source by fetching only your target: + +```bash +go run main.go -target your-source -vuln-list-dir /path/to/vuln-list +``` + +Verify that advisories are correctly saved in the vuln-list directory. + +### Test trivy-db + +```bash +cd trivy-db +make db-build CACHE_DIR=/path/to/cache +``` + +Check that the database is built without errors and contains your advisories. + +!!! note + The `CACHE_DIR` should point to the parent directory of your vuln-list directory. For example, if your vuln-list is at `/tmp/test/vuln-list`, set `CACHE_DIR=/tmp/test`. + +You can inspect the built database using BoltDB viewer tools like [boltwiz](https://github.com/Moniseeta/boltwiz): + +```bash +# Open the database +boltwiz out/trivy.db +``` + +This allows you to verify that your vulnerabilities are correctly stored in the database. + +### Test Trivy + +```bash +# Build Trivy with your changes +mage build + +# Use your local database +./trivy image --skip-db-update --cache-dir /path/to/cache your-test-image +``` + +Verify that vulnerabilities from your new data source are detected correctly. + +## Getting Help + +If you have questions or need help: + +1. Check existing data sources for reference implementations +2. [Start a discussion](https://github.com/aquasecurity/trivy/discussions/new) in the Trivy repository + +[vuln-list]: https://github.com/aquasecurity/vuln-list +[vuln-list-update]: https://github.com/aquasecurity/vuln-list-update +[trivy-db]: https://github.com/aquasecurity/trivy-db +[trivy]: https://github.com/aquasecurity/trivy \ No newline at end of file diff --git a/docs/community/contribute/vulnerability-database/overview.md b/docs/community/contribute/vulnerability-database/overview.md new file mode 100644 index 000000000000..4b4298621b04 --- /dev/null +++ b/docs/community/contribute/vulnerability-database/overview.md @@ -0,0 +1,86 @@ +# Vulnerability Data Sources + +This section explains how Trivy's vulnerability database works and how to contribute new advisory data sources. + +## Overview + +Trivy's vulnerability database is built through a multi-repository workflow involving three main repositories: + +```mermaid +graph LR + A[Advisory Sources] -->|vuln-list-update| B[vuln-list] + B --> C["trivy-db
(Trivy DB)"] + C --> D["trivy
(Trivy CLI)"] + E[GitHub-managed
Advisories] --> C +``` + +### Workflow Steps + +1. **Advisory Collection** ([vuln-list-update]) + - Fetch raw advisories from upstream sources + - Store them in [vuln-list] repository + - Run periodically via cron to keep advisories up-to-date + - This step can be skipped if advisories are already managed in a Git repository (e.g., GitHub Security Advisories) + +2. **Database Build** ([trivy-db]) + - Parse advisories from [vuln-list] or directly from Git-managed sources + - Transform them into Trivy's database format + - Publish the built database periodically via cron + +3. **Database Consumption** ([trivy]) + - Download the latest vulnerability database at scan time + - Use it to detect vulnerabilities in scan targets + +## Why Store Advisories in vuln-list? + +For data sources that are not already Git-managed, storing advisories in the [vuln-list] repository provides several benefits: + +- **Transparency**: Easy to track changes and differences between advisory versions +- **Web UI**: Browse advisories directly on GitHub with a user-friendly interface +- **Stability**: Mitigate issues when upstream advisory servers are unstable or unavailable +- **Shareability**: Provide stable URLs to reference specific advisories +- **Data Quality**: Validate advisory data before committing to vuln-list, preventing malformed data or unexpected format changes from breaking Trivy DB +- **Historical Data**: Preserve past advisories when upstream formats change + +## Repository Overview + +### [vuln-list-update] + +This repository contains scripts that fetch advisories from various upstream sources. Each data source has its own package that handles: + +- Fetching advisories from APIs or web sources +- Validating the advisory format and data +- Saving them to the [vuln-list] repository + +### [vuln-list] + +This repository serves as a data storage for raw advisories fetched by [vuln-list-update]. Key characteristics: + +- Contains raw advisory data in JSON format +- Updated automatically by [vuln-list-update] scripts via cron +- **Not for manual contributions**: Direct pull requests to this repository are not accepted +- Used as the source for [trivy-db] to build the vulnerability database + +### [trivy-db] + +This repository contains parsers that transform raw advisories into Trivy's database format. Each data source has its own vulnerability source handler that: + +- Reads advisory files from [vuln-list] or directly from Git-managed sources (e.g., GitHub Security Advisories) +- Maps advisory fields to Trivy's schema +- Stores vulnerability information in the database + +### [trivy] + +The main Trivy repository contains: + +- OS and package analyzers to detect what's installed +- Vulnerability detection logic + +## Next Steps + +Ready to add a new vulnerability advisory source? See the [Add Vulnerability Advisory Source](add-vulnerability-source.md) guide for detailed steps. + +[vuln-list]: https://github.com/aquasecurity/vuln-list +[vuln-list-update]: https://github.com/aquasecurity/vuln-list-update +[trivy-db]: https://github.com/aquasecurity/trivy-db +[trivy]: https://github.com/aquasecurity/trivy \ No newline at end of file diff --git a/docs/community/maintainer/backporting.md b/docs/community/maintainer/backporting.md new file mode 100644 index 000000000000..bc70f26a9836 --- /dev/null +++ b/docs/community/maintainer/backporting.md @@ -0,0 +1,59 @@ +# Backporting Process + +This document outlines the backporting process for Trivy, including when to create patch releases and how to perform the backporting. + +## When to Create Patch Releases + +In general, small changes should not be backported and should be included in the next minor release. +However, patch releases should be made in the following cases: + +* Fixes for HIGH or CRITICAL vulnerabilities in Trivy itself or Trivy's dependencies +* Fixes for bugs that cause panic during Trivy execution or otherwise interfere with normal usage + +In these cases, the fixes should be backported using the procedure [described below](#backporting-procedure). +At the maintainer's discretion, other bug fixes may be included in the patch release containing these hotfixes. + +## Versioning + +Trivy follows [Semantic Versioning](https://semver.org/), using version numbers in the format MAJOR.MINOR.PATCH. +When creating a patch release, the PATCH part of the version number is incremented. +For example, if a fix is being distributed for v0.50.0, the patch release would be v0.50.1. + +## Backporting Procedure + +1. A release branch (e.g., `release/v0.50`) is automatically created when a new minor version is released. +1. Create a pull request (PR) against the main branch with the necessary fixes. If the fixes are already merged into the main branch, skip this step. +1. Once the PR with the fixes is merged, comment `@aqua-bot backport ` on the PR (e.g., `@aqua-bot backport release/v0.50`). This will trigger the automated backporting process using GitHub Actions. +1. The automated process will create a new PR with the backported changes. Ensure that all tests pass for this PR. +1. Once the tests pass, merge the automatically created PR into the release branch. +1. Merge [a release PR](release-flow.md) on the release branch and release the patch version. + +!!! note + Even if a conflict occurs, a PR is created by forceful commit, in which case the conflict should be resolved manually. + If you want to re-run a backport of the same PR, close the existing PR, delete the branch and re-run it. + +### Example +To better understand the backporting procedure, let's walk through an example using the releases of v0.50. + +```mermaid +gitGraph: + commit id:"Feature 1" + commit id:"v0.50.0 release" tag:"v0.50.0" + + branch "release/v0.50" + + checkout main + commit id:"Bugfix 1" + + checkout "release/v0.50" + cherry-pick id:"Bugfix 1" + + checkout main + commit id:"Feature 2" + commit id:"Bugfix 2" + commit id:"Feature 3" + + checkout "release/v0.50" + cherry-pick id:"Bugfix 2" + commit id:"v0.50.1 release" tag:"v0.50.1" +``` \ No newline at end of file diff --git a/docs/community/maintainer/help-wanted.md b/docs/community/maintainer/help-wanted.md new file mode 100644 index 000000000000..41111731a9fc --- /dev/null +++ b/docs/community/maintainer/help-wanted.md @@ -0,0 +1,78 @@ +# Overview + +We use two labels [help wanted](#help-wanted) and [good first +issue](#good-first-issue) to identify issues that have been specially groomed +for new contributors. The `good first issue` label is a subset of `help wanted` +label, indicating that members have committed to providing extra assistance for +new contributors. All `good first issue` items also have the `help wanted` +label. + +## Help Wanted + +Items marked with the `help wanted` label need to ensure that they are: + +- **Low Barrier to Entry** + + It should be tractable for new contributors. Documentation on how that type of + change should be made should already exist. + +- **Clear Task** + + The task is agreed upon and does not require further discussions in the + community. Call out if that area of code is untested and requires new + fixtures. + + API / CLI behavior is decided and included in the OP issue, for example: "The + new command syntax is `trivy --format yaml IMAGE_NAME`"_ with + expected validations called out. + +- **Goldilocks priority** + + Not too high that a core contributor should do it, but not too low that it + isn't useful enough for a core contributor to spend time to review it, answer + questions, help get it into a release, etc. + +- **Up-To-Date** + + Often these issues become obsolete and have already been done, are no longer + desired, no longer make sense, have changed priority or difficulty , etc. + + +## Good First Issue + +Items marked with the `good first issue` label are intended for _first-time +contributors_. It indicates that members will keep an eye out for these pull +requests and shepherd it through our processes. + +These items need to ensure that they follow the guidelines for `help wanted` +labels (above) in addition to meeting the following criteria: + +- **No Barrier to Entry** + + The task is something that a new contributor can tackle without advanced + setup, or domain knowledge. + +- **Solution Explained** + + The recommended solution is clearly described in the issue. + +- **Provides Context** + + If background knowledge is required, this should be explicitly mentioned and a + list of suggested readings included. + +- **Gives Examples** + + Link to examples of similar implementations so new contributors have a + reference guide for their changes. + +- **Identifies Relevant Code** + + The relevant code and tests to be changed should be linked in the issue. + +- **Ready to Test** + + There should be existing tests that can be modified, or existing test cases + fit to be copied. If the area of code doesn't have tests, before labeling the + issue, add a test fixture. This prep often makes a great `help wanted` task! + diff --git a/docs/community/maintainer/pr-review.md b/docs/community/maintainer/pr-review.md new file mode 100644 index 000000000000..1539f9f70372 --- /dev/null +++ b/docs/community/maintainer/pr-review.md @@ -0,0 +1,24 @@ +# Pull Request Review Policy + +This document outlines the review policy for pull requests in the Trivy project. + +## Core Principles + +### 1. All Changes Through Pull Requests +All changes to the `main` branch must be made through pull requests. +Direct commits to `main` are not allowed. + +### 2. Required Approvals +Every pull request requires approval from at least one CODEOWNER before merging. + +For changes that span multiple domains (e.g., both vulnerability and misconfiguration scanning), approval from at least one code owner from each affected domain is required. + +When a pull request is created by the only code owner of a domain, approval from any other maintainer is required. + +When a code owner wants additional input from other owners or maintainers, they should comment requesting feedback and wait for others to approve before providing their own approval. +This prevents accidental merging by the PR author. + +### 3. Merge Responsibility +- **General Rule**: The pull request author should click the merge button after receiving required approvals +- **Exception**: For urgent fixes (hotfixes), a CODEOWNER may merge the PR directly +- **External Contributors**: Pull requests from external contributors should be merged by a CODEOWNER \ No newline at end of file diff --git a/docs/community/maintainer/release-flow.md b/docs/community/maintainer/release-flow.md new file mode 100644 index 000000000000..d703da898a68 --- /dev/null +++ b/docs/community/maintainer/release-flow.md @@ -0,0 +1,98 @@ +# Release Flow + +## Overview +Trivy adopts [conventional commit messages][conventional-commits], and [Release Please][release-please] automatically creates a [release PR](https://github.com/googleapis/release-please?tab=readme-ov-file#whats-a-release-pr) based on the messages of the merged commits. +This release PR is automatically updated every time a new commit is added to the release branch. + +If a commit has the prefix `feat:`, a PR is automatically created to increment the minor version, and if a commit has the prefix `fix:`, a PR is created to increment the patch version. +When the PR is merged, GitHub Actions automatically creates a version tag and the release is performed. +For detailed behavior, please refer to [the GitHub Actions configuration][workflows]. + +!!! note + Commits with prefixes like `chore` or `build` are not considered releasable, and no release PR is created. + To include such commits in a release, you need to either include commits with `feat` or `fix` prefixes or perform a manual release as described [below](#manual-release-pr-creation). + +!!! tip + It's a good idea to check if there are any outstanding vulnerability updates created by dependabot waiting for your review. + They can be found in the "Security" tab of the repository. + If there are any, please review and merge them before creating a release. This will help to ensure that the release includes the latest security patches. + +## Flow +The release flow consists of the following main steps: + +1. Creating the release PR (automatically or manually) +1. Drafting the release notes in GitHub Discussions +1. Merging the release PR +1. Updating the release notes in GitHub Discussions +1. Navigating to the release notes in GitHub Releases page + +### Automatic Release PR Creation +When a releasable commit (a commit with `feat` or `fix` prefix) is merged, a release PR is automatically created. +These Release PRs are kept up-to-date as additional work is merged. +When it's ready to tag a release, simply merge the release PR. +See the [Release Please documentation][release-please] for more information. + +The title of the PR will be in the format `release: v${version} [${branch}]` (e.g., `release: v0.51.0 [main]`). +The format of the PR title is important for identifying the release commit, so it should not be changed. + +The `release/vX.Y` release branches are also subject to automatic release PR creation for patch releases. +The PR title will be like `release: v0.51.1 [release/v0.51]`. + +### Manual Release PR Creation +If you want to release commits like `chore`, a release PR is not automatically created, so you need to manually trigger the creation of a release PR. +The [Release Please workflow](https://github.com/aquasecurity/trivy/actions/workflows/release-please.yaml) supports `workflow_dispatch` and can be triggered manually. +Click "Run workflow" in the top right corner and specify the release branch. +In Trivy, the following branches are the release branches. + +- `main` +- `release/vX.Y` (e.g. `release/v0.51`) + +Specify the release version (without the `v` prefix) and click "Run workflow" to create a release PR for the specified version. + +### Drafting the Release Notes +Next, create release notes for this version. +Draft a new post in GitHub Discussions, and maintainers edit these release notes (e.g., https://github.com/aquasecurity/trivy/discussions/6605). +Currently, the creation of this draft is done manually. +For patch version updates, this step can be skipped since they only involve bug fixes. + +### Merging the Release PR +Once the draft of the release notes is complete, merge the release PR. +When the PR is merged, a tag is automatically created, and [GoReleaser][goreleaser] releases binaries, container images, etc. + +### Updating the Release Notes +If the release completes without errors, a page for the release notes is created in GitHub Discussions (e.g., https://github.com/aquasecurity/trivy/discussions/6622). +Copy the draft release notes, adjust the formatting, and finalize the release notes. + +### Navigating to the Release Notes +To navigate to the release highlights and summary in GitHub Discussions, place a link in the GitHub Releases page as below: + +``` +## ⚡Release highlights and summary⚡ + +👉 https://github.com/aquasecurity/trivy/discussions/6838 + +## Changelog +https://github.com/aquasecurity/trivy/blob/main/CHANGELOG.md#0520-2024-06-03 +``` + +Replace URLs with appropriate ones. + +Example: https://github.com/aquasecurity/trivy/releases/tag/v0.52.0 + +### Merging the auto-generated Helm chart update PR +Once the release PR is merged, there will be an auto-generated PR that bumps the Trivy version for the Trivy Helm Chart. An example can be seen [here](https://github.com/aquasecurity/trivy/pull/8638). + +> [!NOTE] +> It is possible that the release action takes a while to finish and the Helm chart action runs prior. In such a case the Helm chart action will fail as it will not be able to find the latest Trivy container image. +> In such a case, it is advised to manually restart the Helm chart action, once the release action is finished. + +If things look good, approve and merge this PR to further trigger the publishing of the Helm Chart. + + +The release is now complete 🍻 + + +[conventional-commits]: https://www.conventionalcommits.org/en/v1.0.0/ +[release-please]: https://github.com/googleapis/release-please +[goreleaser]: https://goreleaser.com/ +[workflows]: https://github.com/aquasecurity/trivy/tree/main/.github/workflows diff --git a/docs/community/maintainer/triage.md b/docs/community/maintainer/triage.md new file mode 100644 index 000000000000..1921a421d0ab --- /dev/null +++ b/docs/community/maintainer/triage.md @@ -0,0 +1,198 @@ +# Triage + +Triage is an important part of maintaining the health of the trivy repo. +A well organized repo allows maintainers to prioritize feature requests, fix bugs, and respond to users facing difficulty with the tool as quickly as possible. + +Triage includes: + +- Labeling issues +- Responding to issues +- Closing issues + +# Daily Triage +Daily triage has two goals: + +1. Responsiveness for new issues +1. Responsiveness when explicitly requested information was provided + +It covers: + +1. Issues without a `kind/` or `triage/` label +1. Issues without a `priority/` label +1. `triage/needs-information` issues which the user has followed up on, and now require a response. + +## Categorization + +The most important level of categorizing the issue is defining what type it is. +We typically want at least one of the following labels on every issue, and some issues may fall into multiple categories: + +- `triage/support` - The default for most incoming issues +- `kind/bug` - When it’s a bug or we aren’t delivering the best user experience + +Other possibilities: +- `kind/feature`- Identify new feature requests +- `kind/testing` - Update or fix unit/integration tests +- `kind/cleanup` - Cleaning up/refactoring the codebase +- `kind/documentation` - Updates or additions to trivy documentation + +If the issue is specific to a driver for OS packages or libraries: + +**co/[driver for OS packages]** + + - `co/alpine` + - `co/amazon` + - `co/debian` + - `co/oracle` + - `co/photon` + - `co/redhat` + - `co/suse` + - `co/ubuntu` + +**co/[driver for libraries of programming languages]** + + - `co/bundler` + - `co/cargo` + - `co/composer` + - `co/npm` + - `co/yarn` + - `co/pipenv` + - `co/poetry` + + +**Help wanted?** + +`Good First Issue` - bug has a proposed solution, can be implemented w/o further discussion. + +`Help wanted` - if the bug could use help from a contributor + + +## Prioritization +If the issue is not `triage/support`, it needs a priority label. + +`priority/critical-urgent` - someones top priority ASAP, such as security issue, user-visible bug, or build breakage. Rarely used. + +`priority/important-soon`: in time for the next two releases. It should be attached to a milestone. + +`priority/important-longterm`: 2-4 releases from now + +`priority/backlog`: agreed that this would be good to have, but no one is available at the moment. Consider tagging as `help wanted` + +`priority/awaiting-more-evidence`: may be useful, but there is not yet enough support. + + +# Weekly Triage + +Weekly triage has three goals: + +1. Catching up on unresponded issues +1. Reviewing and closing PR’s +1. Closing stale issues + + +## Post-Release Triage + +Post-release triage occurs after a major release (around every 4-6 weeks). +It focuses on: + +1. Closing bugs that have been resolved by the release +1. Reprioritizing bugs that have not been resolved by the release +1. Letting users know if we believe that there is still an issue + +This includes reviewing: + +1. Every issue that hasn’t been touched in the last 2 days +1. Re-evaluation of long-term issues +1. Re-evaluation of short-term issues + + +## Responding to Issues + +### Needs More Information +A sample response to ask for more info: + +> I don’t yet have a clear way to replicate this issue. Do you mind adding some additional details. Here is additional information that would be helpful: +> +> \* The exact `trivy` command line used +> +> \* The exact image you want to scan +> +> \* The full output of the `trivy` command, preferably with `--debug` for extra logging. +> +> +> Thank you for sharing your experience! + + +Then: Label with `triage/needs-information`. + +### Issue might be resolved +If you think a release may have resolved an issue, ask the author to see if their issue has been resolved: + +> Could you please check to see if trivy addresses this issue? We've made some changes with how this is handled, and improved the trivy logs output to help us debug tricky cases like this. + +Then: Label with `triage/needs-information`. + + +## Closing with Care + +Issues typically need to be closed for the following reasons: + +- The issue has been addressed +- The issue is a duplicate of an existing issue +- There has been a lack of information over a long period of time + +In any of these situations, we aim to be kind when closing the issue, and offer the author action items should they need to reopen their issue or still require a solution. + +Samples responses for these situations include: + +### Issue has been addressed + +>@author: I believe this issue is now addressed by trivy v1.0.0, as it . If you still see this issue with trivy v1.0 or higher, please reopen this issue. +> +>Thank you for reporting this issue! + +Then: Close the issue + +### Duplicate Issue + +>This issue appears to be a duplicate of #X, do you mind if we move the conversation there? +> +>This way we can centralize the content relating to the issue. If you feel that this issue is not in fact a duplicate, please re-open it. If you have additional information to share, please add it to the new issue. +> +>Thank you for reporting this! + +Then: Label with `triage/duplicate` and close the issue. + +### Lack of Information +If an issue hasn't been active for more than four weeks, and the author has been pinged at least once, then the issue can be closed. + +>Hey @author -- hopefully it's OK if I close this - there wasn't enough information to make it actionable, and some time has already passed. If you are able to provide additional details, you may reopen it at any point. +> +>Here is additional information that may be helpful to us: +> +>\* Whether the issue occurs with the latest trivy release +> +>\* The exact `trivy` command line used +> +>\* The exact image you want to scan +> +>\* The full output of the `trivy` command, preferably with `--debug` for extra logging. +> +> +>Thank you for sharing your experience! + +Then: Close the issue. + +## Help Wanted issues + +We use two labels [help wanted](https://github.com/aquasecurity/trivy/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) +and [good first issue](https://github.com/aquasecurity/trivy/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) +to identify issues that have been specially groomed for new contributors. + +We have specific [guidelines](./help-wanted.md) +for how to use these labels. If you see an issue that satisfies these +guidelines, you can add the `help wanted` label and the `good first issue` label. +Please note that adding the `good first issue` label must also +add the `help wanted` label. + +If an issue has these labels but does not satisfy the guidelines, please +ask for more details to be added to the issue or remove the labels. diff --git a/docs/community/principles.md b/docs/community/principles.md new file mode 100644 index 000000000000..aecb34b00ba9 --- /dev/null +++ b/docs/community/principles.md @@ -0,0 +1,53 @@ +# Trivy Project Principles +This document outlines the guiding principles and governance framework for the Trivy project. + +## Core Principles +Trivy is a security scanner focused on static analysis and designed with simplicity and security at its core. +All new proposals to the project must adhere to the following principles. + +### Static Analysis (No Runtime Required) +Trivy operates without requiring container or VM image startups, eliminating the need for Docker or similar runtimes, except for scanning images stored within a container runtime. +This approach enhances security and efficiency by minimizing dependencies. + +### External Dependency Free (Single Binary) +Operating as a single binary, Trivy is independent of external environments and avoids executing external OS commands or processes. +If specific functionality, like Maven's, is needed, Trivy opts for internal reimplementations or processing outputs of the tool without direct execution of external tools. + +This approach obviously requires more effort but significantly reduces security risks associated with executing OS commands and dependency errors due to external environment versions. +Simplifying the scanner's use by making it operational immediately upon binary download facilitates easier initiation of scans. + +### No Setup Required +Trivy must be ready to use immediately after installation. +It's unacceptable for Trivy not to function without setting up a database or writing configuration files by default. +Such setups should only be necessary for users requiring specific customizations. + +Security often isn't a top priority for many organizations and can be easily deferred. +Trivy aims to lower the barrier to entry by simplifying the setup process, making it easier for users to start securing their projects. + +### Security Focus +Trivy prioritizes the identification of security issues, excluding features unrelated to security, such as performance metrics or content listings of container images. +It can, however, produce and output intermediate representations like SBOMs for comprehensive security assessments. + +Trivy serves as a tool with opinions on security, used to warn users about potential issues. + +### Detecting Unintended States +Trivy is designed to detect unintended vulnerable states in projects, such as the use of vulnerable versions of dependencies or misconfigurations in Infrastructure as Code (IaC) that may unintentionally expose servers to the internet. +The focus is on identifying developer mistakes or undesirable states, not on detecting intentional attacks, such as malicious images and malware. + +## Out of Scope Features +Aqua Security offers a premium version with several features not available in the open-source Trivy project. +While detailed information can be found [here][trivy-aqua], it's beneficial to highlight specific functionalities frequently inquired about: + +### Runtime Security +As mentioned in [the Core Principles](#static-analysis-no-runtime-required), Trivy is a static analysis security scanner, making runtime security outside its scope. +Runtime security needs are addressed by [Tracee][tracee] or [the commercial version of Aqua Security](). + +### Intentional Attacks +As mentioned in [the Core Principles](#detecting-unintended-states), detection of intentional attacks, such as malware or malicious container images, is not covered by Trivy and is supported in [the commercial version][aqua]. + +### User Interface +Trivy primarily operates via CLI for displaying results, with a richer UI available in [the commercial version][aqua]. + +[trivy-aqua]: ../commercial/compare.md +[tracee]: https://github.com/aquasecurity/tracee +[aqua]: https://www.aquasec.com/ \ No newline at end of file diff --git a/docs/ecosystem/cicd.md b/docs/ecosystem/cicd.md new file mode 100644 index 000000000000..3973174b77ba --- /dev/null +++ b/docs/ecosystem/cicd.md @@ -0,0 +1,89 @@ +# CI/CD Integrations + +## Azure DevOps (Official) +[Azure Devops](https://azure.microsoft.com/en-us/products/devops/#overview) is Microsoft Azure cloud native CI/CD service. + +Trivy has a "Azure Devops Pipelines Task" for Trivy, that lets you easily introduce security scanning into your workflow, with an integrated Azure Devops UI. + +👉 Get it at: + +## GitHub Actions +[GitHub Actions](https://github.com/features/actions) is GitHub's native CI/CD and job orchestration service. + +### trivy-action (Official) + +GitHub Action for integrating Trivy into your GitHub pipeline + +👉 Get it at: + +### trivy-action (Community) + +GitHub Action to scan vulnerability using Trivy. If vulnerabilities are found by Trivy, it creates a GitHub Issue. + +👉 Get it at: + +### trivy-github-issues (Community) + +In this action, Trivy scans the dependency files such as package-lock.json and go.sum in your repository, then create GitHub issues according to the result. + +👉 Get it at: + +## Buildkite Plugin (Community) + +The trivy buildkite plugin provides a convenient mechanism for running the open-source trivy static analysis tool on your project. + +👉 Get it at: https://github.com/equinixmetal-buildkite/trivy-buildkite-plugin + +## Dagger (Community) +[Dagger](https://dagger.io/) is CI/CD as code that runs anywhere. + +The Dagger module for Trivy provides functions for scanning container images from registries as well as Dagger Container objects from any Dagger SDK (e.g. Go, Python, Node.js, etc). + +👉 Get it at: + + +## Semaphore (Community) +[Semaphore](https://semaphore.io/) is a CI/CD service. + +You can use Trivy in Semaphore for scanning code, containers, infrastructure, and Kubernetes in Semaphore workflow. + +👉 Get it at: + +## CircleCI (Community) +[CircleCI](https://circleci.com/) is a CI/CD service. + +You can use the Trivy Orb for Circle CI to introduce security scanning into your workflow. + +👉 Get it at: +Source: + +## Woodpecker CI (Community) + +Example Trivy step in pipeline + +```yml +pipeline: + securitycheck: + image: aquasec/trivy:latest + commands: + # use any trivy command, if exit code is 0 woodpecker marks it as passed, else it assumes it failed + - trivy fs --exit-code 1 --skip-dirs web/ --skip-dirs docs/ --severity MEDIUM,HIGH,CRITICAL . +``` + +Woodpecker does use Trivy itself so you can [see it in use there](https://github.com/woodpecker-ci/woodpecker/pull/1163). + +## Concourse CI (Community) +[Concourse CI](https://concourse-ci.org/) is a CI/CD service. + +You can use Trivy Resource in Concourse for scanning containers and introducing security scanning into your workflow. +It has capabilities to fail the pipeline, create issues, alert communication channels (using respective resources) based on Trivy scan output. + +👉 Get it at: + + +## SecObserve GitHub actions and GitLab templates (Community) +[SecObserve GitHub actions and GitLab templates](https://github.com/SecObserve/secobserve_actions_templates) run various vulnerability scanners, providing uniform methods and parameters for launching the tools. + +The Trivy integration supports scanning Docker images and local filesystems for vulnerabilities as well as scanning IaC files for misconfigurations. + +👉 Get it at: diff --git a/docs/ecosystem/ide.md b/docs/ecosystem/ide.md new file mode 100644 index 000000000000..1849c2112496 --- /dev/null +++ b/docs/ecosystem/ide.md @@ -0,0 +1,79 @@ +# IDE and developer tools Integrations + +## VSCode (Official) + +[Visual Studio Code](https://code.visualstudio.com/) is an open source versatile code editor and development environment. + +👉 Get it at: + +## JetBrains (Official) + +[JetBrains](https://jetbrains.com) makes IDEs such as Goland, Pycharm, IntelliJ, Webstorm, and more. + +The Trivy plugin for JetBrains IDEs lets you use Trivy right from your development environment. + +👉 Get it at: + +## Kubernetes Lens (Official) + +[Kubernetes Lens](https://k8slens.dev/) is a management application for Kubernetes clusters. + +Trivy has an extension for Kubernetes Lens that lets you scan Kubernetes workloads and view the results in the Lens UI. + +👉 Get it at: + +## Vim (Community) + +[Vim](https://www.vim.org/) is a terminal based text editor. + +Vim plugin for Trivy to install and run Trivy. + +👉 Get it at: + +## Docker Desktop (Community) + +[Docker Desktop](https://www.docker.com/products/docker-desktop/) is an easy way to install [Docker]() container engine on your development machine, and manage it in a GUI . + +Trivy Docker Desktop extension for scanning container images for vulnerabilities and generating SBOMs + +👉 Get it at: + +## Rancher Desktop (Community) + +[Rancher Desktop](https://rancherdesktop.io/) is an easy way to use containers and Kubernetes on your development machine, and manage it in a GUI. + +Trivy is natively integrated with Rancher, no installation is needed. More info in Rancher documentation: + +## LazyTrivy (Community) + +A terminal native UI for Trivy + +👉 Get it at: + +## Trivy Vulnerability explorer (Community) + +Web application that allows to load a Trivy report in json format and displays the vulnerabilities of a single target in an interactive data table + +👉 Get it at: + +## Trivy pre-commit (Community) + +A trivy pre-commit hook that runs a `trivy fs` in your git repo before committing, preventing you from committing secrets in the first place. + +👉 Get it at: + +## AWS CDK + +[The AWS Cloud Development Kit (AWS CDK)](https://aws.amazon.com/cdk/) is an open-source software development framework to define cloud infrastructure in code and provision it through AWS CloudFormation. + +### image-scanner-with-trivy (Community) + +A CDK Construct Library to scan an image with trivy in CDK codes. + +👉 Get it at: + +## Headlamp plugin (Community) + +[Headlamp](https://headlamp.dev/) is a user-friendly Kubernetes UI focused on extensibility. The Kubescape plugin extends Headlamp with views on Trivy reports. + +👉 Get it at: diff --git a/docs/ecosystem/index.md b/docs/ecosystem/index.md new file mode 100644 index 000000000000..f9141d9be50b --- /dev/null +++ b/docs/ecosystem/index.md @@ -0,0 +1,10 @@ +# Ecosystem +Trivy is integrated into many popular tools and applications, so that you can easily add security to your workflow. + +In this section you will find an aggregation of the different integrations. Integrations are listed as either "official" or "community". Official integrations are developed by the core Trivy team and supported by it. Community integrations are integrations developed by the community, and collected here for your convenience. For support or questions about community integrations, please contact the original developers. + +👈 Please use the side-navigation on the left in order to browse the different topics. + +## Add missing integration + +We are happy to showcase community integrations in this section. To suggest an addition simply make a Pull Request to add the missing integration. diff --git a/docs/ecosystem/prod.md b/docs/ecosystem/prod.md new file mode 100644 index 000000000000..4d037ebdc78d --- /dev/null +++ b/docs/ecosystem/prod.md @@ -0,0 +1,39 @@ +# Production and cloud Integrations + +## Kubernetes + +[Kubernetes](https://kubernetes.io/) is an open-source system for automating deployment, scaling, and management of containerized applications. + +### Trivy Operator (Official) + +Using the Trivy Operator you can install Trivy into a Kubernetes cluster so that it automatically and continuously scan your workloads and cluster for security issues. + +👉 Get it at: + +## Harbor (Official) +[Harbor](https://goharbor.io/) is an open source cloud native container and artifact registry. + +Trivy is natively integrated into Harbor, no installation is needed. More info in Harbor documentation: + +## Kyverno (Community) +[Kyverno](https://kyverno.io/) is a policy management tool for Kubernetes. + +You can use Kyverno to ensure and enforce that deployed workloads' images are scanned for vulnerabilities. + +👉 Get it at: + +## Zora (Community) + +[Zora](https://zora-docs.undistro.io/) is an open-source solution that scans Kubernetes clusters with multiple plugins at scheduled times. + +Trivy is integrated into Zora as a vulnerability scanner plugin. + +👉 Get it at: + +## Helmper (Community) + +[Helmper](https://christoffernissen.github.io/helmper/) is a go program that reads Helm Charts from remote OCI registries and pushes the Helm Charts and the Helm Charts container images to your OCI registries with optional OS level vulnerability patching + +Trivy is integrated into Helmper as a vulnerability scanner in combination with Copacetic to fix detected vulnerabilities. + +👉 Get it at: diff --git a/docs/ecosystem/reporting.md b/docs/ecosystem/reporting.md new file mode 100644 index 000000000000..63c5901746be --- /dev/null +++ b/docs/ecosystem/reporting.md @@ -0,0 +1,38 @@ +# Reporting + +## DefectDojo (Community) +DefectDojo can parse Trivy JSON reports. The parser supports deduplication and auto-close features. + +👉 Get it at: + +## SecObserve (Community) +SecObserve can parse Trivy results as CycloneDX reports and provides an unified overview of vulnerabilities from different sources. Vulnerabilities can be evaluated with manual and rule based assessments. + +👉 Get it at: + +## Scan2html (Community) +A Trivy plugin that scans and outputs the results to an interactive html file. + +👉 Get it at: + +## SonarQube (Community) +A Trivy plugin that converts JSON report to SonarQube [generic issues format](https://docs.sonarqube.org/9.6/analyzing-source-code/importing-external-issues/generic-issue-import-format/). + +👉 Get it at: + +## Trivy-Streamlit (Community) +Trivy-Streamlit is a Streamlit application that allows you to quickly parse the results from a Trivy JSON report. + +👉 Get it at: + +## Trivy-Vulnerability-Explorer (Community) + +This project is a web application that allows to load a Trivy report in json format and displays the vulnerabilities of a single target in an interactive data table. + +👉 Get it at: + +## plopsec.com (Community) + +This project is a web application designed to help you visualize Trivy image scan reports. It enriches the data with additional exploitability metrics from EPSS, Metasploit, and Exploit-DB, updated daily. + +👉 Get it at: | diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md new file mode 100644 index 000000000000..b001d5387cdc --- /dev/null +++ b/docs/getting-started/faq.md @@ -0,0 +1,25 @@ +## FAQ + +### How to pronounce the name "Trivy"? + +`tri` is pronounced like **tri**gger, `vy` is pronounced like en**vy**. + +### Does Trivy support X? + +Check out the [Scanning coverage page](../guide/coverage/index.md). + +### Is there a paid version of Trivy? + +If you liked Trivy, you will love Aqua which builds on top of Trivy to provide even more enhanced capabilities for a complete security management offering. +You can find a high level comparison table specific to Trivy users [here](../commercial/compare.md). +In addition check out the website for more information about our products and services. +If you'd like to contact Aqua or request a demo, please use this form: + +### How to generate multiple reports? +See [here](../guide/configuration/reporting.md#converting). + +### How to run Trivy under air-gapped environment? +See [here](../guide/advanced/air-gap.md). + +### Why `trivy fs` and `trivy repo` does not scan JAR files for vulnerabilities? +See [here](../guide/target/repository.md#rationale). diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 000000000000..27d84911e1ea --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,74 @@ +# First steps with Trivy + +## Get Trivy + +Trivy is available in most common distribution channels. The complete list of installation options is available in the [Installation](./installation.md) page. Here are a few popular examples: + +- macOS: `brew install trivy` +- Docker: `docker run aquasec/trivy` +- Download binary from [GitHub Release](https://github.com/aquasecurity/trivy/releases/latest/) +- See [Installation](./installation.md) for more + +Trivy is integrated with many popular platforms and applications. The complete list of integrations is available in the [Ecosystem](../ecosystem/index.md) page. Here are a few popular examples: + +- [GitHub Actions](https://github.com/aquasecurity/trivy-action) +- [Kubernetes operator](https://github.com/aquasecurity/trivy-operator) +- [VS Code plugin](https://github.com/aquasecurity/trivy-vscode-extension) +- See [Ecosystem](../ecosystem/index.md) for more + +## General usage + +Trivy's Command Line Interface pattern follows its major concepts: targets (what you want to scan), and scanners (what you want to scan for): + +```bash +trivy [--scanners ] +``` + +### Examples + +Scan a container image from a registry with the default scanner, which is the Vulnerabilities scanner: + +```bash +trivy image python:3.4-alpine +``` + + + +Scan a local code repository, for vulnerabilities, exposed secrets and misconfigurations: + +```bash +trivy fs --scanners vuln,secret,misconfig /path/to/myproject +``` + + + +Scan a Kubernetes cluster, with all available scanners, and show a summary report: + +```bash +trivy k8s --report summary cluster +``` + +trivy-k8s + +For a more complete introduction, check out the basic Trivy Demo: + +## Learn more + +Now that you are up and ready, here are some resources to help you deepen your knowledge: + +- Learn more about Trivy's capabilities by exploring the complete [documentation](../guide/index.md). +- Explore community questions under [GitHub Discussions](https://github.com/aquasecurity/trivy/discussions). +- Stay up to date by watching for [New Releases & Announcements](https://github.com/aquasecurity/trivy/discussions/categories/announcements). +- Follow Trivy on Twitter/X: [@aquatrivy](https://x.com/aquatrivy) +- Explore and subscribe to our YouTube channel [@AquaSecOSS](http://youtube.com/@aquasecoss) + +# Want more? Check out Aqua + +If you liked Trivy, you will love Aqua which builds on top of Trivy to provide even more enhanced capabilities for a complete security management offering. +You can find a high level comparison table specific to Trivy users [here](../commercial/compare.md). +In addition, check out the website for more information about our products and services. +If you'd like to contact Aqua or request a demo, please use this form: diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md new file mode 100644 index 000000000000..939b35a25774 --- /dev/null +++ b/docs/getting-started/installation.md @@ -0,0 +1,214 @@ +# Installing Trivy + +In this section you will find an aggregation of the different ways to install Trivy. Installation options are labeled as either "Official" or "Community". Official installations are developed by the Trivy team and supported by it. Community installations could be developed by anyone from the Trivy community, and collected here for your convenience. For support or questions about community installations, please contact the original developers. + +!!! note + If you are looking to integrate Trivy into another system, such as CI/CD, IDE, Kubernetes, etc, please see [Ecosystem section](../ecosystem/index.md) to explore integrations of Trivy with other tools. + +## Container image (Official) + +Use one of the official Trivy images: + +| Registry | Repository | Link | +| --- | --- | --- | +| Docker Hub | `docker.io/aquasec/trivy` | https://hub.docker.com/r/aquasec/trivy | +| GitHub Container Registry (GHCR) | `ghcr.io/aquasecurity/trivy` | https://github.com/orgs/aquasecurity/packages/container/package/trivy | +| AWS Elastic Container Registry (ECR) | `public.ecr.aws/aquasecurity/trivy` | https://gallery.ecr.aws/aquasecurity/trivy | + +!!! Tip + It is advisable to mount a persistent [cache dir](../guide/configuration/cache.md) on the host into the Trivy container. + +!!! Tip + For scanning container images with Trivy, mount the container engine socket from the host into the Trivy container. + +Example: + +``` bash +docker run -v /var/run/docker.sock:/var/run/docker.sock -v $HOME/Library/Caches:/root/.cache/ aquasec/trivy:{{ git.tag[1:] }} image python:3.4-alpine +``` + +## GitHub Release (Official) + +1. Download the file for your operating system/architecture from [GitHub Release assets](https://github.com/aquasecurity/trivy/releases/tag/{{ git.tag }}). +2. Unpack the downloaded archive (`tar -xzf ./trivy.tar.gz`). +3. Make sure the binary has execution bit turned on (`chmod +x ./trivy`). + +## Install Script (Official) + +For convenience, you can use the install script to download and install Trivy from GitHub Release. + +```bash +curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sudo sh -s -- -b /usr/local/bin {{ git.tag }} +``` + +## RHEL/CentOS (Official) + +=== "Repository" + Add repository setting to `/etc/yum.repos.d`. + + ``` bash + cat << EOF | sudo tee -a /etc/yum.repos.d/trivy.repo + [trivy] + name=Trivy repository + baseurl=https://aquasecurity.github.io/trivy-repo/rpm/releases/\$basearch/ + gpgcheck=1 + enabled=1 + gpgkey=https://aquasecurity.github.io/trivy-repo/rpm/public.key + EOF + sudo yum -y update + sudo yum -y install trivy + ``` + +=== "RPM" + + ``` bash + rpm -ivh https://github.com/aquasecurity/trivy/releases/download/{{ git.tag }}/trivy_{{ git.tag[1:] }}_Linux-64bit.rpm + ``` + +## Debian/Ubuntu (Official) + +=== "Repository" + Add repository setting to `/etc/apt/sources.list.d`. + + ``` bash + sudo apt-get install wget gnupg + wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | sudo tee /usr/share/keyrings/trivy.gpg > /dev/null + echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee -a /etc/apt/sources.list.d/trivy.list + sudo apt-get update + sudo apt-get install trivy + ``` + +=== "DEB" + + ``` bash + wget https://github.com/aquasecurity/trivy/releases/download/{{ git.tag }}/trivy_{{ git.tag[1:] }}_Linux-64bit.deb + sudo dpkg -i trivy_{{ git.tag[1:] }}_Linux-64bit.deb + ``` + +## Homebrew (Official) + +Homebrew for macOS and Linux. + +```bash +brew install trivy +``` + +## Windows (Official) + +1. Download trivy_x.xx.x_windows-64bit.zip file from [releases page](https://github.com/aquasecurity/trivy/releases/). +2. Unzip file and copy to any folder. + +## Arch Linux (Community) + +Arch Linux Package Repository. + +```bash +sudo pacman -S trivy +``` + +References: +- +- + + +## OpenSUSE (Community) + +OpenSUSE Package Repository. + +```bash +sudo zypper install trivy +``` + +References: +- + + +## MacPorts (Community) + +[MacPorts](https://www.macports.org) for macOS. + +```bash +sudo port install trivy +``` + +References: +- + +## Nix/NixOS (Community) + +Nix package manager for Linux and macOS. + +=== "Command line" + `nix-env --install -A nixpkgs.trivy` + +=== "Configuration" + ```nix + # your other config ... + environment.systemPackages = with pkgs; [ + # your other packages ... + trivy + ]; + ``` + +=== "Home Manager" + ```nix + # your other config ... + home.packages = with pkgs; [ + # your other packages ... + trivy + ]; + ``` + +References: + +- https://github.com/NixOS/nixpkgs/blob/master/pkgs/tools/admin/trivy/default.nix + +## FreeBSD (Official) + +Pkg package manager for FreeBSD. + +```bash +pkg install trivy +``` + +## asdf/mise (Community) + +[asdf](https://github.com/asdf-vm/asdf) and [mise](https://github.com/jdx/mise) are quite similar tools you can use to install trivy. +See their respective documentation for more information of how to install them and use them: + +- [asdf](https://asdf-vm.com/guide/getting-started.html) +- [mise](https://mise.jdx.dev/getting-started.html) + +The plugin used by both tools is developed [here](https://github.com/zufardhiyaulhaq/asdf-trivy) + + +=== "asdf" + A basic global installation is shown below, for specific version or/and local version to a directory see "asdf" documentation. + + ```shell + # Install plugin + asdf plugin add trivy https://github.com/zufardhiyaulhaq/asdf-trivy.git + + # Install latest version + asdf install trivy latest + + # Set a version globally (on your ~/.tool-versions file) + asdf global trivy latest + + # Now trivy commands are available + trivy --version + ``` + +=== "mise" + A basic global installation is shown below, for specific version or/and local version to a directory see "mise" documentation. + + ``` shell + # Install plugin and install latest version + mise install trivy@latest + + # Set a version globally (on your ~/.tool-versions file) + mise use -g trivy@latest + + # Now trivy commands are available + trivy --version + ``` diff --git a/docs/getting-started/signature-verification.md b/docs/getting-started/signature-verification.md new file mode 100644 index 000000000000..ea76b72f3cb3 --- /dev/null +++ b/docs/getting-started/signature-verification.md @@ -0,0 +1,92 @@ +# Signature Verification + +All binaries and container images are signed by [Cosign](https://github.com/sigstore/cosign). + +## Verifying container image + +Use the following command for keyless [verification](https://docs.sigstore.dev/cosign/verify/): + +```shell +cosign verify aquasec/trivy: \ +--certificate-identity-regexp 'https://github\.com/aquasecurity/trivy/\.github/workflows/.+' \ +--certificate-oidc-issuer "https://token.actions.githubusercontent.com" +``` + +You should get the following output + +``` +Verification for index.docker.io/aquasec/trivy:latest -- +The following checks were performed on each of these signatures: + - The cosign claims were validated + - Existence of the claims in the transparency log was verified offline + - The code-signing certificate was verified using trusted certificate authority certificates + + .... +``` + +## Verifying binary + +Since Trivy v0.68.1, GitHub Releases provide [sigstore signature bundles](https://docs.sigstore.dev/cosign/bundle/). Separate `.sig` and certificate (`.pem`) files are no longer published. + +Download the required tarball and its associated `.sigstore.json` bundle file from the [GitHub Release](https://github.com/aquasecurity/trivy/releases). + +Use the following command for keyless verification: + +```shell +cosign verify-blob-attestation \ + --bundle .sigstore.json \ + --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ + --certificate-identity 'https://github.com/aquasecurity/trivy/.github/workflows/reusable-release.yaml@refs/tags/' +``` + +Example for `trivy_0.68.1_Linux-64bit.tar.gz`: + +```shell +cosign verify-blob-attestation trivy_0.68.1_Linux-64bit.tar.gz \ + --bundle trivy_0.68.1_Linux-64bit.tar.gz.sigstore.json \ + --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ + --certificate-identity 'https://github.com/aquasecurity/trivy/.github/workflows/reusable-release.yaml@refs/tags/v0.68.1' +``` + +You should get the following output + +``` +Verified OK +``` + +## Verifying a GPG signature + +RPM and Deb packages are also signed by GPG. + +### Verifying RPM + +The public key is available at . + +First, download and import the key: + +```shell +curl https://aquasecurity.github.io/trivy-repo/rpm/public.key \ +--output pub.key +rpm --import pub.key +rpm -q --queryformat "%{SUMMARY}\n" $(rpm -q gpg-pubkey) +``` + +You should get the following output: + +``` +gpg(trivy) +``` + +Then you can verify the signature: + +```shell +curl -L https://github.com/aquasecurity/trivy/releases/download//.rpm \ +--output trivy.rpm +rpm -K trivy.rpm +``` + +You should get the following output + +``` +trivy.rpm: digests signatures OK +``` diff --git a/docs/guide/advanced/air-gap.md b/docs/guide/advanced/air-gap.md new file mode 100644 index 000000000000..797ab8891b7b --- /dev/null +++ b/docs/guide/advanced/air-gap.md @@ -0,0 +1,82 @@ +# Connectivity and Network considerations + +Trivy requires internet connectivity in order to function normally. If your organization blocks or restricts network traffic, that could prevent Trivy from working correctly. +This document explains Trivy's network connectivity requirements, and how to configure Trivy to work in restricted network environments, including completely air-gapped environments. + +The following table lists all external resources that are required by Trivy: + +External Resource | Feature | Details +--- | --- | --- +Vulnerability Database | Vulnerability scanning | [Trivy DB](../scanner/vulnerability.md) +Java Vulnerability Database | Java vulnerability scanning | [Trivy Java DB](../coverage/language/java.md) +Checks Bundle | Misconfigurations scanning | [Trivy Checks](../scanner/misconfiguration/check/builtin.md) +VEX Hub | VEX Hub | [VEX Hub](../supply-chain/vex/repo.md) +Maven Central / Remote Repositories | Java vulnerability scanning | [Java Scanner/Remote Repositories](../coverage/language/java.md#remote-repositories) + +!!! note + Trivy is an open source project that relies on public free infrastructure. In case of extreme load, you may encounter rate limiting when Trivy attempts to connect to external resources. + +The rest of this document details each resource's connectivity requirements and network related considerations. + +## OCI Databases + +Trivy's Vulnerability, Java, and Checks Bundle are packaged as OCI images and stored in public container registries. + +### Connectivity requirements + +The specific registries and locations are detailed in the [databases document](../configuration/db.md). + +Communication with OCI Registries follows the [OCI Distribution](https://github.com/opencontainers/distribution-spec) spec. + +The following hosts are known to be used by the default container registries: + +Registry | Hosts | Additional info +--- | --- | --- +Google Artifact Registry |
  • `mirror.gcr.io`
  • `googlecode.l.googleusercontent.com`
| [Google's IP addresses](https://support.google.com/a/answer/10026322?hl=en) +GitHub Container Registry |
  • `ghcr.io`
  • `pkg-containers.githubusercontent.com`
| [GitHub's IP addresses](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-githubs-ip-addresses) + +### Self-hosting + +You can host Trivy's databases in your own container registry. Please refer to [Self-hosting document](./self-hosting.md#oci-databases) for a detailed guide. + +## Embedded Checks + +Checks Bundle is embedded in the Trivy binary (at build time), and will be used as a fallback if the external database is not available. This means that you can still scan for misconfigurations in an air-gapped environment using the database from the time of the Trivy release you are using. + +## VEX Hub + +### Connectivity Requirements + +VEX Hub is hosted at . + +Trivy is fetching VEX Hub GitHub Repository directly using simple HTTPS requests. + +The following hosts are known to be used by GitHub's services: + +- `api.github.com` +- `codeload.github.com` + +For more information about GitHub connectivity (including specific IP addresses), please refer to [GitHub's connectivity troubleshooting guide](https://docs.github.com/en/get-started/using-github/troubleshooting-connectivity-problems). + +### Self-hosting + +You can host a copy of VEX Hub on your own internal server. Please refer to the [self-hosting document](./self-hosting.md#vex-hub) for a detailed guide. + +## Maven Central / Remote Repositories + +Trivy might call out to Maven Central or other remote repositories in order to correctly identify Java packages during a vulnerability scan. + +### Connectivity requirements + +Trivy might attempt to connect (over HTTPS) to the following URLs: + +- `https://repo.maven.apache.org/maven2` + +### Offline mode + +There's no way to leverage Maven Central in a network-restricted environment, but you can prevent Trivy from trying to connect to it by using the `--offline-scan` flag. + +## Check updates service + +Trivy [checks for updates](../configuration/others.md#check-for-updates) and [collects usage telemetry](../advanced/telemetry.md) by connecting to the following domain: `https://check.trivy.dev`. +Connectivity with this domain is entirely optional and is not necessary for the normal operation of Trivy. diff --git a/docs/guide/advanced/container/embed-in-dockerfile.md b/docs/guide/advanced/container/embed-in-dockerfile.md new file mode 100644 index 000000000000..2c467b7ae999 --- /dev/null +++ b/docs/guide/advanced/container/embed-in-dockerfile.md @@ -0,0 +1,28 @@ +# Embed in Dockerfile + +Scan your image as part of the build process by embedding Trivy in the +Dockerfile. This approach can be used to update Dockerfiles currently using +Aqua’s [Microscanner][microscanner]. + +```bash +$ cat Dockerfile +FROM alpine:3.7 + +RUN apk add curl \ + && curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin \ + && trivy rootfs --exit-code 1 --no-progress / + +$ docker build -t vulnerable-image . +``` +Alternatively you can use Trivy in a multistage build. Thus avoiding the +insecure `curl | sh`. Also the image is not changed. +```bash +[...] +# Run vulnerability scan on build image +FROM build AS vulnscan +COPY --from=aquasec/trivy:latest /usr/local/bin/trivy /usr/local/bin/trivy +RUN trivy rootfs --exit-code 1 --no-progress / +[...] +``` + +[microscanner]: https://github.com/aquasecurity/microscanner diff --git a/docs/guide/advanced/container/unpacked-filesystem.md b/docs/guide/advanced/container/unpacked-filesystem.md new file mode 100644 index 000000000000..cbc27fbf07f5 --- /dev/null +++ b/docs/guide/advanced/container/unpacked-filesystem.md @@ -0,0 +1,116 @@ +# Unpacked Filesystem + +Scan an unpacked container image filesystem. + +In this case, Trivy works the same way when scanning containers + +```bash +$ docker export $(docker create alpine:3.10.2) | tar -C /tmp/rootfs -xvf - +$ trivy rootfs /tmp/rootfs +``` + +
+Result + +```bash +2021-03-08T05:22:26.378Z INFO Need to update DB +2021-03-08T05:22:26.380Z INFO Downloading DB... +20.37 MiB / 20.37 MiB [-------------------------------------------------------------------------------------------------------------------------------------] 100.00% 8.24 MiB p/s 2s +2021-03-08T05:22:30.134Z INFO Detecting Alpine vulnerabilities... + +/tmp/rootfs (alpine 3.10.2) +=========================== +Total: 20 (UNKNOWN: 0, LOW: 2, MEDIUM: 10, HIGH: 8, CRITICAL: 0) + ++--------------+------------------+----------+-------------------+---------------+---------------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++--------------+------------------+----------+-------------------+---------------+---------------------------------------+ +| libcrypto1.1 | CVE-2020-1967 | HIGH | 1.1.1c-r0 | 1.1.1g-r0 | openssl: Segmentation | +| | | | | | fault in SSL_check_chain | +| | | | | | causes denial of service | +| | | | | | -->avd.aquasec.com/nvd/cve-2020-1967 | ++ +------------------+ + +---------------+---------------------------------------+ +| | CVE-2021-23839 | | | 1.1.1j-r0 | openssl: incorrect SSLv2 | +| | | | | | rollback protection | +| | | | | | -->avd.aquasec.com/nvd/cve-2021-23839 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2021-23840 | | | | openssl: integer | +| | | | | | overflow in CipherUpdate | +| | | | | | -->avd.aquasec.com/nvd/cve-2021-23840 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2021-23841 | | | | openssl: NULL pointer dereference | +| | | | | | in X509_issuer_and_serial_hash() | +| | | | | | -->avd.aquasec.com/nvd/cve-2021-23841 | ++ +------------------+----------+ +---------------+---------------------------------------+ +| | CVE-2019-1547 | MEDIUM | | 1.1.1d-r0 | openssl: side-channel weak | +| | | | | | encryption vulnerability | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-1547 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2019-1549 | | | | openssl: information | +| | | | | | disclosure in fork() | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-1549 | ++ +------------------+ + +---------------+---------------------------------------+ +| | CVE-2019-1551 | | | 1.1.1d-r2 | openssl: Integer overflow in RSAZ | +| | | | | | modular exponentiation on x86_64 | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-1551 | ++ +------------------+ + +---------------+---------------------------------------+ +| | CVE-2020-1971 | | | 1.1.1i-r0 | openssl: EDIPARTYNAME | +| | | | | | NULL pointer de-reference | +| | | | | | -->avd.aquasec.com/nvd/cve-2020-1971 | ++ +------------------+----------+ +---------------+---------------------------------------+ +| | CVE-2019-1563 | LOW | | 1.1.1d-r0 | openssl: information | +| | | | | | disclosure in PKCS7_dataDecode | +| | | | | | and CMS_decrypt_set1_pkey | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-1563 | ++--------------+------------------+----------+ +---------------+---------------------------------------+ +| libssl1.1 | CVE-2020-1967 | HIGH | | 1.1.1g-r0 | openssl: Segmentation | +| | | | | | fault in SSL_check_chain | +| | | | | | causes denial of service | +| | | | | | -->avd.aquasec.com/nvd/cve-2020-1967 | ++ +------------------+ + +---------------+---------------------------------------+ +| | CVE-2021-23839 | | | 1.1.1j-r0 | openssl: incorrect SSLv2 | +| | | | | | rollback protection | +| | | | | | -->avd.aquasec.com/nvd/cve-2021-23839 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2021-23840 | | | | openssl: integer | +| | | | | | overflow in CipherUpdate | +| | | | | | -->avd.aquasec.com/nvd/cve-2021-23840 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2021-23841 | | | | openssl: NULL pointer dereference | +| | | | | | in X509_issuer_and_serial_hash() | +| | | | | | -->avd.aquasec.com/nvd/cve-2021-23841 | ++ +------------------+----------+ +---------------+---------------------------------------+ +| | CVE-2019-1547 | MEDIUM | | 1.1.1d-r0 | openssl: side-channel weak | +| | | | | | encryption vulnerability | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-1547 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2019-1549 | | | | openssl: information | +| | | | | | disclosure in fork() | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-1549 | ++ +------------------+ + +---------------+---------------------------------------+ +| | CVE-2019-1551 | | | 1.1.1d-r2 | openssl: Integer overflow in RSAZ | +| | | | | | modular exponentiation on x86_64 | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-1551 | ++ +------------------+ + +---------------+---------------------------------------+ +| | CVE-2020-1971 | | | 1.1.1i-r0 | openssl: EDIPARTYNAME | +| | | | | | NULL pointer de-reference | +| | | | | | -->avd.aquasec.com/nvd/cve-2020-1971 | ++ +------------------+----------+ +---------------+---------------------------------------+ +| | CVE-2019-1563 | LOW | | 1.1.1d-r0 | openssl: information | +| | | | | | disclosure in PKCS7_dataDecode | +| | | | | | and CMS_decrypt_set1_pkey | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-1563 | ++--------------+------------------+----------+-------------------+---------------+---------------------------------------+ +| musl | CVE-2020-28928 | MEDIUM | 1.1.22-r3 | 1.1.22-r4 | In musl libc through 1.2.1, | +| | | | | | wcsnrtombs mishandles particular | +| | | | | | combinations of destination buffer... | +| | | | | | -->avd.aquasec.com/nvd/cve-2020-28928 | ++--------------+ + + + + + +| musl-utils | | | | | | +| | | | | | | +| | | | | | | +| | | | | | | ++--------------+------------------+----------+-------------------+---------------+---------------------------------------+ +``` + +
\ No newline at end of file diff --git a/docs/guide/advanced/modules.md b/docs/guide/advanced/modules.md new file mode 100644 index 000000000000..3b88fbf94327 --- /dev/null +++ b/docs/guide/advanced/modules.md @@ -0,0 +1,368 @@ +# Modules + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy provides a module feature to allow others to extend the Trivy CLI without the need to change the Trivy code base. +It changes the behavior during scanning by WebAssembly. + +## Overview +Trivy modules are add-on tools that integrate seamlessly with Trivy. +They provide a way to extend the core feature set of Trivy, but without updating the Trivy binary. + +- They can be added and removed from a Trivy installation without impacting the core Trivy tool. +- They can be written in any programming language supporting WebAssembly. + - It supports only Go at the moment. + +You can write your own detection logic. + +- Evaluate complex vulnerability conditions like [Spring4Shell][spring4shell] +- Detect a shell script communicating with malicious domains +- Detect malicious python install script (setup.py) +- Even detect misconfigurations in WordPress setting +- etc. + +Then, you can update the scan result however you want. + +- Change a severity +- Remove a vulnerability +- Add a new vulnerability +- etc. + +Modules should be distributed in OCI registries like GitHub Container Registry. + +!!! warning + WebAssembly doesn't allow file access and network access by default. + Modules can read required files only, but cannot overwrite them. + WebAssembly is sandboxed and secure by design, but Trivy modules available in public are not audited for security. + You should install and run third-party modules at your own risk even though + +Under the hood Trivy leverages [wazero][wazero] to run WebAssembly modules without CGO. + +## Installing a Module +A module can be installed using the `trivy module install` command. +This command takes an url. It will download the module and install it in the module cache. + +Trivy adheres to the XDG specification, so the location depends on whether XDG_DATA_HOME is set. +Trivy will now search XDG_DATA_HOME for the location of the Trivy modules cache. +The preference order is as follows: + +- XDG_DATA_HOME if set and .trivy/modules exists within the XDG_DATA_HOME dir +- $HOME/.trivy/modules + +For example, to download the WebAssembly module, you can execute the following command: + +```bash +$ trivy module install ghcr.io/aquasecurity/trivy-module-spring4shell +``` + +## Using Modules +Once the module is installed, Trivy will load all available modules in the cache on the start of the next Trivy execution. +The modules may inject custom logic into scanning and change the result. +You can run Trivy as usual and modules are loaded automatically. + +You will see the log messages about WASM modules. + +```shell +$ trivy image ghcr.io/aquasecurity/trivy-test-images:spring4shell-jre8 +2022-06-12T12:57:13.210+0300 INFO Loading ghcr.io/aquasecurity/trivy-module-spring4shell/spring4shell.wasm... +2022-06-12T12:57:13.596+0300 INFO Registering WASM module: spring4shell@v1 +... +2022-06-12T12:57:14.865+0300 INFO Module spring4shell: Java Version: 8, Tomcat Version: 8.5.77 +2022-06-12T12:57:14.865+0300 INFO Module spring4shell: change CVE-2022-22965 severity from CRITICAL to LOW + +Java (jar) + +Total: 9 (UNKNOWN: 1, LOW: 3, MEDIUM: 2, HIGH: 3, CRITICAL: 0) + +┌──────────────────────────────────────────────────────────────┬─────────────────────┬──────────┬───────────────────┬────────────────────────┬────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├──────────────────────────────────────────────────────────────┼─────────────────────┼──────────┼───────────────────┼────────────────────────┼────────────────────────────────────────────────────────────┤ +│ org.springframework.boot:spring-boot (helloworld.war) │ CVE-2022-22965 │ LOW │ 2.6.3 │ 2.5.12, 2.6.6 │ spring-framework: RCE via Data Binding on JDK 9+ │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-22965 │ +├──────────────────────────────────────────────────────────────┼─────────────────────┼──────────┼───────────────────┼────────────────────────┼────────────────────────────────────────────────────────────┤ +...(snip)... +``` + +In the above example, the Spring4Shell module changed the severity from CRITICAL to LOW because the application doesn't satisfy one of conditions. + +## Uninstalling Modules +Specify a module repository with `trivy module uninstall` command. + +```bash +$ trivy module uninstall ghcr.io/aquasecurity/trivy-module-spring4shell +``` + +## Building Modules +It supports Go only at the moment. + +### Go +Trivy provides Go SDK including three interfaces. +Your own module needs to implement either or both `Analyzer` and `PostScanner` in addition to `Module`. + +```go +type Module interface { + Version() int + Name() string +} + +type Analyzer interface { + RequiredFiles() []string + Analyze(filePath string) (*serialize.AnalysisResult, error) +} + +type PostScanner interface { + PostScanSpec() serialize.PostScanSpec + PostScan(types.Results) (types.Results, error) +} +``` + +In the following tutorial, it creates a WordPress module that detects a WordPress version and a critical vulnerability accordingly. + +!!! tips + You can use logging functions such as `Debug` and `Info` for debugging. + See [examples](#examples) for the detail. + +#### Initialize your module +Replace the repository name with yours. + +``` +$ go mod init github.com/aquasecurity/trivy-module-wordpress +``` + +#### Module interface +`Version()` returns your module version and should be incremented after updates. +`Name()` returns your module name. + +```go +package main + +import ( + "github.com/aquasecurity/trivy/pkg/module/wasm" +) + +const ( + version = 1 + name = "wordpress-module" +) + +// main is required for Go to compile the Wasm module +func main() {} + +func init() { + wasm.RegisterModule(WordpressModule{}) +} + +type WordpressModule struct{ + // Cannot define fields as modules can't keep state. +} + +func (WordpressModule) Version() int { + return version +} + +func (WordpressModule) Name() string { + return name +} +``` + +!!! info + A struct cannot have any fields. Each method invocation is performed in different states. + +#### Analyzer interface +If you implement the `Analyzer` interface, `Analyze` method is called when the file path is matched to file patterns returned by `RequiredFiles()`. +A file pattern must be a regular expression. The syntax detail is [here][regexp]. + +`Analyze` takes the matched file path, then the file can be opened by `os.Open()`. + +```go +const typeWPVersion = "wordpress-version" + +func (WordpressModule) RequiredFiles() []string { + return []string{ + `wp-includes\/version.php`, + } +} + +func (WordpressModule) Analyze(filePath string) (*serialize.AnalysisResult, error) { + f, err := os.Open(filePath) // e.g. filePath: /usr/src/wordpress/wp-includes/version.php + if err != nil { + return nil, err + } + defer f.Close() + + var wpVersion string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "$wp_version=") { + continue + } + + ss := strings.Split(line, "=") + if len(ss) != 2 { + return nil, fmt.Errorf("invalid wordpress version: %s", line) + } + + // NOTE: it is an example; you actually need to handle comments, etc + ss[1] = strings.TrimSpace(ss[1]) + wpVersion = strings.Trim(ss[1], `";`) + } + + if err = scanner.Err(); err != nil { + return nil, err + } + + return &serialize.AnalysisResult{ + CustomResources: []ftypes.CustomResource{ + { + Type: typeWPVersion, + FilePath: filePath, + Data: wpVersion, + }, + }, + }, nil +} +``` + +!!! tips + Trivy caches analysis results according to the module version. + We'd recommend cleaning the cache or changing the module version every time you update `Analyzer`. + + +#### PostScanner interface +`PostScan` is called after scanning and takes the scan result as an argument from Trivy. +In post scanning, your module can perform one of three actions: + +- Insert + - Add a new security finding + - e.g. Add a new vulnerability and misconfiguration +- Update + - Update the detected vulnerability and misconfiguration + - e.g. Change a severity +- Delete + - Delete the detected vulnerability and misconfiguration + - e.g. Remove Spring4Shell because it is not actually affected. + +`PostScanSpec()` returns which action the module does. +If it is `Update` or `Delete`, it also needs to return IDs such as CVE-ID and misconfiguration ID, which your module wants to update or delete. + +`serialize.Results` contains the filtered results matching IDs you specified. +Also, it includes `CustomResources` with the values your `Analyze` returns, so you can modify the scan result according to the custom resources. + +```go +func (WordpressModule) PostScanSpec() serialize.PostScanSpec { + return serialize.PostScanSpec{ + Action: api.ActionInsert, // Add new vulnerabilities + } +} + +func (WordpressModule) PostScan(results types.Results) (types.Results, error) { + // e.g. results + // [ + // { + // "Target": "", + // "Class": "custom", + // "CustomResources": [ + // { + // "Type": "wordpress-version", + // "FilePath": "/usr/src/wordpress/wp-includes/version.php", + // "Layer": { + // "DiffID": "sha256:057649e61046e02c975b84557c03c6cca095b8c9accd3bd20eb4e432f7aec887" + // }, + // "Data": "5.7.1" + // } + // ] + // } + // ] + var wpVersion int + for _, result := range results { + if result.Class != types.ClassCustom { + continue + } + + for _, c := range result.CustomResources { + if c.Type != typeWPVersion { + continue + } + wpVersion = c.Data.(string) + wasm.Info(fmt.Sprintf("WordPress Version: %s", wpVersion)) + + ...snip... + + if affectedVersion.Check(ver) { + vulnerable = true + } + break + } + } + + if vulnerable { + // Add CVE-2020-36326 + results = append(results, types.Result{ + Target: wpPath, + Class: types.ClassLangPkg, + Type: "wordpress", + Vulnerabilities: []types.DetectedVulnerability { + { + VulnerabilityID: "CVE-2020-36326", + PkgName: "wordpress", + InstalledVersion: wpVersion, + FixedVersion: "5.7.2", + Vulnerability: dbTypes.Vulnerability{ + Title: "PHPMailer 6.1.8 through 6.4.0 allows object injection through Phar Deserialization via addAttachment with a UNC pathname.", + Severity: "CRITICAL", + }, + }, + }, + }) + } + return results, nil +} +``` + +The new vulnerability will be added to the scan results. +This example shows how the module inserts a new finding. +If you are interested in `Update`, you can see an example of [Spring4Shell][trivy-module-spring4shell]. + +In the `Delete` action, `PostScan` needs to return results you want to delete. +If `PostScan` returns an empty, Trivy will not delete anything. + +#### Build +Follow [the install guide][go-installation] and install Go. + +```bash +$ GOOS=wasip1 GOARCH=wasm go build -o wordpress.wasm -buildmode=c-shared wordpress.go +``` + +Put the built binary to the module directory that is under the home directory by default. + +```bash +$ mkdir -p ~/.trivy/modules +$ cp wordpress.wasm ~/.trivy/modules +``` + +## Distribute Your Module +You can distribute your own module in OCI registries. Please follow [the oras installation instruction][oras]. + +```bash +oras push ghcr.io/aquasecurity/trivy-module-wordpress:latest wordpress.wasm:application/vnd.module.wasm.content.layer.v1+wasm +Uploading 3daa3dac086b wordpress.wasm +Pushed ghcr.io/aquasecurity/trivy-module-wordpress:latest +Digest: sha256:6416d0199d66ce52ced19f01d75454b22692ff3aa7737e45f7a189880840424f +``` + +## Examples +- [Spring4Shell][trivy-module-spring4shell] +- [WordPress][trivy-module-wordpress] + +[regexp]: https://github.com/google/re2/wiki/Syntax + +[spring4shell]: https://blog.aquasec.com/zero-day-rce-vulnerability-spring4shell +[wazero]: https://github.com/tetratelabs/wazero + +[trivy-module-spring4shell]: https://github.com/aquasecurity/trivy/tree/main/examples/module/spring4shell +[trivy-module-wordpress]: https://github.com/aquasecurity/trivy-module-wordpress + +[go-installation]: https://go.dev/doc/install +[oras]: https://oras.land/cli/ \ No newline at end of file diff --git a/docs/guide/advanced/private-registries/acr.md b/docs/guide/advanced/private-registries/acr.md new file mode 100644 index 000000000000..df960697af3b --- /dev/null +++ b/docs/guide/advanced/private-registries/acr.md @@ -0,0 +1,27 @@ +# Requirements +None, Trivy uses Azure SDK for Go. You don't need to install `az` command. + +# Privileges +Service principal must have the `AcrPull` permissions. + +## Creation of a service principal +```bash +export SP_DATA=$(az ad sp create-for-rbac --name TrivyTest --role AcrPull --scope "/subscriptions//resourceGroups//providers/Microsoft.ContainerRegistry/registries/") +``` + +# Usage +```bash +# must set TRIVY_USERNAME empty char +export AZURE_CLIENT_ID=$(echo $SP_DATA | jq -r '.appId') +export AZURE_CLIENT_SECRET=$(echo $SP_DATA | jq -r '.password') +export AZURE_TENANT_ID=$(echo $SP_DATA | jq -r '.tenant') +``` + +# Testing +You can test credentials in the following manner. + +```bash +docker run -it --rm -v /tmp:/tmp \ + -e AZURE_CLIENT_ID -e AZURE_CLIENT_SECRET -e AZURE_TENANT_ID \ + aquasec/trivy image your_special_project.azurecr.io/your_special_image:your_special_tag +``` diff --git a/docs/guide/advanced/private-registries/docker-hub.md b/docs/guide/advanced/private-registries/docker-hub.md new file mode 100644 index 000000000000..f069fa43a01b --- /dev/null +++ b/docs/guide/advanced/private-registries/docker-hub.md @@ -0,0 +1,2 @@ +See [here](./index.md) for the detail. +You don't need to provide a credential when download from public repository. diff --git a/docs/guide/advanced/private-registries/ecr.md b/docs/guide/advanced/private-registries/ecr.md new file mode 100644 index 000000000000..b3e4b5b49c75 --- /dev/null +++ b/docs/guide/advanced/private-registries/ecr.md @@ -0,0 +1,35 @@ +Trivy uses AWS SDK. You don't need to install `aws` CLI tool. +You can use [AWS CLI's ENV Vars][env-var]. + +[env-var]: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html + +### AWS private registry permissions + +You may need to grant permissions to allow Trivy to pull images from private ECR. + +It depends on how you want to provide AWS Role to trivy. + +- [IAM Role Service account](https://github.com/aws/amazon-eks-pod-identity-webhook) +- [Kube2iam](https://github.com/jtblin/kube2iam) or [Kiam](https://github.com/uswitch/kiam) + +#### IAM Role Service account + +Add the AWS role in trivy's service account annotations: + +```yaml +trivy: + + serviceAccount: + annotations: {} + # eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/IAM_ROLE_NAME +``` + +#### Kube2iam or Kiam + +Add the AWS role to pod's annotations: + +```yaml +podAnnotations: {} + ## kube2iam/kiam annotation + # iam.amazonaws.com/role: arn:aws:iam::ACCOUNT_ID:role/IAM_ROLE_NAME +``` diff --git a/docs/guide/advanced/private-registries/gar.md b/docs/guide/advanced/private-registries/gar.md new file mode 100644 index 000000000000..ca3b4dadd4e7 --- /dev/null +++ b/docs/guide/advanced/private-registries/gar.md @@ -0,0 +1,40 @@ +# Requirements +None, Trivy uses Google Cloud SDK. You don't need to install `gcloud` command. + +# Privileges +Credential file must have the `roles/artifactregistry.reader` permissions. +More information can be found in [Google's documentation](https://cloud.google.com/artifact-registry/docs/access-control) + +## JSON File Format +The JSON file specified should have the following format provided by google's service account mechanisms: + +```json +{ + "type": "service_account", + "project_id": "your_special_project", + "private_key_id": "XXXXXXXXXXXXXXXXXXXXxx", + "private_key": "-----BEGIN PRIVATE KEY-----\nNONONONO\n-----END PRIVATE KEY-----\n", + "client_email": "somedude@your_special_project.iam.gserviceaccount.com", + "client_id": "1234567890", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/somedude%40your_special_project.iam.gserviceaccount.com" +} +``` + +# Usage +If you want to use target project's repository, you can set them via `GOOGLE_APPLICATION_CREDENTIALS`. +```bash +# must set TRIVY_USERNAME empty char +export GOOGLE_APPLICATION_CREDENTIALS=/path/to/credential.json +``` + +# Testing +You can test credentials in the following manner (assuming they are in `/tmp` on host machine). + +```bash +docker run -it --rm -v /tmp:/tmp\ + -e GOOGLE_APPLICATION_CREDENTIALS=/tmp/service_account.json\ + aquasec/trivy image LOCATION-docker.pkg.dev/PROJECT_ID/REPOSITORY_NAME/IMAGE:TAG +``` diff --git a/docs/guide/advanced/private-registries/index.md b/docs/guide/advanced/private-registries/index.md new file mode 100644 index 000000000000..e40f2f8f7230 --- /dev/null +++ b/docs/guide/advanced/private-registries/index.md @@ -0,0 +1,57 @@ +Trivy can download images from a private registry without the need for installing Docker or any other 3rd party tools. +This makes it easy to run within a CI process. + +## Login +You can log in to a private registry using the `trivy registry login` command. +It uses the Docker configuration file (`~/.docker/config.json`) to store the credentials under the hood, and the configuration file path can be configured by `DOCKER_CONFIG` environment variable. + +```shell +$ cat ~/my_password.txt | trivy registry login --username foo --password-stdin ghcr.io +$ trivy image ghcr.io/your/private_image +``` + +## Passing Credentials +You can also provide your credentials when scanning. + +```shell +$ TRIVY_USERNAME=YOUR_USERNAME TRIVY_PASSWORD=YOUR_PASSWORD trivy image YOUR_PRIVATE_IMAGE +``` + +!!! warning + When passing credentials via environment variables or CLI flags, Trivy will attempt to use these credentials for all registries encountered during scanning, regardless of the target registry. + This can potentially lead to unintended credential exposure. + To mitigate this risk: + + 1. Set credentials cautiously and only when necessary. + 2. Prefer using `trivy registry login` to pre-configure credentials with specific registries, which ensures credentials are only sent to appropriate registries. + +Trivy also supports providing credentials through CLI flags: + +```shell +$ TRIVY_PASSWORD=YOUR_PASSWORD trivy image --username YOUR_USERNAME YOUR_PRIVATE_IMAGE +``` + +!!! warning + The CLI flag `--password` is available, but its use is not recommended for security reasons. + + +You can also store your credentials in `trivy.yaml`. +For more information, please refer to [the documentation](../../references/configuration/config-file.md). + +It can handle multiple sets of credentials as well: + +```shell +$ export TRIVY_USERNAME=USERNAME1,USERNAME2 +$ export TRIVY_PASSWORD=PASSWORD1,PASSWORD2 +$ trivy image YOUR_PRIVATE_IMAGE +``` + +In the example above, Trivy attempts to use two pairs of credentials: + +- USERNAME1/PASSWORD1 +- USERNAME2/PASSWORD2 + +Please note that the number of usernames and passwords must be the same. + +!!! note + `--password-stdin` doesn't support comma-separated passwords. \ No newline at end of file diff --git a/docs/guide/advanced/private-registries/self.md b/docs/guide/advanced/private-registries/self.md new file mode 100644 index 000000000000..8af48a4ff120 --- /dev/null +++ b/docs/guide/advanced/private-registries/self.md @@ -0,0 +1,9 @@ +BasicAuth server needs `TRIVY_USERNAME` and `TRIVY_PASSWORD`. + +```bash +export TRIVY_USERNAME={USERNAME} +export TRIVY_PASSWORD={PASSWORD} + +# if you want to use 80 port, use NonSSL +export TRIVY_NON_SSL=true +``` diff --git a/docs/guide/advanced/self-hosting.md b/docs/guide/advanced/self-hosting.md new file mode 100644 index 000000000000..a5c301fbdfcc --- /dev/null +++ b/docs/guide/advanced/self-hosting.md @@ -0,0 +1,133 @@ +# Self-Hosting Trivy's Databases + +This document explains how to host Trivy's [external dependencies](./air-gap.md) in your own infrastructure to prevent external network access. If you haven't already, please familiarize yourself with the [Databases document](../configuration/db.md) that explains about the different databases used by Trivy and the different configuration options that control them. This guide assumes you are already familiar with the concepts explained there. + +## OCI databases + +The following [Trivy Databases](../configuration/db.md) are packaged as OCI images: + +- `trivy-db` +- `trivy-java-db` +- `trivy-checks` + +To host these databases in your own infrastructure: + +### Make a local copy + +Use any container registry manipulation tool (e.g , [crane](https://github.com/google/go-containerregistry/blob/main/cmd/crane/doc/crane.md), [ORAS](https://oras.land), [regclient](https://github.com/regclient/regclient/tree/main)) to copy the images to your destination registry. + +!!! note + You will need to keep the databases updated in order to maintain relevant scanning results over time. + +### Configure Trivy + +Use the appropriate [database location flags](../configuration/db.md#database-locations) to change the db-repository location: + +- `--db-repository` +- `--java-db-repository` +- `--checks-bundle-repository` + +### Authentication + +If the registry requires authentication, you can configure it as described in the [private registry authentication document](../advanced/private-registries/index.md). + +### OCI Media Types + +When serving, proxying, or manipulating Trivy's databases, note that the media type of the OCI layer is not a standard container image type: + +DB | Media Type | Reference +--- | --- | --- +`trivy-db` | `application/vnd.aquasec.trivy.db.layer.v1.tar+gzip` | +`trivy-java-db` | `application/vnd.aquasec.trivy.javadb.layer.v1.tar+gzip` | https://github.com/aquasecurity/trivy-java-db/pkgs/container/trivy-java-db +`trivy-checks` | `application/vnd.oci.image.manifest.v1+json` | https://github.com/aquasecurity/trivy-checks/pkgs/container/trivy-checks + +## Manual cache population + +Trivy uses a local cache directory to store the database files, as described in the [cache](../configuration/cache.md) document. +You can download the databases files and surgically populate the Trivy cache directory with them. + +### Downloading the DB files + +On a machine with internet access, pull the database container archive from the public registry into your local workspace: + +Note that these examples operate in the current working directory. + +=== "Using ORAS" + This example uses [ORAS](https://oras.land), but you can use any other container registry manipulation tool. + + ```shell + oras pull ghcr.io/aquasecurity/trivy-db:2 + ``` + + You should now have a file called `db.tar.gz`. Next, extract it to reveal the db files: + + ```shell + tar -xzf db.tar.gz + ``` + + +=== "Using Trivy" + This example uses Trivy to pull the database container archive. The `--cache-dir` flag makes Trivy download the database files into our current working directory. The `--download-db-only` flag tells Trivy to only download the database files, not to scan any images. + + ```shell + trivy image --cache-dir . --download-db-only + ``` + +You should now have 2 new files, `metadata.json` and `trivy.db`. These are the Trivy DB files, copy them over to the air-gapped environment. + +### Populating the Trivy Cache + +In order to populate the cache, you need to identify the location of the cache directory. If it is under the default location, you can run the following command to find it: + +```shell +trivy -h | grep cache +``` + +For the example, we will assume the `TRIVY_CACHE_DIR` variable holds the cache location: + +```shell +TRIVY_CACHE_DIR=/home/user/.cache/trivy +``` + +Put the Trivy DB files in the Trivy cache directory under a `db` subdirectory: + +```shell +# ensure cache db directory exists +mkdir -p ${TRIVY_CACHE_DIR}/db +# copy the db files +cp /path/to/trivy.db /path/to/metadata.json ${TRIVY_CACHE_DIR}/db/ +``` + +### Java DB adaptations + +For Java DB the process is the same, except for the following: + +1. Image location is `ghcr.io/aquasecurity/trivy-java-db:1` +2. Archive file name is `javadb.tar.gz` +3. Java DB files names are `trivy-java.db` and `metadata.json` +4. The cache subdirectory is `java-db`. + +## VEX Hub + +### Make a local copy + +To make a copy of VEX Hub in a location that is accessible to Trivy. + +1. Download the [VEX Hub](https://github.com/aquasecurity/vexhub) archive from: . +1. Download the [VEX Hub Repository Manifest](https://github.com/aquasecurity/vex-repo-spec#2-repository-manifest) file from: . +1. Create or identify an internal HTTP server that can serve the VEX Hub repository in your environment (e.g `https://server.local`). +1. Make the downloaded archive file available for serving from your server (e.g `https://server.local/main.zip`). +1. Modify the downloaded manifest file's [Location URL](https://github.com/aquasecurity/vex-repo-spec?tab=readme-ov-file#locations-subfields) field to the URL of the archive file on your server (e.g `url: https://server.local/main.zip`). +1. Make the manifest file available for serving from your server under the `/.well-known` path (e.g `https://server.local/.well-known/vex-repository.json`). + +### Configure Trivy + +To configure Trivy to use the local VEX Repository: + +1. Locate your [Trivy VEX configuration file](../supply-chain/vex/repo.md#configuration-file) by running `trivy vex repo init`. Make the following changes to the file. +1. Disable the default VEX Hub repo (`enabled: false`) +1. Add your internal VEX Hub repository as a [custom repository](../supply-chain/vex/repo.md#custom-repositories) with the URL pointing to your local server (e.g `url: https://server.local`). + +### Authentication + +If your server requires authentication, you can configure it as described in the [VEX Repository Authentication document](../supply-chain/vex/repo.md#authentication). diff --git a/docs/guide/advanced/telemetry-flags.md b/docs/guide/advanced/telemetry-flags.md new file mode 100644 index 000000000000..56066fc3e7b4 --- /dev/null +++ b/docs/guide/advanced/telemetry-flags.md @@ -0,0 +1,42 @@ +``` +--clear-cache +--debug +--dependency-tree +--detection-priority +--distro +--exit-code +--exit-on-eol +--format +--ignore-status +--ignore-unfixed +--image-config-scanners +--include-deprecated-checks +--include-dev-deps +--include-non-failures +--insecure +--license-full +--list-all-pkgs +--misconfig-scanners +--offline-scan +--parallel +--password-stdin +--pkg-relationships +--pkg-types +--quiet +--redis-tls +--rego-error-limit +--removed-pkgs +--report +--scanners +--severity +--show-suppressed +--skip-check-update +--skip-version-check +--skip-vex-repo-update +--slow +--tf-exclude-downloaded-modules +--timeout +--trace-http +--trace-rego +--vuln-severity-source +``` diff --git a/docs/guide/advanced/telemetry.md b/docs/guide/advanced/telemetry.md new file mode 100644 index 000000000000..73a1a9e7eac7 --- /dev/null +++ b/docs/guide/advanced/telemetry.md @@ -0,0 +1,39 @@ +# Usage Telemetry + +Trivy collects anonymous usage data in order to help us improve the product. This document explains what is collected and how you can control it. + +## Data collected + +The following information could be collected: + +- Environmental information: + - Installation identifier + - Trivy version + - Operating system +- Scan: + - Non-revealing scan options (see below for comprehensive list) + +### Captured scan options +The following flags will be included with their value: + +--8<-- "./docs/guide/advanced/telemetry-flags.md" + + +## Privacy + +No personal information, scan results, or sensitive data is specifically collected. We take the following measures to ensure that: + +- Installation identifier: one-way hash of machine fingerprint, resulting in opaque ID. +- Scan: any option that is user-controlled is omitted (never collected). For example, file paths, image names, etc are never collected. + +Trivy is an Aqua Security product and adheres to the company's privacy policy: . + +## Disabling telemetry + +You can disable telemetry altogether using the `--disable-telemetry` flag. Like other Trivy flags, this can be set on the command line, YAML configuration file, or environment variable. For more details see [here](../configuration/index.md). + +For example: + +```bash +trivy image --disable-telemetry alpine +``` diff --git a/docs/guide/compliance/compliance.md b/docs/guide/compliance/compliance.md new file mode 100644 index 000000000000..d44c3d63ca1a --- /dev/null +++ b/docs/guide/compliance/compliance.md @@ -0,0 +1,290 @@ +# Built-in Compliance Reports + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy’s compliance flag lets you curate a specific set of checks into a report. In a typical Trivy scan, there are hundreds of different checks for many different components and configurations, but sometimes you already know which specific checks you are interested in. Often this would be an industry accepted set of checks such as CIS, or some vendor specific guideline, or your own organization policy that you want to comply with. These are all possible using the flexible compliance infrastructure that's built into Trivy. Compliance reports are defined as simple YAML documents that select checks to include in the report. + +## Usage + +Compliance report is currently supported in the following targets (trivy sub-commands): + +- `trivy image` +- `trivy k8s` + +Add the `--compliance` flag to the command line, and set its value to the desired report. +For example: `trivy k8s cluster --compliance k8s-nsa` (see below for built-in and custom reports) + +### Options + +The following flags are compatible with the `--compliance` flag and allow customizing its output: + +| flag | effect | +|--------------------|--------------------------------------------------------------------------------------| +| `--report summary` | shows a summary of the results. for every control shows the number of failed checks. | +| `--report all` | shows fully detailed results. for every control shows where it failed and why. | +| `--format table` | shows results in textual table format (good for human readability). | +| `--format json` | shows results in json format (good for machine readability). | + +## Built-in compliance + +Trivy has a number of built-in compliance reports that you can assess right out of the box. +To specify a built-in compliance report, select it by ID like `trivy --compliance `. + +For the list of built-in compliance reports, please see the relevant section: + +- [Docker compliance](../target/container_image.md#compliance) +- [Kubernetes compliance](../target/kubernetes.md#compliance) + +## Contribute a Built-in Compliance Report + +### Define a Compliance spec, based on CIS benchmark or other specs + +Here is an example for CIS compliance report: + +```yaml +--- +spec: + id: k8s-cis-1.23 + title: CIS Kubernetes Benchmarks v1.23 + description: CIS Kubernetes Benchmarks + platform: k8s + type: cis + version: '1.23' + relatedResources: + - https://www.cisecurity.org/benchmark/kubernetes + controls: + - id: 1.1.1 + name: Ensure that the API server pod specification file permissions are set to + 600 or more restrictive + description: Ensure that the API server pod specification file has permissions + of 600 or more restrictive + checks: + - id: AVD-KCV-0073 + commands: + - id: CMD-0001 + severity: HIGH + +``` + +### Compliance ID + +ID field is the name used to execute the compliance scan via trivy +example: + +```sh +trivy k8s --compliance k8s-cis-1.23 +``` + +ID naming convention: {platform}-{type}-{version} + +### Compliance Platform + +The platform field specifies the type of platform on which to run this compliance report. +Supported platforms: + +- k8s (native kubernetes cluster) +- eks (elastic kubernetes service) +- aks (azure kubernetes service) +- gke (google kubernetes engine) +- rke2 (rancher kubernetes engine v2) +- ocp (OpenShift Container Platform) +- docker (docker engine) +- aws (amazon web services) + +### Compliance Type + +The type field specifies the kind compliance report. + +- cis (Center for Internet Security) +- nsa (National Security Agency) +- pss (Pod Security Standards) + +### Compliance Version + +The version field specifies the version of the compliance report. + +- 1.23 + +### Compliance Check ID + +Specify the check ID that needs to be evaluated based on the information collected from the command data output to assess the control. + +Example of how to define check data under [checks folder](https://github.com/aquasecurity/trivy-checks/tree/main/checks): + +```sh +# METADATA +# title: "Ensure that the --kubeconfig kubelet.conf file permissions are set to 600 or more restrictive" +# description: "Ensure that the kubelet.conf file has permissions of 600 or more restrictive." +# scope: package +# schemas: +# - input: schema["kubernetes"] +# related_resources: +# - https://www.cisecurity.org/benchmark/kubernetes +# custom: +# id: KCV0073 +# avd_id: AVD-KCV-0073 +# severity: HIGH +# short_code: ensure-kubelet.conf-file-permissions-600-or-more-restrictive. +# recommended_action: "Change the kubelet.conf file permissions to 600 or more restrictive if exist" +# input: +# selector: +# - type: kubernetes +package builtin.kubernetes.KCV0073 + +import data.lib.kubernetes + +types := ["master", "worker"] + +validate_kubelet_file_permission(sp) := {"kubeletConfFilePermissions": violation} { + sp.kind == "NodeInfo" + sp.type == types[_] + violation := {permission | permission = sp.info.kubeletConfFilePermissions.values[_]; permission > 600} + count(violation) > 0 +} + +deny[res] { + output := validate_kubelet_file_permission(input) + msg := "Ensure that the --kubeconfig kubelet.conf file permissions are set to 600 or more restrictive" + res := result.new(msg, output) +} +``` + +### Compliance Command ID + +***Note:*** This field is not mandatory, it is relevant to k8s compliance report when node-collector is in use + +Specify the command ID (#ref) that needs to be executed to collect the information required to evaluate the control. + +Example of how to define command data under [commands folder](https://github.com/aquasecurity/trivy-checks/tree/main/commands) + +```yaml +--- +- id: CMD-0001 + key: kubeletConfFilePermissions + title: kubelet.conf file permissions + nodeType: worker + audit: stat -c %a $kubelet.kubeconfig + platforms: + - k8s + - aks +``` + +#### Command ID + +Find the next command ID by running the command on [trivy-checks project](https://github.com/aquasecurity/trivy-checks). + +```sh +make command-id +``` + +#### Command Key + +- Re-use an existing key or specify a new one (make sure key name has no spaces) + +Note: The key value should match the key name evaluated by the Rego check. + +### Command Title + +Represent the purpose of the command + +### Command NodeType + +Specify the node type on which the command is supposed to run. + +- worker +- master + +### Command Audit + +Specify here the shell command to be used please make sure to add error suppression (2>/dev/null) + +### Command Platforms + +The list of platforms that support this command. Name should be taken from this list [Platforms](#compliance-platform) + +### Command Config Files + +The commands use a configuration file that helps obtain the paths to binaries and configuration files based on different platforms (e.g., Rancher, native Kubernetes, etc.). + +For example: + +```yaml +kubelet: + bins: + - kubelet + - hyperkube kubelet + confs: + - /etc/kubernetes/kubelet-config.yaml + - /var/lib/kubelet/config.yaml +``` + +### Commands Files Location + +Currently checks files location are :`https://github.com/aquasecurity/trivy-checks/tree/main/checks` + +Command files location: `https://github.com/aquasecurity/trivy-checks/tree/main/commands` +under command file + +Note: command config files will be located under `https://github.com/aquasecurity/trivy-checks/tree/main/commands` as well + +### Node-collector output + +The node collector will read commands and execute each command, and incorporate the output into the NodeInfo resource. + +example: + +```json +{ + "apiVersion": "v1", + "kind": "NodeInfo", + "metadata": { + "creationTimestamp": "2023-01-04T11:37:11+02:00" + }, + "type": "master", + "info": { + "adminConfFileOwnership": { + "values": [ + "root:root" + ] + }, + "adminConfFilePermissions": { + "values": [ + 600 + ] + } + ... + } +} +``` + +## Custom compliance + +You can create your own custom compliance report. A compliance report is a simple YAML document in the following format: + +```yaml +spec: + id: "k8s-myreport" # report unique identifier. this should not contain spaces. + title: "My custom Kubernetes report" # report title. Any one-line title. + description: "Describe your report" # description of the report. Any text. + relatedResources : + - https://some.url # useful references. URLs only. + version: "1.0" # spec version (string) + controls: + - name: "Non-root containers" # Name for the control (appears in the report as is). Any one-line name. + description: 'Check that container is not running as root' # Description (appears in the report as is). Any text. + id: "1.0" # control identifier (string) + checks: # list of existing Trivy checks that define the control + - id: AVD-KSV-0012 # check ID. Must start with `AVD-` or `CVE-` + severity: "MEDIUM" # Severity for the control (note that checks severity isn't used) + - name: "Immutable container file systems" + description: 'Check that container root file system is immutable' + id: "1.1" + checks: + - id: AVD-KSV-0014 + severity: "LOW" +``` + +The check id field (`controls[].checks[].id`) is referring to existing check by it's "AVD ID". This AVD ID is easily located in the check's source code metadata header, or by browsing [Aqua vulnerability DB](https://avd.aquasec.com/), specifically in the [Misconfigurations](https://avd.aquasec.com/misconfig/) and [Vulnerabilities](https://avd.aquasec.com/nvd) sections. + +Once you have a compliance spec, you can select it by file path: `trivy --compliance @
` (note the `@` indicating file path instead of report id). diff --git a/docs/guide/compliance/contrib-compliance.md b/docs/guide/compliance/contrib-compliance.md new file mode 100644 index 000000000000..357b9eb3e7f6 --- /dev/null +++ b/docs/guide/compliance/contrib-compliance.md @@ -0,0 +1,101 @@ +# Custom Compliance Spec + +Trivy supports several different compliance specs. The details on compliance scanning with Trivy are provided in the [compliance documentation](../../guide/compliance/compliance.md). +All of the Compliance Specs currently available in Trivy can be found in the `trivy-checks/pkg/specs/compliance/` directory ([Link](https://github.com/aquasecurity/trivy-checks/tree/main/pkg/specs/compliance)). + +New checks are based on the custom compliance report detailed in the [main documentation.](./compliance.md#custom-compliance) +If you would like to create your custom compliance report, please reference the information in the main documentation. This section details how community members can contribute new Compliance Specs to Trivy. + +All compliance specs in Trivy are based on formal compliance reports such as CIS Benchmarks. + +## Contributing new Compliance Specs + +Compliance specs can be based on new compliance reports becoming available e.g. a new CIS Benchmark version, or identifying missing compliance specs that Trivy users would like to access. + +### Create a new Compliance Spec + +The existing compliance specs in Trivy are located under the `trivy-checks/pkg/specs/compliance/` directory ([Link](https://github.com/aquasecurity/trivy-checks/tree/main/pkg/specs/compliance)). + +Create a new file under `trivy-checks/specs/compliance/` and name the file in the format of "provider-resource-spectype-version.yaml". For example, the file name for AWS CIS Benchmarks for EKS version 1.4 is: `aws-eks-cis-1.4.yaml`. Note that if the compliance spec is not specific to a provider, the `provider` field can be ignored. + +### Minimum spec structure + +The structure of the compliance spec is detailed in the [main documentation](./compliance.md#custom-compliance). + +The first section in the spec is focused on the metadata of the spec. Replace all the fields of the metadata with the information relevant to the compliance spec that will be added. This information can be taken from the official report e.g. the CIS Benchmark report. + +### Populating the `control` section + +Compliance specs detail a set of checks that should pass so that the resource is compliant with the official benchmark specifications. There are two ways in which Trivy compliance checks can enforce the compliance specification: + +1. The check is available in Trivy, as part of the `trivy-checks` and can be referenced in the Compliance Spec +2. The check is not available in Trivy and a manual check has to be added to the Compliance Spec + +Additional information is provided below. + +#### 1. Referencing a check that is already part of Trivy + +Trivy has a comprehensive list of checks as part of its misconfiguration scanning. These can be found in the `trivy-checks/checks` directory ([Link](https://github.com/aquasecurity/trivy-checks/tree/main/checks)). If the check is present, the `AVD_ID` and other information from the check has to be used. + +Note: Take a look at the more generic compliance specs that are already available in Trivy. If you are adding new compliance spec to Kubernetes e.g. AWS EKS CIS Benchmarks, chances are high that the check you would like to add to the new spec has already been defined in the general `k8s-ci-v.000.yaml` compliance spec. The same applies for creating specific Cloud Provider Compliance Specs and the [generic compliance specs](https://github.com/aquasecurity/trivy-checks/tree/main/pkg/specs/compliance) available. + +For example, the following check is detailed in the AWS EKS CIS v1.4 Benchmark: +`3.1.2 Ensure that the kubelet kubeconfig file ownership is set to root:root (Manual)` + +This check can be found in the general K8s CIS Compliance Benchmark: `k8s-cis-1.23.yaml` ([Link](https://github.com/aquasecurity/trivy-checks/blob/31e779916f3863dd74a28cee869ea24fdc4ca8c2/specs/compliance/k8s-cis-1.23.yaml#L480)) + +Thus, we can use the information already present: + +``` + - id: 3.1.2 + name: Ensure that the kubelet service file ownership is set to root:root (Manual) + description: Ensure that the kubelet service file ownership is set to root:root + checks: + - id: AVD-KCV-0070 + severity: HIGH +``` + +- The `ID`, `name`, and `description` is taken directly from the AWS EKS CIS Benchmarks +- The `check` and `severity` are taken from the existing compliance check in the `k8s-cis-1.23.yaml` + + +#### 2. Referencing a check manually that is not part of the Trivy default checks + +If the check does not already exist in the [Aqua Vulnerability Database](https://avd.aquasec.com/) (AVD) and is not part of the trivy-checks, the fields in the compliance spec for the check have to be populated manually. This is done by referencing the information in the official compliance specification. + +Below is the beginning of the information of the EKS CIS Benchmarks v1.4.0: + +![EKS Benchmarks 2.1.1](../../imgs/eks-benchmarks.png) + +The corresponding check in the `control` section will look like this: + +``` + - id: 2.1.1 + name: Enable audit Logs (Manual) + description: | + Control plane logs provide visibility into operation of the EKS Control plane components systems. + The API server audit logs record all accepted and rejected requests in the cluster. + When enabled via EKS configuration the control plane logs for a cluster are exported to a CloudWatch + Log Group for persistence. + checks: null + severity: MEDIUM +``` + +- Again, the `id`, `name` and `description` are taken directly from the EKS CIS Benchmarks v1.4.0 +- The `checks` is in this case `null` as the check is not currently present in the AVD and does not have a check in the [trivy policies](https://github.com/aquasecurity/trivy-checks/tree/main/checks) repository +- Since the check does not exist in Trivy, the `severity` will be `MEDIUM`. However, in some cases, the compliance report e.g. the CIS Benchmark report will specify the severity + +#### Contributing new checks to trivy-checks + +All of the checks in trivy-policies can be referenced in the compliance specs. +To write new Rego checks for Trivy, please take a look at the contributing documentation for checks. + +### Test the Compliance Spec + +To test the compliance check, pass the new path into the Trivy scan through the `--compliance` flag. For instance, to pass the check to the Trivy Kubernetes scan use the following command structure: + +``` +trivy k8s cluster --compliance @
--report summary +``` + +Note: The `@` is required before the filepath. \ No newline at end of file diff --git a/docs/guide/configuration/cache.md b/docs/guide/configuration/cache.md new file mode 100644 index 000000000000..ddda6dd8f401 --- /dev/null +++ b/docs/guide/configuration/cache.md @@ -0,0 +1,107 @@ +# Cache +The cache directory includes + +- Cache of previous scans (Scan cache). +- [Vulnerability Database][trivy-db][^1] +- [Java Index Database][trivy-java-db][^2] +- [Misconfiguration Checks][misconf-checks][^3] +- [VEX Repositories](../supply-chain/vex/repo.md) + +The cache option is common to all scanners. + +## Clear Caches +`trivy clean` subcommand removes caches. + +```bash +$ trivy clean --scan-cache +``` + +
+Result + +``` +2024-06-21T21:58:21+04:00 INFO Removing scan cache... +``` + +
+ +If you want to delete cached vulnerability databases, use `--vuln-db`. +You can also delete all caches with `--all`. +See `trivy clean --help` for details. + +## Cache Directory +Specify where the cache is stored with `--cache-dir`. + +```bash +$ trivy --cache-dir /tmp/trivy/ image python:3.4-alpine3.9 +``` + +## Scan Cache Backend +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy utilizes a scan cache to store analysis results, such as package lists. +It supports three types of backends for this cache: + +- Local File System (`fs`) + - The cache path can be specified by `--cache-dir` +- Memory (`memory`) +- Redis (`redis://`) + - `redis://[HOST]:[PORT]` + - TTL can be configured via `--cache-ttl` + +### Local File System +The local file system backend is the default choice for container image, VM image and repository scans. + +!!! note + Internally, this backend uses [BoltDB][boltdb], which has an important limitation: only one process can access the cache at a time. + Subsequent processes attempting to access the cache will be locked. + For more details on this limitation, refer to the [troubleshooting guide][parallel-run]. + +### Memory +The memory backend stores analysis results in memory, which means the cache is discarded when the process ends. +This makes it useful in scenarios where caching is not required or desired. +It serves as the default for filesystem and SBOM scans and can also be employed for container image scans when caching is unnecessary. + +To use the memory backend for a container image scan, you can use the following command: + +```bash +$ trivy image debian:11 --cache-backend memory +``` + +### Redis + +The Redis backend is particularly useful when you need to share the cache across multiple Trivy instances. +You can set up Trivy to use a Redis backend with a command like this: + +```bash +$ trivy server --cache-backend redis://localhost:6379 +``` + +This approach allows for centralized caching, which can be beneficial in distributed or high-concurrency environments. + +If you want to use TLS with Redis, you can enable it by specifying the `--redis-tls` flag. + +```bash +$ trivy server --cache-backend redis://localhost:6379 --redis-tls +``` + +Trivy also supports connecting to Redis with your certificates. +You need to specify `--redis-ca` , `--redis-cert` , and `--redis-key` options. + +``` +$ trivy server --cache-backend redis://localhost:6379 \ + --redis-ca /path/to/ca-cert.pem \ + --redis-cert /path/to/cert.pem \ + --redis-key /path/to/key.pem +``` + +[trivy-db]: ./db.md +[trivy-java-db]: ./db.md +[misconf-checks]: ../scanner/misconfiguration/check/builtin.md +[boltdb]: https://github.com/etcd-io/bbolt +[parallel-run]: https://trivy.dev/docs/{{ git.tag}}/guide/references/troubleshooting/#running-in-parallel-takes-same-time-as-series-run + +[^1]: Downloaded when scanning for vulnerabilities +[^2]: Downloaded when scanning `jar/war/par/ear` files +[^3]: Downloaded when scanning for misconfigurations \ No newline at end of file diff --git a/docs/guide/configuration/db.md b/docs/guide/configuration/db.md new file mode 100644 index 000000000000..79b4f9fb2271 --- /dev/null +++ b/docs/guide/configuration/db.md @@ -0,0 +1,129 @@ +# Trivy Databases + +When you install Trivy, the installed artifact contains the scanner engine but is lacking relevant security information needed to make security detections and recommendations. +These so called "databases" are automatically fetched and maintained by Trivy as needed, so normally you shouldn't notice or worry about them. +This document elaborates on the database management mechanism and its configuration options. + +Trivy relies on the following databases: + +DB | Artifact name | Contents | Purpose +--- | --- | --- | --- +Vulnerabilities DB | `trivy-db` | CVE information collected from various feeds | used only for [vulnerability scanning](../scanner/vulnerability.md) +Java DB | `trivy-java-db` | Index of Java artifacts and their hash digest | used to identify Java artifacts only in [JAR scanning](../coverage/language/java.md) +Checks Bundle | `trivy-checks` | Logic of misconfiguration checks | used only in [misconfiguration/IaC scanning](../scanner/misconfiguration/check/builtin.md) + +!!! note + This is not an exhaustive list of Trivy's external connectivity requirements. + There are additional external resources which may be required by specific Trivy features. + To learn about external connectivity requirements, see the [Advanced Network Scenarios](../advanced/air-gap.md). + +## Locations + +Trivy's databases are published to the following locations: + +| Registry | Image Address | Link +| --- | --- | --- +| GHCR | `ghcr.io/aquasecurity/trivy-db` | +| | `ghcr.io/aquasecurity/trivy-java-db` | +| | `ghcr.io/aquasecurity/trivy-checks` | +| Docker Hub | `aquasec/trivy-db` | +| | `aquasec/trivy-java-db` | +| | `aquasec/trivy-checks` | +| AWS ECR | `public.ecr.aws/aquasecurity/trivy-db` | +| | `public.ecr.aws/aquasecurity/trivy-java-db` | +| | `public.ecr.aws/aquasecurity/trivy-checks` | + +In addition, images are also available via pull-through cache registries like [Google Container Registry Mirror](https://cloud.google.com/artifact-registry/docs/pull-cached-dockerhub-images). + +## Default Locations + +Trivy will attempt to pull images from the following registries in the order specified. + +1. `mirror.gcr.io/aquasec` +2. `ghcr.io/aquasecurity` + +You can specify additional alternative repositories as explained in the [configuring database locations section](#database-locations). + +## DB Management Configuration + +### Database Locations + +You can configure Trivy to download databases from alternative locations by using the flags: + +- `--db-repository` +- `--java-db-repository` +- `--checks-bundle-repository` + +The value should be an image address in a container registry. + +For example: + +``` +trivy image --db-repository registry.gitlab.com/gitlab-org/security-products/dependencies/trivy-db alpine +``` + +The flag accepts multiple values, which can be used to specify multiple alternative repository locations. In case of transient errors (e.g. status 429 or 5xx), Trivy will fall back to alternative registries in the order specified. + +For example: + +``` +trivy image --db-repository my.registry.local/trivy-db --db-repository registry.gitlab.com/gitlab-org/security-products/dependencies/trivy-db alpine +``` + +The Checks Bundle registry location option does not support fallback through multiple options. This is because in case of a failure pulling the Checks Bundle, Trivy will use the embedded checks as a fallback. + +!!! note + Setting the repository location flags overrides the default values which include the official db locations. In case you want to preserve the default locations, you should include them in the list you set as repository locations. + +!!!note + When pulling `trivy-db` or `trivy-java-db`, if image tag is not specified, Trivy defaults to the db schema number instead of the `latest` tag. + +### Skip updates + +You can configure Trivy to not attempt to download any or all database(s), using the flags: + +- `--skip-db-update` +- `--skip-java-db-update` +- `--skip-check-update` + +For example: + +``` +trivy image --skip-db-update --skip-java-db-update --skip-check-update alpine +``` + +### Only update + +You can ask `Trivy` to only update the database without performing a scan. This action will ensure Trivy is up to date, and populate Trivy's database cache for subsequent scans. + +- `--download-db-only` +- `--download-java-db-only` + +For example: + +``` +trivy image --download-db-only +``` + +Note that currently there is no option to download only the Checks Bundle. + +### Remove Databases + +`trivy clean` command removes caches and databases. +You can select which cache component to remove: + +option | description +--- | --- +`-a`/`--all` | remove all caches +`--checks-bundle` | remove checks bundle +`--java-db` | remove Java database +`--scan-cache` | remove scan cache (container and VM image analysis results) +`--vuln-db` | remove vulnerability database + +Example: + +``` +$ trivy clean --vuln-db --java-db +2024-06-24T11:42:31+06:00 INFO Removing vulnerability database... +2024-06-24T11:42:31+06:00 INFO Removing Java database... +``` diff --git a/docs/guide/configuration/filtering.md b/docs/guide/configuration/filtering.md new file mode 100644 index 000000000000..ce79a6876486 --- /dev/null +++ b/docs/guide/configuration/filtering.md @@ -0,0 +1,532 @@ +# Filtering +Trivy provides various methods for filtering the results. + +```mermaid +flowchart LR + Issues("Detected\nIssues") --> Severity + + subgraph Filtering + subgraph Prioritization + direction TB + Severity("By Severity") --> Status("By Status") + end + subgraph Suppression + Status --> Ignore("By Finding IDs") + Ignore --> Rego("By Rego") + Rego --> VEX("By VEX") + end + end + VEX --> Results +``` + +Similar to the functionality of filtering results, you can also limit the sub-targets for each scanner. +For information on these settings, please refer to the scanner-specific documentation ([vulnerability](../scanner/vulnerability.md) , [misconfiguration](../scanner/misconfiguration/index.md), etc.). + +## Prioritization +You can filter the results by + +- [Severity](#by-severity) +- [Status](#by-status) + +### By Severity + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +Use `--severity` option. + +```bash +$ trivy image --severity HIGH,CRITICAL ruby:2.4.0 +``` + +
+Result + +```bash +2019-05-16T01:51:46.255+0900 INFO Updating vulnerability database... +2019-05-16T01:51:49.213+0900 INFO Detecting Debian vulnerabilities... + +ruby:2.4.0 (debian 8.7) +======================= +Total: 1785 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1680, CRITICAL: 105) + ++-----------------------------+------------------+----------+---------------------------+----------------------------------+-------------------------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++-----------------------------+------------------+----------+---------------------------+----------------------------------+-------------------------------------------------+ +| apt | CVE-2019-3462 | CRITICAL | 1.0.9.8.3 | 1.0.9.8.5 | Incorrect sanitation of the | +| | | | | | 302 redirect field in HTTP | +| | | | | | transport method of... | ++-----------------------------+------------------+----------+---------------------------+----------------------------------+-------------------------------------------------+ +| bash | CVE-2019-9924 | HIGH | 4.3-11 | 4.3-11+deb8u2 | bash: BASH_CMD is writable in | +| | | | | | restricted bash shells | ++ +------------------+ + +----------------------------------+-------------------------------------------------+ +| | CVE-2016-7543 | | | 4.3-11+deb8u1 | bash: Specially crafted | +| | | | | | SHELLOPTS+PS4 variables allows | +| | | | | | command substitution | ++-----------------------------+------------------+ +---------------------------+----------------------------------+-------------------------------------------------+ +| binutils | CVE-2017-8421 | | 2.25-5 | | binutils: Memory exhaustion in | +| | | | | | objdump via a crafted PE file | ++ +------------------+ + +----------------------------------+-------------------------------------------------+ +| | CVE-2017-14930 | | | | binutils: Memory leak in | +| | | | | | decode_line_info | ++ +------------------+ + +----------------------------------+-------------------------------------------------+ +| | CVE-2017-7614 | | | | binutils: NULL | +| | | | | | pointer dereference in | +| | | | | | bfd_elf_final_link function | ++ +------------------+ + +----------------------------------+-------------------------------------------------+ +| | CVE-2014-9939 | | | | binutils: buffer overflow in | +| | | | | | ihex.c | ++ +------------------+ + +----------------------------------+-------------------------------------------------+ +| | CVE-2017-13716 | | | | binutils: Memory leak with the | +| | | | | | C++ symbol demangler routine | +| | | | | | in libiberty | ++ +------------------+ + +----------------------------------+-------------------------------------------------+ +| | CVE-2018-12699 | | | | binutils: heap-based buffer | +| | | | | | overflow in finish_stab in | +| | | | | | stabs.c | ++-----------------------------+------------------+ +---------------------------+----------------------------------+-------------------------------------------------+ +| bsdutils | CVE-2015-5224 | | 2.25.2-6 | | util-linux: File name | +| | | | | | collision due to incorrect | +| | | | | | mkstemp use | ++ +------------------+ + +----------------------------------+-------------------------------------------------+ +| | CVE-2016-2779 | | | | util-linux: runuser tty hijack | +| | | | | | via TIOCSTI ioctl | ++-----------------------------+------------------+----------+---------------------------+----------------------------------+-------------------------------------------------+ +``` + +
+ +```bash +trivy config --severity HIGH,CRITICAL examples/misconf/mixed +``` + +
+Result + +```shell +2022-05-16T13:50:42.718+0100 INFO Detected config files: 3 + +Dockerfile (dockerfile) +======================= +Tests: 17 (SUCCESSES: 16, FAILURES: 1) +Failures: 1 (HIGH: 1, CRITICAL: 0) + +HIGH: Last USER command in Dockerfile should not be 'root' +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile. + +See https://avd.aquasec.com/misconfig/ds002 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Dockerfile:3 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 3 [ USER root +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + +deployment.yaml (kubernetes) +============================ +Tests: 8 (SUCCESSES: 8, FAILURES: 0) +Failures: 0 (HIGH: 0, CRITICAL: 0) + + +main.tf (terraform) +=================== +Tests: 1 (SUCCESSES: 0, FAILURES: 1) +Failures: 1 (HIGH: 0, CRITICAL: 1) + +CRITICAL: Classic resources should not be used. +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +AWS Classic resources run in a shared environment with infrastructure owned by other AWS customers. You should run +resources in a VPC instead. + +See https://avd.aquasec.com/misconfig/avd-aws-0081 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + main.tf:2-4 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 2 ┌ resource "aws_db_security_group" "sg" { + 3 │ + 4 └ } +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +``` +
+ +### By Status + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | | +| Secret | | +| License | | + +Trivy supports the following vulnerability statuses: + +- `unknown` +- `not_affected`: this package is not affected by this vulnerability on this platform +- `affected`: this package is affected by this vulnerability on this platform, but there is no patch released yet +- `fixed`: this vulnerability is fixed on this platform +- `under_investigation`: it is currently unknown whether or not this vulnerability affects this package on this platform, and it is under investigation +- `will_not_fix`: this package is affected by this vulnerability on this platform, but there is currently no intention to fix it (this would primarily be for flaws that are of Low or Moderate impact that pose no significant risk to customers) +- `fix_deferred`: this package is affected by this vulnerability on this platform, and may be fixed in the future +- `end_of_life`: this package has been identified to contain the impacted component, but analysis to determine whether it is affected or not by this vulnerability was not performed + +Note that vulnerabilities with the `unknown`, `not_affected` or `under_investigation` status are not detected. +These are only defined for comprehensiveness, and you will not have the opportunity to specify these statuses. + +Some statuses are supported in limited distributions. + +| OS | Fixed | Affected | Under Investigation | Will Not Fix | Fix Deferred | End of Life | +|:----------:|:-----:|:--------:|:-------------------:|:------------:|:------------:|:-----------:| +| Debian | ✓ | ✓ | | | ✓ | ✓ | +| RHEL | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| Other OSes | ✓ | ✓ | | | | | + + +To ignore vulnerabilities with specific statuses, use the `--ignore-status ` option. + + +```bash +$ trivy image --ignore-status affected,fixed ruby:2.4.0 +``` + +
+Result + +``` +2019-05-16T12:50:14.786+0900 INFO Detecting Debian vulnerabilities... + +ruby:2.4.0 (debian 8.7) +======================= +Total: 527 (UNKNOWN: 0, LOW: 276, MEDIUM: 83, HIGH: 158, CRITICAL: 10) + +┌─────────────────────────────┬──────────────────┬──────────┬──────────────┬────────────────────────────┬───────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────────────────────────┼──────────────────┼──────────┼──────────────┼────────────────────────────┼───────────────┼──────────────────────────────────────────────────────────────┤ +│ binutils │ CVE-2014-9939 │ CRITICAL │ will_not_fix │ 2.25-5 │ │ binutils: buffer overflow in ihex.c │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2014-9939 │ +│ ├──────────────────┤ │ │ ├───────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2017-6969 │ │ │ │ │ binutils: Heap-based buffer over-read in readelf when │ +│ │ │ │ │ │ │ processing corrupt RL78 binaries │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2017-6969 │ +│ ├──────────────────┤ │ │ ├───────────────┼──────────────────────────────────────────────────────────────┤ +... +``` + +
+ +!!! tip + To skip all unfixed vulnerabilities, you can use the `--ignore-unfixed` flag . + It is a shorthand of `--ignore-status affected,will_not_fix,fix_deferred,end_of_life`. + It displays "fixed" vulnerabilities only. + +```bash +$ trivy image --ignore-unfixed ruby:2.4.0 +``` + +## Suppression +You can filter the results by + +- [Finding IDs](#by-finding-ids) +- [Rego](#by-rego) +- [Vulnerability Exploitability Exchange (VEX)](#by-vulnerability-exploitability-exchange-vex) + +To show the suppressed results, use the `--show-suppressed` flag. + +!!! note + It's exported as `ExperimentalModifiedFindings` in the JSON output. + +```bash +$ trivy image --vex debian11.csaf.vex --ignorefile .trivyignore.yaml --show-suppressed debian:11 +... + +Suppressed Vulnerabilities (Total: 9) + +┌───────────────┬───────────────┬──────────┬──────────────┬─────────────────────────────────────────────┬───────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Statement │ Source │ +├───────────────┼───────────────┼──────────┼──────────────┼─────────────────────────────────────────────┼───────────────────┤ +│ libdb5.3 │ CVE-2019-8457 │ CRITICAL │ not_affected │ vulnerable_code_not_in_execute_path │ CSAF VEX │ +├───────────────┼───────────────┼──────────┼──────────────┼─────────────────────────────────────────────┼───────────────────┤ +│ bsdutils │ CVE-2022-0563 │ LOW │ ignored │ Accept the risk │ .trivyignore.yaml │ +├───────────────┤ │ │ │ │ │ +│ libblkid1 │ │ │ │ │ │ +├───────────────┤ │ │ │ │ │ +│ libmount1 │ │ │ │ │ │ +├───────────────┤ │ │ │ │ │ +│ libsmartcols1 │ │ │ │ │ │ +├───────────────┤ │ │ │ │ │ +│ libuuid1 │ │ │ │ │ │ +├───────────────┤ │ │ │ │ │ +│ mount │ │ │ │ │ │ +├───────────────┼───────────────┤ │ ├─────────────────────────────────────────────┤ │ +│ tar │ CVE-2005-2541 │ │ │ The vulnerable configuration is not enabled │ │ +├───────────────┼───────────────┤ │ ├─────────────────────────────────────────────┤ │ +│ util-linux │ CVE-2022-0563 │ │ │ Accept the risk │ │ +└───────────────┴───────────────┴──────────┴──────────────┴─────────────────────────────────────────────┴───────────────────┘ +``` + +### By Finding IDs + +Trivy supports the [.trivyignore](#trivyignore) and [.trivyignore.yaml](#trivyignoreyaml) ignore files. + +#### .trivyignore + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +```bash +$ cat .trivyignore +# Accept the risk +CVE-2018-14618 + +# Accept the risk until 2023-01-01 +CVE-2019-14697 exp:2023-01-01 + +# No impact in our settings +CVE-2019-1543 + +# Ignore misconfigurations +AVD-DS-0002 + +# Ignore secrets +generic-unwanted-rule +aws-account-id + +# Ignore licenses +GPL-3.0 +Apache-2.0 WITH LLVM-exception +``` + +```bash +$ trivy image python:3.4-alpine3.9 +``` + +
+Result + +```bash +2019-05-16T12:53:10.076+0900 INFO Updating vulnerability database... +2019-05-16T12:53:28.134+0900 INFO Detecting Alpine vulnerabilities... + +python:3.4-alpine3.9 (alpine 3.9.2) +=================================== +Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +``` + +
+ +#### .trivyignore.yaml + +| Scanner | Supported | +| :--------------: | :-------: | +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +When the extension of the specified ignore file is either `.yml` or `.yaml`, Trivy will load the file as YAML. +For the `.trivyignore.yaml` file, you can set ignored IDs separately for `vulnerabilities`, `misconfigurations`, `secrets`, or `licenses`[^1]. + +Available fields: + +| Field | Required | Type | Description | +|------------|:--------:|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| id | ✓ | string | The identifier of the vulnerability, misconfiguration, secret, or license[^1]. | +| paths[^2] | | string array | The list of file paths to ignore. If `paths` is not set, the ignore finding is applied to all files. | +| purls | | string array | The list of PURLs to ignore packages. If `purls` is not set, the ignore finding is applied to all packages. This field is currently available only for vulnerabilities. | +| expired_at | | date (`yyyy-mm-dd`) | The expiration date of the ignore finding. If `expired_at` is not set, the ignore finding is always valid. | +| statement | | string | The reason for ignoring the finding. (This field is not used for filtering.) | + +```bash +$ cat .trivyignore.yaml +vulnerabilities: + - id: CVE-2022-40897 + paths: + - "usr/local/lib/python3.9/site-packages/setuptools-58.1.0.dist-info/METADATA" + statement: Accept the risk + - id: CVE-2023-2650 + - id: CVE-2023-3446 + - id: CVE-2023-3817 + purls: + - "pkg:deb/debian/libssl1.1" + - id: CVE-2023-29491 + expired_at: 2023-09-01 + +misconfigurations: + - id: AVD-DS-0001 + - id: AVD-DS-0002 + paths: + - "docs/Dockerfile" + statement: The image needs root privileges + +secrets: + - id: aws-access-key-id + - id: aws-secret-access-key + paths: + - "foo/bar/aws.secret" + +licenses: + - id: GPL-3.0 # License name is used as ID + paths: + - "usr/share/gcc/python/libstdcxx/v6/__init__.py" + - id: MIT AND GPL-2.0-or-later # Compound license expressions are supported + - id: Apache-2.0 WITH LLVM-exception # License expressions with exceptions are supported + - id: LLVM-exception # Individual license components or exceptions can be ignored +``` + +!!! info "Enhanced License Expression Support" + Trivy supports filtering complex SPDX license expressions including: + + - **Compound expressions** with AND/OR operators: `MIT AND GPL-2.0-or-later` + - **License exceptions** with WITH operator: `Apache-2.0 WITH LLVM-exception` + - **Individual components**: You can ignore specific license components or exceptions from compound expressions + + When filtering compound expressions: + + - **AND/OR expressions**: All individual license components must be explicitly ignored for the entire expression to be ignored + - **WITH expressions**: License expressions with exceptions are treated as single entities and can be ignored as a whole + - **Component matching**: You can also ignore individual license names or exception names to filter specific parts of compound expressions + +Since this feature is experimental, you must explicitly specify the YAML file path using the `--ignorefile` flag. +Once this functionality is stable, the YAML file will be loaded automatically. + +```bash +$ trivy image --ignorefile ./.trivyignore.yaml python:3.9.16-alpine3.16 +``` + +
+Result + +```bash +2023-08-31T11:10:27.155+0600 INFO Vulnerability scanning is enabled +2023-08-31T11:10:27.155+0600 INFO Secret scanning is enabled +2023-08-31T11:10:27.155+0600 INFO If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2023-08-31T11:10:27.155+0600 INFO Please see also https://trivy.dev/docs/latest/docs/scanner/secret/#recommendation for faster secret detection +2023-08-31T11:10:29.164+0600 INFO Detected OS: alpine +2023-08-31T11:10:29.164+0600 INFO Detecting Alpine vulnerabilities... +2023-08-31T11:10:29.169+0600 INFO Number of language-specific files: 1 +2023-08-31T11:10:29.170+0600 INFO Detecting python-pkg vulnerabilities... + +python:3.9.16-alpine3.16 (alpine 3.16.5) +======================================== +Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + + +``` + +
+ +### By Rego + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +[Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) is a policy language that allows you to express decision logic in a concise syntax. +Rego is part of the popular [Open Policy Agent (OPA)](https://www.openpolicyagent.org) CNCF project. +For advanced filtering, Trivy allows you to use Rego language to filter vulnerabilities. + +Use the `--ignore-policy` flag which takes a path to a Rego file that defines the filtering policy. +The Rego package name must be `trivy` and it must include a "rule" named `ignore` which determines if each individual scan result should be excluded (ignore=true) or not (ignore=false). +The `input` for the evaluation is each [DetectedVulnerability](https://github.com/aquasecurity/trivy/blob/00f2059e5d7bc2ca2e3e8b1562bdfede1ed570e3/pkg/types/vulnerability.go#L9) and [DetectedMisconfiguration](https://github.com/aquasecurity/trivy/blob/00f2059e5d7bc2ca2e3e8b1562bdfede1ed570e3/pkg/types/misconfiguration.go#L6). + +A practical way to observe the filtering policy input in your case, is to run a scan with the `--format json` option and look at the resulting structure: + +```bash +trivy image -f json centos:7 + +... + "Results": [ + { + "Target": "centos:7 (centos 7.9.2009)", + "Class": "os-pkgs", + "Type": "centos", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2015-5186", + "PkgID": "audit-libs@2.8.5-4.el7.x86_64", + "PkgName": "audit-libs", + "InstalledVersion": "2.8.5-4.el7", + "Layer": { + "Digest": "sha256:2d473b07cdd5f0912cd6f1a703352c82b512407db6b05b43f2553732b55df3bc", + "DiffID": "sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2015-5186", + "Title": "log terminal emulator escape sequences handling", + "Description": "Audit before 2.4.4 in Linux does not sanitize escape characters in filenames.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-20" + ], +... +``` + +Each individual Vulnerability, Misconfiguration, License and Secret (under `Results.Vulnerabilities`, `Results.Misconfigurations`, +`Results.Licenses`, `Results.Secrets`) is evaluated for exclusion or inclusion by the `ignore` rule. + +The following is a Rego ignore policy that filters out every vulnerability with a specific CWE ID (as seen in the JSON example above): + +```rego +package trivy + +default ignore = false + +ignore { + input.CweIDs[_] == "CWE-20" +} +``` + +```bash +trivy image --ignore-policy examples/ignore-policies/basic.rego centos:7 +``` + +To filter findings of a specific type based on a field that may exist in multiple structures (for example, `PkgName` in both `DetectedVulnerability` and `DetectedLicense`), you can use the `Type` field. This field is automatically added when exporting findings to Rego and indicates the kind of finding. Possible values are: `vulnerability`, `misconfiguration`, `secret`, and `license`. + +For example, the following policy ignores vulnerabilities with a specific package name without affecting other finding types: + +```rego +package trivy + +ignore { + input.Type == "vulnerability" + input.PkgName == "foo" +} +``` + +For more advanced use cases, there is a built-in Rego library with helper functions that you can import into your policy using: `import data.lib.trivy`. +More info about the helper functions are in the library [here](https://github.com/aquasecurity/trivy/tree/{{ git.tag }}/pkg/result/module.go). + +You can create a whitelist of checks using Rego, see the detailed [example](https://github.com/aquasecurity/trivy/tree/{{ git.tag }}/examples/ignore-policies/whitelist.rego). Additional examples are available [here](https://github.com/aquasecurity/trivy/tree/{{ git.tag }}/examples/ignore-policies). + +### By Vulnerability Exploitability Exchange (VEX) +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | | +| Secret | | +| License | | + +Please refer to the [VEX documentation](../supply-chain/vex/index.md) for the details. + + +[^1]: license name is used as id for `.trivyignore.yaml` files. +[^2]: This doesn't work for os package licenses (e.g. apk, dpkg, rpm). For projects which manage dependencies through a dependency file (e.g. go.mod, yarn.lock) `path` should point to that particular file. diff --git a/docs/guide/configuration/index.md b/docs/guide/configuration/index.md new file mode 100644 index 000000000000..645906057510 --- /dev/null +++ b/docs/guide/configuration/index.md @@ -0,0 +1,30 @@ +# Configuration +Trivy's settings can be configured in any of the following methods, which will apply in the following precedence: + +1. CLI flags (overrides all other settings) +2. Environment variables (overrides config file settings) +3. Configuration file + +## CLI Flags +You can view the list of available flags by adding the `--help` flag to a Trivy command, or by exploring the [CLI reference](../references/configuration/cli/trivy.md). + +## Environment Variables +Any CLI option can be set as an environment variable. The environment variable names are similar to the CLI option names, with the following augmentations: + +- Add `TRIVY_` prefix +- All uppercase letters +- Replace `-` with `_` + +For example: + +- `--debug` => `TRIVY_DEBUG` +- `--cache-dir` => `TRIVY_CACHE_DIR` + +``` +$ TRIVY_DEBUG=true TRIVY_SEVERITY=CRITICAL trivy image alpine:3.15 +``` + +## Configuration File +Any setting can be set in a YAML file. By default, config file named `trivy.yaml` is read from the current directory where Trivy is run. To load configuration from a different file, use the `--config` flag and specify the config path to load: `trivy --config /etc/trivy/myconfig.yaml`. + +The structure and settings of the YAML config file is documented in the [Config file](../references/configuration/config-file.md) document. diff --git a/docs/guide/configuration/others.md b/docs/guide/configuration/others.md new file mode 100644 index 000000000000..b31fc8984fd3 --- /dev/null +++ b/docs/guide/configuration/others.md @@ -0,0 +1,173 @@ +# Others + +## Enable/Disable Scanners +You can enable/disable scanners with the `--scanners` flag. + +Supported values: + +- vuln +- misconfig +- secret +- license + +For example, container image scanning enables vulnerability and secret scanners by default. +If you don't need secret scanning, it can be disabled. + +``` shell +$ trivy image --scanners vuln alpine:3.15 +``` + +## Exit Code +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +By default, `Trivy` exits with code 0 even when security issues are detected. +Use the `--exit-code` option if you want to exit with a non-zero exit code. + +``` +$ trivy image --exit-code 1 python:3.4-alpine3.9 +``` + +
+Result + +``` +2019-05-16T12:51:43.500+0900 INFO Updating vulnerability database... +2019-05-16T12:52:00.387+0900 INFO Detecting Alpine vulnerabilities... + +python:3.4-alpine3.9 (alpine 3.9.2) +=================================== +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0) + ++---------+------------------+----------+-------------------+---------------+--------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++---------+------------------+----------+-------------------+---------------+--------------------------------+ +| openssl | CVE-2019-1543 | MEDIUM | 1.1.1a-r1 | 1.1.1b-r1 | openssl: ChaCha20-Poly1305 | +| | | | | | with long nonces | ++---------+------------------+----------+-------------------+---------------+--------------------------------+ +``` + +
+ +This option is useful for CI/CD. In the following example, the test will fail only when a critical vulnerability is found. + +``` +$ trivy image --exit-code 0 --severity MEDIUM,HIGH ruby:2.4.0 +$ trivy image --exit-code 1 --severity CRITICAL ruby:2.4.0 +``` + +## Exit on EOL +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | | +| Secret | | +| License | | + +Sometimes you may surprisingly get 0 vulnerabilities in an old image: + +- Enabling `--ignore-unfixed` option while all packages have no fixed versions. +- Scanning a rather outdated OS (e.g. Ubuntu 10.04). + +An OS at the end of service/life (EOL) usually gets into this situation, which is definitely full of vulnerabilities. +`--exit-on-eol` can fail scanning on EOL OS with a non-zero code. +This flag is available with the following targets. + +- Container images (`trivy image`) +- Virtual machine images (`trivy vm`) +- SBOM (`trivy sbom`) +- Root filesystem (`trivy rootfs`) + +``` +$ trivy image --exit-on-eol 1 alpine:3.10 +``` + +
+Result + +``` +2023-03-01T11:07:15.455+0200 INFO Vulnerability scanning is enabled +... +2023-03-01T11:07:17.938+0200 WARN This OS version is no longer supported by the distribution: alpine 3.10.9 +2023-03-01T11:07:17.938+0200 WARN The vulnerability detection may be insufficient because security updates are not provided + +alpine:3.10 (alpine 3.10.9) +=========================== +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 1) + +┌───────────┬────────────────┬──────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├───────────┼────────────────┼──────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤ +│ apk-tools │ CVE-2021-36159 │ CRITICAL │ 2.10.6-r0 │ 2.10.7-r0 │ libfetch before 2021-07-26, as used in apk-tools, xbps, and │ +│ │ │ │ │ │ other products, mishandles... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-36159 │ +└───────────┴────────────────┴──────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────────┘ +2023-03-01T11:07:17.941+0200 ERROR Detected EOL OS: alpine 3.10.9 +``` + +
+ +This option is useful for CI/CD. +The following example will fail when a critical vulnerability is found or the OS is EOSL: + +``` +$ trivy image --exit-code 1 --exit-on-eol 1 --severity CRITICAL alpine:3.16.3 +``` + +## Mirror Registries + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy supports mirrors for [remote container images](../target/container_image.md#container-registry) and [databases](./db.md). + +To configure them, add a list of mirrors along with the host to the [trivy config file](../references/configuration/config-file.md#registry-options). + +!!! note + Use the `index.docker.io` host for images from `Docker Hub`, even if you don't use that prefix. + +Example for `index.docker.io`: +```yaml +registry: + mirrors: + index.docker.io: + - mirror.gcr.io +``` + +### Registry check procedure +Trivy uses the following registry order to get the image: + +- mirrors in the same order as they are specified in the configuration file +- source registry + +In cases where we can't get the image from the mirror registry (e.g. when authentication fails, image doesn't exist, etc.) - Trivy will check other mirrors (or the source registry if all mirrors have already been checked). + +Example: +```yaml +registry: + mirrors: + index.docker.io: + - mirror.with.bad.auth // We don't have credentials for this registry + - mirror.without.image // Registry doesn't have this image +``` + +When we want to get the image `alpine` with the settings above. The logic will be as follows: + +1. Try to get the image from `mirror.with.bad.auth/library/alpine`, but we get an error because there are no credentials for this registry. +2. Try to get the image from `mirror.without.image/library/alpine`, but we get an error because this registry doesn't have this image (but most likely it will be an error about authorization). +3. Get the image from `index.docker.io` (the original registry). + +## Check for updates + +Trivy periodically checks for updates and notices, and displays a message to the user with recommendations. +Updates checking is non-blocking and has no impact on scanning time, performance, results, or any user experience aspect besides displaying the message. +You can disable updates checking by specifying the `--skip-version-check` flag. + +## Telemetry + +Trivy collected usage data for product improvement. More details in the [Telemetry document](../advanced/telemetry.md). +You can disable telemetry collection using the `--disable-telemetry` flag. diff --git a/docs/guide/configuration/reporting.md b/docs/guide/configuration/reporting.md new file mode 100644 index 000000000000..caae8a091689 --- /dev/null +++ b/docs/guide/configuration/reporting.md @@ -0,0 +1,666 @@ +# Reporting + +## Format +Trivy supports the following formats: + +- Table +- JSON +- [SARIF][sarif-home] +- Template +- SBOM +- GitHub dependency snapshot + +### Table (Default) + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +```bash +$ trivy image -f table golang:1.22.11-alpine3.20 +``` + +
+Result + +``` +... + +Report Summary + +┌─────────────────────────────────────────────┬──────────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤ +│ golang:1.22.11-alpine3.20 (alpine 3.20.5) │ alpine │ 6 │ - │ +├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤ +│ usr/local/go/bin/go │ gobinary │ 1 │ - │ +├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤ +... +├─────────────────────────────────────────────┼──────────┼─────────────────┼─────────┤ +│ usr/local/go/pkg/tool/linux_amd64/vet │ gobinary │ 1 │ - │ +└─────────────────────────────────────────────┴──────────┴─────────────────┴─────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + + +golang:1.22.11-alpine3.20 (alpine 3.20.5) + +Total: 6 (UNKNOWN: 2, LOW: 0, MEDIUM: 2, HIGH: 2, CRITICAL: 0) + +┌────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤ +│ libcrypto3 │ CVE-2024-12797 │ HIGH │ fixed │ 3.3.2-r1 │ 3.3.3-r0 │ openssl: RFC7250 handshakes with unauthenticated servers │ +│ │ │ │ │ │ │ don't abort as expected │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2024-12797 │ +│ ├────────────────┼──────────┤ │ ├───────────────┼─────────────────────────────────────────────────────────────┤ +│ │ CVE-2024-13176 │ MEDIUM │ │ │ 3.3.2-r2 │ openssl: Timing side-channel in ECDSA signature computation │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2024-13176 │ +├────────────┼────────────────┼──────────┤ │ ├───────────────┼─────────────────────────────────────────────────────────────┤ +│ libssl3 │ CVE-2024-12797 │ HIGH │ │ │ 3.3.3-r0 │ openssl: RFC7250 handshakes with unauthenticated servers │ +│ │ │ │ │ │ │ don't abort as expected │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2024-12797 │ +│ ├────────────────┼──────────┤ │ ├───────────────┼─────────────────────────────────────────────────────────────┤ +│ │ CVE-2024-13176 │ MEDIUM │ │ │ 3.3.2-r2 │ openssl: Timing side-channel in ECDSA signature computation │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2024-13176 │ +├────────────┼────────────────┼──────────┤ ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤ +│ musl │ CVE-2025-26519 │ UNKNOWN │ │ 1.2.5-r0 │ 1.2.5-r1 │ musl libc 0.9.13 through 1.2.5 before 1.2.6 has an │ +│ │ │ │ │ │ │ out-of-bounds write ...... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2025-26519 │ +├────────────┤ │ │ │ │ │ │ +│ musl-utils │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ +└────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────────┘ + +usr/local/go/bin/go (gobinary) + +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0) + +┌─────────┬────────────────┬──────────┬────────┬───────────────────┬──────────────────────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼────────────────┼──────────┼────────┼───────────────────┼──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ stdlib │ CVE-2025-22866 │ MEDIUM │ fixed │ v1.22.11 │ 1.22.12, 1.23.6, 1.24.0-rc.3 │ crypto/internal/nistec: golang: Timing sidechannel for P-256 │ +│ │ │ │ │ │ │ on ppc64le in crypto/internal/nistec │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2025-22866 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴──────────────────────────────┴──────────────────────────────────────────────────────────────┘ + +... +``` + +
+ +#### Table mode +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy supports the following modes for `table` format: + +| Mode | Enabled by default | +|:----------------------------:|:-----------------:| +| [summary](#summary-table) | ✓[^1] | +| [detailed](#detailed-tables) | ✓ | + +You can use `--table-mode` flag to enable/disable table mode(s). + + +##### Summary table +Summary table contains general information about the scan performed. + +Nuances of table contents: + +- Table includes columns for enabled [scanners](../references/terminology.md#scanner) only. Use `--scanners` flag to enable/disable scanners. +- Table includes separate lines for the same targets but different scanners. + - `-` means that the scanner didn't scan this target. + - `0` means that the scanner scanned this target, but found no security issues. + +!!! Note + For the secret/license scanner, the Trivy report contains only findings. + Therefore, we can’t say for sure whether Trivy scanned at least one file or simply didn’t find any findings. + That’s why, for these scanners, the summary table uses “-” if no findings are found. + +
+Report Summary + +``` +┌───────────────────────┬────────────┬─────────────────┬───────────────────┬─────────┬──────────┐ +│ Target │ Type │ Vulnerabilities │ Misconfigurations │ Secrets │ Licenses │ +├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ test (alpine 3.20.3) │ alpine │ 2 │ - │ - │ - │ +├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ Java │ jar │ 2 │ - │ - │ - │ +├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ app/Dockerfile │ dockerfile │ - │ 2 │ - │ - │ +├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ requirements.txt │ text │ 0 │ - │ - │ - │ +├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ requirements.txt │ text │ - │ - │ 1 │ - │ +├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ OS Packages │ - │ - │ - │ - │ 1 │ +├───────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ Java │ - │ - │ - │ - │ 0 │ +└───────────────────────┴────────────┴─────────────────┴───────────────────┴─────────┴──────────┘ +``` + +
+ +##### Detailed tables +Detailed tables contain information about found security issues for each target with more detailed information (CVE-ID, severity, version, etc.). + +
+Detailed tables + +``` + +usr/local/go/bin/go (gobinary) + +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0) + +┌─────────┬────────────────┬──────────┬────────┬───────────────────┬──────────────────────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼────────────────┼──────────┼────────┼───────────────────┼──────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ stdlib │ CVE-2025-22866 │ MEDIUM │ fixed │ v1.22.11 │ 1.22.12, 1.23.6, 1.24.0-rc.3 │ crypto/internal/nistec: golang: Timing sidechannel for P-256 │ +│ │ │ │ │ │ │ on ppc64le in crypto/internal/nistec │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2025-22866 │ +└─────────┴────────────────┴──────────┴────────┴───────────────────┴──────────────────────────────┴──────────────────────────────────────────────────────────────┘ + +``` +
+ +#### Show origins of vulnerable dependencies + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | | +| Secret | | +| License | | + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Modern software development relies on the use of third-party libraries. +Third-party dependencies also depend on others so a list of dependencies can be represented as a dependency graph. +In some cases, vulnerable dependencies are not linked directly, and it requires analyses of the tree. +To make this task simpler Trivy can show a dependency origin tree with the `--dependency-tree` flag. +This flag is only available with the `--format table` flag. + +The following OS package managers are currently supported: + +| OS Package Managers | +|---------------------| +| apk | +| dpkg | +| rpm | + +The following languages are currently supported: + +| Language | File | +|----------|--------------------------------------------| +| Node.js | [package-lock.json][nodejs-package-lock] | +| | [pnpm-lock.yaml][pnpm-lock] | +| | [yarn.lock][yarn-lock] | +| .NET | [packages.lock.json][dotnet-packages-lock] | +| Python | [poetry.lock][poetry-lock] | +| | [uv.lock][uv-lock] | +| Ruby | [Gemfile.lock][gemfile-lock] | +| Rust | [cargo-auditable binaries][cargo-binaries] | +| Go | [go.mod][go-mod] | +| PHP | [composer.lock][composer-lock] | +| Java | [pom.xml][pom-xml] | +| | [*gradle.lockfile][gradle-lockfile] | +| | [*.sbt.lock][sbt-lockfile] | +| Dart | [pubspec.lock][pubspec-lock] | + +This tree is the reverse of the dependency graph. +However, if you want to resolve a vulnerability in a particular indirect dependency, the reversed tree is useful to know where that dependency comes from and identify which package you actually need to update. + +In table output, it looks like: + +```sh +$ trivy fs --severity HIGH,CRITICAL --dependency-tree /path/to/your_node_project + +package-lock.json (npm) +======================= +Total: 2 (HIGH: 1, CRITICAL: 1) + +┌──────────────────┬────────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├──────────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ follow-redirects │ CVE-2022-0155 │ HIGH │ 1.14.6 │ 1.14.7 │ follow-redirects: Exposure of Private Personal Information │ +│ │ │ │ │ │ to an Unauthorized Actor │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-0155 │ +├──────────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ glob-parent │ CVE-2020-28469 │ CRITICAL │ 3.1.0 │ 5.1.2 │ nodejs-glob-parent: Regular expression denial of service │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-28469 │ +└──────────────────┴────────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────────────────┘ + +Dependency Origin Tree (Reversed) +================================= +package-lock.json +├── follow-redirects@1.14.6, (HIGH: 1, CRITICAL: 0) +│ └── axios@0.21.4 +└── glob-parent@3.1.0, (HIGH: 0, CRITICAL: 1) + └── chokidar@2.1.8 + └── watchpack-chokidar2@2.0.1 + └── watchpack@1.7.5 + └── webpack@4.46.0 + └── cra-append-sw@2.7.0 +``` + +Vulnerable dependencies are shown in the top level of the tree. +Lower levels show how those vulnerabilities are introduced. +In the example above **axios@0.21.4** included in the project directly depends on the vulnerable **follow-redirects@1.14.6**. +Also, **glob-parent@3.1.0** with some vulnerabilities is included through chain of dependencies that is added by **cra-append-sw@2.7.0**. + +Then, you can try to update **axios@0.21.4** and **cra-append-sw@2.7.0** to resolve vulnerabilities in **follow-redirects@1.14.6** and **glob-parent@3.1.0**. + +### JSON + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +``` +$ trivy image -f json -o results.json alpine:latest +``` + +
+JSON + +``` +{ + "SchemaVersion": 2, + "CreatedAt": "2024-12-26T21:58:15.943876+05:30", + "ArtifactName": "alpine:latest", + "ArtifactType": "container_image", + "Metadata": { + "OS": { + "Family": "alpine", + "Name": "3.20.3" + }, + "ImageID": "sha256:511a44083d3a23416fadc62847c45d14c25cbace86e7a72b2b350436978a0450", + "DiffIDs": [ + "sha256:651d9022c23486dfbd396c13db293af6845731cbd098a5f5606db4bc9f5573e8" + ], + "RepoTags": [ + "alpine:latest" + ], + "RepoDigests": [ + "alpine@sha256:1e42bbe2508154c9126d48c2b8a75420c3544343bf86fd041fb7527e017a4b4a" + ], + "ImageConfig": { + "architecture": "arm64", + "created": "2024-09-06T12:05:36Z", + "history": [ + { + "created": "2024-09-06T12:05:36Z", + "created_by": "ADD alpine-minirootfs-3.20.3-aarch64.tar.gz / # buildkit", + "comment": "buildkit.dockerfile.v0" + }, + { + "created": "2024-09-06T12:05:36Z", + "created_by": "CMD [\"/bin/sh\"]", + "comment": "buildkit.dockerfile.v0", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:651d9022c23486dfbd396c13db293af6845731cbd098a5f5606db4bc9f5573e8" + ] + }, + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "WorkingDir": "/", + "ArgsEscaped": true + } + } + }, + "Results": [ + { + "Target": "alpine:latest (alpine 3.20.3)", + "Class": "os-pkgs", + "Type": "alpine", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2024-9143", + "PkgID": "libcrypto3@3.3.2-r0", + "PkgName": "libcrypto3", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libcrypto3@3.3.2-r0?arch=aarch64\u0026distro=3.20.3", + "UID": "f705555b49cd2259" + }, + "InstalledVersion": "3.3.2-r0", + "FixedVersion": "3.3.2-r1", + "Status": "fixed", + "Layer": { + "DiffID": "sha256:651d9022c23486dfbd396c13db293af6845731cbd098a5f5606db4bc9f5573e8" + }, + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-9143", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Title": "openssl: Low-level invalid GF(2^m) parameters lead to OOB memory access", + "Description": "Issue summary: Use of the low-level GF(2^m) elliptic curve APIs with untrusted\nexplicit values for the field polynomial can lead to out-of-bounds memory reads\nor writes.\n\nImpact summary: Out of bound memory writes can lead to an application crash or\neven a possibility of a remote code execution, however, in all the protocols\ninvolving Elliptic Curve Cryptography that we're aware of, either only \"named\ncurves\" are supported, or, if explicit curve parameters are supported, they\nspecify an X9.62 encoding of binary (GF(2^m)) curves that can't represent\nproblematic input values. Thus the likelihood of existence of a vulnerable\napplication is low.\n\nIn particular, the X9.62 encoding is used for ECC keys in X.509 certificates,\nso problematic inputs cannot occur in the context of processing X.509\ncertificates. Any problematic use-cases would have to be using an \"exotic\"\ncurve encoding.\n\nThe affected APIs include: EC_GROUP_new_curve_GF2m(), EC_GROUP_new_from_params(),\nand various supporting BN_GF2m_*() functions.\n\nApplications working with \"exotic\" explicit binary (GF(2^m)) curve parameters,\nthat make it possible to represent invalid field polynomials with a zero\nconstant term, via the above or similar APIs, may terminate abruptly as a\nresult of reading or writing outside of array bounds. Remote code execution\ncannot easily be ruled out.\n\nThe FIPS modules in 3.3, 3.2, 3.1 and 3.0 are not affected by this issue.", + "Severity": "LOW", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L", + "V3Score": 3.7 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2024-9143", + "https://github.com/openssl/openssl/commit/72ae83ad214d2eef262461365a1975707f862712", + "https://github.com/openssl/openssl/commit/bc7e04d7c8d509fb78fc0e285aa948fb0da04700", + "https://github.com/openssl/openssl/commit/c0d3e4d32d2805f49bec30547f225bc4d092e1f4", + "https://github.com/openssl/openssl/commit/fdf6723362ca51bd883295efe206cb5b1cfa5154", + "https://github.openssl.org/openssl/extended-releases/commit/8efc0cbaa8ebba8e116f7b81a876a4123594d86a", + "https://github.openssl.org/openssl/extended-releases/commit/9d576994cec2b7aa37a91740ea7e680810957e41", + "https://nvd.nist.gov/vuln/detail/CVE-2024-9143", + "https://openssl-library.org/news/secadv/20241016.txt", + "https://www.cve.org/CVERecord?id=CVE-2024-9143" + ], + "PublishedDate": "2024-10-16T17:15:18.13Z", + "LastModifiedDate": "2024-11-08T16:35:21.58Z" + }, + { + "VulnerabilityID": "CVE-2024-9143", + "PkgID": "libssl3@3.3.2-r0", + "PkgName": "libssl3", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libssl3@3.3.2-r0?arch=aarch64\u0026distro=3.20.3", + "UID": "c4a39ef718e71832" + }, + "InstalledVersion": "3.3.2-r0", + "FixedVersion": "3.3.2-r1", + "Status": "fixed", + "Layer": { + "DiffID": "sha256:651d9022c23486dfbd396c13db293af6845731cbd098a5f5606db4bc9f5573e8" + }, + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2024-9143", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Title": "openssl: Low-level invalid GF(2^m) parameters lead to OOB memory access", + "Description": "Issue summary: Use of the low-level GF(2^m) elliptic curve APIs with untrusted\nexplicit values for the field polynomial can lead to out-of-bounds memory reads\nor writes.\n\nImpact summary: Out of bound memory writes can lead to an application crash or\neven a possibility of a remote code execution, however, in all the protocols\ninvolving Elliptic Curve Cryptography that we're aware of, either only \"named\ncurves\" are supported, or, if explicit curve parameters are supported, they\nspecify an X9.62 encoding of binary (GF(2^m)) curves that can't represent\nproblematic input values. Thus the likelihood of existence of a vulnerable\napplication is low.\n\nIn particular, the X9.62 encoding is used for ECC keys in X.509 certificates,\nso problematic inputs cannot occur in the context of processing X.509\ncertificates. Any problematic use-cases would have to be using an \"exotic\"\ncurve encoding.\n\nThe affected APIs include: EC_GROUP_new_curve_GF2m(), EC_GROUP_new_from_params(),\nand various supporting BN_GF2m_*() functions.\n\nApplications working with \"exotic\" explicit binary (GF(2^m)) curve parameters,\nthat make it possible to represent invalid field polynomials with a zero\nconstant term, via the above or similar APIs, may terminate abruptly as a\nresult of reading or writing outside of array bounds. Remote code execution\ncannot easily be ruled out.\n\nThe FIPS modules in 3.3, 3.2, 3.1 and 3.0 are not affected by this issue.", + "Severity": "LOW", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L", + "V3Score": 3.7 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2024-9143", + "https://github.com/openssl/openssl/commit/72ae83ad214d2eef262461365a1975707f862712", + "https://github.com/openssl/openssl/commit/bc7e04d7c8d509fb78fc0e285aa948fb0da04700", + "https://github.com/openssl/openssl/commit/c0d3e4d32d2805f49bec30547f225bc4d092e1f4", + "https://github.com/openssl/openssl/commit/fdf6723362ca51bd883295efe206cb5b1cfa5154", + "https://github.openssl.org/openssl/extended-releases/commit/8efc0cbaa8ebba8e116f7b81a876a4123594d86a", + "https://github.openssl.org/openssl/extended-releases/commit/9d576994cec2b7aa37a91740ea7e680810957e41", + "https://nvd.nist.gov/vuln/detail/CVE-2024-9143", + "https://openssl-library.org/news/secadv/20241016.txt", + "https://www.cve.org/CVERecord?id=CVE-2024-9143" + ], + "PublishedDate": "2024-10-16T17:15:18.13Z", + "LastModifiedDate": "2024-11-08T16:35:21.58Z" + } + ] + } + ] +} + +``` + +
+ +`VulnerabilityID`, `PkgName`, `InstalledVersion`, and `Severity` in `Vulnerabilities` are always filled with values, but other fields might be empty. + +### SARIF +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +[SARIF][sarif-home] (Static Analysis Results Interchange Format) complying with [SARIF 2.1.0 OASIS standard][sarif-spec] can be generated with the `--format sarif` flag. + +``` +$ trivy image --format sarif -o report.sarif golang:1.12-alpine +``` + +This SARIF file can be uploaded to several platforms, including: + +- [GitHub code scanning results][sarif-github], and there is a [Trivy GitHub Action][action] for automating this process +- [SonarQube][sarif-sonar] + +### GitHub dependency snapshot +Trivy supports the following packages: + +- [OS packages][os_packages] +- [Language-specific packages][language_packages] + +[GitHub dependency snapshots][github-sbom] can be generated with the `--format github` flag. + +``` +$ trivy image --format github -o report.gsbom alpine +``` + +This snapshot file can be [submitted][github-sbom-submit] to your GitHub repository. + +### Template + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +#### Custom Template + +{% raw %} +``` +$ trivy image --format template --template "{{ range . }} {{ .Target }} {{ end }}" golang:1.12-alpine +``` +{% endraw %} + +
+Result + +``` +2020-01-02T18:02:32.856+0100 INFO Detecting Alpine vulnerabilities... + golang:1.12-alpine (alpine 3.10.2) +``` +
+ +You can compute different figures within the template using [sprig][sprig] functions. +As an example you can summarize the different classes of issues: + + +{% raw %} +``` +$ trivy image --format template --template '{{- $critical := 0 }}{{- $high := 0 }}{{- range . }}{{- range .Vulnerabilities }}{{- if eq .Severity "CRITICAL" }}{{- $critical = add $critical 1 }}{{- end }}{{- if eq .Severity "HIGH" }}{{- $high = add $high 1 }}{{- end }}{{- end }}{{- end }}Critical: {{ $critical }}, High: {{ $high }}' golang:1.12-alpine +``` +{% endraw %} + +
+Result + +``` +Critical: 0, High: 2 +``` +
+ +For other features of sprig, see the official [sprig][sprig] documentation. + +#### Load templates from a file +You can load templates from a file prefixing the template path with an @. + +``` +$ trivy image --format template --template "@/path/to/template" golang:1.12-alpine +``` + +#### Default Templates + +If Trivy is installed using rpm then default templates can be found at `/usr/local/share/trivy/templates`. + +##### JUnit +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +In the following example using the template `junit.tpl` XML can be generated. +``` +$ trivy image --format template --template "@contrib/junit.tpl" -o junit-report.xml golang:1.12-alpine +``` + +##### ASFF +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | | + +Trivy also supports an [ASFF template for reporting findings to AWS Security Hub][asff] + +##### HTML +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | | +| License | | + +``` +$ trivy image --format template --template "@contrib/html.tpl" -o report.html golang:1.12-alpine +``` + +The following example shows use of default HTML template when Trivy is installed using rpm. + +``` +$ trivy image --format template --template "@/usr/local/share/trivy/templates/html.tpl" -o report.html golang:1.12-alpine +``` + +### SBOM +See [here](../supply-chain/sbom.md) for details. + +## Output +Trivy supports the following output destinations: + +- File +- Plugin + +### File +By specifying `--output `, you can output the results to a file. +Here is an example: + +``` +$ trivy image --format json --output result.json debian:12 +``` + +### Plugin +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Plugins capable of receiving Trivy's results via standard input, called "output plugin", can be seamlessly invoked using the `--output` flag. + +``` +$ trivy [--format ] --output plugin= [--output-plugin-arg ] +``` + +This is useful for cases where you want to convert the output into a custom format, or when you want to send the output somewhere. +For more details, please check [here](../plugin/user-guide.md#output-mode-support). + +## Converting +To generate multiple reports, you can generate the JSON report first and convert it to other formats with the `convert` subcommand. + +```shell +$ trivy image --format json -o result.json debian:11 +$ trivy convert --format cyclonedx --output result.cdx result.json +``` + +[Filtering options](./filtering.md) such as `--severity` are also available with `convert`. + +```shell +# Output all severities in JSON +$ trivy image --format json -o result.json debian:11 + +# Output only critical issues in table format +$ trivy convert --format table --severity CRITICAL result.json +``` + +!!! note + JSON reports from "trivy k8s" are not yet supported. + +[cargo-auditable]: https://github.com/rust-secure-code/cargo-auditable/ +[action]: https://github.com/aquasecurity/trivy-action +[asff]: ../../tutorials/integrations/aws-security-hub.md +[sarif-home]: https://sarifweb.azurewebsites.net +[sarif-spec]: https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html +[sarif-github]: https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning +[sarif-sonar]: https://docs.sonarsource.com/sonarqube/latest/analyzing-source-code/importing-external-issues/importing-issues-from-sarif-reports/ +[sprig]: http://masterminds.github.io/sprig/ +[github-sbom]: https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#about-dependency-submissions +[github-sbom-submit]: https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository + +[os_packages]: ../scanner/vulnerability.md#os-packages +[language_packages]: ../scanner/vulnerability.md#language-specific-packages + +[nodejs-package-lock]: ../coverage/language/nodejs.md#npm +[pnpm-lock]: ../coverage/language/nodejs.md#pnpm +[yarn-lock]: ../coverage/language/nodejs.md#yarn +[dotnet-packages-lock]: ../coverage/language/dotnet.md#packageslockjson +[poetry-lock]: ../coverage/language/python.md#poetry +[uv-lock]: ../coverage/language/python.md#uv +[gemfile-lock]: ../coverage/language/ruby.md#bundler +[go-mod]: ../coverage/language/golang.md#go-module +[composer-lock]: ../coverage/language/php.md#composerlock +[pom-xml]: ../coverage/language/java.md#pomxml +[gradle-lockfile]: ../coverage/language/java.md#gradlelock +[sbt-lockfile]: ../coverage/language/java.md#sbt +[pubspec-lock]: ../coverage/language/dart.md#dart +[cargo-binaries]: ../coverage/language/rust.md#binaries + +[^1]: To show summary table in `convert` mode - you need to enable the scanners used during JSON report generation. \ No newline at end of file diff --git a/docs/guide/configuration/skipping.md b/docs/guide/configuration/skipping.md new file mode 100644 index 000000000000..2f90405a1a5c --- /dev/null +++ b/docs/guide/configuration/skipping.md @@ -0,0 +1,112 @@ +# Selecting files for scanning + +When scanning a target (image, code repository, etc), Trivy traverses all directories and files in that target and looks for known files to scan. For example, vulnerability scanner might look for `/lib/apk/db/installed` for Alpine APK scanning or `requirements.txt` file for Python pip scanning, and misconfiguration scanner might look for `Dockerfile` for Dockerfile scanning. This document explains how to control which files Trivy looks (including skipping files) for and how it should process them. + +!!! note + Selecting/skipping files is different from filtering/ignoring results, which is covered in the [Filtering document](./filtering.md) + +## Skip Files and Directories + +You can skip specific files and directories using the `--skip-files` and `--skip-dirs` flags. + +For example: + +```bash +trivy image --skip-files "/Gemfile.lock" --skip-dirs "/var/lib/gems/2.5.0/gems/http_parser.rb-0.6.0" quay.io/fluentd_elasticsearch/fluentd:v2.9.0 +``` + +This feature is relevant for the following scanners: + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | ✓ | +| License | ✓ | + +It's possible to specify glob patterns when referring to a file or directory. The glob expression follows the ["doublestar" library syntax](https://pkg.go.dev/github.com/bmatcuk/doublestar/v4@v4.8.1#readme-patterns). + +Examples: + +```bash +# skip any file named `bar` in the subdirectories of testdata +trivy image --skip-files "./testdata/*/bar" . +``` + +```bash +# skip any files with the extension `.tf` in subdirectories of foo at any depth +trivy config --skip-files "./foo/**/*.tf" . +``` + +```bash +# skip all subdirectories of the testdata directory. +trivy image --skip-dirs "./testdata/*" . +``` + +```bash +# skip subdirectories at any depth named `.terraform/`. +# this will match `./foo/.terraform` or `./foo/bar/.terraform`, but not `./.terraform` +trivy config --skip-dirs "**/.terraform" . +``` + +Like any other flag, this is available as Trivy YAML configuration. + +For example: + +```yaml +scan: + skip-files: + - foo + - "testdata/*/bar" + skip-dirs: + - foo/bar/ + - "**/.terraform" +``` + +## Customizing file handling + +You can customize which files Trivy scans and how it interprets them with the `--file-patterns` flag. +A file pattern configuration takes the following form: `:`, such that files matching the `` will be processed with the respective ``. + +!!! Note + `--file-patterns` flag doesn't disable the default file detection behavior of Trivy. It only adds the file detection based on the specified patterns. + +For example: + +```bash +trivy fs --file-patterns "pip:.requirements-test.txt" . +``` + +This feature is relevant for the following scanners: + +| Scanner | Supported | +|:----------------:|:---------:| +| Vulnerability | ✓ | +| Misconfiguration | ✓ | +| Secret | | +| License | ✓[^1] | + +The list of analyzers can be found [here](https://github.com/aquasecurity/trivy/tree/{{ git.commit }}/pkg/fanal/analyzer/const.go). +Note that this flag is not applicable for parsers that accepts files of different extensions, for example the Terraform file parser which handles .tf and .tf.json files. + + +The file path can use a [regular expression](https://pkg.go.dev/regexp/syntax). For example: + +```bash +# interpret any file with .txt extension as a python pip requirements file +trivy fs --file-patterns "pip:requirements-.*\.txt" . +``` + +The flag can be repeated for specifying multiple file patterns. For example: + +```bash +# look for Dockerfile called production.docker and a python pip requirements file called requirements-test.txt +trivy fs --scanners misconfig,vuln --file-patterns "dockerfile:.production.docker" --file-patterns "pip:.requirements-test.txt" . +``` + +[^1]: Only work with the [license-full](../scanner/license.md) flag + +## Avoid full filesystem traversal + +In specific scenarios Trivy can avoid traversing the entire filesystem, which makes scanning faster and more efficient. +For more information see [here](../target/rootfs.md#performance-optimization) diff --git a/docs/guide/coverage/iac/ansible.md b/docs/guide/coverage/iac/ansible.md new file mode 100644 index 000000000000..3303464645ce --- /dev/null +++ b/docs/guide/coverage/iac/ansible.md @@ -0,0 +1,177 @@ +# Ansible + +Trivy analyzes tasks in playbooks and roles for misconfigurations in cloud resources. + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +!!! warning "LIMITATIONS" + Not all Ansible features are supported. See the [Limitations](#limitations) section for a detailed list. + +## Misconfigurations + +Trivy recursively scans directories starting from the root and detects Ansible projects by the presence of key files and folders: + +- `ansible.cfg`, `inventory`, `group_vars`, `host_vars`, `roles` and `playbooks` +- YAML files that resemble playbooks + +For each project, Trivy performs the following steps: + +- **Playbook discovery** — determines entry points, i.e., playbooks that are not used as imports in other playbooks. +- **Task and variable resolution** — Trivy resolves tasks and variables from plays, imports, and roles. +- **Module analysis** — modules used in tasks are scanned for insecure configurations. Currently, only cloud resource modules are supported. + +### Project scanning + +The Ansible scanner is enabled by default. To run only this scanner, use the `--misconfig-scanners ansible` flag: + +```bash +trivy conf --misconfig-scanners ansible . +``` + +Example playbook: + +```yaml +- name: Example playbook + hosts: localhost + connection: local + tasks: + - name: Create S3 bucket + amazon.aws.s3_bucket: + name: "{{ bucket_name }}" + region: "{{ bucket_region }}" + state: present +``` + +Scan result: + +```bash +AVD-AWS-0093 (HIGH): Public access block does not restrict public buckets +══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +S3 buckets should restrict public policies for the bucket. By enabling, the restrict_public_buckets, only the bucket owner and AWS Services can access if it has a public policy. + + +See https://avd.aquasec.com/misconfig/avd-aws-0093 +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + test.yaml:6-9 + via test.yaml:5-9 (tasks) + via test.yaml:1-9 (play) +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 1 - name: Example playbook + 2 hosts: localhost + 3 connection: local + 4 tasks: + 5 - name: Create S3 bucket + 6 ┌ amazon.aws.s3_bucket: + 7 │ name: "{{ bucket_name }}" + 8 │ region: "{{ bucket_region }}" + 9 └ state: present +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +``` + +If the project defines a collection (contains a `galaxy.yaml` file), Trivy can resolve roles using the full name `namespace.collection.role` within the project. + +Example `galaxy.yaml`: +```yaml +namespace: myorg +name: mycollection +version: 1.0.0 +``` + +Project structure: +```bash +roles/ + myrole/ + tasks/ + main.yml +galaxy.yaml +``` + +Using the role in a playbook: +```yaml +- name: Apply custom role + hosts: localhost + tasks: + - name: Run role from collection + include_role: + name: myorg.mycollection.myrole +``` + +Trivy can correctly locate and analyze the `myrole` role via the full collection name. + + +### Scanning specific playbooks + +To limit scanning to specific playbooks instead of automatically discovering them, use the `--ansible-playbook` flag (can be repeated) with the path to the playbook: + +```bash +trivy config --ansible-playbook playbooks/main.yaml . +``` + +### Using inventory + +By default, Trivy searches for inventory [in the default location](https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html#how-to-build-your-inventory): `/etc/ansible/hosts`. If an `ansible.cfg` file exists at the project root, the inventory path is taken from it. + +To specify a custom inventory source, use the `--ansible-inventory` flag (same as Ansible’s `--inventory`). The flag can be repeated: + +```bash +trivy config --ansible-inventory hosts.ini \ + --ansible-inventory inventory . +``` + +### Passing extra variables + +To pass extra variables, use the `--ansible-extra-vars` flag (same as Ansible’s `--extra-vars`). The flag can be repeated: + +```bash +trivy config --ansible-extra-vars region=us-east-1 \ + --ansible-extra-vars @vars.json . +``` + +### Rendering misconfiguration snippet + +To display the rendered snippet, use the `--render-cause` flag. + +Example output for an S3 bucket task using the `amazon.aws.s3_bucket` module: + +```bash +trivy config --render-cause ansible . +... +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 447 - name: "Hetzner Cloud: Create Object Storage (S3 bucket) {{ hetzner_object_storage_name }}" + 448 ┌ amazon.aws.s3_bucket: + 449 │ endpoint_url: "{{ hetzner_object_storage_endpoint }}" + 450 │ ceph: true + 451 │ aws_access_key: "{{ hetzner_object_storage_access_key }}" + 452 │ aws_secret_key: "{{ hetzner_object_storage_secret_key }}" + 453 │ name: "{{ hetzner_object_storage_name }}" + 454 │ region: "{{ hetzner_object_storage_region }}" + 455 └ requester_pays: false + ... +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +Rendered cause: +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +amazon.aws.s3_bucket: + endpoint_url: https://us-east-1.your-objectstorage.com + ceph: true + aws_access_key: "" + aws_secret_key: "" + name: test-pgcluster-backup + region: us-east-1 + requester_pays: false + state: present + +────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +``` + +## Limitations + +Ansible scanning has several limitations and does not support the following: + +- Resolving remote collections +- Inventory, lookup, and filter plugins (except `dirname`) +- Setting facts (`set_fact`) +- Loops: `loop`, `with_`, etc. +- Patterns in a play’s hosts field +- Host ranges in inventory, e.g., `www[01:50:2].example.com` +- Only supports the following services: AWS S3. If you have other services or clouds that you would like to see support for, please open a discussion in the Trivy project. \ No newline at end of file diff --git a/docs/guide/coverage/iac/azure-arm.md b/docs/guide/coverage/iac/azure-arm.md new file mode 100644 index 000000000000..609c6e0c3055 --- /dev/null +++ b/docs/guide/coverage/iac/azure-arm.md @@ -0,0 +1,33 @@ +# Azure ARM Template +Trivy supports the scanners listed in the table below. + +| Scanner | Supported | +| :----------------: | :-------: | +| [Misconfiguration] | ✓ | +| [Secret] | ✓ | + +It supports the following configurations: + +| Format | Supported | +| :----------: | :-------: | +| ARM template | ✓ | +| Bicep | ✓[^1] | + +To scan Bicep codes, you need to convert them into ARM templates first. + +``` +az bicep build -f main.bicep +or +bicep build main.bicep +``` + +## Misconfiguration +Trivy recursively searches directories and scans all found Azure ARM templates. + +## Secret +The secret scan is performed on plain text files, with no special treatment for Azure ARM templates. + +[Misconfiguration]: ../../scanner/misconfiguration/index.md +[Secret]: ../../scanner/secret.md + +[^1]: Bicep is not natively supported. It needs to be converted into Azure ARM templates. \ No newline at end of file diff --git a/docs/guide/coverage/iac/cloudformation.md b/docs/guide/coverage/iac/cloudformation.md new file mode 100644 index 000000000000..7faea9769d7d --- /dev/null +++ b/docs/guide/coverage/iac/cloudformation.md @@ -0,0 +1,35 @@ +# CloudFormation +Trivy supports the scanners listed in the table below. + +| Scanner | Supported | +|:------------------:|:---------:| +| [Misconfiguration] | ✓ | +| [Secret] | ✓ | + +It supports the following formats. + +| Format | Supported | +|:------:|:---------:| +| JSON | ✓ | +| YAML | ✓ | + +## Misconfiguration +Trivy recursively searches directories and scans all found CloudFormation files. +It evaluates properties, functions, and other elements within CloudFormation files to detect misconfigurations. + +### Value Overrides +You can provide `cf-params` with path to [CloudFormation Parameters] file to Trivy to scan your CloudFormation code with parameters. + +```bash +trivy config --cf-params params.json ./infrastructure/cf +``` + +You can check a [CloudFormation Parameters Example] + +## Secret +The secret scan is performed on plain text files, with no special treatment for CloudFormation. + +[Misconfiguration]: ../../scanner/misconfiguration/index.md +[Secret]: ../../scanner/secret.md +[CloudFormation Parameters]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html +[CloudFormation Parameters Example]: https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cloudformation/deploy.html#supported-json-syntax \ No newline at end of file diff --git a/docs/guide/coverage/iac/docker.md b/docs/guide/coverage/iac/docker.md new file mode 100644 index 000000000000..8b554e623021 --- /dev/null +++ b/docs/guide/coverage/iac/docker.md @@ -0,0 +1,24 @@ +# Docker +Trivy supports the scanners listed in the table below. + +| Scanner | Supported | +| :----------------: | :-------: | +| [Misconfiguration] | ✓ | +| [Secret] | ✓ | + +It supports the following configurations. + +| Config | Supported | +| :-----------: | :-------: | +| Dockerfile | ✓ | +| Containerfile | ✓ | +| Compose | - | + +## Misconfiguration +Trivy recursively searches directories and scans all found Docker files. + +## Secret +The secret scan is performed on plain text files, with no special treatment for Dockerfile. + +[Misconfiguration]: ../../scanner/misconfiguration/index.md +[Secret]: ../../scanner/secret.md \ No newline at end of file diff --git a/docs/guide/coverage/iac/helm.md b/docs/guide/coverage/iac/helm.md new file mode 100644 index 000000000000..0c213e1fd901 --- /dev/null +++ b/docs/guide/coverage/iac/helm.md @@ -0,0 +1,60 @@ +# Helm +Trivy supports two types of Helm scanning, templates and packaged charts. +The following scanners are supported. + +| Format | [Misconfiguration] | [Secret] | +| -------- | :----------------: | :------: | +| Template | ✓ | ✓ | +| Chart | ✓ | - | + +## Misconfiguration +Trivy recursively searches directories and scans all found Helm files. + +It evaluates variables, functions, and other elements within Helm templates and resolve the chart to Kubernetes manifests then run the Kubernetes checks. +See [here](../../scanner/misconfiguration/check/builtin.md) for more details on the built-in checks. + +### Value overrides +There are a number of options for overriding values in Helm charts. +When override values are passed to the Helm scanner, the values will be used during the Manifest rendering process and will become part of the scanned artifact. + +#### Setting inline value overrides +Overrides can be set inline on the command line + +```bash +trivy config --helm-set securityContext.runAsUser=0 ./charts/mySql +``` + +#### Setting value file overrides +Overrides can be in a file that has the key=value set. + +```yaml +# Example override file (overrides.yaml) + +securityContext: + runAsUser: 0 +``` + +```bash +trivy config --helm-values overrides.yaml ./charts/mySql +``` + +#### Setting value as explicit string +the `--helm-set-string` is the same as `--helm-set` but explicitly retains the value as a string + +```bash +trivy config --helm-set-string name=false ./infrastructure/tf +``` + +#### Setting specific values from files +Specific override values can come from specific files + +```bash +trivy config --helm-set-file environment=dev.values.yaml ./charts/mySql +``` + +## Secret +The secret scan is performed on plain text files, with no special treatment for Helm. +Secret scanning is not conducted on the contents of packaged Charts, such as tar or tar.gz. + +[Misconfiguration]: ../../scanner/misconfiguration/index.md +[Secret]: ../../scanner/secret.md \ No newline at end of file diff --git a/docs/guide/coverage/iac/index.md b/docs/guide/coverage/iac/index.md new file mode 100644 index 000000000000..7d2f674e733b --- /dev/null +++ b/docs/guide/coverage/iac/index.md @@ -0,0 +1,26 @@ +# Infrastructure as Code + +## Scanner +Trivy scans Infrastructure as Code (IaC) files for + +- [Misconfigurations][misconf] +- [Secrets][secret] + +## Supported configurations + +| Config type | File patterns | +|-------------------------------------|-----------------------------------------------------| +| [Kubernetes](kubernetes.md) | \*.yml, \*.yaml, \*.json | +| [Docker](docker.md) | Dockerfile, Containerfile | +| [Terraform](terraform.md) | \*.tf, \*.tf.json, \*.tfvars | +| [Terraform Plan](terraform.md) | tfplan, \*.tfplan, \*.json | +| [CloudFormation](cloudformation.md) | \*.yml, \*.yaml, \*.json | +| [Azure ARM Template](azure-arm.md) | \*.json | +| [Helm](helm.md) | \*.yml, \*.yaml, \*.tpl, \*.tar.gz, etc. | +| [YAML][json-and-yaml] | \*.yaml, \*.yml | +| [JSON][json-and-yaml] | \*.json | +| [Ansible](ansible.md) | \*.yml, \*.yaml, \*.json, \*.ini, without extension | + +[misconf]: ../../scanner/misconfiguration/index.md +[secret]: ../../scanner/secret.md +[json-and-yaml]: ../../scanner/misconfiguration/config/config.md#scan-arbitrary-json-and-yaml-configurations diff --git a/docs/guide/coverage/iac/kubernetes.md b/docs/guide/coverage/iac/kubernetes.md new file mode 100644 index 000000000000..61b5163438ca --- /dev/null +++ b/docs/guide/coverage/iac/kubernetes.md @@ -0,0 +1,31 @@ +# Kubernetes +Trivy supports the scanners listed in the table below. + +| Scanner | Supported | +| :----------------: | :-------: | +| [Misconfiguration] | ✓ | +| [Secret] | ✓ | + +In addition to raw YAML and JSON, it supports the following templates: + +| Template | Supported | +| :-------------: | :-------: | +| [Helm](helm.md) | ✓ | +| Kustomize | ✓[^1] | + +!!! note + Trivy does not support Kustomize overlays, so it scans files defined in the base. + Or, you can scan the output of `kustomize build`. + +## Misconfiguration +Trivy recursively searches directories and scans all found Kubernetes files. + +## Secret +The secret scan is performed on plain text files, with no special treatment for Kubernetes. +This means that Base64 encoded secrets are not scanned, and only secrets written in plain text are detected. + + +[Misconfiguration]: ../../scanner/misconfiguration/index.md +[Secret]: ../../scanner/secret.md + +[^1]: Kustomize is not natively supported. \ No newline at end of file diff --git a/docs/guide/coverage/iac/terraform.md b/docs/guide/coverage/iac/terraform.md new file mode 100644 index 000000000000..b7e95a7186a3 --- /dev/null +++ b/docs/guide/coverage/iac/terraform.md @@ -0,0 +1,105 @@ +# Terraform +Trivy supports the scanners listed in the table below. + +| Scanner | Supported | +|:----------------:|:---------:| +| Misconfiguration | ✓ | +| Secret | ✓ | + +It supports the following formats: + +| Format | Supported | +|:-------------:|:---------:| +| JSON | ✓ | +| HCL | ✓ | +| Plan Snapshot | ✓ | +| Plan JSON | ✓ | + +Trivy can scan Terraform Plan files (snapshots) or their JSON representations. To create a Terraform Plan and scan it, run the following command: +```bash +terraform plan --out tfplan +trivy config tfplan +``` + +To scan a Terraform Plan representation in JSON format, run the following command: +```bash +terraform show -json tfplan > tfplan.json +trivy config tfplan.json +``` + +## Misconfiguration +Trivy recursively searches directories and scans all found Terraform files. +It also evaluates variables, imports, and other elements within Terraform files to detect misconfigurations. + +### Value Overrides +You can provide `tf-vars` files to Trivy to override default values specified in the Terraform HCL code. + +```bash +trivy config --tf-vars dev.terraform.tfvars ./infrastructure/tf +``` + +### Exclude Downloaded Terraform Modules +By default, downloaded modules are also scanned. +If you don't want to scan them, you can use the `--tf-exclude-downloaded-modules` flag. + +```bash +trivy config --tf-exclude-downloaded-modules ./configs +``` + +## Secret +The secret scan is performed on plain text files, with no special treatment for Terraform. + +## Limitations + +### Terraform configurations + +#### Data sources and computed attributes + +Trivy relies on static analysis of Terraform configurations. As a result, data sources and +resource attributes whose values are only known after creation or update are not fully supported. + +Terraform evaluates `data` blocks by querying the provider during the `plan` phase. However, +Trivy does not execute provider calls and therefore cannot resolve values that depend on +external or cloud state. Such values are treated as `unknown` during static analysis. + +The same limitation applies to resource attributes marked as computed, whose values become +available only after a resource is created or updated (for example, autogenerated IDs, IP addresses, +or DNS names). + +Because of this, expressions that depend on `data` blocks or computed attributes may: + +- be evaluated as `unknown`, or +- fall back to default values (for example, when using the `try` function), + +which can lead to false positives or false negatives during misconfiguration detection. + +### Terraform Plan JSON + +#### for_each and count in expressions + +The plan created by Terraform does not provide complete information about references in expressions +that use `each` or `count` objects. For this reason, in some situations it is not possible +to establish references between resources that are needed for checks when detecting misconfigurations. +An example of such a configuration is: + +```hcl +locals { + buckets = toset(["test"]) +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.key +} + +resource "aws_s3_bucket_acl" "this" { + for_each = local.buckets + bucket = aws_s3_bucket.this[each.key].id + acl = "private" +} +``` + +With this configuration, the plan will not contain information about which attribute +of the `aws_s3_bucket` resource is referenced by the `aws_s3_bucket_acl` resource. + +See more [here](https://github.com/hashicorp/terraform/issues/30826). \ No newline at end of file diff --git a/docs/guide/coverage/index.md b/docs/guide/coverage/index.md new file mode 100644 index 000000000000..41637ef5a60b --- /dev/null +++ b/docs/guide/coverage/index.md @@ -0,0 +1,9 @@ +# Scanning Coverage +Trivy can detect security issues in many different platforms, languages and configuration files. +This section gives a general overview of that coverage, and can help answer the frequently asked question "Does Trivy support X?". +For more detailed information about the specific platforms and languages, check the relevant documentation. + +- [OS Packages](os/index.md) +- [Language-specific Packages](language/index.md) +- [IaC files](iac/index.md) +- [Kubernetes clusters](./kubernetes.md) \ No newline at end of file diff --git a/docs/guide/coverage/kubernetes.md b/docs/guide/coverage/kubernetes.md new file mode 100644 index 000000000000..9e925ca39900 --- /dev/null +++ b/docs/guide/coverage/kubernetes.md @@ -0,0 +1,24 @@ +# Kubernetes + +When scanning a Kubernetes cluster, Trivy differentiates between the following: + +1. Cluster infrastructure (e.g api-server, kubelet, addons) +1. Cluster configuration (e.g Roles, ClusterRoles). +1. Application workloads (e.g nginx, postgresql). + +Whenever Trivy scans either of these Kubernetes resources, the container image is scanned separately to the Kubernetes resource definition (the YAML manifest) that defines the resource. +When scanning any of the above, the container image is scanned separately to the Kubernetes resource definition (the YAML manifest) that defines the resource. + +Container image is scanned for: + +- Vulnerabilities +- Misconfigurations +- Exposed secrets + +Kubernetes resource definition is scanned for: + +- Vulnerabilities - partially supported through [KBOM scanning](../target/kubernetes.md#kbom) +- Misconfigurations +- Exposed secrets + +To learn more, please see the [documentation for Kubernetes scanning](../target/kubernetes.md). diff --git a/docs/guide/coverage/language/c.md b/docs/guide/coverage/language/c.md new file mode 100644 index 000000000000..25ea0bfe2505 --- /dev/null +++ b/docs/guide/coverage/language/c.md @@ -0,0 +1,35 @@ +# C/C++ + +Trivy supports Conan C/C++ Package Manager ([v1][conanV1] and [v2][conanV2] with limitations). + +The following scanners are supported. + +| Package manager | SBOM | Vulnerability | License | +|-----------------|:----:|:-------------:|:-------:| +| Conan | ✓ | ✓ | ✓[^1] | + +The following table provides an outline of the features Trivy offers. + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | +|-----------------------|----------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:| +| Conan (lockfile v1) | conan.lock[^2] | ✓ | Excluded | ✓ | ✓ | +| Conan (lockfile v2) | conan.lock[^2] | ✓ [^3] | Excluded | - | ✓ | + +## Conan +In order to detect dependencies, Trivy searches for `conan.lock`[^1]. + +[conanV1]: https://docs.conan.io/1/index.html +[conanV2]: https://docs.conan.io/2/ + +### Licenses +The Conan lock file doesn't contain any license information. +To obtain licenses we parse the `conanfile.py` files from the [conan v1 cache directory][conan-v1-cache-dir] and [conan v2 cache directory][conan-v2-cache-dir]. +To correctly detection licenses, ensure that the cache directory contains all dependencies used. + +[conan-v1-cache-dir]: https://docs.conan.io/1/mastering/custom_cache.html +[conan-v2-cache-dir]: https://docs.conan.io/2/reference/environment.html#conan-home +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[^1]: The local cache should contain the dependencies used. See [licenses](#licenses). +[^2]: `conan.lock` is default name. To scan a custom filename use [file-patterns](../../configuration/skipping.md#customizing-file-handling). +[^3]: For `conan.lock` in version 2, indirect dependencies are included in analysis but not flagged explicitly in dependency tree diff --git a/docs/guide/coverage/language/dart.md b/docs/guide/coverage/language/dart.md new file mode 100644 index 000000000000..5e86b6c399f4 --- /dev/null +++ b/docs/guide/coverage/language/dart.md @@ -0,0 +1,53 @@ +# Dart + +Trivy supports [Dart][dart]. + +The following scanners are supported. + +| Package manager | SBOM | Vulnerability | License | +|-------------------------|:----:|:-------------:|:-------:| +| [Dart][dart-repository] | ✓ | ✓ | - | + +The following table provides an outline of the features Trivy offers. + + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | [Detection Priority][detection-priority] | +|-------------------------|--------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|:----------------------------------------:| +| [Dart][dart-repository] | pubspec.lock | ✓ | Included | ✓ | - | ✓ | + +## Dart +In order to detect dependencies, Trivy searches for `pubspec.lock`. + +Trivy marks indirect dependencies, but `pubspec.lock` file doesn't have options to separate root and dev transitive dependencies. +So Trivy includes all dependencies in report. + +### SDK dependencies +Dart uses version `0.0.0` for SDK dependencies (e.g. Flutter). +It is not possible to accurately determine the versions of these dependencies. +Trivy just treats them as `0.0.0`. + +If [--detection-priority comprehensive][detection-priority] is passed, Trivy uses the minimum version of the constraint for the SDK. +For example, in the following case, the version of `flutter` would be `3.3.0`: + +```yaml +flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: "^3.3.0" +``` + +### Dependency tree +To build `dependency tree` Trivy parses [cache directory][cache-directory]. Currently supported default directories and `PUB_CACHE` environment (absolute path only). + +!!! note + Make sure the cache directory contains all the dependencies installed in your application. To download missing dependencies, use `dart pub get` command. + +[dart]: https://dart.dev/ +[dart-repository]: https://pub.dev/ +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[cache-directory]: https://dart.dev/tools/pub/glossary#system-cache +[detection-priority]: ../../scanner/vulnerability.md#detection-priority \ No newline at end of file diff --git a/docs/guide/coverage/language/dotnet.md b/docs/guide/coverage/language/dotnet.md new file mode 100644 index 000000000000..73dc6f4aea5e --- /dev/null +++ b/docs/guide/coverage/language/dotnet.md @@ -0,0 +1,54 @@ +# .NET + +Trivy supports `.NET core` and `NuGet` package managers. + +The following scanners are supported. + +| Artifact | SBOM | Vulnerability | License | +|-----------|:----:|:-------------:|:-------:| +| .Net Core | ✓ | ✓ | - | +| NuGet | ✓ | ✓ | ✓ | + +The following table provides an outline of the features Trivy offers. + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | +|:---------------:|--------------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:| +| .Net Core | *.deps.json | ✓ | Excluded | ✓ | ✓ | +| NuGet | packages.config | ✓ | Excluded | - | - | +| NuGet | *Packages.props | - | Excluded | - | - | +| NuGet | packages.lock.json | ✓ | Included | ✓ | ✓ | + +## *.deps.json +Trivy parses `*.deps.json` files. Trivy currently excludes dev dependencies from the report. + +!!! note + Trivy only includes runtime dependencies in the report. + +## packages.config +Trivy only finds dependency names and versions from `packages.config` files. To build dependency graph, it is better to use `packages.lock.json` files. + +## *Packages.props +Trivy parses `*Packages.props` files. Both legacy `Packages.props` and modern `Directory.Packages.props` are supported. + +### license detection +`packages.config` files don't have information about the licenses used. +Trivy uses [*.nuspec][nuspec] files from [global packages folder][global-packages] to detect licenses. +!!! note + The `licenseUrl` field is [deprecated][license-url]. Trivy doesn't parse this field and only checks the [license] field (license `expression` type only). +Currently only the default path and `NUGET_PACKAGES` environment variable are supported. + +## packages.lock.json +Don't forgot to [enable][enable-lock] lock files in your project. + +!!! tip + Please make sure your lock file is up-to-date after modifying dependencies. + +### license detection +Same as [packages.config](#license-detection) + +[enable-lock]: https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#enabling-the-lock-file +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[nuspec]: https://learn.microsoft.com/en-us/nuget/reference/nuspec +[global-packages]: https://learn.microsoft.com/en-us/nuget/consume-packages/managing-the-global-packages-and-cache-folders +[license]: https://learn.microsoft.com/en-us/nuget/reference/nuspec#license +[license-url]: https://learn.microsoft.com/en-us/nuget/reference/nuspec#licenseurl diff --git a/docs/guide/coverage/language/elixir.md b/docs/guide/coverage/language/elixir.md new file mode 100644 index 000000000000..65dc4298afce --- /dev/null +++ b/docs/guide/coverage/language/elixir.md @@ -0,0 +1,27 @@ +# Elixir + +Trivy supports [Hex][hex] repository for [Elixir][elixir]. + +The following scanners are supported. + +| Package manager | SBOM | Vulnerability | License | +|-----------------| :---: | :-----------: |:-------:| +| [hex][hex] | ✓ | ✓ | - | + +The following table provides an outline of the features Trivy offers. + + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | +|-----------------|--------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:| +| [hex][hex] | mix.lock[^1] | ✓ | Excluded | - | ✓ | + +## Hex +In order to detect dependencies, Trivy searches for `mix.lock`[^1]. + +[Configure](https://hexdocs.pm/mix/Mix.Project.html#module-configuration) your project to use `mix.lock`[^1] file. + +[elixir]: https://elixir-lang.org/ +[hex]: https://hex.pm/ +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[^1]: `mix.lock` is default name. To scan a custom filename use [file-patterns](../../configuration/skipping.md#customizing-file-handling) \ No newline at end of file diff --git a/docs/guide/coverage/language/golang.md b/docs/guide/coverage/language/golang.md new file mode 100644 index 000000000000..81549cba269b --- /dev/null +++ b/docs/guide/coverage/language/golang.md @@ -0,0 +1,134 @@ +# Go + +## Overview +Trivy supports two types of Go scanning, Go Modules and binaries built by Go. + +The following scanners are supported. + +| Artifact | SBOM | Vulnerability | License | +|----------|:----:|:-------------:|:-------------:| +| Modules | ✓ | ✓ | [✓](#license) | +| Binaries | ✓ | ✓ | - | + +The table below provides an outline of the features Trivy offers. + +| Artifact | Offline[^1] | Dev dependencies | [Dependency graph][dependency-graph] | Stdlib | [Detection Priority][detection-priority] | +|----------|:-----------:|:-----------------|:------------------------------------:|:----------------------:|:----------------------------------------:| +| Modules | ✅ | Include | [✅](#dependency-graph) | [✅](#gomod-stdlib) | [✅](#gomod-stdlib) | +| Binaries | ✅ | Exclude | - | [✅](#go-binary-stdlib) | Not needed | + +!!! note + When scanning Go projects (go.mod or binaries built with Go), Trivy scans only dependencies of the project, and does not detect vulnerabilities of application itself. + For example, when scanning the Docker project (Docker's source code with go.mod or the Docker binary), Trivy might find vulnerabilities in Go modules that Docker depends on, but won't find vulnerabilities of Docker itself. Moreover, when scanning the Trivy project, which happens to use Docker, Docker's vulnerabilities might be detected as dependencies of Trivy. + +## Data Sources +The data sources are listed [here](../../scanner/vulnerability.md#langpkg-data-sources). +Trivy uses Go Vulnerability Database for [standard library](https://pkg.go.dev/std) and uses GitHub Advisory Database for other Go modules. + +## Go Module +Depending on Go versions, the required files are different. + +| Version | Required files | Offline | +| ------- | :------------: | :-----: | +| \>=1.17 | go.mod | ✅ | +| <1.17 | go.mod, go.sum | ✅ | + +In Go 1.17+ projects, Trivy uses `go.mod` for direct/indirect dependencies. +On the other hand, it uses `go.mod` for direct dependencies and `go.sum` for indirect dependencies in Go 1.16 or less. + +Go 1.17+ holds actually needed indirect dependencies in `go.mod`, and it reduces false detection. +`go.sum` in Go 1.16 or less contains all indirect dependencies that are even not needed for compiling. +If you want to have better detection, please consider updating the Go version in your project. + +!!! note + The Go version doesn't mean your Go tool version, but the Go version in your go.mod. + + ``` + module github.com/aquasecurity/trivy + + go 1.18 + + require ( + github.com/CycloneDX/cyclonedx-go v0.5.0 + ... + ) + ``` + + To update the Go version in your project, you need to run the following command. + + ``` + $ go mod tidy -go=1.18 + ``` + +### Main Module { #gomod-main } +Trivy scans only dependencies of the project, and does not detect vulnerabilities of the main module. +For example, when scanning the Docker project (Docker's source code with go.mod), Trivy might find vulnerabilities in Go modules that Docker depends on, but won't find vulnerabilities of Docker itself. +Moreover, when scanning the Trivy project, which happens to use Docker, Docker's vulnerabilities might be detected as dependencies of Trivy. + +### Standard Library { #gomod-stdlib } +Detecting the version of Go used in the project can be tricky. +The go.mod file include hints that allows Trivy to guess the Go version but it eventually depends on the Go tool version in the build environment. +Since this strategy is not fully deterministic and accurate, it is enabled only in [--detection-priority comprehensive][detection-priority] mode. +When enabled, Trivy detects stdlib version as the minimum between the `go` and the `toolchain` directives in the `go.mod` file. +To obtain reproducible scan results Trivy doesn't check the locally installed version of `Go`. + +!!! note + Trivy detects `stdlib` only for `Go` 1.21 or higher. + + The version from the `go` line (for `Go` 1.20 or early) is not a minimum required version. + For details, see [this](https://go.googlesource.com/proposal/+/master/design/57001-gotoolchain.md). + +It possibly produces false positives. +See [the caveat](#stdlib-vulnerabilities) for details. + +### License +To identify licenses, you need to download modules to local cache beforehand, such as `go mod download`, `go mod tidy`, `go mod vendor`, etc. +If the `vendor` directory exists, Trivy uses this directory when scanning for license files. +For other cases Trivy traverses `$GOPATH/pkg/mod`dir and collects those extra information. + +### Dependency Graph +Same as licenses, you need to download modules to local cache beforehand. + +## Go Binary +Trivy scans Go binaries when it encounters them during scans such as container images or file systems. +When scanning binaries built by Go, Trivy finds dependencies and Go version information as [embedded in the binary by Go tool at build time](https://tip.golang.org/doc/go1.18#go-version). + +``` +$ trivy rootfs ./your_binary +``` + +!!! note + It doesn't work with UPX-compressed binaries. + +### Main Module +Go binaries installed using the `go install` command contains correct (semver) version for the main module and therefore are detected by Trivy. +In other cases, Go uses the `(devel)` version[^2]. +In this case, Trivy will attempt to parse any `-ldflags` as it's a common practice to pass versions this way. +If the `-ldflags` are not available (e.g., due to the Go `-trimpath` flag), Trivy will attempt to extract the version from the ELF symbol table. +If unsuccessful, the version will be empty[^3]. + +### Standard Library { #go-binary-stdlib } +Trivy detects the Go version used to compile the binary and detects its vulnerabilities in the standard libraries. +It possibly produces false positives. +See [the caveat](#stdlib-vulnerabilities) for details. + +## Caveats + +### Stdlib Vulnerabilities +Trivy does not know if or how you use stdlib functions, therefore it is possible that stdlib vulnerabilities are not applicable to your use case. +There are a few ways to mitigate this: + +1. Analyze vulnerability reachability using a tool such as [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck). This will ensure that reported vulnerabilities are applicable to your project. +2. Suppress non-applicable vulnerabilities using either [ignore file](../../configuration/filtering.md) for self-use or [VEX Hub](../../supply-chain/vex/repo.md) for public use. + +### Empty Version +As described in the [Main Module](#gomod-main) section, the main module of Go binaries might have an empty version. +Also, dependencies replaced with local ones will have an empty version. + +[^1]: It doesn't require the Internet access. +[^2]: See https://github.com/aquasecurity/trivy/issues/1837#issuecomment-1832523477 +[^3]: See https://github.com/golang/go/issues/63432#issuecomment-1751610604 + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[toolchain]: https://go.dev/doc/toolchain +[detection-priority]: ../../scanner/vulnerability.md#detection-priority diff --git a/docs/guide/coverage/language/index.md b/docs/guide/coverage/language/index.md new file mode 100644 index 000000000000..1a35891d5f7d --- /dev/null +++ b/docs/guide/coverage/language/index.md @@ -0,0 +1,71 @@ +# Programming Language + +Trivy supports programming languages for + +- [SBOM][sbom] +- [Vulnerabilities][vuln] +- [Licenses][license] + +## Supported languages +The files analyzed vary depending on the target. +This is because Trivy primarily categorizes targets into two groups: + +- Pre-build +- Post-build + +If the target is a pre-build project, like a code repository, Trivy will analyze files used for building, such as lock files. +On the other hand, when the target is a post-build artifact, like a container image, Trivy will analyze installed package metadata like `.gemspec`, binary files, and so on. + +| Language | File | Image[^4] | Rootfs[^5] | Filesystem[^6] | Repository[^7] | +|----------------------|--------------------------------------------------------------------------------------------|:---------:|:----------:|:--------------:|:--------------:| +| [Ruby](ruby.md) | Gemfile.lock | - | - | ✅ | ✅ | +| | gemspec | ✅ | ✅ | - | - | +| [Python](python.md) | Pipfile.lock | - | - | ✅ | ✅ | +| | poetry.lock | - | - | ✅ | ✅ | +| | uv.lock | - | - | ✅ | ✅ | +| | requirements.txt | - | - | ✅ | ✅ | +| | egg package[^1] | ✅ | ✅ | - | - | +| | wheel package[^2] | ✅ | ✅ | - | - | +| [PHP](php.md) | composer.lock | - | - | ✅ | ✅ | +| | installed.json | ✅ | ✅ | - | - | +| [Node.js](nodejs.md) | package-lock.json | - | - | ✅ | ✅ | +| | yarn.lock | - | - | ✅ | ✅ | +| | pnpm-lock.yaml | - | - | ✅ | ✅ | +| | bun.lock | - | - | ✅ | ✅ | +| | package.json | ✅ | ✅ | - | - | +| [.NET](dotnet.md) | packages.lock.json | ✅ | ✅ | ✅ | ✅ | +| | packages.config | ✅ | ✅ | ✅ | ✅ | +| | .deps.json | ✅ | ✅ | ✅ | ✅ | +| | *Packages.props[^9] | ✅ | ✅ | ✅ | ✅ | +| [Java](java.md) | JAR/WAR/PAR/EAR[^3] | ✅ | ✅ | - | - | +| | pom.xml | - | - | ✅ | ✅ | +| | *gradle.lockfile | - | - | ✅ | ✅ | +| | *.sbt.lock | - | - | ✅ | ✅ | +| [Go](golang.md) | Binaries built by Go | ✅ | ✅ | - | - | +| | go.mod | - | - | ✅ | ✅ | +| [Rust](rust.md) | Cargo.lock | ✅ | ✅ | ✅ | ✅ | +| | Binaries built with [cargo-auditable](https://github.com/rust-secure-code/cargo-auditable) | ✅ | ✅ | - | - | +| [C/C++](c.md) | conan.lock | - | - | ✅ | ✅ | +| [Elixir](elixir.md) | mix.lock[^8] | - | - | ✅ | ✅ | +| [Dart](dart.md) | pubspec.lock | - | - | ✅ | ✅ | +| [Swift](swift.md) | Podfile.lock | - | - | ✅ | ✅ | +| | Package.resolved | - | - | ✅ | ✅ | +| [Julia](julia.md) | Manifest.toml | ✅ | ✅ | ✅ | ✅ | + +The path of these files does not matter. + +Example: [Dockerfile](https://github.com/aquasecurity/trivy-ci-test/blob/main/Dockerfile) + +[sbom]: ../../supply-chain/sbom.md +[vuln]: ../../scanner/vulnerability.md +[license]: ../../scanner/license.md + +[^1]: `*.egg-info`, `*.egg-info/PKG-INFO`, `*.egg` and `EGG-INFO/PKG-INFO` +[^2]: `.dist-info/METADATA` +[^3]: `*.jar`, `*.war`, `*.par` and `*.ear` +[^4]: ✅ means "enabled" and `-` means "disabled" in the image scanning +[^5]: ✅ means "enabled" and `-` means "disabled" in the rootfs scanning +[^6]: ✅ means "enabled" and `-` means "disabled" in the filesystem scanning +[^7]: ✅ means "enabled" and `-` means "disabled" in the git repository scanning +[^8]: To scan a filename other than the default filename use [file-patterns](../../configuration/skipping.md#customizing-file-handling) +[^9]: `Directory.Packages.props` and legacy `Packages.props` file names are supported diff --git a/docs/guide/coverage/language/java.md b/docs/guide/coverage/language/java.md new file mode 100644 index 000000000000..83272f91739e --- /dev/null +++ b/docs/guide/coverage/language/java.md @@ -0,0 +1,140 @@ +# Java +Trivy supports four types of Java scanning: `JAR/WAR/PAR/EAR`, `pom.xml`, `*gradle.lockfile` and `*.sbt.lock` files. + +Each artifact supports the following scanners: + +| Artifact | SBOM | Vulnerability | License | +|------------------|:----:|:-------------:|:-------:| +| JAR/WAR/PAR/EAR | ✓ | ✓ | - | +| pom.xml | ✓ | ✓ | ✓ | +| *gradle.lockfile | ✓ | ✓ | ✓ | +| *.sbt.lock | ✓ | ✓ | - | + +The following table provides an outline of the features Trivy offers. + +| Artifact | Internet access | Dev dependencies | [Dependency graph][dependency-graph] | Position | [Detection Priority][detection-priority] | +|------------------|:---------------------:|:----------------------:|:------------------------------------:|:--------:|:----------------------------------------:| +| JAR/WAR/PAR/EAR | Trivy Java DB | Include | - | - | Not needed | +| pom.xml | Maven repository [^1] | Exclude | ✓ | ✓[^7] | - | +| *gradle.lockfile | - | [Exclude](#gradlelock) | ✓ | ✓ | Not needed | +| *.sbt.lock | - | Exclude | - | ✓ | Not needed | + +These may be enabled or disabled depending on the target. +See [here](./index.md) for the detail. + +## JAR/WAR/PAR/EAR +To find information about your JAR[^2] file, Trivy parses `pom.properties` and `MANIFEST.MF` files in your JAR[^2] file and takes required properties[^3]. + +If those files don't exist or don't contain enough information - Trivy will try to find this JAR[^2] file in [trivy-java-db](https://github.com/aquasecurity/trivy-java-db). +The Java DB will be automatically downloaded/updated when any JAR[^2] file is found. +It is stored in [the cache directory](../../configuration/cache.md#cache-directory). + +!!! warning "EXPERIMENTAL" + Finding JARs in `trivy-java-db` is an experimental function. + +Base JAR[^2] may contain inner JARs[^2] within itself. +To find information about these JARs[^2], the same logic is used as for the base JAR[^2]. + +`table` format only contains the name of root JAR[^2] . To get the full path to inner JARs[^2] use the `json` format. + +## pom.xml +Trivy parses your `pom.xml` file and tries to find files with dependencies from these local locations. + +- project directory[^4] +- relativePath field[^5] +- local repository directory[^6]. + +### remote repositories +If your machine doesn't have the necessary files - Trivy tries to find the information about these dependencies in the remote repositories: + +- [repositories from pom files][maven-pom-repos] +- [maven central repository][maven-central] + +Trivy reproduces Maven's repository selection and priority: + +- for snapshot artifacts: + - check only snapshot repositories from pom files (if exists) +- for other artifacts: + - check release repositories from pom files (if exists) + - check [maven central][maven-central] + +!!! Note + Trivy only takes information about packages. We don't take a list of vulnerabilities for packages from the `maven repository`. + Information about data sources for Java you can see [here](../../scanner/vulnerability.md#langpkg-data-sources). + +You can disable connecting to the maven repository with the `--offline-scan` flag. +The `--offline-scan` flag does not affect the Trivy database. +The vulnerability database will be downloaded anyway. + +!!! Warning + Trivy may skip some dependencies (that were not found on your local machine) when the `--offline-scan` flag is passed. + +### supported scopes +Trivy only scans `import`, `compile`, `runtime` and empty [maven scopes][maven-scopes]. Other scopes and `Optional` dependencies are not currently being analyzed. + +### empty dependency version +There are cases when Trivy cannot determine the version of dependencies: + +- Unable to determine the version from the parent because the parent is not reachable; +- The dependency uses a [hard requirement][version-requirement] with more than one version. + +In these cases, Trivy uses an empty version for the dependency. + +!!! Warning + Trivy doesn't detect child dependencies for dependencies without a version. + +### maven-invoker-plugin +Typically, the integration tests directory (`**/[src|target]/it/*/pom.xml`) of [maven-invoker-plugin][maven-invoker-plugin] doesn't contain actual `pom.xml` files and should be skipped to avoid noise. + +Trivy marks dependencies from these files as the development dependencies and skip them by default. +If you need to show them, use the `--include-dev-deps` flag. + + +## Gradle.lock +`gradle.lock` files only contain information about used dependencies. + +!!!note + All necessary files are checked locally. Gradle file scanning doesn't require internet access. + +By default, Trivy doesn't report development dependencies. +Use the `--include-dev-deps` flag to include them in the results. + +### Dependency-tree +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. +Trivy finds child dependencies from `*.pom` files in the cache[^8] directory. + +But there is no reliable way to determine direct dependencies (even using other files). +Therefore, we mark all dependencies as indirect to use logic to guess direct dependencies and build a dependency tree. + +### Licenses +Trivy also can detect licenses for dependencies. + +Make sure that you have cache[^8] directory to find licenses from `*.pom` dependency files. + + +## SBT + +`build.sbt.lock` files only contain information about used dependencies. This requires a lockfile generated using the +[sbt-dependency-lock][sbt-dependency-lock] plugin. + +!!!note + All necessary files are checked locally. SBT file scanning doesn't require internet access. + +[^1]: Uses maven repository to get information about dependencies. Internet access required. +[^2]: It means `*.jar`, `*.war`, `*.par` and `*.ear` file +[^3]: `ArtifactID`, `GroupID` and `Version` +[^4]: e.g. when parent pom.xml file has `../pom.xml` path +[^5]: When you use dependency path in `relativePath` field in pom.xml file +[^6]: `/Users//.m2/repository` (for Linux and Mac) and `C:/Users//.m2/repository` (for Windows) by default +[^7]: To avoid confusion, Trivy only finds locations for direct dependencies from the base pom.xml file. +[^8]: The supported directories are `$GRADLE_USER_HOME/caches` and `$HOME/.gradle/caches` (`%HOMEPATH%\.gradle\caches` for Windows). + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[maven-invoker-plugin]: https://maven.apache.org/plugins/maven-invoker-plugin/usage.html +[maven-central]: https://repo.maven.apache.org/maven2/ +[maven-pom-repos]: https://maven.apache.org/settings.html#repositories +[maven-scopes]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#Dependency_Scope +[sbt-dependency-lock]: https://stringbean.github.io/sbt-dependency-lock +[detection-priority]: ../../scanner/vulnerability.md#detection-priority +[version-requirement]: https://maven.apache.org/pom.html#dependency-version-requirement-specification diff --git a/docs/guide/coverage/language/julia.md b/docs/guide/coverage/language/julia.md new file mode 100644 index 000000000000..8e8bb17a3081 --- /dev/null +++ b/docs/guide/coverage/language/julia.md @@ -0,0 +1,30 @@ +# Julia + +## Features + +Trivy supports [Pkg.jl](https://pkgdocs.julialang.org/v1/), which is the Julia package manager. +The following scanners are supported. + +| Package manager | SBOM | Vulnerability | License | +|-----------------|:----:|:-------------:|:-------:| +| Pkg.jl | ✓ | ✓ | - | + +The following table provides an outline of the features Trivy offers. + +| Package manager | File | Transitive dependencies | Dev dependencies | License | Dependency graph | Position | +| --------------- | ------------- | :---------------------: | :--------------- | :-----: | :--------------: | :------: | +| Pkg.jl | Manifest.toml | ✅ | Excluded[^1] | - | ✅ | ✅ | + +### Pkg.jl + +Trivy searches for `Manifest.toml` to detect dependencies. + +Trivy also supports dependency trees; however, to display an accurate tree, it needs to know whether each package is a direct dependency of the project. +Since this information is not included in `Manifest.toml`, Trivy parses `Project.toml`, which should be located next to `Project.toml`. +If you want to see the dependency tree, please ensure that `Project.toml` is present. + +Scanning `Manifest.toml` and `Project.toml` together also removes developer dependencies. + +Dependency extensions are currently ignored. + +[^1]: When you scan `Manifest.toml` and `Project.toml` together. diff --git a/docs/guide/coverage/language/nodejs.md b/docs/guide/coverage/language/nodejs.md new file mode 100644 index 000000000000..292b963a591a --- /dev/null +++ b/docs/guide/coverage/language/nodejs.md @@ -0,0 +1,98 @@ +# Node.js + +Trivy supports four types of Node.js package managers: `npm`, `Yarn`, `pnpm` and `Bun`[^1]. + +The following scanners are supported. + +| Artifact | SBOM | Vulnerability | License | +|----------|:----:|:-------------:|:-------:| +| npm | ✓ | ✓ | ✓ | +| Yarn | ✓ | ✓ | ✓ | +| pnpm | ✓ | ✓ | ✓ | +| Bun | ✓ | ✓ | ✓ | + +The following table provides an outline of the features Trivy offers. + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | +|:---------------:|-------------------|:-----------------------:|:---------------------------------:|:------------------------------------:|:--------:| +| npm | package-lock.json | ✓ | [Excluded](#npm) | ✓ | ✓ | +| Yarn | yarn.lock | ✓ | [Excluded](#yarn) | ✓ | ✓ | +| pnpm | pnpm-lock.yaml | ✓ | [Excluded](#lock-file-v9-version) | ✓ | - | +| Bun | bun.lock | ✓ | [Excluded](#bun) | ✓ | ✓ | + +In addition, Trivy scans installed packages with `package.json`. + +| File | Dependency graph | Position | License | +|--------------|:----------------:|:--------:|:-------:| +| package.json | - | - | ✅ | + +These may be enabled or disabled depending on the target. +See [here](./index.md) for the detail. + +## Package managers +Trivy parses your files generated by package managers in filesystem/repository scanning. + +!!! tip + Please make sure your lock file is up-to-date after modifying `package.json`. + +### npm +Trivy parses `package-lock.json`. + +Trivy retrieves dependency license information from the lock file (starting with v2). +If license information is missing, Trivy checks the `package.json` files from the `node_modules` folder. + +By default, Trivy doesn't report development dependencies. Use the `--include-dev-deps` flag to include them. + +### Yarn +Trivy parses `yarn.lock`. + +Trivy also analyzes additional files to gather more information about the detected dependencies. + +- package.json +- node_modules/** + +#### Package relationships +`yarn.lock` files don't contain information about package relationships, such as direct or indirect dependencies. +To enrich this information, Trivy parses the `package.json` file located next to the `yarn.lock` file as well as workspace `package.json` files. + +By default, Trivy doesn't report development dependencies. +Use the `--include-dev-deps` flag to include them in the results. + +#### Development dependencies +`yarn.lock` files don't contain information about package groups, such as production and development dependencies. +To identify dev dependencies and support [aliases][yarn-aliases], Trivy parses the `package.json` file located next to the `yarn.lock` file as well as workspace `package.json` files. + +#### Licenses +Trivy analyzes the `.yarn` directory (for Yarn 2+) or the `node_modules` directory (for Yarn Classic) located next to the `yarn.lock` file to detect licenses. + +### pnpm +Trivy parses `pnpm-lock.yaml`, then finds production dependencies and builds a [tree][dependency-graph] of dependencies with vulnerabilities. +To identify licenses, you need to download dependencies to `node_modules` beforehand. Trivy analyzes `node_modules` for licenses. + +#### lock file v9 version +Trivy supports `Dev` field for `pnpm-lock.yaml` v9 or later. Use the `--include-dev-deps` flag to include the developer's dependencies in the result. + +### Bun +Trivy also supports scanning `bun.lock` file generated by [Bun](https://bun.sh/blog/bun-lock-text-lockfile). +You can use Bun v1.2 which uses this file as default or use `bun install --save-text-lockfile` in Bun v1.1.39 to generate it. + +For previous Bun versions you can use the command `bun install -y` to generate a Yarn-compatible `yarn.lock` and then scan it with Trivy. + +#### Development dependencies +`bun.lock` contains information about package groups, such as production and development dependencies. By default, Trivy doesn't report development dependencies. Use the `--include-dev-deps` flag to include them. + +!!! note + `bun.lockb` is not supported. + +## Packages +Trivy parses the manifest files of installed packages in container image scanning and so on. + +### package.json +Trivy searches for `package.json` files under `node_modules` and identifies installed packages. +It only extracts package names, versions and licenses for those packages. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[pnpm-lockfile-v6]: https://github.com/pnpm/spec/blob/fd3238639af86c09b7032cc942bab3438b497036/lockfile/6.0.md +[yarn-aliases]: https://classic.yarnpkg.com/lang/en/docs/cli/add/#toc-yarn-add-alias + +[^1]: [yarn.lock](#bun) must be generated diff --git a/docs/guide/coverage/language/php.md b/docs/guide/coverage/language/php.md new file mode 100644 index 000000000000..d2c8dbd78e99 --- /dev/null +++ b/docs/guide/coverage/language/php.md @@ -0,0 +1,36 @@ +# PHP + +Trivy supports [Composer][composer], which is a tool for dependency management in PHP. + +The following scanners are supported. + +| Package manager | SBOM | Vulnerability | License | +|-----------------|:----:|:-------------:|:-------:| +| Composer | ✓ | ✓ | ✓ | + +The following table provides an outline of the features Trivy offers. + + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | +|-----------------|----------------|:-----------------------:|:----------------------------------:|:------------------------------------:|:--------:| +| Composer | composer.lock | ✓ | [Excluded](#development-dependencies) | ✓ | ✓ | +| Composer | installed.json | ✓ | Excluded | - | ✓ | + +## composer.lock +In order to detect dependencies, Trivy searches for `composer.lock`. + +Trivy also supports dependency trees; however, to display an accurate tree, it needs to know whether each package is a direct dependency of the project. +Since this information is not included in `composer.lock`, Trivy parses `composer.json`, which should be located next to `composer.lock`. +If you want to see the dependency tree, please ensure that `composer.json` is present. + +### Development dependencies +By default, Trivy doesn't report development dependencies (`packages-dev` in `composer.lock`). +Use the `--include-dev-deps` flag to include them. + +To correctly identify direct development dependencies, Trivy parses `require-dev` from `composer.json`, which should be located next to `composer.lock`. + +## installed.json +Trivy also supports dependency detection for `installed.json` files. By default, you can find this file at `path_to_app/vendor/composer/installed.json`. + +[composer]: https://getcomposer.org/ +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies \ No newline at end of file diff --git a/docs/guide/coverage/language/python.md b/docs/guide/coverage/language/python.md new file mode 100644 index 000000000000..4240098422e2 --- /dev/null +++ b/docs/guide/coverage/language/python.md @@ -0,0 +1,156 @@ +# Python + +Trivy supports the following Python package managers: `pip`, `Pipenv`, `Poetry` and `uv`. +The following scanners are supported for package managers. + +| Package manager | SBOM | Vulnerability | License | +|-----------------|:----:|:-------------:|:-------:| +| pip | ✓ | ✓ | ✓ | +| Pipenv | ✓ | ✓ | - | +| Poetry | ✓ | ✓ | - | +| uv | ✓ | ✓ | - | + +In addition, Trivy supports these formats of Python packages: `egg`, `wheel` and `conda`. +The following scanners are supported for Python packages. + +| Packaging | SBOM | Vulnerability | License | +|-----------|:----:|:-------------:|:-------:| +| Egg | ✓ | ✓ | ✓ | +| Wheel | ✓ | ✓ | ✓ | +| Conda | ✓ | - | - | + + +The following table provides an outline of the features Trivy offers. + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | [Detection Priority][detection-priority] | +|-----------------|------------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|:----------------------------------------:| +| pip | requirements.txt | - | Include | - | ✓ | ✓ | +| Pipenv | Pipfile.lock | ✓ | Include | - | ✓ | Not needed | +| Poetry | poetry.lock | ✓ | [Exclude](#poetry) | ✓ | - | Not needed | +| uv | uv.lock | ✓ | [Exclude](#uv) | ✓ | - | Not needed | | + + +| Packaging | Dependency graph | +| --------- | :--------------: | +| Egg | ✓ | +| Wheel | ✓ | + +These may be enabled or disabled depending on the target. +See [here](./index.md) for the detail. + +## Package managers +Trivy parses your files generated by package managers in filesystem/repository scanning. + +### pip + +#### Dependency detection +By default, Trivy only parses [version specifiers](https://packaging.python.org/en/latest/specifications/version-specifiers/#id5) with `==` comparison operator and without `.*`. + +Using the [--detection-priority comprehensive][detection-priority] option ensures that the tool establishes a minimum version, which is particularly useful in scenarios where identifying the exact version is challenging. +In such case Trivy parses specifiers `>=`,`~=` and a trailing `.*`. + +``` +keyring >= 4.1.1 # Minimum version 4.1.1 +Mopidy-Dirble ~= 1.1 # Minimum version 1.1 +python-gitlab==2.0.* # Minimum version 2.0.0 +``` +Also, there is a way to convert unsupported version specifiers - use either the `pip-compile` tool (which doesn't install the packages) +or call `pip freeze` from the virtual environment where the requirements are already installed. + +```bash +$ cat requirements.txt +boto3~=1.24.60 +click>=8.0 +json-fix==0.5.* +$ pip install -r requirements.txt +... +$ pip freeze > requirements.txt +$ cat requirements.txt +boto3==1.24.96 +botocore==1.27.96 +click==8.1.7 +jmespath==1.0.1 +json-fix==0.5.2 +python-dateutil==2.8.2 +s3transfer==0.6.2 +setuptools==69.0.2 +six==1.16.0 +urllib3==1.26.18 +wheel==0.42.0 +``` + +`requirements.txt` files usually contain only the direct dependencies and not contain the transitive dependencies. +Therefore, Trivy scans only for the direct dependencies with `requirements.txt`. + +To detect transitive dependencies as well, you need to generate `requirements.txt` that contains them. +Like described above, tou can do it with `pip freeze` or `pip-compile`. + +```zsh +$ cat requirements.txt # it will only find `requests@2.28.2`. +requests==2.28.2 +$ pip install -r requirements.txt +... + +$ pip freeze > requirements.txt +$ cat requirements.txt # it will also find the transitive dependencies of `requests@2.28.2`. +certifi==2022.12.7 +charset-normalizer==3.1.0 +idna==3.4 +PyJWT==2.1.0 +requests==2.28.2 +urllib3==1.26.15 +``` + +`pip freeze` also helps to resolve [extras](https://packaging.python.org/en/latest/tutorials/installing-packages/#installing-extras)(optional) dependencies (like `package[extras]=0.0.0`). + +`requirements.txt` files don't contain information about dependencies used for development. +Trivy could detect vulnerabilities on the development packages, which not affect your production environment. + +#### License detection + +`requirements.txt` files don't contain information about licenses. +Therefore, Trivy checks `METADATA` files from `lib/site-packages` directory. + +Trivy uses 3 ways to detect `site-packages` directory: + +- Checks `VIRTUAL_ENV` environment variable. +- Detects path to `python`[^1] binary and checks `../lib/pythonX.Y/site-packages` directory. +- Detects path to `python`[^1] binary and checks `../../lib/site-packages` directory. + +### Pipenv +Trivy parses `Pipfile.lock`. +`Pipfile.lock` files don't contain information about dependencies used for development. +Trivy could detect vulnerabilities on the development packages, which not affect your production environment. + +License detection is not supported for `Pipenv`. + +### Poetry +Trivy uses `poetry.lock` to identify dependencies and find vulnerabilities. +To build the correct dependency graph, `pyproject.toml` also needs to be present next to `poetry.lock`. + +License detection is not supported for `Poetry`. + +By default, Trivy doesn't report development dependencies. Use the `--include-dev-deps` flag to include them. + + +### uv +Trivy uses `uv.lock` to identify dependencies and find vulnerabilities. + +License detection is not supported for `uv`. + +By default, Trivy doesn't report development dependencies. Use the `--include-dev-deps` flag to include them. + +## Packaging +Trivy parses the manifest files of installed packages in container image scanning and so on. +See [here](https://packaging.python.org/en/latest/discussions/package-formats/) for the detail. + +### Egg +Trivy looks for `*.egg-info`, `*.egg-info/METADATA`, `*.egg-info/PKG-INFO`, `*.egg` and `EGG-INFO/PKG-INFO` to identify Python packages. + +### Wheel +Trivy looks for `.dist-info/METADATA` to identify Python packages. + +[^1]: Trivy checks `python`, `python3`, `python2` and `python.exe` file names. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[detection-priority]: ../../scanner/vulnerability.md#detection-priority diff --git a/docs/guide/coverage/language/ruby.md b/docs/guide/coverage/language/ruby.md new file mode 100644 index 000000000000..515680744750 --- /dev/null +++ b/docs/guide/coverage/language/ruby.md @@ -0,0 +1,30 @@ +# Ruby + +Trivy supports [Bundler][bundler] and [RubyGems][rubygems]. +The following scanners are supported for Bundler and RubyGems. + +| Package manager | SBOM | Vulnerability | License | +|-----------------|:----:|:-------------:|:-------:| +| Bundler | ✓ | ✓ | - | +| RubyGems | ✓ | ✓ | ✓ | + + +The following table provides an outline of the features Trivy offers. + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | +|-----------------|--------------|:-----------------------:|:-----------------|:------------------------------------:|:--------:| +| Bundler | Gemfile.lock | ✓ | Included | ✓ | ✓ | +| RubyGems | .gemspec | - | Included | - | - | + + +### Bundler +Trivy searches for `Gemfile.lock` to detect dependencies. + + +### RubyGems +`.gemspec` files doesn't contains transitive dependencies. You need to scan each `.gemspec` file separately. + +[bundler]: https://bundler.io +[rubygems]: https://rubygems.org/ +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + diff --git a/docs/guide/coverage/language/rust.md b/docs/guide/coverage/language/rust.md new file mode 100644 index 000000000000..ddcda798d4cb --- /dev/null +++ b/docs/guide/coverage/language/rust.md @@ -0,0 +1,44 @@ +# Rust + +Trivy supports [Cargo](https://doc.rust-lang.org/stable/cargo/), which is the Rust package manager. +The following scanners are supported for Cargo. + +| Package manager | SBOM | Vulnerability | License | +| --------------- | :---: | :-----------: | :-----: | +| Cargo | ✓ | ✓ | - | + +In addition, it supports binaries built with [cargo-auditable](https://github.com/rust-secure-code/cargo-auditable). + +| Artifact | SBOM | Vulnerability | License | +| -------- | :---: | :-----------: | :-----: | +| Binaries | ✓ | ✓ | - | + +## Features +The following table provides an outline of the features Trivy offers. + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | +|-----------------|------------|:-----------------------:|:-----------------|:------------------------------------:|:--------:| +| Cargo | Cargo.lock | ✓ | Excluded[^1] | ✓ | ✓ | + + +| Artifact | Transitive dependencies | Dev dependencies | Dependency graph | Position | +| -------- | :---------------------: | :--------------- | :--------------: | :------: | +| Binaries | ✓ | Excluded | - | - | + + +### Cargo +Trivy searches for `Cargo.lock` to detect dependencies. + +Trivy also supports dependency trees; however, to display an accurate tree, it needs to know whether each package is a direct dependency of the project. +Since this information is not included in `Cargo.lock`, Trivy parses `Cargo.toml`, which should be located next to `Cargo.lock`. +If you want to see the dependency tree, please ensure that `Cargo.toml` is present. + +Scan `Cargo.lock` and `Cargo.toml` together also removes developer dependencies. + +### Binaries +Trivy scans binaries built with [cargo-auditable](https://github.com/rust-secure-code/cargo-auditable). +If such a binary exists, Trivy will identify it as being built with cargo-audit and scan it. + +[^1]: When you scan Cargo.lock and Cargo.toml together. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies \ No newline at end of file diff --git a/docs/guide/coverage/language/swift.md b/docs/guide/coverage/language/swift.md new file mode 100644 index 000000000000..0245a06b44a3 --- /dev/null +++ b/docs/guide/coverage/language/swift.md @@ -0,0 +1,44 @@ +# Swift + +Trivy supports [CocoaPods][cocoapods] and [Swift][swift] package managers. + +The following scanners are supported. + +| Package manager | SBOM | Vulnerability | License | +|-----------------|:----:|:-------------:|:-------:| +| Swift | ✓ | ✓ | - | +| CocoaPods | ✓ | ✓ | - | + +The following table provides an outline of the features Trivy offers. + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | +|:---------------:|------------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:| +| Swift | Package.resolved | ✓ | Included | - | ✓ | +| CocoaPods | Podfile.lock | ✓ | Included | ✓ | - | + +These may be enabled or disabled depending on the target. +See [here](./index.md) for the detail. + +## Swift +Trivy parses [Package.resolved][package-resolved] file to find dependencies. +Don't forget to update (`swift package update` command) this file before scanning. + +## CocoaPods +CocoaPods uses package names in `PodFile.lock`, but [GitHub Advisory Database (GHSA)][ghsa] Trivy relies on uses Git URLs. +We parse [the CocoaPods Specs][cocoapods-specs] to match package names and links. + +!!! note "Limitation" + Since [GHSA][ghsa] holds only Git URLs, such as github.com/apple/swift-nio, + Trivy can't identify affected submodules, and detect all submodules maintained by the same URL. + For example, [SwiftNIOHTTP1][niohttp1] and [SwiftNIOWebSocket][niowebsocket] both are maintained under `github.com/apple/swift-nio`, + and Trivy detect CVE-2022-3215 for both of them, even though only [SwiftNIOHTTP1][niohttp1] is actually affected. + +[cocoapods]: https://cocoapods.org/ +[cocoapods-specs]: https://github.com/CocoaPods/Specs +[ghsa]: https://github.com/advisories?query=type%3Areviewed+ecosystem%3Aswift +[swift]: https://www.swift.org/package-manager/ +[package-resolved]: https://github.com/apple/swift-package-manager/blob/4a42f2519e3f7b8a731c5ed89b47ed577df8f86c/Documentation/Usage.md#resolving-versions-packageresolved-file +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[niohttp1]: https://cocoapods.org/pods/SwiftNIOHTTP1 +[niowebsocket]: https://cocoapods.org/pods/SwiftNIOWebSocket \ No newline at end of file diff --git a/docs/guide/coverage/os/alma.md b/docs/guide/coverage/os/alma.md new file mode 100644 index 000000000000..00f9e7604c8c --- /dev/null +++ b/docs/guide/coverage/os/alma.md @@ -0,0 +1,74 @@ +# AlmaLinux +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Trivy detects packages that have been installed through package managers such as `dnf` and `yum`. + +## Vulnerability +AlmaLinux offers its own security advisories, and these are utilized when scanning AlmaLinux for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +When looking at fixed versions, it's crucial to consider the patches supplied by AlmaLinux. +For example, for CVE-2023-0464, the fixed version for AlmaLinux 9 is listed as `3.0.7-16.el9_2` in [their advisory][ALSA-2023:3722]. +Note that this is different from the upstream fixed version, which is `3.0.9`, `3.1.1`, and son on. +Typically, only the upstream information gets listed on [NVD], so it's important not to get confused. + +### Severity +Trivy calculates the severity of an issue based on the severity provided by AlmaLinux. +If the severity is not provided or defined yet by AlmaLinux, the severity from the NVD is taken into account. + +Using CVE-2023-0464 as an example, while it is rated as "High" in NVD, AlmaLinux has marked as ["moderate"][ALSA-2023:3722]. +As a result, Trivy will display it as "Medium". + +The table below is the mapping of AlmaLinux's severity to Trivy's severity levels. + +| AlmaLinux | Trivy | +| :-------: | :------: | +| Low | Low | +| Moderate | Medium | +| Important | High | +| Critical | Critical | + +### Status +Trivy supports the following [vulnerability statuses] for AlmaLinux. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | | +| End of Life | | + + +## License +Trivy identifies licenses by examining the metadata of RPM packages. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[errata]: https://errata.almalinux.org/ + +[ALSA-2023:3722]: https://errata.almalinux.org/9/ALSA-2023-3722.html +[NVD]: https://nvd.nist.gov/vuln/detail/CVE-2023-0464 + +[vulnerability statuses]: ../../configuration/filtering.md#by-status \ No newline at end of file diff --git a/docs/guide/coverage/os/alpine.md b/docs/guide/coverage/os/alpine.md new file mode 100644 index 000000000000..936fc1d37b2d --- /dev/null +++ b/docs/guide/coverage/os/alpine.md @@ -0,0 +1,60 @@ +# Alpine Linux +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Trivy detects packages that have been installed through `apk`. + +## Vulnerability +Alpine Linux offers its own security advisories, and these are utilized when scanning Alpine for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +When looking at fixed versions, it's crucial to consider the patches supplied by Alpine. +For example, for CVE-2023-0464, the fixed version for Alpine Linux is listed as `3.1.0-r1` in [the secfixes][CVE-2023-0464]. +Note that this is different from the upstream fixed version, which is `3.1.1`. +Typically, only the upstream information gets listed on [NVD], so it's important not to get confused. + +### Severity +For Alpine vulnerabilities, the severity is determined using the values set by NVD. + +### Status +Trivy supports the following [vulnerability statuses] for Alpine. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | | +| End of Life | | + +## License +Trivy identifies licenses by examining the metadata of APK packages. + + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[secdb]: https://secdb.alpinelinux.org/ + +[CVE-2023-0464]: https://gitlab.alpinelinux.org/alpine/aports/-/blob/dad5b7380ab3be705951ce6fd2d7bba513d6a744/main/openssl/APKBUILD#L36-37 +[NVD]: https://nvd.nist.gov/vuln/detail/CVE-2023-0464 + +[vulnerability statuses]: ../../configuration/filtering.md#by-status \ No newline at end of file diff --git a/docs/guide/coverage/os/amazon.md b/docs/guide/coverage/os/amazon.md new file mode 100644 index 000000000000..27ca8af57f4d --- /dev/null +++ b/docs/guide/coverage/os/amazon.md @@ -0,0 +1,73 @@ +# Amazon Linux +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Trivy detects packages that have been installed through package managers such as `dnf` and `yum`. + +## Vulnerability +Amazon Linux offers its own security advisories, and these are utilized when scanning Amazon Linux for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +When looking at fixed versions, it's crucial to consider the patches supplied by Amazon. +For example, for CVE-2023-0464, the fixed version for Amazon Linux 2023 is listed as `3.0.8-1.amzn2023.0.2` in [ALAS2023-2023-181]. +Note that this is different from the upstream fixed version, which is `3.0.9`, `3.1.1`, and so on. +Typically, only the upstream information gets listed on [NVD], so it's important not to get confused. + +### Severity +Trivy determines vulnerability severity based on the severity metric provided by Amazon. +For example, the security patch for [CVE-2023-0464] in Amazon Linux 2023 is provided as [ALAS2023-2023-181]. +Its severity is rated as "Medium". +Thus, even though it's evaluated as "HIGH" in the NVD, Trivy displays it with a severity of "MEDIUM". + +The table below is the mapping of Amazon's severity to Trivy's severity levels. + +| Amazon | Trivy | +| :-------: | :------: | +| Low | Low | +| Medium | Medium | +| Important | High | +| Critical | Critical | + +### Status +Trivy supports the following [vulnerability statuses] for Amazon Linux. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | | +| End of Life | | + +## License +Trivy identifies licenses by examining the metadata of RPM packages. + + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[center]: https://alas.aws.amazon.com/ + +[CVE-2023-0464]: https://alas.aws.amazon.com/cve/html/CVE-2023-0464.html +[ALAS2023-2023-181]: https://alas.aws.amazon.com/AL2023/ALAS-2023-181.html +[NVD]: https://nvd.nist.gov/vuln/detail/CVE-2023-0464 + +[vulnerability statuses]: ../../configuration/filtering.md#by-status \ No newline at end of file diff --git a/docs/guide/coverage/os/azure.md b/docs/guide/coverage/os/azure.md new file mode 100644 index 000000000000..18025925a952 --- /dev/null +++ b/docs/guide/coverage/os/azure.md @@ -0,0 +1,72 @@ +# Azure Linux (CBL-Mariner) + +*CBL-Mariner was rebranded to Azure Linux for version 3.0 onwards.* + +Trivy supports the following scanners for OS packages. + +| Version | SBOM | Vulnerability | License | +| ---------------- | :---: | :-----------: | :-----: | +| 1.0 | ✔ | ✔ | ✔ | +| 1.0 (Distroless) | ✔ | ✔ | | +| 2.0 | ✔ | ✔ | ✔ | +| 2.0 (Distroless) | ✔ | ✔ | | +| 3.0 | ✔ | ✔ | ✔ | +| 3.0 (Distroless) | ✔ | ✔ | | + + +The following table provides an outline of the targets Trivy supports. + +| Version | Container image | Virtual machine | Arch | +| ------- | :-------------: | :-------------: | :----------: | +| 1.0 | ✔ | ✔ | amd64, arm64 | +| 2.0 | ✔ | ✔ | amd64, arm64 | +| 3.0 | ✔ | ✔ | amd64, arm64 | + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Detect unfixed vulnerabilities | ✓ | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | - | + +## SBOM +Trivy detects packages that have been installed through package managers such as `tdnf`, `dnf` and `yum`. + +## Vulnerability +Azure Linux offers its own security advisories, and these are utilized when scanning Azure Linux for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +Trivy takes fixed versions from [Azure Linux OVAL][oval]. + +### Severity +Trivy calculates the severity of an issue based on the severity provided in [Azure Linux OVAL][oval]. + +### Status +Trivy supports the following [vulnerability statuses] for Azure Linux. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | | +| End of Life | | + + +## License +Trivy identifies licenses by examining the metadata of RPM packages. + +!!! note + License detection is not supported for Azure Linux Distroless images. + + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[oval]: https://github.com/microsoft/AzureLinuxVulnerabilityData/ + +[vulnerability statuses]: ../../configuration/filtering.md#by-status diff --git a/docs/guide/coverage/os/bottlerocket.md b/docs/guide/coverage/os/bottlerocket.md new file mode 100644 index 000000000000..f70f6c33a3f2 --- /dev/null +++ b/docs/guide/coverage/os/bottlerocket.md @@ -0,0 +1,21 @@ +# Bottlerocket +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | - | +| License | - | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| End of life awareness | - | + +## SBOM +Trivy detects packages that are listed in the [software inventory]. + +[software inventory]: https://bottlerocket.dev/en/os/1.37.x/concepts/variants/#software-inventory diff --git a/docs/guide/coverage/os/centos.md b/docs/guide/coverage/os/centos.md new file mode 100644 index 000000000000..1e6bb6b833cf --- /dev/null +++ b/docs/guide/coverage/os/centos.md @@ -0,0 +1,39 @@ +# CentOS +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +| :-----------------------------------: | :-------: | +| Unfixed vulnerabilities | ✓ | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Same as [RHEL](rhel.md#sbom). + +## Vulnerability +CentOS does not provide straightforward machine-readable security advisories. +As a result, Trivy utilizes the security advisories from [Red Hat Enterprise Linux (RHEL)](rhel.md#vulnerability) for detecting vulnerabilities in CentOS. +This approach might lead to situations where, even though Trivy displays a fixed version, CentOS might not have the patch available yet. +Since patches released for RHEL often become available in CentOS after some time, it's usually just a matter of waiting. + +!!! note + The case for CentOS Stream, which is not supported by Trivy, is entirely different from CentOS. + +As Trivy relies on Red Hat's advisories, please refer to [Red Hat](rhel.md) for details regarding vulnerability severity and status. + + +## License +Same as [RHEL](rhel.md#license). + + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies \ No newline at end of file diff --git a/docs/guide/coverage/os/chainguard.md b/docs/guide/coverage/os/chainguard.md new file mode 100644 index 000000000000..e76e5ef3a6e2 --- /dev/null +++ b/docs/guide/coverage/os/chainguard.md @@ -0,0 +1,33 @@ +# Chainguard +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +| :-----------------------------------: | :-------: | +| Detect unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | - | + +## SBOM +Same as [Alpine Linux](alpine.md#sbom). + +## Vulnerability +Chainguard offers its own security advisories, and these are utilized when scanning Chainguard for vulnerabilities. +Everything else is the same as [Alpine Linux](alpine.md#vulnerability). + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +## License +Same as [Alpine Linux](alpine.md#license). + + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[secdb]: https://packages.cgr.dev/chainguard/security.json \ No newline at end of file diff --git a/docs/guide/coverage/os/coreos.md b/docs/guide/coverage/os/coreos.md new file mode 100644 index 000000000000..fff3f0481073 --- /dev/null +++ b/docs/guide/coverage/os/coreos.md @@ -0,0 +1,17 @@ +# CoreOS +This page describes the deprecated `CoreOS Container Linux` (EOL) and its successor, [Fedora CoreOS][fedora-coreos]. + +Trivy supports the following scanners for OS packages on these systems. + +| Scanner | Supported | +|:-------------:|:---------:| +| SBOM | ✓ | +| Vulnerability | - | +| License | - | + +Please see [here](index.md#supported-os) for supported versions. + +## SBOM +Trivy detects packages that are listed in the RPM database. + +[fedora-coreos]: https://fedoraproject.org/coreos/ \ No newline at end of file diff --git a/docs/guide/coverage/os/debian.md b/docs/guide/coverage/os/debian.md new file mode 100644 index 000000000000..4dc553e3838d --- /dev/null +++ b/docs/guide/coverage/os/debian.md @@ -0,0 +1,75 @@ +# Debian +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +| :-----------------------------------: | :-------: | +| Unfixed vulnerabilities | ✓ | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Trivy detects packages that have been installed through package managers such as `apt` and `dpkg`. +While there are some exceptions, like Go binaries and JAR files, it's important to note that binaries that have been custom-built using `make` or tools installed via `curl` are generally not detected. + +## Vulnerability +Debian offers its own security advisories, and these are utilized when scanning Debian for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +When looking at fixed versions, it's crucial to consider the patches supplied by Debian. +For example, for CVE-2023-3269, the fixed version for Debian 12 (bookworm) is listed as `6.1.37-1` in [the Security Tracker][CVE-2023-3269]. +This patch is provided in [DSA-5448-1]. +Note that this is different from the upstream fixed version, which is `6.5`. +Typically, only the upstream information gets listed on [NVD], so it's important not to get confused. + +### Severity +Trivy calculates the severity of an issue based on the 'Urgency' metric found in the Security Tracker. +If 'Urgency' isn't provided by Debian, the severity from the NVD is taken into account. + +Using CVE-2019-15052 as an example, while it is rated as "Critical" in NVD, Debian has marked its "Urgency" as ["Low"][CVE-2019-15052]. +As a result, Trivy will display it as "Low". + +### Status +Trivy supports the following [vulnerability statuses] for Debian. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | ✓ | +| End of Life | ✓ | + +## License +To identify the license of a package, Trivy checks the copyright file located at `/usr/share/doc/*/copyright`. + +However, this method has its limitations as the file isn't machine-readable, leading to situations where the license isn't detected. +In such scenarios, the `--license-full` flag can be passed. +It compares the contents of known licenses with the copyright file to discern the license in question. +Please be aware that using this flag can increase memory usage, so it's disabled by default for efficiency. + + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[debian-tracker]: https://security-tracker.debian.org/tracker/ +[debian-oval]: https://www.debian.org/security/oval/ + +[CVE-2023-3269]: https://security-tracker.debian.org/tracker/CVE-2023-3269 +[CVE-2019-15052]: https://security-tracker.debian.org/tracker/CVE-2019-15052 +[DSA-5448-1]: https://security-tracker.debian.org/tracker/DSA-5448-1 +[NVD]: https://nvd.nist.gov/vuln/detail/CVE-2023-3269 + +[vulnerability statuses]: ../../configuration/filtering.md#by-status \ No newline at end of file diff --git a/docs/guide/coverage/os/echo.md b/docs/guide/coverage/os/echo.md new file mode 100644 index 000000000000..2e79a85d7b65 --- /dev/null +++ b/docs/guide/coverage/os/echo.md @@ -0,0 +1,31 @@ +# Echo +Trivy supports these scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Unfixed vulnerabilities | ✓ | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | - | + +## SBOM +Same as [Debian](debian.md#sbom). + +## Vulnerability +Echo offers its own security advisories, and these are utilized when scanning Echo for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +## License +Same as [Debian](debian.md#license). + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[advisory]: https://advisory.echohq.com/data.json \ No newline at end of file diff --git a/docs/guide/coverage/os/google-distroless.md b/docs/guide/coverage/os/google-distroless.md new file mode 100644 index 000000000000..914fc44df1d8 --- /dev/null +++ b/docs/guide/coverage/os/google-distroless.md @@ -0,0 +1,35 @@ +# Google Distroless Images +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +| :----------------------------------: | :-------: | +| Unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | - | + +## SBOM +Trivy detects packages pre-installed in distroless images. + +## Vulnerability +Google Distroless is based on [Debian]; see there for details. + +## License +Google Distroless is based on [Debian]; see there for details. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[metadata]: https://packages.vmware.com/photon/photon_cve_metadata/ + +[vulnerability statuses]: ../../configuration/filtering.md#by-status + +[Debian]: debian.md \ No newline at end of file diff --git a/docs/guide/coverage/os/index.md b/docs/guide/coverage/os/index.md new file mode 100644 index 000000000000..d5ae866bfc5a --- /dev/null +++ b/docs/guide/coverage/os/index.md @@ -0,0 +1,54 @@ +# OS + +## Scanner +Trivy supports operating systems for + +- [SBOM][sbom] +- [Vulnerabilities][vuln] +- [Licenses][license] + +## Supported OS + +| OS | Supported Versions | Package Managers | +|------------------------------------------------|-------------------------------------|------------------| +| [Alpine Linux](alpine.md) | 2.2 - 2.7, 3.0 - 3.22, edge | apk | +| [Wolfi Linux](wolfi.md) | (n/a) | apk | +| [Chainguard](chainguard.md) | (n/a) | apk | +| [MinimOS](minimos.md) | (n/a) | apk | +| [Red Hat Enterprise Linux](rhel.md) | 6, 7, 8, 9 | dnf/yum/rpm | +| [Red Hat Enterprise Linux](rhel.md) | 10 (SBOM only) | dnf/yum/rpm | +| [CentOS](centos.md)[^1] | 6, 7, 8 | dnf/yum/rpm | +| [AlmaLinux](alma.md) | 8, 9, 10 | dnf/yum/rpm | +| [Rocky Linux](rocky.md) | 8, 9 | dnf/yum/rpm | +| [Oracle Linux](oracle.md) | 5, 6, 7, 8 | dnf/yum/rpm | +| [Azure Linux (CBL-Mariner)](azure.md) | 1.0, 2.0, 3.0 | tdnf/dnf/yum/rpm | +| [Amazon Linux](amazon.md) | 1, 2, 2023 | dnf/yum/rpm | +| [openSUSE Leap](suse.md) | 42, 15 | zypper/rpm | +| [openSUSE Tumbleweed](suse.md) | (n/a) | zypper/rpm | +| [SUSE Linux Enterprise](suse.md) | 11, 12, 15 | zypper/rpm | +| [SUSE Linux Enterprise Micro](suse.md) | 5, 6 | zypper/rpm | +| [Photon OS](photon.md) | 1.0, 2.0, 3.0, 4.0, 5.0 | tndf/yum/rpm | +| [CoreOS](coreos.md)[^3] | All versions (SBOM only) | rpm | +| [Echo](echo.md) | (n/a) | apt/dpkg | +| [Debian GNU/Linux](debian.md) | 7, 8, 9, 10, 11, 12 | apt/dpkg | +| [Ubuntu](ubuntu.md) | All versions supported by Canonical | apt/dpkg | +| [Bottlerocket](bottlerocket.md) | 1.7.0 and upper | bottlerocket | +| [OSs with installed Conda](../others/conda.md) | - | conda | + +## Supported container images + +| Container image | Supported Versions | Package Managers | +|-----------------------------------------------|--------------------|------------------| +| [Google Distroless](google-distroless.md)[^2] | Any | apt/dpkg | +| [Bitnami](../others/bitnami.md) | Any | - | + +Each page gives more details. + +[^1]: CentOS Stream is not supported +[^2]: https://github.com/GoogleContainerTools/distroless +[^3]: Fedora CoreOS and the deprecated CoreOS Container Linux + + +[sbom]: ../../supply-chain/sbom.md +[vuln]: ../../scanner/vulnerability.md +[license]: ../../scanner/license.md diff --git a/docs/guide/coverage/os/minimos.md b/docs/guide/coverage/os/minimos.md new file mode 100644 index 000000000000..e9301aa6cc7e --- /dev/null +++ b/docs/guide/coverage/os/minimos.md @@ -0,0 +1,31 @@ +# MinimOS +Trivy supports these scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Detect unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | - | + +## SBOM +Same as [Alpine Linux](alpine.md#sbom). + +## Vulnerability +MinimOS offers its own security advisories, and these are utilized when scanning MinimOS for vulnerabilities. +Everything else is the same as [Alpine Linux](alpine.md#vulnerability). + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +## License +Same as [Alpine Linux](alpine.md#license). + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies \ No newline at end of file diff --git a/docs/guide/coverage/os/oracle.md b/docs/guide/coverage/os/oracle.md new file mode 100644 index 000000000000..a620e5ee1961 --- /dev/null +++ b/docs/guide/coverage/os/oracle.md @@ -0,0 +1,84 @@ +# Oracle Linux +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +| :-----------------------------------: | :-------: | +| Unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Trivy detects packages that have been installed through package managers such as `dnf` and `yum`. + +## Vulnerability +Oracle Linux offers its own security advisories, and these are utilized when scanning Oracle Linux for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +Trivy takes fixed versions from [Oracle security advisories][alerts]. + +#### Flavors +Trivy detects the flavor for version of the found package and finds vulnerabilities only for that flavor. + +| Flavor | Format | Example | +|:-------:|:------------------------------------:|------------------------------------------------------| +| normal | version without `fips` and `ksplice` | 3.6.16-4.el8 | +| fips | `*_fips` | 10:3.6.16-4.0.1.el8_fips | +| ksplice | `*.ksplice*.*` | 2:2.34-60.0.3.ksplice1.el9_2.7, 151.0.1.ksplice2.el8 | + + +For example Trivy finds [CVE-2021-33560](https://linux.oracle.com/cve/CVE-2021-33560.html) only for the `normal` and `fips` flavors. +For the `ksplice` flavor, [CVE-2021-33560](https://linux.oracle.com/cve/CVE-2021-33560.html) will be skipped. + +### Severity +Trivy determines vulnerability severity based on the severity metric provided in [Oracle security advisories][alerts]. +For example, the security patch for [CVE-2023-0464][CVE-2023-0464] is provided as [ELSA-2023-2645][ELSA-2023-2645]. +Its severity is rated as "MODERATE". +Thus, even though it's evaluated as "HIGH" in the NVD, Trivy displays it with a severity of "MEDIUM". + +The table below is the mapping of Oracle's threat to Trivy's severity levels. + +| Oracle | Trivy | +| :-------: | :------: | +| Low | Low | +| Moderate | Medium | +| Important | High | +| Critical | Critical | + +### Status +Trivy supports the following [vulnerability statuses] for Oracle Linux. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | | +| End of Life | | + +## License +Trivy identifies licenses by examining the metadata of RPM packages. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[oval]: https://linux.oracle.com/security/oval/ +[alerts]: https://www.oracle.com/security-alerts/ + +[CVE-2023-0464]: https://linux.oracle.com/cve/CVE-2023-0464.html +[ELSA-2023-2645]: https://linux.oracle.com/errata/ELSA-2023-2645.html +[NVD]: https://nvd.nist.gov/vuln/detail/CVE-2023-0464 + +[vulnerability statuses]: ../../configuration/filtering.md#by-status \ No newline at end of file diff --git a/docs/guide/coverage/os/photon.md b/docs/guide/coverage/os/photon.md new file mode 100644 index 000000000000..73af9b79ecd1 --- /dev/null +++ b/docs/guide/coverage/os/photon.md @@ -0,0 +1,55 @@ +# Photon OS +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Trivy detects packages that have been installed through package managers such as `tdnf` and `yum`. + +## Vulnerability +Photon OS offers its own security advisories, and these are utilized when scanning Photon OS for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +Trivy takes fixed versions from [Photon CVE metadata][metadata]. + +### Severity +Trivy determines the severity of vulnerabilities based on the CVSSv3 score provided by Photon OS. +See [here](../../scanner/vulnerability.md#severity-selection) for the conversion table from CVSS score to severity. + +### Status +Trivy supports the following [vulnerability statuses] for Photon OS. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | | +| End of Life | | + +## License +Trivy identifies licenses by examining the metadata of RPM packages. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[metadata]: https://packages.broadcom.com/photon/photon_cve_metadata/ + +[vulnerability statuses]: ../../configuration/filtering.md#by-status \ No newline at end of file diff --git a/docs/guide/coverage/os/rhel.md b/docs/guide/coverage/os/rhel.md new file mode 100644 index 000000000000..578acec9ed36 --- /dev/null +++ b/docs/guide/coverage/os/rhel.md @@ -0,0 +1,94 @@ +# Red Hat Enterprise Linux +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +| :----------------------------------: | :-------: | +| Unfixed vulnerabilities | ✓ | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Trivy detects packages that have been installed through package managers such as `dnf` and `yum`. + +## Vulnerability +Red Hat offers its own security advisories, and these are utilized when scanning Red Hat Enterprise Linux (RHEL) for vulnerabilities. + +### Content manifests +Red Hat’s security advisories use CPEs to identify product sets. For example, even packages installed in the same container image can have different CPEs. +For this reason, Red Hat’s container images include stored content manifests, which we convert to CPEs, and perform vulnerability scanning. + +Since this system ties each content manifest to its packages on a per-layer basis, +if layers get merged (for instance, by using `docker run` or `docker export`) we can no longer determine the correct CPE, which may lead to false detection. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +When looking at fixed versions, it's crucial to consider the patches supplied by Red Hat. +For example, for CVE-2023-0464, the fixed version for RHEL 9 is listed as `3.0.7-16.el9_2` in [their advisory][CVE-2023-0464]. +This patch is provided in [RHSA-2023:3722]. +Note that this is different from the upstream fixed version, which is `3.0.9`, `3.1.1`, and so on. +Typically, only the upstream information gets listed on [NVD], so it's important not to get confused. + +### Severity +Trivy calculates the severity of a vulnerability based on the 'Impact' metric provided by Red Hat. +If the impact is not provided or defined yet by Red Hat, the severity from the NVD is taken into account. + +Using CVE-2023-0464 as an example, while it is rated as "HIGH" in NVD, Red Hat has marked its 'Impact' as ["Low"][CVE-2023-0464]. +As a result, Trivy will display it as "Low". + +The table below is the mapping of Red Hat's impact to Trivy's severity levels. + +| Red Hat | Trivy | +| :-------: | :------: | +| Low | Low | +| Moderate | Medium | +| Important | High | +| Critical | Critical | + +### Status +Trivy supports the following [vulnerability statuses] for RHEL. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | ✓ | +| Will Not Fix | ✓ | +| Fix Deferred | ✓ | +| End of Life | ✓ | + +When a vulnerability status is listed as "End of Life", it means a vulnerability with the impact level assigned to this CVE is no longer covered by its current support lifecycle phase. +The product has been identified to contain the impacted component, but analysis to determine whether it is affected or not by this vulnerability was not performed. +Red Hat advises that the product should be assumed to be affected. +Therefore, Trivy detects vulnerabilities with this status as "End of Life". + +On the other hand, for those marked "Under Investigation," the impact is unclear as they are still being examined, so Trivy does not detect them. Once the investigation is completed, the status should be updated. + +!!! abstract + Vulnerabilities with a status of "End of Life", where the presence or absence of impact is unclear, are detected by Trivy. However, those with a status of "Under Investigation" are not detected. + +## License +Trivy identifies licenses by examining the metadata of RPM packages. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[oval]: https://www.redhat.com/security/data/oval/v2/ +[api]: https://www.redhat.com/security/data/metrics/ + +[CVE-2023-0464]: https://access.redhat.com/security/cve/cve-2023-0464 +[RHSA-2023:3722]: https://access.redhat.com/errata/RHSA-2023:3722 +[NVD]: https://nvd.nist.gov/vuln/detail/CVE-2023-0464 + +[vulnerability statuses]: ../../configuration/filtering.md#by-status + +[content-set-default]: https://github.com/aquasecurity/trivy/blob/c80310d7690d8aeb7d3d77416c18c0c8b9aebe17/pkg/detector/ospkg/redhat/redhat.go#L25-L42 diff --git a/docs/guide/coverage/os/rocky.md b/docs/guide/coverage/os/rocky.md new file mode 100644 index 000000000000..2c2c0d10803c --- /dev/null +++ b/docs/guide/coverage/os/rocky.md @@ -0,0 +1,71 @@ +# Rocky Linux +Trivy supports the following scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Trivy detects packages that have been installed through package managers such as `dnf` and `yum`. + +## Vulnerability +Rocky Linux offers its own security advisories, and these are utilized when scanning Rocky Linux for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +Trivy takes fixed versions from [Rocky Linux Errata][errata], not NVD or somewhere else. +See [here](../../scanner/vulnerability.md#data-source-selection) for more details. + +!!! architectures + There are cases when the vulnerability affects packages of not all architectures. + For example, vulnerable packages for [CVE-2023-0361](https://errata.rockylinux.org/RLSA-2023:1141) are only `aarch64` packages. + + Trivy only detects vulnerabilities for packages of your architecture. + +### Severity +Trivy calculates the severity of an issue based on the severity provided in [Rocky Linux Errata][errata]. + +The table below is the mapping of Rocky Linux's severity to Trivy's severity levels. + +| Rocky Linux | Trivy | +| :---------: | :------: | +| Low | Low | +| Moderate | Medium | +| Important | High | +| Critical | Critical | + +### Status +Trivy supports the following [vulnerability statuses] for Rocky Linux. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | | +| End of Life | | + + +## License +Trivy identifies licenses by examining the metadata of RPM packages. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[updateinfo]: https://download.rockylinux.org/pub/rocky/ +[errata]: https://errata.rockylinux.org/ + +[vulnerability statuses]: ../../configuration/filtering.md#by-status \ No newline at end of file diff --git a/docs/guide/coverage/os/suse.md b/docs/guide/coverage/os/suse.md new file mode 100644 index 000000000000..ac4cb2cbb28d --- /dev/null +++ b/docs/guide/coverage/os/suse.md @@ -0,0 +1,43 @@ +# SUSE +Trivy supports the following distributions: + +- openSUSE Leap +- openSUSE Tumbleweed +- SUSE Linux Enterprise (SLE) +- SUSE Linux Enterprise Micro + +Please see [here](index.md#supported-os) for supported versions. + +Trivy supports these scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Trivy detects packages that have been installed through package managers such as `dnf` and `yum`. + +## Vulnerability +SUSE offers its [own security advisories][cvrf], and these are utilized when scanning openSUSE/SLE for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +## License +Trivy identifies licenses by examining the metadata of RPM packages. + + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[cvrf]: https://ftp.suse.com/pub/projects/security/cvrf/ + +[vulnerability statuses]: ../../configuration/filtering.md#by-status diff --git a/docs/guide/coverage/os/ubuntu.md b/docs/guide/coverage/os/ubuntu.md new file mode 100644 index 000000000000..0e53085374d1 --- /dev/null +++ b/docs/guide/coverage/os/ubuntu.md @@ -0,0 +1,66 @@ +# Ubuntu +Trivy supports these scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +Please see [here](index.md#supported-os) for supported versions. + +The following table provides an outline of the features Trivy offers. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Detect unfixed vulnerabilities | ✓ | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | ✓ | + +## SBOM +Same as [Debian](debian.md#sbom). + +## Vulnerability +Ubuntu offers its own security advisories, and these are utilized when scanning Ubuntu for vulnerabilities. + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +### Fixed Version +When looking at fixed versions, it's crucial to consider the patches supplied by Ubuntu. +As an illustration, for CVE-2023-3269, the fixed version for Ubuntu 23.04 (lunar) is listed as `6.2.0-26.26` in [the Security Tracker][CVE-2023-3269]. +It's essential to recognize that this differs from the upstream fixed version, which stands at `6.5`. +Typically, only the upstream information gets listed on [NVD][CVE-2023-3269 NVD], so it's important not to get confused. + +### Severity +Trivy calculates the severity of an issue based on the 'Priority' metric found in the Security Tracker. +If 'Priority' isn't provided by Ubuntu, the severity from the NVD is taken into account. + +Using CVE-2019-15052 as an example, while it is rated as ["Critical" in NVD][CVE-2019-15052 NVD], Ubuntu has marked its "Priority" as ["Medium"][CVE-2019-15052]. +As a result, Trivy will display it as "Medium". + +### Status +Trivy supports the following [vulnerability statuses] for Ubuntu. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | | +| End of Life | | + +## License +Same as [Debian](debian.md#license). + + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[Ubuntu CVE Tracker]: https://ubuntu.com/security/cve + +[CVE-2023-3269]: https://ubuntu.com/security/CVE-2023-3269 +[CVE-2019-15052]: https://ubuntu.com/security/CVE-2019-15052 +[CVE-2023-3269 NVD]: https://nvd.nist.gov/vuln/detail/CVE-2023-3269 +[CVE-2019-15052 NVD]: https://nvd.nist.gov/vuln/detail/CVE-2019-15052 + +[vulnerability statuses]: ../../configuration/filtering.md#by-status \ No newline at end of file diff --git a/docs/guide/coverage/os/wolfi.md b/docs/guide/coverage/os/wolfi.md new file mode 100644 index 000000000000..f2d80449eee7 --- /dev/null +++ b/docs/guide/coverage/os/wolfi.md @@ -0,0 +1,32 @@ +# Wolfi Linux +Trivy supports these scanners for OS packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +|:------------------------------------:|:---------:| +| Detect unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | ✓ | +| End of life awareness | - | + +## SBOM +Same as [Alpine Linux](alpine.md#sbom). + +## Vulnerability +Wolfi Linux offers its own security advisories, and these are utilized when scanning Wolfi for vulnerabilities. +Everything else is the same as [Alpine Linux](alpine.md#vulnerability). + +### Data Source +See [here](../../scanner/vulnerability.md#data-sources). + +## License +Same as [Alpine Linux](alpine.md#license). + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[secdb]: https://packages.wolfi.dev/os/security.json \ No newline at end of file diff --git a/docs/guide/coverage/others/activestate.md b/docs/guide/coverage/others/activestate.md new file mode 100644 index 000000000000..a887a85d28f8 --- /dev/null +++ b/docs/guide/coverage/others/activestate.md @@ -0,0 +1,41 @@ +# ActiveState Images + +While it is not an OS with a package manager, this page describes the details of ActiveState container images. +ActiveState images don't contain OS packages. + +Trivy supports the following scanners for ActiveState images. + +| Scanner | Supported | +|:-------------:|:---------:| +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +## SBOM +Trivy collects packages from two sources: + +- Pre-built SBOM file at `/opt/activestate/.spdx.json` (if present) +- Language-specific packages (e.g., npm, pip, go.mod) + +!!! note + This may result in [duplicates](#duplicates) if both sources contain the same packages. + +## Vulnerability +Trivy detects vulnerabilities in language-specific packages found in the image. + +ActiveState images don't contain OS packages, so vulnerability detection for OS packages is not performed. + +## License +Trivy detects licenses from language-specific packages found in the image. + +## Duplicates +Scan results may contain duplicates when the same packages are detected both from the SBOM file +and by Trivy's analyzers. This is expected behavior. + +To avoid duplicates, you can either: + +- [Skip the SBOM file][skipping] from scanning +- [Filter the results][filtering] to remove duplicates + +[skipping]: ../../configuration/skipping.md +[filtering]: ../../configuration/filtering.md diff --git a/docs/guide/coverage/others/bitnami.md b/docs/guide/coverage/others/bitnami.md new file mode 100644 index 000000000000..4773fd9ab20c --- /dev/null +++ b/docs/guide/coverage/others/bitnami.md @@ -0,0 +1,63 @@ +# Bitnami Images + +!!! warning "EXPERIMENTAL" + Scanning results may be inaccurate. + +While it is not an OS, this page describes the details of the [container images provided by Bitnami](https://github.com/bitnami/containers). +Bitnami images are based on [Debian](../os/debian.md). +Please see [the Debian page](../os/debian.md) for OS packages. + +Trivy supports the following scanners for Bitnami packages. + +| Scanner | Supported | +| :-----------: | :-------: | +| SBOM | ✓ | +| Vulnerability | ✓ | +| License | ✓ | + +The table below outlines the features offered by Trivy. + +| Feature | Supported | +| :----------------------------------: | :-------: | +| Unfixed vulnerabilities | - | +| [Dependency graph][dependency-graph] | - | + +## SBOM +Trivy analyzes the SBOM information contained within the container images provided by Bitnami. +The SBOM files are located at `/opt/bitnami//.spdx-.spdx`. + +## Vulnerability +Since Bitnami has its [own vulnerability database][vulndb], it uses these for vulnerability detection of applications and packages distributed by Bitnami. + +!!! note + Trivy does not support vulnerability detection of independently compiled binaries, so even if you scan container images like `nginx:1.15.2`, vulnerabilities in Nginx cannot be detected. + This is because main applications like Nginx are [not installed by the package manager](https://github.com/nginxinc/docker-nginx/blob/321a13a966eeff945196ddd31a629dad2aa85eda/mainline/debian/Dockerfile). + However, in the case of Bitnami images, since these SBOMs are stored within the image, scanning `bitnami/nginx:1.15.2` allows for the detection of vulnerabilities in Nginx. + +### Fixed Version +Trivy refers to the [Bitnami database][vulndb]. Please note that these may differ from the upstream fixed versions. + +### Severity +Similar to Fixed versions, it follows Bitnami's vulnerability database. + +### Status +Trivy supports the following [vulnerability statuses] for Bitnami packages. + +| Status | Supported | +| :-----------------: | :-------: | +| Fixed | ✓ | +| Affected | ✓ | +| Under Investigation | | +| Will Not Fix | | +| Fix Deferred | | +| End of Life | | + + + +## License +If licenses are included in the SBOM distributed by Bitnami, they will be used for scanning. + +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies + +[vulndb]: https://github.com/bitnami/vulndb +[vulnerability statuses]: ../../configuration/filtering.md#by-status diff --git a/docs/guide/coverage/others/conda.md b/docs/guide/coverage/others/conda.md new file mode 100644 index 000000000000..773bd8097b6d --- /dev/null +++ b/docs/guide/coverage/others/conda.md @@ -0,0 +1,48 @@ +# Conda + +Trivy supports the following scanners for Conda packages. + +| Scanner | Supported | +|:-------------:|:---------:| +| SBOM | ✓ | +| Vulnerability | - | +| License | ✓ | + +| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position | [Detection Priority][detection-priority] | +|-----------------|-----------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|:----------------------------------------:| +| Conda | environment.yml | - | Include | - | ✓ | - | + + +## `.json` +### SBOM +Trivy parses `/envs//conda-meta/.json` files to find the dependencies installed in your env. + +### License +The `.json` files contain package license information. +Trivy includes licenses for the packages it finds without having to parse additional files. + +## `environment.yml`[^1] +### SBOM +Trivy supports parsing [environment.yml][environment.yml][^1] files to find dependency list. + +`environment.yml`[^1] files supports [version range][env-version-range]. We can't be sure about versions for these dependencies. +Therefore, you need to use `conda env export` command to get dependency list in `Conda` default format before scanning `environment.yml`[^1] file. + +!!! note + For dependencies in a non-Conda format, Trivy doesn't include a version of them. + +### License +Trivy parses `conda-meta/.json` files at the [prefix] path. + +To correctly define licenses, make sure your `environment.yml`[^1] contains `prefix` field and `prefix` directory contains `package.json` files. + +!!! note + To get correct `environment.yml`[^1] file and fill `prefix` directory - use `conda env export` command. + +[^1]: Trivy supports both `yaml` and `yml` extensions. + +[environment.yml]: https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#sharing-an-environment +[env-version-range]: https://docs.conda.io/projects/conda-build/en/latest/resources/package-spec.html#examples-of-package-specs +[prefix]: https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#specifying-a-location-for-an-environment +[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies +[detection-priority]: ../../scanner/vulnerability.md#detection-priority diff --git a/docs/guide/coverage/others/index.md b/docs/guide/coverage/others/index.md new file mode 100644 index 000000000000..99a295f05685 --- /dev/null +++ b/docs/guide/coverage/others/index.md @@ -0,0 +1,31 @@ +# Others + +In this section we have placed images, package managers and files that we can't assign to existing sections. + +Trivy supports them for + +- [SBOM][sbom] +- [Vulnerabilities][vuln] +- [Licenses][license] + +## Supported elements + +| Element | File | Image[^1] | Rootfs[^2] | Filesystem[^3] | Repository[^4] | +|--------------------------------|-----------------------------------------------------|:---------:|:----------:|:--------------:|:--------------:| +| [ActiveState images](activestate.md) | `/opt/activestate/.spdx.json` | ✅ | ✅ | - | - | +| [Bitnami packages](bitnami.md) | `/opt/bitnami//.spdx-.spdx` | ✅ | ✅ | - | - | +| [Conda](conda.md) | `/envs//conda-meta/.json` | ✅ | ✅ | - | - | +| | `environment.yml` | - | - | ✅ | ✅ | +| [Root.io images](rootio.md) | - | ✅ | ✅ | - | - | +| [Seal Security](seal.md) | - | ✅ | ✅ | - | - | +| [RPM Archives](rpm.md) | `*.rpm` | ✅[^5] | ✅[^5] | ✅[^5] | ✅[^5] | + +[sbom]: ../../supply-chain/sbom.md +[vuln]: ../../scanner/vulnerability.md +[license]: ../../scanner/license.md + +[^1]: ✅ means "enabled" and `-` means "disabled" in the image scanning +[^2]: ✅ means "enabled" and `-` means "disabled" in the rootfs scanning +[^3]: ✅ means "enabled" and `-` means "disabled" in the filesystem scanning +[^4]: ✅ means "enabled" and `-` means "disabled" in the git repository scanning +[^5]: Only if the `TRIVY_EXPERIMENTAL_RPM_ARCHIVE` env is set. diff --git a/docs/guide/coverage/others/rootio.md b/docs/guide/coverage/others/rootio.md new file mode 100644 index 000000000000..2d36a333933a --- /dev/null +++ b/docs/guide/coverage/others/rootio.md @@ -0,0 +1,23 @@ +# Root.io + +!!! warning "EXPERIMENTAL" + Scanning results may be inaccurate. + +While it is not an OS, this page describes the details of [Root.io](https://root.io/) patch distribution service. +Root.io provides security patches for [Debian](../os/debian.md), [Ubuntu](../os/ubuntu.md), and [Alpine](../os/alpine.md)-based container images. +Root.io patches are detected when Trivy finds packages with specific version suffixes: + +- **Debian/Ubuntu**: packages with `.root.io` in version string +- **Alpine**: packages with `-r\d007\d` pattern in version string (e.g., `-r10071`, `-r20072`) + +When Root.io patches are detected, Trivy automatically switches to Root.io scanning mode for vulnerability detection. +Even when the original OS distributor (Debian, Ubuntu, Alpine) has not provided a patch for a vulnerability, Trivy will display Root.io patches if they are available. + +!!! note + For vulnerabilities, Trivy uses the severity level from the original OS vendor (if the vendor has specified a severity). + +For detailed information about supported scanners, features, and functionality, please refer to the documentation for the underlying OS: + +- [Debian](../os/debian.md) +- [Ubuntu](../os/ubuntu.md) +- [Alpine](../os/alpine.md) \ No newline at end of file diff --git a/docs/guide/coverage/others/rpm.md b/docs/guide/coverage/others/rpm.md new file mode 100644 index 000000000000..092cd4396b07 --- /dev/null +++ b/docs/guide/coverage/others/rpm.md @@ -0,0 +1,42 @@ +# RPM Archives + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy supports the following scanners for RPM archives. + +| Scanner | Supported | +|:-------------:|:---------:| +| SBOM | ✓ | +| Vulnerability | ✓[^1] | +| License | ✓ | + +The table below outlines the features offered by Trivy. + +## SBOM +Trivy analyzes RPM archives matching `*.rpm`. +This feature is currently disabled by default but can be enabled with an environment variable, `TRIVY_EXPERIMENTAL_RPM_ARCHIVE`. + +```shell +TRIVY_EXPERIMENTAL_RPM_ARCHIVE=true trivy fs ./rpms --format cyclonedx --output rpms.cdx.json +``` + +!!! note + Currently, it works with `--format cyclonedx`, `--format spdx` or `--format spdx-json`. + + +## Vulnerability +Since RPM files don't have OS information, you need to generate SBOM, fill in the OS information manually and then scan the SBOM for vulnerabilities. + +For example: + +```shell +$ TRIVY_EXPERIMENTAL_RPM_ARCHIVE=true trivy fs ./rpms -f cyclonedx -o rpms.cdx.json +$ jq '(.components[] | select(.type == "operating-system")) |= (.name = "redhat" | .version = "7.9")' rpms.cdx.json > rpms-res.cdx.json +$ trivy sbom ./rpms-res.cdx.json +``` + +## License +If licenses are included in the RPM archive, Trivy extracts it. + +[^1]: Need to generate SBOM first and add OS information to that SBOM diff --git a/docs/guide/coverage/others/seal.md b/docs/guide/coverage/others/seal.md new file mode 100644 index 000000000000..7131f346237c --- /dev/null +++ b/docs/guide/coverage/others/seal.md @@ -0,0 +1,27 @@ +# Seal Security + +!!! warning "EXPERIMENTAL" + Scanning results may be inaccurate. + +While it is not an OS, this page describes the details of the [Seal Security]( https://sealsecurity.io/) vulnerability feed. +Seal provides security advisories and patched versions for multiple Linux distributions, including [Debian](../os/debian.md), [Ubuntu](../os/ubuntu.md), [Alpine](../os/alpine.md), [Red Hat Enterprise Linux](../os/rhel.md), [CentOS](../os/centos.md), [Oracle Linux](../os/oracle.md), and [Azure Linux (CBL‑Mariner)](../os/azure.md). + +Seal advisories are used when Trivy finds packages that indicate Seal-provided components: + +- Packages whose name or source name starts with `seal-` (for example, `seal-wget`, `seal-zlib`). + +When such Seal packages are detected, Trivy automatically enables Seal scanning for those packages while continuing to use the base OS scanner for the rest. + +!!! note + For vulnerabilities, Trivy prefers severity from the base OS vendor when available. + +For details on supported scanners, features, and behavior for each base OS, refer to their respective pages: + +- [Debian](../os/debian.md) +- [Ubuntu](../os/ubuntu.md) +- [Alpine](../os/alpine.md) +- [Red Hat Enterprise Linux](../os/rhel.md) +- [CentOS](../os/centos.md) +- [Oracle Linux](../os/oracle.md) +- [Azure Linux (CBL‑Mariner)](../os/azure.md) + diff --git a/docs/guide/index.md b/docs/guide/index.md new file mode 100644 index 000000000000..852d4fe34012 --- /dev/null +++ b/docs/guide/index.md @@ -0,0 +1,6 @@ +# User Guide + +Welcome to the Trivy User Guide! +Here you can find complete and thorough information about every aspect of Trivy, how to use it, features available, and configuration options. + +👈 Please use the left side navigation browse the different topics. diff --git a/docs/guide/plugin/developer-guide.md b/docs/guide/plugin/developer-guide.md new file mode 100644 index 000000000000..bf77edee00b0 --- /dev/null +++ b/docs/guide/plugin/developer-guide.md @@ -0,0 +1,213 @@ +# Developer Guide + +## Developing Trivy plugins +This section will guide you through the process of developing Trivy plugins. +To help you get started quickly, we have published a [plugin template repository][plugin-template]. +You can use this template as a starting point for your plugin development. + +### Introduction +If you are looking to start developing plugins for Trivy, read [the user guide](./user-guide.md) first. + +The development process involves the following steps: + +- Create a repository for your plugin, named `trivy-plugin-`. +- Create an executable binary that can be invoked as `trivy `. +- Place the executable binary in a repository. +- Create a `plugin.yaml` file that describes the plugin. +- (Submit your plugin to the [Trivy plugin index][trivy-plugin-index].) + +After you develop a plugin with a good name following the best practices and publish it, you can submit your plugin to the [Trivy plugin index][trivy-plugin-index]. + +### Naming +This section describes guidelines for naming your plugins. + +#### Use `trivy-plugin-` prefix +The name of the plugin repository should be prefixed with `trivy-plugin-`. + +#### Use lowercase and hyphens +Plugin names must be all lowercase and separate words with hyphens. +Don’t use camelCase, PascalCase, or snake_case; use kebab-case. + +- NO: `trivy OpenSvc` +- YES: `trivy open-svc` + +#### Be specific +Plugin names should not be verbs or nouns that are generic, already overloaded, or likely to be used for broader purposes by another plugin. + +- NO: trivy sast (Too broad) +- YES: trivy govulncheck + + +#### Be unique +Find a unique name for your plugin that differentiates it from other plugins that perform a similar function. + +- NO: `trivy images` (Unclear how it is different from the builtin “image" command) +- YES: `trivy registry-images` (Unique name). + +#### Prefix Vendor Identifiers +Use vendor-specific strings as prefix, separated with a dash. +This makes it easier to search/group plugins that are about a specific vendor. + +- NO: `trivy security-hub-aws (Makes it harder to search or locate in a plugin list) +- YES: `trivy aws-security-hub (Will show up together with other aws-* plugins) + +### Choosing a language +Since Trivy plugins are standalone executables, you can write them in any programming language. + +If you are planning to write a plugin with Go, check out [the Report struct](https://github.com/aquasecurity/trivy/blob/787b466e069e2d04e73b3eddbda621e5eec8543b/pkg/types/report.go#L13-L24), +which is the output of Trivy scan. + + +### Writing your plugin +Each plugin has a top-level directory, and then a `plugin.yaml` file. + +```bash +your-plugin/ + | + |- plugin.yaml + |- your-plugin.sh +``` + +In the example above, the plugin is contained inside a directory named `your-plugin`. +It has two files: `plugin.yaml` (required) and an executable script, `your-plugin.sh` (optional). + +#### Writing a plugin manifest +The plugin manifest is a simple YAML file named `plugin.yaml`. +Here is an example YAML of [trivy-plugin-kubectl][trivy-plugin-kubectl] plugin that adds support for Kubernetes scanning. + +```yaml +name: "kubectl" +version: "0.1.0" +repository: github.com/aquasecurity/trivy-plugin-kubectl +maintainer: aquasecurity +output: false +summary: Scan kubectl resources +description: |- + A Trivy plugin that scans the images of a kubernetes resource. + Usage: trivy kubectl TYPE[.VERSION][.GROUP] NAME +platforms: + - selector: # optional + os: darwin + arch: amd64 + uri: ./trivy-kubectl # where the execution file is (local file, http, git, etc.) + bin: ./trivy-kubectl # path to the execution file + - selector: # optional + os: linux + arch: amd64 + uri: https://github.com/aquasecurity/trivy-plugin-kubectl/releases/download/v0.1.0/trivy-kubectl.tar.gz + bin: ./trivy-kubectl +``` + +We encourage you to copy and adapt plugin manifests of existing plugins. + +- [count][trivy-plugin-count] +- [referrer][trivy-plugin-referrer] + +The `plugin.yaml` field should contain the following information: + +- name: The name of the plugin. This also determines how the plugin will be made available in the Trivy CLI. For example, if the plugin is named kubectl, you can call the plugin with `trivy kubectl`. (required) +- version: The version of the plugin. [Semantic Versioning][semver] should be used. (required) +- repository: The repository name where the plugin is hosted. (required) +- maintainer: The name of the maintainer of the plugin. (required) +- output: Whether the plugin supports [the output mode](./user-guide.md#output-mode-support). (optional) +- usage: Deprecated: use summary instead. (optional) +- summary: A short usage description. (required) +- description: A long description of the plugin. This is where you could provide a helpful documentation of your plugin. (required) +- platforms: (required) + - selector: The OS/Architecture specific variations of a execution file. (optional) + - os: OS information based on GOOS (linux, darwin, etc.) (optional) + - arch: The architecture information based on GOARCH (amd64, arm64, etc.) (optional) + - uri: Where the executable file is. Relative path from the root directory of the plugin or remote URL such as HTTP and S3. (required) + - bin: Which file to call when the plugin is executed. Relative path from the root directory of the plugin. (required) + +The following rules will apply in deciding which platform to select: + +- If both `os` and `arch` under `selector` match the current platform, search will stop and the platform will be used. +- If `selector` is not present, the platform will be used. +- If `os` matches and there is no more specific `arch` match, the platform will be used. +- If no `platform` match is found, Trivy will exit with an error. + +After determining platform, Trivy will download the execution file from `uri` and store it in the plugin cache. +When the plugin is called via Trivy CLI, `bin` command will be executed. + +#### Tagging plugin repositories +If you are hosting your plugin in a Git repository, it is strongly recommended to tag your releases with a version number. +By tagging your releases, Trivy can install specific versions of your plugin. + +```bash +$ trivy plugin install referrer@v0.3.0 +``` + +When tagging versions, you must follow [the Semantic Versioning][semver] and prefix the tag with `v`, like `v1.2.3`. + +#### Plugin arguments/flags +The plugin is responsible for handling flags and arguments. +Any arguments are passed to the plugin from the `trivy` command. + +#### Testing plugin installation locally +A plugin should be archived `*.tar.gz`. +After you have archived your plugin into a `.tar.gz` file, you can verify that your plugin installs correctly with Trivy. + +```bash +$ tar -czvf myplugin.tar.gz plugin.yaml script.py +plugin.yaml +script.py + +$ trivy plugin install myplugin.tar.gz +2023-03-03T19:04:42.026+0600 INFO Installing the plugin from myplugin.tar.gz... +2023-03-03T19:04:42.026+0600 INFO Loading the plugin metadata... + +$ trivy myplugin +Hello from Trivy demo plugin! +``` + +## Publishing plugins +The [plugin.yaml](#writing-a-plugin-manifest) file is the core of your plugin, so as long as it is published somewhere, your plugin can be installed. +If you choose to publish your plugin on GitHub, you can make it installable by placing the plugin.yaml file in the root directory of your repository. +Users can then install your plugin with the command, `trivy plugin install github.com/org/repo`. + +While the `uri` specified in the plugin.yaml file doesn't necessarily need to point to the same repository, it's a good practice to host the executable file within the same repository when using GitHub. +You can utilize GitHub Releases to distribute the executable file. +For an example of how to structure your plugin repository, refer to [the plugin template repository][plugin-template]. + +## Distributing plugins via the Trivy plugin index +Trivy can install plugins directly by specifying a repository, like `trivy plugin install github.com/aquasecurity/trivy-plugin-referrer`, +so you don't necessarily need to register your plugin in the Trivy plugin index. +However, we would recommend distributing your plugin via the Trivy plugin index +since it makes it easier for other users to find (`trivy plugin search`) and install your plugin (e.g. `trivy plugin install kubectl`). + +### Pre-submit checklist +- Review [the plugin naming guide](#naming). +- Ensure the `plugin.yaml` file has all the required fields. +- Tag a git release with a semantic version (e.g. v1.0.0). +- [Test your plugin installation locally](#testing-plugin-installation-locally). + +### Submitting plugins +Submitting your plugin to the plugin index is a straightforward process. +All you need to do is create a YAML file for your plugin and place it in the [plugins/](https://github.com/aquasecurity/trivy-plugin-index/tree/main/plugins) directory of [the index repository][trivy-plugin-index]. + +Once you've done that, create a pull request (PR) and have it reviewed by the maintainers. +Once your PR is merged, the index will be updated, and your plugin will be available for installation. +[The plugin index page][plugin-list] will also be automatically updated to list your newly added plugin. + +The content of the YAML file is very simple. +You only need to specify the name of your plugin and the repository where it is distributed. + +```yaml +name: referrer +repository: github.com/aquasecurity/trivy-plugin-referrer +``` + +After your PR is merged, the CI system will automatically retrieve the `plugin.yaml` file from your repository and update [the index.yaml file][index]. +If any required fields are missing from your `plugin.yaml`, the CI will fail, so make sure your `plugin.yaml` has all the required fields before creating a PR. +Once [the index.yaml][index] has been updated, running `trivy plugin update` will download the updated index to your local machine. + + +[plugin-template]: https://github.com/aquasecurity/trivy-plugin-template +[plugin-list]: https://aquasecurity.github.io/trivy-plugin-index/ +[index]: https://aquasecurity.github.io/trivy-plugin-index/v1/index.yaml +[semver]: https://semver.org/ +[trivy-plugin-index]: https://github.com/aquasecurity/trivy-plugin-index +[trivy-plugin-kubectl]: https://github.com/aquasecurity/trivy-plugin-kubectl +[trivy-plugin-count]: https://github.com/aquasecurity/trivy-plugin-count/blob/main/plugin.yaml +[trivy-plugin-referrer]: https://github.com/aquasecurity/trivy-plugin-referrer/blob/main/plugin.yaml diff --git a/docs/guide/plugin/index.md b/docs/guide/plugin/index.md new file mode 100644 index 000000000000..b640dce47b90 --- /dev/null +++ b/docs/guide/plugin/index.md @@ -0,0 +1,70 @@ +# Plugins +Trivy provides a plugin feature to allow others to extend the Trivy CLI without the need to change the Trivy code base. +This plugin system was inspired by the plugin system used in [kubectl][kubectl], [Helm][helm], and [Conftest][conftest]. + +## Overview +Trivy plugins are add-on tools that integrate seamlessly with Trivy. +They provide a way to extend the core feature set of Trivy, but without requiring every new feature to be written in Go and added to the core tool. + +- They can be added and removed from a Trivy installation without impacting the core Trivy tool. +- They can be written in any programming language. +- They integrate with Trivy, and will show up in Trivy help and subcommands. + +!!! warning + Trivy plugins available in public are not audited for security. + You should install and run third-party plugins at your own risk, since they are arbitrary programs running on your machine. + +## Quickstart +Trivy helps you discover and install plugins on your machine. + +You can install and use a wide variety of Trivy plugins to enhance your experience. + +Let’s get started: + +1. Download the plugin list: + + ```bash + $ trivy plugin update + ``` + +2. Discover Trivy plugins available on the plugin index: + + ```bash + $ trivy plugin search + NAME DESCRIPTION MAINTAINER OUTPUT + aqua A plugin for integration with Aqua Security SaaS platform aquasecurity + kubectl A plugin scanning the images of a kubernetes resource aquasecurity + referrer A plugin for OCI referrers aquasecurity ✓ + [...] + ``` + +3. Choose a plugin from the list and install it: + + ```bash + $ trivy plugin install referrer + ``` + +4. Use the installed plugin: + + ```bash + $ trivy referrer --help + ``` + +5. Keep your plugins up-to-date: + + ```bash + $ trivy plugin upgrade + ``` + +6. Uninstall a plugin you no longer use: + + ```bash + trivy plugin uninstall referrer + ``` + +This is practically all you need to know to start using Trivy plugins. + + +[kubectl]: https://kubernetes.io/docs/tasks/extend-kubectl/kubectl-plugins/ +[helm]: https://helm.sh/docs/topics/plugins/ +[conftest]: https://www.conftest.dev/plugins/ diff --git a/docs/guide/plugin/user-guide.md b/docs/guide/plugin/user-guide.md new file mode 100644 index 000000000000..e809f0784735 --- /dev/null +++ b/docs/guide/plugin/user-guide.md @@ -0,0 +1,218 @@ +# User Guide + +## Discovering Plugins +You can find a list of Trivy plugins distributed via trivy-plugin-index [here][trivy-plugin-index]. +However, you can find plugins using the command line as well. + +First, refresh your local copy of the plugin index: + +```bash +$ trivy plugin update +``` + +To list all plugins available, run: + +```bash +$ trivy plugin search +NAME DESCRIPTION MAINTAINER OUTPUT +aqua A plugin for integration with Aqua Security SaaS platform aquasecurity +kubectl A plugin scanning the images of a kubernetes resource aquasecurity +referrer A plugin for OCI referrers aquasecurity ✓ +``` + +You can specify search keywords as arguments: + +```bash +$ trivy plugin search referrer + +NAME DESCRIPTION MAINTAINER OUTPUT +referrer A plugin for OCI referrers aquasecurity ✓ +``` + +It lists plugins with the keyword in the name or description. + +## Installing Plugins +Plugins can be installed with the `trivy plugin install` command: + +```bash +$ trivy plugin install referrer +``` + +This command will download the plugin and install it in the plugin cache. + +Trivy adheres to the XDG specification, so the location depends on whether XDG_DATA_HOME is set. +Trivy will now search XDG_DATA_HOME for the location of the Trivy plugins cache. +The preference order is as follows: + +- XDG_DATA_HOME if set and .trivy/plugins exists within the XDG_DATA_HOME dir +- ~/.trivy/plugins + +Furthermore, it is possible to download plugins that are not registered in the index by specifying the URL directly or by specifying the file path. + +```bash +$ trivy plugin install github.com/aquasecurity/trivy-plugin-kubectl +``` +```bash +$ trivy plugin install https://github.com/aquasecurity/trivy-plugin-kubectl/archive/refs/heads/main.zip +``` +```bash +$ trivy plugin install ./myplugin.tar.gz +``` + +If the plugin's Git repository is [properly tagged](./developer-guide.md#tagging-plugin-repositories), you can specify the version to install like this: + +```bash +$ trivy plugin install referrer@v0.3.0 +``` + +!!! note + The leading `v` in the version is required. Also, the version must follow the [Semantic Versioning](https://semver.org/). + +Under the hood Trivy leverages [go-getter][go-getter] to download plugins. +This means the following protocols are supported for downloading plugins: + +- OCI Registries +- Local Files +- Git +- HTTP/HTTPS +- Mercurial +- Amazon S3 +- Google Cloud Storage + +## Listing Installed Plugins +To list all plugins installed, run: + +```bash +$ trivy plugin list +``` + +## Using Plugins +Once the plugin is installed, Trivy will load all available plugins in the cache on the start of the next Trivy execution. +A plugin will be made in the Trivy CLI based on the plugin name. +To display all plugins, you can list them by `trivy --help` + +```bash +$ trivy --help +NAME: + trivy - A simple and comprehensive vulnerability scanner for containers + +USAGE: + trivy [global options] command [command options] target + +VERSION: + dev + +Scanning Commands + config Scan config files for misconfigurations + filesystem Scan local filesystem + image Scan a container image + +... + +Plugin Commands + kubectl scan kubectl resources + referrer Put referrers to OCI registry +``` + +As shown above, `kubectl` subcommand exists in the `Plugin Commands` section. +To call the kubectl plugin and scan existing Kubernetes deployments, you can execute the following command: + +``` +$ trivy kubectl deployment -- --ignore-unfixed --severity CRITICAL +``` + +Internally the kubectl plugin calls the kubectl binary to fetch information about that deployment and passes the using images to Trivy. +You can see the detail [here][trivy-plugin-kubectl]. + +If you want to omit even the subcommand, you can use `TRIVY_RUN_AS_PLUGIN` environment variable. + +```bash +$ TRIVY_RUN_AS_PLUGIN=kubectl trivy job your-job -- --format json +``` + +## Installing and Running Plugins on the fly +`trivy plugin run` installs a plugin and runs it on the fly. +If the plugin is already present in the cache, the installation is skipped. + +```bash +trivy plugin run kubectl pod your-pod -- --exit-code 1 +``` + +## Upgrading Plugins +To upgrade all plugins that you have installed to their latest versions, run: + +```bash +$ trivy plugin upgrade +``` + +To upgrade only certain plugins, you can explicitly specify their names: + +```bash +$ trivy plugin upgrade +``` + +## Uninstalling Plugins +Specify a plugin name with `trivy plugin uninstall` command. + +```bash +$ trivy plugin uninstall kubectl +``` + +Here's the revised English documentation based on your requested changes: + +## Output Mode Support +While plugins are typically intended to be used as subcommands of Trivy, plugins supporting the output mode can be invoked as part of Trivy's built-in commands. + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy supports plugins that are compatible with the output mode, which process Trivy's output, such as by transforming the output format or sending it elsewhere. +You can determine whether a plugin supports the output mode by checking the `OUTPUT` column in the output of `trivy plugin search` or `trivy plugin list`. + +```bash +$ trivy plugin search +NAME DESCRIPTION MAINTAINER OUTPUT +aqua A plugin for integration with Aqua Security SaaS platform aquasecurity +kubectl A plugin scanning the images of a kubernetes resource aquasecurity +referrer A plugin for OCI referrers aquasecurity ✓ +``` + +In this case, the `referrer` plugin supports the output mode. + +For instance, in the case of image scanning, a plugin supporting the output mode can be called as follows: + +```bash +$ trivy image --format json --output plugin= [--output-plugin-arg ] +``` + +Since scan results are passed to the plugin via standard input, plugins must be capable of handling standard input. + +!!! warning + To avoid Trivy hanging, you need to read all data from `Stdin` before the plugin exits successfully or stops with an error. + +While the example passes JSON to the plugin, other formats like SBOM can also be passed (e.g., `--format cyclonedx`). + +If a plugin requires flags or other arguments, they can be passed using `--output-plugin-arg`. +This is directly forwarded as arguments to the plugin. +For example, `--output plugin=myplugin --output-plugin-arg "--foo --bar=baz"` translates to `myplugin --foo --bar=baz` in execution. + +An example of a plugin supporting the output mode is available [here][trivy-plugin-count]. +It can be used as below: + +```bash +# Install the plugin first +$ trivy plugin install count + +# Call the plugin supporting the output mode in image scanning +$ trivy image --format json --output plugin=count --output-plugin-arg "--published-after 2023-10-01" debian:12 +``` + +## Example + +- [kubectl][trivy-plugin-kubectl] +- [count][trivy-plugin-count] + +[trivy-plugin-index]: https://aquasecurity.github.io/trivy-plugin-index/ +[go-getter]: https://github.com/hashicorp/go-getter +[trivy-plugin-kubectl]: https://github.com/aquasecurity/trivy-plugin-kubectl +[trivy-plugin-count]: https://github.com/aquasecurity/trivy-plugin-count diff --git a/docs/guide/references/abbreviations.md b/docs/guide/references/abbreviations.md new file mode 100644 index 000000000000..7cb097fb70bd --- /dev/null +++ b/docs/guide/references/abbreviations.md @@ -0,0 +1,56 @@ +# Abbreviation List + +This list compiles words that frequently appear in CLI flags or configuration files and are commonly abbreviated in industry and OSS communities. +Trivy may use the abbreviation in place of the full spelling for flag names. +It is also acceptable to add even shorter aliases if needed. + +Words not included in this list should be spelled out in full when used in flags. + +This list is intentionally limited to the most common and widely recognized abbreviations. +Excessive use of abbreviations in CLI flags can hinder initial user understanding and create a steeper learning curve. + +!!! note + This list serves as a guideline rather than a strict requirement. + Its purpose is to maintain consistency across the project when naming flags and configuration options. + While we strive to follow these abbreviations, there may be exceptions where context or clarity demands a different approach. + +## Scope +This list focuses on abbreviations of single words commonly used in technical contexts. It does not include: + +1. Acronyms formed from the initial letters of multiple words (e.g., OS for Operating System, HTTP for Hypertext Transfer Protocol) +2. Domain-specific terminology that already has standardized short forms +3. Brand names or product-specific abbreviations + +The abbreviations listed here are primarily intended for CLI flags, configuration keys, and similar technical interfaces where brevity is valued while maintaining clarity. + +## Example +For a flag containing multiple words, only abbreviate words that appear in this list. +For instance, in `--database-repository`, "database" is in the list so it should be abbreviated to "db", but "repository" is not in the list so it must be spelled out completely. +The correct flag name would be `--db-repository`. +It's acceptable to add a shorter alias like `--db-repo` if desired. + +## List + +| Full Name | Default Abbreviation | Examples | +|-------------------|----------------------|-----------------------------------------------------------| +| application | app | `--app-name`, `--app-mode` | +| authentication | auth | `--auth-method`, `--auth-token` | +| authorization | authz | `--authz-rule`, `--authz-policy` | +| command | cmd | `--cmd-option`, `--cmd-args` | +| configuration | config | `--config`, `--config-dir` | +| database | db | `--db-repository`, `--db-user`, `--db-pass` | +| development | dev | `--dev-dependencies`, `--dev-mode` | +| directory | dir | `--dir-path`, `--output-dir` | +| environment | env | `--env-file`, `--env-vars` | +| information | info | `--info-level`, `--show-info` | +| initialization | init | `--init-script`, `--init-config` | +| library | lib | `--lib-path`, `--lib-dir` | +| maximum | max | `--max-image-size`, `--max-depth` | +| minimum | min | `--min-value`, `--min-severity` | +| misconfiguration | misconfig | `--misconfig-scanners` | +| package | pkg | `--pkg-types` | +| production | prod | `--prod-env`, `--prod-deploy` | +| specification | spec | `--spec-file`, `--spec-version` | +| temporary | tmp | `--tmp-dir`, `--tmp-file` | +| utility | util | `--util-script`, `--util-name` | +| vulnerability | vuln | `--vuln-scan`, `--vuln-report` | \ No newline at end of file diff --git a/docs/guide/references/configuration/cli/trivy.md b/docs/guide/references/configuration/cli/trivy.md new file mode 100644 index 000000000000..9776926b8386 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy.md @@ -0,0 +1,63 @@ +## trivy + +Unified security scanner + +### Synopsis + +Scanner for vulnerabilities in container images, file systems, and Git repositories, as well as for configuration issues and hard-coded secrets + +``` +trivy [global flags] command [flags] target +``` + +### Examples + +``` + # Scan a container image + $ trivy image python:3.4-alpine + + # Scan a container image from a tar archive + $ trivy image --input ruby-3.1.tar + + # Scan local filesystem + $ trivy fs . + + # Run in server mode + $ trivy server +``` + +### Options + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + -f, --format string version format (json) + --generate-default-config write the default config to trivy-default.yaml + -h, --help help for trivy + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy clean](trivy_clean.md) - Remove cached files +* [trivy config](trivy_config.md) - Scan config files for misconfigurations +* [trivy convert](trivy_convert.md) - Convert Trivy JSON report into a different format +* [trivy filesystem](trivy_filesystem.md) - Scan local filesystem +* [trivy image](trivy_image.md) - Scan a container image +* [trivy kubernetes](trivy_kubernetes.md) - [EXPERIMENTAL] Scan kubernetes cluster +* [trivy module](trivy_module.md) - Manage modules +* [trivy plugin](trivy_plugin.md) - Manage plugins +* [trivy registry](trivy_registry.md) - Manage registry authentication +* [trivy repository](trivy_repository.md) - Scan a repository +* [trivy rootfs](trivy_rootfs.md) - Scan rootfs +* [trivy sbom](trivy_sbom.md) - Scan SBOM for vulnerabilities and licenses +* [trivy server](trivy_server.md) - Server mode +* [trivy version](trivy_version.md) - Print the version +* [trivy vex](trivy_vex.md) - [EXPERIMENTAL] VEX utilities +* [trivy vm](trivy_vm.md) - [EXPERIMENTAL] Scan a virtual machine image + diff --git a/docs/guide/references/configuration/cli/trivy_clean.md b/docs/guide/references/configuration/cli/trivy_clean.md new file mode 100644 index 000000000000..4479819a7392 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_clean.md @@ -0,0 +1,52 @@ +## trivy clean + +Remove cached files + +``` +trivy clean [flags] +``` + +### Examples + +``` + # Remove all caches + $ trivy clean --all + + # Remove scan cache + $ trivy clean --scan-cache + + # Remove vulnerability database + $ trivy clean --vuln-db + +``` + +### Options + +``` + -a, --all remove all caches + --checks-bundle remove checks bundle + -h, --help help for clean + --java-db remove Java database + --scan-cache remove scan cache (container and VM image analysis results) + --vex-repo remove VEX repositories + --vuln-db remove vulnerability database +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_config.md b/docs/guide/references/configuration/cli/trivy_config.md new file mode 100644 index 000000000000..738d3df68ed1 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_config.md @@ -0,0 +1,104 @@ +## trivy config + +Scan config files for misconfigurations + +``` +trivy config [flags] DIR +``` + +### Options + +``` + --ansible-extra-vars strings set additional variables as key=value or @file (YAML/JSON) + --ansible-inventory strings specify inventory host path or comma separated host list + --ansible-playbook strings specify playbook file path(s) to scan + --cache-backend string [EXPERIMENTAL] cache backend (e.g. redis://localhost:6379) (default "memory") + --cache-ttl duration cache TTL when using redis as cache backend + --cf-params strings specify paths to override the CloudFormation parameters files + --check-namespaces strings Rego namespaces + --checks-bundle-repository string OCI registry URL to retrieve checks bundle from (default "mirror.gcr.io/aquasec/trivy-checks:2") + --compliance string compliance report to generate + --config-check strings specify the paths to the Rego check files or to the directories containing them, applying config files + --config-data strings specify paths from which data for the Rego checks will be recursively loaded + --config-file-schemas strings specify paths to JSON configuration file schemas to determine that a file matches some configuration and pass the schema to Rego checks for type checking + --disable-telemetry disable sending anonymous usage data to Aqua + --enable-modules strings [EXPERIMENTAL] module names to enable + --exit-code int specify exit code when any security issues are found + --file-patterns strings specify config file patterns + -f, --format string format + Allowed values: + - table + - json + - template + - sarif + - cyclonedx + - spdx + - spdx-json + - github + - cosign-vuln + (default "table") + --helm-api-versions strings Available API versions used for Capabilities.APIVersions. This flag is the same as the api-versions flag of the helm template command. (can specify multiple or separate values with commas: policy/v1/PodDisruptionBudget,apps/v1/Deployment) + --helm-kube-version string Kubernetes version used for Capabilities.KubeVersion. This flag is the same as the kube-version flag of the helm template command. + --helm-set strings specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-set-file strings specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) + --helm-set-string strings specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-values strings specify paths to override the Helm values.yaml files + -h, --help help for config + --ignore-policy string specify the Rego file path to evaluate each vulnerability + --ignorefile string specify .trivyignore file (default ".trivyignore") + --include-deprecated-checks include deprecated checks + --include-non-failures include successes, available with '--scanners misconfig' + --k8s-version string specify k8s version to validate outdated api by it (example: 1.21.0) + --misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot,ansible]) + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") + -o, --output string output file name + --output-plugin-arg string [EXPERIMENTAL] output plugin arguments + --password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons. + --password-stdin password from stdin. Comma-separated passwords are not supported. + --raw-config-scanners strings specify the types of scanners that will also scan raw configurations. For example, scanners will scan a non-adapted configuration into a shared state (allowed values: terraform) + --redis-ca string redis ca file location, if using redis as cache backend + --redis-cert string redis certificate file location, if using redis as cache backend + --redis-key string redis key file location, if using redis as cache backend + --redis-tls enable redis TLS with public certificates, if using redis as cache backend + --registry-token string registry token + --rego-error-limit int maximum number of compile errors allowed during Rego policy evaluation (default 10) + --render-cause strings specify configuration types for which the rendered causes will be shown in the table report (allowed values: terraform,ansible) + --report string specify a compliance report format for the output (allowed values: all,summary) (default "all") + -s, --severity strings severities of security issues to be displayed + Allowed values: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL]) + --skip-check-update skip fetching rego check updates + --skip-dirs strings specify the directories or glob patterns to skip + --skip-files strings specify the files or glob patterns to skip + --skip-version-check suppress notices about version updates and Trivy announcements + --table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed]) + -t, --template string output template + --tf-exclude-downloaded-modules exclude misconfigurations for downloaded terraform modules + --tf-vars strings specify paths to override the Terraform tfvars files + --trace-rego enable more verbose trace output for custom queries + --username strings username. Comma-separated usernames allowed. +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_convert.md b/docs/guide/references/configuration/cli/trivy_convert.md new file mode 100644 index 000000000000..23929921cb6b --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_convert.md @@ -0,0 +1,75 @@ +## trivy convert + +Convert Trivy JSON report into a different format + +``` +trivy convert [flags] RESULT_JSON +``` + +### Examples + +``` + # report conversion + $ trivy image --format json --output result.json debian:11 + $ trivy convert --format cyclonedx --output result.cdx result.json + +``` + +### Options + +``` + --compliance string compliance report to generate + --dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages + --exit-code int specify exit code when any security issues are found + --exit-on-eol int exit with the specified code when the OS reaches end of service/life + -f, --format string format + Allowed values: + - table + - json + - template + - sarif + - cyclonedx + - spdx + - spdx-json + - github + - cosign-vuln + (default "table") + -h, --help help for convert + --ignore-policy string specify the Rego file path to evaluate each vulnerability + --ignorefile string specify .trivyignore file (default ".trivyignore") + --list-all-pkgs output all packages in the JSON report regardless of vulnerability (default true) + -o, --output string output file name + --output-plugin-arg string [EXPERIMENTAL] output plugin arguments + --report string specify a report format for the output (allowed values: all,summary) (default "all") + --scanners strings List of scanners included when generating the json report. Used only for rendering the summary table. (allowed values: vuln,misconfig,secret,license) + -s, --severity strings severities of security issues to be displayed + Allowed values: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL]) + --show-suppressed [EXPERIMENTAL] show suppressed vulnerabilities + --table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed]) + -t, --template string output template +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_filesystem.md b/docs/guide/references/configuration/cli/trivy_filesystem.md new file mode 100644 index 000000000000..579157a73cbf --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_filesystem.md @@ -0,0 +1,199 @@ +## trivy filesystem + +Scan local filesystem + +``` +trivy filesystem [flags] PATH +``` + +### Examples + +``` + # Scan a local project including language-specific files + $ trivy fs /path/to/your_project + + # Scan a single file + $ trivy fs ./trivy-ci-test/Pipfile.lock +``` + +### Options + +``` + --ansible-extra-vars strings set additional variables as key=value or @file (YAML/JSON) + --ansible-inventory strings specify inventory host path or comma separated host list + --ansible-playbook strings specify playbook file path(s) to scan + --cache-backend string [EXPERIMENTAL] cache backend (e.g. redis://localhost:6379) (default "memory") + --cache-ttl duration cache TTL when using redis as cache backend + --cf-params strings specify paths to override the CloudFormation parameters files + --check-namespaces strings Rego namespaces + --checks-bundle-repository string OCI registry URL to retrieve checks bundle from (default "mirror.gcr.io/aquasec/trivy-checks:2") + --compliance string compliance report to generate + --config-check strings specify the paths to the Rego check files or to the directories containing them, applying config files + --config-data strings specify paths from which data for the Rego checks will be recursively loaded + --config-file-schemas strings specify paths to JSON configuration file schemas to determine that a file matches some configuration and pass the schema to Rego checks for type checking + --custom-headers strings custom headers in client mode + --db-repository strings OCI repository(ies) to retrieve trivy-db in order of priority (default [mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2]) + --dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages + --detection-priority string specify the detection priority: + - "precise": Prioritizes precise by minimizing false positives. + - "comprehensive": Aims to detect more security findings at the cost of potential false positives. + (allowed values: precise,comprehensive) (default "precise") + --disable-telemetry disable sending anonymous usage data to Aqua + --distro string [EXPERIMENTAL] specify a distribution, / + --download-db-only download/update vulnerability database but don't run a scan + --download-java-db-only download/update Java index database but don't run a scan + --enable-modules strings [EXPERIMENTAL] module names to enable + --exit-code int specify exit code when any security issues are found + --file-patterns strings specify config file patterns + -f, --format string format + Allowed values: + - table + - json + - template + - sarif + - cyclonedx + - spdx + - spdx-json + - github + - cosign-vuln + (default "table") + --helm-api-versions strings Available API versions used for Capabilities.APIVersions. This flag is the same as the api-versions flag of the helm template command. (can specify multiple or separate values with commas: policy/v1/PodDisruptionBudget,apps/v1/Deployment) + --helm-kube-version string Kubernetes version used for Capabilities.KubeVersion. This flag is the same as the kube-version flag of the helm template command. + --helm-set strings specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-set-file strings specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) + --helm-set-string strings specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-values strings specify paths to override the Helm values.yaml files + -h, --help help for filesystem + --ignore-policy string specify the Rego file path to evaluate each vulnerability + --ignore-status strings comma-separated list of vulnerability status to ignore + Allowed values: + - unknown + - not_affected + - affected + - fixed + - under_investigation + - will_not_fix + - fix_deferred + - end_of_life + --ignore-unfixed display only fixed vulnerabilities + --ignored-licenses strings specify a list of license to ignore + --ignorefile string specify .trivyignore file (default ".trivyignore") + --include-deprecated-checks include deprecated checks + --include-dev-deps include development dependencies in the report (supported: npm, yarn, gradle) + --include-non-failures include successes, available with '--scanners misconfig' + --java-db-repository strings OCI repository(ies) to retrieve trivy-java-db in order of priority (default [mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1]) + --license-confidence-level float specify license classifier's confidence level (default 0.9) + --license-full eagerly look for licenses in source code headers and license files + --list-all-pkgs output all packages in the JSON report regardless of vulnerability (default true) + --misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot,ansible]) + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") + --no-progress suppress progress bar + --offline-scan do not issue API requests to identify dependencies + -o, --output string output file name + --output-plugin-arg string [EXPERIMENTAL] output plugin arguments + --parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5) + --password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons. + --password-stdin password from stdin. Comma-separated passwords are not supported. + --pkg-relationships strings list of package relationships + Allowed values: + - unknown + - root + - workspace + - direct + - indirect + (default [unknown,root,workspace,direct,indirect]) + --pkg-types strings list of package types (allowed values: os,library) (default [os,library]) + --raw-config-scanners strings specify the types of scanners that will also scan raw configurations. For example, scanners will scan a non-adapted configuration into a shared state (allowed values: terraform) + --redis-ca string redis ca file location, if using redis as cache backend + --redis-cert string redis certificate file location, if using redis as cache backend + --redis-key string redis key file location, if using redis as cache backend + --redis-tls enable redis TLS with public certificates, if using redis as cache backend + --registry-token string registry token + --rego-error-limit int maximum number of compile errors allowed during Rego policy evaluation (default 10) + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --render-cause strings specify configuration types for which the rendered causes will be shown in the table report (allowed values: terraform,ansible) + --report string specify a compliance report format for the output (allowed values: all,summary) (default "all") + --sbom-sources strings [EXPERIMENTAL] try to retrieve SBOM from the specified sources (allowed values: oci,rekor) + --scanners strings comma-separated list of what security issues to detect (allowed values: vuln,misconfig,secret,license) (default [vuln,secret]) + --secret-config string specify a path to config file for secret scanning (default "trivy-secret.yaml") + --server string server address in client mode + -s, --severity strings severities of security issues to be displayed + Allowed values: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL]) + --show-suppressed [EXPERIMENTAL] show suppressed vulnerabilities + --skip-check-update skip fetching rego check updates + --skip-db-update skip updating vulnerability database + --skip-dirs strings specify the directories or glob patterns to skip + --skip-files strings specify the files or glob patterns to skip + --skip-java-db-update skip updating Java index database + --skip-version-check suppress notices about version updates and Trivy announcements + --skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update + --table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed]) + -t, --template string output template + --tf-exclude-downloaded-modules exclude misconfigurations for downloaded terraform modules + --tf-vars strings specify paths to override the Terraform tfvars files + --token string for authentication in client/server mode + --token-header string specify a header name for token in client/server mode (default "Trivy-Token") + --trace-rego enable more verbose trace output for custom queries + --username strings username. Comma-separated usernames allowed. + --vex strings [EXPERIMENTAL] VEX sources ("repo", "oci" or file path) + --vuln-severity-source strings order of data sources for selecting vulnerability severity level + Allowed values: + - nvd + - redhat + - redhat-oval + - debian + - ubuntu + - alpine + - amazon + - oracle-oval + - suse-cvrf + - photon + - arch-linux + - alma + - rocky + - cbl-mariner + - azure + - ruby-advisory-db + - php-security-advisories + - nodejs-security-wg + - ghsa + - glad + - aqua + - osv + - k8s + - wolfi + - chainguard + - bitnami + - govulndb + - julia + - echo + - minimos + - rootio + - auto + (default [auto]) +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_image.md b/docs/guide/references/configuration/cli/trivy_image.md new file mode 100644 index 000000000000..579a6d97116d --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_image.md @@ -0,0 +1,220 @@ +## trivy image + +Scan a container image + +``` +trivy image [flags] IMAGE_NAME +``` + +### Examples + +``` + # Scan a container image + $ trivy image python:3.4-alpine + + # Scan a container image from a tar archive + $ trivy image --input ruby-3.1.tar + + # Filter by severities + $ trivy image --severity HIGH,CRITICAL alpine:3.15 + + # Ignore unfixed/unpatched vulnerabilities + $ trivy image --ignore-unfixed alpine:3.15 + + # Scan a container image in client mode + $ trivy image --server http://127.0.0.1:4954 alpine:latest + + # Generate json result + $ trivy image --format json --output result.json alpine:3.15 + + # Generate a report in the CycloneDX format + $ trivy image --format cyclonedx --output result.cdx alpine:3.15 +``` + +### Options + +``` + --ansible-extra-vars strings set additional variables as key=value or @file (YAML/JSON) + --ansible-inventory strings specify inventory host path or comma separated host list + --ansible-playbook strings specify playbook file path(s) to scan + --cache-backend string [EXPERIMENTAL] cache backend (e.g. redis://localhost:6379) (default "fs") + --cache-ttl duration cache TTL when using redis as cache backend + --check-namespaces strings Rego namespaces + --checks-bundle-repository string OCI registry URL to retrieve checks bundle from (default "mirror.gcr.io/aquasec/trivy-checks:2") + --compliance string compliance report to generate (built-in compliance's: docker-cis-1.6.0) + --config-check strings specify the paths to the Rego check files or to the directories containing them, applying config files + --config-data strings specify paths from which data for the Rego checks will be recursively loaded + --config-file-schemas strings specify paths to JSON configuration file schemas to determine that a file matches some configuration and pass the schema to Rego checks for type checking + --custom-headers strings custom headers in client mode + --db-repository strings OCI repository(ies) to retrieve trivy-db in order of priority (default [mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2]) + --dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages + --detection-priority string specify the detection priority: + - "precise": Prioritizes precise by minimizing false positives. + - "comprehensive": Aims to detect more security findings at the cost of potential false positives. + (allowed values: precise,comprehensive) (default "precise") + --disable-telemetry disable sending anonymous usage data to Aqua + --distro string [EXPERIMENTAL] specify a distribution, / + --docker-host string unix domain socket path to use for docker scanning + --download-db-only download/update vulnerability database but don't run a scan + --download-java-db-only download/update Java index database but don't run a scan + --enable-modules strings [EXPERIMENTAL] module names to enable + --exit-code int specify exit code when any security issues are found + --exit-on-eol int exit with the specified code when the OS reaches end of service/life + --file-patterns strings specify config file patterns + -f, --format string format + Allowed values: + - table + - json + - template + - sarif + - cyclonedx + - spdx + - spdx-json + - github + - cosign-vuln + (default "table") + --helm-api-versions strings Available API versions used for Capabilities.APIVersions. This flag is the same as the api-versions flag of the helm template command. (can specify multiple or separate values with commas: policy/v1/PodDisruptionBudget,apps/v1/Deployment) + --helm-kube-version string Kubernetes version used for Capabilities.KubeVersion. This flag is the same as the kube-version flag of the helm template command. + --helm-set strings specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-set-file strings specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) + --helm-set-string strings specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-values strings specify paths to override the Helm values.yaml files + -h, --help help for image + --ignore-policy string specify the Rego file path to evaluate each vulnerability + --ignore-status strings comma-separated list of vulnerability status to ignore + Allowed values: + - unknown + - not_affected + - affected + - fixed + - under_investigation + - will_not_fix + - fix_deferred + - end_of_life + --ignore-unfixed display only fixed vulnerabilities + --ignored-licenses strings specify a list of license to ignore + --ignorefile string specify .trivyignore file (default ".trivyignore") + --image-config-scanners strings comma-separated list of what security issues to detect on container image configurations (allowed values: misconfig,secret) + --image-src strings image source(s) to use, in priority order (allowed values: docker,containerd,podman,remote) (default [docker,containerd,podman,remote]) + --include-deprecated-checks include deprecated checks + --include-non-failures include successes, available with '--scanners misconfig' + --input string input file path instead of image name + --java-db-repository strings OCI repository(ies) to retrieve trivy-java-db in order of priority (default [mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1]) + --license-confidence-level float specify license classifier's confidence level (default 0.9) + --license-full eagerly look for licenses in source code headers and license files + --list-all-pkgs output all packages in the JSON report regardless of vulnerability (default true) + --max-image-size string [EXPERIMENTAL] maximum image size to process, specified in a human-readable format (e.g., '44kB', '17MB'); an error will be returned if the image exceeds this size + --misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot,ansible]) + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") + --no-progress suppress progress bar + --offline-scan do not issue API requests to identify dependencies + -o, --output string output file name + --output-plugin-arg string [EXPERIMENTAL] output plugin arguments + --parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5) + --password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons. + --password-stdin password from stdin. Comma-separated passwords are not supported. + --pkg-relationships strings list of package relationships + Allowed values: + - unknown + - root + - workspace + - direct + - indirect + (default [unknown,root,workspace,direct,indirect]) + --pkg-types strings list of package types (allowed values: os,library) (default [os,library]) + --platform string set platform in the form os/arch if image is multi-platform capable + --podman-host string unix podman socket path to use for podman scanning + --raw-config-scanners strings specify the types of scanners that will also scan raw configurations. For example, scanners will scan a non-adapted configuration into a shared state (allowed values: terraform) + --redis-ca string redis ca file location, if using redis as cache backend + --redis-cert string redis certificate file location, if using redis as cache backend + --redis-key string redis key file location, if using redis as cache backend + --redis-tls enable redis TLS with public certificates, if using redis as cache backend + --registry-token string registry token + --rego-error-limit int maximum number of compile errors allowed during Rego policy evaluation (default 10) + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --removed-pkgs detect vulnerabilities of removed packages (only for Alpine) + --render-cause strings specify configuration types for which the rendered causes will be shown in the table report (allowed values: terraform,ansible) + --report string specify a format for the compliance report. (allowed values: all,summary) (default "summary") + --sbom-sources strings [EXPERIMENTAL] try to retrieve SBOM from the specified sources (allowed values: oci,rekor) + --scanners strings comma-separated list of what security issues to detect (allowed values: vuln,misconfig,secret,license) (default [vuln,secret]) + --secret-config string specify a path to config file for secret scanning (default "trivy-secret.yaml") + --server string server address in client mode + -s, --severity strings severities of security issues to be displayed + Allowed values: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL]) + --show-suppressed [EXPERIMENTAL] show suppressed vulnerabilities + --skip-check-update skip fetching rego check updates + --skip-db-update skip updating vulnerability database + --skip-dirs strings specify the directories or glob patterns to skip + --skip-files strings specify the files or glob patterns to skip + --skip-java-db-update skip updating Java index database + --skip-version-check suppress notices about version updates and Trivy announcements + --skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update + --table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed]) + -t, --template string output template + --tf-exclude-downloaded-modules exclude misconfigurations for downloaded terraform modules + --token string for authentication in client/server mode + --token-header string specify a header name for token in client/server mode (default "Trivy-Token") + --trace-rego enable more verbose trace output for custom queries + --username strings username. Comma-separated usernames allowed. + --vex strings [EXPERIMENTAL] VEX sources ("repo", "oci" or file path) + --vuln-severity-source strings order of data sources for selecting vulnerability severity level + Allowed values: + - nvd + - redhat + - redhat-oval + - debian + - ubuntu + - alpine + - amazon + - oracle-oval + - suse-cvrf + - photon + - arch-linux + - alma + - rocky + - cbl-mariner + - azure + - ruby-advisory-db + - php-security-advisories + - nodejs-security-wg + - ghsa + - glad + - aqua + - osv + - k8s + - wolfi + - chainguard + - bitnami + - govulndb + - julia + - echo + - minimos + - rootio + - auto + (default [auto]) +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_kubernetes.md b/docs/guide/references/configuration/cli/trivy_kubernetes.md new file mode 100644 index 000000000000..b200e336c6fa --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_kubernetes.md @@ -0,0 +1,208 @@ +## trivy kubernetes + +[EXPERIMENTAL] Scan kubernetes cluster + +### Synopsis + +Default context in kube configuration will be used unless specified + +``` +trivy kubernetes [flags] [CONTEXT] +``` + +### Examples + +``` + # cluster scanning + $ trivy k8s --report summary + + # cluster scanning with specific namespace: + $ trivy k8s --include-namespaces kube-system --report summary + + # cluster with specific context: + $ trivy k8s kind-kind --report summary + + + +``` + +### Options + +``` + --ansible-extra-vars strings set additional variables as key=value or @file (YAML/JSON) + --ansible-inventory strings specify inventory host path or comma separated host list + --ansible-playbook strings specify playbook file path(s) to scan + --burst int specify the maximum burst for throttle (default 10) + --cache-backend string [EXPERIMENTAL] cache backend (e.g. redis://localhost:6379) (default "fs") + --cache-ttl duration cache TTL when using redis as cache backend + --check-namespaces strings Rego namespaces + --checks-bundle-repository string OCI registry URL to retrieve checks bundle from (default "mirror.gcr.io/aquasec/trivy-checks:2") + --compliance string compliance report to generate + Built-in compliance's: + - k8s-nsa-1.0 + - k8s-cis-1.23 + - eks-cis-1.4 + - rke2-cis-1.24 + - k8s-pss-baseline-0.1 + - k8s-pss-restricted-0.1 + --config-check strings specify the paths to the Rego check files or to the directories containing them, applying config files + --config-data strings specify paths from which data for the Rego checks will be recursively loaded + --config-file-schemas strings specify paths to JSON configuration file schemas to determine that a file matches some configuration and pass the schema to Rego checks for type checking + --db-repository strings OCI repository(ies) to retrieve trivy-db in order of priority (default [mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2]) + --dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages + --detection-priority string specify the detection priority: + - "precise": Prioritizes precise by minimizing false positives. + - "comprehensive": Aims to detect more security findings at the cost of potential false positives. + (allowed values: precise,comprehensive) (default "precise") + --disable-node-collector When the flag is activated, the node-collector job will not be executed, thus skipping misconfiguration findings on the node. + --disable-telemetry disable sending anonymous usage data to Aqua + --distro string [EXPERIMENTAL] specify a distribution, / + --download-db-only download/update vulnerability database but don't run a scan + --download-java-db-only download/update Java index database but don't run a scan + --exclude-kinds strings indicate the kinds exclude from scanning (example: node) + --exclude-namespaces strings indicate the namespaces excluded from scanning (example: kube-system) + --exclude-nodes strings indicate the node labels that the node-collector job should exclude from scanning (example: kubernetes.io/arch:arm64,team:dev) + --exclude-owned exclude resources that have an owner reference + --exit-code int specify exit code when any security issues are found + --file-patterns strings specify config file patterns + -f, --format string format (allowed values: table,json,cyclonedx) (default "table") + --helm-api-versions strings Available API versions used for Capabilities.APIVersions. This flag is the same as the api-versions flag of the helm template command. (can specify multiple or separate values with commas: policy/v1/PodDisruptionBudget,apps/v1/Deployment) + --helm-kube-version string Kubernetes version used for Capabilities.KubeVersion. This flag is the same as the kube-version flag of the helm template command. + --helm-set strings specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-set-file strings specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) + --helm-set-string strings specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-values strings specify paths to override the Helm values.yaml files + -h, --help help for kubernetes + --ignore-policy string specify the Rego file path to evaluate each vulnerability + --ignore-status strings comma-separated list of vulnerability status to ignore + Allowed values: + - unknown + - not_affected + - affected + - fixed + - under_investigation + - will_not_fix + - fix_deferred + - end_of_life + --ignore-unfixed display only fixed vulnerabilities + --ignorefile string specify .trivyignore file (default ".trivyignore") + --image-src strings image source(s) to use, in priority order (allowed values: docker,containerd,podman,remote) (default [docker,containerd,podman,remote]) + --include-deprecated-checks include deprecated checks + --include-kinds strings indicate the kinds included in scanning (example: node) + --include-namespaces strings indicate the namespaces included in scanning (example: kube-system) + --include-non-failures include successes, available with '--scanners misconfig' + --java-db-repository strings OCI repository(ies) to retrieve trivy-java-db in order of priority (default [mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1]) + --k8s-version string specify k8s version to validate outdated api by it (example: 1.21.0) + --kubeconfig string specify the kubeconfig file path to use + --list-all-pkgs output all packages in the JSON report regardless of vulnerability (default true) + --misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot,ansible]) + --no-progress suppress progress bar + --node-collector-imageref string indicate the image reference for the node-collector scan job (default "ghcr.io/aquasecurity/node-collector:0.3.1") + --node-collector-namespace string specify the namespace in which the node-collector job should be deployed (default "trivy-temp") + --offline-scan do not issue API requests to identify dependencies + -o, --output string output file name + --output-plugin-arg string [EXPERIMENTAL] output plugin arguments + --parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5) + --password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons. + --password-stdin password from stdin. Comma-separated passwords are not supported. + --pkg-relationships strings list of package relationships + Allowed values: + - unknown + - root + - workspace + - direct + - indirect + (default [unknown,root,workspace,direct,indirect]) + --pkg-types strings list of package types (allowed values: os,library) (default [os,library]) + --qps float specify the maximum QPS to the master from this client (default 5) + --raw-config-scanners strings specify the types of scanners that will also scan raw configurations. For example, scanners will scan a non-adapted configuration into a shared state (allowed values: terraform) + --redis-ca string redis ca file location, if using redis as cache backend + --redis-cert string redis certificate file location, if using redis as cache backend + --redis-key string redis key file location, if using redis as cache backend + --redis-tls enable redis TLS with public certificates, if using redis as cache backend + --registry-token string registry token + --rego-error-limit int maximum number of compile errors allowed during Rego policy evaluation (default 10) + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --render-cause strings specify configuration types for which the rendered causes will be shown in the table report (allowed values: terraform,ansible) + --report string specify a report format for the output (allowed values: all,summary) (default "all") + --sbom-sources strings [EXPERIMENTAL] try to retrieve SBOM from the specified sources (allowed values: oci,rekor) + --scanners strings comma-separated list of what security issues to detect (allowed values: vuln,misconfig,secret,rbac) (default [vuln,misconfig,secret,rbac]) + --secret-config string specify a path to config file for secret scanning (default "trivy-secret.yaml") + -s, --severity strings severities of security issues to be displayed + Allowed values: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL]) + --show-suppressed [EXPERIMENTAL] show suppressed vulnerabilities + --skip-check-update skip fetching rego check updates + --skip-db-update skip updating vulnerability database + --skip-dirs strings specify the directories or glob patterns to skip + --skip-files strings specify the files or glob patterns to skip + --skip-images skip the downloading and scanning of images (vulnerabilities and secrets) in the cluster resources + --skip-java-db-update skip updating Java index database + --skip-version-check suppress notices about version updates and Trivy announcements + --skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update + -t, --template string output template + --tf-exclude-downloaded-modules exclude misconfigurations for downloaded terraform modules + --tolerations strings specify node-collector job tolerations (example: key1=value1:NoExecute,key2=value2:NoSchedule) + --trace-rego enable more verbose trace output for custom queries + --username strings username. Comma-separated usernames allowed. + --vex strings [EXPERIMENTAL] VEX sources ("repo", "oci" or file path) + --vuln-severity-source strings order of data sources for selecting vulnerability severity level + Allowed values: + - nvd + - redhat + - redhat-oval + - debian + - ubuntu + - alpine + - amazon + - oracle-oval + - suse-cvrf + - photon + - arch-linux + - alma + - rocky + - cbl-mariner + - azure + - ruby-advisory-db + - php-security-advisories + - nodejs-security-wg + - ghsa + - glad + - aqua + - osv + - k8s + - wolfi + - chainguard + - bitnami + - govulndb + - julia + - echo + - minimos + - rootio + - auto + (default [auto]) +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_module.md b/docs/guide/references/configuration/cli/trivy_module.md new file mode 100644 index 000000000000..a305c1fe1d3c --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_module.md @@ -0,0 +1,32 @@ +## trivy module + +Manage modules + +### Options + +``` + --enable-modules strings [EXPERIMENTAL] module names to enable + -h, --help help for module + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner +* [trivy module install](trivy_module_install.md) - Install a module +* [trivy module uninstall](trivy_module_uninstall.md) - Uninstall a module + diff --git a/docs/guide/references/configuration/cli/trivy_module_install.md b/docs/guide/references/configuration/cli/trivy_module_install.md new file mode 100644 index 000000000000..fed88072a3f7 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_module_install.md @@ -0,0 +1,34 @@ +## trivy module install + +Install a module + +``` +trivy module install [flags] REPOSITORY +``` + +### Options + +``` + -h, --help help for install +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --enable-modules strings [EXPERIMENTAL] module names to enable + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy module](trivy_module.md) - Manage modules + diff --git a/docs/guide/references/configuration/cli/trivy_module_uninstall.md b/docs/guide/references/configuration/cli/trivy_module_uninstall.md new file mode 100644 index 000000000000..14c902ff961d --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_module_uninstall.md @@ -0,0 +1,34 @@ +## trivy module uninstall + +Uninstall a module + +``` +trivy module uninstall [flags] REPOSITORY +``` + +### Options + +``` + -h, --help help for uninstall +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --enable-modules strings [EXPERIMENTAL] module names to enable + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy module](trivy_module.md) - Manage modules + diff --git a/docs/guide/references/configuration/cli/trivy_plugin.md b/docs/guide/references/configuration/cli/trivy_plugin.md new file mode 100644 index 000000000000..f0b2726acf3c --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_plugin.md @@ -0,0 +1,36 @@ +## trivy plugin + +Manage plugins + +### Options + +``` + -h, --help help for plugin +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner +* [trivy plugin info](trivy_plugin_info.md) - Show information about the specified plugin +* [trivy plugin install](trivy_plugin_install.md) - Install a plugin +* [trivy plugin list](trivy_plugin_list.md) - List installed plugin +* [trivy plugin run](trivy_plugin_run.md) - Run a plugin on the fly +* [trivy plugin search](trivy_plugin_search.md) - List Trivy plugins available on the plugin index and search among them +* [trivy plugin uninstall](trivy_plugin_uninstall.md) - Uninstall a plugin +* [trivy plugin update](trivy_plugin_update.md) - Update the local copy of the plugin index +* [trivy plugin upgrade](trivy_plugin_upgrade.md) - Upgrade installed plugins to newer versions + diff --git a/docs/guide/references/configuration/cli/trivy_plugin_info.md b/docs/guide/references/configuration/cli/trivy_plugin_info.md new file mode 100644 index 000000000000..6009d421377a --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_plugin_info.md @@ -0,0 +1,32 @@ +## trivy plugin info + +Show information about the specified plugin + +``` +trivy plugin info PLUGIN_NAME +``` + +### Options + +``` + -h, --help help for info +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/docs/guide/references/configuration/cli/trivy_plugin_install.md b/docs/guide/references/configuration/cli/trivy_plugin_install.md new file mode 100644 index 000000000000..5b553e696d48 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_plugin_install.md @@ -0,0 +1,45 @@ +## trivy plugin install + +Install a plugin + +``` +trivy plugin install NAME | URL | FILE_PATH +``` + +### Examples + +``` + # Install a plugin from the plugin index + $ trivy plugin install referrer + + # Specify the version of the plugin to install + $ trivy plugin install referrer@v0.3.0 + + # Install a plugin from a URL + $ trivy plugin install github.com/aquasecurity/trivy-plugin-referrer +``` + +### Options + +``` + -h, --help help for install +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/docs/guide/references/configuration/cli/trivy_plugin_list.md b/docs/guide/references/configuration/cli/trivy_plugin_list.md new file mode 100644 index 000000000000..7bc7237c4b4e --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_plugin_list.md @@ -0,0 +1,32 @@ +## trivy plugin list + +List installed plugin + +``` +trivy plugin list +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/docs/guide/references/configuration/cli/trivy_plugin_run.md b/docs/guide/references/configuration/cli/trivy_plugin_run.md new file mode 100644 index 000000000000..e99ecd00e9f2 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_plugin_run.md @@ -0,0 +1,32 @@ +## trivy plugin run + +Run a plugin on the fly + +``` +trivy plugin run NAME | URL | FILE_PATH +``` + +### Options + +``` + -h, --help help for run +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/docs/guide/references/configuration/cli/trivy_plugin_search.md b/docs/guide/references/configuration/cli/trivy_plugin_search.md new file mode 100644 index 000000000000..d93b5d366aa1 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_plugin_search.md @@ -0,0 +1,32 @@ +## trivy plugin search + +List Trivy plugins available on the plugin index and search among them + +``` +trivy plugin search [KEYWORD] +``` + +### Options + +``` + -h, --help help for search +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/docs/guide/references/configuration/cli/trivy_plugin_uninstall.md b/docs/guide/references/configuration/cli/trivy_plugin_uninstall.md new file mode 100644 index 000000000000..669bf0e6ccae --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_plugin_uninstall.md @@ -0,0 +1,32 @@ +## trivy plugin uninstall + +Uninstall a plugin + +``` +trivy plugin uninstall PLUGIN_NAME +``` + +### Options + +``` + -h, --help help for uninstall +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/docs/guide/references/configuration/cli/trivy_plugin_update.md b/docs/guide/references/configuration/cli/trivy_plugin_update.md new file mode 100644 index 000000000000..5b706e77ebcd --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_plugin_update.md @@ -0,0 +1,32 @@ +## trivy plugin update + +Update the local copy of the plugin index + +``` +trivy plugin update +``` + +### Options + +``` + -h, --help help for update +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/docs/guide/references/configuration/cli/trivy_plugin_upgrade.md b/docs/guide/references/configuration/cli/trivy_plugin_upgrade.md new file mode 100644 index 000000000000..0c5c6e14253a --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_plugin_upgrade.md @@ -0,0 +1,32 @@ +## trivy plugin upgrade + +Upgrade installed plugins to newer versions + +``` +trivy plugin upgrade [PLUGIN_NAMES] +``` + +### Options + +``` + -h, --help help for upgrade +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy plugin](trivy_plugin.md) - Manage plugins + diff --git a/docs/guide/references/configuration/cli/trivy_registry.md b/docs/guide/references/configuration/cli/trivy_registry.md new file mode 100644 index 000000000000..bf7af19c7526 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_registry.md @@ -0,0 +1,30 @@ +## trivy registry + +Manage registry authentication + +### Options + +``` + -h, --help help for registry +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner +* [trivy registry login](trivy_registry_login.md) - Log in to a registry +* [trivy registry logout](trivy_registry_logout.md) - Log out of a registry + diff --git a/docs/guide/references/configuration/cli/trivy_registry_login.md b/docs/guide/references/configuration/cli/trivy_registry_login.md new file mode 100644 index 000000000000..2f5460bf4f62 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_registry_login.md @@ -0,0 +1,42 @@ +## trivy registry login + +Log in to a registry + +``` +trivy registry login SERVER [flags] +``` + +### Examples + +``` + # Log in to reg.example.com + cat ~/my_password.txt | trivy registry login --username foo --password-stdin reg.example.com +``` + +### Options + +``` + -h, --help help for login + --password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons. + --password-stdin password from stdin. Comma-separated passwords are not supported. + --username strings username. Comma-separated usernames allowed. +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy registry](trivy_registry.md) - Manage registry authentication + diff --git a/docs/guide/references/configuration/cli/trivy_registry_logout.md b/docs/guide/references/configuration/cli/trivy_registry_logout.md new file mode 100644 index 000000000000..809488f66539 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_registry_logout.md @@ -0,0 +1,39 @@ +## trivy registry logout + +Log out of a registry + +``` +trivy registry logout SERVER [flags] +``` + +### Examples + +``` + # Log out of reg.example.com + trivy registry logout reg.example.com +``` + +### Options + +``` + -h, --help help for logout +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy registry](trivy_registry.md) - Manage registry authentication + diff --git a/docs/guide/references/configuration/cli/trivy_repository.md b/docs/guide/references/configuration/cli/trivy_repository.md new file mode 100644 index 000000000000..d7a58cf7bf87 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_repository.md @@ -0,0 +1,198 @@ +## trivy repository + +Scan a repository + +``` +trivy repository [flags] (REPO_PATH | REPO_URL) +``` + +### Examples + +``` + # Scan your remote git repository + $ trivy repo https://github.com/knqyf263/trivy-ci-test + # Scan your local git repository + $ trivy repo /path/to/your/repository +``` + +### Options + +``` + --ansible-extra-vars strings set additional variables as key=value or @file (YAML/JSON) + --ansible-inventory strings specify inventory host path or comma separated host list + --ansible-playbook strings specify playbook file path(s) to scan + --branch string pass the branch name to be scanned + --cache-backend string [EXPERIMENTAL] cache backend (e.g. redis://localhost:6379) (default "fs") + --cache-ttl duration cache TTL when using redis as cache backend + --cf-params strings specify paths to override the CloudFormation parameters files + --check-namespaces strings Rego namespaces + --checks-bundle-repository string OCI registry URL to retrieve checks bundle from (default "mirror.gcr.io/aquasec/trivy-checks:2") + --commit string pass the commit hash to be scanned + --config-check strings specify the paths to the Rego check files or to the directories containing them, applying config files + --config-data strings specify paths from which data for the Rego checks will be recursively loaded + --config-file-schemas strings specify paths to JSON configuration file schemas to determine that a file matches some configuration and pass the schema to Rego checks for type checking + --custom-headers strings custom headers in client mode + --db-repository strings OCI repository(ies) to retrieve trivy-db in order of priority (default [mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2]) + --dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages + --detection-priority string specify the detection priority: + - "precise": Prioritizes precise by minimizing false positives. + - "comprehensive": Aims to detect more security findings at the cost of potential false positives. + (allowed values: precise,comprehensive) (default "precise") + --disable-telemetry disable sending anonymous usage data to Aqua + --download-db-only download/update vulnerability database but don't run a scan + --download-java-db-only download/update Java index database but don't run a scan + --enable-modules strings [EXPERIMENTAL] module names to enable + --exit-code int specify exit code when any security issues are found + --file-patterns strings specify config file patterns + -f, --format string format + Allowed values: + - table + - json + - template + - sarif + - cyclonedx + - spdx + - spdx-json + - github + - cosign-vuln + (default "table") + --helm-api-versions strings Available API versions used for Capabilities.APIVersions. This flag is the same as the api-versions flag of the helm template command. (can specify multiple or separate values with commas: policy/v1/PodDisruptionBudget,apps/v1/Deployment) + --helm-kube-version string Kubernetes version used for Capabilities.KubeVersion. This flag is the same as the kube-version flag of the helm template command. + --helm-set strings specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-set-file strings specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) + --helm-set-string strings specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-values strings specify paths to override the Helm values.yaml files + -h, --help help for repository + --ignore-policy string specify the Rego file path to evaluate each vulnerability + --ignore-status strings comma-separated list of vulnerability status to ignore + Allowed values: + - unknown + - not_affected + - affected + - fixed + - under_investigation + - will_not_fix + - fix_deferred + - end_of_life + --ignore-unfixed display only fixed vulnerabilities + --ignored-licenses strings specify a list of license to ignore + --ignorefile string specify .trivyignore file (default ".trivyignore") + --include-deprecated-checks include deprecated checks + --include-dev-deps include development dependencies in the report (supported: npm, yarn, gradle) + --include-non-failures include successes, available with '--scanners misconfig' + --java-db-repository strings OCI repository(ies) to retrieve trivy-java-db in order of priority (default [mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1]) + --license-confidence-level float specify license classifier's confidence level (default 0.9) + --license-full eagerly look for licenses in source code headers and license files + --list-all-pkgs output all packages in the JSON report regardless of vulnerability (default true) + --misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot,ansible]) + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") + --no-progress suppress progress bar + --offline-scan do not issue API requests to identify dependencies + -o, --output string output file name + --output-plugin-arg string [EXPERIMENTAL] output plugin arguments + --parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5) + --password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons. + --password-stdin password from stdin. Comma-separated passwords are not supported. + --pkg-relationships strings list of package relationships + Allowed values: + - unknown + - root + - workspace + - direct + - indirect + (default [unknown,root,workspace,direct,indirect]) + --pkg-types strings list of package types (allowed values: os,library) (default [os,library]) + --raw-config-scanners strings specify the types of scanners that will also scan raw configurations. For example, scanners will scan a non-adapted configuration into a shared state (allowed values: terraform) + --redis-ca string redis ca file location, if using redis as cache backend + --redis-cert string redis certificate file location, if using redis as cache backend + --redis-key string redis key file location, if using redis as cache backend + --redis-tls enable redis TLS with public certificates, if using redis as cache backend + --registry-token string registry token + --rego-error-limit int maximum number of compile errors allowed during Rego policy evaluation (default 10) + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --render-cause strings specify configuration types for which the rendered causes will be shown in the table report (allowed values: terraform,ansible) + --sbom-sources strings [EXPERIMENTAL] try to retrieve SBOM from the specified sources (allowed values: oci,rekor) + --scanners strings comma-separated list of what security issues to detect (allowed values: vuln,misconfig,secret,license) (default [vuln,secret]) + --secret-config string specify a path to config file for secret scanning (default "trivy-secret.yaml") + --server string server address in client mode + -s, --severity strings severities of security issues to be displayed + Allowed values: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL]) + --show-suppressed [EXPERIMENTAL] show suppressed vulnerabilities + --skip-check-update skip fetching rego check updates + --skip-db-update skip updating vulnerability database + --skip-dirs strings specify the directories or glob patterns to skip + --skip-files strings specify the files or glob patterns to skip + --skip-java-db-update skip updating Java index database + --skip-version-check suppress notices about version updates and Trivy announcements + --skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update + --table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed]) + --tag string pass the tag name to be scanned + -t, --template string output template + --tf-exclude-downloaded-modules exclude misconfigurations for downloaded terraform modules + --tf-vars strings specify paths to override the Terraform tfvars files + --token string for authentication in client/server mode + --token-header string specify a header name for token in client/server mode (default "Trivy-Token") + --trace-rego enable more verbose trace output for custom queries + --username strings username. Comma-separated usernames allowed. + --vex strings [EXPERIMENTAL] VEX sources ("repo", "oci" or file path) + --vuln-severity-source strings order of data sources for selecting vulnerability severity level + Allowed values: + - nvd + - redhat + - redhat-oval + - debian + - ubuntu + - alpine + - amazon + - oracle-oval + - suse-cvrf + - photon + - arch-linux + - alma + - rocky + - cbl-mariner + - azure + - ruby-advisory-db + - php-security-advisories + - nodejs-security-wg + - ghsa + - glad + - aqua + - osv + - k8s + - wolfi + - chainguard + - bitnami + - govulndb + - julia + - echo + - minimos + - rootio + - auto + (default [auto]) +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_rootfs.md b/docs/guide/references/configuration/cli/trivy_rootfs.md new file mode 100644 index 000000000000..f560f272b038 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_rootfs.md @@ -0,0 +1,200 @@ +## trivy rootfs + +Scan rootfs + +``` +trivy rootfs [flags] ROOTDIR +``` + +### Examples + +``` + # Scan unpacked filesystem + $ docker export $(docker create alpine:3.10.2) | tar -C /tmp/rootfs -xvf - + $ trivy rootfs /tmp/rootfs + + # Scan from inside a container + $ docker run --rm -it alpine:3.11 + / # curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + / # trivy rootfs / +``` + +### Options + +``` + --ansible-extra-vars strings set additional variables as key=value or @file (YAML/JSON) + --ansible-inventory strings specify inventory host path or comma separated host list + --ansible-playbook strings specify playbook file path(s) to scan + --cache-backend string [EXPERIMENTAL] cache backend (e.g. redis://localhost:6379) (default "memory") + --cache-ttl duration cache TTL when using redis as cache backend + --cf-params strings specify paths to override the CloudFormation parameters files + --check-namespaces strings Rego namespaces + --checks-bundle-repository string OCI registry URL to retrieve checks bundle from (default "mirror.gcr.io/aquasec/trivy-checks:2") + --config-check strings specify the paths to the Rego check files or to the directories containing them, applying config files + --config-data strings specify paths from which data for the Rego checks will be recursively loaded + --config-file-schemas strings specify paths to JSON configuration file schemas to determine that a file matches some configuration and pass the schema to Rego checks for type checking + --custom-headers strings custom headers in client mode + --db-repository strings OCI repository(ies) to retrieve trivy-db in order of priority (default [mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2]) + --dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages + --detection-priority string specify the detection priority: + - "precise": Prioritizes precise by minimizing false positives. + - "comprehensive": Aims to detect more security findings at the cost of potential false positives. + (allowed values: precise,comprehensive) (default "precise") + --disable-telemetry disable sending anonymous usage data to Aqua + --distro string [EXPERIMENTAL] specify a distribution, / + --download-db-only download/update vulnerability database but don't run a scan + --download-java-db-only download/update Java index database but don't run a scan + --enable-modules strings [EXPERIMENTAL] module names to enable + --exit-code int specify exit code when any security issues are found + --exit-on-eol int exit with the specified code when the OS reaches end of service/life + --file-patterns strings specify config file patterns + -f, --format string format + Allowed values: + - table + - json + - template + - sarif + - cyclonedx + - spdx + - spdx-json + - github + - cosign-vuln + (default "table") + --helm-api-versions strings Available API versions used for Capabilities.APIVersions. This flag is the same as the api-versions flag of the helm template command. (can specify multiple or separate values with commas: policy/v1/PodDisruptionBudget,apps/v1/Deployment) + --helm-kube-version string Kubernetes version used for Capabilities.KubeVersion. This flag is the same as the kube-version flag of the helm template command. + --helm-set strings specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-set-file strings specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) + --helm-set-string strings specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-values strings specify paths to override the Helm values.yaml files + -h, --help help for rootfs + --ignore-policy string specify the Rego file path to evaluate each vulnerability + --ignore-status strings comma-separated list of vulnerability status to ignore + Allowed values: + - unknown + - not_affected + - affected + - fixed + - under_investigation + - will_not_fix + - fix_deferred + - end_of_life + --ignore-unfixed display only fixed vulnerabilities + --ignored-licenses strings specify a list of license to ignore + --ignorefile string specify .trivyignore file (default ".trivyignore") + --include-deprecated-checks include deprecated checks + --include-non-failures include successes, available with '--scanners misconfig' + --java-db-repository strings OCI repository(ies) to retrieve trivy-java-db in order of priority (default [mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1]) + --license-confidence-level float specify license classifier's confidence level (default 0.9) + --license-full eagerly look for licenses in source code headers and license files + --list-all-pkgs output all packages in the JSON report regardless of vulnerability (default true) + --misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot,ansible]) + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") + --no-progress suppress progress bar + --offline-scan do not issue API requests to identify dependencies + -o, --output string output file name + --output-plugin-arg string [EXPERIMENTAL] output plugin arguments + --parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5) + --password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons. + --password-stdin password from stdin. Comma-separated passwords are not supported. + --pkg-relationships strings list of package relationships + Allowed values: + - unknown + - root + - workspace + - direct + - indirect + (default [unknown,root,workspace,direct,indirect]) + --pkg-types strings list of package types (allowed values: os,library) (default [os,library]) + --raw-config-scanners strings specify the types of scanners that will also scan raw configurations. For example, scanners will scan a non-adapted configuration into a shared state (allowed values: terraform) + --redis-ca string redis ca file location, if using redis as cache backend + --redis-cert string redis certificate file location, if using redis as cache backend + --redis-key string redis key file location, if using redis as cache backend + --redis-tls enable redis TLS with public certificates, if using redis as cache backend + --registry-token string registry token + --rego-error-limit int maximum number of compile errors allowed during Rego policy evaluation (default 10) + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --render-cause strings specify configuration types for which the rendered causes will be shown in the table report (allowed values: terraform,ansible) + --sbom-sources strings [EXPERIMENTAL] try to retrieve SBOM from the specified sources (allowed values: oci,rekor) + --scanners strings comma-separated list of what security issues to detect (allowed values: vuln,misconfig,secret,license) (default [vuln,secret]) + --secret-config string specify a path to config file for secret scanning (default "trivy-secret.yaml") + --server string server address in client mode + -s, --severity strings severities of security issues to be displayed + Allowed values: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL]) + --show-suppressed [EXPERIMENTAL] show suppressed vulnerabilities + --skip-check-update skip fetching rego check updates + --skip-db-update skip updating vulnerability database + --skip-dirs strings specify the directories or glob patterns to skip + --skip-files strings specify the files or glob patterns to skip + --skip-java-db-update skip updating Java index database + --skip-version-check suppress notices about version updates and Trivy announcements + --skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update + --table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed]) + -t, --template string output template + --tf-exclude-downloaded-modules exclude misconfigurations for downloaded terraform modules + --tf-vars strings specify paths to override the Terraform tfvars files + --token string for authentication in client/server mode + --token-header string specify a header name for token in client/server mode (default "Trivy-Token") + --trace-rego enable more verbose trace output for custom queries + --username strings username. Comma-separated usernames allowed. + --vex strings [EXPERIMENTAL] VEX sources ("repo", "oci" or file path) + --vuln-severity-source strings order of data sources for selecting vulnerability severity level + Allowed values: + - nvd + - redhat + - redhat-oval + - debian + - ubuntu + - alpine + - amazon + - oracle-oval + - suse-cvrf + - photon + - arch-linux + - alma + - rocky + - cbl-mariner + - azure + - ruby-advisory-db + - php-security-advisories + - nodejs-security-wg + - ghsa + - glad + - aqua + - osv + - k8s + - wolfi + - chainguard + - bitnami + - govulndb + - julia + - echo + - minimos + - rootio + - auto + (default [auto]) +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_sbom.md b/docs/guide/references/configuration/cli/trivy_sbom.md new file mode 100644 index 000000000000..a6091bb64130 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_sbom.md @@ -0,0 +1,165 @@ +## trivy sbom + +Scan SBOM for vulnerabilities and licenses + +``` +trivy sbom [flags] SBOM_PATH +``` + +### Examples + +``` + # Scan CycloneDX and show the result in tables + $ trivy sbom /path/to/report.cdx + + # Scan CycloneDX-type attestation and show the result in tables + $ trivy sbom /path/to/report.cdx.intoto.jsonl + +``` + +### Options + +``` + --cache-backend string [EXPERIMENTAL] cache backend (e.g. redis://localhost:6379) (default "memory") + --cache-ttl duration cache TTL when using redis as cache backend + --compliance string compliance report to generate + --custom-headers strings custom headers in client mode + --db-repository strings OCI repository(ies) to retrieve trivy-db in order of priority (default [mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2]) + --detection-priority string specify the detection priority: + - "precise": Prioritizes precise by minimizing false positives. + - "comprehensive": Aims to detect more security findings at the cost of potential false positives. + (allowed values: precise,comprehensive) (default "precise") + --disable-telemetry disable sending anonymous usage data to Aqua + --distro string [EXPERIMENTAL] specify a distribution, / + --download-db-only download/update vulnerability database but don't run a scan + --download-java-db-only download/update Java index database but don't run a scan + --exit-code int specify exit code when any security issues are found + --exit-on-eol int exit with the specified code when the OS reaches end of service/life + --file-patterns strings specify config file patterns + -f, --format string format + Allowed values: + - table + - json + - template + - sarif + - cyclonedx + - spdx + - spdx-json + - github + - cosign-vuln + (default "table") + -h, --help help for sbom + --ignore-policy string specify the Rego file path to evaluate each vulnerability + --ignore-status strings comma-separated list of vulnerability status to ignore + Allowed values: + - unknown + - not_affected + - affected + - fixed + - under_investigation + - will_not_fix + - fix_deferred + - end_of_life + --ignore-unfixed display only fixed vulnerabilities + --ignored-licenses strings specify a list of license to ignore + --ignorefile string specify .trivyignore file (default ".trivyignore") + --java-db-repository strings OCI repository(ies) to retrieve trivy-java-db in order of priority (default [mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1]) + --list-all-pkgs output all packages in the JSON report regardless of vulnerability (default true) + --no-progress suppress progress bar + --offline-scan do not issue API requests to identify dependencies + -o, --output string output file name + --output-plugin-arg string [EXPERIMENTAL] output plugin arguments + --password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons. + --password-stdin password from stdin. Comma-separated passwords are not supported. + --pkg-relationships strings list of package relationships + Allowed values: + - unknown + - root + - workspace + - direct + - indirect + (default [unknown,root,workspace,direct,indirect]) + --pkg-types strings list of package types (allowed values: os,library) (default [os,library]) + --redis-ca string redis ca file location, if using redis as cache backend + --redis-cert string redis certificate file location, if using redis as cache backend + --redis-key string redis key file location, if using redis as cache backend + --redis-tls enable redis TLS with public certificates, if using redis as cache backend + --registry-token string registry token + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --sbom-sources strings [EXPERIMENTAL] try to retrieve SBOM from the specified sources (allowed values: oci,rekor) + --scanners strings comma-separated list of what security issues to detect (allowed values: vuln,license) (default [vuln]) + --server string server address in client mode + -s, --severity strings severities of security issues to be displayed + Allowed values: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL]) + --show-suppressed [EXPERIMENTAL] show suppressed vulnerabilities + --skip-db-update skip updating vulnerability database + --skip-java-db-update skip updating Java index database + --skip-version-check suppress notices about version updates and Trivy announcements + --skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update + --table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed]) + -t, --template string output template + --token string for authentication in client/server mode + --token-header string specify a header name for token in client/server mode (default "Trivy-Token") + --username strings username. Comma-separated usernames allowed. + --vex strings [EXPERIMENTAL] VEX sources ("repo", "oci" or file path) + --vuln-severity-source strings order of data sources for selecting vulnerability severity level + Allowed values: + - nvd + - redhat + - redhat-oval + - debian + - ubuntu + - alpine + - amazon + - oracle-oval + - suse-cvrf + - photon + - arch-linux + - alma + - rocky + - cbl-mariner + - azure + - ruby-advisory-db + - php-security-advisories + - nodejs-security-wg + - ghsa + - glad + - aqua + - osv + - k8s + - wolfi + - chainguard + - bitnami + - govulndb + - julia + - echo + - minimos + - rootio + - auto + (default [auto]) +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_server.md b/docs/guide/references/configuration/cli/trivy_server.md new file mode 100644 index 000000000000..ed4e132dfec8 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_server.md @@ -0,0 +1,62 @@ +## trivy server + +Server mode + +``` +trivy server [flags] +``` + +### Examples + +``` + # Run a server + $ trivy server + + # Listen on 0.0.0.0:10000 + $ trivy server --listen 0.0.0.0:10000 + +``` + +### Options + +``` + --cache-backend string [EXPERIMENTAL] cache backend (e.g. redis://localhost:6379) (default "fs") + --cache-ttl duration cache TTL when using redis as cache backend + --db-repository strings OCI repository(ies) to retrieve trivy-db in order of priority (default [mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2]) + --download-db-only download/update vulnerability database but don't run a scan + --enable-modules strings [EXPERIMENTAL] module names to enable + -h, --help help for server + --listen string listen address in server mode (default "localhost:4954") + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") + --no-progress suppress progress bar + --password strings password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons. + --password-stdin password from stdin. Comma-separated passwords are not supported. + --redis-ca string redis ca file location, if using redis as cache backend + --redis-cert string redis certificate file location, if using redis as cache backend + --redis-key string redis key file location, if using redis as cache backend + --redis-tls enable redis TLS with public certificates, if using redis as cache backend + --registry-token string registry token + --skip-db-update skip updating vulnerability database + --token string for authentication in client/server mode + --token-header string specify a header name for token in client/server mode (default "Trivy-Token") + --username strings username. Comma-separated usernames allowed. +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_version.md b/docs/guide/references/configuration/cli/trivy_version.md new file mode 100644 index 000000000000..99ab0b520232 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_version.md @@ -0,0 +1,33 @@ +## trivy version + +Print the version + +``` +trivy version [flags] +``` + +### Options + +``` + -f, --format string version format (json) + -h, --help help for version +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/cli/trivy_vex.md b/docs/guide/references/configuration/cli/trivy_vex.md new file mode 100644 index 000000000000..6b30950ad62e --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_vex.md @@ -0,0 +1,29 @@ +## trivy vex + +[EXPERIMENTAL] VEX utilities + +### Options + +``` + -h, --help help for vex +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner +* [trivy vex repo](trivy_vex_repo.md) - Manage VEX repositories + diff --git a/docs/guide/references/configuration/cli/trivy_vex_repo.md b/docs/guide/references/configuration/cli/trivy_vex_repo.md new file mode 100644 index 000000000000..5758f05cb327 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_vex_repo.md @@ -0,0 +1,45 @@ +## trivy vex repo + +Manage VEX repositories + +### Examples + +``` + # Initialize the configuration file + $ trivy vex repo init + + # List VEX repositories + $ trivy vex repo list + + # Download the VEX repositories + $ trivy vex repo download + +``` + +### Options + +``` + -h, --help help for repo +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy vex](trivy_vex.md) - [EXPERIMENTAL] VEX utilities +* [trivy vex repo download](trivy_vex_repo_download.md) - Download the VEX repositories +* [trivy vex repo init](trivy_vex_repo_init.md) - Initialize a configuration file +* [trivy vex repo list](trivy_vex_repo_list.md) - List VEX repositories + diff --git a/docs/guide/references/configuration/cli/trivy_vex_repo_download.md b/docs/guide/references/configuration/cli/trivy_vex_repo_download.md new file mode 100644 index 000000000000..fa6ec2ec6a77 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_vex_repo_download.md @@ -0,0 +1,36 @@ +## trivy vex repo download + +Download the VEX repositories + +### Synopsis + +Downloads enabled VEX repositories. If specific repository names are provided as arguments, only those repositories will be downloaded. Otherwise, all enabled repositories are downloaded. + +``` +trivy vex repo download [REPO_NAMES] [flags] +``` + +### Options + +``` + -h, --help help for download +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy vex repo](trivy_vex_repo.md) - Manage VEX repositories + diff --git a/docs/guide/references/configuration/cli/trivy_vex_repo_init.md b/docs/guide/references/configuration/cli/trivy_vex_repo_init.md new file mode 100644 index 000000000000..1d3be26287e1 --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_vex_repo_init.md @@ -0,0 +1,32 @@ +## trivy vex repo init + +Initialize a configuration file + +``` +trivy vex repo init [flags] +``` + +### Options + +``` + -h, --help help for init +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy vex repo](trivy_vex_repo.md) - Manage VEX repositories + diff --git a/docs/guide/references/configuration/cli/trivy_vex_repo_list.md b/docs/guide/references/configuration/cli/trivy_vex_repo_list.md new file mode 100644 index 000000000000..92b246f52a0e --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_vex_repo_list.md @@ -0,0 +1,32 @@ +## trivy vex repo list + +List VEX repositories + +``` +trivy vex repo list [flags] +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy vex repo](trivy_vex_repo.md) - Manage VEX repositories + diff --git a/docs/guide/references/configuration/cli/trivy_vm.md b/docs/guide/references/configuration/cli/trivy_vm.md new file mode 100644 index 000000000000..828c043f028b --- /dev/null +++ b/docs/guide/references/configuration/cli/trivy_vm.md @@ -0,0 +1,184 @@ +## trivy vm + +[EXPERIMENTAL] Scan a virtual machine image + +``` +trivy vm [flags] VM_IMAGE +``` + +### Examples + +``` + # Scan your AWS AMI + $ trivy vm --scanners vuln ami:${your_ami_id} + + # Scan your AWS EBS snapshot + $ trivy vm ebs:${your_ebs_snapshot_id} + +``` + +### Options + +``` + --ansible-extra-vars strings set additional variables as key=value or @file (YAML/JSON) + --ansible-inventory strings specify inventory host path or comma separated host list + --ansible-playbook strings specify playbook file path(s) to scan + --aws-region string AWS region to scan + --cache-backend string [EXPERIMENTAL] cache backend (e.g. redis://localhost:6379) (default "fs") + --cache-ttl duration cache TTL when using redis as cache backend + --checks-bundle-repository string OCI registry URL to retrieve checks bundle from (default "mirror.gcr.io/aquasec/trivy-checks:2") + --compliance string compliance report to generate + --config-file-schemas strings specify paths to JSON configuration file schemas to determine that a file matches some configuration and pass the schema to Rego checks for type checking + --custom-headers strings custom headers in client mode + --db-repository strings OCI repository(ies) to retrieve trivy-db in order of priority (default [mirror.gcr.io/aquasec/trivy-db:2,ghcr.io/aquasecurity/trivy-db:2]) + --dependency-tree [EXPERIMENTAL] show dependency origin tree of vulnerable packages + --detection-priority string specify the detection priority: + - "precise": Prioritizes precise by minimizing false positives. + - "comprehensive": Aims to detect more security findings at the cost of potential false positives. + (allowed values: precise,comprehensive) (default "precise") + --disable-telemetry disable sending anonymous usage data to Aqua + --distro string [EXPERIMENTAL] specify a distribution, / + --download-db-only download/update vulnerability database but don't run a scan + --download-java-db-only download/update Java index database but don't run a scan + --enable-modules strings [EXPERIMENTAL] module names to enable + --exit-code int specify exit code when any security issues are found + --exit-on-eol int exit with the specified code when the OS reaches end of service/life + --file-patterns strings specify config file patterns + -f, --format string format + Allowed values: + - table + - json + - template + - sarif + - cyclonedx + - spdx + - spdx-json + - github + - cosign-vuln + (default "table") + --helm-api-versions strings Available API versions used for Capabilities.APIVersions. This flag is the same as the api-versions flag of the helm template command. (can specify multiple or separate values with commas: policy/v1/PodDisruptionBudget,apps/v1/Deployment) + --helm-kube-version string Kubernetes version used for Capabilities.KubeVersion. This flag is the same as the kube-version flag of the helm template command. + --helm-set strings specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-set-file strings specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) + --helm-set-string strings specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --helm-values strings specify paths to override the Helm values.yaml files + -h, --help help for vm + --ignore-policy string specify the Rego file path to evaluate each vulnerability + --ignore-status strings comma-separated list of vulnerability status to ignore + Allowed values: + - unknown + - not_affected + - affected + - fixed + - under_investigation + - will_not_fix + - fix_deferred + - end_of_life + --ignore-unfixed display only fixed vulnerabilities + --ignorefile string specify .trivyignore file (default ".trivyignore") + --include-non-failures include successes, available with '--scanners misconfig' + --java-db-repository strings OCI repository(ies) to retrieve trivy-java-db in order of priority (default [mirror.gcr.io/aquasec/trivy-java-db:1,ghcr.io/aquasecurity/trivy-java-db:1]) + --list-all-pkgs output all packages in the JSON report regardless of vulnerability (default true) + --misconfig-scanners strings comma-separated list of misconfig scanners to use for misconfiguration scanning (default [azure-arm,cloudformation,dockerfile,helm,kubernetes,terraform,terraformplan-json,terraformplan-snapshot,ansible]) + --module-dir string specify directory to the wasm modules that will be loaded (default "$HOME/.trivy/modules") + --no-progress suppress progress bar + --offline-scan do not issue API requests to identify dependencies + -o, --output string output file name + --output-plugin-arg string [EXPERIMENTAL] output plugin arguments + --parallel int number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism (default 5) + --pkg-relationships strings list of package relationships + Allowed values: + - unknown + - root + - workspace + - direct + - indirect + (default [unknown,root,workspace,direct,indirect]) + --pkg-types strings list of package types (allowed values: os,library) (default [os,library]) + --raw-config-scanners strings specify the types of scanners that will also scan raw configurations. For example, scanners will scan a non-adapted configuration into a shared state (allowed values: terraform) + --redis-ca string redis ca file location, if using redis as cache backend + --redis-cert string redis certificate file location, if using redis as cache backend + --redis-key string redis key file location, if using redis as cache backend + --redis-tls enable redis TLS with public certificates, if using redis as cache backend + --rekor-url string [EXPERIMENTAL] address of rekor STL server (default "https://rekor.sigstore.dev") + --render-cause strings specify configuration types for which the rendered causes will be shown in the table report (allowed values: terraform,ansible) + --sbom-sources strings [EXPERIMENTAL] try to retrieve SBOM from the specified sources (allowed values: oci,rekor) + --scanners strings comma-separated list of what security issues to detect (allowed values: vuln,misconfig,secret,license) (default [vuln,secret]) + --secret-config string specify a path to config file for secret scanning (default "trivy-secret.yaml") + --server string server address in client mode + -s, --severity strings severities of security issues to be displayed + Allowed values: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + (default [UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL]) + --show-suppressed [EXPERIMENTAL] show suppressed vulnerabilities + --skip-db-update skip updating vulnerability database + --skip-dirs strings specify the directories or glob patterns to skip + --skip-files strings specify the files or glob patterns to skip + --skip-java-db-update skip updating Java index database + --skip-version-check suppress notices about version updates and Trivy announcements + --skip-vex-repo-update [EXPERIMENTAL] Skip VEX Repository update + --table-mode strings [EXPERIMENTAL] tables that will be displayed in 'table' format (allowed values: summary,detailed) (default [summary,detailed]) + -t, --template string output template + --tf-exclude-downloaded-modules exclude misconfigurations for downloaded terraform modules + --token string for authentication in client/server mode + --token-header string specify a header name for token in client/server mode (default "Trivy-Token") + --vex strings [EXPERIMENTAL] VEX sources ("repo", "oci" or file path) + --vuln-severity-source strings order of data sources for selecting vulnerability severity level + Allowed values: + - nvd + - redhat + - redhat-oval + - debian + - ubuntu + - alpine + - amazon + - oracle-oval + - suse-cvrf + - photon + - arch-linux + - alma + - rocky + - cbl-mariner + - azure + - ruby-advisory-db + - php-security-advisories + - nodejs-security-wg + - ghsa + - glad + - aqua + - osv + - k8s + - wolfi + - chainguard + - bitnami + - govulndb + - julia + - echo + - minimos + - rootio + - auto + (default [auto]) +``` + +### Options inherited from parent commands + +``` + --cacert string Path to PEM-encoded CA certificate file + --cache-dir string cache directory (default "/path/to/cache") + -c, --config string config path (default "trivy.yaml") + -d, --debug debug mode + --generate-default-config write the default config to trivy-default.yaml + --insecure allow insecure server connections + -q, --quiet suppress progress bar and log output + --timeout duration timeout (default 5m0s) + -v, --version show version +``` + +### SEE ALSO + +* [trivy](trivy.md) - Unified security scanner + diff --git a/docs/guide/references/configuration/config-file.md b/docs/guide/references/configuration/config-file.md new file mode 100644 index 000000000000..363ab839d9a8 --- /dev/null +++ b/docs/guide/references/configuration/config-file.md @@ -0,0 +1,673 @@ +# Config file + +Trivy can be customized by tweaking a `trivy.yaml` file. +The config path can be overridden by the `--config` flag. + +An example is [here][example] and a [JSON Schema][schema] is also available. + +These samples contain default values for flags. +## Global options + +```yaml +# Same as '--cacert' +cacert: "" + +cache: + # Same as '--cache-dir' + dir: "/path/to/cache" + +# Same as '--debug' +debug: false + +# Same as '--insecure' +insecure: false + +# Same as '--quiet' +quiet: false + +# Same as '--timeout' +timeout: 5m0s + +``` +## Cache options + +```yaml +cache: + # Same as '--cache-backend' + backend: "fs" + + redis: + # Same as '--redis-ca' + ca: "" + + # Same as '--redis-cert' + cert: "" + + # Same as '--redis-key' + key: "" + + # Same as '--redis-tls' + tls: false + + # Same as '--cache-ttl' + ttl: 0s + +``` +## Clean options + +```yaml +clean: + # Same as '--all' + all: false + + # Same as '--checks-bundle' + checks-bundle: false + + # Same as '--java-db' + java-db: false + + # Same as '--scan-cache' + scan-cache: false + + # Same as '--vex-repo' + vex-repo: false + + # Same as '--vuln-db' + vuln-db: false + +``` +## Client/Server options + +```yaml +server: + # Same as '--server' + addr: "" + + # Same as '--custom-headers' + custom-headers: [] + + # Same as '--listen' + listen: "localhost:4954" + + # Same as '--token' + token: "" + + # Same as '--token-header' + token-header: "Trivy-Token" + +``` +## DB options + +```yaml +db: + # Same as '--download-java-db-only' + download-java-only: false + + # Same as '--download-db-only' + download-only: false + + # Same as '--java-db-repository' + java-repository: + - mirror.gcr.io/aquasec/trivy-java-db:1 + - ghcr.io/aquasecurity/trivy-java-db:1 + + # Same as '--skip-java-db-update' + java-skip-update: false + + # Same as '--no-progress' + no-progress: false + + # Same as '--db-repository' + repository: + - mirror.gcr.io/aquasec/trivy-db:2 + - ghcr.io/aquasecurity/trivy-db:2 + + # Same as '--skip-db-update' + skip-update: false + +``` +## Image options + +```yaml +image: + docker: + # Same as '--docker-host' + host: "" + + # Same as '--image-config-scanners' + image-config-scanners: [] + + # Same as '--input' + input: "" + + # Same as '--max-image-size' + max-size: "" + + # Same as '--platform' + platform: "" + + podman: + # Same as '--podman-host' + host: "" + + # Same as '--removed-pkgs' + removed-pkgs: false + + # Same as '--image-src' + source: + - docker + - containerd + - podman + - remote + +``` +## Kubernetes options + +```yaml +kubernetes: + # Same as '--burst' + burst: 10 + + # Same as '--disable-node-collector' + disableNodeCollector: false + + exclude: + # Same as '--exclude-nodes' + nodes: [] + + # Same as '--exclude-owned' + owned: false + + # Same as '--exclude-kinds' + excludeKinds: [] + + # Same as '--exclude-namespaces' + excludeNamespaces: [] + + # Same as '--include-kinds' + includeKinds: [] + + # Same as '--include-namespaces' + includeNamespaces: [] + + # Same as '--k8s-version' + k8s-version: "" + + # Same as '--kubeconfig' + kubeconfig: "" + + node-collector: + # Same as '--node-collector-imageref' + imageref: "ghcr.io/aquasecurity/node-collector:0.3.1" + + # Same as '--node-collector-namespace' + namespace: "trivy-temp" + + # Same as '--qps' + qps: 5 + + # Same as '--skip-images' + skipImages: false + + # Same as '--tolerations' + tolerations: [] + +``` +## License options + +```yaml +license: + # Same as '--license-confidence-level' + confidenceLevel: 0.9 + + forbidden: + - AGPL-1.0 + - AGPL-3.0 + - CC-BY-NC-1.0 + - CC-BY-NC-2.0 + - CC-BY-NC-2.5 + - CC-BY-NC-3.0 + - CC-BY-NC-4.0 + - CC-BY-NC-ND-1.0 + - CC-BY-NC-ND-2.0 + - CC-BY-NC-ND-2.5 + - CC-BY-NC-ND-3.0 + - CC-BY-NC-ND-4.0 + - CC-BY-NC-SA-1.0 + - CC-BY-NC-SA-2.0 + - CC-BY-NC-SA-2.5 + - CC-BY-NC-SA-3.0 + - CC-BY-NC-SA-4.0 + - Commons-Clause + - Facebook-2-Clause + - Facebook-3-Clause + - Facebook-Examples + - WTFPL + + # Same as '--license-full' + full: false + + # Same as '--ignored-licenses' + ignored: [] + + notice: + - AFL-1.1 + - AFL-1.2 + - AFL-2.0 + - AFL-2.1 + - AFL-3.0 + - Apache-1.0 + - Apache-1.1 + - Apache-2.0 + - Artistic-1.0-cl8 + - Artistic-1.0-Perl + - Artistic-1.0 + - Artistic-2.0 + - BSL-1.0 + - BSD-1-Clause + - BSD-2-Clause-FreeBSD + - BSD-2-Clause-NetBSD + - BSD-2-Clause + - BSD-3-Clause-Attribution + - BSD-3-Clause-Clear + - BSD-3-Clause-LBNL + - BSD-3-Clause + - BSD-4-Clause + - BSD-4-Clause-UC + - BSD-Protection + - CC-BY-1.0 + - CC-BY-2.0 + - CC-BY-2.5 + - CC-BY-3.0 + - CC-BY-4.0 + - FTL + - ISC + - ImageMagick + - Libpng + - Lil-1.0 + - Linux-OpenIB + - LPL-1.02 + - LPL-1.0 + - MS-PL + - MIT + - NCSA + - OpenSSL + - PHP-3.01 + - PHP-3.0 + - PIL + - Python-2.0 + - Python-2.0-complete + - PostgreSQL + - SGI-B-1.0 + - SGI-B-1.1 + - SGI-B-2.0 + - Unicode-DFS-2015 + - Unicode-DFS-2016 + - Unicode-TOU + - UPL-1.0 + - W3C-19980720 + - W3C-20150513 + - W3C + - X11 + - Xnet + - Zend-2.0 + - zlib-acknowledgement + - Zlib + - ZPL-1.1 + - ZPL-2.0 + - ZPL-2.1 + + permissive: [] + + reciprocal: + - APSL-1.0 + - APSL-1.1 + - APSL-1.2 + - APSL-2.0 + - CDDL-1.0 + - CDDL-1.1 + - CPL-1.0 + - EPL-1.0 + - EPL-2.0 + - FreeImage + - IPL-1.0 + - MPL-1.0 + - MPL-1.1 + - MPL-2.0 + - Ruby + + restricted: + - BCL + - CC-BY-ND-1.0 + - CC-BY-ND-2.0 + - CC-BY-ND-2.5 + - CC-BY-ND-3.0 + - CC-BY-ND-4.0 + - CC-BY-SA-1.0 + - CC-BY-SA-2.0 + - CC-BY-SA-2.5 + - CC-BY-SA-3.0 + - CC-BY-SA-4.0 + - GPL-1.0 + - GPL-2.0 + - GPL-2.0-with-autoconf-exception + - GPL-2.0-with-bison-exception + - GPL-2.0-with-classpath-exception + - GPL-2.0-with-font-exception + - GPL-2.0-with-GCC-exception + - GPL-3.0 + - GPL-3.0-with-autoconf-exception + - GPL-3.0-with-GCC-exception + - LGPL-2.0 + - LGPL-2.1 + - LGPL-3.0 + - NPL-1.0 + - NPL-1.1 + - OSL-1.0 + - OSL-1.1 + - OSL-2.0 + - OSL-2.1 + - OSL-3.0 + - QPL-1.0 + - Sleepycat + + unencumbered: + - CC0-1.0 + - Unlicense + - 0BSD + +``` +## Misconfiguration options + +```yaml +ansible: + # Same as '--ansible-extra-vars' + extra-vars: [] + + # Same as '--ansible-inventory' + inventories: [] + + # Same as '--ansible-playbook' + playbooks: [] + +misconfiguration: + # Same as '--checks-bundle-repository' + checks-bundle-repository: "mirror.gcr.io/aquasec/trivy-checks:2" + + cloudformation: + # Same as '--cf-params' + params: [] + + # Same as '--config-file-schemas' + config-file-schemas: [] + + helm: + # Same as '--helm-api-versions' + api-versions: [] + + # Same as '--helm-kube-version' + kube-version: "" + + # Same as '--helm-set' + set: [] + + # Same as '--helm-set-file' + set-file: [] + + # Same as '--helm-set-string' + set-string: [] + + # Same as '--helm-values' + values: [] + + # Same as '--include-non-failures' + include-non-failures: false + + # Same as '--raw-config-scanners' + raw-config-scanners: [] + + # Same as '--render-cause' + render-cause: [] + + # Same as '--misconfig-scanners' + scanners: + - azure-arm + - cloudformation + - dockerfile + - helm + - kubernetes + - terraform + - terraformplan-json + - terraformplan-snapshot + - ansible + + terraform: + # Same as '--tf-exclude-downloaded-modules' + exclude-downloaded-modules: false + + # Same as '--tf-vars' + vars: [] + +``` +## Module options + +```yaml +module: + # Same as '--module-dir' + dir: "$HOME/.trivy/modules" + + # Same as '--enable-modules' + enable-modules: [] + +``` +## Package options + +```yaml +pkg: + # Same as '--include-dev-deps' + include-dev-deps: false + + # Same as '--pkg-relationships' + relationships: + - unknown + - root + - workspace + - direct + - indirect + + # Same as '--pkg-types' + types: + - os + - library + +``` +## Registry options + +```yaml +registry: + mirrors: + + # Same as '--password' + password: [] + + # Same as '--password-stdin' + password-stdin: false + + # Same as '--registry-token' + token: "" + + # Same as '--username' + username: [] + +``` +## Rego options + +```yaml +rego: + # Same as '--config-check' + check: [] + + # Same as '--config-data' + data: [] + + # Same as '--rego-error-limit' + error-limit: 10 + + # Same as '--include-deprecated-checks' + include-deprecated-checks: false + + # Same as '--check-namespaces' + namespaces: [] + + # Same as '--skip-check-update' + skip-check-update: false + + # Same as '--trace-rego' + trace: false + +``` +## Report options + +```yaml +# Same as '--dependency-tree' +dependency-tree: false + +# Same as '--exit-code' +exit-code: 0 + +# Same as '--exit-on-eol' +exit-on-eol: 0 + +# Same as '--format' +format: "table" + +# Same as '--ignore-policy' +ignore-policy: "" + +# Same as '--ignorefile' +ignorefile: ".trivyignore" + +# Same as '--list-all-pkgs' +list-all-pkgs: true + +# Same as '--output' +output: "" + +# Same as '--output-plugin-arg' +output-plugin-arg: "" + +# Same as '--report' +report: "all" + +scan: + # Same as '--compliance' + compliance: "" + + # Same as '--show-suppressed' + show-suppressed: false + +# Same as '--severity' +severity: + - UNKNOWN + - LOW + - MEDIUM + - HIGH + - CRITICAL + +# Same as '--table-mode' +table-mode: + - summary + - detailed + +# Same as '--template' +template: "" + +``` +## Repository options + +```yaml +repository: + # Same as '--branch' + branch: "" + + # Same as '--commit' + commit: "" + + # Same as '--tag' + tag: "" + +``` +## Scan options + +```yaml +scan: + # Same as '--detection-priority' + detection-priority: "precise" + + # Same as '--disable-telemetry' + disable-telemetry: false + + # Same as '--distro' + distro: "" + + # Same as '--file-patterns' + file-patterns: [] + + # Same as '--offline-scan' + offline: false + + # Same as '--parallel' + parallel: 5 + + # Same as '--rekor-url' + rekor-url: "https://rekor.sigstore.dev" + + # Same as '--sbom-sources' + sbom-sources: [] + + # Same as '--scanners' + scanners: + - vuln + - secret + + # Same as '--skip-dirs' + skip-dirs: [] + + # Same as '--skip-files' + skip-files: [] + + # Same as '--skip-version-check' + skip-version-check: false + +``` +## Secret options + +```yaml +secret: + # Same as '--secret-config' + config: "trivy-secret.yaml" + +``` +## Vulnerability options + +```yaml +vulnerability: + # Same as '--ignore-status' + ignore-status: [] + + # Same as '--ignore-unfixed' + ignore-unfixed: false + + # Same as '--vuln-severity-source' + severity-source: + - auto + + # Same as '--skip-vex-repo-update' + skip-vex-repo-update: false + + # Same as '--vex' + vex: [] + +``` +[example]: https://github.com/aquasecurity/trivy/blob/{{ git.tag }}/examples/trivy-conf/trivy.yaml +[schema]: https://github.com/aquasecurity/trivy/blob/{{ git.tag }}/schema/trivy-config.json \ No newline at end of file diff --git a/docs/guide/references/modes/client-server.md b/docs/guide/references/modes/client-server.md new file mode 100644 index 000000000000..747acf42884b --- /dev/null +++ b/docs/guide/references/modes/client-server.md @@ -0,0 +1,345 @@ +# Client/Server + +Trivy has client/server mode. Trivy server has vulnerability database and Trivy client doesn't have to download vulnerability database. It is useful if you want to scan images or files at multiple locations and do not want to download the database at every location. + +| Client/Server Mode | Image | Rootfs | Filesystem | Repository | Config | K8s | +|:------------------:|:-----:|:------:|:----------:|:----------:|:------:|:---:| +| Supported | ✅ | ✅ | ✅ | ✅ | - | - | + +Some scanners run on the client side, even in client/server mode. + +| Scanner | Run on Client or Server | +|:----------------:|:-----------------------:| +| Vulnerability | Server | +| Misconfiguration | Client[^1] | +| Secret | Client[^2] | +| License | Server | + +!!! note + Scanning of misconfigurations and secrets is performed on the client side (as in standalone mode). + Otherwise, the client would need to send files to the server that may contain sensitive information. + +## Server +At first, you need to launch Trivy server. It downloads vulnerability database automatically and continue to fetch the latest DB in the background. +``` +$ trivy server --listen localhost:8080 +2019-12-12T15:17:06.551+0200 INFO Need to update DB +2019-12-12T15:17:56.706+0200 INFO Reopening DB... +2019-12-12T15:17:56.707+0200 INFO Listening localhost:8080... +``` + +If you want to accept a connection from outside, you have to specify `0.0.0.0` or your ip address, not `localhost`. + +``` +$ trivy server --listen 0.0.0.0:8080 +``` + +## Remote image scan +Then, specify the server address for `image` command. +``` +$ trivy image --server http://localhost:8080 alpine:3.10 +``` +**Note**: It's important to specify the protocol (http or https). + +
+Result + +``` +alpine:3.10 (alpine 3.10.2) +=========================== +Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 2, HIGH: 0, CRITICAL: 0) + ++---------+------------------+----------+-------------------+---------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | ++---------+------------------+----------+-------------------+---------------+ +| openssl | CVE-2019-1549 | MEDIUM | 1.1.1c-r0 | 1.1.1d-r0 | ++ +------------------+ + + + +| | CVE-2019-1563 | | | | ++ +------------------+----------+ + + +| | CVE-2019-1547 | LOW | | | ++---------+------------------+----------+-------------------+---------------+ +``` +
+ +## Remote scan of local filesystem +Also, there is a way to scan local file system: +```shell +$ trivy fs --server http://localhost:8080 --severity CRITICAL ./integration/testdata/fixtures/fs/pom/ +``` +**Note**: It's important to specify the protocol (http or https). +
+Result + +``` +pom.xml (pom) +============= +Total: 24 (CRITICAL: 24) + ++---------------------------------------------+------------------+----------+-------------------+--------------------------------+---------------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++---------------------------------------------+------------------+----------+-------------------+--------------------------------+---------------------------------------+ +| com.fasterxml.jackson.core:jackson-databind | CVE-2017-17485 | CRITICAL | 2.9.1 | 2.8.11, 2.9.4 | jackson-databind: Unsafe | +| | | | | | deserialization due to | +| | | | | | incomplete black list (incomplete | +| | | | | | fix for CVE-2017-15095)... | +| | | | | | -->avd.aquasec.com/nvd/cve-2017-17485 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2018-11307 | | | 2.7.9.4, 2.8.11.2, 2.9.6 | jackson-databind: Potential | +| | | | | | information exfiltration with | +| | | | | | default typing, serialization | +| | | | | | gadget from MyBatis | +| | | | | | -->avd.aquasec.com/nvd/cve-2018-11307 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2018-14718 | | | 2.6.7.2, 2.9.7 | jackson-databind: arbitrary code | +| | | | | | execution in slf4j-ext class | +| | | | | | -->avd.aquasec.com/nvd/cve-2018-14718 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2018-14719 | | | | jackson-databind: arbitrary | +| | | | | | code execution in blaze-ds-opt | +| | | | | | and blaze-ds-core classes | +| | | | | | -->avd.aquasec.com/nvd/cve-2018-14719 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2018-14720 | | | | jackson-databind: exfiltration/XXE | +| | | | | | in some JDK classes | +| | | | | | -->avd.aquasec.com/nvd/cve-2018-14720 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2018-14721 | | | | jackson-databind: server-side request | +| | | | | | forgery (SSRF) in axis2-jaxws class | +| | | | | | -->avd.aquasec.com/nvd/cve-2018-14721 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2018-19360 | | | 2.6.7.3, 2.7.9.5, 2.8.11.3, | jackson-databind: improper | +| | | | | 2.9.8 | polymorphic deserialization | +| | | | | | in axis2-transport-jms class | +| | | | | | -->avd.aquasec.com/nvd/cve-2018-19360 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2018-19361 | | | | jackson-databind: improper | +| | | | | | polymorphic deserialization | +| | | | | | in openjpa class | +| | | | | | -->avd.aquasec.com/nvd/cve-2018-19361 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2018-19362 | | | | jackson-databind: improper | +| | | | | | polymorphic deserialization | +| | | | | | in jboss-common-core class | +| | | | | | -->avd.aquasec.com/nvd/cve-2018-19362 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2018-7489 | | | 2.7.9.3, 2.8.11.1, 2.9.5 | jackson-databind: incomplete fix | +| | | | | | for CVE-2017-7525 permits unsafe | +| | | | | | serialization via c3p0 libraries | +| | | | | | -->avd.aquasec.com/nvd/cve-2018-7489 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2019-14379 | | | 2.7.9.6, 2.8.11.4, 2.9.9.2 | jackson-databind: default | +| | | | | | typing mishandling leading | +| | | | | | to remote code execution | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-14379 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2019-14540 | | | 2.9.10 | jackson-databind: | +| | | | | | Serialization gadgets in | +| | | | | | com.zaxxer.hikari.HikariConfig | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-14540 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2019-14892 | | | 2.6.7.3, 2.8.11.5, 2.9.10 | jackson-databind: Serialization | +| | | | | | gadgets in classes of the | +| | | | | | commons-configuration package | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-14892 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2019-14893 | | | 2.8.11.5, 2.9.10 | jackson-databind: | +| | | | | | Serialization gadgets in | +| | | | | | classes of the xalan package | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-14893 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2019-16335 | | | 2.9.10 | jackson-databind: | +| | | | | | Serialization gadgets in | +| | | | | | com.zaxxer.hikari.HikariDataSource | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-16335 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2019-16942 | | | 2.9.10.1 | jackson-databind: | +| | | | | | Serialization gadgets in | +| | | | | | org.apache.commons.dbcp.datasources.* | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-16942 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2019-16943 | | | | jackson-databind: | +| | | | | | Serialization gadgets in | +| | | | | | com.p6spy.engine.spy.P6DataSource | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-16943 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2019-17267 | | | 2.9.10 | jackson-databind: Serialization | +| | | | | | gadgets in classes of | +| | | | | | the ehcache package | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-17267 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2019-17531 | | | 2.9.10.1 | jackson-databind: | +| | | | | | Serialization gadgets in | +| | | | | | org.apache.log4j.receivers.db.* | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-17531 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2019-20330 | | | 2.8.11.5, 2.9.10.2 | jackson-databind: lacks | +| | | | | | certain net.sf.ehcache blocking | +| | | | | | -->avd.aquasec.com/nvd/cve-2019-20330 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2020-8840 | | | 2.7.9.7, 2.8.11.5, 2.9.10.3 | jackson-databind: Lacks certain | +| | | | | | xbean-reflect/JNDI blocking | +| | | | | | -->avd.aquasec.com/nvd/cve-2020-8840 | ++ +------------------+ + +--------------------------------+---------------------------------------+ +| | CVE-2020-9546 | | | 2.7.9.7, 2.8.11.6, 2.9.10.4 | jackson-databind: Serialization | +| | | | | | gadgets in shaded-hikari-config | +| | | | | | -->avd.aquasec.com/nvd/cve-2020-9546 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2020-9547 | | | | jackson-databind: Serialization | +| | | | | | gadgets in ibatis-sqlmap | +| | | | | | -->avd.aquasec.com/nvd/cve-2020-9547 | ++ +------------------+ + + +---------------------------------------+ +| | CVE-2020-9548 | | | | jackson-databind: Serialization | +| | | | | | gadgets in anteros-core | +| | | | | | -->avd.aquasec.com/nvd/cve-2020-9548 | ++---------------------------------------------+------------------+----------+-------------------+--------------------------------+---------------------------------------+ +``` +
+ +## Remote scan of root filesystem +Also, there is a way to scan root file system: +```shell +$ trivy rootfs --server http://localhost:8080 --severity CRITICAL /tmp/rootfs +``` +**Note**: It's important to specify the protocol (http or https). +
+Result + +``` +/tmp/rootfs (alpine 3.10.2) + +Total: 1 (CRITICAL: 1) + +┌───────────┬────────────────┬──────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├───────────┼────────────────┼──────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤ +│ apk-tools │ CVE-2021-36159 │ CRITICAL │ 2.10.4-r2 │ 2.10.7-r0 │ libfetch before 2021-07-26, as used in apk-tools, xbps, and │ +│ │ │ │ │ │ other products, mishandles... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-36159 │ +└───────────┴────────────────┴──────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────────┘ +``` +
+ +## Remote scan of git repository +Also, there is a way to scan remote git repository: +```shell +$ trivy repo https://github.com/knqyf263/trivy-ci-test --server http://localhost:8080 +``` +**Note**: It's important to specify the protocol (http or https). +
+Result + +``` +Cargo.lock (cargo) +================== +Total: 5 (UNKNOWN: 0, LOW: 0, MEDIUM: 2, HIGH: 2, CRITICAL: 0) + +┌───────────┬─────────────────────┬──────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├───────────┼─────────────────────┼──────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤ +│ ammonia │ CVE-2019-15542 │ HIGH │ 1.9.0 │ 2.1.0 │ Uncontrolled recursion in ammonia │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-15542 │ +│ ├─────────────────────┼──────────┤ ├───────────────┼─────────────────────────────────────────────────────────────┤ +│ │ CVE-2021-38193 │ MEDIUM │ │ 2.1.3, 3.1.0 │ An issue was discovered in the ammonia crate before 3.1.0 │ +│ │ │ │ │ │ for Rust.... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-38193 │ +├───────────┼─────────────────────┤ ├───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤ +│ smallvec │ CVE-2019-15551 │ │ 0.6.9 │ 0.6.10 │ An issue was discovered in the smallvec crate before 0.6.10 │ +│ │ │ │ │ │ for Rust.... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-15551 │ +│ ├─────────────────────┼──────────┤ ├───────────────┼─────────────────────────────────────────────────────────────┤ +│ │ CVE-2018-25023 │ HIGH │ │ 0.6.13 │ An issue was discovered in the smallvec crate before 0.6.13 │ +│ │ │ │ │ │ for Rust.... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2018-25023 │ +│ ├─────────────────────┼──────────┤ │ ├─────────────────────────────────────────────────────────────┤ +│ │ GHSA-66p5-j55p-32r9 │ MEDIUM │ │ │ smallvec creates uninitialized value of any type │ +│ │ │ │ │ │ https://github.com/advisories/GHSA-66p5-j55p-32r9 │ +└───────────┴─────────────────────┴──────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────────┘ + +Pipfile.lock (pipenv) +===================== +Total: 8 (UNKNOWN: 0, LOW: 0, MEDIUM: 6, HIGH: 2, CRITICAL: 0) + +┌─────────────────────┬────────────────┬──────────┬───────────────────┬────────────────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├─────────────────────┼────────────────┼──────────┼───────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ celery │ CVE-2021-23727 │ HIGH │ 4.3.0 │ 5.2.2 │ celery: stored command injection vulnerability may allow │ +│ │ │ │ │ │ privileges escalation │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-23727 │ +├─────────────────────┼────────────────┤ ├───────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ django │ CVE-2019-6975 │ │ 2.0.9 │ 1.11.19, 2.0.12, 2.1.7 │ python-django: memory exhaustion in │ +│ │ │ │ │ │ django.utils.numberformat.format() │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-6975 │ +│ ├────────────────┼──────────┤ ├────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2019-3498 │ MEDIUM │ │ 1.11.18, 2.0.10, 2.1.5 │ python-django: Content spoofing via URL path in default 404 │ +│ │ │ │ │ │ page │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-3498 │ +│ ├────────────────┤ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2021-33203 │ │ │ 2.2.24, 3.1.12, 3.2.4 │ django: Potential directory traversal via ``admindocs`` │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-33203 │ +├─────────────────────┼────────────────┤ ├───────────────────┼────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ urllib3 │ CVE-2019-11324 │ │ 1.24.1 │ 1.24.2 │ python-urllib3: Certification mishandle when error should be │ +│ │ │ │ │ │ thrown │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-11324 │ +│ ├────────────────┤ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2021-33503 │ │ │ 1.26.5 │ python-urllib3: ReDoS in the parsing of authority part of │ +│ │ │ │ │ │ URL │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-33503 │ +│ ├────────────────┼──────────┤ ├────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2019-11236 │ MEDIUM │ │ 1.24.3 │ python-urllib3: CRLF injection due to not encoding the │ +│ │ │ │ │ │ '\r\n' sequence leading to... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-11236 │ +│ ├────────────────┤ │ ├────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2020-26137 │ │ │ 1.25.9 │ python-urllib3: CRLF injection via HTTP request method │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-26137 │ +└─────────────────────┴────────────────┴──────────┴───────────────────┴────────────────────────┴──────────────────────────────────────────────────────────────┘ +``` +
+ +## Authentication + +``` +$ trivy server --listen localhost:8080 --token dummy +``` + +``` +$ trivy image --server http://localhost:8080 --token dummy alpine:3.10 +``` + +## Endpoints + +### Health +Checks whether the Trivy server is running. Authentication is not required. + +Example request: +```bash +curl -s 0.0.0.0:8080/healthz +ok +``` + +Returns the `200 OK` status if the request was successful. +### Version + +Returns the version of the Trivy server and the vulnerability database metadata. Authentication is not required. + +Example request: +```bash +curl -s 0.0.0.0:8080/version | jq +{ + "Version": "dev", + "VulnerabilityDB": { + "Version": 2, + "NextUpdate": "2023-07-25T14:15:29.876639806Z", + "UpdatedAt": "2023-07-25T08:15:29.876640206Z", + "DownloadedAt": "2023-07-25T09:36:25.599004Z" + } +} +``` + +Returns the `200 OK` status if the request was successful. + +## Architecture + +![architecture](../../../imgs/client-server.png) + +[^1]: The checks bundle is also downloaded on the client side. +[^2]: The scan result with masked secrets is sent to the server \ No newline at end of file diff --git a/docs/guide/references/modes/standalone.md b/docs/guide/references/modes/standalone.md new file mode 100644 index 000000000000..c36776af9009 --- /dev/null +++ b/docs/guide/references/modes/standalone.md @@ -0,0 +1,16 @@ +# Standalone + +`trivy image`, `trivy filesystem`, and `trivy repo` works as standalone mode. + +## Image + +![standalone](../../../imgs/image.png) + +## Filesystem + +![fs](../../../imgs/fs.png) + +## Git Repository + +![repo](../../../imgs/repo.png) + diff --git a/docs/guide/references/terminology.md b/docs/guide/references/terminology.md new file mode 100644 index 000000000000..e3611962f7d4 --- /dev/null +++ b/docs/guide/references/terminology.md @@ -0,0 +1,160 @@ +# Terminology + +This page explains the terminology system used in Trivy, helping users understand the specific terms and concepts unique to the Trivy ecosystem. + +**Inclusion Criteria** + +1. Core Components of Trivy + - Primary features such as Scanner, Target + - Essential components such as Scan Assets (trivy-db, trivy-java-db) + - Components that users directly interact with + +2. Trivy-specific Terms + - Terms unique to Trivy (e.g., VEX Hub) + - Terms that have special meaning in Trivy's context (e.g., Plugin, Module) + +**Exclusion Criteria** + +1. General Terms + - Common security/technical terms (e.g., CVE, CVSS, Container, Registry) + - Standard industry terminology + +2. Implementation Details + - Internal workings of components + - Usage instructions (these belong in feature documentation) + + +## Core Concepts + +### Target +Types of artifacts that Trivy can scan, like container images and filesystem. + +### Scanner +Trivy's built-in security scanning engines. Trivy has four main scanners: + +- [Vulnerability Scanner](../scanner/vulnerability.md) +- [Misconfiguration Scanner](../scanner/misconfiguration/index.md) +- [Secret Scanner](../scanner/secret.md) +- [License Scanner](../scanner/license.md) + +!!! note + SBOM is not a scanner but an output format option. + +### Scan Assets +External data that Trivy downloads (if needed for scanner) and uses during scanning: + +- [Vulnerability Database (Trivy DB, trivy-db)](#vulnerability-database-trivy-db-trivy-db): Database containing vulnerability information +- [Java Index Database (Trivy Java DB, trivy-java-db)](#java-index-database-trivy-java-db-trivy-java-db): Database for Java artifact identification +- [Checks Bundle (trivy-checks)](#checks-bundle): Archive containing misconfiguration detection rules +- [VEX Repository](#vex-repository): Repository containing VEX documents + +## Vulnerability Scanning + +### Vulnerability Database (Trivy DB, trivy-db) +The core vulnerability database required for vulnerability detection. +Contains comprehensive vulnerability information for multiple ecosystems. +Distributed via OCI registry. + +Managed at https://github.com/aquasecurity/trivy-db. + +The vulnerability database is built from a GitHub repository that collects and stores vulnerability information from various data sources. +This repository serves as the foundation for building the Trivy DB. + +Managed at: + +- https://github.com/aquasecurity/vuln-list +- https://github.com/aquasecurity/vuln-list-nvd +- https://github.com/aquasecurity/vuln-list-redhat +- https://github.com/aquasecurity/vuln-list-debian +- etc. + +### Java Index Database (Trivy Java DB, trivy-java-db) +Specialized database used for identifying Java libraries and their components during JAR/WAR/PAR/EAR scanning. +Distributed via OCI registry. + +Managed at https://github.com/aquasecurity/trivy-java-db. + + +## Misconfiguration Scanning +When the context does not clearly indicate these terms are related to misconfiguration scanning, they may be prefixed with "Misconfiguration" for clarity. +For example, "Check" may be referred to as "Misconfiguration Check", and "Checks Bundle" as "Misconfiguration Checks Bundle". + +### Check +A Rego file that defines rules for detecting misconfigurations in various types of IaC files. + +### Built-in Checks +Default set of checks distributed through [the trivy-checks repository](https://github.com/aquasecurity/trivy-checks), providing standard security and configuration best practices. + +### Checks Bundle +A tar.gz archive containing [the built-in checks](#built-in-checks), distributed via OCI registry. + +## Secret Scanning + +### Rule +Pattern matching rules used to detect hardcoded secrets and sensitive information. +Each rule consists of: + +- Metadata (ID, Category, Title, etc.) +- Regular expressions for matching sensitive patterns +- Additional context for detection accuracy + +## Kubernetes Integration + +### KBOM (Kubernetes Bill of Materials) +A specialized SBOM format for Kubernetes clusters that includes detailed information about the cluster's components. + +## VEX (Vulnerability Exploitability eXchange) + +### VEX Repository +A repository system that stores VEX documents following [the VEX Repository Specification](https://github.com/aquasecurity/vex-repo-spec). +VEX repositories help users manage and share information about vulnerability applicability and exploitability. + +For detailed information about VEX repositories, see [the document](../supply-chain/vex/repo.md). + +### VEX Hub +The default VEX repository managed by Aqua Security at https://github.com/aquasecurity/vexhub. +It primarily aggregates VEX documents published by package maintainers in their source repositories. +VEX Hub serves as a central point for collecting and distributing vulnerability applicability information for OSS projects. + +## Cache System + +### Cache Types +The cache directory contains several distinct types of data: + +- [Vulnerability Database](#vulnerability-database-trivy-db-trivy-db) +- [Java Index Database](#java-index-database-trivy-java-db-trivy-java-db) +- [Misconfiguration Checks](#misconfiguration-scanning) +- [VEX Repositories](#vex-repository) +- [Scan Cache](#scan-cache) + +### Asset Cache +Downloaded assets like vulnerability databases and Java index databases. + +### Scan Cache +A caching mechanism that stores analysis results from previous scans to speed up subsequent scans. +For container image scanning, the scan cache stores analysis results including package names and versions per layer. + +For detailed information about caching, see [the document](../configuration/cache.md). + +## Plugin System + +### Plugin +An add-on tool that integrates with Trivy to extend its core functionality. +Plugins can be written in any programming language and integrate seamlessly with Trivy CLI, appearing in Trivy help and subcommands. +They can be installed and removed independently without affecting the core Trivy installation. + +For detailed information about plugins, see [the document](../plugin/index.md). + +### Plugin Index (trivy-plugin-index) +A centralized registry that lists available Trivy plugins, managed at https://github.com/aquasecurity/trivy-plugin-index. +The index maintains a curated list of official and community plugins, providing metadata such as plugin names, descriptions, and maintainers. +It enables plugin discovery through the `trivy plugin search` command and facilitates automatic plugin installation and updates. + +For detailed information about the plugin index, see [the document](../plugin/user-guide.md). + +## Module System +### Module +A WebAssembly-based extension mechanism that allows custom scanning logic without modifying the Trivy binary. +Modules can modify scan results by analyzing files or post-processing results. + +For detailed information about modules, see [the document](../advanced/modules.md). \ No newline at end of file diff --git a/docs/guide/references/troubleshooting.md b/docs/guide/references/troubleshooting.md new file mode 100644 index 000000000000..e696b610575c --- /dev/null +++ b/docs/guide/references/troubleshooting.md @@ -0,0 +1,376 @@ +# Troubleshooting + +## Scan +### Timeout + +!!! error + ``` bash + $ trivy image ... + ... + analyze error: timeout: context deadline exceeded + ``` + +Your scan may time out. Java takes a particularly long time to scan. Try increasing the value of the `--timeout` option such as `--timeout 15m`. + +### Unable to initialize an image scanner + +!!! error + ```bash + $ trivy image ... + ... + 2024-01-19T08:15:33.288Z FATAL image scan error: scan error: unable to initialize a scanner: unable to initialize an image scanner: 4 errors occurred: + * docker error: unable to inspect the image (ContainerImageName): Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? + * containerd error: containerd socket not found: /run/containerd/containerd.sock + * podman error: unable to initialize Podman client: no podman socket found: stat podman/podman.sock: no such file or directory + * remote error: GET https://index.docker.io/v2/ContainerImageName: MANIFEST_UNKNOWN: manifest unknown; unknown tag=0.1 + ``` + +It means Trivy is unable to find the container image in the following places: + +* Docker Engine +* containerd +* Podman +* A remote registry + +Please see error messages for details of each error. + +Common mistakes include the following, depending on where you are pulling images from: + +#### Common +- Typos in the image name + - Common mistake :) +- Forgetting to specify the registry + - By default, it is considered to be Docker Hub ( `index.docker.io` ). + +#### Docker Engine +- Incorrect Docker host + - If the Docker daemon's socket path is not `/var/run/docker.sock`, you need to specify the `--docker-host` flag or the `DOCKER_HOST` environment variable. + The same applies when using TCP; you must specify the correct host address. + +#### containerd +- Incorrect containerd address + - If you are using a non-default path, you need to specify the `CONTAINERD_ADDRESS` environment variable. + Please refer to [this documentation](../target/container_image.md#containerd). +- Incorrect namespace + - If you are using a non-default namespace, you need to specify the `CONTAINERD_NAMESPACE` environment variable. + Please refer to [this documentation](../target/container_image.md#containerd). + - +#### Podman +- Podman socket configuration + - You need to enable the Podman socket. Please refer to [this documentation](../target/container_image.md#podman). + +#### Container Registry +- Unauthenticated + - If you are using a private container registry, you need to authenticate. Please refer to [this documentation](../advanced/private-registries/index.md). +- Using a proxy + - If you are using a proxy within your network, you need to correctly set the `HTTP_PROXY`, `HTTPS_PROXY`, etc., environment variables. +- Use of a self-signed certificate in the registry + - Because certificate verification will fail, you need to either trust that certificate or use the `--insecure` flag (not recommended in production). + +### Certification + +!!! error + Error: x509: certificate signed by unknown authority + +`TRIVY_INSECURE` can be used to allow insecure connections to a container registry when using SSL. + +``` +$ TRIVY_INSECURE=true trivy image [YOUR_IMAGE] +``` + +If you need to trust a custom CA certificate, you can provide a PEM-encoded bundle. + +=== "Unix (except macOS)" + + You can specify the location of your certificate using the `SSL_CERT_FILE` or `SSL_CERT_DIR` environment variables. + + ```bash + $ SSL_CERT_FILE=/path/to/ca.pem trivy image [YOUR_IMAGE] + ``` + + ```bash + $ SSL_CERT_DIR=/path/to/certs trivy image [YOUR_IMAGE] + ``` + +=== "All systems" + + Use the `--cacert` flag to point Trivy to a PEM-encoded CA certificate file, regardless of the operating system. + + ```bash + $ trivy image --cacert /path/to/ca.pem [YOUR_IMAGE] + ``` + +### GitHub Rate limiting +Trivy uses GitHub API for [VEX repositories](../supply-chain/vex/repo.md). + +!!! error + ``` bash + $ trivy image --vex repo ... + ... + API rate limit exceeded for xxx.xxx.xxx.xxx. + ``` + +Specify GITHUB_TOKEN for [authentication](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28) + +``` +$ GITHUB_TOKEN=XXXXXXXXXX trivy image --vex repo [YOUR_IMAGE] +``` + +!!! note + `GITHUB_TOKEN` doesn't help with the rate limit for the vulnerability database and other assets. + See https://github.com/aquasecurity/trivy/discussions/8009 + +### Unable to open JAR files + +!!! error + ``` bash + $ trivy image ... + ... + failed to analyze file: failed to analyze usr/lib/jvm/java-1.8-openjdk/lib/tools.jar: unable to open usr/lib/jvm/java-1.8-openjdk/lib/tools.jar: failed to open: unable to read the file: stream error: stream ID 9; PROTOCOL_ERROR; received from peer + ``` + +Currently, we're investigating this issue. As a temporary mitigation, you may be able to avoid this issue by downloading the Java DB in advance. + +```shell +$ trivy image --download-java-db-only +2023-02-01T16:57:04.322+0900 INFO Downloading the Java DB... +$ trivy image [YOUR_JAVA_IMAGE] +``` + +### Cache lock errors + +!!! error + ``` + cache may be in use by another process + ``` + +Trivy's vulnerability database is opened in read-only mode, so it does not cause lock issues. Lock errors occur only when using filesystem cache for scan cache storage. + +Filesystem cache uses BoltDB internally, which creates file locks to prevent data corruption. As stated in the BoltDB documentation: + +> Please note that Bolt obtains a file lock on the data file so multiple processes cannot open the same database at the same time. Opening an already open Bolt database will cause it to hang until the other process closes it. + +Reference: [BoltDB README](https://github.com/boltdb/bolt#opening-a-database) + +If you're using memory cache (default for some commands like `fs`, `rootfs`, `config`, and `sbom`) or external cache (Redis), you will not encounter lock errors. Lock issues only occur when using filesystem cache with multiple concurrent processes. +See [Cache Backend](../configuration/cache.md#scan-cache-backend) for more details. + +These errors occur when: + +- Multiple Trivy processes try to use the same filesystem cache directory simultaneously +- A previous Trivy process did not shut down cleanly +- Trivy server is running with filesystem cache and holding a lock on the cache + +#### Solutions + +**Solution 1: Use memory cache or Redis cache** (Recommended) + +Memory cache is the default for some commands (e.g., `fs`, `rootfs`, `config`, `sbom`). For other commands like image scanning, you can use `--cache-backend memory` to enable concurrent execution: + +```bash +$ trivy image --cache-backend memory debian:11 & +$ trivy image --cache-backend memory debian:12 & +``` + +Note that memory cache does not persist scan results, so subsequent scans will take longer as layers need to be scanned again each time. + +For server mode or persistent cache with concurrent access, use Redis cache: + +```bash +$ trivy server --cache-backend redis://localhost:6379 +``` + +**Solution 2: Terminate conflicting processes** + +If you need to use filesystem cache, check for running Trivy processes and terminate them: + +```bash +$ ps aux | grep trivy +$ kill [process_id] +``` + +**Solution 3: Use different cache directories** + +If you must run multiple Trivy processes with filesystem cache, specify different cache directories for each process: + +```bash +$ trivy image --cache-dir /tmp/trivy-cache-1 debian:11 & +$ trivy image --cache-dir /tmp/trivy-cache-2 debian:12 & +``` + +Note that each cache directory will download its own copy of the vulnerability database and other scan assets, which will increase network traffic and storage usage. + +### Multiple Trivy servers + +!!! error + ``` + $ trivy image --server http://xxx.com:xxxx test-image + ... + - twirp error internal: failed scan, test-image: failed to apply layers: layer cache missing: sha256:***** + ``` +To run multiple Trivy servers, you need to use Redis as the cache backend so that those servers can share the cache. +Follow [this instruction][redis-cache] to do so. + + +### Problems with `/tmp` on remote Git repository scans + +!!! error + FATAL repository scan error: scan error: unable to initialize a scanner: unable to initialize a filesystem scanner: git clone error: write /tmp/fanal-remote... + +Trivy clones remote Git repositories under the `/tmp` directory before scanning them. If `/tmp` doesn't work for you, you can change it by setting the `TMPDIR` environment variable. + +Try: + +``` +$ TMPDIR=/my/custom/path trivy repo ... +``` + +### Running out of space during image scans + +!!! error + ``` bash + image scan failed: + failed to copy the image: + write /tmp/fanal-3323732142: no space left on device + ``` + +Trivy uses a temporary directory during image scans. +The directory path would be determined as follows: + +- On Unix systems: Use `$TMPDIR` if non-empty, else `/tmp`. +- On Windows: Uses GetTempPath, returning the first non-empty value from `%TMP%`, `%TEMP%`, `%USERPROFILE%`, or the Windows directory. + +See [this documentation](https://golang.org/pkg/os/#TempDir) for more details. + +If the image is large or the temporary directory has insufficient space, the scan will fail. +You can configure the directory path to redirect Trivy to a directory with adequate storage. +On Unix systems, you can set the `$TMPDIR` environment variable. + +``` +$ TMPDIR=/my/custom/path trivy image ... +``` + +When scanning images from a container registry, Trivy processes each layer by streaming, loading only the necessary files for the scan into memory and discarding unnecessary files. +If a layer contains large files that are necessary for the scan (such as JAR files or binary files), Trivy saves them to a temporary directory (e.g. $TMPDIR) on local storage to avoid increased memory consumption. +Although these files are deleted after the scan is complete, they can temporarily increase disk consumption and potentially exhaust storage. +In such cases, there are currently three workarounds: + +1. Use a temporary directory with sufficient capacity + + This is the same as explained above. + +2. Specify a small value for `--parallel` + + By default, multiple layers are processed in parallel. + If each layer contains large files, disk space may be consumed rapidly. + By specifying a small value such as `--parallel 1`, parallelism is reduced, which can mitigate the issue. + +3. Specify `--skip-files` or `--skip-dirs` + + If the container image contains large files that do not need to be scanned, you can skip their processing by specifying --skip-files or --skip-dirs. + For more details, please refer to [this documentation](../configuration/skipping.md). + +## DB +### Old DB schema + +!!! error + --skip-update cannot be specified with the old DB schema. + +Trivy v0.23.0 or later requires Trivy DB v2. Please update your local database or follow [the instruction of air-gapped environment][air-gapped]. + +### Error downloading vulnerability DB + +!!! error + FATAL failed to download vulnerability DB + +If Trivy is running behind corporate firewall, refer to the necessary connectivity requirements as described [here][network]. + +### Denied + +!!! error + GET https://ghcr.io/token?scope=repository%3Aaquasecurity%2Ftrivy-db%3Apull&service=ghcr.io: DENIED: denied + +Your local GHCR (GitHub Container Registry) token might be expired. +Please remove the token and try downloading the DB again. + +```shell +docker logout ghcr.io +``` + +or + +```shell +unset GITHUB_TOKEN +``` + +## Homebrew +### Scope error +!!! error + Error: Your macOS keychain GitHub credentials do not have sufficient scope! + +``` +$ brew tap aquasecurity/trivy +Error: Your macOS keychain GitHub credentials do not have sufficient scope! +Scopes they need: none +Scopes they have: +Create a personal access token: +https://github.com/settings/tokens/new?scopes=gist,public_repo&description=Homebrew +echo 'export HOMEBREW_GITHUB_API_TOKEN=your_token_here' >> ~/.zshrc +``` + +Try: + +``` +$ printf "protocol=https\nhost=github.com\n" | git credential-osxkeychain erase +``` + +### Already installed +!!! error + Error: aquasecurity/trivy/trivy 64 already installed + +``` +$ brew upgrade +... +Error: aquasecurity/trivy/trivy 64 already installed +``` + +Try: + +``` +$ brew unlink trivy && brew uninstall trivy +($ rm -rf /usr/local/Cellar/trivy/64) +$ brew install aquasecurity/trivy/trivy +``` + + +## Debugging +### HTTP Request/Response Tracing + +For debugging network issues, connection problems, or authentication failures, you can enable HTTP request/response tracing using the `--trace-http` flag. + +!!! danger "Security Warning" + While Trivy attempts to redact known sensitive information such as authentication headers and common secrets, the `--trace-http` flag may still expose sensitive data in HTTP requests and responses. + + **Never use this flag in production environments or CI/CD pipelines.** + This flag is automatically disabled in CI environments for security. + +```bash +# Enable HTTP tracing for debugging registry issues +$ trivy image --trace-http registry.example.com/my-image:latest + +# HTTP tracing with other debugging options +$ trivy image --trace-http --debug --insecure my-image:tag +``` + +## Others +### Unknown error + +Try again after running `trivy clean --all`: + +``` +$ trivy clean --all +``` + +[air-gapped]: ../advanced/air-gap.md +[network]: ../advanced/air-gap.md#connectivity-requirements +[redis-cache]: ../configuration/cache.md#redis diff --git a/docs/guide/scanner/license.md b/docs/guide/scanner/license.md new file mode 100644 index 000000000000..44a2837f5b1f --- /dev/null +++ b/docs/guide/scanner/license.md @@ -0,0 +1,388 @@ +# License Scanning + +Trivy scans any container image for license files and offers an opinionated view on the risk associated with the license. + +Licenses are classified using the [Google License Classification][google-license-classification] - + + - Forbidden + - Restricted + - Reciprocal + - Notice + - Permissive + - Unencumbered + - Unknown + +!!! tip + Licenses that Trivy fails to recognize are classified as UNKNOWN. + As those licenses may be in violation, it is recommended to check those unknown licenses as well. + +By default, Trivy scans licenses for packages installed by `apk`, `apt-get`, `dnf`, `npm`, `pip`, `gem`, etc. +Check out [the coverage document][coverage] for details. + +To enable extended license scanning, you can use `--license-full`. +In addition to package licenses, Trivy scans source code files, Markdown documents, text files and `LICENSE` documents to identify license usage within the image or filesystem. + +By default, Trivy only classifies licenses that are matched with a confidence level of 0.9 or more by the classifier. +To configure the confidence level, you can use `--license-confidence-level`. This enables us to classify licenses that might be matched with a lower confidence level by the classifier. + +!!! note + The full license scanning is expensive. It takes a while. + +| License scanning | Image | Rootfs | Filesystem | Repository | SBOM | +|:---------------------:|:-----:|:------:|:----------:|:----------:|:----:| +| Standard | ✅ | ✅ | ✅[^1][^2] | ✅[^1][^2] | ✅ | +| Full (--license-full) | ✅ | ✅ | ✅ | ✅ | - | + +License checking classifies the identified licenses and maps the classification to severity. + +| Classification | Severity | +| -------------- | -------- | +| Forbidden | CRITICAL | +| Restricted | HIGH | +| Reciprocal | MEDIUM | +| Notice | LOW | +| Permissive | LOW | +| Unencumbered | LOW | +| Unknown | UNKNOWN | + +## Quick start +This section shows how to scan license in container image and filesystem. + +### Standard scanning +Specify an image name with `--scanners license`. + +``` shell +$ trivy image --scanners license --severity UNKNOWN,HIGH,CRITICAL alpine:3.15 +2022-07-13T17:28:39.526+0300 INFO License scanning is enabled + +OS Packages (license) +===================== +Total: 6 (UNKNOWN: 0, HIGH: 6, CRITICAL: 0) + +┌───────────────────┬─────────┬────────────────┬──────────┐ +│ Package │ License │ Classification │ Severity │ +├───────────────────┼─────────┼────────────────┼──────────┤ +│ alpine-baselayout │ GPL-2.0 │ Restricted │ HIGH │ +├───────────────────┤ │ │ │ +│ apk-tools │ │ │ │ +├───────────────────┤ │ │ │ +│ busybox │ │ │ │ +├───────────────────┤ │ │ │ +│ musl-utils │ │ │ │ +├───────────────────┤ │ │ │ +│ scanelf │ │ │ │ +├───────────────────┤ │ │ │ +│ ssl_client │ │ │ │ +└───────────────────┴─────────┴────────────────┴──────────┘ +``` + +### Full scanning +Specify `--license-full` + +``` shell +$ trivy image --scanners license --severity UNKNOWN,HIGH,CRITICAL --license-full grafana/grafana +2022-07-13T17:48:40.905+0300 INFO Full license scanning is enabled + +OS Packages (license) +===================== +Total: 20 (UNKNOWN: 9, HIGH: 11, CRITICAL: 0) + +┌───────────────────┬───────────────────┬────────────────┬──────────┐ +│ Package │ License │ Classification │ Severity │ +├───────────────────┼───────────────────┼────────────────┼──────────┤ +│ alpine-baselayout │ GPL-2.0 │ Restricted │ HIGH │ +├───────────────────┤ │ │ │ +│ apk-tools │ │ │ │ +├───────────────────┼───────────────────┤ │ │ +│ bash │ GPL-3.0 │ │ │ +├───────────────────┼───────────────────┼────────────────┼──────────┤ +│ keyutils-libs │ GPL-2.0 │ Restricted │ HIGH │ +│ ├───────────────────┼────────────────┼──────────┤ +│ │ LGPL-2.0-or-later │ Non Standard │ UNKNOWN │ +├───────────────────┼───────────────────┤ │ │ +│ libaio │ LGPL-2.1-or-later │ │ │ +├───────────────────┼───────────────────┼────────────────┼──────────┤ +│ libcom_err │ GPL-2.0 │ Restricted │ HIGH │ +│ ├───────────────────┼────────────────┼──────────┤ +│ │ LGPL-2.0-or-later │ Non Standard │ UNKNOWN │ +├───────────────────┼───────────────────┼────────────────┼──────────┤ +│ tzdata │ Public-Domain │ Non Standard │ UNKNOWN │ +└───────────────────┴───────────────────┴────────────────┴──────────┘ + +Loose File License(s) (license) +=============================== +Total: 6 (UNKNOWN: 4, HIGH: 0, CRITICAL: 2) + +┌────────────────┬──────────┬──────────────┬──────────────────────────────────────────────────────────────┐ +│ Classification │ Severity │ License │ File Location │ +├────────────────┼──────────┼──────────────┼──────────────────────────────────────────────────────────────┤ +│ Forbidden │ CRITICAL │ AGPL-3.0 │ /usr/share/grafana/LICENSE │ +│ │ │ │ │ +│ │ │ │ │ +├────────────────┼──────────┼──────────────┼──────────────────────────────────────────────────────────────┤ +│ Non Standard │ UNKNOWN │ BSD-0-Clause │ /usr/share/grafana/public/build/5069.d6aae9dd11d49c741a80.j- │ +│ │ │ │ s.LICENSE.txt │ +│ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ │ │ /usr/share/grafana/public/build/6444.d6aae9dd11d49c741a80.j- │ +│ │ │ │ s.LICENSE.txt │ +│ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ │ │ /usr/share/grafana/public/build/7889.d6aae9dd11d49c741a80.j- │ +│ │ │ │ s.LICENSE.txt │ +│ │ │ ├──────────────────────────────────────────────────────────────┤ +│ │ │ │ /usr/share/grafana/public/build/canvasPanel.d6aae9dd11d49c7- │ +│ │ │ │ 41a80.js.LICENSE.txt │ +└────────────────┴──────────┴──────────────┴──────────────────────────────────────────────────────────────┘ +``` + +## Configuration + +Trivy has a number of configuration flags for use with license scanning: + +### Ignored Licenses + +Trivy license scanning can ignore licenses that are identified to explicitly remove them from the results using the `--ignored-licenses` flag; + +```shell +$ trivy image --scanners license --ignored-licenses MPL-2.0,MIT --severity HIGH grafana/grafana:latest +2022-07-13T18:15:28.605Z INFO License scanning is enabled + +OS Packages (license) +===================== +Total: 2 (HIGH: 2, CRITICAL: 0) + +┌───────────────────┬─────────┬────────────────┬──────────┐ +│ Package │ License │ Classification │ Severity │ +├───────────────────┼─────────┼────────────────┼──────────┤ +│ alpine-baselayout │ GPL-2.0 │ Restricted │ HIGH │ +├───────────────────┤ │ │ │ +│ ssl_client │ │ │ │ +└───────────────────┴─────────┴────────────────┴──────────┘ + +``` + +### Configuring Classifier Confidence Level +You can use the `--license-confidence-level` flag to adjust the confidence level between 0.0 to 1.0 (default 0.9). +For example, when you run the scanner with the default confidence level on [SPDX license list data](https://github.com/spdx/license-list-data/tree/main/text), it is able to detect only 258 licenses. + +```shell +$ trivy fs --scanners license --license-full +2023-04-18T10:05:13.601-0700 INFO Full license scanning is enabled + +Loose File License(s) (license) +=============================== +Total: 258 (UNKNOWN: 70, LOW: 90, MEDIUM: 18, HIGH: 58, CRITICAL: 22) +``` + +However, by configuring the confidence level to 0.8, the scanner is now able to detect 282 licenses. + +```shell +$ trivy fs --scanners license --license-full --license-confidence-level 0.8 +2023-04-18T10:21:39.637-0700 INFO Full license scanning is enabled + +Loose File License(s) (license) +=============================== +Total: 282 (UNKNOWN: 81, LOW: 97, MEDIUM: 24, HIGH: 58, CRITICAL: 22) +``` + +### Custom Classification +You can generate the default config by the `--generate-default-config` flag and customize the license classification. +For example, if you want to forbid only AGPL-3.0, you can leave it under `forbidden` and move other licenses to another classification. + +```shell +$ trivy image --generate-default-config +$ vim trivy.yaml +license: + forbidden: + - AGPL-3.0 + + restricted: + - AGPL-1.0 + - CC-BY-NC-1.0 + - CC-BY-NC-2.0 + - CC-BY-NC-2.5 + - CC-BY-NC-3.0 + - CC-BY-NC-4.0 + - CC-BY-NC-ND-1.0 + - CC-BY-NC-ND-2.0 + - CC-BY-NC-ND-2.5 + - CC-BY-NC-ND-3.0 + - CC-BY-NC-ND-4.0 + - CC-BY-NC-SA-1.0 + - CC-BY-NC-SA-2.0 + - CC-BY-NC-SA-2.5 + - CC-BY-NC-SA-3.0 + - CC-BY-NC-SA-4.0 + - Commons-Clause + - Facebook-2-Clause + - Facebook-3-Clause + - Facebook-Examples + - WTFPL + - BCL + - CC-BY-ND-1.0 + - CC-BY-ND-2.0 + - CC-BY-ND-2.5 + - CC-BY-ND-3.0 + - CC-BY-ND-4.0 + - CC-BY-SA-1.0 + - CC-BY-SA-2.0 + - CC-BY-SA-2.5 + - CC-BY-SA-3.0 + - CC-BY-SA-4.0 + - GPL-1.0 + - GPL-2.0 + - GPL-2.0-with-autoconf-exception + - GPL-2.0-with-bison-exception + - GPL-2.0-with-classpath-exception + - GPL-2.0-with-font-exception + - GPL-2.0-with-GCC-exception + - GPL-3.0 + - GPL-3.0-with-autoconf-exception + - GPL-3.0-with-GCC-exception + - LGPL-2.0 + - LGPL-2.1 + - LGPL-3.0 + - NPL-1.0 + - NPL-1.1 + - OSL-1.0 + - OSL-1.1 + - OSL-2.0 + - OSL-2.1 + - OSL-3.0 + - QPL-1.0 + - Sleepycat + + reciprocal: + - APSL-1.0 + - APSL-1.1 + - APSL-1.2 + - APSL-2.0 + - CDDL-1.0 + - CDDL-1.1 + - CPL-1.0 + - EPL-1.0 + - EPL-2.0 + - FreeImage + - IPL-1.0 + - MPL-1.0 + - MPL-1.1 + - MPL-2.0 + - Ruby + + notice: + - AFL-1.1 + - AFL-1.2 + - AFL-2.0 + - AFL-2.1 + - AFL-3.0 + - Apache-1.0 + - Apache-1.1 + - Apache-2.0 + - Artistic-1.0-cl8 + - Artistic-1.0-Perl + - Artistic-1.0 + - Artistic-2.0 + - BSL-1.0 + - BSD-2-Clause-FreeBSD + - BSD-2-Clause-NetBSD + - BSD-2-Clause + - BSD-3-Clause-Attribution + - BSD-3-Clause-Clear + - BSD-3-Clause-LBNL + - BSD-3-Clause + - BSD-4-Clause + - BSD-4-Clause-UC + - BSD-Protection + - CC-BY-1.0 + - CC-BY-2.0 + - CC-BY-2.5 + - CC-BY-3.0 + - CC-BY-4.0 + - FTL + - ISC + - ImageMagick + - Libpng + - Lil-1.0 + - Linux-OpenIB + - LPL-1.02 + - LPL-1.0 + - MS-PL + - MIT + - NCSA + - OpenSSL + - PHP-3.01 + - PHP-3.0 + - PIL + - Python-2.0 + - Python-2.0-complete + - PostgreSQL + - SGI-B-1.0 + - SGI-B-1.1 + - SGI-B-2.0 + - Unicode-DFS-2015 + - Unicode-DFS-2016 + - Unicode-TOU + - UPL-1.0 + - W3C-19980720 + - W3C-20150513 + - W3C + - X11 + - Xnet + - Zend-2.0 + - zlib-acknowledgement + - Zlib + - ZPL-1.1 + - ZPL-2.0 + - ZPL-2.1 + + unencumbered: + - CC0-1.0 + - Unlicense + - 0BSD + + permissive: [] +``` + +#### Text licenses +By default, Trivy categorizes a license as UNKNOWN if it cannot determine the license name from the license text. + +To define a category for a text license, you need to add license with the `text://` prefix to license classification. +For example: +```yaml +license: + forbidden: + - "text://Text of Apache Software Foundation License" +``` + +But a text license can be large. So for these cases, Trivy supports using `regex` in license classification. +For example: +```yaml +license: + forbidden: + - "text://.* Apache Software .*" +``` + +!!! note + `regex` is only used for text licenses and can't be used to configure license IDs. + +### Enabling a Subset of Package Types + +It's possible to only enable certain package types if you prefer. +You can do so by passing the `--pkg-types` option. +This flag takes a comma-separated list of package types. + +Available values: + +- os + - Scan OS packages managed by the OS package manager (e.g. `dpkg`, `yum`, `apk`). +- library + - Scan language-specific packages (e.g. packages installed by `pip`, `npm`, or `gem`). + +```bash +$ trivy image --pkg-types os ruby:2.4.0 +``` + +[^1]: See the list of supported language files [here](../coverage/language/index.md). +[^2]: Some lock files require additional files (e.g. files from the cache directory) to detect licenses. Check [coverage][coverage] for more information. + +[coverage]: ../coverage/index.md +[google-license-classification]: https://opensource.google/documentation/reference/thirdparty/licenses diff --git a/docs/guide/scanner/misconfiguration/check/builtin.md b/docs/guide/scanner/misconfiguration/check/builtin.md new file mode 100644 index 000000000000..77b68f7ed09d --- /dev/null +++ b/docs/guide/scanner/misconfiguration/check/builtin.md @@ -0,0 +1,20 @@ +# Built-in Checks + +## Checks Sources +Trivy has an extensive library of misconfiguration checks that is maintained at . +Trivy checks are mainly written in [Rego][rego], while some checks are written in Go. +See [here](../../../coverage/iac/index.md) for the list of supported config types. + +## Checks Bundle +When performing a misconfiguration scan, Trivy will automatically download the relevant Checks bundle. The bundle is cached locally and Trivy will reuse it for subsequent scans on the same machine. Trivy takes care of updating the cache automatically, so normally users can be oblivious to it. + +## Checks Distribution +Trivy checks are distributed as an [OPA bundle][opa-bundle] hosted in the following GitHub Container Registry: . +Trivy checks for updates to OPA bundle on GHCR every 24 hours and pulls it if there are any updates. + +### External connectivity +Trivy needs to connect to the internet to download the bundle. If you are running Trivy in an air-gapped environment, or an tightly controlled network, please refer to the [Advanced Network Scenarios document](../../../advanced/air-gap.md). +The Checks bundle is also embedded in the Trivy binary (at build time), and will be used as a fallback if Trivy is unable to download the bundle. This means that you can still scan for misconfigurations in an air-gapped environment using the Checks from the time of the Trivy release you are using. + +[rego]: https://www.openpolicyagent.org/docs/latest/policy-language/ +[opa-bundle]: https://www.openpolicyagent.org/docs/latest/management-bundles/ diff --git a/docs/guide/scanner/misconfiguration/config/config.md b/docs/guide/scanner/misconfiguration/config/config.md new file mode 100644 index 000000000000..e10a69dc12fc --- /dev/null +++ b/docs/guide/scanner/misconfiguration/config/config.md @@ -0,0 +1,345 @@ +This page describes misconfiguration-specific configuration. + +### Enabling a subset of misconfiguration scanners +It's possible to only enable certain misconfiguration scanners if you prefer. +You can do so by passing the `--misconfig-scanners` option. +This flag takes a comma-separated list of configuration scanner types. + +```bash +trivy config --misconfig-scanners=terraform,dockerfile . +``` + +Will only scan for misconfigurations that pertain to Terraform and Dockerfiles. + +### Loading custom checks +You can load check files or directories including your custom checks using the `--config-check` flag. +This can be repeated for specifying multiple files or directories. + +```bash +trivy config --config-check custom-policy/policy --config-check combine/policy --config-check policy.rego --namespaces user myapp +``` + +You can load checks bundle as OCI Image from a Container Registry using the `--checks-bundle-repository` flag. + +```bash +trivy config --checks-bundle-repository myregistry.local/mychecks --namespaces user myapp +``` + + +### Passing custom data +You can pass directories including your custom data through `--data` option. +This can be repeated for specifying multiple directories. + +```bash +cd examples/misconf/custom-data +trivy config --config-check ./my-check --data ./data --namespaces user ./configs +``` + +For more details, see [Custom Data](../custom/data.md). + +### Passing namespaces +By default, Trivy evaluates checks defined in `builtin.*`. +If you want to evaluate custom checks in other packages, you have to specify package prefixes through `--namespaces` option. +This can be repeated for specifying multiple packages. + +``` bash +trivy config --config-check ./my-check --namespaces main --namespaces user ./configs +``` + +### Limiting Rego compile errors + +By default, Trivy limits the number of compile errors allowed during Rego policy compilation. +You can configure this limit using the `--rego-error-limit` flag. + +```bash +trivy config --rego-error-limit 20 ./configs +``` + +This flag controls the maximum number of compile errors Trivy will tolerate before stopping the compilation. + +If the number of compile errors exceeds this limit, Trivy will terminate the scan. +You can set `--rego-error-limit 0` to enforce strict checking and disallow any compile errors. + +The default value is defined internally via [CompileErrorLimit](https://github.com/aquasecurity/trivy/tree/{{ git.commit }}pkg/iac/rego/scanner.go). + +### Private Terraform registries +Trivy can download Terraform code from private registries. +To pass credentials you must use the `TF_TOKEN_` environment variables. +You cannot use a `.terraformrc` or `terraform.rc` file, these are not supported by trivy yet. + +From the Terraform [docs](https://developer.hashicorp.com/terraform/cli/config/config-file#environment-variable-credentials): + +> Environment variable names should have the prefix TF_TOKEN_ added to the domain name, with periods encoded as underscores. +> For example, the value of a variable named `TF_TOKEN_app_terraform_io` will be used as a bearer authorization token when the CLI makes service requests to the hostname `app.terraform.io`. +> +> You must convert domain names containing non-ASCII characters to their punycode equivalent with an ACE prefix. +> For example, token credentials for `例えば.com` must be set in a variable called `TF_TOKEN_xn--r8j3dr99h_com`. +> +> Hyphens are also valid within host names but usually invalid as variable names and may be encoded as double underscores. +> For example, you can set a token for the domain name café.fr as TF_TOKEN_xn--caf-dma_fr or TF_TOKEN_xn____caf__dma_fr. + +If multiple variables evaluate to the same hostname, Trivy will choose the environment variable name where the dashes have not been encoded as double underscores. + +### Scan arbitrary JSON and YAML configurations +By default, scanning JSON and YAML configurations is disabled, since Trivy does not contain built-in checks for these configurations. To enable it, pass the `json` or `yaml` to `--misconfig-scanners`. See [Enabling a subset of misconfiguration scanners](#enabling-a-subset-of-misconfiguration-scanners) for more information. Trivy will pass each file as is to the checks input. + + +!!! example +```bash +$ cat iac/serverless.yaml +service: serverless-rest-api-with-pynamodb + +frameworkVersion: ">=2.24.0" + +plugins: + - serverless-python-requirements +... + +$ cat serverless.rego +# METADATA +# title: Serverless Framework service name not starting with "aws-" +# description: Ensure that Serverless Framework service names start with "aws-" +# schemas: +# - input: schema["serverless-schema"] +# custom: +# avd_id: AVD-SF-0001 +# severity: LOW +package user.serverless001 + +deny[res] { + not startswith(input.service, "aws-") + res := result.new( + sprintf("Service name %q is not allowed", [input.service]), + input.service + ) +} + +$ trivy config --misconfig-scanners=json,yaml --config-check ./serverless.rego --check-namespaces user ./iac +serverless.yaml (yaml) + +Tests: 4 (SUCCESSES: 3, FAILURES: 1) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +LOW: Service name "serverless-rest-api-with-pynamodb" is not allowed +═════════════════════════════════════════════════════════════════════════════════════════════════════════ +Ensure that Serverless Framework service names start with "aws-" +``` + +!!! note +In the case above, the custom check specified has a metadata annotation for the input schema `input: schema["serverless-schema"]`. This allows Trivy to type check the input IaC files provided. + +Optionally, you can also pass schemas using the `config-file-schemas` flag. Trivy will use these schemas for file filtering and type checking in Rego checks. + +!!! example +```bash +$ trivy config --misconfig-scanners=json,yaml --config-check ./serverless.rego --check-namespaces user --config-file-schemas ./serverless-schema.json ./iac +``` + +If the `--config-file-schemas` flag is specified Trivy ensures that each input IaC config file being scanned is type-checked against the schema. If the input file does not match any of the passed schemas, it will be ignored. + +If the schema is specified in the check metadata and is in the directory specified in the `--config-check` argument, it will be automatically loaded as specified [here](../custom/schema.md#custom-checks-with-custom-schemas), and will only be used for type checking in Rego. + +!!! note +If a user specifies the `--config-file-schemas` flag, all input IaC config files are ensured that they pass type-checking. It is not required to pass an input schema in case type checking is not required. This is helpful for scenarios where you simply want to write a Rego check and pass in IaC input for it. Such a use case could include scanning for a new service which Trivy might not support just yet. + +!!! tip +It is also possible to specify multiple input schemas with `--config-file-schema` flag as it can accept a comma separated list of file paths or a directory as input. In the case of multiple schemas being specified, all of them will be evaluated against all the input files. + + +### Filtering resources by inline comments + +Trivy supports ignoring misconfigured resources by inline comments for Terraform, CloudFormation, Helm and Dockerfile configuration files only. + +In cases where Trivy can detect comments of a specific format immediately adjacent to resource definitions, it is possible to ignore findings from a single source of resource definition (in contrast to `.trivyignore`, which has a directory-wide scope on all of the files scanned). The format for these comments is `trivy:ignore:` immediately following the format-specific line-comment [token](https://developer.hashicorp.com/terraform/language/syntax/configuration#comments). + + +!!!note + Inline ignore rules only work for checks associated with an existing resource. + Checks triggered by the absence of a resource (e.g., **AVD-DS-0002** when a Dockerfile lacks a `USER` instruction) cannot be ignored inline. + Use a [.trivyignore.yaml](../../../configuration/filtering.md#trivyignoreyaml) file to ignore such checks. + + +The ignore rule must contain one of the possible check IDs that can be found in its metadata: ID, short code or alias. The `id` from the metadata is not case-sensitive, so you can specify, for example, `AVD-AWS-0089` or `avd-aws-0089`. + +For example, to ignore a misconfiguration ID `AVD-GCP-0051` in a Terraform HCL file: + +```terraform +#trivy:ignore:AVD-GCP-0051 +resource "google_container_cluster" "example" { + name = var.cluster_name + location = var.region +} +``` + +You can add multiple ignores on the same comment line: +```terraform +#trivy:ignore:AVD-GCP-0051 trivy:ignore:AVD-GCP-0053 +resource "google_container_cluster" "example" { + name = var.cluster_name + location = var.region +} +``` + +You can also specify a long ID, which is formed as follows: `--`. + +As an example, consider the following check metadata: + +```yaml +# custom: + # id: AVD-AWS-0089 + # avd_id: AVD-AWS-0089 + # provider: aws + # service: s3 + # severity: LOW + # short_code: enable-logging +``` +Long ID would look like the following: `aws-s3-enable-logging`. + +Example for CloudFromation: +```yaml +AWSTemplateFormatVersion: "2010-09-09" +Resources: +#trivy:ignore:* + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket +``` + +!!!note + Ignore rules for Helm files should be placed before the YAML object, since only it contains the location data needed for ignoring. + +Example for Helm: +```yaml + serviceAccountName: "testchart.serviceAccountName" + containers: + # trivy:ignore:KSV018 + - name: "testchart" + securityContext: + runAsUser: 1000 + runAsGroup: 3000 + image: "your-repository/your-image:your-tag" + imagePullPolicy: "Always" +``` + +#### Expiration Date + +You can specify the expiration date of the ignore rule in `yyyy-mm-dd` format. This is a useful feature when you want to make sure that an ignored issue is not forgotten and worth revisiting in the future. For example: +```tf +#trivy:ignore:aws-s3-enable-logging:exp:2024-03-10 +resource "aws_s3_bucket" "example" { + bucket = "test" +} +``` + +The `aws-s3-enable-logging` check will be ignored until `2024-03-10` until the ignore rule expires. + +#### Ignoring by attributes + +You can ignore a resource by its attribute value. This is useful when using the `for-each` meta-argument. For example: + +```tf +locals { + ports = ["3306", "5432"] +} + +#trivy:ignore:aws-ec2-no-public-ingress-sgr[from_port=3306] +resource "aws_security_group_rule" "example" { + for_each = toset(local.ports) + type = "ingress" + from_port = each.key + to_port = each.key + protocol = "TCP" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.example.id + source_security_group_id = aws_security_group.example.id +} +``` + +The `aws-ec2-no-public-ingress-sgr` check will be ignored only for the `aws_security_group_rule` resource with port number `5432`. It is important to note that the ignore rule should not enclose the attribute value in quotes, despite the fact that the port is represented as a string. + +If you want to ignore multiple resources on different attributes, you can specify multiple ignore rules: + +```tf +#trivy:ignore:aws-ec2-no-public-ingress-sgr[from_port=3306] +#trivy:ignore:aws-ec2-no-public-ingress-sgr[from_port=5432] +``` + +You can also ignore a resource on multiple attributes in the same rule: +```tf +locals { + rules = { + first = { + port = 1000 + type = "ingress" + }, + second = { + port = 1000 + type = "egress" + } + } +} + +#trivy:ignore:aws-ec2-no-public-ingress-sgr[from_port=1000,type=egress] +resource "aws_security_group_rule" "example" { + for_each = { for k, v in local.rules : k => v } + + type = each.value.type + from_port = each.value.port + to_port = each.value.port + protocol = "TCP" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.example.id + source_security_group_id = aws_security_group.example.id +} +``` + +Checks can also be ignored by nested attributes: + +```tf +#trivy:ignore:*[logging_config.prefix=myprefix] +resource "aws_cloudfront_distribution" "example" { + logging_config { + include_cookies = false + bucket = "mylogs.s3.amazonaws.com" + prefix = "myprefix" + } +} +``` + +#### Ignoring module issues + +Issues in third-party modules cannot be ignored using the method described above, because you may not have access to modify the module source code. In such a situation you can add ignore rules above the module block, for example: + +```tf +#trivy:ignore:aws-s3-enable-logging +module "s3_bucket" { + source = "terraform-aws-modules/s3-bucket/aws" + + bucket = "my-s3-bucket" +} +``` + +An example of ignoring checks for a specific bucket in a module: +```tf +locals { + bucket = ["test1", "test2"] +} + +#trivy:ignore:*[bucket=test1] +module "s3_bucket" { + for_each = toset(local.bucket) + source = "terraform-aws-modules/s3-bucket/aws" + bucket = each.value +} +``` + +#### Support for Wildcards + +You can use wildcards in the `ws` (workspace) and `ignore` sections of the ignore rules. + +```tf +# trivy:ignore:aws-s3-*:ws:dev-* +``` + +This example ignores all checks starting with `aws-s3-` for workspaces matching the pattern `dev-*`. diff --git a/docs/guide/scanner/misconfiguration/custom/combine.md b/docs/guide/scanner/misconfiguration/custom/combine.md new file mode 100644 index 000000000000..0d7997bc0272 --- /dev/null +++ b/docs/guide/scanner/misconfiguration/custom/combine.md @@ -0,0 +1,44 @@ +# Combined input + +## Overview +Trivy usually scans each configuration file individually. +Sometimes it might be useful to compare values from different configuration files simultaneously. + +When `combine` is set to true, all config files under the specified directory are combined into one input data structure. + +!!! example + ``` + __rego_input__ := { + "combine": false, + } + ``` + +In "combine" mode, the `input` document becomes an array, where each element is an object with two fields: + +- `"path": "path/to/file"`: the relative file path of the respective file +- `"contents": ...`: the parsed content of the respective file + +Now you can ensure that duplicate values match across the entirety of your configuration files. + +## Return value +In "combine" mode, the `deny` entrypoint must return an object with two keys + +`filepath` (required) +: the relative file path of the file being evaluated + +`msg` (required) +: the message describing an issue + +!!! example + ``` + deny[res] { + resource := input[i].contents + ... some logic ... + + res := { + "filepath": input[i].path, + "msg": "something bad", + } + } + ``` + diff --git a/docs/guide/scanner/misconfiguration/custom/contribute-checks.md b/docs/guide/scanner/misconfiguration/custom/contribute-checks.md new file mode 100644 index 000000000000..8288d3b2189d --- /dev/null +++ b/docs/guide/scanner/misconfiguration/custom/contribute-checks.md @@ -0,0 +1,5 @@ +## Contribute Rego Checks + +The contributing section provides detailed information on how to contribute custom checks to the [trivy-checks repository](../../../../community/contribute/checks/overview.md/) + +This way, they become accessible as default [checks.](https://github.com/aquasecurity/trivy-checks) \ No newline at end of file diff --git a/docs/guide/scanner/misconfiguration/custom/data.md b/docs/guide/scanner/misconfiguration/custom/data.md new file mode 100644 index 000000000000..1c9ec7d8e8a7 --- /dev/null +++ b/docs/guide/scanner/misconfiguration/custom/data.md @@ -0,0 +1,54 @@ +# Custom Data + +Custom checks may require additional data in order to make a resolution. You can pass arbitrary data files to Trivy to be used when evaluating rego checks using the `--config-data` flag. +Trivy recursively searches the specified data paths for JSON (`*.json`) and YAML (`*.yaml`) files. + +For example, consider an allowed list of resources that can be created. +Instead of hardcoding this information inside your check, you can maintain the list in a separate file. + +Example data file: + +```yaml +services: + ports: + - "20" + - "20/tcp" + - "20/udp" + - "23" + - "23/tcp" +``` + +Example usage in a Rego check: + +```rego +import data.services + +ports := services.ports +``` + +Example loading the data file: + +```bash +trivy config --config-check ./checks --config-data ./data --namespaces user ./configs +``` + +## Customizing default checks data + +Some checks allow you to customize the default data values. To do this, simply pass a data file via `--config-data` (see the section above). + +Table of supported data for customizing and their paths: + +| Check ID | Data path | Description | +|--------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------|--------------------------------------------------------------| +| [KSV0125](https://github.com/aquasecurity/trivy-checks/blob/db2e49de5ff5fd5c8e5cd702b7891f9d9e971a65/checks/kubernetes/uses_untrusted_registry.rego#L76-L78) | `ksv0125.trusted_registries` | List of trusted container registries | +| [DS031](https://github.com/aquasecurity/trivy-checks/blob/db2e49de5ff5fd5c8e5cd702b7891f9d9e971a65/checks/docker/leaked_secrets.rego#L135) | `ds031.included_envs` | List of allowed environment variables (merged with defaults) | + + +Example of overriding trusted registries for `KSV0125`: + +```yaml +ksv0125: + trusted_registries: + - "my-registry.example.com" + - "registry.internal.local" +``` \ No newline at end of file diff --git a/docs/guide/scanner/misconfiguration/custom/debug.md b/docs/guide/scanner/misconfiguration/custom/debug.md new file mode 100644 index 000000000000..e7fe53cd3d34 --- /dev/null +++ b/docs/guide/scanner/misconfiguration/custom/debug.md @@ -0,0 +1,304 @@ +# Debugging checks +When working on more complex queries (or when learning Rego), it's useful to see exactly how the policy is applied. +For this purpose you can use the `--trace-rego` flag. +This will output a large trace from Open Policy Agent like the following: + +!!! tip + Only failed checks show traces. If you want to debug a passed check, you need to make it fail on purpose. + +```shell +$ trivy config --trace-rego configs/ +2022-05-16T13:47:58.853+0100 INFO Detected config files: 1 + +Dockerfile (dockerfile) +======================= +Tests: 23 (SUCCESSES: 21, FAILURES: 2) +Failures: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 1, CRITICAL: 0) + +MEDIUM: Specify a tag in the 'FROM' statement for image 'alpine' +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated. + +See https://avd.aquasec.com/misconfig/ds001 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Dockerfile:1 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 1 [ FROM alpine:latest +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + +HIGH: Last USER command in Dockerfile should not be 'root' +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile. + +See https://avd.aquasec.com/misconfig/ds002 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Dockerfile:3 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 3 [ USER root +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + +ID: DS001 +File: Dockerfile +Namespace: builtin.dockerfile.DS001 +Query: data.builtin.dockerfile.DS001.deny +Message: Specify a tag in the 'FROM' statement for image 'alpine' +TRACE Enter data.builtin.dockerfile.DS001.deny = _ +TRACE | Eval data.builtin.dockerfile.DS001.deny = _ +TRACE | Index data.builtin.dockerfile.DS001.deny (matched 1 rule) +TRACE | Enter data.builtin.dockerfile.DS001.deny +TRACE | | Eval output = data.builtin.dockerfile.DS001.fail_latest[_] +TRACE | | Index data.builtin.dockerfile.DS001.fail_latest (matched 1 rule) +TRACE | | Enter data.builtin.dockerfile.DS001.fail_latest +TRACE | | | Eval output = data.builtin.dockerfile.DS001.image_tags[_] +TRACE | | | Index data.builtin.dockerfile.DS001.image_tags (matched 2 rules) +TRACE | | | Enter data.builtin.dockerfile.DS001.image_tags +TRACE | | | | Eval from = data.lib.docker.from[_] +TRACE | | | | Index data.lib.docker.from (matched 1 rule) +TRACE | | | | Enter data.lib.docker.from +TRACE | | | | | Eval instruction = input.stages[_][_] +TRACE | | | | | Eval instruction.Cmd = "from" +TRACE | | | | | Exit data.lib.docker.from +TRACE | | | | Redo data.lib.docker.from +TRACE | | | | | Redo instruction.Cmd = "from" +TRACE | | | | | Redo instruction = input.stages[_][_] +TRACE | | | | | Eval instruction.Cmd = "from" +TRACE | | | | | Fail instruction.Cmd = "from" +TRACE | | | | | Redo instruction = input.stages[_][_] +TRACE | | | | | Eval instruction.Cmd = "from" +TRACE | | | | | Fail instruction.Cmd = "from" +TRACE | | | | | Redo instruction = input.stages[_][_] +TRACE | | | | Eval name = from.Value[0] +TRACE | | | | Eval not startswith(name, "$") +TRACE | | | | Enter startswith(name, "$") +TRACE | | | | | Eval startswith(name, "$") +TRACE | | | | | Fail startswith(name, "$") +TRACE | | | | Eval data.builtin.dockerfile.DS001.parse_tag(name, __local505__) +TRACE | | | | Index data.builtin.dockerfile.DS001.parse_tag (matched 2 rules) +TRACE | | | | Enter data.builtin.dockerfile.DS001.parse_tag +TRACE | | | | | Eval split(name, ":", __local504__) +TRACE | | | | | Eval [img, tag] = __local504__ +TRACE | | | | | Exit data.builtin.dockerfile.DS001.parse_tag +TRACE | | | | Eval [img, tag] = __local505__ +TRACE | | | | Eval output = {"cmd": from, "img": img, "tag": tag} +TRACE | | | | Exit data.builtin.dockerfile.DS001.image_tags +TRACE | | | Redo data.builtin.dockerfile.DS001.image_tags +TRACE | | | | Redo output = {"cmd": from, "img": img, "tag": tag} +TRACE | | | | Redo [img, tag] = __local505__ +TRACE | | | | Redo data.builtin.dockerfile.DS001.parse_tag(name, __local505__) +TRACE | | | | Redo data.builtin.dockerfile.DS001.parse_tag +TRACE | | | | | Redo [img, tag] = __local504__ +TRACE | | | | | Redo split(name, ":", __local504__) +TRACE | | | | Enter data.builtin.dockerfile.DS001.parse_tag +TRACE | | | | | Eval tag = "latest" +TRACE | | | | | Eval not contains(img, ":") +TRACE | | | | | Enter contains(img, ":") +TRACE | | | | | | Eval contains(img, ":") +TRACE | | | | | | Exit contains(img, ":") +TRACE | | | | | Redo contains(img, ":") +TRACE | | | | | | Redo contains(img, ":") +TRACE | | | | | Fail not contains(img, ":") +TRACE | | | | | Redo tag = "latest" +TRACE | | | | Redo name = from.Value[0] +TRACE | | | | Redo from = data.lib.docker.from[_] +TRACE | | | Enter data.builtin.dockerfile.DS001.image_tags +TRACE | | | | Eval from = data.lib.docker.from[i] +TRACE | | | | Index data.lib.docker.from (matched 1 rule) +TRACE | | | | Eval name = from.Value[0] +TRACE | | | | Eval cmd_obj = input.stages[j][k] +TRACE | | | | Eval possibilities = {"arg", "env"} +TRACE | | | | Eval cmd_obj.Cmd = possibilities[l] +TRACE | | | | Fail cmd_obj.Cmd = possibilities[l] +TRACE | | | | Redo possibilities = {"arg", "env"} +TRACE | | | | Redo cmd_obj = input.stages[j][k] +TRACE | | | | Eval possibilities = {"arg", "env"} +TRACE | | | | Eval cmd_obj.Cmd = possibilities[l] +TRACE | | | | Fail cmd_obj.Cmd = possibilities[l] +TRACE | | | | Redo possibilities = {"arg", "env"} +TRACE | | | | Redo cmd_obj = input.stages[j][k] +TRACE | | | | Eval possibilities = {"arg", "env"} +TRACE | | | | Eval cmd_obj.Cmd = possibilities[l] +TRACE | | | | Fail cmd_obj.Cmd = possibilities[l] +TRACE | | | | Redo possibilities = {"arg", "env"} +TRACE | | | | Redo cmd_obj = input.stages[j][k] +TRACE | | | | Redo name = from.Value[0] +TRACE | | | | Redo from = data.lib.docker.from[i] +TRACE | | | Eval __local752__ = output.img +TRACE | | | Eval neq(__local752__, "scratch") +TRACE | | | Eval __local753__ = output.img +TRACE | | | Eval not data.builtin.dockerfile.DS001.is_alias(__local753__) +TRACE | | | Enter data.builtin.dockerfile.DS001.is_alias(__local753__) +TRACE | | | | Eval data.builtin.dockerfile.DS001.is_alias(__local753__) +TRACE | | | | Index data.builtin.dockerfile.DS001.is_alias (matched 1 rule, early exit) +TRACE | | | | Enter data.builtin.dockerfile.DS001.is_alias +TRACE | | | | | Eval img = data.builtin.dockerfile.DS001.get_aliases[_] +TRACE | | | | | Index data.builtin.dockerfile.DS001.get_aliases (matched 1 rule) +TRACE | | | | | Enter data.builtin.dockerfile.DS001.get_aliases +TRACE | | | | | | Eval from_cmd = data.lib.docker.from[_] +TRACE | | | | | | Index data.lib.docker.from (matched 1 rule) +TRACE | | | | | | Eval __local749__ = from_cmd.Value +TRACE | | | | | | Eval data.builtin.dockerfile.DS001.get_alias(__local749__, __local503__) +TRACE | | | | | | Index data.builtin.dockerfile.DS001.get_alias (matched 1 rule) +TRACE | | | | | | Enter data.builtin.dockerfile.DS001.get_alias +TRACE | | | | | | | Eval __local748__ = values[i] +TRACE | | | | | | | Eval lower(__local748__, __local501__) +TRACE | | | | | | | Eval "as" = __local501__ +TRACE | | | | | | | Fail "as" = __local501__ +TRACE | | | | | | | Redo lower(__local748__, __local501__) +TRACE | | | | | | | Redo __local748__ = values[i] +TRACE | | | | | | Fail data.builtin.dockerfile.DS001.get_alias(__local749__, __local503__) +TRACE | | | | | | Redo __local749__ = from_cmd.Value +TRACE | | | | | | Redo from_cmd = data.lib.docker.from[_] +TRACE | | | | | Fail img = data.builtin.dockerfile.DS001.get_aliases[_] +TRACE | | | | Fail data.builtin.dockerfile.DS001.is_alias(__local753__) +TRACE | | | Eval output.tag = "latest" +TRACE | | | Exit data.builtin.dockerfile.DS001.fail_latest +TRACE | | Redo data.builtin.dockerfile.DS001.fail_latest +TRACE | | | Redo output.tag = "latest" +TRACE | | | Redo __local753__ = output.img +TRACE | | | Redo neq(__local752__, "scratch") +TRACE | | | Redo __local752__ = output.img +TRACE | | | Redo output = data.builtin.dockerfile.DS001.image_tags[_] +TRACE | | Eval __local754__ = output.img +TRACE | | Eval sprintf("Specify a tag in the 'FROM' statement for image '%s'", [__local754__], __local509__) +TRACE | | Eval msg = __local509__ +TRACE | | Eval __local755__ = output.cmd +TRACE | | Eval data.lib.docker.result(msg, __local755__, __local510__) +TRACE | | Index data.lib.docker.result (matched 1 rule) +TRACE | | Enter data.lib.docker.result +TRACE | | | Eval object.get(cmd, "EndLine", 0, __local470__) +TRACE | | | Eval object.get(cmd, "Path", "", __local471__) +TRACE | | | Eval object.get(cmd, "StartLine", 0, __local472__) +TRACE | | | Eval result = {"endline": __local470__, "filepath": __local471__, "msg": msg, "startline": __local472__} +TRACE | | | Exit data.lib.docker.result +TRACE | | Eval res = __local510__ +TRACE | | Exit data.builtin.dockerfile.DS001.deny +TRACE | Redo data.builtin.dockerfile.DS001.deny +TRACE | | Redo res = __local510__ +TRACE | | Redo data.lib.docker.result(msg, __local755__, __local510__) +TRACE | | Redo data.lib.docker.result +TRACE | | | Redo result = {"endline": __local470__, "filepath": __local471__, "msg": msg, "startline": __local472__} +TRACE | | | Redo object.get(cmd, "StartLine", 0, __local472__) +TRACE | | | Redo object.get(cmd, "Path", "", __local471__) +TRACE | | | Redo object.get(cmd, "EndLine", 0, __local470__) +TRACE | | Redo __local755__ = output.cmd +TRACE | | Redo msg = __local509__ +TRACE | | Redo sprintf("Specify a tag in the 'FROM' statement for image '%s'", [__local754__], __local509__) +TRACE | | Redo __local754__ = output.img +TRACE | | Redo output = data.builtin.dockerfile.DS001.fail_latest[_] +TRACE | Exit data.builtin.dockerfile.DS001.deny = _ +TRACE Redo data.builtin.dockerfile.DS001.deny = _ +TRACE | Redo data.builtin.dockerfile.DS001.deny = _ +TRACE + + +ID: DS002 +File: Dockerfile +Namespace: builtin.dockerfile.DS002 +Query: data.builtin.dockerfile.DS002.deny +Message: Last USER command in Dockerfile should not be 'root' +TRACE Enter data.builtin.dockerfile.DS002.deny = _ +TRACE | Eval data.builtin.dockerfile.DS002.deny = _ +TRACE | Index data.builtin.dockerfile.DS002.deny (matched 2 rules) +TRACE | Enter data.builtin.dockerfile.DS002.deny +TRACE | | Eval data.builtin.dockerfile.DS002.fail_user_count +TRACE | | Index data.builtin.dockerfile.DS002.fail_user_count (matched 1 rule, early exit) +TRACE | | Enter data.builtin.dockerfile.DS002.fail_user_count +TRACE | | | Eval __local771__ = data.builtin.dockerfile.DS002.get_user +TRACE | | | Index data.builtin.dockerfile.DS002.get_user (matched 1 rule) +TRACE | | | Enter data.builtin.dockerfile.DS002.get_user +TRACE | | | | Eval user = data.lib.docker.user[_] +TRACE | | | | Index data.lib.docker.user (matched 1 rule) +TRACE | | | | Enter data.lib.docker.user +TRACE | | | | | Eval instruction = input.stages[_][_] +TRACE | | | | | Eval instruction.Cmd = "user" +TRACE | | | | | Fail instruction.Cmd = "user" +TRACE | | | | | Redo instruction = input.stages[_][_] +TRACE | | | | | Eval instruction.Cmd = "user" +TRACE | | | | | Exit data.lib.docker.user +TRACE | | | | Redo data.lib.docker.user +TRACE | | | | | Redo instruction.Cmd = "user" +TRACE | | | | | Redo instruction = input.stages[_][_] +TRACE | | | | | Eval instruction.Cmd = "user" +TRACE | | | | | Fail instruction.Cmd = "user" +TRACE | | | | | Redo instruction = input.stages[_][_] +TRACE | | | | Eval username = user.Value[_] +TRACE | | | | Exit data.builtin.dockerfile.DS002.get_user +TRACE | | | Redo data.builtin.dockerfile.DS002.get_user +TRACE | | | | Redo username = user.Value[_] +TRACE | | | | Redo user = data.lib.docker.user[_] +TRACE | | | Eval count(__local771__, __local536__) +TRACE | | | Eval lt(__local536__, 1) +TRACE | | | Fail lt(__local536__, 1) +TRACE | | | Redo count(__local771__, __local536__) +TRACE | | | Redo __local771__ = data.builtin.dockerfile.DS002.get_user +TRACE | | Fail data.builtin.dockerfile.DS002.fail_user_count +TRACE | Enter data.builtin.dockerfile.DS002.deny +TRACE | | Eval cmd = data.builtin.dockerfile.DS002.fail_last_user_root[_] +TRACE | | Index data.builtin.dockerfile.DS002.fail_last_user_root (matched 1 rule) +TRACE | | Enter data.builtin.dockerfile.DS002.fail_last_user_root +TRACE | | | Eval stage_users = data.lib.docker.stage_user[_] +TRACE | | | Index data.lib.docker.stage_user (matched 1 rule) +TRACE | | | Enter data.lib.docker.stage_user +TRACE | | | | Eval stage = input.stages[stage_name] +TRACE | | | | Eval users = [cmd | cmd = stage[_]; cmd.Cmd = "user"] +TRACE | | | | Enter cmd = stage[_]; cmd.Cmd = "user" +TRACE | | | | | Eval cmd = stage[_] +TRACE | | | | | Eval cmd.Cmd = "user" +TRACE | | | | | Fail cmd.Cmd = "user" +TRACE | | | | | Redo cmd = stage[_] +TRACE | | | | | Eval cmd.Cmd = "user" +TRACE | | | | | Exit cmd = stage[_]; cmd.Cmd = "user" +TRACE | | | | Redo cmd = stage[_]; cmd.Cmd = "user" +TRACE | | | | | Redo cmd.Cmd = "user" +TRACE | | | | | Redo cmd = stage[_] +TRACE | | | | | Eval cmd.Cmd = "user" +TRACE | | | | | Fail cmd.Cmd = "user" +TRACE | | | | | Redo cmd = stage[_] +TRACE | | | | Exit data.lib.docker.stage_user +TRACE | | | Redo data.lib.docker.stage_user +TRACE | | | | Redo users = [cmd | cmd = stage[_]; cmd.Cmd = "user"] +TRACE | | | | Redo stage = input.stages[stage_name] +TRACE | | | Eval count(stage_users, __local537__) +TRACE | | | Eval len = __local537__ +TRACE | | | Eval minus(len, 1, __local538__) +TRACE | | | Eval last = stage_users[__local538__] +TRACE | | | Eval user = last.Value[0] +TRACE | | | Eval user = "root" +TRACE | | | Exit data.builtin.dockerfile.DS002.fail_last_user_root +TRACE | | Redo data.builtin.dockerfile.DS002.fail_last_user_root +TRACE | | | Redo user = "root" +TRACE | | | Redo user = last.Value[0] +TRACE | | | Redo last = stage_users[__local538__] +TRACE | | | Redo minus(len, 1, __local538__) +TRACE | | | Redo len = __local537__ +TRACE | | | Redo count(stage_users, __local537__) +TRACE | | | Redo stage_users = data.lib.docker.stage_user[_] +TRACE | | Eval msg = "Last USER command in Dockerfile should not be 'root'" +TRACE | | Eval data.lib.docker.result(msg, cmd, __local540__) +TRACE | | Index data.lib.docker.result (matched 1 rule) +TRACE | | Enter data.lib.docker.result +TRACE | | | Eval object.get(cmd, "EndLine", 0, __local470__) +TRACE | | | Eval object.get(cmd, "Path", "", __local471__) +TRACE | | | Eval object.get(cmd, "StartLine", 0, __local472__) +TRACE | | | Eval result = {"endline": __local470__, "filepath": __local471__, "msg": msg, "startline": __local472__} +TRACE | | | Exit data.lib.docker.result +TRACE | | Eval res = __local540__ +TRACE | | Exit data.builtin.dockerfile.DS002.deny +TRACE | Redo data.builtin.dockerfile.DS002.deny +TRACE | | Redo res = __local540__ +TRACE | | Redo data.lib.docker.result(msg, cmd, __local540__) +TRACE | | Redo data.lib.docker.result +TRACE | | | Redo result = {"endline": __local470__, "filepath": __local471__, "msg": msg, "startline": __local472__} +TRACE | | | Redo object.get(cmd, "StartLine", 0, __local472__) +TRACE | | | Redo object.get(cmd, "Path", "", __local471__) +TRACE | | | Redo object.get(cmd, "EndLine", 0, __local470__) +TRACE | | Redo msg = "Last USER command in Dockerfile should not be 'root'" +TRACE | | Redo cmd = data.builtin.dockerfile.DS002.fail_last_user_root[_] +TRACE | Exit data.builtin.dockerfile.DS002.deny = _ +TRACE Redo data.builtin.dockerfile.DS002.deny = _ +TRACE | Redo data.builtin.dockerfile.DS002.deny = _ +TRACE +``` \ No newline at end of file diff --git a/docs/guide/scanner/misconfiguration/custom/index.md b/docs/guide/scanner/misconfiguration/custom/index.md new file mode 100644 index 000000000000..414b14d296d1 --- /dev/null +++ b/docs/guide/scanner/misconfiguration/custom/index.md @@ -0,0 +1,227 @@ +# Custom Checks + +## Overview +You can write custom checks in [Rego][rego]. +Once you finish writing custom checks, you can pass the check files or the directory where those checks are stored with --config-check` option. + +``` bash +trivy config --config-check /path/to/policy.rego --config-check /path/to/custom_checks --namespaces user /path/to/config_dir +``` + +As for `--namespaces` option, the detail is described as below. + +### File formats +If a file name matches the following file patterns, Trivy will parse the file and pass it as input to your Rego policy. + +| File format | File pattern | +|---------------|-----------------------------------------------------------| +| JSON | `*.json` | +| YAML | `*.yaml` and `*.yml` | +| Dockerfile | `Dockerfile`, `Dockerfile.*`, and `*.Dockerfile` | +| Containerfile | `Containerfile`, `Containerfile.*`, and `*.Containerfile` | +| Terraform | `*.tf` and `*.tf.json` | + +### Configuration languages +In the above general file formats, Trivy automatically identifies the following types of configuration files: + +- CloudFormation (JSON/YAML) +- Kubernetes (JSON/YAML) +- Helm (YAML) +- Terraform Plan (JSON/Snapshot) + +This is useful for filtering inputs, as described below. + +## Rego format +A single package must contain only one policy. + +!!!example + ``` rego + # METADATA + # title: Deployment not allowed + # description: Deployments are not allowed because of some reasons. + # schemas: + # - input: schema["kubernetes"] + # custom: + # id: ID001 + # severity: LOW + # input: + # selector: + # - type: kubernetes + package user.kubernetes.ID001 + + deny[res] { + input.kind == "Deployment" + msg := sprintf("Found deployment '%s' but deployments are not allowed", [input.metadata.name]) + res := result.new(msg, input.kind) + } + ``` + +In this example, ID001 "Deployment not allowed" is defined under `user.kubernetes.ID001`. +If you add a new custom policy, it must be defined under a new package like `user.kubernetes.ID002`. + +### Policy structure + +`# METADATA` (optional unless the check will be contributed into Trivy) +: - SHOULD be defined for clarity since these values will be displayed in the scan results + - `custom.input` SHOULD be set to indicate the input type the policy should be applied to. See [list of available types][source-types] + +`package` (required) +: - MUST follow the Rego's [specification][package] + - MUST be unique per policy + - SHOULD include policy id for uniqueness + - MAY include the group name such as `kubernetes` for clarity + - Group name has no effect on policy evaluation + +`deny` (required) +: - SHOULD be `deny` or start with `deny_` + - Although `warn`, `warn_*`, `violation`, `violation_` also work for compatibility, `deny` is recommended as severity can be defined in `__rego_metadata__`. + - SHOULD return ONE OF: + - The result of a call to `result.new(msg, cause)`. The `msg` is a `string` describing the issue occurrence, and the `cause` is the property/object where the issue occurred. Providing this allows Trivy to ascertain line numbers and highlight code in the output. + - A `string` denoting the detected issue + - Although `object` with `msg` field is accepted, other fields are dropped and `string` is recommended if `result.new()` is not utilised. + - e.g. `{"msg": "deny message", "details": "something"}` + +### Package +A package name must be unique per policy. + +!!!example + ``` rego + package user.kubernetes.ID001 + ``` + +By default, only `builtin.*` packages will be evaluated. +If you define custom packages, you have to specify the package prefix via `--namespaces` option. By default, Trivy only runs in its own namespace, unless specified by the user. Note that the custom namespace does not have to be `user` as in this example. It could be anything user-defined. + +``` bash +trivy config --config-check /path/to/custom_checks --namespaces user /path/to/config_dir +``` + +In this case, `user.*` will be evaluated. +Any package prefixes such as `main` and `user` are allowed. + +### Metadata + +The check must contain a [Rego Metadata](https://www.openpolicyagent.org/docs/latest/policy-language/#metadata) section. Trivy uses standard rego metadata to define the new policy and general information about it. + +Trivy supports extra fields in the `custom` section as described below. + +!!!example + ``` rego + # METADATA + # title: Deployment not allowed + # description: Deployments are not allowed because of some reasons. + # custom: + # id: ID001 + # severity: LOW + # input: + # selector: + # - type: kubernetes + ``` + +If you are creating checks for your Trivy misconfiguration scans, some fields are optional as referenced in the table below. The `schemas` field should be used to enable policy validation using a built-in schema. It is recommended to use this to ensure your checks are +correct and do not reference incorrect properties/values. + +| Field name | Allowed values | Default value | In table | In JSON | +|------------------------------|----------------------------------------------------------------------------------------------|:----------------------------:|:--------:|:-------:| +| title | Any characters | N/A | ✅ | ✅ | +| description | Any characters | | - | ✅ | +| schemas.input | `schema["kubernetes"]`, `schema["dockerfile"]`, `schema["cloud"]`, `schema["terraform-raw"]` | (applied to all input types) | - | - | +| custom.id | Any characters | N/A | ✅ | ✅ | +| custom.severity | `LOW`, `MEDIUM`, `HIGH`, `CRITICAL` | UNKNOWN | ✅ | ✅ | +| custom.recommended_actions | Any characters | | - | ✅ | +| custom.deprecated | `true`, `false` | `false` | - | ✅ | +| custom.input.selector.type | Any item(s) in [this list][source-types] | | - | ✅ | +| custom.minimum_trivy_version | The minimum version of Trivy that's required to evaluate this check | | - | ✅ | +| url | Any characters | | - | ✅ | + +#### custom.avd_id and custom.id + +The AVD_ID can be used to link the check to the Aqua Vulnerability Database (AVD) entry. For example, the `avd_id` `AVD-AWS-0176` is the ID of the check in the [AWS Vulnerability Database](https://avd.aquasec.com/). If you are [contributing your check to trivy-checks](../../../../community/contribute/checks/overview.md), you need to generate an ID using `make id` in the [trivy-checks](https://github.com/aquasecurity/trivy-checks) repository. The output of the command will provide you the next free IDs for the different providers in Trivy. + +The ID is based on the AVD_ID. For instance if the `avd_id` is `AVD-AWS-0176`, the ID is `ID0176`. + +#### custom.provider + +The `provider` field references the [provider](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/providers) available in Trivy. This should be the same as the provider name in the `pkg/iac/providers` directory, e.g. `aws`. + +#### custom.service + +Services are defined within a provider. For instance, RDS is a service and AWS is a provider. This should be the same as the service name in one of the provider directories. ([Link](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/providers)), e.g. `aws/rds`. + +#### custom.input + +The `input` tells Trivy what inputs this check should be applied to. Cloud provider checks should always use the `selector` input, and should always use the `type` selector with `cloud`. Check targeting Kubernetes yaml can use `kubernetes`, RBAC can use `rbac`, and so on. + +#### Subtypes in the custom data + +Subtypes currently only need to be defined for cloud providers [as detailed in the documentation.](./selectors.md/#enabling-selectors-and-subtypes) + +#### Scan Result + +Some fields are displayed in scan results. + +``` bash +k.yaml (kubernetes) +─────────────────── + +Tests: 32 (SUCCESSES: 31, FAILURES: 1) +Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +LOW: Found deployment 'my-deployment' but deployments are not allowed +════════════════════════════════════════════════════════════════════════ +Deployments are not allowed because of some reasons. +──────────────────────────────────────────────────────────────────────── + k.yaml:1-2 +──────────────────────────────────────────────────────────────────────── + 1 ┌ apiVersion: v1 + 2 └ kind: Deployment +──────────────────────────────────────────────────────────────────────── +``` + +### Input +You can specify input format via the `custom.input` annotation. + +!!!example + ``` rego + # METADATA + # custom: + # input: + # combine: false + # selector: + # - type: kubernetes + ``` + +`combine` (boolean) +: The details are [here](combine.md). + +`selector` (array) +: This option filters the input by file format or configuration language. + In the above example, Trivy passes only Kubernetes files to this policy. + Even if a Dockerfile exists in the specified directory, it will not be passed to the policy as input. + + Possible values for input types are: + + - `dockerfile` (Dockerfile) + - `kubernetes` (Kubernetes YAML/JSON) + - `rbac` (Kubernetes RBAC YAML/JSON) + - `cloud` (Cloud format, as defined by Trivy - this is used for Terraform, CloudFormation, and Cloud/AWS scanning) + - `yaml` (Generic YAML) + - `json` (Generic JSON) + - `toml` (Generic TOML) + - `terraform-raw` (Terraform configuration is not converted to common state as for the Cloud format, allowing for more flexible and direct checks on the original code) + + When configuration languages such as Kubernetes are not identified, file formats such as JSON will be used as `type`. + When a configuration language is identified, it will overwrite `type`. + + !!! example + `pod.yaml` including Kubernetes Pod will be handled as `kubernetes`, not `yaml`. + `type` is overwritten by `kubernetes` from `yaml`. + + `type` accepts `kubernetes`, `dockerfile`, `cloudformation`, `terraform`, `terraformplan`, `json`, or `yaml`. + +### Schemas +See [here](schema.md) for the detail. + +[rego]: https://www.openpolicyagent.org/docs/latest/policy-language/ +[package]: https://www.openpolicyagent.org/docs/latest/policy-language/#packages +[source-types]: https://github.com/aquasecurity/trivy/blob/e4af279b29ed5b77ed1d62e31b232b1f9b92ef4f/pkg/iac/types/sources.go#L5-L17 diff --git a/docs/guide/scanner/misconfiguration/custom/schema.md b/docs/guide/scanner/misconfiguration/custom/schema.md new file mode 100644 index 000000000000..6bf980cfb676 --- /dev/null +++ b/docs/guide/scanner/misconfiguration/custom/schema.md @@ -0,0 +1,180 @@ +# Input Schema + +## Overview + +Schemas are declarative documents that define the structure, data types and constraints of inputs being scanned. Trivy provides certain schemas out of the box as seen in the explorer [here](https://aquasecurity.github.io/trivy-schemas/). You can also find the source code for the schemas [here](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/rego/schemas). + +It is not required to pass in schemas, in order to scan inputs by Trivy but are required if type-checking is needed. + +Checks can be defined with custom schemas that allow inputs to be verified against them. Adding an input schema +enables Trivy to show more detailed error messages when an invalid input is encountered. + +## Unified Schema + +One of the unique advantages of Trivy is to take a variety of inputs, such as IaC files (e.g. CloudFormation, Terraform etc.) and also live cloud scanning +(e.g. [Trivy AWS plugin](https://github.com/aquasecurity/trivy-aws)) and normalize them into a standard structure, as defined by the schema. + +An example of such an application would be scanning AWS resources. You can scan them prior to deployment via the Trivy misconfiguration scanner and also +scan them after they've been deployed in the cloud with Trivy AWS scanning. Both scan methods should yield the same result as resources are gathered into +a unified representation as defined by the [Cloud schema](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/cloud.json). + + +## Supported Schemas +Currently out of the box the following schemas are supported natively: + +1. [Docker](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/dockerfile.json) +2. [Kubernetes](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/kubernetes.json) +3. [Cloud](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/cloud.json) +4. [Terraform Raw Format](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/terraform-raw.json) + +You can interactively view these schemas with the [Trivy Schema Explorer](https://aquasecurity.github.io/trivy-schemas/) + + +## Example +As mentioned earlier, amongst other built-in schemas, Trivy offers a built in-schema for scanning Dockerfiles. It is available [here](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/rego/schemas) +Without input schemas, a check would be as follows: + +!!! example + ``` + # METADATA + package mypackage + + deny { + input.evil == "foo bar" + } + ``` + +If this check is run against an offending Dockerfile(s), there will not be any issues as the check will fail to evaluate. +Although the check's failure to evaluate is legitimate, this should not result in a positive result for the scan. + +For instance if we have a check that checks for misconfigurations in a `Dockerfile`, we could define the +schema as such + +!!! example + ``` + # METADATA + # schemas: + # - input: schema["dockerfile"] + package mypackage + + deny { + input.evil == "foo bar" + } + ``` + +Here `input: schema["dockerfile"]` points to a schema that expects a valid `Dockerfile` as input. An example of this +can be found [here](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/dockerfile.json). + +Now if this check is evaluated against, a more descriptive error will be available to help fix the problem. + +```bash +1 error occurred: testcheck.rego:8: rego_type_error: undefined ref: input.evil + input.evil + ^ + have: "evil" + want (one of): ["Stages"] +``` + + +## Custom Checks with Custom Schemas + +You can also bring a custom check that defines one or more custom schema. + +!!! example + ``` + # METADATA + # schemas: + # - input: schema["fooschema"] + # - input: schema["barschema"] + package mypackage + + deny { + input.evil == "foo bar" + } + ``` + +The checks can be placed in a structure as follows + +!!! example + ``` + /Users/user/my-custom-checks + ├── my_check.rego + └── schemas + └── fooschema.json + └── barschema.json + ``` + +To use such a check with Trivy, use the `--config-check` flag that points to the check file or to the directory where the schemas and checks are contained. + +```bash +$ trivy --config-check=/Users/user/my-custom-checks +``` + +For more details on how to define schemas within Rego checks, please see the [OPA guide](https://www.openpolicyagent.org/docs/latest/policy-language/#schema-annotations) that describes it in more detail. + +### Scan arbitrary JSON and YAML configurations +By default, scanning JSON and YAML configurations is disabled, since Trivy does not contain built-in checks for these configurations. To enable it, pass the `json` or `yaml` to `--misconfig-scanners`. Trivy will pass each file as is to the checks input. + + +!!! example + ```bash + $ cat iac/serverless.yaml + service: serverless-rest-api-with-pynamodb + + frameworkVersion: ">=2.24.0" + + plugins: + - serverless-python-requirements + ... + + $ cat serverless.rego + # METADATA + # title: Serverless Framework service name not starting with "aws-" + # description: Ensure that Serverless Framework service names start with "aws-" + # schemas: + # - input: schema["serverless-schema"] + # custom: + # id: SF001 + # severity: LOW + package user.serverless001 + + deny[res] { + not startswith(input.service, "aws-") + res := result.new( + sprintf("Service name %q is not allowed", [input.service]), + input.service + ) + } + + $ trivy config --misconfig-scanners=json,yaml --config-check ./serverless.rego --check-namespaces user ./iac + serverless.yaml (yaml) + + Tests: 4 (SUCCESSES: 3, FAILURES: 1) + Failures: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + + LOW: Service name "serverless-rest-api-with-pynamodb" is not allowed + ═════════════════════════════════════════════════════════════════════════════════════════════════════════ + Ensure that Serverless Framework service names start with "aws-" + ``` + +!!! note + In the case above, the custom check specified has a metadata annotation for the input schema `input: schema["serverless-schema"]`. This allows Trivy to type check the input IaC files provided. + +Optionally, you can also pass schemas using the `config-file-schemas` flag. Trivy will use these schemas for file filtering and type checking in Rego checks. + +!!! example + ```bash + $ trivy config --misconfig-scanners=json,yaml --config-check ./serverless.rego --check-namespaces user --config-file-schemas ./serverless-schema.json ./iac + ``` + +If the `--config-file-schemas` flag is specified Trivy ensures that each input IaC config file being scanned is type-checked against the schema. If the input file does not match any of the passed schemas, it will be ignored. + +If the schema is specified in the check metadata and is in the directory specified in the `--config-check` argument, it will be automatically loaded as specified [here](./schema.md#custom-checks-with-custom-schemas), and will only be used for type checking in Rego. + +!!! note + If a user specifies the `--config-file-schemas` flag, all input IaC config files are ensured that they pass type-checking. It is not required to pass an input schema in case type checking is not required. This is helpful for scenarios where you simply want to write a Rego check and pass in IaC input for it. Such a use case could include scanning for a new service which Trivy might not support just yet. + +!!! tip + It is also possible to specify multiple input schemas with `--config-file-schema` flag as it can accept a comma seperated list of file paths or a directory as input. In the case of multiple schemas being specified, all of them will be evaluated against all the input files. + + diff --git a/docs/guide/scanner/misconfiguration/custom/selectors.md b/docs/guide/scanner/misconfiguration/custom/selectors.md new file mode 100644 index 000000000000..3def3d8e9a20 --- /dev/null +++ b/docs/guide/scanner/misconfiguration/custom/selectors.md @@ -0,0 +1,51 @@ +# Input Selectors + +## Overview +Sometimes you might want to limit a certain policy to only be run on certain resources. This can be +achieved with input selectors. + +## Use case +For instance, if you have a custom policy that you only want to be evaluated if a certain resource type is being scanned. +In such a case you could utilize input selectors to limit its evaluation on only those resources. + +!!! example + ``` + # METADATA + # title: "RDS Publicly Accessible" + # description: "Ensures RDS instances are not launched into the public cloud." + # custom: + # input: + # selector: + # - type: cloud + # subtypes: + # - provider: aws + # service: rds + package builtin.aws.rds.aws0999 + + deny[res] { + instance := input.aws.rds.instances[_] + instance.publicaccess.value + res := result.new("Instance has Public Access enabled", instance.publicaccess) + ``` + +Observe the following `subtypes` defined: +```yaml + # subtypes: + # - provider: aws + # service: rds +``` + +They will ensure that the policy is only run when the input to such a policy contains an `RDS` instance. + +## Enabling selectors and subtypes +Currently, the following are supported: + +| Selector | Subtype fields required | Example | +|--------------------------|-------------------------|---------------------------------| +| Cloud (AWS, Azure, etc.) | `provider`, `service` | `provider: aws`, `service: rds` | +| Kubernetes | | `type: kubernetes` | +| Dockerfile | | `type: dockerfile` | + + +## Default behaviour +If no subtypes or selectors are specified, the policy will be evaluated regardless of input. \ No newline at end of file diff --git a/docs/guide/scanner/misconfiguration/custom/testing.md b/docs/guide/scanner/misconfiguration/custom/testing.md new file mode 100644 index 000000000000..042b96753cb1 --- /dev/null +++ b/docs/guide/scanner/misconfiguration/custom/testing.md @@ -0,0 +1,90 @@ +# Testing +It is highly recommended to write tests for your custom checks. + +## Rego testing +To help you verify the correctness of your custom checks, OPA gives you a framework that you can use to write tests for your checks. +By writing tests for your custom checks you can speed up the development process of new rules and reduce the amount of time it takes to modify rules as requirements evolve. + +For more details, see [Policy Testing][opa-testing]. + +!!! example + ``` + package user.dockerfile.ID002 + + test_add_denied { + r := deny with input as {"stages": {"alpine:3.13": [ + {"Cmd": "add", "Value": ["/target/resources.tar.gz", "resources.jar"]}, + {"Cmd": "add", "Value": ["/target/app.jar", "app.jar"]}, + ]}} + + count(r) == 1 + r[_] == "Consider using 'COPY /target/app.jar app.jar' command instead of 'ADD /target/app.jar app.jar'" + } + ``` + +To write tests for custom checks, you can refer to existing tests under [trivy-checks][trivy-checks]. + +## Go testing +[Fanal][fanal] which is a core library of Trivy can be imported as a Go library. +You can scan config files in Go and test your custom checks using Go's testing methods, such as [table-driven tests][table]. +This allows you to use the actual configuration file as input, making it easy to prepare test data and ensure that your custom checks work in practice. + +In particular, Dockerfile and HCL need to be converted to structural data as input, which may be different from the expected input format. + +!!! tip + We recommend writing OPA and Go tests both since they have different roles, like unit tests and integration tests. + +The following example stores allowed and denied configuration files in a directory. +`Successes` contains the result of successes, and `Failures` contains the result of failures. + +``` go +{ + name: "disallowed ports", + input: "configs/", + fields: fields{ + policyPaths: []string{"policy"}, + dataPaths: []string{"data"}, + namespaces: []string{"user"}, + }, + want: []types.Misconfiguration{ + { + FileType: types.Dockerfile, + FilePath: "Dockerfile.allowed", + Successes: types.MisconfResults{ + { + Namespace: "user.dockerfile.ID002", + PolicyMetadata: types.PolicyMetadata{ + ID: "ID002", + Type: "Docker Custom Check", + Title: "Disallowed ports exposed", + Severity: "HIGH", + }, + }, + }, + }, + { + FileType: types.Dockerfile, + FilePath: "Dockerfile.denied", + Failures: types.MisconfResults{ + { + Namespace: "user.dockerfile.ID002", + Message: "Port 23 should not be exposed", + PolicyMetadata: types.PolicyMetadata{ + ID: "ID002", + Type: "Docker Custom Check", + Title: "Disallowed ports exposed", + Severity: "HIGH", + }, + }, + }, + }, + }, +}, +``` + +`Dockerfile.allowed` has one successful result in `Successes`, while `Dockerfile.denied` has one failure result in `Failures`. + +[opa-testing]: https://www.openpolicyagent.org/docs/latest/policy-testing/ +[defsec]: https://github.com/aquasecurity/trivy-checks/tree/main +[table]: https://go.dev/wiki/TableDrivenTests +[fanal]: https://github.com/aquasecurity/fanal \ No newline at end of file diff --git a/docs/guide/scanner/misconfiguration/index.md b/docs/guide/scanner/misconfiguration/index.md new file mode 100644 index 000000000000..da6fad3576dd --- /dev/null +++ b/docs/guide/scanner/misconfiguration/index.md @@ -0,0 +1,414 @@ +# Misconfiguration Scanning +Trivy provides built-in checks to detect configuration issues in popular Infrastructure as Code files, such as: Docker, Kubernetes, Terraform, CloudFormation, and more. +In addition to built-in checks, you can write your own custom checks, as you can see [here][custom]. + +## Quick start + +Simply specify a directory containing IaC files such as Terraform, CloudFormation, Azure ARM templates, Helm Charts and Dockerfile. + +```bash +$ trivy config [YOUR_IaC_DIRECTORY] +``` + + +!!! example + ``` + $ ls build/ + Dockerfile + $ trivy config ./build + 2022-05-16T13:29:29.952+0100 INFO Detected config files: 1 + + Dockerfile (dockerfile) + ======================= + Tests: 23 (SUCCESSES: 22, FAILURES: 1) + Failures: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0) + + MEDIUM: Specify a tag in the 'FROM' statement for image 'alpine' + ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated. + + See https://avd.aquasec.com/misconfig/ds001 + ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Dockerfile:1 + ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 1 [ FROM alpine:latest + ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ``` + +You can also enable misconfiguration detection in container image, filesystem and git repository scanning via `--scanners misconfig`. + +```bash +$ trivy image --scanners misconfig IMAGE_NAME +``` + +```bash +$ trivy fs --scanners misconfig /path/to/dir +``` + +!!! note + Misconfiguration detection is not enabled by default in `image`, `fs` and `repo` subcommands. + +Unlike the `config` subcommand, `image`, `fs` and `repo` subcommands can also scan for vulnerabilities and secrets at the same time. +You can specify `--scanners vuln,misconfig,secret` to enable vulnerability and secret detection as well as misconfiguration detection. + + +!!! example + ``` bash + $ ls myapp/ + Dockerfile Pipfile.lock + $ trivy fs --scanners vuln,misconfig,secret --severity HIGH,CRITICAL myapp/ + 2022-05-16T13:42:21.440+0100 INFO Number of language-specific files: 1 + 2022-05-16T13:42:21.440+0100 INFO Detecting pipenv vulnerabilities... + 2022-05-16T13:42:21.440+0100 INFO Detected config files: 1 + + Pipfile.lock (pipenv) + ===================== + Total: 1 (HIGH: 1, CRITICAL: 0) + + ┌──────────┬────────────────┬──────────┬───────────────────┬───────────────┬───────────────────────────────────────────────────────────┐ + │ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ + ├──────────┼────────────────┼──────────┼───────────────────┼───────────────┼───────────────────────────────────────────────────────────┤ + │ httplib2 │ CVE-2021-21240 │ HIGH │ 0.12.1 │ 0.19.0 │ python-httplib2: Regular expression denial of service via │ + │ │ │ │ │ │ malicious header │ + │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-21240 │ + └──────────┴────────────────┴──────────┴───────────────────┴───────────────┴───────────────────────────────────────────────────────────┘ + + Dockerfile (dockerfile) + ======================= + Tests: 17 (SUCCESSES: 16, FAILURES: 1) + Failures: 1 (HIGH: 1, CRITICAL: 0) + + HIGH: Last USER command in Dockerfile should not be 'root' + ════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ + Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile. + + See https://avd.aquasec.com/misconfig/ds002 + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + Dockerfile:3 + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 3 [ USER root + ──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + ``` + +In the above example, Trivy detected vulnerabilities of Python dependencies and misconfigurations in Dockerfile. + +## Type detection +The specified directory can contain mixed types of IaC files. +Trivy automatically detects config types and applies relevant checks. + +For example, the following example holds IaC files for Terraform, CloudFormation, Kubernetes, Helm Charts, and Dockerfile in the same directory. + +``` bash +$ ls iac/ +Dockerfile deployment.yaml main.tf mysql-8.8.26.tar +$ trivy config --severity HIGH,CRITICAL ./iac +``` + +
+Result + +```bash +2022-06-06T11:01:21.142+0100 INFO Detected config files: 8 + +Dockerfile (dockerfile) + +Tests: 21 (SUCCESSES: 20, FAILURES: 1) +Failures: 1 (MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +HIGH: Specify at least 1 USER command in Dockerfile with non-root user as argument +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile. + +See https://avd.aquasec.com/misconfig/ds002 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + +deployment.yaml (kubernetes) + +Tests: 20 (SUCCESSES: 15, FAILURES: 5) +Failures: 5 (MEDIUM: 4, HIGH: 1, CRITICAL: 0) + +MEDIUM: Container 'hello-kubernetes' of Deployment 'hello-kubernetes' should set 'securityContext.allowPrivilegeEscalation' to false +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node. + +See https://avd.aquasec.com/misconfig/ksv001 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + deployment.yaml:16-19 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 16 ┌ - name: hello-kubernetes + 17 │ image: hello-kubernetes:1.5 + 18 │ ports: + 19 └ - containerPort: 8080 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + +HIGH: Deployment 'hello-kubernetes' should not specify '/var/run/docker.socker' in 'spec.template.volumes.hostPath.path' +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +Mounting docker.sock from the host can give the container full root access to the host. + +See https://avd.aquasec.com/misconfig/ksv006 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + deployment.yaml:6-29 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 6 ┌ replicas: 3 + 7 │ selector: + 8 │ matchLabels: + 9 │ app: hello-kubernetes + 10 │ template: + 11 │ metadata: + 12 │ labels: + 13 │ app: hello-kubernetes + 14 └ spec: + .. +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + +MEDIUM: Container 'hello-kubernetes' of Deployment 'hello-kubernetes' should set 'securityContext.runAsNonRoot' to true +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +'runAsNonRoot' forces the running image to run as a non-root user to ensure least privileges. + +See https://avd.aquasec.com/misconfig/ksv012 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + deployment.yaml:16-19 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 16 ┌ - name: hello-kubernetes + 17 │ image: hello-kubernetes:1.5 + 18 │ ports: + 19 └ - containerPort: 8080 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + +MEDIUM: Deployment 'hello-kubernetes' should not set 'spec.template.volumes.hostPath' +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +HostPath volumes must be forbidden. + +See https://avd.aquasec.com/misconfig/ksv023 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + deployment.yaml:6-29 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 6 ┌ replicas: 3 + 7 │ selector: + 8 │ matchLabels: + 9 │ app: hello-kubernetes + 10 │ template: + 11 │ metadata: + 12 │ labels: + 13 │ app: hello-kubernetes + 14 └ spec: + .. +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + +MEDIUM: Deployment 'hello-kubernetes' should set 'securityContext.sysctl' to the allowed values +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +Sysctls can disable security mechanisms or affect all containers on a host, and should be disallowed except for an allowed 'safe' subset. A sysctl is considered safe if it is namespaced in the container or the Pod, and it is isolated from other Pods or processes on the same Node. + +See https://avd.aquasec.com/misconfig/ksv026 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + deployment.yaml:6-29 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 6 ┌ replicas: 3 + 7 │ selector: + 8 │ matchLabels: + 9 │ app: hello-kubernetes + 10 │ template: + 11 │ metadata: + 12 │ labels: + 13 │ app: hello-kubernetes + 14 └ spec: + .. +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + + +mysql-8.8.26.tar:templates/primary/statefulset.yaml (helm) + +Tests: 20 (SUCCESSES: 18, FAILURES: 2) +Failures: 2 (MEDIUM: 2, HIGH: 0, CRITICAL: 0) + +MEDIUM: Container 'mysql' of StatefulSet 'mysql' should set 'securityContext.allowPrivilegeEscalation' to false +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node. + +See https://avd.aquasec.com/misconfig/ksv001 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + mysql-8.8.26.tar:templates/primary/statefulset.yaml:56-130 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 56 ┌ - name: mysql + 57 │ image: docker.io/bitnami/mysql:8.0.28-debian-10-r23 + 58 │ imagePullPolicy: "IfNotPresent" + 59 │ securityContext: + 60 │ runAsUser: 1001 + 61 │ env: + 62 │ - name: BITNAMI_DEBUG + 63 │ value: "false" + 64 └ - name: MYSQL_ROOT_PASSWORD + .. +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + +MEDIUM: Container 'mysql' of StatefulSet 'mysql' should set 'securityContext.runAsNonRoot' to true +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +'runAsNonRoot' forces the running image to run as a non-root user to ensure least privileges. + +See https://avd.aquasec.com/misconfig/ksv012 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + mysql-8.8.26.tar:templates/primary/statefulset.yaml:56-130 +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 56 ┌ - name: mysql + 57 │ image: docker.io/bitnami/mysql:8.0.28-debian-10-r23 + 58 │ imagePullPolicy: "IfNotPresent" + 59 │ securityContext: + 60 │ runAsUser: 1001 + 61 │ env: + 62 │ - name: BITNAMI_DEBUG + 63 │ value: "false" + 64 └ - name: MYSQL_ROOT_PASSWORD + .. +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +``` + +
+ +You can see the config type next to each file name. + +!!! example +``` bash +Dockerfile (dockerfile) +======================= +Tests: 23 (SUCCESSES: 22, FAILURES: 1) +Failures: 1 (HIGH: 1, CRITICAL: 0) + +... + +deployment.yaml (kubernetes) +============================ +Tests: 28 (SUCCESSES: 15, FAILURES: 13) +Failures: 13 (MEDIUM: 4, HIGH: 1, CRITICAL: 0) + +... + +main.tf (terraform) +=================== +Tests: 23 (SUCCESSES: 14, FAILURES: 9) +Failures: 9 (HIGH: 6, CRITICAL: 1) + +... + +bucket.yaml (cloudformation) +============================ +Tests: 9 (SUCCESSES: 3, FAILURES: 6) +Failures: 6 (UNKNOWN: 0, LOW: 0, MEDIUM: 2, HIGH: 4, CRITICAL: 0) + +... + +mysql-8.8.26.tar:templates/primary/statefulset.yaml (helm) +========================================================== +Tests: 20 (SUCCESSES: 18, FAILURES: 2) +Failures: 2 (MEDIUM: 2, HIGH: 0, CRITICAL: 0) +``` + +## Scan raw configurations +IaC configurations from cloud providers such as Terraform, CloudFormation, and ARM are converted into a unified structure that is exported to Rego. Checks are developed only for the unified structure, not for each configuration type with its own structure. This avoids duplication and simplifies maintenance. Using the unified structure has a limitation: it is not possible to create checks for resources or attributes that are not exported. + +The `--raw-config-scanners` flag allows scanning the raw configuration — that is, evaluated but not converted into the unified structure. Currently, only `terraform` is supported. + +!!! note + The raw configuration scanner does not work on its own. To use `--raw-config-scanners`, you must also specify the corresponding `--misconfig-scanners`. The report will include results from both scanners. + +For more information on custom checks and exported data schemas, see [here](../misconfiguration/custom/index.md). + +Example check: +```rego +# METADATA +# title: AWS required resource tags +# description: Ensure required tags are set on AWS resources +# scope: package +# schemas: +# - input: schema["terraform-raw"] +# custom: +# id: USR-TFRAW-0001 +# severity: CRITICAL +# short_code: required-aws-resource-tags +# recommended_actions: Add the required tags to AWS resources. +# input: +# selector: +# - type: terraform-raw +package user.terraform.required_aws_tags + +import rego.v1 + +resource_types_to_check := {"aws_s3_bucket"} + +resources_to_check := {block | + some module in input.modules + some block in module.blocks + block.kind == "resource" + block.type in resource_types_to_check +} + +required_tags := {"Access", "Owner"} + +deny contains res if { + some block in resources_to_check + not block.attributes.tags + res := result.new( + sprintf("The resource %q does not contain the following required tags: %v", [block.type, required_tags]), + block, + ) +} + +deny contains res if { + some block in resources_to_check + tags_attr := block.attributes.tags + tags := object.keys(tags_attr.value) + missing_tags := required_tags - tags + count(missing_tags) > 0 + res := result.new( + sprintf("The resource %q does not contain the following required tags: %v", [block.type, missing_tags]), + tags_attr, + ) +} +``` + +Running Trivy: +```bash +trivy conf main.tf \ + --check-namespaces user \ + --config-check examples/terraform-raw/required-aws-tags.rego \ + --misconfig-scanners terraform --raw-config-scanners terraform +``` + +Example output: +```bash +main.tf (terraform) + +Tests: 10 (SUCCESSES: 0, FAILURES: 10) +Failures: 10 (UNKNOWN: 0, LOW: 2, MEDIUM: 1, HIGH: 6, CRITICAL: 1) + + (CRITICAL): The resource "aws_s3_bucket" does not contain the following required tags: {"Access"} +═══════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +Ensure required tags are set on AWS resources +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + main.tf:3-5 + via main.tf:1-6 (aws_s3_bucket.this) +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 1 resource "aws_s3_bucket" "this" { + 2 bucket = "test" + 3 ┌ tags = { + 4 │ Owner: "user" + 5 └ } + 6 } +─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +``` + +## External connectivity +Trivy needs to connect to the internet to download the checks bundle. If you are running Trivy in an air-gapped environment, or a tightly controlled network, please refer to the [Advanced Network Scenarios document](../../advanced/air-gap.md). + +## Configuration +More misconfiguration scanning specific configurations can be found [here](../misconfiguration/config/config.md). + +[custom]: custom/index.md \ No newline at end of file diff --git a/docs/guide/scanner/secret.md b/docs/guide/scanner/secret.md new file mode 100644 index 000000000000..1f42263d0c48 --- /dev/null +++ b/docs/guide/scanner/secret.md @@ -0,0 +1,317 @@ +# Secret Scanning + +Trivy scans any container image, filesystem, and git repository to detect exposed secrets like passwords, API keys, and tokens. +Secret scanning is enabled by default. + +Trivy will scan every plaintext file, according to builtin rules or configuration. Also, Trivy can detect secrets in compiled Python files (`.pyc`). + +There are plenty of builtin rules: + +- AWS access key +- GCP service account +- GitHub personal access token +- GitLab personal access token +- Slack access token +- etc. + +You can see a full list of [built-in rules][builtin] and [built-in allow rules][builtin-allow]. + +!!! tip + If your secret is not detected properly, please make sure that your file including the secret is not in [the allowed paths][builtin-allow]. + You can disable allow rules via [disable-allow-rules](#disable-rules). + +## Quick start +This section shows how to scan secrets in container image and filesystem. Other subcommands should be the same. + +### Container image +Specify an image name. + +``` shell +$ trivy image myimage:1.0.0 +2022-04-21T18:56:44.099+0300 INFO Detected OS: alpine +2022-04-21T18:56:44.099+0300 INFO Detecting Alpine vulnerabilities... +2022-04-21T18:56:44.101+0300 INFO Number of language-specific files: 0 + +myimage:1.0.0 (alpine 3.15.0) +============================= +Total: 6 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 2) + ++--------------+------------------+----------+-------------------+---------------+---------------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++--------------+------------------+----------+-------------------+---------------+---------------------------------------+ +| busybox | CVE-2022-28391 | CRITICAL | 1.34.1-r3 | 1.34.1-r5 | CVE-2022-28391 affecting | +| | | | | | package busybox 1.35.0 | +| | | | | | -->avd.aquasec.com/nvd/cve-2022-28391 | ++--------------+------------------| |-------------------+---------------+---------------------------------------+ +| ssl_client | CVE-2022-28391 | | 1.34.1-r3 | 1.34.1-r5 | CVE-2022-28391 affecting | +| | | | | | package busybox 1.35.0 | +| | | | | | -->avd.aquasec.com/nvd/cve-2022-28391 | ++--------------+------------------+----------+-------------------+---------------+---------------------------------------+ + +app/secret.sh (secrets) +======================= +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 1) + ++----------+-------------------+----------+---------+--------------------------------+ +| CATEGORY | DESCRIPTION | SEVERITY | LINE NO | MATCH | ++----------+-------------------+----------+---------+--------------------------------+ +| AWS | AWS Access Key ID | CRITICAL | 10 | export AWS_ACCESS_KEY_ID=***** | ++----------+-------------------+----------+---------+--------------------------------+ +``` + + +!!! tip + Trivy tries to detect a base image and skip those layers for secret scanning. + A base image usually contains a lot of files and makes secret scanning much slower. + If a secret is not detected properly, you can see base layers with the `--debug` flag. + +### Filesystem + +``` shell +$ trivy fs /path/to/your_project +...(snip)... + +certs/key.pem (secrets) +======================== +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + ++----------------------+------------------------+----------+---------+---------------------------------+ +| CATEGORY | DESCRIPTION | SEVERITY | LINE NO | MATCH | ++----------------------+------------------------+----------+---------+---------------------------------+ +| AsymmetricPrivateKey | Asymmetric Private Key | HIGH | 1 | -----BEGIN RSA PRIVATE KEY----- | ++----------------------+------------------------+----------+---------+---------------------------------+ +``` + + +!!! tip + Your project may have some secrets for testing. You can skip them with `--skip-dirs` or `--skip-files`. + We would recommend specifying these options so that the secret scanning can be faster if those files don't need to be scanned. + Also, you can specify paths to be allowed in a configuration file. See the detail [here](#configuration). + +## Configuration +This section describes secret-specific configuration. +Other common options are documented [here](../configuration/index.md). + +Trivy has a set of builtin rules for secret scanning, which can be extended or modified by a configuration file. +Trivy tries to load `trivy-secret.yaml` in the current directory by default. +If the file doesn't exist, only built-in rules are used. +You can customize the config file path via the `--secret-config` flag. + +!!! warning + Trivy uses [Golang regexp package](https://pkg.go.dev/regexp/syntax#hdr-Syntax). To use `^` and `$` as symbols of begin and end of line use multi-line mode -`(?m)`. + +### Custom Rules +Trivy allows defining custom rules. + +``` yaml +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + path: .*\.sh + keywords: + - secret + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret + allow-rules: + - id: skip-text + description: skip text files + path: .*\.txt +``` + +`id` (required) +: - Unique identifier for this rule. + +`category` (required) +: - String used for metadata and reporting purposes. + +`title` (required) +: - Short human-readable title of the rule. + +`severity` (required) +: - How critical this rule is. +- Allowed values: +- CRITICAL +- HIGH +- MEDIUM +- LOW + +`regex` (required) +: - Golang regular expression used to detect secrets. + +`path` (optional) +: - Golang regular expression used to match paths. + +`keywords` (optional, recommended) +: - Keywords are used for pre-regex check filtering. +- Rules that contain keywords will perform a quick string compare check to make sure the keyword(s) are in the content being scanned. +- Ideally these values should either be part of the identifier or unique strings specific to the rule's regex. +- It is recommended to define for better performance. + +`allow-rules` (optional) +: - Allow rules for a single rule to reduce false positives with known secrets. +- The details are below. + +### Allow Rules +If the detected secret is matched with the specified `regex`, then that secret will be skipped and not detected. +The same logic applies for `path`. + +`allow-rules` can be defined globally and per each rule. The fields are the same. + +``` yaml +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + allow-rules: + - id: skip-text + description: skip text files + path: .*\.txt +allow-rules: + - id: social-security-number + description: skip social security number + regex: 219-09-9999 +``` + + +`id` (required) +: - Unique identifier for this allow rule. + +`description` (optional) +: - Short human-readable description of this allow rule. + +`regex` (optional) +: - Golang regular expression used to allow detected secrets. +- `regex` or `path` must be specified. + +`path` (optional) +: - Golang regular expression used to allow matched paths. +- `regex` or `path` must be specified. + +### Enable Rules +Trivy provides plenty of out-of-box rules and allow rules, but you may not need all of them. +In that case, `enable-builtin-rules` will be helpful. +If you just need AWS secret detection, you can enable only relevant rules as shown below. +It specifies AWS-related rule IDs in `enable-builtin-rules`. +All other rules are disabled, so the scanning will be much faster. +We would strongly recommend using this option if you don't need all rules. + +You can see a full list of [built-in rule IDs][builtin] and [built-in allow rule IDs][builtin-allow]. + +``` yaml +enable-builtin-rules: + - aws-access-key-id + - aws-account-id + - aws-secret-access-key +``` + +### Disable Rules +Trivy offers built-in rules and allow rules, but you may want to disable some of them. +For example, you don't use Slack, so Slack doesn't have to be scanned. +You can specify the Slack rule IDs, `slack-access-token` and `slack-web-hook` in `disable-rules` so that those rules will be disabled for less false positives. + +You should specify either `enable-builtin-rules` or `disable-rules`. +If they both are specified, `disable-rules` takes precedence. +In case `github-pat` is specified in `enable-builtin-rules` and `disable-rules`, it will be disabled. + +In addition, there are some allow rules. +Markdown files are ignored by default, but you may want to scan markdown files as well. +You can disable the allow rule by adding `markdown` to `disable-allow-rules`. + +You can see a full list of [built-in rule IDs][builtin] and [built-in allow rule IDs][builtin-allow]. + +``` yaml +disable-rules: + - slack-access-token + - slack-web-hook +disable-allow-rules: + - markdown +``` + +## Recommendation +We would recommend specifying `--skip-dirs` for faster secret scanning. +In container image scanning, Trivy walks the file tree rooted at `/` and scans all the files other than [built-in allowed paths][builtin-allow]. +It will take a while if your image contains a lot of files even though Trivy tries to avoid scanning layers from a base image. +If you want to make scanning faster, `--skip-dirs` and `--skip-files` helps so that Trivy will skip scanning those files and directories. +You can see more options [here](../configuration/others.md). + +`allow-rules` is also helpful. See the [allow-rules](#allow-rules) section. + +In addition, all the built-in rules are enabled by default, so it takes some time to scan all of them. +If you don't need all those rules, you can use `enable-builtin-rules` or `disable-rules` in the configuration file. +You should use `enable-builtin-rules` if you need only AWS secret detection, for example. +All rules are disabled except for the ones you specify, so it runs very fast. +On the other hand, you should use `disable-rules` if you just want to disable some built-in rules. +See the [enable-rules](#enable-rules) and [disable-rules](#disable-rules) sections for the detail. + +If you don't need secret scanning, you can disable it via the `--scanners` flag. + +```shell +$ trivy image --scanners vuln alpine:3.15 +``` + +## Example +`trivy-secret.yaml` in the working directory is loaded by default. + +``` yaml +$ cat trivy-secret.yaml +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] +allow-rules: + - id: social-security-number + description: skip social security number + regex: 219-09-9999 + - id: log-dir + description: skip log directory + path: ^\/var\/log\/ +disable-rules: + - slack-access-token + - slack-web-hook +disable-allow-rules: + - markdown + +# The following command automatically loads the above configuration. +$ trivy image YOUR_IMAGE +``` + +Also, you can customize the config file path via `--secret-config`. + +``` yaml +$ cat ./secret-config/trivy.yaml +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + allow-rules: + - id: skip-text + description: skip text files + path: .*\.txt +enable-builtin-rules: + - aws-access-key-id + - aws-account-id + - aws-secret-access-key +disable-allow-rules: + - usr-dirs + +# Pass the above config with `--secret-config`. +$ trivy fs --secret-config ./secret-config/trivy.yaml /path/to/your_project +``` + +## Credit +This feature is inspired by [gitleaks][gitleaks]. + +[builtin]: https://github.com/aquasecurity/trivy/blob/{{ git.tag }}/pkg/fanal/secret/builtin-rules.go +[builtin-allow]: https://github.com/aquasecurity/trivy/blob/{{ git.tag }}/pkg/fanal/secret/builtin-allow-rules.go +[gitleaks]: https://github.com/gitleaks/gitleaks + +[builtin]: https://github.com/aquasecurity/trivy/blob/main/pkg/fanal/secret/builtin-rules.go +[builtin-allow]: https://github.com/aquasecurity/trivy/blob/main/pkg/fanal/secret/builtin-allow-rules.go diff --git a/docs/guide/scanner/vulnerability.md b/docs/guide/scanner/vulnerability.md new file mode 100644 index 000000000000..774b9f8353fb --- /dev/null +++ b/docs/guide/scanner/vulnerability.md @@ -0,0 +1,451 @@ +# Vulnerability Scanning +Trivy detects known vulnerabilities in software components that it finds in the scan target. + +The following are supported: + +- [OS packages](#os-packages) +- [Language-specific packages](#language-specific-packages) +- [Non-packaged software](#non-packaged-software) +- [Kubernetes components](#kubernetes) + +## OS Packages +Trivy is capable of automatically detecting installed OS packages when scanning container images, VM images and running hosts. + +!!! note + Trivy focuses on packages provided by official OS vendors and may skip [third-party packages](#third-party-packages). + +### Supported OS +See [here](../coverage/os/index.md#supported-os) for the supported OSes. + +### Data Sources + +| OS | Source | +|---------------------------|--------------------------------------------------------------| +| Arch Linux | [Vulnerable Issues][arch] | +| Alpine Linux | [secdb][alpine] | +| Wolfi Linux | [secdb][wolfi] | +| Chainguard | [secdb][chainguard] | +| MinimOS | [secdb][minimos] | +| Amazon Linux | [Amazon Linux Security Center][amazon] | +| Echo | [Echo][echo] | +| Debian | [Security Bug Tracker][debian-tracker] / [OVAL][debian-oval] | +| Ubuntu | [Ubuntu CVE Tracker][ubuntu] | +| RHEL/CentOS | [OVAL][rhel-oval] / [Security Data][rhel-api] | +| AlmaLinux | [AlmaLinux Product Errata][alma] | +| Rocky Linux | [Rocky Linux UpdateInfo][rocky] | +| Oracle Linux | [OVAL][oracle] | +| Azure Linux (CBL-Mariner) | [OVAL][azure] | +| OpenSUSE/SLES | [CVRF][suse] | +| Photon OS | [Photon Security Advisory][photon] | +| Root.io | [Root.io Patch Feed][rootio] | +| Seal Security | [Seal Security vulnerability feed][seal] | + +#### Data Source Selection +Trivy **only** consumes security advisories from the sources listed in the above table. + +As for packages installed from OS package managers (`dpkg`, `yum`, `apk`, etc.), Trivy uses the advisory database from the appropriate **OS vendor**. + +For example: for a python package installed from `yum` (Amazon linux), Trivy will only get advisories from [ALAS][amazon]. +But for a python package installed from another source (e.g. `pip`), Trivy will get advisories from the `GitLab` and `GitHub` databases. + +This advisory selection is essential to avoid getting false positives because OS vendors usually backport upstream fixes, and the fixed version can be different from the upstream fixed version. + +#### Severity Selection +The severity is taken from the selected data source since the severity from vendors is more accurate. +Using CVE-2023-0464 as an example, while it is [rated as "HIGH" in NVD][nvd-CVE-2023-0464], Red Hat has marked its 'Impact' as ["Low"][redhat-CVE-2023-0464]. +As a result, Trivy will display it as "Low". + +The severity depends on the compile option, the default configuration, etc. +NVD doesn't know how the vendor distributes the software. +Red Hat evaluates the severity more accurately. +That's why Trivy prefers vendor scores over NVD. + +If the data source does not provide a severity, the severity is determined based on the CVSS score as follows: + +| Base Score Range | Severity | +| ---------------- | -------- | +| 0.1-3.9 | Low | +| 4.0-6.9 | Medium | +| 7.0-8.9 | High | +| 9.0-10.0 | Critical | + +If the CVSS score is also not provided, it falls back to [NVD][nvd]. + +NVD and some vendors may delay severity analysis, while other vendors, such as Red Hat, are able to quickly evaluate and announce the severity of vulnerabilities. +To avoid marking too many vulnerabilities as "UNKNOWN" severity, Trivy uses severity ratings from other vendors when the NVD information is not yet available. +The order of preference for vendor severity data can be found [here](https://github.com/aquasecurity/trivy-db/blob/79d0fbd1e246f3c77eef4b9826fe4bf65940b221/pkg/vulnsrc/vulnerability/vulnerability.go#L17-L19). + +You can reference `SeveritySource` in the [JSON reporting format](../configuration/reporting.md#json) to see from where the severity is taken for a given vulnerability. + +```shell +"SeveritySource": "debian", +``` + + +In addition, you can see all the vendor severity ratings. + +```json +"VendorSeverity": { + "amazon": 2, + "cbl-mariner": 4, + "ghsa": 4, + "nvd": 4, + "photon": 4, + "redhat": 2, + "ubuntu": 2 +} +``` + +Here is the severity mapping in Trivy: + +| Number | Severity | +|:------:|----------| +| 0 | Unknown | +| 1 | Low | +| 2 | Medium | +| 3 | High | +| 4 | Critical | + +If no vendor has a severity, the `UNKNOWN` severity will be used. + +### Third-Party Packages +Trivy skips vulnerability scanning for packages installed from third-party repositories (e.g., EPEL, Remi, Docker, NVIDIA) because they are not covered by official OS security advisories. +Scanning these packages against official advisories would lead to false positives, as the advisories only track packages distributed by the OS vendor. + +Trivy identifies third-party packages using heuristic methods such as checking the RPM vendor field or the APT source repository. +Since this detection is heuristic-based, it may not identify all third-party packages. +In such cases, unrecognized third-party packages will still be scanned against official advisories, which could result in false positives. + +Conversely, packages from lesser-known but legitimate OS vendors may be incorrectly classified as third-party and skipped, leading to false negatives. +If you notice such cases, please [report it](https://github.com/aquasecurity/trivy/discussions/new?category=bugs). + +### Unfixed Vulnerabilities +The unfixed/unfixable vulnerabilities mean that the patch has not yet been provided on their distribution. +To hide unfixed/unfixable vulnerabilities, you can use the `--ignore-unfixed` flag. + +## Language-specific Packages + +### Supported Languages +See [here](../coverage/language/index.md#supported-languages) for the supported languages. + +### Data Sources { #langpkg-data-sources } + +| Language | Source | Commercial Use | Delay[^1] | +|----------|-----------------------------------------------------|:--------------:|:---------:| +| PHP | [PHP Security Advisories Database][php] | ✅ | - | +| | [GitHub Advisory Database (Composer)][php-ghsa] | ✅ | - | +| Python | [GitHub Advisory Database (pip)][python-ghsa] | ✅ | - | +| Ruby | [Ruby Advisory Database][ruby] | ✅ | - | +| | [GitHub Advisory Database (RubyGems)][ruby-ghsa] | ✅ | - | +| Node.js | [Ecosystem Security Working Group][nodejs] | ✅ | - | +| | [GitHub Advisory Database (npm)][nodejs-ghsa] | ✅ | - | +| Java | [GitHub Advisory Database (Maven)][java-ghsa] | ✅ | - | +| Go | [GitHub Advisory Database (Go)][go-ghsa] | ✅ | - | +| | [Go Vulnerability Database][go-vulndb] | ✅ | - | +| Rust | [Open Source Vulnerabilities (crates.io)][rust-osv] | ✅ | - | +| .NET | [GitHub Advisory Database (NuGet)][dotnet-ghsa] | ✅ | - | +| C/C++ | [GitLab Advisories Community][gitlab] | ✅ | 1 month | +| Dart | [GitHub Advisory Database (Pub)][pub-ghsa] | ✅ | - | +| Elixir | [GitHub Advisory Database (Erlang)][erlang-ghsa] | ✅ | - | +| Swift | [GitHub Advisory Database (Swift)][swift-ghsa] | ✅ | - | +| Julia | [Open Source Vulnerabilities (Julia)][julia-osv] | ✅ | - | + +[^1]: Intentional delay between vulnerability disclosure and registration in the DB + +## Non-packaged software + +If you have software that is not managed by a package manager, Trivy can still detect vulnerabilities in it in some cases: + +- [Using SBOM from Sigstore Rekor](../supply-chain/attestation/rekor.md#non-packaged-binaries) +- [Go Binaries with embedded module information](../coverage/language/golang.md#go-binary) +- [Rust Binaries with embedded information](../coverage/language/rust.md#binaries) +- [SBOM embedded in container images](../supply-chain/sbom.md#sbom-detection-inside-targets) + +## Detection Behavior +Trivy prioritizes precision in vulnerability detection, aiming to minimize false positives while potentially accepting some false negatives. +This approach is particularly relevant in two key areas: + +- [Handling Software Installed via OS Packages](#handling-software-installed-via-os-packages) +- [Handling Packages with Unspecified Versions](#handling-packages-with-unspecified-versions) + +Trivy can also detect only specific packages: + +- [Subset of Package Types](#enabling-a-subset-of-package-types) +- [Specific package Relationship](#filtering-by-package-relationships) + +### Handling Software Installed via OS Packages +For files installed by OS package managers, such as `apt`, Trivy exclusively uses advisories from the OS vendor. +This means that even if a JAR file is present in a container image, if it was installed via an OS package manager (e.g., `apt`), Trivy will not analyze the JAR file itself and use upstream security advisories. + +For example, consider the Python `requests` package in Red Hat Universal Base Image 8: + +```bash +[root@987ee49dc93d /]# head -n 3 /usr/lib/python3.6/site-packages/requests-2.20.0-py3.6.egg-info/PKG-INFO +Metadata-Version: 2.1 +Name: requests +Version: 2.20.0 +``` + +Version 2.20.0 is installed, and this package is installed by `dnf`. + +```bash +[root@987ee49dc93d /]# rpm -ql python3-requests | grep PKG-INFO +/usr/lib/python3.6/site-packages/requests-2.20.0-py3.6.egg-info/PKG-INFO +``` + +At first glance, this might seem vulnerable to [CVE-2023-32681], which affects versions of requests prior to v2.31.0. +However, Red Hat backported the fix to v2.20.0-3 in [RHSA-2023:4520], and the package is not vulnerable. + +- Upstream (PyPI [requests]): Fixed in v2.31.0 +- Red Hat (`python-requests`): Backported fix applied in v2.20.0-3 (RHSA-2023:4520) + +If Trivy were to detect CVE-2023-32681 in this case, it would be a false positive. +This illustrates why using the correct security advisory is crucial to avoid false detections. +To minimize false positives, Trivy trusts the OS vendor's advisory for software installed via OS package managers and does not use upstream advisories for these packages. + +However, this approach may lead to false negatives if the OS vendor's advisories are delayed or missing. +In such cases, using [--detection-priority comprehensive](#detection-priority) allows Trivy to consider upstream advisories (e.g., [GitHub Advisory Database][ghsa]), potentially increasing false positives but reducing false negatives. + +### Handling Packages with Unspecified Versions +When a package version cannot be uniquely determined (e.g., `package-a: ">=3.0"`), Trivy typically skips vulnerability detection for that package to avoid false positives. +If a lock file is present with fixed versions, Trivy will use those for detection. + +To detect potential vulnerabilities even with unspecified versions, use [--detection-priority comprehensive](#detection-priority). +This option makes Trivy use the minimum version in the specified range for vulnerability detection. +While this may increase false positives if the actual version used is not the minimum, it helps reduce false negatives. + +## Package Detection +Vulnerability detection is based on package detection. +This section describes the specifics of package detection, which also affect SBOM generation. + +### Detection Priority + +Trivy provides a `--detection-priority` flag to control the balance between false positives and false negatives in package/vulnerability detection. +This concept is similar to the relationship between [precision and recall][precision-recall] in machine learning evaluation. + +```bash +$ trivy image --detection-priority {precise|comprehensive} alpine:3.15 +``` + +- `precise`: This mode prioritizes reducing false positives. It results in less noisy vulnerability reports but may miss some potential vulnerabilities. +- `comprehensive`: This mode aims to detect more vulnerabilities, potentially including some that might be false positives. + It provides broader coverage but may increase the noise in the results. + +The default value is `precise`. Also refer to the [detection behavior](#detection-behavior) section for more information. + +Regardless of the chosen mode, user review of detected vulnerabilities is crucial: + +- `precise`: Review thoroughly, considering potential missed vulnerabilities. +- `comprehensive`: Carefully investigate each reported vulnerability due to increased false positive possibility. + +### Enabling a Subset of Package Types + +It's possible to only enable certain package types if you prefer. +You can do so by passing the `--pkg-types` option. +This flag takes a comma-separated list of package types. + +Available values: + +- os + - Scan OS packages managed by the OS package manager (e.g. `dpkg`, `yum`, `apk`). +- library + - Scan language-specific packages (e.g. packages installed by `pip`, `npm`, or `gem`). + +```bash +$ trivy image --pkg-types os ruby:2.4.0 +``` + + +
+Result + +```bash +2019-05-22T19:36:50.530+0200 INFO Updating vulnerability database... +2019-05-22T19:36:51.681+0200 INFO Detecting Alpine vulnerabilities... +2019-05-22T19:36:51.685+0200 INFO Updating npm Security DB... +2019-05-22T19:36:52.389+0200 INFO Detecting npm vulnerabilities... +2019-05-22T19:36:52.390+0200 INFO Updating pipenv Security DB... +2019-05-22T19:36:53.406+0200 INFO Detecting pipenv vulnerabilities... + +ruby:2.4.0 (debian 8.7) +======================= +Total: 7 (UNKNOWN: 0, LOW: 1, MEDIUM: 1, HIGH: 3, CRITICAL: 2) + ++---------+------------------+----------+-------------------+---------------+----------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++---------+------------------+----------+-------------------+---------------+----------------------------------+ +| curl | CVE-2018-14618 | CRITICAL | 7.61.0-r0 | 7.61.1-r0 | curl: NTLM password overflow | +| | | | | | via integer overflow | ++ +------------------+----------+ +---------------+----------------------------------+ +| | CVE-2018-16839 | HIGH | | 7.61.1-r1 | curl: Integer overflow leading | +| | | | | | to heap-based buffer overflow in | +| | | | | | Curl_sasl_create_plain_message() | ++---------+------------------+----------+-------------------+---------------+----------------------------------+ +| git | CVE-2018-17456 | HIGH | 2.15.2-r0 | 2.15.3-r0 | git: arbitrary code execution | +| | | | | | via .gitmodules | ++ +------------------+ + + +----------------------------------+ +| | CVE-2018-19486 | | | | git: Improper handling of | +| | | | | | PATH allows for commands to be | +| | | | | | executed from... | ++---------+------------------+----------+-------------------+---------------+----------------------------------+ +| libssh2 | CVE-2019-3855 | CRITICAL | 1.8.0-r2 | 1.8.1-r0 | libssh2: Integer overflow in | +| | | | | | transport read resulting in | +| | | | | | out of bounds write... | ++---------+------------------+----------+-------------------+---------------+----------------------------------+ +| sqlite | CVE-2018-20346 | MEDIUM | 3.21.0-r1 | 3.25.3-r0 | CVE-2018-20505 CVE-2018-20506 | +| | | | | | sqlite: Multiple flaws in | +| | | | | | sqlite which can be triggered | +| | | | | | via... | ++---------+------------------+----------+-------------------+---------------+----------------------------------+ +| tar | CVE-2018-20482 | LOW | 1.29-r1 | 1.31-r0 | tar: Infinite read loop in | +| | | | | | sparse_dump_region function in | +| | | | | | sparse.c | ++---------+------------------+----------+-------------------+---------------+----------------------------------+ +``` + +
+ +!!! info + This flag filters the packages themselves, so it also affects the list of detected packages in JSON reports and SBOM generation. + +### Filtering by Package Relationships + +Trivy supports filtering vulnerabilities based on the relationship of packages within a project. +This is achieved through the `--pkg-relationships` flag. +This feature allows you to focus on vulnerabilities in specific types of dependencies, such as only those in direct dependencies. + +In Trivy, there are four types of package relationships: + +1. `root`: The root package being scanned +2. `workspace`: Workspaces of the root package (Currently only `pom.xml`, `yarn.lock` and `cargo.lock` files are supported) +3. `direct`: Direct dependencies of the root/workspace package +4. `indirect`: Transitive dependencies +5. `unknown`: Packages whose relationship cannot be determined + +The available relationships may vary depending on the ecosystem. +To see which relationships are supported for a particular project, you can use the JSON output format and check the `Relationship` field: + +``` +$ trivy repo -f json /path/to/project +``` + +To scan only the root package and its direct dependencies, you can use the flag as follows: + +``` +$ trivy repo --pkg-relationships root,direct /path/to/project +``` + +By default, all relationships are included in the scan. + +!!! info + This flag filters the packages themselves, so it also affects the list of detected packages in JSON reports and SBOM generation. + +!!! warning + As it may not provide a complete package list, `--pkg-relationships` cannot be used with `--dependency-tree`, `--vex` or SBOM generation. + +## Kubernetes + +Trivy can detect vulnerabilities in Kubernetes clusters and components by scanning a Kubernetes Cluster, or a KBOM (Kubernetes bill of Material). To learn more, see the [documentation for Kubernetes scanning](../target/kubernetes.md). + +### Data Sources + +| Vendor | Source | +|------------|---------------------------------------------| +| Kubernetes | [Kubernetes Official CVE feed][k8s-cve][^1] | + +[^1]: Some manual triage and correction has been made. + +## Databases +The information from the above sources is collected and stored in databases that Trivy uses for vulnerability scanning. Trivy automatically fetches, maintains, and caches the relevant databases when performing a vulnerability scan +For more information about Trivy's Databases mechanism and configurations, refer to the [Databases document](../configuration/db.md). + +## Configuration +This section describes vulnerability-specific configuration. +Other common options are documented [here](../configuration/index.md). + +### Overriding OS version +By default, Trivy automatically detects the OS during container image scanning and performs vulnerability detection based on that OS. +However, in some cases, you may want to scan an image with a different OS version than the one detected. +Also, you may want to specify the OS version when OS is not detected. +For these cases, Trivy supports a `--distro` flag using the `/` format (e.g. `alpine/3.20`) to set the desired OS version. + +### Severity selection +By default, Trivy automatically detects severity (as described [here](#severity-selection)). +But there are cases when you may want to use your own source priority. Trivy supports the `--vuln-severity-source` flag for this. + +Fill in a list of required sources, and Trivy will check the sources in that order until it finds an existing severity. +If no source has the severity - Trivy will use the `UNKNOWN` severity. + +!!! note + To use the default logic in combination with your sources - use the `auto` value. + +Example logic for the following vendor severity levels when scanning an Alpine image: + +```json +"VendorSeverity": { + "ghsa": 3, + "nvd": 4, +} +``` + +- `--vuln-severity-source auto,nvd` - severity is `CRITICAL`, got from `auto`. +- `--vuln-severity-source alpine,auto` - severity is `CRITICAL`, got from `auto`. +- `--vuln-severity-source alpine,ghsa` - severity is `HIGH`, got from `ghsa`. +- `--vuln-severity-source alpine,alma` - severity is `UNKNOWN`. + +[^1]: https://github.com/GoogleContainerTools/distroless + +[nvd-CVE-2023-0464]: https://nvd.nist.gov/vuln/detail/CVE-2023-0464 +[redhat-CVE-2023-0464]: https://access.redhat.com/security/cve/cve-2023-0464 + +[arch]: https://security.archlinux.org/ +[alpine]: https://secdb.alpinelinux.org/ +[wolfi]: https://packages.wolfi.dev/os/security.json +[chainguard]: https://packages.cgr.dev/chainguard/security.json +[minimos]: https://packages.mini.dev/advisories/secdb/security.json +[amazon]: https://alas.aws.amazon.com/ +[echo]: https://advisory.echohq.com/data.json +[debian-tracker]: https://security-tracker.debian.org/tracker/ +[debian-oval]: https://www.debian.org/security/oval/ +[ubuntu]: https://ubuntu.com/security/cve +[rhel-oval]: https://www.redhat.com/security/data/oval/v2/ +[rhel-api]: https://www.redhat.com/security/data/metrics/ +[alma]: https://errata.almalinux.org/ +[rocky]: https://download.rockylinux.org/pub/rocky/ +[oracle]: https://linux.oracle.com/security/oval/ +[suse]: http://ftp.suse.com/pub/projects/security/cvrf/ +[photon]: https://packages.broadcom.com/photon/photon_cve_metadata/ +[azure]: https://github.com/microsoft/AzureLinuxVulnerabilityData/ +[rootio]: https://api.root.io/external/patch_feed +[seal]: http://vulnfeed.sealsecurity.io/v1/osv/renamed/vulnerabilities.zip + +[php-ghsa]: https://github.com/advisories?query=ecosystem%3Acomposer +[python-ghsa]: https://github.com/advisories?query=ecosystem%3Apip +[ruby-ghsa]: https://github.com/advisories?query=ecosystem%3Arubygems +[nodejs-ghsa]: https://github.com/advisories?query=ecosystem%3Anpm +[java-ghsa]: https://github.com/advisories?query=ecosystem%3Amaven +[dotnet-ghsa]: https://github.com/advisories?query=ecosystem%3Anuget +[pub-ghsa]: https://github.com/advisories?query=ecosystem%3Apub +[erlang-ghsa]: https://github.com/advisories?query=ecosystem%3Aerlang +[go-ghsa]: https://github.com/advisories?query=ecosystem%3Ago +[swift-ghsa]: https://github.com/advisories?query=ecosystem%3Aswift + +[go-vulndb]: https://pkg.go.dev/vuln/ +[php]: https://github.com/FriendsOfPHP/security-advisories +[ruby]: https://github.com/rubysec/ruby-advisory-db +[nodejs]: https://github.com/nodejs/security-wg +[gitlab]: https://gitlab.com/gitlab-org/advisories-community + +[python-osv]: https://osv.dev/list?q=&ecosystem=PyPI +[rust-osv]: https://osv.dev/list?q=&ecosystem=crates.io +[julia-osv]: https://osv.dev/list?q=&ecosystem=Julia + +[nvd]: https://nvd.nist.gov/vuln + +[k8s-cve]: https://kubernetes.io/docs/reference/issues-security/official-cve-feed/ + +[CVE-2023-32681]: https://nvd.nist.gov/vuln/detail/CVE-2023-32681 +[RHSA-2023:4520]: https://access.redhat.com/errata/RHSA-2023:4520 +[ghsa]: https://github.com/advisories +[requests]: https://pypi.org/project/requests/ +[precision-recall]: https://developers.google.com/machine-learning/crash-course/classification/precision-and-recall diff --git a/docs/guide/supply-chain/attestation/rekor.md b/docs/guide/supply-chain/attestation/rekor.md new file mode 100644 index 000000000000..8f95a4ce2996 --- /dev/null +++ b/docs/guide/supply-chain/attestation/rekor.md @@ -0,0 +1,147 @@ +# Scan SBOM attestation in Rekor + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +## Container images +Trivy can retrieve SBOM attestation of the specified container image in the [Rekor][rekor] instance and scan it for vulnerabilities. + +### Prerequisites +1. SBOM attestation stored in Rekor + - See [the "Keyless signing" section][sbom-attest] if you want to upload your SBOM attestation to Rekor. + + +### Scanning +You need to pass `--sbom-sources rekor` so that Trivy will look for SBOM attestation in Rekor. + +!!! note + `--sbom-sources` can be used only with `trivy image` at the moment. + +```bash +$ trivy image --sbom-sources rekor otms61/alpine:3.7.3 [~/src/github.com/aquasecurity/trivy] +2022-09-16T17:37:13.258+0900 INFO Vulnerability scanning is enabled +2022-09-16T17:37:13.258+0900 INFO Secret scanning is enabled +2022-09-16T17:37:13.258+0900 INFO If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2022-09-16T17:37:13.258+0900 INFO Please see also https://trivy.dev/docs/latest/docs/scanner/secret/#recommendation for faster secret detection +2022-09-16T17:37:14.827+0900 INFO Detected SBOM format: cyclonedx-json +2022-09-16T17:37:14.901+0900 INFO Found SBOM (cyclonedx) attestation in Rekor +2022-09-16T17:37:14.903+0900 INFO Detected OS: alpine +2022-09-16T17:37:14.903+0900 INFO Detecting Alpine vulnerabilities... +2022-09-16T17:37:14.907+0900 INFO Number of language-specific files: 0 +2022-09-16T17:37:14.908+0900 WARN This OS version is no longer supported by the distribution: alpine 3.7.3 +2022-09-16T17:37:14.908+0900 WARN The vulnerability detection may be insufficient because security updates are not provided + +otms61/alpine:3.7.3 (alpine 3.7.3) +================================== +Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 2) + +┌────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────┤ +│ musl │ CVE-2019-14697 │ CRITICAL │ 1.1.18-r3 │ 1.1.18-r4 │ musl libc through 1.1.23 has an x87 floating-point stack │ +│ │ │ │ │ │ adjustment im ...... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-14697 │ +├────────────┤ │ │ │ │ │ +│ musl-utils │ │ │ │ │ │ +│ │ │ │ │ │ │ +│ │ │ │ │ │ │ +└────────────┴────────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────┘ + +``` + +If you have your own Rekor instance, you can specify the URL via `--rekor-url`. + +```bash +$ trivy image --sbom-sources rekor --rekor-url https://my-rekor.dev otms61/alpine:3.7.3 +``` + +## Non-packaged binaries +Trivy can retrieve SBOM attestation of non-packaged binaries in the [Rekor][rekor] instance and scan it for vulnerabilities. + +### Prerequisites +1. SBOM attestation stored in Rekor + - See [the "Keyless signing" section][sbom-attest] if you want to upload your SBOM attestation to Rekor. + +Cosign currently does not support keyless signing for blob attestation, so use our plugin at the moment. +This example uses a cat clone [bat][bat] written in Rust. +You need to generate SBOM from lock files like `Cargo.lock` at first. + +```bash +$ git clone -b v0.20.0 https://github.com/sharkdp/bat +$ trivy fs --format cyclonedx --output bat.cdx ./bat/Cargo.lock +``` + +Then [our attestation plugin][plugin-attest] allows you to store the SBOM attestation linking to a `bat` binary in the Rekor instance. + +```bash +$ wget https://github.com/sharkdp/bat/releases/download/v0.20.0/bat-v0.20.0-x86_64-apple-darwin.tar.gz +$ tar xvf bat-v0.20.0-x86_64-apple-darwin.tar.gz +$ trivy plugin install github.com/aquasecurity/trivy-plugin-attest +$ trivy attest --predicate ./bat.cdx --type cyclonedx ./bat-v0.20.0-x86_64-apple-darwin/bat +``` + +!!! note + The public instance of the Rekor maintained by the Sigstore team limits the attestation size. + If you are using the public instance, please make sure that your SBOM is small enough. + To get more detail, please refer to the Rekor project's [documentation](https://github.com/sigstore/rekor#public-instance). + +### Scan a non-packaged binary +Trivy calculates the digest of the `bat` binary and searches for the SBOM attestation by the digest in Rekor. +If it is found, Trivy uses that for vulnerability scanning. + +```bash +$ trivy fs --sbom-sources rekor ./bat-v0.20.0-x86_64-apple-darwin/bat +2022-10-25T13:27:25.950+0300 INFO Found SBOM attestation in Rekor: bat +2022-10-25T13:27:25.993+0300 INFO Number of language-specific files: 1 +2022-10-25T13:27:25.993+0300 INFO Detecting cargo vulnerabilities... + +bat (cargo) +=========== +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +┌───────────┬───────────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├───────────┼───────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ regex │ CVE-2022-24713 │ HIGH │ 1.5.4 │ 1.5.5 │ Mozilla: Denial of Service via complex regular expressions │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-24713 │ +└───────────┴───────────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────────────────┘ +``` + +Also, it is applied to non-packaged binaries even in container images. + +```bash +$ trivy image --sbom-sources rekor --scanners vuln alpine-with-bat +2022-10-25T13:40:14.920+0300 INFO Vulnerability scanning is enabled +2022-10-25T13:40:18.047+0300 INFO Found SBOM attestation in Rekor: bat +2022-10-25T13:40:18.186+0300 INFO Detected OS: alpine +2022-10-25T13:40:18.186+0300 INFO Detecting Alpine vulnerabilities... +2022-10-25T13:40:18.199+0300 INFO Number of language-specific files: 1 +2022-10-25T13:40:18.199+0300 INFO Detecting cargo vulnerabilities... + +alpine-with-bat (alpine 3.15.6) +=============================== +Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + + +bat (cargo) +=========== +Total: 4 (UNKNOWN: 3, LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +┌───────────┬───────────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├───────────┼───────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ regex │ CVE-2022-24713 │ HIGH │ 1.5.4 │ 1.5.5 │ Mozilla: Denial of Service via complex regular expressions │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-24713 │ +└───────────┴───────────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────────────────┘ +``` + + +!!! note + The `--sbom-sources rekor` flag slows down the scanning as it queries Rekor on the Internet for all non-packaged binaries. + +[rekor]: https://github.com/sigstore/rekor +[sbom-attest]: sbom.md#keyless-signing + +[plugin-attest]: https://github.com/aquasecurity/trivy-plugin-attest + +[bat]: https://github.com/sharkdp/bat \ No newline at end of file diff --git a/docs/guide/supply-chain/attestation/sbom.md b/docs/guide/supply-chain/attestation/sbom.md new file mode 100644 index 000000000000..50493016b677 --- /dev/null +++ b/docs/guide/supply-chain/attestation/sbom.md @@ -0,0 +1,87 @@ +# SBOM attestation + +[Cosign](https://github.com/sigstore/cosign) supports generating and verifying [in-toto attestations](https://github.com/in-toto/attestation). This tool enables you to sign and verify SBOM attestation. +And, Trivy can take an SBOM attestation as input and scan for vulnerabilities + +!!! note + In the following examples, the `cosign` command will write an attestation to a target OCI registry, so you must have permission to write. + If you want to avoid writing an OCI registry and only want to see an attestation, add the `--no-upload` option to the `cosign` command. + +## Sign with a local key pair + +Cosign can generate key pairs and use them for signing and verification. After you run the following command, you will get a public and private key pair. Read more about [how to generate key pairs](https://docs.sigstore.dev/cosign/key_management/signing_with_self-managed_keys). + +```bash +$ cosign generate-key-pair +``` + +In the following example, Trivy generates an SBOM in the CycloneDX format, and then Cosign attaches an attestation of the SBOM to a container image with a local key pair. + +```bash +# The cyclonedx type is supported in Cosign v1.10.0 or later. +$ trivy image --format cyclonedx -o sbom.cdx.json +$ cosign attest --key /path/to/cosign.key --type cyclonedx --predicate sbom.cdx.json +``` + +Then, you can verify attestations on the image. + +```bash +$ cosign verify-attestation --key /path/to/cosign.pub --type cyclonedx +``` + +You can also create attestations of other formatted SBOM. + +```bash +# spdx +$ trivy image --format spdx -o sbom.spdx +$ cosign attest --key /path/to/cosign.key --type spdx --predicate sbom.spdx + +# spdx-json +$ trivy image --format spdx-json -o sbom.spdx.json +$ cosign attest --key /path/to/cosign.key --type spdx --predicate sbom.spdx.json +``` + +## Keyless signing + +You can use Cosign to sign without keys by authenticating with an OpenID Connect protocol supported by sigstore (Google, GitHub, or Microsoft). + +```bash +# The cyclonedx type is supported in Cosign v1.10.0 or later. +$ trivy image --format cyclonedx -o sbom.cdx.json +# The following command uploads SBOM attestation to the public Rekor instance. +$ COSIGN_EXPERIMENTAL=1 cosign attest --type cyclonedx --predicate sbom.cdx.json +``` + +You can verify attestations. +```bash +$ COSIGN_EXPERIMENTAL=1 cosign verify-attestation --type cyclonedx +``` + +## Scanning + +Trivy can take an SBOM attestation as input and scan for vulnerabilities. Currently, Trivy supports CycloneDX-type attestation. + +In the following example, Cosign can get an CycloneDX-type attestation and trivy scan it. +You must create CycloneDX-type attestation before trying the example. +To learn more about how to create an CycloneDX-Type attestation and attach it to an image, see the [Sign with a local key pair](#sign-with-a-local-key-pair) section. + +```bash +$ cosign verify-attestation --key /path/to/cosign.pub --type cyclonedx > sbom.cdx.intoto.jsonl +$ trivy sbom ./sbom.cdx.intoto.jsonl + +sbom.cdx.intoto.jsonl (alpine 3.7.3) +========================= +Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 2) + +┌────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────┤ +│ musl │ CVE-2019-14697 │ CRITICAL │ 1.1.18-r3 │ 1.1.18-r4 │ musl libc through 1.1.23 has an x87 floating-point stack │ +│ │ │ │ │ │ adjustment im ...... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-14697 │ +├────────────┤ │ │ │ │ │ +│ musl-utils │ │ │ │ │ │ +│ │ │ │ │ │ │ +│ │ │ │ │ │ │ +└────────────┴────────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────┘ +``` diff --git a/docs/guide/supply-chain/attestation/vuln.md b/docs/guide/supply-chain/attestation/vuln.md new file mode 100644 index 000000000000..812601190679 --- /dev/null +++ b/docs/guide/supply-chain/attestation/vuln.md @@ -0,0 +1,192 @@ +# Cosign Vulnerability Attestation + +## Generate Cosign Vulnerability Scan Record + +Trivy generates reports in the [Cosign vulnerability scan record format][vuln-attest-spec]. + +You can use the regular subcommands (like image, fs and rootfs) and specify `cosign-vuln` with the --format option. + +``` +$ trivy image --format cosign-vuln --output vuln.json alpine:3.10 +``` + +
+Result + +```json +{ + "invocation": { + "parameters": null, + "uri": "", + "event_id": "", + "builder.id": "" + }, + "scanner": { + "uri": "pkg:github/aquasecurity/trivy@v0.30.1-8-gf9cb8a28", + "version": "v0.30.1-8-gf9cb8a28", + "db": { + "uri": "", + "version": "" + }, + "result": { + "SchemaVersion": 2, + "CreatedAt": 1629894030, + "ArtifactName": "alpine:3.10", + "ArtifactType": "container_image", + "Metadata": { + "OS": { + "Family": "alpine", + "Name": "3.10.9", + "EOSL": true + }, + "ImageID": "sha256:e7b300aee9f9bf3433d32bc9305bfdd22183beb59d933b48d77ab56ba53a197a", + "DiffIDs": [ + "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635" + ], + "RepoTags": [ + "alpine:3.10" + ], + "RepoDigests": [ + "alpine@sha256:451eee8bedcb2f029756dc3e9d73bab0e7943c1ac55cff3a4861c52a0fdd3e98" + ], + "ImageConfig": { + "architecture": "amd64", + "container": "fdb7e80e3339e8d0599282e606c907aa5881ee4c668a68136119e6dfac6ce3a4", + "created": "2021-04-14T19:20:05.338397761Z", + "docker_version": "19.03.12", + "history": [ + { + "created": "2021-04-14T19:20:04.987219124Z", + "created_by": "/bin/sh -c #(nop) ADD file:c5377eaa926bf412dd8d4a08b0a1f2399cfd708743533b0aa03b53d14cb4bb4e in / " + }, + { + "created": "2021-04-14T19:20:05.338397761Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635" + ] + }, + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:eb2080c455e94c22ae35b3aef9e078c492a00795412e026e4d6b41ef64bc7dd8" + } + } + }, + "Results": [ + { + "Target": "alpine:3.10 (alpine 3.10.9)", + "Class": "os-pkgs", + "Type": "alpine", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2021-36159", + "PkgName": "apk-tools", + "InstalledVersion": "2.10.6-r0", + "FixedVersion": "2.10.7-r0", + "Layer": { + "Digest": "sha256:396c31837116ac290458afcb928f68b6cc1c7bdd6963fc72f52f365a2a89c1b5", + "DiffID": "sha256:9fb3aa2f8b8023a4bebbf92aa567caf88e38e969ada9f0ac12643b2847391635" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-36159", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Description": "libfetch before 2021-07-26, as used in apk-tools, xbps, and other products, mishandles numeric strings for the FTP and HTTP protocols. The FTP passive mode implementation allows an out-of-bounds read because strtol is used to parse the relevant numbers into address bytes. It does not check if the line ends prematurely. If it does, the for-loop condition checks for the '\\0' terminator one byte too late.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-125" + ], + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V2Score": 6.4, + "V3Score": 9.1 + } + }, + "References": [ + "https://github.com/freebsd/freebsd-src/commits/main/lib/libfetch", + "https://gitlab.alpinelinux.org/alpine/apk-tools/-/issues/10749", + "https://lists.apache.org/thread.html/r61db8e7dcb56dc000a5387a88f7a473bacec5ee01b9ff3f55308aacc@%3Cdev.kafka.apache.org%3E", + "https://lists.apache.org/thread.html/r61db8e7dcb56dc000a5387a88f7a473bacec5ee01b9ff3f55308aacc@%3Cusers.kafka.apache.org%3E", + "https://lists.apache.org/thread.html/rbf4ce74b0d1fa9810dec50ba3ace0caeea677af7c27a97111c06ccb7@%3Cdev.kafka.apache.org%3E", + "https://lists.apache.org/thread.html/rbf4ce74b0d1fa9810dec50ba3ace0caeea677af7c27a97111c06ccb7@%3Cusers.kafka.apache.org%3E" + ], + "PublishedDate": "2021-08-03T14:15:00Z", + "LastModifiedDate": "2021-10-18T12:19:00Z" + } + ] + } + ] + } + }, + "metadata": { + "scanStartedOn": "2022-07-24T17:14:04.864682+09:00", + "scanFinishedOn": "2022-07-24T17:14:04.864682+09:00" + } +} +``` + +
+ +## Create Cosign Vulnerability Attestation + +[Cosign](https://github.com/sigstore/cosign) supports generating and verifying [in-toto attestations](https://github.com/in-toto/attestation). This tool enables you to sign and verify Cosign vulnerability attestation. + +!!! note + In the following examples, the `cosign` command will write an attestation to a target OCI registry, so you must have permission to write. + If you want to avoid writing an OCI registry and only want to see an attestation, add the `--no-upload` option to the `cosign` command. + + +### Sign with a local key pair + +Cosign can generate key pairs and use them for signing and verification. After you run the following command, you will get a public and private key pair. Read more about [how to generate key pairs](https://docs.sigstore.dev/cosign/key_management/signing_with_self-managed_keys). + +```bash +$ cosign generate-key-pair +``` + +In the following example, Trivy generates a cosign vulnerability scan record, and then Cosign attaches an attestation of it to a container image with a local key pair. + +``` +$ trivy image --format cosign-vuln --output vuln.json +$ cosign attest --key /path/to/cosign.key --type vuln --predicate vuln.json +``` + +Then, you can verify attestations on the image. + +``` +$ cosign verify-attestation --key /path/to/cosign.pub --type vuln +``` + +### Keyless signing + +You can use Cosign to sign without keys by authenticating with an OpenID Connect protocol supported by sigstore (Google, GitHub, or Microsoft). + +``` +$ trivy image --format cosign-vuln -o vuln.json +$ cosign attest --type vuln --predicate vuln.json +``` +This will provide a certificate in the output section. + +You can verify attestations: + +``` +$ cosign verify-attestation --certificate=path-to-the-certificate --type vuln --certificate-identity Email-used-to-sign --certificate-oidc-issuer='the-issuer-used' +``` + +[vuln-attest-spec]: https://github.com/sigstore/cosign/blob/95b74db89941e8ec85e768f639efd4d948db06cd/specs/COSIGN_VULN_ATTESTATION_SPEC.md \ No newline at end of file diff --git a/docs/guide/supply-chain/sbom.md b/docs/guide/supply-chain/sbom.md new file mode 100644 index 000000000000..75fc70310d7f --- /dev/null +++ b/docs/guide/supply-chain/sbom.md @@ -0,0 +1,1776 @@ +# SBOM + +## Generating + +Trivy can generate the following SBOM formats. + +- [CycloneDX](#cyclonedx) +- [SPDX](#spdx) + +### CLI commands +To generate SBOM, you can use the `--format` option for each subcommand such as `image`, `fs` and `vm`. + +``` +$ trivy image --format spdx-json --output result.json alpine:3.15 +``` + + +``` +$ trivy fs --format cyclonedx --output result.json /app/myproject +``` + +
+Result + +``` +{ + "bomFormat": "CycloneDX", + "specVersion": "1.3", + "serialNumber": "urn:uuid:2be5773d-7cd3-4b4b-90a5-e165474ddace", + "version": 1, + "metadata": { + "timestamp": "2022-02-22T15:11:40.270597Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "pkg:oci/alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64", + "type": "container", + "name": "alpine:3.15", + "version": "", + "purl": "pkg:oci/alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:c059bfaa849c4d8e4aecaeb3a10c2d9b3d85f5165c66ad3a4d937758128c4d18" + }, + { + "name": "aquasecurity:trivy:RepoDigest", + "value": "alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "alpine:3.15" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/alpine-baselayout@3.2.0-r18?distro=3.15.0", + "type": "library", + "name": "alpine-baselayout", + "version": "3.2.0-r18", + "licenses": [ + { + "expression": "GPL-2.0-only" + } + ], + "purl": "pkg:apk/alpine/alpine-baselayout@3.2.0-r18?distro=3.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "alpine-baselayout" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.2.0-r18" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:59bf1c3509f33515622619af21ed55bbe26d24913cedbca106468a5fb37a50c3" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759" + } + ] + }, + ...(snip)... + { + "bom-ref": "pkg:apk/alpine/zlib@1.2.11-r3?distro=3.15.0", + "type": "library", + "name": "zlib", + "version": "1.2.11-r3", + "licenses": [ + { + "expression": "Zlib" + } + ], + "purl": "pkg:apk/alpine/zlib@1.2.11-r3?distro=3.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "zlib" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.2.11-r3" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:59bf1c3509f33515622619af21ed55bbe26d24913cedbca106468a5fb37a50c3" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759" + } + ] + }, + { + "bom-ref": "3da6a469-964d-4b4e-b67d-e94ec7c88d37", + "type": "operating-system", + "name": "alpine", + "version": "3.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "3da6a469-964d-4b4e-b67d-e94ec7c88d37", + "dependsOn": [ + "pkg:apk/alpine/alpine-baselayout@3.2.0-r18?distro=3.15.0", + "pkg:apk/alpine/alpine-keys@2.4-r1?distro=3.15.0", + "pkg:apk/alpine/apk-tools@2.12.7-r3?distro=3.15.0", + "pkg:apk/alpine/busybox@1.34.1-r3?distro=3.15.0", + "pkg:apk/alpine/ca-certificates-bundle@20191127-r7?distro=3.15.0", + "pkg:apk/alpine/libc-utils@0.7.2-r3?distro=3.15.0", + "pkg:apk/alpine/libcrypto1.1@1.1.1l-r7?distro=3.15.0", + "pkg:apk/alpine/libretls@3.3.4-r2?distro=3.15.0", + "pkg:apk/alpine/libssl1.1@1.1.1l-r7?distro=3.15.0", + "pkg:apk/alpine/musl@1.2.2-r7?distro=3.15.0", + "pkg:apk/alpine/musl-utils@1.2.2-r7?distro=3.15.0", + "pkg:apk/alpine/scanelf@1.3.3-r0?distro=3.15.0", + "pkg:apk/alpine/ssl_client@1.34.1-r3?distro=3.15.0", + "pkg:apk/alpine/zlib@1.2.11-r3?distro=3.15.0" + ] + }, + { + "ref": "pkg:oci/alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64", + "dependsOn": [ + "3da6a469-964d-4b4e-b67d-e94ec7c88d37" + ] + } + ] +} + +``` + +
+ +### Supported packages +Trivy supports the following packages. + +- [OS packages][os_packages] +- [Language-specific packages][language_packages] + +Trivy has a specific logic for package detection. +See the [package detection](../scanner/vulnerability.md#package-detection) section for more information. + +### Formats +#### CycloneDX +Trivy can generate SBOM in the [CycloneDX][cyclonedx] format. +Note that XML format is not supported at the moment. + +You can use the regular subcommands (like `image`, `fs` and `rootfs`) and specify `cyclonedx` with the `--format` option. + +CycloneDX can represent either or both SBOM or BOV. + +- [Software Bill of Materials (SBOM)][sbom] +- [Bill of Vulnerabilities (BOV)][bov] + +By default, `--format cyclonedx` represents SBOM and doesn't include vulnerabilities in the CycloneDX output. + +``` +$ trivy image --format cyclonedx --output result.json alpine:3.15 +2022-07-19T07:47:27.624Z INFO "--format cyclonedx" disables security scanning. Specify "--scanners vuln" explicitly if you want to include vulnerabilities in the CycloneDX report. +``` + +
+Result + +``` +$ cat result.json | jq . +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:2be5773d-7cd3-4b4b-90a5-e165474ddace", + "version": 1, + "metadata": { + "timestamp": "2022-02-22T15:11:40.270597Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "pkg:oci/alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64", + "type": "container", + "name": "alpine:3.15", + "version": "", + "purl": "pkg:oci/alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:c059bfaa849c4d8e4aecaeb3a10c2d9b3d85f5165c66ad3a4d937758128c4d18" + }, + { + "name": "aquasecurity:trivy:RepoDigest", + "value": "alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "alpine:3.15" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/alpine-baselayout@3.2.0-r18?distro=3.15.0", + "type": "library", + "name": "alpine-baselayout", + "version": "3.2.0-r18", + "licenses": [ + { + "expression": "GPL-2.0-only" + } + ], + "purl": "pkg:apk/alpine/alpine-baselayout@3.2.0-r18?distro=3.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "alpine-baselayout" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.2.0-r18" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:59bf1c3509f33515622619af21ed55bbe26d24913cedbca106468a5fb37a50c3" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759" + } + ] + }, + ...(snip)... + { + "bom-ref": "pkg:apk/alpine/zlib@1.2.11-r3?distro=3.15.0", + "type": "library", + "name": "zlib", + "version": "1.2.11-r3", + "licenses": [ + { + "expression": "Zlib" + } + ], + "purl": "pkg:apk/alpine/zlib@1.2.11-r3?distro=3.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "zlib" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.2.11-r3" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:59bf1c3509f33515622619af21ed55bbe26d24913cedbca106468a5fb37a50c3" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759" + } + ] + }, + { + "bom-ref": "3da6a469-964d-4b4e-b67d-e94ec7c88d37", + "type": "operating-system", + "name": "alpine", + "version": "3.15.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "3da6a469-964d-4b4e-b67d-e94ec7c88d37", + "dependsOn": [ + "pkg:apk/alpine/alpine-baselayout@3.2.0-r18?distro=3.15.0", + "pkg:apk/alpine/alpine-keys@2.4-r1?distro=3.15.0", + "pkg:apk/alpine/apk-tools@2.12.7-r3?distro=3.15.0", + "pkg:apk/alpine/busybox@1.34.1-r3?distro=3.15.0", + "pkg:apk/alpine/ca-certificates-bundle@20191127-r7?distro=3.15.0", + "pkg:apk/alpine/libc-utils@0.7.2-r3?distro=3.15.0", + "pkg:apk/alpine/libcrypto1.1@1.1.1l-r7?distro=3.15.0", + "pkg:apk/alpine/libretls@3.3.4-r2?distro=3.15.0", + "pkg:apk/alpine/libssl1.1@1.1.1l-r7?distro=3.15.0", + "pkg:apk/alpine/musl@1.2.2-r7?distro=3.15.0", + "pkg:apk/alpine/musl-utils@1.2.2-r7?distro=3.15.0", + "pkg:apk/alpine/scanelf@1.3.3-r0?distro=3.15.0", + "pkg:apk/alpine/ssl_client@1.34.1-r3?distro=3.15.0", + "pkg:apk/alpine/zlib@1.2.11-r3?distro=3.15.0" + ] + }, + { + "ref": "pkg:oci/alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64", + "dependsOn": [ + "3da6a469-964d-4b4e-b67d-e94ec7c88d37" + ] + } + ], + "vulnerabilities": [ + { + "id": "CVE-2021-42386", + "source": { + "name": "alpine", + "url": "https://secdb.alpinelinux.org/" + }, + "ratings": [ + { + "source": { + "name": "nvd" + }, + "score": 7.2, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "nvd" + }, + "score": 6.5, + "severity": "medium", + "method": "CVSSv2", + "vector": "AV:N/AC:L/Au:S/C:P/I:P/A:P" + }, + { + "source": { + "name": "redhat" + }, + "score": 6.6, + "severity": "medium", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H" + } + ], + "cwes": [ + 416 + ], + "description": "A use-after-free in Busybox's awk applet leads to denial of service and possibly code execution when processing a crafted awk pattern in the nvalloc function", + "advisories": [ + { + "url": "https://access.redhat.com/security/cve/CVE-2021-42386" + }, + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-42386" + } + ], + "published": "2021-11-15 21:15:00 +0000 UTC", + "updated": "2022-01-04 17:14:00 +0000 UTC", + "affects": [ + { + "ref": "pkg:apk/alpine/busybox@1.33.1-r3?distro=3.14.2" + }, + { + "ref": "pkg:apk/alpine/ssl_client@1.33.1-r3?distro=3.14.2" + } + ] + } + ] +} + +``` + +
+ +If you want to include vulnerabilities, you can enable vulnerability scanning via `--scanners vuln`. + +``` +$ trivy image --scanners vuln --format cyclonedx --output result.json alpine:3.15 +``` + +#### SPDX +Trivy can generate SBOM in the [SPDX][spdx] format. + +You can use the regular subcommands (like `image`, `fs` and `rootfs`) and specify `spdx` or `spdx-json` with the `--format` option. + +``` +$ trivy image --format spdx --output result.spdx alpine:3.15 +``` + +
+Result + +```spdx +SPDXVersion: SPDX-2.3 +DataLicense: CC0-1.0 +SPDXID: SPDXRef-DOCUMENT +DocumentName: alpine:3.15 +DocumentNamespace: http://trivy.dev/container_image/alpine:3.15-12db86e1-4aa4-40ec-900b-5aaa5d82461b +Creator: Organization: aquasecurity +Creator: Tool: trivy-0.58.0 +Created: 2025-02-11T07:43:38Z + +##### Package: alpine:3.15 + +PackageName: alpine:3.15 +SPDXID: SPDXRef-ContainerImage-d8b2a386253047e7 +PackageDownloadLocation: NONE +PrimaryPackagePurpose: CONTAINER +FilesAnalyzed: false +ExternalRef: PACKAGE-MANAGER purl pkg:oci/alpine@sha256%3A19b4bcc4f60e99dd5ebdca0cbce22c503bbcff197549d7e19dab4f22254dc864?arch=amd64&repository_url=index.docker.io%2Flibrary%2Falpine + +##### Package: alpine + +PackageName: alpine +SPDXID: SPDXRef-OperatingSystem-c24750c3b737d897 +PackageVersion: 3.15.11 +PackageDownloadLocation: NONE +PrimaryPackagePurpose: OPERATING-SYSTEM +FilesAnalyzed: false + +##### Package: libretls + +PackageName: libretls +SPDXID: SPDXRef-Package-343391d704e00fbd +PackageVersion: 3.3.4-r3 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: 67dfefe5456c45192b60d76ade98c501b0ae814f +PackageSourceInfo: built package from: libretls 3.3.4-r3 +PackageLicenseConcluded: ISC AND BSD-3-Clause AND MIT +PackageLicenseDeclared: ISC AND BSD-3-Clause AND MIT +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/libretls@3.3.4-r3?arch=x86_64&distro=3.15.11 + +##### Package: libc-utils + +PackageName: libc-utils +SPDXID: SPDXRef-Package-43343abe5c1a0439 +PackageVersion: 0.7.2-r3 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: 798de3ebb57f3e28f408080746935f213a099722 +PackageSourceInfo: built package from: libc-dev 0.7.2-r3 +PackageLicenseConcluded: BSD-2-Clause AND BSD-3-Clause +PackageLicenseDeclared: BSD-2-Clause AND BSD-3-Clause +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64&distro=3.15.11 + +##### Package: alpine-baselayout + +PackageName: alpine-baselayout +SPDXID: SPDXRef-Package-64b7e662458dcd5f +PackageVersion: 3.2.0-r18 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: 132992eab020986b3b5d886a77212889680467a0 +PackageSourceInfo: built package from: alpine-baselayout 3.2.0-r18 +PackageLicenseConcluded: GPL-2.0-only +PackageLicenseDeclared: GPL-2.0-only +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/alpine-baselayout@3.2.0-r18?arch=x86_64&distro=3.15.11 + +##### Package: busybox + +PackageName: busybox +SPDXID: SPDXRef-Package-6c7c9dac75e301b7 +PackageVersion: 1.34.1-r7 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: 21f9265e7a34c795fba4e99c8ae37b57f31cd1a2 +PackageSourceInfo: built package from: busybox 1.34.1-r7 +PackageLicenseConcluded: GPL-2.0-only +PackageLicenseDeclared: GPL-2.0-only +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/busybox@1.34.1-r7?arch=x86_64&distro=3.15.11 + +##### Package: ca-certificates-bundle + +PackageName: ca-certificates-bundle +SPDXID: SPDXRef-Package-702c9bf0cfddb42e +PackageVersion: 20230506-r0 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: 99894c0b834a3f5955e6e5d5f0d804943f05ff52 +PackageSourceInfo: built package from: ca-certificates 20230506-r0 +PackageLicenseConcluded: MPL-2.0 AND MIT +PackageLicenseDeclared: MPL-2.0 AND MIT +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/ca-certificates-bundle@20230506-r0?arch=x86_64&distro=3.15.11 + +##### Package: musl-utils + +PackageName: musl-utils +SPDXID: SPDXRef-Package-92eb9ab29b057905 +PackageVersion: 1.2.2-r9 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: f69aa6d6a57c90358005ce61ccb4ad96cdc303f4 +PackageSourceInfo: built package from: musl 1.2.2-r9 +PackageLicenseConcluded: MIT AND BSD-3-Clause AND GPL-2.0-or-later +PackageLicenseDeclared: MIT AND BSD-3-Clause AND GPL-2.0-or-later +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/musl-utils@1.2.2-r9?arch=x86_64&distro=3.15.11 + +##### Package: scanelf + +PackageName: scanelf +SPDXID: SPDXRef-Package-988bca2f70cf58f6 +PackageVersion: 1.3.3-r0 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: d7f7590e450870a4f79671c2369b31b5bb07349a +PackageSourceInfo: built package from: pax-utils 1.3.3-r0 +PackageLicenseConcluded: GPL-2.0-only +PackageLicenseDeclared: GPL-2.0-only +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/scanelf@1.3.3-r0?arch=x86_64&distro=3.15.11 + +##### Package: apk-tools + +PackageName: apk-tools +SPDXID: SPDXRef-Package-aa2e51a695e95cb9 +PackageVersion: 2.12.7-r3 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: ddf3ddf8545768bc323649559feaae1560f29273 +PackageSourceInfo: built package from: apk-tools 2.12.7-r3 +PackageLicenseConcluded: GPL-2.0-only +PackageLicenseDeclared: GPL-2.0-only +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/apk-tools@2.12.7-r3?arch=x86_64&distro=3.15.11 + +##### Package: libcrypto1.1 + +PackageName: libcrypto1.1 +SPDXID: SPDXRef-Package-ba5f079c5c32fc8 +PackageVersion: 1.1.1w-r1 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: e378634f5c8af32ca75ac56f41ecf4e8d49584a0 +PackageSourceInfo: built package from: openssl 1.1.1w-r1 +PackageLicenseConcluded: OpenSSL +PackageLicenseDeclared: OpenSSL +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/libcrypto1.1@1.1.1w-r1?arch=x86_64&distro=3.15.11 + +##### Package: alpine-keys + +PackageName: alpine-keys +SPDXID: SPDXRef-Package-be18726b6be779d1 +PackageVersion: 2.4-r1 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: 903176b2d2a8ddefd1ba6940f19ad17c2c1d4aff +PackageSourceInfo: built package from: alpine-keys 2.4-r1 +PackageLicenseConcluded: MIT +PackageLicenseDeclared: MIT +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/alpine-keys@2.4-r1?arch=x86_64&distro=3.15.11 + +##### Package: ssl_client + +PackageName: ssl_client +SPDXID: SPDXRef-Package-d9ad92ed9413c93b +PackageVersion: 1.34.1-r7 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: dddfa62dd51bd8807ee1d8660e860574a9dd78ed +PackageSourceInfo: built package from: busybox 1.34.1-r7 +PackageLicenseConcluded: GPL-2.0-only +PackageLicenseDeclared: GPL-2.0-only +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/ssl_client@1.34.1-r7?arch=x86_64&distro=3.15.11 + +##### Package: musl + +PackageName: musl +SPDXID: SPDXRef-Package-ee9b5186331e7a76 +PackageVersion: 1.2.2-r9 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: 7ebdef6cf7f9b58c0e213b333db946d22b00b777 +PackageSourceInfo: built package from: musl 1.2.2-r9 +PackageLicenseConcluded: MIT +PackageLicenseDeclared: MIT +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/musl@1.2.2-r9?arch=x86_64&distro=3.15.11 + +##### Package: libssl1.1 + +PackageName: libssl1.1 +SPDXID: SPDXRef-Package-f00669065070476c +PackageVersion: 1.1.1w-r1 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: 9306ed15b3bdfc7553d5c14c472d87a41fef8541 +PackageSourceInfo: built package from: openssl 1.1.1w-r1 +PackageLicenseConcluded: OpenSSL +PackageLicenseDeclared: OpenSSL +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/libssl1.1@1.1.1w-r1?arch=x86_64&distro=3.15.11 + +##### Package: zlib + +PackageName: zlib +SPDXID: SPDXRef-Package-fcb106f21773cad3 +PackageVersion: 1.2.12-r3 +PackageSupplier: NOASSERTION +PackageDownloadLocation: NONE +PrimaryPackagePurpose: LIBRARY +FilesAnalyzed: false +PackageChecksum: SHA1: ab98d0416bf1dcd245c7b0800f99cbceacfa48b3 +PackageSourceInfo: built package from: zlib 1.2.12-r3 +PackageLicenseConcluded: Zlib +PackageLicenseDeclared: Zlib +ExternalRef: PACKAGE-MANAGER purl pkg:apk/alpine/zlib@1.2.12-r3?arch=x86_64&distro=3.15.11 + +##### Relationships + +Relationship: SPDXRef-ContainerImage-d8b2a386253047e7 CONTAINS SPDXRef-OperatingSystem-c24750c3b737d897 +Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-ContainerImage-d8b2a386253047e7 +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-343391d704e00fbd +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-43343abe5c1a0439 +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-64b7e662458dcd5f +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-6c7c9dac75e301b7 +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-702c9bf0cfddb42e +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-92eb9ab29b057905 +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-988bca2f70cf58f6 +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-aa2e51a695e95cb9 +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-ba5f079c5c32fc8 +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-be18726b6be779d1 +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-d9ad92ed9413c93b +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-f00669065070476c +Relationship: SPDXRef-OperatingSystem-c24750c3b737d897 CONTAINS SPDXRef-Package-fcb106f21773cad3 +Relationship: SPDXRef-Package-343391d704e00fbd DEPENDS_ON SPDXRef-Package-702c9bf0cfddb42e +Relationship: SPDXRef-Package-343391d704e00fbd DEPENDS_ON SPDXRef-Package-ba5f079c5c32fc8 +Relationship: SPDXRef-Package-343391d704e00fbd DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-Package-343391d704e00fbd DEPENDS_ON SPDXRef-Package-f00669065070476c +Relationship: SPDXRef-Package-43343abe5c1a0439 DEPENDS_ON SPDXRef-Package-92eb9ab29b057905 +Relationship: SPDXRef-Package-64b7e662458dcd5f DEPENDS_ON SPDXRef-Package-6c7c9dac75e301b7 +Relationship: SPDXRef-Package-64b7e662458dcd5f DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-Package-6c7c9dac75e301b7 DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-Package-92eb9ab29b057905 DEPENDS_ON SPDXRef-Package-988bca2f70cf58f6 +Relationship: SPDXRef-Package-92eb9ab29b057905 DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-Package-988bca2f70cf58f6 DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-Package-aa2e51a695e95cb9 DEPENDS_ON SPDXRef-Package-702c9bf0cfddb42e +Relationship: SPDXRef-Package-aa2e51a695e95cb9 DEPENDS_ON SPDXRef-Package-ba5f079c5c32fc8 +Relationship: SPDXRef-Package-aa2e51a695e95cb9 DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-Package-aa2e51a695e95cb9 DEPENDS_ON SPDXRef-Package-f00669065070476c +Relationship: SPDXRef-Package-aa2e51a695e95cb9 DEPENDS_ON SPDXRef-Package-fcb106f21773cad3 +Relationship: SPDXRef-Package-ba5f079c5c32fc8 DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-Package-d9ad92ed9413c93b DEPENDS_ON SPDXRef-Package-343391d704e00fbd +Relationship: SPDXRef-Package-d9ad92ed9413c93b DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-Package-f00669065070476c DEPENDS_ON SPDXRef-Package-ba5f079c5c32fc8 +Relationship: SPDXRef-Package-f00669065070476c DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +Relationship: SPDXRef-Package-fcb106f21773cad3 DEPENDS_ON SPDXRef-Package-ee9b5186331e7a76 +``` + +
+ +``` +$ trivy image --format spdx-json --output result.spdx alpine:3.15 +``` + +
+Result + +```json +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "alpine:3.15", + "documentNamespace": "http://trivy.dev/container_image/alpine:3.15-bbe0096f-0ed0-47b4-bbea-82121a9201f1", + "creationInfo": { + "creators": [ + "Organization: aquasecurity", + "Tool: trivy-0.58.0" + ], + "created": "2025-02-13T12:22:22Z" + }, + "packages": [ + { + "name": "alpine:3.15", + "SPDXID": "SPDXRef-ContainerImage-d8b2a386253047e7", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:oci/alpine@sha256%3A19b4bcc4f60e99dd5ebdca0cbce22c503bbcff197549d7e19dab4f22254dc864?arch=amd64\u0026repository_url=index.docker.io%2Flibrary%2Falpine" + } + ], + "primaryPackagePurpose": "CONTAINER", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "DiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "ImageID: sha256:32b91e3161c8fc2e3baf2732a594305ca5093c82ff4e0c9f6ebbd2a879468e1d" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "RepoDigest: alpine@sha256:19b4bcc4f60e99dd5ebdca0cbce22c503bbcff197549d7e19dab4f22254dc864" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "RepoTag: alpine:3.15" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "SchemaVersion: 2" + } + ] + }, + { + "name": "alpine-baselayout", + "SPDXID": "SPDXRef-Package-64b7e662458dcd5f", + "versionInfo": "3.2.0-r18", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "132992eab020986b3b5d886a77212889680467a0" + } + ], + "sourceInfo": "built package from: alpine-baselayout 3.2.0-r18", + "licenseConcluded": "GPL-2.0-only", + "licenseDeclared": "GPL-2.0-only", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/alpine-baselayout@3.2.0-r18?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: alpine-baselayout@3.2.0-r18" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "alpine-keys", + "SPDXID": "SPDXRef-Package-be18726b6be779d1", + "versionInfo": "2.4-r1", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "903176b2d2a8ddefd1ba6940f19ad17c2c1d4aff" + } + ], + "sourceInfo": "built package from: alpine-keys 2.4-r1", + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/alpine-keys@2.4-r1?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: alpine-keys@2.4-r1" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "apk-tools", + "SPDXID": "SPDXRef-Package-aa2e51a695e95cb9", + "versionInfo": "2.12.7-r3", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "ddf3ddf8545768bc323649559feaae1560f29273" + } + ], + "sourceInfo": "built package from: apk-tools 2.12.7-r3", + "licenseConcluded": "GPL-2.0-only", + "licenseDeclared": "GPL-2.0-only", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/apk-tools@2.12.7-r3?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: apk-tools@2.12.7-r3" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "busybox", + "SPDXID": "SPDXRef-Package-6c7c9dac75e301b7", + "versionInfo": "1.34.1-r7", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "21f9265e7a34c795fba4e99c8ae37b57f31cd1a2" + } + ], + "sourceInfo": "built package from: busybox 1.34.1-r7", + "licenseConcluded": "GPL-2.0-only", + "licenseDeclared": "GPL-2.0-only", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/busybox@1.34.1-r7?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: busybox@1.34.1-r7" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "ca-certificates-bundle", + "SPDXID": "SPDXRef-Package-702c9bf0cfddb42e", + "versionInfo": "20230506-r0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "99894c0b834a3f5955e6e5d5f0d804943f05ff52" + } + ], + "sourceInfo": "built package from: ca-certificates 20230506-r0", + "licenseConcluded": "MPL-2.0 AND MIT", + "licenseDeclared": "MPL-2.0 AND MIT", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/ca-certificates-bundle@20230506-r0?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: ca-certificates-bundle@20230506-r0" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "libc-utils", + "SPDXID": "SPDXRef-Package-43343abe5c1a0439", + "versionInfo": "0.7.2-r3", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "798de3ebb57f3e28f408080746935f213a099722" + } + ], + "sourceInfo": "built package from: libc-dev 0.7.2-r3", + "licenseConcluded": "BSD-2-Clause AND BSD-3-Clause", + "licenseDeclared": "BSD-2-Clause AND BSD-3-Clause", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/libc-utils@0.7.2-r3?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: libc-utils@0.7.2-r3" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "libcrypto1.1", + "SPDXID": "SPDXRef-Package-ba5f079c5c32fc8", + "versionInfo": "1.1.1w-r1", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "e378634f5c8af32ca75ac56f41ecf4e8d49584a0" + } + ], + "sourceInfo": "built package from: openssl 1.1.1w-r1", + "licenseConcluded": "OpenSSL", + "licenseDeclared": "OpenSSL", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/libcrypto1.1@1.1.1w-r1?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: libcrypto1.1@1.1.1w-r1" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "libretls", + "SPDXID": "SPDXRef-Package-343391d704e00fbd", + "versionInfo": "3.3.4-r3", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "67dfefe5456c45192b60d76ade98c501b0ae814f" + } + ], + "sourceInfo": "built package from: libretls 3.3.4-r3", + "licenseConcluded": "ISC AND BSD-3-Clause AND MIT", + "licenseDeclared": "ISC AND BSD-3-Clause AND MIT", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/libretls@3.3.4-r3?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: libretls@3.3.4-r3" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "libssl1.1", + "SPDXID": "SPDXRef-Package-f00669065070476c", + "versionInfo": "1.1.1w-r1", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "9306ed15b3bdfc7553d5c14c472d87a41fef8541" + } + ], + "sourceInfo": "built package from: openssl 1.1.1w-r1", + "licenseConcluded": "OpenSSL", + "licenseDeclared": "OpenSSL", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/libssl1.1@1.1.1w-r1?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: libssl1.1@1.1.1w-r1" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "musl", + "SPDXID": "SPDXRef-Package-ee9b5186331e7a76", + "versionInfo": "1.2.2-r9", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "7ebdef6cf7f9b58c0e213b333db946d22b00b777" + } + ], + "sourceInfo": "built package from: musl 1.2.2-r9", + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/musl@1.2.2-r9?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: musl@1.2.2-r9" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "musl-utils", + "SPDXID": "SPDXRef-Package-92eb9ab29b057905", + "versionInfo": "1.2.2-r9", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "f69aa6d6a57c90358005ce61ccb4ad96cdc303f4" + } + ], + "sourceInfo": "built package from: musl 1.2.2-r9", + "licenseConcluded": "MIT AND BSD-3-Clause AND GPL-2.0-or-later", + "licenseDeclared": "MIT AND BSD-3-Clause AND GPL-2.0-or-later", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/musl-utils@1.2.2-r9?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: musl-utils@1.2.2-r9" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "scanelf", + "SPDXID": "SPDXRef-Package-988bca2f70cf58f6", + "versionInfo": "1.3.3-r0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "d7f7590e450870a4f79671c2369b31b5bb07349a" + } + ], + "sourceInfo": "built package from: pax-utils 1.3.3-r0", + "licenseConcluded": "GPL-2.0-only", + "licenseDeclared": "GPL-2.0-only", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/scanelf@1.3.3-r0?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: scanelf@1.3.3-r0" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "ssl_client", + "SPDXID": "SPDXRef-Package-d9ad92ed9413c93b", + "versionInfo": "1.34.1-r7", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "dddfa62dd51bd8807ee1d8660e860574a9dd78ed" + } + ], + "sourceInfo": "built package from: busybox 1.34.1-r7", + "licenseConcluded": "GPL-2.0-only", + "licenseDeclared": "GPL-2.0-only", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/ssl_client@1.34.1-r7?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: ssl_client@1.34.1-r7" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "zlib", + "SPDXID": "SPDXRef-Package-fcb106f21773cad3", + "versionInfo": "1.2.12-r3", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "ab98d0416bf1dcd245c7b0800f99cbceacfa48b3" + } + ], + "sourceInfo": "built package from: zlib 1.2.12-r3", + "licenseConcluded": "Zlib", + "licenseDeclared": "Zlib", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:apk/alpine/zlib@1.2.12-r3?arch=x86_64\u0026distro=3.15.11" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:2879a4821959ab702528e28a1c59cd26c4956112497f6d1dbfd86c8d88003983" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "LayerDigest: sha256:d078792c4f9122259f14b539315bd92cbd9490ed73e08255a08689122b143108" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgID: zlib@1.2.12-r3" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "PkgType: alpine" + } + ] + }, + { + "name": "alpine", + "SPDXID": "SPDXRef-OperatingSystem-c24750c3b737d897", + "versionInfo": "3.15.11", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "primaryPackagePurpose": "OPERATING-SYSTEM", + "annotations": [ + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "Class: os-pkgs" + }, + { + "annotator": "Tool: trivy-0.58.0", + "annotationDate": "2025-02-13T12:22:22Z", + "annotationType": "OTHER", + "comment": "Type: alpine" + } + ] + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-ContainerImage-d8b2a386253047e7", + "relatedSpdxElement": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-ContainerImage-d8b2a386253047e7", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-343391d704e00fbd", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-43343abe5c1a0439", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-64b7e662458dcd5f", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-6c7c9dac75e301b7", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-702c9bf0cfddb42e", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-92eb9ab29b057905", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-988bca2f70cf58f6", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-aa2e51a695e95cb9", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-ba5f079c5c32fc8", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-be18726b6be779d1", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-d9ad92ed9413c93b", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-f00669065070476c", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-OperatingSystem-c24750c3b737d897", + "relatedSpdxElement": "SPDXRef-Package-fcb106f21773cad3", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-343391d704e00fbd", + "relatedSpdxElement": "SPDXRef-Package-702c9bf0cfddb42e", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-343391d704e00fbd", + "relatedSpdxElement": "SPDXRef-Package-ba5f079c5c32fc8", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-343391d704e00fbd", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-343391d704e00fbd", + "relatedSpdxElement": "SPDXRef-Package-f00669065070476c", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-43343abe5c1a0439", + "relatedSpdxElement": "SPDXRef-Package-92eb9ab29b057905", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-64b7e662458dcd5f", + "relatedSpdxElement": "SPDXRef-Package-6c7c9dac75e301b7", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-64b7e662458dcd5f", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-6c7c9dac75e301b7", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-92eb9ab29b057905", + "relatedSpdxElement": "SPDXRef-Package-988bca2f70cf58f6", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-92eb9ab29b057905", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-988bca2f70cf58f6", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-aa2e51a695e95cb9", + "relatedSpdxElement": "SPDXRef-Package-702c9bf0cfddb42e", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-aa2e51a695e95cb9", + "relatedSpdxElement": "SPDXRef-Package-ba5f079c5c32fc8", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-aa2e51a695e95cb9", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-aa2e51a695e95cb9", + "relatedSpdxElement": "SPDXRef-Package-f00669065070476c", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-aa2e51a695e95cb9", + "relatedSpdxElement": "SPDXRef-Package-fcb106f21773cad3", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-ba5f079c5c32fc8", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-d9ad92ed9413c93b", + "relatedSpdxElement": "SPDXRef-Package-343391d704e00fbd", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-d9ad92ed9413c93b", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-f00669065070476c", + "relatedSpdxElement": "SPDXRef-Package-ba5f079c5c32fc8", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-f00669065070476c", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + }, + { + "spdxElementId": "SPDXRef-Package-fcb106f21773cad3", + "relatedSpdxElement": "SPDXRef-Package-ee9b5186331e7a76", + "relationshipType": "DEPENDS_ON" + } + ] +} +``` + +
+ +## Scanning + +### SBOM as Target +Trivy can take SBOM documents as input for scanning, e.g `trivy sbom ./sbom.spdx`. +See [here](../target/sbom.md) for more details. + +### SBOM Detection inside Targets +Trivy searches for SBOM files in container images with the following extensions: + +- `.spdx` +- `.spdx.json` +- `.cdx` +- `.cdx.json` + +In addition, Trivy automatically detects SBOM files in [Bitnami images](https://github.com/bitnami/containers), [see here](../coverage/others/bitnami.md) for more details. + +It is enabled in the following targets. + +| Target | Enabled | +| :-------------: | :-----: | +| Container Image | ✓ | +| Filesystem | | +| Rootfs | ✓ | +| Git Repository | | +| VM Image | ✓ | +| Kubernetes | | +| AWS | | +| SBOM | | + +### SBOM Discovery for Container Images + +When scanning container images, Trivy can discover SBOM for those images. [See here](../target/container_image.md) for more details. + +[spdx]: https://spdx.github.io/spdx-spec/v2.2.2/ + +[cyclonedx]: https://cyclonedx.org/ +[sbom]: https://cyclonedx.org/capabilities/sbom/ +[bov]: https://cyclonedx.org/capabilities/bov/ + +[os_packages]: ../scanner/vulnerability.md#os-packages +[language_packages]: ../scanner/vulnerability.md#language-specific-packages diff --git a/docs/guide/supply-chain/vex/file.md b/docs/guide/supply-chain/vex/file.md new file mode 100644 index 000000000000..bc5e92d34643 --- /dev/null +++ b/docs/guide/supply-chain/vex/file.md @@ -0,0 +1,581 @@ +# Local VEX Files + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +In addition to [VEX repositories](./repo.md), Trivy also supports the use of local VEX files for vulnerability filtering. +This method is useful when you have specific VEX documents that you want to apply to your scans. +Currently, Trivy supports the following formats: + +- [CycloneDX](https://cyclonedx.org/capabilities/vex/) +- [OpenVEX](https://github.com/openvex/spec) +- [CSAF](https://oasis-open.github.io/csaf-documentation/specification.html) + +## CycloneDX +| Target | Supported | +|:---------------:|:---------:| +| Container Image | | +| Filesystem | | +| Code Repository | | +| VM Image | | +| Kubernetes | | +| SBOM | ✅ | + +There are [two VEX formats](https://cyclonedx.org/capabilities/vex/) for CycloneDX: + +- Independent BOM and VEX BOM +- BOM With Embedded VEX + +Trivy only supports the Independent BOM and VEX BOM format, so you need to provide a separate VEX file alongside the SBOM. +The input SBOM format must be in CycloneDX format. + +The following steps are required: + +1. Generate a CycloneDX SBOM +2. Create a VEX based on the SBOM generated in step 1 +3. Provide the VEX when scanning the CycloneDX SBOM + +### Generate the SBOM +You can generate a CycloneDX SBOM with Trivy as follows: + +```bash +$ trivy image --format cyclonedx --output debian11.sbom.cdx debian:11 +``` + +### Create the VEX +Next, create a VEX based on the generated SBOM. +Multiple vulnerability statuses can be defined under `vulnerabilities`. +Take a look at the example below. + +```bash +$ cat < trivy.vex.cdx +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "vulnerabilities": [ + { + "id": "CVE-2020-8911", + "analysis": { + "state": "not_affected", + "justification": "code_not_reachable", + "response": ["will_not_fix", "update"], + "detail": "The vulnerable function is not called" + }, + "affects": [ + { + "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:golang/github.com/aws/aws-sdk-go@v1.44.234" + } + ] + } + ] +} +EOF +``` + +This is a VEX document in the CycloneDX format. +The vulnerability ID, such as a CVE-ID or GHSA-ID, should be placed in `vulnerabilities.id`. +When the `analysis.state` is set to `not_affected`, Trivy will not detect the vulnerability. + +BOM-Links must be placed in `affects.ref`. +The BOM-Link has the following syntax and consists of three elements: + +``` +urn:cdx:serialNumber/version#bom-ref +``` + +- serialNumber +- version +- bom-ref + +These values must be obtained from the CycloneDX SBOM. +Please note that while the serialNumber starts with `urn:uuid:`, the BOM-Link starts with `urn:cdx:`. + +The `bom-ref` must contain the BOM-Ref of the package affected by the vulnerability. +In the example above, since the Go package `github.com/aws/aws-sdk-go` is affected by CVE-2020-8911, it was necessary to specify the SBOM's BOM-Ref, `pkg:golang/github.com/aws/aws-sdk-go@1.44.234`. + +For more details on CycloneDX VEX and BOM-Link, please refer to the following links: + +- [CycloneDX VEX](https://cyclonedx.org/capabilities/vex/) +- [BOM-Link](https://cyclonedx.org/capabilities/bomlink/) +- [Examples](https://github.com/CycloneDX/bom-examples/tree/master) + +### Scan SBOM with VEX +Provide the VEX when scanning the CycloneDX SBOM. + +```bash +$ trivy sbom trivy.sbom.cdx --vex trivy.vex.cdx +... +2023-04-13T12:55:44.838+0300 INFO Filtered out the detected vulnerability {"VEX format": "CycloneDX", "vulnerability-id": "CVE-2020-8911", "status": "not_affected", "justification": "code_not_reachable"} + +go.mod (gomod) +============== +Total: 1 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + +┌───────────────────────────┬───────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├───────────────────────────┼───────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ github.com/aws/aws-sdk-go │ CVE-2020-8912 │ LOW │ v1.44.234 │ │ aws-sdk-go: In-band key negotiation issue in AWS S3 Crypto │ +│ │ │ │ │ │ SDK for golang... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-8912 │ +└───────────────────────────┴───────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────────────────┘ +``` + +CVE-2020-8911 is no longer shown as it is filtered out according to the given CycloneDX VEX document. + +## OpenVEX +| Target | Supported | +|:---------------:|:---------:| +| Container Image | ✅ | +| Filesystem | ✅ | +| Code Repository | ✅ | +| VM Image | ✅ | +| Kubernetes | ✅ | +| SBOM | ✅ | + +Trivy also supports [OpenVEX][openvex] that is designed to be minimal, compliant, interoperable, and embeddable. +OpenVEX can be used in all Trivy targets, unlike CycloneDX VEX. + +The following steps are required: + +1. Create a VEX document +2. Provide the VEX when scanning your target + +### Create the VEX document +Please see also [the example](https://github.com/openvex/examples). +Trivy requires [the Package URL (PURL)][purl] as the product identifier. + +```bash +$ cat < debian11.openvex.json +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-2e67563e128250cbcb3e98930df948dd053e43271d70dc50cfa22d57e03fe96f", + "author": "Aqua Security", + "timestamp": "2023-08-29T19:07:16.853479631-06:00", + "version": 1, + "statements": [ + { + "vulnerability": {"name": "CVE-2019-8457"}, + "products": [ + {"@id": "pkg:deb/debian/libdb5.3@5.3.28+dfsg1-0.8"} + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} +EOF +``` + +In the above example, PURLs, `pkg:deb/debian/libdb5.3@5.3.28+dfsg1-0.8` are used for the product identifier. +You can find PURLs in the JSON report generated by Trivy. +This VEX statement is applied if the PURL specified in the VEX matches the PURL found during the scan. +See [here](#purl-matching) for more details of PURL matching. + +Trivy also supports [OpenVEX subcomponents][openvex-subcomponent], which allow for more precise specification of the scope of a VEX statement, reducing the risk of incorrect filtering. +Let's say you want to suppress vulnerabilities within a container image. +If you only specify the PURL of the container image as the product, the resulting VEX would look like this: + +
+OpenVEX products only + +```json +"statements": [ + { + "vulnerability": {"name": "CVE-2024-32002"}, + "products": [ + {"@id": "pkg:oci/trivy?repository_url=ghcr.io%2Faquasecurity%2Ftrivy"} + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } +] +``` + +
+ +However, this approach would suppress all instances of CVE-2024-32002 within the container image. +If the intention is to declare that the `git` package distributed by Alpine Linux within this image is not affected, subcomponents can be utilized as follows: + +
+OpenVEX subcomponents + +```json +"statements": [ + { + "vulnerability": {"name": "CVE-2024-32002"}, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io%2Faquasecurity%2Ftrivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/git"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } +] +``` + +
+ +By declaring the subcomponent in this manner, Trivy will filter the results, considering only the `git` package within the `ghcr.io/aquasecurity/trivy` container image as not affected. +Omitting the version in the PURL applies the statement to all versions of the package. +More details about PURL matching can be found [here](#purl-matching). + +Furthermore, the product specified in a VEX statement does not necessarily need to be the target of the scan. +It is possible to specify a component that is included in the scan target as the product. +For example, you can designate a specific Go project as the product and its dependent modules as subcomponents. + +In the following example, the VEX statement declares that the `github.com/docker/docker` module, which is a dependency of the `github.com/aquasecurity/trivy` Go project, is not affected by CVE-2024-29018. + +
+OpenVEX intermediate components + +```json +"statements": [ + { + "vulnerability": {"name": "CVE-2024-29018"}, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/trivy", + "subcomponents": [ + { "@id": "pkg:golang/github.com/docker/docker" } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } +] +``` + +
+ +This VEX document can be used when scanning a container image as well as other targets. +The VEX statement will be applied when Trivy finds the Go binary within the container image. + +```bash +$ trivy image ghcr.io/aquasecurity/trivy:0.50.0 --vex trivy.openvex.json +``` + +VEX documents can indeed be reused across different container images, eliminating the need to issue separate VEX documents for each image. +This is particularly useful when there is a common component or library that is used across multiple projects or container images. + +You can see [the appendix](#applying-vex-to-dependency-trees) for more details on how VEX is applied in Trivy. + +### Scan with VEX +Provide the VEX when scanning your target. + +```bash +$ trivy image debian:11.6 --vex debian11.openvex.json +... +2023-04-26T17:56:05.358+0300 INFO Filtered out the detected vulnerability {"VEX format": "OpenVEX", "vulnerability-id": "CVE-2019-8457", "status": "not_affected", "justification": "vulnerable_code_not_in_execute_path"} + +debian:11.6 (debian 11.6) + +Total: 176 (UNKNOWN: 1, LOW: 82, MEDIUM: 46, HIGH: 41, CRITICAL: 5) +``` + +CVE-2019-8457 is no longer shown as it is filtered out according to the given OpenVEX document. + + +## CSAF +| Target | Supported | +|:---------------:|:---------:| +| Container Image | ✅ | +| Filesystem | ✅ | +| Code Repository | ✅ | +| VM Image | ✅ | +| Kubernetes | ✅ | +| SBOM | ✅ | + +Trivy also supports [CSAF][csaf] format for VEX. +Since CSAF aims to be SBOM format agnostic, both CycloneDX and SPDX formats are available for use as input SBOMs in Trivy. + +The following steps are required: + +1. Create a CSAF document +2. Provide the CSAF when scanning your target + + +### Create the CSAF document +Create a CSAF document in JSON format as follows: + +
+CSAF VEX + +```bash +$ cat < debian11.vex.csaf +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "notes": [ + { + "category": "summary", + "text": "Example Company VEX document. Unofficial content for demonstration purposes only.", + "title": "Author comment" + } + ], + "publisher": { + "category": "vendor", + "name": "Example Company ProductCERT", + "namespace": "https://psirt.example.com" + }, + "title": "AquaSecurity example VEX document", + "tracking": { + "current_release_date": "2024-01-01T11:00:00.000Z", + "generator": { + "date": "2024-01-01T11:00:00.000Z", + "engine": { + "name": "Secvisogram", + "version": "1.11.0" + } + }, + "id": "2024-EVD-UC-01-A-001", + "initial_release_date": "2024-01-01T11:00:00.000Z", + "revision_history": [ + { + "date": "2024-01-01T11:00:00.000Z", + "number": "1", + "summary": "Initial version." + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "branches": [ + { + "branches": [ + { + "category": "product_version", + "name": "5.3", + "product": { + "name": "Database Libraries 5.3", + "product_id": "LIBDB-5328", + "product_identification_helper": { + "purl": "pkg:deb/debian/libdb5.3@5.3.28%2Bdfsg1-0.8?arch=amd64\u0026distro=debian-11.8" + } + } + } + ], + "category": "product_name", + "name": "Database Libraries" + } + ], + "category": "vendor", + "name": "Debian" + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2019-8457", + "notes": [ + { + "category": "description", + "text": "SQLite3 from 3.6.0 to and including 3.27.2 is vulnerable to heap out-of-bound read in the rtreenode() function when handling invalid rtree tables.", + "title": "CVE description" + } + ], + "product_status": { + "known_not_affected": [ + "LIBDB-5328" + ] + }, + "threats": [ + { + "category": "impact", + "details": "Vulnerable code not in execute path.", + "product_ids": [ + "LIBDB-5328" + ] + } + ] + } + ] +} +EOF +``` + +
+ +Trivy also supports [CSAF relationships][csaf-relationship], reducing the risk of incorrect filtering. +It works in the same way as OpenVEX subcomponents. +At present, the specified relationship category is not taken into account and all the following categories are treated internally as "depends_on". + +- default_component_of +- installed_on +- installed_with + +You can see [the appendix](#applying-vex-to-dependency-trees) for more details on how VEX is applied in Trivy. + +### Scan with CSAF VEX +Provide the CSAF document when scanning your target. + +```bash +$ trivy image debian:11.8 --vex debian11.vex.csaf +... +2024-01-02T10:28:26.704+0100 INFO Filtered out the detected vulnerability {"VEX format": "CSAF", "vulnerability-id": "CVE-2019-8457", "status": "not_affected"} + +debian:11.8 (debian 11.8) + +Total: 153 (UNKNOWN: 1, LOW: 82, MEDIUM: 33, HIGH: 32, CRITICAL: 5) +``` + +CVE-2019-8457 is no longer shown as it is filtered out according to the given CSAF document. + +## Appendix +### PURL matching +In the context of VEX, Package URLs (PURLs) are utilized to identify specific software packages and their versions. +The PURL matching specification outlines how PURLs are interpreted for vulnerability exception processing, ensuring precise identification and broad coverage of software packages. + +!!! note + The following PURL matching rules are not formally defined within the current official PURL specification. + Instead, they represent [a community consensus][purl-matching] on how to interpret PURLs. + +Below are the key aspects of the PURL matching rules: + +#### Matching Without Version +A PURL without a specified version (e.g., `pkg:maven/com.google.guava/guava`) matches all versions of that package. +This rule simplifies the application of vulnerability exceptions to all versions of a package. + +**Example**: `pkg:maven/com.google.guava/guava` matches: + +- All versions of `guava`, such as `com.google.guava:guava:24.1.1`, `com.google.guava:guava:30.0`. + +#### Matching Without Qualifiers +A PURL without any qualifiers (e.g., `pkg:maven/com.google.guava/guava@24.1.1`) matches any variation of that package, irrespective of qualifiers. +This approach ensures broad matching capabilities, covering all architectural or platform-specific variations of a package version. + +**Example**: `pkg:maven/com.google.guava/guava@24.1.1` matches: + +- `pkg:maven/com.google.guava/guava@24.1.1?classifier=x86` +- `pkg:maven/com.google.guava/guava@24.1.1?type=pom` + +#### Matching With Specific Qualifiers +A PURL that includes specific qualifiers (e.g., `pkg:maven/com.google.guava/guava@24.1.1?classifier=x86`) matches only those package versions that include the same qualifiers. + +**Example**: `pkg:maven/com.google.guava/guava@24.1.1?classifier=x86` matches: + +- `pkg:maven/com.google.guava/guava@24.1.1?classifier=x86&type=dll` + - Extra qualifiers (e.g., `type=dll`) are ignored. + +does not match: + +- `pkg:maven/com.google.guava/guava@24.1.1` + - `classifier=x86` is missing. +- `pkg:maven/com.google.guava/guava@24.1.1?classifier=sources` + - `classifier` must have the same value. + +### Applying VEX to Dependency Trees + +Trivy internally generates a dependency tree and applies VEX statements to this graph. +Let's consider a project with the following dependency tree, where `Module C v2.0.0` is assumed to have a vulnerability CVE-XXXX-YYYY: + +```mermaid +graph TD; + modRootA(Module Root A v1.0.0) + modB(Module B v1.0.0) + modC(Module C v2.0.0) + + modRootA-->modB + modB-->modC +``` + +Now, suppose a VEX statement is issued for `Module B` as follows: + +```json +"statements": [ + { + "vulnerability": {"name": "CVE-XXXX-YYYY"}, + "products": [ + { + "@id": "pkg:golang/module-b@v1.0.0", + "subcomponents": [ + { "@id": "pkg:golang/module-c@v2.0.0" } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } +] +``` + +It declares that `Module B` is not affected by CVE-XXXX-YYYY on `Module C`. + +!!! note + The VEX in this example defines the relationship between `Module B` and `Module C`. + However, as Trivy traverses all parents from vulnerable packages, it is also possible to define a VEX for the relationship between a vulnerable package and any parent, such as `Module A` and `Module C`, etc. + +Mapping this VEX onto the dependency tree would look like this: + +```mermaid +graph TD; + modRootA(Module Root A v1.0.0) + + subgraph "VEX (Not Affected)" + modB(Module B v1.0.0) + modC(Module C v2.0.0) + end + + modRootA-->modB + modB-->modC +``` + +In this case, it's clear that `Module Root A` is also not affected by CVE-XXXX-YYYY, so this vulnerability is suppressed. + +Now, let's consider another project: + +```mermaid +graph TD; + modRootZ(Module Root Z v1.0.0) + modB'(Module B v1.0.0) + modC'(Module C v2.0.0) + modD'(Module D v3.0.0) + + modRootZ-->modB' + modRootZ-->modD' + modB'-->modC' + modD'-->modC' +``` + +Assuming the same VEX as before, applying it to this dependency tree would look like: + +```mermaid +graph TD; + modRootZ(Module Root Z v1.0.0) + + subgraph "VEX (Not Affected)" + modB'(Module B v1.0.0) + modC'(Module C v2.0.0) + end + + modD'(Module D v3.0.0) + + modRootZ-->modB' + modRootZ-->modD' + modB'-->modC' + modD'-->modC' +``` + +`Module Root Z` depends on `Module C` via multiple paths. +While the VEX tells us that `Module B` is not affected by the vulnerability, `Module D` might be. +In the absence of a VEX, the default assumption is that it is affected. +Taking all of this into account, Trivy determines that `Module Root Z` is affected by this vulnerability. + + +[csaf]: https://oasis-open.github.io/csaf-documentation/specification.html +[openvex]: https://github.com/openvex/spec +[purl]: https://github.com/package-url/purl-spec +[purl-matching]: https://github.com/openvex/spec/issues/27 + +[openvex-subcomponent]: https://github.com/openvex/spec/blob/main/OPENVEX-SPEC.md#subcomponent +[csaf-relationship]: https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html#3224-product-tree-property---relationships \ No newline at end of file diff --git a/docs/guide/supply-chain/vex/index.md b/docs/guide/supply-chain/vex/index.md new file mode 100644 index 000000000000..e4ae7c977a5d --- /dev/null +++ b/docs/guide/supply-chain/vex/index.md @@ -0,0 +1,37 @@ +# Vulnerability Exploitability Exchange (VEX) + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy supports filtering detected vulnerabilities using the [Vulnerability Exploitability eXchange (VEX)](https://www.ntia.gov/files/ntia/publications/vex_one-page_summary.pdf), a standardized format for sharing and exchanging information about vulnerabilities. +By providing VEX during scanning, it is possible to filter vulnerabilities based on their status. + +## VEX Usage Methods + +Trivy currently supports four methods for utilizing VEX: + +1. [VEX Repository](./repo.md) +2. [Local VEX Files](./file.md) +3. [VEX Attestation](./oci.md) +4. [SBOM Reference](./sbom-ref.md) + +### Enabling VEX +To enable VEX, use the `--vex` option. +You can specify the method to use: + +- To enable the VEX Repository: `--vex repo` +- To use a local VEX file: `--vex /path/to/vex-document.json` +- To enable VEX attestation discovery in OCI registry: `--vex oci` +- To use remote VEX files referenced in SBOMs: `--vex sbom-ref` + +```bash +$ trivy image ghcr.io/aquasecurity/trivy:0.52.0 --vex repo +``` + +You can enable these methods simultaneously. +The order of specification determines the priority: + +- `--vex repo --vex /path/to/vex-document.json`: VEX Repository has priority +- `--vex /path/to/vex-document.json --vex repo`: Local file has priority + +For detailed information on each method, please refer to each page. \ No newline at end of file diff --git a/docs/guide/supply-chain/vex/oci.md b/docs/guide/supply-chain/vex/oci.md new file mode 100644 index 000000000000..d50c210a97c1 --- /dev/null +++ b/docs/guide/supply-chain/vex/oci.md @@ -0,0 +1,121 @@ +# Discover VEX Attestation in OCI Registry + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy can discover VEX attestations for container images. +This feature allows you to automatically use VEX during container image scanning. + +## How It Works + +Trivy can automatically discover and utilize VEX attestations for container images during scanning by using the `--vex oci` flag. +This process enhances vulnerability detection results by incorporating the information from the VEX attestation. + +To use this feature, follow these three steps: + +1. Create a VEX document +2. Generate and upload a VEX attestation to an OCI registry +3. Use the VEX attestation with Trivy + +Steps 1 and 2 are not necessary if you are trying to scan a third-party container image and already have VEX attestation attached. + +Let's go through each step in detail. + +!!! note + In the following examples, the `cosign` command will write an attestation to a target OCI registry, so you must have permission to write. + If you want to avoid writing an OCI registry and only want to see an attestation, add the `--no-upload` option to the cosign command. + +### Step 1: Create a VEX Document + +Currently, Trivy does not have a built-in feature to create VEX documents, so you need to create them manually. +You can refer to the [OpenVEX section](./file.md#openvex) for guidance on creating VEX files. + +For container image vulnerabilities, the product ID should be the OCI type in the [PURL][purl] format. +For example: + +``` +pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy +``` + +This product ID applies the VEX statement to all tags of the `ghcr.io/aquasecurity/trivy` container image. +If you want to declare a statement for a specific digest only, you can use: + +``` +pkg:oci/trivy@sha256:5bd5ab35814f86783561603ebb35d5d5d99006dcdcd5c3f828ea1afb4c12d159?repository_url=ghcr.io/aquasecurity/trivy +``` + +!!! note + Using an image tag, like `pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy&tag=0.50.0`, is not supported in the product ID at the moment. + +Next, specify vulnerable packages as subcomponents, such as `pkg:apk/alpine/busybox`. +You can also include the package version and other [qualifiers][qualifiers] (e.g., `arch`) to limit statements, like `pkg:apk/alpine/busybox@1.36.1-r29?arch=x86`. + +Lastly, include the vulnerability IDs. + +Here's an example VEX document: + +```json +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-2e67563e128250cbcb3e98930df948dd053e43271d70dc50cfa22d57e03fe96f", + "author": "Aqua Security", + "timestamp": "2024-07-30T19:07:16.853479631-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2023-42363" + }, + "products": [ + { + "@id": "pkg:oci/trivy?repository_url=ghcr.io/aquasecurity/trivy", + "subcomponents": [ + {"@id": "pkg:apk/alpine/busybox"}, + {"@id": "pkg:apk/alpine/busybox-binsh"} + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_cannot_be_controlled_by_adversary", + "impact_statement": "awk is not used" + } + ] +} +``` + +You can also refer to [Trivy's example](https://github.com/aquasecurity/trivy/blob/4e54a7e84c33c1be80c52c6db78c634bc3911715/.vex/oci.openvex.json) for more inspiration. + +### Step 2: Generate and Upload a VEX Attestation to an OCI Registry + +You can use the [Cosign command](https://docs.sigstore.dev/cosign/verifying/attestation/) to generate and upload the VEX attestation. +Cosign offers methods both with and without keys. +For detailed instructions, please refer to the Cosign documentation. + +To generate and attach a VEX attestation to your image, use the following command: + +``` +$ cosign attest --predicate oci.openvex.json --type openvex +``` + +Note that this command attaches the attestation only to the specified image tag. +If needed, repeat the process for other tags and digests. + +### Step 3: Use VEX Attestation with Trivy + +Once you've attached the VEX attestation to the container image, Trivy can automatically discover and use it during scanning. +Simply add the `--vex oci` flag when scanning a container image: + +``` +$ trivy image --vex oci +``` + +To see which vulnerabilities were filtered by the VEX attestation, use the `--show-suppressed` flag: + +``` +$ trivy image --vex oci --show-suppressed +``` + +The `` specified in these commands must be the same as the one to which you attached the VEX attestation. + +[purl]: https://github.com/package-url/purl-spec +[qualifiers]: https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst \ No newline at end of file diff --git a/docs/guide/supply-chain/vex/repo.md b/docs/guide/supply-chain/vex/repo.md new file mode 100644 index 000000000000..eb7fd4944f02 --- /dev/null +++ b/docs/guide/supply-chain/vex/repo.md @@ -0,0 +1,221 @@ +# VEX Repository + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +## Using VEX Repository + +Trivy can download and utilize VEX documents from repositories that comply with [the VEX Repository Specification][vex-repo]. +While it's planned to be enabled by default in the future, currently it can be activated by explicitly specifying `--vex repo`. + +``` +$ trivy image ghcr.io/aquasecurity/trivy:0.52.0 --vex repo +2024-07-20T11:22:58+04:00 INFO [vex] The default repository config has been created +file_path="/Users/teppei/.trivy/vex/repository.yaml" +2024-07-20T11:23:23+04:00 INFO [vex] Updating repository... repo="default" url="https://github.com/aquasecurity/vexhub" +``` + +During scanning, Trivy generates PURLs for discovered packages and searches for matching PURLs in the VEX Repository. +If a match is found, the corresponding VEX is utilized. + +### Configuration File + +#### Default Configuration + +When `--vex repo` is specified for the first time, a default configuration file is created at `$HOME/.trivy/vex/repository.yaml`. +The home directory can be configured through environment variable `$XDG_DATA_HOME`. + +You can also create the configuration file in advance using the `trivy vex repo init` command and edit it. + +The default configuration file looks like this: + +```yaml +repositories: + - name: default + url: https://github.com/aquasecurity/vexhub + enabled: true + username: "" + password: "" + token: "" +``` + +By default, [VEX Hub][vexhub] managed by Aqua Security is used. +VEX Hub primarily trusts VEX documents published by the package maintainers. + +#### Show Configuration +You can see the config file path and the configured repositories with `trivy vex repo list`: + +```bash +$ trivy vex repo list +VEX Repositories (config: /home/username/.trivy/vex/repository.yaml) + +- Name: default + URL: https://github.com/aquasecurity/vexhub + Status: Enabled +``` + +#### Custom Repositories + +If you want to trust VEX documents published by other organizations or use your own VEX repository, you can specify a custom repository that complies with [the VEX Repository Specification][vex-repo]. +You can add a custom repository as below: + +```yaml +- name: custom + url: https://example.com/custom-repo + enabled: true +``` + + +#### Authentication + +For private repositories: + +- `username`/`password` can be used for Basic authentication +- `token` can be used for Bearer authentication + +```yaml +- name: custom + url: https://example.com/custom-repo + enabled: true + token: "my-token" +``` + +#### TLS Verification + +In some cases, you might want to skip the TLS verification, per-repository: + +```yaml +- name: custom + url: https://example.com/custom-repo + enabled: true + insecure: true +``` + +#### Repository Priority + +The priority of VEX repositories is determined by their order in the configuration file. +You can add repositories with higher priority than the default or even remove the default VEX Hub. + +```yaml +- name: repo1 + url: https://example.com/repo1 +- name: repo2 + url: https://example.com/repo2 +``` + +In this configuration, when Trivy detects a vulnerability in a package, it generates a PURL for that package and searches for matching VEX documents in the configured repositories. +The search process follows this order: + +1. Trivy first looks for a VEX document matching the package's PURL in `repo1`. +2. If no matching VEX document is found in `repo1`, Trivy then searches in `repo2`. +3. This process continues through all configured repositories until a match is found. + +If a matching VEX document is found in any repository (e.g., `repo1`), the search stops, and Trivy uses that VEX document. +Subsequent repositories (e.g., `repo2`) are not checked for that specific vulnerability and package combination. + +It's important to note that the first matching VEX document found determines the final status of the vulnerability. +For example, if `repo1` states that a package is "Affected" by a vulnerability, this status will be used even if `repo2` states that the same package is "Not Affected" for the same vulnerability. +The "Affected" status from the higher-priority repository (`repo1`) takes precedence, and Trivy will consider the package as affected by the vulnerability. + +### Repository Updates + +VEX repositories are automatically updated during scanning. +Updates are performed based on the update frequency specified by the repository. + +To disable auto-update, pass `--skip-vex-repo-update`. + +```shell +$ trivy image ghcr.io/aquasecurity/trivy:0.50.0 --vex repo --skip-vex-repo-update +``` + +To download VEX repositories in advance without scanning, use `trivy vex repo download`. + +The cache can be cleared with `trivy clean --vex-repo`. + +### Displaying Filtered Vulnerabilities + +To see which vulnerabilities were filtered and why, use the `--show-suppressed` option: + +```shell +$ trivy image ghcr.io/aquasecurity/trivy:0.50.0 --vex repo --show-suppressed +... + +Suppressed Vulnerabilities (Total: 4) +===================================== +┌───────────────┬────────────────┬──────────┬──────────────┬───────────────────────────────────────────────────┬──────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Statement │ Source │ +├───────────────┼────────────────┼──────────┼──────────────┼───────────────────────────────────────────────────┼──────────────────────────────────────────┤ +│ busybox │ CVE-2023-42364 │ MEDIUM │ not_affected │ vulnerable_code_cannot_be_controlled_by_adversary │ VEX Repository: default │ +│ │ │ │ │ │ (https://github.com/aquasecurity/vexhub) │ +│ ├────────────────┤ │ │ │ │ +│ │ CVE-2023-42365 │ │ │ │ │ +│ │ │ │ │ │ │ +├───────────────┼────────────────┤ │ │ │ │ +│ busybox-binsh │ CVE-2023-42364 │ │ │ │ │ +│ │ │ │ │ │ │ +│ ├────────────────┤ │ │ │ │ +│ │ CVE-2023-42365 │ │ │ │ │ +│ │ │ │ │ │ │ +└───────────────┴────────────────┴──────────┴──────────────┴───────────────────────────────────────────────────┴──────────────────────────────────────────┘ + +``` + +## Publishing VEX Documents + +### For OSS Projects + +As an OSS developer or maintainer, you may encounter vulnerabilities in the packages your project depends on. +These vulnerabilities might be discovered through your own scans or reported by third parties using your OSS project. + +While Trivy strives to minimize false positives, it doesn't perform code graph analysis, which means it can't evaluate exploitability at the code level. +Consequently, Trivy may report vulnerabilities even in cases where: + +1. The vulnerable function in a dependency is never called in your project. +2. The vulnerable code cannot be controlled by an attacker in the context of your project. + +If you're confident that a reported vulnerability in a dependency doesn't affect your OSS project or container image, you can publish a VEX document to reduce noise in Trivy scans. +To assess exploitability, you have several options: + +1. Manual assessment: As a maintainer, you can read the source code and determine if the vulnerability is exploitable in your project's context. +2. Automated assessment: You can use SAST (Static Application Security Testing) tools or similar tools to analyze the code and determine exploitability. + +By publishing VEX documents in the source repository, Trivy can automatically utilize them through VEX Hub. +The main steps are: + +1. Generate a VEX document +2. Commit the VEX document to the `.vex/` directory in the source repository (e.g., [Trivy's VEX][trivy-vex]) +3. Register your project's [PURL][purl] in VEX Hub + +Step 3 is only necessary once. +After that, updating the VEX file in your repository will automatically be fetched by VEX Hub and utilized by Trivy. +See the [VEX Hub repository][vexhub] for more information. + +If you want to issue a VEX for an OSS project that you don't maintain, consider first proposing the VEX publication to the original repository. +Many OSS maintainers are open to contributions that improve the security posture of their projects. +However, if your proposal is not accepted, or if you want to issue a VEX with statements that differ from the maintainer's judgment, you may want to consider creating a [custom repository](#hosting-custom-repositories). + +### For Private Projects + +If you're working on private software or personal projects, you have several options: + +1. [Local VEX files](./file.md): You can create local VEX files and have Trivy read them during scans. This is suitable for individual use or small teams. +2. [.trivyignore](../../configuration/filtering.md#trivyignore): For simpler cases, using a .trivyignore file might be sufficient to suppress specific vulnerabilities. +3. [Custom repositories](#hosting-custom-repositories): For large organizations wanting to share VEX information for internally used software across different departments, setting up a custom VEX repository might be the best approach. + +## Hosting Custom Repositories + +While the principle is to store VEX documents for OSS packages in the source repository, it's possible to create a custom repository if that's difficult. + +There are various use cases for providing custom repositories: + +- A Pull Request to add a VEX document upstream was not merged +- Consolidating VEX documents output by SAST tools +- Publishing vendor-specific VEX documents that differ from OSS maintainer statements +- Creating a private VEX repository to publish common VEX for your company + +In these cases, you can create a repository that complies with [the VEX Repository Specification][vex-repo] to make it available for use with Trivy. + +[vex-repo]: https://github.com/aquasecurity/vex-repo-spec +[vexhub]: https://github.com/aquasecurity/vexhub +[trivy-vex]: https://github.com/aquasecurity/trivy/blob/b76a7250912cfc028cfef743f0f98cd81b39f8aa/.vex/trivy.openvex.json +[purl]: https://github.com/package-url/purl-spec \ No newline at end of file diff --git a/docs/guide/supply-chain/vex/sbom-ref.md b/docs/guide/supply-chain/vex/sbom-ref.md new file mode 100644 index 000000000000..c3303935e88e --- /dev/null +++ b/docs/guide/supply-chain/vex/sbom-ref.md @@ -0,0 +1,44 @@ +# VEX SBOM Reference + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +## Using externally referenced VEX documents + +Trivy can discover and download VEX documents referenced in the `externalReferences` of a scanned CycloneDX SBOM. This +requires the references to be of type `exploitability-statement`. + +To be picked up by Trivy, following top level content needs to be part of a CycloneDx SBOM to dynamically resolve a +remotely hosted file VEX file at the location `https://vex.example.com`: + +``` + "externalReferences": [ + { + "type": "exploitability-statement", + "url": "https://vex.example.com/vex" + } + ] +``` + +This can also be used to dynamically retrieve VEX files stored on GitHub with an `externalReference` such as: + +``` + "externalReferences": [ + { + "type": "exploitability-statement", + "url": "https://raw.githubusercontent.com/aquasecurity/trivy/refs/heads/main/.vex/trivy.openvex.json" + } + ] +``` + +This is not enabled by default at the moment, but can be used when scanning a CycloneDx SBOM and explicitly specifying +`--vex sbom-ref`. + +```shell +$ trivy sbom trivy.cdx.json --vex sbom-ref +2025-01-19T13:29:31+01:00 INFO [vex] Retrieving external VEX document from host vex.example.com type="externalReference" +2025-01-19T13:29:31+01:00 INFO Some vulnerabilities have been ignored/suppressed. Use the "--show-suppressed" flag to display them. +``` + +All the referenced VEX files are retrieved via HTTP/HTTPS and used in the same way as if they were explicitly specified +via a [file reference](./file.md). \ No newline at end of file diff --git a/docs/guide/target/container_image.md b/docs/guide/target/container_image.md new file mode 100644 index 000000000000..75f46173c1e0 --- /dev/null +++ b/docs/guide/target/container_image.md @@ -0,0 +1,573 @@ +# Container Image + +Trivy supports two targets for container images. + +- Files inside container images +- Container image metadata + +## Files inside container images +Container images consist of files. +For instance, new files will be installed if you install a package. + +Trivy scans the files inside container images for + +- Vulnerabilities +- Misconfigurations +- Secrets +- Licenses + +By default, vulnerability and secret scanning are enabled, and you can configure that with `--scanners`. + +### Vulnerabilities +It is enabled by default. +You can simply specify your image name (and a tag). +It detects known vulnerabilities in your container image. +See [here](../scanner/vulnerability.md) for the detail. + +``` +$ trivy image [YOUR_IMAGE_NAME] +``` + +For example: + +``` +$ trivy image python:3.4-alpine +``` + +
+Result + +``` +2019-05-16T01:20:43.180+0900 INFO Updating vulnerability database... +2019-05-16T01:20:53.029+0900 INFO Detecting Alpine vulnerabilities... + +python:3.4-alpine3.9 (alpine 3.9.2) +=================================== +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0) + ++---------+------------------+----------+-------------------+---------------+--------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++---------+------------------+----------+-------------------+---------------+--------------------------------+ +| openssl | CVE-2019-1543 | MEDIUM | 1.1.1a-r1 | 1.1.1b-r1 | openssl: ChaCha20-Poly1305 | +| | | | | | with long nonces | ++---------+------------------+----------+-------------------+---------------+--------------------------------+ +``` + +
+ +To enable only vulnerability scanning, you can specify `--scanners vuln`. + +```shell +$ trivy image --scanners vuln [YOUR_IMAGE_NAME] +``` + +### Misconfigurations +It is supported, but it is not useful in most cases. +As mentioned [here](../scanner/misconfiguration/index.md), Trivy mainly supports Infrastructure as Code (IaC) files for misconfigurations. +If your container image includes IaC files such as Kubernetes YAML files or Terraform files, you should enable this feature with `--scanners misconfig`. + +``` +$ trivy image --scanners misconfig [YOUR_IMAGE_NAME] +``` + +### Secrets +It is enabled by default. +See [here](../scanner/secret.md) for the detail. + +```shell +$ trivy image [YOUR_IMAGE_NAME] +``` + +### Licenses +It is disabled by default. +See [here](../scanner/license.md) for the detail. + +```shell +$ trivy image --scanners license [YOUR_IMAGE_NAME] +``` + +## Container image metadata +Container images have [configuration](https://github.com/opencontainers/image-spec/blob/2fb996805b3734779bf9a3a84dc9a9691ad7efdd/config.md). +`docker inspect` and `docker history` show the information according to the configuration. + +Trivy scans the configuration of container images for + +- Misconfigurations +- Secrets + +They are disabled by default. +You can enable them with `--image-config-scanners`. + +!!! tips + The configuration can be exported as the JSON file by `docker save`. + +### Misconfigurations +Trivy detects misconfigurations on the configuration of container images. +The image config is converted into Dockerfile and Trivy handles it as Dockerfile. +See [here](../scanner/misconfiguration/index.md) for the detail of Dockerfile scanning. + +It is disabled by default. +You can enable it with `--image-config-scanners misconfig`. + +``` +$ trivy image --image-config-scanners misconfig [YOUR_IMAGE_NAME] +``` + +
+Result + +``` +alpine:3.17 (dockerfile) +======================== +Tests: 24 (SUCCESSES: 21, FAILURES: 3) +Failures: 3 (UNKNOWN: 0, LOW: 2, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +HIGH: Specify at least 1 USER command in Dockerfile with non-root user as argument +════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile. + +See https://avd.aquasec.com/misconfig/ds002 +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + +LOW: Consider using 'COPY file:e4d600fc4c9c293efe360be7b30ee96579925d1b4634c94332e2ec73f7d8eca1 in /' command instead of 'ADD file:e4d600fc4c9c293efe360be7b30ee96579925d1b4634c94332e2ec73f7d8eca1 in /' +════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files. + +See https://avd.aquasec.com/misconfig/ds005 +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + alpine:3.17:1 +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 1 [ ADD file:e4d600fc4c9c293efe360be7b30ee96579925d1b4634c94332e2ec73f7d8eca1 in / +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + +LOW: Add HEALTHCHECK instruction in your Dockerfile +════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +You should add HEALTHCHECK instruction in your docker container images to perform the health check on running containers. + +See https://avd.aquasec.com/misconfig/ds026 +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── +``` +
+ +!!! tip + You can see how each layer is created with `docker history`. + +#### Disabled checks + +The following checks are disabled for this scan type due to known issues. See the linked issues for more details. + +| Check ID | Reason | Issue | +|----------|------------|--------| +| [AVD-DS-0007](https://avd.aquasec.com/misconfig/dockerfile/general/avd-ds-0007/) | This check detects multiple `ENTRYPOINT` instructions in a stage, but since image history analysis does not identify stages, this check is not relevant for this scan type. | [#8364](https://github.com/aquasecurity/trivy/issues/8364) | +| [AVD-DS-0016](https://avd.aquasec.com/misconfig/dockerfile/general/avd-ds-0016/) | This check detects multiple `CMD` instructions in a stage, but since image history analysis does not identify stages, this check is not relevant for this scan type. | [#7368](https://github.com/aquasecurity/trivy/issues/7368) | + + +### Secrets +Trivy detects secrets on the configuration of container images. +The image config is converted into JSON and Trivy scans the file for secrets. +It is especially useful for environment variables that are likely to have credentials by accident. +See [here](../scanner/secret.md) for the detail. + +```shell +$ trivy image --image-config-scanners secret [YOUR_IMAGE_NAME] +``` + +
+Result + +``` +vuln-image (alpine 3.17.1) +========================== +Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + + +vuln-image (secrets) +==================== +Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 2) + +CRITICAL: GitHub (github-pat) +════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +GitHub Personal Access Token +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + test:16 +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 14 { + 15 "created": "2023-01-09T17:05:20Z", + 16 [ "created_by": "ENV secret=****************************************", + 17 "comment": "buildkit.dockerfile.v0", +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + + +CRITICAL: GitHub (github-pat) +════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════════ +GitHub Personal Access Token +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + test:34 +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + 32 "Env": [ + 33 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + 34 [ "secret=****************************************" + 35 ] +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +``` + +
+ +!!! tip + You can see environment variables with `docker inspect`. + +## Supported + +Trivy will look for the specified image in a series of locations. By default, it +will first look in the local Docker Engine, then Containerd, Podman, and +finally container registry. + +This behavior can be modified with the `--image-src` flag. For example, the +command + +```bash +trivy image --image-src podman,containerd alpine:3.7.3 +``` + +Will first search in Podman. If the image is found there, it will be scanned +and the results returned. If the image is not found in Podman, then Trivy will +search in Containerd. If the image is not found there either, the scan will +fail and no more image sources will be searched. + +### Docker Engine +Trivy tries to look for the specified image in your local Docker Engine. +It will be skipped if Docker Engine is not running locally. + +If your docker socket is not the default path, you can override it via `DOCKER_HOST`. + +### containerd + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy tries to look for the specified image in your local [containerd](https://containerd.io/). +It will be skipped if containerd is not running locally. + +Specify your image name in containerd running locally. + +```bash +$ nerdctl images +REPOSITORY TAG IMAGE ID CREATED PLATFORM SIZE BLOB SIZE +aquasec/nginx latest 2bcabc23b454 3 hours ago linux/amd64 149.1 MiB 54.1 MiB +$ trivy image aquasec/nginx +``` + +If your containerd socket is not the default path (`//run/containerd/containerd.sock`), you can override it via `CONTAINERD_ADDRESS`. + +```bash +$ export CONTAINERD_ADDRESS=/run/k3s/containerd/containerd.sock +$ trivy image aquasec/nginx +``` + +If your scan targets are images in a namespace other than containerd's default namespace (`default`), you can override it via `CONTAINERD_NAMESPACE`. + +```bash +$ export CONTAINERD_NAMESPACE=k8s.io +$ trivy image aquasec/nginx +``` + +### Podman + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Scan your image in Podman (>=2.0) running locally. The remote Podman is not supported. +If you prefer to keep the socket open at all times, then before performing Trivy commands, you can enable the podman.sock systemd service on your machine. +For more details, see [here](https://github.com/containers/podman/blob/master/docs/tutorials/remote_client.md#enable-the-podman-service-on-the-server-machine). + + +```bash +$ systemctl --user enable --now podman.socket +``` + +Then, you can scan your image in Podman. + +```bash +$ cat Dockerfile +FROM alpine:3.12 +RUN apk add --no-cache bash +$ podman build -t test . +$ podman images +REPOSITORY TAG IMAGE ID CREATED SIZE +localhost/test latest efc372d4e0de About a minute ago 7.94 MB +$ trivy image test +``` + +If you prefer not to keep the socket open at all times, but to limit the socket opening for your trivy scanning duration only then you can scan your image with the following command: + +```bash +podman system service --time=0 "${TMP_PODMAN_SOCKET}" & +PODMAN_SYSTEM_SERVICE_PID="$!" +trivy image --podman-host="${TMP_PODMAN_SOCKET}" --docker-host="${TMP_PODMAN_SOCKET}" test +kill "${PODMAN_SYSTEM_SERVICE_PID}" +``` + +### Container Registry +Trivy supports registries that comply with the following specifications. + +- [Docker Registry HTTP API V2](https://docs.docker.com/registry/spec/api/) +- [OCI Distribution Specification](https://github.com/opencontainers/distribution-spec) + +You can configure credentials with `trivy registry login`. +See [here](../advanced/private-registries/index.md) for the detail. + +### Tar Files +Trivy supports image tar files generated by the following tools. + +- [Docker Image Specification](https://github.com/moby/docker-image-spec) + - [Moby Project](https://github.com/moby/moby/) + - [Buildah](https://github.com/containers/buildah) + - [Podman](https://github.com/containers/podman) + - [img](https://github.com/genuinetools/img) +- [Kaniko](https://github.com/GoogleContainerTools/kaniko) + +``` +$ docker pull ruby:3.1-alpine3.15 +$ docker save ruby:3.1-alpine3.15 -o ruby-3.1.tar +$ trivy image --input ruby-3.1.tar +``` + +
+Result + +``` +2022-02-03T10:08:19.127Z INFO Detected OS: alpine +2022-02-03T10:08:19.127Z WARN This OS version is not on the EOL list: alpine 3.15 +2022-02-03T10:08:19.127Z INFO Detecting Alpine vulnerabilities... +2022-02-03T10:08:19.127Z INFO Number of language-specific files: 2 +2022-02-03T10:08:19.127Z INFO Detecting gemspec vulnerabilities... +2022-02-03T10:08:19.128Z INFO Detecting node-pkg vulnerabilities... +2022-02-03T10:08:19.128Z WARN This OS version is no longer supported by the distribution: alpine 3.15.0 +2022-02-03T10:08:19.128Z WARN The vulnerability detection may be insufficient because security updates are not provided + +ruby-3.1.tar (alpine 3.15.0) +============================ +Total: 3 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 3, CRITICAL: 0) + ++----------+------------------+----------+-------------------+---------------+---------------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++----------+------------------+----------+-------------------+---------------+---------------------------------------+ +| gmp | CVE-2021-43618 | HIGH | 6.2.1-r0 | 6.2.1-r1 | gmp: Integer overflow and resultant | +| | | | | | buffer overflow via crafted input | +| | | | | | -->avd.aquasec.com/nvd/cve-2021-43618 | ++----------+ + + + + + +| gmp-dev | | | | | | +| | | | | | | +| | | | | | | ++----------+ + + + + + +| libgmpxx | | | | | | +| | | | | | | +| | | | | | | ++----------+------------------+----------+-------------------+---------------+---------------------------------------+ + +Node.js (node-pkg) +================== +Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) + + +Ruby (gemspec) +============== +Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0) +``` + +
+ +### OCI Layout +Trivy supports image directories compliant with [Open Container Image Layout Specification](https://github.com/opencontainers/image-spec/blob/master/spec.md). + +Buildah: + +``` +$ buildah push docker.io/library/alpine:3.11 oci:/path/to/alpine +$ trivy image --input /path/to/alpine +``` + +Skopeo: + +``` +$ skopeo copy docker-daemon:alpine:3.11 oci:/path/to/alpine +$ trivy image --input /path/to/alpine +``` + +Referencing specific images can be done by their tag or by their manifest digest: +``` +# Referenced by tag +$ trivy image --input /path/to/alpine:3.15 + +# Referenced by digest +$ trivy image --input /path/to/alpine@sha256:82389ea44e50c696aba18393b168a833929506f5b29b9d75eb817acceb6d54ba +``` + +## SBOM +Trivy supports the generation of Software Bill of Materials (SBOM) for container images and the search for SBOMs during vulnerability scanning. + +### Generation +Trivy can generate SBOM for container images. +See [here](../supply-chain/sbom.md) for details. + +### Discover SBOM inside container images +Trivy can search for Software Bill of Materials (SBOMs) within container image files and scan their components for vulnerabilities. + +#### Third-party SBOM files +SBOM specifications define key requirements for component documentation[^2]. +However, different tools and systems often have varying approaches to documenting component types and their relationships. + +Due to these variations, Trivy cannot always accurately interpret SBOMs generated by other tools. +For example, it may have difficulty determining the correct file paths to component information files (such as lock files or binaries). +In such cases, Trivy uses the path to the scanned SBOM file itself to maintain traceability and ensure accurate dependency reporting. + +### Discover SBOM referencing the container image +Trivy can search for Software Bill of Materials (SBOMs) that reference container images. +If an SBOM is found, the vulnerability scan is performed using the SBOM instead of the container image. +By using the SBOM, you can perform a vulnerability scan more quickly, as it allows you to skip pulling the container image and analyzing its layers. + +To enable this functionality, you need to specify the `--sbom-sources` flag. +The following two sources are supported: + +- OCI Registry (`oci`) +- Rekor (`rekor`) + +Example: + +```bash +$ trivy image --sbom-sources oci ghcr.io/knqyf263/oci-referrers +2023-03-05T17:36:55.278+0200 INFO Vulnerability scanning is enabled +2023-03-05T17:36:58.103+0200 INFO Detected SBOM format: cyclonedx-json +2023-03-05T17:36:58.129+0200 INFO Found SBOM (cyclonedx) in the OCI referrers +... + +ghcr.io/knqyf263/oci-referrers (alpine 3.16.2) +============================================== +Total: 17 (UNKNOWN: 0, LOW: 0, MEDIUM: 5, HIGH: 9, CRITICAL: 3) +``` + +The OCI Registry utilizes the [Referrers API](https://github.com/opencontainers/distribution-spec/blob/main/spec.md#listing-referrers). +For more information about Rekor, please refer to [its documentation](../supply-chain/attestation/rekor.md). + +## Compliance + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +This section describes container image specific compliance reports. +For an overview of Trivy's Compliance feature, including working with custom compliance, check out the [Compliance documentation](../compliance/compliance.md). + +### Built in reports + +The following reports are available out of the box: + +| Compliance | Version | Name for command | More info | +|----------------------------------------|---------|------------------|---------------------------------------------------------------------------------------------| +| CIS Docker Community Edition Benchmark | 1.1.0 | `docker-cis-1.6.0` | [Link](https://www.aquasec.com/cloud-native-academy/docker-container/docker-cis-benchmark/) | + +### Examples + +Scan a container image configuration and generate a compliance summary report: + +``` +trivy image --compliance docker-cis-1.6.0 [YOUR_IMAGE_NAME] +``` + +!!! note + The `Issues` column represent the total number of failed checks for this control. + +## Authentication +Please reference [this page](../advanced/private-registries/index.md). + +## Scan Cache +When scanning container images, it stores analysis results in the cache, using the image ID and the layer IDs as the key. +This approach enables faster scans of the same container image or different images that share layers. + +More details are available in the [cache documentation](../configuration/cache.md#scan-cache-backend). + +## Options +### Scan Image on a specific Architecture and OS +By default, Trivy loads an image on a "linux/amd64" machine. +To customise this, pass a `--platform` argument in the format OS/Architecture for the image: + +``` +$ trivy image --platform=os/architecture [YOUR_IMAGE_NAME] +``` + +For example: + +``` +$ trivy image --platform=linux/arm alpine:3.16.1 +``` + +
+Result + +``` +2022-10-25T21:00:50.972+0300 INFO Vulnerability scanning is enabled +2022-10-25T21:00:50.972+0300 INFO Secret scanning is enabled +2022-10-25T21:00:50.972+0300 INFO If your scanning is slow, please try '--scanners vuln' to disable secret scanning +2022-10-25T21:00:50.972+0300 INFO Please see also https://trivy.dev/docs/latest/docs/scanner/secret/#recommendation for faster secret detection +2022-10-25T21:00:56.190+0300 INFO Detected OS: alpine +2022-10-25T21:00:56.190+0300 INFO Detecting Alpine vulnerabilities... +2022-10-25T21:00:56.191+0300 INFO Number of language-specific files: 0 + +alpine:3.16.1 (alpine 3.16.1) +============================= +Total: 1 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 1) + +┌─────────┬────────────────┬──────────┬───────────────────┬───────────────┬─────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├─────────┼────────────────┼──────────┼───────────────────┼───────────────┼─────────────────────────────────────────────────────────────┤ +│ zlib │ CVE-2022-37434 │ CRITICAL │ 1.2.12-r1 │ 1.2.12-r2 │ zlib: heap-based buffer over-read and overflow in inflate() │ +│ │ │ │ │ │ in inflate.c via a... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-37434 │ +└─────────┴────────────────┴──────────┴───────────────────┴───────────────┴─────────────────────────────────────────────────────────────┘ +``` + +
+ +### Configure Docker daemon socket to connect to. +You can configure Docker daemon socket with `DOCKER_HOST` or `--docker-host`. + +```shell +$ trivy image --docker-host tcp://127.0.0.1:2375 YOUR_IMAGE +``` + +### Configure Podman daemon socket to connect to. +You can configure Podman daemon socket with `--podman-host`. + +```shell +$ trivy image --podman-host /run/user/1000/podman/podman.sock YOUR_IMAGE +``` + +### Prevent scanning oversized container images +Use the `--max-image-size` flag to avoid scanning images that exceed a specified size. The size is specified in a human-readable format[^1] (e.g., `100MB`, `10GB`). + +An error is returned in the following cases: + +- if the compressed image size exceeds the limit, +- if the accumulated size of the uncompressed layers exceeds the limit during their pulling. + +The layers are pulled into a temporary folder during their pulling and are always cleaned up, even after a successful scan. + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + + +Example Usage: +```bash +# Limit uncompressed image size to 10GB +$ trivy image --max-image-size=10GB myapp:latest +``` + +Error Output: +```bash +Error: uncompressed image size (15GB) exceeds maximum allowed size (10GB) +``` + +[^1]: Trivy uses decimal (SI) prefixes (based on 1000) for size. +[^2]: SPDX uses `package` instead of `component`. diff --git a/docs/guide/target/filesystem.md b/docs/guide/target/filesystem.md new file mode 100644 index 000000000000..273a51188717 --- /dev/null +++ b/docs/guide/target/filesystem.md @@ -0,0 +1,103 @@ +# Filesystem + +Scan your local projects for + +- Vulnerabilities +- Misconfigurations +- Secrets +- Licenses + +By default, vulnerability and secret scanning are enabled, and you can configure that with `--scanners`. + +```bash +$ trivy fs /path/to/project +``` + +It's also possible to scan a single file. + +``` +$ trivy fs ~/src/github.com/aquasecurity/trivy-ci-test/Pipfile.lock +``` + +## Scanners +### Vulnerabilities +It is enabled by default. +Trivy will look for vulnerabilities based on lock files such as Gemfile.lock and package-lock.json. +See [here](../scanner/vulnerability.md) for the detail. + +``` +$ trivy fs ~/src/github.com/aquasecurity/trivy-ci-test +``` + +
+Result + +``` +2020-06-01T17:06:58.652+0300 WARN OS is not detected and vulnerabilities in OS packages are not detected. +2020-06-01T17:06:58.652+0300 INFO Detecting pipenv vulnerabilities... +2020-06-01T17:06:58.691+0300 INFO Detecting cargo vulnerabilities... + +Pipfile.lock +============ +Total: 10 (UNKNOWN: 2, LOW: 0, MEDIUM: 6, HIGH: 2, CRITICAL: 0) + ++---------------------+------------------+----------+-------------------+------------------------+------------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++---------------------+------------------+----------+-------------------+------------------------+------------------------------------+ +| django | CVE-2020-7471 | HIGH | 2.0.9 | 3.0.3, 2.2.10, 1.11.28 | django: potential | +| | | | | | SQL injection via | +| | | | | | StringAgg(delimiter) | ++ +------------------+----------+ +------------------------+------------------------------------+ +| | CVE-2019-19844 | MEDIUM | | 3.0.1, 2.2.9, 1.11.27 | Django: crafted email address | +| | | | | | allows account takeover | ++ +------------------+ + +------------------------+------------------------------------+ +| | CVE-2019-3498 | | | 2.1.5, 2.0.10, 1.11.18 | python-django: Content | +| | | | | | spoofing via URL path in | +| | | | | | default 404 page | ++ +------------------+ + +------------------------+------------------------------------+ +| | CVE-2019-6975 | | | 2.1.6, 2.0.11, 1.11.19 | python-django: | +| | | | | | memory exhaustion in | +| | | | | | django.utils.numberformat.format() | ++---------------------+------------------+----------+-------------------+------------------------+------------------------------------+ +... +``` + +
+ +### Misconfigurations +It is disabled by default and can be enabled with `--scanners misconfig`. +See [here](../scanner/misconfiguration/index.md) for the detail. + +```shell +$ trivy fs --scanners misconfig /path/to/project +``` + +### Secrets +It is enabled by default. +See [here](../scanner/secret.md) for the detail. + +```shell +$ trivy fs /path/to/project +``` + +### Licenses +It is disabled by default. +See [here](../scanner/license.md) for the detail. + +```shell +$ trivy fs --scanners license /path/to/project +``` + +## SBOM generation +Trivy can generate SBOM for local projects. +See [here](../supply-chain/sbom.md) for the detail. + +## Scan Cache +When scanning local projects, it doesn't use the cache by default. +However, when the local project is a git repository with clean status and the cache backend other than the memory one is enabled, it stores analysis results, using the latest commit hash as the key. + +```shell +$ trivy fs --cache-backend fs /path/to/git/repo +``` + +More details are available in the [cache documentation](../configuration/cache.md#scan-cache-backend). diff --git a/docs/guide/target/kubernetes.md b/docs/guide/target/kubernetes.md new file mode 100644 index 000000000000..e9aec4dafc7f --- /dev/null +++ b/docs/guide/target/kubernetes.md @@ -0,0 +1,490 @@ +# Kubernetes + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +Trivy can connect to your Kubernetes cluster and scan it for security issues using the `trivy k8s` command. This page covers the technical capabilities of Trivy Kubernetes scanning. +Trivy can also be installed *inside* your cluster as a Kubernetes Operator, and continuously scan it. For more about this, please see the [Trivy Operator](https://aquasecurity.github.io/trivy-operator/) project. + +When scanning a Kubernetes cluster, Trivy differentiates between the following: + +1. Cluster infrastructure (e.g api-server, kubelet, addons) +1. Cluster configuration (e.g Roles, ClusterRoles). +1. Application workloads (e.g nginx, postgresql). + +When scanning any of the above, the container image is scanned separately to the Kubernetes resource definition (the YAML manifest) that defines the resource. + +Container image is scanned for: + +- Vulnerabilities +- Misconfigurations +- Exposed secrets + +Kubernetes resource definition is scanned for: + +- Vulnerabilities (Open Source Libraries, Control Plane and Node Components) +- Misconfigurations +- Exposed secrets + +## Kubernetes target configurations + +```sh +trivy k8s [flags] [CONTEXT] - if the target name [CONTEXT] is not specified, the default will be used. +``` + +for example: + +```sh +trivy k8s --report summary +``` + +!!! note "JSON result for multi-container pods" + For multi-container pods, it may be challenging to associate results with specific images in the JSON summary report. Kubernetes treats a pod as a single object, so individual images within the pod aren't distinguished. + For detailed information, please use the `--report all` option. + +By default Trivy will look for a [`kubeconfig` configuration file in the default location](https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/), and use the default cluster that is specified. +You can also specify a `kubeconfig` using the `--kubeconfig` flag: + +```sh +trivy k8s --kubeconfig ~/.kube/config2 +``` + +## Required roles +To successfully scan a Kubernetes cluster, `trivy kubernetes` subcommand must be executed under a role or a cluster role that has some specific permissions. + +The role must have `list` verb for all resources (`"*"`) inside the following API groups: core (`""`), `"apps"`, `"batch"`,`"networking.k8s.io"`, `"rbac.authorization.k8s.io"`: +```yaml +- apiGroups: [""] + resources: ["*"] + verbs: ["list"] +- apiGroups: ["apps", "batch", "networking.k8s.io", "rbac.authorization.k8s.io"] + resources: ["*"] + verbs: ["list"] +``` +If `node collector` is enabled (default: enabled), Trivy needs a cluster role with some additional permissions to run and track the jobs: +```yaml +- apiGroups: [""] + resources: ["nodes/proxy", "pods/log"] + verbs: ["get"] +- apiGroups: [""] + resources: ["events"] + verbs: ["watch"] +- apiGroups: ["batch"] + resources: ["jobs", "cronjobs"] + verbs: ["list", "get"] +- apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["create","delete", "watch"] +- apiGroups: [""] + resources: ["namespaces"] + verbs: ["create"] +``` + +### Skip-images + +By default, all cluster resource images will be downloaded and scanned. + +You can control whether Trivy will scan and download the cluster resource images. To disable this feature, add the --skip-images flag. + +- `--skip-images` flag will prevent the downloading and scanning of images (including vulnerabilities and secrets) in the cluster resources. + +Example: + +```sh +trivy k8s --report summary --skip-images +``` + +### Include/Exclude Kinds + +You can control which kinds of resources will be discovered using the `--include-kinds` or `--exclude-kinds` comma-separated flags: + +***Note:*** Both flags (`--include-kinds` or `--exclude-kinds`) cannot be set in conjunction. + +- `--include-kinds` will include the listed kinds in cluster scanning. +- `--exclude-kinds` will exclude the listed kinds from cluster scanning. + +By default, all kinds will be included in cluster scanning. + +Example: + +```sh +trivy k8s --report summary --exclude-kinds node,pod +``` + +### Include/Exclude Namespaces + +You can control which namespaces will be discovered using the `--include-namespaces` or `--exclude-namespaces` comma-separated flags: + +***Note:*** Both flags (`--include-namespaces` or `--exclude-namespaces`) cannot be set in conjunction. + +- `--include-namespaces` will include the listed namespaces in cluster scanning. +- `--exclude-namespaces` will exclude the listed namespaces from cluster scanning. + +By default, all namespaces will be included in cluster scanning. + +!!! note "using `--exclude-namespaces`" + Trivy requires a complete list of namespaces to exclude specific ones. Therefore, `--exclude-namespaces` option is only available for cluster roles now. + +Example: + +```sh +trivy k8s --report summary --exclude-namespace dev-system,staging-system +``` + +## Control Plane and Node Components Vulnerability Scanning + +Trivy is capable of discovering Kubernetes control plane (apiserver, controller-manager and etc) and node components(kubelet, kube-proxy and etc), matching them against the [official Kubernetes vulnerability database feed](https://github.com/aquasecurity/vuln-list-k8s), and reporting any vulnerabilities it finds. + +To read more about KBOM, see the [documentation for Kubernetes scanning](./sbom.md#kbom). + +```sh +trivy k8s --scanners vuln --report all + +NodeComponents/kind-control-plane (kubernetes) + +Total: 3 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 2, CRITICAL: 0) + +┌────────────────┬────────────────┬──────────┬────────┬───────────────────┬──────────────────────────────────┬───────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├────────────────┼────────────────┼──────────┼────────┼───────────────────┼──────────────────────────────────┼───────────────────────────────────────────────────┤ +│ k8s.io/kubelet │ CVE-2023-2431 │ LOW │ fixed │ 1.21.1 │ 1.24.14, 1.25.10, 1.26.5, 1.27.2 │ Bypass of seccomp profile enforcement │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-2431 │ +│ ├────────────────┼──────────┤ │ ├──────────────────────────────────┼───────────────────────────────────────────────────┤ +│ │ CVE-2021-25741 │ HIGH │ │ │ 1.19.16, 1.20.11, 1.21.5, 1.22.1 │ Symlink exchange can allow host filesystem access │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25741 │ +│ ├────────────────┤ │ │ ├──────────────────────────────────┼───────────────────────────────────────────────────┤ +│ │ CVE-2021-25749 │ │ │ │ 1.22.14, 1.23.11, 1.24.5 │ runAsNonRoot logic bypass for Windows containers │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25749 │ +└────────────────┴────────────────┴──────────┴────────┴───────────────────┴──────────────────────────────────┴───────────────────────────────────────────────────┘ +``` + +## Node-Collector + +Node-collector is a scan job that collects node configuration parameters and permission information. This information will be evaluated against Kubernetes hardening (e.g. CIS benchmark) and best practices values. The scan results will be output in infrastructure assessment and CIS benchmark compliance reports. + +### Disable Node Collector + +You can control whether the node scan-job (`node-collector`) will run in the cluster. To disable it, add the `--disable-node-collector` flag + +- `--disable-node-collector` This flag will exclude findings related to Node (infra assessment) misconfigurations + +By default, the node scan-job (`node-collector`) will run in the cluster. + +Example: + +```sh +trivy k8s --report summary --disable-node-collector +``` + +### Taints and Tolerations + +The node-collector scan-job will run on every node. In case the node has been tainted, it is possible to add toleration to the scan job for it to be scheduled on the tainted node. for more details [see k8s docs](https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/) + +- `--tolerations key1=value1:NoExecute,key2=value2:NoSchedule` this flag will enable node-collector to be schedule on tainted Node + +Example: + +```sh +trivy k8s --report summary --tolerations key1=value1:NoExecute,key2=value2:NoSchedule +``` + +### Exclude Nodes by Label + +You can exclude specific nodes from the scan using the `--exclude-nodes` flag, which takes a label in the format `label-name:label-value` and excludes all matching nodes: + +```sh +trivy k8s --report summary --exclude-nodes kubernetes.io/arch:arm6 +``` + +## Reporting and filtering + +Since scanning an entire cluster for any security issue can be overwhelming, By default Trivy summarizes the results in a simple "summary" view. +By scoping the scan on a specific resource, you can see the detailed report. +You can always choose the report granularity using the `--report summary`/`--report all` flag. + +Scan a full cluster and generate a simple summary report: + +```sh +trivy k8s --report=summary +``` + +![k8s Summary Report](../../imgs/trivy-k8s.png) + +Filter by severity: + +``` +trivy k8s --severity=CRITICAL --report=all +``` + +Filter by scanners (Vulnerabilities, Secrets or Misconfigurations): + +``` +trivy k8s --scanners=secret --report=summary +# or +trivy k8s --scanners=misconfig --report=summary +``` + +The supported output formats are `table`, which is the default, and `json`. + +``` +trivy k8s --format json -o results.json cluster +``` + +
+Result + +```json +{ + "ClusterName": "minikube", + "Vulnerabilities": [ + { + "Namespace": "default", + "Kind": "Deployment", + "Name": "app", + "Results": [ + { + "Target": "ubuntu:latest (ubuntu 22.04)", + "Class": "os-pkgs", + "Type": "ubuntu", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2016-2781", + "PkgName": "coreutils", + "InstalledVersion": "8.32-4.1ubuntu1", + "Layer": { + "Digest": "sha256:125a6e411906fe6b0aaa50fc9d600bf6ff9bb11a8651727ce1ed482dc271c24c", + "DiffID": "sha256:e59fc94956120a6c7629f085027578e6357b48061d45714107e79f04a81a6f0c" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2016-2781", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Title": "coreutils: Non-privileged session can escape to the parent session in chroot", + "Description": "chroot in GNU coreutils, when used with --userspec, allows local users to escape to the parent session via a crafted TIOCSTI ioctl call, which pushes characters to the terminal's input buffer.", + "Severity": "LOW", + "CweIDs": [ + "CWE-20" + ], + "VendorSeverity": { + "cbl-mariner": 2, + "nvd": 2, + "redhat": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:C/C:N/I:H/A:N", + "V2Score": 2.1, + "V3Score": 6.5 + }, + "redhat": { + "V2Vector": "AV:L/AC:H/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H", + "V2Score": 6.2, + "V3Score": 8.6 + } + }, + "References": [ + "http://seclists.org/oss-sec/2016/q1/452", + "http://www.openwall.com/lists/oss-security/2016/02/28/2", + "http://www.openwall.com/lists/oss-security/2016/02/28/3", + "https://access.redhat.com/security/cve/CVE-2016-2781", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-2781", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://lore.kernel.org/patchwork/patch/793178/", + "https://nvd.nist.gov/vuln/detail/CVE-2016-2781" + ], + "PublishedDate": "2017-02-07T15:59:00Z", + "LastModifiedDate": "2021-02-25T17:15:00Z" + } + ] + } + ] + } + ], + "Misconfigurations": [ + { + "Namespace": "default", + "Kind": "Deployment", + "Name": "app", + "Results": [ + { + "Target": "Deployment/app", + "Class": "config", + "Type": "kubernetes", + "MisconfSummary": { + "Successes": 20, + "Failures": 19 + }, + "Misconfigurations": [ + { + "Type": "Kubernetes Security Check", + "ID": "KSV001", + "Title": "Process can elevate its own privileges", + "Description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.", + "Message": "Container 'app' of Deployment 'app' should set 'securityContext.allowPrivilegeEscalation' to false", + "Namespace": "builtin.kubernetes.KSV001", + "Query": "data.builtin.kubernetes.KSV001.deny", + "Resolution": "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv001", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv001" + ], + "Status": "FAIL", + "Layer": {}, + "IacMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 121, + "EndLine": 133 + } + }, + { + "Type": "Kubernetes Security Check", + "ID": "KSV003", + "Title": "Default capabilities not dropped", + "Description": "The container should drop all default capabilities and add only those that are needed for its execution.", + "Message": "Container 'app' of Deployment 'app' should add 'ALL' to 'securityContext.capabilities.drop'", + "Namespace": "builtin.kubernetes.KSV003", + "Query": "data.builtin.kubernetes.KSV003.deny", + "Resolution": "Add 'ALL' to containers[].securityContext.capabilities.drop.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv003", + "References": [ + "https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/", + "https://avd.aquasec.com/misconfig/ksv003" + ], + "Status": "FAIL", + "Layer": {}, + "IacMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 121, + "EndLine": 133 + } + } + ] + } + ] + }, + { + "Namespace": "default", + "Kind": "ConfigMap", + "Name": "kube-root-ca.crt" + } + ] +} + +``` + +
+ +## Compliance + +This section describes Kubernetes specific compliance reports. +For an overview of Trivy's Compliance feature, including working with custom compliance, check out the [Compliance documentation](../compliance/compliance.md). + +The following reports are available out of the box: + +| Compliance | Name for command | More info | +|----------------------------------------------|--------------------------|---------------------------------------------------------------------------------------------------------------------| +| NSA, CISA Kubernetes Hardening Guidance v1.0 | `k8s-nsa-1.0` | [Link](https://media.defense.gov/2022/Aug/29/2003066362/-1/-1/0/CTR_KUBERNETES_HARDENING_GUIDANCE_1.2_20220829.PDF) | +| CIS Benchmark for Kubernetes v1.23 | `k8s-cis-1.23` | [Link](https://www.cisecurity.org/benchmark/kubernetes) | +| CIS Benchmark for RKE2 v1.24 | `rke2-cis-1.24` | [Link](https://www.cisecurity.org/benchmark/kubernetes) | +| CIS Benchmark for EKS v1.4 | `eks-cis-1.4` | [Link](https://www.cisecurity.org/benchmark/kubernetes) | +| Pod Security Standards, Baseline | `k8s-pss-baseline-0.1` | [Link](https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline) | +| Pod Security Standards, Restricted | `k8s-pss-restricted-0.1` | [Link](https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted) | + +Examples: + +Scan the cluster for Kubernetes Pod Security Standards Baseline compliance: + +``` + +trivy k8s --compliance=k8s-pss-baseline --report summary + +``` + +Get the detailed report for checks: + +``` + +trivy k8s --compliance=k8s-cis-1.23 --report all + +``` + +Get summary report in JSON format: + +``` + +trivy k8s --compliance=k8s-cis-1.23 --report summary --format json + +``` + +Get detailed report in JSON format: + +``` + +trivy k8s --compliance=k8s-cis-1.23 --report all --format json + +``` + +## KBOM + +KBOM, Kubernetes Bill of Materials, is a manifest of all the important components that make up your Kubernetes cluster – Control plane components, Node Components, and Addons, including their versions and images. Which “api-server” version are you currently running? Which flavor of "kubelet" is running on each node? What kind of etcd or storage are you currently using? And most importantly – are there any vulnerabilities known to affect these components? These are all questions that KBOM can help you answer. +For more background on KBOM, see [here](https://blog.aquasec.com/introducing-kbom-kubernetes-bill-of-materials). + +Trivy can generate KBOM in CycloneDX format: + +```sh + +trivy k8s --format cyclonedx --output mykbom.cdx.json + +``` + +Trivy can also scan that generated KBOM (or any SBOM) for vulnerabilities: + +```sh + +trivy sbom mykbom.cdx.json + +``` + +
+Result + +```sh + +2023-09-28T22:52:25.707+0300 INFO Vulnerability scanning is enabled + 2023-09-28T22:52:25.707+0300 INFO Detected SBOM format: cyclonedx-json + 2023-09-28T22:52:25.717+0300 WARN No OS package is detected. Make sure you haven't deleted any files that contain information about the installed packages. + 2023-09-28T22:52:25.717+0300 WARN e.g. files under "/lib/apk/db/", "/var/lib/dpkg/" and "/var/lib/rpm" + 2023-09-28T22:52:25.717+0300 INFO Detected OS: debian gnu/linux + 2023-09-28T22:52:25.717+0300 WARN unsupported os : debian gnu/linux + 2023-09-28T22:52:25.717+0300 INFO Number of language-specific files: 3 + 2023-09-28T22:52:25.717+0300 INFO Detecting kubernetes vulnerabilities... + 2023-09-28T22:52:25.718+0300 INFO Detecting gobinary vulnerabilities... + Kubernetes (kubernetes) + Total: 2 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + ┌────────────────┬────────────────┬──────────┬────────┬───────────────────┬─────────────────────────────────┬──────────────────────────────────────────────────┐ + │ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ + ├────────────────┼────────────────┼──────────┼────────┼───────────────────┼─────────────────────────────────┼──────────────────────────────────────────────────┤ + │ k8s.io/kubelet │ CVE-2021-25749 │ HIGH │ fixed │ 1.24.0 │ 1.22.14, 1.23.11, 1.24.5 │ runAsNonRoot logic bypass for Windows containers │ + │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25749 │ + │ ├────────────────┼──────────┤ │ ├─────────────────────────────────┼──────────────────────────────────────────────────┤ + │ │ CVE-2023-2431 │ LOW │ │ │ 1.24.14, 1.25.9, 1.26.4, 1.27.1 │ Bypass of seccomp profile enforcement │ + │ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-2431 │ + └────────────────┴────────────────┴──────────┴────────┴───────────────────┴─────────────────────────────────┴──────────────────────────────────────────────────┘ +``` + +
+ +Find more in the [documentation for SBOM scanning](./sbom.md). + +Currently KBOM vulnerability matching works for plain Kubernetes distributions and does not work well for vendor variants, including some cloud managed distributions. diff --git a/docs/guide/target/repository.md b/docs/guide/target/repository.md new file mode 100644 index 000000000000..3146913c1aec --- /dev/null +++ b/docs/guide/target/repository.md @@ -0,0 +1,181 @@ +# Code Repository + +Scan your local or remote code repositories for + +- Vulnerabilities +- Misconfigurations +- Secrets +- Licenses + +By default, vulnerability and secret scanning are enabled, and you can configure that with `--scanners`. + +```bash +$ trivy repo (REPO_PATH | REPO_URL) +``` + +For example, you can scan a local repository as below. + +```bash +$ trivy repo ./ +``` + +It's also possible to scan a single file. + +``` +$ trivy repo ./trivy-ci-test/Pipfile.lock +``` + +To scan remote code repositories, you need to specify the URL. + +```bash +$ trivy repo https://github.com/aquasecurity/trivy-ci-test +``` + +## Rationale +`trivy repo` is designed to scan code repositories, and it is intended to be used for scanning local/remote repositories in your machine or in your CI environment. +Therefore, unlike container/VM image scanning, it targets lock files such as package-lock.json and does not target artifacts like JAR files, binary files, etc. +See [here](../scanner/vulnerability.md#language-specific-packages) for the detail. + +## Scanners +### Vulnerabilities +It is enabled by default. +Trivy will look for vulnerabilities based on lock files such as Gemfile.lock and package-lock.json. +See [here](../scanner/vulnerability.md) for the detail. + +``` +$ trivy repo ~/src/github.com/aquasecurity/trivy-ci-test +``` + +
+Result + +``` +2020-06-01T17:06:58.652+0300 WARN OS is not detected and vulnerabilities in OS packages are not detected. +2020-06-01T17:06:58.652+0300 INFO Detecting pipenv vulnerabilities... +2020-06-01T17:06:58.691+0300 INFO Detecting cargo vulnerabilities... + +Pipfile.lock +============ +Total: 10 (UNKNOWN: 2, LOW: 0, MEDIUM: 6, HIGH: 2, CRITICAL: 0) + ++---------------------+------------------+----------+-------------------+------------------------+------------------------------------+ +| LIBRARY | VULNERABILITY ID | SEVERITY | INSTALLED VERSION | FIXED VERSION | TITLE | ++---------------------+------------------+----------+-------------------+------------------------+------------------------------------+ +| django | CVE-2020-7471 | HIGH | 2.0.9 | 3.0.3, 2.2.10, 1.11.28 | django: potential | +| | | | | | SQL injection via | +| | | | | | StringAgg(delimiter) | ++ +------------------+----------+ +------------------------+------------------------------------+ +| | CVE-2019-19844 | MEDIUM | | 3.0.1, 2.2.9, 1.11.27 | Django: crafted email address | +| | | | | | allows account takeover | ++ +------------------+ + +------------------------+------------------------------------+ +| | CVE-2019-3498 | | | 2.1.5, 2.0.10, 1.11.18 | python-django: Content | +| | | | | | spoofing via URL path in | +| | | | | | default 404 page | ++ +------------------+ + +------------------------+------------------------------------+ +| | CVE-2019-6975 | | | 2.1.6, 2.0.11, 1.11.19 | python-django: | +| | | | | | memory exhaustion in | +| | | | | | django.utils.numberformat.format() | ++---------------------+------------------+----------+-------------------+------------------------+------------------------------------+ +... +``` + +
+ +### Misconfigurations +It is disabled by default and can be enabled with `--scanners misconfig`. +See [here](../scanner/misconfiguration/index.md) for the detail. + +```shell +$ trivy repo --scanners misconfig (REPO_PATH | REPO_URL) +``` + +### Secrets +It is enabled by default. +See [here](../scanner/secret.md) for the detail. + +```shell +$ trivy repo (REPO_PATH | REPO_URL) +``` + +### Licenses +It is disabled by default. +See [here](../scanner/license.md) for the detail. + +```shell +$ trivy repo --scanners license (REPO_PATH | REPO_URL) +``` + +## SBOM generation +Trivy can generate SBOM for code repositories. +See [here](../supply-chain/sbom.md) for the detail. + +## Git Metadata +When scanning git repositories (both local and remote), Trivy automatically extracts and includes git metadata in the scan results. +This metadata provides context about the scanned repository. + +The metadata includes information such as: + +- Repository URL +- Branch name +- Tags +- Commit details (hash, message, commiter) +- Author information + +This feature works automatically for any git repository. +When using JSON format output, the git metadata will be included in the `Metadata` field. +For detailed information about the available fields, please refer to the JSON output of your scan results. + +```bash +$ trivy repo --format json +``` + +## Scan Cache +When scanning git repositories, it stores analysis results in the cache, using the latest commit hash as the key. +Note that the cache is not used when the repository is dirty, otherwise Trivy will miss the files that are not committed. + +More details are available in the [cache documentation](../configuration/cache.md#scan-cache-backend). + +## References +The following flags and environmental variables are available for remote git repositories. + +### Scanning a Branch + +Pass a `--branch` argument with a valid branch name on the remote repository provided: + +``` +$ trivy repo --branch +``` + +### Scanning upto a Commit + +Pass a `--commit` argument with a valid commit hash on the remote repository provided: + +``` +$ trivy repo --commit +``` + +### Scanning a Tag + +Pass a `--tag` argument with a valid tag on the remote repository provided: + +``` +$ trivy repo --tag +``` + +### Scanning Private Repositories +In order to scan private GitHub or GitLab repositories, the environment variable `GITHUB_TOKEN` or `GITLAB_TOKEN` must be set, respectively, with a valid token that has access to the private repository being scanned. + +The `GITHUB_TOKEN` environment variable will take precedence over `GITLAB_TOKEN`, so if a private GitLab repository will be scanned, then `GITHUB_TOKEN` must be unset. + +You can find how to generate your GitHub Token in the following [GitHub documentation.](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) + +For example: + +``` +$ export GITHUB_TOKEN="your_private_github_token" +$ trivy repo + +# or +$ export GITLAB_TOKEN="your_private_gitlab_token" +$ trivy repo +``` diff --git a/docs/guide/target/rootfs.md b/docs/guide/target/rootfs.md new file mode 100644 index 000000000000..8a403f57d5e2 --- /dev/null +++ b/docs/guide/target/rootfs.md @@ -0,0 +1,31 @@ +# Rootfs +Rootfs scanning is for special use cases such as + +- Host machine +- [Root filesystem](../advanced/container/embed-in-dockerfile.md) +- [Unpacked filesystem](../advanced/container/unpacked-filesystem.md) + +```bash +$ trivy rootfs /path/to/rootfs +``` + +!!! note + Rootfs scanning works differently from the Filesystem scanning. + You should use `trivy fs` to scan your local projects in CI/CD. + See [here](../scanner/vulnerability.md) for the differences. + +!!! note + Scanning vulnerabilities for `Red Hat` has a limitation, see the [Red Hat](../coverage/os/rhel.md#content-manifests) page for details. + +## Performance Optimization + +By default, Trivy traverses all files from the specified root directory to find target files for scanning. +However, when you only need to scan specific files with absolute paths, you can avoid this traversal, which makes scanning faster. +For example, when scanning only OS packages, no full traversal is performed: + +```bash +$ trivy rootfs --pkg-types os --scanners vuln / +``` + +When scanning language-specific packages or secrets, traversal is necessary because the location of these files is unknown. +If you want to exclude specific directories from scanning for better performance, you can use the [--skip-dirs](../configuration/skipping.md) option. diff --git a/docs/guide/target/sbom.md b/docs/guide/target/sbom.md new file mode 100644 index 000000000000..5d6a5f184463 --- /dev/null +++ b/docs/guide/target/sbom.md @@ -0,0 +1,152 @@ +# SBOM scanning + +Trivy can take the following SBOM formats as an input and scan for vulnerabilities and licenses. + +- CycloneDX +- SPDX +- SPDX JSON +- CycloneDX-type attestation +- [KBOM](./kubernetes.md#kbom) in CycloneDX format + +To scan SBOM, you can use the `sbom` subcommand and pass the path to the SBOM. +The input format is automatically detected. + +```bash + +$ trivy sbom /path/to/sbom_file + +``` + +By default, vulnerability scan in SBOM is executed. You can use `--scanners vuln,license` +command property to select also license scan, or `--scanners license` alone. + +!!! note + Passing SBOMs generated by tool other than Trivy may result in inaccurate detection + because Trivy relies on custom properties in SBOM for accurate scanning. + +## CycloneDX + +Trivy supports CycloneDX as an input. + +!!! note + CycloneDX XML is not supported at the moment. + +```bash +$ trivy sbom /path/to/cyclonedx.json +``` + +## SPDX + +Trivy supports the SPDX SBOM as an input. + +The following SPDX formats are supported: + +- Tag-value (`--format spdx`) +- JSON (`--format spdx-json`) + +```bash +$ trivy image --format spdx-json --output spdx.json alpine:3.16.0 +$ trivy sbom spdx.json +``` + +
+Result + +``` +2022-09-15T21:32:27.168+0300 INFO Vulnerability scanning is enabled +2022-09-15T21:32:27.169+0300 INFO Detected SBOM format: spdx-json +2022-09-15T21:32:27.210+0300 INFO Detected OS: alpine +2022-09-15T21:32:27.210+0300 INFO Detecting Alpine vulnerabilities... +2022-09-15T21:32:27.211+0300 INFO Number of language-specific files: 0 + +spdx.json (alpine 3.16.0) +========================= +Total: 5 (UNKNOWN: 0, LOW: 0, MEDIUM: 2, HIGH: 2, CRITICAL: 1) + +┌──────────────┬────────────────┬──────────┬───────────────────┬───────────────┬────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ busybox │ CVE-2022-30065 │ HIGH │ 1.35.0-r13 │ 1.35.0-r15 │ busybox: A use-after-free in Busybox's awk applet leads to │ +│ │ │ │ │ │ denial of service... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-30065 │ +├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ libcrypto1.1 │ CVE-2022-2097 │ MEDIUM │ 1.1.1o-r0 │ 1.1.1q-r0 │ openssl: AES OCB fails to encrypt some bytes │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-2097 │ +├──────────────┤ │ │ │ │ │ +│ libssl1.1 │ │ │ │ │ │ +│ │ │ │ │ │ │ +├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ ssl_client │ CVE-2022-30065 │ HIGH │ 1.35.0-r13 │ 1.35.0-r15 │ busybox: A use-after-free in Busybox's awk applet leads to │ +│ │ │ │ │ │ denial of service... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-30065 │ +├──────────────┼────────────────┼──────────┼───────────────────┼───────────────┼────────────────────────────────────────────────────────────┤ +│ zlib │ CVE-2022-37434 │ CRITICAL │ 1.2.12-r1 │ 1.2.12-r2 │ zlib: a heap-based buffer over-read or buffer overflow in │ +│ │ │ │ │ │ inflate in inflate.c... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-37434 │ +└──────────────┴────────────────┴──────────┴───────────────────┴───────────────┴────────────────────────────────────────────────────────────┘ +``` + +
+ +## SBOM attestation + +You can also scan an SBOM attestation. +In the following example, [Cosign](https://github.com/sigstore/cosign) gets an attestation and Trivy scans it. +You must create CycloneDX-type attestation before trying the example. +To learn more about how to create an CycloneDX-Type attestation and attach it to an image, see the [SBOM attestation page](../supply-chain/attestation/sbom.md#sign-with-a-local-key-pair). + +```bash +$ cosign verify-attestation --key /path/to/cosign.pub --type cyclonedx > sbom.cdx.intoto.jsonl +$ trivy sbom ./sbom.cdx.intoto.jsonl + +sbom.cdx.intoto.jsonl (alpine 3.7.3) +========================= +Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 2) + +┌────────────┬────────────────┬──────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├────────────┼────────────────┼──────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────┤ +│ musl │ CVE-2019-14697 │ CRITICAL │ 1.1.18-r3 │ 1.1.18-r4 │ musl libc through 1.1.23 has an x87 floating-point stack │ +│ │ │ │ │ │ adjustment im ...... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2019-14697 │ +├────────────┤ │ │ │ │ │ +│ musl-utils │ │ │ │ │ │ +│ │ │ │ │ │ │ +│ │ │ │ │ │ │ +└────────────┴────────────────┴──────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────┘ +``` + +## KBOM + +To read more about KBOM, see the [documentation for Kubernetes scanning](./kubernetes.md#kbom). + +The supported Kubernetes distributions for core components vulnerability scanning are: + +- [Kubernetes upstream](https://github.com/kubernetes/kubernetes) +- [Rancher rke2](https://github.com/rancher/rke2) + +```sh + +$ trivy k8s --format cyclonedx cluster -o kbom.json +$ trivy sbom kbom.json +2023-09-28T22:52:25.707+0300 INFO Vulnerability scanning is enabled +2023-09-28T22:52:25.717+0300 INFO Number of language-specific files: 3 +2023-09-28T22:52:25.717+0300 INFO Detecting kubernetes vulnerabilities... +2023-09-28T22:52:25.718+0300 INFO Detecting gobinary vulnerabilities... + +Kubernetes (kubernetes) + +Total: 2 (UNKNOWN: 0, LOW: 1, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + + +┌────────────────┬────────────────┬──────────┬────────┬───────────────────┬────────────────────────────────┬──────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├────────────────┼────────────────┼──────────┼────────┼───────────────────┼────────────────────────────────┼──────────────────────────────────────────────────┤ +│ k8s.io/kubelet │ CVE-2021-25749 │ HIGH │ fixed │ 1.24.0 │ 1.22.14, 1.23.11, 1.24.5 │ runAsNonRoot logic bypass for Windows containers │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25749 │ +│ ├────────────────┼──────────┤ │ ├────────────────────────────────┼──────────────────────────────────────────────────┤ +│ │ CVE-2023-2431 │ LOW │ │ │1.24.14, 1.25.9, 1.26.4, 1.27.1 │ Bypass of seccomp profile enforcement │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2023-2431 │ +└────────────────┴────────────────┴──────────┴────────┴───────────────────┴────────────────────────────────┴──────────────────────────────────────────────────┘ + +``` diff --git a/docs/guide/target/vm.md b/docs/guide/target/vm.md new file mode 100644 index 000000000000..8cbe5284af45 --- /dev/null +++ b/docs/guide/target/vm.md @@ -0,0 +1,251 @@ +# Virtual Machine Image + +!!! warning "EXPERIMENTAL" + This feature might change without preserving backwards compatibility. + +To scan virtual machine (VM) images, you can use the `vm` subcommand. + +## Targets +The following targets are currently supported: + +- Local file +- AWS EC2 + - Amazon Machine Image (AMI) + - Amazon Elastic Block Store (EBS) Snapshot + +### Local file +Pass the path to your local VM image file. + +```bash +$ trivy vm --scanners vuln disk.vmdk +``` + +
+Result + +``` +disk.vmdk (amazon 2 (Karoo)) +=========================================================================================== +Total: 802 (UNKNOWN: 0, LOW: 17, MEDIUM: 554, HIGH: 221, CRITICAL: 10) + +┌────────────────────────────┬────────────────┬──────────┬───────────────────────────────┬───────────────────────────────┬──────────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Installed Version │ Fixed Version │ Title │ +├────────────────────────────┼────────────────┼──────────┼───────────────────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ amazon-ssm-agent │ CVE-2022-24675 │ HIGH │ 3.0.529.0-1.amzn2 │ 3.1.1575.0-1.amzn2 │ golang: encoding/pem: fix stack overflow in Decode │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2022-24675 │ +├────────────────────────────┼────────────────┤ ├───────────────────────────────┼───────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ bind-export-libs │ CVE-2021-25215 │ │ 32:9.11.4-26.P2.amzn2.4 │ 32:9.11.4-26.P2.amzn2.5 │ bind: An assertion check can fail while answering queries │ +│ │ │ │ │ │ for DNAME records... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25215 │ +│ ├────────────────┼──────────┤ ├───────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2021-25214 │ MEDIUM │ │ 32:9.11.4-26.P2.amzn2.5.2 │ bind: Broken inbound incremental zone update (IXFR) can │ +│ │ │ │ │ │ cause named to terminate... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25214 │ +├────────────────────────────┼────────────────┼──────────┤ ├───────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ bind-libs │ CVE-2021-25215 │ HIGH │ │ 32:9.11.4-26.P2.amzn2.5 │ bind: An assertion check can fail while answering queries │ +│ │ │ │ │ │ for DNAME records... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25215 │ +│ ├────────────────┼──────────┤ ├───────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2021-25214 │ MEDIUM │ │ 32:9.11.4-26.P2.amzn2.5.2 │ bind: Broken inbound incremental zone update (IXFR) can │ +│ │ │ │ │ │ cause named to terminate... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25214 │ +├────────────────────────────┼────────────────┼──────────┤ ├───────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ bind-libs-lite │ CVE-2021-25215 │ HIGH │ │ 32:9.11.4-26.P2.amzn2.5 │ bind: An assertion check can fail while answering queries │ +│ │ │ │ │ │ for DNAME records... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25215 │ +│ ├────────────────┼──────────┤ ├───────────────────────────────┼──────────────────────────────────────────────────────────────┤ +│ │ CVE-2021-25214 │ MEDIUM │ │ 32:9.11.4-26.P2.amzn2.5.2 │ bind: Broken inbound incremental zone update (IXFR) can │ +│ │ │ │ │ │ cause named to terminate... │ +│ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2021-25214 │ +├────────────────────────────┼────────────────┼──────────┤ ├───────────────────────────────┼──────────────────────────────────────────────────────────────┤ +... +``` + +
+ +### Amazon Machine Image (AMI) +You can specify your AMI ID with the `ami:` prefix. + +```shell +$ trivy vm ami:${your_ami_id} +``` + +!!! note + AMIs in the marketplace are not supported because the EBS direct APIs don't support that. + See [the AWS documentation][ebsapi-elements] for the detail. + +#### Example + +```shell +$ trivy vm --scanners vuln ami:ami-0123456789abcdefg +``` + +If you want to scan a AMI of non-default setting region, you can set any region via `--aws-region` option. + +```shell +$ trivy vm --aws-region ap-northeast-1 ami:ami-0123456789abcdefg +``` + + +#### Required Actions +Some actions on EBS are also necessary since Trivy scans an EBS snapshot tied to the specified AMI under the hood. + +- ec2:DescribeImages +- ebs:ListSnapshotBlocks +- ebs:GetSnapshotBlock + +### Amazon Elastic Block Store (EBS) Snapshot +You can specify your EBS snapshot ID with the `ebs:` prefix. + +```shell +$ trivy vm ebs:${your_ebs_snapshot_id} +``` + +!!! note + Public snapshots are not supported because the EBS direct APIs don't support that. + See [the AWS documentation][ebsapi-elements] for the detail. + +#### Example + +```shell +$ trivy vm --scanners vuln ebs:snap-0123456789abcdefg +``` + + +If you want to scan an EBS Snapshot of non-default setting region, you can set any region via `--aws-region` option. + +```shell +$ trivy vm --aws-region ap-northeast-1 ebs:ebs-0123456789abcdefg +``` + +The above command takes a while as it calls EBS API and fetches the EBS blocks. +If you want to scan the same snapshot several times, you can download the snapshot locally by using [coldsnap][coldsnap] maintained by AWS. +Then, Trivy can scan the local VM image file. + +```shell +$ coldsnap download snap-0123456789abcdefg disk.img +$ trivy vm ./disk.img +``` + +#### Required Actions + +- ebs:ListSnapshotBlocks +- ebs:GetSnapshotBlock + +## Scanners +Trivy supports VM image scanning for + +- Vulnerabilities +- Misconfigurations +- Secrets +- Licenses + +### Vulnerabilities +It is enabled by default. +You can simply specify your VM image location. +It detects known vulnerabilities in your VM image. +See [here](../scanner/vulnerability.md) for the detail. + +``` +$ trivy vm [YOUR_VM_IMAGE] +``` + +!!! note + Scanning `Red Hat` has a limitation, see the [Red Hat](../coverage/os/rhel.md#content-manifests) page for details. + +### Misconfigurations +It is supported, but it is not useful in most cases. +As mentioned [here](../scanner/misconfiguration/index.md), Trivy mainly supports Infrastructure as Code (IaC) files for misconfigurations. +If your VM image includes IaC files such as Kubernetes YAML files or Terraform files, you should enable this feature with `--scanners misconfig`. + +``` +$ trivy vm --scanners misconfig [YOUR_VM_IMAGE] +``` + +### Secrets +It is enabled by default. +See [here](../scanner/secret.md) for the detail. + +```shell +$ trivy vm [YOUR_VM_IMAGE] +``` + +!!! tip + The scanning could be faster if you enable only vulnerability scanning (`--scanners vuln`) because Trivy tries to download only necessary blocks for vulnerability detection. + +### Licenses +It is disabled by default. +See [here](../scanner/license.md) for the detail. + +```shell +$ trivy vm --scanners license [YOUR_VM_IMAGE] +``` + +## SBOM generation +Trivy can generate SBOM for VM images. +See [here](../supply-chain/sbom.md) for the detail. + +## Scan Cache +When scanning AMI or EBS snapshots, it stores analysis results in the cache, using the snapshot ID. +Scanning the same snapshot several times skips analysis if the cache is already available. + +When scanning local files, it doesn't use the cache by default. + +More details are available in the [cache documentation](../configuration/cache.md#scan-cache-backend). + +## Supported Architectures + +### Virtual machine images + +| Image format | Support | +|--------------|:-------:| +| VMDK | ✔ | +| OVA | | +| VHD | | +| VHDX | | +| QCOW2 | | + + +#### VMDK disk types + +| VMDK disk type | Support | +|-----------------------------|:-------:| +| streamOptimized | ✔ | +| monolithicSparse | | +| vmfs | | +| vmfsSparse | | +| twoGbMaxExtentSparse | | +| monolithicFlat | | +| twoGbMaxExtentFlat | | +| vmfsRaw | | +| fullDevice | | +| partitionedDevice | | +| vmfsRawDeviceMap | | +| vmfsPassthroughRawDeviceMap | | + +Reference: [VMware Virtual Disk Format 1.1.pdf][vmdk] + + +### Disk partitions + +| Disk format | Support | +|------------------------------|:-------:| +| Master boot record (MBR) | ✔ | +| Extended master boot record | | +| GUID partition table (GPT) | ✔ | +| Logical volume manager (LVM) | | + +### Filesystems + +| Filesystem format | Support | +|-------------------|:-------:| +| XFS | ✔ | +| EXT4 | ✔ | +| EXT2/3 | ✔ | +| ZFS | | + + +[vmdk]: https://github.com/libyal/libvmdk/blob/main/documentation/VMWare%20Virtual%20Disk%20Format%20(VMDK).asciidoc +[ebsapi-elements]: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-accessing-snapshot.html#ebsapi-elements +[coldsnap]: https://github.com/awslabs/coldsnap + diff --git a/docs/imgs/Security-Hub.jpeg b/docs/imgs/Security-Hub.jpeg new file mode 100644 index 000000000000..cece6bcc0c18 Binary files /dev/null and b/docs/imgs/Security-Hub.jpeg differ diff --git a/docs/imgs/argocd-ui.png b/docs/imgs/argocd-ui.png new file mode 100644 index 000000000000..f9e31a958abb Binary files /dev/null and b/docs/imgs/argocd-ui.png differ diff --git a/docs/imgs/client-server.png b/docs/imgs/client-server.png new file mode 100644 index 000000000000..fce67eefdf0b Binary files /dev/null and b/docs/imgs/client-server.png differ diff --git a/docs/imgs/docker-desktop.png b/docs/imgs/docker-desktop.png new file mode 100644 index 000000000000..3eafeb8e8941 Binary files /dev/null and b/docs/imgs/docker-desktop.png differ diff --git a/docs/imgs/eks-benchmarks.png b/docs/imgs/eks-benchmarks.png new file mode 100644 index 000000000000..68d931cffe37 Binary files /dev/null and b/docs/imgs/eks-benchmarks.png differ diff --git a/docs/imgs/excalidraw/client-server.excalidraw b/docs/imgs/excalidraw/client-server.excalidraw new file mode 100644 index 000000000000..ad67667a9c4a --- /dev/null +++ b/docs/imgs/excalidraw/client-server.excalidraw @@ -0,0 +1,1151 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "ribsikcWWSf8Aw4M6BOYJ", + "type": "rectangle", + "x": 458.6370544433594, + "y": 379.5105285644531, + "width": 169.21945190429688, + "height": 104.2457275390625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 698826281, + "version": 381, + "versionNonce": 413251305, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "JinTKutXOSTzURP969rwo", + "nXkZQsrtjmNqPM6SmPxrc", + "Ljv7RJF8FjyOJX3vZ2ou8", + "o_BKODe9vjtHWQsJ8F3tD", + "zTrormMP-N-W6thSxlTgK", + "fBva4zCGT2vIFPpTWC-oZ" + ] + }, + { + "id": "GkrbG--OvBT9zJ-w8E5oQ", + "type": "ellipse", + "x": 427.04335021972656, + "y": 65.240966796875, + "width": 215.20677185058594, + "height": 152.14088439941406, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#fa5252", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 995070601, + "version": 684, + "versionNonce": 78137383, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY" + ] + }, + { + "id": "KPLvdnBVoU3U5XGhCch-x", + "type": "text", + "x": 511.77996826171875, + "y": 407.1625671386719, + "width": 62, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 938497417, + "version": 253, + "versionNonce": 788328905, + "isDeleted": false, + "boundElementIds": null, + "text": "Trivy\nServer", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "Ry65W-Cczzy8M9JsouEgZ", + "type": "text", + "x": 488.6784362792969, + "y": 116.34368896484375, + "width": 89, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1676815783, + "version": 456, + "versionNonce": 14330695, + "isDeleted": false, + "boundElementIds": null, + "text": "GitHub\n(trivy-db)", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "eJn9MVaNlcyj8-YPfrBSY", + "type": "arrow", + "x": 538.7528045696993, + "y": 220.23935960349465, + "width": 0.45618097890837817, + "height": 154.16252445127046, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1741763111, + "version": 1298, + "versionNonce": 267243689, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.45618097890837817, + 154.16252445127046 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "GkrbG--OvBT9zJ-w8E5oQ", + "focus": -0.03598869040285281, + "gap": 2.911871895302724 + }, + "endBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": -0.04563780983822428, + "gap": 5.10864450968802 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "2P02jU3j2eEc92lH0YSwm", + "type": "text", + "x": 575.9393615722656, + "y": 254.42640686035156, + "width": 327, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 501371753, + "version": 629, + "versionNonce": 185838183, + "isDeleted": false, + "boundElementIds": null, + "text": "1. Download Trivy DB\n(including vulnerability information)", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "nXkZQsrtjmNqPM6SmPxrc", + "type": "arrow", + "x": 128.84802246093747, + "y": 395.4753877561888, + "width": 316.63877589590845, + "height": 0.1213064482017785, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 2075020231, + "version": 327, + "versionNonce": 1795630503, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 316.63877589590845, + 0.1213064482017785 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "2ZWTQQ2dQDWF8xj1BLdVG", + "focus": 1.483056059007069, + "gap": 12.799499511718778 + }, + "endBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": 0.46421771350547186, + "gap": 12.273425031825923 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "OPRFQTrsfmF5a7us-mxkI", + "type": "draw", + "x": -60.78019714355469, + "y": 464.1100280880928, + "width": 72.64572143554688, + "height": 72.18890380859375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1541066697, + "version": 569, + "versionNonce": 1115805895, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + -39.526153564453125, + 16.35540771484375 + ], + [ + -32.219512939453125, + 72.170166015625 + ], + [ + 30.40386962890625, + 72.18890380859375 + ], + [ + 33.11956787109375, + 32.5743408203125 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "QzKnYI7o5Yxg_7szebL6Z", + "type": "draw", + "x": -37.06263732910158, + "y": 508.8429382443428, + "width": 16.976165771484375, + "height": 16.45367431640625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 2144447815, + "version": 203, + "versionNonce": 28982057, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 6.905059814453125, + -16.45367431640625 + ], + [ + 16.976165771484375, + -5.1099853515625 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "qqbEan2u1uU3loTdTvQ4B", + "type": "text", + "x": -184.67498779296875, + "y": 548.9957397580147, + "width": 230, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1619652615, + "version": 833, + "versionNonce": 981018599, + "isDeleted": false, + "boundElementIds": null, + "text": "6. Analyze pulled layers", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "GgiFj7vEEZT-VPfKiX6oo", + "type": "rectangle", + "x": -48.80625915527344, + "y": 386.3571411073208, + "width": 169.21945190429688, + "height": 104.2457275390625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1005158727, + "version": 500, + "versionNonce": 1358533383, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "JinTKutXOSTzURP969rwo", + "nXkZQsrtjmNqPM6SmPxrc", + "2tbWET6O9G38YibdIyLpy", + "FuK6iJ6YprzRoh4wg1UHf", + "o_BKODe9vjtHWQsJ8F3tD", + "fBva4zCGT2vIFPpTWC-oZ" + ] + }, + { + "id": "qQZYQjma-4h8rOrxn5yBo", + "type": "text", + "x": 4.3366546630859375, + "y": 414.00917968153954, + "width": 57, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1918851753, + "version": 381, + "versionNonce": 56771817, + "isDeleted": false, + "boundElementIds": null, + "text": "Trivy\nClient", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "vtOTfv319aihVmgTMMbQG", + "type": "ellipse", + "x": -95.58086395263672, + "y": 68.13672637939453, + "width": 249.63902282714844, + "height": 152.14088439941406, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#228be6", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1265127495, + "version": 929, + "versionNonce": 1316732873, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "2tbWET6O9G38YibdIyLpy", + "FuK6iJ6YprzRoh4wg1UHf" + ] + }, + { + "id": "bz64cTcmJtjZotVar2MHr", + "type": "text", + "x": -73.4975357055664, + "y": 108.37564849853516, + "width": 202, + "height": 74, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 976812969, + "version": 670, + "versionNonce": 938982727, + "isDeleted": false, + "boundElementIds": null, + "text": "Container Registries\nor\nContainer Engines", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 67 + }, + { + "id": "Z5DzSXSTeTNfyo9GRr57B", + "type": "ellipse", + "x": 871.677619934082, + "y": 347.7999496459961, + "width": 215.20677185058594, + "height": 152.14088439941406, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#15aabf", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1020418857, + "version": 836, + "versionNonce": 845340329, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "Ljv7RJF8FjyOJX3vZ2ou8", + "zTrormMP-N-W6thSxlTgK" + ] + }, + { + "id": "SGILIw_oLMs1yuieBWcoP", + "type": "text", + "x": 898.8127059936523, + "y": 398.90267181396484, + "width": 158, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1953695719, + "version": 653, + "versionNonce": 1086225511, + "isDeleted": false, + "boundElementIds": [ + "zTrormMP-N-W6thSxlTgK" + ], + "text": "Cache Backend\n(Local or Redis)", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "Ljv7RJF8FjyOJX3vZ2ou8", + "type": "arrow", + "x": 635.243173087202, + "y": 408.1721813855087, + "width": 237.82178502385182, + "height": 0.48828450950475144, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 744746601, + "version": 617, + "versionNonce": 230891913, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 237.82178502385182, + -0.48828450950475144 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": -0.4450219327551968, + "gap": 7.386666739545717 + }, + "endBinding": { + "elementId": "Z5DzSXSTeTNfyo9GRr57B", + "focus": 0.21564885268585596, + "gap": 1.0335001891177882 + }, + "startArrowhead": "arrow", + "endArrowhead": "arrow" + }, + { + "id": "BlRXUB6fETT_zZD6O1fNL", + "type": "text", + "x": -163.05282592773438, + "y": 268.6500244140625, + "width": 124, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 609582183, + "version": 841, + "versionNonce": 2090882951, + "isDeleted": false, + "boundElementIds": null, + "text": "2. Download\n manifest", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "2tbWET6O9G38YibdIyLpy", + "type": "arrow", + "x": -12.186006749219864, + "y": 221.1919311337398, + "width": 0.45618097890837817, + "height": 154.16252445127046, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1740746569, + "version": 1437, + "versionNonce": 1574439017, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.45618097890837817, + 154.16252445127046 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "vtOTfv319aihVmgTMMbQG", + "focus": 0.3337009650548285, + "gap": 5.11281200236435 + }, + "endBinding": { + "elementId": "GgiFj7vEEZT-VPfKiX6oo", + "focus": -0.5585687247500966, + "gap": 11.002685522310514 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "2ZWTQQ2dQDWF8xj1BLdVG", + "type": "text", + "x": 141.64752197265625, + "y": 364.4148864746094, + "width": 294, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1932287977, + "version": 912, + "versionNonce": 299486887, + "isDeleted": false, + "boundElementIds": [ + "nXkZQsrtjmNqPM6SmPxrc" + ], + "text": "3. Ask missing layers in cache", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "pIwVlwytOx1ZJ4aOCvtRF", + "type": "text", + "x": 636.46630859375, + "y": 352.5987548828125, + "width": 246, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 973317671, + "version": 871, + "versionNonce": 1819087689, + "isDeleted": false, + "boundElementIds": null, + "text": "4. Return existing layers", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "FuK6iJ6YprzRoh4wg1UHf", + "type": "arrow", + "x": 75.37933138554581, + "y": 220.52838743256788, + "width": 0.45618097890837817, + "height": 154.16252445127046, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1148774855, + "version": 1487, + "versionNonce": 722952647, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.45618097890837817, + 154.16252445127046 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "vtOTfv319aihVmgTMMbQG", + "focus": -0.36784928325571326, + "gap": 5.485033392258245 + }, + "endBinding": { + "elementId": "GgiFj7vEEZT-VPfKiX6oo", + "focus": 0.4745035772454494, + "gap": 11.666229223482446 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "jgdyeMn1ZO33kPci2KoGL", + "type": "text", + "x": 94.91287231445312, + "y": 265.060302734375, + "width": 162, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1318802377, + "version": 872, + "versionNonce": 1037943337, + "isDeleted": false, + "boundElementIds": null, + "text": "5. Pull only\n missing layers", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "o_BKODe9vjtHWQsJ8F3tD", + "type": "arrow", + "x": 128.61206957157697, + "y": 440.13062341766545, + "width": 316.63877589590845, + "height": 0.1213064482017785, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 20958471, + "version": 415, + "versionNonce": 1769312487, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 316.63877589590845, + 0.1213064482017785 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "GgiFj7vEEZT-VPfKiX6oo", + "focus": 0.030966433153118762, + "gap": 8.19887682255353 + }, + "endBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": -0.16596748618058757, + "gap": 13.386208975873956 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "6DaFskX4dI8KM57tYJKwz", + "type": "text", + "x": 151.09302660282708, + "y": 409.41115607163283, + "width": 259, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 221555433, + "version": 1091, + "versionNonce": 1058379017, + "isDeleted": false, + "boundElementIds": null, + "text": "7. Send the analysis result", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "zTrormMP-N-W6thSxlTgK", + "type": "arrow", + "x": 639.0015349036082, + "y": 455.7986607407429, + "width": 233.13104569613938, + "height": 0.2533468001336132, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1122927111, + "version": 471, + "versionNonce": 377543687, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 233.13104569613938, + 0.2533468001336132 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": 0.4608119841777207, + "gap": 11.145028555951967 + }, + "endBinding": { + "elementId": "Z5DzSXSTeTNfyo9GRr57B", + "focus": -0.42458037983475105, + "gap": 8.237658674598492 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "2sst3d11OgZLJZ3vdmH15", + "type": "text", + "x": 709.851318359375, + "y": 424.4541015625, + "width": 82, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1801175527, + "version": 1132, + "versionNonce": 865078249, + "isDeleted": false, + "boundElementIds": null, + "text": "8. Store", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "QsQ0GcuViNzFPK5QPICis", + "type": "text", + "x": 408.48883056640625, + "y": 561.8040618896484, + "width": 254, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 773954023, + "version": 992, + "versionNonce": 419481159, + "isDeleted": false, + "boundElementIds": null, + "text": "9. Detect security issues", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "fBva4zCGT2vIFPpTWC-oZ", + "type": "arrow", + "x": 448.39657694205636, + "y": 484.4299201560043, + "width": 322.1210694778896, + "height": 0.5052042161862573, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 184790569, + "version": 677, + "versionNonce": 151709097, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + -322.1210694778896, + -0.5052042161862573 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": -1.0131990709910756, + "gap": 10.240477501303019 + }, + "endBinding": { + "elementId": "GgiFj7vEEZT-VPfKiX6oo", + "focus": 0.8669472708483955, + "gap": 5.862314715143327 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "PALQlCWrOXdjqBkRKhS6D", + "type": "draw", + "x": 523.952392578125, + "y": 487.95794677734375, + "width": 72.486083984375, + "height": 62.78338623046875, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#15aabf", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 273802089, + "version": 310, + "versionNonce": 230581607, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + -23.2962646484375, + 36.8623046875 + ], + [ + -3.4830322265625, + 59.561767578125 + ], + [ + 29.205810546875, + 62.78338623046875 + ], + [ + 49.1898193359375, + 49.74090576171875 + ], + [ + 45.6522216796875, + 21.439453125 + ], + [ + 24.2618408203125, + 1.5714111328125 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "vP9i2PUtjapBX142LXX7m", + "type": "draw", + "x": 552.87939453125, + "y": 504.59832763671875, + "width": 16.5072021484375, + "height": 18.1295166015625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#15aabf", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 786227753, + "version": 48, + "versionNonce": 2144487561, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.4810791015625, + -18.1295166015625 + ], + [ + 16.5072021484375, + -13.106201171875 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "2XUWMZRUNlhKGsJGNjK9k", + "type": "text", + "x": 154.5665283203125, + "y": 450.849365234375, + "width": 258, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 826013511, + "version": 1180, + "versionNonce": 1404772487, + "isDeleted": false, + "boundElementIds": null, + "text": "10. Return the scan result", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + } +} \ No newline at end of file diff --git a/docs/imgs/excalidraw/fs.excalidraw b/docs/imgs/excalidraw/fs.excalidraw new file mode 100644 index 000000000000..bd97566531aa --- /dev/null +++ b/docs/imgs/excalidraw/fs.excalidraw @@ -0,0 +1,397 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "ribsikcWWSf8Aw4M6BOYJ", + "type": "rectangle", + "x": 528.53466796875, + "y": 377.9640197753906, + "width": 169.21945190429688, + "height": 104.2457275390625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 698826281, + "version": 274, + "versionNonce": 942385065, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "JinTKutXOSTzURP969rwo" + ] + }, + { + "id": "GkrbG--OvBT9zJ-w8E5oQ", + "type": "ellipse", + "x": 496.9409637451172, + "y": 63.6944580078125, + "width": 215.20677185058594, + "height": 152.14088439941406, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#fa5252", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 995070601, + "version": 595, + "versionNonce": 1870840679, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY" + ] + }, + { + "id": "KPLvdnBVoU3U5XGhCch-x", + "type": "text", + "x": 585.4711608886719, + "y": 418.1110534667969, + "width": 48, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 938497417, + "version": 119, + "versionNonce": 1368050313, + "isDeleted": false, + "boundElementIds": null, + "text": "Trivy", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "Ry65W-Cczzy8M9JsouEgZ", + "type": "text", + "x": 558.5760498046875, + "y": 114.79718017578125, + "width": 89, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1676815783, + "version": 368, + "versionNonce": 2034482823, + "isDeleted": false, + "boundElementIds": null, + "text": "GitHub\n(trivy-db)", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "eJn9MVaNlcyj8-YPfrBSY", + "type": "arrow", + "x": 608.6504180950899, + "y": 218.69285081443215, + "width": 0.45618097890837817, + "height": 154.16252445127046, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1741763111, + "version": 1069, + "versionNonce": 2093125993, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.45618097890837817, + 154.16252445127046 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "GkrbG--OvBT9zJ-w8E5oQ", + "focus": -0.035986229233252585, + "gap": 2.9120411440381986 + }, + "endBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": -0.04563780983822369, + "gap": 5.10864450968802 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "2P02jU3j2eEc92lH0YSwm", + "type": "text", + "x": 637.9371185302734, + "y": 248.28482055664062, + "width": 327, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 501371753, + "version": 360, + "versionNonce": 326653351, + "isDeleted": false, + "boundElementIds": null, + "text": "1. Download Trivy DB\n(including vulnerability information)", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "zdNQvzJczyD9GSExNdKS1", + "type": "draw", + "x": 513.9153137207031, + "y": 461.14288330078125, + "width": 72.64572143554688, + "height": 72.18890380859375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1270145927, + "version": 459, + "versionNonce": 1094761993, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + -39.526153564453125, + 16.35540771484375 + ], + [ + -32.219512939453125, + 72.170166015625 + ], + [ + 30.40386962890625, + 72.18890380859375 + ], + [ + 33.11956787109375, + 32.5743408203125 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "YK7xYiSKb1RwCCobsRAXm", + "type": "text", + "x": 391.5774230957031, + "y": 541.0142517089844, + "width": 280, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2027931817, + "version": 510, + "versionNonce": 1538345895, + "isDeleted": false, + "boundElementIds": null, + "text": "3. Traverse directories\nand look for necessary files", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "Z_DiM_TKY2bBa4Q5VEWgI", + "type": "text", + "x": 684.9671020507812, + "y": 621.1561279296875, + "width": 160, + "height": 25, + "angle": 0, + "strokeColor": "#1864ab", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1365660617, + "version": 564, + "versionNonce": 2113117703, + "isDeleted": false, + "boundElementIds": null, + "text": "Local Filesystem", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "kqEATPhet5tYxzkEOFZng", + "type": "text", + "x": 571.0751342773438, + "y": 494.0994567871094, + "width": 254, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 130557095, + "version": 654, + "versionNonce": 2037434313, + "isDeleted": false, + "boundElementIds": null, + "text": "4. Detect security issues", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "K0XdIaMYVmUP2kGc8Oe1O", + "type": "rectangle", + "x": 360.9377136230469, + "y": 357.07373046875, + "width": 495.45401000976557, + "height": 297.8450317382812, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1367853545, + "version": 339, + "versionNonce": 314224297, + "isDeleted": false, + "boundElementIds": null + }, + { + "id": "_vOsyHICDRWn01MF3g7rB", + "type": "draw", + "x": 539.7380981445312, + "y": 501.50762939453125, + "width": 16.976165771484375, + "height": 16.45367431640625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1322966281, + "version": 82, + "versionNonce": 244034663, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 6.905059814453125, + -16.45367431640625 + ], + [ + 16.976165771484375, + -5.1099853515625 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + } +} \ No newline at end of file diff --git a/docs/imgs/excalidraw/image.excalidraw b/docs/imgs/excalidraw/image.excalidraw new file mode 100644 index 000000000000..005aa44462a7 --- /dev/null +++ b/docs/imgs/excalidraw/image.excalidraw @@ -0,0 +1,504 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "ribsikcWWSf8Aw4M6BOYJ", + "type": "rectangle", + "x": 528.53466796875, + "y": 359.7196350097656, + "width": 169.21945190429688, + "height": 104.2457275390625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 698826281, + "version": 235, + "versionNonce": 865905065, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "JinTKutXOSTzURP969rwo" + ] + }, + { + "id": "GkrbG--OvBT9zJ-w8E5oQ", + "type": "ellipse", + "x": 382.4654998779297, + "y": 70.28388977050781, + "width": 215.20677185058594, + "height": 152.14088439941406, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#fa5252", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 995070601, + "version": 442, + "versionNonce": 1305668297, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY" + ] + }, + { + "id": "KPLvdnBVoU3U5XGhCch-x", + "type": "text", + "x": 585.4711608886719, + "y": 399.8666687011719, + "width": 48, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 938497417, + "version": 82, + "versionNonce": 463601353, + "isDeleted": false, + "boundElementIds": null, + "text": "Trivy", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "Ry65W-Cczzy8M9JsouEgZ", + "type": "text", + "x": 445.5017395019531, + "y": 121.72871398925781, + "width": 89, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1676815783, + "version": 306, + "versionNonce": 1721298503, + "isDeleted": false, + "boundElementIds": null, + "text": "GitHub\n(trivy-db)", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "eJn9MVaNlcyj8-YPfrBSY", + "type": "arrow", + "x": 497.981827043938, + "y": 226.98470679602065, + "width": 90.39040277767413, + "height": 127.08286800676547, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1741763111, + "version": 591, + "versionNonce": 1564262983, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 90.39040277767413, + 127.08286800676547 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "GkrbG--OvBT9zJ-w8E5oQ", + "focus": 0.41046776258752976, + "gap": 4.7596344319156 + }, + "endBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": 0.1341309277800711, + "gap": 5.652060206979513 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "2P02jU3j2eEc92lH0YSwm", + "type": "text", + "x": 181.22366333007812, + "y": 252.094970703125, + "width": 327, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 501371753, + "version": 281, + "versionNonce": 1175555431, + "isDeleted": false, + "boundElementIds": null, + "text": "1. Download Trivy DB\n(including vulnerability information)", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "P3WUTj2Q9se-JE7t3AUeq", + "type": "ellipse", + "x": 635.5376052856445, + "y": 68.77783966064453, + "width": 286.11024475097656, + "height": 152.14088439941406, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#228be6", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1633931305, + "version": 684, + "versionNonce": 1011691465, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "JinTKutXOSTzURP969rwo" + ] + }, + { + "id": "2q_e-oMWU2gv7ZEClixAx", + "type": "text", + "x": 680.7248458862305, + "y": 102.90502166748047, + "width": 186, + "height": 74, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 866083559, + "version": 676, + "versionNonce": 1669555559, + "isDeleted": false, + "boundElementIds": null, + "text": "Container Registry\nor\nContainer Engine", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 67 + }, + { + "id": "JinTKutXOSTzURP969rwo", + "type": "arrow", + "x": 725.3393330640303, + "y": 216.94283962355922, + "width": 69.9915382406898, + "height": 134.02688987715163, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1348821927, + "version": 938, + "versionNonce": 1580551495, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + -69.9915382406898, + 134.02688987715163 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "P3WUTj2Q9se-JE7t3AUeq", + "focus": 0.10510087993199528, + "gap": 1.4587528984692284 + }, + "endBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": 0.0931277668871816, + "gap": 8.74990550905477 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "kHnEt-AjbEzMxB61VSDc2", + "type": "text", + "x": 723.4168395996094, + "y": 258.063232421875, + "width": 295, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1080814281, + "version": 613, + "versionNonce": 1064224615, + "isDeleted": false, + "boundElementIds": null, + "text": "2. Pull missing layers in cache", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "zdNQvzJczyD9GSExNdKS1", + "type": "draw", + "x": 524.4000854492188, + "y": 435.25982666015625, + "width": 72.64572143554688, + "height": 72.18890380859375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1270145927, + "version": 331, + "versionNonce": 1090245193, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + -39.526153564453125, + 16.35540771484375 + ], + [ + -32.219512939453125, + 72.170166015625 + ], + [ + 30.40386962890625, + 72.18890380859375 + ], + [ + 33.11956787109375, + 32.5743408203125 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "13A9Y6sL_9DQ1KskiX5Oj", + "type": "draw", + "x": 545.8597412109375, + "y": 483.20318603515625, + "width": 25.76263427734375, + "height": 16.2581787109375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 788909033, + "version": 85, + "versionNonce": 991433415, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 13.24993896484375, + -16.2581787109375 + ], + [ + 25.76263427734375, + -5.01812744140625 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "YK7xYiSKb1RwCCobsRAXm", + "type": "text", + "x": 261.88555908203125, + "y": 379.65887451171875, + "width": 249, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2027931817, + "version": 255, + "versionNonce": 809279785, + "isDeleted": false, + "boundElementIds": null, + "text": "3. Analyze layers &\nStore informatin in cache", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "Z_DiM_TKY2bBa4Q5VEWgI", + "type": "text", + "x": 404.58673095703125, + "y": 522.828125, + "width": 144, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1365660617, + "version": 327, + "versionNonce": 2016944615, + "isDeleted": false, + "boundElementIds": null, + "text": "4. Apply layers", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "kqEATPhet5tYxzkEOFZng", + "type": "text", + "x": 598.8711547851562, + "y": 480.22222900390625, + "width": 257, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 130557095, + "version": 539, + "versionNonce": 562048487, + "isDeleted": false, + "boundElementIds": null, + "text": "5. Detect security issues", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + } +} \ No newline at end of file diff --git a/docs/imgs/excalidraw/overview.excalidraw b/docs/imgs/excalidraw/overview.excalidraw new file mode 100644 index 000000000000..2762654df7f2 --- /dev/null +++ b/docs/imgs/excalidraw/overview.excalidraw @@ -0,0 +1,466 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "type": "rectangle", + "version": 788, + "versionNonce": 555477386, + "isDeleted": false, + "id": "BkXuq_6BxgqZGZWc8oCtu", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 599.653076171875, + "y": 734.7542114257812, + "strokeColor": "#000000", + "backgroundColor": "#fd7e14", + "width": 1227.452155219184, + "height": 151.39703369140625, + "seed": 1632394695, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177570112, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 713, + "versionNonce": 44400470, + "isDeleted": false, + "id": "YQURTHNPSe05RPSlYRcok", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1118.2101508246528, + "y": 763.5906914605034, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 88, + "height": 45, + "seed": 891391049, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177702292, + "link": null, + "locked": false, + "fontSize": 36, + "fontFamily": 1, + "text": "Trivy", + "baseline": 32, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Trivy" + }, + { + "type": "text", + "version": 1191, + "versionNonce": 1166344150, + "isDeleted": false, + "id": "6dpF2EyZBtYgO6MrvGj0-", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 875.3033447265625, + "y": 820.7327100965712, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 612, + "height": 36, + "seed": 687997545, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177705177, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Vulnerability/Misconfiguration/Secret Scanner", + "baseline": 25, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Vulnerability/Misconfiguration/Secret Scanner" + }, + { + "type": "rectangle", + "version": 858, + "versionNonce": 1118008458, + "isDeleted": false, + "id": "cpnTMy7L2AUg9IDJppF4H", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 600.9835205078125, + "y": 635.5783640543619, + "strokeColor": "#000000", + "backgroundColor": "#fab005", + "width": 335.3091227213542, + "height": 82.36856587727866, + "seed": 77164935, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177872265, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1077, + "versionNonce": 1122201878, + "isDeleted": false, + "id": "9-blmNVtLesthMSY_f60t", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 649.8531494140625, + "y": 660.223378499349, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 224, + "height": 36, + "seed": 860091815, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177872265, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Container Image", + "baseline": 25, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Container Image" + }, + { + "type": "rectangle", + "version": 1118, + "versionNonce": 1679315786, + "isDeleted": false, + "id": "gugZxhi7ThlcjWY_MFO7q", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 954.3485412597656, + "y": 635.849225362142, + "strokeColor": "#000000", + "backgroundColor": "#be4bdb", + "width": 409.35879516601574, + "height": 82.97188822428383, + "seed": 1232790121, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177872265, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1300, + "versionNonce": 1187044950, + "isDeleted": false, + "id": "K48gtpesBxIGJxLTnI2CB", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1084.4311319986978, + "y": 660.9795074462891, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 139, + "height": 36, + "seed": 449264361, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177872265, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Filesystem", + "baseline": 25, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Filesystem" + }, + { + "type": "rectangle", + "version": 1204, + "versionNonce": 688085514, + "isDeleted": false, + "id": "La6f87LDZ0uEIZB947bXo", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1375.0136108398438, + "y": 636.5654322306316, + "strokeColor": "#000000", + "backgroundColor": "#12b886", + "width": 452.76554361979186, + "height": 80.08313496907543, + "seed": 2005637801, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177872265, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1432, + "versionNonce": 1593746326, + "isDeleted": false, + "id": "aOgRPVQ81jhOfkvzjWTMF", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1498.8465237087673, + "y": 658.0244835747612, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "width": 201, + "height": 36, + "seed": 1284472935, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177872265, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Git Repository", + "baseline": 25, + "textAlign": "center", + "verticalAlign": "top", + "containerId": null, + "originalText": "Git Repository" + }, + { + "type": "rectangle", + "version": 2792, + "versionNonce": 183831882, + "isDeleted": false, + "id": "10WjipxoLx2zzSI91pXbR", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 599.7894943723566, + "y": 905.6027750791251, + "strokeColor": "#000000", + "backgroundColor": "#fa5252", + "width": 344.482180700969, + "height": 83.67398764683533, + "seed": 1813731484, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177825759, + "link": null, + "locked": false + }, + { + "type": "rectangle", + "version": 2771, + "versionNonce": 617525398, + "isDeleted": false, + "id": "M7Cngti6H0_kawKRN8yJ6", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 963.2554264391833, + "y": 904.2447769132434, + "strokeColor": "#000000", + "backgroundColor": "#82c91e", + "width": 402.42137951281796, + "height": 86.03696372105414, + "seed": 1260603804, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177777585, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1057, + "versionNonce": 405881110, + "isDeleted": false, + "id": "Iq57wFRtO1a8AU0rT6lRD", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1046.152429428344, + "y": 930.8676815998951, + "strokeColor": "#000000", + "backgroundColor": "#82c91e", + "width": 218, + "height": 36, + "seed": 1329695396, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177655817, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Misconfiguration", + "baseline": 25, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Misconfiguration" + }, + { + "type": "text", + "version": 883, + "versionNonce": 969949898, + "isDeleted": false, + "id": "_cm6xpfcL9Yv2XBK5MBZF", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 681.3134368986982, + "y": 931.5212932384402, + "strokeColor": "#000000", + "backgroundColor": "#82c91e", + "width": 161, + "height": 36, + "seed": 807441828, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177624726, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Vulnerability", + "baseline": 25, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Vulnerability" + }, + { + "type": "rectangle", + "version": 2874, + "versionNonce": 1934391254, + "isDeleted": false, + "id": "Fq7meULupm1A9leboPlko", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1389.3043677318824, + "y": 903.8533384764222, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef5", + "width": 437.15079032010976, + "height": 84.42746665074158, + "seed": 230693534, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177785481, + "link": null, + "locked": false + }, + { + "type": "text", + "version": 1121, + "versionNonce": 110517002, + "isDeleted": false, + "id": "OUGk8nZzvgcKUHhKUcQov", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "angle": 0, + "x": 1556.0451356485157, + "y": 930.8040952304675, + "strokeColor": "#000000", + "backgroundColor": "#82c91e", + "width": 91, + "height": 36, + "seed": 2044527454, + "groupIds": [], + "strokeSharpness": "sharp", + "boundElements": [], + "updated": 1652177636085, + "link": null, + "locked": false, + "fontSize": 28, + "fontFamily": 1, + "text": "Secret", + "baseline": 25, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": null, + "originalText": "Secret" + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + }, + "files": {} +} \ No newline at end of file diff --git a/docs/imgs/excalidraw/repo.excalidraw b/docs/imgs/excalidraw/repo.excalidraw new file mode 100644 index 000000000000..72fcf1b47905 --- /dev/null +++ b/docs/imgs/excalidraw/repo.excalidraw @@ -0,0 +1,631 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "ribsikcWWSf8Aw4M6BOYJ", + "type": "rectangle", + "x": 458.6370544433594, + "y": 379.5105285644531, + "width": 169.21945190429688, + "height": 104.2457275390625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 698826281, + "version": 360, + "versionNonce": 899087049, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "JinTKutXOSTzURP969rwo", + "nXkZQsrtjmNqPM6SmPxrc" + ] + }, + { + "id": "GkrbG--OvBT9zJ-w8E5oQ", + "type": "ellipse", + "x": 427.04335021972656, + "y": 65.240966796875, + "width": 215.20677185058594, + "height": 152.14088439941406, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#fa5252", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 995070601, + "version": 679, + "versionNonce": 1333899847, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY" + ] + }, + { + "id": "KPLvdnBVoU3U5XGhCch-x", + "type": "text", + "x": 515.5735473632812, + "y": 419.6575622558594, + "width": 48, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 938497417, + "version": 202, + "versionNonce": 677296553, + "isDeleted": false, + "boundElementIds": null, + "text": "Trivy", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "Ry65W-Cczzy8M9JsouEgZ", + "type": "text", + "x": 488.6784362792969, + "y": 116.34368896484375, + "width": 89, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1676815783, + "version": 451, + "versionNonce": 490852711, + "isDeleted": false, + "boundElementIds": null, + "text": "GitHub\n(trivy-db)", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "eJn9MVaNlcyj8-YPfrBSY", + "type": "arrow", + "x": 538.7528045696993, + "y": 220.23935960349465, + "width": 0.45618097890837817, + "height": 154.16252445127046, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1741763111, + "version": 1292, + "versionNonce": 764124297, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.45618097890837817, + 154.16252445127046 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "GkrbG--OvBT9zJ-w8E5oQ", + "focus": -0.03598869040285281, + "gap": 2.911871895302724 + }, + "endBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": -0.04563780983822428, + "gap": 5.10864450968802 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "2P02jU3j2eEc92lH0YSwm", + "type": "text", + "x": 190.61294555664062, + "y": 255.17030334472656, + "width": 327, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 501371753, + "version": 569, + "versionNonce": 2012286087, + "isDeleted": false, + "boundElementIds": null, + "text": "1. Download Trivy DB\n(including vulnerability information)", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "YK7xYiSKb1RwCCobsRAXm", + "type": "text", + "x": 588.7474975585938, + "y": 488.1255798339844, + "width": 280, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 2027931817, + "version": 628, + "versionNonce": 30461609, + "isDeleted": false, + "boundElementIds": null, + "text": "3. Traverse directories\nand look for necessary files", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "Z_DiM_TKY2bBa4Q5VEWgI", + "type": "text", + "x": 877.162353515625, + "y": 618.5094604492188, + "width": 160, + "height": 25, + "angle": 0, + "strokeColor": "#1864ab", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1365660617, + "version": 647, + "versionNonce": 760156423, + "isDeleted": false, + "boundElementIds": null, + "text": "Local Filesystem", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + }, + { + "id": "K0XdIaMYVmUP2kGc8Oe1O", + "type": "rectangle", + "x": 360.9377136230469, + "y": 357.07373046875, + "width": 695.5669860839844, + "height": 297.8450317382812, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1367853545, + "version": 417, + "versionNonce": 1598611913, + "isDeleted": false, + "boundElementIds": null + }, + { + "id": "9cBakj4Z-FKXwYYdweyW1", + "type": "ellipse", + "x": 774.155647277832, + "y": 56.2083175778389, + "width": 215.20677185058594, + "height": 152.14088439941406, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef5", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1010956009, + "version": 673, + "versionNonce": 825999529, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "vGa683rpZ9AztfvrVvEyD" + ] + }, + { + "id": "5y2AKGYkXhCldwQOecwl6", + "type": "text", + "x": 806.7732543945312, + "y": 106.18511658906937, + "width": 147, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1289267591, + "version": 83, + "versionNonce": 1127203721, + "isDeleted": false, + "boundElementIds": null, + "text": "Remote\nGit Repository", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "vGa683rpZ9AztfvrVvEyD", + "type": "arrow", + "x": 884.4609964246083, + "y": 220.86864013003574, + "width": 0.45618097890837817, + "height": 154.16252445127046, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1762737031, + "version": 1200, + "versionNonce": 457627015, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 0.45618097890837817, + 154.16252445127046 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "9cBakj4Z-FKXwYYdweyW1", + "focus": -0.022674122391029432, + "gap": 12.541597764893623 + }, + "endBinding": { + "elementId": "4_okkFmweGK_2DBTmRp4i", + "focus": 0.047202684587572305, + "gap": 3.295263653270979 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "4_okkFmweGK_2DBTmRp4i", + "type": "rectangle", + "x": 796.4703521728516, + "y": 378.3264282345772, + "width": 169.21945190429688, + "height": 104.2457275390625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "#4c6ef5", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 853231143, + "version": 387, + "versionNonce": 1096837737, + "isDeleted": false, + "boundElementIds": [ + "eJn9MVaNlcyj8-YPfrBSY", + "JinTKutXOSTzURP969rwo", + "vGa683rpZ9AztfvrVvEyD", + "nXkZQsrtjmNqPM6SmPxrc" + ] + }, + { + "id": "zvgwnUmSms_XcbMDespIP", + "type": "text", + "x": 847.181640625, + "y": 404.5675476193428, + "width": 72, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1769283399, + "version": 307, + "versionNonce": 1392544935, + "isDeleted": false, + "boundElementIds": null, + "text": "Git\nProject", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "center", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "f0kayc0oXZazxPJKizaM1", + "type": "text", + "x": 916.8968811035156, + "y": 253.85886842012405, + "width": 258, + "height": 50, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 49135625, + "version": 649, + "versionNonce": 1150891337, + "isDeleted": false, + "boundElementIds": null, + "text": "2. Clone Git Repository\n to local temp directory", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 43 + }, + { + "id": "nXkZQsrtjmNqPM6SmPxrc", + "type": "arrow", + "x": 640.9059448242188, + "y": 429.34260255098343, + "width": 146.4581298828125, + "height": 0.93572998046875, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 2075020231, + "version": 93, + "versionNonce": 1421238215, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 146.4581298828125, + -0.93572998046875 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "ribsikcWWSf8Aw4M6BOYJ", + "focus": -0.031650787945314215, + "gap": 13.0494384765625 + }, + "endBinding": { + "elementId": "4_okkFmweGK_2DBTmRp4i", + "focus": 0.050151997400131744, + "gap": 9.106277465820312 + }, + "startArrowhead": null, + "endArrowhead": "arrow" + }, + { + "id": "OPRFQTrsfmF5a7us-mxkI", + "type": "draw", + "x": 446.98912048339844, + "y": 467.28928834199905, + "width": 72.64572143554688, + "height": 72.18890380859375, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 1541066697, + "version": 504, + "versionNonce": 1115999975, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + -39.526153564453125, + 16.35540771484375 + ], + [ + -32.219512939453125, + 72.170166015625 + ], + [ + 30.40386962890625, + 72.18890380859375 + ], + [ + 33.11956787109375, + 32.5743408203125 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "QzKnYI7o5Yxg_7szebL6Z", + "type": "draw", + "x": 472.81190490722656, + "y": 507.65403443574905, + "width": 16.976165771484375, + "height": 16.45367431640625, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "round", + "seed": 2144447815, + "version": 127, + "versionNonce": 807483145, + "isDeleted": false, + "boundElementIds": null, + "points": [ + [ + 0, + 0 + ], + [ + 6.905059814453125, + -16.45367431640625 + ], + [ + 16.976165771484375, + -5.1099853515625 + ] + ], + "lastCommittedPoint": null, + "startBinding": null, + "endBinding": null, + "startArrowhead": null, + "endArrowhead": null + }, + { + "id": "qqbEan2u1uU3loTdTvQ4B", + "type": "text", + "x": 376.983642578125, + "y": 561.5254028439522, + "width": 254, + "height": 25, + "angle": 0, + "strokeColor": "#000000", + "backgroundColor": "transparent", + "fillStyle": "hachure", + "strokeWidth": 1, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "strokeSharpness": "sharp", + "seed": 1619652615, + "version": 718, + "versionNonce": 1190681095, + "isDeleted": false, + "boundElementIds": null, + "text": "4. Detect security issues", + "fontSize": 20, + "fontFamily": 1, + "textAlign": "left", + "verticalAlign": "top", + "baseline": 18 + } + ], + "appState": { + "gridSize": null, + "viewBackgroundColor": "#ffffff" + } +} \ No newline at end of file diff --git a/docs/imgs/fs.png b/docs/imgs/fs.png new file mode 100644 index 000000000000..2a6acb83d058 Binary files /dev/null and b/docs/imgs/fs.png differ diff --git a/docs/imgs/gitlab-codequality.png b/docs/imgs/gitlab-codequality.png new file mode 100644 index 000000000000..b7069b8af7c1 Binary files /dev/null and b/docs/imgs/gitlab-codequality.png differ diff --git a/docs/imgs/image.png b/docs/imgs/image.png new file mode 100644 index 000000000000..e014f0c7b653 Binary files /dev/null and b/docs/imgs/image.png differ diff --git a/docs/imgs/logo-horizontal.svg b/docs/imgs/logo-horizontal.svg new file mode 100644 index 000000000000..9cdd1b594dd9 --- /dev/null +++ b/docs/imgs/logo-horizontal.svg @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/imgs/logo-white.svg b/docs/imgs/logo-white.svg new file mode 100644 index 000000000000..f546d23b3358 --- /dev/null +++ b/docs/imgs/logo-white.svg @@ -0,0 +1,3124 @@ + + + + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + o2fP3CYLc0HTkrm0lY1xu0zMha0ey7rdZNm2T5Y96/OLnNmM862zcbx3fycRz5bhW6t5Hvdqo06v +J7I07En0+z+PgN5I89Au6iz0QpqCHefwrr/YoWlf6u4yGq8bDWQJ2H8s+X7P5F6to8hHG4Ea9iZW +xJ80anj3OPL9nsa8G+db93HAc3YN4ZsnyhS8n1gXccB0P4ZPjXaBs8toAOPqpVPvncCDoz00SWjb +BNr5mT65WqZvrXZJ81uXM+4222bG1RHf2TWEcj1Gj22O2WujY/LcZhk+uJmHUc/3UOb1lzk0mXdZ +m13ryjJ5bH2GD87L4LnVMXhr9Awf24eRS6uzal926c5cl7V5KxPIRuso4vmbwjuap3HP+zzy2TuI +ebeOuK4ro9fmaf7e7CDKP7uo8vATYRLaS6Xfe+iS8N8Y3v2awLhvU0jnawTnuk0hndcB49lMpN+b +ysSkXVR5aNMAvnmbwblf5DnodxD1Pgxe2owDnrOJLgftpc/wzQQK7hptFtpKn987qRNs90Dq2TiH +dfYOop59853zM31w9YwfXF20ediVQL3dIUnBblBk4I3zrbNlANtmF7hedsaBw4vezZ0hdKOzbr2M +azFo4BsmfPEtBg38LgMzKwuD+TvTzoTZuDB7atts3B/jaWMduJk2LCaQbcwmMY5rA6aTX+7QymL0 +2LTZt5uM97ENnAyDBl7mdYHXYBiyclyxLF5XzEVuFuMdwwauAcIEPub1lcWzyWL61rQyhG1cmcI2 +7oxh2yxzyLZ1eeNq+Q8D49oLGLgP0+DSNtbBRa1MzAVNHsum+Q5ZN5os60aTuaxtO3C6wwbewcIX +pzdosNlQ52xpaVVhMTlA20O7wITFRMVUxcRVxVTFhMXERcVE9VYzUTFZaTFxaVUx3VRSTFRMVEw/ +R6xmCusdLLCpwfWxXWiQk6FmqmJKEJWTx7anh6Z1cDeVdcWBiSpLC2uqC0trSyqHjokqS2uLqUpr +aw+NjGmt7itHzmnL4UB1NUWFhYWVdTXV1WWllTXVZdVlRVdlZWVlZbWVpUWVtXVVxTWFRdXVxTWF +lUW1lcV1NXVFxXVFtXV1hUVlpTV11aWVZVVF1aVldUU1ZVVFZWXVpaXFRdXVhZXFYYtrK2uLS4uL +qivryoqLiotLy2pqa4tqa2urisqq6upqC+sKq0vrampLyyrrqutqi4PVllbXltXVlpaVllXW1ZWW +FtcVlawgpy3HQlXWlRaWlpVWV1dbjoWrqa2tKq0qrS0urakqrSu1rS0rLjqrC0xVfeuKi2pra4vr +qmqramsrq6tqy2prq6pqi2uL6kprympra8pqi2tqy2pra2vrSmpri2oKa2sLawvrqiprKyvrSuuK +qwsLq6tra0pLa2tra2uryuqKqoprC2tqa2vr6iqLSsvKasuKi2pqa2sLqwurampLK+sKa6tKS2oq +a0tqKitrSwtra+tqS+tqa2vrSmpLa4pqq2pKq2uqS6tLq2uKawuLaypLS6ura0pLS2uLamtKS0tL +S2srS6urKitLa2trKktLayoLCyvrCktrS2tK60rLauqKqmrqSkury8rKwpXVVVZXF9aV1haV1taV +1JWWlZbWlFWWlZVWlpXWFpcWVhXWlJXWlpWWVhYX1RUXFteWFZdWVpXVlhaVFpVWVldWV1dWVlYW +lVUWF1VXVhYXV1ZW1dVWVtVU1laWFVZW1pRWltZV1lbWlFZWFxZXVlfXVBbX1VRW1lYW19YUVpbW +FFYWVlZW1tbVVZdVlpZVVtdWFtcVV1ZWlVYXVxfXVhVX1hYXFtYWldUV1hWW1taVFJZVFlYVVtZU +FdZWVhUWFlYVVhYVFgcqrCuuKSqsriyuKyysqSyuq62tqquqrK4qLq0urKktLSqtrGKYplLmIvWw +BIA08EkClndUxpJ9TJiAKgrZ5EQEcopp6AMHphdHDU1vjgI0D3vA2ETssYpZ6KOG5mGPFpyJOFg3 +xTtSNcM5PmiKb6BuKuoIBZUksugpIgpcJJQqtmKV2B/essRDUrnyK2NkMQscB0BbN06ALMM4FkPS +llAvnAL+KLxcnkngZVsNONOPEw96YeJAfIjn8B1ew214DcfhNzyHB8EQcSchJn6AyABvwOTE4bQU +oJB4gjkYrwDLQkCKDVQJeABj4iEZJmTisAOPaiCYDwhDDVMB8KBhbKwEDBFLAeTQ8BJLSr8mSSr/ +Yo14Xi4hBF/BQv8FQIqs+eJKWiejD0zPRR6gmog7QmiCdYzwVNRBKlo5BHE0cgjjpyJsZydiENJM +Qx4vLgdhb2Aa9tCBaagDh6dYh6smDylndwjo8GtjHTOfAaZw/ZhIhwfjGBqChDRW4HdkWXxCggQc +C8kAn/ph4lA0SJwJhogbZXm40EsOL+I/vIgfcaQeKE5DcPCC2M2QJbQ38F+hAn8yasCNaBRwIc7E +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 +oCQNjTQGKKDGwTHUplKAo/rmQxQFZxKjtfVodPYNVRHDuD6UOTwAXRJZwGhp4Po9h4XVbx/+MDYH +ALfdu9+4/5GZnrSiMBQYXHduBF3M7Do6NEWCeFO+dNSWpqgV80xD7ypIj4tqyRjoUu7ANXodg6bZ +YG8XQxtngXzFdHpzKsAn0NBxFOQ1vQBhylz7zOSbojA2BlOy6GANJPqgkwGWUbrtKp6np4oarXIw +SUE8gpC9YNaIOdYHclFoGNP1ZyAodrNJpuK3qTZhmpfxSTD26e9dlnDLuPf/ia8Qrf7WNUFruosu +cER5x5K8JwhODqCNGLGUQJWG3ykKei73iMJwYsvqoXDWrIkHDmSa0lfjWJA27OL8kOxEHFgYmK// +8air3g2id4SBDBaIG95ECHoUJTDvjz+9DgkJY9YIRS8UgoiKfuZIyJNH2CQEV7AQsKQCvRc/znVk +kfTc7csxZBDUZYUhxK5oAWrx6xQwLVxF3q4SxCP9u8nBa+Qvut1HHrap7h8fwvSQLrK5ZDvsO2OK +Wiqmpo+9zizEMSMImjMhShf6tHtwSMwUh7Isb9tVxfaWLHH0wgjqdulhmHEAay+RLofs/r7mB6tq +4gQ+sdQFcjwEj3DcZ8ybgjV2JV4smKN4INfIkbyRBwAA7Ad8Z3gHZ38H/3CKQaZf3r3e9ZcYqPut +vFZ86LKrF074/WYWY05TxvO4IYhg1sijSbsMVDU7dil7wi7qDeDDNOwJAHQKqDJhZGM5MDU1MC1l +Y2UwLTQxMzctOTFmNC0xN2M0MTVmZWI2ZWVkNTliODUwZC01NjBlLTQzNjgtYmE3NS1iODYyZTBm +YmQ3NDQgNjM4LjI1MjM3MjNkZTBiNTAtYWQ2MS00NzVmLWI0MTYtNDk3NDMwYmExZjgwOTI0OWEz +OTUtNDBiYi00YjVhLTkxZDMtYmMzM2NhNDlhMGUyMy40NjEyNTUwMTMuIEEFyyEiPK/2b5795nSQ +BlCtZhpYq5HmeaC5I+e3mao5IrTNqQTfThDoBqQNOeCvwLpQOYMQgljDWOean5oxmIYH3NoN2gWy +JmrHSno0ph88+OW3c/SY4ADufA/sOhk27tQGjjVXgOc3pyk8mDWj8d9EBOoGFqYB8gMLp2HQf3PP +ES6BCPTmG1hLj9b0g5cK0QU2dIwAxDttMTgzZTQzZjg0LTRmY2QtNDQ1Mi04ZTNmLThkYTE3ZWNi +NjIzODJlZGI4NjVkLTg1M2MtNDk4YS1hZjMyLWE2NzZlMWUzOWZlNm1sMTBfU1ZHRmlsdGVyDS8g +Og0vWE1MTm9kZSA6DShmeG1sbm9kZS1ub2RlbmFtdmFsdTEgL0ludHR5cC9BcnJheWVUdXJidWxl +bmM7Y2hpbGRyZW4vcmVzdWx0KHR1cmIyYXR0cmlidXRlOyAsc3RpdGNoVGlsZXMobm9TYmFzZUZy +ZXF1ZW5jeTAuMDVudW1PY3RhdjJ0ZmVDb21wb3NpdG9wZXJhdG9yKGluaW5Tb3VyY2VHcmFwaGlj +eDAleGgxMHl3d2lkKUFJX19pZG9iamVjdC9EZWYgOzRmcmFjdGFsTm9pczRHYXVzc2lhbkJsdTFi +MmREZXZpZmVPZmZzZW9kZGRTcGVjdWxhckxpZ2h0aW5nUG9pbnRMLTUwMC16LTJ6c3BlY091c3Vy +ZmFjZXR5bGwtY29sb3I6d2hFeHBvbmVudCgxQ29uc3RhbGl0UGFpYXJpdGhtZXRrazRrM2szMTAx +MjEyTWVyZ05vZC0yMTR5NEJldmVsU2hhZG93TW9ycGhvbG9neWFkaWxhcmFkaXUxLmJibmItZG5i +Mm41RGlzcGxhY2VtZW50TWFwKGJuczN4Q2hhbm5lbFNlbGVjUnlBQ29sb3JNYXRyaTQ0MW1hbmlt +YWNjdW11KG5vbmNhbGNNKGxpbmVhZDVmcm9tdG81dG9yZXN0YXJhbHdheWZpbGxmcmVlemVOZGRp +dHJlYmVnMHM1NTRuY2M4Y2NjY2NjYzhjY2MxY2NjbmIoLTU0MUNvb2xCRF82NmVyRXJvZDY2NF8o +N3JlcGVhdEQoaW5kZWZpbnNwbGkxcmVtb3ZSMSAxOzIwIDE1OzIwMCAyMDA7IDE1IDIwOzEgMSBj +blBpeGVsUGxheTUwIDU7MjAgMjA7RGlmZnVzZTV5ZWxsb3c7Z3JlZW47Ymx1ZTtpbmRpZ287dmlv +bGV0O3JlZDtvcmFuRGlhemltdWVsZXY2ZDExbDFyZWQ1MDEwMTAxMjJucmVkNjg4LTEzNDIwLjAu +MTVudGE4eDU0ZGRvbkZsb29mbG9vZGJsYWNrOyBvcGFjaXR5OnNDbjM1bjFuMDEwMUdyYXkoME94 +LUNvbXBCbHVyVDFuZW50VHJhbnNmRnVuY3RhYmxlVjJGdW5jRy43IDBCMUNvbXBYZmVyRmlyZUEt +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 +yoYSYWyu/bml55/3q1drZU967z91a298mv+p36cST3pv41sf9/QqrRW9Wvzu9srb1/btee2N9336 +W0rrfSqr7TstlfZ6zu/Z1htxvZS6W+yXfq799KesPq3nrrdnz27aM/u8uFaLZb3Y2vpNcX3s9Nqv +Lqm999/OpvhK/JTOt1i6Z4lL2CJh062X0306FRK7ocRtG4ybsSlsUiWn9XO/19u1Ttw/s8+Z7Xe0 +Xj3TOT3T+dPeOyfG7/Lb4n5vb/r/OMvrf2HZljCY+FQIQ40+0WODNK5B6Am/CyhPXEACQBXlUPo8 +ksbE5XKII230eSKtE3oFKrHBLUaEubMdZ/fOVOkz7Y2Ztr1WnJ9v7m7b104rvWNj1IUpDFvRypst +m2u7xBZVwtg6dVFYxqirukVhLa9FXfZWijOV1sJKVdro1U2X2KcBvJHJ+zJzUgy99sZ5LaZ+3YrT +rej3NmUR26c77XyiU9knZpe0Unc8661z1kprpZROCouzUqXu/+7/xtSKl/Y/VZp39pzus7tpd8/p +Ptv7WrHndJ+2r511zlox184PrdKttx/pR2tZirHttForYivSbN1OK1KLwqJbkVbbdlZKq62YKt1W +Sa14q72RUq9uRbditZXCYrWitaxYKYtIrb3RRnspLForWstaF8u2k9b67pfW29k2nRfjx3nmSRvX +tvn+W6+Sjjht22hhlnGPc0ZMo/XGqCtaCrOyW6X8lkVh2aOVtxStx/xUJvYV/y0bf8Z/6or/0b4V +/+NP6Vb8+fktCrOokv0ffdkVw22kfdeM2GPbSOtIKNJIn1bdKcWOrbVivyi2WxYX2y/3imXH7NRG +A4XBxKZITTEiTLy4vhW/9InXWvHfik6FYKLHzlZsp0IYp9fG9rasditeK/q0nF6bc5Y5d2zPH31S +pXijTyoTm0RduS/NFBavFT/+jDV6T4y6onvEb8Wcs2x/mePPeHHG/ZZFYfipUnS38FMhsdupU8On +LfBAnReGJ0z4eSYGmtH2cUK6beM8GVRRT19WbPP/a4y2E0fjQlmnmd6gClTheqdnK5vC/DaW7hSW +JbVKLm2sT2UfTPvSGe3n2HndZRKd6kAVqAJVoApUgSpQBapAFagCVaAKVIEqUAWqQBWoAlWgCgNV +oIoqgSpLjT6xY1MbFIm65sz9nmVX6RTW1BWfzpb1Y63Us7TR5pY02r+19sunLvoR1w== + + + a+O0KIy6flo+rfrWpC2xW9e95XULXytSi7rifKvSOuV9q8ppu+WlRNVl9VinFa1Tor6e+1rZk8I8 +Y2tFmu+UXq+kluxpac4yP4VFay1qRZy/WtHWWSW1dWaKUdfPWiX9+ZhK6j+zpE6JfHZu2Y4prFJY +tPOvdLcsLKsV/eW/FbOVZbtFierPt6xb863rlpyfabfst96isHixW5mtOC2lsr71VLYV8bQidesW +VaKyT/y+1uXMjTHOt+df6qSF2+ctyDZxNLpMKM4LlRpQn7hUo05TeQH+siudsy3F1Ge2uNbG1t6v +ddr2iafbjCv+mfPEtun8Seekd15v+nY+dgsnWuv0J/bum3H+vtUpre304pmpZ3rrbFp95rYV51sp +rbmdTkzbsXv3pG5tZ0vttH2vWzypvRNP2TRX+28tmOjXwokT//RJ37bFNTv+TOvjiW/TS7s6bTtz +nfXxvG0vtmBizhZOrD5l56+12m87H9OMf17aN7eV7RbTpn97VtkxX3qd0q7uFkyk0y2cOKvTKmtt +bDHt6rP+rX3dfWZbL85z4pw74n/6T2vOc16Mc+c87cVPH+N//znnxHTijO2d/W/n4745v1fabm32 +jC/+ip22vbbmPOdn92zBxLZwIq4+61vc1OfFczbuO7N998du3WmlmE7qtfp0bNu7vT6dPqn/9KYY +27fU7eeMsb1N53QLJlZq4cRvl33zxD8rxvi/2+K2+OettSvF2fPnWpvWSm+3WzCxP/fTmSnu/plx +u/1ZLaUWTrT0uuzYten0TNv+bX8672w8K7VZNr233nmz54yxWzBxuv23YKJft3Qmt7oFE7O1862l +95v67HwtxV7/Vir71kq9540Xvzud+O/3Z3rl/fn21mrBRNqNc1PZ8T6tbX++xXXm+e24G9OeGP/s +nC19t9/ubZ9ar9Pefvu3qcWSXgsmOq7z+yXurvbmLCdt/+x5WkuvtWAizm7hxKZ+J+5uWut075mz +7evWM664zu5K/7q722o9+317MXY551s48X/mLPHfd/wZY59Oc7Vgor1ecU8rO1771tb6F+dbqa2W +VkxnTwsmXuoWTrz0LZ1Jlte7cfv9b4uz7Jj98e3uad9O/+6WnS2c6PT63/737qaztsUZ03/P+cqO +dlowEb+FE+/8Ot8942x/fleMu5t+xu10fvZ77cWyv+u3zd+VWjAxTwsnzuyWzqRpvvg//7Utu2Ja +MX18P9Nr570333/8d17P2HoWdlpIZlGrY9nxbWe37fdPHObSn9X2V3zxxDhrcFlLq308+9Z3iS1u +7LXTtI/zn91xtW87/tan9dKet9aaH7vtkdaJGE5nMqGyjwATEQZFBeATwtDCUOMyjFGnAeWJS0Vd +IAi5U0bSMh6hKdY4gMdx4qeFp4v511brjb9eSm21+D97z/k504pztt65PUuvFnv+KaXH9ogU2oKN +Avw0jqh9HknDxFGXmRhoovWOC22ZiROB2UVFW/CBIlFmCsDniYS+bFQpBwzmwdDAUzR5X2bWfwwm +eqay47/Fb6fsSwiSvPt1yrbCLkFp40oiYYYhEmYYEV9cb9uufnG22TruSy+VjbHX3JnWOy+mtlbZ +noa2YAszTuuEdJdMOUSGSRQQ40MkaZ0EoQtOhXKIDPXLOEBQ6y7AUaeZtMynhVop2zKdvSMSsklE +reR9Itetc+L6ld6mjefLjvT+u/+7rZ8ptbZttZjGenPbiaetU3aVPWXn+tUbv//tmbH/pXPKrthO +Kme19XFnenu254krtvPil/PzvD49z8fUXvr/9NavlzqutOIrP9N657Tu/dQ7R5rttfavtV7ppD6/ +5pvne32cKXX79e+3ndNvV6/WI0X0rxdfi6/PS7+zv51OnepoBaJ24jDulgyTLYANoPkCoCEy7ePc +wRvcdbp0UybBT+s+74q3QXi6mIw2sJsIN41KZAhFGomkidqCDYvctqlEBtOWSY+d13XeFp5EGgNN +haIEjDqNw4xGF522QKOBG5RDZDBFBNR6tdKzrfZK94gdLzhu23Dctgm306jzrvDLMCqREhAnkYoP +le+Ce23E+0KPaAMzzIgoCz1caAM3jkrstodY0RZsoUgrZQUqkcHDAT3EbgCvq6hQYO4Zp61RwyYU +g8eOHSuVjh0rN1biTyiGCcUACjNuA7Idq+cCNfiy0IZpXAhuHwY0cYkzVtlxzio70tgTimFCMbyL +gDl2jjXMuE3tOLs9+oRimFAyMlRKmeaJnyYSZhoWK9oppPM0IrgQZpcIePowI24TdRpGtCKXeaIv +E7liRQQUEZ08jTDjNlGGpljbJ5QVGSqj7uR1ogxNccZKZPi00XcRPy0k6zKLKAKePkykLrot5MRh +YM+1OohZGKUGiNRSDT6yQczCKo1V3ovr9JxlXz62Tq/XOp3df7GVTXXAk8gjgTLtu4QMNI2/bi+u +jnHXt0p0zqazJ52z6bxyLl3M7a6y4vvzZ7bqW9VfUgqT7Z+tKu3Tx7VaOd+K86WHstD6Mkwo0/iu +V85Y6Vt5RXfiv95PleqV9EXf1lrpjykRzVRWt7CFffTO+OIqvSTbLSzSx06VppxWxF8xiVGnStJa +iyp1pNhW6ZRoOoXlnKX9t+hL9+i00o/Z2pY2v6zTstOisLZWdiu/dR2r8n/K+/Hzv/TZEjt1ucSy +rSk90kxd9qzyvb39y67oidRM85SXxsf5ykrzuzWhLrez/+Wdcj52ianrfVbs1PXWmlb0SV1uTY9e +5fvMuGWmrvkuaZyz6ZTU5cdvizbO1FZqRZytl9RaS+W0Ys2dXb7lVGluqMtt/Sux5d8v34o00nov +9f+XN0q38sq39nJZY77RShqf2q+y5fXuOWX2SGt9Sl2R0n+ZpfWZ5f2uVVqqlPPFLuu9n+MGCaP1 +RlpffvQ7e94qH1RtcKAKVHGzX16n1sqbva+8L6c7fnldvlvTVpldTite2vPe+BbFRZxxfeoup+XX +nQYODmIsYBDgEPAsKhXdKUFHJNjgOMIByME/FiCCB6lmwGZD4+PiIjnAIV1fR3vjBIbuiqqEOCIu +GC5JHUDCpqwTVEGSs4+CxLpdJancrpKAmDgUCfaxcJCATI7TyRBImCxI+nbVdzMJEo8RyEFSURMH +GRSJgQQiDhKvHxYkDhdFQdJwu0oS3kyCxOsowCC5F0VB0hdFQUKZoAoSAyKQg+Ri8w6SAwsVS/IM +3UBBAJHoEkWFoFgeWum3aEuS+Vg4SCrK83CiSCQJhEFiyhywJAUWKpYEQoooEoSJAYrksnkHSSij +syQl8L8PxEKMTckQQqH4DrcZaQwzNQ/ejL9wQOpfQBCCxQXiQihQsfDAEK8oi48CDAKVBQXM9Egm +KjICYBYUzQnzaRmEBgtBwSAVghJBASIxQ5EISkbCBKkk9NngyA8PUcUKGaDpivUQ3sBoEpG5UCgA +gjkgLwE0f0BCgAESFazIijJ5KgSFocNS6Q0TMki4kY1BUkcgB4nmg8NBgkkgDBKPFFEk2MKWZOKD +w0FCv49BImI7Fso/ZAygb0GpNILJ6S8X9WPiOb6Lke4QRhTXNIB0KggkLPKKKkkkI33isAcCDdmm +MDc6MAvFglQIyqinANfbgFgDDJSsAYsnxswECRIZgoBHYdQQIBcRdCRTISiZDoVRL1hQKl0JcURQ +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 +bnGL2wRlixvNY3XcJ+Cg4xA+b+HmQYbHPWjILBwFlpkkIFAwWoIicDdBShV9ij1KYNw2FkCmb2Zn +Fj4qFcg0j4qUaUqeSmQi0ET1N4UvxqHi5n2Eoi+ekBYAllHqKB+haIZWJm6eS9y6ipD0he5GQP6g +aEbhw2ncs6lxMQsYtwsZpgSKKg+ibhmBAJZMkc0UZdXkMTJW+mJPIWoOxHowlYCAAE4ECiKVT8O3 +mQIFkUrcOFSm4zaRXDgzioeRDIDTIltFEHC0Twfh8QCrF/ipvDDCD+g9VKGWMVSMRUAiwIICCFFB +yFxIT6MJEYWHBlilfVLWirguTq0l8Y7b7AG1abAXAx3lgbvgTCRxuxP0UFnkcTOJQ8eJKEIL96Gv +jtNMHDrOhNCxIKhmIP+IhwuOOAaLywgoJjBuBQ2jPHeBcYEE3aj0SU7yQe7OSF7xOJELECZEwano +2UNWIbDwWD4hwSAyTNxoD4KHy6gncQvFbQPj4UAQS7Wl+HTc5EINgaNM7dOQEjYNngTfqKDfwJ4c +JxQ4AY09V/fp2GVjupFmnm7tnRJj+tYpvp2ttJduHEfu2dnnxNRzdJ70Uoff631/XowlzdRz0G5L +6cyfcct56cX+fbHLm6mXM8Zuq8tqaf2+706p57j0L+4scaUbafb3nbPOWan/xN0Y4/mU9rxva534 +bi9a532/tV6Z88V1dr1/c77veTp11IszfnrvxP82W/mzKf1L8ZyVdrazYluf0mlnrV4r9Smb1ov7 +ev0qba4ezlVeWh3f16+dNbe9M3emV+a2MMf1624fX4o9TGungs78GGP5dl73bF92xdVim6nTaq/X +mmetG8lx7Td+69R+y6b1Wktz55642pf/7k+r/M7Uf17sVmKvG8mR+yd+Sen1tjT7zyvzvdLOiy29 ++D3X7yzvnbZSO7H8n5dOSm29PeXbeS+mnbFjnGl/lfn/5um5c/2Zp8Xv8j693Y5xbc+BJ661f1aJ +56x/qcwU11np7c+0ZZ7Tc9h6s30qu+L/DLNN67V/pVOca5bT4v5toxLGMIYxjGEM4/9/rZRmb5lz +dXzN9LrtbOXNtXH3pU/xtLS/v97pluLG9Fp3K9/a++3U/lfpXe3snO/Es96J8b3YWultN5bjs89L +3W3bS6vb6Vh6W8fT7PZmbGdtaTHN3jV/bitr22xnpvVr04un2+3loO23dsY+b0tcq+fAndtefH9i +KrNbp3XmO92ppE0903wvxhvJ4V0/2695fn68wRz23eLqOSrOuWfGtG0YwxjGMIYxjGFkOWidk3a9 +tVbPcak3rVZaSx2eZa3Voz0rfVsl/WznnbNpnk2rz3/7+GKf2d/il2/r41vnzT5r5+vVSnwptZ/b +XjunU4e1uFba/bNKbKvnmJS+Xyr/r3ul1l3aWz0Hzdbalo7p9pq49mfPMt/qOWbfSt2z41onprht +04olxbctnflJtmK31HM4rvZS+fQ3GK5Z3lor/uzW0jCGMYxhDOMG8zAgLHBC8HQCR2FCQRhSAE8+ +P0/61H7Oful9pxbjOi9mOWr1mWudtF6ZZ3XM2hPn2p/nvT+rw+t0+nnep00xvtX652u/biSH46/Z +6dss86wbyfEZu81/u+uUs59e+jhntxLPp279ykqp58Buc/WXs1aH55ndZtzzNrX/T6e8tG4sB3bv +aTGltm+u9O/1jCduWmu1tVaca/VKPUe2M7vLptVDD+joLYPkoLhjmyA52tHV0Y6NIsMTum+YAdDi +FrePkiokKg+br6Sn0QAiixUnZLIh0EFKzDwOl4zC5wIBM5spzP9Xff9LL24ft58VMwvN96/j9snP +s3xfWz6LV6H4R4nbqfJxRqAA2sO+xS0Wi0WquIHouSgUFTcQK3FzVShfqmTAEph1JpmYjJOBVCQo +HDgUl18m5uyhi9sTrCS1kAZRMd0MaREvGVCb/0oEUgLklk7Ayv1BIDI9AQiPY8QIXqDJBXhCob4a +0OrQ5xMQA2UwAQEzlScf8mQgnI7CEAByAjQHkClBo6BFfAvEUHFCDPe7aIA8biYJkMctbpEPyeKV +L0Z5rsEz6hdDhIMgC5Dpq6VmE67B4Bn1zIuU/LsRF3yYIBF0cfMeqtAWt40AtIjvIFCbr1giFC0L +QAQcyMrCAUkQgCQMQAsByIxbRBfULpTkTS4V3YUqputkkooOVUxXaQWYhKtWf3LSMgDjw1hYWFwj +IXYKw0/3ESxbmQIOurh9C51R1QQsdK1fgNj4TA4sEz4MZYHwmbzL8hCC66SBfK8HE14zcFIqG4TV +hPKVuq87DRo3DYK4cRpKqg2B6HIA+pgOWFbB49CxsBoCPfMy0jxkfBw6Na7VwpRAjoKg9aMKmoVb +0JSoRVY1edBjpc8hQTJoZDLz0siA1UUTsXgmGRSICuA0eGynihEIdJnOLJEcBBYuMixAg0po8bgV +jAIw+rVggfD1xrWguRjUWk3rNkrgvUxAfmBAu0mjmNrnwoqIW9xKWeRxAq8LCqeJ0EKT0f1c7oFI +lp80oYObB5DpM0Xc5QEmHg/GDYa6jFrhIaPgH1z1klQbMwH8GVio0CB8JtVm/sIL9FBbbXQoHfvG +zSKZ6GbC6co0OCqUvpOE7KTRaD0ONAIX1OqIDC0O0gAx7x1OF3H7XGhE3DhUpuMWc4gbWKL9SbI6 +zgBVsnDyATLhEDzscbIK+1VoaGjkJMmP2+lidNJIwFgCNwUY0zd5gSzHaD0eHD0yBxqOISXqWkGq +cZtpZGwaVJEZEzQl0EPCaHWsscs7EE34nBsrfZE/eGlKJYduSgym51Op083MZnGL2zfIuE0DD3uc +QMBBFxHAZeEkk7hFOk8NgQGp/lRBs/AYzAv5DCUx+RwVSh+m4CTqAo2QqD9JpOK04lg8bnGLAdvH +wBNjjFvLYqURgJiL5mjNYjVaa2bhaFmsNFq1laMFgBXNYpu1XhermYLnkcIYxjCGMYzUUk+559ry +zsYYN33p+HP93E2r55yt/fk2f37anR2Xvn9feutTeZt6DvsUX2vdqXXQauek9O2teMpM/17vjGm/ +9c5X4vxeJ30qP0/P4dhW68D+/7Pi7jslxdbzvLcxpRb79May8ZwUY3mpnZnaOt8nfetV1reOv/Sz +ldS+20yt15butuektKd9+/ennLf6/SpvW0d1/65W5js9B3XvOTOduObcTzFu6pheefH0HPQ6tf/S +tt1eDr+X0mo7W5/S8/Qctimltsp3Sz3/xNlWLGfTz1nSacMYxjCG0Tt/rrSztDlfp7POfDF+em+t +lDq2NNfG+a/s23jWnp4tvhdfn//5Uge10u9tXOv1O6etMtufFv+dt6XPmWlXS/va7HLmWZ9aSXN1 +XNs9sbR1evn9vleH0/v9dOJ6r8R01kmzrLluL9wvK825273aW2XGudbcXmfNk9r8mN6bZfbpOWq9 +9doqKa355sbTw48tzpR+vtdpvpXSfK91iuul71bin//+ts6ab5Z04jvrzPhSl5POimlLt/Pev+7f +Ntfa3hLPusEy9UltxV/DGMYwhjGMYQyj9Upr27cV98SN86RtvU78tPu+xe4/vzO9ubo79nq7uk/P +9K+kd3q11knnrBXja6ltK23f7eXoWt9604kt/ffOLa3jtrbWia/LxjZTTOdfmfur9/uljoyxtdIf +T7c540ul+71uu+KJM64by1Hv/ezX0m6XmM72+/PdZe3ruDhfn5V2buuebXX8/Wkf53ylvf6Ob6We +rexst5fja7X4vu1J88a63bPO252v/Xd5sXW4rfVpT5+zylyn53N6nrXKia2j+r3uVV6LreM7+x/L +ii2u1NJ577Uwx2Oabb3U3eJ6uz+HMYxhDKNbb1y/731KvauVtuY77bxNcZ2O57X/+M7aN/djmat/ +255dHRN7Z+y4Tjnr9NxO2rZiarvtxPlWz1Ria7eX42Z887RPu9KN5ahYUjrbtvyL6XR7bZ31Pb+V +9VoHrvQ2tj9n5+rdbqec12Ls2O2d80r707uV2klzltbajdW5LW6v0i2+c9Zpr73WG2fs/+0v277n +sG27P+f+9p7u3/hajO2t0tr37sS5G+Ovk2b596+dFeNq781ZNn7Pkb1Wij1XSfF7OHvOs94859OM +r8y0rZ21b9swhjGMYQxjGMPY2Zv4rX2vO/aMvb78z3d6vvNi/NjaOzOV9rvp/faKc/0VbBYzTebj +i+W0/9Pfel8uLQ0BKGzEEFoT4GReLnZmfLm0XhYZqxqrhXC5XKweT3svldVz7e6mktLpObKDVjpd +Pp1exY7d7ctc7/Zy3Glp9af40o3lsO9f7ZV15nfcvvV+vvln7jyl289OJ6VT+s/Pt9v23F4Oime2 +tt5a75XXf1K3edqNRe/T+tdKr7TtbEprdXzf2S5xtjRbb9z3JaXVcTs7zt7Zr+fw2/MztdX6S+s5 +d3WKu/GlVv60tdp5pa3f1158L83W4vy1v3qOx7Va+//Vcb9rzk+pbdqY+qVT+qzby+HTXq+5/99z +5M//+eI76wZzOJ393fTp9X/bsymWlN4w0lnrRZGxClFgq9nqdLm0NFauhBYALkgCqxnFauPgtbqo +XWQU1GarWgJFhqd1ccLZ2llro9pp4zmz44sAYLBiL07IP+PP1z9bmZq1ahktzuXSomlcvF6ey+Vi +Ra/jjVVx9a6yYuq4dGb8U35vrFtn+6z2Su9qnVLL8nt9urvXpjPbe6f9jF+6rY5Mq2dq8ddvWemc +l2I53y2etumtVOJbK2EMYxjDGMYwhvEVRlgYwxjGMIYxjGEMYxjDCLu3MZ349XJMfKf7Y/yVzqbW +rzfGn2W/dcxbZ/0qr79j+7St4+JaZ/vseVtez+25/mzal1o5m+aeE2NK/85ccX08sw== + + + 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 +sZWZn4SSBvdvaZYgKg== + + + diff --git a/docs/imgs/logo.png b/docs/imgs/logo.png new file mode 100644 index 000000000000..08943b5accaa Binary files /dev/null and b/docs/imgs/logo.png differ diff --git a/docs/imgs/overview.png b/docs/imgs/overview.png new file mode 100644 index 000000000000..777c32d939d2 Binary files /dev/null and b/docs/imgs/overview.png differ diff --git a/docs/imgs/repo.png b/docs/imgs/repo.png new file mode 100644 index 000000000000..80b80689d02f Binary files /dev/null and b/docs/imgs/repo.png differ diff --git a/docs/imgs/secret-demo.gif b/docs/imgs/secret-demo.gif new file mode 100644 index 000000000000..085606ccb563 Binary files /dev/null and b/docs/imgs/secret-demo.gif differ diff --git a/docs/imgs/trivy-aws.png b/docs/imgs/trivy-aws.png new file mode 100644 index 000000000000..5e748fea5757 Binary files /dev/null and b/docs/imgs/trivy-aws.png differ diff --git a/docs/imgs/trivy-k8s.png b/docs/imgs/trivy-k8s.png new file mode 100644 index 000000000000..d9060bdda1b9 Binary files /dev/null and b/docs/imgs/trivy-k8s.png differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000000..b45469756c68 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,6 @@ +# Docs + +Welcome to the Trivy documentation! +Here you can find complete and thorough information about every aspect of Trivy, how to use it, features available, and configuration options. + +👈 Please use the left side navigation browse the different topics. diff --git a/docs/overrides/main.html b/docs/overrides/main.html new file mode 100644 index 000000000000..7b574b0f5354 --- /dev/null +++ b/docs/overrides/main.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block extrahead %} + +{% set title = config.site_name %} +{% if page and page.title and not page.is_homepage %} + {% set title = config.site_name ~ " - " ~ page.title | striptags %} +{% endif %} + +{% set image = config.site_url ~ 'assets/images/illustrations/banner.png' %} + + + + + + + + + + + +{% endblock %} + +{% block outdated %} +You're not viewing the latest version of the documentation. + + Click here to go to latest. + +{% endblock %} \ No newline at end of file diff --git a/docs/tutorials/additional-resources/cks.md b/docs/tutorials/additional-resources/cks.md new file mode 100644 index 000000000000..657899c3b977 --- /dev/null +++ b/docs/tutorials/additional-resources/cks.md @@ -0,0 +1,26 @@ +# CKS preparation resources + +The [Certified Kubernetes Security Specialist (CKS) Exam](https://training.linuxfoundation.org/certification/certified-kubernetes-security-specialist/) is offered by The Linux Foundation. It provides assurance that a CKS has the skills, knowledge, and competence on a broad range of best practices for securing container-based applications and Kubernetes platforms during build, deployment and runtime. CKA certification is required to sit for this exam. + +### Community Resources + +- [Trivy Video overview (short)][overview] +- [Example questions from the exam][exam] +- [More example questions][questions] +- [CKS exam study guide][study-guide] +- [Docker Image Vulnerabilities & Trivy Image Scanning Demo | K21Academy](https://youtu.be/gHz10UsEdys) + +### Aqua Security Blog posts to learn more + +- Supply chain security best [practices][supply-chain-best-practices] +- Supply chain [attacks][supply-chain-attacks] + +If you know of interesting resources, please start a PR to add those to the list. + +[overview]: https://youtu.be/2cjH6Zkieys +[exam]: https://jonathan18186.medium.com/certified-kubernetes-security-specialist-cks-preparation-part-7-supply-chain-security-9cf62c34cf6a +[questions]: https://github.com/kodekloudhub/certified-kubernetes-security-specialist-cks-course/blob/main/docs/06-Supply-Chain-Security/09-Scan-images-for-known-vulnerabilities-(Trivy).md +[study-guide]: https://devopscube.com/cks-exam-guide-tips/ + +[supply-chain-best-practices]: https://blog.aquasec.com/supply-chain-security-best-practices +[supply-chain-attacks]: https://blog.aquasec.com/supply-chain-threats-using-container-images diff --git a/docs/tutorials/additional-resources/community.md b/docs/tutorials/additional-resources/community.md new file mode 100644 index 000000000000..2f5ab5a15c41 --- /dev/null +++ b/docs/tutorials/additional-resources/community.md @@ -0,0 +1,38 @@ +# Community References +Below is a list of additional resources from the community. + +## Vulnerability Scanning + +- [Detecting Spring4Shell with Trivy and Grype](https://youtu.be/mOfBcpJWwSs) +- [Scan OS of your EC2 instances with Trivy](https://pabis.eu/blog/2023-05-01-Scan-Instances-With-Trivy.html) + +## CI/CD Pipelines + +- [How to use Tekton to set up a CI pipeline with OpenShift Pipelines](https://www.redhat.com/architect/cicd-pipeline-openshift-tekton) +- [Continuous Container Vulnerability Testing with Trivy](https://semaphoreci.com/blog/continuous-container-vulnerability-testing-with-trivy) +- [Getting Started With Trivy and Jenkins](https://youtu.be/MWe01VdwuMA) +- [How to use Tekton to set up a CI pipeline with OpenShift Pipelines](https://www.redhat.com/architect/cicd-pipeline-openshift-tekton) + +## Misconfiguration Scanning + +- [Identifying Misconfigurations in your Terraform](https://youtu.be/cps1V5fOHtE) +- [How to write custom checks for Trivy](https://blog.ediri.io/how-to-write-custom-policies-for-trivy) + +## SBOM, Attestation & related + +- [Attesting Image Scans With Kyverno](https://neonmirrors.net/post/2022-07/attesting-image-scans-kyverno/) + +## Trivy Kubernetes + +- [Using Trivy Kubernetes in OVHCloud documentation.](https://docs.ovh.com/gb/en/kubernetes/installing-trivy/) + +## Comparisons + +- [the vulnerability remediation lifecycle of Alpine containers](https://ariadne.space/2021/06/08/the-vulnerability-remediation-lifecycle-of-alpine-containers/) +- [Open Source CVE Scanner Round-Up: Clair vs Anchore vs Trivy](https://boxboat.com/2020/04/24/image-scanning-tech-compared/) +- [Docker Image Security: Static Analysis Tool Comparison – Anchore Engine vs Clair vs Trivy](https://www.a10o.net/devsecops/docker-image-security-static-analysis-tool-comparison-anchore-engine-vs-clair-vs-trivy/) + +### Evaluations + +- [Istio evaluating to use Trivy](https://github.com/istio/release-builder/pull/687#issuecomment-874938417) +- [Research Spike: evaluate Trivy for scanning running containers](https://gitlab.com/gitlab-org/gitlab/-/issues/270888) diff --git a/docs/tutorials/additional-resources/references.md b/docs/tutorials/additional-resources/references.md new file mode 100644 index 000000000000..fa45cc99d12c --- /dev/null +++ b/docs/tutorials/additional-resources/references.md @@ -0,0 +1,38 @@ +# Additional Resources and Tutorials +Below is a list of additional resources from Aqua Security. + +## Announcements + +- [Trivy Vulnerability Scanner Joins the Aqua Open-source Family](https://blog.aquasec.com/trivy-vulnerability-scanner-joins-aqua-family) +- [Trivy Image Vulnerability Scanner Now Under Apache 2.0 License](https://blog.aquasec.com/trivy-open-source-vulnerability-scanner-apache2.0-license) + +## Vulnerability Scanning + +- [Using Trivy to Discover Vulnerabilities in VS Code Projects](https://blog.aquasec.com/trivy-open-source-vulnerability-scanner-vs-code) +- [How does a vulnerability scanner identify packages?](https://youtu.be/PaMnzeHBa8M) +- [Handling Container Vulnerabilities with Open Policy Agent - Teppei Fukuda, Aqua Security](https://youtu.be/WKE2XNZ2zr4) + +## CI/CD Pipelines + +- [DevSecOps with Trivy and GitHub Actions](https://blog.aquasec.com/devsecops-with-trivy-github-actions) +- [Find Image Vulnerabilities Using GitHub and Aqua Security Trivy Action](https://blog.aquasec.com/github-vulnerability-scanner-trivy) + +## Misconfiguration Scanning + +- [Identifying Misconfigurations in your Terraform](https://youtu.be/cps1V5fOHtE) + +## Client/Server + +- [Using Trivy in client server mode](https://youtu.be/tNQ-VlahtYM) + +## Workshops + +- [Trivy Live Demo & Q&A](https://youtu.be/6Vw0QgJ-k5o) +- [First Steps to Full Lifecycle Security with Open Source Tools - Rory McCune & Anais Urlichs](https://youtu.be/nwJ0366rs6s) + + +## Older Resources + +- [Webinar: Trivy Open Source Scanner for Container Images – Just Download and Run!](https://youtu.be/XnYxX9uueoQ) +- [Kubernetes Security through GitOps Best Practices: ArgoCD and Starboard](https://youtu.be/YvMY8to9aHI) +- [Get started with Kubernetes Security and Starboard](https://youtu.be/QgctrpTpJec) diff --git a/docs/tutorials/integrations/aws-codepipeline.md b/docs/tutorials/integrations/aws-codepipeline.md new file mode 100644 index 000000000000..396339e1c382 --- /dev/null +++ b/docs/tutorials/integrations/aws-codepipeline.md @@ -0,0 +1,4 @@ +# AWS CodePipeline +See [this blog post][blog] for an example of using Trivy within AWS CodePipeline. + +[blog]: https://aws.amazon.com/blogs/containers/scanning-images-with-trivy-in-an-aws-codepipeline/ diff --git a/docs/tutorials/integrations/aws-security-hub.md b/docs/tutorials/integrations/aws-security-hub.md new file mode 100644 index 000000000000..6963eed57241 --- /dev/null +++ b/docs/tutorials/integrations/aws-security-hub.md @@ -0,0 +1,72 @@ +# AWS Security Hub + +![Amazon Security Hub](../../imgs/Security-Hub.jpeg){ width=50 } + +## Upload findings to Security Hub + +In the following example using the template `asff.tpl`, [ASFF][asff] file can be generated. + +``` +$ AWS_REGION=us-west-1 AWS_ACCOUNT_ID=123456789012 trivy image --format template --template "@contrib/asff.tpl" -o report.asff golang:1.12-alpine +``` + +ASFF template needs AWS_REGION and AWS_ACCOUNT_ID from environment variables. + +The Product [ARN][arn] field follows the pattern below to match what AWS requires for the [product resource type][resource-type]. + +{% raw %} +``` +"ProductArn": "arn:aws:securityhub:{{ env "AWS_REGION" }}::product/aquasecurity/aquasecurity", +``` +{% endraw %} + +In order to upload results you must first run [enable-import-findings-for-product][enable] like: + +``` +aws securityhub enable-import-findings-for-product --product-arn arn:aws:securityhub:::product/aquasecurity/aquasecurity +``` + +The findings are [formatted for the API][asff-syntax] with a key of `Findings` and a value of the array of findings. +In order to upload via the CLI the outer wrapping must be removed being left with only the array of findings. +The easiest way of doing this is with the [jq library][jq] using the command + +``` +cat report.asff | jq '.Findings' +``` + +Then, you can upload it with AWS CLI. + +``` +$ aws securityhub batch-import-findings --findings file://report.asff +``` + +### Note + +The [batch-import-findings][batch-import-findings] command limits the number of findings uploaded to 100 per request. +The best known workaround to this problem is using [jq][jq] to run the following command + +``` +jq '.[:100]' report.asff 1> short_report.asff +``` + +## Customize +You can customize [asff.tpl][asff.tpl] + +``` +$ export AWS_REGION=us-west-1 +$ export AWS_ACCOUNT_ID=123456789012 +$ trivy image --format template --template "@your-asff.tpl" -o report.asff golang:1.12-alpine +``` + +## Reference +[aws.amazon.com/blogs/security/how-to-build-ci-cd-pipeline-container-vulnerability-scanning-trivy-and-aws-security-hub/](https://aws.amazon.com/blogs/security/how-to-build-ci-cd-pipeline-container-vulnerability-scanning-trivy-and-aws-security-hub/) + +[asff]: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format.html +[asff-syntax]: https://docs.aws.amazon.com/securityhub/latest/userguide/securityhub-findings-format-syntax.html +[arn]: https://docs.aws.amazon.com/general/latest/gr/aws-arns-and-namespaces.html +[resource-type]: https://github.com/awsdocs/aws-security-hub-user-guide/blob/master/doc_source/securityhub-partner-providers.md#aqua-security--aqua-cloud-native-security-platform-sends-findings +[enable]: https://docs.aws.amazon.com/cli/latest/reference/securityhub/enable-import-findings-for-product.html +[batch-import-findings]: https://docs.aws.amazon.com/cli/latest/reference/securityhub/batch-import-findings.html#options +[asff.tpl]: https://github.com/aquasecurity/trivy/blob/main/contrib/asff.tpl + +[jq]: https://stedolan.github.io/jq/ \ No newline at end of file diff --git a/docs/tutorials/integrations/azure-devops.md b/docs/tutorials/integrations/azure-devops.md new file mode 100644 index 000000000000..1fbac0e848d7 --- /dev/null +++ b/docs/tutorials/integrations/azure-devops.md @@ -0,0 +1,22 @@ +# Azure Devops + +- Here is the [Azure DevOps Pipelines Task for Trivy][action] + +![trivy-azure](https://github.com/aquasecurity/trivy-azure-pipelines-task/blob/main/screenshot.png?raw=true) + +### [Use ImageCleaner to clean up stale images on your Azure Kubernetes Service cluster][azure2] + +It's common to use pipelines to build and deploy images on Azure Kubernetes Service (AKS) clusters. While great for image creation, this process often doesn't account for the stale images left behind and can lead to image bloat on cluster nodes. These images can present security issues as they may contain vulnerabilities. By cleaning these unreferenced images, you can remove an area of risk in your clusters. When done manually, this process can be time intensive, which ImageCleaner can mitigate via automatic image identification and removal. + + Vulnerability is determined based on a trivy scan, after which images with a LOW, MEDIUM, HIGH, or CRITICAL classification are flagged. An updated ImageList will be automatically generated by ImageCleaner based on a set time interval, and can also be supplied manually. +### [Microsoft Defender for container registries and Trivy][azure] + +This blog explains how to scan your Azure Container Registry-based container images with the integrated vulnerability scanner when they're built as part of your GitHub workflows. + +To set up the scanner, you'll need to enable Microsoft Defender for Containers and the CI/CD integration. When your CI/CD workflows push images to your registries, you can view registry scan results and a summary of CI/CD scan results. + +The findings of the CI/CD scans are an enrichment to the existing registry scan findings by Qualys. Defender for Cloud's CI/CD scanning is powered by Aqua Trivy + +[action]: https://github.com/aquasecurity/trivy-azure-pipelines-task +[azure]: https://docs.microsoft.com/en-us/azure/defender-for-cloud/defender-for-containers-cicd +[azure2]: https://docs.microsoft.com/en-us/azure/aks/image-cleaner?tabs=azure-cli diff --git a/docs/tutorials/integrations/bitbucket.md b/docs/tutorials/integrations/bitbucket.md new file mode 100644 index 000000000000..6b42855dcb38 --- /dev/null +++ b/docs/tutorials/integrations/bitbucket.md @@ -0,0 +1,5 @@ +# Bitbucket Pipelines + +See [trivy-pipe][trivy-pipe] for the details. + +[trivy-pipe]: https://github.com/aquasecurity/trivy-pipe diff --git a/docs/tutorials/integrations/circleci.md b/docs/tutorials/integrations/circleci.md new file mode 100644 index 000000000000..5f0152c2ac0f --- /dev/null +++ b/docs/tutorials/integrations/circleci.md @@ -0,0 +1,34 @@ +# CircleCI + +``` +$ cat .circleci/config.yml +jobs: + build: + docker: + - image: docker:stable-git + steps: + - checkout + - setup_remote_docker + - run: + name: Build image + command: docker build -t trivy-ci-test:${CIRCLE_SHA1} . + - run: + name: Install trivy + command: | + apk add --update-cache --upgrade curl + curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + - run: + name: Scan the local image with trivy + command: trivy image --exit-code 0 --no-progress trivy-ci-test:${CIRCLE_SHA1} +workflows: + version: 2 + release: + jobs: + - build +``` + +[Example][example] +[Repository][repository] + +[example]: https://circleci.com/gh/aquasecurity/trivy-ci-test +[repository]: https://github.com/aquasecurity/trivy-ci-test diff --git a/docs/tutorials/integrations/github-actions.md b/docs/tutorials/integrations/github-actions.md new file mode 100644 index 000000000000..2dade1051a11 --- /dev/null +++ b/docs/tutorials/integrations/github-actions.md @@ -0,0 +1,9 @@ +# GitHub Actions + +- Here is the [Trivy GitHub Action][action] +- The Microsoft Azure team have written a [container-scan action][azure] that uses Trivy and Dockle +- For full control over the options specified to Trivy, this [blog post][blog] describes adding Trivy into your own GitHub action workflows + +[action]: https://github.com/aquasecurity/trivy-action +[azure]: https://github.com/Azure/container-scan +[blog]: https://blog.aquasec.com/devsecops-with-trivy-github-actions diff --git a/docs/tutorials/integrations/gitlab-ci.md b/docs/tutorials/integrations/gitlab-ci.md new file mode 100644 index 000000000000..afef98f3681b --- /dev/null +++ b/docs/tutorials/integrations/gitlab-ci.md @@ -0,0 +1,179 @@ +# GitLab CI + +GitLab 15.0 includes [free](https://gitlab.com/groups/gitlab-org/-/epics/2233) integration with Trivy. + +To [configure container scanning with Trivy in GitLab](https://docs.gitlab.com/ee/user/application_security/container_scanning/#configuration), simply include the CI template in your `.gitlab-ci.yml` file: + +```yaml +include: + - template: Security/Container-Scanning.gitlab-ci.yml +``` + +If you're a GitLab 14.x Ultimate customer, you can use the same configuration above. + +Alternatively, you can always use the example configurations below. + +```yaml +stages: + - test + +trivy: + stage: test + image: docker:stable + services: + - name: docker:dind + entrypoint: ["env", "-u", "DOCKER_HOST"] + command: ["dockerd-entrypoint.sh"] + variables: + DOCKER_HOST: tcp://docker:2375/ + DOCKER_DRIVER: overlay2 + # See https://github.com/docker-library/docker/pull/166 + DOCKER_TLS_CERTDIR: "" + IMAGE: trivy-ci-test:$CI_COMMIT_SHA + TRIVY_NO_PROGRESS: "true" + TRIVY_CACHE_DIR: ".trivycache/" + before_script: + - export TRIVY_VERSION=$(wget -qO - "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') + - echo $TRIVY_VERSION + - wget --no-verbose https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz -O - | tar -zxvf - + allow_failure: true + script: + # Build image + - docker build -t $IMAGE . + # Build report + - ./trivy image --exit-code 0 --format template --template "@/contrib/gitlab.tpl" -o gl-container-scanning-report.json $IMAGE + # Print report + - ./trivy image --exit-code 0 --severity HIGH $IMAGE + # Fail on severe vulnerabilities + - ./trivy image --exit-code 1 --severity CRITICAL $IMAGE + cache: + paths: + - .trivycache/ + # Enables https://docs.gitlab.com/ee/user/application_security/container_scanning/ (Container Scanning report is available on GitLab Ultimate) + artifacts: + reports: + container_scanning: gl-container-scanning-report.json +``` + +[Example][example] +[Repository][repository] + +### GitLab CI using Trivy container + +To scan a previously built image that has already been pushed into the +GitLab container registry the following CI job manifest can be used. +Note that `entrypoint` needs to be unset for the `script` section to work. +In case of a non-public GitLab project Trivy additionally needs to +authenticate to the registry to be able to pull your application image. +Finally, it is not necessary to clone the project repo as we only work +with the container image. + +```yaml +container_scanning: + image: + name: docker.io/aquasec/trivy:latest + entrypoint: [""] + variables: + # No need to clone the repo, we exclusively work on artifacts. See + # https://docs.gitlab.com/ee/ci/runners/configure_runners.html#git-strategy + GIT_STRATEGY: none + TRIVY_USERNAME: "$CI_REGISTRY_USER" + TRIVY_PASSWORD: "$CI_REGISTRY_PASSWORD" + TRIVY_AUTH_URL: "$CI_REGISTRY" + TRIVY_NO_PROGRESS: "true" + TRIVY_CACHE_DIR: ".trivycache/" + FULL_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + script: + - trivy --version + # update vulnerabilities db + - time trivy image --download-db-only + # Builds report and puts it in the default workdir $CI_PROJECT_DIR, so `artifacts:` can take it from there + - time trivy image --exit-code 0 --format template --template "@/contrib/gitlab.tpl" + --output "$CI_PROJECT_DIR/gl-container-scanning-report.json" "$FULL_IMAGE_NAME" + # Prints full report + - time trivy image --exit-code 0 "$FULL_IMAGE_NAME" + # Fail on critical vulnerabilities + - time trivy image --exit-code 1 --severity CRITICAL "$FULL_IMAGE_NAME" + cache: + paths: + - .trivycache/ + # Enables https://docs.gitlab.com/ee/user/application_security/container_scanning/ (Container Scanning report is available on GitLab EE Ultimate or GitLab.com Gold) + artifacts: + when: always + reports: + container_scanning: gl-container-scanning-report.json + tags: + - docker-runner +``` + +[example]: https://gitlab.com/aquasecurity/trivy-ci-test/pipelines +[repository]: https://github.com/aquasecurity/trivy-ci-test + +### GitLab CI alternative template + +Depending on the edition of gitlab you have or your desired workflow, the +container scanning template may not meet your needs. As an addition to the +above container scanning template, a template for +[code climate](https://docs.gitlab.com/ee/ci/testing/code_quality.html) +has been included. The key things to update from the above examples are +the `template` and `report` type. An updated example is below. + +```yaml +stages: + - test + +trivy: + stage: test + image: docker:stable + services: + - name: docker:dind + entrypoint: ["env", "-u", "DOCKER_HOST"] + command: ["dockerd-entrypoint.sh"] + variables: + DOCKER_HOST: tcp://docker:2375/ + DOCKER_DRIVER: overlay2 + # See https://github.com/docker-library/docker/pull/166 + DOCKER_TLS_CERTDIR: "" + IMAGE: trivy-ci-test:$CI_COMMIT_SHA + TRIVY_NO_PROGRESS: "true" + TRIVY_CACHE_DIR: ".trivycache/" + before_script: + - export TRIVY_VERSION=$(wget -qO - "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') + - echo $TRIVY_VERSION + - wget --no-verbose https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz -O - | tar -zxvf - + allow_failure: true + script: + # Build image + - docker build -t $IMAGE . + # Image report + - ./trivy image --exit-code 0 --format template --template "@/contrib/gitlab-codequality.tpl" -o gl-codeclimate-image.json $IMAGE + # Filesystem report + - ./trivy filesystem --scanners misconfig,vuln --exit-code 0 --format template --template "@/contrib/gitlab-codequality.tpl" -o gl-codeclimate-fs.json . + # Combine report + - apk update && apk add jq + - jq -s 'add' gl-codeclimate-image.json gl-codeclimate-fs.json > gl-codeclimate.json + cache: + paths: + - .trivycache/ + # Enables https://docs.gitlab.com/ee/user/application_security/container_scanning/ (Container Scanning report is available on GitLab EE Ultimate or GitLab.com Gold) + artifacts: + paths: + - gl-codeclimate.json + reports: + codequality: gl-codeclimate.json +``` + +Currently gitlab only supports a single code quality report. There is an +open [feature request](https://gitlab.com/gitlab-org/gitlab/-/issues/9014) +to support multiple reports. Until this has been implemented, if you +already have a code quality report in your pipeline, you can use +`jq` to combine reports. Depending on how you name your artifacts, it may +be necessary to rename the artifact if you want to reuse the name. To then +combine the previous artifact with the output of trivy, the following `jq` +command can be used, `jq -s 'add' prev-codeclimate.json trivy-codeclimate.json > gl-codeclimate.json`. + +### GitLab CI alternative template example report + +You'll be able to see a full report in the GitLab pipeline code quality UI, where filesystem vulnerabilities and misconfigurations include links to the flagged files and image vulnerabilities report the image/os or runtime/library that the vulnerability originates from instead. + +![codequality](../../imgs/gitlab-codequality.png) diff --git a/docs/tutorials/integrations/index.md b/docs/tutorials/integrations/index.md new file mode 100644 index 000000000000..64c6b993fc6a --- /dev/null +++ b/docs/tutorials/integrations/index.md @@ -0,0 +1,2 @@ +# Integrations +Scan your image automatically as part of your CI workflow, failing the workflow if a vulnerability is found. When you don't want to fail the test, specify `--exit-code 0`. diff --git a/docs/tutorials/integrations/travis-ci.md b/docs/tutorials/integrations/travis-ci.md new file mode 100644 index 000000000000..c9376a4234e7 --- /dev/null +++ b/docs/tutorials/integrations/travis-ci.md @@ -0,0 +1,29 @@ +# Travis CI + +``` +$ cat .travis.yml +services: + - docker + +env: + global: + - COMMIT=${TRAVIS_COMMIT::8} + +before_install: + - docker build -t trivy-ci-test:${COMMIT} . + - export VERSION=$(curl --silent "https://api.github.com/repos/aquasecurity/trivy/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') + - wget https://github.com/aquasecurity/trivy/releases/download/v${VERSION}/trivy_${VERSION}_Linux-64bit.tar.gz + - tar zxvf trivy_${VERSION}_Linux-64bit.tar.gz +script: + - ./trivy image --exit-code 0 --severity HIGH --no-progress trivy-ci-test:${COMMIT} + - ./trivy image --exit-code 1 --severity CRITICAL --no-progress trivy-ci-test:${COMMIT} +cache: + directories: + - $HOME/.cache/trivy +``` + +[Example][example] +[Repository][repository] + +[example]: https://travis-ci.org/aquasecurity/trivy-ci-test +[repository]: https://github.com/aquasecurity/trivy-ci-test diff --git a/docs/tutorials/kubernetes/cluster-scanning.md b/docs/tutorials/kubernetes/cluster-scanning.md new file mode 100644 index 000000000000..e18595579900 --- /dev/null +++ b/docs/tutorials/kubernetes/cluster-scanning.md @@ -0,0 +1,67 @@ +# Kubernetes Scanning Tutorial + +## Prerequisites + +To test the following commands yourself, make sure that you’re connected to a Kubernetes cluster. A simple kind, a Docker-Desktop or microk8s cluster will do. In our case, we’ll use a one-node kind cluster. +Pro tip: The output of the commands will be even more interesting if you have some workloads running in your cluster. + +## Cluster Scanning + +Trivy K8s is great to get an overview of all the vulnerabilities and misconfiguration issues or to scan specific workloads that are running in your cluster. You would want to use the Trivy K8s command either on your own local cluster or in your CI/CD pipeline post deployments. + +The `trivy k8s` command is part of the Trivy CLI. + +With the following command, we can scan our entire Kubernetes cluster for vulnerabilities and get a summary of the scan: + +```sh +trivy k8s --report=summary +``` + +To get detailed information for all your resources, just replace ‘summary’ with ‘all’: + +```sh +trivy k8s --report=all +``` + +However, we recommend displaying all information only in case you scan a specific namespace or resource since you can get overwhelmed with additional details. + +Furthermore, we can specify the namespace that Trivy is supposed to scan to focus on specific resources in the scan result: + +```sh +trivy k8s --include-namespaces kube-system --report summary +``` + +Again, if you’d like to receive additional details, use the ‘--report=all’ flag: + +```sh +trivy k8s --include-namespaces kube-system --report all +``` + +Like with scanning for vulnerabilities, we can also filter in-cluster security issues by severity of the vulnerabilities: + +```sh +trivy k8s --severity=CRITICAL --report=summary +``` + +Note that you can use any of the Trivy flags on the Trivy K8s command. + +## Trivy Operator + +The Trivy K8s command is an imperative model to scan resources. We wouldn’t want to manually scan each resource across different environments. The larger the cluster and the more workloads are running in it, the more error-prone this process would become. With the Trivy Operator, we can automate the scanning process after the deployment. + +The Trivy Operator follows the Kubernetes Operator Model. Operators automate human actions, and the result of the task is saved as custom resource definitions (CRDs) within your cluster. + +This has several benefits: + +- Trivy Operator is installed CRDs in our cluster. As a result, all our resources, including our security scanner and its scan results, are Kubernetes resources. This makes it much easier to integrate the Trivy Operator directly into our existing processes, such as connecting Trivy with Prometheus, a monitoring system. + +- The Trivy Operator will automatically scan your resources every six hours. You can set up automatic alerting in case new critical security issues are discovered. + +- The CRDs can be both machine and human-readable depending on which applications consume the CRDs. This allows for more versatile applications of the Trivy operator. + +There are several ways that you can install the Trivy Operator in your cluster. In this guide, we’re going to use the Helm installation. + +Please follow the Trivy Operator documentation for further information on: + +- [Installation of the Trivy Operator](https://aquasecurity.github.io/trivy-operator/latest/getting-started/installation/) +- [Getting started guide](https://aquasecurity.github.io/trivy-operator/latest/getting-started/quick-start/) \ No newline at end of file diff --git a/docs/tutorials/kubernetes/gitops.md b/docs/tutorials/kubernetes/gitops.md new file mode 100644 index 000000000000..b5a30ff47450 --- /dev/null +++ b/docs/tutorials/kubernetes/gitops.md @@ -0,0 +1,131 @@ +# Installing the Trivy-Operator through GitOps + +This tutorial shows you how to install the Trivy Operator through GitOps platforms, namely ArgoCD and FluxCD. + +## ArgoCD + +Make sure to have [ArgoCD installed](https://argo-cd.readthedocs.io/en/stable/getting_started/) and running in your Kubernetes cluster. + +You can either deploy the Trivy Operator through the argocd CLI or by applying a Kubernetes manifest. + +ArgoCD command: +``` +> kubectl create ns trivy-system +> argocd app create trivy-operator --repo https://github.com/aquasecurity/trivy-operator --path deploy/helm --dest-server https://kubernetes.default.svc --dest-namespace trivy-system +``` +Note that this installation is directly related to our official Helm Chart. If you want to change any of the value, we'd suggest you to create a separate values.yaml file. + +Kubernetes manifest `trivy-operator.yaml`: +``` +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: trivy-operator + namespace: argocd +spec: + project: default + source: + chart: trivy-operator + repoURL: https://aquasecurity.github.io/helm-charts/ + targetRevision: 0.0.3 + helm: + values: | + trivy: + ignoreUnfixed: true + destination: + server: https://kubernetes.default.svc + namespace: trivy-system + syncPolicy: + automated: + prune: true + selfHeal: true +``` + +To apply the Kubernetes manifest, if you have the manifest locally, you can use the following command through kubectl: +``` +> kubectl apply -f trivy-operator.yaml + +application.argoproj.io/trivy-operator created +``` + +If you have the manifest in a Git repository, you can apply it to your cluster through the following command: +``` +> kubectl apply -n argocd -f https://raw.githubusercontent.com/AnaisUrlichs/argocd-starboard/main/starboard/argocd-starboard.yaml +``` +The latter command would allow you to make changes to the YAML manifest that ArgoCD would register automatically. + +Once deployed, you want to tell ArgoCD to sync the application from the actual state to the desired state: +``` +argocd app sync trivy-operator +``` + +Now you can see the deployment in the ArgoCD UI. Have a look at the ArgoCD documentation to know how to access the UI. + +![ArgoCD UI after deploying the Trivy Operator](../../imgs/argocd-ui.png) + +Note that ArgoCD is unable to show the Trivy CRDs as synced. + + +## FluxCD + +Make sure to have [FluxCD installed](https://fluxcd.io/docs/installation/#install-the-flux-cli) and running in your Kubernetes cluster. + +You can either deploy the Trivy Operator through the Flux CLI or by applying a Kubernetes manifest. + +Flux command: +``` +> kubectl create ns trivy-system +> flux create source helm trivy-operator --url https://aquasecurity.github.io/helm-charts --namespace trivy-system +> flux create helmrelease trivy-operator --chart trivy-operator + --source HelmRepository/trivy-operator + --chart-version 0.0.3 + --namespace trivy-system +``` + +Kubernetes manifest `trivy-operator.yaml`: +``` +apiVersion: source.toolkit.fluxcd.io/v1beta2 +kind: HelmRepository +metadata: + name: trivy-operator + namespace: flux-system +spec: + interval: 60m + url: https://aquasecurity.github.io/helm-charts/ + +--- +apiVersion: helm.toolkit.fluxcd.io/v2beta1 +kind: HelmRelease +metadata: + name: trivy-operator + namespace: trivy-system +spec: + chart: + spec: + chart: trivy-operator + sourceRef: + kind: HelmRepository + name: trivy-operator + namespace: flux-system + version: 0.10.1 + interval: 60m + values: + trivy: + ignoreUnfixed: true + install: + crds: CreateReplace + createNamespace: true +``` + +You can then apply the file to your Kubernetes cluster: +``` +kubectl apply -f trivy-operator.yaml +``` + +## After the installation + +After the install, you want to check that the Trivy operator is running in the trivy-system namespace: +``` +kubectl get deployment -n trivy-system +``` + diff --git a/docs/tutorials/kubernetes/kyverno.md b/docs/tutorials/kubernetes/kyverno.md new file mode 100644 index 000000000000..f2e9d12006a7 --- /dev/null +++ b/docs/tutorials/kubernetes/kyverno.md @@ -0,0 +1,99 @@ +# Attesting Image Scans With Kyverno + +This tutorial is based on the following blog post by Chip Zoller: [Attesting Image Scans With Kyverno](https://neonmirrors.net/post/2022-07/attesting-image-scans-kyverno/) + +This tutorial details + +- Verify the container image has an attestation with Kyverno + +### Prerequisites +1. A running Kubernetes cluster that kubectl is connected to +2. A Container image signed with Cosign and an attestation generated for a Trivy Vulnerability scan. + [Follow this tutorial for more information.][vuln-attestation] + +### Kyverno Policy to check attestation + +The following policy ensures that the attestation is no older than 168h: + +vuln-attestation.yaml + +{% raw %} + +```bash +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: check-vulnerabilities +spec: + validationFailureAction: Enforce + background: false + webhookTimeoutSeconds: 30 + failurePolicy: Fail + rules: + - name: checking-vulnerability-scan-not-older-than-one-hour + match: + any: + - resources: + kinds: + - Pod + verifyImages: + - imageReferences: + - "*" + attestations: + - type: https://cosign.sigstore.dev/attestation/vuln/v1 + conditions: + - all: + - key: "{{ time_since('','{{ metadata.scanFinishedOn }}', '') }}" + operator: LessThanOrEquals + value: "1h" + attestors: + - count: 1 + entries: + - keys: + publicKeys: |- + -----BEGIN PUBLIC KEY----- + abc + xyz + -----END PUBLIC KEY----- +``` + +{% endraw %} + +### Apply the policy to your Kubernetes cluster + +Ensure that you have Kyverno already deployed and running on your cluster -- for instance through he Kyverno Helm Chart. + +Next, apply the above policy: +``` +kubectl apply -f vuln-attestation.yaml +``` + +To ensure that the policy worked, we can deploy an example Kubernetes Pod with our container image: + +``` +kubectl run app-signed --image= docker.io/anaisurlichs/signed-example@sha256:c5911ac313e0be82a740bd726dc290e655800a9588424ba4e0558c705d1287fd  +``` +Note that the image is based on the [signing tutorial.][vuln-attestation] + +Once we apply the deployment, it should pass since our attestation is available: +``` +kubectl apply -f deployment.yaml -n app +deployment.apps/cns-website created +``` + +However, if we try to deploy any other container image, our deployment will fail. We can verify this by replacing the image referenced in the deployment with `docker.io/anaisurlichs/cns-website:0.0.5` and applying the deployment: +``` +kubectl run app-unsigned --image=docker.io/anaisurlichs/cns-website:0.1.1  + +Resource: "apps/v1, Resource=deployments", GroupVersionKind: "apps/v1, Kind=Deployment" +Name: "cns-website", Namespace: "app" +for: "deployment-two.yaml": admission webhook "mutate.kyverno.svc-fail" denied the request: + +resource Deployment/app/cns-website was blocked due to the following policies + +check-image: + autogen-check-image: | + failed to verify signature for docker.io/anaisurlichs/cns-website:0.0.5: .attestors[0].entries[0].keys: no matching signatures: +``` + +[vuln-attestation]: ../signing/vuln-attestation.md \ No newline at end of file diff --git a/docs/tutorials/misconfiguration/custom-checks.md b/docs/tutorials/misconfiguration/custom-checks.md new file mode 100644 index 000000000000..e77f6a6e6c55 --- /dev/null +++ b/docs/tutorials/misconfiguration/custom-checks.md @@ -0,0 +1,111 @@ +# Custom Checks with Rego + +Trivy can scan configuration files for common security issues (a.k.a IaC misconfiguration scanning). In addition to a comprehensive built in database of checks, you can add your own custom checks. Checks are written in [Rego](https://www.openpolicyagent.org/docs/latest/policy-language/) language and the full documentation for checks and customizing them is available [here](https://trivy.dev/docs/latest/scanner/misconfiguration/custom/). + +This tutorial will walk you through writing a custom check in Rego that checks for an issue in a Dockerfile. + +When you are writing a check, it's important to understand the input to the check. This will be the IaC file that you are scanning; for example, a Kubernetes YAML resource definition, or an AWS JSON CloudFormation, or in our case a Dockerfile. + +Since Rego is primarily tailored to query JSON objects, all incoming configuration files needs to be first converted to structured objects, which is available to the Rego code as the input variable. This is nothing that users have to do manually in Trivy. Instead, Rego makes it possible to pass in custom Schemas that detail how files are converted. Once Rego has access to a custom Schema, it will know in which format to access configuration files such as a Dockerfile. + +[Here you can find the schemas](https://github.com/aquasecurity/trivy/tree/main/pkg/iac/rego/schemas) that define how different configuration files are converted to JSON by Trivy. +This tutorial will make use of the [dockerfile.json schema](https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/dockerfile.json). The schema will need to be parsed into your custom check. + +Users can also use the [Schema Explorer](https://aquasecurity.github.io/trivy-schemas/) to view the structure of the data provided to Rego. + +## Create a Rego file and Specify Trivy metadata + +First, create a new `.rego` file e.g. a `docker-check.rego` file: +``` +touch docker-check.rego +``` + +Next, we need to specify metadata about the check. This is information that helps Trivy load and process the check. + +``` +# METADATA +# title: Verify Image +# description: Verify Image is allowed to be used and in the right format +# schemas: +# - input: schema["dockerfile"] +# custom: +# id: ID001 +# severity: MEDIUM +# input: +# selector: +# - type: dockerfile +``` + +Important: The `METADATA` has to be defined on top of the file. + +More information on the different fields in the metadata can be found in the [Trivy documentation.](https://trivy.dev/docs/latest/scanner/misconfiguration/custom/) + +## Package and imports + +``` +package custom.dockerfile.ID001 + +import future.keywords.in +``` + +Every Rego check has a package name. In our case, we will call it `custom.dockerfile.ID001` to avoid confusion between custom checks and built-in checks. The group name `dockerfile` has no effect on the package name. Note that each package has to contain only one check. However, we can pass multiple checks into our Trivy scan. +The first keyword of the package, in this case `custom`, will be reused in the `trivy` command as the `--namespace`. + +## Allowed data + +The check that we are setting up compares the container images used in the Dockerfile with a list of white-listed container images. Thus, we need to add the images that are allowed to be used in the Dockerfile to our check. In our case, we will store them in an array of arrays: + +``` +allowed_images := { + ["node:21-alpine3.19", "as", "build-deps"], + ["nginx:1.2"] +} +``` + +## Select the images that are used in the Dockerfile + +Next, we need to iterate over the different commands in our Dockerfile and identify the commands that provide the base container images: + +``` +deny[msg] { + input.Stages[m].Commands[l].Cmd == "from" + val := input.Stages[m].Commands[l].Value + not val in allowed_images + msg := sprintf("The container image '%s' used in the Dockerfile is not allowed", val) +} +``` + +Let's look at the check line by line: + +1. The rule should always be `deny` in the Trivy Rego checks +2. `input.Stages[m].Commands[l].Cmd` `input` allows us to access the different commands in the Dockerfile. We need to access the commands that use "FROM". Every command will be converted to lowercase. +3. `val := input.Stages[m].Commands[l].Value` accesses the value of the `FROM` command and stores it in `val` +4. `not val in allowed_images` checks whether val is not part of our allowed images list; this part of the check relies on the import statement +5. In case our check fails, the `msg` will be printed with the image name used in `val` + +Note that Rego + +* uses `AND` automatically to combine conditions in this check +* automatically iterates through the array of commands in the Dockerfile and allowed images + +## Run the check in a Trivy misconfiguration scan + +Ensure that you have Trivy installed and run the following command: + +```bash +trivy fs --scanners misconf --config-check ./docker-check.rego --namespaces custom ./Dockerfile +``` + +Please replace: + +* `./docker-check.rego` with the file path to your check +* `custom` should be replaced with your package name if different +* `./Dockerfile` is the path to the Dockerfile that should be scanned + +**Note**: If you define custom packages, you have to specify the package prefix via `--namespaces` option. In our case, we called the custom package `custom`. + +## Resources + +* [Rego provides a long list of courses](https://academy.styra.com/collections) that can be useful in writing more complex checks +* [The Rego documentation provides detailed information on the different types, iterations etc.](https://www.openpolicyagent.org/docs/latest/) +* Have a look at the [built-in checks](https://github.com/aquasecurity/trivy-checks/tree/main/checks) for Trivy for inspiration on how to write custom checks. \ No newline at end of file diff --git a/docs/tutorials/misconfiguration/terraform.md b/docs/tutorials/misconfiguration/terraform.md new file mode 100644 index 000000000000..3b3cdf3fbb9e --- /dev/null +++ b/docs/tutorials/misconfiguration/terraform.md @@ -0,0 +1,114 @@ +# Scanning Terraform files with Trivy + +This tutorial is focused on ways Trivy can scan Terraform IaC configuration files. + +A video tutorial on Terraform Misconfiguration scans can be found on the [Aqua Open Source YouTube account.](https://youtu.be/BWp5JLXkbBc) + +**A note to tfsec users** +We have been consolidating all of our scanning-related efforts in one place, and that is Trivy. You can read more on the decision in the [tfsec discussions.](https://github.com/aquasecurity/tfsec/discussions/1994) + +## Trivy Config Command + +Terraform configuration scanning is available as part of the `trivy config` command. This command scans all configuration files for misconfiguration issues. You can find the details within [misconfiguration scans in the Trivy documentation.](https://trivy.dev/docs/latest/scanner/misconfiguration/) + +Command structure: +``` +trivy config +``` + +The `trivy config` command can scan Terraform configuration, CloudFormation, Dockerfile, Kubernetes manifests, and Helm Charts for misconfiguration. Trivy will compare the configuration found in the file with a set of best practices. + +- If the configuration is following best practices, the check will pass, +- If the configuration does not define the resource of some configuration, Trivy will assume the default configuration for the resource creation is used. In this case, the check might fail. +- If the configuration that has been defined does not follow best practices, the check will fail. + +### Prerequisites +Install Trivy on your local machines. The documentation provides several [different installation options.](https://trivy.dev/docs/latest/getting-started/installation/) +This tutorial will use this example [Terraform tutorial](https://github.com/Cloud-Native-Security/trivy-demo/tree/main/bad_iac/terraform) for terraform misconfiguration scanning with Trivy. + +Git clone the tutorial and cd into the directory: +``` +git clone git@github.com:Cloud-Native-Security/trivy-demo.git +cd bad_iac/terraform +``` +In this case, the folder only contains Terraform configuration files. However, you could scan a directory that contains several different configurations e.g. Kubernetes YAML manifests, Dockerfile, and Terraform. Trivy will then detect the different configuration files and apply the right rules automatically. + +## Different types of `trivy config` scans + +Below are several examples of how the trivy config scan can be used. + +General Terraform scan with trivy: +``` +trivy config +``` + +So if we are already in the directory that we want to scan: +``` +trivy config ./ +``` +### Specify the scan format +The `--format` flag changes the way that Trivy displays the scan result: + +JSON: +``` +trivy config -f json terraform-infra +``` + +Sarif: +``` +trivy config -f sarif terraform-infra +``` + +### Specifying the output location + +The `--output` flag specifies the file location in which the scan result should be saved: + +JSON: +``` +trivy config -f json -o example.json terraform-infra +``` + +Sarif: +``` +trivy config -f sarif -o example.sarif terraform-infra +``` + +### Filtering by severity + +If you are presented with lots and lots of misconfiguration across different files, you might want to filter or the misconfiguration with the highest severity: + +``` +trivy config --severity CRITICAL, MEDIUM terraform-infra +``` + +### Passing tf.tfvars files into `trivy config` scans + +You can pass terraform values to Trivy to override default values found in the Terraform HCL code. More information are provided [in the documentation.](https://trivy.dev/docs/latest/coverage/iac/terraform/#value-overrides) + +``` +trivy config --tf-vars terraform.tfvars ./ +``` +### Custom Checks + +We have lots of examples in the [documentation](https://trivy.dev/docs/latest/scanner/misconfiguration/custom/) on how you can write and pass custom Rego checks into terraform misconfiguration scans. + +## Secret and vulnerability scans + +The `trivy config` command does not perform secret and vulnerability checks out of the box. However, you can specify as part of your `trivy fs` scan that you would like to scan you terraform files for exposed secrets and misconfiguraction through the following flags: + +``` +trivy fs --scanners secret,misconfig ./ +``` + +The `trivy config` command is a sub-command of the `trivy fs` command. You can learn more about this command in the [documentation.](https://trivy.dev/docs/latest/target/filesystem/) + +## Scanning Terraform Plan files + +Instead of scanning your different Terraform resources individually, you could also scan your Terraform Plan file before it is deployed for misconfiguration. This will give you insights into any misconfiguration of your resources as they would become deployed. [Here](https://trivy.dev/docs/latest/coverage/iac/terraform/#terraform) is the link to the documentation. + +Note that you need to be able to create a terraform init and plan without any errors. + +## Using Trivy in your CI/CD pipeline +Similar to tfsec, Trivy can be used either on local developer machines or integrated into your CI/CD pipeline. There are several steps available for different pipelines, including GitHub Actions, Circle CI, GitLab, Travis and more in the tutorials section of the documentation: [https://trivy.dev/docs/latest/tutorials/integrations/](https://trivy.dev/docs/latest/tutorials/integrations/) + + diff --git a/docs/tutorials/overview.md b/docs/tutorials/overview.md new file mode 100644 index 000000000000..f932fe4d6aea --- /dev/null +++ b/docs/tutorials/overview.md @@ -0,0 +1,18 @@ +# Tutorials + +In this section you can find step-by-step guides that help you accomplish specific tasks. + +👈 Please use the side-navigation on the left in order to browse the different topics. + +## Adding tutorials + +You are welcome to create tutorials and showcase them here. Tutorials can be either included in here as full articles, or included as external links under [external community resources][community-resources]. +Before sending a PR, please first create an issue (of kind "Documentation") and describe the suggestion, whether it's an external link or article, and what category it's under. + +Guidelines: + +- Focus on a specific use case. Start by clearly describing the use case and when/who it is relevant for. +- Provide an end-to-end set of instructions. Make sure anyone can easily follow. +- Describe the expected outcome after each step. Include examples as much as possible. + +[community-resources]: additional-resources/community.md \ No newline at end of file diff --git a/docs/tutorials/shell/shell-completion.md b/docs/tutorials/shell/shell-completion.md new file mode 100644 index 000000000000..a9d21e3f2bc4 --- /dev/null +++ b/docs/tutorials/shell/shell-completion.md @@ -0,0 +1,65 @@ +# Enable shell completion + +Below is example steps to enable shell completion feature for `trivy` cli: + +### 1. Know your current shell + +```bash +$ echo $SHELL +/bin/zsh # For this example it is zsh, but will be vary depend on your $SHELL, maybe /bin/bash or /bin/fish +``` + +### 2. Run `completion` command to get sub-commands + +``` bash +$ trivy completion zsh -h +Generate the autocompletion script for the zsh shell. + +If shell completion is not already enabled in your environment you will need +to enable it. You can execute the following once: + + echo "autoload -U compinit; compinit" >> ~/.zshrc + +To load completions in your current shell session: + + source <(trivy completion zsh); compdef _trivy trivy + +To load completions for every new session, execute once: + +#### Linux: + + trivy completion zsh > "${fpath[1]}/_trivy" + +#### macOS: + + trivy completion zsh > $(brew --prefix)/share/zsh/site-functions/_trivy + +You will need to start a new shell for this setup to take effect. +``` + +### 3. Run the sub-commands following the instruction + +```bash +echo "autoload -U compinit; compinit" >> ~/.zshrc +source <(trivy completion zsh); compdef _trivy trivy +trivy completion zsh > "${fpath[1]}/_trivy" +``` + +### 4. Start a new shell and you can see the shell completion + +```bash +$ trivy [tab] +completion -- Generate the autocompletion script for the specified shell +config -- Scan config files for misconfigurations +filesystem -- Scan local filesystem +help -- Help about any command +image -- Scan a container image +kubernetes -- scan kubernetes cluster +module -- Manage modules +plugin -- Manage plugins +repository -- Scan a repository +rootfs -- Scan rootfs +sbom -- Scan SBOM for vulnerabilities +server -- Server mode +version -- Print the version +``` \ No newline at end of file diff --git a/docs/tutorials/signing/vuln-attestation.md b/docs/tutorials/signing/vuln-attestation.md new file mode 100644 index 000000000000..7883810b7f7e --- /dev/null +++ b/docs/tutorials/signing/vuln-attestation.md @@ -0,0 +1,145 @@ +# Vulnerability Scan Record Attestation + +This tutorial details how to + +- Scan container images for vulnerabilities +- Generate an attestation, using Cosign, with and without generating a separate key pair + +#### Prerequisites + +1. [Trivy CLI](../../getting-started/installation.md) installed +2. [Cosign CLI](https://docs.sigstore.dev/cosign/system_config/installation/) installed +3. Ensure that you have access to a container image in a remote container registry that you own/within your account. In this tutorial, we will use DockerHub. + +## Scan Container Image for vulnerabilities + +Scan your container image for vulnerabilities and save the scan result to a scan.json file: +``` +trivy image --ignore-unfixed --format cosign-vuln --output scan.json DockerHubID/imagename:imagetag +``` + +For example: +``` +trivy image --ignore-unfixed --format cosign-vuln --output scan.json anaisurlichs/signed-example:0.1 +``` + +* `--ignore-unfixed`: Ensures only the vulnerabilities, which have a already a fix available, are displayed +* `--output scan.json`: The scan output is saved to a scan.json file instead of being displayed in the terminal. + +Note: Replace the container image with the container image that you want to scan. + +## Option 1: Signing and Generating an attestation without new key pair + +#### Signing + +Sign the container image: +``` +cosign sign DockerHubID/imagename@imageSHA +``` + +The `imageSHA` can be obtained through the following docker command: +``` +docker image ls --digests +``` +The SHA will be displayed next to the image name and tag. + +Note that it is better practice to sign the image SHA rather than the tag as the SHA will remain the same for the particular image that we have signed. + +For example: +``` +cosign sign docker.io/anaisurlichs/signed-example@sha256:c5911ac313e0be82a740bd726dc290e655800a9588424ba4e0558c705d1287fd +``` + +#### Attestation + +The following command generates an attestation for the vulnerability scan and uploads it to the container image used: +``` +cosign attest --predicate scan.json --type vuln docker.io/DockerHubID/imagename:imageSHA +``` + +For example: +``` +cosign attest --predicate scan.json --type vuln docker.io/anaisurlichs/signed-example@sha256:c5911ac313e0be82a740bd726dc290e655800a9588424ba4e0558c705d1287fd +``` + +Note: Replace the container image with the container image that you would like to scan. + +Next, Sigstore will ask you to verify with an account -- Microsoft, GitHub, or Google. + +Once done, the user will be provided with a certificate in the terminal where they ran the command. Example certificate: +``` +-----BEGIN CERTIFICATE----- +MIIC1TCCAlygAwIBAgIUfSXI7xTWSLq4nuygd8YPuhPZlEswCgYIKoZIzj0EAwMw +NzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl +cm1lZGlhdGUwHhcNMjQwMTExMTMzODUzWhcNMjQwMTExMTM0ODUzWjAAMFkwEwYH +KoZIzj0CAQYIKoZIzj0DAQcDQgAETcUNnK76mfo9G3j1c7NN6Vcn6yQPDX5rd3QB +unkHs1Uk59CWv3qm6sUyRNYaATs9zdHAZqLck8G4P/Pj7+GzCKOCAXswggF3MA4G +........ +-----END CERTIFICATE----- +``` + + +## Option 2: Signing and Generating an attestation with a new Cosign key pair + +To generate an attestation for the container image with a separate key pair, we can use Cosign to generate a new key pair: +``` +cosign generate-key-pair  +``` + +This will generate a `cosign.key` and a `cosign.pub` file. The `cosign.key` file is your private key that should be kept confidential as it is used to sign artefacts. However, the `cosign.pub` file contains the information of the corresponding public key. This key can be used by third parties to verify the attestation -- basically that this person who claims to have signed the attestation actually is the one who signed it. + +#### Signing + +Sign the container image: +``` +cosign sign --key cosign.key docker.io/anaisurlichs/signed-example@sha256:c5911ac313e0be82a740bd726dc290e655800a9588424ba4e0558c705d1287fd +``` + +#### Attestation + +To generate the attestation with the specific key pairs, run the following command: +``` +cosign attest --key cosign.key --type vuln --predicate scan.json docker.io/anaisurlichs/signed-example@sha256:c5911ac313e0be82a740bd726dc290e655800a9588424ba4e0558c705d1287fd  +``` + +## Verify the attestation + +### Option 1 -- No separate key pair + +If you have not generated a key pair but received a certificate after the container image was signed, use the following command to verify the attestation: + +``` +cosign verify-attestation --type vuln --certificate-identity Email-used-to-sign --certificate-oidc-issuer='the-issuer-used' docker.io/DockerHubID/imagename:imageSHA +``` + +For example, the command could be like this: +``` +cosign verify-attestation --type vuln --certificate-identity urlichsanais@gmail.com --certificate-oidc-issuer='https://github.com/login/oauth' anaisurlichs/signed-example@sha256:c5911ac313e0be82a740bd726dc290e655800a9588424ba4e0558c705d1287fd +``` + +### Option 2 -- Separate key pair + +If you have used a new cosign key pair, the attestation can be verified through the following command: +``` +cosign verify-attestation --key cosign.pub --type vuln anaisurlichs/signed-example@sha256:c5911ac313e0be82a740bd726dc290e655800a9588424ba4e0558c705d1287fd  +``` + +
+Output + +The output should look similar to the following: +``` +Verification for anaisurlichs/signed-example@sha256:c5911ac313e0be82a740bd726dc290e655800a9588424ba4e0558c705d1287fd -- +The following checks were performed on each of these signatures: + - The cosign claims were validated + - Existence of the claims in the transparency log was verified offline + - The signatures were verified against the specified public key +{"payloadType":"application/vnd.in-toto+json","payload": +``` +
+ +## More information + +See [here][vuln-attestation] for more details. + +[vuln-attestation]: ../../guide/supply-chain/attestation/vuln.md \ No newline at end of file diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 000000000000..1f44a8b86978 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,70 @@ +# End-to-End (E2E) Tests + +## Testing Philosophy + +The E2E tests in this directory are designed to test Trivy's functionality in realistic environments with **external dependencies and network connectivity**. These tests complement unit tests and integration tests by focusing on scenarios that require real external resources. + +### What E2E Tests Should Cover + +E2E tests should focus on functionality that involves: +- **External network connections** (downloading container images, vulnerability databases) +- **External service dependencies** (Docker daemon, registry access, proxy servers) +- **Real-world scenarios** that cannot be easily mocked or simulated +- **Cross-component integration** involving external systems + +### What E2E Tests Should NOT Cover + +E2E tests should **avoid** detailed assertions and comprehensive validation: +- **Detailed JSON output validation** - this should be covered by integration tests +- **Comprehensive vulnerability detection** - this should be covered by unit tests +- **Complex result comparison** - basic functionality verification is sufficient +- **Edge cases and error conditions** - these should be covered by unit tests + +### Testing Approach + +- **Minimal assertions**: Focus on basic functionality rather than detailed output validation +- **External dependencies**: Use real registries, databases, and services where practical +- **Environment isolation**: Each test should use isolated cache and working directories +- **Golden files**: Use -update flag for maintainable output comparison +- **Conditional execution**: Tests should validate required dependencies during setup + +### Dependencies + +- **Docker**: Required for local image scanning tests +- **Internet access**: Required for downloading images and databases + +### Test Execution + +The E2E tests build and execute trivy in isolated temporary directories. When you run `mage test:e2e`, it automatically: +1. Builds trivy in a test-specific temporary directory (via `t.TempDir()`) +2. Adds the temporary directory to the PATH for test execution +3. Runs the E2E tests using the isolated binary + +This approach ensures: +- No pollution of the global environment +- Each test run uses a freshly built binary +- Test isolation between different test runs +- Clean test environment without side effects + +### Running Tests + +```bash +# Run all E2E tests +mage test:e2e + +# Run specific test +go test -v -tags=e2e ./e2e/ -run TestE2E/image_scan + +# Update golden files when output changes +go test -v -tags=e2e ./e2e/ -update +``` + +### Adding New Tests + +When adding new E2E tests: +1. Focus on external dependencies and real-world scenarios +2. Use minimal assertions - verify functionality, not detailed output +3. Use golden files with -update flag for output comparison +4. Validate required dependencies in test setup +5. Use fixed/pinned versions for reproducible results +6. Include clear test documentation explaining the scenario being tested \ No newline at end of file diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go new file mode 100644 index 000000000000..850fdb02b0a6 --- /dev/null +++ b/e2e/e2e_test.go @@ -0,0 +1,94 @@ +//go:build e2e + +package e2e + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + + "github.com/rogpeppe/go-internal/testscript" +) + +var update = flag.Bool("update", false, "update golden files") + +func TestE2E(t *testing.T) { + testscript.Run(t, testscript.Params{ + Dir: "testdata", + Setup: func(env *testscript.Env) error { + return setupTestEnvironment(t, env) + }, + UpdateScripts: *update, + }) +} + +func buildTrivy(t *testing.T) string { + t.Helper() + + tmp := t.TempDir() // Test-specific directory + exe := filepath.Join(tmp, "trivy") + if runtime.GOOS == "windows" { + exe += ".exe" + } + + cmd := exec.Command("go", "build", + "-o", exe, + "../cmd/trivy", + ) + // Prevent environment pollution + cmd.Env = append(os.Environ(), "CGO_ENABLED=0") + + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("Trivy build failed: %v\n%s", err, out) + } + return exe +} + +func setupTestEnvironment(t *testing.T, env *testscript.Env) error { + // Validate Docker availability - fail if not available + if err := validateDockerAvailability(); err != nil { + return fmt.Errorf("Docker validation failed: %v", err) + } + + // Build Trivy once and cache it + trivyExe := buildTrivy(t) + + // Add directory containing trivy to PATH + env.Setenv("PATH", filepath.Dir(trivyExe)+string(os.PathListSeparator)+env.Getenv("PATH")) + + // Set environment variables for test scripts + env.Setenv("TRIVY_DB_DIGEST", "sha256:b4d3718a89a78d4a6b02250953e92fcd87776de4774e64e818c1d0e01c928025") + // Disable VEX notice in test environment + env.Setenv("TRIVY_DISABLE_VEX_NOTICE", "true") + + // Define test image + testImage := "alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b" + env.Setenv("TEST_IMAGE", testImage) + + // Pre-pull the test image to Docker daemon + t.Logf("Pre-pulling test image: %s", testImage) + cmd := exec.Command("docker", "pull", testImage) + if output, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("failed to pull test image: %v\nOutput: %s", err, output) + } + + // Pass through DOCKER_HOST if set + if dockerHost := os.Getenv("DOCKER_HOST"); dockerHost != "" { + env.Setenv("DOCKER_HOST", dockerHost) + } + + return nil +} + +func validateDockerAvailability() error { + cmd := exec.Command("docker", "version") + if err := cmd.Run(); err != nil { + return fmt.Errorf("Docker is not available or not running: %v", err) + } + return nil +} \ No newline at end of file diff --git a/e2e/testdata/image_scan.txtar b/e2e/testdata/image_scan.txtar new file mode 100644 index 000000000000..53963e92562a --- /dev/null +++ b/e2e/testdata/image_scan.txtar @@ -0,0 +1,58 @@ +# Image scan test +# This test verifies that Trivy can scan images from both local Docker daemon and remote registry +# and that both methods produce equivalent results +# +# Image is already pre-pulled in setupTestEnvironment + +# Test 1: Remote image scanning +exec trivy image --image-src remote --cache-dir $WORK/.cache --format table --output remote_result.txt --db-repository mirror.gcr.io/aquasec/trivy-db@sha256:b4d3718a89a78d4a6b02250953e92fcd87776de4774e64e818c1d0e01c928025 --severity HIGH,CRITICAL --no-progress $TEST_IMAGE + +# Verify DB download message appears for remote scan +stderr 'Downloading vulnerability DB...' + +# Test 2: Local Docker daemon scanning +exec trivy image --image-src docker --cache-dir $WORK/.cache --format table --output local_result.txt --db-repository mirror.gcr.io/aquasec/trivy-db@sha256:b4d3718a89a78d4a6b02250953e92fcd87776de4774e64e818c1d0e01c928025 --severity HIGH,CRITICAL --no-progress $TEST_IMAGE + +# Test 3: Exit code testing - scan with exit code for vulnerabilities +! exec trivy image --exit-code 1 --cache-dir $WORK/.cache --format table --db-repository mirror.gcr.io/aquasec/trivy-db@sha256:b4d3718a89a78d4a6b02250953e92fcd87776de4774e64e818c1d0e01c928025 --severity HIGH,CRITICAL --no-progress $TEST_IMAGE + +# Verify all scans completed successfully +! stderr 'FATAL' +exists remote_result.txt +exists local_result.txt + + +# Verify both scans produce equivalent results +cmp remote_result.txt local_result.txt + +# Compare with golden file to ensure expected output format +cmp remote_result.txt image_scan_golden.txt + +-- image_scan_golden.txt -- + +Report Summary + +┌────────────────────────────────────────────────────────────────────────────────┬────────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├────────────────────────────────────────────────────────────────────────────────┼────────┼─────────────────┼─────────┤ +│ alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b │ alpine │ 2 │ - │ +│ (alpine 3.19.1) │ │ │ │ +└────────────────────────────────────────────────────────────────────────────────┴────────┴─────────────────┴─────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + + +alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b (alpine 3.19.1) +============================================================================================== +Total: 2 (HIGH: 2, CRITICAL: 0) + +┌────────────┬───────────────┬──────────┬────────┬───────────────────┬───────────────┬──────────────────────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├────────────┼───────────────┼──────────┼────────┼───────────────────┼───────────────┼──────────────────────────────────────────────────────────┤ +│ libcrypto3 │ CVE-2024-6119 │ HIGH │ fixed │ 3.1.4-r5 │ 3.1.7-r0 │ openssl: Possible denial of service in X.509 name checks │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2024-6119 │ +├────────────┤ │ │ │ │ │ │ +│ libssl3 │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ +└────────────┴───────────────┴──────────┴────────┴───────────────────┴───────────────┴──────────────────────────────────────────────────────────┘ diff --git a/e2e/testdata/proxy_scan.txtar b/e2e/testdata/proxy_scan.txtar new file mode 100644 index 000000000000..d8ced2410e76 --- /dev/null +++ b/e2e/testdata/proxy_scan.txtar @@ -0,0 +1,27 @@ +# Proxy scan test +# This test verifies that Trivy can work through HTTP/HTTPS proxy with self-signed certificates +# Tests both --insecure flag behavior: failure without it, success with it + +# Start mitmdump proxy container on port 18080 +exec docker rm -f mitmproxy-e2e-test +exec docker run -d --name mitmproxy-e2e-test -p 18080:8080 mitmproxy/mitmproxy:latest mitmdump +exec sleep 3 + +# Set proxy environment variables +env HTTPS_PROXY=http://localhost:18080 +env HTTP_PROXY=http://localhost:18080 + +# Test 1: Scan without --insecure flag (should fail due to certificate issues) +! exec trivy image --cache-dir $WORK/.cache --format table --db-repository mirror.gcr.io/aquasec/trivy-db@sha256:b4d3718a89a78d4a6b02250953e92fcd87776de4774e64e818c1d0e01c928025 --severity HIGH,CRITICAL --no-progress alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b + +# Verify certificate error occurred +stderr 'certificate' + +# Test 2: Scan with --insecure flag (should succeed despite certificate issues) +exec trivy image --insecure --cache-dir $WORK/.cache --format table --db-repository mirror.gcr.io/aquasec/trivy-db@sha256:b4d3718a89a78d4a6b02250953e92fcd87776de4774e64e818c1d0e01c928025 --severity HIGH,CRITICAL --no-progress alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b + +# Verify scan completed successfully with --insecure +! stderr 'FATAL' + +# Stop proxy container +exec docker rm -f mitmproxy-e2e-test \ No newline at end of file diff --git a/examples/ignore-policies/advanced.rego b/examples/ignore-policies/advanced.rego new file mode 100644 index 000000000000..33e0a6232ea0 --- /dev/null +++ b/examples/ignore-policies/advanced.rego @@ -0,0 +1,106 @@ +package trivy + +import data.lib.trivy + +default ignore = false + +nvd_v3_vector = v { + v := input.CVSS.nvd.V3Vector +} + +redhat_v3_vector = v { + v := input.CVSS.redhat.V3Vector +} + +# Ignore a vulnerability which requires high privilege +ignore { + nvd_cvss_vector := trivy.parse_cvss_vector_v3(nvd_v3_vector) + nvd_cvss_vector.PrivilegesRequired == "High" + + # Check against RedHat scores as well as NVD + redhat_cvss_vector := trivy.parse_cvss_vector_v3(redhat_v3_vector) + redhat_cvss_vector.PrivilegesRequired == "High" +} + +# Ignore a vulnerability which requires user interaction +ignore { + nvd_cvss_vector := trivy.parse_cvss_vector_v3(nvd_v3_vector) + nvd_cvss_vector.UserInteraction == "Required" + + # Check against RedHat scores as well as NVD + redhat_cvss_vector := trivy.parse_cvss_vector_v3(redhat_v3_vector) + redhat_cvss_vector.UserInteraction == "Required" +} + +ignore { + input.PkgName == "openssl" + + # Split CVSSv3 vector + nvd_cvss_vector := trivy.parse_cvss_vector_v3(nvd_v3_vector) + + # Evaluate Attack Vector + ignore_attack_vectors := {"Physical", "Local"} + nvd_cvss_vector.AttackVector == ignore_attack_vectors[_] +} + +ignore { + input.PkgName == "openssl" + + # Evaluate severity + input.Severity == {"LOW", "MEDIUM", "HIGH"}[_] + + # Evaluate CWE-ID + deny_cwe_ids := { + "CWE-119", # Improper Restriction of Operations within the Bounds of a Memory Buffer + "CWE-200", # Exposure of Sensitive Information to an Unauthorized Actor + } + + count({x | x := input.CweIDs[_]; x == deny_cwe_ids[_]}) == 0 +} + +ignore { + input.PkgName == "bash" + + # Split CVSSv3 vector + nvd_cvss_vector := trivy.parse_cvss_vector_v3(nvd_v3_vector) + + # Evaluate Attack Vector + ignore_attack_vectors := {"Physical", "Local", "Adjacent"} + nvd_cvss_vector.AttackVector == ignore_attack_vectors[_] + + # Evaluate severity + input.Severity == {"LOW", "MEDIUM", "HIGH"}[_] +} + +ignore { + input.PkgName == "django" + + # Split CVSSv3 vector + nvd_cvss_vector := trivy.parse_cvss_vector_v3(nvd_v3_vector) + + # Evaluate Attack Vector + ignore_attack_vectors := {"Physical", "Local"} + nvd_cvss_vector.AttackVector == ignore_attack_vectors[_] + + # Evaluate severity + input.Severity == {"LOW", "MEDIUM"}[_] + + # Evaluate CWE-ID + deny_cwe_ids := { + "CWE-89", # SQL Injection + "CWE-78", # OS Command Injection + } + + count({x | x := input.CweIDs[_]; x == deny_cwe_ids[_]}) == 0 +} + +ignore { + input.PkgName == "jquery" + + # Split CVSSv3 vector + nvd_cvss_vector := trivy.parse_cvss_vector_v3(nvd_v3_vector) + + # Evaluate CWE-ID + deny_cwe_ids := {"CWE-79"} # XSS + count({x | x := input.CweIDs[_]; x == deny_cwe_ids[_]}) == 0 +} diff --git a/examples/ignore-policies/basic.rego b/examples/ignore-policies/basic.rego new file mode 100644 index 000000000000..0cf9f0ea04e7 --- /dev/null +++ b/examples/ignore-policies/basic.rego @@ -0,0 +1,76 @@ +package trivy + +import data.lib.trivy + +default ignore = false + +ignore_pkgs := {"bash", "bind-license", "rpm", "vim", "vim-minimal"} + +ignore_severities := {"LOW", "MEDIUM"} + +nvd_v3_vector = v { + v := input.CVSS.nvd.V3Vector +} + +redhat_v3_vector = v { + v := input.CVSS.redhat.V3Vector +} + +ignore { + input.PkgName == ignore_pkgs[_] +} + +ignore { + input.Severity == ignore_severities[_] +} + +# Ignore a vulnerability which is not remotely exploitable +ignore { + nvd_cvss_vector := trivy.parse_cvss_vector_v3(nvd_v3_vector) + nvd_cvss_vector.AttackVector != "Network" + + redhat_cvss_vector := trivy.parse_cvss_vector_v3(redhat_v3_vector) + redhat_cvss_vector.AttackVector != "Network" +} + +# Ignore a vulnerability which requires high privilege +ignore { + nvd_cvss_vector := trivy.parse_cvss_vector_v3(nvd_v3_vector) + nvd_cvss_vector.PrivilegesRequired == "High" + + redhat_cvss_vector := trivy.parse_cvss_vector_v3(redhat_v3_vector) + redhat_cvss_vector.PrivilegesRequired == "High" +} + +# Ignore a vulnerability which requires user interaction +ignore { + nvd_cvss_vector := trivy.parse_cvss_vector_v3(nvd_v3_vector) + nvd_cvss_vector.UserInteraction == "Required" + + redhat_cvss_vector := trivy.parse_cvss_vector_v3(redhat_v3_vector) + redhat_cvss_vector.UserInteraction == "Required" +} + +# Ignore CSRF +ignore { + # https://cwe.mitre.org/data/definitions/352.html + input.CweIDs[_] == "CWE-352" +} + +# Ignore a license +ignore { + input.PkgName == "alpine-baselayout" + input.Name == "GPL-2.0" +} + +# Ignore loose file license +ignore { + input.Name == "AGPL-3.0" + input.FilePath == "/usr/share/grafana/LICENSE" +} + +# Ignore secret +ignore { + input.RuleID == "aws-access-key-id" + input.Match == "AWS_ACCESS_KEY_ID=\"********************\"" +} diff --git a/examples/ignore-policies/whitelist.rego b/examples/ignore-policies/whitelist.rego new file mode 100644 index 000000000000..51a75c37c72f --- /dev/null +++ b/examples/ignore-policies/whitelist.rego @@ -0,0 +1,13 @@ +package trivy + +import rego.v1 + +allowed_checks := { + "AVD-AWS-0089" +} + +default ignore := false + +ignore if not is_check_allowed + +is_check_allowed if input.AVDID in allowed_checks \ No newline at end of file diff --git a/examples/module/spring4shell/README.md b/examples/module/spring4shell/README.md new file mode 100644 index 000000000000..5f5db83e4c30 --- /dev/null +++ b/examples/module/spring4shell/README.md @@ -0,0 +1,40 @@ +# Spring4Shell module + +This module provides a more in-depth investigation of Spring4Shell detection. + +## Set up + +``` +$ GOOS=wasip1 GOARCH=wasm go build -o spring4shell.wasm -buildmode=c-shared spring4shell.go +$ mkdir -p ~/.trivy/modules +$ cp spring4shell.wasm ~/.trivy/modules +``` + +It is also available in [GHCR][trivy-module-spring4shell]. +You can install it via `trivy module install`. + +```bash +$ trivy module install ghcr.io/aquasecurity/trivy-module-spring4shell +2022-06-13T15:32:21.972+0300 INFO Installing the module from ghcr.io/aquasecurity/trivy-module-spring4shell... +``` + +## Run Trivy + +``` +$ trivy image spring-core-rce-jdk8:latest +2022-05-29T22:35:04.873+0300 INFO Loading spring4shell.wasm... +2022-05-29T22:35:05.348+0300 INFO Registering WASM module: spring4shell@v1 +2022-05-29T22:35:07.124+0300 INFO Module spring4shell: analyzing /app/tomcat/RELEASE-NOTES... +2022-05-29T22:35:07.139+0300 INFO Module spring4shell: analyzing /app/jdk9/release... +2022-05-29T22:37:04.636+0300 INFO Module spring4shell: analyzing /app/jdk9/release... +... +2022-05-29T22:37:08.917+0300 INFO Module spring4shell: Java Version: 8, Tomcat Version: 8.5.77 +2022-05-29T22:37:08.917+0300 INFO Module spring4shell: change CVE-2022-22965 severity from CRITICAL to LOW +``` + +In the above example, the Java version is 8 which is not affected by CVE-2022-22965, so this module changes the severity from CRITICAL to LOW. + +## Note +This module is also used for testing in Trivy. + +[trivy-module-spring4shell]: https://github.com/orgs/aquasecurity/packages/container/package/trivy-module-spring4shell \ No newline at end of file diff --git a/examples/module/spring4shell/spring4shell.go b/examples/module/spring4shell/spring4shell.go new file mode 100644 index 000000000000..67520c37e366 --- /dev/null +++ b/examples/module/spring4shell/spring4shell.go @@ -0,0 +1,289 @@ +//go:generate go build -o spring4shell.wasm -buildmode=c-shared spring4shell.go +//go:build wasip1 + +package main + +import ( + "bufio" + "errors" + "fmt" + "io" + "os" + "regexp" + "strconv" + "strings" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/module/api" + "github.com/aquasecurity/trivy/pkg/module/serialize" + "github.com/aquasecurity/trivy/pkg/module/wasm" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + ModuleVersion = 1 + ModuleName = "spring4shell" + TypeJavaMajor = ModuleName + "/java-major-version" + TypeTomcatVersion = ModuleName + "/tomcat-version" +) + +var ( + tomcatVersionRegex = regexp.MustCompile(`Apache Tomcat Version ([\d.]+)`) +) + +// main is required for Go to compile the Wasm module +func main() {} + +func init() { + wasm.RegisterModule(Spring4Shell{}) +} + +type Spring4Shell struct { + // Cannot define fields as modules can't keep state. +} + +func (Spring4Shell) Version() int { + return ModuleVersion +} + +func (Spring4Shell) Name() string { + return ModuleName +} + +func (Spring4Shell) RequiredFiles() []string { + return []string{ + `\/openjdk-\d+\/release`, // For OpenJDK version + `\/jdk\d+\/release`, // For JDK version + `tomcat\/RELEASE-NOTES`, // For Tomcat version + } +} + +func (s Spring4Shell) Analyze(filePath string) (*serialize.AnalysisResult, error) { + wasm.Info(fmt.Sprintf("analyzing %s...", filePath)) + f, err := os.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + + switch { + case strings.HasSuffix(filePath, "/release"): + return s.parseJavaRelease(f, filePath) + case strings.HasSuffix(filePath, "/RELEASE-NOTES"): + return s.parseTomcatReleaseNotes(f, filePath) + } + + return nil, nil +} + +// Parse a jdk release file like "/usr/local/openjdk-11/release" +func (Spring4Shell) parseJavaRelease(f *os.File, filePath string) (*serialize.AnalysisResult, error) { + var javaVersion string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if !strings.HasPrefix(line, "JAVA_VERSION=") { + continue + } + + ss := strings.Split(line, "=") + if len(ss) != 2 { + return nil, fmt.Errorf("invalid java version: %s", line) + } + + javaVersion = strings.Trim(ss[1], `"`) + } + + if err := scanner.Err(); err != nil { + return nil, err + } + + return &serialize.AnalysisResult{ + CustomResources: []ftypes.CustomResource{ + { + Type: TypeJavaMajor, + FilePath: filePath, + Data: javaVersion, + }, + }, + }, nil +} + +func (Spring4Shell) parseTomcatReleaseNotes(f *os.File, filePath string) (*serialize.AnalysisResult, error) { + b, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + m := tomcatVersionRegex.FindStringSubmatch(string(b)) + if len(m) != 2 { + return nil, errors.New("unknown tomcat release notes format") + } + + return &serialize.AnalysisResult{ + CustomResources: []ftypes.CustomResource{ + { + Type: TypeTomcatVersion, + FilePath: filePath, + Data: m[1], + }, + }, + }, nil +} + +func (Spring4Shell) PostScanSpec() serialize.PostScanSpec { + return serialize.PostScanSpec{ + Action: api.ActionUpdate, // Update severity + IDs: []string{"CVE-2022-22965"}, + } +} + +// PostScan takes results including custom resources and detected CVE-2022-22965. +// +// Example input: +// [ +// +// { +// "Target": "", +// "Class": "custom", +// "CustomResources": [ +// { +// "Type": "spring4shell/java-major-version", +// "FilePath": "/usr/local/openjdk-8/release", +// "Layer": { +// "Digest": "sha256:d7b564a873af313eb2dbcb1ed0d393c57543e3666bdedcbe5d75841d72b1f791", +// "DiffID": "sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1" +// }, +// "Data": "1.8.0_322" +// }, +// { +// "Type": "spring4shell/tomcat-version", +// "FilePath": "/usr/local/tomcat/RELEASE-NOTES", +// "Layer": { +// "Digest": "sha256:59c0978ccb117247fd40d936973c40df89195f60466118c5acc6a55f8ba29f06", +// "DiffID": "sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6" +// }, +// "Data": "8.5.77" +// } +// ] +// }, +// { +// "Target": "Java", +// "Class": "lang-pkgs", +// "Type": "jar", +// "Vulnerabilities": [ +// { +// "VulnerabilityID": "CVE-2022-22965", +// "PkgName": "org.springframework.boot:spring-boot", +// "PkgPath": "usr/local/tomcat/webapps/helloworld.war", +// "InstalledVersion": "2.6.3", +// "FixedVersion": "2.5.12, 2.6.6", +// "Layer": { +// "Digest": "sha256:cc44af318e91e6f9f9bf73793fa4f0639487613f46aa1f819b02b6e8fb5c6c07", +// "DiffID": "sha256:eb769943b91f10a0418f2fc3b4a4fde6c6293be60c37293fcc0fa319edaf27a5" +// }, +// "SeveritySource": "nvd", +// "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-22965", +// "DataSource": { +// "ID": "glad", +// "Name": "GitLab Advisory Database Community", +// "URL": "https://gitlab.com/gitlab-org/advisories-community" +// }, +// "Title": "spring-framework: RCE via Data Binding on JDK 9+", +// "Description": "A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it.", +// "Severity": "CRITICAL", +// "CweIDs": [ +// "CWE-94" +// ], +// "VendorSeverity": { +// "ghsa": 4, +// "nvd": 4, +// "redhat": 3 +// }, +// "CVSS": { +// "ghsa": { +// "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", +// "V3Score": 9.8 +// }, +// "nvd": { +// "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", +// "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", +// "V2Score": 7.5, +// "V3Score": 9.8 +// }, +// "redhat": { +// "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", +// "V3Score": 8.1 +// } +// }, +// "References": [ +// "https://github.com/advisories/GHSA-36p3-wjmg-h94x" +// ], +// "PublishedDate": "2022-04-01T23:15:00Z", +// "LastModifiedDate": "2022-05-19T14:21:00Z" +// } +// ] +// } +// +// ] +func (Spring4Shell) PostScan(results types.Results) (types.Results, error) { + var javaMajorVersion int + var tomcatVersion string + for _, result := range results { + if result.Class != "custom" { + continue + } + + for _, c := range result.CustomResources { + if c.Type == TypeJavaMajor { + v := c.Data.(string) + ss := strings.Split(v, ".") + if len(ss) == 0 || len(ss) < 2 { + wasm.Warn("Invalid Java version: " + v) + continue + } + + ver := ss[0] + if ver == "1" { + ver = ss[1] + } + + var err error + javaMajorVersion, err = strconv.Atoi(ver) + if err != nil { + wasm.Warn("Invalid Java version: " + v) + continue + } + } else if c.Type == TypeTomcatVersion { + tomcatVersion = c.Data.(string) + } + } + } + + wasm.Info(fmt.Sprintf("Java Version: %d, Tomcat Version: %s", javaMajorVersion, tomcatVersion)) + + vulnerable := true + // TODO: version comparison + if tomcatVersion == "10.0.20" || tomcatVersion == "9.0.62" || tomcatVersion == "8.5.78" { + vulnerable = false + } else if javaMajorVersion <= 8 { + vulnerable = false + } + + for i, result := range results { + for j, vuln := range result.Vulnerabilities { + // Look up Spring4Shell + if vuln.VulnerabilityID != "CVE-2022-22965" { + continue + } + + // If it doesn't satisfy any of requirements, the severity should be changed to LOW. + if !strings.Contains(vuln.PkgPath, ".war") || !vulnerable { + wasm.Info(fmt.Sprintf("change %s CVE-2022-22965 severity from CRITICAL to LOW", vuln.PkgName)) + results[i].Vulnerabilities[j].Severity = "LOW" + } + } + } + + return results, nil +} diff --git a/examples/trivy-conf/trivy.yaml b/examples/trivy-conf/trivy.yaml new file mode 100644 index 000000000000..6b6fa490af23 --- /dev/null +++ b/examples/trivy-conf/trivy.yaml @@ -0,0 +1,24 @@ +timeout: 10m +format: json +dependency-tree: true +list-all-pkgs: true +exit-code: 1 +output: result.json +severity: + - HIGH + - CRITICAL +scan: + skip-dirs: + - /lib64 + - /lib + - /usr/lib + - /usr/include + + scanners: + - vuln + - secret +vulnerability: + type: + - os + - library + ignore-unfixed: true \ No newline at end of file diff --git a/go.mod b/go.mod index a8ffbf7d54bf..139bbaaceffc 100644 --- a/go.mod +++ b/go.mod @@ -1,41 +1,530 @@ -module github.com/knqyf263/trivy +module github.com/aquasecurity/trivy -go 1.12 +go 1.25.7 require ( - github.com/briandowns/spinner v0.0.0-20190319032542-ac46072a5a91 - github.com/emirpasic/gods v1.12.0 // indirect - github.com/etcd-io/bbolt v1.3.2 - github.com/fatih/color v1.7.0 - github.com/gliderlabs/ssh v0.1.3 // indirect - github.com/golang/protobuf v1.3.1 // indirect - github.com/knqyf263/fanal v0.0.0-20190507123206-ceab60083e70 - github.com/knqyf263/go-deb-version v0.0.0-20170509080151-9865fe14d09b - github.com/knqyf263/go-dep-parser v0.0.0-20190429154931-c377a5391790 - github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936 - github.com/knqyf263/go-version v1.1.1 - github.com/mattn/go-colorable v0.1.1 // indirect - github.com/mattn/go-runewidth v0.0.4 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/olekukonko/tablewriter v0.0.1 - github.com/stretchr/testify v1.3.0 // indirect - github.com/urfave/cli v1.20.0 - github.com/xanzy/ssh-agent v0.2.1 // indirect - go.etcd.io/bbolt v1.3.2 // indirect - go.uber.org/atomic v1.3.2 // indirect - go.uber.org/multierr v1.1.0 // indirect - go.uber.org/zap v1.9.1 - golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 - golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 // indirect - golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 // indirect - golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373 - gopkg.in/cheggaaa/pb.v1 v1.0.28 - gopkg.in/src-d/go-billy.v4 v4.3.0 // indirect - gopkg.in/src-d/go-git-fixtures.v3 v3.4.0 // indirect - gopkg.in/src-d/go-git.v4 v4.10.0 - gopkg.in/yaml.v2 v2.2.2 + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 + github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 + github.com/BurntSushi/toml v1.6.0 + github.com/CycloneDX/cyclonedx-go v0.10.0 + github.com/GoogleCloudPlatform/docker-credential-gcr/v2 v2.1.31 + github.com/Masterminds/sprig/v3 v3.3.0 + github.com/NYTimes/gziphandler v1.1.1 + github.com/alecthomas/chroma v0.10.0 + github.com/alicebob/miniredis/v2 v2.36.1 + github.com/apparentlymart/go-cidr v1.1.0 + github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 + github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce + github.com/aquasecurity/go-npm-version v0.0.2 + github.com/aquasecurity/go-pep440-version v0.0.1 + github.com/aquasecurity/go-version v0.0.1 + github.com/aquasecurity/iamgo v0.0.10 + github.com/aquasecurity/table v1.11.0 + github.com/aquasecurity/testdocker v0.0.0-20250616060700-ba6845ac6d17 + github.com/aquasecurity/tml v0.6.1 + github.com/aquasecurity/trivy-checks v1.12.2-0.20251219190323-79d27547baf5 + github.com/aquasecurity/trivy-db v0.0.0-20251222105351-a833f47f8f0d + github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48 + github.com/aquasecurity/trivy-kubernetes v0.9.1 + github.com/aws/aws-sdk-go-v2 v1.41.1 + github.com/aws/aws-sdk-go-v2/config v1.32.7 + github.com/aws/aws-sdk-go-v2/credentials v1.19.7 + github.com/aws/aws-sdk-go-v2/service/ec2 v1.281.0 + github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1 + github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 + github.com/aws/smithy-go v1.24.0 + github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c + github.com/bmatcuk/doublestar/v4 v4.10.0 + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/cheggaaa/pb/v3 v3.1.7 + github.com/containerd/containerd/v2 v2.2.1 + github.com/containerd/platforms v1.0.0-rc.2 + github.com/distribution/reference v0.6.0 + github.com/docker/cli v29.3.0+incompatible + github.com/docker/go-connections v0.6.0 + github.com/docker/go-units v0.5.0 + github.com/fatih/color v1.18.0 + github.com/go-git/go-git/v5 v5.16.5 + github.com/go-redis/redis/v8 v8.11.5 + github.com/go-viper/mapstructure/v2 v2.5.0 + github.com/gocsaf/csaf/v3 v3.5.1 + github.com/golang-jwt/jwt/v5 v5.3.1 + github.com/google/go-containerregistry v0.21.3 + github.com/google/go-github/v62 v62.0.0 + github.com/google/licenseclassifier/v2 v2.0.0 + github.com/google/uuid v1.6.0 + github.com/hashicorp/go-getter v1.8.4 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-retryablehttp v0.7.8 + github.com/hashicorp/go-uuid v1.0.3 + github.com/hashicorp/go-version v1.8.0 + github.com/hashicorp/golang-lru/v2 v2.0.7 + github.com/hashicorp/hc-install v0.9.2 + github.com/hashicorp/hcl/v2 v2.24.0 + github.com/hashicorp/terraform-exec v0.24.0 + github.com/in-toto/in-toto-golang v0.10.0 + github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f + github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 + github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 + github.com/knqyf263/go-rpmdb v0.1.1 + github.com/knqyf263/nested v0.0.1 + github.com/kylelemons/godebug v1.1.0 + github.com/magefile/mage v1.15.0 + github.com/masahiro331/go-disk v0.0.0-20240625071113-56c933208fee + github.com/masahiro331/go-ebs-file v0.0.0-20240917043618-e6d2bea5c32e + github.com/masahiro331/go-ext4-filesystem v0.0.0-20240620024024-ca14e6327bbd + github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a + github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd + github.com/masahiro331/go-xfs-filesystem v0.0.0-20231205045356-1b22259a6c44 + github.com/mattn/go-shellwords v1.0.12 + github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/hashstructure/v2 v2.0.2 + github.com/moby/buildkit v0.27.1 + github.com/moby/docker-image-spec v1.3.1 + github.com/moby/moby/client v0.3.0 + github.com/open-policy-agent/opa v1.13.1 + github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.1.1 + github.com/openvex/discovery v0.1.1-0.20240802171711-7c54efc57553 + github.com/openvex/go-vex v0.2.7 + github.com/owenrumney/go-sarif/v2 v2.3.3 + github.com/package-url/packageurl-go v0.1.3 + github.com/quasilyte/go-ruleguard/dsl v0.3.23 + github.com/rogpeppe/go-internal v1.14.1 + github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c + github.com/samber/lo v1.52.0 + github.com/sassoftware/go-rpmutils v0.4.0 + github.com/secure-systems-lab/go-securesystemslib v0.10.0 + github.com/sigstore/rekor v1.5.0 + github.com/sirupsen/logrus v1.9.4 + github.com/sosedoff/gitkit v0.4.0 + github.com/spdx/tools-golang v0.5.7 // v0.5.3 with necessary changes. Can be upgraded to version 0.5.4 after release. + github.com/spf13/cast v1.10.0 + github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 + github.com/stretchr/testify v1.11.1 + github.com/testcontainers/testcontainers-go v0.40.0 + github.com/testcontainers/testcontainers-go/modules/localstack v0.40.0 + github.com/tetratelabs/wazero v1.11.0 + github.com/twitchtv/twirp v8.1.3+incompatible + github.com/xeipuuv/gojsonschema v1.2.0 + github.com/xlab/treeprint v1.2.0 + github.com/zalando/go-keyring v0.2.6 // indirect + github.com/zclconf/go-cty v1.17.0 + github.com/zclconf/go-cty-yaml v1.2.0 + go.etcd.io/bbolt v1.4.3 + golang.org/x/crypto v0.49.0 + golang.org/x/mod v0.34.0 + golang.org/x/net v0.52.0 + golang.org/x/sync v0.20.0 + golang.org/x/term v0.41.0 + golang.org/x/text v0.35.0 + golang.org/x/tools v0.43.0 + golang.org/x/vuln v1.1.4 + golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 + google.golang.org/protobuf v1.36.11 + gopkg.in/yaml.v3 v3.0.1 + helm.sh/helm/v3 v3.20.0 + k8s.io/api v0.35.0 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 + modernc.org/sqlite v1.45.0 ) -replace github.com/genuinetools/reg => github.com/tomoyamachi/reg v0.16.2-0.20190418055600-c6010b917a55 +require ( + github.com/go-ini/ini v1.67.0 + github.com/invopop/jsonschema v0.13.0 + github.com/moby/moby/api v1.54.0 + github.com/nikolalohinski/gonja/v2 v2.6.0 +) -replace github.com/olekukonko/tablewriter => github.com/knqyf263/tablewriter v0.0.2 +require ( + buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250718181942-e35f9b667443.1 // indirect + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1 // indirect + buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250721151928-2b7ae473b098.1 // indirect + buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250721151928-2b7ae473b098.1 // indirect + buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 // indirect + buf.build/go/app v0.1.0 // indirect + buf.build/go/bufplugin v0.9.0 // indirect + buf.build/go/interrupt v1.1.0 // indirect + buf.build/go/protovalidate v0.14.0 // indirect + buf.build/go/protoyaml v0.6.0 // indirect + buf.build/go/spdx v0.2.0 // indirect + buf.build/go/standard v0.1.0 // indirect + cel.dev/expr v0.24.0 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.18.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + cloud.google.com/go/iam v1.5.3 // indirect + cloud.google.com/go/monitoring v1.24.3 // indirect + cloud.google.com/go/storage v1.58.0 // indirect + connectrpc.com/connect v1.18.1 // indirect + connectrpc.com/otelconnect v0.7.2 // indirect + cyphar.com/go-pathrs v0.2.1 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/DataDog/zstd v1.5.5 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect + github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect + github.com/Intevation/gval v1.3.0 // indirect + github.com/Intevation/jsonpath v0.2.1 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Masterminds/squirrel v1.5.4 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect + github.com/ProtonMail/go-crypto v1.3.0 // indirect + github.com/VividCortex/ewma v1.2.0 // indirect + github.com/agext/levenshtein v1.2.3 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/alessio/shellescape v1.4.1 // indirect + github.com/anchore/go-struct-converter v0.1.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/aquasecurity/jfather v0.0.8 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 // indirect + github.com/aws/aws-sdk-go-v2/service/ebs v1.22.1 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 // indirect + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/blang/semver v3.5.1+incompatible // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/briandowns/spinner v1.23.0 // indirect + github.com/bufbuild/buf v1.56.0 // indirect + github.com/bufbuild/protocompile v0.14.1 // indirect + github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.2 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect + github.com/containerd/cgroups/v3 v3.1.2 // indirect + github.com/containerd/containerd v1.7.30 // indirect + github.com/containerd/containerd/api v1.10.0 // indirect + github.com/containerd/continuity v0.4.5 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect + github.com/containerd/fifo v1.1.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/plugin v1.0.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect + github.com/containerd/ttrpc v1.2.7 // indirect + github.com/containerd/typeurl/v2 v2.2.3 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect + github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect + github.com/cyphar/filepath-securejoin v0.6.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect + github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect + github.com/dlclark/regexp2 v1.11.0 // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v28.5.2+incompatible // indirect + github.com/docker/docker-credential-helpers v0.9.5 // indirect + github.com/docker/go-events v0.0.0-20250808211157-605354379745 // indirect + github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/ebitengine/purego v0.8.4 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect + github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect + github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/evanphx/json-patch/v5 v5.6.0 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fvbommel/sortorder v1.1.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-chi/chi/v5 v5.2.4 // indirect + github.com/go-errors/errors v1.4.2 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-gorp/gorp/v3 v3.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/go-openapi/analysis v0.24.1 // indirect + github.com/go-openapi/errors v0.22.6 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/loads v0.23.2 // indirect + github.com/go-openapi/runtime v0.29.2 // indirect + github.com/go-openapi/spec v0.22.3 // indirect + github.com/go-openapi/strfmt v0.25.0 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect + github.com/go-openapi/validate v0.25.1 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.0 // indirect + github.com/gofrs/flock v0.13.0 // indirect + github.com/gofrs/uuid v4.3.1+incompatible // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/cel-go v0.26.0 // indirect + github.com/google/certificate-transparency-go v1.3.2 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-github/v31 v31.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.9 // indirect + github.com/googleapis/gax-go/v2 v2.16.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gosuri/uitable v0.0.4 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect + github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/terraform-json v0.27.1 // indirect + github.com/huandu/xstrings v1.5.0 // indirect + github.com/in-toto/attestation v1.1.2 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jdx/go-netrc v1.0.0 // indirect + github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 // indirect + github.com/jmoiron/sqlx v1.4.0 // indirect + github.com/josephburnett/jd/v2 v2.3.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.18.4 // indirect + github.com/klauspost/pgzip v1.2.6 // indirect + github.com/knqyf263/labeler v0.0.0-20200423181506-7a6e545148c3 // indirect + github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect + github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect + github.com/lestrrat-go/blackmagic v1.0.4 // indirect + github.com/lestrrat-go/dsig v1.0.0 // indirect + github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect + github.com/lestrrat-go/httpcc v1.0.1 // indirect + github.com/lestrrat-go/httprc/v3 v3.0.2 // indirect + github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect + github.com/lestrrat-go/option/v2 v2.0.0 // indirect + github.com/letsencrypt/boulder v0.20251110.0 // indirect + github.com/lib/pq v1.10.9 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect + github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/miekg/dns v1.1.72 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/go-archive v0.2.0 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect + github.com/moby/sys/atomicwriter v0.1.0 // indirect + github.com/moby/sys/mountinfo v0.7.2 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/signal v0.7.1 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect + github.com/moby/term v0.5.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect + github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/oklog/ulid/v2 v2.1.1 // indirect + github.com/opencontainers/runtime-spec v1.3.0 // indirect + github.com/opencontainers/selinux v1.13.1 // indirect + github.com/owenrumney/squealer v1.2.12 // indirect + github.com/pandatix/go-cvss v0.6.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.4 // indirect + github.com/prometheus/procfs v0.17.0 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.57.0 // indirect + github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/rubenv/sql-migrate v1.8.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/samber/oops v1.18.1 // indirect + github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect + github.com/sassoftware/relic v7.2.1+incompatible // indirect + github.com/segmentio/asm v1.2.1 // indirect + github.com/segmentio/encoding v0.5.3 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/shibumi/go-pathspec v1.3.0 // indirect + github.com/shirou/gopsutil/v4 v4.25.6 // indirect + github.com/shopspring/decimal v1.4.0 // indirect + github.com/sigstore/cosign/v2 v2.6.2 // indirect + github.com/sigstore/protobuf-specs v0.5.0 // indirect + github.com/sigstore/rekor-tiles/v2 v2.0.1 // indirect + github.com/sigstore/sigstore v1.10.4 // indirect + github.com/sigstore/sigstore-go v1.1.4 // indirect + github.com/sigstore/timestamp-authority/v2 v2.0.3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect + github.com/stoewer/go-strcase v1.3.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect + github.com/tchap/go-patricia/v2 v2.3.3 // indirect + github.com/theupdateframework/go-tuf v0.7.0 // indirect + github.com/theupdateframework/go-tuf/v2 v2.4.1 // indirect + github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect + github.com/tklauser/go-sysconf v0.3.13 // indirect + github.com/tklauser/numcpus v0.7.0 // indirect + github.com/tonglil/versioning v0.0.0-20170205083536-8b2a4334bd1d // indirect + github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 // indirect + github.com/toqueteos/webbrowser v1.2.0 // indirect + github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c // indirect + github.com/transparency-dev/merkle v0.0.2 // indirect + github.com/ulikunitz/xz v0.5.15 // indirect + github.com/valyala/fastjson v1.6.7 // indirect + github.com/vbatts/tar-split v0.12.2 // indirect + github.com/vektah/gqlparser/v2 v2.5.31 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + github.com/yashtewari/glob-intersection v0.2.0 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.lsp.dev/jsonrpc2 v0.10.0 // indirect + go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 // indirect + go.lsp.dev/protocol v0.12.0 // indirect + go.lsp.dev/uri v0.3.0 // indirect + go.mongodb.org/mongo-driver v1.17.6 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/sdk v1.39.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect + go.opentelemetry.io/proto/otlp v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.1 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools/gopls v0.21.0 // indirect + google.golang.org/api v0.260.0 // indirect + google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b // indirect + google.golang.org/grpc v1.78.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.67.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/apiextensions-apiserver v0.35.0 // indirect + k8s.io/apimachinery v0.35.0 // indirect + k8s.io/apiserver v0.35.0 // indirect + k8s.io/cli-runtime v0.35.0 // indirect + k8s.io/client-go v0.35.0 // indirect + k8s.io/component-base v0.35.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/kubectl v0.35.0 // indirect + modernc.org/libc v1.67.6 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + mvdan.cc/sh/v3 v3.12.0 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect + pluginrpc.com/pluginrpc v0.5.0 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect + sigs.k8s.io/kind v0.19.0 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +// github.com/docker/docker v28 has known security vulnerabilities (CVEs in authz +// and plugin packages). The 28.x branch of moby/moby contains the fixes but no new +// tag has been released. Redirect to the patched commit until a v28.5.3+ is tagged +// or transitive dependencies migrate to github.com/moby/moby sub-modules. +replace github.com/docker/docker => github.com/moby/moby v28.5.3-0.20260325154711-31a1689cb0a1+incompatible + +tool ( + github.com/bufbuild/buf/cmd/buf + github.com/knqyf263/labeler + github.com/magefile/mage + github.com/twitchtv/twirp/protoc-gen-twirp + golang.org/x/tools/cmd/goyacc + golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize + sigs.k8s.io/kind +) diff --git a/go.sum b/go.sum index 878c37c47565..21f7cec4b0c1 100644 --- a/go.sum +++ b/go.sum @@ -1,319 +1,1597 @@ +al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= +al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250718181942-e35f9b667443.1 h1:8kSz6PsTC64z3itQqwMgswSGR/QpB3ShZGycu+zq+58= +buf.build/gen/go/bufbuild/bufplugin/protocolbuffers/go v1.36.6-20250718181942-e35f9b667443.1/go.mod h1:TsmeaGU5CZAF7zRM05vIKgXh56GgwaoMS8X+a77RV5Q= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1 h1:Lg6klmCi3v7VvpqeeLEER9/m5S8y9e9DjhqQnSCNy4k= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.6-20250717185734-6c6e0d3c608e.1/go.mod h1:avRlCjnFzl98VPaeCtJ24RrV/wwHFzB8sWXhj26+n/U= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250721151928-2b7ae473b098.1 h1:icgV8NMRNi31JwLZ8OJQK1HNIX3RTBdhjvpRPJF4fyI= +buf.build/gen/go/bufbuild/registry/connectrpc/go v1.18.1-20250721151928-2b7ae473b098.1/go.mod h1:/MMEAJmz7PEmksjkSxhWXell82FXiG7BLUPBJRmKBsA= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250721151928-2b7ae473b098.1 h1:fgiFo9f0jCni7kb5QxQ78CccZv6WLTH5Iea2B+AvSKY= +buf.build/gen/go/bufbuild/registry/protocolbuffers/go v1.36.6-20250721151928-2b7ae473b098.1/go.mod h1:RsJBKYlgzsbl5LAhIu0cNrPPzBNenMLTAAykRxFidtw= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1 h1:trcsXBDm8exui7mvndZnvworCyBq1xuMnod2N0j79K8= +buf.build/gen/go/pluginrpc/pluginrpc/protocolbuffers/go v1.36.6-20241007202033-cf42259fcbfc.1/go.mod h1:OUbhXurY+VHFGn9FBxcRy8UB7HXk9NvJ2qCgifOMypQ= +buf.build/go/app v0.1.0 h1:nlqD/h0rhIN73ZoiDElprrPiO2N6JV+RmNK34K29Ihg= +buf.build/go/app v0.1.0/go.mod h1:0XVOYemubVbxNXVY0DnsVgWeGkcbbAvjDa1fmhBC+Wo= +buf.build/go/bufplugin v0.9.0 h1:ktZJNP3If7ldcWVqh46XKeiYJVPxHQxCfjzVQDzZ/lo= +buf.build/go/bufplugin v0.9.0/go.mod h1:Z0CxA3sKQ6EPz/Os4kJJneeRO6CjPeidtP1ABh5jPPY= +buf.build/go/interrupt v1.1.0 h1:olBuhgv9Sav4/9pkSLoxgiOsZDgM5VhRhvRpn3DL0lE= +buf.build/go/interrupt v1.1.0/go.mod h1:ql56nXPG1oHlvZa6efNC7SKAQ/tUjS6z0mhJl0gyeRM= +buf.build/go/protovalidate v0.14.0 h1:kr/rC/no+DtRyYX+8KXLDxNnI1rINz0imk5K44ZpZ3A= +buf.build/go/protovalidate v0.14.0/go.mod h1:+F/oISho9MO7gJQNYC2VWLzcO1fTPmaTA08SDYJZncA= +buf.build/go/protoyaml v0.6.0 h1:Nzz1lvcXF8YgNZXk+voPPwdU8FjDPTUV4ndNTXN0n2w= +buf.build/go/protoyaml v0.6.0/go.mod h1:RgUOsBu/GYKLDSIRgQXniXbNgFlGEZnQpRAUdLAFV2Q= +buf.build/go/spdx v0.2.0 h1:IItqM0/cMxvFJJumcBuP8NrsIzMs/UYjp/6WSpq8LTw= +buf.build/go/spdx v0.2.0/go.mod h1:bXdwQFem9Si3nsbNy8aJKGPoaPi5DKwdeEp5/ArZ6w8= +buf.build/go/standard v0.1.0 h1:g98T9IyvAl0vS3Pq8iVk6Cvj2ZiFvoUJRtfyGa0120U= +buf.build/go/standard v0.1.0/go.mod h1:PiqpHz/7ZFq+kqvYhc/SK3lxFIB9N/aiH2CFC2JHIQg= +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.37.4 h1:glPeL3BQJsbF6aIIYfZizMwc5LTYz250bDMjttbBGAU= -cloud.google.com/go v0.37.4/go.mod h1:NHPJ89PdicEuT9hdPXMROBD91xc5uRDxsMtSB16k7hw= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= -github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= +cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= +cloud.google.com/go/auth v0.18.0 h1:wnqy5hrv7p3k7cShwAU/Br3nzod7fxoqG+k0VZ+/Pk0= +cloud.google.com/go/auth v0.18.0/go.mod h1:wwkPM1AgE1f2u6dG443MiWoD8C3BtOywNsUMcUTVDRo= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= +cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= +cloud.google.com/go/kms v1.23.2 h1:4IYDQL5hG4L+HzJBhzejUySoUOheh3Lk5YT4PCyyW6k= +cloud.google.com/go/kms v1.23.2/go.mod h1:rZ5kK0I7Kn9W4erhYVoIRPtpizjunlrfU4fUkumUp8g= +cloud.google.com/go/logging v1.13.1 h1:O7LvmO0kGLaHY/gq8cV7T0dyp6zJhYAOtZPX4TF3QtY= +cloud.google.com/go/logging v1.13.1/go.mod h1:XAQkfkMBxQRjQek96WLPNze7vsOmay9H5PqfsNYDqvw= +cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= +cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= +cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= +cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= +cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo= +cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= +cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= +cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= +connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw= +connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8= +connectrpc.com/otelconnect v0.7.2 h1:WlnwFzaW64dN06JXU+hREPUGeEzpz3Acz2ACOmN8cMI= +connectrpc.com/otelconnect v0.7.2/go.mod h1:JS7XUKfuJs2adhCnXhNHPHLz6oAaZniCJdSF00OZSew= +cyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8= +cyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= +github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= +github.com/Azure/azure-sdk-for-go v68.0.0+incompatible h1:fcYLmCpyNYRnvJbPerq7U0hS+6+I79yEDJBqVNcqUzU= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3 h1:ldKsKtEIblsgsr6mPwrd9yRntoX6uLz/K89wsldwx/k= +github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry v0.2.3/go.mod h1:MAm7bk0oDLmD8yIkvfbxPW04fxzphPyL+7GzwHxOp6Y= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0 h1:E4MgwLBGeVB5f2MdcIVD3ELVAWpr+WD6MUe1i+tM/PA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.4.0/go.mod h1:Y2b/1clN4zsAoUd/pgNAQHjLDnTis/6ROkUfyob6psM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/GoogleCloudPlatform/docker-credential-gcr v1.5.0 h1:wykTgKwhVr2t2qs+xI020s6W5dt614QqCHV+7W9dg64= -github.com/GoogleCloudPlatform/docker-credential-gcr v1.5.0/go.mod h1:BB1eHdMLYEFuFdBlRMb0N7YGVdM5s6Pt0njxgvfbGGs= -github.com/Microsoft/go-winio v0.4.11 h1:zoIOcVf0xPN1tnMVbTtEdI+P8OofVk3NObnwOQ6nK2Q= -github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= -github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= -github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= -github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= -github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= -github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= -github.com/aws/aws-sdk-go v1.19.11 h1:tqaTGER6Byw3QvsjGW0p018U2UOqaJPeJuzoaF7jjoQ= -github.com/aws/aws-sdk-go v1.19.11/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/briandowns/spinner v0.0.0-20190319032542-ac46072a5a91 h1:GMmnK0dvr0Sf0gx3DvTbln0c8DE07B7sPVD9dgHOqo4= -github.com/briandowns/spinner v0.0.0-20190319032542-ac46072a5a91/go.mod h1:hw/JEQBIE+c/BLI4aKM8UU8v+ZqrD3h7HC27kKt8JQU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= +github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= +github.com/CycloneDX/cyclonedx-go v0.10.0 h1:7xyklU7YD+CUyGzSFIARG18NYLsKVn4QFg04qSsu+7Y= +github.com/CycloneDX/cyclonedx-go v0.10.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= +github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= +github.com/GoogleCloudPlatform/docker-credential-gcr/v2 v2.1.31 h1:Zw6Ahy7WYGLrjDnpfwD6uHGqdNuF5/VmMWse8EdrNdM= +github.com/GoogleCloudPlatform/docker-credential-gcr/v2 v2.1.31/go.mod h1:gpV8+EIzfZlF+d64UpPBv7KwaqLhV82bZfOf2wMNl1E= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= +github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= +github.com/Intevation/gval v1.3.0 h1:+Ze5sft5MmGbZrHj06NVUbcxCb67l9RaPTLMNr37mjw= +github.com/Intevation/gval v1.3.0/go.mod h1:xmGyGpP5be12EL0P12h+dqiYG8qn2j3PJxIgkoOHO5o= +github.com/Intevation/jsonpath v0.2.1 h1:rINNQJ0Pts5XTFEG+zamtdL7l9uuE1z0FBA+r55Sw+A= +github.com/Intevation/jsonpath v0.2.1/go.mod h1:WnZ8weMmwAx/fAO3SutjYFU+v7DFreNYnibV7CiaYIw= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= +github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= +github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ= +github.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw= +github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE= +github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow= +github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4= +github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= +github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek= +github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= +github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= +github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= +github.com/alicebob/miniredis/v2 v2.36.1 h1:Dvc5oAnNOr7BIfPn7tF269U8DvRW1dBG2D5n0WrfYMI= +github.com/alicebob/miniredis/v2 v2.36.1/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30= +github.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30xLN2sUZcMXl50hg+PJCIDdJgIvIbVcKqLJ/ZrtM= +github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8= +github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM= +github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s= +github.com/aquasecurity/go-npm-version v0.0.2 h1:6sNIaeW4Hw8Xg51nPoD3VSo/5qmFSu0VL809iehEOvc= +github.com/aquasecurity/go-npm-version v0.0.2/go.mod h1:DXyKqRe2yb83peANMjQr8dGDkHanEgoFv8BOQdWlSUQ= +github.com/aquasecurity/go-pep440-version v0.0.1 h1:8VKKQtH2aV61+0hovZS3T//rUF+6GDn18paFTVS0h0M= +github.com/aquasecurity/go-pep440-version v0.0.1/go.mod h1:3naPe+Bp6wi3n4l5iBFCZgS0JG8vY6FT0H4NGhFJ+i4= +github.com/aquasecurity/go-version v0.0.0-20201107203531-5e48ac5d022a/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= +github.com/aquasecurity/go-version v0.0.1 h1:4cNl516agK0TCn5F7mmYN+xVs1E3S45LkgZk3cbaW2E= +github.com/aquasecurity/go-version v0.0.1/go.mod h1:s1UU6/v2hctXcOa3OLwfj5d9yoXHa3ahf+ipSwEvGT0= +github.com/aquasecurity/iamgo v0.0.10 h1:t/HG/MI1eSephztDc+Rzh/YfgEa+NqgYRSfr6pHdSCQ= +github.com/aquasecurity/iamgo v0.0.10/go.mod h1:GI9IQJL2a+C+V2+i3vcwnNKuIJXZ+HAfqxZytwy+cPk= +github.com/aquasecurity/jfather v0.0.8 h1:tUjPoLGdlkJU0qE7dSzd1MHk2nQFNPR0ZfF+6shaExE= +github.com/aquasecurity/jfather v0.0.8/go.mod h1:Ag+L/KuR/f8vn8okUi8Wc1d7u8yOpi2QTaGX10h71oY= +github.com/aquasecurity/table v1.11.0 h1:SzgCAv7dZcv/gyAyzxorS6OgEk7w/WU5iT2pStIkpl4= +github.com/aquasecurity/table v1.11.0/go.mod h1:eqOmvjjB7AhXFgFqpJUEE/ietg7RrMSJZXyTN8E/wZw= +github.com/aquasecurity/testdocker v0.0.0-20250616060700-ba6845ac6d17 h1:/xWTD1YaNdjvFdClrz5t3GutwVcyJPsMkYzHWlbE3ys= +github.com/aquasecurity/testdocker v0.0.0-20250616060700-ba6845ac6d17/go.mod h1:6kYuX29QyBWHJejvbKkA4yzz8EUX/Fn+GmQ09JAZ5lY= +github.com/aquasecurity/tml v0.6.1 h1:y2ZlGSfrhnn7t4ZJ/0rotuH+v5Jgv6BDDO5jB6A9gwo= +github.com/aquasecurity/tml v0.6.1/go.mod h1:OnYMWY5lvI9ejU7yH9LCberWaaTBW7hBFsITiIMY2yY= +github.com/aquasecurity/trivy-checks v1.12.2-0.20251219190323-79d27547baf5 h1:8HnXyjgCiJwVX1mTKeqdyizd7ZBmXMPL+BMQ5UZd0Nk= +github.com/aquasecurity/trivy-checks v1.12.2-0.20251219190323-79d27547baf5/go.mod h1:hBSA3ziBFwGENK6/PYNIKm6N24SFg0wsv1VXeqPG/3M= +github.com/aquasecurity/trivy-db v0.0.0-20251222105351-a833f47f8f0d h1:mwCxwhDRnW5UkSQdZfekTCjaLyWp1rqfIa6KKRdMDAo= +github.com/aquasecurity/trivy-db v0.0.0-20251222105351-a833f47f8f0d/go.mod h1:B0cbg/BEHbJg2RcS7PLdlbGCzz2TkChcZAiI4oSs0VI= +github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48 h1:JVgBIuIYbwG+ekC5lUHUpGJboPYiCcxiz06RCtz8neI= +github.com/aquasecurity/trivy-java-db v0.0.0-20240109071736-184bd7481d48/go.mod h1:Ldya37FLi0e/5Cjq2T5Bty7cFkzUDwTcPeQua+2M8i8= +github.com/aquasecurity/trivy-kubernetes v0.9.1 h1:bSErQcavKXDh7XMwbGX7Vy//jR5+xhe/bOgfn9G+9lQ= +github.com/aquasecurity/trivy-kubernetes v0.9.1/go.mod h1:dSiJq54hOAkgIPcgCYbeaxykWq3XxEfi/o16N0/BKBM= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ= +github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk= +github.com/aws/aws-sdk-go-v2 v1.41.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= +github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= +github.com/aws/aws-sdk-go-v2/config v1.32.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= +github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= +github.com/aws/aws-sdk-go-v2/service/ebs v1.22.1 h1:SeDJWG4pmye+/aO6k+zt9clPTUy1MXqUmkW8rbAddQg= +github.com/aws/aws-sdk-go-v2/service/ebs v1.22.1/go.mod h1:wRzaW0v9GGQS0h//wpsVDw3Hah5gs5UP+NxoyGeZIGM= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.281.0 h1:9bFLf1b1EQS9JWghInM4cLlfv7bfJCdW5I6dECnWens= +github.com/aws/aws-sdk-go-v2/service/ec2 v1.281.0/go.mod h1:Uy+C+Sc58jozdoL1McQr8bDsEvNFx+/nBY+vpO1HVUY= +github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1 h1:B7f9R99lCF83XlolTg6d6Lvghyto+/VU83ZrneAVfK8= +github.com/aws/aws-sdk-go-v2/service/ecr v1.55.1/go.mod h1:cpYRXx5BkmS3mwWRKPbWSPKmyAUNL7aLWAPiiinwk/U= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.1 h1:U0asSZ3ifpuIehDPkRI2rxHbmFUMplDA2VeR9Uogrmw= +github.com/aws/aws-sdk-go-v2/service/kms v1.49.1/go.mod h1:NZo9WJqQ0sxQ1Yqu1IwCHQFQunTms2MlVgejg16S1rY= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1 h1:C2dUPSnEpy4voWFIq3JNd8gN0Y5vYGDo44eUE58a/p8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.95.1/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= +github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= +github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c h1:C4UZIaS+HAw+X6jGUsoP2ZbM99PuqhCttjomg1yhNAI= +github.com/bitnami/go-version v0.0.0-20231130084017-bb00604d650c/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo= +github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= +github.com/briandowns/spinner v1.23.0 h1:alDF2guRWqa/FOZZYWjlMIx2L6H0wyewPxo/CH4Pt2A= +github.com/briandowns/spinner v1.23.0/go.mod h1:rPG4gmXeN3wQV/TsAY4w8lPdIM6RX3yqeBQJSrbXjuE= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= +github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/bufbuild/buf v1.56.0 h1:Z0eK+npK01FB924rtDVMOJtvBh9c421mYLo9QhUP3pM= +github.com/bufbuild/buf v1.56.0/go.mod h1:uDNMYshCJIXL99OQc71SDeFiDqOse9sSHXPpZlrqElw= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= +github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1 h1:V1xulAoqLqVg44rY97xOR+mQpD2N+GzhMHVwJ030WEU= +github.com/bufbuild/protoplugin v0.0.0-20250218205857-750e09ce93e1/go.mod h1:c5D8gWRIZ2HLWO3gXYTtUfw/hbJyD8xikv2ooPxnklQ= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytecodealliance/wasmtime-go/v39 v39.0.1 h1:RibaT47yiyCRxMOj/l2cvL8cWiWBSqDXHyqsa9sGcCE= +github.com/bytecodealliance/wasmtime-go/v39 v39.0.1/go.mod h1:miR4NYIEBXeDNamZIzpskhJ0z/p8al+lwMWylQ/ZJb4= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= +github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/cheggaaa/pb/v3 v3.1.7 h1:2FsIW307kt7A/rz/ZI2lvPO+v3wKazzE4K/0LtTWsOI= +github.com/cheggaaa/pb/v3 v3.1.7/go.mod h1:/Ji89zfVPeC/u5j8ukD0MBPHt2bzTYp74lQ7KlgFWTQ= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/containerd/continuity v0.0.0-20180921161001-7f53d412b9eb h1:qSMRxG547z/BgQmyVyADxaMADQXVAD9uleP2sQeClbo= -github.com/containerd/continuity v0.0.0-20180921161001-7f53d412b9eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= -github.com/coreos/clair v0.0.0-20180919182544-44ae4bc9590a h1:glxUtT0RlaVJU86kg78ygzfhwW6D+uj5H+aOK01QDgI= -github.com/coreos/clair v0.0.0-20180919182544-44ae4bc9590a/go.mod h1:uXhHPWAoRqw0jJc2f8RrPCwRhIo9otQ8OEWUFtpCiwA= -github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U= -github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= +github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= +github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= +github.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4= +github.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw= +github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= +github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= +github.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o= +github.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM= +github.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk= +github.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU= +github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= +github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= +github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= +github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4= +github.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= +github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= +github.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8= +github.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q= +github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= +github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= +github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= +github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= +github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= +github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= +github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= +github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE= +github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= +github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= +github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= -github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= -github.com/docker/cli v0.0.0-20180920165730-54c19e67f69c h1:QlAVcyoF7QQVN7zV+xYBjgwtRVlRU3WCTCpb2mcqQrM= -github.com/docker/cli v0.0.0-20180920165730-54c19e67f69c/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v0.0.0-20180920194744-16128bbac47f h1:hYf+mPizfvpH6VgIxdntnOmQHd1F1mQUc1oG+j3Ol2g= -github.com/docker/distribution v0.0.0-20180920194744-16128bbac47f/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v0.0.0-20180924202107-a9c061deec0f h1:W4fbqg0JUwy6lLesoJaV/rE0fwAmtdtinMa64X1CEh0= -github.com/docker/docker v0.0.0-20180924202107-a9c061deec0f/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/docker-ce v0.0.0-20180924210327-f53bd8bb8e43 h1:gZ4lWixV821UVbYtr+oz1ZPCHkbtE+ivfmHyZRgyl2Y= -github.com/docker/docker-ce v0.0.0-20180924210327-f53bd8bb8e43/go.mod h1:l1FUGRYBvbjnZ8MS6A2xOji4aZFlY/Qmgz7p4oXH7ac= -github.com/docker/docker-credential-helpers v0.6.1 h1:Dq4iIfcM7cNtddhLVWe9h4QDjsi4OER3Z8voPu/I52g= -github.com/docker/docker-credential-helpers v0.6.1/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= -github.com/docker/go-connections v0.0.0-20180821093606-97c2040d34df/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916 h1:yWHOI+vFjEsAakUTSrtqc/SAHrhSkmn48pqjidZX3QA= -github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= -github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= -github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= -github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= -github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= -github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= -github.com/etcd-io/bbolt v1.3.2 h1:RLRQ0TKLX7DlBRXAJHvbmXL17Q3KNnTBtZ9B6Qo+/Y0= -github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +github.com/dgraph-io/badger/v4 v4.9.0 h1:tpqWb0NewSrCYqTvywbcXOhQdWcqephkVkbBmaaqHzc= +github.com/dgraph-io/badger/v4 v4.9.0/go.mod h1:5/MEx97uzdPUHR4KtkNt8asfI2T4JiEiQlV7kWUo8c0= +github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= +github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= +github.com/digitorus/pkcs7 v0.0.0-20230713084857-e76b763bdc49/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 h1:ge14PCmCvPjpMQMIAH7uKg0lrtNSOdpYsRXlwk3QbaE= +github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352/go.mod h1:SKVExuS+vpu2l9IoOc0RwqE7NYnb0JlcFHFnEJkVDzc= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 h1:lxmTCgmHE1GUYL7P0MlNa00M67axePTq+9nBSGddR8I= +github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7/go.mod h1:GvWntX9qiTlOud0WkQ6ewFm0LPy5JUR1Xo0Ngbd1w6Y= +github.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM= +github.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= +github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/docker/cli v29.1.4+incompatible h1:AI8fwZhqsAsrqZnVv9h6lbexeW/LzNTasf6A4vcNN8M= +github.com/docker/cli v29.1.4+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.3.0+incompatible h1:z3iWveU7h19Pqx7alZES8j+IeFQZ1lhTwb2F+V9SVvk= +github.com/docker/cli v29.3.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= +github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.9.5 h1:EFNN8DHvaiK8zVqFA2DT6BjXE0GzfLOZ38ggPTKePkY= +github.com/docker/docker-credential-helpers v0.9.5/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c= +github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= +github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-events v0.0.0-20250808211157-605354379745 h1:yOn6Ze6IbYI/KAw2lw/83ELYvZh6hvsygTVkD0dzMC4= +github.com/docker/go-events v0.0.0-20250808211157-605354379745/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4= +github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= +github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= +github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= +github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= +github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= +github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= +github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fernet/fernet-go v0.0.0-20180830025343-9eac43b88a5e/go.mod h1:2H9hjfbpSMHwY503FclkV/lZTBh2YlOmLLSda12uL8c= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= -github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0= +github.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/genuinetools/pkg v0.0.0-20180910213200-1c141f661797/go.mod h1:XTcrCYlXPxnxL2UpnwuRn7tcaTn9HAhxFoFJucootk8= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/gliderlabs/ssh v0.1.3 h1:cBU46h1lYQk5f2Z+jZbewFKy+1zzE2aUX/ilcPDAm9M= -github.com/gliderlabs/ssh v0.1.3/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= -github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= +github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= +github.com/glebarez/go-sqlite v1.20.3/go.mod h1:u3N6D/wftiAzIOJtZl6BmedqxmmkDfH3q+ihjqxC9u0= +github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= +github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s= +github.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M= +github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= +github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= +github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= +github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= +github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-openapi/analysis v0.24.1 h1:Xp+7Yn/KOnVWYG8d+hPksOYnCYImE3TieBa7rBOesYM= +github.com/go-openapi/analysis v0.24.1/go.mod h1:dU+qxX7QGU1rl7IYhBC8bIfmWQdX4Buoea4TGtxXY84= +github.com/go-openapi/errors v0.22.6 h1:eDxcf89O8odEnohIXwEjY1IB4ph5vmbUsBMsFNwXWPo= +github.com/go-openapi/errors v0.22.6/go.mod h1:z9S8ASTUqx7+CP1Q8dD8ewGH/1JWFFLX/2PmAYNQLgk= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/loads v0.23.2 h1:rJXAcP7g1+lWyBHC7iTY+WAF0rprtM+pm8Jxv1uQJp4= +github.com/go-openapi/loads v0.23.2/go.mod h1:IEVw1GfRt/P2Pplkelxzj9BYFajiWOtY2nHZNj4UnWY= +github.com/go-openapi/runtime v0.29.2 h1:UmwSGWNmWQqKm1c2MGgXVpC2FTGwPDQeUsBMufc5Yj0= +github.com/go-openapi/runtime v0.29.2/go.mod h1:biq5kJXRJKBJxTDJXAa00DOTa/anflQPhT0/wmjuy+0= +github.com/go-openapi/spec v0.22.3 h1:qRSmj6Smz2rEBxMnLRBMeBWxbbOvuOoElvSvObIgwQc= +github.com/go-openapi/spec v0.22.3/go.mod h1:iIImLODL2loCh3Vnox8TY2YWYJZjMAKYyLH2Mu8lOZs= +github.com/go-openapi/strfmt v0.25.0 h1:7R0RX7mbKLa9EYCTHRcCuIPcaqlyQiWNPTXwClK0saQ= +github.com/go-openapi/strfmt v0.25.0/go.mod h1:nNXct7OzbwrMY9+5tLX4I21pzcmE6ccMGXl3jFdPfn8= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-openapi/validate v0.25.1 h1:sSACUI6Jcnbo5IWqbYHgjibrhhmt3vR6lCzKZnmAgBw= +github.com/go-openapi/validate v0.25.1/go.mod h1:RMVyVFYte0gbSTaZ0N4KmTn6u/kClvAFp+mAVfS/DQc= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= +github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= +github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= +github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= +github.com/go-rod/rod v0.116.2 h1:A5t2Ky2A+5eD/ZJQr1EfsQSe5rms5Xof/qj296e+ZqA= +github.com/go-rod/rod v0.116.2/go.mod h1:H+CMO9SCNc2TJ2WfrG+pKhITz57uGNYU43qYHh438Mg= +github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.8.1/go.mod h1:wS4gNoLalDSJxo/SpngzPQ2BN4uuZVLCmbM4S3vd4+Y= +github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE= +github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gocsaf/csaf/v3 v3.5.1 h1:jTA1fLrK0/JIczPs7itTD53qANoO4tn2VaGvUeitePc= +github.com/gocsaf/csaf/v3 v3.5.1/go.mod h1:pga89lE+iWJm7smTdzYcXuetYUbgY8caXfaIP4BJG98= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= +github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= +github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= +github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= +github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0 h1:28o5sBqPkBsMGnC6b4MvE2TzSr5/AT4c/1fLqVGIwlk= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= +github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.26.0 h1:DPGjXackMpJWH680oGY4lZhYjIameYmR+/6RBdDGmaI= +github.com/google/cel-go v0.26.0/go.mod h1:A9O8OU9rdvrK5MQyrqfIxo1a0u4g3sF8KB6PUIaryMM= +github.com/google/certificate-transparency-go v1.3.2 h1:9ahSNZF2o7SYMaKaXhAumVEzXB2QaayzII9C8rv7v+A= +github.com/google/certificate-transparency-go v1.3.2/go.mod h1:H5FpMUaGa5Ab2+KCYsxg6sELw3Flkl7pGZzWdBoYLXs= +github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= +github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk= -github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I= +github.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM= +github.com/google/go-containerregistry v0.21.3 h1:Xr+yt3VvwOOn/5nJzd7UoOhwPGiPkYW0zWDLLUXqAi4= +github.com/google/go-containerregistry v0.21.3/go.mod h1:D5ZrJF1e6dMzvInpBPuMCX0FxURz7GLq2rV3Us9aPkc= +github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= +github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= +github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4= +github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/licenseclassifier/v2 v2.0.0 h1:1Y57HHILNf4m0ABuMVb6xk4vAJYEUO0gDxNpog0pyeA= +github.com/google/licenseclassifier/v2 v2.0.0/go.mod h1:cOjbdH0kyC9R22sdQbYsFkto4NGCAc+ZSwbeThazEtM= +github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= +github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6 h1:EEHtgt9IwisQ2AZ4pIsMjahcegHh6rmhqxzIRQIyepY= +github.com/google/pprof v0.0.0-20250820193118-f64d9cf942d6/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2 h1:SJ+NtwL6QaZ21U+IrK7d0gGgpjGGvd2kz+FzTHVzdqI= +github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2/go.mod h1:Tv1PlzqC9t8wNnpPdctvtSUOPUUg4SHeE6vR1Ir2hmg= +github.com/google/trillian v1.7.2 h1:EPBxc4YWY4Ak8tcuhyFleY+zYlbCDCa4Sn24e1Ka8Js= +github.com/google/trillian v1.7.2/go.mod h1:mfQJW4qRH6/ilABtPYNBerVJAJ/upxHLX81zxNQw05s= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.9 h1:TOpi/QG8iDcZlkQlGlFUti/ZtyLkliXvHDcyUIMuFrU= +github.com/googleapis/enterprise-certificate-proxy v0.3.9/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.16.0 h1:iHbQmKLLZrexmb0OSsNGTeSTS0HO4YvFOG8g5E4Zd0Y= +github.com/googleapis/gax-go/v2 v2.16.0/go.mod h1:o1vfQjjNZn4+dPnRdl/4ZD7S9414Y4xA+a/6Icj6l14= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= +github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ= +github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A= +github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48= +github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw= +github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM= +github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts= +github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= +github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw= +github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= +github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw= +github.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= +github.com/hashicorp/hc-install v0.9.2 h1:v80EtNX4fCVHqzL9Lg/2xkp62bbvQMnvPQ0G+OmtO24= +github.com/hashicorp/hc-install v0.9.2/go.mod h1:XUqBQNnuT4RsxoxiM9ZaUk0NX8hi2h+Lb6/c0OZnC/I= +github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I= +github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= +github.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE= +github.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM= +github.com/hashicorp/terraform-exec v0.24.0 h1:mL0xlk9H5g2bn0pPF6JQZk5YlByqSqrO5VoaNtAf8OE= +github.com/hashicorp/terraform-exec v0.24.0/go.mod h1:lluc/rDYfAhYdslLJQg3J0oDqo88oGQAdHR+wDqFvo4= +github.com/hashicorp/terraform-json v0.27.1 h1:zWhEracxJW6lcjt/JvximOYyc12pS/gaKSy/wzzE7nY= +github.com/hashicorp/terraform-json v0.27.1/go.mod h1:GzPLJ1PLdUG5xL6xn1OXWIjteQRT2CNT9o/6A9mi9hE= +github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0= +github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef h1:A9HsByNhogrvm9cWb28sjiS3i7tcKCkflWFEkHfuAgM= +github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/in-toto/attestation v1.1.2 h1:MBFn6lsMq6dptQZJBhalXTcWMb/aJy3V+GX3VYj/V1E= +github.com/in-toto/attestation v1.1.2/go.mod h1:gYFddHMZj3DiQ0b62ltNi1Vj5rC879bTmBbrv9CRHpM= +github.com/in-toto/in-toto-golang v0.10.0 h1:+s2eZQSK3WmWfYV85qXVSBfqgawi/5L02MaqA4o/tpM= +github.com/in-toto/in-toto-golang v0.10.0/go.mod h1:wjT4RiyFlLWCmLUJjwB8oZcjaq7HA390aMJcD3xXgmg= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= +github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs= +github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jdx/go-netrc v1.0.0 h1:QbLMLyCZGj0NA8glAhxUpf1zDg6cxnWgMBbjq40W0gQ= +github.com/jdx/go-netrc v1.0.0/go.mod h1:Gh9eFQJnoTNIRHXl2j5bJXA1u84hQWJWgGh569zF3v8= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267 h1:TMtDYDHKYY15rFihtRfck/bfFqNfvcabqvXAFQfAUpY= +github.com/jedisct1/go-minisign v0.0.0-20230811132847-661be99b8267/go.mod h1:h1nSAbGFqGVzn6Jyl1R/iCcBUHN4g+gW1u9CoBTrb9E= +github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP4mnWdTY= +github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= -github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8= -github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect/v2 v2.0.0-beta.2 h1:qZU+rEZUOYTz1Bnhi3xbwn+VxdXkLVeEpAeZzVXLY88= +github.com/jhump/protoreflect/v2 v2.0.0-beta.2/go.mod h1:4tnOYkB/mq7QTyS3YKtVtNrJv4Psqout8HA1U+hZtgM= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24 h1:liMMTbpW34dhU4az1GN0pTPADwNmvoRSeoZ6PItiqnY= +github.com/jmespath/go-jmespath v0.4.1-0.20220621161143-b0104c826a24/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmhodges/clock v1.2.0 h1:eq4kys+NI0PLngzaHEe7AmPT90XMGIEySD1JfV1PDIs= +github.com/jmhodges/clock v1.2.0/go.mod h1:qKjhA7x7u/lQpPB1XAqX1b1lCI/w3/fNuYpI/ZjLynI= +github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= +github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= +github.com/josephburnett/jd/v2 v2.3.0 h1:AyNT0zSStJ2j28zutWDO4fkc95JoICryWQRmDTRzPTQ= +github.com/josephburnett/jd/v2 v2.3.0/go.mod h1:0I5+gbo7y8diuajJjm79AF44eqTheSJy1K7DSbIUFAQ= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/knqyf263/berkeleydb v0.0.0-20190501065933-fafe01fb9662/go.mod h1:bu1CcN4tUtoRcI/B/RFHhxMNKFHVq/c3SV+UTyduoXg= -github.com/knqyf263/fanal v0.0.0-20190506110705-2b5cb3000ff6 h1:iSztZNfwEPMN2CvUX1SxNEclRZn+rwRMdsnAegxRJk4= -github.com/knqyf263/fanal v0.0.0-20190506110705-2b5cb3000ff6/go.mod h1:OiuWIClssf5WzbMcR8lfspdBVaP+vRQndY4kHeFgrDw= -github.com/knqyf263/fanal v0.0.0-20190507123206-ceab60083e70 h1:L27WBZxk7N70WilG91kgvs0EnV+JVCoOTsNQa8tMBJs= -github.com/knqyf263/fanal v0.0.0-20190507123206-ceab60083e70/go.mod h1:OiuWIClssf5WzbMcR8lfspdBVaP+vRQndY4kHeFgrDw= -github.com/knqyf263/go-deb-version v0.0.0-20170509080151-9865fe14d09b h1:DiDMmSwuY27PJxA2Gs0+uI/bQ/ehKARaGXRdlp+wFis= -github.com/knqyf263/go-deb-version v0.0.0-20170509080151-9865fe14d09b/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao= -github.com/knqyf263/go-dep-parser v0.0.0-20190429154931-c377a5391790 h1:c02gG0yRNr25lcLOH+678SuuxxMUq36i48PQnmAweWk= -github.com/knqyf263/go-dep-parser v0.0.0-20190429154931-c377a5391790/go.mod h1:CtT+dtv38jSz5EYYCX21LgtVXP+J3soF2fzQT8lHCfY= -github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936 h1:HDjRqotkViMNcGMGicb7cgxklx8OwnjtCBmyWEqrRvM= -github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= -github.com/knqyf263/go-rpmdb v0.0.0-20190501070121-10a1c42a10dc/go.mod h1:MrSSvdMpTSymaQWk1yFr9sxFSyQmKMj6jkbvGrchBV8= -github.com/knqyf263/go-version v1.1.1 h1:+MpcBC9b7rk5ihag8Y/FLG8get1H2GjniwKQ+9DxI2o= -github.com/knqyf263/go-version v1.1.1/go.mod h1:0tBvHvOBSf5TqGNcY+/ih9o8qo3R16iZCpB9rP0D3VM= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= +github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= +github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg= +github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8= +github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 h1:dWzdsqjh1p2gNtRKqNwuBvKqMNwnLOPLzVZT1n6DK7s= +github.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23/go.mod h1:lUaIXCWzf7BRKTY5iEcrYy1TfgbYLYVIS/B2vPkJzOc= +github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075 h1:aC6MEAs3PE3lWD7lqrJfDxHd6hcced9R4JTZu85cJwU= +github.com/knqyf263/go-rpm-version v0.0.0-20220614171824-631e686d1075/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= +github.com/knqyf263/go-rpmdb v0.1.1 h1:oh68mTCvp1XzxdU7EfafcWzzfstUZAEa3MW0IJye584= +github.com/knqyf263/go-rpmdb v0.1.1/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww= +github.com/knqyf263/labeler v0.0.0-20200423181506-7a6e545148c3 h1:AvRd4VDhlo8/opGjS79zaVmIfoZzEOtBV1PvSRUkC7o= +github.com/knqyf263/labeler v0.0.0-20200423181506-7a6e545148c3/go.mod h1:DfoJpLAw0HeB4cYJFg1S8LqtYDvSKv3rh3wGAFwv5Bg= github.com/knqyf263/nested v0.0.1 h1:Sv26CegUMhjt19zqbBKntjwESdxe5hxVPSk0+AKjdUc= github.com/knqyf263/nested v0.0.1/go.mod h1:zwhsIhMkBg90DTOJQvxPkKIypEHPYkgWHs4gybdlUmk= -github.com/knqyf263/tablewriter v0.0.2 h1:lGaBruL/oJt8FAlMVy9KU0oQ/6NXAJjvK7wBgZyc+Og= -github.com/knqyf263/tablewriter v0.0.2/go.mod h1:NDOJQAZxabBL3e13jQVktkvbr6bxXXPon8Lyh7fRPPc= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= -github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= -github.com/mattn/go-colorable v0.1.1 h1:G1f5SKeVxmagw/IyvzvtZE4Gybcc4Tr1tf7I8z0XgOg= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-isatty v0.0.5 h1:tHXDdz1cpzGaovsTB+TVB8q90WEokoVmfMqoVcrLUgw= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= +github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= +github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= +github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= +github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38= +github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY= +github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU= +github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= +github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= +github.com/lestrrat-go/httprc/v3 v3.0.2 h1:7u4HUaD0NQbf2/n5+fyp+T10hNCsAnwKfqn4A4Baif0= +github.com/lestrrat-go/httprc/v3 v3.0.2/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0= +github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk= +github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU= +github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss= +github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg= +github.com/letsencrypt/boulder v0.20251110.0 h1:J8MnKICeilO91dyQ2n5eBbab24neHzUpYMUIOdOtbjc= +github.com/letsencrypt/boulder v0.20251110.0/go.mod h1:ogKCJQwll82m7OVHWyTuf8eeFCjuzdRQlgnZcCl0V+8= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI= +github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= +github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/masahiro331/go-disk v0.0.0-20240625071113-56c933208fee h1:cgm8mE25x5XXX2oyvJDlyJ72K+rDu/4ZCYce2worNb8= +github.com/masahiro331/go-disk v0.0.0-20240625071113-56c933208fee/go.mod h1:rojbW5tVhH1cuVYFKZS+QX+VGXK45JVsRO+jW92kkKM= +github.com/masahiro331/go-ebs-file v0.0.0-20240917043618-e6d2bea5c32e h1:nCgF1JEYIS8KNuJtIeUrmjjhktIMKWNmASZqwK2ynu0= +github.com/masahiro331/go-ebs-file v0.0.0-20240917043618-e6d2bea5c32e/go.mod h1:XFWPTlAcEL733RUjbr0QBybdt6oK2DH7LZk8id2qtd4= +github.com/masahiro331/go-ext4-filesystem v0.0.0-20240620024024-ca14e6327bbd h1:JEIW94K3spsvBI5Xb9PGhKSIza9/jxO1lF30tPCAJlA= +github.com/masahiro331/go-ext4-filesystem v0.0.0-20240620024024-ca14e6327bbd/go.mod h1:3XMMY1M486mWGTD13WPItg6FsgflQR72ZMAkd+gsyoQ= +github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a h1:eLvAzVoRfHEOl64OxFhepPf3vj7SKvXY/tFc3BS0b7s= +github.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a/go.mod h1:jZ3F25l7DbD7l7DcA8aj7eo1EZ84nbzcQHBB4lCSrI8= +github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd h1:Y30EzvuoVp97b0unb/GOFXzBUKRXZXUN2e0wYmvC+ic= +github.com/masahiro331/go-vmdk-parser v0.0.0-20221225061455-612096e4bbbd/go.mod h1:5f7mCJGW9cJb8SDn3z8qodGxpMCOo8d/2nls/tiwRrw= +github.com/masahiro331/go-xfs-filesystem v0.0.0-20231205045356-1b22259a6c44 h1:VmSjn0UCyfXUNdePDr7uM/uZTnGSp+mKD5+cYkEoLx4= +github.com/masahiro331/go-xfs-filesystem v0.0.0-20231205045356-1b22259a6c44/go.mod h1:QKBZqdn6teT0LK3QhAf3K6xakItd1LonOShOEC44idQ= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= +github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= +github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI= +github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/olekukonko/tablewriter v0.0.1 h1:b3iUnf1v+ppJiOfNX4yxxqfWKMQPZR5yoh8urCTFX88= -github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c h1:cqn374mizHuIWj+OSJCajGr/phAmuMug9qIX3l9CflE= +github.com/mitchellh/mapstructure v1.5.1-0.20231216201459-8508981c8b6c/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/buildkit v0.27.1 h1:qlIWpnZzqCkrYiGkctM1gBD/YZPOJTjtUdRBlI0oBOU= +github.com/moby/buildkit v0.27.1/go.mod h1:99qLrCrIAFgEOiFnCi9Y0Wwp6/qA7QvZ3uq/6wF0IsI= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.2.0 h1:zg5QDUM2mi0JIM9fdQZWC7U8+2ZfixfTYoHL7rWUcP8= +github.com/moby/go-archive v0.2.0/go.mod h1:mNeivT14o8xU+5q1YnNrkQVpK+dnNe/K6fHqnTg4qPU= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/moby v28.5.3-0.20260325154711-31a1689cb0a1+incompatible h1:CV8L2AzOqgfTFeQYRNWJ7ViUKlTTd1e1K2nhbiT2Euo= +github.com/moby/moby v28.5.3-0.20260325154711-31a1689cb0a1+incompatible/go.mod h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc= +github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w= +github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= +github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= +github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM= +github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ= +github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= +github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= +github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0= +github.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= +github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM= +github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= +github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nikolalohinski/gonja/v2 v2.6.0 h1:IKGrHn3OnegccenPl4w4xf0gRkTqILx4R5X3KxZhPSY= +github.com/nikolalohinski/gonja/v2 v2.6.0/go.mod h1:UIzXPVuOsr5h7dZ5DUbqk3/Z7oFA/NLGQGMjqT4L2aU= +github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 h1:Up6+btDp321ZG5/zdSLo48H9Iaq0UQGthrhWC6pCxzE= +github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481/go.mod h1:yKZQO8QE2bHlgozqWDiRVqTFlLQSj30K/6SAK8EeYFw= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.1 h1:suPZ4ARWLOJLegGFiZZ1dFAkqzhMjL3J1TzI+5wHz8s= +github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.2/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2 h1:QhPf3A2AZW3tTGvHPg0TA+CR3oHbVLlXUhlghqISp1I= -github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= -github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= -github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y= -github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= -github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= -github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= -github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= -github.com/peterhellberg/link v1.0.0 h1:mUWkiegowUXEcmlb+ybF75Q/8D2Y0BjZtR8cxoKhaQo= -github.com/peterhellberg/link v1.0.0/go.mod h1:gtSlOT4jmkY8P47hbTc8PTgiDDWpdPbFYl75keYyBB8= -github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/open-policy-agent/opa v1.13.1 h1:2odxAcL3L0GNTlsuDcoguxViGxQxlpGL6zR8jdJjID8= +github.com/open-policy-agent/opa v1.13.1/go.mod h1:M3Asy9yp1YTusUU5VQuENDe92GLmamIuceqjw+C8PHY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg= +github.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE= +github.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg= +github.com/openvex/discovery v0.1.1-0.20240802171711-7c54efc57553 h1:c4u0GIH0w2Q57Pm2Oldrq6EiHFnLCCnRs98A+ggj/YQ= +github.com/openvex/discovery v0.1.1-0.20240802171711-7c54efc57553/go.mod h1:z4b//Qi7p7zcM/c41ogeTy+/nqfMbbeYnfZ+EMCTCD0= +github.com/openvex/go-vex v0.2.7 h1:/pN3bqvS4QOc6WkkL0hbKzJuAtsUD9vmvk9IZkzD3Zc= +github.com/openvex/go-vex v0.2.7/go.mod h1:ZyQC3NXl9jjS53JOpBG3LAUXySkW8IlJ/GIhsnf5D54= +github.com/owenrumney/go-sarif v1.1.1/go.mod h1:dNDiPlF04ESR/6fHlPyq7gHKmrM0sHUvAGjsoh8ZH0U= +github.com/owenrumney/go-sarif/v2 v2.3.3 h1:ubWDJcF5i3L/EIOER+ZyQ03IfplbSU1BLOE26uKQIIU= +github.com/owenrumney/go-sarif/v2 v2.3.3/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= +github.com/owenrumney/squealer v1.2.12 h1:6VQxQ323q7q0eKNP5p7MX4nbTW1z8wK44YvyHLwDcx0= +github.com/owenrumney/squealer v1.2.12/go.mod h1:yBdddxW+31mPHXgjOAYiqCtK1/f1S3o5Jq6S6vZGtxc= +github.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs= +github.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0= +github.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITGI= +github.com/pandatix/go-cvss v0.6.2/go.mod h1:jDXYlQBZrc8nvrMUVVvTG8PhmuShOnKrxP53nOFkt8Q= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= +github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= +github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.0.0-20180924113449-f69c853d21c1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829 h1:D+CiwcpGTW6pL6bv6KI3KbyEyCKyS+1JWS2h8PNDnGA= -github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f h1:BVwpUVJDADN2ufcGik7W992pyps0wZ888b/y9GXcLTU= -github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= -github.com/prometheus/common v0.2.0 h1:kUZDBDTdBVBYBj5Tmh2NZLlF60mfjA27rM34b+cVwNU= -github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20180920065004-418d78d0b9a7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1 h1:/K3IL0Z1quvmJ7X0A1AwNEK7CRkVK3YwfOU/QAL4WGg= -github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= -github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= -github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= +github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.4 h1:yR3NqWO1/UyO1w2PhUvXlGQs/PtFmoveVO0KZ4+Lvsc= +github.com/prometheus/common v0.67.4/go.mod h1:gP0fq6YjjNCLssJCQp0yk4M8W6ikLURwkdd/YKtTbyI= +github.com/prometheus/otlptranslator v0.0.2 h1:+1CdeLVrRQ6Psmhnobldo0kTp96Rj80DRXRd5OSnMEQ= +github.com/prometheus/otlptranslator v0.0.2/go.mod h1:P8AwMgdD7XEr6QRUJ2QWLpiAZTgTE2UYgjlu3svompI= +github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0= +github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw= +github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= +github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE= +github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9 h1:bsUq1dX0N8AOIL7EB/X911+m4EHsnWEHeJ0c+3TTBrg= +github.com/rcrowley/go-metrics v0.0.0-20250401214520-65e299d6c5c9/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3 h1:1/BDligzCa40GTllkDnY3Y5DTHuKCONbB2JcRyIfl20= +github.com/redis/go-redis/extra/rediscmd/v9 v9.5.3/go.mod h1:3dZmcLn3Qw6FLlWASn1g4y+YO9ycEFUOM+bhBmzLVKQ= +github.com/redis/go-redis/extra/redisotel/v9 v9.5.3 h1:kuvuJL/+MZIEdvtb/kTBRiRgYaOmx1l+lYJyVdrRUOs= +github.com/redis/go-redis/extra/redisotel/v9 v9.5.3/go.mod h1:7f/FMrf5RRRVHXgfk7CzSVzXHiWeuOQUu2bsVqWoa+g= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0= +github.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c h1:8gOLsYwaY2JwlTMT4brS5/9XJdrdIbmk2obvQ748CC0= +github.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c/go.mod h1:kwM/7r/rVluTE8qJbHAffduuqmSv4knVQT2IajGvSiA= +github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= +github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= +github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/oops v1.18.1 h1:qjhZbqbdyhWBKntkY8sxrDNKA8b4c5VHlmI1rli7X7M= +github.com/samber/oops v1.18.1/go.mod h1:xYqvimigkKV70HyLXiBZJFpIWi2CGcc6Xx7eV+2HycI= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= +github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= +github.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg= +github.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI= +github.com/sassoftware/relic v7.2.1+incompatible h1:Pwyh1F3I0r4clFJXkSI8bOyJINGqpgjJU3DYAZeI05A= +github.com/sassoftware/relic v7.2.1+incompatible/go.mod h1:CWfAxv73/iLZ17rbyhIEq3K9hs5w6FpNMdUT//qR+zk= +github.com/sassoftware/relic/v7 v7.6.2 h1:rS44Lbv9G9eXsukknS4mSjIAuuX+lMq/FnStgmZlUv4= +github.com/sassoftware/relic/v7 v7.6.2/go.mod h1:kjmP0IBVkJZ6gXeAu35/KCEfca//+PKM6vTAsyDPY+k= +github.com/secure-systems-lab/go-securesystemslib v0.10.0 h1:l+H5ErcW0PAehBNrBxoGv1jjNpGYdZ9RcheFkB2WI14= +github.com/secure-systems-lab/go-securesystemslib v0.10.0/go.mod h1:MRKONWmRoFzPNQ9USRF9i1mc7MvAVvF1LlW8X5VWDvk= +github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= +github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= +github.com/segmentio/encoding v0.5.3 h1:OjMgICtcSFuNvQCdwqMCv9Tg7lEOXGwm1J5RPQccx6w= +github.com/segmentio/encoding v0.5.3/go.mod h1:HS1ZKa3kSN32ZHVZ7ZLPLXWvOVIiZtyJnO1gPH1sKt0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= +github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs= +github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c= +github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sigstore/cosign/v2 v2.6.2 h1:mi6EAUJoPZ+yuyooU7m06a1DZWJouwYgh9a67L9tm2M= +github.com/sigstore/cosign/v2 v2.6.2/go.mod h1:g+P/LgYyJkC85WGGDho7yySl3C6xTJzzpLm21ZV+E6s= +github.com/sigstore/protobuf-specs v0.5.0 h1:F8YTI65xOHw70NrvPwJ5PhAzsvTnuJMGLkA4FIkofAY= +github.com/sigstore/protobuf-specs v0.5.0/go.mod h1:+gXR+38nIa2oEupqDdzg4qSBT0Os+sP7oYv6alWewWc= +github.com/sigstore/rekor v1.5.0 h1:rL7SghHd5HLCtsCrxw0yQg+NczGvM75EjSPPWuGjaiQ= +github.com/sigstore/rekor v1.5.0/go.mod h1:D7JoVCUkxwQOpPDNYeu+CE8zeBC18Y5uDo6tF8s2rcQ= +github.com/sigstore/rekor-tiles/v2 v2.0.1 h1:1Wfz15oSRNGF5Dzb0lWn5W8+lfO50ork4PGIfEKjZeo= +github.com/sigstore/rekor-tiles/v2 v2.0.1/go.mod h1:Pjsbhzj5hc3MKY8FfVTYHBUHQEnP0ozC4huatu4x7OU= +github.com/sigstore/sigstore v1.10.4 h1:ytOmxMgLdcUed3w1SbbZOgcxqwMG61lh1TmZLN+WeZE= +github.com/sigstore/sigstore v1.10.4/go.mod h1:tDiyrdOref3q6qJxm2G+JHghqfmvifB7hw+EReAfnbI= +github.com/sigstore/sigstore-go v1.1.4 h1:wTTsgCHOfqiEzVyBYA6mDczGtBkN7cM8mPpjJj5QvMg= +github.com/sigstore/sigstore-go v1.1.4/go.mod h1:2U/mQOT9cjjxrtIUeKDVhL+sHBKsnWddn8URlswdBsg= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.3 h1:D/FRl5J9UYAJPGZRAJbP0dH78pfwWnKsyCSBwFBU8CI= +github.com/sigstore/sigstore/pkg/signature/kms/aws v1.10.3/go.mod h1:2GIWuNvTRMvrzd0Nl8RNqxrt9H7X0OBStwOSzGYRjYw= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.3 h1:k5VMLf/ms7hh6MLgVoorM0K+hSMwZLXoywlxh4CXqP8= +github.com/sigstore/sigstore/pkg/signature/kms/azure v1.10.3/go.mod h1:S1Bp3dmP7jYlXcGLAxG81wRbE01NIZING8ZIy0dJlAI= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.3 h1:AVWs0E6rVZMoDTE0Iyezrpo1J6RlI5B4QZhAC4FLE30= +github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.10.3/go.mod h1:nxQYF0D6u7mVtiP1azj1YVDIrtz7S0RYCVTqUG8IcCk= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.3 h1:lJSdaC/aOlFHlvqmmV696n1HdXLMLEKGwpNZMV0sKts= +github.com/sigstore/sigstore/pkg/signature/kms/hashivault v1.10.3/go.mod h1:b2rV9qPbt/jv/Yy75AIOZThP8j+pe1ZdLEjOwmjPdoA= +github.com/sigstore/timestamp-authority/v2 v2.0.3 h1:sRyYNtdED/ttLCMdaYnwpf0zre1A9chvjTnCmWWxN8Y= +github.com/sigstore/timestamp-authority/v2 v2.0.3/go.mod h1:mDaHxkt3HmZYoIlwYj4QWo0RUr7VjYU52aVO5f5Qb3I= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/sosedoff/gitkit v0.4.0 h1:opyQJ/h9xMRLsz2ca/2CRXtstePcpldiZN8DpLLF8Os= +github.com/sosedoff/gitkit v0.4.0/go.mod h1:V3EpGZ0nvCBhXerPsbDeqtyReNb48cwP9KtkUYTKT5I= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg= +github.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v0.0.0-20170130214531-35136c09d8da/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= +github.com/stoewer/go-strcase v1.3.1 h1:iS0MdW+kVTxgMoE1LAZyMiYJFKlOzLooE4MxjirtkAs= +github.com/stoewer/go-strcase v1.3.1/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/tomoyamachi/reg v0.16.2-0.20190418055600-c6010b917a55 h1:O7Xl4zpk6zjYnwxUd7lubrx7xdzQ+PqfTgaxLE9nF+o= -github.com/tomoyamachi/reg v0.16.2-0.20190418055600-c6010b917a55/go.mod h1:12Fe9EIvK3dG/qWhNk5e9O96I8SGmCKLsJ8GsXUbk+Y= -github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= -github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= -github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= -go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= -go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= -go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20180910181607-0e37d006457b/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDdvS342BElfbETmL1Aiz3i2t0zfRj16Hs= +github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= +github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= +github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo= +github.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= +github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU= +github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY= +github.com/testcontainers/testcontainers-go/modules/k3s v0.37.0 h1:lqwknybf56hBLi2YsKs01VLSUK8qXnIcG1FM/6/L5qI= +github.com/testcontainers/testcontainers-go/modules/k3s v0.37.0/go.mod h1:RIsXAxAUiaDNfsGsYcZB1TyDn2mqy52lO0HrGFts8cs= +github.com/testcontainers/testcontainers-go/modules/localstack v0.40.0 h1:b+lN2Ch4J/6EwqB+Af+QQbSfv4sFGetHlBHpXi+1yJU= +github.com/testcontainers/testcontainers-go/modules/localstack v0.40.0/go.mod h1:8LuTSboTo2MJKFKV5xH6z4ZH1s3jhRJWwvtPJzKogj4= +github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= +github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= +github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= +github.com/theupdateframework/go-tuf v0.7.0/go.mod h1:uEB7WSY+7ZIugK6R1hiBMBjQftaFzn7ZCDJcp1tCUug= +github.com/theupdateframework/go-tuf/v2 v2.4.1 h1:K6ewW064rKZCPkRo1W/CTbTtm/+IB4+coG1iNURAGCw= +github.com/theupdateframework/go-tuf/v2 v2.4.1/go.mod h1:Nex2enPVYDFCklrnbTzl3OVwD7fgIAj0J5++z/rvCj8= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0 h1:N9UxlsOzu5mttdjhxkDLbzwtEecuXmlxZVo/ds7JKJI= +github.com/tink-crypto/tink-go-awskms/v2 v2.1.0/go.mod h1:PxSp9GlOkKL9rlybW804uspnHuO9nbD98V/fDX4uSis= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0 h1:3B9i6XBXNTRspfkTC0asN5W0K6GhOSgcujNiECNRNb0= +github.com/tink-crypto/tink-go-gcpkms/v2 v2.2.0/go.mod h1:jY5YN2BqD/KSCHM9SqZPIpJNG/u3zwfLXHgws4x2IRw= +github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0 h1:6nAX1aRGnkg2SEUMwO5toB2tQkP0Jd6cbmZ/K5Le1V0= +github.com/tink-crypto/tink-go-hcvault/v2 v2.3.0/go.mod h1:HOC5NWW1wBI2Vke1FGcRBvDATkEYE7AUDiYbXqi2sBw= +github.com/tink-crypto/tink-go/v2 v2.6.0 h1:+KHNBHhWH33Vn+igZWcsgdEPUxKwBMEe0QC60t388v4= +github.com/tink-crypto/tink-go/v2 v2.6.0/go.mod h1:2WbBA6pfNsAfBwDCggboaHeB2X29wkU8XHtGwh2YIk8= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= +github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= +github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= +github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4= +github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY= +github.com/tonglil/versioning v0.0.0-20170205083536-8b2a4334bd1d h1:3H+wrTJTy3PVEeCyrjiCWjrh7pVEodGgJgA8Q1tpcbg= +github.com/tonglil/versioning v0.0.0-20170205083536-8b2a4334bd1d/go.mod h1:/jU0OcDkhtRrbaJPiG/p3X7XOP1pkFWLvUbsnQKP6hY= +github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 h1:2f304B10LaZdB8kkVEaoXvAMVan2tl9AiK4G0odjQtE= +github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= +github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ= +github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= +github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= +github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= +github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= +github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= +github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= +github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/valyala/fastjson v1.6.7 h1:ZE4tRy0CIkh+qDc5McjatheGX2czdn8slQjomexVpBM= +github.com/valyala/fastjson v1.6.7/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= +github.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA= +github.com/vektah/gqlparser/v2 v2.5.31 h1:YhWGA1mfTjID7qJhd1+Vxhpk5HTgydrGU9IgkWBTJ7k= +github.com/vektah/gqlparser/v2 v2.5.31/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/yashtewari/glob-intersection v0.2.0 h1:8iuHdN88yYuCzCdjt0gDe+6bAhUwBeEWqThExu54RFg= +github.com/yashtewari/glob-intersection v0.2.0/go.mod h1:LK7pIC3piUjovexikBbJ26Yml7g8xa5bsjfx2v1fwok= +github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= +github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= +github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= +github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= +github.com/ysmood/got v0.40.0 h1:ZQk1B55zIvS7zflRrkGfPDrPG3d7+JOza1ZkNxcc74Q= +github.com/ysmood/got v0.40.0/go.mod h1:W7DdpuX6skL3NszLmAsC5hT7JAhuLZhByVzHTq874Qg= +github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= +github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= +github.com/ysmood/leakless v0.9.0 h1:qxCG5VirSBvmi3uynXFkcnLMzkphdh3xx5FtrORwDCU= +github.com/ysmood/leakless v0.9.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= +github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= +github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +github.com/zclconf/go-cty v1.17.0 h1:seZvECve6XX4tmnvRzWtJNHdscMtYEx5R7bnnVyd/d0= +github.com/zclconf/go-cty v1.17.0/go.mod h1:wqFzcImaLTI6A5HfsRwB0nj5n0MRZFwmey8YoFPPs3U= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo= +github.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM= +github.com/zclconf/go-cty-yaml v1.2.0 h1:GDyL4+e/Qe/S0B7YaecMLbVvAR/Mp21CXMOSiCTOi1M= +github.com/zclconf/go-cty-yaml v1.2.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.lsp.dev/jsonrpc2 v0.10.0 h1:Pr/YcXJoEOTMc/b6OTmcR1DPJ3mSWl/SWiU1Cct6VmI= +go.lsp.dev/jsonrpc2 v0.10.0/go.mod h1:fmEzIdXPi/rf6d4uFcayi8HpFP1nBF99ERP1htC72Ac= +go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2 h1:hCzQgh6UcwbKgNSRurYWSqh8MufqRRPODRBblutn4TE= +go.lsp.dev/pkg v0.0.0-20210717090340-384b27a52fb2/go.mod h1:gtSHRuYfbCT0qnbLnovpie/WEmqyJ7T4n6VXiFMBtcw= +go.lsp.dev/protocol v0.12.0 h1:tNprUI9klQW5FAFVM4Sa+AbPFuVQByWhP1ttNUAjIWg= +go.lsp.dev/protocol v0.12.0/go.mod h1:Qb11/HgZQ72qQbeyPfJbu3hZBH23s1sr4st8czGeDMQ= +go.lsp.dev/uri v0.3.0 h1:KcZJmh6nFIBeJzTugn5JTU6OOyG0lDOo3R9KwTxTYbo= +go.lsp.dev/uri v0.3.0/go.mod h1:P5sbO1IQR+qySTWOCnhnK7phBx+W3zbLqSMDJNTw88I= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w= +go.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= +go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4= +go.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0 h1:ssfIgGNANqpVFCndZvcuyKbl0g+UAVcbBcqGkG28H0Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.64.0/go.mod h1:GQ/474YrbE4Jx8gZ4q5I4hrhUzM6UPzyrqJYV2AqPoQ= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8= +go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 h1:vl9obrcoWVKp/lwl8tRE33853I8Xru9HFbw/skNeLs8= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0/go.mod h1:GAXRxmLJcVM3u22IjTg74zWBrRCKq8BnOqUVLodpcpw= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0 h1:Oe2z/BCg5q7k4iXC3cqJxKYg0ieRiOqF0cecFYdPTwk= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.38.0/go.mod h1:ZQM5lAJpOsKnYagGg/zV2krVqTtaVdYdDkhMoX6Oalg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0 h1:Ckwye2FpXkYgiHX7fyVrN1uA/UYd9ounqqTuSNAv0k4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.39.0/go.mod h1:teIFJh5pW2y+AN7riv6IBPX2DuesS3HgP39mwOspKwU= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0 h1:cGtQxGvZbnrWdC2GyjZi0PDKVSLWP/Jocix3QWfXtbo= +go.opentelemetry.io/otel/exporters/prometheus v0.60.0/go.mod h1:hkd1EekxNo69PTV4OWFGZcKQiIqg0RfuWExcPKFvepk= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU= +go.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= +go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s= +go.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk= +go.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs= +go.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= +go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= +go.step.sm/crypto v0.75.0 h1:UAHYD6q6ggYyzLlIKHv1MCUVjZIesXRZpGTlRC/HSHw= +go.step.sm/crypto v0.75.0/go.mod h1:wwQ57+ajmDype9mrI/2hRyrvJd7yja5xVgWYqpUN3PE= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= +go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5 h1:bselrhR0Or1vomJZC8ZIjWtbDmn9OYFLX5Ik9alpJpE= -golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= +golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180925072008-f04abc6bdfa7/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= +golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421 h1:Wo7BWFiOk0QRFMLYMqJGFMd9CgUAcGx7V+qEg/h5IBI= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180925112736-b09afc3d579e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67 h1:1Fzlr8kkDLQwqMP8GxrhptBLqZG/EDpiATneiZHY998= -golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4 h1:bTLqdHv7xrGlFbvf5/TXNxy/iUwwdkjhqQTJDjW7aj0= +golang.org/x/telemetry v0.0.0-20260209163413-e7419c687ee4/go.mod h1:g5NllXBEermZrmR51cJDQxmJUHUOfRAaNyWBM+R+548= +golang.org/x/telemetry v0.0.0-20260311193753-579e4da9a98c/go.mod h1:TpUTTEp9frx7rTdLpC9gFG9kdI7zVLFTFFlqaH2Cncw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= +golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c h1:fqgJT0MGcGpPgpWU7VRdRjuArfcOvC4AoJmILihzhDg= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= +golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373 h1:PPwnA7z1Pjf7XYaBP9GL1VAMZmcIWyFz7QCMSIIa3Bg= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= +golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= +golang.org/x/tools/gopls v0.21.0 h1:k8RlBm3ES+GVe+fbTSkzwKgarmNwN+6aDalb0T0xfag= +golang.org/x/tools/gopls v0.21.0/go.mod h1:x/34IonzHuKpDDlMUjYezcjbwNOJ32FtrYOLqAuOmNo= +golang.org/x/vuln v1.1.4 h1:Ju8QsuyhX3Hk8ma3CesTbO8vfJD9EvUBgHvkxHBzj0I= +golang.org/x/vuln v1.1.4/go.mod h1:F+45wmU18ym/ca5PLTPLsSzr2KppzswxPP603ldA67s= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= +golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/api v0.260.0 h1:XbNi5E6bOVEj/uLXQRlt6TKuEzMD7zvW/6tNwltE4P4= +google.golang.org/api v0.260.0/go.mod h1:Shj1j0Phr/9sloYrKomICzdYgsSDImpTxME8rGLaZ/o= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180924164928-221a8d4f7494/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107 h1:xtNn7qFlagY2mQNFHMSRPjT2RkOV4OXM7P5TVy9xATo= -google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.15.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= -google.golang.org/grpc v1.19.0 h1:cfg4PD8YEdSFnm7qLV4++93WcmhH2nIUhMjhdCvl3j8= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217 h1:GvESR9BIyHUahIb0NcTum6itIWtdoglGX+rnGxm2934= +google.golang.org/genproto v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:yJ2HH4EHEDTd3JiLmhds6NkJ17ITVYOdV3m3VKOnws0= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b h1:Mv8VFug0MP9e5vUxfBcE3vUkV6CImK3cMNMIDFjmzxU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251222181119-0a764e51fe1b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= +google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/cheggaaa/pb.v1 v1.0.28 h1:n1tBJnnK2r7g9OW2btFH91V92STTUevLXYFb8gy9EMk= -gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= -gopkg.in/src-d/go-billy.v4 v4.3.0 h1:KtlZ4c1OWbIs4jCv5ZXrTqG8EQocr0g/d4DjNg70aek= -gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= -gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git-fixtures.v3 v3.4.0 h1:KFpaNTUcLHLoP/OkdcRXR+MA5p55MhA41YVb7Wd8EfM= -gopkg.in/src-d/go-git-fixtures.v3 v3.4.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.10.0 h1:NWjTJTQnk8UpIGlssuefyDZ6JruEjo5s88vm88uASbw= -gopkg.in/src-d/go-git.v4 v4.10.0/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= +gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.0.0-20160928153709-a5b47d31c556/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gotest.tools v2.1.0+incompatible h1:5USw7CrJBYKqjg9R7QlA6jzqZKEAtvW82aNmsxxGPxw= -gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= +helm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50= +helm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= +k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.35.0 h1:CUGo5o+7hW9GcAEF3x3usT3fX4f9r8xmgQeCBDaOgX4= +k8s.io/apiserver v0.35.0/go.mod h1:QUy1U4+PrzbJaM3XGu2tQ7U9A4udRRo5cyxkFX0GEds= +k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE= +k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY= +k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc= +k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= +modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= +modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= +modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= +modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= +modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= +modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= +modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= +modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI= +modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.45.0 h1:r51cSGzKpbptxnby+EIIz5fop4VuE4qFoVEjNvWoObs= +modernc.org/sqlite v1.45.0/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= +mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= +pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= +pluginrpc.com/pluginrpc v0.5.0 h1:tOQj2D35hOmvHyPu8e7ohW2/QvAnEtKscy2IJYWQ2yo= +pluginrpc.com/pluginrpc v0.5.0/go.mod h1:UNWZ941hcVAoOZUn8YZsMmOZBzbUjQa3XMns8RQLp9o= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kind v0.19.0 h1:ZSUh6/kpab6fiowT6EqL4k8xSbedI2NWxyuUOtoPFe4= +sigs.k8s.io/kind v0.19.0/go.mod h1:aBlbxg08cauDgZ612shr017/rZwqd7AS563FvpWKPVs= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= diff --git a/goreleaser-canary.yml b/goreleaser-canary.yml new file mode 100644 index 000000000000..7bf9128d3393 --- /dev/null +++ b/goreleaser-canary.yml @@ -0,0 +1,42 @@ +version: 2 + +project_name: trivy_canary_build +builds: + - + main: ./cmd/trivy/ + binary: trivy + ldflags: + - -s -w + - "-extldflags '-static'" + - -X github.com/aquasecurity/trivy/pkg/version/app.ver={{.Version}} + env: + - CGO_ENABLED=0 + - GOEXPERIMENT=jsonv2 + goos: + - darwin + - linux + - windows + goarch: + - amd64 + - arm64 + ignore: + - goos: windows + goarch: arm64 + +archives: + - + format: tar.gz + name_template: >- + {{ .ProjectName }}_{{ .Version }}_ + {{- if eq .Os "darwin" }}macOS + {{- else}}{{- title .Os }}{{ end }}- + {{- if eq .Arch "amd64" }}64bit + {{- else if eq .Arch "arm64" }}ARM64 + {{- else }}{{ .Arch }}{{ end }} + files: + - README.md + - LICENSE + - contrib/*.tpl + format_overrides: + - goos: windows + format: zip diff --git a/goreleaser.yml b/goreleaser.yml index f617ffb65ec1..bc8346f07e29 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -1,81 +1,314 @@ +version: 2 + project_name: trivy builds: - - main: cmd/trivy/main.go + - id: build-linux + main: ./cmd/trivy/ binary: trivy - ldflags: + ldflags: - -s -w - "-extldflags '-static'" - - -X main.version={{.Version}} + - -X github.com/aquasecurity/trivy/pkg/version/app.ver={{.Version}} env: - CGO_ENABLED=0 + - GOEXPERIMENT=jsonv2 goos: - - darwin - linux - - windows - - freebsd - - openbsd goarch: - - amd64 - 386 - arm + - amd64 + - arm64 + - s390x + - ppc64le + goarm: + - 7 + - id: build-bsd + main: ./cmd/trivy/ + binary: trivy + ldflags: + - -s -w + - "-extldflags '-static'" + - -X github.com/aquasecurity/trivy/pkg/version/app.ver={{.Version}} + env: + - CGO_ENABLED=0 + - GOEXPERIMENT=jsonv2 + goos: + - freebsd + goarch: + # modernc.org/sqlite doesn't support freebsd/arm64, etc. + # Also, freebsd/386 is no longer supported in modernc.org/libc + # See: https://gitlab.com/cznic/libc/-/issues/45 + - amd64 + - id: build-macos + main: ./cmd/trivy/ + binary: trivy + ldflags: + - -s -w + - "-extldflags '-static'" + - -X github.com/aquasecurity/trivy/pkg/version/app.ver={{.Version}} + env: + - CGO_ENABLED=0 + - GOEXPERIMENT=jsonv2 + goos: + - darwin + goarch: + - amd64 - arm64 goarm: - 7 + - id: build-windows + main: ./cmd/trivy/ + binary: trivy + ldflags: + - -s -w + - "-extldflags '-static'" + - -X github.com/aquasecurity/trivy/pkg/version/app.ver={{.Version}} + env: + - CGO_ENABLED=0 + - GOEXPERIMENT=jsonv2 + goos: + - windows + goarch: + # modernc.org/sqlite doesn't support windows/386 and windows/arm, etc. + - amd64 + goarm: + - 7 + +release: + extra_files: + - glob: ./bom.json + discussion_category_name: Announcements + +nfpms: + - + formats: + - deb + - rpm + vendor: "aquasecurity" + homepage: "https://github.com/aquasecurity" + maintainer: "Teppei Fukuda " + description: "A Fast Vulnerability Scanner for Containers" + license: "Apache-2.0" + file_name_template: >- + {{ .ProjectName }}_{{ .Version }}_ + {{- if eq .Os "darwin" }}macOS + {{- else if eq .Os "openbsd" }}OpenBSD + {{- else if eq .Os "netbsd" }}NetBSD + {{- else if eq .Os "freebsd" }}FreeBSD + {{- else if eq .Os "dragonfly" }}DragonFlyBSD + {{- else}}{{- title .Os }}{{ end }}- + {{- if eq .Arch "amd64" }}64bit + {{- else if eq .Arch "386" }}32bit + {{- else if eq .Arch "arm" }}ARM + {{- else if eq .Arch "arm64" }}ARM64 + {{- else if eq .Arch "ppc64le" }}PPC64LE + {{- else }}{{ .Arch }}{{ end }} + contents: + - src: contrib/*.tpl + dst: /usr/local/share/trivy/templates + rpm: + signature: + key_file: '{{ .Env.GPG_FILE }}' + +archives: + - id: archive + format: tar.gz + name_template: >- + {{ .ProjectName }}_{{ .Version }}_ + {{- if eq .Os "darwin" }}macOS + {{- else if eq .Os "linux" }}Linux + {{- else if eq .Os "openbsd" }}OpenBSD + {{- else if eq .Os "netbsd" }}NetBSD + {{- else if eq .Os "freebsd" }}FreeBSD + {{- else if eq .Os "dragonfly" }}DragonFlyBSD + {{- else}}{{- .Os }}{{ end }}- + {{- if eq .Arch "amd64" }}64bit + {{- else if eq .Arch "386" }}32bit + {{- else if eq .Arch "arm" }}ARM + {{- else if eq .Arch "arm64" }}ARM64 + {{- else if eq .Arch "ppc64le" }}PPC64LE + {{- else }}{{ .Arch }}{{ end }} + files: + - README.md + - LICENSE + - contrib/*.tpl + format_overrides: + - goos: windows + format: zip + + +brews: + - + repository: + owner: aquasecurity + name: homebrew-trivy + homepage: "https://github.com/aquasecurity/trivy" + description: "Scanner for vulnerabilities in container images, file systems, and Git repositories, as well as for configuration issues" + url_template: "https://get.trivy.dev/trivy?version={{ .Version }}&os={{ tolower .Os }}&arch={{ tolower .Arch }}" + test: | + system "#{bin}/trivy", "--version" + +dockers: + - image_templates: + - "docker.io/aquasec/trivy:{{ .Version }}-amd64" + - "docker.io/aquasec/trivy:latest-amd64" + - "ghcr.io/aquasecurity/trivy:{{ .Version }}-amd64" + - "ghcr.io/aquasecurity/trivy:latest-amd64" + - "public.ecr.aws/aquasecurity/trivy:latest-amd64" + - "public.ecr.aws/aquasecurity/trivy:{{ .Version }}-amd64" + use: buildx + goos: linux + goarch: amd64 + ids: + - build-linux + build_flag_templates: + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.description=A Fast Vulnerability Scanner for Containers" + - "--label=org.opencontainers.image.vendor=Aqua Security" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.source=https://github.com/aquasecurity/trivy" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.url=https://www.aquasec.com/products/trivy/" + - "--label=org.opencontainers.image.documentation=https://trivy.dev/v{{ .Version }}/" + - "--platform=linux/amd64" + extra_files: + - contrib/ + - image_templates: + - "docker.io/aquasec/trivy:{{ .Version }}-arm64" + - "docker.io/aquasec/trivy:latest-arm64" + - "ghcr.io/aquasecurity/trivy:{{ .Version }}-arm64" + - "ghcr.io/aquasecurity/trivy:latest-arm64" + - "public.ecr.aws/aquasecurity/trivy:latest-arm64" + - "public.ecr.aws/aquasecurity/trivy:{{ .Version }}-arm64" + use: buildx + goos: linux + goarch: arm64 + ids: + - build-linux + build_flag_templates: + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.description=A Fast Vulnerability Scanner for Containers" + - "--label=org.opencontainers.image.vendor=Aqua Security" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.source=https://github.com/aquasecurity/trivy" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.url=https://www.aquasec.com/products/trivy/" + - "--label=org.opencontainers.image.documentation=https://trivy.dev/v{{ .Version }}/" + - "--platform=linux/arm64" + extra_files: + - contrib/ + - image_templates: + - "docker.io/aquasec/trivy:{{ .Version }}-s390x" + - "docker.io/aquasec/trivy:latest-s390x" + - "ghcr.io/aquasecurity/trivy:{{ .Version }}-s390x" + - "ghcr.io/aquasecurity/trivy:latest-s390x" + - "public.ecr.aws/aquasecurity/trivy:latest-s390x" + - "public.ecr.aws/aquasecurity/trivy:{{ .Version }}-s390x" + use: buildx + goos: linux + goarch: s390x + ids: + - build-linux + build_flag_templates: + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.description=A Fast Vulnerability Scanner for Containers" + - "--label=org.opencontainers.image.vendor=Aqua Security" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.source=https://github.com/aquasecurity/trivy" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.url=https://www.aquasec.com/products/trivy/" + - "--label=org.opencontainers.image.documentation=https://trivy.dev/v{{ .Version }}/" + - "--platform=linux/s390x" + extra_files: + - contrib/ + - image_templates: + - "docker.io/aquasec/trivy:{{ .Version }}-ppc64le" + - "docker.io/aquasec/trivy:latest-ppc64le" + - "ghcr.io/aquasecurity/trivy:{{ .Version }}-ppc64le" + - "ghcr.io/aquasecurity/trivy:latest-ppc64le" + - "public.ecr.aws/aquasecurity/trivy:latest-ppc64le" + - "public.ecr.aws/aquasecurity/trivy:{{ .Version }}-ppc64le" + use: buildx + goos: linux + goarch: ppc64le + ids: + - build-linux + build_flag_templates: + - "--label=org.opencontainers.image.title={{ .ProjectName }}" + - "--label=org.opencontainers.image.description=A Fast Vulnerability Scanner for Containers" + - "--label=org.opencontainers.image.vendor=Aqua Security" + - "--label=org.opencontainers.image.version={{ .Version }}" + - "--label=org.opencontainers.image.created={{ .Date }}" + - "--label=org.opencontainers.image.source=https://github.com/aquasecurity/trivy" + - "--label=org.opencontainers.image.revision={{ .FullCommit }}" + - "--label=org.opencontainers.image.url=https://www.aquasec.com/products/trivy/" + - "--label=org.opencontainers.image.documentation=https://trivy.dev/v{{ .Version }}/" + - "--platform=linux/ppc64le" + extra_files: + - contrib/ -nfpm: - formats: - - deb - - rpm - dependencies: - - rpm - vendor: "knqyf263" - homepage: "https://github.com/knqyf263" - maintainer: "Teppei Fukuda " - description: "A Fast Vulnerability Scanner for Containers" - license: "MIT" - name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}" - replacements: - amd64: 64bit - 386: 32bit - arm: ARM - arm64: ARM64 - darwin: macOS - linux: Linux - windows: Windows - openbsd: OpenBSD - netbsd: NetBSD - freebsd: FreeBSD - dragonfly: DragonFlyBSD +docker_manifests: + - name_template: 'aquasec/trivy:{{ .Version }}' + image_templates: + - 'aquasec/trivy:{{ .Version }}-amd64' + - 'aquasec/trivy:{{ .Version }}-arm64' + - 'aquasec/trivy:{{ .Version }}-s390x' + - 'aquasec/trivy:{{ .Version }}-ppc64le' + - name_template: 'ghcr.io/aquasecurity/trivy:{{ .Version }}' + image_templates: + - 'ghcr.io/aquasecurity/trivy:{{ .Version }}-amd64' + - 'ghcr.io/aquasecurity/trivy:{{ .Version }}-arm64' + - 'ghcr.io/aquasecurity/trivy:{{ .Version }}-s390x' + - 'ghcr.io/aquasecurity/trivy:{{ .Version }}-ppc64le' + - name_template: 'public.ecr.aws/aquasecurity/trivy:{{ .Version }}' + image_templates: + - 'public.ecr.aws/aquasecurity/trivy:{{ .Version }}-amd64' + - 'public.ecr.aws/aquasecurity/trivy:{{ .Version }}-arm64' + - 'public.ecr.aws/aquasecurity/trivy:{{ .Version }}-s390x' + - 'public.ecr.aws/aquasecurity/trivy:{{ .Version }}-ppc64le' + - name_template: 'aquasec/trivy:latest' + image_templates: + - 'aquasec/trivy:{{ .Version }}-amd64' + - 'aquasec/trivy:{{ .Version }}-arm64' + - 'aquasec/trivy:{{ .Version }}-s390x' + - 'aquasec/trivy:{{ .Version }}-ppc64le' + - name_template: 'ghcr.io/aquasecurity/trivy:latest' + image_templates: + - 'ghcr.io/aquasecurity/trivy:{{ .Version }}-amd64' + - 'ghcr.io/aquasecurity/trivy:{{ .Version }}-arm64' + - 'ghcr.io/aquasecurity/trivy:{{ .Version }}-s390x' + - 'ghcr.io/aquasecurity/trivy:{{ .Version }}-ppc64le' + - name_template: 'public.ecr.aws/aquasecurity/trivy:latest' + image_templates: + - 'public.ecr.aws/aquasecurity/trivy:{{ .Version }}-amd64' + - 'public.ecr.aws/aquasecurity/trivy:{{ .Version }}-arm64' + - 'public.ecr.aws/aquasecurity/trivy:{{ .Version }}-s390x' + - 'public.ecr.aws/aquasecurity/trivy:{{ .Version }}-ppc64le' -archive: - format: tar.gz - format_overrides: - - goos: windows - format: zip - name_template: "{{.ProjectName}}_{{.Version}}_{{.Os}}-{{.Arch}}" - replacements: - amd64: 64bit - 386: 32bit - arm: ARM - arm64: ARM64 - darwin: macOS - linux: Linux - windows: Windows - openbsd: OpenBSD - netbsd: NetBSD - freebsd: FreeBSD - dragonfly: DragonFlyBSD - files: - - README.md - - LICENSE +signs: +- cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + signature: "${artifact}.sigstore.json" + args: + - "sign-blob" + - "--bundle=${signature}" + - "${artifact}" + - "--yes" + artifacts: all + output: true -brew: - github: - owner: knqyf263 - name: homebrew-trivy - dependencies: - - rpm - homepage: "https://github.com/knqyf263/trivy" - description: "" - test: | - system "#{bin}/program --version" +docker_signs: +- cmd: cosign + env: + - COSIGN_EXPERIMENTAL=1 + artifacts: manifests + output: true + args: + - 'sign' + - '${artifact}' + - '--yes' diff --git a/helm/trivy/.helmignore b/helm/trivy/.helmignore new file mode 100644 index 000000000000..50af03172541 --- /dev/null +++ b/helm/trivy/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/helm/trivy/Chart.yaml b/helm/trivy/Chart.yaml new file mode 100644 index 000000000000..cef435d43ab3 --- /dev/null +++ b/helm/trivy/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: trivy +version: 0.21.1 +appVersion: 0.69.1 +description: Trivy helm chart +keywords: + - scanner + - trivy + - vulnerability +sources: + - https://github.com/aquasecurity/trivy diff --git a/helm/trivy/README.md b/helm/trivy/README.md new file mode 100644 index 000000000000..92f96a8dae74 --- /dev/null +++ b/helm/trivy/README.md @@ -0,0 +1,112 @@ +# Trivy Scanner + +Trivy vulnerability scanner standalone installation. + +## TL;DR; + +``` +$ helm install trivy . --namespace trivy --create-namespace +``` + +## Introduction + +This chart bootstraps a Trivy deployment on a [Kubernetes](http://kubernetes.io) cluster using the +[Helm](https://helm.sh) package manager. + +## Prerequisites + +- Kubernetes 1.12+ +- Helm 3+ + +## Installing from the Aqua Chart Repository + +``` +helm repo add aquasecurity https://aquasecurity.github.io/helm-charts/ +helm repo update +helm search repo trivy +helm install my-trivy aquasecurity/trivy +``` + +## Installing the Chart + +To install the chart with the release name `my-release`: + +``` +$ helm install my-release . +``` + +The command deploys Trivy on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) +section lists the parameters that can be configured during installation. + +> **Tip**: List all releases using `helm list`. + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +``` +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Parameters + +The following table lists the configurable parameters of the Trivy chart and their default values. + +| Parameter | Description | Default | +|---------------------------------------|-------------------------------------------------------------------------|----------------| +| `image.registry` | Image registry | `docker.io` | +| `image.repository` | Image name | `aquasec/trivy` | +| `image.tag` | Image tag | `{TAG_NAME}` | +| `image.pullPolicy` | Image pull policy | `IfNotPresent` | +| `image.pullSecret` | The name of an imagePullSecret used to pull trivy image from e.g. Docker Hub or a private registry | | +| `replicaCount` | Number of Trivy Pods to run | `1` | +| `trivy.debugMode` | The flag to enable or disable Trivy debug mode | `false` | +| `trivy.gitHubToken` | The GitHub access token to download Trivy DB. More info: https://trivy.dev/docs/latest/references/troubleshooting/#github-rate-limiting | | +| `trivy.registryUsername` | The username used to log in at dockerhub. More info: https://trivy.dev/docs/latest/advanced/private-registries/docker-hub/ | | +| `trivy.registryPassword` | The password used to log in at dockerhub. More info: https://trivy.dev/docs/latest/advanced/private-registries/docker-hub/ | | +| `trivy.registryCredentialsExistingSecret` | Name of Secret containing dockerhub credentials. Alternative to the 2 parameters above, has precedence if set. | | +| `trivy.serviceAccount.annotations` | Additional annotations to add to the Kubernetes service account resource | | +| `trivy.skipDBUpdate` | The flag to enable or disable Trivy DB downloads from GitHub | `false` | +| `trivy.dbRepository` | OCI repository to retrieve the trivy vulnerability database from | `ghcr.io/aquasecurity/trivy-db` | +| `trivy.cache.redis.enabled` | Enable Redis as caching backend | `false` | +| `trivy.cache.redis.url` | Specify redis connection url, e.g. redis://redis.redis.svc:6379 | `` | +| `trivy.cache.redis.ttl` | Specify redis TTL, e.g. 3600s or 24h | `` | +| `trivy.cache.redis.tls` | Enable Redis TLS with public certificates | `` | +| `trivy.serverToken` | The token to authenticate Trivy client with Trivy server | `` | +| `trivy.existingSecret` | existingSecret if an existing secret has been created outside the chart. Overrides gitHubToken, registryUsername, registryPassword, serverToken | `` | +| `trivy.podAnnotations` | Annotations for pods created by statefulset | `{}` | +| `trivy.extraEnvVars` | extraEnvVars to be set on the container | `{}` | +| `trivy.sslCertDir` | Can be used to override the system default locations for SSL certificate files directory, example: `/ssl/certs` | `` | +| `service.name` | If specified, the name used for the Trivy service | | +| `service.type` | Kubernetes service type | `ClusterIP` | +| `service.port` | Kubernetes service port | `4954` | +| `service.sessionAffinity` | Kubernetes service session affinity | `ClientIP` | +| `httpProxy` | The URL of the HTTP proxy server | | +| `httpsProxy` | The URL of the HTTPS proxy server | | +| `noProxy` | The URLs that the proxy settings do not apply to | | +| `nodeSelector` | Node labels for pod assignment | | +| `affinity` | Affinity settings for pod assignment | | +| `tolerations` | Tolerations for pod assignment | | +| `podAnnotations` | Annotations for pods created by statefulset | `{}` | + +The above parameters map to the env variables defined in [trivy](https://trivy.dev/docs/latest/configuration/#configuration). + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. + +``` +$ helm install my-release . \ + --namespace my-namespace \ + --set "service.port=9090" \ + --set "trivy.vulnType=os\,library" +``` + +## Storage + +This chart uses a PersistentVolumeClaim to reduce the number of database downloads between POD restarts or updates. The storageclass should have the reclaim policy `Retain`. + +## Caching + +You can specify a Redis server as cache backend. This Redis server has to be already present. You can use the [bitnami chart](https://bitnami.com/stack/redis/helm). +More Information about the caching backends can be found [here](https://trivy.dev/docs/latest/configuration/cache/#scan-cache-backend). diff --git a/helm/trivy/templates/NOTES.txt b/helm/trivy/templates/NOTES.txt new file mode 100644 index 000000000000..443a853e1e47 --- /dev/null +++ b/helm/trivy/templates/NOTES.txt @@ -0,0 +1,2 @@ +You should be able to access Trivy server installation within +the cluster at http://{{ include "trivy.fullname" . }}.{{ .Release.Namespace }}:{{ .Values.service.port }} diff --git a/helm/trivy/templates/_helpers.tpl b/helm/trivy/templates/_helpers.tpl new file mode 100644 index 000000000000..28d83fcff0bd --- /dev/null +++ b/helm/trivy/templates/_helpers.tpl @@ -0,0 +1,55 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "trivy.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "trivy.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "trivy.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "trivy.labels" -}} +app.kubernetes.io/name: {{ include "trivy.name" . }} +helm.sh/chart: {{ include "trivy.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Return the proper imageRef as used by the container template spec. +*/}} +{{- define "trivy.imageRef" -}} +{{- $registryName := .Values.image.registry -}} +{{- $repositoryName := .Values.image.repository -}} +{{- $tag := .Values.image.tag | default .Chart.AppVersion | toString -}} +{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} +{{- end -}} diff --git a/helm/trivy/templates/configmap.yaml b/helm/trivy/templates/configmap.yaml new file mode 100644 index 000000000000..75dfb35d04b2 --- /dev/null +++ b/helm/trivy/templates/configmap.yaml @@ -0,0 +1,32 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "trivy.fullname" . }} + labels: +{{ include "trivy.labels" . | indent 4 }} +data: + TRIVY_LISTEN: "0.0.0.0:{{ .Values.service.port }}" + TRIVY_CACHE_DIR: "/home/scanner/.cache/trivy" +{{- if .Values.trivy.cache.redis.enabled }} + TRIVY_CACHE_BACKEND: {{ .Values.trivy.cache.redis.url | quote }} + TRIVY_CACHE_TTL: {{ .Values.trivy.cache.redis.ttl | quote }} + TRIVY_REDIS_TLS: {{ .Values.trivy.cache.redis.tls | quote }} +{{- end }} + TRIVY_DEBUG: {{ .Values.trivy.debugMode | quote }} + TRIVY_SKIP_DB_UPDATE: {{ .Values.trivy.skipDBUpdate | quote }} + TRIVY_DB_REPOSITORY: {{ .Values.trivy.dbRepository | quote }} +{{- if .Values.httpProxy }} + HTTP_PROXY: {{ .Values.httpProxy | quote }} +{{- end }} +{{- if .Values.httpsProxy }} + HTTPS_PROXY: {{ .Values.httpsProxy | quote }} +{{- end }} +{{- if .Values.noProxy }} + NO_PROXY: {{ .Values.noProxy | quote }} +{{- end }} +{{- with .Values.trivy.extraEnvVars }} + {{- . | toYaml | nindent 2 }} +{{- end }} +{{- if .Values.trivy.sslCertDir }} + SSL_CERT_DIR: {{ .Values.trivy.sslCertDir | quote }} +{{- end }} diff --git a/helm/trivy/templates/ingress.yaml b/helm/trivy/templates/ingress.yaml new file mode 100644 index 000000000000..fc4a59fc98c9 --- /dev/null +++ b/helm/trivy/templates/ingress.yaml @@ -0,0 +1,53 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "trivy.fullname" . -}} +{{- if .Capabilities.APIVersions.Has "networking.k8s.io/v1" }} +apiVersion: networking.k8s.io/v1 +{{- else if .Capabilities.APIVersions.Has "networking.k8s.io/v1beta1" }} +apiVersion: networking.k8s.io/v1beta1 +{{- else }} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ include "trivy.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: +{{ include "trivy.labels" . | indent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: +{{- if and (.Values.ingress.ingressClassName) (semverCompare ">= v1.18.0" .Capabilities.KubeVersion.Version) }} + ingressClassName: {{ .Values.ingress.ingressClassName }} +{{- end }} +{{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} +{{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + - path: {{ $.Values.ingress.path }} + {{- if $.Capabilities.APIVersions.Has "networking.k8s.io/v1/Ingress" }} + pathType: {{ $.Values.ingress.pathType }} + backend: + service: + name: {{ $fullName }} + port: + number: {{ $.Values.service.port -}} + {{- else }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $.Values.service.port -}} + {{- end }} + {{- end }} +{{- end }} diff --git a/helm/trivy/templates/podsecuritypolicy.yaml b/helm/trivy/templates/podsecuritypolicy.yaml new file mode 100644 index 000000000000..4539667771ea --- /dev/null +++ b/helm/trivy/templates/podsecuritypolicy.yaml @@ -0,0 +1,44 @@ +{{- if .Values.rbac.pspEnabled }} + {{- if .Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy" }} +apiVersion: policy/v1beta1 +kind: PodSecurityPolicy +metadata: + name: {{ include "trivy.fullname" . }} + {{- with .Values.rbac.pspAnnotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} + labels: +{{ include "trivy.labels" . | indent 4 }} +spec: + privileged: false + allowPrivilegeEscalation: false + volumes: + - 'configMap' + - 'emptyDir' + - 'persistentVolumeClaim' + - 'secret' + - 'projected' + - 'downwardAPI' + hostNetwork: false + hostIPC: false + hostPID: false + runAsUser: + rule: 'MustRunAsNonRoot' + seLinux: + rule: 'RunAsAny' + supplementalGroups: + rule: 'MustRunAs' + ranges: + - min: 1 + max: 65535 + fsGroup: + rule: 'MustRunAs' + ranges: + - min: 1 + max: 65535 + readOnlyRootFilesystem: true + requiredDropCapabilities: + - ALL + {{- end }} +{{- end }} diff --git a/helm/trivy/templates/role.yaml b/helm/trivy/templates/role.yaml new file mode 100644 index 000000000000..461ec60fab57 --- /dev/null +++ b/helm/trivy/templates/role.yaml @@ -0,0 +1,18 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "trivy.fullname" . }} + labels: +{{ include "trivy.labels" . | indent 4 }} + namespace: {{ .Release.Namespace }} +{{- if .Values.rbac.pspEnabled }} + {{- if .Capabilities.APIVersions.Has "policy/v1beta1/PodSecurityPolicy" }} +rules: +- apiGroups: ['policy'] + resources: ['podsecuritypolicies'] + verbs: ['use'] + resourceNames: [{{ include "trivy.fullname" . }}] + {{- end }} +{{- end }} +{{- end }} diff --git a/helm/trivy/templates/rolebinding.yaml b/helm/trivy/templates/rolebinding.yaml new file mode 100644 index 000000000000..8b35061b8c34 --- /dev/null +++ b/helm/trivy/templates/rolebinding.yaml @@ -0,0 +1,16 @@ +{{- if .Values.rbac.create }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "trivy.fullname" . }} + labels: +{{ include "trivy.labels" . | indent 4 }} + namespace: {{ .Release.Namespace }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "trivy.fullname" . }} +subjects: +- kind: ServiceAccount + name: {{ include "trivy.fullname" . }} +{{- end }} \ No newline at end of file diff --git a/helm/trivy/templates/secret.yaml b/helm/trivy/templates/secret.yaml new file mode 100644 index 000000000000..0aa79c8a7b71 --- /dev/null +++ b/helm/trivy/templates/secret.yaml @@ -0,0 +1,16 @@ +{{- if not .Values.trivy.existingSecret }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "trivy.fullname" . }} + labels: +{{ include "trivy.labels" . | indent 4 }} +type: Opaque +data: + GITHUB_TOKEN: {{ .Values.trivy.gitHubToken | default "" | b64enc | quote }} + TRIVY_TOKEN: {{ .Values.trivy.serverToken | default "" | b64enc | quote }} +{{- if not .Values.trivy.registryCredentialsExistingSecret }} + TRIVY_USERNAME: {{ .Values.trivy.registryUsername | default "" | b64enc | quote }} + TRIVY_PASSWORD: {{ .Values.trivy.registryPassword | default "" | b64enc | quote }} +{{- end -}} +{{- end }} diff --git a/helm/trivy/templates/service.yaml b/helm/trivy/templates/service.yaml new file mode 100644 index 000000000000..8744b2201424 --- /dev/null +++ b/helm/trivy/templates/service.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ .Values.service.name | default (include "trivy.fullname" .) }} + labels: +{{ include "trivy.labels" . | indent 4 }} +spec: + type: {{ .Values.service.type | default "ClusterIP" }} + selector: + app.kubernetes.io/name: {{ include "trivy.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + ports: + - name: trivy-http + protocol: TCP + port: {{ .Values.service.port | default 4954 }} + targetPort: {{ .Values.service.port | default 4954 }} + sessionAffinity: {{ .Values.service.sessionAffinity | default "ClientIP" }} + diff --git a/helm/trivy/templates/serviceaccount.yaml b/helm/trivy/templates/serviceaccount.yaml new file mode 100644 index 000000000000..2b10e9069bd4 --- /dev/null +++ b/helm/trivy/templates/serviceaccount.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "trivy.fullname" . }} + labels: +{{ include "trivy.labels" . | indent 4 }} +{{- if (.Values.trivy.serviceAccount).annotations }} + annotations: +{{ toYaml .Values.trivy.serviceAccount.annotations | indent 4 }} +{{- end }} + namespace: {{ .Release.Namespace }} \ No newline at end of file diff --git a/helm/trivy/templates/statefulset.yaml b/helm/trivy/templates/statefulset.yaml new file mode 100644 index 000000000000..7d7211c39955 --- /dev/null +++ b/helm/trivy/templates/statefulset.yaml @@ -0,0 +1,148 @@ +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: {{ include "trivy.fullname" . }} + labels: +{{ include "trivy.labels" . | indent 4 }} + {{- with .Values.trivy.labels }} + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + podManagementPolicy: "Parallel" + serviceName: {{ include "trivy.fullname" . }} + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "trivy.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + {{- if .Values.persistence.enabled }} + volumeClaimTemplates: + - apiVersion: v1 + kind: PersistentVolumeClaim + metadata: + name: data + spec: + resources: + requests: + storage: {{ .Values.persistence.size }} + accessModes: + - {{ .Values.persistence.accessMode }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- . | toYaml | nindent 8 }} + {{- end }} + labels: + app.kubernetes.io/name: {{ include "trivy.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + {{- with .Values.trivy.labels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + serviceAccountName: {{ include "trivy.fullname" . }} + automountServiceAccountToken: false + {{- if .Values.podSecurityContext }} + securityContext: +{{ toYaml .Values.podSecurityContext | indent 8 }} + {{- end }} + {{- if .Values.image.pullSecret }} + imagePullSecrets: + - name: {{ .Values.image.pullSecret }} + {{- end }} + {{- if .Values.nodeSelector }} + nodeSelector: +{{ toYaml .Values.nodeSelector | indent 8 }} + {{- end }} + {{- if .Values.tolerations }} + tolerations: +{{ toYaml .Values.tolerations | indent 8 }} + {{- end }} + {{- if .Values.affinity }} + affinity: +{{ toYaml .Values.affinity | indent 8 }} + {{- end }} + containers: + - name: main + image: {{ template "trivy.imageRef" . }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + {{- if .Values.securityContext }} + securityContext: +{{ toYaml .Values.securityContext | indent 12 }} + {{- end }} + args: + - server + {{- if .Values.trivy.registryCredentialsExistingSecret }} + env: + - name: TRIVY_USERNAME + valueFrom: + secretKeyRef: + name: {{ .Values.trivy.registryCredentialsExistingSecret }} + key: TRIVY_USERNAME + - name: TRIVY_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.trivy.registryCredentialsExistingSecret }} + key: TRIVY_PASSWORD + {{- end }} + envFrom: + - configMapRef: + name: {{ include "trivy.fullname" . }} + - secretRef: + {{- if not .Values.trivy.existingSecret }} + name: {{ include "trivy.fullname" . }} + {{- else }} + name: {{ .Values.trivy.existingSecret }} + {{- end }} + ports: + - name: trivy-http + containerPort: {{ .Values.service.port }} + livenessProbe: + httpGet: + scheme: HTTP + path: /healthz + port: trivy-http + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 10 + readinessProbe: + httpGet: + scheme: HTTP + path: /healthz + port: trivy-http + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + failureThreshold: 3 + volumeMounts: + - mountPath: /tmp + name: tmp-data + readOnly: false + - mountPath: /home/scanner/.cache + name: data + readOnly: false + {{- with .Values.trivy.sslCertDir }} + - mountPath: {{ . }} + name: ssl-cert-dir + readOnly: true + {{- end }} + {{- if .Values.resources }} + resources: +{{ toYaml .Values.resources | indent 12 }} + {{- end }} + volumes: + - name: tmp-data + emptyDir: {} + {{- if not .Values.persistence.enabled }} + - name: data + emptyDir: {} + {{- end }} + {{- with .Values.trivy.sslCertDir }} + - name: ssl-cert-dir + hostPath: + path: {{ . }} + {{- end }} diff --git a/helm/trivy/values.yaml b/helm/trivy/values.yaml new file mode 100644 index 000000000000..177a62fab7a7 --- /dev/null +++ b/helm/trivy/values.yaml @@ -0,0 +1,165 @@ +nameOverride: "" +fullnameOverride: "" + +image: + registry: docker.io + repository: aquasec/trivy + # tag is an override of the image tag, which is by default set by the + # appVersion field in Chart.yaml. + tag: "" + pullPolicy: IfNotPresent + pullSecret: "" + +replicaCount: 1 + +persistence: + enabled: true + storageClass: "" + accessMode: ReadWriteOnce + size: 5Gi + +resources: + requests: + cpu: 200m + memory: 512Mi + limits: + cpu: 1 + memory: 1Gi + +rbac: + create: true + pspEnabled: false + pspAnnotations: {} + +podSecurityContext: + runAsUser: 65534 + runAsNonRoot: true + fsGroup: 65534 + +securityContext: + privileged: false + readOnlyRootFilesystem: true + +## Node labels for pod assignment +## Ref: https://kubernetes.io/docs/user-guide/node-selection/ +nodeSelector: {} + +## Affinity settings for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/ +affinity: {} + +## Tolerations for pod assignment +## Ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +tolerations: [] + +## Annotations for pods created by statefulset +## Ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} + +trivy: + # debugMode the flag to enable Trivy debug mode + debugMode: false + # gitHubToken the GitHub access token to download Trivy DB + # + # Trivy DB contains vulnerability information from NVD, Red Hat, and many other upstream vulnerability databases. + # It is downloaded by Trivy from the GitHub release page https://github.com/aquasecurity/trivy-db/releases and cached + # in the local file system (`/home/scanner/.cache/trivy/db/trivy.db`). In addition, the database contains the update + # timestamp so Trivy can detect whether it should download a newer version from the Internet or use the cached one. + # Currently, the database is updated every 12 hours and published as a new release to GitHub. + # + # Anonymous downloads from GitHub are subject to the limit of 60 requests per hour. Normally such rate limit is enough + # for production operations. If, for any reason, it's not enough, you could increase the rate limit to 5000 + # requests per hour by specifying the GitHub access token. For more details on GitHub rate limiting please consult + # https://developer.github.com/v3/#rate-limiting + # + # You can create a GitHub token by following the instructions in + # https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line + gitHubToken: "" + + # Docker registry credentials + # See also: https://trivy.dev/docs/latest/advanced/private-registries/docker-hub/ + # + # Either + # Directly in this file + # + # TRIVY_USERNAME + registryUsername: "" + # TRIVY_PASSWORD + registryPassword: "" + # + # Or + # From an existing secret + # + # The secret must be Opaque and just contain "TRIVY_USERNAME: your_user" and "TRIVY_PASSWORD: your_password" as k/v pairs. + # NOTE: When this is set the previous parameters are ignored. + # + # registryCredentialsExistingSecret: name-of-existing-secret + # skipDBUpdate the flag to enable or disable Trivy DB downloads from GitHub + # + # You might want to enable this flag in test or CI/CD environments to avoid GitHub rate limiting issues. + # If the flag is enabled you have to manually download the `trivy.db` file and mount it in the + # `/home/scanner/.cache/trivy/db/trivy.db` path (see `cacheDir`). + skipDBUpdate: false + # OCI repository to retrieve the trivy vulnerability database from + dbRepository: ghcr.io/aquasecurity/trivy-db + # Trivy supports filesystem and redis as caching backend + # https://github.com/aquasecurity/trivy#specify-cache-backend + # This location is only used for the cache, not the db storage: https://github.com/aquasecurity/trivy/issues/765#issue-756010345 + # + # In case you specify redis as backend, make sure you installed a redis server yourself, e.g. + # https://bitnami.com/stack/redis/helm + # + # In case redis is not enabled, the filesystem will be used + cache: + redis: + enabled: false + url: "" # e.g. redis://redis.redis.svc:6379 + ttl: "" # e.g 3600s, 24h + tls: false + serviceAccount: + annotations: {} + # eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/IAM_ROLE_NAME + # If you want to add custom labels to your statefulset and podTemplate + labels: {} + # serverToken is the token to authenticate Trivy client with Trivy server. + serverToken: "" + # existingSecret if an existing secret has been created outside the chart. + # Overrides gitHubToken, registryUsername, registryPassword, serverToken + existingSecret: "" + # extraEnvVars to be set on the container + extraEnvVars: {} + # sslCertDir can be used to override the system default locations for SSL certificate files directory, example: /ssl/certs + sslCertDir: "" + +service: + # If specified, the name used for the Trivy service. + name: + # type Kubernetes service type + type: ClusterIP + # port Kubernetes service port + port: 4954 + # sessionAffinity Kubernetes service session affinity + sessionAffinity: ClientIP + +ingress: + enabled: false + # From Kubernetes 1.18+ this field is supported in case your ingress controller supports it. When set, you do not need to add the ingress class as annotation. + ingressClassName: + annotations: {} + # kubernetes.io/ingress.class: nginx + hosts: + - host: trivy.example.com + path: "/" + # type is only needed for networking.k8s.io/v1 in k8s 1.19+ + pathType: Prefix + tls: [] + # - secretName: trivy-example-tls + # hosts: + # - trivy.example.com + +# httpProxy the URL of the HTTP proxy server +httpProxy: +# httpsProxy the URL of the HTTPS proxy server +httpsProxy: +# noProxy the URLs that the proxy settings do not apply to +noProxy: diff --git a/imgs/logo.png b/imgs/logo.png deleted file mode 100644 index b8d821ad6e2d..000000000000 Binary files a/imgs/logo.png and /dev/null differ diff --git a/integration/README.md b/integration/README.md new file mode 100644 index 000000000000..34c15294c754 --- /dev/null +++ b/integration/README.md @@ -0,0 +1,193 @@ +# Integration Tests + +This directory contains integration tests for Trivy. These tests verify Trivy's behavior by running actual commands and comparing the output against golden files. + +## Running Tests + +### Run integration tests +```bash +# Run standard integration tests (excludes VM, K8s, and module tests) +mage test:integration + +# Run all types of integration tests separately +mage test:integration # Standard integration tests +mage test:module # Wasm module tests +mage test:vm # VM integration tests +mage test:k8s # Kubernetes integration tests +``` + +### Run specific test +```bash +GOEXPERIMENT=jsonv2 go test -tags=integration -run TestRepository ./integration -v +``` + +## Golden Files + +Golden files store the expected output for integration tests. They are located in `integration/testdata/*.golden`. + +### Updating Golden Files + +When you make changes that affect test output, you need to update the golden files: + +```bash +# Update golden files for standard integration tests +mage test:updateGolden + +# Update golden files for Wasm module tests +mage test:updateModuleGolden + +# Update golden files for VM integration tests +mage test:updateVMGolden + +# Update specific golden files manually +GOEXPERIMENT=jsonv2 go test -tags=integration -run TestRepository ./integration -v -update +``` + +**Important**: +- Only tests that generate golden files as the canonical source support the `-update` flag +- Tests that reuse golden files from other tests will be **skipped** during updates +- Look for `override: nil` comment in test code to identify canonical source tests + +### Golden File Management Strategy + +#### 1. Canonical Source Tests (Can Update Golden Files) + +These tests generate golden files and should have: +- `override: nil` comment in the code +- No `t.Skipf()` for the `-update` flag + +Example: +```go +func TestRepository(t *testing.T) { + // ... + runTest(t, osArgs, tt.golden, format, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: nil, // Do not use overrides - golden files are generated from this test as the canonical source + }) +} +``` + +#### 2. Consumer Tests (Cannot Update Golden Files) + +These tests reuse golden files from canonical source tests and should have: +- `if *update { t.Skipf(...) }` at the beginning of the test function +- `override` functions to adjust for differences (e.g., different artifact names, paths) +- Simplified comment: `Golden files are shared with TestXXX.` + +Example: +```go +// TestClientServer tests the client-server mode of Trivy. +// +// Golden files are shared with TestTar or TestRepository. +func TestClientServer(t *testing.T) { + if *update { + t.Skipf("Skipping TestClientServer when -update flag is set. Golden files should be updated via TestTar or TestRepository.") + } + + // ... + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + override: overrideFuncs(overrideUID, func(_ *testing.T, want, _ *types.Report) { + want.ArtifactName = "https://github.com/knqyf263/trivy-ci-test" + }), + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) +} +``` + +### Why Only One Test Updates Each Golden File + +**Critical constraint**: Each golden file must be updated by exactly one test function. + +If multiple tests update the same golden file, they may introduce subtle differences in the output. This causes the golden file to change every time tests are run, depending on which test executed last. This makes the golden files unstable and defeats their purpose. + +**Solution**: Designate one test as the "canonical source" for each golden file. Other tests that want to verify equivalent results share the golden file in read-only mode (with `t.Skipf()` during updates). + +### When to Share Golden Files + +Share golden files between tests when you want to verify that different commands, flags, or configurations produce equivalent results with the **same output format**: + +**Good reasons to share:** +- Testing different input methods that produce the same JSON output (local path vs remote URL vs client-server mode) +- Testing different ways to specify the same configuration (environment variables vs CLI flags vs config files) +- Testing different image sources that produce the same scan results (tar archive vs Docker Engine vs registry) + +**Use override functions to handle:** +- Different artifact names or paths +- Different metadata (e.g., image config, repo info) +- Different ReportIDs or UUIDs +- Minor formatting differences in paths (e.g., Windows vs Unix separators) + +**Example**: TestTar generates golden files for image scanning, and these are reused by: +- TestDockerEngine (different image source: Docker Engine API) +- TestRegistry (different image source: container registry) +- TestClientServer (different execution mode: client-server) + +All of these produce the same JSON format with the same vulnerability data, but with different artifact names and metadata. + +### Validation + +The test framework automatically validates that: +- Tests updating golden files (`*update == true`) cannot use override functions +- This prevents accidentally updating golden files with modified data + +If you try to update a golden file with an override function, the test will fail with: +``` +invalid test configuration: cannot use override functions when update=true +``` + +## Test Organization + +### Test Files + +Tests are organized by functionality: + +- `standalone_tar_test.go` - Container image scanning from tar archives +- `repo_test.go` - Repository and filesystem scanning +- `sbom_test.go` - SBOM scanning and generation +- `client_server_test.go` - Client-server mode +- `docker_engine_test.go` - Docker Engine API integration +- `registry_test.go` - Container registry integration +- `config_test.go` - Configuration handling (CLI flags, env vars, config files) +- `vm_test.go` - Virtual machine image scanning +- `module_test.go` - Wasm module integration + +### Test Data Directory Structure + +``` +integration/testdata/ +├── *.golden # Golden files (expected test outputs) +└── fixtures/ # Test input files + ├── images/ # Container images (auto-downloaded) + ├── vm-images/ # VM images (auto-downloaded) + ├── repo/ # Repository and filesystem test data + ├── sbom/ # SBOM test files + └── ... +``` + +**Important**: `testdata/fixtures/images/` and `testdata/fixtures/vm-images/` are automatically downloaded by mage commands: +- `mage test:integration` downloads container images +- `mage test:vm` downloads VM images + +If you run tests directly with `go test` without using mage commands, these fixtures will not be present and tests will fail. Use mage commands to ensure fixtures are properly set up. + +## Troubleshooting + +### Golden file shared between tests shows unexpected differences + +1. Identify which test is the canonical source (has `override: nil`) +2. Update golden file from the canonical source test only +3. Adjust override functions in consumer tests to handle differences + +### Cannot update golden files for a specific test + +1. Check if the test has `if *update { t.Skipf(...) }` - this prevents updates +2. Find the canonical source test mentioned in the skip message +3. Update golden files from the canonical source test instead + +## Best Practices + +1. **One golden file, one updater**: Each golden file should be updated by exactly one test function +2. **Use `mage test:updateGolden`**: This automatically updates all golden files from canonical source tests +3. **Minimize golden file duplication**: Share golden files when testing equivalent functionality +4. **Keep override functions simple**: Complex overrides may indicate tests shouldn't share golden files +5. **Add `override: nil` comments**: Clearly mark canonical source tests in the code diff --git a/integration/client_server_test.go b/integration/client_server_test.go new file mode 100644 index 000000000000..9467adf61ecc --- /dev/null +++ b/integration/client_server_test.go @@ -0,0 +1,787 @@ +//go:build integration + +package integration + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +type csArgs struct { + Command string + RemoteAddrOption string + Format types.Format + TemplatePath string + IgnoreUnfixed bool + Severity []string + IgnoreIDs []string + Input string + ClientToken string + ClientTokenHeader string + PathPrefix string + ListAllPackages bool + Target string + secretConfig string + Distro string + VulnSeveritySources []string +} + +// TestClientServer tests the client-server mode of Trivy. +// +// Golden files are shared with TestTar or TestRepository. +func TestClientServer(t *testing.T) { + if *update { + t.Skipf("Skipping TestClientServer when -update flag is set. Golden files should be updated via TestTar or TestRepository.") + } + + tests := []struct { + name string + args csArgs + golden string + override func(t *testing.T, want, got *types.Report) + }{ + { + name: "alpine 3.9", + args: csArgs{ + Input: "testdata/fixtures/images/alpine-39.tar.gz", + }, + golden: goldenAlpine39, + }, + { + name: "alpine 3.9 as alpine 3.10", + args: csArgs{ + Input: "testdata/fixtures/images/alpine-39.tar.gz", + Distro: "alpine/3.10", + }, + override: func(_ *testing.T, want, _ *types.Report) { + want.Metadata.OS.Name = "3.10" + want.Results[0].Target = "testdata/fixtures/images/alpine-39.tar.gz (alpine 3.10)" + }, + golden: goldenAlpine39, + }, + { + name: "alpine 3.9 with high and critical severity", + args: csArgs{ + IgnoreUnfixed: true, + Severity: []string{ + "HIGH", + "CRITICAL", + }, + Input: "testdata/fixtures/images/alpine-39.tar.gz", + }, + golden: goldenAlpine39HighCritical, + }, + { + name: "alpine 3.9 with .trivyignore", + args: csArgs{ + IgnoreUnfixed: false, + IgnoreIDs: []string{ + "CVE-2019-1549", + "CVE-2019-14697", + }, + Input: "testdata/fixtures/images/alpine-39.tar.gz", + }, + golden: goldenAlpine39IgnoreCVEIDs, + }, + { + name: "alpine 3.10", + args: csArgs{ + Input: "testdata/fixtures/images/alpine-310.tar.gz", + }, + golden: goldenAlpine310JSON, + }, + { + name: "alpine distroless", + args: csArgs{ + Input: "testdata/fixtures/images/alpine-distroless.tar.gz", + }, + golden: goldenAlpineDistroless, + }, + { + name: "debian buster/10", + args: csArgs{ + Input: "testdata/fixtures/images/debian-buster.tar.gz", + }, + golden: goldenDebianBuster, + }, + { + name: "debian buster/10 with --ignore-unfixed option", + args: csArgs{ + IgnoreUnfixed: true, + Input: "testdata/fixtures/images/debian-buster.tar.gz", + }, + golden: goldenDebianBusterIgnoreUnfixed, + }, + { + name: "debian stretch/9", + args: csArgs{ + Input: "testdata/fixtures/images/debian-stretch.tar.gz", + }, + golden: goldenDebianStretch, + }, + { + name: "ubuntu 18.04", + args: csArgs{ + Input: "testdata/fixtures/images/ubuntu-1804.tar.gz", + }, + golden: goldenUbuntu1804, + }, + { + name: "centos 7", + args: csArgs{ + Input: "testdata/fixtures/images/centos-7.tar.gz", + }, + golden: goldenCentOS7, + }, + { + name: "centos 7 with --ignore-unfixed option", + args: csArgs{ + IgnoreUnfixed: true, + Input: "testdata/fixtures/images/centos-7.tar.gz", + }, + golden: goldenCentOS7IgnoreUnfixed, + }, + { + name: "centos 7 with medium severity", + args: csArgs{ + IgnoreUnfixed: true, + Severity: []string{"MEDIUM"}, + Input: "testdata/fixtures/images/centos-7.tar.gz", + }, + golden: goldenCentOS7Medium, + }, + { + name: "centos 6", + args: csArgs{ + Input: "testdata/fixtures/images/centos-6.tar.gz", + }, + golden: goldenCentOS6, + }, + { + name: "ubi 7", + args: csArgs{ + Input: "testdata/fixtures/images/ubi-7.tar.gz", + }, + golden: goldenUBI7, + }, + { + name: "almalinux 8", + args: csArgs{ + Input: "testdata/fixtures/images/almalinux-8.tar.gz", + }, + golden: goldenAlmaLinux8, + }, + { + name: "rocky linux 8", + args: csArgs{ + Input: "testdata/fixtures/images/rockylinux-8.tar.gz", + }, + golden: goldenRockyLinux8, + }, + { + name: "distroless base", + args: csArgs{ + Input: "testdata/fixtures/images/distroless-base.tar.gz", + }, + golden: goldenDistrolessBase, + }, + { + name: "distroless python27", + args: csArgs{ + Input: "testdata/fixtures/images/distroless-python27.tar.gz", + }, + golden: goldenDistrolessPython27, + }, + { + name: "amazon 1", + args: csArgs{ + Input: "testdata/fixtures/images/amazon-1.tar.gz", + }, + golden: goldenAmazon1, + }, + { + name: "amazon 2", + args: csArgs{ + Input: "testdata/fixtures/images/amazon-2.tar.gz", + }, + golden: goldenAmazon2, + }, + { + name: "oracle 8", + args: csArgs{ + Input: "testdata/fixtures/images/oraclelinux-8.tar.gz", + }, + golden: goldenOracleLinux8, + }, + { + name: "opensuse leap 15.1", + args: csArgs{ + Input: "testdata/fixtures/images/opensuse-leap-151.tar.gz", + }, + golden: goldenOpenSUSELeap151, + }, + { + name: "opensuse tumbleweed", + args: csArgs{ + Input: "testdata/fixtures/images/opensuse-tumbleweed.tar.gz", + }, + golden: goldenOpenSUSETumbleweed, + }, + { + name: "sle micro rancher 5.4", + args: csArgs{ + Input: "testdata/fixtures/images/sle-micro-rancher-5.4_ndb.tar.gz", + }, + golden: goldenSLMicroRancher54, + }, + { + name: "photon 3.0", + args: csArgs{ + Input: "testdata/fixtures/images/photon-30.tar.gz", + }, + golden: goldenPhoton30, + }, + { + name: "CBL-Mariner 1.0", + args: csArgs{ + Input: "testdata/fixtures/images/mariner-1.0.tar.gz", + }, + golden: goldenMariner10, + }, + { + name: "busybox with Cargo.lock", + args: csArgs{ + Input: "testdata/fixtures/images/busybox-with-lockfile.tar.gz", + }, + golden: goldenBusyboxWithLockfile, + }, + { + name: "scan pox.xml with repo command in client/server mode", + args: csArgs{ + Command: "repo", + RemoteAddrOption: "--server", + Target: "testdata/fixtures/repo/pom/", + }, + golden: goldenPom, + }, + { + name: "scan package-lock.json with repo command in client/server mode", + args: csArgs{ + Command: "repo", + RemoteAddrOption: "--server", + Target: "testdata/fixtures/repo/npm/", + ListAllPackages: true, + }, + golden: goldenNPM, + }, + { + name: "scan package-lock.json with severity from `ubuntu` in client/server mode", + args: csArgs{ + Command: "repo", + RemoteAddrOption: "--server", + Target: "testdata/fixtures/repo/npm/", + VulnSeveritySources: []string{ + "alpine", + "ubuntu", + }, + }, + golden: goldenNPMUbuntuSeverity, + }, + { + name: "scan sample.pem with repo command in client/server mode", + args: csArgs{ + Command: "repo", + RemoteAddrOption: "--server", + secretConfig: "testdata/fixtures/repo/secrets/trivy-secret.yaml", + Target: "testdata/fixtures/repo/secrets/", + }, + golden: goldenSecrets, + }, + { + name: "scan remote repository with repo command in client/server mode", + args: csArgs{ + Command: "repo", + RemoteAddrOption: "--server", + Target: "https://github.com/knqyf263/trivy-ci-test", + }, + golden: goldenTestRepo, + }, + } + + addr, cacheDir := setup(t, setupOptions{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := setupClient(t, tt.args, addr, cacheDir) + + if tt.args.secretConfig != "" { + osArgs = append(osArgs, "--secret-config", tt.args.secretConfig) + } + + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + override: overrideFuncs(overrideUID, overrideFingerprint, overrideServerInfo, tt.override), + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + } +} + +// TestClientServerWithFormat tests the client-server mode with various output formats. +// +// NOTE: Unlike TestClientServer, this test CAN update golden files with the -update flag +// because the golden files used here are not shared with other tests. These format-specific +// golden files (GitLab, SARIF, ASFF, etc.) are unique to this test and should be updated here. +func TestClientServerWithFormat(t *testing.T) { + tests := []struct { + name string + args csArgs + golden string + }{ + { + name: "alpine 3.10 with gitlab template", + args: csArgs{ + Format: "template", + TemplatePath: "@../contrib/gitlab.tpl", + Input: "testdata/fixtures/images/alpine-310.tar.gz", + }, + golden: goldenAlpine310GitLab, + }, + { + name: "scan package-lock.json with gitlab template (Unknown os and image)", + args: csArgs{ + Command: "fs", + Format: "template", + TemplatePath: "@../contrib/gitlab.tpl", + Target: "testdata/fixtures/repo/npm/", + ListAllPackages: true, + }, + golden: goldenNPMGitLab, + }, + { + name: "alpine 3.10 with gitlab-codequality template", + args: csArgs{ + Format: "template", + TemplatePath: "@../contrib/gitlab-codequality.tpl", + Input: "testdata/fixtures/images/alpine-310.tar.gz", + }, + golden: goldenAlpine310GitLabCodeQuality, + }, + { + name: "alpine 3.10 with sarif format", + args: csArgs{ + Format: "sarif", + Input: "testdata/fixtures/images/alpine-310.tar.gz", + }, + golden: goldenAlpine310SARIF, + }, + { + name: "alpine 3.10 with ASFF template", + args: csArgs{ + Format: "template", + TemplatePath: "@../contrib/asff.tpl", + Input: "testdata/fixtures/images/alpine-310.tar.gz", + }, + golden: goldenAlpine310ASFF, + }, + { + name: "scan secrets with ASFF template", + args: csArgs{ + Command: "repo", + RemoteAddrOption: "--server", + Format: "template", + TemplatePath: "@../contrib/asff.tpl", + Target: "testdata/fixtures/repo/secrets/", + }, + golden: goldenSecretsASFF, + }, + { + name: "alpine 3.10 with html template", + args: csArgs{ + Format: "template", + TemplatePath: "@../contrib/html.tpl", + Input: "testdata/fixtures/images/alpine-310.tar.gz", + }, + golden: goldenAlpine310HTML, + }, + { + name: "alpine 3.10 with junit template", + args: csArgs{ + Format: "template", + TemplatePath: "@../contrib/junit.tpl", + Input: "testdata/fixtures/images/alpine-310.tar.gz", + }, + golden: goldenAlpine310JUnit, + }, + { + name: "alpine 3.10 with github dependency snapshots format", + args: csArgs{ + Format: "github", + Input: "testdata/fixtures/images/alpine-310.tar.gz", + }, + golden: goldenAlpine310GSBOM, + }, + } + + fakeTime := time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC) + report.CustomTemplateFuncMap = map[string]any{ + "now": func() time.Time { + return fakeTime + }, + "date": func(format string, t time.Time) string { + return t.Format(format) + }, + } + + // For GitHub Dependency Snapshots + t.Setenv("GITHUB_REF", "/ref/feature-1") + t.Setenv("GITHUB_SHA", "39da54a1ff04120a31df8cbc94ce9ede251d21a3") + t.Setenv("GITHUB_JOB", "integration") + t.Setenv("GITHUB_RUN_ID", "1910764383") + t.Setenv("GITHUB_WORKFLOW", "workflow-name") + + t.Cleanup(func() { + report.CustomTemplateFuncMap = make(map[string]any) + }) + + addr, cacheDir := setup(t, setupOptions{}) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("AWS_REGION", "test-region") + t.Setenv("AWS_ACCOUNT_ID", "123456789012") + osArgs := setupClient(t, tt.args, addr, cacheDir) + + runTest(t, osArgs, tt.golden, tt.args.Format, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: nil, // Do not use overrides - golden files are generated from this test as the canonical source + }) + }) + } +} + +// TestClientServerWithCycloneDX tests the client-server mode with CycloneDX format. +// +// NOTE: This test CAN update golden files with the -update flag because the golden files +// used here are not shared with other tests. These format-specific golden files should be +// updated here. +func TestClientServerWithCycloneDX(t *testing.T) { + tests := []struct { + name string + args csArgs + golden string + }{ + { + name: "fluentd with RubyGems with CycloneDX format", + args: csArgs{ + Format: "cyclonedx", + Input: "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz", + }, + golden: goldenFluentdMultipleLockfilesCDX, + }, + } + + addr, cacheDir := setup(t, setupOptions{}) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := setupClient(t, tt.args, addr, cacheDir) + runTest(t, osArgs, tt.golden, types.FormatCycloneDX, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: nil, // Do not use overrides - golden files are generated from this test as the canonical source + }) + }) + } +} + +// TestClientServerWithCustomOptions tests the client-server mode with custom options. +// +// Golden files are shared with TestTar or TestRepository. +func TestClientServerWithCustomOptions(t *testing.T) { + if *update { + t.Skipf("Skipping TestClientServerWithCustomOptions when -update flag is set. Golden files should be updated via TestTar or TestRepository.") + } + + token := "token" + tokenHeader := "Trivy-Token" + pathPrefix := "prefix" + + tests := []struct { + name string + args csArgs + golden string + wantErr string + }{ + { + name: "alpine 3.9 with token and prefix", + args: csArgs{ + Input: "testdata/fixtures/images/alpine-39.tar.gz", + ClientToken: token, + ClientTokenHeader: tokenHeader, + PathPrefix: pathPrefix, + }, + golden: goldenAlpine39, + }, + { + name: "invalid token", + args: csArgs{ + Input: "testdata/fixtures/images/distroless-base.tar.gz", + ClientToken: "invalidtoken", + ClientTokenHeader: tokenHeader, + PathPrefix: pathPrefix, + }, + wantErr: "twirp error unauthenticated: invalid token", + }, + { + name: "invalid token header", + args: csArgs{ + Input: "testdata/fixtures/images/distroless-base.tar.gz", + ClientToken: token, + ClientTokenHeader: "Unknown-Header", + PathPrefix: pathPrefix, + }, + wantErr: "twirp error unauthenticated: invalid token", + }, + { + name: "wrong path prefix", + args: csArgs{ + Input: "testdata/fixtures/images/distroless-base.tar.gz", + ClientToken: token, + ClientTokenHeader: tokenHeader, + PathPrefix: "wrong", + }, + wantErr: "HTTP status code 404", + }, + } + + addr, cacheDir := setup(t, setupOptions{ + token: token, + tokenHeader: tokenHeader, + pathPrefix: pathPrefix, + }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := setupClient(t, tt.args, addr, cacheDir) + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + override: overrideFuncs(overrideUID, overrideServerInfo), + wantErr: tt.wantErr, + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + } +} + +// TestClientServerWithRedis tests the client-server mode with Redis cache backend. +// +// Golden files are shared with TestTar or TestRepository. +func TestClientServerWithRedis(t *testing.T) { + if *update { + t.Skipf("Skipping TestClientServerWithRedis when -update flag is set. Golden files should be updated via TestTar or TestRepository.") + } + + // Set up a Redis container + ctx := t.Context() + // This test includes 2 checks + // redisC container will terminate after first check + redisC, addr := setupRedis(t, ctx) + + // Set up Trivy server + addr, cacheDir := setup(t, setupOptions{cacheBackend: addr}) + t.Cleanup(func() { os.RemoveAll(cacheDir) }) + + // Test parameters + testArgs := csArgs{ + Input: "testdata/fixtures/images/alpine-39.tar.gz", + } + golden := goldenAlpine39 + + t.Run("alpine 3.9", func(t *testing.T) { + osArgs := setupClient(t, testArgs, addr, cacheDir) + + // Run Trivy client + runTest(t, osArgs, golden, types.FormatJSON, runOptions{ + override: overrideFuncs(overrideUID, overrideServerInfo), + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + + // Terminate the Redis container + require.NoError(t, testcontainers.TerminateContainer(redisC)) + + t.Run("sad path", func(t *testing.T) { + osArgs := setupClient(t, testArgs, addr, cacheDir) + + // Run Trivy client + runTest(t, osArgs, "", types.FormatJSON, runOptions{ + wantErr: "unable to store cache", + }) + }) +} + +type setupOptions struct { + token string + tokenHeader string + pathPrefix string + cacheBackend string +} + +func setup(t *testing.T, options setupOptions) (string, string) { + t.Helper() + + // Set up testing DB + cacheDir := initDB(t) + + // Set a temp dir so that modules will not be loaded + t.Setenv("XDG_DATA_HOME", cacheDir) + + port, err := getFreePort() + require.NoError(t, err) + addr := fmt.Sprintf("localhost:%d", port) + + go func() { + osArgs := setupServer(addr, options.token, options.tokenHeader, options.pathPrefix, cacheDir, options.cacheBackend) + + // Run Trivy server + assert.NoError(t, execute(osArgs)) + }() + + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + defer cancel() + err = waitPort(ctx, addr) + require.NoError(t, err) + + return addr, cacheDir +} + +func setupServer(addr, token, tokenHeader, pathPrefix, cacheDir, cacheBackend string) []string { + osArgs := []string{ + "--cache-dir", + cacheDir, + "server", + "--skip-db-update", + "--listen", + addr, + } + if token != "" { + osArgs = append(osArgs, "--token", token, "--token-header", tokenHeader) + } + if pathPrefix != "" { + osArgs = append(osArgs, "--path-prefix", pathPrefix) + } + if cacheBackend != "" { + osArgs = append(osArgs, "--cache-backend", cacheBackend) + } + return osArgs +} + +func setupClient(t *testing.T, c csArgs, addr, cacheDir string) []string { + t.Helper() + if c.Command == "" { + c.Command = "image" + } + if c.RemoteAddrOption == "" { + c.RemoteAddrOption = "--server" + } + osArgs := []string{ + "--cache-dir", + cacheDir, + c.Command, + c.RemoteAddrOption, + "http://" + addr, + "--quiet", + } + + if c.Format != "" { + osArgs = append(osArgs, "--format", string(c.Format)) + if c.TemplatePath != "" { + osArgs = append(osArgs, "--template", c.TemplatePath) + } + } else { + osArgs = append(osArgs, "--format", "json") + } + + if !c.ListAllPackages { + osArgs = append(osArgs, "--list-all-pkgs=false") + } + + if c.IgnoreUnfixed { + osArgs = append(osArgs, "--ignore-unfixed") + } + if len(c.Severity) != 0 { + osArgs = append(osArgs, + "--severity", strings.Join(c.Severity, ","), + ) + } + + if len(c.VulnSeveritySources) != 0 { + osArgs = append(osArgs, + "--vuln-severity-source", strings.Join(c.VulnSeveritySources, ","), + ) + } + + if len(c.IgnoreIDs) != 0 { + trivyIgnore := filepath.Join(t.TempDir(), ".trivyignore") + err := os.WriteFile(trivyIgnore, []byte(strings.Join(c.IgnoreIDs, "\n")), 0o444) + require.NoError(t, err, "failed to write .trivyignore") + osArgs = append(osArgs, "--ignorefile", trivyIgnore) + } + if c.ClientToken != "" { + osArgs = append(osArgs, "--token", c.ClientToken, "--token-header", c.ClientTokenHeader) + } + if c.PathPrefix != "" { + osArgs = append(osArgs, "--path-prefix", c.PathPrefix) + } + if c.Input != "" { + osArgs = append(osArgs, "--input", c.Input) + } + + if c.Target != "" { + osArgs = append(osArgs, c.Target) + } + + if c.Distro != "" { + osArgs = append(osArgs, "--distro", c.Distro) + } + + return osArgs +} + +func setupRedis(t *testing.T, ctx context.Context) (testcontainers.Container, string) { + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + t.Helper() + imageName := "redis:5.0" + port := "6379/tcp" + req := testcontainers.ContainerRequest{ + Name: "redis", + Image: imageName, + ExposedPorts: []string{port}, + AutoRemove: true, + } + + redis, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + require.NoError(t, err) + + ip, err := redis.Host(ctx) + require.NoError(t, err) + + p, err := redis.MappedPort(ctx, nat.Port(port)) + require.NoError(t, err) + + addr := fmt.Sprintf("redis://%s:%s", ip, p.Port()) + return redis, addr +} diff --git a/integration/config_test.go b/integration/config_test.go new file mode 100644 index 000000000000..a20e94bc5463 --- /dev/null +++ b/integration/config_test.go @@ -0,0 +1,233 @@ +//go:build integration + +package integration + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/types" +) + +// TestConfiguration tests the configuration of the CLI flags, environmental variables, and config file. +// +// Golden files are shared with TestRepository. +func TestConfiguration(t *testing.T) { + if *update { + t.Skipf("Skipping TestConfiguration when -update flag is set. Golden files should be updated via TestRepository.") + } + + type args struct { + input string + flags map[string]string + envs map[string]string + configFile string + } + type test struct { + name string + args args + golden string + wantErr string + } + + tests := []test{ + { + name: "skip files", + args: args{ + input: "testdata/fixtures/repo/gomod", + flags: map[string]string{ + "scanners": "vuln", + "skip-files": "path/to/dummy,testdata/fixtures/repo/gomod/submod2/go.mod", + }, + envs: map[string]string{ + "TRIVY_SCANNERS": "vuln", + "TRIVY_SKIP_FILES": "path/to/dummy,testdata/fixtures/repo/gomod/submod2/go.mod", + }, + configFile: `--- +scan: + scanners: + - vuln + skip-files: + - path/to/dummy + - testdata/fixtures/repo/gomod/submod2/go.mod +`, + }, + golden: goldenGoModSkip, + }, + { + name: "dockerfile with custom file pattern", + args: args{ + input: "testdata/fixtures/repo/dockerfile_file_pattern", + flags: map[string]string{ + "scanners": "misconfig", + "file-patterns": "dockerfile:Customfile", + "namespaces": "testing", + }, + envs: map[string]string{ + "TRIVY_SCANNERS": "misconfig", + "TRIVY_FILE_PATTERNS": "dockerfile:Customfile", + "TRIVY_NAMESPACES": "testing", + }, + configFile: `--- +scan: + scanners: + - misconfig + file-patterns: + - dockerfile:Customfile +rego: + skip-policy-update: true + namespaces: + - testing +`, + }, + golden: goldenDockerfileFilePattern, + }, + { + name: "key alias", // "--scanners" vs "--security-checks" + args: args{ + input: "testdata/fixtures/repo/gomod", + flags: map[string]string{ + "security-checks": "vuln", + }, + envs: map[string]string{ + "TRIVY_SECURITY_CHECKS": "vuln", + }, + configFile: `--- +scan: + security-checks: + - vuln +`, + }, + golden: goldenGoMod, + }, + { + name: "value alias", // "--scanners vuln" vs "--scanners vulnerability" + args: args{ + input: "testdata/fixtures/repo/gomod", + flags: map[string]string{ + "scanners": "vulnerability", + }, + envs: map[string]string{ + "TRIVY_SCANNERS": "vulnerability", + }, + configFile: `--- +scan: + scanners: + - vulnerability +`, + }, + golden: goldenGoMod, + }, + { + name: "invalid value", + args: args{ + input: "testdata/fixtures/repo/gomod", + flags: map[string]string{ + "scanners": "vulnerability", + "severity": "CRITICAL,INVALID", + }, + envs: map[string]string{ + "TRIVY_SCANNERS": "vulnerability", + "TRIVY_SEVERITY": "CRITICAL,INVALID", + }, + configFile: `--- +scan: + scanners: + - vulnerability +severity: + - CRITICAL + - INVALID +`, + }, + wantErr: `invalid argument "[CRITICAL INVALID]" for "--severity" flag`, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + // Set a temp dir so that modules will not be loaded + t.Setenv("XDG_DATA_HOME", cacheDir) + + // Disable Go license detection + t.Setenv("GOPATH", cacheDir) + + for _, tt := range tests { + command := "repo" + + t.Run(tt.name+" with CLI flags", func(t *testing.T) { + osArgs := []string{ + "--format", + "json", + "--list-all-pkgs=false", + "--cache-dir", + cacheDir, + "--skip-db-update", + "--skip-policy-update", + command, + tt.args.input, + } + for key, value := range tt.args.flags { + osArgs = append(osArgs, "--"+key, value) + } + + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + wantErr: tt.wantErr, + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + + t.Run(tt.name+" with environmental variables", func(t *testing.T) { + t.Setenv("TRIVY_FORMAT", "json") + t.Setenv("TRIVY_LIST_ALL_PKGS", "false") + t.Setenv("TRIVY_CACHE_DIR", cacheDir) + t.Setenv("TRIVY_SKIP_DB_UPDATE", "true") + t.Setenv("TRIVY_SKIP_POLICY_UPDATE", "true") + for key, value := range tt.args.envs { + t.Setenv(key, value) + } + + osArgs := []string{ + command, + tt.args.input, + } + + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + wantErr: tt.wantErr, + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + + t.Run(tt.name+" with config file", func(t *testing.T) { + configFile := tt.args.configFile + configFile += fmt.Sprintf(` +format: json +list-all-pkgs: false +cache: + dir: %s +db: + skip-update: true +`, cacheDir) + + configPath := filepath.Join(t.TempDir(), "trivy.yaml") + err := os.WriteFile(configPath, []byte(configFile), 0o444) + require.NoError(t, err) + + osArgs := []string{ + command, + "--config", + configPath, + tt.args.input, + } + + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + wantErr: tt.wantErr, + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + } +} diff --git a/integration/convert_test.go b/integration/convert_test.go new file mode 100644 index 000000000000..554f4cbdc75b --- /dev/null +++ b/integration/convert_test.go @@ -0,0 +1,85 @@ +//go:build integration + +package integration + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/types" +) + +// TestConvert tests the convert command with various output formats. +// +// NOTE: This test CAN update golden files with the -update flag because the golden files +// used here are not shared with other tests. These format conversion golden files are unique +// to this test and should be updated here. +func TestConvert(t *testing.T) { + type args struct { + input string + format string + scanners string + showSuppressed bool + listAllPkgs bool + } + tests := []struct { + name string + args args + golden string + }{ + { + name: "npm", + args: args{ + input: goldenNPM, + format: "cyclonedx", + }, + golden: goldenNPMCycloneDX, + }, + { + name: "npm without package UID", + args: args{ + input: "testdata/fixtures/convert/npm.json.golden", + format: "cyclonedx", + }, + golden: goldenNPMCycloneDX, + }, + { + name: "npm with suppressed vulnerability", + args: args{ + input: goldenConvertNPMWithSuppressed, + format: "json", + showSuppressed: true, + listAllPkgs: true, + }, + golden: goldenConvertNPMWithSuppressed, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := []string{ + "convert", + tt.args.input, + "--cache-dir", + t.TempDir(), + "-q", + "--format", + tt.args.format, + } + + if tt.args.showSuppressed { + osArgs = append(osArgs, "--show-suppressed") + } + + if !tt.args.listAllPkgs { + osArgs = append(osArgs, "--list-all-pkgs=false") + } + + // Run "trivy convert" + runTest(t, osArgs, tt.golden, types.Format(tt.args.format), runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: nil, // Do not use overrides - golden files are generated from this test as the canonical source + }) + }) + } + +} diff --git a/integration/data/auth_config/config.yml b/integration/data/auth_config/config.yml new file mode 100644 index 000000000000..a30db449750f --- /dev/null +++ b/integration/data/auth_config/config.yml @@ -0,0 +1,23 @@ +server: + addr: ":5001" + certificate: "/certs/cert.pem" + key: "/certs/key.pem" + +token: + issuer: "Trivy auth server" # Must match issuer in the Registry config. + expiration: 900 + +users: + # Password is specified as a BCrypt hash. Use `htpasswd -nB USERNAME` to generate. + "admin": + password: "$2y$05$LO.vzwpWC5LZGqThvEfznu8qhb5SGqvBSWY1J3yZ4AxtMRZ3kN5jC" # badmin + "test": + password: "$2y$05$WuwBasGDAgr.QCbGIjKJaep4dhxeai9gNZdmBnQXqpKly57oNutya" # 123 + +acl: + - match: {account: "admin"} + actions: ["*"] + comment: "Admin has full access to everything." + - match: {account: "test"} + actions: ["pull"] + comment: "User \"test\" can pull stuff." diff --git a/integration/data/certs/cert.pem b/integration/data/certs/cert.pem new file mode 100644 index 000000000000..921165489467 --- /dev/null +++ b/integration/data/certs/cert.pem @@ -0,0 +1,17 @@ +-----BEGIN CERTIFICATE----- +MIICwTCCAamgAwIBAgIJAP09YW8ChPlwMA0GCSqGSIb3DQEBCwUAMBIxEDAOBgNV +BAoMB0FjbWUgQ28wIBcNMjEwNTEyMDQ0NzA1WhgPMjEwMDA0MTQwNDQ3MDVaMBIx +EDAOBgNVBAoMB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQDNmKpDOzU8GK5Xb3GfeqU1kKQ0gBejGtqK5ydH8tlRoy2NKGvjJ95nhIxUXMKe +e345JFlzkCen5Ekvt70LT0O253z0FecfpaFilreIiu5J2YWWNtlruMhpjp4kYVMO +piKnujiNK9eAUcz++YeAmrog7QPBJBCgdu18xTy/yOW/Y414e1efvbRJZ4TaQb0Y +LgXRl1nlOLPPr5ew9pgnct7DxJVXpjXtgBxCsfcjH4kZGfc9zP0IKyODqaSCFRtj +eKH8gSpJCimBp3hpWvsSTHTRraOxAGXqhIYPhqRM83eB2QbeHnyk+YOn76pdMndb +vqAPksmTyHcgZShkhGcHKvbVAgMBAAGjGDAWMBQGA1UdEQQNMAuCCWxvY2FsaG9z +dDANBgkqhkiG9w0BAQsFAAOCAQEAHxXOTKGP1hl3J2jQrpha5LuYdMEbK1HFbPhV +042k0tBmfP3wRgx0o/WQhg4f5RswQRtipdUCmMZVOAoQfos8j9LFmIKwcsboEQe/ +Fvqq2+W/5TRhsKn/1OxvCZAEurazSygtm6hyiMGwKjJLfyzwjZx+Oopn3lqRUP36 +gLQQ57szoNZFKyPN2z2unXAuDG5wpG2InX8WJvlrhaiCHGUoxO8r0rVawm58bahM +uGPlVPCNdxl1h7K8aecKpm+7Wh8n06Nl/kOWBDFAXeI8IwrnIy1rAZLngvnjqL// +umjXKCBWya48ed9HMoOR2aruzseXc8k6cGXuBxYFtHissPvPPQ== +-----END CERTIFICATE----- diff --git a/integration/data/certs/key.pem b/integration/data/certs/key.pem new file mode 100644 index 000000000000..26b79f722008 --- /dev/null +++ b/integration/data/certs/key.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAzZiqQzs1PBiuV29xn3qlNZCkNIAXoxraiucnR/LZUaMtjShr +4yfeZ4SMVFzCnnt+OSRZc5Anp+RJL7e9C09Dtud89BXnH6WhYpa3iIruSdmFljbZ +a7jIaY6eJGFTDqYip7o4jSvXgFHM/vmHgJq6IO0DwSQQoHbtfMU8v8jlv2ONeHtX +n720SWeE2kG9GC4F0ZdZ5Tizz6+XsPaYJ3Lew8SVV6Y17YAcQrH3Ix+JGRn3Pcz9 +CCsjg6mkghUbY3ih/IEqSQopgad4aVr7Ekx00a2jsQBl6oSGD4akTPN3gdkG3h58 +pPmDp++qXTJ3W76gD5LJk8h3IGUoZIRnByr21QIDAQABAoIBAQCVN2ETjIxVgqA+ +K08u7Ses2b6jr/f31AybVasnx/S8EI+F7Llo003SmdvzeqNxvLVeqagWfKCbdM89 +R8B3zd6aiCYjTSZCzMZ1tGeePR83EB2paUOhsCocmnricpSChEeQrlJO+2vb4QLE +Z7xVtXazYPIhophCri4tKUWu+BLvNPez+TndaE5Xg77HLmu24rloZh6XhYDdFWd+ +u/eF+QiWy4/EoLUv2TLym8ivUws+r2G9yK57kcQCJw+BqlaRew7Ts0RHnam53OxV +T4dEHJxAfXO8jC1F5NCjoBO/+0HJqrMtD0NqWH9G+fEtakL7h5oeh6vYrSQfpZGC +V7MXojqdAoGBAOhwBi0erXOn4strtkGvSjJ6HVLwWfmm+rlfm23JGigYghYTSxBM +ESuwppt1QPXK5jfil89RqrvDqKG9BjXV4yWyaJlIRaYeJe8/TZa3e8WkLr0XaKGH +v1LTW+/uc73ihDJ/M2axmP4vjThCfqiG9aKXLCDM+DIgfdvIbkXUfPZjAoGBAOJw +Fc6D3z0r09F3/UgtADhQlbD2jzs6xdcqCu7af3527F6ePXXU8CTLS5jusAiDW5xH +ukQS/0ZM92UTUJxpQxgzHSWOImhcv3o45vzQ0C0pXSSaE+Pp8QYWaE/BdE+VoVOK +YGAfppZywPGnKYt4R5ho5XLwAL3rrH+2m7z51mdnAoGBANP5LbjCLF64Mb1f2pOm +f1zvPoTfyr5BSI/7n+yMJL2CNEhbie4v4MzeSeKmGPrO8grvK5EXIkQgGE5/6wT3 +rTI4tOltHo9zGRdJvMGBTXAd3b32diYxfQrU1BhIducph3PhyweRWTweM4SmJ4ob +ojGH+edj5ckZFo50CBTIxrmPAoGBAI8SpSSsfCRJiffjadzt2iK7AColT9DrzM+r +1+adlksQ1z7dmxXVqrqE3UpPHljyrrKrO40Bt9vyi6qIrrl1ZRhoS3VMPn9UgwO1 +6nU5dx/h7+FNnV23ljvzcotaP6R9dca0OzrhJMAQ18qYhY6DPDGXrcqWzNEzlPXJ +KtQXxBnnAoGAFQPvW/wDahrGcm1MBw83E0TgNJpoB10tz6R1dLdKVSHJUXMfxmij +Wj4MaF0JB0GWRRjutng+i7y7Tx+mUpu80qV8E9zAH7jGFnpqjw8A9zp5ftK00e7Y +shRlg+lhJhlvMA5QCYNzpYj+7EXJm7nzbhC6pMtBapT9a/MUPYFte38= +-----END RSA PRIVATE KEY----- diff --git a/integration/docker_engine_test.go b/integration/docker_engine_test.go new file mode 100644 index 000000000000..c6094c592a70 --- /dev/null +++ b/integration/docker_engine_test.go @@ -0,0 +1,412 @@ +//go:build integration + +package integration + +import ( + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/types" +) + +type overrideValues struct { + artifactID string + imageID string +} + +var ( + alpineIDs = overrideValues{ + artifactID: "sha256:243ee36277d8f2a79a980373843fe7834a484a024cd7e647d6db3d0434335e6b", + imageID: "sha256:e1b6a1e6b446c39fa12f218e3050e59b47a870de1b1afca840dce65e913b196f", + } + + centOS7IDs = overrideValues{ + artifactID: "sha256:14bd400fe6aba45977110fadb29ab402fed2356ae68d071ed8e6ffe95ec4e387", + imageID: "sha256:c51c4bac51fc7a3d6b6035336eb6a3cc9d57113dd1c10b2109e14dfc3a509740", + } + + debianBusterIDs = overrideValues{ + artifactID: "sha256:542c2a0b2641139673b479e67ae5bd3e6c62559e3fc882dcf2679b6b0ffb9837", + imageID: "sha256:c46a5c17bdd6698cc02e4dd15d033898840e1e0f8016dcc86bb1f5d3239a9cb6", + } + + ubuntu1804IDs = overrideValues{ + artifactID: "sha256:133f1c0f30523d26ea2686df45a950b80fb957f8da83c10c01b95df3acfeedc6", + imageID: "sha256:50c3ea3e170e9e02f880dd630d46f6bafd0966066b7d5ff1424e4268412c5344", + } +) + +// TestDockerEngine tests scanning images via Docker Engine API. +// +// Golden files are shared with TestTar. +func TestDockerEngine(t *testing.T) { + if *update { + t.Skipf("Skipping TestDockerEngine when -update flag is set. Golden files should be updated via TestTar.") + } + + tests := []struct { + name string + invalidImage bool + ignoreUnfixed bool + ignoreStatus []string + severity []string + ignoreIDs []string + maxImageSize string + input string + override overrideValues + golden string + wantErr string + }{ + { + name: "alpine:3.9", + input: "testdata/fixtures/images/alpine-39.tar.gz", + golden: goldenAlpine39, + override: alpineIDs, + }, + { + name: "alpine:3.9, with max image size", + maxImageSize: "100mb", + input: "testdata/fixtures/images/alpine-39.tar.gz", + golden: goldenAlpine39, + override: alpineIDs, + }, + { + name: "alpine:3.9, with high and critical severity", + severity: []string{ + "HIGH", + "CRITICAL", + }, + input: "testdata/fixtures/images/alpine-39.tar.gz", + golden: goldenAlpine39HighCritical, + override: alpineIDs, + }, + { + name: "alpine:3.9, with .trivyignore", + ignoreIDs: []string{ + "CVE-2019-1549", + "CVE-2019-14697", + }, + input: "testdata/fixtures/images/alpine-39.tar.gz", + golden: goldenAlpine39IgnoreCVEIDs, + override: alpineIDs, + }, + { + name: "alpine:3.10", + input: "testdata/fixtures/images/alpine-310.tar.gz", + golden: goldenAlpine310JSON, + override: overrideValues{ + artifactID: "sha256:61bd381a03b7e96b5950ac465077b9b104cd7ac5a38aa57b64ae0c99bb2470d0", + imageID: "sha256:f12582b2f2190f350e3904462c1c23aaf366b4f76705e97b199f9bbded1d816a", + }, + }, + { + name: "amazonlinux:1", + input: "testdata/fixtures/images/amazon-1.tar.gz", + golden: goldenAmazon1, + override: overrideValues{ + artifactID: "sha256:372784e31f6a7c4efc3d433e193d668ac61d4e1533e308267cdf1bffb7d0dafb", + imageID: "sha256:83b15d0365d6ff7b7c45595bf29a999cdcc03d8d7899fcb74639319ad3f9f18a", + }, + }, + { + name: "amazonlinux:2", + input: "testdata/fixtures/images/amazon-2.tar.gz", + golden: goldenAmazon2, + override: overrideValues{ + artifactID: "sha256:9ed60ac6eedb461967fd109f8721d8761379d6f38a6ea770d10fbc9a280dd1a6", + imageID: "sha256:b553b1064cde62ceee90a8575f4954ef9adda2e45a9e712fbcc67b63114cd940", + }, + }, + { + name: "almalinux 8", + input: "testdata/fixtures/images/almalinux-8.tar.gz", + golden: goldenAlmaLinux8, + override: overrideValues{ + artifactID: "sha256:2ef11ad6245281caff084e034208cd2a71e195405cb15396feb6d4e7cf83a587", + imageID: "sha256:52318004f70433054028789494879e77893a956437fb3a4842b69a1a762e83f4", + }, + }, + { + name: "rocky linux 8", + input: "testdata/fixtures/images/rockylinux-8.tar.gz", + golden: goldenRockyLinux8, + override: overrideValues{ + artifactID: "sha256:280a62f2a00f406b1e74187ea60f0fe95de83b9a5a5dfaef382b7c58ce6fd196", + imageID: "sha256:566c2bd67e04ad98ca170128846ed3ab326ff90caad4a1715ed4f47939cc3f69", + }, + }, + { + name: "centos 6", + input: "testdata/fixtures/images/centos-6.tar.gz", + golden: goldenCentOS6, + override: overrideValues{ + artifactID: "sha256:e910d8a213217aefe45b1cc7923bda2a60398f6d3e0d958926892abff9c10da4", + imageID: "sha256:8b35caa27ab5e27bdf5aef411cd6ee0b6c7ad7777cb9cfcc37f2949ca273cdb5", + }, + }, + { + name: "centos 7", + input: "testdata/fixtures/images/centos-7.tar.gz", + golden: goldenCentOS7, + override: centOS7IDs, + }, + { + name: "centos 7, with --ignore-unfixed option", + ignoreUnfixed: true, + input: "testdata/fixtures/images/centos-7.tar.gz", + golden: goldenCentOS7IgnoreUnfixed, + override: centOS7IDs, + }, + { + name: "centos 7, with --ignore-status option", + ignoreStatus: []string{"will_not_fix"}, + input: "testdata/fixtures/images/centos-7.tar.gz", + golden: goldenCentOS7IgnoreUnfixed, + override: centOS7IDs, + }, + { + name: "centos 7, with --ignore-unfixed option, with medium severity", + ignoreUnfixed: true, + severity: []string{"MEDIUM"}, + input: "testdata/fixtures/images/centos-7.tar.gz", + golden: goldenCentOS7Medium, + override: centOS7IDs, + }, + { + name: "registry.redhat.io/ubi7", + input: "testdata/fixtures/images/ubi-7.tar.gz", + golden: goldenUBI7, + override: overrideValues{ + artifactID: "sha256:af6cb66f90c404bc6c3d69c3d9f16c10c272af173a236a17fd4b9fa7f791a4eb", + imageID: "sha256:a80dd41cbeea43204ae0f77c7e18a39512563803f85425fc6cdab45f52158e73", + }, + }, + { + name: "debian buster/10", + input: "testdata/fixtures/images/debian-buster.tar.gz", + golden: goldenDebianBuster, + override: debianBusterIDs, + }, + { + name: "debian buster/10, with --ignore-unfixed option", + ignoreUnfixed: true, + input: "testdata/fixtures/images/debian-buster.tar.gz", + golden: goldenDebianBusterIgnoreUnfixed, + override: debianBusterIDs, + }, + { + name: "debian buster/10, with --ignore-status option", + ignoreStatus: []string{"affected"}, + input: "testdata/fixtures/images/debian-buster.tar.gz", + golden: goldenDebianBusterIgnoreUnfixed, + override: debianBusterIDs, + }, + { + name: "debian stretch/9", + input: "testdata/fixtures/images/debian-stretch.tar.gz", + golden: goldenDebianStretch, + override: overrideValues{ + artifactID: "sha256:ae095a2b1168c65ba0300011f1eda9a792f3f6a2ee0b50c899be29c652fb0439", + imageID: "sha256:fc77e6160638c0695323710349cb88597e451f9e4ff265e7f1fea888aead83fb", + }, + }, + { + name: "distroless base", + input: "testdata/fixtures/images/distroless-base.tar.gz", + golden: goldenDistrolessBase, + override: overrideValues{ + artifactID: "sha256:50b91b7954a592a5582854a23f16ca5d8d206a70ce68eb2c84f81721546050fa", + imageID: "sha256:2257d71271eb9b723b25d7dc17998ca0cf3dd3ffb145c68addebabb6956633d8", + }, + }, + { + name: "distroless python2.7", + input: "testdata/fixtures/images/distroless-python27.tar.gz", + golden: goldenDistrolessPython27, + override: overrideValues{ + artifactID: "sha256:e13c0afa7d2811598be25b369cd3dc78c455fd0a5f8415993dd2f46d360ccd51", + imageID: "sha256:eb04cafc9222da50d0647e515e9409712cf8c9ce0e6ea149e24cf01525dfd613", + }, + }, + { + name: "oracle linux 8", + input: "testdata/fixtures/images/oraclelinux-8.tar.gz", + golden: goldenOracleLinux8, + override: overrideValues{ + artifactID: "sha256:a818222f173267637277bd66d4a3dddcd7a271ea5686d72848b2006ab4f2b999", + imageID: "sha256:5bad2539f9e86fe1c81636eed263ab8166b7a6508ebaa1084ec5695fd4855fe1", + }, + }, + { + name: "ubuntu 18.04", + input: "testdata/fixtures/images/ubuntu-1804.tar.gz", + golden: goldenUbuntu1804, + override: ubuntu1804IDs, + }, + { + name: "ubuntu 18.04, with --ignore-unfixed option", + ignoreUnfixed: true, + input: "testdata/fixtures/images/ubuntu-1804.tar.gz", + golden: goldenUbuntu1804IgnoreUnfixed, + override: ubuntu1804IDs, + }, + { + name: "opensuse leap 15.1", + input: "testdata/fixtures/images/opensuse-leap-151.tar.gz", + golden: goldenOpenSUSELeap151, + override: overrideValues{ + artifactID: "sha256:b48df6bf517580ff3b846112870f09d1a42399af52a740ce69233ba16d5228f0", + imageID: "sha256:38d0b4bb21b5e2095d0ce40c69c4d24e4ae0e76bbe4a45db53afe87d2e1ba228", + }, + }, + { + name: "opensuse tumbleweed", + input: "testdata/fixtures/images/opensuse-tumbleweed.tar.gz", + golden: goldenOpenSUSETumbleweed, + override: overrideValues{ + artifactID: "sha256:28c7ebf409a2ca8a1d33d5479a19a543f228baec9b22f41df7dfacd95ca6ed0e", + imageID: "sha256:b59aaf21088bbdd92253b5a3d415a4f48a2103007935840a651ac691284a051e", + }, + }, + { + name: "sle micro rancher 5.4", + input: "testdata/fixtures/images/sle-micro-rancher-5.4_ndb.tar.gz", + golden: goldenSLMicroRancher54, + override: overrideValues{ + artifactID: "sha256:6c1e0eadb53697c85176149ead4287154dfbc1dd91ef439d199a84fd7c60dc59", + imageID: "sha256:6e58434b86592f28909d8fafc030694d8c6b630eeb94ad4208431b207a6acd1a", + }, + }, + { + name: "photon 3.0", + input: "testdata/fixtures/images/photon-30.tar.gz", + golden: goldenPhoton30, + override: overrideValues{ + artifactID: "sha256:2b7cef41f06fa33e05c64f408fcda876d57d87141aa4ee1fb8a50fb56b875b84", + imageID: "sha256:0041aaabd5852bf345e8f6d70f8596c0a0ab9c15695193977bfa3abc58d3a41d", + }, + }, + { + name: "CBL-Mariner 1.0", + input: "testdata/fixtures/images/mariner-1.0.tar.gz", + golden: goldenMariner10, + override: overrideValues{ + artifactID: "sha256:e85742037b190a22f7ff747e466bd66c302bbc7266f04c7518ba9ab146ca7f43", + imageID: "sha256:330dd5ae6d537d2dc18e89a56409a2dab6d3afff8c6cd6e16943c1b340a828a9", + }, + }, + { + name: "busybox with Cargo.lock", + input: "testdata/fixtures/images/busybox-with-lockfile.tar.gz", + golden: goldenBusyboxWithLockfile, + override: overrideValues{ + artifactID: "sha256:e0edf846fd89372b0869a4826791a1c22dd620e75e32b586bc7e7bba245793a4", + imageID: "sha256:51dccb50b93f4f7d860045f19b34fd9eeca83fec890bdba10e0b706d6289a8bf", + }, + }, + { + name: "sad path, invalid image", + invalidImage: true, + input: "badimage:latest", + wantErr: "unable to inspect the image (badimage:latest)", + }, + { + name: "sad path, image size is larger than the maximum", + input: "testdata/fixtures/images/alpine-39.tar.gz", + maxImageSize: "3mb", + wantErr: "uncompressed layers size 5.8MB exceeds maximum allowed size 3MB", + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + // Set a temp dir so that modules will not be loaded + t.Setenv("XDG_DATA_HOME", cacheDir) + + ctx := t.Context() + defer ctx.Done() + + cli := testutil.NewDockerClient(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + imageName := tt.input + if !tt.invalidImage { + // Removes any existing images with conflicting RepoTags and loading images + imageName = cli.ImageCleanLoad(t, ctx, tt.input) + } + + osArgs := []string{ + "image", + imageName, + "--cache-dir", + cacheDir, + "--quiet", + "--skip-db-update", + "--format=json", + "--list-all-pkgs=false", + "--image-src=docker", + } + + if tt.ignoreUnfixed { + osArgs = append(osArgs, "--ignore-unfixed") + } + + if len(tt.ignoreStatus) != 0 { + osArgs = append(osArgs, + []string{ + "--ignore-status", + strings.Join(tt.ignoreStatus, ","), + }..., + ) + } + if len(tt.severity) != 0 { + osArgs = append(osArgs, + []string{ + "--severity", + strings.Join(tt.severity, ","), + }..., + ) + } + if len(tt.ignoreIDs) != 0 { + trivyIgnore := ".trivyignore" + err := os.WriteFile(trivyIgnore, []byte(strings.Join(tt.ignoreIDs, "\n")), 0o444) + require.NoError(t, err, "failed to write .trivyignore") + defer os.Remove(trivyIgnore) + } + + if tt.maxImageSize != "" { + osArgs = append(osArgs, []string{ + "--max-image-size", + tt.maxImageSize, + }...) + } + + // Run Trivy + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + wantErr: tt.wantErr, + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + // Image config fields were removed + override: overrideFuncs(overrideUID, overrideFingerprint, overrideDockerRemovedFields, func(t *testing.T, want, got *types.Report) { + // Override ArtifactName to match the archive file path + got.ArtifactName = tt.input + + // Override Result.Target for each result to match golden file expectations + require.Len(t, got.Results, len(want.Results)) + for i := range got.Results { + got.Results[i].Target = want.Results[i].Target + } + + // Docker API v0.29.0+ calculates the imageID digest based on the manifest, rather than the config file. + want.ArtifactID = tt.override.artifactID + want.Metadata.ImageID = tt.override.imageID + }), + }) + }) + } +} diff --git a/integration/integration_test.go b/integration/integration_test.go new file mode 100644 index 000000000000..5dd571f8c499 --- /dev/null +++ b/integration/integration_test.go @@ -0,0 +1,550 @@ +//go:build integration || vm_integration || module_integration || k8s_integration + +package integration + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "io" + "net" + "os" + "path/filepath" + "sort" + "strings" + "testing" + "time" + + cdx "github.com/CycloneDX/cyclonedx-go" + spdxjson "github.com/spdx/tools-golang/json" + "github.com/spdx/tools-golang/spdx" + "github.com/spdx/tools-golang/spdxlib" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xeipuuv/gojsonschema" + + "github.com/aquasecurity/trivy-db/pkg/metadata" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/commands" + "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" + "github.com/aquasecurity/trivy/pkg/vex/repo" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" + + _ "modernc.org/sqlite" +) + +var update = flag.Bool("update", false, "update golden files") + +const SPDXSchema = "https://raw.githubusercontent.com/spdx/spdx-spec/support/v%s/schemas/spdx-schema.json" + +// Golden file paths +const ( + // Container image tests (docker_engine_test.go, client_server_test.go, standalone_tar_test.go, registry_test.go) + goldenAlmaLinux8 = "testdata/almalinux-8.json.golden" + goldenAlpine39 = "testdata/alpine-39.json.golden" + goldenAlpine39HighCritical = "testdata/alpine-39-high-critical.json.golden" + goldenAlpine39IgnoreCVEIDs = "testdata/alpine-39-ignore-cveids.json.golden" + goldenAlpine39Skip = "testdata/alpine-39-skip.json.golden" + goldenAlpine310JSON = "testdata/alpine-310.json.golden" + goldenAlpine310ASFF = "testdata/alpine-310.asff.golden" + goldenAlpine310GitLab = "testdata/alpine-310.gitlab.golden" + goldenAlpine310GitLabCodeQuality = "testdata/alpine-310.gitlab-codequality.golden" + goldenAlpine310GSBOM = "testdata/alpine-310.gsbom.golden" + goldenAlpine310HTML = "testdata/alpine-310.html.golden" + goldenAlpine310JUnit = "testdata/alpine-310.junit.golden" + goldenAlpine310SARIF = "testdata/alpine-310.sarif.golden" + goldenAlpineDistroless = "testdata/alpine-distroless.json.golden" + goldenAmazon1 = "testdata/amazon-1.json.golden" + goldenAmazon2 = "testdata/amazon-2.json.golden" + goldenBusyboxWithLockfile = "testdata/busybox-with-lockfile.json.golden" + goldenCentOS6 = "testdata/centos-6.json.golden" + goldenCentOS7 = "testdata/centos-7.json.golden" + goldenCentOS7IgnoreUnfixed = "testdata/centos-7-ignore-unfixed.json.golden" + goldenCentOS7Medium = "testdata/centos-7-medium.json.golden" + goldenDebianBuster = "testdata/debian-buster.json.golden" + goldenDebianBusterIgnoreUnfixed = "testdata/debian-buster-ignore-unfixed.json.golden" + goldenDebianStretch = "testdata/debian-stretch.json.golden" + goldenDistrolessBase = "testdata/distroless-base.json.golden" + goldenDistrolessPython27 = "testdata/distroless-python27.json.golden" + goldenFluentdGems = "testdata/fluentd-gems.json.golden" + goldenFluentdMultipleLockfilesCDX = "testdata/fluentd-multiple-lockfiles.cdx.json.golden" + goldenMariner10 = "testdata/mariner-1.0.json.golden" + goldenNPM = "testdata/npm.json.golden" + goldenNPMGitLab = "testdata/npm.gitlab.golden" + goldenNPMUbuntuSeverity = "testdata/npm-ubuntu-severity.json.golden" + goldenOpenSUSELeap151 = "testdata/opensuse-leap-151.json.golden" + goldenOpenSUSETumbleweed = "testdata/opensuse-tumbleweed.json.golden" + goldenOracleLinux8 = "testdata/oraclelinux-8.json.golden" + goldenPhoton30 = "testdata/photon-30.json.golden" + goldenPom = "testdata/pom.json.golden" + goldenRockyLinux8 = "testdata/rockylinux-8.json.golden" + goldenSecrets = "testdata/secrets.json.golden" + goldenSecretsASFF = "testdata/secrets.asff.golden" + goldenSLMicroRancher54 = "testdata/sl-micro-rancher5.4.json.golden" + goldenTestRepo = "testdata/test-repo.json.golden" + goldenUBI7 = "testdata/ubi-7.json.golden" + goldenUBI7Comprehensive = "testdata/ubi-7-comprehensive.json.golden" + goldenUbuntu1804 = "testdata/ubuntu-1804.json.golden" + goldenUbuntu1804IgnoreUnfixed = "testdata/ubuntu-1804-ignore-unfixed.json.golden" + + // Repository/Filesystem tests (repo_test.go, config_test.go) + goldenBun = "testdata/bun.json.golden" + goldenCargoLock = "testdata/cargo.lock.json.golden" + goldenCocoaPods = "testdata/cocoapods.json.golden" + goldenComposerLock = "testdata/composer.lock.json.golden" + goldenComposerVendor = "testdata/composer.vendor.json.golden" + goldenConan = "testdata/conan.json.golden" + goldenCondaCycloneDX = "testdata/conda-cyclonedx.json.golden" + goldenCondaEnvironmentCycloneDX = "testdata/conda-environment-cyclonedx.json.golden" + goldenCondaSPDX = "testdata/conda-spdx.json.golden" + goldenDockerfile = "testdata/dockerfile.json.golden" + goldenDockerfileCustomPolicies = "testdata/dockerfile-custom-policies.json.golden" + goldenDockerfileFilePattern = "testdata/dockerfile_file_pattern.json.golden" + goldenDotNet = "testdata/dotnet.json.golden" + goldenGoMod = "testdata/gomod.json.golden" + goldenGoModSkip = "testdata/gomod-skip.json.golden" + goldenGoModVEX = "testdata/gomod-vex.json.golden" + goldenGradle = "testdata/gradle.json.golden" + goldenHelm = "testdata/helm.json.golden" + goldenHelmBadName = "testdata/helm_badname.json.golden" + goldenHelmTestChart = "testdata/helm_testchart.json.golden" + goldenHelmTestChartOverridden = "testdata/helm_testchart.overridden.json.golden" + goldenJuliaSPDX = "testdata/julia-spdx.json.golden" + goldenMixLock = "testdata/mix.lock.json.golden" + goldenNPMWithDev = "testdata/npm-with-dev.json.golden" + goldenNuGet = "testdata/nuget.json.golden" + goldenPackagesProps = "testdata/packagesprops.json.golden" + goldenPip = "testdata/pip.json.golden" + goldenPipenv = "testdata/pipenv.json.golden" + goldenPnpm = "testdata/pnpm.json.golden" + goldenPoetry = "testdata/poetry.json.golden" + goldenPomCycloneDX = "testdata/pom-cyclonedx.json.golden" + goldenPubspecLock = "testdata/pubspec.lock.json.golden" + goldenSBT = "testdata/sbt.json.golden" + goldenSwift = "testdata/swift.json.golden" + goldenTerraformExcludeMisconfigsRemoteModule = "testdata/terraform-exclude-misconfs-remote-module.json.golden" + goldenTerraformOpenTofuRegistry = "testdata/terraform-opentofu-registry.json.golden" + goldenTerraformRemoteModule = "testdata/terraform-remote-module.json.golden" + goldenTerraformRemoteModuleInChild = "testdata/terraform-remote-module-in-child.json.golden" + goldenTerraformRemoteSubmodule = "testdata/terraform-remote-submodule.json.golden" + goldenTerraformTerraformRegistry = "testdata/terraform-terraform-registry.json.golden" + goldenUV = "testdata/uv.json.golden" + goldenYarn = "testdata/yarn.json.golden" + + // SBOM tests (sbom_test.go) + goldenFluentdMultipleLockfiles = "testdata/fluentd-multiple-lockfiles.json.golden" + goldenFluentdMultipleLockfilesShortCDX = "testdata/fluentd-multiple-lockfiles-short.cdx.json.golden" + goldenLicenseCycloneDX = "testdata/license-cyclonedx.json.golden" + goldenMinikubeKBOM = "testdata/minikube-kbom.json.golden" + + // Convert tests (convert_test.go) + goldenNPMCycloneDX = "testdata/npm-cyclonedx.json.golden" + goldenConvertNPMWithSuppressed = "testdata/fixtures/convert/npm-with-suppressed.json.golden" + + // VM tests (vm_test.go) + goldenAmazonLinux2GP2X86VM = "testdata/amazonlinux2-gp2-x86-vm.json.golden" + goldenUbuntuGP2X86VM = "testdata/ubuntu-gp2-x86-vm.json.golden" + + // Module tests (module_test.go) + goldenSpring4ShellJRE8 = "testdata/spring4shell-jre8.json.golden" + goldenSpring4ShellJRE11 = "testdata/spring4shell-jre11.json.golden" + + // Plugin tests (plugin_test.go) + goldenCountPlugin020 = "testdata/count-0.2.0-plugin.txt.golden" + goldenCountPlugin010WithBeforeFlag = "testdata/count-0.1.0-plugin-with-before-flag.txt.golden" +) + +func initDB(t *testing.T) string { + fixtureDir := filepath.Join("testdata", "fixtures", "db") + entries, err := os.ReadDir(fixtureDir) + require.NoError(t, err) + + var fixtures []string + for _, entry := range entries { + if entry.IsDir() { + continue + } + fixtures = append(fixtures, filepath.Join(fixtureDir, entry.Name())) + } + + cacheDir := dbtest.InitDB(t, fixtures) + defer dbtest.Close() + + err = metadata.NewClient(db.Dir(cacheDir)).Update(metadata.Metadata{ + Version: db.SchemaVersion, + NextUpdate: time.Now().Add(24 * time.Hour), + UpdatedAt: time.Now(), + DownloadedAt: time.Now(), + }) + require.NoError(t, err) + + dbtest.InitJavaDB(t, cacheDir) + return cacheDir +} + +func initVEXRepository(t *testing.T, homeDir, cacheDir string) { + t.Helper() + + // Copy config directory + configSrc := "testdata/fixtures/vex/config/repository.yaml" + configDst := filepath.Join(homeDir, ".trivy", "vex", "repository.yaml") + testutil.CopyFile(t, configSrc, configDst) + + // Copy repository directory + repoSrc := "testdata/fixtures/vex/repositories" + repoDst := filepath.Join(cacheDir, "vex", "repositories") + testutil.CopyDir(t, repoSrc, repoDst) + + // Copy VEX file + vexSrc := "testdata/fixtures/vex/file/openvex.json" + repoDir := filepath.Join(repoDst, "default") + vexDst := filepath.Join(repoDir, "0.1", "openvex.json") + testutil.CopyFile(t, vexSrc, vexDst) + + // Write a dummy cache metadata + testutil.MustWriteJSON(t, filepath.Join(repoDir, "cache.json"), repo.CacheMetadata{ + UpdatedAt: time.Now(), + }) + + // Verify that necessary files exist + requiredFiles := []string{ + configDst, + filepath.Join(repoDir, "vex-repository.json"), + filepath.Join(repoDir, "0.1", "index.json"), + filepath.Join(repoDir, "0.1", "openvex.json"), + } + + for _, file := range requiredFiles { + require.FileExists(t, file) + } +} + +func getFreePort() (int, error) { + addr, err := net.ResolveTCPAddr("tcp", "localhost:0") + if err != nil { + return 0, err + } + + l, err := net.ListenTCP("tcp", addr) + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +func waitPort(ctx context.Context, addr string) error { + for { + conn, err := net.Dial("tcp", addr) + if err == nil && conn != nil { + return nil + } + select { + case <-ctx.Done(): + return err + default: + time.Sleep(1 * time.Second) + } + } +} + +func readReport(t *testing.T, filePath string) types.Report { + t.Helper() + + f, err := os.Open(filePath) + require.NoError(t, err, filePath) + defer f.Close() + + var report types.Report + err = json.NewDecoder(f).Decode(&report) + require.NoError(t, err, filePath) + + // We don't compare history because the nano-seconds in "created" don't match + report.Metadata.ImageConfig.History = nil + + // We don't compare repo digests because archives don't contain them + report.Metadata.RepoDigests = nil + for i := range report.Metadata.Layers { + report.Metadata.Layers[i].Digest = "" + } + + for i, result := range report.Results { + for j := range result.Vulnerabilities { + report.Results[i].Vulnerabilities[j].Layer.Digest = "" + } + + sort.Slice(result.CustomResources, func(i, j int) bool { + if result.CustomResources[i].Type != result.CustomResources[j].Type { + return result.CustomResources[i].Type < result.CustomResources[j].Type + } + return result.CustomResources[i].FilePath < result.CustomResources[j].FilePath + }) + } + + return report +} + +func readCycloneDX(t *testing.T, filePath string) *cdx.BOM { + f, err := os.Open(filePath) + require.NoError(t, err) + defer f.Close() + + bom := cdx.NewBOM() + decoder := cdx.NewBOMDecoder(f, cdx.BOMFileFormatJSON) + err = decoder.Decode(bom) + require.NoError(t, err) + + // Sort components + if bom.Components != nil { + sort.Slice(*bom.Components, func(i, j int) bool { + return (*bom.Components)[i].Name < (*bom.Components)[j].Name + }) + for i := range *bom.Components { + sort.Slice(*(*bom.Components)[i].Properties, func(ii, jj int) bool { + return (*(*bom.Components)[i].Properties)[ii].Name < (*(*bom.Components)[i].Properties)[jj].Name + }) + } + sort.Slice(*bom.Vulnerabilities, func(i, j int) bool { + return (*bom.Vulnerabilities)[i].ID < (*bom.Vulnerabilities)[j].ID + }) + } + + return bom +} + +func readSpdxJson(t *testing.T, filePath string) *spdx.Document { + f, err := os.Open(filePath) + require.NoError(t, err) + defer f.Close() + + bom, err := spdxjson.Read(f) + require.NoError(t, err) + + sort.Slice(bom.Relationships, func(i, j int) bool { + if bom.Relationships[i].RefA.ElementRefID != bom.Relationships[j].RefA.ElementRefID { + return bom.Relationships[i].RefA.ElementRefID < bom.Relationships[j].RefA.ElementRefID + } + return bom.Relationships[i].RefB.ElementRefID < bom.Relationships[j].RefB.ElementRefID + }) + + sort.Slice(bom.Files, func(i, j int) bool { + return bom.Files[i].FileSPDXIdentifier < bom.Files[j].FileSPDXIdentifier + }) + + // We don't compare values which change each time an SBOM is generated + bom.CreationInfo.Created = "" + bom.DocumentNamespace = "" + + return bom +} + +type OverrideFunc func(t *testing.T, want, got *types.Report) +type runOptions struct { + wantErr string + override OverrideFunc + fakeUUID string +} + +// runTest runs Trivy with the given args and compares the output with the golden file. +// The output file is created in a temporary directory, unless -update flag is set, in which case +// the golden file is updated directly. +func runTest(t *testing.T, osArgs []string, wantFile string, format types.Format, opts runOptions) { + // Ensure that tests updating golden files don't use override functions + // as overrides would modify the golden file output + if *update && opts.override != nil { + require.Fail(t, "invalid test configuration", "cannot use override functions when -update is set") + } + + if opts.fakeUUID != "" { + uuid.SetFakeUUID(t, opts.fakeUUID) + } + // Set fake UUID v7 for ReportID generation. Format is not configurable. + uuid.SetFakeUUIDV7(t, "017b7d41-e09f-7000-80ea-%012d") + + // Set up the output file + outputFile := filepath.Join(t.TempDir(), "output.json") + if *update { + outputFile = wantFile + } + osArgs = append(osArgs, "--output", outputFile) + + // Run Trivy + err := execute(osArgs) + if opts.wantErr != "" { + require.ErrorContains(t, err, opts.wantErr) + return + } + require.NoError(t, err) + + // Compare want and got + switch format { + case types.FormatCycloneDX: + compareCycloneDX(t, wantFile, outputFile) + case types.FormatSPDXJSON: + compareSPDXJson(t, wantFile, outputFile) + case types.FormatJSON: + compareReports(t, wantFile, outputFile, opts.override) + case types.FormatTemplate, types.FormatSarif, types.FormatGitHub: + compareRawFiles(t, wantFile, outputFile) + default: + require.Fail(t, "invalid format", "format: %s", format) + } +} + +func execute(osArgs []string) error { + // viper.XXX() (e.g. viper.ReadInConfig()) affects the global state, so we need to reset it after each test. + defer viper.Reset() + + // Set a fake time + ctx := clock.With(context.Background(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + + // Setup CLI App + app := commands.NewApp() + app.SetOut(io.Discard) + app.SetArgs(osArgs) + + // Run Trivy + return app.ExecuteContext(ctx) +} + +func compareRawFiles(t *testing.T, wantFile, gotFile string) { + want, err := os.ReadFile(wantFile) + require.NoError(t, err) + got, err := os.ReadFile(gotFile) + require.NoError(t, err) + assert.Equal(t, string(want), string(got)) +} + +func compareReports(t *testing.T, wantFile, gotFile string, override func(t *testing.T, want, got *types.Report)) { + want := readReport(t, wantFile) + got := readReport(t, gotFile) + if override != nil { + override(t, &want, &got) + } + + assert.Equal(t, want, got) +} + +func compareCycloneDX(t *testing.T, wantFile, gotFile string) { + want := readCycloneDX(t, wantFile) + got := readCycloneDX(t, gotFile) + assert.Equal(t, want, got) + + // Validate CycloneDX output against the JSON schema + validateReport(t, got.JSONSchema, got) +} + +func compareSPDXJson(t *testing.T, wantFile, gotFile string) { + want := readSpdxJson(t, wantFile) + got := readSpdxJson(t, gotFile) + assert.Equal(t, want, got) + + SPDXVersion, ok := strings.CutPrefix(want.SPDXVersion, "SPDX-") + assert.True(t, ok) + + require.NoError(t, spdxlib.ValidateDocument(got)) + + // Validate SPDX output against the JSON schema + validateReport(t, fmt.Sprintf(SPDXSchema, SPDXVersion), got) +} + +func validateReport(t *testing.T, schema string, report any) { + schemaLoader := gojsonschema.NewReferenceLoader(schema) + documentLoader := gojsonschema.NewGoLoader(report) + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + require.NoError(t, err) + + if valid := result.Valid(); !valid { + errs := xslices.Map(result.Errors(), func(err gojsonschema.ResultError) string { + return err.String() + }) + assert.True(t, valid, strings.Join(errs, "\n")) + } +} + +func overrideFuncs(funcs ...OverrideFunc) OverrideFunc { + return func(t *testing.T, want, got *types.Report) { + for _, f := range funcs { + if f == nil { + continue + } + f(t, want, got) + } + } +} + +// overrideUID only checks for the presence of the package UID and clears the UID; +// the UID is calculated from the package metadata, but the UID does not match +// as it varies slightly depending on the mode of scanning, e.g. the digest of the layer. +func overrideUID(t *testing.T, want, got *types.Report) { + for i, result := range got.Results { + for j, vuln := range result.Vulnerabilities { + assert.NotEmptyf(t, vuln.PkgIdentifier.UID, "UID is empty: %s", vuln.VulnerabilityID) + // Do not compare UID as the package metadata is slightly different between the tests, + // causing different UIDs. + got.Results[i].Vulnerabilities[j].PkgIdentifier.UID = "" + } + } + for i, result := range want.Results { + for j := range result.Vulnerabilities { + want.Results[i].Vulnerabilities[j].PkgIdentifier.UID = "" + } + } +} + +// overrideFingerprint only checks for the presence of the fingerprint and clears it; +// the fingerprint is calculated from artifactID, target, pkgID, and vulnerabilityID, +// but may not match as the artifactID can vary depending on the scanning context. +func overrideFingerprint(t *testing.T, want, got *types.Report) { + for i, result := range got.Results { + for j, vuln := range result.Vulnerabilities { + assert.NotEmptyf(t, vuln.Fingerprint, "Fingerprint is empty: %s", vuln.VulnerabilityID) + assert.Lenf(t, vuln.Fingerprint, 71, "Fingerprint should be 71 characters (sha256: + 64 hex chars): %s", vuln.VulnerabilityID) + // Do not compare Fingerprint as the artifactID varies between tests + got.Results[i].Vulnerabilities[j].Fingerprint = "" + } + } + for i, result := range want.Results { + for j := range result.Vulnerabilities { + want.Results[i].Vulnerabilities[j].Fingerprint = "" + } + } +} + +// overrideDockerRemovedFields clears image config fields that were removed from Docker API +// cf. https://github.com/moby/moby/blob/1f71f2217d2196239ca52685ce6b3c4f93a1cc07/api/docs/CHANGELOG.md +func overrideDockerRemovedFields(_ *testing.T, want, got *types.Report) { + // Clear Container field (removed in Docker API v1.45) + got.Metadata.ImageConfig.Container = "" + want.Metadata.ImageConfig.Container = "" + + // Clear Image field (removed in Docker API v1.50) + got.Metadata.ImageConfig.Config.Image = "" + want.Metadata.ImageConfig.Config.Image = "" + + // Clear Hostname field (removed in Docker API v1.50) + got.Metadata.ImageConfig.Config.Hostname = "" + want.Metadata.ImageConfig.Config.Hostname = "" + + // Clear DockerVersion field (omitted in Docker API v1.52) + want.Metadata.ImageConfig.DockerVersion = "" +} + +// overrideServerInfo verifies that Server info exists and then clears it. +// Server info is only populated in client/server mode (empty in standalone mode). +// When golden files are shared with standalone tests, this override is needed +// because standalone tests don't produce server info. +func overrideServerInfo(t *testing.T, want, got *types.Report) { + // Verify that server info was actually fetched + assert.NotEmpty(t, got.Trivy.Server.Version, "Server version should be set in client/server mode") + + // Clear server info for comparison with shared golden files + got.Trivy.Server = types.VersionInfo{} + want.Trivy.Server = types.VersionInfo{} +} diff --git a/integration/k8s_test.go b/integration/k8s_test.go new file mode 100644 index 000000000000..2f93b245cd68 --- /dev/null +++ b/integration/k8s_test.go @@ -0,0 +1,208 @@ +//go:build k8s_integration + +package integration + +import ( + "encoding/json" + "os" + "path/filepath" + "sort" + "testing" + + cdx "github.com/CycloneDX/cyclonedx-go" + + "github.com/aquasecurity/trivy/pkg/k8s/report" + "github.com/aquasecurity/trivy/pkg/types" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestK8s tests Kubernetes cluster scanning. +// +// NOTE: This test CAN update golden files with the -update flag. The K8s-specific golden files +// are unique to this test and not shared with other tests. +// Requires k8s (kind) cluster installed. Run with "mage test:k8s". +func TestK8s(t *testing.T) { + // Set up testing DB + cacheDir := initDB(t) + t.Run("misconfig and vulnerability scan", func(t *testing.T) { + // Set up the output file + outputFile := filepath.Join(t.TempDir(), "output.json") + + // it uses a fixed version of trivy-checks bundle - v1.11.2 + // its hash is sha256:f3ea8227f838a985f0c884909e9d226362f5fc5ab6021310a179fbb24c5b57fd + osArgs := []string{ + "--cache-dir", + cacheDir, + "k8s", + "kind-kind-test", + "--report", + "summary", + "--checks-bundle-repository", + "mirror.gcr.io/aquasec/trivy-checks:1.11.2@sha256:f3ea8227f838a985f0c884909e9d226362f5fc5ab6021310a179fbb24c5b57fd", + "--include-namespaces", "default", + "--debug", + "--timeout", + "5m0s", + "--format", + "json", + "--output", + outputFile, + } + + // Run Trivy + err := execute(osArgs) + require.NoError(t, err) + + var got report.ConsolidatedReport + f, err := os.Open(outputFile) + require.NoError(t, err) + defer f.Close() + + err = json.NewDecoder(f).Decode(&got) + require.NoError(t, err) + + // Flatten findings + results := lo.FlatMap(got.Findings, func(resource report.Resource, _ int) []types.Result { + return resource.Results + }) + + // Collect IDs (CVEs for vulns, IDs for failed misconfigs), allowing duplicates. + ids := k8sFindingIDs{} + for _, r := range results { + for _, v := range r.Vulnerabilities { + if v.VulnerabilityID != "" { + ids.Vulnerabilities = append(ids.Vulnerabilities, v.VulnerabilityID) + } + } + for _, m := range r.Misconfigurations { + if m.Status == types.MisconfStatusFailure && m.ID != "" { + ids.Misconfigurations = append(ids.Misconfigurations, m.ID) + } + } + } + + // Sort for deterministic golden files + sort.Strings(ids.Vulnerabilities) + sort.Strings(ids.Misconfigurations) + + fixture := filepath.Join("testdata", "fixtures", "k8s", "summary-ids.json.golden") + if *update { + // Update fixture with current IDs (duplicates kept, sorted) + // Note: mage test:k8s may create additional k8s artifacts. + f, err := os.Create(fixture) + require.NoError(t, err) + defer f.Close() + enc := json.NewEncoder(f) + enc.SetIndent("", " ") + require.NoError(t, enc.Encode(ids)) + t.Logf("updated fixture: %s", fixture) + return + } + + // Read expected IDs from fixture and compare + ef, err := os.Open(fixture) + require.NoError(t, err) + defer ef.Close() + + var want k8sFindingIDs + require.NoError(t, json.NewDecoder(ef).Decode(&want)) + assert.Equal(t, want, ids) + }) + t.Run("kbom cycloneDx", func(t *testing.T) { + // Set up the output file + outputFile := filepath.Join(t.TempDir(), "output.json") + osArgs := []string{ + "k8s", + "kind-kind-test", + "--format", + "cyclonedx", + "-q", + "--output", + outputFile, + } + + // Run Trivy + err := execute(osArgs) + require.NoError(t, err) + + var got *cdx.BOM + f, err := os.Open(outputFile) + require.NoError(t, err) + defer f.Close() + + err = json.NewDecoder(f).Decode(&got) + require.NoError(t, err) + + assert.Equal(t, got.Metadata.Component.Name, "k8s.io/kubernetes") + assert.Equal(t, got.Metadata.Component.Type, cdx.ComponentType("platform")) + + // Has components + assert.True(t, len(*got.Components) > 0) + + // Has dependecies + assert.True(t, lo.SomeBy(*got.Dependencies, func(r cdx.Dependency) bool { + return len(*r.Dependencies) > 0 + })) + }) + t.Run("limited user test", func(t *testing.T) { + // Set up the output file + outputFile := filepath.Join(t.TempDir(), "output.json") + + osArgs := []string{ + "--cache-dir", + cacheDir, + "k8s", + "limitedcontext", + "--kubeconfig", + "limitedconfig", + "--report", + "summary", + "-q", + "--timeout", + "5m0s", + "--include-namespaces", + "limitedns", + "--format", + "json", + "--output", + outputFile, + } + + // Run Trivy + err := execute(osArgs) + require.NoError(t, err) + + var got report.ConsolidatedReport + f, err := os.Open(outputFile) + require.NoError(t, err) + defer f.Close() + + err = json.NewDecoder(f).Decode(&got) + require.NoError(t, err) + + // Flatten findings + results := lo.FlatMap(got.Findings, func(resource report.Resource, _ int) []types.Result { + return resource.Results + }) + + // Has vulnerabilities + assert.True(t, lo.SomeBy(results, func(r types.Result) bool { + return len(r.Vulnerabilities) > 0 + })) + + // Has misconfigurations + assert.True(t, lo.SomeBy(results, func(r types.Result) bool { + return len(r.Misconfigurations) > 0 + })) + + }) +} + +// k8sFindingIDs is the structure saved into the golden file. +type k8sFindingIDs struct { + Vulnerabilities []string `json:"vulnerabilities"` + Misconfigurations []string `json:"misconfigurations"` +} diff --git a/integration/module_test.go b/integration/module_test.go new file mode 100644 index 000000000000..64387275b90e --- /dev/null +++ b/integration/module_test.go @@ -0,0 +1,71 @@ +//go:build module_integration + +package integration + +import ( + "path/filepath" + "testing" + + "github.com/aquasecurity/trivy/pkg/extension" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/types" +) + +// TestModule tests Trivy with Wasm modules. +// +// NOTE: This test CAN update golden files with the -update flag because the golden files +// used here are not shared with other tests. These module-specific golden files are unique +// to this test and should be updated here. +func TestModule(t *testing.T) { + tests := []struct { + name string + input string + golden string + }{ + { + name: "spring4shell jre 8, severity update", + input: "testdata/fixtures/images/spring4shell-jre8.tar.gz", + golden: goldenSpring4ShellJRE8, + }, + { + name: "spring4shell jre 11, no severity update", + input: "testdata/fixtures/images/spring4shell-jre11.tar.gz", + golden: goldenSpring4ShellJRE11, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := []string{ + "--cache-dir", + cacheDir, + "image", + "--ignore-unfixed", + "--format", + "json", + "--list-all-pkgs=false", + "--skip-db-update", + "--offline-scan", + "--quiet", + "--module-dir", + filepath.Join("../", "examples", "module", "spring4shell"), + "--input", + tt.input, + } + + t.Cleanup(func() { + analyzer.DeregisterAnalyzer("spring4shell") + extension.DeregisterHook("spring4shell") + }) + + // Run Trivy + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: nil, // Do not use overrides - golden files are generated from this test as the canonical source + }) + }) + } +} diff --git a/integration/plugin_test.go b/integration/plugin_test.go new file mode 100644 index 000000000000..f63e3ca0d85e --- /dev/null +++ b/integration/plugin_test.go @@ -0,0 +1,117 @@ +//go:build integration + +package integration + +import ( + "io" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +// TestPlugin tests Trivy plugins. +// +// NOTE: This test CAN update golden files with the -update flag because the golden files +// used here are not shared with other tests. These plugin-specific golden files are unique +// to this test and should be updated here. +func TestPlugin(t *testing.T) { + tests := []struct { + name string + plugin string + pluginArgs string + golden string + }{ + { + name: "count plugin installed from `index`", + plugin: "count@v0.2.0", + golden: goldenCountPlugin020, + }, + { + name: "count plugin installed from github archive", + plugin: "https://github.com/aquasecurity/trivy-plugin-count/archive/refs/tags/v0.1.0.zip", + pluginArgs: "--published-before=2020-01-01", + golden: goldenCountPlugin010WithBeforeFlag, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + tempStdOut := setTempStdout(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // We can overwrite stdout for `_default_Manager` only once. + // So we need to clear the temporary stdout file before each test case. + clearFile(t, tempStdOut) + + t.Setenv("XDG_DATA_HOME", t.TempDir()) + + // Install plugin + err := execute([]string{ + "plugin", + "install", + tt.plugin, + }) + require.NoError(t, err) + + // Get list of plugins + err = execute([]string{ + "plugin", + "list", + }) + require.NoError(t, err) + + // Run Trivy with plugin as output + args := []string{ + "--cache-dir", + cacheDir, + "fs", + "-f", + "json", + "-o", + "plugin=count", + "testdata/fixtures/repo/pip", + } + + if tt.pluginArgs != "" { + args = append(args, "--output-plugin-arg", tt.pluginArgs) + } + + err = execute(args) + require.NoError(t, err) + + if *update { + fsutils.CopyFile(tempStdOut.Name(), tt.golden) + } + + compareRawFiles(t, tt.golden, tempStdOut.Name()) + }) + } +} + +func setTempStdout(t *testing.T) *os.File { + tmpFile := filepath.Join(t.TempDir(), "output.txt") + f, err := os.Create(tmpFile) + require.NoError(t, err) + + // Overwrite Stdout to get output of plugin + defaultStdout := os.Stdout + os.Stdout = f + t.Cleanup(func() { + os.Stdout = defaultStdout + f.Close() + }) + return f +} + +func clearFile(t *testing.T, file *os.File) { + _, err := file.Seek(0, io.SeekStart) + require.NoError(t, err) + + _, err = file.Write([]byte{}) + require.NoError(t, err) +} diff --git a/integration/registry_test.go b/integration/registry_test.go new file mode 100644 index 000000000000..d0630e2a109e --- /dev/null +++ b/integration/registry_test.go @@ -0,0 +1,407 @@ +//go:build integration + +package integration + +import ( + "bytes" + "compress/gzip" + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/docker/go-connections/nat" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + registryImage = "registry:2.7.0" + registryPort = "5443/tcp" + + authImage = "cesanta/docker_auth:1" + authPort = "5001/tcp" + authUsername = "admin" + authPassword = "badmin" +) + +func setupRegistry(ctx context.Context, baseDir string, authURL *url.URL) (testcontainers.Container, error) { + req := testcontainers.ContainerRequest{ + Name: "registry", + Image: registryImage, + ExposedPorts: []string{registryPort}, + Env: map[string]string{ + "REGISTRY_HTTP_ADDR": "0.0.0.0:5443", + "REGISTRY_HTTP_TLS_CERTIFICATE": "/certs/cert.pem", + "REGISTRY_HTTP_TLS_KEY": "/certs/key.pem", + "REGISTRY_AUTH": "token", + "REGISTRY_AUTH_TOKEN_REALM": fmt.Sprintf("%s/auth", authURL), + "REGISTRY_AUTH_TOKEN_SERVICE": "registry.docker.io", + "REGISTRY_AUTH_TOKEN_ISSUER": "Trivy auth server", + "REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE": "/certs/cert.pem", + "REGISTRY_AUTH_TOKEN_AUTOREDIRECT": "false", + }, + Mounts: testcontainers.Mounts( + testcontainers.BindMount(filepath.Join(baseDir, "data", "certs"), "/certs"), + ), + AutoRemove: true, + WaitingFor: wait.ForHTTP("v2").WithTLS(true).WithAllowInsecure(true). + WithStatusCodeMatcher(func(status int) bool { + return status == http.StatusUnauthorized + }), + } + + registryC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + return registryC, err +} + +func setupAuthServer(ctx context.Context, baseDir string) (testcontainers.Container, error) { + req := testcontainers.ContainerRequest{ + Name: "docker_auth", + Image: authImage, + ExposedPorts: []string{authPort}, + Mounts: testcontainers.Mounts( + testcontainers.BindMount(filepath.Join(baseDir, "data", "auth_config"), "/config"), + testcontainers.BindMount(filepath.Join(baseDir, "data", "certs"), "/certs"), + ), + AutoRemove: true, + Cmd: []string{"/config/config.yml"}, + } + + authC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + return authC, err +} + +func getURL(ctx context.Context, container testcontainers.Container, exposedPort nat.Port) (*url.URL, error) { + ip, err := container.Host(ctx) + if err != nil { + return nil, err + } + + port, err := container.MappedPort(ctx, exposedPort) + if err != nil { + return nil, err + } + + urlStr := fmt.Sprintf("https://%s:%s", ip, port.Port()) + return url.Parse(urlStr) +} + +type registryOption struct { + AuthURL *url.URL + Username string + Password string + RegistryToken bool + AuthLogin bool +} + +// TestRegistry tests scanning images from a container registry. +// +// Golden files are shared with TestTar. +func TestRegistry(t *testing.T) { + if *update { + t.Skipf("Skipping TestRegistry when -update flag is set. Golden files should be updated via TestTar.") + } + + ctx := t.Context() + + baseDir, err := filepath.Abs(".") + require.NoError(t, err) + + // disable Reaper for auth server and registry containers + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + + // set up auth server + authC, err := setupAuthServer(ctx, baseDir) + require.NoError(t, err) + testcontainers.CleanupContainer(t, authC) + + authURL, err := getURL(ctx, authC, authPort) + require.NoError(t, err) + + // set up registry + registryC, err := setupRegistry(ctx, baseDir, authURL) + require.NoError(t, err) + testcontainers.CleanupContainer(t, registryC) + + registryURL, err := getURL(ctx, registryC, registryPort) + require.NoError(t, err) + + auth := &authn.Basic{ + Username: authUsername, + Password: authPassword, + } + + tests := []struct { + name string + imageName string + imageFile string + os string + option registryOption + golden string + wantErr string + }{ + { + name: "authenticate with username/password", + imageName: "alpine:3.10", + imageFile: "testdata/fixtures/images/alpine-310.tar.gz", + os: "alpine 3.10.2", + option: registryOption{ + Username: authUsername, + Password: authPassword, + }, + golden: goldenAlpine310JSON, + }, + { + name: "authenticate with registry token", + imageName: "alpine:3.10", + imageFile: "testdata/fixtures/images/alpine-310.tar.gz", + os: "alpine 3.10.2", + option: registryOption{ + AuthURL: authURL, + Username: authUsername, + Password: authPassword, + RegistryToken: true, + }, + golden: goldenAlpine310JSON, + }, + { + name: "authenticate with 'trivy registry login'", + imageName: "alpine:3.10", + imageFile: "testdata/fixtures/images/alpine-310.tar.gz", + os: "alpine 3.10.2", + option: registryOption{ + Username: authUsername, + Password: authPassword, + AuthLogin: true, + }, + golden: goldenAlpine310JSON, + }, + { + name: "amazonlinux 2", + imageName: "amazonlinux:2", + imageFile: "testdata/fixtures/images/amazon-2.tar.gz", + os: "amazon 2 (Karoo)", + option: registryOption{ + Username: authUsername, + Password: authPassword, + }, + golden: goldenAmazon2, + }, + { + name: "debian buster", + imageName: "debian:buster", + imageFile: "testdata/fixtures/images/debian-buster.tar.gz", + os: "debian 10.1", + option: registryOption{ + Username: authUsername, + Password: authPassword, + }, + golden: goldenDebianBuster, + }, + { + name: "sad path", + imageName: "alpine:3.10", + imageFile: "testdata/fixtures/images/alpine-310.tar.gz", + wantErr: "unexpected status code 401 Unauthorized: Auth failed", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := fmt.Sprintf("%s/%s", registryURL.Host, tt.imageName) + imageRef, err := name.ParseReference(s) + require.NoError(t, err) + + // Load a test image from the tar file, tag it and push to the test registry. + err = replicateImage(imageRef, tt.imageFile, auth) + require.NoError(t, err) + + osArgs, err := scan(t, imageRef, baseDir, tt.option) + require.NoError(t, err) + + // Run Trivy + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + wantErr: tt.wantErr, + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: overrideFuncs(overrideUID, overrideFingerprint, func(t *testing.T, want, got *types.Report) { + // Exclude ArtifactID from comparison because registry tests use random ports + // (e.g., localhost:54321/alpine:3.10), which causes RepoTags and the calculated + // Artifact ID to vary on each test run. + got.ArtifactID = "" + want.ArtifactID = "" + + want.ArtifactName = s + want.Metadata.RepoTags = []string{s} + want.Metadata.Reference = testutil.MustParseReference(t, s) + for i := range want.Results { + want.Results[i].Target = fmt.Sprintf("%s (%s)", s, tt.os) + } + }), + }) + }) + } +} + +func scan(t *testing.T, imageRef name.Reference, baseDir string, opt registryOption) ([]string, error) { + // Set up testing DB + cacheDir := initDB(t) + + // Set a temp dir so that modules will not be loaded + t.Setenv("XDG_DATA_HOME", cacheDir) + + // Setup env + if err := setupEnv(t, imageRef, baseDir, opt); err != nil { + return nil, err + } + + osArgs := []string{ + "-q", + "--cache-dir", + cacheDir, + "image", + "--format", + "json", + "--list-all-pkgs=false", + "--image-src", + "remote", + "--skip-db-update", + imageRef.Name(), + } + + return osArgs, nil +} + +func setupEnv(t *testing.T, imageRef name.Reference, baseDir string, opt registryOption) error { + t.Setenv("TRIVY_INSECURE", "true") + + if opt.Username != "" && opt.Password != "" { + switch { + case opt.RegistryToken: + // Get a registry token in advance + token, err := requestRegistryToken(imageRef, baseDir, opt) + if err != nil { + return err + } + t.Setenv("TRIVY_REGISTRY_TOKEN", token) + case opt.AuthLogin: + t.Setenv("DOCKER_CONFIG", t.TempDir()) + err := execute([]string{ + "registry", + "login", + "--username", + opt.Username, + "--password", + opt.Password, + "--insecure", + imageRef.Context().RegistryStr(), + }) + if err != nil { + return err + } + default: + t.Setenv("TRIVY_USERNAME", opt.Username) + t.Setenv("TRIVY_PASSWORD", opt.Password) + } + } + return nil +} + +func requestRegistryToken(imageRef name.Reference, baseDir string, opt registryOption) (string, error) { + // Create a CA certificate pool and add cert.pem to it + caCert, err := os.ReadFile(filepath.Join(baseDir, "data", "certs", "cert.pem")) + if err != nil { + return "", err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + // Create a HTTPS client and supply the created CA pool + client := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: caCertPool, + }, + }, + } + + // Get a registry token + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/auth", opt.AuthURL), http.NoBody) + if err != nil { + return "", err + } + + // Set query parameters + values := req.URL.Query() + values.Set("service", "registry.docker.io") + values.Set("scope", imageRef.Scope("pull")) + req.URL.RawQuery = values.Encode() + + req.SetBasicAuth(opt.Username, opt.Password) + resp, err := client.Do(req) + if err != nil { + return "", err + } + defer resp.Body.Close() + + type res struct { + AccessToken string `json:"access_token"` + } + + var r res + if err = json.NewDecoder(resp.Body).Decode(&r); err != nil { + return "", err + } + + return r.AccessToken, nil +} + +// ReplicateImage tags the given imagePath and pushes it to the given dest registry. +func replicateImage(imageRef name.Reference, imagePath string, auth authn.Authenticator) error { + img, err := tarball.Image(func() (io.ReadCloser, error) { + b, err := os.ReadFile(imagePath) + if err != nil { + return nil, err + } + gr, err := gzip.NewReader(bytes.NewReader(b)) + if err != nil { + return nil, err + } + return io.NopCloser(gr), nil + }, nil) + if err != nil { + return err + } + + t := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + } + + err = remote.Write(imageRef, img, remote.WithAuth(auth), remote.WithTransport(t)) + if err != nil { + return err + } + + return nil +} diff --git a/integration/repo_test.go b/integration/repo_test.go new file mode 100644 index 000000000000..0fa3d4753f9d --- /dev/null +++ b/integration/repo_test.go @@ -0,0 +1,687 @@ +//go:build integration + +package integration + +import ( + "cmp" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +type repoTestArgs struct { + scanner types.Scanner + ignoreIDs []string + policyPaths []string + namespaces []string + listAllPkgs bool + input string + secretConfig string + filePatterns []string + helmSet []string + helmValuesFile []string + skipFiles []string + skipDirs []string + command string + format types.Format + includeDevDeps bool + parallel int + vex string + vulnSeveritySources []string + tfExcludeDownloadedModules bool +} + +// TestRepository tests `trivy repo` with the local code repositories. +// +// NOTE: This test CAN update golden files with the -update flag. +// This is the canonical source for repository/filesystem scanning golden files. +// Golden files generated here may be shared with other tests like TestRepositoryWithOverride, +// TestConfiguration, and TestClientServerWithRedis (when scanning repositories). +func TestRepository(t *testing.T) { + t.Setenv("NUGET_PACKAGES", t.TempDir()) + tests := []struct { + name string + args repoTestArgs + golden string + }{ + { + name: "gomod", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/gomod", + }, + golden: goldenGoMod, + }, + { + name: "gomod with skip files", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/gomod", + skipFiles: []string{"testdata/fixtures/repo/gomod/submod2/go.mod"}, + }, + golden: goldenGoModSkip, + }, + { + name: "gomod with skip dirs", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/gomod", + skipDirs: []string{"testdata/fixtures/repo/gomod/submod2"}, + }, + golden: goldenGoModSkip, + }, + { + name: "gomod in series", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/gomod", + parallel: 1, + }, + golden: goldenGoMod, + }, + { + name: "gomod with local VEX file", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/gomod", + vex: "testdata/fixtures/vex/file/openvex.json", + }, + golden: goldenGoModVEX, + }, + { + name: "gomod with VEX repository", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/gomod", + vex: "repo", + }, + golden: goldenGoModVEX, + }, + { + name: "npm", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/npm", + listAllPkgs: true, + }, + golden: goldenNPM, + }, + { + name: "npm with severity from ubuntu", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/npm", + vulnSeveritySources: []string{ + "alpine", + "ubuntu", + }, + }, + golden: goldenNPMUbuntuSeverity, + }, + { + name: "npm with dev deps", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/npm", + listAllPkgs: true, + includeDevDeps: true, + }, + golden: goldenNPMWithDev, + }, + { + name: "yarn", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/yarn", + listAllPkgs: true, + }, + golden: goldenYarn, + }, + { + name: "pnpm", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/pnpm", + listAllPkgs: true, + }, + golden: goldenPnpm, + }, + { + name: "bun", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/bun", + listAllPkgs: true, + }, + golden: goldenBun, + }, + { + name: "pip", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/pip", + }, + golden: goldenPip, + }, + { + name: "pipenv", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/pipenv", + }, + golden: goldenPipenv, + }, + { + name: "poetry", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/poetry", + }, + golden: goldenPoetry, + }, + { + name: "uv", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/uv", + }, + golden: goldenUV, + }, + { + name: "pom", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/pom", + }, + golden: goldenPom, + }, + { + name: "gradle", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/gradle", + }, + golden: goldenGradle, + }, + { + name: "sbt", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/sbt", + }, + golden: goldenSBT, + }, + { + name: "conan", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/conan", + }, + golden: goldenConan, + }, + { + name: "nuget", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/nuget", + }, + golden: goldenNuGet, + }, + { + name: "dotnet", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/dotnet", + }, + golden: goldenDotNet, + }, + { + name: "packages-props", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/packagesprops", + }, + golden: goldenPackagesProps, + }, + { + name: "swift", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/swift", + }, + golden: goldenSwift, + }, + { + name: "cocoapods", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/cocoapods", + }, + golden: goldenCocoaPods, + }, + { + name: "pubspec.lock", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/pubspec", + }, + golden: goldenPubspecLock, + }, + { + name: "mix.lock", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/mixlock", + }, + golden: goldenMixLock, + }, + { + name: "composer.lock", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/composer", + }, + golden: goldenComposerLock, + }, + { + name: "cargo.lock", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/cargo", + }, + golden: goldenCargoLock, + }, + { + name: "installed.json", + args: repoTestArgs{ + command: "rootfs", + scanner: types.VulnerabilityScanner, + listAllPkgs: true, + input: "testdata/fixtures/repo/composer-vendor", + }, + golden: goldenComposerVendor, + }, + { + name: "dockerfile", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/dockerfile", + namespaces: []string{"testing"}, + }, + golden: goldenDockerfile, + }, + { + name: "dockerfile with custom file pattern", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/dockerfile_file_pattern", + namespaces: []string{"testing"}, + filePatterns: []string{"dockerfile:Customfile"}, + }, + golden: goldenDockerfileFilePattern, + }, + { + name: "dockerfile with custom policies", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + policyPaths: []string{"testdata/fixtures/repo/custom-policy/policy"}, + namespaces: []string{"user"}, + input: "testdata/fixtures/repo/custom-policy", + }, + golden: goldenDockerfileCustomPolicies, + }, + { + name: "tarball helm chart scanning with builtin policies", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/helm", + }, + golden: goldenHelm, + }, + { + name: "helm chart directory scanning with builtin policies", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/helm_testchart", + }, + golden: goldenHelmTestChart, + }, + { + name: "helm chart directory scanning with value overrides using set", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/helm_testchart", + helmSet: []string{"securityContext.runAsUser=0"}, + }, + golden: goldenHelmTestChartOverridden, + }, + { + name: "helm chart directory scanning with value overrides using value file", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/helm_testchart", + helmValuesFile: []string{"testdata/fixtures/repo/helm_values/values.yaml"}, + }, + golden: goldenHelmTestChartOverridden, + }, + { + name: "helm chart directory scanning with builtin policies and non string Chart name", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/helm_badname", + }, + golden: goldenHelmBadName, + }, + { + name: "terraform config with remote module", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/terraform/remote-module", + }, + golden: goldenTerraformRemoteModule, + }, + { + name: "terraform config with remote submodule", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/terraform/remote-submodule", + }, + golden: goldenTerraformRemoteSubmodule, + }, + { + name: "terraform config with remote module in child local module", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/terraform/remote-module-in-child", + }, + golden: goldenTerraformRemoteModuleInChild, + }, + { + name: "exclude misconfigurations for remote module", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/terraform/remote-module", + tfExcludeDownloadedModules: true, + }, + golden: goldenTerraformExcludeMisconfigsRemoteModule, + }, + { + name: "module from Terraform registry", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/terraform/opentofu-registry", + }, + golden: goldenTerraformTerraformRegistry, + }, + { + name: "module from OpenTofu registry", + args: repoTestArgs{ + scanner: types.MisconfigScanner, + input: "testdata/fixtures/repo/terraform/opentofu-registry", + }, + golden: goldenTerraformOpenTofuRegistry, + }, + { + name: "secrets", + args: repoTestArgs{ + scanner: "vuln,secret", + input: "testdata/fixtures/repo/secrets", + secretConfig: "testdata/fixtures/repo/secrets/trivy-secret.yaml", + }, + golden: goldenSecrets, + }, + { + name: "conda generating CycloneDX SBOM", + args: repoTestArgs{ + command: "rootfs", + format: "cyclonedx", + input: "testdata/fixtures/repo/conda", + }, + golden: goldenCondaCycloneDX, + }, + { + name: "conda environment.yaml generating CycloneDX SBOM", + args: repoTestArgs{ + command: "fs", + format: "cyclonedx", + input: "testdata/fixtures/repo/conda-environment", + }, + golden: goldenCondaEnvironmentCycloneDX, + }, + { + name: "pom.xml generating CycloneDX SBOM (with vulnerabilities)", + args: repoTestArgs{ + command: "fs", + scanner: types.VulnerabilityScanner, + format: "cyclonedx", + input: "testdata/fixtures/repo/pom", + }, + golden: goldenPomCycloneDX, + }, + { + name: "conda generating SPDX SBOM", + args: repoTestArgs{ + command: "rootfs", + format: "spdx-json", + input: "testdata/fixtures/repo/conda", + }, + golden: goldenCondaSPDX, + }, + { + name: "julia generating SPDX SBOM", + args: repoTestArgs{ + command: "rootfs", + format: "spdx-json", + input: "testdata/fixtures/repo/julia", + }, + golden: goldenJuliaSPDX, + }, + { + name: "multiple lockfiles", + args: repoTestArgs{ + scanner: types.VulnerabilityScanner, + input: "https://github.com/knqyf263/trivy-ci-test", + }, + golden: goldenTestRepo, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + // Set up VEX + initVEXRepository(t, cacheDir, cacheDir) + + // Set a temp dir so that the VEX config will be loaded and modules will not be loaded + t.Setenv("XDG_DATA_HOME", cacheDir) + + // Disable Go license detection + t.Setenv("GOPATH", cacheDir) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + command := cmp.Or(tt.args.command, "repo") + format := cmp.Or(tt.args.format, types.FormatJSON) + + osArgs := buildArgs(t, cacheDir, command, format, tt.args) + + runTest(t, osArgs, tt.golden, format, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: nil, // Do not use overrides - golden files are generated from this test as the canonical source + }) + }) + } +} + +// TestRepositoryWithOverride tests `trivy repo` with override functions for specific edge cases. +// +// IMPORTANT: Golden files used in this test cannot be updated with the -update flag +// because the golden files are shared with TestRepository. +// If golden files need to be updated, they should be generated from TestRepository. +// +// All golden files used in TestRepositoryWithOverride MUST also be used in TestRepository +// to ensure they can be properly updated when needed. +func TestRepositoryWithOverride(t *testing.T) { + if *update { + t.Skipf("Skipping TestRepositoryWithOverride when -update flag is set. Golden files should be updated via TestRepository.") + } + + t.Setenv("NUGET_PACKAGES", t.TempDir()) + tests := []struct { + name string + args repoTestArgs + golden string + override func(t *testing.T, want, got *types.Report) + }{ + { + name: "gomod with fs subcommand", + args: repoTestArgs{ + command: "fs", + scanner: types.VulnerabilityScanner, + input: "testdata/fixtures/repo/gomod", + skipFiles: []string{"testdata/fixtures/repo/gomod/submod2/go.mod"}, + }, + golden: goldenGoModSkip, + override: func(_ *testing.T, want, _ *types.Report) { + want.ArtifactType = ftypes.TypeFilesystem + }, + }, + { + name: "dockerfile with fs subcommand and an alias scanner", + args: repoTestArgs{ + command: "fs", + scanner: "config", // for backward compatibility + policyPaths: []string{"testdata/fixtures/repo/custom-policy/policy"}, + namespaces: []string{"user"}, + input: "testdata/fixtures/repo/custom-policy", + }, + golden: goldenDockerfileCustomPolicies, + override: func(_ *testing.T, want, _ *types.Report) { + want.ArtifactType = ftypes.TypeFilesystem + }, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + command := "repo" + if tt.args.command != "" { + command = tt.args.command + } + + format := types.FormatJSON + if tt.args.format != "" { + format = tt.args.format + } + + osArgs := buildArgs(t, cacheDir, command, format, tt.args) + + runTest(t, osArgs, tt.golden, format, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: overrideFuncs(overrideUID, tt.override), + }) + }) + } +} + +func buildArgs(t *testing.T, cacheDir, command string, format types.Format, testArgs repoTestArgs) []string { + // Build base arguments + osArgs := []string{ + "-q", + "--cache-dir", + cacheDir, + command, + "--skip-db-update", + "--skip-policy-update", + "--format", + string(format), + "--parallel", + strconv.Itoa(testArgs.parallel), + "--offline-scan", + testArgs.input, + } + + if testArgs.scanner != "" { + osArgs = append(osArgs, "--scanners", string(testArgs.scanner)) + } + + for _, policyPath := range testArgs.policyPaths { + osArgs = append(osArgs, "--config-policy", policyPath) + } + for _, namespace := range testArgs.namespaces { + osArgs = append(osArgs, "--policy-namespaces", namespace) + } + + // Handle ignore file using temporary directory + if len(testArgs.ignoreIDs) != 0 { + trivyIgnore := filepath.Join(t.TempDir(), ".trivyignore") + err := os.WriteFile(trivyIgnore, []byte(strings.Join(testArgs.ignoreIDs, "\n")), 0o444) + require.NoError(t, err, "failed to write .trivyignore") + osArgs = append(osArgs, "--ignorefile", trivyIgnore) + } + + for _, filePattern := range testArgs.filePatterns { + osArgs = append(osArgs, "--file-patterns", filePattern) + } + + for _, hs := range testArgs.helmSet { + osArgs = append(osArgs, "--helm-set", hs) + } + for _, hvf := range testArgs.helmValuesFile { + osArgs = append(osArgs, "--helm-values", hvf) + } + + for _, skipFile := range testArgs.skipFiles { + osArgs = append(osArgs, "--skip-files", skipFile) + } + for _, skipDir := range testArgs.skipDirs { + osArgs = append(osArgs, "--skip-dirs", skipDir) + } + + if len(testArgs.vulnSeveritySources) != 0 { + osArgs = append(osArgs, + "--vuln-severity-source", strings.Join(testArgs.vulnSeveritySources, ","), + ) + } + if !testArgs.listAllPkgs { + osArgs = append(osArgs, "--list-all-pkgs=false") + } + if testArgs.includeDevDeps { + osArgs = append(osArgs, "--include-dev-deps") + } + if testArgs.secretConfig != "" { + osArgs = append(osArgs, "--secret-config", testArgs.secretConfig) + } + if testArgs.vex != "" { + osArgs = append(osArgs, "--vex", testArgs.vex) + } + if testArgs.tfExcludeDownloadedModules { + osArgs = append(osArgs, "--tf-exclude-downloaded-modules") + } + + return osArgs +} diff --git a/integration/sbom_test.go b/integration/sbom_test.go new file mode 100644 index 000000000000..af738ec5e138 --- /dev/null +++ b/integration/sbom_test.go @@ -0,0 +1,266 @@ +//go:build integration + +package integration + +import ( + "path/filepath" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +// TestSBOM tests scanning SBOM files (CycloneDX, SPDX). +// +// NOTE: This test CAN update golden files with the -update flag because the golden files +// used here are not shared with other tests. These SBOM-specific golden files are unique +// to this test and should be updated here. +func TestSBOM(t *testing.T) { + type args struct { + input string + format string + artifactType string + scanners string + } + tests := []struct { + name string + args args + golden string + }{ + { + name: "fluentd-multiple-lockfiles cyclonedx", + args: args{ + input: "testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json", + format: "json", + artifactType: "cyclonedx", + }, + golden: goldenFluentdMultipleLockfiles, + }, + { + name: "scan SBOM into SBOM", + args: args{ + input: "testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json", + format: "cyclonedx", + artifactType: "cyclonedx", + }, + golden: goldenFluentdMultipleLockfilesShortCDX, + }, + { + name: "minikube KBOM", + args: args{ + input: "testdata/fixtures/sbom/minikube-kbom.json", + format: "json", + artifactType: "cyclonedx", + }, + golden: goldenMinikubeKBOM, + }, + { + name: "license check cyclonedx json", + args: args{ + input: "testdata/fixtures/sbom/license-cyclonedx.json", + format: "json", + artifactType: "cyclonedx", + scanners: "license", + }, + golden: goldenLicenseCycloneDX, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scanners := "vuln" + if tt.args.scanners != "" { + scanners = tt.args.scanners + } + + osArgs := []string{ + "sbom", + "--cache-dir", + cacheDir, + "-q", + "--skip-db-update", + "--format", + tt.args.format, + "--scanners", + scanners, + "--list-all-pkgs=false", + tt.args.input, + } + + // Run "trivy sbom" + runTest(t, osArgs, tt.golden, types.Format(tt.args.format), runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: nil, // Do not use overrides - golden files are generated from this test as the canonical source + }) + }) + } +} + +// TestSBOMEquivalence tests that scanning an SBOM produces equivalent results to scanning the original artifact. +// +// This test verifies that scanning an image through SBOM: +// +// trivy image centos:7 -f cyclonedx -o centos7.cdx.json && trivy sbom centos7.cdx.json +// +// produces the same vulnerability results as direct image scanning: +// +// trivy image centos:7 +// +// IMPORTANT: Golden files used in this test cannot be updated with the -update flag +// because the golden files are shared with TestTar. +// If golden files need to be updated, they should be generated from TestTar. +// +// All golden files used in TestSBOMEquivalence MUST also be used in TestTar +// to ensure they can be properly updated when needed. +func TestSBOMEquivalence(t *testing.T) { + if *update { + t.Skipf("Skipping TestSBOMEquivalence when -update flag is set. Golden files should be updated via TestTar.") + } + + type args struct { + input string + format string + artifactType string + } + tests := []struct { + name string + args args + golden string + override OverrideFunc + }{ + { + name: "centos7 cyclonedx", + args: args{ + input: "testdata/fixtures/sbom/centos-7-cyclonedx.json", + format: "json", + artifactType: "cyclonedx", + }, + golden: goldenCentOS7, + override: func(t *testing.T, want, got *types.Report) { + want.ArtifactName = "testdata/fixtures/sbom/centos-7-cyclonedx.json" + want.ArtifactType = ftypes.TypeCycloneDX + + require.Len(t, got.Results, 1) + want.Results[0].Target = "testdata/fixtures/sbom/centos-7-cyclonedx.json (centos 7.6.1810)" + + require.Len(t, got.Results[0].Vulnerabilities, 3) + want.Results[0].Vulnerabilities[0].PkgIdentifier.BOMRef = "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810" + want.Results[0].Vulnerabilities[1].PkgIdentifier.BOMRef = "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810" + want.Results[0].Vulnerabilities[2].PkgIdentifier.BOMRef = "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810" + + // ReportID uses v7 UUID with independent counter from v4 UUIDs used for SBOM components + want.ReportID = "017b7d41-e09f-7000-80ea-000000000001" + }, + }, + { + name: "centos7 spdx tag-value", + args: args{ + input: "testdata/fixtures/sbom/centos-7-spdx.txt", + format: "json", + artifactType: "spdx", + }, + golden: goldenCentOS7, + override: func(t *testing.T, want, got *types.Report) { + want.ArtifactName = "testdata/fixtures/sbom/centos-7-spdx.txt" + want.ArtifactType = ftypes.TypeSPDX + + require.Len(t, got.Results, 1) + want.Results[0].Target = "testdata/fixtures/sbom/centos-7-spdx.txt (centos 7.6.1810)" + + // ReportID uses v7 UUID with independent counter from v4 UUIDs used for SBOM components + want.ReportID = "017b7d41-e09f-7000-80ea-000000000001" + }, + }, + { + name: "centos7 spdx json", + args: args{ + input: "testdata/fixtures/sbom/centos-7-spdx.json", + format: "json", + artifactType: "spdx", + }, + golden: goldenCentOS7, + override: func(t *testing.T, want, got *types.Report) { + want.ArtifactName = "testdata/fixtures/sbom/centos-7-spdx.json" + want.ArtifactType = ftypes.TypeSPDX + + require.Len(t, got.Results, 1) + want.Results[0].Target = "testdata/fixtures/sbom/centos-7-spdx.json (centos 7.6.1810)" + + // ReportID uses v7 UUID with independent counter from v4 UUIDs used for SBOM components + want.ReportID = "017b7d41-e09f-7000-80ea-000000000001" + }, + }, + { + name: "centos7 in in-toto attestation", + args: args{ + input: "testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl", + format: "json", + artifactType: "cyclonedx", + }, + golden: goldenCentOS7, + override: func(t *testing.T, want, got *types.Report) { + want.ArtifactName = "testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl" + want.ArtifactType = ftypes.TypeCycloneDX + + require.Len(t, got.Results, 1) + want.Results[0].Target = "testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl (centos 7.6.1810)" + + require.Len(t, got.Results[0].Vulnerabilities, 3) + want.Results[0].Vulnerabilities[0].PkgIdentifier.BOMRef = "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810" + want.Results[0].Vulnerabilities[1].PkgIdentifier.BOMRef = "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810" + want.Results[0].Vulnerabilities[2].PkgIdentifier.BOMRef = "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810" + + // ReportID uses v7 UUID with independent counter from v4 UUIDs used for SBOM components + want.ReportID = "017b7d41-e09f-7000-80ea-000000000001" + }, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := []string{ + "sbom", + "--cache-dir", + cacheDir, + "-q", + "--skip-db-update", + "--format", + tt.args.format, + "--scanners", + "vuln", + "--list-all-pkgs=false", + tt.args.input, + } + + // Run "trivy sbom" + runTest(t, osArgs, tt.golden, types.Format(tt.args.format), runOptions{ + override: overrideFuncs(overrideSBOMReport, overrideUID, overrideFingerprint, tt.override), + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + } +} + +func overrideSBOMReport(_ *testing.T, want, got *types.Report) { + want.ArtifactID = "" + want.Metadata.ImageConfig = v1.ConfigFile{} + + // SBOM file doesn't contain info about layers + want.Metadata.Size = 0 + want.Metadata.Layers = nil + + // when running on Windows FS + got.ArtifactName = filepath.ToSlash(filepath.Clean(got.ArtifactName)) + for i, result := range got.Results { + got.Results[i].Target = filepath.ToSlash(filepath.Clean(result.Target)) + } +} diff --git a/integration/standalone_tar_test.go b/integration/standalone_tar_test.go new file mode 100644 index 000000000000..5fcbba960a1c --- /dev/null +++ b/integration/standalone_tar_test.go @@ -0,0 +1,681 @@ +//go:build integration + +package integration + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +// TestTar tests `trivy image --input` with tar archives of container images. +// +// NOTE: This test CAN update golden files with the -update flag. +// This is the canonical source for container image scanning golden files. +// Golden files generated here may be shared with other tests like TestClientServer, +// TestDockerEngine, TestRegistry, and TestClientServerWithRedis (when scanning images). +func TestTar(t *testing.T) { + type args struct { + IgnoreUnfixed bool + Severity []string + IgnoreIDs []string + Format types.Format + Input string + SkipDirs []string + SkipFiles []string + DetectionPriority ftypes.DetectionPriority + } + tests := []struct { + name string + args args + golden string + }{ + { + name: "alpine 3.9", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/alpine-39.tar.gz", + }, + golden: goldenAlpine39, + }, + { + name: "alpine 3.9 with skip dirs", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/alpine-39.tar.gz", + SkipDirs: []string{ + "/etc", + }, + }, + golden: goldenAlpine39Skip, + }, + { + name: "alpine 3.9 with skip files", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/alpine-39.tar.gz", + SkipFiles: []string{ + "/etc", + "/etc/TZ", + "/etc/alpine-release", + "/etc/apk", + "/etc/apk/arch", + "/etc/apk/keys", + "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", + "/etc/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", + "/etc/apk/protected_paths.d", + "/etc/apk/repositories", + "/etc/apk/world", + "/etc/conf.d", + "/etc/crontabs", + "/etc/crontabs/root", + "/etc/fstab", + "/etc/group", + "/etc/hostname", + "/etc/hosts", + "/etc/init.d", + "/etc/inittab", + "/etc/issue", + "/etc/logrotate.d", + "/etc/logrotate.d/acpid", + "/etc/modprobe.d", + "/etc/modprobe.d/aliases.conf", + "/etc/modprobe.d/blacklist.conf", + "/etc/modprobe.d/i386.conf", + "/etc/modprobe.d/kms.conf", + "/etc/modules", + "/etc/modules-load.d", + "/etc/motd", + "/etc/mtab", + "/etc/network", + "/etc/network/if-down.d", + "/etc/network/if-post-down.d", + "/etc/network/if-post-up.d", + "/etc/network/if-pre-down.d", + "/etc/network/if-pre-up.d", + "/etc/network/if-up.d", + "/etc/network/if-up.d/dad", + "/etc/opt", + "/etc/os-release", + "/etc/passwd", + "/etc/periodic", + "/etc/periodic/15min", + "/etc/periodic/daily", + "/etc/periodic/hourly", + "/etc/periodic/monthly", + "/etc/periodic/weekly", + "/etc/profile", + "/etc/profile.d", + "/etc/profile.d/color_prompt", + "/etc/protocols", + "/etc/securetty", + "/etc/services", + "/etc/shadow", + "/etc/shells", + "/etc/ssl", + "/etc/ssl/cert.pem", + "/etc/ssl/certs", + "/etc/ssl/ct_log_list.cnf", + "/etc/ssl/ct_log_list.cnf.dist", + "/etc/ssl/misc", + "/etc/ssl/misc/CA.pl", + "/etc/ssl/misc/tsget", + "/etc/ssl/misc/tsget.pl", + "/etc/ssl/openssl.cnf", + "/etc/ssl/openssl.cnf.dist", + "/etc/ssl/private", + "/etc/sysctl.conf", + "/etc/sysctl.d", + "/etc/sysctl.d/00-alpine.conf", + "/etc/udhcpd.conf", + }, + }, + golden: goldenAlpine39Skip, + }, + { + name: "alpine 3.9 with high and critical severity", + args: args{ + IgnoreUnfixed: true, + Severity: []string{ + "HIGH", + "CRITICAL", + }, + Format: types.FormatJSON, + Input: "testdata/fixtures/images/alpine-39.tar.gz", + }, + golden: goldenAlpine39HighCritical, + }, + { + name: "alpine 3.9 with .trivyignore", + args: args{ + IgnoreUnfixed: false, + IgnoreIDs: []string{ + "CVE-2019-1549", + "CVE-2019-14697", + }, + Format: types.FormatJSON, + Input: "testdata/fixtures/images/alpine-39.tar.gz", + }, + golden: goldenAlpine39IgnoreCVEIDs, + }, + { + name: "alpine 3.10", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/alpine-310.tar.gz", + }, + golden: goldenAlpine310JSON, + }, + { + name: "alpine distroless", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/alpine-distroless.tar.gz", + }, + golden: goldenAlpineDistroless, + }, + { + name: "amazon linux 1", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/amazon-1.tar.gz", + }, + golden: goldenAmazon1, + }, + { + name: "amazon linux 2", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/amazon-2.tar.gz", + }, + golden: goldenAmazon2, + }, + { + name: "debian buster/10", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/debian-buster.tar.gz", + }, + golden: goldenDebianBuster, + }, + { + name: "debian buster/10 with --ignore-unfixed option", + args: args{ + IgnoreUnfixed: true, + Format: types.FormatJSON, + Input: "testdata/fixtures/images/debian-buster.tar.gz", + }, + golden: goldenDebianBusterIgnoreUnfixed, + }, + { + name: "debian stretch/9", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/debian-stretch.tar.gz", + }, + golden: goldenDebianStretch, + }, + { + name: "ubuntu 18.04", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/ubuntu-1804.tar.gz", + }, + golden: goldenUbuntu1804, + }, + { + name: "ubuntu 18.04 with --ignore-unfixed option", + args: args{ + IgnoreUnfixed: true, + Format: types.FormatJSON, + Input: "testdata/fixtures/images/ubuntu-1804.tar.gz", + }, + golden: goldenUbuntu1804IgnoreUnfixed, + }, + { + name: "centos 7", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/centos-7.tar.gz", + }, + golden: goldenCentOS7, + }, + { + name: "centos 7 with --ignore-unfixed option", + args: args{ + IgnoreUnfixed: true, + Format: types.FormatJSON, + Input: "testdata/fixtures/images/centos-7.tar.gz", + }, + golden: goldenCentOS7IgnoreUnfixed, + }, + { + name: "centos 7 with medium severity", + args: args{ + IgnoreUnfixed: true, + Severity: []string{"MEDIUM"}, + Format: types.FormatJSON, + Input: "testdata/fixtures/images/centos-7.tar.gz", + }, + golden: goldenCentOS7Medium, + }, + { + name: "centos 6", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/centos-6.tar.gz", + }, + golden: goldenCentOS6, + }, + { + name: "ubi 7", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/ubi-7.tar.gz", + }, + golden: goldenUBI7, + }, + { + name: "ubi 7 with comprehensive priority", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/ubi-7.tar.gz", + DetectionPriority: ftypes.PriorityComprehensive, + }, + golden: goldenUBI7Comprehensive, + }, + { + name: "almalinux 8", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/almalinux-8.tar.gz", + }, + golden: goldenAlmaLinux8, + }, + { + name: "rocky linux 8", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/rockylinux-8.tar.gz", + }, + golden: goldenRockyLinux8, + }, + { + name: "distroless base", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/distroless-base.tar.gz", + }, + golden: goldenDistrolessBase, + }, + { + name: "distroless python27", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/distroless-python27.tar.gz", + }, + golden: goldenDistrolessPython27, + }, + { + name: "oracle linux 8", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/oraclelinux-8.tar.gz", + }, + golden: goldenOracleLinux8, + }, + { + name: "opensuse leap 15.1", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/opensuse-leap-151.tar.gz", + }, + golden: goldenOpenSUSELeap151, + }, + { + name: "opensuse tumbleweed", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/opensuse-tumbleweed.tar.gz", + }, + golden: goldenOpenSUSETumbleweed, + }, + { + name: "sle micro rancher 5.4", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/sle-micro-rancher-5.4_ndb.tar.gz", + }, + golden: goldenSLMicroRancher54, + }, + { + name: "photon 3.0", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/photon-30.tar.gz", + }, + golden: goldenPhoton30, + }, + { + name: "CBL-Mariner 1.0", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/mariner-1.0.tar.gz", + }, + golden: goldenMariner10, + }, + { + name: "busybox with Cargo.lock integration", + args: args{ + Format: types.FormatJSON, + Input: "testdata/fixtures/images/busybox-with-lockfile.tar.gz", + }, + golden: goldenBusyboxWithLockfile, + }, + { + name: "fluentd with RubyGems", + args: args{ + IgnoreUnfixed: true, + Format: types.FormatJSON, + Input: "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz", + }, + golden: goldenFluentdGems, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + // Set a temp dir so that modules will not be loaded + t.Setenv("XDG_DATA_HOME", cacheDir) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := []string{ + "--cache-dir", + cacheDir, + "image", + "-q", + "--format", + string(tt.args.Format), + "--skip-db-update", + "--list-all-pkgs=false", + } + + if tt.args.IgnoreUnfixed { + osArgs = append(osArgs, "--ignore-unfixed") + } + if len(tt.args.Severity) != 0 { + osArgs = append(osArgs, "--severity", strings.Join(tt.args.Severity, ",")) + } + if len(tt.args.IgnoreIDs) != 0 { + trivyIgnore := ".trivyignore" + err := os.WriteFile(trivyIgnore, []byte(strings.Join(tt.args.IgnoreIDs, "\n")), 0o444) + require.NoError(t, err, "failed to write .trivyignore") + defer os.Remove(trivyIgnore) + } + if tt.args.Input != "" { + osArgs = append(osArgs, "--input", tt.args.Input) + } + + if len(tt.args.SkipFiles) != 0 { + for _, skipFile := range tt.args.SkipFiles { + osArgs = append(osArgs, "--skip-files", skipFile) + } + } + + if len(tt.args.SkipDirs) != 0 { + for _, skipDir := range tt.args.SkipDirs { + osArgs = append(osArgs, "--skip-dirs", skipDir) + } + } + + if tt.args.DetectionPriority != "" { + osArgs = append(osArgs, "--detection-priority", string(tt.args.DetectionPriority)) + } + + // Run Trivy + runTest(t, osArgs, tt.golden, tt.args.Format, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: nil, // Do not use overrides - golden files are generated from this test as the canonical source + }) + }) + } +} + +// TestTarWithOverride tests container image scanning with overrides applied. +// +// Golden files are shared with TestTar. +func TestTarWithOverride(t *testing.T) { + if *update { + t.Skipf("Skipping TestTarWithOverride when -update flag is set. Golden files should be updated via TestTar.") + } + + type args struct { + input string + distro string + } + tests := []struct { + name string + args args + golden string + override OverrideFunc + }{ + { + name: "alpine 3.9 as alpine 3.10", + args: args{ + input: "testdata/fixtures/images/alpine-39.tar.gz", + distro: "alpine/3.10", + }, + override: func(_ *testing.T, want, _ *types.Report) { + want.Metadata.OS.Name = "3.10" + want.Results[0].Target = "testdata/fixtures/images/alpine-39.tar.gz (alpine 3.10)" + }, + golden: goldenAlpine39, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := []string{ + "--cache-dir", + cacheDir, + "image", + "--format", + "json", + "--skip-db-update", + "--list-all-pkgs=false", + "--input", + tt.args.input, + } + + if tt.args.distro != "" { + osArgs = append(osArgs, "--distro", tt.args.distro) + } + + // Run Trivy + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + override: overrideFuncs(overrideUID, overrideFingerprint, tt.override), + }) + }) + } +} + +// TestTarWithEnv tests container image scanning with environment variables. +// +// Golden files are shared with TestTar. +func TestTarWithEnv(t *testing.T) { + if *update { + t.Skipf("Skipping TestTarWithEnv when -update flag is set. Golden files should be updated via TestTar.") + } + + type args struct { + IgnoreUnfixed bool + Severity []string + Format string + Input string + SkipDirs []string + } + tests := []struct { + name string + testArgs args + golden string + }{ + { + name: "alpine 3.9 with skip dirs", + testArgs: args{ + Format: "json", + Input: "testdata/fixtures/images/alpine-39.tar.gz", + SkipDirs: []string{ + "/etc", + }, + }, + golden: goldenAlpine39Skip, + }, + { + name: "alpine 3.9 with high and critical severity", + testArgs: args{ + IgnoreUnfixed: true, + Severity: []string{ + "HIGH", + "CRITICAL", + }, + Format: "json", + Input: "testdata/fixtures/images/alpine-39.tar.gz", + }, + golden: goldenAlpine39HighCritical, + }, + { + name: "debian buster/10 with --ignore-unfixed option", + testArgs: args{ + IgnoreUnfixed: true, + Format: "json", + Input: "testdata/fixtures/images/debian-buster.tar.gz", + }, + golden: goldenDebianBusterIgnoreUnfixed, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + // Set a temp dir so that modules will not be loaded + t.Setenv("XDG_DATA_HOME", cacheDir) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("TRIVY_FORMAT", tt.testArgs.Format) + t.Setenv("TRIVY_LIST_ALL_PKGS", "false") + t.Setenv("TRIVY_CACHE_DIR", cacheDir) + t.Setenv("TRIVY_QUIET", "true") + t.Setenv("TRIVY_SKIP_UPDATE", "true") + + if tt.testArgs.IgnoreUnfixed { + t.Setenv("TRIVY_IGNORE_UNFIXED", "true") + } + if len(tt.testArgs.Severity) != 0 { + t.Setenv("TRIVY_SEVERITY", strings.Join(tt.testArgs.Severity, ",")) + } + if tt.testArgs.Input != "" { + t.Setenv("TRIVY_INPUT", tt.testArgs.Input) + } + + if len(tt.testArgs.SkipDirs) != 0 { + t.Setenv("TRIVY_SKIP_DIRS", strings.Join(tt.testArgs.SkipDirs, ",")) + } + + // Run Trivy + runTest(t, []string{"image"}, tt.golden, types.FormatJSON, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + } +} + +// TestTarWithConfigFile tests container image scanning with config files. +// +// Golden files are shared with TestTar. +func TestTarWithConfigFile(t *testing.T) { + if *update { + t.Skipf("Skipping TestTarWithConfigFile when -update flag is set. Golden files should be updated via TestTar.") + } + + tests := []struct { + name string + input string + configFile string + golden string + }{ + { + name: "alpine 3.9 with high and critical severity", + input: "testdata/fixtures/images/alpine-39.tar.gz", + configFile: `quiet: true +format: json +list-all-pkgs: false +severity: + - HIGH + - CRITICAL +vulnerability: + type: + - os +cache: + dir: /should/be/overwritten +`, + golden: goldenAlpine39HighCritical, + }, + { + name: "debian buster/10 with --ignore-unfixed option", + input: "testdata/fixtures/images/debian-buster.tar.gz", + configFile: `quiet: true +format: json +list-all-pkgs: false +vulnerability: + ignore-unfixed: true +cache: + dir: /should/be/overwritten +`, + golden: goldenDebianBusterIgnoreUnfixed, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + // Set a temp dir so that modules will not be loaded + t.Setenv("XDG_DATA_HOME", cacheDir) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + configPath := filepath.Join(t.TempDir(), "trivy.yaml") + err := os.WriteFile(configPath, []byte(tt.configFile), 0o600) + require.NoError(t, err) + + osArgs := []string{ + "--cache-dir", + cacheDir, + "image", + "--skip-db-update", + "--config", + configPath, + "--input", + tt.input, + } + + // Run Trivy + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + } +} diff --git a/integration/testdata/almalinux-8.json.golden b/integration/testdata/almalinux-8.json.golden new file mode 100644 index 000000000000..f3057d270c07 --- /dev/null +++ b/integration/testdata/almalinux-8.json.golden @@ -0,0 +1,160 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:fb75459277a4cbcf98182b48c789cfbd4b34414e05898e1231ae8b2ca099f4e7", + "ArtifactName": "testdata/fixtures/images/almalinux-8.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 204637184, + "OS": { + "Family": "alma", + "Name": "8.5" + }, + "ImageID": "sha256:4ca63ce1d8a90da2ed4f2d5e93e8e9db2f32d0fabf0718a2edebbe0e70826622", + "DiffIDs": [ + "sha256:124d41c237c5e823577dda97e87cebaecce62d585c725d07e709ce410681de4d" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:almalinux-8" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:almalinux-8", + "ImageConfig": { + "architecture": "amd64", + "container": "a467f67a48d469e1975b7414f33f2cf87121d4cc59d2ee029ea58e6b81774769", + "created": "2021-11-13T12:10:27.09871973Z", + "docker_version": "20.10.7", + "history": [ + { + "created": "2021-11-13T12:10:26.29818864Z", + "created_by": "/bin/sh -c #(nop) ADD file:2e002305ccb9d8a4dcef52509c4c50b9a15e76c9c49ca6abda3e0d7091c63fa7 in / " + }, + { + "created": "2021-11-13T12:10:27.09871973Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:124d41c237c5e823577dda97e87cebaecce62d585c725d07e709ce410681de4d" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:d38d2eac03bc19e080df596d6148863a0f8293f3a277a7524f378da79a1feb0f" + } + }, + "Layers": [ + { + "Size": 204637184, + "Digest": "sha256:a1f18d9dc5496c63197eb9a4f1d4bf5cc88c6a34f64f0fe11ea233070392ce48", + "DiffID": "sha256:124d41c237c5e823577dda97e87cebaecce62d585c725d07e709ce410681de4d" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/almalinux-8.tar.gz (alma 8.5)", + "Class": "os-pkgs", + "Type": "alma", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2021-3712", + "PkgID": "openssl-libs@1.1.1k-4.el8.x86_64", + "PkgName": "openssl-libs", + "PkgIdentifier": { + "PURL": "pkg:rpm/alma/openssl-libs@1.1.1k-4.el8?arch=x86_64\u0026distro=alma-8.5\u0026epoch=1", + "UID": "d88cb05095ee62ab" + }, + "InstalledVersion": "1:1.1.1k-4.el8", + "FixedVersion": "1:1.1.1k-5.el8_5", + "Status": "fixed", + "Layer": { + "Digest": "sha256:a1f18d9dc5496c63197eb9a4f1d4bf5cc88c6a34f64f0fe11ea233070392ce48", + "DiffID": "sha256:124d41c237c5e823577dda97e87cebaecce62d585c725d07e709ce410681de4d" + }, + "SeveritySource": "alma", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-3712", + "DataSource": { + "ID": "alma", + "Name": "AlmaLinux Product Errata", + "URL": "https://errata.almalinux.org/" + }, + "Fingerprint": "sha256:423564f47df066642faca6be8d949f2e201e2578173845e6ee2673ecf9229861", + "Title": "openssl: Read buffer overruns processing ASN.1 strings", + "Description": "ASN.1 strings are represented internally within OpenSSL as an ASN1_STRING structure which contains a buffer holding the string data and a field holding the buffer length. This contrasts with normal C strings which are represented as a buffer for the string data which is terminated with a NUL (0) byte. Although not a strict requirement, ASN.1 strings that are parsed using OpenSSL's own \"d2i\" functions (and other similar parsing functions) as well as any string whose value has been set with the ASN1_STRING_set() function will additionally NUL terminate the byte array in the ASN1_STRING structure. However, it is possible for applications to directly construct valid ASN1_STRING structures which do not NUL terminate the byte array by directly setting the \"data\" and \"length\" fields in the ASN1_STRING array. This can also happen by using the ASN1_STRING_set0() function. Numerous OpenSSL functions that print ASN.1 data have been found to assume that the ASN1_STRING byte array will be NUL terminated, even though this is not guaranteed for strings that have been directly constructed. Where an application requests an ASN.1 structure to be printed, and where that ASN.1 structure contains ASN1_STRINGs that have been directly constructed by the application without NUL terminating the \"data\" field, then a read buffer overrun can occur. The same thing can also occur during name constraints processing of certificates (for example if a certificate has been directly constructed by the application instead of loading it via the OpenSSL parsing functions, and the certificate contains non NUL terminated ASN1_STRING structures). It can also occur in the X509_get1_email(), X509_REQ_get1_email() and X509_get1_ocsp() functions. If a malicious actor can cause an application to directly construct an ASN1_STRING and then process it through one of the affected OpenSSL functions then this issue could be hit. This might result in a crash (causing a Denial of Service attack). It could also result in the disclosure of private memory contents (such as private keys, or sensitive plaintext). Fixed in OpenSSL 1.1.1l (Affected 1.1.1-1.1.1k). Fixed in OpenSSL 1.0.2za (Affected 1.0.2-1.0.2y).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-125" + ], + "VendorSeverity": { + "alma": 2, + "amazon": 2, + "arch-linux": 3, + "cbl-mariner": 3, + "nvd": 3, + "oracle-oval": 2, + "photon": 3, + "redhat": 2, + "rocky": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V2Score": 5.8, + "V3Score": 7.4 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V3Score": 7.4 + } + }, + "References": [ + "http://www.openwall.com/lists/oss-security/2021/08/26/2", + "https://access.redhat.com/hydra/rest/securitydata/cve/CVE-2021-3712.json", + "https://access.redhat.com/security/cve/CVE-2021-3712", + "https://crates.io/crates/openssl-src", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3712", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=94d23fcff9b2a7a8368dfe52214d5c2569882c11", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=ccb0a11145ee72b042d10593a64eaf9e8a55ec12", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10366", + "https://linux.oracle.com/cve/CVE-2021-3712.html", + "https://linux.oracle.com/errata/ELSA-2022-9023.html", + "https://lists.apache.org/thread.html/r18995de860f0e63635f3008fd2a6aca82394249476d21691e7c59c9e@%3Cdev.tomcat.apache.org%3E", + "https://lists.apache.org/thread.html/rad5d9f83f0d11fb3f8bb148d179b8a9ad7c6a17f18d70e5805a713d1@%3Cdev.tomcat.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2021/09/msg00014.html", + "https://lists.debian.org/debian-lts-announce/2021/09/msg00021.html", + "https://nvd.nist.gov/vuln/detail/CVE-2021-3712", + "https://rustsec.org/advisories/RUSTSEC-2021-0098.html", + "https://security.netapp.com/advisory/ntap-20210827-0010/", + "https://ubuntu.com/security/notices/USN-5051-1", + "https://ubuntu.com/security/notices/USN-5051-2", + "https://ubuntu.com/security/notices/USN-5051-3", + "https://ubuntu.com/security/notices/USN-5051-4 (regression only in trusty/esm)", + "https://ubuntu.com/security/notices/USN-5088-1", + "https://www.debian.org/security/2021/dsa-4963", + "https://www.openssl.org/news/secadv/20210824.txt", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://www.tenable.com/security/tns-2021-16", + "https://www.tenable.com/security/tns-2022-02" + ], + "PublishedDate": "2021-08-24T15:15:00Z", + "LastModifiedDate": "2022-01-06T09:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/alpine-310.asff.golden b/integration/testdata/alpine-310.asff.golden new file mode 100644 index 000000000000..6164ba5f1140 --- /dev/null +++ b/integration/testdata/alpine-310.asff.golden @@ -0,0 +1,184 @@ +{ + "Findings": [ + { + "SchemaVersion": "2018-10-08", + "Id": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)/CVE-2019-1549", + "ProductArn": "arn:aws:securityhub:test-region::product/aquasecurity/aquasecurity", + "GeneratorId": "Trivy/CVE-2019-1549", + "AwsAccountId": "123456789012", + "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "UpdatedAt": "2021-08-25T12:20:30.000000005Z", + "Severity": { + "Label": "MEDIUM" + }, + "Title": "Trivy found a vulnerability to CVE-2019-1549 in container testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2), related to libcrypto1.1", + "Description": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an applicati ..", + "Remediation": { + "Recommendation": { + "Text": "More information on this vulnerability is provided in the hyperlink", + "Url": "https://avd.aquasec.com/nvd/cve-2019-1549" + } + }, + "ProductFields": { "Product Name": "Trivy" }, + "Resources": [ + { + "Type": "Container", + "Id": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)", + "Partition": "aws", + "Region": "test-region", + "Details": { + "Container": { "ImageName": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)" }, + "Other": { + "CVE ID": "CVE-2019-1549", + "CVE Title": "openssl: information disclosure in fork()", + "PkgName": "libcrypto1.1", + "Installed Package": "1.1.1c-r0", + "Patched Package": "1.1.1d-r0", + "NvdCvssScoreV3": "5.3", + "NvdCvssVectorV3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "NvdCvssScoreV2": "5", + "NvdCvssVectorV2": "AV:N/AC:L/Au:N/C:P/I:N/A:N" + } + } + } + ], + "RecordState": "ACTIVE" + }, + { + "SchemaVersion": "2018-10-08", + "Id": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)/CVE-2019-1551", + "ProductArn": "arn:aws:securityhub:test-region::product/aquasecurity/aquasecurity", + "GeneratorId": "Trivy/CVE-2019-1551", + "AwsAccountId": "123456789012", + "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "UpdatedAt": "2021-08-25T12:20:30.000000005Z", + "Severity": { + "Label": "MEDIUM" + }, + "Title": "Trivy found a vulnerability to CVE-2019-1551 in container testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2), related to libcrypto1.1", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly us ..", + "Remediation": { + "Recommendation": { + "Text": "More information on this vulnerability is provided in the hyperlink", + "Url": "https://avd.aquasec.com/nvd/cve-2019-1551" + } + }, + "ProductFields": { "Product Name": "Trivy" }, + "Resources": [ + { + "Type": "Container", + "Id": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)", + "Partition": "aws", + "Region": "test-region", + "Details": { + "Container": { "ImageName": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)" }, + "Other": { + "CVE ID": "CVE-2019-1551", + "CVE Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "PkgName": "libcrypto1.1", + "Installed Package": "1.1.1c-r0", + "Patched Package": "1.1.1d-r2", + "NvdCvssScoreV3": "5.3", + "NvdCvssVectorV3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "NvdCvssScoreV2": "5", + "NvdCvssVectorV2": "AV:N/AC:L/Au:N/C:P/I:N/A:N" + } + } + } + ], + "RecordState": "ACTIVE" + }, + { + "SchemaVersion": "2018-10-08", + "Id": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)/CVE-2019-1549", + "ProductArn": "arn:aws:securityhub:test-region::product/aquasecurity/aquasecurity", + "GeneratorId": "Trivy/CVE-2019-1549", + "AwsAccountId": "123456789012", + "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "UpdatedAt": "2021-08-25T12:20:30.000000005Z", + "Severity": { + "Label": "MEDIUM" + }, + "Title": "Trivy found a vulnerability to CVE-2019-1549 in container testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2), related to libssl1.1", + "Description": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an applicati ..", + "Remediation": { + "Recommendation": { + "Text": "More information on this vulnerability is provided in the hyperlink", + "Url": "https://avd.aquasec.com/nvd/cve-2019-1549" + } + }, + "ProductFields": { "Product Name": "Trivy" }, + "Resources": [ + { + "Type": "Container", + "Id": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)", + "Partition": "aws", + "Region": "test-region", + "Details": { + "Container": { "ImageName": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)" }, + "Other": { + "CVE ID": "CVE-2019-1549", + "CVE Title": "openssl: information disclosure in fork()", + "PkgName": "libssl1.1", + "Installed Package": "1.1.1c-r0", + "Patched Package": "1.1.1d-r0", + "NvdCvssScoreV3": "5.3", + "NvdCvssVectorV3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "NvdCvssScoreV2": "5", + "NvdCvssVectorV2": "AV:N/AC:L/Au:N/C:P/I:N/A:N" + } + } + } + ], + "RecordState": "ACTIVE" + }, + { + "SchemaVersion": "2018-10-08", + "Id": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)/CVE-2019-1551", + "ProductArn": "arn:aws:securityhub:test-region::product/aquasecurity/aquasecurity", + "GeneratorId": "Trivy/CVE-2019-1551", + "AwsAccountId": "123456789012", + "Types": [ "Software and Configuration Checks/Vulnerabilities/CVE" ], + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "UpdatedAt": "2021-08-25T12:20:30.000000005Z", + "Severity": { + "Label": "MEDIUM" + }, + "Title": "Trivy found a vulnerability to CVE-2019-1551 in container testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2), related to libssl1.1", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly us ..", + "Remediation": { + "Recommendation": { + "Text": "More information on this vulnerability is provided in the hyperlink", + "Url": "https://avd.aquasec.com/nvd/cve-2019-1551" + } + }, + "ProductFields": { "Product Name": "Trivy" }, + "Resources": [ + { + "Type": "Container", + "Id": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)", + "Partition": "aws", + "Region": "test-region", + "Details": { + "Container": { "ImageName": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)" }, + "Other": { + "CVE ID": "CVE-2019-1551", + "CVE Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "PkgName": "libssl1.1", + "Installed Package": "1.1.1c-r0", + "Patched Package": "1.1.1d-r2", + "NvdCvssScoreV3": "5.3", + "NvdCvssVectorV3": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "NvdCvssScoreV2": "5", + "NvdCvssVectorV2": "AV:N/AC:L/Au:N/C:P/I:N/A:N" + } + } + } + ], + "RecordState": "ACTIVE" + } + ] +} diff --git a/integration/testdata/alpine-310.gitlab-codequality.golden b/integration/testdata/alpine-310.gitlab-codequality.golden new file mode 100644 index 000000000000..1a400bfd44d3 --- /dev/null +++ b/integration/testdata/alpine-310.gitlab-codequality.golden @@ -0,0 +1,62 @@ +[ + { + "type": "issue", + "check_name": "container_scanning", + "categories": [ "Security" ], + "description": "CVE-2019-1549 - libcrypto1.1 - 1.1.1c-r0 - openssl: information disclosure in fork()", + "fingerprint": "aeda1fbbe0e7f685887445359f8078c98eafd6de", + "content": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).", + "severity": "minor", + "location": { + "path": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)", + "lines": { + "begin": 0 + } + } + }, + { + "type": "issue", + "check_name": "container_scanning", + "categories": [ "Security" ], + "description": "CVE-2019-1551 - libcrypto1.1 - 1.1.1c-r0 - openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "fingerprint": "473af5b2ba6b728fa3f356551c4b07a3b64d4f2a", + "content": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "severity": "minor", + "location": { + "path": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)", + "lines": { + "begin": 0 + } + } + }, + { + "type": "issue", + "check_name": "container_scanning", + "categories": [ "Security" ], + "description": "CVE-2019-1549 - libssl1.1 - 1.1.1c-r0 - openssl: information disclosure in fork()", + "fingerprint": "45d39f7ecf688270aeab0da7fcad5c0cf5a57886", + "content": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).", + "severity": "minor", + "location": { + "path": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)", + "lines": { + "begin": 0 + } + } + }, + { + "type": "issue", + "check_name": "container_scanning", + "categories": [ "Security" ], + "description": "CVE-2019-1551 - libssl1.1 - 1.1.1c-r0 - openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "fingerprint": "28d484eb3b3439c4991f14b3b1b26cc339eee128", + "content": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "severity": "minor", + "location": { + "path": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)", + "lines": { + "begin": 0 + } + } + } +] diff --git a/integration/testdata/alpine-310.gitlab.golden b/integration/testdata/alpine-310.gitlab.golden new file mode 100644 index 000000000000..a0339dcf6bfb --- /dev/null +++ b/integration/testdata/alpine-310.gitlab.golden @@ -0,0 +1,337 @@ +{ + "version": "15.0.7", + "scan": { + "analyzer": { + "id": "trivy", + "name": "Trivy", + "vendor": { + "name": "Aqua Security" + }, + "version": "dev" + }, + "end_time": "2021-08-25T12:20:30", + "scanner": { + "id": "trivy", + "name": "Trivy", + "url": "https://github.com/aquasecurity/trivy/", + "vendor": { + "name": "Aqua Security" + }, + "version": "dev" + }, + "start_time": "2021-08-25T12:20:30", + "status": "success", + "type": "container_scanning" + }, + "vulnerabilities": [ + { + "id": "CVE-2019-1549", + "name": "openssl: information disclosure in fork()", + "description": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).", + "severity": "Medium", + "solution": "Upgrade libcrypto1.1 to 1.1.1d-r0", + "location": { + "dependency": { + "package": { + "name": "libcrypto1.1" + }, + "version": "1.1.1c-r0" + }, + "operating_system": "alpine 3.10.2", + "image": "testdata/fixtures/images/alpine-310.tar.gz" + }, + "identifiers": [ + { + "type": "cve", + "name": "CVE-2019-1549", + "value": "CVE-2019-1549", + "url": "https://avd.aquasec.com/nvd/cve-2019-1549" + } + ], + "links": [{ + "url": "https://access.redhat.com/security/cve/CVE-2019-1549" + },{ + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549" + },{ + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=1b0fe00e2704b5e20334a16d3c9099d1ba2ef1be" + },{ + "url": "https://linux.oracle.com/cve/CVE-2019-1549.html" + },{ + "url": "https://linux.oracle.com/errata/ELSA-2020-1840.html" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/" + },{ + "url": "https://seclists.org/bugtraq/2019/Oct/1" + },{ + "url": "https://security.netapp.com/advisory/ntap-20190919-0002/" + },{ + "url": "https://support.f5.com/csp/article/K44070243" + },{ + "url": "https://support.f5.com/csp/article/K44070243?utm_source=f5support&utm_medium=RSS" + },{ + "url": "https://ubuntu.com/security/notices/USN-4376-1" + },{ + "url": "https://usn.ubuntu.com/4376-1/" + },{ + "url": "https://www.debian.org/security/2019/dsa-4539" + },{ + "url": "https://www.openssl.org/news/secadv/20190910.txt" + },{ + "url": "https://www.oracle.com/security-alerts/cpuapr2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujan2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujul2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpuoct2020.html" + },{ + "url": "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + } + ] + }, + { + "id": "CVE-2019-1551", + "name": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "severity": "Medium", + "solution": "Upgrade libcrypto1.1 to 1.1.1d-r2", + "location": { + "dependency": { + "package": { + "name": "libcrypto1.1" + }, + "version": "1.1.1c-r0" + }, + "operating_system": "alpine 3.10.2", + "image": "testdata/fixtures/images/alpine-310.tar.gz" + }, + "identifiers": [ + { + "type": "cve", + "name": "CVE-2019-1551", + "value": "CVE-2019-1551", + "url": "https://avd.aquasec.com/nvd/cve-2019-1551" + } + ], + "links": [{ + "url": "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html" + },{ + "url": "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html" + },{ + "url": "https://access.redhat.com/security/cve/CVE-2019-1551" + },{ + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551" + },{ + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f" + },{ + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98" + },{ + "url": "https://github.com/openssl/openssl/pull/10575" + },{ + "url": "https://linux.oracle.com/cve/CVE-2019-1551.html" + },{ + "url": "https://linux.oracle.com/errata/ELSA-2020-4514.html" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/" + },{ + "url": "https://seclists.org/bugtraq/2019/Dec/39" + },{ + "url": "https://seclists.org/bugtraq/2019/Dec/46" + },{ + "url": "https://security.gentoo.org/glsa/202004-10" + },{ + "url": "https://security.netapp.com/advisory/ntap-20191210-0001/" + },{ + "url": "https://ubuntu.com/security/notices/USN-4376-1" + },{ + "url": "https://ubuntu.com/security/notices/USN-4504-1" + },{ + "url": "https://usn.ubuntu.com/4376-1/" + },{ + "url": "https://usn.ubuntu.com/4504-1/" + },{ + "url": "https://www.debian.org/security/2019/dsa-4594" + },{ + "url": "https://www.debian.org/security/2021/dsa-4855" + },{ + "url": "https://www.openssl.org/news/secadv/20191206.txt" + },{ + "url": "https://www.oracle.com/security-alerts/cpuApr2021.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujan2021.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujul2020.html" + },{ + "url": "https://www.tenable.com/security/tns-2019-09" + },{ + "url": "https://www.tenable.com/security/tns-2020-03" + },{ + "url": "https://www.tenable.com/security/tns-2020-11" + },{ + "url": "https://www.tenable.com/security/tns-2021-10" + } + ] + }, + { + "id": "CVE-2019-1549", + "name": "openssl: information disclosure in fork()", + "description": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).", + "severity": "Medium", + "solution": "Upgrade libssl1.1 to 1.1.1d-r0", + "location": { + "dependency": { + "package": { + "name": "libssl1.1" + }, + "version": "1.1.1c-r0" + }, + "operating_system": "alpine 3.10.2", + "image": "testdata/fixtures/images/alpine-310.tar.gz" + }, + "identifiers": [ + { + "type": "cve", + "name": "CVE-2019-1549", + "value": "CVE-2019-1549", + "url": "https://avd.aquasec.com/nvd/cve-2019-1549" + } + ], + "links": [{ + "url": "https://access.redhat.com/security/cve/CVE-2019-1549" + },{ + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549" + },{ + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=1b0fe00e2704b5e20334a16d3c9099d1ba2ef1be" + },{ + "url": "https://linux.oracle.com/cve/CVE-2019-1549.html" + },{ + "url": "https://linux.oracle.com/errata/ELSA-2020-1840.html" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/" + },{ + "url": "https://seclists.org/bugtraq/2019/Oct/1" + },{ + "url": "https://security.netapp.com/advisory/ntap-20190919-0002/" + },{ + "url": "https://support.f5.com/csp/article/K44070243" + },{ + "url": "https://support.f5.com/csp/article/K44070243?utm_source=f5support&utm_medium=RSS" + },{ + "url": "https://ubuntu.com/security/notices/USN-4376-1" + },{ + "url": "https://usn.ubuntu.com/4376-1/" + },{ + "url": "https://www.debian.org/security/2019/dsa-4539" + },{ + "url": "https://www.openssl.org/news/secadv/20190910.txt" + },{ + "url": "https://www.oracle.com/security-alerts/cpuapr2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujan2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujul2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpuoct2020.html" + },{ + "url": "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + } + ] + }, + { + "id": "CVE-2019-1551", + "name": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "severity": "Medium", + "solution": "Upgrade libssl1.1 to 1.1.1d-r2", + "location": { + "dependency": { + "package": { + "name": "libssl1.1" + }, + "version": "1.1.1c-r0" + }, + "operating_system": "alpine 3.10.2", + "image": "testdata/fixtures/images/alpine-310.tar.gz" + }, + "identifiers": [ + { + "type": "cve", + "name": "CVE-2019-1551", + "value": "CVE-2019-1551", + "url": "https://avd.aquasec.com/nvd/cve-2019-1551" + } + ], + "links": [{ + "url": "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html" + },{ + "url": "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html" + },{ + "url": "https://access.redhat.com/security/cve/CVE-2019-1551" + },{ + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551" + },{ + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f" + },{ + "url": "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98" + },{ + "url": "https://github.com/openssl/openssl/pull/10575" + },{ + "url": "https://linux.oracle.com/cve/CVE-2019-1551.html" + },{ + "url": "https://linux.oracle.com/errata/ELSA-2020-4514.html" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/" + },{ + "url": "https://seclists.org/bugtraq/2019/Dec/39" + },{ + "url": "https://seclists.org/bugtraq/2019/Dec/46" + },{ + "url": "https://security.gentoo.org/glsa/202004-10" + },{ + "url": "https://security.netapp.com/advisory/ntap-20191210-0001/" + },{ + "url": "https://ubuntu.com/security/notices/USN-4376-1" + },{ + "url": "https://ubuntu.com/security/notices/USN-4504-1" + },{ + "url": "https://usn.ubuntu.com/4376-1/" + },{ + "url": "https://usn.ubuntu.com/4504-1/" + },{ + "url": "https://www.debian.org/security/2019/dsa-4594" + },{ + "url": "https://www.debian.org/security/2021/dsa-4855" + },{ + "url": "https://www.openssl.org/news/secadv/20191206.txt" + },{ + "url": "https://www.oracle.com/security-alerts/cpuApr2021.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujan2021.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujul2020.html" + },{ + "url": "https://www.tenable.com/security/tns-2019-09" + },{ + "url": "https://www.tenable.com/security/tns-2020-03" + },{ + "url": "https://www.tenable.com/security/tns-2020-11" + },{ + "url": "https://www.tenable.com/security/tns-2021-10" + } + ] + } + ], + "remediations": [] +} diff --git a/integration/testdata/alpine-310.gsbom.golden b/integration/testdata/alpine-310.gsbom.golden new file mode 100644 index 000000000000..98dbcdc07749 --- /dev/null +++ b/integration/testdata/alpine-310.gsbom.golden @@ -0,0 +1,138 @@ +{ + "version": 0, + "detector": { + "name": "trivy", + "version": "dev", + "url": "https://github.com/aquasecurity/trivy" + }, + "metadata": { + "aquasecurity:trivy:RepoTag": "ghcr.io/aquasecurity/trivy-test-images:alpine-310" + }, + "ref": "/ref/feature-1", + "sha": "39da54a1ff04120a31df8cbc94ce9ede251d21a3", + "job": { + "correlator": "workflow-name_integration", + "id": "1910764383" + }, + "scanned": "2021-08-25T12:20:30Z", + "manifests": { + "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)": { + "name": "alpine", + "resolved": { + "alpine-baselayout": { + "package_url": "pkg:apk/alpine/alpine-baselayout@3.1.2-r0?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "busybox@1.30.1-r2", + "musl@1.1.22-r3" + ], + "scope": "runtime" + }, + "alpine-keys": { + "package_url": "pkg:apk/alpine/alpine-keys@2.1-r2?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "scope": "runtime" + }, + "apk-tools": { + "package_url": "pkg:apk/alpine/apk-tools@2.10.4-r2?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "libcrypto1.1@1.1.1c-r0", + "libssl1.1@1.1.1c-r0", + "musl@1.1.22-r3", + "zlib@1.2.11-r1" + ], + "scope": "runtime" + }, + "busybox": { + "package_url": "pkg:apk/alpine/busybox@1.30.1-r2?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "musl@1.1.22-r3" + ], + "scope": "runtime" + }, + "ca-certificates-cacert": { + "package_url": "pkg:apk/alpine/ca-certificates-cacert@20190108-r0?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "scope": "runtime" + }, + "libc-utils": { + "package_url": "pkg:apk/alpine/libc-utils@0.7.1-r0?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "musl-utils@1.1.22-r3" + ], + "scope": "runtime" + }, + "libcrypto1.1": { + "package_url": "pkg:apk/alpine/libcrypto1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "musl@1.1.22-r3" + ], + "scope": "runtime" + }, + "libssl1.1": { + "package_url": "pkg:apk/alpine/libssl1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "libcrypto1.1@1.1.1c-r0", + "musl@1.1.22-r3" + ], + "scope": "runtime" + }, + "libtls-standalone": { + "package_url": "pkg:apk/alpine/libtls-standalone@2.9.1-r0?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "ca-certificates-cacert@20190108-r0", + "libcrypto1.1@1.1.1c-r0", + "libssl1.1@1.1.1c-r0", + "musl@1.1.22-r3" + ], + "scope": "runtime" + }, + "musl": { + "package_url": "pkg:apk/alpine/musl@1.1.22-r3?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "scope": "runtime" + }, + "musl-utils": { + "package_url": "pkg:apk/alpine/musl-utils@1.1.22-r3?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "musl@1.1.22-r3", + "scanelf@1.2.3-r0" + ], + "scope": "runtime" + }, + "scanelf": { + "package_url": "pkg:apk/alpine/scanelf@1.2.3-r0?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "musl@1.1.22-r3" + ], + "scope": "runtime" + }, + "ssl_client": { + "package_url": "pkg:apk/alpine/ssl_client@1.30.1-r2?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "libtls-standalone@2.9.1-r0", + "musl@1.1.22-r3" + ], + "scope": "runtime" + }, + "zlib": { + "package_url": "pkg:apk/alpine/zlib@1.2.11-r1?arch=x86_64\u0026distro=3.10.2", + "relationship": "direct", + "dependencies": [ + "musl@1.1.22-r3" + ], + "scope": "runtime" + } + } + } + } +} \ No newline at end of file diff --git a/integration/testdata/alpine-310.html.golden b/integration/testdata/alpine-310.html.golden new file mode 100644 index 000000000000..83840bd015d5 --- /dev/null +++ b/integration/testdata/alpine-310.html.golden @@ -0,0 +1,234 @@ + + + + + + testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2) - Trivy Report - 2021-08-25 12:20:30.000000005 +0000 UTC + + + +

testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2) - Trivy Report - 2021-08-25 12:20:30.000000005 +0000 UTC

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
alpine
PackageVulnerability IDSeverityInstalled VersionFixed VersionLinks
libcrypto1.1CVE-2019-1549MEDIUM1.1.1c-r01.1.1d-r0
libcrypto1.1CVE-2019-1551MEDIUM1.1.1c-r01.1.1d-r2
libssl1.1CVE-2019-1549MEDIUM1.1.1c-r01.1.1d-r0
libssl1.1CVE-2019-1551MEDIUM1.1.1c-r01.1.1d-r2
No Misconfigurations found
+ + diff --git a/integration/testdata/alpine-310.json.golden b/integration/testdata/alpine-310.json.golden new file mode 100644 index 000000000000..ad2b3df4e92e --- /dev/null +++ b/integration/testdata/alpine-310.json.golden @@ -0,0 +1,393 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:39549bf49d696f172a6513103cdc8f53717024ad1fbce62d680a8e7ddde1a612", + "ArtifactName": "testdata/fixtures/images/alpine-310.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 5843968, + "OS": { + "Family": "alpine", + "Name": "3.10.2", + "EOSL": true + }, + "ImageID": "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", + "DiffIDs": [ + "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:alpine-310" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:alpine-310", + "ImageConfig": { + "architecture": "amd64", + "container": "0a80155a31551fcc1a36fccbbda79fcd3f0b1c7d270653d00310e6e2217c57e6", + "created": "2019-08-20T20:19:55.211423266Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-08-20T20:19:55.062606894Z", + "created_by": "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / " + }, + { + "created": "2019-08-20T20:19:55.211423266Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" + ] + }, + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:06f4121dff4d0123ce11bd2e44f48da9ba9ddcd23ae376ea1f363f63ea0849b5", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 5843968, + "Digest": "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609", + "DiffID": "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/alpine-310.tar.gz (alpine 3.10.2)", + "Class": "os-pkgs", + "Type": "alpine", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-1549", + "PkgID": "libcrypto1.1@1.1.1c-r0", + "PkgName": "libcrypto1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libcrypto1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2", + "UID": "c3465bd7087640a7" + }, + "InstalledVersion": "1.1.1c-r0", + "FixedVersion": "1.1.1d-r0", + "Status": "fixed", + "Layer": { + "Digest": "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609", + "DiffID": "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1549", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:6843bc8bcd09d2629416906fb83f17f31dde9a2702183571f9d447c3185814ca", + "Title": "openssl: information disclosure in fork()", + "Description": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-330" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-1549", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=1b0fe00e2704b5e20334a16d3c9099d1ba2ef1be", + "https://linux.oracle.com/cve/CVE-2019-1549.html", + "https://linux.oracle.com/errata/ELSA-2020-1840.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/", + "https://seclists.org/bugtraq/2019/Oct/1", + "https://security.netapp.com/advisory/ntap-20190919-0002/", + "https://support.f5.com/csp/article/K44070243", + "https://support.f5.com/csp/article/K44070243?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://usn.ubuntu.com/4376-1/", + "https://www.debian.org/security/2019/dsa-4539", + "https://www.openssl.org/news/secadv/20190910.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + ], + "PublishedDate": "2019-09-10T17:15:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "libcrypto1.1@1.1.1c-r0", + "PkgName": "libcrypto1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libcrypto1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2", + "UID": "c3465bd7087640a7" + }, + "InstalledVersion": "1.1.1c-r0", + "FixedVersion": "1.1.1d-r2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609", + "DiffID": "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:692932c929e26ea1116ebcaf2ca427771924d31272883401dedb3381d825063a", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1549", + "PkgID": "libssl1.1@1.1.1c-r0", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libssl1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2", + "UID": "5743e53dcec3d29c" + }, + "InstalledVersion": "1.1.1c-r0", + "FixedVersion": "1.1.1d-r0", + "Status": "fixed", + "Layer": { + "Digest": "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609", + "DiffID": "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1549", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:70c608adcc518d4c4bd9a183e0181da83ba22ba8b0d3a462573a72c8e6ad7362", + "Title": "openssl: information disclosure in fork()", + "Description": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-330" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-1549", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=1b0fe00e2704b5e20334a16d3c9099d1ba2ef1be", + "https://linux.oracle.com/cve/CVE-2019-1549.html", + "https://linux.oracle.com/errata/ELSA-2020-1840.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/", + "https://seclists.org/bugtraq/2019/Oct/1", + "https://security.netapp.com/advisory/ntap-20190919-0002/", + "https://support.f5.com/csp/article/K44070243", + "https://support.f5.com/csp/article/K44070243?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://usn.ubuntu.com/4376-1/", + "https://www.debian.org/security/2019/dsa-4539", + "https://www.openssl.org/news/secadv/20190910.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + ], + "PublishedDate": "2019-09-10T17:15:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "libssl1.1@1.1.1c-r0", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libssl1.1@1.1.1c-r0?arch=x86_64\u0026distro=3.10.2", + "UID": "5743e53dcec3d29c" + }, + "InstalledVersion": "1.1.1c-r0", + "FixedVersion": "1.1.1d-r2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609", + "DiffID": "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:7327b4ce6851d27db6528dfec9b6ea312143eb7e77e1f7ada4f567a64e567663", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/alpine-310.junit.golden b/integration/testdata/alpine-310.junit.golden new file mode 100644 index 000000000000..b3f5aab40c3b --- /dev/null +++ b/integration/testdata/alpine-310.junit.golden @@ -0,0 +1,25 @@ + + + + + + + + OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). + + + There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t). + + + OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). + + + There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t). + + + + + + + + diff --git a/integration/testdata/alpine-310.sarif.golden b/integration/testdata/alpine-310.sarif.golden new file mode 100644 index 000000000000..bdda1088a4fd --- /dev/null +++ b/integration/testdata/alpine-310.sarif.golden @@ -0,0 +1,204 @@ +{ + "version": "2.1.0", + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + "runs": [ + { + "tool": { + "driver": { + "fullName": "Trivy Vulnerability Scanner", + "informationUri": "https://github.com/aquasecurity/trivy", + "name": "Trivy", + "rules": [ + { + "id": "CVE-2019-1549", + "name": "OsPackageVulnerability", + "shortDescription": { + "text": "openssl: information disclosure in fork()" + }, + "fullDescription": { + "text": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c)." + }, + "defaultConfiguration": { + "level": "warning" + }, + "helpUri": "https://avd.aquasec.com/nvd/cve-2019-1549", + "help": { + "text": "Vulnerability CVE-2019-1549\nSeverity: MEDIUM\nPackage: libssl1.1\nFixed Version: 1.1.1d-r0\nLink: [CVE-2019-1549](https://avd.aquasec.com/nvd/cve-2019-1549)\nOpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).", + "markdown": "**Vulnerability CVE-2019-1549**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|MEDIUM|libssl1.1|1.1.1d-r0|[CVE-2019-1549](https://avd.aquasec.com/nvd/cve-2019-1549)|\n\nOpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c)." + }, + "properties": { + "cvssv2_score": 5, + "cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "cvssv3_baseScore": 5.3, + "cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "precision": "very-high", + "security-severity": "5.3", + "tags": [ + "vulnerability", + "security", + "MEDIUM" + ] + } + }, + { + "id": "CVE-2019-1551", + "name": "OsPackageVulnerability", + "shortDescription": { + "text": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64" + }, + "fullDescription": { + "text": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t)." + }, + "defaultConfiguration": { + "level": "warning" + }, + "helpUri": "https://avd.aquasec.com/nvd/cve-2019-1551", + "help": { + "text": "Vulnerability CVE-2019-1551\nSeverity: MEDIUM\nPackage: libssl1.1\nFixed Version: 1.1.1d-r2\nLink: [CVE-2019-1551](https://avd.aquasec.com/nvd/cve-2019-1551)\nThere is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "markdown": "**Vulnerability CVE-2019-1551**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|MEDIUM|libssl1.1|1.1.1d-r2|[CVE-2019-1551](https://avd.aquasec.com/nvd/cve-2019-1551)|\n\nThere is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t)." + }, + "properties": { + "cvssv2_score": 5, + "cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "cvssv3_baseScore": 5.3, + "cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "precision": "very-high", + "security-severity": "5.3", + "tags": [ + "vulnerability", + "security", + "MEDIUM" + ] + } + } + ], + "version": "dev" + } + }, + "results": [ + { + "ruleId": "CVE-2019-1549", + "ruleIndex": 0, + "level": "warning", + "message": { + "text": "Package: libcrypto1.1\nInstalled Version: 1.1.1c-r0\nVulnerability CVE-2019-1549\nSeverity: MEDIUM\nFixed Version: 1.1.1d-r0\nLink: [CVE-2019-1549](https://avd.aquasec.com/nvd/cve-2019-1549)" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "testdata/fixtures/images/alpine-310.tar.gz", + "uriBaseId": "ROOTPATH" + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endLine": 1, + "endColumn": 1 + } + }, + "message": { + "text": "testdata/fixtures/images/alpine-310.tar.gz: libcrypto1.1@1.1.1c-r0" + } + } + ] + }, + { + "ruleId": "CVE-2019-1551", + "ruleIndex": 1, + "level": "warning", + "message": { + "text": "Package: libcrypto1.1\nInstalled Version: 1.1.1c-r0\nVulnerability CVE-2019-1551\nSeverity: MEDIUM\nFixed Version: 1.1.1d-r2\nLink: [CVE-2019-1551](https://avd.aquasec.com/nvd/cve-2019-1551)" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "testdata/fixtures/images/alpine-310.tar.gz", + "uriBaseId": "ROOTPATH" + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endLine": 1, + "endColumn": 1 + } + }, + "message": { + "text": "testdata/fixtures/images/alpine-310.tar.gz: libcrypto1.1@1.1.1c-r0" + } + } + ] + }, + { + "ruleId": "CVE-2019-1549", + "ruleIndex": 0, + "level": "warning", + "message": { + "text": "Package: libssl1.1\nInstalled Version: 1.1.1c-r0\nVulnerability CVE-2019-1549\nSeverity: MEDIUM\nFixed Version: 1.1.1d-r0\nLink: [CVE-2019-1549](https://avd.aquasec.com/nvd/cve-2019-1549)" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "testdata/fixtures/images/alpine-310.tar.gz", + "uriBaseId": "ROOTPATH" + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endLine": 1, + "endColumn": 1 + } + }, + "message": { + "text": "testdata/fixtures/images/alpine-310.tar.gz: libssl1.1@1.1.1c-r0" + } + } + ] + }, + { + "ruleId": "CVE-2019-1551", + "ruleIndex": 1, + "level": "warning", + "message": { + "text": "Package: libssl1.1\nInstalled Version: 1.1.1c-r0\nVulnerability CVE-2019-1551\nSeverity: MEDIUM\nFixed Version: 1.1.1d-r2\nLink: [CVE-2019-1551](https://avd.aquasec.com/nvd/cve-2019-1551)" + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { + "uri": "testdata/fixtures/images/alpine-310.tar.gz", + "uriBaseId": "ROOTPATH" + }, + "region": { + "startLine": 1, + "startColumn": 1, + "endLine": 1, + "endColumn": 1 + } + }, + "message": { + "text": "testdata/fixtures/images/alpine-310.tar.gz: libssl1.1@1.1.1c-r0" + } + } + ] + } + ], + "columnKind": "utf16CodeUnits", + "originalUriBaseIds": { + "ROOTPATH": { + "uri": "file:///" + } + }, + "properties": { + "imageID": "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", + "imageName": "testdata/fixtures/images/alpine-310.tar.gz", + "repoDigests": null, + "repoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:alpine-310" + ] + } + } + ] +} \ No newline at end of file diff --git a/integration/testdata/alpine-39-high-critical.json.golden b/integration/testdata/alpine-39-high-critical.json.golden new file mode 100644 index 000000000000..d2bd143c9999 --- /dev/null +++ b/integration/testdata/alpine-39-high-critical.json.golden @@ -0,0 +1,171 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:988a8e3eb049d90c20fafb183d0e792c99b8ba28433be1d1e4447a8b5a1adbdf", + "ArtifactName": "testdata/fixtures/images/alpine-39.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 5796352, + "OS": { + "Family": "alpine", + "Name": "3.9.4", + "EOSL": true + }, + "ImageID": "sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1", + "DiffIDs": [ + "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:alpine-39" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:alpine-39", + "ImageConfig": { + "architecture": "amd64", + "container": "c10d36fa368a7ea673683682666758adf35efe98e10989505f4f566b5b18538f", + "created": "2019-05-11T00:07:03.510395965Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-05-11T00:07:03.358250803Z", + "created_by": "/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6ae03397b99ea77f2e9ee901c5c59e59f76f93adbb4035913 in / " + }, + { + "created": "2019-05-11T00:07:03.510395965Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + ] + }, + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:09f2bbe58e774849d74dc1391c2e01731896c745c4aba1ecf69a283bdb4b537a", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 5796352, + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/alpine-39.tar.gz (alpine 3.9.4)", + "Class": "os-pkgs", + "Type": "alpine", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-14697", + "PkgID": "musl@1.1.20-r4", + "PkgName": "musl", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/musl@1.1.20-r4?arch=x86_64\u0026distro=3.9.4", + "UID": "54bca8ef3ee9e338" + }, + "InstalledVersion": "1.1.20-r4", + "FixedVersion": "1.1.20-r5", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-14697", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:294b2f8c416dd7b50971286b95e1f2685d7daf3c18957237fa7dc666178b6183", + "Description": "musl libc through 1.1.23 has an x87 floating-point stack adjustment imbalance, related to the math/i386/ directory. In some cases, use of this library could introduce out-of-bounds writes that are not present in an application's source code.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "nvd": 4 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + } + }, + "References": [ + "http://www.openwall.com/lists/oss-security/2019/08/06/4", + "https://security.gentoo.org/glsa/202003-13", + "https://www.openwall.com/lists/musl/2019/08/06/1" + ], + "PublishedDate": "2019-08-06T16:15:00Z", + "LastModifiedDate": "2020-03-14T19:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-14697", + "PkgID": "musl-utils@1.1.20-r4", + "PkgName": "musl-utils", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/musl-utils@1.1.20-r4?arch=x86_64\u0026distro=3.9.4", + "UID": "792dadf1e7110cf2" + }, + "InstalledVersion": "1.1.20-r4", + "FixedVersion": "1.1.20-r5", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-14697", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:fae2ab3e458f1237c8202860ac513a204cdc999c3283eba49665d0f1b3b79856", + "Description": "musl libc through 1.1.23 has an x87 floating-point stack adjustment imbalance, related to the math/i386/ directory. In some cases, use of this library could introduce out-of-bounds writes that are not present in an application's source code.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "nvd": 4 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + } + }, + "References": [ + "http://www.openwall.com/lists/oss-security/2019/08/06/4", + "https://security.gentoo.org/glsa/202003-13", + "https://www.openwall.com/lists/musl/2019/08/06/1" + ], + "PublishedDate": "2019-08-06T16:15:00Z", + "LastModifiedDate": "2020-03-14T19:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/alpine-39-ignore-cveids.json.golden b/integration/testdata/alpine-39-ignore-cveids.json.golden new file mode 100644 index 000000000000..9c87bd47e0a0 --- /dev/null +++ b/integration/testdata/alpine-39-ignore-cveids.json.golden @@ -0,0 +1,245 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:988a8e3eb049d90c20fafb183d0e792c99b8ba28433be1d1e4447a8b5a1adbdf", + "ArtifactName": "testdata/fixtures/images/alpine-39.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 5796352, + "OS": { + "Family": "alpine", + "Name": "3.9.4", + "EOSL": true + }, + "ImageID": "sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1", + "DiffIDs": [ + "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:alpine-39" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:alpine-39", + "ImageConfig": { + "architecture": "amd64", + "container": "c10d36fa368a7ea673683682666758adf35efe98e10989505f4f566b5b18538f", + "created": "2019-05-11T00:07:03.510395965Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-05-11T00:07:03.358250803Z", + "created_by": "/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6ae03397b99ea77f2e9ee901c5c59e59f76f93adbb4035913 in / " + }, + { + "created": "2019-05-11T00:07:03.510395965Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + ] + }, + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:09f2bbe58e774849d74dc1391c2e01731896c745c4aba1ecf69a283bdb4b537a", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 5796352, + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/alpine-39.tar.gz (alpine 3.9.4)", + "Class": "os-pkgs", + "Type": "alpine", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "libcrypto1.1@1.1.1b-r1", + "PkgName": "libcrypto1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libcrypto1.1@1.1.1b-r1?arch=x86_64\u0026distro=3.9.4", + "UID": "3148053bd3cfcabf" + }, + "InstalledVersion": "1.1.1b-r1", + "FixedVersion": "1.1.1d-r2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:3dee09788d3e7607e0f6e4be151b5588fd339556441786d03efe99d73f92ada3", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "libssl1.1@1.1.1b-r1", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libssl1.1@1.1.1b-r1?arch=x86_64\u0026distro=3.9.4", + "UID": "603cbbc71f2d6dec" + }, + "InstalledVersion": "1.1.1b-r1", + "FixedVersion": "1.1.1d-r2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:ddf71a0dc7fcaa5777046af6e5c12587fe24223e5ef5c6b020047b5e7297c041", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/alpine-39-skip.json.golden b/integration/testdata/alpine-39-skip.json.golden new file mode 100644 index 000000000000..c110e4318dd9 --- /dev/null +++ b/integration/testdata/alpine-39-skip.json.golden @@ -0,0 +1,67 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:988a8e3eb049d90c20fafb183d0e792c99b8ba28433be1d1e4447a8b5a1adbdf", + "ArtifactName": "testdata/fixtures/images/alpine-39.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 5796352, + "OS": { + "Family": "none", + "Name": "" + }, + "ImageID": "sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1", + "DiffIDs": [ + "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:alpine-39" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:alpine-39", + "ImageConfig": { + "architecture": "amd64", + "container": "c10d36fa368a7ea673683682666758adf35efe98e10989505f4f566b5b18538f", + "created": "2019-05-11T00:07:03.510395965Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-05-11T00:07:03.358250803Z", + "created_by": "/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6ae03397b99ea77f2e9ee901c5c59e59f76f93adbb4035913 in / " + }, + { + "created": "2019-05-11T00:07:03.510395965Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + ] + }, + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:09f2bbe58e774849d74dc1391c2e01731896c745c4aba1ecf69a283bdb4b537a", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 5796352, + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + } + ] + } +} diff --git a/integration/testdata/alpine-39.json.golden b/integration/testdata/alpine-39.json.golden new file mode 100644 index 000000000000..77306e342be1 --- /dev/null +++ b/integration/testdata/alpine-39.json.golden @@ -0,0 +1,487 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:988a8e3eb049d90c20fafb183d0e792c99b8ba28433be1d1e4447a8b5a1adbdf", + "ArtifactName": "testdata/fixtures/images/alpine-39.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 5796352, + "OS": { + "Family": "alpine", + "Name": "3.9.4", + "EOSL": true + }, + "ImageID": "sha256:055936d3920576da37aa9bc460d70c5f212028bda1c08c0879aedf03d7a66ea1", + "DiffIDs": [ + "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:alpine-39" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:alpine-39", + "ImageConfig": { + "architecture": "amd64", + "container": "c10d36fa368a7ea673683682666758adf35efe98e10989505f4f566b5b18538f", + "created": "2019-05-11T00:07:03.510395965Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-05-11T00:07:03.358250803Z", + "created_by": "/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6ae03397b99ea77f2e9ee901c5c59e59f76f93adbb4035913 in / " + }, + { + "created": "2019-05-11T00:07:03.510395965Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + ] + }, + "config": { + "Cmd": [ + "/bin/sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:09f2bbe58e774849d74dc1391c2e01731896c745c4aba1ecf69a283bdb4b537a", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 5796352, + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/alpine-39.tar.gz (alpine 3.9.4)", + "Class": "os-pkgs", + "Type": "alpine", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-1549", + "PkgID": "libcrypto1.1@1.1.1b-r1", + "PkgName": "libcrypto1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libcrypto1.1@1.1.1b-r1?arch=x86_64\u0026distro=3.9.4", + "UID": "3148053bd3cfcabf" + }, + "InstalledVersion": "1.1.1b-r1", + "FixedVersion": "1.1.1d-r0", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1549", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:04e4bf2b8440c9c264b01802ec2f574e5aa5d3a9494a8910b51f0e2ff332d0ed", + "Title": "openssl: information disclosure in fork()", + "Description": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-330" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-1549", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=1b0fe00e2704b5e20334a16d3c9099d1ba2ef1be", + "https://linux.oracle.com/cve/CVE-2019-1549.html", + "https://linux.oracle.com/errata/ELSA-2020-1840.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/", + "https://seclists.org/bugtraq/2019/Oct/1", + "https://security.netapp.com/advisory/ntap-20190919-0002/", + "https://support.f5.com/csp/article/K44070243", + "https://support.f5.com/csp/article/K44070243?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://usn.ubuntu.com/4376-1/", + "https://www.debian.org/security/2019/dsa-4539", + "https://www.openssl.org/news/secadv/20190910.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + ], + "PublishedDate": "2019-09-10T17:15:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "libcrypto1.1@1.1.1b-r1", + "PkgName": "libcrypto1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libcrypto1.1@1.1.1b-r1?arch=x86_64\u0026distro=3.9.4", + "UID": "3148053bd3cfcabf" + }, + "InstalledVersion": "1.1.1b-r1", + "FixedVersion": "1.1.1d-r2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:3dee09788d3e7607e0f6e4be151b5588fd339556441786d03efe99d73f92ada3", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1549", + "PkgID": "libssl1.1@1.1.1b-r1", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libssl1.1@1.1.1b-r1?arch=x86_64\u0026distro=3.9.4", + "UID": "603cbbc71f2d6dec" + }, + "InstalledVersion": "1.1.1b-r1", + "FixedVersion": "1.1.1d-r0", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1549", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:5f8a6a0ec0df8088bbbeb7b5e4e8d495a5801ee295223c3493cb06de2baaaf2e", + "Title": "openssl: information disclosure in fork()", + "Description": "OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-330" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-1549", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=1b0fe00e2704b5e20334a16d3c9099d1ba2ef1be", + "https://linux.oracle.com/cve/CVE-2019-1549.html", + "https://linux.oracle.com/errata/ELSA-2020-1840.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/", + "https://seclists.org/bugtraq/2019/Oct/1", + "https://security.netapp.com/advisory/ntap-20190919-0002/", + "https://support.f5.com/csp/article/K44070243", + "https://support.f5.com/csp/article/K44070243?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://usn.ubuntu.com/4376-1/", + "https://www.debian.org/security/2019/dsa-4539", + "https://www.openssl.org/news/secadv/20190910.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + ], + "PublishedDate": "2019-09-10T17:15:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "libssl1.1@1.1.1b-r1", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/libssl1.1@1.1.1b-r1?arch=x86_64\u0026distro=3.9.4", + "UID": "603cbbc71f2d6dec" + }, + "InstalledVersion": "1.1.1b-r1", + "FixedVersion": "1.1.1d-r2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:ddf71a0dc7fcaa5777046af6e5c12587fe24223e5ef5c6b020047b5e7297c041", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + }, + { + "VulnerabilityID": "CVE-2019-14697", + "PkgID": "musl@1.1.20-r4", + "PkgName": "musl", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/musl@1.1.20-r4?arch=x86_64\u0026distro=3.9.4", + "UID": "54bca8ef3ee9e338" + }, + "InstalledVersion": "1.1.20-r4", + "FixedVersion": "1.1.20-r5", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-14697", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:294b2f8c416dd7b50971286b95e1f2685d7daf3c18957237fa7dc666178b6183", + "Description": "musl libc through 1.1.23 has an x87 floating-point stack adjustment imbalance, related to the math/i386/ directory. In some cases, use of this library could introduce out-of-bounds writes that are not present in an application's source code.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "nvd": 4 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + } + }, + "References": [ + "http://www.openwall.com/lists/oss-security/2019/08/06/4", + "https://security.gentoo.org/glsa/202003-13", + "https://www.openwall.com/lists/musl/2019/08/06/1" + ], + "PublishedDate": "2019-08-06T16:15:00Z", + "LastModifiedDate": "2020-03-14T19:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-14697", + "PkgID": "musl-utils@1.1.20-r4", + "PkgName": "musl-utils", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/musl-utils@1.1.20-r4?arch=x86_64\u0026distro=3.9.4", + "UID": "792dadf1e7110cf2" + }, + "InstalledVersion": "1.1.20-r4", + "FixedVersion": "1.1.20-r5", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10", + "DiffID": "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-14697", + "DataSource": { + "ID": "alpine", + "Name": "Alpine Secdb", + "URL": "https://secdb.alpinelinux.org/" + }, + "Fingerprint": "sha256:fae2ab3e458f1237c8202860ac513a204cdc999c3283eba49665d0f1b3b79856", + "Description": "musl libc through 1.1.23 has an x87 floating-point stack adjustment imbalance, related to the math/i386/ directory. In some cases, use of this library could introduce out-of-bounds writes that are not present in an application's source code.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "nvd": 4 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + } + }, + "References": [ + "http://www.openwall.com/lists/oss-security/2019/08/06/4", + "https://security.gentoo.org/glsa/202003-13", + "https://www.openwall.com/lists/musl/2019/08/06/1" + ], + "PublishedDate": "2019-08-06T16:15:00Z", + "LastModifiedDate": "2020-03-14T19:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/alpine-distroless.json.golden b/integration/testdata/alpine-distroless.json.golden new file mode 100644 index 000000000000..4d5ed47d0f85 --- /dev/null +++ b/integration/testdata/alpine-distroless.json.golden @@ -0,0 +1,109 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:0edd1906378dca3abc435f47f2e4b91059e9950e55cd82c76089d60b9ca68f90", + "ArtifactName": "testdata/fixtures/images/alpine-distroless.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 35812864, + "OS": { + "Family": "alpine", + "Name": "3.16" + }, + "ImageID": "sha256:22848737c0d272ad5d7c7369d8ca830a62929e63e38edcb22085139a6ae0688d", + "DiffIDs": [ + "sha256:89da7cc836da4b53ab1ceb572576458c005e7e444b8bb79abda196668a2f0c92" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:alpine-distroless" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:alpine-distroless", + "ImageConfig": { + "architecture": "amd64", + "author": "github.com/chainguard-dev/apko", + "created": "1970-01-01T00:00:00Z", + "history": [ + { + "author": "apko", + "created": "1970-01-01T00:00:00Z", + "created_by": "apko", + "comment": "This is an apko single-layer image" + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:89da7cc836da4b53ab1ceb572576458c005e7e444b8bb79abda196668a2f0c92" + ] + }, + "config": { + "Entrypoint": [ + "/usr/bin/git" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt" + ], + "User": "65532" + } + }, + "Layers": [ + { + "Size": 35812864, + "Digest": "sha256:6c6f69aa25501b090c54c62a9c17e978064c2f1328f67a7ef88c81ce5f2d7983", + "DiffID": "sha256:89da7cc836da4b53ab1ceb572576458c005e7e444b8bb79abda196668a2f0c92" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/alpine-distroless.tar.gz (alpine 3.16)", + "Class": "os-pkgs", + "Type": "alpine", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-24765", + "PkgID": "git@2.35.1-r2", + "PkgName": "git", + "PkgIdentifier": { + "PURL": "pkg:apk/alpine/git@2.35.1-r2?arch=x86_64\u0026distro=3.16", + "UID": "c6d52f037391dfc7" + }, + "InstalledVersion": "2.35.1-r2", + "FixedVersion": "2.35.2-r0", + "Status": "fixed", + "Layer": { + "Digest": "sha256:6c6f69aa25501b090c54c62a9c17e978064c2f1328f67a7ef88c81ce5f2d7983", + "DiffID": "sha256:89da7cc836da4b53ab1ceb572576458c005e7e444b8bb79abda196668a2f0c92" + }, + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-24765", + "Fingerprint": "sha256:07e9315f491c021347b65547ff629f3f2e8179f1b8ff646070390c4919e1a837", + "Title": "Git for Windows is a fork of Git containing Windows-specific patches. ...", + "Description": "Git for Windows is a fork of Git containing Windows-specific patches.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-427" + ], + "VendorSeverity": { + "ubuntu": 2 + }, + "References": [ + "http://www.openwall.com/lists/oss-security/2022/04/12/7", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24765", + "https://git-scm.com/book/en/v2/Appendix-A%3A-Git-in-Other-Environments-Git-in-Bash", + "https://git-scm.com/docs/git#Documentation/git.txt-codeGITCEILINGDIRECTORIEScode", + "https://github.com/git-for-windows/git/security/advisories/GHSA-vw2c-22j4-2fh2", + "https://ubuntu.com/security/notices/USN-5376-1" + ], + "PublishedDate": "2022-04-12T18:15:00Z", + "LastModifiedDate": "2022-04-12T21:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/amazon-1.json.golden b/integration/testdata/amazon-1.json.golden new file mode 100644 index 000000000000..e761250739df --- /dev/null +++ b/integration/testdata/amazon-1.json.golden @@ -0,0 +1,149 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:5a0fd7bb415c9b52d1bb909e40b9f498a89a5572724bd107d26ead4a25f203e1", + "ArtifactName": "testdata/fixtures/images/amazon-1.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 172655616, + "OS": { + "Family": "amazon", + "Name": "AMI release 2018.03" + }, + "ImageID": "sha256:961c4ee06269351d858969ea0426878675ed708d3a140246eabbc0bfc352bffa", + "DiffIDs": [ + "sha256:984fe1509738f6f00f34d9be7398b07ebeb8b98dda077ff6be2cdb87111b73cf" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:amazon-1" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:amazon-1", + "ImageConfig": { + "architecture": "amd64", + "container": "ef1b126795001e9b4bdc14a01180e4d8146282d279f53e05adfaa8195ecda20e", + "created": "2019-09-05T23:37:46.854286502Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-09-05T23:37:46.575366692Z", + "created_by": "/bin/sh -c #(nop) ADD file:45ed06ba8960dec70e01e809fe38df2718d4b16aa2b0f88835522d8366de71e3 in / " + }, + { + "created": "2019-09-05T23:37:46.854286502Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:984fe1509738f6f00f34d9be7398b07ebeb8b98dda077ff6be2cdb87111b73cf" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:8db654f611aca1693ac658bd981ee35e4b6517e6ef74fa608c4b3b3595a986c8", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 172655616, + "Digest": "sha256:105ff6bf468b1422ad7c47ea9d63eae82f875c93310cb8d34551951e754ef43b", + "DiffID": "sha256:984fe1509738f6f00f34d9be7398b07ebeb8b98dda077ff6be2cdb87111b73cf" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/amazon-1.tar.gz (amazon AMI release 2018.03)", + "Class": "os-pkgs", + "Type": "amazon", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-5481", + "PkgID": "curl@7.61.1-11.91.amzn1.x86_64", + "PkgName": "curl", + "PkgIdentifier": { + "PURL": "pkg:rpm/amazon/curl@7.61.1-11.91.amzn1?arch=x86_64\u0026distro=amazon-AMI+release+2018.03", + "UID": "e50eea8f63fe4bb6" + }, + "InstalledVersion": "7.61.1-11.91.amzn1", + "FixedVersion": "7.61.1-12.93.amzn1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:105ff6bf468b1422ad7c47ea9d63eae82f875c93310cb8d34551951e754ef43b", + "DiffID": "sha256:984fe1509738f6f00f34d9be7398b07ebeb8b98dda077ff6be2cdb87111b73cf" + }, + "SeveritySource": "amazon", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5481", + "DataSource": { + "ID": "amazon", + "Name": "Amazon Linux Security Center", + "URL": "https://alas.aws.amazon.com/" + }, + "Fingerprint": "sha256:6d5df637f78490e5091381186a322db8c42a7018fb2cfae27aaa084906e65f02", + "Title": "curl: double free due to subsequent call of realloc()", + "Description": "Double-free vulnerability in the FTP-kerberos code in cURL 7.52.0 to 7.65.3.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-415" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 2, + "nvd": 4, + "oracle-oval": 2, + "photon": 4, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H", + "V3Score": 5.7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00048.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00055.html", + "https://access.redhat.com/security/cve/CVE-2019-5481", + "https://curl.haxx.se/docs/CVE-2019-5481.html", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5481", + "https://linux.oracle.com/cve/CVE-2019-5481.html", + "https://linux.oracle.com/errata/ELSA-2020-1792.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6CI4QQ2RSZX4VCFM76SIWGKY6BY7UWIC/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RGDVKSLY5JUNJRLYRUA6CXGQ2LM63XC3/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UA7KDM2WPM5CJDDGOEGFV6SSGD2J7RNT/", + "https://seclists.org/bugtraq/2020/Feb/36", + "https://security.gentoo.org/glsa/202003-29", + "https://security.netapp.com/advisory/ntap-20191004-0003/", + "https://ubuntu.com/security/notices/USN-4129-1", + "https://www.debian.org/security/2020/dsa-4633", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html" + ], + "PublishedDate": "2019-09-16T19:15:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/amazon-2.json.golden b/integration/testdata/amazon-2.json.golden new file mode 100644 index 000000000000..481a0f3ef42f --- /dev/null +++ b/integration/testdata/amazon-2.json.golden @@ -0,0 +1,224 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:87e7ebcf8b5c0a26985fd80875e09e11850fa4828e1156da190f85b17dcecb71", + "ArtifactName": "testdata/fixtures/images/amazon-2.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 168852480, + "OS": { + "Family": "amazon", + "Name": "2 (Karoo)" + }, + "ImageID": "sha256:b94321659aca6a89cb7650a5b864bc8ec4bf62c620b8f1a01530c2e90a88c391", + "DiffIDs": [ + "sha256:f387c8b346c85cae37abd1f1a63015acb69f593dc425d0269f57d1012c3a81f6" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:amazon-2" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:amazon-2", + "ImageConfig": { + "architecture": "amd64", + "container": "e020a5508b9f809b29659128692cd634e3d4fba3f2c13d2029d797317b5c3a56", + "created": "2019-05-23T22:20:00.121624838Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-05-23T22:19:59.161963646Z", + "created_by": "/bin/sh -c #(nop) ADD file:3cf811fe5073384ff1d5f405992ef7e5e452ad6d4a4cb873eee65007382f3a4a in / " + }, + { + "created": "2019-05-23T22:20:00.121624838Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:f387c8b346c85cae37abd1f1a63015acb69f593dc425d0269f57d1012c3a81f6" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:648b8b37f8b5087423bec7f4331271253f8aff63154761a67c22cd0c3ba2661b", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 168852480, + "Digest": "sha256:72d97abdfae3b3c933ff41e39779cc72853d7bd9dc1e4800c5294d6715257799", + "DiffID": "sha256:f387c8b346c85cae37abd1f1a63015acb69f593dc425d0269f57d1012c3a81f6" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/amazon-2.tar.gz (amazon 2 (Karoo))", + "Class": "os-pkgs", + "Type": "amazon", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-5481", + "PkgID": "curl@7.61.1-9.amzn2.0.1.x86_64", + "PkgName": "curl", + "PkgIdentifier": { + "PURL": "pkg:rpm/amazon/curl@7.61.1-9.amzn2.0.1?arch=x86_64\u0026distro=amazon-2+%28Karoo%29", + "UID": "dfda69b8a2968a7f" + }, + "InstalledVersion": "7.61.1-9.amzn2.0.1", + "FixedVersion": "7.61.1-12.amzn2.0.1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:72d97abdfae3b3c933ff41e39779cc72853d7bd9dc1e4800c5294d6715257799", + "DiffID": "sha256:f387c8b346c85cae37abd1f1a63015acb69f593dc425d0269f57d1012c3a81f6" + }, + "SeveritySource": "amazon", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5481", + "DataSource": { + "ID": "amazon", + "Name": "Amazon Linux Security Center", + "URL": "https://alas.aws.amazon.com/" + }, + "Fingerprint": "sha256:4b87e2eef59ac046b40959211c0a4c52437e121ee2fedb34552c3bbf5ae51842", + "Title": "curl: double free due to subsequent call of realloc()", + "Description": "Double-free vulnerability in the FTP-kerberos code in cURL 7.52.0 to 7.65.3.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-415" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 2, + "nvd": 4, + "oracle-oval": 2, + "photon": 4, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H", + "V3Score": 5.7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00048.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00055.html", + "https://access.redhat.com/security/cve/CVE-2019-5481", + "https://curl.haxx.se/docs/CVE-2019-5481.html", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5481", + "https://linux.oracle.com/cve/CVE-2019-5481.html", + "https://linux.oracle.com/errata/ELSA-2020-1792.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6CI4QQ2RSZX4VCFM76SIWGKY6BY7UWIC/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RGDVKSLY5JUNJRLYRUA6CXGQ2LM63XC3/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UA7KDM2WPM5CJDDGOEGFV6SSGD2J7RNT/", + "https://seclists.org/bugtraq/2020/Feb/36", + "https://security.gentoo.org/glsa/202003-29", + "https://security.netapp.com/advisory/ntap-20191004-0003/", + "https://ubuntu.com/security/notices/USN-4129-1", + "https://www.debian.org/security/2020/dsa-4633", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html" + ], + "PublishedDate": "2019-09-16T19:15:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5436", + "PkgID": "curl@7.61.1-9.amzn2.0.1.x86_64", + "PkgName": "curl", + "PkgIdentifier": { + "PURL": "pkg:rpm/amazon/curl@7.61.1-9.amzn2.0.1?arch=x86_64\u0026distro=amazon-2+%28Karoo%29", + "UID": "dfda69b8a2968a7f" + }, + "InstalledVersion": "7.61.1-9.amzn2.0.1", + "FixedVersion": "7.61.1-11.amzn2.0.2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:72d97abdfae3b3c933ff41e39779cc72853d7bd9dc1e4800c5294d6715257799", + "DiffID": "sha256:f387c8b346c85cae37abd1f1a63015acb69f593dc425d0269f57d1012c3a81f6" + }, + "SeveritySource": "amazon", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5436", + "DataSource": { + "ID": "amazon", + "Name": "Amazon Linux Security Center", + "URL": "https://alas.aws.amazon.com/" + }, + "Fingerprint": "sha256:b489925a382f52a2f2e1a58e0e68b1c78ada14ccc2927003d2ac6de327d3aa31", + "Title": "curl: TFTP receive heap buffer overflow in tftp_receive_packet() function", + "Description": "A heap buffer overflow in the TFTP receiving code allows for DoS or arbitrary code execution in libcurl versions 7.19.4 through 7.64.1.", + "Severity": "LOW", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 1, + "arch-linux": 3, + "nvd": 3, + "oracle-oval": 2, + "photon": 3, + "redhat": 1, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00008.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00017.html", + "http://www.openwall.com/lists/oss-security/2019/09/11/6", + "https://access.redhat.com/security/cve/CVE-2019-5436", + "https://curl.haxx.se/docs/CVE-2019-5436.html", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5436", + "https://linux.oracle.com/cve/CVE-2019-5436.html", + "https://linux.oracle.com/errata/ELSA-2020-1792.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/SMG3V4VTX2SE3EW3HQTN3DDLQBTORQC2/", + "https://seclists.org/bugtraq/2020/Feb/36", + "https://security.gentoo.org/glsa/202003-29", + "https://security.netapp.com/advisory/ntap-20190606-0004/", + "https://support.f5.com/csp/article/K55133295", + "https://support.f5.com/csp/article/K55133295?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-3993-1", + "https://ubuntu.com/security/notices/USN-3993-2", + "https://www.debian.org/security/2020/dsa-4633", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + ], + "PublishedDate": "2019-05-28T19:29:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/amazonlinux2-gp2-x86-vm.json.golden b/integration/testdata/amazonlinux2-gp2-x86-vm.json.golden new file mode 100644 index 000000000000..6226441b6a59 --- /dev/null +++ b/integration/testdata/amazonlinux2-gp2-x86-vm.json.golden @@ -0,0 +1,82 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "disk.img", + "ArtifactType": "vm", + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "Metadata": { + "OS": { + "Family": "amazon", + "Name": "2 (Karoo)" + }, + "ImageConfig": { + "architecture": "", + "created": "0001-01-01T00:00:00Z", + "os": "", + "rootfs": { + "type": "", + "diff_ids": null + }, + "config": {} + } + }, + "Results": [ + { + "Target": "disk.img (amazon 2 (Karoo))", + "Class": "os-pkgs", + "Type": "amazon", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-38177", + "PkgID": "bind-export-libs@9.11.4-26.P2.amzn2.5.2.x86_64", + "PkgName": "bind-export-libs", + "PkgIdentifier": { + "PURL": "pkg:rpm/amazon/bind-export-libs@9.11.4-26.P2.amzn2.5.2?arch=x86_64\u0026distro=amazon-2+%28Karoo%29\u0026epoch=32" + }, + "InstalledVersion": "32:9.11.4-26.P2.amzn2.5.2", + "FixedVersion": "99:9.11.4-26.P2.amzn2.13", + "Status": "fixed", + "Layer": {}, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-38177", + "DataSource": { + "ID": "amazon", + "Name": "Amazon Linux Security Center", + "URL": "https://alas.aws.amazon.com/" + }, + "Title": "bind: memory leak in ECDSA DNSSEC verification code", + "Description": "By spoofing the target resolver with responses that have a malformed ECDSA signature, an attacker can trigger a small memory leak. It is possible to gradually erode available memory to the point where named crashes for lack of resources.", + "Severity": "MEDIUM", + "VendorSeverity": { + "arch-linux": 2, + "nvd": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N", + "V3Score": 7.5 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N", + "V3Score": 7.5 + } + }, + "References": [ + "http://www.openwall.com/lists/oss-security/2022/09/21/3", + "https://access.redhat.com/errata/RHSA-2022:6763", + "https://access.redhat.com/hydra/rest/securitydata/cve/CVE-2022-38177.json", + "https://access.redhat.com/hydra/rest/securitydata/cve/CVE-2022-38178.json", + "https://access.redhat.com/security/cve/CVE-2022-38177" + ], + "PublishedDate": "2022-09-21T11:15:00Z", + "LastModifiedDate": "2022-09-21T11:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/bun.json.golden b/integration/testdata/bun.json.golden new file mode 100644 index 000000000000..5735a42ef645 --- /dev/null +++ b/integration/testdata/bun.json.golden @@ -0,0 +1,188 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/bun", + "ArtifactType": "repository", + "Results": [ + { + "Target": "bun.lock", + "Class": "lang-pkgs", + "Type": "bun", + "Packages": [ + { + "ID": "jquery@3.3.1", + "Name": "jquery", + "Identifier": { + "PURL": "pkg:npm/jquery@3.3.1", + "UID": "4ee32c908d25947f" + }, + "Version": "3.3.1", + "Relationship": "direct", + "Locations": [ + { + "StartLine": 24, + "EndLine": 24 + } + ], + "AnalyzedBy": "bun" + }, + { + "ID": "typescript@5.8.3", + "Name": "typescript", + "Identifier": { + "PURL": "pkg:npm/typescript@5.8.3", + "UID": "57c0617aad078250" + }, + "Version": "5.8.3", + "Relationship": "direct", + "Locations": [ + { + "StartLine": 26, + "EndLine": 26 + } + ], + "AnalyzedBy": "bun" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-11358", + "PkgID": "jquery@3.3.1", + "PkgName": "jquery", + "PkgIdentifier": { + "PURL": "pkg:npm/jquery@3.3.1", + "UID": "4ee32c908d25947f" + }, + "InstalledVersion": "3.3.1", + "FixedVersion": "3.4.0", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-11358", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Fingerprint": "sha256:b7d2e6c655945265efa1caa0b5aaab246390ffc7e2f64938c9e0a6c21d69b6b6", + "Title": "jquery: Prototype pollution in object's prototype leading to denial of service, remote code execution, or property injection", + "Description": "jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-79" + ], + "VendorSeverity": { + "alma": 2, + "amazon": 2, + "arch-linux": 2, + "ghsa": 2, + "nodejs-security-wg": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ruby-advisory-db": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 4.3, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html", + "http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html", + "http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html", + "http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html", + "http://seclists.org/fulldisclosure/2019/May/10", + "http://seclists.org/fulldisclosure/2019/May/11", + "http://seclists.org/fulldisclosure/2019/May/13", + "http://www.openwall.com/lists/oss-security/2019/06/03/2", + "http://www.securityfocus.com/bid/108023", + "https://access.redhat.com/errata/RHBA-2019:1570", + "https://access.redhat.com/errata/RHSA-2019:1456", + "https://access.redhat.com/errata/RHSA-2019:2587", + "https://access.redhat.com/errata/RHSA-2019:3023", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://access.redhat.com/security/cve/CVE-2019-11358", + "https://backdropcms.org/security/backdrop-sa-core-2019-009", + "https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358", + "https://github.com/DanielRuf/snyk-js-jquery-174006?files=1", + "https://github.com/advisories/GHSA-6c3j-c64m-qhgq", + "https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b", + "https://github.com/jquery/jquery/pull/4333", + "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434", + "https://hackerone.com/reports/454365", + "https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601", + "https://linux.oracle.com/cve/CVE-2019-11358.html", + "https://linux.oracle.com/errata/ELSA-2020-4847.html", + "https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E", + "https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E", + "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", + "https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E", + "https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E", + "https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html", + "https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-11358", + "https://seclists.org/bugtraq/2019/Apr/32", + "https://seclists.org/bugtraq/2019/Jun/12", + "https://seclists.org/bugtraq/2019/May/18", + "https://security.netapp.com/advisory/ntap-20190919-0001/", + "https://snyk.io/vuln/SNYK-JS-JQUERY-174006", + "https://www.debian.org/security/2019/dsa-4434", + "https://www.debian.org/security/2019/dsa-4460", + "https://www.drupal.org/sa-core-2019-006", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/", + "https://www.synology.com/security/advisory/Synology_SA_19_19", + "https://www.tenable.com/security/tns-2019-08", + "https://www.tenable.com/security/tns-2020-02" + ], + "PublishedDate": "2019-04-20T00:29:00Z", + "LastModifiedDate": "2021-10-20T11:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/busybox-with-lockfile.json.golden b/integration/testdata/busybox-with-lockfile.json.golden new file mode 100644 index 000000000000..d3a1465d0bdd --- /dev/null +++ b/integration/testdata/busybox-with-lockfile.json.golden @@ -0,0 +1,176 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:4f807ebe9dfe3b25af3d4354d6cff8288e9a8c63477dabcde5e63998f0e68188", + "ArtifactName": "testdata/fixtures/images/busybox-with-lockfile.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 1491456, + "ImageID": "sha256:88702f6b6133bf06cc46af48437d0c0fc661239155548757c65916504a0e5eee", + "DiffIDs": [ + "sha256:797ac4999b67d8c38a596919efa5b7b6a4a8fd5814cb8564efa482c5d8403e6d", + "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:busybox-with-lockfile" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:busybox-with-lockfile", + "ImageConfig": { + "architecture": "amd64", + "created": "2022-06-07T04:24:40.230164Z", + "docker_version": "20.10.14", + "history": [ + { + "created": "2022-03-11T20:19:46.778911455Z", + "created_by": "/bin/sh -c #(nop) ADD file:39f6523fbc03f554a59461a34850d68c31cd5822e5a6fddf2d0ea198ed9a11c4 in / " + }, + { + "created": "2022-03-11T20:19:46.866228701Z", + "created_by": "/bin/sh -c #(nop) CMD [\"sh\"]", + "empty_layer": true + }, + { + "created": "2022-06-07T04:24:40.230164Z", + "created_by": "/bin/sh -c #(nop) COPY file:343df0159abcc51b06b4e56bfd4c06d2003b88947ed93b0cec6214ae5985669e in . " + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:797ac4999b67d8c38a596919efa5b7b6a4a8fd5814cb8564efa482c5d8403e6d", + "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" + ] + }, + "config": { + "Cmd": [ + "sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:2fb6fc2d97e10c79983aa10e013824cc7fc8bae50630e32159821197dda95fe3" + } + }, + "Layers": [ + { + "Size": 1459200, + "Digest": "sha256:554879bb300427c7301c1cbdf266a7eba24a85b10d19f270b3d348b9eb9ca7df", + "DiffID": "sha256:797ac4999b67d8c38a596919efa5b7b6a4a8fd5814cb8564efa482c5d8403e6d" + }, + { + "Size": 32256, + "Digest": "sha256:fd2e3bc9bccc9c677572a542d020998389de94f127ca2c252ae627fc7c241cee", + "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" + } + ] + }, + "Results": [ + { + "Target": "Cargo.lock", + "Class": "lang-pkgs", + "Type": "cargo", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-15542", + "PkgID": "ammonia@1.9.0", + "PkgName": "ammonia", + "PkgIdentifier": { + "PURL": "pkg:cargo/ammonia@1.9.0", + "UID": "9ec7999f7bb7437e" + }, + "InstalledVersion": "1.9.0", + "FixedVersion": "\u003e= 2.1.0", + "Status": "fixed", + "Layer": { + "Digest": "sha256:fd2e3bc9bccc9c677572a542d020998389de94f127ca2c252ae627fc7c241cee", + "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-15542", + "DataSource": { + "Name": "RustSec Advisory Database", + "URL": "https://github.com/RustSec/advisory-db" + }, + "Fingerprint": "sha256:bfae72d3823b3bfdea9a6be46c5615a187df54835c8d0884e4e228be9ccea9df", + "Title": "Uncontrolled recursion leads to abort in HTML serialization", + "Description": "An issue was discovered in the ammonia crate before 2.1.0 for Rust. There is uncontrolled recursion during HTML DOM tree serialization.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-674" + ], + "VendorSeverity": { + "nvd": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V2Score": 5, + "V3Score": 7.5 + } + }, + "References": [ + "https://crates.io/crates/ammonia", + "https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md#210", + "https://rustsec.org/advisories/RUSTSEC-2019-0001.html" + ], + "PublishedDate": "2019-08-26T18:15:00Z", + "LastModifiedDate": "2020-08-24T17:37:00Z" + }, + { + "VulnerabilityID": "CVE-2021-38193", + "PkgID": "ammonia@1.9.0", + "PkgName": "ammonia", + "PkgIdentifier": { + "PURL": "pkg:cargo/ammonia@1.9.0", + "UID": "9ec7999f7bb7437e" + }, + "InstalledVersion": "1.9.0", + "FixedVersion": "\u003e= 3.1.0, \u003e= 2.1.3, \u003c 3.0.0", + "Status": "fixed", + "Layer": { + "Digest": "sha256:fd2e3bc9bccc9c677572a542d020998389de94f127ca2c252ae627fc7c241cee", + "DiffID": "sha256:ea6f6933da66090da8bfe233d68f083792a68f944cd2d8f9fbb52da795813a4f" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-38193", + "DataSource": { + "Name": "RustSec Advisory Database", + "URL": "https://github.com/RustSec/advisory-db" + }, + "Fingerprint": "sha256:88b962523a98404a7807da75a3d208735778884a634ea21067683084a258c0bc", + "Title": "Incorrect handling of embedded SVG and MathML leads to mutation XSS", + "Description": "An issue was discovered in the ammonia crate before 3.1.0 for Rust. XSS can occur because the parsing differences for HTML, SVG, and MathML are mishandled, a similar issue to CVE-2020-26870.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-79" + ], + "VendorSeverity": { + "nvd": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 4.3, + "V3Score": 6.1 + } + }, + "References": [ + "https://crates.io/crates/ammonia", + "https://github.com/rust-ammonia/ammonia/pull/142", + "https://raw.githubusercontent.com/rustsec/advisory-db/main/crates/ammonia/RUSTSEC-2021-0074.md", + "https://rustsec.org/advisories/RUSTSEC-2021-0074.html" + ], + "PublishedDate": "2021-08-08T06:15:00Z", + "LastModifiedDate": "2021-08-16T16:37:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/cargo.lock.json.golden b/integration/testdata/cargo.lock.json.golden new file mode 100644 index 000000000000..b97f69ae7865 --- /dev/null +++ b/integration/testdata/cargo.lock.json.golden @@ -0,0 +1,157 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/cargo", + "ArtifactType": "repository", + "Results": [ + { + "Target": "Cargo.lock", + "Class": "lang-pkgs", + "Type": "cargo", + "Packages": [ + { + "ID": "app@0.1.0", + "Name": "app", + "Identifier": { + "PURL": "pkg:cargo/app@0.1.0", + "UID": "f8f61814a1b98c1f" + }, + "Version": "0.1.0", + "Relationship": "root", + "DependsOn": [ + "memchr@1.0.2", + "regex@1.7.3" + ], + "Locations": [ + { + "StartLine": 14, + "EndLine": 21 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "memchr@1.0.2", + "Name": "memchr", + "Identifier": { + "PURL": "pkg:cargo/memchr@1.0.2", + "UID": "536e148edad11c28" + }, + "Version": "1.0.2", + "Relationship": "direct", + "DependsOn": [ + "libc@0.2.171" + ], + "Locations": [ + { + "StartLine": 29, + "EndLine": 36 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "regex@1.7.3", + "Name": "regex", + "Identifier": { + "PURL": "pkg:cargo/regex@1.7.3", + "UID": "57a64064d2c70151" + }, + "Version": "1.7.3", + "Relationship": "direct", + "DependsOn": [ + "aho-corasick@0.7.20", + "memchr@2.7.4", + "regex-syntax@0.6.29" + ], + "Locations": [ + { + "StartLine": 44, + "EndLine": 53 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "aho-corasick@0.7.20", + "Name": "aho-corasick", + "Identifier": { + "PURL": "pkg:cargo/aho-corasick@0.7.20", + "UID": "4f314108cf35f82c" + }, + "Version": "0.7.20", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "memchr@2.7.4" + ], + "Locations": [ + { + "StartLine": 5, + "EndLine": 12 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "libc@0.2.171", + "Name": "libc", + "Identifier": { + "PURL": "pkg:cargo/libc@0.2.171", + "UID": "5e1e8e550c5392bb" + }, + "Version": "0.2.171", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 23, + "EndLine": 27 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "memchr@2.7.4", + "Name": "memchr", + "Identifier": { + "PURL": "pkg:cargo/memchr@2.7.4", + "UID": "fad175ee2c904d6a" + }, + "Version": "2.7.4", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 38, + "EndLine": 42 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "regex-syntax@0.6.29", + "Name": "regex-syntax", + "Identifier": { + "PURL": "pkg:cargo/regex-syntax@0.6.29", + "UID": "40c17f59020809c" + }, + "Version": "0.6.29", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 55, + "EndLine": 59 + } + ], + "AnalyzedBy": "cargo" + } + ] + } + ] +} diff --git a/integration/testdata/centos-6.json.golden b/integration/testdata/centos-6.json.golden new file mode 100644 index 000000000000..ebda263390ef --- /dev/null +++ b/integration/testdata/centos-6.json.golden @@ -0,0 +1,248 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:6fd360ff01eb63800aaafdb5e58af85fab9d7849344b577f5f6077cceb0399bb", + "ArtifactName": "testdata/fixtures/images/centos-6.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 201540608, + "OS": { + "Family": "centos", + "Name": "6.10", + "EOSL": true + }, + "ImageID": "sha256:5bf9684f472089d6d5cb636041d3d6dc748dbde39f1aefc374bbd367bd2aabbf", + "DiffIDs": [ + "sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:centos-6" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:centos-6", + "ImageConfig": { + "architecture": "amd64", + "author": "https://github.com/CentOS/sig-cloud-instance-images", + "container": "39f18700e994db8cbf9769e400afd93f3e9d0b00e06d44fe31f5e81513204d42", + "created": "2021-09-15T18:20:39.183628076Z", + "docker_version": "20.10.7", + "history": [ + { + "author": "https://github.com/CentOS/sig-cloud-instance-images", + "created": "2021-09-15T18:20:32.237976425Z", + "created_by": "/bin/sh -c #(nop) MAINTAINER https://github.com/CentOS/sig-cloud-instance-images", + "empty_layer": true + }, + { + "author": "https://github.com/CentOS/sig-cloud-instance-images", + "created": "2021-09-15T18:20:38.186052996Z", + "created_by": "/bin/sh -c #(nop) ADD file:0065316a41144e95bcb133567cc86816b8368a823cc067d741e06ded59849fd8 in / " + }, + { + "author": "https://github.com/CentOS/sig-cloud-instance-images", + "created": "2021-09-15T18:20:38.990977879Z", + "created_by": "/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20181006", + "empty_layer": true + }, + { + "author": "https://github.com/CentOS/sig-cloud-instance-images", + "created": "2021-09-15T18:20:39.183628076Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:60e456a7493aee808844ffd1e87bffc99b84bb095366bfd68b8843e0328f9e20", + "Labels": { + "org.label-schema.build-date": "20181006", + "org.label-schema.license": "GPLv2", + "org.label-schema.name": "CentOS Base Image", + "org.label-schema.schema-version": "1.0", + "org.label-schema.vendor": "CentOS" + } + } + }, + "Layers": [ + { + "Size": 201540608, + "Digest": "sha256:ff50d722b38227ec8f2bbf0cdbce428b66745077c173d8117d91376128fa532e", + "DiffID": "sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/centos-6.tar.gz (centos 6.10)", + "Class": "os-pkgs", + "Type": "centos", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-29573", + "PkgID": "glibc@2.12-1.212.el6.x86_64", + "PkgName": "glibc", + "PkgIdentifier": { + "PURL": "pkg:rpm/centos/glibc@2.12-1.212.el6?arch=x86_64\u0026distro=centos-6.10", + "UID": "236c38764cbd245a" + }, + "InstalledVersion": "2.12-1.212.el6", + "Status": "end_of_life", + "Layer": { + "Digest": "sha256:ff50d722b38227ec8f2bbf0cdbce428b66745077c173d8117d91376128fa532e", + "DiffID": "sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-29573", + "Fingerprint": "sha256:62a496897de8943df9f21f55ef389d695bc52f72b4f61228527e732c6aeecf32", + "Title": "glibc: stack-based buffer overflow if the input to any of the printf family of functions is an 80-bit long double with a non-canonical bit pattern", + "Description": "sysdeps/i386/ldbl2mpn.c in the GNU C Library (aka glibc or libc6) before 2.23 on x86 targets has a stack-based buffer overflow if the input to any of the printf family of functions is an 80-bit long double with a non-canonical bit pattern, as seen when passing a \\x00\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04 value to sprintf. NOTE: the issue does not affect glibc by default in 2016 or later (i.e., 2.23 or later) because of commits made in 2015 for inlining of C99 math functions through use of GCC built-ins. In other words, the reference to 2.23 is intentional despite the mention of \"Fixed for glibc 2.33\" in the 26649 reference.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 2, + "nvd": 3, + "oracle-oval": 2, + "photon": 3, + "redhat": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V2Score": 5, + "V3Score": 7.5 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V3Score": 7.5 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2020-29573", + "https://linux.oracle.com/cve/CVE-2020-29573.html", + "https://linux.oracle.com/errata/ELSA-2021-0348.html", + "https://security.gentoo.org/glsa/202101-20", + "https://security.netapp.com/advisory/ntap-20210122-0004/", + "https://sourceware.org/bugzilla/show_bug.cgi?id=26649", + "https://sourceware.org/pipermail/libc-alpha/2020-September/117779.html" + ], + "PublishedDate": "2020-12-06T00:15:00Z", + "LastModifiedDate": "2021-01-26T18:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1559", + "VendorIDs": [ + "RHSA-2019:2471" + ], + "PkgID": "openssl@1.0.1e-57.el6.x86_64", + "PkgName": "openssl", + "PkgIdentifier": { + "PURL": "pkg:rpm/centos/openssl@1.0.1e-57.el6?arch=x86_64\u0026distro=centos-6.10", + "UID": "5eb1cc9177164e7a" + }, + "InstalledVersion": "1.0.1e-57.el6", + "FixedVersion": "1.0.1e-58.el6_10", + "Status": "fixed", + "Layer": { + "Digest": "sha256:ff50d722b38227ec8f2bbf0cdbce428b66745077c173d8117d91376128fa532e", + "DiffID": "sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1559", + "Fingerprint": "sha256:b78bc648b47d435c3e7a2edeb44addc0e2919d0607b86c4348291c6fc5eceb4e", + "Title": "openssl: 0-byte record padding oracle", + "Description": "If an application encounters a fatal protocol error and then calls SSL_shutdown() twice (once to send a close_notify, and once to receive one) then OpenSSL can respond differently to the calling application if a 0 byte record is received with invalid padding compared to if a 0 byte record is received with an invalid MAC. If the application then behaves differently based on that in a way that is detectable to the remote peer, then this amounts to a padding oracle that could be used to decrypt data. In order for this to be exploitable \"non-stitched\" ciphersuites must be in use. Stitched ciphersuites are optimised implementations of certain commonly used ciphersuites. Also the application must call SSL_shutdown() twice even if a protocol error has occurred (applications should not do this but some do anyway). Fixed in OpenSSL 1.0.2r (Affected 1.0.2-1.0.2q).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-203" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 4.3, + "V3Score": 5.9 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 5.9 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-03/msg00041.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00019.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00046.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00047.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00049.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00080.html", + "http://www.securityfocus.com/bid/107174", + "https://access.redhat.com/errata/RHSA-2019:2304", + "https://access.redhat.com/errata/RHSA-2019:2437", + "https://access.redhat.com/errata/RHSA-2019:2439", + "https://access.redhat.com/errata/RHSA-2019:2471", + "https://access.redhat.com/errata/RHSA-2019:3929", + "https://access.redhat.com/errata/RHSA-2019:3931", + "https://access.redhat.com/security/cve/CVE-2019-1559", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1559", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e9bbefbf0f24c57645e7ad6a5a71ae649d18ac8e", + "https://github.com/RUB-NDS/TLS-Padding-Oracles", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10282", + "https://linux.oracle.com/cve/CVE-2019-1559.html", + "https://linux.oracle.com/errata/ELSA-2019-2471.html", + "https://lists.debian.org/debian-lts-announce/2019/03/msg00003.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", + "https://security.gentoo.org/glsa/201903-10", + "https://security.netapp.com/advisory/ntap-20190301-0001/", + "https://security.netapp.com/advisory/ntap-20190301-0002/", + "https://security.netapp.com/advisory/ntap-20190423-0002/", + "https://support.f5.com/csp/article/K18549143", + "https://support.f5.com/csp/article/K18549143?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-3899-1", + "https://ubuntu.com/security/notices/USN-4376-2", + "https://usn.ubuntu.com/3899-1/", + "https://usn.ubuntu.com/4376-2/", + "https://www.debian.org/security/2019/dsa-4400", + "https://www.openssl.org/news/secadv/20190226.txt", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.tenable.com/security/tns-2019-02", + "https://www.tenable.com/security/tns-2019-03" + ], + "PublishedDate": "2019-02-27T23:29:00Z", + "LastModifiedDate": "2021-01-20T15:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/centos-7-ignore-unfixed.json.golden b/integration/testdata/centos-7-ignore-unfixed.json.golden new file mode 100644 index 000000000000..4a6a05ec3f5d --- /dev/null +++ b/integration/testdata/centos-7-ignore-unfixed.json.golden @@ -0,0 +1,272 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:8af996fb4a61e515887a173fdea3d5111c90c76e9f8247b3f668b17ab8215946", + "ArtifactName": "testdata/fixtures/images/centos-7.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 209451008, + "OS": { + "Family": "centos", + "Name": "7.6.1810" + }, + "ImageID": "sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427", + "DiffIDs": [ + "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:centos-7" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:centos-7", + "ImageConfig": { + "architecture": "amd64", + "container": "cc6043a787f6d1c7ae3e121ebdf1c4478186336aa7274871780a0a7bcc3a061a", + "created": "2019-03-14T21:20:29.635970966Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-03-14T21:20:28.997703205Z", + "created_by": "/bin/sh -c #(nop) ADD file:54b004357379717dfb7ea6f024ca80ce762ea4b06647fcddc0f6697146551172 in / " + }, + { + "created": "2019-03-14T21:20:29.452720615Z", + "created_by": "/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20181204", + "empty_layer": true + }, + { + "created": "2019-03-14T21:20:29.635970966Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:698a0848ee35389ab7b98494bdc60f887c54ddb94fc2326a1fb4eff8895aff43", + "Labels": { + "org.label-schema.build-date": "20181204", + "org.label-schema.license": "GPLv2", + "org.label-schema.name": "CentOS Base Image", + "org.label-schema.schema-version": "1.0", + "org.label-schema.vendor": "CentOS" + }, + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 209451008, + "Digest": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "DiffID": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/centos-7.tar.gz (centos 7.6.1810)", + "Class": "os-pkgs", + "Type": "centos", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-1559", + "VendorIDs": [ + "RHSA-2019:2304" + ], + "PkgID": "openssl-libs@1.0.2k-16.el7.x86_64", + "PkgName": "openssl-libs", + "PkgIdentifier": { + "PURL": "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810\u0026epoch=1", + "UID": "38b1fc09c4285fe1" + }, + "InstalledVersion": "1:1.0.2k-16.el7", + "FixedVersion": "1:1.0.2k-19.el7", + "Status": "fixed", + "Layer": { + "Digest": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "DiffID": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1559", + "Fingerprint": "sha256:638871deb7cddea2771313ab3cff2b012e60f6c4346567d35bd9ceee1d9226c3", + "Title": "openssl: 0-byte record padding oracle", + "Description": "If an application encounters a fatal protocol error and then calls SSL_shutdown() twice (once to send a close_notify, and once to receive one) then OpenSSL can respond differently to the calling application if a 0 byte record is received with invalid padding compared to if a 0 byte record is received with an invalid MAC. If the application then behaves differently based on that in a way that is detectable to the remote peer, then this amounts to a padding oracle that could be used to decrypt data. In order for this to be exploitable \"non-stitched\" ciphersuites must be in use. Stitched ciphersuites are optimised implementations of certain commonly used ciphersuites. Also the application must call SSL_shutdown() twice even if a protocol error has occurred (applications should not do this but some do anyway). Fixed in OpenSSL 1.0.2r (Affected 1.0.2-1.0.2q).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-203" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 4.3, + "V3Score": 5.9 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 5.9 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-03/msg00041.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00019.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00046.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00047.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00049.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00080.html", + "http://www.securityfocus.com/bid/107174", + "https://access.redhat.com/errata/RHSA-2019:2304", + "https://access.redhat.com/errata/RHSA-2019:2437", + "https://access.redhat.com/errata/RHSA-2019:2439", + "https://access.redhat.com/errata/RHSA-2019:2471", + "https://access.redhat.com/errata/RHSA-2019:3929", + "https://access.redhat.com/errata/RHSA-2019:3931", + "https://access.redhat.com/security/cve/CVE-2019-1559", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1559", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e9bbefbf0f24c57645e7ad6a5a71ae649d18ac8e", + "https://github.com/RUB-NDS/TLS-Padding-Oracles", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10282", + "https://linux.oracle.com/cve/CVE-2019-1559.html", + "https://linux.oracle.com/errata/ELSA-2019-2471.html", + "https://lists.debian.org/debian-lts-announce/2019/03/msg00003.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", + "https://security.gentoo.org/glsa/201903-10", + "https://security.netapp.com/advisory/ntap-20190301-0001/", + "https://security.netapp.com/advisory/ntap-20190301-0002/", + "https://security.netapp.com/advisory/ntap-20190423-0002/", + "https://support.f5.com/csp/article/K18549143", + "https://support.f5.com/csp/article/K18549143?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-3899-1", + "https://ubuntu.com/security/notices/USN-4376-2", + "https://usn.ubuntu.com/3899-1/", + "https://usn.ubuntu.com/4376-2/", + "https://www.debian.org/security/2019/dsa-4400", + "https://www.openssl.org/news/secadv/20190226.txt", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.tenable.com/security/tns-2019-02", + "https://www.tenable.com/security/tns-2019-03" + ], + "PublishedDate": "2019-02-27T23:29:00Z", + "LastModifiedDate": "2021-01-20T15:15:00Z" + }, + { + "VulnerabilityID": "CVE-2018-0734", + "VendorIDs": [ + "RHSA-2019:2304" + ], + "PkgID": "openssl-libs@1.0.2k-16.el7.x86_64", + "PkgName": "openssl-libs", + "PkgIdentifier": { + "PURL": "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810\u0026epoch=1", + "UID": "38b1fc09c4285fe1" + }, + "InstalledVersion": "1:1.0.2k-16.el7", + "FixedVersion": "1:1.0.2k-19.el7", + "Status": "fixed", + "Layer": { + "Digest": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "DiffID": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2018-0734", + "Fingerprint": "sha256:73f7b1753e4dcaa7edff2d6f81e8373c85c2be4c1e1c5b04e8f56d1811c0aa1c", + "Title": "openssl: timing side channel attack in the DSA signature algorithm", + "Description": "The OpenSSL DSA signature algorithm has been shown to be vulnerable to a timing side channel attack. An attacker could use variations in the signing algorithm to recover the private key. Fixed in OpenSSL 1.1.1a (Affected 1.1.1). Fixed in OpenSSL 1.1.0j (Affected 1.1.0-1.1.0i). Fixed in OpenSSL 1.0.2q (Affected 1.0.2-1.0.2p).", + "Severity": "LOW", + "CweIDs": [ + "CWE-327" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 1, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 4.3, + "V3Score": 5.9 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 5.1 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00030.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-07/msg00056.html", + "http://www.securityfocus.com/bid/105758", + "https://access.redhat.com/errata/RHSA-2019:2304", + "https://access.redhat.com/errata/RHSA-2019:3700", + "https://access.redhat.com/errata/RHSA-2019:3932", + "https://access.redhat.com/errata/RHSA-2019:3933", + "https://access.redhat.com/errata/RHSA-2019:3935", + "https://access.redhat.com/security/cve/CVE-2018-0734", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0734", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=43e6a58d4991a451daf4891ff05a48735df871ac", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=8abfe72e8c1de1b95f50aa0d9134803b4d00070f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=ef11e19d1365eea2b1851e6f540a0bf365d303e7", + "https://linux.oracle.com/cve/CVE-2018-0734.html", + "https://linux.oracle.com/errata/ELSA-2019-3700.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", + "https://nodejs.org/en/blog/vulnerability/november-2018-security-releases/", + "https://nvd.nist.gov/vuln/detail/CVE-2018-0734", + "https://security.netapp.com/advisory/ntap-20181105-0002/", + "https://security.netapp.com/advisory/ntap-20190118-0002/", + "https://security.netapp.com/advisory/ntap-20190423-0002/", + "https://ubuntu.com/security/notices/USN-3840-1", + "https://usn.ubuntu.com/3840-1/", + "https://www.debian.org/security/2018/dsa-4348", + "https://www.debian.org/security/2018/dsa-4355", + "https://www.openssl.org/news/secadv/20181030.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", + "https://www.oracle.com/technetwork/security-advisory/cpujan2019-5072801.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.tenable.com/security/tns-2018-16", + "https://www.tenable.com/security/tns-2018-17" + ], + "PublishedDate": "2018-10-30T12:29:00Z", + "LastModifiedDate": "2020-08-24T17:37:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/centos-7-medium.json.golden b/integration/testdata/centos-7-medium.json.golden new file mode 100644 index 000000000000..01bb09573fc8 --- /dev/null +++ b/integration/testdata/centos-7-medium.json.golden @@ -0,0 +1,183 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:8af996fb4a61e515887a173fdea3d5111c90c76e9f8247b3f668b17ab8215946", + "ArtifactName": "testdata/fixtures/images/centos-7.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 209451008, + "OS": { + "Family": "centos", + "Name": "7.6.1810" + }, + "ImageID": "sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427", + "DiffIDs": [ + "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:centos-7" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:centos-7", + "ImageConfig": { + "architecture": "amd64", + "container": "cc6043a787f6d1c7ae3e121ebdf1c4478186336aa7274871780a0a7bcc3a061a", + "created": "2019-03-14T21:20:29.635970966Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-03-14T21:20:28.997703205Z", + "created_by": "/bin/sh -c #(nop) ADD file:54b004357379717dfb7ea6f024ca80ce762ea4b06647fcddc0f6697146551172 in / " + }, + { + "created": "2019-03-14T21:20:29.452720615Z", + "created_by": "/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20181204", + "empty_layer": true + }, + { + "created": "2019-03-14T21:20:29.635970966Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:698a0848ee35389ab7b98494bdc60f887c54ddb94fc2326a1fb4eff8895aff43", + "Labels": { + "org.label-schema.build-date": "20181204", + "org.label-schema.license": "GPLv2", + "org.label-schema.name": "CentOS Base Image", + "org.label-schema.schema-version": "1.0", + "org.label-schema.vendor": "CentOS" + }, + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 209451008, + "Digest": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "DiffID": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/centos-7.tar.gz (centos 7.6.1810)", + "Class": "os-pkgs", + "Type": "centos", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-1559", + "VendorIDs": [ + "RHSA-2019:2304" + ], + "PkgID": "openssl-libs@1.0.2k-16.el7.x86_64", + "PkgName": "openssl-libs", + "PkgIdentifier": { + "PURL": "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810\u0026epoch=1", + "UID": "38b1fc09c4285fe1" + }, + "InstalledVersion": "1:1.0.2k-16.el7", + "FixedVersion": "1:1.0.2k-19.el7", + "Status": "fixed", + "Layer": { + "Digest": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "DiffID": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1559", + "Fingerprint": "sha256:638871deb7cddea2771313ab3cff2b012e60f6c4346567d35bd9ceee1d9226c3", + "Title": "openssl: 0-byte record padding oracle", + "Description": "If an application encounters a fatal protocol error and then calls SSL_shutdown() twice (once to send a close_notify, and once to receive one) then OpenSSL can respond differently to the calling application if a 0 byte record is received with invalid padding compared to if a 0 byte record is received with an invalid MAC. If the application then behaves differently based on that in a way that is detectable to the remote peer, then this amounts to a padding oracle that could be used to decrypt data. In order for this to be exploitable \"non-stitched\" ciphersuites must be in use. Stitched ciphersuites are optimised implementations of certain commonly used ciphersuites. Also the application must call SSL_shutdown() twice even if a protocol error has occurred (applications should not do this but some do anyway). Fixed in OpenSSL 1.0.2r (Affected 1.0.2-1.0.2q).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-203" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 4.3, + "V3Score": 5.9 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 5.9 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-03/msg00041.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00019.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00046.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00047.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00049.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00080.html", + "http://www.securityfocus.com/bid/107174", + "https://access.redhat.com/errata/RHSA-2019:2304", + "https://access.redhat.com/errata/RHSA-2019:2437", + "https://access.redhat.com/errata/RHSA-2019:2439", + "https://access.redhat.com/errata/RHSA-2019:2471", + "https://access.redhat.com/errata/RHSA-2019:3929", + "https://access.redhat.com/errata/RHSA-2019:3931", + "https://access.redhat.com/security/cve/CVE-2019-1559", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1559", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e9bbefbf0f24c57645e7ad6a5a71ae649d18ac8e", + "https://github.com/RUB-NDS/TLS-Padding-Oracles", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10282", + "https://linux.oracle.com/cve/CVE-2019-1559.html", + "https://linux.oracle.com/errata/ELSA-2019-2471.html", + "https://lists.debian.org/debian-lts-announce/2019/03/msg00003.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", + "https://security.gentoo.org/glsa/201903-10", + "https://security.netapp.com/advisory/ntap-20190301-0001/", + "https://security.netapp.com/advisory/ntap-20190301-0002/", + "https://security.netapp.com/advisory/ntap-20190423-0002/", + "https://support.f5.com/csp/article/K18549143", + "https://support.f5.com/csp/article/K18549143?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-3899-1", + "https://ubuntu.com/security/notices/USN-4376-2", + "https://usn.ubuntu.com/3899-1/", + "https://usn.ubuntu.com/4376-2/", + "https://www.debian.org/security/2019/dsa-4400", + "https://www.openssl.org/news/secadv/20190226.txt", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.tenable.com/security/tns-2019-02", + "https://www.tenable.com/security/tns-2019-03" + ], + "PublishedDate": "2019-02-27T23:29:00Z", + "LastModifiedDate": "2021-01-20T15:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/centos-7.json.golden b/integration/testdata/centos-7.json.golden new file mode 100644 index 000000000000..5f4a9a07ef25 --- /dev/null +++ b/integration/testdata/centos-7.json.golden @@ -0,0 +1,331 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:8af996fb4a61e515887a173fdea3d5111c90c76e9f8247b3f668b17ab8215946", + "ArtifactName": "testdata/fixtures/images/centos-7.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 209451008, + "OS": { + "Family": "centos", + "Name": "7.6.1810" + }, + "ImageID": "sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427", + "DiffIDs": [ + "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:centos-7" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:centos-7", + "ImageConfig": { + "architecture": "amd64", + "container": "cc6043a787f6d1c7ae3e121ebdf1c4478186336aa7274871780a0a7bcc3a061a", + "created": "2019-03-14T21:20:29.635970966Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-03-14T21:20:28.997703205Z", + "created_by": "/bin/sh -c #(nop) ADD file:54b004357379717dfb7ea6f024ca80ce762ea4b06647fcddc0f6697146551172 in / " + }, + { + "created": "2019-03-14T21:20:29.452720615Z", + "created_by": "/bin/sh -c #(nop) LABEL org.label-schema.schema-version=1.0 org.label-schema.name=CentOS Base Image org.label-schema.vendor=CentOS org.label-schema.license=GPLv2 org.label-schema.build-date=20181204", + "empty_layer": true + }, + { + "created": "2019-03-14T21:20:29.635970966Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:698a0848ee35389ab7b98494bdc60f887c54ddb94fc2326a1fb4eff8895aff43", + "Labels": { + "org.label-schema.build-date": "20181204", + "org.label-schema.license": "GPLv2", + "org.label-schema.name": "CentOS Base Image", + "org.label-schema.schema-version": "1.0", + "org.label-schema.vendor": "CentOS" + }, + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 209451008, + "Digest": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "DiffID": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/centos-7.tar.gz (centos 7.6.1810)", + "Class": "os-pkgs", + "Type": "centos", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18276", + "PkgID": "bash@4.2.46-31.el7.x86_64", + "PkgName": "bash", + "PkgIdentifier": { + "PURL": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "UID": "83a567dd95e4bc19" + }, + "InstalledVersion": "4.2.46-31.el7", + "Status": "will_not_fix", + "Layer": { + "Digest": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "DiffID": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", + "Fingerprint": "sha256:df06395185e8c9e2e8c36ae96ab18505e0b22b76734cae6322207b8aebe57b7d", + "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", + "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "Severity": "LOW", + "CweIDs": [ + "CWE-273" + ], + "VendorSeverity": { + "cbl-mariner": 3, + "nvd": 3, + "oracle-oval": 1, + "photon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.2, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + } + }, + "References": [ + "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", + "https://access.redhat.com/security/cve/CVE-2019-18276", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", + "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", + "https://linux.oracle.com/cve/CVE-2019-18276.html", + "https://linux.oracle.com/errata/ELSA-2021-1679.html", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", + "https://security.gentoo.org/glsa/202105-34", + "https://security.netapp.com/advisory/ntap-20200430-0003/", + "https://www.youtube.com/watch?v=-wGtxJ8opa8" + ], + "PublishedDate": "2019-11-28T01:15:00Z", + "LastModifiedDate": "2021-05-26T12:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1559", + "VendorIDs": [ + "RHSA-2019:2304" + ], + "PkgID": "openssl-libs@1.0.2k-16.el7.x86_64", + "PkgName": "openssl-libs", + "PkgIdentifier": { + "PURL": "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810\u0026epoch=1", + "UID": "38b1fc09c4285fe1" + }, + "InstalledVersion": "1:1.0.2k-16.el7", + "FixedVersion": "1:1.0.2k-19.el7", + "Status": "fixed", + "Layer": { + "Digest": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "DiffID": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1559", + "Fingerprint": "sha256:638871deb7cddea2771313ab3cff2b012e60f6c4346567d35bd9ceee1d9226c3", + "Title": "openssl: 0-byte record padding oracle", + "Description": "If an application encounters a fatal protocol error and then calls SSL_shutdown() twice (once to send a close_notify, and once to receive one) then OpenSSL can respond differently to the calling application if a 0 byte record is received with invalid padding compared to if a 0 byte record is received with an invalid MAC. If the application then behaves differently based on that in a way that is detectable to the remote peer, then this amounts to a padding oracle that could be used to decrypt data. In order for this to be exploitable \"non-stitched\" ciphersuites must be in use. Stitched ciphersuites are optimised implementations of certain commonly used ciphersuites. Also the application must call SSL_shutdown() twice even if a protocol error has occurred (applications should not do this but some do anyway). Fixed in OpenSSL 1.0.2r (Affected 1.0.2-1.0.2q).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-203" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 4.3, + "V3Score": 5.9 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 5.9 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-03/msg00041.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00019.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00046.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00047.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00049.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00080.html", + "http://www.securityfocus.com/bid/107174", + "https://access.redhat.com/errata/RHSA-2019:2304", + "https://access.redhat.com/errata/RHSA-2019:2437", + "https://access.redhat.com/errata/RHSA-2019:2439", + "https://access.redhat.com/errata/RHSA-2019:2471", + "https://access.redhat.com/errata/RHSA-2019:3929", + "https://access.redhat.com/errata/RHSA-2019:3931", + "https://access.redhat.com/security/cve/CVE-2019-1559", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1559", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e9bbefbf0f24c57645e7ad6a5a71ae649d18ac8e", + "https://github.com/RUB-NDS/TLS-Padding-Oracles", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10282", + "https://linux.oracle.com/cve/CVE-2019-1559.html", + "https://linux.oracle.com/errata/ELSA-2019-2471.html", + "https://lists.debian.org/debian-lts-announce/2019/03/msg00003.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", + "https://security.gentoo.org/glsa/201903-10", + "https://security.netapp.com/advisory/ntap-20190301-0001/", + "https://security.netapp.com/advisory/ntap-20190301-0002/", + "https://security.netapp.com/advisory/ntap-20190423-0002/", + "https://support.f5.com/csp/article/K18549143", + "https://support.f5.com/csp/article/K18549143?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-3899-1", + "https://ubuntu.com/security/notices/USN-4376-2", + "https://usn.ubuntu.com/3899-1/", + "https://usn.ubuntu.com/4376-2/", + "https://www.debian.org/security/2019/dsa-4400", + "https://www.openssl.org/news/secadv/20190226.txt", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.tenable.com/security/tns-2019-02", + "https://www.tenable.com/security/tns-2019-03" + ], + "PublishedDate": "2019-02-27T23:29:00Z", + "LastModifiedDate": "2021-01-20T15:15:00Z" + }, + { + "VulnerabilityID": "CVE-2018-0734", + "VendorIDs": [ + "RHSA-2019:2304" + ], + "PkgID": "openssl-libs@1.0.2k-16.el7.x86_64", + "PkgName": "openssl-libs", + "PkgIdentifier": { + "PURL": "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64\u0026distro=centos-7.6.1810\u0026epoch=1", + "UID": "38b1fc09c4285fe1" + }, + "InstalledVersion": "1:1.0.2k-16.el7", + "FixedVersion": "1:1.0.2k-19.el7", + "Status": "fixed", + "Layer": { + "Digest": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "DiffID": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2018-0734", + "Fingerprint": "sha256:73f7b1753e4dcaa7edff2d6f81e8373c85c2be4c1e1c5b04e8f56d1811c0aa1c", + "Title": "openssl: timing side channel attack in the DSA signature algorithm", + "Description": "The OpenSSL DSA signature algorithm has been shown to be vulnerable to a timing side channel attack. An attacker could use variations in the signing algorithm to recover the private key. Fixed in OpenSSL 1.1.1a (Affected 1.1.1). Fixed in OpenSSL 1.1.0j (Affected 1.1.0-1.1.0i). Fixed in OpenSSL 1.0.2q (Affected 1.0.2-1.0.2p).", + "Severity": "LOW", + "CweIDs": [ + "CWE-327" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 1, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 4.3, + "V3Score": 5.9 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 5.1 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00030.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-07/msg00056.html", + "http://www.securityfocus.com/bid/105758", + "https://access.redhat.com/errata/RHSA-2019:2304", + "https://access.redhat.com/errata/RHSA-2019:3700", + "https://access.redhat.com/errata/RHSA-2019:3932", + "https://access.redhat.com/errata/RHSA-2019:3933", + "https://access.redhat.com/errata/RHSA-2019:3935", + "https://access.redhat.com/security/cve/CVE-2018-0734", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0734", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=43e6a58d4991a451daf4891ff05a48735df871ac", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=8abfe72e8c1de1b95f50aa0d9134803b4d00070f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=ef11e19d1365eea2b1851e6f540a0bf365d303e7", + "https://linux.oracle.com/cve/CVE-2018-0734.html", + "https://linux.oracle.com/errata/ELSA-2019-3700.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/", + "https://nodejs.org/en/blog/vulnerability/november-2018-security-releases/", + "https://nvd.nist.gov/vuln/detail/CVE-2018-0734", + "https://security.netapp.com/advisory/ntap-20181105-0002/", + "https://security.netapp.com/advisory/ntap-20190118-0002/", + "https://security.netapp.com/advisory/ntap-20190423-0002/", + "https://ubuntu.com/security/notices/USN-3840-1", + "https://usn.ubuntu.com/3840-1/", + "https://www.debian.org/security/2018/dsa-4348", + "https://www.debian.org/security/2018/dsa-4355", + "https://www.openssl.org/news/secadv/20181030.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", + "https://www.oracle.com/technetwork/security-advisory/cpujan2019-5072801.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.tenable.com/security/tns-2018-16", + "https://www.tenable.com/security/tns-2018-17" + ], + "PublishedDate": "2018-10-30T12:29:00Z", + "LastModifiedDate": "2020-08-24T17:37:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/cocoapods.json.golden b/integration/testdata/cocoapods.json.golden new file mode 100644 index 000000000000..7a570cbac1fa --- /dev/null +++ b/integration/testdata/cocoapods.json.golden @@ -0,0 +1,65 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/cocoapods", + "ArtifactType": "repository", + "Results": [ + { + "Target": "Podfile.lock", + "Class": "lang-pkgs", + "Type": "cocoapods", + "Packages": [ + { + "ID": "_NIODataStructures@2.41.0", + "Name": "_NIODataStructures", + "Identifier": { + "PURL": "pkg:cocoapods/_NIODataStructures@2.41.0", + "UID": "2292b92e4e573949" + }, + "Version": "2.41.0", + "AnalyzedBy": "cocoapods" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-3215", + "PkgID": "_NIODataStructures@2.41.0", + "PkgName": "_NIODataStructures", + "PkgIdentifier": { + "PURL": "pkg:cocoapods/_NIODataStructures@2.41.0", + "UID": "2292b92e4e573949" + }, + "InstalledVersion": "2.41.0", + "FixedVersion": "2.29.1, 2.39.1, 2.42.0", + "Status": "fixed", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-3215", + "Fingerprint": "sha256:21aa7836213a6a8eb3937e503b3b0cba4c68648574a148349574f22c1fdd9c0a", + "Title": "SwiftNIO vulnerable to Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting')", + "Description": "`NIOHTTP1` and projects using it for generating HTTP responses, including SwiftNIO, can be subject to a HTTP Response Injection attack...", + "Severity": "MEDIUM", + "VendorSeverity": { + "ghsa": 2 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N", + "V3Score": 5.3 + } + }, + "References": [ + "https://github.com/apple/swift-nio/security/advisories/GHSA-7fj7-39wj-c64f", + "https://nvd.nist.gov/vuln/detail/CVE-2022-3215", + "https://github.com/apple/swift-nio/commit/a16e2f54a25b2af217044e5168997009a505930f", + "https://github.com/advisories/GHSA-7fj7-39wj-c64f" + ], + "PublishedDate": "2023-06-07T16:01:53Z", + "LastModifiedDate": "2023-06-19T16:45:07Z" + } + ] + } + ] +} diff --git a/integration/testdata/composer.lock.json.golden b/integration/testdata/composer.lock.json.golden new file mode 100644 index 000000000000..b78933e88a88 --- /dev/null +++ b/integration/testdata/composer.lock.json.golden @@ -0,0 +1,106 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/composer", + "ArtifactType": "repository", + "Results": [ + { + "Target": "composer.lock", + "Class": "lang-pkgs", + "Type": "composer", + "Packages": [ + { + "ID": "guzzlehttp/guzzle@7.4.4", + "Name": "guzzlehttp/guzzle", + "Identifier": { + "PURL": "pkg:composer/guzzlehttp/guzzle@7.4.4", + "UID": "bc13d175ea3245db" + }, + "Version": "7.4.4", + "Licenses": [ + "MIT" + ], + "Relationship": "direct", + "DependsOn": [ + "guzzlehttp/psr7@1.8.3" + ], + "Locations": [ + { + "StartLine": 9, + "EndLine": 129 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "guzzlehttp/psr7@1.8.3", + "Name": "guzzlehttp/psr7", + "Identifier": { + "PURL": "pkg:composer/guzzlehttp/psr7@1.8.3", + "UID": "57beb2412ee42604" + }, + "Version": "1.8.3", + "Licenses": [ + "MIT" + ], + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 130, + "EndLine": 245 + } + ], + "AnalyzedBy": "composer" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-24775", + "PkgID": "guzzlehttp/psr7@1.8.3", + "PkgName": "guzzlehttp/psr7", + "PkgIdentifier": { + "PURL": "pkg:composer/guzzlehttp/psr7@1.8.3", + "UID": "57beb2412ee42604" + }, + "InstalledVersion": "1.8.3", + "FixedVersion": "1.8.4", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-24775", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Composer", + "URL": "https://github.com/advisories?query=type%%3Areviewed+ecosystem%%3Acomposer" + }, + "Fingerprint": "sha256:59ce4eb8bdfe3bbdf3187ec7d5fd51beb0ce398c8377a3be0d811b002df8ec0b", + "Title": "Improper Input Validation in guzzlehttp/psr7", + "Description": "### Impact\nIn proper header parsing. An attacker could sneak in a new line character and pass untrusted values. \n\n### Patches\nThe issue is patched in 1.8.4 and 2.1.1.\n\n### Workarounds\nThere are no known workarounds.\n", + "Severity": "HIGH", + "CweIDs": [ + "CWE-20" + ], + "VendorSeverity": { + "ghsa": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N", + "V3Score": 7.5 + } + }, + "References": [ + "https://github.com/guzzle/psr7/security/advisories/GHSA-q7rv-6hp3-vh96", + "https://nvd.nist.gov/vuln/detail/CVE-2022-24775" + ], + "PublishedDate": "2022-03-25T19:26:33Z", + "LastModifiedDate": "2022-06-14T20:02:29Z" + } + ] + } + ] +} diff --git a/integration/testdata/composer.vendor.json.golden b/integration/testdata/composer.vendor.json.golden new file mode 100644 index 000000000000..b3e33238b4e3 --- /dev/null +++ b/integration/testdata/composer.vendor.json.golden @@ -0,0 +1,123 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/composer-vendor", + "ArtifactType": "filesystem", + "Results": [ + { + "Target": "installed.json", + "Class": "lang-pkgs", + "Type": "composer-vendor", + "Packages": [ + { + "ID": "guzzlehttp/psr7@1.8.3", + "Name": "guzzlehttp/psr7", + "Identifier": { + "PURL": "pkg:composer/guzzlehttp/psr7@1.8.3", + "UID": "a1f3a04d90429be3" + }, + "Version": "1.8.3", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "psr/http-message@1.1", + "ralouphie/getallheaders@3.0.3" + ], + "Locations": [ + { + "StartLine": 3, + "EndLine": 115 + } + ], + "AnalyzedBy": "composer-vendor" + }, + { + "ID": "psr/http-message@1.1", + "Name": "psr/http-message", + "Identifier": { + "PURL": "pkg:composer/psr/http-message@1.1", + "UID": "59ee860afeff08fa" + }, + "Version": "1.1", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 116, + "EndLine": 171 + } + ], + "AnalyzedBy": "composer-vendor" + }, + { + "ID": "ralouphie/getallheaders@3.0.3", + "Name": "ralouphie/getallheaders", + "Identifier": { + "PURL": "pkg:composer/ralouphie/getallheaders@3.0.3", + "UID": "94049bcc6a2f542c" + }, + "Version": "3.0.3", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 172, + "EndLine": 218 + } + ], + "AnalyzedBy": "composer-vendor" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-24775", + "PkgID": "guzzlehttp/psr7@1.8.3", + "PkgName": "guzzlehttp/psr7", + "PkgIdentifier": { + "PURL": "pkg:composer/guzzlehttp/psr7@1.8.3", + "UID": "a1f3a04d90429be3" + }, + "InstalledVersion": "1.8.3", + "FixedVersion": "1.8.4", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-24775", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Composer", + "URL": "https://github.com/advisories?query=type%%3Areviewed+ecosystem%%3Acomposer" + }, + "Fingerprint": "sha256:b6d44694486405fbbfd232d7722681bc06d79a81a79a2da11b5bc01364aa84fb", + "Title": "Improper Input Validation in guzzlehttp/psr7", + "Description": "### Impact\nIn proper header parsing. An attacker could sneak in a new line character and pass untrusted values. \n\n### Patches\nThe issue is patched in 1.8.4 and 2.1.1.\n\n### Workarounds\nThere are no known workarounds.\n", + "Severity": "HIGH", + "CweIDs": [ + "CWE-20" + ], + "VendorSeverity": { + "ghsa": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N", + "V3Score": 7.5 + } + }, + "References": [ + "https://github.com/guzzle/psr7/security/advisories/GHSA-q7rv-6hp3-vh96", + "https://nvd.nist.gov/vuln/detail/CVE-2022-24775" + ], + "PublishedDate": "2022-03-25T19:26:33Z", + "LastModifiedDate": "2022-06-14T20:02:29Z" + } + ] + } + ] +} diff --git a/integration/testdata/conan.json.golden b/integration/testdata/conan.json.golden new file mode 100644 index 000000000000..e37fedc2e26e --- /dev/null +++ b/integration/testdata/conan.json.golden @@ -0,0 +1,200 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/conan", + "ArtifactType": "repository", + "Results": [ + { + "Target": "conan.lock", + "Class": "lang-pkgs", + "Type": "conan", + "Packages": [ + { + "ID": "poco/1.9.4", + "Name": "poco", + "Identifier": { + "PURL": "pkg:conan/poco@1.9.4", + "UID": "da7062258611d4cc" + }, + "Version": "1.9.4", + "Relationship": "direct", + "DependsOn": [ + "pcre/8.43", + "zlib/1.2.12", + "expat/2.4.8", + "sqlite3/3.39.2", + "openssl/1.1.1q" + ], + "Locations": [ + { + "StartLine": 12, + "EndLine": 25 + } + ], + "AnalyzedBy": "conan-lock" + }, + { + "ID": "bzip2/1.0.8", + "Name": "bzip2", + "Identifier": { + "PURL": "pkg:conan/bzip2@1.0.8", + "UID": "b510b604433d1be0" + }, + "Version": "1.0.8", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 37, + "EndLine": 43 + } + ], + "AnalyzedBy": "conan-lock" + }, + { + "ID": "expat/2.4.8", + "Name": "expat", + "Identifier": { + "PURL": "pkg:conan/expat@2.4.8", + "UID": "f293d4bbe4291e24" + }, + "Version": "2.4.8", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 51, + "EndLine": 57 + } + ], + "AnalyzedBy": "conan-lock" + }, + { + "ID": "openssl/1.1.1q", + "Name": "openssl", + "Identifier": { + "PURL": "pkg:conan/openssl@1.1.1q", + "UID": "13e4c291040aa3da" + }, + "Version": "1.1.1q", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 65, + "EndLine": 71 + } + ], + "AnalyzedBy": "conan-lock" + }, + { + "ID": "pcre/8.43", + "Name": "pcre", + "Identifier": { + "PURL": "pkg:conan/pcre@8.43", + "UID": "adc7cb44f77b9bfc" + }, + "Version": "8.43", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "bzip2/1.0.8", + "zlib/1.2.12" + ], + "Locations": [ + { + "StartLine": 26, + "EndLine": 36 + } + ], + "AnalyzedBy": "conan-lock" + }, + { + "ID": "sqlite3/3.39.2", + "Name": "sqlite3", + "Identifier": { + "PURL": "pkg:conan/sqlite3@3.39.2", + "UID": "99b2c7dc64d6c85b" + }, + "Version": "3.39.2", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 58, + "EndLine": 64 + } + ], + "AnalyzedBy": "conan-lock" + }, + { + "ID": "zlib/1.2.12", + "Name": "zlib", + "Identifier": { + "PURL": "pkg:conan/zlib@1.2.12", + "UID": "322af4309791cb84" + }, + "Version": "1.2.12", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 44, + "EndLine": 50 + } + ], + "AnalyzedBy": "conan-lock" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-14155", + "PkgID": "pcre/8.43", + "PkgName": "pcre", + "PkgIdentifier": { + "PURL": "pkg:conan/pcre@8.43", + "UID": "adc7cb44f77b9bfc" + }, + "InstalledVersion": "8.43", + "FixedVersion": "8.45", + "Status": "fixed", + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-14155", + "Fingerprint": "sha256:5169c33c7fbcb3f6d281b9c8d4b4ffe49a7d7b79c52e1810f35353d1bcc79f26", + "Title": "pcre: Integer overflow when parsing callout numeric arguments", + "Description": "libpcre in PCRE before 8.44 allows an integer overflow via a large number after a (?C substring.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-190" + ], + "VendorSeverity": { + "alma": 1, + "nvd": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", + "V3Score": 5.3 + } + }, + "References": [ + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-14155", + "https://nvd.nist.gov/vuln/detail/CVE-2020-14155" + ], + "PublishedDate": "2020-06-15T17:15:00Z", + "LastModifiedDate": "2022-04-28T15:06:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/conda-cyclonedx.json.golden b/integration/testdata/conda-cyclonedx.json.golden new file mode 100644 index 000000000000..a1142a474636 --- /dev/null +++ b/integration/testdata/conda-cyclonedx.json.golden @@ -0,0 +1,114 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3ff14136-e09f-4df9-80ea-000000000005", + "version": 1, + "metadata": { + "timestamp": "2021-08-25T12:20:30+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "3ff14136-e09f-4df9-80ea-000000000002", + "type": "application", + "name": "testdata/fixtures/repo/conda", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:conda/openssl@1.1.1q", + "type": "library", + "name": "openssl", + "version": "1.1.1q", + "hashes": [ + { + "alg": "SHA-1", + "content": "237db0da53131e4548cb1181337fa0f420299e1f" + } + ], + "licenses": [ + { + "license": { + "id": "OpenSSL" + } + } + ], + "purl": "pkg:conda/openssl@1.1.1q", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "miniconda3/envs/testenv/conda-meta/openssl-1.1.1q-h7f8727e_0.json" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "conda-pkg" + } + ] + }, + { + "bom-ref": "pkg:conda/pip@22.2.2", + "type": "library", + "name": "pip", + "version": "22.2.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "a6a2db7668f1ad541d704369fc66c96a4415aa24" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:conda/pip@22.2.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "miniconda3/envs/testenv/conda-meta/pip-22.2.2-py38h06a4308_0.json" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "conda-pkg" + } + ] + } + ], + "dependencies": [ + { + "ref": "3ff14136-e09f-4df9-80ea-000000000002", + "dependsOn": [ + "pkg:conda/openssl@1.1.1q", + "pkg:conda/pip@22.2.2" + ] + }, + { + "ref": "pkg:conda/openssl@1.1.1q", + "dependsOn": [] + }, + { + "ref": "pkg:conda/pip@22.2.2", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/integration/testdata/conda-environment-cyclonedx.json.golden b/integration/testdata/conda-environment-cyclonedx.json.golden new file mode 100644 index 000000000000..e4a874d06238 --- /dev/null +++ b/integration/testdata/conda-environment-cyclonedx.json.golden @@ -0,0 +1,83 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3ff14136-e09f-4df9-80ea-000000000005", + "version": 1, + "metadata": { + "timestamp": "2021-08-25T12:20:30+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "3ff14136-e09f-4df9-80ea-000000000002", + "type": "application", + "name": "testdata/fixtures/repo/conda-environment", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "3ff14136-e09f-4df9-80ea-000000000003", + "type": "application", + "name": "environment.yaml", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "conda-environment" + } + ] + }, + { + "bom-ref": "pkg:conda/bzip2@1.0.8", + "type": "library", + "name": "bzip2", + "version": "1.0.8", + "purl": "pkg:conda/bzip2@1.0.8", + "properties": [ + { + "name": "aquasecurity:trivy:PkgType", + "value": "conda-environment" + } + ] + } + ], + "dependencies": [ + { + "ref": "3ff14136-e09f-4df9-80ea-000000000002", + "dependsOn": [ + "3ff14136-e09f-4df9-80ea-000000000003" + ] + }, + { + "ref": "3ff14136-e09f-4df9-80ea-000000000003", + "dependsOn": [ + "pkg:conda/bzip2@1.0.8" + ] + }, + { + "ref": "pkg:conda/bzip2@1.0.8", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/integration/testdata/conda-spdx.json.golden b/integration/testdata/conda-spdx.json.golden new file mode 100644 index 000000000000..3607b562bad9 --- /dev/null +++ b/integration/testdata/conda-spdx.json.golden @@ -0,0 +1,140 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "testdata/fixtures/repo/conda", + "documentNamespace": "http://trivy.dev/filesystem/testdata/fixtures/repo/conda-3ff14136-e09f-4df9-80ea-000000000005", + "creationInfo": { + "creators": [ + "Organization: aquasecurity", + "Tool: trivy-dev" + ], + "created": "2021-08-25T12:20:30Z" + }, + "packages": [ + { + "name": "openssl", + "SPDXID": "SPDXRef-Package-8217dab1d30aca2", + "versionInfo": "1.1.1q", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": true, + "packageVerificationCode": { + "packageVerificationCodeValue": "2a05c7ad4baa728c79bc1ee1adea9b837d0bf6c0" + }, + "licenseConcluded": "OpenSSL", + "licenseDeclared": "OpenSSL", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:conda/openssl@1.1.1q" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "PkgType: conda-pkg" + } + ] + }, + { + "name": "pip", + "SPDXID": "SPDXRef-Package-1c3eafceb0a76303", + "versionInfo": "22.2.2", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": true, + "packageVerificationCode": { + "packageVerificationCodeValue": "8619446a1cd1118e82f1f984ce59116422a59151" + }, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:conda/pip@22.2.2" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "PkgType: conda-pkg" + } + ] + }, + { + "name": "testdata/fixtures/repo/conda", + "SPDXID": "SPDXRef-Filesystem-2e2426fd0f2580ef", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "primaryPackagePurpose": "SOURCE", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "SchemaVersion: 2" + } + ] + } + ], + "files": [ + { + "fileName": "miniconda3/envs/testenv/conda-meta/openssl-1.1.1q-h7f8727e_0.json", + "SPDXID": "SPDXRef-File-600e5e0110a84891", + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "237db0da53131e4548cb1181337fa0f420299e1f" + } + ], + "copyrightText": "" + }, + { + "fileName": "miniconda3/envs/testenv/conda-meta/pip-22.2.2-py38h06a4308_0.json", + "SPDXID": "SPDXRef-File-7eb62e2a3edddc0a", + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "a6a2db7668f1ad541d704369fc66c96a4415aa24" + } + ], + "copyrightText": "" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-Filesystem-2e2426fd0f2580ef", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-Filesystem-2e2426fd0f2580ef", + "relatedSpdxElement": "SPDXRef-Package-1c3eafceb0a76303", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Filesystem-2e2426fd0f2580ef", + "relatedSpdxElement": "SPDXRef-Package-8217dab1d30aca2", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-1c3eafceb0a76303", + "relatedSpdxElement": "SPDXRef-File-7eb62e2a3edddc0a", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-8217dab1d30aca2", + "relatedSpdxElement": "SPDXRef-File-600e5e0110a84891", + "relationshipType": "CONTAINS" + } + ] +} \ No newline at end of file diff --git a/integration/testdata/count-0.1.0-plugin-with-before-flag.txt.golden b/integration/testdata/count-0.1.0-plugin-with-before-flag.txt.golden new file mode 100644 index 000000000000..4c9d2bbaeb41 --- /dev/null +++ b/integration/testdata/count-0.1.0-plugin-with-before-flag.txt.golden @@ -0,0 +1,5 @@ +Installed Plugins: + Name: count + Version: 0.1.0 + +Number of vulnerabilities: 1 diff --git a/integration/testdata/count-0.2.0-plugin.txt.golden b/integration/testdata/count-0.2.0-plugin.txt.golden new file mode 100644 index 000000000000..9f86fc48e6dd --- /dev/null +++ b/integration/testdata/count-0.2.0-plugin.txt.golden @@ -0,0 +1,5 @@ +Installed Plugins: + Name: count + Version: 0.2.0 + +Number of vulnerabilities: 2 diff --git a/integration/testdata/debian-buster-ignore-unfixed.json.golden b/integration/testdata/debian-buster-ignore-unfixed.json.golden new file mode 100644 index 000000000000..25e6d7fcbb64 --- /dev/null +++ b/integration/testdata/debian-buster-ignore-unfixed.json.golden @@ -0,0 +1,145 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:b75d2c78a42eae6eaa44e99638ba5fa36900538fa0b7a4feba19d18dd588552d", + "ArtifactName": "testdata/fixtures/images/debian-buster.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 119199744, + "OS": { + "Family": "debian", + "Name": "10.1" + }, + "ImageID": "sha256:c2c03a296d2329a4f3ab72a7bf38b78a8a80108204d326b0139d6af700e152d1", + "DiffIDs": [ + "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:debian-buster" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:debian-buster", + "ImageConfig": { + "architecture": "amd64", + "container": "cbb6a20ddb7dedfeee41aeb21e9780f14afbb0f47a6b1ffa514a1822f45d0a51", + "created": "2019-09-11T23:21:51.562946709Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-09-11T23:21:51.123609274Z", + "created_by": "/bin/sh -c #(nop) ADD file:770e381defc5e4a0ba5df52265a96494b9f5d94309234cb3f7bc6b00e1d18f9a in / " + }, + { + "created": "2019-09-11T23:21:51.562946709Z", + "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d" + ] + }, + "config": { + "Cmd": [ + "bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:5519bb349f72eef81944da56843c995b1b81ed67c8e7e48ac29dd6c543c1dd2d", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 119199744, + "Digest": "sha256:4a56a430b2bac33260d6449e162017e2b23076c6411a17b46db67f5b84dde2bd", + "DiffID": "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/debian-buster.tar.gz (debian 10.1)", + "Class": "os-pkgs", + "Type": "debian", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18224", + "VendorIDs": [ + "DSA-4613-1" + ], + "PkgID": "libidn2-0@2.0.5-1", + "PkgName": "libidn2-0", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libidn2-0@2.0.5-1?arch=amd64\u0026distro=debian-10.1", + "UID": "dbc5a507d1ad704d" + }, + "InstalledVersion": "2.0.5-1", + "FixedVersion": "2.0.5-1+deb10u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:4a56a430b2bac33260d6449e162017e2b23076c6411a17b46db67f5b84dde2bd", + "DiffID": "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18224", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:96ec45ab3d0cc9ab034c0ce21ce0bf7692ea49979b4844ab9e97a7e5db629fd4", + "Title": "libidn2: heap-based buffer overflow in idn2_to_ascii_4i in lib/lookup.c", + "Description": "idn2_to_ascii_4i in lib/lookup.c in GNU libidn2 before 2.1.1 has a heap-based buffer overflow via a long domain string.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 4, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00008.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00009.html", + "https://access.redhat.com/security/cve/CVE-2019-18224", + "https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=12420", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18224", + "https://github.com/libidn/libidn2/commit/e4d1558aa2c1c04a05066ee8600f37603890ba8c", + "https://github.com/libidn/libidn2/compare/libidn2-2.1.0...libidn2-2.1.1", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/JDQVQ2XPV5BTZUFINT7AFJSKNNBVURNJ/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MINU5RKDFE6TKAFY5DRFN3WSFDS4DYVS/", + "https://seclists.org/bugtraq/2020/Feb/4", + "https://security.gentoo.org/glsa/202003-63", + "https://ubuntu.com/security/notices/USN-4168-1", + "https://usn.ubuntu.com/4168-1/", + "https://www.debian.org/security/2020/dsa-4613" + ], + "PublishedDate": "2019-10-21T17:15:00Z", + "LastModifiedDate": "2019-10-29T19:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/debian-buster.json.golden b/integration/testdata/debian-buster.json.golden new file mode 100644 index 000000000000..44bdd6f90fa5 --- /dev/null +++ b/integration/testdata/debian-buster.json.golden @@ -0,0 +1,210 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:b75d2c78a42eae6eaa44e99638ba5fa36900538fa0b7a4feba19d18dd588552d", + "ArtifactName": "testdata/fixtures/images/debian-buster.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 119199744, + "OS": { + "Family": "debian", + "Name": "10.1" + }, + "ImageID": "sha256:c2c03a296d2329a4f3ab72a7bf38b78a8a80108204d326b0139d6af700e152d1", + "DiffIDs": [ + "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:debian-buster" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:debian-buster", + "ImageConfig": { + "architecture": "amd64", + "container": "cbb6a20ddb7dedfeee41aeb21e9780f14afbb0f47a6b1ffa514a1822f45d0a51", + "created": "2019-09-11T23:21:51.562946709Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-09-11T23:21:51.123609274Z", + "created_by": "/bin/sh -c #(nop) ADD file:770e381defc5e4a0ba5df52265a96494b9f5d94309234cb3f7bc6b00e1d18f9a in / " + }, + { + "created": "2019-09-11T23:21:51.562946709Z", + "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d" + ] + }, + "config": { + "Cmd": [ + "bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:5519bb349f72eef81944da56843c995b1b81ed67c8e7e48ac29dd6c543c1dd2d", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 119199744, + "Digest": "sha256:4a56a430b2bac33260d6449e162017e2b23076c6411a17b46db67f5b84dde2bd", + "DiffID": "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/debian-buster.tar.gz (debian 10.1)", + "Class": "os-pkgs", + "Type": "debian", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18276", + "PkgID": "bash@5.0-4", + "PkgName": "bash", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/bash@5.0-4?arch=amd64\u0026distro=debian-10.1", + "UID": "be7e0e99d0e7acf6" + }, + "InstalledVersion": "5.0-4", + "Status": "affected", + "Layer": { + "Digest": "sha256:4a56a430b2bac33260d6449e162017e2b23076c6411a17b46db67f5b84dde2bd", + "DiffID": "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d" + }, + "SeveritySource": "debian", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:cafe98610a3a511390be596c1d90dc529b57bb1159885872be88e33a85bb2a15", + "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", + "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "Severity": "LOW", + "CweIDs": [ + "CWE-273" + ], + "VendorSeverity": { + "cbl-mariner": 3, + "debian": 1, + "nvd": 3, + "oracle-oval": 1, + "photon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.2, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + } + }, + "References": [ + "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", + "https://access.redhat.com/security/cve/CVE-2019-18276", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", + "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", + "https://linux.oracle.com/cve/CVE-2019-18276.html", + "https://linux.oracle.com/errata/ELSA-2021-1679.html", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", + "https://security.gentoo.org/glsa/202105-34", + "https://security.netapp.com/advisory/ntap-20200430-0003/", + "https://www.youtube.com/watch?v=-wGtxJ8opa8" + ], + "PublishedDate": "2019-11-28T01:15:00Z", + "LastModifiedDate": "2021-05-26T12:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-18224", + "VendorIDs": [ + "DSA-4613-1" + ], + "PkgID": "libidn2-0@2.0.5-1", + "PkgName": "libidn2-0", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libidn2-0@2.0.5-1?arch=amd64\u0026distro=debian-10.1", + "UID": "dbc5a507d1ad704d" + }, + "InstalledVersion": "2.0.5-1", + "FixedVersion": "2.0.5-1+deb10u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:4a56a430b2bac33260d6449e162017e2b23076c6411a17b46db67f5b84dde2bd", + "DiffID": "sha256:78c1b9419976227e05be9d243b7fa583bea44a5258e52018b2af4cdfe23d148d" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18224", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:96ec45ab3d0cc9ab034c0ce21ce0bf7692ea49979b4844ab9e97a7e5db629fd4", + "Title": "libidn2: heap-based buffer overflow in idn2_to_ascii_4i in lib/lookup.c", + "Description": "idn2_to_ascii_4i in lib/lookup.c in GNU libidn2 before 2.1.1 has a heap-based buffer overflow via a long domain string.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 4, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00008.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00009.html", + "https://access.redhat.com/security/cve/CVE-2019-18224", + "https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=12420", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18224", + "https://github.com/libidn/libidn2/commit/e4d1558aa2c1c04a05066ee8600f37603890ba8c", + "https://github.com/libidn/libidn2/compare/libidn2-2.1.0...libidn2-2.1.1", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/JDQVQ2XPV5BTZUFINT7AFJSKNNBVURNJ/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MINU5RKDFE6TKAFY5DRFN3WSFDS4DYVS/", + "https://seclists.org/bugtraq/2020/Feb/4", + "https://security.gentoo.org/glsa/202003-63", + "https://ubuntu.com/security/notices/USN-4168-1", + "https://usn.ubuntu.com/4168-1/", + "https://www.debian.org/security/2020/dsa-4613" + ], + "PublishedDate": "2019-10-21T17:15:00Z", + "LastModifiedDate": "2019-10-29T19:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/debian-stretch.json.golden b/integration/testdata/debian-stretch.json.golden new file mode 100644 index 000000000000..020d9a4ba46d --- /dev/null +++ b/integration/testdata/debian-stretch.json.golden @@ -0,0 +1,441 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:e7f0b65012f754f3e69bfa9e94999ba080f12cd7e6ac2742d5cb908252f9609f", + "ArtifactName": "testdata/fixtures/images/debian-stretch.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 105582080, + "OS": { + "Family": "debian", + "Name": "9.9" + }, + "ImageID": "sha256:f26939cc87ef44a6fc554eedd0a976ab30b5bc2769d65d2e986b6c5f1fd4053d", + "DiffIDs": [ + "sha256:f73e7e79899a33b4b9b78da62efb71520844f8dd518f3c390e27bc3063bce307" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:debian-stretch" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:debian-stretch", + "ImageConfig": { + "architecture": "amd64", + "container": "957bc0b73d29f0e1030fec9c63f81d3e81baa610cffcc9c574b14fee6d1821ae", + "created": "2019-08-14T00:24:45.872523599Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-08-14T00:24:45.612796997Z", + "created_by": "/bin/sh -c #(nop) ADD file:b9b24bd862a79bf6c6e79daf6babca27245063eb52a2f72ffc4fc3494ddd3d48 in / " + }, + { + "created": "2019-08-14T00:24:45.872523599Z", + "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:f73e7e79899a33b4b9b78da62efb71520844f8dd518f3c390e27bc3063bce307" + ] + }, + "config": { + "Cmd": [ + "bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:2ce0e924e5d43d66387e476478ce3c857b1eaae74b5c74693ed47b3502bbdc3e", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 105582080, + "Digest": "sha256:9cc2ad81d40d54dcae7fa5e8e17d9c34e8bba3b7c2cc7e26fb22734608bda32e", + "DiffID": "sha256:f73e7e79899a33b4b9b78da62efb71520844f8dd518f3c390e27bc3063bce307" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/debian-stretch.tar.gz (debian 9.9)", + "Class": "os-pkgs", + "Type": "debian", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18276", + "PkgID": "bash@4.4-5", + "PkgName": "bash", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/bash@4.4-5?arch=amd64\u0026distro=debian-9.9", + "UID": "c15fdc5c69f1dca4" + }, + "InstalledVersion": "4.4-5", + "Status": "end_of_life", + "Layer": { + "Digest": "sha256:9cc2ad81d40d54dcae7fa5e8e17d9c34e8bba3b7c2cc7e26fb22734608bda32e", + "DiffID": "sha256:f73e7e79899a33b4b9b78da62efb71520844f8dd518f3c390e27bc3063bce307" + }, + "SeveritySource": "debian", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:9adeadcf67c2ffed3f4c77a699667e96cdd0f638621e4568a0b7f369542b7ac9", + "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", + "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "Severity": "LOW", + "CweIDs": [ + "CWE-273" + ], + "VendorSeverity": { + "cbl-mariner": 3, + "debian": 1, + "nvd": 3, + "oracle-oval": 1, + "photon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.2, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + } + }, + "References": [ + "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", + "https://access.redhat.com/security/cve/CVE-2019-18276", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", + "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", + "https://linux.oracle.com/cve/CVE-2019-18276.html", + "https://linux.oracle.com/errata/ELSA-2021-1679.html", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", + "https://security.gentoo.org/glsa/202105-34", + "https://security.netapp.com/advisory/ntap-20200430-0003/", + "https://www.youtube.com/watch?v=-wGtxJ8opa8" + ], + "PublishedDate": "2019-11-28T01:15:00Z", + "LastModifiedDate": "2021-05-26T12:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "VendorIDs": [ + "DSA-4535-1" + ], + "PkgID": "e2fslibs@1.43.4-2", + "PkgName": "e2fslibs", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/e2fslibs@1.43.4-2?arch=amd64\u0026distro=debian-9.9", + "UID": "9f94b1dd02406b5" + }, + "InstalledVersion": "1.43.4-2", + "FixedVersion": "1.43.4-2+deb9u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:9cc2ad81d40d54dcae7fa5e8e17d9c34e8bba3b7c2cc7e26fb22734608bda32e", + "DiffID": "sha256:f73e7e79899a33b4b9b78da62efb71520844f8dd518f3c390e27bc3063bce307" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:b04aa895da3f042ac6507dc4f0d156f9531360f187d074e96880eda78471c3af", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "VendorIDs": [ + "DSA-4535-1" + ], + "PkgID": "e2fsprogs@1.43.4-2", + "PkgName": "e2fsprogs", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/e2fsprogs@1.43.4-2?arch=amd64\u0026distro=debian-9.9", + "UID": "5d43b599b340c996" + }, + "InstalledVersion": "1.43.4-2", + "FixedVersion": "1.43.4-2+deb9u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:9cc2ad81d40d54dcae7fa5e8e17d9c34e8bba3b7c2cc7e26fb22734608bda32e", + "DiffID": "sha256:f73e7e79899a33b4b9b78da62efb71520844f8dd518f3c390e27bc3063bce307" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:477209bbac3b7f0ec8eea382a6778a9f14448185bf37500315bf47dfd1c2d619", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "VendorIDs": [ + "DSA-4535-1" + ], + "PkgID": "libcomerr2@1.43.4-2", + "PkgName": "libcomerr2", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libcomerr2@1.43.4-2?arch=amd64\u0026distro=debian-9.9", + "UID": "7e38fd4935d4d0d0" + }, + "InstalledVersion": "1.43.4-2", + "FixedVersion": "1.43.4-2+deb9u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:9cc2ad81d40d54dcae7fa5e8e17d9c34e8bba3b7c2cc7e26fb22734608bda32e", + "DiffID": "sha256:f73e7e79899a33b4b9b78da62efb71520844f8dd518f3c390e27bc3063bce307" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:4013e70d72539f72c0bb62e9d041adea27f65ba5e3a6740f9b2522cac9fa7c5a", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "VendorIDs": [ + "DSA-4535-1" + ], + "PkgID": "libss2@1.43.4-2", + "PkgName": "libss2", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libss2@1.43.4-2?arch=amd64\u0026distro=debian-9.9", + "UID": "5871861d62f76b19" + }, + "InstalledVersion": "1.43.4-2", + "FixedVersion": "1.43.4-2+deb9u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:9cc2ad81d40d54dcae7fa5e8e17d9c34e8bba3b7c2cc7e26fb22734608bda32e", + "DiffID": "sha256:f73e7e79899a33b4b9b78da62efb71520844f8dd518f3c390e27bc3063bce307" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:cbb54b0273c282fd61899d8d6ed57065a8815e6856e1dc72f02362a4a985fc12", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/distroless-base.json.golden b/integration/testdata/distroless-base.json.golden new file mode 100644 index 000000000000..72319052f22d --- /dev/null +++ b/integration/testdata/distroless-base.json.golden @@ -0,0 +1,435 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:6c3688715cb42ea1466b96eb45b39d8afc9f8cdcf723df8464fb26391711a7db", + "ArtifactName": "testdata/fixtures/images/distroless-base.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 18503680, + "OS": { + "Family": "debian", + "Name": "9.9" + }, + "ImageID": "sha256:7f04a8d247173b1f2546d22913af637bbab4e7411e00ae6207da8d94c445750d", + "DiffIDs": [ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:distroless-base" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:distroless-base", + "ImageConfig": { + "architecture": "amd64", + "author": "Bazel", + "created": "1970-01-01T00:00:00Z", + "history": [ + { + "author": "Bazel", + "created": "1970-01-01T00:00:00Z", + "created_by": "bazel build ..." + }, + { + "author": "Bazel", + "created": "1970-01-01T00:00:00Z", + "created_by": "bazel build ..." + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + ] + }, + "config": { + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt" + ] + } + }, + "Layers": [ + { + "Size": 3061760, + "Digest": "sha256:e8d8785a314f385d3675a017f4e2df1707c528c06e7a7989663fdab4900bd8ff", + "DiffID": "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02" + }, + { + "Size": 15441920, + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/distroless-base.tar.gz (debian 9.9)", + "Class": "os-pkgs", + "Type": "debian", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "libssl1.1@1.1.0k-1~deb9u1", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libssl1.1@1.1.0k-1~deb9u1?arch=amd64\u0026distro=debian-9.9", + "UID": "3e39945fa102851d" + }, + "InstalledVersion": "1.1.0k-1~deb9u1", + "Status": "affected", + "Layer": { + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:988273935622ab69d9fc3f6ec47784680e2c038a9f43c7256bc7ae9533fee128", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1563", + "VendorIDs": [ + "DSA-4539-1" + ], + "PkgID": "libssl1.1@1.1.0k-1~deb9u1", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libssl1.1@1.1.0k-1~deb9u1?arch=amd64\u0026distro=debian-9.9", + "UID": "3e39945fa102851d" + }, + "InstalledVersion": "1.1.0k-1~deb9u1", + "FixedVersion": "1.1.0l-1~deb9u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1563", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:0fd47b00a9b5903c69e544845f4c525af5c9aad5fee07976949c8091607b3029", + "Title": "openssl: information disclosure in PKCS7_dataDecode and CMS_decrypt_set1_pkey", + "Description": "In situations where an attacker receives automated notification of the success or failure of a decryption attempt an attacker, after sending a very large number of messages to be decrypted, can recover a CMS/PKCS7 transported encryption key or decrypt any RSA encrypted message that was encrypted with the public RSA key, using a Bleichenbacher padding oracle attack. Applications are not affected if they use a certificate together with the private RSA key to the CMS_decrypt or PKCS7_decrypt functions to select the correct recipient info to decrypt. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).", + "Severity": "LOW", + "CweIDs": [ + "CWE-327", + "CWE-203" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 1, + "oracle-oval": 2, + "photon": 1, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 4.3, + "V3Score": 3.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V3Score": 3.7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00054.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00072.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00012.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00016.html", + "http://packetstormsecurity.com/files/154467/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1563", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1563", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=08229ad838c50f644d7e928e2eef147b4308ad64", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=631f94db0065c78181ca9ba5546ebc8bb3884b97", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e21f8cf78a125cd3c8c0d1a1a6c8bb0b901f893f", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10365", + "https://linux.oracle.com/cve/CVE-2019-1563.html", + "https://linux.oracle.com/errata/ELSA-2020-1840.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00026.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/", + "https://seclists.org/bugtraq/2019/Oct/0", + "https://seclists.org/bugtraq/2019/Oct/1", + "https://seclists.org/bugtraq/2019/Sep/25", + "https://security.gentoo.org/glsa/201911-04", + "https://security.netapp.com/advisory/ntap-20190919-0002/", + "https://support.f5.com/csp/article/K97324400?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4376-2", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4376-2/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4539", + "https://www.debian.org/security/2019/dsa-4540", + "https://www.openssl.org/news/secadv/20190910.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.tenable.com/security/tns-2019-09" + ], + "PublishedDate": "2019-09-10T17:15:00Z", + "LastModifiedDate": "2021-07-31T08:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "openssl@1.1.0k-1~deb9u1", + "PkgName": "openssl", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/openssl@1.1.0k-1~deb9u1?arch=amd64\u0026distro=debian-9.9", + "UID": "464eae002f08322" + }, + "InstalledVersion": "1.1.0k-1~deb9u1", + "Status": "affected", + "Layer": { + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:322d845429d01a4d5c814cf25fdc290de542001c5d71eb8aedd7056ba0c7a00c", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1563", + "VendorIDs": [ + "DSA-4539-1" + ], + "PkgID": "openssl@1.1.0k-1~deb9u1", + "PkgName": "openssl", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/openssl@1.1.0k-1~deb9u1?arch=amd64\u0026distro=debian-9.9", + "UID": "464eae002f08322" + }, + "InstalledVersion": "1.1.0k-1~deb9u1", + "FixedVersion": "1.1.0l-1~deb9u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1563", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:731634fea1bbf46b1bf7da40f0bfd9fae4c4ec2bebc48a32172a42c78ad0eb53", + "Title": "openssl: information disclosure in PKCS7_dataDecode and CMS_decrypt_set1_pkey", + "Description": "In situations where an attacker receives automated notification of the success or failure of a decryption attempt an attacker, after sending a very large number of messages to be decrypted, can recover a CMS/PKCS7 transported encryption key or decrypt any RSA encrypted message that was encrypted with the public RSA key, using a Bleichenbacher padding oracle attack. Applications are not affected if they use a certificate together with the private RSA key to the CMS_decrypt or PKCS7_decrypt functions to select the correct recipient info to decrypt. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).", + "Severity": "LOW", + "CweIDs": [ + "CWE-327", + "CWE-203" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 1, + "oracle-oval": 2, + "photon": 1, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 4.3, + "V3Score": 3.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V3Score": 3.7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00054.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00072.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00012.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00016.html", + "http://packetstormsecurity.com/files/154467/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1563", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1563", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=08229ad838c50f644d7e928e2eef147b4308ad64", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=631f94db0065c78181ca9ba5546ebc8bb3884b97", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e21f8cf78a125cd3c8c0d1a1a6c8bb0b901f893f", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10365", + "https://linux.oracle.com/cve/CVE-2019-1563.html", + "https://linux.oracle.com/errata/ELSA-2020-1840.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00026.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/", + "https://seclists.org/bugtraq/2019/Oct/0", + "https://seclists.org/bugtraq/2019/Oct/1", + "https://seclists.org/bugtraq/2019/Sep/25", + "https://security.gentoo.org/glsa/201911-04", + "https://security.netapp.com/advisory/ntap-20190919-0002/", + "https://support.f5.com/csp/article/K97324400?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4376-2", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4376-2/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4539", + "https://www.debian.org/security/2019/dsa-4540", + "https://www.openssl.org/news/secadv/20190910.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.tenable.com/security/tns-2019-09" + ], + "PublishedDate": "2019-09-10T17:15:00Z", + "LastModifiedDate": "2021-07-31T08:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/distroless-python27.json.golden b/integration/testdata/distroless-python27.json.golden new file mode 100644 index 000000000000..03ba1c69a56a --- /dev/null +++ b/integration/testdata/distroless-python27.json.golden @@ -0,0 +1,462 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:b9ec0b7f93064fddace988e9a901386ccd55a6c16a34c00d5c45b06f62ec20ca", + "ArtifactName": "testdata/fixtures/images/distroless-python27.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 48271360, + "OS": { + "Family": "debian", + "Name": "9.9" + }, + "ImageID": "sha256:6fcac2cc8a710f21577b5bbd534e0bfc841c0cca569b57182ba19054696cddda", + "DiffIDs": [ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + "sha256:6189abe095d53c1c9f2bfc8f50128ee876b9a5d10f9eda1564e5f5357d6ffe61", + "sha256:e92caab8efcf25a24bea5213ab7e54d4a5f5f08644836bb2d296070b1ae1044e" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:distroless-python27" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:distroless-python27", + "ImageConfig": { + "architecture": "amd64", + "author": "Bazel", + "created": "1970-01-01T00:00:00Z", + "history": [ + { + "author": "Bazel", + "created": "1970-01-01T00:00:00Z", + "created_by": "bazel build ..." + }, + { + "author": "Bazel", + "created": "1970-01-01T00:00:00Z", + "created_by": "bazel build ..." + }, + { + "author": "Bazel", + "created": "1970-01-01T00:00:00Z", + "created_by": "bazel build ..." + }, + { + "author": "Bazel", + "created": "1970-01-01T00:00:00Z", + "created_by": "bazel build ..." + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + "sha256:6189abe095d53c1c9f2bfc8f50128ee876b9a5d10f9eda1564e5f5357d6ffe61", + "sha256:e92caab8efcf25a24bea5213ab7e54d4a5f5f08644836bb2d296070b1ae1044e" + ] + }, + "config": { + "Entrypoint": [ + "/usr/bin/python2.7" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt" + ] + } + }, + "Layers": [ + { + "Size": 3061760, + "Digest": "sha256:e8d8785a314f385d3675a017f4e2df1707c528c06e7a7989663fdab4900bd8ff", + "DiffID": "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02" + }, + { + "Size": 15441920, + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + { + "Size": 1966080, + "Digest": "sha256:3e010093287c245d72a774033b4cddd6451a820bfbb1948c97798e1838858dd2", + "DiffID": "sha256:6189abe095d53c1c9f2bfc8f50128ee876b9a5d10f9eda1564e5f5357d6ffe61" + }, + { + "Size": 27801600, + "Digest": "sha256:0fedbb4c6207e6eebb78477eb49d550cafa9eccbf1079c57c026a36d727d684a", + "DiffID": "sha256:e92caab8efcf25a24bea5213ab7e54d4a5f5f08644836bb2d296070b1ae1044e" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/distroless-python27.tar.gz (debian 9.9)", + "Class": "os-pkgs", + "Type": "debian", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "libssl1.1@1.1.0k-1~deb9u1", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libssl1.1@1.1.0k-1~deb9u1?arch=amd64\u0026distro=debian-9.9", + "UID": "3e39945fa102851d" + }, + "InstalledVersion": "1.1.0k-1~deb9u1", + "Status": "affected", + "Layer": { + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:621cf3bfcafb7e4e0e8a6d4e46b56e4ccf7e55c5383cb1a7eaaa179776acfbbc", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1563", + "VendorIDs": [ + "DSA-4539-1" + ], + "PkgID": "libssl1.1@1.1.0k-1~deb9u1", + "PkgName": "libssl1.1", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libssl1.1@1.1.0k-1~deb9u1?arch=amd64\u0026distro=debian-9.9", + "UID": "3e39945fa102851d" + }, + "InstalledVersion": "1.1.0k-1~deb9u1", + "FixedVersion": "1.1.0l-1~deb9u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1563", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:43cbe4454d144da8fe2039fe6c6eafb8348fe278401934232f989393107473a2", + "Title": "openssl: information disclosure in PKCS7_dataDecode and CMS_decrypt_set1_pkey", + "Description": "In situations where an attacker receives automated notification of the success or failure of a decryption attempt an attacker, after sending a very large number of messages to be decrypted, can recover a CMS/PKCS7 transported encryption key or decrypt any RSA encrypted message that was encrypted with the public RSA key, using a Bleichenbacher padding oracle attack. Applications are not affected if they use a certificate together with the private RSA key to the CMS_decrypt or PKCS7_decrypt functions to select the correct recipient info to decrypt. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).", + "Severity": "LOW", + "CweIDs": [ + "CWE-327", + "CWE-203" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 1, + "oracle-oval": 2, + "photon": 1, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 4.3, + "V3Score": 3.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V3Score": 3.7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00054.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00072.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00012.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00016.html", + "http://packetstormsecurity.com/files/154467/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1563", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1563", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=08229ad838c50f644d7e928e2eef147b4308ad64", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=631f94db0065c78181ca9ba5546ebc8bb3884b97", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e21f8cf78a125cd3c8c0d1a1a6c8bb0b901f893f", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10365", + "https://linux.oracle.com/cve/CVE-2019-1563.html", + "https://linux.oracle.com/errata/ELSA-2020-1840.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00026.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/", + "https://seclists.org/bugtraq/2019/Oct/0", + "https://seclists.org/bugtraq/2019/Oct/1", + "https://seclists.org/bugtraq/2019/Sep/25", + "https://security.gentoo.org/glsa/201911-04", + "https://security.netapp.com/advisory/ntap-20190919-0002/", + "https://support.f5.com/csp/article/K97324400?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4376-2", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4376-2/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4539", + "https://www.debian.org/security/2019/dsa-4540", + "https://www.openssl.org/news/secadv/20190910.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.tenable.com/security/tns-2019-09" + ], + "PublishedDate": "2019-09-10T17:15:00Z", + "LastModifiedDate": "2021-07-31T08:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1551", + "PkgID": "openssl@1.1.0k-1~deb9u1", + "PkgName": "openssl", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/openssl@1.1.0k-1~deb9u1?arch=amd64\u0026distro=debian-9.9", + "UID": "464eae002f08322" + }, + "InstalledVersion": "1.1.0k-1~deb9u1", + "Status": "affected", + "Layer": { + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1551", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:26e7213b994b38ec9b2b1208220057c5a93c2d0eef0f175f8c275d2fdbe7563b", + "Title": "openssl: Integer overflow in RSAZ modular exponentiation on x86_64", + "Description": "There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-200" + ], + "VendorSeverity": { + "amazon": 1, + "nvd": 2, + "oracle-oval": 1, + "photon": 2, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 4.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1551", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98", + "https://github.com/openssl/openssl/pull/10575", + "https://linux.oracle.com/cve/CVE-2019-1551.html", + "https://linux.oracle.com/errata/ELSA-2020-4514.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/", + "https://seclists.org/bugtraq/2019/Dec/39", + "https://seclists.org/bugtraq/2019/Dec/46", + "https://security.gentoo.org/glsa/202004-10", + "https://security.netapp.com/advisory/ntap-20191210-0001/", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4594", + "https://www.debian.org/security/2021/dsa-4855", + "https://www.openssl.org/news/secadv/20191206.txt", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.tenable.com/security/tns-2019-09", + "https://www.tenable.com/security/tns-2020-03", + "https://www.tenable.com/security/tns-2020-11", + "https://www.tenable.com/security/tns-2021-10" + ], + "PublishedDate": "2019-12-06T18:15:00Z", + "LastModifiedDate": "2021-07-21T11:39:00Z" + }, + { + "VulnerabilityID": "CVE-2019-1563", + "VendorIDs": [ + "DSA-4539-1" + ], + "PkgID": "openssl@1.1.0k-1~deb9u1", + "PkgName": "openssl", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/openssl@1.1.0k-1~deb9u1?arch=amd64\u0026distro=debian-9.9", + "UID": "464eae002f08322" + }, + "InstalledVersion": "1.1.0k-1~deb9u1", + "FixedVersion": "1.1.0l-1~deb9u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e005d777a298a3529b1c8cf890883359e050cc966089ce84fea4d17b111907db", + "DiffID": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-1563", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:d819fbc64a73608da70e6b6fa147954c1325f34448c10e7df60efef770dbcb08", + "Title": "openssl: information disclosure in PKCS7_dataDecode and CMS_decrypt_set1_pkey", + "Description": "In situations where an attacker receives automated notification of the success or failure of a decryption attempt an attacker, after sending a very large number of messages to be decrypted, can recover a CMS/PKCS7 transported encryption key or decrypt any RSA encrypted message that was encrypted with the public RSA key, using a Bleichenbacher padding oracle attack. Applications are not affected if they use a certificate together with the private RSA key to the CMS_decrypt or PKCS7_decrypt functions to select the correct recipient info to decrypt. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s).", + "Severity": "LOW", + "CweIDs": [ + "CWE-327", + "CWE-203" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 1, + "oracle-oval": 2, + "photon": 1, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V2Score": 4.3, + "V3Score": 3.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N", + "V3Score": 3.7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00054.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00072.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00012.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00016.html", + "http://packetstormsecurity.com/files/154467/Slackware-Security-Advisory-openssl-Updates.html", + "https://access.redhat.com/security/cve/CVE-2019-1563", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1563", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=08229ad838c50f644d7e928e2eef147b4308ad64", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=631f94db0065c78181ca9ba5546ebc8bb3884b97", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e21f8cf78a125cd3c8c0d1a1a6c8bb0b901f893f", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10365", + "https://linux.oracle.com/cve/CVE-2019-1563.html", + "https://linux.oracle.com/errata/ELSA-2020-1840.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00026.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/", + "https://seclists.org/bugtraq/2019/Oct/0", + "https://seclists.org/bugtraq/2019/Oct/1", + "https://seclists.org/bugtraq/2019/Sep/25", + "https://security.gentoo.org/glsa/201911-04", + "https://security.netapp.com/advisory/ntap-20190919-0002/", + "https://support.f5.com/csp/article/K97324400?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-4376-1", + "https://ubuntu.com/security/notices/USN-4376-2", + "https://ubuntu.com/security/notices/USN-4504-1", + "https://usn.ubuntu.com/4376-1/", + "https://usn.ubuntu.com/4376-2/", + "https://usn.ubuntu.com/4504-1/", + "https://www.debian.org/security/2019/dsa-4539", + "https://www.debian.org/security/2019/dsa-4540", + "https://www.openssl.org/news/secadv/20190910.txt", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.tenable.com/security/tns-2019-09" + ], + "PublishedDate": "2019-09-10T17:15:00Z", + "LastModifiedDate": "2021-07-31T08:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/dockerfile-custom-policies.json.golden b/integration/testdata/dockerfile-custom-policies.json.golden new file mode 100644 index 000000000000..daf9a7bb1e8f --- /dev/null +++ b/integration/testdata/dockerfile-custom-policies.json.golden @@ -0,0 +1,57 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/custom-policy", + "ArtifactType": "repository", + "Results": [ + { + "Target": "Dockerfile", + "Class": "config", + "Type": "dockerfile", + "MisconfSummary": { + "Successes": 27, + "Failures": 2 + }, + "Misconfigurations": [ + { + "Type": "Dockerfile Security Check", + "ID": "AVD-BAR-0001", + "AVDID": "AVD-BAR-0001", + "Title": "Custom policy", + "Description": "Custom policy for testing", + "Message": "something bad: bar", + "Namespace": "user.bar", + "Query": "data.user.bar.deny", + "Resolution": "Custom policy for testing", + "Severity": "LOW", + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Custom", + "Service": "custom" + } + }, + { + "Type": "Dockerfile Security Check", + "ID": "AVD-FOO-0001", + "AVDID": "AVD-FOO-0001", + "Title": "Custom policy", + "Description": "Custom policy for testing", + "Message": "something bad: foo", + "Namespace": "user.foo", + "Query": "data.user.foo.deny", + "Resolution": "Custom policy for testing", + "Severity": "LOW", + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Custom", + "Service": "custom" + } + } + ] + } + ] +} diff --git a/integration/testdata/dockerfile.json.golden b/integration/testdata/dockerfile.json.golden new file mode 100644 index 000000000000..6cfd9d95a479 --- /dev/null +++ b/integration/testdata/dockerfile.json.golden @@ -0,0 +1,44 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/dockerfile", + "ArtifactType": "repository", + "Results": [ + { + "Target": "Dockerfile", + "Class": "config", + "Type": "dockerfile", + "MisconfSummary": { + "Successes": 26, + "Failures": 1 + }, + "Misconfigurations": [ + { + "Type": "Dockerfile Security Check", + "ID": "DS-0002", + "Title": "Image user should not be 'root'", + "Description": "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", + "Message": "Specify at least 1 USER command in Dockerfile with non-root user as argument", + "Namespace": "builtin.dockerfile.DS002", + "Query": "data.builtin.dockerfile.DS002.deny", + "Resolution": "Add 'USER \u003cnon root user name\u003e' line to the Dockerfile", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ds-0002", + "References": [ + "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/", + "https://avd.aquasec.com/misconfig/ds-0002" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Dockerfile", + "Service": "general" + } + } + ] + } + ] +} diff --git a/integration/testdata/dockerfile_file_pattern.json.golden b/integration/testdata/dockerfile_file_pattern.json.golden new file mode 100644 index 000000000000..d6d47e605bf4 --- /dev/null +++ b/integration/testdata/dockerfile_file_pattern.json.golden @@ -0,0 +1,44 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/dockerfile_file_pattern", + "ArtifactType": "repository", + "Results": [ + { + "Target": "Customfile", + "Class": "config", + "Type": "dockerfile", + "MisconfSummary": { + "Successes": 26, + "Failures": 1 + }, + "Misconfigurations": [ + { + "Type": "Dockerfile Security Check", + "ID": "DS-0002", + "Title": "Image user should not be 'root'", + "Description": "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", + "Message": "Specify at least 1 USER command in Dockerfile with non-root user as argument", + "Namespace": "builtin.dockerfile.DS002", + "Query": "data.builtin.dockerfile.DS002.deny", + "Resolution": "Add 'USER \u003cnon root user name\u003e' line to the Dockerfile", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ds-0002", + "References": [ + "https://docs.docker.com/develop/develop-images/dockerfile_best-practices/", + "https://avd.aquasec.com/misconfig/ds-0002" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Dockerfile", + "Service": "general" + } + } + ] + } + ] +} diff --git a/integration/testdata/dotnet.json.golden b/integration/testdata/dotnet.json.golden new file mode 100644 index 000000000000..f270127dabec --- /dev/null +++ b/integration/testdata/dotnet.json.golden @@ -0,0 +1,95 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/dotnet", + "ArtifactType": "repository", + "Results": [ + { + "Target": "datacollector.deps.json", + "Class": "lang-pkgs", + "Type": "dotnet-core", + "Packages": [ + { + "ID": "Microsoft.VisualStudio.TestPlatform.Common/17.2.0-release-20220408-11", + "Name": "Microsoft.VisualStudio.TestPlatform.Common", + "Identifier": { + "PURL": "pkg:nuget/Microsoft.VisualStudio.TestPlatform.Common@17.2.0-release-20220408-11", + "UID": "bb3617fb83983d34" + }, + "Version": "17.2.0-release-20220408-11", + "Relationship": "root", + "Locations": [ + { + "StartLine": 15, + "EndLine": 19 + } + ], + "AnalyzedBy": "dotnet-core" + }, + { + "ID": "Newtonsoft.Json/9.0.1", + "Name": "Newtonsoft.Json", + "Identifier": { + "PURL": "pkg:nuget/Newtonsoft.Json@9.0.1", + "UID": "b4411236cba42a08" + }, + "Version": "9.0.1", + "Locations": [ + { + "StartLine": 8, + "EndLine": 14 + } + ], + "AnalyzedBy": "dotnet-core" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "GHSA-5crp-9r3c-p9vr", + "PkgID": "Newtonsoft.Json/9.0.1", + "PkgName": "Newtonsoft.Json", + "PkgIdentifier": { + "PURL": "pkg:nuget/Newtonsoft.Json@9.0.1", + "UID": "b4411236cba42a08" + }, + "InstalledVersion": "9.0.1", + "FixedVersion": "13.0.1", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://github.com/advisories/GHSA-5crp-9r3c-p9vr", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Nuget", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anuget" + }, + "Fingerprint": "sha256:d64b86877361f584534af31e69d7974ef7da7fdebbc024fef7d2fcc8a0b90a1a", + "Title": "Improper Handling of Exceptional Conditions in Newtonsoft.Json", + "Description": "Newtonsoft.Json prior to version 13.0.1 is vulnerable to Insecure Defaults due to improper handling of expressions with high nesting level that lead to StackOverFlow exception or high CPU and RAM usage.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-755" + ], + "VendorSeverity": { + "ghsa": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V3Score": 7.5 + } + }, + "References": [ + "https://alephsecurity.com/2018/10/22/StackOverflowException/", + "https://alephsecurity.com/vulns/aleph-2018004" + ], + "PublishedDate": "2022-06-22T15:08:47Z", + "LastModifiedDate": "2022-06-27T18:37:23Z" + } + ] + } + ] +} diff --git a/integration/testdata/fixtures/convert/npm-with-suppressed.json.golden b/integration/testdata/fixtures/convert/npm-with-suppressed.json.golden new file mode 100644 index 000000000000..712081d0ae30 --- /dev/null +++ b/integration/testdata/fixtures/convert/npm-with-suppressed.json.golden @@ -0,0 +1,179 @@ +{ + "SchemaVersion": 2, + "CreatedAt": "2024-09-09T13:21:09.230231+06:00", + "ArtifactName": "package-lock.json", + "ArtifactType": "filesystem", + "Results": [ + { + "Target": "package-lock.json", + "Class": "lang-pkgs", + "Type": "npm", + "Packages": [ + { + "ID": "debug@3.0.1", + "Name": "debug", + "Identifier": { + "PURL": "pkg:npm/debug@3.0.1", + "UID": "45acc377fa09cc3" + }, + "Version": "3.0.1", + "Relationship": "direct", + "DependsOn": [ + "ms@2.0.0" + ], + "Locations": [ + { + "StartLine": 11, + "EndLine": 19 + } + ] + }, + { + "ID": "ms@2.0.0", + "Name": "ms", + "Identifier": { + "PURL": "pkg:npm/ms@2.0.0", + "UID": "f51af0181daf2ced" + }, + "Version": "2.0.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 20, + "EndLine": 25 + } + ] + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2017-20165", + "PkgID": "debug@3.0.1", + "PkgName": "debug", + "PkgIdentifier": { + "PURL": "pkg:npm/debug@3.0.1", + "UID": "45acc377fa09cc3" + }, + "InstalledVersion": "3.0.1", + "FixedVersion": "3.1.0, 2.6.9", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2017-20165", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Title": "A vulnerability classified as problematic has been found in debug-js d ...", + "Description": "A vulnerability classified as problematic has been found in debug-js debug up to 3.0.x. This affects the function useColors of the file src/node.js. The manipulation of the argument str leads to inefficient regular expression complexity. Upgrading to version 3.1.0 is able to address this issue. The identifier of the patch is c38a0166c266a679c8de012d4eaccec3f944e685. It is recommended to upgrade the affected component. The identifier VDB-217665 was assigned to this vulnerability.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-1333" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V3Score": 7.5 + }, + "nvd": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V3Score": 7.5 + } + }, + "References": [ + "https://github.com/debug-js/debug", + "https://github.com/debug-js/debug/commit/c38a0166c266a679c8de012d4eaccec3f944e685", + "https://github.com/debug-js/debug/commit/f53962e944a87e6ca9bb622a2a12dffc22a9bb5a", + "https://github.com/debug-js/debug/pull/504", + "https://github.com/debug-js/debug/releases/tag/2.6.9", + "https://github.com/debug-js/debug/releases/tag/3.1.0", + "https://nvd.nist.gov/vuln/detail/CVE-2017-20165", + "https://vuldb.com/?ctiid.217665", + "https://vuldb.com/?id.217665" + ], + "PublishedDate": "2023-01-09T10:15:10.447Z", + "LastModifiedDate": "2024-05-17T01:17:24.28Z" + } + ], + "ExperimentalModifiedFindings": [ + { + "Type": "vulnerability", + "Status": "not_affected", + "Statement": "vulnerable_code_not_in_execute_path", + "Source": "./vex.json", + "Finding": { + "VulnerabilityID": "CVE-2017-16137", + "PkgID": "debug@3.0.1", + "PkgName": "debug", + "PkgIdentifier": { + "PURL": "pkg:npm/debug@3.0.1", + "UID": "45acc377fa09cc3" + }, + "InstalledVersion": "3.0.1", + "FixedVersion": "2.6.9, 3.1.0, 3.2.7, 4.3.1", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2017-16137", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Title": "nodejs-debug: Regular expression Denial of Service", + "Description": "The debug module is vulnerable to regular expression denial of service when untrusted user input is passed into the o formatter. It takes around 50k characters to block for 2 seconds making this a low severity issue.", + "Severity": "LOW", + "CweIDs": [ + "CWE-400" + ], + "VendorSeverity": { + "ghsa": 1, + "nvd": 2, + "redhat": 2 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:L", + "V3Score": 3.7 + }, + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", + "V2Score": 5, + "V3Score": 5.3 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L", + "V3Score": 5.3 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2017-16137", + "https://github.com/debug-js/debug/commit/4e2150207c568adb9ead8f4c4528016081c88020", + "https://github.com/debug-js/debug/commit/71169065b5262f9858ac78cc0b688c84a438f290", + "https://github.com/debug-js/debug/commit/b6d12fdbc63b483e5c969da33ea6adc09946b5ac", + "https://github.com/debug-js/debug/commit/f53962e944a87e6ca9bb622a2a12dffc22a9bb5a", + "https://github.com/debug-js/debug/issues/797", + "https://github.com/visionmedia/debug", + "https://github.com/visionmedia/debug/issues/501", + "https://github.com/visionmedia/debug/pull/504", + "https://lists.apache.org/thread.html/r8ba4c628fba7181af58817d452119481adce4ba92e889c643e4c7dd3%40%3Ccommits.netbeans.apache.org%3E", + "https://lists.apache.org/thread.html/r8ba4c628fba7181af58817d452119481adce4ba92e889c643e4c7dd3@%3Ccommits.netbeans.apache.org%3E", + "https://lists.apache.org/thread.html/rb5ac16fad337d1f3bb7079549f97d8166d0ef3082629417c39f12d63%40%3Cnotifications.netbeans.apache.org%3E", + "https://lists.apache.org/thread.html/rb5ac16fad337d1f3bb7079549f97d8166d0ef3082629417c39f12d63@%3Cnotifications.netbeans.apache.org%3E", + "https://nodesecurity.io/advisories/534", + "https://nvd.nist.gov/vuln/detail/CVE-2017-16137", + "https://www.cve.org/CVERecord?id=CVE-2017-16137" + ], + "PublishedDate": "2018-06-07T02:29:03.817Z", + "LastModifiedDate": "2023-11-07T02:40:28.13Z" + } + } + ] + } + ] +} diff --git a/integration/testdata/fixtures/convert/npm.json.golden b/integration/testdata/fixtures/convert/npm.json.golden new file mode 100644 index 000000000000..a576da82c72e --- /dev/null +++ b/integration/testdata/fixtures/convert/npm.json.golden @@ -0,0 +1,381 @@ +{ + "SchemaVersion": 2, + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/npm", + "ArtifactType": "repository", + "Metadata": { + "ImageConfig": { + "architecture": "", + "created": "0001-01-01T00:00:00Z", + "os": "", + "rootfs": { + "type": "", + "diff_ids": null + }, + "config": {} + } + }, + "Results": [ + { + "Target": "package-lock.json", + "Class": "lang-pkgs", + "Type": "npm", + "Packages": [ + { + "ID": "asap@2.0.6", + "Name": "asap", + "Identifier": { + "PURL": "pkg:npm/asap@2.0.6" + }, + "Version": "2.0.6", + "Layer": {}, + "Locations": [ + { + "StartLine": 6, + "EndLine": 10 + } + ] + }, + { + "ID": "jquery@3.3.9", + "Name": "jquery", + "Identifier": { + "PURL": "pkg:npm/jquery@3.3.9" + }, + "Version": "3.3.9", + "Licenses": [ + "MIT" + ], + "Layer": {}, + "Locations": [ + { + "StartLine": 11, + "EndLine": 15 + } + ] + }, + { + "ID": "js-tokens@4.0.0", + "Name": "js-tokens", + "Identifier": { + "PURL": "pkg:npm/js-tokens@4.0.0" + }, + "Version": "4.0.0", + "Layer": {}, + "Locations": [ + { + "StartLine": 16, + "EndLine": 20 + } + ] + }, + { + "ID": "loose-envify@1.4.0", + "Name": "loose-envify", + "Identifier": { + "PURL": "pkg:npm/loose-envify@1.4.0" + }, + "Version": "1.4.0", + "DependsOn": [ + "js-tokens@4.0.0" + ], + "Layer": {}, + "Locations": [ + { + "StartLine": 21, + "EndLine": 28 + } + ] + }, + { + "ID": "object-assign@4.1.1", + "Name": "object-assign", + "Identifier": { + "PURL": "pkg:npm/object-assign@4.1.1" + }, + "Version": "4.1.1", + "Layer": {}, + "Locations": [ + { + "StartLine": 29, + "EndLine": 33 + } + ] + }, + { + "ID": "promise@8.0.3", + "Name": "promise", + "Identifier": { + "PURL": "pkg:npm/promise@8.0.3" + }, + "Version": "8.0.3", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "asap@2.0.6" + ], + "Layer": {}, + "Locations": [ + { + "StartLine": 34, + "EndLine": 41 + } + ] + }, + { + "ID": "prop-types@15.7.2", + "Name": "prop-types", + "Identifier": { + "PURL": "pkg:npm/prop-types@15.7.2" + }, + "Version": "15.7.2", + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "react-is@16.8.6" + ], + "Layer": {}, + "Locations": [ + { + "StartLine": 42, + "EndLine": 51 + } + ] + }, + { + "ID": "react@16.8.6", + "Name": "react", + "Identifier": { + "PURL": "pkg:npm/react@16.8.6" + }, + "Version": "16.8.6", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "prop-types@15.7.2", + "scheduler@0.13.6" + ], + "Layer": {}, + "Locations": [ + { + "StartLine": 52, + "EndLine": 62 + } + ] + }, + { + "ID": "react-is@16.8.6", + "Name": "react-is", + "Identifier": { + "PURL": "pkg:npm/react-is@16.8.6" + }, + "Version": "16.8.6", + "Licenses": [ + "MIT" + ], + "Layer": {}, + "Locations": [ + { + "StartLine": 63, + "EndLine": 67 + } + ] + }, + { + "ID": "redux@4.0.1", + "Name": "redux", + "Identifier": { + "PURL": "pkg:npm/redux@4.0.1" + }, + "Version": "4.0.1", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "loose-envify@1.4.0", + "symbol-observable@1.2.0" + ], + "Layer": {}, + "Locations": [ + { + "StartLine": 68, + "EndLine": 76 + } + ] + }, + { + "ID": "scheduler@0.13.6", + "Name": "scheduler", + "Identifier": { + "PURL": "pkg:npm/scheduler@0.13.6" + }, + "Version": "0.13.6", + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1" + ], + "Layer": {}, + "Locations": [ + { + "StartLine": 77, + "EndLine": 85 + } + ] + }, + { + "ID": "symbol-observable@1.2.0", + "Name": "symbol-observable", + "Identifier": { + "PURL": "pkg:npm/symbol-observable@1.2.0" + }, + "Version": "1.2.0", + "Layer": {}, + "Locations": [ + { + "StartLine": 86, + "EndLine": 90 + } + ] + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-11358", + "PkgID": "jquery@3.3.9", + "PkgName": "jquery", + "PkgIdentifier": { + "PURL": "pkg:npm/jquery@3.3.9" + }, + "InstalledVersion": "3.3.9", + "FixedVersion": "3.4.0", + "Status": "fixed", + "Layer": {}, + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-11358", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Title": "jquery: Prototype pollution in object's prototype leading to denial of service, remote code execution, or property injection", + "Description": "jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-79" + ], + "VendorSeverity": { + "alma": 2, + "amazon": 2, + "arch-linux": 2, + "ghsa": 2, + "nodejs-security-wg": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ruby-advisory-db": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 4.3, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html", + "http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html", + "http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html", + "http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html", + "http://seclists.org/fulldisclosure/2019/May/10", + "http://seclists.org/fulldisclosure/2019/May/11", + "http://seclists.org/fulldisclosure/2019/May/13", + "http://www.openwall.com/lists/oss-security/2019/06/03/2", + "http://www.securityfocus.com/bid/108023", + "https://access.redhat.com/errata/RHBA-2019:1570", + "https://access.redhat.com/errata/RHSA-2019:1456", + "https://access.redhat.com/errata/RHSA-2019:2587", + "https://access.redhat.com/errata/RHSA-2019:3023", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://access.redhat.com/security/cve/CVE-2019-11358", + "https://backdropcms.org/security/backdrop-sa-core-2019-009", + "https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358", + "https://github.com/DanielRuf/snyk-js-jquery-174006?files=1", + "https://github.com/advisories/GHSA-6c3j-c64m-qhgq", + "https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b", + "https://github.com/jquery/jquery/pull/4333", + "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434", + "https://hackerone.com/reports/454365", + "https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601", + "https://linux.oracle.com/cve/CVE-2019-11358.html", + "https://linux.oracle.com/errata/ELSA-2020-4847.html", + "https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E", + "https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E", + "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", + "https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E", + "https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E", + "https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html", + "https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-11358", + "https://seclists.org/bugtraq/2019/Apr/32", + "https://seclists.org/bugtraq/2019/Jun/12", + "https://seclists.org/bugtraq/2019/May/18", + "https://security.netapp.com/advisory/ntap-20190919-0001/", + "https://snyk.io/vuln/SNYK-JS-JQUERY-174006", + "https://www.debian.org/security/2019/dsa-4434", + "https://www.debian.org/security/2019/dsa-4460", + "https://www.drupal.org/sa-core-2019-006", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/", + "https://www.synology.com/security/advisory/Synology_SA_19_19", + "https://www.tenable.com/security/tns-2019-08", + "https://www.tenable.com/security/tns-2020-02" + ], + "PublishedDate": "2019-04-20T00:29:00Z", + "LastModifiedDate": "2021-10-20T11:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/fixtures/db/almalinux.yaml b/integration/testdata/fixtures/db/almalinux.yaml new file mode 100644 index 000000000000..e7e463ebe389 --- /dev/null +++ b/integration/testdata/fixtures/db/almalinux.yaml @@ -0,0 +1,10 @@ +- bucket: alma 8 + pairs: + - bucket: openssl-libs + pairs: + - key: CVE-2021-3450 + value: + FixedVersion: 1:1.1.1g-15.el8_3 + - key: CVE-2021-3712 + value: + FixedVersion: 1:1.1.1k-5.el8_5 \ No newline at end of file diff --git a/integration/testdata/fixtures/db/alpine.yaml b/integration/testdata/fixtures/db/alpine.yaml new file mode 100644 index 000000000000..15e5308012a7 --- /dev/null +++ b/integration/testdata/fixtures/db/alpine.yaml @@ -0,0 +1,37 @@ +- bucket: alpine 3.9 + pairs: + - bucket: openssl + pairs: + - key: CVE-2019-1549 + value: + FixedVersion: 1.1.1d-r0 + - key: CVE-2019-1551 + value: + FixedVersion: 1.1.1d-r2 + - bucket: musl + pairs: + - key: CVE-2019-14697 + value: + FixedVersion: 1.1.20-r5 +- bucket: alpine 3.10 + pairs: + - bucket: openssl + pairs: + - key: CVE-2019-1549 + value: + FixedVersion: 1.1.1d-r0 + - key: CVE-2019-1551 + value: + FixedVersion: 1.1.1d-r2 + - bucket: musl + pairs: + - key: CVE-2019-14697 + value: + FixedVersion: 1.1.20-r5 +- bucket: alpine edge + pairs: + - bucket: git + pairs: + - key: CVE-2022-24765 + value: + FixedVersion: 2.35.2-r0 \ No newline at end of file diff --git a/integration/testdata/fixtures/db/amazon.yaml b/integration/testdata/fixtures/db/amazon.yaml new file mode 100644 index 000000000000..b1d71e016546 --- /dev/null +++ b/integration/testdata/fixtures/db/amazon.yaml @@ -0,0 +1,25 @@ +- bucket: amazon linux 1 + pairs: + - bucket: curl + pairs: + - key: CVE-2019-5436 + value: + FixedVersion: 7.61.1-11.91.amzn1 + - key: CVE-2019-5481 + value: + FixedVersion: 7.61.1-12.93.amzn1 +- bucket: amazon linux 2 + pairs: + - bucket: curl + pairs: + - key: CVE-2019-5436 + value: + FixedVersion: 7.61.1-11.amzn2.0.2 + - key: CVE-2019-5481 + value: + FixedVersion: 7.61.1-12.amzn2.0.1 + - bucket: bind-export-libs + pairs: + - key: CVE-2022-38177 + value: + FixedVersion: 99:9.11.4-26.P2.amzn2.13 diff --git a/integration/testdata/fixtures/db/cocoapods.yaml b/integration/testdata/fixtures/db/cocoapods.yaml new file mode 100644 index 000000000000..3b218912e36a --- /dev/null +++ b/integration/testdata/fixtures/db/cocoapods.yaml @@ -0,0 +1,14 @@ +- bucket: "cocoapods::GitHub Security Advisory Cocoapods" + pairs: + - bucket: _NIODataStructures + pairs: + - key: CVE-2022-3215 + value: + PatchedVersions: + - "2.29.1" + - "2.39.1" + - "2.42.0" + VulnerableVersions: + - "< 2.29.1" + - ">= 2.39.0, < 2.39.1" + - ">= 2.41.0, < 2.42.0" \ No newline at end of file diff --git a/integration/testdata/fixtures/db/composer.yaml b/integration/testdata/fixtures/db/composer.yaml new file mode 100644 index 000000000000..638c73a838fb --- /dev/null +++ b/integration/testdata/fixtures/db/composer.yaml @@ -0,0 +1,10 @@ +- bucket: "composer::GitHub Security Advisory Composer" + pairs: + - bucket: guzzlehttp/psr7 + pairs: + - key: CVE-2022-24775 + value: + PatchedVersions: + - 1.8.4 + VulnerableVersions: + - < 1.8.4 \ No newline at end of file diff --git a/integration/testdata/fixtures/db/conan.yaml b/integration/testdata/fixtures/db/conan.yaml new file mode 100644 index 000000000000..9d7089ee8f71 --- /dev/null +++ b/integration/testdata/fixtures/db/conan.yaml @@ -0,0 +1,10 @@ +- bucket: conan::GitLab Advisory Database Community + pairs: + - bucket: pcre + pairs: + - key: CVE-2020-14155 + value: + PatchedVersions: + - "8.45" + VulnerableVersions: + - "<8.44" diff --git a/integration/testdata/fixtures/db/cpe.yaml b/integration/testdata/fixtures/db/cpe.yaml new file mode 100644 index 000000000000..174e63ee286c --- /dev/null +++ b/integration/testdata/fixtures/db/cpe.yaml @@ -0,0 +1,21 @@ +- bucket: Red Hat CPE + pairs: + - bucket: repository + pairs: + - key: "rhel-6-server-rpms" + value: + - 857 + - key: "rhel-7-server-rpms" + value: + - 869 + - bucket: nvr + pairs: + - key: "ubi7-container-7.7-140-x86_64" + value: + - 869 + - bucket: cpe + pairs: + - key: "857" + value: "cpe:/o:redhat:enterprise_linux:6::server" + - key: "869" + value: "cpe:/o:redhat:enterprise_linux:7::server" diff --git a/integration/testdata/fixtures/db/data-source.yaml b/integration/testdata/fixtures/db/data-source.yaml new file mode 100644 index 000000000000..684d42191040 --- /dev/null +++ b/integration/testdata/fixtures/db/data-source.yaml @@ -0,0 +1,151 @@ +- bucket: data-source + pairs: + - key: "composer::GitHub Security Advisory Composer" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Composer" + URL: "https://github.com/advisories?query=type%%3Areviewed+ecosystem%%3Acomposer" + - key: "maven::GitHub Security Advisory Maven" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Maven" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven" + - key: "npm::GitHub Security Advisory Npm" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Npm" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + - key: "nuget::GitHub Security Advisory Nuget" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Nuget" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anuget" + - key: "pip::GitHub Security Advisory Pip" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Pip" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + - key: "rubygems::GitHub Security Advisory RubyGems" + value: + ID: "ghsa" + Name: "GitHub Security Advisory RubyGems" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Arubygems" + - key: "pub::GitHub Security Advisory Pub" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Pub" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apub" + - key: "erlang::GitHub Security Advisory Erlang" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Erlang" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Aerlang" + - key: "go::GitHub Security Advisory Go" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Go" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + - key: Oracle Linux 8 + value: + ID: "oracle-oval" + Name: "Oracle Linux OVAL definitions" + URL: "https://linux.oracle.com/security/oval/" + - key: Photon OS 3.0 + value: + ID: "photon" + Name: "Photon OS CVE metadata" + URL: "https://packages.broadcom.com/photon/photon_cve_metadata/" + - key: alma 8 + value: + ID: "alma" + Name: "AlmaLinux Product Errata" + URL: "https://errata.almalinux.org/" + - key: alpine 3.9 + value: + ID: "alpine" + Name: "Alpine Secdb" + URL: "https://secdb.alpinelinux.org/" + - key: alpine 3.10 + value: + ID: "alpine" + Name: "Alpine Secdb" + URL: "https://secdb.alpinelinux.org/" + - key: amazon linux 1 + value: + ID: "amazon" + Name: "Amazon Linux Security Center" + URL: "https://alas.aws.amazon.com/" + - key: amazon linux 2 + value: + ID: "amazon" + Name: "Amazon Linux Security Center" + URL: "https://alas.aws.amazon.com/" + - key: cargo::Open Source Vulnerability + value: + Name: "RustSec Advisory Database" + URL: "https://github.com/RustSec/advisory-db" + - key: debian 10 + value: + ID: "debian" + Name: "Debian Security Tracker" + URL: "https://salsa.debian.org/security-tracker-team/security-tracker" + - key: debian 9 + value: + ID: "debian" + Name: "Debian Security Tracker" + URL: "https://salsa.debian.org/security-tracker-team/security-tracker" + - key: maven::GitLab Advisory Database Community + value: + ID: "glad" + Name: "GitLab Advisory Database Community" + URL: "https://gitlab.com/gitlab-org/advisories-community" + - key: npm::nodejs-security-wg + value: + ID: "nodejs-security-wg" + Name: "Node.js Ecosystem Security Working Group" + URL: "https://github.com/nodejs/security-wg" + - key: openSUSE Leap 15.0 + value: + ID: "suse-cvrf" + Name: "SUSE CVRF" + URL: "https://ftp.suse.com/pub/projects/security/cvrf/" + - key: openSUSE Leap 15.1 + value: + ID: "suse-cvrf" + Name: "SUSE CVRF" + URL: "https://ftp.suse.com/pub/projects/security/cvrf/" + - key: composer::php-security-advisories + value: + ID: "php-security-advisories" + Name: "PHP Security Advisories Database" + URL: "https://github.com/FriendsOfPHP/security-advisories" + - key: pip::Open Source Vulnerability + value: + ID: "osv" + Name: "Python Packaging Advisory Database" + URL: "https://github.com/pypa/advisory-db" + - key: rocky 8 + value: + ID: "rocky" + Name: "Rocky Linux updateinfo" + URL: "https://download.rockylinux.org/pub/rocky/" + - key: rubygems::ruby-advisory-db + value: + ID: "ruby-advisory-db" + Name: "Ruby Advisory Database" + URL: "https://github.com/rubysec/ruby-advisory-db" + - key: ubuntu 18.04 + value: + ID: "ubuntu" + Name: "Ubuntu CVE Tracker" + URL: "https://git.launchpad.net/ubuntu-cve-tracker" + - key: CBL-Mariner 1.0 + value: + ID: "cbl-mariner" + Name: "CBL-Mariner Vulnerability Data" + URL: "https://github.com/microsoft/CBL-MarinerVulnerabilityData" + - key: k8s::Official Kubernetes CVE Feed + value: + ID: "k8s" + Name: "Official Kubernetes CVE Feed" + URL: "https://kubernetes.io/docs/reference/issues-security/official-cve-feed/index.json" diff --git a/integration/testdata/fixtures/db/debian.yaml b/integration/testdata/fixtures/db/debian.yaml new file mode 100644 index 000000000000..a357227473f8 --- /dev/null +++ b/integration/testdata/fixtures/db/debian.yaml @@ -0,0 +1,41 @@ +- bucket: debian 9 + pairs: + - bucket: bash + pairs: + - key: CVE-2019-18276 + value: + Severity: 1.0 + Status: 7 # changed for test + - bucket: openssl + pairs: + - key: CVE-2019-1551 + value: + State: postponed + - bucket: e2fsprogs + pairs: + - key: CVE-2019-5094 + value: + FixedVersion: 1.43.4-2+deb9u1 + VendorIDs: + - DSA-4535-1 + - bucket: openssl + pairs: + - key: CVE-2019-1563 + value: + FixedVersion: 1.1.0l-1~deb9u1 + VendorIDs: + - DSA-4539-1 +- bucket: debian 10 + pairs: + - bucket: bash + pairs: + - key: CVE-2019-18276 + value: + Severity: 1 + - bucket: libidn2 + pairs: + - key: CVE-2019-18224 + value: + FixedVersion: 2.0.5-1+deb10u1 + VendorIDs: + - DSA-4613-1 \ No newline at end of file diff --git a/integration/testdata/fixtures/db/erlang.yaml b/integration/testdata/fixtures/db/erlang.yaml new file mode 100644 index 000000000000..a5fd04d19359 --- /dev/null +++ b/integration/testdata/fixtures/db/erlang.yaml @@ -0,0 +1,10 @@ +- bucket: erlang::GitHub Security Advisory Erlang + pairs: + - bucket: phoenix + pairs: + - key: CVE-2022-42975 + value: + PatchedVersions: + - 1.6.14 + VulnerableVersions: + - "<= 1.6.14" \ No newline at end of file diff --git a/integration/testdata/fixtures/db/go.yaml b/integration/testdata/fixtures/db/go.yaml new file mode 100644 index 000000000000..0b7696c28db6 --- /dev/null +++ b/integration/testdata/fixtures/db/go.yaml @@ -0,0 +1,26 @@ +- bucket: go::GitHub Security Advisory Go + pairs: + - bucket: golang.org/x/text + pairs: + - key: CVE-2021-38561 + value: + PatchedVersions: + - "0.3.7" + VulnerableVersions: + - ">= 0, < 0.3.7" + - bucket: github.com/docker/distribution + pairs: + - key: GMS-2022-20 + value: + PatchedVersions: + - "v2.8.0" + VulnerableVersions: + - "< v2.8.0" + - bucket: github.com/open-policy-agent/opa + pairs: + - key: CVE-2022-23628 + value: + PatchedVersions: + - "0.37.0" + VulnerableVersions: + - ">= 0.33.1, < 0.37.0" diff --git a/integration/testdata/fixtures/db/java.yaml b/integration/testdata/fixtures/db/java.yaml new file mode 100644 index 000000000000..0ff255077d46 --- /dev/null +++ b/integration/testdata/fixtures/db/java.yaml @@ -0,0 +1,28 @@ +- bucket: maven::GitHub Security Advisory Maven + pairs: + - bucket: com.fasterxml.jackson.core:jackson-databind + pairs: + - key: CVE-2020-9548 + value: + PatchedVersions: + - 2.9.10.4 + VulnerableVersions: + - ">= 2.0.0, <= 2.9.10.3" + - bucket: org.springframework:spring-beans + pairs: + - key: CVE-2022-22965 + value: + PatchedVersions: + - 5.3.18 + VulnerableVersions: + - ">= 5.3.0, < 5.3.18" +- bucket: maven::GitLab Advisory Database Community + pairs: + - bucket: com.fasterxml.jackson.core:jackson-databind + pairs: + - key: CVE-2021-20190 + value: + PatchedVersions: + - 2.9.10.7 + VulnerableVersions: + - "[2.9.0,2.9.10.7)" diff --git a/integration/testdata/fixtures/db/k8s.yaml b/integration/testdata/fixtures/db/k8s.yaml new file mode 100644 index 000000000000..4fa27ca07828 --- /dev/null +++ b/integration/testdata/fixtures/db/k8s.yaml @@ -0,0 +1,16 @@ +- bucket: "k8s::Official Kubernetes CVE Feed" + pairs: + - bucket: k8s.io/kubelet + pairs: + - key: CVE-2023-2431 + value: + PatchedVersions: + - 1.24.14 + - 1.25.9 + - 1.26.4 + - 1.27.1 + VulnerableVersions: + - "< 1.24.14" + - ">= 1.25.0, < 1.25.9" + - ">= 1.26.0, < 1.26.4" + - ">= 1.27.0, < 1.27.1" diff --git a/integration/testdata/fixtures/db/mariner.yaml b/integration/testdata/fixtures/db/mariner.yaml new file mode 100644 index 000000000000..9475840c50a3 --- /dev/null +++ b/integration/testdata/fixtures/db/mariner.yaml @@ -0,0 +1,8 @@ +- bucket: CBL-Mariner 1.0 + pairs: + - bucket: vim + pairs: + - key: CVE-2022-0158 + value: + FixedVersion: 0:8.2.4082-1.cm1 + - key: CVE-2022-0261 diff --git a/integration/testdata/fixtures/db/nodejs.yaml b/integration/testdata/fixtures/db/nodejs.yaml new file mode 100644 index 000000000000..af22671978c9 --- /dev/null +++ b/integration/testdata/fixtures/db/nodejs.yaml @@ -0,0 +1,24 @@ +- bucket: "npm::GitHub Security Advisory Npm" + pairs: + - bucket: jquery + pairs: + - key: CVE-2015-9251 + value: + PatchedVersions: + - 3.0.0 + VulnerableVersions: + - < 3.0.0 + - key: CVE-2019-11358 + value: + PatchedVersions: + - 3.4.0 + VulnerableVersions: + - < 3.4.0 + - bucket: lodash + pairs: + - key: CVE-2019-10744 + value: + PatchedVersions: + - 4.17.12 + VulnerableVersions: + - < 4.17.12 diff --git a/integration/testdata/fixtures/db/nuget.yaml b/integration/testdata/fixtures/db/nuget.yaml new file mode 100644 index 000000000000..97bb06f93ce4 --- /dev/null +++ b/integration/testdata/fixtures/db/nuget.yaml @@ -0,0 +1,10 @@ +- bucket: "nuget::GitHub Security Advisory Nuget" + pairs: + - bucket: newtonsoft.json + pairs: + - key: GHSA-5crp-9r3c-p9vr + value: + PatchedVersions: + - 13.0.1 + VulnerableVersions: + - < 13.0.1 \ No newline at end of file diff --git a/integration/testdata/fixtures/db/opensuse.yaml b/integration/testdata/fixtures/db/opensuse.yaml new file mode 100644 index 000000000000..76518a73d883 --- /dev/null +++ b/integration/testdata/fixtures/db/opensuse.yaml @@ -0,0 +1,19 @@ +- bucket: "openSUSE Leap 15.1" + pairs: + - bucket: libopenssl1_1 + pairs: + - key: "openSUSE-SU-2020:0062-1" + value: + FixedVersion: 1.1.0i-lp151.8.6.1 + - bucket: openssl-1_1 + pairs: + - key: "openSUSE-SU-2020:0062-1" + value: + FixedVersion: 1.1.0i-lp151.8.6.1 +- bucket: "openSUSE Tumbleweed" + pairs: + - bucket: libopenssl3 + pairs: + - key: "openSUSE-SU-2024:13065-1" + value: + FixedVersion: 3.1.5-9.1 # changed for test diff --git a/integration/testdata/fixtures/db/oracle.yaml b/integration/testdata/fixtures/db/oracle.yaml new file mode 100644 index 000000000000..88745d05b23d --- /dev/null +++ b/integration/testdata/fixtures/db/oracle.yaml @@ -0,0 +1,20 @@ +- bucket: Oracle Linux 8 + pairs: + - bucket: curl + pairs: + - key: CVE-2019-3823 + value: + FixedVersion: "7.61.1-11.el8" + Entries: + - FixedVersion: "7.61.1-11.el8" + Arches: + - x86_64 + - aarch64 + - key: CVE-2019-5436 + value: + FixedVersion: "7.61.1-12.el8" + Entries: + - FixedVersion: "7.61.1-12.el8" + Arches: + - x86_64 + - aarch64 diff --git a/integration/testdata/fixtures/db/photon.yaml b/integration/testdata/fixtures/db/photon.yaml new file mode 100644 index 000000000000..ccc2abfa2576 --- /dev/null +++ b/integration/testdata/fixtures/db/photon.yaml @@ -0,0 +1,12 @@ +- bucket: Photon OS 3.0 + pairs: + - bucket: bash + pairs: + - key: CVE-2019-18276 + value: + FixedVersion: 4.4.18-2.ph3 + - bucket: curl + pairs: + - key: CVE-2019-5481 + value: + FixedVersion: 7.61.1-5.ph3 \ No newline at end of file diff --git a/integration/testdata/fixtures/db/pub.yaml b/integration/testdata/fixtures/db/pub.yaml new file mode 100644 index 000000000000..603bc82cf843 --- /dev/null +++ b/integration/testdata/fixtures/db/pub.yaml @@ -0,0 +1,10 @@ +- bucket: pub::GitHub Security Advisory Pub + pairs: + - bucket: http + pairs: + - key: CVE-2020-35669 + value: + PatchedVersions: + - 0.13.3 + VulnerableVersions: + - "<= 0.13.3" \ No newline at end of file diff --git a/integration/testdata/fixtures/db/python.yaml b/integration/testdata/fixtures/db/python.yaml new file mode 100644 index 000000000000..728f11b67e6d --- /dev/null +++ b/integration/testdata/fixtures/db/python.yaml @@ -0,0 +1,24 @@ +- bucket: "pip::GitHub Security Advisory Pip" + pairs: + - bucket: werkzeug + pairs: + - key: CVE-2019-14806 + value: + PatchedVersions: + - 0.15.3 + VulnerableVersions: + - < 0.15.3 + - key: CVE-2020-28724 + value: + PatchedVersions: + - 0.11.6 + VulnerableVersions: + - < 0.11.6 + - bucket: setuptools + pairs: + - key: CVE-2022-40897 + value: + PatchedVersions: + - 65.5.1 + VulnerableVersions: + - < 65.5.1 \ No newline at end of file diff --git a/integration/testdata/fixtures/db/redhat.yaml b/integration/testdata/fixtures/db/redhat.yaml new file mode 100644 index 000000000000..2d07860741b6 --- /dev/null +++ b/integration/testdata/fixtures/db/redhat.yaml @@ -0,0 +1,77 @@ +- bucket: Red Hat + pairs: + - bucket: bash + pairs: + - key: CVE-2019-18276 + value: + Entries: + - Affected: + - 596 + - 597 + - 598 + - 601 + - 602 + - 869 + - 870 + - 924 + Cves: + - Severity: 1.0 + Arches: + - x86_64 + Status: 5 + - bucket: openssl + pairs: + - key: RHSA-2019:2304 + value: + Entries: + - Affected: + - 859 + - 860 + - 862 + - 869 + Cves: + - ID: CVE-2018-0734 + Severity: 1.0 + - ID: CVE-2019-1559 + Severity: 2.0 + FixedVersion: 1:1.0.2k-19.el7 + - key: RHSA-2019:2471 + value: + Entries: + - Affected: + - 855 + - 857 + - 858 + - 924 + Cves: + - ID: CVE-2019-1559 + Severity: 2.0 + FixedVersion: 0:1.0.1e-58.el6_10 + - bucket: openssl-libs + pairs: + - key: RHSA-2019:2304 + value: + Entries: + - Affected: + - 859 + - 860 + - 862 + - 869 + Cves: + - ID: CVE-2018-0734 + Severity: 1.0 + - ID: CVE-2019-1559 + Severity: 2.0 + FixedVersion: 1:1.0.2k-19.el7 + - bucket: glibc + pairs: + - key: CVE-2020-29573 + value: + Entries: + - Affected: + - 596 + - 857 + - 858 + Cves: + - Severity: 2.0 + Status: 7 \ No newline at end of file diff --git a/integration/testdata/fixtures/db/rockylinux.yaml b/integration/testdata/fixtures/db/rockylinux.yaml new file mode 100644 index 000000000000..7a18405eec90 --- /dev/null +++ b/integration/testdata/fixtures/db/rockylinux.yaml @@ -0,0 +1,13 @@ +- bucket: rocky 8 + pairs: + - bucket: openssl-libs + pairs: + - key: CVE-2021-3712 + value: + FixedVersion: 1:1.1.1k-5.el8_5 + Entries: + - FixedVersion: "1:1.1.1k-5.el8_5" + Arches: + - x86_64 + VendorIds: + - RLSA-2021:4647 \ No newline at end of file diff --git a/integration/testdata/fixtures/db/ruby.yaml b/integration/testdata/fixtures/db/ruby.yaml new file mode 100644 index 000000000000..802c7b9b05fd --- /dev/null +++ b/integration/testdata/fixtures/db/ruby.yaml @@ -0,0 +1,37 @@ +- bucket: "rubygems::GitHub Security Advisory RubyGems" + pairs: + - bucket: activesupport + pairs: + - key: CVE-2015-3226 + value: + PatchedVersions: + - 3.2.22.5 + - 4.2.2 + - 4.1.11 + VulnerableVersions: + - ">= 3.0.0, <= 3.2.22.4" + - ">= 4.2.0, < 4.2.2" + - ">= 4.1.0, < 4.1.11" + - key: CVE-2015-3227 + value: + PatchedVersions: + - 4.2.2 + - 4.1.11 + VulnerableVersions: + - ">= 4.2.0, < 4.2.2" + - < 4.1.11 + - key: CVE-2020-8165 + value: + PatchedVersions: + - 6.0.3.1 + - 5.2.4.3 + VulnerableVersions: + - ">= 6.0.0, <= 6.0.3" + - ">= 5.0.0, <= 5.2.4.2" + - bucket: http_parser + pairs: + - key: CVE-2020-9999 + value: + VulnerableVersions: + - ">= 0" + diff --git a/integration/testdata/fixtures/db/rust.yaml b/integration/testdata/fixtures/db/rust.yaml new file mode 100644 index 000000000000..a6c80c4358e4 --- /dev/null +++ b/integration/testdata/fixtures/db/rust.yaml @@ -0,0 +1,13 @@ +- bucket: cargo::Open Source Vulnerability + pairs: + - bucket: ammonia + pairs: + - key: CVE-2019-15542 + value: + PatchedVersions: + - ">= 2.1.0" + - key: CVE-2021-38193 + value: + PatchedVersions: + - ">= 3.1.0" + - ">= 2.1.3, < 3.0.0" diff --git a/integration/testdata/fixtures/db/suse.yaml b/integration/testdata/fixtures/db/suse.yaml new file mode 100644 index 000000000000..690360c5de13 --- /dev/null +++ b/integration/testdata/fixtures/db/suse.yaml @@ -0,0 +1,19 @@ +- bucket: "SUSE Linux Enterprise 15-SP3" + pairs: + - bucket: libopenssl1_1 + pairs: + - key: "SUSE-SU-2022:2251-1" + value: + FixedVersion: 1.1.1d-150200.11.48.1 + - bucket: openssl-1_1 + pairs: + - key: "SUSE-SU-2022:2251-1" + value: + FixedVersion: 1.1.1d-150200.11.48.1 +- bucket: "SUSE Linux Enterprise Micro 5.3" + pairs: + - bucket: libopenssl1_1 + pairs: + - key: "SUSE-SU-2023:0311-1" + value: + FixedVersion: 1.1.1l-150400.7.22.1 diff --git a/integration/testdata/fixtures/db/swift.yaml b/integration/testdata/fixtures/db/swift.yaml new file mode 100644 index 000000000000..f380da408a33 --- /dev/null +++ b/integration/testdata/fixtures/db/swift.yaml @@ -0,0 +1,14 @@ +- bucket: "swift::GitHub Security Advisory Swift" + pairs: + - bucket: github.com/apple/swift-nio + pairs: + - key: CVE-2022-3215 + value: + PatchedVersions: + - "2.29.1" + - "2.39.1" + - "2.42.0" + VulnerableVersions: + - "< 2.29.1" + - ">= 2.39.0, < 2.39.1" + - ">= 2.41.0, < 2.42.0" \ No newline at end of file diff --git a/integration/testdata/fixtures/db/ubuntu.yaml b/integration/testdata/fixtures/db/ubuntu.yaml new file mode 100644 index 000000000000..d0de918d0476 --- /dev/null +++ b/integration/testdata/fixtures/db/ubuntu.yaml @@ -0,0 +1,20 @@ +- bucket: ubuntu 18.04 + pairs: + - bucket: bash + pairs: + - key: CVE-2016-9401 + value: + FixedVersion: 4.4-5ubuntu1 + - key: CVE-2019-18276 + value: {} + - bucket: e2fsprogs + pairs: + - key: CVE-2019-5094 + value: + FixedVersion: 1.44.1-1ubuntu1.2 +- bucket: ubuntu 22.04 + pairs: + - bucket: bash + pairs: + - key: CVE-2022-3715 + value: {} \ No newline at end of file diff --git a/integration/testdata/fixtures/db/vulnerability.yaml b/integration/testdata/fixtures/db/vulnerability.yaml new file mode 100644 index 000000000000..e18d731a30c8 --- /dev/null +++ b/integration/testdata/fixtures/db/vulnerability.yaml @@ -0,0 +1,1430 @@ +- bucket: vulnerability + pairs: + - key: CVE-2022-38177 + value: + Title: "bind: memory leak in ECDSA DNSSEC verification code" + Description: "By spoofing the target resolver with responses that have a malformed ECDSA signature, an attacker can trigger a small memory leak. It is possible to gradually erode available memory to the point where named crashes for lack of resources." + Severity: HIGH + CVSS: + nvd: + V3Score: 7.5 + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N" + redhat: + V3Score: 7.5 + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:N/A:N" + LastModifiedDate: "2022-09-21T11:15:00Z" + PublishedDate: "2022-09-21T11:15:00Z" + References: + - "http://www.openwall.com/lists/oss-security/2022/09/21/3" + - "https://access.redhat.com/errata/RHSA-2022:6763" + - "https://access.redhat.com/hydra/rest/securitydata/cve/CVE-2022-38177.json" + - "https://access.redhat.com/hydra/rest/securitydata/cve/CVE-2022-38178.json" + - "https://access.redhat.com/security/cve/CVE-2022-38177" + VendorSeverity: + arch-linux: 2 + nvd: 2 + redhat: 2 + ubuntu: 2 + - key: CVE-2022-3715 + value: + Title: a heap-buffer-overflow in valid_parameter_transform + Severity: LOW + Description: A flaw was found in the bash package, where a heap-buffer overflow can occur in valid parameter_transform. This issue may lead to memory problems. + CVSS: + nvd: + V3Score: 7.8 + V3Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + redhat: + V3Score: 6.6 + V3Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:H + CweIDs: + - CWE-787 + LastModifiedDate: 2023-02-24T18:38:00Z + PublishedDate: 2023-01-05T15:15:00Z + References: + - https://access.redhat.com/errata/RHSA-2023:0340 + - https://access.redhat.com/security/cve/CVE-2022-3715 + - https://bugzilla.redhat.com/2126720 + - https://bugzilla.redhat.com/show_bug.cgi?id=2126720 + VendorSeverity: + cbl-mariner: 3.0 + nvd: 3.0 + photon: 3.0 + redhat: 1.0 + ubuntu: 2.0 + - key: CVE-2016-9401 + value: + CVSS: + nvd: + V2Score: 2.1 + V2Vector: AV:L/AC:L/Au:N/C:N/I:N/A:P + V3Score: 5.5 + V3Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H + redhat: + V2Score: 1.9 + V2Vector: AV:L/AC:M/Au:N/C:N/I:N/A:P + V3Score: 3.3 + V3Vector: CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L + CweIDs: + - CWE-416 + Description: popd in bash might allow local users to bypass the restricted shell and cause a use-after-free via a crafted address. + LastModifiedDate: 2020-09-14T18:32:00Z + PublishedDate: 2017-01-23T21:59:00Z + References: + - http://rhn.redhat.com/errata/RHSA-2017-0725.html + - http://www.openwall.com/lists/oss-security/2016/11/17/5 + - http://www.openwall.com/lists/oss-security/2016/11/17/9 + - http://www.securityfocus.com/bid/94398 + - https://access.redhat.com/errata/RHSA-2017:1931 + - https://access.redhat.com/security/cve/CVE-2016-9401 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2016-9401 + - https://linux.oracle.com/cve/CVE-2016-9401.html + - https://linux.oracle.com/errata/ELSA-2017-1931.html + - https://lists.debian.org/debian-lts-announce/2019/03/msg00028.html + - https://security.gentoo.org/glsa/201701-02 + - https://ubuntu.com/security/notices/USN-3294-1 + Severity: MEDIUM + Title: "bash: popd controlled free" + VendorSeverity: + amazon: 2.0 + nvd: 2.0 + oracle-oval: 2.0 + photon: 2.0 + redhat: 1.0 + ubuntu: 1.0 + - key: CVE-2018-0734 + value: + CVSS: + nvd: + V2Score: 4.3 + V2Vector: AV:N/AC:M/Au:N/C:P/I:N/A:N + V3Score: 5.9 + V3Vector: CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N + redhat: + V3Score: 5.1 + V3Vector: CVSS:3.0/AV:L/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N + CweIDs: + - CWE-327 + Description: The OpenSSL DSA signature algorithm has been shown to be vulnerable to a timing side channel attack. An attacker could use variations in the signing algorithm to recover the private key. Fixed in OpenSSL 1.1.1a (Affected 1.1.1). Fixed in OpenSSL 1.1.0j (Affected 1.1.0-1.1.0i). Fixed in OpenSSL 1.0.2q (Affected 1.0.2-1.0.2p). + LastModifiedDate: 2020-08-24T17:37:00Z + PublishedDate: 2018-10-30T12:29:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00030.html + - http://lists.opensuse.org/opensuse-security-announce/2019-07/msg00056.html + - http://www.securityfocus.com/bid/105758 + - https://access.redhat.com/errata/RHSA-2019:2304 + - https://access.redhat.com/errata/RHSA-2019:3700 + - https://access.redhat.com/errata/RHSA-2019:3932 + - https://access.redhat.com/errata/RHSA-2019:3933 + - https://access.redhat.com/errata/RHSA-2019:3935 + - https://access.redhat.com/security/cve/CVE-2018-0734 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-0734 + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=43e6a58d4991a451daf4891ff05a48735df871ac + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=8abfe72e8c1de1b95f50aa0d9134803b4d00070f + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=ef11e19d1365eea2b1851e6f540a0bf365d303e7 + - https://linux.oracle.com/cve/CVE-2018-0734.html + - https://linux.oracle.com/errata/ELSA-2019-3700.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/ + - https://nodejs.org/en/blog/vulnerability/november-2018-security-releases/ + - https://nvd.nist.gov/vuln/detail/CVE-2018-0734 + - https://security.netapp.com/advisory/ntap-20181105-0002/ + - https://security.netapp.com/advisory/ntap-20190118-0002/ + - https://security.netapp.com/advisory/ntap-20190423-0002/ + - https://ubuntu.com/security/notices/USN-3840-1 + - https://usn.ubuntu.com/3840-1/ + - https://www.debian.org/security/2018/dsa-4348 + - https://www.debian.org/security/2018/dsa-4355 + - https://www.openssl.org/news/secadv/20181030.txt + - https://www.oracle.com/security-alerts/cpuapr2020.html + - https://www.oracle.com/security-alerts/cpujan2020.html + - https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html + - https://www.oracle.com/technetwork/security-advisory/cpujan2019-5072801.html + - https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html + - https://www.tenable.com/security/tns-2018-16 + - https://www.tenable.com/security/tns-2018-17 + Severity: MEDIUM + Title: "openssl: timing side channel attack in the DSA signature algorithm" + VendorSeverity: + amazon: 2.0 + arch-linux: 1.0 + cbl-mariner: 2.0 + nvd: 2.0 + oracle-oval: 1.0 + photon: 2.0 + redhat: 1.0 + ubuntu: 1.0 + - key: CVE-2019-10744 + value: + CVSS: + nvd: + V2Score: 6.4 + V2Vector: AV:N/AC:L/Au:N/C:N/I:P/A:P + V3Score: 9.1 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H + redhat: + V3Score: 9.1 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H + Description: Versions of lodash lower than 4.17.12 are vulnerable to Prototype Pollution. The function defaultsDeep could be tricked into adding or modifying properties of Object.prototype using a constructor payload. + LastModifiedDate: 2021-03-16T13:57:00Z + PublishedDate: 2019-07-26T00:15:00Z + References: + - https://access.redhat.com/errata/RHSA-2019:3024 + - https://access.redhat.com/security/cve/CVE-2019-10744 + - https://github.com/advisories/GHSA-jf85-cpcp-j695 + - https://github.com/lodash/lodash/pull/4336 + - https://nvd.nist.gov/vuln/detail/CVE-2019-10744 + - https://security.netapp.com/advisory/ntap-20191004-0005/ + - https://snyk.io/vuln/SNYK-JS-LODASH-450202 + - https://support.f5.com/csp/article/K47105354?utm_source=f5support&utm_medium=RSS + - https://www.npmjs.com/advisories/1065 + - https://www.oracle.com/security-alerts/cpujan2021.html + - https://www.oracle.com/security-alerts/cpuoct2020.html + Severity: CRITICAL + Title: "nodejs-lodash: prototype pollution in defaultsDeep function leading to modifying properties" + VendorSeverity: + ghsa: 4.0 + nvd: 4.0 + redhat: 3.0 + - key: CVE-2019-11358 + value: + CVSS: + nvd: + V2Score: 4.3 + V2Vector: AV:N/AC:M/Au:N/C:N/I:P/A:N + V3Score: 6.1 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N + redhat: + V3Score: 5.6 + V3Vector: CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L + CweIDs: + - CWE-79 + Description: jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype. + LastModifiedDate: 2021-10-20T11:15:00Z + PublishedDate: 2019-04-20T00:29:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html + - http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html + - http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html + - http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html + - http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html + - http://seclists.org/fulldisclosure/2019/May/10 + - http://seclists.org/fulldisclosure/2019/May/11 + - http://seclists.org/fulldisclosure/2019/May/13 + - http://www.openwall.com/lists/oss-security/2019/06/03/2 + - http://www.securityfocus.com/bid/108023 + - https://access.redhat.com/errata/RHBA-2019:1570 + - https://access.redhat.com/errata/RHSA-2019:1456 + - https://access.redhat.com/errata/RHSA-2019:2587 + - https://access.redhat.com/errata/RHSA-2019:3023 + - https://access.redhat.com/errata/RHSA-2019:3024 + - https://access.redhat.com/security/cve/CVE-2019-11358 + - https://backdropcms.org/security/backdrop-sa-core-2019-009 + - https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/ + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358 + - https://github.com/DanielRuf/snyk-js-jquery-174006?files=1 + - https://github.com/advisories/GHSA-6c3j-c64m-qhgq + - https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b + - https://github.com/jquery/jquery/pull/4333 + - "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434" + - https://hackerone.com/reports/454365 + - https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601 + - https://linux.oracle.com/cve/CVE-2019-11358.html + - https://linux.oracle.com/errata/ELSA-2020-4847.html + - https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E + - https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E + - https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E + - https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E + - https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E + - https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E + - https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E + - https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E + - https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E + - https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E + - https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E + - https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E + - https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E + - https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E + - https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E + - https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E + - https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E + - https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E + - https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E + - https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html + - https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html + - https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/ + - https://nvd.nist.gov/vuln/detail/CVE-2019-11358 + - https://seclists.org/bugtraq/2019/Apr/32 + - https://seclists.org/bugtraq/2019/Jun/12 + - https://seclists.org/bugtraq/2019/May/18 + - https://security.netapp.com/advisory/ntap-20190919-0001/ + - https://snyk.io/vuln/SNYK-JS-JQUERY-174006 + - https://www.debian.org/security/2019/dsa-4434 + - https://www.debian.org/security/2019/dsa-4460 + - https://www.drupal.org/sa-core-2019-006 + - https://www.oracle.com//security-alerts/cpujul2021.html + - https://www.oracle.com/security-alerts/cpuApr2021.html + - https://www.oracle.com/security-alerts/cpuapr2020.html + - https://www.oracle.com/security-alerts/cpujan2020.html + - https://www.oracle.com/security-alerts/cpujan2021.html + - https://www.oracle.com/security-alerts/cpujul2020.html + - https://www.oracle.com/security-alerts/cpuoct2020.html + - https://www.oracle.com/security-alerts/cpuoct2021.html + - https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html + - https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html + - https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/ + - https://www.synology.com/security/advisory/Synology_SA_19_19 + - https://www.tenable.com/security/tns-2019-08 + - https://www.tenable.com/security/tns-2020-02 + Severity: MEDIUM + Title: "jquery: Prototype pollution in object's prototype leading to denial of service, remote code execution, or property injection" + VendorSeverity: + alma: 2.0 + amazon: 2.0 + arch-linux: 2.0 + ghsa: 2.0 + nodejs-security-wg: 2.0 + nvd: 2.0 + oracle-oval: 2.0 + redhat: 2.0 + ruby-advisory-db: 2.0 + ubuntu: 1.0 + - key: CVE-2019-14697 + value: + CVSS: + nvd: + V2Score: 7.5 + V2Vector: AV:N/AC:L/Au:N/C:P/I:P/A:P + V3Score: 9.8 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + CweIDs: + - CWE-787 + Description: musl libc through 1.1.23 has an x87 floating-point stack adjustment imbalance, related to the math/i386/ directory. In some cases, use of this library could introduce out-of-bounds writes that are not present in an application's source code. + LastModifiedDate: 2020-03-14T19:15:00Z + PublishedDate: 2019-08-06T16:15:00Z + References: + - http://www.openwall.com/lists/oss-security/2019/08/06/4 + - https://security.gentoo.org/glsa/202003-13 + - https://www.openwall.com/lists/musl/2019/08/06/1 + Severity: CRITICAL + VendorSeverity: + nvd: 4.0 + - key: CVE-2019-14806 + value: + CVSS: + nvd: + V2Score: 5.0 + V2Vector: AV:N/AC:L/Au:N/C:P/I:N/A:N + V3Score: 7.5 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N + redhat: + V3Score: 7.5 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N + CweIDs: + - CWE-331 + Description: Pallets Werkzeug before 0.15.3, when used with Docker, has insufficient debugger PIN randomness because Docker containers share the same machine id. + LastModifiedDate: 2019-09-11T00:15:00Z + PublishedDate: 2019-08-09T15:15:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00034.html + - http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00047.html + - https://access.redhat.com/security/cve/CVE-2019-14806 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14806 + - https://github.com/advisories/GHSA-gq9m-qvpx-68hc + - "https://github.com/pallets/werkzeug/blob/7fef41b120327d3912fbe12fb64f1951496fcf3e/src/werkzeug/debug/__init__.py#L168" + - https://github.com/pallets/werkzeug/commit/00bc43b1672e662e5e3b8cecd79e67fc968fa246 + - https://nvd.nist.gov/vuln/detail/CVE-2019-14806 + - https://palletsprojects.com/blog/werkzeug-0-15-3-released/ + - https://ubuntu.com/security/notices/USN-4655-1 + Severity: HIGH + Title: "python-werkzeug: insufficient debugger PIN randomness vulnerability" + VendorSeverity: + ghsa: 3.0 + nvd: 3.0 + redhat: 2.0 + ubuntu: 1.0 + - key: CVE-2019-1549 + value: + CVSS: + nvd: + V2Score: 5.0 + V2Vector: AV:N/AC:L/Au:N/C:P/I:N/A:N + V3Score: 5.3 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N + redhat: + V3Score: 4.8 + V3Vector: CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N + CweIDs: + - CWE-330 + Description: OpenSSL 1.1.1 introduced a rewritten random number generator (RNG). This was intended to include protection in the event of a fork() system call in order to ensure that the parent and child processes did not share the same RNG state. However this protection was not being used in the default case. A partial mitigation for this issue is that the output from a high precision timer is mixed into the RNG state so the likelihood of a parent and child process sharing state is significantly reduced. If an application already calls OPENSSL_init_crypto() explicitly using OPENSSL_INIT_ATFORK then this problem does not occur at all. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). + LastModifiedDate: 2020-10-20T22:15:00Z + PublishedDate: 2019-09-10T17:15:00Z + References: + - https://access.redhat.com/security/cve/CVE-2019-1549 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1549 + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=1b0fe00e2704b5e20334a16d3c9099d1ba2ef1be + - https://linux.oracle.com/cve/CVE-2019-1549.html + - https://linux.oracle.com/errata/ELSA-2020-1840.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/ + - https://seclists.org/bugtraq/2019/Oct/1 + - https://security.netapp.com/advisory/ntap-20190919-0002/ + - https://support.f5.com/csp/article/K44070243 + - https://support.f5.com/csp/article/K44070243?utm_source=f5support&utm_medium=RSS + - https://ubuntu.com/security/notices/USN-4376-1 + - https://usn.ubuntu.com/4376-1/ + - https://www.debian.org/security/2019/dsa-4539 + - https://www.openssl.org/news/secadv/20190910.txt + - https://www.oracle.com/security-alerts/cpuapr2020.html + - https://www.oracle.com/security-alerts/cpujan2020.html + - https://www.oracle.com/security-alerts/cpujul2020.html + - https://www.oracle.com/security-alerts/cpuoct2020.html + - https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html + Severity: MEDIUM + Title: "openssl: information disclosure in fork()" + VendorSeverity: + amazon: 2.0 + nvd: 2.0 + oracle-oval: 2.0 + photon: 2.0 + redhat: 1.0 + ubuntu: 1.0 + - key: CVE-2019-1551 + value: + CVSS: + nvd: + V2Score: 5.0 + V2Vector: AV:N/AC:L/Au:N/C:P/I:N/A:N + V3Score: 5.3 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:N + redhat: + V3Score: 4.8 + V3Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N + CweIDs: + - CWE-200 + Description: There is an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli. No EC algorithms are affected. Analysis suggests that attacks against 2-prime RSA1024, 3-prime RSA1536, and DSA1024 as a result of this defect would be very difficult to perform and are not believed likely. Attacks against DH512 are considered just feasible. However, for an attack the target would have to re-use the DH512 private key, which is not recommended anyway. Also applications directly using the low level API BN_mod_exp may be affected if they use BN_FLG_CONSTTIME. Fixed in OpenSSL 1.1.1e (Affected 1.1.1-1.1.1d). Fixed in OpenSSL 1.0.2u (Affected 1.0.2-1.0.2t). + LastModifiedDate: 2021-07-21T11:39:00Z + PublishedDate: 2019-12-06T18:15:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html + - http://packetstormsecurity.com/files/155754/Slackware-Security-Advisory-openssl-Updates.html + - https://access.redhat.com/security/cve/CVE-2019-1551 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1551 + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=419102400a2811582a7a3d4a4e317d72e5ce0a8f + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=f1c5eea8a817075d31e43f5876993c6710238c98 + - https://github.com/openssl/openssl/pull/10575 + - https://linux.oracle.com/cve/CVE-2019-1551.html + - https://linux.oracle.com/errata/ELSA-2020-4514.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/DDHOAATPWJCXRNFMJ2SASDBBNU5RJONY/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EXDDAOWSAIEFQNBHWYE6PPYFV4QXGMCD/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/XVEP3LAK4JSPRXFO4QF4GG2IVXADV3SO/ + - https://seclists.org/bugtraq/2019/Dec/39 + - https://seclists.org/bugtraq/2019/Dec/46 + - https://security.gentoo.org/glsa/202004-10 + - https://security.netapp.com/advisory/ntap-20191210-0001/ + - https://ubuntu.com/security/notices/USN-4376-1 + - https://ubuntu.com/security/notices/USN-4504-1 + - https://usn.ubuntu.com/4376-1/ + - https://usn.ubuntu.com/4504-1/ + - https://www.debian.org/security/2019/dsa-4594 + - https://www.debian.org/security/2021/dsa-4855 + - https://www.openssl.org/news/secadv/20191206.txt + - https://www.oracle.com/security-alerts/cpuApr2021.html + - https://www.oracle.com/security-alerts/cpujan2021.html + - https://www.oracle.com/security-alerts/cpujul2020.html + - https://www.tenable.com/security/tns-2019-09 + - https://www.tenable.com/security/tns-2020-03 + - https://www.tenable.com/security/tns-2020-11 + - https://www.tenable.com/security/tns-2021-10 + Severity: MEDIUM + Title: "openssl: Integer overflow in RSAZ modular exponentiation on x86_64" + VendorSeverity: + amazon: 1.0 + nvd: 2.0 + oracle-oval: 1.0 + photon: 2.0 + redhat: 1.0 + ubuntu: 1.0 + - key: CVE-2019-15542 + value: + CVSS: + nvd: + V2Score: 5.0 + V2Vector: AV:N/AC:L/Au:N/C:N/I:N/A:P + V3Score: 7.5 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H + CweIDs: + - CWE-674 + Description: An issue was discovered in the ammonia crate before 2.1.0 for Rust. There is uncontrolled recursion during HTML DOM tree serialization. + LastModifiedDate: 2020-08-24T17:37:00Z + PublishedDate: 2019-08-26T18:15:00Z + References: + - https://crates.io/crates/ammonia + - "https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md#210" + - https://rustsec.org/advisories/RUSTSEC-2019-0001.html + Severity: HIGH + Title: Uncontrolled recursion leads to abort in HTML serialization + VendorSeverity: + nvd: 3.0 + - key: CVE-2019-1559 + value: + CVSS: + nvd: + V2Score: 4.3 + V2Vector: AV:N/AC:M/Au:N/C:P/I:N/A:N + V3Score: 5.9 + V3Vector: CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N + redhat: + V3Score: 5.9 + V3Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N + CweIDs: + - CWE-203 + Description: If an application encounters a fatal protocol error and then calls SSL_shutdown() twice (once to send a close_notify, and once to receive one) then OpenSSL can respond differently to the calling application if a 0 byte record is received with invalid padding compared to if a 0 byte record is received with an invalid MAC. If the application then behaves differently based on that in a way that is detectable to the remote peer, then this amounts to a padding oracle that could be used to decrypt data. In order for this to be exploitable "non-stitched" ciphersuites must be in use. Stitched ciphersuites are optimised implementations of certain commonly used ciphersuites. Also the application must call SSL_shutdown() twice even if a protocol error has occurred (applications should not do this but some do anyway). Fixed in OpenSSL 1.0.2r (Affected 1.0.2-1.0.2q). + LastModifiedDate: 2021-01-20T15:15:00Z + PublishedDate: 2019-02-27T23:29:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2019-03/msg00041.html + - http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00019.html + - http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00046.html + - http://lists.opensuse.org/opensuse-security-announce/2019-04/msg00047.html + - http://lists.opensuse.org/opensuse-security-announce/2019-05/msg00049.html + - http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00080.html + - http://www.securityfocus.com/bid/107174 + - https://access.redhat.com/errata/RHSA-2019:2304 + - https://access.redhat.com/errata/RHSA-2019:2437 + - https://access.redhat.com/errata/RHSA-2019:2439 + - https://access.redhat.com/errata/RHSA-2019:2471 + - https://access.redhat.com/errata/RHSA-2019:3929 + - https://access.redhat.com/errata/RHSA-2019:3931 + - https://access.redhat.com/security/cve/CVE-2019-1559 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1559 + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e9bbefbf0f24c57645e7ad6a5a71ae649d18ac8e + - https://github.com/RUB-NDS/TLS-Padding-Oracles + - https://kc.mcafee.com/corporate/index?page=content&id=SB10282 + - https://linux.oracle.com/cve/CVE-2019-1559.html + - https://linux.oracle.com/errata/ELSA-2019-2471.html + - https://lists.debian.org/debian-lts-announce/2019/03/msg00003.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/EWC42UXL5GHTU5G77VKBF6JYUUNGSHOM/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/Y3IVFGSERAZLNJCK35TEM2R4726XIH3Z/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZBEV5QGDRFUZDMNECFXUSN5FMYOZDE4V/ + - https://security.gentoo.org/glsa/201903-10 + - https://security.netapp.com/advisory/ntap-20190301-0001/ + - https://security.netapp.com/advisory/ntap-20190301-0002/ + - https://security.netapp.com/advisory/ntap-20190423-0002/ + - https://support.f5.com/csp/article/K18549143 + - https://support.f5.com/csp/article/K18549143?utm_source=f5support&utm_medium=RSS + - https://ubuntu.com/security/notices/USN-3899-1 + - https://ubuntu.com/security/notices/USN-4376-2 + - https://usn.ubuntu.com/3899-1/ + - https://usn.ubuntu.com/4376-2/ + - https://www.debian.org/security/2019/dsa-4400 + - https://www.openssl.org/news/secadv/20190226.txt + - https://www.oracle.com/security-alerts/cpujan2020.html + - https://www.oracle.com/security-alerts/cpujan2021.html + - https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html + - https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html + - https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html + - https://www.tenable.com/security/tns-2019-02 + - https://www.tenable.com/security/tns-2019-03 + Severity: MEDIUM + Title: "openssl: 0-byte record padding oracle" + VendorSeverity: + amazon: 2.0 + arch-linux: 2.0 + nvd: 2.0 + oracle-oval: 2.0 + redhat: 2.0 + ubuntu: 2.0 + - key: CVE-2019-1563 + value: + CVSS: + nvd: + V2Score: 4.3 + V2Vector: AV:N/AC:M/Au:N/C:P/I:N/A:N + V3Score: 3.7 + V3Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N + redhat: + V3Score: 3.7 + V3Vector: CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N + CweIDs: + - CWE-327 + - CWE-203 + Description: In situations where an attacker receives automated notification of the success or failure of a decryption attempt an attacker, after sending a very large number of messages to be decrypted, can recover a CMS/PKCS7 transported encryption key or decrypt any RSA encrypted message that was encrypted with the public RSA key, using a Bleichenbacher padding oracle attack. Applications are not affected if they use a certificate together with the private RSA key to the CMS_decrypt or PKCS7_decrypt functions to select the correct recipient info to decrypt. Fixed in OpenSSL 1.1.1d (Affected 1.1.1-1.1.1c). Fixed in OpenSSL 1.1.0l (Affected 1.1.0-1.1.0k). Fixed in OpenSSL 1.0.2t (Affected 1.0.2-1.0.2s). + LastModifiedDate: 2021-07-31T08:15:00Z + PublishedDate: 2019-09-10T17:15:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00054.html + - http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00072.html + - http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00012.html + - http://lists.opensuse.org/opensuse-security-announce/2019-10/msg00016.html + - http://packetstormsecurity.com/files/154467/Slackware-Security-Advisory-openssl-Updates.html + - https://access.redhat.com/security/cve/CVE-2019-1563 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-1563 + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=08229ad838c50f644d7e928e2eef147b4308ad64 + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=631f94db0065c78181ca9ba5546ebc8bb3884b97 + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=e21f8cf78a125cd3c8c0d1a1a6c8bb0b901f893f + - https://kc.mcafee.com/corporate/index?page=content&id=SB10365 + - https://linux.oracle.com/cve/CVE-2019-1563.html + - https://linux.oracle.com/errata/ELSA-2020-1840.html + - https://lists.debian.org/debian-lts-announce/2019/09/msg00026.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/GY6SNRJP2S7Y42GIIDO3HXPNMDYN2U3A/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/ZN4VVQJ3JDCHGIHV4Y2YTXBYQZ6PWQ7E/ + - https://seclists.org/bugtraq/2019/Oct/0 + - https://seclists.org/bugtraq/2019/Oct/1 + - https://seclists.org/bugtraq/2019/Sep/25 + - https://security.gentoo.org/glsa/201911-04 + - https://security.netapp.com/advisory/ntap-20190919-0002/ + - https://support.f5.com/csp/article/K97324400?utm_source=f5support&utm_medium=RSS + - https://ubuntu.com/security/notices/USN-4376-1 + - https://ubuntu.com/security/notices/USN-4376-2 + - https://ubuntu.com/security/notices/USN-4504-1 + - https://usn.ubuntu.com/4376-1/ + - https://usn.ubuntu.com/4376-2/ + - https://usn.ubuntu.com/4504-1/ + - https://www.debian.org/security/2019/dsa-4539 + - https://www.debian.org/security/2019/dsa-4540 + - https://www.openssl.org/news/secadv/20190910.txt + - https://www.oracle.com/security-alerts/cpuapr2020.html + - https://www.oracle.com/security-alerts/cpujan2020.html + - https://www.oracle.com/security-alerts/cpujul2020.html + - https://www.oracle.com/security-alerts/cpuoct2020.html + - https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html + - https://www.tenable.com/security/tns-2019-09 + Severity: LOW + Title: "openssl: information disclosure in PKCS7_dataDecode and CMS_decrypt_set1_pkey" + VendorSeverity: + amazon: 2.0 + nvd: 1.0 + oracle-oval: 2.0 + photon: 1.0 + redhat: 1.0 + ubuntu: 1.0 + - key: CVE-2019-18224 + value: + CVSS: + nvd: + V2Score: 7.5 + V2Vector: AV:N/AC:L/Au:N/C:P/I:P/A:P + V3Score: 9.8 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + redhat: + V3Score: 5.6 + V3Vector: CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L + CweIDs: + - CWE-787 + Description: idn2_to_ascii_4i in lib/lookup.c in GNU libidn2 before 2.1.1 has a heap-based buffer overflow via a long domain string. + LastModifiedDate: 2019-10-29T19:15:00Z + PublishedDate: 2019-10-21T17:15:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00008.html + - http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00009.html + - https://access.redhat.com/security/cve/CVE-2019-18224 + - https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=12420 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18224 + - https://github.com/libidn/libidn2/commit/e4d1558aa2c1c04a05066ee8600f37603890ba8c + - https://github.com/libidn/libidn2/compare/libidn2-2.1.0...libidn2-2.1.1 + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/JDQVQ2XPV5BTZUFINT7AFJSKNNBVURNJ/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MINU5RKDFE6TKAFY5DRFN3WSFDS4DYVS/ + - https://seclists.org/bugtraq/2020/Feb/4 + - https://security.gentoo.org/glsa/202003-63 + - https://ubuntu.com/security/notices/USN-4168-1 + - https://usn.ubuntu.com/4168-1/ + - https://www.debian.org/security/2020/dsa-4613 + Severity: CRITICAL + Title: "libidn2: heap-based buffer overflow in idn2_to_ascii_4i in lib/lookup.c" + VendorSeverity: + amazon: 2.0 + nvd: 4.0 + redhat: 2.0 + ubuntu: 2.0 + - key: CVE-2019-18276 + value: + CVSS: + nvd: + V2Score: 7.2 + V2Vector: AV:L/AC:L/Au:N/C:C/I:C/A:C + V3Score: 7.8 + V3Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + redhat: + V3Score: 7.8 + V3Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + CweIDs: + - CWE-273 + Description: An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support "saved UID" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use "enable -f" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected. + LastModifiedDate: 2021-05-26T12:15:00Z + PublishedDate: 2019-11-28T01:15:00Z + References: + - http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html + - https://access.redhat.com/security/cve/CVE-2019-18276 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276 + - https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff + - https://linux.oracle.com/cve/CVE-2019-18276.html + - https://linux.oracle.com/errata/ELSA-2021-1679.html + - https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E + - https://nvd.nist.gov/vuln/detail/CVE-2019-18276 + - https://security.gentoo.org/glsa/202105-34 + - https://security.netapp.com/advisory/ntap-20200430-0003/ + - https://www.youtube.com/watch?v=-wGtxJ8opa8 + Severity: HIGH + Title: "bash: when effective UID is not equal to its real UID the saved UID is not dropped" + VendorSeverity: + cbl-mariner: 3.0 + nvd: 3.0 + oracle-oval: 1.0 + photon: 3.0 + redhat: 1.0 + ubuntu: 1.0 + - key: CVE-2019-3823 + value: + CVSS: + nvd: + V2Score: 5.0 + V2Vector: AV:N/AC:L/Au:N/C:N/I:N/A:P + V3Score: 7.5 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H + redhat: + V3Score: 4.3 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L + CweIDs: + - CWE-125 + Description: libcurl versions from 7.34.0 to before 7.64.0 are vulnerable to a heap out-of-bounds read in the code handling the end-of-response for SMTP. If the buffer passed to `smtp_endofresp()` isn't NUL terminated and contains no character ending the parsed number, and `len` is set to 5, then the `strtol()` call reads beyond the allocated buffer. The read contents will not be returned to the caller. + LastModifiedDate: 2021-03-09T15:15:00Z + PublishedDate: 2019-02-06T20:29:00Z + References: + - http://www.securityfocus.com/bid/106950 + - https://access.redhat.com/errata/RHSA-2019:3701 + - https://access.redhat.com/security/cve/CVE-2019-3823 + - https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2019-3823 + - https://cert-portal.siemens.com/productcert/pdf/ssa-936080.pdf + - https://curl.haxx.se/docs/CVE-2019-3823.html + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-3823 + - https://linux.oracle.com/cve/CVE-2019-3823.html + - https://linux.oracle.com/errata/ELSA-2019-3701.html + - https://lists.apache.org/thread.html/8338a0f605bdbb3a6098bb76f666a95fc2b2f53f37fa1ecc89f1146f@%3Cdevnull.infra.apache.org%3E + - https://security.gentoo.org/glsa/201903-03 + - https://security.netapp.com/advisory/ntap-20190315-0001/ + - https://ubuntu.com/security/notices/USN-3882-1 + - https://usn.ubuntu.com/3882-1/ + - https://www.debian.org/security/2019/dsa-4386 + - https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html + - https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html + Severity: HIGH + Title: "curl: SMTP end-of-response out-of-bounds read" + VendorSeverity: + amazon: 2.0 + arch-linux: 3.0 + nvd: 3.0 + oracle-oval: 2.0 + photon: 3.0 + redhat: 1.0 + ubuntu: 1.0 + - key: CVE-2019-5094 + value: + CVSS: + nvd: + V2Score: 4.6 + V2Vector: AV:L/AC:L/Au:N/C:P/I:P/A:P + V3Score: 6.7 + V3Vector: CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H + redhat: + V3Score: 6.4 + V3Vector: CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H + CweIDs: + - CWE-787 + Description: An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability. + LastModifiedDate: 2021-01-11T19:21:00Z + PublishedDate: 2019-09-24T22:15:00Z + References: + - https://access.redhat.com/security/cve/CVE-2019-5094 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094 + - https://linux.oracle.com/cve/CVE-2019-5094.html + - https://linux.oracle.com/errata/ELSA-2020-4011.html + - https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/ + - https://nvd.nist.gov/vuln/detail/CVE-2019-5094 + - https://seclists.org/bugtraq/2019/Sep/58 + - https://security.gentoo.org/glsa/202003-05 + - https://security.netapp.com/advisory/ntap-20200115-0002/ + - https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887 + - https://ubuntu.com/security/notices/USN-4142-1 + - https://ubuntu.com/security/notices/USN-4142-2 + - https://usn.ubuntu.com/4142-1/ + - https://usn.ubuntu.com/4142-2/ + - https://www.debian.org/security/2019/dsa-4535 + Severity: MEDIUM + Title: "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write" + VendorSeverity: + amazon: 2.0 + cbl-mariner: 2.0 + nvd: 2.0 + oracle-oval: 2.0 + photon: 2.0 + redhat: 2.0 + ubuntu: 2.0 + - key: CVE-2019-5436 + value: + CVSS: + nvd: + V2Score: 4.6 + V2Vector: AV:L/AC:L/Au:N/C:P/I:P/A:P + V3Score: 7.8 + V3Vector: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H + redhat: + V3Score: 7.0 + V3Vector: CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H + CweIDs: + - CWE-787 + Description: A heap buffer overflow in the TFTP receiving code allows for DoS or arbitrary code execution in libcurl versions 7.19.4 through 7.64.1. + LastModifiedDate: 2020-10-20T22:15:00Z + PublishedDate: 2019-05-28T19:29:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00008.html + - http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00017.html + - http://www.openwall.com/lists/oss-security/2019/09/11/6 + - https://access.redhat.com/security/cve/CVE-2019-5436 + - https://curl.haxx.se/docs/CVE-2019-5436.html + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5436 + - https://linux.oracle.com/cve/CVE-2019-5436.html + - https://linux.oracle.com/errata/ELSA-2020-1792.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/SMG3V4VTX2SE3EW3HQTN3DDLQBTORQC2/ + - https://seclists.org/bugtraq/2020/Feb/36 + - https://security.gentoo.org/glsa/202003-29 + - https://security.netapp.com/advisory/ntap-20190606-0004/ + - https://support.f5.com/csp/article/K55133295 + - https://support.f5.com/csp/article/K55133295?utm_source=f5support&utm_medium=RSS + - https://ubuntu.com/security/notices/USN-3993-1 + - https://ubuntu.com/security/notices/USN-3993-2 + - https://www.debian.org/security/2020/dsa-4633 + - https://www.oracle.com/security-alerts/cpuapr2020.html + - https://www.oracle.com/security-alerts/cpuoct2020.html + - https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html + Severity: HIGH + Title: "curl: TFTP receive heap buffer overflow in tftp_receive_packet() function" + VendorSeverity: + amazon: 1.0 + arch-linux: 3.0 + nvd: 3.0 + oracle-oval: 2.0 + photon: 3.0 + redhat: 1.0 + ubuntu: 2.0 + - key: CVE-2019-5481 + value: + CVSS: + nvd: + V2Score: 7.5 + V2Vector: AV:N/AC:L/Au:N/C:P/I:P/A:P + V3Score: 9.8 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + redhat: + V3Score: 5.7 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H + CweIDs: + - CWE-415 + Description: Double-free vulnerability in the FTP-kerberos code in cURL 7.52.0 to 7.65.3. + LastModifiedDate: 2020-10-20T22:15:00Z + PublishedDate: 2019-09-16T19:15:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00048.html + - http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00055.html + - https://access.redhat.com/security/cve/CVE-2019-5481 + - https://curl.haxx.se/docs/CVE-2019-5481.html + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5481 + - https://linux.oracle.com/cve/CVE-2019-5481.html + - https://linux.oracle.com/errata/ELSA-2020-1792.html + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6CI4QQ2RSZX4VCFM76SIWGKY6BY7UWIC/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RGDVKSLY5JUNJRLYRUA6CXGQ2LM63XC3/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UA7KDM2WPM5CJDDGOEGFV6SSGD2J7RNT/ + - https://seclists.org/bugtraq/2020/Feb/36 + - https://security.gentoo.org/glsa/202003-29 + - https://security.netapp.com/advisory/ntap-20191004-0003/ + - https://ubuntu.com/security/notices/USN-4129-1 + - https://www.debian.org/security/2020/dsa-4633 + - https://www.oracle.com/security-alerts/cpuapr2020.html + - https://www.oracle.com/security-alerts/cpujan2020.html + - https://www.oracle.com/security-alerts/cpuoct2020.html + Severity: CRITICAL + Title: "curl: double free due to subsequent call of realloc()" + VendorSeverity: + amazon: 2.0 + arch-linux: 2.0 + nvd: 4.0 + oracle-oval: 2.0 + photon: 4.0 + redhat: 2.0 + ubuntu: 2.0 + - key: CVE-2020-28724 + value: + CVSS: + nvd: + V2Score: 5.8 + V2Vector: AV:N/AC:M/Au:N/C:P/I:P/A:N + V3Score: 6.1 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N + redhat: + V3Score: 5.4 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N + CweIDs: + - CWE-601 + Description: Open redirect vulnerability in werkzeug before 0.11.6 via a double slash in the URL. + LastModifiedDate: 2020-12-01T16:05:00Z + PublishedDate: 2020-11-18T15:15:00Z + References: + - https://access.redhat.com/security/cve/CVE-2020-28724 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28724 + - https://github.com/advisories/GHSA-3p3h-qghp-hvh2 + - https://github.com/pallets/flask/issues/1639 + - https://github.com/pallets/werkzeug/issues/822 + - https://github.com/pallets/werkzeug/pull/890/files + - https://nvd.nist.gov/vuln/detail/CVE-2020-28724 + - https://ubuntu.com/security/notices/USN-4655-1 + Severity: MEDIUM + Title: "python-werkzeug: open redirect via double slash in the URL" + VendorSeverity: + ghsa: 2.0 + nvd: 2.0 + redhat: 2.0 + ubuntu: 2.0 + - key: CVE-2020-29573 + value: + CVSS: + nvd: + V2Score: 5.0 + V2Vector: AV:N/AC:L/Au:N/C:N/I:N/A:P + V3Score: 7.5 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H + redhat: + V3Score: 7.5 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H + CweIDs: + - CWE-787 + Description: "sysdeps/i386/ldbl2mpn.c in the GNU C Library (aka glibc or libc6) before 2.23 on x86 targets has a stack-based buffer overflow if the input to any of the printf family of functions is an 80-bit long double with a non-canonical bit pattern, as seen when passing a \\x00\\x04\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x04 value to sprintf. NOTE: the issue does not affect glibc by default in 2016 or later (i.e., 2.23 or later) because of commits made in 2015 for inlining of C99 math functions through use of GCC built-ins. In other words, the reference to 2.23 is intentional despite the mention of \"Fixed for glibc 2.33\" in the 26649 reference." + LastModifiedDate: 2021-01-26T18:15:00Z + PublishedDate: 2020-12-06T00:15:00Z + References: + - https://access.redhat.com/security/cve/CVE-2020-29573 + - https://linux.oracle.com/cve/CVE-2020-29573.html + - https://linux.oracle.com/errata/ELSA-2021-0348.html + - https://security.gentoo.org/glsa/202101-20 + - https://security.netapp.com/advisory/ntap-20210122-0004/ + - https://sourceware.org/bugzilla/show_bug.cgi?id=26649 + - https://sourceware.org/pipermail/libc-alpha/2020-September/117779.html + Severity: HIGH + Title: "glibc: stack-based buffer overflow if the input to any of the printf family of functions is an 80-bit long double with a non-canonical bit pattern" + VendorSeverity: + amazon: 2.0 + arch-linux: 2.0 + nvd: 3.0 + oracle-oval: 2.0 + photon: 3.0 + redhat: 2.0 + - key: CVE-2020-8165 + value: + CVSS: + nvd: + V2Score: 7.5 + V2Vector: AV:N/AC:L/Au:N/C:P/I:P/A:P + V3Score: 9.8 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + redhat: + V3Score: 9.8 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + CweIDs: + - CWE-502 + Description: A deserialization of untrusted data vulnerability exists in rails < 5.2.4.3, rails < 6.0.3.1 that can allow an attacker to unmarshal user-provided objects in MemCacheStore and RedisCacheStore potentially resulting in an RCE. + LastModifiedDate: 2020-10-17T12:15:00Z + PublishedDate: 2020-06-19T18:15:00Z + References: + - http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00031.html + - http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00034.html + - https://access.redhat.com/security/cve/CVE-2020-8165 + - https://github.com/advisories/GHSA-2p68-f74v-9wc6 + - https://github.com/rubysec/ruby-advisory-db/blob/master/gems/activesupport/CVE-2020-8165.yml + - "https://groups.google.com/forum/#!msg/rubyonrails-security/bv6fW4S0Y1c/KnkEqM7AAQAJ" + - "https://groups.google.com/forum/#!topic/rubyonrails-security/bv6fW4S0Y1c" + - https://groups.google.com/g/rubyonrails-security/c/bv6fW4S0Y1c + - https://hackerone.com/reports/413388 + - https://lists.debian.org/debian-lts-announce/2020/06/msg00022.html + - https://lists.debian.org/debian-lts-announce/2020/07/msg00013.html + - https://nvd.nist.gov/vuln/detail/CVE-2020-8165 + - https://weblog.rubyonrails.org/2020/5/18/Rails-5-2-4-3-and-6-0-3-1-have-been-released/ + - https://www.debian.org/security/2020/dsa-4766 + Severity: CRITICAL + Title: "rubygem-activesupport: potentially unintended unmarshalling of user-provided objects in MemCacheStore and RedisCacheStore" + VendorSeverity: + ghsa: 3.0 + nvd: 4.0 + redhat: 3.0 + - key: CVE-2020-9548 + value: + CVSS: + nvd: + V2Score: 6.8 + V2Vector: AV:N/AC:M/Au:N/C:P/I:P/A:P + V3Score: 9.8 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + redhat: + V3Score: 8.1 + V3Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H + CweIDs: + - CWE-502 + Description: FasterXML jackson-databind 2.x before 2.9.10.4 mishandles the interaction between serialization gadgets and typing, related to br.com.anteros.dbcp.AnterosDBCPConfig (aka anteros-core). + LastModifiedDate: 2021-12-02T21:23:00Z + PublishedDate: 2020-03-02T04:15:00Z + References: + - https://access.redhat.com/security/cve/CVE-2020-9548 + - https://github.com/FasterXML/jackson-databind/issues/2634 + - https://github.com/advisories/GHSA-p43x-xfjf-5jhr + - https://lists.apache.org/thread.html/r35d30db00440ef63b791c4b7f7acb036e14d4a23afa2a249cb66c0fd@%3Cissues.zookeeper.apache.org%3E + - https://lists.apache.org/thread.html/r9464a40d25c3ba1a55622db72f113eb494a889656962d098c70c5bb1@%3Cdev.zookeeper.apache.org%3E + - https://lists.apache.org/thread.html/r98c9b6e4c9e17792e2cd1ec3e4aa20b61a791939046d3f10888176bb@%3Cissues.zookeeper.apache.org%3E + - https://lists.apache.org/thread.html/rb6fecb5e96a6d61e175ff49f33f2713798dd05cf03067c169d195596@%3Cissues.zookeeper.apache.org%3E + - https://lists.apache.org/thread.html/rd5a4457be4623038c3989294429bc063eec433a2e55995d81591e2ca@%3Cissues.zookeeper.apache.org%3E + - https://lists.apache.org/thread.html/rdd49ab9565bec436a896bc00c4b9fc9dce1598e106c318524fbdfec6@%3Cissues.zookeeper.apache.org%3E + - https://lists.apache.org/thread.html/rdd4df698d5d8e635144d2994922bf0842e933809eae259521f3b5097@%3Cissues.zookeeper.apache.org%3E + - https://lists.apache.org/thread.html/rf1bbc0ea4a9f014cf94df9a12a6477d24a27f52741dbc87f2fd52ff2@%3Cissues.geode.apache.org%3E + - https://lists.debian.org/debian-lts-announce/2020/03/msg00008.html + - https://medium.com/@cowtowncoder/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062 + - https://nvd.nist.gov/vuln/detail/CVE-2020-9548 + - https://security.netapp.com/advisory/ntap-20200904-0006/ + - https://www.oracle.com/security-alerts/cpujan2021.html + - https://www.oracle.com/security-alerts/cpujul2020.html + - https://www.oracle.com/security-alerts/cpuoct2020.html + - https://www.oracle.com/security-alerts/cpuoct2021.html + Severity: CRITICAL + Title: "jackson-databind: Serialization gadgets in anteros-core" + VendorSeverity: + ghsa: 4.0 + nvd: 4.0 + redhat: 3.0 + - key: CVE-2021-20190 + value: + CVSS: + nvd: + V2Score: 8.3 + V2Vector: AV:N/AC:M/Au:N/C:P/I:P/A:C + V3Score: 8.1 + V3Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H + redhat: + V3Score: 8.1 + V3Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H + CweIDs: + - CWE-502 + Description: A flaw was found in jackson-databind before 2.9.10.7. FasterXML mishandles the interaction between serialization gadgets and typing. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability. + LastModifiedDate: 2021-07-20T23:15:00Z + PublishedDate: 2021-01-19T17:15:00Z + References: + - https://access.redhat.com/security/cve/CVE-2021-20190 + - https://bugzilla.redhat.com/show_bug.cgi?id=1916633 + - https://github.com/FasterXML/jackson-databind/commit/7dbf51bf78d157098074a20bd9da39bd48c18e4a + - https://github.com/FasterXML/jackson-databind/issues/2854 + - https://github.com/advisories/GHSA-5949-rw7g-wx7w + - https://lists.apache.org/thread.html/r380e9257bacb8551ee6fcf2c59890ae9477b2c78e553fa9ea08e9d9a@%3Ccommits.nifi.apache.org%3E + - https://lists.debian.org/debian-lts-announce/2021/04/msg00025.html + - https://nvd.nist.gov/vuln/detail/CVE-2021-20190 + - https://security.netapp.com/advisory/ntap-20210219-0008/ + Severity: HIGH + Title: "jackson-databind: mishandles the interaction between serialization gadgets and typing, related to javax.swing" + VendorSeverity: + ghsa: 3.0 + nvd: 3.0 + redhat: 3.0 + - key: CVE-2023-2431 + value: + Title: "Bypass of seccomp profile enforcement " + Description: "A security issue was discovered in Kubelet that allows pods to bypass the seccomp profile enforcement..." + Severity: LOW + VendorSeverity: + k8s: 1 + CVSS: + k8s: + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N" + V3Score: 3.4 + References: + - https://github.com/kubernetes/kubernetes/issues/118690 + - https://www.cve.org/cverecord?id=CVE-2023-2431 + - key: CVE-2021-3712 + value: + CVSS: + nvd: + V2Score: 5.8 + V2Vector: AV:N/AC:M/Au:N/C:P/I:N/A:P + V3Score: 7.4 + V3Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H + redhat: + V3Score: 7.4 + V3Vector: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H + CweIDs: + - CWE-125 + Description: ASN.1 strings are represented internally within OpenSSL as an ASN1_STRING structure which contains a buffer holding the string data and a field holding the buffer length. This contrasts with normal C strings which are represented as a buffer for the string data which is terminated with a NUL (0) byte. Although not a strict requirement, ASN.1 strings that are parsed using OpenSSL's own "d2i" functions (and other similar parsing functions) as well as any string whose value has been set with the ASN1_STRING_set() function will additionally NUL terminate the byte array in the ASN1_STRING structure. However, it is possible for applications to directly construct valid ASN1_STRING structures which do not NUL terminate the byte array by directly setting the "data" and "length" fields in the ASN1_STRING array. This can also happen by using the ASN1_STRING_set0() function. Numerous OpenSSL functions that print ASN.1 data have been found to assume that the ASN1_STRING byte array will be NUL terminated, even though this is not guaranteed for strings that have been directly constructed. Where an application requests an ASN.1 structure to be printed, and where that ASN.1 structure contains ASN1_STRINGs that have been directly constructed by the application without NUL terminating the "data" field, then a read buffer overrun can occur. The same thing can also occur during name constraints processing of certificates (for example if a certificate has been directly constructed by the application instead of loading it via the OpenSSL parsing functions, and the certificate contains non NUL terminated ASN1_STRING structures). It can also occur in the X509_get1_email(), X509_REQ_get1_email() and X509_get1_ocsp() functions. If a malicious actor can cause an application to directly construct an ASN1_STRING and then process it through one of the affected OpenSSL functions then this issue could be hit. This might result in a crash (causing a Denial of Service attack). It could also result in the disclosure of private memory contents (such as private keys, or sensitive plaintext). Fixed in OpenSSL 1.1.1l (Affected 1.1.1-1.1.1k). Fixed in OpenSSL 1.0.2za (Affected 1.0.2-1.0.2y). + LastModifiedDate: 2022-01-06T09:15:00Z + PublishedDate: 2021-08-24T15:15:00Z + References: + - http://www.openwall.com/lists/oss-security/2021/08/26/2 + - https://access.redhat.com/hydra/rest/securitydata/cve/CVE-2021-3712.json + - https://access.redhat.com/security/cve/CVE-2021-3712 + - https://crates.io/crates/openssl-src + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3712 + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=94d23fcff9b2a7a8368dfe52214d5c2569882c11 + - https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=ccb0a11145ee72b042d10593a64eaf9e8a55ec12 + - https://kc.mcafee.com/corporate/index?page=content&id=SB10366 + - https://linux.oracle.com/cve/CVE-2021-3712.html + - https://linux.oracle.com/errata/ELSA-2022-9023.html + - https://lists.apache.org/thread.html/r18995de860f0e63635f3008fd2a6aca82394249476d21691e7c59c9e@%3Cdev.tomcat.apache.org%3E + - https://lists.apache.org/thread.html/rad5d9f83f0d11fb3f8bb148d179b8a9ad7c6a17f18d70e5805a713d1@%3Cdev.tomcat.apache.org%3E + - https://lists.debian.org/debian-lts-announce/2021/09/msg00014.html + - https://lists.debian.org/debian-lts-announce/2021/09/msg00021.html + - https://nvd.nist.gov/vuln/detail/CVE-2021-3712 + - https://rustsec.org/advisories/RUSTSEC-2021-0098.html + - https://security.netapp.com/advisory/ntap-20210827-0010/ + - https://ubuntu.com/security/notices/USN-5051-1 + - https://ubuntu.com/security/notices/USN-5051-2 + - https://ubuntu.com/security/notices/USN-5051-3 + - https://ubuntu.com/security/notices/USN-5051-4 (regression only in trusty/esm) + - https://ubuntu.com/security/notices/USN-5088-1 + - https://www.debian.org/security/2021/dsa-4963 + - https://www.openssl.org/news/secadv/20210824.txt + - https://www.oracle.com/security-alerts/cpuoct2021.html + - https://www.tenable.com/security/tns-2021-16 + - https://www.tenable.com/security/tns-2022-02 + Severity: HIGH + Title: "openssl: Read buffer overruns processing ASN.1 strings" + VendorSeverity: + alma: 2.0 + amazon: 2.0 + arch-linux: 3.0 + cbl-mariner: 3.0 + nvd: 3.0 + oracle-oval: 2.0 + photon: 3.0 + redhat: 2.0 + rocky: 2.0 + ubuntu: 2.0 + - key: CVE-2021-38193 + value: + CVSS: + nvd: + V2Score: 4.3 + V2Vector: AV:N/AC:M/Au:N/C:N/I:P/A:N + V3Score: 6.1 + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N + CweIDs: + - CWE-79 + Description: An issue was discovered in the ammonia crate before 3.1.0 for Rust. XSS can occur because the parsing differences for HTML, SVG, and MathML are mishandled, a similar issue to CVE-2020-26870. + LastModifiedDate: 2021-08-16T16:37:00Z + PublishedDate: 2021-08-08T06:15:00Z + References: + - https://crates.io/crates/ammonia + - https://github.com/rust-ammonia/ammonia/pull/142 + - https://raw.githubusercontent.com/rustsec/advisory-db/main/crates/ammonia/RUSTSEC-2021-0074.md + - https://rustsec.org/advisories/RUSTSEC-2021-0074.html + Severity: MEDIUM + Title: Incorrect handling of embedded SVG and MathML leads to mutation XSS + VendorSeverity: + nvd: 2.0 + - key: CVE-2022-0158 + value: + CVSS: + nvd: + V2Score: 4.3 + V2Vector: AV:N/AC:M/Au:N/C:P/I:N/A:N + V3Score: 3.3 + V3Vector: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N + redhat: + V3Score: 3.3 + V3Vector: CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N + CweIDs: + - CWE-122 + Description: vim is vulnerable to Heap-based Buffer Overflow + LastModifiedDate: 2022-01-15T16:15:00Z + PublishedDate: 2022-01-10T16:15:00Z + References: + - http://www.openwall.com/lists/oss-security/2022/01/15/1 + - https://access.redhat.com/security/cve/CVE-2022-0158 + - https://github.com/vim/vim/commit/5f25c3855071bd7e26255c68bf458b1b5cf92f39 + - https://huntr.dev/bounties/ac5d7005-07c6-4a0a-b251-ba9cdbf6738b + - https://huntr.dev/bounties/ac5d7005-07c6-4a0a-b251-ba9cdbf6738b/ + - https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/HD5S2FC2HF22A7XQXK2XXIR46EARVWIM/ + - https://nvd.nist.gov/vuln/detail/CVE-2022-0158 + Severity: LOW + Title: "vim: heap-based read buffer overflow in compile_get_env()" + VendorSeverity: + cbl-mariner: 1.0 + nvd: 1.0 + redhat: 1.0 + - key: CVE-2022-0261 + value: + CweIDs: + - CWE-122 + Description: Heap-based Buffer Overflow in GitHub repository vim/vim prior to 8.2. + LastModifiedDate: 2022-01-18T16:15:00Z + PublishedDate: 2022-01-18T16:15:00Z + References: + - https://github.com/vim/vim/commit/9f8c304c8a390ade133bac29963dc8e56ab14cbc + - https://huntr.dev/bounties/fa795954-8775-4f23-98c6-d4d4d3fe8a82 + - https://nvd.nist.gov/vuln/detail/CVE-2022-0261 + Severity: HIGH + Title: CVE-2022-0261 affecting package vim 8.2.4081 + VendorSeverity: + cbl-mariner: 3.0 + - key: openSUSE-SU-2020:0062-1 + value: + Description: "This update for openssl-1_1 fixes the following issues:\n\nSecurity issue fixed:\n\n- CVE-2019-1551: Fixed an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli (bsc#1158809). \n\nVarious FIPS related improvements were done:\n\n- FIPS: Backport SSH KDF to openssl (jsc#SLE-8789, bsc#1157775).\n- Port FIPS patches from SLE-12 (bsc#1158101).\n- Use SHA-2 in the RSA pairwise consistency check (bsc#1155346).\n\nThis update was imported from the SUSE:SLE-15-SP1:Update update project." + References: + - https://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html + - https://www.suse.com/support/security/rating/ + Severity: MEDIUM + Title: Security update for openssl-1_1 + VendorSeverity: + suse-cvrf: 2.0 + - key: CVE-2022-24765 + value: + Title: "Git for Windows is a fork of Git containing Windows-specific patches. ..." + Description: "Git for Windows is a fork of Git containing Windows-specific patches." + CweIDs: + - CWE-427 + References: + - http://www.openwall.com/lists/oss-security/2022/04/12/7 + - https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24765 + - https://git-scm.com/book/en/v2/Appendix-A%3A-Git-in-Other-Environments-Git-in-Bash + - https://git-scm.com/docs/git#Documentation/git.txt-codeGITCEILINGDIRECTORIEScode + - https://github.com/git-for-windows/git/security/advisories/GHSA-vw2c-22j4-2fh2 + - https://ubuntu.com/security/notices/USN-5376-1 + Severity: MEDIUM + VendorSeverity: + ubuntu: 2 + LastModifiedDate: 2022-04-12T21:15:00Z + PublishedDate: 2022-04-12T18:15:00Z + - key: GMS-2022-20 + value: + Title: OCI Manifest Type Confusion Issue + Description: "### Impact\n\nSystems that rely on digest equivalence for image attestations may be vulnerable to type confusion." + Severity: UNKNOWN + References: + - https://github.com/advisories/GHSA-qq97-vm5h-rrhg + - https://github.com/distribution/distribution/commit/b59a6f827947f9e0e67df0cfb571046de4733586 + - https://github.com/distribution/distribution/security/advisories/GHSA-qq97-vm5h-rrhg + - https://github.com/opencontainers/image-spec/pull/411 + - key: CVE-2022-23628 + value: + Title: Incorrect Calculation + Description: "OPA is an open source, general-purpose policy engine. Under certain conditions, pretty-printing an abstract syntax tree (AST) that contains synthetic nodes could change the logic of some statements by reordering array literals. Example of policies impacted are those that parse and compare web paths. **All of these** three conditions have to be met to create an adverse effect: 1. An AST of Rego had to be **created programmatically** such that it ends up containing terms without a location (such as wildcard variables). 2. The AST had to be **pretty-printed** using the `github.com/open-policy-agent/opa/format` package. 3. The result of the pretty-printing had to be **parsed and evaluated again** via an OPA instance using the bundles, or the Golang packages. If any of these three conditions are not met, you are not affected. Notably, all three would be true if using **optimized bundles**, i.e. bundles created with `opa build -O=1` or higher. In that case, the optimizer would fulfil condition (1.), the result of that would be pretty-printed when writing the bundle to disk, fulfilling (2.). When the bundle was then used, we'd satisfy (3.). As a workaround users may disable optimization when creating bundles." + Severity: MEDIUM + CweIDs: + - CWE-682 + VendorSeverity: + nvd: 2 + CVSS: + nvd: + V2Vector: AV:N/AC:M/Au:N/C:N/I:P/A:N + V3Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N + V2Score: 4.3 + V3Score: 5.3 + References: + - https://github.com/advisories/GHSA-hcw3-j74m-qc58 + - https://github.com/open-policy-agent/opa/commit/932e4ffc37a590ace79e9b75ca4340288c220239 + - https://github.com/open-policy-agent/opa/commit/bfd984ddf93ef2c4963a08d4fdadae0bcf1a3717 + - https://github.com/open-policy-agent/opa/pull/3851 + - https://github.com/open-policy-agent/opa/security/advisories/GHSA-hcw3-j74m-qc58 + - https://nvd.nist.gov/vuln/detail/CVE-2022-23628 + PublishedDate: '2022-02-09T22:15:00Z' + LastModifiedDate: '2022-02-17T02:37:00Z' + - key: CVE-2021-38561 + value: + Description: "Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n" + Severity: UNKNOWN + References: + - https://go-review.googlesource.com/c/text/+/340830 + - https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f + - https://pkg.go.dev/vuln/GO-2021-0113 + - key: GHSA-5crp-9r3c-p9vr + value: + Title: "Improper Handling of Exceptional Conditions in Newtonsoft.Json" + Description: "Newtonsoft.Json prior to version 13.0.1 is vulnerable to Insecure Defaults due to improper handling of expressions with high nesting level that lead to StackOverFlow exception or high CPU and RAM usage." + Severity: HIGH + VendorSeverity: + ghsa: 3 + CweIDs: + - CWE-755 + CVSS: + ghsa: + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + V3Score: 7.5 + References: + - https://alephsecurity.com/2018/10/22/StackOverflowException/ + - https://alephsecurity.com/vulns/aleph-2018004 + PublishedDate: "2022-06-22T15:08:47Z" + LastModifiedDate: "2022-06-27T18:37:23Z" + - key: CVE-2022-42975 + value: + Title: "Phoenix before 1.6.14 mishandles check_origin wildcarding" + Description: "socket/transport.ex in Phoenix before 1.6.14 mishandles check_origin wildcarding. NOTE: LiveView applications are unaffected by default because of the presence of a LiveView CSRF token." + Severity: HIGH + VendorSeverity: + ghsa: 3 + CVSS: + ghsa: + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + V3Score: 7.5 + References: + - https://nvd.nist.gov/vuln/detail/CVE-2022-42975 + - https://github.com/phoenixframework/phoenix/commit/6e7185b33a59e0b1d1c0b4223adf340a73e963ae + - https://hexdocs.pm/phoenix/1.6.14/changelog.html#1-6-14-2022-10-10 + - https://github.com/advisories/GHSA-p8f7-22gq-m7j9 + PublishedDate: "2022-10-17T12:00:27Z" + LastModifiedDate: "2022-10-18T18:01:44Z" + - key: CVE-2020-35669 + value: + Title: "http before 0.13.3 vulnerable to header injection" + Description: "An issue was discovered in the http package before 0.13.3 for Dart. If the attacker controls the HTTP method and the app is using Request directly, it's possible to achieve CRLF injection in an HTTP request via HTTP header injection. This issue has been addressed in commit abb2bb182 by validating request methods." + Severity: MEDIUM + VendorSeverity: + ghsa: 2 + CweIDs: + - CWE-74 + CVSS: + ghsa: + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N" + V3Score: 6.1 + References: + - https://nvd.nist.gov/vuln/detail/CVE-2020-35669 + - https://github.com/dart-lang/http/issues/511 + - https://github.com/dart-lang/http/blob/master/CHANGELOG.md#0133 + - https://github.com/dart-lang/http/pull/512 + - https://github.com/dart-lang/http/commit/abb2bb182fbd7f03aafd1f889b902d7b3bdb8769 + - https://pub.dev/packages/http/changelog#0133 + - https://github.com/advisories/GHSA-4rgh-jx4f-qfcq + PublishedDate: "2022-05-24T17:37:16Z" + LastModifiedDate: "2022-10-06T20:26:08Z" + - key: CVE-2022-3215 + value: + Title: "SwiftNIO vulnerable to Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting')" + Description: "`NIOHTTP1` and projects using it for generating HTTP responses, including SwiftNIO, can be subject to a HTTP Response Injection attack..." + Severity: MEDIUM + VendorSeverity: + ghsa: 2 + CVSS: + ghsa: + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N" + V3Score: 5.3 + References: + - https://github.com/apple/swift-nio/security/advisories/GHSA-7fj7-39wj-c64f + - https://nvd.nist.gov/vuln/detail/CVE-2022-3215 + - https://github.com/apple/swift-nio/commit/a16e2f54a25b2af217044e5168997009a505930f + - https://github.com/advisories/GHSA-7fj7-39wj-c64f + PublishedDate: "2023-06-07T16:01:53Z" + LastModifiedDate: "2023-06-19T16:45:07Z" + - key: CVE-2022-24775 + value: + Title: "Improper Input Validation in guzzlehttp/psr7" + Description: "### Impact\nIn proper header parsing. An attacker could sneak in a new line character and pass untrusted values. \n\n### Patches\nThe issue is patched in 1.8.4 and 2.1.1.\n\n### Workarounds\nThere are no known workarounds.\n" + Severity: HIGH + VendorSeverity: + ghsa: 3 + CweIDs: + - CWE-20 + CVSS: + ghsa: + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N" + V3Score: 7.5 + References: + - https://github.com/guzzle/psr7/security/advisories/GHSA-q7rv-6hp3-vh96 + - https://nvd.nist.gov/vuln/detail/CVE-2022-24775 + PublishedDate: "2022-03-25T19:26:33Z" + LastModifiedDate: "2022-06-14T20:02:29Z" + - key: openSUSE-SU-2024:13065-1 + value: + Title: "libopenssl-3-devel-3.1.1-3.1 on GA media" + Description: "These are all security issues fixed in the libopenssl-3-devel-3.1.1-3.1 package on the GA media of openSUSE Tumbleweed." + Severity: MEDIUM + References: + - "https://www.suse.com/security/cve/CVE-2023-2975/" + - "https://www.suse.com/security/cve/CVE-2023-3446/" + - "https://www.suse.com/support/security/rating/" + - key: SUSE-SU-2022:2251-1 + value: + Title: "Security update for openssl-1_1" + Description: "This update for openssl-1_1 fixes the following issues:\nCVE-2022-1292: Fixed command injection in c_rehash (bsc#1199166).\nCVE-2022-2068: Fixed more shell code injection issues in c_rehash. (bsc#1200550)" + Severity: MEDIUM + References: + - "https://www.suse.com/security/cve/CVE-2022-1292/" + - "https://www.suse.com/security/cve/CVE-2022-2068/" + - "https://www.suse.com/support/security/rating/" + - key: CVE-2022-22965 + value: + Title: "spring-framework: RCE via Data Binding on JDK 9+" + Description: "A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it." + Severity: CRITICAL + CweIDs: + - CWE-94 + VendorSeverity: + nvd: 4 + ghsa: 4 + redhat: 3 + CVSS: + ghsa: + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + V3Score: 9.8 + nvd: + V2Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P" + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + V2Score: 7.5 + V3Score: 9.8 + redhat: + V3Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H" + V3Score: 8.1 + References: + - "https://github.com/advisories/GHSA-36p3-wjmg-h94x" + PublishedDate: "2022-04-01T23:15:00Z" + LastModifiedDate: "2022-05-19T14:21:00Z" + - key: CVE-2020-14155 + value: + Title: "pcre: Integer overflow when parsing callout numeric arguments" + Description: "libpcre in PCRE before 8.44 allows an integer overflow via a large number after a (?C substring." + Severity: MEDIUM + CweIDs: + - CWE-190 + VendorSeverity: + alma: 1 + nvd: 2 + CVSS: + nvd: + V2Vector: "AV:N/AC:L/Au:N/C:N/I:N/A:P" + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + V2Score: 5 + V3Score: 5.3 + redhat: + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L" + V3Score: 5.3 + References: + - "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-14155" + - "https://nvd.nist.gov/vuln/detail/CVE-2020-14155" + PublishedDate: "2020-06-15T17:15:00Z" + LastModifiedDate: "2022-04-28T15:06:00Z" + - key: CVE-2022-40897 + value: + Title: "pypa-setuptools: Regular Expression Denial of Service (ReDoS) in package_index.py" + Description: "Python Packaging Authority (PyPA) setuptools before 65.5.1 allows remote attackers to cause a denial of service via HTML in a crafted package or custom PackageIndex page. There is a Regular Expression Denial of Service (ReDoS) in package_index.py." + Severity: MEDIUM + CweIDs: + - CWE-1333 + VendorSeverity: + ghsa: 3 + nvd: 2 + CVSS: + nvd: + V3Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H" + V3Score: 5.9 + References: + - "https://access.redhat.com/errata/RHSA-2023:0952" + - "https://access.redhat.com/security/cve/CVE-2022-40897" + PublishedDate: "2022-12-23T00:15:13.987Z" + LastModifiedDate: "2024-06-21T19:15:23.877Z" diff --git a/integration/testdata/fixtures/k8s/kube-config-template b/integration/testdata/fixtures/k8s/kube-config-template new file mode 100644 index 000000000000..90b8b082e579 --- /dev/null +++ b/integration/testdata/fixtures/k8s/kube-config-template @@ -0,0 +1,18 @@ +apiVersion: v1 +clusters: +- cluster: + certificate-authority-data: {{CA}} + server: {{URL}} + name: kind-kind-test +contexts: +- context: + cluster: kind-kind-test + namespace: limitedns + user: limiteduser + name: limitedcontext +kind: Config +preferences: {} +users: +- name: limiteduser + user: + token: {{TOKEN}} \ No newline at end of file diff --git a/integration/testdata/fixtures/k8s/limited-binding.yaml b/integration/testdata/fixtures/k8s/limited-binding.yaml new file mode 100644 index 000000000000..6844922417be --- /dev/null +++ b/integration/testdata/fixtures/k8s/limited-binding.yaml @@ -0,0 +1,15 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: limited-binding + namespace: limitedns + +subjects: +- kind: ServiceAccount + name: limiteduser + namespace: default + +roleRef: + kind: Role + name: limited-role + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/integration/testdata/fixtures/k8s/limited-pod.yaml b/integration/testdata/fixtures/k8s/limited-pod.yaml new file mode 100644 index 000000000000..a4968e7fae84 --- /dev/null +++ b/integration/testdata/fixtures/k8s/limited-pod.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Pod +metadata: + name: limited-pod + namespace: limitedns +spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/integration/testdata/fixtures/k8s/limited-role.yaml b/integration/testdata/fixtures/k8s/limited-role.yaml new file mode 100644 index 000000000000..31dff1fb1f26 --- /dev/null +++ b/integration/testdata/fixtures/k8s/limited-role.yaml @@ -0,0 +1,12 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: limited-role + namespace: limitedns +rules: +- apiGroups: [""] + resources: ["*"] + verbs: ["list"] +- apiGroups: ["apps", "batch", "networking.k8s.io","rbac.authorization.k8s.io"] + resources: ["*"] + verbs: ["list"] diff --git a/integration/testdata/fixtures/k8s/summary-ids.json.golden b/integration/testdata/fixtures/k8s/summary-ids.json.golden new file mode 100644 index 000000000000..bb3ecfbd7329 --- /dev/null +++ b/integration/testdata/fixtures/k8s/summary-ids.json.golden @@ -0,0 +1,31 @@ +{ + "vulnerabilities": [ + "CVE-2019-1551", + "CVE-2019-1563", + "CVE-2019-18276", + "CVE-2019-5094", + "CVE-2019-5094", + "CVE-2019-5094", + "CVE-2019-5094" + ], + "misconfigurations": [ + "KSV001", + "KSV003", + "KSV004", + "KSV011", + "KSV012", + "KSV014", + "KSV015", + "KSV016", + "KSV018", + "KSV020", + "KSV021", + "KSV030", + "KSV104", + "KSV106", + "KSV110", + "KSV117", + "KSV118", + "KSV118" + ] +} diff --git a/integration/testdata/fixtures/k8s/test_nginx.yaml b/integration/testdata/fixtures/k8s/test_nginx.yaml new file mode 100644 index 000000000000..c295e5f91af2 --- /dev/null +++ b/integration/testdata/fixtures/k8s/test_nginx.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 diff --git a/integration/testdata/fixtures/repo/bun/bun.lock b/integration/testdata/fixtures/repo/bun/bun.lock new file mode 100644 index 000000000000..575ff25a1f24 --- /dev/null +++ b/integration/testdata/fixtures/repo/bun/bun.lock @@ -0,0 +1,30 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "buntest", + "dependencies": { + "jquery": "3.3.1", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="], + + "@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="], + + "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="], + + "jquery": ["jquery@3.3.1", "", {}, "sha512-Ubldcmxp5np52/ENotGxlLe6aGMvmF4R8S6tZjsP6Knsaxd/xp3Zrh50cG93lR6nPXyUFwzN3ZSOQI0wRJNdGg=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/integration/testdata/fixtures/repo/cargo/Cargo.lock b/integration/testdata/fixtures/repo/cargo/Cargo.lock new file mode 100644 index 000000000000..d3b73a3baf7e --- /dev/null +++ b/integration/testdata/fixtures/repo/cargo/Cargo.lock @@ -0,0 +1,81 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr 2.7.4", +] + +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "memchr 1.0.2", + "regex", + "winapi", +] + +[[package]] +name = "libc" +version = "0.2.171" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "aho-corasick", + "memchr 2.7.4", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/integration/testdata/fixtures/repo/cargo/Cargo.toml b/integration/testdata/fixtures/repo/cargo/Cargo.toml new file mode 100644 index 000000000000..86bc84d93087 --- /dev/null +++ b/integration/testdata/fixtures/repo/cargo/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "app" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +regex = "=1.7.3" + +[target.'cfg(not(target_os = "windows"))'.dependencies] +memchr = { version = "1.*", optional = true } + +[dev-dependencies] +winapi = "*" \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/cocoapods/Podfile.lock b/integration/testdata/fixtures/repo/cocoapods/Podfile.lock new file mode 100644 index 000000000000..905fe5fc5119 --- /dev/null +++ b/integration/testdata/fixtures/repo/cocoapods/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - _NIODataStructures (2.41.0) + +DEPENDENCIES: + - _NIODataStructures (= 2.41.0) + +SPEC REPOS: + trunk: + - _NIODataStructures + +SPEC CHECKSUMS: + _NIODataStructures: 3d45d8e70a1d17a15b1dc59d102c63dbc0525ffd + +PODFILE CHECKSUM: 2acff18c7f9246879b6a1a2d04e5decbc9410ef4 + +COCOAPODS: 1.12.1 \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/composer-vendor/installed.json b/integration/testdata/fixtures/repo/composer-vendor/installed.json new file mode 100644 index 000000000000..532876cd7ff5 --- /dev/null +++ b/integration/testdata/fixtures/repo/composer-vendor/installed.json @@ -0,0 +1,222 @@ +{ + "packages": [ + { + "name": "guzzlehttp/psr7", + "version": "1.8.3", + "version_normalized": "1.8.3.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", + "reference": "1afdd860a2566ed3c2b0b4a3de6e23434a79ec85", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "time": "2021-10-05T13:56:00+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.7-dev" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.8.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "install-path": "../guzzlehttp/psr7" + }, + { + "name": "psr/http-message", + "version": "1.1", + "version_normalized": "1.1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "time": "2023-04-04T09:50:52+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/1.1" + }, + "install-path": "../psr/http-message" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "version_normalized": "3.0.3.0", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "time": "2019-03-08T08:55:37+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "install-path": "../ralouphie/getallheaders" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/integration/testdata/fixtures/repo/composer/composer.json b/integration/testdata/fixtures/repo/composer/composer.json new file mode 100644 index 000000000000..99e5b5f9aefd --- /dev/null +++ b/integration/testdata/fixtures/repo/composer/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "guzzlehttp/guzzle": "7.4.4" + } +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/composer/composer.lock b/integration/testdata/fixtures/repo/composer/composer.lock new file mode 100644 index 000000000000..16bd667e36b5 --- /dev/null +++ b/integration/testdata/fixtures/repo/composer/composer.lock @@ -0,0 +1,256 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "31bb67df1cc2bbbb0cd07dd1a0f516df", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "7.4.4", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "e3ff079b22820c2029d4c2a87796b6a0b8716ad8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/e3ff079b22820c2029d4c2a87796b6a0b8716ad8", + "reference": "e3ff079b22820c2029d4c2a87796b6a0b8716ad8", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/psr7": "^1.8.2 || ^2.1", + "php": "^7.2.5 || ^8.0" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "ext-curl": "*", + "php-http/client-integration-tests": "^3.0", + "phpunit/phpunit": "^8.5.5 || ^9.3.5", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.4-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.4.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-06-09T21:39:15+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.8.3", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf", + "reference": "3cf1b6d4f0c820a2cf8bcaec39fc698f3443b5cf", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.1", + "http-interop/http-factory-tests": "^0.9", + "phpunit/phpunit": "^8.5.29 || ^9.5.23" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "2.4-dev" + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.4.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2023-03-09T13:19:02+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/integration/testdata/fixtures/repo/conan/conan.lock b/integration/testdata/fixtures/repo/conan/conan.lock new file mode 100644 index 000000000000..3c066117ed59 --- /dev/null +++ b/integration/testdata/fixtures/repo/conan/conan.lock @@ -0,0 +1,77 @@ +{ + "graph_lock": { + "nodes": { + "0": { + "options": "bzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nexpat:char_type=char\nexpat:fPIC=True\nexpat:shared=False\nopenssl:386=False\nopenssl:enable_weak_ssl_ciphers=False\nopenssl:fPIC=True\nopenssl:no_aria=False\nopenssl:no_asm=False\nopenssl:no_async=False\nopenssl:no_bf=False\nopenssl:no_blake2=False\nopenssl:no_camellia=False\nopenssl:no_cast=False\nopenssl:no_chacha=False\nopenssl:no_cms=False\nopenssl:no_comp=False\nopenssl:no_ct=False\nopenssl:no_deprecated=False\nopenssl:no_des=False\nopenssl:no_dgram=False\nopenssl:no_dh=False\nopenssl:no_dsa=False\nopenssl:no_dso=False\nopenssl:no_ec=False\nopenssl:no_ecdh=False\nopenssl:no_ecdsa=False\nopenssl:no_engine=False\nopenssl:no_filenames=False\nopenssl:no_gost=False\nopenssl:no_hmac=False\nopenssl:no_idea=False\nopenssl:no_md4=False\nopenssl:no_md5=False\nopenssl:no_mdc2=False\nopenssl:no_ocsp=False\nopenssl:no_pinshared=False\nopenssl:no_rc2=False\nopenssl:no_rfc3779=False\nopenssl:no_rmd160=False\nopenssl:no_rsa=False\nopenssl:no_seed=False\nopenssl:no_sha=False\nopenssl:no_sm2=False\nopenssl:no_sm3=False\nopenssl:no_sm4=False\nopenssl:no_sock=False\nopenssl:no_srp=False\nopenssl:no_srtp=False\nopenssl:no_sse2=False\nopenssl:no_ssl=False\nopenssl:no_ssl3=False\nopenssl:no_stdio=False\nopenssl:no_tests=False\nopenssl:no_threads=False\nopenssl:no_tls1=False\nopenssl:no_ts=False\nopenssl:no_whirlpool=False\nopenssl:openssldir=None\nopenssl:shared=False\npcre:build_pcre_16=True\npcre:build_pcre_32=True\npcre:build_pcre_8=True\npcre:build_pcrecpp=False\npcre:build_pcregrep=True\npcre:fPIC=True\npcre:shared=False\npcre:with_bzip2=True\npcre:with_jit=False\npcre:with_stack_for_recursion=True\npcre:with_unicode_properties=True\npcre:with_utf=True\npcre:with_zlib=True\npoco:enable_active_record=deprecated\npoco:enable_apacheconnector=False\npoco:enable_cppparser=False\npoco:enable_crypto=True\npoco:enable_data=True\npoco:enable_data_odbc=False\npoco:enable_data_sqlite=True\npoco:enable_encodings=True\npoco:enable_fork=True\npoco:enable_json=True\npoco:enable_mongodb=True\npoco:enable_net=True\npoco:enable_netssl=True\npoco:enable_pagecompiler=False\npoco:enable_pagecompiler_file2page=False\npoco:enable_pdf=False\npoco:enable_pocodoc=False\npoco:enable_redis=True\npoco:enable_sevenzip=False\npoco:enable_util=True\npoco:enable_xml=True\npoco:enable_zip=True\npoco:fPIC=True\npoco:shared=False\nsqlite3:build_executable=True\nsqlite3:disable_gethostuuid=False\nsqlite3:enable_column_metadata=True\nsqlite3:enable_dbpage_vtab=False\nsqlite3:enable_dbstat_vtab=False\nsqlite3:enable_default_secure_delete=False\nsqlite3:enable_default_vfs=True\nsqlite3:enable_explain_comments=False\nsqlite3:enable_fts3=False\nsqlite3:enable_fts3_parenthesis=False\nsqlite3:enable_fts4=False\nsqlite3:enable_fts5=False\nsqlite3:enable_json1=False\nsqlite3:enable_math_functions=True\nsqlite3:enable_preupdate_hook=False\nsqlite3:enable_rtree=True\nsqlite3:enable_soundex=False\nsqlite3:enable_unlock_notify=True\nsqlite3:fPIC=True\nsqlite3:max_blob_size=None\nsqlite3:max_column=None\nsqlite3:max_variable_number=None\nsqlite3:omit_deprecated=False\nsqlite3:omit_load_extension=False\nsqlite3:shared=False\nsqlite3:threadsafe=1\nsqlite3:use_alloca=False\nzlib:fPIC=True\nzlib:shared=False", + "requires": [ + "1" + ], + "path": "../conanfile.txt", + "context": "host" + }, + "1": { + "ref": "poco/1.9.4", + "options": "enable_active_record=deprecated\nenable_apacheconnector=False\nenable_cppparser=False\nenable_crypto=True\nenable_data=True\nenable_data_odbc=False\nenable_data_sqlite=True\nenable_encodings=True\nenable_fork=True\nenable_json=True\nenable_mongodb=True\nenable_net=True\nenable_netssl=True\nenable_pagecompiler=False\nenable_pagecompiler_file2page=False\nenable_pdf=False\nenable_pocodoc=False\nenable_redis=True\nenable_sevenzip=False\nenable_util=True\nenable_xml=True\nenable_zip=True\nfPIC=True\nshared=False\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nexpat:char_type=char\nexpat:fPIC=True\nexpat:shared=False\nopenssl:386=False\nopenssl:enable_weak_ssl_ciphers=False\nopenssl:fPIC=True\nopenssl:no_aria=False\nopenssl:no_asm=False\nopenssl:no_async=False\nopenssl:no_bf=False\nopenssl:no_blake2=False\nopenssl:no_camellia=False\nopenssl:no_cast=False\nopenssl:no_chacha=False\nopenssl:no_cms=False\nopenssl:no_comp=False\nopenssl:no_ct=False\nopenssl:no_deprecated=False\nopenssl:no_des=False\nopenssl:no_dgram=False\nopenssl:no_dh=False\nopenssl:no_dsa=False\nopenssl:no_dso=False\nopenssl:no_ec=False\nopenssl:no_ecdh=False\nopenssl:no_ecdsa=False\nopenssl:no_engine=False\nopenssl:no_filenames=False\nopenssl:no_gost=False\nopenssl:no_hmac=False\nopenssl:no_idea=False\nopenssl:no_md4=False\nopenssl:no_md5=False\nopenssl:no_mdc2=False\nopenssl:no_ocsp=False\nopenssl:no_pinshared=False\nopenssl:no_rc2=False\nopenssl:no_rfc3779=False\nopenssl:no_rmd160=False\nopenssl:no_rsa=False\nopenssl:no_seed=False\nopenssl:no_sha=False\nopenssl:no_sm2=False\nopenssl:no_sm3=False\nopenssl:no_sm4=False\nopenssl:no_sock=False\nopenssl:no_srp=False\nopenssl:no_srtp=False\nopenssl:no_sse2=False\nopenssl:no_ssl=False\nopenssl:no_ssl3=False\nopenssl:no_stdio=False\nopenssl:no_tests=False\nopenssl:no_threads=False\nopenssl:no_tls1=False\nopenssl:no_ts=False\nopenssl:no_whirlpool=False\nopenssl:openssldir=None\nopenssl:shared=False\npcre:build_pcre_16=True\npcre:build_pcre_32=True\npcre:build_pcre_8=True\npcre:build_pcrecpp=False\npcre:build_pcregrep=True\npcre:fPIC=True\npcre:shared=False\npcre:with_bzip2=True\npcre:with_jit=False\npcre:with_stack_for_recursion=True\npcre:with_unicode_properties=True\npcre:with_utf=True\npcre:with_zlib=True\nsqlite3:build_executable=True\nsqlite3:disable_gethostuuid=False\nsqlite3:enable_column_metadata=True\nsqlite3:enable_dbpage_vtab=False\nsqlite3:enable_dbstat_vtab=False\nsqlite3:enable_default_secure_delete=False\nsqlite3:enable_default_vfs=True\nsqlite3:enable_explain_comments=False\nsqlite3:enable_fts3=False\nsqlite3:enable_fts3_parenthesis=False\nsqlite3:enable_fts4=False\nsqlite3:enable_fts5=False\nsqlite3:enable_json1=False\nsqlite3:enable_math_functions=True\nsqlite3:enable_preupdate_hook=False\nsqlite3:enable_rtree=True\nsqlite3:enable_soundex=False\nsqlite3:enable_unlock_notify=True\nsqlite3:fPIC=True\nsqlite3:max_blob_size=None\nsqlite3:max_column=None\nsqlite3:max_variable_number=None\nsqlite3:omit_deprecated=False\nsqlite3:omit_load_extension=False\nsqlite3:shared=False\nsqlite3:threadsafe=1\nsqlite3:use_alloca=False\nzlib:fPIC=True\nzlib:shared=False", + "package_id": "c3c2e0fbf9199382c510453d5fa86501149cf57a", + "prev": "0", + "requires": [ + "2", + "4", + "5", + "6", + "7" + ], + "context": "host" + }, + "2": { + "ref": "pcre/8.43", + "options": "build_pcre_16=True\nbuild_pcre_32=True\nbuild_pcre_8=True\nbuild_pcrecpp=False\nbuild_pcregrep=True\nfPIC=True\nshared=False\nwith_bzip2=True\nwith_jit=False\nwith_stack_for_recursion=True\nwith_unicode_properties=True\nwith_utf=True\nwith_zlib=True\nbzip2:build_executable=True\nbzip2:fPIC=True\nbzip2:shared=False\nzlib:fPIC=True\nzlib:shared=False", + "package_id": "fab187555fa87e54b51a5e8e8ff95b0f5855d00b", + "prev": "0", + "requires": [ + "3", + "4" + ], + "context": "host" + }, + "3": { + "ref": "bzip2/1.0.8", + "options": "build_executable=True\nfPIC=True\nshared=False", + "package_id": "3df6ebb8a308d309e882b21988fd9ea103560e16", + "prev": "0", + "context": "host" + }, + "4": { + "ref": "zlib/1.2.12", + "options": "fPIC=True\nshared=False", + "package_id": "76f87539fc90ff313e0b3182641a9bb558a717d2", + "prev": "0", + "context": "host" + }, + "5": { + "ref": "expat/2.4.8", + "options": "char_type=char\nfPIC=True\nshared=False", + "package_id": "b025735bb0d121754b0b4aaae6c02d3b9546c56f", + "prev": "0", + "context": "host" + }, + "6": { + "ref": "sqlite3/3.39.2", + "options": "build_executable=True\ndisable_gethostuuid=False\nenable_column_metadata=True\nenable_dbpage_vtab=False\nenable_dbstat_vtab=False\nenable_default_secure_delete=False\nenable_default_vfs=True\nenable_explain_comments=False\nenable_fts3=False\nenable_fts3_parenthesis=False\nenable_fts4=False\nenable_fts5=False\nenable_json1=False\nenable_math_functions=True\nenable_preupdate_hook=False\nenable_rtree=True\nenable_soundex=False\nenable_unlock_notify=True\nfPIC=True\nmax_blob_size=None\nmax_column=None\nmax_variable_number=None\nomit_deprecated=False\nomit_load_extension=False\nshared=False\nthreadsafe=1\nuse_alloca=False", + "package_id": "bc01b0a8d9a484b3b4226ef647e2ba7dd5b627ed", + "prev": "0", + "context": "host" + }, + "7": { + "ref": "openssl/1.1.1q", + "options": "386=False\nenable_weak_ssl_ciphers=False\nfPIC=True\nno_aria=False\nno_asm=False\nno_async=False\nno_bf=False\nno_blake2=False\nno_camellia=False\nno_cast=False\nno_chacha=False\nno_cms=False\nno_comp=False\nno_ct=False\nno_deprecated=False\nno_des=False\nno_dgram=False\nno_dh=False\nno_dsa=False\nno_dso=False\nno_ec=False\nno_ecdh=False\nno_ecdsa=False\nno_engine=False\nno_filenames=False\nno_gost=False\nno_hmac=False\nno_idea=False\nno_md4=False\nno_md5=False\nno_mdc2=False\nno_ocsp=False\nno_pinshared=False\nno_rc2=False\nno_rfc3779=False\nno_rmd160=False\nno_rsa=False\nno_seed=False\nno_sha=False\nno_sm2=False\nno_sm3=False\nno_sm4=False\nno_sock=False\nno_srp=False\nno_srtp=False\nno_sse2=False\nno_ssl=False\nno_ssl3=False\nno_stdio=False\nno_tests=False\nno_threads=False\nno_tls1=False\nno_ts=False\nno_whirlpool=False\nopenssldir=None\nshared=False", + "package_id": "76f87539fc90ff313e0b3182641a9bb558a717d2", + "prev": "0", + "context": "host" + } + }, + "revisions_enabled": false + }, + "version": "0.4", + "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=5\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n" +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/conda-environment/environment.yaml b/integration/testdata/fixtures/repo/conda-environment/environment.yaml new file mode 100644 index 000000000000..cf47d3632faf --- /dev/null +++ b/integration/testdata/fixtures/repo/conda-environment/environment.yaml @@ -0,0 +1,6 @@ +name: test-env +channels: + - defaults +dependencies: + - bzip2=1.0.8=h998d150_5 +prefix: /opt/conda/envs/test-env diff --git a/integration/testdata/fixtures/repo/conda/miniconda3/envs/testenv/conda-meta/openssl-1.1.1q-h7f8727e_0.json b/integration/testdata/fixtures/repo/conda/miniconda3/envs/testenv/conda-meta/openssl-1.1.1q-h7f8727e_0.json new file mode 100644 index 000000000000..2cc3c34a1a81 --- /dev/null +++ b/integration/testdata/fixtures/repo/conda/miniconda3/envs/testenv/conda-meta/openssl-1.1.1q-h7f8727e_0.json @@ -0,0 +1,60 @@ +{ + "build": "h7f8727e_0", + "build_number": 0, + "channel": "https://repo.anaconda.com/pkgs/main/linux-64", + "constrains": [], + "depends": [ + "ca-certificates", + "libgcc-ng >=7.5.0" + ], + "extracted_package_dir": "/home/mmaitre/miniconda3/pkgs/openssl-1.1.1q-h7f8727e_0", + "features": "", + "files": [ + "bin/c_rehash", + "", + "ssl/openssl.cnf.dist" + ], + "fn": "openssl-1.1.1q-h7f8727e_0.conda", + "legacy_bz2_md5": "ad51928702694e3f6d25b7d4229c84e6", + "license": "OpenSSL", + "license_family": "Apache", + "link": { + "source": "/home/user/miniconda3/pkgs/openssl-1.1.1q-h7f8727e_0", + "type": 1 + }, + "md5": "2ac47797afee2ece8d339c18b095b8d8", + "name": "openssl", + "package_tarball_full_path": "/home/user/miniconda3/pkgs/openssl-1.1.1q-h7f8727e_0.conda", + "paths_data": { + "paths": [ + { + "_path": "bin/c_rehash", + "file_mode": "text", + "path_type": "hardlink", + "prefix_placeholder": "/opt/conda/conda-bld/openssl_1657551138854/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_place", + "sha256": "04c9f4a5c91e20a24d14a36668b5bebe826d6087fb2337b68e33a4d485f5586f", + "sha256_in_prefix": "fc2a6b708cccb8ba90d20e54a9b07257fce009bf315a36f73d3e542dd8674921", + "size_in_bytes": 6991 + }, + { + "_path": "" + }, + { + "_path": "ssl/openssl.cnf.dist", + "path_type": "hardlink", + "sha256": "f10ba64917b4458fafc1e078c2eb9e6a7602e68fc98c2e9e6df5e1636ae27d6b", + "sha256_in_prefix": "f10ba64917b4458fafc1e078c2eb9e6a7602e68fc98c2e9e6df5e1636ae27d6b", + "size_in_bytes": 10909 + } + ], + "paths_version": 1 + }, + "requested_spec": "None", + "sha256": "49804293b87141523b2606836ece8e2aaa5202983698fd91e7c36bdb8c8a8de5", + "size": 2649280, + "subdir": "linux-64", + "timestamp": 1657551292835, + "track_features": "", + "url": "https://repo.anaconda.com/pkgs/main/linux-64/openssl-1.1.1q-h7f8727e_0.conda", + "version": "1.1.1q" +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/conda/miniconda3/envs/testenv/conda-meta/pip-22.2.2-py38h06a4308_0.json b/integration/testdata/fixtures/repo/conda/miniconda3/envs/testenv/conda-meta/pip-22.2.2-py38h06a4308_0.json new file mode 100644 index 000000000000..4788d70e21aa --- /dev/null +++ b/integration/testdata/fixtures/repo/conda/miniconda3/envs/testenv/conda-meta/pip-22.2.2-py38h06a4308_0.json @@ -0,0 +1,62 @@ +{ + "build": "py38h06a4308_0", + "build_number": 0, + "channel": "https://repo.anaconda.com/pkgs/main/linux-64", + "constrains": [], + "depends": [ + "python >=3.8,<3.9.0a0", + "setuptools", + "wheel" + ], + "extracted_package_dir": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0", + "features": "", + "files": [ + "bin/pip", + "", + "lib/python3.8/site-packages/pip/py.typed" + ], + "fn": "pip-22.2.2-py38h06a4308_0.conda", + "legacy_bz2_md5": "2ac9f1cfec65a1e4ef00cc0132ecd753", + "legacy_bz2_size": 2849993, + "license": "MIT", + "license_family": "MIT", + "link": { + "source": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0", + "type": 1 + }, + "md5": "ed3e0331e7c614b3148c9911e1fc15e3", + "name": "pip", + "package_tarball_full_path": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0.conda", + "paths_data": { + "paths": [ + { + "_path": "bin/pip", + "file_mode": "text", + "path_type": "hardlink", + "prefix_placeholder": "/opt/conda/conda-bld/pip_1664552683240/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold", + "sha256": "1110c03ca2fb86e43e8b52a61cd9d7c722b2817fc893a613e1a947f5d7049008", + "sha256_in_prefix": "14ed5ba79d096035b83a469e863acbbaadd3ea6ca5f23f79eefa91fa857d3f19", + "size_in_bytes": 475 + }, + { + "_path": "" + }, + { + "_path": "lib/python3.8/site-packages/pip/py.typed", + "path_type": "hardlink", + "sha256": "10156fbcf4539ff788a73e5ee50ced48276b317ed0c1ded53fddd14a82256762", + "sha256_in_prefix": "10156fbcf4539ff788a73e5ee50ced48276b317ed0c1ded53fddd14a82256762", + "size_in_bytes": 286 + } + ], + "paths_version": 1 + }, + "requested_spec": "None", + "sha256": "3fb76b94cfa5ea9732bfb241b3d234ec0a5a48d16755c3c1ef3c94630f91eb26", + "size": 2417732, + "subdir": "linux-64", + "timestamp": 1664552878795, + "track_features": "", + "url": "https://repo.anaconda.com/pkgs/main/linux-64/pip-22.2.2-py38h06a4308_0.conda", + "version": "22.2.2" +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/custom-policy/Dockerfile b/integration/testdata/fixtures/repo/custom-policy/Dockerfile new file mode 100644 index 000000000000..e98bcba8e22b --- /dev/null +++ b/integration/testdata/fixtures/repo/custom-policy/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:3.13 + +USER nobody +HEALTHCHECK NONE diff --git a/integration/testdata/fixtures/repo/custom-policy/policy/bar.rego b/integration/testdata/fixtures/repo/custom-policy/policy/bar.rego new file mode 100644 index 000000000000..830f9b9b6257 --- /dev/null +++ b/integration/testdata/fixtures/repo/custom-policy/policy/bar.rego @@ -0,0 +1,19 @@ +# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: AVD-BAR-0001 +# avd_id: AVD-BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing +package user.bar + +deny[res] { + res := "something bad: bar" +} diff --git a/integration/testdata/fixtures/repo/custom-policy/policy/foo.rego b/integration/testdata/fixtures/repo/custom-policy/policy/foo.rego new file mode 100644 index 000000000000..aaaaeba68c40 --- /dev/null +++ b/integration/testdata/fixtures/repo/custom-policy/policy/foo.rego @@ -0,0 +1,19 @@ +# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: AVD-FOO-0001 +# avd_id: AVD-FOO-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing +package user.foo + +deny[res] { + res := "something bad: foo" +} diff --git a/integration/testdata/fixtures/repo/dockerfile/Dockerfile b/integration/testdata/fixtures/repo/dockerfile/Dockerfile new file mode 100644 index 000000000000..7e9a47af8d3a --- /dev/null +++ b/integration/testdata/fixtures/repo/dockerfile/Dockerfile @@ -0,0 +1,3 @@ +FROM alpine:3.13 + +HEALTHCHECK NONE \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/dockerfile_file_pattern/Customfile b/integration/testdata/fixtures/repo/dockerfile_file_pattern/Customfile new file mode 100644 index 000000000000..7e9a47af8d3a --- /dev/null +++ b/integration/testdata/fixtures/repo/dockerfile_file_pattern/Customfile @@ -0,0 +1,3 @@ +FROM alpine:3.13 + +HEALTHCHECK NONE \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/dotnet/datacollector.deps.json b/integration/testdata/fixtures/repo/dotnet/datacollector.deps.json new file mode 100644 index 000000000000..08699fee4a7a --- /dev/null +++ b/integration/testdata/fixtures/repo/dotnet/datacollector.deps.json @@ -0,0 +1,21 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v2.1", + "signature": "" + }, + "compilationOptions": {}, + "libraries": { + "Newtonsoft.Json/9.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-U82mHQSKaIk+lpSVCbWYKNavmNH1i5xrExDEquU1i6I5pV6UMOqRnJRSlKO3cMPfcpp0RgDY+8jUXHdQ4IfXvw==", + "path": "newtonsoft.json/9.0.1", + "hashPath": "newtonsoft.json.9.0.1.nupkg.sha512" + }, + "Microsoft.VisualStudio.TestPlatform.Common/17.2.0-release-20220408-11": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/gomod/go.mod b/integration/testdata/fixtures/repo/gomod/go.mod new file mode 100644 index 000000000000..f59c97baf941 --- /dev/null +++ b/integration/testdata/fixtures/repo/gomod/go.mod @@ -0,0 +1,27 @@ +module github.com/testdata/testdata + +go 1.17 + +require ( + github.com/open-policy-agent/opa v0.35.0 + golang.org/x/net v0.0.0-20211111083644-e5c967477495 + golang.org/x/sys v0.0.0-20211205182925-97ca703d548d +) + +require ( + github.com/docker/distribution v2.7.1+incompatible // indirect + github.com/docker/docker v20.10.11+incompatible // indirect + github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-units v0.4.0 // indirect + go.opencensus.io v0.23.0 // indirect + go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect + go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect + golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 // indirect + golang.org/x/text v0.3.6 // indirect + golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect + google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c // indirect + google.golang.org/grpc v1.38.0 // indirect + google.golang.org/protobuf v1.27.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/integration/testdata/fixtures/repo/gomod/go.sum b/integration/testdata/fixtures/repo/gomod/go.sum new file mode 100644 index 000000000000..43be8bf10cec --- /dev/null +++ b/integration/testdata/fixtures/repo/gomod/go.sum @@ -0,0 +1,1283 @@ +bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw= +github.com/Azure/go-autorest/autorest/adal v0.9.0/go.mod h1:/c022QCutn2P7uY+/oQWWNcK9YU+MH96NgK+jErpbcg= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.0/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Flaque/filet v0.0.0-20201012163910-45f684403088 h1:PnnQln5IGbhLeJOi6hVs+lCeF+B1dRfFKPGXUAez0Ww= +github.com/Flaque/filet v0.0.0-20201012163910-45f684403088/go.mod h1:TK+jB3mBs+8ZMWhU5BqZKnZWJ1MrLo8etNVg51ueTBo= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= +github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/go-winio v0.4.16-0.20201130162521-d1ffc52c7331/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3 h1:mw6pDQqv38/WGF1cO/jF5t/jyAJ2yi7CmtFLLO5tGFI= +github.com/Microsoft/go-winio v0.4.17-0.20210211115548-6eac466e5fa3/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpzfwIujj1a84= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7-0.20190325164909-8abdbb8205e4/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/Microsoft/hcsshim v0.8.7/go.mod h1:OHd7sQqRFrYd3RmSgbgji+ctCwkbq2wbEYNSzOYtcBQ= +github.com/Microsoft/hcsshim v0.8.9/go.mod h1:5692vkUqntj1idxauYlpoINNKeqCiG6Sg38RRsjT5y8= +github.com/Microsoft/hcsshim v0.8.14/go.mod h1:NtVKoYxQuTLx6gEq0L96c9Ju4JbRJ4nY2ow3VK6a9Lg= +github.com/Microsoft/hcsshim v0.8.15/go.mod h1:x38A4YbHbdxJtc0sF6oIz+RG0npwSCAvn69iY6URG00= +github.com/Microsoft/hcsshim v0.8.16 h1:8/auA4LFIZFTGrqfKhGBSXwM6/4X1fHa/xniyEHu8ac= +github.com/Microsoft/hcsshim v0.8.16/go.mod h1:o5/SZqmR7x9JNKsW3pu+nqHm0MF8vbA+VxGOoXdC600= +github.com/Microsoft/hcsshim/test v0.0.0-20201218223536-d3e5debf77da/go.mod h1:5hlzMzRKMLyo42nCZ9oml8AdTlq/0cvIaBv6tK1RehU= +github.com/Microsoft/hcsshim/test v0.0.0-20210227013316-43a75bb4edd3/go.mod h1:mw7qgWloBUl75W/gVH3cQszUg1+gUITj7D6NY7ywVnY= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8= +github.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/aquasecurity/libbpfgo v0.2.5-libbpf-0.7.0 h1:BpW7qxkveYXx8TCtvYWIvmliPqaTCz/IYs1i+Gyj0MQ= +github.com/aquasecurity/libbpfgo v0.2.5-libbpf-0.7.0/go.mod h1:/+clceXE103FaXvVTIY2HAkQjxNtkra4DRWvZYr2SKw= +github.com/aquasecurity/tracee/types v0.0.0-20220412160215-ccc81e5e59cd h1:eZ0KdM1drinNluLlfyMGKLBWaeECnuFDNrWgClli/tM= +github.com/aquasecurity/tracee/types v0.0.0-20220412160215-ccc81e5e59cd/go.mod h1:l8MikK8yNCxoFVFq+WqvRg3kiDUX4wDTQwo7oD7YnWM= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= +github.com/beorn7/perks v0.0.0-20160804104726-4c0e84591b9a/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bits-and-blooms/bitset v1.2.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= +github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= +github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= +github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytecodealliance/wasmtime-go v0.31.0 h1:AbMdV1pwjw/0Ito5yARcGzY366cq5NIiDk5vpy1c2Lw= +github.com/bytecodealliance/wasmtime-go v0.31.0/go.mod h1:q320gUxqyI8yB+ZqRuaJOEnGkAnHh6WtJjMaT2CW4wI= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= +github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg= +github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc= +github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs= +github.com/cilium/ebpf v0.6.2/go.mod h1:4tRaxcgiL706VnOzHOdBlY8IEAIdxINsQBcU4xJJXRs= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/containerd/aufs v0.0.0-20200908144142-dab0cbea06f4/go.mod h1:nukgQABAEopAHvB6j7cnP5zJ+/3aVcE7hCYqvIwAHyE= +github.com/containerd/aufs v0.0.0-20201003224125-76a6863f2989/go.mod h1:AkGGQs9NM2vtYHaUen+NljV0/baGCAPELGm2q9ZXpWU= +github.com/containerd/aufs v0.0.0-20210316121734-20793ff83c97/go.mod h1:kL5kd6KM5TzQjR79jljyi4olc1Vrx6XBlcyj3gNv2PU= +github.com/containerd/btrfs v0.0.0-20201111183144-404b9149801e/go.mod h1:jg2QkJcsabfHugurUvvPhS3E08Oxiuh5W/g1ybB4e0E= +github.com/containerd/btrfs v0.0.0-20210316141732-918d888fb676/go.mod h1:zMcX3qkXTAi9GI50+0HOeuV8LU2ryCE/V2vG/ZBiTss= +github.com/containerd/cgroups v0.0.0-20190717030353-c4b9ac5c7601/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI= +github.com/containerd/cgroups v0.0.0-20190919134610-bf292b21730f/go.mod h1:OApqhQ4XNSNC13gXIwDjhOQxjWa/NxkwZXJ1EvqT0ko= +github.com/containerd/cgroups v0.0.0-20200531161412-0dbf7f05ba59/go.mod h1:pA0z1pT8KYB3TCXK/ocprsh7MAkoW8bZVzPdih9snmM= +github.com/containerd/cgroups v0.0.0-20200710171044-318312a37340/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20200824123100-0b889c03f102/go.mod h1:s5q4SojHctfxANBDvMeIaIovkq29IP48TKAxnhYRxvo= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68 h1:hkGVFjz+plgr5UfxZUTPFbUFIF/Km6/s+RVRIRHLrrY= +github.com/containerd/cgroups v0.0.0-20210114181951-8a68de567b68/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE= +github.com/containerd/console v0.0.0-20180822173158-c12b1e7919c1/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20181022165439-0650fd9eeb50/go.mod h1:Tj/on1eG8kiEhd0+fhSDzsPAFESxzBBvdyEgyryXffw= +github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE= +github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/containerd/console v1.0.2/go.mod h1:ytZPjGgY2oeTkAONYafi2kSj0aYggsf8acV1PGKCbzQ= +github.com/containerd/containerd v1.2.10/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0-beta.2.0.20190828155532-0293cbd26c69/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.1-0.20191213020239-082f7e3aed57/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.2/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.0-beta.2.0.20200729163537-40b22ef07410/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.1/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.4.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.5.0-beta.1/go.mod h1:5HfvG1V2FsKesEGQ17k5/T7V960Tmcumvqn8Mc+pCYQ= +github.com/containerd/containerd v1.5.0-beta.3/go.mod h1:/wr9AVtEM7x9c+n0+stptlo/uBBoBORwEx6ardVcmKU= +github.com/containerd/containerd v1.5.0-beta.4 h1:zjz4MOAOFgdBlwid2nNUlJ3YLpVi/97L36lfMYJex60= +github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09ZvgqEq8EfBp/m3lcVZIvPHhI= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo= +github.com/containerd/continuity v0.0.0-20201208142359-180525291bb7/go.mod h1:kR3BEg7bDFaEddKm54WSmrol1fKWDU1nKYkgrcgZT7Y= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e h1:6JKvHHt396/qabvMhnhUZvWaHZzfVfldxE60TK8YLhg= +github.com/containerd/continuity v0.0.0-20210208174643-50096c924a4e/go.mod h1:EXlVlkqNba9rJe3j7w3Xa924itAMLgZH4UD/Q4PExuQ= +github.com/containerd/fifo v0.0.0-20180307165137-3d5202aec260/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20190226154929-a9fb20d87448/go.mod h1:ODA38xgv3Kuk8dQz2ZQXpnv/UZZUHUCL7pnLehbXgQI= +github.com/containerd/fifo v0.0.0-20200410184934-f15a3290365b/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20201026212402-0724c46b320c/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0= +github.com/containerd/fifo v0.0.0-20210316144830-115abcc95a1d/go.mod h1:ocF/ME1SX5b1AOlWi9r677YJmCPSwwWnQ9O123vzpE4= +github.com/containerd/go-cni v1.0.1/go.mod h1:+vUpYxKvAF72G9i1WoDOiPGRtQpqsNW/ZHtSlv++smU= +github.com/containerd/go-runc v0.0.0-20180907222934-5a6d9f37cfa3/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20190911050354-e029b79d8cda/go.mod h1:IV7qH3hrUgRmyYrtgEeGWJfWbgcHL9CSRruz2Vqcph0= +github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g= +github.com/containerd/go-runc v0.0.0-20201020171139-16b287bc67d0/go.mod h1:cNU0ZbCgCQVZK4lgG3P+9tn9/PaJNmoDXPpoJhDR+Ok= +github.com/containerd/imgcrypt v1.0.1/go.mod h1:mdd8cEPW7TPgNG4FpuP3sGBiQ7Yi/zak9TYCG3juvb0= +github.com/containerd/imgcrypt v1.0.4-0.20210301171431-0ae5c75f59ba/go.mod h1:6TNsg0ctmizkrOgXRNQjAPFWpMYRWuiB6dSF4Pfa5SA= +github.com/containerd/imgcrypt v1.1.1-0.20210312161619-7ed62a527887/go.mod h1:5AZJNI6sLHJljKuI9IHnw1pWqo/F0nGDOuR9zgTs7ow= +github.com/containerd/nri v0.0.0-20201007170849-eb1350a75164/go.mod h1:+2wGSDGFYfE5+So4M5syatU0N0f0LbWpuqyMi4/BE8c= +github.com/containerd/nri v0.0.0-20210316161719-dbaa18c31c14/go.mod h1:lmxnXF6oMkbqs39FiCt1s0R2HSMhcLel9vNL3m4AaeY= +github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20190828172938-92c8520ef9f8/go.mod h1:PvCDdDGpgqzQIzDW1TphrGLssLDZp2GuS+X5DkEJB8o= +github.com/containerd/ttrpc v0.0.0-20191028202541-4f1b8fe65a5c/go.mod h1:LPm1u0xBw8r8NOKoOdNMeVHSawSsltak+Ihv+etqsE8= +github.com/containerd/ttrpc v1.0.1/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/ttrpc v1.0.2/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y= +github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= +github.com/containerd/typeurl v0.0.0-20190911142611-5eb25027c9fd/go.mod h1:GeKYzf2pQcqv7tJ0AoCuuhtnqhva5LNU3U+OyKxxJpk= +github.com/containerd/typeurl v1.0.1/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg= +github.com/containerd/zfs v0.0.0-20200918131355-0a33824f23a2/go.mod h1:8IgZOBdv8fAgXddBT4dBXJPtxyRsejFIpXoklgxgEjw= +github.com/containerd/zfs v0.0.0-20210301145711-11e8f1707f62/go.mod h1:A9zfAbMlQwE+/is6hi0Xw8ktpL+6glmqZYtevJgaB8Y= +github.com/containerd/zfs v0.0.0-20210315114300-dde8f0fda960/go.mod h1:m+m51S1DvAP6r3FcmYCp54bQ34pyOwTieQDNRIRHsFY= +github.com/containernetworking/cni v0.7.1/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY= +github.com/containernetworking/plugins v0.8.6/go.mod h1:qnw5mN19D8fIwkqW7oHHYDHVlzhJpcY6TQxn/fUyDDM= +github.com/containers/ocicrypt v1.0.1/go.mod h1:MeJDzk1RJHv89LjsH0Sp5KTY3ZYkjXO/C+bKAeWFIrc= +github.com/containers/ocicrypt v1.1.0/go.mod h1:b8AOe0YR67uU8OqfVNcznfFpAzu3rdgUV4GP9qXPfu4= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20161114122254-48702e0da86b/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= +github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= +github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= +github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= +github.com/d2g/dhcp4server v0.0.0-20181031114812-7d4a0a7f59a5/go.mod h1:Eo87+Kg/IX2hfWJfwxMzLyuSZyxSoAug2nGa1G2QAi8= +github.com/d2g/hardwareaddr v0.0.0-20190221164911-e7d9fbe030e4/go.mod h1:bMl4RjIciD2oAxI7DmWRx6gbeqrkoLqv3MV0vzNad+I= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba/go.mod h1:dV8lFg6daOBZbT6/BDGIz6Y3WFGn8juu6G+CQ6LHtl0= +github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= +github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= +github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= +github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= +github.com/dgrijalva/jwt-go v0.0.0-20170104182250-a601269ab70c/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo= +github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20170721190031-9461782956ad/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-metrics v0.0.0-20180209012529-399ea8c73916/go.mod h1:/u0gXw0Gay3ceNrsHubL3BtdOL2fHf93USgMTe0W5dI= +github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= +github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897 h1:E52jfcE64UG42SwLmrW0QByONfGynWuzBvm86BoB9z8= +github.com/foxcpp/go-mockdns v0.0.0-20210729171921-fb145fc6f897/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= +github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= +github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-ini/ini v1.25.4/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-redis/redis v6.15.9+incompatible h1:K0pv1D7EQUjfyoMql+r/jZqCLizCGKFlFgcHWWmHQjg= +github.com/go-redis/redis v6.15.9+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= +github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/googleapis v1.2.0/go.mod h1:Njal3psf3qN6dwBtQfUmBZh2ybovJ0tlu3o/AC7HYjU= +github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= +github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= +github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= +github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v0.0.0-20141028054710-7554cd9344ce/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v0.0.0-20161216184304-ed905158d874/go.mod h1:JMRHfdO9jKNzS/+BTlxCjKNQHg/jZAft8U7LloJvN7I= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/xstrings v1.3.1 h1:4jgBlKK6tLKFvO8u5pmYjG91cqytmDCDvGh7ECVFfFs= +github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA= +github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= +github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jmespath/go-jmespath v0.0.0-20160803190731-bd40a432e4c7/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= +github.com/klauspost/compress v1.13.5 h1:9O69jUPDcsT9fEm74W92rZL9FQY7rCdaXVneq+yyzl4= +github.com/klauspost/compress v1.13.5/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.43 h1:JKfpVSCB84vrAmHzyrsxB5NAr5kLoMXZArPSw7Qlgyg= +github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= +github.com/miekg/pkcs11 v1.0.3/go.mod h1:XsNlhZGX73bx86s2hdc/FuaLm2CPZJemRLMA+WTFxgs= +github.com/mistifyio/go-zfs v2.1.2-0.20190413222219-f784269be439+incompatible/go.mod h1:8AuVvqP/mXw1px98n46wfvcGfQ4ci2FwoAjKYxuo3Z4= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/osext v0.0.0-20151018003038-5e2d6d41470f/go.mod h1:OkQIRizQZAeMln+1tSwduZz7+Af5oFlKirV/MSYes2A= +github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/sys/mount v0.2.0 h1:WhCW5B355jtxndN5ovugJlMFJawbUODuW8fSnEH6SSM= +github.com/moby/sys/mount v0.2.0/go.mod h1:aAivFE2LB3W4bACsUXChRHQ0qKWsetY4Y9V7sxOougM= +github.com/moby/sys/mountinfo v0.4.0/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.4.1/go.mod h1:rEr8tzG/lsIZHBtN/JjGG+LMYx9eXgW2JI+6q0qou+A= +github.com/moby/sys/mountinfo v0.5.0 h1:2Ks8/r6lopsxWi9m58nlwjaeSzUX9iiL1vj5qB/9ObI= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGqsZeMYowQ= +github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 h1:rzf0wL0CHVc8CEsgyygG0Mn9CNCCPZqOPaz8RiiHYQk= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c h1:nXxl5PrvVm2L/wCy8dQu6DMTwH4oIuGN8GJDAlqDdVE= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/open-policy-agent/opa v0.35.0 h1:wsXkq/3JJucRUN4h46pn9Zv6cC6fnHWrVxjgoykxM7o= +github.com/open-policy-agent/opa v0.35.0/go.mod h1:xEmekKlk6/c+so5HF9wtPnGPXDfBuBsrMGhSHOHEF+U= +github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/opencontainers/runc v1.0.0-rc93/go.mod h1:3NOsor4w32B2tC0Zbl8Knk4Wg84SM2ImC1fxBuqJ/H0= +github.com/opencontainers/runc v1.0.2 h1:opHZMaswlyxz1OuGpBE53Dwe4/xF7EZTY0A2L/FpCOg= +github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= +github.com/opencontainers/runtime-spec v0.1.2-0.20190507144316-5b71a03e2700/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2-0.20190207185410-29686dbc5559/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20200929063507-e6143ca7d51d/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-tools v0.0.0-20181011054405-1d69bd0f9c39/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs= +github.com/opencontainers/selinux v1.6.0/go.mod h1:VVGKuOLlE7v4PJyT6h7mNWvq1rzqiriPsEqVhc+svHE= +github.com/opencontainers/selinux v1.8.0/go.mod h1:RScLhm78qiWa2gbVCcGkC7tCGdgk3ogry1nUQF8Evvo= +github.com/opencontainers/selinux v1.8.2/go.mod h1:MUIHuUEvKB1wtJjQdOyYRgOnLD2xAPP8dBsCoU0KuF8= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20171117100541-99fa1f4be8e5/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20180110214958-89604d197083/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/common v0.29.0 h1:3jqPBvKT4OHAbje2Ql7KeaaSicDBCxMYwEJU1zRJceE= +github.com/prometheus/common v0.29.0/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= +github.com/prometheus/procfs v0.0.0-20180125133057-cb4147076ac7/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.5/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 h1:MkV+77GLUNo5oJ0jf870itWm3D0Sjh7+Za9gazKc5LQ= +github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/safchain/ethtool v0.0.0-20190326074333-42ed695e3de8/go.mod h1:Z0q5wiBQGYcxhMZ6gUqHn6pYNLypFAvaL3UvgZLR0U4= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/afero v1.6.0 h1:xoax2sJ2DT8S8xA2paPFjDCScCNeWsg75VG0DLRreiY= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= +github.com/stretchr/objx v0.0.0-20180129172003-8a3f7159479f/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20180303142811-b89eecf5ca5d/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= +github.com/testcontainers/testcontainers-go v0.12.0 h1:SK0NryGHIx7aifF6YqReORL18aGAA4bsDPtikDVCEyg= +github.com/testcontainers/testcontainers-go v0.12.0/go.mod h1:SIndOQXZng0IW8iWU1Js0ynrfZ8xcxrTtDfF6rD2pxs= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo= +github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/willf/bitset v1.1.11-0.20200630133818-d5bec3311243/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= +github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v0.0.0-20180618132009-1d523034197f/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b h1:vVRagRXf67ESqAb72hG2C/ZwI8NtJF2u2V76EsuOHGY= +github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mod h1:HptNXiXVDcJjXe9SqMd0v2FsL9f8dz4GnXgltU6q/co= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= +github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= +github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= +go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211108170745-6635138e15ea/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211111083644-e5c967477495 h1:cjxxlQm6d4kYbhpZ2ghvmI8xnq0AG+jXmzrhzfkyu5A= +golang.org/x/net v0.0.0-20211111083644-e5c967477495/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190514135907-3a4b5fb9f71f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190522044717-8097e1b27ff5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190812073006-9eafafc0a87e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200622214017-ed371f2e16b4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200922070232-aee5d888a860/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211109184856-51b60fd695b3/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211110154304-99a53858aa08/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d h1:FjkYO/PPp4Wi0EAUOVLxePm7qVW4r4ctbWpURyuOD0E= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/cloud v0.0.0-20151119220103-975617b05ea8/go.mod h1:0H1ncTHf11KCFhTc/+EFRbzSCOZx+VUbRMk55Yv5MYk= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190522204451-c2c4e71fbf69/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.38.0 h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20141024133853-64131543e789/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw= +inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8= +k8s.io/api v0.20.1/go.mod h1:KqwcCVogGxQY3nBlRpwt+wpAMF/KjaCc7RpywacvqUo= +k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU= +k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU= +k8s.io/client-go v0.20.1/go.mod h1:/zcHdt1TeWSd5HoUe6elJmHSQ6uLLgp4bIJHVEuy+/Y= +k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk= +k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= +k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/integration/testdata/fixtures/repo/gomod/submod/go.mod b/integration/testdata/fixtures/repo/gomod/submod/go.mod new file mode 100644 index 000000000000..f1f3268e1fbc --- /dev/null +++ b/integration/testdata/fixtures/repo/gomod/submod/go.mod @@ -0,0 +1,7 @@ +module github.com/testdata/testdata/submod + +go 1.15 + +require ( + github.com/docker/distribution v2.7.1+incompatible +) diff --git a/integration/testdata/fixtures/repo/gomod/submod/go.sum b/integration/testdata/fixtures/repo/gomod/submod/go.sum new file mode 100644 index 000000000000..38efa1d61ad4 --- /dev/null +++ b/integration/testdata/fixtures/repo/gomod/submod/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/integration/testdata/fixtures/repo/gomod/submod2/go.mod b/integration/testdata/fixtures/repo/gomod/submod2/go.mod new file mode 100644 index 000000000000..08f06b0e7227 --- /dev/null +++ b/integration/testdata/fixtures/repo/gomod/submod2/go.mod @@ -0,0 +1,7 @@ +module github.com/testdata/testdata/submod2 + +go 1.15 + +require ( + github.com/davecgh/go-spew v1.1.0 +) diff --git a/integration/testdata/fixtures/repo/gomod/submod2/go.sum b/integration/testdata/fixtures/repo/gomod/submod2/go.sum new file mode 100644 index 000000000000..7e62d8411389 --- /dev/null +++ b/integration/testdata/fixtures/repo/gomod/submod2/go.sum @@ -0,0 +1,14 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible h1:a5mlkVzth6W5A4fOsS3D2EO5BUmsJpcB+cRlLU7cSug= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= diff --git a/integration/testdata/fixtures/repo/gradle/gradle.lockfile b/integration/testdata/fixtures/repo/gradle/gradle.lockfile new file mode 100644 index 000000000000..b6ba3d4b8b2e --- /dev/null +++ b/integration/testdata/fixtures/repo/gradle/gradle.lockfile @@ -0,0 +1,5 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +com.fasterxml.jackson.core:jackson-databind:2.9.1=compileClasspath, runtimeClasspath +empty= \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/helm/testchart.tar.gz b/integration/testdata/fixtures/repo/helm/testchart.tar.gz new file mode 100644 index 000000000000..e36b2b474f3e Binary files /dev/null and b/integration/testdata/fixtures/repo/helm/testchart.tar.gz differ diff --git a/integration/testdata/fixtures/repo/helm_badname/Chart.yaml b/integration/testdata/fixtures/repo/helm_badname/Chart.yaml new file mode 100644 index 000000000000..e840fbabf456 --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_badname/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: 1001 +version: 1.0.0 \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/helm_testchart/.helmignore b/integration/testdata/fixtures/repo/helm_testchart/.helmignore new file mode 100644 index 000000000000..0e8a0eb36f4c --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/integration/testdata/fixtures/repo/helm_testchart/Chart.yaml b/integration/testdata/fixtures/repo/helm_testchart/Chart.yaml new file mode 100644 index 000000000000..0ffb7d074a72 --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: testchart +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/integration/testdata/fixtures/repo/helm_testchart/templates/NOTES.txt b/integration/testdata/fixtures/repo/helm_testchart/templates/NOTES.txt new file mode 100644 index 000000000000..45e51670a862 --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "testchart.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "testchart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "testchart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "testchart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/integration/testdata/fixtures/repo/helm_testchart/templates/_helpers.tpl b/integration/testdata/fixtures/repo/helm_testchart/templates/_helpers.tpl new file mode 100644 index 000000000000..4b0db05bf5f5 --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "testchart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "testchart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "testchart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "testchart.labels" -}} +helm.sh/chart: {{ include "testchart.chart" . }} +{{ include "testchart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "testchart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "testchart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "testchart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "testchart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/integration/testdata/fixtures/repo/helm_testchart/templates/deployment.yaml b/integration/testdata/fixtures/repo/helm_testchart/templates/deployment.yaml new file mode 100644 index 000000000000..cde22bc4fcc5 --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/templates/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "testchart.fullname" . }} + labels: + {{- include "testchart.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "testchart.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "testchart.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "testchart.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/integration/testdata/fixtures/repo/helm_testchart/templates/hpa.yaml b/integration/testdata/fixtures/repo/helm_testchart/templates/hpa.yaml new file mode 100644 index 000000000000..51734471d41d --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "testchart.fullname" . }} + labels: + {{- include "testchart.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "testchart.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/integration/testdata/fixtures/repo/helm_testchart/templates/ingress.yaml b/integration/testdata/fixtures/repo/helm_testchart/templates/ingress.yaml new file mode 100644 index 000000000000..9732d2a24a14 --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "testchart.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "testchart.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/integration/testdata/fixtures/repo/helm_testchart/templates/service.yaml b/integration/testdata/fixtures/repo/helm_testchart/templates/service.yaml new file mode 100644 index 000000000000..86baf148215d --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "testchart.fullname" . }} + labels: + {{- include "testchart.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "testchart.selectorLabels" . | nindent 4 }} diff --git a/integration/testdata/fixtures/repo/helm_testchart/templates/serviceaccount.yaml b/integration/testdata/fixtures/repo/helm_testchart/templates/serviceaccount.yaml new file mode 100644 index 000000000000..f728deb2a6bb --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "testchart.serviceAccountName" . }} + labels: + {{- include "testchart.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/integration/testdata/fixtures/repo/helm_testchart/templates/tests/test-connection.yaml b/integration/testdata/fixtures/repo/helm_testchart/templates/tests/test-connection.yaml new file mode 100644 index 000000000000..a391ef1c462f --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "testchart.fullname" . }}-test-connection" + labels: + {{- include "testchart.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "testchart.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/integration/testdata/fixtures/repo/helm_testchart/values.yaml b/integration/testdata/fixtures/repo/helm_testchart/values.yaml new file mode 100644 index 000000000000..54dd329d3c6f --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_testchart/values.yaml @@ -0,0 +1,83 @@ +# Default values for testchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: + {} + # fsGroup: 2000 + +securityContext: + { + "capabilities": { + "drop": ["ALL"] + }, + "runAsUser": 10001, + "runAsNonRoot": true, + "runAsGroup": 10001, + "readOnlyRootFilesystem": true, + } + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 100m + memory: 126Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/integration/testdata/fixtures/repo/helm_values/values.yaml b/integration/testdata/fixtures/repo/helm_values/values.yaml new file mode 100644 index 000000000000..041169482cbd --- /dev/null +++ b/integration/testdata/fixtures/repo/helm_values/values.yaml @@ -0,0 +1,2 @@ +securityContext: + runAsUser: 0 \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/julia/Manifest.toml b/integration/testdata/fixtures/repo/julia/Manifest.toml new file mode 100644 index 000000000000..dd4ea00b943d --- /dev/null +++ b/integration/testdata/fixtures/repo/julia/Manifest.toml @@ -0,0 +1,16 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.9.0" +manifest_format = "2.0" +project_hash = "f0a796fb78285c02ad123fec6e14c8bac09a2ccc" + +[[deps.A]] +uuid = "ead4f63c-334e-11e9-00e6-e7f0a5f21b60" + + [deps.A.deps] + B = "f41f7b98-334e-11e9-1257-49272045fb24" + +[[deps.B]] +uuid = "f41f7b98-334e-11e9-1257-49272045fb24" +[[deps.B]] +uuid = "edca9bc6-334e-11e9-3554-9595dbb4349c" diff --git a/integration/testdata/fixtures/repo/julia/Project.toml b/integration/testdata/fixtures/repo/julia/Project.toml new file mode 100644 index 000000000000..24fe6178c480 --- /dev/null +++ b/integration/testdata/fixtures/repo/julia/Project.toml @@ -0,0 +1,7 @@ +name = "packageName" +uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773" +version = "0.1.0" + +[deps] +A = "ead4f63c-334e-11e9-00e6-e7f0a5f21b60" +B = "edca9bc6-334e-11e9-3554-9595dbb4349c" diff --git a/integration/testdata/fixtures/repo/mixlock/mix.lock b/integration/testdata/fixtures/repo/mixlock/mix.lock new file mode 100644 index 000000000000..a13ebcf57c21 --- /dev/null +++ b/integration/testdata/fixtures/repo/mixlock/mix.lock @@ -0,0 +1,12 @@ +%{ + "castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "phoenix": {:hex, :phoenix, "1.6.13", "0a1d96bbc10747fd83525370d691953cdb6f3ccbac61aa01b4acb012474b047d", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0 or ~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d70ab9fbf6b394755ea88b644d34d79d8b146e490973151f248cacd122d20672"}, + "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, + "phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"}, + "phoenix_view": {:hex, :phoenix_view, "2.0.1", "a653e3d9d944aace0a064e4a13ad473ffa68f7bc4ca42dbf83cc1d464f1fb295", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "6c358e2cefc5f341c728914b867c556bbfd239fed9e881bac257d70cb2b8a6f6"}, + "plug": {:hex, :plug, "1.14.0", "ba4f558468f69cbd9f6b356d25443d0b796fbdc887e03fa89001384a9cac638f", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "bf020432c7d4feb7b3af16a0c2701455cbbbb95e5b6866132cb09eb0c29adc14"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, + "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/npm/node_modules/jquery/package.json b/integration/testdata/fixtures/repo/npm/node_modules/jquery/package.json new file mode 100644 index 000000000000..db81fb8f6928 --- /dev/null +++ b/integration/testdata/fixtures/repo/npm/node_modules/jquery/package.json @@ -0,0 +1,113 @@ +{ + "name": "jquery", + "title": "jQuery", + "description": "JavaScript library for DOM operations", + "version": "3.3.9", + "main": "dist/jquery.js", + "homepage": "https://jquery.com", + "author": { + "name": "JS Foundation and other contributors", + "url": "https://github.com/jquery/jquery/blob/3.4.0/AUTHORS.txt" + }, + "repository": { + "type": "git", + "url": "https://github.com/jquery/jquery.git" + }, + "keywords": [ + "jquery", + "javascript", + "browser", + "library" + ], + "bugs": { + "url": "https://github.com/jquery/jquery/issues" + }, + "license": "MIT", + "dependencies": {}, + "devDependencies": { + "@babel/core": "7.3.3", + "@babel/plugin-transform-for-of": "7.2.0", + "commitplease": "3.2.0", + "core-js": "2.6.5", + "eslint-config-jquery": "1.0.1", + "grunt": "1.0.3", + "grunt-babel": "8.0.0", + "grunt-cli": "1.3.2", + "grunt-compare-size": "0.4.2", + "grunt-contrib-uglify": "3.4.0", + "grunt-contrib-watch": "1.1.0", + "grunt-eslint": "21.0.0", + "grunt-git-authors": "3.2.0", + "grunt-jsonlint": "1.1.0", + "grunt-karma": "3.0.1", + "grunt-newer": "1.3.0", + "grunt-npmcopy": "0.1.0", + "gzip-js": "0.3.2", + "husky": "1.3.1", + "insight": "0.10.1", + "jsdom": "13.2.0", + "karma": "4.0.1", + "karma-browserstack-launcher": "1.4.0", + "karma-chrome-launcher": "2.2.0", + "karma-firefox-launcher": "1.1.0", + "karma-ie-launcher": "1.0.0", + "karma-jsdom-launcher": "7.1.0", + "karma-qunit": "3.0.0", + "load-grunt-tasks": "4.0.0", + "native-promise-only": "0.8.1", + "promises-aplus-tests": "2.1.2", + "q": "1.5.1", + "qunit": "2.9.2", + "raw-body": "2.3.3", + "requirejs": "2.3.6", + "sinon": "2.3.7", + "sizzle": "2.3.4", + "strip-json-comments": "2.0.1", + "testswarm": "1.1.0", + "uglify-js": "3.4.7" + }, + "scripts": { + "build": "npm install && grunt", + "start": "grunt watch", + "test:browserless": "grunt && grunt test:slow", + "test:browser": "grunt && grunt karma:main", + "test": "grunt && grunt test:slow && grunt karma:main", + "jenkins": "npm run test:browserless" + }, + "commitplease": { + "nohook": true, + "components": [ + "Docs", + "Tests", + "Build", + "Support", + "Release", + "Core", + "Ajax", + "Attributes", + "Callbacks", + "CSS", + "Data", + "Deferred", + "Deprecated", + "Dimensions", + "Effects", + "Event", + "Manipulation", + "Offset", + "Queue", + "Selector", + "Serialize", + "Traversing", + "Wrap" + ], + "markerPattern": "^((clos|fix|resolv)(e[sd]|ing))|^(refs?)", + "ticketPattern": "^((Closes|Fixes) ([a-zA-Z]{2,}-)[0-9]+)|^(Refs? [^#])" + }, + "husky": { + "hooks": { + "commit-msg": "node node_modules/commitplease", + "pre-commit": "grunt lint:newer qunit_fixture" + } + } +} diff --git a/integration/testdata/fixtures/repo/npm/node_modules/promise/package.json b/integration/testdata/fixtures/repo/npm/node_modules/promise/package.json new file mode 100644 index 000000000000..6b6df7e49f1d --- /dev/null +++ b/integration/testdata/fixtures/repo/npm/node_modules/promise/package.json @@ -0,0 +1,35 @@ +{ + "name": "promise", + "version": "8.0.3", + "description": "Bare bones Promises/A+ implementation", + "main": "index.js", + "scripts": { + "prepublish": "node build", + "pretest": "node build", + "pretest-resolve": "node build", + "pretest-extensions": "node build", + "pretest-memory-leak": "node build", + "test": "mocha --bail --timeout 200 --slow 99999 -R dot && npm run test-memory-leak", + "test-resolve": "mocha test/resolver-tests.js --timeout 200 --slow 999999", + "test-extensions": "mocha test/extensions-tests.js --timeout 200 --slow 999999", + "test-memory-leak": "node --expose-gc test/memory-leak.js", + "coverage": "istanbul cover node_modules/mocha/bin/_mocha -- --bail --timeout 200 --slow 99999 -R dot" + }, + "repository": { + "type": "git", + "url": "https://github.com/then/promise.git" + }, + "author": "ForbesLindesay", + "license": "MIT", + "devDependencies": { + "acorn": "^1.0.1", + "better-assert": "*", + "istanbul": "^0.3.13", + "mocha": "*", + "promises-aplus-tests": "*", + "rimraf": "^2.3.2" + }, + "dependencies": { + "asap": "~2.0.6" + } +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/npm/node_modules/react-is/package.json b/integration/testdata/fixtures/repo/npm/node_modules/react-is/package.json new file mode 100644 index 000000000000..6e502546f89e --- /dev/null +++ b/integration/testdata/fixtures/repo/npm/node_modules/react-is/package.json @@ -0,0 +1,27 @@ +{ + "name": "react-is", + "version": "16.8.6", + "description": "Brand checking of React Elements.", + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/facebook/react.git", + "directory": "packages/react-is" + }, + "keywords": [ + "react" + ], + "license": "MIT", + "bugs": { + "url": "https://github.com/facebook/react/issues" + }, + "homepage": "https://reactjs.org/", + "files": [ + "LICENSE", + "README.md", + "build-info.json", + "index.js", + "cjs/", + "umd/" + ] +} diff --git a/integration/testdata/fixtures/repo/npm/node_modules/react/package.json b/integration/testdata/fixtures/repo/npm/node_modules/react/package.json new file mode 100644 index 000000000000..fb0216747f2b --- /dev/null +++ b/integration/testdata/fixtures/repo/npm/node_modules/react/package.json @@ -0,0 +1,39 @@ +{ + "name": "react", + "description": "React is a JavaScript library for building user interfaces.", + "keywords": [ + "react" + ], + "version": "16.8.6", + "homepage": "https://reactjs.org/", + "bugs": "https://github.com/facebook/react/issues", + "license": "MIT", + "files": [ + "LICENSE", + "README.md", + "build-info.json", + "index.js", + "cjs/", + "umd/" + ], + "main": "index.js", + "repository": { + "type": "git", + "url": "https://github.com/facebook/react.git", + "directory": "packages/react" + }, + "engines": { + "node": ">=0.10.0" + }, + "dependencies": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + }, + "browserify": { + "transform": [ + "loose-envify" + ] + } +} diff --git a/integration/testdata/fixtures/repo/npm/node_modules/redux/package.json b/integration/testdata/fixtures/repo/npm/node_modules/redux/package.json new file mode 100644 index 000000000000..449e8fa08bcf --- /dev/null +++ b/integration/testdata/fixtures/repo/npm/node_modules/redux/package.json @@ -0,0 +1,105 @@ +{ + "name": "redux", + "version": "4.0.1", + "description": "Predictable state container for JavaScript apps", + "license": "MIT", + "homepage": "http://redux.js.org", + "repository": "github:reduxjs/redux", + "bugs": "https://github.com/reduxjs/redux/issues", + "keywords": [ + "redux", + "reducer", + "state", + "predictable", + "functional", + "immutable", + "hot", + "live", + "replay", + "flux", + "elm" + ], + "authors": [ + "Dan Abramov (https://github.com/gaearon)", + "Andrew Clark (https://github.com/acdlite)" + ], + "main": "lib/redux.js", + "unpkg": "dist/redux.js", + "module": "es/redux.js", + "typings": "./index.d.ts", + "files": [ + "dist", + "lib", + "es", + "src", + "index.d.ts" + ], + "scripts": { + "clean": "rimraf lib dist es coverage", + "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts", + "format:check": "prettier --list-different \"{src,test}/**/*.{js,ts}\" index.d.ts", + "lint": "eslint src test", + "pretest": "npm run build", + "test": "jest", + "test:watch": "npm test -- --watch", + "test:cov": "npm test -- --coverage", + "build": "rollup -c", + "prepare": "npm run clean && npm run format:check && npm run lint && npm test", + "examples:lint": "eslint examples", + "examples:test": "cross-env CI=true babel-node examples/testAll.js" + }, + "dependencies": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + }, + "devDependencies": { + "@babel/cli": "^7.0.0", + "@babel/core": "^7.0.0", + "@babel/node": "^7.0.0", + "@babel/plugin-external-helpers": "^7.0.0", + "@babel/plugin-proposal-object-rest-spread": "^7.0.0", + "@babel/preset-env": "^7.0.0", + "@babel/preset-flow": "^7.0.0", + "@babel/register": "^7.0.0", + "babel-core": "^7.0.0-bridge.0", + "babel-eslint": "^9.0.0", + "babel-jest": "^23.6.0", + "cross-env": "^5.2.0", + "eslint": "^5.6.0", + "eslint-config-react-app": "^2.1.0", + "eslint-plugin-flowtype": "^2.50.1", + "eslint-plugin-import": "^2.14.0", + "eslint-plugin-jsx-a11y": "^6.1.1", + "eslint-plugin-react": "^7.11.1", + "glob": "^7.1.3", + "jest": "^23.6.0", + "prettier": "^1.14.3", + "rimraf": "^2.6.2", + "rollup": "^0.66.2", + "rollup-plugin-babel": "^4.0.1", + "rollup-plugin-node-resolve": "^3.4.0", + "rollup-plugin-replace": "^2.0.0", + "rollup-plugin-terser": "^3.0.0", + "rxjs": "^6.3.2", + "typescript": "^3.0.3", + "typings-tester": "^0.3.2" + }, + "npmName": "redux", + "npmFileMap": [ + { + "basePath": "/dist/", + "files": [ + "*.js" + ] + } + ], + "browserify": { + "transform": [ + "loose-envify" + ] + }, + "jest": { + "testRegex": "(/test/.*\\.spec.js)$" + }, + "sideEffects": false +} diff --git a/integration/testdata/fixtures/repo/npm/node_modules/z-lock/package.json b/integration/testdata/fixtures/repo/npm/node_modules/z-lock/package.json new file mode 100644 index 000000000000..fd196fe2db6b --- /dev/null +++ b/integration/testdata/fixtures/repo/npm/node_modules/z-lock/package.json @@ -0,0 +1,26 @@ +{ + "name": "z-lock", + "version": "1.0.0", + "description": "This CommonJS module can create a simple \"lock\" that can be checked, locked/unlocked.", + "main": "lock.js", + "scripts": { + "test": "mocha test/*.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/ZeeCoder/z-lock.git" + }, + "keywords": [ + "lock" + ], + "author": "Hubert Viktor", + "license": "MIT", + "bugs": { + "url": "https://github.com/ZeeCoder/z-lock/issues" + }, + "homepage": "https://github.com/ZeeCoder/z-lock", + "devDependencies": { + "clone": "^1.0.2", + "mocha": "^2.2.5" + } +} diff --git a/integration/testdata/fixtures/repo/npm/package-lock.json b/integration/testdata/fixtures/repo/npm/package-lock.json new file mode 100644 index 000000000000..5658cc502e0a --- /dev/null +++ b/integration/testdata/fixtures/repo/npm/package-lock.json @@ -0,0 +1,98 @@ +{ + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "jquery": { + "version": "3.3.9", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.0.tgz", + "integrity": "sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ==" + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "promise": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.3.tgz", + "integrity": "sha512-HeRDUL1RJiLhyA0/grn+PTShlBAcLuh/1BJGtrvjwbvRDCTLLMEz9rOGCV+R3vHY4MixIuoMEd9Yq/XvsTPcjw==", + "requires": { + "asap": "~2.0.6" + } + }, + "prop-types": { + "version": "15.7.2", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", + "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.8.1" + } + }, + "react": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz", + "integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1", + "prop-types": "^15.6.2", + "scheduler": "^0.13.6" + } + }, + "react-is": { + "version": "16.8.6", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", + "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" + }, + "redux": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.0.1.tgz", + "integrity": "sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==", + "requires": { + "loose-envify": "^1.4.0", + "symbol-observable": "^1.2.0" + } + }, + "scheduler": { + "version": "0.13.6", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz", + "integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==", + "requires": { + "loose-envify": "^1.1.0", + "object-assign": "^4.1.1" + } + }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, + "z-lock": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/z-lock/-/z-lock-1.0.0.tgz", + "integrity": "sha512-TWoID7h5wphb4YHcY/tu9u7nZb6wtmBjqpRUYbQCemrhmJXL+7/Vblb6rs7ANnBInIt9Qccb7bXUCNGZpxekeA==", + "dev": true + } + } +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/nuget/packages.lock.json b/integration/testdata/fixtures/repo/nuget/packages.lock.json new file mode 100644 index 000000000000..7c06bd9d435f --- /dev/null +++ b/integration/testdata/fixtures/repo/nuget/packages.lock.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v5.0": { + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[12.0.3, )", + "resolved": "12.0.3", + "contentHash": "6mgjfnRB4jKMlzHSl+VD+oUc1IebOZabkbyWj2RiTgWwYPPuaK1H97G1sHqGwPlS5npiF5Q0OrxN1wni2n5QWg==" + }, + "NuGet.Frameworks": { + "type": "Direct", + "requested": "[5.7.0, )", + "resolved": "5.7.0", + "contentHash": "7Q/wUoB3jCBcq9zoBOBGHFhe78C13jViPmvjvzTwthVV8DAjMfpXnqAYtgwdaRLJMkTXrtdLxfPBIFFhmlsnIQ==", + "dependencies": { + "Newtonsoft.Json": "12.0.3" + } + } + } + } +} diff --git a/integration/testdata/fixtures/repo/packagesprops/Directory.Packages.props b/integration/testdata/fixtures/repo/packagesprops/Directory.Packages.props new file mode 100644 index 000000000000..daa6f7ccdc4f --- /dev/null +++ b/integration/testdata/fixtures/repo/packagesprops/Directory.Packages.props @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/pip/requirements.txt b/integration/testdata/fixtures/repo/pip/requirements.txt new file mode 100644 index 000000000000..83c457b8014b --- /dev/null +++ b/integration/testdata/fixtures/repo/pip/requirements.txt @@ -0,0 +1,8 @@ +click==8.0.0 +Flask==2.0.0 +itsdangerous==2.0.0 +Jinja2==3.0.0 +MarkupSafe>2.0.0 +Werkzeug==0.11 +oauth2-client==4.0.0 +python-gitlab==2.0.0 diff --git a/integration/testdata/fixtures/repo/pipenv/Pipfile.lock b/integration/testdata/fixtures/repo/pipenv/Pipfile.lock new file mode 100644 index 000000000000..c7e495b1e5f2 --- /dev/null +++ b/integration/testdata/fixtures/repo/pipenv/Pipfile.lock @@ -0,0 +1,29 @@ +{ + "_meta": { + "hash": { + "sha256": "06bf5e1462f5cf5abd8c226d9db597827c8fde5c6bbb0e9c87c2977720130c56" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.10" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "werkzeug": { + "hashes": [ + "sha256:1e0dedc2acb1f46827daa2e399c1485c8fa17c0d8e70b6b875b4e7f54bf408d2", + "sha256:b353856d37dec59d6511359f97f6a4b2468442e454bd1c98298ddce53cac1f04" + ], + "index": "pypi", + "version": "==0.11.1" + } + }, + "develop": {} +} diff --git a/integration/testdata/fixtures/repo/pnpm/node_modules/.pnpm/jquery@3.3.9/node_modules/jquery/package.json b/integration/testdata/fixtures/repo/pnpm/node_modules/.pnpm/jquery@3.3.9/node_modules/jquery/package.json new file mode 100644 index 000000000000..54dd7c03a0bd --- /dev/null +++ b/integration/testdata/fixtures/repo/pnpm/node_modules/.pnpm/jquery@3.3.9/node_modules/jquery/package.json @@ -0,0 +1,108 @@ +{ + "name": "jquery", + "title": "jQuery", + "description": "JavaScript library for DOM operations", + "version": "3.3.9", + "main": "dist/jquery.js", + "homepage": "https://jquery.com", + "author": { + "name": "JS Foundation and other contributors", + "url": "https://github.com/jquery/jquery/blob/3.3.1/AUTHORS.txt" + }, + "repository": { + "type": "git", + "url": "https://github.com/jquery/jquery.git" + }, + "keywords": [ + "jquery", + "javascript", + "browser", + "library" + ], + "bugs": { + "url": "https://github.com/jquery/jquery/issues" + }, + "license": "MIT", + "dependencies": {}, + "devDependencies": { + "babel-core": "7.0.0-beta.0", + "babel-plugin-transform-es2015-for-of": "7.0.0-beta.0", + "commitplease": "2.7.10", + "core-js": "2.4.1", + "eslint-config-jquery": "1.0.1", + "grunt": "1.0.1", + "grunt-babel": "7.0.0", + "grunt-cli": "1.2.0", + "grunt-compare-size": "0.4.2", + "grunt-contrib-uglify": "3.0.1", + "grunt-contrib-watch": "1.0.0", + "grunt-eslint": "20.0.0", + "grunt-git-authors": "3.2.0", + "grunt-jsonlint": "1.1.0", + "grunt-karma": "2.0.0", + "grunt-newer": "1.3.0", + "grunt-npmcopy": "0.1.0", + "gzip-js": "0.3.2", + "husky": "0.14.3", + "insight": "0.8.4", + "jsdom": "5.6.1", + "karma": "1.7.0", + "karma-browserstack-launcher": "1.3.0", + "karma-chrome-launcher": "2.2.0", + "karma-firefox-launcher": "1.0.1", + "karma-qunit": "1.2.1", + "load-grunt-tasks": "3.5.2", + "native-promise-only": "0.8.1", + "promises-aplus-tests": "2.1.2", + "q": "1.5.0", + "qunit-assert-step": "1.0.3", + "qunitjs": "1.23.1", + "raw-body": "2.2.0", + "requirejs": "2.3.3", + "sinon": "2.3.7", + "sizzle": "2.3.3", + "strip-json-comments": "2.0.1", + "testswarm": "1.1.0", + "uglify-js": "3.3.4" + }, + "scripts": { + "build": "npm install && grunt", + "start": "grunt watch", + "test:browserless": "grunt && grunt test:slow", + "test:browser": "grunt && grunt karma:main", + "test": "grunt && grunt test:slow && grunt karma:main", + "jenkins": "npm run test:browserless", + "precommit": "grunt lint:newer qunit_fixture", + "commitmsg": "node node_modules/commitplease" + }, + "commitplease": { + "nohook": true, + "components": [ + "Docs", + "Tests", + "Build", + "Support", + "Release", + "Core", + "Ajax", + "Attributes", + "Callbacks", + "CSS", + "Data", + "Deferred", + "Deprecated", + "Dimensions", + "Effects", + "Event", + "Manipulation", + "Offset", + "Queue", + "Selector", + "Serialize", + "Traversing", + "Wrap" + ], + "markerPattern": "^((clos|fix|resolv)(e[sd]|ing))|^(refs?)", + "ticketPattern": "^((Closes|Fixes) ([a-zA-Z]{2,}-)[0-9]+)|^(Refs? [^#])" + } +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/pnpm/node_modules/.pnpm/lodash@4.17.4/node_modules/lodash/package.json b/integration/testdata/fixtures/repo/pnpm/node_modules/.pnpm/lodash@4.17.4/node_modules/lodash/package.json new file mode 100644 index 000000000000..3fa8669cb337 --- /dev/null +++ b/integration/testdata/fixtures/repo/pnpm/node_modules/.pnpm/lodash@4.17.4/node_modules/lodash/package.json @@ -0,0 +1,17 @@ +{ + "name": "lodash", + "version": "4.17.4", + "description": "Lodash modular utilities.", + "keywords": "modules, stdlib, util", + "homepage": "https://lodash.com/", + "repository": "lodash/lodash", + "icon": "https://lodash.com/icon.svg", + "license": "MIT", + "main": "lodash.js", + "author": "John-David Dalton (http://allyoucanleet.com/)", + "contributors": [ + "John-David Dalton (http://allyoucanleet.com/)", + "Mathias Bynens (https://mathiasbynens.be/)" + ], + "scripts": { "test": "echo \"See https://travis-ci.org/lodash/lodash-cli for testing details.\"" } +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/pnpm/pnpm-lock.yaml b/integration/testdata/fixtures/repo/pnpm/pnpm-lock.yaml new file mode 100644 index 000000000000..f2b129bc448d --- /dev/null +++ b/integration/testdata/fixtures/repo/pnpm/pnpm-lock.yaml @@ -0,0 +1,23 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + jquery: + specifier: ^3.3.9 + version: 3.3.9 + lodash: + specifier: ^4.17.4 + version: 4.17.4 + +packages: + + /jquery@3.3.9: + resolution: {integrity: sha512-ggRCXln9zEqv6OqAGXFEcshF5dSBvCkzj6Gm2gzuR5fWawaX8t7cxKVkkygKODrDAzKdoYw3l/e3pm3vlT4IbQ==} + dev: false + + /lodash@4.17.4: + resolution: {integrity: sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=} + dev: false diff --git a/integration/testdata/fixtures/repo/poetry/poetry.lock b/integration/testdata/fixtures/repo/poetry/poetry.lock new file mode 100644 index 000000000000..1e2f8f71e94c --- /dev/null +++ b/integration/testdata/fixtures/repo/poetry/poetry.lock @@ -0,0 +1,50 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "werkzeug" +version = "0.14" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "Werkzeug-0.14-py2.py3-none-any.whl", hash = "sha256:322b15deb0e503c3e96c267b676d47ca069edccbf6338549bea7916583822a55"}, + {file = "Werkzeug-0.14.tar.gz", hash = "sha256:4aea27a9513b056346e9c8b49107f4ee7927f7bcf0be63024ecee39d5b87e9ef"}, +] + +[package.extras] +dev = ["coverage", "pytest", "sphinx", "tox"] +termcolor = ["termcolor"] +watchdog = ["watchdog"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "7bf54e5dc4ab511438271b965af1def5798ef80c82c39a3cdfe9308fd7881ff1" \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/poetry/pyproject.toml b/integration/testdata/fixtures/repo/poetry/pyproject.toml new file mode 100644 index 000000000000..332d56ab7a96 --- /dev/null +++ b/integration/testdata/fixtures/repo/poetry/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "test" +version = "0.1.0" +description = "" +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.9" +werkzeug = "0.14" +click = "8.1.3" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/integration/testdata/fixtures/repo/pom/pom.xml b/integration/testdata/fixtures/repo/pom/pom.xml new file mode 100644 index 000000000000..0f4214b5d254 --- /dev/null +++ b/integration/testdata/fixtures/repo/pom/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + com.example + log4shell + 1.0-SNAPSHOT + log4shell + war + + + UTF-8 + 1.8 + 1.8 + 5.7.1 + + + + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + + com.fasterxml.jackson.core + jackson-databind + 2.9.1 + + + + + + + org.apache.maven.plugins + maven-war-plugin + 3.3.1 + + + + + + \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/pubspec/pubspec.lock b/integration/testdata/fixtures/repo/pubspec/pubspec.lock new file mode 100644 index 000000000000..043c82efcea2 --- /dev/null +++ b/integration/testdata/fixtures/repo/pubspec/pubspec.lock @@ -0,0 +1,20 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + http: + dependency: "direct main" + description: + name: http + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.13.2" + shelf: + dependency: transitive + description: + name: shelf + url: "https://pub.flutter-io.cn" + source: hosted + version: "1.3.1" +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.3.0" \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/sbt/build.sbt.lock b/integration/testdata/fixtures/repo/sbt/build.sbt.lock new file mode 100644 index 000000000000..33bcdbee245e --- /dev/null +++ b/integration/testdata/fixtures/repo/sbt/build.sbt.lock @@ -0,0 +1,29 @@ +{ + "lockVersion" : 1, + "timestamp" : "2024-06-06T11:03:09.964557Z", + "configurations" : [ + "compile", + "optional", + "provided", + "runtime", + "test" + ], + "dependencies" : [ + { + "org" : "com.fasterxml.jackson.core", + "name" : "jackson-databind", + "version" : "2.9.1", + "artifacts" : [ + { + "name" : "jackson-databind.jar", + "hash" : "sha1:716da1830a2043f18882fc036ec26eb32cbe5aff" + } + ], + "configurations" : [ + "compile", + "runtime", + "test" + ] + } + ] +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/secrets/deploy.sh b/integration/testdata/fixtures/repo/secrets/deploy.sh new file mode 100644 index 000000000000..f696ab73ac43 --- /dev/null +++ b/integration/testdata/fixtures/repo/secrets/deploy.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +export AWS_ACCESS_KEY_ID=AKIAABCDEFGHI1234567 + +export GITHUB_PAT=ghp_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ + +echo mysecret \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/secrets/trivy-secret.yaml b/integration/testdata/fixtures/repo/secrets/trivy-secret.yaml new file mode 100644 index 000000000000..8eb7d957e0ee --- /dev/null +++ b/integration/testdata/fixtures/repo/secrets/trivy-secret.yaml @@ -0,0 +1,8 @@ +rules: + - id: mysecret + category: Custom + title: My Secret + severity: HIGH + regex: mysecret +disable-rules: + - github-pat diff --git a/integration/testdata/fixtures/repo/swift/Package.resolved b/integration/testdata/fixtures/repo/swift/Package.resolved new file mode 100644 index 000000000000..f9ca89fb741c --- /dev/null +++ b/integration/testdata/fixtures/repo/swift/Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "swift-atomics", + "repositoryURL": "https://github.com/apple/swift-atomics.git", + "state": { + "branch": null, + "revision": "6c89474e62719ddcc1e9614989fff2f68208fe10", + "version": "1.1.0" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio", + "state": { + "branch": null, + "revision": "ece5057615d1bee848341eceafdf04ca54d60177", + "version": "2.41.0" + } + } + ] + }, + "version": 1 +} diff --git a/integration/testdata/fixtures/repo/terraform/opentofu-registry/main.tf b/integration/testdata/fixtures/repo/terraform/opentofu-registry/main.tf new file mode 100644 index 000000000000..b4585e26b457 --- /dev/null +++ b/integration/testdata/fixtures/repo/terraform/opentofu-registry/main.tf @@ -0,0 +1,5 @@ +module "bucket" { + source = "registry.opentofu.org/terraform-aws-modules/s3-bucket/aws" + version = "5.2.0" + bucket = "test" +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/terraform/remote-module-in-child/main.tf b/integration/testdata/fixtures/repo/terraform/remote-module-in-child/main.tf new file mode 100644 index 000000000000..185625869de0 --- /dev/null +++ b/integration/testdata/fixtures/repo/terraform/remote-module-in-child/main.tf @@ -0,0 +1,4 @@ +module "this" { + source = "./modules/s3" + bucket = "test" +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/terraform/remote-module-in-child/modules/s3/main.tf b/integration/testdata/fixtures/repo/terraform/remote-module-in-child/modules/s3/main.tf new file mode 100644 index 000000000000..6b9ed58d3a80 --- /dev/null +++ b/integration/testdata/fixtures/repo/terraform/remote-module-in-child/modules/s3/main.tf @@ -0,0 +1,8 @@ +variable "bucket" { + type = string +} + +module "bucket" { + source = "github.com/terraform-aws-modules/terraform-aws-s3-bucket?ref=v5.2.0" + bucket = var.bucket +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/terraform/remote-module/main.tf b/integration/testdata/fixtures/repo/terraform/remote-module/main.tf new file mode 100644 index 000000000000..f2e3927aee9e --- /dev/null +++ b/integration/testdata/fixtures/repo/terraform/remote-module/main.tf @@ -0,0 +1,5 @@ +module "bucket" { + source = "terraform-aws-modules/s3-bucket/aws" + version = "5.2.0" + bucket = "test" +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/terraform/remote-submodule/main.tf b/integration/testdata/fixtures/repo/terraform/remote-submodule/main.tf new file mode 100644 index 000000000000..9fe2621143e4 --- /dev/null +++ b/integration/testdata/fixtures/repo/terraform/remote-submodule/main.tf @@ -0,0 +1,20 @@ +module "endpoints" { + source = "terraform-aws-modules/vpc/aws//modules/vpc-endpoints" + version = "6.0.1" + + create = true + create_security_group = true + vpc_id = "vpc-12345678" + + endpoints = { + s3 = { + service = "s3" + } + } + + security_group_rules = [{ + description = "" + type = "egress" + cidr_blocks = ["0.0.0.0/0"] + }] +} diff --git a/integration/testdata/fixtures/repo/terraform/terraform-registry/main.tf b/integration/testdata/fixtures/repo/terraform/terraform-registry/main.tf new file mode 100644 index 000000000000..124e32546b51 --- /dev/null +++ b/integration/testdata/fixtures/repo/terraform/terraform-registry/main.tf @@ -0,0 +1,5 @@ +module "bucket" { + source = "registry.terraform.io/terraform-aws-modules/s3-bucket/aws" + version = "5.2.0" + bucket = "test" +} \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/trivy-ci-test/Cargo.lock b/integration/testdata/fixtures/repo/trivy-ci-test/Cargo.lock new file mode 100644 index 000000000000..ca70dc137dd5 --- /dev/null +++ b/integration/testdata/fixtures/repo/trivy-ci-test/Cargo.lock @@ -0,0 +1,666 @@ +# It is not intended for manual editing. +[[package]] +name = "ammonia" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "html5ever 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "tendril 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", + "url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "autocfg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "bitflags" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cfg-if" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "cloudabi" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "futf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "new_debug_unreachable 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "gdi32-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "html5ever" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "markup5ever 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "itoa" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "lazy_static" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libc" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "libressl-pnacl-sys" +version = "2.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pnacl-build-helper 1.4.11 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "log" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "maplit" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "markup5ever" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_codegen 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", + "string_cache 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "string_cache_codegen 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tendril 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "new_debug_unreachable" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "normal" +version = "0.1.0" +dependencies = [ + "ammonia 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "openssl-sys 0.7.17 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "openssl-sys" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)", + "user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "phf" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_codegen" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_generator" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "phf_shared" +version = "0.7.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "pkg-config" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "pnacl-build-helper" +version = "1.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", + "walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "proc-macro2" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "quote" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_chacha" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_core" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "rand_hc" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_isaac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_jitter" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_os" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_pcg" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rand_xorshift" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "remove_dir_all" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "ryu" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "same-file" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "serde_derive" +version = "1.0.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "serde_json" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", + "ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "siphasher" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "smallvec" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "string_cache" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "new_debug_unreachable 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)", + "string_cache_codegen 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "string_cache_codegen" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "string_cache_shared" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "syn" +version = "0.15.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tempdir" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", + "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tendril" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "futf 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", + "mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-normalization" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unicode-xid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "user32-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "utf-8" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "walkdir" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum ammonia 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c799ecf1ad77acb48b643e2f45b12d60ee41576287fc575031aa020de88b8f45" +"checksum autocfg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a6d640bee2da49f60a4068a7fae53acde8982514ab7bae8b8cea9e88cbcfd799" +"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" +"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12" +"checksum cfg-if 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11d43355396e872eefb45ce6342e4374ed7bc2b3a502d1b28e36d6e23c05d1f4" +"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f" +"checksum fuchsia-cprng 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +"checksum futf 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b" +"checksum gdi32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0912515a8ff24ba900422ecda800b52f4016a56251922d397c576bf92c690518" +"checksum html5ever 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5ce65ac8028cf5a287a7dbf6c4e0a6cf2dcf022ed5b167a81bae66ebf599a8b7" +"checksum idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "38f09e0f0b1fb55fdee1f17470ad800da77af5186a1a76c026b679358b7e844e" +"checksum itoa 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501266b7edd0174f8530248f87f99c88fbe60ca4ef3dd486835b8d8d53136f7f" +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" +"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" +"checksum libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)" = "c6785aa7dd976f5fbf3b71cfd9cd49d7f783c1ff565a858d71031c6c313aa5c6" +"checksum libressl-pnacl-sys 2.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "cbc058951ab6a3ef35ca16462d7642c4867e6403520811f28537a4e2f2db3e71" +"checksum log 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c84ec4b527950aa83a329754b01dbe3f58361d1c5efacd1f6d68c494d08a17c6" +"checksum mac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +"checksum maplit 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "08cbb6b4fef96b6d77bfc40ec491b1690c779e77b05cd9f07f787ed376fd4c43" +"checksum markup5ever 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f1af46a727284117e09780d05038b1ce6fc9c76cc6df183c3dae5a8955a25e21" +"checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +"checksum new_debug_unreachable 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f40f005c60db6e03bae699e414c58bf9aa7ea02a2d0b9bfbcf19286cc4c82b30" +"checksum openssl 0.8.3 (registry+https://github.com/rust-lang/crates.io-index)" = "b11754cb6c81bb9e62faaf0eb6d94dde2aab0928c04db5078b74242880f35eb1" +"checksum openssl-sys 0.7.17 (registry+https://github.com/rust-lang/crates.io-index)" = "89c47ee94c352eea9ddaf8e364be7f978a3bb6d66d73176572484238dd5a5c3f" +"checksum percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "31010dd2e1ac33d5b46a5b413495239882813e0369f8ed8a5e266f173602f831" +"checksum phf 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b3da44b85f8e8dfaec21adae67f95d93244b2ecf6ad2a692320598dcc8e6dd18" +"checksum phf_codegen 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "b03e85129e324ad4166b06b2c7491ae27fe3ec353af72e72cd1654c7225d517e" +"checksum phf_generator 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "09364cc93c159b8b06b1f4dd8a4398984503483891b0c26b867cf431fb132662" +"checksum phf_shared 0.7.24 (registry+https://github.com/rust-lang/crates.io-index)" = "234f71a15de2288bcb7e3b6515828d22af7ec8598ee6d24c3b526fa0a80b67a0" +"checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c" +"checksum pnacl-build-helper 1.4.11 (registry+https://github.com/rust-lang/crates.io-index)" = "dfbe13ee77c06fb633d71c72438bd983286bb3521863a753ade8e951c7efb090" +"checksum precomputed-hash 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" +"checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" +"checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db" +"checksum rand 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +"checksum rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "6d71dacdc3c88c1fde3885a3be3fbab9f35724e6ce99467f7d9c5026132184ca" +"checksum rand_chacha 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "556d3a1ca6600bfcbab7c7c91ccb085ac7fbbcd70e008a98742e7847f4f7bcef" +"checksum rand_core 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +"checksum rand_core 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d0e7a549d590831370895ab7ba4ea0c1b6b011d106b5ff2da6eee112615e6dc0" +"checksum rand_hc 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7b40677c7be09ae76218dc623efbf7b18e34bced3f38883af07bb75630a21bc4" +"checksum rand_isaac 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "ded997c9d5f13925be2a6fd7e66bf1872597f759fd9dd93513dd7e92e5a5ee08" +"checksum rand_jitter 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "1166d5c91dc97b88d1decc3285bb0a99ed84b05cfd0bc2341bdf2d43fc41e39b" +"checksum rand_os 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7b75f676a1e053fc562eafbb47838d67c84801e38fc1ba459e8f180deabd5071" +"checksum rand_pcg 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "abf9b09b01790cfe0364f52bf32995ea3c39f4d2dd011eac241d2914146d0b44" +"checksum rand_xorshift 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cbf7e9e623549b0e21f6e97cf8ecf247c1a8fd2e8a992ae265314300b2455d5c" +"checksum rdrand 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" +"checksum ryu 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "b96a9549dc8d48f2c283938303c4b5a77aa29bfbc5b54b084fb1630408899a8f" +"checksum same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d931a44fdaa43b8637009e7632a02adc4f2b2e0733c08caa4cf00e8da4a117a7" +"checksum serde 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)" = "a72e9b96fa45ce22a4bc23da3858dfccfd60acd28a25bcd328a98fdd6bea43fd" +"checksum serde_derive 1.0.91 (registry+https://github.com/rust-lang/crates.io-index)" = "101b495b109a3e3ca8c4cbe44cf62391527cdfb6ba15821c5ce80bcd5ea23f9f" +"checksum serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "5a23aa71d4a4d43fdbfaac00eff68ba8a06a51759a89ac3304323e800c4dd40d" +"checksum siphasher 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac" +"checksum smallvec 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "c4488ae950c49d403731982257768f48fada354a5203fe81f9bb6f43ca9002be" +"checksum string_cache 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "25d70109977172b127fe834e5449e5ab1740b9ba49fa18a2020f509174f25423" +"checksum string_cache_codegen 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "1eea1eee654ef80933142157fdad9dd8bc43cf7c74e999e369263496f04ff4da" +"checksum string_cache_shared 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b1884d1bc09741d466d9b14e6d37ac89d6909cbcac41dd9ae982d4d063bbedfc" +"checksum syn 0.15.34 (registry+https://github.com/rust-lang/crates.io-index)" = "a1393e4a97a19c01e900df2aec855a29f71cf02c402e2f443b8d2747c25c5dbe" +"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" +"checksum tendril 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "707feda9f2582d5d680d733e38755547a3e8fb471e7ba11452ecfd9ce93a5d3b" +"checksum unicode-bidi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "49f2bd0c6468a8230e1db229cff8029217cf623c767ea5d60bfbd42729ea54d5" +"checksum unicode-normalization 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "141339a08b982d942be2ca06ff8b076563cbe223d1befd5450716790d44e2426" +"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" +"checksum url 1.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dd4e7c0d531266369519a4aa4f399d748bd37043b00bde1e4ff1f60a120b355a" +"checksum user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ef4711d107b21b410a3a974b1204d9accc8b10dad75d8324b5d755de1617d47" +"checksum utf-8 0.7.5 (registry+https://github.com/rust-lang/crates.io-index)" = "05e42f7c18b8f902290b009cde6d651262f956c98bc51bca4cd1d511c9cd85c7" +"checksum walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bb08f9e670fab86099470b97cd2b252d6527f0b3cc1401acdb595ffc9dd288ff" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "f10e386af2b13e47c89e7236a7a14a086791a2b88ebad6df9bf42040195cf770" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" +"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/integration/testdata/fixtures/repo/trivy-ci-test/Pipfile.lock b/integration/testdata/fixtures/repo/trivy-ci-test/Pipfile.lock new file mode 100644 index 000000000000..ce297a6a0515 --- /dev/null +++ b/integration/testdata/fixtures/repo/trivy-ci-test/Pipfile.lock @@ -0,0 +1,872 @@ +{ + "_meta": { + "hash": { + "sha256": "ad1805ab0e16cf08032c3fe45eeaa29b79e9c196650411977af14e31b12ff0cd" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "amqp": { + "hashes": [ + "sha256:043beb485774ca69718a35602089e524f87168268f0d1ae115f28b88d27f92d7", + "sha256:35a3b5006ca00b21aaeec8ceea07130f07b902dd61bfe42815039835f962f5f1" + ], + "version": "==2.4.2" + }, + "autopep8": { + "hashes": [ + "sha256:33d2b5325b7e1afb4240814fe982eea3a92ebea712869bfd08b3c0393404248c" + ], + "version": "==1.4.3" + }, + "babel": { + "hashes": [ + "sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", + "sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23" + ], + "version": "==2.6.0" + }, + "billiard": { + "hashes": [ + "sha256:756bf323f250db8bf88462cd042c992ba60d8f5e07fc5636c24ba7d6f4261d84" + ], + "version": "==3.6.0.0" + }, + "boto3": { + "hashes": [ + "sha256:bb69628f933a8dba22817c85289b3421b23ac643ff3202b13dd2e933c2717109", + "sha256:c75c45bae9dbdb2ff3fc3482d421a3901e552574a882dba1cffa064715acfbe7" + ], + "index": "pypi", + "version": "==1.9.130" + }, + "botocore": { + "hashes": [ + "sha256:128130b12f8ba4bf07a673b119135264060eb98f6a4a7419cbd1f2c6dc926827", + "sha256:59376112fdee707927b644dd44a1771861f8fe354a48d596131ced83d7a3c05b" + ], + "version": "==1.12.130" + }, + "celery": { + "extras": [ + "redis" + ], + "hashes": [ + "sha256:4c4532aa683f170f40bd76f928b70bc06ff171a959e06e71bf35f2f9d6031ef9", + "sha256:528e56767ae7e43a16cfef24ee1062491f5754368d38fcfffa861cdb9ef219be" + ], + "index": "pypi", + "version": "==4.3.0" + }, + "certifi": { + "hashes": [ + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" + ], + "version": "==2019.3.9" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "decorator": { + "hashes": [ + "sha256:86156361c50488b84a3f148056ea716ca587df2f0de1d34750d35c21312725de", + "sha256:f069f3a01830ca754ba5258fde2278454a0b5b79e0d7f5c13b3b97e57d4acff6" + ], + "version": "==4.4.0" + }, + "django": { + "hashes": [ + "sha256:665457d4146bbd34ae9d2970fa3b37082d7b225b0671bfd24c337458f229db78", + "sha256:bde46d4dbc410678e89bc95ea5d312dd6eb4c37d0fa0e19c9415cad94addf22f" + ], + "index": "pypi", + "version": "==2.0.9" + }, + "django-celery-beat": { + "hashes": [ + "sha256:3c2c22647455be5503aca7450db64ea53acacee2d0aef3d7ac49aa3ef3845724", + "sha256:bfc22dad2884524697e1fcdfa63c0555a65151a97902c3045cd2cf7bf63970e4" + ], + "index": "pypi", + "version": "==1.4.0" + }, + "django-cors-headers": { + "hashes": [ + "sha256:1ccedec2973087be9d73f96d58c4f6660c823efc0385581e13efb77f060d0e02", + "sha256:fb44f6b9f10de847919305c3f0d38fcfbadfe0dd5cf1c866f37df66ad0dda1bb" + ], + "index": "pypi", + "version": "==2.5.2" + }, + "django-extensions": { + "hashes": [ + "sha256:109004f80b6f45ad1f56addaa59debca91d94aa0dc1cb19678b9364b4fe9b6f4", + "sha256:307766e5e6c1caffe76c5d99239d8115d14ae3f7cab2cd991fcffd763dad904b" + ], + "index": "pypi", + "version": "==2.1.6" + }, + "django-postgres-extra": { + "editable": true, + "git": "https://github.com/SectorLabs/django-postgres-extra", + "ref": "eef2ed5504d225858d4e4f5d77a838082ca6053e" + }, + "django-redis-cache": { + "hashes": [ + "sha256:77dcb9d11beef5ce77dadfb95328b7712c3d9bde8419a0ba92968712b9bff48b" + ], + "index": "pypi", + "version": "==2.0.0" + }, + "django-silk": { + "hashes": [ + "sha256:ab6b7151a54eaa14d4fc77a58fd75e7c0c8bd60d29c87e55575845a304b0c0eb", + "sha256:bce0e35d2a6ec3688a0c062c6964695beef4a452be48085f2c1e25f685652d9d" + ], + "index": "pypi", + "version": "==3.0.1" + }, + "django-timezone-field": { + "hashes": [ + "sha256:7d7a37cfeacec5b1e81cd2f0aa334d46ebaa369cd516028579ed343cbc676c38", + "sha256:d9fdab77c443b78c362ffaeb50fe7d7b54692c89aaae8ca1cae67848139b82ac" + ], + "version": "==3.0" + }, + "djangorestframework": { + "hashes": [ + "sha256:8a435df9007c8b7d8e69a21ef06650e3c0cbe0d4b09e55dd1bd74c89a75a9fcd", + "sha256:f7a266260d656e1cf4ca54d7a7349609dc8af4fe2590edd0ecd7d7643ea94a17" + ], + "index": "pypi", + "version": "==3.9.2" + }, + "djangorestframework-jwt": { + "hashes": [ + "sha256:5efe33032f3a4518a300dc51a51c92145ad95fb6f4b272e5aa24701db67936a7", + "sha256:ab15dfbbe535eede8e2e53adaf52ef0cf018ee27dbfad10cbc4cbec2ab63d38c" + ], + "index": "pypi", + "version": "==1.11.0" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "version": "==0.14" + }, + "flower": { + "hashes": [ + "sha256:7f45acb297ab7cf3dd40140816143a2588f6938dbd70b8c46b59c7d8d1e93d55" + ], + "index": "pypi", + "version": "==0.9.3" + }, + "gprof2dot": { + "hashes": [ + "sha256:48c1e168c28b8a8eb23bf30fda78fe2ef218269a41505341ec27c27083e47cf4" + ], + "version": "==2016.10.13" + }, + "gunicorn": { + "hashes": [ + "sha256:aa8e0b40b4157b36a5df5e599f45c9c76d6af43845ba3b3b0efe2c70473c2471", + "sha256:fa2662097c66f920f53f70621c6c58ca4a3c4d3434205e608e121b5b3b71f4f3" + ], + "index": "pypi", + "version": "==19.9.0" + }, + "hiredis": { + "hashes": [ + "sha256:0124911115f2cb7deb4f8e221e109a53d3d718174b238a2c5e2162175a3929a5", + "sha256:0656658d0448c2c82c4890ae933c2c2e51196101d3d06fc19cc92e062410c2fd", + "sha256:09d284619f7142ddd7a4ffa94c12a0445e834737f4ce8739a737f2b1ca0f6142", + "sha256:12299b7026e5dc22ed0ff603375c1bf583cf59adbb0e4d062df434e9140d72dd", + "sha256:12fc6210f8dc3e9c8ce4b95e8f5db404b838dbdeb25bca41e33497de6d89334f", + "sha256:197febe5e63c77f4ad19b36e15ed33152064dc606c8b7413c7a0ca3fd04672cc", + "sha256:20e48289fbffb59a5ac7cc677fc02c2726c1da22488e5f7636b9feb9afde199f", + "sha256:26bed296b92b88db02afe214aa1fefad7f9e8ba88a5a7c0e355b55c4b168d212", + "sha256:321b19d2a21fd576111032fe7694d317de2c11b265ef775f2e3f22734a6b94c8", + "sha256:32d5f2c461250f5fc7ccef647682651b1d9f69443f16c213d7fa5e183222b233", + "sha256:36bfcc86715d109a5ef6edefd52b893de97d555cb5cb0e9cab83eb9665942ccc", + "sha256:438ddfd1484e98110959dc4648c0ba22c3307c9c0ae7e2a856755067f9ce9cef", + "sha256:66f17c1633b2fb967bf4165f7b3d369a1bdfe3537d3646cf9a7c208506c96c49", + "sha256:94ab0fa3ac93ab36a5400c474439881d182b43fd38a2766d984470c57931ae88", + "sha256:955f12da861f2608c181049f623bbb52851769e10639c4919cc586395b89813f", + "sha256:b1fd831f96ce0f715e9356574f5184b840b59eb8901fc5f9124fedbe84ad2a59", + "sha256:b3813c641494fca2eda66c32a2117816472a5a39b12f59f7887c6d17bdb8c77e", + "sha256:bbc3ee8663024c82a1226a0d56ad882f42a2fd8c2999bf52d27bdd25f1320f4b", + "sha256:bd12c2774b574f5b209196e25b03b5d62c7919bf69046bc7b955ebe84e0ec1fe", + "sha256:c54d2b3d7a2206df35f3c1140ac20ca6faf7819ff92ea5be8bf4d1cbdb433216", + "sha256:c7b0bcaf2353a2ad387dd8b5e1b5f55991adc3a7713ac3345a4ef0de58276690", + "sha256:c9319a1503efb3b5a4ec13b2f8fae2c23610a645e999cb8954d330f0610b0f6d", + "sha256:cbe5c0273224babe2ec77058643312d07aa5e8fed08901b3f7bccaa744c5728e", + "sha256:cc884ea50185009d794b31314a144110efc76b71beb0a5827a8bff970ae6d248", + "sha256:d1e2e751327781ad81df5a5a29d7c7b19ee0ebfbeddf037fd8df19ec1c06e18b", + "sha256:d2ef58cece6cae4b354411df498350d836f10b814c8a890df0d8079aff30c518", + "sha256:e97c953f08729900a5e740f1760305434d62db9f281ac351108d6c4b5bf51795", + "sha256:fcdf2e10f56113e1cb4326dbca7bf7edbfdbd246cd6d7ec088688e5439129e2c" + ], + "index": "pypi", + "version": "==1.0.0" + }, + "httplib2": { + "hashes": [ + "sha256:4ba6b8fd77d0038769bf3c33c9a96a6f752bc4cdf739701fdcaf210121f399d4" + ], + "version": "==0.12.1" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "jinja2": { + "hashes": [ + "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", + "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" + ], + "version": "==2.10.1" + }, + "jmespath": { + "hashes": [ + "sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6", + "sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c" + ], + "version": "==0.9.4" + }, + "kombu": { + "hashes": [ + "sha256:389ba09e03b15b55b1a7371a441c894fd8121d174f5583bbbca032b9ea8c9edd", + "sha256:7b92303af381ef02fad6899fd5f5a9a96031d781356cd8e505fa54ae5ddee181" + ], + "version": "==4.5.0" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, + "oauth2": { + "hashes": [ + "sha256:15b5c42301f46dd63113f1214b0d81a8b16254f65a86d3c32a1b52297f3266e6", + "sha256:c006a85e7c60107c7cc6da1b184b5c719f6dd7202098196dfa6e55df669b59bf" + ], + "index": "pypi", + "version": "==1.9.0.post1" + }, + "psycopg2-binary": { + "hashes": [ + "sha256:163d3ee445a0b4c0109877da9e46271aacf4e5e3d60ae7368669555c30f13e7c", + "sha256:1af0bfe7b0c13a0e613a27311fd4f9c5d024e8fc0f4b3d284e7df02a58a11fc0", + "sha256:2169c3a1bf52d5b30cc98625b5919a964c571a32e8646be20be6c7e3e82079de", + "sha256:218f079fa48e2ef812dc3d3ce6ec2f67ac56427ba4b038d5d6331f2cceb489c2", + "sha256:26a958930687e94c4c6c73c171e4d4783b82ae4e16aa3424e6bcd4529bceedf0", + "sha256:2c7c195aef3acdbc853942bc674844031a732890d2fee88a324298ed376b6c2b", + "sha256:2ecdbfed7004669472bfa27c8d51012c717c241c7154ae17e4c8f93024043525", + "sha256:345fc31b71a90ada1b51826537917b19a1af685a91c0f066787069c184d7d00f", + "sha256:378a06649503f548be5f1e9eec2e94cc1d6138250b82a08dcc6151bca8cec107", + "sha256:3f300bf2930e501dde09605de85cb2b84c2638e2c954be02a3c86f28176d3525", + "sha256:6c2f66c653ce8bbd7e789d0f7f92c3f9fea881b55226f0ae5ee550cce9e3cf0e", + "sha256:6fccbac2633831b877a8fbf865f7082d34895e82a015795a9f80f99a2efe2576", + "sha256:7a166f8ccb6888358d3e67795b057540ea7caa71ab9e089b0cb0097f01088965", + "sha256:8f6b84f887ec6fef6c1796779f8ec2603dc7e9ef52bc9269de719d4bcbdaebbb", + "sha256:92cf3ceb7bb90cf35b8bd993c640b15d4832ba0e142a3b9da5006ef217da595d", + "sha256:a20dfdf73f56da674926a3811929cff9fd23b9af90be9a6c36ac246a3486eef3", + "sha256:a84415df4689251556c961e4fe3b25d30e32f00faa8064ce0909458dbe0d67b2", + "sha256:ab1aa1cd50df3860f624c9713ee9e690eefd4e049d3a4d86577bab6e741e9616", + "sha256:abc9dcf85e75a8687f2a6d560c0c1a2593e8e34ba6f9ad6721f8212c5de179a2", + "sha256:c10454710a81a2f4b1ff4d1c83ac2cec63e0e55845a56324991514af5b1299d0", + "sha256:c38f80719e4dfae7a6311a4f091f07f4fb2fb5d602352015d5639f63f8fabb68", + "sha256:d75cf00605630b2cfefa5c62373c605dcda1cc0d607902847dbb8e8e9b67c1ce", + "sha256:dce15cb6ef604c9e38fdaa848f58f83153ade9f4aa5e4cf5812aa27163561750", + "sha256:e7e0db4311bb76bf3f6e0380f71912cfa6d0be7cc635e3772476050b0dabdabd", + "sha256:eac59cae78dfe3fbf7ece25c170d7a152f88df7643381aa5e7344c2028a8d8d4", + "sha256:ead7b3e1567bd14cacd44279c5e42cd19f54b9feed39180220253f4fbe3abd56", + "sha256:ed772a5e8e7e5dd6bede960a86940c17cf653c7f158dafa5d52e919b676f10ba", + "sha256:f2d73131acb94afa45de8b6b8a4bfb21bbe3736633d6478e53247f19dd8c299c" + ], + "index": "pypi", + "version": "==2.8.1" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pycurl": { + "hashes": [ + "sha256:0f0cdfc7a92d4f2a5c44226162434e34f7d6967d3af416a6f1448649c09a25a4", + "sha256:10510a0016c862af467c6e069e051409f15f5831552bed03f5104b395a5d7dd1", + "sha256:208dd2c89e80d32a69397ba8a5cdb3bc0dc60f961a4f2a9662e5e1624dc799d1", + "sha256:6dc6ee5e7628400083471cba8044010860fe8b22e4dee05e42150a68047d7d9d", + "sha256:794bda39ea6fe434b6e1f58ab3bea9f0e6123fb43702fecd760eed6f1547b20a", + "sha256:dae7277e7c06da00947f3cd32c095b1e65eae09f07478ada4ea9dfa57020b646", + "sha256:eccea049aef47decc380746b3ff242d95636d578c907d0eab3b00918292d6c48" + ], + "index": "pypi", + "version": "==7.43.0.2" + }, + "pygments": { + "hashes": [ + "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", + "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + ], + "version": "==2.3.1" + }, + "pyjwt": { + "hashes": [ + "sha256:5c6eca3c2940464d106b99ba83b00c6add741c9becaec087fb7ccdefea71350e", + "sha256:8d59a976fb773f3e6a39c85636357c4f0e242707394cadadd9814f5cbaa20e96" + ], + "version": "==1.7.1" + }, + "python-crontab": { + "hashes": [ + "sha256:91ce4b245ee5e5c117aa0b21b485bc43f2d80df854a36e922b707643f50d7923" + ], + "version": "==2.3.6" + }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "markers": "python_version >= '2.7'", + "version": "==2.8.0" + }, + "python-http-client": { + "hashes": [ + "sha256:7e430f4b9dd2b621b0051f6a362f103447ea8e267594c602a5c502a0c694ee38" + ], + "version": "==3.1.0" + }, + "pytz": { + "hashes": [ + "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", + "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + ], + "version": "==2019.1" + }, + "pyyaml": { + "hashes": [ + "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", + "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", + "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", + "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", + "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", + "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", + "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", + "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", + "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", + "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", + "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" + ], + "index": "pypi", + "version": "==5.1" + }, + "redis": { + "hashes": [ + "sha256:6946b5dca72e86103edc8033019cc3814c031232d339d5f4533b02ea85685175", + "sha256:8ca418d2ddca1b1a850afa1680a7d2fd1f3322739271de4b704e0d4668449273" + ], + "version": "==3.2.1" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "retry": { + "hashes": [ + "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", + "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4" + ], + "index": "pypi", + "version": "==0.9.2" + }, + "s3transfer": { + "hashes": [ + "sha256:7b9ad3213bff7d357f888e0fab5101b56fa1a0548ee77d121c3a3dbfbef4cb2e", + "sha256:f23d5cb7d862b104401d9021fc82e5fa0e0cf57b7660a1331425aab0c691d021" + ], + "version": "==0.2.0" + }, + "sendgrid": { + "hashes": [ + "sha256:351a7fc501d2b9d5afdcbc70a02490917057d6ce5cc22c558cadfc16229f157b", + "sha256:e1f93c72b3db3bd00d86f79ee926a093ee7e65533936a140855916569b08e0b0" + ], + "index": "pypi", + "version": "==6.0.4" + }, + "sentry-sdk": { + "hashes": [ + "sha256:ca2723556c102a1fabdf461b9a038d1d8631608c4d10085a7c06a0b590e79ad4", + "sha256:ced85a48171b3421d71f14f1682168f8008581411893e42359469c397fdf6285" + ], + "index": "pypi", + "version": "==0.7.10" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "sqlparse": { + "hashes": [ + "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", + "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" + ], + "version": "==0.3.0" + }, + "tornado": { + "hashes": [ + "sha256:0662d28b1ca9f67108c7e3b77afabfb9c7e87bde174fbda78186ecedc2499a9d", + "sha256:4e5158d97583502a7e2739951553cbd88a72076f152b4b11b64b9a10c4c49409", + "sha256:732e836008c708de2e89a31cb2fa6c0e5a70cb60492bee6f1ea1047500feaf7f", + "sha256:8154ec22c450df4e06b35f131adc4f2f3a12ec85981a203301d310abf580500f", + "sha256:8e9d728c4579682e837c92fdd98036bd5cdefa1da2aaf6acf26947e6dd0c01c5", + "sha256:d4b3e5329f572f055b587efc57d29bd051589fb5a43ec8898c77a47ec2fa2bbb", + "sha256:e5f2585afccbff22390cddac29849df463b252b711aa2ce7c5f3f342a5b3b444" + ], + "version": "==5.1.1" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "markers": "python_version >= '3.4'", + "version": "==1.24.1" + }, + "vine": { + "hashes": [ + "sha256:133ee6d7a9016f177ddeaf191c1f58421a1dcc6ee9a42c58b34bed40e1d2cd87", + "sha256:ea4947cc56d1fd6f2095c8d543ee25dad966f78692528e68b4fada11ba3f98af" + ], + "version": "==1.3.0" + }, + "xmltodict": { + "hashes": [ + "sha256:50d8c638ed7ecb88d90561beedbf720c9b4e851a9fa6c47ebd64e99d166d8a21", + "sha256:8bbcb45cc982f48b2ca8fe7e7827c5d792f217ecf1792626f808bf41c3b86051" + ], + "index": "pypi", + "version": "==0.12.0" + } + }, + "develop": { + "appdirs": { + "hashes": [ + "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92", + "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e" + ], + "version": "==1.4.3" + }, + "aspy.yaml": { + "hashes": [ + "sha256:ae249074803e8b957c83fdd82a99160d0d6d26dff9ba81ba608b42eebd7d8cd3", + "sha256:c7390d79f58eb9157406966201abf26da0d56c07e0ff0deadc39c8f4dbc13482" + ], + "version": "==1.2.0" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "autoflake": { + "hashes": [ + "sha256:c103e63466f11db3617167a2c68ff6a0cda35b940222920631c6eeec6b67e807" + ], + "index": "pypi", + "version": "==1.2" + }, + "black": { + "hashes": [ + "sha256:22158b89c1a6b4eb333a1e65e791a3f8b998cf3b11ae094adb2570f31f769a44", + "sha256:4b475bbd528acce094c503a3d2dbc2d05a4075f6d0ef7d9e7514518e14cc5191" + ], + "index": "pypi", + "version": "==18.6b4" + }, + "cfgv": { + "hashes": [ + "sha256:6e9f2feea5e84bc71e56abd703140d7a2c250fc5ba38b8702fd6a68ed4e3b2ef", + "sha256:e7f186d4a36c099a9e20b04ac3108bd8bb9b9257e692ce18c8c3764d5cb12172" + ], + "version": "==1.6.0" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "coverage": { + "hashes": [ + "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", + "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", + "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", + "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", + "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", + "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", + "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", + "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", + "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", + "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", + "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", + "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", + "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", + "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", + "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", + "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", + "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", + "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", + "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", + "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", + "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", + "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", + "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", + "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", + "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", + "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", + "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", + "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", + "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", + "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", + "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" + ], + "index": "pypi", + "version": "==4.5.3" + }, + "dredd-hooks": { + "hashes": [ + "sha256:7d0527ee269d716126de912098b6d8750fcb3755232cb902e5a360f1921df780" + ], + "index": "pypi", + "version": "==0.2.0" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "factory-boy": { + "hashes": [ + "sha256:6f25cc4761ac109efd503f096e2ad99421b1159f01a29dbb917359dcd68e08ca", + "sha256:d552cb872b310ae78bd7429bf318e42e1e903b1a109e899a523293dfa762ea4f" + ], + "index": "pypi", + "version": "==2.11.1" + }, + "faker": { + "hashes": [ + "sha256:00b7011757c4907546f17d0e47df098b542ea2b04c966ee0e80a493aae2c13c8", + "sha256:745ac8b9c9526e338696e07b7f2e206e5e317e5744e22fdd7c2894bf19af41f1" + ], + "version": "==1.0.4" + }, + "flake8": { + "hashes": [ + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" + ], + "index": "pypi", + "version": "==3.7.7" + }, + "identify": { + "hashes": [ + "sha256:244e7864ef59f0c7c50c6db73f58564151d91345cd9b76ed793458953578cadd", + "sha256:8ff062f90ad4b09cfe79b5dfb7a12e40f19d2e68a5c9598a49be45f16aba7171" + ], + "version": "==1.4.1" + }, + "importlib-metadata": { + "hashes": [ + "sha256:46fc60c34b6ed7547e2a723fc8de6dc2e3a1173f8423246b3ce497f064e9c3de", + "sha256:bc136180e961875af88b1ab85b4009f4f1278f8396a60526c0009f503a1a96ca" + ], + "version": "==0.9" + }, + "isort": { + "hashes": [ + "sha256:01cb7e1ca5e6c5b3f235f0385057f70558b70d2f00320208825fa62887292f43", + "sha256:268067462aed7eb2a1e237fcb287852f22077de3fb07964e87e00f829eea2d1a" + ], + "index": "pypi", + "version": "==4.3.17" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:2112d2ca570bb7c3e53ea1a35cd5df42bb0fd10c45f0fb97178679c3c03d64c7", + "sha256:c3e4748ba1aad8dba30a4886b0b1a2004f9a863837b8654e7059eebf727afa5a" + ], + "index": "pypi", + "version": "==7.0.0" + }, + "nodeenv": { + "hashes": [ + "sha256:ad8259494cf1c9034539f6cced78a1da4840a4b157e23640bc4a0c0546b0cb7a" + ], + "version": "==1.3.3" + }, + "numpy": { + "hashes": [ + "sha256:1980f8d84548d74921685f68096911585fee393975f53797614b34d4f409b6da", + "sha256:22752cd809272671b273bb86df0f505f505a12368a3a5fc0aa811c7ece4dfd5c", + "sha256:23cc40313036cffd5d1873ef3ce2e949bdee0646c5d6f375bf7ee4f368db2511", + "sha256:2b0b118ff547fecabc247a2668f48f48b3b1f7d63676ebc5be7352a5fd9e85a5", + "sha256:3a0bd1edf64f6a911427b608a894111f9fcdb25284f724016f34a84c9a3a6ea9", + "sha256:3f25f6c7b0d000017e5ac55977a3999b0b1a74491eacb3c1aa716f0e01f6dcd1", + "sha256:4061c79ac2230594a7419151028e808239450e676c39e58302ad296232e3c2e8", + "sha256:560ceaa24f971ab37dede7ba030fc5d8fa173305d94365f814d9523ffd5d5916", + "sha256:62be044cd58da2a947b7e7b2252a10b42920df9520fc3d39f5c4c70d5460b8ba", + "sha256:6c692e3879dde0b67a9dc78f9bfb6f61c666b4562fd8619632d7043fb5b691b0", + "sha256:6f65e37b5a331df950ef6ff03bd4136b3c0bbcf44d4b8e99135d68a537711b5a", + "sha256:7a78cc4ddb253a55971115f8320a7ce28fd23a065fc33166d601f51760eecfa9", + "sha256:80a41edf64a3626e729a62df7dd278474fc1726836552b67a8c6396fd7e86760", + "sha256:893f4d75255f25a7b8516feb5766c6b63c54780323b9bd4bc51cdd7efc943c73", + "sha256:972ea92f9c1b54cc1c1a3d8508e326c0114aaf0f34996772a30f3f52b73b942f", + "sha256:9f1d4865436f794accdabadc57a8395bd3faa755449b4f65b88b7df65ae05f89", + "sha256:9f4cd7832b35e736b739be03b55875706c8c3e5fe334a06210f1a61e5c2c8ca5", + "sha256:adab43bf657488300d3aeeb8030d7f024fcc86e3a9b8848741ea2ea903e56610", + "sha256:bd2834d496ba9b1bdda3a6cf3de4dc0d4a0e7be306335940402ec95132ad063d", + "sha256:d20c0360940f30003a23c0adae2fe50a0a04f3e48dc05c298493b51fd6280197", + "sha256:d3b3ed87061d2314ff3659bb73896e622252da52558f2380f12c421fbdee3d89", + "sha256:dc235bf29a406dfda5790d01b998a1c01d7d37f449128c0b1b7d1c89a84fae8b", + "sha256:fb3c83554f39f48f3fa3123b9c24aecf681b1c289f9334f8215c1d3c8e2f6e5b" + ], + "version": "==1.16.2" + }, + "pandas": { + "hashes": [ + "sha256:071e42b89b57baa17031af8c6b6bbd2e9a5c68c595bc6bf9adabd7a9ed125d3b", + "sha256:17450e25ae69e2e6b303817bdf26b2cd57f69595d8550a77c308be0cd0fd58fa", + "sha256:17916d818592c9ec891cbef2e90f98cc85e0f1e89ed0924c9b5220dc3209c846", + "sha256:2538f099ab0e9f9c9d09bbcd94b47fd889bad06dc7ae96b1ed583f1dc1a7a822", + "sha256:366f30710172cb45a6b4f43b66c220653b1ea50303fbbd94e50571637ffb9167", + "sha256:42e5ad741a0d09232efbc7fc648226ed93306551772fc8aecc6dce9f0e676794", + "sha256:4e718e7f395ba5bfe8b6f6aaf2ff1c65a09bb77a36af6394621434e7cc813204", + "sha256:4f919f409c433577a501e023943e582c57355d50a724c589e78bc1d551a535a2", + "sha256:4fe0d7e6438212e839fc5010c78b822664f1a824c0d263fd858f44131d9166e2", + "sha256:5149a6db3e74f23dc3f5a216c2c9ae2e12920aa2d4a5b77e44e5b804a5f93248", + "sha256:627594338d6dd995cfc0bacd8e654cd9e1252d2a7c959449228df6740d737eb8", + "sha256:83c702615052f2a0a7fb1dd289726e29ec87a27272d775cb77affe749cca28f8", + "sha256:8c872f7fdf3018b7891e1e3e86c55b190e6c5cee70cab771e8f246c855001296", + "sha256:90f116086063934afd51e61a802a943826d2aac572b2f7d55caaac51c13db5b5", + "sha256:a3352bacac12e1fc646213b998bce586f965c9d431773d9e91db27c7c48a1f7d", + "sha256:bcdd06007cca02d51350f96debe51331dec429ac8f93930a43eb8fb5639e3eb5", + "sha256:c1bd07ebc15285535f61ddd8c0c75d0d6293e80e1ee6d9a8d73f3f36954342d0", + "sha256:c9a4b7c55115eb278c19aa14b34fcf5920c8fe7797a09b7b053ddd6195ea89b3", + "sha256:cc8fc0c7a8d5951dc738f1c1447f71c43734244453616f32b8aa0ef6013a5dfb", + "sha256:d7b460bc316064540ce0c41c1438c416a40746fd8a4fb2999668bf18f3c4acf1" + ], + "index": "pypi", + "version": "==0.24.2" + }, + "pre-commit": { + "hashes": [ + "sha256:75a9110eae00d009c913616c0fc8a6a02e7716c4a29a14cac9b313d2c7338ab0", + "sha256:f882c65316eb5b705fe4613e92a7c91055c1800102e4d291cfd18912ec9cf90e" + ], + "index": "pypi", + "version": "==1.15.1" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pydot": { + "hashes": [ + "sha256:67be714300c78fda5fd52f79ec994039e3f76f074948c67b5ff539b433ad354f", + "sha256:d49c9d4dd1913beec2a997f831543c8cbd53e535b1a739e921642fe416235f01" + ], + "index": "pypi", + "version": "==1.4.1" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pyparsing": { + "hashes": [ + "sha256:1873c03321fc118f4e9746baf201ff990ceb915f433f23b395f5580d1840cb2a", + "sha256:9b6323ef4ab914af344ba97510e966d64ba91055d6b9afa6b30799340e89cc03" + ], + "index": "pypi", + "version": "==2.4.0" + }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "markers": "python_version >= '2.7'", + "version": "==2.8.0" + }, + "pytz": { + "hashes": [ + "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", + "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + ], + "version": "==2019.1" + }, + "pyyaml": { + "hashes": [ + "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", + "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", + "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", + "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", + "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", + "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", + "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", + "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", + "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", + "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", + "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" + ], + "index": "pypi", + "version": "==5.1" + }, + "selenium": { + "hashes": [ + "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", + "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d" + ], + "index": "pypi", + "version": "==3.141.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "tblib": { + "hashes": [ + "sha256:436e4200e63d92316551179dc540906652878df4ff39b43db30fcf6400444fe7", + "sha256:9bae4b8c44b06af0e114bfc4d5f6aa3eafd2119af5a4dcab34f51f1665f16c59" + ], + "index": "pypi", + "version": "==1.3.2" + }, + "text-unidecode": { + "hashes": [ + "sha256:5a1375bb2ba7968740508ae38d92e1f889a0832913cb1c447d5e2046061a396d", + "sha256:801e38bd550b943563660a91de8d4b6fa5df60a542be9093f7abf819f86050cc" + ], + "version": "==1.2" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "markers": "python_version >= '3.4'", + "version": "==1.24.1" + }, + "virtualenv": { + "hashes": [ + "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", + "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" + ], + "version": "==16.4.3" + }, + "zipp": { + "hashes": [ + "sha256:55ca87266c38af6658b84db8cfb7343cdb0bf275f93c7afaea0d8e7a209c7478", + "sha256:682b3e1c62b7026afe24eadf6be579fb45fec54c07ea218bded8092af07a68c4" + ], + "version": "==0.3.3" + } + } +} diff --git a/integration/testdata/fixtures/repo/uv/uv.lock b/integration/testdata/fixtures/repo/uv/uv.lock new file mode 100644 index 000000000000..e16e05558ffb --- /dev/null +++ b/integration/testdata/fixtures/repo/uv/uv.lock @@ -0,0 +1,51 @@ +version = 1 +requires-python = ">=3.11" +resolution-markers = [ + "python_full_version < '3.12'", + "python_full_version >= '3.12'", +] + +[[package]] +name = "click" +version = "8.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "platform_system == 'Windows'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/59/87/84326af34517fca8c58418d148f2403df25303e02736832403587318e9e8/click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e", size = 331147 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f1/df59e28c642d583f7dacffb1e0965d0e00b218e0186d7858ac5233dce840/click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48", size = 96588 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "uv-test" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "click" }, + { name = "werkzeug" }, +] + +[package.metadata] +requires-dist = [ + { name = "click", specifier = "==8.1.3" }, + { name = "werkzeug", specifier = "==0.11.1" }, +] + +[[package]] +name = "werkzeug" +version = "0.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/b4/824c916f1451164bd871ad062defa2f48a34e2e952b12c0422f3ecf3a531/Werkzeug-0.11.1.tar.gz", hash = "sha256:280b37866a1fa8604549cbc278f06d310143e20f1a6520ae5cf66e0df615e0a7", size = 1151713 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/37/398042f9b852c57f7e9b4e1173dcd40e7b9980d9c76a700647c458f582b1/Werkzeug-0.11.1-py2.py3-none-any.whl", hash = "sha256:bca5d6a06e4603abd0d2c10bb5ce523039bf16a0a7b8320bd74f6458d20d4c3b", size = 304783 }, +] diff --git a/integration/testdata/fixtures/repo/yarn/node_modules/jquery/package.json b/integration/testdata/fixtures/repo/yarn/node_modules/jquery/package.json new file mode 100644 index 000000000000..d50be28053ce --- /dev/null +++ b/integration/testdata/fixtures/repo/yarn/node_modules/jquery/package.json @@ -0,0 +1,118 @@ +{ + "name": "jquery", + "title": "jQuery", + "description": "JavaScript library for DOM operations", + "version": "3.2.1", + "main": "dist/jquery.js", + "homepage": "https://jquery.com", + "author": { + "name": "OpenJS Foundation and other contributors", + "url": "https://github.com/jquery/jquery/blob/3.7.0/AUTHORS.txt" + }, + "repository": { + "type": "git", + "url": "https://github.com/jquery/jquery.git" + }, + "keywords": [ + "jquery", + "javascript", + "browser", + "library" + ], + "bugs": { + "url": "https://github.com/jquery/jquery/issues" + }, + "license": "MIT", + "devDependencies": { + "@babel/core": "7.3.3", + "@babel/plugin-transform-for-of": "7.2.0", + "colors": "1.4.0", + "commitplease": "3.2.0", + "core-js": "2.6.5", + "eslint-config-jquery": "3.0.0", + "grunt": "1.5.3", + "grunt-babel": "8.0.0", + "grunt-cli": "1.4.3", + "grunt-compare-size": "0.4.2", + "grunt-contrib-uglify": "3.4.0", + "grunt-contrib-watch": "1.1.0", + "grunt-eslint": "22.0.0", + "grunt-git-authors": "3.2.0", + "grunt-jsonlint": "2.1.2", + "grunt-karma": "4.0.2", + "grunt-newer": "1.3.0", + "grunt-npmcopy": "0.2.0", + "gzip-js": "0.3.2", + "husky": "4.2.5", + "jsdom": "19.0.0", + "karma": "6.4.1", + "karma-browserstack-launcher": "1.6.0", + "karma-chrome-launcher": "3.1.1", + "karma-firefox-launcher": "2.1.2", + "karma-ie-launcher": "1.0.0", + "karma-jsdom-launcher": "12.0.0", + "karma-qunit": "4.1.2", + "karma-webkit-launcher": "2.1.0", + "load-grunt-tasks": "5.1.0", + "native-promise-only": "0.8.1", + "playwright-webkit": "1.30.0", + "promises-aplus-tests": "2.1.2", + "q": "1.5.1", + "qunit": "2.9.2", + "raw-body": "2.3.3", + "requirejs": "2.3.6", + "sinon": "2.3.7", + "strip-json-comments": "2.0.1", + "testswarm": "1.1.2", + "uglify-js": "3.4.7" + }, + "scripts": { + "build": "npm install && grunt", + "start": "grunt watch", + "test:browserless": "grunt && grunt test:slow", + "test:browser": "grunt && grunt karma:main", + "test:amd": "grunt && grunt karma:amd", + "test:no-deprecated": "grunt test:prepare && grunt custom:-deprecated && grunt karma:main", + "test:selector-native": "grunt test:prepare && grunt custom:-selector && grunt karma:main", + "test:slim": "grunt test:prepare && grunt custom:slim && grunt karma:main", + "test": "npm run test:slim && npm run test:no-deprecated && npm run test:selector-native && grunt && grunt test:slow && grunt karma:main && grunt karma:amd", + "jenkins": "npm run test:browserless" + }, + "commitplease": { + "nohook": true, + "components": [ + "Docs", + "Tests", + "Build", + "Support", + "Release", + "Core", + "Ajax", + "Attributes", + "Callbacks", + "CSS", + "Data", + "Deferred", + "Deprecated", + "Dimensions", + "Effects", + "Event", + "Manipulation", + "Offset", + "Queue", + "Selector", + "Serialize", + "Traversing", + "Wrap" + ], + "markerPattern": "^((clos|fix|resolv)(e[sd]|ing))|^(refs?)", + "ticketPattern": "^((Closes|Fixes) ([a-zA-Z]{2,}-)[0-9]+)|^(Refs? [^#])" + }, + "husky": { + "hooks": { + "commit-msg": "commitplease .git/COMMIT_EDITMSG", + "pre-commit": "grunt lint:newer qunit_fixture" + } + } + } + \ No newline at end of file diff --git a/integration/testdata/fixtures/repo/yarn/package.json b/integration/testdata/fixtures/repo/yarn/package.json new file mode 100644 index 000000000000..bd8d908300e8 --- /dev/null +++ b/integration/testdata/fixtures/repo/yarn/package.json @@ -0,0 +1,12 @@ +{ + "name": "integration", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "jquery": "^3.2.1" + }, + "devDependencies": { + "promise": "^8.0.3" + } +} diff --git a/integration/testdata/fixtures/repo/yarn/yarn.lock b/integration/testdata/fixtures/repo/yarn/yarn.lock new file mode 100644 index 000000000000..584449388a07 --- /dev/null +++ b/integration/testdata/fixtures/repo/yarn/yarn.lock @@ -0,0 +1,20 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +jquery@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" + integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== + +promise@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6" + integrity sha512-HeRDUL1RJiLhyA0/grn+PTShlBAcLuh/1BJGtrvjwbvRDCTLLMEz9rOGCV+R3vHY4MixIuoMEd9Yq/XvsTPcjw== + dependencies: + asap "~2.0.6" diff --git a/integration/testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl b/integration/testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl new file mode 100644 index 000000000000..797174e66427 --- /dev/null +++ b/integration/testdata/fixtures/sbom/centos-7-cyclonedx.intoto.jsonl @@ -0,0 +1 @@ +{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6ImluZGV4LmRvY2tlci5pby9saWJyYXJ5L2NlbnRvcyIsImRpZ2VzdCI6eyJzaGEyNTYiOiJiZTY1ZjQ4OGI3NzY0YWQzNjM4ZjIzNmI3YjUxNWIzNjc4MzY5YTUxMjRjNDdiOGQzMjkxNmQ2NDg3NDE4ZWE0In19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsInNwZWNWZXJzaW9uIjoiMS40Iiwic2VyaWFsTnVtYmVyIjoidXJuOnV1aWQ6MTQ1NWMwMmQtNjRjYS00NTNlLWE1ZGYtZGRmYjcwYTdjODA0IiwidmVyc2lvbiI6MSwibWV0YWRhdGEiOnsidGltZXN0YW1wIjoiMjAyMi0wNi0xNFQxNTowODo0OCswMDowMCIsInRvb2xzIjpbeyJ2ZW5kb3IiOiJhcXVhc2VjdXJpdHkiLCJuYW1lIjoidHJpdnkiLCJ2ZXJzaW9uIjoiZGV2In1dLCJjb21wb25lbnQiOnsiYm9tLXJlZiI6ImQwZDQxZTMwLTk2NTAtNDg5ZC05NDhkLTQyNWZmMmVkNjNkMiIsInR5cGUiOiJjb250YWluZXIiLCJuYW1lIjoiaW50ZWdyYXRpb24vdGVzdGRhdGEvZml4dHVyZXMvaW1hZ2VzL2NlbnRvcy03LnRhci5neiIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6U2NoZW1hVmVyc2lvbiIsInZhbHVlIjoiMiJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpJbWFnZUlEIiwidmFsdWUiOiJzaGEyNTY6ZjFjYjdjN2Q1OGI3M2VhYzg1OWMzOTU4ODJlZWM0OWQ1MDY1MTI0NGUzNDJjZDZjNjhhNWM3ODA5Nzg1ZjQyNyJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpEaWZmSUQiLCJ2YWx1ZSI6InNoYTI1Njo4OTE2OWQ4N2RiZTJiNzJiYTQyYmZiYjM1NzljOTU3MzIyYmFjYTI4ZTAzYTFlNTU4MDc2NTQyYTFjMWIyYjRhIn0seyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlJlcG9UYWciLCJ2YWx1ZSI6ImdoY3IuaW8vYXF1YXNlY3VyaXR5L3RyaXZ5LXRlc3QtaW1hZ2VzOmNlbnRvcy03In0seyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlJlZmVyZW5jZSIsInZhbHVlIjoiZ2hjci5pby9hcXVhc2VjdXJpdHkvdHJpdnktdGVzdC1pbWFnZXM6Y2VudG9zLTcifV19fSwiY29tcG9uZW50cyI6W3siYm9tLXJlZiI6InBrZzpycG0vY2VudG9zL2Jhc2hANC4yLjQ2LTMxLmVsNz9hcmNoPXg4Nl82NCZkaXN0cm89Y2VudG9zLTcuNi4xODEwIiwidHlwZSI6ImxpYnJhcnkiLCJuYW1lIjoiYmFzaCIsInZlcnNpb24iOiI0LjIuNDYtMzEuZWw3IiwibGljZW5zZXMiOlt7ImV4cHJlc3Npb24iOiJHUEx2MysifV0sInB1cmwiOiJwa2c6cnBtL2NlbnRvcy9iYXNoQDQuMi40Ni0zMS5lbDc/YXJjaD14ODZfNjQmZGlzdHJvPWNlbnRvcy03LjYuMTgxMCIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6UGtnSUQiLCJ2YWx1ZSI6ImJhc2hANC4yLjQ2LTMxLmVsNy54ODZfNjQifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6U3JjTmFtZSIsInZhbHVlIjoiYmFzaCJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpTcmNWZXJzaW9uIiwidmFsdWUiOiI0LjIuNDYifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6U3JjUmVsZWFzZSIsInZhbHVlIjoiMzEuZWw3In0seyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OkxheWVyRGlnZXN0IiwidmFsdWUiOiJzaGEyNTY6YWM5MjA4MjA3YWRhYWMzYTQ4ZTU0YTRkYzZiNDljNjllNzhjMzA3MmQyYjNhZGQ3ZWZkYWJmODE0ZGIyMTMzYiJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpMYXllckRpZmZJRCIsInZhbHVlIjoic2hhMjU2Ojg5MTY5ZDg3ZGJlMmI3MmJhNDJiZmJiMzU3OWM5NTczMjJiYWNhMjhlMDNhMWU1NTgwNzY1NDJhMWMxYjJiNGEifV19LHsiYm9tLXJlZiI6InBrZzpycG0vY2VudG9zL29wZW5zc2wtbGlic0AxLjAuMmstMTYuZWw3P2FyY2g9eDg2XzY0JmVwb2NoPTEmZGlzdHJvPWNlbnRvcy03LjYuMTgxMCIsInR5cGUiOiJsaWJyYXJ5IiwibmFtZSI6Im9wZW5zc2wtbGlicyIsInZlcnNpb24iOiIxLjAuMmstMTYuZWw3IiwibGljZW5zZXMiOlt7ImV4cHJlc3Npb24iOiJPcGVuU1NMIn1dLCJwdXJsIjoicGtnOnJwbS9jZW50b3Mvb3BlbnNzbC1saWJzQDEuMC4yay0xNi5lbDc/YXJjaD14ODZfNjQmZXBvY2g9MSZkaXN0cm89Y2VudG9zLTcuNi4xODEwIiwicHJvcGVydGllcyI6W3sibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpQa2dJRCIsInZhbHVlIjoib3BlbnNzbC1saWJzQDEuMC4yay0xNi5lbDcueDg2XzY0In0seyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlNyY05hbWUiLCJ2YWx1ZSI6Im9wZW5zc2wifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6U3JjVmVyc2lvbiIsInZhbHVlIjoiMS4wLjJrIn0seyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlNyY1JlbGVhc2UiLCJ2YWx1ZSI6IjE2LmVsNyJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpTcmNFcG9jaCIsInZhbHVlIjoiMSJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpMYXllckRpZ2VzdCIsInZhbHVlIjoic2hhMjU2OmFjOTIwODIwN2FkYWFjM2E0OGU1NGE0ZGM2YjQ5YzY5ZTc4YzMwNzJkMmIzYWRkN2VmZGFiZjgxNGRiMjEzM2IifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6TGF5ZXJEaWZmSUQiLCJ2YWx1ZSI6InNoYTI1Njo4OTE2OWQ4N2RiZTJiNzJiYTQyYmZiYjM1NzljOTU3MzIyYmFjYTI4ZTAzYTFlNTU4MDc2NTQyYTFjMWIyYjRhIn1dfSx7ImJvbS1yZWYiOiIwMTc1ZjczMi1kZjlkLTRiYjgtOWY1Ni04NzA4OThlM2ZmODkiLCJ0eXBlIjoib3BlcmF0aW5nLXN5c3RlbSIsIm5hbWUiOiJjZW50b3MiLCJ2ZXJzaW9uIjoiNy42LjE4MTAiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlR5cGUiLCJ2YWx1ZSI6ImNlbnRvcyJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpDbGFzcyIsInZhbHVlIjoib3MtcGtncyJ9XX1dLCJkZXBlbmRlbmNpZXMiOlt7InJlZiI6IjAxNzVmNzMyLWRmOWQtNGJiOC05ZjU2LTg3MDg5OGUzZmY4OSIsImRlcGVuZHNPbiI6WyJwa2c6cnBtL2NlbnRvcy9iYXNoQDQuMi40Ni0zMS5lbDc/YXJjaD14ODZfNjQmZGlzdHJvPWNlbnRvcy03LjYuMTgxMCIsInBrZzpycG0vY2VudG9zL29wZW5zc2wtbGlic0AxLjAuMmstMTYuZWw3P2FyY2g9eDg2XzY0JmVwb2NoPTEmZGlzdHJvPWNlbnRvcy03LjYuMTgxMCJdfSx7InJlZiI6ImQwZDQxZTMwLTk2NTAtNDg5ZC05NDhkLTQyNWZmMmVkNjNkMiIsImRlcGVuZHNPbiI6WyIwMTc1ZjczMi1kZjlkLTRiYjgtOWY1Ni04NzA4OThlM2ZmODkiXX1dfX0K","signatures":[{"keyid":"","sig":"MEUCIQCtj78dipe+yzdlIsmwjn9QeaBTAPQacwIJAWfnrtp7FwIgcViOUgPA0WFYjimrIl7vbygdSpduM+ZzY3cqrDciH1U="}]} diff --git a/integration/testdata/fixtures/sbom/centos-7-cyclonedx.json b/integration/testdata/fixtures/sbom/centos-7-cyclonedx.json new file mode 100644 index 000000000000..81d4a72e240f --- /dev/null +++ b/integration/testdata/fixtures/sbom/centos-7-cyclonedx.json @@ -0,0 +1,160 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:1455c02d-64ca-453e-a5df-ddfb70a7c804", + "version": 1, + "metadata": { + "timestamp": "2022-06-14T15:08:48+00:00", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "d0d41e30-9650-489d-948d-425ff2ed63d2", + "type": "container", + "name": "integration/testdata/fixtures/images/centos-7.tar.gz", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "ghcr.io/aquasecurity/trivy-test-images:centos-7" + }, + { + "name": "aquasecurity:trivy:Reference", + "value": "ghcr.io/aquasecurity/trivy-test-images:centos-7" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810", + "type": "library", + "name": "bash", + "version": "4.2.46-31.el7", + "licenses": [ + { + "license": { + "name": "GPLv3+" + } + } + ], + "purl": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "bash@4.2.46-31.el7.x86_64" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "bash" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.2.46" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "31.el7" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + } + ] + }, + { + "bom-ref": "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810", + "type": "library", + "name": "openssl-libs", + "version": "1.0.2k-16.el7", + "licenses": [ + { + "license": { + "name": "OpenSSL+" + } + } + ], + "purl": "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "openssl-libs@1.0.2k-16.el7.x86_64" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.0.2k" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "16.el7" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + } + ] + }, + { + "bom-ref": "0175f732-df9d-4bb8-9f56-870898e3ff89", + "type": "operating-system", + "name": "centos", + "version": "7.6.1810", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "centos" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "0175f732-df9d-4bb8-9f56-870898e3ff89", + "dependsOn": [ + "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810", + "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810" + ] + }, + { + "ref": "d0d41e30-9650-489d-948d-425ff2ed63d2", + "dependsOn": [ + "0175f732-df9d-4bb8-9f56-870898e3ff89" + ] + } + ] +} \ No newline at end of file diff --git a/integration/testdata/fixtures/sbom/centos-7-spdx.json b/integration/testdata/fixtures/sbom/centos-7-spdx.json new file mode 100644 index 000000000000..9ae0ec0cf10b --- /dev/null +++ b/integration/testdata/fixtures/sbom/centos-7-spdx.json @@ -0,0 +1,98 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-13T13:27:55.874784Z", + "creators": [ + "Tool: trivy-dev", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://trivy.dev/container_image/integration/testdata/fixtures/images/centos-7.tar.gz-2906855d-5098-4a22-9a72-4f7099ea3d66", + "name": "integration/testdata/fixtures/images/centos-7.tar.gz", + "packages": [ + { + "SPDXID": "SPDXRef-ContainerImage-dd5cad897c6263", + "attributionTexts": [ + "SchemaVersion: 2", + "ImageID: sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427", + "DiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a", + "RepoTag: ghcr.io/aquasecurity/trivy-test-images:centos-7", + "Reference: ghcr.io/aquasecurity/trivy-test-images:centos-7" + ], + "filesAnalyzed": false, + "name": "integration/testdata/fixtures/images/centos-7.tar.gz" + }, + { + "SPDXID": "SPDXRef-OperatingSystem-2e91c856c499a371", + "filesAnalyzed": false, + "name": "centos", + "versionInfo": "7.6.1810" + }, + { + "SPDXID": "SPDXRef-Package-5a18334f22149877", + "attributionTexts": [ + "PkgID: bash@4.2.46-31.el7.x86_64", + "LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64\u0026distro=centos-7.6.1810", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "GPLv3+", + "licenseDeclared": "GPLv3+", + "name": "bash", + "sourceInfo": "built package from: bash 4.2.46-31.el7", + "versionInfo": "4.2.46" + }, + { + "SPDXID": "SPDXRef-Package-e16b1cbaa5186199", + "attributionTexts": [ + "PkgID: openssl-libs@1.0.2k-16.el7.x86_64", + "LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b", + "LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a" + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64\u0026epoch=1\u0026distro=centos-7.6.1810", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "OpenSSL", + "licenseDeclared": "OpenSSL", + "name": "openssl-libs", + "sourceInfo": "built package from: openssl-libs 1:1.0.2k-16.el7", + "versionInfo": "1:1.0.2k-19.el7" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-dd5cad897c6263", + "relationshipType": "DESCRIBES", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-OperatingSystem-2e91c856c499a371", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-dd5cad897c6263" + }, + { + "relatedSpdxElement": "SPDXRef-Package-5a18334f22149877", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-OperatingSystem-2e91c856c499a371" + }, + { + "relatedSpdxElement": "SPDXRef-Package-e16b1cbaa5186199", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-OperatingSystem-2e91c856c499a371" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/integration/testdata/fixtures/sbom/centos-7-spdx.txt b/integration/testdata/fixtures/sbom/centos-7-spdx.txt new file mode 100644 index 000000000000..fc57bb0d824a --- /dev/null +++ b/integration/testdata/fixtures/sbom/centos-7-spdx.txt @@ -0,0 +1,61 @@ +SPDXVersion: SPDX-2.2 +DataLicense: CC0-1.0 +SPDXID: SPDXRef-DOCUMENT +DocumentName: integration/testdata/fixtures/images/centos-7.tar.gz +DocumentNamespace: http://trivy.dev/container_image/integration/testdata/fixtures/images/centos-7.tar.gz-6a2c050f-bc12-46dc-b2df-1f4e3e0b5e1d +Creator: Organization: aquasecurity +Creator: Tool: trivy-dev +Created: 2022-09-13T13:24:58.796907Z + +##### Package: integration/testdata/fixtures/images/centos-7.tar.gz + +PackageName: integration/testdata/fixtures/images/centos-7.tar.gz +SPDXID: SPDXRef-ContainerImage-dd5cad897c6263 +FilesAnalyzed: false +PackageAttributionText: SchemaVersion: 2 +PackageAttributionText: ImageID: sha256:f1cb7c7d58b73eac859c395882eec49d50651244e342cd6c68a5c7809785f427 +PackageAttributionText: DiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a +PackageAttributionText: RepoTag: ghcr.io/aquasecurity/trivy-test-images:centos-7 +PackageAttributionText: Reference: ghcr.io/aquasecurity/trivy-test-images:centos-7 + +##### Package: centos + +PackageName: centos +SPDXID: SPDXRef-OperatingSystem-2e91c856c499a371 +PackageVersion: 7.6.1810 +FilesAnalyzed: false + +##### Package: bash + +PackageName: bash +SPDXID: SPDXRef-Package-5a18334f22149877 +PackageVersion: 4.2.46 +FilesAnalyzed: false +PackageSourceInfo: built package from: bash 4.2.46-31.el7 +PackageLicenseConcluded: GPLv3+ +PackageLicenseDeclared: GPLv3+ +ExternalRef: PACKAGE-MANAGER purl pkg:rpm/centos/bash@4.2.46-31.el7?arch=x86_64&distro=centos-7.6.1810 +PackageAttributionText: PkgID: bash@4.2.46-31.el7.x86_64 +PackageAttributionText: LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b +PackageAttributionText: LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a + +##### Package: openssl-libs + +PackageName: openssl-libs +SPDXID: SPDXRef-Package-e16b1cbaa5186199 +PackageVersion: 1.0.2k +FilesAnalyzed: false +PackageSourceInfo: built package from: openssl-libs 1:1.0.2k-16.el7 +PackageLicenseConcluded: OpenSSL +PackageLicenseDeclared: OpenSSL +ExternalRef: PACKAGE-MANAGER purl pkg:rpm/centos/openssl-libs@1.0.2k-16.el7?arch=x86_64&epoch=1&distro=centos-7.6.1810 +PackageAttributionText: PkgID: openssl-libs@1.0.2k-16.el7.x86_64 +PackageAttributionText: LayerDigest: sha256:ac9208207adaac3a48e54a4dc6b49c69e78c3072d2b3add7efdabf814db2133b +PackageAttributionText: LayerDiffID: sha256:89169d87dbe2b72ba42bfbb3579c957322baca28e03a1e558076542a1c1b2b4a + +##### Relationships + +Relationship: SPDXRef-DOCUMENT DESCRIBES SPDXRef-ContainerImage-dd5cad897c6263 +Relationship: SPDXRef-ContainerImage-dd5cad897c6263 CONTAINS SPDXRef-OperatingSystem-2e91c856c499a371 +Relationship: SPDXRef-OperatingSystem-2e91c856c499a371 DEPENDS_ON SPDXRef-Package-5a18334f22149877 +Relationship: SPDXRef-OperatingSystem-2e91c856c499a371 DEPENDS_ON SPDXRef-Package-e16b1cbaa5186199 \ No newline at end of file diff --git a/integration/testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json b/integration/testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json new file mode 100644 index 000000000000..f5db904207d1 --- /dev/null +++ b/integration/testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json @@ -0,0 +1,171 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:31ee662c-480e-4f63-9765-23ea8afc754d", + "version": 1, + "metadata": { + "timestamp": "2022-06-14T15:10:14+00:00", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "95de56ee-980c-413d-8f68-6c674dc3e9d1", + "type": "container", + "name": "integration/testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:5a992077baba51b97f27591a10d54d2f2723dc9c81a3fe419e261023f2554933" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:02874b2b269dea8dde0f7edb4c9906904dfe38a09de1a214f20c650cfb15c60e" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3752e1f6fd759c795c13aff2c93c081529366e27635ba6621e849b0f9cfc77f0" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:788c00e2cfc8f2a018ae4344ccf0b2c226ebd756d7effd1ce50eea1a4252cd89" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:25165eb51d15842f870f97873e0a58409d5e860e6108e3dd829bd10e484c0065" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "type": "library", + "name": "bash", + "version": "5.0-4", + "purl": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "bash" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.0-4" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2", + "type": "library", + "name": "libidn2-0", + "version": "2.0.5-1", + "purl": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "libidn2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.0.5-1" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + } + ] + }, + { + "bom-ref": "353f2470-9c8b-4647-9d0d-96d893838dc8", + "type": "operating-system", + "name": "debian", + "version": "10.2", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + }, + { + "bom-ref": "pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec", + "type": "library", + "name": "activesupport", + "version": "6.0.2.1", + "licenses": [ + { + "license": { + "name": "MIT" + } + } + ], + "purl": "pkg:gem/activesupport@6.0.2.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gemspec" + } + ] + } + ], + "dependencies": [ + { + "ref": "353f2470-9c8b-4647-9d0d-96d893838dc8", + "dependsOn": [ + "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2" + ] + }, + { + "ref": "95de56ee-980c-413d-8f68-6c674dc3e9d1", + "dependsOn": [ + "353f2470-9c8b-4647-9d0d-96d893838dc8", + "pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec" + ] + } + ] +} \ No newline at end of file diff --git a/integration/testdata/fixtures/sbom/license-cyclonedx.json b/integration/testdata/fixtures/sbom/license-cyclonedx.json new file mode 100644 index 000000000000..e8353ca609cc --- /dev/null +++ b/integration/testdata/fixtures/sbom/license-cyclonedx.json @@ -0,0 +1,125 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c09512e3-47e7-4eff-8f76-5d7ae72b26a5", + "version": 1, + "metadata": { + "timestamp": "2024-03-10T14:57:31+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "acc9d4aa-4158-4969-a497-637e114fde0c", + "type": "application", + "name": "C:/Users/bedla.czech/IdeaProjects/sbom-demo", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "eb56cd49-da98-4b08-bfc8-9880fb063cf1", + "type": "application", + "name": "pom.xml", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "pom" + } + ] + }, + { + "bom-ref": "pkg:maven/org.eclipse.sisu/org.eclipse.sisu.plexus@0.3.0.M1", + "type": "library", + "group": "org.eclipse.sisu", + "name": "org.eclipse.sisu.plexus", + "version": "0.3.0.M1", + "licenses": [ + { + "license": { + "name": "EPL-1.0" + } + } + ], + "purl": "pkg:maven/org.eclipse.sisu/org.eclipse.sisu.plexus@0.3.0.M1", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "org.eclipse.sisu:org.eclipse.sisu.plexus:0.3.0.M1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "pom" + } + ] + }, + { + "bom-ref": "pkg:maven/org.ow2.asm/asm@9.5", + "type": "library", + "group": "org.ow2.asm", + "name": "asm", + "version": "9.5", + "licenses": [ + { + "license": { + "name": "BSD-3-Clause" + } + } + ], + "purl": "pkg:maven/org.ow2.asm/asm@9.5", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "org.ow2.asm:asm:9.5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "pom" + } + ] + }, + { + "bom-ref": "pkg:maven/org.slf4j/slf4j-api@2.0.11", + "type": "library", + "group": "org.slf4j", + "name": "slf4j-api", + "version": "2.0.11", + "licenses": [ + { + "license": { + "name": "MIT License" + } + } + ], + "purl": "pkg:maven/org.slf4j/slf4j-api@2.0.11", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "org.slf4j:slf4j-api:2.0.11" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "pom" + } + ] + } + ], + "dependencies": [], + "vulnerabilities": [] +} diff --git a/integration/testdata/fixtures/sbom/minikube-kbom.json b/integration/testdata/fixtures/sbom/minikube-kbom.json new file mode 100644 index 000000000000..a6e9bc7550aa --- /dev/null +++ b/integration/testdata/fixtures/sbom/minikube-kbom.json @@ -0,0 +1,445 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:e2daaea6-d96f-4b84-960c-0d72c348cd23", + "version": 1, + "metadata": { + "timestamp": "2023-09-29T06:25:00+00:00", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "0.45.1-15-g7bbd0d097" + } + ], + "component": { + "bom-ref": "pkg:k8s/k8s.io%2Fkubernetes@1.27.0", + "type": "platform", + "name": "k8s.io/kubernetes", + "version": "1.27.0", + "purl": "pkg:k8s/k8s.io%2Fkubernetes@1.27.0", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "cluster" + } + ] + } + }, + "components": [ + { + "bom-ref": "5262e708-f1a3-4fca-a1c3-0a8384f7f4a5", + "type": "operating-system", + "name": "ubuntu", + "version": "22.04.2", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "ubuntu" + } + ] + }, + { + "bom-ref": "b6f66546-5a5c-4fe8-a30f-acb04013c151", + "type": "operating-system", + "name": "ubuntu", + "version": "22.04.2", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "ubuntu" + } + ] + }, + { + "bom-ref": "a62abb1f-cb38-4fde-90f3-2bda3b87ddb2", + "type": "application", + "name": "node-core-components" + }, + { + "bom-ref": "a6350ac3-52f6-4c5f-a3e3-184b9a634bef", + "type": "platform", + "name": "minikube", + "properties": [ + { + "name": "aquasecurity:trivy:Architecture", + "value": "arm64" + }, + { + "name": "aquasecurity:trivy:HostName", + "value": "minikube" + }, + { + "name": "aquasecurity:trivy:KernelVersion", + "value": "5.15.49-linuxkit-pr" + }, + { + "name": "aquasecurity:trivy:NodeRole", + "value": "master" + }, + { + "name": "aquasecurity:trivy:OperatingSystem", + "value": "linux" + }, + { + "name": "aquasecurity:trivy:resource:Name", + "value": "minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "node" + } + ] + }, + { + "bom-ref": "b19a88a3-017d-4e70-a73a-75f48696ec0f", + "type": "application", + "name": "kube-dns", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "coredns-5d78c9869d-nd92n" + } + ] + }, + { + "bom-ref": "b1c502c9-3c6e-43af-822b-1cb55c6c6ff3", + "type": "application", + "name": "go.etcd.io/etcd/v3", + "version": "3.5.7-0", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "etcd-minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "controlPlane" + } + ] + }, + { + "bom-ref": "pkg:golang/docker@24.0.4", + "type": "application", + "name": "docker", + "version": "24.0.4", + "purl": "pkg:golang/docker@24.0.4", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "docker" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "node" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fapiserver@1.27.0", + "type": "application", + "name": "k8s.io/apiserver", + "version": "1.27.0", + "purl": "pkg:k8s/k8s.io%2Fapiserver@1.27.0", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "kube-apiserver-minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "controlPlane" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.0", + "type": "application", + "name": "k8s.io/controller-manager", + "version": "1.27.0", + "purl": "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.0", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "kube-controller-manager-minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "controlPlane" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fkube-proxy@1.27.0", + "type": "application", + "name": "k8s.io/kube-proxy", + "version": "1.27.0", + "purl": "pkg:k8s/k8s.io%2Fkube-proxy@1.27.0", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "kube-proxy-4wftc" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "node" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.0", + "type": "application", + "name": "k8s.io/kube-scheduler", + "version": "1.27.0", + "purl": "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.0", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "kube-scheduler-minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "controlPlane" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fkubelet@1.27.0", + "type": "application", + "name": "k8s.io/kubelet", + "version": "1.27.0", + "purl": "pkg:k8s/k8s.io%2Fkubelet@1.27.0", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "k8s.io/kubelet" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "node" + } + ] + }, + { + "bom-ref": "pkg:oci/coredns@sha256%3Aa0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e?repository_url=registry.k8s.io%2Fcoredns%2Fcoredns", + "type": "container", + "name": "registry.k8s.io/coredns/coredns", + "version": "sha256:a0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e", + "purl": "pkg:oci/coredns@sha256%3Aa0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e?repository_url=registry.k8s.io%2Fcoredns%2Fcoredns", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/coredns/coredns:1.10.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/etcd@sha256%3A51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83?repository_url=registry.k8s.io%2Fetcd", + "type": "container", + "name": "registry.k8s.io/etcd", + "version": "sha256:51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83", + "purl": "pkg:oci/etcd@sha256%3A51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83?repository_url=registry.k8s.io%2Fetcd", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/etcd:3.5.7-0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/kube-apiserver@sha256%3A697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d?repository_url=registry.k8s.io%2Fkube-apiserver", + "type": "container", + "name": "registry.k8s.io/kube-apiserver", + "version": "sha256:697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d", + "purl": "pkg:oci/kube-apiserver@sha256%3A697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d?repository_url=registry.k8s.io%2Fkube-apiserver", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/kube-apiserver:1.27.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/kube-controller-manager@sha256%3A6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265?repository_url=registry.k8s.io%2Fkube-controller-manager", + "type": "container", + "name": "registry.k8s.io/kube-controller-manager", + "version": "sha256:6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265", + "purl": "pkg:oci/kube-controller-manager@sha256%3A6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265?repository_url=registry.k8s.io%2Fkube-controller-manager", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/kube-controller-manager:1.27.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/kube-proxy@sha256%3A4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf?repository_url=registry.k8s.io%2Fkube-proxy", + "type": "container", + "name": "registry.k8s.io/kube-proxy", + "version": "sha256:4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf", + "purl": "pkg:oci/kube-proxy@sha256%3A4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf?repository_url=registry.k8s.io%2Fkube-proxy", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/kube-proxy:1.27.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/kube-scheduler@sha256%3A5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af?repository_url=registry.k8s.io%2Fkube-scheduler", + "type": "container", + "name": "registry.k8s.io/kube-scheduler", + "version": "sha256:5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af", + "purl": "pkg:oci/kube-scheduler@sha256%3A5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af?repository_url=registry.k8s.io%2Fkube-scheduler", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/kube-scheduler:1.27.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + } + ], + "dependencies": [ + { + "ref": "5262e708-f1a3-4fca-a1c3-0a8384f7f4a5", + "dependsOn": [] + }, + { + "ref": "b6f66546-5a5c-4fe8-a30f-acb04013c151", + "dependsOn": [] + }, + { + "ref": "a62abb1f-cb38-4fde-90f3-2bda3b87ddb2", + "dependsOn": [ + "pkg:golang/docker@24.0.4", + "pkg:k8s/k8s.io%2Fkubelet@1.27.0" + ] + }, + { + "ref": "a6350ac3-52f6-4c5f-a3e3-184b9a634bef", + "dependsOn": [ + "5262e708-f1a3-4fca-a1c3-0a8384f7f4a5", + "b6f66546-5a5c-4fe8-a30f-acb04013c151", + "a62abb1f-cb38-4fde-90f3-2bda3b87ddb2" + ] + }, + { + "ref": "b19a88a3-017d-4e70-a73a-75f48696ec0f", + "dependsOn": [ + "pkg:oci/coredns@sha256%3Aa0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e?repository_url=registry.k8s.io%2Fcoredns%2Fcoredns" + ] + }, + { + "ref": "b1c502c9-3c6e-43af-822b-1cb55c6c6ff3", + "dependsOn": [ + "pkg:oci/etcd@sha256%3A51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83?repository_url=registry.k8s.io%2Fetcd" + ] + }, + { + "ref": "pkg:golang/docker@24.0.4", + "dependsOn": [] + }, + { + "ref": "pkg:k8s/k8s.io%2Fapiserver@1.27.0", + "dependsOn": [ + "pkg:oci/kube-apiserver@sha256%3A697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d?repository_url=registry.k8s.io%2Fkube-apiserver" + ] + }, + { + "ref": "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.0", + "dependsOn": [ + "pkg:oci/kube-controller-manager@sha256%3A6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265?repository_url=registry.k8s.io%2Fkube-controller-manager" + ] + }, + { + "ref": "pkg:k8s/k8s.io%2Fkube-proxy@1.27.0", + "dependsOn": [ + "pkg:oci/kube-proxy@sha256%3A4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf?repository_url=registry.k8s.io%2Fkube-proxy" + ] + }, + { + "ref": "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.0", + "dependsOn": [ + "pkg:oci/kube-scheduler@sha256%3A5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af?repository_url=registry.k8s.io%2Fkube-scheduler" + ] + }, + { + "ref": "pkg:k8s/k8s.io%2Fkubelet@1.27.0", + "dependsOn": [] + }, + { + "ref": "pkg:k8s/k8s.io%2Fkubernetes@1.27.0", + "dependsOn": [ + "a6350ac3-52f6-4c5f-a3e3-184b9a634bef", + "b19a88a3-017d-4e70-a73a-75f48696ec0f", + "b1c502c9-3c6e-43af-822b-1cb55c6c6ff3", + "pkg:k8s/k8s.io%2Fapiserver@1.27.0", + "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.0", + "pkg:k8s/k8s.io%2Fkube-proxy@1.27.0", + "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.0" + ] + }, + { + "ref": "pkg:oci/coredns@sha256%3Aa0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e?repository_url=registry.k8s.io%2Fcoredns%2Fcoredns", + "dependsOn": [] + }, + { + "ref": "pkg:oci/etcd@sha256%3A51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83?repository_url=registry.k8s.io%2Fetcd", + "dependsOn": [] + }, + { + "ref": "pkg:oci/kube-apiserver@sha256%3A697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d?repository_url=registry.k8s.io%2Fkube-apiserver", + "dependsOn": [] + }, + { + "ref": "pkg:oci/kube-controller-manager@sha256%3A6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265?repository_url=registry.k8s.io%2Fkube-controller-manager", + "dependsOn": [] + }, + { + "ref": "pkg:oci/kube-proxy@sha256%3A4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf?repository_url=registry.k8s.io%2Fkube-proxy", + "dependsOn": [] + }, + { + "ref": "pkg:oci/kube-scheduler@sha256%3A5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af?repository_url=registry.k8s.io%2Fkube-scheduler", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/integration/testdata/fixtures/vex/config/repository.yaml b/integration/testdata/fixtures/vex/config/repository.yaml new file mode 100644 index 000000000000..cd1865d9d4a5 --- /dev/null +++ b/integration/testdata/fixtures/vex/config/repository.yaml @@ -0,0 +1,4 @@ +repositories: + - name: default + url: https://localhost + enabled: true \ No newline at end of file diff --git a/integration/testdata/fixtures/vex/file/openvex.json b/integration/testdata/fixtures/vex/file/openvex.json new file mode 100644 index 000000000000..9782e1716387 --- /dev/null +++ b/integration/testdata/fixtures/vex/file/openvex.json @@ -0,0 +1,23 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "aquasecurity/trivy:613fd55abbc2857b5ca28b07a26f3cd4c8b0ddc4c8a97c57497a2d4c4880d7fc", + "author": "Aqua Security", + "timestamp": "2024-07-09T11:38:00.115697+04:00", + "version": 1, + "statements": [ + { + "vulnerability": { "@id": "CVE-2022-23628" }, + "products": [ + { + "@id": "pkg:golang/github.com/testdata/testdata", + "subcomponents": [ + { "@id": "pkg:golang/github.com/open-policy-agent/opa@v0.35.0" } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path", + "impact_statement": "The vulnerable code isn't called" + } + ] +} diff --git a/integration/testdata/fixtures/vex/repositories/default/0.1/index.json b/integration/testdata/fixtures/vex/repositories/default/0.1/index.json new file mode 100644 index 000000000000..12d7ee936e8f --- /dev/null +++ b/integration/testdata/fixtures/vex/repositories/default/0.1/index.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "packages": [ + { + "ID": "pkg:golang/github.com/testdata/testdata", + "Location": "./openvex.json" + } + ] +} diff --git a/integration/testdata/fixtures/vex/repositories/default/vex-repository.json b/integration/testdata/fixtures/vex/repositories/default/vex-repository.json new file mode 100644 index 000000000000..e064c0e1b3cf --- /dev/null +++ b/integration/testdata/fixtures/vex/repositories/default/vex-repository.json @@ -0,0 +1,15 @@ +{ + "name": "Test VEX Repository", + "description": "VEX Repository for Testing", + "versions": [ + { + "spec_version": "0.1", + "locations": [ + { + "url": "never used" + } + ], + "update_interval": "24h" + } + ] +} \ No newline at end of file diff --git a/integration/testdata/fluentd-gems.json.golden b/integration/testdata/fluentd-gems.json.golden new file mode 100644 index 000000000000..3cc5ebed9e28 --- /dev/null +++ b/integration/testdata/fluentd-gems.json.golden @@ -0,0 +1,295 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:7a550fb73ac2bf3e1fe50c96a8a5ba699be62cbb09ba9bd982557e574c904b3d", + "ArtifactName": "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 157340160, + "OS": { + "Family": "debian", + "Name": "10.2" + }, + "ImageID": "sha256:5a992077baba51b97f27591a10d54d2f2723dc9c81a3fe419e261023f2554933", + "DiffIDs": [ + "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f", + "sha256:02874b2b269dea8dde0f7edb4c9906904dfe38a09de1a214f20c650cfb15c60e", + "sha256:3752e1f6fd759c795c13aff2c93c081529366e27635ba6621e849b0f9cfc77f0", + "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9", + "sha256:788c00e2cfc8f2a018ae4344ccf0b2c226ebd756d7effd1ce50eea1a4252cd89", + "sha256:25165eb51d15842f870f97873e0a58409d5e860e6108e3dd829bd10e484c0065" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:fluentd-multiple-lockfiles" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:fluentd-multiple-lockfiles", + "ImageConfig": { + "architecture": "amd64", + "container": "232f3fc7ddffd71dc3ff52c6c0c3a5feea2f51acffd9b53850a8fc6f1a15319a", + "created": "2020-03-04T13:59:39.161374106Z", + "docker_version": "19.03.4", + "history": [ + { + "created": "2019-11-22T14:55:09.912242636Z", + "created_by": "/bin/sh -c #(nop) ADD file:bc8179c87c8dbb3d962bed1801f99e7c860ff03797cde6ad19b107d43b973ada in / " + }, + { + "created": "2019-11-22T14:55:10.253859615Z", + "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", + "empty_layer": true + }, + { + "created": "2020-03-04T13:58:17.973854594Z", + "created_by": "/bin/sh -c #(nop) ARG DEBIAN_FRONTEND=noninteractive", + "empty_layer": true + }, + { + "created": "2020-03-04T13:58:18.12120844Z", + "created_by": "/bin/sh -c #(nop) COPY file:4e7fdb1bc31a0f689d88f6af28d4f0352e89a2ac598c523e9637da3de75bfada in /tmp/install.sh " + }, + { + "created": "2020-03-04T13:58:18.26894021Z", + "created_by": "/bin/sh -c #(nop) COPY file:c03560fcb4f0aff4cecd93039c348ba4992564740c77e3d6049a44fe79ca44ab in /Gemfile " + }, + { + "created": "2020-03-04T13:59:37.96119583Z", + "created_by": "|1 DEBIAN_FRONTEND=noninteractive /bin/sh -c chmod +x /tmp/install.sh \u0026\u0026 /bin/bash -l -c /tmp/install.sh \u0026\u0026 rm /tmp/*" + }, + { + "created": "2020-03-04T13:59:38.583719926Z", + "created_by": "/bin/sh -c #(nop) COPY file:f742fdea941d5baccbf9a9c45ccc9cd943377f3c3e07da787a8d8d9f92a8b3d3 in /etc/fluent/fluent.conf " + }, + { + "created": "2020-03-04T13:59:38.72131564Z", + "created_by": "/bin/sh -c #(nop) COPY file:a9ce963551c165ec55bb4d982d96336caa97e8c70011eb4ca58927956bd08e2a in /run.sh " + }, + { + "created": "2020-03-04T13:59:38.844116271Z", + "created_by": "/bin/sh -c #(nop) EXPOSE 80", + "empty_layer": true + }, + { + "created": "2020-03-04T13:59:38.99446051Z", + "created_by": "/bin/sh -c #(nop) ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2", + "empty_layer": true + }, + { + "created": "2020-03-04T13:59:39.161374106Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/run.sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f", + "sha256:02874b2b269dea8dde0f7edb4c9906904dfe38a09de1a214f20c650cfb15c60e", + "sha256:3752e1f6fd759c795c13aff2c93c081529366e27635ba6621e849b0f9cfc77f0", + "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9", + "sha256:788c00e2cfc8f2a018ae4344ccf0b2c226ebd756d7effd1ce50eea1a4252cd89", + "sha256:25165eb51d15842f870f97873e0a58409d5e860e6108e3dd829bd10e484c0065" + ] + }, + "config": { + "Cmd": [ + "/run.sh" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2" + ], + "Image": "sha256:2a538358cddc4824e9eff1531e0c63ae5e3cda85d2984c647df9b1c816b9b86b", + "ExposedPorts": { + "80/tcp": {} + } + } + }, + "Layers": [ + { + "Size": 72479232, + "Digest": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c", + "DiffID": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "Size": 3584, + "Digest": "sha256:aa8e216680f366acbe83d055382ae12208c341300fefd996fbebae49c59a14a5", + "DiffID": "sha256:02874b2b269dea8dde0f7edb4c9906904dfe38a09de1a214f20c650cfb15c60e" + }, + { + "Size": 2048, + "Digest": "sha256:2b92c5265c693f3aa71c011dabcfca1da81b3e5aef0530c2192fe9237a0725ed", + "DiffID": "sha256:3752e1f6fd759c795c13aff2c93c081529366e27635ba6621e849b0f9cfc77f0" + }, + { + "Size": 84848640, + "Digest": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602", + "DiffID": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "Size": 3584, + "Digest": "sha256:8c669d8f22e42bdc70ab962cd36f36c44d5299a04fbf8de62654eaf1aa70a01a", + "DiffID": "sha256:788c00e2cfc8f2a018ae4344ccf0b2c226ebd756d7effd1ce50eea1a4252cd89" + }, + { + "Size": 3072, + "Digest": "sha256:de335f1b3d0014660d2c8617930e431e23ee369075e14597c3fcad6903a81255", + "DiffID": "sha256:25165eb51d15842f870f97873e0a58409d5e860e6108e3dd829bd10e484c0065" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz (debian 10.2)", + "Class": "os-pkgs", + "Type": "debian", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18224", + "VendorIDs": [ + "DSA-4613-1" + ], + "PkgID": "libidn2-0@2.0.5-1", + "PkgName": "libidn2-0", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libidn2-0@2.0.5-1?arch=amd64\u0026distro=debian-10.2", + "UID": "d0b48508c0c12b58" + }, + "InstalledVersion": "2.0.5-1", + "FixedVersion": "2.0.5-1+deb10u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c", + "DiffID": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18224", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:45d3cfb43101cb3984ae8671cc6104e42b47a8ec1fa9c320295df4c661c708cc", + "Title": "libidn2: heap-based buffer overflow in idn2_to_ascii_4i in lib/lookup.c", + "Description": "idn2_to_ascii_4i in lib/lookup.c in GNU libidn2 before 2.1.1 has a heap-based buffer overflow via a long domain string.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 4, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00008.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00009.html", + "https://access.redhat.com/security/cve/CVE-2019-18224", + "https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=12420", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18224", + "https://github.com/libidn/libidn2/commit/e4d1558aa2c1c04a05066ee8600f37603890ba8c", + "https://github.com/libidn/libidn2/compare/libidn2-2.1.0...libidn2-2.1.1", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/JDQVQ2XPV5BTZUFINT7AFJSKNNBVURNJ/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MINU5RKDFE6TKAFY5DRFN3WSFDS4DYVS/", + "https://seclists.org/bugtraq/2020/Feb/4", + "https://security.gentoo.org/glsa/202003-63", + "https://ubuntu.com/security/notices/USN-4168-1", + "https://usn.ubuntu.com/4168-1/", + "https://www.debian.org/security/2020/dsa-4613" + ], + "PublishedDate": "2019-10-21T17:15:00Z", + "LastModifiedDate": "2019-10-29T19:15:00Z" + } + ] + }, + { + "Target": "Ruby", + "Class": "lang-pkgs", + "Type": "gemspec", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-8165", + "PkgName": "activesupport", + "PkgPath": "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec", + "PkgIdentifier": { + "PURL": "pkg:gem/activesupport@6.0.2.1", + "UID": "f60a6fadd4200fe7" + }, + "InstalledVersion": "6.0.2.1", + "FixedVersion": "6.0.3.1, 5.2.4.3", + "Status": "fixed", + "Layer": { + "Digest": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602", + "DiffID": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-8165", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory RubyGems", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Arubygems" + }, + "Fingerprint": "sha256:0bf18943adff42d16e0d020edb1c408ccf61081e1cea6476005dd1018102d24a", + "Title": "rubygem-activesupport: potentially unintended unmarshalling of user-provided objects in MemCacheStore and RedisCacheStore", + "Description": "A deserialization of untrusted data vulnerability exists in rails \u003c 5.2.4.3, rails \u003c 6.0.3.1 that can allow an attacker to unmarshal user-provided objects in MemCacheStore and RedisCacheStore potentially resulting in an RCE.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 4, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 9.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00031.html", + "http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00034.html", + "https://access.redhat.com/security/cve/CVE-2020-8165", + "https://github.com/advisories/GHSA-2p68-f74v-9wc6", + "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/activesupport/CVE-2020-8165.yml", + "https://groups.google.com/forum/#!msg/rubyonrails-security/bv6fW4S0Y1c/KnkEqM7AAQAJ", + "https://groups.google.com/forum/#!topic/rubyonrails-security/bv6fW4S0Y1c", + "https://groups.google.com/g/rubyonrails-security/c/bv6fW4S0Y1c", + "https://hackerone.com/reports/413388", + "https://lists.debian.org/debian-lts-announce/2020/06/msg00022.html", + "https://lists.debian.org/debian-lts-announce/2020/07/msg00013.html", + "https://nvd.nist.gov/vuln/detail/CVE-2020-8165", + "https://weblog.rubyonrails.org/2020/5/18/Rails-5-2-4-3-and-6-0-3-1-have-been-released/", + "https://www.debian.org/security/2020/dsa-4766" + ], + "PublishedDate": "2020-06-19T18:15:00Z", + "LastModifiedDate": "2020-10-17T12:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/fluentd-multiple-lockfiles-short.cdx.json.golden b/integration/testdata/fluentd-multiple-lockfiles-short.cdx.json.golden new file mode 100644 index 000000000000..bef8a43cbef4 --- /dev/null +++ b/integration/testdata/fluentd-multiple-lockfiles-short.cdx.json.golden @@ -0,0 +1,521 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3ff14136-e09f-4df9-80ea-000000000006", + "version": 1, + "metadata": { + "timestamp": "2021-08-25T12:20:30+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "95de56ee-980c-413d-8f68-6c674dc3e9d1", + "type": "container", + "name": "integration/testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:02874b2b269dea8dde0f7edb4c9906904dfe38a09de1a214f20c650cfb15c60e" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:25165eb51d15842f870f97873e0a58409d5e860e6108e3dd829bd10e484c0065" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3752e1f6fd759c795c13aff2c93c081529366e27635ba6621e849b0f9cfc77f0" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:788c00e2cfc8f2a018ae4344ccf0b2c226ebd756d7effd1ce50eea1a4252cd89" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:5a992077baba51b97f27591a10d54d2f2723dc9c81a3fe419e261023f2554933" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "353f2470-9c8b-4647-9d0d-96d893838dc8", + "type": "operating-system", + "name": "debian", + "version": "10.2", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "type": "library", + "name": "bash", + "version": "5.0-4", + "purl": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "bash" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.0-4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2", + "type": "library", + "name": "libidn2-0", + "version": "2.0.5-1", + "purl": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libidn2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.0.5-1" + } + ] + }, + { + "bom-ref": "pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec", + "type": "library", + "name": "activesupport", + "version": "6.0.2.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/activesupport@6.0.2.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gemspec" + } + ] + } + ], + "dependencies": [ + { + "ref": "353f2470-9c8b-4647-9d0d-96d893838dc8", + "dependsOn": [ + "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2" + ] + }, + { + "ref": "95de56ee-980c-413d-8f68-6c674dc3e9d1", + "dependsOn": [ + "353f2470-9c8b-4647-9d0d-96d893838dc8", + "pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec" + ] + } + ], + "vulnerabilities": [ + { + "id": "CVE-2019-18224", + "source": { + "name": "debian", + "url": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "ratings": [ + { + "source": { + "name": "amazon" + }, + "severity": "medium" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv2", + "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + }, + { + "source": { + "name": "nvd" + }, + "score": 9.8, + "severity": "critical", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "redhat" + }, + "score": 5.6, + "severity": "medium", + "method": "CVSSv3", + "vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L" + }, + { + "source": { + "name": "ubuntu" + }, + "severity": "medium" + } + ], + "cwes": [ + 787 + ], + "description": "idn2_to_ascii_4i in lib/lookup.c in GNU libidn2 before 2.1.1 has a heap-based buffer overflow via a long domain string.", + "recommendation": "Upgrade libidn2-0 to version 2.0.5-1+deb10u1", + "advisories": [ + { + "url": "https://avd.aquasec.com/nvd/cve-2019-18224" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00008.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00009.html" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2019-18224" + }, + { + "url": "https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=12420" + }, + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18224" + }, + { + "url": "https://github.com/libidn/libidn2/commit/e4d1558aa2c1c04a05066ee8600f37603890ba8c" + }, + { + "url": "https://github.com/libidn/libidn2/compare/libidn2-2.1.0...libidn2-2.1.1" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/JDQVQ2XPV5BTZUFINT7AFJSKNNBVURNJ/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MINU5RKDFE6TKAFY5DRFN3WSFDS4DYVS/" + }, + { + "url": "https://seclists.org/bugtraq/2020/Feb/4" + }, + { + "url": "https://security.gentoo.org/glsa/202003-63" + }, + { + "url": "https://ubuntu.com/security/notices/USN-4168-1" + }, + { + "url": "https://usn.ubuntu.com/4168-1/" + }, + { + "url": "https://www.debian.org/security/2020/dsa-4613" + } + ], + "published": "2019-10-21T17:15:00+00:00", + "updated": "2019-10-29T19:15:00+00:00", + "affects": [ + { + "ref": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2", + "versions": [ + { + "version": "2.0.5-1", + "status": "affected" + } + ] + } + ] + }, + { + "id": "CVE-2019-18276", + "source": { + "name": "debian", + "url": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "ratings": [ + { + "source": { + "name": "cbl-mariner" + }, + "severity": "high" + }, + { + "source": { + "name": "debian" + }, + "severity": "low" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.2, + "severity": "high", + "method": "CVSSv2", + "vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.8, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "oracle-oval" + }, + "severity": "low" + }, + { + "source": { + "name": "photon" + }, + "severity": "high" + }, + { + "source": { + "name": "redhat" + }, + "score": 7.8, + "severity": "low", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "ubuntu" + }, + "severity": "low" + } + ], + "cwes": [ + 273 + ], + "description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "advisories": [ + { + "url": "https://avd.aquasec.com/nvd/cve-2019-18276" + }, + { + "url": "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2019-18276" + }, + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276" + }, + { + "url": "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff" + }, + { + "url": "https://linux.oracle.com/cve/CVE-2019-18276.html" + }, + { + "url": "https://linux.oracle.com/errata/ELSA-2021-1679.html" + }, + { + "url": "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E" + }, + { + "url": "https://nvd.nist.gov/vuln/detail/CVE-2019-18276" + }, + { + "url": "https://security.gentoo.org/glsa/202105-34" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20200430-0003/" + }, + { + "url": "https://www.youtube.com/watch?v=-wGtxJ8opa8" + } + ], + "published": "2019-11-28T01:15:00+00:00", + "updated": "2021-05-26T12:15:00+00:00", + "affects": [ + { + "ref": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "versions": [ + { + "version": "5.0-4", + "status": "affected" + } + ] + } + ] + }, + { + "id": "CVE-2020-8165", + "source": { + "name": "ghsa", + "url": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Arubygems" + }, + "ratings": [ + { + "source": { + "name": "ghsa" + }, + "severity": "high" + }, + { + "source": { + "name": "nvd" + }, + "score": 7.5, + "severity": "high", + "method": "CVSSv2", + "vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P" + }, + { + "source": { + "name": "nvd" + }, + "score": 9.8, + "severity": "critical", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "redhat" + }, + "score": 9.8, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "cwes": [ + 502 + ], + "description": "A deserialization of untrusted data vulnerability exists in rails < 5.2.4.3, rails < 6.0.3.1 that can allow an attacker to unmarshal user-provided objects in MemCacheStore and RedisCacheStore potentially resulting in an RCE.", + "recommendation": "Upgrade activesupport to version 6.0.3.1, 5.2.4.3", + "advisories": [ + { + "url": "https://avd.aquasec.com/nvd/cve-2020-8165" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00031.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00034.html" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2020-8165" + }, + { + "url": "https://github.com/advisories/GHSA-2p68-f74v-9wc6" + }, + { + "url": "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/activesupport/CVE-2020-8165.yml" + }, + { + "url": "https://groups.google.com/forum/#!msg/rubyonrails-security/bv6fW4S0Y1c/KnkEqM7AAQAJ" + }, + { + "url": "https://groups.google.com/forum/#!topic/rubyonrails-security/bv6fW4S0Y1c" + }, + { + "url": "https://groups.google.com/g/rubyonrails-security/c/bv6fW4S0Y1c" + }, + { + "url": "https://hackerone.com/reports/413388" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2020/06/msg00022.html" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2020/07/msg00013.html" + }, + { + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-8165" + }, + { + "url": "https://weblog.rubyonrails.org/2020/5/18/Rails-5-2-4-3-and-6-0-3-1-have-been-released/" + }, + { + "url": "https://www.debian.org/security/2020/dsa-4766" + } + ], + "published": "2020-06-19T18:15:00+00:00", + "updated": "2020-10-17T12:15:00+00:00", + "affects": [ + { + "ref": "pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec", + "versions": [ + { + "version": "6.0.2.1", + "status": "affected" + } + ] + } + ] + } + ] +} diff --git a/integration/testdata/fluentd-multiple-lockfiles.cdx.json.golden b/integration/testdata/fluentd-multiple-lockfiles.cdx.json.golden new file mode 100644 index 000000000000..eaf8a69f99f6 --- /dev/null +++ b/integration/testdata/fluentd-multiple-lockfiles.cdx.json.golden @@ -0,0 +1,9580 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3ff14136-e09f-4df9-80ea-000000000163", + "version": 1, + "metadata": { + "timestamp": "2021-08-25T12:20:30+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "3ff14136-e09f-4df9-80ea-000000000001", + "type": "container", + "name": "testdata/fixtures/images/fluentd-multiple-lockfiles.tar.gz", + "properties": [ + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:02874b2b269dea8dde0f7edb4c9906904dfe38a09de1a214f20c650cfb15c60e" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:25165eb51d15842f870f97873e0a58409d5e860e6108e3dd829bd10e484c0065" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3752e1f6fd759c795c13aff2c93c081529366e27635ba6621e849b0f9cfc77f0" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:788c00e2cfc8f2a018ae4344ccf0b2c226ebd756d7effd1ce50eea1a4252cd89" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:5a992077baba51b97f27591a10d54d2f2723dc9c81a3fe419e261023f2554933" + }, + { + "name": "aquasecurity:trivy:Reference", + "value": "ghcr.io/aquasecurity/trivy-test-images:fluentd-multiple-lockfiles" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "ghcr.io/aquasecurity/trivy-test-images:fluentd-multiple-lockfiles" + }, + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:Size", + "value": "157340160" + } + ] + } + }, + "components": [ + { + "bom-ref": "3ff14136-e09f-4df9-80ea-000000000002", + "type": "operating-system", + "name": "debian", + "version": "10.2", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "debian" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/adduser@3.118?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Adduser Developers " + }, + "name": "adduser", + "version": "3.118", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/adduser@3.118?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "adduser@3.118" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "adduser" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.118" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/apt@1.8.2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "APT Development Team " + }, + "name": "apt", + "version": "1.8.2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/apt@1.8.2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "apt@1.8.2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "apt" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.8.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-files@10.3%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "base-files", + "version": "10.3+deb10u2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/base-files@10.3%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-files@10.3+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-files" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "10.3+deb10u2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/base-passwd@3.5.46?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Colin Watson " + }, + "name": "base-passwd", + "version": "3.5.46", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "PD" + } + } + ], + "purl": "pkg:deb/debian/base-passwd@3.5.46?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "base-passwd@3.5.46" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "base-passwd" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.5.46" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/bash@5.0-4?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Matthias Klose " + }, + "name": "bash", + "version": "5.0-4", + "licenses": [ + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/bash@5.0-4?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "bash@5.0-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "bash" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/bsdutils@2.33.1-0.1?arch=amd64&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "LaMont Jones " + }, + "name": "bsdutils", + "version": "1:2.33.1-0.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/bsdutils@2.33.1-0.1?arch=amd64&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "bsdutils@1:2.33.1-0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.33.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ca-certificates@20190110?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Michael Shuler " + }, + "name": "ca-certificates", + "version": "20190110", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "MPL-2.0" + } + } + ], + "purl": "pkg:deb/debian/ca-certificates@20190110?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ca-certificates@20190110" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ca-certificates" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "20190110" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/coreutils@8.30-3?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Michael Stone " + }, + "name": "coreutils", + "version": "8.30-3", + "licenses": [ + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/coreutils@8.30-3?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "coreutils@8.30-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "coreutils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.30" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/dash@0.5.10.2-5?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Andrej Shadura " + }, + "name": "dash", + "version": "0.5.10.2-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/dash@0.5.10.2-5?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "dash@0.5.10.2-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "dash" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.5.10.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debconf Developers " + }, + "name": "debconf", + "version": "1.5.71", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + } + ], + "purl": "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "debconf@1.5.71" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "debconf" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.5.71" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/debian-archive-keyring@2019.1?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Release Team " + }, + "name": "debian-archive-keyring", + "version": "2019.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/debian-archive-keyring@2019.1?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "debian-archive-keyring@2019.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "debian-archive-keyring" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2019.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/debianutils@4.8.6.1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Clint Adams " + }, + "name": "debianutils", + "version": "4.8.6.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/debianutils@4.8.6.1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "debianutils@4.8.6.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "debianutils" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.8.6.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/diffutils@3.7-3?arch=amd64&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "Santiago Vila " + }, + "name": "diffutils", + "version": "1:3.7-3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-or-later" + } + } + ], + "purl": "pkg:deb/debian/diffutils@3.7-3?arch=amd64&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "diffutils@1:3.7-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "diffutils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.7" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/dpkg@1.19.7?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Dpkg Developers " + }, + "name": "dpkg", + "version": "1.19.7", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "public-domain-s-s-d" + } + }, + { + "license": { + "name": "public-domain-md5" + } + } + ], + "purl": "pkg:deb/debian/dpkg@1.19.7?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "dpkg@1.19.7" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "dpkg" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.19.7" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/e2fsprogs@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Theodore Y. Ts'o " + }, + "name": "e2fsprogs", + "version": "1.44.5-1+deb10u2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/e2fsprogs@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "e2fsprogs@1.44.5-1+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "e2fsprogs" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.44.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/fdisk@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "LaMont Jones " + }, + "name": "fdisk", + "version": "2.33.1-0.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/fdisk@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "fdisk@2.33.1-0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.33.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/findutils@4.6.0%2Bgit%2B20190209-2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Andreas Metzler " + }, + "name": "findutils", + "version": "4.6.0+git+20190209-2", + "licenses": [ + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + } + ], + "purl": "pkg:deb/debian/findutils@4.6.0%2Bgit%2B20190209-2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "findutils@4.6.0+git+20190209-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "findutils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.6.0+git+20190209" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/gcc-8-base@8.3.0-6?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "gcc-8-base", + "version": "8.3.0-6", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GFDL-1.2-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/gcc-8-base@8.3.0-6?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "gcc-8-base@8.3.0-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gcc-8" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.3.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/gpgv@2.2.12-1%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian GnuPG Maintainers " + }, + "name": "gpgv", + "version": "2.2.12-1+deb10u1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "permissive" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "name": "RFC-Reference" + } + }, + { + "license": { + "name": "TinySCHEME" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/gpgv@2.2.12-1%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "gpgv@2.2.12-1+deb10u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gnupg2" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1+deb10u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.2.12" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/grep@3.3-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Anibal Monsalve Salazar " + }, + "name": "grep", + "version": "3.3-1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/grep@3.3-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "grep@3.3-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "grep" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/gzip@1.9-3?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Bdale Garbee " + }, + "name": "gzip", + "version": "1.9-3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/gzip@1.9-3?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "gzip@1.9-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gzip" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.9" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/hostname@3.21?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Michael Meskes " + }, + "name": "hostname", + "version": "3.21", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/hostname@3.21?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "hostname@3.21" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "hostname" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.21" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/init-system-helpers@1.56%2Bnmu1?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian systemd Maintainers " + }, + "name": "init-system-helpers", + "version": "1.56+nmu1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/init-system-helpers@1.56%2Bnmu1?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "init-system-helpers@1.56+nmu1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "init-system-helpers" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.56+nmu1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libacl1@2.2.53-4?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libacl1", + "version": "2.2.53-4", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libacl1@2.2.53-4?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libacl1@2.2.53-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "acl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.2.53" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libapt-pkg5.0@1.8.2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "APT Development Team " + }, + "name": "libapt-pkg5.0", + "version": "1.8.2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libapt-pkg5.0@1.8.2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libapt-pkg5.0@1.8.2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "apt" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.8.2" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libattr1@2.4.48-4?arch=amd64&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "Guillem Jover " + }, + "name": "libattr1", + "version": "1:2.4.48-4", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libattr1@2.4.48-4?arch=amd64&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libattr1@1:2.4.48-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "attr" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.4.48" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libaudit-common@2.8.4-3?arch=all&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "Laurent Bigonville " + }, + "name": "libaudit-common", + "version": "1:2.8.4-3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/libaudit-common@2.8.4-3?arch=all&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libaudit-common@1:2.8.4-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "audit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.8.4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libaudit1@2.8.4-3?arch=amd64&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "Laurent Bigonville " + }, + "name": "libaudit1", + "version": "1:2.8.4-3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + } + ], + "purl": "pkg:deb/debian/libaudit1@2.8.4-3?arch=amd64&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libaudit1@1:2.8.4-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "audit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.8.4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libblkid1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "LaMont Jones " + }, + "name": "libblkid1", + "version": "2.33.1-0.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libblkid1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libblkid1@2.33.1-0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.33.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libbz2-1.0@1.0.6-9.2~deb10u1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Anibal Monsalve Salazar " + }, + "name": "libbz2-1.0", + "version": "1.0.6-9.2~deb10u1", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libbz2-1.0@1.0.6-9.2~deb10u1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libbz2-1.0@1.0.6-9.2~deb10u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "bzip2" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "9.2~deb10u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.0.6" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libc-bin@2.28-10?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "libc-bin", + "version": "2.28-10", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libc-bin@2.28-10?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libc-bin@2.28-10" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "glibc" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "10" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.28" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "libc6", + "version": "2.28-10", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libc6@2.28-10" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "glibc" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "10" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.28" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libcap-ng0@0.7.9-2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Pierre Chifflier " + }, + "name": "libcap-ng0", + "version": "0.7.9-2", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libcap-ng0@0.7.9-2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcap-ng0@0.7.9-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libcap-ng" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.7.9" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libcom-err2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Theodore Y. Ts'o " + }, + "name": "libcom-err2", + "version": "1.44.5-1+deb10u2", + "purl": "pkg:deb/debian/libcom-err2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcom-err2@1.44.5-1+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "e2fsprogs" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.44.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libdb5.3@5.3.28%2Bdfsg1-0.5?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Berkeley DB Team " + }, + "name": "libdb5.3", + "version": "5.3.28+dfsg1-0.5", + "purl": "pkg:deb/debian/libdb5.3@5.3.28%2Bdfsg1-0.5?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libdb5.3@5.3.28+dfsg1-0.5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "db5.3" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.3.28+dfsg1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libdebconfclient0@0.249?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Install System Team " + }, + "name": "libdebconfclient0", + "version": "0.249", + "purl": "pkg:deb/debian/libdebconfclient0@0.249?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libdebconfclient0@0.249" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "cdebconf" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.249" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libext2fs2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Theodore Y. Ts'o " + }, + "name": "libext2fs2", + "version": "1.44.5-1+deb10u2", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libext2fs2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libext2fs2@1.44.5-1+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "e2fsprogs" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.44.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libfdisk1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "LaMont Jones " + }, + "name": "libfdisk1", + "version": "2.33.1-0.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libfdisk1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libfdisk1@2.33.1-0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.33.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libffi6@3.2.1-9?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "libffi6", + "version": "3.2.1-9", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libffi6@3.2.1-9?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libffi6@3.2.1-9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libffi" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "9" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.2.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "libgcc1", + "version": "1:8.3.0-6", + "purl": "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgcc1@1:8.3.0-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gcc-8" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.3.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgcrypt20@1.8.4-5?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian GnuTLS Maintainers " + }, + "name": "libgcrypt20", + "version": "1.8.4-5", + "licenses": [ + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libgcrypt20@1.8.4-5?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgcrypt20@1.8.4-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libgcrypt20" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.8.4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgdbm-compat4@1.18.1-4?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Dmitry Bogatov " + }, + "name": "libgdbm-compat4", + "version": "1.18.1-4", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libgdbm-compat4@1.18.1-4?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgdbm-compat4@1.18.1-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gdbm" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.18.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgdbm6@1.18.1-4?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Dmitry Bogatov " + }, + "name": "libgdbm6", + "version": "1.18.1-4", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GFDL-1.3-no-invariants-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libgdbm6@1.18.1-4?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgdbm6@1.18.1-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gdbm" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.18.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgmp10@6.1.2%2Bdfsg-4?arch=amd64&distro=debian-10.2&epoch=2", + "type": "library", + "supplier": { + "name": "Debian Science Team " + }, + "name": "libgmp10", + "version": "2:6.1.2+dfsg-4", + "licenses": [ + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libgmp10@6.1.2%2Bdfsg-4?arch=amd64&distro=debian-10.2&epoch=2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgmp10@2:6.1.2+dfsg-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gmp" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.1.2+dfsg" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgnutls30@3.6.7-4?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian GnuTLS Maintainers " + }, + "name": "libgnutls30", + "version": "3.6.7-4", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "Apache-2.0" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libgnutls30@3.6.7-4?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgnutls30@3.6.7-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gnutls28" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.6.7" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libgpg-error0@1.35-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian GnuPG Maintainers " + }, + "name": "libgpg-error0", + "version": "1.35-1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "g10-permissive" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libgpg-error0@1.35-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libgpg-error0@1.35-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libgpg-error" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.35" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libhogweed4@3.4.1-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Magnus Holmgren " + }, + "name": "libhogweed4", + "version": "3.4.1-1", + "purl": "pkg:deb/debian/libhogweed4@3.4.1-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libhogweed4@3.4.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "nettle" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.4.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libidn2-0@2.0.5-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Libidn team " + }, + "name": "libidn2-0", + "version": "2.0.5-1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "Unicode" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libidn2-0@2.0.5-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libidn2-0@2.0.5-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libidn2" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.0.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libjemalloc2@5.1.0-3?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Faidon Liambotis " + }, + "name": "libjemalloc2", + "version": "5.1.0-3", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "name": "BSD-2-Clause-Chemeris" + } + }, + { + "license": { + "name": "BSD-3-Clause-Google" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "BSD-3-Clause-Hiroshima-University" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:deb/debian/libjemalloc2@5.1.0-3?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libjemalloc2@5.1.0-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "jemalloc" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.1.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/liblz4-1@1.8.3-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Nobuhiro Iwamatsu " + }, + "name": "liblz4-1", + "version": "1.8.3-1", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/liblz4-1@1.8.3-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "liblz4-1@1.8.3-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "lz4" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.8.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/liblzma5@5.2.4-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Jonathan Nieder " + }, + "name": "liblzma5", + "version": "5.2.4-1", + "licenses": [ + { + "license": { + "name": "PD" + } + }, + { + "license": { + "name": "probably-PD" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "name": "permissive-fsf" + } + }, + { + "license": { + "name": "Autoconf" + } + }, + { + "license": { + "name": "permissive-nowarranty" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "none" + } + }, + { + "license": { + "name": "config-h" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "name": "noderivs" + } + }, + { + "license": { + "name": "PD-debian" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/liblzma5@5.2.4-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "liblzma5@5.2.4-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "xz-utils" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.2.4" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libmount1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "LaMont Jones " + }, + "name": "libmount1", + "version": "2.33.1-0.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libmount1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libmount1@2.33.1-0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.33.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libncurses6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Craig Small " + }, + "name": "libncurses6", + "version": "6.1+20181013-2+deb10u2", + "purl": "pkg:deb/debian/libncurses6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libncurses6@6.1+20181013-2+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.1+20181013" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libncursesw6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Craig Small " + }, + "name": "libncursesw6", + "version": "6.1+20181013-2+deb10u2", + "purl": "pkg:deb/debian/libncursesw6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libncursesw6@6.1+20181013-2+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.1+20181013" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libnettle6@3.4.1-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Magnus Holmgren " + }, + "name": "libnettle6", + "version": "3.4.1-1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "name": "other" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "GPL-2.0-only WITH autoconf-exception+" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "GAP" + } + } + ], + "purl": "pkg:deb/debian/libnettle6@3.4.1-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libnettle6@3.4.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "nettle" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.4.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libp11-kit0@0.23.15-2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian GnuTLS Maintainers " + }, + "name": "libp11-kit0", + "version": "0.23.15-2", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "permissive-like-automake-output" + } + }, + { + "license": { + "id": "ISC" + } + }, + { + "license": { + "name": "ISC-IBM" + } + }, + { + "license": { + "name": "same-as-rest-of-p11kit" + } + } + ], + "purl": "pkg:deb/debian/libp11-kit0@0.23.15-2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libp11-kit0@0.23.15-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "p11-kit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.23.15" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-modules-bin@1.3.1-5?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Steve Langasek " + }, + "name": "libpam-modules-bin", + "version": "1.3.1-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libpam-modules-bin@1.3.1-5?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam-modules-bin@1.3.1-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-modules@1.3.1-5?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Steve Langasek " + }, + "name": "libpam-modules", + "version": "1.3.1-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libpam-modules@1.3.1-5?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam-modules@1.3.1-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam-runtime@1.3.1-5?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Steve Langasek " + }, + "name": "libpam-runtime", + "version": "1.3.1-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libpam-runtime@1.3.1-5?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam-runtime@1.3.1-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpam0g@1.3.1-5?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Steve Langasek " + }, + "name": "libpam0g", + "version": "1.3.1-5", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libpam0g@1.3.1-5?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpam0g@1.3.1-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pam" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libpcre3@8.39-12?arch=amd64&distro=debian-10.2&epoch=2", + "type": "library", + "supplier": { + "name": "Matthew Vernon " + }, + "name": "libpcre3", + "version": "2:8.39-12", + "purl": "pkg:deb/debian/libpcre3@8.39-12?arch=amd64&distro=debian-10.2&epoch=2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libpcre3@2:8.39-12" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "pcre3" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "12" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.39" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libreadline7@7.0-5?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Matthias Klose " + }, + "name": "libreadline7", + "version": "7.0-5", + "licenses": [ + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-or-later" + } + } + ], + "purl": "pkg:deb/debian/libreadline7@7.0-5?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libreadline7@7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "readline" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libruby2.5@2.5.5-3%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Team " + }, + "name": "libruby2.5", + "version": "2.5.5-3+deb10u1", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "Ruby" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "SIL-1.1" + } + }, + { + "license": { + "name": "CC-BY-3.0-famfamfam" + } + }, + { + "license": { + "name": "PreserveNotice" + } + }, + { + "license": { + "name": "3C-BSD" + } + }, + { + "license": { + "name": "PublicDomain" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "AllPermissions" + } + }, + { + "license": { + "name": "PartialGplArtisticAndRuby" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "zlib-acknowledgement" + } + }, + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "name": "Unicode" + } + }, + { + "license": { + "name": "Permissive" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libruby2.5@2.5.5-3%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libruby2.5@2.5.5-3+deb10u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ruby2.5" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3+deb10u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.5.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libseccomp2@2.3.3-4?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Kees Cook " + }, + "name": "libseccomp2", + "version": "2.3.3-4", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libseccomp2@2.3.3-4?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libseccomp2@2.3.3-4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libseccomp" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "4" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.3.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libselinux1@2.8-1%2Bb1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libselinux1", + "version": "2.8-1+b1", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/libselinux1@2.8-1%2Bb1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libselinux1@2.8-1+b1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libselinux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.8" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsemanage-common@2.8-2?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libsemanage-common", + "version": "2.8-2", + "licenses": [ + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libsemanage-common@2.8-2?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsemanage-common@2.8-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libsemanage" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.8" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsemanage1@2.8-2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libsemanage1", + "version": "2.8-2", + "licenses": [ + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libsemanage1@2.8-2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsemanage1@2.8-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libsemanage" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.8" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsepol1@2.8-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian SELinux maintainers " + }, + "name": "libsepol1", + "version": "2.8-1", + "licenses": [ + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/libsepol1@2.8-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsepol1@2.8-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libsepol" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.8" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsmartcols1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "LaMont Jones " + }, + "name": "libsmartcols1", + "version": "2.33.1-0.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libsmartcols1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsmartcols1@2.33.1-0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.33.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libss2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Theodore Y. Ts'o " + }, + "name": "libss2", + "version": "1.44.5-1+deb10u2", + "purl": "pkg:deb/debian/libss2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libss2@1.44.5-1+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "e2fsprogs" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.44.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libssl1.1@1.1.1d-0%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian OpenSSL Team " + }, + "name": "libssl1.1", + "version": "1.1.1d-0+deb10u2", + "purl": "pkg:deb/debian/libssl1.1@1.1.1d-0%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libssl1.1@1.1.1d-0+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.1.1d" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libstdc%2B%2B6@8.3.0-6?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian GCC Maintainers " + }, + "name": "libstdc++6", + "version": "8.3.0-6", + "purl": "pkg:deb/debian/libstdc%2B%2B6@8.3.0-6?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libstdc++6@8.3.0-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "gcc-8" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "8.3.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libsystemd0@241-7~deb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian systemd Maintainers " + }, + "name": "libsystemd0", + "version": "241-7~deb10u2", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libsystemd0@241-7~deb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libsystemd0@241-7~deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "systemd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "7~deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "241" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libtasn1-6@4.13-3?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian GnuTLS Maintainers " + }, + "name": "libtasn1-6", + "version": "4.13-3", + "licenses": [ + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-only" + } + } + ], + "purl": "pkg:deb/debian/libtasn1-6@4.13-3?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libtasn1-6@4.13-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libtasn1-6" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.13" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libtinfo6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Craig Small " + }, + "name": "libtinfo6", + "version": "6.1+20181013-2+deb10u2", + "purl": "pkg:deb/debian/libtinfo6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libtinfo6@6.1+20181013-2+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.1+20181013" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libudev1@241-7~deb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian systemd Maintainers " + }, + "name": "libudev1", + "version": "241-7~deb10u2", + "licenses": [ + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/libudev1@241-7~deb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libudev1@241-7~deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "systemd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "7~deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "241" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libunistring2@0.9.10-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Jörg Frings-Fürst " + }, + "name": "libunistring2", + "version": "0.9.10-1", + "licenses": [ + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "name": "FreeSoftware" + } + }, + { + "license": { + "name": "GPL-2.0-or-later WITH distribution-exception" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "GFDL-1.2-or-later" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "GFDL-1.2-only" + } + } + ], + "purl": "pkg:deb/debian/libunistring2@0.9.10-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libunistring2@0.9.10-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libunistring" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.9.10" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libuuid1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "LaMont Jones " + }, + "name": "libuuid1", + "version": "2.33.1-0.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/libuuid1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libuuid1@2.33.1-0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.33.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libyaml-0-2@0.2.1-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Anders Kaseorg " + }, + "name": "libyaml-0-2", + "version": "0.2.1-1", + "licenses": [ + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "name": "permissive" + } + } + ], + "purl": "pkg:deb/debian/libyaml-0-2@0.2.1-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libyaml-0-2@0.2.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libyaml" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.2.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/libzstd1@1.3.8%2Bdfsg-3?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Med Packaging Team " + }, + "name": "libzstd1", + "version": "1.3.8+dfsg-3", + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "Zlib" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:deb/debian/libzstd1@1.3.8%2Bdfsg-3?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "libzstd1@1.3.8+dfsg-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libzstd" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.8+dfsg" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/login@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "Shadow package maintainers " + }, + "name": "login", + "version": "1:4.5-1.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/login@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "login@1:4.5-1.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "shadow" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/mawk@1.3.3-17%2Bb3?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Steve Langasek " + }, + "name": "mawk", + "version": "1.3.3-17+b3", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/mawk@1.3.3-17%2Bb3?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "mawk@1.3.3-17+b3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "mawk" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "17" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.3.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/mount@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "LaMont Jones " + }, + "name": "mount", + "version": "2.33.1-0.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/mount@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "mount@2.33.1-0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.33.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ncurses-base@6.1%2B20181013-2%2Bdeb10u2?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Craig Small " + }, + "name": "ncurses-base", + "version": "6.1+20181013-2+deb10u2", + "purl": "pkg:deb/debian/ncurses-base@6.1%2B20181013-2%2Bdeb10u2?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ncurses-base@6.1+20181013-2+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.1+20181013" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ncurses-bin@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Craig Small " + }, + "name": "ncurses-bin", + "version": "6.1+20181013-2+deb10u2", + "purl": "pkg:deb/debian/ncurses-bin@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ncurses-bin@6.1+20181013-2+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ncurses" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "6.1+20181013" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/openssl@1.1.1d-0%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian OpenSSL Team " + }, + "name": "openssl", + "version": "1.1.1d-0+deb10u2", + "purl": "pkg:deb/debian/openssl@1.1.1d-0%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "openssl@1.1.1d-0+deb10u2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "openssl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb10u2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.1.1d" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/passwd@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "Shadow package maintainers " + }, + "name": "passwd", + "version": "1:4.5-1.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/passwd@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "passwd@1:4.5-1.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "shadow" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/perl-base@5.28.1-6?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Niko Tyni " + }, + "name": "perl-base", + "version": "5.28.1-6", + "purl": "pkg:deb/debian/perl-base@5.28.1-6?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "perl-base@5.28.1-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "perl" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.28.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/rake@12.3.1-3?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Extras Maintainers " + }, + "name": "rake", + "version": "12.3.1-3", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:deb/debian/rake@12.3.1-3?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "rake@12.3.1-3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "rake" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "12.3.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/readline-common@7.0-5?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Matthias Klose " + }, + "name": "readline-common", + "version": "7.0-5", + "licenses": [ + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GFDL-1.3-or-later" + } + } + ], + "purl": "pkg:deb/debian/readline-common@7.0-5?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "readline-common@7.0-5" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "readline" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "5" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "7.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ruby-did-you-mean@1.2.1-1?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Extras Maintainers " + }, + "name": "ruby-did-you-mean", + "version": "1.2.1-1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:deb/debian/ruby-did-you-mean@1.2.1-1?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ruby-did-you-mean@1.2.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ruby-did-you-mean" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.2.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ruby-minitest@5.11.3-1?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Extras Maintainers " + }, + "name": "ruby-minitest", + "version": "5.11.3-1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:deb/debian/ruby-minitest@5.11.3-1?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ruby-minitest@5.11.3-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ruby-minitest" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "5.11.3" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ruby-net-telnet@0.1.1-2?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Extras Maintainers " + }, + "name": "ruby-net-telnet", + "version": "0.1.1-2", + "licenses": [ + { + "license": { + "id": "Ruby" + } + } + ], + "purl": "pkg:deb/debian/ruby-net-telnet@0.1.1-2?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ruby-net-telnet@0.1.1-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ruby-net-telnet" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.1.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ruby-power-assert@1.1.1-1?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Extras Maintainers " + }, + "name": "ruby-power-assert", + "version": "1.1.1-1", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "Ruby" + } + } + ], + "purl": "pkg:deb/debian/ruby-power-assert@1.1.1-1?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ruby-power-assert@1.1.1-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ruby-power-assert" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.1.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ruby-test-unit@3.2.8-1?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Extras Maintainers " + }, + "name": "ruby-test-unit", + "version": "3.2.8-1", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "Ruby" + } + }, + { + "license": { + "name": "PSF" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + } + ], + "purl": "pkg:deb/debian/ruby-test-unit@3.2.8-1?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ruby-test-unit@3.2.8-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ruby-test-unit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "3.2.8" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ruby-xmlrpc@0.3.0-2?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Extras Maintainers " + }, + "name": "ruby-xmlrpc", + "version": "0.3.0-2", + "licenses": [ + { + "license": { + "id": "Ruby" + } + } + ], + "purl": "pkg:deb/debian/ruby-xmlrpc@0.3.0-2?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ruby-xmlrpc@0.3.0-2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ruby-xmlrpc" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "2" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "0.3.0" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ruby2.5@2.5.5-3%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Team " + }, + "name": "ruby2.5", + "version": "2.5.5-3+deb10u1", + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "Ruby" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "name": "SIL-1.1" + } + }, + { + "license": { + "name": "CC-BY-3.0-famfamfam" + } + }, + { + "license": { + "name": "PreserveNotice" + } + }, + { + "license": { + "name": "3C-BSD" + } + }, + { + "license": { + "name": "PublicDomain" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "name": "AllPermissions" + } + }, + { + "license": { + "name": "PartialGplArtisticAndRuby" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "id": "Artistic-2.0" + } + }, + { + "license": { + "id": "zlib-acknowledgement" + } + }, + { + "license": { + "id": "GPL-1.0-or-later" + } + }, + { + "license": { + "id": "CC0-1.0" + } + }, + { + "license": { + "name": "Unicode" + } + }, + { + "license": { + "name": "Permissive" + } + }, + { + "license": { + "id": "GPL-1.0-only" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/ruby2.5@2.5.5-3%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ruby2.5@2.5.5-3+deb10u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ruby2.5" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "3+deb10u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.5.5" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/ruby@2.5.1?arch=amd64&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "Antonio Terceiro " + }, + "name": "ruby", + "version": "1:2.5.1", + "licenses": [ + { + "license": { + "id": "Ruby" + } + }, + { + "license": { + "id": "GPL-2.0-or-later" + } + } + ], + "purl": "pkg:deb/debian/ruby@2.5.1?arch=amd64&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "ruby@1:2.5.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "ruby-defaults" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.5.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/rubygems-integration@1.11?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian Ruby Extras Maintainers " + }, + "name": "rubygems-integration", + "version": "1.11", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:deb/debian/rubygems-integration@1.11?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "rubygems-integration@1.11" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "rubygems-integration" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.11" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/sed@4.7-1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Clint Adams " + }, + "name": "sed", + "version": "4.7-1", + "licenses": [ + { + "license": { + "id": "GPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/sed@4.7-1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "sed@4.7-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "sed" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.7" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/sysvinit-utils@2.93-8?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Debian sysvinit maintainers " + }, + "name": "sysvinit-utils", + "version": "2.93-8", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/sysvinit-utils@2.93-8?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "sysvinit-utils@2.93-8" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "sysvinit" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "8" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.93" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tar@1.30%2Bdfsg-6?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "Bdale Garbee " + }, + "name": "tar", + "version": "1.30+dfsg-6", + "licenses": [ + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + } + ], + "purl": "pkg:deb/debian/tar@1.30%2Bdfsg-6?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tar@1.30+dfsg-6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tar" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "6" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.30+dfsg" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/tzdata@2019c-0%2Bdeb10u1?arch=all&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "GNU Libc Maintainers " + }, + "name": "tzdata", + "version": "2019c-0+deb10u1", + "purl": "pkg:deb/debian/tzdata@2019c-0%2Bdeb10u1?arch=all&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "tzdata@2019c-0+deb10u1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "tzdata" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0+deb10u1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2019c" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/util-linux@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "type": "library", + "supplier": { + "name": "LaMont Jones " + }, + "name": "util-linux", + "version": "2.33.1-0.1", + "licenses": [ + { + "license": { + "id": "GPL-2.0-or-later" + } + }, + { + "license": { + "id": "GPL-2.0-only" + } + }, + { + "license": { + "name": "public-domain" + } + }, + { + "license": { + "id": "BSD-4-Clause" + } + }, + { + "license": { + "id": "MIT" + } + }, + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "LGPL-2.0-or-later" + } + }, + { + "license": { + "id": "LGPL-2.1-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-or-later" + } + }, + { + "license": { + "id": "LGPL-3.0-or-later" + } + }, + { + "license": { + "id": "GPL-3.0-only" + } + }, + { + "license": { + "id": "LGPL-2.0-only" + } + }, + { + "license": { + "id": "LGPL-2.1-only" + } + }, + { + "license": { + "id": "LGPL-3.0-only" + } + } + ], + "purl": "pkg:deb/debian/util-linux@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "util-linux@2.33.1-0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "util-linux" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0.1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.33.1" + } + ] + }, + { + "bom-ref": "pkg:deb/debian/zlib1g@1.2.11.dfsg-1?arch=amd64&distro=debian-10.2&epoch=1", + "type": "library", + "supplier": { + "name": "Mark Brown " + }, + "name": "zlib1g", + "version": "1:1.2.11.dfsg-1", + "licenses": [ + { + "license": { + "id": "Zlib" + } + } + ], + "purl": "pkg:deb/debian/zlib1g@1.2.11.dfsg-1?arch=amd64&distro=debian-10.2&epoch=1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c" + }, + { + "name": "aquasecurity:trivy:PkgID", + "value": "zlib1g@1:1.2.11.dfsg-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "debian" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "zlib" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.2.11.dfsg" + } + ] + }, + { + "bom-ref": "pkg:gem/activesupport@6.0.2.1", + "type": "library", + "name": "activesupport", + "version": "6.0.2.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "a2cd09dcbaf8ca1951fb8e3f2ebdfe6728ab44f7" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/activesupport@6.0.2.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/addressable@2.7.0", + "type": "library", + "name": "addressable", + "version": "2.7.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "b4596fdeffcb1c89b24623b6f775a6b054a8323f" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/addressable@2.7.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/addressable-2.7.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/concurrent-ruby@1.1.6", + "type": "library", + "name": "concurrent-ruby", + "version": "1.1.6", + "hashes": [ + { + "alg": "SHA-1", + "content": "c96749b0390ad63300b13dca6fd83e5508facf18" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/concurrent-ruby@1.1.6", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/concurrent-ruby-1.1.6.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/cool.io@1.6.0", + "type": "library", + "name": "cool.io", + "version": "1.6.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "706a2490b54301e8ae8f2ca8f9f56b279b96ac7b" + } + ], + "purl": "pkg:gem/cool.io@1.6.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/cool.io-1.6.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/dig_rb@1.0.1", + "type": "library", + "name": "dig_rb", + "version": "1.0.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "04a4a555fe3a7e253098e870cf8a6c8746828829" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/dig_rb@1.0.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/dig_rb-1.0.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/domain_name@0.5.20190701", + "type": "library", + "name": "domain_name", + "version": "0.5.20190701", + "hashes": [ + { + "alg": "SHA-1", + "content": "e45a352deedbf1d48c2563caa583d0864d6ac62b" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + }, + { + "license": { + "id": "BSD-3-Clause" + } + }, + { + "license": { + "id": "MPL-2.0" + } + } + ], + "purl": "pkg:gem/domain_name@0.5.20190701", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/domain_name-0.5.20190701.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/elasticsearch-api@7.5.0", + "type": "library", + "name": "elasticsearch-api", + "version": "7.5.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "aac794d1d845525dc57d73d8bd5bda4b7f593ea4" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/elasticsearch-api@7.5.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/elasticsearch-api-7.5.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/elasticsearch-transport@7.5.0", + "type": "library", + "name": "elasticsearch-transport", + "version": "7.5.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "29ab0a306cfc109b82ac19c37f288956a4d6d1d9" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/elasticsearch-transport@7.5.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/elasticsearch-transport-7.5.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/elasticsearch@7.5.0", + "type": "library", + "name": "elasticsearch", + "version": "7.5.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "f3996e145e83f80d27ed48f8d2dca84f02c696c3" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/elasticsearch@7.5.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/elasticsearch-7.5.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/excon@0.72.0", + "type": "library", + "name": "excon", + "version": "0.72.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "8b5c81a189d2748ae488dff8a7b4876493b86f76" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/excon@0.72.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/excon-0.72.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/faraday@0.17.3", + "type": "library", + "name": "faraday", + "version": "0.17.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "b8c741fbdc2d729a59e2e855037421040673ca45" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/faraday@0.17.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/faraday-0.17.3.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/ffi-compiler@1.0.1", + "type": "library", + "name": "ffi-compiler", + "version": "1.0.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "b9ffee214ef79e695c14c8703566f7c13be4c2ba" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/ffi-compiler@1.0.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/ffi-compiler-1.0.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/ffi@1.12.2", + "type": "library", + "name": "ffi", + "version": "1.12.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "e6345da46b7a923b2248bc76d074362e7491376b" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-3-Clause" + } + } + ], + "purl": "pkg:gem/ffi@1.12.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/ffi-1.12.2.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/fluent-plugin-concat@2.4.0", + "type": "library", + "name": "fluent-plugin-concat", + "version": "2.4.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "b6a0da88821e50d052cb244b57562f00abe79888" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/fluent-plugin-concat@2.4.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/fluent-plugin-concat-2.4.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/fluent-plugin-detect-exceptions@0.0.13", + "type": "library", + "name": "fluent-plugin-detect-exceptions", + "version": "0.0.13", + "hashes": [ + { + "alg": "SHA-1", + "content": "d1a7b50f7723ead908453f463e24424735be0a56" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/fluent-plugin-detect-exceptions@0.0.13", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/fluent-plugin-detect-exceptions-0.0.13.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/fluent-plugin-elasticsearch@3.8.0", + "type": "library", + "name": "fluent-plugin-elasticsearch", + "version": "3.8.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "d60372f3af2757abd0a4ff68484e9328b2cbe386" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/fluent-plugin-elasticsearch@3.8.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/fluent-plugin-elasticsearch-3.8.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/fluent-plugin-kubernetes_metadata_filter@2.4.1", + "type": "library", + "name": "fluent-plugin-kubernetes_metadata_filter", + "version": "2.4.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "eeb3ce046c69c9c83ed1d4bc949058ef6a124f96" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/fluent-plugin-kubernetes_metadata_filter@2.4.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/fluent-plugin-kubernetes_metadata_filter-2.4.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/fluent-plugin-multi-format-parser@1.0.0", + "type": "library", + "name": "fluent-plugin-multi-format-parser", + "version": "1.0.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "9aa245fc07627474621e29f04507a377dfae09d3" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/fluent-plugin-multi-format-parser@1.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/fluent-plugin-multi-format-parser-1.0.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/fluent-plugin-prometheus@1.7.0", + "type": "library", + "name": "fluent-plugin-prometheus", + "version": "1.7.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "597a311791f0d05968c558e8015c8bed864137e2" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/fluent-plugin-prometheus@1.7.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/fluent-plugin-prometheus-1.7.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/fluent-plugin-systemd@1.0.2", + "type": "library", + "name": "fluent-plugin-systemd", + "version": "1.0.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "115901208913bc5031597a20fae33c50c6de6500" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/fluent-plugin-systemd@1.0.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/fluent-plugin-systemd-1.0.2.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/fluentd@1.8.0", + "type": "library", + "name": "fluentd", + "version": "1.8.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "5f31ca316e345410e5a5b70b5fafed8a51fd5092" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/fluentd@1.8.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/fluentd-1.8.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/http-accept@1.7.0", + "type": "library", + "name": "http-accept", + "version": "1.7.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "21dedf2ba79a24f86528c2dfc32d17dd9324d9fd" + } + ], + "purl": "pkg:gem/http-accept@1.7.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/http-accept-1.7.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/http-cookie@1.0.3", + "type": "library", + "name": "http-cookie", + "version": "1.0.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "e6f5b8e237e694b3729797cca134525822769964" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/http-cookie@1.0.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/http-cookie-1.0.3.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/http-form_data@2.2.0", + "type": "library", + "name": "http-form_data", + "version": "2.2.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "53c844c1f954a9c43b78b8e57f18c0ec965beb1f" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/http-form_data@2.2.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/http-form_data-2.2.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/http-parser@1.2.1", + "type": "library", + "name": "http-parser", + "version": "1.2.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "7c6889d98755a1fe8859d850892412a0e001ba9a" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/http-parser@1.2.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/http-parser-1.2.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/http@4.3.0", + "type": "library", + "name": "http", + "version": "4.3.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "79032e0328aa1d3ee184a38c50cd93d5bce8998b" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/http@4.3.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/http-4.3.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/http_parser.rb@0.6.0", + "type": "library", + "name": "http_parser.rb", + "version": "0.6.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "0ed80e936eaf7151f540186333c3df436afd46c6" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/http_parser.rb@0.6.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/http_parser.rb-0.6.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/i18n@1.8.2", + "type": "library", + "name": "i18n", + "version": "1.8.2", + "hashes": [ + { + "alg": "SHA-1", + "content": "d13bccc2521cef33fc4303888b24f327a7369877" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/i18n@1.8.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/i18n-1.8.2.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/kubeclient@4.6.0", + "type": "library", + "name": "kubeclient", + "version": "4.6.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "31916cb42ac6b30c68a5422414946d0328be94d4" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/kubeclient@4.6.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/kubeclient-4.6.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/lru_redux@1.1.0", + "type": "library", + "name": "lru_redux", + "version": "1.1.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "c8c5874f406a8fefc655fee48b75dfa276a5b8fa" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/lru_redux@1.1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/lru_redux-1.1.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/mime-types-data@3.2019.1009", + "type": "library", + "name": "mime-types-data", + "version": "3.2019.1009", + "hashes": [ + { + "alg": "SHA-1", + "content": "8a80ce9bf4961df0184d25699612d29293a05aee" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/mime-types-data@3.2019.1009", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/mime-types-data-3.2019.1009.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/mime-types@3.3.1", + "type": "library", + "name": "mime-types", + "version": "3.3.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "b70aa1555acff548ee282c76ddd562e831483187" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/mime-types@3.3.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/mime-types-3.3.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/minitest@5.14.0", + "type": "library", + "name": "minitest", + "version": "5.14.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "8fcf25c201eacdf1a0e4db78efcb37ad590f33c0" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/minitest@5.14.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/minitest-5.14.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/msgpack@1.3.3", + "type": "library", + "name": "msgpack", + "version": "1.3.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "bda4ff6f3cd395534ba441ccacc4640f9dc43942" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/msgpack@1.3.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/msgpack-1.3.3.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/multi_json@1.14.1", + "type": "library", + "name": "multi_json", + "version": "1.14.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "fead333877a2db2e2aaca87d8cd1f270952cd42e" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/multi_json@1.14.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/multi_json-1.14.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/multipart-post@2.1.1", + "type": "library", + "name": "multipart-post", + "version": "2.1.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "927edb51d5b23a49a417fe1503f196896c0e8034" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/multipart-post@2.1.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/multipart-post-2.1.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/netrc@0.11.0", + "type": "library", + "name": "netrc", + "version": "0.11.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "98d7bbb894429413feb5c0a3b766a7945f65e3ba" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/netrc@0.11.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/netrc-0.11.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/oj@3.10.0", + "type": "library", + "name": "oj", + "version": "3.10.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "b348b933b9c2f6a6e952f6a15c7cbd9f1186815f" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/oj@3.10.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/oj-3.10.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/prometheus-client@0.9.0", + "type": "library", + "name": "prometheus-client", + "version": "0.9.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "f829d25ee6b39cdda518f7b6f85be6563d7b5990" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/prometheus-client@0.9.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/prometheus-client-0.9.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/public_suffix@4.0.3", + "type": "library", + "name": "public_suffix", + "version": "4.0.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "ac88907845a8bec2a0df25cf2e3ef61121e47252" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/public_suffix@4.0.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/public_suffix-4.0.3.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/quantile@0.2.1", + "type": "library", + "name": "quantile", + "version": "0.2.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "07a0817cd9df688930b2d8481a647a7ec321b870" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/quantile@0.2.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/quantile-0.2.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/rake@13.0.1", + "type": "library", + "name": "rake", + "version": "13.0.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "d01a832a472daf914670adda88b44b419a4d2daf" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/rake@13.0.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/rake-13.0.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/recursive-open-struct@1.1.0", + "type": "library", + "name": "recursive-open-struct", + "version": "1.1.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "5a4a02765d82d6786a832384b890f0a2497c2e14" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/recursive-open-struct@1.1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/recursive-open-struct-1.1.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/rest-client@2.1.0", + "type": "library", + "name": "rest-client", + "version": "2.1.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "a7e5943a216b16e0867693f20d5d1604cd015486" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/rest-client@2.1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/rest-client-2.1.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/serverengine@2.2.1", + "type": "library", + "name": "serverengine", + "version": "2.2.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "16e5806b2d513f6b075355c602aed0f960584267" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/serverengine@2.2.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/serverengine-2.2.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/sigdump@0.2.4", + "type": "library", + "name": "sigdump", + "version": "0.2.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "1bae5c6042dc82a4bec6aacb42f75ba71f7cb634" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/sigdump@0.2.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/sigdump-0.2.4.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/strptime@0.2.3", + "type": "library", + "name": "strptime", + "version": "0.2.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "17150d9e40754ea1a732796f224b9be78e77b86a" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + } + ], + "purl": "pkg:gem/strptime@0.2.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/strptime-0.2.3.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/systemd-journal@1.3.3", + "type": "library", + "name": "systemd-journal", + "version": "1.3.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "4f310622fe58e95897147736c96d3d42174a3363" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/systemd-journal@1.3.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/systemd-journal-1.3.3.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/thread_safe@0.3.6", + "type": "library", + "name": "thread_safe", + "version": "0.3.6", + "hashes": [ + { + "alg": "SHA-1", + "content": "546993ac33864e279ae73e918d6da5d4ca083098" + } + ], + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "purl": "pkg:gem/thread_safe@0.3.6", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/thread_safe-0.3.6.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/tzinfo-data@1.2019.3", + "type": "library", + "name": "tzinfo-data", + "version": "1.2019.3", + "hashes": [ + { + "alg": "SHA-1", + "content": "26832d11382943b02433f3ad1df7653b4cfdf3a2" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/tzinfo-data@1.2019.3", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/tzinfo-data-1.2019.3.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/tzinfo@1.2.6", + "type": "library", + "name": "tzinfo", + "version": "1.2.6", + "hashes": [ + { + "alg": "SHA-1", + "content": "5b7db490d431d97366729086683e736d2b5fee99" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/tzinfo@1.2.6", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/tzinfo-1.2.6.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/unf@0.1.4", + "type": "library", + "name": "unf", + "version": "0.1.4", + "hashes": [ + { + "alg": "SHA-1", + "content": "85ff87b60a6d16ffddf4db5f5f91c0ef76bacd3d" + } + ], + "licenses": [ + { + "license": { + "id": "BSD-2-Clause" + } + } + ], + "purl": "pkg:gem/unf@0.1.4", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/unf-0.1.4.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/unf_ext@0.0.7.6", + "type": "library", + "name": "unf_ext", + "version": "0.0.7.6", + "hashes": [ + { + "alg": "SHA-1", + "content": "1b5141ee855f16e832534c2e73d81fec0601ebd3" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/unf_ext@0.0.7.6", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/unf_ext-0.0.7.6.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/yajl-ruby@1.4.1", + "type": "library", + "name": "yajl-ruby", + "version": "1.4.1", + "hashes": [ + { + "alg": "SHA-1", + "content": "670f3cd2fc601c9b7fde02b1d8c60e90491a7221" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/yajl-ruby@1.4.1", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/yajl-ruby-1.4.1.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + }, + { + "bom-ref": "pkg:gem/zeitwerk@2.3.0", + "type": "library", + "name": "zeitwerk", + "version": "2.3.0", + "hashes": [ + { + "alg": "SHA-1", + "content": "803894c06d28932016866a26fc2b22c4db942094" + } + ], + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:gem/zeitwerk@2.3.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "var/lib/gems/2.5.0/specifications/zeitwerk-2.3.0.gemspec" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gemspec" + } + ] + } + ], + "dependencies": [ + { + "ref": "3ff14136-e09f-4df9-80ea-000000000001", + "dependsOn": [ + "3ff14136-e09f-4df9-80ea-000000000002", + "pkg:gem/activesupport@6.0.2.1", + "pkg:gem/addressable@2.7.0", + "pkg:gem/concurrent-ruby@1.1.6", + "pkg:gem/cool.io@1.6.0", + "pkg:gem/dig_rb@1.0.1", + "pkg:gem/domain_name@0.5.20190701", + "pkg:gem/elasticsearch-api@7.5.0", + "pkg:gem/elasticsearch-transport@7.5.0", + "pkg:gem/elasticsearch@7.5.0", + "pkg:gem/excon@0.72.0", + "pkg:gem/faraday@0.17.3", + "pkg:gem/ffi-compiler@1.0.1", + "pkg:gem/ffi@1.12.2", + "pkg:gem/fluent-plugin-concat@2.4.0", + "pkg:gem/fluent-plugin-detect-exceptions@0.0.13", + "pkg:gem/fluent-plugin-elasticsearch@3.8.0", + "pkg:gem/fluent-plugin-kubernetes_metadata_filter@2.4.1", + "pkg:gem/fluent-plugin-multi-format-parser@1.0.0", + "pkg:gem/fluent-plugin-prometheus@1.7.0", + "pkg:gem/fluent-plugin-systemd@1.0.2", + "pkg:gem/fluentd@1.8.0", + "pkg:gem/http-accept@1.7.0", + "pkg:gem/http-cookie@1.0.3", + "pkg:gem/http-form_data@2.2.0", + "pkg:gem/http-parser@1.2.1", + "pkg:gem/http@4.3.0", + "pkg:gem/http_parser.rb@0.6.0", + "pkg:gem/i18n@1.8.2", + "pkg:gem/kubeclient@4.6.0", + "pkg:gem/lru_redux@1.1.0", + "pkg:gem/mime-types-data@3.2019.1009", + "pkg:gem/mime-types@3.3.1", + "pkg:gem/minitest@5.14.0", + "pkg:gem/msgpack@1.3.3", + "pkg:gem/multi_json@1.14.1", + "pkg:gem/multipart-post@2.1.1", + "pkg:gem/netrc@0.11.0", + "pkg:gem/oj@3.10.0", + "pkg:gem/prometheus-client@0.9.0", + "pkg:gem/public_suffix@4.0.3", + "pkg:gem/quantile@0.2.1", + "pkg:gem/rake@13.0.1", + "pkg:gem/recursive-open-struct@1.1.0", + "pkg:gem/rest-client@2.1.0", + "pkg:gem/serverengine@2.2.1", + "pkg:gem/sigdump@0.2.4", + "pkg:gem/strptime@0.2.3", + "pkg:gem/systemd-journal@1.3.3", + "pkg:gem/thread_safe@0.3.6", + "pkg:gem/tzinfo-data@1.2019.3", + "pkg:gem/tzinfo@1.2.6", + "pkg:gem/unf@0.1.4", + "pkg:gem/unf_ext@0.0.7.6", + "pkg:gem/yajl-ruby@1.4.1", + "pkg:gem/zeitwerk@2.3.0" + ] + }, + { + "ref": "3ff14136-e09f-4df9-80ea-000000000002", + "dependsOn": [ + "pkg:deb/debian/apt@1.8.2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/base-passwd@3.5.46?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/bash@5.0-4?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/bsdutils@2.33.1-0.1?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/coreutils@8.30-3?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/dash@0.5.10.2-5?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/diffutils@3.7-3?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/e2fsprogs@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/findutils@4.6.0%2Bgit%2B20190209-2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/grep@3.3-1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/gzip@1.9-3?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/hostname@3.21?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libacl1@2.2.53-4?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libc-bin@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libdb5.3@5.3.28%2Bdfsg1-0.5?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libext2fs2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libjemalloc2@5.1.0-3?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libpam-modules-bin@1.3.1-5?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libpam-runtime@1.3.1-5?arch=all&distro=debian-10.2", + "pkg:deb/debian/libss2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/mawk@1.3.3-17%2Bb3?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/mount@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/ncurses-base@6.1%2B20181013-2%2Bdeb10u2?arch=all&distro=debian-10.2", + "pkg:deb/debian/ncurses-bin@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/sed@4.7-1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/sysvinit-utils@2.93-8?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/tzdata@2019c-0%2Bdeb10u1?arch=all&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/adduser@3.118?arch=all&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2", + "pkg:deb/debian/passwd@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/apt@1.8.2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/adduser@3.118?arch=all&distro=debian-10.2", + "pkg:deb/debian/debian-archive-keyring@2019.1?arch=all&distro=debian-10.2", + "pkg:deb/debian/gpgv@2.2.12-1%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libapt-pkg5.0@1.8.2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/libgnutls30@3.6.7-4?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libseccomp2@2.3.3-4?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libstdc%2B%2B6@8.3.0-6?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/base-files@10.3%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/base-passwd@3.5.46?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libdebconfclient0@0.249?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/bash@5.0-4?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/base-files@10.3%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/debianutils@4.8.6.1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/bsdutils@2.33.1-0.1?arch=amd64&distro=debian-10.2&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ca-certificates@20190110?arch=all&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2", + "pkg:deb/debian/openssl@1.1.1d-0%2Bdeb10u2?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/coreutils@8.30-3?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/dash@0.5.10.2-5?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2", + "pkg:deb/debian/debianutils@4.8.6.1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/dpkg@1.19.7?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/debian-archive-keyring@2019.1?arch=all&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/debianutils@4.8.6.1?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/diffutils@3.7-3?arch=amd64&distro=debian-10.2&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/dpkg@1.19.7?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/tar@1.30%2Bdfsg-6?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/e2fsprogs@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/fdisk@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libfdisk1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libmount1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libncursesw6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libsmartcols1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libtinfo6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/findutils@4.6.0%2Bgit%2B20190209-2?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/gcc-8-base@8.3.0-6?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/gpgv@2.2.12-1%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libbz2-1.0@1.0.6-9.2~deb10u1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgcrypt20@1.8.4-5?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgpg-error0@1.35-1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/zlib1g@1.2.11.dfsg-1?arch=amd64&distro=debian-10.2&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/grep@3.3-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/dpkg@1.19.7?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/gzip@1.9-3?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/dpkg@1.19.7?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/hostname@3.21?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/init-system-helpers@1.56%2Bnmu1?arch=all&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/perl-base@5.28.1-6?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libacl1@2.2.53-4?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libattr1@2.4.48-4?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libapt-pkg5.0@1.8.2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libbz2-1.0@1.0.6-9.2~deb10u1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/liblz4-1@1.8.3-1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/liblzma5@5.2.4-1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libstdc%2B%2B6@8.3.0-6?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libsystemd0@241-7~deb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libudev1@241-7~deb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libzstd1@1.3.8%2Bdfsg-3?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/zlib1g@1.2.11.dfsg-1?arch=amd64&distro=debian-10.2&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libattr1@2.4.48-4?arch=amd64&distro=debian-10.2&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libaudit-common@2.8.4-3?arch=all&distro=debian-10.2&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libaudit1@2.8.4-3?arch=amd64&distro=debian-10.2&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libaudit-common@2.8.4-3?arch=all&distro=debian-10.2&epoch=1", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libcap-ng0@0.7.9-2?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libblkid1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libuuid1@2.33.1-0.1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libbz2-1.0@1.0.6-9.2~deb10u1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libc-bin@2.28-10?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libcap-ng0@0.7.9-2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libcom-err2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libdb5.3@5.3.28%2Bdfsg1-0.5?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libdebconfclient0@0.249?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libext2fs2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libfdisk1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libblkid1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libuuid1@2.33.1-0.1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libffi6@3.2.1-9?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1", + "dependsOn": [ + "pkg:deb/debian/gcc-8-base@8.3.0-6?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libgcrypt20@1.8.4-5?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgpg-error0@1.35-1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libgdbm-compat4@1.18.1-4?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgdbm6@1.18.1-4?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libgdbm6@1.18.1-4?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libgmp10@6.1.2%2Bdfsg-4?arch=amd64&distro=debian-10.2&epoch=2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libgnutls30@3.6.7-4?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgmp10@6.1.2%2Bdfsg-4?arch=amd64&distro=debian-10.2&epoch=2", + "pkg:deb/debian/libhogweed4@3.4.1-1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libidn2-0@2.0.5-1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libnettle6@3.4.1-1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libp11-kit0@0.23.15-2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libtasn1-6@4.13-3?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libunistring2@0.9.10-1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libgpg-error0@1.35-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libhogweed4@3.4.1-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgmp10@6.1.2%2Bdfsg-4?arch=amd64&distro=debian-10.2&epoch=2", + "pkg:deb/debian/libnettle6@3.4.1-1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libidn2-0@2.0.5-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libunistring2@0.9.10-1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libjemalloc2@5.1.0-3?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/libstdc%2B%2B6@8.3.0-6?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/liblz4-1@1.8.3-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/liblzma5@5.2.4-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libmount1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libblkid1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libselinux1@2.8-1%2Bb1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libncurses6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libtinfo6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libncursesw6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libtinfo6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libnettle6@3.4.1-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libp11-kit0@0.23.15-2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libffi6@3.2.1-9?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libpam-modules-bin@1.3.1-5?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libaudit1@2.8.4-3?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libpam0g@1.3.1-5?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libselinux1@2.8-1%2Bb1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libpam-modules@1.3.1-5?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libpam-runtime@1.3.1-5?arch=all&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2", + "pkg:deb/debian/libpam-modules@1.3.1-5?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libpam0g@1.3.1-5?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2", + "pkg:deb/debian/libaudit1@2.8.4-3?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libpcre3@8.39-12?arch=amd64&distro=debian-10.2&epoch=2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libreadline7@7.0-5?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libtinfo6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/readline-common@7.0-5?arch=all&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libruby2.5@2.5.5-3%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libffi6@3.2.1-9?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgdbm-compat4@1.18.1-4?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgdbm6@1.18.1-4?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgmp10@6.1.2%2Bdfsg-4?arch=amd64&distro=debian-10.2&epoch=2", + "pkg:deb/debian/libncurses6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libreadline7@7.0-5?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libssl1.1@1.1.1d-0%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libtinfo6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libyaml-0-2@0.2.1-1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/rake@12.3.1-3?arch=all&distro=debian-10.2", + "pkg:deb/debian/ruby-did-you-mean@1.2.1-1?arch=all&distro=debian-10.2", + "pkg:deb/debian/ruby-minitest@5.11.3-1?arch=all&distro=debian-10.2", + "pkg:deb/debian/ruby-net-telnet@0.1.1-2?arch=all&distro=debian-10.2", + "pkg:deb/debian/ruby-test-unit@3.2.8-1?arch=all&distro=debian-10.2", + "pkg:deb/debian/ruby-xmlrpc@0.3.0-2?arch=all&distro=debian-10.2", + "pkg:deb/debian/zlib1g@1.2.11.dfsg-1?arch=amd64&distro=debian-10.2&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libseccomp2@2.3.3-4?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libselinux1@2.8-1%2Bb1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libpcre3@8.39-12?arch=amd64&distro=debian-10.2&epoch=2" + ] + }, + { + "ref": "pkg:deb/debian/libsemanage-common@2.8-2?arch=all&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libsemanage1@2.8-2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libaudit1@2.8.4-3?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/libbz2-1.0@1.0.6-9.2~deb10u1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libselinux1@2.8-1%2Bb1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libsemanage-common@2.8-2?arch=all&distro=debian-10.2", + "pkg:deb/debian/libsepol1@2.8-1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libsepol1@2.8-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libsmartcols1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libss2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libcom-err2@1.44.5-1%2Bdeb10u2?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libssl1.1@1.1.1d-0%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libstdc%2B%2B6@8.3.0-6?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/gcc-8-base@8.3.0-6?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgcc1@8.3.0-6?arch=amd64&distro=debian-10.2&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/libsystemd0@241-7~deb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/libtasn1-6@4.13-3?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libtinfo6@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libudev1@241-7~deb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libunistring2@0.9.10-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libuuid1@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libyaml-0-2@0.2.1-1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/libzstd1@1.3.8%2Bdfsg-3?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/login@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/mawk@1.3.3-17%2Bb3?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/mount@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/util-linux@2.33.1-0.1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/ncurses-base@6.1%2B20181013-2%2Bdeb10u2?arch=all&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ncurses-bin@6.1%2B20181013-2%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/openssl@1.1.1d-0%2Bdeb10u2?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libssl1.1@1.1.1d-0%2Bdeb10u2?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/passwd@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libaudit1@2.8.4-3?arch=amd64&distro=debian-10.2&epoch=1", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libpam-modules@1.3.1-5?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libpam0g@1.3.1-5?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libselinux1@2.8-1%2Bb1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libsemanage1@2.8-2?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/perl-base@5.28.1-6?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/rake@12.3.1-3?arch=all&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/ruby@2.5.1?arch=amd64&distro=debian-10.2&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/readline-common@7.0-5?arch=all&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/dpkg@1.19.7?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/ruby-did-you-mean@1.2.1-1?arch=all&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ruby-minitest@5.11.3-1?arch=all&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ruby-net-telnet@0.1.1-2?arch=all&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ruby-power-assert@1.1.1-1?arch=all&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ruby-test-unit@3.2.8-1?arch=all&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/ruby-power-assert@1.1.1-1?arch=all&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/ruby-xmlrpc@0.3.0-2?arch=all&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/ruby2.5@2.5.5-3%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/libgmp10@6.1.2%2Bdfsg-4?arch=amd64&distro=debian-10.2&epoch=2", + "pkg:deb/debian/libruby2.5@2.5.5-3%2Bdeb10u1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/rubygems-integration@1.11?arch=all&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/ruby@2.5.1?arch=amd64&distro=debian-10.2&epoch=1", + "dependsOn": [ + "pkg:deb/debian/ruby2.5@2.5.5-3%2Bdeb10u1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/rubygems-integration@1.11?arch=all&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/ca-certificates@20190110?arch=all&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/sed@4.7-1?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/sysvinit-utils@2.93-8?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/init-system-helpers@1.56%2Bnmu1?arch=all&distro=debian-10.2", + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/util-linux@2.33.1-0.1?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/tar@1.30%2Bdfsg-6?arch=amd64&distro=debian-10.2", + "dependsOn": [] + }, + { + "ref": "pkg:deb/debian/tzdata@2019c-0%2Bdeb10u1?arch=all&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/debconf@1.5.71?arch=all&distro=debian-10.2" + ] + }, + { + "ref": "pkg:deb/debian/util-linux@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "dependsOn": [ + "pkg:deb/debian/fdisk@2.33.1-0.1?arch=amd64&distro=debian-10.2", + "pkg:deb/debian/login@4.5-1.1?arch=amd64&distro=debian-10.2&epoch=1" + ] + }, + { + "ref": "pkg:deb/debian/zlib1g@1.2.11.dfsg-1?arch=amd64&distro=debian-10.2&epoch=1", + "dependsOn": [ + "pkg:deb/debian/libc6@2.28-10?arch=amd64&distro=debian-10.2" + ] + }, + { + "ref": "pkg:gem/activesupport@6.0.2.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/addressable@2.7.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/concurrent-ruby@1.1.6", + "dependsOn": [] + }, + { + "ref": "pkg:gem/cool.io@1.6.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/dig_rb@1.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/domain_name@0.5.20190701", + "dependsOn": [] + }, + { + "ref": "pkg:gem/elasticsearch-api@7.5.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/elasticsearch-transport@7.5.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/elasticsearch@7.5.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/excon@0.72.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/faraday@0.17.3", + "dependsOn": [] + }, + { + "ref": "pkg:gem/ffi-compiler@1.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/ffi@1.12.2", + "dependsOn": [] + }, + { + "ref": "pkg:gem/fluent-plugin-concat@2.4.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/fluent-plugin-detect-exceptions@0.0.13", + "dependsOn": [] + }, + { + "ref": "pkg:gem/fluent-plugin-elasticsearch@3.8.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/fluent-plugin-kubernetes_metadata_filter@2.4.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/fluent-plugin-multi-format-parser@1.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/fluent-plugin-prometheus@1.7.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/fluent-plugin-systemd@1.0.2", + "dependsOn": [] + }, + { + "ref": "pkg:gem/fluentd@1.8.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/http-accept@1.7.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/http-cookie@1.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:gem/http-form_data@2.2.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/http-parser@1.2.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/http@4.3.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/http_parser.rb@0.6.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/i18n@1.8.2", + "dependsOn": [] + }, + { + "ref": "pkg:gem/kubeclient@4.6.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/lru_redux@1.1.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/mime-types-data@3.2019.1009", + "dependsOn": [] + }, + { + "ref": "pkg:gem/mime-types@3.3.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/minitest@5.14.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/msgpack@1.3.3", + "dependsOn": [] + }, + { + "ref": "pkg:gem/multi_json@1.14.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/multipart-post@2.1.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/netrc@0.11.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/oj@3.10.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/prometheus-client@0.9.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/public_suffix@4.0.3", + "dependsOn": [] + }, + { + "ref": "pkg:gem/quantile@0.2.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/rake@13.0.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/recursive-open-struct@1.1.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/rest-client@2.1.0", + "dependsOn": [] + }, + { + "ref": "pkg:gem/serverengine@2.2.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/sigdump@0.2.4", + "dependsOn": [] + }, + { + "ref": "pkg:gem/strptime@0.2.3", + "dependsOn": [] + }, + { + "ref": "pkg:gem/systemd-journal@1.3.3", + "dependsOn": [] + }, + { + "ref": "pkg:gem/thread_safe@0.3.6", + "dependsOn": [] + }, + { + "ref": "pkg:gem/tzinfo-data@1.2019.3", + "dependsOn": [] + }, + { + "ref": "pkg:gem/tzinfo@1.2.6", + "dependsOn": [] + }, + { + "ref": "pkg:gem/unf@0.1.4", + "dependsOn": [] + }, + { + "ref": "pkg:gem/unf_ext@0.0.7.6", + "dependsOn": [] + }, + { + "ref": "pkg:gem/yajl-ruby@1.4.1", + "dependsOn": [] + }, + { + "ref": "pkg:gem/zeitwerk@2.3.0", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/integration/testdata/fluentd-multiple-lockfiles.json.golden b/integration/testdata/fluentd-multiple-lockfiles.json.golden new file mode 100644 index 000000000000..436ea65d8b8a --- /dev/null +++ b/integration/testdata/fluentd-multiple-lockfiles.json.golden @@ -0,0 +1,244 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json", + "ArtifactType": "cyclonedx", + "Metadata": { + "OS": { + "Family": "debian", + "Name": "10.2" + }, + "ImageID": "sha256:5a992077baba51b97f27591a10d54d2f2723dc9c81a3fe419e261023f2554933", + "DiffIDs": [ + "sha256:02874b2b269dea8dde0f7edb4c9906904dfe38a09de1a214f20c650cfb15c60e", + "sha256:25165eb51d15842f870f97873e0a58409d5e860e6108e3dd829bd10e484c0065", + "sha256:3752e1f6fd759c795c13aff2c93c081529366e27635ba6621e849b0f9cfc77f0", + "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9", + "sha256:788c00e2cfc8f2a018ae4344ccf0b2c226ebd756d7effd1ce50eea1a4252cd89", + "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/sbom/fluentd-multiple-lockfiles-cyclonedx.json (debian 10.2)", + "Class": "os-pkgs", + "Type": "debian", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18276", + "PkgID": "bash@5.0-4", + "PkgName": "bash", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2", + "UID": "8ca99d0ea2f4b0a3", + "BOMRef": "pkg:deb/debian/bash@5.0-4?distro=debian-10.2" + }, + "InstalledVersion": "5.0-4", + "Status": "affected", + "Layer": { + "Digest": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c", + "DiffID": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + "SeveritySource": "debian", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:34e0d0620913b03c4e279d15e6afe937d836fbec36ae505ac3e32418cdcf32b3", + "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", + "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "Severity": "LOW", + "CweIDs": [ + "CWE-273" + ], + "VendorSeverity": { + "cbl-mariner": 3, + "debian": 1, + "nvd": 3, + "oracle-oval": 1, + "photon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.2, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + } + }, + "References": [ + "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", + "https://access.redhat.com/security/cve/CVE-2019-18276", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", + "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", + "https://linux.oracle.com/cve/CVE-2019-18276.html", + "https://linux.oracle.com/errata/ELSA-2021-1679.html", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", + "https://security.gentoo.org/glsa/202105-34", + "https://security.netapp.com/advisory/ntap-20200430-0003/", + "https://www.youtube.com/watch?v=-wGtxJ8opa8" + ], + "PublishedDate": "2019-11-28T01:15:00Z", + "LastModifiedDate": "2021-05-26T12:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-18224", + "VendorIDs": [ + "DSA-4613-1" + ], + "PkgID": "libidn2-0@2.0.5-1", + "PkgName": "libidn2-0", + "PkgIdentifier": { + "PURL": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2", + "UID": "bd31ad93af9a5d2", + "BOMRef": "pkg:deb/debian/libidn2-0@2.0.5-1?distro=debian-10.2" + }, + "InstalledVersion": "2.0.5-1", + "FixedVersion": "2.0.5-1+deb10u1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:000eee12ec04cc914bf96e8f5dee7767510c2aca3816af6078bd9fbe3150920c", + "DiffID": "sha256:831c5620387fb9efec59fc82a42b948546c6be601e3ab34a87108ecf852aa15f" + }, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18224", + "DataSource": { + "ID": "debian", + "Name": "Debian Security Tracker", + "URL": "https://salsa.debian.org/security-tracker-team/security-tracker" + }, + "Fingerprint": "sha256:be3b0333e02ee8f2a565ebd4dfe0f73e215072536d5791d989f98c511c5bd336", + "Title": "libidn2: heap-based buffer overflow in idn2_to_ascii_4i in lib/lookup.c", + "Description": "idn2_to_ascii_4i in lib/lookup.c in GNU libidn2 before 2.1.1 has a heap-based buffer overflow via a long domain string.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "nvd": 4, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00008.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-12/msg00009.html", + "https://access.redhat.com/security/cve/CVE-2019-18224", + "https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=12420", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18224", + "https://github.com/libidn/libidn2/commit/e4d1558aa2c1c04a05066ee8600f37603890ba8c", + "https://github.com/libidn/libidn2/compare/libidn2-2.1.0...libidn2-2.1.1", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/JDQVQ2XPV5BTZUFINT7AFJSKNNBVURNJ/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/MINU5RKDFE6TKAFY5DRFN3WSFDS4DYVS/", + "https://seclists.org/bugtraq/2020/Feb/4", + "https://security.gentoo.org/glsa/202003-63", + "https://ubuntu.com/security/notices/USN-4168-1", + "https://usn.ubuntu.com/4168-1/", + "https://www.debian.org/security/2020/dsa-4613" + ], + "PublishedDate": "2019-10-21T17:15:00Z", + "LastModifiedDate": "2019-10-29T19:15:00Z" + } + ] + }, + { + "Target": "Ruby", + "Class": "lang-pkgs", + "Type": "gemspec", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-8165", + "PkgID": "activesupport@6.0.2.1", + "PkgName": "activesupport", + "PkgPath": "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec", + "PkgIdentifier": { + "PURL": "pkg:gem/activesupport@6.0.2.1", + "UID": "66a6de64809697cd", + "BOMRef": "pkg:gem/activesupport@6.0.2.1?file_path=var%2Flib%2Fgems%2F2.5.0%2Fspecifications%2Factivesupport-6.0.2.1.gemspec" + }, + "InstalledVersion": "6.0.2.1", + "FixedVersion": "6.0.3.1, 5.2.4.3", + "Status": "fixed", + "Layer": { + "Digest": "sha256:a8877cad19f14a7044524a145ce33170085441a7922458017db1631dcd5f7602", + "DiffID": "sha256:75e43d55939745950bc3f8fad56c5834617c4339f0f54755e69a0dd5372624e9" + }, + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-8165", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory RubyGems", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Arubygems" + }, + "Fingerprint": "sha256:6af93df86a454ee80626db94b3fc0d8af052f5c9ca10406bf0fdc9515255df92", + "Title": "rubygem-activesupport: potentially unintended unmarshalling of user-provided objects in MemCacheStore and RedisCacheStore", + "Description": "A deserialization of untrusted data vulnerability exists in rails \u003c 5.2.4.3, rails \u003c 6.0.3.1 that can allow an attacker to unmarshal user-provided objects in MemCacheStore and RedisCacheStore potentially resulting in an RCE.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 4, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 9.8 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00031.html", + "http://lists.opensuse.org/opensuse-security-announce/2020-10/msg00034.html", + "https://access.redhat.com/security/cve/CVE-2020-8165", + "https://github.com/advisories/GHSA-2p68-f74v-9wc6", + "https://github.com/rubysec/ruby-advisory-db/blob/master/gems/activesupport/CVE-2020-8165.yml", + "https://groups.google.com/forum/#!msg/rubyonrails-security/bv6fW4S0Y1c/KnkEqM7AAQAJ", + "https://groups.google.com/forum/#!topic/rubyonrails-security/bv6fW4S0Y1c", + "https://groups.google.com/g/rubyonrails-security/c/bv6fW4S0Y1c", + "https://hackerone.com/reports/413388", + "https://lists.debian.org/debian-lts-announce/2020/06/msg00022.html", + "https://lists.debian.org/debian-lts-announce/2020/07/msg00013.html", + "https://nvd.nist.gov/vuln/detail/CVE-2020-8165", + "https://weblog.rubyonrails.org/2020/5/18/Rails-5-2-4-3-and-6-0-3-1-have-been-released/", + "https://www.debian.org/security/2020/dsa-4766" + ], + "PublishedDate": "2020-06-19T18:15:00Z", + "LastModifiedDate": "2020-10-17T12:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/gomod-skip.json.golden b/integration/testdata/gomod-skip.json.golden new file mode 100644 index 000000000000..bdf7a7b83550 --- /dev/null +++ b/integration/testdata/gomod-skip.json.golden @@ -0,0 +1,153 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/gomod", + "ArtifactType": "repository", + "Results": [ + { + "Target": "go.mod", + "Class": "lang-pkgs", + "Type": "gomod", + "Vulnerabilities": [ + { + "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", + "PkgName": "github.com/docker/distribution", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/docker/distribution@v2.7.1%2Bincompatible", + "UID": "debd192c5b1bc1d2" + }, + "InstalledVersion": "v2.7.1+incompatible", + "FixedVersion": "v2.8.0", + "Status": "fixed", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:64b5011ff630ca20a45001519f1ab3614d52a70f8c635f5b443d8e7b559bf8e7", + "Title": "OCI Manifest Type Confusion Issue", + "Description": "### Impact\n\nSystems that rely on digest equivalence for image attestations may be vulnerable to type confusion.", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/distribution/distribution/commit/b59a6f827947f9e0e67df0cfb571046de4733586", + "https://github.com/distribution/distribution/security/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/opencontainers/image-spec/pull/411" + ] + }, + { + "VulnerabilityID": "CVE-2022-23628", + "PkgID": "github.com/open-policy-agent/opa@v0.35.0", + "PkgName": "github.com/open-policy-agent/opa", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/open-policy-agent/opa@v0.35.0", + "UID": "27f967c9d1c1a361" + }, + "InstalledVersion": "v0.35.0", + "FixedVersion": "0.37.0", + "Status": "fixed", + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-23628", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:a654f7b9edfe2608ebc91a0f5eb3fd7053cd26e0a89941b89f4b6290492884b8", + "Title": "Incorrect Calculation", + "Description": "OPA is an open source, general-purpose policy engine. Under certain conditions, pretty-printing an abstract syntax tree (AST) that contains synthetic nodes could change the logic of some statements by reordering array literals. Example of policies impacted are those that parse and compare web paths. **All of these** three conditions have to be met to create an adverse effect: 1. An AST of Rego had to be **created programmatically** such that it ends up containing terms without a location (such as wildcard variables). 2. The AST had to be **pretty-printed** using the `github.com/open-policy-agent/opa/format` package. 3. The result of the pretty-printing had to be **parsed and evaluated again** via an OPA instance using the bundles, or the Golang packages. If any of these three conditions are not met, you are not affected. Notably, all three would be true if using **optimized bundles**, i.e. bundles created with `opa build -O=1` or higher. In that case, the optimizer would fulfil condition (1.), the result of that would be pretty-printed when writing the bundle to disk, fulfilling (2.). When the bundle was then used, we'd satisfy (3.). As a workaround users may disable optimization when creating bundles.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-682" + ], + "VendorSeverity": { + "nvd": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N", + "V2Score": 4.3, + "V3Score": 5.3 + } + }, + "References": [ + "https://github.com/advisories/GHSA-hcw3-j74m-qc58", + "https://github.com/open-policy-agent/opa/commit/932e4ffc37a590ace79e9b75ca4340288c220239", + "https://github.com/open-policy-agent/opa/commit/bfd984ddf93ef2c4963a08d4fdadae0bcf1a3717", + "https://github.com/open-policy-agent/opa/pull/3851", + "https://github.com/open-policy-agent/opa/security/advisories/GHSA-hcw3-j74m-qc58", + "https://nvd.nist.gov/vuln/detail/CVE-2022-23628" + ], + "PublishedDate": "2022-02-09T22:15:00Z", + "LastModifiedDate": "2022-02-17T02:37:00Z" + }, + { + "VulnerabilityID": "CVE-2021-38561", + "PkgID": "golang.org/x/text@v0.3.6", + "PkgName": "golang.org/x/text", + "PkgIdentifier": { + "PURL": "pkg:golang/golang.org/x/text@v0.3.6", + "UID": "b615a892755bf9c7" + }, + "InstalledVersion": "v0.3.6", + "FixedVersion": "0.3.7", + "Status": "fixed", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-38561", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:a07e93bf36df24f3dabb58febefbf2977347f381a8ea8350457425f679943800", + "Description": "Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n", + "Severity": "UNKNOWN", + "References": [ + "https://go-review.googlesource.com/c/text/+/340830", + "https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f", + "https://pkg.go.dev/vuln/GO-2021-0113" + ] + } + ] + }, + { + "Target": "submod/go.mod", + "Class": "lang-pkgs", + "Type": "gomod", + "Vulnerabilities": [ + { + "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", + "PkgName": "github.com/docker/distribution", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/docker/distribution@v2.7.1%2Bincompatible", + "UID": "e542bafcded31fe0" + }, + "InstalledVersion": "v2.7.1+incompatible", + "FixedVersion": "v2.8.0", + "Status": "fixed", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:0a441ad53c8c37db9775897e0a2fb656b45e52c36018df47cdc3c5ffecee5bd8", + "Title": "OCI Manifest Type Confusion Issue", + "Description": "### Impact\n\nSystems that rely on digest equivalence for image attestations may be vulnerable to type confusion.", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/distribution/distribution/commit/b59a6f827947f9e0e67df0cfb571046de4733586", + "https://github.com/distribution/distribution/security/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/opencontainers/image-spec/pull/411" + ] + } + ] + } + ] +} diff --git a/integration/testdata/gomod-vex.json.golden b/integration/testdata/gomod-vex.json.golden new file mode 100644 index 000000000000..6521ad8676f2 --- /dev/null +++ b/integration/testdata/gomod-vex.json.golden @@ -0,0 +1,140 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/gomod", + "ArtifactType": "repository", + "Results": [ + { + "Target": "go.mod", + "Class": "lang-pkgs", + "Type": "gomod", + "Vulnerabilities": [ + { + "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", + "PkgName": "github.com/docker/distribution", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/docker/distribution@v2.7.1%2Bincompatible", + "UID": "debd192c5b1bc1d2" + }, + "InstalledVersion": "v2.7.1+incompatible", + "FixedVersion": "v2.8.0", + "Status": "fixed", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:64b5011ff630ca20a45001519f1ab3614d52a70f8c635f5b443d8e7b559bf8e7", + "Title": "OCI Manifest Type Confusion Issue", + "Description": "### Impact\n\nSystems that rely on digest equivalence for image attestations may be vulnerable to type confusion.", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/distribution/distribution/commit/b59a6f827947f9e0e67df0cfb571046de4733586", + "https://github.com/distribution/distribution/security/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/opencontainers/image-spec/pull/411" + ] + }, + { + "VulnerabilityID": "CVE-2021-38561", + "PkgID": "golang.org/x/text@v0.3.6", + "PkgName": "golang.org/x/text", + "PkgIdentifier": { + "PURL": "pkg:golang/golang.org/x/text@v0.3.6", + "UID": "b615a892755bf9c7" + }, + "InstalledVersion": "v0.3.6", + "FixedVersion": "0.3.7", + "Status": "fixed", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-38561", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:a07e93bf36df24f3dabb58febefbf2977347f381a8ea8350457425f679943800", + "Description": "Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n", + "Severity": "UNKNOWN", + "References": [ + "https://go-review.googlesource.com/c/text/+/340830", + "https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f", + "https://pkg.go.dev/vuln/GO-2021-0113" + ] + } + ] + }, + { + "Target": "submod/go.mod", + "Class": "lang-pkgs", + "Type": "gomod", + "Vulnerabilities": [ + { + "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", + "PkgName": "github.com/docker/distribution", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/docker/distribution@v2.7.1%2Bincompatible", + "UID": "e542bafcded31fe0" + }, + "InstalledVersion": "v2.7.1+incompatible", + "FixedVersion": "v2.8.0", + "Status": "fixed", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:0a441ad53c8c37db9775897e0a2fb656b45e52c36018df47cdc3c5ffecee5bd8", + "Title": "OCI Manifest Type Confusion Issue", + "Description": "### Impact\n\nSystems that rely on digest equivalence for image attestations may be vulnerable to type confusion.", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/distribution/distribution/commit/b59a6f827947f9e0e67df0cfb571046de4733586", + "https://github.com/distribution/distribution/security/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/opencontainers/image-spec/pull/411" + ] + } + ] + }, + { + "Target": "submod2/go.mod", + "Class": "lang-pkgs", + "Type": "gomod", + "Vulnerabilities": [ + { + "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", + "PkgName": "github.com/docker/distribution", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/docker/distribution@v2.7.1%2Bincompatible", + "UID": "560ad8ad0be3b950" + }, + "InstalledVersion": "v2.7.1+incompatible", + "FixedVersion": "v2.8.0", + "Status": "fixed", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:c7b56b9d670deeed8f902fefcfb8897991e363b00e41393fa1733e9d50d44900", + "Title": "OCI Manifest Type Confusion Issue", + "Description": "### Impact\n\nSystems that rely on digest equivalence for image attestations may be vulnerable to type confusion.", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/distribution/distribution/commit/b59a6f827947f9e0e67df0cfb571046de4733586", + "https://github.com/distribution/distribution/security/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/opencontainers/image-spec/pull/411" + ] + } + ] + } + ] +} diff --git a/integration/testdata/gomod.json.golden b/integration/testdata/gomod.json.golden new file mode 100644 index 000000000000..996037a85958 --- /dev/null +++ b/integration/testdata/gomod.json.golden @@ -0,0 +1,187 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/gomod", + "ArtifactType": "repository", + "Results": [ + { + "Target": "go.mod", + "Class": "lang-pkgs", + "Type": "gomod", + "Vulnerabilities": [ + { + "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", + "PkgName": "github.com/docker/distribution", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/docker/distribution@v2.7.1%2Bincompatible", + "UID": "debd192c5b1bc1d2" + }, + "InstalledVersion": "v2.7.1+incompatible", + "FixedVersion": "v2.8.0", + "Status": "fixed", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:64b5011ff630ca20a45001519f1ab3614d52a70f8c635f5b443d8e7b559bf8e7", + "Title": "OCI Manifest Type Confusion Issue", + "Description": "### Impact\n\nSystems that rely on digest equivalence for image attestations may be vulnerable to type confusion.", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/distribution/distribution/commit/b59a6f827947f9e0e67df0cfb571046de4733586", + "https://github.com/distribution/distribution/security/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/opencontainers/image-spec/pull/411" + ] + }, + { + "VulnerabilityID": "CVE-2022-23628", + "PkgID": "github.com/open-policy-agent/opa@v0.35.0", + "PkgName": "github.com/open-policy-agent/opa", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/open-policy-agent/opa@v0.35.0", + "UID": "27f967c9d1c1a361" + }, + "InstalledVersion": "v0.35.0", + "FixedVersion": "0.37.0", + "Status": "fixed", + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-23628", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:a654f7b9edfe2608ebc91a0f5eb3fd7053cd26e0a89941b89f4b6290492884b8", + "Title": "Incorrect Calculation", + "Description": "OPA is an open source, general-purpose policy engine. Under certain conditions, pretty-printing an abstract syntax tree (AST) that contains synthetic nodes could change the logic of some statements by reordering array literals. Example of policies impacted are those that parse and compare web paths. **All of these** three conditions have to be met to create an adverse effect: 1. An AST of Rego had to be **created programmatically** such that it ends up containing terms without a location (such as wildcard variables). 2. The AST had to be **pretty-printed** using the `github.com/open-policy-agent/opa/format` package. 3. The result of the pretty-printing had to be **parsed and evaluated again** via an OPA instance using the bundles, or the Golang packages. If any of these three conditions are not met, you are not affected. Notably, all three would be true if using **optimized bundles**, i.e. bundles created with `opa build -O=1` or higher. In that case, the optimizer would fulfil condition (1.), the result of that would be pretty-printed when writing the bundle to disk, fulfilling (2.). When the bundle was then used, we'd satisfy (3.). As a workaround users may disable optimization when creating bundles.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-682" + ], + "VendorSeverity": { + "nvd": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N", + "V2Score": 4.3, + "V3Score": 5.3 + } + }, + "References": [ + "https://github.com/advisories/GHSA-hcw3-j74m-qc58", + "https://github.com/open-policy-agent/opa/commit/932e4ffc37a590ace79e9b75ca4340288c220239", + "https://github.com/open-policy-agent/opa/commit/bfd984ddf93ef2c4963a08d4fdadae0bcf1a3717", + "https://github.com/open-policy-agent/opa/pull/3851", + "https://github.com/open-policy-agent/opa/security/advisories/GHSA-hcw3-j74m-qc58", + "https://nvd.nist.gov/vuln/detail/CVE-2022-23628" + ], + "PublishedDate": "2022-02-09T22:15:00Z", + "LastModifiedDate": "2022-02-17T02:37:00Z" + }, + { + "VulnerabilityID": "CVE-2021-38561", + "PkgID": "golang.org/x/text@v0.3.6", + "PkgName": "golang.org/x/text", + "PkgIdentifier": { + "PURL": "pkg:golang/golang.org/x/text@v0.3.6", + "UID": "b615a892755bf9c7" + }, + "InstalledVersion": "v0.3.6", + "FixedVersion": "0.3.7", + "Status": "fixed", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-38561", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:a07e93bf36df24f3dabb58febefbf2977347f381a8ea8350457425f679943800", + "Description": "Due to improper index calculation, an incorrectly formatted language tag can cause Parse\nto panic via an out of bounds read. If Parse is used to process untrusted user inputs,\nthis may be used as a vector for a denial of service attack.\n", + "Severity": "UNKNOWN", + "References": [ + "https://go-review.googlesource.com/c/text/+/340830", + "https://go.googlesource.com/text/+/383b2e75a7a4198c42f8f87833eefb772868a56f", + "https://pkg.go.dev/vuln/GO-2021-0113" + ] + } + ] + }, + { + "Target": "submod/go.mod", + "Class": "lang-pkgs", + "Type": "gomod", + "Vulnerabilities": [ + { + "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", + "PkgName": "github.com/docker/distribution", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/docker/distribution@v2.7.1%2Bincompatible", + "UID": "e542bafcded31fe0" + }, + "InstalledVersion": "v2.7.1+incompatible", + "FixedVersion": "v2.8.0", + "Status": "fixed", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:0a441ad53c8c37db9775897e0a2fb656b45e52c36018df47cdc3c5ffecee5bd8", + "Title": "OCI Manifest Type Confusion Issue", + "Description": "### Impact\n\nSystems that rely on digest equivalence for image attestations may be vulnerable to type confusion.", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/distribution/distribution/commit/b59a6f827947f9e0e67df0cfb571046de4733586", + "https://github.com/distribution/distribution/security/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/opencontainers/image-spec/pull/411" + ] + } + ] + }, + { + "Target": "submod2/go.mod", + "Class": "lang-pkgs", + "Type": "gomod", + "Vulnerabilities": [ + { + "VulnerabilityID": "GMS-2022-20", + "PkgID": "github.com/docker/distribution@v2.7.1+incompatible", + "PkgName": "github.com/docker/distribution", + "PkgIdentifier": { + "PURL": "pkg:golang/github.com/docker/distribution@v2.7.1%2Bincompatible", + "UID": "560ad8ad0be3b950" + }, + "InstalledVersion": "v2.7.1+incompatible", + "FixedVersion": "v2.8.0", + "Status": "fixed", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Go", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + }, + "Fingerprint": "sha256:c7b56b9d670deeed8f902fefcfb8897991e363b00e41393fa1733e9d50d44900", + "Title": "OCI Manifest Type Confusion Issue", + "Description": "### Impact\n\nSystems that rely on digest equivalence for image attestations may be vulnerable to type confusion.", + "Severity": "UNKNOWN", + "References": [ + "https://github.com/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/distribution/distribution/commit/b59a6f827947f9e0e67df0cfb571046de4733586", + "https://github.com/distribution/distribution/security/advisories/GHSA-qq97-vm5h-rrhg", + "https://github.com/opencontainers/image-spec/pull/411" + ] + } + ] + } + ] +} diff --git a/integration/testdata/gradle.json.golden b/integration/testdata/gradle.json.golden new file mode 100644 index 000000000000..713dd70161fa --- /dev/null +++ b/integration/testdata/gradle.json.golden @@ -0,0 +1,141 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/gradle", + "ArtifactType": "repository", + "Results": [ + { + "Target": "gradle.lockfile", + "Class": "lang-pkgs", + "Type": "gradle", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-9548", + "PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1", + "PkgName": "com.fasterxml.jackson.core:jackson-databind", + "PkgIdentifier": { + "PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "UID": "5ccdadc1617a1fee" + }, + "InstalledVersion": "2.9.1", + "FixedVersion": "2.9.10.4", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-9548", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Maven", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven" + }, + "Fingerprint": "sha256:cdb3d2e8310a150c61839c59650fbd98a5b07d4b1725cf7544b9f47e11dcca92", + "Title": "jackson-databind: Serialization gadgets in anteros-core", + "Description": "FasterXML jackson-databind 2.x before 2.9.10.4 mishandles the interaction between serialization gadgets and typing, related to br.com.anteros.dbcp.AnterosDBCPConfig (aka anteros-core).", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 4, + "nvd": 4, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 6.8, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2020-9548", + "https://github.com/FasterXML/jackson-databind/issues/2634", + "https://github.com/advisories/GHSA-p43x-xfjf-5jhr", + "https://lists.apache.org/thread.html/r35d30db00440ef63b791c4b7f7acb036e14d4a23afa2a249cb66c0fd@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/r9464a40d25c3ba1a55622db72f113eb494a889656962d098c70c5bb1@%3Cdev.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/r98c9b6e4c9e17792e2cd1ec3e4aa20b61a791939046d3f10888176bb@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rb6fecb5e96a6d61e175ff49f33f2713798dd05cf03067c169d195596@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rd5a4457be4623038c3989294429bc063eec433a2e55995d81591e2ca@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rdd49ab9565bec436a896bc00c4b9fc9dce1598e106c318524fbdfec6@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rdd4df698d5d8e635144d2994922bf0842e933809eae259521f3b5097@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rf1bbc0ea4a9f014cf94df9a12a6477d24a27f52741dbc87f2fd52ff2@%3Cissues.geode.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2020/03/msg00008.html", + "https://medium.com/@cowtowncoder/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062", + "https://nvd.nist.gov/vuln/detail/CVE-2020-9548", + "https://security.netapp.com/advisory/ntap-20200904-0006/", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html" + ], + "PublishedDate": "2020-03-02T04:15:00Z", + "LastModifiedDate": "2021-12-02T21:23:00Z" + }, + { + "VulnerabilityID": "CVE-2021-20190", + "PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1", + "PkgName": "com.fasterxml.jackson.core:jackson-databind", + "PkgIdentifier": { + "PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "UID": "5ccdadc1617a1fee" + }, + "InstalledVersion": "2.9.1", + "FixedVersion": "2.9.10.7", + "Status": "fixed", + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-20190", + "DataSource": { + "ID": "glad", + "Name": "GitLab Advisory Database Community", + "URL": "https://gitlab.com/gitlab-org/advisories-community" + }, + "Fingerprint": "sha256:db114c2fd4b8fd2872a737aa11bb5347f570c90793aeb00940421a514cfca41a", + "Title": "jackson-databind: mishandles the interaction between serialization gadgets and typing, related to javax.swing", + "Description": "A flaw was found in jackson-databind before 2.9.10.7. FasterXML mishandles the interaction between serialization gadgets and typing. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 3, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:C", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 8.3, + "V3Score": 8.1 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2021-20190", + "https://bugzilla.redhat.com/show_bug.cgi?id=1916633", + "https://github.com/FasterXML/jackson-databind/commit/7dbf51bf78d157098074a20bd9da39bd48c18e4a", + "https://github.com/FasterXML/jackson-databind/issues/2854", + "https://github.com/advisories/GHSA-5949-rw7g-wx7w", + "https://lists.apache.org/thread.html/r380e9257bacb8551ee6fcf2c59890ae9477b2c78e553fa9ea08e9d9a@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2021/04/msg00025.html", + "https://nvd.nist.gov/vuln/detail/CVE-2021-20190", + "https://security.netapp.com/advisory/ntap-20210219-0008/" + ], + "PublishedDate": "2021-01-19T17:15:00Z", + "LastModifiedDate": "2021-07-20T23:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/helm.json.golden b/integration/testdata/helm.json.golden new file mode 100644 index 000000000000..a225b9aae99f --- /dev/null +++ b/integration/testdata/helm.json.golden @@ -0,0 +1,1204 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/helm", + "ArtifactType": "repository", + "Results": [ + { + "Target": "testchart.tar.gz:templates/pod.yaml", + "Class": "config", + "Type": "helm", + "MisconfSummary": { + "Successes": 81, + "Failures": 18 + }, + "Misconfigurations": [ + { + "Type": "Helm Security Check", + "ID": "KSV-0001", + "Title": "Can elevate its own privileges", + "Description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should set 'securityContext.allowPrivilegeEscalation' to false", + "Namespace": "builtin.kubernetes.KSV001", + "Query": "data.builtin.kubernetes.KSV001.deny", + "Resolution": "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0001", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv-0001" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0003", + "Title": "Default capabilities: some containers do not drop all", + "Description": "The container should drop all default capabilities and add only those that are needed for its execution.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should add 'ALL' to 'securityContext.capabilities.drop'", + "Namespace": "builtin.kubernetes.KSV003", + "Query": "data.builtin.kubernetes.KSV003.deny", + "Resolution": "Add 'ALL' to containers[].securityContext.capabilities.drop.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0003", + "References": [ + "https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/", + "https://avd.aquasec.com/misconfig/ksv-0003" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0004", + "Title": "Default capabilities: some containers do not drop any", + "Description": "Security best practices require containers to run with minimal required capabilities.", + "Message": "Container 'nginx' of 'deployment' 'nginx-deployment' in 'default' namespace should set securityContext.capabilities.drop", + "Namespace": "builtin.kubernetes.KSV004", + "Query": "data.builtin.kubernetes.KSV004.deny", + "Resolution": "Specify at least one unneeded capability in 'containers[].securityContext.capabilities.drop'", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0004", + "References": [ + "https://kubesec.io/basics/containers-securitycontext-capabilities-drop-index-all/", + "https://avd.aquasec.com/misconfig/ksv-0004" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0011", + "Title": "CPU not limited", + "Description": "Enforcing CPU limits prevents DoS via resource exhaustion.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should set 'resources.limits.cpu'", + "Namespace": "builtin.kubernetes.KSV011", + "Query": "data.builtin.kubernetes.KSV011.deny", + "Resolution": "Set a limit value under 'containers[].resources.limits.cpu'.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0011", + "References": [ + "https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits", + "https://avd.aquasec.com/misconfig/ksv-0011" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0012", + "Title": "Runs as root user", + "Description": "Force the running image to run as a non-root user to ensure least privileges.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should set 'securityContext.runAsNonRoot' to true", + "Namespace": "builtin.kubernetes.KSV012", + "Query": "data.builtin.kubernetes.KSV012.deny", + "Resolution": "Set 'containers[].securityContext.runAsNonRoot' to true.", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0012", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv-0012" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0014", + "Title": "Root file system is not read-only", + "Description": "An immutable root file system prevents applications from writing to their local disk. This can limit intrusions, as attackers will not be able to tamper with the file system or write foreign executables to disk.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should set 'securityContext.readOnlyRootFilesystem' to true", + "Namespace": "builtin.kubernetes.KSV014", + "Query": "data.builtin.kubernetes.KSV014.deny", + "Resolution": "Change 'containers[].securityContext.readOnlyRootFilesystem' to 'true'.", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0014", + "References": [ + "https://kubesec.io/basics/containers-securitycontext-readonlyrootfilesystem-true/", + "https://avd.aquasec.com/misconfig/ksv-0014" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0015", + "Title": "CPU requests not specified", + "Description": "When containers have resource requests specified, the scheduler can make better decisions about which nodes to place pods on, and how to deal with resource contention.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should set 'resources.requests.cpu'", + "Namespace": "builtin.kubernetes.KSV015", + "Query": "data.builtin.kubernetes.KSV015.deny", + "Resolution": "Set 'containers[].resources.requests.cpu'.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0015", + "References": [ + "https://cloud.google.com/blog/products/containers-kubernetes/kubernetes-best-practices-resource-requests-and-limits", + "https://avd.aquasec.com/misconfig/ksv-0015" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0016", + "Title": "Memory requests not specified", + "Description": "When containers have memory requests specified, the scheduler can make better decisions about which nodes to place pods on, and how to deal with resource contention.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should set 'resources.requests.memory'", + "Namespace": "builtin.kubernetes.KSV016", + "Query": "data.builtin.kubernetes.KSV016.deny", + "Resolution": "Set 'containers[].resources.requests.memory'.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0016", + "References": [ + "https://kubesec.io/basics/containers-resources-limits-memory/", + "https://avd.aquasec.com/misconfig/ksv-0016" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0018", + "Title": "Memory not limited", + "Description": "Enforcing memory limits prevents DoS via resource exhaustion.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should set 'resources.limits.memory'", + "Namespace": "builtin.kubernetes.KSV018", + "Query": "data.builtin.kubernetes.KSV018.deny", + "Resolution": "Set a limit value under 'containers[].resources.limits.memory'.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0018", + "References": [ + "https://kubesec.io/basics/containers-resources-limits-memory/", + "https://avd.aquasec.com/misconfig/ksv-0018" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0020", + "Title": "Runs with UID \u003c= 10000", + "Description": "Force the container to run with user ID \u003e 10000 to avoid conflicts with the host’s user table.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should set 'securityContext.runAsUser' \u003e 10000", + "Namespace": "builtin.kubernetes.KSV020", + "Query": "data.builtin.kubernetes.KSV020.deny", + "Resolution": "Set 'containers[].securityContext.runAsUser' to an integer \u003e 10000.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0020", + "References": [ + "https://kubesec.io/basics/containers-securitycontext-runasuser/", + "https://avd.aquasec.com/misconfig/ksv-0020" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0021", + "Title": "Runs with GID \u003c= 10000", + "Description": "Force the container to run with group ID \u003e 10000 to avoid conflicts with the host’s user table.", + "Message": "Container 'nginx' of Deployment 'nginx-deployment' should set 'securityContext.runAsGroup' \u003e 10000", + "Namespace": "builtin.kubernetes.KSV021", + "Query": "data.builtin.kubernetes.KSV021.deny", + "Resolution": "Set 'containers[].securityContext.runAsGroup' to an integer \u003e 10000.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0021", + "References": [ + "https://kubesec.io/basics/containers-securitycontext-runasuser/", + "https://avd.aquasec.com/misconfig/ksv-0021" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0030", + "Title": "Runtime/Default Seccomp profile not set", + "Description": "According to pod security standard 'Seccomp', the RuntimeDefault seccomp profile must be required, or allow specific additional profiles.", + "Message": "Either Pod or Container should set 'securityContext.seccompProfile.type' to 'RuntimeDefault'", + "Namespace": "builtin.kubernetes.KSV030", + "Query": "data.builtin.kubernetes.KSV030.deny", + "Resolution": "Set 'spec.securityContext.seccompProfile.type', 'spec.containers[*].securityContext.seccompProfile' and 'spec.initContainers[*].securityContext.seccompProfile' to 'RuntimeDefault' or undefined.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0030", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv-0030" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0104", + "Title": "Seccomp policies disabled", + "Description": "A program inside the container can bypass Seccomp protection policies.", + "Message": "container \"nginx\" of deployment \"nginx-deployment\" in \"default\" namespace should specify a seccomp profile", + "Namespace": "builtin.kubernetes.KSV104", + "Query": "data.builtin.kubernetes.KSV104.deny", + "Resolution": "Specify seccomp either by annotation or by seccomp profile type having allowed values as per pod security standards", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0104", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", + "https://avd.aquasec.com/misconfig/ksv-0104" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0106", + "Title": "Container capabilities must only include NET_BIND_SERVICE", + "Description": "Containers must drop ALL capabilities, and are only permitted to add back the NET_BIND_SERVICE capability.", + "Message": "container should drop all", + "Namespace": "builtin.kubernetes.KSV106", + "Query": "data.builtin.kubernetes.KSV106.deny", + "Resolution": "Set 'spec.containers[*].securityContext.capabilities.drop' to 'ALL' and only add 'NET_BIND_SERVICE' to 'spec.containers[*].securityContext.capabilities.add'.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0106", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv-0106" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0110", + "Title": "Workloads in the default namespace", + "Description": "Checks whether a workload is running in the default namespace.", + "Message": "deployment nginx-deployment in default namespace should set metadata.namespace to a non-default namespace", + "Namespace": "builtin.kubernetes.KSV110", + "Query": "data.builtin.kubernetes.KSV110.deny", + "Resolution": "Set 'metadata.namespace' to a non-default namespace.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0110", + "References": [ + "https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", + "https://avd.aquasec.com/misconfig/ksv-0110" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 4, + "EndLine": 7, + "Code": { + "Lines": [ + { + "Number": 4, + "Content": "metadata:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mmetadata\u001b[0m:", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 5, + "Content": " name: nginx-deployment", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mname\u001b[0m: nginx-deployment", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 6, + "Content": " labels:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mlabels\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 7, + "Content": " app: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mapp\u001b[0m: nginx", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0117", + "Title": "Prevent binding to privileged ports", + "Description": "The ports which are lower than 1024 receive and transmit various sensitive and privileged data. Allowing containers to use them can bring serious implications.", + "Message": "deployment nginx-deployment in default namespace should not set spec.template.spec.containers.ports.containerPort to less than 1024", + "Namespace": "builtin.kubernetes.KSV117", + "Query": "data.builtin.kubernetes.KSV117.deny", + "Resolution": "Do not map the container ports to privileged host ports when starting a container.", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0117", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "https://www.stigviewer.com/stig/kubernetes/2022-12-02/finding/V-242414", + "https://avd.aquasec.com/misconfig/ksv-0117" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general" + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0118", + "Title": "Default security context configured", + "Description": "Security context controls the allocation of security parameters for the pod/container/volume, ensuring the appropriate level of protection. Relying on default security context may expose vulnerabilities to potential attacks that rely on privileged access.", + "Message": "container nginx-deployment in default namespace is using the default security context", + "Namespace": "builtin.kubernetes.KSV118", + "Query": "data.builtin.kubernetes.KSV118.deny", + "Resolution": "To enhance security, it is strongly recommended not to rely on the default security context. Instead, it is advisable to explicitly define the required security parameters (such as runAsNonRoot, capabilities, readOnlyRootFilesystem, etc.) within the security context.", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0118", + "References": [ + "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", + "https://avd.aquasec.com/misconfig/ksv-0118" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 19, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0118", + "Title": "Default security context configured", + "Description": "Security context controls the allocation of security parameters for the pod/container/volume, ensuring the appropriate level of protection. Relying on default security context may expose vulnerabilities to potential attacks that rely on privileged access.", + "Message": "deployment nginx-deployment in default namespace is using the default security context, which allows root privileges", + "Namespace": "builtin.kubernetes.KSV118", + "Query": "data.builtin.kubernetes.KSV118.deny", + "Resolution": "To enhance security, it is strongly recommended not to rely on the default security context. Instead, it is advisable to explicitly define the required security parameters (such as runAsNonRoot, capabilities, readOnlyRootFilesystem, etc.) within the security context.", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0118", + "References": [ + "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", + "https://avd.aquasec.com/misconfig/ksv-0118" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 17, + "EndLine": 22, + "Code": { + "Lines": [ + { + "Number": 17, + "Content": " spec:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mspec\u001b[0m:", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 18, + "Content": " containers:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcontainers\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 19, + "Content": " - name: nginx", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: nginx", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 20, + "Content": " image: nginx:1.14.2", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mimage\u001b[0m: nginx:1.14.2", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 21, + "Content": " ports:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mports\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 22, + "Content": " - containerPort: 80", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mcontainerPort\u001b[0m: \u001b[38;5;37m80\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + } + ] + } + ] +} diff --git a/integration/testdata/helm_badname.json.golden b/integration/testdata/helm_badname.json.golden new file mode 100644 index 000000000000..f6350fc7e852 --- /dev/null +++ b/integration/testdata/helm_badname.json.golden @@ -0,0 +1,10 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/helm_badname", + "ArtifactType": "repository" +} diff --git a/integration/testdata/helm_testchart.json.golden b/integration/testdata/helm_testchart.json.golden new file mode 100644 index 000000000000..dd138e51d315 --- /dev/null +++ b/integration/testdata/helm_testchart.json.golden @@ -0,0 +1,674 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/helm_testchart", + "ArtifactType": "repository", + "Results": [ + { + "Target": "templates/deployment.yaml", + "Class": "config", + "Type": "helm", + "MisconfSummary": { + "Successes": 92, + "Failures": 6 + }, + "Misconfigurations": [ + { + "Type": "Helm Security Check", + "ID": "KSV-0001", + "Title": "Can elevate its own privileges", + "Description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.", + "Message": "Container 'testchart' of Deployment 'testchart' should set 'securityContext.allowPrivilegeEscalation' to false", + "Namespace": "builtin.kubernetes.KSV001", + "Query": "data.builtin.kubernetes.KSV001.deny", + "Resolution": "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0001", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv-0001" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 28, + "EndLine": 57, + "Code": { + "Lines": [ + { + "Number": 28, + "Content": " - name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": " - ALL", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - ALL", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " readOnlyRootFilesystem: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mreadOnlyRootFilesystem\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 34, + "Content": " runAsGroup: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsGroup\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 35, + "Content": " runAsNonRoot: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsNonRoot\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 36, + "Content": " runAsUser: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsUser\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 37, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0030", + "Title": "Runtime/Default Seccomp profile not set", + "Description": "According to pod security standard 'Seccomp', the RuntimeDefault seccomp profile must be required, or allow specific additional profiles.", + "Message": "Either Pod or Container should set 'securityContext.seccompProfile.type' to 'RuntimeDefault'", + "Namespace": "builtin.kubernetes.KSV030", + "Query": "data.builtin.kubernetes.KSV030.deny", + "Resolution": "Set 'spec.securityContext.seccompProfile.type', 'spec.containers[*].securityContext.seccompProfile' and 'spec.initContainers[*].securityContext.seccompProfile' to 'RuntimeDefault' or undefined.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0030", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv-0030" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 28, + "EndLine": 57, + "Code": { + "Lines": [ + { + "Number": 28, + "Content": " - name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": " - ALL", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - ALL", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " readOnlyRootFilesystem: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mreadOnlyRootFilesystem\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 34, + "Content": " runAsGroup: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsGroup\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 35, + "Content": " runAsNonRoot: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsNonRoot\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 36, + "Content": " runAsUser: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsUser\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 37, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0104", + "Title": "Seccomp policies disabled", + "Description": "A program inside the container can bypass Seccomp protection policies.", + "Message": "container \"testchart\" of deployment \"testchart\" in \"default\" namespace should specify a seccomp profile", + "Namespace": "builtin.kubernetes.KSV104", + "Query": "data.builtin.kubernetes.KSV104.deny", + "Resolution": "Specify seccomp either by annotation or by seccomp profile type having allowed values as per pod security standards", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0104", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", + "https://avd.aquasec.com/misconfig/ksv-0104" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 28, + "EndLine": 57, + "Code": { + "Lines": [ + { + "Number": 28, + "Content": " - name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": " - ALL", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - ALL", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " readOnlyRootFilesystem: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mreadOnlyRootFilesystem\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 34, + "Content": " runAsGroup: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsGroup\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 35, + "Content": " runAsNonRoot: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsNonRoot\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 36, + "Content": " runAsUser: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsUser\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 37, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0110", + "Title": "Workloads in the default namespace", + "Description": "Checks whether a workload is running in the default namespace.", + "Message": "deployment testchart in default namespace should set metadata.namespace to a non-default namespace", + "Namespace": "builtin.kubernetes.KSV110", + "Query": "data.builtin.kubernetes.KSV110.deny", + "Resolution": "Set 'metadata.namespace' to a non-default namespace.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0110", + "References": [ + "https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", + "https://avd.aquasec.com/misconfig/ksv-0110" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 4, + "EndLine": 11, + "Code": { + "Lines": [ + { + "Number": 4, + "Content": "metadata:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mmetadata\u001b[0m:", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 5, + "Content": " name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 6, + "Content": " labels:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mlabels\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 7, + "Content": " helm.sh/chart: testchart-0.1.0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mhelm.sh/chart\u001b[0m: testchart-0.1.0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 8, + "Content": " app.kubernetes.io/name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mapp.kubernetes.io/name\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 9, + "Content": " app.kubernetes.io/instance: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mapp.kubernetes.io/instance\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 10, + "Content": " app.kubernetes.io/version: \"1.16.0\"", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mapp.kubernetes.io/version\u001b[0m: \u001b[38;5;37m\"1.16.0\"", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 11, + "Content": " app.kubernetes.io/managed-by: Helm", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mapp.kubernetes.io/managed-by\u001b[0m: Helm", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0117", + "Title": "Prevent binding to privileged ports", + "Description": "The ports which are lower than 1024 receive and transmit various sensitive and privileged data. Allowing containers to use them can bring serious implications.", + "Message": "deployment testchart in default namespace should not set spec.template.spec.containers.ports.containerPort to less than 1024", + "Namespace": "builtin.kubernetes.KSV117", + "Query": "data.builtin.kubernetes.KSV117.deny", + "Resolution": "Do not map the container ports to privileged host ports when starting a container.", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0117", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "https://www.stigviewer.com/stig/kubernetes/2022-12-02/finding/V-242414", + "https://avd.aquasec.com/misconfig/ksv-0117" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general" + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0118", + "Title": "Default security context configured", + "Description": "Security context controls the allocation of security parameters for the pod/container/volume, ensuring the appropriate level of protection. Relying on default security context may expose vulnerabilities to potential attacks that rely on privileged access.", + "Message": "deployment testchart in default namespace is using the default security context, which allows root privileges", + "Namespace": "builtin.kubernetes.KSV118", + "Query": "data.builtin.kubernetes.KSV118.deny", + "Resolution": "To enhance security, it is strongly recommended not to rely on the default security context. Instead, it is advisable to explicitly define the required security parameters (such as runAsNonRoot, capabilities, readOnlyRootFilesystem, etc.) within the security context.", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0118", + "References": [ + "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", + "https://avd.aquasec.com/misconfig/ksv-0118" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 23, + "EndLine": 57, + "Code": { + "Lines": [ + { + "Number": 23, + "Content": " spec:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mspec\u001b[0m:", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 24, + "Content": " serviceAccountName: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mserviceAccountName\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 25, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 26, + "Content": " {}", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " {}", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": " containers:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcontainers\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " - name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 32, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + } + } + } + ] + }, + { + "Target": "templates/service.yaml", + "Class": "config", + "Type": "helm", + "MisconfSummary": { + "Successes": 62, + "Failures": 0 + } + }, + { + "Target": "templates/serviceaccount.yaml", + "Class": "config", + "Type": "helm", + "MisconfSummary": { + "Successes": 61, + "Failures": 0 + } + } + ] +} diff --git a/integration/testdata/helm_testchart.overridden.json.golden b/integration/testdata/helm_testchart.overridden.json.golden new file mode 100644 index 000000000000..b2d46a8b74fa --- /dev/null +++ b/integration/testdata/helm_testchart.overridden.json.golden @@ -0,0 +1,907 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/helm_testchart", + "ArtifactType": "repository", + "Results": [ + { + "Target": "templates/deployment.yaml", + "Class": "config", + "Type": "helm", + "MisconfSummary": { + "Successes": 90, + "Failures": 8 + }, + "Misconfigurations": [ + { + "Type": "Helm Security Check", + "ID": "KSV-0001", + "Title": "Can elevate its own privileges", + "Description": "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.", + "Message": "Container 'testchart' of Deployment 'testchart' should set 'securityContext.allowPrivilegeEscalation' to false", + "Namespace": "builtin.kubernetes.KSV001", + "Query": "data.builtin.kubernetes.KSV001.deny", + "Resolution": "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0001", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv-0001" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 28, + "EndLine": 57, + "Code": { + "Lines": [ + { + "Number": 28, + "Content": " - name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": " - ALL", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - ALL", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " readOnlyRootFilesystem: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mreadOnlyRootFilesystem\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 34, + "Content": " runAsGroup: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsGroup\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 35, + "Content": " runAsNonRoot: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsNonRoot\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 36, + "Content": " runAsUser: 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsUser\u001b[0m: \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 37, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0020", + "Title": "Runs with UID \u003c= 10000", + "Description": "Force the container to run with user ID \u003e 10000 to avoid conflicts with the host’s user table.", + "Message": "Container 'testchart' of Deployment 'testchart' should set 'securityContext.runAsUser' \u003e 10000", + "Namespace": "builtin.kubernetes.KSV020", + "Query": "data.builtin.kubernetes.KSV020.deny", + "Resolution": "Set 'containers[].securityContext.runAsUser' to an integer \u003e 10000.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0020", + "References": [ + "https://kubesec.io/basics/containers-securitycontext-runasuser/", + "https://avd.aquasec.com/misconfig/ksv-0020" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 28, + "EndLine": 57, + "Code": { + "Lines": [ + { + "Number": 28, + "Content": " - name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": " - ALL", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - ALL", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " readOnlyRootFilesystem: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mreadOnlyRootFilesystem\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 34, + "Content": " runAsGroup: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsGroup\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 35, + "Content": " runAsNonRoot: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsNonRoot\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 36, + "Content": " runAsUser: 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsUser\u001b[0m: \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 37, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0030", + "Title": "Runtime/Default Seccomp profile not set", + "Description": "According to pod security standard 'Seccomp', the RuntimeDefault seccomp profile must be required, or allow specific additional profiles.", + "Message": "Either Pod or Container should set 'securityContext.seccompProfile.type' to 'RuntimeDefault'", + "Namespace": "builtin.kubernetes.KSV030", + "Query": "data.builtin.kubernetes.KSV030.deny", + "Resolution": "Set 'spec.securityContext.seccompProfile.type', 'spec.containers[*].securityContext.seccompProfile' and 'spec.initContainers[*].securityContext.seccompProfile' to 'RuntimeDefault' or undefined.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0030", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv-0030" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 28, + "EndLine": 57, + "Code": { + "Lines": [ + { + "Number": 28, + "Content": " - name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": " - ALL", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - ALL", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " readOnlyRootFilesystem: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mreadOnlyRootFilesystem\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 34, + "Content": " runAsGroup: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsGroup\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 35, + "Content": " runAsNonRoot: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsNonRoot\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 36, + "Content": " runAsUser: 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsUser\u001b[0m: \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 37, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0104", + "Title": "Seccomp policies disabled", + "Description": "A program inside the container can bypass Seccomp protection policies.", + "Message": "container \"testchart\" of deployment \"testchart\" in \"default\" namespace should specify a seccomp profile", + "Namespace": "builtin.kubernetes.KSV104", + "Query": "data.builtin.kubernetes.KSV104.deny", + "Resolution": "Specify seccomp either by annotation or by seccomp profile type having allowed values as per pod security standards", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0104", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#baseline", + "https://avd.aquasec.com/misconfig/ksv-0104" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 28, + "EndLine": 57, + "Code": { + "Lines": [ + { + "Number": 28, + "Content": " - name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": " - ALL", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - ALL", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " readOnlyRootFilesystem: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mreadOnlyRootFilesystem\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 34, + "Content": " runAsGroup: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsGroup\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 35, + "Content": " runAsNonRoot: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsNonRoot\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 36, + "Content": " runAsUser: 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsUser\u001b[0m: \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 37, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0105", + "Title": "Containers must not set runAsUser to 0", + "Description": "Containers should be forbidden from running with a root UID.", + "Message": "securityContext.runAsUser should be set to a value greater than 0", + "Namespace": "builtin.kubernetes.KSV105", + "Query": "data.builtin.kubernetes.KSV105.deny", + "Resolution": "Set 'securityContext.runAsUser' to a non-zero integer or leave undefined.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0105", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv-0105" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 29, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": " - ALL", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - ALL", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " readOnlyRootFilesystem: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mreadOnlyRootFilesystem\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 34, + "Content": " runAsGroup: 10001", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsGroup\u001b[0m: \u001b[38;5;37m10001", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 35, + "Content": " runAsNonRoot: true", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsNonRoot\u001b[0m: \u001b[38;5;166mtrue", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 36, + "Content": " runAsUser: 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mrunAsUser\u001b[0m: \u001b[38;5;37m0\u001b[0m", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0110", + "Title": "Workloads in the default namespace", + "Description": "Checks whether a workload is running in the default namespace.", + "Message": "deployment testchart in default namespace should set metadata.namespace to a non-default namespace", + "Namespace": "builtin.kubernetes.KSV110", + "Query": "data.builtin.kubernetes.KSV110.deny", + "Resolution": "Set 'metadata.namespace' to a non-default namespace.", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0110", + "References": [ + "https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", + "https://avd.aquasec.com/misconfig/ksv-0110" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 4, + "EndLine": 11, + "Code": { + "Lines": [ + { + "Number": 4, + "Content": "metadata:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mmetadata\u001b[0m:", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 5, + "Content": " name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 6, + "Content": " labels:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mlabels\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 7, + "Content": " helm.sh/chart: testchart-0.1.0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mhelm.sh/chart\u001b[0m: testchart-0.1.0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 8, + "Content": " app.kubernetes.io/name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mapp.kubernetes.io/name\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 9, + "Content": " app.kubernetes.io/instance: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mapp.kubernetes.io/instance\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 10, + "Content": " app.kubernetes.io/version: \"1.16.0\"", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mapp.kubernetes.io/version\u001b[0m: \u001b[38;5;37m\"1.16.0\"", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 11, + "Content": " app.kubernetes.io/managed-by: Helm", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;33mapp.kubernetes.io/managed-by\u001b[0m: Helm", + "FirstCause": false, + "LastCause": true + } + ] + } + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0117", + "Title": "Prevent binding to privileged ports", + "Description": "The ports which are lower than 1024 receive and transmit various sensitive and privileged data. Allowing containers to use them can bring serious implications.", + "Message": "deployment testchart in default namespace should not set spec.template.spec.containers.ports.containerPort to less than 1024", + "Namespace": "builtin.kubernetes.KSV117", + "Query": "data.builtin.kubernetes.KSV117.deny", + "Resolution": "Do not map the container ports to privileged host ports when starting a container.", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0117", + "References": [ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/", + "https://www.stigviewer.com/stig/kubernetes/2022-12-02/finding/V-242414", + "https://avd.aquasec.com/misconfig/ksv-0117" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general" + } + }, + { + "Type": "Helm Security Check", + "ID": "KSV-0118", + "Title": "Default security context configured", + "Description": "Security context controls the allocation of security parameters for the pod/container/volume, ensuring the appropriate level of protection. Relying on default security context may expose vulnerabilities to potential attacks that rely on privileged access.", + "Message": "deployment testchart in default namespace is using the default security context, which allows root privileges", + "Namespace": "builtin.kubernetes.KSV118", + "Query": "data.builtin.kubernetes.KSV118.deny", + "Resolution": "To enhance security, it is strongly recommended not to rely on the default security context. Instead, it is advisable to explicitly define the required security parameters (such as runAsNonRoot, capabilities, readOnlyRootFilesystem, etc.) within the security context.", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/ksv-0118", + "References": [ + "https://kubernetes.io/docs/tasks/configure-pod-container/security-context/", + "https://avd.aquasec.com/misconfig/ksv-0118" + ], + "Status": "FAIL", + "CauseMetadata": { + "Provider": "Kubernetes", + "Service": "general", + "StartLine": 23, + "EndLine": 57, + "Code": { + "Lines": [ + { + "Number": 23, + "Content": " spec:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mspec\u001b[0m:", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 24, + "Content": " serviceAccountName: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mserviceAccountName\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 25, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 26, + "Content": " {}", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " {}", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": " containers:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcontainers\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " - name: testchart", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " - \u001b[38;5;33mname\u001b[0m: testchart", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": " securityContext:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33msecurityContext\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " capabilities:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mcapabilities\u001b[0m:", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " drop:", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;33mdrop\u001b[0m:", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 32, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + } + } + } + ] + }, + { + "Target": "templates/service.yaml", + "Class": "config", + "Type": "helm", + "MisconfSummary": { + "Successes": 62, + "Failures": 0 + } + }, + { + "Target": "templates/serviceaccount.yaml", + "Class": "config", + "Type": "helm", + "MisconfSummary": { + "Successes": 61, + "Failures": 0 + } + } + ] +} diff --git a/integration/testdata/julia-spdx.json.golden b/integration/testdata/julia-spdx.json.golden new file mode 100644 index 000000000000..61c6ef95830f --- /dev/null +++ b/integration/testdata/julia-spdx.json.golden @@ -0,0 +1,178 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "testdata/fixtures/repo/julia", + "documentNamespace": "http://trivy.dev/filesystem/testdata/fixtures/repo/julia-3ff14136-e09f-4df9-80ea-000000000007", + "creationInfo": { + "creators": [ + "Organization: aquasecurity", + "Tool: trivy-dev" + ], + "created": "2021-08-25T12:20:30Z" + }, + "packages": [ + { + "name": "Manifest.toml", + "SPDXID": "SPDXRef-Application-c39d15beb6bdf085", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "primaryPackagePurpose": "APPLICATION", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "Class: lang-pkgs" + }, + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "Type: julia" + } + ] + }, + { + "name": "A", + "SPDXID": "SPDXRef-Package-87b4e00fb433805d", + "versionInfo": "1.9.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "sourceInfo": "package found in: Manifest.toml", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:julia/A@1.9.0?uuid=ead4f63c-334e-11e9-00e6-e7f0a5f21b60" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "PkgID: ead4f63c-334e-11e9-00e6-e7f0a5f21b60" + }, + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "PkgType: julia" + } + ] + }, + { + "name": "B", + "SPDXID": "SPDXRef-Package-32e95bcb6c889958", + "versionInfo": "1.9.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "sourceInfo": "package found in: Manifest.toml", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:julia/B@1.9.0?uuid=f41f7b98-334e-11e9-1257-49272045fb24" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "PkgID: f41f7b98-334e-11e9-1257-49272045fb24" + }, + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "PkgType: julia" + } + ] + }, + { + "name": "B", + "SPDXID": "SPDXRef-Package-789679ef84876c8d", + "versionInfo": "1.9.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "sourceInfo": "package found in: Manifest.toml", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:julia/B@1.9.0?uuid=edca9bc6-334e-11e9-3554-9595dbb4349c" + } + ], + "primaryPackagePurpose": "LIBRARY", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "PkgID: edca9bc6-334e-11e9-3554-9595dbb4349c" + }, + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "PkgType: julia" + } + ] + }, + { + "name": "testdata/fixtures/repo/julia", + "SPDXID": "SPDXRef-Filesystem-1be792dd0077c431", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "primaryPackagePurpose": "SOURCE", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2021-08-25T12:20:30Z", + "annotationType": "OTHER", + "comment": "SchemaVersion: 2" + } + ] + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Application-c39d15beb6bdf085", + "relatedSpdxElement": "SPDXRef-Package-789679ef84876c8d", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Application-c39d15beb6bdf085", + "relatedSpdxElement": "SPDXRef-Package-87b4e00fb433805d", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-Filesystem-1be792dd0077c431", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-Filesystem-1be792dd0077c431", + "relatedSpdxElement": "SPDXRef-Application-c39d15beb6bdf085", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-87b4e00fb433805d", + "relatedSpdxElement": "SPDXRef-Package-32e95bcb6c889958", + "relationshipType": "DEPENDS_ON" + } + ] +} \ No newline at end of file diff --git a/integration/testdata/license-cyclonedx.json.golden b/integration/testdata/license-cyclonedx.json.golden new file mode 100644 index 000000000000..768b932dee07 --- /dev/null +++ b/integration/testdata/license-cyclonedx.json.golden @@ -0,0 +1,48 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/sbom/license-cyclonedx.json", + "ArtifactType": "cyclonedx", + "Results": [ + { + "Target": "Java", + "Class": "license", + "Licenses": [ + { + "Severity": "MEDIUM", + "Category": "reciprocal", + "PkgName": "org.eclipse.sisu:org.eclipse.sisu.plexus", + "FilePath": "", + "Name": "EPL-1.0", + "Text": "", + "Confidence": 1, + "Link": "" + }, + { + "Severity": "LOW", + "Category": "notice", + "PkgName": "org.ow2.asm:asm", + "FilePath": "", + "Name": "BSD-3-Clause", + "Text": "", + "Confidence": 1, + "Link": "" + }, + { + "Severity": "LOW", + "Category": "notice", + "PkgName": "org.slf4j:slf4j-api", + "FilePath": "", + "Name": "MIT License", + "Text": "", + "Confidence": 1, + "Link": "" + } + ] + } + ] +} diff --git a/integration/testdata/mariner-1.0.json.golden b/integration/testdata/mariner-1.0.json.golden new file mode 100644 index 000000000000..5f88dc39d606 --- /dev/null +++ b/integration/testdata/mariner-1.0.json.golden @@ -0,0 +1,156 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:357e30db7fb673e279ababa1128b33c8acc2fce50826727ec57ef24f5213fe0d", + "ArtifactName": "testdata/fixtures/images/mariner-1.0.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 177582080, + "OS": { + "Family": "cbl-mariner", + "Name": "1.0" + }, + "ImageID": "sha256:8cdcbf18341ed8afa5322e7b0077f8ef3f46896882c921df5f97c51b369f6767", + "DiffIDs": [ + "sha256:4266328c97a194b2ca52ec83bc05496596303f5e9b244ffa99cf84763a487804" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:mariner-1.0" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:mariner-1.0", + "ImageConfig": { + "architecture": "amd64", + "created": "2022-01-27T01:19:38.526301656Z", + "docker_version": "20.10.12", + "history": [ + { + "created": "2022-01-27T01:19:38.526301656Z", + "comment": "Imported from -" + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:4266328c97a194b2ca52ec83bc05496596303f5e9b244ffa99cf84763a487804" + ] + }, + "config": {} + }, + "Layers": [ + { + "Size": 177582080, + "Digest": "sha256:3df36548ffbf2fa7319966e038058a3d2a922880009e535202546a6b250b9d57", + "DiffID": "sha256:4266328c97a194b2ca52ec83bc05496596303f5e9b244ffa99cf84763a487804" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/mariner-1.0.tar.gz (cbl-mariner 1.0)", + "Class": "os-pkgs", + "Type": "cbl-mariner", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-0261", + "PkgName": "vim", + "PkgIdentifier": { + "PURL": "pkg:rpm/cbl-mariner/vim@8.2.4081-1.cm1?arch=x86_64\u0026distro=cbl-mariner-1.0", + "UID": "63c8a1617938d7b" + }, + "InstalledVersion": "8.2.4081-1.cm1", + "Status": "affected", + "Layer": { + "Digest": "sha256:3df36548ffbf2fa7319966e038058a3d2a922880009e535202546a6b250b9d57", + "DiffID": "sha256:4266328c97a194b2ca52ec83bc05496596303f5e9b244ffa99cf84763a487804" + }, + "SeveritySource": "cbl-mariner", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-0261", + "DataSource": { + "ID": "cbl-mariner", + "Name": "CBL-Mariner Vulnerability Data", + "URL": "https://github.com/microsoft/CBL-MarinerVulnerabilityData" + }, + "Fingerprint": "sha256:734110094f55c15456105c9c03b2a591208a8675c5f37f89e1ee1ac67feb639f", + "Title": "CVE-2022-0261 affecting package vim 8.2.4081", + "Description": "Heap-based Buffer Overflow in GitHub repository vim/vim prior to 8.2.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-122" + ], + "VendorSeverity": { + "cbl-mariner": 3 + }, + "References": [ + "https://github.com/vim/vim/commit/9f8c304c8a390ade133bac29963dc8e56ab14cbc", + "https://huntr.dev/bounties/fa795954-8775-4f23-98c6-d4d4d3fe8a82", + "https://nvd.nist.gov/vuln/detail/CVE-2022-0261" + ], + "PublishedDate": "2022-01-18T16:15:00Z", + "LastModifiedDate": "2022-01-18T16:15:00Z" + }, + { + "VulnerabilityID": "CVE-2022-0158", + "PkgName": "vim", + "PkgIdentifier": { + "PURL": "pkg:rpm/cbl-mariner/vim@8.2.4081-1.cm1?arch=x86_64\u0026distro=cbl-mariner-1.0", + "UID": "63c8a1617938d7b" + }, + "InstalledVersion": "8.2.4081-1.cm1", + "FixedVersion": "8.2.4082-1.cm1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:3df36548ffbf2fa7319966e038058a3d2a922880009e535202546a6b250b9d57", + "DiffID": "sha256:4266328c97a194b2ca52ec83bc05496596303f5e9b244ffa99cf84763a487804" + }, + "SeveritySource": "cbl-mariner", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-0158", + "DataSource": { + "ID": "cbl-mariner", + "Name": "CBL-Mariner Vulnerability Data", + "URL": "https://github.com/microsoft/CBL-MarinerVulnerabilityData" + }, + "Fingerprint": "sha256:82a2bac425b237b1470e50583c70c71974812e56d734774897201c0a80256d82", + "Title": "vim: heap-based read buffer overflow in compile_get_env()", + "Description": "vim is vulnerable to Heap-based Buffer Overflow", + "Severity": "LOW", + "CweIDs": [ + "CWE-122" + ], + "VendorSeverity": { + "cbl-mariner": 1, + "nvd": 1, + "redhat": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N", + "V2Score": 4.3, + "V3Score": 3.3 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N", + "V3Score": 3.3 + } + }, + "References": [ + "http://www.openwall.com/lists/oss-security/2022/01/15/1", + "https://access.redhat.com/security/cve/CVE-2022-0158", + "https://github.com/vim/vim/commit/5f25c3855071bd7e26255c68bf458b1b5cf92f39", + "https://huntr.dev/bounties/ac5d7005-07c6-4a0a-b251-ba9cdbf6738b", + "https://huntr.dev/bounties/ac5d7005-07c6-4a0a-b251-ba9cdbf6738b/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/HD5S2FC2HF22A7XQXK2XXIR46EARVWIM/", + "https://nvd.nist.gov/vuln/detail/CVE-2022-0158" + ], + "PublishedDate": "2022-01-10T16:15:00Z", + "LastModifiedDate": "2022-01-15T16:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/minikube-kbom.json.golden b/integration/testdata/minikube-kbom.json.golden new file mode 100644 index 000000000000..9eeeb616f184 --- /dev/null +++ b/integration/testdata/minikube-kbom.json.golden @@ -0,0 +1,67 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/sbom/minikube-kbom.json", + "ArtifactType": "cyclonedx", + "Metadata": { + "OS": { + "Family": "ubuntu", + "Name": "22.04.2" + } + }, + "Results": [ + { + "Target": "testdata/fixtures/sbom/minikube-kbom.json (ubuntu 22.04.2)", + "Class": "os-pkgs", + "Type": "ubuntu" + }, + { + "Target": "Kubernetes", + "Class": "lang-pkgs", + "Type": "kubernetes", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2023-2431", + "PkgID": "k8s.io/kubelet@1.27.0", + "PkgName": "k8s.io/kubelet", + "PkgIdentifier": { + "PURL": "pkg:k8s/k8s.io%2Fkubelet@1.27.0", + "UID": "4cb15d0a98eeae67", + "BOMRef": "pkg:k8s/k8s.io%2Fkubelet@1.27.0" + }, + "InstalledVersion": "1.27.0", + "FixedVersion": "1.24.14, 1.25.9, 1.26.4, 1.27.1", + "Status": "fixed", + "SeveritySource": "k8s", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2023-2431", + "DataSource": { + "ID": "k8s", + "Name": "Official Kubernetes CVE Feed", + "URL": "https://kubernetes.io/docs/reference/issues-security/official-cve-feed/index.json" + }, + "Fingerprint": "sha256:65f2ed2ea3842a48189cbecd06b84ffa5b0eefc79e81032dcf99ce4e8d9fde46", + "Title": "Bypass of seccomp profile enforcement ", + "Description": "A security issue was discovered in Kubelet that allows pods to bypass the seccomp profile enforcement...", + "Severity": "LOW", + "VendorSeverity": { + "k8s": 1 + }, + "CVSS": { + "k8s": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:L/A:N", + "V3Score": 3.4 + } + }, + "References": [ + "https://github.com/kubernetes/kubernetes/issues/118690", + "https://www.cve.org/cverecord?id=CVE-2023-2431" + ] + } + ] + } + ] +} diff --git a/integration/testdata/mix.lock.json.golden b/integration/testdata/mix.lock.json.golden new file mode 100644 index 000000000000..c00da9354d2e --- /dev/null +++ b/integration/testdata/mix.lock.json.golden @@ -0,0 +1,221 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/mixlock", + "ArtifactType": "repository", + "Results": [ + { + "Target": "mix.lock", + "Class": "lang-pkgs", + "Type": "hex", + "Packages": [ + { + "ID": "castore@0.1.18", + "Name": "castore", + "Identifier": { + "PURL": "pkg:hex/castore@0.1.18", + "UID": "d527b7c47937d54a" + }, + "Version": "0.1.18", + "Locations": [ + { + "StartLine": 2, + "EndLine": 2 + } + ], + "AnalyzedBy": "mix-lock" + }, + { + "ID": "jason@1.4.0", + "Name": "jason", + "Identifier": { + "PURL": "pkg:hex/jason@1.4.0", + "UID": "61124fecb637123a" + }, + "Version": "1.4.0", + "Locations": [ + { + "StartLine": 3, + "EndLine": 3 + } + ], + "AnalyzedBy": "mix-lock" + }, + { + "ID": "phoenix@1.6.13", + "Name": "phoenix", + "Identifier": { + "PURL": "pkg:hex/phoenix@1.6.13", + "UID": "1ad66010cf0dd9cb" + }, + "Version": "1.6.13", + "Locations": [ + { + "StartLine": 4, + "EndLine": 4 + } + ], + "AnalyzedBy": "mix-lock" + }, + { + "ID": "phoenix_html@3.2.0", + "Name": "phoenix_html", + "Identifier": { + "PURL": "pkg:hex/phoenix_html@3.2.0", + "UID": "31ead881cd7a4cf7" + }, + "Version": "3.2.0", + "Locations": [ + { + "StartLine": 5, + "EndLine": 5 + } + ], + "AnalyzedBy": "mix-lock" + }, + { + "ID": "phoenix_pubsub@2.1.1", + "Name": "phoenix_pubsub", + "Identifier": { + "PURL": "pkg:hex/phoenix_pubsub@2.1.1", + "UID": "1127bf230c3815ad" + }, + "Version": "2.1.1", + "Locations": [ + { + "StartLine": 6, + "EndLine": 6 + } + ], + "AnalyzedBy": "mix-lock" + }, + { + "ID": "phoenix_template@1.0.0", + "Name": "phoenix_template", + "Identifier": { + "PURL": "pkg:hex/phoenix_template@1.0.0", + "UID": "38842fc89e8ee0eb" + }, + "Version": "1.0.0", + "Locations": [ + { + "StartLine": 7, + "EndLine": 7 + } + ], + "AnalyzedBy": "mix-lock" + }, + { + "ID": "phoenix_view@2.0.1", + "Name": "phoenix_view", + "Identifier": { + "PURL": "pkg:hex/phoenix_view@2.0.1", + "UID": "3a29d814644268c9" + }, + "Version": "2.0.1", + "Locations": [ + { + "StartLine": 8, + "EndLine": 8 + } + ], + "AnalyzedBy": "mix-lock" + }, + { + "ID": "plug@1.14.0", + "Name": "plug", + "Identifier": { + "PURL": "pkg:hex/plug@1.14.0", + "UID": "3ae26000ce20e664" + }, + "Version": "1.14.0", + "Locations": [ + { + "StartLine": 9, + "EndLine": 9 + } + ], + "AnalyzedBy": "mix-lock" + }, + { + "ID": "plug_crypto@1.2.3", + "Name": "plug_crypto", + "Identifier": { + "PURL": "pkg:hex/plug_crypto@1.2.3", + "UID": "5db5d253a7d30c61" + }, + "Version": "1.2.3", + "Locations": [ + { + "StartLine": 10, + "EndLine": 10 + } + ], + "AnalyzedBy": "mix-lock" + }, + { + "ID": "telemetry@1.1.0", + "Name": "telemetry", + "Identifier": { + "PURL": "pkg:hex/telemetry@1.1.0", + "UID": "90db2c6e0c05f48f" + }, + "Version": "1.1.0", + "Locations": [ + { + "StartLine": 11, + "EndLine": 11 + } + ], + "AnalyzedBy": "mix-lock" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-42975", + "PkgID": "phoenix@1.6.13", + "PkgName": "phoenix", + "PkgIdentifier": { + "PURL": "pkg:hex/phoenix@1.6.13", + "UID": "1ad66010cf0dd9cb" + }, + "InstalledVersion": "1.6.13", + "FixedVersion": "1.6.14", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-42975", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Erlang", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Aerlang" + }, + "Fingerprint": "sha256:1bb1e08039e33e804cc2999cb56244081c7a90c486e8337344d4f0263f1c28dc", + "Title": "Phoenix before 1.6.14 mishandles check_origin wildcarding", + "Description": "socket/transport.ex in Phoenix before 1.6.14 mishandles check_origin wildcarding. NOTE: LiveView applications are unaffected by default because of the presence of a LiveView CSRF token.", + "Severity": "HIGH", + "VendorSeverity": { + "ghsa": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N", + "V3Score": 7.5 + } + }, + "References": [ + "https://nvd.nist.gov/vuln/detail/CVE-2022-42975", + "https://github.com/phoenixframework/phoenix/commit/6e7185b33a59e0b1d1c0b4223adf340a73e963ae", + "https://hexdocs.pm/phoenix/1.6.14/changelog.html#1-6-14-2022-10-10", + "https://github.com/advisories/GHSA-p8f7-22gq-m7j9" + ], + "PublishedDate": "2022-10-17T12:00:27Z", + "LastModifiedDate": "2022-10-18T18:01:44Z" + } + ] + } + ] +} diff --git a/integration/testdata/npm-cyclonedx.json.golden b/integration/testdata/npm-cyclonedx.json.golden new file mode 100644 index 000000000000..7a3e9ea3cdcb --- /dev/null +++ b/integration/testdata/npm-cyclonedx.json.golden @@ -0,0 +1,720 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3ff14136-e09f-4df9-80ea-000000000015", + "version": 1, + "metadata": { + "timestamp": "2021-08-25T12:20:30+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "3ff14136-e09f-4df9-80ea-000000000001", + "type": "application", + "name": "testdata/fixtures/repo/npm", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "3ff14136-e09f-4df9-80ea-000000000002", + "type": "application", + "name": "package-lock.json", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/asap@2.0.6", + "type": "library", + "name": "asap", + "version": "2.0.6", + "purl": "pkg:npm/asap@2.0.6", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "asap@2.0.6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/jquery@3.3.9", + "type": "library", + "name": "jquery", + "version": "3.3.9", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:npm/jquery@3.3.9", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "jquery@3.3.9" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/js-tokens@4.0.0", + "type": "library", + "name": "js-tokens", + "version": "4.0.0", + "purl": "pkg:npm/js-tokens@4.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "js-tokens@4.0.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/loose-envify@1.4.0", + "type": "library", + "name": "loose-envify", + "version": "1.4.0", + "purl": "pkg:npm/loose-envify@1.4.0", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "loose-envify@1.4.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/object-assign@4.1.1", + "type": "library", + "name": "object-assign", + "version": "4.1.1", + "purl": "pkg:npm/object-assign@4.1.1", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "object-assign@4.1.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/promise@8.0.3", + "type": "library", + "name": "promise", + "version": "8.0.3", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:npm/promise@8.0.3", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "promise@8.0.3" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/prop-types@15.7.2", + "type": "library", + "name": "prop-types", + "version": "15.7.2", + "purl": "pkg:npm/prop-types@15.7.2", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "prop-types@15.7.2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/react-is@16.8.6", + "type": "library", + "name": "react-is", + "version": "16.8.6", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:npm/react-is@16.8.6", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "react-is@16.8.6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/react@16.8.6", + "type": "library", + "name": "react", + "version": "16.8.6", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:npm/react@16.8.6", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "react@16.8.6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/redux@4.0.1", + "type": "library", + "name": "redux", + "version": "4.0.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ], + "purl": "pkg:npm/redux@4.0.1", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "redux@4.0.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/scheduler@0.13.6", + "type": "library", + "name": "scheduler", + "version": "0.13.6", + "purl": "pkg:npm/scheduler@0.13.6", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "scheduler@0.13.6" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + }, + { + "bom-ref": "pkg:npm/symbol-observable@1.2.0", + "type": "library", + "name": "symbol-observable", + "version": "1.2.0", + "purl": "pkg:npm/symbol-observable@1.2.0", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "symbol-observable@1.2.0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "npm" + } + ] + } + ], + "dependencies": [ + { + "ref": "3ff14136-e09f-4df9-80ea-000000000001", + "dependsOn": [ + "3ff14136-e09f-4df9-80ea-000000000002" + ] + }, + { + "ref": "3ff14136-e09f-4df9-80ea-000000000002", + "dependsOn": [ + "pkg:npm/jquery@3.3.9", + "pkg:npm/promise@8.0.3", + "pkg:npm/react@16.8.6", + "pkg:npm/redux@4.0.1" + ] + }, + { + "ref": "pkg:npm/asap@2.0.6", + "dependsOn": [] + }, + { + "ref": "pkg:npm/jquery@3.3.9", + "dependsOn": [] + }, + { + "ref": "pkg:npm/js-tokens@4.0.0", + "dependsOn": [] + }, + { + "ref": "pkg:npm/loose-envify@1.4.0", + "dependsOn": [ + "pkg:npm/js-tokens@4.0.0" + ] + }, + { + "ref": "pkg:npm/object-assign@4.1.1", + "dependsOn": [] + }, + { + "ref": "pkg:npm/promise@8.0.3", + "dependsOn": [ + "pkg:npm/asap@2.0.6" + ] + }, + { + "ref": "pkg:npm/prop-types@15.7.2", + "dependsOn": [ + "pkg:npm/loose-envify@1.4.0", + "pkg:npm/object-assign@4.1.1", + "pkg:npm/react-is@16.8.6" + ] + }, + { + "ref": "pkg:npm/react-is@16.8.6", + "dependsOn": [] + }, + { + "ref": "pkg:npm/react@16.8.6", + "dependsOn": [ + "pkg:npm/loose-envify@1.4.0", + "pkg:npm/object-assign@4.1.1", + "pkg:npm/prop-types@15.7.2", + "pkg:npm/scheduler@0.13.6" + ] + }, + { + "ref": "pkg:npm/redux@4.0.1", + "dependsOn": [ + "pkg:npm/loose-envify@1.4.0", + "pkg:npm/symbol-observable@1.2.0" + ] + }, + { + "ref": "pkg:npm/scheduler@0.13.6", + "dependsOn": [ + "pkg:npm/loose-envify@1.4.0", + "pkg:npm/object-assign@4.1.1" + ] + }, + { + "ref": "pkg:npm/symbol-observable@1.2.0", + "dependsOn": [] + } + ], + "vulnerabilities": [ + { + "id": "CVE-2019-11358", + "source": { + "name": "ghsa", + "url": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "ratings": [ + { + "source": { + "name": "alma" + }, + "severity": "medium" + }, + { + "source": { + "name": "amazon" + }, + "severity": "medium" + }, + { + "source": { + "name": "arch-linux" + }, + "severity": "medium" + }, + { + "source": { + "name": "ghsa" + }, + "severity": "medium" + }, + { + "source": { + "name": "nodejs-security-wg" + }, + "severity": "medium" + }, + { + "source": { + "name": "nvd" + }, + "score": 4.3, + "severity": "medium", + "method": "CVSSv2", + "vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N" + }, + { + "source": { + "name": "nvd" + }, + "score": 6.1, + "severity": "medium", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N" + }, + { + "source": { + "name": "oracle-oval" + }, + "severity": "medium" + }, + { + "source": { + "name": "redhat" + }, + "score": 5.6, + "severity": "medium", + "method": "CVSSv3", + "vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L" + }, + { + "source": { + "name": "ruby-advisory-db" + }, + "severity": "medium" + }, + { + "source": { + "name": "ubuntu" + }, + "severity": "low" + } + ], + "cwes": [ + 79 + ], + "description": "jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype.", + "recommendation": "Upgrade jquery to version 3.4.0", + "advisories": [ + { + "url": "https://avd.aquasec.com/nvd/cve-2019-11358" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html" + }, + { + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html" + }, + { + "url": "http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html" + }, + { + "url": "http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html" + }, + { + "url": "http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html" + }, + { + "url": "http://seclists.org/fulldisclosure/2019/May/10" + }, + { + "url": "http://seclists.org/fulldisclosure/2019/May/11" + }, + { + "url": "http://seclists.org/fulldisclosure/2019/May/13" + }, + { + "url": "http://www.openwall.com/lists/oss-security/2019/06/03/2" + }, + { + "url": "http://www.securityfocus.com/bid/108023" + }, + { + "url": "https://access.redhat.com/errata/RHBA-2019:1570" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:1456" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:2587" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:3023" + }, + { + "url": "https://access.redhat.com/errata/RHSA-2019:3024" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2019-11358" + }, + { + "url": "https://backdropcms.org/security/backdrop-sa-core-2019-009" + }, + { + "url": "https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/" + }, + { + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358" + }, + { + "url": "https://github.com/DanielRuf/snyk-js-jquery-174006?files=1" + }, + { + "url": "https://github.com/advisories/GHSA-6c3j-c64m-qhgq" + }, + { + "url": "https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b" + }, + { + "url": "https://github.com/jquery/jquery/pull/4333" + }, + { + "url": "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434" + }, + { + "url": "https://hackerone.com/reports/454365" + }, + { + "url": "https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601" + }, + { + "url": "https://linux.oracle.com/cve/CVE-2019-11358.html" + }, + { + "url": "https://linux.oracle.com/errata/ELSA-2020-4847.html" + }, + { + "url": "https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/" + }, + { + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/" + }, + { + "url": "https://nvd.nist.gov/vuln/detail/CVE-2019-11358" + }, + { + "url": "https://seclists.org/bugtraq/2019/Apr/32" + }, + { + "url": "https://seclists.org/bugtraq/2019/Jun/12" + }, + { + "url": "https://seclists.org/bugtraq/2019/May/18" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20190919-0001/" + }, + { + "url": "https://snyk.io/vuln/SNYK-JS-JQUERY-174006" + }, + { + "url": "https://www.debian.org/security/2019/dsa-4434" + }, + { + "url": "https://www.debian.org/security/2019/dsa-4460" + }, + { + "url": "https://www.drupal.org/sa-core-2019-006" + }, + { + "url": "https://www.oracle.com//security-alerts/cpujul2021.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpuApr2021.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpuapr2020.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpujan2020.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpujan2021.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpujul2020.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpuoct2020.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpuoct2021.html" + }, + { + "url": "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html" + }, + { + "url": "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + }, + { + "url": "https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/" + }, + { + "url": "https://www.synology.com/security/advisory/Synology_SA_19_19" + }, + { + "url": "https://www.tenable.com/security/tns-2019-08" + }, + { + "url": "https://www.tenable.com/security/tns-2020-02" + } + ], + "published": "2019-04-20T00:29:00+00:00", + "updated": "2021-10-20T11:15:00+00:00", + "affects": [ + { + "ref": "pkg:npm/jquery@3.3.9", + "versions": [ + { + "version": "3.3.9", + "status": "affected" + } + ] + } + ] + } + ] +} diff --git a/integration/testdata/npm-ubuntu-severity.json.golden b/integration/testdata/npm-ubuntu-severity.json.golden new file mode 100644 index 000000000000..21325b2c6f97 --- /dev/null +++ b/integration/testdata/npm-ubuntu-severity.json.golden @@ -0,0 +1,152 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/npm", + "ArtifactType": "repository", + "Results": [ + { + "Target": "package-lock.json", + "Class": "lang-pkgs", + "Type": "npm", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-11358", + "PkgID": "jquery@3.3.9", + "PkgName": "jquery", + "PkgIdentifier": { + "PURL": "pkg:npm/jquery@3.3.9", + "UID": "5624abe72853740" + }, + "InstalledVersion": "3.3.9", + "FixedVersion": "3.4.0", + "Status": "fixed", + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-11358", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Fingerprint": "sha256:3952ef90d8606b4067901c413142af693504f820972f5b017a6fdb627ba56af0", + "Title": "jquery: Prototype pollution in object's prototype leading to denial of service, remote code execution, or property injection", + "Description": "jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype.", + "Severity": "LOW", + "CweIDs": [ + "CWE-79" + ], + "VendorSeverity": { + "alma": 2, + "amazon": 2, + "arch-linux": 2, + "ghsa": 2, + "nodejs-security-wg": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ruby-advisory-db": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 4.3, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html", + "http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html", + "http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html", + "http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html", + "http://seclists.org/fulldisclosure/2019/May/10", + "http://seclists.org/fulldisclosure/2019/May/11", + "http://seclists.org/fulldisclosure/2019/May/13", + "http://www.openwall.com/lists/oss-security/2019/06/03/2", + "http://www.securityfocus.com/bid/108023", + "https://access.redhat.com/errata/RHBA-2019:1570", + "https://access.redhat.com/errata/RHSA-2019:1456", + "https://access.redhat.com/errata/RHSA-2019:2587", + "https://access.redhat.com/errata/RHSA-2019:3023", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://access.redhat.com/security/cve/CVE-2019-11358", + "https://backdropcms.org/security/backdrop-sa-core-2019-009", + "https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358", + "https://github.com/DanielRuf/snyk-js-jquery-174006?files=1", + "https://github.com/advisories/GHSA-6c3j-c64m-qhgq", + "https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b", + "https://github.com/jquery/jquery/pull/4333", + "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434", + "https://hackerone.com/reports/454365", + "https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601", + "https://linux.oracle.com/cve/CVE-2019-11358.html", + "https://linux.oracle.com/errata/ELSA-2020-4847.html", + "https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E", + "https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E", + "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", + "https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E", + "https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E", + "https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html", + "https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-11358", + "https://seclists.org/bugtraq/2019/Apr/32", + "https://seclists.org/bugtraq/2019/Jun/12", + "https://seclists.org/bugtraq/2019/May/18", + "https://security.netapp.com/advisory/ntap-20190919-0001/", + "https://snyk.io/vuln/SNYK-JS-JQUERY-174006", + "https://www.debian.org/security/2019/dsa-4434", + "https://www.debian.org/security/2019/dsa-4460", + "https://www.drupal.org/sa-core-2019-006", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/", + "https://www.synology.com/security/advisory/Synology_SA_19_19", + "https://www.tenable.com/security/tns-2019-08", + "https://www.tenable.com/security/tns-2020-02" + ], + "PublishedDate": "2019-04-20T00:29:00Z", + "LastModifiedDate": "2021-10-20T11:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/npm-with-dev.json.golden b/integration/testdata/npm-with-dev.json.golden new file mode 100644 index 000000000000..d672024d2208 --- /dev/null +++ b/integration/testdata/npm-with-dev.json.golden @@ -0,0 +1,406 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/npm", + "ArtifactType": "repository", + "Results": [ + { + "Target": "package-lock.json", + "Class": "lang-pkgs", + "Type": "npm", + "Packages": [ + { + "ID": "asap@2.0.6", + "Name": "asap", + "Identifier": { + "PURL": "pkg:npm/asap@2.0.6", + "UID": "a9ed9c990b55c97e" + }, + "Version": "2.0.6", + "Locations": [ + { + "StartLine": 6, + "EndLine": 10 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "jquery@3.3.9", + "Name": "jquery", + "Identifier": { + "PURL": "pkg:npm/jquery@3.3.9", + "UID": "5624abe72853740" + }, + "Version": "3.3.9", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 11, + "EndLine": 15 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "js-tokens@4.0.0", + "Name": "js-tokens", + "Identifier": { + "PURL": "pkg:npm/js-tokens@4.0.0", + "UID": "64fc20ee370178b3" + }, + "Version": "4.0.0", + "Locations": [ + { + "StartLine": 16, + "EndLine": 20 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "loose-envify@1.4.0", + "Name": "loose-envify", + "Identifier": { + "PURL": "pkg:npm/loose-envify@1.4.0", + "UID": "880231ec08096120" + }, + "Version": "1.4.0", + "DependsOn": [ + "js-tokens@4.0.0" + ], + "Locations": [ + { + "StartLine": 21, + "EndLine": 28 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "object-assign@4.1.1", + "Name": "object-assign", + "Identifier": { + "PURL": "pkg:npm/object-assign@4.1.1", + "UID": "48119107a22d86bc" + }, + "Version": "4.1.1", + "Locations": [ + { + "StartLine": 29, + "EndLine": 33 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "promise@8.0.3", + "Name": "promise", + "Identifier": { + "PURL": "pkg:npm/promise@8.0.3", + "UID": "755f2bbb480f470d" + }, + "Version": "8.0.3", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "asap@2.0.6" + ], + "Locations": [ + { + "StartLine": 34, + "EndLine": 41 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "prop-types@15.7.2", + "Name": "prop-types", + "Identifier": { + "PURL": "pkg:npm/prop-types@15.7.2", + "UID": "2826a59f397d6438" + }, + "Version": "15.7.2", + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "react-is@16.8.6" + ], + "Locations": [ + { + "StartLine": 42, + "EndLine": 51 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "react@16.8.6", + "Name": "react", + "Identifier": { + "PURL": "pkg:npm/react@16.8.6", + "UID": "82d9f16b21d9e2c2" + }, + "Version": "16.8.6", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "prop-types@15.7.2", + "scheduler@0.13.6" + ], + "Locations": [ + { + "StartLine": 52, + "EndLine": 62 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "react-is@16.8.6", + "Name": "react-is", + "Identifier": { + "PURL": "pkg:npm/react-is@16.8.6", + "UID": "847264915e79444a" + }, + "Version": "16.8.6", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 63, + "EndLine": 67 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "redux@4.0.1", + "Name": "redux", + "Identifier": { + "PURL": "pkg:npm/redux@4.0.1", + "UID": "c38443b568b7d5b9" + }, + "Version": "4.0.1", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "loose-envify@1.4.0", + "symbol-observable@1.2.0" + ], + "Locations": [ + { + "StartLine": 68, + "EndLine": 76 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "scheduler@0.13.6", + "Name": "scheduler", + "Identifier": { + "PURL": "pkg:npm/scheduler@0.13.6", + "UID": "92cfb6b69c6b19ce" + }, + "Version": "0.13.6", + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1" + ], + "Locations": [ + { + "StartLine": 77, + "EndLine": 85 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "symbol-observable@1.2.0", + "Name": "symbol-observable", + "Identifier": { + "PURL": "pkg:npm/symbol-observable@1.2.0", + "UID": "2d38df676788a02f" + }, + "Version": "1.2.0", + "Locations": [ + { + "StartLine": 86, + "EndLine": 90 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "z-lock@1.0.0", + "Name": "z-lock", + "Identifier": { + "PURL": "pkg:npm/z-lock@1.0.0", + "UID": "4802db92de545cc3" + }, + "Version": "1.0.0", + "Dev": true, + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 91, + "EndLine": 96 + } + ], + "AnalyzedBy": "npm" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-11358", + "PkgID": "jquery@3.3.9", + "PkgName": "jquery", + "PkgIdentifier": { + "PURL": "pkg:npm/jquery@3.3.9", + "UID": "5624abe72853740" + }, + "InstalledVersion": "3.3.9", + "FixedVersion": "3.4.0", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-11358", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Fingerprint": "sha256:3952ef90d8606b4067901c413142af693504f820972f5b017a6fdb627ba56af0", + "Title": "jquery: Prototype pollution in object's prototype leading to denial of service, remote code execution, or property injection", + "Description": "jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-79" + ], + "VendorSeverity": { + "alma": 2, + "amazon": 2, + "arch-linux": 2, + "ghsa": 2, + "nodejs-security-wg": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ruby-advisory-db": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 4.3, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html", + "http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html", + "http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html", + "http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html", + "http://seclists.org/fulldisclosure/2019/May/10", + "http://seclists.org/fulldisclosure/2019/May/11", + "http://seclists.org/fulldisclosure/2019/May/13", + "http://www.openwall.com/lists/oss-security/2019/06/03/2", + "http://www.securityfocus.com/bid/108023", + "https://access.redhat.com/errata/RHBA-2019:1570", + "https://access.redhat.com/errata/RHSA-2019:1456", + "https://access.redhat.com/errata/RHSA-2019:2587", + "https://access.redhat.com/errata/RHSA-2019:3023", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://access.redhat.com/security/cve/CVE-2019-11358", + "https://backdropcms.org/security/backdrop-sa-core-2019-009", + "https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358", + "https://github.com/DanielRuf/snyk-js-jquery-174006?files=1", + "https://github.com/advisories/GHSA-6c3j-c64m-qhgq", + "https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b", + "https://github.com/jquery/jquery/pull/4333", + "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434", + "https://hackerone.com/reports/454365", + "https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601", + "https://linux.oracle.com/cve/CVE-2019-11358.html", + "https://linux.oracle.com/errata/ELSA-2020-4847.html", + "https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E", + "https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E", + "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", + "https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E", + "https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E", + "https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html", + "https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-11358", + "https://seclists.org/bugtraq/2019/Apr/32", + "https://seclists.org/bugtraq/2019/Jun/12", + "https://seclists.org/bugtraq/2019/May/18", + "https://security.netapp.com/advisory/ntap-20190919-0001/", + "https://snyk.io/vuln/SNYK-JS-JQUERY-174006", + "https://www.debian.org/security/2019/dsa-4434", + "https://www.debian.org/security/2019/dsa-4460", + "https://www.drupal.org/sa-core-2019-006", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/", + "https://www.synology.com/security/advisory/Synology_SA_19_19", + "https://www.tenable.com/security/tns-2019-08", + "https://www.tenable.com/security/tns-2020-02" + ], + "PublishedDate": "2019-04-20T00:29:00Z", + "LastModifiedDate": "2021-10-20T11:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/npm.gitlab.golden b/integration/testdata/npm.gitlab.golden new file mode 100644 index 000000000000..6c8cc2ce97ba --- /dev/null +++ b/integration/testdata/npm.gitlab.golden @@ -0,0 +1,214 @@ +{ + "version": "15.0.7", + "scan": { + "analyzer": { + "id": "trivy", + "name": "Trivy", + "vendor": { + "name": "Aqua Security" + }, + "version": "dev" + }, + "end_time": "2021-08-25T12:20:30", + "scanner": { + "id": "trivy", + "name": "Trivy", + "url": "https://github.com/aquasecurity/trivy/", + "vendor": { + "name": "Aqua Security" + }, + "version": "dev" + }, + "start_time": "2021-08-25T12:20:30", + "status": "success", + "type": "container_scanning" + }, + "vulnerabilities": [ + { + "id": "CVE-2019-11358", + "name": "jquery: Prototype pollution in object's prototype leading to denial of service, remote code execution, or property injection", + "description": "jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype.", + "severity": "Medium", + "solution": "Upgrade jquery to 3.4.0", + "location": { + "dependency": { + "package": { + "name": "jquery" + }, + "version": "3.3.9" + }, + "operating_system": "Unknown", + "image": "Unknown" + }, + "identifiers": [ + { + "type": "cve", + "name": "CVE-2019-11358", + "value": "CVE-2019-11358", + "url": "https://avd.aquasec.com/nvd/cve-2019-11358" + } + ], + "links": [{ + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html" + },{ + "url": "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html" + },{ + "url": "http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html" + },{ + "url": "http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html" + },{ + "url": "http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html" + },{ + "url": "http://seclists.org/fulldisclosure/2019/May/10" + },{ + "url": "http://seclists.org/fulldisclosure/2019/May/11" + },{ + "url": "http://seclists.org/fulldisclosure/2019/May/13" + },{ + "url": "http://www.openwall.com/lists/oss-security/2019/06/03/2" + },{ + "url": "http://www.securityfocus.com/bid/108023" + },{ + "url": "https://access.redhat.com/errata/RHBA-2019:1570" + },{ + "url": "https://access.redhat.com/errata/RHSA-2019:1456" + },{ + "url": "https://access.redhat.com/errata/RHSA-2019:2587" + },{ + "url": "https://access.redhat.com/errata/RHSA-2019:3023" + },{ + "url": "https://access.redhat.com/errata/RHSA-2019:3024" + },{ + "url": "https://access.redhat.com/security/cve/CVE-2019-11358" + },{ + "url": "https://backdropcms.org/security/backdrop-sa-core-2019-009" + },{ + "url": "https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/" + },{ + "url": "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358" + },{ + "url": "https://github.com/DanielRuf/snyk-js-jquery-174006?files=1" + },{ + "url": "https://github.com/advisories/GHSA-6c3j-c64m-qhgq" + },{ + "url": "https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b" + },{ + "url": "https://github.com/jquery/jquery/pull/4333" + },{ + "url": "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434" + },{ + "url": "https://hackerone.com/reports/454365" + },{ + "url": "https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601" + },{ + "url": "https://linux.oracle.com/cve/CVE-2019-11358.html" + },{ + "url": "https://linux.oracle.com/errata/ELSA-2020-4847.html" + },{ + "url": "https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E" + },{ + "url": "https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E" + },{ + "url": "https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html" + },{ + "url": "https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html" + },{ + "url": "https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/" + },{ + "url": "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/" + },{ + "url": "https://nvd.nist.gov/vuln/detail/CVE-2019-11358" + },{ + "url": "https://seclists.org/bugtraq/2019/Apr/32" + },{ + "url": "https://seclists.org/bugtraq/2019/Jun/12" + },{ + "url": "https://seclists.org/bugtraq/2019/May/18" + },{ + "url": "https://security.netapp.com/advisory/ntap-20190919-0001/" + },{ + "url": "https://snyk.io/vuln/SNYK-JS-JQUERY-174006" + },{ + "url": "https://www.debian.org/security/2019/dsa-4434" + },{ + "url": "https://www.debian.org/security/2019/dsa-4460" + },{ + "url": "https://www.drupal.org/sa-core-2019-006" + },{ + "url": "https://www.oracle.com//security-alerts/cpujul2021.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpuApr2021.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpuapr2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujan2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujan2021.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpujul2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpuoct2020.html" + },{ + "url": "https://www.oracle.com/security-alerts/cpuoct2021.html" + },{ + "url": "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html" + },{ + "url": "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + },{ + "url": "https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/" + },{ + "url": "https://www.synology.com/security/advisory/Synology_SA_19_19" + },{ + "url": "https://www.tenable.com/security/tns-2019-08" + },{ + "url": "https://www.tenable.com/security/tns-2020-02" + } + ] + } + ], + "remediations": [] +} diff --git a/integration/testdata/npm.json.golden b/integration/testdata/npm.json.golden new file mode 100644 index 000000000000..1a9d999549ca --- /dev/null +++ b/integration/testdata/npm.json.golden @@ -0,0 +1,386 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/npm", + "ArtifactType": "repository", + "Results": [ + { + "Target": "package-lock.json", + "Class": "lang-pkgs", + "Type": "npm", + "Packages": [ + { + "ID": "asap@2.0.6", + "Name": "asap", + "Identifier": { + "PURL": "pkg:npm/asap@2.0.6", + "UID": "a9ed9c990b55c97e" + }, + "Version": "2.0.6", + "Locations": [ + { + "StartLine": 6, + "EndLine": 10 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "jquery@3.3.9", + "Name": "jquery", + "Identifier": { + "PURL": "pkg:npm/jquery@3.3.9", + "UID": "5624abe72853740" + }, + "Version": "3.3.9", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 11, + "EndLine": 15 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "js-tokens@4.0.0", + "Name": "js-tokens", + "Identifier": { + "PURL": "pkg:npm/js-tokens@4.0.0", + "UID": "64fc20ee370178b3" + }, + "Version": "4.0.0", + "Locations": [ + { + "StartLine": 16, + "EndLine": 20 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "loose-envify@1.4.0", + "Name": "loose-envify", + "Identifier": { + "PURL": "pkg:npm/loose-envify@1.4.0", + "UID": "880231ec08096120" + }, + "Version": "1.4.0", + "DependsOn": [ + "js-tokens@4.0.0" + ], + "Locations": [ + { + "StartLine": 21, + "EndLine": 28 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "object-assign@4.1.1", + "Name": "object-assign", + "Identifier": { + "PURL": "pkg:npm/object-assign@4.1.1", + "UID": "48119107a22d86bc" + }, + "Version": "4.1.1", + "Locations": [ + { + "StartLine": 29, + "EndLine": 33 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "promise@8.0.3", + "Name": "promise", + "Identifier": { + "PURL": "pkg:npm/promise@8.0.3", + "UID": "755f2bbb480f470d" + }, + "Version": "8.0.3", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "asap@2.0.6" + ], + "Locations": [ + { + "StartLine": 34, + "EndLine": 41 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "prop-types@15.7.2", + "Name": "prop-types", + "Identifier": { + "PURL": "pkg:npm/prop-types@15.7.2", + "UID": "2826a59f397d6438" + }, + "Version": "15.7.2", + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "react-is@16.8.6" + ], + "Locations": [ + { + "StartLine": 42, + "EndLine": 51 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "react@16.8.6", + "Name": "react", + "Identifier": { + "PURL": "pkg:npm/react@16.8.6", + "UID": "82d9f16b21d9e2c2" + }, + "Version": "16.8.6", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "prop-types@15.7.2", + "scheduler@0.13.6" + ], + "Locations": [ + { + "StartLine": 52, + "EndLine": 62 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "react-is@16.8.6", + "Name": "react-is", + "Identifier": { + "PURL": "pkg:npm/react-is@16.8.6", + "UID": "847264915e79444a" + }, + "Version": "16.8.6", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 63, + "EndLine": 67 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "redux@4.0.1", + "Name": "redux", + "Identifier": { + "PURL": "pkg:npm/redux@4.0.1", + "UID": "c38443b568b7d5b9" + }, + "Version": "4.0.1", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "loose-envify@1.4.0", + "symbol-observable@1.2.0" + ], + "Locations": [ + { + "StartLine": 68, + "EndLine": 76 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "scheduler@0.13.6", + "Name": "scheduler", + "Identifier": { + "PURL": "pkg:npm/scheduler@0.13.6", + "UID": "92cfb6b69c6b19ce" + }, + "Version": "0.13.6", + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1" + ], + "Locations": [ + { + "StartLine": 77, + "EndLine": 85 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "symbol-observable@1.2.0", + "Name": "symbol-observable", + "Identifier": { + "PURL": "pkg:npm/symbol-observable@1.2.0", + "UID": "2d38df676788a02f" + }, + "Version": "1.2.0", + "Locations": [ + { + "StartLine": 86, + "EndLine": 90 + } + ], + "AnalyzedBy": "npm" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-11358", + "PkgID": "jquery@3.3.9", + "PkgName": "jquery", + "PkgIdentifier": { + "PURL": "pkg:npm/jquery@3.3.9", + "UID": "5624abe72853740" + }, + "InstalledVersion": "3.3.9", + "FixedVersion": "3.4.0", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-11358", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Fingerprint": "sha256:3952ef90d8606b4067901c413142af693504f820972f5b017a6fdb627ba56af0", + "Title": "jquery: Prototype pollution in object's prototype leading to denial of service, remote code execution, or property injection", + "Description": "jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-79" + ], + "VendorSeverity": { + "alma": 2, + "amazon": 2, + "arch-linux": 2, + "ghsa": 2, + "nodejs-security-wg": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ruby-advisory-db": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 4.3, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html", + "http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html", + "http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html", + "http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html", + "http://seclists.org/fulldisclosure/2019/May/10", + "http://seclists.org/fulldisclosure/2019/May/11", + "http://seclists.org/fulldisclosure/2019/May/13", + "http://www.openwall.com/lists/oss-security/2019/06/03/2", + "http://www.securityfocus.com/bid/108023", + "https://access.redhat.com/errata/RHBA-2019:1570", + "https://access.redhat.com/errata/RHSA-2019:1456", + "https://access.redhat.com/errata/RHSA-2019:2587", + "https://access.redhat.com/errata/RHSA-2019:3023", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://access.redhat.com/security/cve/CVE-2019-11358", + "https://backdropcms.org/security/backdrop-sa-core-2019-009", + "https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358", + "https://github.com/DanielRuf/snyk-js-jquery-174006?files=1", + "https://github.com/advisories/GHSA-6c3j-c64m-qhgq", + "https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b", + "https://github.com/jquery/jquery/pull/4333", + "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434", + "https://hackerone.com/reports/454365", + "https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601", + "https://linux.oracle.com/cve/CVE-2019-11358.html", + "https://linux.oracle.com/errata/ELSA-2020-4847.html", + "https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E", + "https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E", + "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", + "https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E", + "https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E", + "https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html", + "https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-11358", + "https://seclists.org/bugtraq/2019/Apr/32", + "https://seclists.org/bugtraq/2019/Jun/12", + "https://seclists.org/bugtraq/2019/May/18", + "https://security.netapp.com/advisory/ntap-20190919-0001/", + "https://snyk.io/vuln/SNYK-JS-JQUERY-174006", + "https://www.debian.org/security/2019/dsa-4434", + "https://www.debian.org/security/2019/dsa-4460", + "https://www.drupal.org/sa-core-2019-006", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/", + "https://www.synology.com/security/advisory/Synology_SA_19_19", + "https://www.tenable.com/security/tns-2019-08", + "https://www.tenable.com/security/tns-2020-02" + ], + "PublishedDate": "2019-04-20T00:29:00Z", + "LastModifiedDate": "2021-10-20T11:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/nuget.json.golden b/integration/testdata/nuget.json.golden new file mode 100644 index 000000000000..21ea2b4395c7 --- /dev/null +++ b/integration/testdata/nuget.json.golden @@ -0,0 +1,99 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/nuget", + "ArtifactType": "repository", + "Results": [ + { + "Target": "packages.lock.json", + "Class": "lang-pkgs", + "Type": "nuget", + "Packages": [ + { + "ID": "Newtonsoft.Json@12.0.3", + "Name": "Newtonsoft.Json", + "Identifier": { + "PURL": "pkg:nuget/Newtonsoft.Json@12.0.3", + "UID": "f11f6e4dc1155888" + }, + "Version": "12.0.3", + "Relationship": "direct", + "Locations": [ + { + "StartLine": 5, + "EndLine": 10 + } + ], + "AnalyzedBy": "nuget" + }, + { + "ID": "NuGet.Frameworks@5.7.0", + "Name": "NuGet.Frameworks", + "Identifier": { + "PURL": "pkg:nuget/NuGet.Frameworks@5.7.0", + "UID": "f5bb6bea9e88283" + }, + "Version": "5.7.0", + "Relationship": "direct", + "DependsOn": [ + "Newtonsoft.Json@12.0.3" + ], + "Locations": [ + { + "StartLine": 11, + "EndLine": 19 + } + ], + "AnalyzedBy": "nuget" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "GHSA-5crp-9r3c-p9vr", + "PkgID": "Newtonsoft.Json@12.0.3", + "PkgName": "Newtonsoft.Json", + "PkgIdentifier": { + "PURL": "pkg:nuget/Newtonsoft.Json@12.0.3", + "UID": "f11f6e4dc1155888" + }, + "InstalledVersion": "12.0.3", + "FixedVersion": "13.0.1", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://github.com/advisories/GHSA-5crp-9r3c-p9vr", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Nuget", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anuget" + }, + "Fingerprint": "sha256:dbba4d559d9826c440ebd1085313e0499718066bb7ea694fcd20ff8d63c51310", + "Title": "Improper Handling of Exceptional Conditions in Newtonsoft.Json", + "Description": "Newtonsoft.Json prior to version 13.0.1 is vulnerable to Insecure Defaults due to improper handling of expressions with high nesting level that lead to StackOverFlow exception or high CPU and RAM usage.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-755" + ], + "VendorSeverity": { + "ghsa": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V3Score": 7.5 + } + }, + "References": [ + "https://alephsecurity.com/2018/10/22/StackOverflowException/", + "https://alephsecurity.com/vulns/aleph-2018004" + ], + "PublishedDate": "2022-06-22T15:08:47Z", + "LastModifiedDate": "2022-06-27T18:37:23Z" + } + ] + } + ] +} diff --git a/integration/testdata/opensuse-leap-151.json.golden b/integration/testdata/opensuse-leap-151.json.golden new file mode 100644 index 000000000000..447e3abe107b --- /dev/null +++ b/integration/testdata/opensuse-leap-151.json.golden @@ -0,0 +1,152 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:b9f31768f6c3908af7de80cff1a9e53c62f46d1bc54aaf3b97b2c922ed9cf1fd", + "ArtifactName": "testdata/fixtures/images/opensuse-leap-151.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 105899520, + "OS": { + "Family": "opensuse-leap", + "Name": "15.1", + "EOSL": true + }, + "ImageID": "sha256:fef5ad254f6378f08071cfa2daaf05a1ce9857141c944b67a40742e63e65cecc", + "DiffIDs": [ + "sha256:f7f9ae80878a1c56d8f9ca977a5d844168f7afc0c1429feef9366e713eac06ff" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:opensuse-leap-151" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:opensuse-leap-151", + "ImageConfig": { + "architecture": "amd64", + "author": "Fabian Vogt \u003cfvogt@suse.com\u003e", + "created": "2019-11-05T15:54:41Z", + "history": [ + { + "created": "2019-11-05T15:54:41Z", + "created_by": "KIWI 9.17.16" + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:f7f9ae80878a1c56d8f9ca977a5d844168f7afc0c1429feef9366e713eac06ff" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Labels": { + "org.openbuildservice.disturl": "obs://build.opensuse.org/openSUSE:Leap:15.1:Images/images/740264e3294afe7ca32a3ea9deb863d2-opensuse-leap-image:docker", + "org.opencontainers.image.created": "2019-11-05T15:54:10.571514200Z", + "org.opencontainers.image.description": "Image containing a minimal environment for containers based on openSUSE Leap 15.1.", + "org.opencontainers.image.title": "openSUSE Leap 15.1 Base Container", + "org.opencontainers.image.url": "https://www.opensuse.org/", + "org.opencontainers.image.vendor": "openSUSE Project", + "org.opencontainers.image.version": "15.1.3.67", + "org.opensuse.base.created": "2019-11-05T15:54:10.571514200Z", + "org.opensuse.base.description": "Image containing a minimal environment for containers based on openSUSE Leap 15.1.", + "org.opensuse.base.disturl": "obs://build.opensuse.org/openSUSE:Leap:15.1:Images/images/740264e3294afe7ca32a3ea9deb863d2-opensuse-leap-image:docker", + "org.opensuse.base.reference": "registry.opensuse.org/opensuse/leap:15.1.3.67", + "org.opensuse.base.title": "openSUSE Leap 15.1 Base Container", + "org.opensuse.base.url": "https://www.opensuse.org/", + "org.opensuse.base.vendor": "openSUSE Project", + "org.opensuse.base.version": "15.1.3.67", + "org.opensuse.reference": "registry.opensuse.org/opensuse/leap:15.1.3.67" + } + } + }, + "Layers": [ + { + "Size": 105899520, + "Digest": "sha256:5c5a844f54abd051851758624820ae6a08a9d6ddffddaebbb335601c32608fb3", + "DiffID": "sha256:f7f9ae80878a1c56d8f9ca977a5d844168f7afc0c1429feef9366e713eac06ff" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/opensuse-leap-151.tar.gz (opensuse-leap 15.1)", + "Class": "os-pkgs", + "Type": "opensuse-leap", + "Vulnerabilities": [ + { + "VulnerabilityID": "openSUSE-SU-2020:0062-1", + "PkgID": "libopenssl1_1@1.1.0i-lp151.8.3.1.x86_64", + "PkgName": "libopenssl1_1", + "PkgIdentifier": { + "PURL": "pkg:rpm/opensuse/libopenssl1_1@1.1.0i-lp151.8.3.1?arch=x86_64\u0026distro=opensuse-leap-15.1", + "UID": "10e12f4783a16c7c" + }, + "InstalledVersion": "1.1.0i-lp151.8.3.1", + "FixedVersion": "1.1.0i-lp151.8.6.1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:5c5a844f54abd051851758624820ae6a08a9d6ddffddaebbb335601c32608fb3", + "DiffID": "sha256:f7f9ae80878a1c56d8f9ca977a5d844168f7afc0c1429feef9366e713eac06ff" + }, + "SeveritySource": "suse-cvrf", + "PrimaryURL": "https://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "DataSource": { + "ID": "suse-cvrf", + "Name": "SUSE CVRF", + "URL": "https://ftp.suse.com/pub/projects/security/cvrf/" + }, + "Fingerprint": "sha256:20fab639050347334f826ff7d076eb534ae2291198f36c46914e3291f2e60320", + "Title": "Security update for openssl-1_1", + "Description": "This update for openssl-1_1 fixes the following issues:\n\nSecurity issue fixed:\n\n- CVE-2019-1551: Fixed an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli (bsc#1158809). \n\nVarious FIPS related improvements were done:\n\n- FIPS: Backport SSH KDF to openssl (jsc#SLE-8789, bsc#1157775).\n- Port FIPS patches from SLE-12 (bsc#1158101).\n- Use SHA-2 in the RSA pairwise consistency check (bsc#1155346).\n\nThis update was imported from the SUSE:SLE-15-SP1:Update update project.", + "Severity": "MEDIUM", + "VendorSeverity": { + "suse-cvrf": 2 + }, + "References": [ + "https://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "https://www.suse.com/support/security/rating/" + ] + }, + { + "VulnerabilityID": "openSUSE-SU-2020:0062-1", + "PkgID": "openssl-1_1@1.1.0i-lp151.8.3.1.x86_64", + "PkgName": "openssl-1_1", + "PkgIdentifier": { + "PURL": "pkg:rpm/opensuse/openssl-1_1@1.1.0i-lp151.8.3.1?arch=x86_64\u0026distro=opensuse-leap-15.1", + "UID": "23c07ff656c4d709" + }, + "InstalledVersion": "1.1.0i-lp151.8.3.1", + "FixedVersion": "1.1.0i-lp151.8.6.1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:5c5a844f54abd051851758624820ae6a08a9d6ddffddaebbb335601c32608fb3", + "DiffID": "sha256:f7f9ae80878a1c56d8f9ca977a5d844168f7afc0c1429feef9366e713eac06ff" + }, + "SeveritySource": "suse-cvrf", + "PrimaryURL": "https://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "DataSource": { + "ID": "suse-cvrf", + "Name": "SUSE CVRF", + "URL": "https://ftp.suse.com/pub/projects/security/cvrf/" + }, + "Fingerprint": "sha256:af7aebe9d7dd2e4d00a9495e5417b647dba0aac27bb49fdf9bc86f76fab8273d", + "Title": "Security update for openssl-1_1", + "Description": "This update for openssl-1_1 fixes the following issues:\n\nSecurity issue fixed:\n\n- CVE-2019-1551: Fixed an overflow bug in the x64_64 Montgomery squaring procedure used in exponentiation with 512-bit moduli (bsc#1158809). \n\nVarious FIPS related improvements were done:\n\n- FIPS: Backport SSH KDF to openssl (jsc#SLE-8789, bsc#1157775).\n- Port FIPS patches from SLE-12 (bsc#1158101).\n- Use SHA-2 in the RSA pairwise consistency check (bsc#1155346).\n\nThis update was imported from the SUSE:SLE-15-SP1:Update update project.", + "Severity": "MEDIUM", + "VendorSeverity": { + "suse-cvrf": 2 + }, + "References": [ + "https://lists.opensuse.org/opensuse-security-announce/2020-01/msg00030.html", + "https://www.suse.com/support/security/rating/" + ] + } + ] + } + ] +} diff --git a/integration/testdata/opensuse-tumbleweed.json.golden b/integration/testdata/opensuse-tumbleweed.json.golden new file mode 100644 index 000000000000..de1edd02c214 --- /dev/null +++ b/integration/testdata/opensuse-tumbleweed.json.golden @@ -0,0 +1,112 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:1ab435ff0da0a8c95292bfd8a3b270b457cfca575a4a68731dd2fc142e2c13ef", + "ArtifactName": "testdata/fixtures/images/opensuse-tumbleweed.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 115281408, + "OS": { + "Family": "opensuse-tumbleweed", + "Name": "20240607" + }, + "ImageID": "sha256:580e73f5c823232e6587136e9f5428a89afdf77a123bb8575d08208e0cc34b12", + "DiffIDs": [ + "sha256:7a335bdf2d91d6d158da360054aa7e477d708187d43fe9d0ac20144cdf90f763" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:opensuse-tumbleweed" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:opensuse-tumbleweed", + "ImageConfig": { + "architecture": "amd64", + "author": "Fabian Vogt \u003cfvogt@suse.com\u003e", + "created": "2024-06-07T17:19:44Z", + "history": [ + { + "author": "Fabian Vogt \u003cfvogt@suse.com\u003e", + "created": "2024-06-07T17:19:44Z", + "created_by": "KIWI 10.0.19", + "comment": "openSUSE Tumbleweed 20240607 Base Container" + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:7a335bdf2d91d6d158da360054aa7e477d708187d43fe9d0ac20144cdf90f763" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Labels": { + "org.openbuildservice.disturl": "obs://build.opensuse.org/openSUSE:Factory/images/b068e2522114e1c009e9bfa1b5cb1146-opensuse-tumbleweed-image:docker", + "org.opencontainers.image.created": "2024-06-07T17:19:38.229693664Z", + "org.opencontainers.image.description": "Image containing a minimal environment for containers based on openSUSE Tumbleweed.", + "org.opencontainers.image.source": "https://build.opensuse.org/package/show/openSUSE:Factory/opensuse-tumbleweed-image?rev=b068e2522114e1c009e9bfa1b5cb1146", + "org.opencontainers.image.title": "openSUSE Tumbleweed Base Container", + "org.opencontainers.image.url": "https://www.opensuse.org/", + "org.opencontainers.image.vendor": "openSUSE Project", + "org.opencontainers.image.version": "20240607.30.45", + "org.opensuse.base.created": "2024-06-07T17:19:38.229693664Z", + "org.opensuse.base.description": "Image containing a minimal environment for containers based on openSUSE Tumbleweed.", + "org.opensuse.base.disturl": "obs://build.opensuse.org/openSUSE:Factory/images/b068e2522114e1c009e9bfa1b5cb1146-opensuse-tumbleweed-image:docker", + "org.opensuse.base.reference": "registry.opensuse.org/opensuse/tumbleweed:20240607.30.45", + "org.opensuse.base.source": "https://build.opensuse.org/package/show/openSUSE:Factory/opensuse-tumbleweed-image?rev=b068e2522114e1c009e9bfa1b5cb1146", + "org.opensuse.base.title": "openSUSE Tumbleweed Base Container", + "org.opensuse.base.url": "https://www.opensuse.org/", + "org.opensuse.base.vendor": "openSUSE Project", + "org.opensuse.base.version": "20240607.30.45", + "org.opensuse.reference": "registry.opensuse.org/opensuse/tumbleweed:20240607.30.45" + } + } + }, + "Layers": [ + { + "Size": 115281408, + "Digest": "sha256:427d16a14c45614f51357aeebee0dfe209a1cebfc044b3b724b6ea35663b3111", + "DiffID": "sha256:7a335bdf2d91d6d158da360054aa7e477d708187d43fe9d0ac20144cdf90f763" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/opensuse-tumbleweed.tar.gz (opensuse-tumbleweed 20240607)", + "Class": "os-pkgs", + "Type": "opensuse-tumbleweed", + "Vulnerabilities": [ + { + "VulnerabilityID": "openSUSE-SU-2024:13065-1", + "PkgID": "libopenssl3@3.1.4-9.1.x86_64", + "PkgName": "libopenssl3", + "PkgIdentifier": { + "PURL": "pkg:rpm/opensuse/libopenssl3@3.1.4-9.1?arch=x86_64\u0026distro=opensuse-tumbleweed-20240607", + "UID": "89b97ebaaa8a52cf" + }, + "InstalledVersion": "3.1.4-9.1", + "FixedVersion": "3.1.5-9.1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:427d16a14c45614f51357aeebee0dfe209a1cebfc044b3b724b6ea35663b3111", + "DiffID": "sha256:7a335bdf2d91d6d158da360054aa7e477d708187d43fe9d0ac20144cdf90f763" + }, + "Fingerprint": "sha256:a2b4e178fa87fb6e4f6f965e69b0b1134812d02f62acc2fc5bfc6cfaed68b9f7", + "Title": "libopenssl-3-devel-3.1.1-3.1 on GA media", + "Description": "These are all security issues fixed in the libopenssl-3-devel-3.1.1-3.1 package on the GA media of openSUSE Tumbleweed.", + "Severity": "MEDIUM", + "References": [ + "https://www.suse.com/security/cve/CVE-2023-2975/", + "https://www.suse.com/security/cve/CVE-2023-3446/", + "https://www.suse.com/support/security/rating/" + ] + } + ] + } + ] +} diff --git a/integration/testdata/oraclelinux-8.json.golden b/integration/testdata/oraclelinux-8.json.golden new file mode 100644 index 000000000000..1d686ffa94f2 --- /dev/null +++ b/integration/testdata/oraclelinux-8.json.golden @@ -0,0 +1,232 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:2982b2b5fd16f59d6b9ccdef6292e710791eb7fa12895a593959c5bceb7780e6", + "ArtifactName": "testdata/fixtures/images/oraclelinux-8.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 416893952, + "OS": { + "Family": "oracle", + "Name": "8.0" + }, + "ImageID": "sha256:8988c7081e1f7b6c2928cbc4832b8a05968bb589d45d444ca1e3027c68f97f56", + "DiffIDs": [ + "sha256:91bac58a9ffae0dc2031e3f90d7bf04f66ccf019f180372152b0916d6e8a796f" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:oraclelinux-8" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:oraclelinux-8", + "ImageConfig": { + "architecture": "amd64", + "author": "Oracle Linux Product Team \u003col-ovm-info_ww@oracle.com\u003e", + "container": "b5339bf682ea7c2a5b817144038646d7fa55d72e9943283bd0ff5e01eb8e5e40", + "created": "2019-10-15T21:23:26.082686371Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "author": "Oracle Linux Product Team \u003col-ovm-info_ww@oracle.com\u003e", + "created": "2018-08-30T21:49:27.028879762Z", + "created_by": "/bin/sh -c #(nop) MAINTAINER Oracle Linux Product Team \u003col-ovm-info_ww@oracle.com\u003e", + "empty_layer": true + }, + { + "author": "Oracle Linux Product Team \u003col-ovm-info_ww@oracle.com\u003e", + "created": "2019-10-15T21:23:25.715030578Z", + "created_by": "/bin/sh -c #(nop) ADD file:40a576906f00da132c935b4713c8012d0ac0422f507fc3239d647b0ed9930ed7 in / " + }, + { + "author": "Oracle Linux Product Team \u003col-ovm-info_ww@oracle.com\u003e", + "created": "2019-10-15T21:23:26.082686371Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:91bac58a9ffae0dc2031e3f90d7bf04f66ccf019f180372152b0916d6e8a796f" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:d2f0ba2a964f3d0b1935be99979b6930f8b989217ff6a5e6d4093e9df9baee11", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 416893952, + "Digest": "sha256:e1b9aa33b064e76023cc29e9fac51bcebe62740c92ed38f09ba6205ddd9aa6f4", + "DiffID": "sha256:91bac58a9ffae0dc2031e3f90d7bf04f66ccf019f180372152b0916d6e8a796f" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/oraclelinux-8.tar.gz (oracle 8.0)", + "Class": "os-pkgs", + "Type": "oracle", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-3823", + "PkgID": "curl@7.61.1-8.el8.x86_64", + "PkgName": "curl", + "PkgIdentifier": { + "PURL": "pkg:rpm/oracle/curl@7.61.1-8.el8?arch=x86_64\u0026distro=oracle-8.0", + "UID": "2d0430c9abbc973" + }, + "InstalledVersion": "7.61.1-8.el8", + "FixedVersion": "7.61.1-11.el8", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e1b9aa33b064e76023cc29e9fac51bcebe62740c92ed38f09ba6205ddd9aa6f4", + "DiffID": "sha256:91bac58a9ffae0dc2031e3f90d7bf04f66ccf019f180372152b0916d6e8a796f" + }, + "SeveritySource": "oracle-oval", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-3823", + "DataSource": { + "ID": "oracle-oval", + "Name": "Oracle Linux OVAL definitions", + "URL": "https://linux.oracle.com/security/oval/" + }, + "Fingerprint": "sha256:6b218febe29fe60b77e69f6473825cf7d94ac09603d635af21b281a08b0dc1b0", + "Title": "curl: SMTP end-of-response out-of-bounds read", + "Description": "libcurl versions from 7.34.0 to before 7.64.0 are vulnerable to a heap out-of-bounds read in the code handling the end-of-response for SMTP. If the buffer passed to `smtp_endofresp()` isn't NUL terminated and contains no character ending the parsed number, and `len` is set to 5, then the `strtol()` call reads beyond the allocated buffer. The read contents will not be returned to the caller.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-125" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 3, + "nvd": 3, + "oracle-oval": 2, + "photon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V2Score": 5, + "V3Score": 7.5 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:L", + "V3Score": 4.3 + } + }, + "References": [ + "http://www.securityfocus.com/bid/106950", + "https://access.redhat.com/errata/RHSA-2019:3701", + "https://access.redhat.com/security/cve/CVE-2019-3823", + "https://bugzilla.redhat.com/show_bug.cgi?id=CVE-2019-3823", + "https://cert-portal.siemens.com/productcert/pdf/ssa-936080.pdf", + "https://curl.haxx.se/docs/CVE-2019-3823.html", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-3823", + "https://linux.oracle.com/cve/CVE-2019-3823.html", + "https://linux.oracle.com/errata/ELSA-2019-3701.html", + "https://lists.apache.org/thread.html/8338a0f605bdbb3a6098bb76f666a95fc2b2f53f37fa1ecc89f1146f@%3Cdevnull.infra.apache.org%3E", + "https://security.gentoo.org/glsa/201903-03", + "https://security.netapp.com/advisory/ntap-20190315-0001/", + "https://ubuntu.com/security/notices/USN-3882-1", + "https://usn.ubuntu.com/3882-1/", + "https://www.debian.org/security/2019/dsa-4386", + "https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html" + ], + "PublishedDate": "2019-02-06T20:29:00Z", + "LastModifiedDate": "2021-03-09T15:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5436", + "PkgID": "curl@7.61.1-8.el8.x86_64", + "PkgName": "curl", + "PkgIdentifier": { + "PURL": "pkg:rpm/oracle/curl@7.61.1-8.el8?arch=x86_64\u0026distro=oracle-8.0", + "UID": "2d0430c9abbc973" + }, + "InstalledVersion": "7.61.1-8.el8", + "FixedVersion": "7.61.1-12.el8", + "Status": "fixed", + "Layer": { + "Digest": "sha256:e1b9aa33b064e76023cc29e9fac51bcebe62740c92ed38f09ba6205ddd9aa6f4", + "DiffID": "sha256:91bac58a9ffae0dc2031e3f90d7bf04f66ccf019f180372152b0916d6e8a796f" + }, + "SeveritySource": "oracle-oval", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5436", + "DataSource": { + "ID": "oracle-oval", + "Name": "Oracle Linux OVAL definitions", + "URL": "https://linux.oracle.com/security/oval/" + }, + "Fingerprint": "sha256:6a1430f0c4d3c46a331ce7517353e3c41b50c653176ab2a78f1d8a56f3f77c45", + "Title": "curl: TFTP receive heap buffer overflow in tftp_receive_packet() function", + "Description": "A heap buffer overflow in the TFTP receiving code allows for DoS or arbitrary code execution in libcurl versions 7.19.4 through 7.64.1.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 1, + "arch-linux": 3, + "nvd": 3, + "oracle-oval": 2, + "photon": 3, + "redhat": 1, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00008.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-06/msg00017.html", + "http://www.openwall.com/lists/oss-security/2019/09/11/6", + "https://access.redhat.com/security/cve/CVE-2019-5436", + "https://curl.haxx.se/docs/CVE-2019-5436.html", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5436", + "https://linux.oracle.com/cve/CVE-2019-5436.html", + "https://linux.oracle.com/errata/ELSA-2020-1792.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/SMG3V4VTX2SE3EW3HQTN3DDLQBTORQC2/", + "https://seclists.org/bugtraq/2020/Feb/36", + "https://security.gentoo.org/glsa/202003-29", + "https://security.netapp.com/advisory/ntap-20190606-0004/", + "https://support.f5.com/csp/article/K55133295", + "https://support.f5.com/csp/article/K55133295?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://ubuntu.com/security/notices/USN-3993-1", + "https://ubuntu.com/security/notices/USN-3993-2", + "https://www.debian.org/security/2020/dsa-4633", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html" + ], + "PublishedDate": "2019-05-28T19:29:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/packagesprops.json.golden b/integration/testdata/packagesprops.json.golden new file mode 100644 index 000000000000..d2ca8fc1d8f1 --- /dev/null +++ b/integration/testdata/packagesprops.json.golden @@ -0,0 +1,72 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/packagesprops", + "ArtifactType": "repository", + "Results": [ + { + "Target": "Directory.Packages.props", + "Class": "lang-pkgs", + "Type": "packages-props", + "Packages": [ + { + "ID": "Newtonsoft.Json@9.0.1", + "Name": "Newtonsoft.Json", + "Identifier": { + "PURL": "pkg:nuget/Newtonsoft.Json@9.0.1", + "UID": "9350cd491a97d1ee" + }, + "Version": "9.0.1", + "AnalyzedBy": "packages-props" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "GHSA-5crp-9r3c-p9vr", + "PkgID": "Newtonsoft.Json@9.0.1", + "PkgName": "Newtonsoft.Json", + "PkgIdentifier": { + "PURL": "pkg:nuget/Newtonsoft.Json@9.0.1", + "UID": "9350cd491a97d1ee" + }, + "InstalledVersion": "9.0.1", + "FixedVersion": "13.0.1", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://github.com/advisories/GHSA-5crp-9r3c-p9vr", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Nuget", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anuget" + }, + "Fingerprint": "sha256:e13b3b0cb6475415aa4e65e77e3ebb7b3455dc20f1e5e9b0544864f596c71e98", + "Title": "Improper Handling of Exceptional Conditions in Newtonsoft.Json", + "Description": "Newtonsoft.Json prior to version 13.0.1 is vulnerable to Insecure Defaults due to improper handling of expressions with high nesting level that lead to StackOverFlow exception or high CPU and RAM usage.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-755" + ], + "VendorSeverity": { + "ghsa": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V3Score": 7.5 + } + }, + "References": [ + "https://alephsecurity.com/2018/10/22/StackOverflowException/", + "https://alephsecurity.com/vulns/aleph-2018004" + ], + "PublishedDate": "2022-06-22T15:08:47Z", + "LastModifiedDate": "2022-06-27T18:37:23Z" + } + ] + } + ] +} diff --git a/integration/testdata/photon-30.json.golden b/integration/testdata/photon-30.json.golden new file mode 100644 index 000000000000..73b6a8d0dd85 --- /dev/null +++ b/integration/testdata/photon-30.json.golden @@ -0,0 +1,297 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:1d4bc53b38b27a97aca270bea3abf3e9ea14964d02c71d2c93cd7bc53b74d660", + "ArtifactName": "testdata/fixtures/images/photon-30.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 34946560, + "OS": { + "Family": "photon", + "Name": "3.0" + }, + "ImageID": "sha256:5ccb5186b75cd13ff0d028f5b5b2bdf7ef7ca2b3d56eb2c6eb6c136077a6991a", + "DiffIDs": [ + "sha256:0f379947a276b7b051643960392fa66c2f0cb493bc1dcd471abb5545005949fd" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:photon-30" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:photon-30", + "ImageConfig": { + "architecture": "amd64", + "container": "ed27e7f1fbd8ef9d3ea89947f682907e9a65a8e51bbe2e0eba60db6e69213848", + "created": "2019-08-23T22:26:32.857588774Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-08-23T22:26:32.53400229Z", + "created_by": "/bin/sh -c #(nop) ADD file:0d19c0b1adc18a00f073eeb1a9d6e5e4fdde392b20a3229ec0ef88642549b2df in / " + }, + { + "created": "2019-08-23T22:26:32.689364313Z", + "created_by": "/bin/sh -c #(nop) LABEL name=Photon OS x86_64/3.0 Base Image vendor=VMware build-date=20190823", + "empty_layer": true + }, + { + "created": "2019-08-23T22:26:32.857588774Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:0f379947a276b7b051643960392fa66c2f0cb493bc1dcd471abb5545005949fd" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:e7cbb54381cebcd7eea8e391127352224e1d8268fd14bfa5c7dd53e507299f60", + "Labels": { + "build-date": "20190823", + "name": "Photon OS x86_64/3.0 Base Image", + "vendor": "VMware" + }, + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 34946560, + "Digest": "sha256:675aead3dff5e25094cb9f4d7cc64f05e9f04a3f3397d5d45bfbc1c8a99c3a73", + "DiffID": "sha256:0f379947a276b7b051643960392fa66c2f0cb493bc1dcd471abb5545005949fd" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/photon-30.tar.gz (photon 3.0)", + "Class": "os-pkgs", + "Type": "photon", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18276", + "PkgID": "bash@4.4.18-1.ph3.x86_64", + "PkgName": "bash", + "PkgIdentifier": { + "PURL": "pkg:rpm/photon/bash@4.4.18-1.ph3?arch=x86_64\u0026distro=photon-3.0", + "UID": "33364ec84901a6b0" + }, + "InstalledVersion": "4.4.18-1.ph3", + "FixedVersion": "4.4.18-2.ph3", + "Status": "fixed", + "Layer": { + "Digest": "sha256:675aead3dff5e25094cb9f4d7cc64f05e9f04a3f3397d5d45bfbc1c8a99c3a73", + "DiffID": "sha256:0f379947a276b7b051643960392fa66c2f0cb493bc1dcd471abb5545005949fd" + }, + "SeveritySource": "photon", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", + "DataSource": { + "ID": "photon", + "Name": "Photon OS CVE metadata", + "URL": "https://packages.broadcom.com/photon/photon_cve_metadata/" + }, + "Fingerprint": "sha256:4e81b35fc699744ea7b1f01c4d40f4be024599b0b89bc703b5d96118537d214b", + "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", + "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-273" + ], + "VendorSeverity": { + "cbl-mariner": 3, + "nvd": 3, + "oracle-oval": 1, + "photon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.2, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + } + }, + "References": [ + "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", + "https://access.redhat.com/security/cve/CVE-2019-18276", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", + "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", + "https://linux.oracle.com/cve/CVE-2019-18276.html", + "https://linux.oracle.com/errata/ELSA-2021-1679.html", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", + "https://security.gentoo.org/glsa/202105-34", + "https://security.netapp.com/advisory/ntap-20200430-0003/", + "https://www.youtube.com/watch?v=-wGtxJ8opa8" + ], + "PublishedDate": "2019-11-28T01:15:00Z", + "LastModifiedDate": "2021-05-26T12:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5481", + "PkgID": "curl@7.61.1-4.ph3.x86_64", + "PkgName": "curl", + "PkgIdentifier": { + "PURL": "pkg:rpm/photon/curl@7.61.1-4.ph3?arch=x86_64\u0026distro=photon-3.0", + "UID": "26b82259da41314d" + }, + "InstalledVersion": "7.61.1-4.ph3", + "FixedVersion": "7.61.1-5.ph3", + "Status": "fixed", + "Layer": { + "Digest": "sha256:675aead3dff5e25094cb9f4d7cc64f05e9f04a3f3397d5d45bfbc1c8a99c3a73", + "DiffID": "sha256:0f379947a276b7b051643960392fa66c2f0cb493bc1dcd471abb5545005949fd" + }, + "SeveritySource": "photon", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5481", + "DataSource": { + "ID": "photon", + "Name": "Photon OS CVE metadata", + "URL": "https://packages.broadcom.com/photon/photon_cve_metadata/" + }, + "Fingerprint": "sha256:12a8a4ae7fd278f8aeb8001707801d38ca7ccea68430208692a2b7d40860f590", + "Title": "curl: double free due to subsequent call of realloc()", + "Description": "Double-free vulnerability in the FTP-kerberos code in cURL 7.52.0 to 7.65.3.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-415" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 2, + "nvd": 4, + "oracle-oval": 2, + "photon": 4, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H", + "V3Score": 5.7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00048.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00055.html", + "https://access.redhat.com/security/cve/CVE-2019-5481", + "https://curl.haxx.se/docs/CVE-2019-5481.html", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5481", + "https://linux.oracle.com/cve/CVE-2019-5481.html", + "https://linux.oracle.com/errata/ELSA-2020-1792.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6CI4QQ2RSZX4VCFM76SIWGKY6BY7UWIC/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RGDVKSLY5JUNJRLYRUA6CXGQ2LM63XC3/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UA7KDM2WPM5CJDDGOEGFV6SSGD2J7RNT/", + "https://seclists.org/bugtraq/2020/Feb/36", + "https://security.gentoo.org/glsa/202003-29", + "https://security.netapp.com/advisory/ntap-20191004-0003/", + "https://ubuntu.com/security/notices/USN-4129-1", + "https://www.debian.org/security/2020/dsa-4633", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html" + ], + "PublishedDate": "2019-09-16T19:15:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5481", + "PkgID": "curl-libs@7.61.1-4.ph3.x86_64", + "PkgName": "curl-libs", + "PkgIdentifier": { + "PURL": "pkg:rpm/photon/curl-libs@7.61.1-4.ph3?arch=x86_64\u0026distro=photon-3.0", + "UID": "8bdb578e9ceef510" + }, + "InstalledVersion": "7.61.1-4.ph3", + "FixedVersion": "7.61.1-5.ph3", + "Status": "fixed", + "Layer": { + "Digest": "sha256:675aead3dff5e25094cb9f4d7cc64f05e9f04a3f3397d5d45bfbc1c8a99c3a73", + "DiffID": "sha256:0f379947a276b7b051643960392fa66c2f0cb493bc1dcd471abb5545005949fd" + }, + "SeveritySource": "photon", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5481", + "DataSource": { + "ID": "photon", + "Name": "Photon OS CVE metadata", + "URL": "https://packages.broadcom.com/photon/photon_cve_metadata/" + }, + "Fingerprint": "sha256:8625bdfcbb7eeaffcef1af0571c227ca2c742411ee81cb598749dbfdc4a40d8c", + "Title": "curl: double free due to subsequent call of realloc()", + "Description": "Double-free vulnerability in the FTP-kerberos code in cURL 7.52.0 to 7.65.3.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-415" + ], + "VendorSeverity": { + "amazon": 2, + "arch-linux": 2, + "nvd": 4, + "oracle-oval": 2, + "photon": 4, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:L/UI:R/S:U/C:N/I:N/A:H", + "V3Score": 5.7 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00048.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00055.html", + "https://access.redhat.com/security/cve/CVE-2019-5481", + "https://curl.haxx.se/docs/CVE-2019-5481.html", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5481", + "https://linux.oracle.com/cve/CVE-2019-5481.html", + "https://linux.oracle.com/errata/ELSA-2020-1792.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6CI4QQ2RSZX4VCFM76SIWGKY6BY7UWIC/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RGDVKSLY5JUNJRLYRUA6CXGQ2LM63XC3/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/UA7KDM2WPM5CJDDGOEGFV6SSGD2J7RNT/", + "https://seclists.org/bugtraq/2020/Feb/36", + "https://security.gentoo.org/glsa/202003-29", + "https://security.netapp.com/advisory/ntap-20191004-0003/", + "https://ubuntu.com/security/notices/USN-4129-1", + "https://www.debian.org/security/2020/dsa-4633", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html" + ], + "PublishedDate": "2019-09-16T19:15:00Z", + "LastModifiedDate": "2020-10-20T22:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/pip.json.golden b/integration/testdata/pip.json.golden new file mode 100644 index 000000000000..03d6c33e6d2e --- /dev/null +++ b/integration/testdata/pip.json.golden @@ -0,0 +1,238 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/pip", + "ArtifactType": "repository", + "Results": [ + { + "Target": "requirements.txt", + "Class": "lang-pkgs", + "Type": "pip", + "Packages": [ + { + "Name": "Flask", + "Identifier": { + "PURL": "pkg:pypi/flask@2.0.0", + "UID": "bb446a3b5f805121" + }, + "Version": "2.0.0", + "Locations": [ + { + "StartLine": 2, + "EndLine": 2 + } + ], + "AnalyzedBy": "pip" + }, + { + "Name": "Jinja2", + "Identifier": { + "PURL": "pkg:pypi/jinja2@3.0.0", + "UID": "ef6b19211d5888a8" + }, + "Version": "3.0.0", + "Locations": [ + { + "StartLine": 4, + "EndLine": 4 + } + ], + "AnalyzedBy": "pip" + }, + { + "Name": "Werkzeug", + "Identifier": { + "PURL": "pkg:pypi/werkzeug@0.11", + "UID": "8e52473a212c1e1b" + }, + "Version": "0.11", + "Locations": [ + { + "StartLine": 6, + "EndLine": 6 + } + ], + "AnalyzedBy": "pip" + }, + { + "Name": "click", + "Identifier": { + "PURL": "pkg:pypi/click@8.0.0", + "UID": "40c670b6dc896e05" + }, + "Version": "8.0.0", + "Locations": [ + { + "StartLine": 1, + "EndLine": 1 + } + ], + "AnalyzedBy": "pip" + }, + { + "Name": "itsdangerous", + "Identifier": { + "PURL": "pkg:pypi/itsdangerous@2.0.0", + "UID": "df94b2dbd6de57ad" + }, + "Version": "2.0.0", + "Locations": [ + { + "StartLine": 3, + "EndLine": 3 + } + ], + "AnalyzedBy": "pip" + }, + { + "Name": "oauth2-client", + "Identifier": { + "PURL": "pkg:pypi/oauth2-client@4.0.0", + "UID": "83f56890872d092d" + }, + "Version": "4.0.0", + "Locations": [ + { + "StartLine": 7, + "EndLine": 7 + } + ], + "AnalyzedBy": "pip" + }, + { + "Name": "python-gitlab", + "Identifier": { + "PURL": "pkg:pypi/python-gitlab@2.0.0", + "UID": "bb219054748d8bf" + }, + "Version": "2.0.0", + "Locations": [ + { + "StartLine": 8, + "EndLine": 8 + } + ], + "AnalyzedBy": "pip" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-14806", + "PkgName": "Werkzeug", + "PkgIdentifier": { + "PURL": "pkg:pypi/werkzeug@0.11", + "UID": "8e52473a212c1e1b" + }, + "InstalledVersion": "0.11", + "FixedVersion": "0.15.3", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-14806", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Pip", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + }, + "Fingerprint": "sha256:9c1631587034d298dcb56b2f2a7d70c5111500ff661917a8b9e74c04163bf987", + "Title": "python-werkzeug: insufficient debugger PIN randomness vulnerability", + "Description": "Pallets Werkzeug before 0.15.3, when used with Docker, has insufficient debugger PIN randomness because Docker containers share the same machine id.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-331" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 3, + "redhat": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 5, + "V3Score": 7.5 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 7.5 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00034.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00047.html", + "https://access.redhat.com/security/cve/CVE-2019-14806", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14806", + "https://github.com/advisories/GHSA-gq9m-qvpx-68hc", + "https://github.com/pallets/werkzeug/blob/7fef41b120327d3912fbe12fb64f1951496fcf3e/src/werkzeug/debug/__init__.py#L168", + "https://github.com/pallets/werkzeug/commit/00bc43b1672e662e5e3b8cecd79e67fc968fa246", + "https://nvd.nist.gov/vuln/detail/CVE-2019-14806", + "https://palletsprojects.com/blog/werkzeug-0-15-3-released/", + "https://ubuntu.com/security/notices/USN-4655-1" + ], + "PublishedDate": "2019-08-09T15:15:00Z", + "LastModifiedDate": "2019-09-11T00:15:00Z" + }, + { + "VulnerabilityID": "CVE-2020-28724", + "PkgName": "Werkzeug", + "PkgIdentifier": { + "PURL": "pkg:pypi/werkzeug@0.11", + "UID": "8e52473a212c1e1b" + }, + "InstalledVersion": "0.11", + "FixedVersion": "0.11.6", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-28724", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Pip", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + }, + "Fingerprint": "sha256:300309b8c04811de9465c6d286bffac243e6ff8a73e44228301621b573bb0496", + "Title": "python-werkzeug: open redirect via double slash in the URL", + "Description": "Open redirect vulnerability in werkzeug before 0.11.6 via a double slash in the URL.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-601" + ], + "VendorSeverity": { + "ghsa": 2, + "nvd": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 5.8, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N", + "V3Score": 5.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2020-28724", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28724", + "https://github.com/advisories/GHSA-3p3h-qghp-hvh2", + "https://github.com/pallets/flask/issues/1639", + "https://github.com/pallets/werkzeug/issues/822", + "https://github.com/pallets/werkzeug/pull/890/files", + "https://nvd.nist.gov/vuln/detail/CVE-2020-28724", + "https://ubuntu.com/security/notices/USN-4655-1" + ], + "PublishedDate": "2020-11-18T15:15:00Z", + "LastModifiedDate": "2020-12-01T16:05:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/pipenv.json.golden b/integration/testdata/pipenv.json.golden new file mode 100644 index 000000000000..f9ffeaa48acc --- /dev/null +++ b/integration/testdata/pipenv.json.golden @@ -0,0 +1,148 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/pipenv", + "ArtifactType": "repository", + "Results": [ + { + "Target": "Pipfile.lock", + "Class": "lang-pkgs", + "Type": "pipenv", + "Packages": [ + { + "Name": "werkzeug", + "Identifier": { + "PURL": "pkg:pypi/werkzeug@0.11.1", + "UID": "c69cfa509e6106cb" + }, + "Version": "0.11.1", + "Locations": [ + { + "StartLine": 19, + "EndLine": 26 + } + ], + "AnalyzedBy": "pipenv" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-14806", + "PkgName": "werkzeug", + "PkgIdentifier": { + "PURL": "pkg:pypi/werkzeug@0.11.1", + "UID": "c69cfa509e6106cb" + }, + "InstalledVersion": "0.11.1", + "FixedVersion": "0.15.3", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-14806", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Pip", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + }, + "Fingerprint": "sha256:2fd97e5070563e83de609aa96428cd65ae1c91b8a010d51684946273d361594c", + "Title": "python-werkzeug: insufficient debugger PIN randomness vulnerability", + "Description": "Pallets Werkzeug before 0.15.3, when used with Docker, has insufficient debugger PIN randomness because Docker containers share the same machine id.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-331" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 3, + "redhat": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 5, + "V3Score": 7.5 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 7.5 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00034.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00047.html", + "https://access.redhat.com/security/cve/CVE-2019-14806", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14806", + "https://github.com/advisories/GHSA-gq9m-qvpx-68hc", + "https://github.com/pallets/werkzeug/blob/7fef41b120327d3912fbe12fb64f1951496fcf3e/src/werkzeug/debug/__init__.py#L168", + "https://github.com/pallets/werkzeug/commit/00bc43b1672e662e5e3b8cecd79e67fc968fa246", + "https://nvd.nist.gov/vuln/detail/CVE-2019-14806", + "https://palletsprojects.com/blog/werkzeug-0-15-3-released/", + "https://ubuntu.com/security/notices/USN-4655-1" + ], + "PublishedDate": "2019-08-09T15:15:00Z", + "LastModifiedDate": "2019-09-11T00:15:00Z" + }, + { + "VulnerabilityID": "CVE-2020-28724", + "PkgName": "werkzeug", + "PkgIdentifier": { + "PURL": "pkg:pypi/werkzeug@0.11.1", + "UID": "c69cfa509e6106cb" + }, + "InstalledVersion": "0.11.1", + "FixedVersion": "0.11.6", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-28724", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Pip", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + }, + "Fingerprint": "sha256:ac2fd70194b69ef5db6a0f32d726fc67aa5c0d0f71fe71f5dcb3f26bc573e048", + "Title": "python-werkzeug: open redirect via double slash in the URL", + "Description": "Open redirect vulnerability in werkzeug before 0.11.6 via a double slash in the URL.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-601" + ], + "VendorSeverity": { + "ghsa": 2, + "nvd": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 5.8, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N", + "V3Score": 5.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2020-28724", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28724", + "https://github.com/advisories/GHSA-3p3h-qghp-hvh2", + "https://github.com/pallets/flask/issues/1639", + "https://github.com/pallets/werkzeug/issues/822", + "https://github.com/pallets/werkzeug/pull/890/files", + "https://nvd.nist.gov/vuln/detail/CVE-2020-28724", + "https://ubuntu.com/security/notices/USN-4655-1" + ], + "PublishedDate": "2020-11-18T15:15:00Z", + "LastModifiedDate": "2020-12-01T16:05:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/pnpm.json.golden b/integration/testdata/pnpm.json.golden new file mode 100644 index 000000000000..7cb18afb7b95 --- /dev/null +++ b/integration/testdata/pnpm.json.golden @@ -0,0 +1,237 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/pnpm", + "ArtifactType": "repository", + "Results": [ + { + "Target": "pnpm-lock.yaml", + "Class": "lang-pkgs", + "Type": "pnpm", + "Packages": [ + { + "ID": "jquery@3.3.9", + "Name": "jquery", + "Identifier": { + "PURL": "pkg:npm/jquery@3.3.9", + "UID": "25b60ed46b0948c6" + }, + "Version": "3.3.9", + "Licenses": [ + "MIT" + ], + "Relationship": "direct", + "AnalyzedBy": "pnpm" + }, + { + "ID": "lodash@4.17.4", + "Name": "lodash", + "Identifier": { + "PURL": "pkg:npm/lodash@4.17.4", + "UID": "e6922bf2badcc4a1" + }, + "Version": "4.17.4", + "Licenses": [ + "MIT" + ], + "Relationship": "direct", + "AnalyzedBy": "pnpm" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-11358", + "PkgID": "jquery@3.3.9", + "PkgName": "jquery", + "PkgIdentifier": { + "PURL": "pkg:npm/jquery@3.3.9", + "UID": "25b60ed46b0948c6" + }, + "InstalledVersion": "3.3.9", + "FixedVersion": "3.4.0", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-11358", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Fingerprint": "sha256:b224983d08f6952e8e045d27f01129e92af0c21d1b26ca2e60dd7f5a5e20541c", + "Title": "jquery: Prototype pollution in object's prototype leading to denial of service, remote code execution, or property injection", + "Description": "jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-79" + ], + "VendorSeverity": { + "alma": 2, + "amazon": 2, + "arch-linux": 2, + "ghsa": 2, + "nodejs-security-wg": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ruby-advisory-db": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 4.3, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html", + "http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html", + "http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html", + "http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html", + "http://seclists.org/fulldisclosure/2019/May/10", + "http://seclists.org/fulldisclosure/2019/May/11", + "http://seclists.org/fulldisclosure/2019/May/13", + "http://www.openwall.com/lists/oss-security/2019/06/03/2", + "http://www.securityfocus.com/bid/108023", + "https://access.redhat.com/errata/RHBA-2019:1570", + "https://access.redhat.com/errata/RHSA-2019:1456", + "https://access.redhat.com/errata/RHSA-2019:2587", + "https://access.redhat.com/errata/RHSA-2019:3023", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://access.redhat.com/security/cve/CVE-2019-11358", + "https://backdropcms.org/security/backdrop-sa-core-2019-009", + "https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358", + "https://github.com/DanielRuf/snyk-js-jquery-174006?files=1", + "https://github.com/advisories/GHSA-6c3j-c64m-qhgq", + "https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b", + "https://github.com/jquery/jquery/pull/4333", + "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434", + "https://hackerone.com/reports/454365", + "https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601", + "https://linux.oracle.com/cve/CVE-2019-11358.html", + "https://linux.oracle.com/errata/ELSA-2020-4847.html", + "https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E", + "https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E", + "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", + "https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E", + "https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E", + "https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html", + "https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-11358", + "https://seclists.org/bugtraq/2019/Apr/32", + "https://seclists.org/bugtraq/2019/Jun/12", + "https://seclists.org/bugtraq/2019/May/18", + "https://security.netapp.com/advisory/ntap-20190919-0001/", + "https://snyk.io/vuln/SNYK-JS-JQUERY-174006", + "https://www.debian.org/security/2019/dsa-4434", + "https://www.debian.org/security/2019/dsa-4460", + "https://www.drupal.org/sa-core-2019-006", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/", + "https://www.synology.com/security/advisory/Synology_SA_19_19", + "https://www.tenable.com/security/tns-2019-08", + "https://www.tenable.com/security/tns-2020-02" + ], + "PublishedDate": "2019-04-20T00:29:00Z", + "LastModifiedDate": "2021-10-20T11:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-10744", + "PkgID": "lodash@4.17.4", + "PkgName": "lodash", + "PkgIdentifier": { + "PURL": "pkg:npm/lodash@4.17.4", + "UID": "e6922bf2badcc4a1" + }, + "InstalledVersion": "4.17.4", + "FixedVersion": "4.17.12", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-10744", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Fingerprint": "sha256:3621fc78d65c2c488364cb190536be9cc41896291bf1a3d8c1874a392a5c9e40", + "Title": "nodejs-lodash: prototype pollution in defaultsDeep function leading to modifying properties", + "Description": "Versions of lodash lower than 4.17.12 are vulnerable to Prototype Pollution. The function defaultsDeep could be tricked into adding or modifying properties of Object.prototype using a constructor payload.", + "Severity": "CRITICAL", + "VendorSeverity": { + "ghsa": 4, + "nvd": 4, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:N/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H", + "V2Score": 6.4, + "V3Score": 9.1 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:H", + "V3Score": 9.1 + } + }, + "References": [ + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://access.redhat.com/security/cve/CVE-2019-10744", + "https://github.com/advisories/GHSA-jf85-cpcp-j695", + "https://github.com/lodash/lodash/pull/4336", + "https://nvd.nist.gov/vuln/detail/CVE-2019-10744", + "https://security.netapp.com/advisory/ntap-20191004-0005/", + "https://snyk.io/vuln/SNYK-JS-LODASH-450202", + "https://support.f5.com/csp/article/K47105354?utm_source=f5support\u0026amp;utm_medium=RSS", + "https://www.npmjs.com/advisories/1065", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html" + ], + "PublishedDate": "2019-07-26T00:15:00Z", + "LastModifiedDate": "2021-03-16T13:57:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/poetry.json.golden b/integration/testdata/poetry.json.golden new file mode 100644 index 000000000000..17e7e1f1ec2d --- /dev/null +++ b/integration/testdata/poetry.json.golden @@ -0,0 +1,116 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/poetry", + "ArtifactType": "repository", + "Results": [ + { + "Target": "poetry.lock", + "Class": "lang-pkgs", + "Type": "poetry", + "Packages": [ + { + "ID": "click@8.1.3", + "Name": "click", + "Identifier": { + "PURL": "pkg:pypi/click@8.1.3", + "UID": "f583a10d818c65e3" + }, + "Version": "8.1.3", + "Relationship": "direct", + "DependsOn": [ + "colorama@0.4.6" + ], + "AnalyzedBy": "poetry" + }, + { + "ID": "werkzeug@0.14", + "Name": "werkzeug", + "Identifier": { + "PURL": "pkg:pypi/werkzeug@0.14", + "UID": "af148406687755af" + }, + "Version": "0.14", + "Relationship": "direct", + "AnalyzedBy": "poetry" + }, + { + "ID": "colorama@0.4.6", + "Name": "colorama", + "Identifier": { + "PURL": "pkg:pypi/colorama@0.4.6", + "UID": "ea4efeecb78be4ea" + }, + "Version": "0.4.6", + "Indirect": true, + "Relationship": "indirect", + "AnalyzedBy": "poetry" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-14806", + "PkgID": "werkzeug@0.14", + "PkgName": "werkzeug", + "PkgIdentifier": { + "PURL": "pkg:pypi/werkzeug@0.14", + "UID": "af148406687755af" + }, + "InstalledVersion": "0.14", + "FixedVersion": "0.15.3", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-14806", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Pip", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + }, + "Fingerprint": "sha256:384f3c3504a17ae835c015e5cab33befe675700289119a47182dd7102165eb2c", + "Title": "python-werkzeug: insufficient debugger PIN randomness vulnerability", + "Description": "Pallets Werkzeug before 0.15.3, when used with Docker, has insufficient debugger PIN randomness because Docker containers share the same machine id.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-331" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 3, + "redhat": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 5, + "V3Score": 7.5 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 7.5 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00034.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00047.html", + "https://access.redhat.com/security/cve/CVE-2019-14806", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14806", + "https://github.com/advisories/GHSA-gq9m-qvpx-68hc", + "https://github.com/pallets/werkzeug/blob/7fef41b120327d3912fbe12fb64f1951496fcf3e/src/werkzeug/debug/__init__.py#L168", + "https://github.com/pallets/werkzeug/commit/00bc43b1672e662e5e3b8cecd79e67fc968fa246", + "https://nvd.nist.gov/vuln/detail/CVE-2019-14806", + "https://palletsprojects.com/blog/werkzeug-0-15-3-released/", + "https://ubuntu.com/security/notices/USN-4655-1" + ], + "PublishedDate": "2019-08-09T15:15:00Z", + "LastModifiedDate": "2019-09-11T00:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/pom-cyclonedx.json.golden b/integration/testdata/pom-cyclonedx.json.golden new file mode 100644 index 000000000000..bb5f88732ed1 --- /dev/null +++ b/integration/testdata/pom-cyclonedx.json.golden @@ -0,0 +1,327 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:3ff14136-e09f-4df9-80ea-000000000006", + "version": 1, + "metadata": { + "timestamp": "2021-08-25T12:20:30+00:00", + "tools": { + "components": [ + { + "type": "application", + "manufacturer": { + "name": "Aqua Security Software Ltd." + }, + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "3ff14136-e09f-4df9-80ea-000000000002", + "type": "application", + "name": "testdata/fixtures/repo/pom", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "3ff14136-e09f-4df9-80ea-000000000003", + "type": "application", + "name": "pom.xml", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "pom" + } + ] + }, + { + "bom-ref": "pkg:maven/com.example/log4shell@1.0-SNAPSHOT", + "type": "library", + "group": "com.example", + "name": "log4shell", + "version": "1.0-SNAPSHOT", + "purl": "pkg:maven/com.example/log4shell@1.0-SNAPSHOT", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "com.example:log4shell:1.0-SNAPSHOT::b21b31f8" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "pom" + } + ] + }, + { + "bom-ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "type": "library", + "group": "com.fasterxml.jackson.core", + "name": "jackson-databind", + "version": "2.9.1", + "purl": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "com.fasterxml.jackson.core:jackson-databind:2.9.1::6a9e9547" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "pom" + } + ] + } + ], + "dependencies": [ + { + "ref": "3ff14136-e09f-4df9-80ea-000000000002", + "dependsOn": [ + "3ff14136-e09f-4df9-80ea-000000000003" + ] + }, + { + "ref": "3ff14136-e09f-4df9-80ea-000000000003", + "dependsOn": [ + "pkg:maven/com.example/log4shell@1.0-SNAPSHOT" + ] + }, + { + "ref": "pkg:maven/com.example/log4shell@1.0-SNAPSHOT", + "dependsOn": [ + "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1" + ] + }, + { + "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "dependsOn": [] + } + ], + "vulnerabilities": [ + { + "id": "CVE-2020-9548", + "source": { + "name": "ghsa", + "url": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven" + }, + "ratings": [ + { + "source": { + "name": "ghsa" + }, + "severity": "critical" + }, + { + "source": { + "name": "nvd" + }, + "score": 6.8, + "severity": "medium", + "method": "CVSSv2", + "vector": "AV:N/AC:M/Au:N/C:P/I:P/A:P" + }, + { + "source": { + "name": "nvd" + }, + "score": 9.8, + "severity": "critical", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "redhat" + }, + "score": 8.1, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "cwes": [ + 502 + ], + "description": "FasterXML jackson-databind 2.x before 2.9.10.4 mishandles the interaction between serialization gadgets and typing, related to br.com.anteros.dbcp.AnterosDBCPConfig (aka anteros-core).", + "recommendation": "Upgrade com.fasterxml.jackson.core:jackson-databind to version 2.9.10.4", + "advisories": [ + { + "url": "https://avd.aquasec.com/nvd/cve-2020-9548" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2020-9548" + }, + { + "url": "https://github.com/FasterXML/jackson-databind/issues/2634" + }, + { + "url": "https://github.com/advisories/GHSA-p43x-xfjf-5jhr" + }, + { + "url": "https://lists.apache.org/thread.html/r35d30db00440ef63b791c4b7f7acb036e14d4a23afa2a249cb66c0fd@%3Cissues.zookeeper.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/r9464a40d25c3ba1a55622db72f113eb494a889656962d098c70c5bb1@%3Cdev.zookeeper.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/r98c9b6e4c9e17792e2cd1ec3e4aa20b61a791939046d3f10888176bb@%3Cissues.zookeeper.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/rb6fecb5e96a6d61e175ff49f33f2713798dd05cf03067c169d195596@%3Cissues.zookeeper.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/rd5a4457be4623038c3989294429bc063eec433a2e55995d81591e2ca@%3Cissues.zookeeper.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/rdd49ab9565bec436a896bc00c4b9fc9dce1598e106c318524fbdfec6@%3Cissues.zookeeper.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/rdd4df698d5d8e635144d2994922bf0842e933809eae259521f3b5097@%3Cissues.zookeeper.apache.org%3E" + }, + { + "url": "https://lists.apache.org/thread.html/rf1bbc0ea4a9f014cf94df9a12a6477d24a27f52741dbc87f2fd52ff2@%3Cissues.geode.apache.org%3E" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2020/03/msg00008.html" + }, + { + "url": "https://medium.com/@cowtowncoder/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062" + }, + { + "url": "https://nvd.nist.gov/vuln/detail/CVE-2020-9548" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20200904-0006/" + }, + { + "url": "https://www.oracle.com/security-alerts/cpujan2021.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpujul2020.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpuoct2020.html" + }, + { + "url": "https://www.oracle.com/security-alerts/cpuoct2021.html" + } + ], + "published": "2020-03-02T04:15:00+00:00", + "updated": "2021-12-02T21:23:00+00:00", + "affects": [ + { + "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "versions": [ + { + "version": "2.9.1", + "status": "affected" + } + ] + } + ] + }, + { + "id": "CVE-2021-20190", + "source": { + "name": "glad", + "url": "https://gitlab.com/gitlab-org/advisories-community" + }, + "ratings": [ + { + "source": { + "name": "ghsa" + }, + "severity": "high" + }, + { + "source": { + "name": "nvd" + }, + "score": 8.3, + "severity": "high", + "method": "CVSSv2", + "vector": "AV:N/AC:M/Au:N/C:P/I:P/A:C" + }, + { + "source": { + "name": "nvd" + }, + "score": 8.1, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H" + }, + { + "source": { + "name": "redhat" + }, + "score": 8.1, + "severity": "high", + "method": "CVSSv31", + "vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H" + } + ], + "cwes": [ + 502 + ], + "description": "A flaw was found in jackson-databind before 2.9.10.7. FasterXML mishandles the interaction between serialization gadgets and typing. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "recommendation": "Upgrade com.fasterxml.jackson.core:jackson-databind to version 2.9.10.7", + "advisories": [ + { + "url": "https://avd.aquasec.com/nvd/cve-2021-20190" + }, + { + "url": "https://access.redhat.com/security/cve/CVE-2021-20190" + }, + { + "url": "https://bugzilla.redhat.com/show_bug.cgi?id=1916633" + }, + { + "url": "https://github.com/FasterXML/jackson-databind/commit/7dbf51bf78d157098074a20bd9da39bd48c18e4a" + }, + { + "url": "https://github.com/FasterXML/jackson-databind/issues/2854" + }, + { + "url": "https://github.com/advisories/GHSA-5949-rw7g-wx7w" + }, + { + "url": "https://lists.apache.org/thread.html/r380e9257bacb8551ee6fcf2c59890ae9477b2c78e553fa9ea08e9d9a@%3Ccommits.nifi.apache.org%3E" + }, + { + "url": "https://lists.debian.org/debian-lts-announce/2021/04/msg00025.html" + }, + { + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-20190" + }, + { + "url": "https://security.netapp.com/advisory/ntap-20210219-0008/" + } + ], + "published": "2021-01-19T17:15:00+00:00", + "updated": "2021-07-20T23:15:00+00:00", + "affects": [ + { + "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "versions": [ + { + "version": "2.9.1", + "status": "affected" + } + ] + } + ] + } + ] +} diff --git a/integration/testdata/pom.json.golden b/integration/testdata/pom.json.golden new file mode 100644 index 000000000000..ff815ab73901 --- /dev/null +++ b/integration/testdata/pom.json.golden @@ -0,0 +1,141 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/pom", + "ArtifactType": "repository", + "Results": [ + { + "Target": "pom.xml", + "Class": "lang-pkgs", + "Type": "pom", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-9548", + "PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1::6a9e9547", + "PkgName": "com.fasterxml.jackson.core:jackson-databind", + "PkgIdentifier": { + "PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "UID": "9b28c9c796734030" + }, + "InstalledVersion": "2.9.1", + "FixedVersion": "2.9.10.4", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-9548", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Maven", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven" + }, + "Fingerprint": "sha256:e8090470bc38669d0a9a73e684105597938cc3890b6e77f6cfda6e73b98c2f08", + "Title": "jackson-databind: Serialization gadgets in anteros-core", + "Description": "FasterXML jackson-databind 2.x before 2.9.10.4 mishandles the interaction between serialization gadgets and typing, related to br.com.anteros.dbcp.AnterosDBCPConfig (aka anteros-core).", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 4, + "nvd": 4, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 6.8, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2020-9548", + "https://github.com/FasterXML/jackson-databind/issues/2634", + "https://github.com/advisories/GHSA-p43x-xfjf-5jhr", + "https://lists.apache.org/thread.html/r35d30db00440ef63b791c4b7f7acb036e14d4a23afa2a249cb66c0fd@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/r9464a40d25c3ba1a55622db72f113eb494a889656962d098c70c5bb1@%3Cdev.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/r98c9b6e4c9e17792e2cd1ec3e4aa20b61a791939046d3f10888176bb@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rb6fecb5e96a6d61e175ff49f33f2713798dd05cf03067c169d195596@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rd5a4457be4623038c3989294429bc063eec433a2e55995d81591e2ca@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rdd49ab9565bec436a896bc00c4b9fc9dce1598e106c318524fbdfec6@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rdd4df698d5d8e635144d2994922bf0842e933809eae259521f3b5097@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rf1bbc0ea4a9f014cf94df9a12a6477d24a27f52741dbc87f2fd52ff2@%3Cissues.geode.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2020/03/msg00008.html", + "https://medium.com/@cowtowncoder/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062", + "https://nvd.nist.gov/vuln/detail/CVE-2020-9548", + "https://security.netapp.com/advisory/ntap-20200904-0006/", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html" + ], + "PublishedDate": "2020-03-02T04:15:00Z", + "LastModifiedDate": "2021-12-02T21:23:00Z" + }, + { + "VulnerabilityID": "CVE-2021-20190", + "PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1::6a9e9547", + "PkgName": "com.fasterxml.jackson.core:jackson-databind", + "PkgIdentifier": { + "PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "UID": "9b28c9c796734030" + }, + "InstalledVersion": "2.9.1", + "FixedVersion": "2.9.10.7", + "Status": "fixed", + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-20190", + "DataSource": { + "ID": "glad", + "Name": "GitLab Advisory Database Community", + "URL": "https://gitlab.com/gitlab-org/advisories-community" + }, + "Fingerprint": "sha256:bceb5c9900547204285dff59a40c785acbcffbece6b4bc9fc31055352809ba0a", + "Title": "jackson-databind: mishandles the interaction between serialization gadgets and typing, related to javax.swing", + "Description": "A flaw was found in jackson-databind before 2.9.10.7. FasterXML mishandles the interaction between serialization gadgets and typing. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 3, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:C", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 8.3, + "V3Score": 8.1 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2021-20190", + "https://bugzilla.redhat.com/show_bug.cgi?id=1916633", + "https://github.com/FasterXML/jackson-databind/commit/7dbf51bf78d157098074a20bd9da39bd48c18e4a", + "https://github.com/FasterXML/jackson-databind/issues/2854", + "https://github.com/advisories/GHSA-5949-rw7g-wx7w", + "https://lists.apache.org/thread.html/r380e9257bacb8551ee6fcf2c59890ae9477b2c78e553fa9ea08e9d9a@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2021/04/msg00025.html", + "https://nvd.nist.gov/vuln/detail/CVE-2021-20190", + "https://security.netapp.com/advisory/ntap-20210219-0008/" + ], + "PublishedDate": "2021-01-19T17:15:00Z", + "LastModifiedDate": "2021-07-20T23:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/pubspec.lock.json.golden b/integration/testdata/pubspec.lock.json.golden new file mode 100644 index 000000000000..6a0c3f39facc --- /dev/null +++ b/integration/testdata/pubspec.lock.json.golden @@ -0,0 +1,90 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/pubspec", + "ArtifactType": "repository", + "Results": [ + { + "Target": "pubspec.lock", + "Class": "lang-pkgs", + "Type": "pub", + "Packages": [ + { + "ID": "http@0.13.2", + "Name": "http", + "Identifier": { + "PURL": "pkg:pub/http@0.13.2", + "UID": "2907d8123351fdba" + }, + "Version": "0.13.2", + "Relationship": "direct", + "AnalyzedBy": "pubspec-lock" + }, + { + "ID": "shelf@1.3.1", + "Name": "shelf", + "Identifier": { + "PURL": "pkg:pub/shelf@1.3.1", + "UID": "3e0921fc3c061e4" + }, + "Version": "1.3.1", + "Indirect": true, + "Relationship": "indirect", + "AnalyzedBy": "pubspec-lock" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-35669", + "PkgID": "http@0.13.2", + "PkgName": "http", + "PkgIdentifier": { + "PURL": "pkg:pub/http@0.13.2", + "UID": "2907d8123351fdba" + }, + "InstalledVersion": "0.13.2", + "FixedVersion": "0.13.3", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-35669", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Pub", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apub" + }, + "Fingerprint": "sha256:0ec48cdcd118d8d131695d33df58c350289079a51fcfd6e9e53d48c3b5b623cc", + "Title": "http before 0.13.3 vulnerable to header injection", + "Description": "An issue was discovered in the http package before 0.13.3 for Dart. If the attacker controls the HTTP method and the app is using Request directly, it's possible to achieve CRLF injection in an HTTP request via HTTP header injection. This issue has been addressed in commit abb2bb182 by validating request methods.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-74" + ], + "VendorSeverity": { + "ghsa": 2 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V3Score": 6.1 + } + }, + "References": [ + "https://nvd.nist.gov/vuln/detail/CVE-2020-35669", + "https://github.com/dart-lang/http/issues/511", + "https://github.com/dart-lang/http/blob/master/CHANGELOG.md#0133", + "https://github.com/dart-lang/http/pull/512", + "https://github.com/dart-lang/http/commit/abb2bb182fbd7f03aafd1f889b902d7b3bdb8769", + "https://pub.dev/packages/http/changelog#0133", + "https://github.com/advisories/GHSA-4rgh-jx4f-qfcq" + ], + "PublishedDate": "2022-05-24T17:37:16Z", + "LastModifiedDate": "2022-10-06T20:26:08Z" + } + ] + } + ] +} diff --git a/integration/testdata/rockylinux-8.json.golden b/integration/testdata/rockylinux-8.json.golden new file mode 100644 index 000000000000..a1981360f958 --- /dev/null +++ b/integration/testdata/rockylinux-8.json.golden @@ -0,0 +1,160 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:ed11998b28b0bcd0488c4d2fda300f80d71ac058ce7eb12a43a4deb312ce429c", + "ArtifactName": "testdata/fixtures/images/rockylinux-8.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 211280384, + "OS": { + "Family": "rocky", + "Name": "8.5" + }, + "ImageID": "sha256:210996f98b856d7cd00496ddbe9412e73f1c714c95de09661e07b4e43648f9ab", + "DiffIDs": [ + "sha256:65dbea0a4b39709e0a2cc8624fd99478e9f302c0a5661d7676d6d3bd3cb6d181" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:rockylinux-8" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:rockylinux-8", + "ImageConfig": { + "architecture": "amd64", + "container": "16458df10693f731fae0492f791a5e4b725245c35cf28c7fca00982219d7bdf3", + "created": "2021-12-15T20:22:37.180885096Z", + "docker_version": "20.10.7", + "history": [ + { + "created": "2021-12-15T20:22:36.373826081Z", + "created_by": "/bin/sh -c #(nop) ADD file:790b4c6a174560d4701baf59e884e7d07f50f0e193e545d6d5ed1d7390979d1a in / " + }, + { + "created": "2021-12-15T20:22:37.180885096Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:65dbea0a4b39709e0a2cc8624fd99478e9f302c0a5661d7676d6d3bd3cb6d181" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:b3d7893772a2427ad53224d9db4c70be399de0a28c09804ac0c5cb203ab0244e" + } + }, + "Layers": [ + { + "Size": 211280384, + "Digest": "sha256:72a2451028f11c6927678e5f1bb8f35b4e723d3b342ec1a6980d7b5591cf81d6", + "DiffID": "sha256:65dbea0a4b39709e0a2cc8624fd99478e9f302c0a5661d7676d6d3bd3cb6d181" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/rockylinux-8.tar.gz (rocky 8.5)", + "Class": "os-pkgs", + "Type": "rocky", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2021-3712", + "PkgID": "openssl-libs@1.1.1k-4.el8.x86_64", + "PkgName": "openssl-libs", + "PkgIdentifier": { + "PURL": "pkg:rpm/rocky/openssl-libs@1.1.1k-4.el8?arch=x86_64\u0026distro=rocky-8.5\u0026epoch=1", + "UID": "5eab5fb49fb02b60" + }, + "InstalledVersion": "1:1.1.1k-4.el8", + "FixedVersion": "1:1.1.1k-5.el8_5", + "Status": "fixed", + "Layer": { + "Digest": "sha256:72a2451028f11c6927678e5f1bb8f35b4e723d3b342ec1a6980d7b5591cf81d6", + "DiffID": "sha256:65dbea0a4b39709e0a2cc8624fd99478e9f302c0a5661d7676d6d3bd3cb6d181" + }, + "SeveritySource": "rocky", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-3712", + "DataSource": { + "ID": "rocky", + "Name": "Rocky Linux updateinfo", + "URL": "https://download.rockylinux.org/pub/rocky/" + }, + "Fingerprint": "sha256:ec40d326eb53ea2b209aeca6f61763f4d2f93f26821899c0642bd84aca475149", + "Title": "openssl: Read buffer overruns processing ASN.1 strings", + "Description": "ASN.1 strings are represented internally within OpenSSL as an ASN1_STRING structure which contains a buffer holding the string data and a field holding the buffer length. This contrasts with normal C strings which are represented as a buffer for the string data which is terminated with a NUL (0) byte. Although not a strict requirement, ASN.1 strings that are parsed using OpenSSL's own \"d2i\" functions (and other similar parsing functions) as well as any string whose value has been set with the ASN1_STRING_set() function will additionally NUL terminate the byte array in the ASN1_STRING structure. However, it is possible for applications to directly construct valid ASN1_STRING structures which do not NUL terminate the byte array by directly setting the \"data\" and \"length\" fields in the ASN1_STRING array. This can also happen by using the ASN1_STRING_set0() function. Numerous OpenSSL functions that print ASN.1 data have been found to assume that the ASN1_STRING byte array will be NUL terminated, even though this is not guaranteed for strings that have been directly constructed. Where an application requests an ASN.1 structure to be printed, and where that ASN.1 structure contains ASN1_STRINGs that have been directly constructed by the application without NUL terminating the \"data\" field, then a read buffer overrun can occur. The same thing can also occur during name constraints processing of certificates (for example if a certificate has been directly constructed by the application instead of loading it via the OpenSSL parsing functions, and the certificate contains non NUL terminated ASN1_STRING structures). It can also occur in the X509_get1_email(), X509_REQ_get1_email() and X509_get1_ocsp() functions. If a malicious actor can cause an application to directly construct an ASN1_STRING and then process it through one of the affected OpenSSL functions then this issue could be hit. This might result in a crash (causing a Denial of Service attack). It could also result in the disclosure of private memory contents (such as private keys, or sensitive plaintext). Fixed in OpenSSL 1.1.1l (Affected 1.1.1-1.1.1k). Fixed in OpenSSL 1.0.2za (Affected 1.0.2-1.0.2y).", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-125" + ], + "VendorSeverity": { + "alma": 2, + "amazon": 2, + "arch-linux": 3, + "cbl-mariner": 3, + "nvd": 3, + "oracle-oval": 2, + "photon": 3, + "redhat": 2, + "rocky": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:N/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V2Score": 5.8, + "V3Score": 7.4 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:H", + "V3Score": 7.4 + } + }, + "References": [ + "http://www.openwall.com/lists/oss-security/2021/08/26/2", + "https://access.redhat.com/hydra/rest/securitydata/cve/CVE-2021-3712.json", + "https://access.redhat.com/security/cve/CVE-2021-3712", + "https://crates.io/crates/openssl-src", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3712", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=94d23fcff9b2a7a8368dfe52214d5c2569882c11", + "https://git.openssl.org/gitweb/?p=openssl.git;a=commitdiff;h=ccb0a11145ee72b042d10593a64eaf9e8a55ec12", + "https://kc.mcafee.com/corporate/index?page=content\u0026id=SB10366", + "https://linux.oracle.com/cve/CVE-2021-3712.html", + "https://linux.oracle.com/errata/ELSA-2022-9023.html", + "https://lists.apache.org/thread.html/r18995de860f0e63635f3008fd2a6aca82394249476d21691e7c59c9e@%3Cdev.tomcat.apache.org%3E", + "https://lists.apache.org/thread.html/rad5d9f83f0d11fb3f8bb148d179b8a9ad7c6a17f18d70e5805a713d1@%3Cdev.tomcat.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2021/09/msg00014.html", + "https://lists.debian.org/debian-lts-announce/2021/09/msg00021.html", + "https://nvd.nist.gov/vuln/detail/CVE-2021-3712", + "https://rustsec.org/advisories/RUSTSEC-2021-0098.html", + "https://security.netapp.com/advisory/ntap-20210827-0010/", + "https://ubuntu.com/security/notices/USN-5051-1", + "https://ubuntu.com/security/notices/USN-5051-2", + "https://ubuntu.com/security/notices/USN-5051-3", + "https://ubuntu.com/security/notices/USN-5051-4 (regression only in trusty/esm)", + "https://ubuntu.com/security/notices/USN-5088-1", + "https://www.debian.org/security/2021/dsa-4963", + "https://www.openssl.org/news/secadv/20210824.txt", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://www.tenable.com/security/tns-2021-16", + "https://www.tenable.com/security/tns-2022-02" + ], + "PublishedDate": "2021-08-24T15:15:00Z", + "LastModifiedDate": "2022-01-06T09:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/sbt.json.golden b/integration/testdata/sbt.json.golden new file mode 100644 index 000000000000..655e32d792cb --- /dev/null +++ b/integration/testdata/sbt.json.golden @@ -0,0 +1,141 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/sbt", + "ArtifactType": "repository", + "Results": [ + { + "Target": "build.sbt.lock", + "Class": "lang-pkgs", + "Type": "sbt", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-9548", + "PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1", + "PkgName": "com.fasterxml.jackson.core:jackson-databind", + "PkgIdentifier": { + "PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "UID": "cd79961e5c058bb1" + }, + "InstalledVersion": "2.9.1", + "FixedVersion": "2.9.10.4", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-9548", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Maven", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven" + }, + "Fingerprint": "sha256:5041dfe19323df2a9efd7b1330da7e17d55b6a3e7677fb33f456cdba6fa1c130", + "Title": "jackson-databind: Serialization gadgets in anteros-core", + "Description": "FasterXML jackson-databind 2.x before 2.9.10.4 mishandles the interaction between serialization gadgets and typing, related to br.com.anteros.dbcp.AnterosDBCPConfig (aka anteros-core).", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 4, + "nvd": 4, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 6.8, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2020-9548", + "https://github.com/FasterXML/jackson-databind/issues/2634", + "https://github.com/advisories/GHSA-p43x-xfjf-5jhr", + "https://lists.apache.org/thread.html/r35d30db00440ef63b791c4b7f7acb036e14d4a23afa2a249cb66c0fd@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/r9464a40d25c3ba1a55622db72f113eb494a889656962d098c70c5bb1@%3Cdev.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/r98c9b6e4c9e17792e2cd1ec3e4aa20b61a791939046d3f10888176bb@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rb6fecb5e96a6d61e175ff49f33f2713798dd05cf03067c169d195596@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rd5a4457be4623038c3989294429bc063eec433a2e55995d81591e2ca@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rdd49ab9565bec436a896bc00c4b9fc9dce1598e106c318524fbdfec6@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rdd4df698d5d8e635144d2994922bf0842e933809eae259521f3b5097@%3Cissues.zookeeper.apache.org%3E", + "https://lists.apache.org/thread.html/rf1bbc0ea4a9f014cf94df9a12a6477d24a27f52741dbc87f2fd52ff2@%3Cissues.geode.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2020/03/msg00008.html", + "https://medium.com/@cowtowncoder/on-jackson-cves-dont-panic-here-is-what-you-need-to-know-54cd0d6e8062", + "https://nvd.nist.gov/vuln/detail/CVE-2020-9548", + "https://security.netapp.com/advisory/ntap-20200904-0006/", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html" + ], + "PublishedDate": "2020-03-02T04:15:00Z", + "LastModifiedDate": "2021-12-02T21:23:00Z" + }, + { + "VulnerabilityID": "CVE-2021-20190", + "PkgID": "com.fasterxml.jackson.core:jackson-databind:2.9.1", + "PkgName": "com.fasterxml.jackson.core:jackson-databind", + "PkgIdentifier": { + "PURL": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.9.1", + "UID": "cd79961e5c058bb1" + }, + "InstalledVersion": "2.9.1", + "FixedVersion": "2.9.10.7", + "Status": "fixed", + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-20190", + "DataSource": { + "ID": "glad", + "Name": "GitLab Advisory Database Community", + "URL": "https://gitlab.com/gitlab-org/advisories-community" + }, + "Fingerprint": "sha256:bccb32c5dccac94eda00dcd6c75ef35b85bb9aa07503c4b717295f49f00146af", + "Title": "jackson-databind: mishandles the interaction between serialization gadgets and typing, related to javax.swing", + "Description": "A flaw was found in jackson-databind before 2.9.10.7. FasterXML mishandles the interaction between serialization gadgets and typing. The highest threat from this vulnerability is to data confidentiality and integrity as well as system availability.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-502" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 3, + "redhat": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:C", + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 8.3, + "V3Score": 8.1 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2021-20190", + "https://bugzilla.redhat.com/show_bug.cgi?id=1916633", + "https://github.com/FasterXML/jackson-databind/commit/7dbf51bf78d157098074a20bd9da39bd48c18e4a", + "https://github.com/FasterXML/jackson-databind/issues/2854", + "https://github.com/advisories/GHSA-5949-rw7g-wx7w", + "https://lists.apache.org/thread.html/r380e9257bacb8551ee6fcf2c59890ae9477b2c78e553fa9ea08e9d9a@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2021/04/msg00025.html", + "https://nvd.nist.gov/vuln/detail/CVE-2021-20190", + "https://security.netapp.com/advisory/ntap-20210219-0008/" + ], + "PublishedDate": "2021-01-19T17:15:00Z", + "LastModifiedDate": "2021-07-20T23:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/secrets.asff.golden b/integration/testdata/secrets.asff.golden new file mode 100644 index 000000000000..b3a7c45fadac --- /dev/null +++ b/integration/testdata/secrets.asff.golden @@ -0,0 +1,62 @@ +{ + "Findings": [{ + "SchemaVersion": "2018-10-08", + "Id": "deploy.sh", + "ProductArn": "arn:aws:securityhub:test-region::product/aquasecurity/aquasecurity", + "GeneratorId": "Trivy", + "AwsAccountId": "123456789012", + "Types": [ "Sensitive Data Identifications" ], + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "UpdatedAt": "2021-08-25T12:20:30.000000005Z", + "Severity": { + "Label": "CRITICAL" + }, + "Title": "Trivy found a secret in deploy.sh: AWS Access Key ID", + "Description": "Trivy found a secret in deploy.sh: AWS Access Key ID", + "ProductFields": { "Product Name": "Trivy" }, + "Resources": [ + { + "Type": "Other", + "Id": "deploy.sh", + "Partition": "aws", + "Region": "test-region", + "Details": { + "Other": { + "Filename": "deploy.sh" + } + } + } + ], + "RecordState": "ACTIVE" + },{ + "SchemaVersion": "2018-10-08", + "Id": "deploy.sh", + "ProductArn": "arn:aws:securityhub:test-region::product/aquasecurity/aquasecurity", + "GeneratorId": "Trivy", + "AwsAccountId": "123456789012", + "Types": [ "Sensitive Data Identifications" ], + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "UpdatedAt": "2021-08-25T12:20:30.000000005Z", + "Severity": { + "Label": "CRITICAL" + }, + "Title": "Trivy found a secret in deploy.sh: GitHub Personal Access Token", + "Description": "Trivy found a secret in deploy.sh: GitHub Personal Access Token", + "ProductFields": { "Product Name": "Trivy" }, + "Resources": [ + { + "Type": "Other", + "Id": "deploy.sh", + "Partition": "aws", + "Region": "test-region", + "Details": { + "Other": { + "Filename": "deploy.sh" + } + } + } + ], + "RecordState": "ACTIVE" + } + ] +} diff --git a/integration/testdata/secrets.json.golden b/integration/testdata/secrets.json.golden new file mode 100644 index 000000000000..7b046f96b961 --- /dev/null +++ b/integration/testdata/secrets.json.golden @@ -0,0 +1,113 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/secrets", + "ArtifactType": "repository", + "Results": [ + { + "Target": "deploy.sh", + "Class": "secret", + "Secrets": [ + { + "RuleID": "aws-access-key-id", + "Category": "AWS", + "Severity": "CRITICAL", + "Title": "AWS Access Key ID", + "StartLine": 3, + "EndLine": 3, + "Code": { + "Lines": [ + { + "Number": 1, + "Content": "#!/bin/sh", + "IsCause": false, + "Annotation": "", + "Truncated": false, + "Highlighted": "#!/bin/sh", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 2, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 3, + "Content": "export AWS_ACCESS_KEY_ID=********************", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "export AWS_ACCESS_KEY_ID=********************", + "FirstCause": true, + "LastCause": true + }, + { + "Number": 4, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Match": "export AWS_ACCESS_KEY_ID=********************", + "Offset": 36 + }, + { + "RuleID": "mysecret", + "Category": "Custom", + "Severity": "HIGH", + "Title": "My Secret", + "StartLine": 7, + "EndLine": 7, + "Code": { + "Lines": [ + { + "Number": 5, + "Content": "export GITHUB_PAT=ghp_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "IsCause": false, + "Annotation": "", + "Truncated": false, + "Highlighted": "export GITHUB_PAT=ghp_0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 6, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 7, + "Content": "echo ********", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "echo ********", + "FirstCause": true, + "LastCause": true + } + ] + }, + "Match": "echo ********", + "Offset": 123 + } + ] + } + ] +} diff --git a/integration/testdata/sl-micro-rancher5.4.json.golden b/integration/testdata/sl-micro-rancher5.4.json.golden new file mode 100644 index 000000000000..0466d9b316c9 --- /dev/null +++ b/integration/testdata/sl-micro-rancher5.4.json.golden @@ -0,0 +1,86 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:f3d716e4652bf4a60bf4289eace7bb2b46878fa9461c86b12b4c059126563aee", + "ArtifactName": "testdata/fixtures/images/sle-micro-rancher-5.4_ndb.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 709748736, + "OS": { + "Family": "slem", + "Name": "5.4" + }, + "ImageID": "sha256:c45ec974938acac29c893b5d273d73e4ebdd7e6a97b6fa861dfbd8dd430b9016", + "DiffIDs": [ + "sha256:7cdd3aec849d122d63dc83a5e1e2fb89b341c67b03e25979131ca335a463bb57" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:sle-micro-rancher-5.4_ndb" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:sle-micro-rancher-5.4_ndb", + "ImageConfig": { + "architecture": "amd64", + "author": "SUSE LLC (https://www.suse.com/)", + "created": "2024-09-03T17:54:39Z", + "history": [ + { + "author": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "created": "2024-09-03T17:54:39Z", + "created_by": "KIWI 9.24.43" + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:7cdd3aec849d122d63dc83a5e1e2fb89b341c67b03e25979131ca335a463bb57" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Labels": { + "com.suse.eula": "sle-eula", + "com.suse.image-type": "sle-micro", + "com.suse.release-stage": "released", + "com.suse.sle.micro.rancher.created": "2024-09-03T17:53:32.129328086Z", + "com.suse.sle.micro.rancher.description": "Image containing a micro environment for containers based on the SLE Micro for Rancher.", + "com.suse.sle.micro.rancher.disturl": "obs://build.suse.de/SUSE:SLE-15-SP4:Update:Products:Micro54:Update:CR/images/fcaa3a91b132f1955fa900b902aef7f2-SLE-Micro-Rancher", + "com.suse.sle.micro.rancher.reference": "registry.suse.com/suse/sle-micro-rancher/5.4:%PKG_VERSION%-%RELEASE", + "com.suse.sle.micro.rancher.title": "SLE Micro for Rancher Base Container", + "com.suse.sle.micro.rancher.url": "https://www.suse.com/products/micro/", + "com.suse.sle.micro.rancher.vendor": "SUSE LLC", + "com.suse.sle.micro.rancher.version": "5.4", + "com.suse.supportlevel": "l3", + "org.openbuildservice.disturl": "obs://build.suse.de/SUSE:SLE-15-SP4:Update:Products:Micro54:Update:CR/images/fcaa3a91b132f1955fa900b902aef7f2-SLE-Micro-Rancher", + "org.opencontainers.image.created": "2024-09-03T17:53:32.129328086Z", + "org.opencontainers.image.description": "Image containing a micro environment for containers based on the SLE Micro for Rancher.", + "org.opencontainers.image.title": "SLE Micro for Rancher Base Container", + "org.opencontainers.image.url": "https://www.suse.com/products/micro/", + "org.opencontainers.image.vendor": "SUSE LLC", + "org.opencontainers.image.version": "5.4", + "org.suse.reference": "registry.suse.com/suse/sle-micro-rancher/5.4:%PKG_VERSION%-%RELEASE" + } + } + }, + "Layers": [ + { + "Size": 709748736, + "Digest": "sha256:832d9b8a90d142f11cf1b5d3f0b5d6c099be9b7af791b426138a4205b6ee78de", + "DiffID": "sha256:7cdd3aec849d122d63dc83a5e1e2fb89b341c67b03e25979131ca335a463bb57" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/sle-micro-rancher-5.4_ndb.tar.gz (slem 5.4)", + "Class": "os-pkgs", + "Type": "slem" + } + ] +} diff --git a/integration/testdata/spring4shell-jre11.json.golden b/integration/testdata/spring4shell-jre11.json.golden new file mode 100644 index 000000000000..0638db17e3d0 --- /dev/null +++ b/integration/testdata/spring4shell-jre11.json.golden @@ -0,0 +1,337 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:d5fbfb11da0ffb72f8bdeae29420a8101a04534c281854fe5ace22ec13d133bb", + "ArtifactName": "testdata/fixtures/images/spring4shell-jre11.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 270418944, + "OS": { + "Family": "debian", + "Name": "11.3" + }, + "ImageID": "sha256:ed8f0747d483b60657982f0ef1ba74482aed08795cf0eb774b00bc53022a8351", + "DiffIDs": [ + "sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce", + "sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72", + "sha256:1f0e278ace87a84577de56c99e5c05c6af6f8b582d1eb8dfd7de7be4cf215775", + "sha256:64272e9218cd019d57b84ac283aa35036cbd8c1dcface8c69f756088a0a13c45", + "sha256:8e6776c643c1db15d540016171fe04137ee2a26c7d0b18bfebdcbd31c6b0d8b3", + "sha256:0b201a611e5455d637c719d70eb5dd76fd4154bc4a5cf597d67ed2fb6647cc42", + "sha256:19da2426772aaa344a242e474fd7906d272fc8ded6eef5b4e461a4aa0725d7e5", + "sha256:1fdc094b0e85888d2204310083e3c09fff6a4daeecf22692aa6be5e8b4001f94", + "sha256:192960b65b1579403b36581de471fd2bd75a043b4743552f27ba16623f02c68f" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:spring4shell-jre11" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:spring4shell-jre11", + "ImageConfig": { + "architecture": "amd64", + "created": "2022-06-07T03:41:13.228952Z", + "docker_version": "20.10.14", + "history": [ + { + "created": "2022-03-29T00:22:18.812238611Z", + "created_by": "/bin/sh -c #(nop) ADD file:966d3669b40f5fbaecee1ecbeb58debe19001076da5d94717080d55efbc25971 in / " + }, + { + "created": "2022-03-29T00:22:19.186561403Z", + "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", + "empty_layer": true + }, + { + "created": "2022-03-29T00:52:15.681202963Z", + "created_by": "/bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates p11-kit \t; \trm -rf /var/lib/apt/lists/*" + }, + { + "created": "2022-03-29T00:55:28.571451389Z", + "created_by": "/bin/sh -c #(nop) ENV JAVA_HOME=/usr/local/openjdk-11", + "empty_layer": true + }, + { + "created": "2022-03-29T00:55:29.092016566Z", + "created_by": "/bin/sh -c { echo '#/bin/sh'; echo 'echo \"$JAVA_HOME\"'; } \u003e /usr/local/bin/docker-java-home \u0026\u0026 chmod +x /usr/local/bin/docker-java-home \u0026\u0026 [ \"$JAVA_HOME\" = \"$(docker-java-home)\" ] # backwards compatibility" + }, + { + "created": "2022-03-29T00:55:29.206969756Z", + "created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "empty_layer": true + }, + { + "created": "2022-03-29T00:55:29.302995298Z", + "created_by": "/bin/sh -c #(nop) ENV LANG=C.UTF-8", + "empty_layer": true + }, + { + "created": "2022-03-29T00:55:29.392969112Z", + "created_by": "/bin/sh -c #(nop) ENV JAVA_VERSION=11.0.14.1", + "empty_layer": true + }, + { + "created": "2022-03-29T00:56:45.085797918Z", + "created_by": "/bin/sh -c set -eux; \t\tarch=\"$(dpkg --print-architecture)\"; \tcase \"$arch\" in \t\t'amd64') \t\t\tdownloadUrl='https://github.com/AdoptOpenJDK/openjdk11-upstream-binaries/releases/download/jdk-11.0.14.1%2B1/OpenJDK11U-jre_x64_linux_11.0.14.1_1.tar.gz'; \t\t\t;; \t\t'arm64') \t\t\tdownloadUrl='https://github.com/AdoptOpenJDK/openjdk11-upstream-binaries/releases/download/jdk-11.0.14.1%2B1/OpenJDK11U-jre_aarch64_linux_11.0.14.1_1.tar.gz'; \t\t\t;; \t\t*) echo \u003e\u00262 \"error: unsupported architecture: '$arch'\"; exit 1 ;; \tesac; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tdirmngr \t\tgnupg \t\twget \t; \trm -rf /var/lib/apt/lists/*; \t\twget --progress=dot:giga -O openjdk.tgz \"$downloadUrl\"; \twget --progress=dot:giga -O openjdk.tgz.asc \"$downloadUrl.sign\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \tgpg --batch --keyserver keyserver.ubuntu.com --recv-keys EAC843EBD3EFDB98CC772FADA5CD6035332FA671; \tgpg --batch --keyserver keyserver.ubuntu.com --keyserver-options no-self-sigs-only --recv-keys CA5F11C6CE22644D42C6AC4492EF8D39DC13168F; \tgpg --batch --list-sigs --keyid-format 0xLONG CA5F11C6CE22644D42C6AC4492EF8D39DC13168F \t\t| tee /dev/stderr \t\t| grep '0xA5CD6035332FA671' \t\t| grep 'Andrew Haley'; \tgpg --batch --verify openjdk.tgz.asc openjdk.tgz; \tgpgconf --kill all; \trm -rf \"$GNUPGHOME\"; \t\tmkdir -p \"$JAVA_HOME\"; \ttar --extract \t\t--file openjdk.tgz \t\t--directory \"$JAVA_HOME\" \t\t--strip-components 1 \t\t--no-same-owner \t; \trm openjdk.tgz*; \t\tapt-mark auto '.*' \u003e /dev/null; \t[ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark \u003e /dev/null; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \t\t{ \t\techo '#!/usr/bin/env bash'; \t\techo 'set -Eeuo pipefail'; \t\techo 'trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth \"$JAVA_HOME/lib/security/cacerts\"'; \t} \u003e /etc/ca-certificates/update.d/docker-openjdk; \tchmod +x /etc/ca-certificates/update.d/docker-openjdk; \t/etc/ca-certificates/update.d/docker-openjdk; \t\tfind \"$JAVA_HOME/lib\" -name '*.so' -exec dirname '{}' ';' | sort -u \u003e /etc/ld.so.conf.d/docker-openjdk.conf; \tldconfig; \t\tjava -Xshare:dump; \t\tjava --version" + }, + { + "created": "2022-03-30T05:16:56.493239413Z", + "created_by": "/bin/sh -c #(nop) ENV CATALINA_HOME=/usr/local/tomcat", + "empty_layer": true + }, + { + "created": "2022-03-30T05:16:56.592339446Z", + "created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/tomcat/bin:/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "empty_layer": true + }, + { + "created": "2022-03-30T05:16:57.135799132Z", + "created_by": "/bin/sh -c mkdir -p \"$CATALINA_HOME\"" + }, + { + "created": "2022-03-30T05:16:57.234962251Z", + "created_by": "/bin/sh -c #(nop) WORKDIR /usr/local/tomcat", + "empty_layer": true + }, + { + "created": "2022-03-30T05:16:57.332478398Z", + "created_by": "/bin/sh -c #(nop) ENV TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib", + "empty_layer": true + }, + { + "created": "2022-03-30T05:16:57.423152329Z", + "created_by": "/bin/sh -c #(nop) ENV LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib", + "empty_layer": true + }, + { + "created": "2022-03-30T05:38:59.455604207Z", + "created_by": "/bin/sh -c #(nop) ENV GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 5C3C5F3E314C866292F359A8F3AD5C94A67F707E 765908099ACF92702C7D949BFA0C35EA8AA299F1 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23", + "empty_layer": true + }, + { + "created": "2022-03-30T05:38:59.550766811Z", + "created_by": "/bin/sh -c #(nop) ENV TOMCAT_MAJOR=8", + "empty_layer": true + }, + { + "created": "2022-03-30T05:38:59.643674076Z", + "created_by": "/bin/sh -c #(nop) ENV TOMCAT_VERSION=8.5.77", + "empty_layer": true + }, + { + "created": "2022-03-30T05:38:59.744285526Z", + "created_by": "/bin/sh -c #(nop) ENV TOMCAT_SHA512=50f96584cbbbeeda92a3b573e7fe7e2c49e57ed4bc5246257dc1409abac0710b49fa7049a0dd9a3b8467bca2aa078ef608f49b676c1abf12529528ff71bb0260", + "empty_layer": true + }, + { + "created": "2022-03-30T05:39:00.204794279Z", + "created_by": "/bin/sh -c #(nop) COPY dir:92f3a0f303b55a048a73bf243c664f89aa86500eab95c7d20c2da44ed3fb434b in /usr/local/tomcat " + }, + { + "created": "2022-03-30T05:39:03.786979035Z", + "created_by": "/bin/sh -c set -eux; \tapt-get update; \txargs -rt apt-get install -y --no-install-recommends \u003c \"$TOMCAT_NATIVE_LIBDIR/.dependencies.txt\"; \trm -rf /var/lib/apt/lists/*" + }, + { + "created": "2022-03-30T05:39:05.151055599Z", + "created_by": "/bin/sh -c set -eux; \tnativeLines=\"$(catalina.sh configtest 2\u003e\u00261)\"; \tnativeLines=\"$(echo \"$nativeLines\" | grep 'Apache Tomcat Native')\"; \tnativeLines=\"$(echo \"$nativeLines\" | sort -u)\"; \tif ! echo \"$nativeLines\" | grep -E 'INFO: Loaded( APR based)? Apache Tomcat Native library' \u003e\u00262; then \t\techo \u003e\u00262 \"$nativeLines\"; \t\texit 1; \tfi" + }, + { + "created": "2022-03-30T05:39:05.243348189Z", + "created_by": "/bin/sh -c #(nop) EXPOSE 8080", + "empty_layer": true + }, + { + "created": "2022-03-30T05:39:05.342897424Z", + "created_by": "/bin/sh -c #(nop) CMD [\"catalina.sh\" \"run\"]", + "empty_layer": true + }, + { + "created": "2022-06-07T03:41:13.228952Z", + "created_by": "/bin/sh -c #(nop) COPY file:4a1136b54136f8775efe918c4cd6af1ad1e507b36a49286d4f2c6bde722d33f4 in /usr/local/tomcat/webapps/ " + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce", + "sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72", + "sha256:1f0e278ace87a84577de56c99e5c05c6af6f8b582d1eb8dfd7de7be4cf215775", + "sha256:64272e9218cd019d57b84ac283aa35036cbd8c1dcface8c69f756088a0a13c45", + "sha256:8e6776c643c1db15d540016171fe04137ee2a26c7d0b18bfebdcbd31c6b0d8b3", + "sha256:0b201a611e5455d637c719d70eb5dd76fd4154bc4a5cf597d67ed2fb6647cc42", + "sha256:19da2426772aaa344a242e474fd7906d272fc8ded6eef5b4e461a4aa0725d7e5", + "sha256:1fdc094b0e85888d2204310083e3c09fff6a4daeecf22692aa6be5e8b4001f94", + "sha256:192960b65b1579403b36581de471fd2bd75a043b4743552f27ba16623f02c68f" + ] + }, + "config": { + "Cmd": [ + "catalina.sh", + "run" + ], + "Env": [ + "PATH=/usr/local/tomcat/bin:/usr/local/openjdk-11/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "JAVA_HOME=/usr/local/openjdk-11", + "LANG=C.UTF-8", + "JAVA_VERSION=11.0.14.1", + "CATALINA_HOME=/usr/local/tomcat", + "TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib", + "LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib", + "GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 5C3C5F3E314C866292F359A8F3AD5C94A67F707E 765908099ACF92702C7D949BFA0C35EA8AA299F1 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23", + "TOMCAT_MAJOR=8", + "TOMCAT_VERSION=8.5.77", + "TOMCAT_SHA512=50f96584cbbbeeda92a3b573e7fe7e2c49e57ed4bc5246257dc1409abac0710b49fa7049a0dd9a3b8467bca2aa078ef608f49b676c1abf12529528ff71bb0260" + ], + "Image": "sha256:8ac2c9cef8f1bb48394c1b2ee81cc1d2096323a7a7cec4781d601eeaf7c32b03", + "WorkingDir": "/usr/local/tomcat", + "ExposedPorts": { + "8080/tcp": {} + } + } + }, + "Layers": [ + { + "Size": 83895296, + "Digest": "sha256:c229119241af7b23b121052a1cae4c03e0a477a72ea6a7f463ad7623ff8f274b", + "DiffID": "sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce" + }, + { + "Size": 5177344, + "Digest": "sha256:2f1dc05f270bad654ee17f1143c48586c188a72929a128d61fd8ae15905d7b00", + "DiffID": "sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72" + }, + { + "Size": 3584, + "Digest": "sha256:39b2c24c052eb115ae98ac01ea7a403af9bd678866744f0eea033d71d18f893b", + "DiffID": "sha256:1f0e278ace87a84577de56c99e5c05c6af6f8b582d1eb8dfd7de7be4cf215775" + }, + { + "Size": 142896128, + "Digest": "sha256:e94fd7d3bf7a9b78b61be8303cd35eb9da3f8d121cf572a3b8878cbf11e84818", + "DiffID": "sha256:64272e9218cd019d57b84ac283aa35036cbd8c1dcface8c69f756088a0a13c45" + }, + { + "Size": 3072, + "Digest": "sha256:b7bcef2d90f7acb11e94822db33bc4011654954b978fa35c89acac5f7b8d9620", + "DiffID": "sha256:8e6776c643c1db15d540016171fe04137ee2a26c7d0b18bfebdcbd31c6b0d8b3" + }, + { + "Size": 18070016, + "Digest": "sha256:ac3639dc6fd33e9eeead58a99c277cb06b8f69ba6a30fe7028e9677a67d94bd8", + "DiffID": "sha256:0b201a611e5455d637c719d70eb5dd76fd4154bc4a5cf597d67ed2fb6647cc42" + }, + { + "Size": 1369088, + "Digest": "sha256:7b4e8e3a75a692d7c80924f540916787b858aac41b99d55e2786a6edb9520c19", + "DiffID": "sha256:19da2426772aaa344a242e474fd7906d272fc8ded6eef5b4e461a4aa0725d7e5" + }, + { + "Size": 2048, + "Digest": "sha256:9177f8e1f9863beb4f13bfeb344734617e6c30d09ccbbaa4ff2830c2874a3c99", + "DiffID": "sha256:1fdc094b0e85888d2204310083e3c09fff6a4daeecf22692aa6be5e8b4001f94" + }, + { + "Size": 19002368, + "Digest": "sha256:b47862f824700e0ea830e568e989fba777d8223c1f8321c6256b0c965b9f61ee", + "DiffID": "sha256:192960b65b1579403b36581de471fd2bd75a043b4743552f27ba16623f02c68f" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/spring4shell-jre11.tar.gz (debian 11.3)", + "Class": "os-pkgs", + "Type": "debian" + }, + { + "Target": "Java", + "Class": "lang-pkgs", + "Type": "jar", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-22965", + "PkgName": "org.springframework:spring-beans", + "PkgPath": "usr/local/tomcat/webapps/helloworld.war/WEB-INF/lib/spring-beans-5.3.15.jar", + "PkgIdentifier": { + "PURL": "pkg:maven/org.springframework/spring-beans@5.3.15", + "UID": "382ba53322e3ac2a" + }, + "InstalledVersion": "5.3.15", + "FixedVersion": "5.3.18", + "Status": "fixed", + "Layer": { + "Digest": "sha256:b47862f824700e0ea830e568e989fba777d8223c1f8321c6256b0c965b9f61ee", + "DiffID": "sha256:192960b65b1579403b36581de471fd2bd75a043b4743552f27ba16623f02c68f" + }, + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-22965", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Maven", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven" + }, + "Fingerprint": "sha256:cf84a68b74dc2edf3ffe5f8c5bc3b401b54272268c332d9a041dde1d8c90e25b", + "Title": "spring-framework: RCE via Data Binding on JDK 9+", + "Description": "A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it.", + "Severity": "CRITICAL", + "CweIDs": [ + "CWE-94" + ], + "VendorSeverity": { + "ghsa": 4, + "nvd": 4, + "redhat": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 9.8 + }, + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://github.com/advisories/GHSA-36p3-wjmg-h94x" + ], + "PublishedDate": "2022-04-01T23:15:00Z", + "LastModifiedDate": "2022-05-19T14:21:00Z" + } + ] + }, + { + "Target": "", + "Class": "custom", + "CustomResources": [ + { + "Type": "spring4shell/java-major-version", + "FilePath": "/usr/local/openjdk-11/release", + "Layer": { + "Digest": "sha256:e94fd7d3bf7a9b78b61be8303cd35eb9da3f8d121cf572a3b8878cbf11e84818", + "DiffID": "sha256:64272e9218cd019d57b84ac283aa35036cbd8c1dcface8c69f756088a0a13c45" + }, + "Data": "11.0.14.1" + }, + { + "Type": "spring4shell/tomcat-version", + "FilePath": "/usr/local/tomcat/RELEASE-NOTES", + "Layer": { + "Digest": "sha256:ac3639dc6fd33e9eeead58a99c277cb06b8f69ba6a30fe7028e9677a67d94bd8", + "DiffID": "sha256:0b201a611e5455d637c719d70eb5dd76fd4154bc4a5cf597d67ed2fb6647cc42" + }, + "Data": "8.5.77" + } + ] + } + ] +} diff --git a/integration/testdata/spring4shell-jre8.json.golden b/integration/testdata/spring4shell-jre8.json.golden new file mode 100644 index 000000000000..5aadc737de81 --- /dev/null +++ b/integration/testdata/spring4shell-jre8.json.golden @@ -0,0 +1,337 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:8ac64b751688d0e2577befd47be60baea54f7294bee6deeb0288b580daf6ca52", + "ArtifactName": "testdata/fixtures/images/spring4shell-jre8.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 236810240, + "OS": { + "Family": "debian", + "Name": "11.3" + }, + "ImageID": "sha256:b88bc3d2f0b5aacf1d36efa498f427d923b01c854dac090acf5368c55ac04fda", + "DiffIDs": [ + "sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce", + "sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72", + "sha256:0e78b1e5673e8cc7c102fdda9e6b830b7dee2b29b178f34d25d9be59387e6950", + "sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1", + "sha256:053db4876c0df3df3294ee00e32e140b130ba33807d088750cb69b0e6fad158e", + "sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6", + "sha256:868d710aa4dc5fc4793508564fc45c991ed8d5f6ab3e4cf52bb856f29546f3d8", + "sha256:77b2d158369254d5055183f5483f8b6661170857b61768d1d95d18c2ec1714b6", + "sha256:eb769943b91f10a0418f2fc3b4a4fde6c6293be60c37293fcc0fa319edaf27a5" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:spring4shell-jre8" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:spring4shell-jre8", + "ImageConfig": { + "architecture": "amd64", + "created": "2022-06-06T13:51:57.120019Z", + "docker_version": "20.10.14", + "history": [ + { + "created": "2022-03-29T00:22:18.812238611Z", + "created_by": "/bin/sh -c #(nop) ADD file:966d3669b40f5fbaecee1ecbeb58debe19001076da5d94717080d55efbc25971 in / " + }, + { + "created": "2022-03-29T00:22:19.186561403Z", + "created_by": "/bin/sh -c #(nop) CMD [\"bash\"]", + "empty_layer": true + }, + { + "created": "2022-03-29T00:52:15.681202963Z", + "created_by": "/bin/sh -c set -eux; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tca-certificates p11-kit \t; \trm -rf /var/lib/apt/lists/*" + }, + { + "created": "2022-03-29T00:57:17.289858958Z", + "created_by": "/bin/sh -c #(nop) ENV JAVA_HOME=/usr/local/openjdk-8", + "empty_layer": true + }, + { + "created": "2022-03-29T00:57:17.842070347Z", + "created_by": "/bin/sh -c { echo '#/bin/sh'; echo 'echo \"$JAVA_HOME\"'; } \u003e /usr/local/bin/docker-java-home \u0026\u0026 chmod +x /usr/local/bin/docker-java-home \u0026\u0026 [ \"$JAVA_HOME\" = \"$(docker-java-home)\" ] # backwards compatibility" + }, + { + "created": "2022-03-29T00:57:17.930626834Z", + "created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/openjdk-8/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "empty_layer": true + }, + { + "created": "2022-03-29T00:57:18.024334574Z", + "created_by": "/bin/sh -c #(nop) ENV LANG=C.UTF-8", + "empty_layer": true + }, + { + "created": "2022-03-29T00:57:18.115954625Z", + "created_by": "/bin/sh -c #(nop) ENV JAVA_VERSION=8u322", + "empty_layer": true + }, + { + "created": "2022-03-29T00:58:15.033935002Z", + "created_by": "/bin/sh -c set -eux; \t\tarch=\"$(dpkg --print-architecture)\"; \tcase \"$arch\" in \t\t'amd64') \t\t\tdownloadUrl='https://github.com/AdoptOpenJDK/openjdk8-upstream-binaries/releases/download/jdk8u322-b06/OpenJDK8U-jre_x64_linux_8u322b06.tar.gz'; \t\t\t;; \t\t'arm64') \t\t\tdownloadUrl='https://github.com/AdoptOpenJDK/openjdk8-upstream-binaries/releases/download/jdk8u322-b06/OpenJDK8U-jre_aarch64_linux_8u322b06.tar.gz'; \t\t\t;; \t\t*) echo \u003e\u00262 \"error: unsupported architecture: '$arch'\"; exit 1 ;; \tesac; \t\tsavedAptMark=\"$(apt-mark showmanual)\"; \tapt-get update; \tapt-get install -y --no-install-recommends \t\tdirmngr \t\tgnupg \t\twget \t; \trm -rf /var/lib/apt/lists/*; \t\twget --progress=dot:giga -O openjdk.tgz \"$downloadUrl\"; \twget --progress=dot:giga -O openjdk.tgz.asc \"$downloadUrl.sign\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \tgpg --batch --keyserver keyserver.ubuntu.com --recv-keys EAC843EBD3EFDB98CC772FADA5CD6035332FA671; \tgpg --batch --keyserver keyserver.ubuntu.com --keyserver-options no-self-sigs-only --recv-keys CA5F11C6CE22644D42C6AC4492EF8D39DC13168F; \tgpg --batch --list-sigs --keyid-format 0xLONG CA5F11C6CE22644D42C6AC4492EF8D39DC13168F \t\t| tee /dev/stderr \t\t| grep '0xA5CD6035332FA671' \t\t| grep 'Andrew Haley'; \tgpg --batch --verify openjdk.tgz.asc openjdk.tgz; \tgpgconf --kill all; \trm -rf \"$GNUPGHOME\"; \t\tmkdir -p \"$JAVA_HOME\"; \ttar --extract \t\t--file openjdk.tgz \t\t--directory \"$JAVA_HOME\" \t\t--strip-components 1 \t\t--no-same-owner \t; \trm openjdk.tgz*; \t\tapt-mark auto '.*' \u003e /dev/null; \t[ -z \"$savedAptMark\" ] || apt-mark manual $savedAptMark \u003e /dev/null; \tapt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \t\t{ \t\techo '#!/usr/bin/env bash'; \t\techo 'set -Eeuo pipefail'; \t\techo 'trust extract --overwrite --format=java-cacerts --filter=ca-anchors --purpose=server-auth \"$JAVA_HOME/lib/security/cacerts\"'; \t} \u003e /etc/ca-certificates/update.d/docker-openjdk; \tchmod +x /etc/ca-certificates/update.d/docker-openjdk; \t/etc/ca-certificates/update.d/docker-openjdk; \t\tfind \"$JAVA_HOME/lib\" -name '*.so' -exec dirname '{}' ';' | sort -u \u003e /etc/ld.so.conf.d/docker-openjdk.conf; \tldconfig; \t\tjava -version" + }, + { + "created": "2022-03-30T05:24:58.62001394Z", + "created_by": "/bin/sh -c #(nop) ENV CATALINA_HOME=/usr/local/tomcat", + "empty_layer": true + }, + { + "created": "2022-03-30T05:24:58.713370816Z", + "created_by": "/bin/sh -c #(nop) ENV PATH=/usr/local/tomcat/bin:/usr/local/openjdk-8/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "empty_layer": true + }, + { + "created": "2022-03-30T05:24:59.23051474Z", + "created_by": "/bin/sh -c mkdir -p \"$CATALINA_HOME\"" + }, + { + "created": "2022-03-30T05:24:59.327279736Z", + "created_by": "/bin/sh -c #(nop) WORKDIR /usr/local/tomcat", + "empty_layer": true + }, + { + "created": "2022-03-30T05:24:59.421537441Z", + "created_by": "/bin/sh -c #(nop) ENV TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib", + "empty_layer": true + }, + { + "created": "2022-03-30T05:24:59.517366785Z", + "created_by": "/bin/sh -c #(nop) ENV LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib", + "empty_layer": true + }, + { + "created": "2022-03-30T05:42:09.694872721Z", + "created_by": "/bin/sh -c #(nop) ENV GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 5C3C5F3E314C866292F359A8F3AD5C94A67F707E 765908099ACF92702C7D949BFA0C35EA8AA299F1 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23", + "empty_layer": true + }, + { + "created": "2022-03-30T05:42:09.790546995Z", + "created_by": "/bin/sh -c #(nop) ENV TOMCAT_MAJOR=8", + "empty_layer": true + }, + { + "created": "2022-03-30T05:42:09.882411219Z", + "created_by": "/bin/sh -c #(nop) ENV TOMCAT_VERSION=8.5.77", + "empty_layer": true + }, + { + "created": "2022-03-30T05:42:09.972728447Z", + "created_by": "/bin/sh -c #(nop) ENV TOMCAT_SHA512=50f96584cbbbeeda92a3b573e7fe7e2c49e57ed4bc5246257dc1409abac0710b49fa7049a0dd9a3b8467bca2aa078ef608f49b676c1abf12529528ff71bb0260", + "empty_layer": true + }, + { + "created": "2022-03-30T05:42:10.425808771Z", + "created_by": "/bin/sh -c #(nop) COPY dir:4b2b2ad5d081ef9a92d92841b7b643214ae6e21bbb961e3197c3615dd08813ab in /usr/local/tomcat " + }, + { + "created": "2022-03-30T05:42:13.894391571Z", + "created_by": "/bin/sh -c set -eux; \tapt-get update; \txargs -rt apt-get install -y --no-install-recommends \u003c \"$TOMCAT_NATIVE_LIBDIR/.dependencies.txt\"; \trm -rf /var/lib/apt/lists/*" + }, + { + "created": "2022-03-30T05:42:15.098598192Z", + "created_by": "/bin/sh -c set -eux; \tnativeLines=\"$(catalina.sh configtest 2\u003e\u00261)\"; \tnativeLines=\"$(echo \"$nativeLines\" | grep 'Apache Tomcat Native')\"; \tnativeLines=\"$(echo \"$nativeLines\" | sort -u)\"; \tif ! echo \"$nativeLines\" | grep -E 'INFO: Loaded( APR based)? Apache Tomcat Native library' \u003e\u00262; then \t\techo \u003e\u00262 \"$nativeLines\"; \t\texit 1; \tfi" + }, + { + "created": "2022-03-30T05:42:15.18926788Z", + "created_by": "/bin/sh -c #(nop) EXPOSE 8080", + "empty_layer": true + }, + { + "created": "2022-03-30T05:42:15.283787651Z", + "created_by": "/bin/sh -c #(nop) CMD [\"catalina.sh\" \"run\"]", + "empty_layer": true + }, + { + "created": "2022-06-06T13:51:57.120019Z", + "created_by": "/bin/sh -c #(nop) COPY file:4a1136b54136f8775efe918c4cd6af1ad1e507b36a49286d4f2c6bde722d33f4 in /usr/local/tomcat/webapps/ " + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce", + "sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72", + "sha256:0e78b1e5673e8cc7c102fdda9e6b830b7dee2b29b178f34d25d9be59387e6950", + "sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1", + "sha256:053db4876c0df3df3294ee00e32e140b130ba33807d088750cb69b0e6fad158e", + "sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6", + "sha256:868d710aa4dc5fc4793508564fc45c991ed8d5f6ab3e4cf52bb856f29546f3d8", + "sha256:77b2d158369254d5055183f5483f8b6661170857b61768d1d95d18c2ec1714b6", + "sha256:eb769943b91f10a0418f2fc3b4a4fde6c6293be60c37293fcc0fa319edaf27a5" + ] + }, + "config": { + "Cmd": [ + "catalina.sh", + "run" + ], + "Env": [ + "PATH=/usr/local/tomcat/bin:/usr/local/openjdk-8/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "JAVA_HOME=/usr/local/openjdk-8", + "LANG=C.UTF-8", + "JAVA_VERSION=8u322", + "CATALINA_HOME=/usr/local/tomcat", + "TOMCAT_NATIVE_LIBDIR=/usr/local/tomcat/native-jni-lib", + "LD_LIBRARY_PATH=/usr/local/tomcat/native-jni-lib", + "GPG_KEYS=05AB33110949707C93A279E3D3EFE6B686867BA6 07E48665A34DCAFAE522E5E6266191C37C037D42 47309207D818FFD8DCD3F83F1931D684307A10A5 541FBE7D8F78B25E055DDEE13C370389288584E7 5C3C5F3E314C866292F359A8F3AD5C94A67F707E 765908099ACF92702C7D949BFA0C35EA8AA299F1 79F7026C690BAA50B92CD8B66A3AD3F4F22C4FED 9BA44C2621385CB966EBA586F72C284D731FABEE A27677289986DB50844682F8ACB77FC2E86E29AC A9C5DF4D22E99998D9875A5110C01C5A2F6059E7 DCFD35E0BF8CA7344752DE8B6FB21E8933C60243 F3A04C595DB5B6A5F1ECA43E3B7BBB100D811BBE F7DA48BB64BCB84ECBA7EE6935CD23C10D498E23", + "TOMCAT_MAJOR=8", + "TOMCAT_VERSION=8.5.77", + "TOMCAT_SHA512=50f96584cbbbeeda92a3b573e7fe7e2c49e57ed4bc5246257dc1409abac0710b49fa7049a0dd9a3b8467bca2aa078ef608f49b676c1abf12529528ff71bb0260" + ], + "Image": "sha256:d69eef03ed55cfa0d799181c477762549e7ce94deb23193f04e6bec0c23d0dfd", + "WorkingDir": "/usr/local/tomcat", + "ExposedPorts": { + "8080/tcp": {} + } + } + }, + "Layers": [ + { + "Size": 83895296, + "Digest": "sha256:c229119241af7b23b121052a1cae4c03e0a477a72ea6a7f463ad7623ff8f274b", + "DiffID": "sha256:608f3a074261105f129d707e4d9ad3d41b5baa94887f092b7c2857f7274a2fce" + }, + { + "Size": 5177344, + "Digest": "sha256:2f1dc05f270bad654ee17f1143c48586c188a72929a128d61fd8ae15905d7b00", + "DiffID": "sha256:1f6e409d1c59c8e06608a024b82d50490313abc3b2ff93730e43135d5be0cd72" + }, + { + "Size": 3584, + "Digest": "sha256:69e68111de204b0c46a897b6ac5dd87c1e8aafae5aca100c679358a7a00513a8", + "DiffID": "sha256:0e78b1e5673e8cc7c102fdda9e6b830b7dee2b29b178f34d25d9be59387e6950" + }, + { + "Size": 109289984, + "Digest": "sha256:d7b564a873af313eb2dbcb1ed0d393c57543e3666bdedcbe5d75841d72b1f791", + "DiffID": "sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1" + }, + { + "Size": 3072, + "Digest": "sha256:3a1b5fc9d258d3604588adb7cdea42ffb34c49f273d1e81dd7d94dab6dc3c1e9", + "DiffID": "sha256:053db4876c0df3df3294ee00e32e140b130ba33807d088750cb69b0e6fad158e" + }, + { + "Size": 18067968, + "Digest": "sha256:59c0978ccb117247fd40d936973c40df89195f60466118c5acc6a55f8ba29f06", + "DiffID": "sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6" + }, + { + "Size": 1368576, + "Digest": "sha256:457ac2ac52ffcc6b352c6a8b314f7bc364bf1f407506e3337da06955597433cf", + "DiffID": "sha256:868d710aa4dc5fc4793508564fc45c991ed8d5f6ab3e4cf52bb856f29546f3d8" + }, + { + "Size": 2048, + "Digest": "sha256:c87852fd4f78fd9aa07b6dc3ca5e6292a466246036c517fa6abf1532da9be38f", + "DiffID": "sha256:77b2d158369254d5055183f5483f8b6661170857b61768d1d95d18c2ec1714b6" + }, + { + "Size": 19002368, + "Digest": "sha256:cc44af318e91e6f9f9bf73793fa4f0639487613f46aa1f819b02b6e8fb5c6c07", + "DiffID": "sha256:eb769943b91f10a0418f2fc3b4a4fde6c6293be60c37293fcc0fa319edaf27a5" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/spring4shell-jre8.tar.gz (debian 11.3)", + "Class": "os-pkgs", + "Type": "debian" + }, + { + "Target": "Java", + "Class": "lang-pkgs", + "Type": "jar", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-22965", + "PkgName": "org.springframework:spring-beans", + "PkgPath": "usr/local/tomcat/webapps/helloworld.war/WEB-INF/lib/spring-beans-5.3.15.jar", + "PkgIdentifier": { + "PURL": "pkg:maven/org.springframework/spring-beans@5.3.15", + "UID": "382ba53322e3ac2a" + }, + "InstalledVersion": "5.3.15", + "FixedVersion": "5.3.18", + "Status": "fixed", + "Layer": { + "Digest": "sha256:cc44af318e91e6f9f9bf73793fa4f0639487613f46aa1f819b02b6e8fb5c6c07", + "DiffID": "sha256:eb769943b91f10a0418f2fc3b4a4fde6c6293be60c37293fcc0fa319edaf27a5" + }, + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-22965", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Maven", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven" + }, + "Fingerprint": "sha256:8e22111721ea457f88428647e12e4eaf9eb1a9f37bfd809796390baf262685ff", + "Title": "spring-framework: RCE via Data Binding on JDK 9+", + "Description": "A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it.", + "Severity": "LOW", + "CweIDs": [ + "CWE-94" + ], + "VendorSeverity": { + "ghsa": 4, + "nvd": 4, + "redhat": 3 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 9.8 + }, + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.5, + "V3Score": 9.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 8.1 + } + }, + "References": [ + "https://github.com/advisories/GHSA-36p3-wjmg-h94x" + ], + "PublishedDate": "2022-04-01T23:15:00Z", + "LastModifiedDate": "2022-05-19T14:21:00Z" + } + ] + }, + { + "Target": "", + "Class": "custom", + "CustomResources": [ + { + "Type": "spring4shell/java-major-version", + "FilePath": "/usr/local/openjdk-8/release", + "Layer": { + "Digest": "sha256:d7b564a873af313eb2dbcb1ed0d393c57543e3666bdedcbe5d75841d72b1f791", + "DiffID": "sha256:ba40706eccba610401e4942e29f50bdf36807f8638942ce20805b359ae3ac1c1" + }, + "Data": "1.8.0_322" + }, + { + "Type": "spring4shell/tomcat-version", + "FilePath": "/usr/local/tomcat/RELEASE-NOTES", + "Layer": { + "Digest": "sha256:59c0978ccb117247fd40d936973c40df89195f60466118c5acc6a55f8ba29f06", + "DiffID": "sha256:85595543df2b1115a18284a8ef62d0b235c4bc29e3d33b55f89b54ee1eadf4c6" + }, + "Data": "8.5.77" + } + ] + } + ] +} diff --git a/integration/testdata/swift.json.golden b/integration/testdata/swift.json.golden new file mode 100644 index 000000000000..2d503bdb357e --- /dev/null +++ b/integration/testdata/swift.json.golden @@ -0,0 +1,87 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/swift", + "ArtifactType": "repository", + "Results": [ + { + "Target": "Package.resolved", + "Class": "lang-pkgs", + "Type": "swift", + "Packages": [ + { + "ID": "github.com/apple/swift-atomics@1.1.0", + "Name": "github.com/apple/swift-atomics", + "Identifier": { + "PURL": "pkg:swift/github.com/apple/swift-atomics@1.1.0", + "UID": "2e92b45ea6db543b" + }, + "Version": "1.1.0", + "Locations": [ + { + "StartLine": 4, + "EndLine": 12 + } + ], + "AnalyzedBy": "swift" + }, + { + "ID": "github.com/apple/swift-nio@2.41.0", + "Name": "github.com/apple/swift-nio", + "Identifier": { + "PURL": "pkg:swift/github.com/apple/swift-nio@2.41.0", + "UID": "c9051e4608766d2f" + }, + "Version": "2.41.0", + "Locations": [ + { + "StartLine": 13, + "EndLine": 21 + } + ], + "AnalyzedBy": "swift" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-3215", + "PkgID": "github.com/apple/swift-nio@2.41.0", + "PkgName": "github.com/apple/swift-nio", + "PkgIdentifier": { + "PURL": "pkg:swift/github.com/apple/swift-nio@2.41.0", + "UID": "c9051e4608766d2f" + }, + "InstalledVersion": "2.41.0", + "FixedVersion": "2.29.1, 2.39.1, 2.42.0", + "Status": "fixed", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-3215", + "Fingerprint": "sha256:5e6d347d902170f39ce0bb9c580cadecc025cbd132fa3a99e171ec1fdaba6a91", + "Title": "SwiftNIO vulnerable to Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Response Splitting')", + "Description": "`NIOHTTP1` and projects using it for generating HTTP responses, including SwiftNIO, can be subject to a HTTP Response Injection attack...", + "Severity": "MEDIUM", + "VendorSeverity": { + "ghsa": 2 + }, + "CVSS": { + "ghsa": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N", + "V3Score": 5.3 + } + }, + "References": [ + "https://github.com/apple/swift-nio/security/advisories/GHSA-7fj7-39wj-c64f", + "https://nvd.nist.gov/vuln/detail/CVE-2022-3215", + "https://github.com/apple/swift-nio/commit/a16e2f54a25b2af217044e5168997009a505930f", + "https://github.com/advisories/GHSA-7fj7-39wj-c64f" + ], + "PublishedDate": "2023-06-07T16:01:53Z", + "LastModifiedDate": "2023-06-19T16:45:07Z" + } + ] + } + ] +} diff --git a/integration/testdata/terraform-exclude-misconfs-remote-module.json.golden b/integration/testdata/terraform-exclude-misconfs-remote-module.json.golden new file mode 100644 index 000000000000..c90a266c568e --- /dev/null +++ b/integration/testdata/terraform-exclude-misconfs-remote-module.json.golden @@ -0,0 +1,26 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/terraform/remote-module", + "ArtifactType": "repository", + "Results": [ + { + "Target": ".", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 58, + "Failures": 0 + } + }, + { + "Target": "terraform-aws-modules/s3-bucket/aws/main.tf", + "Class": "config", + "Type": "terraform" + } + ] +} diff --git a/integration/testdata/terraform-opentofu-registry.json.golden b/integration/testdata/terraform-opentofu-registry.json.golden new file mode 100644 index 000000000000..527a1732cc69 --- /dev/null +++ b/integration/testdata/terraform-opentofu-registry.json.golden @@ -0,0 +1,439 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/terraform/opentofu-registry", + "ArtifactType": "repository", + "Results": [ + { + "Target": ".", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 58, + "Failures": 0 + } + }, + { + "Target": "registry.opentofu.org/terraform-aws-modules/s3-bucket/aws/main.tf", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 0, + "Failures": 3 + }, + "Misconfigurations": [ + { + "Type": "Terraform Security Check", + "ID": "AWS-0089", + "Title": "S3 Bucket Logging", + "Description": "Ensures S3 bucket logging is enabled for S3 buckets", + "Message": "Bucket has logging disabled", + "Namespace": "builtin.aws.s3.aws0089", + "Query": "data.builtin.aws.s3.aws0089.deny", + "Resolution": "Add a logging block to the resource to enable access logging", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0089", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-server-access-logging.html", + "https://avd.aquasec.com/misconfig/aws-0089" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.bucket", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 5 + } + } + ] + } + }, + { + "Type": "Terraform Security Check", + "ID": "AWS-0090", + "Title": "S3 Data should be versioned", + "Description": "Versioning in Amazon S3 is a means of keeping multiple variants of an object in the same bucket.\n\nYou can use the S3 Versioning feature to preserve, retrieve, and restore every version of every object stored in your buckets.\n\nWith versioning you can recover more easily from both unintended user actions and application failures.\n\nWhen you enable versioning, also keep in mind the potential costs of storing noncurrent versions of objects. To help manage those costs, consider setting up an S3 Lifecycle configuration.\n", + "Message": "Bucket does not have versioning enabled", + "Namespace": "builtin.aws.s3.aws0090", + "Query": "data.builtin.aws.s3.aws0090.deny", + "Resolution": "Enable versioning to protect against accidental/malicious removal or modification", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0090", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html", + "https://aws.amazon.com/blogs/storage/reduce-storage-costs-with-fewer-noncurrent-versions-using-amazon-s3-lifecycle/", + "https://avd.aquasec.com/misconfig/aws-0090" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.bucket", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 5 + } + } + ] + } + }, + { + "Type": "Terraform Security Check", + "ID": "AWS-0132", + "Title": "S3 encryption should use Customer Managed Keys", + "Description": "Encryption using AWS keys provides protection for your S3 buckets. To gain greater control over encryption, such as key rotation, access policies, and auditability, use customer managed keys (CMKs) with SSE-KMS.\nNote that SSE-KMS is not supported for S3 server access logging destination buckets; in such cases, use SSE-S3 instead.\n", + "Message": "Bucket does not encrypt data with a customer managed key.", + "Namespace": "builtin.aws.s3.aws0132", + "Query": "data.builtin.aws.s3.aws0132.deny", + "Resolution": "Use SSE-KMS with a customer managed key (CMK)", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0132", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-encryption.html", + "https://avd.aquasec.com/misconfig/aws-0132" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.bucket", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 5 + } + } + ] + } + } + ] + } + ] +} diff --git a/integration/testdata/terraform-remote-module-in-child.json.golden b/integration/testdata/terraform-remote-module-in-child.json.golden new file mode 100644 index 000000000000..273178a0882b --- /dev/null +++ b/integration/testdata/terraform-remote-module-in-child.json.golden @@ -0,0 +1,463 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/terraform/remote-module-in-child", + "ArtifactType": "repository", + "Results": [ + { + "Target": ".", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 58, + "Failures": 0 + } + }, + { + "Target": "github.com/terraform-aws-modules/terraform-aws-s3-bucket?ref=v5.2.0/main.tf", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 0, + "Failures": 3 + }, + "Misconfigurations": [ + { + "Type": "Terraform Security Check", + "ID": "AWS-0089", + "Title": "S3 Bucket Logging", + "Description": "Ensures S3 bucket logging is enabled for S3 buckets", + "Message": "Bucket has logging disabled", + "Namespace": "builtin.aws.s3.aws0089", + "Query": "data.builtin.aws.s3.aws0089.deny", + "Resolution": "Add a logging block to the resource to enable access logging", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0089", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-server-access-logging.html", + "https://avd.aquasec.com/misconfig/aws-0089" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.this", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "modules/s3/main.tf", + "Location": { + "StartLine": 5, + "EndLine": 8 + } + }, + { + "Resource": "module.this", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 4 + } + } + ] + } + }, + { + "Type": "Terraform Security Check", + "ID": "AWS-0090", + "Title": "S3 Data should be versioned", + "Description": "Versioning in Amazon S3 is a means of keeping multiple variants of an object in the same bucket.\n\nYou can use the S3 Versioning feature to preserve, retrieve, and restore every version of every object stored in your buckets.\n\nWith versioning you can recover more easily from both unintended user actions and application failures.\n\nWhen you enable versioning, also keep in mind the potential costs of storing noncurrent versions of objects. To help manage those costs, consider setting up an S3 Lifecycle configuration.\n", + "Message": "Bucket does not have versioning enabled", + "Namespace": "builtin.aws.s3.aws0090", + "Query": "data.builtin.aws.s3.aws0090.deny", + "Resolution": "Enable versioning to protect against accidental/malicious removal or modification", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0090", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html", + "https://aws.amazon.com/blogs/storage/reduce-storage-costs-with-fewer-noncurrent-versions-using-amazon-s3-lifecycle/", + "https://avd.aquasec.com/misconfig/aws-0090" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.this", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "modules/s3/main.tf", + "Location": { + "StartLine": 5, + "EndLine": 8 + } + }, + { + "Resource": "module.this", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 4 + } + } + ] + } + }, + { + "Type": "Terraform Security Check", + "ID": "AWS-0132", + "Title": "S3 encryption should use Customer Managed Keys", + "Description": "Encryption using AWS keys provides protection for your S3 buckets. To gain greater control over encryption, such as key rotation, access policies, and auditability, use customer managed keys (CMKs) with SSE-KMS.\nNote that SSE-KMS is not supported for S3 server access logging destination buckets; in such cases, use SSE-S3 instead.\n", + "Message": "Bucket does not encrypt data with a customer managed key.", + "Namespace": "builtin.aws.s3.aws0132", + "Query": "data.builtin.aws.s3.aws0132.deny", + "Resolution": "Use SSE-KMS with a customer managed key (CMK)", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0132", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-encryption.html", + "https://avd.aquasec.com/misconfig/aws-0132" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.this", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "modules/s3/main.tf", + "Location": { + "StartLine": 5, + "EndLine": 8 + } + }, + { + "Resource": "module.this", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 4 + } + } + ] + } + } + ] + } + ] +} diff --git a/integration/testdata/terraform-remote-module.json.golden b/integration/testdata/terraform-remote-module.json.golden new file mode 100644 index 000000000000..a9ef2a211e1b --- /dev/null +++ b/integration/testdata/terraform-remote-module.json.golden @@ -0,0 +1,439 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/terraform/remote-module", + "ArtifactType": "repository", + "Results": [ + { + "Target": ".", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 58, + "Failures": 0 + } + }, + { + "Target": "terraform-aws-modules/s3-bucket/aws/main.tf", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 0, + "Failures": 3 + }, + "Misconfigurations": [ + { + "Type": "Terraform Security Check", + "ID": "AWS-0089", + "Title": "S3 Bucket Logging", + "Description": "Ensures S3 bucket logging is enabled for S3 buckets", + "Message": "Bucket has logging disabled", + "Namespace": "builtin.aws.s3.aws0089", + "Query": "data.builtin.aws.s3.aws0089.deny", + "Resolution": "Add a logging block to the resource to enable access logging", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0089", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-server-access-logging.html", + "https://avd.aquasec.com/misconfig/aws-0089" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.bucket", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 5 + } + } + ] + } + }, + { + "Type": "Terraform Security Check", + "ID": "AWS-0090", + "Title": "S3 Data should be versioned", + "Description": "Versioning in Amazon S3 is a means of keeping multiple variants of an object in the same bucket.\n\nYou can use the S3 Versioning feature to preserve, retrieve, and restore every version of every object stored in your buckets.\n\nWith versioning you can recover more easily from both unintended user actions and application failures.\n\nWhen you enable versioning, also keep in mind the potential costs of storing noncurrent versions of objects. To help manage those costs, consider setting up an S3 Lifecycle configuration.\n", + "Message": "Bucket does not have versioning enabled", + "Namespace": "builtin.aws.s3.aws0090", + "Query": "data.builtin.aws.s3.aws0090.deny", + "Resolution": "Enable versioning to protect against accidental/malicious removal or modification", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0090", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html", + "https://aws.amazon.com/blogs/storage/reduce-storage-costs-with-fewer-noncurrent-versions-using-amazon-s3-lifecycle/", + "https://avd.aquasec.com/misconfig/aws-0090" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.bucket", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 5 + } + } + ] + } + }, + { + "Type": "Terraform Security Check", + "ID": "AWS-0132", + "Title": "S3 encryption should use Customer Managed Keys", + "Description": "Encryption using AWS keys provides protection for your S3 buckets. To gain greater control over encryption, such as key rotation, access policies, and auditability, use customer managed keys (CMKs) with SSE-KMS.\nNote that SSE-KMS is not supported for S3 server access logging destination buckets; in such cases, use SSE-S3 instead.\n", + "Message": "Bucket does not encrypt data with a customer managed key.", + "Namespace": "builtin.aws.s3.aws0132", + "Query": "data.builtin.aws.s3.aws0132.deny", + "Resolution": "Use SSE-KMS with a customer managed key (CMK)", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0132", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-encryption.html", + "https://avd.aquasec.com/misconfig/aws-0132" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.bucket", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 5 + } + } + ] + } + } + ] + } + ] +} diff --git a/integration/testdata/terraform-remote-submodule.json.golden b/integration/testdata/terraform-remote-submodule.json.golden new file mode 100644 index 000000000000..ba94aea4d368 --- /dev/null +++ b/integration/testdata/terraform-remote-submodule.json.golden @@ -0,0 +1,228 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/terraform/remote-submodule", + "ArtifactType": "repository", + "Results": [ + { + "Target": ".", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 69, + "Failures": 0 + } + }, + { + "Target": "terraform-aws-modules/vpc/aws/modules/vpc-endpoints/modules/vpc-endpoints/main.tf", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 0, + "Failures": 2 + }, + "Misconfigurations": [ + { + "Type": "Terraform Security Check", + "ID": "AWS-0104", + "Title": "A security group rule should not allow unrestricted egress to any IP address.", + "Description": "Opening up ports to connect out to the public internet is generally to be avoided. You should restrict access to IP addresses or ranges that are explicitly required where possible.\n", + "Message": "Security group rule allows unrestricted egress to any IP address.", + "Namespace": "builtin.aws.ec2.aws0104", + "Query": "data.builtin.aws.ec2.aws0104.deny", + "Resolution": "Set a more restrictive cidr range", + "Severity": "CRITICAL", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0104", + "References": [ + "https://docs.aws.amazon.com/whitepapers/latest/building-scalable-secure-multi-vpc-network-infrastructure/centralized-egress-to-internet.html", + "https://avd.aquasec.com/misconfig/aws-0104" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.endpoints", + "Provider": "AWS", + "Service": "ec2", + "StartLine": 107, + "EndLine": 107, + "Code": { + "Lines": [ + { + "Number": 95, + "Content": "resource \"aws_security_group_rule\" \"this\" {", + "IsCause": false, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_security_group_rule\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 96, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 107, + "Content": " cidr_blocks = lookup(each.value, \"cidr_blocks\", null)", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcidr_blocks\u001b[0m =\u001b[38;5;33m lookup\u001b[0m(\u001b[38;5;33meach\u001b[0m.value, \u001b[38;5;37m\"cidr_blocks\"\u001b[0m, null)", + "FirstCause": true, + "LastCause": true + }, + { + "Number": 111, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 112, + "Content": "}", + "IsCause": false, + "Annotation": "", + "Truncated": false, + "Highlighted": "}", + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "aws_security_group_rule.this[\"0\"]", + "Filename": "terraform-aws-modules/vpc/aws/modules/vpc-endpoints/modules/vpc-endpoints/main.tf", + "Location": { + "StartLine": 95, + "EndLine": 112 + } + }, + { + "Resource": "module.endpoints", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 20 + } + } + ], + "RenderedCause": { + "Raw": "resource \"aws_security_group_rule\" \"this\" {\n cidr_blocks = [\"0.0.0.0/0\"]\n}", + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_security_group_rule\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {\n \u001b[38;5;245mcidr_blocks\u001b[0m = [\u001b[38;5;37m\"0.0.0.0/0\"\u001b[0m]\n}" + } + } + }, + { + "Type": "Terraform Security Check", + "ID": "AWS-0124", + "Title": "Missing description for security group rule.", + "Description": "Security group rules should include a description for auditing purposes.\n\nSimplifies auditing, debugging, and managing security groups.\n", + "Message": "Security group rule does not have a description.", + "Namespace": "builtin.aws.ec2.aws0124", + "Query": "data.builtin.aws.ec2.aws0124.deny", + "Resolution": "Add descriptions for all security groups rules", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0124", + "References": [ + "https://www.cloudconformity.com/knowledge-base/aws/EC2/security-group-rules-description.html", + "https://avd.aquasec.com/misconfig/aws-0124" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.endpoints", + "Provider": "AWS", + "Service": "ec2", + "StartLine": 106, + "EndLine": 106, + "Code": { + "Lines": [ + { + "Number": 95, + "Content": "resource \"aws_security_group_rule\" \"this\" {", + "IsCause": false, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_security_group_rule\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 96, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 106, + "Content": " description = try(each.value.description, null)", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m \u001b[38;5;245mdescription\u001b[0m = \u001b[38;5;33mtry\u001b[0m(\u001b[38;5;33meach\u001b[0m.value.description, null)", + "FirstCause": true, + "LastCause": true + }, + { + "Number": 111, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 112, + "Content": "}", + "IsCause": false, + "Annotation": "", + "Truncated": false, + "Highlighted": "}", + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "aws_security_group_rule.this[\"0\"]", + "Filename": "terraform-aws-modules/vpc/aws/modules/vpc-endpoints/modules/vpc-endpoints/main.tf", + "Location": { + "StartLine": 95, + "EndLine": 112 + } + }, + { + "Resource": "module.endpoints", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 20 + } + } + ], + "RenderedCause": { + "Raw": "resource \"aws_security_group_rule\" \"this\" {\n description = \"\"\n}", + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_security_group_rule\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {\n \u001b[38;5;245mdescription\u001b[0m = \u001b[38;5;37m\"\"\n\u001b[0m}" + } + } + } + ] + } + ] +} diff --git a/integration/testdata/terraform-terraform-registry.json.golden b/integration/testdata/terraform-terraform-registry.json.golden new file mode 100644 index 000000000000..527a1732cc69 --- /dev/null +++ b/integration/testdata/terraform-terraform-registry.json.golden @@ -0,0 +1,439 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/terraform/opentofu-registry", + "ArtifactType": "repository", + "Results": [ + { + "Target": ".", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 58, + "Failures": 0 + } + }, + { + "Target": "registry.opentofu.org/terraform-aws-modules/s3-bucket/aws/main.tf", + "Class": "config", + "Type": "terraform", + "MisconfSummary": { + "Successes": 0, + "Failures": 3 + }, + "Misconfigurations": [ + { + "Type": "Terraform Security Check", + "ID": "AWS-0089", + "Title": "S3 Bucket Logging", + "Description": "Ensures S3 bucket logging is enabled for S3 buckets", + "Message": "Bucket has logging disabled", + "Namespace": "builtin.aws.s3.aws0089", + "Query": "data.builtin.aws.s3.aws0089.deny", + "Resolution": "Add a logging block to the resource to enable access logging", + "Severity": "LOW", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0089", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/ServerLogs.html", + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/enable-server-access-logging.html", + "https://avd.aquasec.com/misconfig/aws-0089" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.bucket", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 5 + } + } + ] + } + }, + { + "Type": "Terraform Security Check", + "ID": "AWS-0090", + "Title": "S3 Data should be versioned", + "Description": "Versioning in Amazon S3 is a means of keeping multiple variants of an object in the same bucket.\n\nYou can use the S3 Versioning feature to preserve, retrieve, and restore every version of every object stored in your buckets.\n\nWith versioning you can recover more easily from both unintended user actions and application failures.\n\nWhen you enable versioning, also keep in mind the potential costs of storing noncurrent versions of objects. To help manage those costs, consider setting up an S3 Lifecycle configuration.\n", + "Message": "Bucket does not have versioning enabled", + "Namespace": "builtin.aws.s3.aws0090", + "Query": "data.builtin.aws.s3.aws0090.deny", + "Resolution": "Enable versioning to protect against accidental/malicious removal or modification", + "Severity": "MEDIUM", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0090", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/Versioning.html", + "https://aws.amazon.com/blogs/storage/reduce-storage-costs-with-fewer-noncurrent-versions-using-amazon-s3-lifecycle/", + "https://avd.aquasec.com/misconfig/aws-0090" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.bucket", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 5 + } + } + ] + } + }, + { + "Type": "Terraform Security Check", + "ID": "AWS-0132", + "Title": "S3 encryption should use Customer Managed Keys", + "Description": "Encryption using AWS keys provides protection for your S3 buckets. To gain greater control over encryption, such as key rotation, access policies, and auditability, use customer managed keys (CMKs) with SSE-KMS.\nNote that SSE-KMS is not supported for S3 server access logging destination buckets; in such cases, use SSE-S3 instead.\n", + "Message": "Bucket does not encrypt data with a customer managed key.", + "Namespace": "builtin.aws.s3.aws0132", + "Query": "data.builtin.aws.s3.aws0132.deny", + "Resolution": "Use SSE-KMS with a customer managed key (CMK)", + "Severity": "HIGH", + "PrimaryURL": "https://avd.aquasec.com/misconfig/aws-0132", + "References": [ + "https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucket-encryption.html", + "https://avd.aquasec.com/misconfig/aws-0132" + ], + "Status": "FAIL", + "CauseMetadata": { + "Resource": "module.bucket", + "Provider": "AWS", + "Service": "s3", + "StartLine": 25, + "EndLine": 36, + "Code": { + "Lines": [ + { + "Number": 25, + "Content": "resource \"aws_s3_bucket\" \"this\" {", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[38;5;33mresource\u001b[0m \u001b[38;5;37m\"aws_s3_bucket\"\u001b[0m \u001b[38;5;37m\"this\"\u001b[0m {", + "FirstCause": true, + "LastCause": false + }, + { + "Number": 26, + "Content": " count = local.create_bucket \u0026\u0026 !var.is_directory_bucket ? 1 : 0", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mcount\u001b[0m = local.create_bucket \u001b[38;5;245m\u0026\u0026\u001b[0m \u001b[38;5;245m!\u001b[0m\u001b[38;5;33mvar\u001b[0m.is_directory_bucket \u001b[38;5;245m?\u001b[0m \u001b[38;5;37m1\u001b[0m \u001b[38;5;245m:\u001b[0m \u001b[38;5;37m0", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 27, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": "\u001b[0m", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 28, + "Content": " region = var.region", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mregion\u001b[0m = \u001b[38;5;33mvar\u001b[0m.region", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 29, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 30, + "Content": " bucket = var.bucket", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 31, + "Content": " bucket_prefix = var.bucket_prefix", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mbucket_prefix\u001b[0m = \u001b[38;5;33mvar\u001b[0m.bucket_prefix", + "FirstCause": false, + "LastCause": false + }, + { + "Number": 32, + "Content": "", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "FirstCause": false, + "LastCause": false + }, + { + "Number": 33, + "Content": " force_destroy = var.force_destroy", + "IsCause": true, + "Annotation": "", + "Truncated": false, + "Highlighted": " \u001b[38;5;245mforce_destroy\u001b[0m = \u001b[38;5;33mvar\u001b[0m.force_destroy", + "FirstCause": false, + "LastCause": true + }, + { + "Number": 34, + "Content": "", + "IsCause": false, + "Annotation": "", + "Truncated": true, + "FirstCause": false, + "LastCause": false + } + ] + }, + "Occurrences": [ + { + "Resource": "module.bucket", + "Filename": "main.tf", + "Location": { + "StartLine": 1, + "EndLine": 5 + } + } + ] + } + } + ] + } + ] +} diff --git a/integration/testdata/test-repo.json.golden b/integration/testdata/test-repo.json.golden new file mode 100644 index 000000000000..346b41eb7516 --- /dev/null +++ b/integration/testdata/test-repo.json.golden @@ -0,0 +1,120 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:4f8b4cb139bdac63ad60b7382a80cd0414c55018e2f80d4163f59e40f71766cc", + "ArtifactName": "https://github.com/knqyf263/trivy-ci-test", + "ArtifactType": "repository", + "Metadata": { + "RepoURL": "https://github.com/knqyf263/trivy-ci-test", + "Branch": "master", + "Commit": "5ae342eb2802672402d9b2c26f09e2051bbd91b8", + "CommitMsg": "Use COPY instead of ADD in Dockerfile (#4)", + "Author": "gy741 \u003cgy741.kim@gmail.com\u003e", + "Committer": "knqyf263 \u003cknqyf263@gmail.com\u003e" + }, + "Results": [ + { + "Target": "Cargo.lock", + "Class": "lang-pkgs", + "Type": "cargo", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-15542", + "PkgID": "ammonia@1.9.0", + "PkgName": "ammonia", + "PkgIdentifier": { + "PURL": "pkg:cargo/ammonia@1.9.0", + "UID": "9ec7999f7bb7437e" + }, + "InstalledVersion": "1.9.0", + "FixedVersion": "\u003e= 2.1.0", + "Status": "fixed", + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-15542", + "DataSource": { + "Name": "RustSec Advisory Database", + "URL": "https://github.com/RustSec/advisory-db" + }, + "Fingerprint": "sha256:f2b941fc60acd755d2c6a7e168e6b45422ea44a089d689f5c0a4971470acd837", + "Title": "Uncontrolled recursion leads to abort in HTML serialization", + "Description": "An issue was discovered in the ammonia crate before 2.1.0 for Rust. There is uncontrolled recursion during HTML DOM tree serialization.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-674" + ], + "VendorSeverity": { + "nvd": 3 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:N/I:N/A:P", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V2Score": 5, + "V3Score": 7.5 + } + }, + "References": [ + "https://crates.io/crates/ammonia", + "https://github.com/rust-ammonia/ammonia/blob/master/CHANGELOG.md#210", + "https://rustsec.org/advisories/RUSTSEC-2019-0001.html" + ], + "PublishedDate": "2019-08-26T18:15:00Z", + "LastModifiedDate": "2020-08-24T17:37:00Z" + }, + { + "VulnerabilityID": "CVE-2021-38193", + "PkgID": "ammonia@1.9.0", + "PkgName": "ammonia", + "PkgIdentifier": { + "PURL": "pkg:cargo/ammonia@1.9.0", + "UID": "9ec7999f7bb7437e" + }, + "InstalledVersion": "1.9.0", + "FixedVersion": "\u003e= 3.1.0, \u003e= 2.1.3, \u003c 3.0.0", + "Status": "fixed", + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2021-38193", + "DataSource": { + "Name": "RustSec Advisory Database", + "URL": "https://github.com/RustSec/advisory-db" + }, + "Fingerprint": "sha256:4b40d82614bc892f89dcd47e6ae81700c23b0bfb15a000ed2738451831bd1f72", + "Title": "Incorrect handling of embedded SVG and MathML leads to mutation XSS", + "Description": "An issue was discovered in the ammonia crate before 3.1.0 for Rust. XSS can occur because the parsing differences for HTML, SVG, and MathML are mishandled, a similar issue to CVE-2020-26870.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-79" + ], + "VendorSeverity": { + "nvd": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 4.3, + "V3Score": 6.1 + } + }, + "References": [ + "https://crates.io/crates/ammonia", + "https://github.com/rust-ammonia/ammonia/pull/142", + "https://raw.githubusercontent.com/rustsec/advisory-db/main/crates/ammonia/RUSTSEC-2021-0074.md", + "https://rustsec.org/advisories/RUSTSEC-2021-0074.html" + ], + "PublishedDate": "2021-08-08T06:15:00Z", + "LastModifiedDate": "2021-08-16T16:37:00Z" + } + ] + }, + { + "Target": "Pipfile.lock", + "Class": "lang-pkgs", + "Type": "pipenv" + } + ] +} diff --git a/integration/testdata/ubi-7-comprehensive.json.golden b/integration/testdata/ubi-7-comprehensive.json.golden new file mode 100644 index 000000000000..0e2dc3a31f7f --- /dev/null +++ b/integration/testdata/ubi-7-comprehensive.json.golden @@ -0,0 +1,216 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:b4b4762f4769903a61d4605927079c4e60d007d9b12f65c39714e5311fa3d0ca", + "ArtifactName": "testdata/fixtures/images/ubi-7.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 215162880, + "OS": { + "Family": "redhat", + "Name": "7.7" + }, + "ImageID": "sha256:6fecccc91c83e11ae4fede6793e9410841221d4779520c2b9e9fb7f7b3830264", + "DiffIDs": [ + "sha256:4468e6d912c76d5b127f3554c3cd83b7dc07cce6107c6b916299ba76fa7d15ac", + "sha256:ecb0311889b3478bc9b62660fa9391d5ebf8da4c6ae143cb33434873668f9e36" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:ubi-7" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:ubi-7", + "ImageConfig": { + "architecture": "amd64", + "created": "2019-09-02T12:56:43.939095Z", + "docker_version": "1.13.1", + "history": [ + { + "created": "2019-09-02T12:56:36.440695936Z", + "comment": "Imported from -" + }, + { + "created": "2019-09-02T12:56:43.939095Z" + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:4468e6d912c76d5b127f3554c3cd83b7dc07cce6107c6b916299ba76fa7d15ac", + "sha256:ecb0311889b3478bc9b62660fa9391d5ebf8da4c6ae143cb33434873668f9e36" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "container=oci" + ], + "Hostname": "0da2e3774382", + "Image": "2e9103a7b91a7ffe333e9162ce98ea078263747527571655e93bd4d35ee278f0", + "Labels": { + "architecture": "x86_64", + "authoritative-source-url": "registry.access.redhat.com", + "build-date": "2019-09-02T12:56:18.824770", + "com.redhat.build-host": "cpt-1005.osbs.prod.upshift.rdu2.redhat.com", + "com.redhat.component": "ubi7-container", + "com.redhat.license_terms": "https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI", + "description": "The Universal Base Image is designed and engineered to be the base layer for all of your containerized applications, middleware and utilities. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly.", + "distribution-scope": "public", + "io.k8s.description": "The Universal Base Image is designed and engineered to be the base layer for all of your containerized applications, middleware and utilities. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly.", + "io.k8s.display-name": "Red Hat Universal Base Image 7", + "io.openshift.tags": "base rhel7", + "maintainer": "Red Hat, Inc.", + "name": "ubi7", + "release": "140", + "summary": "Provides the latest release of the Red Hat Universal Base Image 7.", + "url": "https://access.redhat.com/containers/#/registry.access.redhat.com/ubi7/images/7.7-140", + "vcs-ref": "4c80c8aa26e69950ab11b87789c8fb7665b1632d", + "vcs-type": "git", + "vendor": "Red Hat, Inc.", + "version": "7.7" + }, + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 215142400, + "Digest": "sha256:7b1c937e0f6794db2535be6e4cb6d60a0b668ef78c2576611a3fb9c97a95ccdf", + "DiffID": "sha256:4468e6d912c76d5b127f3554c3cd83b7dc07cce6107c6b916299ba76fa7d15ac" + }, + { + "Size": 20480, + "Digest": "sha256:bff3b73cbcc496de1de4ea51df88b7249169d0b6eb7d677169eaf90b8a92240e", + "DiffID": "sha256:ecb0311889b3478bc9b62660fa9391d5ebf8da4c6ae143cb33434873668f9e36" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/ubi-7.tar.gz (redhat 7.7)", + "Class": "os-pkgs", + "Type": "redhat", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18276", + "PkgID": "bash@4.2.46-33.el7.x86_64", + "PkgName": "bash", + "PkgIdentifier": { + "PURL": "pkg:rpm/redhat/bash@4.2.46-33.el7?arch=x86_64\u0026distro=redhat-7.7", + "UID": "83a539f92a1b8c47" + }, + "InstalledVersion": "4.2.46-33.el7", + "Status": "will_not_fix", + "Layer": { + "Digest": "sha256:7b1c937e0f6794db2535be6e4cb6d60a0b668ef78c2576611a3fb9c97a95ccdf", + "DiffID": "sha256:4468e6d912c76d5b127f3554c3cd83b7dc07cce6107c6b916299ba76fa7d15ac" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", + "Fingerprint": "sha256:d88ec8270470584c0d7d9b5355fb694ac6896c1249c5c27ec68aaac30733a95b", + "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", + "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "Severity": "LOW", + "CweIDs": [ + "CWE-273" + ], + "VendorSeverity": { + "cbl-mariner": 3, + "nvd": 3, + "oracle-oval": 1, + "photon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.2, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + } + }, + "References": [ + "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", + "https://access.redhat.com/security/cve/CVE-2019-18276", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", + "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", + "https://linux.oracle.com/cve/CVE-2019-18276.html", + "https://linux.oracle.com/errata/ELSA-2021-1679.html", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", + "https://security.gentoo.org/glsa/202105-34", + "https://security.netapp.com/advisory/ntap-20200430-0003/", + "https://www.youtube.com/watch?v=-wGtxJ8opa8" + ], + "PublishedDate": "2019-11-28T01:15:00Z", + "LastModifiedDate": "2021-05-26T12:15:00Z" + } + ] + }, + { + "Target": "Python", + "Class": "lang-pkgs", + "Type": "python-pkg", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-40897", + "PkgName": "setuptools", + "PkgPath": "usr/lib/python2.7/site-packages/setuptools-0.9.8-py2.7.egg-info/PKG-INFO", + "PkgIdentifier": { + "PURL": "pkg:pypi/setuptools@0.9.8", + "UID": "81fa6be5e18bf8e1" + }, + "InstalledVersion": "0.9.8", + "FixedVersion": "65.5.1", + "Status": "fixed", + "Layer": { + "Digest": "sha256:7b1c937e0f6794db2535be6e4cb6d60a0b668ef78c2576611a3fb9c97a95ccdf", + "DiffID": "sha256:4468e6d912c76d5b127f3554c3cd83b7dc07cce6107c6b916299ba76fa7d15ac" + }, + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-40897", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Pip", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + }, + "Fingerprint": "sha256:064ee0c0b2bd92abb75fe4a4462ba20b783dc75e4f59d2e2a5542df962456566", + "Title": "pypa-setuptools: Regular Expression Denial of Service (ReDoS) in package_index.py", + "Description": "Python Packaging Authority (PyPA) setuptools before 65.5.1 allows remote attackers to cause a denial of service via HTML in a crafted package or custom PackageIndex page. There is a Regular Expression Denial of Service (ReDoS) in package_index.py.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-1333" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 2 + }, + "CVSS": { + "nvd": { + "V3Vector": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H", + "V3Score": 5.9 + } + }, + "References": [ + "https://access.redhat.com/errata/RHSA-2023:0952", + "https://access.redhat.com/security/cve/CVE-2022-40897" + ], + "PublishedDate": "2022-12-23T00:15:13.987Z", + "LastModifiedDate": "2024-06-21T19:15:23.877Z" + } + ] + } + ] +} diff --git a/integration/testdata/ubi-7.json.golden b/integration/testdata/ubi-7.json.golden new file mode 100644 index 000000000000..839c1516f6f3 --- /dev/null +++ b/integration/testdata/ubi-7.json.golden @@ -0,0 +1,163 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:b4b4762f4769903a61d4605927079c4e60d007d9b12f65c39714e5311fa3d0ca", + "ArtifactName": "testdata/fixtures/images/ubi-7.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 215162880, + "OS": { + "Family": "redhat", + "Name": "7.7" + }, + "ImageID": "sha256:6fecccc91c83e11ae4fede6793e9410841221d4779520c2b9e9fb7f7b3830264", + "DiffIDs": [ + "sha256:4468e6d912c76d5b127f3554c3cd83b7dc07cce6107c6b916299ba76fa7d15ac", + "sha256:ecb0311889b3478bc9b62660fa9391d5ebf8da4c6ae143cb33434873668f9e36" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:ubi-7" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:ubi-7", + "ImageConfig": { + "architecture": "amd64", + "created": "2019-09-02T12:56:43.939095Z", + "docker_version": "1.13.1", + "history": [ + { + "created": "2019-09-02T12:56:36.440695936Z", + "comment": "Imported from -" + }, + { + "created": "2019-09-02T12:56:43.939095Z" + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:4468e6d912c76d5b127f3554c3cd83b7dc07cce6107c6b916299ba76fa7d15ac", + "sha256:ecb0311889b3478bc9b62660fa9391d5ebf8da4c6ae143cb33434873668f9e36" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "container=oci" + ], + "Hostname": "0da2e3774382", + "Image": "2e9103a7b91a7ffe333e9162ce98ea078263747527571655e93bd4d35ee278f0", + "Labels": { + "architecture": "x86_64", + "authoritative-source-url": "registry.access.redhat.com", + "build-date": "2019-09-02T12:56:18.824770", + "com.redhat.build-host": "cpt-1005.osbs.prod.upshift.rdu2.redhat.com", + "com.redhat.component": "ubi7-container", + "com.redhat.license_terms": "https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI", + "description": "The Universal Base Image is designed and engineered to be the base layer for all of your containerized applications, middleware and utilities. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly.", + "distribution-scope": "public", + "io.k8s.description": "The Universal Base Image is designed and engineered to be the base layer for all of your containerized applications, middleware and utilities. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly.", + "io.k8s.display-name": "Red Hat Universal Base Image 7", + "io.openshift.tags": "base rhel7", + "maintainer": "Red Hat, Inc.", + "name": "ubi7", + "release": "140", + "summary": "Provides the latest release of the Red Hat Universal Base Image 7.", + "url": "https://access.redhat.com/containers/#/registry.access.redhat.com/ubi7/images/7.7-140", + "vcs-ref": "4c80c8aa26e69950ab11b87789c8fb7665b1632d", + "vcs-type": "git", + "vendor": "Red Hat, Inc.", + "version": "7.7" + }, + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 215142400, + "Digest": "sha256:7b1c937e0f6794db2535be6e4cb6d60a0b668ef78c2576611a3fb9c97a95ccdf", + "DiffID": "sha256:4468e6d912c76d5b127f3554c3cd83b7dc07cce6107c6b916299ba76fa7d15ac" + }, + { + "Size": 20480, + "Digest": "sha256:bff3b73cbcc496de1de4ea51df88b7249169d0b6eb7d677169eaf90b8a92240e", + "DiffID": "sha256:ecb0311889b3478bc9b62660fa9391d5ebf8da4c6ae143cb33434873668f9e36" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/ubi-7.tar.gz (redhat 7.7)", + "Class": "os-pkgs", + "Type": "redhat", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18276", + "PkgID": "bash@4.2.46-33.el7.x86_64", + "PkgName": "bash", + "PkgIdentifier": { + "PURL": "pkg:rpm/redhat/bash@4.2.46-33.el7?arch=x86_64\u0026distro=redhat-7.7", + "UID": "83a539f92a1b8c47" + }, + "InstalledVersion": "4.2.46-33.el7", + "Status": "will_not_fix", + "Layer": { + "Digest": "sha256:7b1c937e0f6794db2535be6e4cb6d60a0b668ef78c2576611a3fb9c97a95ccdf", + "DiffID": "sha256:4468e6d912c76d5b127f3554c3cd83b7dc07cce6107c6b916299ba76fa7d15ac" + }, + "SeveritySource": "redhat", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", + "Fingerprint": "sha256:d88ec8270470584c0d7d9b5355fb694ac6896c1249c5c27ec68aaac30733a95b", + "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", + "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "Severity": "LOW", + "CweIDs": [ + "CWE-273" + ], + "VendorSeverity": { + "cbl-mariner": 3, + "nvd": 3, + "oracle-oval": 1, + "photon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.2, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + } + }, + "References": [ + "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", + "https://access.redhat.com/security/cve/CVE-2019-18276", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", + "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", + "https://linux.oracle.com/cve/CVE-2019-18276.html", + "https://linux.oracle.com/errata/ELSA-2021-1679.html", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", + "https://security.gentoo.org/glsa/202105-34", + "https://security.netapp.com/advisory/ntap-20200430-0003/", + "https://www.youtube.com/watch?v=-wGtxJ8opa8" + ], + "PublishedDate": "2019-11-28T01:15:00Z", + "LastModifiedDate": "2021-05-26T12:15:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/ubuntu-1804-ignore-unfixed.json.golden b/integration/testdata/ubuntu-1804-ignore-unfixed.json.golden new file mode 100644 index 000000000000..d300f2f8fd1e --- /dev/null +++ b/integration/testdata/ubuntu-1804-ignore-unfixed.json.golden @@ -0,0 +1,397 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:61d0ff5073e754270b3b59487ac8049c50e12bca1cf792d84ec9a62dad36a8a1", + "ArtifactName": "testdata/fixtures/images/ubuntu-1804.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 66571264, + "OS": { + "Family": "ubuntu", + "Name": "18.04" + }, + "ImageID": "sha256:a2a15febcdf362f6115e801d37b5e60d6faaeedcb9896155e5fe9d754025be12", + "DiffIDs": [ + "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f", + "sha256:f7eae43028b334123c3a1d778f7bdf9783bbe651c8b15371df0120fd13ec35c5", + "sha256:7beb13bce073c21c9ee608acb13c7e851845245dc76ce81b418fdf580c45076b", + "sha256:122be11ab4a29e554786b4a1ec4764dd55656b59d6228a0a3de78eaf5c1f226c" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:ubuntu-1804" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:ubuntu-1804", + "ImageConfig": { + "architecture": "amd64", + "container": "41b694b9b42f9c5ef7fb40c24272927a727a6d6cb8120bb3eae5849ceb9bee77", + "created": "2019-08-15T07:28:14.830150536Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-08-15T07:28:12.433344678Z", + "created_by": "/bin/sh -c #(nop) ADD file:c477cb0e95c56b51e0b7353f3805165393689902b82a41bbe77dbef4b31667e1 in / " + }, + { + "created": "2019-08-15T07:28:13.20852008Z", + "created_by": "/bin/sh -c [ -z \"$(apt-get indextargets)\" ]" + }, + { + "created": "2019-08-15T07:28:13.964607567Z", + "created_by": "/bin/sh -c set -xe \t\t\u0026\u0026 echo '#!/bin/sh' \u003e /usr/sbin/policy-rc.d \t\u0026\u0026 echo 'exit 101' \u003e\u003e /usr/sbin/policy-rc.d \t\u0026\u0026 chmod +x /usr/sbin/policy-rc.d \t\t\u0026\u0026 dpkg-divert --local --rename --add /sbin/initctl \t\u0026\u0026 cp -a /usr/sbin/policy-rc.d /sbin/initctl \t\u0026\u0026 sed -i 's/^exit.*/exit 0/' /sbin/initctl \t\t\u0026\u0026 echo 'force-unsafe-io' \u003e /etc/dpkg/dpkg.cfg.d/docker-apt-speedup \t\t\u0026\u0026 echo 'DPkg::Post-Invoke { \"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true\"; };' \u003e /etc/apt/apt.conf.d/docker-clean \t\u0026\u0026 echo 'APT::Update::Post-Invoke { \"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true\"; };' \u003e\u003e /etc/apt/apt.conf.d/docker-clean \t\u0026\u0026 echo 'Dir::Cache::pkgcache \"\"; Dir::Cache::srcpkgcache \"\";' \u003e\u003e /etc/apt/apt.conf.d/docker-clean \t\t\u0026\u0026 echo 'Acquire::Languages \"none\";' \u003e /etc/apt/apt.conf.d/docker-no-languages \t\t\u0026\u0026 echo 'Acquire::GzipIndexes \"true\"; Acquire::CompressionTypes::Order:: \"gz\";' \u003e /etc/apt/apt.conf.d/docker-gzip-indexes \t\t\u0026\u0026 echo 'Apt::AutoRemove::SuggestsImportant \"false\";' \u003e /etc/apt/apt.conf.d/docker-autoremove-suggests" + }, + { + "created": "2019-08-15T07:28:14.64282638Z", + "created_by": "/bin/sh -c mkdir -p /run/systemd \u0026\u0026 echo 'docker' \u003e /run/systemd/container" + }, + { + "created": "2019-08-15T07:28:14.830150536Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f", + "sha256:f7eae43028b334123c3a1d778f7bdf9783bbe651c8b15371df0120fd13ec35c5", + "sha256:7beb13bce073c21c9ee608acb13c7e851845245dc76ce81b418fdf580c45076b", + "sha256:122be11ab4a29e554786b4a1ec4764dd55656b59d6228a0a3de78eaf5c1f226c" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:bcbe079849fdbb50b3eb04798547e046bdbc82020b8b780d767cf29f7e60b396", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 65561088, + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Size": 991232, + "Digest": "sha256:251f5509d51d9e4119d4ffb70d4820f8e2d7dc72ad15df3ebd7cd755539e40fd", + "DiffID": "sha256:f7eae43028b334123c3a1d778f7bdf9783bbe651c8b15371df0120fd13ec35c5" + }, + { + "Size": 15872, + "Digest": "sha256:8e829fe70a46e3ac4334823560e98b257234c23629f19f05460e21a453091e6d", + "DiffID": "sha256:7beb13bce073c21c9ee608acb13c7e851845245dc76ce81b418fdf580c45076b" + }, + { + "Size": 3072, + "Digest": "sha256:6001e1789921cf851f6fb2e5fe05be70f482fe9c2286f66892fe5a3bc404569c", + "DiffID": "sha256:122be11ab4a29e554786b4a1ec4764dd55656b59d6228a0a3de78eaf5c1f226c" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/ubuntu-1804.tar.gz (ubuntu 18.04)", + "Class": "os-pkgs", + "Type": "ubuntu", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-5094", + "PkgID": "e2fsprogs@1.44.1-1ubuntu1.1", + "PkgName": "e2fsprogs", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/e2fsprogs@1.44.1-1ubuntu1.1?arch=amd64\u0026distro=ubuntu-18.04", + "UID": "f254d7378713afd9" + }, + "InstalledVersion": "1.44.1-1ubuntu1.1", + "FixedVersion": "1.44.1-1ubuntu1.2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Fingerprint": "sha256:3c6011602650271fbca4f60c65fa4d645ac120eda6d795daff77eb375b7906a5", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "PkgID": "libcom-err2@1.44.1-1ubuntu1.1", + "PkgName": "libcom-err2", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/libcom-err2@1.44.1-1ubuntu1.1?arch=amd64\u0026distro=ubuntu-18.04", + "UID": "cb7f33bdf35a8b61" + }, + "InstalledVersion": "1.44.1-1ubuntu1.1", + "FixedVersion": "1.44.1-1ubuntu1.2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Fingerprint": "sha256:80978831d19a7e127e628d949a2c1b4baf2056414c20dd9998ffe351950b75e4", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "PkgID": "libext2fs2@1.44.1-1ubuntu1.1", + "PkgName": "libext2fs2", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/libext2fs2@1.44.1-1ubuntu1.1?arch=amd64\u0026distro=ubuntu-18.04", + "UID": "8d6ac68a7d76b8e7" + }, + "InstalledVersion": "1.44.1-1ubuntu1.1", + "FixedVersion": "1.44.1-1ubuntu1.2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Fingerprint": "sha256:e25514120e2898fc5f15127980590681a6fb0c456804ecaad641fed404a63fe3", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "PkgID": "libss2@1.44.1-1ubuntu1.1", + "PkgName": "libss2", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/libss2@1.44.1-1ubuntu1.1?arch=amd64\u0026distro=ubuntu-18.04", + "UID": "c872d0871f9da8dd" + }, + "InstalledVersion": "1.44.1-1ubuntu1.1", + "FixedVersion": "1.44.1-1ubuntu1.2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Fingerprint": "sha256:5a234eae518954cb5f92cd1afbf9021e8e82874442d9188508907a913cd51183", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/ubuntu-1804.json.golden b/integration/testdata/ubuntu-1804.json.golden new file mode 100644 index 000000000000..554500ad5a37 --- /dev/null +++ b/integration/testdata/ubuntu-1804.json.golden @@ -0,0 +1,461 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactID": "sha256:61d0ff5073e754270b3b59487ac8049c50e12bca1cf792d84ec9a62dad36a8a1", + "ArtifactName": "testdata/fixtures/images/ubuntu-1804.tar.gz", + "ArtifactType": "container_image", + "Metadata": { + "Size": 66571264, + "OS": { + "Family": "ubuntu", + "Name": "18.04" + }, + "ImageID": "sha256:a2a15febcdf362f6115e801d37b5e60d6faaeedcb9896155e5fe9d754025be12", + "DiffIDs": [ + "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f", + "sha256:f7eae43028b334123c3a1d778f7bdf9783bbe651c8b15371df0120fd13ec35c5", + "sha256:7beb13bce073c21c9ee608acb13c7e851845245dc76ce81b418fdf580c45076b", + "sha256:122be11ab4a29e554786b4a1ec4764dd55656b59d6228a0a3de78eaf5c1f226c" + ], + "RepoTags": [ + "ghcr.io/aquasecurity/trivy-test-images:ubuntu-1804" + ], + "Reference": "ghcr.io/aquasecurity/trivy-test-images:ubuntu-1804", + "ImageConfig": { + "architecture": "amd64", + "container": "41b694b9b42f9c5ef7fb40c24272927a727a6d6cb8120bb3eae5849ceb9bee77", + "created": "2019-08-15T07:28:14.830150536Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2019-08-15T07:28:12.433344678Z", + "created_by": "/bin/sh -c #(nop) ADD file:c477cb0e95c56b51e0b7353f3805165393689902b82a41bbe77dbef4b31667e1 in / " + }, + { + "created": "2019-08-15T07:28:13.20852008Z", + "created_by": "/bin/sh -c [ -z \"$(apt-get indextargets)\" ]" + }, + { + "created": "2019-08-15T07:28:13.964607567Z", + "created_by": "/bin/sh -c set -xe \t\t\u0026\u0026 echo '#!/bin/sh' \u003e /usr/sbin/policy-rc.d \t\u0026\u0026 echo 'exit 101' \u003e\u003e /usr/sbin/policy-rc.d \t\u0026\u0026 chmod +x /usr/sbin/policy-rc.d \t\t\u0026\u0026 dpkg-divert --local --rename --add /sbin/initctl \t\u0026\u0026 cp -a /usr/sbin/policy-rc.d /sbin/initctl \t\u0026\u0026 sed -i 's/^exit.*/exit 0/' /sbin/initctl \t\t\u0026\u0026 echo 'force-unsafe-io' \u003e /etc/dpkg/dpkg.cfg.d/docker-apt-speedup \t\t\u0026\u0026 echo 'DPkg::Post-Invoke { \"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true\"; };' \u003e /etc/apt/apt.conf.d/docker-clean \t\u0026\u0026 echo 'APT::Update::Post-Invoke { \"rm -f /var/cache/apt/archives/*.deb /var/cache/apt/archives/partial/*.deb /var/cache/apt/*.bin || true\"; };' \u003e\u003e /etc/apt/apt.conf.d/docker-clean \t\u0026\u0026 echo 'Dir::Cache::pkgcache \"\"; Dir::Cache::srcpkgcache \"\";' \u003e\u003e /etc/apt/apt.conf.d/docker-clean \t\t\u0026\u0026 echo 'Acquire::Languages \"none\";' \u003e /etc/apt/apt.conf.d/docker-no-languages \t\t\u0026\u0026 echo 'Acquire::GzipIndexes \"true\"; Acquire::CompressionTypes::Order:: \"gz\";' \u003e /etc/apt/apt.conf.d/docker-gzip-indexes \t\t\u0026\u0026 echo 'Apt::AutoRemove::SuggestsImportant \"false\";' \u003e /etc/apt/apt.conf.d/docker-autoremove-suggests" + }, + { + "created": "2019-08-15T07:28:14.64282638Z", + "created_by": "/bin/sh -c mkdir -p /run/systemd \u0026\u0026 echo 'docker' \u003e /run/systemd/container" + }, + { + "created": "2019-08-15T07:28:14.830150536Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/bash\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f", + "sha256:f7eae43028b334123c3a1d778f7bdf9783bbe651c8b15371df0120fd13ec35c5", + "sha256:7beb13bce073c21c9ee608acb13c7e851845245dc76ce81b418fdf580c45076b", + "sha256:122be11ab4a29e554786b4a1ec4764dd55656b59d6228a0a3de78eaf5c1f226c" + ] + }, + "config": { + "Cmd": [ + "/bin/bash" + ], + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Image": "sha256:bcbe079849fdbb50b3eb04798547e046bdbc82020b8b780d767cf29f7e60b396", + "ArgsEscaped": true + } + }, + "Layers": [ + { + "Size": 65561088, + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Size": 991232, + "Digest": "sha256:251f5509d51d9e4119d4ffb70d4820f8e2d7dc72ad15df3ebd7cd755539e40fd", + "DiffID": "sha256:f7eae43028b334123c3a1d778f7bdf9783bbe651c8b15371df0120fd13ec35c5" + }, + { + "Size": 15872, + "Digest": "sha256:8e829fe70a46e3ac4334823560e98b257234c23629f19f05460e21a453091e6d", + "DiffID": "sha256:7beb13bce073c21c9ee608acb13c7e851845245dc76ce81b418fdf580c45076b" + }, + { + "Size": 3072, + "Digest": "sha256:6001e1789921cf851f6fb2e5fe05be70f482fe9c2286f66892fe5a3bc404569c", + "DiffID": "sha256:122be11ab4a29e554786b4a1ec4764dd55656b59d6228a0a3de78eaf5c1f226c" + } + ] + }, + "Results": [ + { + "Target": "testdata/fixtures/images/ubuntu-1804.tar.gz (ubuntu 18.04)", + "Class": "os-pkgs", + "Type": "ubuntu", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-18276", + "PkgID": "bash@4.4.18-2ubuntu1.2", + "PkgName": "bash", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/bash@4.4.18-2ubuntu1.2?arch=amd64\u0026distro=ubuntu-18.04", + "UID": "72115b0ee60dbb71" + }, + "InstalledVersion": "4.4.18-2ubuntu1.2", + "Status": "affected", + "Layer": { + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-18276", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Fingerprint": "sha256:3eb059eaf0d15c3b12577858405b74958ba73745fe7b4ae1bc0eb0ae30dc7adf", + "Title": "bash: when effective UID is not equal to its real UID the saved UID is not dropped", + "Description": "An issue was discovered in disable_priv_mode in shell.c in GNU Bash through 5.0 patch 11. By default, if Bash is run with its effective UID not equal to its real UID, it will drop privileges by setting its effective UID to its real UID. However, it does so incorrectly. On Linux and other systems that support \"saved UID\" functionality, the saved UID is not dropped. An attacker with command execution in the shell can use \"enable -f\" for runtime loading of a new builtin, which can be a shared object that calls setuid() and therefore regains privileges. However, binaries running with an effective UID of 0 are unaffected.", + "Severity": "LOW", + "CweIDs": [ + "CWE-273" + ], + "VendorSeverity": { + "cbl-mariner": 3, + "nvd": 3, + "oracle-oval": 1, + "photon": 3, + "redhat": 1, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:C/I:C/A:C", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 7.2, + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + } + }, + "References": [ + "http://packetstormsecurity.com/files/155498/Bash-5.0-Patch-11-Privilege-Escalation.html", + "https://access.redhat.com/security/cve/CVE-2019-18276", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-18276", + "https://github.com/bminor/bash/commit/951bdaad7a18cc0dc1036bba86b18b90874d39ff", + "https://linux.oracle.com/cve/CVE-2019-18276.html", + "https://linux.oracle.com/errata/ELSA-2021-1679.html", + "https://lists.apache.org/thread.html/rf9fa47ab66495c78bb4120b0754dd9531ca2ff0430f6685ac9b07772@%3Cdev.mina.apache.org%3E", + "https://nvd.nist.gov/vuln/detail/CVE-2019-18276", + "https://security.gentoo.org/glsa/202105-34", + "https://security.netapp.com/advisory/ntap-20200430-0003/", + "https://www.youtube.com/watch?v=-wGtxJ8opa8" + ], + "PublishedDate": "2019-11-28T01:15:00Z", + "LastModifiedDate": "2021-05-26T12:15:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "PkgID": "e2fsprogs@1.44.1-1ubuntu1.1", + "PkgName": "e2fsprogs", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/e2fsprogs@1.44.1-1ubuntu1.1?arch=amd64\u0026distro=ubuntu-18.04", + "UID": "f254d7378713afd9" + }, + "InstalledVersion": "1.44.1-1ubuntu1.1", + "FixedVersion": "1.44.1-1ubuntu1.2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Fingerprint": "sha256:3c6011602650271fbca4f60c65fa4d645ac120eda6d795daff77eb375b7906a5", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "PkgID": "libcom-err2@1.44.1-1ubuntu1.1", + "PkgName": "libcom-err2", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/libcom-err2@1.44.1-1ubuntu1.1?arch=amd64\u0026distro=ubuntu-18.04", + "UID": "cb7f33bdf35a8b61" + }, + "InstalledVersion": "1.44.1-1ubuntu1.1", + "FixedVersion": "1.44.1-1ubuntu1.2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Fingerprint": "sha256:80978831d19a7e127e628d949a2c1b4baf2056414c20dd9998ffe351950b75e4", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "PkgID": "libext2fs2@1.44.1-1ubuntu1.1", + "PkgName": "libext2fs2", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/libext2fs2@1.44.1-1ubuntu1.1?arch=amd64\u0026distro=ubuntu-18.04", + "UID": "8d6ac68a7d76b8e7" + }, + "InstalledVersion": "1.44.1-1ubuntu1.1", + "FixedVersion": "1.44.1-1ubuntu1.2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Fingerprint": "sha256:e25514120e2898fc5f15127980590681a6fb0c456804ecaad641fed404a63fe3", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + }, + { + "VulnerabilityID": "CVE-2019-5094", + "PkgID": "libss2@1.44.1-1ubuntu1.1", + "PkgName": "libss2", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/libss2@1.44.1-1ubuntu1.1?arch=amd64\u0026distro=ubuntu-18.04", + "UID": "c872d0871f9da8dd" + }, + "InstalledVersion": "1.44.1-1ubuntu1.1", + "FixedVersion": "1.44.1-1ubuntu1.2", + "Status": "fixed", + "Layer": { + "Digest": "sha256:35c102085707f703de2d9eaad8752d6fe1b8f02b5d2149f1d8357c9cc7fb7d0a", + "DiffID": "sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + "SeveritySource": "ubuntu", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-5094", + "DataSource": { + "ID": "ubuntu", + "Name": "Ubuntu CVE Tracker", + "URL": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "Fingerprint": "sha256:5a234eae518954cb5f92cd1afbf9021e8e82874442d9188508907a913cd51183", + "Title": "e2fsprogs: Crafted ext4 partition leads to out-of-bounds write", + "Description": "An exploitable code execution vulnerability exists in the quota file functionality of E2fsprogs 1.45.3. A specially crafted ext4 partition can cause an out-of-bounds write on the heap, resulting in code execution. An attacker can corrupt a partition to trigger this vulnerability.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "amazon": 2, + "cbl-mariner": 2, + "nvd": 2, + "oracle-oval": 2, + "photon": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:L/AC:L/Au:N/C:P/I:P/A:P", + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V2Score": 4.6, + "V3Score": 6.7 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 6.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2019-5094", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-5094", + "https://linux.oracle.com/cve/CVE-2019-5094.html", + "https://linux.oracle.com/errata/ELSA-2020-4011.html", + "https://lists.debian.org/debian-lts-announce/2019/09/msg00029.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/2AKETJ6BREDUHRWQTV35SPGG5C6H7KSI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/6DOBCYQKCTTWXBLMUPJ5TX3FY7JNCOKY/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-5094", + "https://seclists.org/bugtraq/2019/Sep/58", + "https://security.gentoo.org/glsa/202003-05", + "https://security.netapp.com/advisory/ntap-20200115-0002/", + "https://talosintelligence.com/vulnerability_reports/TALOS-2019-0887", + "https://ubuntu.com/security/notices/USN-4142-1", + "https://ubuntu.com/security/notices/USN-4142-2", + "https://usn.ubuntu.com/4142-1/", + "https://usn.ubuntu.com/4142-2/", + "https://www.debian.org/security/2019/dsa-4535" + ], + "PublishedDate": "2019-09-24T22:15:00Z", + "LastModifiedDate": "2021-01-11T19:21:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/ubuntu-gp2-x86-vm.json.golden b/integration/testdata/ubuntu-gp2-x86-vm.json.golden new file mode 100644 index 000000000000..412f8372270a --- /dev/null +++ b/integration/testdata/ubuntu-gp2-x86-vm.json.golden @@ -0,0 +1,79 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "disk.img", + "ArtifactType": "vm", + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "Metadata": { + "OS": { + "Family": "ubuntu", + "Name": "22.04" + }, + "ImageConfig": { + "architecture": "", + "created": "0001-01-01T00:00:00Z", + "os": "", + "rootfs": { + "type": "", + "diff_ids": null + }, + "config": {} + } + }, + "Results": [ + { + "Target": "disk.img (ubuntu 22.04)", + "Class": "os-pkgs", + "Type": "ubuntu", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2022-3715", + "PkgID": "bash@5.1-6ubuntu1", + "PkgName": "bash", + "PkgIdentifier": { + "PURL": "pkg:deb/ubuntu/bash@5.1-6ubuntu1?arch=amd64\u0026distro=ubuntu-22.04" + }, + "InstalledVersion": "5.1-6ubuntu1", + "Status": "affected", + "Layer": {}, + "SeveritySource": "nvd", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2022-3715", + "Title": "a heap-buffer-overflow in valid_parameter_transform", + "Description": "A flaw was found in the bash package, where a heap-buffer overflow can occur in valid parameter_transform. This issue may lead to memory problems.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-787" + ], + "VendorSeverity": { + "cbl-mariner": 3, + "nvd": 3, + "photon": 3, + "redhat": 1, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + "V3Score": 7.8 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:H", + "V3Score": 6.6 + } + }, + "References": [ + "https://access.redhat.com/errata/RHSA-2023:0340", + "https://access.redhat.com/security/cve/CVE-2022-3715", + "https://bugzilla.redhat.com/2126720", + "https://bugzilla.redhat.com/show_bug.cgi?id=2126720" + ], + "PublishedDate": "2023-01-05T15:15:00Z", + "LastModifiedDate": "2023-02-24T18:38:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/uv.json.golden b/integration/testdata/uv.json.golden new file mode 100644 index 000000000000..16b0d36698d8 --- /dev/null +++ b/integration/testdata/uv.json.golden @@ -0,0 +1,187 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/uv", + "ArtifactType": "repository", + "Results": [ + { + "Target": "uv.lock", + "Class": "lang-pkgs", + "Type": "uv", + "Packages": [ + { + "ID": "uv-test@0.1.0", + "Name": "uv-test", + "Identifier": { + "PURL": "pkg:pypi/uv-test@0.1.0", + "UID": "228f4e2a887863ad" + }, + "Version": "0.1.0", + "Relationship": "root", + "DependsOn": [ + "click@8.1.3", + "werkzeug@0.11.1" + ], + "AnalyzedBy": "uv" + }, + { + "ID": "click@8.1.3", + "Name": "click", + "Identifier": { + "PURL": "pkg:pypi/click@8.1.3", + "UID": "f6767aead12b99cf" + }, + "Version": "8.1.3", + "Relationship": "direct", + "DependsOn": [ + "colorama@0.4.6" + ], + "AnalyzedBy": "uv" + }, + { + "ID": "werkzeug@0.11.1", + "Name": "werkzeug", + "Identifier": { + "PURL": "pkg:pypi/werkzeug@0.11.1", + "UID": "40cd870d03659073" + }, + "Version": "0.11.1", + "Relationship": "direct", + "AnalyzedBy": "uv" + }, + { + "ID": "colorama@0.4.6", + "Name": "colorama", + "Identifier": { + "PURL": "pkg:pypi/colorama@0.4.6", + "UID": "a9b0f41cf3bb79cc" + }, + "Version": "0.4.6", + "Indirect": true, + "Relationship": "indirect", + "AnalyzedBy": "uv" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-14806", + "PkgID": "werkzeug@0.11.1", + "PkgName": "werkzeug", + "PkgIdentifier": { + "PURL": "pkg:pypi/werkzeug@0.11.1", + "UID": "40cd870d03659073" + }, + "InstalledVersion": "0.11.1", + "FixedVersion": "0.15.3", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-14806", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Pip", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + }, + "Fingerprint": "sha256:aa64e889de7c75491704abe3a427c786acb12261fbffaaa93547c2e67d13c344", + "Title": "python-werkzeug: insufficient debugger PIN randomness vulnerability", + "Description": "Pallets Werkzeug before 0.15.3, when used with Docker, has insufficient debugger PIN randomness because Docker containers share the same machine id.", + "Severity": "HIGH", + "CweIDs": [ + "CWE-331" + ], + "VendorSeverity": { + "ghsa": 3, + "nvd": 3, + "redhat": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V2Score": 5, + "V3Score": 7.5 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N", + "V3Score": 7.5 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00034.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-09/msg00047.html", + "https://access.redhat.com/security/cve/CVE-2019-14806", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-14806", + "https://github.com/advisories/GHSA-gq9m-qvpx-68hc", + "https://github.com/pallets/werkzeug/blob/7fef41b120327d3912fbe12fb64f1951496fcf3e/src/werkzeug/debug/__init__.py#L168", + "https://github.com/pallets/werkzeug/commit/00bc43b1672e662e5e3b8cecd79e67fc968fa246", + "https://nvd.nist.gov/vuln/detail/CVE-2019-14806", + "https://palletsprojects.com/blog/werkzeug-0-15-3-released/", + "https://ubuntu.com/security/notices/USN-4655-1" + ], + "PublishedDate": "2019-08-09T15:15:00Z", + "LastModifiedDate": "2019-09-11T00:15:00Z" + }, + { + "VulnerabilityID": "CVE-2020-28724", + "PkgID": "werkzeug@0.11.1", + "PkgName": "werkzeug", + "PkgIdentifier": { + "PURL": "pkg:pypi/werkzeug@0.11.1", + "UID": "40cd870d03659073" + }, + "InstalledVersion": "0.11.1", + "FixedVersion": "0.11.6", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-28724", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Pip", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + }, + "Fingerprint": "sha256:18bbe0c4a1942e5d43645bba04a0067358adccbaa71922ba4d269c78100cd018", + "Title": "python-werkzeug: open redirect via double slash in the URL", + "Description": "Open redirect vulnerability in werkzeug before 0.11.6 via a double slash in the URL.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-601" + ], + "VendorSeverity": { + "ghsa": 2, + "nvd": 2, + "redhat": 2, + "ubuntu": 2 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:P/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 5.8, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N", + "V3Score": 5.4 + } + }, + "References": [ + "https://access.redhat.com/security/cve/CVE-2020-28724", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28724", + "https://github.com/advisories/GHSA-3p3h-qghp-hvh2", + "https://github.com/pallets/flask/issues/1639", + "https://github.com/pallets/werkzeug/issues/822", + "https://github.com/pallets/werkzeug/pull/890/files", + "https://nvd.nist.gov/vuln/detail/CVE-2020-28724", + "https://ubuntu.com/security/notices/USN-4655-1" + ], + "PublishedDate": "2020-11-18T15:15:00Z", + "LastModifiedDate": "2020-12-01T16:05:00Z" + } + ] + } + ] +} diff --git a/integration/testdata/yarn.json.golden b/integration/testdata/yarn.json.golden new file mode 100644 index 000000000000..b95878d440e6 --- /dev/null +++ b/integration/testdata/yarn.json.golden @@ -0,0 +1,191 @@ +{ + "SchemaVersion": 2, + "Trivy": { + "Version": "dev" + }, + "ReportID": "017b7d41-e09f-7000-80ea-000000000001", + "CreatedAt": "2021-08-25T12:20:30.000000005Z", + "ArtifactName": "testdata/fixtures/repo/yarn", + "ArtifactType": "repository", + "Results": [ + { + "Target": "yarn.lock", + "Class": "lang-pkgs", + "Type": "yarn", + "Packages": [ + { + "ID": "integration@1.0.0", + "Name": "integration", + "Identifier": { + "PURL": "pkg:npm/integration@1.0.0", + "UID": "ca7e77fc5b068ad9" + }, + "Version": "1.0.0", + "Licenses": [ + "MIT" + ], + "Relationship": "root", + "DependsOn": [ + "jquery@3.2.1" + ], + "AnalyzedBy": "yarn" + }, + { + "ID": "jquery@3.2.1", + "Name": "jquery", + "Identifier": { + "PURL": "pkg:npm/jquery@3.2.1", + "UID": "dc546992cc134cdc" + }, + "Version": "3.2.1", + "Licenses": [ + "MIT" + ], + "Relationship": "direct", + "Locations": [ + { + "StartLine": 10, + "EndLine": 13 + } + ], + "AnalyzedBy": "yarn" + } + ], + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2019-11358", + "PkgID": "jquery@3.2.1", + "PkgName": "jquery", + "PkgIdentifier": { + "PURL": "pkg:npm/jquery@3.2.1", + "UID": "dc546992cc134cdc" + }, + "InstalledVersion": "3.2.1", + "FixedVersion": "3.4.0", + "Status": "fixed", + "SeveritySource": "ghsa", + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2019-11358", + "DataSource": { + "ID": "ghsa", + "Name": "GitHub Security Advisory Npm", + "URL": "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Anpm" + }, + "Fingerprint": "sha256:23bbddd3e1db8191a5b07ecf31200cab4d339681264b902ac86b334501d10d8f", + "Title": "jquery: Prototype pollution in object's prototype leading to denial of service, remote code execution, or property injection", + "Description": "jQuery before 3.4.0, as used in Drupal, Backdrop CMS, and other products, mishandles jQuery.extend(true, {}, ...) because of Object.prototype pollution. If an unsanitized source object contained an enumerable __proto__ property, it could extend the native Object.prototype.", + "Severity": "MEDIUM", + "CweIDs": [ + "CWE-79" + ], + "VendorSeverity": { + "alma": 2, + "amazon": 2, + "arch-linux": 2, + "ghsa": 2, + "nodejs-security-wg": 2, + "nvd": 2, + "oracle-oval": 2, + "redhat": 2, + "ruby-advisory-db": 2, + "ubuntu": 1 + }, + "CVSS": { + "nvd": { + "V2Vector": "AV:N/AC:M/Au:N/C:N/I:P/A:N", + "V3Vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N", + "V2Score": 4.3, + "V3Score": 6.1 + }, + "redhat": { + "V3Vector": "CVSS:3.0/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:L", + "V3Score": 5.6 + } + }, + "References": [ + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00006.html", + "http://lists.opensuse.org/opensuse-security-announce/2019-08/msg00025.html", + "http://packetstormsecurity.com/files/152787/dotCMS-5.1.1-Vulnerable-Dependencies.html", + "http://packetstormsecurity.com/files/153237/RetireJS-CORS-Issue-Script-Execution.html", + "http://packetstormsecurity.com/files/156743/OctoberCMS-Insecure-Dependencies.html", + "http://seclists.org/fulldisclosure/2019/May/10", + "http://seclists.org/fulldisclosure/2019/May/11", + "http://seclists.org/fulldisclosure/2019/May/13", + "http://www.openwall.com/lists/oss-security/2019/06/03/2", + "http://www.securityfocus.com/bid/108023", + "https://access.redhat.com/errata/RHBA-2019:1570", + "https://access.redhat.com/errata/RHSA-2019:1456", + "https://access.redhat.com/errata/RHSA-2019:2587", + "https://access.redhat.com/errata/RHSA-2019:3023", + "https://access.redhat.com/errata/RHSA-2019:3024", + "https://access.redhat.com/security/cve/CVE-2019-11358", + "https://backdropcms.org/security/backdrop-sa-core-2019-009", + "https://blog.jquery.com/2019/04/10/jquery-3-4-0-released/", + "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2019-11358", + "https://github.com/DanielRuf/snyk-js-jquery-174006?files=1", + "https://github.com/advisories/GHSA-6c3j-c64m-qhgq", + "https://github.com/jquery/jquery/commit/753d591aea698e57d6db58c9f722cd0808619b1b", + "https://github.com/jquery/jquery/pull/4333", + "https://github.com/rails/jquery-rails/blob/master/CHANGELOG.md#434", + "https://hackerone.com/reports/454365", + "https://kb.pulsesecure.net/articles/Pulse_Security_Advisories/SA44601", + "https://linux.oracle.com/cve/CVE-2019-11358.html", + "https://linux.oracle.com/errata/ELSA-2020-4847.html", + "https://lists.apache.org/thread.html/08720ef215ee7ab3386c05a1a90a7d1c852bf0706f176a7816bf65fc@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/519eb0fd45642dcecd9ff74cb3e71c20a4753f7d82e2f07864b5108f@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/5928aa293e39d248266472210c50f176cac1535220f2486e6a7fa844@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/6097cdbd6f0a337bedd9bb5cc441b2d525ff002a96531de367e4259f@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/88fb0362fd40e5b605ea8149f63241537b8b6fb5bfa315391fc5cbb7@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/b0656d359c7d40ec9f39c8cc61bca66802ef9a2a12ee199f5b0c1442@%3Cdev.drill.apache.org%3E", + "https://lists.apache.org/thread.html/b736d0784cf02f5a30fbb4c5902762a15ad6d47e17e2c5a17b7d6205@%3Ccommits.airflow.apache.org%3E", + "https://lists.apache.org/thread.html/ba79cf1658741e9f146e4c59b50aee56656ea95d841d358d006c18b6@%3Ccommits.roller.apache.org%3E", + "https://lists.apache.org/thread.html/bcce5a9c532b386c68dab2f6b3ce8b0cc9b950ec551766e76391caa3@%3Ccommits.nifi.apache.org%3E", + "https://lists.apache.org/thread.html/f9bc3e55f4e28d1dcd1a69aae6d53e609a758e34d2869b4d798e13cc@%3Cissues.drill.apache.org%3E", + "https://lists.apache.org/thread.html/r2041a75d3fc09dec55adfd95d598b38d22715303f65c997c054844c9@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r2baacab6e0acb5a2092eb46ae04fd6c3e8277b4fd79b1ffb7f3254fa@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r38f0d1aa3c923c22977fe7376508f030f22e22c1379fbb155bf29766@%3Cdev.syncope.apache.org%3E", + "https://lists.apache.org/thread.html/r41b5bfe009c845f67d4f68948cc9419ac2d62e287804aafd72892b08@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7aac081cbddb6baa24b75e74abf0929bf309b176755a53e3ed810355@%3Cdev.flink.apache.org%3E", + "https://lists.apache.org/thread.html/r7d64895cc4dff84d0becfc572b20c0e4bf9bfa7b10c6f5f73e783734@%3Cdev.storm.apache.org%3E", + "https://lists.apache.org/thread.html/r7e8ebccb7c022e41295f6fdb7b971209b83702339f872ddd8cf8bf73@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rac25da84ecdcd36f6de5ad0d255f4e967209bbbebddb285e231da37d@%3Cissues.flink.apache.org%3E", + "https://lists.apache.org/thread.html/rca37935d661f4689cb4119f1b3b224413b22be161b678e6e6ce0c69b@%3Ccommits.nifi.apache.org%3E", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00006.html", + "https://lists.debian.org/debian-lts-announce/2019/05/msg00029.html", + "https://lists.debian.org/debian-lts-announce/2020/02/msg00024.html", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/4UOAZIFCSZ3ENEFOR5IXX6NFAD3HV7FA/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/5IABSKTYZ5JUGL735UKGXL5YPRYOPUYI/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/KYH3OAGR2RTCHRA5NOKX2TES7SNQMWGO/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/QV3PKZC3PQCO3273HAT76PAQZFBEO4KP/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/RLXRX23725JL366CNZGJZ7AQQB7LHQ6F/", + "https://lists.fedoraproject.org/archives/list/package-announce@lists.fedoraproject.org/message/WZW27UCJ5CYFL4KFFFMYMIBNMIU2ALG5/", + "https://nvd.nist.gov/vuln/detail/CVE-2019-11358", + "https://seclists.org/bugtraq/2019/Apr/32", + "https://seclists.org/bugtraq/2019/Jun/12", + "https://seclists.org/bugtraq/2019/May/18", + "https://security.netapp.com/advisory/ntap-20190919-0001/", + "https://snyk.io/vuln/SNYK-JS-JQUERY-174006", + "https://www.debian.org/security/2019/dsa-4434", + "https://www.debian.org/security/2019/dsa-4460", + "https://www.drupal.org/sa-core-2019-006", + "https://www.oracle.com//security-alerts/cpujul2021.html", + "https://www.oracle.com/security-alerts/cpuApr2021.html", + "https://www.oracle.com/security-alerts/cpuapr2020.html", + "https://www.oracle.com/security-alerts/cpujan2020.html", + "https://www.oracle.com/security-alerts/cpujan2021.html", + "https://www.oracle.com/security-alerts/cpujul2020.html", + "https://www.oracle.com/security-alerts/cpuoct2020.html", + "https://www.oracle.com/security-alerts/cpuoct2021.html", + "https://www.oracle.com/technetwork/security-advisory/cpujul2019-5072835.html", + "https://www.oracle.com/technetwork/security-advisory/cpuoct2019-5072832.html", + "https://www.privacy-wise.com/mitigating-cve-2019-11358-in-old-versions-of-jquery/", + "https://www.synology.com/security/advisory/Synology_SA_19_19", + "https://www.tenable.com/security/tns-2019-08", + "https://www.tenable.com/security/tns-2020-02" + ], + "PublishedDate": "2019-04-20T00:29:00Z", + "LastModifiedDate": "2021-10-20T11:15:00Z" + } + ] + } + ] +} diff --git a/integration/testimages.ini b/integration/testimages.ini new file mode 100644 index 000000000000..1a7b8ef1a0c0 --- /dev/null +++ b/integration/testimages.ini @@ -0,0 +1,3 @@ +# Configuration file for both shell scripts and Go programs +TEST_IMAGES=ghcr.io/aquasecurity/trivy-test-images +TEST_VM_IMAGES=ghcr.io/aquasecurity/trivy-test-vm-images diff --git a/integration/vm_test.go b/integration/vm_test.go new file mode 100644 index 000000000000..27526a730c41 --- /dev/null +++ b/integration/vm_test.go @@ -0,0 +1,113 @@ +//go:build vm_integration + +package integration + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/types" +) + +// TestVM tests scanning VM images (VMDK, disk images). +// +// TODO: Golden files cannot be updated with the -update flag currently because +// ArtifactName contains random file paths from t.TempDir() and Target contains full paths. +// This test applies overrides to normalize these values for comparison, but those overrides +// would not be applied to golden files in update mode. +// For now, golden files must be updated manually. +func TestVM(t *testing.T) { + if *update { + t.Fatal("TestVM does not support -update flag. Golden files must be updated manually. See TODO comment above.") + } + type args struct { + input string + format string + artifactType string + } + tests := []struct { + name string + args args + golden string + override types.Report + }{ + { + name: "amazon linux 2 in VMDK, filesystem XFS", + args: args{ + input: "testdata/fixtures/vm-images/amazon-2.vmdk.gz", + format: "json", + artifactType: "vm", + }, + golden: goldenAmazonLinux2GP2X86VM, + }, + { + name: "amazon linux 2 in Snapshot, filesystem XFS", + args: args{ + input: "testdata/fixtures/vm-images/amazon-2.img.gz", + format: "json", + artifactType: "vm", + }, + golden: goldenAmazonLinux2GP2X86VM, + }, + { + name: "Ubuntu in Snapshot, filesystem EXT4", + args: args{ + input: "testdata/fixtures/vm-images/ubuntu-2204.img.gz", + format: "json", + artifactType: "vm", + }, + golden: goldenUbuntuGP2X86VM, + }, + { + name: "Ubuntu in VMDK, filesystem EXT4", + args: args{ + input: "testdata/fixtures/vm-images/ubuntu-2204.vmdk.gz", + format: "json", + artifactType: "vm", + }, + golden: goldenUbuntuGP2X86VM, + }, + } + + // Set up testing DB + cacheDir := initDB(t) + + const imageFile = "disk.img" + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + osArgs := []string{ + "--cache-dir", + cacheDir, + "vm", + "--scanners", + "vuln", + "-q", + "--skip-db-update", + "--format", + tt.args.format, + "--list-all-pkgs=false", + } + + // Decompress the gzipped image file + imagePath := filepath.Join(t.TempDir(), imageFile) + testutil.DecompressSparseGzip(t, tt.args.input, imagePath) + + osArgs = append(osArgs, imagePath) + + // Run "trivy vm" + runTest(t, osArgs, tt.golden, types.FormatJSON, runOptions{ + override: overrideFuncs(overrideUID, overrideFingerprint, func(t *testing.T, _, got *types.Report) { + got.ArtifactName = "disk.img" + for i := range got.Results { + lastIndex := strings.LastIndex(got.Results[i].Target, "/") + got.Results[i].Target = got.Results[i].Target[lastIndex+1:] + } + }), + fakeUUID: "3ff14136-e09f-4df9-80ea-%012d", + }) + }) + } +} diff --git a/internal/cachetest/cache.go b/internal/cachetest/cache.go new file mode 100644 index 000000000000..b91adac87f22 --- /dev/null +++ b/internal/cachetest/cache.go @@ -0,0 +1,122 @@ +package cachetest + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +type WantArtifact struct { + ID string + ArtifactInfo types.ArtifactInfo +} + +type WantBlob struct { + ID string + BlobInfo types.BlobInfo +} + +type ErrorCache struct { + *cache.MemoryCache + opts ErrorCacheOptions +} + +type ErrorCacheOptions struct { + MissingBlobs bool + PutArtifact bool + PutBlob bool + GetArtifact bool + GetBlob bool +} + +func NewErrorCache(opts ErrorCacheOptions) *ErrorCache { + return &ErrorCache{ + MemoryCache: cache.NewMemoryCache(), + opts: opts, + } +} + +func (c *ErrorCache) MissingBlobs(ctx context.Context, artifactID string, blobIDs []string) (bool, []string, error) { + if c.opts.MissingBlobs { + return false, nil, errors.New("MissingBlobs failed") + } + return c.MemoryCache.MissingBlobs(ctx, artifactID, blobIDs) +} + +func (c *ErrorCache) PutArtifact(ctx context.Context, artifactID string, artifactInfo types.ArtifactInfo) error { + if c.opts.PutArtifact { + return errors.New("PutArtifact failed") + } + return c.MemoryCache.PutArtifact(ctx, artifactID, artifactInfo) +} + +func (c *ErrorCache) PutBlob(ctx context.Context, artifactID string, blobInfo types.BlobInfo) error { + if c.opts.PutBlob { + return errors.New("PutBlob failed") + } + return c.MemoryCache.PutBlob(ctx, artifactID, blobInfo) +} + +func (c *ErrorCache) GetArtifact(ctx context.Context, artifactID string) (types.ArtifactInfo, error) { + if c.opts.GetArtifact { + return types.ArtifactInfo{}, errors.New("GetArtifact failed") + } + return c.MemoryCache.GetArtifact(ctx, artifactID) +} + +func (c *ErrorCache) GetBlob(ctx context.Context, blobID string) (types.BlobInfo, error) { + if c.opts.GetBlob { + return types.BlobInfo{}, errors.New("GetBlob failed") + } + return c.MemoryCache.GetBlob(ctx, blobID) +} + +func NewCache(t *testing.T, setUpCache func(t *testing.T) cache.Cache) cache.Cache { + if setUpCache != nil { + return setUpCache(t) + } + return cache.NewMemoryCache() +} + +func AssertArtifact(t *testing.T, c cache.Cache, wantArtifact WantArtifact) { + gotArtifact, err := c.GetArtifact(t.Context(), wantArtifact.ID) + require.NoError(t, err, "artifact not found") + assert.Equal(t, wantArtifact.ArtifactInfo, gotArtifact, wantArtifact.ID) +} + +func AssertBlobs(t *testing.T, c cache.Cache, wantBlobs []WantBlob) { + if m, ok := c.(*cache.MemoryCache); ok { + blobIDs := m.BlobIDs() + wantBlobIDs := xslices.Map(wantBlobs, func(want WantBlob) string { + return want.ID + }) + require.ElementsMatch(t, wantBlobIDs, blobIDs, "blob IDs mismatch") + } + + for _, want := range wantBlobs { + got, err := c.GetBlob(t.Context(), want.ID) + require.NoError(t, err, "blob not found") + + for i := range got.Misconfigurations { + // suppress misconfiguration code block + for j := range got.Misconfigurations[i].Failures { + got.Misconfigurations[i].Failures[j].Code = types.Code{} + } + for j := range got.Misconfigurations[i].Successes { + got.Misconfigurations[i].Successes[j].Code = types.Code{} + } + for j := range got.Misconfigurations[i].Warnings { + got.Misconfigurations[i].Warnings[j].Code = types.Code{} + } + } + + assert.Equal(t, want.BlobInfo, got, want.ID) + } +} diff --git a/internal/dbtest/db.go b/internal/dbtest/db.go new file mode 100644 index 000000000000..05c1478408bb --- /dev/null +++ b/internal/dbtest/db.go @@ -0,0 +1,92 @@ +package dbtest + +import ( + "os" + "path/filepath" + "testing" + "time" + + "github.com/samber/lo" + "github.com/stretchr/testify/require" + + fixtures "github.com/aquasecurity/bolt-fixtures" + trivydb "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/metadata" + jdb "github.com/aquasecurity/trivy-java-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/db" +) + +// InitDB initializes testing database. +func InitDB(t *testing.T, fixtureFiles []string) string { + // Create a temp dir + cacheDir := t.TempDir() + + dbDir := db.Dir(cacheDir) + dbPath := trivydb.Path(dbDir) + err := os.MkdirAll(dbDir, 0o700) + require.NoError(t, err) + + // Load testdata into BoltDB + loader, err := fixtures.New(dbPath, fixtureFiles) + require.NoError(t, err) + require.NoError(t, loader.Load()) + require.NoError(t, loader.Close()) + + // Initialize DB + require.NoError(t, db.Init(dbDir)) + + return cacheDir +} + +func Close() error { + return db.Close() +} + +// InitWithMetadata initializes a database with optional metadata and DB file creation. +// If meta is empty, no metadata file is created. +// If createDBFile is false, no DB file is created (useful for testing "DB not found" scenarios). +// Returns the dbDir path. +func InitWithMetadata(t *testing.T, meta metadata.Metadata, createDBFile bool) string { + t.Helper() + + cacheDir := t.TempDir() + dbDir := db.Dir(cacheDir) + + // Create metadata if provided + if !lo.IsEmpty(meta) { + metaClient := metadata.NewClient(dbDir) + err := metaClient.Update(meta) + require.NoError(t, err) + } + + // Create DB file if requested + if createDBFile { + // First, create the DB file using trivy-db directly in write mode + err := trivydb.Init(dbDir) + require.NoError(t, err) + require.NoError(t, trivydb.Close()) + + // Then open it in read-only mode using our wrapper + err = db.Init(dbDir) + require.NoError(t, err) + } + + return dbDir +} + +func InitJavaDB(t *testing.T, cacheDir string) { + dbDir := filepath.Join(cacheDir, "java-db") + javaDB, err := jdb.New(dbDir) + require.NoError(t, err) + err = javaDB.Init() + require.NoError(t, err) + + meta := jdb.Metadata{ + Version: jdb.SchemaVersion, + NextUpdate: time.Now().Add(24 * time.Hour), + UpdatedAt: time.Now(), + } + metac := jdb.NewMetadata(dbDir) + err = metac.Update(meta) + require.NoError(t, err) +} diff --git a/internal/dbtest/fake.go b/internal/dbtest/fake.go new file mode 100644 index 000000000000..528c549bda7f --- /dev/null +++ b/internal/dbtest/fake.go @@ -0,0 +1,81 @@ +package dbtest + +import ( + "archive/tar" + "os" + "path/filepath" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + fakei "github.com/google/go-containerregistry/pkg/v1/fake" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/samber/lo" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/oci" +) + +const defaultMediaType = "application/vnd.aquasec.trivy.db.layer.v1.tar+gzip" + +type fakeLayer struct { + v1.Layer +} + +func (f fakeLayer) MediaType() (types.MediaType, error) { + return f.Layer.MediaType() +} + +func NewFakeLayer(t *testing.T, input string, mediaType types.MediaType) v1.Layer { + layer, err := tarball.LayerFromFile(input, tarball.WithMediaType(mediaType)) + require.NoError(t, err) + + return fakeLayer{layer} +} + +type FakeDBOptions struct { + MediaType types.MediaType +} + +func NewFakeDB(t *testing.T, dbPath string, opts FakeDBOptions) *oci.Artifact { + mediaType := lo.Ternary(opts.MediaType != "", opts.MediaType, defaultMediaType) + img := new(fakei.FakeImage) + img.LayersReturns([]v1.Layer{NewFakeLayer(t, dbPath, mediaType)}, nil) + img.ManifestReturns(&v1.Manifest{ + Layers: []v1.Descriptor{ + { + MediaType: mediaType, + Size: 100, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "aec482bc254b5dd025d3eaf5bb35997d3dba783e394e8f91d5a415963151bfb8", + }, + Annotations: map[string]string{ + "org.opencontainers.image.title": "db.tar.gz", + }, + }, + }, + }, nil) + + // Mock OCI artifact + opt := ftypes.RegistryOptions{ + Insecure: false, + } + return oci.NewArtifact("dummy", opt, oci.WithImage(img)) +} + +func ArchiveDir(t *testing.T, dir string) string { + tmpDBPath := filepath.Join(t.TempDir(), "db.tar") + f, err := os.Create(tmpDBPath) + require.NoError(t, err) + defer f.Close() + + tr := tar.NewWriter(f) + defer tr.Close() + + err = tr.AddFS(os.DirFS(dir)) + require.NoError(t, err) + + return tmpDBPath +} diff --git a/internal/gittest/server.go b/internal/gittest/server.go new file mode 100644 index 000000000000..2d02c4a220fb --- /dev/null +++ b/internal/gittest/server.go @@ -0,0 +1,200 @@ +//go:build unix + +package gittest + +import ( + "errors" + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/config" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/sosedoff/gitkit" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" +) + +var signature = &object.Signature{ + Name: "Test", + Email: "test@example.com", + When: time.Now(), +} + +// Options contains configuration options for git server authentication +type Options struct { + Username string + Password string +} + +// setupGitServer creates and starts a git server with the given bare repository directory +func setupGitServer(t *testing.T, bareDir string, opts Options) *httptest.Server { + hasAuth := opts.Username != "" && opts.Password != "" + service := gitkit.New(gitkit.Config{ + Dir: bareDir, + Auth: hasAuth, + }) + if hasAuth { + service.AuthFunc = func(cred gitkit.Credential, _ *gitkit.Request) (bool, error) { + return cred.Username == opts.Username && cred.Password == opts.Password, nil + } + } + err := service.Setup() + require.NoError(t, err) + + return httptest.NewServer(service) +} + +func NewServer(t *testing.T, repo, dir string, opts Options) *httptest.Server { + wtDir := t.TempDir() + + // git init + r, err := git.PlainInit(wtDir, false) + require.NoError(t, err) + + wt, err := r.Worktree() + require.NoError(t, err) + + testutil.CopyDir(t, dir, wtDir) + + _, err = wt.Add(".") + require.NoError(t, err) + + _, err = wt.Commit("initial commit", &git.CommitOptions{ + Author: signature, + }) + require.NoError(t, err) + + // Create a bare repository + bareDir := t.TempDir() + gitDir := filepath.Join(bareDir, repo+".git") + _, err = git.PlainClone(gitDir, true, &git.CloneOptions{URL: wtDir}) + require.NoError(t, err) + + return setupGitServer(t, bareDir, opts) +} + +func NewServerWithRepository(t *testing.T, repo, dir string, opts Options) *httptest.Server { + // Create a bare repository + bareDir := t.TempDir() + gitDir := filepath.Join(bareDir, repo+".git") + + // Clone the existing repository as a bare repository + r, err := git.PlainClone(gitDir, true, &git.CloneOptions{ + URL: dir, + Tags: git.AllTags, + }) + require.NoError(t, err) + + // Fetch all remote branches and create local branches + err = r.Fetch(&git.FetchOptions{ + RefSpecs: []config.RefSpec{ + "+refs/remotes/origin/*:refs/heads/*", + }, + Tags: git.AllTags, + }) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { + require.NoError(t, err) + } + + return setupGitServer(t, bareDir, opts) +} + +// NewTestServer creates a git server with the local copy of "github.com/aquasecurity/trivy-test-repo". +// If the test repository doesn't exist, it suggests running 'mage test:unit'. +func NewTestServer(t *testing.T, opts Options) *httptest.Server { + _, filePath, _, _ := runtime.Caller(0) + dir := filepath.Join(filepath.Dir(filePath), "testdata", "test-repo") + + if _, err := os.Stat(dir); os.IsNotExist(err) { + require.Fail(t, "test-repo not found. Please run 'mage test:unit' to set up the test fixtures") + } + + return NewServerWithRepository(t, "test-repo", dir, opts) +} + +func Clone(t *testing.T, ts *httptest.Server, repo, worktree string, opts Options) *git.Repository { + cloneOptions := git.CloneOptions{ + URL: ts.URL + "/" + repo + ".git", + } + + if opts.Username != "" && opts.Password != "" { + cloneOptions.Auth = &http.BasicAuth{ + Username: opts.Username, + Password: opts.Password, + } + } + + r, err := git.PlainClone(worktree, false, &cloneOptions) + require.NoError(t, err) + + return r +} + +func CommitAll(t *testing.T, r *git.Repository, msg string) { + w, err := r.Worktree() + require.NoError(t, err) + + _, err = w.Add(".") + require.NoError(t, err) + + _, err = w.Commit(msg, &git.CommitOptions{ + Author: signature, + }) + require.NoError(t, err) +} + +func SetTag(t *testing.T, r *git.Repository, tag string) { + h, err := r.Head() + require.NoError(t, err) + + t.Logf("git tag -a %s %s -m \"%s\"", tag, h.Hash(), tag) + _, err = r.CreateTag(tag, h.Hash(), &git.CreateTagOptions{ + Tagger: signature, + Message: tag, + }) + require.NoError(t, err) +} + +func PushTags(t *testing.T, r *git.Repository) { + t.Log("git push --tags") + err := r.Push(&git.PushOptions{ + RemoteName: "origin", + RefSpecs: []config.RefSpec{"refs/tags/*:refs/tags/*"}, + }) + + if err != nil { + if errors.Is(err, git.NoErrAlreadyUpToDate) { + return + } + require.NoError(t, err) + } +} + +func CreateRemoteBranch(t *testing.T, r *git.Repository, branchName string) { + wt, err := r.Worktree() + require.NoError(t, err) + + ref := plumbing.NewBranchReferenceName(branchName) + err = wt.Checkout(&git.CheckoutOptions{ + Branch: ref, + Create: true, + }) + require.NoError(t, err) + defer func() { + require.NoError(t, wt.Checkout(&git.CheckoutOptions{})) + }() + + err = r.Push(&git.PushOptions{ + RemoteName: "origin", + RefSpecs: []config.RefSpec{config.RefSpec(ref + ":" + ref)}, + }) + require.NoError(t, err) +} diff --git a/internal/gittest/testdata/fixture.go b/internal/gittest/testdata/fixture.go new file mode 100644 index 000000000000..75b0d75b6280 --- /dev/null +++ b/internal/gittest/testdata/fixture.go @@ -0,0 +1,43 @@ +package gittest + +import ( + "log/slog" + "path/filepath" + "runtime" + + "github.com/go-git/go-git/v5" + "github.com/magefile/mage/target" + "golang.org/x/xerrors" +) + +const ( + repoURL = "https://github.com/aquasecurity/trivy-test-repo/" + repoDir = "test-repo" // subdirectory for the cloned repository +) + +// Fixtures clones a Git repository for unit tests +func Fixtures() error { + _, filePath, _, _ := runtime.Caller(0) + dir := filepath.Dir(filePath) + cloneDir := filepath.Join(dir, repoDir) + + // Check if the directory already exists and is up to date + if updated, err := target.Path(cloneDir, filePath); err != nil { + return err + } else if !updated { + return nil + } + + slog.Info("Cloning...", slog.String("url", repoURL)) + + // Clone the repository with all branches and tags + _, err := git.PlainClone(cloneDir, false, &git.CloneOptions{ + URL: repoURL, + Tags: git.AllTags, + }) + if err != nil { + return xerrors.Errorf("error cloning repository: %w", err) + } + + return nil +} diff --git a/internal/hooktest/hook.go b/internal/hooktest/hook.go new file mode 100644 index 000000000000..c4b41fec6459 --- /dev/null +++ b/internal/hooktest/hook.go @@ -0,0 +1,96 @@ +package hooktest + +import ( + "context" + "errors" + "testing" + + "github.com/aquasecurity/trivy/pkg/extension" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/types" +) + +type testHook struct{} + +func (*testHook) Name() string { + return "test" +} + +func (*testHook) Version() int { + return 1 +} + +// RunHook implementation +func (*testHook) PreRun(_ context.Context, opts flag.Options) error { + if opts.GlobalOptions.ConfigFile == "bad-config" { + return errors.New("bad pre-run") + } + return nil +} + +func (*testHook) PostRun(_ context.Context, opts flag.Options) error { + if opts.GlobalOptions.ConfigFile == "bad-config" { + return errors.New("bad post-run") + } + return nil +} + +// ScanHook implementation +func (*testHook) PreScan(_ context.Context, target *types.ScanTarget, _ types.ScanOptions) error { + if target.Name == "bad-pre" { + return errors.New("bad pre-scan") + } + target.Name += " (pre-scan)" + return nil +} + +func (*testHook) PostScan(_ context.Context, results types.Results) (types.Results, error) { + for i, r := range results { + if r.Target == "bad" { + return nil, errors.New("bad") + } + for j := range r.Vulnerabilities { + results[i].Vulnerabilities[j].References = []string{ + "https://example.com/post-scan", + } + } + } + return results, nil +} + +// ReportHook implementation +func (*testHook) PreReport(_ context.Context, report *types.Report, _ flag.Options) error { + if report.ArtifactName == "bad-report" { + return errors.New("bad pre-report") + } + + // Modify the report + for i := range report.Results { + for j := range report.Results[i].Vulnerabilities { + report.Results[i].Vulnerabilities[j].Title = "Modified by pre-report hook" + } + } + return nil +} + +func (*testHook) PostReport(_ context.Context, report *types.Report, _ flag.Options) error { + if report.ArtifactName == "bad-report" { + return errors.New("bad post-report") + } + + // Modify the report + for i := range report.Results { + for j := range report.Results[i].Vulnerabilities { + report.Results[i].Vulnerabilities[j].Description = "Modified by post-report hook" + } + } + return nil +} + +func Init(t *testing.T) { + h := &testHook{} + extension.RegisterHook(h) + t.Cleanup(func() { + extension.DeregisterHook(h.Name()) + }) +} diff --git a/internal/registrytest/registrytest.go b/internal/registrytest/registrytest.go new file mode 100644 index 000000000000..1810248d372d --- /dev/null +++ b/internal/registrytest/registrytest.go @@ -0,0 +1,81 @@ +// Package registrytest provides utilities for testing with OCI registries. +package registrytest + +import ( + "fmt" + "net/http/httptest" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/stretchr/testify/require" +) + +// NewServer starts a test registry server with OCI 1.1 referrers support. +func NewServer(t *testing.T) *httptest.Server { + t.Helper() + return httptest.NewServer(registry.New(registry.WithReferrersSupport(true))) +} + +// PushRandomImage pushes a random image to the registry and returns its reference and descriptor. +func PushRandomImage(t *testing.T, registryHost, repo, tag string) (name.Reference, v1.Descriptor) { + t.Helper() + + ref, err := name.ParseReference(fmt.Sprintf("%s/%s:%s", registryHost, repo, tag)) + require.NoError(t, err) + + img, err := random.Image(10, 1) + require.NoError(t, err) + + err = remote.Write(ref, img) + require.NoError(t, err) + + d, err := img.Digest() + require.NoError(t, err) + sz, err := img.Size() + require.NoError(t, err) + mt, err := img.MediaType() + require.NoError(t, err) + + return ref, v1.Descriptor{ + Digest: d, + Size: sz, + MediaType: mt, + } +} + +// PushReferrer pushes an artifact referrer to the registry attached to the subject image. +// The artifactType is used both as the config media type (for OCI 1.1 artifact type) and layer media type. +func PushReferrer(t *testing.T, registryHost, repo string, subjectDesc v1.Descriptor, artifactType string, content []byte) { + t.Helper() + + // Create an OCI artifact with the content as a layer + layer := static.NewLayer(content, types.MediaType(artifactType)) + + // Start with an empty image and add the layer + img := mutate.MediaType(empty.Image, types.OCIManifestSchema1) + // Set the config media type to the artifact type - this is how OCI 1.1 identifies the artifact type + // The registry will use this as the artifactType in the referrers API response + img = mutate.ConfigMediaType(img, types.MediaType(artifactType)) + img, err := mutate.AppendLayers(img, layer) + require.NoError(t, err) + + // Set the subject to create the referrer relationship + img = mutate.Subject(img, subjectDesc).(v1.Image) + + // Push the referrer + d, err := img.Digest() + require.NoError(t, err) + ref, err := name.ParseReference(fmt.Sprintf("%s/%s@%s", registryHost, repo, d.String())) + require.NoError(t, err) + + err = remote.Write(ref, img) + require.NoError(t, err) +} diff --git a/internal/testutil/docker.go b/internal/testutil/docker.go new file mode 100644 index 000000000000..f7d3c8b5d997 --- /dev/null +++ b/internal/testutil/docker.go @@ -0,0 +1,89 @@ +package testutil + +import ( + "context" + "encoding/json" + "io" + "os" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/moby/moby/client" + "github.com/stretchr/testify/require" + + gzutil "github.com/aquasecurity/trivy/pkg/fanal/utils/gzip" +) + +type DockerClient struct { + *client.Client +} + +func NewDockerClient(t *testing.T) *DockerClient { + cli, err := client.New(client.FromEnv) + require.NoError(t, err) + return &DockerClient{Client: cli} +} + +// ImageLoad loads a Docker image from a tar archive file into the Docker engine. +// It automatically registers cleanup via t.Cleanup() to remove the loaded image after the test. +func (c *DockerClient) ImageLoad(t *testing.T, ctx context.Context, imageFile string) string { + t.Helper() + testfile, err := os.Open(imageFile) + require.NoError(t, err) + defer testfile.Close() + + // Load image into docker engine + res, err := c.Client.ImageLoad(ctx, testfile, client.ImageLoadWithQuiet(true)) + require.NoError(t, err) + defer res.Close() + + // Parse the response and extract the loaded image name + var data struct { + Stream string `json:"stream"` + } + err = json.NewDecoder(res).Decode(&data) + require.NoError(t, err) + loadedImage := strings.TrimPrefix(data.Stream, "Loaded image: ") + loadedImage = strings.TrimSpace(loadedImage) + require.NotEmpty(t, loadedImage, data.Stream) + + // Register cleanup to remove the loaded image after the test + t.Cleanup(func() { c.ImageRemove(t, ctx, loadedImage) }) + + return loadedImage +} + +func (c *DockerClient) ImageRemove(t *testing.T, ctx context.Context, imageID string) { + t.Helper() + _, _ = c.Client.ImageRemove(ctx, imageID, client.ImageRemoveOptions{ + Force: true, + PruneChildren: true, + }) +} + +// ImageCleanLoad performs a clean load of a Docker image from a tar archive. +// It removes any existing images with conflicting RepoTags before loading, +// ensuring the loaded image has the correct RepoTags from the archive. +// It automatically registers cleanup via t.Cleanup() to remove the loaded image after the test. +func (c *DockerClient) ImageCleanLoad(t *testing.T, ctx context.Context, archivePath string) string { + t.Helper() + + // Extract RepoTags from archive + opener := func() (io.ReadCloser, error) { + return gzutil.OpenFile(archivePath) + } + + manifest, err := tarball.LoadManifest(opener) + require.NoError(t, err, "failed to load manifest from archive") + + // Remove existing images with the same RepoTags to avoid conflicts + for _, m := range manifest { + for _, tag := range m.RepoTags { + c.ImageRemove(t, ctx, tag) + } + } + + // Load image + return c.ImageLoad(t, ctx, archivePath) +} diff --git a/internal/testutil/error.go b/internal/testutil/error.go new file mode 100644 index 000000000000..3a24df91c102 --- /dev/null +++ b/internal/testutil/error.go @@ -0,0 +1,11 @@ +package testutil + +import ( + "runtime" + + "github.com/samber/lo" +) + +var ErrNotExist string = lo.Ternary(runtime.GOOS == "windows", + "The system cannot find the file specified.", + "no such file or directory") diff --git a/internal/testutil/fs.go b/internal/testutil/fs.go new file mode 100644 index 000000000000..1ed303933e7b --- /dev/null +++ b/internal/testutil/fs.go @@ -0,0 +1,92 @@ +package testutil + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func CopyFile(t *testing.T, src, dst string) { + MustMkdirAll(t, filepath.Dir(dst)) + + _, err := fsutils.CopyFile(src, dst) + require.NoError(t, err) +} + +// CopyDir copies the directory content from src to dst. +// It supports only simple cases for testing. +func CopyDir(t *testing.T, src, dst string) { + srcInfo, err := os.Stat(src) + require.NoError(t, err) + + err = os.MkdirAll(dst, srcInfo.Mode()) + require.NoError(t, err) + + entries, err := os.ReadDir(src) + require.NoError(t, err) + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + CopyDir(t, srcPath, dstPath) + } else { + _, err = fsutils.CopyFile(srcPath, dstPath) + require.NoError(t, err) + } + } +} + +func MustWriteYAML(t *testing.T, path string, data any) { + t.Helper() + MustMkdirAll(t, filepath.Dir(path)) + + f, err := os.Create(path) + require.NoError(t, err) + defer f.Close() + + require.NoError(t, yaml.NewEncoder(f).Encode(data)) +} + +func MustReadYAML(t *testing.T, path string, out any) { + t.Helper() + f, err := os.Open(path) + require.NoError(t, err) + defer f.Close() + + require.NoError(t, yaml.NewDecoder(f).Decode(out)) +} + +func MustMkdirAll(t *testing.T, dir string) { + err := os.MkdirAll(dir, 0o750) + require.NoError(t, err) +} + +func MustReadJSON(t *testing.T, filePath string, v any) { + b, err := os.ReadFile(filePath) + require.NoError(t, err) + err = json.Unmarshal(b, v) + require.NoError(t, err) +} + +func MustWriteJSON(t *testing.T, filePath string, v any) { + data, err := json.Marshal(v) + require.NoError(t, err) + + MustWriteFile(t, filePath, data) +} + +func MustWriteFile(t *testing.T, filePath string, content []byte) { + dir := filepath.Dir(filePath) + MustMkdirAll(t, dir) + + err := os.WriteFile(filePath, content, 0o600) + require.NoError(t, err) +} diff --git a/internal/testutil/gzip.go b/internal/testutil/gzip.go new file mode 100644 index 000000000000..68e97395cb3a --- /dev/null +++ b/internal/testutil/gzip.go @@ -0,0 +1,77 @@ +package testutil + +import ( + "bytes" + "compress/gzip" + "io" + "os" + "testing" + + "github.com/stretchr/testify/require" +) + +const ( + maxSize = int64(10) << 30 // 10GB + blockSize = 4096 +) + +func DecompressGzip(t *testing.T, src, dst string) { + w, err := os.Create(dst) + require.NoError(t, err) + defer w.Close() + + f, err := os.Open(src) + require.NoError(t, err) + defer f.Close() + + gr, err := gzip.NewReader(f) + require.NoError(t, err) + + _, err = io.CopyN(w, gr, maxSize) + require.ErrorIs(t, err, io.EOF) +} + +// DecompressSparseGzip decompresses a sparse gzip file for virtual machine image. +func DecompressSparseGzip(t *testing.T, src, dst string) { + w, err := os.Create(dst) + require.NoError(t, err) + defer w.Close() + + f, err := os.Open(src) + require.NoError(t, err) + defer f.Close() + + gr, err := gzip.NewReader(f) + require.NoError(t, err) + + buf := make([]byte, blockSize) + var size int + var written int64 + for { + n, err := gr.Read(buf) + if n == 0 && err != nil { + if err == io.EOF { + break + } + require.NoError(t, err) + } + + size += n + err = w.Truncate(int64(size)) + require.NoError(t, err) + + if !bytes.Equal(buf[:n], make([]byte, n)) { + wn, err := w.WriteAt(buf[:n], int64(size-n)) + if err != nil { + if err == io.EOF { + break + } + require.NoError(t, err) + } + written += int64(wn) + if written > maxSize { + require.Fail(t, "written size exceeds max") + } + } + } +} diff --git a/internal/testutil/image.go b/internal/testutil/image.go new file mode 100644 index 000000000000..a9a233729df7 --- /dev/null +++ b/internal/testutil/image.go @@ -0,0 +1,80 @@ +package testutil + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/image/name" +) + +var ( + testImages string + testVMImages string +) + +func init() { + _, b, _, _ := runtime.Caller(0) + currentDir := filepath.Dir(b) + f, err := os.Open(filepath.Join(currentDir, "..", "..", "integration", "testimages.ini")) + if err != nil { + panic(err) + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "#") { + continue + } + parts := strings.SplitN(scanner.Text(), "=", 2) + if len(parts) == 2 { + key := strings.TrimSpace(parts[0]) + value := strings.TrimSpace(parts[1]) + switch key { + case "TEST_IMAGES": + testImages = value + case "TEST_VM_IMAGES": + testVMImages = value + } + } + } + if err = scanner.Err(); err != nil { + panic(err) + } +} + +func ImageName(subpath, tag, digest string) string { + return imageName(testImages, subpath, tag, digest) +} + +func VMImageName(subpath, tag, digest string) string { + return imageName(testVMImages, subpath, tag, digest) +} + +func imageName(img, subpath, tag, digest string) string { + if subpath != "" { + img = fmt.Sprintf("%s/%s", img, subpath) + } + if tag != "" { + img = fmt.Sprintf("%s:%s", img, tag) + } + if digest != "" { + img = fmt.Sprintf("%s@%s", img, digest) + } + return img +} + +// MustParseReference parses a string into a Reference and fails the test if there's an error +func MustParseReference(t *testing.T, s string) name.Reference { + t.Helper() + ref, err := name.ParseReference(s) + require.NoError(t, err) + return ref +} diff --git a/internal/testutil/localstack.go b/internal/testutil/localstack.go new file mode 100644 index 000000000000..e5fa0469992a --- /dev/null +++ b/internal/testutil/localstack.go @@ -0,0 +1,48 @@ +package testutil + +import ( + "context" + "fmt" + "os" + + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/localstack" +) + +func SetupLocalStack(ctx context.Context, version string) (*localstack.LocalStackContainer, string, error) { + + if err := os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true"); err != nil { + return nil, "", err + } + + container, err := localstack.RunContainer(ctx, testcontainers.CustomizeRequest( + testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + Image: "localstack/localstack:" + version, + AutoRemove: true, + }, + }, + )) + if err != nil { + return nil, "", err + } + + p, err := container.MappedPort(ctx, "4566/tcp") + if err != nil { + return nil, "", err + } + + provider, err := testcontainers.NewDockerProvider() + if err != nil { + return nil, "", err + } + defer provider.Close() + + host, err := provider.DaemonHost(ctx) + if err != nil { + return nil, "", err + } + + return container, fmt.Sprintf("http://%s:%d", host, p.Int()), nil + +} diff --git a/internal/testutil/txtar.go b/internal/testutil/txtar.go new file mode 100644 index 000000000000..f8ab3945f762 --- /dev/null +++ b/internal/testutil/txtar.go @@ -0,0 +1,19 @@ +package testutil + +import ( + "io/fs" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/tools/txtar" +) + +// TxtarToFS reads a txtar file and returns it as an fs.FS. +func TxtarToFS(t *testing.T, path string) fs.FS { + t.Helper() + archive, err := txtar.ParseFile(path) + require.NoError(t, err) + fsys, err := txtar.FS(archive) + require.NoError(t, err) + return fsys +} diff --git a/internal/testutil/util.go b/internal/testutil/util.go new file mode 100644 index 000000000000..86741a2f59be --- /dev/null +++ b/internal/testutil/util.go @@ -0,0 +1,118 @@ +package testutil + +import ( + "encoding/json" + "io/fs" + "strings" + "testing" + "testing/fstest" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +func AssertRuleFound(t *testing.T, ruleID string, results scan.Results, message string, args ...any) { + found := ruleIDInResults(ruleID, results.GetFailed()) + assert.True(t, found, append([]any{message}, args...)...) + for _, result := range results.GetFailed() { + if result.Rule().CanonicalID() == ruleID { + m := result.Metadata() + meta := &m + for meta != nil { + assert.NotNil(t, meta.Range(), 0) + assert.Positive(t, meta.Range().GetStartLine()) + assert.Positive(t, meta.Range().GetEndLine()) + meta = meta.Parent() + } + } + } +} + +func AssertRuleNotFound(t *testing.T, ruleID string, results scan.Results, message string, args ...any) { + found := ruleIDInResults(ruleID, results.GetFailed()) + assert.False(t, found, append([]any{message}, args...)...) +} + +func AssertRuleNotFailed(t *testing.T, ruleID string, results scan.Results, message string, args ...any) { + failedExists := ruleIDInResults(ruleID, results.GetFailed()) + assert.False(t, failedExists, append([]any{message}, args...)...) + passedResults := lo.Filter(results, func(res scan.Result, _ int) bool { + return res.Status() == scan.StatusPassed || res.Status() == scan.StatusIgnored + }) + passedExists := ruleIDInResults(ruleID, passedResults) + assert.True(t, passedExists, append([]any{message}, args...)...) +} + +func ruleIDInResults(ruleID string, results scan.Results) bool { + for _, res := range results { + if res.Rule().CanonicalID() == ruleID { + return true + } + } + return false +} + +func CreateFS(files map[string]string) fs.FS { + return fstest.MapFS(lo.MapEntries(files, func(k, v string) (string, *fstest.MapFile) { + return strings.TrimPrefix(k, "/"), &fstest.MapFile{Data: []byte(v)} + })) +} + +func AssertDefsecEqual(t *testing.T, expected, actual any) { + expectedJson, err := json.MarshalIndent(expected, "", "\t") + require.NoError(t, err) + actualJson, err := json.MarshalIndent(actual, "", "\t") + require.NoError(t, err) + + if expectedJson[0] == '[' { + var expectedSlice []map[string]any + require.NoError(t, json.Unmarshal(expectedJson, &expectedSlice)) + var actualSlice []map[string]any + require.NoError(t, json.Unmarshal(actualJson, &actualSlice)) + expectedSlice = purgeMetadataSlice(expectedSlice) + actualSlice = purgeMetadataSlice(actualSlice) + assert.Equal(t, expectedSlice, actualSlice, "defsec adapted and expected values do not match") + } else { + var expectedMap map[string]any + require.NoError(t, json.Unmarshal(expectedJson, &expectedMap)) + var actualMap map[string]any + require.NoError(t, json.Unmarshal(actualJson, &actualMap)) + expectedMap = purgeMetadata(expectedMap) + actualMap = purgeMetadata(actualMap) + assert.Equal(t, expectedMap, actualMap, "defsec adapted and expected values do not match") + } +} + +func purgeMetadata(input map[string]any) map[string]any { + for k, v := range input { + if k == "metadata" || k == "Metadata" { + delete(input, k) + continue + } + if v, ok := v.(map[string]any); ok { + input[k] = purgeMetadata(v) + } + if v, ok := v.([]any); ok { + if len(v) > 0 { + if _, ok := v[0].(map[string]any); ok { + maps := make([]map[string]any, len(v)) + for i := range v { + maps[i] = v[i].(map[string]any) + } + input[k] = purgeMetadataSlice(maps) + } + } + } + } + return input +} + +func purgeMetadataSlice(input []map[string]any) []map[string]any { + for i := range input { + input[i] = purgeMetadata(input[i]) + } + return input +} diff --git a/magefiles/config_schema.go b/magefiles/config_schema.go new file mode 100644 index 000000000000..2fd53b4400dd --- /dev/null +++ b/magefiles/config_schema.go @@ -0,0 +1,154 @@ +//go:build mage_docs + +package main + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/invopop/jsonschema" + + "github.com/aquasecurity/trivy/pkg/flag" +) + +// JSON Schema type constants +const ( + schemaTypeString = "string" + schemaTypeBoolean = "boolean" + schemaTypeInteger = "integer" + schemaTypeNumber = "number" + schemaTypeArray = "array" + schemaTypeObject = "object" +) + +const configSchemaPath = "schema/trivy-config.json" + +// generateConfigSchema generates a JSON schema for trivy.yaml configuration file. +func generateConfigSchema(outputPath string, allFlagGroups []flag.FlagGroup) error { + root := &jsonschema.Schema{ + Version: jsonschema.Version, + Type: schemaTypeObject, + Title: "Trivy Configuration", + Description: "Configuration file for Trivy security scanner (trivy.yaml)", + Properties: jsonschema.NewProperties(), + } + + for _, group := range allFlagGroups { + for _, f := range group.Flags() { + configName := f.GetConfigName() + if configName == "" || f.Hidden() { + continue + } + if err := addFlagToSchema(root, f); err != nil { + return err + } + } + } + + data, err := json.MarshalIndent(root, "", " ") + if err != nil { + return err + } + + // Ensure directory exists + if err := os.MkdirAll("schema", 0755); err != nil { + return err + } + + return os.WriteFile(outputPath, data, 0644) +} + +// addFlagToSchema adds a flag to the schema, creating nested objects as needed. +func addFlagToSchema(root *jsonschema.Schema, f flag.Flagger) error { + configName := f.GetConfigName() + parts := strings.Split(configName, ".") + + // Split into parent path and leaf name + parentParts, leafName := parts[:len(parts)-1], parts[len(parts)-1] + + // Navigate/create intermediate objects + current := root + for _, part := range parentParts { + if existing, ok := current.Properties.Get(part); ok { + current = existing + } else { + newSchema := &jsonschema.Schema{ + Type: schemaTypeObject, + Properties: jsonschema.NewProperties(), + } + current.Properties.Set(part, newSchema) + current = newSchema + } + } + + // Add the leaf property + schema, err := schemaFromFlag(f) + if err != nil { + return err + } + current.Properties.Set(leafName, schema) + return nil +} + +// schemaFromFlag creates a JSON schema based on the flag's type, description, and allowed values. +func schemaFromFlag(f flag.Flagger) (*jsonschema.Schema, error) { + schema, err := schemaFromFlagValue(f.GetDefaultValue()) + if err != nil { + return nil, fmt.Errorf("flag %q: %w", f.GetConfigName(), err) + } + + // Add description from Usage + if usage := f.GetUsage(); usage != "" { + schema.Description = usage + } + + // Add enum if Values is set + if values := f.GetValues(); len(values) > 0 { + enumValues := make([]any, len(values)) + for i, v := range values { + enumValues[i] = v + } + // For array types, enum should be in items, not at the array level + if schema.Type == schemaTypeArray && schema.Items != nil { + schema.Items.Enum = enumValues + } else { + schema.Enum = enumValues + } + } + + return schema, nil +} + +// schemaFromFlagValue creates a JSON schema based on the flag's default value type. +func schemaFromFlagValue(val any) (*jsonschema.Schema, error) { + switch val.(type) { + case string: + return &jsonschema.Schema{Type: schemaTypeString}, nil + case bool: + return &jsonschema.Schema{Type: schemaTypeBoolean}, nil + case int: + return &jsonschema.Schema{Type: schemaTypeInteger}, nil + case float64: + return &jsonschema.Schema{Type: schemaTypeNumber}, nil + case []string: + return &jsonschema.Schema{ + Type: schemaTypeArray, + Items: &jsonschema.Schema{Type: schemaTypeString}, + }, nil + case time.Duration: + return &jsonschema.Schema{Type: schemaTypeString}, nil + case map[string][]string: + return &jsonschema.Schema{ + Type: schemaTypeObject, + AdditionalProperties: &jsonschema.Schema{ + Type: schemaTypeArray, + Items: &jsonschema.Schema{Type: schemaTypeString}, + }, + }, nil + default: + return nil, fmt.Errorf("unknown type %T, please update schemaFromFlagValue()", val) + } +} diff --git a/magefiles/docs.go b/magefiles/docs.go new file mode 100644 index 000000000000..791ce2ace354 --- /dev/null +++ b/magefiles/docs.go @@ -0,0 +1,200 @@ +//go:build mage_docs + +package main + +import ( + "cmp" + "fmt" + "os" + "slices" + "sort" + "strings" + + "github.com/aquasecurity/trivy/pkg/commands" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/spf13/cobra/doc" +) + +const ( + title = "Config file" + description = "Trivy can be customized by tweaking a `trivy.yaml` file.\n" + + "The config path can be overridden by the `--config` flag.\n\n" + + "An example is [here][example] and a [JSON Schema][schema] is also available.\n\n" + + "These samples contain default values for flags." + footer = "[example]: https://github.com/aquasecurity/trivy/blob/{{ git.tag }}/examples/trivy-conf/trivy.yaml\n" + + "[schema]: https://github.com/aquasecurity/trivy/blob/{{ git.tag }}/schema/trivy-config.json" +) + +// Generate CLI references +func main() { + // Set a dummy path for the documents + flag.CacheDirFlag.Default = "/path/to/cache" + flag.ModuleDirFlag.Default = "$HOME/.trivy/modules" + + // Set a dummy path not to load plugins + os.Setenv("XDG_DATA_HOME", os.TempDir()) + + cmd := commands.NewApp() + allFlagGroups := getAllFlags() + + cmd.DisableAutoGenTag = true + if err := doc.GenMarkdownTree(cmd, "./docs/guide/references/configuration/cli"); err != nil { + log.Fatal("Fatal error", log.Err(err)) + } + if err := generateConfigDocs("./docs/guide/references/configuration/config-file.md", allFlagGroups); err != nil { + log.Fatal("Fatal error in config file generation", log.Err(err)) + } + if err := generateTelemetryFlagDocs("./docs/guide/advanced/telemetry-flags.md", allFlagGroups); err != nil { + log.Fatal("Fatal error in telemetry docs generation", log.Err(err)) + } + if err := generateConfigSchema(configSchemaPath, allFlagGroups); err != nil { + log.Fatal("Fatal error in config schema generation", log.Err(err)) + } +} + +// generateTelemetryFlagDocs updates the telemetry section in the documentation file +// with the flags that are safe to be included in telemetry. +func generateTelemetryFlagDocs(filename string, allFlagGroups []flag.FlagGroup) error { + var telemetryFlags []string + for _, group := range allFlagGroups { + flags := group.Flags() + for _, f := range flags { + if f.IsTelemetrySafe() && f.GetConfigName() != "" { + telemetryFlags = append(telemetryFlags, fmt.Sprintf("--%s", f.GetName())) + } + } + } + + sort.Strings(telemetryFlags) + flagContent := fmt.Sprintf("```\n%s\n```\n", strings.Join(telemetryFlags, "\n")) + if err := os.WriteFile(filename, []byte(flagContent), 0644); err != nil { + return fmt.Errorf("failed to write to %s: %w", filename, err) + } + return nil +} + +// generateConfigDocs creates markdown file for Trivy config. +func generateConfigDocs(filename string, allFlagGroups []flag.FlagGroup) error { + f, err := os.Create(filename) + if err != nil { + return err + } + defer f.Close() + f.WriteString("# " + title + "\n\n") + f.WriteString(description + "\n") + + if len(allFlagGroups) == 0 { + return fmt.Errorf("no flag groups found") + } + + for _, group := range allFlagGroups { + f.WriteString("## " + group.Name() + " options\n") + writeFlags(group, f) + } + + f.WriteString(footer) + return nil +} + +func writeFlags(group flag.FlagGroup, w *os.File) { + flags := group.Flags() + // Sort flags to avoid duplicates of non-last parts of config file + slices.SortFunc(flags, func(a, b flag.Flagger) int { + return cmp.Compare(a.GetConfigName(), b.GetConfigName()) + }) + w.WriteString("\n```yaml\n") + + var lastParts []string + for _, flg := range flags { + if flg.GetConfigName() == "" || flg.Hidden() { + continue + } + // We need to split the config name on `.` to make the indentations needed in yaml. + parts := strings.Split(flg.GetConfigName(), ".") + for i := range parts { + // Skip already added part + if len(lastParts) >= i+1 && parts[i] == lastParts[i] { + continue + } + ind := strings.Repeat(" ", i) + // We need to add a comment and example values only for the last part of the config name. + isLastPart := i == len(parts)-1 + if isLastPart { + // Some `Flags` don't support flag for CLI. (e.g.`LicenseForbidden`). + if flg.GetName() != "" { + fmt.Fprintf(w, "%s# Same as '--%s'\n", ind, flg.GetName()) + } + } + w.WriteString(ind + parts[i] + ":") + if isLastPart { + writeFlagValue(flg.GetDefaultValue(), ind, w) + } + w.WriteString("\n") + } + lastParts = parts + } + w.WriteString("```\n") +} + +func writeFlagValue(val any, ind string, w *os.File) { + switch v := val.(type) { + case []string: + if len(v) > 0 { + w.WriteString("\n") + for _, vv := range v { + fmt.Fprintf(w, "%s - %s\n", ind, vv) + } + } else { + w.WriteString(" []\n") + } + case map[string][]string: + w.WriteString("\n") + for k, vv := range v { + fmt.Fprintf(w, "%s %s:\n", ind, k) + for _, vvv := range vv { + fmt.Fprintf(w, " %s - %s\n", ind, vvv) + } + } + case string: + fmt.Fprintf(w, " %q\n", v) + default: + fmt.Fprintf(w, " %v\n", v) + } +} + +func getAllFlags() []flag.FlagGroup { + // remoteFlags should contain Client and Server flags. + // NewClientFlags doesn't initialize `Listen` field + remoteFlags := flag.NewClientFlags() + remoteFlags.Listen = flag.ServerListenFlag.Clone() + + // These flags don't work from config file. + // Clear configName to skip them later. + globalFlags := flag.NewGlobalFlagGroup() + globalFlags.ConfigFile.ConfigName = "" + globalFlags.ShowVersion.ConfigName = "" + globalFlags.GenerateDefaultConfig.ConfigName = "" + + return []flag.FlagGroup{ + globalFlags, + flag.NewCacheFlagGroup(), + flag.NewCleanFlagGroup(), + remoteFlags, + flag.NewDBFlagGroup(), + flag.NewImageFlagGroup(), + flag.NewK8sFlagGroup(), + flag.NewLicenseFlagGroup(), + flag.NewMisconfFlagGroup(), + flag.NewModuleFlagGroup(), + flag.NewPackageFlagGroup(), + flag.NewRegistryFlagGroup(), + flag.NewRegoFlagGroup(), + flag.NewReportFlagGroup(), + flag.NewRepoFlagGroup(), + flag.NewScanFlagGroup(), + flag.NewSecretFlagGroup(), + flag.NewVulnerabilityFlagGroup(), + } + +} diff --git a/magefiles/fixture.go b/magefiles/fixture.go new file mode 100644 index 000000000000..b466c85977f5 --- /dev/null +++ b/magefiles/fixture.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/authn/github" + "github.com/google/go-containerregistry/pkg/crane" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/magefile/mage/sh" + + "github.com/aquasecurity/trivy/internal/testutil" +) + +const dir = "integration/testdata/fixtures/images/" + +var auth = crane.WithAuthFromKeychain(authn.NewMultiKeychain(authn.DefaultKeychain, github.Keychain)) + +func fixtureContainerImages() error { + var testImages = testutil.ImageName("", "", "") + + if err := os.MkdirAll(dir, 0o750); err != nil { + return err + } + tags, err := crane.ListTags(testImages, auth) + if err != nil { + return err + } + // Save all tags for trivy-test-images + for _, tag := range tags { + if err := saveImage("", tag); err != nil { + return err + } + } + + // Save trivy-test-images/containerd image + return saveImage("containerd", "latest") +} + +func saveImage(subpath, tag string) error { + fileName := tag + ".tar.gz" + imgName := testutil.ImageName("", tag, "") + if subpath != "" { + fileName = subpath + ".tar.gz" + imgName = testutil.ImageName(subpath, "", "") + } + filePath := filepath.Join(dir, fileName) + if exists(filePath) { + return nil + } + fmt.Printf("Downloading %s...\n", imgName) + + img, err := crane.Pull(imgName, auth) + if err != nil { + return err + } + tarPath := strings.TrimSuffix(filePath, ".gz") + if err = crane.Save(img, imgName, tarPath); err != nil { + return err + } + return sh.Run("gzip", tarPath) +} + +func fixtureVMImages() error { + var testVMImages = testutil.VMImageName("", "", "") + const ( + titleAnnotation = "org.opencontainers.image.title" + dir = "integration/testdata/fixtures/vm-images/" + ) + if err := os.MkdirAll(dir, 0o750); err != nil { + return err + } + tags, err := crane.ListTags(testVMImages, auth) + if err != nil { + return err + } + for _, tag := range tags { + img, err := crane.Pull(fmt.Sprintf("%s:%s", testVMImages, tag), auth) + if err != nil { + return err + } + + manifest, err := img.Manifest() + if err != nil { + return err + } + + layers, err := img.Layers() + if err != nil { + return err + } + + for i, layer := range layers { + fileName, ok := manifest.Layers[i].Annotations[titleAnnotation] + if !ok { + continue + } + filePath := filepath.Join(dir, fileName) + if exists(filePath) { + return nil + } + fmt.Printf("Downloading %s...\n", fileName) + if err = saveLayer(layer, filePath); err != nil { + return err + } + } + } + return nil +} + +func saveLayer(layer v1.Layer, filePath string) error { + f, err := os.Create(filePath) + if err != nil { + return err + } + defer f.Close() + + c, err := layer.Compressed() + if err != nil { + return err + } + if _, err = io.Copy(f, c); err != nil { + return err + } + return nil +} diff --git a/magefiles/helm.go b/magefiles/helm.go new file mode 100644 index 000000000000..1d904fccd96e --- /dev/null +++ b/magefiles/helm.go @@ -0,0 +1,123 @@ +//go:build mage_helm + +package main + +import ( + "fmt" + "log" + "os" + + "github.com/aquasecurity/go-version/pkg/semver" + + "github.com/magefile/mage/sh" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" +) + +const chartFile = "./helm/trivy/Chart.yaml" + +func main() { + trivyVersion, err := version() + if err != nil { + log.Fatalf("could not determine Trivy version: %v", err) + } + + // Checkout the main branch to get the latest chart version, that was changed after the previous release + // It needs for correctly updating the chart version of patch releases + if err := sh.Run("git", "checkout", "main"); err != nil { + log.Fatalf("failed to run `git checkout main`: %w", err) + } + + newHelmVersion, err := bumpHelmChart(chartFile, trivyVersion) + if err != nil { + log.Fatalf("could not bump Trivy version to %q: %v", trivyVersion, err) + } + + log.Printf("Current helm version will bump up %q with Trivy %q", newHelmVersion, trivyVersion) + + newBranch := fmt.Sprintf("ci/helm-chart/bump-trivy-to-%s", trivyVersion) + title := fmt.Sprintf("ci(helm): bump Trivy version to %s for Trivy Helm Chart %s", trivyVersion, newHelmVersion) + description := fmt.Sprintf("This PR bumps Trivy up to the %s version for the Trivy Helm chart %s.", + trivyVersion, newHelmVersion) + + cmds := [][]string{ + []string{"git", "switch", "-c", newBranch}, + []string{"git", "add", chartFile}, + []string{"git", "commit", "-m", title}, + []string{"git", "push", "origin", newBranch}, + []string{"gh", "pr", "create", "--base", "main", "--head", newBranch, "--title", title, "--body", description, "--repo", "$GITHUB_REPOSITORY"}, + } + + if err := runShCommands(cmds); err != nil { + log.Fatal(err) + } + log.Print("Successfully created PR with a new helm version") +} + +type Chart struct { + Version string `yaml:"version"` + AppVersion string `yaml:"appVersion"` +} + +// bumpHelmChart bumps up helm and trivy versions inside a file (Chart.yaml) +// it returns a new helm version and error +func bumpHelmChart(filename, trivyVersion string) (string, error) { + input, err := os.ReadFile(filename) + if err != nil { + return "", xerrors.Errorf("could not read file %q: %w", filename, err) + } + currentHelmChart := &Chart{} + if err := yaml.Unmarshal(input, currentHelmChart); err != nil { + return "", xerrors.Errorf("could not unmarshal helm chart %q: %w", filename, err) + } + + newHelmVersion, err := buildNewHelmVersion(currentHelmChart.Version, currentHelmChart.AppVersion, trivyVersion) + if err != nil { + return "", xerrors.Errorf("could not build new helm version: %v", err) + } + cmds := [][]string{ + []string{"sed", "-i", "-e", fmt.Sprintf("s/appVersion: %s/appVersion: %s/g", currentHelmChart.AppVersion, trivyVersion), filename}, + []string{"sed", "-i", "-e", fmt.Sprintf("s/version: %s/version: %s/g", currentHelmChart.Version, newHelmVersion), filename}, + } + + if err := runShCommands(cmds); err != nil { + return "", xerrors.Errorf("could not update Helm Chart %q: %w", newHelmVersion, err) + } + return newHelmVersion, nil +} + +func runShCommands(cmds [][]string) error { + for _, cmd := range cmds { + if err := sh.Run(cmd[0], cmd[1:]...); err != nil { + return xerrors.Errorf("failed to run %v: %w", cmd, err) + } + } + return nil +} + +func buildNewHelmVersion(currentHelm, currentTrivy, newTrivy string) (string, error) { + currentHelmVersion, err := semver.Parse(currentHelm) + if err != nil { + return "", xerrors.Errorf("could not parse current helm version: %w", err) + } + + currentTrivyVersion, err := semver.Parse(currentTrivy) + if err != nil { + return "", xerrors.Errorf("could not parse current trivy version: %w", err) + } + + newTrivyVersion, err := semver.Parse(newTrivy) + if err != nil { + return "", xerrors.Errorf("could not parse new trivy version: %w", err) + } + + if newTrivyVersion.Major().Compare(currentTrivyVersion.Major()) > 0 { + return currentHelmVersion.IncMajor().String(), nil + } + + if newTrivyVersion.Minor().Compare(currentTrivyVersion.Minor()) > 0 { + return currentHelmVersion.IncMinor().String(), nil + } + + return currentHelmVersion.IncPatch().String(), nil +} diff --git a/magefiles/helm_test.go b/magefiles/helm_test.go new file mode 100644 index 000000000000..355dacf04980 --- /dev/null +++ b/magefiles/helm_test.go @@ -0,0 +1,92 @@ +//go:build mage_helm + +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewVersion(t *testing.T) { + tests := []struct { + name string + currentHelmVersion string + currentTrivyVersion string + newTrivyVersion string + newHelmVersion string + }{ + { + "created the first patch", + "0.1.0", + "0.55.0", + "0.55.1", + "0.1.1", + }, + { + "created the second patch", + "0.1.1", + "0.55.1", + "0.55.2", + "0.1.2", + }, + { + "created the second patch but helm chart was changed", + "0.1.2", + "0.55.1", + "0.55.2", + "0.1.3", + }, + { + "created a new minor version", + "0.1.1", + "0.55.1", + "0.56.0", + "0.2.0", + }, + { + "created a new major version", + "0.1.1", + "0.55.1", + "1.0.0", + "1.0.0", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + newHelmVersion, err := buildNewHelmVersion(test.currentHelmVersion, test.currentTrivyVersion, test.newTrivyVersion) + assert.NoError(t, err) + assert.Equal(t, test.newHelmVersion, newHelmVersion) + }) + } +} + +func TestBumpHelmChart_Success(t *testing.T) { + tempFile, err := os.CreateTemp(t.TempDir(), "Chart-*.yaml") + assert.NoError(t, err) + + content := ` +apiVersion: v2 +name: trivy +version: 0.8.0 +appVersion: 0.55.0 +description: Trivy helm chart +keywords: + - scanner + - trivy + - vulnerability +` + err = os.WriteFile(tempFile.Name(), []byte(content), 0o644) + assert.NoError(t, err) + + newVersion, err := bumpHelmChart(tempFile.Name(), "0.55.1") + assert.NoError(t, err) + assert.Equal(t, "0.8.1", newVersion) + + updatedContent, err := os.ReadFile(tempFile.Name()) + assert.NoError(t, err) + assert.Contains(t, string(updatedContent), "appVersion: 0.55.1") + assert.Contains(t, string(updatedContent), "version: 0.8.1") +} diff --git a/magefiles/magefile.go b/magefiles/magefile.go new file mode 100644 index 000000000000..703e1f8189f9 --- /dev/null +++ b/magefiles/magefile.go @@ -0,0 +1,510 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "log/slog" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/magefile/mage/mg" + "github.com/magefile/mage/sh" + "github.com/magefile/mage/target" + + // Trivy packages should not be imported in Mage (see https://github.com/aquasecurity/trivy/pull/4242), + // but this package doesn't have so many dependencies, and Mage is still fast. + //mage:import gittest + gittest "github.com/aquasecurity/trivy/internal/gittest/testdata" + //mage:import rpm + rpm "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/rpm/testdata" + "github.com/aquasecurity/trivy/pkg/log" +) + +var ( + GOPATH = os.Getenv("GOPATH") + GOBIN = filepath.Join(GOPATH, "bin") + + ENV = map[string]string{ + "CGO_ENABLED": "0", + "GOEXPERIMENT": "jsonv2", + } +) + +func init() { + slog.SetDefault(log.New(log.NewHandler(os.Stderr, nil))) // stdout is suppressed in mage +} + +func version() (string, error) { + ver, err := sh.Output("git", "describe", "--tags", "--always") + if err != nil { + return "", err + } + // Strips the v prefix from the tag + return strings.TrimPrefix(ver, "v"), nil +} + +func buildLdflags() (string, error) { + ver, err := version() + if err != nil { + return "", err + } + return fmt.Sprintf("-s -w -X=github.com/aquasecurity/trivy/pkg/version/app.ver=%s", ver), nil +} + +type Tool mg.Namespace + +// Sass installs saas if not installed. npm is assumed to be available +func (Tool) Sass() error { + if installed("sass") { + return nil + } + return sh.Run("npm", "install", "-g", "saas") +} + +// PipTools installs PipTools if not installed. python is assumed to be available and relevant environment to have been activated +func (Tool) PipTools() error { + if installed("pip-compile") { + return nil + } + return sh.Run("python", "-m", "pip", "install", "pip-tools") +} + +// GolangciLint installs golangci-lint +func (t Tool) GolangciLint() error { + const version = "v2.4.0" + bin := filepath.Join(GOBIN, "golangci-lint") + if exists(bin) && t.matchGolangciLintVersion(bin, version) { + return nil + } + // TODO: use `go install tool` + // cf. https://golangci-lint.run/welcome/install/#install-from-sources + command := fmt.Sprintf("curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b %s %s", GOBIN, version) + return sh.Run("bash", "-c", command) +} + +func (Tool) matchGolangciLintVersion(bin, version string) bool { + out, err := sh.Output(bin, "version", "--json") + if err != nil { + slog.Error("Unable to get golangci-lint version", slog.Any("err", err)) + return false + } + var output struct { + Version string `json:"Version"` + } + if err = json.Unmarshal([]byte(out), &output); err != nil { + slog.Error("Unable to parse golangci-lint version", slog.Any("err", err)) + return false + } + + version = strings.TrimPrefix(version, "v") + if output.Version != version { + slog.Info("golangci-lint version mismatch", slog.String("expected", version), slog.String("actual", output.Version)) + return false + } + return true +} + +func (Tool) Install() error { + log.Info("Installing tools, make sure you add $GOBIN to the $PATH") + return sh.Run("go", "install", "tool") +} + +type Protoc mg.Namespace + +// Generate parses PROTO_FILES and generates the Go code for client/server mode +func (Protoc) Generate() error { + mg.Deps(Tool{}.Install) // Install buf and protoc-gen-twirp + + // Run buf generate + return sh.RunV("buf", "generate") +} + +// Fmt formats protobuf files using buf +func (Protoc) Fmt() error { + mg.Deps(Tool{}.Install) // Install buf + + // Run buf format + return sh.RunV("buf", "format", "-w") +} + +// Lint runs linting on protobuf files using buf +func (Protoc) Lint() error { + mg.Deps(Tool{}.Install) // Install buf + + // Run buf lint + return sh.RunV("buf", "lint") +} + +// Breaking checks for breaking changes in protobuf files using buf +func (Protoc) Breaking() error { + mg.Deps(Tool{}.Install) // Install buf + + // Run buf breaking against main branch + return sh.RunV("buf", "breaking", "--against", ".git#branch=main") +} + +// Yacc generates parser +func Yacc() error { + mg.Deps(Tool{}.Install) // Install yacc + return sh.Run("go", "generate", "./pkg/licensing/expression/...") +} + +type Test mg.Namespace + +// FixtureContainerImages downloads and extracts required images +func (Test) FixtureContainerImages() error { + return fixtureContainerImages() +} + +// FixtureVMImages downloads and extracts required VM images +func (Test) FixtureVMImages() error { + return fixtureVMImages() +} + +// FixtureTerraformPlanSnapshots generates Terraform Plan files in test folders +func (Test) FixtureTerraformPlanSnapshots() error { + return fixtureTerraformPlanSnapshots(context.TODO()) +} + +// GenerateModules compiles WASM modules for unit tests +func (Test) GenerateModules() error { + pattern := filepath.Join("pkg", "module", "testdata", "*", "*.go") + return compileWasmModules(pattern) +} + +// GenerateExampleModules compiles example Wasm modules for integration tests +func (Test) GenerateExampleModules() error { + pattern := filepath.Join("examples", "module", "*", "*.go") + return compileWasmModules(pattern) +} + +// UpdateGolden updates golden files for integration tests +func (Test) UpdateGolden() error { + return sh.RunWithV(ENV, "go", "test", "-tags=integration", "./integration/...", "./pkg/fanal/test/integration/...", "-update") +} + +func compileWasmModules(pattern string) error { + goFiles, err := filepath.Glob(pattern) + if err != nil { + return err + } + + for _, src := range goFiles { + // e.g. examples/module/spring4shell/spring4shell.go + // => examples/module/spring4shell/spring4shell.wasm + dst := strings.TrimSuffix(src, ".go") + ".wasm" + if updated, err := target.Path(dst, src); err != nil { + return err + } else if !updated { + continue + } + envs := map[string]string{ + "GOOS": "wasip1", + "GOARCH": "wasm", + } + if err = sh.RunWith(envs, "go", "generate", src); err != nil { + return err + } + } + return nil +} + +// Unit runs unit tests +func (t Test) Unit() error { + mg.Deps(t.GenerateModules, rpm.Fixtures, gittest.Fixtures) + return sh.RunWithV(ENV, "go", "test", "-v", "-short", "-coverprofile=coverage.txt", "-covermode=atomic", "./...") +} + +// Integration runs integration tests +func (t Test) Integration() error { + mg.Deps(t.FixtureContainerImages) + return sh.RunWithV(ENV, "go", "test", "-timeout", "15m", "-v", "-tags=integration", "./integration/...", "./pkg/fanal/test/integration/...") +} + +// K8s runs k8s integration tests +func (t Test) K8s() error { + mg.Deps(Tool{}.Install) // Install kind + err := sh.RunWithV(ENV, "kind", "create", "cluster", "--name", "kind-test", "--image", "kindest/node:v1.27.1@sha256:b7d12ed662b873bd8510879c1846e87c7e676a79fefc93e17b2a52989d3ff42b") + if err != nil { + return err + } + defer func() { + _ = sh.RunWithV(ENV, "kind", "delete", "cluster", "--name", "kind-test") + }() + // wait for the kind cluster is running correctly + err = sh.RunWithV(ENV, "kubectl", "wait", "--for=condition=Ready", "nodes", "--all", "--timeout=300s") + if err != nil { + return fmt.Errorf("can't wait for the kind cluster: %w", err) + } + + err = sh.RunWithV(ENV, "kubectl", "apply", "-f", "./integration/testdata/fixtures/k8s/test_nginx.yaml") + if err != nil { + return fmt.Errorf("can't create a test deployment: %w", err) + } + + // create an environment for limited user test + err = initk8sLimitedUserEnv() + if err != nil { + return fmt.Errorf("can't create environment for limited user: %w", err) + } + + // wait for all pods are running correctly + err = sh.RunWithV(ENV, "kubectl", "wait", "--for=condition=Ready", "pod", "--all", "--all-namespaces", "--timeout=300s") + if err != nil { + return fmt.Errorf("can't wait for the pods: %w", err) + } + + // print all resources for info + err = sh.RunWithV(ENV, "kubectl", "get", "all", "-A") + if err != nil { + return fmt.Errorf("can't get workloads: %w", err) + } + err = sh.RunWithV(ENV, "kubectl", "get", "cm", "-A") + if err != nil { + return fmt.Errorf("can't get configmaps: %w", err) + } + + return sh.RunWithV(ENV, "go", "test", "-v", "-tags=k8s_integration", "./integration/...") +} + +func initk8sLimitedUserEnv() error { + commands := [][]string{ + {"kubectl", "create", "namespace", "limitedns"}, + {"kubectl", "create", "-f", "./integration/testdata/fixtures/k8s/limited-pod.yaml"}, + {"kubectl", "create", "serviceaccount", "limiteduser"}, + {"kubectl", "create", "-f", "./integration/testdata/fixtures/k8s/limited-role.yaml"}, + {"kubectl", "create", "-f", "./integration/testdata/fixtures/k8s/limited-binding.yaml"}, + {"cp", "./integration/testdata/fixtures/k8s/kube-config-template", "./integration/limitedconfig"}, + } + + for _, cmd := range commands { + if err := sh.RunV(cmd[0], cmd[1:]...); err != nil { + return err + } + } + envs := make(map[string]string) + var err error + envs["CA"], err = sh.Output("kubectl", "config", "view", "-o", "jsonpath=\"{.clusters[?(@.name == 'kind-kind-test')].cluster.certificate-authority-data}\"", "--flatten") + if err != nil { + return err + } + envs["URL"], err = sh.Output("kubectl", "config", "view", "-o", "jsonpath=\"{.clusters[?(@.name == 'kind-kind-test')].cluster.server}\"") + if err != nil { + return err + } + envs["TOKEN"], err = sh.Output("kubectl", "create", "token", "limiteduser", "--duration=8760h") + if err != nil { + return err + } + commandsWith := [][]string{ + {"sed", "-i", "-e", "s|{{CA}}|$CA|g", "./integration/limitedconfig"}, + {"sed", "-i", "-e", "s|{{URL}}|$URL|g", "./integration/limitedconfig"}, + {"sed", "-i", "-e", "s|{{TOKEN}}|$TOKEN|g", "./integration/limitedconfig"}, + } + for _, cmd := range commandsWith { + if err := sh.RunWithV(envs, cmd[0], cmd[1:]...); err != nil { + return err + } + } + return nil +} + +// Module runs Wasm integration tests +func (t Test) Module() error { + mg.Deps(t.FixtureContainerImages, t.GenerateExampleModules) + return sh.RunWithV(ENV, "go", "test", "-v", "-tags=module_integration", "./integration/...") +} + +// UpdateModuleGolden updates golden files for Wasm integration tests +func (t Test) UpdateModuleGolden() error { + mg.Deps(t.FixtureContainerImages, t.GenerateExampleModules) + return sh.RunWithV(ENV, "go", "test", "-v", "-tags=module_integration", "./integration/...", "-update") +} + +// VM runs VM integration tests +func (t Test) VM() error { + mg.Deps(t.FixtureVMImages) + return sh.RunWithV(ENV, "go", "test", "-v", "-tags=vm_integration", "./integration/...") +} + +// UpdateVMGolden updates golden files for integration tests +func (t Test) UpdateVMGolden() error { + return errors.New("`mage test:updateVMGolden` is currently not supported. See TestVM function comments in integration/vm_test.go for details") + // mg.Deps(t.FixtureVMImages) + // return sh.RunWithV(ENV, "go", "test", "-v", "-tags=vm_integration", "./integration/...", "-update") +} + +// E2e runs E2E tests using testscript framework +func (t Test) E2e() error { + return sh.RunWithV(ENV, "go", "test", "-v", "-tags=e2e", "./e2e/...") +} + +type Lint mg.Namespace + +// Run runs linters +func (l Lint) Run() error { + mg.Deps(Tool{}.GolangciLint, Tool{}.Install) + if err := sh.RunWithV(ENV, "golangci-lint", "run", "--build-tags=integration"); err != nil { + return err + } + return sh.RunWithV(ENV, "modernize", "./...") +} + +// Fix auto fixes linters +func (l Lint) Fix() error { + mg.Deps(Tool{}.GolangciLint, Tool{}.Install) + if err := sh.RunWithV(ENV, "golangci-lint", "run", "--fix", "--build-tags=integration"); err != nil { + return err + } + return sh.RunWithV(ENV, "modernize", "-fix", "./...") +} + +// Fmt formats Go code +func Fmt() error { + // Format Go code + return sh.Run("go", "fmt", "./...") +} + +// Tidy makes sure go.mod matches the source code in the module +func Tidy() error { + return sh.RunV("go", "mod", "tidy") +} + +// Build builds Trivy +func Build() error { + if updated, err := target.Dir("trivy", "pkg", "cmd"); err != nil { + return err + } else if !updated { + return nil + } + + ldflags, err := buildLdflags() + if err != nil { + return err + } + wd, err := os.Getwd() + if err != nil { + return err + } + return sh.RunWith(ENV, "go", "build", "-ldflags", ldflags, filepath.Join(wd, "cmd", "trivy")) +} + +// Install installs Trivy +func Install() error { + ldflags, err := buildLdflags() + if err != nil { + return err + } + wd, err := os.Getwd() + if err != nil { + return err + } + return sh.RunWith(ENV, "go", "install", "-ldflags", ldflags, filepath.Join(wd, "cmd", "trivy")) +} + +// Clean cleans up the fixtures +func Clean() error { + fixtureDir := filepath.Join("integration", "testdata", "fixtures") + paths := []string{ + filepath.Join(fixtureDir, "images"), + filepath.Join(fixtureDir, "vm-images"), + } + for _, p := range paths { + if err := sh.Rm(p); err != nil { + return err + } + } + return nil +} + +// Label updates labels +func Label() error { + mg.Deps(Tool{}.Install) // Install labeler + + args := []string{"apply", "misc/triage/labels.yaml", "-l", "5"} + + // Add --repo flag if GITHUB_REPOSITORY environment variable is set + if repo := os.Getenv("GITHUB_REPOSITORY"); repo != "" { + args = append(args, "-r", repo) + } + + return sh.RunV("labeler", args...) +} + +type Docs mg.Namespace + +// Prepare python requirements +func (Docs) Pip() error { + const ( + requirementsIn = "docs/build/requirements.in" + ) + requirementsTxt := strings.TrimSuffix(requirementsIn, ".in") + ".txt" + if updated, err := target.Path(requirementsTxt, requirementsIn); err != nil { + return err + } else if !updated { + return nil + } + return sh.Run("pip-compile", requirementsIn, "--output-file", requirementsTxt) +} + +// Serve launches MkDocs development server to preview the documentation page +func (Docs) Serve() error { + const ( + mkdocsImage = "trivy-docs:dev" + mkdocsPort = "8000" + ) + if err := sh.Run("docker", "build", "-t", mkdocsImage, "docs/build"); err != nil { + return err + } + return sh.Run("docker", "run", "--name", "mkdocs-serve", "--rm", "-v", "${PWD}:/docs", "-p", mkdocsPort+":8000", mkdocsImage) +} + +// Generate generates CLI references +func (Docs) Generate() error { + return sh.RunWith(ENV, "go", "run", "-tags=mage_docs", "./magefiles") +} + +func exists(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} + +func installed(cmd string) bool { + _, err := exec.LookPath(cmd) + return err == nil +} + +type Schema mg.Namespace + +// Generate generates Cloud Schema for misconfiguration scanning +func (Schema) Generate() error { + return sh.RunWith(ENV, "go", "run", "-tags=mage_schema", "./magefiles", "--", "generate") +} + +// Verify verifies Cloud Schema for misconfiguration scanning +func (Schema) Verify() error { + return sh.RunWith(ENV, "go", "run", "-tags=mage_schema", "./magefiles", "--", "verify") +} + +// VEX generates a VEX document for Trivy +func VEX(_ context.Context, dir string) error { + return sh.RunWith(ENV, "go", "run", "-tags=mage_vex", "./magefiles/vex.go", "--dir", dir) +} + +type Helm mg.Namespace + +// UpdateVersion updates a version for Trivy Helm Chart and creates a PR +func (Helm) UpdateVersion() error { + return sh.RunWith(ENV, "go", "run", "-tags=mage_helm", "./magefiles") +} + +type SPDX mg.Namespace + +// UpdateLicenseEntries updates both SPDX license IDs and exceptions +func (SPDX) UpdateLicenseEntries() error { + return sh.RunWith(ENV, "go", "run", "-tags=mage_spdx", "./magefiles/spdx.go") +} diff --git a/magefiles/schema.go b/magefiles/schema.go new file mode 100644 index 000000000000..5b29487d4001 --- /dev/null +++ b/magefiles/schema.go @@ -0,0 +1,72 @@ +//go:build mage_schema + +package main + +import ( + "bytes" + "encoding/json" + "errors" + "log" + "os" + + "github.com/aquasecurity/trivy/pkg/iac/rego/schemas" +) + +const ( + schemaPath = "pkg/iac/rego/schemas/cloud.json" +) + +func main() { + if len(os.Args) < 3 { + log.Fatalf("invalid schema command args: %s", os.Args) + } + + switch os.Args[2] { + case "generate": + if err := GenSchema(); err != nil { + log.Fatalf(err.Error()) + } + log.Println("schema generated") + case "verify": + if err := VerifySchema(); err != nil { + log.Fatalf(err.Error()) + } + log.Println("schema valid") + } +} + +// GenSchema generates the Trivy IaC schema +func GenSchema() error { + schema, err := schemas.Build() + if err != nil { + return err + } + data, err := json.MarshalIndent(schema, "", " ") + if err != nil { + return err + } + if err := os.WriteFile(schemaPath, data, 0o600); err != nil { + return err + } + return nil +} + +// VerifySchema verifies a generated schema for validity +func VerifySchema() error { + schema, err := schemas.Build() + if err != nil { + return err + } + data, err := json.MarshalIndent(schema, "", " ") + if err != nil { + return err + } + existing, err := os.ReadFile(schemaPath) + if err != nil { + return err + } + if !bytes.Equal(data, existing) { + return errors.New("schema is out of date:\n\nplease run 'mage schema:generate' and commit the changes\n") + } + return nil +} diff --git a/magefiles/spdx.go b/magefiles/spdx.go new file mode 100644 index 000000000000..2620372df70c --- /dev/null +++ b/magefiles/spdx.go @@ -0,0 +1,116 @@ +//go:build mage_spdx + +package main + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + "sort" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/downloader" + "github.com/aquasecurity/trivy/pkg/log" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +const ( + expressionDir = "./pkg/licensing/expression" + + exceptionFileName = "exceptions.json" + exceptionURL = "https://spdx.org/licenses/exceptions.json" + + licenseFileName = "licenses.json" + licenseURL = "https://spdx.org/licenses/licenses.json" +) + +func main() { + if err := run(); err != nil { + log.Fatal("Fatal error", log.Err(err)) + } + +} + +type Exceptions struct { + Exceptions []Exception `json:"exceptions"` +} + +type Exception struct { + ID string `json:"licenseExceptionId"` +} + +// run downloads SPDX licenses and exceptions, extracts only IDs and writes flat arrays into the `expression` package. +func run() error { + if err := updateLicenses(); err != nil { + return err + } + return updateExceptions() +} + +func updateExceptions() error { + return fetchAndWrite(exceptionURL, exceptionFileName, filepath.Join(expressionDir, exceptionFileName), func(b []byte) ([]string, error) { + var exceptions Exceptions + if err := json.Unmarshal(b, &exceptions); err != nil { + return nil, xerrors.Errorf("unable to unmarshal exceptions.json file: %w", err) + } + exs := xslices.Map(exceptions.Exceptions, func(ex Exception) string { return ex.ID }) + return exs, nil + }) +} + +type Licenses struct { + Licenses []License `json:"licenses"` +} + +type License struct { + ID string `json:"licenseId"` +} + +func updateLicenses() error { + return fetchAndWrite(licenseURL, licenseFileName, filepath.Join(expressionDir, licenseFileName), func(b []byte) ([]string, error) { + var licenses Licenses + if err := json.Unmarshal(b, &licenses); err != nil { + return nil, xerrors.Errorf("unable to unmarshal licenses.json file: %w", err) + } + ids := xslices.Map(licenses.Licenses, func(l License) string { return l.ID }) + return ids, nil + }) +} + +// fetchAndWrite downloads a SPDX index file, extracts IDs using extractor, sorts and writes them to destPath +func fetchAndWrite(url, tmpFileName, destPath string, extractor func([]byte) ([]string, error)) error { + tmpDir, err := downloader.DownloadToTempDir(context.Background(), url, downloader.Options{}) + if err != nil { + return xerrors.Errorf("unable to download %s: %w", tmpFileName, err) + } + tmpFile, err := os.ReadFile(filepath.Join(tmpDir, tmpFileName)) + if err != nil { + return xerrors.Errorf("unable to read %s: %w", tmpFileName, err) + } + + ids, err := extractor(tmpFile) + if err != nil { + return err + } + sort.Strings(ids) + return writeIDs(destPath, ids) +} + +func writeIDs(path string, ids []string) error { + f, err := os.Create(path) + if err != nil { + return xerrors.Errorf("unable to create file %s: %w", path, err) + } + defer f.Close() + + b, err := json.Marshal(ids) + if err != nil { + return xerrors.Errorf("unable to marshal id list: %w", err) + } + if _, err = f.Write(b); err != nil { + return xerrors.Errorf("unable to write id list: %w", err) + } + return nil +} diff --git a/magefiles/terraformplan.go b/magefiles/terraformplan.go new file mode 100644 index 000000000000..d0b571e5b64c --- /dev/null +++ b/magefiles/terraformplan.go @@ -0,0 +1,139 @@ +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + hversion "github.com/hashicorp/go-version" //nolint:gomodguard // hc-install uses hashicorp/go-version + "github.com/hashicorp/hc-install/product" + "github.com/hashicorp/hc-install/releases" + "github.com/hashicorp/terraform-exec/tfexec" + "golang.org/x/sync/errgroup" + + "github.com/aquasecurity/trivy/internal/testutil" +) + +const ( + terraformVersion = "1.7.3" + terraformParallelLimit = 5 + + tfplanFile = "tfplan" +) + +func fixtureTerraformPlanSnapshots(ctx context.Context) error { + localstackC, addr, err := testutil.SetupLocalStack(ctx, "3.1.0") + if err != nil { + return err + } + defer localstackC.Terminate(ctx) + + envs := []struct { + key string + val string + }{ + {"AWS_DEFAULT_REGION", "us-east-1"}, + {"AWS_ACCESS_KEY_ID", "test"}, + {"AWS_SECRET_ACCESS_KEY", "test"}, + {"AWS_ENDPOINT_URL", addr}, + } + + for _, env := range envs { + if err := os.Setenv(env.key, env.val); err != nil { + return err + } + } + + dirs := []string{ + "pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots", + "pkg/iac/scanners/terraformplan/snapshot/testdata", + } + + var workingDirs []string + + for _, dir := range dirs { + entries, err := os.ReadDir(filepath.FromSlash(dir)) + if err != nil { + return err + } + + for _, entry := range entries { + workingDirs = append(workingDirs, filepath.Join(dir, entry.Name())) + } + } + + installer := &releases.ExactVersion{ + Product: product.Terraform, + Version: hversion.Must(hversion.NewVersion(terraformVersion)), + } + + execPath, err := installer.Install(ctx) + if err != nil { + return fmt.Errorf("failed to install Terraform: %w", err) + } + + g, ctx := errgroup.WithContext(ctx) + g.SetLimit(terraformParallelLimit) + + for _, workingDir := range workingDirs { + g.Go(func() error { + if err := os.Remove(tfplanFile); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + + if err := generatePlan(ctx, execPath, workingDir); err != nil { + return fmt.Errorf("failed to generate Terraform Plan: %w", err) + } + + return nil + }) + } + + return g.Wait() +} + +func generatePlan(ctx context.Context, execPath, workingDir string) error { + if err := cleanup(workingDir); err != nil { + return err + } + defer cleanup(workingDir) + + tf, err := tfexec.NewTerraform(workingDir, execPath) + if err != nil { + return fmt.Errorf("failed to run Terraform: %w", err) + } + + prefix := fmt.Sprintf("tfplan:%s:", filepath.Base(workingDir)) + tf.SetLogger(log.New(os.Stdout, prefix, log.LstdFlags)) + + if err = tf.Init(ctx, tfexec.Upgrade(true)); err != nil { + return fmt.Errorf("failed to run Init cmd: %w", err) + } + + if _, err := tf.Plan(ctx, tfexec.Out(tfplanFile)); err != nil { + return fmt.Errorf("failed to run Plan cmd: %w", err) + } + + return nil +} + +func cleanup(workingDir string) error { + entries, err := os.ReadDir(workingDir) + if err != nil { + return err + } + + for _, entry := range entries { + if entry.Name() == "terraform.tfstate" || strings.HasPrefix(entry.Name(), ".terraform") { + path := filepath.Join(workingDir, entry.Name()) + if err := os.RemoveAll(path); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } + } + } + return nil +} diff --git a/magefiles/vex.go b/magefiles/vex.go new file mode 100644 index 000000000000..f5eb52e2efd7 --- /dev/null +++ b/magefiles/vex.go @@ -0,0 +1,425 @@ +//go:build mage_vex + +package main + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "os" + "os/exec" + "path" + "strings" + "time" + + "github.com/openvex/go-vex/pkg/vex" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "golang.org/x/vuln/scan" + + "github.com/aquasecurity/go-version/pkg/version" + "github.com/aquasecurity/trivy/pkg/log" +) + +const ( + repoURL = "https://github.com/aquasecurity/trivy" + minVersion = "0.40.0" +) + +var ( + minVer, _ = version.Parse(minVersion) + + // Product ID for Trivy + productID = &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + // According to https://github.com/package-url/purl-spec/issues/63, + // It's probably better to leave namespace empty and put a module name into `name`. + Namespace: "", + Name: "github.com/aquasecurity/trivy", + } +) + +// VulnerabilityFinding is for parsing govulncheck JSON output +type VulnerabilityFinding struct { + Finding Finding `json:"finding"` +} + +type Finding struct { + OSV string `json:"osv"` + FixedVersion string `json:"fixed_version"` + Trace []Trace `json:"trace"` +} + +type Trace struct { + Module string `json:"module"` + Version string `json:"version"` + Package string `json:"package"` +} + +// UniqueKey is used to identify unique vulnerability-subcomponent pairs +type UniqueKey struct { + VulnerabilityID vex.VulnerabilityID + SubcomponentID string +} + +func main() { + if err := run(); err != nil { + log.Fatal("Fatal error", log.Err(err)) + } +} + +// run is the main entry point for the VEX generator +func run() error { + log.InitLogger(false, false) + + // Parse command-line flags + cloneDir := flag.String("dir", "trivy", "Directory to clone the repository") + output := flag.String("output", ".vex/trivy.openvex.json", "Output file") + flag.Parse() + + ctx := context.Background() + + // Clone or pull the Trivy repository + if _, err := cloneOrPullRepo(ctx, *cloneDir); err != nil { + return err + } + + defer func() { + // Ensure we are on the main branch after processing + _, _ = checkoutMain(ctx, *cloneDir) + }() + + // Save the current working directory + wd, err := os.Getwd() + if err != nil { + return fmt.Errorf("failed to get current working directory: %w", err) + } + + // Change to the target directory as govulncheck doesn't support Dir + // cf. https://github.com/golang/go/blob/6d89b38ed86e0bfa0ddaba08dc4071e6bb300eea/src/os/exec/exec.go#L171-L174 + if err = os.Chdir(*cloneDir); err != nil { + return fmt.Errorf("failed to change to directory %s: %w", *cloneDir, err) + } + + // Get the latest tags from the repository + tags, err := getLatestTags(ctx) + if err != nil { + return err + } + log.Info("Latest tags", log.Any("tags", tags)) + + // Maps to store "not_affected" statements across Trivy versions + notAffectedVulns := make(map[UniqueKey][]vex.Statement) + + // Indicate one or more Trivy versions are affected by the vulnerability. + // This means that the version cannot be omitted later. + affectedVulns := make(map[UniqueKey]struct{}) + + // Process each tag + for _, tag := range tags { + notAffected, affected, err := processTag(ctx, tag) + if err != nil { + return err + } + log.Info("Processed tag", log.String("tag", tag), + log.Int("not_affected", len(notAffected)), log.Int("affected", len(affected))) + lo.Assign(affectedVulns, affected) + for k, v := range notAffected { + notAffectedVulns[k] = append(notAffectedVulns[k], v) + } + } + + // Change back to the original directory + if err = os.Chdir(wd); err != nil { + return fmt.Errorf("failed to change back to original directory: %w", err) + } + + // Generate the final VEX document + if err = updateVEX(*output, combineDocs(notAffectedVulns, affectedVulns)); err != nil { + return err + } + + return nil +} + +// cloneOrPullRepo clones the Trivy repository or pulls updates if it already exists +func cloneOrPullRepo(ctx context.Context, dir string) ([]byte, error) { + if _, err := os.Stat(dir); os.IsNotExist(err) { + return runCommandWithTimeout(ctx, 20*time.Minute, "git", "clone", repoURL, dir) + } + + if _, err := checkoutMain(ctx, dir); err != nil { + return nil, fmt.Errorf("failed to checkout main: %w", err) + } + return runCommandWithTimeout(ctx, 2*time.Minute, "git", "-C", dir, "pull", "--tags") +} + +// checkoutMain checks out the main branch of the repository +func checkoutMain(ctx context.Context, dir string) ([]byte, error) { + return runCommandWithTimeout(ctx, 1*time.Minute, "git", "-C", dir, "checkout", "main") +} + +// getLatestTags retrieves and sorts the latest tags from the repository +func getLatestTags(ctx context.Context) ([]string, error) { + output, err := runCommandWithTimeout(ctx, 1*time.Minute, "git", "tag") + if err != nil { + return nil, fmt.Errorf("failed to get tags: %w", err) + } + + tags := strings.Split(strings.TrimSpace(string(output)), "\n") + versions := make([]string, 0, len(tags)) + + for _, tag := range tags { + v, err := version.Parse(tag) + if err != nil { + continue + } + if v.GreaterThanOrEqual(minVer) { + versions = append(versions, tag) + } + } + + return versions, nil +} + +// processTag processes a single tag, running govulncheck and generating VEX statements +func processTag(ctx context.Context, tag string) (map[UniqueKey]vex.Statement, map[UniqueKey]struct{}, error) { + log.Info("Processing tag...", log.String("tag", tag)) + if _, err := runCommandWithTimeout(ctx, 1*time.Minute, "git", "checkout", tag); err != nil { + return nil, nil, fmt.Errorf("failed to checkout tag %s: %w", tag, err) + } + + // Run govulncheck and generate VEX document + vexDoc, err := generateVEX(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to run govulncheck: %w", err) + } + + // Run govulncheck and generate JSON result + // Need to generate JSON as well as OpenVEX for the following reasons: + // - Subcomponent + // - OpenVEX from govulncheck doesn't fill in subcomponents. + // - Status + // - govulncheck uses "not_affected" for all vulnerabilities. Need to determine "fixed" vulnerabilities. + // cf. https://github.com/golang/go/issues/68338 + findings, err := generateJSON(ctx) + if err != nil { + return nil, nil, fmt.Errorf("failed to run govulncheck: %w", err) + } + + product := *productID // Clone Trivy PURL + product.Version = tag + notAffected := make(map[UniqueKey]vex.Statement) + affected := make(map[UniqueKey]struct{}) + + // Update VEX document generated by govulncheck + for _, stmt := range vexDoc.Statements { + finding, ok := findings[stmt.Vulnerability.Name] + if !ok { + // Considered as "fixed" vulnerabilities + // cf. https://github.com/golang/go/issues/68338 + continue + } else if len(finding.Finding.Trace) == 0 { + continue + } + + namespace, name := path.Split(finding.Finding.Trace[0].Module) + subcomponent := &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: namespace, + Name: name, + } + + key := UniqueKey{ + VulnerabilityID: stmt.Vulnerability.Name, + SubcomponentID: subcomponent.String(), + } + + if stmt.Status == vex.StatusAffected { + affected[key] = struct{}{} + continue + } else if stmt.Status != vex.StatusNotAffected { + continue + } + + // Update the statement with product and subcomponent information + stmt.Products = []vex.Product{ + { + // Fill in components manually + // cf. https://github.com/golang/go/issues/68152 + Component: vex.Component{ + ID: product.String(), + Identifiers: map[vex.IdentifierType]string{ + vex.PURL: product.String(), + }, + }, + Subcomponents: []vex.Subcomponent{ + { + Component: vex.Component{ + ID: key.SubcomponentID, + Identifiers: map[vex.IdentifierType]string{ + vex.PURL: key.SubcomponentID, + }, + }, + }, + }, + }, + } + notAffected[key] = stmt + } + + return notAffected, affected, nil +} + +// generateVEX runs govulncheck with OpenVEX format and parses the output +func generateVEX(ctx context.Context) (*vex.VEX, error) { + buf, err := runGovulncheck(ctx, "openvex") + if err != nil { + return nil, fmt.Errorf("failed to run govulncheck: %w", err) + } + + vexDoc, err := vex.Parse(buf.Bytes()) + if err != nil { + return nil, fmt.Errorf("failed to parse govulncheck output: %w", err) + } + return vexDoc, nil +} + +// generateJSON runs govulncheck with JSON format and parses the output +func generateJSON(ctx context.Context) (map[vex.VulnerabilityID]VulnerabilityFinding, error) { + buf, err := runGovulncheck(ctx, "json") + if err != nil { + return nil, fmt.Errorf("failed to run govulncheck: %w", err) + } + + decoder := json.NewDecoder(buf) + findings := make(map[vex.VulnerabilityID]VulnerabilityFinding) + for { + var finding VulnerabilityFinding + if err := decoder.Decode(&finding); err == io.EOF { + break + } else if err != nil { + return nil, fmt.Errorf("failed to decode govulncheck output: %w", err) + } + findings[vex.VulnerabilityID(finding.Finding.OSV)] = finding + } + return findings, nil +} + +// runGovulncheck executes the govulncheck command with the specified format +func runGovulncheck(ctx context.Context, format string) (*bytes.Buffer, error) { + var buf bytes.Buffer + cmd := scan.Command(ctx, "-format", format, "./...") + cmd.Stdout = &buf + + log.Info("Running govulncheck", log.String("format", format)) + if err := cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start govulncheck: %w", err) + } + + if err := cmd.Wait(); err != nil { + return nil, fmt.Errorf("failed to run govulncheck: %w", err) + } + return &buf, nil +} + +// combineDocs merges the VEX statements from all processed tags +func combineDocs(notAffected map[UniqueKey][]vex.Statement, affected map[UniqueKey]struct{}) []vex.Statement { + log.Info("Combining VEX documents") + statements := make(map[UniqueKey]vex.Statement) + for key, stmts := range notAffected { + for _, stmt := range stmts { + if _, ok := affected[key]; !ok { + // All versions are "not_affected" or "fixed" by the vulnerability, omitting a version in PURL + // => pkg:golang/github.com/aquasecurity/trivy + stmt.Products[0].ID = productID.String() + stmt.Products[0].Identifiers[vex.PURL] = productID.String() + statements[key] = stmt + break + } + + // At least one version is "affected" by the vulnerability, so we need to include the version in PURL. + // => pkg:golang/github.com/aquasecurity/trivy@0.52.0 + if s, ok := statements[key]; ok { + s.Products = append(s.Products, stmt.Products...) + statements[key] = s + } else { + statements[key] = stmt + } + } + } + return lo.Values(statements) +} + +// runCommandWithTimeout executes a command with a specified timeout +func runCommandWithTimeout(ctx context.Context, timeout time.Duration, name string, args ...string) ([]byte, error) { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + cmd := exec.CommandContext(ctx, name, args...) + log.Info("Executing command", log.String("cmd", cmd.String())) + + output, err := cmd.CombinedOutput() + if err != nil { + return output, fmt.Errorf("%w, output: %s", err, string(output)) + } + + if errors.Is(ctx.Err(), context.DeadlineExceeded) { + return nil, fmt.Errorf("command timed out after %v", timeout) + } + + return output, nil +} + +// updateVEX updates the final VEX document with the combined statements +func updateVEX(output string, statements []vex.Statement) error { + doc, err := vex.Load(output) + if errors.Is(err, os.ErrNotExist) { + doc = &vex.VEX{} + } else if err != nil { + return err + } + + vex.SortStatements(statements, time.Now()) + d := &vex.VEX{ + Metadata: vex.Metadata{ + Context: "https://openvex.dev/ns/v0.2.0", + Author: "Aqua Security", + Timestamp: lo.ToPtr(time.Now()), + Version: doc.Version + 1, + Tooling: "https://github.com/aquasecurity/trivy/tree/main/magefiles/vex.go", + }, + Statements: statements, + } + h, err := hashVEX(d) + if err != nil { + return err + } + d.ID = "aquasecurity/trivy:" + h + + f, err := os.Create(output) + if err != nil { + return err + } + defer f.Close() + + e := json.NewEncoder(f) + e.SetIndent("", " ") + if err = e.Encode(d); err != nil { + return err + } + return err +} + +func hashVEX(d *vex.VEX) (string, error) { + out, err := json.Marshal(d) + if err != nil { + return "", err + } + return fmt.Sprintf("%x", sha256.Sum256(out)), nil +} diff --git a/misc/backport/backport.sh b/misc/backport/backport.sh new file mode 100755 index 000000000000..263a27d8b653 --- /dev/null +++ b/misc/backport/backport.sh @@ -0,0 +1,71 @@ +#!/bin/bash + +set -e + +BRANCH_NAME=$1 +PR_NUMBER=$2 + +echo "Backporting PR #$PR_NUMBER to branch $BRANCH_NAME" + +# Get the merge commit hash of the pull request +echo "Fetching merge commit hash of PR #$PR_NUMBER..." +COMMIT_HASH=$(gh api /repos/"$GITHUB_REPOSITORY"/pulls/"$PR_NUMBER" | jq -r '.merge_commit_sha') +echo "Merge commit hash: $COMMIT_HASH" + +# Get the title of the original pull request +echo "Fetching title of PR #$PR_NUMBER..." +ORIGINAL_PR_TITLE=$(gh api /repos/"$GITHUB_REPOSITORY"/pulls/"$PR_NUMBER" | jq -r '.title') +echo "Original PR title: $ORIGINAL_PR_TITLE" + +# Checkout the base branch +echo "Checking out base branch: $BRANCH_NAME" +git checkout "$BRANCH_NAME" + +# Create a new branch with the PR number and branch name +NEW_BRANCH="backport-pr-$PR_NUMBER-to-$BRANCH_NAME" + +echo "Creating new branch: $NEW_BRANCH" +git switch -c "$NEW_BRANCH" + +# Create the pull request title +PR_TITLE="$ORIGINAL_PR_TITLE [backport: $BRANCH_NAME]" + +# Create the pull request description +PR_DESCRIPTION="# Backport + +This will backport the following commits from \`main\` to \`$BRANCH_NAME\`: + - https://github.com/$GITHUB_REPOSITORY/pull/$PR_NUMBER" + +echo "Cherry-picking commit: $COMMIT_HASH" +if git cherry-pick "$COMMIT_HASH"; then + echo "Cherry-pick successful" +else + echo "Cherry-pick failed due to conflicts, force-committing changes" + + # Add only conflicted files + git diff --name-only --diff-filter=U | xargs git add + + # Force-commit the changes with conflicts + git commit -m "Force-committed changes with conflicts for cherry-pick of $COMMIT_HASH" + + PR_DESCRIPTION="$PR_DESCRIPTION + +## ⚠️ Warning +Conflicts occurred during the cherry-pick and were force-committed without proper resolution. Please carefully review the changes, resolve any remaining conflicts, and ensure the code is in a valid state." +fi + +echo "Pushing new branch to origin: $NEW_BRANCH" +git push origin "$NEW_BRANCH" + +echo "Pull request title: $PR_TITLE" + +echo "Pull request description:" +echo "$PR_DESCRIPTION" + +# Create a new pull request with the original PR title, backport suffix, and description +echo "Creating pull request..." +gh pr create --base "$BRANCH_NAME" --head "$NEW_BRANCH" --title "$PR_TITLE" --body "$PR_DESCRIPTION" --repo "$GITHUB_REPOSITORY" --label "backport" + +# Add a comment to the original PR +echo "Adding comment to the original PR #$PR_NUMBER" +gh pr comment "$PR_NUMBER" --body "Backport PR created: https://github.com/$GITHUB_REPOSITORY/pull/$(gh pr view "$NEW_BRANCH" --json number --jq .number)" \ No newline at end of file diff --git a/misc/eol/data/debian.csv b/misc/eol/data/debian.csv new file mode 100644 index 000000000000..d87461000db3 --- /dev/null +++ b/misc/eol/data/debian.csv @@ -0,0 +1,19 @@ +1.1,Buzz,buzz,1993-08-16,1996-06-17,1997-06-05 +1.2,Rex,rex,1996-06-17,1996-12-12,1998-06-05 +1.3,Bo,bo,1996-12-12,1997-06-05,1999-03-09 +2.0,Hamm,hamm,1997-06-05,1998-07-24,2000-03-09 +2.1,Slink,slink,1998-07-24,1999-03-09,2000-10-30 +2.2,Potato,potato,1999-03-09,2000-08-15,2003-07-30 +3.0,Woody,woody,2000-08-15,2002-07-19,2006-06-30 +3.1,Sarge,sarge,2002-07-19,2005-06-06,2008-03-30 +4.0,Etch,etch,2005-06-06,2007-04-08,2010-02-15 +5.0,Lenny,lenny,2007-04-08,2009-02-14,2012-02-06 +6.0,Squeeze,squeeze,2009-02-14,2011-02-06,2014-05-31 +7,Wheezy,wheezy,2011-02-06,2013-05-04,2016-04-26 +8,Jessie,jessie,2013-05-04,2015-04-25,2018-06-06 +9,Stretch,stretch,2015-04-25,2017-06-17 +10,Buster,buster,2017-06-17 +11,Bullseye,bullseye,2019-08-01 +12,Bookworm,bookworm,2021-08-01 +,Sid,sid,1993-08-16 +,Experimental,experimental,1993-08-16 diff --git a/misc/eol/data/ubuntu.csv b/misc/eol/data/ubuntu.csv new file mode 100644 index 000000000000..bd9e81a95bbc --- /dev/null +++ b/misc/eol/data/ubuntu.csv @@ -0,0 +1,33 @@ +4.10,Warty Warthog,warty,2004-03-05,2004-10-20,2006-04-30 +5.04,Hoary Hedgehog,hoary,2004-10-20,2005-04-08,2006-10-31 +5.10,Breezy Badger,breezy,2005-04-08,2005-10-12,2007-04-13 +6.06 LTS,Dapper Drake,dapper,2005-10-12,2006-06-01,2009-07-14,2011-06-01 +6.10,Edgy Eft,edgy,2006-06-01,2006-10-26,2008-04-25 +7.04,Feisty Fawn,feisty,2006-10-26,2007-04-19,2008-10-19 +7.10,Gutsy Gibbon,gutsy,2007-04-19,2007-10-18,2009-04-18 +8.04 LTS,Hardy Heron,hardy,2007-10-18,2008-04-24,2011-05-12,2013-05-09 +8.10,Intrepid Ibex,intrepid,2008-04-24,2008-10-30,2010-04-30 +9.04,Jaunty Jackalope,jaunty,2008-10-30,2009-04-23,2010-10-23 +9.10,Karmic Koala,karmic,2009-04-23,2009-10-29,2011-04-29 +10.04 LTS,Lucid Lynx,lucid,2009-10-29,2010-04-29,2013-05-09,2015-04-29 +10.10,Maverick Meerkat,maverick,2010-04-29,2010-10-10,2012-04-10 +11.04,Natty Narwhal,natty,2010-10-10,2011-04-28,2012-10-28 +11.10,Oneiric Ocelot,oneiric,2011-04-28,2011-10-13,2013-05-09 +12.04 LTS,Precise Pangolin,precise,2011-10-13,2012-04-26,2017-04-26,2017-04-26,2019-04-26 +12.10,Quantal Quetzal,quantal,2012-04-26,2012-10-18,2014-05-16 +13.04,Raring Ringtail,raring,2012-10-18,2013-04-25,2014-01-27 +13.10,Saucy Salamander,saucy,2013-04-25,2013-10-17,2014-07-17 +14.04 LTS,Trusty Tahr,trusty,2013-10-17,2014-04-17,2019-04-25,2019-04-25,2022-04-25 +14.10,Utopic Unicorn,utopic,2014-04-17,2014-10-23,2015-07-23 +15.04,Vivid Vervet,vivid,2014-10-23,2015-04-23,2016-01-23 +15.10,Wily Werewolf,wily,2015-04-23,2015-10-22,2016-07-22 +16.04 LTS,Xenial Xerus,xenial,2015-10-22,2016-04-21,2021-04-21,2021-04-21,2024-04-21 +16.10,Yakkety Yak,yakkety,2016-04-21,2016-10-13,2017-07-20 +17.04,Zesty Zapus,zesty,2016-10-13,2017-04-13,2018-01-13 +17.10,Artful Aardvark,artful,2017-04-13,2017-10-19,2018-07-19 +18.04 LTS,Bionic Beaver,bionic,2017-10-19,2018-04-26,2023-04-26,2023-04-26,2028-04-26 +18.10,Cosmic Cuttlefish,cosmic,2018-04-26,2018-10-18,2019-07-18 +19.04,Disco Dingo,disco,2018-10-18,2019-04-18,2020-01-18 +19.10,Eoan Ermine,eoan,2019-04-18,2019-10-17,2020-07-17 +20.04 LTS,Focal Fossa,focal,2020-04-23,2025-04-23,2030-04-23 +22.04 LTS,Jammy Jellyfish,jammy,2022-04-21,2027-04-21,2032-04-21 diff --git a/misc/eol/main.go b/misc/eol/main.go new file mode 100644 index 000000000000..9cb4719b0acb --- /dev/null +++ b/misc/eol/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "strings" + "time" +) + +// This script displays EOL dates +func main() { + fmt.Println("Debian") + debianEOL() + + fmt.Println("\nUbuntu") + ubuntuEOL() +} + +func debianEOL() { + f, err := os.Open("data/debian.csv") + if err != nil { + panic(err) + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Split(line, ",") + + if len(fields) < 6 && fields[0] != "" { + fmt.Printf("\"%s\": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC),\n", fields[0]) + } else if len(fields) == 6 { + eol, _ := time.Parse("2006-1-2", fields[5]) // nolint: errcheck + fmt.Printf("\"%s\": time.Date(%d, %d, %d, 23, 59, 59, 0, time.UTC),\n", fields[0], eol.Year(), eol.Month(), eol.Day()) + } + } +} + +func ubuntuEOL() { + f, err := os.Open("data/ubuntu.csv") + if err != nil { + panic(err) + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Split(line, ",") + eol, _ := time.Parse("2006-1-2", fields[len(fields)-1]) // nolint: errcheck + fmt.Printf("\"%s\": time.Date(%d, %d, %d, 23, 59, 59, 0, time.UTC),\n", strings.Fields(fields[0])[0], eol.Year(), eol.Month(), eol.Day()) + } +} diff --git a/misc/lint/rules.go b/misc/lint/rules.go new file mode 100644 index 000000000000..adecdbbc2b46 --- /dev/null +++ b/misc/lint/rules.go @@ -0,0 +1,50 @@ +//go:build ruleguard + +package gorules + +import "github.com/quasilyte/go-ruleguard/dsl" + +// cf. https://go.dev/wiki/CodeReviewComments#declaring-empty-slices +func declareEmptySlices(m dsl.Matcher) { + m.Match( + `$name := []$t{}`, + `$name := make([]$t, 0)`, + ). + Suggest(`var $name []$t`). + Report(`replace '$$' with 'var $name []$t'`) +} + +// cf. https://github.com/uber-go/guide/blob/master/style.md#initializing-maps +func initializeMaps(m dsl.Matcher) { + m.Match(`map[$key]$value{}`). + Suggest(`make(map[$key]$value)`). + Report(`replace '$$' with 'make(map[$key]$value)`) +} + +// While errors.Join from standard library can combine multiple errors, +// we use hashicorp/go-multierror for more user-friendly error outputs. +func errorsJoin(m dsl.Matcher) { + m.Match(`errors.Join($x...)`). + Report("use github.com/hashicorp/go-multierror.Append instead of errors.Join.") + + m.Match(`errors.Join($*args)`). + Report("use github.com/hashicorp/go-multierror.Append instead of errors.Join.") +} + +func mapSet(m dsl.Matcher) { + m.Match(`map[$x]struct{}`). + Report("use github.com/aquasecurity/trivy/pkg/set.Set instead of map.") +} + +// Enforce usage of x/os package for temporary file operations +func tempFileOps(m dsl.Matcher) { + m.Match(`os.CreateTemp($*args)`). + Where(!m.File().Name.Matches(`.*_test\.go$`)). + Suggest(`xos.CreateTemp($args)`). + Report("use github.com/aquasecurity/trivy/pkg/x/os.CreateTemp instead of os.CreateTemp for process-safe temp file cleanup") + + m.Match(`os.MkdirTemp($*args)`). + Where(!m.File().Name.Matches(`.*_test\.go$`)). + Suggest(`xos.MkdirTemp($args)`). + Report("use github.com/aquasecurity/trivy/pkg/x/os.MkdirTemp instead of os.MkdirTemp for process-safe temp file cleanup") +} diff --git a/misc/triage/labels.yaml b/misc/triage/labels.yaml new file mode 100644 index 000000000000..e353cceb2a3d --- /dev/null +++ b/misc/triage/labels.yaml @@ -0,0 +1,149 @@ +--- +repo: aquasecurity/trivy +labels: + +# kind +- name: kind/breaking + color: e11d21 + description: Categorizes issue or PR as related to breaking compatibility. + from: kind/api-change +- name: kind/bug + color: e11d21 + from: bug + description: Categorizes issue or PR as related to a bug. +- name: kind/security-advisory + color: e11d21 + description: Categorizes issue or PR as related to security advisories. +- name: kind/cleanup + color: c7def8 + description: Categorizes issue or PR as related to cleaning up code, process, or technical debt. +- name: kind/deprecation + color: e11d21 + from: enhancement + description: Categorizes issue or PR as related to a feature/enhancement marked for deprecation. +- name: kind/documentation + color: c7def8 + description: Categorizes issue or PR as related to documentation. +- name: kind/feature + color: c7def8 + description: Categorizes issue or PR as related to a new feature. +- name: kind/testing + color: c7def8 + description: Categorizes issue or PR as related to a unit/integration test. +- name: kind/security + color: f4dddc + description: Categorizes issue or PR as related to Trivy's own security or internal vulnerabilities. +- name: kind/integration + color: f4dddc + description: Categorizes issue or PR as related to a third party integration of Trivy. + +# lifecycle for the stale bot +- name: lifecycle/frozen + color: d3e2f0 + description: Indicates that an issue or PR should not be auto-closed due to staleness. +- name: lifecycle/stale + color: "795548" + description: Denotes an issue or PR has remained open with no activity and will be auto-closed. +- name: lifecycle/active + color: "1d76db" + description: Indicates that an issue or PR is actively being worked on by a contributor. + +# priority +- name: priority/critical-urgent + color: e11d21 + description: Highest priority. Must be actively worked on as someone's top priority right now. +- name: priority/important-soon + color: eb6420 + description: Must be staffed and worked on either currently, or very soon, ideally in time for the next release. +- name: priority/important-longterm + color: eb6420 + description: Important over the long term, but may not be staffed and/or may need multiple releases to complete. +- name: priority/backlog + color: fbca04 + description: Higher priority than priority/awaiting-more-evidence. +- name: priority/awaiting-more-evidence + color: fef2c0 + description: Lowest priority. Possibly useful, but not yet enough support to actually get it done. + +# triage +- name: triage/discuss + color: faff84 + description: Items for discussion +- name: triage/duplicate + color: d455d0 + from: duplicate + description: Indicates an issue is a duplicate of other open issue. +- name: triage/needs-information + color: d455d0 + description: Indicates an issue needs more information in order to work on it. +- name: triage/not-reproducible + color: d455d0 + description: Indicates an issue can not be reproduced as described. +- name: triage/obsolete + color: faff84 + description: Bugs that no longer occur in the latest stable release +- name: triage/support + color: d455d0 + from: question + description: Indicates an issue that is a support question. +- name: triage/unresolved + color: d455d0 + description: Indicates an issue that can not or will not be resolved. + +# scanning +- name: scan/vulnerability + color: d9ead3 + description: Issues relating to vulnerability scanning +- name: scan/misconfiguration + color: d9ead3 + description: Issues relating to misconfiguration scanning +- name: scan/secret + color: d9ead3 + description: Issues relating to secret scanning +- name: scan/sbom + color: d9ead3 + description: Issues relating to SBOM +- name: scan/license + color: d9ead3 + description: Issues relating to license scanning + +# target +- name: target/container-image + color: 0ebdb0 + description: Issues relating to container image scanning +- name: target/filesystem + color: 0ebdb0 + description: Issues relating to filesystem scanning +- name: target/repository + color: 0ebdb0 + description: Issues relating to VCS repository scanning +- name: target/kubernetes + color: 0ebdb0 + description: Issues relating to kubernetes cluster scanning +- name: target/cloud + color: 0ebdb0 + description: Issues relating to cloud account scanning +- name: target/vm + color: 0ebdb0 + description: Issues relating to virtual machine scanning + +# community +- name: good first issue + color: 7057ff + description: Denotes an issue ready for a new contributor, according to the "help wanted" guidelines. +- name: help wanted + color: 006b75 + description: Denotes an issue that needs help from a contributor. Must meet "help wanted" guidelines. + +# release +- name: backport + color: A8F7BC + description: Backport PRs + +# automation +- name: autoready + color: 1d76db + description: Automatically mark PR as ready for review when all checks pass +- name: apidiff + color: ededed + description: Indicates Go API changes relevant to library consumers (CLI compatibility may be unaffected) diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000000..4293c8c32990 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,298 @@ +site_name: Trivy +site_url: https://trivy.dev/ +site_description: Trivy - All-in-one open source security scanner +docs_dir: docs/ +repo_name: GitHub +repo_url: https://github.com/aquasecurity/trivy +edit_uri: "blob/main/docs/" + +nav: + - Home: index.md + - Getting Started: + - First steps: getting-started/index.md + - Installation: getting-started/installation.md + - Signature Verification: getting-started/signature-verification.md + - FAQ: getting-started/faq.md + - Tutorials: + - Overview: tutorials/overview.md + - CI/CD: + - Overview: tutorials/integrations/index.md + - GitHub Actions: tutorials/integrations/github-actions.md + - CircleCI: tutorials/integrations/circleci.md + - Travis CI: tutorials/integrations/travis-ci.md + - GitLab CI: tutorials/integrations/gitlab-ci.md + - Bitbucket Pipelines: tutorials/integrations/bitbucket.md + - AWS CodePipeline: tutorials/integrations/aws-codepipeline.md + - AWS Security Hub: tutorials/integrations/aws-security-hub.md + - Azure: tutorials/integrations/azure-devops.md + - Kubernetes: + - Cluster Scanning: tutorials/kubernetes/cluster-scanning.md + - Kyverno: tutorials/kubernetes/kyverno.md + - GitOps: tutorials/kubernetes/gitops.md + - Misconfiguration: + - Terraform scanning: tutorials/misconfiguration/terraform.md + - Custom Checks with Rego: tutorials/misconfiguration/custom-checks.md + - Signing: + - Vulnerability Scan Record Attestation: tutorials/signing/vuln-attestation.md + - Shell: + - Completion: tutorials/shell/shell-completion.md + - Additional Resources: + - Additional Resources: tutorials/additional-resources/references.md + - Community References: tutorials/additional-resources/community.md + - CKS Reference: tutorials/additional-resources/cks.md + - User Guide: + - Overview: guide/index.md + - Target: + - Container Image: guide/target/container_image.md + - Filesystem: guide/target/filesystem.md + - Rootfs: guide/target/rootfs.md + - Code Repository: guide/target/repository.md + - Virtual Machine Image: guide/target/vm.md + - Kubernetes: guide/target/kubernetes.md + - SBOM: guide/target/sbom.md + - Scanner: + - Vulnerability: guide/scanner/vulnerability.md + - Misconfiguration: + - Overview: guide/scanner/misconfiguration/index.md + - Configuration: guide/scanner/misconfiguration/config/config.md + - Policy: + - Built-in Checks: guide/scanner/misconfiguration/check/builtin.md + - Custom Checks: + - Overview: guide/scanner/misconfiguration/custom/index.md + - Data: guide/scanner/misconfiguration/custom/data.md + - Combine: guide/scanner/misconfiguration/custom/combine.md + - Selectors: guide/scanner/misconfiguration/custom/selectors.md + - Schemas: guide/scanner/misconfiguration/custom/schema.md + - Testing: guide/scanner/misconfiguration/custom/testing.md + - Debugging Policies: guide/scanner/misconfiguration/custom/debug.md + - Contribute Checks: guide/scanner/misconfiguration/custom/contribute-checks.md + - Secret: guide/scanner/secret.md + - License: guide/scanner/license.md + - Coverage: + - Overview: guide/coverage/index.md + - OS: + - Overview: guide/coverage/os/index.md + - AlmaLinux: guide/coverage/os/alma.md + - Alpine Linux: guide/coverage/os/alpine.md + - Amazon Linux: guide/coverage/os/amazon.md + - Azure Linux (CBL-Mariner): guide/coverage/os/azure.md + - Bottlerocket: guide/coverage/os/bottlerocket.md + - CentOS: guide/coverage/os/centos.md + - Chainguard: guide/coverage/os/chainguard.md + - CoreOS: guide/coverage/os/coreos.md + - Debian: guide/coverage/os/debian.md + - Echo: guide/coverage/os/echo.md + - MinimOS: guide/coverage/os/minimos.md + - Oracle Linux: guide/coverage/os/oracle.md + - Photon OS: guide/coverage/os/photon.md + - Red Hat: guide/coverage/os/rhel.md + - Rocky Linux: guide/coverage/os/rocky.md + - SUSE: guide/coverage/os/suse.md + - Ubuntu: guide/coverage/os/ubuntu.md + - Wolfi: guide/coverage/os/wolfi.md + - Google Distroless (Images): guide/coverage/os/google-distroless.md + - Language: + - Overview: guide/coverage/language/index.md + - C/C++: guide/coverage/language/c.md + - Dart: guide/coverage/language/dart.md + - .NET: guide/coverage/language/dotnet.md + - Elixir: guide/coverage/language/elixir.md + - Go: guide/coverage/language/golang.md + - Java: guide/coverage/language/java.md + - Julia: guide/coverage/language/julia.md + - Node.js: guide/coverage/language/nodejs.md + - PHP: guide/coverage/language/php.md + - Python: guide/coverage/language/python.md + - Ruby: guide/coverage/language/ruby.md + - Rust: guide/coverage/language/rust.md + - Swift: guide/coverage/language/swift.md + - IaC: + - Overview: guide/coverage/iac/index.md + - Ansible: guide/coverage/iac/ansible.md + - Azure ARM Template: guide/coverage/iac/azure-arm.md + - CloudFormation: guide/coverage/iac/cloudformation.md + - Docker: guide/coverage/iac/docker.md + - Helm: guide/coverage/iac/helm.md + - Kubernetes: guide/coverage/iac/kubernetes.md + - Terraform: guide/coverage/iac/terraform.md + - Others: + - Overview: guide/coverage/others/index.md + - ActiveState Images: guide/coverage/others/activestate.md + - Bitnami Images: guide/coverage/others/bitnami.md + - Conda: guide/coverage/others/conda.md + - Root.io Images: guide/coverage/others/rootio.md + - Seal Security: guide/coverage/others/seal.md + - RPM Archives: guide/coverage/others/rpm.md + - Kubernetes: guide/coverage/kubernetes.md + - Configuration: + - Overview: guide/configuration/index.md + - Filtering: guide/configuration/filtering.md + - Selecting Files: guide/configuration/skipping.md + - Reporting: guide/configuration/reporting.md + - Cache: guide/configuration/cache.md + - Databases: guide/configuration/db.md + - Others: guide/configuration/others.md + - Supply Chain: + - SBOM: guide/supply-chain/sbom.md + - Attestation: + - SBOM: guide/supply-chain/attestation/sbom.md + - Cosign Vulnerability Scan Record: guide/supply-chain/attestation/vuln.md + - SBOM Attestation in Rekor: guide/supply-chain/attestation/rekor.md + - VEX: + - Overview: guide/supply-chain/vex/index.md + - VEX Repository: guide/supply-chain/vex/repo.md + - Local VEX Files: guide/supply-chain/vex/file.md + - VEX SBOM Reference: guide/supply-chain/vex/sbom-ref.md + - VEX Attestation: guide/supply-chain/vex/oci.md + - Compliance: + - Built-in Compliance: guide/compliance/compliance.md + - Custom Compliance: guide/compliance/contrib-compliance.md + - Plugins: + - Overview: guide/plugin/index.md + - User guide: guide/plugin/user-guide.md + - Developer guide: guide/plugin/developer-guide.md + - Advanced: + - Modules: guide/advanced/modules.md + - Connectivity and Network considerations: guide/advanced/air-gap.md + - Self-Hosting Trivy's Databases: guide/advanced/self-hosting.md + - Container Image: + - Embed in Dockerfile: guide/advanced/container/embed-in-dockerfile.md + - Unpacked container image filesystem: guide/advanced/container/unpacked-filesystem.md + - Private Docker Registries: + - Overview: guide/advanced/private-registries/index.md + - Docker Hub: guide/advanced/private-registries/docker-hub.md + - AWS ECR (Elastic Container Registry): guide/advanced/private-registries/ecr.md + - GAR (Google Artifact Registry): guide/advanced/private-registries/gar.md + - ACR (Azure Container Registry): guide/advanced/private-registries/acr.md + - Self-Hosted: guide/advanced/private-registries/self.md + - Usage Telemetry: guide/advanced/telemetry.md + - References: + - Configuration: + - CLI: + - Overview: guide/references/configuration/cli/trivy.md + - Clean: guide/references/configuration/cli/trivy_clean.md + - Config: guide/references/configuration/cli/trivy_config.md + - Convert: guide/references/configuration/cli/trivy_convert.md + - Filesystem: guide/references/configuration/cli/trivy_filesystem.md + - Image: guide/references/configuration/cli/trivy_image.md + - Kubernetes: guide/references/configuration/cli/trivy_kubernetes.md + - Module: + - Module: guide/references/configuration/cli/trivy_module.md + - Module Install: guide/references/configuration/cli/trivy_module_install.md + - Module Uninstall: guide/references/configuration/cli/trivy_module_uninstall.md + - Plugin: + - Plugin: guide/references/configuration/cli/trivy_plugin.md + - Plugin Info: guide/references/configuration/cli/trivy_plugin_info.md + - Plugin Install: guide/references/configuration/cli/trivy_plugin_install.md + - Plugin List: guide/references/configuration/cli/trivy_plugin_list.md + - Plugin Run: guide/references/configuration/cli/trivy_plugin_run.md + - Plugin Uninstall: guide/references/configuration/cli/trivy_plugin_uninstall.md + - Plugin Update: guide/references/configuration/cli/trivy_plugin_update.md + - Plugin Upgrade: guide/references/configuration/cli/trivy_plugin_upgrade.md + - Plugin Search: guide/references/configuration/cli/trivy_plugin_search.md + - Registry: + - Registry: guide/references/configuration/cli/trivy_registry.md + - Registry Login: guide/references/configuration/cli/trivy_registry_login.md + - Registry Logout: guide/references/configuration/cli/trivy_registry_logout.md + - Repository: guide/references/configuration/cli/trivy_repository.md + - Rootfs: guide/references/configuration/cli/trivy_rootfs.md + - SBOM: guide/references/configuration/cli/trivy_sbom.md + - Server: guide/references/configuration/cli/trivy_server.md + - Version: guide/references/configuration/cli/trivy_version.md + - VEX: + - VEX: guide/references/configuration/cli/trivy_vex.md + - VEX Download: guide/references/configuration/cli/trivy_vex_repo_download.md + - VEX Init: guide/references/configuration/cli/trivy_vex_repo_init.md + - VEX List: guide/references/configuration/cli/trivy_vex_repo_list.md + - VEX Repo: guide/references/configuration/cli/trivy_vex_repo.md + - VM: guide/references/configuration/cli/trivy_vm.md + - Config file: guide/references/configuration/config-file.md + - Modes: + - Standalone: guide/references/modes/standalone.md + - Client/Server: guide/references/modes/client-server.md + - Troubleshooting: guide/references/troubleshooting.md + - Terminology: guide/references/terminology.md + - Abbreviations: guide/references/abbreviations.md + - Ecosystem: + - Overview: ecosystem/index.md + - CI/CD: ecosystem/cicd.md + - IDE and Dev tools: ecosystem/ide.md + - Production and Clouds: ecosystem/prod.md + - Reporting: ecosystem/reporting.md + - Contributing: + - Principles: community/principles.md + - How to contribute: + - Issues: community/contribute/issue.md + - Discussions: community/contribute/discussion.md + - Pull Requests: community/contribute/pr.md + - Contribute Rego Checks: + - Overview: community/contribute/checks/overview.md + - Add Service Support: community/contribute/checks/service-support.md + - Contribute Vulnerability Data Sources: + - Overview: community/contribute/vulnerability-database/overview.md + - Add Vulnerability Advisory Source: community/contribute/vulnerability-database/add-vulnerability-source.md + - Maintainer: + - PR Review: community/maintainer/pr-review.md + - Release Flow: community/maintainer/release-flow.md + - Backporting: community/maintainer/backporting.md + - Help Wanted: community/maintainer/help-wanted.md + - Triage: community/maintainer/triage.md + - Enterprise: + - Comparison: commercial/compare.md + - Contact Us: commercial/contact.md +theme: + name: material + custom_dir: docs/overrides + language: "en" + logo: imgs/logo-white.svg + features: + - navigation.tabs + - navigation.tabs.sticky + - navigation.sections + - navigation.footer + - content.action.edit + - content.tabs.link + - content.code.annotate + - content.code.copy + font: + text: Inter + +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - toc: + permalink: true + - pymdownx.highlight + - pymdownx.details + - pymdownx.magiclink + - pymdownx.snippets + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + +extra: + generator: false + version: + method: mike + provider: mike + default: latest + social: + - icon: fontawesome/brands/x-twitter + link: https://twitter.com/AquaTrivy + - icon: fontawesome/brands/github + link: https://github.com/aquasecurity/trivy + analytics: + provider: google + property: G-V9LJGFH7GX + +plugins: + - search + - macros diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go new file mode 100644 index 000000000000..7d804fdf33cb --- /dev/null +++ b/pkg/attestation/attestation.go @@ -0,0 +1,51 @@ +package attestation + +import ( + "bytes" + "encoding/base64" + "encoding/json" + + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "golang.org/x/xerrors" +) + +// CosignPredicate specifies the format of the Custom Predicate. +// Cosign uses this structure when creating an SBOM attestation. +// cf. https://github.com/sigstore/cosign/blob/e0547cff64f98585a837a524ff77ff6b47ff5609/pkg/cosign/attestation/attestation.go#L39-L43 +type CosignPredicate struct { + Data any +} + +// SigstoreBundle represents the structure of a Sigstore bundle containing a DSSE envelope. +// This format is used by Cosign v3+ with the new bundle format. +// cf. https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md +type SigstoreBundle struct { + DSSEEnvelope Statement `json:"dsseEnvelope"` +} + +// Statement holds in-toto statement headers and the predicate. +type Statement in_toto.Statement + +func (s *Statement) UnmarshalJSON(b []byte) error { + var envelope dsse.Envelope + err := json.NewDecoder(bytes.NewReader(b)).Decode(&envelope) + if err != nil { + return xerrors.Errorf("failed to decode as a dsse envelope: %w", err) + } + if envelope.PayloadType != in_toto.PayloadType { + return xerrors.Errorf("invalid attestation payload type: %s", envelope.PayloadType) + } + + decoded, err := base64.StdEncoding.DecodeString(envelope.Payload) + if err != nil { + return xerrors.Errorf("failed to decode attestation payload: %w", err) + } + + statement := (*in_toto.Statement)(s) + if err = json.NewDecoder(bytes.NewReader(decoded)).Decode(statement); err != nil { + return xerrors.Errorf("failed to decode attestation payload as in-toto statement: %w", err) + } + + return nil +} diff --git a/pkg/attestation/attestation_test.go b/pkg/attestation/attestation_test.go new file mode 100644 index 000000000000..0233ba30cf78 --- /dev/null +++ b/pkg/attestation/attestation_test.go @@ -0,0 +1,55 @@ +package attestation_test + +import ( + "encoding/json" + "os" + "testing" + + "github.com/in-toto/in-toto-golang/in_toto" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/attestation" +) + +func TestStatement_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + inputFile string + want attestation.Statement + }{ + { + name: "happy path", + inputFile: "testdata/attestation.json", + want: attestation.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: "https://in-toto.io/Statement/v0.1", + PredicateType: "cosign.sigstore.dev/attestation/v1", + Subject: []in_toto.Subject{ + { + Name: "ghcr.io/aquasecurity/trivy-test-images", // Defined in the attestations.json file + Digest: slsa.DigestSet{ + "sha256": "72c42ed48c3a2db31b7dafe17d275b634664a708d901ec9fd57b1529280f01fb", + }, + }, + }, + }, + Predicate: &attestation.CosignPredicate{ + Data: "foo\n", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + got := attestation.Statement{Predicate: &attestation.CosignPredicate{}} + err = json.NewDecoder(f).Decode(&got) + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/attestation/sbom/rekor.go b/pkg/attestation/sbom/rekor.go new file mode 100644 index 000000000000..a5872152c8f5 --- /dev/null +++ b/pkg/attestation/sbom/rekor.go @@ -0,0 +1,94 @@ +package sbom + +import ( + "bytes" + "context" + "encoding/json" + "errors" + + "github.com/in-toto/in-toto-golang/in_toto" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/attestation" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rekor" +) + +var ErrNoSBOMAttestation = xerrors.New("no SBOM attestation found") + +type Rekor struct { + client *rekor.Client +} + +func NewRekor(url string) (Rekor, error) { + c, err := rekor.NewClient(url) + if err != nil { + return Rekor{}, xerrors.Errorf("rekor client error: %w", err) + } + return Rekor{ + client: c, + }, nil +} + +func (r *Rekor) RetrieveSBOM(ctx context.Context, digest string) ([]byte, error) { + entryIDs, err := r.client.Search(ctx, digest) + if err != nil { + return nil, xerrors.Errorf("failed to search rekor records: %w", err) + } else if len(entryIDs) == 0 { + return nil, ErrNoSBOMAttestation + } + + log.Debug("Found matching Rekor entries", log.Any("entry_ids", entryIDs)) + + for _, ids := range lo.Chunk[rekor.EntryID](entryIDs, rekor.MaxGetEntriesLimit) { + entries, err := r.client.GetEntries(ctx, ids) + if err != nil { + return nil, xerrors.Errorf("failed to get entries: %w", err) + } + + for _, entry := range entries { + ref, err := r.inspectRecord(entry) + if errors.Is(err, ErrNoSBOMAttestation) { + continue + } else if err != nil { + return nil, xerrors.Errorf("rekor record inspection error: %w", err) + } + return ref, nil + } + } + return nil, ErrNoSBOMAttestation +} + +func (r *Rekor) inspectRecord(entry rekor.Entry) ([]byte, error) { + // TODO: Trivy SBOM should take precedence + raw, err := r.parseStatement(entry) + if err != nil { + return nil, err + } + return raw, nil +} + +func (r *Rekor) parseStatement(entry rekor.Entry) (json.RawMessage, error) { + // Skip base64-encoded attestation + if bytes.HasPrefix(entry.Statement, []byte(`eyJ`)) { + return nil, ErrNoSBOMAttestation + } + + // Parse statement of in-toto attestation + var raw json.RawMessage + statement := &in_toto.Statement{ + Predicate: &attestation.CosignPredicate{ + Data: &raw, // Extract CycloneDX or SPDX + }, + } + if err := json.Unmarshal(entry.Statement, &statement); err != nil { + return nil, xerrors.Errorf("attestation parse error: %w", err) + } + + // TODO: add support for SPDX + if statement.PredicateType != in_toto.PredicateCycloneDX { + return nil, xerrors.Errorf("unsupported predicate type %s: %w", statement.PredicateType, ErrNoSBOMAttestation) + } + return raw, nil +} diff --git a/pkg/attestation/sbom/rekor_test.go b/pkg/attestation/sbom/rekor_test.go new file mode 100644 index 000000000000..381a52306e01 --- /dev/null +++ b/pkg/attestation/sbom/rekor_test.go @@ -0,0 +1,52 @@ +package sbom_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/attestation/sbom" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rekortest" +) + +func TestRekor_RetrieveSBOM(t *testing.T) { + tests := []struct { + name string + digest string + want string + wantErr string + }{ + { + name: "happy path", + digest: "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03", + want: `{"bomFormat":"CycloneDX","specVersion":"1.5","version":2}`, + }, + { + name: "404", + digest: "sha256:unknown", + wantErr: "failed to search", + }, + } + + log.InitLogger(false, true) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := rekortest.NewServer(t) + defer ts.Close() + + // Set the testing URL + rc, err := sbom.NewRekor(ts.URL()) + require.NoError(t, err) + + got, err := rc.RetrieveSBOM(t.Context(), tt.digest) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, string(got)) + }) + } +} diff --git a/pkg/attestation/testdata/attestation.json b/pkg/attestation/testdata/attestation.json new file mode 100644 index 000000000000..2e9dbf326a24 --- /dev/null +++ b/pkg/attestation/testdata/attestation.json @@ -0,0 +1 @@ +{"payloadType":"application/vnd.in-toto+json","payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjb3NpZ24uc2lnc3RvcmUuZGV2L2F0dGVzdGF0aW9uL3YxIiwic3ViamVjdCI6W3sibmFtZSI6ImdoY3IuaW8vYXF1YXNlY3VyaXR5L3RyaXZ5LXRlc3QtaW1hZ2VzIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjcyYzQyZWQ0OGMzYTJkYjMxYjdkYWZlMTdkMjc1YjYzNDY2NGE3MDhkOTAxZWM5ZmQ1N2IxNTI5MjgwZjAxZmIifX1dLCJwcmVkaWNhdGUiOnsiRGF0YSI6ImZvb1xuIiwiVGltZXN0YW1wIjoiMjAyMi0wOC0wM1QxMzowODoyN1oifX0=","signatures":[{"keyid":"","sig":"MEUCIQClJhJ2mS78MWy4L32wxd+8gPXYwpvyn0nmuY9r5t8iiAIgHKKoIJbKAKQ8i/bgN76ocuGhwUMdbgqpgKF0yFfPfGI="}]} diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go new file mode 100644 index 000000000000..9bd96217066b --- /dev/null +++ b/pkg/cache/cache.go @@ -0,0 +1,51 @@ +package cache + +import ( + "context" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const ( + scanCacheDirName = "fanal" + + // artifactBucket stores artifact information with artifact ID such as image ID + artifactBucket = "artifact" + // blobBucket stores os, package and library information per blob ID such as layer ID + blobBucket = "blob" +) + +type Cache interface { + ArtifactCache + LocalArtifactCache +} + +// ArtifactCache uses local or remote cache +type ArtifactCache interface { + // MissingBlobs returns missing blob IDs such as layer IDs in cache + MissingBlobs(ctx context.Context, artifactID string, blobIDs []string) (missingArtifact bool, missingBlobIDs []string, err error) + + // PutArtifact stores artifact information such as image metadata in cache + PutArtifact(ctx context.Context, artifactID string, artifactInfo types.ArtifactInfo) (err error) + + // PutBlob stores blob information such as layer information in local cache + PutBlob(ctx context.Context, blobID string, blobInfo types.BlobInfo) (err error) + + // DeleteBlobs removes blobs by IDs + DeleteBlobs(ctx context.Context, blobIDs []string) error +} + +// LocalArtifactCache always uses local cache +type LocalArtifactCache interface { + // GetArtifact gets artifact information such as image metadata from local cache + GetArtifact(ctx context.Context, artifactID string) (artifactInfo types.ArtifactInfo, err error) + + // GetBlob gets blob information such as layer data from local cache + GetBlob(ctx context.Context, blobID string) (blobInfo types.BlobInfo, err error) + + // Close closes the local database + Close() (err error) + + // Clear deletes the local database + Clear(ctx context.Context) (err error) +} diff --git a/pkg/cache/client.go b/pkg/cache/client.go new file mode 100644 index 000000000000..667900366e3e --- /dev/null +++ b/pkg/cache/client.go @@ -0,0 +1,73 @@ +package cache + +import ( + "strings" + "time" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" +) + +const ( + TypeUnknown Type = "unknown" + TypeFS Type = "fs" + TypeRedis Type = "redis" + TypeMemory Type = "memory" +) + +type Type string + +type Options struct { + Backend string + CacheDir string + RedisCACert string + RedisCert string + RedisKey string + RedisTLS bool + TTL time.Duration +} + +func NewType(backend string) Type { + // "redis://" or "fs" are allowed for now + // An empty value is also allowed for testability + switch { + case strings.HasPrefix(backend, "redis://"): + return TypeRedis + case backend == "fs", backend == "": + return TypeFS + case backend == "memory": + return TypeMemory + default: + return TypeUnknown + } +} + +// New returns a new cache client +func New(opts Options) (Cache, func(), error) { + cleanup := func() {} // To avoid panic + + var cache Cache + t := NewType(opts.Backend) + log.Debug("Initializing scan cache...", log.String("type", string(t))) + switch t { + case TypeRedis: + redisCache, err := NewRedisCache(opts.Backend, opts.RedisCACert, opts.RedisCert, opts.RedisKey, opts.RedisTLS, opts.TTL) + if err != nil { + return nil, cleanup, xerrors.Errorf("unable to initialize redis cache: %w", err) + } + cache = redisCache + case TypeFS: + // standalone mode + fsCache, err := NewFSCache(opts.CacheDir) + if err != nil { + return nil, cleanup, xerrors.Errorf("unable to initialize fs cache: %w", err) + } + cache = fsCache + case TypeMemory: + cache = NewMemoryCache() + default: + return nil, cleanup, xerrors.Errorf("unknown cache type: %s", t) + } + return cache, func() { _ = cache.Close() }, nil +} diff --git a/pkg/cache/client_test.go b/pkg/cache/client_test.go new file mode 100644 index 000000000000..c72eb3de4d13 --- /dev/null +++ b/pkg/cache/client_test.go @@ -0,0 +1,121 @@ +package cache_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/cache" +) + +func TestNew(t *testing.T) { + tests := []struct { + name string + opts cache.Options + wantType any + wantErr string + }{ + { + name: "fs backend", + opts: cache.Options{ + Backend: "fs", + CacheDir: "/tmp/cache", + }, + wantType: cache.FSCache{}, + }, + { + name: "redis backend", + opts: cache.Options{ + Backend: "redis://localhost:6379", + }, + wantType: cache.RedisCache{}, + }, + { + name: "unknown backend", + opts: cache.Options{ + Backend: "unknown", + }, + wantErr: "unknown cache type", + }, + { + name: "invalid redis URL", + opts: cache.Options{ + Backend: "redis://invalid-url:foo/bar", + }, + wantErr: "failed to parse Redis URL", + }, + { + name: "incomplete TLS options", + opts: cache.Options{ + Backend: "redis://localhost:6379", + RedisCACert: "testdata/ca-cert.pem", + RedisTLS: true, + }, + wantErr: "you must provide Redis CA, cert and key file path when using TLS", + }, + { + name: "invalid TLS file paths", + opts: cache.Options{ + Backend: "redis://localhost:6379", + RedisCACert: "testdata/non-existent-ca-cert.pem", + RedisCert: "testdata/non-existent-cert.pem", + RedisKey: "testdata/non-existent-key.pem", + RedisTLS: true, + }, + wantErr: "failed to get TLS config", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, cleanup, err := cache.New(tt.opts) + defer cleanup() + + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.NotNil(t, c) + assert.IsType(t, tt.wantType, c) + }) + } +} + +func TestNewType(t *testing.T) { + tests := []struct { + name string + backend string + wantType cache.Type + }{ + { + name: "redis backend", + backend: "redis://localhost:6379", + wantType: cache.TypeRedis, + }, + { + name: "fs backend", + backend: "fs", + wantType: cache.TypeFS, + }, + { + name: "empty backend", + backend: "", + wantType: cache.TypeFS, + }, + { + name: "unknown backend", + backend: "unknown", + wantType: cache.TypeUnknown, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := cache.NewType(tt.backend) + assert.Equal(t, tt.wantType, got) + }) + } +} diff --git a/pkg/cache/dir.go b/pkg/cache/dir.go new file mode 100644 index 000000000000..2a67d269bdfe --- /dev/null +++ b/pkg/cache/dir.go @@ -0,0 +1,15 @@ +package cache + +import ( + "os" + "path/filepath" +) + +// DefaultDir returns/creates the cache-dir to be used for trivy operations +func DefaultDir() string { + tmpDir, err := os.UserCacheDir() + if err != nil { + tmpDir = os.TempDir() + } + return filepath.Join(tmpDir, "trivy") +} diff --git a/pkg/cache/fs.go b/pkg/cache/fs.go new file mode 100644 index 000000000000..017406a4f375 --- /dev/null +++ b/pkg/cache/fs.go @@ -0,0 +1,224 @@ +package cache + +import ( + "context" + "encoding/json" + "errors" + "os" + "path/filepath" + "time" + + "github.com/hashicorp/go-multierror" + bolt "go.etcd.io/bbolt" + bberrors "go.etcd.io/bbolt/errors" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const defaultFSCacheTimeout = 5 * time.Second + +var _ Cache = &FSCache{} + +type FSCache struct { + db *bolt.DB + directory string +} + +func NewFSCache(cacheDir string) (FSCache, error) { + dir := filepath.Join(cacheDir, scanCacheDirName) + if err := os.MkdirAll(dir, 0o700); err != nil { + return FSCache{}, xerrors.Errorf("failed to create cache dir: %w", err) + } + + db, err := bolt.Open(filepath.Join(dir, "fanal.db"), 0o600, &bolt.Options{ + Timeout: defaultFSCacheTimeout, + }) + if err != nil { + // Check if the error is due to timeout (database locked by another process) + if errors.Is(err, bberrors.ErrTimeout) { + return FSCache{}, xerrors.Errorf("cache may be in use by another process: %w", err) + } + return FSCache{}, xerrors.Errorf("unable to open cache DB: %w", err) + } + + err = db.Update(func(tx *bolt.Tx) error { + for _, bucket := range []string{ + artifactBucket, + blobBucket, + } { + if _, err := tx.CreateBucketIfNotExists([]byte(bucket)); err != nil { + return xerrors.Errorf("unable to create %s bucket: %w", bucket, err) + } + } + return nil + }) + if err != nil { + return FSCache{}, xerrors.Errorf("DB error: %w", err) + } + + return FSCache{ + db: db, + directory: dir, + }, nil +} + +// GetBlob gets blob information such as layer data from local cache +func (fs FSCache) GetBlob(_ context.Context, blobID string) (types.BlobInfo, error) { + var blobInfo types.BlobInfo + err := fs.db.View(func(tx *bolt.Tx) error { + var err error + blobBucket := tx.Bucket([]byte(blobBucket)) + blobInfo, err = fs.getBlob(blobBucket, blobID) + if err != nil { + return xerrors.Errorf("failed to get blob from the cache: %w", err) + } + return nil + }) + if err != nil { + return types.BlobInfo{}, xerrors.Errorf("DB error: %w", err) + } + return blobInfo, nil +} + +func (fs FSCache) getBlob(blobBucket *bolt.Bucket, diffID string) (types.BlobInfo, error) { + b := blobBucket.Get([]byte(diffID)) + + var l types.BlobInfo + if err := json.Unmarshal(b, &l); err != nil { + return types.BlobInfo{}, xerrors.Errorf("JSON unmarshal error: %w", err) + } + return l, nil +} + +// PutBlob stores blob information such as layer information in local cache +func (fs FSCache) PutBlob(_ context.Context, blobID string, blobInfo types.BlobInfo) error { + b, err := json.Marshal(blobInfo) + if err != nil { + return xerrors.Errorf("unable to marshal blob JSON (%s): %w", blobID, err) + } + err = fs.db.Update(func(tx *bolt.Tx) error { + blobBucket := tx.Bucket([]byte(blobBucket)) + err = blobBucket.Put([]byte(blobID), b) + if err != nil { + return xerrors.Errorf("unable to store blob information in cache (%s): %w", blobID, err) + } + return nil + }) + if err != nil { + return xerrors.Errorf("DB update error: %w", err) + } + return nil +} + +// GetArtifact gets artifact information such as image metadata from local cache +func (fs FSCache) GetArtifact(_ context.Context, artifactID string) (types.ArtifactInfo, error) { + var blob []byte + err := fs.db.View(func(tx *bolt.Tx) error { + artifactBucket := tx.Bucket([]byte(artifactBucket)) + blob = artifactBucket.Get([]byte(artifactID)) + return nil + }) + if err != nil { + return types.ArtifactInfo{}, xerrors.Errorf("DB error: %w", err) + } + + var info types.ArtifactInfo + if err := json.Unmarshal(blob, &info); err != nil { + return types.ArtifactInfo{}, xerrors.Errorf("JSON unmarshal error: %w", err) + } + return info, nil +} + +// DeleteBlobs removes blobs by IDs +func (fs FSCache) DeleteBlobs(_ context.Context, blobIDs []string) error { + var errs error + err := fs.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(blobBucket)) + for _, blobID := range blobIDs { + if err := bucket.Delete([]byte(blobID)); err != nil { + errs = multierror.Append(errs, err) + } + } + return nil + }) + if err != nil { + return xerrors.Errorf("DB delete error: %w", err) + } + return errs +} + +// PutArtifact stores artifact information such as image metadata in local cache +func (fs FSCache) PutArtifact(_ context.Context, artifactID string, artifactInfo types.ArtifactInfo) (err error) { + b, err := json.Marshal(artifactInfo) + if err != nil { + return xerrors.Errorf("unable to marshal artifact JSON (%s): %w", artifactID, err) + } + + err = fs.db.Update(func(tx *bolt.Tx) error { + artifactBucket := tx.Bucket([]byte(artifactBucket)) + err = artifactBucket.Put([]byte(artifactID), b) + if err != nil { + return xerrors.Errorf("unable to store artifact information in cache (%s): %w", artifactID, err) + } + return nil + }) + if err != nil { + return xerrors.Errorf("DB update error: %w", err) + } + return nil +} + +// MissingBlobs returns missing blob IDs such as layer IDs +func (fs FSCache) MissingBlobs(ctx context.Context, artifactID string, blobIDs []string) (bool, []string, error) { + var missingArtifact bool + var missingBlobIDs []string + err := fs.db.View(func(tx *bolt.Tx) error { + blobBucket := tx.Bucket([]byte(blobBucket)) + for _, blobID := range blobIDs { + blobInfo, err := fs.getBlob(blobBucket, blobID) + if err != nil { + // error means cache missed blob info + missingBlobIDs = append(missingBlobIDs, blobID) + continue + } + if blobInfo.SchemaVersion != types.BlobJSONSchemaVersion { + missingBlobIDs = append(missingBlobIDs, blobID) + } + } + return nil + }) + if err != nil { + return false, nil, xerrors.Errorf("DB error: %w", err) + } + + // get artifact info + artifactInfo, err := fs.GetArtifact(ctx, artifactID) + if err != nil { + // error means cache missed artifact info + return true, missingBlobIDs, nil + } + if artifactInfo.SchemaVersion != types.ArtifactJSONSchemaVersion { + missingArtifact = true + } + return missingArtifact, missingBlobIDs, nil +} + +// Close closes the database +func (fs FSCache) Close() error { + if err := fs.db.Close(); err != nil { + return xerrors.Errorf("unable to close DB: %w", err) + } + return nil +} + +// Clear removes the database +func (fs FSCache) Clear(_ context.Context) error { + if err := fs.Close(); err != nil { + return err + } + if err := os.RemoveAll(fs.directory); err != nil { + return xerrors.Errorf("failed to remove cache: %w", err) + } + return nil +} diff --git a/pkg/cache/fs_test.go b/pkg/cache/fs_test.go new file mode 100644 index 000000000000..25642c46ccfd --- /dev/null +++ b/pkg/cache/fs_test.go @@ -0,0 +1,492 @@ +package cache + +import ( + "fmt" + "io" + "os" + "path/filepath" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + bolt "go.etcd.io/bbolt" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func newTempDB(t *testing.T, dbPath string) (string, error) { + dir := t.TempDir() + if dbPath != "" { + d := filepath.Join(dir, "fanal") + if err := os.MkdirAll(d, 0o700); err != nil { + return "", err + } + + dst := filepath.Join(d, "fanal.db") + if _, err := copyFile(dbPath, dst); err != nil { + return "", err + } + } + + return dir, nil +} + +func copyFile(src, dst string) (int64, error) { + sourceFileStat, err := os.Stat(src) + if err != nil { + return 0, err + } + + if !sourceFileStat.Mode().IsRegular() { + return 0, fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return 0, err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return 0, err + } + defer destination.Close() + n, err := io.Copy(destination, source) + return n, err +} + +func TestFSCache_GetBlob(t *testing.T) { + type args struct { + layerID string + } + tests := []struct { + name string + dbPath string + args args + want types.BlobInfo + wantErr bool + }{ + { + name: "happy path", + dbPath: "testdata/fanal.db", + args: args{ + layerID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7/11101", + }, + want: types.BlobInfo{ + SchemaVersion: 2, + OS: types.OS{ + Family: "alpine", + Name: "3.10", + }, + }, + }, + { + name: "sad path", + dbPath: "testdata/fanal.db", + args: args{ + layerID: "sha256:unknown", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, err := newTempDB(t, tt.dbPath) + require.NoError(t, err) + + fs, err := NewFSCache(tmpDir) + require.NoError(t, err) + defer func() { + _ = fs.Clear(t.Context()) + _ = fs.Close() + }() + + got, err := fs.GetBlob(t.Context(), tt.args.layerID) + assert.Equal(t, tt.wantErr, err != nil, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestFSCache_PutBlob(t *testing.T) { + type fields struct { + db *bolt.DB + directory string + } + type args struct { + diffID string + layerInfo types.BlobInfo + } + tests := []struct { + name string + fields fields + args args + want string + wantLayerID string + wantErr string + }{ + { + name: "happy path", + args: args{ + diffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + layerInfo: types.BlobInfo{ + SchemaVersion: 1, + OS: types.OS{ + Family: "alpine", + Name: "3.10", + }, + Repository: &types.Repository{ + Family: "alpine", + Release: "3.10", + }, + Size: 1000, + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + want: ` + { + "SchemaVersion": 1, + "Size": 1000, + "DiffID": "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + "OS": { + "Family": "alpine", + "Name": "3.10" + }, + "Repository": { + "Family": "alpine", + "Release": "3.10" + } + }`, + wantLayerID: "", + }, + { + name: "happy path: different decompressed layer ID", + args: args{ + diffID: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + layerInfo: types.BlobInfo{ + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec", + OpaqueDirs: []string{"php-app/"}, + WhiteoutFiles: []string{"etc/foobar"}, + OS: types.OS{ + Family: "alpine", + Name: "3.10", + }, + Repository: &types.Repository{ + Family: "alpine", + Release: "3.10", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: types.Packages{ + { + Name: "musl", + Version: "1.1.22-r3", + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: types.Packages{ + { + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + }, + { + Name: "guzzlehttp/promises", + Version: "v1.3.1", + }, + }, + }, + }, + }, + }, + want: ` + { + "SchemaVersion": 1, + "OS": { + "Family": "alpine", + "Name": "3.10" + }, + "Repository": { + "Family": "alpine", + "Release": "3.10" + }, + "PackageInfos": [ + { + "FilePath": "lib/apk/db/installed", + "Packages": [ + { + "Name": "musl", + "Version": "1.1.22-r3" + } + ] + } + ], + "Applications": [ + { + "Type": "composer", + "FilePath": "php-app/composer.lock", + "Packages": [ + { + "Name":"guzzlehttp/guzzle", + "Version":"6.2.0" + }, + { + "Name":"guzzlehttp/promises", + "Version":"v1.3.1" + } + ] + } + ], + "Size": 1000, + "Digest": "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + "DiffID": "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec", + "OpaqueDirs": [ + "php-app/" + ], + "WhiteoutFiles": [ + "etc/foobar" + ] + }`, + wantLayerID: "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec", + }, + { + name: "sad path closed DB", + args: args{ + diffID: "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec/11111", + }, + wantErr: "database not open", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, err := newTempDB(t, "") + require.NoError(t, err) + + fs, err := NewFSCache(tmpDir) + require.NoError(t, err) + defer func() { + _ = fs.Clear(t.Context()) + _ = fs.Close() + }() + + if strings.HasPrefix(tt.name, "sad") { + require.NoError(t, fs.Close()) + } + + err = fs.PutBlob(t.Context(), tt.args.diffID, tt.args.layerInfo) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err, tt.name) + + fs.db.View(func(tx *bolt.Tx) error { + layerBucket := tx.Bucket([]byte(blobBucket)) + b := layerBucket.Get([]byte(tt.args.diffID)) + assert.JSONEq(t, tt.want, string(b)) + + return nil + }) + }) + } +} + +func TestFSCache_PutArtifact(t *testing.T) { + type args struct { + imageID string + imageConfig types.ArtifactInfo + } + tests := []struct { + name string + args args + want string + wantErr string + }{ + { + name: "happy path", + args: args{ + imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", + imageConfig: types.ArtifactInfo{ + SchemaVersion: 1, + Architecture: "amd64", + Created: time.Date(2020, 1, 2, 3, 4, 5, 0, time.UTC), + DockerVersion: "18.06.1-ce", + OS: "linux", + HistoryPackages: types.Packages{ + { + Name: "musl", + Version: "1.2.3", + }, + }, + }, + }, + want: ` + { + "SchemaVersion": 1, + "Architecture": "amd64", + "Created": "2020-01-02T03:04:05Z", + "DockerVersion": "18.06.1-ce", + "OS": "linux", + "HistoryPackages": [ + { + "Name": "musl", + "Version": "1.2.3" + } + ] + } + `, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, err := newTempDB(t, "") + require.NoError(t, err) + + fs, err := NewFSCache(tmpDir) + require.NoError(t, err) + defer func() { + _ = fs.Clear(t.Context()) + _ = fs.Close() + }() + + err = fs.PutArtifact(t.Context(), tt.args.imageID, tt.args.imageConfig) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err, tt.name) + + err = fs.db.View(func(tx *bolt.Tx) error { + // check decompressedDigestBucket + imageBucket := tx.Bucket([]byte(artifactBucket)) + b := imageBucket.Get([]byte(tt.args.imageID)) + assert.JSONEq(t, tt.want, string(b)) + + return nil + }) + require.NoError(t, err) + }) + } +} + +func TestFSCache_MissingBlobs(t *testing.T) { + type args struct { + imageID string + layerIDs []string + } + tests := []struct { + name string + dbPath string + args args + wantMissingImage bool + wantMissingLayerIDs []string + wantErr string + }{ + { + name: "happy path", + dbPath: "testdata/fanal.db", + args: args{ + imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4/1", + layerIDs: []string{ + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7/11101", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5/11101", + "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec/11101", + }, + }, + wantMissingImage: false, + wantMissingLayerIDs: []string{ + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5/11101", + "sha256:dab15cac9ebd43beceeeda3ce95c574d6714ed3d3969071caead678c065813ec/11101", + }, + }, + { + name: "happy path: broken layer JSON", + dbPath: "testdata/broken-layer.db", + args: args{ + imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", + layerIDs: []string{ + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + wantMissingImage: true, + wantMissingLayerIDs: []string{ + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + { + name: "happy path: broken image JSON", + dbPath: "testdata/broken-image.db", + args: args{ + imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", + layerIDs: []string{ + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + wantMissingImage: true, + wantMissingLayerIDs: []string{ + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + { + name: "happy path: the schema version of image JSON doesn't match", + dbPath: "testdata/different-image-schema.db", + args: args{ + imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", + layerIDs: []string{ + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + wantMissingImage: true, + wantMissingLayerIDs: []string{ + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + { + name: "happy path: new config analyzer", + dbPath: "testdata/fanal.db", + args: args{ + imageID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4/2", + layerIDs: []string{ + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7/11102", + }, + }, + wantMissingImage: true, + wantMissingLayerIDs: []string{ + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7/11102", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, err := newTempDB(t, tt.dbPath) + require.NoError(t, err) + + fs, err := NewFSCache(tmpDir) + require.NoError(t, err) + defer func() { + _ = fs.Clear(t.Context()) + _ = fs.Close() + }() + + gotMissingImage, gotMissingLayerIDs, err := fs.MissingBlobs(t.Context(), tt.args.imageID, tt.args.layerIDs) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err, tt.name) + assert.Equal(t, tt.wantMissingImage, gotMissingImage, tt.name) + assert.Equal(t, tt.wantMissingLayerIDs, gotMissingLayerIDs, tt.name) + }) + } +} diff --git a/pkg/cache/key.go b/pkg/cache/key.go new file mode 100644 index 000000000000..3e006ee26ca4 --- /dev/null +++ b/pkg/cache/key.go @@ -0,0 +1,95 @@ +package cache + +import ( + "crypto/sha256" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "golang.org/x/mod/sumdb/dirhash" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func CalcKey(id string, artifactVersion int, analyzerVersions analyzer.Versions, hookVersions map[string]int, artifactOpt artifact.Option) (string, error) { + // Sort options for consistent results + artifactOpt.Sort() + artifactOpt.MisconfScannerOption.Sort() + + h := sha256.New() + + // Write ID, analyzer/handler versions, skipped files/dirs and file patterns + keyBase := struct { + ID string + ArtifactVersion int `json:",omitzero"` + AnalyzerVersions analyzer.Versions + HookVersions map[string]int + SkipFiles []string + SkipDirs []string + FilePatterns []string `json:",omitempty"` + DetectionPriority types.DetectionPriority `json:",omitempty"` + }{ + id, + artifactVersion, + analyzerVersions, + hookVersions, + artifactOpt.WalkerOption.SkipFiles, + artifactOpt.WalkerOption.SkipDirs, + artifactOpt.FilePatterns, + artifactOpt.DetectionPriority, + } + + if err := json.NewEncoder(h).Encode(keyBase); err != nil { + return "", xerrors.Errorf("json encode error: %w", err) + } + + // Write check, data contents and secret config file + paths := append(artifactOpt.MisconfScannerOption.PolicyPaths, artifactOpt.MisconfScannerOption.DataPaths...) + + // Check if the secret config exists. + if _, err := os.Stat(artifactOpt.SecretScannerOption.ConfigPath); err == nil { + paths = append(paths, artifactOpt.SecretScannerOption.ConfigPath) + } + + for _, p := range paths { + hash, err := hashContents(p) + if err != nil { + return "", err + } + + if _, err := h.Write([]byte(hash)); err != nil { + return "", xerrors.Errorf("sha256 write error: %w", err) + } + } + + return fmt.Sprintf("sha256:%x", h.Sum(nil)), nil +} + +func hashContents(path string) (string, error) { + fi, err := os.Stat(path) + if err != nil { + return "", xerrors.Errorf("file %q stat error: %w", path, err) + } + + var hash string + + if fi.IsDir() { + hash, err = dirhash.HashDir(path, "", dirhash.DefaultHash) + if err != nil { + return "", xerrors.Errorf("hash dir error (%s): %w", path, err) + } + } else { + hash, err = dirhash.DefaultHash([]string{filepath.Base(path)}, func(_ string) (io.ReadCloser, error) { + return os.Open(path) + }) + if err != nil { + return "", xerrors.Errorf("hash file error (%s): %w", path, err) + } + } + return hash, nil +} diff --git a/pkg/cache/key_test.go b/pkg/cache/key_test.go new file mode 100644 index 000000000000..996cb5bbf940 --- /dev/null +++ b/pkg/cache/key_test.go @@ -0,0 +1,287 @@ +package cache + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/misconf" +) + +func TestCalcKey(t *testing.T) { + type args struct { + key string + artifactVersion int + analyzerVersions analyzer.Versions + hookVersions map[string]int + skipFiles []string + skipDirs []string + patterns []string + policy []string + data []string + secretConfigPath string + detectionPriority types.DetectionPriority + } + tests := []struct { + name string + args args + want string + wantErr string + }{ + { + name: "happy path", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + artifactVersion: 1, + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + hookVersions: map[string]int{ + "python-pkg": 1, + }, + }, + want: "sha256:9ac2cf39c18b5c936694820bc9c380dd8c74d33484ebd08183c5620215a1d33c", + }, + { + name: "with disabled analyzer", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 0, + "redhat": 2, + }, + }, + hookVersions: map[string]int{ + "python-pkg": 1, + }, + }, + want: "sha256:d63724cc72729edd3c81205739d64fcb414a4e6345dd4dde7f0fe6bdd56bedf9", + }, + { + name: "with empty slice file patterns", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + patterns: []string{}, + }, + want: "sha256:9f7afa4d27c4c4f371dc6bb47bcc09e7a4a00b1d870e8156f126e35d8f6522e6", + }, + { + name: "with single empty string in file patterns", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + patterns: []string{""}, + }, + want: "sha256:bcfc5da13ef9bf0b85e719584800a010063474546f1051a781b78bd83de01102", + }, + { + name: "with single non empty string in file patterns", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + patterns: []string{"test"}, + }, + want: "sha256:8c9750b8eca507628417f21d7db707a7876d2e22c3e75b13f31a795af4051c57", + }, + { + name: "with non empty followed by empty string in file patterns", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + patterns: []string{ + "test", + "", + }, + }, + want: "sha256:71abf09bf1422531e2838db692b80f9b9f48766f56b7d3d02aecdb36b019e103", + }, + { + name: "with non empty preceded by empty string in file patterns", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + patterns: []string{ + "", + "test", + }, + }, + want: "sha256:71abf09bf1422531e2838db692b80f9b9f48766f56b7d3d02aecdb36b019e103", + }, + { + name: "with policy", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + policy: []string{"testdata/policy"}, + }, + want: "sha256:9602d5ef5af086112cc9fae8310390ed3fb79f4b309d8881b9807e379c8dfa57", + }, + { + name: "with policy file", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + policy: []string{"testdata/policy/test.rego"}, + }, + want: "sha256:9602d5ef5af086112cc9fae8310390ed3fb79f4b309d8881b9807e379c8dfa57", + }, + { + name: "skip files and dirs", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + skipFiles: []string{"app/deployment.yaml"}, + skipDirs: []string{"usr/java"}, + policy: []string{"testdata/policy"}, + }, + want: "sha256:363f70f4ee795f250873caea11c2fc94ef12945444327e7e2f8a99e3884695e0", + }, + { + name: "detection priority", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + skipFiles: []string{"app/deployment.yaml"}, + skipDirs: []string{"usr/java"}, + policy: []string{"testdata/policy"}, + detectionPriority: types.PriorityComprehensive, + }, + want: "sha256:2f1c898271e84f4382cd48ae7533069cc3dc656c2d688ac108f5db1a0d9fd393", + }, + { + + name: "secret config", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + hookVersions: map[string]int{ + "python-pkg": 1, + }, + secretConfigPath: "testdata/trivy-secret.yaml", + }, + want: "sha256:d3fb9503f2851ae9bdb250b7ef55c00c0bfa1250b19d4d398a9219c2c0e20958", + }, + { + + name: "secret config file doesn't exist", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + hookVersions: map[string]int{ + "python-pkg": 1, + }, + secretConfigPath: "trivy-secret.yaml", + }, + want: "sha256:c720b502991465ea11929cfefc71cf4b5aeaa9a8c0ae59fdaf597f957f5cdb18", + }, + { + name: "with policy/non-existent dir", + args: args{ + key: "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + analyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "debian": 1, + }, + }, + policy: []string{"policydir"}, + }, + wantErr: "file \"policydir\" stat error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + artifactOpt := artifact.Option{ + FilePatterns: tt.args.patterns, + DetectionPriority: tt.args.detectionPriority, + + MisconfScannerOption: misconf.ScannerOption{ + PolicyPaths: tt.args.policy, + DataPaths: tt.args.data, + }, + + SecretScannerOption: analyzer.SecretScannerOption{ + ConfigPath: tt.args.secretConfigPath, + }, + + WalkerOption: walker.Option{ + SkipFiles: tt.args.skipFiles, + SkipDirs: tt.args.skipDirs, + }, + } + got, err := CalcKey(tt.args.key, tt.args.artifactVersion, tt.args.analyzerVersions, tt.args.hookVersions, artifactOpt) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/cache/memory.go b/pkg/cache/memory.go new file mode 100644 index 000000000000..3d8d7973fbf9 --- /dev/null +++ b/pkg/cache/memory.go @@ -0,0 +1,113 @@ +package cache + +import ( + "context" + "sync" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var _ Cache = &MemoryCache{} + +type MemoryCache struct { + artifacts sync.Map // Map to store artifact information + blobs sync.Map // Map to store blob information +} + +func NewMemoryCache() *MemoryCache { + return &MemoryCache{} +} + +// PutArtifact stores the artifact information in the memory cache +func (c *MemoryCache) PutArtifact(_ context.Context, artifactID string, artifactInfo types.ArtifactInfo) error { + c.artifacts.Store(artifactID, artifactInfo) + return nil +} + +// PutBlob stores the blob information in the memory cache +func (c *MemoryCache) PutBlob(_ context.Context, blobID string, blobInfo types.BlobInfo) error { + c.blobs.Store(blobID, blobInfo) + return nil +} + +// DeleteBlobs removes the specified blobs from the memory cache +func (c *MemoryCache) DeleteBlobs(_ context.Context, blobIDs []string) error { + for _, blobID := range blobIDs { + c.blobs.Delete(blobID) + } + return nil +} + +// GetArtifact retrieves the artifact information from the memory cache +func (c *MemoryCache) GetArtifact(_ context.Context, artifactID string) (types.ArtifactInfo, error) { + info, ok := c.artifacts.Load(artifactID) + if !ok { + return types.ArtifactInfo{}, xerrors.Errorf("artifact (%s) not found in memory cache", artifactID) + } + artifactInfo, ok := info.(types.ArtifactInfo) + if !ok { + return types.ArtifactInfo{}, xerrors.Errorf("invalid type for artifact (%s) in memory cache", artifactID) + } + return artifactInfo, nil +} + +// GetBlob retrieves the blob information from the memory cache +func (c *MemoryCache) GetBlob(_ context.Context, blobID string) (types.BlobInfo, error) { + info, ok := c.blobs.Load(blobID) + if !ok { + return types.BlobInfo{}, xerrors.Errorf("blob (%s) not found in memory cache", blobID) + } + blobInfo, ok := info.(types.BlobInfo) + if !ok { + return types.BlobInfo{}, xerrors.Errorf("invalid type for blob (%s) in memory cache", blobID) + } + return blobInfo, nil +} + +// MissingBlobs determines the missing artifact and blob information in the memory cache +func (c *MemoryCache) MissingBlobs(ctx context.Context, artifactID string, blobIDs []string) (bool, []string, error) { + var missingArtifact bool + var missingBlobIDs []string + + if _, err := c.GetArtifact(ctx, artifactID); err != nil { + missingArtifact = true + } + + for _, blobID := range blobIDs { + if _, err := c.GetBlob(ctx, blobID); err != nil { + missingBlobIDs = append(missingBlobIDs, blobID) + } + } + + return missingArtifact, missingBlobIDs, nil +} + +// Close clears the artifact and blob information from the memory cache +func (c *MemoryCache) Close() error { + c.artifacts = sync.Map{} + c.blobs = sync.Map{} + return nil +} + +// Clear clears the artifact and blob information from the memory cache +func (c *MemoryCache) Clear(_ context.Context) error { + c.artifacts = sync.Map{} + c.blobs = sync.Map{} + return nil +} + +// BlobIDs returns all the blob IDs in the memory cache for testing +func (c *MemoryCache) BlobIDs() []string { + var blobIDs []string + c.blobs.Range(func(key, _ any) bool { + blobID, ok := key.(string) + if !ok { + return false + } + blobIDs = append(blobIDs, blobID) + return true + }) + return blobIDs +} diff --git a/pkg/cache/memory_test.go b/pkg/cache/memory_test.go new file mode 100644 index 000000000000..13c580e47301 --- /dev/null +++ b/pkg/cache/memory_test.go @@ -0,0 +1,391 @@ +package cache_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestMemoryCache_PutArtifact(t *testing.T) { + tests := []struct { + name string + artifactID string + artifactInfo types.ArtifactInfo + }{ + { + name: "happy path", + artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf", + artifactInfo: types.ArtifactInfo{ + SchemaVersion: 2, + Architecture: "amd64", + Created: time.Date(2020, 11, 14, 0, 20, 4, 0, time.UTC), + DockerVersion: "19.03.12", + OS: "linux", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewMemoryCache() + + err := c.PutArtifact(t.Context(), tt.artifactID, tt.artifactInfo) + require.NoError(t, err) + + got, err := c.GetArtifact(t.Context(), tt.artifactID) + require.NoError(t, err) + assert.Equal(t, tt.artifactInfo, got) + }) + } +} + +func TestMemoryCache_PutBlob(t *testing.T) { + tests := []struct { + name string + blobID string + blobInfo types.BlobInfo + }{ + { + name: "happy path", + blobID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + blobInfo: types.BlobInfo{ + SchemaVersion: 2, + Digest: "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609", + DiffID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + OS: types.OS{ + Family: "alpine", + Name: "3.10.2", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []types.Package{ + { + Name: "musl", + Version: "1.1.22-r3", + SrcName: "musl", + SrcVersion: "1.1.22-r3", + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewMemoryCache() + + err := c.PutBlob(t.Context(), tt.blobID, tt.blobInfo) + require.NoError(t, err) + + got, err := c.GetBlob(t.Context(), tt.blobID) + require.NoError(t, err) + assert.Equal(t, tt.blobInfo, got) + }) + } +} + +func TestMemoryCache_GetArtifact(t *testing.T) { + tests := []struct { + name string + artifactID string + artifactInfo types.ArtifactInfo + wantErr bool + }{ + { + name: "happy path", + artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf", + artifactInfo: types.ArtifactInfo{ + SchemaVersion: 2, + Architecture: "amd64", + Created: time.Date(2020, 11, 14, 0, 20, 4, 0, time.UTC), + DockerVersion: "19.03.12", + OS: "linux", + }, + wantErr: false, + }, + { + name: "not found", + artifactID: "sha256:nonexistent", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewMemoryCache() + + if !tt.wantErr { + err := c.PutArtifact(t.Context(), tt.artifactID, tt.artifactInfo) + require.NoError(t, err) + } + + got, err := c.GetArtifact(t.Context(), tt.artifactID) + if tt.wantErr { + require.ErrorContains(t, err, "not found in memory cache") + return + } + require.NoError(t, err) + assert.Equal(t, tt.artifactInfo, got) + }) + } +} + +func TestMemoryCache_GetBlob(t *testing.T) { + tests := []struct { + name string + blobID string + blobInfo types.BlobInfo + wantErr bool + }{ + { + name: "happy path", + blobID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + blobInfo: types.BlobInfo{ + SchemaVersion: 2, + Digest: "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609", + DiffID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + OS: types.OS{ + Family: "alpine", + Name: "3.10.2", + }, + }, + wantErr: false, + }, + { + name: "not found", + blobID: "sha256:nonexistent", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewMemoryCache() + + if !tt.wantErr { + err := c.PutBlob(t.Context(), tt.blobID, tt.blobInfo) + require.NoError(t, err) + } + + got, err := c.GetBlob(t.Context(), tt.blobID) + if tt.wantErr { + require.ErrorContains(t, err, "not found in memory cache") + return + } + require.NoError(t, err) + assert.Equal(t, tt.blobInfo, got) + }) + } +} + +func TestMemoryCache_MissingBlobs(t *testing.T) { + tests := []struct { + name string + artifactID string + blobIDs []string + putArtifact bool + putBlobs []string + wantMissingArtifact bool + wantMissingBlobIDs []string + }{ + { + name: "missing both artifact and blob", + artifactID: "sha256:artifact1", + blobIDs: []string{ + "sha256:blob1", + "sha256:blob2", + }, + putArtifact: false, + putBlobs: []string{}, + wantMissingArtifact: true, + wantMissingBlobIDs: []string{ + "sha256:blob1", + "sha256:blob2", + }, + }, + { + name: "missing artifact only", + artifactID: "sha256:artifact1", + blobIDs: []string{ + "sha256:blob1", + "sha256:blob2", + }, + putArtifact: false, + putBlobs: []string{ + "sha256:blob1", + "sha256:blob2", + }, + wantMissingArtifact: true, + wantMissingBlobIDs: nil, + }, + { + name: "missing one blob", + artifactID: "sha256:artifact1", + blobIDs: []string{ + "sha256:blob1", + "sha256:blob2", + }, + putArtifact: true, + putBlobs: []string{"sha256:blob1"}, + wantMissingArtifact: false, + wantMissingBlobIDs: []string{"sha256:blob2"}, + }, + { + name: "no missing blobs", + artifactID: "sha256:artifact1", + blobIDs: []string{ + "sha256:blob1", + "sha256:blob2", + }, + putArtifact: true, + putBlobs: []string{ + "sha256:blob1", + "sha256:blob2", + }, + wantMissingArtifact: false, + wantMissingBlobIDs: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewMemoryCache() + + if tt.putArtifact { + err := c.PutArtifact(t.Context(), tt.artifactID, types.ArtifactInfo{}) + require.NoError(t, err) + } + + for _, blobID := range tt.putBlobs { + err := c.PutBlob(t.Context(), blobID, types.BlobInfo{}) + require.NoError(t, err) + } + + gotMissingArtifact, gotMissingBlobIDs, err := c.MissingBlobs(t.Context(), tt.artifactID, tt.blobIDs) + require.NoError(t, err) + assert.Equal(t, tt.wantMissingArtifact, gotMissingArtifact) + assert.Equal(t, tt.wantMissingBlobIDs, gotMissingBlobIDs) + }) + } +} + +func TestMemoryCache_DeleteBlobs(t *testing.T) { + tests := []struct { + name string + blobIDs []string + }{ + { + name: "delete existing blobs", + blobIDs: []string{ + "sha256:blob1", + "sha256:blob2", + }, + }, + { + name: "delete non-existing blobs", + blobIDs: []string{ + "sha256:nonexistent1", + "sha256:nonexistent2", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewMemoryCache() + + // Put some blobs in the cache + for _, blobID := range tt.blobIDs { + err := c.PutBlob(t.Context(), blobID, types.BlobInfo{}) + require.NoError(t, err) + } + + err := c.DeleteBlobs(t.Context(), tt.blobIDs) + require.NoError(t, err) + + // Check that the blobs are no longer in the cache + for _, blobID := range tt.blobIDs { + _, err := c.GetBlob(t.Context(), blobID) + require.ErrorContains(t, err, "not found in memory cache") + } + }) + } +} + +func TestMemoryCache_Clear(t *testing.T) { + tests := []struct { + name string + artifactID string + blobID string + }{ + { + name: "clear cache", + artifactID: "sha256:artifact1", + blobID: "sha256:blob1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewMemoryCache() + + err := c.PutArtifact(t.Context(), tt.artifactID, types.ArtifactInfo{}) + require.NoError(t, err) + + err = c.PutBlob(t.Context(), tt.blobID, types.BlobInfo{}) + require.NoError(t, err) + + err = c.Clear(t.Context()) + require.NoError(t, err) + + _, err = c.GetArtifact(t.Context(), tt.artifactID) + require.ErrorContains(t, err, "not found in memory cache") + + _, err = c.GetBlob(t.Context(), tt.blobID) + require.ErrorContains(t, err, "not found in memory cache") + }) + } +} + +func TestMemoryCache_Close(t *testing.T) { + tests := []struct { + name string + artifactID string + blobID string + }{ + { + name: "close cache", + artifactID: "sha256:artifact1", + blobID: "sha256:blob1", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewMemoryCache() + + err := c.PutArtifact(t.Context(), tt.artifactID, types.ArtifactInfo{}) + require.NoError(t, err) + + err = c.PutBlob(t.Context(), tt.blobID, types.BlobInfo{}) + require.NoError(t, err) + + err = c.Close() + require.NoError(t, err) + + _, err = c.GetArtifact(t.Context(), tt.artifactID) + require.ErrorContains(t, err, "not found in memory cache") + + _, err = c.GetBlob(t.Context(), tt.blobID) + require.ErrorContains(t, err, "not found in memory cache") + }) + } +} diff --git a/pkg/cache/nop.go b/pkg/cache/nop.go new file mode 100644 index 000000000000..316cac1860f4 --- /dev/null +++ b/pkg/cache/nop.go @@ -0,0 +1,19 @@ +package cache + +import ( + "context" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type NopCache struct{} + +func NewNopCache() NopCache { return NopCache{} } +func (NopCache) GetArtifact(context.Context, string) (types.ArtifactInfo, error) { + return types.ArtifactInfo{}, nil +} +func (NopCache) GetBlob(context.Context, string) (types.BlobInfo, error) { + return types.BlobInfo{}, nil +} +func (NopCache) Close() error { return nil } +func (NopCache) Clear(context.Context) error { return nil } diff --git a/pkg/cache/redis.go b/pkg/cache/redis.go new file mode 100644 index 000000000000..780274b1924d --- /dev/null +++ b/pkg/cache/redis.go @@ -0,0 +1,248 @@ +package cache + +import ( + "context" + "crypto/tls" + "crypto/x509" + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/go-redis/redis/v8" + "github.com/hashicorp/go-multierror" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +var _ Cache = (*RedisCache)(nil) + +const redisPrefix = "fanal" + +type RedisOptions struct { + Backend string + TLS bool + TLSOptions RedisTLSOptions +} + +func NewRedisOptions(backend, caCert, cert, key string, enableTLS bool) (RedisOptions, error) { + tlsOpts, err := NewRedisTLSOptions(caCert, cert, key) + if err != nil { + return RedisOptions{}, xerrors.Errorf("redis TLS option error: %w", err) + } + + return RedisOptions{ + Backend: backend, + TLS: enableTLS, + TLSOptions: tlsOpts, + }, nil +} + +// BackendMasked returns the redis connection string masking credentials +func (o *RedisOptions) BackendMasked() string { + endIndex := strings.Index(o.Backend, "@") + if endIndex == -1 { + return o.Backend + } + + startIndex := strings.Index(o.Backend, "//") + + return fmt.Sprintf("%s****%s", o.Backend[:startIndex+2], o.Backend[endIndex:]) +} + +// RedisTLSOptions holds the options for redis cache +type RedisTLSOptions struct { + CACert string + Cert string + Key string +} + +func NewRedisTLSOptions(caCert, cert, key string) (RedisTLSOptions, error) { + opts := RedisTLSOptions{ + CACert: caCert, + Cert: cert, + Key: key, + } + + // If one of redis option not nil, make sure CA, cert, and key provided + if !lo.IsEmpty(opts) { + if opts.CACert == "" || opts.Cert == "" || opts.Key == "" { + return RedisTLSOptions{}, xerrors.Errorf("you must provide Redis CA, cert and key file path when using TLS") + } + } + return opts, nil +} + +type RedisCache struct { + client *redis.Client + expiration time.Duration +} + +func NewRedisCache(backend, caCertPath, certPath, keyPath string, enableTLS bool, ttl time.Duration) (RedisCache, error) { + opts, err := NewRedisOptions(backend, caCertPath, certPath, keyPath, enableTLS) + if err != nil { + return RedisCache{}, xerrors.Errorf("failed to create Redis options: %w", err) + } + + log.Info("Redis scan cache", log.String("url", opts.BackendMasked())) + options, err := redis.ParseURL(opts.Backend) + if err != nil { + return RedisCache{}, xerrors.Errorf("failed to parse Redis URL: %w", err) + } + + if tlsOpts := opts.TLSOptions; !lo.IsEmpty(tlsOpts) { + caCert, cert, err := GetTLSConfig(tlsOpts.CACert, tlsOpts.Cert, tlsOpts.Key) + if err != nil { + return RedisCache{}, xerrors.Errorf("failed to get TLS config: %w", err) + } + + options.TLSConfig = &tls.Config{ + RootCAs: caCert, + Certificates: []tls.Certificate{cert}, + MinVersion: tls.VersionTLS12, + } + } else if opts.TLS { + options.TLSConfig = &tls.Config{ + MinVersion: tls.VersionTLS12, + } + } + return RedisCache{ + client: redis.NewClient(options), + expiration: ttl, + }, nil +} + +func (c RedisCache) PutArtifact(ctx context.Context, artifactID string, artifactConfig types.ArtifactInfo) error { + key := fmt.Sprintf("%s::%s::%s", redisPrefix, artifactBucket, artifactID) + b, err := json.Marshal(artifactConfig) + if err != nil { + return xerrors.Errorf("failed to marshal artifact JSON: %w", err) + } + if err := c.client.Set(ctx, key, string(b), c.expiration).Err(); err != nil { + return xerrors.Errorf("unable to store artifact information in Redis cache (%s): %w", artifactID, err) + } + return nil +} + +func (c RedisCache) PutBlob(ctx context.Context, blobID string, blobInfo types.BlobInfo) error { + b, err := json.Marshal(blobInfo) + if err != nil { + return xerrors.Errorf("failed to marshal blob JSON: %w", err) + } + key := fmt.Sprintf("%s::%s::%s", redisPrefix, blobBucket, blobID) + if err := c.client.Set(ctx, key, string(b), c.expiration).Err(); err != nil { + return xerrors.Errorf("unable to store blob information in Redis cache (%s): %w", blobID, err) + } + return nil +} +func (c RedisCache) DeleteBlobs(ctx context.Context, blobIDs []string) error { + var errs error + for _, blobID := range blobIDs { + key := fmt.Sprintf("%s::%s::%s", redisPrefix, blobBucket, blobID) + if err := c.client.Del(ctx, key).Err(); err != nil { + errs = multierror.Append(errs, xerrors.Errorf("unable to delete blob %s: %w", blobID, err)) + } + } + return errs +} + +func (c RedisCache) GetArtifact(ctx context.Context, artifactID string) (types.ArtifactInfo, error) { + key := fmt.Sprintf("%s::%s::%s", redisPrefix, artifactBucket, artifactID) + val, err := c.client.Get(ctx, key).Bytes() + if err == redis.Nil { + return types.ArtifactInfo{}, xerrors.Errorf("artifact (%s) is missing in Redis cache", artifactID) + } else if err != nil { + return types.ArtifactInfo{}, xerrors.Errorf("failed to get artifact from the Redis cache: %w", err) + } + + var info types.ArtifactInfo + err = json.Unmarshal(val, &info) + if err != nil { + return types.ArtifactInfo{}, xerrors.Errorf("failed to unmarshal artifact (%s) from Redis value: %w", artifactID, err) + } + return info, nil +} + +func (c RedisCache) GetBlob(ctx context.Context, blobID string) (types.BlobInfo, error) { + key := fmt.Sprintf("%s::%s::%s", redisPrefix, blobBucket, blobID) + val, err := c.client.Get(ctx, key).Bytes() + if err == redis.Nil { + return types.BlobInfo{}, xerrors.Errorf("blob (%s) is missing in Redis cache", blobID) + } else if err != nil { + return types.BlobInfo{}, xerrors.Errorf("failed to get blob from the Redis cache: %w", err) + } + + var blobInfo types.BlobInfo + if err = json.Unmarshal(val, &blobInfo); err != nil { + return types.BlobInfo{}, xerrors.Errorf("failed to unmarshal blob (%s) from Redis value: %w", blobID, err) + } + return blobInfo, nil +} + +func (c RedisCache) MissingBlobs(ctx context.Context, artifactID string, blobIDs []string) (bool, []string, error) { + var missingArtifact bool + var missingBlobIDs []string + for _, blobID := range blobIDs { + blobInfo, err := c.GetBlob(ctx, blobID) + if err != nil { + // error means cache missed blob info + missingBlobIDs = append(missingBlobIDs, blobID) + continue + } + if blobInfo.SchemaVersion != types.BlobJSONSchemaVersion { + missingBlobIDs = append(missingBlobIDs, blobID) + } + } + // get artifact info + artifactInfo, err := c.GetArtifact(ctx, artifactID) + // error means cache missed artifact info + if err != nil { + return true, missingBlobIDs, nil + } + if artifactInfo.SchemaVersion != types.ArtifactJSONSchemaVersion { + missingArtifact = true + } + return missingArtifact, missingBlobIDs, nil +} + +func (c RedisCache) Close() error { + return c.client.Close() +} + +func (c RedisCache) Clear(ctx context.Context) error { + for { + keys, cursor, err := c.client.Scan(ctx, 0, redisPrefix+"::*", 100).Result() + if err != nil { + return xerrors.Errorf("failed to perform prefix scanning: %w", err) + } + if err = c.client.Unlink(ctx, keys...).Err(); err != nil { + return xerrors.Errorf("failed to unlink redis keys: %w", err) + } + if cursor == 0 { // We cleared all keys + break + } + } + return nil +} + +// GetTLSConfig gets tls config from CA, Cert and Key file +func GetTLSConfig(caCertPath, certPath, keyPath string) (*x509.CertPool, tls.Certificate, error) { + cert, err := tls.LoadX509KeyPair(certPath, keyPath) + if err != nil { + return nil, tls.Certificate{}, err + } + + caCert, err := os.ReadFile(caCertPath) + if err != nil { + return nil, tls.Certificate{}, err + } + + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + return caCertPool, cert, nil +} diff --git a/pkg/cache/redis_test.go b/pkg/cache/redis_test.go new file mode 100644 index 000000000000..555b98fb287e --- /dev/null +++ b/pkg/cache/redis_test.go @@ -0,0 +1,574 @@ +package cache_test + +import ( + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/alicebob/miniredis/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const correctHash = "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7" + +func TestRedisCache_PutArtifact(t *testing.T) { + type args struct { + artifactID string + artifactConfig types.ArtifactInfo + } + tests := []struct { + name string + setupRedis bool + args args + wantKey string + wantErr string + }{ + { + name: "happy path", + setupRedis: true, + args: args{ + artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf", + artifactConfig: types.ArtifactInfo{ + SchemaVersion: 2, + Architecture: "amd64", + Created: time.Date(2020, 11, 14, 0, 20, 4, 0, time.UTC), + DockerVersion: "19.03.12", + OS: "linux", + }, + }, + wantKey: "fanal::artifact::sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf", + }, + { + name: "no such host", + setupRedis: false, + args: args{ + artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf", + artifactConfig: types.ArtifactInfo{}, + }, + wantErr: "unable to store artifact information in Redis cache", + }, + } + + // Set up Redis test server + s, err := miniredis.Run() + require.NoError(t, err) + defer s.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr := s.Addr() + if !tt.setupRedis { + addr = "dummy:16379" + } + + c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0) + require.NoError(t, err) + + err = c.PutArtifact(t.Context(), tt.args.artifactID, tt.args.artifactConfig) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + got, err := s.Get(tt.wantKey) + require.NoError(t, err) + + want, err := json.Marshal(tt.args.artifactConfig) + require.NoError(t, err) + + assert.JSONEq(t, string(want), got) + }) + } +} + +func TestRedisCache_PutBlob(t *testing.T) { + type args struct { + blobID string + blobConfig types.BlobInfo + } + tests := []struct { + name string + setupRedis bool + args args + wantKey string + wantErr string + }{ + { + name: "happy path", + setupRedis: true, + args: args{ + blobID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + blobConfig: types.BlobInfo{ + SchemaVersion: 2, + Digest: "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609", + DiffID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + OS: types.OS{ + Family: "alpine", + Name: "3.10.2", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []types.Package{ + { + Name: "musl", + Version: "1.1.22-r3", + SrcName: "musl", + SrcVersion: "1.1.22-r3", + }, + }, + }, + }, + }, + }, + wantKey: "fanal::blob::sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + }, + { + name: "no such host", + setupRedis: false, + args: args{ + blobID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + blobConfig: types.BlobInfo{}, + }, + wantErr: "unable to store blob information in Redis cache", + }, + } + + // Set up Redis test server + s, err := miniredis.Run() + require.NoError(t, err) + defer s.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr := s.Addr() + if !tt.setupRedis { + addr = "dummy:16379" + } + + c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0) + require.NoError(t, err) + + err = c.PutBlob(t.Context(), tt.args.blobID, tt.args.blobConfig) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + got, err := s.Get(tt.wantKey) + require.NoError(t, err) + + want, err := json.Marshal(tt.args.blobConfig) + require.NoError(t, err) + + assert.JSONEq(t, string(want), got) + }) + } +} + +func TestRedisCache_GetArtifact(t *testing.T) { + info := types.ArtifactInfo{ + SchemaVersion: 2, + Architecture: "amd64", + Created: time.Date(2020, 11, 14, 0, 20, 4, 0, time.UTC), + DockerVersion: "19.03.12", + OS: "linux", + } + + tests := []struct { + name string + setupRedis bool + artifactID string + want types.ArtifactInfo + wantErr string + }{ + { + name: "happy path", + setupRedis: true, + artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf", + want: info, + }, + { + name: "malformed JSON", + setupRedis: true, + artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", + wantErr: "failed to unmarshal artifact", + }, + { + name: "no such host", + setupRedis: false, + artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", + wantErr: "failed to get artifact from the Redis cache", + }, + { + name: "nonexistent key", + setupRedis: true, + artifactID: "sha256:foo", + wantErr: "artifact (sha256:foo) is missing in Redis cache", + }, + } + + // Set up Redis test server + s, err := miniredis.Run() + require.NoError(t, err) + defer s.Close() + + // Set key/value pairs + b, err := json.Marshal(info) + require.NoError(t, err) + + s.Set("fanal::artifact::sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf", string(b)) + s.Set("fanal::artifact::sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", "foobar") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr := s.Addr() + if !tt.setupRedis { + addr = "dummy:16379" + } + + c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0) + require.NoError(t, err) + + got, err := c.GetArtifact(t.Context(), tt.artifactID) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRedisCache_GetBlob(t *testing.T) { + blobInfo := types.BlobInfo{ + SchemaVersion: 2, + Digest: "sha256:9d48c3bd43c520dc2784e868a780e976b207cbf493eaff8c6596eb871cbd9609", + DiffID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + OS: types.OS{ + Family: "alpine", + Name: "3.10.2", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []types.Package{ + { + Name: "musl", + Version: "1.1.22-r3", + SrcName: "musl", + SrcVersion: "1.1.22-r3", + }, + }, + }, + }, + } + + tests := []struct { + name string + setupRedis bool + blobID string + want types.BlobInfo + wantErr string + }{ + { + name: "happy path", + setupRedis: true, + blobID: "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + want: blobInfo, + }, + { + name: "malformed JSON", + setupRedis: true, + blobID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", + wantErr: "failed to unmarshal blob", + }, + { + name: "no such host", + setupRedis: false, + blobID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", + wantErr: "failed to get blob from the Redis cache", + }, + { + name: "nonexistent key", + setupRedis: true, + blobID: "sha256:foo", + wantErr: "blob (sha256:foo) is missing in Redis cache", + }, + } + + // Set up Redis test server + s, err := miniredis.Run() + require.NoError(t, err) + defer s.Close() + + // Set key/value pairs + b, err := json.Marshal(blobInfo) + require.NoError(t, err) + s.Set("fanal::blob::sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", string(b)) + s.Set("fanal::blob::sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", "foobar") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr := s.Addr() + if !tt.setupRedis { + addr = "dummy:16379" + } + + c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0) + require.NoError(t, err) + + got, err := c.GetBlob(t.Context(), tt.blobID) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRedisCache_MissingBlobs(t *testing.T) { + type args struct { + artifactID string + blobIDs []string + } + tests := []struct { + name string + setupRedis bool + args args + wantMissingArtifact bool + wantMissingBlobIDs []string + wantErr string + }{ + { + name: "missing both", + setupRedis: true, + args: args{ + artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4/1", + blobIDs: []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"}, + }, + wantMissingArtifact: true, + wantMissingBlobIDs: []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"}, + }, + { + name: "missing artifact", + setupRedis: true, + args: args{ + artifactID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4/1", + blobIDs: []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11111"}, + }, + wantMissingArtifact: true, + }, + { + name: "missing blobs", + setupRedis: true, + args: args{ + artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/1", + blobIDs: []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"}, + }, + wantMissingArtifact: false, + wantMissingBlobIDs: []string{"sha256:1b3ee35aacca9866b01dd96e870136266bde18006ac2f0d6eb706c798d1fa3c3/11111"}, + }, + { + name: "missing artifact with different schema version", + setupRedis: true, + args: args{ + artifactID: "sha256:be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c/1", + blobIDs: []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11111"}, + }, + wantMissingArtifact: true, + }, + { + name: "missing blobs with different schema version", + setupRedis: true, + args: args{ + artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/1", + blobIDs: []string{"sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02/11111"}, + }, + wantMissingArtifact: false, + wantMissingBlobIDs: []string{"sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02/11111"}, + }, + { + name: "different analyzer versions", + setupRedis: true, + args: args{ + artifactID: "sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/0", + blobIDs: []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11012"}, + }, + wantMissingArtifact: true, + wantMissingBlobIDs: []string{"sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11012"}, + }, + } + + // Set up Redis test server + s, err := miniredis.Run() + require.NoError(t, err) + defer s.Close() + + s.Set("fanal::artifact::sha256:8652b9f0cb4c0599575e5a003f5906876e10c1ceb2ab9fe1786712dac14a50cf/1", + fmt.Sprintf("{\"SchemaVersion\": %d}", types.ArtifactJSONSchemaVersion)) + s.Set("fanal::artifact::sha256:be4e4bea2c2e15b403bb321562e78ea84b501fb41497472e91ecb41504e8a27c/1", + `{"SchemaVersion": 999999}`) // This version should not match the current version + s.Set("fanal::blob::sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0/11111", + fmt.Sprintf("{\"SchemaVersion\": %d}", types.BlobJSONSchemaVersion)) + s.Set("fanal::blob::sha256:174f5685490326fc0a1c0f5570b8663732189b327007e47ff13d2ca59673db02/11111", + `{"SchemaVersion": 999999}`) // This version should not match the current version + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr := s.Addr() + if !tt.setupRedis { + addr = "dummy:6379" + } + + c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0) + require.NoError(t, err) + + missingArtifact, missingBlobIDs, err := c.MissingBlobs(t.Context(), tt.args.artifactID, tt.args.blobIDs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wantMissingArtifact, missingArtifact) + assert.Equal(t, tt.wantMissingBlobIDs, missingBlobIDs) + }) + } +} + +func TestRedisCache_Close(t *testing.T) { + // Set up Redis test server + s, err := miniredis.Run() + require.NoError(t, err) + defer s.Close() + + t.Run("close", func(t *testing.T) { + c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", s.Addr()), "", "", "", false, 0) + require.NoError(t, err) + + closeErr := c.Close() + require.NoError(t, closeErr) + time.Sleep(3 * time.Second) // give it some time + assert.Equal(t, 0, s.CurrentConnectionCount(), "The client is disconnected") + }) +} + +func TestRedisCache_Clear(t *testing.T) { + // Set up Redis test server + s, err := miniredis.Run() + require.NoError(t, err) + defer s.Close() + + for i := range 200 { + s.Set(fmt.Sprintf("fanal::key%d", i), "value") + } + s.Set("foo", "bar") + + t.Run("clear", func(t *testing.T) { + c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", s.Addr()), "", "", "", false, 0) + require.NoError(t, err) + + require.NoError(t, c.Clear(t.Context())) + for i := range 200 { + assert.False(t, s.Exists(fmt.Sprintf("fanal::key%d", i))) + } + assert.True(t, s.Exists("foo")) + }) +} + +func TestRedisCache_DeleteBlobs(t *testing.T) { + type args struct { + blobIDs []string + } + tests := []struct { + name string + setupRedis bool + args args + wantKey string + wantErr string + }{ + { + name: "happy path", + setupRedis: true, + args: args{ + blobIDs: []string{correctHash}, + }, + wantKey: "fanal::blob::" + correctHash, + }, + { + name: "no such host", + setupRedis: false, + args: args{ + blobIDs: []string{"sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae800"}, + }, + wantErr: "unable to delete blob", + }, + } + + // Set up Redis test server + s, err := miniredis.Run() + require.NoError(t, err) + defer s.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + addr := s.Addr() + if !tt.setupRedis { + addr = "dummy:16379" + } + + c, err := cache.NewRedisCache(fmt.Sprintf("redis://%s", addr), "", "", "", false, 0) + require.NoError(t, err) + + s.Set(tt.wantKey, "any string") + + err = c.DeleteBlobs(t.Context(), tt.args.blobIDs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + // Verify that the blobs are deleted + got := s.Keys() + assert.NotContains(t, got, tt.wantKey) + }) + } +} + +func TestRedisOptions_BackendMasked(t *testing.T) { + tests := []struct { + name string + fields cache.RedisOptions + want string + }{ + { + name: "redis cache backend masked", + fields: cache.RedisOptions{Backend: "redis://root:password@localhost:6379"}, + want: "redis://****@localhost:6379", + }, + { + name: "redis cache backend masked does nothing", + fields: cache.RedisOptions{Backend: "redis://localhost:6379"}, + want: "redis://localhost:6379", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.fields.BackendMasked()) + }) + } +} diff --git a/pkg/cache/remote.go b/pkg/cache/remote.go new file mode 100644 index 000000000000..6f71543189fb --- /dev/null +++ b/pkg/cache/remote.go @@ -0,0 +1,97 @@ +package cache + +import ( + "context" + "net/http" + + "github.com/twitchtv/twirp" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/rpc" + "github.com/aquasecurity/trivy/pkg/rpc/client" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" + rpcCache "github.com/aquasecurity/trivy/rpc/cache" +) + +var _ ArtifactCache = (*RemoteCache)(nil) + +type RemoteOptions struct { + ServerAddr string + CustomHeaders http.Header + PathPrefix string +} + +// RemoteCache implements remote cache +type RemoteCache struct { + ctx context.Context // for custom header + client rpcCache.Cache +} + +// NewRemoteCache is the factory method for RemoteCache +func NewRemoteCache(ctx context.Context, opts RemoteOptions) *RemoteCache { + ctx = client.WithCustomHeaders(ctx, opts.CustomHeaders) + + var twirpOpts []twirp.ClientOption + if opts.PathPrefix != "" { + twirpOpts = append(twirpOpts, twirp.WithClientPathPrefix(opts.PathPrefix)) + } + c := rpcCache.NewCacheProtobufClient(opts.ServerAddr, xhttp.ClientWithContext(ctx), twirpOpts...) + return &RemoteCache{ + ctx: ctx, + client: c, + } +} + +// PutArtifact sends artifact to remote client +func (c RemoteCache) PutArtifact(_ context.Context, imageID string, artifactInfo types.ArtifactInfo) error { + err := rpc.Retry(func() error { + var err error + _, err = c.client.PutArtifact(c.ctx, rpc.ConvertToRPCArtifactInfo(imageID, artifactInfo)) + return err + }) + if err != nil { + return xerrors.Errorf("unable to store cache on the server: %w", err) + } + return nil +} + +// PutBlob sends blobInfo to remote client +func (c RemoteCache) PutBlob(_ context.Context, diffID string, blobInfo types.BlobInfo) error { + err := rpc.Retry(func() error { + var err error + _, err = c.client.PutBlob(c.ctx, rpc.ConvertToRPCPutBlobRequest(diffID, blobInfo)) + return err + }) + if err != nil { + return xerrors.Errorf("unable to store cache on the server: %w", err) + } + return nil +} + +// MissingBlobs fetches missing blobs from RemoteCache +func (c RemoteCache) MissingBlobs(_ context.Context, imageID string, layerIDs []string) (bool, []string, error) { + var layers *rpcCache.MissingBlobsResponse + err := rpc.Retry(func() error { + var err error + layers, err = c.client.MissingBlobs(c.ctx, rpc.ConvertToMissingBlobsRequest(imageID, layerIDs)) + return err + }) + if err != nil { + return false, nil, xerrors.Errorf("unable to fetch missing layers: %w", err) + } + return layers.MissingArtifact, layers.MissingBlobIds, nil +} + +// DeleteBlobs removes blobs by IDs from RemoteCache +func (c RemoteCache) DeleteBlobs(_ context.Context, blobIDs []string) error { + err := rpc.Retry(func() error { + var err error + _, err = c.client.DeleteBlobs(c.ctx, rpc.ConvertToDeleteBlobsRequest(blobIDs)) + return err + }) + if err != nil { + return xerrors.Errorf("unable to delete blobs on the server: %w", err) + } + return nil +} diff --git a/pkg/cache/remote_test.go b/pkg/cache/remote_test.go new file mode 100644 index 000000000000..25c924d103b2 --- /dev/null +++ b/pkg/cache/remote_test.go @@ -0,0 +1,355 @@ +package cache_test + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/twitchtv/twirp" + "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/types" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" + rpcCache "github.com/aquasecurity/trivy/rpc/cache" + rpcScanner "github.com/aquasecurity/trivy/rpc/scanner" +) + +type mockCacheServer struct { + cache cache.Cache +} + +func (s *mockCacheServer) PutArtifact(_ context.Context, in *rpcCache.PutArtifactRequest) (*emptypb.Empty, error) { + if strings.Contains(in.ArtifactId, "invalid") { + return &emptypb.Empty{}, xerrors.New("invalid image ID") + } + return &emptypb.Empty{}, nil +} + +func (s *mockCacheServer) PutBlob(_ context.Context, in *rpcCache.PutBlobRequest) (*emptypb.Empty, error) { + if strings.Contains(in.DiffId, "invalid") { + return &emptypb.Empty{}, xerrors.New("invalid layer ID") + } + return &emptypb.Empty{}, nil +} + +func (s *mockCacheServer) MissingBlobs(_ context.Context, in *rpcCache.MissingBlobsRequest) (*rpcCache.MissingBlobsResponse, error) { + var layerIDs []string + for _, layerID := range in.BlobIds[:len(in.BlobIds)-1] { + if strings.Contains(layerID, "invalid") { + return nil, xerrors.New("invalid layer ID") + } + layerIDs = append(layerIDs, layerID) + } + return &rpcCache.MissingBlobsResponse{ + MissingArtifact: true, + MissingBlobIds: layerIDs, + }, nil +} + +func (s *mockCacheServer) DeleteBlobs(_ context.Context, in *rpcCache.DeleteBlobsRequest) (*emptypb.Empty, error) { + for _, blobId := range in.GetBlobIds() { + if strings.Contains(blobId, "invalid") { + return &emptypb.Empty{}, xerrors.New("invalid layer ID") + } + } + return &emptypb.Empty{}, nil +} + +func withToken(base http.Handler, token, tokenHeader string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if token != "" && token != r.Header.Get(tokenHeader) { + rpcScanner.WriteError(w, twirp.NewError(twirp.Unauthenticated, "invalid token")) + return + } + base.ServeHTTP(w, r) + }) +} + +func TestRemoteCache_PutArtifact(t *testing.T) { + mux := http.NewServeMux() + layerHandler := rpcCache.NewCacheServer(new(mockCacheServer), nil) + mux.Handle(rpcCache.CachePathPrefix, withToken(layerHandler, "valid-token", "Trivy-Token")) + ts := httptest.NewServer(mux) + + type args struct { + imageID string + imageInfo types.ArtifactInfo + customHeaders http.Header + } + tests := []struct { + name string + args args + wantErr string + }{ + { + name: "happy path", + args: args{ + imageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + imageInfo: types.ArtifactInfo{ + SchemaVersion: 1, + Architecture: "amd64", + Created: time.Time{}, + DockerVersion: "18.06", + OS: "linux", + HistoryPackages: []types.Package{ + { + Name: "musl", + Version: "1.2.3", + }, + }, + }, + customHeaders: http.Header{ + "Trivy-Token": []string{"valid-token"}, + }, + }, + }, + { + name: "sad path", + args: args{ + imageID: "sha256:invalid", + imageInfo: types.ArtifactInfo{ + SchemaVersion: 1, + Architecture: "amd64", + Created: time.Time{}, + DockerVersion: "18.06", + OS: "linux", + HistoryPackages: []types.Package{ + { + Name: "musl", + Version: "1.2.3", + }, + }, + }, + customHeaders: http.Header{ + "Trivy-Token": []string{"valid-token"}, + }, + }, + wantErr: "twirp error internal", + }, + { + name: "sad path: invalid token", + args: args{ + imageID: "sha256:invalid", + customHeaders: http.Header{ + "Trivy-Token": []string{"invalid-token"}, + }, + }, + wantErr: "twirp error unauthenticated", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewRemoteCache(t.Context(), cache.RemoteOptions{ + ServerAddr: ts.URL, + CustomHeaders: tt.args.customHeaders, + }) + err := c.PutArtifact(t.Context(), tt.args.imageID, tt.args.imageInfo) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err, tt.name) + }) + } +} + +func TestRemoteCache_PutBlob(t *testing.T) { + mux := http.NewServeMux() + layerHandler := rpcCache.NewCacheServer(new(mockCacheServer), nil) + mux.Handle(rpcCache.CachePathPrefix, withToken(layerHandler, "valid-token", "Trivy-Token")) + ts := httptest.NewServer(mux) + + type args struct { + diffID string + layerInfo types.BlobInfo + customHeaders http.Header + } + tests := []struct { + name string + args args + wantErr string + }{ + { + name: "happy path", + args: args{ + diffID: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + customHeaders: http.Header{ + "Trivy-Token": []string{"valid-token"}, + }, + }, + }, + { + name: "sad path", + args: args{ + diffID: "sha256:invalid", + customHeaders: http.Header{ + "Trivy-Token": []string{"valid-token"}, + }, + }, + wantErr: "twirp error internal", + }, + { + name: "sad path: invalid token", + args: args{ + diffID: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + customHeaders: http.Header{ + "Trivy-Token": []string{"invalid-token"}, + }, + }, + wantErr: "twirp error unauthenticated", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewRemoteCache(t.Context(), cache.RemoteOptions{ + ServerAddr: ts.URL, + CustomHeaders: tt.args.customHeaders, + }) + err := c.PutBlob(t.Context(), tt.args.diffID, tt.args.layerInfo) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err, tt.name) + }) + } +} + +func TestRemoteCache_MissingBlobs(t *testing.T) { + mux := http.NewServeMux() + layerHandler := rpcCache.NewCacheServer(new(mockCacheServer), nil) + mux.Handle(rpcCache.CachePathPrefix, withToken(layerHandler, "valid-token", "Trivy-Token")) + ts := httptest.NewServer(mux) + + type args struct { + imageID string + layerIDs []string + customHeaders http.Header + } + tests := []struct { + name string + args args + wantMissingImage bool + wantMissingLayerIDs []string + wantErr string + }{ + { + name: "happy path", + args: args{ + imageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + layerIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + }, + customHeaders: http.Header{ + "Trivy-Token": []string{"valid-token"}, + }, + }, + wantMissingImage: true, + wantMissingLayerIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + name: "sad path", + args: args{ + imageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + layerIDs: []string{ + "sha256:invalid", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + }, + customHeaders: http.Header{ + "Trivy-Token": []string{"valid-token"}, + }, + }, + wantErr: "twirp error internal", + }, + { + name: "sad path with invalid token", + args: args{ + imageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + layerIDs: []string{ + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + }, + customHeaders: http.Header{ + "Trivy-Token": []string{"invalid-token"}, + }, + }, + wantErr: "twirp error unauthenticated", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cache.NewRemoteCache(t.Context(), cache.RemoteOptions{ + ServerAddr: ts.URL, + CustomHeaders: tt.args.customHeaders, + }) + gotMissingImage, gotMissingLayerIDs, err := c.MissingBlobs(t.Context(), tt.args.imageID, tt.args.layerIDs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err, tt.name) + assert.Equal(t, tt.wantMissingImage, gotMissingImage) + assert.Equal(t, tt.wantMissingLayerIDs, gotMissingLayerIDs) + }) + } +} + +func TestRemoteCache_PutArtifactInsecure(t *testing.T) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) + defer ts.Close() + + type args struct { + imageID string + imageInfo types.ArtifactInfo + insecure bool + } + tests := []struct { + name string + args args + wantErr string + }{ + { + name: "happy path", + args: args{ + imageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + imageInfo: types.ArtifactInfo{}, + insecure: true, + }, + }, + { + name: "sad path", + args: args{ + imageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + imageInfo: types.ArtifactInfo{}, + insecure: false, + }, + wantErr: "failed to do request", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := xhttp.WithTransport(t.Context(), xhttp.NewTransport(xhttp.Options{ + Insecure: tt.args.insecure, + })) + c := cache.NewRemoteCache(ctx, cache.RemoteOptions{ + ServerAddr: ts.URL, + CustomHeaders: nil, + }) + err := c.PutArtifact(t.Context(), tt.args.imageID, tt.args.imageInfo) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err, tt.name) + }) + } +} diff --git a/pkg/cache/testdata/broken-image.db b/pkg/cache/testdata/broken-image.db new file mode 100644 index 000000000000..c27eb16b6454 Binary files /dev/null and b/pkg/cache/testdata/broken-image.db differ diff --git a/pkg/cache/testdata/broken-layer.db b/pkg/cache/testdata/broken-layer.db new file mode 100644 index 000000000000..434a88d2811f Binary files /dev/null and b/pkg/cache/testdata/broken-layer.db differ diff --git a/pkg/cache/testdata/different-image-schema.db b/pkg/cache/testdata/different-image-schema.db new file mode 100644 index 000000000000..0a205256e605 Binary files /dev/null and b/pkg/cache/testdata/different-image-schema.db differ diff --git a/pkg/cache/testdata/fanal.db b/pkg/cache/testdata/fanal.db new file mode 100644 index 000000000000..7fb522be223e Binary files /dev/null and b/pkg/cache/testdata/fanal.db differ diff --git a/pkg/cache/testdata/policy/test.rego b/pkg/cache/testdata/policy/test.rego new file mode 100644 index 000000000000..b27c498ab823 --- /dev/null +++ b/pkg/cache/testdata/policy/test.rego @@ -0,0 +1 @@ +package foo \ No newline at end of file diff --git a/pkg/cache/testdata/trivy-secret.yaml b/pkg/cache/testdata/trivy-secret.yaml new file mode 100644 index 000000000000..8ce416eaba9d --- /dev/null +++ b/pkg/cache/testdata/trivy-secret.yaml @@ -0,0 +1,4 @@ +disable-allow-rules: + - usr-dirs + - examples + diff --git a/pkg/clock/clock.go b/pkg/clock/clock.go new file mode 100644 index 000000000000..38df49259e55 --- /dev/null +++ b/pkg/clock/clock.go @@ -0,0 +1,38 @@ +package clock + +import ( + "context" + "time" + + "k8s.io/utils/clock" + clocktesting "k8s.io/utils/clock/testing" +) + +type ( + RealClock = clock.RealClock + FakeClock = clocktesting.FakeClock +) + +// clockKey is the context key for clock. It is unexported to prevent collisions with context keys defined in +// other packages. +type clockKey struct{} + +// With returns a new context with the given time. +func With(ctx context.Context, t time.Time) context.Context { + c := clocktesting.NewFakeClock(t) + return context.WithValue(ctx, clockKey{}, c) +} + +// Now returns the current time. +func Now(ctx context.Context) time.Time { + return Clock(ctx).Now() +} + +// Clock returns the clock from the context. +func Clock(ctx context.Context) clock.Clock { + t, ok := ctx.Value(clockKey{}).(clock.Clock) + if !ok { + return RealClock{} + } + return t +} diff --git a/pkg/commands/app.go b/pkg/commands/app.go new file mode 100644 index 000000000000..c528c06afc3c --- /dev/null +++ b/pkg/commands/app.go @@ -0,0 +1,1490 @@ +package commands + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "time" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/commands/artifact" + "github.com/aquasecurity/trivy/pkg/commands/auth" + "github.com/aquasecurity/trivy/pkg/commands/clean" + "github.com/aquasecurity/trivy/pkg/commands/convert" + "github.com/aquasecurity/trivy/pkg/commands/server" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/flag" + k8scommands "github.com/aquasecurity/trivy/pkg/k8s/commands" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/module" + "github.com/aquasecurity/trivy/pkg/plugin" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/version" + "github.com/aquasecurity/trivy/pkg/version/app" + vexrepo "github.com/aquasecurity/trivy/pkg/vex/repo" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" +) + +const ( + usageTemplate = `Usage:{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +Aliases: + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +Examples: +{{.Example}}{{end}}{{if .HasAvailableSubCommands}} + +Available Commands:{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +%s + +Global Flags: +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +` + + groupScanning = "scanning" + groupManagement = "management" + groupUtility = "utility" + groupPlugin = "plugin" +) + +// NewApp is the factory method to return Trivy CLI +func NewApp() *cobra.Command { + cobra.EnableTraverseRunHooks = true // To execute persistent pre-run hooks from all parents. + globalFlags := flag.NewGlobalFlagGroup() + rootCmd := NewRootCommand(globalFlags) + rootCmd.AddGroup( + &cobra.Group{ + ID: groupScanning, + Title: "Scanning Commands", + }, + &cobra.Group{ + ID: groupManagement, + Title: "Management Commands", + }, + &cobra.Group{ + ID: groupUtility, + Title: "Utility Commands", + }, + ) + rootCmd.SetCompletionCommandGroupID(groupUtility) + rootCmd.SetHelpCommandGroupID(groupUtility) + rootCmd.AddCommand( + NewImageCommand(globalFlags), + NewFilesystemCommand(globalFlags), + NewRootfsCommand(globalFlags), + NewRepositoryCommand(globalFlags), + NewClientCommand(globalFlags), + NewServerCommand(globalFlags), + NewConfigCommand(globalFlags), + NewConvertCommand(globalFlags), + NewPluginCommand(globalFlags), + NewModuleCommand(globalFlags), + NewKubernetesCommand(globalFlags), + NewSBOMCommand(globalFlags), + NewVersionCommand(globalFlags), + NewVMCommand(globalFlags), + NewCleanCommand(globalFlags), + NewRegistryCommand(globalFlags), + NewVEXCommand(globalFlags), + ) + + if plugins := loadPluginCommands(); len(plugins) > 0 { + rootCmd.AddGroup(&cobra.Group{ + ID: groupPlugin, + Title: "Plugin Commands", + }) + rootCmd.AddCommand(plugins...) + } + + return rootCmd +} + +func loadPluginCommands() []*cobra.Command { + ctx := context.Background() + + var commands []*cobra.Command + plugins, err := plugin.NewManager().LoadAll(ctx) + if err != nil { + log.DebugContext(ctx, "No plugins loaded") + return nil + } + for _, p := range plugins { + cmd := &cobra.Command{ + Use: fmt.Sprintf("%s [flags]", p.Name), + Short: p.Summary, + Long: p.Description, + GroupID: groupPlugin, + RunE: func(cmd *cobra.Command, args []string) error { + if err = p.Run(cmd.Context(), plugin.Options{Args: args}); err != nil { + return xerrors.Errorf("plugin error: %w", err) + } + return nil + }, + DisableFlagParsing: true, + SilenceUsage: true, + SilenceErrors: true, + } + commands = append(commands, cmd) + } + return commands +} + +func initConfig(configFile string, pathChanged bool) error { + // Read from config + viper.SetConfigFile(configFile) + viper.SetConfigType("yaml") + if err := viper.ReadInConfig(); err != nil { + if errors.Is(err, os.ErrNotExist) { + if !pathChanged { + log.Debugf("Default config file %q not found, using built in values", log.FilePath(configFile)) + return nil + } + } + return xerrors.Errorf("config file %q loading error: %s", configFile, err) + } + log.Info("Loaded", log.FilePath(configFile)) + return nil +} + +func NewRootCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + var versionFormat string + cmd := &cobra.Command{ + Use: "trivy [global flags] command [flags] target", + Short: "Unified security scanner", + Long: "Scanner for vulnerabilities in container images, file systems, and Git repositories, as well as for configuration issues and hard-coded secrets", + Example: ` # Scan a container image + $ trivy image python:3.4-alpine + + # Scan a container image from a tar archive + $ trivy image --input ruby-3.1.tar + + # Scan local filesystem + $ trivy fs . + + # Run in server mode + $ trivy server`, + Args: cobra.NoArgs, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // Set the Trivy version here so that we can override version printer. + cmd.Version = app.Version() + + // viper.BindPFlag cannot be called in init(). + // cf. https://github.com/spf13/cobra/issues/875 + // https://github.com/spf13/viper/issues/233 + if err := globalFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + + // The config path is needed for config initialization. + // It needs to be obtained before ToOptions(). + configPath := viper.GetString(flag.ConfigFileFlag.ConfigName) + + // Configure environment variables and config file + // It cannot be called in init() because it must be called after viper.BindPFlags. + if err := initConfig(configPath, cmd.Flags().Changed(flag.ConfigFileFlag.ConfigName)); err != nil { + return err + } + + flags := flag.Flags{globalFlags} + opts, err := flags.ToOptions(args) + if err != nil { + return err + } + // Initialize logger + log.InitLogger(opts.Debug, opts.Quiet) + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + flags := flag.Flags{globalFlags} + opts, err := flags.ToOptions(args) + if err != nil { + return err + } + + if opts.ShowVersion { + // Customize version output + return showVersion(opts.CacheDir, versionFormat, cmd.OutOrStdout()) + } + return cmd.Help() + }, + } + + // Add version format flag, only json is supported + cmd.Flags().StringVarP(&versionFormat, flag.FormatFlag.Name, flag.FormatFlag.Shorthand, "", "version format (json)") + + globalFlags.AddFlags(cmd) + + return cmd +} + +func NewImageCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + report := flag.ReportFormatFlag.Clone() + report.Default = "summary" // override the default value as the summary is preferred for the compliance report + report.Usage = "specify a format for the compliance report." // "--report" works only with "--compliance" + reportFlagGroup.ReportFormat = report + + compliance := flag.ComplianceFlag.Clone() + compliance.Usage = fmt.Sprintf("%s (built-in compliance's: %s)", compliance.Usage, types.ComplianceDockerCIS160) + reportFlagGroup.Compliance = compliance // override usage as the accepted values differ for each subcommand. + + packageFlagGroup := flag.NewPackageFlagGroup() + packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps' + + misconfFlagGroup := flag.NewMisconfFlagGroup() + misconfFlagGroup.CloudformationParamVars = nil // disable '--cf-params' + misconfFlagGroup.TerraformTFVars = nil // disable '--tf-vars' + + imageFlags := flag.Flags{ + globalFlags, + flag.NewCacheFlagGroup(), + flag.NewDBFlagGroup(), + flag.NewImageFlagGroup(), // container image specific flags + flag.NewLicenseFlagGroup(), + misconfFlagGroup, + flag.NewModuleFlagGroup(), + packageFlagGroup, + flag.NewClientFlags(), + flag.NewRegistryFlagGroup(), + flag.NewRegoFlagGroup(), + reportFlagGroup, + flag.NewScanFlagGroup(), + flag.NewSecretFlagGroup(), + flag.NewVulnerabilityFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "image [flags] IMAGE_NAME", + Aliases: []string{"i"}, + GroupID: groupScanning, + Short: "Scan a container image", + Example: ` # Scan a container image + $ trivy image python:3.4-alpine + + # Scan a container image from a tar archive + $ trivy image --input ruby-3.1.tar + + # Filter by severities + $ trivy image --severity HIGH,CRITICAL alpine:3.15 + + # Ignore unfixed/unpatched vulnerabilities + $ trivy image --ignore-unfixed alpine:3.15 + + # Scan a container image in client mode + $ trivy image --server http://127.0.0.1:4954 alpine:latest + + # Generate json result + $ trivy image --format json --output result.json alpine:3.15 + + # Generate a report in the CycloneDX format + $ trivy image --format cyclonedx --output result.cdx alpine:3.15`, + + // 'Args' cannot be used since it is called before PreRunE and viper is not configured yet. + // cmd.Args -> cannot validate args here + // cmd.PreRunE -> configure viper && validate args + // cmd.RunE -> run the command + PreRunE: func(cmd *cobra.Command, args []string) error { + // viper.BindPFlag cannot be called in init(), so it is called in PreRunE. + // cf. https://github.com/spf13/cobra/issues/875 + // https://github.com/spf13/viper/issues/233 + if err := imageFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + options, err := imageFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetContainerImage) + }, + SilenceErrors: true, + SilenceUsage: true, + } + + imageFlags.AddFlags(cmd) + cmd.SetFlagErrorFunc(flagErrorFunc) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, imageFlags.Usages(cmd))) + + return cmd +} + +func NewFilesystemCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + cacheFlagGroup := flag.NewCacheFlagGroup() + cacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) // Use memory cache by default + + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat.Usage = "specify a compliance report format for the output" // @TODO: support --report summary for non compliance reports + reportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol' + + fsFlags := flag.Flags{ + globalFlags, + cacheFlagGroup, + flag.NewDBFlagGroup(), + flag.NewLicenseFlagGroup(), + flag.NewMisconfFlagGroup(), + flag.NewModuleFlagGroup(), + flag.NewPackageFlagGroup(), + flag.NewClientFlags(), // for client/server mode + flag.NewRegistryFlagGroup(), + flag.NewRegoFlagGroup(), + reportFlagGroup, + flag.NewScanFlagGroup(), + flag.NewSecretFlagGroup(), + flag.NewVulnerabilityFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "filesystem [flags] PATH", + Aliases: []string{"fs"}, + GroupID: groupScanning, + Short: "Scan local filesystem", + Example: ` # Scan a local project including language-specific files + $ trivy fs /path/to/your_project + + # Scan a single file + $ trivy fs ./trivy-ci-test/Pipfile.lock`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := fsFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := fsFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := fsFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetFilesystem) + }, + SilenceErrors: true, + SilenceUsage: true, + } + + cmd.SetFlagErrorFunc(flagErrorFunc) + fsFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, fsFlags.Usages(cmd))) + + return cmd +} + +func NewRootfsCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat = nil // TODO: support --report summary + reportFlagGroup.Compliance = nil // disable '--compliance' + reportFlagGroup.ReportFormat = nil // disable '--report' + + packageFlagGroup := flag.NewPackageFlagGroup() + packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps' + + cacheFlagGroup := flag.NewCacheFlagGroup() + cacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) // Use memory cache by default + + rootfsFlags := flag.Flags{ + globalFlags, + cacheFlagGroup, + flag.NewDBFlagGroup(), + flag.NewLicenseFlagGroup(), + flag.NewMisconfFlagGroup(), + flag.NewModuleFlagGroup(), + packageFlagGroup, + flag.NewClientFlags(), // for client/server mode + flag.NewRegistryFlagGroup(), + flag.NewRegoFlagGroup(), + reportFlagGroup, + flag.NewScanFlagGroup(), + flag.NewSecretFlagGroup(), + flag.NewVulnerabilityFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "rootfs [flags] ROOTDIR", + Short: "Scan rootfs", + GroupID: groupScanning, + Example: ` # Scan unpacked filesystem + $ docker export $(docker create alpine:3.10.2) | tar -C /tmp/rootfs -xvf - + $ trivy rootfs /tmp/rootfs + + # Scan from inside a container + $ docker run --rm -it alpine:3.11 + / # curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin + / # trivy rootfs /`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := rootfsFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := rootfsFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := rootfsFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetRootfs) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + rootfsFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, rootfsFlags.Usages(cmd))) + + return cmd +} + +func NewRepositoryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat = nil // TODO: support --report summary + reportFlagGroup.Compliance = nil // disable '--compliance' + reportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol' + + scanFlagGroup := flag.NewScanFlagGroup() + scanFlagGroup.DistroFlag = nil // repo subcommand doesn't support scanning OS packages + + repoFlags := flag.Flags{ + globalFlags, + flag.NewCacheFlagGroup(), + flag.NewDBFlagGroup(), + flag.NewLicenseFlagGroup(), + flag.NewMisconfFlagGroup(), + flag.NewModuleFlagGroup(), + flag.NewPackageFlagGroup(), + flag.NewRegistryFlagGroup(), + flag.NewRegoFlagGroup(), + flag.NewClientFlags(), // for client/server mode + reportFlagGroup, + scanFlagGroup, + flag.NewSecretFlagGroup(), + flag.NewVulnerabilityFlagGroup(), + flag.NewRepoFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "repository [flags] (REPO_PATH | REPO_URL)", + Aliases: []string{"repo"}, + GroupID: groupScanning, + Short: "Scan a repository", + Example: ` # Scan your remote git repository + $ trivy repo https://github.com/knqyf263/trivy-ci-test + # Scan your local git repository + $ trivy repo /path/to/your/repository`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := repoFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := repoFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := repoFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetRepository) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + repoFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, repoFlags.Usages(cmd))) + + return cmd +} + +func NewConvertCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + // To display the summary table, we need to enable scanners (to build columns). + // We can't get scanner information from the report (we don't include empty licenses and secrets in the report). + // So we need to ask the user to configure scanners (if needed). + scanFlagGroup := &flag.ScanFlagGroup{ + Scanners: flag.ScannersFlag.Clone(), + } + scanFlagGroup.Scanners.Default = nil // disable default scanners + scanFlagGroup.Scanners.Usage = "List of scanners included when generating the json report. Used only for rendering the summary table." + + convertFlags := flag.Flags{ + globalFlags, + scanFlagGroup, + flag.NewReportFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "convert [flags] RESULT_JSON", + Aliases: []string{"conv"}, + GroupID: groupUtility, + Short: "Convert Trivy JSON report into a different format", + Example: ` # report conversion + $ trivy image --format json --output result.json debian:11 + $ trivy convert --format cyclonedx --output result.cdx result.json +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := convertFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := convertFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + opts, err := convertFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + + return convert.Run(cmd.Context(), opts) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + convertFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, convertFlags.Usages(cmd))) + + return cmd +} + +// NewClientCommand returns the 'client' subcommand that is deprecated +func NewClientCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + remoteFlags := flag.NewClientFlags() + remoteAddr := flag.Flag[string]{ + Name: "remote", + ConfigName: "server.addr", + Shorthand: "", + Default: "http://localhost:4954", + Usage: "server address", + } + remoteFlags.ServerAddr = &remoteAddr // disable '--server' and enable '--remote' instead. + + clientFlags := flag.Flags{ + globalFlags, + flag.NewCacheFlagGroup(), + flag.NewDBFlagGroup(), + flag.NewMisconfFlagGroup(), + flag.NewRegistryFlagGroup(), + flag.NewRegoFlagGroup(), + remoteFlags, + flag.NewReportFlagGroup(), + flag.NewScanFlagGroup(), + flag.NewVulnerabilityFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "client [flags] IMAGE_NAME", + Aliases: []string{"c"}, + Hidden: true, // 'client' command is deprecated + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := clientFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + log.Warn("'client' subcommand is deprecated now. See https://github.com/aquasecurity/trivy/discussions/2119") + + if err := clientFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := clientFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return artifact.Run(cmd.Context(), options, artifact.TargetContainerImage) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + clientFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, clientFlags.Usages(cmd))) + + return cmd +} + +func NewServerCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + // The Java DB is not needed for the server mode + dbFlagGroup := flag.NewDBFlagGroup() + dbFlagGroup.DownloadJavaDBOnly = nil // disable '--download-java-db-only' + dbFlagGroup.SkipJavaDBUpdate = nil // disable '--skip-java-db-update' + dbFlagGroup.JavaDBRepositories = nil // disable '--java-db-repository' + + serverFlags := flag.Flags{ + globalFlags, + flag.NewCacheFlagGroup(), + dbFlagGroup, + flag.NewModuleFlagGroup(), + flag.NewServerFlags(), + flag.NewRegistryFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "server [flags]", + Aliases: []string{"s"}, + GroupID: groupUtility, + Short: "Server mode", + Example: ` # Run a server + $ trivy server + + # Listen on 0.0.0.0:10000 + $ trivy server --listen 0.0.0.0:10000 +`, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + if err := serverFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := serverFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return server.Run(cmd.Context(), options) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + serverFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, serverFlags.Usages(cmd))) + + return cmd +} + +func NewConfigCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + scanFlags := &flag.ScanFlagGroup{ + // Enable only '--skip-dirs', '--skip-files', `--skip-version-check` + // and `--disable-telemetry`, disable other scan flags + SkipDirs: flag.SkipDirsFlag.Clone(), + SkipFiles: flag.SkipFilesFlag.Clone(), + FilePatterns: flag.FilePatternsFlag.Clone(), + SkipVersionCheck: flag.SkipVersionCheckFlag.Clone(), + DisableTelemetry: flag.DisableTelemetryFlag.Clone(), + } + + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.DependencyTree = nil // disable '--dependency-tree' + reportFlagGroup.ListAllPkgs = nil // disable '--list-all-pkgs' + reportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol' + reportFlagGroup.ShowSuppressed = nil // disable '--show-suppressed' + reportFlagGroup.ReportFormat.Usage = "specify a compliance report format for the output" // @TODO: support --report summary for non compliance reports + + cacheFlagGroup := flag.NewCacheFlagGroup() + cacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) + + configFlags := &flag.Flags{ + globalFlags, + cacheFlagGroup, + flag.NewMisconfFlagGroup(), + flag.NewModuleFlagGroup(), + flag.NewRegistryFlagGroup(), + flag.NewRegoFlagGroup(), + &flag.K8sFlagGroup{ + // Keep only --k8s-version flag and disable others + K8sVersion: flag.K8sVersionFlag.Clone(), + }, + reportFlagGroup, + scanFlags, + } + + cmd := &cobra.Command{ + Use: "config [flags] DIR", + Aliases: []string{"conf"}, + GroupID: groupScanning, + Short: "Scan config files for misconfigurations", + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := configFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := configFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := configFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + + // Disable OS and language analyzers + options.DisabledAnalyzers = append(analyzer.TypeOSes, analyzer.TypeLanguages...) + + // Scan only for misconfigurations + options.Scanners = types.Scanners{types.MisconfigScanner} + + return artifact.Run(cmd.Context(), options, artifact.TargetFilesystem) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + configFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, configFlags.Usages(cmd))) + + return cmd +} + +func NewPluginCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + var pluginOptions flag.Options + pluginFlags := &flag.Flags{ + globalFlags, + } + cmd := &cobra.Command{ + Use: "plugin subcommand", + Aliases: []string{"p"}, + GroupID: groupManagement, + Short: "Manage plugins", + SilenceErrors: true, + SilenceUsage: true, + PersistentPreRunE: func(_ *cobra.Command, args []string) error { + var err error + pluginOptions, err = pluginFlags.ToOptions(args) + if err != nil { + return err + } + return nil + }, + } + cmd.AddCommand( + &cobra.Command{ + Use: "install NAME | URL | FILE_PATH", + Aliases: []string{"i"}, + Short: "Install a plugin", + Example: ` # Install a plugin from the plugin index + $ trivy plugin install referrer + + # Specify the version of the plugin to install + $ trivy plugin install referrer@v0.3.0 + + # Install a plugin from a URL + $ trivy plugin install github.com/aquasecurity/trivy-plugin-referrer`, + SilenceErrors: true, + SilenceUsage: true, + DisableFlagsInUseLine: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if _, err := plugin.Install(cmd.Context(), args[0], plugin.Options{Insecure: pluginOptions.Insecure}); err != nil { + return xerrors.Errorf("plugin install error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "uninstall PLUGIN_NAME", + Aliases: []string{"u"}, + DisableFlagsInUseLine: true, + Short: "Uninstall a plugin", + SilenceErrors: true, + SilenceUsage: true, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if err := plugin.Uninstall(cmd.Context(), args[0]); err != nil { + return xerrors.Errorf("plugin uninstall error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "list", + Aliases: []string{"l"}, + DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, + Short: "List installed plugin", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + if err := plugin.List(cmd.Context()); err != nil { + return xerrors.Errorf("plugin list display error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "info PLUGIN_NAME", + Short: "Show information about the specified plugin", + DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, + Args: cobra.ExactArgs(1), + RunE: func(_ *cobra.Command, args []string) error { + if err := plugin.Information(args[0]); err != nil { + return xerrors.Errorf("plugin information display error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "run NAME | URL | FILE_PATH", + Aliases: []string{"r"}, + DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, + Short: "Run a plugin on the fly", + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return plugin.Run(cmd.Context(), args[0], plugin.Options{ + Args: args[1:], + Insecure: pluginOptions.Insecure, + }) + }, + }, + &cobra.Command{ + Use: "update", + Short: "Update the local copy of the plugin index", + DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, _ []string) error { + if err := plugin.Update(cmd.Context(), plugin.Options{Insecure: pluginOptions.Insecure}); err != nil { + return xerrors.Errorf("plugin update error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "search [KEYWORD]", + DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, + Short: "List Trivy plugins available on the plugin index and search among them", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + var keyword string + if len(args) == 1 { + keyword = args[0] + } + return plugin.Search(cmd.Context(), keyword) + }, + }, + &cobra.Command{ + Use: "upgrade [PLUGIN_NAMES]", + Short: "Upgrade installed plugins to newer versions", + DisableFlagsInUseLine: true, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + if err := plugin.Upgrade(cmd.Context(), args); err != nil { + return xerrors.Errorf("plugin upgrade error: %w", err) + } + return nil + }, + }, + ) + cmd.SetFlagErrorFunc(flagErrorFunc) + return cmd +} + +func NewModuleCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + moduleFlags := &flag.Flags{ + globalFlags, + flag.NewModuleFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "module subcommand", + Aliases: []string{"m"}, + GroupID: groupManagement, + Short: "Manage modules", + SilenceErrors: true, + SilenceUsage: true, + } + + // Add subcommands + cmd.AddCommand( + &cobra.Command{ + Use: "install [flags] REPOSITORY", + Aliases: []string{"i"}, + Short: "Install a module", + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := moduleFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return cmd.Help() + } + + repo := args[0] + opts, err := moduleFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return module.Install(cmd.Context(), opts.ModuleDir, repo, opts.Quiet, opts.RegistryOpts()) + }, + }, + &cobra.Command{ + Use: "uninstall [flags] REPOSITORY", + Aliases: []string{"u"}, + Short: "Uninstall a module", + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := moduleFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return cmd.Help() + } + + repo := args[0] + opts, err := moduleFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return module.Uninstall(cmd.Context(), opts.ModuleDir, repo) + }, + }, + ) + moduleFlags.AddFlags(cmd) + cmd.SetFlagErrorFunc(flagErrorFunc) + return cmd +} + +func NewKubernetesCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + scanFlags := flag.NewScanFlagGroup() + scanners := flag.ScannersFlag.Clone() + // overwrite the default scanners + scanners.Values = xstrings.ToStringSlice(types.Scanners{ + types.VulnerabilityScanner, + types.MisconfigScanner, + types.SecretScanner, + types.RBACScanner, + }) + scanners.Default = scanners.Values + scanFlags.Scanners = scanners + + // required only SourceFlag + imageFlags := &flag.ImageFlagGroup{ImageSources: flag.SourceFlag.Clone()} + + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ExitOnEOL = nil // disable '--exit-on-eol' + reportFlagGroup.TableMode = nil // disable '--table-mode's + compliance := flag.ComplianceFlag.Clone() + var compliances string + for _, val := range types.BuiltInK8sCompliances { + compliances += fmt.Sprintf("\n - %s", val) + } + compliance.Usage = fmt.Sprintf("%s\nBuilt-in compliance's:%s", compliance.Usage, compliances) + reportFlagGroup.Compliance = compliance // override usage as the accepted values differ for each subcommand. + + formatFlag := flag.FormatFlag.Clone() + formatFlag.Values = xstrings.ToStringSlice([]types.Format{ + types.FormatTable, + types.FormatJSON, + types.FormatCycloneDX, + }) + reportFlagGroup.Format = formatFlag + + misconfFlagGroup := flag.NewMisconfFlagGroup() + misconfFlagGroup.CloudformationParamVars = nil // disable '--cf-params' + misconfFlagGroup.TerraformTFVars = nil // disable '--tf-vars' + + packageFlagGroup := flag.NewPackageFlagGroup() + packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps' + + k8sFlags := &flag.Flags{ + globalFlags, + flag.NewCacheFlagGroup(), + flag.NewDBFlagGroup(), + imageFlags, + flag.NewK8sFlagGroup(), // kubernetes-specific flags + misconfFlagGroup, + packageFlagGroup, + flag.NewRegoFlagGroup(), + reportFlagGroup, + scanFlags, + flag.NewSecretFlagGroup(), + flag.NewRegistryFlagGroup(), + flag.NewVulnerabilityFlagGroup(), + } + + cmd := &cobra.Command{ + Use: "kubernetes [flags] [CONTEXT]", + Aliases: []string{"k8s"}, + GroupID: groupScanning, + Short: "[EXPERIMENTAL] Scan kubernetes cluster", + Long: `Default context in kube configuration will be used unless specified`, + Example: ` # cluster scanning + $ trivy k8s --report summary + + # cluster scanning with specific namespace: + $ trivy k8s --include-namespaces kube-system --report summary + + # cluster with specific context: + $ trivy k8s kind-kind --report summary + + +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := k8sFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := k8sFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + opts, err := k8sFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + + return k8scommands.Run(cmd.Context(), args, opts) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + k8sFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, k8sFlags.Usages(cmd))) + + return cmd +} + +func NewVMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.ReportFormat = nil // disable '--report' + + packageFlagGroup := flag.NewPackageFlagGroup() + packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps' + + misconfFlagGroup := flag.NewMisconfFlagGroup() + misconfFlagGroup.CloudformationParamVars = nil // disable '--cf-params' + misconfFlagGroup.TerraformTFVars = nil // disable '--tf-vars' + + vmFlags := &flag.Flags{ + globalFlags, + flag.NewCacheFlagGroup(), + flag.NewDBFlagGroup(), + misconfFlagGroup, + flag.NewModuleFlagGroup(), + packageFlagGroup, + flag.NewClientFlags(), // for client/server mode + reportFlagGroup, + flag.NewScanFlagGroup(), + flag.NewSecretFlagGroup(), + flag.NewVulnerabilityFlagGroup(), + &flag.AWSFlagGroup{ + Region: &flag.Flag[string]{ + Name: "aws-region", + ConfigName: "aws.region", + Usage: "AWS region to scan", + }, + }, + } + + cmd := &cobra.Command{ + Use: "vm [flags] VM_IMAGE", + Aliases: []string{}, + GroupID: groupScanning, + Short: "[EXPERIMENTAL] Scan a virtual machine image", + Example: ` # Scan your AWS AMI + $ trivy vm --scanners vuln ami:${your_ami_id} + + # Scan your AWS EBS snapshot + $ trivy vm ebs:${your_ebs_snapshot_id} +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := vmFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := vmFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := vmFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + if options.Timeout < time.Minute*30 { + options.Timeout = time.Minute * 30 + log.Info("Timeout is set to less than 30 min - upgrading to 30 min for this command.") + } + return artifact.Run(cmd.Context(), options, artifact.TargetVM) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + vmFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, vmFlags.Usages(cmd))) + + return cmd +} + +func NewSBOMCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + reportFlagGroup := flag.NewReportFlagGroup() + reportFlagGroup.DependencyTree = nil // disable '--dependency-tree' + reportFlagGroup.ReportFormat = nil // TODO: support --report summary + + scanners := flag.ScannersFlag.Clone() + scanners.Values = xstrings.ToStringSlice(types.Scanners{ + types.VulnerabilityScanner, + types.LicenseScanner, + }) + scanners.Default = xstrings.ToStringSlice(types.Scanners{ + types.VulnerabilityScanner, + }) + scanFlagGroup := flag.NewScanFlagGroup() + scanFlagGroup.Scanners = scanners // allow only 'vuln' and 'license' options for '--scanners' + scanFlagGroup.Parallel = nil // disable '--parallel' + scanFlagGroup.SkipFiles = nil // disable `--skip-files` since `sbom` command only supports scanning one file. + scanFlagGroup.SkipDirs = nil // disable `--skip-dirs` since `sbom` command only supports scanning one file. + + licenseFlagGroup := flag.NewLicenseFlagGroup() + // License full-scan and confidence-level are for file content only + licenseFlagGroup.LicenseFull = nil + licenseFlagGroup.LicenseConfidenceLevel = nil + + cacheFlagGroup := flag.NewCacheFlagGroup() + cacheFlagGroup.CacheBackend.Default = string(cache.TypeMemory) // Use memory cache by default + + packageFlagGroup := flag.NewPackageFlagGroup() + packageFlagGroup.IncludeDevDeps = nil // disable '--include-dev-deps' + + sbomFlags := &flag.Flags{ + globalFlags, + cacheFlagGroup, + flag.NewDBFlagGroup(), + packageFlagGroup, + flag.NewClientFlags(), // for client/server mode + flag.NewRegistryFlagGroup(), // for DBs in private registries + reportFlagGroup, + scanFlagGroup, + flag.NewVulnerabilityFlagGroup(), + licenseFlagGroup, + } + + cmd := &cobra.Command{ + Use: "sbom [flags] SBOM_PATH", + Short: "Scan SBOM for vulnerabilities and licenses", + GroupID: groupScanning, + Example: ` # Scan CycloneDX and show the result in tables + $ trivy sbom /path/to/report.cdx + + # Scan CycloneDX-type attestation and show the result in tables + $ trivy sbom /path/to/report.cdx.intoto.jsonl +`, + PreRunE: func(cmd *cobra.Command, args []string) error { + if err := sbomFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return validateArgs(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if err := sbomFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + options, err := sbomFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + + return artifact.Run(cmd.Context(), options, artifact.TargetSBOM) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + sbomFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, sbomFlags.Usages(cmd))) + + return cmd +} + +func NewCleanCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + cleanFlags := &flag.Flags{ + globalFlags, + flag.NewCleanFlagGroup(), + } + cmd := &cobra.Command{ + Use: "clean [flags]", + GroupID: groupUtility, + Short: "Remove cached files", + Example: ` # Remove all caches + $ trivy clean --all + + # Remove scan cache + $ trivy clean --scan-cache + + # Remove vulnerability database + $ trivy clean --vuln-db +`, + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := cleanFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + opts, err := cleanFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + + return clean.Run(cmd.Context(), opts) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + cleanFlags.AddFlags(cmd) + cmd.SetUsageTemplate(fmt.Sprintf(usageTemplate, cleanFlags.Usages(cmd))) + + return cmd +} + +func NewRegistryCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + cmd := &cobra.Command{ + Use: "registry [flags]", + GroupID: groupUtility, + Short: "Manage registry authentication", + SilenceErrors: true, + SilenceUsage: true, + } + + registryFlagGroup := flag.NewRegistryFlagGroup() + registryFlagGroup.RegistryToken = nil + + loginFlags := &flag.Flags{ + globalFlags, + registryFlagGroup, + } + loginCmd := &cobra.Command{ + Use: "login SERVER", + Short: "Log in to a registry", + SilenceErrors: true, + SilenceUsage: true, + Example: ` # Log in to reg.example.com + cat ~/my_password.txt | trivy registry login --username foo --password-stdin reg.example.com`, + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, _ []string) error { + if err := loginFlags.Bind(cmd); err != nil { + return xerrors.Errorf("flag bind error: %w", err) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + loginOpts, err := loginFlags.ToOptions(args) + if err != nil { + return xerrors.Errorf("flag error: %w", err) + } + return auth.Login(cmd.Context(), args[0], loginOpts) + }, + } + logoutCmd := &cobra.Command{ + Use: "logout SERVER", + Short: "Log out of a registry", + SilenceErrors: true, + SilenceUsage: true, + Example: ` # Log out of reg.example.com + trivy registry logout reg.example.com`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return auth.Logout(cmd.Context(), args[0]) + }, + } + loginFlags.AddFlags(loginCmd) + cmd.AddCommand(loginCmd, logoutCmd) + + cmd.SetFlagErrorFunc(flagErrorFunc) + + return cmd +} + +func NewVEXCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + vexFlags := flag.Flags{ + globalFlags, + } + var vexOptions flag.Options + + cmd := &cobra.Command{ + Use: "vex subcommand", + GroupID: groupManagement, + Short: "[EXPERIMENTAL] VEX utilities", + SilenceErrors: true, + SilenceUsage: true, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + cmd.SetContext(log.WithContextPrefix(cmd.Context(), "vex")) + + opts, err := vexFlags.ToOptions(args) + if err != nil { + return err + } + vexOptions = opts + return nil + }, + } + + repoCmd := &cobra.Command{ + Use: "repo subcommand", + Short: "Manage VEX repositories", + SilenceErrors: true, + SilenceUsage: true, + Example: ` # Initialize the configuration file + $ trivy vex repo init + + # List VEX repositories + $ trivy vex repo list + + # Download the VEX repositories + $ trivy vex repo download +`, + } + + repoCmd.AddCommand( + &cobra.Command{ + Use: "init", + Short: "Initialize a configuration file", + SilenceErrors: true, + SilenceUsage: true, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, _ []string) error { + if err := vexrepo.NewManager(vexOptions.CacheDir).Init(cmd.Context()); err != nil { + return xerrors.Errorf("config init error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "list", + Short: "List VEX repositories", + SilenceErrors: true, + SilenceUsage: true, + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, _ []string) error { + if err := vexrepo.NewManager(vexOptions.CacheDir).List(cmd.Context()); err != nil { + return xerrors.Errorf("list error: %w", err) + } + return nil + }, + }, + &cobra.Command{ + Use: "download [REPO_NAMES]", + Short: "Download the VEX repositories", + Long: `Downloads enabled VEX repositories. If specific repository names are provided as arguments, only those repositories will be downloaded. Otherwise, all enabled repositories are downloaded.`, + SilenceErrors: true, + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + err := vexrepo.NewManager(vexOptions.CacheDir).DownloadRepositories(cmd.Context(), args, + vexrepo.Options{Insecure: vexOptions.Insecure}) + if err != nil { + return xerrors.Errorf("repository download error: %w", err) + } + return nil + }, + }, + ) + + cmd.AddCommand(repoCmd) + return cmd +} + +func NewVersionCommand(globalFlags *flag.GlobalFlagGroup) *cobra.Command { + var versionFormat string + cmd := &cobra.Command{ + Use: "version [flags]", + Short: "Print the version", + GroupID: groupUtility, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + flags := flag.Flags{globalFlags} + opts, err := flags.ToOptions(args) + if err != nil { + return err + } + return showVersion(opts.CacheDir, versionFormat, cmd.OutOrStdout()) + }, + SilenceErrors: true, + SilenceUsage: true, + } + cmd.SetFlagErrorFunc(flagErrorFunc) + + // Add version format flag, only json is supported + cmd.Flags().StringVarP(&versionFormat, flag.FormatFlag.Name, flag.FormatFlag.Shorthand, "", "version format (json)") + + return cmd +} + +func showVersion(cacheDir, outputFormat string, w io.Writer) error { + versionInfo := version.NewVersionInfo(cacheDir) + switch outputFormat { + case "json": + if err := json.NewEncoder(w).Encode(versionInfo); err != nil { + return xerrors.Errorf("json encode error: %w", err) + } + default: + fmt.Fprint(w, versionInfo.String()) + } + return nil +} + +func validateArgs(cmd *cobra.Command, args []string) error { + // '--clear-cache' (removed), '--download-db-only', '--download-java-db-only', '--reset' (removed), + // '--reset-checks-bundle' (removed) and '--generate-default-config' don't conduct the subsequent scanning + if viper.GetBool(flag.ClearCacheFlag.ConfigName) || viper.GetBool(flag.DownloadDBOnlyFlag.ConfigName) || + viper.GetBool(flag.ResetFlag.ConfigName) || viper.GetBool(flag.GenerateDefaultConfigFlag.ConfigName) || + viper.GetBool(flag.DownloadJavaDBOnlyFlag.ConfigName) || viper.GetBool(flag.ResetChecksBundleFlag.ConfigName) { + return nil + } + + if len(args) == 0 && viper.GetString(flag.InputFlag.ConfigName) == "" && cmd.Name() != "kubernetes" { + if err := cmd.Help(); err != nil { + return err + } + + if f := cmd.Flags().Lookup(flag.InputFlag.ConfigName); f != nil { + return xerrors.New(`Require at least 1 argument or --input option`) + } + return xerrors.New(`Require at least 1 argument`) + } else if cmd.Name() != "kubernetes" && len(args) > 1 { + if err := cmd.Help(); err != nil { + return err + } + return xerrors.New(`multiple targets cannot be specified`) + } + + return nil +} + +// show help on using the command when an invalid flag is encountered +func flagErrorFunc(command *cobra.Command, err error) error { + if err := command.Help(); err != nil { + return err + } + command.Println() // add empty line after list of flags + return err +} diff --git a/pkg/commands/app_test.go b/pkg/commands/app_test.go new file mode 100644 index 000000000000..52f03d2c8618 --- /dev/null +++ b/pkg/commands/app_test.go @@ -0,0 +1,354 @@ +package commands + +import ( + "bytes" + "io" + "testing" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/types" +) + +func Test_showVersion(t *testing.T) { + type args struct { + cacheDir string + outputFormat string + version string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "happy path, table output", + args: args{ + outputFormat: "table", + version: "dev", + cacheDir: "testdata", + }, + want: `Version: dev +Vulnerability DB: + Version: 2 + UpdatedAt: 2022-03-02 06:07:07.99504083 +0000 UTC + NextUpdate: 2022-03-02 12:07:07.99504023 +0000 UTC + DownloadedAt: 2022-03-02 10:03:38.383312 +0000 UTC +Java DB: + Version: 1 + UpdatedAt: 2023-03-14 00:47:02.774253754 +0000 UTC + NextUpdate: 2023-03-17 00:47:02.774253254 +0000 UTC + DownloadedAt: 2023-03-14 03:04:55.058541039 +0000 UTC +Check Bundle: + Digest: sha256:19a017cdc798631ad42f6f4dce823d77b2989128f0e1a7f9bc83ae3c59024edd + DownloadedAt: 2023-03-02 01:06:08.191725 +0000 UTC +`, + }, + { + name: "sad path, bogus cache dir", + args: args{ + outputFormat: "json", + version: "dev", + cacheDir: "/foo/bar/bogus", + }, + want: `{"Version":"dev"} +`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := new(bytes.Buffer) + showVersion(tt.args.cacheDir, tt.args.outputFormat, got) + assert.Equal(t, tt.want, got.String(), tt.name) + }) + } +} + +// Check flag and command for print version +func TestPrintVersion(t *testing.T) { + tableOutput := `Version: dev +Vulnerability DB: + Version: 2 + UpdatedAt: 2022-03-02 06:07:07.99504083 +0000 UTC + NextUpdate: 2022-03-02 12:07:07.99504023 +0000 UTC + DownloadedAt: 2022-03-02 10:03:38.383312 +0000 UTC +Java DB: + Version: 1 + UpdatedAt: 2023-03-14 00:47:02.774253754 +0000 UTC + NextUpdate: 2023-03-17 00:47:02.774253254 +0000 UTC + DownloadedAt: 2023-03-14 03:04:55.058541039 +0000 UTC +Check Bundle: + Digest: sha256:19a017cdc798631ad42f6f4dce823d77b2989128f0e1a7f9bc83ae3c59024edd + DownloadedAt: 2023-03-02 01:06:08.191725 +0000 UTC +` + jsonOutput := `{"Version":"dev","VulnerabilityDB":{"Version":2,"NextUpdate":"2022-03-02T12:07:07.99504023Z","UpdatedAt":"2022-03-02T06:07:07.99504083Z","DownloadedAt":"2022-03-02T10:03:38.383312Z"},"JavaDB":{"Version":1,"NextUpdate":"2023-03-17T00:47:02.774253254Z","UpdatedAt":"2023-03-14T00:47:02.774253754Z","DownloadedAt":"2023-03-14T03:04:55.058541039Z"},"CheckBundle":{"Digest":"sha256:19a017cdc798631ad42f6f4dce823d77b2989128f0e1a7f9bc83ae3c59024edd","DownloadedAt":"2023-03-02T01:06:08.191725Z"}} +` + tests := []struct { + name string + arguments []string // 1st argument is path to trivy binaries + want string + }{ + { + name: "happy path. '-v' flag is used", + arguments: []string{ + "-v", + "--cache-dir", + "testdata", + }, + want: tableOutput, + }, + { + name: "happy path. '-version' flag is used", + arguments: []string{ + "--version", + "--cache-dir", + "testdata", + }, + want: tableOutput, + }, + { + name: "happy path. 'version' command is used", + arguments: []string{ + "--cache-dir", + "testdata", + "version", + }, + want: tableOutput, + }, + { + name: "happy path. 'version', '--format json' flags are used", + arguments: []string{ + "--cache-dir", + "testdata", + "version", + "--format", + "json", + }, + want: jsonOutput, + }, + { + name: "happy path. '-v', '--format json' flags are used", + arguments: []string{ + "--cache-dir", + "testdata", + "-v", + "--format", + "json", + }, + want: jsonOutput, + }, + { + name: "happy path. '--version', '--format json' flags are used", + arguments: []string{ + "--cache-dir", + "testdata", + "--version", + "--format", + "json", + }, + want: jsonOutput, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := new(bytes.Buffer) + app := NewApp() + app.SetOut(got) + app.SetArgs(test.arguments) + + err := app.Execute() + require.NoError(t, err) + assert.Equal(t, test.want, got.String()) + }) + } +} + +func TestFlags(t *testing.T) { + type want struct { + format types.Format + severities []dbTypes.Severity + scanners types.Scanners + } + tests := []struct { + name string + arguments []string // 1st argument is path to trivy binaries + want want + wantErr string + }{ + { + name: "happy path", + arguments: []string{ + "test", + }, + want: want{ + format: types.FormatTable, + severities: []dbTypes.Severity{ + dbTypes.SeverityUnknown, + dbTypes.SeverityLow, + dbTypes.SeverityMedium, + dbTypes.SeverityHigh, + dbTypes.SeverityCritical, + }, + scanners: types.Scanners{ + types.VulnerabilityScanner, + types.SecretScanner, + types.SBOMScanner, + }, + }, + }, + { + name: "happy path with comma-separated severities", + arguments: []string{ + "test", + "--severity", + "LOW,MEDIUM", + }, + want: want{ + format: types.FormatTable, + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + dbTypes.SeverityMedium, + }, + scanners: types.Scanners{ + types.VulnerabilityScanner, + types.SecretScanner, + types.SBOMScanner, + }, + }, + }, + { + name: "happy path with repeated severities", + arguments: []string{ + "test", + "--severity", + "LOW", + "--severity", + "HIGH", + }, + want: want{ + format: types.FormatTable, + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + dbTypes.SeverityHigh, + }, + scanners: types.Scanners{ + types.VulnerabilityScanner, + types.SecretScanner, + types.SBOMScanner, + }, + }, + }, + { + name: "happy path with json", + arguments: []string{ + "test", + "--format", + "json", + "--severity", + "CRITICAL", + }, + want: want{ + format: types.FormatJSON, + severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, + }, + scanners: types.Scanners{ + types.VulnerabilityScanner, + types.SecretScanner, + types.SBOMScanner, + }, + }, + }, + { + name: "happy path with scanners for compliance report", + arguments: []string{ + "test", + "--scanners", + "license", + "--compliance", + "docker-cis-1.6.0", + }, + want: want{ + format: types.FormatTable, + severities: []dbTypes.Severity{ + dbTypes.SeverityUnknown, + dbTypes.SeverityLow, + dbTypes.SeverityMedium, + dbTypes.SeverityHigh, + dbTypes.SeverityCritical, + }, + scanners: types.Scanners{ + types.VulnerabilityScanner, + }, + }, + }, + { + name: "invalid format", + arguments: []string{ + "test", + "--format", + "foo", + }, + wantErr: `invalid argument "foo" for "--format" flag`, + }, + { + name: "missing config file", + arguments: []string{ + "test", + "--config", + "none", + }, + wantErr: `config file "none" loading error: open none:`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + globalFlags := flag.NewGlobalFlagGroup() + rootCmd := NewRootCommand(globalFlags) + rootCmd.SetErr(io.Discard) + rootCmd.SetOut(io.Discard) + + flags := &flag.Flags{ + globalFlags, + flag.NewReportFlagGroup(), + flag.NewScanFlagGroup(), + } + cmd := &cobra.Command{ + Use: "test", + RunE: func(cmd *cobra.Command, args []string) error { + // Bind + if err := flags.Bind(cmd); err != nil { + return err + } + + options, err := flags.ToOptions(args) + if err != nil { + return err + } + + assert.Equal(t, tt.want.format, options.Format) + assert.Equal(t, tt.want.severities, options.Severities) + assert.Equal(t, tt.want.scanners, options.Scanners) + return nil + }, + } + flags.AddFlags(cmd) + rootCmd.AddCommand(cmd) + + rootCmd.SetArgs(tt.arguments) + + err := rootCmd.Execute() + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} diff --git a/pkg/commands/artifact/run.go b/pkg/commands/artifact/run.go new file mode 100644 index 000000000000..365f9454e74d --- /dev/null +++ b/pkg/commands/artifact/run.go @@ -0,0 +1,824 @@ +package artifact + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "maps" + "os" + "slices" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/samber/lo" + "github.com/spf13/viper" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/commands/operation" + "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/extension" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/javadb" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/module" + "github.com/aquasecurity/trivy/pkg/notification" + "github.com/aquasecurity/trivy/pkg/policy" + pkgReport "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/result" + "github.com/aquasecurity/trivy/pkg/rpc/client" + "github.com/aquasecurity/trivy/pkg/scan" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/version/doc" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" +) + +// TargetKind represents what kind of artifact Trivy scans +type TargetKind string + +const ( + TargetContainerImage TargetKind = "image" + TargetFilesystem TargetKind = "fs" + TargetRootfs TargetKind = "rootfs" + TargetRepository TargetKind = "repo" + TargetSBOM TargetKind = "sbom" + TargetVM TargetKind = "vm" + TargetK8s TargetKind = "k8s" +) + +var SkipScan = errors.New("skip subsequent processes") + +// InitializeScanService defines the initialize function signature of scan service +type InitializeScanService func(context.Context, ScannerConfig) (scan.Service, func(), error) + +type ScannerConfig struct { + // e.g. image name and file path + Target string + + // Cache + CacheOptions cache.Options + RemoteCacheOptions cache.RemoteOptions + + // Client/Server options + ServerOption client.ServiceOption + + // Artifact options + ArtifactOption artifact.Option +} + +type Runner interface { + // ScanImage scans an image + ScanImage(ctx context.Context, opts flag.Options) (types.Report, error) + // ScanFilesystem scans a filesystem + ScanFilesystem(ctx context.Context, opts flag.Options) (types.Report, error) + // ScanRootfs scans rootfs + ScanRootfs(ctx context.Context, opts flag.Options) (types.Report, error) + // ScanRepository scans repository + ScanRepository(ctx context.Context, opts flag.Options) (types.Report, error) + // ScanSBOM scans SBOM + ScanSBOM(ctx context.Context, opts flag.Options) (types.Report, error) + // ScanVM scans VM + ScanVM(ctx context.Context, opts flag.Options) (types.Report, error) + // Filter filter a report + Filter(ctx context.Context, opts flag.Options, report types.Report) (types.Report, error) + // Report a writes a report + Report(ctx context.Context, opts flag.Options, report types.Report) error + // Close closes runner + Close(ctx context.Context) error +} + +type runner struct { + initializeScanService InitializeScanService + versionChecker *notification.VersionChecker + dbOpen bool + + // WASM modules + module *module.Manager +} + +type RunnerOption func(*runner) + +// WithInitializeService takes a custom service initialization function. +// It is useful when Trivy is imported as a library. +func WithInitializeService(f InitializeScanService) RunnerOption { + return func(r *runner) { + r.initializeScanService = f + } +} + +// NewRunner initializes Runner that provides scanning functionalities. +// It is possible to return SkipScan and it must be handled by caller. +func NewRunner(ctx context.Context, cliOptions flag.Options, targetKind TargetKind, opts ...RunnerOption) (_ Runner, err error) { + r := &runner{} + for _, opt := range opts { + opt(r) + } + + defer func() { + if err != nil { + if cErr := r.Close(ctx); cErr != nil { + log.ErrorContext(ctx, "failed to close runner: %s", cErr) + } + } + }() + + // Set the default HTTP transport + xhttp.SetDefaultTransport(xhttp.NewTransport(xhttp.Options{ + Insecure: cliOptions.Insecure, + CACerts: cliOptions.CACerts, + Timeout: cliOptions.Timeout, + TraceHTTP: cliOptions.TraceHTTP, + })) + + r.versionChecker = notification.NewVersionChecker(string(targetKind), &cliOptions) + + // Update the vulnerability database if needed. + if err := r.initDB(ctx, cliOptions); err != nil { + return nil, xerrors.Errorf("DB error: %w", err) + } + + // Update the VEX repositories if needed + if err := operation.DownloadVEXRepositories(ctx, cliOptions); err != nil { + return nil, xerrors.Errorf("VEX repositories download error: %w", err) + } + + // Initialize WASM modules + m, err := module.NewManager(ctx, module.Options{ + Dir: cliOptions.ModuleDir, + EnabledModules: cliOptions.EnabledModules, + }) + if err != nil { + return nil, xerrors.Errorf("WASM module error: %w", err) + } + m.Register() + r.module = m + + // Make a silent attempt to check for updates in the background + // only do this if the user has not disabled notices or is running + // in quiet mode + if r.versionChecker != nil { + r.versionChecker.RunUpdateCheck(ctx) + } + + return r, nil +} + +// Close closes everything +func (r *runner) Close(ctx context.Context) error { + var errs error + if r.dbOpen { + if err := db.Close(); err != nil { + errs = multierror.Append(errs, err) + } + } + + if r.module != nil { + if err := r.module.Close(ctx); err != nil { + errs = multierror.Append(errs, err) + } + } + + // silently check if there is notifications + if r.versionChecker != nil { + r.versionChecker.PrintNotices(ctx, os.Stderr) + } + + return errs +} + +func (r *runner) ScanImage(ctx context.Context, opts flag.Options) (types.Report, error) { + // Disable the lock file scanning + opts.DisabledAnalyzers = analyzer.TypeLockfiles + + var s InitializeScanService + switch { + case opts.Input != "" && opts.ServerAddr == "": + // Scan image tarball in standalone mode + s = archiveStandaloneScanService + case opts.Input != "" && opts.ServerAddr != "": + // Scan image tarball in client/server mode + s = archiveRemoteScanService + case opts.Input == "" && opts.ServerAddr == "": + // Scan container image in standalone mode + s = imageStandaloneScanService + case opts.Input == "" && opts.ServerAddr != "": + // Scan container image in client/server mode + s = imageRemoteScanService + } + + return r.scanArtifact(ctx, opts, s) +} + +func (r *runner) ScanFilesystem(ctx context.Context, opts flag.Options) (types.Report, error) { + // Disable scanning of individual package and SBOM files + opts.DisabledAnalyzers = append(opts.DisabledAnalyzers, analyzer.TypeIndividualPkgs...) + opts.DisabledAnalyzers = append(opts.DisabledAnalyzers, analyzer.TypeSBOM) + + return r.scanFS(ctx, opts) +} + +func (r *runner) ScanRootfs(ctx context.Context, opts flag.Options) (types.Report, error) { + // Disable the lock file scanning + opts.DisabledAnalyzers = append(opts.DisabledAnalyzers, analyzer.TypeLockfiles...) + + return r.scanFS(ctx, opts) +} + +func (r *runner) scanFS(ctx context.Context, opts flag.Options) (types.Report, error) { + var s InitializeScanService + if opts.ServerAddr == "" { + // Scan filesystem in standalone mode + s = filesystemStandaloneScanService + } else { + // Scan filesystem in client/server mode + s = filesystemRemoteScanService + } + + return r.scanArtifact(ctx, opts, s) +} + +func (r *runner) ScanRepository(ctx context.Context, opts flag.Options) (types.Report, error) { + // Do not scan OS packages + opts.PkgTypes = []string{types.PkgTypeLibrary} + + // Disable the OS analyzers, individual package analyzers and SBOM analyzer + opts.DisabledAnalyzers = append(analyzer.TypeIndividualPkgs, analyzer.TypeOSes...) + opts.DisabledAnalyzers = append(opts.DisabledAnalyzers, analyzer.TypeSBOM) + + var s InitializeScanService + if opts.ServerAddr == "" { + // Scan repository in standalone mode + s = repositoryStandaloneScanService + } else { + // Scan repository in client/server mode + s = repositoryRemoteScanService + } + return r.scanArtifact(ctx, opts, s) +} + +func (r *runner) ScanSBOM(ctx context.Context, opts flag.Options) (types.Report, error) { + var s InitializeScanService + if opts.ServerAddr == "" { + // Scan cycloneDX in standalone mode + s = sbomStandaloneScanService + } else { + // Scan cycloneDX in client/server mode + s = sbomRemoteScanService + } + + return r.scanArtifact(ctx, opts, s) +} + +func (r *runner) ScanVM(ctx context.Context, opts flag.Options) (types.Report, error) { + // TODO: Does VM scan disable lock file..? + opts.DisabledAnalyzers = analyzer.TypeLockfiles + + var s InitializeScanService + if opts.ServerAddr == "" { + // Scan virtual machine in standalone mode + s = vmStandaloneScanService + } else { + // Scan virtual machine in client/server mode + s = vmRemoteScanService + } + + return r.scanArtifact(ctx, opts, s) +} + +func (r *runner) scanArtifact(ctx context.Context, opts flag.Options, initializeService InitializeScanService) (types.Report, error) { + if r.initializeScanService != nil { + initializeService = r.initializeScanService + } + report, err := r.scan(ctx, opts, initializeService) + if err != nil { + return types.Report{}, xerrors.Errorf("scan error: %w", err) + } + + return report, nil +} + +func (r *runner) Filter(ctx context.Context, opts flag.Options, report types.Report) (types.Report, error) { + // Filter results + if err := result.Filter(ctx, report, opts.FilterOpts()); err != nil { + return types.Report{}, xerrors.Errorf("filtering error: %w", err) + } + return report, nil +} + +func (r *runner) Report(ctx context.Context, opts flag.Options, report types.Report) error { + if err := pkgReport.Write(ctx, report, opts); err != nil { + return xerrors.Errorf("unable to write results: %w", err) + } + return nil +} + +func (r *runner) initDB(ctx context.Context, opts flag.Options) error { + if err := r.initJavaDB(opts); err != nil { + return err + } + + // When scanning config files or running as client mode, it doesn't need to download the vulnerability database. + if opts.ServerAddr != "" || !opts.Scanners.Enabled(types.VulnerabilityScanner) { + return nil + } + + // download the database file + noProgress := opts.Quiet || opts.NoProgress + if err := operation.DownloadDB(ctx, opts.AppVersion, opts.CacheDir, opts.DBRepositories, noProgress, opts.SkipDBUpdate, opts.RegistryOpts()); err != nil { + return err + } + + if opts.DownloadDBOnly { + return SkipScan + } + + if err := db.Init(db.Dir(opts.CacheDir)); err != nil { + return xerrors.Errorf("error in vulnerability DB initialize: %w", err) + } + r.dbOpen = true + + return nil +} + +func (r *runner) initJavaDB(opts flag.Options) error { + // When running as server mode, it doesn't need to download the Java database. + if opts.Listen != "" { + return nil + } + + // If vulnerability scanning and SBOM generation are disabled, it doesn't need to download the Java database. + if !opts.Scanners.Enabled(types.VulnerabilityScanner) && + !slices.Contains(types.SupportedSBOMFormats, opts.Format) { + return nil + } + + // Update the Java DB + noProgress := opts.Quiet || opts.NoProgress + javadb.Init(opts.CacheDir, opts.JavaDBRepositories, opts.SkipJavaDBUpdate, noProgress, opts.RegistryOpts()) + if opts.DownloadJavaDBOnly { + if err := javadb.Update(); err != nil { + return xerrors.Errorf("Java DB error: %w", err) + } + return SkipScan + } + + return nil +} + +// Run performs artifact scanning +func Run(ctx context.Context, opts flag.Options, targetKind TargetKind) (err error) { + ctx, cancel := context.WithTimeout(ctx, opts.Timeout) + defer cancel() + + if opts.GenerateDefaultConfig { + log.Info("Writing the default config to trivy-default.yaml...") + + hiddenFlags := flag.HiddenFlags() + // Viper does not have the ability to remove flags. + // So we only save the necessary flags and set these flags after viper.Reset + v := viper.New() + for _, k := range viper.AllKeys() { + // Skip the `GenerateDefaultConfigFlag` flags to avoid errors with default config file. + // Users often use "normal" formats instead of compliance. So we'll skip ComplianceFlag + // Also don't keep removed or deprecated flags to avoid confusing users. + if k == flag.GenerateDefaultConfigFlag.ConfigName || k == flag.ComplianceFlag.ConfigName || slices.Contains(hiddenFlags, k) { + continue + } + v.Set(k, viper.Get(k)) + } + + return v.SafeWriteConfigAs("trivy-default.yaml") + } + + // Call pre-run hooks + if err := extension.PreRun(ctx, opts); err != nil { + return xerrors.Errorf("pre run error: %w", err) + } + + // Run the application + report, err := run(ctx, opts, targetKind) + if err != nil { + return xerrors.Errorf("run error: %w", err) + } + + // Call post-run hooks + if err := extension.PostRun(ctx, opts); err != nil { + return xerrors.Errorf("post run error: %w", err) + } + + return operation.Exit(opts, report.Results.Failed(), report.Metadata) +} + +func run(ctx context.Context, opts flag.Options, targetKind TargetKind) (types.Report, error) { + // Perform validation checks + checkOptions(ctx, opts, targetKind) + + r, err := NewRunner(ctx, opts, targetKind) + if err != nil { + if errors.Is(err, SkipScan) { + return types.Report{}, nil + } + return types.Report{}, xerrors.Errorf("init error: %w", err) + } + defer r.Close(ctx) + + scans := map[TargetKind]func(context.Context, flag.Options) (types.Report, error){ + TargetContainerImage: r.ScanImage, + TargetFilesystem: r.ScanFilesystem, + TargetRootfs: r.ScanRootfs, + TargetRepository: r.ScanRepository, + TargetSBOM: r.ScanSBOM, + TargetVM: r.ScanVM, + } + + scanFunction, exists := scans[targetKind] + if !exists { + return types.Report{}, xerrors.Errorf("unknown target kind: %s", targetKind) + } + + // 1. Scan the artifact + report, err := scanFunction(ctx, opts) + if err != nil { + return types.Report{}, xerrors.Errorf("%s scan error: %w", targetKind, err) + } + + // 2. Filter the results + report, err = r.Filter(ctx, opts, report) + if err != nil { + return types.Report{}, xerrors.Errorf("filter error: %w", err) + } + + // 3. Report the results + if err = r.Report(ctx, opts, report); err != nil { + return types.Report{}, xerrors.Errorf("report error: %w", err) + } + + return report, nil +} + +// checkOptions performs various checks on scan options and shows warnings +func checkOptions(ctx context.Context, opts flag.Options, targetKind TargetKind) { + // Check client/server mode with misconfiguration and secret scanning + if opts.ServerAddr != "" && opts.Scanners.AnyEnabled(types.MisconfigScanner, types.SecretScanner) { + log.WarnContext(ctx, + fmt.Sprintf( + "Trivy runs in client/server mode, but misconfiguration and secret scanning will be done on the client side, see %s", + doc.URL("guide/references/modes/client-server", ""), + ), + ) + } + + // Check SBOM to SBOM scanning with package filtering flags + // For SBOM-to-SBOM scanning (for example, to add vulnerabilities to the SBOM file), we should not modify the scanned file. + // cf. https://github.com/aquasecurity/trivy/pull/9439#issuecomment-3295533665 + if targetKind == TargetSBOM && slices.Contains(types.SupportedSBOMFormats, opts.Format) && + (!slices.Equal(opts.PkgTypes, types.PkgTypes) || !slices.Equal(opts.PkgRelationships, ftypes.Relationships)) { + log.Warn("'--pkg-types' and '--pkg-relationships' options will be ignored when scanning SBOM and outputting SBOM format.") + } +} + +func disabledAnalyzers(opts flag.Options) []analyzer.Type { + // Specified analyzers to be disabled depending on scanning modes + // e.g. The 'image' subcommand should disable the lock file scanning. + analyzers := opts.DisabledAnalyzers + // It doesn't analyze apk commands by default. + if !opts.ScanRemovedPkgs { + analyzers = append(analyzers, analyzer.TypeApkCommand) + } + + // Do not analyze programming language packages when not running in 'library' + if !slices.Contains(opts.PkgTypes, types.PkgTypeLibrary) { + analyzers = append(analyzers, analyzer.TypeLanguages...) + } + + // Do not perform secret scanning when it is not specified. + if !opts.Scanners.Enabled(types.SecretScanner) { + analyzers = append(analyzers, analyzer.TypeSecret) + } + + // Do not perform misconfiguration scanning when it is not specified. + if !opts.Scanners.AnyEnabled(types.MisconfigScanner, types.RBACScanner) { + analyzers = append(analyzers, analyzer.TypeConfigFiles...) + } else { + // Filter only enabled misconfiguration scanners + ma := disabledMisconfigAnalyzers(opts.MisconfigScanners) + analyzers = append(analyzers, ma...) + + log.Debug("Enabling misconfiguration scanners", + log.Any("scanners", lo.Without(analyzer.TypeConfigFiles, ma...))) + } + + // Scanning file headers and license files is expensive. + // It is performed only when '--scanners license' and '--license-full' are specified together. + if !opts.Scanners.Enabled(types.LicenseScanner) || !opts.LicenseFull { + analyzers = append(analyzers, analyzer.TypeLicenseFile) + } + + // Parsing jar files requires Java-db client + // But we don't create client if vulnerability analysis is disabled and SBOM format is not used + // We need to disable jar analyzer to avoid errors + // TODO disable all languages that don't contain license information for this case + if !opts.Scanners.Enabled(types.VulnerabilityScanner) && !slices.Contains(types.SupportedSBOMFormats, opts.Format) { + analyzers = append(analyzers, analyzer.TypeJar) + } + + // Do not perform misconfiguration scanning on container image config + // when it is not specified. + if !opts.ImageConfigScanners.Enabled(types.MisconfigScanner) { + analyzers = append(analyzers, analyzer.TypeHistoryDockerfile) + } + + // Skip executable file analysis if Rekor isn't a specified SBOM source. + if !slices.Contains(opts.SBOMSources, types.SBOMSourceRekor) { + analyzers = append(analyzers, analyzer.TypeExecutable) + } + + // Disable RPM archive analyzer unless the environment variable is set + // TODO: add '--enable-analyzers' and delete this environment variable + if os.Getenv("TRIVY_EXPERIMENTAL_RPM_ARCHIVE") == "" { + analyzers = append(analyzers, analyzer.TypeRpmArchive) + } + + return analyzers +} + +func disabledMisconfigAnalyzers(included []analyzer.Type) []analyzer.Type { + _, missing := lo.Difference(analyzer.TypeConfigFiles, included) + if len(missing) > 0 { + log.Error( + "Invalid misconfiguration scanners provided, using default scanners", + log.Any("invalid_scanners", missing), log.Any("default_scanners", analyzer.TypeConfigFiles), + ) + return nil + } + + return lo.Without(analyzer.TypeConfigFiles, included...) +} + +func (r *runner) initScannerConfig(ctx context.Context, opts flag.Options) (ScannerConfig, types.ScanOptions, error) { + target := opts.Target + if opts.Input != "" { + target = opts.Input + } + + scanOptions := opts.ScanOpts() + + if len(opts.ImageConfigScanners) != 0 { + log.WithPrefix(log.PrefixContainerImage).Info("Container image config scanners", log.Any("scanners", opts.ImageConfigScanners)) + } + + if opts.Scanners.Enabled(types.SBOMScanner) { + logger := log.WithPrefix(log.PrefixPackage) + logger.Debug("Package types", log.Any("types", scanOptions.PkgTypes)) + logger.Debug("Package relationships", log.Any("relationships", scanOptions.PkgRelationships)) + } + + if opts.Scanners.Enabled(types.VulnerabilityScanner) { + log.WithPrefix(log.PrefixVulnerability).Info("Vulnerability scanning is enabled") + } + + // Misconfig ScannerOption is filled only when config scanning is enabled. + var configScannerOptions misconf.ScannerOption + if opts.Scanners.Enabled(types.MisconfigScanner) || opts.ImageConfigScanners.Enabled(types.MisconfigScanner) { + var err error + configScannerOptions, err = initMisconfScannerOption(ctx, opts) + if err != nil { + return ScannerConfig{}, types.ScanOptions{}, err + } + } + + // Do not load config file for secret scanning + if opts.Scanners.Enabled(types.SecretScanner) { + logger := log.WithPrefix(log.PrefixSecret) + logger.Info("Secret scanning is enabled") + if nonSecrets := lo.Without(opts.Scanners, types.SecretScanner, types.SBOMScanner); len(nonSecrets) > 0 { + logger.Info(fmt.Sprintf( + "If your scanning is slow, please try '--scanners %s' to disable secret scanning", + strings.Join(xstrings.ToStringSlice(nonSecrets), ","))) + } + // e.g. https://trivy.dev/docs/latest/scanner/secret/#recommendation + logger.Info(fmt.Sprintf("Please see %s for faster secret detection", doc.URL("guide/scanner/secret/", "recommendation"))) + } else { + opts.SecretConfigPath = "" + } + + if opts.Scanners.Enabled(types.LicenseScanner) { + logger := log.WithPrefix(log.PrefixLicense) + if opts.LicenseFull { + logger.Info("Full license scanning is enabled") + } else { + logger.Info("License scanning is enabled") + } + } + + // SPDX and CycloneDX need to calculate digests for package files + var fileChecksum bool + if opts.Format == types.FormatSPDXJSON || opts.Format == types.FormatSPDX || opts.Format == types.FormatCycloneDX { + fileChecksum = true + } + + // Disable the post handler for filtering system file when detection priority is comprehensive. + disabledHandlers := lo.Ternary(opts.DetectionPriority == ftypes.PriorityComprehensive, + []ftypes.HandlerType{ftypes.SystemFileFilteringPostHandler}, nil) + + return ScannerConfig{ + Target: target, + CacheOptions: opts.CacheOpts(), + RemoteCacheOptions: opts.RemoteCacheOpts(), + ServerOption: opts.ClientScannerOpts(), + ArtifactOption: artifact.Option{ + DisabledAnalyzers: disabledAnalyzers(opts), + DisabledHandlers: disabledHandlers, + FilePatterns: opts.FilePatterns, + Parallel: opts.Parallel, + Offline: opts.OfflineScan, + NoProgress: opts.NoProgress || opts.Quiet, + Insecure: opts.Insecure, + RepoBranch: opts.RepoBranch, + RepoCommit: opts.RepoCommit, + RepoTag: opts.RepoTag, + SBOMSources: opts.SBOMSources, + RekorURL: opts.RekorURL, + AWSRegion: opts.Region, + AWSEndpoint: opts.Endpoint, + FileChecksum: fileChecksum, + DetectionPriority: opts.DetectionPriority, + + // For image scanning + ImageOption: ftypes.ImageOptions{ + RegistryOptions: opts.RegistryOpts(), + DockerOptions: ftypes.DockerOptions{ + Host: opts.DockerHost, + }, + PodmanOptions: ftypes.PodmanOptions{ + Host: opts.PodmanHost, + }, + ImageSources: opts.ImageSources, + MaxImageSize: opts.MaxImageSize, + }, + + // For misconfiguration scanning + MisconfScannerOption: configScannerOptions, + + // For secret scanning + SecretScannerOption: analyzer.SecretScannerOption{ + ConfigPath: opts.SecretConfigPath, + }, + + // For license scanning + LicenseScannerOption: analyzer.LicenseScannerOption{ + Full: opts.LicenseFull, + ClassifierConfidenceLevel: opts.LicenseConfidenceLevel, + }, + + // For file walking + WalkerOption: walker.Option{ + SkipFiles: opts.SkipFiles, + SkipDirs: opts.SkipDirs, + }, + }, + }, scanOptions, nil +} + +func (r *runner) scan(ctx context.Context, opts flag.Options, initializeService InitializeScanService) (types.Report, error) { + scannerConfig, scanOptions, err := r.initScannerConfig(ctx, opts) + if err != nil { + return types.Report{}, err + } + s, cleanup, err := initializeService(ctx, scannerConfig) + if err != nil { + return types.Report{}, xerrors.Errorf("unable to initialize a scan service: %w", err) + } + defer cleanup() + + report, err := s.ScanArtifact(ctx, scanOptions) + if err != nil { + return types.Report{}, xerrors.Errorf("scan failed: %w", err) + } + return report, nil +} + +func initMisconfScannerOption(ctx context.Context, opts flag.Options) (misconf.ScannerOption, error) { + ctx = log.WithContextPrefix(ctx, log.PrefixMisconfiguration) + log.InfoContext(ctx, "Misconfiguration scanning is enabled") + + var downloadedPolicyPath string + var disableEmbedded bool + + c, err := policy.NewClient(opts.CacheDir, opts.Quiet, opts.MisconfOptions.ChecksBundleRepository) + if err != nil { + return misconf.ScannerOption{}, xerrors.Errorf("check client error: %w", err) + } + + downloadedPolicyPath, err = operation.InitBuiltinChecks(ctx, c, opts.SkipCheckUpdate, opts.RegistryOpts()) + if err != nil { + log.ErrorContext(ctx, "Falling back to embedded checks", log.Err(err)) + } else { + log.DebugContext(ctx, "Checks successfully loaded from disk") + disableEmbedded = true + } + + policyPaths := slices.Clone(opts.CheckPaths) + if downloadedPolicyPath != "" { + policyPaths = append(policyPaths, downloadedPolicyPath) + } + + configSchemas, err := misconf.LoadConfigSchemas(opts.ConfigFileSchemas) + if err != nil { + return misconf.ScannerOption{}, xerrors.Errorf("load schemas error: %w", err) + } + + ansibleExtraVars, err := resolveAnsibleExtraVars(opts.AnsibleExtraVars) + if err != nil { + log.DebugContext(ctx, "Failed to resolve Ansible extra-vars", log.Err(err)) + ansibleExtraVars = make(map[string]any) + } + + misconfOpts := misconf.ScannerOption{ + Trace: opts.RegoOptions.Trace, + Namespaces: append(opts.CheckNamespaces, rego.BuiltinNamespaces()...), + PolicyPaths: policyPaths, + DataPaths: opts.DataPaths, + HelmValues: opts.HelmValues, + HelmValueFiles: opts.HelmValueFiles, + HelmFileValues: opts.HelmFileValues, + HelmStringValues: opts.HelmStringValues, + HelmAPIVersions: opts.HelmAPIVersions, + HelmKubeVersion: opts.HelmKubeVersion, + TerraformTFVars: opts.TerraformTFVars, + CloudFormationParamVars: opts.CloudFormationParamVars, + K8sVersion: opts.K8sVersion, + DisableEmbeddedPolicies: disableEmbedded, + DisableEmbeddedLibraries: disableEmbedded, + IncludeDeprecatedChecks: opts.IncludeDeprecatedChecks, + RegoErrorLimit: opts.RegoOptions.ErrorLimit, + TfExcludeDownloaded: opts.TfExcludeDownloaded, + RawConfigScanners: opts.RawConfigScanners, + FilePatterns: opts.FilePatterns, + ConfigFileSchemas: configSchemas, + SkipFiles: opts.SkipFiles, + SkipDirs: opts.SkipDirs, + AnsiblePlaybooks: opts.AnsiblePlaybooks, + AnsibleInventories: opts.AnsibleInventories, + AnsibleExtraVars: ansibleExtraVars, + } + + regoScanner, err := misconf.InitRegoScanner(misconfOpts) + if err != nil { + return misconf.ScannerOption{}, xerrors.Errorf("init Rego scanner: %w", err) + } + + misconfOpts.RegoScanner = regoScanner + return misconfOpts, nil +} + +func resolveAnsibleExtraVars(inputs []string) (map[string]any, error) { + result := make(map[string]any) + + for _, input := range inputs { + var vars map[string]any + + switch { + case strings.HasPrefix(input, "@"): + data, err := os.ReadFile(input[1:]) + if err != nil { + return nil, fmt.Errorf("read extra-vars file %s: %w", input[1:], err) + } + trimmed := bytes.TrimSpace(data) + if len(trimmed) > 0 && trimmed[0] == '{' { + // parse as JSON object + if err := json.Unmarshal(trimmed, &vars); err != nil { + return nil, fmt.Errorf("parse extra-vars JSON file %s: %w", input[1:], err) + } + } else { + // parse as YAML + if err := yaml.Unmarshal(trimmed, &vars); err != nil { + return nil, fmt.Errorf("parse extra-vars YAML file %s: %w", input[1:], err) + } + } + case strings.Contains(input, "="): + kv := strings.SplitN(input, "=", 2) + var val string + if len(kv) == 2 { + val = kv[1] + } + vars = map[string]any{kv[0]: val} + default: + return nil, fmt.Errorf("invalid extra-vars input: %s", input) + } + + maps.Copy(result, vars) + } + + return result, nil +} diff --git a/pkg/commands/artifact/scanner.go b/pkg/commands/artifact/scanner.go new file mode 100644 index 000000000000..20e8d23891c6 --- /dev/null +++ b/pkg/commands/artifact/scanner.go @@ -0,0 +1,298 @@ +package artifact + +import ( + "context" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + artimage "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" + artlocal "github.com/aquasecurity/trivy/pkg/fanal/artifact/local" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/repo" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/sbom" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/vm" + "github.com/aquasecurity/trivy/pkg/fanal/image" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/rpc/client" + "github.com/aquasecurity/trivy/pkg/scan" + "github.com/aquasecurity/trivy/pkg/scan/langpkg" + "github.com/aquasecurity/trivy/pkg/scan/local" + "github.com/aquasecurity/trivy/pkg/scan/ospkg" + "github.com/aquasecurity/trivy/pkg/vulnerability" +) + +// newArtifactFunc is a function that creates an artifact with its cleanup function. +// It takes an artifact cache (either local or remote) and returns the artifact, cleanup function, and any error. +type newArtifactFunc func(cache.ArtifactCache) (artifact.Artifact, func(), error) + +//////////////////////////////////////// +// Standalone Mode Scanner Functions // +//////////////////////////////////////// + +// imageStandaloneScanService scans container images from registries +// Target: Container image (e.g., alpine:3.15, gcr.io/project/image:tag) +// Mode: Standalone (local scanning without server) +func imageStandaloneScanService(ctx context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createLocalService(conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + img, cleanupImage, err := image.NewContainerImage(ctx, conf.Target, conf.ArtifactOption.ImageOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize container image: %w", err) + } + + art, err := artimage.NewArtifact(img, c, conf.ArtifactOption) + if err != nil { + cleanupImage() + return nil, nil, xerrors.Errorf("unable to initialize artifact: %w", err) + } + return art, cleanupImage, nil + }) +} + +// archiveStandaloneScanService scans container image archives +// Target: Image archive file (e.g., alpine.tar created by 'docker save') +// Mode: Standalone (local scanning without server) +func archiveStandaloneScanService(_ context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createLocalService(conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + img, err := image.NewArchiveImage(conf.Target) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize archive image: %w", err) + } + + art, err := artimage.NewArtifact(img, c, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize artifact: %w", err) + } + return art, func() {}, nil + }) +} + +// filesystemStandaloneScanService scans local filesystems and directories +// Target: Local directory or file path (e.g., /path/to/project, .) +// Mode: Standalone (local scanning without server) +func filesystemStandaloneScanService(_ context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createLocalService(conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + fs := walker.NewFS() + art, err := artlocal.NewArtifact(conf.Target, c, fs, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize filesystem artifact: %w", err) + } + return art, func() {}, nil + }) +} + +// repositoryStandaloneScanService scans git repositories +// Target: Git repository URL or local path (e.g., https://github.com/example/repo, .) +// Mode: Standalone (local scanning without server) +func repositoryStandaloneScanService(_ context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createLocalService(conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + fs := walker.NewFS() + art, cleanupArtifact, err := repo.NewArtifact(conf.Target, c, fs, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize repository artifact: %w", err) + } + return art, cleanupArtifact, nil + }) +} + +// sbomStandaloneScanService scans SBOM files +// Target: SBOM file path (e.g., sbom.json, sbom.spdx, bom.xml) +// Mode: Standalone (local scanning without server) +func sbomStandaloneScanService(_ context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createLocalService(conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + art, err := sbom.NewArtifact(conf.Target, c, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize SBOM artifact: %w", err) + } + return art, func() {}, nil + }) +} + +// vmStandaloneScanService scans virtual machine images +// Target: VM image file or AMI ID (e.g., disk.vmdk, ami-1234567890abcdef0) +// Mode: Standalone (local scanning without server) +func vmStandaloneScanService(_ context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createLocalService(conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + walkerVM := walker.NewVM() + art, err := vm.NewArtifact(conf.Target, c, walkerVM, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize VM artifact: %w", err) + } + return art, func() {}, nil + }) +} + +////////////////////////////////////////// +// Client/Server Mode Scanner Functions // +////////////////////////////////////////// + +// imageRemoteScanService scans container images via Trivy server +// Target: Container image (e.g., alpine:3.15, gcr.io/project/image:tag) +// Mode: Client/Server (sends image data to Trivy server for scanning) +func imageRemoteScanService(ctx context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createRemoteService(ctx, conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + img, cleanupImage, err := image.NewContainerImage(ctx, conf.Target, conf.ArtifactOption.ImageOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize container image: %w", err) + } + + art, err := artimage.NewArtifact(img, c, conf.ArtifactOption) + if err != nil { + cleanupImage() + return nil, nil, xerrors.Errorf("unable to initialize artifact: %w", err) + } + return art, cleanupImage, nil + }) +} + +// archiveRemoteScanService scans container image archives via Trivy server +// Target: Image archive file (e.g., alpine.tar created by 'docker save') +// Mode: Client/Server (sends archive to Trivy server for scanning) +func archiveRemoteScanService(ctx context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createRemoteService(ctx, conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + img, err := image.NewArchiveImage(conf.Target) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize archive image: %w", err) + } + + art, err := artimage.NewArtifact(img, c, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize artifact: %w", err) + } + return art, func() {}, nil + }) +} + +// filesystemRemoteScanService scans filesystems via Trivy server +// Target: Local directory or file path (e.g., /path/to/project, .) +// Mode: Client/Server (sends filesystem data to Trivy server for scanning) +func filesystemRemoteScanService(ctx context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createRemoteService(ctx, conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + fs := walker.NewFS() + art, err := artlocal.NewArtifact(conf.Target, c, fs, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize filesystem artifact: %w", err) + } + return art, func() {}, nil + }) +} + +// repositoryRemoteScanService scans git repositories via Trivy server +// Target: Git repository URL or local path (e.g., https://github.com/example/repo, .) +// Mode: Client/Server (sends repository data to Trivy server for scanning) +func repositoryRemoteScanService(ctx context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createRemoteService(ctx, conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + fs := walker.NewFS() + art, cleanupArtifact, err := repo.NewArtifact(conf.Target, c, fs, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize repository artifact: %w", err) + } + return art, cleanupArtifact, nil + }) +} + +// sbomRemoteScanService scans SBOM files via Trivy server +// Target: SBOM file path (e.g., sbom.json, sbom.spdx, bom.xml) +// Mode: Client/Server (sends SBOM to Trivy server for scanning) +func sbomRemoteScanService(ctx context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createRemoteService(ctx, conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + art, err := sbom.NewArtifact(conf.Target, c, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize SBOM artifact: %w", err) + } + return art, func() {}, nil + }) +} + +// vmRemoteScanService scans virtual machine images via Trivy server +// Target: VM image file or AMI ID (e.g., disk.vmdk, ami-1234567890abcdef0) +// Mode: Client/Server (sends VM image data to Trivy server for scanning) +func vmRemoteScanService(ctx context.Context, conf ScannerConfig) (scan.Service, func(), error) { + return createRemoteService(ctx, conf, func(c cache.ArtifactCache) (artifact.Artifact, func(), error) { + walkerVM := walker.NewVM() + art, err := vm.NewArtifact(conf.Target, c, walkerVM, conf.ArtifactOption) + if err != nil { + return nil, nil, xerrors.Errorf("unable to initialize VM artifact: %w", err) + } + return art, func() {}, nil + }) +} + +// createLocalService creates a local scanning service using manual dependency injection. +// This replaces the previous Wire-based code generation with explicit dependency management. +// +// Dependency injection process: +// 1. Initialize cache (for storing analysis results) +// 2. Create artifact using the provided factory function +// 3. Manually inject dependencies into the local service: +// - Applier: Merges layers and applies analysis results +// - OS Scanner: Detects OS packages and vulnerabilities +// - Language Scanner: Detects language-specific packages and vulnerabilities +// - Vulnerability Client: Fetches vulnerability information from the database +// +// The function returns: +// - scan.Service: The configured scanning service with all dependencies injected +// - cleanup function: Must be called to properly release resources +// - error: If any initialization fails +// +// Dependency injection graph (all manually wired): +// +// Cache → Applier → LocalService → Artifact → ScanService +// ↘ OS Scanner ↗ +// ↘ Lang Scanner ↗ +// ↘ Vuln Client ↗ +func createLocalService(conf ScannerConfig, newArtifact newArtifactFunc) (scan.Service, func(), error) { + c, cleanupCache, err := cache.New(conf.CacheOptions) + if err != nil { + return scan.Service{}, nil, xerrors.Errorf("unable to initialize cache: %w", err) + } + + app := applier.NewApplier(c) + osScanner := ospkg.NewScanner() + langScanner := langpkg.NewScanner() + vulnClient := vulnerability.NewClient(db.Config{}) + service := local.NewService(app, osScanner, langScanner, vulnClient) + + art, cleanupArtifact, err := newArtifact(c) + if err != nil { + cleanupCache() + return scan.Service{}, nil, xerrors.Errorf("unable to initialize artifact: %w", err) + } + + return scan.NewService(service, art), func() { + cleanupArtifact() + cleanupCache() + }, nil +} + +// createRemoteService creates a remote scanning service using manual dependency injection. +// This replaces the previous Wire-based code generation with explicit dependency management. +// +// Dependency injection process: +// 1. Initialize remote cache (for caching results on server side) +// 2. Create artifact using the provided factory function +// 3. Manually inject the RPC client service to communicate with the server +// +// The function returns: +// - scan.Service: The configured scanning service with all dependencies injected +// - cleanup function: Must be called to properly release resources +// - error: If any initialization fails +// +// Dependency injection graph (all manually wired): +// +// RemoteCache → Artifact → ClientService → ScanService +// ↗ +// Trivy Server (handles scanning logic) +func createRemoteService(ctx context.Context, conf ScannerConfig, newArtifact newArtifactFunc) (scan.Service, func(), error) { + remoteCache := cache.NewRemoteCache(ctx, conf.RemoteCacheOptions) + + art, cleanupArtifact, err := newArtifact(remoteCache) + if err != nil { + return scan.Service{}, nil, err + } + + service := client.NewService(conf.ServerOption) + return scan.NewService(service, art), cleanupArtifact, nil +} diff --git a/pkg/commands/auth/run.go b/pkg/commands/auth/run.go new file mode 100644 index 000000000000..798d66484bbf --- /dev/null +++ b/pkg/commands/auth/run.go @@ -0,0 +1,100 @@ +package auth + +import ( + "context" + "os" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/config/types" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" +) + +func Login(ctx context.Context, registry string, opts flag.Options) error { + if len(opts.Credentials) == 0 { + return xerrors.New("username and password required") + } else if len(opts.Credentials) > 1 { + return xerrors.New("multiple credentials are not allowed") + } + + reg, err := parseRegistry(registry, opts) + if err != nil { + return xerrors.Errorf("failed to parse registry: %w", err) + } + serverAddress := reg.Name() + + // Validate the credential + _, err = transport.NewWithContext(ctx, reg, &authn.Basic{ + Username: opts.Credentials[0].Username, + Password: opts.Credentials[0].Password, + }, xhttp.RoundTripper(ctx), nil) + if err != nil { + return xerrors.Errorf("failed to authenticate: %w", err) + } + + cf, err := config.Load(os.Getenv("DOCKER_CONFIG")) + if err != nil { + return xerrors.Errorf("failed to load docker config: %w", err) + } + creds := cf.GetCredentialsStore(serverAddress) + if serverAddress == name.DefaultRegistry { + serverAddress = authn.DefaultAuthKey + } + if err := creds.Store(types.AuthConfig{ + ServerAddress: serverAddress, + Username: opts.Credentials[0].Username, + Password: opts.Credentials[0].Password, + }); err != nil { + return xerrors.Errorf("failed to store credentials: %w", err) + } + + if err := cf.Save(); err != nil { + return xerrors.Errorf("failed to save docker config: %w", err) + } + log.Info("Login succeeded", log.FilePath(cf.Filename), log.String("username", opts.Credentials[0].Username)) + return nil +} + +func Logout(_ context.Context, registry string) error { + reg, err := parseRegistry(registry, flag.Options{}) + if err != nil { + return xerrors.Errorf("failed to parse registry: %w", err) + } + serverAddress := reg.Name() + + cf, err := config.Load(os.Getenv("DOCKER_CONFIG")) + if err != nil { + return xerrors.Errorf("failed to load docker config: %w", err) + } + creds := cf.GetCredentialsStore(serverAddress) + if serverAddress == name.DefaultRegistry { + serverAddress = authn.DefaultAuthKey + } + if err := creds.Erase(serverAddress); err != nil { + return xerrors.Errorf("failed to delete credentials: %w", err) + } + + if err := cf.Save(); err != nil { + return xerrors.Errorf("failed to save docker config: %w", err) + } + log.Info("Logged out", log.FilePath(cf.Filename)) + return nil +} + +func parseRegistry(registry string, opts flag.Options) (name.Registry, error) { + var nameOpts []name.Option + if opts.Insecure { + nameOpts = append(nameOpts, name.Insecure) + } + reg, err := name.NewRegistry(registry, nameOpts...) + if err != nil { + return name.Registry{}, xerrors.Errorf("failed to parse registry: %w", err) + } + return reg, nil +} diff --git a/pkg/commands/auth/run_test.go b/pkg/commands/auth/run_test.go new file mode 100644 index 000000000000..5b9ea3a0b051 --- /dev/null +++ b/pkg/commands/auth/run_test.go @@ -0,0 +1,141 @@ +package auth_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/require" + + testauth "github.com/aquasecurity/testdocker/auth" + "github.com/aquasecurity/testdocker/registry" + "github.com/aquasecurity/trivy/pkg/commands/auth" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/flag" +) + +func TestLogin(t *testing.T) { + type args struct { + registry string + opts flag.Options + } + tests := []struct { + name string + args args + wantErr string + }{ + { + name: "single credential", + args: args{ + opts: flag.Options{ + RegistryOptions: flag.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "user", + Password: "pass", + }, + }, + }, + }, + }, + }, + { + name: "multiple credentials", + args: args{ + opts: flag.Options{ + RegistryOptions: flag.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "user1", + Password: "pass1", + }, + { + Username: "user2", + Password: "pass2", + }, + }, + }, + }, + }, + wantErr: "multiple credentials are not allowed", + }, + { + name: "no credentials", + args: args{ + registry: "auth.test", + opts: flag.Options{}, + }, + wantErr: "username and password required", + }, + { + name: "invalid registry", + args: args{ + registry: "aaa://invalid.test", + opts: flag.Options{ + RegistryOptions: flag.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "user", + Password: "pass", + }, + }, + }, + }, + }, + wantErr: "registries must be valid RFC 3986 URI authorities", + }, + } + + tr := registry.NewDockerRegistry(registry.Option{ + Auth: testauth.Auth{ + User: "user", + Password: "pass", + }, + }) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set the DOCKER_CONFIG environment variable to a temporary directory + // so that the test does not interfere with the user's configuration. + t.Setenv("DOCKER_CONFIG", filepath.Join(t.TempDir(), "config.json")) + + reg := lo.Ternary(tt.args.registry == "", strings.TrimPrefix(tr.URL, "http://"), tt.args.registry) + err := auth.Login(t.Context(), reg, tt.args.opts) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestLogout(t *testing.T) { + // Set the DOCKER_CONFIG environment variable to a temporary directory + // so that the test does not interfere with the user's configuration. + tmpDir := t.TempDir() + t.Setenv("DOCKER_CONFIG", tmpDir) + + t.Run("success", func(t *testing.T) { + configFile := filepath.Join(tmpDir, "config.json") + err := os.WriteFile(configFile, []byte(`{"auths": {"auth.test": {"auth": "dXNlcjpwYXNz"}}}`), 0o600) + require.NoError(t, err) + + err = auth.Logout(t.Context(), "auth.test") + require.NoError(t, err) + b, err := os.ReadFile(configFile) + require.NoError(t, err) + require.JSONEq(t, `{"auths": {}}`, string(b)) + }) + t.Run("not found", func(t *testing.T) { + err := auth.Logout(t.Context(), "notfound.test") + require.NoError(t, err) // Return an error if "credsStore" is "osxkeychain". + }) + + t.Run("invalid registry", func(t *testing.T) { + err := auth.Logout(t.Context(), "aaa://invalid.test") + require.ErrorContains(t, err, "registries must be valid RFC 3986 URI authorities") + }) +} diff --git a/pkg/commands/clean/run.go b/pkg/commands/clean/run.go new file mode 100644 index 000000000000..5fc5b8ecd98c --- /dev/null +++ b/pkg/commands/clean/run.go @@ -0,0 +1,115 @@ +package clean + +import ( + "context" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/javadb" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/policy" + "github.com/aquasecurity/trivy/pkg/vex/repo" +) + +func Run(ctx context.Context, opts flag.Options) error { + ctx, cancel := context.WithTimeout(ctx, opts.Timeout) + defer cancel() + + if !opts.CleanAll && !opts.CleanScanCache && !opts.CleanVulnerabilityDB && !opts.CleanJavaDB && + !opts.CleanChecksBundle && !opts.CleanVEXRepositories { + return xerrors.New("no clean option is specified") + } + + if opts.CleanAll { + opts.CleanScanCache = true + opts.CleanVulnerabilityDB = true + opts.CleanJavaDB = true + opts.CleanChecksBundle = true + opts.CleanVEXRepositories = true + } + + if opts.CleanScanCache { + if err := cleanScanCache(ctx, opts); err != nil { + return xerrors.Errorf("failed to remove scan cache : %w", err) + } + } + + if opts.CleanVulnerabilityDB { + if err := cleanVulnerabilityDB(ctx, opts); err != nil { + return xerrors.Errorf("vuln db clean error: %w", err) + } + } + + if opts.CleanJavaDB { + if err := cleanJavaDB(ctx, opts); err != nil { + return xerrors.Errorf("java db clean error: %w", err) + } + } + + if opts.CleanChecksBundle { + if err := cleanCheckBundle(opts); err != nil { + return xerrors.Errorf("check bundle clean error: %w", err) + } + } + + if opts.CleanVEXRepositories { + if err := cleanVEXRepositories(opts); err != nil { + return xerrors.Errorf("VEX repositories clean error: %w", err) + } + } + return nil +} + +func cleanScanCache(ctx context.Context, opts flag.Options) error { + log.InfoContext(ctx, "Removing scan cache...") + c, cleanup, err := cache.New(opts.CacheOpts()) + if err != nil { + return xerrors.Errorf("failed to instantiate cache client: %w", err) + } + defer cleanup() + + if err = c.Clear(ctx); err != nil { + return xerrors.Errorf("clear scan cache: %w", err) + } + return nil +} + +func cleanVulnerabilityDB(ctx context.Context, opts flag.Options) error { + log.InfoContext(ctx, "Removing vulnerability database...") + if err := db.NewClient(db.Dir(opts.CacheDir), true).Clear(ctx); err != nil { + return xerrors.Errorf("clear vulnerability database: %w", err) + + } + return nil +} + +func cleanJavaDB(ctx context.Context, opts flag.Options) error { + log.InfoContext(ctx, "Removing Java database...") + if err := javadb.Clear(ctx, opts.CacheDir); err != nil { + return xerrors.Errorf("clear Java database: %w", err) + } + return nil +} + +func cleanCheckBundle(opts flag.Options) error { + log.Info("Removing check bundle...") + c, err := policy.NewClient(opts.CacheDir, true, opts.MisconfOptions.ChecksBundleRepository) + if err != nil { + return xerrors.Errorf("failed to instantiate check client: %w", err) + } + if err := c.Clear(); err != nil { + return xerrors.Errorf("clear check bundle: %w", err) + } + return nil +} + +func cleanVEXRepositories(opts flag.Options) error { + log.Info("Removing VEX repositories...") + if err := repo.NewManager(opts.CacheDir).Clear(); err != nil { + return xerrors.Errorf("clear VEX repositories: %w", err) + } + return nil +} diff --git a/pkg/commands/clean/run_test.go b/pkg/commands/clean/run_test.go new file mode 100644 index 000000000000..84080a88d9b6 --- /dev/null +++ b/pkg/commands/clean/run_test.go @@ -0,0 +1,162 @@ +package clean_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/commands/clean" + "github.com/aquasecurity/trivy/pkg/flag" +) + +func TestRun(t *testing.T) { + tests := []struct { + name string + cleanOpts flag.CleanOptions + wantErr bool + checkFunc func(*testing.T, string) + }{ + { + name: "clean all", + cleanOpts: flag.CleanOptions{ + CleanAll: true, + }, + wantErr: false, + checkFunc: func(t *testing.T, dir string) { + assert.NoDirExists(t, filepath.Join(dir, "fanal")) + assert.NoDirExists(t, filepath.Join(dir, "db")) + assert.NoDirExists(t, filepath.Join(dir, "java-db")) + assert.NoDirExists(t, filepath.Join(dir, "policy")) + assert.NoDirExists(t, filepath.Join(dir, "vex")) + assert.DirExists(t, dir) + }, + }, + { + name: "clean scan cache", + cleanOpts: flag.CleanOptions{ + CleanScanCache: true, + }, + wantErr: false, + checkFunc: func(t *testing.T, dir string) { + assert.NoDirExists(t, filepath.Join(dir, "fanal")) + assert.DirExists(t, filepath.Join(dir, "db")) + assert.DirExists(t, filepath.Join(dir, "java-db")) + assert.DirExists(t, filepath.Join(dir, "policy")) + assert.DirExists(t, filepath.Join(dir, "vex")) + }, + }, + { + name: "clean vulnerability DB", + cleanOpts: flag.CleanOptions{ + CleanVulnerabilityDB: true, + }, + wantErr: false, + checkFunc: func(t *testing.T, dir string) { + assert.NoDirExists(t, filepath.Join(dir, "db")) + assert.DirExists(t, filepath.Join(dir, "fanal")) + assert.DirExists(t, filepath.Join(dir, "java-db")) + assert.DirExists(t, filepath.Join(dir, "policy")) + assert.DirExists(t, filepath.Join(dir, "vex")) + }, + }, + { + name: "clean Java DB", + cleanOpts: flag.CleanOptions{ + CleanJavaDB: true, + }, + wantErr: false, + checkFunc: func(t *testing.T, dir string) { + assert.NoDirExists(t, filepath.Join(dir, "java-db")) + assert.DirExists(t, filepath.Join(dir, "fanal")) + assert.DirExists(t, filepath.Join(dir, "db")) + assert.DirExists(t, filepath.Join(dir, "policy")) + assert.DirExists(t, filepath.Join(dir, "vex")) + }, + }, + { + name: "clean check bundle", + cleanOpts: flag.CleanOptions{ + CleanChecksBundle: true, + }, + wantErr: false, + checkFunc: func(t *testing.T, dir string) { + assert.NoDirExists(t, filepath.Join(dir, "policy")) + assert.DirExists(t, filepath.Join(dir, "fanal")) + assert.DirExists(t, filepath.Join(dir, "db")) + assert.DirExists(t, filepath.Join(dir, "java-db")) + assert.DirExists(t, filepath.Join(dir, "vex")) + }, + }, + { + name: "clean vex repositories", + cleanOpts: flag.CleanOptions{ + CleanVEXRepositories: true, + }, + wantErr: false, + checkFunc: func(t *testing.T, dir string) { + assert.DirExists(t, filepath.Join(dir, "policy")) + assert.DirExists(t, filepath.Join(dir, "fanal")) + assert.DirExists(t, filepath.Join(dir, "db")) + assert.DirExists(t, filepath.Join(dir, "java-db")) + assert.NoDirExists(t, filepath.Join(dir, "vex")) + }, + }, + { + name: "no clean option specified", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary directory for testing + tempDir := t.TempDir() + + // Create test directories and files + createTestFiles(t, tempDir) + + opts := flag.Options{ + GlobalOptions: flag.GlobalOptions{ + CacheDir: tempDir, + }, + CacheOptions: flag.CacheOptions{ + CacheBackend: string(cache.TypeFS), + }, + CleanOptions: tt.cleanOpts, + } + + err := clean.Run(t.Context(), opts) + + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + if tt.checkFunc != nil { + tt.checkFunc(t, tempDir) + } + }) + } +} + +func createTestFiles(t *testing.T, dir string) { + subdirs := []string{ + "fanal", + "db", + "java-db", + "policy", + "vex", + } + for _, subdir := range subdirs { + err := os.MkdirAll(filepath.Join(dir, subdir), 0o755) + require.NoError(t, err) + + testFile := filepath.Join(dir, subdir, "testfile.txt") + err = os.WriteFile(testFile, []byte("test content"), 0o644) + require.NoError(t, err) + } +} diff --git a/pkg/commands/convert/run.go b/pkg/commands/convert/run.go new file mode 100644 index 000000000000..0b9bb2c1d2fc --- /dev/null +++ b/pkg/commands/convert/run.go @@ -0,0 +1,88 @@ +package convert + +import ( + "context" + "encoding/json" + "os" + "slices" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/commands/operation" + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/result" + "github.com/aquasecurity/trivy/pkg/types" +) + +func Run(ctx context.Context, opts flag.Options) (err error) { + logger := log.WithPrefix("convert") + ctx, cancel := context.WithTimeout(ctx, opts.Timeout) + defer cancel() + + f, err := os.Open(opts.Target) + if err != nil { + return xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + var r types.Report + if err = json.NewDecoder(f).Decode(&r); err != nil { + return xerrors.Errorf("json decode error: %w", err) + } + + // "convert" supports JSON results produced by Trivy scanning other than AWS and Kubernetes + if r.ArtifactName == "" && r.ArtifactType == "" { + return xerrors.New("AWS and Kubernetes scanning reports are not yet supported") + } + + compat(&r) + if err = result.Filter(ctx, r, opts.FilterOpts()); err != nil { + return xerrors.Errorf("unable to filter results: %w", err) + } + + if len(opts.Scanners) == 0 && opts.Format == types.FormatTable && slices.Contains(opts.TableModes, types.Summary) { + logger.Info("To display the summary table, enable the scanners used during JSON report generation.") + opts.TableModes = lo.Filter(opts.TableModes, func(mode types.TableMode, _ int) bool { + return mode != types.Summary + }) + } + + logger.Debug("Writing report to output...") + if err = report.Write(ctx, r, opts); err != nil { + return xerrors.Errorf("unable to write results: %w", err) + } + + return operation.Exit(opts, r.Results.Failed(), r.Metadata) +} + +// compat converts the JSON report to the latest format +func compat(r *types.Report) { + for i, res := range r.Results { + pkgs := make(map[string]ftypes.Package, len(res.Packages)) + for j, pkg := range res.Packages { + if pkg.Identifier.UID != "" { + continue + } + // Fill in the UID field since older JSON reports don't have it + pkg.Identifier.UID = dependency.UID(res.Target, pkg) + pkgs[pkg.ID+pkg.FilePath] = pkg + r.Results[i].Packages[j] = pkg + } + + for j, vuln := range res.Vulnerabilities { + if vuln.PkgIdentifier.UID != "" { + continue + } + pkg, ok := pkgs[vuln.PkgID+vuln.PkgPath] + if !ok { + continue + } + r.Results[i].Vulnerabilities[j].PkgIdentifier = pkg.Identifier + } + } +} diff --git a/pkg/commands/operation/operation.go b/pkg/commands/operation/operation.go new file mode 100644 index 000000000000..850368df428a --- /dev/null +++ b/pkg/commands/operation/operation.go @@ -0,0 +1,133 @@ +package operation + +import ( + "context" + "sync" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/db" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/policy" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vex" + "github.com/aquasecurity/trivy/pkg/vex/repo" +) + +var mu sync.Mutex + +// DownloadDB downloads the DB +func DownloadDB(ctx context.Context, appVersion, cacheDir string, dbRepositories []name.Reference, quiet, skipUpdate bool, + opt ftypes.RegistryOptions) error { + mu.Lock() + defer mu.Unlock() + + ctx = log.WithContextPrefix(ctx, log.PrefixVulnerabilityDB) + dbDir := db.Dir(cacheDir) + client := db.NewClient(dbDir, quiet, db.WithDBRepository(dbRepositories)) + needsUpdate, err := client.NeedsUpdate(ctx, appVersion, skipUpdate) + if err != nil { + return xerrors.Errorf("database error: %w", err) + } + + if needsUpdate { + log.InfoContext(ctx, "Need to update DB") + if err = client.Download(ctx, dbDir, opt); err != nil { + return xerrors.Errorf("failed to download vulnerability DB: %w", err) + } + } + + // for debug + if err = client.ShowInfo(); err != nil { + return xerrors.Errorf("failed to show database info: %w", err) + } + return nil +} + +func DownloadVEXRepositories(ctx context.Context, opts flag.Options) error { + ctx = log.WithContextPrefix(ctx, "vex") + if opts.SkipVEXRepoUpdate { + log.InfoContext(ctx, "Skipping VEX repository update") + return nil + } + + mu.Lock() + defer mu.Unlock() + + // Download VEX repositories only if `--vex repo` is passed. + _, enabled := lo.Find(opts.VEXSources, func(src vex.Source) bool { + return src.Type == vex.TypeRepository + }) + if !enabled { + return nil + } + + err := repo.NewManager(opts.CacheDir).DownloadRepositories(ctx, nil, repo.Options{ + Insecure: opts.Insecure, + }) + if err != nil { + return xerrors.Errorf("failed to download vex repositories: %w", err) + } + + return nil + +} + +// InitBuiltinChecks downloads the built-in policies and loads them +func InitBuiltinChecks(ctx context.Context, client *policy.Client, skipUpdate bool, registryOpts ftypes.RegistryOptions) (string, error) { + mu.Lock() + defer mu.Unlock() + + ctx = log.WithContextPrefix(ctx, "checks-client") + + var err error + + if skipUpdate { + log.InfoContext(ctx, "No downloadable checks were loaded as --skip-check-update is enabled, loading from existing cache...") + + path := client.BuiltinChecksPath() + _, _, err := misconf.CheckPathExists(path) + if err != nil { + return "", xerrors.Errorf("failed to check cache: %w", err) + } + return path, nil + } + + needsUpdate, err := client.NeedsUpdate(ctx, registryOpts) + if err != nil { + return "", xerrors.Errorf("unable to check if built-in policies need to be updated: %w", err) + } + + if needsUpdate { + log.InfoContext(ctx, "Need to update the checks bundle") + log.InfoContext(ctx, "Downloading the checks bundle...") + if err = client.DownloadBuiltinChecks(ctx, registryOpts); err != nil { + return "", xerrors.Errorf("failed to download checks bundle: %w", err) + } + } else { + log.InfoContext(ctx, + "Using existing checks from cache", + log.String("path", client.BuiltinChecksPath()), + ) + } + + return client.BuiltinChecksPath(), nil +} + +func Exit(opts flag.Options, failedResults bool, m types.Metadata) error { + if opts.ExitOnEOL != 0 && m.OS != nil && m.OS.Eosl { + log.Error("Detected EOL OS", log.String("family", string(m.OS.Family)), + log.String("version", m.OS.Name)) + return &types.ExitError{Code: opts.ExitOnEOL} + } + + if opts.ExitCode != 0 && failedResults { + return &types.ExitError{Code: opts.ExitCode} + } + return nil +} diff --git a/pkg/commands/operation/operation_test.go b/pkg/commands/operation/operation_test.go new file mode 100644 index 000000000000..79609226c1cc --- /dev/null +++ b/pkg/commands/operation/operation_test.go @@ -0,0 +1,190 @@ +package operation + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "testing" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + fakei "github.com/google/go-containerregistry/pkg/v1/fake" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/utils/clock" + fake "k8s.io/utils/clock/testing" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/oci" + "github.com/aquasecurity/trivy/pkg/policy" +) + +type stubLayer struct { + v1.Layer +} + +func (f stubLayer) MediaType() (types.MediaType, error) { + return "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", nil +} + +func newStubLayer(t *testing.T) v1.Layer { + layer, err := tarball.LayerFromOpener(func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewBufferString("foo bar baz")), nil + }) + require.NoError(t, err) + require.NotNil(t, layer) + + return stubLayer{layer} +} + +func TestInitBuiltinChecks(t *testing.T) { + type digestReturns struct { + h v1.Hash + err error + } + type layersReturns struct { + layers []v1.Layer + } + tests := []struct { + name string + metadata any + checkDir string + skipUpdate bool + wantErr string + layersReturns layersReturns + digestReturns digestReturns + want *policy.Metadata + clock clock.Clock + }{ + { + name: "happy path - no need to update", + digestReturns: digestReturns{ + h: v1.Hash{ + Algorithm: "sha256", + Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + }, + }, + checkDir: "policy", + clock: fake.NewFakeClock(time.Date(1992, 1, 1, 1, 0, 0, 0, time.UTC)), + metadata: policy.Metadata{ + Digest: `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(policy.BundleVersion), + }, + skipUpdate: false, + }, + { + name: "skip update flag set with no existing cache to fallback to", + skipUpdate: true, + checkDir: "policy", + wantErr: "cache does not exist at", + }, + { + name: "skip update flag set with existing cache to fallback to", + skipUpdate: true, + checkDir: filepath.Join("policy", "content"), + clock: fake.NewFakeClock(time.Date(1992, 1, 1, 1, 0, 0, 0, time.UTC)), + metadata: policy.Metadata{ + Digest: `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + name: "needs update returns an error", + digestReturns: digestReturns{ + err: errors.New("digest error"), + }, + metadata: policy.Metadata{ + Digest: `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(policy.BundleVersion), + }, + checkDir: "policy", + clock: fake.NewFakeClock(time.Date(3000, 1, 1, 1, 0, 0, 0, time.UTC)), + wantErr: "unable to check if built-in policies need to be updated", + }, + { + name: "sad: download builtin checks returns an error", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + layersReturns: layersReturns{ + layers: []v1.Layer{newStubLayer(t)}, + }, + digestReturns: digestReturns{ + err: errors.New("error"), + }, + want: &policy.Metadata{ + Digest: "sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + DownloadedAt: time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC), + }, + wantErr: "failed to download checks bundle", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := t.Context() + + // Set up a temporary directory + tmpDir := t.TempDir() + + // Create a check directory + err := os.MkdirAll(filepath.Join(tmpDir, tt.checkDir), os.ModePerm) + require.NoError(t, err) + + if tt.metadata != nil { + b, err := json.Marshal(tt.metadata) + require.NoError(t, err) + + // Write a metadata file + metadataPath := filepath.Join(tmpDir, "policy", "metadata.json") + err = os.WriteFile(metadataPath, b, os.ModePerm) + require.NoError(t, err) + } + + // Mock image + img := new(fakei.FakeImage) + img.LayersReturns([]v1.Layer{newStubLayer(t)}, nil) + img.DigestReturns(tt.digestReturns.h, tt.digestReturns.err) + img.ManifestReturns(&v1.Manifest{ + Layers: []v1.Descriptor{ + { + MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", + Size: 100, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8", + }, + Annotations: map[string]string{ + "org.opencontainers.image.title": "bundle.tar.gz", + }, + }, + }, + Annotations: map[string]string{ + policy.VersionAnnotationKey: fmt.Sprintf("%d.0.0", policy.BundleVersion), + }, + }, nil) + + // Mock OCI artifact + art := oci.NewArtifact("repo", ftypes.RegistryOptions{}, oci.WithImage(img)) + c, err := policy.NewClient(tmpDir, true, "", policy.WithOCIArtifact(art), policy.WithClock(tt.clock)) + require.NoError(t, err) + + got, err := InitBuiltinChecks(ctx, c, tt.skipUpdate, ftypes.RegistryOptions{}) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + assert.Empty(t, got) + return + } + require.NoError(t, err) + assert.NotEmpty(t, got) + }) + } +} diff --git a/pkg/commands/run.go b/pkg/commands/run.go new file mode 100644 index 000000000000..32e6e07e31ad --- /dev/null +++ b/pkg/commands/run.go @@ -0,0 +1,33 @@ +package commands + +import ( + "context" + "errors" + "fmt" + + bberrors "go.etcd.io/bbolt/errors" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/version/doc" +) + +const ( + troubleshootingDocPath = "guide/references/troubleshooting/" + lockDocFragment = "database-and-cache-lock-errors" + timeoutDocFragment = "timeout" +) + +// Run builds the CLI application and executes it with centralized error handling. +func Run(ctx context.Context) error { + app := NewApp() + if err := app.ExecuteContext(ctx); err != nil { + if errors.Is(err, context.DeadlineExceeded) { + log.WarnContext(ctx, fmt.Sprintf("Provide a higher timeout value, see %s", doc.URL(troubleshootingDocPath, timeoutDocFragment))) + } + if errors.Is(err, bberrors.ErrTimeout) { + log.ErrorContext(ctx, fmt.Sprintf("Failed to acquire cache or database lock, see %s for troubleshooting", doc.URL(troubleshootingDocPath, lockDocFragment))) + } + return err + } + return nil +} diff --git a/pkg/commands/server/run.go b/pkg/commands/server/run.go new file mode 100644 index 000000000000..0f29c5f35176 --- /dev/null +++ b/pkg/commands/server/run.go @@ -0,0 +1,63 @@ +package server + +import ( + "context" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/commands/operation" + "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/module" + rpcServer "github.com/aquasecurity/trivy/pkg/rpc/server" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" +) + +// Run runs the scan +func Run(ctx context.Context, opts flag.Options) (err error) { + log.InitLogger(opts.Debug, opts.Quiet) + + // Set the default HTTP transport + xhttp.SetDefaultTransport(xhttp.NewTransport(xhttp.Options{ + Insecure: opts.Insecure, + CACerts: opts.CACerts, + Timeout: opts.Timeout, + })) + + // configure cache dir + cacheClient, cleanup, err := cache.New(opts.CacheOpts()) + if err != nil { + return xerrors.Errorf("server cache error: %w", err) + } + defer cleanup() + + // download the database file + if err = operation.DownloadDB(ctx, opts.AppVersion, opts.CacheDir, opts.DBRepositories, + true, opts.SkipDBUpdate, opts.RegistryOpts()); err != nil { + return err + } + + if opts.DownloadDBOnly { + return nil + } + + if err = db.Init(db.Dir(opts.CacheDir)); err != nil { + return xerrors.Errorf("error in vulnerability DB initialize: %w", err) + } + + // Initialize WASM modules + m, err := module.NewManager(ctx, module.Options{ + Dir: opts.ModuleDir, + EnabledModules: opts.EnabledModules, + }) + if err != nil { + return xerrors.Errorf("WASM module error: %w", err) + } + m.Register() + + server := rpcServer.NewServer(opts.AppVersion, opts.Listen, opts.CacheDir, opts.Token, opts.TokenHeader, + opts.PathPrefix, opts.DBRepositories, opts.RegistryOpts()) + return server.ListenAndServe(ctx, cacheClient, opts.SkipDBUpdate) +} diff --git a/pkg/commands/signal.go b/pkg/commands/signal.go new file mode 100644 index 000000000000..7dce942e8e87 --- /dev/null +++ b/pkg/commands/signal.go @@ -0,0 +1,46 @@ +package commands + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/aquasecurity/trivy/pkg/log" + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +// NotifyContext returns a context that is canceled when SIGINT or SIGTERM is received. +// It also ensures cleanup of temporary files when the signal is received. +// +// When a signal is received, Trivy will attempt to gracefully shut down by canceling +// the context and waiting for all operations to complete. If users want to force an +// immediate exit, they can send a second SIGINT or SIGTERM signal. +func NotifyContext(parent context.Context) context.Context { + ctx, stop := signal.NotifyContext(parent, os.Interrupt, syscall.SIGTERM) + + // Start a goroutine to handle cleanup when context is done + go func() { + <-ctx.Done() + + // Log that we're shutting down gracefully + log.Info("Received signal, attempting graceful shutdown...") + log.Info("Press Ctrl+C again to force exit") + + // Perform cleanup + if err := Cleanup(); err != nil { + log.Debug("Failed to clean up temporary files", log.Err(err)) + } + + // Clean up signal handling + // After calling stop(), a second signal will cause immediate termination + stop() + }() + + return ctx +} + +// Cleanup performs cleanup tasks before Trivy exits +func Cleanup() error { + return xos.Cleanup() +} diff --git a/pkg/commands/testdata/db/metadata.json b/pkg/commands/testdata/db/metadata.json new file mode 100644 index 000000000000..4710ea229b3d --- /dev/null +++ b/pkg/commands/testdata/db/metadata.json @@ -0,0 +1,7 @@ +{ + "Version": 2, + "NextUpdate": "2022-03-02T12:07:07.99504023Z", + "UpdatedAt": "2022-03-02T06:07:07.99504083Z", + "DownloadedAt": "2022-03-02T10:03:38.383312Z" +} + diff --git a/pkg/commands/testdata/java-db/metadata.json b/pkg/commands/testdata/java-db/metadata.json new file mode 100644 index 000000000000..67ccfdf8b3be --- /dev/null +++ b/pkg/commands/testdata/java-db/metadata.json @@ -0,0 +1 @@ +{"Version":1,"NextUpdate":"2023-03-17T00:47:02.774253254Z","UpdatedAt":"2023-03-14T00:47:02.774253754Z","DownloadedAt":"2023-03-14T03:04:55.058541039Z"} diff --git a/pkg/commands/testdata/policy/metadata.json b/pkg/commands/testdata/policy/metadata.json new file mode 100644 index 000000000000..7dfb4dd56692 --- /dev/null +++ b/pkg/commands/testdata/policy/metadata.json @@ -0,0 +1 @@ +{"Digest":"sha256:19a017cdc798631ad42f6f4dce823d77b2989128f0e1a7f9bc83ae3c59024edd","DownloadedAt":"2023-03-01T17:06:08.191725-08:00"} \ No newline at end of file diff --git a/pkg/compliance/report/json.go b/pkg/compliance/report/json.go new file mode 100644 index 000000000000..6dd33fc4a8a3 --- /dev/null +++ b/pkg/compliance/report/json.go @@ -0,0 +1,41 @@ +package report + +import ( + "encoding/json" + "fmt" + "io" + + "golang.org/x/xerrors" +) + +type JSONWriter struct { + Output io.Writer + Report string +} + +// Write writes the results in JSON format +func (jw JSONWriter) Write(report *ComplianceReport) error { + var output []byte + var err error + + var v any + switch jw.Report { + case allReport: + v = report + case summaryReport: + v = BuildSummary(report) + default: + return xerrors.Errorf(`report %q not supported. Use "summary" or "all"`, jw.Report) + } + + output, err = json.MarshalIndent(v, "", " ") + if err != nil { + return xerrors.Errorf("failed to marshal json: %w", err) + } + + if _, err = fmt.Fprintln(jw.Output, string(output)); err != nil { + return xerrors.Errorf("failed to write json: %w", err) + } + + return nil +} diff --git a/pkg/compliance/report/json_test.go b/pkg/compliance/report/json_test.go new file mode 100644 index 000000000000..31680b4ff513 --- /dev/null +++ b/pkg/compliance/report/json_test.go @@ -0,0 +1,91 @@ +package report_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/compliance/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestJSONWriter_Write(t *testing.T) { + input := &report.ComplianceReport{ + ID: "1234", + Title: "NSA", + RelatedResources: []string{"https://example.com"}, + Results: []*report.ControlCheckResult{ + { + ID: "1.0", + Name: "Non-root containers", + Severity: "MEDIUM", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + AVDID: "AVD-KSV012", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + }, + { + ID: "1.1", + Name: "Immutable container file systems", + Severity: "LOW", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + AVDID: "AVD-KSV013", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + reportType string + input *report.ComplianceReport + want string + }{ + { + name: "build summary json output report", + reportType: "summary", + input: input, + want: filepath.Join("testdata", "summary.json"), + }, + { + name: "build full json output report", + reportType: "all", + input: input, + want: filepath.Join("testdata", "all.json"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := new(bytes.Buffer) + tr := report.JSONWriter{ + Report: tt.reportType, + Output: buf, + } + err := tr.Write(tt.input) + require.NoError(t, err) + + want, err := os.ReadFile(tt.want) + require.NoError(t, err) + + assert.JSONEq(t, string(want), buf.String()) + }) + } +} diff --git a/pkg/compliance/report/report.go b/pkg/compliance/report/report.go new file mode 100644 index 000000000000..d4b31668ed6d --- /dev/null +++ b/pkg/compliance/report/report.go @@ -0,0 +1,137 @@ +package report + +import ( + "context" + "io" + + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/compliance/spec" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + allReport = "all" + summaryReport = "summary" +) + +type Option struct { + Format types.Format + Report string + Output io.Writer + Severities []dbTypes.Severity + ColumnHeading []string +} + +// ComplianceReport represents a kubernetes scan report +type ComplianceReport struct { + ID string + Title string + Description string + Version string + RelatedResources []string + Results []*ControlCheckResult +} + +type ControlCheckResult struct { + ID string + Name string + Description string + DefaultStatus iacTypes.ControlStatus `json:",omitempty"` + Severity string + Results types.Results +} + +// SummaryReport represents a kubernetes scan report with consolidated findings +type SummaryReport struct { + SchemaVersion int `json:",omitempty"` + ID string + Title string + SummaryControls []ControlCheckSummary `json:",omitempty"` +} + +type ControlCheckSummary struct { + ID string + Name string + Severity string + TotalFail *int `json:",omitempty"` +} + +// Writer defines the result write operation +type Writer interface { + Write(ComplianceReport) error +} + +// Write writes the results in the given format +func Write(ctx context.Context, report *ComplianceReport, option Option) error { + switch option.Format { + case types.FormatJSON: + jwriter := JSONWriter{ + Output: option.Output, + Report: option.Report, + } + return jwriter.Write(report) + case types.FormatTable: + if !report.empty() { + complianceWriter := &TableWriter{ + Output: option.Output, + Report: option.Report, + Severities: option.Severities, + } + err := complianceWriter.Write(ctx, report) + if err != nil { + return err + } + } + return nil + default: + return xerrors.Errorf(`unknown format %q. Use "json" or "table"`, option.Format) + } +} + +func (r ComplianceReport) empty() bool { + return len(r.Results) == 0 +} + +// buildControlCheckResults create compliance results data +func buildControlCheckResults(checksMap map[string]types.Results, controls []iacTypes.Control) []*ControlCheckResult { + var complianceResults []*ControlCheckResult + for _, control := range controls { + var results types.Results + for _, c := range control.Checks { + results = append(results, checksMap[c.ID]...) + } + complianceResults = append(complianceResults, &ControlCheckResult{ + Name: control.Name, + ID: control.ID, + Description: control.Description, + Severity: string(control.Severity), + DefaultStatus: control.DefaultStatus, + Results: results, + }) + } + return complianceResults +} + +// buildComplianceReportResults create compliance results data +func buildComplianceReportResults(checksMap map[string]types.Results, s iacTypes.Spec) *ComplianceReport { + controlCheckResult := buildControlCheckResults(checksMap, s.Controls) + return &ComplianceReport{ + ID: s.ID, + Title: s.Title, + Description: s.Description, + Version: s.Version, + RelatedResources: s.RelatedResources, + Results: controlCheckResult, + } +} + +func BuildComplianceReport(scanResults []types.Results, cs spec.ComplianceSpec) (*ComplianceReport, error) { + // aggregate checks by ID + aggregateChecksByID := spec.AggregateAllChecksBySpecID(scanResults, cs) + + // build compliance report results + return buildComplianceReportResults(aggregateChecksByID, cs.Spec), nil +} diff --git a/pkg/compliance/report/report_test.go b/pkg/compliance/report/report_test.go new file mode 100644 index 000000000000..91e9015f426c --- /dev/null +++ b/pkg/compliance/report/report_test.go @@ -0,0 +1,238 @@ +package report_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/compliance/report" + "github.com/aquasecurity/trivy/pkg/compliance/spec" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestBuildComplianceReport(t *testing.T) { + type args struct { + scanResults []types.Results + cs spec.ComplianceSpec + } + tests := []struct { + name string + args args + want *report.ComplianceReport + wantErr assert.ErrorAssertionFunc + }{ + { + name: "happy", + args: args{ + scanResults: []types.Results{ + { + { + Target: "Deployment/metrics-server", + Class: types.ClassConfig, + Type: ftypes.Kubernetes, + MisconfSummary: &types.MisconfSummary{ + Successes: 1, + Failures: 0, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Kubernetes Security Check", + ID: "KSV-0001", + Title: "Process can elevate its own privileges", + Description: "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.", + Message: "Container 'metrics-server' of Deployment 'metrics-server' should set 'securityContext.allowPrivilegeEscalation' to false", + Namespace: "builtin.kubernetes.KSV001", + Query: "data.builtin.kubernetes.KSV001.deny", + Resolution: "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.", + Severity: dbTypes.SeverityMedium.String(), + PrimaryURL: "https://avd.aquasec.com/misconfig/ksv001", + References: []string{ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv001", + }, + Status: types.MisconfStatusPassed, + }, + { + Type: "Kubernetes Security Check", + ID: "KSV-9999", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + { + { + Target: "rancher/metrics-server:v0.3.6 (debian 9.9)", + Class: types.ClassOSPkg, + Type: "debian", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "DLA-2424-1", + VendorIDs: []string{"DLA-2424-1"}, + PkgName: "tzdata", + InstalledVersion: "2019a-0+deb9u1", + FixedVersion: "2020d-0+deb9u1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Debian, + Name: "Debian Security Tracker", + URL: "https://salsa.debian.org/security-tracker-team/security-tracker", + }, + Vulnerability: dbTypes.Vulnerability{ + Title: "tzdata - new upstream version", + Severity: dbTypes.SeverityUnknown.String(), + }, + }, + }, + }, + }, + }, + cs: spec.ComplianceSpec{ + Spec: iacTypes.Spec{ + ID: "1234", + Title: "NSA", + Description: "National Security Agency - Kubernetes Hardening Guidance", + Version: "1.0", + RelatedResources: []string{ + "https://example.com", + }, + Controls: []iacTypes.Control{ + { + ID: "1.0", + Name: "Non-root containers", + Description: "Check that container is not running as root", + Severity: "MEDIUM", + Checks: []iacTypes.SpecCheck{ + {ID: "KSV-0001"}, + }, + }, + { + ID: "1.1", + Name: "Immutable container file systems", + Description: "Check that container root file system is immutable", + Severity: "LOW", + Checks: []iacTypes.SpecCheck{ + {ID: "KSV-0002"}, + }, + }, + { + ID: "1.2", + Name: "tzdata - new upstream version", + Description: "Bad tzdata package", + Severity: "CRITICAL", + Checks: []iacTypes.SpecCheck{ + {ID: "DLA-2424-1"}, + }, + }, + }, + }, + }, + }, + want: &report.ComplianceReport{ + ID: "1234", + Title: "NSA", + Description: "National Security Agency - Kubernetes Hardening Guidance", + Version: "1.0", + RelatedResources: []string{ + "https://example.com", + }, + Results: []*report.ControlCheckResult{ + { + ID: "1.0", + Name: "Non-root containers", + Description: "Check that container is not running as root", + Severity: "MEDIUM", + Results: types.Results{ + { + Target: "Deployment/metrics-server", + Class: types.ClassConfig, + Type: ftypes.Kubernetes, + MisconfSummary: &types.MisconfSummary{ + Successes: 1, + Failures: 0, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Kubernetes Security Check", + ID: "KSV-0001", + Title: "Process can elevate its own privileges", + Description: "A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node.", + Message: "Container 'metrics-server' of Deployment 'metrics-server' should set 'securityContext.allowPrivilegeEscalation' to false", + Namespace: "builtin.kubernetes.KSV001", + Query: "data.builtin.kubernetes.KSV001.deny", + Resolution: "Set 'set containers[].securityContext.allowPrivilegeEscalation' to 'false'.", + Severity: dbTypes.SeverityMedium.String(), + PrimaryURL: "https://avd.aquasec.com/misconfig/ksv001", + References: []string{ + "https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted", + "https://avd.aquasec.com/misconfig/ksv001", + }, + Status: types.MisconfStatusPassed, + }, + }, + }, + }, + }, + { + ID: "1.1", + Name: "Immutable container file systems", + Description: "Check that container root file system is immutable", + Severity: "LOW", + Results: nil, + }, + { + ID: "1.2", + Name: "tzdata - new upstream version", + Description: "Bad tzdata package", + Severity: "CRITICAL", + Results: types.Results{ + { + Target: "rancher/metrics-server:v0.3.6 (debian 9.9)", + Class: types.ClassOSPkg, + Type: "debian", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "DLA-2424-1", + VendorIDs: []string{"DLA-2424-1"}, + PkgName: "tzdata", + InstalledVersion: "2019a-0+deb9u1", + FixedVersion: "2020d-0+deb9u1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Debian, + Name: "Debian Security Tracker", + URL: "https://salsa.debian.org/security-tracker-team/security-tracker", + }, + Vulnerability: dbTypes.Vulnerability{ + Title: "tzdata - new upstream version", + Severity: dbTypes.SeverityUnknown.String(), + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := report.BuildComplianceReport(tt.args.scanResults, tt.args.cs) + if !tt.wantErr(t, err, fmt.Sprintf("BuildComplianceReport(%v, %v)", tt.args.scanResults, tt.args.cs)) { + return + } + assert.Equalf(t, tt.want, got, "BuildComplianceReport(%v, %v)", tt.args.scanResults, tt.args.cs) + }) + } +} diff --git a/pkg/compliance/report/summary.go b/pkg/compliance/report/summary.go new file mode 100644 index 000000000000..0262249e7214 --- /dev/null +++ b/pkg/compliance/report/summary.go @@ -0,0 +1,111 @@ +package report + +import ( + "fmt" + "io" + "strconv" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/table" +) + +func BuildSummary(cr *ComplianceReport) *SummaryReport { + var ccma []ControlCheckSummary + for _, control := range cr.Results { + ccm := ControlCheckSummary{ + ID: control.ID, + Name: control.Name, + Severity: control.Severity, + } + if !strings.Contains(control.Name, "Manual") { + ccm.TotalFail = lo.ToPtr(len(control.Results)) + } + ccma = append(ccma, ccm) + } + return &SummaryReport{ + ID: cr.ID, + Title: cr.Title, + SummaryControls: ccma, + } +} + +type SummaryWriter struct { + Output io.Writer +} + +func NewSummaryWriter(output io.Writer) SummaryWriter { + return SummaryWriter{ + Output: output, + } +} + +// Write writes the results in a summarized table format +func (s SummaryWriter) Write(report *ComplianceReport) error { + if _, err := fmt.Fprintln(s.Output); err != nil { + return xerrors.Errorf("failed to write summary report: %w", err) + } + + if _, err := fmt.Fprintf(s.Output, "Summary Report for compliance: %s\n", report.Title); err != nil { + return xerrors.Errorf("failed to write summary report: %w", err) + } + sr := BuildSummary(report) + t := table.New(s.Output) + t.SetRowLines(false) + configureHeader(t, s.columns()) + + for _, summaryControl := range sr.SummaryControls { + rowParts := s.generateSummary(summaryControl) + t.AddRow(rowParts...) + } + + t.Render() + + return nil +} + +func (s SummaryWriter) columns() []string { + return []string{ + ControlIDColumn, + SeverityColumn, + ControlNameColumn, + StatusColumn, + IssuesColumn, + } +} + +func (s SummaryWriter) generateSummary(summaryControls ControlCheckSummary) []string { + // "-" means manual checks + numOfIssues := "-" + status := "-" + if summaryControls.TotalFail != nil { + if *summaryControls.TotalFail == 0 { + status = "PASS" + } else { + status = "FAIL" + } + numOfIssues = strconv.Itoa(*summaryControls.TotalFail) + } + return []string{ + summaryControls.ID, + summaryControls.Severity, + summaryControls.Name, + status, + numOfIssues, + } +} + +func configureHeader(t *table.Table, columnHeading []string) { + headerAlignment := []table.Alignment{ + table.AlignLeft, + table.AlignCenter, + table.AlignLeft, + table.AlignCenter, + table.AlignCenter, + } + t.SetHeaders(columnHeading...) + t.SetAlignment(headerAlignment...) + t.SetAutoMergeHeaders(true) +} diff --git a/pkg/compliance/report/summary_test.go b/pkg/compliance/report/summary_test.go new file mode 100644 index 000000000000..1dbf862d59ba --- /dev/null +++ b/pkg/compliance/report/summary_test.go @@ -0,0 +1,167 @@ +package report_test + +import ( + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/compliance/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestBuildSummary(t *testing.T) { + tests := []struct { + name string + reportType string + input *report.ComplianceReport + want *report.SummaryReport + }{ + { + name: "build report summary config only", + reportType: "summary", + input: &report.ComplianceReport{ + ID: "1234", + Title: "NSA", + RelatedResources: []string{"https://example.com"}, + Results: []*report.ControlCheckResult{ + { + ID: "1.0", + Name: "Non-root containers", + Severity: "MEDIUM", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + AVDID: "AVD-KSV012", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + }, + { + ID: "1.1", + Name: "Immutable container file systems", + Severity: "LOW", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + AVDID: "AVD-KSV013", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + }, + }, + }, + want: &report.SummaryReport{ + SchemaVersion: 0, + ID: "1234", + Title: "NSA", + SummaryControls: []report.ControlCheckSummary{ + { + ID: "1.0", + Name: "Non-root containers", + Severity: "MEDIUM", + TotalFail: lo.ToPtr(1), + }, + { + ID: "1.1", + Name: "Immutable container file systems", + Severity: "LOW", + TotalFail: lo.ToPtr(1), + }, + }, + }, + }, + { + name: "build full json output report", + reportType: "all", + input: &report.ComplianceReport{ + ID: "1234", + Title: "NSA", + RelatedResources: []string{"https://example.com"}, + Results: []*report.ControlCheckResult{ + { + ID: "1.0", + Name: "Non-root containers", + Severity: "MEDIUM", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + AVDID: "AVD-KSV012", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + }, + { + ID: "1.1", + Name: "Immutable container file systems", + Severity: "LOW", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + AVDID: "AVD-KSV013", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + }, + { + ID: "1.2", + Name: "tzdata - new upstream version", + Severity: "LOW", + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-9999-0001"}, + {VulnerabilityID: "CVE-9999-0002"}, + }, + }, + }, + }, + }, + }, + want: &report.SummaryReport{ + SchemaVersion: 0, + ID: "1234", + Title: "NSA", + SummaryControls: []report.ControlCheckSummary{ + { + ID: "1.0", + Name: "Non-root containers", + Severity: "MEDIUM", + TotalFail: lo.ToPtr(1), + }, + { + ID: "1.1", + Name: "Immutable container file systems", + Severity: "LOW", + TotalFail: lo.ToPtr(1), + }, + { + ID: "1.2", + Name: "tzdata - new upstream version", + Severity: "LOW", + TotalFail: lo.ToPtr(1), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := report.BuildSummary(tt.input) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/compliance/report/table.go b/pkg/compliance/report/table.go new file mode 100644 index 000000000000..b3ceed569980 --- /dev/null +++ b/pkg/compliance/report/table.go @@ -0,0 +1,51 @@ +package report + +import ( + "context" + "io" + + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + pkgReport "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +type TableWriter struct { + Report string + Output io.Writer + Severities []dbTypes.Severity + ColumnHeading []string +} + +const ( + ControlIDColumn = "ID" + SeverityColumn = "Severity" + ControlNameColumn = "Control Name" + StatusColumn = "Status" + IssuesColumn = "Issues" +) + +func (tw TableWriter) Write(ctx context.Context, report *ComplianceReport) error { + switch tw.Report { + case allReport: + t := pkgReport.NewWriter(pkgReport.Options{ + Output: tw.Output, + Severities: tw.Severities, + }) + for _, cr := range report.Results { + r := types.Report{Results: cr.Results} + err := t.Write(ctx, r) + if err != nil { + return err + } + } + case summaryReport: + writer := NewSummaryWriter(tw.Output) + return writer.Write(report) + default: + return xerrors.Errorf(`report %q not supported. Use "summary" or "all"`, tw.Report) + } + + return nil +} diff --git a/pkg/compliance/report/table_test.go b/pkg/compliance/report/table_test.go new file mode 100644 index 000000000000..15f130883647 --- /dev/null +++ b/pkg/compliance/report/table_test.go @@ -0,0 +1,86 @@ +package report_test + +import ( + "bytes" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/compliance/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestTableWriter_Write(t *testing.T) { + + tests := []struct { + name string + reportType string + input *report.ComplianceReport + want string + }{ + { + name: "build summary table", + reportType: "summary", + input: &report.ComplianceReport{ + ID: "1234", + Title: "NSA", + RelatedResources: []string{"https://example.com"}, + Results: []*report.ControlCheckResult{ + { + ID: "1.0", + Name: "Non-root containers", + Severity: "MEDIUM", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + AVDID: "AVD-KSV012", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + }, + { + ID: "1.1", + Name: "Immutable container file systems", + Severity: "LOW", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + AVDID: "AVD-KSV013", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + }, + }, + }, + want: filepath.Join("testdata", "table_summary.txt"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := new(bytes.Buffer) + tr := report.TableWriter{ + Report: tt.reportType, + Output: buf, + } + err := tr.Write(t.Context(), tt.input) + require.NoError(t, err) + + want, err := os.ReadFile(tt.want) + want = bytes.ReplaceAll(want, []byte("\r"), []byte("")) + + require.NoError(t, err) + + assert.Equal(t, string(want), buf.String()) + }) + } +} diff --git a/pkg/compliance/report/testdata/all.json b/pkg/compliance/report/testdata/all.json new file mode 100644 index 000000000000..79e099ef19c6 --- /dev/null +++ b/pkg/compliance/report/testdata/all.json @@ -0,0 +1,45 @@ +{ + "ID": "1234", + "Title": "NSA", + "Description": "", + "Version": "", + "RelatedResources": [ + "https://example.com" + ], + "Results": [ + { + "ID": "1.0", + "Name": "Non-root containers", + "Description": "", + "Severity": "MEDIUM", + "Results": [ + { + "Target": "", + "Misconfigurations": [ + { + "AVDID": "AVD-KSV012", + "Status": "FAIL" + } + ] + } + ] + }, + { + "ID": "1.1", + "Name": "Immutable container file systems", + "Description": "", + "Severity": "LOW", + "Results": [ + { + "Target": "", + "Misconfigurations": [ + { + "AVDID": "AVD-KSV013", + "Status": "FAIL" + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/pkg/compliance/report/testdata/summary.json b/pkg/compliance/report/testdata/summary.json new file mode 100644 index 000000000000..2ee48756272b --- /dev/null +++ b/pkg/compliance/report/testdata/summary.json @@ -0,0 +1,18 @@ +{ + "ID": "1234", + "Title": "NSA", + "SummaryControls": [ + { + "ID": "1.0", + "Name": "Non-root containers", + "Severity": "MEDIUM", + "TotalFail": 1 + }, + { + "ID": "1.1", + "Name": "Immutable container file systems", + "Severity": "LOW", + "TotalFail": 1 + } + ] +} \ No newline at end of file diff --git a/pkg/compliance/report/testdata/table_all.txt b/pkg/compliance/report/testdata/table_all.txt new file mode 100644 index 000000000000..159d5f8590ad --- /dev/null +++ b/pkg/compliance/report/testdata/table_all.txt @@ -0,0 +1,52 @@ + +Deployment/metrics-server (kubernetes) +====================================== +Tests: 1 (SUCCESSES: 1, FAILURES: 0) +Failures: 0 () + +MEDIUM: Container 'metrics-server' of Deployment 'metrics-server' should set 'securityContext.allowPrivilegeEscalation' to false +════════════════════════════════════════ +A program inside the container can elevate its own privileges and run as root, which might give the program control over the container and node. + +See https://avd.aquasec.com/misconfig/ksv001 +──────────────────────────────────────── + Deployment/metrics-server:132-140 +──────────────────────────────────────── + 132 ┌ - image: rancher/metrics-server:v0.3.6 + 133 │ imagePullPolicy: IfNotPresent + 134 │ name: metrics-server + 135 │ resources: {} + 136 │ terminationMessagePath: /dev/termination-log + 137 │ terminationMessagePolicy: File + 138 │ volumeMounts: + 139 │ - mountPath: /tmp + 140 └ name: tmp-dir +──────────────────────────────────────── + + + +Deployment/metrics-server (kubernetes) +====================================== +Tests: 1 (SUCCESSES: 1, FAILURES: 0) +Failures: 0 () + +LOW: Container 'metrics-server' of Deployment 'metrics-server' should add 'ALL' to 'securityContext.capabilities.drop' +════════════════════════════════════════ +The container should drop all default capabilities and add only those that are needed for its execution. + +See https://avd.aquasec.com/misconfig/ksv003 +──────────────────────────────────────── + Deployment/metrics-server:132-140 +──────────────────────────────────────── + 132 ┌ - image: rancher/metrics-server:v0.3.6 + 133 │ imagePullPolicy: IfNotPresent + 134 │ name: metrics-server + 135 │ resources: {} + 136 │ terminationMessagePath: /dev/termination-log + 137 │ terminationMessagePolicy: File + 138 │ volumeMounts: + 139 │ - mountPath: /tmp + 140 └ name: tmp-dir +──────────────────────────────────────── + + diff --git a/pkg/compliance/report/testdata/table_summary.txt b/pkg/compliance/report/testdata/table_summary.txt new file mode 100644 index 000000000000..5600ae6a8cc2 --- /dev/null +++ b/pkg/compliance/report/testdata/table_summary.txt @@ -0,0 +1,8 @@ + +Summary Report for compliance: NSA +┌─────┬──────────┬──────────────────────────────────┬────────┬────────┐ +│ ID │ Severity │ Control Name │ Status │ Issues │ +├─────┼──────────┼──────────────────────────────────┼────────┼────────┤ +│ 1.0 │ MEDIUM │ Non-root containers │ FAIL │ 1 │ +│ 1.1 │ LOW │ Immutable container file systems │ FAIL │ 1 │ +└─────┴──────────┴──────────────────────────────────┴────────┴────────┘ diff --git a/pkg/compliance/spec/compliance.go b/pkg/compliance/spec/compliance.go new file mode 100644 index 000000000000..aa67fda0dc0e --- /dev/null +++ b/pkg/compliance/spec/compliance.go @@ -0,0 +1,140 @@ +package spec + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy-checks/pkg/compliance" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/types" +) + +type Severity string + +// ComplianceSpec represent the compliance specification +type ComplianceSpec struct { + Spec iacTypes.Spec `yaml:"spec"` +} + +const ( + FailStatus iacTypes.ControlStatus = "FAIL" + PassStatus iacTypes.ControlStatus = "PASS" + WarnStatus iacTypes.ControlStatus = "WARN" +) + +// Scanners reads spec control and determines the scanners by check ID prefix +func (cs *ComplianceSpec) Scanners() (types.Scanners, error) { + scannerTypes := set.New[types.Scanner]() + for _, control := range cs.Spec.Controls { + for _, check := range control.Checks { + scannerType := scannerByCheckID(check.ID) + if scannerType == types.UnknownScanner { + return nil, xerrors.Errorf("unsupported check ID: %s", check.ID) + } + scannerTypes.Append(scannerType) + } + } + return scannerTypes.Items(), nil +} + +// CheckIDs return list of compliance check IDs +func (cs *ComplianceSpec) CheckIDs() map[types.Scanner][]string { + checkIDsMap := make(map[types.Scanner][]string) + for _, control := range cs.Spec.Controls { + for _, check := range control.Checks { + scannerType := scannerByCheckID(check.ID) + checkIDsMap[scannerType] = append(checkIDsMap[scannerType], check.ID) + } + } + return checkIDsMap +} + +func scannerByCheckID(checkID string) types.Scanner { + checkID = strings.ToLower(checkID) + switch { + case strings.HasPrefix(checkID, "cve-") || strings.HasPrefix(checkID, "dla-"): + return types.VulnerabilityScanner + case strings.HasPrefix(checkID, "vuln-"): // custom id for filtering vulnerabilities by severity + return types.VulnerabilityScanner + case strings.HasPrefix(checkID, "secret-"): // custom id for filtering secrets by severity + return types.SecretScanner + // check the "avd-" prefix for backward compatibility + case strings.HasPrefix(checkID, "avd-") || isMisconfCheck(checkID): + return types.MisconfigScanner + default: + return types.UnknownScanner + } +} + +var misconfPrefixes = set.New( + "aws", "azu", "nif", "dig", "oci", "cldstk", "git", + "gcp", "ksv", "kcv", "ds", "kube", "opnstk", +) + +func isMisconfCheck(checkID string) bool { + prefix, _, ok := strings.Cut(checkID, "-") + if !ok { + return false + } + return misconfPrefixes.Contains(prefix) +} + +func checksDir(cacheDir string) string { + return filepath.Join(cacheDir, "policy") +} + +func complianceSpecDir(cacheDir string) string { + return filepath.Join(checksDir(cacheDir), "content", "specs", "compliance") +} + +// GetComplianceSpec accepct compliance flag name/path and return builtin or file system loaded spec +func GetComplianceSpec(specNameOrPath, cacheDir string) (ComplianceSpec, error) { + if specNameOrPath == "" { + return ComplianceSpec{}, nil + } + + var b []byte + var err error + if after, ok := strings.CutPrefix(specNameOrPath, "@"); ok { // load user specified spec from disk + b, err = os.ReadFile(after) + if err != nil { + return ComplianceSpec{}, fmt.Errorf("error retrieving compliance spec from path: %w", err) + } + log.Debug("Compliance spec loaded from specified path", log.String("path", specNameOrPath)) + } else { + _, err := os.Stat(filepath.Join(checksDir(cacheDir), "metadata.json")) + if err != nil { // cache corrupt or bundle does not exist, load embedded version + b = []byte(compliance.GetSpec(specNameOrPath)) + log.Debug("Compliance spec loaded from embedded library", log.String("spec", specNameOrPath)) + } else { + // load from bundle on disk + b, err = LoadFromBundle(cacheDir, specNameOrPath) + if err != nil { + return ComplianceSpec{}, err + } + log.Debug("Compliance spec loaded from disk bundle", log.String("spec", specNameOrPath)) + } + } + + var complianceSpec ComplianceSpec + if err = yaml.Unmarshal(b, &complianceSpec); err != nil { + return ComplianceSpec{}, xerrors.Errorf("spec yaml decode error: %w", err) + } + return complianceSpec, nil + +} + +func LoadFromBundle(cacheDir, specNameOrPath string) ([]byte, error) { + b, err := os.ReadFile(filepath.Join(complianceSpecDir(cacheDir), specNameOrPath+".yaml")) + if err != nil { + return nil, fmt.Errorf("error retrieving compliance spec from bundle %s: %w", specNameOrPath, err) + } + return b, nil +} diff --git a/pkg/compliance/spec/compliance_test.go b/pkg/compliance/spec/compliance_test.go new file mode 100644 index 000000000000..4a80eba73989 --- /dev/null +++ b/pkg/compliance/spec/compliance_test.go @@ -0,0 +1,291 @@ +package spec_test + +import ( + "path/filepath" + "slices" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/compliance/spec" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestComplianceSpec_Scanners(t *testing.T) { + tests := []struct { + name string + spec iacTypes.Spec + want types.Scanners + wantErr assert.ErrorAssertionFunc + }{ + { + name: "get config scanner type by check id prefix", + spec: iacTypes.Spec{ + ID: "1234", + Title: "NSA", + Description: "National Security Agency - Kubernetes Hardening Guidance", + RelatedResources: []string{ + "https://example.com", + }, + Version: "1.0", + Controls: []iacTypes.Control{ + { + Name: "Non-root containers", + Description: "Check that container is not running as root", + ID: "1.0", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-KSV012"}, + }, + }, + { + Name: "Check that encryption resource has been set", + Description: "Control checks whether encryption resource has been set", + ID: "1.1", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-1.2.31"}, + {ID: "AVD-1.2.32"}, + }, + }, + }, + }, + want: []types.Scanner{types.MisconfigScanner}, + wantErr: assert.NoError, + }, + { + name: "get config and vuln scanners types by check id prefix", + spec: iacTypes.Spec{ + ID: "1234", + Title: "NSA", + Description: "National Security Agency - Kubernetes Hardening Guidance", + RelatedResources: []string{ + "https://example.com", + }, + Version: "1.0", + Controls: []iacTypes.Control{ + { + Name: "Non-root containers", + Description: "Check that container is not running as root", + ID: "1.0", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-KSV012"}, + }, + }, + { + Name: "Check that encryption resource has been set", + Description: "Control checks whether encryption resource has been set", + ID: "1.1", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-1.2.31"}, + {ID: "AVD-1.2.32"}, + }, + }, + { + Name: "Ensure no critical vulnerabilities", + Description: "Control checks whether critical vulnerabilities are not found", + ID: "7.0", + Checks: []iacTypes.SpecCheck{ + {ID: "CVE-9999-9999"}, + }, + }, + }, + }, + want: []types.Scanner{ + types.MisconfigScanner, + types.VulnerabilityScanner, + }, + wantErr: assert.NoError, + }, + { + name: "unknown prefix", + spec: iacTypes.Spec{ + ID: "1234", + Title: "NSA", + Description: "National Security Agency - Kubernetes Hardening Guidance", + RelatedResources: []string{ + "https://example.com", + }, + Version: "1.0", + Controls: []iacTypes.Control{ + { + Name: "Unknown", + ID: "1.0", + Checks: []iacTypes.SpecCheck{ + {ID: "UNKNOWN-001"}, + }, + }, + }, + }, + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cs := &spec.ComplianceSpec{ + Spec: tt.spec, + } + got, err := cs.Scanners() + if !tt.wantErr(t, err, "Scanners()") { + return + } + slices.Sort(got) // for consistency + assert.Equalf(t, tt.want, got, "Scanners()") + }) + } +} + +func TestComplianceSpec_CheckIDs(t *testing.T) { + tests := []struct { + name string + spec iacTypes.Spec + want map[types.Scanner][]string + }{ + { + name: "get config scanner type by check id prefix", + spec: iacTypes.Spec{ + ID: "1234", + Title: "NSA", + Description: "National Security Agency - Kubernetes Hardening Guidance", + RelatedResources: []string{ + "https://example.com", + }, + Version: "1.0", + Controls: []iacTypes.Control{ + { + Name: "Non-root containers", + Description: "Check that container is not running as root", + ID: "1.0", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-KSV012"}, + }, + }, + { + Name: "Check that encryption resource has been set", + Description: "Control checks whether encryption resource has been set", + ID: "1.1", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-1.2.31"}, + {ID: "AVD-1.2.32"}, + }, + }, + }, + }, + want: map[types.Scanner][]string{ + types.MisconfigScanner: { + "AVD-KSV012", + "AVD-1.2.31", + "AVD-1.2.32", + }, + }, + }, + { + name: "get config and vuln scanners types by check id prefix", + spec: iacTypes.Spec{ + ID: "1234", + Title: "NSA", + Description: "National Security Agency - Kubernetes Hardening Guidance", + RelatedResources: []string{ + "https://example.com", + }, + Version: "1.0", + Controls: []iacTypes.Control{ + { + Name: "Non-root containers", + Description: "Check that container is not running as root", + ID: "1.0", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-KSV012"}, + }, + }, + { + Name: "Check that encryption resource has been set", + Description: "Control checks whether encryption resource has been set", + ID: "1.1", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-1.2.31"}, + {ID: "AVD-1.2.32"}, + }, + }, + { + Name: "Ensure no critical vulnerabilities", + Description: "Control checks whether critical vulnerabilities are not found", + ID: "7.0", + Checks: []iacTypes.SpecCheck{ + {ID: "CVE-9999-9999"}, + }, + }, + }, + }, + want: map[types.Scanner][]string{ + types.MisconfigScanner: { + "AVD-KSV012", + "AVD-1.2.31", + "AVD-1.2.32", + }, + types.VulnerabilityScanner: { + "CVE-9999-9999", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cs := &spec.ComplianceSpec{ + Spec: tt.spec, + } + got := cs.CheckIDs() + assert.Equalf(t, tt.want, got, "CheckIDs()") + }) + } +} + +func TestComplianceSpec_LoadFromDiskBundle(t *testing.T) { + + t.Run("load user specified spec from disk", func(t *testing.T) { + cs, err := spec.GetComplianceSpec(filepath.Join("@testdata", "testcache", "policy", "content", "specs", "compliance", "testspec.yaml"), filepath.Join("testdata", "testcache")) + require.NoError(t, err) + assert.Equal(t, spec.ComplianceSpec{Spec: iacTypes.Spec{ + ID: "test-spec-1.2", + Title: "Test Spec", + Description: "This is a test spec", + RelatedResources: []string{ + "https://www.google.ca", + }, + Version: "1.2", + Controls: []iacTypes.Control{ + { + Name: "moar-testing", + Description: "Test needs foo bar baz", + ID: "1.1", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-TEST-1234"}, + }, + Severity: "LOW", + }, + }, + }}, cs) + }) + + t.Run("load user specified spec from disk fails", func(t *testing.T) { + _, err := spec.GetComplianceSpec("@doesnotexist", "does-not-matter") + require.ErrorContains(t, err, "error retrieving compliance spec from path") + }) + + t.Run("bundle does not exist", func(t *testing.T) { + cs, err := spec.GetComplianceSpec("aws-cis-1.2", "does-not-matter") + require.NoError(t, err) + assert.Equal(t, "aws-cis-1.2", cs.Spec.ID) + }) + + t.Run("load spec from disk", func(t *testing.T) { + cs, err := spec.GetComplianceSpec("testspec", filepath.Join("testdata", "testcache")) + require.NoError(t, err) + assert.Equal(t, "test-spec-1.2", cs.Spec.ID) + }) + + t.Run("load spec yaml unmarshal failure", func(t *testing.T) { + _, err := spec.GetComplianceSpec("invalid", filepath.Join("testdata", "testcache")) + require.ErrorContains(t, err, "spec yaml decode error") + }) +} diff --git a/pkg/compliance/spec/custom.go b/pkg/compliance/spec/custom.go new file mode 100644 index 000000000000..8e7ed6679340 --- /dev/null +++ b/pkg/compliance/spec/custom.go @@ -0,0 +1,74 @@ +package spec + +import ( + "github.com/samber/lo" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +// We might be going to rewrite these functions in Rego, +// but we'll keep them for now until we need flexibility. +var customIDs = map[string]func(types.Result) types.Result{ + "VULN-CRITICAL": filterCriticalVulns, + "VULN-HIGH": filterHighVulns, + "SECRET-CRITICAL": filterCriticalSecrets, + "SECRET-HIGH": filterHighSecrets, +} + +func mapCustomIDsToFilteredResults(result types.Result, checkIDs map[types.Scanner][]string, + mapCheckByID map[string]types.Results) { + for _, ids := range checkIDs { + for _, id := range ids { + filterFunc, ok := customIDs[id] + if !ok { + continue + } + filtered := filterFunc(result) + if filtered.IsEmpty() { + continue + } + mapCheckByID[id] = types.Results{filtered} + } + } +} + +func filterCriticalVulns(result types.Result) types.Result { + return filterVulns(result, dbTypes.SeverityCritical) +} + +func filterHighVulns(result types.Result) types.Result { + return filterVulns(result, dbTypes.SeverityHigh) +} + +func filterVulns(result types.Result, severity dbTypes.Severity) types.Result { + filtered := lo.Filter(result.Vulnerabilities, func(vuln types.DetectedVulnerability, _ int) bool { + return vuln.Severity == severity.String() + }) + return types.Result{ + Target: result.Target, + Class: result.Class, + Type: result.Type, + Vulnerabilities: filtered, + } +} + +func filterCriticalSecrets(result types.Result) types.Result { + return filterSecrets(result, dbTypes.SeverityCritical) +} + +func filterHighSecrets(result types.Result) types.Result { + return filterSecrets(result, dbTypes.SeverityHigh) +} + +func filterSecrets(result types.Result, severity dbTypes.Severity) types.Result { + filtered := lo.Filter(result.Secrets, func(secret types.DetectedSecret, _ int) bool { + return secret.Severity == severity.String() + }) + return types.Result{ + Target: result.Target, + Class: result.Class, + Type: result.Type, + Secrets: filtered, + } +} diff --git a/pkg/compliance/spec/mapper.go b/pkg/compliance/spec/mapper.go new file mode 100644 index 000000000000..03634aaeb339 --- /dev/null +++ b/pkg/compliance/spec/mapper.go @@ -0,0 +1,69 @@ +package spec + +import ( + "slices" + + "github.com/aquasecurity/trivy/pkg/types" +) + +// MapSpecCheckIDToFilteredResults map spec check id to filtered scan results +func MapSpecCheckIDToFilteredResults(result types.Result, checkIDs map[types.Scanner][]string) map[string]types.Results { + mapCheckByID := make(map[string]types.Results) + for _, vuln := range result.Vulnerabilities { + // Skip irrelevant check IDs + if !slices.Contains(checkIDs[types.VulnerabilityScanner], vuln.VulnerabilityID) { + continue + } + mapCheckByID[vuln.VulnerabilityID] = append(mapCheckByID[vuln.VulnerabilityID], types.Result{ + Target: result.Target, + Class: result.Class, + Type: result.Type, + Vulnerabilities: []types.DetectedVulnerability{vuln}, + }) + } + for _, m := range result.Misconfigurations { + // Skip irrelevant check IDs + if !slices.Contains(checkIDs[types.MisconfigScanner], m.ID) { + continue + } + + mapCheckByID[m.ID] = append(mapCheckByID[m.ID], types.Result{ + Target: result.Target, + Class: result.Class, + Type: result.Type, + MisconfSummary: misconfigSummary(m), + Misconfigurations: []types.DetectedMisconfiguration{m}, + }) + } + + // Evaluate custom IDs + mapCustomIDsToFilteredResults(result, checkIDs, mapCheckByID) + + return mapCheckByID +} + +func misconfigSummary(misconfig types.DetectedMisconfiguration) *types.MisconfSummary { + rms := types.MisconfSummary{} + switch misconfig.Status { + case types.MisconfStatusPassed: + rms.Successes = 1 + case types.MisconfStatusFailure: + rms.Failures = 1 + } + return &rms +} + +// AggregateAllChecksBySpecID aggregates all scan results and map it to spec ids +func AggregateAllChecksBySpecID(multiResults []types.Results, cs ComplianceSpec) map[string]types.Results { + checkIDs := cs.CheckIDs() + complianceArr := make(map[string]types.Results, 0) + for _, resResult := range multiResults { + for _, result := range resResult { + m := MapSpecCheckIDToFilteredResults(result, checkIDs) + for id, checks := range m { + complianceArr[id] = append(complianceArr[id], checks...) + } + } + } + return complianceArr +} diff --git a/pkg/compliance/spec/mapper_test.go b/pkg/compliance/spec/mapper_test.go new file mode 100644 index 000000000000..681580375782 --- /dev/null +++ b/pkg/compliance/spec/mapper_test.go @@ -0,0 +1,163 @@ +package spec_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/compliance/spec" + "github.com/aquasecurity/trivy/pkg/fanal/secret" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestMapSpecCheckIDToFilteredResults(t *testing.T) { + checkIDs := map[types.Scanner][]string{ + types.MisconfigScanner: { + "KSV012", + "1.2.31", + "1.2.32", + }, + types.VulnerabilityScanner: { + "CVE-9999-9999", + "VULN-CRITICAL", + }, + types.SecretScanner: { + "SECRET-CRITICAL", + }, + } + tests := []struct { + name string + checkIDs map[types.Scanner][]string + result types.Result + want map[string]types.Results + }{ + { + name: "misconfiguration", + checkIDs: checkIDs, + result: types.Result{ + Target: "target", + Class: types.ClassConfig, + Type: ftypes.Kubernetes, + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "KSV012", + Status: types.MisconfStatusFailure, + }, + { + ID: "KSV013", + Status: types.MisconfStatusFailure, + }, + { + ID: "1.2.31", + Status: types.MisconfStatusFailure, + }, + }, + }, + want: map[string]types.Results{ + "KSV012": { + { + Target: "target", + Class: types.ClassConfig, + Type: ftypes.Kubernetes, + MisconfSummary: &types.MisconfSummary{ + Successes: 0, + Failures: 1, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "KSV012", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + "1.2.31": { + { + Target: "target", + Class: types.ClassConfig, + Type: ftypes.Kubernetes, + MisconfSummary: &types.MisconfSummary{ + Successes: 0, + Failures: 1, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "1.2.31", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + }, + }, + { + name: "secret", + checkIDs: checkIDs, + result: types.Result{ + Target: "target", + Class: types.ClassSecret, + Secrets: []types.DetectedSecret{ + { + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Severity: "CRITICAL", + Title: "AWS Access Key ID", + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 2, + Content: "AWS_ACCESS_KEY_ID=*****", + }, + }, + }, + }, + { + RuleID: "aws-account-id", + Category: secret.CategoryAWS, + Severity: "HIGH", + Title: "AWS Account ID", + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 1, + Content: "AWS_ACCOUNT_ID=*****", + }, + }, + }, + }, + }, + }, + want: map[string]types.Results{ + "SECRET-CRITICAL": { + { + Target: "target", + Class: types.ClassSecret, + Secrets: []types.DetectedSecret{ + { + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Severity: "CRITICAL", + Title: "AWS Access Key ID", + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 2, + Content: "AWS_ACCESS_KEY_ID=*****", + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := spec.MapSpecCheckIDToFilteredResults(tt.result, tt.checkIDs) + assert.Equalf(t, tt.want, got, "MapSpecCheckIDToFilteredResults()") + }) + } +} diff --git a/pkg/compliance/spec/testdata/testcache/policy/content/specs/compliance/invalid.yaml b/pkg/compliance/spec/testdata/testcache/policy/content/specs/compliance/invalid.yaml new file mode 100644 index 000000000000..29dcba2e15af --- /dev/null +++ b/pkg/compliance/spec/testdata/testcache/policy/content/specs/compliance/invalid.yaml @@ -0,0 +1 @@ +this is not yaml but easier to read \ No newline at end of file diff --git a/pkg/compliance/spec/testdata/testcache/policy/content/specs/compliance/testspec.yaml b/pkg/compliance/spec/testdata/testcache/policy/content/specs/compliance/testspec.yaml new file mode 100644 index 000000000000..ec1f75f52970 --- /dev/null +++ b/pkg/compliance/spec/testdata/testcache/policy/content/specs/compliance/testspec.yaml @@ -0,0 +1,15 @@ +spec: + id: test-spec-1.2 + title: Test Spec + description: This is a test spec + version: "1.2" + relatedResources: + - https://www.google.ca + controls: + - id: "1.1" + name: moar-testing + description: |- + Test needs foo bar baz + checks: + - id: AVD-TEST-1234 + severity: LOW \ No newline at end of file diff --git a/pkg/compliance/spec/testdata/testcache/policy/metadata.json b/pkg/compliance/spec/testdata/testcache/policy/metadata.json new file mode 100644 index 000000000000..ba37beda3850 --- /dev/null +++ b/pkg/compliance/spec/testdata/testcache/policy/metadata.json @@ -0,0 +1 @@ +{"Digest":"sha256:ef2d9ad4fce0f933b20a662004d7e55bf200987c180e7f2cd531af631f408bb3","DownloadedAt":"2024-08-07T20:07:48.917915-06:00"} \ No newline at end of file diff --git a/pkg/config/aws/config.go b/pkg/config/aws/config.go new file mode 100644 index 000000000000..d878783ce4a4 --- /dev/null +++ b/pkg/config/aws/config.go @@ -0,0 +1,47 @@ +package aws + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + awsconfig "github.com/aws/aws-sdk-go-v2/config" + "golang.org/x/xerrors" +) + +func EndpointResolver(endpoint string) aws.EndpointResolverWithOptionsFunc { + return func(_, reg string, _ ...any) (aws.Endpoint, error) { + return aws.Endpoint{ + PartitionID: "aws", + URL: endpoint, + SigningRegion: reg, + Source: aws.EndpointSourceCustom, + }, nil + } +} + +func MakeAWSOptions(region, endpoint string) []func(*awsconfig.LoadOptions) error { + var options []func(*awsconfig.LoadOptions) error + + if region != "" { + options = append(options, awsconfig.WithRegion(region)) + } + + if endpoint != "" { + options = append(options, awsconfig.WithEndpointResolverWithOptions(EndpointResolver(endpoint))) + } + + return options +} + +func LoadDefaultAWSConfig(ctx context.Context, region, endpoint string) (aws.Config, error) { + cfg, err := awsconfig.LoadDefaultConfig(ctx, MakeAWSOptions(region, endpoint)...) + if err != nil { + return aws.Config{}, xerrors.Errorf("aws config load error: %w", err) + } + + if cfg.Region == "" { + return aws.Config{}, xerrors.New("aws region is required") + } + + return cfg, nil +} diff --git a/pkg/db/db.go b/pkg/db/db.go index 9b32fceb8bbe..35c5ceed0f9b 100644 --- a/pkg/db/db.go +++ b/pkg/db/db.go @@ -1,144 +1,251 @@ package db import ( - "encoding/json" + "context" + "errors" + "fmt" "os" "path/filepath" + "time" - "github.com/knqyf263/trivy/pkg/log" - + "github.com/google/go-containerregistry/pkg/name" + "github.com/samber/lo" + bolt "go.etcd.io/bbolt" "golang.org/x/xerrors" - "github.com/knqyf263/trivy/pkg/utils" + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/metadata" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/oci" +) - bolt "github.com/etcd-io/bbolt" +const ( + SchemaVersion = db.SchemaVersion + dbMediaType = "application/vnd.aquasec.trivy.db.layer.v1.tar+gzip" ) var ( - db *bolt.DB - dbDir = filepath.Join(utils.CacheDir(), "db") + // GitHub Container Registry + DefaultGHCRRepository = fmt.Sprintf("%s:%d", "ghcr.io/aquasecurity/trivy-db", db.SchemaVersion) + defaultGHCRRepository = lo.Must(name.NewTag(DefaultGHCRRepository)) + + // GCR mirror + DefaultGCRRepository = fmt.Sprintf("%s:%d", "mirror.gcr.io/aquasec/trivy-db", db.SchemaVersion) + defaultGCRRepository = lo.Must(name.NewTag(DefaultGCRRepository)) + + Close = db.Close + Path = db.Path ) -func Init() (err error) { - if err = os.MkdirAll(dbDir, 0700); err != nil { - return xerrors.Errorf("failed to mkdir: %w", err) +// Init initializes the vulnerability database with read-only mode +func Init(dbDir string, opts ...db.Option) error { + opts = append(opts, db.WithBoltOptions(&bolt.Options{ReadOnly: true})) + return db.Init(dbDir, opts...) +} + +type options struct { + artifact *oci.Artifact + dbRepositories []name.Reference +} + +// Option is a functional option +type Option func(*options) + +// WithOCIArtifact takes an OCI artifact +func WithOCIArtifact(art *oci.Artifact) Option { + return func(opts *options) { + opts.artifact = art } +} - dbPath := filepath.Join(dbDir, "trivy.db") - log.Logger.Debugf("db path: %s", dbPath) - db, err = bolt.Open(dbPath, 0600, nil) - if err != nil { - return xerrors.Errorf("failed to open db: %w", err) +// WithDBRepository takes a dbRepository +func WithDBRepository(dbRepository []name.Reference) Option { + return func(opts *options) { + opts.dbRepositories = dbRepository } - return nil } -func Reset() error { - if err := os.RemoveAll(dbDir); err != nil { - return xerrors.Errorf("failed to reset DB: %w", err) +// Client implements DB operations +type Client struct { + *options + + dbDir string + metadata metadata.Client + quiet bool +} + +func Dir(cacheDir string) string { + return filepath.Join(cacheDir, "db") +} + +// NewClient is the factory method for DB client +func NewClient(dbDir string, quiet bool, opts ...Option) *Client { + o := &options{ + dbRepositories: []name.Reference{ + defaultGCRRepository, + defaultGHCRRepository, + }, + } + + for _, opt := range opts { + opt(o) + } + + return &Client{ + options: o, + dbDir: dbDir, + metadata: metadata.NewClient(dbDir), + quiet: quiet, } - return nil } -func GetVersion() string { - var version string - value, err := Get("trivy", "metadata", "version") +// NeedsUpdate check is DB needs update +func (c *Client) NeedsUpdate(ctx context.Context, cliVersion string, skip bool) (bool, error) { + var noRequiredFiles bool + if _, err := os.Stat(db.Path(c.dbDir)); errors.Is(err, os.ErrNotExist) { + log.DebugContext(ctx, "There is no db file") + noRequiredFiles = true + } + meta, err := c.metadata.Get() if err != nil { - return "" + log.DebugContext(ctx, "There is no valid metadata file", log.Err(err)) + noRequiredFiles = true + + meta = metadata.Metadata{Version: db.SchemaVersion} } - if err = json.Unmarshal(value, &version); err != nil { - return "" + + // We can't use the DB if either `trivy.db` or `metadata.json` is missing. + // In that case, we need to download the DB. + if noRequiredFiles { + if skip { + log.ErrorContext(ctx, "The first run cannot skip downloading DB") + return false, xerrors.New("--skip-db-update cannot be specified on the first run") + } + return true, nil + } + + // There are 3 cases when DownloadAt field is zero: + // - metadata file was not created yet. This is the first run of Trivy. + // - trivy-db was downloaded with `oras`. In this case user can use `--skip-db-update` (like for air-gapped) or re-download trivy-db. + // - trivy-db was corrupted while copying from tmp directory to cache directory. We should update this trivy-db. + // We can't detect these cases, so we will show warning for users who use oras + air-gapped. + if meta.DownloadedAt.IsZero() && !skip { + log.WarnContext(ctx, "Trivy DB may be corrupted and will be re-downloaded. If you manually downloaded DB - use the `--skip-db-update` flag to skip updating DB.") + return true, nil + } + + if db.SchemaVersion < meta.Version { + log.ErrorContext(ctx, "Trivy version is old. Update to the latest version.", log.String("version", cliVersion)) + return false, xerrors.Errorf("the version of DB schema doesn't match. Local DB: %d, Expected: %d", + meta.Version, db.SchemaVersion) + } + + if skip { + if err = c.validate(meta); err != nil { + return false, xerrors.Errorf("validate error: %w", err) + } + + log.DebugContext(ctx, "Skipping DB update...") + return false, nil } - return version + + if db.SchemaVersion != meta.Version { + log.DebugContext(ctx, "The local DB schema version does not match with supported version schema.", + log.Int("local_version", meta.Version), log.Int("supported_version", db.SchemaVersion)) + return true, nil + } + + return !c.isNewDB(ctx, meta), nil } -func SetVersion(version string) error { - err := Update("trivy", "metadata", "version", version) - if err != nil { - return xerrors.Errorf("failed to save DB version: %w", err) +func (c *Client) validate(meta metadata.Metadata) error { + if db.SchemaVersion != meta.Version { + log.Error("The local DB has an old schema version which is not supported by the current version of Trivy CLI. DB needs to be updated.") + return xerrors.Errorf("--skip-db-update cannot be specified with the old DB schema. Local DB: %d, Expected: %d", + meta.Version, db.SchemaVersion) } return nil } -func Update(rootBucket, nestedBucket, key string, value interface{}) error { - err := db.Update(func(tx *bolt.Tx) error { - return PutNestedBucket(tx, rootBucket, nestedBucket, key, value) - }) - if err != nil { - return xerrors.Errorf("error in db update: %w", err) +func (c *Client) isNewDB(ctx context.Context, meta metadata.Metadata) bool { + now := clock.Now(ctx) + if now.Before(meta.NextUpdate) { + log.Debug("DB update was skipped because the local DB is the latest") + return true } - return err -} -func PutNestedBucket(tx *bolt.Tx, rootBucket, nestedBucket, key string, value interface{}) error { - root, err := tx.CreateBucketIfNotExists([]byte(rootBucket)) - if err != nil { - return xerrors.Errorf("failed to create a bucket: %w", err) + if now.Before(meta.DownloadedAt.Add(time.Hour)) { + log.Debug("DB update was skipped because the local DB was downloaded during the last hour") + return true } - return Put(root, nestedBucket, key, value) + return false } -func Put(root *bolt.Bucket, nestedBucket, key string, value interface{}) error { - nested, err := root.CreateBucketIfNotExists([]byte(nestedBucket)) - if err != nil { - return xerrors.Errorf("failed to create a bucket: %w", err) + +// Download downloads the DB file +func (c *Client) Download(ctx context.Context, dst string, opt types.RegistryOptions) error { + if err := c.downloadDB(ctx, opt, dst); err != nil { + return xerrors.Errorf("OCI artifact error: %w", err) } - v, err := json.Marshal(value) - if err != nil { - return xerrors.Errorf("failed to unmarshal JSON: %w", err) + + if err := c.updateDownloadedAt(ctx, dst); err != nil { + return xerrors.Errorf("failed to update downloaded_at: %w", err) } - return nested.Put([]byte(key), v) + return nil } -func BatchUpdate(fn func(tx *bolt.Tx) error) error { - err := db.Batch(func(tx *bolt.Tx) error { - return fn(tx) - }) - if err != nil { - return xerrors.Errorf("error in batch update: %w", err) + +func (c *Client) Clear(_ context.Context) error { + if err := os.RemoveAll(c.dbDir); err != nil { + return xerrors.Errorf("failed to remove vulnerability database: %w", err) } return nil } -func Get(rootBucket, nestedBucket, key string) (value []byte, err error) { - err = db.View(func(tx *bolt.Tx) error { - root := tx.Bucket([]byte(rootBucket)) - if root == nil { - return nil - } - nested := root.Bucket([]byte(nestedBucket)) - if nested == nil { - return nil - } - value = nested.Get([]byte(key)) - return nil - }) +func (c *Client) updateDownloadedAt(ctx context.Context, dbDir string) error { + log.Debug("Updating database metadata...") + + // We have to initialize a metadata client here + // since the destination may be different from the cache directory. + client := metadata.NewClient(dbDir) + meta, err := client.Get() if err != nil { - return nil, xerrors.Errorf("failed to get data from db: %w", err) + return xerrors.Errorf("unable to get metadata: %w", err) } - return value, nil + + meta.DownloadedAt = clock.Now(ctx).UTC() + if err = client.Update(meta); err != nil { + return xerrors.Errorf("failed to update metadata: %w", err) + } + + return nil } -func ForEach(rootBucket, nestedBucket string) (value map[string][]byte, err error) { - value = map[string][]byte{} - err = db.View(func(tx *bolt.Tx) error { - root := tx.Bucket([]byte(rootBucket)) - if root == nil { - return nil - } - nested := root.Bucket([]byte(nestedBucket)) - if nested == nil { - return nil - } - err := nested.ForEach(func(k, v []byte) error { - value[string(k)] = v - return nil - }) - if err != nil { - return xerrors.Errorf("error in db foreach: %w", err) - } - return nil - }) +func (c *Client) initArtifacts(opt types.RegistryOptions) oci.Artifacts { + if c.artifact != nil { + return oci.Artifacts{c.artifact} + } + return oci.NewArtifacts(c.dbRepositories, opt) +} + +func (c *Client) downloadDB(ctx context.Context, opt types.RegistryOptions, dst string) error { + log.InfoContext(ctx, "Downloading vulnerability DB...") + downloadOpt := oci.DownloadOption{ + MediaType: dbMediaType, + Quiet: c.quiet, + } + if err := c.initArtifacts(opt).Download(ctx, dst, downloadOpt); err != nil { + return xerrors.Errorf("failed to download vulnerability DB: %w", err) + } + return nil +} + +func (c *Client) ShowInfo() error { + meta, err := c.metadata.Get() if err != nil { - return nil, xerrors.Errorf("failed to get all key/value in the specified bucket: %w", err) + return xerrors.Errorf("something wrong with DB: %w", err) } - return value, nil + log.Debug("DB info", log.Int("schema", meta.Version), log.Time("updated_at", meta.UpdatedAt), + log.Time("next_update", meta.NextUpdate), log.Time("downloaded_at", meta.DownloadedAt)) + return nil } diff --git a/pkg/db/db_test.go b/pkg/db/db_test.go new file mode 100644 index 000000000000..6d6e8a8b8146 --- /dev/null +++ b/pkg/db/db_test.go @@ -0,0 +1,321 @@ +package db_test + +import ( + "bytes" + "fmt" + "strings" + "testing" + "time" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/metadata" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/db" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestClient_NeedsUpdate(t *testing.T) { + timeNextUpdateDay1 := time.Date(2019, 9, 1, 0, 0, 0, 0, time.UTC) + timeNextUpdateDay2 := time.Date(2019, 10, 2, 0, 0, 0, 0, time.UTC) + + timeDownloadAt := time.Date(2019, 9, 30, 22, 30, 0, 0, time.UTC) + + tests := []struct { + name string + skip bool + dbFileExists bool + metadata metadata.Metadata + want bool + wantLogs []string + wantErr string + }{ + { + name: "happy path", + dbFileExists: true, + metadata: metadata.Metadata{ + Version: db.SchemaVersion, + NextUpdate: timeNextUpdateDay1, + DownloadedAt: timeDownloadAt, + }, + want: true, + }, + { + name: "happy path for first run", + dbFileExists: true, + metadata: metadata.Metadata{}, + want: true, + wantLogs: []string{ + "There is no valid metadata file", + }, + }, + { + name: "happy path for first run without trivy.db", + dbFileExists: false, + want: true, + wantLogs: []string{ + "There is no db file", + "There is no valid metadata file", + }, + }, + { + name: "happy path with old schema version", + dbFileExists: true, + metadata: metadata.Metadata{ + Version: 0, + NextUpdate: timeNextUpdateDay1, + DownloadedAt: timeDownloadAt, + }, + want: true, + wantLogs: []string{ + "The local DB schema version does not match with supported version schema.", + }, + }, + { + name: "happy path with --skip-db-update", + dbFileExists: true, + metadata: metadata.Metadata{ + Version: db.SchemaVersion, + NextUpdate: timeNextUpdateDay1, + DownloadedAt: timeDownloadAt, + }, + skip: true, + want: false, + wantLogs: []string{ + "Skipping DB update...", + }, + }, + { + name: "skip downloading DB", + dbFileExists: true, + metadata: metadata.Metadata{ + Version: db.SchemaVersion, + NextUpdate: timeNextUpdateDay2, + DownloadedAt: timeDownloadAt, + }, + want: false, + wantLogs: []string{ + "DB update was skipped because the local DB is the latest", + }, + }, + { + name: "newer schema version", + dbFileExists: true, + metadata: metadata.Metadata{ + Version: db.SchemaVersion + 1, + NextUpdate: timeNextUpdateDay2, + DownloadedAt: timeDownloadAt, + }, + wantErr: fmt.Sprintf("the version of DB schema doesn't match. Local DB: %d, Expected: %d", + db.SchemaVersion+1, db.SchemaVersion), + wantLogs: []string{ + "Trivy version is old. Update to the latest version.", + }, + }, + { + name: "--skip-db-update without trivy.db on the first run", + dbFileExists: false, + skip: true, + wantErr: "--skip-db-update cannot be specified on the first run", + wantLogs: []string{ + "There is no db file", + "There is no valid metadata file", + "The first run cannot skip downloading DB", + }, + }, + { + name: "--skip-db-update without metadata.json on the first run", + dbFileExists: true, + metadata: metadata.Metadata{}, + skip: true, + wantErr: "--skip-db-update cannot be specified on the first run", + wantLogs: []string{ + "There is no valid metadata file", + "The first run cannot skip downloading DB", + }, + }, + { + name: "--skip-db-update with different schema version", + dbFileExists: true, + metadata: metadata.Metadata{ + Version: 0, + NextUpdate: timeNextUpdateDay1, + DownloadedAt: timeDownloadAt, + }, + skip: true, + wantErr: fmt.Sprintf("--skip-db-update cannot be specified with the old DB schema. Local DB: %d, Expected: %d", + 0, db.SchemaVersion), + wantLogs: []string{ + "The local DB has an old schema version which is not supported by the current version of Trivy CLI. DB needs to be updated.", + }, + }, + { + name: "happy with old DownloadedAt", + dbFileExists: true, + metadata: metadata.Metadata{ + Version: db.SchemaVersion, + NextUpdate: timeNextUpdateDay1, + DownloadedAt: timeDownloadAt, + }, + want: true, + }, + { + name: "skip downloading DB with recent DownloadedAt", + dbFileExists: true, + metadata: metadata.Metadata{ + Version: db.SchemaVersion, + NextUpdate: timeNextUpdateDay1, + DownloadedAt: time.Date(2019, 9, 30, 23, 30, 0, 0, time.UTC), + }, + want: false, + wantLogs: []string{ + "DB update was skipped because the local DB was downloaded during the last hour", + }, + }, + { + name: "DownloadedAt is zero, skip is false", + dbFileExists: true, + skip: false, + metadata: metadata.Metadata{ + Version: db.SchemaVersion, + DownloadedAt: time.Time{}, // zero time + NextUpdate: timeNextUpdateDay1, + }, + want: true, + wantLogs: []string{ + "Trivy DB may be corrupted and will be re-downloaded. If you manually downloaded DB - use the `--skip-db-update` flag to skip updating DB.", + }, + }, + { + name: "DownloadedAt is zero, skip is true", + dbFileExists: true, + skip: true, + metadata: metadata.Metadata{ + Version: db.SchemaVersion, + DownloadedAt: time.Time{}, // zero time + NextUpdate: timeNextUpdateDay1, + }, + want: false, + wantLogs: []string{ + "Skipping DB update...", + }, + }, + { + name: "DownloadedAt is zero, skip is true, old schema version", + dbFileExists: true, + skip: true, + metadata: metadata.Metadata{ + Version: 0, + DownloadedAt: time.Time{}, // zero time + NextUpdate: timeNextUpdateDay1, + }, + wantErr: "--skip-db-update cannot be specified with the old DB schema. Local DB: 0, Expected: 2", + want: false, + wantLogs: []string{ + "The local DB has an old schema version which is not supported by the current version of Trivy CLI. DB needs to be updated.", + }, + }, + { + name: "trivy.db is missing but metadata with recent DownloadedAt", + dbFileExists: false, + metadata: metadata.Metadata{ + Version: db.SchemaVersion, + NextUpdate: timeNextUpdateDay1, + DownloadedAt: time.Date(2019, 9, 30, 23, 30, 0, 0, time.UTC), + }, + want: true, + wantLogs: []string{ + "There is no db file", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := bytes.NewBuffer(nil) + logger := log.New(log.NewHandler(out, &log.Options{Level: log.LevelDebug})) + log.SetDefault(logger) + + // Initialize DB with metadata and optionally create DB file + dbDir := dbtest.InitWithMetadata(t, tt.metadata, tt.dbFileExists) + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + // Set a fake time + ctx := clock.With(t.Context(), time.Date(2019, 10, 1, 0, 0, 0, 0, time.UTC)) + + client := db.NewClient(dbDir, true) + needsUpdate, err := client.NeedsUpdate(ctx, "test", tt.skip) + + // Compare log messages + require.Len(t, lo.Compact(strings.Split(out.String(), "\n")), len(tt.wantLogs)) + for _, logMsg := range tt.wantLogs { + assert.Contains(t, out.String(), logMsg) + } + + switch { + case tt.wantErr != "": + require.ErrorContains(t, err, tt.wantErr, tt.name) + default: + require.NoError(t, err, tt.name) + } + + assert.Equal(t, tt.want, needsUpdate) + }) + } +} + +func TestClient_Download(t *testing.T) { + tests := []struct { + name string + input string + want metadata.Metadata + wantErr string + }{ + { + name: "happy path", + input: "testdata/db.tar.gz", + want: metadata.Metadata{ + Version: 1, + NextUpdate: time.Date(3000, 1, 1, 18, 5, 43, 198355188, time.UTC), + UpdatedAt: time.Date(3000, 1, 1, 12, 5, 43, 198355588, time.UTC), + DownloadedAt: time.Date(2019, 10, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + name: "invalid gzip", + input: "testdata/trivy.db", + wantErr: "OCI artifact error: failed to download vulnerability DB", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set a fake time + ctx := clock.With(t.Context(), time.Date(2019, 10, 1, 0, 0, 0, 0, time.UTC)) + + // Fake DB + art := dbtest.NewFakeDB(t, tt.input, dbtest.FakeDBOptions{}) + + dbDir := db.Dir(t.TempDir()) + client := db.NewClient(dbDir, true, db.WithOCIArtifact(art)) + err := client.Download(ctx, dbDir, ftypes.RegistryOptions{}) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + meta := metadata.NewClient(dbDir) + got, err := meta.Get() + require.NoError(t, err) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/db/testdata/db.tar.gz b/pkg/db/testdata/db.tar.gz new file mode 100644 index 000000000000..3580e70a1104 Binary files /dev/null and b/pkg/db/testdata/db.tar.gz differ diff --git a/pkg/db/testdata/trivy.db b/pkg/db/testdata/trivy.db new file mode 100644 index 000000000000..30d74d258442 --- /dev/null +++ b/pkg/db/testdata/trivy.db @@ -0,0 +1 @@ +test \ No newline at end of file diff --git a/pkg/dependency/id.go b/pkg/dependency/id.go new file mode 100644 index 000000000000..309abe946c5a --- /dev/null +++ b/pkg/dependency/id.go @@ -0,0 +1,56 @@ +package dependency + +import ( + "strconv" + "strings" + + "github.com/mitchellh/hashstructure/v2" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +// ID returns a unique ID for the given library. +// The package ID is used to construct the dependency graph. +// The separator is different for each language type. +func ID(ltype types.LangType, name, version string) string { + if version == "" { + return name + } + + sep := "@" + switch ltype { + // cf. https://github.com/dotnet/sdk/blob/529132850841a6bcfce96799262ce688e3851875/documentation/specs/runtime-configuration-file.md#targets-section-depsjson + case types.Conan, types.DotNetCore: + sep = "/" + case types.GoModule, types.GoBinary: + // Return a module ID according the Go way. + // Format: @v + // e.g. github.com/aquasecurity/go-dep-parser@v0.0.0-20230130190635-5e31092b0621 + if !strings.HasPrefix(version, "v") { + version = "v" + version + } + case types.Jar, types.Pom, types.Gradle, types.Sbt: + sep = ":" + } + return name + sep + version +} + +// UID calculates the hash of the package for the unique ID +func UID(filePath string, pkg types.Package) string { + if pkg.Identifier.UID != "" { + return pkg.Identifier.UID + } + v := map[string]any{ + "filePath": filePath, // To differentiate the hash of the same package but different file path + "pkg": pkg, + } + hash, err := hashstructure.Hash(v, hashstructure.FormatV2, &hashstructure.HashOptions{ + ZeroNil: true, + IgnoreZeroValue: true, + }) + if err != nil { + log.Warn("Failed to calculate the package hash", log.String("pkg", pkg.Name), log.Err(err)) + } + return strconv.FormatUint(hash, 16) +} diff --git a/pkg/dependency/id_test.go b/pkg/dependency/id_test.go new file mode 100644 index 000000000000..2b8c0e4a71b1 --- /dev/null +++ b/pkg/dependency/id_test.go @@ -0,0 +1,84 @@ +package dependency_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestID(t *testing.T) { + type args struct { + ltype types.LangType + name string + version string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "conan", + args: args{ + ltype: types.Conan, + name: "test", + version: "1.0.0", + }, + want: "test/1.0.0", + }, + { + name: "go module", + args: args{ + ltype: types.GoModule, + name: "test", + version: "v1.0.0", + }, + want: "test@v1.0.0", + }, + { + name: "gradle", + args: args{ + ltype: types.Gradle, + name: "test", + version: "1.0.0", + }, + want: "test:1.0.0", + }, + { + name: "sbt", + args: args{ + ltype: types.Sbt, + name: "test", + version: "1.0.0", + }, + want: "test:1.0.0", + }, + { + name: "pip", + args: args{ + ltype: types.Pip, + name: "test", + version: "1.0.0", + }, + want: "test@1.0.0", + }, + { + name: "no version", + args: args{ + ltype: types.Pom, + name: "test", + version: "", + }, + want: "test", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := dependency.ID(tt.args.ltype, tt.args.name, tt.args.version) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/c/conan/parse.go b/pkg/dependency/parser/c/conan/parse.go new file mode 100644 index 000000000000..e66d9d54efd2 --- /dev/null +++ b/pkg/dependency/parser/c/conan/parse.go @@ -0,0 +1,163 @@ +package conan + +import ( + "context" + "encoding/json/jsontext" + "encoding/json/v2" + "slices" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type LockFile struct { + GraphLock GraphLock `json:"graph_lock"` + Requires Requires `json:"requires"` +} + +type GraphLock struct { + Nodes map[string]Node `json:"nodes"` +} + +type Node struct { + Ref string `json:"ref"` + Requires []string `json:"requires"` + xjson.Location +} +type Requires []Require + +type Require struct { + Dependency string + xjson.Location +} + +func (r *Require) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return json.UnmarshalDecode(dec, &r.Dependency) +} + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("conan"), + } +} + +func (p *Parser) parseV1(lock LockFile) ([]ftypes.Package, []ftypes.Dependency, error) { + var pkgs []ftypes.Package + var deps []ftypes.Dependency + var directDeps []string + if root, ok := lock.GraphLock.Nodes["0"]; ok { + directDeps = root.Requires + } + + // Parse packages + parsed := make(map[string]ftypes.Package) + for i, node := range lock.GraphLock.Nodes { + if node.Ref == "" { + continue + } + pkg, err := toPackage(node.Ref, node.Location) + if err != nil { + p.logger.Debug("Parse ref error", log.Err(err)) + continue + } + + // Determine if the package is a direct dependency or not + direct := slices.Contains(directDeps, i) + pkg.Relationship = lo.Ternary(direct, ftypes.RelationshipDirect, ftypes.RelationshipIndirect) + + parsed[i] = pkg + } + + // Parse dependency graph + for i, node := range lock.GraphLock.Nodes { + pkg, ok := parsed[i] + if !ok { + continue + } + + var childDeps []string + for _, req := range node.Requires { + if child, ok := parsed[req]; ok { + childDeps = append(childDeps, child.ID) + } + } + if len(childDeps) != 0 { + deps = append(deps, ftypes.Dependency{ + ID: pkg.ID, + DependsOn: childDeps, + }) + } + + pkgs = append(pkgs, pkg) + } + return pkgs, deps, nil +} + +func (p *Parser) parseV2(lock LockFile) ([]ftypes.Package, []ftypes.Dependency, error) { + var pkgs []ftypes.Package + + for _, req := range lock.Requires { + pkg, err := toPackage(req.Dependency, req.Location) + if err != nil { + p.logger.Debug("Creating package entry from requirement failed", log.Err(err)) + continue + } + + pkgs = append(pkgs, pkg) + } + return pkgs, []ftypes.Dependency{}, nil +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lock LockFile + if err := xjson.UnmarshalRead(r, &lock); err != nil { + return nil, nil, xerrors.Errorf("failed to decode conan lock file: %w", err) + } + + // try to parse requirements as conan v1.x + if lock.GraphLock.Nodes != nil { + p.logger.Debug("Handling conan lockfile as v1.x") + return p.parseV1(lock) + } + // try to parse requirements as conan v2.x + p.logger.Debug("Handling conan lockfile as v2.x") + return p.parseV2(lock) +} + +func parsePackage(text string) (string, string, error) { + // full ref format: package/version@user/channel#rrev:package_id#prev + // various examples: + // 'pkga/0.1@user/testing' + // 'pkgb/0.1.0' + // 'pkgc/system' + // 'pkgd/0.1.0#7dcb50c43a5a50d984c2e8fa5898bf18' + ss := strings.Split(strings.Split(strings.Split(text, "@")[0], "#")[0], "/") + if len(ss) != 2 { + return "", "", xerrors.Errorf("Unable to determine conan dependency: %q", text) + } + return ss[0], ss[1], nil +} + +func toPackage(pkg string, location xjson.Location) (ftypes.Package, error) { + name, version, err := parsePackage(pkg) + if err != nil { + return ftypes.Package{}, err + } + return ftypes.Package{ + ID: dependency.ID(ftypes.Conan, name, version), + Name: name, + Version: version, + Locations: []ftypes.Location{ftypes.Location(location)}, + }, nil +} diff --git a/pkg/dependency/parser/c/conan/parse_test.go b/pkg/dependency/parser/c/conan/parse_test.go new file mode 100644 index 000000000000..75a1fbed3c42 --- /dev/null +++ b/pkg/dependency/parser/c/conan/parse_test.go @@ -0,0 +1,164 @@ +package conan_test + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/c/conan" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + inputFile string // Test input file + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + }{ + { + name: "happy path", + inputFile: "testdata/happy_v1_case1.lock", + wantPkgs: []ftypes.Package{ + { + ID: "pkga/0.0.1", + Name: "pkga", + Version: "0.0.1", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 13, + EndLine: 22, + }, + }, + }, + { + ID: "pkgc/0.1.1", + Name: "pkgc", + Version: "0.1.1", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 30, + EndLine: 35, + }, + }, + }, + { + ID: "pkgb/system", + Name: "pkgb", + Version: "system", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 23, + EndLine: 29, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "pkga/0.0.1", + DependsOn: []string{ + "pkgb/system", + }, + }, + }, + }, + { + name: "happy path. lock file with revisions support", + inputFile: "testdata/happy_v1_case2.lock", + wantPkgs: []ftypes.Package{ + { + ID: "openssl/3.0.3", + Name: "openssl", + Version: "3.0.3", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 12, + EndLine: 22, + }, + }, + }, + { + ID: "zlib/1.2.12", + Name: "zlib", + Version: "1.2.12", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 23, + EndLine: 30, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "openssl/3.0.3", + DependsOn: []string{ + "zlib/1.2.12", + }, + }, + }, + }, + { + name: "happy path conan v2", + inputFile: "testdata/happy_v2.lock", + wantPkgs: []ftypes.Package{ + { + ID: "matrix/1.3", + Name: "matrix", + Version: "1.3", + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + ID: "sound32/1.0", + Name: "sound32", + Version: "1.0", + Locations: []ftypes.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{}, + }, + { + name: "happy path. lock file without dependencies", + inputFile: "testdata/empty_v1.lock", + }, + { + name: "sad path. wrong ref format", + inputFile: "testdata/sad_v1.lock", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + gotPkgs, gotDeps, err := conan.NewParser().Parse(t.Context(), f) + require.NoError(t, err) + + sort.Sort(ftypes.Packages(gotPkgs)) + + assert.Equal(t, tt.wantPkgs, gotPkgs) + assert.Equal(t, tt.wantDeps, gotDeps) + }) + } +} diff --git a/pkg/dependency/parser/c/conan/testdata/empty_v1.lock b/pkg/dependency/parser/c/conan/testdata/empty_v1.lock new file mode 100644 index 000000000000..13708b45b920 --- /dev/null +++ b/pkg/dependency/parser/c/conan/testdata/empty_v1.lock @@ -0,0 +1,14 @@ +{ + "graph_lock": { + "nodes": { + "0": { + "options": "", + "path": "conanfile.txt", + "context": "host" + } + }, + "revisions_enabled": false + }, + "version": "0.4", + "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=9\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n" +} \ No newline at end of file diff --git a/pkg/dependency/parser/c/conan/testdata/happy_v1_case1.lock b/pkg/dependency/parser/c/conan/testdata/happy_v1_case1.lock new file mode 100644 index 000000000000..764f490c07e4 --- /dev/null +++ b/pkg/dependency/parser/c/conan/testdata/happy_v1_case1.lock @@ -0,0 +1,41 @@ +{ + "graph_lock": { + "nodes": { + "0": { + "options": "glew:fPIC=True\nglew:shared=False\nglew:with_egl=False", + "requires": [ + "1", + "3" + ], + "path": "conanfile.txt", + "context": "host" + }, + "1": { + "ref": "pkga/0.0.1", + "options": "fPIC=True\nshared=False\nwith_egl=False", + "package_id": "", + "prev": "0", + "requires": [ + "2" + ], + "context": "host" + }, + "2": { + "ref": "pkgb/system", + "options": "", + "package_id": "", + "prev": "0", + "context": "host" + }, + "3": { + "ref": "pkgc/0.1.1@user/testing", + "options": "shared=False", + "path": "conanfile.txt", + "context": "host" + } + }, + "revisions_enabled": false + }, + "version": "0.4", + "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=9\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n" +} \ No newline at end of file diff --git a/pkg/dependency/parser/c/conan/testdata/happy_v1_case2.lock b/pkg/dependency/parser/c/conan/testdata/happy_v1_case2.lock new file mode 100644 index 000000000000..33ba5dbf7e67 --- /dev/null +++ b/pkg/dependency/parser/c/conan/testdata/happy_v1_case2.lock @@ -0,0 +1,36 @@ +{ + "graph_lock": { + "nodes": { + "0": { + "options": "openssl:386=False\nopenssl:enable_weak_ssl_ciphers=False\nopenssl:fPIC=True\nopenssl:no_aria=False\nopenssl:no_asm=False\nopenssl:no_async=False\nopenssl:no_bf=False\nopenssl:no_blake2=False\nopenssl:no_camellia=False\nopenssl:no_cast=False\nopenssl:no_chacha=False\nopenssl:no_cms=False\nopenssl:no_comp=False\nopenssl:no_ct=False\nopenssl:no_deprecated=False\nopenssl:no_des=False\nopenssl:no_dgram=False\nopenssl:no_dh=False\nopenssl:no_dsa=False\nopenssl:no_dso=False\nopenssl:no_ec=False\nopenssl:no_ecdh=False\nopenssl:no_ecdsa=False\nopenssl:no_engine=False\nopenssl:no_filenames=False\nopenssl:no_fips=False\nopenssl:no_gost=False\nopenssl:no_idea=False\nopenssl:no_legacy=False\nopenssl:no_md2=True\nopenssl:no_md4=False\nopenssl:no_mdc2=False\nopenssl:no_module=False\nopenssl:no_ocsp=False\nopenssl:no_pinshared=False\nopenssl:no_rc2=False\nopenssl:no_rc4=False\nopenssl:no_rc5=False\nopenssl:no_rfc3779=False\nopenssl:no_rmd160=False\nopenssl:no_seed=False\nopenssl:no_sm2=False\nopenssl:no_sm3=False\nopenssl:no_sm4=False\nopenssl:no_sock=False\nopenssl:no_srp=False\nopenssl:no_srtp=False\nopenssl:no_sse2=False\nopenssl:no_ssl=False\nopenssl:no_ssl3=False\nopenssl:no_stdio=False\nopenssl:no_threads=False\nopenssl:no_tls1=False\nopenssl:no_ts=False\nopenssl:no_whirlpool=False\nopenssl:no_zlib=False\nopenssl:openssldir=None\nopenssl:shared=False\nzlib:fPIC=True\nzlib:shared=False", + "requires": [ + "1" + ], + "path": "conanfile.txt", + "context": "host" + }, + "1": { + "ref": "openssl/3.0.3#288ab73765e69844899535609ee0dfe4", + "options": "386=False\nenable_weak_ssl_ciphers=False\nfPIC=True\nno_aria=False\nno_asm=False\nno_async=False\nno_bf=False\nno_blake2=False\nno_camellia=False\nno_cast=False\nno_chacha=False\nno_cms=False\nno_comp=False\nno_ct=False\nno_deprecated=False\nno_des=False\nno_dgram=False\nno_dh=False\nno_dsa=False\nno_dso=False\nno_ec=False\nno_ecdh=False\nno_ecdsa=False\nno_engine=False\nno_filenames=False\nno_fips=False\nno_gost=False\nno_idea=False\nno_legacy=False\nno_md2=True\nno_md4=False\nno_mdc2=False\nno_module=False\nno_ocsp=False\nno_pinshared=False\nno_rc2=False\nno_rc4=False\nno_rc5=False\nno_rfc3779=False\nno_rmd160=False\nno_seed=False\nno_sm2=False\nno_sm3=False\nno_sm4=False\nno_sock=False\nno_srp=False\nno_srtp=False\nno_sse2=False\nno_ssl=False\nno_ssl3=False\nno_stdio=False\nno_threads=False\nno_tls1=False\nno_ts=False\nno_whirlpool=False\nno_zlib=False\nopenssldir=None\nshared=False\nzlib:fPIC=True\nzlib:shared=False", + "package_id": "772675ace4978e609416253f5f352cadffbfbb53", + "prev": "8f9ca9645535191ea41a1c349e18523f", + "modified": true, + "requires": [ + "2" + ], + "context": "host" + }, + "2": { + "ref": "zlib/1.2.12#b76db676bd992afa93dd18a675323942", + "options": "fPIC=True\nshared=False", + "package_id": "2a19826344ff00be1c04403f2f8e7008ed3a7cc6", + "prev": "88731f6a10f12adde3a66722c1327ada", + "modified": true, + "context": "host" + } + }, + "revisions_enabled": true + }, + "version": "0.4", + "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++11\ncompiler.version=12\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n" +} \ No newline at end of file diff --git a/pkg/dependency/parser/c/conan/testdata/happy_v2.lock b/pkg/dependency/parser/c/conan/testdata/happy_v2.lock new file mode 100644 index 000000000000..f4103baae1d0 --- /dev/null +++ b/pkg/dependency/parser/c/conan/testdata/happy_v2.lock @@ -0,0 +1,12 @@ +{ + "version": "0.5", + "requires": [ + "sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488", + "matrix/1.3#905c3f0babc520684c84127378fefdd0%1675278900.0103245" + ], + "build_requires": [ + "automake/1.16.5#058bda3e21c36c9aa8425daf3c1faf50%1701120593.68", + "autoconf/2.71#00a1e46d8ba5baaf7f10d64c1a6a0342%1709043523.063" + ], + "python_requires": [] +} \ No newline at end of file diff --git a/pkg/dependency/parser/c/conan/testdata/sad_v1.lock b/pkg/dependency/parser/c/conan/testdata/sad_v1.lock new file mode 100644 index 000000000000..6c951e0ca405 --- /dev/null +++ b/pkg/dependency/parser/c/conan/testdata/sad_v1.lock @@ -0,0 +1,24 @@ +{ + "graph_lock": { + "nodes": { + "0": { + "options": "", + "requires": [ + "1" + ], + "path": "conanfile.txt", + "context": "host" + }, + "1": { + "ref": "wrong/ref/format", + "options": "", + "package_id": "", + "prev": "0", + "context": "host" + } + }, + "revisions_enabled": false + }, + "version": "0.4", + "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=9\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n" +} \ No newline at end of file diff --git a/pkg/dependency/parser/conda/environment/parse.go b/pkg/dependency/parser/conda/environment/parse.go new file mode 100644 index 000000000000..3aaccbc1d34f --- /dev/null +++ b/pkg/dependency/parser/conda/environment/parse.go @@ -0,0 +1,150 @@ +package environment + +import ( + "sort" + "strings" + "sync" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/go-version/pkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type environment struct { + Entries []Entry `yaml:"dependencies"` + Prefix string `yaml:"prefix"` +} + +type Entry struct { + Dependencies []Dependency +} + +type Dependency struct { + Value string + Line int +} + +type Packages struct { + Packages ftypes.Packages + Prefix string +} + +type Parser struct { + logger *log.Logger + once sync.Once +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("conda"), + once: sync.Once{}, + } +} + +func (p *Parser) Parse(r xio.ReadSeekerAt) (Packages, error) { + var env environment + if err := yaml.NewDecoder(r).Decode(&env); err != nil { + return Packages{}, xerrors.Errorf("unable to decode conda environment.yml file: %w", err) + } + + var pkgs ftypes.Packages + for _, entry := range env.Entries { + for _, dep := range entry.Dependencies { + pkg := p.toPackage(dep) + // Skip empty pkgs + if pkg.Name == "" { + continue + } + pkgs = append(pkgs, pkg) + } + } + + sort.Sort(pkgs) + return Packages{ + Packages: pkgs, + Prefix: env.Prefix, + }, nil +} + +func (p *Parser) toPackage(dep Dependency) ftypes.Package { + name, ver := p.parseDependency(dep.Value) + if ver == "" { + p.once.Do(func() { + p.logger.Warn("Unable to detect the dependency versions from `environment.yml` as those versions are not pinned. Use `conda env export` to pin versions.") + }) + } + return ftypes.Package{ + Name: name, + Version: ver, + Locations: ftypes.Locations{ + { + StartLine: dep.Line, + EndLine: dep.Line, + }, + }, + } +} + +// parseDependency parses the dependency line and returns the name and the pinned version. +// The version range is not supported. It parses only the pinned version. +// e.g. +// - numpy 1.8.1 +// - numpy ==1.8.1 +// - numpy 1.8.1 py27_0 +// - numpy=1.8.1=py27_0 +// +// cf. https://docs.conda.io/projects/conda-build/en/latest/resources/package-spec.html#examples-of-package-specs +func (*Parser) parseDependency(line string) (string, string) { + line = strings.NewReplacer(">", " >", "<", " <", "=", " ").Replace(line) + parts := strings.Fields(line) + name := parts[0] + if len(parts) == 1 { + return name, "" + } + if _, err := version.Parse(parts[1]); err != nil { + return name, "" + } + return name, parts[1] +} + +func (e *Entry) UnmarshalYAML(node *yaml.Node) error { + var dependencies []Dependency + // cf. https://github.com/go-yaml/yaml/blob/f6f7691b1fdeb513f56608cd2c32c51f8194bf51/resolve.go#L70-L81 + switch node.Tag { + case "!!str": + dependencies = append(dependencies, Dependency{ + Value: node.Value, + Line: node.Line, + }) + case "!!map": + if node.Content != nil { + // Map key is package manager (e.g. pip). So we need to store only map values (dependencies). + // e.g. dependencies: + // - pip: + // - pandas==2.1.4 + if node.Content[1].Tag != "!!seq" { // Conda supports only map[string][]string format. + return xerrors.Errorf("unsupported dependency type %q on line %d", node.Content[1].Tag, node.Content[1].Line) + } + + for _, depContent := range node.Content[1].Content { + if depContent.Tag != "!!str" { + return xerrors.Errorf("unsupported dependency type %q on line %d", depContent.Tag, depContent.Line) + } + + dependencies = append(dependencies, Dependency{ + Value: depContent.Value, + Line: depContent.Line, + }) + } + } + default: + return xerrors.Errorf("unsupported dependency type %q on line %d", node.Tag, node.Line) + } + + e.Dependencies = dependencies + return nil +} diff --git a/pkg/dependency/parser/conda/environment/parse_test.go b/pkg/dependency/parser/conda/environment/parse_test.go new file mode 100644 index 000000000000..9323f65c6ee4 --- /dev/null +++ b/pkg/dependency/parser/conda/environment/parse_test.go @@ -0,0 +1,230 @@ +package environment_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/conda/environment" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + input string + want environment.Packages + wantErr string + }{ + { + name: "happy path", + input: "testdata/happy.yaml", + want: environment.Packages{ + Packages: []ftypes.Package{ + { + Name: "_openmp_mutex", + Locations: ftypes.Locations{ + { + StartLine: 6, + EndLine: 6, + }, + }, + }, + { + Name: "asgiref", + Version: "3.8.1", + Locations: ftypes.Locations{ + { + StartLine: 21, + EndLine: 21, + }, + }, + }, + { + Name: "blas", + Version: "1.0", + Locations: ftypes.Locations{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + Name: "bzip2", + Version: "1.0.8", + Locations: ftypes.Locations{ + { + StartLine: 19, + EndLine: 19, + }, + }, + }, + { + Name: "ca-certificates", + Version: "2024.2", + Locations: ftypes.Locations{ + { + StartLine: 7, + EndLine: 7, + }, + }, + }, + { + Name: "django", + Version: "5.0.6", + Locations: ftypes.Locations{ + { + StartLine: 22, + EndLine: 22, + }, + }, + }, + { + Name: "ld_impl_linux-aarch64", + Locations: ftypes.Locations{ + { + StartLine: 8, + EndLine: 8, + }, + }, + }, + { + Name: "libblas", + Locations: ftypes.Locations{ + { + StartLine: 9, + EndLine: 9, + }, + }, + }, + { + Name: "libcblas", + Locations: ftypes.Locations{ + { + StartLine: 10, + EndLine: 10, + }, + }, + }, + { + Name: "libexpat", + Version: "2.6.2", + Locations: ftypes.Locations{ + { + StartLine: 11, + EndLine: 11, + }, + }, + }, + { + Name: "libffi", + Version: "3.4.2", + Locations: ftypes.Locations{ + { + StartLine: 12, + EndLine: 12, + }, + }, + }, + { + Name: "libgcc-ng", + Locations: ftypes.Locations{ + { + StartLine: 13, + EndLine: 13, + }, + }, + }, + { + Name: "libgfortran-ng", + Locations: ftypes.Locations{ + { + StartLine: 14, + EndLine: 14, + }, + }, + }, + { + Name: "libgfortran5", + Locations: ftypes.Locations{ + { + StartLine: 15, + EndLine: 15, + }, + }, + }, + { + Name: "libgomp", + Version: "13.2.0", + Locations: ftypes.Locations{ + { + StartLine: 16, + EndLine: 16, + }, + }, + }, + { + Name: "liblapack", + Locations: ftypes.Locations{ + { + StartLine: 17, + EndLine: 17, + }, + }, + }, + { + Name: "libnsl", + Version: "2.0.1", + Locations: ftypes.Locations{ + { + StartLine: 18, + EndLine: 18, + }, + }, + }, + }, + Prefix: "/opt/conda/envs/test-env", + }, + }, + { + name: "invalid yaml file", + input: "testdata/invalid.yaml", + wantErr: "cannot unmarshal !!str `invalid` into environment.environment", + }, + { + name: "`dependency` field uses unsupported type", + input: "testdata/wrong-deps-type.yaml", + wantErr: `unsupported dependency type "!!int" on line 5`, + }, + { + name: "nested field uses unsupported type", + input: "testdata/wrong-nested-type.yaml", + wantErr: `unsupported dependency type "!!str" on line 5`, + }, + { + name: "nested dependency uses unsupported type", + input: "testdata/wrong-nested-dep-type.yaml", + wantErr: `unsupported dependency type "!!map" on line 6`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.input) + require.NoError(t, err) + defer f.Close() + + got, err := environment.NewParser().Parse(f) + + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/conda/environment/testdata/happy.yaml b/pkg/dependency/parser/conda/environment/testdata/happy.yaml new file mode 100644 index 000000000000..29ba800e5bb6 --- /dev/null +++ b/pkg/dependency/parser/conda/environment/testdata/happy.yaml @@ -0,0 +1,24 @@ +name: test-env +channels: + - defaults +dependencies: + - blas=1.0=openblas + - _openmp_mutex + - ca-certificates=2024.2 + - ld_impl_linux-aarch64=2.40.* + - libblas>=3.9 + - libcblas<=3.9.0=22_linuxaarch64_openblas + - libexpat==2.6.2 + - libffi==3.4.2=h3557bc0_5 + - libgcc-ng 13.2|13.3 + - libgfortran-ng >13.2.0,<=13.3 + - libgfortran5 =>13.2.0,<13.3|13.4 + - libgomp 13.2.0 hf8544c7_5 + - liblapack=3.9.*=22_linuxaarch64_openblas + - libnsl=2.0.1=h31becfc_0 + - bzip2=1.0.8=h998d150_5 + - pip: + - asgiref==3.8.1 + - django==5.0.6 + +prefix: /opt/conda/envs/test-env diff --git a/pkg/dependency/parser/conda/environment/testdata/invalid.yaml b/pkg/dependency/parser/conda/environment/testdata/invalid.yaml new file mode 100644 index 000000000000..e466dcbd8e8f --- /dev/null +++ b/pkg/dependency/parser/conda/environment/testdata/invalid.yaml @@ -0,0 +1 @@ +invalid \ No newline at end of file diff --git a/pkg/dependency/parser/conda/environment/testdata/wrong-deps-type.yaml b/pkg/dependency/parser/conda/environment/testdata/wrong-deps-type.yaml new file mode 100644 index 000000000000..e7257be4abe5 --- /dev/null +++ b/pkg/dependency/parser/conda/environment/testdata/wrong-deps-type.yaml @@ -0,0 +1,7 @@ +name: test-env +channels: + - defaults +dependencies: + - 1 + +prefix: /opt/conda/envs/test-env diff --git a/pkg/dependency/parser/conda/environment/testdata/wrong-nested-dep-type.yaml b/pkg/dependency/parser/conda/environment/testdata/wrong-nested-dep-type.yaml new file mode 100644 index 000000000000..0c032b8a6d40 --- /dev/null +++ b/pkg/dependency/parser/conda/environment/testdata/wrong-nested-dep-type.yaml @@ -0,0 +1,9 @@ +name: test-env +channels: + - defaults +dependencies: + - pip: + - wrongType: + - asgiref==3.8.1 + +prefix: /opt/conda/envs/test-env diff --git a/pkg/dependency/parser/conda/environment/testdata/wrong-nested-type.yaml b/pkg/dependency/parser/conda/environment/testdata/wrong-nested-type.yaml new file mode 100644 index 000000000000..2e8c01cad787 --- /dev/null +++ b/pkg/dependency/parser/conda/environment/testdata/wrong-nested-type.yaml @@ -0,0 +1,7 @@ +name: test-env +channels: + - defaults +dependencies: + - pip: asgiref==3.8.1 + +prefix: /opt/conda/envs/test-env diff --git a/pkg/dependency/parser/conda/meta/parse.go b/pkg/dependency/parser/conda/meta/parse.go new file mode 100644 index 000000000000..13c22bcb75c1 --- /dev/null +++ b/pkg/dependency/parser/conda/meta/parse.go @@ -0,0 +1,47 @@ +package meta + +import ( + "context" + "encoding/json" + + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type packageJSON struct { + Name string `json:"name"` + Version string `json:"version"` + License string `json:"license"` +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +// Parse parses Anaconda (a.k.a. conda) environment metadata. +// e.g. /envs//conda-meta/.json +// For details see https://conda.io/projects/conda/en/latest/user-guide/concepts/environments.html +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var data packageJSON + err := json.NewDecoder(r).Decode(&data) + if err != nil { + return nil, nil, xerrors.Errorf("JSON decode error: %w", err) + } + + if data.Name == "" || data.Version == "" { + return nil, nil, xerrors.Errorf("unable to parse conda package") + } + + return []ftypes.Package{ + { + Name: data.Name, + Version: data.Version, + Licenses: licensing.SplitLicenses(data.License), + }, + }, nil, nil +} diff --git a/pkg/dependency/parser/conda/meta/parse_test.go b/pkg/dependency/parser/conda/meta/parse_test.go new file mode 100644 index 000000000000..858fab79b406 --- /dev/null +++ b/pkg/dependency/parser/conda/meta/parse_test.go @@ -0,0 +1,70 @@ +package meta_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/conda/meta" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + input string + want []ftypes.Package + wantErr string + }{ + { + name: "_libgcc_mutex", + input: "testdata/_libgcc_mutex-0.1-main.json", + want: []ftypes.Package{ + { + Name: "_libgcc_mutex", + Version: "0.1", + }, + }, + }, + { + name: "libgomp", + input: "testdata/libgomp-11.2.0-h1234567_1.json", + want: []ftypes.Package{ + { + Name: "libgomp", + Version: "11.2.0", + Licenses: []string{"GPL-3.0-only WITH GCC-exception-3.1"}, + }, + }, + }, + { + name: "invalid_json", + input: "testdata/invalid_json.json", + wantErr: "JSON decode error: invalid character", + }, + { + name: "invalid_package", + input: "testdata/invalid_package.json", + wantErr: "unable to parse conda package", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.input) + require.NoError(t, err) + defer f.Close() + + got, _, err := meta.NewParser().Parse(t.Context(), f) + + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/conda/meta/testdata/README.md b/pkg/dependency/parser/conda/meta/testdata/README.md new file mode 100644 index 000000000000..023eab2caee9 --- /dev/null +++ b/pkg/dependency/parser/conda/meta/testdata/README.md @@ -0,0 +1,14 @@ +To recreate the test files: +- Start a miniconda container: +```bash +docker run --name miniconda --rm -it continuumio/miniconda3@sha256:58b1c7df8d69655ffec017ede784a075e3c2e9feff0fc50ef65300fc75aa45ae bash +``` +- In the container, initialize a conda environment: +```bash +conda create --yes -n test-dep-parser python=3.9.12 +``` +- Export conda package definitions out of the container: +```bash +docker cp miniconda:/opt/conda/envs/test-dep-parser/conda-meta/_libgcc_mutex-0.1-main.json . +docker cp miniconda:/opt/conda/envs/test-dep-parser/conda-meta/libgomp-11.2.0-h1234567_1.json . +``` diff --git a/pkg/dependency/parser/conda/meta/testdata/_libgcc_mutex-0.1-main.json b/pkg/dependency/parser/conda/meta/testdata/_libgcc_mutex-0.1-main.json new file mode 100644 index 000000000000..507261770721 --- /dev/null +++ b/pkg/dependency/parser/conda/meta/testdata/_libgcc_mutex-0.1-main.json @@ -0,0 +1,32 @@ +{ + "build": "main", + "build_number": 0, + "channel": "https://repo.anaconda.com/pkgs/main/linux-64", + "constrains": [], + "depends": [], + "extracted_package_dir": "/opt/conda/pkgs/_libgcc_mutex-0.1-main", + "features": "", + "files": [], + "fn": "_libgcc_mutex-0.1-main.conda", + "legacy_bz2_md5": "013d3f22cd3b87f71224bd936765bcad", + "legacy_bz2_size": 3121, + "link": { + "source": "/opt/conda/pkgs/_libgcc_mutex-0.1-main", + "type": 1 + }, + "md5": "c3473ff8bdb3d124ed5ff11ec380d6f9", + "name": "_libgcc_mutex", + "package_tarball_full_path": "/opt/conda/pkgs/_libgcc_mutex-0.1-main.conda", + "paths_data": { + "paths": [], + "paths_version": 1 + }, + "requested_spec": "None", + "sha256": "476626712f60e5ef0fe04c354727152b1ee5285d57ccd3575c7be930122bd051", + "size": 3473, + "subdir": "linux-64", + "timestamp": 1562011674792, + "track_features": "", + "url": "https://repo.anaconda.com/pkgs/main/linux-64/_libgcc_mutex-0.1-main.conda", + "version": "0.1" +} \ No newline at end of file diff --git a/pkg/dependency/parser/conda/meta/testdata/invalid_json.json b/pkg/dependency/parser/conda/meta/testdata/invalid_json.json new file mode 100644 index 000000000000..1cc7687e5f40 --- /dev/null +++ b/pkg/dependency/parser/conda/meta/testdata/invalid_json.json @@ -0,0 +1 @@ +Invalid JSON \ No newline at end of file diff --git a/pkg/dependency/parser/conda/meta/testdata/invalid_package.json b/pkg/dependency/parser/conda/meta/testdata/invalid_package.json new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/pkg/dependency/parser/conda/meta/testdata/invalid_package.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkg/dependency/parser/conda/meta/testdata/libgomp-11.2.0-h1234567_1.json b/pkg/dependency/parser/conda/meta/testdata/libgomp-11.2.0-h1234567_1.json new file mode 100644 index 000000000000..7a13ea8b0386 --- /dev/null +++ b/pkg/dependency/parser/conda/meta/testdata/libgomp-11.2.0-h1234567_1.json @@ -0,0 +1,60 @@ +{ + "build": "h1234567_1", + "build_number": 1, + "channel": "https://repo.anaconda.com/pkgs/main/linux-64", + "constrains": [], + "depends": [ + "_libgcc_mutex 0.1 main" + ], + "extracted_package_dir": "/opt/conda/pkgs/libgomp-11.2.0-h1234567_1", + "features": "", + "files": [ + "lib/libgomp.so", + "lib/libgomp.so.1.0.0", + "share/licenses/gcc-libs/RUNTIME.LIBRARY.EXCEPTION.gomp_copy" + ], + "fn": "libgomp-11.2.0-h1234567_1.conda", + "legacy_bz2_md5": "3080c2813e6e1d0f8a100a38b5b3456d", + "legacy_bz2_size": 573231, + "license": "GPL-3.0-only WITH GCC-exception-3.1", + "link": { + "source": "/opt/conda/pkgs/libgomp-11.2.0-h1234567_1", + "type": 1 + }, + "md5": "b372c0eea9b60732fdae4b817a63c8cd", + "name": "libgomp", + "package_tarball_full_path": "/opt/conda/pkgs/libgomp-11.2.0-h1234567_1.conda", + "paths_data": { + "paths": [ + { + "_path": "lib/libgomp.so", + "path_type": "softlink", + "sha256": "e3b68c5f37afb7b70bd12273b69706ab33a397714e8336910f0e47f8f1cf6854", + "size_in_bytes": 1265616 + }, + { + "_path": "lib/libgomp.so.1.0.0", + "path_type": "hardlink", + "sha256": "e3b68c5f37afb7b70bd12273b69706ab33a397714e8336910f0e47f8f1cf6854", + "sha256_in_prefix": "e3b68c5f37afb7b70bd12273b69706ab33a397714e8336910f0e47f8f1cf6854", + "size_in_bytes": 1265616 + }, + { + "_path": "share/licenses/gcc-libs/RUNTIME.LIBRARY.EXCEPTION.gomp_copy", + "path_type": "hardlink", + "sha256": "9d6b43ce4d8de0c878bf16b54d8e7a10d9bd42b75178153e3af6a815bdc90f74", + "sha256_in_prefix": "9d6b43ce4d8de0c878bf16b54d8e7a10d9bd42b75178153e3af6a815bdc90f74", + "size_in_bytes": 3324 + } + ], + "paths_version": 1 + }, + "requested_spec": "None", + "sha256": "a1c6e599df45e116af81c36ec4c9efb1793fa3a0b854dd90dd6c8813cd476e90", + "size": 485145, + "subdir": "linux-64", + "timestamp": 1654090775721, + "track_features": "", + "url": "https://repo.anaconda.com/pkgs/main/linux-64/libgomp-11.2.0-h1234567_1.conda", + "version": "11.2.0" +} \ No newline at end of file diff --git a/pkg/dependency/parser/dart/pub/parse.go b/pkg/dependency/parser/dart/pub/parse.go new file mode 100644 index 000000000000..b415a8e57dd6 --- /dev/null +++ b/pkg/dependency/parser/dart/pub/parse.go @@ -0,0 +1,145 @@ +package pub + +import ( + "context" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + goversion "github.com/aquasecurity/go-version/pkg/version" + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +const ( + directMain = "direct main" + directDev = "direct dev" + transitiveDep = "transitive" +) + +// Parser is a parser for pubspec.lock +type Parser struct { + logger *log.Logger + useMinVersion bool +} + +func NewParser(useMinVersion bool) *Parser { + return &Parser{ + logger: log.WithPrefix("pub"), + useMinVersion: useMinVersion, + } +} + +type lock struct { + Packages map[string]Dep `yaml:"packages"` + Sdks map[string]string `yaml:"sdks"` +} + +type Dep struct { + Dependency string `yaml:"dependency"` + Version string `yaml:"version"` + Source string `yaml:"source"` + Description Description `yaml:"description"` +} + +type Description string + +func (p Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + l := &lock{} + if err := yaml.NewDecoder(r).Decode(&l); err != nil { + return nil, nil, xerrors.Errorf("failed to decode pubspec.lock: %w", err) + } + var pkgs []ftypes.Package + for name, dep := range l.Packages { + version := dep.Version + if version == "0.0.0" && dep.Source == "sdk" && p.useMinVersion { + version = p.findSDKVersion(l, name, dep) + } + + // We would like to exclude dev dependencies, but we cannot identify + // which indirect dependencies were introduced by dev dependencies + // as there are 3 dependency types, "direct main", "direct dev" and "transitive". + // It will be confusing if we exclude direct dev dependencies and include transitive dev dependencies. + // We decided to keep all dev dependencies until Pub will add support for "transitive main" and "transitive dev". + pkg := ftypes.Package{ + ID: dependency.ID(ftypes.Pub, name, version), + Name: name, + Version: version, + Relationship: p.relationship(dep.Dependency), + } + pkgs = append(pkgs, pkg) + } + + return pkgs, nil, nil +} + +// findSDKVersion detects the minimum version of the SDK constraint specified in the Description. +// If the constraint is not found, it returns the original version. +func (p Parser) findSDKVersion(l *lock, name string, dep Dep) string { + // Some dependencies use one of the SDK versions. + // In this case dep.Version == `0.0.0`. + // We can't get versions for these dependencies. + // Therefore, we use the minimum version of the SDK constraint specified in the Description. + // See https://github.com/aquasecurity/trivy/issues/6017 + constraint, ok := l.Sdks[string(dep.Description)] + if !ok { + return dep.Version + } + + v, err := minVersionOfConstrain(constraint) + if err != nil { + p.logger.Warn("Unable to get sdk version from constraint", log.Err(err)) + return dep.Version + } else if v == "" { + return dep.Version + } + p.logger.Info("Using the minimum version of the constraint from the sdk source", log.String("dep", name), + log.String("constraint", constraint)) + return v +} + +func (p Parser) relationship(dep string) ftypes.Relationship { + switch dep { + case directMain, directDev: + return ftypes.RelationshipDirect + case transitiveDep: + return ftypes.RelationshipIndirect + } + return ftypes.RelationshipUnknown +} + +// minVersionOfConstrain returns the minimum acceptable version for constraint +func minVersionOfConstrain(constraint string) (string, error) { + css, err := goversion.NewConstraints(constraint) + if err != nil { + return "", xerrors.Errorf("unable to parse constraints: %w", err) + } + + // Dart uses only `>=` and `^` operators: + // cf. https://dart.dev/tools/pub/dependencies#traditional-syntax + constraints := css.List() + if len(constraints) == 0 || len(constraints[0]) == 0 { + return "", nil + } + // We only need to get the minimum version from the range + if constraints[0][0].Operator() != ">=" && constraints[0][0].Operator() != "^" { + return "", nil + } + + return constraints[0][0].Version(), nil +} + +func (d *Description) UnmarshalYAML(value *yaml.Node) error { + var tmp any + if err := value.Decode(&tmp); err != nil { + return err + } + // Description can be a string or a struct + // We only need a string value for SDK mapping + if desc, ok := tmp.(string); ok { + *d = Description(desc) + } + return nil +} diff --git a/pkg/dependency/parser/dart/pub/parse_test.go b/pkg/dependency/parser/dart/pub/parse_test.go new file mode 100644 index 000000000000..9d4340bbf7b4 --- /dev/null +++ b/pkg/dependency/parser/dart/pub/parse_test.go @@ -0,0 +1,103 @@ +package pub_test + +import ( + "fmt" + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/dart/pub" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + useMinVersion bool + inputFile string + want []ftypes.Package + wantErr assert.ErrorAssertionFunc + }{ + { + name: "not use minimum version", + useMinVersion: false, + inputFile: "testdata/happy.lock", + want: []ftypes.Package{ + { + ID: "crypto@3.0.2", + Name: "crypto", + Version: "3.0.2", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "flutter_test@0.0.0", + Name: "flutter_test", + Version: "0.0.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "uuid@3.0.6", + Name: "uuid", + Version: "3.0.6", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantErr: assert.NoError, + }, + { + name: "use minimum version", + useMinVersion: true, + inputFile: "testdata/happy.lock", + want: []ftypes.Package{ + { + ID: "crypto@3.0.2", + Name: "crypto", + Version: "3.0.2", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "flutter_test@3.3.0", + Name: "flutter_test", + Version: "3.3.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "uuid@3.0.6", + Name: "uuid", + Version: "3.0.6", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantErr: assert.NoError, + }, + { + name: "empty path", + inputFile: "testdata/empty.lock", + wantErr: assert.NoError, + }, + { + name: "broken yaml", + inputFile: "testdata/broken.lock", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + gotPkgs, _, err := pub.NewParser(tt.useMinVersion).Parse(t.Context(), f) + if !tt.wantErr(t, err, fmt.Sprintf("Parse(%v)", tt.inputFile)) { + return + } + + sort.Sort(ftypes.Packages(gotPkgs)) + assert.Equal(t, tt.want, gotPkgs) + }) + } +} diff --git a/pkg/dependency/parser/dart/pub/testdata/broken.lock b/pkg/dependency/parser/dart/pub/testdata/broken.lock new file mode 100644 index 000000000000..d86c7d42138e --- /dev/null +++ b/pkg/dependency/parser/dart/pub/testdata/broken.lock @@ -0,0 +1 @@ +[this is not yaml] \ No newline at end of file diff --git a/pkg/dependency/parser/dart/pub/testdata/empty.lock b/pkg/dependency/parser/dart/pub/testdata/empty.lock new file mode 100644 index 000000000000..eb4cea8c2089 --- /dev/null +++ b/pkg/dependency/parser/dart/pub/testdata/empty.lock @@ -0,0 +1,6 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.3.0" \ No newline at end of file diff --git a/pkg/dependency/parser/dart/pub/testdata/happy.lock b/pkg/dependency/parser/dart/pub/testdata/happy.lock new file mode 100644 index 000000000000..b589a2ba3f0a --- /dev/null +++ b/pkg/dependency/parser/dart/pub/testdata/happy.lock @@ -0,0 +1,25 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + crypto: + dependency: "direct main" + description: + name: crypto + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + uuid: + dependency: transitive + description: + name: uuid + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.0.6" +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: "^3.3.0" \ No newline at end of file diff --git a/pkg/dependency/parser/dotnet/core_deps/parse.go b/pkg/dependency/parser/dotnet/core_deps/parse.go new file mode 100644 index 000000000000..06084f80d045 --- /dev/null +++ b/pkg/dependency/parser/dotnet/core_deps/parse.go @@ -0,0 +1,183 @@ +package core_deps + +import ( + "context" + "slices" + "sort" + "strings" + "sync" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type dotNetDependencies struct { + Libraries map[string]dotNetLibrary `json:"libraries"` + RuntimeTarget RuntimeTarget `json:"runtimeTarget"` + Targets map[string]map[string]TargetLib `json:"targets"` +} + +type dotNetLibrary struct { + Type string `json:"type"` + xjson.Location +} + +type RuntimeTarget struct { + Name string `json:"name"` +} + +type TargetLib struct { + Dependencies map[string]string `json:"dependencies"` + Runtime any `json:"runtime"` + RuntimeTargets any `json:"runtimeTargets"` + Native any `json:"native"` +} + +type Parser struct { + logger *log.Logger + once sync.Once +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("dotnet"), + once: sync.Once{}, + } +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var depsFile dotNetDependencies + if err := xjson.UnmarshalRead(r, &depsFile); err != nil { + return nil, nil, xerrors.Errorf("failed to decode .deps.json file: %w", err) + } + + // Get target libraries for RuntimeTarget + targetLibs, targetLibsFound := depsFile.Targets[depsFile.RuntimeTarget.Name] + if !targetLibsFound { + // If the target is not found, take all dependencies + p.logger.Debug("Unable to find `Target` for Runtime Target Name. All dependencies from `libraries` section will be included in the report", log.String("Runtime Target Name", depsFile.RuntimeTarget.Name)) + } + + // First pass: collect all packages + var projectNameVer string + pkgs := make(map[string]ftypes.Package, len(depsFile.Libraries)) + + for nameVer, lib := range depsFile.Libraries { + name, version, ok := strings.Cut(nameVer, "/") + if !ok { + // Invalid name + p.logger.Warn("Cannot parse .NET library version", log.String("library", nameVer)) + continue + } + + // Skip unsupported library types + if !strings.EqualFold(lib.Type, "package") && !strings.EqualFold(lib.Type, "project") { + continue + } + + // Skip non-runtime libraries if target libraries are available + if targetLibsFound && !p.isRuntimeLibrary(targetLibs, nameVer) { + // Skip non-runtime libraries + // cf. https://github.com/aquasecurity/trivy/pull/7039#discussion_r1674566823 + continue + } + + pkg := ftypes.Package{ + ID: packageID(name, version), + Name: name, + Version: version, + Locations: []ftypes.Location{ftypes.Location(lib.Location)}, + } + + // Identify root package + if strings.EqualFold(lib.Type, "project") { + if projectNameVer != "" { + p.logger.Warn("Multiple root projects found in .deps.json", log.String("existing_root", projectNameVer), log.String("new_root", nameVer)) + continue + } + projectNameVer = nameVer + pkg.Relationship = ftypes.RelationshipRoot + } + + pkgs[pkg.ID] = pkg + } + + if len(pkgs) == 0 { + return nil, nil, nil + } + + // If target libraries are not found, return all collected packages without dependencies + if !targetLibsFound { + pkgSlice := lo.Values(pkgs) + sort.Sort(ftypes.Packages(pkgSlice)) + return pkgSlice, nil, nil + } + + directDeps := lo.MapToSlice(targetLibs[projectNameVer].Dependencies, packageID) + + // Second pass: build dependency graph + fill Relationships from targets section + var deps ftypes.Dependencies + for pkgID, pkg := range pkgs { + // Fill relationship field for package + // If Root package didn't find or don't have dependencies, skip setting Relationship, + // because most likely file is broken. + // Root package Relationship is already set + if len(directDeps) > 0 && pkg.Relationship != ftypes.RelationshipRoot { + pkg.Relationship = lo.Ternary(slices.Contains(directDeps, pkgID), ftypes.RelationshipDirect, ftypes.RelationshipIndirect) + pkgs[pkgID] = pkg + } + + // Build dependency graph + dependencies, ok := targetLibs[pkgID] + // Package doesn't have dependencies + if !ok { + continue + } + + var dependsOn []string + for depName, depVersion := range dependencies.Dependencies { + depID := packageID(depName, depVersion) + // Only create dependencies for packages that exist in package lists + if _, exists := pkgs[depID]; exists { + dependsOn = append(dependsOn, depID) + } + } + if len(dependsOn) > 0 { + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: dependsOn, + }) + } + } + + pkgSlice := lo.Values(pkgs) + sort.Sort(ftypes.Packages(pkgSlice)) + sort.Sort(deps) + return pkgSlice, deps, nil +} + +// isRuntimeLibrary returns true if library contains `runtime`, `runtimeTarget` or `native` sections, or if the library is missing from `targetLibs`. +// See https://github.com/aquasecurity/trivy/discussions/4282#discussioncomment-8830365 for more details. +func (p *Parser) isRuntimeLibrary(targetLibs map[string]TargetLib, library string) bool { + lib, ok := targetLibs[library] + // Selected target doesn't contain library + // Mark these libraries as runtime to avoid mistaken omission + if !ok { + p.once.Do(func() { + p.logger.Debug("Unable to determine that this is runtime library. Library not found in `Target` section.", log.String("Library", library)) + }) + return true + } + // Check that `runtime`, `runtimeTarget` and `native` sections are not empty + return !lo.IsEmpty(lib.Runtime) || !lo.IsEmpty(lib.RuntimeTargets) || !lo.IsEmpty(lib.Native) +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.DotNetCore, name, version) +} diff --git a/pkg/dependency/parser/dotnet/core_deps/parse_test.go b/pkg/dependency/parser/dotnet/core_deps/parse_test.go new file mode 100644 index 000000000000..140d61092f9e --- /dev/null +++ b/pkg/dependency/parser/dotnet/core_deps/parse_test.go @@ -0,0 +1,176 @@ +package core_deps + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + file string // Test input file + want []ftypes.Package + wantDeps []ftypes.Dependency + wantErr string + }{ + { + name: "happy path", + file: "testdata/happy.deps.json", + want: []ftypes.Package{ + { + ID: "ExampleApp1/1.0.0", + Name: "ExampleApp1", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + Locations: []ftypes.Location{ + { + StartLine: 28, + EndLine: 32, + }, + }, + }, + { + ID: "Newtonsoft.Json/13.0.1", + Name: "Newtonsoft.Json", + Version: "13.0.1", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 33, + EndLine: 39, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "ExampleApp1/1.0.0", + DependsOn: []string{"Newtonsoft.Json/13.0.1"}, + }, + }, + }, + { + name: "happy path with skipped libs", + file: "testdata/without-runtime.deps.json", + want: []ftypes.Package{ + { + ID: "hello2/1.0.0", + Name: "hello2", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + Locations: []ftypes.Location{ + { + StartLine: 61, + EndLine: 65, + }, + }, + }, + { + ID: "JsonDiffPatch/2.0.61", + Name: "JsonDiffPatch", + Version: "2.0.61", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 66, + EndLine: 72, + }, + }, + }, + { + ID: "Libuv/1.9.1", + Name: "Libuv", + Version: "1.9.1", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 73, + EndLine: 79, + }, + }, + }, + { + ID: "System.Collections.Immutable/1.3.0", + Name: "System.Collections.Immutable", + Version: "1.3.0", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 101, + EndLine: 107, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "hello2/1.0.0", + DependsOn: []string{"JsonDiffPatch/2.0.61"}, + }, + }, + }, + { + name: "happy path without libs", + file: "testdata/no-libraries.deps.json", + want: nil, + wantDeps: nil, + }, + { + name: "target libs not found", + file: "testdata/missing-target.deps.json", + want: []ftypes.Package{ + { + ID: "ExampleApp1/1.0.0", + Name: "ExampleApp1", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + Locations: []ftypes.Location{ + { + StartLine: 28, + EndLine: 32, + }, + }, + }, + { + ID: "Newtonsoft.Json/13.0.1", + Name: "Newtonsoft.Json", + Version: "13.0.1", + Locations: []ftypes.Location{ + { + StartLine: 33, + EndLine: 39, + }, + }, + }, + }, + wantDeps: nil, + }, + { + name: "sad path", + file: "testdata/invalid.deps.json", + wantErr: "failed to decode .deps.json file: jsontext: unexpected EOF within", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + + got, gotDeps, err := NewParser().Parse(t.Context(), f) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.wantDeps, gotDeps) + }) + } +} diff --git a/pkg/dependency/parser/dotnet/core_deps/testdata/happy.deps.json b/pkg/dependency/parser/dotnet/core_deps/testdata/happy.deps.json new file mode 100644 index 000000000000..d2fbdc825ac5 --- /dev/null +++ b/pkg/dependency/parser/dotnet/core_deps/testdata/happy.deps.json @@ -0,0 +1,41 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v5.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v5.0": { + "ExampleApp1/1.0.0": { + "dependencies": { + "Newtonsoft.Json": "13.0.1" + }, + "runtime": { + "ExampleApp1.dll": {} + } + }, + "Newtonsoft.Json/13.0.1": { + "runtime": { + "lib/netstandard2.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.1.25517" + } + } + } + } + }, + "libraries": { + "ExampleApp1/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Newtonsoft.Json/13.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==", + "path": "newtonsoft.json/13.0.1", + "hashPath": "newtonsoft.json.13.0.1.nupkg.sha512" + } + } + } \ No newline at end of file diff --git a/pkg/dependency/parser/dotnet/core_deps/testdata/invalid.deps.json b/pkg/dependency/parser/dotnet/core_deps/testdata/invalid.deps.json new file mode 100644 index 000000000000..300197b6fecb --- /dev/null +++ b/pkg/dependency/parser/dotnet/core_deps/testdata/invalid.deps.json @@ -0,0 +1,12 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v5.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v5.0": { + "ExampleApp1/1.0.0": { + "dependencies": { + "Newtonsoft.Json": "13.0.1" + }, \ No newline at end of file diff --git a/pkg/dependency/parser/dotnet/core_deps/testdata/missing-target.deps.json b/pkg/dependency/parser/dotnet/core_deps/testdata/missing-target.deps.json new file mode 100644 index 000000000000..a4b3833f16d5 --- /dev/null +++ b/pkg/dependency/parser/dotnet/core_deps/testdata/missing-target.deps.json @@ -0,0 +1,41 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v5.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "ExampleApp1/1.0.0": { + "dependencies": { + "Newtonsoft.Json": "13.0.1" + }, + "runtime": { + "ExampleApp1.dll": {} + } + }, + "Newtonsoft.Json/13.0.1": { + "runtime": { + "lib/netstandard2.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.1.25517" + } + } + } + } + }, + "libraries": { + "ExampleApp1/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Newtonsoft.Json/13.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A==", + "path": "newtonsoft.json/13.0.1", + "hashPath": "newtonsoft.json.13.0.1.nupkg.sha512" + } + } +} diff --git a/pkg/dependency/parser/dotnet/core_deps/testdata/no-libraries.deps.json b/pkg/dependency/parser/dotnet/core_deps/testdata/no-libraries.deps.json new file mode 100644 index 000000000000..ebd10345378c --- /dev/null +++ b/pkg/dependency/parser/dotnet/core_deps/testdata/no-libraries.deps.json @@ -0,0 +1,27 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v5.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v5.0": { + "ExampleApp1/1.0.0": { + "dependencies": { + "Newtonsoft.Json": "13.0.1" + }, + "runtime": { + "ExampleApp1.dll": {} + } + }, + "Newtonsoft.Json/13.0.1": { + "runtime": { + "lib/netstandard2.0/Newtonsoft.Json.dll": { + "assemblyVersion": "13.0.0.0", + "fileVersion": "13.0.1.25517" + } + } + } + } + } +} diff --git a/pkg/dependency/parser/dotnet/core_deps/testdata/without-runtime.deps.json b/pkg/dependency/parser/dotnet/core_deps/testdata/without-runtime.deps.json new file mode 100644 index 000000000000..ba882c61cb5f --- /dev/null +++ b/pkg/dependency/parser/dotnet/core_deps/testdata/without-runtime.deps.json @@ -0,0 +1,116 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v6.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v6.0": { + "hello2/1.0.0": { + "dependencies": { + "JsonDiffPatch": "2.0.61" + }, + "runtime": { + "hello2.dll": {} + } + }, + "JsonDiffPatch/2.0.61": { + "dependencies": { + "Microsoft.NETCore.App": "1.1.2" + }, + "runtime": { + "lib/netcoreapp1.1/JsonDiffPatch.dll": { + "assemblyVersion": "1.0.0.0", + "fileVersion": "1.0.0.0" + } + } + }, + "Libuv/1.9.1": { + "dependencies": { + "Microsoft.NETCore.Platforms": "1.1.0" + }, + "runtimeTargets": { + "runtimes/debian-x64/native/libuv.so": { + "rid": "debian-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + }, + "runtimes/fedora-x64/native/libuv.so": { + "rid": "fedora-x64", + "assetType": "native", + "fileVersion": "0.0.0.0" + } + } + }, + "Microsoft.NETCore.App/1.1.2": { + "dependencies": { + "Libuv": "1.9.1", + "System.Collections.Immutable": "1.3.0" + } + }, + "Microsoft.NETCore.Platforms/1.1.0": {}, + "NETStandard.Library/1.6.0": { + "dependencies": { + "System.Net.Http": "4.1.0" + } + }, + "System.Net.Http/4.1.0": {} + } + }, + "libraries": { + "hello2/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "JsonDiffPatch/2.0.61": { + "type": "package", + "serviceable": true, + "sha512": "sha512-nZ4QtcU3jR+CBT69qcJBvCcWi5uKgPRrrvSMm4V8Z76ljJ/MFo1P55qXk/nQY0q0WC4v94m5qH4SDhovFfci+Q==", + "path": "jsondiffpatch/2.0.61", + "hashPath": "jsondiffpatch.2.0.61.nupkg.sha512" + }, + "Libuv/1.9.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-uqX2Frwf9PW8MaY7PRNY6HM5BpW1D8oj1EdqzrmbEFD5nH63Yat3aEjN/tws6Tw6Fk7LwmLBvtUh32tTeTaHiA==", + "path": "libuv/1.9.1", + "hashPath": "libuv.1.9.1.nupkg.sha512" + }, + "Microsoft.NETCore.App/1.1.2": { + "type": "package", + "serviceable": true, + "sha512": "sha512-fcN0Ob6rjY7Zu0770cA5l9wRJvj7+ltJPPdryUidejkkhao+y2AYrtezBTlP9nCSFXLmYR9BtaknORT17x8reA==", + "path": "microsoft.netcore.app/1.1.2", + "hashPath": "microsoft.netcore.app.1.1.2.nupkg.sha512" + }, + "Microsoft.NETCore.Platforms/1.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==", + "path": "microsoft.netcore.platforms/1.1.0", + "hashPath": "microsoft.netcore.platforms.1.1.0.nupkg.sha512" + }, + "NETStandard.Library/1.6.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ypsCvIdCZ4IoYASJHt6tF2fMo7N30NLgV1EbmC+snO490OMl9FvVxmumw14rhReWU3j3g7BYudG6YCrchwHJlA==", + "path": "netstandard.library/1.6.0", + "hashPath": "netstandard.library.1.6.0.nupkg.sha512" + }, + "System.Collections.Immutable/1.3.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-zukBRPUuNxwy9m4TGWLxKAnoiMc9+B+8VXeXVyPiBPvOd7yLgAlZ1DlsRWJjMx4VsvhhF2+6q6kO2GRbPja6hA==", + "path": "system.collections.immutable/1.3.0", + "hashPath": "system.collections.immutable.1.3.0.nupkg.sha512" + }, + "System.Net.Http/4.1.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-ULq9g3SOPVuupt+Y3U+A37coXzdNisB1neFCSKzBwo182u0RDddKJF8I5+HfyXqK6OhJPgeoAwWXrbiUXuRDsg==", + "path": "system.net.http/4.1.0", + "hashPath": "system.net.http.4.1.0.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/pkg/dependency/parser/frameworks/wordpress/parse.go b/pkg/dependency/parser/frameworks/wordpress/parse.go new file mode 100644 index 000000000000..561dfde403d1 --- /dev/null +++ b/pkg/dependency/parser/frameworks/wordpress/parse.go @@ -0,0 +1,78 @@ +package wordpress + +import ( + "bufio" + "io" + "strings" + + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Parse(r io.Reader) (lib ftypes.Package, err error) { + + // If wordpress file, open file and + // find line with content + // $wp_version = ''; + + var version string + isComment := false + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + + // Remove comment + commentIndex := strings.Index(line, "//") + if commentIndex != -1 { + line = line[:commentIndex] + } + + line = strings.TrimSpace(line) + + // Handle comment + switch { + case strings.HasPrefix(line, "/*"): + isComment = true + continue + case isComment && strings.HasSuffix(line, "*/"): + isComment = false + continue + case isComment: + continue + } + + // It might include $wp_version_something + if !strings.HasPrefix(line, "$wp_version") { + continue + } + + ss := strings.Split(line, "=") + if len(ss) != 2 || strings.TrimSpace(ss[0]) != "$wp_version" { + continue + } + + // Each variable must end with ";". + end := strings.Index(ss[1], ";") + if end == -1 { + continue + } + + // Remove ";" and white space. + version = strings.TrimSpace(ss[1][:end]) + + // Remove single and double quotes. + version = strings.Trim(version, `'"`) + + break + } + + if err = scanner.Err(); err != nil || version == "" { + return ftypes.Package{}, xerrors.New("version.php could not be parsed") + } + + return ftypes.Package{ + Name: "wordpress", + Version: version, + }, nil +} diff --git a/pkg/dependency/parser/frameworks/wordpress/parse_test.go b/pkg/dependency/parser/frameworks/wordpress/parse_test.go new file mode 100644 index 000000000000..40080f3afd29 --- /dev/null +++ b/pkg/dependency/parser/frameworks/wordpress/parse_test.go @@ -0,0 +1,48 @@ +package wordpress + +import ( + "os" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParseWordPress(t *testing.T) { + tests := []struct { + file string // Test input file + want ftypes.Package + wantErr string + }{ + { + file: "testdata/version.php", + want: ftypes.Package{ + Name: "wordpress", + Version: "4.9.4-alpha", + }, + }, + { + file: "testdata/versionFail.php", + wantErr: "version.php could not be parsed", + }, + } + + for _, tt := range tests { + t.Run(path.Base(tt.file), func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + + got, err := Parse(f) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/frameworks/wordpress/testdata/version.php b/pkg/dependency/parser/frameworks/wordpress/testdata/version.php new file mode 100644 index 000000000000..dd0bf2c1ab0c --- /dev/null +++ b/pkg/dependency/parser/frameworks/wordpress/testdata/version.php @@ -0,0 +1,50 @@ +/cmd/**/*.version.str + // [1]: defaultVersionPrefixes (main, common, version, cmd) + // [2]: other + var foundVersions = make([][]string, 3) + for i := range syms { + sym := &syms[i] + if !strings.HasSuffix(sym.Name, ".str") || sym.Size == 0 { + continue + } + + // Strip ".str" suffix to get the original key (e.g. "main.version") + key := strings.TrimSuffix(sym.Name, ".str") + if !isVersionXKey(key) { + continue + } + + val := readELFSymbolString(f, sym) + if val == "" || !isValidSemVer(val) { + continue + } + + classifyVersion(foundVersions, key, moduleName, val) + } + + return p.chooseVersion(moduleName, foundVersions) +} + +// readELFSymbolString reads the string value of an ELF symbol. +// The offset is computed as sym.Value (virtual address) minus the section's base address, +// following the same pattern as Go's cmd/internal/objfile/elf.go (symbolData function). +// cf. https://go.dev/src/cmd/internal/objfile/elf.go +func readELFSymbolString(f *elf.File, sym *elf.Symbol) string { + if int(sym.Section) >= len(f.Sections) { + return "" + } + + sec := f.Sections[sym.Section] + offset := sym.Value - sec.Addr + buf := make([]byte, sym.Size) + if _, err := sec.ReadAt(buf, int64(offset)); err != nil { + return "" + } + + return strings.TrimRight(string(buf), "\x00") +} diff --git a/pkg/dependency/parser/golang/binary/export_test.go b/pkg/dependency/parser/golang/binary/export_test.go new file mode 100644 index 000000000000..3f361bc1e1a3 --- /dev/null +++ b/pkg/dependency/parser/golang/binary/export_test.go @@ -0,0 +1,15 @@ +package binary + +import "io" + +// Bridge to expose binary parser internals to tests in the binary_test package. + +// ChooseMainVersion exports chooseMainVersion for testing. +func (p *Parser) ChooseMainVersion(version, ldflagsVersion, elfVersion string) string { + return p.chooseMainVersion(version, ldflagsVersion, elfVersion) +} + +// ELFSymbolVersion exports elfSymbolVersion for testing. +func (p *Parser) ELFSymbolVersion(r io.ReaderAt, name string) string { + return p.elfSymbolVersion(r, name) +} diff --git a/pkg/dependency/parser/golang/binary/parse.go b/pkg/dependency/parser/golang/binary/parse.go new file mode 100644 index 000000000000..d4d9c42056e0 --- /dev/null +++ b/pkg/dependency/parser/golang/binary/parse.go @@ -0,0 +1,308 @@ +package binary + +import ( + "context" + "debug/buildinfo" + "fmt" + "runtime/debug" + "sort" + "strings" + + "github.com/mattn/go-shellwords" + "github.com/spf13/pflag" + "golang.org/x/mod/module" + "golang.org/x/mod/semver" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +var ( + ErrUnrecognizedExe = xerrors.New("unrecognized executable format") + ErrNonGoBinary = xerrors.New("non go binary") + + // defaultVersionPrefixes contains common prefixes used in -ldflags version keys + defaultVersionPrefixes = set.NewCaseInsensitive("main", "common", "version", "cmd") +) + +// convertError detects buildinfo.errUnrecognizedFormat and convert to +// ErrUnrecognizedExe and convert buildinfo.errNotGoExe to ErrNonGoBinary +func convertError(err error) error { + errText := err.Error() + if strings.HasSuffix(errText, "unrecognized file format") { + return ErrUnrecognizedExe + } + if strings.HasSuffix(errText, "not a Go executable") { + return ErrNonGoBinary + } + + return err +} + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("gobinary"), + } +} + +// Parse scans file to try to report the Go and module versions. +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + info, err := buildinfo.Read(r) + if err != nil { + return nil, nil, convertError(err) + } + + // Ex: "go1.22.3 X:boringcrypto" + stdlibVersion := strings.TrimPrefix(info.GoVersion, "go") + stdlibVersion, _, _ = strings.Cut(stdlibVersion, " ") + // Add the `v` prefix to be consistent with module and dependency versions. + stdlibVersion = fmt.Sprintf("v%s", stdlibVersion) + + ldflags := p.ldFlags(info.Settings) + pkgs := make(ftypes.Packages, 0, len(info.Deps)+2) + pkgs = append(pkgs, ftypes.Package{ + // Add the Go version used to build this binary. + ID: dependency.ID(ftypes.GoBinary, "stdlib", stdlibVersion), + Name: "stdlib", + Version: stdlibVersion, + Relationship: ftypes.RelationshipDirect, // Considered a direct dependency as the main module depends on the standard packages. + }) + + for _, dep := range info.Deps { + // binaries with old go version may incorrectly add module in Deps + // In this case Path == "", Version == "Devel" + // we need to skip this + if dep.Path == "" { + continue + } + + mod := dep + if dep.Replace != nil { + mod = dep.Replace + } + + version := p.checkVersion(mod.Path, mod.Version) + pkgs = append(pkgs, ftypes.Package{ + ID: dependency.ID(ftypes.GoBinary, mod.Path, version), + Name: mod.Path, + Version: version, + Relationship: ftypes.RelationshipUnknown, + }) + } + + // There are times when gobinaries don't contain Main information. + // e.g. `Go` binaries (e.g. `go`, `gofmt`, etc.) + var deps []ftypes.Dependency + if info.Main.Path != "" { + // Only binaries installed with `go install` contain semver version of the main module. + // Other binaries use the `(devel)` version, but still may contain a stamped version + // set via `go build -ldflags='-X main.version='`, so we fallback to this as. + // as a secondary source. + // See https://github.com/aquasecurity/trivy/issues/1837#issuecomment-1832523477. + version := p.checkVersion(info.Main.Path, info.Main.Version) + ldflagsVersion := p.ParseLDFlags(info.Main.Path, ldflags) + elfVersion := p.elfSymbolVersion(r, info.Main.Path) + version = p.chooseMainVersion(version, ldflagsVersion, elfVersion) + + root := ftypes.Package{ + ID: dependency.ID(ftypes.GoBinary, info.Main.Path, version), + Name: info.Main.Path, + Version: version, + Relationship: ftypes.RelationshipRoot, + } + + depIDs := xslices.Map(pkgs, func(pkg ftypes.Package) string { + return pkg.ID + }) + sort.Strings(depIDs) + + deps = []ftypes.Dependency{ + { + ID: root.ID, + DependsOn: depIDs, // Consider all packages as dependencies of the main module. + }, + } + // Add main module + pkgs = append(pkgs, root) + } + + sort.Sort(pkgs) + return pkgs, deps, nil +} + +// checkVersion detects `(devel)` versions, removes them and adds a debug message about it. +func (p *Parser) checkVersion(name, version string) string { + if version == "(devel)" { + p.logger.Debug("Unable to detect main module's dependency version - `(devel)` is used", log.String("dependency", name)) + return "" + } + return version +} + +// chooseMainVersion determines which version to use for the main module. +// The priority order is: +// 1. Build info version (if it is a real semver, e.g. "v1.2.3" from `go install`) +// 2. ldflags version (e.g. `-ldflags "-X main.version=v1.0.0"`) +// 3. ELF symbol table version (fallback when `-trimpath` hides `-ldflags`) +// 4. Original version as-is (may be empty or a pseudo-version) +// +// Examples: +// +// chooseMainVersion("v1.2.3", "v1.0.0", "v1.0.0") => "v1.2.3" (real semver wins) +// chooseMainVersion("v0.0.0-2024...", "v1.0.0", "") => "v1.0.0" (ldflags over pseudo) +// chooseMainVersion("v0.0.0-2024...", "", "v2.0.0") => "v2.0.0" (ELF over pseudo) +// chooseMainVersion("", "", "") => "" (nothing available) +func (p *Parser) chooseMainVersion(version, ldflagsVersion, elfVersion string) string { + switch { + case version != "" && !module.IsPseudoVersion(version): + return version + case ldflagsVersion != "": + return ldflagsVersion + case elfVersion != "": + return elfVersion + default: + return version + } +} + +func (p *Parser) ldFlags(settings []debug.BuildSetting) []string { + for _, setting := range settings { + if setting.Key != "-ldflags" { + continue + } + + flags, err := shellwords.Parse(setting.Value) + if err != nil { + p.logger.Error("Could not parse -ldflags found in build info", log.Err(err)) + return nil + } + + return flags + } + return nil +} + +// ParseLDFlags attempts to parse the binary's version from any `-ldflags` passed to `go build` at build time. +func (p *Parser) ParseLDFlags(name string, flags []string) string { + p.logger.Debug("Parsing dependency's build info settings", "dependency", name, "-ldflags", flags) + fset := pflag.NewFlagSet("ldflags", pflag.ContinueOnError) + // This prevents the flag set from erroring out if other flags were provided. + // This helps keep the implementation small, so that only the -X flag is needed. + fset.ParseErrorsWhitelist.UnknownFlags = true + // The shorthand name is needed here because setting the full name + // to `X` will cause the flag set to look for `--X` instead of `-X`. + // The flag can also be set multiple times, so a string slice is needed + // to handle that edge case. + var x map[string]string + fset.StringToStringVarP(&x, "", "X", nil, "") + // Init `help` flag to avoid error in flags with `h` (e.g. `-lpthread`) + fset.BoolP("help", "h", false, "just to disable the built-in help flag") + if err := fset.Parse(flags); err != nil { + p.logger.Error("Could not parse -ldflags found in build info", log.Err(err)) + return "" + } + + // foundVersions contains discovered versions by type. + // foundVersions doesn't contain duplicates. Versions are filled into first corresponding category. + // Possible elements(categories): + // [0]: Versions using format `github.com///cmd/**/*.=x.x.x` + // [1]: Versions that use prefixes from `defaultVersionPrefixes` + // [2]: Other versions + var foundVersions = make([][]string, 3) + for key, val := range x { + // It's valid to set the -X flags with quotes so we trim any that might + // have been provided: Ex: + // + // -X main.version=1.0.0 + // -X=main.version=1.0.0 + // -X 'main.version=1.0.0' + // -X='main.version=1.0.0' + // -X="main.version=1.0.0" + // -X "main.version=1.0.0" + key = strings.TrimLeft(key, `'`) + val = strings.TrimRight(val, `'`) + if isVersionXKey(key) && isValidSemVer(val) { + classifyVersion(foundVersions, key, name, val) + } + } + + return p.chooseVersion(name, foundVersions) +} + +// chooseVersion chooses version from found versions +// Categories order: +// module name with `cmd` => versions with default prefixes => other versions +// See more in https://github.com/aquasecurity/trivy/issues/6702#issuecomment-2122271427 +func (p *Parser) chooseVersion(moduleName string, vers [][]string) string { + for _, versions := range vers { + // Versions for this category was not found + if len(versions) == 0 { + continue + } + + // More than 1 version for one category. + // Use empty version. + if len(versions) > 1 { + p.logger.Debug("Unable to detect dependency version. `-ldflags` build info settings contain more than one version. Empty version used.", log.String("dependency", moduleName)) + return "" + } + return versions[0] + } + + p.logger.Debug("Unable to detect dependency version. `-ldflags` build info settings don't contain version flag. Empty version used.", log.String("dependency", moduleName)) + return "" +} + +func isVersionXKey(key string) bool { + key = strings.ToLower(key) + // The check for a 'ver' prefix enables the parser to pick up Trivy's own version value that's set. + return strings.HasSuffix(key, ".version") || strings.HasSuffix(key, ".ver") +} + +func isValidSemVer(ver string) bool { + // semver.IsValid strictly checks for the v prefix so prepending 'v' + // here and checking validity again increases the chances that we + // parse a valid semver version. + return semver.IsValid(ver) || semver.IsValid("v"+ver) +} + +// classifyVersion categorizes a version value into one of three priority tiers +// based on its key: +// +// [0]: /cmd/**/*.version +// [1]: defaultVersionPrefixes (main, common, version, cmd) +// [2]: other +func classifyVersion(foundVersions [][]string, key, moduleName, val string) { + switch { + case strings.HasPrefix(key, moduleName+"/cmd/"): + foundVersions[0] = append(foundVersions[0], val) + case defaultVersionPrefixes.Contains(versionPrefix(key)): + foundVersions[1] = append(foundVersions[1], val) + default: + foundVersions[2] = append(foundVersions[2], val) + } +} + +// versionPrefix returns version prefix from `-ldflags` flag key +// e.g. +// - `github.com/aquasecurity/trivy/pkg/version/app.ver` => `version` +// - `github.com/google/go-containerregistry/cmd/crane/common.ver` => `common` +func versionPrefix(s string) string { + // Trim module part. + // e.g. `github.com/aquasecurity/trivy/pkg/Version.version` => `Version.version` + if lastIndex := strings.LastIndex(s, "/"); lastIndex > 0 { + s = s[lastIndex+1:] + } + + s, _, _ = strings.Cut(s, ".") + return strings.ToLower(s) +} diff --git a/pkg/dependency/parser/golang/binary/parse_test.go b/pkg/dependency/parser/golang/binary/parse_test.go new file mode 100644 index 000000000000..7b6197fe5604 --- /dev/null +++ b/pkg/dependency/parser/golang/binary/parse_test.go @@ -0,0 +1,590 @@ +package binary_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/binary" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + wantPkgs := []ftypes.Package{ + { + ID: "github.com/aquasecurity/test", + Name: "github.com/aquasecurity/test", + Version: "", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "stdlib@v1.15.2", + Name: "stdlib", + Version: "v1.15.2", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + Name: "github.com/aquasecurity/go-pep440-version", + Version: "v0.0.0-20210121094942-22b2f8951d46", + }, + { + ID: "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + Name: "github.com/aquasecurity/go-version", + Version: "v0.0.0-20210121072130-637058cfe492", + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + }, + } + wantDeps := []ftypes.Dependency{ + { + ID: "github.com/aquasecurity/test", + DependsOn: []string{ + "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "stdlib@v1.15.2", + }, + }, + } + + tests := []struct { + name string + inputFile string + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + wantErr string + }{ + { + name: "ELF", + inputFile: "testdata/test.elf", + wantPkgs: wantPkgs, + wantDeps: wantDeps, + }, + { + name: "PE", + inputFile: "testdata/test.exe", + wantPkgs: wantPkgs, + wantDeps: wantDeps, + }, + { + name: "Mach-O", + inputFile: "testdata/test.macho", + wantPkgs: wantPkgs, + wantDeps: wantDeps, + }, + { + name: "with replace directive", + inputFile: "testdata/replace.elf", + wantPkgs: []ftypes.Package{ + { + ID: "github.com/ebati/trivy-mod-parse", + Name: "github.com/ebati/trivy-mod-parse", + Version: "", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "stdlib@v1.16.4", + Name: "stdlib", + Version: "v1.16.4", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "github.com/davecgh/go-spew@v1.1.1", + Name: "github.com/davecgh/go-spew", + Version: "v1.1.1", + }, + { + ID: "github.com/go-sql-driver/mysql@v1.5.0", + Name: "github.com/go-sql-driver/mysql", + Version: "v1.5.0", + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "github.com/ebati/trivy-mod-parse", + DependsOn: []string{ + "github.com/davecgh/go-spew@v1.1.1", + "github.com/go-sql-driver/mysql@v1.5.0", + "stdlib@v1.16.4", + }, + }, + }, + }, + { + name: "with semver main module version", + inputFile: "testdata/semver-main-module-version.macho", + wantPkgs: []ftypes.Package{ + { + ID: "go.etcd.io/bbolt@v1.3.5", + Name: "go.etcd.io/bbolt", + Version: "v1.3.5", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "stdlib@v1.20.6", + Name: "stdlib", + Version: "v1.20.6", + Relationship: ftypes.RelationshipDirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "go.etcd.io/bbolt@v1.3.5", + DependsOn: []string{ + "stdlib@v1.20.6", + }, + }, + }, + }, + { + name: "with -ldflags=\"-X main.version=v1.0.0\"", + inputFile: "testdata/main-version-via-ldflags.elf", + wantPkgs: []ftypes.Package{ + { + ID: "github.com/aquasecurity/test@v1.0.0", + Name: "github.com/aquasecurity/test", + Version: "v1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "stdlib@v1.22.1", + Name: "stdlib", + Version: "v1.22.1", + Relationship: ftypes.RelationshipDirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "github.com/aquasecurity/test@v1.0.0", + DependsOn: []string{ + "stdlib@v1.22.1", + }, + }, + }, + }, + /* + Uncomment the test after migrating to autogenerated binaries for tests + See for details: https://github.com/aquasecurity/trivy/pull/8433#discussion_r1967317284 + { + name: "with -ldflags=\"-X main.version=v1.0.0\" go v1.24", + inputFile: "testdata/main-version-via-ldflags-go-1-24.elf", + wantPkgs: []ftypes.Package{ + { + ID: "github.com/aquasecurity/test@v1.0.0", + Name: "github.com/aquasecurity/test", + Version: "v1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "stdlib@v1.24.0", + Name: "stdlib", + Version: "v1.24.0", + Relationship: ftypes.RelationshipDirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "github.com/aquasecurity/test@v1.0.0", + DependsOn: []string{ + "stdlib@v1.24.0", + }, + }, + }, + }, + */ + { + name: "goexperiment", + inputFile: "testdata/goexperiment", + wantPkgs: []ftypes.Package{ + { + ID: "stdlib@v1.22.1", + Name: "stdlib", + Version: "v1.22.1", + Relationship: ftypes.RelationshipDirect, + }, + }, + }, + { + name: "sad path", + inputFile: "testdata/dummy", + wantErr: "unrecognized executable format", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + gotPkgs, gotDeps, err := binary.NewParser().Parse(t.Context(), f) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.wantPkgs, gotPkgs) + assert.Equal(t, tt.wantDeps, gotDeps) + }) + } +} + +func TestParser_ChooseMainVersion(t *testing.T) { + tests := []struct { + name string + version string + ldflagsVersion string + elfVersion string + want string + }{ + { + name: "v0.0.0 pseudo-version with ldflags", + version: "v0.0.0-20210121094942-22b2f8951d46", + ldflagsVersion: "v1.2.3", + want: "v1.2.3", + }, + { + name: "v2.x pseudo-version with ldflags", + version: "v2.0.0-20251210145848-560ea94fc7d6", + ldflagsVersion: "v2.83.2", + want: "v2.83.2", + }, + { + name: "v1.x pseudo-version with ldflags", + version: "v1.0.1-0.20220426205730-d0a4cdb22955", + ldflagsVersion: "v1.5.0", + want: "v1.5.0", + }, + { + name: "regular semver version with ldflags", + version: "v1.2.3", + ldflagsVersion: "v1.2.4", + want: "v1.2.3", + }, + { + name: "empty version with ldflags", + version: "", + ldflagsVersion: "v1.2.3", + want: "v1.2.3", + }, + { + name: "pseudo-version without ldflags", + version: "v2.0.0-20251210145848-560ea94fc7d6", + ldflagsVersion: "", + want: "v2.0.0-20251210145848-560ea94fc7d6", + }, + { + name: "pseudo-version with ELF version", + version: "v0.0.0-20210121094942-22b2f8951d46", + elfVersion: "v1.0.0", + want: "v1.0.0", + }, + { + name: "pseudo-version with both ldflags and ELF prefers ldflags", + version: "v0.0.0-20210121094942-22b2f8951d46", + ldflagsVersion: "v1.2.3", + elfVersion: "v1.0.0", + want: "v1.2.3", + }, + { + name: "empty version with ELF version", + version: "", + elfVersion: "v2.0.0", + want: "v2.0.0", + }, + { + name: "empty version with both ldflags and ELF prefers ldflags", + version: "", + ldflagsVersion: "v1.2.3", + elfVersion: "v2.0.0", + want: "v1.2.3", + }, + { + name: "regular semver version with ELF version keeps original", + version: "v1.2.3", + elfVersion: "v1.2.4", + want: "v1.2.3", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := binary.NewParser() + got := p.ChooseMainVersion(tt.version, tt.ldflagsVersion, tt.elfVersion) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestParser_ELFSymbolVersion(t *testing.T) { + tests := []struct { + name string + inputFile string + moduleName string + want string + }{ + { + name: "ELF with version symbol", + inputFile: "testdata/main-version-via-ldflags.elf", + moduleName: "github.com/aquasecurity/test", + want: "v1.0.0", + }, + { + name: "ELF without version symbols", + inputFile: "testdata/test.elf", + moduleName: "github.com/aquasecurity/test", + want: "", + }, + { + name: "PE binary (not ELF)", + inputFile: "testdata/test.exe", + moduleName: "github.com/aquasecurity/test", + want: "", + }, + { + name: "Mach-O binary (not ELF)", + inputFile: "testdata/test.macho", + moduleName: "github.com/aquasecurity/test", + want: "", + }, + { + name: "dummy file", + inputFile: "testdata/dummy", + moduleName: "test", + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + p := binary.NewParser() + got := p.ELFSymbolVersion(f, tt.moduleName) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestParser_ParseLDFlags(t *testing.T) { + type args struct { + name string + flags []string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "with version suffix", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version/app.ver=v0.50.1'", + }, + }, + want: "v0.50.1", + }, + { + name: "with version suffix titlecased", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version.Version=v0.50.1'", + }, + }, + want: "v0.50.1", + }, + { + name: "with ver suffix", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version/app.ver=v0.50.1'", + }, + }, + want: "v0.50.1", + }, + { + name: "with ver suffix titlecased", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version.Ver=v0.50.1'", + }, + }, + want: "v0.50.1", + }, + { + name: "with double quoted flag", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X=\"github.com/aquasecurity/trivy/pkg/version.Ver=0.50.1\"", + }, + }, + want: "0.50.1", + }, + { + name: "with semver version without v prefix", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-s", + "-w", + "-X=foo=bar", + "-X='github.com/aquasecurity/trivy/pkg/version.Ver=0.50.1'", + }, + }, + want: "0.50.1", + }, + { + name: "with `cmd` + `default prefix` flags", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-X='github.com/aquasecurity/trivy/cmd/Any.Ver=0.50.0'", + "-X='github.com/aquasecurity/trivy/pkg/version.Ver=0.50.1'", + }, + }, + want: "0.50.0", + }, + { + name: "with `cmd` flag", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-X='github.com/aquasecurity/trivy/cmd/Any.Ver=0.50.0'", + }, + }, + want: "0.50.0", + }, + { + name: "with `cmd` + `other` flags", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-X='github.com/aquasecurity/trivy/cmd/Any.Ver=0.50.0'", + "-X='github.com/aquasecurity/trivy/pkg/Any.Ver=0.50.1'", + }, + }, + want: "0.50.0", + }, + { + name: "with `default prefix` flag", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-X='github.com/aquasecurity/trivy/pkg/Common.Ver=0.50.1'", + }, + }, + want: "0.50.1", + }, + { + name: "with `default prefix` + `other` flags", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-X='github.com/aquasecurity/trivy/pkg/Any.Ver=0.50.0'", + "-X='github.com/aquasecurity/trivy/pkg/Common.Ver=0.50.1'", + }, + }, + want: "0.50.1", + }, + { + name: "with `other` flag", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-X='github.com/aquasecurity/trivy/pkg/Any.Ver=0.50.1'", + }, + }, + want: "0.50.1", + }, + { + name: "with 2 flags using default prefixes", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-X='github.com/aquasecurity/trivy/pkg/Common.Ver=0.50.0'", + "-X='github.com/aquasecurity/trivy/pkg/Main.Ver=0.50.1'", + }, + }, + want: "", + }, + { + name: "with two `other` flags", + args: args{ + name: "github.com/aquasecurity/trivy", + flags: []string{ + "-X='github.com/aquasecurity/trivy/pkg/Any.Ver=0.50.1'", + "-X='github.com/aquasecurity/trivy/pkg/Any-pref.Ver=0.50.0'", + }, + }, + want: "", + }, + { + name: "with version with extra prefix", + args: args{ + name: "github.com/argoproj/argo-cd/v2", + flags: []string{ + "-s", + "-w", + "-X='github.com/argoproj/argo-cd/v2/common.kubectlVersion=v0.26.11'", + }, + }, + want: "", + }, + { + name: "flag with nested flags", + args: args{ + name: "github.com/k3s-io/k3s", + flags: []string{ + "-X", + "github.com/k3s-io/k3s/version.Version=v1.28.6+k3s2", + "-w", + "-s", + "-extldflags", + "-static -lm -ldl -lz -lpthread", + }, + }, + want: "v1.28.6+k3s2", + }, + { + name: "with no flags", + args: args{ + name: "github.com/aquasecurity/test", + flags: []string{}, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := binary.NewParser() + require.Equal(t, tt.want, p.ParseLDFlags(tt.args.name, tt.args.flags)) + }) + } +} diff --git a/pkg/dependency/parser/golang/binary/testdata/dummy b/pkg/dependency/parser/golang/binary/testdata/dummy new file mode 100644 index 000000000000..26bf640459d3 --- /dev/null +++ b/pkg/dependency/parser/golang/binary/testdata/dummy @@ -0,0 +1 @@ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/pkg/dependency/parser/golang/binary/testdata/goexperiment b/pkg/dependency/parser/golang/binary/testdata/goexperiment new file mode 100755 index 000000000000..af68dd8a351d Binary files /dev/null and b/pkg/dependency/parser/golang/binary/testdata/goexperiment differ diff --git a/pkg/dependency/parser/golang/binary/testdata/main-version-via-ldflags.elf b/pkg/dependency/parser/golang/binary/testdata/main-version-via-ldflags.elf new file mode 100755 index 000000000000..8271b6872e12 Binary files /dev/null and b/pkg/dependency/parser/golang/binary/testdata/main-version-via-ldflags.elf differ diff --git a/pkg/dependency/parser/golang/binary/testdata/replace.elf b/pkg/dependency/parser/golang/binary/testdata/replace.elf new file mode 100755 index 000000000000..7f80f48b1047 Binary files /dev/null and b/pkg/dependency/parser/golang/binary/testdata/replace.elf differ diff --git a/pkg/dependency/parser/golang/binary/testdata/semver-main-module-version.macho b/pkg/dependency/parser/golang/binary/testdata/semver-main-module-version.macho new file mode 100755 index 000000000000..aa06dd0dfd35 Binary files /dev/null and b/pkg/dependency/parser/golang/binary/testdata/semver-main-module-version.macho differ diff --git a/pkg/dependency/parser/golang/binary/testdata/test.elf b/pkg/dependency/parser/golang/binary/testdata/test.elf new file mode 100755 index 000000000000..2890300b9205 Binary files /dev/null and b/pkg/dependency/parser/golang/binary/testdata/test.elf differ diff --git a/pkg/dependency/parser/golang/binary/testdata/test.exe b/pkg/dependency/parser/golang/binary/testdata/test.exe new file mode 100755 index 000000000000..36928a26a15f Binary files /dev/null and b/pkg/dependency/parser/golang/binary/testdata/test.exe differ diff --git a/pkg/dependency/parser/golang/binary/testdata/test.macho b/pkg/dependency/parser/golang/binary/testdata/test.macho new file mode 100755 index 000000000000..3f9295be3ee7 Binary files /dev/null and b/pkg/dependency/parser/golang/binary/testdata/test.macho differ diff --git a/pkg/dependency/parser/golang/mod/parse.go b/pkg/dependency/parser/golang/mod/parse.go new file mode 100644 index 000000000000..3dbdc3e44494 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/parse.go @@ -0,0 +1,261 @@ +package mod + +import ( + "context" + "fmt" + "io" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/samber/lo" + "golang.org/x/mod/modfile" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +var ( + // By convention, modules with a major version equal to or above v2 + // have it as suffix in their module path. + VCSUrlMajorVersionSuffixRegex = regexp.MustCompile(`(/v[\d]+)$`) + + // gopkg.in/user/pkg.v -> github.com/user/pkg + VCSUrlGoPkgInRegexWithUser = regexp.MustCompile(`^gopkg\.in/([^/]+)/([^.]+)\..*$`) + + // gopkg.in without user segment + // Example: gopkg.in/pkg.v3 -> github.com/go-pkg/pkg + VCSUrlGoPkgInRegexWithoutUser = regexp.MustCompile(`^gopkg\.in/([^.]+)\..*$`) +) + +type Parser struct { + replace bool // 'replace' represents if the 'replace' directive should be taken into account. + useMinVersion bool +} + +func NewParser(replace, useMinVersion bool) *Parser { + return &Parser{ + replace: replace, + useMinVersion: useMinVersion, + } +} + +func (p *Parser) GetExternalRefs(path string) []ftypes.ExternalRef { + if url := resolveVCSUrl(path); url != "" { + return []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: url, + }, + } + } + + return nil +} + +func resolveVCSUrl(modulePath string) string { + switch { + case strings.HasPrefix(modulePath, "github.com/"): + return "https://" + VCSUrlMajorVersionSuffixRegex.ReplaceAllString(modulePath, "") + case VCSUrlGoPkgInRegexWithUser.MatchString(modulePath): + return "https://" + VCSUrlGoPkgInRegexWithUser.ReplaceAllString(modulePath, "github.com/$1/$2") + case VCSUrlGoPkgInRegexWithoutUser.MatchString(modulePath): + return "https://" + VCSUrlGoPkgInRegexWithoutUser.ReplaceAllString(modulePath, "github.com/go-$1/$1") + } + + return "" +} + +// Parse parses a go.mod file +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + pkgs := make(map[string]ftypes.Package) + + goModData, err := io.ReadAll(r) + if err != nil { + return nil, nil, xerrors.Errorf("file read error: %w", err) + } + + modFileParsed, err := modfile.Parse("go.mod", goModData, nil) + if err != nil { + return nil, nil, xerrors.Errorf("go.mod parse error: %w", err) + } + + skipIndirect := true + if modFileParsed.Go != nil { // Old go.mod file may not include the go version. Go version for these files is less than 1.17 + skipIndirect = lessThan(modFileParsed.Go.Version, 1, 17) + } + + // Use minimal required go version from `toolchain` line (or from `go` line if `toolchain` is omitted) as `stdlib`. + // Show `stdlib` only with `useMinVersion` flag. + if p.useMinVersion { + if toolchainVer := toolchainVersion(modFileParsed.Toolchain, modFileParsed.Go); toolchainVer != "" { + pkgs["stdlib"] = ftypes.Package{ + ID: packageID("stdlib", toolchainVer), + Name: "stdlib", + // Our versioning library doesn't support canonical (goX.Y.Z) format, + // So we need to add `v` prefix for consistency (with module and dependency versions). + Version: fmt.Sprintf("v%s", toolchainVer), + Relationship: ftypes.RelationshipDirect, // Considered a direct dependency as the main module depends on the standard packages. + } + } + } + + // Required modules + for _, require := range modFileParsed.Require { + // Skip indirect dependencies less than Go 1.17 + if skipIndirect && require.Indirect { + continue + } + pkgs[require.Mod.Path] = ftypes.Package{ + ID: packageID(require.Mod.Path, require.Mod.Version), + Name: require.Mod.Path, + Version: require.Mod.Version, + Relationship: lo.Ternary(require.Indirect, ftypes.RelationshipIndirect, ftypes.RelationshipDirect), + ExternalReferences: p.GetExternalRefs(require.Mod.Path), + } + } + + // No need to evaluate the 'replace' directive for indirect dependencies + if p.replace { + for _, rep := range modFileParsed.Replace { + // Check if replaced path is actually in our pkgs. + old, ok := pkgs[rep.Old.Path] + if !ok { + continue + } + + // If the replace directive has a version on the left side, make sure it matches the version that was imported. + if rep.Old.Version != "" && old.Version != rep.Old.Version { + continue + } + + // Only support replace directive with version on the right side. + // Directive without version is a local path. + if rep.New.Version == "" { + // Delete old pkg, since it's a local path now. + delete(pkgs, rep.Old.Path) + continue + } + + // Delete old pkg, in case the path has changed. + delete(pkgs, rep.Old.Path) + + // Add replaced package to package register. + pkgs[rep.New.Path] = ftypes.Package{ + ID: packageID(rep.New.Path, rep.New.Version), + Name: rep.New.Path, + Version: rep.New.Version, + Relationship: old.Relationship, + ExternalReferences: p.GetExternalRefs(rep.New.Path), + } + } + } + + var deps ftypes.Dependencies + // Main module + if m := modFileParsed.Module; m != nil { + root := ftypes.Package{ + ID: packageID(m.Mod.Path, m.Mod.Version), + Name: m.Mod.Path, + Version: m.Mod.Version, + ExternalReferences: p.GetExternalRefs(m.Mod.Path), + Relationship: ftypes.RelationshipRoot, + } + + // Store child dependencies for the root package (main module). + // We will build a dependency graph for Direct/Indirect in `fanal` using additional files. + dependsOn := lo.FilterMap(lo.Values(pkgs), func(pkg ftypes.Package, _ int) (string, bool) { + return pkg.ID, pkg.Relationship == ftypes.RelationshipDirect + }) + + sort.Strings(dependsOn) + deps = append(deps, ftypes.Dependency{ + ID: root.ID, + DependsOn: dependsOn, + }) + + pkgs[root.Name] = root + } + + pkgSlice := lo.Values(pkgs) + sort.Sort(ftypes.Packages(pkgSlice)) + + return pkgSlice, deps, nil +} + +// lessThan checks if the Go version is less than `.` +func lessThan(ver string, majorVer, minorVer int) bool { + if ver == "" { + return false + } + + ss := strings.Split(ver, ".") + if len(ss) != 2 { + return false + } + major, err := strconv.Atoi(ss[0]) + if err != nil { + return false + } + minor, err := strconv.Atoi(ss[1]) + if err != nil { + return false + } + + return major <= majorVer && minor < minorVer +} + +// toolchainVersion returns version from `toolchain`. +// If `toolchain` is omitted - return version from `go` line (if it is version in toolchain format) +// cf. https://go.dev/doc/toolchain +func toolchainVersion(toolchain *modfile.Toolchain, goVer *modfile.Go) string { + if toolchain != nil && toolchain.Name != "" { + // cf. https://go.dev/doc/toolchain#name + // `dropping the initial go and discarding off any suffix beginning with -` + // e.g. `go1.22.5-custom` => `1.22.5` + name, _, _ := strings.Cut(toolchain.Name, "-") + return strings.TrimPrefix(name, "go") + } + + if goVer != nil { + return toolchainVersionFromGoLine(goVer.Version) + } + return "" +} + +// toolchainVersionFromGoLine detects Go version from `go` line if `toolchain` line is omitted. +// `go` line supports the following formats: +// cf. https://go.dev/doc/toolchain#version +// - `1.N.P`. e.g. `1.22.0` +// - `1.N`. e.g. `1.22` +// - `1.NrcR`. e.g. `1.22rc1` +// - `1.NbetaR`. e.g. `1.18beta1` - only for Go 1.20 or earlier +func toolchainVersionFromGoLine(ver string) string { + var majorMinorVer string + + if ss := strings.Split(ver, "."); len(ss) > 2 { // `1.N.P` + majorMinorVer = strings.Join(ss[:2], ".") + } else if v, _, rcFound := strings.Cut(ver, "rc"); rcFound { // `1.NrcR` + majorMinorVer = v + } else { // `1.N` + majorMinorVer = ver + // Add `.0` suffix to avoid user confusing. + // See https://github.com/aquasecurity/trivy/pull/7163#discussion_r1682424315 + ver = v + ".0" + } + + // `toolchain` has been added in go 1.21. + // So we need to check that Go version is 1.21 or higher. + // cf. https://github.com/aquasecurity/trivy/pull/7163#discussion_r1682424315 + if lessThan(majorMinorVer, 1, 21) { + return "" + } + return ver +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.GoModule, name, version) +} diff --git a/pkg/dependency/parser/golang/mod/parse_test.go b/pkg/dependency/parser/golang/mod/parse_test.go new file mode 100644 index 000000000000..33d6cfe3523d --- /dev/null +++ b/pkg/dependency/parser/golang/mod/parse_test.go @@ -0,0 +1,202 @@ +package mod + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/mod/modfile" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + file string + replace bool + useMinVersion bool + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + }{ + { + name: "normal with stdlib", + file: "testdata/normal/go.mod", + replace: true, + useMinVersion: true, + wantPkgs: GoModNormal, + wantDeps: GoModNormalDeps, + }, + { + name: "normal", + file: "testdata/normal/go.mod", + replace: true, + wantPkgs: GoModNormalWithoutStdlib, + wantDeps: GoModNormalWithoutStdlibDeps, + }, + { + name: "without go version", + file: "testdata/no-go-version/gomod", + replace: true, + wantPkgs: GoModNoGoVersion, + wantDeps: defaultGoDepParserDeps, + }, + { + name: "replace", + file: "testdata/replaced/go.mod", + replace: true, + wantPkgs: GoModReplaced, + wantDeps: GoModReplacedDeps, + }, + { + name: "no replace", + file: "testdata/replaced/go.mod", + replace: false, + wantPkgs: GoModUnreplaced, + wantDeps: GoModUnreplacedDeps, + }, + { + name: "replace with version", + file: "testdata/replaced-with-version/go.mod", + replace: true, + wantPkgs: GoModReplacedWithVersion, + wantDeps: GoModReplacedWithVersionDeps, + }, + { + name: "replaced with version mismatch", + file: "testdata/replaced-with-version-mismatch/go.mod", + replace: true, + wantPkgs: GoModReplacedWithVersionMismatch, + wantDeps: defaultGoDepParserDeps, + }, + { + name: "replaced with local path", + file: "testdata/replaced-with-local-path/go.mod", + replace: true, + wantPkgs: GoModReplacedWithLocalPath, + wantDeps: defaultGoDepParserDeps, + }, + { + name: "replaced with local path and version", + file: "testdata/replaced-with-local-path-and-version/go.mod", + replace: true, + wantPkgs: GoModReplacedWithLocalPathAndVersion, + wantDeps: defaultGoDepParserDeps, + }, + { + name: "replaced with local path and version, mismatch", + file: "testdata/replaced-with-local-path-and-version-mismatch/go.mod", + replace: true, + wantPkgs: GoModReplacedWithLocalPathAndVersionMismatch, + wantDeps: defaultGoDepParserDeps, + }, + { + name: "go 1.16", + file: "testdata/go116/go.mod", + replace: true, + wantPkgs: GoMod116, + wantDeps: defaultGoDepParserDeps, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + + gotPkgs, gotDeps, err := NewParser(tt.replace, tt.useMinVersion).Parse(t.Context(), f) + require.NoError(t, err) + + assert.Equal(t, tt.wantPkgs, gotPkgs) + assert.Equal(t, tt.wantDeps, gotDeps) + }) + } +} + +func TestToolchainVersion(t *testing.T) { + tests := []struct { + name string + modFile modfile.File + want string + }{ + { + name: "version from toolchain line", + modFile: modfile.File{ + Toolchain: &modfile.Toolchain{ + Name: "1.21.1", + }, + }, + want: "1.21.1", + }, + { + name: "version from toolchain line with suffix", + modFile: modfile.File{ + Toolchain: &modfile.Toolchain{ + Name: "1.21.1-custom", + }, + }, + want: "1.21.1", + }, + { + name: "'1.18rc1' from go line", + modFile: modfile.File{ + Go: &modfile.Go{ + Version: "1.18rc1", + }, + }, + want: "", + }, + { + name: "'1.18.1' from go line", + modFile: modfile.File{ + Go: &modfile.Go{ + Version: "1.18.1", + }, + }, + want: "", + }, + { + name: "'1.20' from go line", + modFile: modfile.File{ + Go: &modfile.Go{ + Version: "1.20", + }, + }, + want: "", + }, + { + name: "'1.21' from go line", + modFile: modfile.File{ + Go: &modfile.Go{ + Version: "1.21", + }, + }, + want: "1.21.0", + }, + { + name: "'1.21rc1' from go line", + modFile: modfile.File{ + Go: &modfile.Go{ + Version: "1.21rc1", + }, + }, + want: "1.21rc1", + }, + { + name: "'1.21.2' from go line", + modFile: modfile.File{ + Go: &modfile.Go{ + Version: "1.21.2", + }, + }, + want: "1.21.2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require.Equal(t, tt.want, toolchainVersion(tt.modFile.Toolchain, tt.modFile.Go)) + }) + } +} diff --git a/pkg/dependency/parser/golang/mod/parse_testcase.go b/pkg/dependency/parser/golang/mod/parse_testcase.go new file mode 100644 index 000000000000..70a163c50516 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/parse_testcase.go @@ -0,0 +1,463 @@ +package mod + +import ( + "slices" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + // execute go mod tidy in normal folder + GoModNormal = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-version@v0.0.0-20240603093900-cf8a8d29271d", + Name: "github.com/aquasecurity/go-version", + Version: "v0.0.0-20240603093900-cf8a8d29271d", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-version", + }, + }, + }, + { + ID: "stdlib@v1.22.5", + Name: "stdlib", + Version: "v1.22.5", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "github.com/davecgh/go-spew@v1.1.2-0.20180830191138-d8f796af33cc", + Name: "github.com/davecgh/go-spew", + Version: "v1.1.2-0.20180830191138-d8f796af33cc", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/davecgh/go-spew", + }, + }, + }, + { + ID: "github.com/pmezard/go-difflib@v1.0.1-0.20181226105442-5d4384ee4fb2", + Name: "github.com/pmezard/go-difflib", + Version: "v1.0.1-0.20181226105442-5d4384ee4fb2", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/pmezard/go-difflib", + }, + }, + }, + { + ID: "github.com/stretchr/testify@v1.9.0", + Name: "github.com/stretchr/testify", + Version: "v1.9.0", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/stretchr/testify", + }, + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20231012003039-104605ab7028", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20231012003039-104605ab7028", + Relationship: ftypes.RelationshipIndirect, + }, + } + + GoModNormalDeps = ftypes.Dependencies{ + { + ID: "github.com/org/repo", + DependsOn: []string{ + "github.com/aquasecurity/go-version@v0.0.0-20240603093900-cf8a8d29271d", + "stdlib@v1.22.5", + }, + }, + } + + GoModNormalWithoutStdlib = slices.DeleteFunc(slices.Clone(GoModNormal), func(f ftypes.Package) bool { + return f.Name == "stdlib" + }) + + GoModNormalWithoutStdlibDeps = ftypes.Dependencies{ + { + ID: "github.com/org/repo", + DependsOn: []string{ + "github.com/aquasecurity/go-version@v0.0.0-20240603093900-cf8a8d29271d", + }, + }, + } + + // execute go mod tidy in replaced folder + GoModReplaced = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20220406074731-71021a481237", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Relationship: ftypes.RelationshipIndirect, + }, + } + GoModReplacedDeps = ftypes.Dependencies{ + { + ID: "github.com/org/repo", + DependsOn: []string{ + "github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237", + }, + }, + } + + // execute go mod tidy in replaced folder + GoModUnreplaced = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20211110174639-8257534ffed3", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20211110174639-8257534ffed3", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Relationship: ftypes.RelationshipIndirect, + }, + } + + GoModUnreplacedDeps = ftypes.Dependencies{ + { + ID: "github.com/org/repo", + DependsOn: []string{ + "github.com/aquasecurity/go-dep-parser@v0.0.0-20211110174639-8257534ffed3", + }, + }, + } + + // execute go mod tidy in replaced-with-version folder + GoModReplacedWithVersion = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20220406074731-71021a481237", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Relationship: ftypes.RelationshipIndirect, + }, + } + + GoModReplacedWithVersionDeps = ftypes.Dependencies{ + { + ID: "github.com/org/repo", + DependsOn: []string{ + "github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237", + }, + }, + } + + // execute go mod tidy in replaced-with-version-mismatch folder + GoModReplacedWithVersionMismatch = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20211224170007-df43bca6b6ff", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20211224170007-df43bca6b6ff", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "gopkg.in/yaml.v3@v3.0.0-20210107192922-496545a6307b", + Name: "gopkg.in/yaml.v3", + Version: "v3.0.0-20210107192922-496545a6307b", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/go-yaml/yaml", + }, + }, + }, + } + + defaultGoDepParserDeps = ftypes.Dependencies{ + { + ID: "github.com/org/repo", + DependsOn: []string{ + "github.com/aquasecurity/go-dep-parser@v0.0.0-20211224170007-df43bca6b6ff", + }, + }, + } + + // execute go mod tidy in replaced-with-local-path folder + GoModReplacedWithLocalPath = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20211224170007-df43bca6b6ff", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20211224170007-df43bca6b6ff", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + { + ID: "gopkg.in/yaml.v3@v3.0.0-20210107192922-496545a6307b", + Name: "gopkg.in/yaml.v3", + Version: "v3.0.0-20210107192922-496545a6307b", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/go-yaml/yaml", + }, + }, + }, + } + + // execute go mod tidy in replaced-with-local-path-and-version folder + GoModReplacedWithLocalPathAndVersion = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20211224170007-df43bca6b6ff", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20211224170007-df43bca6b6ff", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + { + ID: "gopkg.in/yaml.v3@v3.0.0-20210107192922-496545a6307b", + Name: "gopkg.in/yaml.v3", + Version: "v3.0.0-20210107192922-496545a6307b", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/go-yaml/yaml", + }, + }, + }, + } + + // execute go mod tidy in replaced-with-local-path-and-version-mismatch folder + GoModReplacedWithLocalPathAndVersionMismatch = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20211224170007-df43bca6b6ff", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20211224170007-df43bca6b6ff", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "gopkg.in/yaml.v3@v3.0.0-20210107192922-496545a6307b", + Name: "gopkg.in/yaml.v3", + Version: "v3.0.0-20210107192922-496545a6307b", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/go-yaml/yaml", + }, + }, + }, + } + + // execute go mod tidy in go116 folder + GoMod116 = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20211224170007-df43bca6b6ff", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20211224170007-df43bca6b6ff", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + } + + // execute go mod tidy in no-go-version folder + GoModNoGoVersion = []ftypes.Package{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: ftypes.RelationshipRoot, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20211224170007-df43bca6b6ff", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20211224170007-df43bca6b6ff", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + } +) diff --git a/pkg/dependency/parser/golang/mod/testdata/go116/go.mod b/pkg/dependency/parser/golang/mod/testdata/go116/go.mod new file mode 100644 index 000000000000..d86af50288e8 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/go116/go.mod @@ -0,0 +1,7 @@ +module github.com/org/repo + +go 1.16 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff + +require gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/pkg/dependency/parser/golang/mod/testdata/go116/go.sum b/pkg/dependency/parser/golang/mod/testdata/go116/go.sum new file mode 100644 index 000000000000..032bb4727cfe --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/go116/go.sum @@ -0,0 +1,62 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff h1:JCKEV3TgUNh9fn+8hXyIdsF9yErA0rUbCkgt2flRKt4= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff/go.mod h1:8fJ//Ob6/03lxbn4xa1F+G/giVtiVLxnZNpBp5xOxNk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/pkg/dependency/parser/golang/mod/testdata/go116/main.go b/pkg/dependency/parser/golang/mod/testdata/go116/main.go new file mode 100644 index 000000000000..0b51fb861f32 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/go116/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/mod" +) + +func main() { + if _, err := mod.Parse(nil); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/dependency/parser/golang/mod/testdata/no-go-version/gomod b/pkg/dependency/parser/golang/mod/testdata/no-go-version/gomod new file mode 100644 index 000000000000..a95a8f7eba91 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/no-go-version/gomod @@ -0,0 +1,3 @@ +module github.com/org/repo + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff \ No newline at end of file diff --git a/pkg/dependency/parser/golang/mod/testdata/no-go-version/main.go b/pkg/dependency/parser/golang/mod/testdata/no-go-version/main.go new file mode 100644 index 000000000000..0b51fb861f32 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/no-go-version/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/mod" +) + +func main() { + if _, err := mod.Parse(nil); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/dependency/parser/golang/mod/testdata/normal/go.mod b/pkg/dependency/parser/golang/mod/testdata/normal/go.mod new file mode 100644 index 000000000000..564414c10583 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/normal/go.mod @@ -0,0 +1,14 @@ +module github.com/org/repo + +go 1.22.0 + +toolchain go1.22.5 + +require github.com/aquasecurity/go-version v0.0.0-20240603093900-cf8a8d29271d + +require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/stretchr/testify v1.9.0 // indirect + golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect +) diff --git a/pkg/dependency/parser/golang/mod/testdata/normal/go.sum b/pkg/dependency/parser/golang/mod/testdata/normal/go.sum new file mode 100644 index 000000000000..eefc0690f47d --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/normal/go.sum @@ -0,0 +1,12 @@ +github.com/aquasecurity/go-version v0.0.0-20240603093900-cf8a8d29271d h1:4zour5Sh9chOg+IqIinIcJ3qtr3cIf8FdFY6aArlXBw= +github.com/aquasecurity/go-version v0.0.0-20240603093900-cf8a8d29271d/go.mod h1:1cPOp4BaQZ1G2F5fnw4dFz6pkOyXJI9KTuak8ghIl3U= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/dependency/parser/golang/mod/testdata/normal/main.go b/pkg/dependency/parser/golang/mod/testdata/normal/main.go new file mode 100644 index 000000000000..fe31e68d5da8 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/normal/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/aquasecurity/go-version/pkg/version" +) + +func main() { + if _, err := version.Parse("v0.1.2"); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/go.mod b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/go.mod new file mode 100644 index 000000000000..7df6b938efc9 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/go.mod @@ -0,0 +1,12 @@ +module github.com/org/repo + +go 1.17 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff + +require ( + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) + +replace golang.org/x/xerrors v0.0.1 => ./xerrors diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/go.sum b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/go.sum new file mode 100644 index 000000000000..8a219a39d474 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/go.sum @@ -0,0 +1,61 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff h1:JCKEV3TgUNh9fn+8hXyIdsF9yErA0rUbCkgt2flRKt4= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff/go.mod h1:8fJ//Ob6/03lxbn4xa1F+G/giVtiVLxnZNpBp5xOxNk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/main.go b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/main.go new file mode 100644 index 000000000000..0b51fb861f32 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/mod" +) + +func main() { + if _, err := mod.Parse(nil); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/xerrors/go.mod b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/xerrors/go.mod new file mode 100644 index 000000000000..a795158073f9 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/xerrors/go.mod @@ -0,0 +1,3 @@ +module golang.org/x/xerrors + +go 1.12 diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/xerrors/xerrors.go b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/xerrors/xerrors.go new file mode 100644 index 000000000000..715777453212 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version-mismatch/xerrors/xerrors.go @@ -0,0 +1,5 @@ +package xerrors + +func Errorf(format string, a ...any) error { + return nil +} diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/go.mod b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/go.mod new file mode 100644 index 000000000000..49ac483441af --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/go.mod @@ -0,0 +1,12 @@ +module github.com/org/repo + +go 1.17 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff + +require ( + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) + +replace golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 => ./xerrors diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/go.sum b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/go.sum new file mode 100644 index 000000000000..3d1d7c0e3913 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/go.sum @@ -0,0 +1,60 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff h1:JCKEV3TgUNh9fn+8hXyIdsF9yErA0rUbCkgt2flRKt4= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff/go.mod h1:8fJ//Ob6/03lxbn4xa1F+G/giVtiVLxnZNpBp5xOxNk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/main.go b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/main.go new file mode 100644 index 000000000000..0b51fb861f32 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/mod" +) + +func main() { + if _, err := mod.Parse(nil); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/xerrors/go.mod b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/xerrors/go.mod new file mode 100644 index 000000000000..a795158073f9 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/xerrors/go.mod @@ -0,0 +1,3 @@ +module golang.org/x/xerrors + +go 1.12 diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/xerrors/xerrors.go b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/xerrors/xerrors.go new file mode 100644 index 000000000000..715777453212 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path-and-version/xerrors/xerrors.go @@ -0,0 +1,5 @@ +package xerrors + +func Errorf(format string, a ...any) error { + return nil +} diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/go.mod b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/go.mod new file mode 100644 index 000000000000..50f5c43a18dc --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/go.mod @@ -0,0 +1,12 @@ +module github.com/org/repo + +go 1.17 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff + +require ( + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) + +replace golang.org/x/xerrors => ./xerrors diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/go.sum b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/go.sum new file mode 100644 index 000000000000..444e67c69532 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/go.sum @@ -0,0 +1,59 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff h1:JCKEV3TgUNh9fn+8hXyIdsF9yErA0rUbCkgt2flRKt4= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff/go.mod h1:8fJ//Ob6/03lxbn4xa1F+G/giVtiVLxnZNpBp5xOxNk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/main.go b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/main.go new file mode 100644 index 000000000000..0b51fb861f32 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/mod" +) + +func main() { + if _, err := mod.Parse(nil); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/xerrors/go.mod b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/xerrors/go.mod new file mode 100644 index 000000000000..a795158073f9 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/xerrors/go.mod @@ -0,0 +1,3 @@ +module golang.org/x/xerrors + +go 1.12 diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/xerrors/xerrors.go b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/xerrors/xerrors.go new file mode 100644 index 000000000000..715777453212 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-local-path/xerrors/xerrors.go @@ -0,0 +1,5 @@ +package xerrors + +func Errorf(format string, a ...any) error { + return nil +} diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-version-mismatch/go.mod b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version-mismatch/go.mod new file mode 100644 index 000000000000..0f2f71faba63 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version-mismatch/go.mod @@ -0,0 +1,12 @@ +module github.com/org/repo + +go 1.17 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff + +require ( + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) + +replace github.com/aquasecurity/go-dep-parser v0.0.1 => github.com/aquasecurity/go-dep-parser v0.0.0-20220406074731-71021a481237 diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-version-mismatch/go.sum b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version-mismatch/go.sum new file mode 100644 index 000000000000..032bb4727cfe --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version-mismatch/go.sum @@ -0,0 +1,62 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff h1:JCKEV3TgUNh9fn+8hXyIdsF9yErA0rUbCkgt2flRKt4= +github.com/aquasecurity/go-dep-parser v0.0.0-20211224170007-df43bca6b6ff/go.mod h1:8fJ//Ob6/03lxbn4xa1F+G/giVtiVLxnZNpBp5xOxNk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-version-mismatch/main.go b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version-mismatch/main.go new file mode 100644 index 000000000000..0b51fb861f32 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version-mismatch/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/mod" +) + +func main() { + if _, err := mod.Parse(nil); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-version/go.mod b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version/go.mod new file mode 100644 index 000000000000..e02dc649e6dc --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version/go.mod @@ -0,0 +1,9 @@ +module github.com/org/repo + +go 1.17 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211110174639-8257534ffed3 + +require golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + +replace github.com/aquasecurity/go-dep-parser v0.0.0-20211110174639-8257534ffed3 => github.com/aquasecurity/go-dep-parser v0.0.0-20220406074731-71021a481237 diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-version/go.sum b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version/go.sum new file mode 100644 index 000000000000..b75b2debc906 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version/go.sum @@ -0,0 +1,61 @@ +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/aquasecurity/go-dep-parser v0.0.0-20220406074731-71021a481237 h1:FX5MaNimz5xK6LYbp+mI23i2m6OmoKaHAEgRVehLDs8= +github.com/aquasecurity/go-dep-parser v0.0.0-20220406074731-71021a481237/go.mod h1:MewgJXyrz9PgCHh8zunRNY4BY72ltNYWeTYAt1paaLc= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced-with-version/main.go b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version/main.go new file mode 100644 index 000000000000..0b51fb861f32 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced-with-version/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/mod" +) + +func main() { + if _, err := mod.Parse(nil); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced/go.mod b/pkg/dependency/parser/golang/mod/testdata/replaced/go.mod new file mode 100644 index 000000000000..cc7e47974be2 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced/go.mod @@ -0,0 +1,9 @@ +module github.com/org/repo + +go 1.17 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211110174639-8257534ffed3 + +require golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + +replace github.com/aquasecurity/go-dep-parser => github.com/aquasecurity/go-dep-parser v0.0.0-20220406074731-71021a481237 diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced/go.sum b/pkg/dependency/parser/golang/mod/testdata/replaced/go.sum new file mode 100644 index 000000000000..b75b2debc906 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced/go.sum @@ -0,0 +1,61 @@ +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/aquasecurity/go-dep-parser v0.0.0-20220406074731-71021a481237 h1:FX5MaNimz5xK6LYbp+mI23i2m6OmoKaHAEgRVehLDs8= +github.com/aquasecurity/go-dep-parser v0.0.0-20220406074731-71021a481237/go.mod h1:MewgJXyrz9PgCHh8zunRNY4BY72ltNYWeTYAt1paaLc= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/dependency/parser/golang/mod/testdata/replaced/main.go b/pkg/dependency/parser/golang/mod/testdata/replaced/main.go new file mode 100644 index 000000000000..0b51fb861f32 --- /dev/null +++ b/pkg/dependency/parser/golang/mod/testdata/replaced/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "log" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/mod" +) + +func main() { + if _, err := mod.Parse(nil); err != nil { + log.Fatal(err) + } +} diff --git a/pkg/dependency/parser/golang/sum/parse.go b/pkg/dependency/parser/golang/sum/parse.go new file mode 100644 index 000000000000..30718f38db8d --- /dev/null +++ b/pkg/dependency/parser/golang/sum/parse.go @@ -0,0 +1,51 @@ +package sum + +import ( + "bufio" + "context" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +// Parse parses a go.sum file +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var pkgs []ftypes.Package + uniquePkgs := make(map[string]string) + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + s := strings.Fields(line) + if len(s) < 2 { + continue + } + + // go.sum records and sorts all non-major versions + // with the latest version as last entry + uniquePkgs[s[0]] = strings.TrimSuffix(s[1], "/go.mod") + } + if err := scanner.Err(); err != nil { + return nil, nil, xerrors.Errorf("scan error: %w", err) + } + + for k, v := range uniquePkgs { + pkgs = append(pkgs, ftypes.Package{ + ID: dependency.ID(ftypes.GoModule, k, v), + Name: k, + Version: v, + }) + } + + return pkgs, nil, nil +} diff --git a/pkg/dependency/parser/golang/sum/parse_test.go b/pkg/dependency/parser/golang/sum/parse_test.go new file mode 100644 index 000000000000..75aac1df236b --- /dev/null +++ b/pkg/dependency/parser/golang/sum/parse_test.go @@ -0,0 +1,57 @@ +package sum + +import ( + "os" + "path" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + vectors := []struct { + file string + want []ftypes.Package + }{ + { + file: "testdata/gomod_normal.sum", + want: GoModNormal, + }, + { + file: "testdata/gomod_emptyline.sum", + want: GoModEmptyLine, + }, + { + file: "testdata/gomod_many.sum", + want: GoModMany, + }, + { + file: "testdata/gomod_trivy.sum", + want: GoModTrivy, + }, + } + + for _, v := range vectors { + t.Run(path.Base(v.file), func(t *testing.T) { + f, err := os.Open(v.file) + require.NoError(t, err) + defer f.Close() + + got, _, err := NewParser().Parse(t.Context(), f) + require.NoError(t, err) + + for i := range got { + got[i].ID = "" // Not compare IDs, tested in mod.TestModuleID() + } + + sort.Sort(ftypes.Packages(got)) + sort.Sort(ftypes.Packages(v.want)) + + assert.Equal(t, v.want, got) + }) + } +} diff --git a/pkg/dependency/parser/golang/sum/parse_testcase.go b/pkg/dependency/parser/golang/sum/parse_testcase.go new file mode 100644 index 000000000000..532a241b1ccc --- /dev/null +++ b/pkg/dependency/parser/golang/sum/parse_testcase.go @@ -0,0 +1,394 @@ +package sum + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +var ( + // docker run --name gomod --rm -it golang:1.15 bash + // export USER=gomod + // mkdir repo + // cd repo + // go mod init github.com/org/repo + // go get golang.org/x/xerrors + // go list -m all | awk 'NR>1 {sub(/^v/, "", $2); printf("{\""$1"\", \""$2"\", },\n")}' + GoModNormal = []ftypes.Package{ + {Name: "golang.org/x/xerrors", Version: "v0.0.0-20200804184101-5ec99f83aff1"}, + } + + // https://github.com/uudashr/gopkgs/blob/616744904701ef01d868da4b66aad0e6856c361d/v2/go.sum + GoModEmptyLine = []ftypes.Package{ + {Name: "github.com/karrick/godirwalk", Version: "v1.12.0"}, + {Name: "github.com/pkg/errors", Version: "v0.8.1"}, + } + + // docker run --name gomod --rm -it golang:1.15 bash + // export USER=gomod + // mkdir repo + // cd repo + // go mod init github.com/org/repo + // go get golang.org/x/xerrors + // go get github.com/urfave/cli + // go get github.com/stretchr/testify + // go get github.com/BurntSushi/toml + // go list -m all | awk 'NR>1 {sub(/^v/, "", $2); printf("{\""$1"\", \""$2"\", },\n")}' + GoModMany = []ftypes.Package{ + {Name: "github.com/BurntSushi/toml", Version: "v0.3.1"}, + {Name: "github.com/cpuguy83/go-md2man/v2", Version: "v2.0.0-20190314233015-f79a8a8ca69d"}, + {Name: "github.com/davecgh/go-spew", Version: "v1.1.0"}, + {Name: "github.com/pmezard/go-difflib", Version: "v1.0.0"}, + {Name: "github.com/russross/blackfriday/v2", Version: "v2.0.1"}, + {Name: "github.com/shurcooL/sanitized_anchor_name", Version: "v1.0.0"}, + {Name: "github.com/stretchr/objx", Version: "v0.1.0"}, + {Name: "github.com/stretchr/testify", Version: "v1.7.0"}, + {Name: "github.com/urfave/cli", Version: "v1.22.5"}, + {Name: "golang.org/x/xerrors", Version: "v0.0.0-20200804184101-5ec99f83aff1"}, + {Name: "gopkg.in/check.v1", Version: "v0.0.0-20161208181325-20d25e280405"}, + {Name: "gopkg.in/yaml.v2", Version: "v2.2.2"}, + {Name: "gopkg.in/yaml.v3", Version: "v3.0.0-20200313102051-9f266ea9e77c"}, + } + + // docker run --name gomod --rm -it golang:1.15 bash + // export USER=gomod + // mkdir repo + // cd repo + // go mod init github.com/org/repo + // go get github.com/aquasecurity/trivy + // go list -m all | awk 'NR>1 {sub(/^v/, "", $2); printf("{\""$1"\", \""$2"\", },\n")}' + GoModTrivy = []ftypes.Package{ + {Name: "cloud.google.com/go", Version: "v0.65.0"}, + {Name: "cloud.google.com/go/bigquery", Version: "v1.8.0"}, + {Name: "cloud.google.com/go/datastore", Version: "v1.1.0"}, + {Name: "cloud.google.com/go/pubsub", Version: "v1.3.1"}, + {Name: "cloud.google.com/go/storage", Version: "v1.10.0"}, + {Name: "dmitri.shuralyov.com/gpu/mtl", Version: "v0.0.0-20190408044501-666a987793e9"}, + {Name: "github.com/Azure/azure-sdk-for-go", Version: "v38.0.0+incompatible"}, + {Name: "github.com/Azure/go-ansiterm", Version: "v0.0.0-20170929234023-d6e3b3328b78"}, + {Name: "github.com/Azure/go-autorest/autorest", Version: "v0.9.3"}, + {Name: "github.com/Azure/go-autorest/autorest/adal", Version: "v0.8.1"}, + {Name: "github.com/Azure/go-autorest/autorest/date", Version: "v0.2.0"}, + {Name: "github.com/Azure/go-autorest/autorest/mocks", Version: "v0.3.0"}, + {Name: "github.com/Azure/go-autorest/autorest/to", Version: "v0.3.0"}, + {Name: "github.com/Azure/go-autorest/autorest/validation", Version: "v0.1.0"}, + {Name: "github.com/Azure/go-autorest/logger", Version: "v0.1.0"}, + {Name: "github.com/Azure/go-autorest/tracing", Version: "v0.5.0"}, + {Name: "github.com/BurntSushi/toml", Version: "v0.3.1"}, + {Name: "github.com/BurntSushi/xgb", Version: "v0.0.0-20160522181843-27f122750802"}, + {Name: "github.com/GoogleCloudPlatform/docker-credential-gcr", Version: "v1.5.0"}, + {Name: "github.com/GoogleCloudPlatform/k8s-cloud-provider", Version: "v0.0.0-20190822182118-27a4ced34534"}, + {Name: "github.com/Microsoft/go-winio", Version: "v0.4.15-0.20190919025122-fc70bd9a86b5"}, + {Name: "github.com/Microsoft/hcsshim", Version: "v0.8.6"}, + {Name: "github.com/NYTimes/gziphandler", Version: "v0.0.0-20170623195520-56545f4a5d46"}, + {Name: "github.com/OneOfOne/xxhash", Version: "v1.2.7"}, + {Name: "github.com/PuerkitoBio/purell", Version: "v1.1.1"}, + {Name: "github.com/PuerkitoBio/urlesc", Version: "v0.0.0-20170810143723-de5bf2ad4578"}, + {Name: "github.com/VividCortex/ewma", Version: "v1.1.1"}, + {Name: "github.com/alcortesm/tgz", Version: "v0.0.0-20161220082320-9c5fe88206d7"}, + {Name: "github.com/alecthomas/template", Version: "v0.0.0-20160405071501-a0175ee3bccc"}, + {Name: "github.com/alecthomas/units", Version: "v0.0.0-20151022065526-2efee857e7cf"}, + {Name: "github.com/alicebob/gopher-json", Version: "v0.0.0-20200520072559-a9ecdc9d1d3a"}, + {Name: "github.com/alicebob/miniredis/v2", Version: "v2.14.1"}, + {Name: "github.com/anmitsu/go-shlex", Version: "v0.0.0-20161002113705-648efa622239"}, + {Name: "github.com/aquasecurity/bolt-fixtures", Version: "v0.0.0-20200903104109-d34e7f983986"}, + {Name: "github.com/aquasecurity/fanal", Version: "v0.0.0-20210119051230-28c249da7cfd"}, + {Name: "github.com/aquasecurity/go-dep-parser", Version: "v0.0.0-20201028043324-889d4a92b8e0"}, + {Name: "github.com/aquasecurity/go-gem-version", Version: "v0.0.0-20201115065557-8eed6fe000ce"}, + {Name: "github.com/aquasecurity/go-npm-version", Version: "v0.0.0-20201110091526-0b796d180798"}, + {Name: "github.com/aquasecurity/go-pep440-version", Version: "v0.0.0-20210121094942-22b2f8951d46"}, + {Name: "github.com/aquasecurity/go-version", Version: "v0.0.0-20210121072130-637058cfe492"}, + {Name: "github.com/aquasecurity/testdocker", Version: "v0.0.0-20210106133225-0b17fe083674"}, + {Name: "github.com/aquasecurity/trivy", Version: "v0.16.0"}, + {Name: "github.com/aquasecurity/trivy-db", Version: "v0.0.0-20210105160501-c5bf4e153277"}, + {Name: "github.com/aquasecurity/vuln-list-update", Version: "v0.0.0-20191016075347-3d158c2bf9a2"}, + {Name: "github.com/araddon/dateparse", Version: "v0.0.0-20190426192744-0d74ffceef83"}, + {Name: "github.com/armon/consul-api", Version: "v0.0.0-20180202201655-eb2c6b5be1b6"}, + {Name: "github.com/armon/go-socks5", Version: "v0.0.0-20160902184237-e75332964ef5"}, + {Name: "github.com/aws/aws-sdk-go", Version: "v1.27.1"}, + {Name: "github.com/beorn7/perks", Version: "v1.0.0"}, + {Name: "github.com/bgentry/speakeasy", Version: "v0.1.0"}, + {Name: "github.com/blang/semver", Version: "v3.5.0+incompatible"}, + {Name: "github.com/briandowns/spinner", Version: "v1.12.0"}, + {Name: "github.com/caarlos0/env/v6", Version: "v6.0.0"}, + {Name: "github.com/cenkalti/backoff", Version: "v2.2.1+incompatible"}, + {Name: "github.com/census-instrumentation/opencensus-proto", Version: "v0.2.1"}, + {Name: "github.com/cespare/xxhash/v2", Version: "v2.1.1"}, + {Name: "github.com/cheggaaa/pb/v3", Version: "v3.0.3"}, + {Name: "github.com/chzyer/logex", Version: "v1.1.10"}, + {Name: "github.com/chzyer/readline", Version: "v0.0.0-20180603132655-2972be24d48e"}, + {Name: "github.com/chzyer/test", Version: "v0.0.0-20180213035817-a1ea475d72b1"}, + {Name: "github.com/client9/misspell", Version: "v0.3.4"}, + {Name: "github.com/cncf/udpa/go", Version: "v0.0.0-20191209042840-269d4d468f6f"}, + {Name: "github.com/cockroachdb/datadriven", Version: "v0.0.0-20190809214429-80d97fb3cbaa"}, + {Name: "github.com/containerd/containerd", Version: "v1.3.3"}, + {Name: "github.com/containerd/continuity", Version: "v0.0.0-20190426062206-aaeac12a7ffc"}, + {Name: "github.com/coreos/etcd", Version: "v3.3.10+incompatible"}, + {Name: "github.com/coreos/go-etcd", Version: "v2.0.0+incompatible"}, + {Name: "github.com/coreos/go-oidc", Version: "v2.1.0+incompatible"}, + {Name: "github.com/coreos/go-semver", Version: "v0.3.0"}, + {Name: "github.com/coreos/go-systemd", Version: "v0.0.0-20190321100706-95778dfbb74e"}, + {Name: "github.com/coreos/pkg", Version: "v0.0.0-20180108230652-97fdf19511ea"}, + {Name: "github.com/cpuguy83/go-md2man", Version: "v1.0.10"}, + {Name: "github.com/cpuguy83/go-md2man/v2", Version: "v2.0.0"}, + {Name: "github.com/creack/pty", Version: "v1.1.9"}, + {Name: "github.com/davecgh/go-spew", Version: "v1.1.1"}, + {Name: "github.com/deckarep/golang-set", Version: "v1.7.1"}, + {Name: "github.com/dgrijalva/jwt-go", Version: "v3.2.0+incompatible"}, + {Name: "github.com/dgryski/go-rendezvous", Version: "v0.0.0-20200823014737-9f7001d12a5f"}, + {Name: "github.com/dnaeon/go-vcr", Version: "v1.0.1"}, + {Name: "github.com/docker/cli", Version: "v0.0.0-20191017083524-a8ff7f821017"}, + {Name: "github.com/docker/distribution", Version: "v2.7.1+incompatible"}, + {Name: "github.com/docker/docker", Version: "v1.4.2-0.20190924003213-a8608b5b67c7"}, + {Name: "github.com/docker/docker-credential-helpers", Version: "v0.6.3"}, + {Name: "github.com/docker/go-connections", Version: "v0.4.0"}, + {Name: "github.com/docker/go-units", Version: "v0.4.0"}, + {Name: "github.com/docker/spdystream", Version: "v0.0.0-20160310174837-449fdfce4d96"}, + {Name: "github.com/dustin/go-humanize", Version: "v1.0.0"}, + {Name: "github.com/elazarl/goproxy", Version: "v0.0.0-20200809112317-0581fc3aee2d"}, + {Name: "github.com/elazarl/goproxy/ext", Version: "v0.0.0-20200809112317-0581fc3aee2d"}, + {Name: "github.com/emicklei/go-restful", Version: "v2.9.5+incompatible"}, + {Name: "github.com/emirpasic/gods", Version: "v1.12.0"}, + {Name: "github.com/envoyproxy/go-control-plane", Version: "v0.9.4"}, + {Name: "github.com/envoyproxy/protoc-gen-validate", Version: "v0.1.0"}, + {Name: "github.com/evanphx/json-patch", Version: "v4.2.0+incompatible"}, + {Name: "github.com/fatih/color", Version: "v1.10.0"}, + {Name: "github.com/flynn/go-shlex", Version: "v0.0.0-20150515145356-3f9db97f8568"}, + {Name: "github.com/fsnotify/fsnotify", Version: "v1.4.9"}, + {Name: "github.com/ghodss/yaml", Version: "v1.0.0"}, + {Name: "github.com/gin-contrib/sse", Version: "v0.1.0"}, + {Name: "github.com/gin-gonic/gin", Version: "v1.5.0"}, + {Name: "github.com/gliderlabs/ssh", Version: "v0.2.2"}, + {Name: "github.com/go-git/gcfg", Version: "v1.5.0"}, + {Name: "github.com/go-git/go-billy/v5", Version: "v5.0.0"}, + {Name: "github.com/go-git/go-git-fixtures/v4", Version: "v4.0.1"}, + {Name: "github.com/go-git/go-git/v5", Version: "v5.0.0"}, + {Name: "github.com/go-gl/glfw", Version: "v0.0.0-20190409004039-e6da0acd62b1"}, + {Name: "github.com/go-gl/glfw/v3.3/glfw", Version: "v0.0.0-20200222043503-6f7a984d4dc4"}, + {Name: "github.com/go-kit/kit", Version: "v0.8.0"}, + {Name: "github.com/go-logfmt/logfmt", Version: "v0.3.0"}, + {Name: "github.com/go-logr/logr", Version: "v0.1.0"}, + {Name: "github.com/go-openapi/jsonpointer", Version: "v0.19.3"}, + {Name: "github.com/go-openapi/jsonreference", Version: "v0.19.3"}, + {Name: "github.com/go-openapi/spec", Version: "v0.19.3"}, + {Name: "github.com/go-openapi/swag", Version: "v0.19.5"}, + {Name: "github.com/go-playground/locales", Version: "v0.13.0"}, + {Name: "github.com/go-playground/universal-translator", Version: "v0.17.0"}, + {Name: "github.com/go-redis/redis", Version: "v6.15.7+incompatible"}, + {Name: "github.com/go-redis/redis/v8", Version: "v8.4.0"}, + {Name: "github.com/go-restruct/restruct", Version: "v0.0.0-20191227155143-5734170a48a1"}, + {Name: "github.com/go-sql-driver/mysql", Version: "v1.5.0"}, + {Name: "github.com/go-stack/stack", Version: "v1.8.0"}, + {Name: "github.com/gobwas/glob", Version: "v0.2.3"}, + {Name: "github.com/goccy/go-yaml", Version: "v1.8.2"}, + {Name: "github.com/gogo/protobuf", Version: "v1.3.1"}, + {Name: "github.com/golang/glog", Version: "v0.0.0-20160126235308-23def4e6c14b"}, + {Name: "github.com/golang/groupcache", Version: "v0.0.0-20200121045136-8c9f03a8e57e"}, + {Name: "github.com/golang/mock", Version: "v1.4.4"}, + {Name: "github.com/golang/protobuf", Version: "v1.4.2"}, + {Name: "github.com/google/btree", Version: "v1.0.0"}, + {Name: "github.com/google/go-cmp", Version: "v0.5.3"}, + {Name: "github.com/google/go-containerregistry", Version: "v0.0.0-20200331213917-3d03ed9b1ca2"}, + {Name: "github.com/google/go-github/v28", Version: "v28.1.1"}, + {Name: "github.com/google/go-querystring", Version: "v1.0.0"}, + {Name: "github.com/google/gofuzz", Version: "v1.0.0"}, + {Name: "github.com/google/martian", Version: "v2.1.0+incompatible"}, + {Name: "github.com/google/martian/v3", Version: "v3.0.0"}, + {Name: "github.com/google/pprof", Version: "v0.0.0-20200708004538-1a94d8640e99"}, + {Name: "github.com/google/renameio", Version: "v0.1.0"}, + {Name: "github.com/google/subcommands", Version: "v1.0.1"}, + {Name: "github.com/google/uuid", Version: "v1.1.1"}, + {Name: "github.com/google/wire", Version: "v0.3.0"}, + {Name: "github.com/googleapis/gax-go/v2", Version: "v2.0.5"}, + {Name: "github.com/googleapis/gnostic", Version: "v0.2.2"}, + {Name: "github.com/gophercloud/gophercloud", Version: "v0.1.0"}, + {Name: "github.com/gopherjs/gopherjs", Version: "v0.0.0-20200217142428-fce0ec30dd00"}, + {Name: "github.com/gorilla/context", Version: "v1.1.1"}, + {Name: "github.com/gorilla/mux", Version: "v1.7.4"}, + {Name: "github.com/gorilla/websocket", Version: "v1.4.0"}, + {Name: "github.com/gregjones/httpcache", Version: "v0.0.0-20180305231024-9cad4c3443a7"}, + {Name: "github.com/grpc-ecosystem/go-grpc-middleware", Version: "v1.0.1-0.20190118093823-f849b5445de4"}, + {Name: "github.com/grpc-ecosystem/go-grpc-prometheus", Version: "v1.2.0"}, + {Name: "github.com/grpc-ecosystem/grpc-gateway", Version: "v1.9.5"}, + {Name: "github.com/hashicorp/errwrap", Version: "v1.0.0"}, + {Name: "github.com/hashicorp/go-multierror", Version: "v1.1.0"}, + {Name: "github.com/hashicorp/go-version", Version: "v1.2.1"}, + {Name: "github.com/hashicorp/golang-lru", Version: "v0.5.3"}, + {Name: "github.com/hashicorp/hcl", Version: "v1.0.0"}, + {Name: "github.com/hpcloud/tail", Version: "v1.0.0"}, + {Name: "github.com/ianlancetaylor/demangle", Version: "v0.0.0-20181102032728-5e5cf60278f6"}, + {Name: "github.com/imdario/mergo", Version: "v0.3.5"}, + {Name: "github.com/inconshreveable/mousetrap", Version: "v1.0.0"}, + {Name: "github.com/jbenet/go-context", Version: "v0.0.0-20150711004518-d14ea06fba99"}, + {Name: "github.com/jessevdk/go-flags", Version: "v1.4.0"}, + {Name: "github.com/jmespath/go-jmespath", Version: "v0.0.0-20180206201540-c2b33e8439af"}, + {Name: "github.com/joefitzgerald/rainbow-reporter", Version: "v0.1.0"}, + {Name: "github.com/jonboulle/clockwork", Version: "v0.1.0"}, + {Name: "github.com/json-iterator/go", Version: "v1.1.8"}, + {Name: "github.com/jstemmer/go-junit-report", Version: "v0.9.1"}, + {Name: "github.com/jtolds/gls", Version: "v4.20.0+incompatible"}, + {Name: "github.com/julienschmidt/httprouter", Version: "v1.2.0"}, + {Name: "github.com/kevinburke/ssh_config", Version: "v0.0.0-20190725054713-01f96b0aa0cd"}, + {Name: "github.com/kisielk/errcheck", Version: "v1.2.0"}, + {Name: "github.com/kisielk/gotool", Version: "v1.0.0"}, + {Name: "github.com/knqyf263/go-apk-version", Version: "v0.0.0-20200609155635-041fdbb8563f"}, + {Name: "github.com/knqyf263/go-deb-version", Version: "v0.0.0-20190517075300-09fca494f03d"}, + {Name: "github.com/knqyf263/go-rpm-version", Version: "v0.0.0-20170716094938-74609b86c936"}, + {Name: "github.com/knqyf263/go-rpmdb", Version: "v0.0.0-20201215100354-a9e3110d8ee1"}, + {Name: "github.com/knqyf263/nested", Version: "v0.0.1"}, + {Name: "github.com/konsorten/go-windows-terminal-sequences", Version: "v1.0.2"}, + {Name: "github.com/kr/logfmt", Version: "v0.0.0-20140226030751-b84e30acd515"}, + {Name: "github.com/kr/pretty", Version: "v0.1.0"}, + {Name: "github.com/kr/pty", Version: "v1.1.5"}, + {Name: "github.com/kr/text", Version: "v0.2.0"}, + {Name: "github.com/kylelemons/godebug", Version: "v1.1.0"}, + {Name: "github.com/leodido/go-urn", Version: "v1.2.0"}, + {Name: "github.com/magiconair/properties", Version: "v1.8.0"}, + {Name: "github.com/mailru/easyjson", Version: "v0.7.0"}, + {Name: "github.com/mattn/go-colorable", Version: "v0.1.8"}, + {Name: "github.com/mattn/go-isatty", Version: "v0.0.12"}, + {Name: "github.com/mattn/go-jsonpointer", Version: "v0.0.0-20180225143300-37667080efed"}, + {Name: "github.com/mattn/go-runewidth", Version: "v0.0.9"}, + {Name: "github.com/matttproud/golang_protobuf_extensions", Version: "v1.0.1"}, + {Name: "github.com/maxbrunsfeld/counterfeiter/v6", Version: "v6.2.2"}, + {Name: "github.com/mitchellh/go-homedir", Version: "v1.1.0"}, + {Name: "github.com/mitchellh/mapstructure", Version: "v1.1.2"}, + {Name: "github.com/modern-go/concurrent", Version: "v0.0.0-20180306012644-bacd9c7ef1dd"}, + {Name: "github.com/modern-go/reflect2", Version: "v1.0.1"}, + {Name: "github.com/morikuni/aec", Version: "v1.0.0"}, + {Name: "github.com/munnerz/goautoneg", Version: "v0.0.0-20191010083416-a7dc8b61c822"}, + {Name: "github.com/mwitkow/go-conntrack", Version: "v0.0.0-20161129095857-cc309e4a2223"}, + {Name: "github.com/mxk/go-flowrate", Version: "v0.0.0-20140419014527-cca7078d478f"}, + {Name: "github.com/niemeyer/pretty", Version: "v0.0.0-20200227124842-a10e7caefd8e"}, + {Name: "github.com/nxadm/tail", Version: "v1.4.4"}, + {Name: "github.com/olekukonko/tablewriter", Version: "v0.0.2-0.20190607075207-195002e6e56a"}, + {Name: "github.com/onsi/ginkgo", Version: "v1.14.2"}, + {Name: "github.com/onsi/gomega", Version: "v1.10.3"}, + {Name: "github.com/open-policy-agent/opa", Version: "v0.21.1"}, + {Name: "github.com/opencontainers/go-digest", Version: "v1.0.0-rc1"}, + {Name: "github.com/opencontainers/image-spec", Version: "v1.0.2-0.20190823105129-775207bd45b6"}, + {Name: "github.com/opencontainers/runc", Version: "v0.1.1"}, + {Name: "github.com/parnurzeal/gorequest", Version: "v0.2.16"}, + {Name: "github.com/pelletier/go-toml", Version: "v1.2.0"}, + {Name: "github.com/peterbourgon/diskv", Version: "v2.0.1+incompatible"}, + {Name: "github.com/peterh/liner", Version: "v0.0.0-20170211195444-bf27d3ba8e1d"}, + {Name: "github.com/pkg/errors", Version: "v0.9.1"}, + {Name: "github.com/pmezard/go-difflib", Version: "v1.0.0"}, + {Name: "github.com/pquerna/cachecontrol", Version: "v0.0.0-20171018203845-0dec1b30a021"}, + {Name: "github.com/prometheus/client_golang", Version: "v1.0.0"}, + {Name: "github.com/prometheus/client_model", Version: "v0.0.0-20190812154241-14fe0d1b01d4"}, + {Name: "github.com/prometheus/common", Version: "v0.4.1"}, + {Name: "github.com/prometheus/procfs", Version: "v0.0.2"}, + {Name: "github.com/rcrowley/go-metrics", Version: "v0.0.0-20181016184325-3113b8401b8a"}, + {Name: "github.com/remyoudompheng/bigfft", Version: "v0.0.0-20170806203942-52369c62f446"}, + {Name: "github.com/rogpeppe/fastuuid", Version: "v0.0.0-20150106093220-6724a57986af"}, + {Name: "github.com/rogpeppe/go-charset", Version: "v0.0.0-20180617210344-2471d30d28b4"}, + {Name: "github.com/rogpeppe/go-internal", Version: "v1.3.0"}, + {Name: "github.com/rubiojr/go-vhd", Version: "v0.0.0-20160810183302-0bfd3b39853c"}, + {Name: "github.com/russross/blackfriday", Version: "v1.5.2"}, + {Name: "github.com/russross/blackfriday/v2", Version: "v2.0.1"}, + {Name: "github.com/saracen/walker", Version: "v0.0.0-20191201085201-324a081bae7e"}, + {Name: "github.com/satori/go.uuid", Version: "v1.2.0"}, + {Name: "github.com/sclevine/spec", Version: "v1.2.0"}, + {Name: "github.com/sergi/go-diff", Version: "v1.1.0"}, + {Name: "github.com/shurcooL/sanitized_anchor_name", Version: "v1.0.0"}, + {Name: "github.com/simplereach/timeutils", Version: "v1.2.0"}, + {Name: "github.com/sirupsen/logrus", Version: "v1.5.0"}, + {Name: "github.com/smartystreets/assertions", Version: "v1.2.0"}, + {Name: "github.com/smartystreets/goconvey", Version: "v1.6.4"}, + {Name: "github.com/soheilhy/cmux", Version: "v0.1.4"}, + {Name: "github.com/sosedoff/gitkit", Version: "v0.2.0"}, + {Name: "github.com/spf13/afero", Version: "v1.2.2"}, + {Name: "github.com/spf13/cast", Version: "v1.3.0"}, + {Name: "github.com/spf13/cobra", Version: "v0.0.5"}, + {Name: "github.com/spf13/jwalterweatherman", Version: "v1.0.0"}, + {Name: "github.com/spf13/pflag", Version: "v1.0.5"}, + {Name: "github.com/spf13/viper", Version: "v1.3.2"}, + {Name: "github.com/stretchr/objx", Version: "v0.3.0"}, + {Name: "github.com/stretchr/testify", Version: "v1.6.1"}, + {Name: "github.com/testcontainers/testcontainers-go", Version: "v0.3.1"}, + {Name: "github.com/tmc/grpc-websocket-proxy", Version: "v0.0.0-20170815181823-89b8d40f7ca8"}, + {Name: "github.com/twitchtv/twirp", Version: "v5.10.1+incompatible"}, + {Name: "github.com/ugorji/go", Version: "v1.1.7"}, + {Name: "github.com/ugorji/go/codec", Version: "v1.1.7"}, + {Name: "github.com/urfave/cli", Version: "v1.22.5"}, + {Name: "github.com/urfave/cli/v2", Version: "v2.3.0"}, + {Name: "github.com/vdemeester/k8s-pkg-credentialprovider", Version: "v1.17.4"}, + {Name: "github.com/vmware/govmomi", Version: "v0.20.3"}, + {Name: "github.com/xanzy/ssh-agent", Version: "v0.2.1"}, + {Name: "github.com/xiang90/probing", Version: "v0.0.0-20190116061207-43a291ad63a2"}, + {Name: "github.com/xordataexchange/crypt", Version: "v0.0.3-0.20170626215501-b2862e3d0a77"}, + {Name: "github.com/yashtewari/glob-intersection", Version: "v0.0.0-20180916065949-5c77d914dd0b"}, + {Name: "github.com/yuin/goldmark", Version: "v1.1.32"}, + {Name: "github.com/yuin/gopher-lua", Version: "v0.0.0-20191220021717-ab39c6098bdb"}, + {Name: "go.etcd.io/bbolt", Version: "v1.3.5"}, + {Name: "go.etcd.io/etcd", Version: "v0.0.0-20191023171146-3cf2f69b5738"}, + {Name: "go.opencensus.io", Version: "v0.22.4"}, + {Name: "go.opentelemetry.io/otel", Version: "v0.14.0"}, + {Name: "go.uber.org/atomic", Version: "v1.5.1"}, + {Name: "go.uber.org/multierr", Version: "v1.4.0"}, + {Name: "go.uber.org/tools", Version: "v0.0.0-20190618225709-2cfd321de3ee"}, + {Name: "go.uber.org/zap", Version: "v1.13.0"}, + {Name: "golang.org/x/crypto", Version: "v0.0.0-20201002170205-7f63de1d35b0"}, + {Name: "golang.org/x/exp", Version: "v0.0.0-20200224162631-6cc2880d07d6"}, + {Name: "golang.org/x/image", Version: "v0.0.0-20190802002840-cff245a6509b"}, + {Name: "golang.org/x/lint", Version: "v0.0.0-20200302205851-738671d3881b"}, + {Name: "golang.org/x/mobile", Version: "v0.0.0-20190719004257-d2bd2a29d028"}, + {Name: "golang.org/x/mod", Version: "v0.3.0"}, + {Name: "golang.org/x/net", Version: "v0.0.0-20201006153459-a7d1128ccaa0"}, + {Name: "golang.org/x/oauth2", Version: "v0.0.0-20201208152858-08078c50e5b5"}, + {Name: "golang.org/x/sync", Version: "v0.0.0-20200625203802-6e8e738ad208"}, + {Name: "golang.org/x/sys", Version: "v0.0.0-20201006155630-ac719f4daadf"}, + {Name: "golang.org/x/text", Version: "v0.3.3"}, + {Name: "golang.org/x/time", Version: "v0.0.0-20191024005414-555d28b269f0"}, + {Name: "golang.org/x/tools", Version: "v0.0.0-20200825202427-b303f430e36d"}, + {Name: "golang.org/x/xerrors", Version: "v0.0.0-20200804184101-5ec99f83aff1"}, + {Name: "gonum.org/v1/gonum", Version: "v0.0.0-20190331200053-3d26580ed485"}, + {Name: "gonum.org/v1/netlib", Version: "v0.0.0-20190331212654-76723241ea4e"}, + {Name: "google.golang.org/api", Version: "v0.30.0"}, + {Name: "google.golang.org/appengine", Version: "v1.6.6"}, + {Name: "google.golang.org/genproto", Version: "v0.0.0-20200825200019-8632dd797987"}, + {Name: "google.golang.org/grpc", Version: "v1.31.0"}, + {Name: "google.golang.org/protobuf", Version: "v1.25.0"}, + {Name: "gopkg.in/alecthomas/kingpin.v2", Version: "v2.2.6"}, + {Name: "gopkg.in/check.v1", Version: "v1.0.0-20200902074654-038fdea0a05b"}, + {Name: "gopkg.in/cheggaaa/pb.v1", Version: "v1.0.28"}, + {Name: "gopkg.in/errgo.v2", Version: "v2.1.0"}, + {Name: "gopkg.in/fsnotify.v1", Version: "v1.4.7"}, + {Name: "gopkg.in/gcfg.v1", Version: "v1.2.0"}, + {Name: "gopkg.in/go-playground/assert.v1", Version: "v1.2.1"}, + {Name: "gopkg.in/go-playground/validator.v9", Version: "v9.31.0"}, + {Name: "gopkg.in/inf.v0", Version: "v0.9.1"}, + {Name: "gopkg.in/mgo.v2", Version: "v2.0.0-20180705113604-9856a29383ce"}, + {Name: "gopkg.in/natefinch/lumberjack.v2", Version: "v2.0.0"}, + {Name: "gopkg.in/resty.v1", Version: "v1.12.0"}, + {Name: "gopkg.in/square/go-jose.v2", Version: "v2.2.2"}, + {Name: "gopkg.in/tomb.v1", Version: "v1.0.0-20141024135613-dd632973f1e7"}, + {Name: "gopkg.in/warnings.v0", Version: "v0.1.2"}, + {Name: "gopkg.in/yaml.v2", Version: "v2.4.0"}, + {Name: "gopkg.in/yaml.v3", Version: "v3.0.0-20200615113413-eeeca48fe776"}, + {Name: "gotest.tools", Version: "v2.2.0+incompatible"}, + {Name: "honnef.co/go/tools", Version: "v0.0.1-2020.1.4"}, + {Name: "k8s.io/api", Version: "v0.17.4"}, + {Name: "k8s.io/apimachinery", Version: "v0.17.4"}, + {Name: "k8s.io/apiserver", Version: "v0.17.4"}, + {Name: "k8s.io/client-go", Version: "v0.17.4"}, + {Name: "k8s.io/cloud-provider", Version: "v0.17.4"}, + {Name: "k8s.io/code-generator", Version: "v0.17.2"}, + {Name: "k8s.io/component-base", Version: "v0.17.4"}, + {Name: "k8s.io/csi-translation-lib", Version: "v0.17.4"}, + {Name: "k8s.io/gengo", Version: "v0.0.0-20190822140433-26a664648505"}, + {Name: "k8s.io/klog", Version: "v1.0.0"}, + {Name: "k8s.io/klog/v2", Version: "v2.0.0"}, + {Name: "k8s.io/kube-openapi", Version: "v0.0.0-20191107075043-30be4d16710a"}, + {Name: "k8s.io/legacy-cloud-providers", Version: "v0.17.4"}, + {Name: "k8s.io/utils", Version: "v0.0.0-20201110183641-67b214c5f920"}, + {Name: "modernc.org/cc", Version: "v1.0.0"}, + {Name: "modernc.org/golex", Version: "v1.0.0"}, + {Name: "modernc.org/mathutil", Version: "v1.0.0"}, + {Name: "modernc.org/strutil", Version: "v1.0.0"}, + {Name: "modernc.org/xc", Version: "v1.0.0"}, + {Name: "moul.io/http2curl", Version: "v1.0.0"}, + {Name: "rsc.io/binaryregexp", Version: "v0.2.0"}, + {Name: "rsc.io/quote/v3", Version: "v3.1.0"}, + {Name: "rsc.io/sampler", Version: "v1.3.0"}, + {Name: "sigs.k8s.io/structured-merge-diff", Version: "v1.0.1-0.20191108220359-b1b620dd3f06"}, + {Name: "sigs.k8s.io/yaml", Version: "v1.1.0"}, + } +) diff --git a/pkg/dependency/parser/golang/sum/testdata/gomod_emptyline.sum b/pkg/dependency/parser/golang/sum/testdata/gomod_emptyline.sum new file mode 100644 index 000000000000..a135140b3ec3 --- /dev/null +++ b/pkg/dependency/parser/golang/sum/testdata/gomod_emptyline.sum @@ -0,0 +1,5 @@ +github.com/karrick/godirwalk v1.12.0 h1:nkS4xxsjiZMvVlazd0mFyiwD4BR9f3m6LXGhM2TUx3Y= +github.com/karrick/godirwalk v1.12.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= + diff --git a/pkg/dependency/parser/golang/sum/testdata/gomod_many.sum b/pkg/dependency/parser/golang/sum/testdata/gomod_many.sum new file mode 100644 index 000000000000..f7e224b946b4 --- /dev/null +++ b/pkg/dependency/parser/golang/sum/testdata/gomod_many.sum @@ -0,0 +1,24 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/dependency/parser/golang/sum/testdata/gomod_normal.sum b/pkg/dependency/parser/golang/sum/testdata/gomod_normal.sum new file mode 100644 index 000000000000..1ff365f579a0 --- /dev/null +++ b/pkg/dependency/parser/golang/sum/testdata/gomod_normal.sum @@ -0,0 +1,2 @@ +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/dependency/parser/golang/sum/testdata/gomod_trivy.sum b/pkg/dependency/parser/golang/sum/testdata/gomod_trivy.sum new file mode 100644 index 000000000000..3bb9ba16c633 --- /dev/null +++ b/pkg/dependency/parser/golang/sum/testdata/gomod_trivy.sum @@ -0,0 +1,869 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/azure-sdk-for-go v35.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/azure-sdk-for-go v38.0.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI= +github.com/Azure/go-autorest/autorest v0.9.3/go.mod h1:GsRuLYvwzLjjjRoWEIyMUaYq8GNUx2nRB378IPt/1p0= +github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0= +github.com/Azure/go-autorest/autorest/adal v0.8.0/go.mod h1:Z6vX6WXXuyieHAXwMj0S6HY6e6wcHn37qQMBQlvY3lc= +github.com/Azure/go-autorest/autorest/adal v0.8.1/go.mod h1:ZjhuQClTqx435SRJ2iMlOxPYt3d2C/T/7TiQCVZSn3Q= +github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA= +github.com/Azure/go-autorest/autorest/date v0.2.0/go.mod h1:vcORJHLJEh643/Ioh9+vPmf1Ij9AEBM5FuBIXLmIy0g= +github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0= +github.com/Azure/go-autorest/autorest/mocks v0.3.0/go.mod h1:a8FDP3DYzQ4RYfVAxAN3SVSiiO77gL2j2ronKKP0syM= +github.com/Azure/go-autorest/autorest/to v0.2.0/go.mod h1:GunWKJp1AEqgMaGLV+iocmRAJWqST1wQYhyyjXJ3SJc= +github.com/Azure/go-autorest/autorest/to v0.3.0/go.mod h1:MgwOyqaIuKdG4TL/2ywSsIWKAfJfgHDo8ObuUk3t5sA= +github.com/Azure/go-autorest/autorest/validation v0.1.0/go.mod h1:Ha3z/SqBeaalWQvokg3NZAlQTalVMtOIAs1aGK7G6u8= +github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc= +github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GoogleCloudPlatform/docker-credential-gcr v1.5.0/go.mod h1:BB1eHdMLYEFuFdBlRMb0N7YGVdM5s6Pt0njxgvfbGGs= +github.com/GoogleCloudPlatform/k8s-cloud-provider v0.0.0-20190822182118-27a4ced34534/go.mod h1:iroGtC8B3tQiqtds1l+mgk/BBOrxbqjH+eUfFQYRc14= +github.com/Microsoft/go-winio v0.4.11/go.mod h1:VhR8bwka0BXejwEJY73c50VrPtXAaKcyvVC4A4RozmA= +github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= +github.com/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw= +github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/OneOfOne/xxhash v1.2.7/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis/v2 v2.14.1/go.mod h1:uS970Sw5Gs9/iK3yBg0l9Uj9s25wXxSpQUE9EaJ/Blg= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8= +github.com/aquasecurity/fanal v0.0.0-20210119051230-28c249da7cfd/go.mod h1:kur6SaohYhsjQLzijAdtn+X8rkTtwxawE51WyVCXLKk= +github.com/aquasecurity/go-dep-parser v0.0.0-20201028043324-889d4a92b8e0/go.mod h1:X42mTIRhgPalSm81Om2kD+3ydeunbC8TZtZj1bvgRo8= +github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s= +github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798/go.mod h1:hxbJZtKlO4P8sZ9nztizR6XLoE33O+BkPmuYQ4ACyz0= +github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46/go.mod h1:olhPNdiiAAMiSujemd1O/sc6GcyePr23f/6uGKtthNg= +github.com/aquasecurity/go-version v0.0.0-20201107203531-5e48ac5d022a/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= +github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= +github.com/aquasecurity/testdocker v0.0.0-20210106133225-0b17fe083674/go.mod h1:psfu0MVaiTDLpNxCoNsTeILSKY2EICBwv345f3M+Ffs= +github.com/aquasecurity/trivy v0.16.0 h1:lyzqYGQ2TYxcjFwKBw6aU3PwHLvc0+6VTech+0/LnqQ= +github.com/aquasecurity/trivy v0.16.0/go.mod h1:SLXDW/zKJWE7XhMEGGfkFGABNvz6b8BAbNjuXztJlj0= +github.com/aquasecurity/trivy-db v0.0.0-20210105160501-c5bf4e153277/go.mod h1:N7CWA/vjVw78GWAdCJGhFQVqNGEA4e47a6eIWm+C/Bc= +github.com/aquasecurity/vuln-list-update v0.0.0-20191016075347-3d158c2bf9a2/go.mod h1:6NhOP0CjZJL27bZZcaHECtzWdwDDm2g6yCY0QgXEGQQ= +github.com/araddon/dateparse v0.0.0-20190426192744-0d74ffceef83/go.mod h1:SLqhdZcd+dF3TEVL2RMoob5bBP5R1P1qkox+HtCBgGI= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go v1.16.26/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go v1.27.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/briandowns/spinner v1.12.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= +github.com/caarlos0/env/v6 v6.0.0/go.mod h1:+wdyOmtjoZIW2GJOc2OYa5NoOFuWD/bIpWqm30NgtRk= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cheggaaa/pb/v3 v3.0.3/go.mod h1:Pp35CDuiEpHa/ZLGCtBbM6CBwMstv1bJlG884V+73Yc= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/containerd/containerd v1.3.0/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/containerd v1.3.3/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180108230652-97fdf19511ea/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +github.com/dnaeon/go-vcr v1.0.1/go.mod h1:aBB1+wY4s93YsC3HHjMBMrwTj2R9FHDzUr9KyGc8n1E= +github.com/docker/cli v0.0.0-20191017083524-a8ff7f821017/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v0.7.3-0.20190327010347-be7ac8be2ae0/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v0.7.3-0.20190506211059-b20a14b54661/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker-credential-helpers v0.6.3/go.mod h1:WRaJzqw3CTB9bk10avuGsjVBZsD05qeibJ1/TYlvc0Y= +github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/elazarl/goproxy v0.0.0-20200809112317-0581fc3aee2d/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190421051319-9d40249d3c2f/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/elazarl/goproxy/ext v0.0.0-20200809112317-0581fc3aee2d/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= +github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-redis/redis/v8 v8.4.0/go.mod h1:A1tbYoHSa1fXwN+//ljcCYYJeLmVrwL9hbQN45Jdy0M= +github.com/go-restruct/restruct v0.0.0-20191227155143-5734170a48a1/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-yaml v1.8.1/go.mod h1:wS4gNoLalDSJxo/SpngzPQ2BN4uuZVLCmbM4S3vd4+Y= +github.com/goccy/go-yaml v1.8.2/go.mod h1:wS4gNoLalDSJxo/SpngzPQ2BN4uuZVLCmbM4S3vd4+Y= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.0/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v0.0.0-20181025225059-d3de96c4c28e/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-containerregistry v0.0.0-20200331213917-3d03ed9b1ca2/go.mod h1:pD1UFYs7MCAx+ZLShBdttcaOSbyc8F9Na/9IZLNwJeA= +github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/subcommands v1.0.1/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/wire v0.3.0/go.mod h1:i1DMg/Lu8Sz5yYl25iOdmc5CT5qusaa+zmRWs16741s= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/googleapis/gnostic v0.2.2/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v0.0.0-20181024020800-521ea7b17d02/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joefitzgerald/rainbow-reporter v0.1.0/go.mod h1:481CNgqmVHQZzdIbN52CupLJyoVwB10FQ/IQlF1pdL8= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8= +github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao= +github.com/knqyf263/go-rpm-version v0.0.0-20170716094938-74609b86c936/go.mod h1:i4sF0l1fFnY1aiw08QQSwVAFxHEm311Me3WsU/X7nL0= +github.com/knqyf263/go-rpmdb v0.0.0-20201215100354-a9e3110d8ee1/go.mod h1:RDPNeIkU5NWXtt0OMEoILyxwUC/DyXeRtK295wpqSi0= +github.com/knqyf263/nested v0.0.1/go.mod h1:zwhsIhMkBg90DTOJQvxPkKIypEHPYkgWHs4gybdlUmk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-jsonpointer v0.0.0-20180225143300-37667080efed/go.mod h1:SDJ4hurDYyQ9/7nc+eCYtXqdufgK4Cq9TJlwPklqEYA= +github.com/mattn/go-runewidth v0.0.0-20181025052659-b20a3daf6a39/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/maxbrunsfeld/counterfeiter/v6 v6.2.2/go.mod h1:eD9eIE7cdwcMi9rYluz88Jz2VyhSmden33/aXg4oVIY= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v0.0.0-20170113033406-39771216ff4c/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/olekukonko/tablewriter v0.0.2-0.20190607075207-195002e6e56a/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= +github.com/open-policy-agent/opa v0.21.1/go.mod h1:cZaTfhxsj7QdIiUI0U9aBtOLLTqVNe+XE60+9kZKLHw= +github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= +github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/image-spec v1.0.2-0.20190823105129-775207bd45b6/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= +github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= +github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/peterh/liner v0.0.0-20170211195444-bf27d3ba8e1d/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/pkg/errors v0.0.0-20181023235946-059132a15dd0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.0.0-20181025174421-f30f42803563/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20170806203942-52369c62f446/go.mod h1:uYEyJGbgTkfkS4+E/PavXkNJcbFIpEtjt2B0KDQ5+9M= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rubiojr/go-vhd v0.0.0-20160810183302-0bfd3b39853c/go.mod h1:DM5xW0nvfNNm2uytzsvhI3OnX8uzaRAg8UX/CnDqbto= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/saracen/walker v0.0.0-20191201085201-324a081bae7e/go.mod h1:G0Z6yVPru183i2MuRJx1DcR4dgIZtLcTdaaE/pC1BJU= +github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= +github.com/sclevine/spec v1.2.0/go.mod h1:W4J29eT/Kzv7/b9IWLB055Z+qvVC9vt0Arko24q7p+U= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/simplereach/timeutils v1.2.0/go.mod h1:VVbQDfN/FHRZa1LSqcwo4kNZ62OOyqLLGQKYB3pB0Q8= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sosedoff/gitkit v0.2.0/go.mod h1:A+o6ZazfVJwetlcHz3ah6th66XcBdsyzLo+aBt/AsK4= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.0-20181021141114-fe5e611709b0/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v0.0.0-20181024212040-082b515c9490/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/testcontainers/testcontainers-go v0.3.1/go.mod h1:br7bkzIukhPSIjy07Ma3OuXjjFvl2jm7CDU0LQNsqLw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/twitchtv/twirp v5.10.1+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/vdemeester/k8s-pkg-credentialprovider v1.17.4/go.mod h1:inCTmtUdr5KJbreVojo06krnTgaeAz/Z7lynpPk/Q2c= +github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +github.com/yashtewari/glob-intersection v0.0.0-20180916065949-5c77d914dd0b/go.mod h1:HptNXiXVDcJjXe9SqMd0v2FsL9f8dz4GnXgltU6q/co= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/otel v0.14.0/go.mod h1:vH5xEuwy7Rts0GNtsCW3HYQoZDY+OmBJ6t1bFGGlxgw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.5.1/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.4.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181023182221-1baf3a9d7d67/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200327173247-9dae0f8f5775/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201006155630-ac719f4daadf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180810170437-e96c4e24768d/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190422233926-fe54fb35175b/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190706070813-72ffa07ba3db/go.mod h1:jcCCGcm9btYwXyDqrUWc6MKQKKGJCWEQ3AfLSRIbEuI= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191011211836-4c025a95b26e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200210192313-1ace956b0e17/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gonum.org/v1/gonum v0.0.0-20190331200053-3d26580ed485/go.mod h1:2ltnJ7xHfj0zHS40VVPYEAAMTa3ZGguvHGBSJeRWqE0= +gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= +gonum.org/v1/netlib v0.0.0-20190331212654-76723241ea4e/go.mod h1:kS+toOQn6AQKjmKJ7gzohV1XkqsFehRA2FbsbkopSuQ= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.6.1-0.20190607001116-5213b8090861/go.mod h1:btoxGiFvQNVUZQ8W08zLtrVS08CNpINPEfxXxgJL1Q4= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/cheggaaa/pb.v1 v1.0.28/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.0/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/validator.v9 v9.29.1/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/go-playground/validator.v9 v9.30.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/mgo.v2 v2.0.0-20180705113604-9856a29383ce/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.1/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools v0.0.0-20181223230014-1083505acf35/go.mod h1:R//lfYlUuTOTfblYI3lGoAAAebUdzjvbmQsuB7Ykd90= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.17.4/go.mod h1:5qxx6vjmwUVG2nHQTKGlLts8Tbok8PzHl4vHtVFuZCA= +k8s.io/apimachinery v0.17.4/go.mod h1:gxLnyZcGNdZTCLnq3fgzyg2A5BVCHTNDFrw8AmuJ+0g= +k8s.io/apiserver v0.17.4/go.mod h1:5ZDQ6Xr5MNBxyi3iUZXS84QOhZl+W7Oq2us/29c0j9I= +k8s.io/client-go v0.17.4/go.mod h1:ouF6o5pz3is8qU0/qYL2RnoxOPqgfuidYLowytyLJmc= +k8s.io/cloud-provider v0.17.4/go.mod h1:XEjKDzfD+b9MTLXQFlDGkk6Ho8SGMpaU8Uugx/KNK9U= +k8s.io/code-generator v0.17.2/go.mod h1:DVmfPQgxQENqDIzVR2ddLXMH34qeszkKSdH/N+s+38s= +k8s.io/component-base v0.17.4/go.mod h1:5BRqHMbbQPm2kKu35v3G+CpVq4K0RJKC7TRioF0I9lE= +k8s.io/csi-translation-lib v0.17.4/go.mod h1:CsxmjwxEI0tTNMzffIAcgR9lX4wOh6AKHdxQrT7L0oo= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20190822140433-26a664648505/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +k8s.io/legacy-cloud-providers v0.17.4/go.mod h1:FikRNoD64ECjkxO36gkDgJeiQWwyZTuBkhu+yxOc1Js= +k8s.io/utils v0.0.0-20191114184206-e782cd3c129f/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +modernc.org/cc v1.0.0/go.mod h1:1Sk4//wdnYJiUIxnW8ddKpaOJCF37yAdqYnkxUpaYxw= +modernc.org/golex v1.0.0/go.mod h1:b/QX9oBD/LhixY6NDh+IdGv17hgB+51fET1i2kPSmvk= +modernc.org/mathutil v1.0.0/go.mod h1:wU0vUrJsVWBZ4P6e7xtFJEhFSNsfRLJ8H458uRjg03k= +modernc.org/strutil v1.0.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs= +modernc.org/xc v1.0.0/go.mod h1:mRNCo0bvLjGhHO9WsyuKVU4q0ceiDDDoEeWDJHrNx8I= +moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/structured-merge-diff v1.0.1-0.20191108220359-b1b620dd3f06/go.mod h1:/ULNhyfzRopfcjskuui0cTITekDduZ7ycKN3oUT9R18= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= diff --git a/pkg/dependency/parser/gradle/lockfile/parse.go b/pkg/dependency/parser/gradle/lockfile/parse.go new file mode 100644 index 000000000000..b43ae4435407 --- /dev/null +++ b/pkg/dependency/parser/gradle/lockfile/parse.go @@ -0,0 +1,65 @@ +package lockfile + +import ( + "bufio" + "context" + "strings" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/utils" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var pkgs []ftypes.Package + scanner := bufio.NewScanner(r) + var lineNum int + for scanner.Scan() { + lineNum++ + line := strings.TrimSpace(scanner.Text()) + if strings.HasPrefix(line, "#") { // skip comments + continue + } + + // dependency format: group:artifact:version=classPaths + dep := strings.Split(line, ":") + if len(dep) != 3 { // skip the last line with lists of empty configurations + continue + } + + name := strings.Join(dep[:2], ":") + version, classPathsString, _ := strings.Cut(dep[2], "=") + + dev := true + + for classPath := range strings.SplitSeq(classPathsString, ",") { + if !strings.HasPrefix(classPath, "test") { + dev = false + break + } + } + + pkgs = append(pkgs, ftypes.Package{ + ID: dependency.ID(ftypes.Gradle, name, version), + Name: name, + Version: version, + Dev: dev, + Locations: []ftypes.Location{ + { + StartLine: lineNum, + EndLine: lineNum, + }, + }, + Relationship: ftypes.RelationshipUnknown, + }) + + } + return utils.UniquePackages(pkgs), nil, nil +} diff --git a/pkg/dependency/parser/gradle/lockfile/parse_test.go b/pkg/dependency/parser/gradle/lockfile/parse_test.go new file mode 100644 index 000000000000..a16c7cea35f8 --- /dev/null +++ b/pkg/dependency/parser/gradle/lockfile/parse_test.go @@ -0,0 +1,92 @@ +package lockfile + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []ftypes.Package + }{ + { + name: "happy path", + inputFile: "testdata/happy.lockfile", + want: []ftypes.Package{ + { + ID: "cglib:cglib-nodep:2.1.2", + Name: "cglib:cglib-nodep", + Version: "2.1.2", + Dev: false, + Locations: []ftypes.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + { + ID: "junit:junit:4.13.2", + Name: "junit:junit", + Version: "4.13.2", + Dev: true, + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + ID: "org.springframework:spring-asm:3.1.3.RELEASE", + Name: "org.springframework:spring-asm", + Version: "3.1.3.RELEASE", + Dev: false, + Locations: []ftypes.Location{ + { + StartLine: 6, + EndLine: 6, + }, + }, + }, + { + ID: "org.springframework:spring-beans:5.0.5.RELEASE", + Name: "org.springframework:spring-beans", + Version: "5.0.5.RELEASE", + Dev: false, + Locations: []ftypes.Location{ + { + StartLine: 7, + EndLine: 7, + }, + }, + }, + }, + }, + { + name: "empty", + inputFile: "testdata/empty.lockfile", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := NewParser() + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + pkgs, _, _ := parser.Parse(t.Context(), f) + sort.Sort(ftypes.Packages(pkgs)) + assert.Equal(t, tt.want, pkgs) + }) + } +} diff --git a/pkg/dependency/parser/gradle/lockfile/testdata/empty.lockfile b/pkg/dependency/parser/gradle/lockfile/testdata/empty.lockfile new file mode 100644 index 000000000000..77cdc75caaca --- /dev/null +++ b/pkg/dependency/parser/gradle/lockfile/testdata/empty.lockfile @@ -0,0 +1,4 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +empty=incomingCatalog \ No newline at end of file diff --git a/pkg/dependency/parser/gradle/lockfile/testdata/happy.lockfile b/pkg/dependency/parser/gradle/lockfile/testdata/happy.lockfile new file mode 100644 index 000000000000..ee7f69aed983 --- /dev/null +++ b/pkg/dependency/parser/gradle/lockfile/testdata/happy.lockfile @@ -0,0 +1,9 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +cglib:cglib-nodep:2.1.2=testRuntimeClasspath,classpath +junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath + org.springframework:spring-asm:3.1.3.RELEASE=classpath +org.springframework:spring-beans:5.0.5.RELEASE=compileClasspath, runtimeClasspath + # io.grpc:grpc-api:1.21.1=classpath +empty= diff --git a/pkg/dependency/parser/hex/mix/parse.go b/pkg/dependency/parser/hex/mix/parse.go new file mode 100644 index 000000000000..be4fbd37530a --- /dev/null +++ b/pkg/dependency/parser/hex/mix/parse.go @@ -0,0 +1,71 @@ +package mix + +import ( + "bufio" + "context" + "strings" + "unicode" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/utils" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +// Parser is a parser for mix.lock +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("mix"), + } +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var pkgs []ftypes.Package + scanner := bufio.NewScanner(r) + var lineNumber int // It is used to save dependency location + for scanner.Scan() { + lineNumber++ + line := strings.TrimSpace(scanner.Text()) + name, body, ok := strings.Cut(line, ":") + if !ok { + // skip 1st and last lines + continue + } + name = strings.Trim(name, `"`) + + // dependency format: + // "": {<:hex|:git>, :, "", "", [:mix], [], hexpm", ""}, + ss := strings.FieldsFunc(body, func(r rune) bool { + return unicode.IsSpace(r) || r == ',' + }) + if len(ss) < 8 { // In the case where array is empty: s == 8, in other cases s > 8 + // git repository doesn't have dependency version + // skip these dependencies + if !strings.Contains(ss[0], ":git") { + p.logger.Warn("Cannot parse dependency", log.String("line", line)) + } else { + p.logger.Debug("Skip git dependencies", log.String("name", name)) + } + continue + } + version := strings.Trim(ss[2], `"`) + pkgs = append(pkgs, ftypes.Package{ + ID: dependency.ID(ftypes.Hex, name, version), + Name: name, + Version: version, + Locations: []ftypes.Location{ + { + StartLine: lineNumber, + EndLine: lineNumber, + }, + }, + }) + + } + return utils.UniquePackages(pkgs), nil, nil +} diff --git a/pkg/dependency/parser/hex/mix/parse_test.go b/pkg/dependency/parser/hex/mix/parse_test.go new file mode 100644 index 000000000000..73b282593158 --- /dev/null +++ b/pkg/dependency/parser/hex/mix/parse_test.go @@ -0,0 +1,88 @@ +package mix + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []ftypes.Package + }{ + { + name: "happy path", + inputFile: "testdata/happy.mix.lock", + want: []ftypes.Package{ + { + ID: "bunt@0.2.0", + Name: "bunt", + Version: "0.2.0", + Locations: []ftypes.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + }, + { + ID: "credo@1.6.6", + Name: "credo", + Version: "1.6.6", + Locations: []ftypes.Location{ + { + StartLine: 3, + EndLine: 3, + }, + }, + }, + { + ID: "file_system@0.2.10", + Name: "file_system", + Version: "0.2.10", + Locations: []ftypes.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + { + ID: "jason@1.3.0", + Name: "jason", + Version: "1.3.0", + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + }, + }, + { + name: "empty", + inputFile: "testdata/empty.mix.lock", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := NewParser() + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + pkgs, _, _ := parser.Parse(t.Context(), f) + sort.Sort(ftypes.Packages(pkgs)) + assert.Equal(t, tt.want, pkgs) + }) + } +} diff --git a/pkg/dependency/parser/hex/mix/testdata/empty.mix.lock b/pkg/dependency/parser/hex/mix/testdata/empty.mix.lock new file mode 100644 index 000000000000..15081287722e --- /dev/null +++ b/pkg/dependency/parser/hex/mix/testdata/empty.mix.lock @@ -0,0 +1,2 @@ +%{ +} \ No newline at end of file diff --git a/pkg/dependency/parser/hex/mix/testdata/happy.mix.lock b/pkg/dependency/parser/hex/mix/testdata/happy.mix.lock new file mode 100644 index 000000000000..7ab92fc60614 --- /dev/null +++ b/pkg/dependency/parser/hex/mix/testdata/happy.mix.lock @@ -0,0 +1,7 @@ +%{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, + "credo": {:hex, :credo, "1.6.6", "f51f8d45db1af3b2e2f7bee3e6d3c871737bda4a91bff00c5eec276517d1a19c", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "625520ce0984ee0f9f1f198165cd46fa73c1e59a17ebc520038b8fce056a5bdc"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"}, + "esaml": {:git, "https://github.com/firezone/esaml.git", "4294a3ac5262582144e117c10a1537287b6c1fe8", []}, +} \ No newline at end of file diff --git a/pkg/dependency/parser/java/jar/parse.go b/pkg/dependency/parser/java/jar/parse.go new file mode 100644 index 000000000000..3b2497fbf186 --- /dev/null +++ b/pkg/dependency/parser/java/jar/parse.go @@ -0,0 +1,464 @@ +package jar + +import ( + "archive/zip" + "bufio" + "context" + "crypto/sha1" // nolint:gosec + "encoding/hex" + "errors" + "io" + "os" + "path" + "path/filepath" + "regexp" + "slices" + "strings" + + mavenversion "github.com/masahiro331/go-mvn-version" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +var ( + jarFileRegEx = regexp.MustCompile(`^([a-zA-Z0-9\._-]*[^-*])-(\d\S*(?:-SNAPSHOT)?).jar$`) +) + +type Client interface { + Exists(groupID, artifactID string) (bool, error) + SearchBySHA1(sha1 string) (Properties, error) + SearchByArtifactID(artifactID, version string) (string, error) +} + +type Parser struct { + logger *log.Logger + rootFilePath string + offline bool + size int64 + + client Client +} + +type Option func(*Parser) + +func WithFilePath(filePath string) Option { + return func(p *Parser) { + p.rootFilePath = filePath + } +} + +func WithOffline(offline bool) Option { + return func(p *Parser) { + p.offline = offline + } +} + +func WithSize(size int64) Option { + return func(p *Parser) { + p.size = size + } +} + +func NewParser(c Client, opts ...Option) *Parser { + p := &Parser{ + logger: log.WithPrefix("jar"), + client: c, + } + + for _, opt := range opts { + opt(p) + } + + return p +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + pkgs, deps, err := p.parseArtifact(p.rootFilePath, p.size, r) + if err != nil { + return nil, nil, xerrors.Errorf("unable to parse %s: %w", p.rootFilePath, err) + } + return removePackageDuplicates(pkgs), deps, nil +} + +func (p *Parser) parseArtifact(filePath string, size int64, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + p.logger.Debug("Parsing Java artifacts...", log.FilePath(filePath)) + + // Try to extract artifactId and version from the file name + // e.g. spring-core-5.3.4-SNAPSHOT.jar => sprint-core, 5.3.4-SNAPSHOT + fileName := filepath.Base(filePath) + fileProps := parseFileName(filePath) + + pkgs, m, foundPomProps, err := p.traverseZip(filePath, size, r, fileProps) + if err != nil { + return nil, nil, xerrors.Errorf("zip error: %w", err) + } + + // If pom.properties is found, it should be preferred than MANIFEST.MF. + if foundPomProps { + return pkgs, nil, nil + } + + manifestProps := m.properties(filePath) + if p.offline { + // In offline mode, we will not check if the artifact information is correct. + if !manifestProps.Valid() { + p.logger.Debug("Unable to identify POM in offline mode", log.String("file", fileName)) + return pkgs, nil, nil + } + return append(pkgs, manifestProps.Package()), nil, nil + } + + if manifestProps.Valid() { + // Even if MANIFEST.MF is found, the groupId and artifactId might not be valid. + // We have to make sure that the artifact exists actually. + if ok, _ := p.client.Exists(manifestProps.GroupID, manifestProps.ArtifactID); ok { + // If groupId and artifactId are valid, they will be returned. + return append(pkgs, manifestProps.Package()), nil, nil + } + } + + // If groupId and artifactId are not found, call Maven Central's search API with SHA-1 digest. + props, err := p.searchBySHA1(r, filePath) + if err == nil { + return append(pkgs, props.Package()), nil, nil + } else if !errors.Is(err, ArtifactNotFoundErr) { + return nil, nil, xerrors.Errorf("failed to search by SHA1: %w", err) + } + + p.logger.Debug("No such POM in the central repositories", log.String("file", fileName)) + + // Return when artifactId or version from the file name are empty + if fileProps.ArtifactID == "" || fileProps.Version == "" { + return pkgs, nil, nil + } + + // Try to search groupId by artifactId via sonatype API + // When some artifacts have the same groupIds, it might result in false detection. + fileProps.GroupID, err = p.client.SearchByArtifactID(fileProps.ArtifactID, fileProps.Version) + if err == nil { + p.logger.Debug("POM was determined in a heuristic way", log.String("file", fileName), + log.String("artifact", fileProps.String())) + pkgs = append(pkgs, fileProps.Package()) + } else if !errors.Is(err, ArtifactNotFoundErr) { + return nil, nil, xerrors.Errorf("failed to search by artifact id: %w", err) + } + + return pkgs, nil, nil +} + +func (p *Parser) traverseZip(filePath string, size int64, r xio.ReadSeekerAt, fileProps Properties) ( + []ftypes.Package, manifest, bool, error) { + var pkgs []ftypes.Package + var m manifest + var foundPomProps bool + + zr, err := zip.NewReader(r, size) + if err != nil { + return nil, manifest{}, false, xerrors.Errorf("zip error: %w", err) + } + + for _, fileInJar := range zr.File { + switch { + case filepath.Base(fileInJar.Name) == "pom.properties": + props, err := parsePomProperties(fileInJar, filePath) + if err != nil { + return nil, manifest{}, false, xerrors.Errorf("failed to parse %s: %w", fileInJar.Name, err) + } + // Validation of props to avoid getting packages with empty Name/Version + if props.Valid() { + pkgs = append(pkgs, props.Package()) + + // Check if the pom.properties is for the original JAR/WAR/EAR + if fileProps.ArtifactID == props.ArtifactID && fileProps.Version == props.Version { + foundPomProps = true + } + } + case filepath.Base(fileInJar.Name) == "MANIFEST.MF": + m, err = parseManifest(fileInJar) + if err != nil { + return nil, manifest{}, false, xerrors.Errorf("failed to parse MANIFEST.MF: %w", err) + } + case isArtifact(fileInJar.Name): + innerPkgs, _, err := p.parseInnerJar(fileInJar, filePath) // TODO process inner deps + if err != nil { + p.logger.Debug("Failed to parse", log.String("file", fileInJar.Name), log.Err(err)) + continue + } + pkgs = append(pkgs, innerPkgs...) + } + } + return pkgs, m, foundPomProps, nil +} + +func (p *Parser) parseInnerJar(zf *zip.File, rootPath string) ([]ftypes.Package, []ftypes.Dependency, error) { + fr, err := zf.Open() + if err != nil { + return nil, nil, xerrors.Errorf("unable to open %s: %w", zf.Name, err) + } + + f, err := xos.CreateTemp("", "jar-inner-") + if err != nil { + return nil, nil, xerrors.Errorf("unable to create a temp file: %w", err) + } + defer func() { + _ = f.Close() + _ = os.Remove(f.Name()) + }() + + // Copy the file content to the temp file + if n, err := io.CopyN(f, fr, int64(zf.UncompressedSize64)); err != nil { + return nil, nil, xerrors.Errorf("file copy error: %w", err) + } else if n != int64(zf.UncompressedSize64) { + return nil, nil, xerrors.Errorf("file copy size error: %w", err) + } + + // build full path to inner jar + fullPath := path.Join(rootPath, zf.Name) // nolint:gosec + if !strings.HasPrefix(fullPath, path.Clean(rootPath)) { + return nil, nil, nil // zip slip + } + + // Parse jar/war/ear recursively + innerPkgs, innerDeps, err := p.parseArtifact(fullPath, int64(zf.UncompressedSize64), f) + if err != nil { + return nil, nil, xerrors.Errorf("failed to parse %s: %w", zf.Name, err) + } + + return innerPkgs, innerDeps, nil +} + +func (p *Parser) searchBySHA1(r io.ReadSeeker, filePath string) (Properties, error) { + if _, err := r.Seek(0, io.SeekStart); err != nil { + return Properties{}, xerrors.Errorf("file seek error: %w", err) + } + + h := sha1.New() // nolint:gosec + if _, err := io.Copy(h, r); err != nil { + return Properties{}, xerrors.Errorf("unable to calculate SHA-1: %w", err) + } + s := hex.EncodeToString(h.Sum(nil)) + prop, err := p.client.SearchBySHA1(s) + if err != nil { + return Properties{}, err + } + prop.FilePath = filePath + return prop, nil +} + +func isArtifact(name string) bool { + ext := filepath.Ext(name) + if ext == ".jar" || ext == ".ear" || ext == ".war" { + return true + } + return false +} + +func parseFileName(filePath string) Properties { + fileName := filepath.Base(filePath) + packageVersion := jarFileRegEx.FindStringSubmatch(fileName) + if len(packageVersion) != 3 { + return Properties{} + } + + return Properties{ + ArtifactID: packageVersion[1], + Version: packageVersion[2], + FilePath: filePath, + } +} + +func parsePomProperties(f *zip.File, filePath string) (Properties, error) { + file, err := f.Open() + if err != nil { + return Properties{}, xerrors.Errorf("unable to open pom.properties: %w", err) + } + defer file.Close() + + p := Properties{ + FilePath: filePath, + } + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + switch { + case strings.HasPrefix(line, "groupId="): + p.GroupID = strings.TrimPrefix(line, "groupId=") + case strings.HasPrefix(line, "artifactId="): + p.ArtifactID = strings.TrimPrefix(line, "artifactId=") + case strings.HasPrefix(line, "version="): + p.Version = strings.TrimPrefix(line, "version=") + } + } + + if err = scanner.Err(); err != nil { + return Properties{}, xerrors.Errorf("scan error: %w", err) + } + return p, nil +} + +type manifest struct { + implementationVersion string + implementationTitle string + implementationVendor string + implementationVendorId string + specificationTitle string + specificationVersion string + specificationVendor string + bundleName string + bundleVersion string + bundleSymbolicName string +} + +func parseManifest(f *zip.File) (manifest, error) { + file, err := f.Open() + if err != nil { + return manifest{}, xerrors.Errorf("unable to open MANIFEST.MF: %w", err) + } + defer file.Close() + + var m manifest + scanner := bufio.NewScanner(file) + for scanner.Scan() { + line := scanner.Text() + + // Skip variables. e.g. Bundle-Name: %bundleName + ss := strings.Fields(line) + if len(ss) <= 1 || (len(ss) > 1 && strings.HasPrefix(ss[1], "%")) { + continue + } + + // It is not determined which fields are present in each application. + // In some cases, none of them are included, in which case they cannot be detected. + switch { + case strings.HasPrefix(line, "Implementation-Version:"): + m.implementationVersion = strings.TrimPrefix(line, "Implementation-Version:") + case strings.HasPrefix(line, "Implementation-Title:"): + m.implementationTitle = strings.TrimPrefix(line, "Implementation-Title:") + case strings.HasPrefix(line, "Implementation-Vendor:"): + m.implementationVendor = strings.TrimPrefix(line, "Implementation-Vendor:") + case strings.HasPrefix(line, "Implementation-Vendor-Id:"): + m.implementationVendorId = strings.TrimPrefix(line, "Implementation-Vendor-Id:") + case strings.HasPrefix(line, "Specification-Version:"): + m.specificationVersion = strings.TrimPrefix(line, "Specification-Version:") + case strings.HasPrefix(line, "Specification-Title:"): + m.specificationTitle = strings.TrimPrefix(line, "Specification-Title:") + case strings.HasPrefix(line, "Specification-Vendor:"): + m.specificationVendor = strings.TrimPrefix(line, "Specification-Vendor:") + case strings.HasPrefix(line, "Bundle-Version:"): + m.bundleVersion = strings.TrimPrefix(line, "Bundle-Version:") + case strings.HasPrefix(line, "Bundle-Name:"): + m.bundleName = strings.TrimPrefix(line, "Bundle-Name:") + case strings.HasPrefix(line, "Bundle-SymbolicName:"): + m.bundleSymbolicName = strings.TrimPrefix(line, "Bundle-SymbolicName:") + } + } + + if err = scanner.Err(); err != nil { + return manifest{}, xerrors.Errorf("scan error: %w", err) + } + return m, nil +} + +func (m manifest) properties(filePath string) Properties { + groupID, err := m.determineGroupID() + if err != nil { + return Properties{} + } + + artifactID, err := m.determineArtifactID() + if err != nil { + return Properties{} + } + + version, err := m.determineVersion() + if err != nil { + return Properties{} + } + + return Properties{ + GroupID: groupID, + ArtifactID: artifactID, + Version: version, + FilePath: filePath, + } +} + +func (m manifest) determineGroupID() (string, error) { + var groupID string + switch { + case m.implementationVendorId != "": + groupID = m.implementationVendorId + case m.bundleSymbolicName != "": + groupID = m.bundleSymbolicName + + // e.g. "com.fasterxml.jackson.core.jackson-databind" => "com.fasterxml.jackson.core" + idx := strings.LastIndex(m.bundleSymbolicName, ".") + if idx > 0 { + groupID = m.bundleSymbolicName[:idx] + } + case m.implementationVendor != "": + groupID = m.implementationVendor + case m.specificationVendor != "": + groupID = m.specificationVendor + default: + return "", xerrors.New("no groupID found") + } + return strings.TrimSpace(groupID), nil +} + +func (m manifest) determineArtifactID() (string, error) { + var artifactID string + switch { + case m.implementationTitle != "": + artifactID = m.implementationTitle + case m.specificationTitle != "": + artifactID = m.specificationTitle + case m.bundleName != "": + artifactID = m.bundleName + default: + return "", xerrors.New("no artifactID found") + } + return strings.TrimSpace(artifactID), nil +} + +func (m manifest) determineVersion() (string, error) { + var version string + switch { + case m.implementationVersion != "": + version = m.implementationVersion + case m.specificationVersion != "": + version = m.specificationVersion + case m.bundleVersion != "": + version = m.bundleVersion + default: + return "", xerrors.New("no version found") + } + return strings.TrimSpace(version), nil +} + +func removePackageDuplicates(pkgs []ftypes.Package) []ftypes.Package { + // name::filePath => versions + var uniq = make(map[string][]mavenversion.Version) + var uniqPkgs []ftypes.Package + for _, pkg := range pkgs { + uniqID := pkg.Name + "::" + pkg.FilePath + // err is always nil + // cf. https://github.com/masahiro331/go-mvn-version/blob/d3157d602a08806ad94464c443e0cef1370694a1/version.go#L20-L25 + pkgVer, _ := mavenversion.NewVersion(pkg.Version) + savedVers, ok := uniq[uniqID] + if !ok || !slices.ContainsFunc(savedVers, func(v mavenversion.Version) bool { + // There are times when patch `0` is omitted. + // So we can't compare versions just as strings + // for example `2.17.0` and `2.17` must be equal + return v.Equal(pkgVer) + }) { + uniq[uniqID] = append(uniq[uniqID], pkgVer) + uniqPkgs = append(uniqPkgs, pkg) + } + } + return uniqPkgs +} diff --git a/pkg/dependency/parser/java/jar/parse_test.go b/pkg/dependency/parser/java/jar/parse_test.go new file mode 100644 index 000000000000..4fdcf105453f --- /dev/null +++ b/pkg/dependency/parser/java/jar/parse_test.go @@ -0,0 +1,360 @@ +package jar_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "sort" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/java/jar" + "github.com/aquasecurity/trivy/pkg/dependency/parser/java/jar/sonatype" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + // cd testdata/testimage/maven && docker build -t test . + // docker run --rm --name test -it test bash + // mvn dependency:list + // mvn dependency:tree -Dscope=compile -Dscope=runtime | awk '/:tree/,/BUILD SUCCESS/' | awk 'NR > 1 { print }' | head -n -2 | awk '{print $NF}' | awk -F":" '{printf("{\""$1":"$2"\", \""$4 "\", \"\"},\n")}' + // paths filled in manually + wantMaven = []ftypes.Package{ + { + Name: "com.example:web-app", + Version: "1.0-SNAPSHOT", + FilePath: "testdata/maven.war", + }, + { + Name: "com.fasterxml.jackson.core:jackson-databind", + Version: "2.9.10.6", + FilePath: "testdata/maven.war/WEB-INF/lib/jackson-databind-2.9.10.6.jar", + }, + { + Name: "com.fasterxml.jackson.core:jackson-annotations", + Version: "2.9.10", + FilePath: "testdata/maven.war/WEB-INF/lib/jackson-annotations-2.9.10.jar", + }, + { + Name: "com.fasterxml.jackson.core:jackson-core", + Version: "2.9.10", + FilePath: "testdata/maven.war/WEB-INF/lib/jackson-core-2.9.10.jar", + }, + { + Name: "com.cronutils:cron-utils", + Version: "9.1.2", + FilePath: "testdata/maven.war/WEB-INF/lib/cron-utils-9.1.2.jar", + }, + { + Name: "org.slf4j:slf4j-api", + Version: "1.7.30", + FilePath: "testdata/maven.war/WEB-INF/lib/slf4j-api-1.7.30.jar", + }, + { + Name: "org.glassfish:javax.el", + Version: "3.0.0", + FilePath: "testdata/maven.war/WEB-INF/lib/javax.el-3.0.0.jar", + }, + { + Name: "org.apache.commons:commons-lang3", + Version: "3.11", + FilePath: "testdata/maven.war/WEB-INF/lib/commons-lang3-3.11.jar", + }, + } + + // cd testdata/testimage/gradle && docker build -t test . + // docker run --rm --name test -it test bash + // gradle app:dependencies --configuration implementation | grep "[+\]---" | cut -d" " -f2 | awk -F":" '{printf("{\""$1":"$2"\", \""$3"\", \"\"},\n")}' + // paths filled in manually + wantGradle = []ftypes.Package{ + { + Name: "commons-dbcp:commons-dbcp", + Version: "1.4", + FilePath: "testdata/gradle.war/WEB-INF/lib/commons-dbcp-1.4.jar", + }, + { + Name: "commons-pool:commons-pool", + Version: "1.6", + FilePath: "testdata/gradle.war/WEB-INF/lib/commons-pool-1.6.jar", + }, + { + Name: "log4j:log4j", + Version: "1.2.17", + FilePath: "testdata/gradle.war/WEB-INF/lib/log4j-1.2.17.jar", + }, + { + Name: "org.apache.commons:commons-compress", + Version: "1.19", + FilePath: "testdata/gradle.war/WEB-INF/lib/commons-compress-1.19.jar", + }, + } + + // manually created + wantSHA1 = []ftypes.Package{ + { + Name: "org.springframework:spring-core", + Version: "5.3.3", + FilePath: "testdata/test.jar", + }, + } + + // offline + wantOffline = []ftypes.Package{ + { + Name: "org.springframework:Spring Framework", + Version: "2.5.6.SEC03", + FilePath: "testdata/test.jar", + }, + } + + // manually created + wantHeuristic = []ftypes.Package{ + { + Name: "com.example:heuristic", + Version: "1.0.0-SNAPSHOT", + FilePath: "testdata/heuristic-1.0.0-SNAPSHOT.jar", + }, + } + + // manually created + wantFatjar = []ftypes.Package{ + { + Name: "com.google.guava:failureaccess", + Version: "1.0.1", + FilePath: "testdata/hadoop-shaded-guava-1.1.0-SNAPSHOT.jar", + }, + { + Name: "com.google.guava:guava", + Version: "29.0-jre", + FilePath: "testdata/hadoop-shaded-guava-1.1.0-SNAPSHOT.jar", + }, + { + Name: "com.google.guava:listenablefuture", + Version: "9999.0-empty-to-avoid-conflict-with-guava", + FilePath: "testdata/hadoop-shaded-guava-1.1.0-SNAPSHOT.jar", + }, + { + Name: "com.google.j2objc:j2objc-annotations", + Version: "1.3", + FilePath: "testdata/hadoop-shaded-guava-1.1.0-SNAPSHOT.jar", + }, + { + Name: "org.apache.hadoop.thirdparty:hadoop-shaded-guava", + Version: "1.1.0-SNAPSHOT", + FilePath: "testdata/hadoop-shaded-guava-1.1.0-SNAPSHOT.jar", + }, + } + + // manually created + wantNestedJar = []ftypes.Package{ + { + Name: "test:nested", + Version: "0.0.1", + FilePath: "testdata/nested.jar", + }, + { + Name: "test:nested2", + Version: "0.0.2", + FilePath: "testdata/nested.jar/META-INF/jars/nested2.jar", + }, + { + Name: "test:nested3", + Version: "0.0.3", + FilePath: "testdata/nested.jar/META-INF/jars/nested2.jar/META-INF/jars/nested3.jar", + }, + } + + // Manually created. + // Files of `io.quarkus.gizmo.gizmo-1.1.jar` (gizmo:1.1.0 (from sha1)): + //├── bar + //│ ├── bar + //│ │ └── pom.properties (jackson-databind:2.13.4) + //│ └── foo + //│ └── pom.properties (jackson-databind:2.12.3) + //├── foo + //│ ├── bar + //│ │ └── pom.properties (jackson-databind:2.12.3) + //│ └── foo + //│ └── pom.properties (jackson-databind:2.13.4) + //├── jars + //│ ├── log4j-1.2.16.jar (log4j:1.2.16) + //│ └── log4j-1.2.17.jar (log4j:1.2.17) + //└── META-INF + // ├── INDEX.LIST + // ├── MANIFEST.MF + // └── maven + // └── io.quarkus.gizmo + // └── gizmo + // ├── pom.properties (gizmo:1.1) + // └── pom.xml + wantDuplicatesJar = []ftypes.Package{ + { + Name: "io.quarkus.gizmo:gizmo", + Version: "1.1", + FilePath: "testdata/io.quarkus.gizmo.gizmo-1.1.jar", + }, + { + Name: "log4j:log4j", + Version: "1.2.16", + FilePath: "testdata/io.quarkus.gizmo.gizmo-1.1.jar/jars/log4j-1.2.16.jar", + }, + { + Name: "log4j:log4j", + Version: "1.2.17", + FilePath: "testdata/io.quarkus.gizmo.gizmo-1.1.jar/jars/log4j-1.2.17.jar", + }, + { + Name: "com.fasterxml.jackson.core:jackson-databind", + Version: "2.12.3", + FilePath: "testdata/io.quarkus.gizmo.gizmo-1.1.jar", + }, + { + Name: "com.fasterxml.jackson.core:jackson-databind", + Version: "2.13.4", + FilePath: "testdata/io.quarkus.gizmo.gizmo-1.1.jar", + }, + } +) + +type apiResponse struct { + Response response `json:"response"` +} + +type response struct { + NumFound int `json:"numFound"` + Docs []doc `json:"docs"` +} + +type doc struct { + ID string `json:"id"` + GroupID string `json:"g"` + ArtifactID string `json:"a"` + Version string `json:"v"` + P string `json:"p"` + VersionCount int `json:"versionCount"` +} + +func TestParse(t *testing.T) { + vectors := []struct { + name string + file string // Test input file + offline bool + want []ftypes.Package + }{ + { + name: "maven", + file: "testdata/maven.war", + want: wantMaven, + }, + { + name: "gradle", + file: "testdata/gradle.war", + want: wantGradle, + }, + { + name: "nested jars", + file: "testdata/nested.jar", + want: wantNestedJar, + }, + { + name: "sha1 search", + file: "testdata/test.jar", + want: wantSHA1, + }, + { + name: "offline", + file: "testdata/test.jar", + offline: true, + want: wantOffline, + }, + { + name: "artifactId search", + file: "testdata/heuristic-1.0.0-SNAPSHOT.jar", + want: wantHeuristic, + }, + { + name: "fat jar", + file: "testdata/hadoop-shaded-guava-1.1.0-SNAPSHOT.jar", + want: wantFatjar, + }, + { + name: "duplicate libraries", + file: "testdata/io.quarkus.gizmo.gizmo-1.1.jar", + want: wantDuplicatesJar, + }, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + res := apiResponse{ + Response: response{ + NumFound: 1, + }, + } + + switch { + case strings.Contains(r.URL.Query().Get("q"), "springframework"): + res.Response.NumFound = 0 + case strings.Contains(r.URL.Query().Get("q"), "c666f5bc47eb64ed3bbd13505a26f58be71f33f0"): + res.Response.Docs = []doc{ + { + ID: "org.springframework.spring-core", + GroupID: "org.springframework", + ArtifactID: "spring-core", + Version: "5.3.3", + }, + } + case strings.Contains(r.URL.Query().Get("q"), "Gizmo"): + res.Response.NumFound = 0 + case strings.Contains(r.URL.Query().Get("q"), "1c78bbc4d8c58b9af8eee82b84f2c26ec48e9a2b"): + res.Response.Docs = []doc{ + { + ID: "io.quarkus.gizmo.gizmo", + GroupID: "io.quarkus.gizmo", + ArtifactID: "gizmo", + Version: "1.1.0", + }, + } + case strings.Contains(r.URL.Query().Get("q"), "heuristic"): + res.Response.Docs = []doc{ + { + ID: "org.springframework.heuristic", + GroupID: "org.springframework", + ArtifactID: "heuristic", + VersionCount: 10, + }, + { + ID: "com.example.heuristic", + GroupID: "com.example", + ArtifactID: "heuristic", + VersionCount: 100, + }, + } + } + _ = json.NewEncoder(w).Encode(res) + })) + + for _, v := range vectors { + t.Run(v.name, func(t *testing.T) { + f, err := os.Open(v.file) + require.NoError(t, err) + + stat, err := f.Stat() + require.NoError(t, err) + + c := sonatype.New(sonatype.WithURL(ts.URL), sonatype.WithHTTPClient(ts.Client())) + p := jar.NewParser(c, jar.WithFilePath(v.file), jar.WithOffline(v.offline), jar.WithSize(stat.Size())) + + got, _, err := p.Parse(t.Context(), f) + require.NoError(t, err) + + sort.Sort(ftypes.Packages(got)) + sort.Sort(ftypes.Packages(v.want)) + + assert.Equal(t, v.want, got) + }) + } +} diff --git a/pkg/dependency/parser/java/jar/sonatype/log.go b/pkg/dependency/parser/java/jar/sonatype/log.go new file mode 100644 index 000000000000..78f7f04824c3 --- /dev/null +++ b/pkg/dependency/parser/java/jar/sonatype/log.go @@ -0,0 +1,30 @@ +package sonatype + +import ( + "context" + "log/slog" + + "github.com/aquasecurity/trivy/pkg/log" +) + +// logger implements LeveledLogger +// https://github.com/hashicorp/go-retryablehttp/blob/991b9d0a42d13014e3689dd49a94c02be01f4237/client.go#L285-L290 +type handler struct { + slog.Handler +} + +func newLogger() *log.Logger { + return log.New(&handler{slog.Default().Handler()}).With(log.Prefix("sonatype")) +} + +func (h *handler) Handle(ctx context.Context, r slog.Record) error { + switch r.Message { + case "request failed": + // Use Debug to suppress errors on failure + r.Level = log.LevelDebug + case "performing request": + // This message is displayed too much + return nil + } + return h.Handler.Handle(ctx, r) +} diff --git a/pkg/dependency/parser/java/jar/sonatype/sonatype.go b/pkg/dependency/parser/java/jar/sonatype/sonatype.go new file mode 100644 index 000000000000..ebf13f96e42b --- /dev/null +++ b/pkg/dependency/parser/java/jar/sonatype/sonatype.go @@ -0,0 +1,196 @@ +package sonatype + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + "sort" + "time" + + "github.com/hashicorp/go-retryablehttp" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/java/jar" +) + +const ( + baseURL = "https://search.maven.org/solrsearch/select" + idQuery = `g:"%s" AND a:"%s"` + artifactIdQuery = `a:"%s" AND p:"jar"` + sha1Query = `1:"%s"` +) + +type apiResponse struct { + Response struct { + NumFound int `json:"numFound"` + Docs []struct { + ID string `json:"id"` + GroupID string `json:"g"` + ArtifactID string `json:"a"` + Version string `json:"v"` + P string `json:"p"` + VersionCount int `json:"versionCount"` + } `json:"docs"` + } `json:"response"` +} + +type Sonatype struct { + baseURL string + httpClient *http.Client +} + +type Option func(*Sonatype) + +func WithURL(url string) Option { + return func(p *Sonatype) { + p.baseURL = url + } +} + +func WithHTTPClient(client *http.Client) Option { + return func(p *Sonatype) { + p.httpClient = client + } +} + +func New(opts ...Option) Sonatype { + // for HTTP retry + retryClient := retryablehttp.NewClient() + retryClient.Logger = newLogger() + retryClient.RetryWaitMin = 20 * time.Second + retryClient.RetryWaitMax = 5 * time.Minute + retryClient.RetryMax = 5 + client := retryClient.StandardClient() + + // attempt to read the maven central api url from os environment, if it's + // not set use the default + mavenURL, ok := os.LookupEnv("MAVEN_CENTRAL_URL") + if !ok { + mavenURL = baseURL + } + + s := Sonatype{ + baseURL: mavenURL, + httpClient: client, + } + + for _, opt := range opts { + opt(&s) + } + + return s +} + +func (s Sonatype) Exists(groupID, artifactID string) (bool, error) { + req, err := http.NewRequest(http.MethodGet, s.baseURL, http.NoBody) + if err != nil { + return false, xerrors.Errorf("unable to initialize HTTP client: %w", err) + } + + q := req.URL.Query() + q.Set("q", fmt.Sprintf(idQuery, groupID, artifactID)) + q.Set("rows", "1") + req.URL.RawQuery = q.Encode() + + resp, err := s.httpClient.Do(req) + if err != nil { + return false, xerrors.Errorf("http error: %w", err) + } + defer resp.Body.Close() + + var res apiResponse + if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + return false, xerrors.Errorf("json decode error: %w", err) + } + return res.Response.NumFound > 0, nil +} + +func (s Sonatype) SearchBySHA1(sha1 string) (jar.Properties, error) { + + req, err := http.NewRequest(http.MethodGet, s.baseURL, http.NoBody) + if err != nil { + return jar.Properties{}, xerrors.Errorf("unable to initialize HTTP client: %w", err) + } + + q := req.URL.Query() + q.Set("q", fmt.Sprintf(sha1Query, sha1)) + q.Set("rows", "1") + q.Set("wt", "json") + req.URL.RawQuery = q.Encode() + + resp, err := s.httpClient.Do(req) + if err != nil { + return jar.Properties{}, xerrors.Errorf("sha1 search error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return jar.Properties{}, xerrors.Errorf("status %s from %s", resp.Status, req.URL.String()) + } + + var res apiResponse + if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + return jar.Properties{}, xerrors.Errorf("json decode error: %w", err) + } + + if len(res.Response.Docs) == 0 { + return jar.Properties{}, xerrors.Errorf("digest %s: %w", sha1, jar.ArtifactNotFoundErr) + } + + // Some artifacts might have the same SHA-1 digests. + // e.g. "javax.servlet:jstl" and "jstl:jstl" + docs := res.Response.Docs + sort.Slice(docs, func(i, j int) bool { + return docs[i].ID < docs[j].ID + }) + d := docs[0] + + return jar.Properties{ + GroupID: d.GroupID, + ArtifactID: d.ArtifactID, + Version: d.Version, + }, nil +} + +func (s Sonatype) SearchByArtifactID(artifactID, _ string) (string, error) { + req, err := http.NewRequest(http.MethodGet, s.baseURL, http.NoBody) + if err != nil { + return "", xerrors.Errorf("unable to initialize HTTP client: %w", err) + } + + q := req.URL.Query() + q.Set("q", fmt.Sprintf(artifactIdQuery, artifactID)) + q.Set("rows", "20") + q.Set("wt", "json") + req.URL.RawQuery = q.Encode() + + resp, err := s.httpClient.Do(req) + if err != nil { + return "", xerrors.Errorf("artifactID search error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return "", xerrors.Errorf("status %s from %s", resp.Status, req.URL.String()) + } + + var res apiResponse + if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + return "", xerrors.Errorf("json decode error: %w", err) + } + + if len(res.Response.Docs) == 0 { + return "", xerrors.Errorf("artifactID %s: %w", artifactID, jar.ArtifactNotFoundErr) + } + + // Some artifacts might have the same artifactId. + // e.g. "javax.servlet:jstl" and "jstl:jstl" + docs := res.Response.Docs + sort.Slice(docs, func(i, j int) bool { + return docs[i].VersionCount > docs[j].VersionCount + }) + d := docs[0] + + return d.GroupID, nil +} diff --git a/pkg/dependency/parser/java/jar/testdata/gradle.war b/pkg/dependency/parser/java/jar/testdata/gradle.war new file mode 100644 index 000000000000..3564bcee40f0 Binary files /dev/null and b/pkg/dependency/parser/java/jar/testdata/gradle.war differ diff --git a/pkg/dependency/parser/java/jar/testdata/hadoop-shaded-guava-1.1.0-SNAPSHOT.jar b/pkg/dependency/parser/java/jar/testdata/hadoop-shaded-guava-1.1.0-SNAPSHOT.jar new file mode 100644 index 000000000000..793c4845e727 Binary files /dev/null and b/pkg/dependency/parser/java/jar/testdata/hadoop-shaded-guava-1.1.0-SNAPSHOT.jar differ diff --git a/pkg/dependency/parser/java/jar/testdata/heuristic-1.0.0-SNAPSHOT.jar b/pkg/dependency/parser/java/jar/testdata/heuristic-1.0.0-SNAPSHOT.jar new file mode 100644 index 000000000000..1922bc51ab3a Binary files /dev/null and b/pkg/dependency/parser/java/jar/testdata/heuristic-1.0.0-SNAPSHOT.jar differ diff --git a/pkg/dependency/parser/java/jar/testdata/io.quarkus.gizmo.gizmo-1.1.jar b/pkg/dependency/parser/java/jar/testdata/io.quarkus.gizmo.gizmo-1.1.jar new file mode 100644 index 000000000000..3b82fae55ec9 Binary files /dev/null and b/pkg/dependency/parser/java/jar/testdata/io.quarkus.gizmo.gizmo-1.1.jar differ diff --git a/pkg/dependency/parser/java/jar/testdata/maven.war b/pkg/dependency/parser/java/jar/testdata/maven.war new file mode 100644 index 000000000000..787d517174b7 Binary files /dev/null and b/pkg/dependency/parser/java/jar/testdata/maven.war differ diff --git a/pkg/dependency/parser/java/jar/testdata/nested.jar b/pkg/dependency/parser/java/jar/testdata/nested.jar new file mode 100644 index 000000000000..8d0c14fe52dc Binary files /dev/null and b/pkg/dependency/parser/java/jar/testdata/nested.jar differ diff --git a/pkg/dependency/parser/java/jar/testdata/test.jar b/pkg/dependency/parser/java/jar/testdata/test.jar new file mode 100644 index 000000000000..c48cc32ee7b2 Binary files /dev/null and b/pkg/dependency/parser/java/jar/testdata/test.jar differ diff --git a/pkg/dependency/parser/java/jar/testdata/testimage/gradle/Dockerfile b/pkg/dependency/parser/java/jar/testdata/testimage/gradle/Dockerfile new file mode 100644 index 000000000000..cbc82c322976 --- /dev/null +++ b/pkg/dependency/parser/java/jar/testdata/testimage/gradle/Dockerfile @@ -0,0 +1,4 @@ +FROM gradle:6.8.1-jdk +RUN gradle init --type java-application +COPY build.gradle app/ +RUN gradle war diff --git a/pkg/dependency/parser/java/jar/testdata/testimage/gradle/build.gradle b/pkg/dependency/parser/java/jar/testdata/testimage/gradle/build.gradle new file mode 100644 index 000000000000..9ea187a36d53 --- /dev/null +++ b/pkg/dependency/parser/java/jar/testdata/testimage/gradle/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'application' + id 'war' +} + +repositories { + // Use JCenter for resolving dependencies. + jcenter() +} + +dependencies { + // Use JUnit test framework. + testImplementation 'junit:junit:4.13' + + implementation 'commons-dbcp:commons-dbcp:1.4' + implementation 'commons-pool:commons-pool:1.6' + implementation 'log4j:log4j:1.2.17' + implementation 'org.apache.commons:commons-compress:1.19' +} + +application { + // Define the main class for the application. + mainClass = 'gradle.App' +} + diff --git a/pkg/dependency/parser/java/jar/testdata/testimage/maven/Dockerfile b/pkg/dependency/parser/java/jar/testdata/testimage/maven/Dockerfile new file mode 100644 index 000000000000..3f327544b293 --- /dev/null +++ b/pkg/dependency/parser/java/jar/testdata/testimage/maven/Dockerfile @@ -0,0 +1,8 @@ +FROM maven:3.6.3-jdk-11 + +RUN mvn archetype:generate -DgroupId=com.example -DartifactId=web-app -DarchetypeArtifactId=maven-archetype-webapp -DinteractiveMode=false +WORKDIR /web-app +COPY pom.xml . +RUN mvn clean install && mvn package + +CMD ["mvn", "dependency:tree"] diff --git a/pkg/dependency/parser/java/jar/testdata/testimage/maven/pom.xml b/pkg/dependency/parser/java/jar/testdata/testimage/maven/pom.xml new file mode 100644 index 000000000000..e7ffc52b67cb --- /dev/null +++ b/pkg/dependency/parser/java/jar/testdata/testimage/maven/pom.xml @@ -0,0 +1,32 @@ + + 4.0.0 + com.example + web-app + war + 1.0-SNAPSHOT + web-app Maven Webapp + http://maven.apache.org + + + junit + junit + 3.8.1 + test + + + com.fasterxml.jackson.core + jackson-databind + 2.9.10.6 + + + com.cronutils + cron-utils + 9.1.2 + + + + web-app + + + diff --git a/pkg/dependency/parser/java/jar/types.go b/pkg/dependency/parser/java/jar/types.go new file mode 100644 index 000000000000..4246bbceb27f --- /dev/null +++ b/pkg/dependency/parser/java/jar/types.go @@ -0,0 +1,34 @@ +package jar + +import ( + "fmt" + + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ArtifactNotFoundErr = xerrors.New("no artifact found") + +type Properties struct { + GroupID string + ArtifactID string + Version string + FilePath string // path to file containing these props +} + +func (p Properties) Package() ftypes.Package { + return ftypes.Package{ + Name: fmt.Sprintf("%s:%s", p.GroupID, p.ArtifactID), + Version: p.Version, + FilePath: p.FilePath, + } +} + +func (p Properties) Valid() bool { + return p.GroupID != "" && p.ArtifactID != "" && p.Version != "" +} + +func (p Properties) String() string { + return fmt.Sprintf("%s:%s:%s", p.GroupID, p.ArtifactID, p.Version) +} diff --git a/pkg/dependency/parser/java/pom/artifact.go b/pkg/dependency/parser/java/pom/artifact.go new file mode 100644 index 000000000000..c33d1bce44c5 --- /dev/null +++ b/pkg/dependency/parser/java/pom/artifact.go @@ -0,0 +1,138 @@ +package pom + +import ( + "fmt" + "strings" + "sync" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/version/doc" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +var ( + emptyVersionWarn = sync.OnceFunc(func() { + log.WithPrefix("pom").Warn("Dependency version cannot be determined. Child dependencies will not be found.", + // e.g. https://trivy.dev/docs/latest/coverage/language/java/#empty-dependency-version + log.String("details", doc.URL("guide/coverage/language/java/", "empty-dependency-version"))) + }) +) + +type artifact struct { + GroupID string + ArtifactID string + Version version + Licenses []string + + Exclusions set.Set[string] + + Module bool + Relationship ftypes.Relationship + + Locations ftypes.Locations + + // For correctly calculation package ID (hash), + // We need to store the file paths for root or module artifacts. + // For other artifacts, it will be empty. + RootFilePath string + + // Repositories got from current POM, upper-level POMs and parent POMs. + Repositories []repository +} + +func newArtifact(groupID, artifactID, version string, licenses []string, props map[string]string) artifact { + return artifact{ + GroupID: evaluateVariable(groupID, props, nil), + ArtifactID: evaluateVariable(artifactID, props, nil), + Version: newVersion(evaluateVariable(version, props, nil)), + Licenses: licenses, + Relationship: ftypes.RelationshipIndirect, // default + } +} + +func (a artifact) IsEmpty() bool { + if a.GroupID == "" || a.ArtifactID == "" { + return true + } + if a.Version.String() == "" { + emptyVersionWarn() + log.WithPrefix("pom").Debug("Dependency version cannot be determined.", + log.String("GroupID", a.GroupID), + log.String("ArtifactID", a.ArtifactID), + ) + } + return false +} + +func (a artifact) Equal(o artifact) bool { + return a.GroupID == o.GroupID || a.ArtifactID == o.ArtifactID || a.Version.String() == o.Version.String() +} + +func (a artifact) ToPOMLicenses() pomLicenses { + return pomLicenses{ + License: xslices.Map(a.Licenses, func(lic string) pomLicense { + return pomLicense{Name: lic} + }), + } +} + +func (a artifact) Inherit(parent artifact) artifact { + // inherited from a parent + if a.GroupID == "" { + a.GroupID = parent.GroupID + } + + if len(a.Licenses) == 0 { + a.Licenses = parent.Licenses + } + + if a.Version.String() == "" { + a.Version = parent.Version + } + return a +} + +func (a artifact) Name() string { + return fmt.Sprintf("%s:%s", a.GroupID, a.ArtifactID) +} + +func (a artifact) String() string { + return fmt.Sprintf("%s:%s", a.Name(), a.Version) +} + +type version struct { + ver string + hard bool +} + +// Only soft and hard requirements for the specified version are supported at the moment. +func newVersion(s string) version { + var hard bool + if strings.HasPrefix(s, "[") && strings.HasSuffix(s, "]") { + s = strings.Trim(s, "[]") + hard = true + } + + // TODO: Other requirements are not supported + if strings.ContainsAny(s, ",()[]") { + s = "" + } + + return version{ + ver: s, + hard: hard, + } +} + +func (v1 version) shouldOverride(v2 version) bool { + if !v1.hard && v2.hard { + return true + } + return false +} + +func (v1 version) String() string { + return v1.ver +} diff --git a/pkg/dependency/parser/java/pom/artifact_test.go b/pkg/dependency/parser/java/pom/artifact_test.go new file mode 100644 index 000000000000..cc8f7ad337de --- /dev/null +++ b/pkg/dependency/parser/java/pom/artifact_test.go @@ -0,0 +1,97 @@ +package pom + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_evaluateVariable(t *testing.T) { + type args struct { + s string + props map[string]string + } + tests := []struct { + name string + args args + want string + }{ + { + name: "happy path", + args: args{ + s: "${java.version}", + props: map[string]string{ + "java.version": "1.7", + }, + }, + want: "1.7", + }, + { + name: "two variables", + args: args{ + s: "${foo.name}-${bar.name}", + props: map[string]string{ + "foo.name": "aaa", + "bar.name": "bbb", + }, + }, + want: "aaa-bbb", + }, + { + name: "looped variables", + args: args{ + s: "${foo.name}", + props: map[string]string{ + "foo.name": "${bar.name}", + "bar.name": "${foo.name}", + }, + }, + want: "", + }, + { + name: "same variables", + args: args{ + s: "${foo.name}-${foo.name}", + props: map[string]string{ + "foo.name": "aaa", + }, + }, + want: "aaa-aaa", + }, + { + name: "nested variables", + args: args{ + s: "${jackson.version.core}", + props: map[string]string{ + "jackson.version": "2.12.1", + "jackson.version.core": "${jackson.version}", + }, + }, + want: "2.12.1", + }, + { + name: "environmental variable", + args: args{ + s: "${env.TEST_GO_DEP_PARSER}", + }, + want: "1.2.3", + }, + { + name: "no variable", + args: args{ + s: "1.12", + }, + want: "1.12", + }, + } + + envName := "TEST_GO_DEP_PARSER" + t.Setenv(envName, "1.2.3") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := evaluateVariable(tt.args.s, tt.args.props, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/java/pom/cache.go b/pkg/dependency/parser/java/pom/cache.go new file mode 100644 index 000000000000..b29673feb02c --- /dev/null +++ b/pkg/dependency/parser/java/pom/cache.go @@ -0,0 +1,21 @@ +package pom + +import "fmt" + +type pomCache map[string]*analysisResult + +func newPOMCache() pomCache { + return pomCache{} +} + +func (c pomCache) put(art artifact, result analysisResult) { + c[c.key(art)] = &result +} + +func (c pomCache) get(art artifact) *analysisResult { + return c[c.key(art)] +} + +func (c pomCache) key(art artifact) string { + return fmt.Sprintf("%s:%s:%s", art.Name(), art.Version, art.RootFilePath) +} diff --git a/pkg/dependency/parser/java/pom/metadata.go b/pkg/dependency/parser/java/pom/metadata.go new file mode 100644 index 000000000000..0a35e9e4f556 --- /dev/null +++ b/pkg/dependency/parser/java/pom/metadata.go @@ -0,0 +1,17 @@ +package pom + +type Metadata struct { + GroupId string `xml:"groupId"` + ArtifactId string `xml:"artifactId"` + Versioning Versioning `xml:"versioning"` + Version string `xml:"version"` +} + +type Versioning struct { + SnapshotVersions []SnapshotVersion `xml:"snapshotVersions>snapshotVersion"` +} + +type SnapshotVersion struct { + Extension string `xml:"extension"` + Value string `xml:"value"` +} diff --git a/pkg/dependency/parser/java/pom/parse.go b/pkg/dependency/parser/java/pom/parse.go new file mode 100644 index 000000000000..728737943a86 --- /dev/null +++ b/pkg/dependency/parser/java/pom/parse.go @@ -0,0 +1,936 @@ +package pom + +import ( + "context" + "encoding/xml" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" + "slices" + "sort" + "strconv" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/mitchellh/hashstructure/v2" + "github.com/samber/lo" + "golang.org/x/net/html/charset" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/utils" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +type options struct { + offline bool + defaultRepo repository + settingsRepos []repository +} + +type option func(*options) + +func WithOffline(offline bool) option { + return func(opts *options) { + opts.offline = offline + } +} + +func WithDefaultRepo(repoURL string, releaseEnabled, snapshotEnabled bool) option { + return func(opts *options) { + u, _ := url.Parse(repoURL) + opts.defaultRepo = repository{ + url: *u, + releaseEnabled: releaseEnabled, + snapshotEnabled: snapshotEnabled, + } + } +} + +func WithSettingsRepos(repoURLs []string, releaseEnabled, snapshotEnabled bool) option { + return func(opts *options) { + opts.settingsRepos = xslices.Map(repoURLs, func(repoURL string) repository { + u, _ := url.Parse(repoURL) + return repository{ + url: *u, + releaseEnabled: releaseEnabled, + snapshotEnabled: snapshotEnabled, + } + }) + } +} + +type Parser struct { + logger *log.Logger + rootPath string + cache pomCache + localRepository string + remoteRepos repositories + offline bool + servers []Server +} + +func NewParser(filePath string, opts ...option) *Parser { + o := &options{ + offline: false, + defaultRepo: mavenCentralRepo, + } + + s := readSettings() + o.settingsRepos = s.effectiveRepositories() + localRepository := s.LocalRepository + if localRepository == "" { + homeDir, _ := os.UserHomeDir() + localRepository = filepath.Join(homeDir, ".m2", "repository") + } + + for _, opt := range opts { + opt(o) + } + + remoteRepos := repositories{ + defaultRepo: o.defaultRepo, + settings: o.settingsRepos, + } + + return &Parser{ + logger: log.WithPrefix("pom"), + rootPath: filepath.Clean(filePath), + cache: newPOMCache(), + localRepository: localRepository, + remoteRepos: remoteRepos, + offline: o.offline, + servers: s.Servers, + } +} + +func (p *Parser) Parse(ctx context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + content, err := parsePom(r, true) + if err != nil { + return nil, nil, xerrors.Errorf("failed to parse POM: %w", err) + } + + root := &pom{ + filePath: p.rootPath, + content: content, + } + + // Analyze root POM + result, err := p.analyze(ctx, root, analysisOptions{ + rootFilePath: p.rootPath, + }) + if err != nil { + return nil, nil, xerrors.Errorf("analyze error (%s): %w", p.rootPath, err) + } + + // Cache root POM + p.cache.put(result.artifact, result) + + rootArt := result.artifact + rootArt.Relationship = ftypes.RelationshipRoot + + return p.parseRoot(ctx, rootArt, set.New[string]()) +} + +// nolint: gocyclo +func (p *Parser) parseRoot(ctx context.Context, root artifact, uniqModules set.Set[string]) ([]ftypes.Package, []ftypes.Dependency, error) { + if root.RootFilePath == "" { + return nil, nil, xerrors.New("root file path is required for package ID generation") + } + + // Prepare a queue for dependencies + queue := newArtifactQueue() + + // Enqueue root POM + root.Module = false + queue.enqueue(root) + + var ( + pkgs ftypes.Packages + deps ftypes.Dependencies + rootDepManagement []pomDependency + uniqArtifacts = make(map[string]artifact) + uniqDeps = make(map[string][]string) + ) + + // Iterate direct and transitive dependencies + for !queue.IsEmpty() { + art := queue.dequeue() + + // Modules should be handled separately so that they can have independent dependencies. + // It means multi-module allows for duplicate dependencies. + if art.Module { + id := packageID(art.Name(), art.Version.String(), art.RootFilePath) + if uniqModules.Contains(id) { + continue + } + uniqModules.Append(id) + + modulePkgs, moduleDeps, err := p.parseRoot(ctx, art, uniqModules) + if err != nil { + return nil, nil, err + } + + pkgs = append(pkgs, modulePkgs...) + if moduleDeps != nil { + deps = append(deps, moduleDeps...) + } + continue + } + + // For soft requirements, skip dependency resolution that has already been resolved. + if uniqueArt, ok := uniqArtifacts[art.Name()]; ok { + if !uniqueArt.Version.shouldOverride(art.Version) { + continue + } + // mark artifact as Direct, if saved artifact is Direct + // take a look `hard requirement for the specified version` test + if uniqueArt.Relationship == ftypes.RelationshipRoot || + uniqueArt.Relationship == ftypes.RelationshipWorkspace || + uniqueArt.Relationship == ftypes.RelationshipDirect { + art.Relationship = uniqueArt.Relationship + } + // We don't need to overwrite dependency location for hard links + if uniqueArt.Locations != nil { + art.Locations = uniqueArt.Locations + } + } + + result, err := p.resolve(ctx, art, rootDepManagement) + if err != nil { + return nil, nil, xerrors.Errorf("resolve error (%s): %w", art, err) + } + + if art.Relationship == ftypes.RelationshipRoot || art.Relationship == ftypes.RelationshipWorkspace { + // Managed dependencies in the root POM affect transitive dependencies + rootDepManagement, err = p.resolveDepManagement(ctx, result.properties, result.dependencyManagement) + if err != nil { + return nil, nil, xerrors.Errorf("unable to resolve dep management: %w", err) + } + + // mark its dependencies as "direct" + result.dependencies = xslices.Map(result.dependencies, func(dep artifact) artifact { + dep.Relationship = ftypes.RelationshipDirect + return dep + }) + } + + // Parse, cache, and enqueue modules. + for _, relativePath := range result.modules { + moduleArtifact, err := p.parseModule(ctx, result.filePath, relativePath, root.Repositories) + if err != nil { + p.logger.Debug("Unable to parse the module", + log.FilePath(result.filePath), log.Err(err)) + continue + } + + queue.enqueue(moduleArtifact) + } + + // Resolve transitive dependencies later + queue.enqueue(result.dependencies...) + + // Offline mode may be missing some fields. + if !art.IsEmpty() { + // Override the version + uniqArtifacts[art.Name()] = artifact{ + Version: art.Version, + Licenses: result.artifact.Licenses, + Relationship: art.Relationship, + Locations: art.Locations, + } + + // save only dependency names + // version will be determined later + dependsOn := xslices.Map(result.dependencies, func(a artifact) string { + return a.Name() + }) + uniqDeps[packageID(art.Name(), art.Version.String(), root.RootFilePath)] = dependsOn + } + } + + // Convert to []ftypes.Package and []ftypes.Dependency + for name, art := range uniqArtifacts { + pkg := ftypes.Package{ + ID: packageID(name, art.Version.String(), root.RootFilePath), + Name: name, + Version: art.Version.String(), + Licenses: art.Licenses, + Relationship: art.Relationship, + Locations: art.Locations, + } + pkgs = append(pkgs, pkg) + + // Convert dependency names into dependency IDs + dependsOn := lo.FilterMap(uniqDeps[pkg.ID], func(dependOnName string, _ int) (string, bool) { + ver := depVersion(dependOnName, uniqArtifacts) + return packageID(dependOnName, ver, root.RootFilePath), ver != "" + }) + + // `mvn` shows modules separately from the root package and does not show module nesting. + // So we can add all modules as dependencies of root package. + if art.Relationship == ftypes.RelationshipRoot { + dependsOn = append(dependsOn, uniqModules.Items()...) + } + + sort.Strings(dependsOn) + if len(dependsOn) > 0 { + deps = append(deps, ftypes.Dependency{ + ID: pkg.ID, + DependsOn: dependsOn, + }) + } + } + + sort.Sort(pkgs) + sort.Sort(deps) + + return pkgs, deps, nil +} + +// depVersion finds dependency in uniqArtifacts and return its version +func depVersion(depName string, uniqArtifacts map[string]artifact) string { + if art, ok := uniqArtifacts[depName]; ok { + return art.Version.String() + } + return "" +} + +func (p *Parser) parseModule(ctx context.Context, currentPath, relativePath string, repos []repository) (artifact, error) { + // modulePath: "root/" + "module/" => "root/module" + module, err := p.openRelativePom(currentPath, relativePath) + if err != nil { + return artifact{}, xerrors.Errorf("unable to open the relative path: %w", err) + } + + result, err := p.analyze(ctx, module, analysisOptions{ + rootFilePath: module.filePath, + repositories: repos, + }) + if err != nil { + return artifact{}, xerrors.Errorf("analyze error: %w", err) + } + + moduleArtifact := module.artifact() + moduleArtifact.Module = true + moduleArtifact.RootFilePath = module.filePath + moduleArtifact.Relationship = ftypes.RelationshipWorkspace + + p.cache.put(moduleArtifact, result) + + return moduleArtifact, nil +} + +func (p *Parser) resolve(ctx context.Context, art artifact, rootDepManagement []pomDependency) (analysisResult, error) { + // If the artifact is found in cache, it is returned. + if result := p.cache.get(art); result != nil { + return *result, nil + } + + // We can't resolve a dependency without a version. + // So let's just keep this dependency. + if art.Version.String() == "" { + return analysisResult{ + artifact: art, + }, nil + } + + p.logger.Debug("Resolving...", log.String("group_id", art.GroupID), + log.String("artifact_id", art.ArtifactID), log.String("version", art.Version.String())) + pomContent, err := p.tryRepository(ctx, art.GroupID, art.ArtifactID, art.Version.String(), art.Repositories) + if err != nil { + if shouldReturnError(err) { + return analysisResult{}, err + } + p.logger.Debug("Repository error", log.Err(err)) + } + + result, err := p.analyze(ctx, pomContent, analysisOptions{ + exclusions: art.Exclusions, + depManagement: rootDepManagement, + rootFilePath: art.RootFilePath, + repositories: art.Repositories, + }) + if err != nil { + return analysisResult{}, xerrors.Errorf("analyze error: %w", err) + } + + p.cache.put(art, result) + return result, nil +} + +type analysisResult struct { + filePath string + artifact artifact + dependencies []artifact + dependencyManagement []pomDependency // Keep the order of dependencies in 'dependencyManagement' + properties map[string]string + modules []string +} + +type analysisOptions struct { + exclusions set.Set[string] + depManagement []pomDependency // from the root POM + rootFilePath string // File path of the root POM or module POM + repositories []repository // Repositories inherited from parent +} + +func (p *Parser) analyze(ctx context.Context, pom *pom, opts analysisOptions) (analysisResult, error) { + if pom.nil() { + return analysisResult{}, nil + } + if opts.exclusions == nil { + opts.exclusions = set.New[string]() + } + + // Get repositories from current POM and merge with inherited ones + pomRepos := pom.repositories(p.servers) + opts.repositories = lo.UniqBy(append(pomRepos, opts.repositories...), func(r repository) url.URL { + return r.url + }) + + // Resolve parent POM + // TODO handle repos from parents + if err := p.resolveParent(ctx, pom, opts.repositories); err != nil { + return analysisResult{}, xerrors.Errorf("pom resolve error: %w", err) + } + + // Resolve dependencies + props := pom.properties() + depManagement := pom.content.DependencyManagement.Dependencies.Dependency + deps, err := p.parseDependencies(ctx, pom.content.Dependencies.Dependency, props, depManagement, opts) + if err != nil { + return analysisResult{}, xerrors.Errorf("unable to parse dependencies: %w", err) + } + deps = p.filterDependencies(deps, opts.exclusions) + + art := pom.artifact() + art.RootFilePath = opts.rootFilePath + art.Repositories = opts.repositories + + return analysisResult{ + filePath: pom.filePath, + artifact: art, + dependencies: deps, + dependencyManagement: depManagement, + properties: props, + modules: pom.content.Modules.Module, + }, nil +} + +// resolveParent resolves its parent POMs and inherits properties, dependencies, and dependencyManagement. +func (p *Parser) resolveParent(ctx context.Context, pom *pom, pomRepos []repository) error { + if pom.nil() { + return nil + } + + // Parse parent POM + parent, err := p.parseParent(ctx, pom.filePath, pom.content.Parent, pomRepos) + if err != nil { + return xerrors.Errorf("parent error: %w", err) + } + + // Inherit values/properties from parent + pom.inherit(parent) + + // Merge properties + pom.content.Properties = p.mergeProperties(pom.content.Properties, parent.content.Properties) + + // Merge dependencyManagement with the following priority: + // 1. Managed dependencies from this POM + // 2. Managed dependencies from parent of this POM + pom.content.DependencyManagement.Dependencies.Dependency = p.mergeDependencyManagements( + pom.content.DependencyManagement.Dependencies.Dependency, + parent.content.DependencyManagement.Dependencies.Dependency) + + // Merge dependencies + pom.content.Dependencies.Dependency = p.mergeDependencies( + pom.content.Dependencies.Dependency, + parent.content.Dependencies.Dependency) + + return nil +} + +func (p *Parser) mergeDependencyManagements(depManagements ...[]pomDependency) []pomDependency { + uniq := set.New[string]() + var depManagement []pomDependency + // The preceding argument takes precedence. + for _, dm := range depManagements { + for _, dep := range dm { + if uniq.Contains(dep.Name()) { + continue + } + depManagement = append(depManagement, dep) + uniq.Append(dep.Name()) + } + } + return depManagement +} + +func (p *Parser) parseDependencies(ctx context.Context, deps []pomDependency, props map[string]string, depManagement []pomDependency, + opts analysisOptions, +) ([]artifact, error) { + // Imported POMs often have no dependencies, so dependencyManagement resolution can be skipped. + if len(deps) == 0 { + return nil, nil + } + + var err error + // Resolve dependencyManagement + depManagement, err = p.resolveDepManagement(ctx, props, depManagement) + if err != nil { + return nil, xerrors.Errorf("unable to resolve dep management: %w", err) + } + + rootDepManagement := opts.depManagement + var dependencies []artifact + for _, d := range deps { + // Resolve dependencies + d = d.Resolve(props, depManagement, rootDepManagement) + + if (d.Scope != "" && d.Scope != "compile" && d.Scope != "runtime") || d.Optional { + continue + } + + dependencies = append(dependencies, d.ToArtifact(opts)) + } + return dependencies, nil +} + +// resolveDepManagement resolves depManagement, including imported POMs. +// It returns resolved depManagement with variables evaluated. +// It continues to resolve even if an error occurs while resolving an imported POM, +// but it returns an error if a context is canceled or the deadline is exceeded. +func (p *Parser) resolveDepManagement(ctx context.Context, props map[string]string, depManagement []pomDependency) ([]pomDependency, error) { + var newDepManagement, imports []pomDependency + for _, dep := range depManagement { + // cf. https://howtodoinjava.com/maven/maven-dependency-scopes/#import + if dep.Scope == "import" { + imports = append(imports, dep) + } else { + // Evaluate variables + newDepManagement = append(newDepManagement, dep.Resolve(props, nil, nil)) + } + } + + // Managed dependencies with a scope of "import" should be processed after other managed dependencies. + // cf. https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#importing-dependencies + for _, imp := range imports { + art := newArtifact(imp.GroupID, imp.ArtifactID, imp.Version, nil, props) + result, err := p.resolve(ctx, art, nil) + if shouldReturnError(err) { + return nil, err + } else if err != nil { + continue + } + + // We need to recursively check all nested depManagements, + // so that we don't miss dependencies on nested depManagements with `Import` scope. + newProps := utils.MergeMaps(props, result.properties) + result.dependencyManagement, err = p.resolveDepManagement(ctx, newProps, result.dependencyManagement) + if err != nil { + return nil, err + } + for k, dd := range result.dependencyManagement { + // Evaluate variables and overwrite dependencyManagement + result.dependencyManagement[k] = dd.Resolve(newProps, nil, nil) + } + newDepManagement = p.mergeDependencyManagements(newDepManagement, result.dependencyManagement) + } + return newDepManagement, nil +} + +func (p *Parser) mergeProperties(child, parent properties) properties { + return lo.Assign(parent, child) +} + +func (p *Parser) mergeDependencies(child, parent []pomDependency) []pomDependency { + return lo.UniqBy(append(child, parent...), func(d pomDependency) string { + return d.Name() + }) +} + +func (p *Parser) filterDependencies(artifacts []artifact, exclusions set.Set[string]) []artifact { + return lo.Filter(artifacts, func(art artifact, _ int) bool { + return !excludeDep(exclusions, art) + }) +} + +func excludeDep(exclusions set.Set[string], art artifact) bool { + if exclusions.Contains(art.Name()) { + return true + } + // Maven can use "*" in GroupID and ArtifactID fields to exclude dependencies + // https://maven.apache.org/pom.html#exclusions + for exlusion := range exclusions.Iter() { + // exclusion format - ":" + e := strings.Split(exlusion, ":") + if (e[0] == art.GroupID || e[0] == "*") && (e[1] == art.ArtifactID || e[1] == "*") { + return true + } + } + return false +} + +func (p *Parser) parseParent(ctx context.Context, currentPath string, parent pomParent, pomRepos []repository) (*pom, error) { + // Pass nil properties so that variables in are not evaluated. + target := newArtifact(parent.GroupId, parent.ArtifactId, parent.Version, nil, nil) + // if version is property (e.g. ${revision}) - we still need to parse this pom + if target.IsEmpty() && !isProperty(parent.Version) { + return &pom{content: &pomXML{}}, nil + } + + logger := p.logger.With("artifact", target.String()) + logger.Debug("Start parent") + defer logger.Debug("Exit parent") + + parentPOM, err := p.retrieveParent(ctx, currentPath, parent.RelativePath, target, pomRepos) + if err != nil { + if shouldReturnError(err) { + return nil, err + } + logger.Debug("Parent POM not found", log.Err(err)) + return &pom{content: &pomXML{}}, nil + } + + if err = p.resolveParent(ctx, parentPOM, pomRepos); err != nil { + return nil, xerrors.Errorf("parent pom resolve error: %w", err) + } + + return parentPOM, nil +} + +func (p *Parser) retrieveParent(ctx context.Context, currentPath, relativePath string, target artifact, pomRepos []repository) (*pom, error) { + var errs error + + // Try relativePath + if relativePath != "" { + pom, err := p.tryRelativePath(ctx, target, currentPath, relativePath, pomRepos) + if err == nil { + return pom, nil + } + errs = multierror.Append(errs, err) + } + + // If not found, search the parent director + pom, err := p.tryRelativePath(ctx, target, currentPath, "../pom.xml", pomRepos) + if err == nil { + return pom, nil + } + errs = multierror.Append(errs, err) + + // If not found, search local/remote remoteRepositories + pom, err = p.tryRepository(ctx, target.GroupID, target.ArtifactID, target.Version.String(), pomRepos) + if err == nil { + return pom, nil + } + errs = multierror.Append(errs, err) + + // Reaching here means the POM wasn't found + return nil, errs +} + +func (p *Parser) tryRelativePath(ctx context.Context, parentArtifact artifact, currentPath, relativePath string, pomRepos []repository) (*pom, error) { + parsedPOM, err := p.openRelativePom(currentPath, relativePath) + if err != nil { + return nil, err + } + + // To avoid an infinite loop or parsing the wrong parent when using relatedPath or `../pom.xml`, + // we need to compare GAV of `parentArtifact` (`parent` tag from base pom) and GAV of pom from `relativePath`. + // See `compare ArtifactIDs for base and parent pom's` test for example. + // But GroupID can be inherited from parent (`p.analyze` function is required to get the GroupID). + // Version can contain a property (`p.analyze` function is required to get the GroupID). + // So we can only match ArtifactID's. + if parsedPOM.artifact().ArtifactID != parentArtifact.ArtifactID { + return nil, xerrors.New("'parent.relativePath' points at wrong local POM") + } + if err := p.resolveParent(ctx, parsedPOM, pomRepos); err != nil { + return nil, xerrors.Errorf("analyze error: %w", err) + } + + if !parentArtifact.Equal(parsedPOM.artifact()) { + return nil, xerrors.New("'parent.relativePath' points at wrong local POM") + } + + return parsedPOM, nil +} + +func (p *Parser) openRelativePom(currentPath, relativePath string) (*pom, error) { + // e.g. child/pom.xml => child/ + dir := filepath.Dir(currentPath) + + // e.g. child + ../parent => parent/ + filePath := filepath.Join(dir, relativePath) + + isDir, err := isDirectory(filePath) + if err != nil { + return nil, err + } else if isDir { + // e.g. parent/ => parent/pom.xml + filePath = filepath.Join(filePath, "pom.xml") + } + + pom, err := p.openPom(filePath) + if err != nil { + return nil, xerrors.Errorf("failed to open %s: %w", filePath, err) + } + return pom, nil +} + +func (p *Parser) openPom(filePath string) (*pom, error) { + f, err := os.Open(filePath) + if err != nil { + return nil, xerrors.Errorf("file open error (%s): %w", filePath, err) + } + defer f.Close() + + content, err := parsePom(f, false) + if err != nil { + return nil, xerrors.Errorf("failed to parse the local POM: %w", err) + } + return &pom{ + filePath: filePath, + content: content, + }, nil +} + +func (p *Parser) tryRepository(ctx context.Context, groupID, artifactID, version string, pomRepos []repository) (*pom, error) { + if version == "" { + return nil, xerrors.Errorf("Version missing for %s:%s", groupID, artifactID) + } + + // Generate a proper path to the pom.xml + // e.g. com.fasterxml.jackson.core, jackson-annotations, 2.10.0 + // => com/fasterxml/jackson/core/jackson-annotations/2.10.0/jackson-annotations-2.10.0.pom + paths := strings.Split(groupID, ".") + paths = append(paths, artifactID, version, fmt.Sprintf("%s-%s.pom", artifactID, version)) + + // Search local remoteRepositories + loaded, err := p.loadPOMFromLocalRepository(paths) + if err == nil { + return loaded, nil + } + + // Search remote remoteRepositories + loaded, err = p.fetchPOMFromRemoteRepositories(ctx, paths, isSnapshot(version), pomRepos) + if err == nil { + return loaded, nil + // We should return error if it's not "not found" error + } else if shouldReturnError(err) { + return nil, err + } + + return nil, xerrors.Errorf("%s:%s:%s was not found in local/remote repositories", groupID, artifactID, version) +} + +func (p *Parser) loadPOMFromLocalRepository(paths []string) (*pom, error) { + paths = append([]string{p.localRepository}, paths...) + localPath := filepath.Join(paths...) + + return p.openPom(localPath) +} + +func (p *Parser) fetchPOMFromRemoteRepositories(ctx context.Context, paths []string, snapshot bool, pomRepos []repository) (*pom, error) { + // Do not try fetching pom.xml from remote repositories in offline mode + if p.offline { + p.logger.Debug("Fetching the remote pom.xml is skipped") + return nil, xerrors.New("offline mode") + } + + // Try all remoteRepositories by following order: + // 1. remoteRepositories from settings.xml + // 2. remoteRepositories from pom.xml (passed as parameter) + // 3. default remoteRepository (Maven Central for Release repository) + for _, repo := range slices.Concat(p.remoteRepos.settings, pomRepos, []repository{p.remoteRepos.defaultRepo}) { + // Skip Release only repositories for snapshot artifacts and vice versa + if snapshot && !repo.snapshotEnabled || !snapshot && !repo.releaseEnabled { + continue + } + + repoPaths := slices.Clone(paths) // Clone slice to avoid overwriting last element of `paths` + if snapshot { + pomFileName, err := p.fetchPomFileNameFromMavenMetadata(ctx, repo.url, repoPaths) + if err != nil { + return nil, xerrors.Errorf("fetch maven-metadata.xml error: %w", err) + } + // Use file name from `maven-metadata.xml` if it exists + if pomFileName != "" { + repoPaths[len(repoPaths)-1] = pomFileName + } + } + fetched, err := p.fetchPOMFromRemoteRepository(ctx, repo.url, repoPaths) + if err != nil { + return nil, xerrors.Errorf("fetch repository error: %w", err) + } else if fetched == nil { + continue + } + return fetched, nil + } + return nil, xerrors.Errorf("the POM was not found in remote remoteRepositories") +} + +func (p *Parser) remoteRepoRequest(ctx context.Context, repoURL url.URL, paths []string) (*http.Request, error) { + paths = append([]string{repoURL.Path}, paths...) + repoURL.Path = path.Join(paths...) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, repoURL.String(), http.NoBody) + if err != nil { + return nil, xerrors.Errorf("unable to create HTTP request: %w", err) + } + if repoURL.User != nil { + password, _ := repoURL.User.Password() + req.SetBasicAuth(repoURL.User.Username(), password) + } + + return req, nil +} + +// fetchPomFileNameFromMavenMetadata fetches `maven-metadata.xml` file to detect file name of pom file. +func (p *Parser) fetchPomFileNameFromMavenMetadata(ctx context.Context, repoURL url.URL, paths []string) (string, error) { + // Overwrite pom file name to `maven-metadata.xml` + mavenMetadataPaths := slices.Clone(paths[:len(paths)-1]) // Clone slice to avoid shadow overwriting last element of `paths` + mavenMetadataPaths = append(mavenMetadataPaths, "maven-metadata.xml") + + req, err := p.remoteRepoRequest(ctx, repoURL, mavenMetadataPaths) + if err != nil { + p.logger.Debug("Unable to create request", log.String("repo", repoURL.Redacted()), log.Err(err)) + return "", nil + } + + client := xhttp.Client() + resp, err := client.Do(req) + if err != nil { + if shouldReturnError(err) { + return "", err + } + p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Err(err)) + return "", nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Int("statusCode", resp.StatusCode)) + return "", nil + } + + mavenMetadata, err := parseMavenMetadata(resp.Body) + if err != nil { + return "", xerrors.Errorf("failed to parse maven-metadata.xml file: %w", err) + } + + var pomFileName string + for _, sv := range mavenMetadata.Versioning.SnapshotVersions { + if sv.Extension == "pom" { + // mavenMetadataPaths[len(mavenMetadataPaths)-3] is always artifactID + pomFileName = fmt.Sprintf("%s-%s.pom", mavenMetadataPaths[len(mavenMetadataPaths)-3], sv.Value) + } + } + + return pomFileName, nil +} + +func (p *Parser) fetchPOMFromRemoteRepository(ctx context.Context, repoURL url.URL, paths []string) (*pom, error) { + req, err := p.remoteRepoRequest(ctx, repoURL, paths) + if err != nil { + p.logger.Debug("Unable to create request", log.String("repo", repoURL.Redacted()), log.Err(err)) + return nil, nil + } + + client := xhttp.Client() + resp, err := client.Do(req) + if err != nil { + if shouldReturnError(err) { + return nil, err + } + p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Err(err)) + return nil, nil + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + p.logger.Debug("Failed to fetch", log.String("url", req.URL.Redacted()), log.Int("statusCode", resp.StatusCode)) + return nil, nil + } + + content, err := parsePom(resp.Body, false) + if err != nil { + return nil, xerrors.Errorf("failed to parse the remote POM: %w", err) + } + + return &pom{ + filePath: "", // from remote repositories + content: content, + }, nil +} + +func parsePom(r io.Reader, lineNumber bool) (*pomXML, error) { + parsed := &pomXML{} + decoder := xml.NewDecoder(r) + decoder.CharsetReader = charset.NewReaderLabel + if err := decoder.Decode(parsed); err != nil { + return nil, xerrors.Errorf("xml decode error: %w", err) + } + if !lineNumber { + for i := range parsed.Dependencies.Dependency { + parsed.Dependencies.Dependency[i].StartLine = 0 + parsed.Dependencies.Dependency[i].EndLine = 0 + } + } + return parsed, nil +} + +func parseMavenMetadata(r io.Reader) (*Metadata, error) { + parsed := &Metadata{} + decoder := xml.NewDecoder(r) + decoder.CharsetReader = charset.NewReaderLabel + if err := decoder.Decode(parsed); err != nil { + return nil, xerrors.Errorf("xml decode error: %w", err) + } + return parsed, nil +} + +func packageID(name, version, pomFilePath string) string { + gav := dependency.ID(ftypes.Pom, name, version) + v := map[string]any{ + "gav": gav, + "path": filepath.ToSlash(pomFilePath), + } + h, err := hashstructure.Hash(v, hashstructure.FormatV2, &hashstructure.HashOptions{ + ZeroNil: true, + IgnoreZeroValue: true, + }) + if err != nil { + log.Warn("Failed to calculate hash", log.Err(err)) + return gav // fallback to GAV only + } + // Append 8-character hash suffix + return fmt.Sprintf("%s::%s", gav, strconv.FormatUint(h, 16)[:8]) +} + +// cf. https://github.com/apache/maven/blob/259404701402230299fe05ee889ecdf1c9dae816/maven-artifact/src/main/java/org/apache/maven/artifact/DefaultArtifact.java#L482-L486 +func isSnapshot(ver string) bool { + return strings.HasSuffix(ver, "SNAPSHOT") || ver == "LATEST" +} + +func isDirectory(path string) (bool, error) { + fileInfo, err := os.Stat(path) + if err != nil { + return false, err + } + return fileInfo.IsDir(), err +} + +func shouldReturnError(err error) bool { + return errors.Is(err, context.DeadlineExceeded) +} diff --git a/pkg/dependency/parser/java/pom/parse_test.go b/pkg/dependency/parser/java/pom/parse_test.go new file mode 100644 index 000000000000..60670a0c803b --- /dev/null +++ b/pkg/dependency/parser/java/pom/parse_test.go @@ -0,0 +1,2630 @@ +package pom_test + +import ( + "bytes" + "fmt" + "io/fs" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/tools/txtar" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/java/pom" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + exampleNestedScopeCompile = func(hash string, start, end int) ftypes.Package { + var location ftypes.Locations + if start != 0 && end != 0 { + location = append(location, ftypes.Location{ + StartLine: start, + EndLine: end, + }) + } + return ftypes.Package{ + ID: fmt.Sprintf("org.example:example-nested-scope-compile:1.0.0::%s", hash), + Name: "org.example:example-nested-scope-compile", + Version: "1.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: location, + } + } + + exampleNestedScopeEmpty = func(hash string, start, end int) ftypes.Package { + var location ftypes.Locations + if start != 0 && end != 0 { + location = append(location, ftypes.Location{ + StartLine: start, + EndLine: end, + }) + } + return ftypes.Package{ + ID: fmt.Sprintf("org.example:example-nested-scope-empty:1.0.0::%s", hash), + Name: "org.example:example-nested-scope-empty", + Version: "1.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: location, + } + } + + exampleNestedScopeRuntime = func(hash string, start, end int) ftypes.Package { + var location ftypes.Locations + if start != 0 && end != 0 { + location = append(location, ftypes.Location{ + StartLine: start, + EndLine: end, + }) + } + return ftypes.Package{ + ID: fmt.Sprintf("org.example:example-nested-scope-runtime:1.0.0::%s", hash), + Name: "org.example:example-nested-scope-runtime", + Version: "1.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: location, + } + } + + exampleScopeCompile = func(hash string) ftypes.Package { + return ftypes.Package{ + ID: fmt.Sprintf("org.example:example-scope-compile:2.0.0::%s", hash), + Name: "org.example:example-scope-compile", + Version: "2.0.0", + Relationship: ftypes.RelationshipIndirect, + } + } + + exampleScopeEmpty = func(hash string) ftypes.Package { + return ftypes.Package{ + ID: fmt.Sprintf("org.example:example-scope-empty:2.0.0::%s", hash), + Name: "org.example:example-scope-empty", + Version: "2.0.0", + Relationship: ftypes.RelationshipIndirect, + } + } + + exampleScopeRuntime = func(hash string) ftypes.Package { + return ftypes.Package{ + ID: fmt.Sprintf("org.example:example-scope-runtime:2.0.0::%s", hash), + Name: "org.example:example-scope-runtime", + Version: "2.0.0", + Relationship: ftypes.RelationshipIndirect, + } + } + exampleApiCompile = func(hash string) ftypes.Package { + return ftypes.Package{ + ID: fmt.Sprintf("org.example:example-api-compile:3.0.0::%s", hash), + Name: "org.example:example-api-compile", + Version: "3.0.0", + Relationship: ftypes.RelationshipIndirect, + } + } + + exampleApiEmpty = func(hash string) ftypes.Package { + return ftypes.Package{ + ID: fmt.Sprintf("org.example:example-api-empty:3.0.0::%s", hash), + Name: "org.example:example-api-empty", + Version: "3.0.0", + Relationship: ftypes.RelationshipIndirect, + } + } + + exampleApiRuntime = func(hash string) ftypes.Package { + return ftypes.Package{ + ID: fmt.Sprintf("org.example:example-api-runtime:3.0.0::%s", hash), + Name: "org.example:example-api-runtime", + Version: "3.0.0", + Relationship: ftypes.RelationshipIndirect, + } + } +) + +func TestPom_Parse(t *testing.T) { + tests := []struct { + name string + inputFile string + local bool + enableRepoForSettingsRepo bool // use another repo for repository from settings.xml + offline bool + want []ftypes.Package + wantDeps []ftypes.Dependency + wantErr string + }{ + { + name: "local repository", + inputFile: filepath.Join("testdata", "happy", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:happy:1.0.0::a302c021", + Name: "com.example:happy", + Version: "1.0.0", + Licenses: []string{"BSD-3-Clause"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::e71631e7", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 32, + EndLine: 36, + }, + }, + }, + { + ID: "org.example:example-runtime:1.0.0::a3bf2630", + Name: "org.example:example-runtime", + Version: "1.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 37, + EndLine: 42, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:happy:1.0.0::a302c021", + DependsOn: []string{ + "org.example:example-api:1.7.30::e71631e7", + "org.example:example-runtime:1.0.0::a3bf2630", + }, + }, + }, + }, + { + name: "remote release repository", + inputFile: filepath.Join("testdata", "happy", "pom.xml"), + local: false, + want: []ftypes.Package{ + { + ID: "com.example:happy:1.0.0::a302c021", + Name: "com.example:happy", + Version: "1.0.0", + Licenses: []string{"BSD-3-Clause"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::e71631e7", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 32, + EndLine: 36, + }, + }, + }, + { + ID: "org.example:example-runtime:1.0.0::a3bf2630", + Name: "org.example:example-runtime", + Version: "1.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 37, + EndLine: 42, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:happy:1.0.0::a302c021", + DependsOn: []string{ + "org.example:example-api:1.7.30::e71631e7", + "org.example:example-runtime:1.0.0::a3bf2630", + }, + }, + }, + }, + { + name: "snapshot dependency", + inputFile: filepath.Join("testdata", "snapshot", "pom.xml"), + local: false, + want: []ftypes.Package{ + { + ID: "com.example:happy:1.0.0::8ccc0def", + Name: "com.example:happy", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency:1.2.3-SNAPSHOT::1f825e0f", + Name: "org.example:example-dependency", + Version: "1.2.3-SNAPSHOT", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 14, + EndLine: 18, + }, + }, + }, + { + ID: "org.example:example-api:2.0.0::23653338", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:happy:1.0.0::8ccc0def", + DependsOn: []string{ + "org.example:example-dependency:1.2.3-SNAPSHOT::1f825e0f", + }, + }, + { + ID: "org.example:example-dependency:1.2.3-SNAPSHOT::1f825e0f", + DependsOn: []string{ + "org.example:example-api:2.0.0::23653338", + }, + }, + }, + }, + { + name: "snapshot repository with maven-metadata.xml", + inputFile: filepath.Join("testdata", "snapshot", "with-maven-metadata", "pom.xml"), + local: false, + want: []ftypes.Package{ + { + ID: "com.example:happy:1.0.0::58fa9f0a", + Name: "com.example:happy", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency:2.17.0-SNAPSHOT::c991922a", + Name: "org.example:example-dependency", + Version: "2.17.0-SNAPSHOT", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 14, + EndLine: 18, + }, + }, + }, + { + ID: "org.example:example-api:2.0.0::d5950bfc", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:happy:1.0.0::58fa9f0a", + DependsOn: []string{ + "org.example:example-dependency:2.17.0-SNAPSHOT::c991922a", + }, + }, + { + ID: "org.example:example-dependency:2.17.0-SNAPSHOT::c991922a", + DependsOn: []string{ + "org.example:example-api:2.0.0::d5950bfc", + }, + }, + }, + }, + { + name: "offline mode", + inputFile: filepath.Join("testdata", "offline", "pom.xml"), + local: false, + offline: true, + want: []ftypes.Package{ + { + ID: "org.example:example-offline:2.3.4::7cc75b41", + Name: "org.example:example-offline", + Version: "2.3.4", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 17, + EndLine: 21, + }, + }, + }, + }, + }, + { + name: "multiple repositories are used", + inputFile: filepath.Join("testdata", "happy", "pom.xml"), + local: false, + enableRepoForSettingsRepo: true, + want: []ftypes.Package{ + { + ID: "com.example:happy:1.0.0::a302c021", + Name: "com.example:happy", + Version: "1.0.0", + Licenses: []string{"BSD-3-Clause"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::e71631e7", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"Custom License from custom repo"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 32, + EndLine: 36, + }, + }, + }, + { + ID: "org.example:example-runtime:1.0.0::a3bf2630", + Name: "org.example:example-runtime", + Version: "1.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 37, + EndLine: 42, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:happy:1.0.0::a302c021", + DependsOn: []string{ + "org.example:example-api:1.7.30::e71631e7", + "org.example:example-runtime:1.0.0::a3bf2630", + }, + }, + }, + }, + { + name: "inherit parent properties", + inputFile: filepath.Join("testdata", "parent-properties", "child", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:child:1.0.0::f99913fa", + Name: "com.example:child", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::c5884361", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 33, + EndLine: 37, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:child:1.0.0::f99913fa", + DependsOn: []string{ + "org.example:example-api:1.7.30::c5884361", + }, + }, + }, + }, + { + name: "inherit project properties from parent", + inputFile: filepath.Join("testdata", "project-version-from-parent", "child", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:child:2.0.0::597860b7", + Name: "com.example:child", + Version: "2.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:2.0.0::9ca5a4d0", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 18, + EndLine: 22, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:child:2.0.0::597860b7", + DependsOn: []string{ + "org.example:example-api:2.0.0::9ca5a4d0", + }, + }, + }, + }, + { + name: "inherit properties in parent depManagement with import scope", + inputFile: filepath.Join("testdata", "inherit-props", "base", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:test:0.0.1-SNAPSHOT::30035f73", + Name: "com.example:test", + Version: "0.0.1-SNAPSHOT", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:2.0.0::ce31c866", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 18, + EndLine: 21, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:test:0.0.1-SNAPSHOT::30035f73", + DependsOn: []string{ + "org.example:example-api:2.0.0::ce31c866", + }, + }, + }, + }, + // [INFO] com.example:child:jar:1.2.3 + // [INFO] +- org.example:example-dependency:jar:1.2.3:compile + // [INFO] | \- org.example:example-api:jar:4.0.0:compile + // [INFO] \- org.example:example-api3:jar:4.0.3:compile + { + name: "dependencyManagement prefers child properties", + inputFile: filepath.Join("testdata", "parent-child-properties", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:child:1.2.3::14cce9f5", + Name: "com.example:child", + Version: "1.2.3", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api3:4.0.3::c4062c26", + Name: "org.example:example-api3", + Version: "4.0.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 30, + EndLine: 34, + }, + }, + }, + { + ID: "org.example:example-dependency:1.2.3::d1f3e5ff", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 25, + EndLine: 29, + }, + }, + }, + { + ID: "org.example:example-api:4.0.0::daf5884b", + Name: "org.example:example-api", + Version: "4.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:child:1.2.3::14cce9f5", + DependsOn: []string{ + "org.example:example-api3:4.0.3::c4062c26", + "org.example:example-dependency:1.2.3::d1f3e5ff", + }, + }, + { + ID: "org.example:example-dependency:1.2.3::d1f3e5ff", + DependsOn: []string{ + "org.example:example-api:4.0.0::daf5884b", + }, + }, + }, + }, + { + name: "inherit parent dependencies", + inputFile: filepath.Join("testdata", "parent-dependencies", "child", "pom.xml"), + local: false, + want: []ftypes.Package{ + { + ID: "com.example:child:1.0.0-SNAPSHOT::cdc3ce21", + Name: "com.example:child", + Version: "1.0.0-SNAPSHOT", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::2f579104", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:child:1.0.0-SNAPSHOT::cdc3ce21", + DependsOn: []string{ + "org.example:example-api:1.7.30::2f579104", + }, + }, + }, + }, + { + name: "inherit parent dependencyManagement", + inputFile: filepath.Join("testdata", "parent-dependency-management", "child", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:child:3.0.0::69b8f328", + Name: "com.example:child", + Version: "3.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::d2229a7d", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 26, + EndLine: 29, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:child:3.0.0::69b8f328", + DependsOn: []string{ + "org.example:example-api:1.7.30::d2229a7d", + }, + }, + }, + }, + { + name: "transitive parents", + inputFile: filepath.Join("testdata", "transitive-parents", "base", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:base:4.0.0::c68788aa", + Name: "com.example:base", + Version: "4.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-child:2.0.0::3bffdbee", + Name: "org.example:example-child", + Version: "2.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 28, + EndLine: 32, + }, + }, + }, + { + ID: "org.example:example-api:1.7.30::a68e9573", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:base:4.0.0::c68788aa", + DependsOn: []string{ + "org.example:example-child:2.0.0::3bffdbee", + }, + }, + { + ID: "org.example:example-child:2.0.0::3bffdbee", + DependsOn: []string{ + "org.example:example-api:1.7.30::a68e9573", + }, + }, + }, + }, + { + name: "parent relativePath", + inputFile: filepath.Join("testdata", "parent-relative-path", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:child:1.0.0::42ad1811", + Name: "com.example:child", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::d5f0ae9b", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 26, + EndLine: 30, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:child:1.0.0::42ad1811", + DependsOn: []string{ + "org.example:example-api:1.7.30::d5f0ae9b", + }, + }, + }, + }, + { + name: "parent version in property", + inputFile: filepath.Join("testdata", "parent-version-is-property", "child", "pom.xml"), + local: false, + want: []ftypes.Package{ + { + ID: "com.example:child:1.0.0-SNAPSHOT::d1a547b0", + Name: "com.example:child", + Version: "1.0.0-SNAPSHOT", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.1.1::a9f8fc7e", + Name: "org.example:example-api", + Version: "1.1.1", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 19, + EndLine: 22, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:child:1.0.0-SNAPSHOT::d1a547b0", + DependsOn: []string{ + "org.example:example-api:1.1.1::a9f8fc7e", + }, + }, + }, + }, + { + name: "parent in a remote repository", + inputFile: filepath.Join("testdata", "parent-remote-repository", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "org.example:child:1.0.0::fad89fc9", + Name: "org.example:child", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::d7e76bdd", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 25, + EndLine: 29, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "org.example:child:1.0.0::fad89fc9", + DependsOn: []string{ + "org.example:example-api:1.7.30::d7e76bdd", + }, + }, + }, + }, + { + // mvn dependency:tree + // [INFO] com.example:soft:jar:1.0.0 + // [INFO] +- org.example:example-api:jar:1.7.30:compile + // [INFO] \- org.example:example-dependency:jar:1.2.3:compile + // Save DependsOn for each package - https://github.com/aquasecurity/go-dep-parser/pull/243#discussion_r1303904548 + name: "soft requirement", + inputFile: filepath.Join("testdata", "soft-requirement", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:soft:1.0.0::8d42a73a", + Name: "com.example:soft", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::2f38d7a0", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 32, + EndLine: 36, + }, + }, + }, + { + ID: "org.example:example-dependency:1.2.3::3c6b5344", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 37, + EndLine: 41, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:soft:1.0.0::8d42a73a", + DependsOn: []string{ + "org.example:example-api:1.7.30::2f38d7a0", + "org.example:example-dependency:1.2.3::3c6b5344", + }, + }, + { + ID: "org.example:example-dependency:1.2.3::3c6b5344", + DependsOn: []string{ + "org.example:example-api:1.7.30::2f38d7a0", + }, + }, + }, + }, + { + // mvn dependency:tree + // [INFO] com.example:soft-transitive:jar:1.0.0 + // [INFO] +- org.example:example-dependency:jar:1.2.3:compile + // [INFO] | \- org.example:example-api:jar:2.0.0:compile + // [INFO] \- org.example:example-dependency2:jar:2.3.4:compile + // Save DependsOn for each package - https://github.com/aquasecurity/go-dep-parser/pull/243#discussion_r1303904548 + name: "soft requirement with transitive dependencies", + inputFile: filepath.Join("testdata", "soft-requirement-with-transitive-dependencies", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:soft-transitive:1.0.0::c0e1772a", + Name: "com.example:soft-transitive", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency:1.2.3::3ce2f1f4", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 13, + EndLine: 17, + }, + }, + }, + { + ID: "org.example:example-dependency2:2.3.4::db8652ac", + Name: "org.example:example-dependency2", + Version: "2.3.4", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 18, + EndLine: 22, + }, + }, + }, + { + ID: "org.example:example-api:2.0.0::497435d5", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:soft-transitive:1.0.0::c0e1772a", + DependsOn: []string{ + "org.example:example-dependency2:2.3.4::db8652ac", + "org.example:example-dependency:1.2.3::3ce2f1f4", + }, + }, + { + ID: "org.example:example-dependency2:2.3.4::db8652ac", + DependsOn: []string{ + "org.example:example-api:2.0.0::497435d5", + }, + }, + { + ID: "org.example:example-dependency:1.2.3::3ce2f1f4", + DependsOn: []string{ + "org.example:example-api:2.0.0::497435d5", + }, + }, + }, + }, + { + // mvn dependency:tree + //[INFO] com.example:hard:jar:1.0.0 + //[INFO] +- org.example:example-nested:jar:3.3.4:compile + //[INFO] \- org.example:example-dependency:jar:1.2.3:compile + //[INFO] \- org.example:example-api:jar:2.0.0:compile + // Save DependsOn for each package - https://github.com/aquasecurity/go-dep-parser/pull/243#discussion_r1303904548 + name: "hard requirement for the specified version", + inputFile: filepath.Join("testdata", "hard-requirement", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:hard:1.0.0::62f5ef2d", + Name: "com.example:hard", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency:1.2.3::31ca2fff", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 33, + EndLine: 37, + }, + }, + }, + { + ID: "org.example:example-nested:3.3.4::8f4a4bcf", + Name: "org.example:example-nested", + Version: "3.3.4", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 28, + EndLine: 32, + }, + }, + }, + { + ID: "org.example:example-api:2.0.0::de424d15", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:hard:1.0.0::62f5ef2d", + DependsOn: []string{ + "org.example:example-dependency:1.2.3::31ca2fff", + "org.example:example-nested:3.3.4::8f4a4bcf", + }, + }, + { + ID: "org.example:example-dependency:1.2.3::31ca2fff", + DependsOn: []string{ + "org.example:example-api:2.0.0::de424d15", + }, + }, + { + ID: "org.example:example-nested:3.3.4::8f4a4bcf", + DependsOn: []string{ + "org.example:example-dependency:1.2.3::31ca2fff", + }, + }, + }, + }, + { + name: "version requirement", + inputFile: filepath.Join("testdata", "version-requirement", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:hard:1.0.0::4675cb8d", + Name: "com.example:hard", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api::ab862f6b", + Name: "org.example:example-api", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 28, + EndLine: 32, + }, + }, + }, + }, + }, + { + name: "import dependencyManagement", + inputFile: filepath.Join("testdata", "import-dependency-management", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:import:2.0.0::5743dcbc", + Name: "com.example:import", + Version: "2.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::6537575e", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 34, + EndLine: 37, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:import:2.0.0::5743dcbc", + DependsOn: []string{ + "org.example:example-api:1.7.30::6537575e", + }, + }, + }, + }, + { + name: "import multiple dependencyManagement", + inputFile: filepath.Join("testdata", "import-dependency-management-multiple", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:import:2.0.0::e3fa6c29", + Name: "com.example:import", + Version: "2.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::807a5be4", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 42, + EndLine: 45, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:import:2.0.0::e3fa6c29", + DependsOn: []string{ + "org.example:example-api:1.7.30::807a5be4", + }, + }, + }, + }, + { + name: "exclusions", + inputFile: filepath.Join("testdata", "exclusions", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:exclusions:3.0.0::5e2d4180", + Name: "com.example:exclusions", + Version: "3.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-nested:3.3.3::7d2a59bf", + Name: "org.example:example-nested", + Version: "3.3.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 14, + EndLine: 28, + }, + }, + }, + { + ID: "org.example:example-dependency:1.2.3::fc52d05e", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:exclusions:3.0.0::5e2d4180", + DependsOn: []string{ + "org.example:example-nested:3.3.3::7d2a59bf", + }, + }, + { + ID: "org.example:example-nested:3.3.3::7d2a59bf", + DependsOn: []string{ + "org.example:example-dependency:1.2.3::fc52d05e", + }, + }, + }, + }, + { + name: "exclusions in child", + inputFile: filepath.Join("testdata", "exclusions-in-child", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:example:1.0.0::1d893cfb", + Name: "com.example:example", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-exclusions:4.0.0::d9903c57", + Name: "org.example:example-exclusions", + Version: "4.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 10, + EndLine: 14, + }, + }, + }, + { + ID: "org.example:example-api:1.7.30::bc3d025d", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "org.example:example-dependency:1.2.3::5bdfcd45", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:example:1.0.0::1d893cfb", + DependsOn: []string{ + "org.example:example-exclusions:4.0.0::d9903c57", + }, + }, + { + ID: "org.example:example-exclusions:4.0.0::d9903c57", + DependsOn: []string{ + "org.example:example-api:1.7.30::bc3d025d", + "org.example:example-dependency:1.2.3::5bdfcd45", + }, + }, + }, + }, + // ➜ mvn dependency:tree + // ... + // [INFO] + // [INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ child --- + // [INFO] com.example:child:jar:3.0.0 + // [INFO] \- org.example:example-exclusions:jar:3.0.0:compile + // [INFO] \- org.example:example-nested:jar:3.3.5:compile + // [INFO] ------------------------------------------------------------------------ + // org.example:example-dependency is excluded via com.example:child (dependencies) + // org.example:example-dependency2 is excluded via com.example:parent (dependencyManagement) + // org.example:example-api2 is excluded via org.example:example-exclusions (dependencies) + // org.example:example-api3 is excluded via com.example:parent (dependencyManagement) + { + name: "exclusions in child and parent dependency management", + inputFile: filepath.Join("testdata", "exclusions-parent-dependency-management", "child", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:child:3.0.0::f51f7e81", + Name: "com.example:child", + Version: "3.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-exclusions:3.0.0::1e4e34b7", + Name: "org.example:example-exclusions", + Version: "3.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 26, + EndLine: 35, + }, + }, + }, + { + ID: "org.example:example-nested:3.3.5::c5a28f33", + Name: "org.example:example-nested", + Version: "3.3.5", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:child:3.0.0::f51f7e81", + DependsOn: []string{ + "org.example:example-exclusions:3.0.0::1e4e34b7", + }, + }, + { + ID: "org.example:example-exclusions:3.0.0::1e4e34b7", + DependsOn: []string{ + "org.example:example-nested:3.3.5::c5a28f33", + }, + }, + }, + }, + { + name: "exclusions with wildcards", + inputFile: filepath.Join("testdata", "wildcard-exclusions", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:wildcard-exclusions:4.0.0::87282928", + Name: "com.example:wildcard-exclusions", + Version: "4.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency:1.2.3::4ee336bf", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 14, + EndLine: 24, + }, + }, + }, + { + ID: "org.example:example-dependency2:2.3.4::d33d5afe", + Name: "org.example:example-dependency2", + Version: "2.3.4", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 25, + EndLine: 35, + }, + }, + }, + { + ID: "org.example:example-nested:3.3.3::8253090a", + Name: "org.example:example-nested", + Version: "3.3.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 36, + EndLine: 46, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:wildcard-exclusions:4.0.0::87282928", + DependsOn: []string{ + "org.example:example-dependency2:2.3.4::d33d5afe", + "org.example:example-dependency:1.2.3::4ee336bf", + "org.example:example-nested:3.3.3::8253090a", + }, + }, + }, + }, + { + name: "multi module", + inputFile: filepath.Join("testdata", "multi-module", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:aggregation:1.0.0::d145d452", + Name: "com.example:aggregation", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "com.example:module:1.1.1::822ec30d", + Name: "com.example:module", + Version: "1.1.1", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "org.example:example-dependency:1.2.3::493c9a85", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "org.example:example-api:2.0.0::fb4eb559", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + }, + // `mvn` doesn't include modules in dep tree of root pom and builds separate graphs. + // But we have `root` and `workspace` relationships, so we can merge these graphs. + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:aggregation:1.0.0::d145d452", + DependsOn: []string{ + "com.example:module:1.1.1::822ec30d", + }, + }, + { + ID: "com.example:module:1.1.1::822ec30d", + DependsOn: []string{ + "org.example:example-dependency:1.2.3::493c9a85", + }, + }, + { + ID: "org.example:example-dependency:1.2.3::493c9a85", + DependsOn: []string{ + "org.example:example-api:2.0.0::fb4eb559", + }, + }, + }, + }, + { + name: "multi module with deps with same GAV", + inputFile: filepath.Join("testdata", "multiple-modules-with-deps-with-same-gav", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:root:1.0.0::436691e6", + Name: "com.example:root", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "com.example:module1:1.0.0::4c39d72a", + Name: "com.example:module1", + Version: "1.0.0", + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "com.example:module2:2.0.0::57fc775b", + Name: "com.example:module2", + Version: "2.0.0", + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "org.example:example-api:1.7.30::80048fd3", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "org.example:example-api:1.7.30::8810f687", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + }, + }, + //[INFO] --------------------------------[ pom ]--------------------------------- + //[INFO] + //[INFO] --- dependency:3.7.0:tree (default-cli) @ module1 --- + //[INFO] com.example:module1:pom:1.0.0 + //[INFO] \- org.example:example-api:jar:1.7.30:compile + //[INFO] --------------------------------[ pom ]--------------------------------- + //[INFO] + //[INFO] --- dependency:3.7.0:tree (default-cli) @ module2 --- + //[INFO] com.example:module2:pom:2.0.0 + //[INFO] \- org.example:example-api:jar:1.7.30:compile + //[INFO] + //[INFO] --------------------------------[ pom ]--------------------------------- + //[INFO] + //[INFO] --- dependency:3.7.0:tree (default-cli) @ root --- + //[INFO] com.example:root:pom:1.0.0 + //[INFO] ------------------------------------------------------------------------ + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:module1:1.0.0::4c39d72a", + DependsOn: []string{ + "org.example:example-api:1.7.30::80048fd3", + }, + }, + { + ID: "com.example:module2:2.0.0::57fc775b", + DependsOn: []string{ + "org.example:example-api:1.7.30::8810f687", + }, + }, + { + ID: "com.example:root:1.0.0::436691e6", + DependsOn: []string{ + "com.example:module1:1.0.0::4c39d72a", + "com.example:module2:2.0.0::57fc775b", + }, + }, + }, + }, + { + name: "multi module with similar deps, but different children", + inputFile: filepath.Join("testdata", "multiple-modules-with-deps-with-same-gav-with-props", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:root:1.0.0::81b56f80", + Name: "com.example:root", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "com.example:module1:1.0.0::919ee306", + Name: "com.example:module1", + Version: "1.0.0", + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "com.example:module2:2.0.0::e4f6bb04", + Name: "com.example:module2", + Version: "2.0.0", + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "org.example:example-dependency:1.2.5::e2ecf3a4", + Name: "org.example:example-dependency", + Version: "1.2.5", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "org.example:example-dependency:1.2.5::e3e3d7fe", + Name: "org.example:example-dependency", + Version: "1.2.5", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "org.example:example-api:1.7.30::a4032585", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "org.example:example-api:2.0.0::6c475df9", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + }, + //[INFO] --------------------------------[ jar ]--------------------------------- + //[INFO] + //[INFO] --- dependency:3.7.0:tree (default-cli) @ module1 --- + //[INFO] com.example:module1:jar:1.0.0 + //[INFO] \- org.example:example-dependency:jar:1.2.5:compile + //[INFO] \- org.example:example-api:jar:1.7.30:compile + //[INFO] + //[INFO] ------------------------< com.example:module2 >------------------------- + //[INFO] Building module2 2.0.0 [2/3] + //[INFO] from module2/pom.xml + //[INFO] --------------------------------[ jar ]--------------------------------- + //[INFO] + //[INFO] --- dependency:3.7.0:tree (default-cli) @ module2 --- + //[INFO] com.example:module2:jar:2.0.0 + //[INFO] \- org.example:example-dependency:jar:1.2.5:compile + //[INFO] \- org.example:example-api:jar:2.0.0:compile + //[INFO] + //[INFO] --------------------------< com.example:root >-------------------------- + //[INFO] Building root 1.0.0 [3/3] + //[INFO] from pom.xml + //[INFO] --------------------------------[ pom ]--------------------------------- + //[INFO] + //[INFO] --- dependency:3.7.0:tree (default-cli) @ root --- + //[INFO] com.example:root:pom:1.0.0 + //[INFO] ------------------------------------------------------------------------ + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:module1:1.0.0::919ee306", + DependsOn: []string{ + "org.example:example-dependency:1.2.5::e3e3d7fe", + }, + }, + { + ID: "com.example:module2:2.0.0::e4f6bb04", + DependsOn: []string{ + "org.example:example-dependency:1.2.5::e2ecf3a4", + }, + }, + { + ID: "com.example:root:1.0.0::81b56f80", + DependsOn: []string{ + "com.example:module1:1.0.0::919ee306", + "com.example:module2:2.0.0::e4f6bb04", + }, + }, + { + ID: "org.example:example-dependency:1.2.5::e2ecf3a4", + DependsOn: []string{ + "org.example:example-api:2.0.0::6c475df9", + }, + }, + { + ID: "org.example:example-dependency:1.2.5::e3e3d7fe", + DependsOn: []string{ + "org.example:example-api:1.7.30::a4032585", + }, + }, + }, + }, + { + name: "nested modules", + inputFile: filepath.Join("testdata", "nested-modules", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:root:1.0.0::1b704e26", + Name: "com.example:root", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "com.example:module1:1.0.0::59758428", + Name: "com.example:module1", + Version: "1.0.0", + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "com.example:module2:2.0.0::a36f3d58", + Name: "com.example:module2", + Version: "2.0.0", + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "org.example:example-api:1.7.30::2946ca63", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{ + "The Apache Software License, Version 2.0", + }, + Relationship: ftypes.RelationshipDirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:module2:2.0.0::a36f3d58", + DependsOn: []string{ + "org.example:example-api:1.7.30::2946ca63", + }, + }, + { + ID: "com.example:root:1.0.0::1b704e26", + DependsOn: []string{ + "com.example:module1:1.0.0::59758428", + "com.example:module2:2.0.0::a36f3d58", + }, + }, + }, + }, + { + name: "Infinity loop for modules", + inputFile: filepath.Join("testdata", "modules-infinity-loop", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "org.example:root:1.0.0::9cfee9e6", + Name: "org.example:root", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + // as module + { + ID: "org.example:module-1:2.0.0::f8434436", + Name: "org.example:module-1", + Version: "2.0.0", + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "org.example:module-2:3.0.0::70a6381c", + Name: "org.example:module-2", + Version: "3.0.0", + Relationship: ftypes.RelationshipWorkspace, + }, + // as dependency + { + ID: "org.example:module-1:2.0.0::86dd9fb8", + Name: "org.example:module-1", + Version: "2.0.0", + Relationship: ftypes.RelationshipDirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "org.example:module-2:3.0.0::70a6381c", + DependsOn: []string{ + "org.example:module-1:2.0.0::86dd9fb8", + }, + }, + { + ID: "org.example:root:1.0.0::9cfee9e6", + DependsOn: []string{ + "org.example:module-1:2.0.0::f8434436", + "org.example:module-2:3.0.0::70a6381c", + }, + }, + }, + }, + { + name: "multi module soft requirement", + inputFile: filepath.Join("testdata", "multi-module-soft-requirement", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:aggregation:1.0.0::d303364d", + Name: "com.example:aggregation", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "com.example:module1:1.1.1::ede50f46", + Name: "com.example:module1", + Version: "1.1.1", + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "com.example:module2:1.1.1::ce791b4f", + Name: "com.example:module2", + Version: "1.1.1", + Relationship: ftypes.RelationshipWorkspace, + }, + { + ID: "org.example:example-api:1.7.30::5832af90", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "org.example:example-api:2.0.0::95bb5ac5", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:aggregation:1.0.0::d303364d", + DependsOn: []string{ + "com.example:module1:1.1.1::ede50f46", + "com.example:module2:1.1.1::ce791b4f", + }, + }, + { + ID: "com.example:module1:1.1.1::ede50f46", + DependsOn: []string{ + "org.example:example-api:1.7.30::5832af90", + }, + }, + { + ID: "com.example:module2:1.1.1::ce791b4f", + DependsOn: []string{ + "org.example:example-api:2.0.0::95bb5ac5", + }, + }, + }, + }, + { + name: "overwrite artifact version from dependencyManagement in the root POM", + inputFile: filepath.Join("testdata", "root-pom-dep-management", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:root-pom-dep-management:1.0.0::6251a251", + Name: "com.example:root-pom-dep-management", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-nested:3.3.3::99356a85", + Name: "org.example:example-nested", + Version: "3.3.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 20, + EndLine: 24, + }, + }, + }, + { + ID: "org.example:example-api:2.0.0::ba01a935", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + // dependency version is taken from `com.example:root-pom-dep-management` from dependencyManagement + // not from `com.example:example-nested` from `com.example:example-nested` + { + ID: "org.example:example-dependency:1.2.4::e4bc8e79", + Name: "org.example:example-dependency", + Version: "1.2.4", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:root-pom-dep-management:1.0.0::6251a251", + DependsOn: []string{ + "org.example:example-nested:3.3.3::99356a85", + }, + }, + { + ID: "org.example:example-dependency:1.2.4::e4bc8e79", + DependsOn: []string{ + "org.example:example-api:2.0.0::ba01a935", + }, + }, + { + ID: "org.example:example-nested:3.3.3::99356a85", + DependsOn: []string{ + "org.example:example-dependency:1.2.4::e4bc8e79", + }, + }, + }, + }, + { + name: "overwrite artifact version from dependencyManagement in the root POM when dependency uses `project.*` props", + inputFile: filepath.Join("testdata", "root-pom-dep-management-for-deps-with-project-props", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:root-pom-dep-management-for-deps-with-project-props:1.0.0::6c22541d", + Name: "com.example:root-pom-dep-management-for-deps-with-project-props", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency:1.7.30::6ae4efb4", + Name: "org.example:example-dependency", + Version: "1.7.30", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 21, + EndLine: 25, + }, + }, + }, + { + ID: "org.example:example-api:2.0.0::7043f18c", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:root-pom-dep-management-for-deps-with-project-props:1.0.0::6c22541d", + DependsOn: []string{ + "org.example:example-dependency:1.7.30::6ae4efb4", + }, + }, + { + ID: "org.example:example-dependency:1.7.30::6ae4efb4", + DependsOn: []string{ + "org.example:example-api:2.0.0::7043f18c", + }, + }, + }, + }, + { + name: "transitive dependencyManagement should not be inherited", + inputFile: filepath.Join("testdata", "transitive-dependency-management", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "org.example:transitive-dependency-management:2.0.0::a4a11976", + Name: "org.example:transitive-dependency-management", + Version: "2.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency-management3:1.1.1::c472e9e4", + Name: "org.example:example-dependency-management3", + Version: "1.1.1", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 14, + EndLine: 18, + }, + }, + }, + // Managed dependencies (org.example:example-api:1.7.30) in org.example:example-dependency-management3 + // should not affect dependencies of example-dependency (org.example:example-api:2.0.0) + { + ID: "org.example:example-api:2.0.0::bd947ff8", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "org.example:example-dependency:1.2.3::a4863849", + Name: "org.example:example-dependency", + Version: "1.2.3", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "org.example:example-dependency-management3:1.1.1::c472e9e4", + DependsOn: []string{ + "org.example:example-dependency:1.2.3::a4863849", + }, + }, + { + ID: "org.example:example-dependency:1.2.3::a4863849", + DependsOn: []string{ + "org.example:example-api:2.0.0::bd947ff8", + }, + }, + { + ID: "org.example:transitive-dependency-management:2.0.0::a4a11976", + DependsOn: []string{ + "org.example:example-dependency-management3:1.1.1::c472e9e4", + }, + }, + }, + }, + { + name: "parent not found", + inputFile: filepath.Join("testdata", "not-found-parent", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:no-parent:1.0-SNAPSHOT::32ac5060", + Name: "com.example:no-parent", + Version: "1.0-SNAPSHOT", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.7.30::d09fe727", + Name: "org.example:example-api", + Version: "1.7.30", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 27, + EndLine: 31, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:no-parent:1.0-SNAPSHOT::32ac5060", + DependsOn: []string{ + "org.example:example-api:1.7.30::d09fe727", + }, + }, + }, + }, + { + name: "dependency not found", + inputFile: filepath.Join("testdata", "not-found-dependency", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:not-found-dependency:1.0.0::e52fae70", + Name: "com.example:not-found-dependency", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-not-found:999::af293727", + Name: "org.example:example-not-found", + Version: "999", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 21, + EndLine: 25, + }, + }, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:not-found-dependency:1.0.0::e52fae70", + DependsOn: []string{ + "org.example:example-not-found:999::af293727", + }, + }, + }, + }, + { + name: "module not found - unable to parse module", + inputFile: filepath.Join("testdata", "not-found-module", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:aggregation:1.0.0::f8aa669d", + Name: "com.example:aggregation", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + }, + }, + { + name: "multiply licenses", + inputFile: filepath.Join("testdata", "multiply-licenses", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:multiply-licenses:1.0.0::22612a3a", + Name: "com.example:multiply-licenses", + Version: "1.0.0", + Licenses: []string{ + "MIT", + "Apache 2.0", + }, + Relationship: ftypes.RelationshipRoot, + }, + }, + }, + { + name: "inherit parent license", + inputFile: filepath.Join("testdata", "inherit-license", "module", "submodule", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example.app:submodule:1.0.0::3a92b985", + Name: "com.example.app:submodule", + Version: "1.0.0", + Licenses: []string{"Apache-2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + }, + }, + { + name: "compare ArtifactIDs for base and parent pom's", + inputFile: filepath.Join("testdata", "no-parent-infinity-loop", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:child:1.0.0::142e8eb6", + Name: "com.example:child", + Version: "1.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipRoot, + }, + }, + }, + { + name: "dependency without version", + inputFile: filepath.Join("testdata", "dep-without-version", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:dep-without-version:1.0.0::e1cb81d9", + Name: "com.example:dep-without-version", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api::35b5f95d", + Name: "org.example:example-api", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 19, + EndLine: 22, + }, + }, + }, + }, + }, + // [INFO] com.example:root-depManagement-in-parent:jar:1.0.0 + // [INFO] \- org.example:example-dependency:jar:2.0.0:compile + // [INFO] \- org.example:example-api:jar:1.0.1:compile + { + name: "dependency from parent uses version from root pom depManagement", + inputFile: filepath.Join("testdata", "use-root-dep-management-in-parent", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:root-depManagement-in-parent:1.0.0::675b2635", + Name: "com.example:root-depManagement-in-parent", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency:2.0.0::310f08f9", + Name: "org.example:example-dependency", + Version: "2.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 25, + EndLine: 29, + }, + }, + }, + { + ID: "org.example:example-api:1.0.1::343ac628", + Name: "org.example:example-api", + Version: "1.0.1", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:root-depManagement-in-parent:1.0.0::675b2635", + DependsOn: []string{ + "org.example:example-dependency:2.0.0::310f08f9", + }, + }, + { + ID: "org.example:example-dependency:2.0.0::310f08f9", + DependsOn: []string{ + "org.example:example-api:1.0.1::343ac628", + }, + }, + }, + }, + // [INFO] com.example:root-depManagement-in-parent:jar:1.0.0 + // [INFO] \- org.example:example-dependency:jar:2.0.0:compile + // [INFO] \- org.example:example-api:jar:2.0.1:compile + { + name: "dependency from parent uses version from child pom depManagement", + inputFile: filepath.Join("testdata", "use-dep-management-from-child-in-parent", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:root-depManagement-in-parent:1.0.0::7d38e0ad", + Name: "com.example:root-depManagement-in-parent", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency:2.0.0::329a1653", + Name: "org.example:example-dependency", + Version: "2.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 15, + EndLine: 19, + }, + }, + }, + { + ID: "org.example:example-api:2.0.1::187df2ac", + Name: "org.example:example-api", + Version: "2.0.1", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:root-depManagement-in-parent:1.0.0::7d38e0ad", + DependsOn: []string{ + "org.example:example-dependency:2.0.0::329a1653", + }, + }, + { + ID: "org.example:example-dependency:2.0.0::329a1653", + DependsOn: []string{ + "org.example:example-api:2.0.1::187df2ac", + }, + }, + }, + }, + // [INFO] com.example:child-depManagement-in-parent:jar:1.0.0 + // [INFO] +- org.example:example-api2:jar:1.0.2:runtime + // [INFO] +- org.example:example-api3:jar:4.0.3:compile + // [INFO] \- org.example:example-api:jar:1.0.1:compile + { + name: "dependency from parent uses version from child(scanned) pom depManagement", + inputFile: filepath.Join("testdata", "use-child-dep-management-in-parent", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:child-depManagement-in-parent:1.0.0::e8d75880", + Name: "com.example:child-depManagement-in-parent", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-api:1.0.1::5e5fc05f", + Name: "org.example:example-api", + Version: "1.0.1", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "org.example:example-api2:1.0.2::e378c24d", + Name: "org.example:example-api2", + Version: "1.0.2", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "org.example:example-api3:4.0.3::f92a10e6", + Name: "org.example:example-api3", + Version: "4.0.3", + Relationship: ftypes.RelationshipDirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:child-depManagement-in-parent:1.0.0::e8d75880", + DependsOn: []string{ + "org.example:example-api2:1.0.2::e378c24d", + "org.example:example-api3:4.0.3::f92a10e6", + "org.example:example-api:1.0.1::5e5fc05f", + }, + }, + }, + }, + // [INFO] com.example:inherit-scopes-from-child-deps-and-their-parents:jar:0.0.1 + // [INFO] +- org.example:example-nested-scope-runtime:jar:1.0.0:runtime + // [INFO] | \- org.example:example-scope-runtime:jar:2.0.0:runtime + // [INFO] | \- org.example:example-api-runtime:jar:3.0.0:runtime + // [INFO] +- org.example:example-nested-scope-compile:jar:1.0.0:compile + // [INFO] | \- org.example:example-scope-compile:jar:2.0.0:compile + // [INFO] | \- org.example:example-api-compile:jar:3.0.0:compile + // [INFO] \- org.example:example-nested-scope-empty:jar:1.0.0:compile + // [INFO] \- org.example:example-scope-empty:jar:2.0.0:compile + // [INFO] \- org.example:example-api-empty:jar:3.0.0:compile + // + // `example-nested-*" dependencies and their parents contain `dependencyManagement` with changed scopes + { + name: "inherit scopes from child dependencies and their parents", + inputFile: filepath.Join("testdata", "inherit-scopes-from-child-deps-and-their-parents", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:inherit-scopes-from-child-deps-and-their-parents:0.0.1::29dfa51e", + Name: "com.example:inherit-scopes-from-child-deps-and-their-parents", + Version: "0.0.1", + Relationship: ftypes.RelationshipRoot, + }, + exampleNestedScopeCompile("c5c1ec8f", 16, 21), + exampleNestedScopeEmpty("2e0c37ea", 22, 26), + exampleNestedScopeRuntime("98ee0b26", 10, 15), + exampleApiCompile("d4015fba"), + exampleApiEmpty("9575d06c"), + exampleApiRuntime("91cf1740"), + exampleScopeCompile("e168f2d0"), + exampleScopeEmpty("af2c22db"), + exampleScopeRuntime("acf8ee16"), + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:inherit-scopes-from-child-deps-and-their-parents:0.0.1::29dfa51e", + DependsOn: []string{ + "org.example:example-nested-scope-compile:1.0.0::c5c1ec8f", + "org.example:example-nested-scope-empty:1.0.0::2e0c37ea", + "org.example:example-nested-scope-runtime:1.0.0::98ee0b26", + }, + }, + { + ID: "org.example:example-nested-scope-compile:1.0.0::c5c1ec8f", + DependsOn: []string{ + "org.example:example-scope-compile:2.0.0::e168f2d0", + }, + }, + { + ID: "org.example:example-nested-scope-empty:1.0.0::2e0c37ea", + DependsOn: []string{ + "org.example:example-scope-empty:2.0.0::af2c22db", + }, + }, + { + ID: "org.example:example-nested-scope-runtime:1.0.0::98ee0b26", + DependsOn: []string{ + "org.example:example-scope-runtime:2.0.0::acf8ee16", + }, + }, + { + ID: "org.example:example-scope-compile:2.0.0::e168f2d0", + DependsOn: []string{ + "org.example:example-api-compile:3.0.0::d4015fba", + }, + }, + { + ID: "org.example:example-scope-empty:2.0.0::af2c22db", + DependsOn: []string{ + "org.example:example-api-empty:3.0.0::9575d06c", + }, + }, + { + ID: "org.example:example-scope-runtime:2.0.0::acf8ee16", + DependsOn: []string{ + "org.example:example-api-runtime:3.0.0::91cf1740", + }, + }, + }, + }, + // [INFO] com.example:inherit-scopes-in-parents-from-root:jar:0.1.0 + // [INFO] +- org.example:example-nested-scope-runtime:jar:1.0.0:runtime + // [INFO] | \- org.example:example-scope-runtime:jar:2.0.0:compile + // [INFO] | \- org.example:example-api-runtime:jar:3.0.0:runtime + // [INFO] +- org.example:example-nested-scope-compile:jar:1.0.0:compile + // [INFO] | \- org.example:example-scope-compile:jar:2.0.0:runtime + // [INFO] | \- org.example:example-api-compile:jar:3.0.0:test + // [INFO] \- org.example:example-nested-scope-empty:jar:1.0.0:compile + // [INFO] \- org.example:example-scope-empty:jar:2.0.0:runtime + // [INFO] \- org.example:example-api-empty:jar:3.0.0:test + // + // `example-nested-*" dependencies and their parents contain `dependencyManagement` with changed scopes + // scopes from `dependencyManagement` of root pom are used + { + name: "inherit scopes in children from root pom", + inputFile: filepath.Join("testdata", "inherit-scopes-in-children-from-root", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:inherit-scopes-in-children-from-root:0.0.1::3dbc8566", + Name: "com.example:inherit-scopes-in-children-from-root", + Version: "0.0.1", + Relationship: ftypes.RelationshipRoot, + }, + exampleNestedScopeCompile("e66ec05e", 51, 56), + exampleNestedScopeEmpty("4c11808b", 57, 61), + exampleNestedScopeRuntime("61fda3e9", 45, 50), + exampleApiRuntime("95f677d8"), + exampleScopeCompile("1d3ae149"), + exampleScopeEmpty("7e01cb21"), + exampleScopeRuntime("aaaa5004"), + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:inherit-scopes-in-children-from-root:0.0.1::3dbc8566", + DependsOn: []string{ + "org.example:example-nested-scope-compile:1.0.0::e66ec05e", + "org.example:example-nested-scope-empty:1.0.0::4c11808b", + "org.example:example-nested-scope-runtime:1.0.0::61fda3e9", + }, + }, + { + ID: "org.example:example-nested-scope-compile:1.0.0::e66ec05e", + DependsOn: []string{ + "org.example:example-scope-compile:2.0.0::1d3ae149", + }, + }, + { + ID: "org.example:example-nested-scope-empty:1.0.0::4c11808b", + DependsOn: []string{ + "org.example:example-scope-empty:2.0.0::7e01cb21", + }, + }, + { + ID: "org.example:example-nested-scope-runtime:1.0.0::61fda3e9", + DependsOn: []string{ + "org.example:example-scope-runtime:2.0.0::aaaa5004", + }, + }, + { + ID: "org.example:example-scope-runtime:2.0.0::aaaa5004", + DependsOn: []string{ + "org.example:example-api-runtime:3.0.0::95f677d8", + }, + }, + }, + }, + // [INFO] com.example:inherit-scopes-in-parents-from-root:jar:0.1.0 + // [INFO] +- org.example:example-nested-scope-runtime:jar:1.0.0:runtime + // [INFO] | \- org.example:example-scope-runtime:jar:2.0.0:compile + // [INFO] | \- org.example:example-api-runtime:jar:3.0.0:runtime + // [INFO] +- org.example:example-nested-scope-compile:jar:1.0.0:compile + // [INFO] | \- org.example:example-scope-compile:jar:2.0.0:runtime + // [INFO] | \- org.example:example-api-compile:jar:3.0.0:test + // [INFO] \- org.example:example-nested-scope-empty:jar:1.0.0:test + // [INFO] \- org.example:example-scope-empty:jar:2.0.0:test + // [INFO] \- org.example:example-api-empty:jar:3.0.0:test + // + // `example-nested-*" dependencies and their parents contain `dependencyManagement` with changed scopes + // scopes from `dependencyManagement` of root pom are used in parent dependencies + { + name: "inherit scopes in parent from root pom", + inputFile: filepath.Join("testdata", "inherit-scopes-in-parents-from-root", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:inherit-scopes-in-parents-from-root:0.1.0::ac1ed632", + Name: "com.example:inherit-scopes-in-parents-from-root", + Version: "0.1.0", + Relationship: ftypes.RelationshipRoot, + }, + exampleNestedScopeCompile("4ce69e4d", 0, 0), + exampleNestedScopeRuntime("9660d657", 0, 0), + exampleApiRuntime("efcdf95e"), + exampleScopeCompile("c30b48fb"), + exampleScopeRuntime("ea3219cb"), + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:inherit-scopes-in-parents-from-root:0.1.0::ac1ed632", + DependsOn: []string{ + "org.example:example-nested-scope-compile:1.0.0::4ce69e4d", + "org.example:example-nested-scope-runtime:1.0.0::9660d657", + }, + }, + { + ID: "org.example:example-nested-scope-compile:1.0.0::4ce69e4d", + DependsOn: []string{ + "org.example:example-scope-compile:2.0.0::c30b48fb", + }, + }, + { + ID: "org.example:example-nested-scope-runtime:1.0.0::9660d657", + DependsOn: []string{ + "org.example:example-scope-runtime:2.0.0::ea3219cb", + }, + }, + { + ID: "org.example:example-scope-runtime:2.0.0::ea3219cb", + DependsOn: []string{ + "org.example:example-api-runtime:3.0.0::efcdf95e", + }, + }, + }, + }, + //[INFO] com.example:root-pom-with-spaces:jar:1.0.0 + //[INFO] \- org.example:example-nested:jar:3.3.3:compile + //[INFO] \- org.example:example-dependency:jar:1.2.4:compile + //[INFO] \- org.example:example-api:jar:2.0.0:compile + { + name: "space at the start and/or end of the text nodes", + inputFile: filepath.Join("testdata", "with-spaces", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:root-pom-with-spaces:1.0.0::eae09abe", + Name: "com.example:root-pom-with-spaces", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-nested:3.3.3::11f4a96e", + Name: "org.example:example-nested", + Version: "3.3.3", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 24, + EndLine: 28, + }, + }, + }, + { + ID: "org.example:example-api:2.0.0::c9934860", + Name: "org.example:example-api", + Version: "2.0.0", + Licenses: []string{"The Apache Software License, Version 2.0"}, + Relationship: ftypes.RelationshipIndirect, + }, + // dependency version is taken from `com.example:root-pom-with-spaces` from dependencyManagement + // not from `com.example:example-nested` from `com.example:example-nested` + { + ID: "org.example:example-dependency:1.2.4::13e3505f", + Name: "org.example:example-dependency", + Version: "1.2.4", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:root-pom-with-spaces:1.0.0::eae09abe", + DependsOn: []string{ + "org.example:example-nested:3.3.3::11f4a96e", + }, + }, + { + ID: "org.example:example-dependency:1.2.4::13e3505f", + DependsOn: []string{ + "org.example:example-api:2.0.0::c9934860", + }, + }, + { + ID: "org.example:example-nested:3.3.3::11f4a96e", + DependsOn: []string{ + "org.example:example-dependency:1.2.4::13e3505f", + }, + }, + }, + }, + // `mvn` can take values from multiple dependencyManagement sections (from both the root and parent POMs). + // However, it does not override the `test` scope defined in the parent POM. + // [INFO] --- dependency:3.7.0:tree (default-cli) @ get-fields-from-multiple-depmanagements --- + // [INFO] com.example:get-fields-from-multiple-depmanagements:jar:1.0.0 + // [INFO] \- org.example:example-dependency:jar:4.0.0:compile + // [INFO] +- org.example:example-api4:jar:4.0.0:compile + // [INFO] +- org.example:example-api5:jar:4.0.0:test + // [INFO] \- org.example:example-api6:jar:1.7.30:runtime + // [INFO] ------------------------------------------------------------------------ + { + name: "don't overwrite test scope from upper depManagement", + inputFile: filepath.Join("testdata", "get-fields-from-multiple-depmanagements", "pom.xml"), + local: true, + want: []ftypes.Package{ + { + ID: "com.example:get-fields-from-multiple-depmanagements:1.0.0::6c3b7d0c", + Name: "com.example:get-fields-from-multiple-depmanagements", + Version: "1.0.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "org.example:example-dependency:4.0.0::1c05143b", + Name: "org.example:example-dependency", + Version: "4.0.0", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 30, + EndLine: 34, + }, + }, + }, + { + ID: "org.example:example-api4:4.0.0::faae46bb", + Name: "org.example:example-api4", + Version: "4.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "org.example:example-api6:1.7.30::779e4060", + Name: "org.example:example-api6", + Version: "1.7.30", + Relationship: ftypes.RelationshipIndirect, + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "com.example:get-fields-from-multiple-depmanagements:1.0.0::6c3b7d0c", + DependsOn: []string{ + "org.example:example-dependency:4.0.0::1c05143b", + }, + }, + { + ID: "org.example:example-dependency:4.0.0::1c05143b", + DependsOn: []string{ + "org.example:example-api4:4.0.0::faae46bb", + "org.example:example-api6:1.7.30::779e4060", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + var defaultRepo string + var settingsRepos []string + if tt.local { + // for local repository + t.Setenv("MAVEN_HOME", "testdata/settings/global") + } else { + // for remote repository + h := http.FileServer(http.Dir(filepath.Join("testdata", "repository"))) + ts := httptest.NewServer(h) + defaultRepo = ts.URL + + // Enable custom repository to be sure in repository order checking + if tt.enableRepoForSettingsRepo { + ch := http.FileServer(http.Dir(filepath.Join("testdata", "repository-for-settings-repo"))) + cts := httptest.NewServer(ch) + settingsRepos = []string{cts.URL} + } + } + + p := pom.NewParser(tt.inputFile, pom.WithDefaultRepo(defaultRepo, true, true), + pom.WithSettingsRepos(settingsRepos, true, false), pom.WithOffline(tt.offline)) + + gotPkgs, gotDeps, err := p.Parse(t.Context(), f) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + assert.Equal(t, tt.want, gotPkgs) + assert.Equal(t, tt.wantDeps, gotDeps) + }) + } +} + +// TestPom_Parse_Remote_Repos verifies that POM files are fetched from the correct repositories. +// The verification is done through the license field, as it's the only way to deterministically +// identify which repository an artifact came from. +func TestPom_Parse_Remote_Repos(t *testing.T) { + const rootRepoPlaceholder = "REPO_ROOT_URL" + + tests := []struct { + name string + inputFile string + rootRepoTxtar string // txtar file for root repository + repos map[string]string // key: repository URL placeholder, value: txtar file path + wantPackages map[string]string + }{ + { + name: "different repos for different dependencies", + inputFile: filepath.Join("testdata", "different-repos-for-different-poms", "pom.xml"), + rootRepoTxtar: filepath.Join("testdata", "different-repos-for-different-poms", "repo-root-artifacts.txtar"), + repos: map[string]string{ + "REPO1_URL": filepath.Join("testdata", "different-repos-for-different-poms", "repo1-artifacts.txtar"), + "REPO2_URL": filepath.Join("testdata", "different-repos-for-different-poms", "repo2-artifacts.txtar"), + }, + wantPackages: map[string]string{ + "org.example:example-api:1.7.30::2cbe1ca4": "License from repo1", + "org.example:example-api2:1.0.0::f8958ec7": "License from repo2", + "org.example:example-api3:1.0.0::887fc940": "License from reporoot", + }, + }, + { + name: "root POM with module inherits repository", + inputFile: filepath.Join("testdata", "repo-from-root-for-dep-from-module", "pom.xml"), + rootRepoTxtar: filepath.Join("testdata", "repo-from-root-for-dep-from-module", "repo-artifacts.txtar"), + repos: nil, + wantPackages: map[string]string{ + "org.example:example-api:1.0.0::4a790a84": "License from root repo", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup remote repositories from txtar files + repoURLs := make(map[string]string) + for placeholder, txtarPath := range tt.repos { + repoURL := setupTxtarRepository(t, txtarPath, nil) + repoURLs[placeholder] = repoURL + } + + // Setup root repository with replacements for repo placeholders + rootRepoURL := setupTxtarRepository(t, tt.rootRepoTxtar, repoURLs) + + // Prepare input POM file with root repository URL + pomContent := applyReplacements(t, tt.inputFile, map[string]string{ + rootRepoPlaceholder: rootRepoURL, + }) + + // Parse the POM + parser := pom.NewParser(tt.inputFile) + pkgs, _, err := parser.Parse(t.Context(), bytes.NewReader(pomContent)) + require.NoError(t, err) + + // Verify expected packages + for pkgID, wantLicense := range tt.wantPackages { + p, found := lo.Find(pkgs, func(pkg ftypes.Package) bool { + return pkg.ID == pkgID + }) + require.True(t, found, "package %s not found", pkgID) + require.NotEmpty(t, p.Licenses, "package %s has no licenses", pkgID) + require.Equal(t, wantLicense, p.Licenses[0]) + } + }) + } +} + +// applyReplacements reads a file, applies replacements, and returns the modified content. +func applyReplacements(t *testing.T, filePath string, replacements map[string]string) []byte { + t.Helper() + + content, err := os.ReadFile(filePath) + require.NoError(t, err) + + for oldURL, newURL := range replacements { + content = bytes.ReplaceAll(content, []byte(oldURL), []byte(newURL)) + } + + return content +} + +// txtarWithReposReplace reads a txtar file, applies repository URL replacements, +// and returns the result as a fs.FS. +func txtarWithReposReplace(t *testing.T, txtarPath string, reposReplacements map[string]string) fs.FS { + t.Helper() + + content := applyReplacements(t, txtarPath, reposReplacements) + + archive := txtar.Parse(content) + + fsys, err := txtar.FS(archive) + require.NoError(t, err) + + return fsys +} + +// setupTxtarRepository reads a txtar file, applies repository URL replacements, +// starts an HTTP test server with the files, and returns the server URL. +func setupTxtarRepository(t *testing.T, txtarPath string, reposReplacements map[string]string) string { + t.Helper() + + fsys := txtarWithReposReplace(t, txtarPath, reposReplacements) + + ts := httptest.NewServer(http.FileServer(http.FS(fsys))) + t.Cleanup(ts.Close) + + return ts.URL +} diff --git a/pkg/dependency/parser/java/pom/pom.go b/pkg/dependency/parser/java/pom/pom.go new file mode 100644 index 000000000000..97c867c549c3 --- /dev/null +++ b/pkg/dependency/parser/java/pom/pom.go @@ -0,0 +1,370 @@ +package pom + +import ( + "encoding/xml" + "fmt" + "io" + "reflect" + "slices" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/utils" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/set" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +type pom struct { + filePath string + content *pomXML +} + +func (p *pom) nil() bool { + return p == nil || p.content == nil +} + +func (p *pom) inherit(parent *pom) { + if parent == nil { + return + } + // Merge properties + p.content.Properties = utils.MergeMaps(parent.properties(), p.content.Properties) + + art := p.artifact().Inherit(parent.artifact()) + + p.content.GroupId = art.GroupID + p.content.ArtifactId = art.ArtifactID + p.content.Licenses = art.ToPOMLicenses() + + if isProperty(art.Version.String()) { + p.content.Version = evaluateVariable(art.Version.String(), p.content.Properties, nil) + } else { + p.content.Version = art.Version.String() + } +} + +func (p *pom) properties() properties { + props := p.content.Properties + return utils.MergeMaps(props, p.projectProperties()) +} + +func (p *pom) projectProperties() map[string]string { + val := reflect.ValueOf(p.content).Elem() + props := p.listProperties(val) + + // "version" and "groupId" elements could be inherited from parent. + // https://maven.apache.org/pom.html#inheritance + props["groupId"] = p.content.GroupId + props["version"] = p.content.Version + + // https://maven.apache.org/pom.html#properties + projectProperties := make(map[string]string) + for k, v := range props { + if strings.HasPrefix(k, "project.") { + continue + } + + // e.g. ${project.groupId} + key := fmt.Sprintf("project.%s", k) + projectProperties[key] = v + + // It is deprecated, but still available. + // e.g. ${groupId} + projectProperties[k] = v + } + + return projectProperties +} + +func (p *pom) listProperties(val reflect.Value) map[string]string { + props := make(map[string]string) + for i := 0; i < val.NumField(); i++ { + f := val.Type().Field(i) + + tag, ok := f.Tag.Lookup("xml") + if !ok || strings.Contains(tag, ",") { + // e.g. ",chardata" + continue + } + + switch f.Type.Kind() { + case reflect.Slice: + continue + case reflect.Map: + m := val.Field(i) + for _, e := range m.MapKeys() { + v := m.MapIndex(e) + + // element may contain: + // - properties from element of current POM + // - properties from parent POMs (we added these properties early) + // - properties from element of parent POMs + // - properties got from fields of parent POMs (`project.groupId`, `parent.project.version`, etc.) + // Properties from field has higher priority than properties from element. + if tag == "properties" && props[e.String()] != "" { + continue + } + props[e.String()] = v.String() + } + case reflect.Struct: + nestedProps := p.listProperties(val.Field(i)) + for k, v := range nestedProps { + key := fmt.Sprintf("%s.%s", tag, k) + props[key] = v + } + default: + props[tag] = val.Field(i).String() + } + } + return props +} + +func (p *pom) artifact() artifact { + return newArtifact(p.content.GroupId, p.content.ArtifactId, p.content.Version, p.licenses(), p.content.Properties) +} + +func (p *pom) licenses() []string { + return xslices.ZeroToNil(lo.FilterMap(p.content.Licenses.License, func(lic pomLicense, _ int) (string, bool) { + return lic.Name, lic.Name != "" + })) +} + +func (p *pom) repositories(servers []Server) []repository { + return resolvePomRepos(servers, p.content.Repositories) +} + +type pomXML struct { + Parent pomParent `xml:"parent"` + GroupId string `xml:"groupId"` + ArtifactId string `xml:"artifactId"` + Version string `xml:"version"` + Licenses pomLicenses `xml:"licenses"` + Modules struct { + Text string `xml:",chardata"` + Module []string `xml:"module"` + } `xml:"modules"` + Properties properties `xml:"properties"` + DependencyManagement struct { + Text string `xml:",chardata"` + Dependencies pomDependencies `xml:"dependencies"` + } `xml:"dependencyManagement"` + Dependencies pomDependencies `xml:"dependencies"` + Repositories []pomRepository `xml:"repositories>repository"` +} + +type pomParent struct { + GroupId string `xml:"groupId"` + ArtifactId string `xml:"artifactId"` + Version string `xml:"version"` + RelativePath string `xml:"relativePath"` +} + +type pomLicenses struct { + Text string `xml:",chardata"` + License []pomLicense `xml:"license"` +} + +type pomLicense struct { + Name string `xml:"name"` +} + +type pomDependencies struct { + Text string `xml:",chardata"` + Dependency []pomDependency `xml:"dependency"` +} + +type pomDependency struct { + Text string `xml:",chardata"` + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` + Scope string `xml:"scope"` + Optional bool `xml:"optional"` + Exclusions pomExclusions `xml:"exclusions"` + StartLine int + EndLine int +} + +type pomExclusions struct { + Text string `xml:",chardata"` + Exclusion []pomExclusion `xml:"exclusion"` +} + +// ref. https://maven.apache.org/guides/introduction/introduction-to-optional-and-excludes-dependencies.html +type pomExclusion struct { + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` +} + +func (d pomDependency) Name() string { + return fmt.Sprintf("%s:%s", d.GroupID, d.ArtifactID) +} + +// Resolve evaluates variables in the dependency and inherit some fields from dependencyManagement to the dependency. +func (d pomDependency) Resolve(props map[string]string, depManagement, rootDepManagement []pomDependency) pomDependency { + // Evaluate variables + dep := pomDependency{ + Text: d.Text, + GroupID: evaluateVariable(d.GroupID, props, nil), + ArtifactID: evaluateVariable(d.ArtifactID, props, nil), + Version: evaluateVariable(d.Version, props, nil), + Scope: evaluateVariable(d.Scope, props, nil), + Optional: d.Optional, + Exclusions: d.Exclusions, + StartLine: d.StartLine, + EndLine: d.EndLine, + } + + // Field resolution order: + // 1. Fill empty fields with values from dependencyManagement. + // 2. Overwrite non-empty fields with values from the root dependencyManagement. + // 2.1. Exception: if a dependency has the `test` scope (either defined explicitly + // or inherited from dependencyManagement), we must NOT overwrite it + // with the value from the root dependencyManagement. + // + // See the test "don't overwrite test scope from upper depManagement" for details. + + // Inherit version, scope and optional from dependencyManagement if empty + if managed, found := findDep(dep.Name(), depManagement); found { // dependencyManagement from parent + if dep.Version == "" { + dep.Version = evaluateVariable(managed.Version, props, nil) + } + if dep.Scope == "" { + dep.Scope = evaluateVariable(managed.Scope, props, nil) + } + // TODO: need to check the behavior + if !dep.Optional { + dep.Optional = managed.Optional + } + // `mvn` always merges exceptions for pom and parent + dep.Exclusions.Exclusion = append(dep.Exclusions.Exclusion, managed.Exclusions.Exclusion...) + } + + // If this dependency is managed in the root POM, + // we need to overwrite fields according to the managed dependency. + if managed, found := findDep(dep.Name(), rootDepManagement); found { // dependencyManagement from the root POM + if managed.Version != "" { + dep.Version = evaluateVariable(managed.Version, props, nil) + } + + if managed.Scope != "" && dep.Scope != "test" { + dep.Scope = evaluateVariable(managed.Scope, props, nil) + } + + if managed.Optional { + dep.Optional = managed.Optional + } + + // 'mvn' always merges exceptions for pom and root POM + dep.Exclusions.Exclusion = append(dep.Exclusions.Exclusion, managed.Exclusions.Exclusion...) + return dep + } + return dep +} + +// ToArtifact converts dependency to artifact. +// It should be called after calling Resolve() so that variables can be evaluated. +func (d pomDependency) ToArtifact(opts analysisOptions) artifact { + // To avoid shadow adding exclusions to top pom's, + // we need to initialize a new map for each new artifact + // See `exclusions in child` test for more information + exclusions := set.New[string]() + if opts.exclusions != nil { + exclusions = opts.exclusions.Clone() + } + for _, e := range d.Exclusions.Exclusion { + exclusions.Append(fmt.Sprintf("%s:%s", e.GroupID, e.ArtifactID)) + } + + var locations ftypes.Locations + if d.StartLine != 0 && d.EndLine != 0 { + locations = ftypes.Locations{ + { + StartLine: d.StartLine, + EndLine: d.EndLine, + }, + } + } + + return artifact{ + GroupID: d.GroupID, + ArtifactID: d.ArtifactID, + Version: newVersion(d.Version), + Exclusions: exclusions, + Locations: locations, + Relationship: ftypes.RelationshipIndirect, // default + RootFilePath: opts.rootFilePath, + Repositories: slices.Clone(opts.repositories), // avoid sharing + } +} + +type properties map[string]string + +type property struct { + XMLName xml.Name + Value string `xml:",chardata"` +} + +func (props *properties) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error { + *props = properties{} + for { + var p property + err := d.Decode(&p) + if err == io.EOF { + break + } else if err != nil { + return xerrors.Errorf("XML decode error: %w", err) + } + + (*props)[p.XMLName.Local] = p.Value + } + return nil +} + +func (deps *pomDependencies) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error { + for { + token, err := d.Token() + if err == io.EOF { + break + } else if err != nil { + return xerrors.Errorf("XML decode error: %w", err) + } + + t, ok := token.(xml.StartElement) + if !ok { + continue + } + + if t.Name.Local == "dependency" { + var dep pomDependency + dep.StartLine, _ = d.InputPos() // tag starts + + // Decode the element + err = d.DecodeElement(&dep, &t) + if err != nil { + return xerrors.Errorf("Error decoding dependency: %w", err) + } + + dep.EndLine, _ = d.InputPos() // tag ends + deps.Dependency = append(deps.Dependency, dep) + } + } + return nil +} + +func findDep(name string, depManagement []pomDependency) (pomDependency, bool) { + return lo.Find(depManagement, func(item pomDependency) bool { + return item.Name() == name + }) +} + +type pomRepository struct { + ID string `xml:"id"` + Name string `xml:"name"` + URL string `xml:"url"` + ReleasesEnabled string `xml:"releases>enabled"` + SnapshotsEnabled string `xml:"snapshots>enabled"` +} diff --git a/pkg/dependency/parser/java/pom/queue.go b/pkg/dependency/parser/java/pom/queue.go new file mode 100644 index 000000000000..a28658564f74 --- /dev/null +++ b/pkg/dependency/parser/java/pom/queue.go @@ -0,0 +1,32 @@ +package pom + +import "sync" + +// artifactQueue the queue of Items +type artifactQueue struct { + items []artifact + lock sync.RWMutex +} + +func newArtifactQueue() *artifactQueue { + return &artifactQueue{} +} + +func (s *artifactQueue) enqueue(items ...artifact) { + s.lock.Lock() + s.items = append(s.items, items...) + s.lock.Unlock() +} + +func (s *artifactQueue) dequeue() artifact { + s.lock.Lock() + item := s.items[0] + s.items = s.items[1:] + s.lock.Unlock() + return item +} + +// IsEmpty returns true if the queue is empty +func (s *artifactQueue) IsEmpty() bool { + return len(s.items) == 0 +} diff --git a/pkg/dependency/parser/java/pom/repository.go b/pkg/dependency/parser/java/pom/repository.go new file mode 100644 index 000000000000..2ce69675887e --- /dev/null +++ b/pkg/dependency/parser/java/pom/repository.go @@ -0,0 +1,68 @@ +package pom + +import ( + "errors" + "net/url" + + "github.com/aquasecurity/trivy/pkg/log" +) + +var centralURL, _ = url.Parse("https://repo.maven.apache.org/maven2/") + +type repository struct { + url url.URL + releaseEnabled bool + snapshotEnabled bool +} + +type repositories struct { + settings []repository // Repositories from settings.xml files + defaultRepo repository // Default repository - Maven Central for Release, empty for Snapshot +} + +var mavenCentralRepo = repository{ + url: *centralURL, + releaseEnabled: true, +} + +func resolvePomRepos(servers []Server, pomRepos []pomRepository) []repository { + logger := log.WithPrefix("pom") + var repos []repository + for _, rep := range pomRepos { + r := repository{ + // ": true or false for whether this repository is enabled for the respective type (releases or snapshots). By default, this is true." + // cf. https://maven.apache.org/pom.html#Repositories + releaseEnabled: rep.ReleasesEnabled == "true" || rep.ReleasesEnabled == "", + snapshotEnabled: rep.SnapshotsEnabled == "true" || rep.SnapshotsEnabled == "", + } + + // Add only enabled repositories + if !r.releaseEnabled && !r.snapshotEnabled { + continue + } + + repoURL, err := url.Parse(rep.URL) + if err != nil { + var ue *url.Error + if errors.As(err, &ue) { + err = ue.Unwrap() + } + logger.Debug("Unable to parse remote repository url", log.String("id", rep.ID), log.Err(err)) + continue + } + + // Get the credentials from settings.xml based on matching server id + // with the repository id from pom.xml and use it for accessing the repository url + for _, server := range servers { + if rep.ID == server.ID && server.Username != "" && server.Password != "" { + repoURL.User = url.UserPassword(server.Username, server.Password) + break + } + } + + logger.Debug("Adding repository", log.String("id", rep.ID), log.String("url", repoURL.Redacted())) + r.url = *repoURL + repos = append(repos, r) + } + return repos +} diff --git a/pkg/dependency/parser/java/pom/settings.go b/pkg/dependency/parser/java/pom/settings.go new file mode 100644 index 000000000000..02bf5270fac2 --- /dev/null +++ b/pkg/dependency/parser/java/pom/settings.go @@ -0,0 +1,128 @@ +package pom + +import ( + "encoding/xml" + "os" + "path/filepath" + "slices" + + "github.com/samber/lo" + "github.com/samber/lo/mutable" + "golang.org/x/net/html/charset" +) + +type Server struct { + ID string `xml:"id"` + Username string `xml:"username"` + Password string `xml:"password"` +} + +type Profile struct { + ID string `xml:"id"` + Repositories []pomRepository `xml:"repositories>repository"` + ActiveByDefault bool `xml:"activation>activeByDefault"` +} + +type settings struct { + LocalRepository string `xml:"localRepository"` + Servers []Server `xml:"servers>server"` + Profiles []Profile `xml:"profiles>profile"` + ActiveProfiles []string `xml:"activeProfiles>activeProfile"` +} + +func (s settings) effectiveRepositories() []repository { + var pomRepos []pomRepository + for _, profile := range s.Profiles { + if slices.Contains(s.ActiveProfiles, profile.ID) || profile.ActiveByDefault { + pomRepos = append(pomRepos, profile.Repositories...) + } + } + pomRepos = lo.UniqBy(pomRepos, func(r pomRepository) string { + return r.ID + }) + + // mvn takes repositories from settings in reverse order + // cf. https://github.com/aquasecurity/trivy/issues/7807#issuecomment-2541485152 + mutable.Reverse(pomRepos) + + return resolvePomRepos(s.Servers, pomRepos) +} + +func readSettings() settings { + s := settings{} + + userSettingsPath := filepath.Join(os.Getenv("HOME"), ".m2", "settings.xml") + userSettings, err := openSettings(userSettingsPath) + if err == nil { + s = userSettings + } + + // Some package managers use this path by default + mavenHome := "/usr/share/maven" + if mHome := os.Getenv("MAVEN_HOME"); mHome != "" { + mavenHome = mHome + } + globalSettingsPath := filepath.Join(mavenHome, "conf", "settings.xml") + globalSettings, err := openSettings(globalSettingsPath) + if err == nil { + // We need to merge global and user settings. User settings being dominant. + // https://maven.apache.org/settings.html#quick-overview + if s.LocalRepository == "" { + s.LocalRepository = globalSettings.LocalRepository + } + + // Maven servers + s.Servers = lo.UniqBy(append(s.Servers, globalSettings.Servers...), func(server Server) string { + return server.ID + }) + + // Merge profiles + s.Profiles = lo.UniqBy(append(s.Profiles, globalSettings.Profiles...), func(p Profile) string { + return p.ID + }) + // Merge active profiles + s.ActiveProfiles = lo.Uniq(append(s.ActiveProfiles, globalSettings.ActiveProfiles...)) + } + + return s +} + +func openSettings(filePath string) (settings, error) { + f, err := os.Open(filePath) + if err != nil { + return settings{}, err + } + defer f.Close() + + s := settings{} + decoder := xml.NewDecoder(f) + decoder.CharsetReader = charset.NewReaderLabel + if err = decoder.Decode(&s); err != nil { + return settings{}, err + } + + expandAllEnvPlaceholders(&s) + + return s, nil +} + +func expandAllEnvPlaceholders(s *settings) { + s.LocalRepository = evaluateVariable(s.LocalRepository, nil, nil) + for i, server := range s.Servers { + s.Servers[i].ID = evaluateVariable(server.ID, nil, nil) + s.Servers[i].Username = evaluateVariable(server.Username, nil, nil) + s.Servers[i].Password = evaluateVariable(server.Password, nil, nil) + } + + for i, profile := range s.Profiles { + s.Profiles[i].ID = evaluateVariable(profile.ID, nil, nil) + for j, repo := range profile.Repositories { + s.Profiles[i].Repositories[j].ID = evaluateVariable(repo.ID, nil, nil) + s.Profiles[i].Repositories[j].Name = evaluateVariable(repo.Name, nil, nil) + s.Profiles[i].Repositories[j].URL = evaluateVariable(repo.URL, nil, nil) + } + } + for i, activeProfile := range s.ActiveProfiles { + s.ActiveProfiles[i] = evaluateVariable(activeProfile, nil, nil) + } +} diff --git a/pkg/dependency/parser/java/pom/settings_test.go b/pkg/dependency/parser/java/pom/settings_test.go new file mode 100644 index 000000000000..305ff6d44dee --- /dev/null +++ b/pkg/dependency/parser/java/pom/settings_test.go @@ -0,0 +1,475 @@ +package pom + +import ( + "net/url" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_ReadSettings(t *testing.T) { + tests := []struct { + name string + envs map[string]string + wantSettings settings + }{ + { + name: "happy path with only global settings", + envs: map[string]string{ + "HOME": "", + "MAVEN_HOME": filepath.Join("testdata", "settings", "global"), + }, + wantSettings: settings{ + LocalRepository: "testdata/repository", + Servers: []Server{ + { + ID: "global-server", + }, + { + ID: "server-with-credentials", + Username: "test-user", + Password: "test-password-from-global", + }, + { + ID: "server-with-name-only", + Username: "test-user-only", + }, + }, + Profiles: []Profile{ + { + ID: "mycompany-global", + Repositories: []pomRepository{ + { + ID: "mycompany-internal-releases", + URL: "https://mycompany.example.com/repository/internal-releases", + ReleasesEnabled: "", + SnapshotsEnabled: "false", + }, + { + ID: "mycompany-global-releases", + URL: "https://mycompany.example.com/repository/global-releases", + ReleasesEnabled: "true", + SnapshotsEnabled: "false", + }, + }, + ActiveByDefault: false, + }, + { + ID: "default", + Repositories: []pomRepository{ + { + ID: "mycompany-default-releases", + URL: "https://mycompany.example.com/repository/default-releases", + ReleasesEnabled: "true", + SnapshotsEnabled: "false", + }, + }, + ActiveByDefault: true, + }, + }, + ActiveProfiles: []string{}, + }, + }, + { + name: "happy path with only user settings", + envs: map[string]string{ + "HOME": filepath.Join("testdata", "settings", "user"), + "MAVEN_HOME": "NOT_EXISTING_PATH", + }, + wantSettings: settings{ + LocalRepository: "testdata/user/repository", + Servers: []Server{ + { + ID: "user-server", + }, + { + ID: "server-with-credentials", + Username: "test-user", + Password: "test-password", + }, + { + ID: "server-with-name-only", + Username: "test-user-only", + }, + }, + Profiles: []Profile{ + { + ID: "mycompany-global", + Repositories: []pomRepository{ + { + ID: "mycompany-releases", + URL: "https://mycompany.example.com/repository/user-releases", + ReleasesEnabled: "true", + SnapshotsEnabled: "false", + }, + { + ID: "mycompany-user-snapshots", + URL: "https://mycompany.example.com/repository/user-snapshots", + ReleasesEnabled: "false", + SnapshotsEnabled: "true", + }, + }, + ActiveByDefault: true, + }, + }, + ActiveProfiles: []string{ + "mycompany-global", + }, + }, + }, + { + // $ mvn help:effective-settings + // [INFO] ------------------< org.apache.maven:standalone-pom >------------------- + // [INFO] --- maven-help-plugin:3.4.0:effective-settings (default-cli) @ standalone-pom --- + // Effective user-specific configuration settings: + // + // + // + // /root/testdata/user/repository + // + // + // user-server + // + // + // test-user + // *** + // server-with-credentials + // + // + // test-user-only + // server-with-name-only + // + // + // global-server + // + // + // + // + // + // true + // + // + // + // + // fail + // + // + // false + // + // mycompany-releases + // https://mycompany.example.com/repository/user-releases + // + // + // + // false + // + // + // mycompany-user-snapshots + // https://mycompany.example.com/repository/user-snapshots + // + // + // mycompany-global + // + // + // + // true + // + // + // + // + // + // false + // + // mycompany-default-releases + // https://mycompany.example.com/repository/default-releases + // + // + // + // + // + // mycompany-global + // + // + name: "happy path with global and user settings", + envs: map[string]string{ + "HOME": filepath.Join("testdata", "settings", "user"), + "MAVEN_HOME": filepath.Join("testdata", "settings", "global"), + }, + wantSettings: settings{ + LocalRepository: "testdata/user/repository", + Servers: []Server{ + { + ID: "user-server", + }, + { + ID: "server-with-credentials", + Username: "test-user", + Password: "test-password", + }, + { + ID: "server-with-name-only", + Username: "test-user-only", + }, + { + ID: "global-server", + }, + }, + Profiles: []Profile{ + { + ID: "mycompany-global", + Repositories: []pomRepository{ + { + ID: "mycompany-releases", + URL: "https://mycompany.example.com/repository/user-releases", + ReleasesEnabled: "true", + SnapshotsEnabled: "false", + }, + { + ID: "mycompany-user-snapshots", + URL: "https://mycompany.example.com/repository/user-snapshots", + ReleasesEnabled: "false", + SnapshotsEnabled: "true", + }, + }, + ActiveByDefault: true, + }, + { + ID: "default", + Repositories: []pomRepository{ + { + ID: "mycompany-default-releases", + URL: "https://mycompany.example.com/repository/default-releases", + ReleasesEnabled: "true", + SnapshotsEnabled: "false", + }, + }, + ActiveByDefault: true, + }, + }, + ActiveProfiles: []string{ + "mycompany-global", + }, + }, + }, + { + name: "without settings", + envs: map[string]string{ + "HOME": "", + "MAVEN_HOME": "NOT_EXISTING_PATH", + }, + wantSettings: settings{}, + }, + { + name: "environment placeholders are dereferenced", + envs: map[string]string{ + "HOME": filepath.Join("testdata", "settings", "user-settings-with-env-placeholders"), + "MAVEN_HOME": "NOT_EXISTING_PATH", + "LOCAL_REPO_PT_1": "part1", + "LOCAL_REPO_PT_2": "part2", + "SERVER_ID": "server-id-from-env", + "USERNAME": "username-from-env", + "PASSWORD": "password-from-env", + "PROFILE_ID": "mycompany-global", + "REPO_ID": "mycompany-releases", + "REPO_URL": "https://mycompany.example.com", + }, + wantSettings: settings{ + LocalRepository: "part1/part2/.m2/repository", + Servers: []Server{ + { + ID: "user-server", + }, + { + ID: "server-id-from-env", + Username: "username-from-env", + Password: "password-from-env", + }, + { + ID: "server-with-name-only", + Username: "test-user-only", + }, + }, + Profiles: []Profile{ + { + ID: "mycompany-global", + Repositories: []pomRepository{ + { + ID: "mycompany-releases", + URL: "https://mycompany.example.com/repository/user-releases", + ReleasesEnabled: "true", + SnapshotsEnabled: "false", + }, + }, + }, + }, + ActiveProfiles: []string{ + "mycompany-global", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for env, settingsDir := range tt.envs { + t.Setenv(env, settingsDir) + } + + gotSettings := readSettings() + require.Equal(t, tt.wantSettings, gotSettings) + }) + } +} + +func Test_effectiveRepositories(t *testing.T) { + tests := []struct { + name string + s settings + want []repository + }{ + { + name: "single active profile, reversed order", + s: settings{ + Servers: []Server{ + { + ID: "r1", + Username: "u", + Password: "p", + }, + }, + Profiles: []Profile{ + { + ID: "p1", + Repositories: []pomRepository{ + { + ID: "r1", + URL: "https://example.com/repo1", + // ReleasesEnabled: "true", Release field is not explicitly set. + SnapshotsEnabled: "false", + }, + { + ID: "r2", + URL: "https://example.com/repo2", + ReleasesEnabled: "invalid", // invalid value treated as false + // SnapshotsEnabled: "true", Snapshot field is not explicitly set. + }, + }, + }, + }, + ActiveProfiles: []string{"p1"}, + }, + want: []repository{ + { + url: mustParseURL(t, "https://example.com/repo2"), + releaseEnabled: false, + snapshotEnabled: true, + }, + { + url: mustParseURL(t, "https://u:p@example.com/repo1"), + releaseEnabled: true, + snapshotEnabled: false, + }, + }, + }, + { + name: "activeByDefault + activeProfiles with dedup and reverse", + s: settings{ + Servers: nil, + Profiles: []Profile{ + { + ID: "p1", + ActiveByDefault: true, + Repositories: []pomRepository{ + { + ID: "dup", + URL: "https://p1.example.com/dup", + ReleasesEnabled: "true", + SnapshotsEnabled: "false", + }, + { + ID: "only-p1", + URL: "https://p1.example.com/only", + ReleasesEnabled: "true", + SnapshotsEnabled: "true", + }, + }, + }, + { + ID: "p2", + Repositories: []pomRepository{ + { + ID: "dup", + URL: "https://p2.example.com/dup", + ReleasesEnabled: "true", + SnapshotsEnabled: "true", + }, + }, + }, + }, + ActiveProfiles: []string{"p2"}, + }, + // Expected order after dedup (keep first occurrence from p1) and reverse: + // Input order before reverse: [dup(from p1), only-p1, dup(from p2 - removed by dedup)] + // After reverse: [only-p1, dup(from p1)] + want: []repository{ + { + url: mustParseURL(t, "https://p1.example.com/only"), + releaseEnabled: true, + snapshotEnabled: true, + }, + { + url: mustParseURL(t, "https://p1.example.com/dup"), + releaseEnabled: true, + snapshotEnabled: false, + }, + }, + }, + { + name: "disabled repositories are ignored", + s: settings{ + Profiles: []Profile{ + { + ID: "p", + ActiveByDefault: true, + Repositories: []pomRepository{ + { + ID: "disabled", + URL: "https://example.com/disabled", + ReleasesEnabled: "false", + SnapshotsEnabled: "false", + }, + { + ID: "enabled", + URL: "https://example.com/enabled", + ReleasesEnabled: "true", + SnapshotsEnabled: "false", + }, + }, + }, + }, + }, + want: []repository{ + { + url: mustParseURL(t, "https://example.com/enabled"), + releaseEnabled: true, + snapshotEnabled: false, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.s.effectiveRepositories() + require.Equal(t, tt.want, got) + }) + } +} + +// mustParseURL parses a URL and panics on error; handy for test literals +func mustParseURL(t *testing.T, s string) url.URL { + t.Helper() + u, err := url.Parse(s) + require.NoError(t, err) + return *u +} diff --git a/pkg/dependency/parser/java/pom/testdata/dep-without-version/pom.xml b/pkg/dependency/parser/java/pom/testdata/dep-without-version/pom.xml new file mode 100644 index 000000000000..d7da70dd4735 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/dep-without-version/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + no-parent + Parent not found + + + com.example + wrong-parent + 1.0.0 + + + com.example + dep-without-version + 1.0.0 + + + + org.example + example-api + + + diff --git a/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/pom.xml b/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/pom.xml new file mode 100644 index 000000000000..0222541963a1 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/pom.xml @@ -0,0 +1,43 @@ + + 4.0.0 + + com.example + different-repos-for-different-poms + 1.0.0 + + + + root-repo + REPO_ROOT_URL + + true + + + + disabled-repo-from-root-pom + http://disabled.com + + false + + + + + + + org.example + example-dependency + 5.0.0 + + + org.example + example-dependency2 + 5.0.0 + + + org.example + example-api3 + 1.0.0 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/repo-root-artifacts.txtar b/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/repo-root-artifacts.txtar new file mode 100644 index 000000000000..10a76d6ff312 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/repo-root-artifacts.txtar @@ -0,0 +1,100 @@ +Test case artifacts for different repos for different dependencies test. + +-- org/example/example-dependency/5.0.0/example-dependency-5.0.0.pom -- + + + + 4.0.0 + + org.example + example-dependency + 5.0.0 + + + + test-id + REPO1_URL + + true + + + + + jar + Example API Dependency + The example API + + + + org.example + example-api + 1.7.30 + + + org.example + example-api3 + 1.0.0 + + + + +-- org/example/example-dependency2/5.0.0/example-dependency2-5.0.0.pom -- + + + + 4.0.0 + + org.example + example-dependency2 + 5.0.0 + + + + test-id + REPO2_URL + + true + + + + + jar + Example API Dependency + The example API + + + + org.example + example-api2 + 1.0.0 + + + + + +-- org/example/example-api3/1.0.0/example-api3-1.0.0.pom -- + + + + 4.0.0 + + org.example + example-api3 + 1.0.0 + + jar + Example API3 Module + The example API3 + + + + License from reporoot + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/repo1-artifacts.txtar b/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/repo1-artifacts.txtar new file mode 100644 index 000000000000..81d0cd5bd342 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/repo1-artifacts.txtar @@ -0,0 +1,51 @@ +Test case artifacts for repo1 (example-api:1.7.30). + +-- org/example/example-api/1.7.30/example-api-1.7.30.pom -- + + + + 4.0.0 + + org.example + example-api + 1.7.30 + + jar + Example API Module + The example API + + + + License from repo1 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + +-- org/example/example-api3/1.0.0/example-api3-1.0.0.pom -- + + + + 4.0.0 + + org.example + example-api3 + 1.0.0 + + jar + Example API3 Module + The example API3 + + + + License from repo1 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/repo2-artifacts.txtar b/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/repo2-artifacts.txtar new file mode 100644 index 000000000000..462e1ee417a6 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/different-repos-for-different-poms/repo2-artifacts.txtar @@ -0,0 +1,26 @@ +Test case artifacts for repo2 (example-api2:1.0.0). + +-- org/example/example-api2/1.0.0/example-api2-1.0.0.pom -- + + + + 4.0.0 + + org.example + example-api2 + 1.0.0 + + jar + Example API Module + The example API + + + + License from repo2 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/pkg/dependency/parser/java/pom/testdata/exclusions-in-child/pom.xml b/pkg/dependency/parser/java/pom/testdata/exclusions-in-child/pom.xml new file mode 100644 index 000000000000..5ff82cec31a8 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/exclusions-in-child/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + com.example + example + 1.0.0 + + + + org.example + example-exclusions + 4.0.0 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/exclusions-parent-dependency-management/child/pom.xml b/pkg/dependency/parser/java/pom/testdata/exclusions-parent-dependency-management/child/pom.xml new file mode 100644 index 000000000000..967033369b92 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/exclusions-parent-dependency-management/child/pom.xml @@ -0,0 +1,38 @@ + + 4.0.0 + + child + 3.0.0 + + child + Child + + + com.example + parent + 2.0.0 + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-exclusions + + + org.example + example-dependency + + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/exclusions-parent-dependency-management/pom.xml b/pkg/dependency/parser/java/pom/testdata/exclusions-parent-dependency-management/pom.xml new file mode 100644 index 000000000000..1eea15982ea3 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/exclusions-parent-dependency-management/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + com.example + parent + 2.0.0 + + pom + parent + Parent + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + + org.example + example-exclusions + 3.0.0 + + + org.example + example-dependency2 + + + + + example-nested + org.example + 3.3.5 + + + org.example + example-api3 + + + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/exclusions/pom.xml b/pkg/dependency/parser/java/pom/testdata/exclusions/pom.xml new file mode 100644 index 000000000000..54e1ff11b47f --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/exclusions/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + + com.example + exclusions + 3.0.0 + + pom + exclusions + Exclusions + + + + org.example + example-nested + 3.3.3 + + + org.example + example-api + + + org.example + example-api-common + + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/get-fields-from-multiple-depmanagements/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/get-fields-from-multiple-depmanagements/parent/pom.xml new file mode 100644 index 000000000000..f11cfa1f158a --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/get-fields-from-multiple-depmanagements/parent/pom.xml @@ -0,0 +1,49 @@ + + + 4.0.0 + + com.example + parent + 1.0.0 + pom + + + + + org.example + example-api + 4.0.0 + + + org.example + example-api3 + 4.0.0 + compile + + + org.example + example-api4 + 4.0.0 + compile + + + org.example + example-api5 + 4.0.0 + test + + + org.example + example-api6 + runtime + + + org.example + example-api7 + runtime + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/get-fields-from-multiple-depmanagements/pom.xml b/pkg/dependency/parser/java/pom/testdata/get-fields-from-multiple-depmanagements/pom.xml new file mode 100644 index 000000000000..001af31bb4b2 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/get-fields-from-multiple-depmanagements/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + + com.example + get-fields-from-multiple-depmanagements + 1.0.0 + + + com.example + parent + 1.0.0 + ./parent/pom.xml + + + + + + org.example + example-api2 + 4.0.0 + compile + + + + + + + org.example + example-dependency + 4.0.0 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/happy/pom.xml b/pkg/dependency/parser/java/pom/testdata/happy/pom.xml new file mode 100644 index 000000000000..1f3c9697a17d --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/happy/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + com.example + happy + 1.0.0 + + happy + Example + + + + BSD-3-Clause + https://opensource.org/licenses/BSD-3-Clause + repo + + + + + + knqyf263 + https://github.com/knqyf263 + + + + + 1.7.30 + + + + + org.example + example-api + ${api.version} + + + org.example + example-runtime + 1.0.0 + runtime + + + org.example + example-provided + 999 + provided + + + diff --git a/pkg/dependency/parser/java/pom/testdata/hard-requirement/pom.xml b/pkg/dependency/parser/java/pom/testdata/hard-requirement/pom.xml new file mode 100644 index 000000000000..179cce4dde68 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/hard-requirement/pom.xml @@ -0,0 +1,39 @@ + + 4.0.0 + + com.example + hard + 1.0.0 + + soft + Example + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + knqyf263 + https://github.com/knqyf263 + + + + + + org.example + example-nested + 3.3.4 + + + org.example + example-dependency + 1.2.4 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/import-dependency-management-multiple/pom.xml b/pkg/dependency/parser/java/pom/testdata/import-dependency-management-multiple/pom.xml new file mode 100644 index 000000000000..c474b500cefa --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/import-dependency-management-multiple/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + com.example + import + 2.0.0 + + pom + import + Import dependencyManagement + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + + org.example + example-dependency-management + 2.2.2 + import + pom + + + + org.example + example-dependency-management2 + 1.1.1 + import + pom + + + + + + + org.example + example-api + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/import-dependency-management/pom.xml b/pkg/dependency/parser/java/pom/testdata/import-dependency-management/pom.xml new file mode 100644 index 000000000000..f26a79f3678c --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/import-dependency-management/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + com.example + import + 2.0.0 + + pom + import + Import dependencyManagement + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + + org.example + example-dependency-management + 3.3.3 + import + pom + + + + + + + org.example + example-api + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/inherit-license/module/pom.xml b/pkg/dependency/parser/java/pom/testdata/inherit-license/module/pom.xml new file mode 100644 index 000000000000..26acf4b82e92 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/inherit-license/module/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + + com.example.app + my-app + 1.0.0 + + pom + module + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/inherit-license/module/submodule/pom.xml b/pkg/dependency/parser/java/pom/testdata/inherit-license/module/submodule/pom.xml new file mode 100644 index 000000000000..a904d152f110 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/inherit-license/module/submodule/pom.xml @@ -0,0 +1,10 @@ + + 4.0.0 + + com.example.app + module + 1.0.0 + + + submodule + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/inherit-license/pom.xml b/pkg/dependency/parser/java/pom/testdata/inherit-license/pom.xml new file mode 100644 index 000000000000..2c080d502ff7 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/inherit-license/pom.xml @@ -0,0 +1,15 @@ + + 4.0.0 + pom + com.example.app + my-app + 1.0.0 + + + + Apache-2.0 + https://www.apache.org/licenses/LICENSE-2.0 + repo + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/inherit-props/base/pom.xml b/pkg/dependency/parser/java/pom/testdata/inherit-props/base/pom.xml new file mode 100644 index 000000000000..02422500889b --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/inherit-props/base/pom.xml @@ -0,0 +1,24 @@ + + + 4.0.0 + + com.example + parent + 1.0.0 + + com.example + test + 0.0.1-SNAPSHOT + Spring Boot project + + 2.0.0 + + + + org.example + example-api + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/inherit-props/pom.xml b/pkg/dependency/parser/java/pom/testdata/inherit-props/pom.xml new file mode 100644 index 000000000000..32437873d2ae --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/inherit-props/pom.xml @@ -0,0 +1,29 @@ + + 4.0.0 + + com.example + parent + 1.0.0 + + pom + parent + Parent + + + 1.0.0 + + + + + + org.example + example-bom + ${bom.version} + pom + import + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/inherit-scopes-from-child-deps-and-their-parents/pom.xml b/pkg/dependency/parser/java/pom/testdata/inherit-scopes-from-child-deps-and-their-parents/pom.xml new file mode 100644 index 000000000000..17e6c55de090 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/inherit-scopes-from-child-deps-and-their-parents/pom.xml @@ -0,0 +1,29 @@ + + 4.0.0 + + com.example + inherit-scopes-from-child-deps-and-their-parents + 0.0.1 + + + + org.example + example-nested-scope-runtime + 1.0.0 + runtime + + + org.example + example-nested-scope-compile + 1.0.0 + compile + + + org.example + example-nested-scope-empty + 1.0.0 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/inherit-scopes-in-children-from-root/pom.xml b/pkg/dependency/parser/java/pom/testdata/inherit-scopes-in-children-from-root/pom.xml new file mode 100644 index 000000000000..26bdb25621d7 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/inherit-scopes-in-children-from-root/pom.xml @@ -0,0 +1,64 @@ + + 4.0.0 + + com.example + inherit-scopes-in-children-from-root + 0.0.1 + + + + + org.example + example-scope-runtime + 2.0.0 + compile + + + org.example + example-scope-compile + 2.0.0 + runtime + + + org.example + example-api-compile + 3.0.0 + test + + + org.example + example-scope-empty + 2.0.0 + runtime + + + org.example + example-api-empty + 3.0.0 + test + + + + + + + org.example + example-nested-scope-runtime + 1.0.0 + runtime + + + org.example + example-nested-scope-compile + 1.0.0 + compile + + + org.example + example-nested-scope-empty + 1.0.0 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/inherit-scopes-in-parents-from-root/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/inherit-scopes-in-parents-from-root/parent/pom.xml new file mode 100644 index 000000000000..dad0101e3f25 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/inherit-scopes-in-parents-from-root/parent/pom.xml @@ -0,0 +1,77 @@ + + 4.0.0 + + com.example + inherit-scopes-in-parents-from-root-parent + 0.0.1 + pom + + + + + org.example + example-scope-runtime + 2.0.0 + compile + + + org.example + example-scope-compile + 2.0.0 + runtime + + + org.example + example-api-compile + 3.0.0 + test + + + org.example + example-scope-empty + 2.0.0 + test + + + org.example + example-api-empty + 3.0.0 + test + + + org.example + example-nested-scope-compile + 1.0.0 + runtime + + + org.example + example-nested-scope-empty + 1.0.0 + compile + + + + + + + org.example + example-nested-scope-runtime + 1.0.0 + runtime + + + org.example + example-nested-scope-compile + 1.0.0 + compile + + + org.example + example-nested-scope-empty + 1.0.0 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/inherit-scopes-in-parents-from-root/pom.xml b/pkg/dependency/parser/java/pom/testdata/inherit-scopes-in-parents-from-root/pom.xml new file mode 100644 index 000000000000..45ef37785e60 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/inherit-scopes-in-parents-from-root/pom.xml @@ -0,0 +1,39 @@ + + 4.0.0 + + com.example + inherit-scopes-in-parents-from-root + 0.1.0 + + + com.example + inherit-scopes-in-parents-from-root-parent + 0.0.1 + ./parent + + + + + + org.example + example-nested-scope-runtime + 1.0.0 + test + + + org.example + example-nested-scope-compile + 1.0.0 + test + + + org.example + example-nested-scope-empty + 1.0.0 + test + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/modules-infinity-loop/module-1/module-2/pom.xml b/pkg/dependency/parser/java/pom/testdata/modules-infinity-loop/module-1/module-2/pom.xml new file mode 100644 index 000000000000..37f39009ce97 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/modules-infinity-loop/module-1/module-2/pom.xml @@ -0,0 +1,16 @@ + + 4.0.0 + + module-2 + org.example + 3.0.0 + + + + org.example + module-1 + 2.0.0 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/modules-infinity-loop/module-1/pom.xml b/pkg/dependency/parser/java/pom/testdata/modules-infinity-loop/module-1/pom.xml new file mode 100644 index 000000000000..9952a80dc685 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/modules-infinity-loop/module-1/pom.xml @@ -0,0 +1,12 @@ + + 4.0.0 + + module-1 + org.example + 2.0.0 + + + module-2 + + diff --git a/pkg/dependency/parser/java/pom/testdata/modules-infinity-loop/pom.xml b/pkg/dependency/parser/java/pom/testdata/modules-infinity-loop/pom.xml new file mode 100644 index 000000000000..372fefd3fce9 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/modules-infinity-loop/pom.xml @@ -0,0 +1,13 @@ + + 4.0.0 + + root + org.example + 1.0.0 + + + module-1 + module-2 + + diff --git a/pkg/dependency/parser/java/pom/testdata/multi-module-soft-requirement/module1/pom.xml b/pkg/dependency/parser/java/pom/testdata/multi-module-soft-requirement/module1/pom.xml new file mode 100644 index 000000000000..78ba97541db4 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multi-module-soft-requirement/module1/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + com.example + module1 + 1.1.1 + + module1 + Module 1 + + + 1.7.30 + + + + + org.example + example-api + ${api.version} + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multi-module-soft-requirement/module2/pom.xml b/pkg/dependency/parser/java/pom/testdata/multi-module-soft-requirement/module2/pom.xml new file mode 100644 index 000000000000..a2a1373f5fe8 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multi-module-soft-requirement/module2/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + com.example + module2 + 1.1.1 + + module2 + Module 2 + + + 2.0.0 + + + + + org.example + example-api + ${api.version} + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multi-module-soft-requirement/pom.xml b/pkg/dependency/parser/java/pom/testdata/multi-module-soft-requirement/pom.xml new file mode 100644 index 000000000000..c0b0789bfbed --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multi-module-soft-requirement/pom.xml @@ -0,0 +1,18 @@ + + 4.0.0 + + com.example + aggregation + 1.0.0 + + pom + aggregation + Aggregation + + + module1 + module2 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multi-module/module/pom.xml b/pkg/dependency/parser/java/pom/testdata/multi-module/module/pom.xml new file mode 100644 index 000000000000..ea72b3e29878 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multi-module/module/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + com.example + module + 1.1.1 + + module + Module + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-dependency + 1.2.3 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multi-module/pom.xml b/pkg/dependency/parser/java/pom/testdata/multi-module/pom.xml new file mode 100644 index 000000000000..398eb1f3850f --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multi-module/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + com.example + aggregation + 1.0.0 + + pom + aggregation + Aggregation + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + module + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav-with-props/module1/pom.xml b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav-with-props/module1/pom.xml new file mode 100644 index 000000000000..9a77e4d15499 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav-with-props/module1/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + com.example + module1 + 1.0.0 + + + + + org.example + example-api + 1.7.30 + + + + + + + org.example + example-dependency + 1.2.5 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav-with-props/module2/pom.xml b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav-with-props/module2/pom.xml new file mode 100644 index 000000000000..78f22c3185ab --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav-with-props/module2/pom.xml @@ -0,0 +1,26 @@ + + 4.0.0 + + com.example + module2 + 2.0.0 + + + + + org.example + example-api + 2.0.0 + + + + + + + org.example + example-dependency + 1.2.5 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav-with-props/pom.xml b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav-with-props/pom.xml new file mode 100644 index 000000000000..a378df7182ee --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav-with-props/pom.xml @@ -0,0 +1,16 @@ + + 4.0.0 + + com.example + root + 1.0.0 + pom + + + module1 + module2 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav/module1/pom.xml b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav/module1/pom.xml new file mode 100644 index 000000000000..9c0302e65a3e --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav/module1/pom.xml @@ -0,0 +1,16 @@ + + 4.0.0 + + com.example + module1 + 1.0.0 + + + + org.example + example-api + 1.7.30 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav/module2/pom.xml b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav/module2/pom.xml new file mode 100644 index 000000000000..fad3196cc7fe --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav/module2/pom.xml @@ -0,0 +1,16 @@ + + 4.0.0 + + com.example + module2 + 2.0.0 + + + + org.example + example-api + 1.7.30 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav/pom.xml b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav/pom.xml new file mode 100644 index 000000000000..a378df7182ee --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multiple-modules-with-deps-with-same-gav/pom.xml @@ -0,0 +1,16 @@ + + 4.0.0 + + com.example + root + 1.0.0 + pom + + + module1 + module2 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/multiply-licenses/pom.xml b/pkg/dependency/parser/java/pom/testdata/multiply-licenses/pom.xml new file mode 100644 index 000000000000..1f9609e670a9 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/multiply-licenses/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + com.example + multiply-licenses + 1.0.0 + + multiply-licenses + Example + + + + MIT + All source code is under the MIT license. + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + diff --git a/pkg/dependency/parser/java/pom/testdata/nested-modules/module1/module2/pom.xml b/pkg/dependency/parser/java/pom/testdata/nested-modules/module1/module2/pom.xml new file mode 100644 index 000000000000..16417a171747 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/nested-modules/module1/module2/pom.xml @@ -0,0 +1,18 @@ + + 4.0.0 + + com.example + module2 + 2.0.0 + + pom + + + + org.example + example-api + 1.7.30 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/nested-modules/module1/pom.xml b/pkg/dependency/parser/java/pom/testdata/nested-modules/module1/pom.xml new file mode 100644 index 000000000000..9e22eb6ba815 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/nested-modules/module1/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + + com.example + module1 + 1.0.0 + + pom + + + module2 + + diff --git a/pkg/dependency/parser/java/pom/testdata/nested-modules/pom.xml b/pkg/dependency/parser/java/pom/testdata/nested-modules/pom.xml new file mode 100644 index 000000000000..48d741859053 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/nested-modules/pom.xml @@ -0,0 +1,16 @@ + + 4.0.0 + + com.example + root + 1.0.0 + + pom + + + module1 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/no-parent-infinity-loop/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/no-parent-infinity-loop/parent/pom.xml new file mode 100644 index 000000000000..0af9c80ea494 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/no-parent-infinity-loop/parent/pom.xml @@ -0,0 +1,19 @@ + + 4.0.0 + + com.example + parent + 1.0.0 + + pom + parent + Parent + + + org.example + example-api + 1.7.30 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/no-parent-infinity-loop/pom.xml b/pkg/dependency/parser/java/pom/testdata/no-parent-infinity-loop/pom.xml new file mode 100644 index 000000000000..f2c9bfb7f078 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/no-parent-infinity-loop/pom.xml @@ -0,0 +1,17 @@ + + 4.0.0 + + child + + child + Child + + + com.example + parent + 1.0.0 + ./parent + + + diff --git a/pkg/dependency/parser/java/pom/testdata/not-found-dependency/pom.xml b/pkg/dependency/parser/java/pom/testdata/not-found-dependency/pom.xml new file mode 100644 index 000000000000..e1a6ff726502 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/not-found-dependency/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + com.example + not-found-dependency + 1.0.0 + + sad + Sad path + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-not-found + 999 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/not-found-module/pom.xml b/pkg/dependency/parser/java/pom/testdata/not-found-module/pom.xml new file mode 100644 index 000000000000..398eb1f3850f --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/not-found-module/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + com.example + aggregation + 1.0.0 + + pom + aggregation + Aggregation + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + module + + + diff --git a/pkg/dependency/parser/java/pom/testdata/not-found-parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/not-found-parent/pom.xml new file mode 100644 index 000000000000..bde412e53455 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/not-found-parent/pom.xml @@ -0,0 +1,33 @@ + + 4.0.0 + + com.example + no-parent + 1.0-SNAPSHOT + + no-parent + Parent not found + + + com.example + parent + 1.0.0 + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-api + 1.7.30 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/offline/pom.xml b/pkg/dependency/parser/java/pom/testdata/offline/pom.xml new file mode 100644 index 000000000000..837d5837a884 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/offline/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + child + + child + Child + + + com.example + parent + ${revision}-${changelist} + + + + + org.example + example-offline + 2.3.4 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-child-properties/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-child-properties/parent/pom.xml new file mode 100644 index 000000000000..d80410c34988 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-child-properties/parent/pom.xml @@ -0,0 +1,30 @@ + + 4.0.0 + + com.example + parent + 4.0.3 + + pom + parent + Parent + + + 3.0.0 + 1.2.3 + + + + + + org.example + example-bom + ${bom.version} + pom + import + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-child-properties/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-child-properties/pom.xml new file mode 100644 index 000000000000..92d978a44c87 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-child-properties/pom.xml @@ -0,0 +1,36 @@ + + 4.0.0 + + child + + child + 1.2.3 + Child + + + com.example + parent + 4.0.3 + ./parent + + + + 4.0.0 + 1.1.1 + 2.3.4 + + + + + org.example + example-dependency + ${project.version} + + + org.example + example-api3 + ${project.parent.version} + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-dependencies/child/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-dependencies/child/pom.xml new file mode 100644 index 000000000000..25a07f9f3644 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-dependencies/child/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + child + + child + Child + + + com.example + parent + ${revision}-${changelist} + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-dependencies/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-dependencies/pom.xml new file mode 100644 index 000000000000..2e7ebf581e66 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-dependencies/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + + com.example + parent + ${revision}${changelist} + + pom + parent + Parent + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + 1.0.0 + -SNAPSHOT + 1.7.30 + + + + + org.example + example-api + ${api.version} + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-dependency-management/child/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-dependency-management/child/pom.xml new file mode 100644 index 000000000000..cbb3ea6f83ef --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-dependency-management/child/pom.xml @@ -0,0 +1,32 @@ + + 4.0.0 + + child + 3.0.0 + + child + Child + + + com.example + parent + 2.0.0 + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-api + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-dependency-management/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-dependency-management/pom.xml new file mode 100644 index 000000000000..a6fb717a8dbf --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-dependency-management/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + + com.example + parent + 2.0.0 + + pom + parent + Parent + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + 1.7.30 + + + + + + org.example + example-api + ${api.version} + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-properties/child/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-properties/child/pom.xml new file mode 100644 index 000000000000..145d77f5da14 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-properties/child/pom.xml @@ -0,0 +1,39 @@ + + 4.0.0 + + child + + child + Child + + + com.example + parent + 1.0.0 + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + + org.example + example-api + ${api.version} + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-properties/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-properties/pom.xml new file mode 100644 index 000000000000..42ffe5a2a2be --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-properties/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + com.example + parent + 1.0.0 + + pom + parent + Parent + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + knqyf263 + https://github.com/knqyf263 + + + + + + + repo.jenkins-ci.org + https://repo.jenkins-ci.org/public/ + + + + + 1.7.30 + + + + + org.example + example-api + 2.2.2 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-relative-path/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-relative-path/parent/pom.xml new file mode 100644 index 000000000000..e62b4cf06eb7 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-relative-path/parent/pom.xml @@ -0,0 +1,40 @@ + + 4.0.0 + + com.example + parent + 1.0.0 + + pom + parent + Parent + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + knqyf263 + https://github.com/knqyf263 + + + + + 1.7.30 + + + + + org.example + example-api + ${api.version} + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-relative-path/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-relative-path/pom.xml new file mode 100644 index 000000000000..ac95e2178777 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-relative-path/pom.xml @@ -0,0 +1,32 @@ + + 4.0.0 + + child + + child + Child + + + com.example + parent + 1.0.0 + ./parent + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-api + ${api.version} + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-remote-repository/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-remote-repository/pom.xml new file mode 100644 index 000000000000..18033a7bc7b9 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-remote-repository/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + + child + + child + Child + + + org.example + example-parent + 1.0.0 + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-api + 1.7.30 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-version-is-property/child/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-version-is-property/child/pom.xml new file mode 100644 index 000000000000..7581c228ef10 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-version-is-property/child/pom.xml @@ -0,0 +1,25 @@ + + 4.0.0 + + child + + child + ${revision} + Child + + + com.example + parent + ${revision} + ../pom.xml + + + + + org.example + example-api + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/parent-version-is-property/pom.xml b/pkg/dependency/parser/java/pom/testdata/parent-version-is-property/pom.xml new file mode 100644 index 000000000000..5848d053447f --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/parent-version-is-property/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + com.example + parent + ${revision} + + pom + parent + Parent + + + 1.0.0-SNAPSHOT + + + + + + org.example + example-api + 1.1.1 + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/project-version-from-parent/child/pom.xml b/pkg/dependency/parser/java/pom/testdata/project-version-from-parent/child/pom.xml new file mode 100644 index 000000000000..96ebac536822 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/project-version-from-parent/child/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + child + + child + Child + + + com.example + parent + 2.0.0 + ../parent + + + + + org.example + example-api + ${project.version} + + + diff --git a/pkg/dependency/parser/java/pom/testdata/project-version-from-parent/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/project-version-from-parent/parent/pom.xml new file mode 100644 index 000000000000..0b9266b87df0 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/project-version-from-parent/parent/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + + com.example + top-parent + 3.0.0 + ../top-parent + + + + com.example + parent + 2.0.0 + + pom + parent + Parent + + diff --git a/pkg/dependency/parser/java/pom/testdata/project-version-from-parent/top-parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/project-version-from-parent/top-parent/pom.xml new file mode 100644 index 000000000000..cd26c583bf09 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/project-version-from-parent/top-parent/pom.xml @@ -0,0 +1,13 @@ + + 4.0.0 + + com.example + top-parent + 3.0.0 + + pom + top-parent + Parent + + diff --git a/pkg/dependency/parser/java/pom/testdata/repo-from-root-for-dep-from-module/module/pom.xml b/pkg/dependency/parser/java/pom/testdata/repo-from-root-for-dep-from-module/module/pom.xml new file mode 100644 index 000000000000..a57753237593 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repo-from-root-for-dep-from-module/module/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + + com.example + root-pom-with-module + 1.0.0 + + + module + + + + org.example + example-api + 1.0.0 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repo-from-root-for-dep-from-module/pom.xml b/pkg/dependency/parser/java/pom/testdata/repo-from-root-for-dep-from-module/pom.xml new file mode 100644 index 000000000000..865d8b98e7f4 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repo-from-root-for-dep-from-module/pom.xml @@ -0,0 +1,23 @@ + + 4.0.0 + + com.example + root-pom-with-module + 1.0.0 + pom + + + + root-repo + REPO_ROOT_URL + + true + + + + + + module + + diff --git a/pkg/dependency/parser/java/pom/testdata/repo-from-root-for-dep-from-module/repo-artifacts.txtar b/pkg/dependency/parser/java/pom/testdata/repo-from-root-for-dep-from-module/repo-artifacts.txtar new file mode 100644 index 000000000000..dd7fc50257e5 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repo-from-root-for-dep-from-module/repo-artifacts.txtar @@ -0,0 +1,26 @@ +Test case artifacts for root POM with module test. + +-- org/example/example-api/1.0.0/example-api-1.0.0.pom -- + + + + 4.0.0 + + org.example + example-api + 1.0.0 + + jar + Example API + Example API for testing + + + + License from root repo + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository-for-settings-repo/org/example/example-api/1.7.30/example-api-1.7.30.pom b/pkg/dependency/parser/java/pom/testdata/repository-for-settings-repo/org/example/example-api/1.7.30/example-api-1.7.30.pom new file mode 100644 index 000000000000..e357908858f2 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository-for-settings-repo/org/example/example-api/1.7.30/example-api-1.7.30.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-api + 1.7.30 + + jar + Example API Module + The example API + + + + Custom License from custom repo + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-api/1.7.30/example-api-1.7.30.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-api/1.7.30/example-api-1.7.30.pom new file mode 100644 index 000000000000..6d0cb6692de2 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-api/1.7.30/example-api-1.7.30.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-api + 1.7.30 + + jar + Example API Module + The example API + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-api/2.0.0/example-api-2.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-api/2.0.0/example-api-2.0.0.pom new file mode 100644 index 000000000000..283d1e0fd4dd --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-api/2.0.0/example-api-2.0.0.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-api + 2.0.0 + + jar + Example API Module + The example API + + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/1.0.0/example-bom-1.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/1.0.0/example-bom-1.0.0.pom new file mode 100644 index 000000000000..8b11960b0e2a --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/1.0.0/example-bom-1.0.0.pom @@ -0,0 +1,23 @@ + + 4.0.0 + + org.example + example-bom + 1.0.0 + + pom + example-bom + Example Bom + + + + + org.example + example-api + 1.7.30 + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/2.0.0/example-bom-2.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/2.0.0/example-bom-2.0.0.pom new file mode 100644 index 000000000000..dfec17c9f838 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/2.0.0/example-bom-2.0.0.pom @@ -0,0 +1,23 @@ + + 4.0.0 + + org.example + example-bom + 2.0.0 + + pom + example-bom + Example Bom + + + + + org.example + example-api + 2.0.0 + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/3.0.0/example-bom-3.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/3.0.0/example-bom-3.0.0.pom new file mode 100644 index 000000000000..cd63e109b570 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/3.0.0/example-bom-3.0.0.pom @@ -0,0 +1,27 @@ + + 4.0.0 + + org.example + example-bom + 3.0.0 + + pom + example-bom + Example Bom + + + 3.0.0 + + + + + + org.example + example-api + ${api.version} + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/4.0.0/example-bom-4.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/4.0.0/example-bom-4.0.0.pom new file mode 100644 index 000000000000..1dda07117915 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-bom/4.0.0/example-bom-4.0.0.pom @@ -0,0 +1,27 @@ + + 4.0.0 + + org.example + example-bom + 4.0.0 + + pom + example-bom + Example Bom + + + 4.0.0 + + + + + + org.example + example-api + ${api.version} + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-child-parent/2.0.0/example-child-parent-2.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-child-parent/2.0.0/example-child-parent-2.0.0.pom new file mode 100644 index 000000000000..451b2e90eb89 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-child-parent/2.0.0/example-child-parent-2.0.0.pom @@ -0,0 +1,23 @@ + + 4.0.0 + + org.example + example-child-parent + 2.0.0 + + pom + example-child-parent + Child-Parent + + + + + org.example + example-api + 2.7.30 + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-child/2.0.0/example-child-2.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-child/2.0.0/example-child-2.0.0.pom new file mode 100644 index 000000000000..3be0f6d65d93 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-child/2.0.0/example-child-2.0.0.pom @@ -0,0 +1,34 @@ + + 4.0.0 + + org.example + example-child + 2.0.0 + + pom + example-child + example-child-parent + + + org.example + example-child-parent + 2.0.0 + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-api + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management/2.2.2/example-dependency-management-2.2.2.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management/2.2.2/example-dependency-management-2.2.2.pom new file mode 100644 index 000000000000..e0d2488bfcdc --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management/2.2.2/example-dependency-management-2.2.2.pom @@ -0,0 +1,25 @@ + + + + 4.0.0 + + org.example + example-dependency-management + 2.2.2 + + pom + Example API Dependency Management + The example API + + + + + org.example + example-api + 1.7.30 + + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management/3.3.3/example-dependency-management-3.3.3.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management/3.3.3/example-dependency-management-3.3.3.pom new file mode 100644 index 000000000000..000e70f101e2 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management/3.3.3/example-dependency-management-3.3.3.pom @@ -0,0 +1,39 @@ + + + + 4.0.0 + + org.example + example-dependency-management + 3.3.3 + + pom + Example API Dependency Management + The example API + + + 1.1.1 + + + + + + org.example + example-dependency-management2 + ${project.managed.version} + import + pom + + + + + + + org.example + example-dependency + 1.2.3 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management2/1.1.1/example-dependency-management2-1.1.1.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management2/1.1.1/example-dependency-management2-1.1.1.pom new file mode 100644 index 000000000000..58cdbf8cf7a6 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management2/1.1.1/example-dependency-management2-1.1.1.pom @@ -0,0 +1,29 @@ + + + + 4.0.0 + + org.example + example-dependency-management2 + 1.1.1 + + pom + Example API Dependency Management + The example API + + + 1.7.30 + + + + + + org.example + example-api + ${project.managed.version} + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management3/1.1.1/example-dependency-management3-1.1.1.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management3/1.1.1/example-dependency-management3-1.1.1.pom new file mode 100644 index 000000000000..fa82d974ea15 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency-management3/1.1.1/example-dependency-management3-1.1.1.pom @@ -0,0 +1,34 @@ + + + + 4.0.0 + + org.example + example-dependency-management3 + 1.1.1 + + pom + Example API Dependency Management + The example API + + + + + + org.example + example-api + 1.7.30 + + + + + + + org.example + example-dependency + 1.2.3 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.3-SNAPSHOT/example-dependency-1.2.3-SNAPSHOT.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.3-SNAPSHOT/example-dependency-1.2.3-SNAPSHOT.pom new file mode 100644 index 000000000000..2ad90646e158 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.3-SNAPSHOT/example-dependency-1.2.3-SNAPSHOT.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-dependency + 1.2.3-SNAPSHOT + + jar + Example API Dependency + The example API + + + + org.example + example-api + 2.0.0 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.3/example-dependency-1.2.3.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.3/example-dependency-1.2.3.pom new file mode 100644 index 000000000000..4eacd92abcd3 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.3/example-dependency-1.2.3.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-dependency + 1.2.3 + + jar + Example API Dependency + The example API + + + + org.example + example-api + 2.0.0 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.4/example-dependency-1.2.4.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.4/example-dependency-1.2.4.pom new file mode 100644 index 000000000000..17a8717d2cec --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.4/example-dependency-1.2.4.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-dependency + 1.2.3 + + jar + Example API Dependency + The example API + + + + org.example + example-api + [2.0.0] + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.5/example-dependency-1.2.5.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.5/example-dependency-1.2.5.pom new file mode 100644 index 000000000000..1263bc83a400 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.2.5/example-dependency-1.2.5.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-dependency + 1.2.5 + + jar + Example API Dependency + The example API + + + + org.example + example-api + 1.0.0 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.7.30/example-dependency-1.7.30.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.7.30/example-dependency-1.7.30.pom new file mode 100644 index 000000000000..c897c4a7d8c0 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/1.7.30/example-dependency-1.7.30.pom @@ -0,0 +1,18 @@ + + + 4.0.0 + + org.example + example-dependency + 1.7.30 + + + + ${project.groupId} + example-api + ${project.version} + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/2.0.0/example-dependency-2.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/2.0.0/example-dependency-2.0.0.pom new file mode 100644 index 000000000000..3f236cdf314e --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/2.0.0/example-dependency-2.0.0.pom @@ -0,0 +1,26 @@ + + + 4.0.0 + + + org.example + example-parent + 3.0.0 + + + org.example + example-dependency + 2.0.0 + + + + + org.example + example-api + 2.0.1 + + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/2.17.0-SNAPSHOT/example-dependency-2.17.0-20240312.035235-10.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/2.17.0-SNAPSHOT/example-dependency-2.17.0-20240312.035235-10.pom new file mode 100644 index 000000000000..0ecec117f873 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/2.17.0-SNAPSHOT/example-dependency-2.17.0-20240312.035235-10.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-dependency + 2.17.0-SNAPSHOT + + jar + Example API Dependency + The example API + + + + org.example + example-api + 2.0.0 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/2.17.0-SNAPSHOT/maven-metadata.xml b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/2.17.0-SNAPSHOT/maven-metadata.xml new file mode 100644 index 000000000000..258de9db99c5 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/2.17.0-SNAPSHOT/maven-metadata.xml @@ -0,0 +1,35 @@ + + org.example + example-dependency + + 20240312035235 + + 20240312.035235 + 10 + + + + sources + jar + 2.17.0-20240312.035235-10 + 20240312035235 + + + module + 2.17.0-20240312.035235-10 + 20240312035235 + + + jar + 2.17.0-20240312.035235-10 + 20240312035235 + + + pom + 2.17.0-20240312.035235-10 + 20240312035235 + + + + 2.17.0-SNAPSHOT + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/4.0.0/example-dependency-4.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/4.0.0/example-dependency-4.0.0.pom new file mode 100644 index 000000000000..61ac950ac10e --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency/4.0.0/example-dependency-4.0.0.pom @@ -0,0 +1,53 @@ + + + + 4.0.0 + + + org.example + example-parent + 4.0.0 + + + org.example + example-dependency + 4.0.0 + + jar + Example API Dependency + The example API + + + + org.example + example-api + + + org.example + example-api2 + + + org.example + example-api3 + + + org.example + example-api4 + + + org.example + example-api5 + + + org.example + example-api6 + + + org.example + example-api7 + test + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency2/2.3.4/example-dependency2-2.3.4.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency2/2.3.4/example-dependency2-2.3.4.pom new file mode 100644 index 000000000000..e2b9395f05d2 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-dependency2/2.3.4/example-dependency2-2.3.4.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-dependency2 + 2.3.4 + + jar + Example API Dependency + The example API + + + + org.example + example-api + 1.7.30 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-exclusions/3.0.0/example-exclusions-3.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-exclusions/3.0.0/example-exclusions-3.0.0.pom new file mode 100644 index 000000000000..85b95a6627d5 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-exclusions/3.0.0/example-exclusions-3.0.0.pom @@ -0,0 +1,33 @@ + + 4.0.0 + + org.example + example-exclusions + 3.0.0 + + + + org.example + example-dependency + 1.2.3 + + + org.example + example-dependency2 + 2.3.4 + + + org.example + example-nested + 3.3.5 + + + org.example + example-api2 + + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-exclusions/4.0.0/example-exclusions-4.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-exclusions/4.0.0/example-exclusions-4.0.0.pom new file mode 100644 index 000000000000..7e02d67c6b1d --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-exclusions/4.0.0/example-exclusions-4.0.0.pom @@ -0,0 +1,28 @@ + + 4.0.0 + + org.example + example-exclusions + 4.0.0 + + + + org.example + example-dependency + 1.2.3 + + + org.example + example-api + + + + + org.example + example-api + 1.7.30 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-compile/1.0.0/example-nested-scope-compile-1.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-compile/1.0.0/example-nested-scope-compile-1.0.0.pom new file mode 100644 index 000000000000..43ca478a88cd --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-compile/1.0.0/example-nested-scope-compile-1.0.0.pom @@ -0,0 +1,41 @@ + + + + 4.0.0 + + + com.example + example-nested-parent-scope-compile + 1.0.1 + ./parent + + + org.example + example-nested-scope-compile + 1.0.0 + + jar + Example pom with example-scope-compile 2.0.0 + + + + + org.example + example-api-compile + 3.0.0 + runtime + + + + + + + org.example + example-scope-compile + 2.0.0 + compile + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-compile/1.0.0/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-compile/1.0.0/parent/pom.xml new file mode 100644 index 000000000000..5fe41b350df1 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-compile/1.0.0/parent/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + com.example + example-nested-parent-scope-compile + 1.0.1 + pom + + + + + org.example + example-scope-compile + 2.0.0 + test + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-empty/1.0.0/example-nested-scope-empty-1.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-empty/1.0.0/example-nested-scope-empty-1.0.0.pom new file mode 100644 index 000000000000..b1def2383f79 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-empty/1.0.0/example-nested-scope-empty-1.0.0.pom @@ -0,0 +1,40 @@ + + + + 4.0.0 + + + com.example + example-nested-parent-scope-empty + 1.0.1 + ./parent + + + org.example + example-nested-scope-empty + 1.0.0 + + jar + Example pom with example-scope-empty 2.0.0 + + + + + org.example + example-api-empty + 3.0.0 + test + + + + + + + org.example + example-scope-empty + 2.0.0 + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-empty/1.0.0/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-empty/1.0.0/parent/pom.xml new file mode 100644 index 000000000000..6ca0232f4fc3 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-empty/1.0.0/parent/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + com.example + example-nested-parent-scope-empty + 1.0.1 + pom + + + + + org.example + example-scope-empty + 2.0.0 + compile + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-runtime/1.0.0/example-nested-scope-runtime-1.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-runtime/1.0.0/example-nested-scope-runtime-1.0.0.pom new file mode 100644 index 000000000000..8e48dcd812c3 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-runtime/1.0.0/example-nested-scope-runtime-1.0.0.pom @@ -0,0 +1,41 @@ + + + + 4.0.0 + + + com.example + example-nested-parent-scope-runtime + 1.0.1 + ./parent + + + org.example + example-nested-scope-runtime + 1.0.0 + + jar + Example pom with example-scope-runtime 2.0.0 + + + + + org.example + example-api-runtime + 3.0.0 + test + + + + + + + org.example + example-scope-runtime + 2.0.0 + runtime + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-runtime/1.0.0/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-runtime/1.0.0/parent/pom.xml new file mode 100644 index 000000000000..f59df7902390 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested-scope-runtime/1.0.0/parent/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + com.example + example-nested-parent-scope-runtime + 1.0.1 + pom + + + + + org.example + example-scope-runtime + 2.0.0 + test + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested/3.3.3/example-nested-3.3.3.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested/3.3.3/example-nested-3.3.3.pom new file mode 100644 index 000000000000..1d618e0ca020 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested/3.3.3/example-nested-3.3.3.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-nested + 3.3.3 + + jar + Example API Dependency + The example API + + + + org.example + example-dependency + 1.2.3 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested/3.3.4/example-nested-3.3.4.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested/3.3.4/example-nested-3.3.4.pom new file mode 100644 index 000000000000..691a8400c381 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested/3.3.4/example-nested-3.3.4.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-nested + 3.3.4 + + jar + Example API Dependency + The example API + + + + org.example + example-dependency + [1.2.3] + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested/3.3.5/example-nested-3.3.5.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested/3.3.5/example-nested-3.3.5.pom new file mode 100644 index 000000000000..918747cf9a40 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-nested/3.3.5/example-nested-3.3.5.pom @@ -0,0 +1,33 @@ + + + + 4.0.0 + + org.example + example-nested + 3.3.3 + + jar + Example API Dependency + The example API + + + + org.example + example-dependency + 1.2.3 + + + org.example + example-api2 + 2.0.0 + + + org.example + example-api3 + 3.0.0 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-parent/1.0.0/example-parent-1.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-parent/1.0.0/example-parent-1.0.0.pom new file mode 100644 index 000000000000..3a4d8749e168 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-parent/1.0.0/example-parent-1.0.0.pom @@ -0,0 +1,14 @@ + + + + 4.0.0 + + org.example + example-parent + 1.0.0 + + pom + Example API Parent + The example API + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-parent/3.0.0/example-parent-3.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-parent/3.0.0/example-parent-3.0.0.pom new file mode 100644 index 000000000000..917908d53d7b --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-parent/3.0.0/example-parent-3.0.0.pom @@ -0,0 +1,32 @@ + + + + 4.0.0 + + org.example + example-parent + 3.0.0 + pom + + + 3.0.1 + + + + + + org.example + example-api + ${api.version} + + + + + + + org.example + example-api + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-parent/4.0.0/example-parent-4.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-parent/4.0.0/example-parent-4.0.0.pom new file mode 100644 index 000000000000..cc722579c71f --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-parent/4.0.0/example-parent-4.0.0.pom @@ -0,0 +1,56 @@ + + + + 4.0.0 + + org.example + example-parent + 4.0.0 + pom + + + + + org.example + example-api + 1.7.30 + test + + + org.example + example-api2 + 1.7.30 + test + + + org.example + example-api3 + 1.7.30 + test + + + org.example + example-api4 + 1.7.30 + runtime + + + org.example + example-api5 + 1.7.30 + compile + + + org.example + example-api6 + 1.7.30 + + + org.example + example-api7 + 1.7.30 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-scope-compile/2.0.0/example-scope-compile-2.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-scope-compile/2.0.0/example-scope-compile-2.0.0.pom new file mode 100644 index 000000000000..1d5bc02ff3b9 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-scope-compile/2.0.0/example-scope-compile-2.0.0.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-scope-compile + 2.0.0 + + jar + Example pom with example-api-compile 3.0.0 + + + + org.example + example-api-compile + 3.0.0 + compile + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-scope-empty/2.0.0/example-scope-empty-2.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-scope-empty/2.0.0/example-scope-empty-2.0.0.pom new file mode 100644 index 000000000000..82ce31008fc3 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-scope-empty/2.0.0/example-scope-empty-2.0.0.pom @@ -0,0 +1,22 @@ + + + + 4.0.0 + + org.example + example-scope-empty + 2.0.0 + + jar + Example pom with example-api-empty 3.0.0 + + + + org.example + example-api-empty + 3.0.0 + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-scope-runtime/2.0.0/example-scope-runtime-2.0.0.pom b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-scope-runtime/2.0.0/example-scope-runtime-2.0.0.pom new file mode 100644 index 000000000000..90bf973def80 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/repository/org/example/example-scope-runtime/2.0.0/example-scope-runtime-2.0.0.pom @@ -0,0 +1,23 @@ + + + + 4.0.0 + + org.example + example-scope-runtime + 2.0.0 + + jar + Example pom with example-api-runtime 3.0.0 + + + + org.example + example-api-runtime + 3.0.0 + runtime + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/root-pom-dep-management-for-deps-with-project-props/pom.xml b/pkg/dependency/parser/java/pom/testdata/root-pom-dep-management-for-deps-with-project-props/pom.xml new file mode 100644 index 000000000000..3f8a6a317962 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/root-pom-dep-management-for-deps-with-project-props/pom.xml @@ -0,0 +1,28 @@ + + 4.0.0 + + com.example + root-pom-dep-management-for-deps-with-project-props + 1.0.0 + pom + + + + + org.example + example-api + 2.0.0 + + + + + + + org.example + example-dependency + 1.7.30 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/root-pom-dep-management/pom.xml b/pkg/dependency/parser/java/pom/testdata/root-pom-dep-management/pom.xml new file mode 100644 index 000000000000..c5cfa4e56871 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/root-pom-dep-management/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + com.example + root-pom-dep-management + 1.0.0 + + + + + org.example + example-dependency + 1.2.4 + + + + + + + org.example + example-nested + 3.3.3 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/settings/global/conf/settings.xml b/pkg/dependency/parser/java/pom/testdata/settings/global/conf/settings.xml new file mode 100644 index 000000000000..5ad67448e64a --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/settings/global/conf/settings.xml @@ -0,0 +1,63 @@ + + + testdata/repository + + + + global-server + + + server-with-credentials + test-user + test-password-from-global + + + server-with-name-only + test-user-only + + + + + mycompany-global + + + mycompany-internal-releases + https://mycompany.example.com/repository/internal-releases + + false + + + + mycompany-global-releases + https://mycompany.example.com/repository/global-releases + + true + + + false + + + + + + default + + + mycompany-default-releases + https://mycompany.example.com/repository/default-releases + + true + + + false + + + + + true + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/settings/user-settings-with-env-placeholders/.m2/settings.xml b/pkg/dependency/parser/java/pom/testdata/settings/user-settings-with-env-placeholders/.m2/settings.xml new file mode 100755 index 000000000000..33a3c3bd6a7a --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/settings/user-settings-with-env-placeholders/.m2/settings.xml @@ -0,0 +1,42 @@ + + + ${env.LOCAL_REPO_PT_1}/${env.LOCAL_REPO_PT_2}/.m2/repository + + + + user-server + + + ${env.SERVER_ID} + ${env.USERNAME} + ${env.PASSWORD} + + + server-with-name-only + test-user-only + + + + + ${env.PROFILE_ID} + + + ${env.REPO_ID} + ${env.REPO_URL}/repository/user-releases + + true + fail + + + false + + + + + + + ${env.PROFILE_ID} + + diff --git a/pkg/dependency/parser/java/pom/testdata/settings/user/.m2/settings.xml b/pkg/dependency/parser/java/pom/testdata/settings/user/.m2/settings.xml new file mode 100755 index 000000000000..240748f48abb --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/settings/user/.m2/settings.xml @@ -0,0 +1,55 @@ + + + testdata/user/repository + + + + user-server + + + server-with-credentials + test-user + test-password + + + server-with-name-only + test-user-only + + + + + mycompany-global + + + mycompany-releases + https://mycompany.example.com/repository/user-releases + + true + fail + + + false + + + + mycompany-user-snapshots + https://mycompany.example.com/repository/user-snapshots + + false + + + true + + + + + true + + + + + mycompany-global + + diff --git a/pkg/dependency/parser/java/pom/testdata/snapshot/pom.xml b/pkg/dependency/parser/java/pom/testdata/snapshot/pom.xml new file mode 100644 index 000000000000..7294ecf0162f --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/snapshot/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + com.example + happy + 1.0.0 + + happy + Example + + + + + org.example + example-dependency + 1.2.3-SNAPSHOT + + + diff --git a/pkg/dependency/parser/java/pom/testdata/snapshot/with-maven-metadata/pom.xml b/pkg/dependency/parser/java/pom/testdata/snapshot/with-maven-metadata/pom.xml new file mode 100644 index 000000000000..7e4c2144d417 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/snapshot/with-maven-metadata/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + com.example + happy + 1.0.0 + + happy + Example + + + + + org.example + example-dependency + 2.17.0-SNAPSHOT + + + diff --git a/pkg/dependency/parser/java/pom/testdata/soft-requirement-with-transitive-dependencies/pom.xml b/pkg/dependency/parser/java/pom/testdata/soft-requirement-with-transitive-dependencies/pom.xml new file mode 100644 index 000000000000..76d62b2e8174 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/soft-requirement-with-transitive-dependencies/pom.xml @@ -0,0 +1,24 @@ + + 4.0.0 + + com.example + soft-transitive + 1.0.0 + + soft-transitive + Example + + + + org.example + example-dependency + 1.2.3 + + + org.example + example-dependency2 + 2.3.4 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/soft-requirement/pom.xml b/pkg/dependency/parser/java/pom/testdata/soft-requirement/pom.xml new file mode 100644 index 000000000000..661b10b66657 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/soft-requirement/pom.xml @@ -0,0 +1,43 @@ + + 4.0.0 + + com.example + soft + 1.0.0 + + soft + Example + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + knqyf263 + https://github.com/knqyf263 + + + + + 1.7.30 + + + + + org.example + example-api + ${api.version} + + + org.example + example-dependency + 1.2.3 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/transitive-dependency-management/pom.xml b/pkg/dependency/parser/java/pom/testdata/transitive-dependency-management/pom.xml new file mode 100644 index 000000000000..4e3c0972b7fe --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/transitive-dependency-management/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + org.example + transitive-dependency-management + 2.0.0 + + jar + Example + Example + + + + org.example + example-dependency-management3 + 1.1.1 + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/transitive-parents/base/pom.xml b/pkg/dependency/parser/java/pom/testdata/transitive-parents/base/pom.xml new file mode 100644 index 000000000000..ae1d6ced6e3f --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/transitive-parents/base/pom.xml @@ -0,0 +1,35 @@ + + 4.0.0 + + com.example + base + 4.0.0 + + base + Base + + + com.example + parent + 3.0.0 + ../ + + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-child + 2.0.0 + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/transitive-parents/pom.xml b/pkg/dependency/parser/java/pom/testdata/transitive-parents/pom.xml new file mode 100644 index 000000000000..2c0de4a24541 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/transitive-parents/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + + com.example + parent + 3.0.0 + + pom + parent + Parent + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + + org.example + example-api + 1.7.30 + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/use-child-dep-management-in-parent/parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/use-child-dep-management-in-parent/parent/pom.xml new file mode 100644 index 000000000000..dec887e6eecc --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/use-child-dep-management-in-parent/parent/pom.xml @@ -0,0 +1,34 @@ + + + + 4.0.0 + + + org.example + example-top-parent + 5.0.0 + ../top-parent/pom.xml + + + org.example + example-parent + 4.0.0 + pom + + + 4.0.1 + + + + org.example + example-api2 + + + org.example + example-api3 + 4.0.3 + compile + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/use-child-dep-management-in-parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/use-child-dep-management-in-parent/pom.xml new file mode 100644 index 000000000000..651fbba9f885 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/use-child-dep-management-in-parent/pom.xml @@ -0,0 +1,45 @@ + + 4.0.0 + + + org.example + example-parent + 4.0.0 + ./parent/pom.xml + + + com.example + child-depManagement-in-parent + 1.0.0 + + + + 1.0.1 + 1.0.2 + 1.0.3 + + + + + + org.example + example-api + ${api.version} + runtime + + + org.example + example-api2 + ${api2.version} + runtime + + + org.example + example-api3 + ${api3.version} + runtime + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/use-child-dep-management-in-parent/top-parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/use-child-dep-management-in-parent/top-parent/pom.xml new file mode 100644 index 000000000000..84d8954a4bf9 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/use-child-dep-management-in-parent/top-parent/pom.xml @@ -0,0 +1,34 @@ + + + + 4.0.0 + + org.example + example-top-parent + 5.0.0 + pom + + + 5.0.1 + + + + + org.example + example-api + ${api.version} + compile + + + org.example + example-api2 + + + org.example + example-api3 + 5.0.3 + compile + + + \ No newline at end of file diff --git a/pkg/dependency/parser/java/pom/testdata/use-dep-management-from-child-in-parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/use-dep-management-from-child-in-parent/pom.xml new file mode 100644 index 000000000000..5d062242161d --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/use-dep-management-from-child-in-parent/pom.xml @@ -0,0 +1,21 @@ + + 4.0.0 + + com.example + root-depManagement-in-parent + 1.0.0 + + + + 1.0.1 + + + + + org.example + example-dependency + 2.0.0 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/use-root-dep-management-in-parent/pom.xml b/pkg/dependency/parser/java/pom/testdata/use-root-dep-management-in-parent/pom.xml new file mode 100644 index 000000000000..685a7fc7caf3 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/use-root-dep-management-in-parent/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + + com.example + root-depManagement-in-parent + 1.0.0 + + + + 1.0.1 + + + + + + org.example + example-api + ${api.version} + + + + + + + org.example + example-dependency + 2.0.0 + + + diff --git a/pkg/dependency/parser/java/pom/testdata/version-requirement/pom.xml b/pkg/dependency/parser/java/pom/testdata/version-requirement/pom.xml new file mode 100644 index 000000000000..5ac8c1087670 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/version-requirement/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + + com.example + hard + 1.0.0 + + soft + Example + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + knqyf263 + https://github.com/knqyf263 + + + + + + org.example + example-api + (,1.0] + + + diff --git a/pkg/dependency/parser/java/pom/testdata/wildcard-exclusions/pom.xml b/pkg/dependency/parser/java/pom/testdata/wildcard-exclusions/pom.xml new file mode 100644 index 000000000000..fc6447b57fa7 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/wildcard-exclusions/pom.xml @@ -0,0 +1,48 @@ + + 4.0.0 + + com.example + wildcard-exclusions + 4.0.0 + + pom + wildcard-exclusions + Exclusions with wildcards + + + + org.example + example-dependency + 1.2.3 + + + * + example-api + + + + + org.example + example-dependency2 + 2.3.4 + + + * + * + + + + + org.example + example-nested + 3.3.3 + + + org.example + * + + + + + diff --git a/pkg/dependency/parser/java/pom/testdata/with-spaces/pom.xml b/pkg/dependency/parser/java/pom/testdata/with-spaces/pom.xml new file mode 100644 index 000000000000..7a0a1013aa31 --- /dev/null +++ b/pkg/dependency/parser/java/pom/testdata/with-spaces/pom.xml @@ -0,0 +1,31 @@ + + 4.0.0 + + com.example + root-pom-with-spaces + 1.0.0 + + + 1.2.4 + + + + + + org.example + example-dependency + ${dependency.version} + + + + + + + org.example + example-nested + 3.3.3 + + + + diff --git a/pkg/dependency/parser/java/pom/var.go b/pkg/dependency/parser/java/pom/var.go new file mode 100644 index 000000000000..dc179151ee6d --- /dev/null +++ b/pkg/dependency/parser/java/pom/var.go @@ -0,0 +1,61 @@ +package pom + +import ( + "fmt" + "os" + "regexp" + "slices" + "strings" + + "github.com/aquasecurity/trivy/pkg/log" +) + +var varRegexp = regexp.MustCompile(`\${(\S+?)}`) + +func isProperty(s string) bool { + if s != "" && strings.HasPrefix(s, "${") && strings.HasSuffix(s, "}") { + return true + } + return false +} + +func evaluateVariable(s string, props map[string]string, seenProps []string) string { + if props == nil { + props = make(map[string]string) + } + + for _, m := range varRegexp.FindAllStringSubmatch(s, -1) { + var newValue string + + // env.X: https://maven.apache.org/pom.html#Properties + // e.g. env.PATH + if after, ok := strings.CutPrefix(m[1], "env."); ok { + newValue = os.Getenv(after) + } else { + // might include another property. + // e.g. ${skipTests} + ss, ok := props[m[1]] + if ok { + // search for looped properties + if slices.Contains(seenProps, ss) { + printLoopedPropertiesStack(m[0], seenProps) + return "" + } + seenProps = append(seenProps, ss) // save evaluated props to check if we get this prop again + newValue = evaluateVariable(ss, props, seenProps) + seenProps = []string{} // clear props if we returned from recursive. Required for correct work with 2 same props like ${foo}-${foo} + } + + } + s = strings.ReplaceAll(s, m[0], newValue) + } + return strings.TrimSpace(s) +} + +func printLoopedPropertiesStack(env string, usedProps []string) { + var s string + for _, prop := range usedProps { + s += fmt.Sprintf("%s -> ", prop) + } + log.Warn("Lopped properties were detected", log.String("prop", s+env)) +} diff --git a/pkg/dependency/parser/julia/manifest/naive_pkg_parser.go b/pkg/dependency/parser/julia/manifest/naive_pkg_parser.go new file mode 100644 index 000000000000..0847ad52780c --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/naive_pkg_parser.go @@ -0,0 +1,69 @@ +package julia + +import ( + "bufio" + "io" + "strings" +) + +type pkgPosition struct { + start int + end int +} +type minPkg struct { + uuid string + version string + position pkgPosition +} + +func (pkg *minPkg) setEndPositionIfEmpty(n int) { + if pkg.position.end == 0 { + pkg.position.end = n + } +} + +type naivePkgParser struct { + r io.Reader +} + +func (parser *naivePkgParser) parse() map[string]pkgPosition { + var currentPkg = minPkg{} + var idx = make(map[string]pkgPosition, 0) + + scanner := bufio.NewScanner(parser.r) + lineNum := 1 + for scanner.Scan() { + line := scanner.Text() + switch { + case strings.HasPrefix(strings.TrimSpace(line), "["): + if currentPkg.uuid != "" { + currentPkg.setEndPositionIfEmpty(lineNum - 1) + idx[currentPkg.uuid] = currentPkg.position + } + currentPkg = minPkg{} + currentPkg.position.start = lineNum + + case strings.HasPrefix(strings.TrimSpace(line), "uuid ="): + currentPkg.uuid = propertyValue(line) + case strings.HasPrefix(strings.TrimSpace(line), "version ="): + currentPkg.version = propertyValue(line) + case strings.TrimSpace(line) == "": + currentPkg.setEndPositionIfEmpty(lineNum - 1) + } + + lineNum++ + } + // add last item + if currentPkg.uuid != "" { + currentPkg.setEndPositionIfEmpty(lineNum - 1) + idx[currentPkg.uuid] = currentPkg.position + } + return idx +} +func propertyValue(line string) string { + parts := strings.Split(line, "=") + if len(parts) == 2 { + return strings.Trim(parts[1], ` "`) + } + return "" +} diff --git a/pkg/dependency/parser/julia/manifest/parse.go b/pkg/dependency/parser/julia/manifest/parse.go new file mode 100644 index 000000000000..12dcb059172a --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/parse.go @@ -0,0 +1,171 @@ +package julia + +import ( + "context" + "io" + "sort" + + "github.com/BurntSushi/toml" + "github.com/samber/lo" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type primitiveManifest struct { + JuliaVersion string `toml:"julia_version"` + ManifestFormat string `toml:"manifest_format"` + Dependencies map[string][]primitiveDependency `toml:"deps"` // e.g. [[deps.Foo]] +} + +type primitiveDependency struct { + Dependencies toml.Primitive `toml:"deps"` // by name. e.g. deps = ["Foo"] or [deps.Foo.deps] + UUID string `toml:"uuid"` + Version string `toml:"version"` // not specified for stdlib packages, which are of the Julia version + DependsOn []string `toml:"-"` // list of dependent UUID's. +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var oldDeps map[string][]primitiveDependency + var primMan primitiveManifest + var manMetadata toml.MetaData + decoder := toml.NewDecoder(r) + // Try to read the old Manifest format. This can also read the v1.0 Manifest format, which we parse out later. + var err error + if manMetadata, err = decoder.Decode(&oldDeps); err != nil { + if _, err = r.Seek(0, io.SeekStart); err != nil { + return nil, nil, xerrors.Errorf("seek error: %w", err) + } + if manMetadata, err = decoder.Decode(&primMan); err != nil { + return nil, nil, xerrors.Errorf("decode error: %w", err) + } + } + + // We can't know the Julia version on an old manifest. + // All newer manifests include a manifest version and a julia version. + if primMan.ManifestFormat == "" { + primMan = primitiveManifest{ + JuliaVersion: "unknown", + Dependencies: oldDeps, + } + } + + man, err := decodeManifest(&primMan, &manMetadata) + if err != nil { + return nil, nil, xerrors.Errorf("unable to decode manifest dependencies: %w", err) + } + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return nil, nil, xerrors.Errorf("seek error: %w", err) + } + + // naive parser to get line numbers + pkgParser := naivePkgParser{r: r} + lineNumIdx := pkgParser.parse() + + var pkgs ftypes.Packages + var deps ftypes.Dependencies + for name, manifestDeps := range man.Dependencies { + for _, manifestDep := range manifestDeps { + version := depVersion(manifestDep.Version, man.JuliaVersion) + pkgID := manifestDep.UUID + pkg := ftypes.Package{ + ID: pkgID, + Name: name, + Version: version, + } + if pos, ok := lineNumIdx[manifestDep.UUID]; ok { + pkg.Locations = []ftypes.Location{ + { + StartLine: pos.start, + EndLine: pos.end, + }, + } + } + + pkgs = append(pkgs, pkg) + + if len(manifestDep.DependsOn) > 0 { + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: manifestDep.DependsOn, + }) + } + } + } + sort.Sort(pkgs) + sort.Sort(deps) + return pkgs, deps, nil +} + +// Returns the effective version of the `dep`. +// stdlib packages do not have a version in the manifest because they are packaged with julia itself +func depVersion(depVersion, juliaVersion string) string { + if depVersion == "" { + return juliaVersion + } + return depVersion +} + +// Decodes a primitive manifest using the metadata from parse time. +func decodeManifest(man *primitiveManifest, metadata *toml.MetaData) (*primitiveManifest, error) { + // Decode each dependency into the new manifest + for depName, primDeps := range man.Dependencies { + var newPrimDeps []primitiveDependency + for _, primDep := range primDeps { + newPrimDep, err := decodeDependency(man, primDep, metadata) + if err != nil { + return nil, err + } + newPrimDeps = append(newPrimDeps, newPrimDep) + } + man.Dependencies[depName] = newPrimDeps + } + + return man, nil +} + +// Decodes a primitive dependency using the metadata from parse time. +func decodeDependency(man *primitiveManifest, dep primitiveDependency, metadata *toml.MetaData) (primitiveDependency, error) { + // Try to decode as []string first where the manifest looks like deps = ["A", "B"] + var possibleDeps []string + err := metadata.PrimitiveDecode(dep.Dependencies, &possibleDeps) + if err == nil { + var possibleUuids []string + for _, depName := range possibleDeps { + primDep := man.Dependencies[depName] + if len(primDep) == 0 { + return primitiveDependency{}, xerrors.Errorf("Dependency %q has invalid format (parsed no deps): %s", depName, primDep) + } + if len(primDep) > 1 { + return primitiveDependency{}, xerrors.Errorf("Dependency %q has invalid format (parsed multiple deps): %s", depName, primDep) + } + possibleUuids = append(possibleUuids, primDep[0].UUID) + } + sort.Strings(possibleUuids) + dep.DependsOn = possibleUuids + return dep, nil + } + + // The other possibility is a map where the manifest looks like + // [deps.A.deps] + // B = "..." + var possibleDepsMap map[string]string + err = metadata.PrimitiveDecode(dep.Dependencies, &possibleDepsMap) + if err == nil { + possibleUuids := lo.Values(possibleDepsMap) + sort.Strings(possibleUuids) + dep.DependsOn = possibleUuids + return dep, nil + } + + // We don't know what the shape of the data is -- i.e. an invalid manifest + return primitiveDependency{}, err +} diff --git a/pkg/dependency/parser/julia/manifest/parse_test.go b/pkg/dependency/parser/julia/manifest/parse_test.go new file mode 100644 index 000000000000..d6d150ebbd3d --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/parse_test.go @@ -0,0 +1,97 @@ +package julia + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + file string // Test input file + want []ftypes.Package + wantDeps []ftypes.Dependency + wantErr string + }{ + { + name: "Manifest v1.6", + file: "testdata/primary/Manifest_v1.6.toml", + want: juliaV1_6Pkgs, + wantDeps: juliaV1_6Deps, + }, + { + name: "Manifest v1.8", + file: "testdata/primary/Manifest_v1.8.toml", + want: juliaV1_8Pkgs, + wantDeps: juliaV1_8Deps, + }, + { + name: "no deps v1.6", + file: "testdata/no_deps_v1.6/Manifest.toml", + want: nil, + wantDeps: nil, + }, + { + name: "no deps v1.9", + file: "testdata/no_deps_v1.9/Manifest.toml", + want: nil, + wantDeps: nil, + }, + { + name: "dep extensions v1.9", + file: "testdata/dep_ext_v1.9/Manifest.toml", + want: juliaV1_9DepExtPkgs, + wantDeps: nil, + }, + { + name: "shadowed dep v1.9", + file: "testdata/shadowed_dep_v1.9/Manifest.toml", + want: juliaV1_9ShadowedDepPkgs, + wantDeps: juliaV1_9ShadowedDepDeps, + }, + { + name: "julia v1.0 format", + file: "testdata/julia_v1.0_format/Manifest.toml", + want: juliaV10FormatPkgs, + wantDeps: juliaV10FormatDeps, + }, + { + name: "Manifest file doesn't contain child dependency of another dependency", + file: "testdata/missed-child-dep/Manifest.toml", + wantErr: "has invalid format (parsed no deps)", + }, + { + name: "Manifest file contains multiple dependencies with same name", + file: "testdata/multiple-same-deps/Manifest.toml", + wantErr: "has invalid format (parsed multiple deps)", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + + gotPkgs, gotDeps, err := NewParser().Parse(t.Context(), f) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + sort.Sort(ftypes.Packages(tt.want)) + assert.Equal(t, tt.want, gotPkgs) + if tt.wantDeps != nil { + sort.Sort(ftypes.Dependencies(tt.wantDeps)) + assert.Equal(t, tt.wantDeps, gotDeps) + } + }) + } +} diff --git a/pkg/dependency/parser/julia/manifest/parse_testcase.go b/pkg/dependency/parser/julia/manifest/parse_testcase.go new file mode 100644 index 000000000000..232529065c55 --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/parse_testcase.go @@ -0,0 +1,92 @@ +package julia + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +var ( + juliaV1_6Pkgs = []ftypes.Package{ + {ID: "ade2ca70-3891-5945-98fb-dc099432e06a", Name: "Dates", Version: "unknown", Locations: []ftypes.Location{{StartLine: 3, EndLine: 5}}}, + {ID: "682c06a0-de6a-54ab-a142-c8b1cf79cde6", Name: "JSON", Version: "0.21.4", Locations: []ftypes.Location{{StartLine: 7, EndLine: 11}}}, + {ID: "a63ad114-7e13-5084-954f-fe012c677804", Name: "Mmap", Version: "unknown", Locations: []ftypes.Location{{StartLine: 13, EndLine: 14}}}, + {ID: "69de0a69-1ddd-5017-9359-2bf0b02dc9f0", Name: "Parsers", Version: "2.4.2", Locations: []ftypes.Location{{StartLine: 16, EndLine: 20}}}, + {ID: "de0858da-6303-5e67-8744-51eddeeeb8d7", Name: "Printf", Version: "unknown", Locations: []ftypes.Location{{StartLine: 22, EndLine: 24}}}, + {ID: "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5", Name: "Unicode", Version: "unknown", Locations: []ftypes.Location{{StartLine: 26, EndLine: 27}}}, + } + + juliaV1_6Deps = []ftypes.Dependency{ + {ID: "ade2ca70-3891-5945-98fb-dc099432e06a", DependsOn: []string{"de0858da-6303-5e67-8744-51eddeeeb8d7"}}, + {ID: "682c06a0-de6a-54ab-a142-c8b1cf79cde6", DependsOn: []string{ + "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5", + "69de0a69-1ddd-5017-9359-2bf0b02dc9f0", + "a63ad114-7e13-5084-954f-fe012c677804", + "ade2ca70-3891-5945-98fb-dc099432e06a", + }}, + {ID: "69de0a69-1ddd-5017-9359-2bf0b02dc9f0", DependsOn: []string{"ade2ca70-3891-5945-98fb-dc099432e06a"}}, + {ID: "de0858da-6303-5e67-8744-51eddeeeb8d7", DependsOn: []string{"4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"}}, + } + + juliaV1_8Pkgs = []ftypes.Package{ + {ID: "ade2ca70-3891-5945-98fb-dc099432e06a", Name: "Dates", Version: "1.8.5", Locations: []ftypes.Location{{StartLine: 7, EndLine: 9}}}, + {ID: "682c06a0-de6a-54ab-a142-c8b1cf79cde6", Name: "JSON", Version: "0.21.4", Locations: []ftypes.Location{{StartLine: 11, EndLine: 15}}}, + {ID: "a63ad114-7e13-5084-954f-fe012c677804", Name: "Mmap", Version: "1.8.5", Locations: []ftypes.Location{{StartLine: 17, EndLine: 18}}}, + {ID: "69de0a69-1ddd-5017-9359-2bf0b02dc9f0", Name: "Parsers", Version: "2.5.10", Locations: []ftypes.Location{{StartLine: 20, EndLine: 24}}}, + {ID: "aea7be01-6a6a-4083-8856-8a6e6704d82a", Name: "PrecompileTools", Version: "1.1.1", Locations: []ftypes.Location{{StartLine: 26, EndLine: 30}}}, + {ID: "21216c6a-2e73-6563-6e65-726566657250", Name: "Preferences", Version: "1.4.0", Locations: []ftypes.Location{{StartLine: 32, EndLine: 36}}}, + {ID: "de0858da-6303-5e67-8744-51eddeeeb8d7", Name: "Printf", Version: "1.8.5", Locations: []ftypes.Location{{StartLine: 38, EndLine: 40}}}, + {ID: "9a3f8284-a2c9-5f02-9a11-845980a1fd5c", Name: "Random", Version: "1.8.5", Locations: []ftypes.Location{{StartLine: 42, EndLine: 44}}}, + {ID: "ea8e919c-243c-51af-8825-aaa63cd721ce", Name: "SHA", Version: "0.7.0", Locations: []ftypes.Location{{StartLine: 46, EndLine: 48}}}, + {ID: "9e88b42a-f829-5b0c-bbe9-9e923198166b", Name: "Serialization", Version: "1.8.5", Locations: []ftypes.Location{{StartLine: 50, EndLine: 51}}}, + {ID: "fa267f1f-6049-4f14-aa54-33bafae1ed76", Name: "TOML", Version: "1.0.0", Locations: []ftypes.Location{{StartLine: 53, EndLine: 56}}}, + {ID: "cf7118a7-6976-5b1a-9a39-7adc72f591a4", Name: "UUIDs", Version: "1.8.5", Locations: []ftypes.Location{{StartLine: 58, EndLine: 60}}}, + {ID: "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5", Name: "Unicode", Version: "1.8.5", Locations: []ftypes.Location{{StartLine: 62, EndLine: 63}}}, + } + + juliaV1_8Deps = []ftypes.Dependency{ + {ID: "ade2ca70-3891-5945-98fb-dc099432e06a", DependsOn: []string{"de0858da-6303-5e67-8744-51eddeeeb8d7"}}, + {ID: "682c06a0-de6a-54ab-a142-c8b1cf79cde6", DependsOn: []string{ + "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5", + "69de0a69-1ddd-5017-9359-2bf0b02dc9f0", + "a63ad114-7e13-5084-954f-fe012c677804", + "ade2ca70-3891-5945-98fb-dc099432e06a", + }}, + {ID: "69de0a69-1ddd-5017-9359-2bf0b02dc9f0", DependsOn: []string{ + "ade2ca70-3891-5945-98fb-dc099432e06a", + "aea7be01-6a6a-4083-8856-8a6e6704d82a", + "cf7118a7-6976-5b1a-9a39-7adc72f591a4", + }}, + {ID: "aea7be01-6a6a-4083-8856-8a6e6704d82a", DependsOn: []string{"21216c6a-2e73-6563-6e65-726566657250"}}, + {ID: "21216c6a-2e73-6563-6e65-726566657250", DependsOn: []string{"fa267f1f-6049-4f14-aa54-33bafae1ed76"}}, + {ID: "de0858da-6303-5e67-8744-51eddeeeb8d7", DependsOn: []string{"4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"}}, + {ID: "9a3f8284-a2c9-5f02-9a11-845980a1fd5c", DependsOn: []string{"9e88b42a-f829-5b0c-bbe9-9e923198166b", "ea8e919c-243c-51af-8825-aaa63cd721ce"}}, + {ID: "fa267f1f-6049-4f14-aa54-33bafae1ed76", DependsOn: []string{"ade2ca70-3891-5945-98fb-dc099432e06a"}}, + {ID: "cf7118a7-6976-5b1a-9a39-7adc72f591a4", DependsOn: []string{"9a3f8284-a2c9-5f02-9a11-845980a1fd5c", "ea8e919c-243c-51af-8825-aaa63cd721ce"}}, + } + + juliaV1_9DepExtPkgs = []ftypes.Package{ + {ID: "621f4979-c628-5d54-868e-fcf4e3e8185c", Name: "AbstractFFTs", Version: "1.3.1", Locations: []ftypes.Location{{StartLine: 7, EndLine: 10}}}, + } + + juliaV1_9ShadowedDepPkgs = []ftypes.Package{ + {ID: "ead4f63c-334e-11e9-00e6-e7f0a5f21b60", Name: "A", Version: "1.9.0", Locations: []ftypes.Location{{StartLine: 7, EndLine: 8}}}, + {ID: "f41f7b98-334e-11e9-1257-49272045fb24", Name: "B", Version: "1.9.0", Locations: []ftypes.Location{{StartLine: 13, EndLine: 14}}}, + {ID: "edca9bc6-334e-11e9-3554-9595dbb4349c", Name: "B", Version: "1.9.0", Locations: []ftypes.Location{{StartLine: 15, EndLine: 16}}}, + } + + juliaV1_9ShadowedDepDeps = []ftypes.Dependency{ + {ID: "ead4f63c-334e-11e9-00e6-e7f0a5f21b60", DependsOn: []string{"f41f7b98-334e-11e9-1257-49272045fb24"}}, + } + + juliaV10FormatPkgs = []ftypes.Package{ + {ID: "767738be-2f1f-45a9-b806-0234f3164144", Name: "Foo", Version: "unknown", Locations: []ftypes.Location{{StartLine: 1, EndLine: 5}}}, + {ID: "6f418443-bd2e-4783-b551-cdbac608adf2", Name: "Foo", Version: "unknown", Locations: []ftypes.Location{{StartLine: 7, EndLine: 10}}}, + {ID: "2a550a13-6bab-4a91-a4ee-dff34d6b99d0", Name: "Bar", Version: "unknown", Locations: []ftypes.Location{{StartLine: 12, EndLine: 14}}}, + {ID: "6801f525-dc68-44e8-a4e8-cabd286279e7", Name: "Baz", Version: "unknown", Locations: []ftypes.Location{{StartLine: 19, EndLine: 21}}}, + {ID: "b5ec9b9c-e354-47fd-b367-a348bdc8f909", Name: "Qux", Version: "unknown", Locations: []ftypes.Location{{StartLine: 26, EndLine: 28}}}, + } + + juliaV10FormatDeps = []ftypes.Dependency{ + {ID: "767738be-2f1f-45a9-b806-0234f3164144", DependsOn: []string{"2a550a13-6bab-4a91-a4ee-dff34d6b99d0", "6801f525-dc68-44e8-a4e8-cabd286279e7", "b5ec9b9c-e354-47fd-b367-a348bdc8f909"}}, + {ID: "6f418443-bd2e-4783-b551-cdbac608adf2", DependsOn: []string{"b5ec9b9c-e354-47fd-b367-a348bdc8f909"}}, + {ID: "2a550a13-6bab-4a91-a4ee-dff34d6b99d0", DependsOn: []string{"6801f525-dc68-44e8-a4e8-cabd286279e7", "6f418443-bd2e-4783-b551-cdbac608adf2"}}, + {ID: "6801f525-dc68-44e8-a4e8-cabd286279e7", DependsOn: []string{"6f418443-bd2e-4783-b551-cdbac608adf2", "b5ec9b9c-e354-47fd-b367-a348bdc8f909"}}, + } +) diff --git a/pkg/dependency/parser/julia/manifest/testdata/dep_ext_v1.9/Manifest.toml b/pkg/dependency/parser/julia/manifest/testdata/dep_ext_v1.9/Manifest.toml new file mode 100644 index 000000000000..c3b22a724c15 --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/dep_ext_v1.9/Manifest.toml @@ -0,0 +1,16 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.9.0" +manifest_format = "2.0" +project_hash = "f0a796fb78285c02ad123fec6e14c8bac09a2ccc" + +[[deps.AbstractFFTs]] +git-tree-sha1 = "16b6dbc4cf7caee4e1e75c49485ec67b667098a0" +uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" +version = "1.3.1" + + [deps.AbstractFFTs.extensions] + AbstractFFTsChainRulesCoreExt = "ChainRulesCore" + + [deps.AbstractFFTs.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/pkg/dependency/parser/julia/manifest/testdata/julia_v1.0_format/Manifest.toml b/pkg/dependency/parser/julia/manifest/testdata/julia_v1.0_format/Manifest.toml new file mode 100644 index 000000000000..a5a893066c36 --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/julia_v1.0_format/Manifest.toml @@ -0,0 +1,28 @@ +[[Foo]] +deps = ["Bar", "Baz", "Qux"] +uuid = "767738be-2f1f-45a9-b806-0234f3164144" +git-tree-sha1 = "7c626031568a5e432112a74009c3763f9b851e3e" +path = "deps/Foo1" + +[[Foo]] +deps = ["Qux"] +uuid = "6f418443-bd2e-4783-b551-cdbac608adf2" +path = "deps/Foo2.jl" + +[[Bar]] +uuid = "2a550a13-6bab-4a91-a4ee-dff34d6b99d0" +path = "deps/Bar" +[Bar.deps] +Baz = "6801f525-dc68-44e8-a4e8-cabd286279e7" +Foo = "6f418443-bd2e-4783-b551-cdbac608adf2" + +[[Baz]] +uuid = "6801f525-dc68-44e8-a4e8-cabd286279e7" +git-tree-sha1 = "efc7e24c53d6a328011975294a2c75fed2f9800a" +[Baz.deps] +Foo = "6f418443-bd2e-4783-b551-cdbac608adf2" +Qux = "b5ec9b9c-e354-47fd-b367-a348bdc8f909" + +[[Qux]] +uuid = "b5ec9b9c-e354-47fd-b367-a348bdc8f909" +path = "deps/Qux.jl" diff --git a/pkg/dependency/parser/julia/manifest/testdata/julia_v1.0_format/Project.toml b/pkg/dependency/parser/julia/manifest/testdata/julia_v1.0_format/Project.toml new file mode 100644 index 000000000000..9d42e37cd67f --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/julia_v1.0_format/Project.toml @@ -0,0 +1,6 @@ +name = "TestProject" +uuid = "84c38c17-0c6f-4d12-a694-d20b69c16777" + +[deps] +Foo = "767738be-2f1f-45a9-b806-0234f3164144" +Bar = "2a550a13-6bab-4a91-a4ee-dff34d6b99d0" diff --git a/pkg/dependency/parser/julia/manifest/testdata/missed-child-dep/Manifest.toml b/pkg/dependency/parser/julia/manifest/testdata/missed-child-dep/Manifest.toml new file mode 100644 index 000000000000..d49d82a34392 --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/missed-child-dep/Manifest.toml @@ -0,0 +1,16 @@ +# This file is machine-generated - editing it directly is not advised + +[[Base64]] +uuid = "2a0f44e3-6c83-55bd-87e4-b1978d98bd5f" + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[InteractiveUtils]] +deps = ["Markdown"] +uuid = "b77e0a4c-d291-57a0-90e8-8db25a27a240" + +[[LibGit2]] +deps = ["Printf"] +uuid = "76f85450-5226-5b5a-8eaa-529ad045b433" \ No newline at end of file diff --git a/pkg/dependency/parser/julia/manifest/testdata/multiple-same-deps/Manifest.toml b/pkg/dependency/parser/julia/manifest/testdata/multiple-same-deps/Manifest.toml new file mode 100644 index 000000000000..e43943743bd8 --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/multiple-same-deps/Manifest.toml @@ -0,0 +1,11 @@ +# This file is machine-generated - editing it directly is not advised + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[Printf]] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[Printf]] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d8" diff --git a/pkg/dependency/parser/julia/manifest/testdata/no_deps_v1.6/Manifest.toml b/pkg/dependency/parser/julia/manifest/testdata/no_deps_v1.6/Manifest.toml new file mode 100644 index 000000000000..f45eecff031f --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/no_deps_v1.6/Manifest.toml @@ -0,0 +1,2 @@ +# This file is machine-generated - editing it directly is not advised + diff --git a/pkg/dependency/parser/julia/manifest/testdata/no_deps_v1.9/Manifest.toml b/pkg/dependency/parser/julia/manifest/testdata/no_deps_v1.9/Manifest.toml new file mode 100644 index 000000000000..9ed5ab94bdf9 --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/no_deps_v1.9/Manifest.toml @@ -0,0 +1,7 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.9.0" +manifest_format = "2.0" +project_hash = "da39a3ee5e6b4b0d3255bfef95601890afd80709" + +[deps] diff --git a/pkg/dependency/parser/julia/manifest/testdata/primary/Manifest_v1.6.toml b/pkg/dependency/parser/julia/manifest/testdata/primary/Manifest_v1.6.toml new file mode 100644 index 000000000000..6883d0041576 --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/primary/Manifest_v1.6.toml @@ -0,0 +1,27 @@ +# This file is machine-generated - editing it directly is not advised + +[[Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.4" + +[[Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[Parsers]] +deps = ["Dates"] +git-tree-sha1 = "6c01a9b494f6d2a9fc180a08b182fcb06f0958a0" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.4.2" + +[[Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/pkg/dependency/parser/julia/manifest/testdata/primary/Manifest_v1.8.toml b/pkg/dependency/parser/julia/manifest/testdata/primary/Manifest_v1.8.toml new file mode 100644 index 000000000000..6b7c988ca956 --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/primary/Manifest_v1.8.toml @@ -0,0 +1,63 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.8.5" +manifest_format = "2.0" +project_hash = "f65b9de676a27ce78ee011db6d477b3d44d1a7c5" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[deps.JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.4" + +[[deps.Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[deps.Parsers]] +deps = ["Dates", "PrecompileTools", "UUIDs"] +git-tree-sha1 = "a5aef8d4a6e8d81f171b2bd4be5265b01384c74c" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.5.10" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "259e206946c293698122f63e2b513a7c99a244e8" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.1.1" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "7eb1686b4f04b82f96ed7a4ea5890a4f0c7a09f1" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.4.0" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[deps.Random]] +deps = ["SHA", "Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.0" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" diff --git a/pkg/dependency/parser/julia/manifest/testdata/shadowed_dep_v1.9/Manifest.toml b/pkg/dependency/parser/julia/manifest/testdata/shadowed_dep_v1.9/Manifest.toml new file mode 100644 index 000000000000..dd4ea00b943d --- /dev/null +++ b/pkg/dependency/parser/julia/manifest/testdata/shadowed_dep_v1.9/Manifest.toml @@ -0,0 +1,16 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.9.0" +manifest_format = "2.0" +project_hash = "f0a796fb78285c02ad123fec6e14c8bac09a2ccc" + +[[deps.A]] +uuid = "ead4f63c-334e-11e9-00e6-e7f0a5f21b60" + + [deps.A.deps] + B = "f41f7b98-334e-11e9-1257-49272045fb24" + +[[deps.B]] +uuid = "f41f7b98-334e-11e9-1257-49272045fb24" +[[deps.B]] +uuid = "edca9bc6-334e-11e9-3554-9595dbb4349c" diff --git a/pkg/dependency/parser/nodejs/bun/parse.go b/pkg/dependency/parser/nodejs/bun/parse.go new file mode 100644 index 000000000000..4e7bfef93af3 --- /dev/null +++ b/pkg/dependency/parser/nodejs/bun/parse.go @@ -0,0 +1,188 @@ +package bun + +import ( + "context" + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + "io" + "sort" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/set" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type LockFile struct { + Packages map[string]ParsedPackage `json:"packages"` + Workspaces map[string]Workspace `json:"workspaces"` + LockfileVersion int `json:"lockfileVersion"` +} + +type Workspace struct { + Name string `json:"name"` + Version string `json:"version"` + Dependencies map[string]string `json:"dependencies"` + DevDependencies map[string]string `json:"devDependencies"` + OptionalDependencies map[string]string `json:"optionalDependencies"` + PeerDependencies map[string]string `json:"peerDependencies"` +} + +type ParsedPackage struct { + Identifier string + Meta map[string]any + xjson.Location +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *ParsedPackage) UnmarshalJSON(data []byte) error { + var raw []jsontext.Value + if err := json.Unmarshal(data, &raw); err != nil { + return fmt.Errorf("expected package format: %w", err) + } + if len(raw) < 1 { + return fmt.Errorf("invalid package entry: not enough elements: %s", string(data)) + } + + if err := json.Unmarshal(raw[0], &p.Identifier); err != nil { + return err + } + + // When package contains only package field [pkg: string] + // cf. https://github.com/oven-sh/bun/blob/61e03a275885b9b48f7a28f6dfbbbe1156eedca6/packages/bun-types/bun.d.ts#L7751 + if len(raw) == 1 { + return nil + } + + // Meta can be 2 or 3 array elements + // [pkg: string, info: BunLockFilePackageInfo] + // [pkg: string, info: BunLockFilePackageInfo, bunTag: string] + // [pkg: string, info: Pick] + // [pkg: string, registry: string, info: BunLockFilePackageInfo, integrity: string] + // cf.https://github.com/oven-sh/bun/blob/61e03a275885b9b48f7a28f6dfbbbe1156eedca6/packages/bun-types/bun.d.ts#L7745-L7755 + metaRaw := raw[1] + if len(raw) > 3 { + metaRaw = raw[2] + } + return json.Unmarshal(metaRaw, &p.Meta) +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockFile LockFile + data, err := io.ReadAll(r) + if err != nil { + return nil, nil, xerrors.Errorf("file read error: %w", err) + } + if err = xjson.UnmarshalJSONC(data, &lockFile); err != nil { + return nil, nil, xerrors.Errorf("JSON decode error: %w", err) + } + + pkgs := make(map[string]ftypes.Package, len(lockFile.Packages)) + deps := make(map[string][]string) + + prodDirectDeps := set.New[string]() + devDirectDeps := set.New[string]() + + for _, ws := range lockFile.Workspaces { + prodDirectDeps.Append(lo.Keys(ws.Dependencies)...) + prodDirectDeps.Append(lo.Keys(ws.PeerDependencies)...) + prodDirectDeps.Append(lo.Keys(ws.OptionalDependencies)...) + devDirectDeps.Append(lo.Keys(ws.DevDependencies)...) + } + for pkgName, parsed := range lockFile.Packages { + pkgVersion := strings.TrimPrefix(parsed.Identifier, pkgName+"@") + if strings.HasPrefix(pkgVersion, "workspace") { + pkgVersion = lockFile.Workspaces[pkgName].Version + } + pkgId := packageID(pkgName, pkgVersion) + isDirect := prodDirectDeps.Contains(pkgName) || devDirectDeps.Contains(pkgName) + + relationship := ftypes.RelationshipIndirect + if _, ok := lockFile.Workspaces[pkgName]; ok { + relationship = ftypes.RelationshipWorkspace + } else if isDirect { + relationship = ftypes.RelationshipDirect + } + + newPkg := ftypes.Package{ + ID: pkgId, + Name: pkgName, + Version: pkgVersion, + Relationship: relationship, + Dev: true, // Mark all dependencies as Dev. We will handle them later. + Locations: []ftypes.Location{ftypes.Location(parsed.Location)}, + } + pkgs[pkgName] = newPkg + + var dependsOn []string + if depMap, ok := parsed.Meta["dependencies"].(map[string]any); ok { + dependsOn = lo.Keys(depMap) + } + + if len(dependsOn) > 0 { + sort.Strings(dependsOn) + deps[pkgName] = dependsOn + } + } + + for _, pkg := range pkgs { + // Workspaces are always prod deps. + if pkg.Relationship == ftypes.RelationshipWorkspace { + pkg.Dev = false + pkgs[pkg.Name] = pkg + continue + } + if pkg.Relationship != ftypes.RelationshipDirect || !prodDirectDeps.Contains(pkg.Name) { + continue + } + walkProdPackages(pkg.Name, pkgs, deps, set.New[string]()) + } + + depSlice := lo.MapToSlice(deps, func(depName string, dependsOn []string) ftypes.Dependency { + id := pkgs[depName] + dependsOnIDs := make([]string, 0, len(dependsOn)) + for _, d := range dependsOn { + dependsOnIDs = append(dependsOnIDs, pkgs[d].ID) + } + return ftypes.Dependency{ + ID: id.ID, + DependsOn: dependsOnIDs, + } + }) + pkgSlice := lo.Values(pkgs) + sort.Sort(ftypes.Packages(pkgSlice)) + sort.Sort(ftypes.Dependencies(depSlice)) + return pkgSlice, depSlice, nil +} + +// walkProdPackages marks all packages in the dependency tree of the given package as prod packages (Dev == false). +func walkProdPackages(pkgName string, pkgs map[string]ftypes.Package, deps map[string][]string, visited set.Set[string]) { + if visited.Contains(pkgName) { + return + } + + // Disable Dev field for prod pkgs. + pkg := pkgs[pkgName] + pkg.Dev = false + pkgs[pkgName] = pkg + + visited.Append(pkgName) + for _, dep := range deps[pkgName] { + walkProdPackages(dep, pkgs, deps, visited) + } +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.Bun, name, version) +} diff --git a/pkg/dependency/parser/nodejs/bun/parse_test.go b/pkg/dependency/parser/nodejs/bun/parse_test.go new file mode 100644 index 000000000000..22f45024d7b5 --- /dev/null +++ b/pkg/dependency/parser/nodejs/bun/parse_test.go @@ -0,0 +1,57 @@ +package bun + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + file string // Test input file + want []ftypes.Package + wantDeps []ftypes.Dependency + wantErr string + }{ + { + name: "normal", + file: "testdata/bun_happy.lock", + want: normalPkgs, + wantDeps: normalDeps, + }, + { + name: "invalid lockfile", + file: "testdata/bun_invalid.lock", + wantErr: "JSON decode error", + }, + { + name: "multiple workspaces", + file: "testdata/bun_multiple_ws.lock", + want: multipleWsPkgs, + wantDeps: multipleWsDeps, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + defer f.Close() + got, deps, err := NewParser().Parse(t.Context(), f) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + if tt.wantDeps != nil { + assert.Equal(t, tt.wantDeps, deps) + } + }) + } +} diff --git a/pkg/dependency/parser/nodejs/bun/parse_testcase.go b/pkg/dependency/parser/nodejs/bun/parse_testcase.go new file mode 100644 index 000000000000..28cee86b844e --- /dev/null +++ b/pkg/dependency/parser/nodejs/bun/parse_testcase.go @@ -0,0 +1,160 @@ +package bun + +import ( + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + normalPkgs = []ftypes.Package{ + { + ID: "@types/bun@1.2.13", + Name: "@types/bun", + Version: "1.2.13", + Relationship: ftypes.RelationshipDirect, + Dev: true, + Locations: ftypes.Locations{ + { + StartLine: 18, + EndLine: 18, + }, + }, + }, + { + ID: "typescript@5.8.3", + Name: "typescript", + Version: "5.8.3", + Relationship: ftypes.RelationshipDirect, + Dev: false, + Locations: ftypes.Locations{ + { + StartLine: 24, + EndLine: 24, + }, + }, + }, + { + ID: "zod@3.24.4", + Name: "zod", + Version: "3.24.4", + Relationship: ftypes.RelationshipDirect, + Dev: false, + Locations: ftypes.Locations{ + { + StartLine: 28, + EndLine: 28, + }, + }, + }, + { + ID: "@types/node@22.15.18", + Name: "@types/node", + Version: "22.15.18", + Relationship: ftypes.RelationshipIndirect, + Dev: true, + Locations: ftypes.Locations{ + { + StartLine: 20, + EndLine: 20, + }, + }, + }, + { + ID: "bun-types@1.2.13", + Name: "bun-types", + Version: "1.2.13", + Relationship: ftypes.RelationshipIndirect, + Dev: true, + Locations: ftypes.Locations{ + { + StartLine: 22, + EndLine: 22, + }, + }, + }, + { + ID: "undici-types@6.21.0", + Name: "undici-types", + Version: "6.21.0", + Relationship: ftypes.RelationshipIndirect, + Dev: false, + Locations: ftypes.Locations{ + { + StartLine: 26, + EndLine: 26, + }, + }, + }, + } + + multipleWsPkgs = []ftypes.Package{ + { + ID: "my-app@1.0.0", + Name: "my-app", + Version: "1.0.0", + Relationship: ftypes.RelationshipWorkspace, + Locations: ftypes.Locations{ + { + StartLine: 27, + EndLine: 27, + }, + }, + }, + { + ID: "my-lib@1.0.0", + Name: "my-lib", + Version: "1.0.0", + Relationship: ftypes.RelationshipWorkspace, + Locations: ftypes.Locations{ + { + StartLine: 29, + EndLine: 29, + }, + }, + }, + { + ID: "chalk@5.0.1", + Name: "chalk", + Version: "5.0.1", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 23, + EndLine: 23, + }, + }, + }, + { + ID: "lodash@4.17.21", + Name: "lodash", + Version: "4.17.21", + Relationship: ftypes.RelationshipDirect, + Locations: ftypes.Locations{ + { + StartLine: 25, + EndLine: 25, + }, + }, + }, + } + + normalDeps = []ftypes.Dependency{ + { + ID: "@types/bun@1.2.13", + DependsOn: []string{"bun-types@1.2.13"}, + }, + { + ID: "@types/node@22.15.18", + DependsOn: []string{"undici-types@6.21.0"}, + }, + { + ID: "bun-types@1.2.13", + DependsOn: []string{"@types/node@22.15.18"}, + }, + { + ID: "zod@3.24.4", + DependsOn: []string{"undici-types@6.21.0"}, + }, + } + + multipleWsDeps = []ftypes.Dependency(nil) +) diff --git a/pkg/dependency/parser/nodejs/bun/testdata/bun_happy.lock b/pkg/dependency/parser/nodejs/bun/testdata/bun_happy.lock new file mode 100644 index 000000000000..a39048b778a5 --- /dev/null +++ b/pkg/dependency/parser/nodejs/bun/testdata/bun_happy.lock @@ -0,0 +1,30 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "buntest", + "devDependencies": { + "@types/bun": "latest", + }, + "optionalDependencies": { + "zod": "^3.24.4", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], + + "@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="], + + "bun-types": ["bun-types@1.2.13", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0"], + + "zod": ["zod@3.24.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], + } +} diff --git a/pkg/dependency/parser/nodejs/bun/testdata/bun_invalid.lock b/pkg/dependency/parser/nodejs/bun/testdata/bun_invalid.lock new file mode 100644 index 000000000000..e0e5a0b1d84e --- /dev/null +++ b/pkg/dependency/parser/nodejs/bun/testdata/bun_invalid.lock @@ -0,0 +1,3 @@ +{ + "lockfileVersion": "not-a-number", +} diff --git a/pkg/dependency/parser/nodejs/bun/testdata/bun_multiple_ws.lock b/pkg/dependency/parser/nodejs/bun/testdata/bun_multiple_ws.lock new file mode 100644 index 000000000000..aa9e87961022 --- /dev/null +++ b/pkg/dependency/parser/nodejs/bun/testdata/bun_multiple_ws.lock @@ -0,0 +1,31 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "buntest", + }, + "my-app": { + "name": "my-app", + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.21", + }, + }, + "my-lib": { + "name": "my-lib", + "version": "1.0.0", + "dependencies": { + "chalk": "5.0.1", + }, + }, + }, + "packages": { + "chalk": ["chalk@5.0.1", "", {}, "sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "my-app": ["my-app@workspace:my-app"], + + "my-lib": ["my-lib@workspace:my-lib"], + } +} diff --git a/pkg/dependency/parser/nodejs/npm/parse.go b/pkg/dependency/parser/nodejs/npm/parse.go new file mode 100644 index 000000000000..73aff1ea63f9 --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/parse.go @@ -0,0 +1,416 @@ +package npm + +import ( + "context" + "fmt" + "maps" + "path" + "slices" + "sort" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/packagejson" + "github.com/aquasecurity/trivy/pkg/dependency/parser/utils" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +const nodeModulesDir = "node_modules" + +type LockFile struct { + Dependencies map[string]Dependency `json:"dependencies"` + Packages map[string]Package `json:"packages"` + LockfileVersion int `json:"lockfileVersion"` +} +type Dependency struct { + Version string `json:"version"` + Dev bool `json:"dev"` + Dependencies map[string]Dependency `json:"dependencies"` + Requires map[string]string `json:"requires"` + Resolved string `json:"resolved"` + xjson.Location +} + +type Package struct { + Name string `json:"name"` + Version string `json:"version"` + License string `json:"license"` + Dependencies map[string]string `json:"dependencies"` + OptionalDependencies map[string]string `json:"optionalDependencies"` + DevDependencies map[string]string `json:"devDependencies"` + PeerDependencies map[string]string `json:"peerDependencies"` + Resolved string `json:"resolved"` + Dev bool `json:"dev"` + Link bool `json:"link"` + Workspaces any `json:"workspaces"` + xjson.Location +} + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("npm"), + } +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockFile LockFile + if err := xjson.UnmarshalRead(r, &lockFile); err != nil { + return nil, nil, xerrors.Errorf("decode error: %w", err) + } + + var pkgs []ftypes.Package + var deps []ftypes.Dependency + if lockFile.LockfileVersion == 1 { + pkgs, deps = p.parseV1(lockFile.Dependencies, make(map[string]string)) + } else { + pkgs, deps = p.parseV2(lockFile.Packages) + } + + return utils.UniquePackages(pkgs), uniqueDeps(deps), nil +} + +func (p *Parser) parseV2(packages map[string]Package) ([]ftypes.Package, []ftypes.Dependency) { + pkgs := make(map[string]ftypes.Package, len(packages)-1) + var deps []ftypes.Dependency + + // Resolve links first + // https://docs.npmjs.com/cli/v9/configuring-npm/package-lock-json#packages + p.resolveLinks(packages) + + directDeps := set.New[string]() + for name, version := range lo.Assign(packages[""].Dependencies, packages[""].OptionalDependencies, packages[""].DevDependencies, packages[""].PeerDependencies) { + pkgPath := joinPaths(nodeModulesDir, name) + if _, ok := packages[pkgPath]; !ok { + p.logger.Debug("Unable to find the direct dependency", + log.String("name", name), log.String("version", version)) + continue + } + // Store the package paths of direct dependencies + // e.g. node_modules/body-parser + directDeps.Append(pkgPath) + } + + for pkgPath, pkg := range packages { + if !strings.HasPrefix(pkgPath, "node_modules") { + continue + } + + // pkg.Name exists when package name != folder name + pkgName := pkg.Name + if pkgName == "" { + pkgName = p.pkgNameFromPath(pkgPath) + } + + pkgID := packageID(pkgName, pkg.Version) + + var ref ftypes.ExternalRef + if pkg.Resolved != "" { + ref = ftypes.ExternalRef{ + Type: ftypes.RefOther, + URL: pkg.Resolved, + } + } + + pkgIndirect := isIndirectPkg(pkgPath, directDeps) + + // There are cases when similar packages use same dependencies + // we need to add location for each these dependencies + if savedPkg, ok := pkgs[pkgID]; ok { + savedPkg.Dev = savedPkg.Dev && pkg.Dev + if savedPkg.Relationship == ftypes.RelationshipIndirect && !pkgIndirect { + savedPkg.Relationship = ftypes.RelationshipDirect + } + + if ref.URL != "" && !slices.Contains(savedPkg.ExternalReferences, ref) { + savedPkg.ExternalReferences = append(savedPkg.ExternalReferences, ref) + sortExternalReferences(savedPkg.ExternalReferences) + } + + savedPkg.Locations = append(savedPkg.Locations, ftypes.Location(pkg.Location)) + sort.Sort(savedPkg.Locations) + + // If for some reason license is missing in savedPkg, but exists in the current pkg, add it. + if len(savedPkg.Licenses) == 0 && pkg.License != "" { + savedPkg.Licenses = []string{pkg.License} + } + + pkgs[pkgID] = savedPkg + continue + } + + var licenses []string + if pkg.License != "" { + licenses = []string{pkg.License} + } + + newPkg := ftypes.Package{ + ID: pkgID, + Name: pkgName, + Version: pkg.Version, + Licenses: licenses, + Relationship: lo.Ternary(pkgIndirect, ftypes.RelationshipIndirect, ftypes.RelationshipDirect), + Dev: pkg.Dev, + ExternalReferences: lo.Ternary(ref.URL != "", []ftypes.ExternalRef{ref}, nil), + Locations: []ftypes.Location{ftypes.Location(pkg.Location)}, + } + pkgs[pkgID] = newPkg + + // npm builds graph using optional deps. e.g.: + // └─┬ watchpack@1.7.5 + // ├─┬ chokidar@3.5.3 - optional dependency + // │ └── glob-parent@5.1. + dependencies := lo.Assign(pkg.Dependencies, pkg.OptionalDependencies, pkg.PeerDependencies) + dependsOn := make([]string, 0, len(dependencies)) + for depName, depVersion := range dependencies { + depID, err := findDependsOn(pkgPath, depName, packages) + if err != nil { + p.logger.Debug("Unable to resolve the version", + log.String("name", depName), log.String("version", depVersion)) + continue + } + dependsOn = append(dependsOn, depID) + } + + if len(dependsOn) > 0 { + deps = append(deps, ftypes.Dependency{ + ID: newPkg.ID, + DependsOn: dependsOn, + }) + } + + } + + return lo.Values(pkgs), deps +} + +// for local package npm uses links. e.g.: +// function/func1 -> target of package +// node_modules/func1 -> link to target +// see `package-lock_v3_with_workspace.json` to better understanding +func (p *Parser) resolveLinks(packages map[string]Package) { + links := lo.PickBy(packages, func(pkgPath string, pkg Package) bool { + if !pkg.Link { + return false + } + if pkg.Resolved == "" { + p.logger.Warn("`package-lock.json` contains broken link with empty `resolved` field. This package will be skipped to avoid receiving an empty package", log.String("pkg", pkgPath)) + delete(packages, pkgPath) + return false + } + return true + }) + // Early return + if len(links) == 0 { + return + } + + rootPkg := packages[""] + if rootPkg.Dependencies == nil { + rootPkg.Dependencies = make(map[string]string) + } + + workspaces := packagejson.ParseWorkspaces(rootPkg.Workspaces) + // Changing the map during the map iteration causes unexpected behavior, + // so we need to iterate over the cloned `packages` map, but change the original `packages` map. + for pkgPath, pkg := range maps.Clone(packages) { + for linkPath, link := range links { + if !strings.HasPrefix(pkgPath, link.Resolved) { + continue + } + // The target doesn't have the "resolved" field, so we need to copy it from the link. + if pkg.Resolved == "" { + pkg.Resolved = link.Resolved + } + + // Resolve the link package so all packages are located under "node_modules". + resolvedPath := strings.ReplaceAll(pkgPath, link.Resolved, linkPath) + packages[resolvedPath] = pkg + + // Delete the target package + delete(packages, pkgPath) + + if p.isWorkspace(pkgPath, workspaces) { + rootPkg.Dependencies[p.pkgNameFromPath(linkPath)] = pkg.Version + } + break + } + } + packages[""] = rootPkg +} + +func (p *Parser) isWorkspace(pkgPath string, workspaces []string) bool { + for _, workspace := range workspaces { + if match, err := path.Match(workspace, pkgPath); err != nil { + p.logger.Debug("Unable to parse workspace", + log.String("workspace", workspace), log.String("pkg_path", pkgPath)) + } else if match { + return true + } + } + return false +} + +func findDependsOn(pkgPath, depName string, packages map[string]Package) (string, error) { + depPath := joinPaths(pkgPath, nodeModulesDir) + paths := strings.Split(depPath, "/") + // Try to resolve the version with the nearest directory + // e.g. for pkgPath == `node_modules/body-parser/node_modules/debug`, depName == `ms`: + // - "node_modules/body-parser/node_modules/debug/node_modules/ms" + // - "node_modules/body-parser/node_modules/ms" + // - "node_modules/ms" + for i := len(paths) - 1; i >= 0; i-- { + if paths[i] != nodeModulesDir { + continue + } + modulePath := joinPaths(paths[:i+1]...) + modulePath = joinPaths(modulePath, depName) + + if dep, ok := packages[modulePath]; ok { + return packageID(depName, dep.Version), nil + } + } + + // It should not reach here. + return "", xerrors.Errorf("can't find dependsOn for %s", depName) +} + +func (p *Parser) parseV1(dependencies map[string]Dependency, versions map[string]string) ([]ftypes.Package, []ftypes.Dependency) { + // Update package name and version mapping. + for pkgName, dep := range dependencies { + // Overwrite the existing package version so that the nested version can take precedence. + versions[pkgName] = dep.Version + } + + var pkgs []ftypes.Package + var deps []ftypes.Dependency + for pkgName, dep := range dependencies { + pkg := ftypes.Package{ + ID: packageID(pkgName, dep.Version), + Name: pkgName, + Version: dep.Version, + Dev: dep.Dev, + Relationship: ftypes.RelationshipUnknown, // lockfile v1 schema doesn't have information about direct dependencies + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: dep.Resolved, + }, + }, + Locations: []ftypes.Location{ftypes.Location(dep.Location)}, + } + pkgs = append(pkgs, pkg) + + dependsOn := make([]string, 0, len(dep.Requires)) + for pName, requiredVer := range dep.Requires { + // Try to resolve the version with nested dependencies first + if resolvedDep, ok := dep.Dependencies[pName]; ok { + pkgID := packageID(pName, resolvedDep.Version) + dependsOn = append(dependsOn, pkgID) + continue + } + + // Try to resolve the version with the higher level dependencies + if ver, ok := versions[pName]; ok { + dependsOn = append(dependsOn, packageID(pName, ver)) + continue + } + + // It should not reach here. + p.logger.Warn("Unable to resolve the version", + log.String("name", pName), log.String("version", requiredVer)) + } + + if len(dependsOn) > 0 { + deps = append(deps, ftypes.Dependency{ + ID: packageID(pkg.Name, pkg.Version), + DependsOn: dependsOn, + }) + } + + if dep.Dependencies != nil { + // Recursion + childpkgs, childDeps := p.parseV1(dep.Dependencies, maps.Clone(versions)) + pkgs = append(pkgs, childpkgs...) + deps = append(deps, childDeps...) + } + } + + return pkgs, deps +} + +func (p *Parser) pkgNameFromPath(pkgPath string) string { + // lock file contains path to dependency in `node_modules`. e.g.: + // node_modules/string-width + // node_modules/string-width/node_modules/strip-ansi + // we renamed to `node_modules` directory prefixes `workspace` when resolving Links + // node_modules/function1 + // node_modules/nested_func/node_modules/debug + if index := strings.LastIndex(pkgPath, nodeModulesDir); index != -1 { + pkgName := pkgPath[index+len(nodeModulesDir):] + pkgName = strings.TrimPrefix(pkgName, "/") + + if pkgName == "" { + p.logger.Warn("Invalid package-lock.json file. Package path doesn't have package name suffix", log.String("pkg_path", pkgPath)) + return "" + } + + return pkgName + } + p.logger.Warn("Package path doesn't have `node_modules` prefix", log.String("pkg_path", pkgPath)) + return pkgPath +} + +func uniqueDeps(deps []ftypes.Dependency) []ftypes.Dependency { + var uniqDeps ftypes.Dependencies + unique := set.New[string]() + + for _, dep := range deps { + sort.Strings(dep.DependsOn) + depKey := fmt.Sprintf("%s:%s", dep.ID, strings.Join(dep.DependsOn, ",")) + if !unique.Contains(depKey) { + unique.Append(depKey) + uniqDeps = append(uniqDeps, dep) + } + } + + sort.Sort(uniqDeps) + return uniqDeps +} + +func isIndirectPkg(pkgPath string, directDeps set.Set[string]) bool { + // A project can contain 2 different versions of the same dependency. + // e.g. `node_modules/string-width/node_modules/strip-ansi` and `node_modules/string-ansi` + // direct dependencies always have root path (`node_modules/`) + if directDeps.Contains(pkgPath) { + return false + } + return true +} + +func joinPaths(paths ...string) string { + return strings.Join(paths, "/") +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.Npm, name, version) +} + +func sortExternalReferences(refs []ftypes.ExternalRef) { + sort.Slice(refs, func(i, j int) bool { + if refs[i].Type != refs[j].Type { + return refs[i].Type < refs[j].Type + } + return refs[i].URL < refs[j].URL + }) +} diff --git a/pkg/dependency/parser/nodejs/npm/parse_test.go b/pkg/dependency/parser/nodejs/npm/parse_test.go new file mode 100644 index 000000000000..f9984b21128f --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/parse_test.go @@ -0,0 +1,130 @@ +package npm + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + file string // Test input file + want []ftypes.Package + wantDeps []ftypes.Dependency + }{ + { + name: "lock version v1", + file: "testdata/package-lock_v1.json", + want: npmV1Pkgs, + wantDeps: npmDeps, + }, + { + name: "lock version v2", + file: "testdata/package-lock_v2.json", + want: npmV2Pkgs, + wantDeps: npmDeps, + }, + { + name: "lock version v3", + file: "testdata/package-lock_v3.json", + want: npmV2Pkgs, + wantDeps: npmDeps, + }, + { + name: "lock version v3 with workspace", + file: "testdata/package-lock_v3_with_workspace.json", + want: npmV3WithWorkspacePkgs, + wantDeps: npmV3WithWorkspaceDeps, + }, + { + name: "lock version v3 with workspace as object", + file: "testdata/package-lock_v3_with_workspace_as_object.json", + want: npmV3WithWorkspaceAsObjectPkgs, + wantDeps: nil, + }, + { + name: "lock version v3 with peer dependencies", + file: "testdata/package-lock_v3_with_peer.json", + want: npmV3WithPeerDependenciesPkgs, + wantDeps: npmV3WithPeerDependenciesDeps, + }, + { + name: "lock file v3 contains same dev and non-dev dependencies", + file: "testdata/package-lock_v3_with-same-dev-and-non-dev.json", + want: npmV3WithSameDevAndNonDevPkgs, + wantDeps: npmV3WithSameDevAndNonDevDeps, + }, + { + name: "lock version v3 with workspace and without direct deps field", + file: "testdata/package-lock_v3_without_root_deps_field.json", + want: npmV3WithoutRootDepsField, + wantDeps: npmV3WithoutRootDepsFieldDeps, + }, + { + name: "lock version v3 with broken link", + file: "testdata/package-lock_v3_broken_link.json", + want: nil, + wantDeps: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + + got, deps, err := NewParser().Parse(t.Context(), f) + require.NoError(t, err) + + assert.Equal(t, tt.want, got) + if tt.wantDeps != nil { + assert.Equal(t, tt.wantDeps, deps) + } + }) + } +} + +func TestPkgNameFromPath(t *testing.T) { + tests := []struct { + path string + expected string + }{ + { + path: "node_modules/package-name", + expected: "package-name", + }, + { + path: "node_modules/@package-namespace/package-name", + expected: "@package-namespace/package-name", + }, + { + path: "node_modules/package-name/node_modules/sub-sub-package", + expected: "sub-sub-package", + }, + { + path: "no/node/modules/dir", + expected: "no/node/modules/dir", + }, + { + path: "node_modules", + expected: "", + }, + { + path: "node_modules/", + expected: "", + }, + } + + parser := NewParser() + for _, test := range tests { + t.Run(test.path, func(t *testing.T) { + path := parser.pkgNameFromPath(test.path) + assert.Equal(t, test.expected, path) + }) + } +} diff --git a/pkg/dependency/parser/nodejs/npm/parse_testcase.go b/pkg/dependency/parser/nodejs/npm/parse_testcase.go new file mode 100644 index 000000000000..0d23e10f4b13 --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/parse_testcase.go @@ -0,0 +1,1770 @@ +package npm + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +var ( + // docker run --name node --rm -it node@sha256:51dd437f31812df71108b81385e2945071ec813d5815fa3403855669c8f3432b sh + // mkdir node_v1 && cd node_v1 + // npm init --force + // npm install --save finalhandler@1.1.1 body-parser@1.18.3 ms@1.0.0 @babel/helper-string-parser@7.19.4 + // npm install --save-dev debug@2.5.2 + // npm install --save-optional promise + // npm i --lockfile-version 1 + // packages are filled manually + + npmV1Pkgs = []ftypes.Package{ + { + ID: "@babel/helper-string-parser@7.19.4", + Name: "@babel/helper-string-parser", + Version: "7.19.4", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 7, + EndLine: 11, + }, + }, + }, + { + ID: "asap@2.0.6", + Name: "asap", + Version: "2.0.6", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 12, + EndLine: 17, + }, + }, + }, + { + ID: "body-parser@1.18.3", + Name: "body-parser", + Version: "1.18.3", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 18, + EndLine: 49, + }, + }, + }, + { + ID: "bytes@3.0.0", + Name: "bytes", + Version: "3.0.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 50, + EndLine: 54, + }, + }, + }, + { + ID: "content-type@1.0.5", + Name: "content-type", + Version: "1.0.5", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 55, + EndLine: 59, + }, + }, + }, + { + ID: "debug@2.5.2", + Name: "debug", + Version: "2.5.2", + Dev: true, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/debug/-/debug-2.5.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 60, + EndLine: 76, + }, + }, + }, + { + ID: "debug@2.6.9", + Name: "debug", + Version: "2.6.9", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 35, + EndLine: 42, + }, + { + StartLine: 111, + EndLine: 118, + }, + }, + }, + { + ID: "depd@1.1.2", + Name: "depd", + Version: "1.1.2", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 77, + EndLine: 81, + }, + }, + }, + { + ID: "ee-first@1.1.1", + Name: "ee-first", + Version: "1.1.1", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 82, + EndLine: 86, + }, + }, + }, + { + ID: "encodeurl@1.0.2", + Name: "encodeurl", + Version: "1.0.2", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 87, + EndLine: 91, + }, + }, + }, + { + ID: "escape-html@1.0.3", + Name: "escape-html", + Version: "1.0.3", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 92, + EndLine: 96, + }, + }, + }, + { + ID: "finalhandler@1.1.1", + Name: "finalhandler", + Version: "1.1.1", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 97, + EndLine: 125, + }, + }, + }, + { + ID: "http-errors@1.6.3", + Name: "http-errors", + Version: "1.6.3", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 126, + EndLine: 136, + }, + }, + }, + { + ID: "iconv-lite@0.4.23", + Name: "iconv-lite", + Version: "0.4.23", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 137, + EndLine: 144, + }, + }, + }, + { + ID: "inherits@2.0.3", + Name: "inherits", + Version: "2.0.3", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 145, + EndLine: 149, + }, + }, + }, + { + ID: "media-typer@0.3.0", + Name: "media-typer", + Version: "0.3.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 150, + EndLine: 154, + }, + }, + }, + { + ID: "mime-db@1.52.0", + Name: "mime-db", + Version: "1.52.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 155, + EndLine: 159, + }, + }, + }, + { + ID: "mime-types@2.1.35", + Name: "mime-types", + Version: "2.1.35", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 160, + EndLine: 167, + }, + }, + }, + { + ID: "ms@0.7.2", + Name: "ms", + Version: "0.7.2", + Dev: true, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 69, + EndLine: 74, + }, + }, + }, + { + ID: "ms@1.0.0", + Name: "ms", + Version: "1.0.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-1.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 168, + EndLine: 172, + }, + }, + }, + { + ID: "ms@2.0.0", + Name: "ms", + Version: "2.0.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 43, + EndLine: 47, + }, + { + StartLine: 119, + EndLine: 123, + }, + }, + }, + { + ID: "on-finished@2.3.0", + Name: "on-finished", + Version: "2.3.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 173, + EndLine: 180, + }, + }, + }, + { + ID: "parseurl@1.3.3", + Name: "parseurl", + Version: "1.3.3", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 181, + EndLine: 185, + }, + }, + }, + { + ID: "promise@8.3.0", + Name: "promise", + Version: "8.3.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 186, + EndLine: 194, + }, + }, + }, + { + ID: "qs@6.5.2", + Name: "qs", + Version: "6.5.2", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 195, + EndLine: 199, + }, + }, + }, + { + ID: "raw-body@2.3.3", + Name: "raw-body", + Version: "2.3.3", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 200, + EndLine: 210, + }, + }, + }, + { + ID: "safer-buffer@2.1.2", + Name: "safer-buffer", + Version: "2.1.2", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 211, + EndLine: 215, + }, + }, + }, + { + ID: "setprototypeof@1.1.0", + Name: "setprototypeof", + Version: "1.1.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 216, + EndLine: 220, + }, + }, + }, + { + ID: "statuses@1.4.0", + Name: "statuses", + Version: "1.4.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 221, + EndLine: 225, + }, + }, + }, + { + ID: "type-is@1.6.18", + Name: "type-is", + Version: "1.6.18", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 226, + EndLine: 234, + }, + }, + }, + { + ID: "unpipe@1.0.0", + Name: "unpipe", + Version: "1.0.0", + Dev: false, + Relationship: ftypes.RelationshipUnknown, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 235, + EndLine: 239, + }, + }, + }, + } + + // dependencies are filled manually + npmDeps = []ftypes.Dependency{ + { + ID: "body-parser@1.18.3", + DependsOn: []string{ + "bytes@3.0.0", + "content-type@1.0.5", + "debug@2.6.9", + "depd@1.1.2", + "http-errors@1.6.3", + "iconv-lite@0.4.23", + "on-finished@2.3.0", + "qs@6.5.2", + "raw-body@2.3.3", + "type-is@1.6.18", + }, + }, + { + ID: "debug@2.5.2", + DependsOn: []string{"ms@0.7.2"}, + }, + { + ID: "debug@2.6.9", + DependsOn: []string{"ms@2.0.0"}, + }, + { + ID: "finalhandler@1.1.1", + DependsOn: []string{ + "debug@2.6.9", + "encodeurl@1.0.2", + "escape-html@1.0.3", + "on-finished@2.3.0", + "parseurl@1.3.3", + "statuses@1.4.0", + "unpipe@1.0.0", + }, + }, + { + ID: "http-errors@1.6.3", + DependsOn: []string{ + "depd@1.1.2", + "inherits@2.0.3", + "setprototypeof@1.1.0", + "statuses@1.4.0", + }, + }, + { + ID: "iconv-lite@0.4.23", + DependsOn: []string{"safer-buffer@2.1.2"}, + }, + { + ID: "mime-types@2.1.35", + DependsOn: []string{"mime-db@1.52.0"}, + }, + { + ID: "on-finished@2.3.0", + DependsOn: []string{"ee-first@1.1.1"}, + }, + { + ID: "promise@8.3.0", + DependsOn: []string{"asap@2.0.6"}, + }, + { + ID: "raw-body@2.3.3", + DependsOn: []string{ + "bytes@3.0.0", + "http-errors@1.6.3", + "iconv-lite@0.4.23", + "unpipe@1.0.0", + }, + }, + { + ID: "type-is@1.6.18", + DependsOn: []string{ + "media-typer@0.3.0", + "mime-types@2.1.35", + }, + }, + } + + // ... and + // npm i --lockfile-version 2 + // same as npmV1Pkgs but change `Indirect` field to false for `body-parser@1.18.3`, `finalhandler@1.1.1`, `@babel/helper-string-parser@7.19.4`, `promise@8.3.0` and `ms@1.0.0` packages. + // also need to get locations from `packages` struct + // --- lockfile version 3 --- + // npm i --lockfile-version 3 + // same as npmV2Pkgs. + npmV2Pkgs = []ftypes.Package{ + { + ID: "@babel/helper-string-parser@7.19.4", + Name: "@babel/helper-string-parser", + Version: "7.19.4", + Dev: false, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 24, + EndLine: 31, + }, + }, + }, + { + ID: "body-parser@1.18.3", + Name: "body-parser", + Version: "1.18.3", + Dev: false, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 38, + EndLine: 57, + }, + }, + }, + { + ID: "debug@2.5.2", + Name: "debug", + Version: "2.5.2", + Dev: true, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/debug/-/debug-2.5.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 87, + EndLine: 95, + }, + }, + }, + { + ID: "finalhandler@1.1.1", + Name: "finalhandler", + Version: "1.1.1", + Dev: false, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 128, + EndLine: 144, + }, + }, + }, + { + ID: "ms@1.0.0", + Name: "ms", + Version: "1.0.0", + Dev: false, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-1.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 215, + EndLine: 219, + }, + }, + }, + { + ID: "promise@8.3.0", + Name: "promise", + Version: "8.3.0", + Dev: false, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 239, + EndLine: 247, + }, + }, + }, + { + ID: "asap@2.0.6", + Name: "asap", + Version: "2.0.6", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 32, + EndLine: 37, + }, + }, + }, + { + ID: "bytes@3.0.0", + Name: "bytes", + Version: "3.0.0", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 71, + EndLine: 78, + }, + }, + }, + { + ID: "content-type@1.0.5", + Name: "content-type", + Version: "1.0.5", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 79, + EndLine: 86, + }, + }, + }, + { + ID: "debug@2.6.9", + Name: "debug", + Version: "2.6.9", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 58, + EndLine: 65, + }, + { + StartLine: 145, + EndLine: 152, + }, + }, + }, + { + ID: "depd@1.1.2", + Name: "depd", + Version: "1.1.2", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 102, + EndLine: 109, + }, + }, + }, + { + ID: "ee-first@1.1.1", + Name: "ee-first", + Version: "1.1.1", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 110, + EndLine: 114, + }, + }, + }, + { + ID: "encodeurl@1.0.2", + Name: "encodeurl", + Version: "1.0.2", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 115, + EndLine: 122, + }, + }, + }, + { + ID: "escape-html@1.0.3", + Name: "escape-html", + Version: "1.0.3", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 123, + EndLine: 127, + }, + }, + }, + { + ID: "http-errors@1.6.3", + Name: "http-errors", + Version: "1.6.3", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 158, + EndLine: 171, + }, + }, + }, + { + ID: "iconv-lite@0.4.23", + Name: "iconv-lite", + Version: "0.4.23", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 172, + EndLine: 182, + }, + }, + }, + { + ID: "inherits@2.0.3", + Name: "inherits", + Version: "2.0.3", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 183, + EndLine: 187, + }, + }, + }, + { + ID: "media-typer@0.3.0", + Name: "media-typer", + Version: "0.3.0", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 188, + EndLine: 195, + }, + }, + }, + { + ID: "mime-db@1.52.0", + Name: "mime-db", + Version: "1.52.0", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 196, + EndLine: 203, + }, + }, + }, + { + ID: "mime-types@2.1.35", + Name: "mime-types", + Version: "2.1.35", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 204, + EndLine: 214, + }, + }, + }, + { + ID: "ms@0.7.2", + Name: "ms", + Version: "0.7.2", + Dev: true, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 96, + EndLine: 101, + }, + }, + }, + { + ID: "ms@2.0.0", + Name: "ms", + Version: "2.0.0", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 66, + EndLine: 70, + }, + { + StartLine: 153, + EndLine: 157, + }, + }, + }, + { + ID: "on-finished@2.3.0", + Name: "on-finished", + Version: "2.3.0", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 220, + EndLine: 230, + }, + }, + }, + { + ID: "parseurl@1.3.3", + Name: "parseurl", + Version: "1.3.3", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 231, + EndLine: 238, + }, + }, + }, + { + ID: "qs@6.5.2", + Name: "qs", + Version: "6.5.2", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 248, + EndLine: 255, + }, + }, + }, + { + ID: "raw-body@2.3.3", + Name: "raw-body", + Version: "2.3.3", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 256, + EndLine: 269, + }, + }, + }, + { + ID: "safer-buffer@2.1.2", + Name: "safer-buffer", + Version: "2.1.2", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 270, + EndLine: 274, + }, + }, + }, + { + ID: "setprototypeof@1.1.0", + Name: "setprototypeof", + Version: "1.1.0", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 275, + EndLine: 279, + }, + }, + }, + { + ID: "statuses@1.4.0", + Name: "statuses", + Version: "1.4.0", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 280, + EndLine: 287, + }, + }, + }, + { + ID: "type-is@1.6.18", + Name: "type-is", + Version: "1.6.18", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 288, + EndLine: 299, + }, + }, + }, + { + ID: "unpipe@1.0.0", + Name: "unpipe", + Version: "1.0.0", + Dev: false, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 300, + EndLine: 307, + }, + }, + }, + } + + // docker run --name node --rm -it node@sha256:51dd437f31812df71108b81385e2945071ec813d5815fa3403855669c8f3432b sh + // mkdir node_v3_with_workspace && cd node_v3_with_workspace + // npm init --force + // npm init -w ./functions/func1 !!! use `function1` name for package + // grep -v "version" ./functions/func1/package.json > tmpfile && mv tmpfile ./functions/func1/package.json + // npm init -w ./functions/nested_func --force + // npm install --save debug@2.5.2 + // sed -i 's/\^/=/g' package.json + // npm install --save debug@2.6.9 -w nested_func + // npm install nested_func -w function1 + // grep -v "functions/func1" ./package.json > tmpfile && mv tmpfile ./package.json + // sed -i 's/functions\/nested_func/functions\/*/g' package.json + // npm update + // packages are filled manually + npmV3WithWorkspacePkgs = []ftypes.Package{ + { + ID: "debug@2.5.2", + Name: "debug", + Version: "2.5.2", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/debug/-/debug-2.5.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 39, + EndLine: 46, + }, + }, + }, + { + ID: "function1", + Name: "function1", + Version: "", + Licenses: []string{"ISC"}, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "functions/func1", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 18, + EndLine: 23, + }, + }, + }, + { + ID: "nested_func@1.0.0", + Name: "nested_func", + Version: "1.0.0", + Licenses: []string{"ISC"}, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "functions/nested_func", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 24, + EndLine: 30, + }, + }, + }, + { + ID: "debug@2.6.9", + Name: "debug", + Version: "2.6.9", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 31, + EndLine: 38, + }, + }, + }, + { + ID: "ms@0.7.2", + Name: "ms", + Version: "0.7.2", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 47, + EndLine: 51, + }, + }, + }, + { + ID: "ms@2.0.0", + Name: "ms", + Version: "2.0.0", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 56, + EndLine: 60, + }, + }, + }, + } + + npmV3WithWorkspaceDeps = []ftypes.Dependency{ + { + ID: "debug@2.5.2", + DependsOn: []string{"ms@0.7.2"}, + }, + { + ID: "debug@2.6.9", + DependsOn: []string{"ms@2.0.0"}, + }, + { + ID: "function1", + DependsOn: []string{"nested_func@1.0.0"}, + }, + { + ID: "nested_func@1.0.0", + DependsOn: []string{"debug@2.6.9"}, + }, + } + + npmV3WithWorkspaceAsObjectPkgs = []ftypes.Package{ + { + ID: "testapp@1.0.0", + Name: "testapp", + Version: "1.0.0", + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "testapp", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 26, + EndLine: 31, + }, + }, + }, + { + ID: "lodash@4.17.21", + Name: "lodash", + Version: "4.17.21", + Licenses: []string{"MIT"}, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 16, + EndLine: 21, + }, + }, + }, + } + // docker run --name node --rm -it node@sha256:51dd437f31812df71108b81385e2945071ec813d5815fa3403855669c8f3432b sh + // mkdir node_v3_with_peer && cd node_v3_with_peer + // npm init --force + // npm install --save winston-mail@2.0.0 + // npm install --save-peer lodash@4.17.21 + // npm update + // + // Delete unnecessary packages from package-lock.json + // Packages are filled manually + npmV3WithPeerDependenciesPkgs = []ftypes.Package{ + { + ID: "lodash@4.17.21", + Name: "lodash", + Version: "4.17.21", + Licenses: []string{"MIT"}, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 30, + EndLine: 36, + }, + }, + }, + { + ID: "winston-mail@2.0.0", + Name: "winston-mail", + Version: "2.0.0", + Licenses: []string{"MIT"}, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/winston-mail/-/winston-mail-2.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 60, + EndLine: 74, + }, + }, + }, + { + ID: "mustache@2.3.2", + Name: "mustache", + Version: "2.3.2", + Licenses: []string{"MIT"}, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 18, + EndLine: 29, + }, + }, + }, + { + ID: "triple-beam@1.4.1", + Name: "triple-beam", + Version: "1.4.1", + Licenses: []string{"MIT"}, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 37, + EndLine: 46, + }, + }, + }, + { + ID: "winston@3.17.0", + Name: "winston", + Version: "3.17.0", + Licenses: []string{"MIT"}, + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 47, + EndLine: 59, + }, + }, + }, + } + npmV3WithPeerDependenciesDeps = []ftypes.Dependency{ + { + ID: "winston-mail@2.0.0", + DependsOn: []string{ + "mustache@2.3.2", + "winston@3.17.0", + }, + }, + { + ID: "winston@3.17.0", + DependsOn: []string{"triple-beam@1.4.1"}, + }, + } + + // docker run --name node --rm -it node@sha256:51dd437f31812df71108b81385e2945071ec813d5815fa3403855669c8f3432b sh + // mkdir node_v3_without_direct_deps && cd node_v3_without_direct_deps + // npm init --force + // npm init -w ./functions/func1 --force + // npm install --save debug@2.6.9 -w func1 + // packages are filled manually + npmV3WithoutRootDepsField = []ftypes.Package{ + { + ID: "func1@1.0.0", + Name: "func1", + Version: "1.0.0", + Licenses: []string{"ISC"}, + Relationship: ftypes.RelationshipDirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "functions/func1", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 15, + EndLine: 21, + }, + }, + }, + { + ID: "debug@2.6.9", + Name: "debug", + Version: "2.6.9", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 22, + EndLine: 29, + }, + }, + }, + { + ID: "ms@2.0.0", + Name: "ms", + Version: "2.0.0", + Relationship: ftypes.RelationshipIndirect, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 34, + EndLine: 38, + }, + }, + }, + } + + npmV3WithoutRootDepsFieldDeps = []ftypes.Dependency{ + { + ID: "debug@2.6.9", + DependsOn: []string{"ms@2.0.0"}, + }, + { + ID: "func1@1.0.0", + DependsOn: []string{"debug@2.6.9"}, + }, + } + + npmV3WithSameDevAndNonDevPkgs = []ftypes.Package{ + { + ID: "fsevents@1.2.9", + Name: "fsevents", + Version: "1.2.9", + Relationship: ftypes.RelationshipDirect, + Dev: true, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + }, + }, + Locations: []ftypes.Location{ + { + StartLine: 18, + EndLine: 37, + }, + }, + }, + { + ID: "minimist@0.0.8", + Name: "minimist", + Version: "0.0.8", + Relationship: ftypes.RelationshipDirect, + Dev: false, + ExternalReferences: []ftypes.ExternalRef{ + { + Type: ftypes.RefOther, + URL: "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + }, + }, + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 38, + EndLine: 43, + }, + { + StartLine: 68, + EndLine: 72, + }, + }, + }, + { + ID: "mkdirp@0.5.1", + Name: "mkdirp", + Version: "0.5.1", + Licenses: []string{"MIT"}, + Relationship: ftypes.RelationshipIndirect, + Dev: true, + Locations: []ftypes.Location{ + { + StartLine: 44, + EndLine: 55, + }, + }, + }, + { + ID: "node-pre-gyp@0.12.0", + Name: "node-pre-gyp", + Version: "0.12.0", + Licenses: []string{"BSD-3-Clause"}, + Relationship: ftypes.RelationshipIndirect, + Dev: true, + Locations: []ftypes.Location{ + { + StartLine: 56, + EndLine: 67, + }, + }, + }, + } + + npmV3WithSameDevAndNonDevDeps = []ftypes.Dependency{ + { + ID: "fsevents@1.2.9", + DependsOn: []string{"node-pre-gyp@0.12.0"}, + }, + { + ID: "mkdirp@0.5.1", + DependsOn: []string{"minimist@0.0.8"}, + }, + { + ID: "node-pre-gyp@0.12.0", + DependsOn: []string{"mkdirp@0.5.1"}, + }, + } +) diff --git a/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v1.json b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v1.json new file mode 100644 index 000000000000..17958d1b90d7 --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v1.json @@ -0,0 +1,241 @@ +{ + "name": "node_v1", + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "optional": true + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "debug": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.5.2.tgz", + "integrity": "sha512-iHrIBaTK1JzBz5WvitFmZGaTCO/mHiU3NKi8UKjh7rU2JboIbVMZU7pFSCpvc2NxfkrvyaQ5zfdNRJnft/TcoQ==", + "dev": true, + "requires": { + "ms": "0.7.2" + }, + "dependencies": { + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha512-5NnE67nQSQDJHVahPJna1PQ/zCXMnQop3yUCxjKPNzCxuyPSKWTQ/5Gu5CZmjetwGLWRA+PzeF5thlbOdbQldA==", + "dev": true + } + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-1.0.0.tgz", + "integrity": "sha512-85ytwCiGUnD84ui6ULG1KBFMaZgHW3jg5KPr9jt+ZPYt75+XK+JGbYddGrBQ+RSHXOhekCnCZwJywBoFvFl0kw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "optional": true, + "requires": { + "asap": "~2.0.6" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + } + } +} \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v2.json b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v2.json new file mode 100644 index 000000000000..f983f461593c --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v2.json @@ -0,0 +1,544 @@ +{ + "name": "node_v1", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "node_v1", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "body-parser": "^1.18.3", + "finalhandler": "^1.1.1", + "ms": "^1.0.0" + }, + "devDependencies": { + "debug": "^2.5.2" + }, + "optionalDependencies": { + "promise": "^8.3.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "optional": true + }, + "node_modules/body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==", + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.5.2.tgz", + "integrity": "sha512-iHrIBaTK1JzBz5WvitFmZGaTCO/mHiU3NKi8UKjh7rU2JboIbVMZU7pFSCpvc2NxfkrvyaQ5zfdNRJnft/TcoQ==", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/debug/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha512-5NnE67nQSQDJHVahPJna1PQ/zCXMnQop3yUCxjKPNzCxuyPSKWTQ/5Gu5CZmjetwGLWRA+PzeF5thlbOdbQldA==", + "dev": true + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-1.0.0.tgz", + "integrity": "sha512-85ytwCiGUnD84ui6ULG1KBFMaZgHW3jg5KPr9jt+ZPYt75+XK+JGbYddGrBQ+RSHXOhekCnCZwJywBoFvFl0kw==" + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "optional": true, + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "dependencies": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + } + }, + "dependencies": { + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "optional": true + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==", + "requires": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==" + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" + }, + "debug": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.5.2.tgz", + "integrity": "sha512-iHrIBaTK1JzBz5WvitFmZGaTCO/mHiU3NKi8UKjh7rU2JboIbVMZU7pFSCpvc2NxfkrvyaQ5zfdNRJnft/TcoQ==", + "dev": true, + "requires": { + "ms": "0.7.2" + }, + "dependencies": { + "ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha512-5NnE67nQSQDJHVahPJna1PQ/zCXMnQop3yUCxjKPNzCxuyPSKWTQ/5Gu5CZmjetwGLWRA+PzeF5thlbOdbQldA==", + "dev": true + } + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "ms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-1.0.0.tgz", + "integrity": "sha512-85ytwCiGUnD84ui6ULG1KBFMaZgHW3jg5KPr9jt+ZPYt75+XK+JGbYddGrBQ+RSHXOhekCnCZwJywBoFvFl0kw==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "requires": { + "ee-first": "1.1.1" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "optional": true, + "requires": { + "asap": "~2.0.6" + } + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "requires": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==" + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + } + } +} \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3.json b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3.json new file mode 100644 index 000000000000..6db46f4f7267 --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3.json @@ -0,0 +1,309 @@ +{ + "name": "node_v1", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node_v1", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "body-parser": "^1.18.3", + "finalhandler": "^1.1.1", + "ms": "^1.0.0" + }, + "devDependencies": { + "debug": "^2.5.2" + }, + "optionalDependencies": { + "promise": "^8.3.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "optional": true + }, + "node_modules/body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha512-YQyoqQG3sO8iCmf8+hyVpgHHOv0/hCEFiS4zTGUwTA1HjAFX66wRcNQrVCeJq9pgESMRvUAOvSil5MJlmccuKQ==", + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/debug": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.5.2.tgz", + "integrity": "sha512-iHrIBaTK1JzBz5WvitFmZGaTCO/mHiU3NKi8UKjh7rU2JboIbVMZU7pFSCpvc2NxfkrvyaQ5zfdNRJnft/TcoQ==", + "dev": true, + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/debug/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha512-5NnE67nQSQDJHVahPJna1PQ/zCXMnQop3yUCxjKPNzCxuyPSKWTQ/5Gu5CZmjetwGLWRA+PzeF5thlbOdbQldA==", + "dev": true + }, + "node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/finalhandler": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.4.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.23.tgz", + "integrity": "sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-1.0.0.tgz", + "integrity": "sha512-85ytwCiGUnD84ui6ULG1KBFMaZgHW3jg5KPr9jt+ZPYt75+XK+JGbYddGrBQ+RSHXOhekCnCZwJywBoFvFl0kw==" + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "optional": true, + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/raw-body": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.3.tgz", + "integrity": "sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw==", + "dependencies": { + "bytes": "3.0.0", + "http-errors": "1.6.3", + "iconv-lite": "0.4.23", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "node_modules/statuses": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz", + "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + } + } +} \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_broken_link.json b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_broken_link.json new file mode 100644 index 000000000000..d01c0fe3e996 --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_broken_link.json @@ -0,0 +1,24 @@ +{ + "name": "node_v3_without_direct_deps", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node_v3_without_direct_deps", + "version": "1.0.0", + "license": "ISC" + }, + "functions/func1": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "debug": "^2.6.9" + } + }, + "node_modules/func1": { + "resolved": "", + "link": true + } + } +} \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with-same-dev-and-non-dev.json b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with-same-dev-and-non-dev.json new file mode 100644 index 000000000000..4fe518b82bcd --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with-same-dev-and-non-dev.json @@ -0,0 +1,74 @@ +{ + "name": "5139", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "5139", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "minimist": "^0.0.8" + }, + "devDependencies": { + "fsevents": "^1.2.9" + } + }, + "node_modules/fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "bundleDependencies": [ + "node-pre-gyp" + ], + "deprecated": "The v1 package contains DANGEROUS / INSECURE binaries. Upgrade to safe fsevents v2", + "dev": true, + "hasInstallScript": true, + "os": [ + "darwin" + ], + "dependencies": { + "node-pre-gyp": "^0.12.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/fsevents/node_modules/minimist": { + "version": "0.0.8", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/fsevents/node_modules/mkdirp": { + "version": "0.5.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/fsevents/node_modules/node-pre-gyp": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "mkdirp": "^0.5.1" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha512-miQKw5Hv4NS1Psg2517mV4e4dYNaO3++hjAvLOAzKqZ61rH8NS1SK+vbfBWZ5PY/Me/bEWhUwqMghEW5Fb9T7Q==" + } + } +} diff --git a/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with_peer.json b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with_peer.json new file mode 100644 index 000000000000..51e0b7351796 --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with_peer.json @@ -0,0 +1,76 @@ +{ + "name": "node_v3_with_peer", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node_v3_with_peer", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "winston-mail": "^2.0.0" + }, + "peerDependencies": { + "lodash": "^4.17.21" + } + }, + "node_modules/mustache": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-2.3.2.tgz", + "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + }, + "engines": { + "npm": ">=1.4.0" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT", + "peer": true + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "peer": true, + "dependencies": { + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-mail": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/winston-mail/-/winston-mail-2.0.0.tgz", + "integrity": "sha512-Wp+mKiieoV6FAZJNyNMS62Zsf5FBSxe17j0f4fpFYeA+rfW8nEZ2eBGGl7+vq+dr3dEpefV5D+ZI3d9jaqdRfw==", + "license": "MIT", + "dependencies": { + "mustache": "^2.2.1" + }, + "engines": { + "node": ">= 0.6.0" + }, + "peerDependencies": { + "winston": ">=0.5.0" + } + } + } +} \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with_workspace.json b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with_workspace.json new file mode 100644 index 000000000000..2d765d5d7fd1 --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with_workspace.json @@ -0,0 +1,66 @@ +{ + "name": "node_v3_with_workspace", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node_v3_with_workspace", + "version": "1.0.0", + "license": "ISC", + "workspaces": [ + "functions/*" + ], + "dependencies": { + "debug": "=2.5.2" + } + }, + "functions/func1": { + "license": "ISC", + "dependencies": { + "nested_func": "^1.0.0" + } + }, + "functions/nested_func": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "debug": "^2.6.9" + } + }, + "functions/nested_func/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/debug": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.5.2.tgz", + "integrity": "sha512-iHrIBaTK1JzBz5WvitFmZGaTCO/mHiU3NKi8UKjh7rU2JboIbVMZU7pFSCpvc2NxfkrvyaQ5zfdNRJnft/TcoQ==", + "dependencies": { + "ms": "0.7.2" + } + }, + "node_modules/debug/node_modules/ms": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.2.tgz", + "integrity": "sha512-5NnE67nQSQDJHVahPJna1PQ/zCXMnQop3yUCxjKPNzCxuyPSKWTQ/5Gu5CZmjetwGLWRA+PzeF5thlbOdbQldA==" + }, + "node_modules/function1": { + "resolved": "functions/func1", + "link": true + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/nested_func": { + "resolved": "functions/nested_func", + "link": true + } + } +} diff --git a/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with_workspace_as_object.json b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with_workspace_as_object.json new file mode 100644 index 000000000000..a3586e1dbf45 --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_with_workspace_as_object.json @@ -0,0 +1,33 @@ +{ + "name": "node_v3_with_workspace_as_object", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node_v3_with_workspace_as_object", + "version": "1.0.0", + "workspaces": { + "packages": [ + "testapp" + ] + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/testapp": { + "resolved": "testapp", + "link": true + }, + "testapp": { + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.21" + } + } + } +} diff --git a/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_without_root_deps_field.json b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_without_root_deps_field.json new file mode 100644 index 000000000000..49cc58f4bb4b --- /dev/null +++ b/pkg/dependency/parser/nodejs/npm/testdata/package-lock_v3_without_root_deps_field.json @@ -0,0 +1,40 @@ +{ + "name": "node_v3_without_direct_deps", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "node_v3_without_direct_deps", + "version": "1.0.0", + "license": "ISC", + "workspaces": [ + "functions/func1" + ] + }, + "functions/func1": { + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "debug": "^2.6.9" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/func1": { + "resolved": "functions/func1", + "link": true + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } +} \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/packagejson/parse.go b/pkg/dependency/parser/nodejs/packagejson/parse.go new file mode 100644 index 000000000000..2db45ef192fc --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/parse.go @@ -0,0 +1,123 @@ +package packagejson + +import ( + "encoding/json" + "io" + "regexp" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +var nameRegexp = regexp.MustCompile(`^(@[A-Za-z0-9-._]+/)?[A-Za-z0-9-._]+$`) + +type packageJSON struct { + Name string `json:"name"` + Version string `json:"version"` + License any `json:"license"` + Dependencies map[string]string `json:"dependencies"` + OptionalDependencies map[string]string `json:"optionalDependencies"` + DevDependencies map[string]string `json:"devDependencies"` + Workspaces any `json:"workspaces"` +} + +type Package struct { + ftypes.Package + Dependencies map[string]string + OptionalDependencies map[string]string + DevDependencies map[string]string + Workspaces []string +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(r io.Reader) (Package, error) { + var pkgJSON packageJSON + if err := json.NewDecoder(r).Decode(&pkgJSON); err != nil { + return Package{}, xerrors.Errorf("JSON decode error: %w", err) + } + + if !IsValidName(pkgJSON.Name) { + return Package{}, xerrors.Errorf("Name can only contain URL-friendly characters") + } + + var id string + // Name and version fields are optional + // https://docs.npmjs.com/cli/v9/configuring-npm/package-json#name + if pkgJSON.Name != "" && pkgJSON.Version != "" { + id = dependency.ID(ftypes.NodePkg, pkgJSON.Name, pkgJSON.Version) + } + + return Package{ + Package: ftypes.Package{ + ID: id, + Name: pkgJSON.Name, + Version: pkgJSON.Version, + Licenses: parseLicense(pkgJSON.License), + }, + Dependencies: pkgJSON.Dependencies, + OptionalDependencies: pkgJSON.OptionalDependencies, + DevDependencies: pkgJSON.DevDependencies, + Workspaces: ParseWorkspaces(pkgJSON.Workspaces), + }, nil +} + +func parseLicense(val any) []string { + // the license isn't always a string, check for legacy struct if not string + switch v := val.(type) { + case string: + if v != "" { + return []string{v} + } + case map[string]any: + if license, ok := v["type"]; ok { + if s, ok := license.(string); ok && s != "" { + return []string{s} + } + } + } + return nil +} + +// ParseWorkspaces returns slice of workspaces +// `workspaces` field supports 2 types - +// string array and map with `packages` key only +// cf. https://github.com/npm/map-workspaces#usage +func ParseWorkspaces(val any) []string { + // Workspaces support 2 types - https://github.com/SchemaStore/schemastore/blob/d9516961f8a5b0e65a457808070147b5a866f60b/src/schemas/json/package.json#L777 + switch ws := val.(type) { + // Workspace as object (map[string][]string) + // e.g. "workspaces": {"packages": ["packages/*", "plugins/*"]}, + case map[string]any: + // Take only workspaces for `packages`: + // cf . https://github.com/npm/map-workspaces#usage + if pkgsWorkspaces, ok := ws["packages"]; ok { + return xslices.Map(pkgsWorkspaces.([]any), func(workspace any) string { + return workspace.(string) + }) + } + // Workspace as string array + // e.g. "workspaces": ["packages/*", "backend"], + case []any: + return xslices.Map(ws, func(workspace any) string { + return workspace.(string) + }) + } + return nil +} + +func IsValidName(name string) bool { + // Name is optional field + // https://docs.npmjs.com/cli/v9/configuring-npm/package-json#name + if name == "" { + return true + } + return nameRegexp.MatchString(name) +} diff --git a/pkg/dependency/parser/nodejs/packagejson/parse_test.go b/pkg/dependency/parser/nodejs/packagejson/parse_test.go new file mode 100644 index 000000000000..56e0dd34c17b --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/parse_test.go @@ -0,0 +1,186 @@ +package packagejson_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/packagejson" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + vectors := []struct { + name string + inputFile string + want packagejson.Package + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/package.json", + + // docker run --name composer --rm -it node:12-alpine sh + // npm init --force + // npm install --save promise jquery + // npm ls | grep -E -o "\S+@\S+" | awk -F@ 'NR>0 {printf("{\""$1"\", \""$2"\"},\n")}' + want: packagejson.Package{ + Package: ftypes.Package{ + ID: "bootstrap@5.0.2", + Name: "bootstrap", + Version: "5.0.2", + Licenses: []string{"MIT"}, + }, + Dependencies: map[string]string{ + "js-tokens": "^4.0.0", + }, + OptionalDependencies: map[string]string{ + "colors": "^1.4.0", + }, + DevDependencies: map[string]string{ + "@babel/cli": "^7.14.5", + "@babel/core": "^7.14.6", + }, + Workspaces: []string{ + "packages/*", + "backend", + }, + }, + }, + { + name: "happy path - legacy license", + inputFile: "testdata/legacy_package.json", + want: packagejson.Package{ + Package: ftypes.Package{ + ID: "angular@4.1.2", + Name: "angular", + Version: "4.1.2", + Licenses: []string{"ISC"}, + }, + Dependencies: make(map[string]string), + DevDependencies: map[string]string{ + "@babel/cli": "^7.14.5", + "@babel/core": "^7.14.6", + }, + }, + }, + { + name: "happy path - version doesn't exist", + inputFile: "testdata/without_version_package.json", + want: packagejson.Package{ + Package: ftypes.Package{ + ID: "", + Name: "angular", + }, + }, + }, + { + name: "happy path - workspace as struct", + inputFile: "testdata/workspace_as_map_package.json", + want: packagejson.Package{ + Package: ftypes.Package{ + ID: "example@1.0.0", + Name: "example", + Version: "1.0.0", + }, + Workspaces: []string{ + "packages/*", + }, + }, + }, + { + name: "invalid package name", + inputFile: "testdata/invalid_name.json", + wantErr: "Name can only contain URL-friendly characters", + }, + { + name: "sad path", + inputFile: "testdata/invalid_package.json", + + // docker run --name composer --rm -it node:12-alpine sh + // npm init --force + // npm install --save promise jquery + // npm ls | grep -E -o "\S+@\S+" | awk -F@ 'NR>0 {printf("{\""$1"\", \""$2"\"},\n")}' + wantErr: "JSON decode error", + }, + { + name: "without name and version", + inputFile: "testdata/without_name_and_version_package.json", + want: packagejson.Package{ + Package: ftypes.Package{ + Licenses: []string{"MIT"}, + }, + }, + }, + } + + for _, v := range vectors { + t.Run(v.name, func(t *testing.T) { + f, err := os.Open(v.inputFile) + require.NoError(t, err) + defer f.Close() + + got, err := packagejson.NewParser().Parse(f) + if v.wantErr != "" { + assert.ErrorContains(t, err, v.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, v.want, got) + }) + } +} + +func TestIsValidName(t *testing.T) { + tests := []struct { + name string + want bool + }{ + { + name: "", + want: true, + }, + { + name: "test_package", + want: true, + }, + { + name: "test.package", + want: true, + }, + { + name: "test-package", + want: true, + }, + { + name: "@test/package", + want: true, + }, + { + name: "test@package", + want: false, + }, + { + name: "test?package", + want: false, + }, + { + name: "test/package", + want: false, + }, + { + name: "package/", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + valid := packagejson.IsValidName(tt.name) + require.Equal(t, tt.want, valid) + }) + } +} diff --git a/pkg/dependency/parser/nodejs/packagejson/testdata/invalid_name.json b/pkg/dependency/parser/nodejs/packagejson/testdata/invalid_name.json new file mode 100644 index 000000000000..f97f91d2c852 --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/testdata/invalid_name.json @@ -0,0 +1,11 @@ +{ + "name": "@invalid/packageName/", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "ISC" +} \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/packagejson/testdata/invalid_package.json b/pkg/dependency/parser/nodejs/packagejson/testdata/invalid_package.json new file mode 100644 index 000000000000..d2b086ecf43a --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/testdata/invalid_package.json @@ -0,0 +1,3 @@ +{ + "name" "bootstrap" +} \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/packagejson/testdata/legacy_package.json b/pkg/dependency/parser/nodejs/packagejson/testdata/legacy_package.json new file mode 100644 index 000000000000..6176081f1760 --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/testdata/legacy_package.json @@ -0,0 +1,136 @@ + +{ + "name": "angular", + "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", + "version": "4.1.2", + "config": { + "version_short": "5.0" + }, + "keywords": [ + "css", + "sass", + "mobile-first", + "responsive", + "front-end", + "framework", + "web" + ], + "homepage": "https://getbootstrap.com/", + "author": "The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)", + "contributors": [ + "Twitter, Inc." + ], + "scripts": { + "start": "npm-run-all --parallel watch docs-serve", + "bundlewatch": "bundlewatch --config .bundlewatch.config.json", + "css": "npm-run-all css-compile css-prefix css-rtl css-minify", + "css-compile": "sass --style expanded --source-map --embed-sources --no-error-css scss/:dist/css/", + "css-rtl": "cross-env NODE_ENV=RTL postcss --config build/postcss.config.js --dir \"dist/css\" --ext \".rtl.css\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*.rtl.css\"", + "css-lint": "npm-run-all --aggregate-output --continue-on-error --parallel css-lint-*", + "css-lint-stylelint": "stylelint \"**/*.{css,scss}\" --cache --cache-location .cache/.stylelintcache --rd", + "css-lint-vars": "fusv scss/ site/assets/scss/", + "css-minify": "npm-run-all --aggregate-output --parallel css-minify-*", + "css-minify-main": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*rtl*.css\"", + "css-minify-rtl": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*rtl.css\" \"!dist/css/*.min.css\"", + "css-prefix": "npm-run-all --aggregate-output --parallel css-prefix-*", + "css-prefix-main": "postcss --config build/postcss.config.js --replace \"dist/css/*.css\" \"!dist/css/*.rtl*.css\" \"!dist/css/*.min.css\"", + "css-prefix-examples": "postcss --config build/postcss.config.js --replace \"site/content/**/*.css\"", + "css-prefix-examples-rtl": "cross-env-shell NODE_ENV=RTL postcss --config build/postcss.config.js --dir \"site/content/docs/$npm_package_config_version_short/examples/\" --ext \".rtl.css\" --base \"site/content/docs/$npm_package_config_version_short/examples/\" \"site/content/docs/$npm_package_config_version_short/examples/{blog,carousel,dashboard,cheatsheet}/*.css\" \"!site/content/docs/$npm_package_config_version_short/examples/{blog,carousel,dashboard,cheatsheet}/*.rtl.css\"", + "js": "npm-run-all js-compile js-minify", + "js-compile": "npm-run-all --aggregate-output --parallel js-compile-*", + "js-compile-standalone": "rollup --environment BUNDLE:false --config build/rollup.config.js --sourcemap", + "js-compile-standalone-esm": "rollup --environment ESM:true,BUNDLE:false --config build/rollup.config.js --sourcemap", + "js-compile-bundle": "rollup --environment BUNDLE:true --config build/rollup.config.js --sourcemap", + "js-compile-plugins": "node build/build-plugins.js", + "js-lint": "eslint --cache --cache-location .cache/.eslintcache --report-unused-disable-directives ", + "js-minify": "npm-run-all --aggregate-output --parallel js-minify-*", + "js-minify-standalone": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.js.map,includeSources,url=bootstrap.min.js.map\" --output dist/js/bootstrap.min.js dist/js/bootstrap.js", + "js-minify-standalone-esm": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.esm.js.map,includeSources,url=bootstrap.esm.min.js.map\" --output dist/js/bootstrap.esm.min.js dist/js/bootstrap.esm.js", + "js-minify-bundle": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.bundle.js.map,includeSources,url=bootstrap.bundle.min.js.map\" --output dist/js/bootstrap.bundle.min.js dist/js/bootstrap.bundle.js", + "js-test": "npm-run-all --aggregate-output --parallel js-test-karma js-test-jquery js-test-integration-*", + "js-debug": "cross-env DEBUG=true npm run js-test-karma", + "js-test-karma": "karma start js/tests/karma.conf.js", + "js-test-integration-bundle": "rollup --config js/tests/integration/rollup.bundle.js", + "js-test-integration-modularity": "rollup --config js/tests/integration/rollup.bundle-modularity.js", + "js-test-cloud": "cross-env BROWSERSTACK=true npm run js-test-karma", + "js-test-jquery": "cross-env JQUERY=true npm run js-test-karma", + "lint": "npm-run-all --aggregate-output --continue-on-error --parallel js-lint css-lint lockfile-lint", + "docs": "npm-run-all docs-build docs-lint", + "docs-build": "hugo --cleanDestinationDir", + "docs-compile": "npm run docs-build", + "docs-linkinator": "linkinator _site --recurse --skip \"^(?!http://localhost)\" --verbosity error", + "docs-vnu": "node build/vnu-jar.js", + "docs-lint": "npm-run-all --aggregate-output --parallel docs-vnu docs-linkinator", + "docs-serve": "hugo server --port 9001 --disableFastRender", + "docs-serve-only": "npx sirv-cli _site --port 9001", + "lockfile-lint": "lockfile-lint --allowed-hosts npm --allowed-schemes https: --empty-hostname false --type npm --path package-lock.json", + "update-deps": "ncu -u -x karma-browserstack-launcher,terser && npm update && echo Manually update site/assets/js/vendor", + "release": "npm-run-all dist release-sri docs-build release-zip*", + "release-sri": "node build/generate-sri.js", + "release-version": "node build/change-version.js", + "release-zip": "cross-env-shell \"rm -rf bootstrap-$npm_package_version-dist && cp -r dist/ bootstrap-$npm_package_version-dist && zip -r9 bootstrap-$npm_package_version-dist.zip bootstrap-$npm_package_version-dist && rm -rf bootstrap-$npm_package_version-dist\"", + "release-zip-examples": "node build/zip-examples.js", + "dist": "npm-run-all --aggregate-output --parallel css js", + "test": "npm-run-all lint dist js-test docs-build docs-lint", + "netlify": "cross-env-shell HUGO_BASEURL=$DEPLOY_PRIME_URL npm-run-all dist release-sri docs-build", + "watch": "npm-run-all --parallel watch-*", + "watch-css-main": "nodemon --watch scss/ --ext scss --exec \"npm-run-all css-lint css-compile css-prefix\"", + "watch-css-dist": "nodemon --watch dist/css/ --ext css --ignore \"dist/css/*.rtl.*\" --exec \"npm run css-rtl\"", + "watch-css-docs": "nodemon --watch site/assets/scss/ --ext scss --exec \"npm run css-lint\"", + "watch-js-main": "nodemon --watch js/src/ --ext js --exec \"npm-run-all js-lint js-compile\"", + "watch-js-docs": "nodemon --watch site/assets/js/ --ext js --exec \"npm run js-lint\"" + }, + "style": "dist/css/bootstrap.css", + "sass": "scss/bootstrap.scss", + "main": "dist/js/bootstrap.js", + "module": "dist/js/bootstrap.esm.js", + "repository": { + "type": "git", + "url": "git+https://github.com/twbs/bootstrap.git" + }, + "bugs": { + "url": "https://github.com/twbs/bootstrap/issues" + }, + "license": { + "type" : "ISC", + "url" : "https://opensource.org/licenses/ISC" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "dependencies": {}, + "peerDependencies": { + "@popperjs/core": "^2.9.2" + }, + "devDependencies": { + "@babel/cli": "^7.14.5", + "@babel/core": "^7.14.6" + }, + "files": [ + "dist/{css,js}/*.{css,js,map}", + "js/{src,dist}/**/*.{js,map}", + "scss/**/*.scss" + ], + "hugo-bin": { + "buildTags": "extended" + }, + "jspm": { + "registry": "npm", + "main": "js/bootstrap", + "directories": { + "lib": "dist" + }, + "shim": { + "js/bootstrap": { + "deps": [ + "@popperjs/core" + ] + } + }, + "dependencies": {}, + "peerDependencies": { + "@popperjs/core": "^2.9.2" + } + } +} diff --git a/pkg/dependency/parser/nodejs/packagejson/testdata/package.json b/pkg/dependency/parser/nodejs/packagejson/testdata/package.json new file mode 100644 index 000000000000..9e1e0858d4cc --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/testdata/package.json @@ -0,0 +1,142 @@ + +{ + "name": "bootstrap", + "description": "The most popular front-end framework for developing responsive, mobile first projects on the web.", + "version": "5.0.2", + "config": { + "version_short": "5.0" + }, + "keywords": [ + "css", + "sass", + "mobile-first", + "responsive", + "front-end", + "framework", + "web" + ], + "workspaces": [ + "packages/*", + "backend" + ], + "homepage": "https://getbootstrap.com/", + "author": "The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors)", + "contributors": [ + "Twitter, Inc." + ], + "scripts": { + "start": "npm-run-all --parallel watch docs-serve", + "bundlewatch": "bundlewatch --config .bundlewatch.config.json", + "css": "npm-run-all css-compile css-prefix css-rtl css-minify", + "css-compile": "sass --style expanded --source-map --embed-sources --no-error-css scss/:dist/css/", + "css-rtl": "cross-env NODE_ENV=RTL postcss --config build/postcss.config.js --dir \"dist/css\" --ext \".rtl.css\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*.rtl.css\"", + "css-lint": "npm-run-all --aggregate-output --continue-on-error --parallel css-lint-*", + "css-lint-stylelint": "stylelint \"**/*.{css,scss}\" --cache --cache-location .cache/.stylelintcache --rd", + "css-lint-vars": "fusv scss/ site/assets/scss/", + "css-minify": "npm-run-all --aggregate-output --parallel css-minify-*", + "css-minify-main": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*.css\" \"!dist/css/*.min.css\" \"!dist/css/*rtl*.css\"", + "css-minify-rtl": "cleancss -O1 --format breakWith=lf --with-rebase --source-map --source-map-inline-sources --output dist/css/ --batch --batch-suffix \".min\" \"dist/css/*rtl.css\" \"!dist/css/*.min.css\"", + "css-prefix": "npm-run-all --aggregate-output --parallel css-prefix-*", + "css-prefix-main": "postcss --config build/postcss.config.js --replace \"dist/css/*.css\" \"!dist/css/*.rtl*.css\" \"!dist/css/*.min.css\"", + "css-prefix-examples": "postcss --config build/postcss.config.js --replace \"site/content/**/*.css\"", + "css-prefix-examples-rtl": "cross-env-shell NODE_ENV=RTL postcss --config build/postcss.config.js --dir \"site/content/docs/$npm_package_config_version_short/examples/\" --ext \".rtl.css\" --base \"site/content/docs/$npm_package_config_version_short/examples/\" \"site/content/docs/$npm_package_config_version_short/examples/{blog,carousel,dashboard,cheatsheet}/*.css\" \"!site/content/docs/$npm_package_config_version_short/examples/{blog,carousel,dashboard,cheatsheet}/*.rtl.css\"", + "js": "npm-run-all js-compile js-minify", + "js-compile": "npm-run-all --aggregate-output --parallel js-compile-*", + "js-compile-standalone": "rollup --environment BUNDLE:false --config build/rollup.config.js --sourcemap", + "js-compile-standalone-esm": "rollup --environment ESM:true,BUNDLE:false --config build/rollup.config.js --sourcemap", + "js-compile-bundle": "rollup --environment BUNDLE:true --config build/rollup.config.js --sourcemap", + "js-compile-plugins": "node build/build-plugins.js", + "js-lint": "eslint --cache --cache-location .cache/.eslintcache --report-unused-disable-directives ", + "js-minify": "npm-run-all --aggregate-output --parallel js-minify-*", + "js-minify-standalone": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.js.map,includeSources,url=bootstrap.min.js.map\" --output dist/js/bootstrap.min.js dist/js/bootstrap.js", + "js-minify-standalone-esm": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.esm.js.map,includeSources,url=bootstrap.esm.min.js.map\" --output dist/js/bootstrap.esm.min.js dist/js/bootstrap.esm.js", + "js-minify-bundle": "terser --compress passes=2 --mangle --comments \"/^!/\" --source-map \"content=dist/js/bootstrap.bundle.js.map,includeSources,url=bootstrap.bundle.min.js.map\" --output dist/js/bootstrap.bundle.min.js dist/js/bootstrap.bundle.js", + "js-test": "npm-run-all --aggregate-output --parallel js-test-karma js-test-jquery js-test-integration-*", + "js-debug": "cross-env DEBUG=true npm run js-test-karma", + "js-test-karma": "karma start js/tests/karma.conf.js", + "js-test-integration-bundle": "rollup --config js/tests/integration/rollup.bundle.js", + "js-test-integration-modularity": "rollup --config js/tests/integration/rollup.bundle-modularity.js", + "js-test-cloud": "cross-env BROWSERSTACK=true npm run js-test-karma", + "js-test-jquery": "cross-env JQUERY=true npm run js-test-karma", + "lint": "npm-run-all --aggregate-output --continue-on-error --parallel js-lint css-lint lockfile-lint", + "docs": "npm-run-all docs-build docs-lint", + "docs-build": "hugo --cleanDestinationDir", + "docs-compile": "npm run docs-build", + "docs-linkinator": "linkinator _site --recurse --skip \"^(?!http://localhost)\" --verbosity error", + "docs-vnu": "node build/vnu-jar.js", + "docs-lint": "npm-run-all --aggregate-output --parallel docs-vnu docs-linkinator", + "docs-serve": "hugo server --port 9001 --disableFastRender", + "docs-serve-only": "npx sirv-cli _site --port 9001", + "lockfile-lint": "lockfile-lint --allowed-hosts npm --allowed-schemes https: --empty-hostname false --type npm --path package-lock.json", + "update-deps": "ncu -u -x karma-browserstack-launcher,terser && npm update && echo Manually update site/assets/js/vendor", + "release": "npm-run-all dist release-sri docs-build release-zip*", + "release-sri": "node build/generate-sri.js", + "release-version": "node build/change-version.js", + "release-zip": "cross-env-shell \"rm -rf bootstrap-$npm_package_version-dist && cp -r dist/ bootstrap-$npm_package_version-dist && zip -r9 bootstrap-$npm_package_version-dist.zip bootstrap-$npm_package_version-dist && rm -rf bootstrap-$npm_package_version-dist\"", + "release-zip-examples": "node build/zip-examples.js", + "dist": "npm-run-all --aggregate-output --parallel css js", + "test": "npm-run-all lint dist js-test docs-build docs-lint", + "netlify": "cross-env-shell HUGO_BASEURL=$DEPLOY_PRIME_URL npm-run-all dist release-sri docs-build", + "watch": "npm-run-all --parallel watch-*", + "watch-css-main": "nodemon --watch scss/ --ext scss --exec \"npm-run-all css-lint css-compile css-prefix\"", + "watch-css-dist": "nodemon --watch dist/css/ --ext css --ignore \"dist/css/*.rtl.*\" --exec \"npm run css-rtl\"", + "watch-css-docs": "nodemon --watch site/assets/scss/ --ext scss --exec \"npm run css-lint\"", + "watch-js-main": "nodemon --watch js/src/ --ext js --exec \"npm-run-all js-lint js-compile\"", + "watch-js-docs": "nodemon --watch site/assets/js/ --ext js --exec \"npm run js-lint\"" + }, + "style": "dist/css/bootstrap.css", + "sass": "scss/bootstrap.scss", + "main": "dist/js/bootstrap.js", + "module": "dist/js/bootstrap.esm.js", + "repository": { + "type": "git", + "url": "git+https://github.com/twbs/bootstrap.git" + }, + "bugs": { + "url": "https://github.com/twbs/bootstrap/issues" + }, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + }, + "dependencies": { + "js-tokens": "^4.0.0" + }, + "peerDependencies": { + "@popperjs/core": "^2.9.2" + }, + "optionalDependencies": { + "colors": "^1.4.0" + }, + "devDependencies": { + "@babel/cli": "^7.14.5", + "@babel/core": "^7.14.6" + }, + "files": [ + "dist/{css,js}/*.{css,js,map}", + "js/{src,dist}/**/*.{js,map}", + "scss/**/*.scss" + ], + "hugo-bin": { + "buildTags": "extended" + }, + "jspm": { + "registry": "npm", + "main": "js/bootstrap", + "directories": { + "lib": "dist" + }, + "shim": { + "js/bootstrap": { + "deps": [ + "@popperjs/core" + ] + } + }, + "dependencies": {}, + "peerDependencies": { + "@popperjs/core": "^2.9.2" + } + } +} diff --git a/pkg/dependency/parser/nodejs/packagejson/testdata/without_name_and_version_package.json b/pkg/dependency/parser/nodejs/packagejson/testdata/without_name_and_version_package.json new file mode 100644 index 000000000000..8f75dc6ee963 --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/testdata/without_name_and_version_package.json @@ -0,0 +1,3 @@ +{ + "license": "MIT" +} diff --git a/pkg/dependency/parser/nodejs/packagejson/testdata/without_version_package.json b/pkg/dependency/parser/nodejs/packagejson/testdata/without_version_package.json new file mode 100644 index 000000000000..2764f1f6f15d --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/testdata/without_version_package.json @@ -0,0 +1,4 @@ + +{ + "name": "angular" +} diff --git a/pkg/dependency/parser/nodejs/packagejson/testdata/workspace_as_map_package.json b/pkg/dependency/parser/nodejs/packagejson/testdata/workspace_as_map_package.json new file mode 100644 index 000000000000..21a198e8bc6e --- /dev/null +++ b/pkg/dependency/parser/nodejs/packagejson/testdata/workspace_as_map_package.json @@ -0,0 +1,8 @@ +{ + "name": "example", + "version": "1.0.0", + "workspaces": { + "packages": ["packages/*"], + "nohoist": ["**/react-native", "**/react-native/**"] + } +} diff --git a/pkg/dependency/parser/nodejs/pnpm/parse.go b/pkg/dependency/parser/nodejs/pnpm/parse.go new file mode 100644 index 000000000000..e37ccf6dffc3 --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/parse.go @@ -0,0 +1,384 @@ +package pnpm + +import ( + "context" + "fmt" + "sort" + "strconv" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/go-version/pkg/semver" + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("pnpm"), + } +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockFile LockFile + if err := yaml.NewDecoder(r).Decode(&lockFile); err != nil { + return nil, nil, xerrors.Errorf("decode error: %w", err) + } + + lockVer := p.parseLockfileVersion(lockFile) + if lockVer < 0 { + return nil, nil, nil + } + + var pkgs []ftypes.Package + var deps []ftypes.Dependency + if lockVer >= 9 { + pkgs, deps = p.parseV9(lockFile) + } else { + pkgs, deps = p.parse(lockVer, lockFile) + } + + sort.Sort(ftypes.Packages(pkgs)) + sort.Sort(ftypes.Dependencies(deps)) + return pkgs, deps, nil +} + +func (p *Parser) parse(lockVer float64, lockFile LockFile) ([]ftypes.Package, []ftypes.Dependency) { + var pkgs []ftypes.Package + var deps []ftypes.Dependency + + // Dependency path is a path to a dependency with a specific set of resolved subdependencies. + // cf. https://github.com/pnpm/spec/blob/ad27a225f81d9215becadfa540ef05fa4ad6dd60/dependency-path.md + for pkgKey, info := range lockFile.Packages { + if info.IsDev { + continue + } + + // Dependency name may be present in dependencyPath or Name field. Same for Version. + // e.g. packages installed from local directory or tarball + // cf. https://github.com/pnpm/spec/blob/274ff02de23376ad59773a9f25ecfedd03a41f64/lockfile/6.0.md#packagesdependencypathname + name := info.Name + version := info.Version + var ref string + + if name == "" { + name, version, ref = p.parsePnpmKey(string(pkgKey), lockVer) + version = p.parseVersion(string(pkgKey), version, lockVer) + } + // Create Trivy's internal package ID + pkgID := packageID(name, version) + + dependencies := make([]string, 0, len(info.Dependencies)) + for depName, depVer := range info.Dependencies { + dependencies = append(dependencies, packageID(depName, depVer)) + } + + pkgs = append(pkgs, ftypes.Package{ + ID: pkgID, + Name: name, + Version: version, + Relationship: lo.Ternary(isDirectPkg(name, lockFile.Dependencies), ftypes.RelationshipDirect, ftypes.RelationshipIndirect), + ExternalReferences: toExternalRefs(ref), + }) + + if len(dependencies) > 0 { + sort.Strings(dependencies) + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: dependencies, + }) + } + } + + return pkgs, deps +} + +// parseV9 parses pnpm-lock.yaml version 9.x format and returns packages and their dependencies. +// Version 9 introduced "snapshots" where each snapshot represents a package with its exact resolved dependencies. +func (p *Parser) parseV9(lockFile LockFile) ([]ftypes.Package, []ftypes.Dependency) { + lockVer := 9.0 + resolvedPkgs := make(map[SnapshotKey]ftypes.Package) + resolvedDeps := make(map[SnapshotKey]ftypes.Dependency) + + // Step 1: Extract direct dependencies from the "importers" section. + // The "importers" section contains the dependencies defined in package.json files. + // We need to identify which packages are direct dependencies (vs transitive) + // and which are development dependencies (vs production dependencies). + devDeps := make(map[string]string) // name -> version for dev dependencies + deps := make(map[string]string) // name -> version for production dependencies + for _, importer := range lockFile.Importers { + for n, v := range importer.DevDependencies { + devDeps[n] = v.Version + } + for n, v := range importer.Dependencies { + deps[n] = v.Version + } + } + + // Step 2: Process each snapshot to create package entries. + // Each snapshot represents a unique package installation with specific peer dependencies. + // The snapshotKey is the key that uniquely identifies this package instance, + // including any peer dependency information (e.g., "package@1.0.0(peer@2.0.0)"). + for snapshotKey, snapshot := range lockFile.Snapshots { + name, version, ref := p.parsePnpmKey(string(snapshotKey), lockVer) + // Clean and validate the version string (remove file: or http: prefixes if invalid) + parsedVer := p.parseVersion(string(snapshotKey), version, lockVer) + + // Try to get the exact version from the "packages" section if available. + // The "packages" section may contain more accurate version information + // for packages installed from non-standard sources (git, local files, etc.). + pkgKey := PackageKey(packageID(name, version)) + if pkgInfo, ok := lockFile.Packages[pkgKey]; ok && pkgInfo.Version != "" { + parsedVer = pkgInfo.Version + } + + // Step 3: Determine if this package is a direct or transitive dependency, + // and whether it's a development or production dependency. + // By default, assume it's a development dependency (will be corrected later if needed). + dev := true + relationship := ftypes.RelationshipIndirect // Assume transitive by default + + // Check if this package matches a direct dev dependency + if v, ok := devDeps[name]; ok && p.trimPeerDeps(v, lockVer) == version { + relationship = ftypes.RelationshipDirect + } + // Check if this package matches a direct production dependency + if v, ok := deps[name]; ok && p.trimPeerDeps(v, lockVer) == version { + relationship = ftypes.RelationshipDirect + dev = false // This is a production dependency, not a dev dependency + } + + // Create the package entry with all extracted information. + pkg := ftypes.Package{ + // ID is the full snapshotKey which uniquely identifies this package instance + // including any peer dependency context. + ID: string(snapshotKey), + Name: name, + Version: parsedVer, + Relationship: relationship, + Dev: dev, + ExternalReferences: toExternalRefs(ref), + } + resolvedPkgs[snapshotKey] = pkg + + // Step 4: Build the dependency graph by recording what this package depends on. + var dependsOn []string // List of snapshot keys this package depends on + for depName, depVer := range lo.Assign(snapshot.OptionalDependencies, snapshot.Dependencies) { + normalizedDepVer := p.trimPeerDeps(depVer, lockVer) + // Only include dependencies that are actually installed (exist in "packages" section). + if _, ok := lockFile.Packages[PackageKey(packageID(depName, normalizedDepVer))]; ok { + // Use the original name/version string (with peer deps) to build the snapshot key correctly. + dependsOn = append(dependsOn, packageID(depName, depVer)) + } + } + if len(dependsOn) > 0 { + resolvedDeps[snapshotKey] = ftypes.Dependency{ + ID: string(snapshotKey), + DependsOn: dependsOn, + } + } + } + + // Step 5: Propagate the "production" status to all transitive dependencies. + // If a package is a production dependency (Dev=false), all packages it depends on + // should also be marked as production dependencies, even if they were initially + // marked as dev dependencies. This ensures we correctly identify which packages + // are needed for production vs only for development. + visited := set.New[SnapshotKey]() + for _, pkg := range resolvedPkgs { + if !pkg.Dev { // If this is a production dependency + // Recursively mark this package and all its dependencies as production + p.markRootPkgs(SnapshotKey(pkg.ID), resolvedPkgs, resolvedDeps, visited) + } + } + + return lo.Values(resolvedPkgs), lo.Values(resolvedDeps) +} + +// markRootPkgs recursively marks a package and all its dependencies as production dependencies. +// This is used to propagate the production status from direct production dependencies +// to all their transitive dependencies, ensuring that any package required for production +// is correctly identified, even if it's also listed as a dev dependency elsewhere. +func (p *Parser) markRootPkgs(id SnapshotKey, pkgs map[SnapshotKey]ftypes.Package, deps map[SnapshotKey]ftypes.Dependency, visited set.Set[SnapshotKey]) { + // Avoid infinite recursion in case of circular dependencies + if visited.Contains(id) { + return + } + // Get the package; skip if not found + pkg, ok := pkgs[id] + if !ok { + return + } + + // Mark this package as a production dependency + pkg.Dev = false + pkgs[id] = pkg + visited.Append(id) // Track that we've processed this package + + // Recursively process all dependencies of this package + for _, depID := range deps[id].DependsOn { + p.markRootPkgs(SnapshotKey(depID), pkgs, deps, visited) + } +} + +func (p *Parser) parseLockfileVersion(lockFile LockFile) float64 { + switch v := lockFile.LockfileVersion.(type) { + // v5 + case float64: + return v + // v6+ + case string: + lockVer, err := strconv.ParseFloat(v, 64) + if err != nil { + p.logger.Debug("Unable to convert the lock file version to float", log.Err(err)) + return -1 + } + return lockVer + default: + p.logger.Debug("Unknown type for the lock file version", + log.Any("version", lockFile.LockfileVersion)) + return -1 + } +} + +// parsePnpmKey parses a pnpm package key (either PackageKey or SnapshotKey) +// and extracts the package name, version, and optional registry reference. +// The key format varies between pnpm versions: +// - v5: "registry.npmjs.org/@babel/generator/7.21.9" +// - v6+: "@babel/generator@7.21.9" +// - v9+: "@babel/generator@7.21.9(peer@1.0.0)" (SnapshotKey with peers) +func (p *Parser) parsePnpmKey(pnpmKey string, lockVer float64) (string, string, string) { + dPath, nonDefaultRegistry := p.trimRegistry(pnpmKey, lockVer) + + var scope string + scope, dPath = p.separateScope(dPath) + + var name string + name, dPath = p.separateName(dPath, lockVer) + + // add scope to pkg name + if scope != "" { + name = fmt.Sprintf("%s/%s", scope, name) + } + + ver := p.trimPeerDeps(dPath, lockVer) + + return name, ver, lo.Ternary(nonDefaultRegistry, pnpmKey, "") +} + +// trimRegistry trims registry (or `/` prefix) from a pnpm key. +// It returns true if non-default registry has been trimmed. +// e.g. +// - "registry.npmjs.org/lodash/4.17.10" => "lodash/4.17.10", false +// - "registry.npmjs.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9", false +// - "private.npm.org/@babel/generator/7.21.9" => "@babel/generator/7.21.9", true +// - "/lodash/4.17.10" => "lodash/4.17.10", false +// - "/asap@2.0.6" => "asap@2.0.6", false +func (p *Parser) trimRegistry(pnpmKey string, lockVer float64) (string, bool) { + var nonDefaultRegistry bool + // lock file v9 doesn't use registry prefix + if lockVer < 9 { + var registry string + registry, pnpmKey, _ = strings.Cut(pnpmKey, "/") + if registry != "" && registry != "registry.npmjs.org" { + nonDefaultRegistry = true + } + } + return pnpmKey, nonDefaultRegistry +} + +// separateScope separates the scope (if set) from the rest of the pnpm key. +// e.g. +// - v5: "@babel/generator/7.21.9" => {"babel", "generator/7.21.9"} +// - v6+: "@babel/helper-annotate-as-pure@7.18.6" => {"babel", "helper-annotate-as-pure@7.18.6"} +func (p *Parser) separateScope(pnpmKey string) (string, string) { + var scope string + if strings.HasPrefix(pnpmKey, "@") { + scope, pnpmKey, _ = strings.Cut(pnpmKey, "/") + } + return scope, pnpmKey +} + +// separateName separates package name and version from a pnpm key. +// e.g. +// - v5: "generator/7.21.9" => {"generator", "7.21.9"} +// - v6+: "7.21.5(@babel/core@7.20.7)" => "7.21.5" +// +// for v9+ version can be filePath or link: +// - "package1@file:package1:" +// - "is-negative@https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5" +// +// Also version can contain peer deps: +// - "debug@4.3.4(supports-color@8.1.1)" +func (p *Parser) separateName(pnpmKey string, lockVer float64) (string, string) { + sep := "@" + if lockVer < 6 { + sep = "/" + } + name, version, _ := strings.Cut(pnpmKey, sep) + return name, version +} + +// trimPeerDeps removes peer dependency suffixes from a version string. +// e.g. +// - v5: "7.21.5_@babel+core@7.21.8" => "7.21.5" +// - v6+: "7.21.5(@babel/core@7.20.7)" => "7.21.5" +func (p *Parser) trimPeerDeps(version string, lockVer float64) string { + sep := "(" + if lockVer < 6 { + sep = "_" + } + v, _, _ := strings.Cut(version, sep) + return v +} + +// parseVersion parses version. +// v9 can use filePath or link as version - we need to clear these versions. +// e.g. +// - "package1@file:package1:" +// - "is-negative@https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5" +// +// Other versions should be semver valid. +func (p *Parser) parseVersion(depPath, ver string, lockVer float64) string { + if lockVer < 9 && (strings.HasPrefix(ver, "file:") || strings.HasPrefix(ver, "http")) { + return "" + } + if _, err := semver.Parse(ver); err != nil { + p.logger.Debug("Skip non-semver package", log.String("pkg_path", depPath), + log.String("version", ver), log.Err(err)) + return "" + } + + return ver +} + +func isDirectPkg(name string, directDeps map[string]any) bool { + _, ok := directDeps[name] + return ok +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.Pnpm, name, version) +} + +func toExternalRefs(ref string) []ftypes.ExternalRef { + if ref == "" { + return nil + } + return []ftypes.ExternalRef{ + { + Type: ftypes.RefVCS, + URL: ref, + }, + } +} diff --git a/pkg/dependency/parser/nodejs/pnpm/parse_test.go b/pkg/dependency/parser/nodejs/pnpm/parse_test.go new file mode 100644 index 000000000000..db81d38ae109 --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/parse_test.go @@ -0,0 +1,294 @@ +package pnpm + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + file string // Test input file + want []ftypes.Package + wantDeps []ftypes.Dependency + }{ + { + name: "normal", + file: "testdata/pnpm-lock_normal.yaml", + want: pnpmNormal, + wantDeps: pnpmNormalDeps, + }, + { + name: "with dev deps", + file: "testdata/pnpm-lock_with_dev.yaml", + want: pnpmWithDev, + wantDeps: pnpmWithDevDeps, + }, + { + name: "many", + file: "testdata/pnpm-lock_many.yaml", + want: pnpmMany, + wantDeps: pnpmManyDeps, + }, + { + name: "archives", + file: "testdata/pnpm-lock_archives.yaml", + want: pnpmArchives, + wantDeps: pnpmArchivesDeps, + }, + { + name: "v6", + file: "testdata/pnpm-lock_v6.yaml", + want: pnpmV6, + wantDeps: pnpmV6Deps, + }, + { + name: "v6 with dev deps", + file: "testdata/pnpm-lock_v6_with_dev.yaml", + want: pnpmV6WithDev, + wantDeps: pnpmV6WithDevDeps, + }, + { + name: "v9", + file: "testdata/pnpm-lock_v9.yaml", + want: pnpmV9, + wantDeps: pnpmV9Deps, + }, + { + name: "v9 with cyclic dependencies import", + file: "testdata/pnpm-lock_v9_cyclic_import.yaml", + want: pnpmV9CyclicImport, + wantDeps: pnpmV9CyclicImportDeps, + }, + { + name: "v9 with same version and different peers for dependency", + file: "testdata/pnpm-lock_v9_same-vers-diff-peers.yaml", + want: pnpmV9SameVersDiffPeers, + wantDeps: pnpmV9SameVersDiffPeersDeps, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + + got, deps, err := NewParser().Parse(t.Context(), f) + require.NoError(t, err) + + sort.Sort(ftypes.Packages(got)) + sort.Sort(ftypes.Packages(tt.want)) + require.Equal(t, tt.want, got) + + if tt.wantDeps != nil { + sort.Sort(ftypes.Dependencies(deps)) + sort.Sort(ftypes.Dependencies(tt.wantDeps)) + for _, dep := range deps { + sort.Strings(dep.DependsOn) + } + for _, dep := range tt.wantDeps { + sort.Strings(dep.DependsOn) + } + require.Equal(t, tt.wantDeps, deps) + } + }) + } +} + +func Test_parsePnpmKey(t *testing.T) { + tests := []struct { + name string + lockFileVer float64 + pkg string + wantName string + wantVersion string + wantRef string + }{ + { + name: "v5 - relative path", + lockFileVer: 5.0, + pkg: "/lodash/4.17.10", + wantName: "lodash", + wantVersion: "4.17.10", + }, + { + name: "v5 - registry", + lockFileVer: 5.0, + pkg: "registry.npmjs.org/lodash/4.17.10", + wantName: "lodash", + wantVersion: "4.17.10", + }, + { + name: "v5 - non-default registry", + lockFileVer: 5.0, + pkg: "private.npmjs.org/lodash/4.17.10", + wantName: "lodash", + wantVersion: "4.17.10", + wantRef: "private.npmjs.org/lodash/4.17.10", + }, + { + name: "v5 - relative path with slash", + lockFileVer: 5.0, + pkg: "/@babel/generator/7.21.9", + wantName: "@babel/generator", + wantVersion: "7.21.9", + }, + { + name: "v5 - registry path with slash", + lockFileVer: 5.0, + pkg: "registry.npmjs.org/@babel/generator/7.21.9", + wantName: "@babel/generator", + wantVersion: "7.21.9", + }, + { + name: "v5 - relative path with slash and peer deps", + lockFileVer: 5.0, + pkg: "/@babel/helper-compilation-targets/7.21.5_@babel+core@7.21.8", + wantName: "@babel/helper-compilation-targets", + wantVersion: "7.21.5", + }, + { + name: "v5 - relative path with underline and peer deps", + lockFileVer: 5.0, + pkg: "/lodash._baseclone/4.5.7_@babel+core@7.21.8", + wantName: "lodash._baseclone", + wantVersion: "4.5.7", + }, + { + name: "v5 - registry with slash and peer deps", + lockFileVer: 5.0, + pkg: "registry.npmjs.org/@babel/helper-compilation-targets/7.21.5_@babel+core@7.21.8", + wantName: "@babel/helper-compilation-targets", + wantVersion: "7.21.5", + }, + { + name: "v6 - relative path", + lockFileVer: 6.0, + pkg: "/update-browserslist-db@1.0.11", + wantName: "update-browserslist-db", + wantVersion: "1.0.11", + }, + { + name: "v6 - registry", + lockFileVer: 6.0, + pkg: "registry.npmjs.org/lodash@4.17.10", + wantName: "lodash", + wantVersion: "4.17.10", + }, + { + name: "v6 - non-default registry", + lockFileVer: 6.0, + pkg: "private.npmjs.org/lodash@4.17.10", + wantName: "lodash", + wantVersion: "4.17.10", + wantRef: "private.npmjs.org/lodash@4.17.10", + }, + { + name: "v6 - relative path with slash", + lockFileVer: 6.0, + pkg: "/@babel/helper-annotate-as-pure@7.18.6", + wantName: "@babel/helper-annotate-as-pure", + wantVersion: "7.18.6", + }, + { + name: "v6 - registry with slash", + lockFileVer: 6.0, + pkg: "registry.npmjs.org/@babel/helper-annotate-as-pure@7.18.6", + wantName: "@babel/helper-annotate-as-pure", + wantVersion: "7.18.6", + }, + { + name: "v6 - relative path with slash and peer deps", + lockFileVer: 6.0, + pkg: "/@babel/helper-compilation-targets@7.21.5(@babel/core@7.20.7)", + wantName: "@babel/helper-compilation-targets", + wantVersion: "7.21.5", + }, + { + name: "v6 - registry with slash and peer deps", + lockFileVer: 6.0, + pkg: "registry.npmjs.org/@babel/helper-compilation-targets@7.21.5(@babel/core@7.20.7)", + wantName: "@babel/helper-compilation-targets", + wantVersion: "7.21.5", + }, + { + name: "v9 - scope and peer deps", + lockFileVer: 9.0, + pkg: "@babel/helper-compilation-targets@7.21.5(@babel/core@7.20.7)", + wantName: "@babel/helper-compilation-targets", + wantVersion: "7.21.5", + }, + { + name: "v9 - filePath as version", + lockFileVer: 9.0, + pkg: "lodash@file:foo/bar/lodash.tgz", + wantName: "lodash", + wantVersion: "file:foo/bar/lodash.tgz", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := NewParser() + gotName, gotVersion, gotRef := p.parsePnpmKey(tt.pkg, tt.lockFileVer) + require.Equal(t, tt.wantName, gotName) + require.Equal(t, tt.wantVersion, gotVersion) + require.Equal(t, tt.wantRef, gotRef) + }) + + } +} + +func Test_parseVersion(t *testing.T) { + tests := []struct { + name string + ver string + lockVer float64 + wantVer string + }{ + { + name: "happy path", + ver: "0.0.1", + lockVer: 5.0, + wantVer: "0.0.1", + }, + { + name: "v6 version is file", + ver: "file:foo/bar/lodash.tgz", + lockVer: 6.0, + wantVer: "", + }, + { + name: "v9 version is file", + ver: "file:foo/bar/lodash.tgz", + lockVer: 9.0, + wantVer: "", + }, + { + name: "v6 version is url", + ver: "https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5", + lockVer: 6.0, + wantVer: "", + }, + { + name: "v9 version is url", + ver: "https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5", + lockVer: 9.0, + wantVer: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := NewParser() + gotVer := p.parseVersion("depPath", tt.ver, tt.lockVer) + require.Equal(t, tt.wantVer, gotVer) + }) + + } +} diff --git a/pkg/dependency/parser/nodejs/pnpm/parse_testcase.go b/pkg/dependency/parser/nodejs/pnpm/parse_testcase.go new file mode 100644 index 000000000000..436417ed1aee --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/parse_testcase.go @@ -0,0 +1,1072 @@ +package pnpm + +import ( + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + // docker run --name node --rm -it node:16-alpine sh + // npm install -g pnpm + // pnpm add promise jquery + // pnpm list --prod --depth 10 | grep -E -o "\S+\s+[0-9]+(\.[0-9]+)+$" | awk '{printf("{ID: \""$1"@"$2"\", Name: \""$1"\", Version: \""$2"\", Relationship: ftypes.RelationshipIndirect},\n")}' | sort -u + pnpmNormal = []ftypes.Package{ + { + ID: "asap@2.0.6", + Name: "asap", + Version: "2.0.6", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "jquery@3.6.0", + Name: "jquery", + Version: "3.6.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "promise@8.1.0", + Name: "promise", + Version: "8.1.0", + Relationship: ftypes.RelationshipDirect, + }, + } + pnpmNormalDeps = []ftypes.Dependency{ + { + ID: "promise@8.1.0", + DependsOn: []string{"asap@2.0.6"}, + }, + } + + // docker run --name node --rm -it node:16-alpine sh + // npm install -g pnpm + // pnpm add react redux + // pnpm add -D mocha + // pnpm list --prod --depth 10 | grep -E -o "\S+\s+[0-9]+(\.[0-9]+)+$" | awk '{printf("{ID: \""$1"@"$2"\", Name: \""$1"\", Version: \""$2"\", Relationship: ftypes.RelationshipIndirect},\n")}' | sort -u + pnpmWithDev = []ftypes.Package{ + { + ID: "@babel/runtime@7.18.3", + Name: "@babel/runtime", + Version: "7.18.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "js-tokens@4.0.0", + Name: "js-tokens", + Version: "4.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "loose-envify@1.4.0", + Name: "loose-envify", + Version: "1.4.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "react@18.1.0", + Name: "react", + Version: "18.1.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "redux@4.2.0", + Name: "redux", + Version: "4.2.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "regenerator-runtime@0.13.9", + Name: "regenerator-runtime", + Version: "0.13.9", + Relationship: ftypes.RelationshipIndirect, + }, + } + pnpmWithDevDeps = []ftypes.Dependency{ + { + ID: "@babel/runtime@7.18.3", + DependsOn: []string{"regenerator-runtime@0.13.9"}, + }, + { + ID: "loose-envify@1.4.0", + DependsOn: []string{"js-tokens@4.0.0"}, + }, + { + ID: "react@18.1.0", + DependsOn: []string{"loose-envify@1.4.0"}, + }, + { + ID: "redux@4.2.0", + DependsOn: []string{"@babel/runtime@7.18.3"}, + }, + } + + // docker run --name node --rm -it node:16-alpine sh + // npm install -g pnpm + // pnpm add react redux lodash request chalk commander + // pnpm add -D mocha + // pnpm list --prod --depth 10 | grep -E -o "\S+\s+[0-9]+(\.[0-9]+)+$" | awk '{printf("{ID: \""$1"@"$2"\", Name: \""$1"\", Version: \""$2"\", Relationship: ftypes.RelationshipIndirect},\n")}' | sort -u + pnpmMany = []ftypes.Package{ + { + ID: "@babel/runtime@7.18.3", + Name: "@babel/runtime", + Version: "7.18.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "ajv@6.12.6", + Name: "ajv", + Version: "6.12.6", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "assert-plus@1.0.0", + Name: "assert-plus", + Version: "1.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "asynckit@0.4.0", + Name: "asynckit", + Version: "0.4.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "aws-sign2@0.7.0", + Name: "aws-sign2", + Version: "0.7.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "aws4@1.11.0", + Name: "aws4", + Version: "1.11.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "bcrypt-pbkdf@1.0.2", + Name: "bcrypt-pbkdf", + Version: "1.0.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "caseless@0.12.0", + Name: "caseless", + Version: "0.12.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "chalk@5.0.1", + Name: "chalk", + Version: "5.0.1", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "combined-stream@1.0.8", + Name: "combined-stream", + Version: "1.0.8", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "commander@9.3.0", + Name: "commander", + Version: "9.3.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "core-util-is@1.0.2", + Name: "core-util-is", + Version: "1.0.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "dashdash@1.14.1", + Name: "dashdash", + Version: "1.14.1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "delayed-stream@1.0.0", + Name: "delayed-stream", + Version: "1.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "ecc-jsbn@0.1.2", + Name: "ecc-jsbn", + Version: "0.1.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "extend@3.0.2", + Name: "extend", + Version: "3.0.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "extsprintf@1.3.0", + Name: "extsprintf", + Version: "1.3.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "fast-deep-equal@3.1.3", + Name: "fast-deep-equal", + Version: "3.1.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "fast-json-stable-stringify@2.1.0", + Name: "fast-json-stable-stringify", + Version: "2.1.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "forever-agent@0.6.1", + Name: "forever-agent", + Version: "0.6.1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "form-data@2.3.3", + Name: "form-data", + Version: "2.3.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "getpass@0.1.7", + Name: "getpass", + Version: "0.1.7", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "har-schema@2.0.0", + Name: "har-schema", + Version: "2.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "har-validator@5.1.5", + Name: "har-validator", + Version: "5.1.5", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "http-signature@1.2.0", + Name: "http-signature", + Version: "1.2.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "is-typedarray@1.0.0", + Name: "is-typedarray", + Version: "1.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "isstream@0.1.2", + Name: "isstream", + Version: "0.1.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "js-tokens@4.0.0", + Name: "js-tokens", + Version: "4.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "jsbn@0.1.1", + Name: "jsbn", + Version: "0.1.1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "json-schema-traverse@0.4.1", + Name: "json-schema-traverse", + Version: "0.4.1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "json-schema@0.4.0", + Name: "json-schema", + Version: "0.4.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "json-stringify-safe@5.0.1", + Name: "json-stringify-safe", + Version: "5.0.1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "jsprim@1.4.2", + Name: "jsprim", + Version: "1.4.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "lodash@4.17.21", + Name: "lodash", + Version: "4.17.21", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "loose-envify@1.4.0", + Name: "loose-envify", + Version: "1.4.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "mime-db@1.52.0", + Name: "mime-db", + Version: "1.52.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "mime-types@2.1.35", + Name: "mime-types", + Version: "2.1.35", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "oauth-sign@0.9.0", + Name: "oauth-sign", + Version: "0.9.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "performance-now@2.1.0", + Name: "performance-now", + Version: "2.1.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "psl@1.8.0", + Name: "psl", + Version: "1.8.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "punycode@2.1.1", + Name: "punycode", + Version: "2.1.1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "qs@6.5.3", + Name: "qs", + Version: "6.5.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "react@18.1.0", + Name: "react", + Version: "18.1.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "redux@4.2.0", + Name: "redux", + Version: "4.2.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "regenerator-runtime@0.13.9", + Name: "regenerator-runtime", + Version: "0.13.9", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "request@2.88.2", + Name: "request", + Version: "2.88.2", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "safe-buffer@5.2.1", + Name: "safe-buffer", + Version: "5.2.1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "safer-buffer@2.1.2", + Name: "safer-buffer", + Version: "2.1.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "sshpk@1.17.0", + Name: "sshpk", + Version: "1.17.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "tough-cookie@2.5.0", + Name: "tough-cookie", + Version: "2.5.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "tunnel-agent@0.6.0", + Name: "tunnel-agent", + Version: "0.6.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "tweetnacl@0.14.5", + Name: "tweetnacl", + Version: "0.14.5", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "uri-js@4.4.1", + Name: "uri-js", + Version: "4.4.1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "uuid@3.4.0", + Name: "uuid", + Version: "3.4.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "verror@1.10.0", + Name: "verror", + Version: "1.10.0", + Relationship: ftypes.RelationshipIndirect, + }, + } + pnpmManyDeps = []ftypes.Dependency{ + { + ID: "@babel/runtime@7.18.3", + DependsOn: []string{"regenerator-runtime@0.13.9"}, + }, + { + ID: "ajv@6.12.6", + DependsOn: []string{ + "fast-deep-equal@3.1.3", + "fast-json-stable-stringify@2.1.0", + "json-schema-traverse@0.4.1", + "uri-js@4.4.1", + }, + }, + { + ID: "asn1@0.2.6", + DependsOn: []string{"safer-buffer@2.1.2"}, + }, + { + ID: "bcrypt-pbkdf@1.0.2", + DependsOn: []string{"tweetnacl@0.14.5"}, + }, + { + ID: "combined-stream@1.0.8", + DependsOn: []string{"delayed-stream@1.0.0"}, + }, + { + ID: "dashdash@1.14.1", + DependsOn: []string{"assert-plus@1.0.0"}, + }, + { + ID: "ecc-jsbn@0.1.2", + DependsOn: []string{ + "jsbn@0.1.1", + "safer-buffer@2.1.2", + }, + }, + { + ID: "form-data@2.3.3", + DependsOn: []string{ + "asynckit@0.4.0", + "combined-stream@1.0.8", + "mime-types@2.1.35", + }, + }, + { + ID: "getpass@0.1.7", + DependsOn: []string{"assert-plus@1.0.0"}, + }, + { + ID: "har-validator@5.1.5", + DependsOn: []string{ + "ajv@6.12.6", + "har-schema@2.0.0", + }, + }, + { + ID: "http-signature@1.2.0", + DependsOn: []string{ + "assert-plus@1.0.0", + "jsprim@1.4.2", + "sshpk@1.17.0", + }, + }, + { + ID: "jsprim@1.4.2", + DependsOn: []string{ + "assert-plus@1.0.0", + "extsprintf@1.3.0", + "json-schema@0.4.0", + "verror@1.10.0", + }, + }, + { + ID: "loose-envify@1.4.0", + DependsOn: []string{"js-tokens@4.0.0"}, + }, + { + ID: "mime-types@2.1.35", + DependsOn: []string{"mime-db@1.52.0"}, + }, + { + ID: "react@18.1.0", + DependsOn: []string{"loose-envify@1.4.0"}, + }, + { + ID: "redux@4.2.0", + DependsOn: []string{"@babel/runtime@7.18.3"}, + }, + { + ID: "request@2.88.2", + DependsOn: []string{ + "aws-sign2@0.7.0", + "aws4@1.11.0", + "caseless@0.12.0", + "combined-stream@1.0.8", + "extend@3.0.2", + "forever-agent@0.6.1", + "form-data@2.3.3", + "har-validator@5.1.5", + "http-signature@1.2.0", + "is-typedarray@1.0.0", + "isstream@0.1.2", + "json-stringify-safe@5.0.1", + "mime-types@2.1.35", + "oauth-sign@0.9.0", + "performance-now@2.1.0", + "qs@6.5.3", + "safe-buffer@5.2.1", + "tough-cookie@2.5.0", + "tunnel-agent@0.6.0", + "uuid@3.4.0", + }, + }, + { + ID: "sshpk@1.17.0", + DependsOn: []string{ + "asn1@0.2.6", + "assert-plus@1.0.0", + "bcrypt-pbkdf@1.0.2", + "dashdash@1.14.1", + "ecc-jsbn@0.1.2", + "getpass@0.1.7", + "jsbn@0.1.1", + "safer-buffer@2.1.2", + "tweetnacl@0.14.5", + }, + }, + { + ID: "tough-cookie@2.5.0", + DependsOn: []string{ + "psl@1.8.0", + "punycode@2.1.1", + }, + }, + { + ID: "tunnel-agent@0.6.0", + DependsOn: []string{"safe-buffer@5.2.1"}, + }, + { + ID: "uri-js@4.4.1", + DependsOn: []string{"punycode@2.1.1"}, + }, + { + ID: "verror@1.10.0", + DependsOn: []string{ + "assert-plus@1.0.0", + "core-util-is@1.0.2", + "extsprintf@1.3.0", + }, + }, + } + + // docker run --name node --rm -it node@sha256:710a2c192ca426e03e4f3ec1869e5c29db855eb6969b74e6c50fd270ffccd3f1 sh + // npm install -g pnpm@8.5.1 + // mkdir /temp && cd /temp + // npm install lodash@4.17.21 + // cd ./node_modules/lodash/ + // npm pack + // mkdir -p /app/foo/bar && cd /app + // cp /temp/node_modules/lodash/lodash-4.17.21.tgz /app/foo/bar/lodash.tgz + // npm init -y + // npm install ./foo/bar/lodash.tgz + // mkdir package1 && cd package1 + // npm init -y + // npm install asynckit@0.4.0 + // cd .. + // npm install ./package1 + // pnpm update + // pnpm add https://github.com/debug-js/debug/tarball/4.3.4 + // pnpm add https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5 + // pnpm list --prod --depth 10 | grep -E -o "\S+\s+[0-9]+(\.[0-9]+)+$" | awk '{printf("{ID: \""$1"@"$2"\", Name: \""$1"\", Version: \""$2"\", Relationship: ftypes.RelationshipDirect},\n")}' | sort -u + // manually update `Indirect` fields + pnpmArchives = []ftypes.Package{ + { + ID: "asynckit@0.4.0", + Name: "asynckit", + Version: "0.4.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "debug@4.3.4", + Name: "debug", + Version: "4.3.4", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "is-negative@2.0.1", + Name: "is-negative", + Version: "2.0.1", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "lodash@4.17.21", + Name: "lodash", + Version: "4.17.21", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "ms@2.1.2", + Name: "ms", + Version: "2.1.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "package1@1.0.0", + Name: "package1", + Version: "1.0.0", + Relationship: ftypes.RelationshipDirect, + }, + } + + pnpmArchivesDeps = []ftypes.Dependency{ + { + ID: "debug@4.3.4", + DependsOn: []string{"ms@2.1.2"}, + }, + { + ID: "package1@1.0.0", + DependsOn: []string{"asynckit@0.4.0"}, + }, + } + + // docker run --name node --rm -it node@sha256:710a2c192ca426e03e4f3ec1869e5c29db855eb6969b74e6c50fd270ffccd3f1 sh + // npm install -g pnpm@8.5.1 + // pnpm add promise@8.1.0 jquery@3.6.0 + // pnpm list --prod --depth 10 | grep -E -o "\S+\s+[0-9]+(\.[0-9]+)+$" | awk '{printf("{ID: \""$1"@"$2"\", Name: \""$1"\", Version: \""$2"\", Relationship: ftypes.RelationshipIndirect},\n")}' | sort -u + pnpmV6 = pnpmNormal + pnpmV6Deps = pnpmNormalDeps + + // docker run --name node --rm -it node@sha256:710a2c192ca426e03e4f3ec1869e5c29db855eb6969b74e6c50fd270ffccd3f1 sh + // npm install -g pnpm@8.5.1 + // pnpm add react@18.1.0 redux@4.2.0 + // pnpm add -D mocha@10.0.0 + // pnpm list --prod --depth 10 | grep -E -o "\S+\s+[0-9]+(\.[0-9]+)+$" | awk '{printf("{ID: \""$1"@"$2"\", Name: \""$1"\", Version: \""$2"\", Relationship: ftypes.RelationshipIndirect},\n")}' | sort -u + pnpmV6WithDev = []ftypes.Package{ + { + ID: "@babel/runtime@7.22.3", + Name: "@babel/runtime", + Version: "7.22.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "js-tokens@4.0.0", + Name: "js-tokens", + Version: "4.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "loose-envify@1.4.0", + Name: "loose-envify", + Version: "1.4.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "react@18.1.0", + Name: "react", + Version: "18.1.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "redux@4.2.0", + Name: "redux", + Version: "4.2.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "regenerator-runtime@0.13.11", + Name: "regenerator-runtime", + Version: "0.13.11", + Relationship: ftypes.RelationshipIndirect, + }, + } + pnpmV6WithDevDeps = []ftypes.Dependency{ + { + ID: "@babel/runtime@7.22.3", + DependsOn: []string{"regenerator-runtime@0.13.11"}, + }, + { + ID: "loose-envify@1.4.0", + DependsOn: []string{"js-tokens@4.0.0"}, + }, + { + ID: "react@18.1.0", + DependsOn: []string{"loose-envify@1.4.0"}, + }, + { + ID: "redux@4.2.0", + DependsOn: []string{"@babel/runtime@7.22.3"}, + }, + } + + pnpmV9 = []ftypes.Package{ + { + ID: "@babel/helper-string-parser@7.24.1", + Name: "@babel/helper-string-parser", + Version: "7.24.1", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "asap@2.0.6", + Name: "asap", + Version: "2.0.6", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "asynckit@https://github.com/alexindigo/asynckit/tarball/v0.4.0", + Name: "asynckit", + Version: "0.4.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "await-sleep@0.0.1", + Name: "await-sleep", + Version: "0.0.1", + Dev: true, + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "debug@4.3.4(supports-color@8.1.1)", + Name: "debug", + Version: "4.3.4", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "debug@https://github.com/debug-js/debug/tarball/4.3.5", + Name: "debug", + Version: "4.3.5", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "ee-first@1.1.1", + Name: "ee-first", + Version: "1.1.1", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "encodeurl@1.0.2", + Name: "encodeurl", + Version: "1.0.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "escape-html@1.0.3", + Name: "escape-html", + Version: "1.0.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "finalhandler@1.1.1", + Name: "finalhandler", + Version: "1.1.1", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "is-negative@https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5", + Name: "is-negative", + Version: "2.0.1", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "jquery@3.6.0", + Name: "jquery", + Version: "3.6.0", + Dev: true, + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "lodash@file:foo/bar/lodash.tgz", + Name: "lodash", + Version: "4.17.21", + Dev: true, + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "ms@2.0.0", + Name: "ms", + Version: "2.0.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "ms@2.1.2", + Name: "ms", + Version: "2.1.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "on-finished@2.3.0", + Name: "on-finished", + Version: "2.3.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "package1@file:package1", + Name: "package1", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "parseurl@1.3.3", + Name: "parseurl", + Version: "1.3.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "promise@8.1.0", + Name: "promise", + Version: "8.1.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "sleep-utils@1.0.3", + Name: "sleep-utils", + Version: "1.0.3", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "statuses@1.4.0", + Name: "statuses", + Version: "1.4.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "unpipe@1.0.0", + Name: "unpipe", + Version: "1.0.0", + Relationship: ftypes.RelationshipIndirect, + }, + } + pnpmV9Deps = []ftypes.Dependency{ + { + ID: "debug@4.3.4(supports-color@8.1.1)", + DependsOn: []string{ + "ms@2.0.0", + }, + }, + { + ID: "debug@https://github.com/debug-js/debug/tarball/4.3.5", + DependsOn: []string{ + "ms@2.1.2", + }, + }, + { + ID: "finalhandler@1.1.1", + DependsOn: []string{ + "debug@4.3.4(supports-color@8.1.1)", + "encodeurl@1.0.2", + "escape-html@1.0.3", + "on-finished@2.3.0", + "parseurl@1.3.3", + "statuses@1.4.0", + "unpipe@1.0.0", + }, + }, + { + ID: "on-finished@2.3.0", + DependsOn: []string{ + "ee-first@1.1.1", + }, + }, + { + ID: "package1@file:package1", + DependsOn: []string{ + "asynckit@https://github.com/alexindigo/asynckit/tarball/v0.4.0", + }, + }, + { + ID: "promise@8.1.0", + DependsOn: []string{ + "asap@2.0.6", + }, + }, + } + + pnpmV9CyclicImport = []ftypes.Package{ + { + ID: "update-browserslist-db@1.0.16(browserslist@4.23.0)", + Name: "update-browserslist-db", + Version: "1.0.16", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "browserslist@4.23.0", + Name: "browserslist", + Version: "4.23.0", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "caniuse-lite@1.0.30001627", + Name: "caniuse-lite", + Version: "1.0.30001627", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "electron-to-chromium@1.4.789", + Name: "electron-to-chromium", + Version: "1.4.789", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "escalade@3.1.2", + Name: "escalade", + Version: "3.1.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "node-releases@2.0.14", + Name: "node-releases", + Version: "2.0.14", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "picocolors@1.0.1", + Name: "picocolors", + Version: "1.0.1", + Relationship: ftypes.RelationshipIndirect, + }, + } + pnpmV9CyclicImportDeps = []ftypes.Dependency{ + { + ID: "browserslist@4.23.0", + DependsOn: []string{ + "caniuse-lite@1.0.30001627", + "electron-to-chromium@1.4.789", + "node-releases@2.0.14", + "update-browserslist-db@1.0.16(browserslist@4.23.0)", + }, + }, + { + ID: "update-browserslist-db@1.0.16(browserslist@4.23.0)", + DependsOn: []string{ + "browserslist@4.23.0", + "escalade@3.1.2", + "picocolors@1.0.1", + }, + }, + } + + pnpmV9SameVersDiffPeers = []ftypes.Package{ + { + ID: "jest-config@30.0.3(@types/node@24.0.7)(babel-plugin-macros@3.1.0)", + Name: "jest-config", + Version: "30.0.3", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "jest-runner@27.5.1", + Name: "jest-runner", + Version: "27.5.1", + Dev: true, + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "jest-pnp-resolver@1.2.3(jest-resolve@27.5.1)", + Name: "jest-pnp-resolver", + Version: "1.2.3", + Dev: true, + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "jest-pnp-resolver@1.2.3(jest-resolve@30.0.2)", + Name: "jest-pnp-resolver", + Version: "1.2.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "jest-resolve@27.5.1", + Name: "jest-resolve", + Version: "27.5.1", + Dev: true, + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "jest-resolve@30.0.2", + Name: "jest-resolve", + Version: "30.0.2", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "jest-runner@30.0.3", + Name: "jest-runner", + Version: "30.0.3", + Relationship: ftypes.RelationshipIndirect, + }, + } + pnpmV9SameVersDiffPeersDeps = []ftypes.Dependency{ + { + ID: "jest-config@30.0.3(@types/node@24.0.7)(babel-plugin-macros@3.1.0)", + DependsOn: []string{ + "jest-runner@30.0.3", + }, + }, + { + ID: "jest-pnp-resolver@1.2.3(jest-resolve@27.5.1)", + DependsOn: []string{ + "jest-resolve@27.5.1", + }, + }, + { + ID: "jest-pnp-resolver@1.2.3(jest-resolve@30.0.2)", + DependsOn: []string{ + "jest-resolve@30.0.2", + }, + }, + { + ID: "jest-resolve@27.5.1", + DependsOn: []string{ + "jest-pnp-resolver@1.2.3(jest-resolve@27.5.1)", + }, + }, + { + ID: "jest-resolve@30.0.2", + DependsOn: []string{ + "jest-pnp-resolver@1.2.3(jest-resolve@30.0.2)", + }, + }, + { + ID: "jest-runner@27.5.1", + DependsOn: []string{ + "jest-pnp-resolver@1.2.3(jest-resolve@27.5.1)", + }, + }, + { + ID: "jest-runner@30.0.3", + DependsOn: []string{ + "jest-pnp-resolver@1.2.3(jest-resolve@30.0.2)", + }, + }, + } +) diff --git a/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_archives.yaml b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_archives.yaml new file mode 100644 index 000000000000..7d7ae2fc1d8a --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_archives.yaml @@ -0,0 +1,60 @@ +lockfileVersion: '6.0' + +dependencies: + debug: + specifier: https://github.com/debug-js/debug/tarball/4.3.4 + version: '@github.com/debug-js/debug/tarball/4.3.4' + is-negative: + specifier: https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5 + version: '@codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5' + lodash: + specifier: file:foo/bar/lodash.tgz + version: file:foo/bar/lodash.tgz + package1: + specifier: file:package1 + version: file:package1 + +packages: + + /asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + + '@codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5': + resolution: {tarball: https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5} + name: is-negative + version: 2.0.1 + engines: {node: '>=0.10.0'} + dev: false + + '@github.com/debug-js/debug/tarball/4.3.4': + resolution: {tarball: https://github.com/debug-js/debug/tarball/4.3.4} + name: debug + version: 4.3.4 + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + + file:foo/bar/lodash.tgz: + resolution: {integrity: sha512-fPftOkGbplay6FszUHWPJ8wV7liS+n2gB/UVN0Wv4G71KJWx+8trhGYsbekWqz6TTzkKur67bAdSIIccmKIyLA==, tarball: file:foo/bar/lodash.tgz} + name: lodash + version: 4.17.21 + dev: false + + file:package1: + resolution: {directory: package1, type: directory} + name: package1 + version: 1.0.0 + dependencies: + asynckit: 0.4.0 + dev: false diff --git a/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_many.yaml b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_many.yaml new file mode 100644 index 000000000000..22ef76912aff --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_many.yaml @@ -0,0 +1,868 @@ +lockfileVersion: 5.4 + +specifiers: + chalk: ^5.0.1 + commander: ^9.3.0 + lodash: ^4.17.21 + mocha: ^10.0.0 + react: ^18.1.0 + redux: ^4.2.0 + request: ^2.88.2 + +dependencies: + chalk: 5.0.1 + commander: 9.3.0 + lodash: 4.17.21 + react: 18.1.0 + redux: 4.2.0 + request: 2.88.2 + +devDependencies: + mocha: 10.0.0 + +packages: + + /@babel/runtime/7.18.3: + resolution: {integrity: sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.9 + dev: false + + /@ungap/promise-all-settled/1.1.2: + resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} + dev: true + + /ajv/6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: false + + /ansi-colors/4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + dev: true + + /ansi-regex/5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /anymatch/3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /asn1/0.2.6: + resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==} + dependencies: + safer-buffer: 2.1.2 + dev: false + + /assert-plus/1.0.0: + resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==} + engines: {node: '>=0.8'} + dev: false + + /asynckit/0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + dev: false + + /aws-sign2/0.7.0: + resolution: {integrity: sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==} + dev: false + + /aws4/1.11.0: + resolution: {integrity: sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==} + dev: false + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /bcrypt-pbkdf/1.0.2: + resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} + dependencies: + tweetnacl: 0.14.5 + dev: false + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion/2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browser-stdout/1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: true + + /camelcase/6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /caseless/0.12.0: + resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==} + dev: false + + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chalk/5.0.1: + resolution: {integrity: sha512-Fo07WOYGqMfCWHOzSXOt2CxDbC6skS/jO9ynEcmpANMoPrD+W1r1K6Vx7iNm+AQmETU1Xr2t+n8nzkV9t6xh3w==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + dev: false + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /cliui/7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /combined-stream/1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + dependencies: + delayed-stream: 1.0.0 + dev: false + + /commander/9.3.0: + resolution: {integrity: sha512-hv95iU5uXPbK83mjrJKuZyFM/LBAoCV/XhVGkS5Je6tl7sxr6A0ITMw5WoRV46/UaJ46Nllm3Xt7IaJhXTIkzw==} + engines: {node: ^12.20.0 || >=14} + dev: false + + /concat-map/0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /core-util-is/1.0.2: + resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==} + dev: false + + /dashdash/1.14.1: + resolution: {integrity: sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==} + engines: {node: '>=0.10'} + dependencies: + assert-plus: 1.0.0 + dev: false + + /debug/4.3.4_supports-color@8.1.1: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: true + + /decamelize/4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: true + + /delayed-stream/1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + dev: false + + /diff/5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: true + + /ecc-jsbn/0.1.2: + resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} + dependencies: + jsbn: 0.1.1 + safer-buffer: 2.1.2 + dev: false + + /emoji-regex/8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /escalade/3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp/4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /extend/3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + dev: false + + /extsprintf/1.3.0: + resolution: {integrity: sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==} + engines: {'0': node >=0.6.0} + dev: false + + /fast-deep-equal/3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: false + + /fast-json-stable-stringify/2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: false + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up/5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat/5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + + /forever-agent/0.6.1: + resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} + dev: false + + /form-data/2.3.3: + resolution: {integrity: sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + + /fs.realpath/1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-caller-file/2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /getpass/0.1.7: + resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} + dependencies: + assert-plus: 1.0.0 + dev: false + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob/7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /har-schema/2.0.0: + resolution: {integrity: sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==} + engines: {node: '>=4'} + dev: false + + /har-validator/5.1.5: + resolution: {integrity: sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==} + engines: {node: '>=6'} + deprecated: this library is no longer supported + dependencies: + ajv: 6.12.6 + har-schema: 2.0.0 + dev: false + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /he/1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + + /http-signature/1.2.0: + resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} + engines: {node: '>=0.8', npm: '>=1.3.7'} + dependencies: + assert-plus: 1.0.0 + jsprim: 1.4.2 + sshpk: 1.17.0 + dev: false + + /inflight/1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point/3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-plain-obj/2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + dev: true + + /is-typedarray/1.0.0: + resolution: {integrity: sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==} + dev: false + + /is-unicode-supported/0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /isstream/0.1.2: + resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + dev: false + + /js-tokens/4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /js-yaml/4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /jsbn/0.1.1: + resolution: {integrity: sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==} + dev: false + + /json-schema-traverse/0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: false + + /json-schema/0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + dev: false + + /json-stringify-safe/5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + dev: false + + /jsprim/1.4.2: + resolution: {integrity: sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==} + engines: {node: '>=0.6.0'} + dependencies: + assert-plus: 1.0.0 + extsprintf: 1.3.0 + json-schema: 0.4.0 + verror: 1.10.0 + dev: false + + /locate-path/6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash/4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + dev: false + + /log-symbols/4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /loose-envify/1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /mime-db/1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + dev: false + + /mime-types/2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + dependencies: + mime-db: 1.52.0 + dev: false + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch/5.0.1: + resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /mocha/10.0.0: + resolution: {integrity: sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==} + engines: {node: '>= 14.0.0'} + hasBin: true + dependencies: + '@ungap/promise-all-settled': 1.1.2 + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.4_supports-color@8.1.1 + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.0.1 + ms: 2.1.3 + nanoid: 3.3.3 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.2.1 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + dev: true + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms/2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid/3.3.3: + resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /oauth-sign/0.9.0: + resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} + dev: false + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /p-limit/3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate/5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /path-exists/4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute/1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /performance-now/2.1.0: + resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} + dev: false + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /psl/1.8.0: + resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} + dev: false + + /punycode/2.1.1: + resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + engines: {node: '>=6'} + dev: false + + /qs/6.5.3: + resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==} + engines: {node: '>=0.6'} + dev: false + + /randombytes/2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /react/18.1.0: + resolution: {integrity: sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /redux/4.2.0: + resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==} + dependencies: + '@babel/runtime': 7.18.3 + dev: false + + /regenerator-runtime/0.13.9: + resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} + dev: false + + /request/2.88.2: + resolution: {integrity: sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==} + engines: {node: '>= 6'} + deprecated: request has been deprecated, see https://github.com/request/request/issues/3142 + dependencies: + aws-sign2: 0.7.0 + aws4: 1.11.0 + caseless: 0.12.0 + combined-stream: 1.0.8 + extend: 3.0.2 + forever-agent: 0.6.1 + form-data: 2.3.3 + har-validator: 5.1.5 + http-signature: 1.2.0 + is-typedarray: 1.0.0 + isstream: 0.1.2 + json-stringify-safe: 5.0.1 + mime-types: 2.1.35 + oauth-sign: 0.9.0 + performance-now: 2.1.0 + qs: 6.5.3 + safe-buffer: 5.2.1 + tough-cookie: 2.5.0 + tunnel-agent: 0.6.0 + uuid: 3.4.0 + dev: false + + /require-directory/2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + /safer-buffer/2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + dev: false + + /serialize-javascript/6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + dependencies: + randombytes: 2.1.0 + dev: true + + /sshpk/1.17.0: + resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==} + engines: {node: '>=0.10.0'} + hasBin: true + dependencies: + asn1: 0.2.6 + assert-plus: 1.0.0 + bcrypt-pbkdf: 1.0.2 + dashdash: 1.14.1 + ecc-jsbn: 0.1.2 + getpass: 0.1.7 + jsbn: 0.1.1 + safer-buffer: 2.1.2 + tweetnacl: 0.14.5 + dev: false + + /string-width/4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /strip-ansi/6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color/8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /tough-cookie/2.5.0: + resolution: {integrity: sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==} + engines: {node: '>=0.8'} + dependencies: + psl: 1.8.0 + punycode: 2.1.1 + dev: false + + /tunnel-agent/0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /tweetnacl/0.14.5: + resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==} + dev: false + + /uri-js/4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.1.1 + dev: false + + /uuid/3.4.0: + resolution: {integrity: sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==} + deprecated: Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details. + hasBin: true + dev: false + + /verror/1.10.0: + resolution: {integrity: sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==} + engines: {'0': node >=0.6.0} + dependencies: + assert-plus: 1.0.0 + core-util-is: 1.0.2 + extsprintf: 1.3.0 + dev: false + + /workerpool/6.2.1: + resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} + dev: true + + /wrap-ansi/7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /y18n/5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yargs-parser/20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + dev: true + + /yargs-unparser/2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + dev: true + + /yargs/16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.4 + dev: true + + /yocto-queue/0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_normal.yaml b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_normal.yaml new file mode 100644 index 000000000000..f76c340a9660 --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_normal.yaml @@ -0,0 +1,25 @@ +lockfileVersion: 5.3 + +specifiers: + jquery: ^3.6.0 + promise: ^8.1.0 + +dependencies: + jquery: 3.6.0 + promise: 8.1.0 + +packages: + + /asap/2.0.6: + resolution: {integrity: sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=} + dev: false + + /jquery/3.6.0: + resolution: {integrity: sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==} + dev: false + + /promise/8.1.0: + resolution: {integrity: sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==} + dependencies: + asap: 2.0.6 + dev: false \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v6.yaml b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v6.yaml new file mode 100644 index 000000000000..c4d0d524896a --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v6.yaml @@ -0,0 +1,25 @@ +lockfileVersion: '6.0' + +dependencies: + jquery: + specifier: 3.6.0 + version: 3.6.0 + promise: + specifier: 8.1.0 + version: 8.1.0 + +packages: + + /asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + dev: false + + /jquery@3.6.0: + resolution: {integrity: sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==} + dev: false + + /promise@8.1.0: + resolution: {integrity: sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==} + dependencies: + asap: 2.0.6 + dev: false diff --git a/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v6_with_dev.yaml b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v6_with_dev.yaml new file mode 100644 index 000000000000..119bb9817bcf --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v6_with_dev.yaml @@ -0,0 +1,563 @@ +lockfileVersion: '6.0' + +dependencies: + react: + specifier: 18.1.0 + version: 18.1.0 + redux: + specifier: 4.2.0 + version: 4.2.0 + +devDependencies: + mocha: + specifier: ^10.0.0 + version: 10.0.0 + +packages: + + /@babel/runtime@7.22.3: + resolution: {integrity: sha512-XsDuspWKLUsxwCp6r7EhsExHtYfbe5oAGQ19kqngTdCPUoPQzOPdUbD/pB9PJiwb2ptYKQDjSJT3R6dC+EPqfQ==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.11 + dev: false + + /@ungap/promise-all-settled@1.1.2: + resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} + dev: true + + /ansi-colors@4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + dev: true + + /ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions@2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion@1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion@2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces@3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: true + + /camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar@3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /debug@4.3.4(supports-color@8.1.1): + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: true + + /decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: true + + /diff@5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: true + + /emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /escalade@3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /fill-range@7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + + /fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob@7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + + /inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + dev: true + + /is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch@5.0.1: + resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /mocha@10.0.0: + resolution: {integrity: sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==} + engines: {node: '>= 14.0.0'} + hasBin: true + dependencies: + '@ungap/promise-all-settled': 1.1.2 + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.4(supports-color@8.1.1) + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.0.1 + ms: 2.1.3 + nanoid: 3.3.3 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.2.1 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + dev: true + + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid@3.3.3: + resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /react@18.1.0: + resolution: {integrity: sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /redux@4.2.0: + resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==} + dependencies: + '@babel/runtime': 7.22.3 + dev: false + + /regenerator-runtime@0.13.11: + resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==} + dev: false + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /serialize-javascript@6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + dependencies: + randombytes: 2.1.0 + dev: true + + /string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /workerpool@6.2.1: + resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} + dev: true + + /wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yargs-parser@20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + dev: true + + /yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + dev: true + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.4 + dev: true + + /yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v9.yaml b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v9.yaml new file mode 100644 index 000000000000..b065716fe899 --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v9.yaml @@ -0,0 +1,213 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@babel/helper-string-parser': + specifier: 7.24.1 + version: 7.24.1 + debug: + specifier: https://github.com/debug-js/debug/tarball/4.3.5 + version: https://github.com/debug-js/debug/tarball/4.3.5 + finalhandler: + specifier: 1.1.1 + version: 1.1.1 + is-negative: + specifier: https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5 + version: https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5 + on-finished: + specifier: 2.3.0 + version: 2.3.0 + package1: + specifier: file:package1 + version: file:package1 + promise: + specifier: 8.1.0 + version: 8.1.0 + devDependencies: + jquery: + specifier: 3.6.0 + version: 3.6.0 + lodash: + specifier: file:foo/bar/lodash.tgz + version: file:foo/bar/lodash.tgz + ms: + specifier: 2.0.0 + version: 2.0.0 + + subdir: + dependencies: + sleep-utils: + specifier: 1.0.3 + version: 1.0.3 + + devDependencies: + await-sleep: + specifier: ^0.0.1 + version: 0.0.1 + +packages: + + '@babel/helper-string-parser@7.24.1': + resolution: {integrity: sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==} + engines: {node: '>=6.9.0'} + + asap@2.0.6: + resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} + + asynckit@https://github.com/alexindigo/asynckit/tarball/v0.4.0: + resolution: {tarball: https://github.com/alexindigo/asynckit/tarball/v0.4.0} + version: 0.4.0 + + await-sleep@0.0.1: + resolution: {integrity: sha512-H3X3eAxwGpeNIk/yvFOs8g7500Q1YvzrxjSC9TNgLGtjrMFxPwhDdcT34QNs2iGWpZ+5WKkMJdjDoYs+Sw+TaA==} + + debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@https://github.com/debug-js/debug/tarball/4.3.5: + resolution: {tarball: https://github.com/debug-js/debug/tarball/4.3.5} + version: 4.3.5 + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + encodeurl@1.0.2: + resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} + engines: {node: '>= 0.8'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + finalhandler@1.1.1: + resolution: {integrity: sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==} + engines: {node: '>= 0.8'} + + is-negative@https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5: + resolution: {tarball: https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5} + version: 2.0.1 + engines: {node: '>=0.10.0'} + + jquery@3.6.0: + resolution: {integrity: sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==} + + lodash@file:foo/bar/lodash.tgz: + resolution: {integrity: sha512-D2TlgcJ2ZQKKj4HVqOFkR+QETqRCXu+TCyxy7qH5QhxuJXCulHzBqmz4WrHA5Fs5ryY5RJYUqw2JBnbzveCtlA==, tarball: file:foo/bar/lodash.tgz} + version: 4.17.21 + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + + on-finished@2.3.0: + resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} + engines: {node: '>= 0.8'} + + package1@file:package1: + resolution: {directory: package1, type: directory} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + promise@8.1.0: + resolution: {integrity: sha512-W04AqnILOL/sPRXziNicCjSNRruLAuIHEOVBazepu0545DDNGYHz7ar9ZgZ1fMU8/MA4mVxp5rkBWRi6OXIy3Q==} + + sleep-utils@1.0.3: + resolution: {integrity: sha512-uJW7WDHISE1zJIdvoIewcdmis3pBvJhM30rni2gH7fHhV1NkTWLKw3J6CPRFdg3h+rFChFHzAgbkCKUErd4s8Q==} + + statuses@1.4.0: + resolution: {integrity: sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==} + engines: {node: '>= 0.6'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + +snapshots: + + '@babel/helper-string-parser@7.24.1': {} + + asap@2.0.6: + optional: true + + asynckit@https://github.com/alexindigo/asynckit/tarball/v0.4.0: {} + + await-sleep@0.0.1: {} + + debug@4.3.4(supports-color@8.1.1): + dependencies: + ms: 2.0.0 + optionalDependencies: + supports-color: 8.1.1 + + debug@https://github.com/debug-js/debug/tarball/4.3.5: + dependencies: + ms: 2.1.2 + + ee-first@1.1.1: {} + + encodeurl@1.0.2: {} + + escape-html@1.0.3: {} + + finalhandler@1.1.1: + dependencies: + debug: 4.3.4(supports-color@8.1.1) + encodeurl: 1.0.2 + escape-html: 1.0.3 + on-finished: 2.3.0 + parseurl: 1.3.3 + statuses: 1.4.0 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + is-negative@https://codeload.github.com/zkochan/is-negative/tar.gz/2fa0531ab04e300a24ef4fd7fb3a280eccb7ccc5: {} + + jquery@3.6.0: {} + + lodash@file:foo/bar/lodash.tgz: {} + + ms@2.0.0: {} + + ms@2.1.2: {} + + on-finished@2.3.0: + dependencies: + ee-first: 1.1.1 + + package1@file:package1: + dependencies: + asynckit: https://github.com/alexindigo/asynckit/tarball/v0.4.0 + + parseurl@1.3.3: {} + + promise@8.1.0: + optionalDependencies: + asap: 2.0.6 + + sleep-utils@1.0.3: {} + + statuses@1.4.0: {} + + unpipe@1.0.0: {} diff --git a/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v9_cyclic_import.yaml b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v9_cyclic_import.yaml new file mode 100644 index 000000000000..08a7991b0e79 --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v9_cyclic_import.yaml @@ -0,0 +1,67 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + update-browserslist-db: + specifier: 1.0.16 + version: 1.0.16(browserslist@4.23.0) + +packages: + + browserslist@4.23.0: + resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + caniuse-lite@1.0.30001627: + resolution: {integrity: sha512-4zgNiB8nTyV/tHhwZrFs88ryjls/lHiqFhrxCW4qSTeuRByBVnPYpDInchOIySWknznucaf31Z4KYqjfbrecVw==} + + electron-to-chromium@1.4.789: + resolution: {integrity: sha512-0VbyiaXoT++Fi2vHGo2ThOeS6X3vgRCWrjPeO2FeIAWL6ItiSJ9BqlH8LfCXe3X1IdcG+S0iLoNaxQWhfZoGzQ==} + + escalade@3.1.2: + resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} + engines: {node: '>=6'} + + node-releases@2.0.14: + resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} + + picocolors@1.0.1: + resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} + + update-browserslist-db@1.0.16: + resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + +snapshots: + + browserslist@4.23.0: + dependencies: + caniuse-lite: 1.0.30001627 + electron-to-chromium: 1.4.789 + node-releases: 2.0.14 + update-browserslist-db: 1.0.16(browserslist@4.23.0) + + caniuse-lite@1.0.30001627: {} + + electron-to-chromium@1.4.789: {} + + escalade@3.1.2: {} + + node-releases@2.0.14: {} + + picocolors@1.0.1: {} + + update-browserslist-db@1.0.16(browserslist@4.23.0): + dependencies: + browserslist: 4.23.0 + escalade: 3.1.2 + picocolors: 1.0.1 diff --git a/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v9_same-vers-diff-peers.yaml b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v9_same-vers-diff-peers.yaml new file mode 100644 index 000000000000..595562efba8f --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_v9_same-vers-diff-peers.yaml @@ -0,0 +1,90 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + jest-config: + specifier: 30.0.3 + version: 30.0.3(@types/node@24.0.7)(babel-plugin-macros@3.1.0) + devDependencies: + jest-runner: + specifier: 27.5.1 + version: 27.5.1 + +packages: + + jest-config@30.0.3: + resolution: {integrity: sha512-j0L4oRCtJwNyZktXIqwzEiDVQXBbQ4dqXuLD/TZdn++hXIcIfZmjHgrViEy5s/+j4HvITmAXbexVZpQ/jnr0bg==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + peerDependencies: + '@types/node': '*' + esbuild-register: '>=3.4.0' + ts-node: '>=9.0.0' + peerDependenciesMeta: + '@types/node': + optional: true + esbuild-register: + optional: true + ts-node: + optional: true + + jest-pnp-resolver@1.2.3: + resolution: {integrity: sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==} + engines: {node: '>=6'} + peerDependencies: + jest-resolve: '*' + peerDependenciesMeta: + jest-resolve: + optional: true + + jest-resolve@27.5.1: + resolution: {integrity: sha512-FFDy8/9E6CV83IMbDpcjOhumAQPDyETnU2KZ1O98DwTnz8AOBsW/Xv3GySr1mOZdItLR+zDZ7I/UdTFbgSOVCw==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-resolve@30.0.2: + resolution: {integrity: sha512-q/XT0XQvRemykZsvRopbG6FQUT6/ra+XV6rPijyjT6D0msOyCvR2A5PlWZLd+fH0U8XWKZfDiAgrUNDNX2BkCw==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + + jest-runner@27.5.1: + resolution: {integrity: sha512-g4NPsM4mFCOwFKXO4p/H/kWGdJp9V8kURY2lX8Me2drgXqG7rrZAx5kv+5H7wtt/cdFIjhqYx1HrlqWHaOvDaQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + jest-runner@30.0.3: + resolution: {integrity: sha512-CxYBzu9WStOBBXAKkLXGoUtNOWsiS1RRmUQb6SsdUdTcqVncOau7m8AJ4cW3Mz+YL1O9pOGPSYLyvl8HBdFmkQ==} + engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} + +snapshots: + + jest-config@30.0.3(@types/node@24.0.7)(babel-plugin-macros@3.1.0): + dependencies: + jest-runner: 30.0.3 + + jest-pnp-resolver@1.2.3(jest-resolve@27.5.1): + optionalDependencies: + jest-resolve: 27.5.1 + + jest-pnp-resolver@1.2.3(jest-resolve@30.0.2): + optionalDependencies: + jest-resolve: 30.0.2 + + jest-resolve@27.5.1: + dependencies: + jest-pnp-resolver: 1.2.3(jest-resolve@27.5.1) + + jest-resolve@30.0.2: + dependencies: + jest-pnp-resolver: 1.2.3(jest-resolve@30.0.2) + + jest-runner@27.5.1: + dependencies: + jest-pnp-resolver: 1.2.3(jest-resolve@27.5.1) + + jest-runner@30.0.3: + dependencies: + jest-pnp-resolver: 1.2.3(jest-resolve@30.0.2) + diff --git a/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_with_dev.yaml b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_with_dev.yaml new file mode 100644 index 000000000000..de07fb897473 --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/testdata/pnpm-lock_with_dev.yaml @@ -0,0 +1,562 @@ +lockfileVersion: 5.4 + +specifiers: + mocha: ^10.0.0 + react: ^18.1.0 + redux: ^4.2.0 + +dependencies: + react: 18.1.0 + redux: 4.2.0 + +devDependencies: + mocha: 10.0.0 + +packages: + + /@babel/runtime/7.18.3: + resolution: {integrity: sha512-38Y8f7YUhce/K7RMwTp7m0uCumpv9hZkitCbBClqQIow1qSbCvGkcegKOXpEWCQLfWmevgRiWokZ1GkpfhbZug==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.9 + dev: false + + /@ungap/promise-all-settled/1.1.2: + resolution: {integrity: sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==} + dev: true + + /ansi-colors/4.1.1: + resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==} + engines: {node: '>=6'} + dev: true + + /ansi-regex/5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /anymatch/3.1.2: + resolution: {integrity: sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /brace-expansion/2.0.1: + resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + dependencies: + balanced-match: 1.0.2 + dev: true + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /browser-stdout/1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + dev: true + + /camelcase/6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + dev: true + + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.2 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /cliui/7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: true + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /concat-map/0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /debug/4.3.4_supports-color@8.1.1: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + supports-color: 8.1.1 + dev: true + + /decamelize/4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + dev: true + + /diff/5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: true + + /emoji-regex/8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: true + + /escalade/3.1.1: + resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} + engines: {node: '>=6'} + dev: true + + /escape-string-regexp/4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up/5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat/5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + dev: true + + /fs.realpath/1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-caller-file/2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: true + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob/7.2.0: + resolution: {integrity: sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /he/1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: true + + /inflight/1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-fullwidth-code-point/3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: true + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-plain-obj/2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + dev: true + + /is-unicode-supported/0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + dev: true + + /js-tokens/4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + dev: false + + /js-yaml/4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /locate-path/6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /log-symbols/4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + dev: true + + /loose-envify/1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + dev: false + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimatch/5.0.1: + resolution: {integrity: sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: true + + /mocha/10.0.0: + resolution: {integrity: sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==} + engines: {node: '>= 14.0.0'} + hasBin: true + dependencies: + '@ungap/promise-all-settled': 1.1.2 + ansi-colors: 4.1.1 + browser-stdout: 1.3.1 + chokidar: 3.5.3 + debug: 4.3.4_supports-color@8.1.1 + diff: 5.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 7.2.0 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 5.0.1 + ms: 2.1.3 + nanoid: 3.3.3 + serialize-javascript: 6.0.0 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.2.1 + yargs: 16.2.0 + yargs-parser: 20.2.4 + yargs-unparser: 2.0.0 + dev: true + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /ms/2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: true + + /nanoid/3.3.3: + resolution: {integrity: sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /p-limit/3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate/5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /path-exists/4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute/1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /randombytes/2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + dependencies: + safe-buffer: 5.2.1 + dev: true + + /react/18.1.0: + resolution: {integrity: sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /redux/4.2.0: + resolution: {integrity: sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA==} + dependencies: + '@babel/runtime': 7.18.3 + dev: false + + /regenerator-runtime/0.13.9: + resolution: {integrity: sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==} + dev: false + + /require-directory/2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: true + + /safe-buffer/5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + dev: true + + /serialize-javascript/6.0.0: + resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==} + dependencies: + randombytes: 2.1.0 + dev: true + + /string-width/4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: true + + /strip-ansi/6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-color/8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + dependencies: + has-flag: 4.0.0 + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /workerpool/6.2.1: + resolution: {integrity: sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==} + dev: true + + /wrap-ansi/7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + dev: true + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /y18n/5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: true + + /yargs-parser/20.2.4: + resolution: {integrity: sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==} + engines: {node: '>=10'} + dev: true + + /yargs-unparser/2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + dev: true + + /yargs/16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.4 + dev: true + + /yocto-queue/0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/pkg/dependency/parser/nodejs/pnpm/types.go b/pkg/dependency/parser/nodejs/pnpm/types.go new file mode 100644 index 000000000000..3f236852096e --- /dev/null +++ b/pkg/dependency/parser/nodejs/pnpm/types.go @@ -0,0 +1,57 @@ +package pnpm + +// PackageKey represents a key in the "packages" section of pnpm lock file. +// It consists of the package name and version without peer dependency information. +// This is different from the snapshot key which includes peer dependencies. +// Examples: +// - "lodash@4.17.21" +// - "@babel/core@7.21.0" +// - "jest-config@30.0.3" +type PackageKey string + +// SnapshotKey uniquely identifies a package instance in the "snapshots" section. +// In pnpm v9+, this includes peer dependency information to distinguish between +// different installations of the same package with different peer dependencies. +// Examples: +// - "lodash@4.17.21" // no peers +// - "jest-config@30.0.3(@types/node@24.0.7)(babel-plugin-macros@3.1.0)" // with peers +// - "jest-pnp-resolver@1.2.3(jest-resolve@27.5.1)" // with peer +type SnapshotKey string + +type PackageResolution struct { + Tarball string `yaml:"tarball,omitempty"` +} + +type PackageInfo struct { + Resolution PackageResolution `yaml:"resolution"` + Dependencies map[string]string `yaml:"dependencies,omitempty"` + DevDependencies map[string]string `yaml:"devDependencies,omitempty"` + IsDev bool `yaml:"dev,omitempty"` + Name string `yaml:"name,omitempty"` + Version string `yaml:"version,omitempty"` +} + +type LockFile struct { + LockfileVersion any `yaml:"lockfileVersion"` + Dependencies map[string]any `yaml:"dependencies,omitempty"` + DevDependencies map[string]any `yaml:"devDependencies,omitempty"` + Packages map[PackageKey]PackageInfo `yaml:"packages,omitempty"` + + // V9: New fields introduced in pnpm v9 lock file format + Importers map[string]Importer `yaml:"importers,omitempty"` + Snapshots map[SnapshotKey]Snapshot `yaml:"snapshots,omitempty"` +} + +type Importer struct { + Dependencies map[string]ImporterDepVersion `yaml:"dependencies,omitempty"` + DevDependencies map[string]ImporterDepVersion `yaml:"devDependencies,omitempty"` +} + +type ImporterDepVersion struct { + Version string `yaml:"version,omitempty"` +} + +type Snapshot struct { + Dependencies map[string]string `yaml:"dependencies,omitempty"` + OptionalDependencies map[string]string `yaml:"optionalDependencies,omitempty"` +} diff --git a/pkg/dependency/parser/nodejs/yarn/parse.go b/pkg/dependency/parser/nodejs/yarn/parse.go new file mode 100644 index 000000000000..b72fbcf2a3c2 --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/parse.go @@ -0,0 +1,335 @@ +package yarn + +import ( + "bufio" + "bytes" + "context" + "io" + "regexp" + "sort" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +var ( + yarnPatternRegexp = regexp.MustCompile(`^\s?\\?"?(?P\S+?)@(?:(?P\S+?):)?(?P.+?)\\?"?:?$`) + yarnVersionRegexp = regexp.MustCompile(`^"?version:?"?\s+"?(?P[^"]+)"?`) + yarnDependencyRegexp = regexp.MustCompile(`\s{4,}"?(?P.+?)"?:?\s"?(?:(?P\S+?):)?(?P[^"]+)"?`) +) + +type LockFile struct { + Dependencies map[string]Dependency +} + +type Library struct { + Patterns []string + Name string + Version string + Location ftypes.Location +} +type Dependency struct { + Pattern string + Name string +} + +type LineScanner struct { + *bufio.Scanner + lineCount int +} + +func NewLineScanner(r io.Reader) *LineScanner { + return &LineScanner{ + Scanner: bufio.NewScanner(r), + } +} + +func (s *LineScanner) Scan() bool { + scan := s.Scanner.Scan() + if scan { + s.lineCount++ + } + return scan +} + +func (s *LineScanner) LineNum(prevNum int) int { + return prevNum + s.lineCount - 1 +} + +func parsePattern(target string) (packagename, protocol, version string, err error) { + capture := yarnPatternRegexp.FindStringSubmatch(target) + if len(capture) < 3 { + return "", "", "", xerrors.New("not package format") + } + for i, group := range yarnPatternRegexp.SubexpNames() { + switch group { + case "package": + packagename = capture[i] + case "protocol": + protocol = capture[i] + case "version": + version = capture[i] + } + } + return +} + +func parsePackagePatterns(target string) (packagename, protocol string, patterns []string, err error) { + patternsSplit := strings.Split(target, ", ") + packagename, protocol, _, err = parsePattern(patternsSplit[0]) + if err != nil { + return "", "", nil, err + } + patterns = xslices.Map(patternsSplit, func(pattern string) string { + _, _, version, _ := parsePattern(pattern) + return packageID(packagename, version) + }) + return +} + +func getVersion(target string) (version string, err error) { + capture := yarnVersionRegexp.FindStringSubmatch(target) + if len(capture) < 2 { + return "", xerrors.Errorf("failed to parse version: '%s", target) + } + return capture[len(capture)-1], nil +} + +func getDependency(target string) (name, version string, err error) { + capture := yarnDependencyRegexp.FindStringSubmatch(target) + if len(capture) < 3 { + return "", "", xerrors.New("not dependency") + } + if !validProtocol(capture[2]) { + return "", "", nil + } + return capture[1], capture[3], nil +} + +func validProtocol(protocol string) bool { + switch protocol { + // only scan npm packages + case "npm", "": + return true + } + return false +} + +func ignoreProtocol(protocol string) bool { + switch protocol { + case "workspace", "patch", "file", "link", "portal", "github", "git", "git+ssh", "git+http", "git+https", "git+file": + return true + } + return false +} + +func parseResults(patternIDs map[string]string, dependsOn map[string][]string) (deps ftypes.Dependencies) { + // find dependencies by patterns + for pkgID, depPatterns := range dependsOn { + depIDs := xslices.Map(depPatterns, func(pattern string) string { + return patternIDs[pattern] + }) + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: depIDs, + }) + } + return deps +} + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("yarn"), + } +} + +func (p *Parser) scanBlocks(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + if i := bytes.Index(data, []byte("\n\n")); i >= 0 { + // We have a full newline-terminated line. + return i + 2, data[0:i], nil + } else if i := bytes.Index(data, []byte("\r\n\r\n")); i >= 0 { + return i + 4, data[0:i], nil + } + + // If we're at EOF, we have a final, non-terminated line. Return it. + if atEOF { + return len(data), data, nil + } + // Request more data. + return 0, nil, nil +} + +func (p *Parser) parseBlock(block []byte, lineNum int) (lib Library, deps []string, newLine int, err error) { + var ( + emptyLines int // lib can start with empty lines first + skipBlock bool + ) + + scanner := NewLineScanner(bytes.NewReader(block)) + for scanner.Scan() { + line := scanner.Text() + + if line == "" { + emptyLines++ + continue + } + + if line[0] == '#' || skipBlock { + continue + } + + // Skip this block + if strings.HasPrefix(line, "__metadata") { + skipBlock = true + continue + } + + line = strings.TrimPrefix(strings.TrimSpace(line), "\"") + + switch { + case strings.HasPrefix(line, "version"): + if lib.Version, err = getVersion(line); err != nil { + skipBlock = true + } + continue + case strings.HasPrefix(line, "dependencies:"): + // start dependencies block + deps = parseDependencies(scanner) + continue + } + + // try parse package patterns + if name, protocol, patterns, patternErr := parsePackagePatterns(line); patternErr == nil { + if patterns == nil || !validProtocol(protocol) { + skipBlock = true + if !ignoreProtocol(protocol) { + // we need to calculate the last line of the block in order to correctly determine the line numbers of the next blocks + // store the error. we will handle it later + err = xerrors.Errorf("unknown protocol: '%s', line: %s", protocol, line) + continue + } + continue + } + lib.Patterns = patterns + lib.Name = name + continue + } + } + + // in case an unsupported protocol is detected + // show warning and continue parsing + if err != nil { + p.logger.Warn("Protocol error", log.Err(err)) + return Library{}, nil, scanner.LineNum(lineNum), nil + } + + lib.Location = ftypes.Location{ + StartLine: lineNum + emptyLines, + EndLine: scanner.LineNum(lineNum), + } + + if scanErr := scanner.Err(); scanErr != nil { + err = scanErr + } + + return lib, deps, scanner.LineNum(lineNum), err +} + +func parseDependencies(scanner *LineScanner) (deps []string) { + for scanner.Scan() { + line := scanner.Text() + dep, err := parseDependency(line) + if err != nil { + // finished dependencies block + return deps + } + deps = append(deps, dep) + } + + return +} + +func parseDependency(line string) (string, error) { + name, version, err := getDependency(line) + if err != nil { + return "", err + } + return packageID(name, version), nil +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, map[string][]string, error) { + lineNumber := 1 + var pkgs ftypes.Packages + + // patternIDs holds mapping between patterns and package IDs + // e.g. ajv@^6.5.5 => ajv@6.10.0 + // This is needed to update dependencies from `DependsOn`. + patternIDs := make(map[string]string) + + // patternIDs holds mapping between package ID and patterns + // e.g. `@babel/helper-regex@7.4.4` => [`@babel/helper-regex@^7.0.0`, `@babel/helper-regex@^7.4.4`] + // This is needed to compare package patterns with patterns from package.json files in `fanal` package. + pkgIDPatterns := make(map[string][]string) + + scanner := bufio.NewScanner(r) + scanner.Split(p.scanBlocks) + dependsOn := make(map[string][]string) + for scanner.Scan() { + block := scanner.Bytes() + lib, deps, newLine, err := p.parseBlock(block, lineNumber) + lineNumber = newLine + 2 + if err != nil { + return nil, nil, nil, err + } else if lib.Name == "" { + continue + } + + pkgID := packageID(lib.Name, lib.Version) + pkgs = append(pkgs, ftypes.Package{ + ID: pkgID, + Name: lib.Name, + Version: lib.Version, + Locations: []ftypes.Location{lib.Location}, + }) + + pkgIDPatterns[pkgID] = lib.Patterns + for _, pattern := range lib.Patterns { + // e.g. + // combined-stream@^1.0.6 => combined-stream@1.0.8 + // combined-stream@~1.0.6 => combined-stream@1.0.8 + patternIDs[pattern] = pkgID + if len(deps) > 0 { + dependsOn[pkgID] = deps + } + } + } + + if err := scanner.Err(); err != nil { + return nil, nil, nil, xerrors.Errorf("failed to scan yarn.lock, got scanner error: %s", err.Error()) + } + + // Replace dependency patterns with package IDs + // e.g. ajv@^6.5.5 => ajv@6.10.0 + deps := parseResults(patternIDs, dependsOn) + + sort.Sort(pkgs) + sort.Sort(deps) + return pkgs, deps, pkgIDPatterns, nil +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.Yarn, name, version) +} diff --git a/pkg/dependency/parser/nodejs/yarn/parse_test.go b/pkg/dependency/parser/nodejs/yarn/parse_test.go new file mode 100644 index 000000000000..f075c4bc87ae --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/parse_test.go @@ -0,0 +1,314 @@ +package yarn + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParsePattern(t *testing.T) { + vectors := []struct { + name string + target string + expectName string + expectProtocol string + expactVersion string + occurErr bool + }{ + { + name: "normal", + target: `asn1@~0.2.3:`, + expectName: "asn1", + expactVersion: "~0.2.3", + }, + { + name: "normal with protocol", + target: `asn1@npm:~0.2.3:`, + expectName: "asn1", + expectProtocol: "npm", + expactVersion: "~0.2.3", + }, + { + name: "scope", + target: `@babel/code-frame@^7.0.0:`, + expectName: "@babel/code-frame", + expactVersion: "^7.0.0", + }, + { + name: "scope with protocol", + target: `@babel/code-frame@npm:^7.0.0:`, + expectName: "@babel/code-frame", + expectProtocol: "npm", + expactVersion: "^7.0.0", + }, + { + name: "scope with protocol and quotes", + target: `"@babel/code-frame@npm:^7.0.0":`, + expectName: "@babel/code-frame", + expectProtocol: "npm", + expactVersion: "^7.0.0", + }, + { + name: "unusual version", + target: `grunt-contrib-cssmin@3.0.*:`, + expectName: "grunt-contrib-cssmin", + expactVersion: "3.0.*", + }, + { + name: "conditional version", + target: `"js-tokens@^3.0.0 || ^4.0.0":`, + expectName: "js-tokens", + expactVersion: "^3.0.0 || ^4.0.0", + }, + { + target: "grunt-contrib-uglify-es@gruntjs/grunt-contrib-uglify#harmony:", + expectName: "grunt-contrib-uglify-es", + expactVersion: "gruntjs/grunt-contrib-uglify#harmony", + }, + { + target: `"jquery@git+https://xxxx:x-oauth-basic@github.com/tomoyamachi/jquery":`, + expectName: "jquery", + expectProtocol: "git+https", + expactVersion: "//xxxx:x-oauth-basic@github.com/tomoyamachi/jquery", + }, + { + target: `normal line`, + occurErr: true, + }, + } + + for _, v := range vectors { + gotName, gotProtocol, gotVersion, err := parsePattern(v.target) + + if v.occurErr != (err != nil) { + t.Errorf("expect error %t but err is %s", v.occurErr, err) + continue + } + + if gotName != v.expectName { + t.Errorf("name mismatch: got %s, want %s, target :%s", gotName, v.expectName, v.target) + } + + if gotProtocol != v.expectProtocol { + t.Errorf("protocol mismatch: got %s, want %s, target :%s", gotProtocol, v.expectProtocol, v.target) + } + + if gotVersion != v.expactVersion { + t.Errorf("version mismatch: got %s, want %s, target :%s", gotVersion, v.expactVersion, v.target) + } + } +} + +func TestParsePackagePatterns(t *testing.T) { + vectors := []struct { + name string + target string + expectName string + expectProtocol string + expactPatterns []string + occurErr bool + }{ + { + name: "normal", + target: `asn1@~0.2.3:`, + expectName: "asn1", + expactPatterns: []string{ + "asn1@~0.2.3", + }, + }, + { + name: "normal with quotes", + target: `"asn1@~0.2.3":`, + expectName: "asn1", + expactPatterns: []string{ + "asn1@~0.2.3", + }, + }, + { + name: "normal with protocol", + target: `asn1@npm:~0.2.3:`, + expectName: "asn1", + expectProtocol: "npm", + expactPatterns: []string{ + "asn1@~0.2.3", + }, + }, + { + name: "multiple patterns", + target: `loose-envify@^1.1.0, loose-envify@^1.4.0:`, + expectName: "loose-envify", + expactPatterns: []string{ + "loose-envify@^1.1.0", + "loose-envify@^1.4.0", + }, + }, + { + name: "multiple patterns v2", + target: `"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":`, + expectName: "loose-envify", + expectProtocol: "npm", + expactPatterns: []string{ + "loose-envify@^1.1.0", + "loose-envify@^1.4.0", + }, + }, + { + target: `normal line`, + occurErr: true, + }, + } + + for _, v := range vectors { + gotName, gotProtocol, gotPatterns, err := parsePackagePatterns(v.target) + + if v.occurErr != (err != nil) { + t.Errorf("expect error %t but err is %s", v.occurErr, err) + continue + } + + if gotName != v.expectName { + t.Errorf("name mismatch: got %s, want %s, target: %s", gotName, v.expectName, v.target) + } + + if gotProtocol != v.expectProtocol { + t.Errorf("protocol mismatch: got %s, want %s, target: %s", gotProtocol, v.expectProtocol, v.target) + } + + sort.Strings(gotPatterns) + sort.Strings(v.expactPatterns) + + assert.Equal(t, v.expactPatterns, gotPatterns) + } +} + +func TestGetDependency(t *testing.T) { + vectors := []struct { + name string + target string + expectName string + expactVersion string + occurErr bool + }{ + { + name: "normal", + target: ` chalk "^2.0.1"`, + expectName: "chalk", + expactVersion: "^2.0.1", + }, + { + name: "range", + target: ` js-tokens "^3.0.0 || ^4.0.0"`, + expectName: "js-tokens", + expactVersion: "^3.0.0 || ^4.0.0", + }, + { + name: "normal v2", + target: ` depd: ~1.1.2`, + expectName: "depd", + expactVersion: "~1.1.2", + }, + { + name: "range version v2", + target: ` statuses: ">= 1.5.0 < 2"`, + expectName: "statuses", + expactVersion: ">= 1.5.0 < 2", + }, + { + name: "name with scope", + target: ` "@types/color-name": ^1.1.1`, + expectName: "@types/color-name", + expactVersion: "^1.1.1", + }, + { + name: "version with protocol", + target: ` ms: "npm:2.1.2"`, + expectName: "ms", + expactVersion: "2.1.2", + }, + } + + for _, v := range vectors { + gotName, gotVersion, err := getDependency(v.target) + + if v.occurErr != (err != nil) { + t.Errorf("expect error %t but err is %s", v.occurErr, err) + continue + } + + if gotName != v.expectName { + t.Errorf("name mismatch: got %s, want %s, target: %s", gotName, v.expectName, v.target) + } + + if gotVersion != v.expactVersion { + t.Errorf("version mismatch: got %s, want %s, target: %s", gotVersion, v.expactVersion, v.target) + } + } +} + +func TestParse(t *testing.T) { + tests := []struct { + name string + file string // Test input file + want []ftypes.Package + wantDeps []ftypes.Dependency + }{ + { + name: "happy", + file: "testdata/yarn_happy.lock", + want: yarnHappy, + wantDeps: yarnHappyDeps, + }, + { + file: "testdata/yarn_with_npm.lock", + want: yarnWithNpm, + }, + { + name: "happy v2", + file: "testdata/yarn_v2_happy.lock", + want: yarnV2Happy, + wantDeps: yarnV2HappyDeps, + }, + { + name: "yarn with local dependency", + file: "testdata/yarn_with_local.lock", + want: yarnWithLocal, + wantDeps: yarnWithLocalDeps, + }, + { + name: "yarn v2 with protocols in dependency section", + file: "testdata/yarn_v2_deps_with_protocol.lock", + want: yarnV2DepsWithProtocol, + wantDeps: yarnV2DepsWithProtocolDeps, + }, + { + name: "yarn with git dependency", + file: "testdata/yarn_with_git.lock", + }, + { + name: "yarn file with bad protocol", + file: "testdata/yarn_with_bad_protocol.lock", + want: yarnBadProtocol, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + + got, deps, _, err := NewParser().Parse(t.Context(), f) + require.NoError(t, err) + + assert.Equal(t, tt.want, got) + + if tt.wantDeps != nil { + assert.Equal(t, tt.wantDeps, deps) + } + }) + } +} diff --git a/pkg/dependency/parser/nodejs/yarn/parse_testcase.go b/pkg/dependency/parser/nodejs/yarn/parse_testcase.go new file mode 100644 index 000000000000..8d829fb195cd --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/parse_testcase.go @@ -0,0 +1,160 @@ +package yarn + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +var ( + yarnHappy = []ftypes.Package{ + {ID: "@babel/helper-regex@7.4.4", Name: "@babel/helper-regex", Version: "7.4.4", Locations: []ftypes.Location{{StartLine: 4, EndLine: 9}}}, + {ID: "ansi-regex@2.1.1", Name: "ansi-regex", Version: "2.1.1", Locations: []ftypes.Location{{StartLine: 11, EndLine: 14}}}, + {ID: "ansi-regex@3.0.0", Name: "ansi-regex", Version: "3.0.0", Locations: []ftypes.Location{{StartLine: 16, EndLine: 19}}}, + {ID: "asap@2.0.6", Name: "asap", Version: "2.0.6", Locations: []ftypes.Location{{StartLine: 21, EndLine: 24}}}, + {ID: "inherits@2.0.3", Name: "inherits", Version: "2.0.3", Locations: []ftypes.Location{{StartLine: 26, EndLine: 29}}}, + {ID: "is-fullwidth-code-point@2.0.0", Name: "is-fullwidth-code-point", Version: "2.0.0", Locations: []ftypes.Location{{StartLine: 31, EndLine: 34}}}, + {ID: "jquery@3.4.1", Name: "jquery", Version: "3.4.1", Locations: []ftypes.Location{{StartLine: 41, EndLine: 44}}}, + {ID: "js-tokens@4.0.0", Name: "js-tokens", Version: "4.0.0", Locations: []ftypes.Location{{StartLine: 36, EndLine: 39}}}, + {ID: "lodash@4.17.11", Name: "lodash", Version: "4.17.11", Locations: []ftypes.Location{{StartLine: 46, EndLine: 49}}}, + {ID: "promise@8.0.3", Name: "promise", Version: "8.0.3", Locations: []ftypes.Location{{StartLine: 51, EndLine: 56}}}, + {ID: "safe-buffer@5.1.2", Name: "safe-buffer", Version: "5.1.2", Locations: []ftypes.Location{{StartLine: 58, EndLine: 61}}}, + {ID: "safer-buffer@2.1.2", Name: "safer-buffer", Version: "2.1.2", Locations: []ftypes.Location{{StartLine: 63, EndLine: 66}}}, + {ID: "statuses@1.5.0", Name: "statuses", Version: "1.5.0", Locations: []ftypes.Location{{StartLine: 68, EndLine: 71}}}, + {ID: "string-width@2.1.1", Name: "string-width", Version: "2.1.1", Locations: []ftypes.Location{{StartLine: 73, EndLine: 79}}}, + {ID: "strip-ansi@3.0.1", Name: "strip-ansi", Version: "3.0.1", Locations: []ftypes.Location{{StartLine: 82, EndLine: 87}}}, + {ID: "strip-ansi@4.0.0", Name: "strip-ansi", Version: "4.0.0", Locations: []ftypes.Location{{StartLine: 89, EndLine: 94}}}, + {ID: "whatwg-fetch@3.0.0", Name: "whatwg-fetch", Version: "3.0.0", Locations: []ftypes.Location{{StartLine: 96, EndLine: 99}}}, + {ID: "wide-align@1.1.3", Name: "wide-align", Version: "1.1.3", Locations: []ftypes.Location{{StartLine: 101, EndLine: 106}}}, + } + + yarnHappyDeps = []ftypes.Dependency{ + { + ID: "@babel/helper-regex@7.4.4", + DependsOn: []string{ + "lodash@4.17.11", + }, + }, + { + ID: "promise@8.0.3", + DependsOn: []string{ + "asap@2.0.6", + }, + }, + { + ID: "string-width@2.1.1", + DependsOn: []string{ + "is-fullwidth-code-point@2.0.0", + "strip-ansi@4.0.0", + }, + }, + { + ID: "strip-ansi@3.0.1", + DependsOn: []string{ + "ansi-regex@2.1.1", + }, + }, + { + ID: "strip-ansi@4.0.0", + DependsOn: []string{ + "ansi-regex@3.0.0", + }, + }, + { + ID: "wide-align@1.1.3", + DependsOn: []string{ + "string-width@2.1.1", + }, + }, + } + + yarnV2Happy = []ftypes.Package{ + {ID: "@types/color-name@1.1.1", Name: "@types/color-name", Version: "1.1.1", Locations: []ftypes.Location{{StartLine: 8, EndLine: 13}}}, + {ID: "abbrev@1.1.1", Name: "abbrev", Version: "1.1.1", Locations: []ftypes.Location{{StartLine: 15, EndLine: 20}}}, + {ID: "ansi-styles@3.2.1", Name: "ansi-styles", Version: "3.2.1", Locations: []ftypes.Location{{StartLine: 22, EndLine: 29}}}, + {ID: "ansi-styles@4.2.1", Name: "ansi-styles", Version: "4.2.1", Locations: []ftypes.Location{{StartLine: 31, EndLine: 39}}}, + {ID: "assert-plus@1.0.0", Name: "assert-plus", Version: "1.0.0", Locations: []ftypes.Location{{StartLine: 41, EndLine: 46}}}, + {ID: "async@3.2.0", Name: "async", Version: "3.2.0", Locations: []ftypes.Location{{StartLine: 48, EndLine: 53}}}, + {ID: "color-convert@1.9.3", Name: "color-convert", Version: "1.9.3", Locations: []ftypes.Location{{StartLine: 63, EndLine: 70}}}, + {ID: "color-convert@2.0.1", Name: "color-convert", Version: "2.0.1", Locations: []ftypes.Location{{StartLine: 72, EndLine: 79}}}, + {ID: "color-name@1.1.3", Name: "color-name", Version: "1.1.3", Locations: []ftypes.Location{{StartLine: 81, EndLine: 86}}}, + {ID: "color-name@1.1.4", Name: "color-name", Version: "1.1.4", Locations: []ftypes.Location{{StartLine: 88, EndLine: 93}}}, + {ID: "ipaddr.js@1.9.1", Name: "ipaddr.js", Version: "1.9.1", Locations: []ftypes.Location{{StartLine: 104, EndLine: 109}}}, + {ID: "js-tokens@4.0.0", Name: "js-tokens", Version: "4.0.0", Locations: []ftypes.Location{{StartLine: 111, EndLine: 116}}}, + {ID: "loose-envify@1.4.0", Name: "loose-envify", Version: "1.4.0", Locations: []ftypes.Location{{StartLine: 118, EndLine: 127}}}, + {ID: "node-gyp@7.1.0", Name: "node-gyp", Version: "7.1.0", Locations: []ftypes.Location{{StartLine: 129, EndLine: 136}}}, + {ID: "once@1.4.0", Name: "once", Version: "1.4.0", Locations: []ftypes.Location{{StartLine: 138, EndLine: 145}}}, + {ID: "wrappy@1.0.2", Name: "wrappy", Version: "1.0.2", Locations: []ftypes.Location{{StartLine: 147, EndLine: 152}}}, + } + + yarnV2HappyDeps = []ftypes.Dependency{ + { + ID: "ansi-styles@3.2.1", + DependsOn: []string{ + "color-convert@1.9.3", + }, + }, + { + ID: "ansi-styles@4.2.1", + DependsOn: []string{ + "@types/color-name@1.1.1", + "color-convert@2.0.1", + }, + }, + { + ID: "color-convert@1.9.3", + DependsOn: []string{ + "color-name@1.1.3", + }, + }, + { + ID: "color-convert@2.0.1", + DependsOn: []string{ + "color-name@1.1.4", + }, + }, + { + ID: "loose-envify@1.4.0", + DependsOn: []string{ + "js-tokens@4.0.0", + }, + }, + { + ID: "once@1.4.0", + DependsOn: []string{ + "wrappy@1.0.2", + }, + }, + } + + yarnWithLocal = []ftypes.Package{ + {ID: "asap@2.0.6", Name: "asap", Version: "2.0.6", Locations: []ftypes.Location{{StartLine: 5, EndLine: 8}}}, + {ID: "jquery@3.4.1", Name: "jquery", Version: "3.4.1", Locations: []ftypes.Location{{StartLine: 10, EndLine: 13}}}, + {ID: "promise@8.0.3", Name: "promise", Version: "8.0.3", Locations: []ftypes.Location{{StartLine: 15, EndLine: 20}}}, + } + + yarnWithLocalDeps = []ftypes.Dependency{ + { + ID: "promise@8.0.3", + DependsOn: []string{ + "asap@2.0.6", + }, + }, + } + + yarnWithNpm = []ftypes.Package{ + {ID: "jquery@3.6.0", Name: "jquery", Version: "3.6.0", Locations: []ftypes.Location{{StartLine: 1, EndLine: 4}}}, + } + + yarnBadProtocol = []ftypes.Package{ + {ID: "jquery@3.4.1", Name: "jquery", Version: "3.4.1", Locations: []ftypes.Location{{StartLine: 4, EndLine: 7}}}, + } + + yarnV2DepsWithProtocol = []ftypes.Package{ + {ID: "debug@4.3.4", Name: "debug", Version: "4.3.4", Locations: []ftypes.Location{{StartLine: 16, EndLine: 26}}}, + {ID: "ms@2.1.2", Name: "ms", Version: "2.1.2", Locations: []ftypes.Location{{StartLine: 28, EndLine: 33}}}, + } + + yarnV2DepsWithProtocolDeps = []ftypes.Dependency{ + { + ID: "debug@4.3.4", + DependsOn: []string{"ms@2.1.2"}, + }, + } +) diff --git a/pkg/dependency/parser/nodejs/yarn/testdata/yarn_happy.lock b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_happy.lock new file mode 100644 index 000000000000..582df0e3c377 --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_happy.lock @@ -0,0 +1,106 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + +"@babel/helper-regex@^7.0.0", "@babel/helper-regex@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/helper-regex/-/helper-regex-7.4.4.tgz#a47e02bc91fb259d2e6727c2a30013e3ac13c4a2" + integrity sha512-Y5nuB/kESmR3tKjU8Nkn1wMGEx1tjJX076HBMeL3XLQCu6vA/YRzuTW0bbb+qRnXvQGn+d6Rx953yffl8vEy7Q== + dependencies: + lodash "^4.17.11" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +inherits@2, inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jquery@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" + integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== + +lodash@4.17.11, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0: + version "4.17.11" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + +promise@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6" + integrity sha512-HeRDUL1RJiLhyA0/grn+PTShlBAcLuh/1BJGtrvjwbvRDCTLLMEz9rOGCV+R3vHY4MixIuoMEd9Yq/XvsTPcjw== + dependencies: + asap "~2.0.6" + +safe-buffer@5.1.2, safe-buffer@^5.0.1, safe-buffer@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= + +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + integrity sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8= + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +whatwg-fetch@>=0.10.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb" + integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q== + +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/yarn/testdata/yarn_v2_deps_with_protocol.lock b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_v2_deps_with_protocol.lock new file mode 100644 index 000000000000..b42fd4f3c5e3 --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_v2_deps_with_protocol.lock @@ -0,0 +1,33 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 8 + cacheKey: 10c0 + +"app@workspace:.": + version: 0.0.0-use.local + resolution: "app@workspace:." + dependencies: + debug: "npm:4.3.4" + languageName: unknown + linkType: soft + +"debug@npm:4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: cedbec45298dd5c501d01b92b119cd3faebe5438c3917ff11ae1bff86a6c722930ac9c8659792824013168ba6db7c4668225d845c633fbdafbbf902a6389f736 + languageName: node + linkType: hard + +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: a437714e2f90dbf881b5191d35a6db792efbca5badf112f87b9e1c712aace4b4b9b742dd6537f3edf90fd6f684de897cec230abde57e87883766712ddda297cc + languageName: node + linkType: hard diff --git a/pkg/dependency/parser/nodejs/yarn/testdata/yarn_v2_happy.lock b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_v2_happy.lock new file mode 100644 index 000000000000..d693d3085670 --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_v2_happy.lock @@ -0,0 +1,152 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 4 + cacheKey: 6 + +"@types/color-name@npm:^1.1.1": + version: 1.1.1 + resolution: "@types/color-name@npm:1.1.1" + checksum: 8311db94a9c4ecd247763b81e783ee49d87678b4ce6a7ee502e2bd5cea242b7357804a04855db009f713174bc654cc0c01c7303d40d757e5d710f5ac0368500f + languageName: node + linkType: hard + +"abbrev@npm:1": + version: 1.1.1 + resolution: "abbrev@npm:1.1.1" + checksum: 9f9236a3cc7f56c167be3aa81c77fcab2e08dfb8047b7861b91440f20b299b9442255856bdbe9d408d7e96a0b64a36e1c27384251126962490b4eee841b533e0 + languageName: node + linkType: hard + +"ansi-styles@npm:^3.2.0": + version: 3.2.1 + resolution: "ansi-styles@npm:3.2.1" + dependencies: + color-convert: ^1.9.0 + checksum: 456e1c23d9277512a47718da75e7fbb0a5ee215ef893c2f76d6b3efe8fceabc861121b80b0362146f5f995d21a1633f05a19bbf6283ae66ac11dc3b9c0bed779 + languageName: node + linkType: hard + +"ansi-styles@npm:^4.1.0": + version: 4.2.1 + resolution: "ansi-styles@npm:4.2.1" + dependencies: + "@types/color-name": ^1.1.1 + color-convert: ^2.0.1 + checksum: c8c007d5dab7b4fea064c9ea318114e1f6fc714bb382d061ac09e66bc83c8f3ce12bb6354be01598722c14a5d710af280b7614d269354f80d2535946aefa82f4 + languageName: node + linkType: hard + +"assert-plus@npm:1.0.0, assert-plus@npm:^1.0.0": + version: 1.0.0 + resolution: "assert-plus@npm:1.0.0" + checksum: 1bda24f67343ccb75a7eee31179c92cf9f79bd6f6bc24101b0ce1495ef979376dd9b0f9b9064812bba564cdade5fbf851ed76b4a44b5e141d49cdaee6ffed6b2 + languageName: node + linkType: hard + +"async@npm:^3.2.0": + version: 3.2.0 + resolution: "async@npm:3.2.0" + checksum: 5c7913c08496877a9896dc6670d3a6c64f02d350e74b9e9191194959c473414a0732539ebdfec0fd2f34c20f439714773a30c20e0e68eb27bd8ee5ec9d8ff5ba + languageName: node + linkType: hard + +"code@workspace:.": + version: 0.0.0-use.local + resolution: "code@workspace:." + dependencies: + async: ^3.2.0 + languageName: unknown + linkType: soft + +"color-convert@npm:^1.9.0": + version: 1.9.3 + resolution: "color-convert@npm:1.9.3" + dependencies: + color-name: 1.1.3 + checksum: 5f244daa3d1fe1f216d48878c550465067d15268688308554e613b7640a068f96588096d51f0b98b68f15d6ff6bb8ad24e172582ac8c0ad43fa4d3da60fd1b79 + languageName: node + linkType: hard + +"color-convert@npm:^2.0.1": + version: 2.0.1 + resolution: "color-convert@npm:2.0.1" + dependencies: + color-name: ~1.1.4 + checksum: 3d5d8a011a43012ca11b6d739049ecf2055d95582fd16ec44bf1e685eb0baa5cc652002be8a1dc92b429c8d87418287d0528266a7595cb1ad8a7f4f1d3049df2 + languageName: node + linkType: hard + +"color-name@npm:1.1.3": + version: 1.1.3 + resolution: "color-name@npm:1.1.3" + checksum: d8b91bb90aefc05b6ff568cf8889566dcc6269824df6f3c9b8ca842b18d7f4d089c07dc166808d33f22092d4a79167aa56a96a5ff0d21efab548bf44614db43b + languageName: node + linkType: hard + +"color-name@npm:~1.1.4": + version: 1.1.4 + resolution: "color-name@npm:1.1.4" + checksum: 3e1c9a4dee12eada307436f61614dd11fe300469db2b83f80c8b7a7cd8a1015f0f18dd13403f018927b249003777ff60baba4a03c65f12e6bddc0dfd9642021f + languageName: node + linkType: hard + +"fsevents@patch:fsevents@~2.1.2#builtin": + version: 2.1.3 + resolution: "fsevents@patch:fsevents@npm%3A2.1.3#builtin::version=2.1.3&hash=87eb42" + dependencies: + node-gyp: latest + checksum: d8ae862048fc127cdbd00d02b2feb7c25946c3ce4cbc44e958134be87e239577a16dafafa1c270d010b8624e1b1e0372e23f7865c55c6f83e83fc9f68b0a30d2 + languageName: node + linkType: hard + +"ipaddr.js@npm:1.9.1": + version: 1.9.1 + resolution: "ipaddr.js@npm:1.9.1" + checksum: de15bc7e63973d960abc43c9fbbf19589c726774f59d157d1b29382a1e86ae87c68cbd8b5c78a1712a87fc4fcd91e10762c7671950c66a1a19040ff4fd2f9c9b + languageName: node + linkType: hard + +"js-tokens@npm:^3.0.0 || ^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0" + checksum: 1fc4e4667ac2d972aba65148b9cbf9c17566b2394d3504238d8492bbd3e68f496c657eab06b26b40b17db5cac0a34d153a12130e2d2d2bb6dc2cdc8a4764eb1b + languageName: node + linkType: hard + +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": + version: 1.4.0 + resolution: "loose-envify@npm:1.4.0" + dependencies: + js-tokens: ^3.0.0 || ^4.0.0 + bin: + loose-envify: cli.js + checksum: 5c3b47bbe5f597a3889fb001a3a98aaea2a3fafa48089c19034de1e0121bf57dbee609d184478514d74d5c5a7e9cfa3d846343455e5123b060040d46c39e91dc + languageName: node + linkType: hard + +"node-gyp@npm:latest": + version: 7.1.0 + resolution: "node-gyp@npm:7.1.0" + bin: + node-gyp: bin/node-gyp.js + checksum: 78518a89047fdacb14c41586ce038584e21993f5c7ad31834c78cf06de0514fe4ef84a9034461695a10667bc81ee9ad8bc7d725cf951d4dfe1c0c175d763da59 + languageName: node + linkType: hard + +"once@npm:^1.3.0": + version: 1.4.0 + resolution: "once@npm:1.4.0" + dependencies: + wrappy: 1 + checksum: 57afc246536cf6494437f982b26475f22bee860f8b77ce8eb1543f42a8bffe04b2c66ddfea9a16cb25ccb80943f8ee4fc639367ef97b7a6a4f2672eb573963f5 + languageName: node + linkType: hard + +"wrappy@npm:1": + version: 1.0.2 + resolution: "wrappy@npm:1.0.2" + checksum: 519fcda0fcdf0c16327be2de9d98646742307bc830277e8868529fcf7566f2b330a6453c233e0cdcb767d5838dd61a90984a02ecc983bcddebea5ad0833bbf98 + languageName: node + linkType: hard \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_bad_protocol.lock b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_bad_protocol.lock new file mode 100644 index 000000000000..0dbcb99dde9b --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_bad_protocol.lock @@ -0,0 +1,7 @@ +asap@unsupported:~2.0.6: + version "2.0.6" + +jquery@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" + integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_git.lock b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_git.lock new file mode 100644 index 000000000000..171f64ed8b16 --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_git.lock @@ -0,0 +1,7 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"mapbox-gl-feature-drag-and-drop@git+https://github.com/jrd/mapbox-gl-feature-drag-and-drop.git": + version "1.1.1" + resolved "git+https://github.com/jrd/mapbox-gl-feature-drag-and-drop.git#538b1126e0d15210e91b4a34ffc223b4fec1ef87" \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_local.lock b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_local.lock new file mode 100644 index 000000000000..e1df9a7fa7d1 --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_local.lock @@ -0,0 +1,24 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +asap@~2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + +jquery@^3.4.1: + version "3.4.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.4.1.tgz#714f1f8d9dde4bdfa55764ba37ef214630d80ef2" + integrity sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw== + +promise@^8.0.3: + version "8.0.3" + resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6" + integrity sha512-HeRDUL1RJiLhyA0/grn+PTShlBAcLuh/1BJGtrvjwbvRDCTLLMEz9rOGCV+R3vHY4MixIuoMEd9Yq/XvsTPcjw== + dependencies: + asap "~2.0.6" + +"local-package@file:vendor/local-package.tar.gz": + version "0.0.1" + resolved "file:vendor/local-package.tar.gz#f0b5161c241e8c94acc6241c49e3ab6806e6d4d7" \ No newline at end of file diff --git a/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_npm.lock b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_npm.lock new file mode 100644 index 000000000000..936ea2a6ab40 --- /dev/null +++ b/pkg/dependency/parser/nodejs/yarn/testdata/yarn_with_npm.lock @@ -0,0 +1,4 @@ +"jquery@^3.6.0": + "integrity" "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" + "resolved" "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz" + "version" "3.6.0" \ No newline at end of file diff --git a/pkg/dependency/parser/nuget/config/parse.go b/pkg/dependency/parser/nuget/config/parse.go new file mode 100644 index 000000000000..4d0755141d86 --- /dev/null +++ b/pkg/dependency/parser/nuget/config/parse.go @@ -0,0 +1,52 @@ +package config + +import ( + "context" + "encoding/xml" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/utils" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type cfgPackageReference struct { + XMLName xml.Name `xml:"package"` + TargetFramework string `xml:"targetFramework,attr"` + Version string `xml:"version,attr"` + DevDependency bool `xml:"developmentDependency,attr"` + ID string `xml:"id,attr"` +} + +type config struct { + XMLName xml.Name `xml:"packages"` + Packages []cfgPackageReference `xml:"package"` +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var cfgData config + if err := xml.NewDecoder(r).Decode(&cfgData); err != nil { + return nil, nil, xerrors.Errorf("failed to decode .config file: %w", err) + } + + var pkgs []ftypes.Package + for _, pkg := range cfgData.Packages { + if pkg.ID == "" || pkg.DevDependency { + continue + } + + pkgs = append(pkgs, ftypes.Package{ + Name: pkg.ID, + Version: pkg.Version, + }) + } + + return utils.UniquePackages(pkgs), nil, nil +} diff --git a/pkg/dependency/parser/nuget/config/parse_test.go b/pkg/dependency/parser/nuget/config/parse_test.go new file mode 100644 index 000000000000..8038fc574b99 --- /dev/null +++ b/pkg/dependency/parser/nuget/config/parse_test.go @@ -0,0 +1,57 @@ +package config_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/nuget/config" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string // Test input file + inputFile string + want []ftypes.Package + wantErr string + }{ + { + name: "Config", + inputFile: "testdata/packages.config", + want: []ftypes.Package{ + {Name: "Newtonsoft.Json", Version: "6.0.4"}, + {Name: "Microsoft.AspNet.WebApi", Version: "5.2.2"}, + }, + }, + { + name: "with development dependency", + inputFile: "testdata/dev_dependency.config", + want: []ftypes.Package{ + {Name: "Newtonsoft.Json", Version: "8.0.3"}, + }, + }, + { + name: "sad path", + inputFile: "testdata/malformed_xml.config", + wantErr: "failed to decode .config file: XML syntax error on line 5: unexpected EOF", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + got, _, err := config.NewParser().Parse(t.Context(), f) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.ElementsMatch(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/nuget/config/testdata/dev_dependency.config b/pkg/dependency/parser/nuget/config/testdata/dev_dependency.config new file mode 100644 index 000000000000..49c08076e17a --- /dev/null +++ b/pkg/dependency/parser/nuget/config/testdata/dev_dependency.config @@ -0,0 +1,5 @@ + + + + + diff --git a/pkg/dependency/parser/nuget/config/testdata/malformed_xml.config b/pkg/dependency/parser/nuget/config/testdata/malformed_xml.config new file mode 100644 index 000000000000..6fdc75174fe9 --- /dev/null +++ b/pkg/dependency/parser/nuget/config/testdata/malformed_xml.config @@ -0,0 +1,4 @@ + + + + diff --git a/pkg/dependency/parser/nuget/config/testdata/packages.config b/pkg/dependency/parser/nuget/config/testdata/packages.config new file mode 100644 index 000000000000..51ea35d31a83 --- /dev/null +++ b/pkg/dependency/parser/nuget/config/testdata/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/pkg/dependency/parser/nuget/lock/parse.go b/pkg/dependency/parser/nuget/lock/parse.go new file mode 100644 index 000000000000..ab58c81a7521 --- /dev/null +++ b/pkg/dependency/parser/nuget/lock/parse.go @@ -0,0 +1,92 @@ +package lock + +import ( + "context" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/utils" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type LockFile struct { + Version int `json:"version"` + Targets map[string]Dependencies `json:"dependencies"` +} + +type Dependencies map[string]Dependency + +type Dependency struct { + Type string `json:"type"` + Resolved string `json:"resolved"` + Dependencies map[string]string `json:"dependencies,omitempty"` + xjson.Location +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockFile LockFile + if err := xjson.UnmarshalRead(r, &lockFile); err != nil { + return nil, nil, xerrors.Errorf("failed to decode packages.lock.json: %w", err) + } + + var pkgs []ftypes.Package + depsMap := make(map[string][]string) + for _, targetContent := range lockFile.Targets { + for packageName, packageContent := range targetContent { + // If package type is "project", it is the actual project, and we skip it. + if packageContent.Type == "Project" { + continue + } + + depId := packageID(packageName, packageContent.Resolved) + + pkg := ftypes.Package{ + ID: depId, + Name: packageName, + Version: packageContent.Resolved, + Relationship: lo.Ternary(packageContent.Type == "Direct", ftypes.RelationshipDirect, ftypes.RelationshipIndirect), + Locations: []ftypes.Location{ftypes.Location(packageContent.Location)}, + } + pkgs = append(pkgs, pkg) + + var dependsOn []string + + for depName := range packageContent.Dependencies { + dependsOn = append(dependsOn, packageID(depName, targetContent[depName].Resolved)) + } + + if savedDependsOn, ok := depsMap[depId]; ok { + dependsOn = lo.Uniq(append(dependsOn, savedDependsOn...)) + } + + if len(dependsOn) > 0 { + depsMap[depId] = dependsOn + } + } + } + + var deps []ftypes.Dependency + for depId, dependsOn := range depsMap { + dep := ftypes.Dependency{ + ID: depId, + DependsOn: dependsOn, + } + deps = append(deps, dep) + } + + return utils.UniquePackages(pkgs), deps, nil +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.NuGet, name, version) +} diff --git a/pkg/dependency/parser/nuget/lock/parse_test.go b/pkg/dependency/parser/nuget/lock/parse_test.go new file mode 100644 index 000000000000..fc374cf654cf --- /dev/null +++ b/pkg/dependency/parser/nuget/lock/parse_test.go @@ -0,0 +1,73 @@ +package lock + +import ( + "os" + "path" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + vectors := []struct { + file string // Test input file + want []ftypes.Package + wantDeps []ftypes.Dependency + }{ + { + file: "testdata/packages_lock_simple.json", + want: nuGetSimple, + wantDeps: nuGetSimpleDeps, + }, + { + file: "testdata/packages_lock_subdependencies.json", + want: nuGetSubDependencies, + wantDeps: nuGetSubDependenciesDeps, + }, + { + file: "testdata/packages_lock_multi.json", + want: nuGetMultiTarget, + wantDeps: nuGetMultiTargetDeps, + }, + { + file: "testdata/packages_lock_legacy.json", + want: nuGetLegacy, + wantDeps: nuGetLegacyDeps, + }, + } + + for _, v := range vectors { + t.Run(path.Base(v.file), func(t *testing.T) { + f, err := os.Open(v.file) + require.NoError(t, err) + + got, deps, err := NewParser().Parse(t.Context(), f) + require.NoError(t, err) + + sort.Sort(ftypes.Packages(got)) + sort.Sort(ftypes.Packages(v.want)) + + assert.Equal(t, v.want, got) + + if v.wantDeps != nil { + sortDeps(deps) + sortDeps(v.wantDeps) + assert.Equal(t, v.wantDeps, deps) + } + }) + } +} + +func sortDeps(deps []ftypes.Dependency) { + sort.Slice(deps, func(i, j int) bool { + return deps[i].ID < deps[j].ID + }) + + for i := range deps { + sort.Strings(deps[i].DependsOn) + } +} diff --git a/pkg/dependency/parser/nuget/lock/parse_testcase.go b/pkg/dependency/parser/nuget/lock/parse_testcase.go new file mode 100644 index 000000000000..d528e2d2a421 --- /dev/null +++ b/pkg/dependency/parser/nuget/lock/parse_testcase.go @@ -0,0 +1,1027 @@ +package lock + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +var ( + // docker run --rm -i -t mcr.microsoft.com/dotnet/sdk:latest + // apt -y update && apt -y install jq + // cd /usr/local/src + // dotnet new mvc + // dotnet add package Newtonsoft.Json + // dotnet add package NuGet.Frameworks + // dotnet restore --use-lock-file + // cat packages.lock.json | jq -rc '.dependencies[] | keys[] as $k | "{\"\($k)\", \"\(.[$k] | .resolved)\", \"\"},"' + nuGetSimple = []ftypes.Package{ + { + ID: "Newtonsoft.Json@12.0.3", + Name: "Newtonsoft.Json", + Version: "12.0.3", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + }, + { + ID: "NuGet.Frameworks@5.7.0", + Name: "NuGet.Frameworks", + Version: "5.7.0", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 11, + EndLine: 16, + }, + }, + }, + } + nuGetSimpleDeps []ftypes.Dependency + + // docker run --rm -i -t mcr.microsoft.com/dotnet/sdk:latest + // apt -y update && apt -y install jq + // cd /usr/local/src + // dotnet new webapi + // dotnet add package Newtonsoft.Json + // dotnet add package NuGet.Frameworks + // dotnet restore --use-lock-file + // cat packages.lock.json | jq -rc '.dependencies[] | keys[] as $k | "{\"\($k)\", \"\(.[$k] | .resolved)\", \"\"},"' + nuGetSubDependencies = []ftypes.Package{ + { + ID: "Microsoft.Extensions.ApiDescription.Server@3.0.0", + Name: "Microsoft.Extensions.ApiDescription.Server", + Version: "3.0.0", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 29, + EndLine: 33, + }, + }, + }, + { + ID: "Microsoft.OpenApi@1.1.4", + Name: "Microsoft.OpenApi", + Version: "1.1.4", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 34, + EndLine: 38, + }, + }, + }, + { + ID: "Newtonsoft.Json@12.0.3", + Name: "Newtonsoft.Json", + Version: "12.0.3", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + }, + { + ID: "NuGet.Frameworks@5.7.0", + Name: "NuGet.Frameworks", + Version: "5.7.0", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 11, + EndLine: 16, + }, + }, + }, + { + ID: "Swashbuckle.AspNetCore@5.5.1", + Name: "Swashbuckle.AspNetCore", + Version: "5.5.1", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 17, + EndLine: 28, + }, + }, + }, + { + ID: "Swashbuckle.AspNetCore.Swagger@5.5.1", + Name: "Swashbuckle.AspNetCore.Swagger", + Version: "5.5.1", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 39, + EndLine: 46, + }, + }, + }, + { + ID: "Swashbuckle.AspNetCore.SwaggerGen@5.5.1", + Name: "Swashbuckle.AspNetCore.SwaggerGen", + Version: "5.5.1", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 47, + EndLine: 54, + }, + }, + }, + { + ID: "Swashbuckle.AspNetCore.SwaggerUI@5.5.1", + Name: "Swashbuckle.AspNetCore.SwaggerUI", + Version: "5.5.1", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 55, + EndLine: 59, + }, + }, + }, + } + nuGetSubDependenciesDeps = []ftypes.Dependency{ + { + ID: "Swashbuckle.AspNetCore.Swagger@5.5.1", + DependsOn: []string{"Microsoft.OpenApi@1.1.4"}, + }, + { + ID: "Swashbuckle.AspNetCore.SwaggerGen@5.5.1", + DependsOn: []string{"Swashbuckle.AspNetCore.Swagger@5.5.1"}, + }, + { + ID: "Swashbuckle.AspNetCore@5.5.1", + DependsOn: []string{ + "Microsoft.Extensions.ApiDescription.Server@3.0.0", + "Swashbuckle.AspNetCore.Swagger@5.5.1", + "Swashbuckle.AspNetCore.SwaggerGen@5.5.1", + "Swashbuckle.AspNetCore.SwaggerUI@5.5.1", + }, + }, + } + + // mcr.microsoft.com/dotnet/sdk:latest + // apt -y update && apt -y install jq + // cd /usr/local/src + // dotnet new console + // dotnet add package Newtonsoft.Json + // dotnet add package AWSSDK.Core + // dotnet restore --use-lock-file + // cat packages.lock.json | jq -rc '.dependencies[] | keys[] as $k | "{\"\($k)\", \"\(.[$k] | .resolved)\", \"\"},"' + nuGetLegacy = []ftypes.Package{ + { + ID: "AWSSDK.Core@3.5.1.30", + Name: "AWSSDK.Core", + Version: "3.5.1.30", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + }, + { + ID: "Newtonsoft.Json@12.0.3", + Name: "Newtonsoft.Json", + Version: "12.0.3", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 11, + EndLine: 16, + }, + }, + }, + } + nuGetLegacyDeps []ftypes.Dependency + + // docker run --rm -i -t mcr.microsoft.com/dotnet/sdk:latest + // apt -y update && apt -y install jq + // cd /usr/local/src + // dotnet new classlib -f net5.0 + // sed -i 's~TargetFramework>net5.0net4.0;netstandard2.0;netstandard1.0;net35;net2.0 + + + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/nuget/packagesprops/testdata/no_item_group.props b/pkg/dependency/parser/nuget/packagesprops/testdata/no_item_group.props new file mode 100644 index 000000000000..aa3b28138667 --- /dev/null +++ b/pkg/dependency/parser/nuget/packagesprops/testdata/no_item_group.props @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/nuget/packagesprops/testdata/no_project.props b/pkg/dependency/parser/nuget/packagesprops/testdata/no_project.props new file mode 100644 index 000000000000..041498a6cc24 --- /dev/null +++ b/pkg/dependency/parser/nuget/packagesprops/testdata/no_project.props @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/pkg/dependency/parser/nuget/packagesprops/testdata/packages.props b/pkg/dependency/parser/nuget/packagesprops/testdata/packages.props new file mode 100644 index 000000000000..7ec6d02a1083 --- /dev/null +++ b/pkg/dependency/parser/nuget/packagesprops/testdata/packages.props @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/nuget/packagesprops/testdata/several_item_groups b/pkg/dependency/parser/nuget/packagesprops/testdata/several_item_groups new file mode 100644 index 000000000000..1575af4ae724 --- /dev/null +++ b/pkg/dependency/parser/nuget/packagesprops/testdata/several_item_groups @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/nuget/packagesprops/testdata/variables_and_empty b/pkg/dependency/parser/nuget/packagesprops/testdata/variables_and_empty new file mode 100644 index 000000000000..d801b1f723f1 --- /dev/null +++ b/pkg/dependency/parser/nuget/packagesprops/testdata/variables_and_empty @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/pkg/dependency/parser/php/composer/parse.go b/pkg/dependency/parser/php/composer/parse.go new file mode 100644 index 000000000000..b74f0d52858b --- /dev/null +++ b/pkg/dependency/parser/php/composer/parse.go @@ -0,0 +1,142 @@ +package composer + +import ( + "context" + "sort" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type LockFile struct { + Packages []packageInfo `json:"packages"` + PackagesDev []packageInfo `json:"packages-dev"` +} +type packageInfo struct { + Name string `json:"name"` + Version string `json:"version"` + Require map[string]string `json:"require"` + License any `json:"license"` + xjson.Location +} + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("composer"), + } +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockFile LockFile + if err := xjson.UnmarshalRead(r, &lockFile); err != nil { + return nil, nil, xerrors.Errorf("decode error: %w", err) + } + + pkgs := make(map[string]ftypes.Package) + foundDeps := make(map[string][]string) + + // Production packages are parsed first to ensure they take precedence + // when the same package exists in both "packages" and "packages-dev". + p.parseProdPackages(lockFile, pkgs, foundDeps) + p.parseDevPackages(lockFile, pkgs, foundDeps) + + // fill deps versions + var deps ftypes.Dependencies + for pkgID, depsOn := range foundDeps { + var dependsOn []string + for _, depName := range depsOn { + if pkg, ok := pkgs[depName]; ok { + dependsOn = append(dependsOn, pkg.ID) + continue + } + p.logger.Debug("Unable to find version", log.String("name", depName)) + } + sort.Strings(dependsOn) + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: dependsOn, + }) + } + + pkgSlice := lo.Values(pkgs) + sort.Sort(ftypes.Packages(pkgSlice)) + sort.Sort(deps) + + return pkgSlice, deps, nil +} + +// parseProdPackages parses packages from the "packages" field in composer.lock. +func (p *Parser) parseProdPackages(lockFile LockFile, pkgs map[string]ftypes.Package, foundDeps map[string][]string) { + p.parsePackages(lockFile.Packages, false, pkgs, foundDeps) +} + +// parseDevPackages parses packages from the "packages-dev" field in composer.lock. +// Packages already present in pkgs (i.e., production packages) are skipped. +func (p *Parser) parseDevPackages(lockFile LockFile, pkgs map[string]ftypes.Package, foundDeps map[string][]string) { + p.parsePackages(lockFile.PackagesDev, true, pkgs, foundDeps) +} + +func (p *Parser) parsePackages(lockPkgs []packageInfo, isDev bool, pkgs map[string]ftypes.Package, foundDeps map[string][]string) { + for _, lpkg := range lockPkgs { + // Skip if the package already exists (production packages take precedence over dev packages) + if _, ok := pkgs[lpkg.Name]; ok { + continue + } + + pkg := ftypes.Package{ + ID: dependency.ID(ftypes.Composer, lpkg.Name, lpkg.Version), + Name: lpkg.Name, + Version: lpkg.Version, + Relationship: ftypes.RelationshipUnknown, // composer.lock file doesn't have info about direct/indirect dependencies + Licenses: licenses(lpkg.License), + Locations: []ftypes.Location{ftypes.Location(lpkg.Location)}, + Dev: isDev, + } + pkgs[pkg.Name] = pkg + + var dependsOn []string + for depName := range lpkg.Require { + // Require field includes required php version, skip this + // Also skip PHP extensions + if depName == "php" || strings.HasPrefix(depName, "ext") { + continue + } + dependsOn = append(dependsOn, depName) // field uses range of versions, so later we will fill in the versions from the packages + } + if len(dependsOn) > 0 { + foundDeps[pkg.ID] = dependsOn + } + } +} + +// licenses returns slice of licenses from string, string with separators (`or`, `and`, etc.) or string array +// cf. https://getcomposer.org/doc/04-schema.md#license +func licenses(val any) []string { + switch v := val.(type) { + case string: + if v != "" { + return licensing.SplitLicenses(v) + } + case []any: + var lics []string + for _, l := range v { + if lic, ok := l.(string); ok { + lics = append(lics, lic) + } + } + return lics + } + return nil +} diff --git a/pkg/dependency/parser/php/composer/parse_test.go b/pkg/dependency/parser/php/composer/parse_test.go new file mode 100644 index 000000000000..5222cc1d5f0d --- /dev/null +++ b/pkg/dependency/parser/php/composer/parse_test.go @@ -0,0 +1,205 @@ +package composer + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + // docker run --name composer --rm -it composer@sha256:082ed124b68e7e880721772a6bf22ad809e3bc87db8bbee9f0ec7127bb21ccad bash + // apk add jq + // composer require guzzlehttp/guzzle:6.5.8 + // composer require pear/log:1.13.3 --dev + // composer show -i --no-dev -f json | jq --sort-keys -rc '.installed[] | "{ID: \"\(.name)@\(.version)\", Name: \"\(.name)\", Version: \"\(.version)\", License: \"MIT\", Locations: []ftypes.Location{{StartLine: , EndLine: }}},"' + // locations are filled manually + composerPkgs = []ftypes.Package{ + { + ID: "guzzlehttp/guzzle@6.5.8", + Name: "guzzlehttp/guzzle", + Version: "6.5.8", + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 9, + EndLine: 123, + }, + }, + }, + { + ID: "guzzlehttp/promises@1.5.2", + Name: "guzzlehttp/promises", + Version: "1.5.2", + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 124, + EndLine: 207, + }, + }, + }, + { + ID: "guzzlehttp/psr7@1.9.0", + Name: "guzzlehttp/psr7", + Version: "1.9.0", + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 208, + EndLine: 317, + }, + }, + }, + { + ID: "pear/log@1.13.3", + Name: "pear/log", + Version: "1.13.3", + Dev: true, + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 660, + EndLine: 719, + }, + }, + }, + { + ID: "pear/pear_exception@v1.0.2", + Name: "pear/pear_exception", + Version: "v1.0.2", + Dev: true, + Licenses: []string{"BSD-2-Clause"}, + Locations: []ftypes.Location{ + { + StartLine: 720, + EndLine: 778, + }, + }, + }, + { + ID: "psr/http-message@1.0.1", + Name: "psr/http-message", + Version: "1.0.1", + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 318, + EndLine: 370, + }, + }, + }, + { + ID: "ralouphie/getallheaders@3.0.3", + Name: "ralouphie/getallheaders", + Version: "3.0.3", + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 371, + EndLine: 414, + }, + }, + }, + { + ID: "symfony/polyfill-intl-idn@v1.27.0", + Name: "symfony/polyfill-intl-idn", + Version: "v1.27.0", + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 415, + EndLine: 501, + }, + }, + }, + { + ID: "symfony/polyfill-intl-normalizer@v1.27.0", + Name: "symfony/polyfill-intl-normalizer", + Version: "v1.27.0", + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 502, + EndLine: 583, + }, + }, + }, + { + ID: "symfony/polyfill-php72@v1.27.0", + Name: "symfony/polyfill-php72", + Version: "v1.27.0", + Licenses: []string{"MIT", "BSD-2-Clause"}, + Locations: []ftypes.Location{ + { + StartLine: 584, + EndLine: 657, + }, + }, + }, + } + // dependencies are filled manually + composerDeps = []ftypes.Dependency{ + { + ID: "guzzlehttp/guzzle@6.5.8", + DependsOn: []string{ + "guzzlehttp/promises@1.5.2", + "guzzlehttp/psr7@1.9.0", + "symfony/polyfill-intl-idn@v1.27.0", + }, + }, + { + ID: "guzzlehttp/psr7@1.9.0", + DependsOn: []string{ + "psr/http-message@1.0.1", + "ralouphie/getallheaders@3.0.3", + }, + }, + { + ID: "pear/log@1.13.3", + DependsOn: []string{ + "pear/pear_exception@v1.0.2", + }, + }, + { + ID: "symfony/polyfill-intl-idn@v1.27.0", + DependsOn: []string{ + "symfony/polyfill-intl-normalizer@v1.27.0", + "symfony/polyfill-php72@v1.27.0", + }, + }, + } +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + file string + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + }{ + { + name: "happy path", + file: "testdata/composer_happy.lock", + wantPkgs: composerPkgs, + wantDeps: composerDeps, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + defer f.Close() + + gotPkgs, gotDeps, err := NewParser().Parse(t.Context(), f) + require.NoError(t, err) + + assert.Equal(t, tt.wantPkgs, gotPkgs) + assert.Equal(t, tt.wantDeps, gotDeps) + }) + } +} diff --git a/pkg/dependency/parser/php/composer/testdata/composer_happy.lock b/pkg/dependency/parser/php/composer/testdata/composer_happy.lock new file mode 100644 index 000000000000..832f35d854d1 --- /dev/null +++ b/pkg/dependency/parser/php/composer/testdata/composer_happy.lock @@ -0,0 +1,788 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "99c95f97c9ab9a0045cd1f3b910a5b77", + "packages": [ + { + "name": "guzzlehttp/guzzle", + "version": "6.5.8", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^1.0", + "guzzlehttp/psr7": "^1.9", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17" + }, + "require-dev": { + "ext-curl": "*", + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.4 || ^7.0", + "psr/log": "^1.1" + }, + "suggest": { + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "6.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5.8" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-06-20T22:16:07+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "1.5.2", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "b94b2807d85443f9719887892882d0329d1e2598" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", + "reference": "b94b2807d85443f9719887892882d0329d1e2598", + "shasum": "" + }, + "require": { + "php": ">=5.5" + }, + "require-dev": { + "symfony/phpunit-bridge": "^4.4 || ^5.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.5-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2022-08-28T14:55:35+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "1.9.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "shasum": "" + }, + "require": { + "php": ">=5.4.0", + "psr/http-message": "~1.0", + "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "ext-zlib": "*", + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.9.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2022-06-20T21:43:03+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "639084e360537a19f9ee352433b84ce831f3d2da" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/639084e360537a19f9ee352433b84ce831f3d2da", + "reference": "639084e360537a19f9ee352433b84ce831f3d2da", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "reference": "19bd1e4fcd5b91116f14d8533c57831ed00571b6", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": "MIT", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.27.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "869329b1e9894268a8a61dabb69153029b7a8c97" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/869329b1e9894268a8a61dabb69153029b7a8c97", + "reference": "869329b1e9894268a8a61dabb69153029b7a8c97", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.27-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": "MIT or BSD-2-Clause", + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.27.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-11-03T14:55:06+00:00" + } + ], + "packages-dev": [ + { + "name": "pear/log", + "version": "1.13.3", + "source": { + "type": "git", + "url": "https://github.com/pear/Log.git", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Log/zipball/21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "shasum": "" + }, + "require": { + "pear/pear_exception": "1.0.1 || 1.0.2", + "php": ">5.2" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "pear/db": "Install optionally via your project's composer.json" + }, + "type": "library", + "autoload": { + "psr-0": { + "Log": "./" + }, + "exclude-from-classmap": [ + "/examples/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jon Parise", + "email": "jon@php.net", + "homepage": "http://www.indelible.org", + "role": "Developer" + } + ], + "description": "PEAR Logging Framework", + "homepage": "http://pear.github.io/Log/", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/pear/Log/issues", + "source": "https://github.com/pear/Log" + }, + "time": "2021-05-04T23:51:30+00:00" + }, + { + "name": "pear/pear_exception", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/pear/PEAR_Exception.git", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "<9" + }, + "type": "class", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "PEAR/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "." + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Helgi Thormar", + "email": "dufuz@php.net" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net" + } + ], + "description": "The PEAR Exception base class.", + "homepage": "https://github.com/pear/PEAR_Exception", + "keywords": [ + "exception" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", + "source": "https://github.com/pear/PEAR_Exception" + }, + "time": "2021-03-21T15:43:46+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} \ No newline at end of file diff --git a/pkg/dependency/parser/python/packaging/parse.go b/pkg/dependency/parser/python/packaging/parse.go new file mode 100644 index 000000000000..f8927f4f6221 --- /dev/null +++ b/pkg/dependency/parser/python/packaging/parse.go @@ -0,0 +1,100 @@ +package packaging + +import ( + "bufio" + "context" + "errors" + "io" + "net/textproto" + "strings" + "sync" + + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +var licenseMetadataInfoOnce sync.Once + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("python"), + } +} + +// Parse parses egg and wheel metadata. +// e.g. .egg-info/PKG-INFO and dist-info/METADATA +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + rd := textproto.NewReader(bufio.NewReader(r)) + h, err := rd.ReadMIMEHeader() + if e := textproto.ProtocolError(""); errors.As(err, &e) { + // A MIME header may contain bytes in the key or value outside the set allowed by RFC 7230. + // cf. https://cs.opensource.google/go/go/+/a6642e67e16b9d769a0c08e486ba08408064df19 + // However, our required key/value could have been correctly parsed, + // so we continue with the subsequent process. + p.logger.Debug("MIME protocol error", log.Err(err)) + } else if err != nil && err != io.EOF { + return nil, nil, xerrors.Errorf("read MIME error: %w", err) + } + + name, version := h.Get("name"), h.Get("version") + if name == "" || version == "" { + return nil, nil, xerrors.New("name or version is empty") + } + + // "License-Expression" takes precedence in accordance with https://peps.python.org/pep-0639/#deprecate-license-field + // Although keep in mind that pep-0639 is still in draft. + var license string + if le := h.Get("License-Expression"); le != "" { + license = le + } else { + // Get possible multiple occurrences of licenses from "Classifier: License" field + // When present it should define the license whereas "License" would define any additional exceptions or modifications + // ref. https://packaging.python.org/en/latest/specifications/core-metadata/#license + var licenses []string + for _, classifier := range h.Values("Classifier") { + if strings.HasPrefix(classifier, "License :: ") { + values := strings.Split(classifier, " :: ") + licenseName := values[len(values)-1] + // According to the classifier list https://pypi.org/classifiers/ there is one classifier which seems more like a grouping + // It has no specific license definition (Classifier: License :: OSI Approved) - it is skipped + if licenseName != "OSI Approved" { + licenses = append(licenses, licenseName) + } + } + } + license = strings.Join(licenses, ", ") + + if l := h.Get("License"); l != "" { + if len(licenses) != 0 { + licenseMetadataInfoOnce.Do(func() { + p.logger.Info("Licenses acquired from one or more METADATA files may be subject to additional terms. Use `--debug` flag to see all affected packages.") + }) + p.logger.Debug("License acquired from METADATA classifiers may be subject to additional terms", + log.String("name", name), log.String("version", version)) + } else { + license = l + } + } + + } + + if license == "" && h.Get("License-File") != "" { + license = licensing.LicenseFilePrefix + h.Get("License-File") + } + + return []ftypes.Package{ + { + Name: name, + Version: version, + Licenses: licensing.SplitLicenses(license), + }, + }, nil, nil +} diff --git a/pkg/dependency/parser/python/packaging/parse_test.go b/pkg/dependency/parser/python/packaging/parse_test.go new file mode 100644 index 000000000000..60c3ac9378b4 --- /dev/null +++ b/pkg/dependency/parser/python/packaging/parse_test.go @@ -0,0 +1,201 @@ +package packaging_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/packaging" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + input string + want []ftypes.Package + wantErr bool + }{ + // listing dependencies based on METADATA/PKG-INFO files + // docker run --name pipenv --rm -it python:3.7-alpine /bin/sh + // pip install pipenv + // find / -wholename "*(dist-info/METADATA|.egg-info/PKG-INFO)" | xargs -I {} sh -c 'cat {} | grep -e "^Name:" -e "^Version:" -e "^License:"' | tee METADATAS + // cat METADATAS | cut -d" " -f2- | tr "\n" "\t" | awk -F "\t" '{for(i=1;i<=NF;i=i+3){printf "\{\""$i"\", \""$(i+1)"\", \""$(i+2)"\"\}\n"}}' + + { + name: "egg PKG-INFO", + input: "testdata/setuptools-51.3.3-py3.8.egg-info.PKG-INFO", + + // docker run --name python --rm -it python:3.9-alpine sh + // apk add py3-setuptools + // cd /usr/lib/python3.9/site-packages/setuptools-52.0.0-py3.9.egg-info/ + // cat PKG-INFO | grep -e "^Name:" -e "^Version:" -e "^License:" | cut -d" " -f2- | \ + // tr "\n" "\t" | awk -F "\t" '{printf("\{\""$1"\", \""$2"\", \""$3"\"\}\n")}' + want: []ftypes.Package{ + { + Name: "setuptools", + Version: "51.3.3", + Licenses: []string{ + "UNKNOWN", + }, + }, + }, + }, + { + name: "egg PKG-INFO with description containing non-RFC 7230 bytes", + input: "testdata/unidecode-egg-info.PKG-INFO", + want: []ftypes.Package{ + { + Name: "Unidecode", + Version: "0.4.1", + Licenses: []string{ + "UNKNOWN", + }, + }, + }, + }, + { + name: "egg-info", + input: "testdata/distlib-0.3.1-py3.9.egg-info", + + // docker run --name python --rm -it python:3.9-alpine sh + // apk add py3-distlib + // cd /usr/lib/python3.9/site-packages/ + // cat distlib-0.3.1-py3.9.egg-info | grep -e "^Name:" -e "^Version:" -e "^License:" | cut -d" " -f2- | \ + // tr "\n" "\t" | awk -F "\t" '{printf("\{\""$1"\", \""$2"\", \""$3"\"\}\n")}' + want: []ftypes.Package{ + { + Name: "distlib", + Version: "0.3.1", + Licenses: []string{ + "Python license", + }, + }, + }, + }, + { + name: "wheel METADATA", + input: "testdata/simple-0.1.0.METADATA", + + // finding relevant metadata files for tests + // mkdir dist-infos + // find / -wholename "*dist-info/METADATA" | rev | cut -d '/' -f2- | rev | xargs -I % cp -r % dist-infos/ + // find dist-infos/ | grep -v METADATA | xargs rm -R + + // for single METADATA file with known name + // cat "{{ libname }}.METADATA | grep -e "^Name:" -e "^Version:" -e "^Licenses: []string{" | cut -d" " -f2- | tr "\n" "\t" | awk -F "\t" '{printf("\{\""$1"\"}, \""$2"\", \""$3"\"\}\n")}' + want: []ftypes.Package{ + { + Name: "simple", + Version: "0.1.0", + Licenses: nil, + }, + }, + }, + { + name: "wheel METADATA", + + // for single METADATA file with known name + // cat "{{ libname }}.METADATA | grep -e "^Name:" -e "^Version:" -e "^Licenses: []string{" | cut -d" " -f2- | tr "\n" "\t" | awk -F "\t" '{printf("\{\""$1"\"}, \""$2"\", \""$3"\"\}\n")}' + input: "testdata/distlib-0.3.1.METADATA", + want: []ftypes.Package{ + { + Name: "distlib", + Version: "0.3.1", + Licenses: []string{ + "Python Software Foundation License", + }, + }, + }, + }, + { + name: "wheel METADATA", + // Input defines "Classifier: License" but it ends at "OSI Approved" which doesn't define any specific license, thus "License" field is added to results + input: "testdata/asyncssh-2.14.2.METADATA", + + want: []ftypes.Package{ + { + Name: "asyncssh", + Version: "2.14.2", + Licenses: []string{ + "Eclipse Public License v2.0", + }, + }, + }, + }, + { + name: "wheel METADATA", + // Input defines multiple "Classifier: License" + input: "testdata/pyphen-0.14.0.METADATA", + + want: []ftypes.Package{ + { + Name: "pyphen", + Version: "0.14.0", + Licenses: []string{ + "GNU General Public License v2 or later (GPLv2+)", + "GNU Lesser General Public License v2 or later (LGPLv2+)", + "Mozilla Public License 1.1 (MPL 1.1)", + }, + }, + }, + }, + { + name: "invalid", + input: "testdata/invalid.json", + wantErr: true, + }, + { + name: "with License-Expression field", + input: "testdata/iniconfig-2.0.0.METADATA", + want: []ftypes.Package{ + { + Name: "iniconfig", + Version: "2.0.0", + Licenses: []string{ + "MIT", + }, + }, + }, + }, + { + name: "with an empty license field but with license in Classifier", + input: "testdata/zipp-3.12.1.METADATA", + want: []ftypes.Package{ + { + Name: "zipp", + Version: "3.12.1", + Licenses: []string{ + "MIT License", + }, + }, + }, + }, + { + name: "without licenses, but with a license file (a license in Classifier was removed)", + input: "testdata/networkx-3.0.METADATA", + want: []ftypes.Package{ + { + Name: "networkx", + Version: "3.0", + Licenses: []string{ + "file://LICENSE.txt", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.input) + require.NoError(t, err) + + got, _, err := packaging.NewParser().Parse(t.Context(), f) + require.Equal(t, tt.wantErr, err != nil) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/python/packaging/testdata/asyncssh-2.14.2.METADATA b/pkg/dependency/parser/python/packaging/testdata/asyncssh-2.14.2.METADATA new file mode 100644 index 000000000000..4896c3f45c2e --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/asyncssh-2.14.2.METADATA @@ -0,0 +1,46 @@ +Metadata-Version: 2.1 +Name: asyncssh +Version: 2.14.2 +Summary: AsyncSSH: Asynchronous SSHv2 client and server library +Home-page: http://asyncssh.timeheart.net +Author: Ron Frederick +Author-email: ronf@timeheart.net +License: Eclipse Public License v2.0 +Project-URL: Documentation, https://asyncssh.readthedocs.io +Project-URL: Source, https://github.com/ronf/asyncssh +Project-URL: Tracker, https://github.com/ronf/asyncssh/issues +Platform: Any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Internet +Classifier: Topic :: Security :: Cryptography +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Networking +Requires-Python: >= 3.6 +License-File: LICENSE +Requires-Dist: cryptography (>=39.0) +Requires-Dist: typing-extensions (>=3.6) +Provides-Extra: bcrypt +Requires-Dist: bcrypt (>=3.1.3) ; extra == 'bcrypt' +Provides-Extra: fido2 +Requires-Dist: fido2 (>=0.9.2) ; extra == 'fido2' +Provides-Extra: gssapi +Requires-Dist: gssapi (>=1.2.0) ; extra == 'gssapi' +Provides-Extra: libnacl +Requires-Dist: libnacl (>=1.4.2) ; extra == 'libnacl' +Provides-Extra: pkcs11 +Requires-Dist: python-pkcs11 (>=0.7.0) ; extra == 'pkcs11' +Provides-Extra: pyopenssl +Requires-Dist: pyOpenSSL (>=23.0.0) ; extra == 'pyopenssl' +Provides-Extra: pywin32 +Requires-Dist: pywin32 (>=227) ; extra == 'pywin32' diff --git a/pkg/dependency/parser/python/packaging/testdata/distlib-0.3.1-py3.9.egg-info b/pkg/dependency/parser/python/packaging/testdata/distlib-0.3.1-py3.9.egg-info new file mode 100644 index 000000000000..95c76f43e0dd --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/distlib-0.3.1-py3.9.egg-info @@ -0,0 +1,30 @@ +Metadata-Version: 1.1 +Name: distlib +Version: 0.3.1 +Summary: Distribution utilities +Home-page: https://bitbucket.org/pypa/distlib +Author: Vinay Sajip +Author-email: vinay_sajip@red-dove.com +License: Python license +Download-URL: https://bitbucket.org/pypa/distlib/downloads/distlib-0.3.1.zip +Description: Low-level components of distutils2/packaging, augmented with higher-level APIs for making packaging easier. + +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Topic :: Software Development +Classifier: Topic :: Utilities diff --git a/pkg/dependency/parser/python/packaging/testdata/distlib-0.3.1.METADATA b/pkg/dependency/parser/python/packaging/testdata/distlib-0.3.1.METADATA new file mode 100644 index 000000000000..54f5f6497f32 --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/distlib-0.3.1.METADATA @@ -0,0 +1,24 @@ +Metadata-Version: 1.1 +Name: distlib +Version: 0.3.1 +Summary: Distribution utilities +Description: Low-level components of distutils2/packaging, augmented with higher-level APIs for making packaging easier. +Home-page: https://bitbucket.org/pypa/distlib +Author: Vinay Sajip +Author-email: vinay_sajip@red-dove.com +License: Python license +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Download-URL: https://bitbucket.org/pypa/distlib/downloads/distlib-0.3.1.zip diff --git a/pkg/dependency/parser/python/packaging/testdata/iniconfig-2.0.0.METADATA b/pkg/dependency/parser/python/packaging/testdata/iniconfig-2.0.0.METADATA new file mode 100644 index 000000000000..3ea1e01cb025 --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/iniconfig-2.0.0.METADATA @@ -0,0 +1,80 @@ +Metadata-Version: 2.1 +Name: iniconfig +Version: 2.0.0 +Summary: brain-dead simple config-ini parsing +Project-URL: Homepage, https://github.com/pytest-dev/iniconfig +Author-email: Ronny Pfannschmidt , Holger Krekel +License-Expression: MIT +License-File: LICENSE +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst + +iniconfig: brain-dead simple parsing of ini files +======================================================= + +iniconfig is a small and simple INI-file parser module +having a unique set of features: + +* maintains order of sections and entries +* supports multi-line values with or without line-continuations +* supports "#" comments everywhere +* raises errors with proper line-numbers +* no bells and whistles like automatic substitutions +* iniconfig raises an Error if two sections have the same name. + +If you encounter issues or have feature wishes please report them to: + + https://github.com/RonnyPfannschmidt/iniconfig/issues + +Basic Example +=================================== + +If you have an ini file like this: + +.. code-block:: ini + + # content of example.ini + [section1] # comment + name1=value1 # comment + name1b=value1,value2 # comment + + [section2] + name2= + line1 + line2 + +then you can do: + +.. code-block:: pycon + + >>> import iniconfig + >>> ini = iniconfig.IniConfig("example.ini") + >>> ini['section1']['name1'] # raises KeyError if not exists + 'value1' + >>> ini.get('section1', 'name1b', [], lambda x: x.split(",")) + ['value1', 'value2'] + >>> ini.get('section1', 'notexist', [], lambda x: x.split(",")) + [] + >>> [x.name for x in list(ini)] + ['section1', 'section2'] + >>> list(list(ini)[0].items()) + [('name1', 'value1'), ('name1b', 'value1,value2')] + >>> 'section1' in ini + True + >>> 'inexistendsection' in ini + False diff --git a/pkg/dependency/parser/python/packaging/testdata/invalid.json b/pkg/dependency/parser/python/packaging/testdata/invalid.json new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/invalid.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkg/dependency/parser/python/packaging/testdata/networkx-3.0.METADATA b/pkg/dependency/parser/python/packaging/testdata/networkx-3.0.METADATA new file mode 100644 index 000000000000..ad434a2057fc --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/networkx-3.0.METADATA @@ -0,0 +1,132 @@ +Metadata-Version: 2.1 +Name: networkx +Version: 3.0 +Summary: Python package for creating and manipulating graphs and networks +Home-page: https://networkx.org/ +Author: Aric Hagberg +Author-email: hagberg@lanl.gov +Maintainer: NetworkX Developers +Maintainer-email: networkx-discuss@googlegroups.com +Project-URL: Bug Tracker, https://github.com/networkx/networkx/issues +Project-URL: Documentation, https://networkx.org/documentation/stable/ +Project-URL: Source Code, https://github.com/networkx/networkx +Keywords: Networks,Graph Theory,Mathematics,network,graph,discrete mathematics,math +Platform: Linux +Platform: Mac OSX +Platform: Windows +Platform: Unix +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Science/Research +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Scientific/Engineering :: Bio-Informatics +Classifier: Topic :: Scientific/Engineering :: Information Analysis +Classifier: Topic :: Scientific/Engineering :: Mathematics +Classifier: Topic :: Scientific/Engineering :: Physics +Requires-Python: >=3.8 +License-File: LICENSE.txt +Provides-Extra: default +Requires-Dist: numpy (>=1.20) ; extra == 'default' +Requires-Dist: scipy (>=1.8) ; extra == 'default' +Requires-Dist: matplotlib (>=3.4) ; extra == 'default' +Requires-Dist: pandas (>=1.3) ; extra == 'default' +Provides-Extra: developer +Requires-Dist: pre-commit (>=2.20) ; extra == 'developer' +Requires-Dist: mypy (>=0.991) ; extra == 'developer' +Provides-Extra: doc +Requires-Dist: sphinx (==5.2.3) ; extra == 'doc' +Requires-Dist: pydata-sphinx-theme (>=0.11) ; extra == 'doc' +Requires-Dist: sphinx-gallery (>=0.11) ; extra == 'doc' +Requires-Dist: numpydoc (>=1.5) ; extra == 'doc' +Requires-Dist: pillow (>=9.2) ; extra == 'doc' +Requires-Dist: nb2plots (>=0.6) ; extra == 'doc' +Requires-Dist: texext (>=0.6.7) ; extra == 'doc' +Provides-Extra: extra +Requires-Dist: lxml (>=4.6) ; extra == 'extra' +Requires-Dist: pygraphviz (>=1.10) ; extra == 'extra' +Requires-Dist: pydot (>=1.4.2) ; extra == 'extra' +Requires-Dist: sympy (>=1.10) ; extra == 'extra' +Provides-Extra: test +Requires-Dist: pytest (>=7.2) ; extra == 'test' +Requires-Dist: pytest-cov (>=4.0) ; extra == 'test' +Requires-Dist: codecov (>=2.1) ; extra == 'test' + +NetworkX +======== + +.. image:: https://github.com/networkx/networkx/workflows/test/badge.svg?tag=networkx-3.0 + :target: https://github.com/networkx/networkx/actions?query=branch%3Anetworkx-3.0 + +.. image:: https://codecov.io/gh/networkx/networkx/branch/main/graph/badge.svg + :target: https://app.codecov.io/gh/networkx/networkx/branch/main + +.. image:: https://img.shields.io/github/labels/networkx/networkx/Good%20First%20Issue?color=green&label=Contribute%20&style=flat-square + :target: https://github.com/networkx/networkx/issues?q=is%3Aopen+is%3Aissue+label%3A%22Good+First+Issue%22 + + +NetworkX is a Python package for the creation, manipulation, +and study of the structure, dynamics, and functions +of complex networks. + +- **Website (including documentation):** https://networkx.org +- **Mailing list:** https://groups.google.com/forum/#!forum/networkx-discuss +- **Source:** https://github.com/networkx/networkx +- **Bug reports:** https://github.com/networkx/networkx/issues +- **Report a security vulnerability:** https://tidelift.com/security +- **Tutorial:** https://networkx.org/documentation/latest/tutorial.html +- **GitHub Discussions:** https://github.com/networkx/networkx/discussions + +Simple example +-------------- + +Find the shortest path between two nodes in an undirected graph: + +.. code:: pycon + + >>> import networkx as nx + >>> G = nx.Graph() + >>> G.add_edge("A", "B", weight=4) + >>> G.add_edge("B", "D", weight=2) + >>> G.add_edge("A", "C", weight=3) + >>> G.add_edge("C", "D", weight=4) + >>> nx.shortest_path(G, "A", "D", weight="weight") + ['A', 'B', 'D'] + +Install +------- + +Install the latest version of NetworkX:: + + $ pip install networkx + +Install with all optional dependencies:: + + $ pip install networkx[all] + +For additional details, please see `INSTALL.rst`. + +Bugs +---- + +Please report any bugs that you find `here `_. +Or, even better, fork the repository on `GitHub `_ +and create a pull request (PR). We welcome all changes, big or small, and we +will help you make the PR if you are new to `git` (just ask on the issue and/or +see `CONTRIBUTING.rst`). + +License +------- + +Released under the 3-Clause BSD license (see `LICENSE.txt`):: + + Copyright (C) 2004-2023 NetworkX Developers + Aric Hagberg + Dan Schult + Pieter Swart diff --git a/pkg/dependency/parser/python/packaging/testdata/pyphen-0.14.0.METADATA b/pkg/dependency/parser/python/packaging/testdata/pyphen-0.14.0.METADATA new file mode 100644 index 000000000000..9a84dcd15884 --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/pyphen-0.14.0.METADATA @@ -0,0 +1,38 @@ +Metadata-Version: 2.1 +Name: pyphen +Version: 0.14.0 +Summary: Pure Python module to hyphenate text +Keywords: hyphenation +Author-email: Guillaume Ayoub +Maintainer-email: CourtBouillon +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+) +Classifier: License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+) +Classifier: License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1) +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Text Processing +Classifier: Topic :: Text Processing :: Linguistic +Requires-Dist: sphinx ; extra == "doc" +Requires-Dist: sphinx_rtd_theme ; extra == "doc" +Requires-Dist: pytest ; extra == "test" +Requires-Dist: isort ; extra == "test" +Requires-Dist: flake8 ; extra == "test" +Project-URL: Changelog, https://github.com/Kozea/Pyphen/releases +Project-URL: Code, https://github.com/Kozea/Pyphen +Project-URL: Documentation, https://pyphen.org/ +Project-URL: Donation, https://opencollective.com/courtbouillon +Project-URL: Homepage, https://www.courtbouillon.org/pyphen +Project-URL: Issues, https://github.com/Kozea/Pyphen/issues +Provides-Extra: doc +Provides-Extra: test diff --git a/pkg/dependency/parser/python/packaging/testdata/setuptools-51.3.3-py3.8.egg-info.PKG-INFO b/pkg/dependency/parser/python/packaging/testdata/setuptools-51.3.3-py3.8.egg-info.PKG-INFO new file mode 100644 index 000000000000..f5341300a798 --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/setuptools-51.3.3-py3.8.egg-info.PKG-INFO @@ -0,0 +1,87 @@ +Metadata-Version: 2.1 +Name: setuptools +Version: 51.3.3 +Summary: Easily download, build, install, upgrade, and uninstall Python packages +Home-page: https://github.com/pypa/setuptools +Author: Python Packaging Authority +Author-email: distutils-sig@python.org +License: UNKNOWN +Project-URL: Documentation, https://setuptools.readthedocs.io/ +Description: .. image:: https://img.shields.io/pypi/v/setuptools.svg + :target: `PyPI link`_ + + .. image:: https://img.shields.io/pypi/pyversions/setuptools.svg + :target: `PyPI link`_ + + .. _PyPI link: https://pypi.org/project/setuptools + + .. image:: https://github.com/pypa/setuptools/workflows/tests/badge.svg + :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22 + :alt: tests + + .. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + + .. image:: https://img.shields.io/readthedocs/setuptools/latest.svg + :target: https://setuptools.readthedocs.io + + .. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white + :target: https://codecov.io/gh/pypa/setuptools + + .. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme + + See the `Installation Instructions + `_ in the Python Packaging + User's Guide for instructions on installing, upgrading, and uninstalling + Setuptools. + + Questions and comments should be directed to the `distutils-sig + mailing list `_. + Bug reports and especially tested patches may be + submitted directly to the `bug tracker + `_. + + + Code of Conduct + =============== + + Everyone interacting in the setuptools project's codebases, issue trackers, + chat rooms, and mailing lists is expected to follow the + `PSF Code of Conduct `_. + + + For Enterprise + ============== + + Available as part of the Tidelift Subscription. + + Setuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + + `Learn more `_. + + + Security Contact + ================ + + To report a security vulnerability, please use the + `Tidelift security contact `_. + Tidelift will coordinate the fix and disclosure. + +Keywords: CPAN PyPI distutils eggs package management +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Archiving :: Packaging +Classifier: Topic :: System :: Systems Administration +Classifier: Topic :: Utilities +Requires-Python: >=3.6 +Provides-Extra: testing +Provides-Extra: docs +Provides-Extra: ssl +Provides-Extra: certs \ No newline at end of file diff --git a/pkg/dependency/parser/python/packaging/testdata/simple-0.1.0.METADATA b/pkg/dependency/parser/python/packaging/testdata/simple-0.1.0.METADATA new file mode 100644 index 000000000000..a92f79bf844d --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/simple-0.1.0.METADATA @@ -0,0 +1,3 @@ +Metadata-Version: 2.1 +Name: simple +Version: 0.1.0 diff --git a/pkg/dependency/parser/python/packaging/testdata/unidecode-egg-info.PKG-INFO b/pkg/dependency/parser/python/packaging/testdata/unidecode-egg-info.PKG-INFO new file mode 100644 index 000000000000..80e0762c5173 --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/unidecode-egg-info.PKG-INFO @@ -0,0 +1,47 @@ +Metadata-Version: 1.1 +Name: Unidecode +Version: 0.4.1 +Summary: US-ASCII transliterations of Unicode text +Home-page: http://www.tablix.org/~avian/blog/archives/2009/01/unicode_transliteration_in_python/ +Author: Tomaz Solc +Author-email: tomaz.solc@tablix.org +License: UNKNOWN +Description: + Unidecode + ========= + + ASCII transliterations of Unicode text + + Example Use + ----------- + + :: + + from unidecode import unidecode + print unidecode(u"北亰") + + # That prints: Bei Jing + + Description + ----------- + + It often happens that you have non-Roman text data in Unicode, but + you can't display it -- usually because you're trying to show it + to a user via an application that doesn't support Unicode, or + because the fonts you need aren't accessible. You could represent + the Unicode characters as "???????" or " BA A0q0...", but + that's nearly useless to the user who actually wants to read what + the text says. + + What Unidecode provides is a function, 'unidecode(...)' that + takes Unicode data and tries to represent it in ASCII characters + (i.e., the universally displayable characters between 0x00 and 0x7F). + The representation is almost always an attempt at *transliteration* + -- i.e., conveying, in Roman letters, the pronunciation expressed by + the text in some other writing system. (See the example above) + + This is a Python port of Text::Unidecode Perl module by + Sean M. Burke . + +Platform: UNKNOWN +Provides: unidecode diff --git a/pkg/dependency/parser/python/packaging/testdata/zipp-3.12.1.METADATA b/pkg/dependency/parser/python/packaging/testdata/zipp-3.12.1.METADATA new file mode 100644 index 000000000000..142b8d071568 --- /dev/null +++ b/pkg/dependency/parser/python/packaging/testdata/zipp-3.12.1.METADATA @@ -0,0 +1,106 @@ +Metadata-Version: 2.1 +Name: zipp +Version: 3.12.1 +Summary: Backport of pathlib-compatible object wrapper for zip files +Home-page: https://github.com/jaraco/zipp +Author: Jason R. Coombs +Author-email: jaraco@jaraco.com +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Requires-Python: >=3.7 +License-File: LICENSE +Provides-Extra: docs +Requires-Dist: sphinx (>=3.5) ; extra == 'docs' +Requires-Dist: jaraco.packaging (>=9) ; extra == 'docs' +Requires-Dist: rst.linker (>=1.9) ; extra == 'docs' +Requires-Dist: furo ; extra == 'docs' +Requires-Dist: sphinx-lint ; extra == 'docs' +Requires-Dist: jaraco.tidelift (>=1.4) ; extra == 'docs' +Provides-Extra: testing +Requires-Dist: pytest (>=6) ; extra == 'testing' +Requires-Dist: pytest-checkdocs (>=2.4) ; extra == 'testing' +Requires-Dist: flake8 (<5) ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Requires-Dist: pytest-enabler (>=1.3) ; extra == 'testing' +Requires-Dist: jaraco.itertools ; extra == 'testing' +Requires-Dist: func-timeout ; extra == 'testing' +Requires-Dist: jaraco.functools ; extra == 'testing' +Requires-Dist: more-itertools ; extra == 'testing' +Requires-Dist: pytest-black (>=0.3.7) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-mypy (>=0.9.1) ; (platform_python_implementation != "PyPy") and extra == 'testing' +Requires-Dist: pytest-flake8 ; (python_version < "3.12") and extra == 'testing' + +.. image:: https://img.shields.io/pypi/v/zipp.svg + :target: https://pypi.org/project/zipp + +.. image:: https://img.shields.io/pypi/pyversions/zipp.svg + +.. image:: https://github.com/jaraco/zipp/workflows/tests/badge.svg + :target: https://github.com/jaraco/zipp/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest +.. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest + +.. image:: https://img.shields.io/badge/skeleton-2023-informational + :target: https://blog.jaraco.com/skeleton + +.. image:: https://tidelift.com/badges/package/pypi/zipp + :target: https://tidelift.com/subscription/pkg/pypi-zipp?utm_source=pypi-zipp&utm_medium=readme + + +A pathlib-compatible Zipfile object wrapper. Official backport of the standard library +`Path object `_. + + +Compatibility +============= + +New features are introduced in this third-party library and later merged +into CPython. The following table indicates which versions of this library +were contributed to different versions in the standard library: + +.. list-table:: + :header-rows: 1 + + * - zipp + - stdlib + * - 3.9 + - 3.12 + * - 3.5 + - 3.11 + * - 3.2 + - 3.10 + * - 3.3 ?? + - 3.9 + * - 1.0 + - 3.8 + + +Usage +===== + +Use ``zipp.Path`` in place of ``zipfile.Path`` on any Python. + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. + +Security Contact +================ + +To report a security vulnerability, please use the +`Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. \ No newline at end of file diff --git a/pkg/dependency/parser/python/pip/parse.go b/pkg/dependency/parser/python/pip/parse.go new file mode 100644 index 000000000000..6bffa5abee8f --- /dev/null +++ b/pkg/dependency/parser/python/pip/parse.go @@ -0,0 +1,137 @@ +package pip + +import ( + "bufio" + "context" + "strings" + "unicode" + + "golang.org/x/text/encoding" + u "golang.org/x/text/encoding/unicode" + "golang.org/x/text/transform" + "golang.org/x/xerrors" + + version "github.com/aquasecurity/go-pep440-version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +const ( + commentMarker string = "#" + endColon string = ";" + hashMarker string = "--" + startExtras string = "[" + endExtras string = "]" +) + +type Parser struct { + logger *log.Logger + useMinVersion bool +} + +func NewParser(useMinVersion bool) *Parser { + return &Parser{ + logger: log.WithPrefix("pip"), + useMinVersion: useMinVersion, + } +} +func (p *Parser) splitLine(line string) []string { + separators := []string{"~=", ">=", "=="} + // Without useMinVersion check only `==` + if !p.useMinVersion { + separators = []string{"=="} + } + for _, sep := range separators { + if result := strings.Split(line, sep); len(result) == 2 { + // Trim the end-of-range suffix. For example, ">=2.31.0,<3" becomes ">=2.31.0". + // In version ranges, only `,` can be used as a separator: + // "The comma (“,”) is equivalent to a logical AND operator" (see PEP 440). + result[1], _, _ = strings.Cut(result[1], ",") + return result + } + } + return nil +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + // `requirements.txt` can use byte order marks (BOM) + // e.g. on Windows `requirements.txt` can use UTF-16LE with BOM + // We need to override them to avoid the file being read incorrectly + var transformer = u.BOMOverride(encoding.Nop.NewDecoder()) + decodedReader := transform.NewReader(r, transformer) + + scanner := bufio.NewScanner(decodedReader) + var pkgs []ftypes.Package + var lineNumber int + for scanner.Scan() { + lineNumber++ + text := scanner.Text() + line := strings.ReplaceAll(text, " ", "") + line = strings.ReplaceAll(line, `\`, "") + line = removeExtras(line) + line = rStripByKey(line, commentMarker) + line = rStripByKey(line, endColon) + line = rStripByKey(line, hashMarker) + + s := p.splitLine(line) + if len(s) != 2 { + continue + } + if p.useMinVersion && strings.HasSuffix(s[1], ".*") { + s[1] = strings.TrimSuffix(s[1], "*") + "0" + } + + if !isValidName(s[0]) || !isValidVersion(s[1]) { + p.logger.Debug("Invalid package name/version in requirements.txt.", log.String("line", text)) + continue + } + + pkgs = append(pkgs, ftypes.Package{ + Name: s[0], + Version: s[1], + Locations: []ftypes.Location{ + { + StartLine: lineNumber, + EndLine: lineNumber, + }, + }, + }) + } + if err := scanner.Err(); err != nil { + return nil, nil, xerrors.Errorf("scan error: %w", err) + } + return pkgs, nil, nil +} + +func rStripByKey(line, key string) string { + if pos := strings.Index(line, key); pos >= 0 { + line = strings.TrimRightFunc((line)[:pos], unicode.IsSpace) + } + return line +} + +func removeExtras(line string) string { + startIndex := strings.Index(line, startExtras) + endIndex := strings.Index(line, endExtras) + 1 + if startIndex != -1 && endIndex != -1 { + line = line[:startIndex] + line[endIndex:] + } + return line +} + +func isValidName(name string) bool { + for _, r := range name { + // only characters [A-Z0-9._-] are allowed (case insensitive) + // cf. https://peps.python.org/pep-0508/#names + if !unicode.IsLetter(r) && !unicode.IsDigit(r) && r != '.' && r != '_' && r != '-' { + return false + } + } + return true +} + +func isValidVersion(ver string) bool { + _, err := version.Parse(ver) + return err == nil +} diff --git a/pkg/dependency/parser/python/pip/parse_test.go b/pkg/dependency/parser/python/pip/parse_test.go new file mode 100644 index 000000000000..1d3abf7e0f8d --- /dev/null +++ b/pkg/dependency/parser/python/pip/parse_test.go @@ -0,0 +1,89 @@ +package pip + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + filePath string + useMinVersion bool + want []ftypes.Package + }{ + { + name: "happy path", + filePath: "testdata/requirements_flask.txt", + want: requirementsFlask, + }, + { + name: "happy path with comments", + filePath: "testdata/requirements_comments.txt", + want: requirementsComments, + }, + { + name: "happy path with spaces", + filePath: "testdata/requirements_spaces.txt", + want: requirementsSpaces, + }, + { + name: "happy path with dependency without version", + filePath: "testdata/requirements_no_version.txt", + want: requirementsNoVersion, + }, + { + name: "happy path with operator", + filePath: "testdata/requirements_operator.txt", + want: requirementsOperator, + }, + { + name: "happy path with hash", + filePath: "testdata/requirements_hash.txt", + want: requirementsHash, + }, + { + name: "happy path with hyphens", + filePath: "testdata/requirements_hyphens.txt", + want: requirementsHyphens, + }, + { + name: "happy path with exstras", + filePath: "testdata/requirement_exstras.txt", + want: requirementsExtras, + }, + { + name: "happy path. File uses utf16le", + filePath: "testdata/requirements_utf16le.txt", + want: requirementsUtf16le, + }, + { + name: "happy path with templating engine", + filePath: "testdata/requirements_with_templating_engine.txt", + want: nil, + }, + { + name: "compatible versions", + filePath: "testdata/requirements_compatible.txt", + useMinVersion: true, + want: requirementsCompatibleVersions, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.filePath) + require.NoError(t, err) + + got, _, err := NewParser(tt.useMinVersion).Parse(t.Context(), f) + require.NoError(t, err) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/python/pip/parse_testcase.go b/pkg/dependency/parser/python/pip/parse_testcase.go new file mode 100644 index 000000000000..e4a8d83d7117 --- /dev/null +++ b/pkg/dependency/parser/python/pip/parse_testcase.go @@ -0,0 +1,304 @@ +package pip + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +var ( + requirementsCompatibleVersions = []ftypes.Package{ + { + Name: "keyring", + Version: "4.1.1", + Locations: []ftypes.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + }, + { + Name: "Mopidy-Dirble", + Version: "1.1", + Locations: []ftypes.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + }, + { + Name: "python-gitlab", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 3, + EndLine: 3, + }, + }, + }, + } + requirementsFlask = []ftypes.Package{ + { + Name: "click", + Version: "8.0.0", + Locations: []ftypes.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + }, + { + Name: "Flask", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + }, + { + Name: "itsdangerous", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 3, + EndLine: 3, + }, + }, + }, + { + Name: "Jinja2", + Version: "3.0.0", + Locations: []ftypes.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + { + Name: "MarkupSafe", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + Name: "Werkzeug", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 6, + EndLine: 6, + }, + }, + }, + } + + requirementsComments = []ftypes.Package{ + { + Name: "click", + Version: "8.0.0", + Locations: []ftypes.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + { + Name: "Flask", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + Name: "Jinja2", + Version: "3.0.0", + Locations: []ftypes.Location{ + { + StartLine: 6, + EndLine: 6, + }, + }, + }, + { + Name: "MarkupSafe", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 7, + EndLine: 7, + }, + }, + }, + } + + requirementsSpaces = []ftypes.Package{ + { + Name: "click", + Version: "8.0.0", + Locations: []ftypes.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + }, + { + Name: "Flask", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + }, + { + Name: "itsdangerous", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 3, + EndLine: 3, + }, + }, + }, + { + Name: "Jinja2", + Version: "3.0.0", + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + } + + requirementsNoVersion = []ftypes.Package{ + { + Name: "Flask", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + }, + } + + requirementsOperator = []ftypes.Package{ + { + Name: "Django", + Version: "2.3.4", + Locations: []ftypes.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + { + Name: "SomeProject", + Version: "5.4", + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + } + + requirementsHash = []ftypes.Package{ + { + Name: "FooProject", + Version: "1.2", + Locations: []ftypes.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + }, + { + Name: "Jinja2", + Version: "3.0.0", + Locations: []ftypes.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + } + + requirementsHyphens = []ftypes.Package{ + { + Name: "oauth2-client", + Version: "4.0.0", + Locations: []ftypes.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + }, + { + Name: "python-gitlab", + Version: "2.0.0", + Locations: []ftypes.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + }, + } + + requirementsExtras = []ftypes.Package{ + { + Name: "pyjwt", + Version: "2.1.0", + Locations: []ftypes.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + }, + { + Name: "celery", + Version: "4.4.7", + Locations: []ftypes.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + }, + } + + requirementsUtf16le = []ftypes.Package{ + { + Name: "attrs", + Version: "20.3.0", + Locations: []ftypes.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + }, + } +) diff --git a/pkg/dependency/parser/python/pip/testdata/requirement_exstras.txt b/pkg/dependency/parser/python/pip/testdata/requirement_exstras.txt new file mode 100644 index 000000000000..9ff057e40f57 --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirement_exstras.txt @@ -0,0 +1,2 @@ +pyjwt[crypto]==2.1.0 +celery[redis, pytest]==4.4.7 \ No newline at end of file diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_comments.txt b/pkg/dependency/parser/python/pip/testdata/requirements_comments.txt new file mode 100644 index 000000000000..e0d29e00085f --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_comments.txt @@ -0,0 +1,8 @@ +# foo==8.0.0 +#bar==8.0.0 +#comment +click==8.0.0 +Flask==2.0.0 #comment +Jinja2==3.0.0#comment +MarkupSafe==2.0.0 # comment + diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_compatible.txt b/pkg/dependency/parser/python/pip/testdata/requirements_compatible.txt new file mode 100644 index 000000000000..dbcde5b7ab10 --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_compatible.txt @@ -0,0 +1,5 @@ +keyring >= 4.1.1 # Minimum version 4.1.1 +Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.* +python-gitlab==2.0.* +django==5.*.* # this dep should be skipped +django==4.*.1 \ No newline at end of file diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_flask.txt b/pkg/dependency/parser/python/pip/testdata/requirements_flask.txt new file mode 100644 index 000000000000..3369a13760c5 --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_flask.txt @@ -0,0 +1,6 @@ +click==8.0.0 +Flask==2.0.0 +itsdangerous==2.0.0 +Jinja2==3.0.0 +MarkupSafe==2.0.0 +Werkzeug==2.0.0 diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_hash.txt b/pkg/dependency/parser/python/pip/testdata/requirements_hash.txt new file mode 100644 index 000000000000..08c31dee996c --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_hash.txt @@ -0,0 +1,6 @@ +FooProject == 1.2 --hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 \ + --hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7 + +Jinja2 == 3.0.0 \ + --hash=sha256:2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 \ + --hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7 \ No newline at end of file diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_hyphens.txt b/pkg/dependency/parser/python/pip/testdata/requirements_hyphens.txt new file mode 100644 index 000000000000..92963565d02f --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_hyphens.txt @@ -0,0 +1,2 @@ +oauth2-client==4.0.0 +python-gitlab==2.0.0 \ No newline at end of file diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_no_version.txt b/pkg/dependency/parser/python/pip/testdata/requirements_no_version.txt new file mode 100644 index 000000000000..26e30e0000aa --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_no_version.txt @@ -0,0 +1,2 @@ +Flask==2.0.0 +pandas diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_operator.txt b/pkg/dependency/parser/python/pip/testdata/requirements_operator.txt new file mode 100644 index 000000000000..9a8bbad92525 --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_operator.txt @@ -0,0 +1,8 @@ +keyring >= 4.1.1 # Minimum version 4.1.1 +coverage != 3.5 # Version Exclusion. Anything except version 3.5 +Mopidy-Dirble ~= 1.1 # Compatible release. Same as >= 1.1, == 1.* +Django == 2.3.4 +SomeProject ==5.4 ; python_version < '3.8' +numpyNew; sys_platform == 'win32' +numpy >= 3.4.1; sys_platform == 'win32' +requests>=2.31.0,<3 # Minimum version 2.31.0. Trim the end-of-range suffix. diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_spaces.txt b/pkg/dependency/parser/python/pip/testdata/requirements_spaces.txt new file mode 100644 index 000000000000..6636231e5e21 --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_spaces.txt @@ -0,0 +1,5 @@ +click == 8.0.0 +Flask ==2.0.0 +itsdangerous== 2.0.0 + +Jinja2 == 3.0.0 # comment diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_utf16le.txt b/pkg/dependency/parser/python/pip/testdata/requirements_utf16le.txt new file mode 100644 index 000000000000..2fa914006dd3 Binary files /dev/null and b/pkg/dependency/parser/python/pip/testdata/requirements_utf16le.txt differ diff --git a/pkg/dependency/parser/python/pip/testdata/requirements_with_templating_engine.txt b/pkg/dependency/parser/python/pip/testdata/requirements_with_templating_engine.txt new file mode 100644 index 000000000000..c121e2517d9f --- /dev/null +++ b/pkg/dependency/parser/python/pip/testdata/requirements_with_templating_engine.txt @@ -0,0 +1,5 @@ +%ifcookiecutter.command_line_interface|lower=='click'-%} +foo|bar=='1.2.4' +foo{bar}==%% +foo,bar&&foobar==1.2.3 +foo=='invalid-version' \ No newline at end of file diff --git a/pkg/dependency/parser/python/pipenv/parse.go b/pkg/dependency/parser/python/pipenv/parse.go new file mode 100644 index 000000000000..b2b56136eccd --- /dev/null +++ b/pkg/dependency/parser/python/pipenv/parse.go @@ -0,0 +1,43 @@ +package pipenv + +import ( + "context" + "strings" + + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type lockFile struct { + Default map[string]dependency `json:"default"` +} +type dependency struct { + Version string `json:"version"` + xjson.Location +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockFile lockFile + if err := xjson.UnmarshalRead(r, &lockFile); err != nil { + return nil, nil, xerrors.Errorf("failed to decode Pipenv.lock: %w", err) + } + + var pkgs []ftypes.Package + for pkgName, dep := range lockFile.Default { + pkgs = append(pkgs, ftypes.Package{ + Name: pkgName, + Version: strings.TrimLeft(dep.Version, "="), + Locations: []ftypes.Location{ftypes.Location(dep.Location)}, + }) + } + return pkgs, nil, nil +} diff --git a/pkg/dependency/parser/python/pipenv/parse_test.go b/pkg/dependency/parser/python/pipenv/parse_test.go new file mode 100644 index 000000000000..10185015b50f --- /dev/null +++ b/pkg/dependency/parser/python/pipenv/parse_test.go @@ -0,0 +1,48 @@ +package pipenv + +import ( + "os" + "path" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + vectors := []struct { + file string // Test input file + want []ftypes.Package + }{ + { + file: "testdata/Pipfile_normal.lock", + want: pipenvNormal, + }, + { + file: "testdata/Pipfile_django.lock", + want: pipenvDjango, + }, + { + file: "testdata/Pipfile_many.lock", + want: pipenvMany, + }, + } + + for _, v := range vectors { + t.Run(path.Base(v.file), func(t *testing.T) { + f, err := os.Open(v.file) + require.NoError(t, err) + + got, _, err := NewParser().Parse(t.Context(), f) + require.NoError(t, err) + + sort.Sort(ftypes.Packages(got)) + sort.Sort(ftypes.Packages(v.want)) + + assert.Equal(t, v.want, got) + }) + } +} diff --git a/pkg/dependency/parser/python/pipenv/parse_testcase.go b/pkg/dependency/parser/python/pipenv/parse_testcase.go new file mode 100644 index 000000000000..6d52afbfd861 --- /dev/null +++ b/pkg/dependency/parser/python/pipenv/parse_testcase.go @@ -0,0 +1,78 @@ +package pipenv + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +var ( + // docker run --name pipenv --rm -it python:3.9-alpine sh + // apk add jq + // mkdir app && cd /app + // pip install pipenv + // pipenv install requests pyyaml + // pipenv graph --json | jq -rc '.[] | "{\"\(.package.package_name | ascii_downcase)\", \"\(.package.installed_version)\", \"\"},"' + // graph doesn't contain information about location of dependency in lock file. + // add locations manually + pipenvNormal = []ftypes.Package{ + {Name: "urllib3", Version: "1.24.2", Locations: []ftypes.Location{{StartLine: 65, EndLine: 71}}}, + {Name: "requests", Version: "2.21.0", Locations: []ftypes.Location{{StartLine: 57, EndLine: 64}}}, + {Name: "pyyaml", Version: "5.1", Locations: []ftypes.Location{{StartLine: 40, EndLine: 56}}}, + {Name: "idna", Version: "2.8", Locations: []ftypes.Location{{StartLine: 33, EndLine: 39}}}, + {Name: "chardet", Version: "3.0.4", Locations: []ftypes.Location{{StartLine: 26, EndLine: 32}}}, + {Name: "certifi", Version: "2019.3.9", Locations: []ftypes.Location{{StartLine: 19, EndLine: 25}}}, + } + + // docker run --name pipenv --rm -it python:3.9-alpine bash + // apk add jq + // mkdir app && cd /app + // pip install pipenv + // pipenv install requests pyyaml django djangorestframework + // pipenv graph --json | jq -rc '.[] | "{\"\(.package.package_name | ascii_downcase)\", \"\(.package.installed_version)\", \"\"},"' + // graph doesn't contain information about location of dependency in lock file. + // add locations manually + pipenvDjango = []ftypes.Package{ + {Name: "urllib3", Version: "1.24.2", Locations: []ftypes.Location{{StartLine: 95, EndLine: 101}}}, + {Name: "sqlparse", Version: "0.3.0", Locations: []ftypes.Location{{StartLine: 88, EndLine: 94}}}, + {Name: "requests", Version: "2.21.0", Locations: []ftypes.Location{{StartLine: 80, EndLine: 87}}}, + {Name: "pyyaml", Version: "5.1", Locations: []ftypes.Location{{StartLine: 63, EndLine: 79}}}, + {Name: "pytz", Version: "2019.1", Locations: []ftypes.Location{{StartLine: 56, EndLine: 62}}}, + {Name: "idna", Version: "2.8", Locations: []ftypes.Location{{StartLine: 49, EndLine: 55}}}, + {Name: "djangorestframework", Version: "3.9.3", Locations: []ftypes.Location{{StartLine: 41, EndLine: 48}}}, + {Name: "django", Version: "2.2", Locations: []ftypes.Location{{StartLine: 33, EndLine: 40}}}, + {Name: "chardet", Version: "3.0.4", Locations: []ftypes.Location{{StartLine: 26, EndLine: 32}}}, + {Name: "certifi", Version: "2019.3.9", Locations: []ftypes.Location{{StartLine: 19, EndLine: 25}}}, + } + + // docker run --name pipenv --rm -it python:3.9-alpine bash + // apk add jq + // mkdir app && cd /app + // pip install pipenv + // pipenv install requests pyyaml django djangorestframework six botocore python-dateutil simplejson setuptools pyasn1 awscli jinja2 + // pipenv graph --json | jq -rc '.[] | "{\"\(.package.package_name | ascii_downcase)\", \"\(.package.installed_version)\", \"\"},"' + // graph doesn't contain information about location of dependency in lock file. + // add locations manually + pipenvMany = []ftypes.Package{ + {Name: "urllib3", Version: "1.24.2", Locations: []ftypes.Location{{StartLine: 237, EndLine: 244}}}, + {Name: "sqlparse", Version: "0.3.0", Locations: []ftypes.Location{{StartLine: 230, EndLine: 236}}}, + {Name: "six", Version: "1.12.0", Locations: []ftypes.Location{{StartLine: 222, EndLine: 229}}}, + {Name: "simplejson", Version: "3.16.0", Locations: []ftypes.Location{{StartLine: 204, EndLine: 221}}}, + {Name: "s3transfer", Version: "0.2.0", Locations: []ftypes.Location{{StartLine: 197, EndLine: 203}}}, + {Name: "rsa", Version: "3.4.2", Locations: []ftypes.Location{{StartLine: 190, EndLine: 196}}}, + {Name: "requests", Version: "2.21.0", Locations: []ftypes.Location{{StartLine: 182, EndLine: 189}}}, + {Name: "pyyaml", Version: "3.13", Locations: []ftypes.Location{{StartLine: 165, EndLine: 181}}}, + {Name: "pytz", Version: "2019.1", Locations: []ftypes.Location{{StartLine: 158, EndLine: 164}}}, + {Name: "python-dateutil", Version: "2.8.0", Locations: []ftypes.Location{{StartLine: 150, EndLine: 157}}}, + {Name: "pyasn1", Version: "0.4.5", Locations: []ftypes.Location{{StartLine: 142, EndLine: 149}}}, + {Name: "markupsafe", Version: "1.1.1", Locations: []ftypes.Location{{StartLine: 109, EndLine: 141}}}, + {Name: "jmespath", Version: "0.9.4", Locations: []ftypes.Location{{StartLine: 102, EndLine: 108}}}, + {Name: "jinja2", Version: "2.10.1", Locations: []ftypes.Location{{StartLine: 94, EndLine: 101}}}, + {Name: "idna", Version: "2.8", Locations: []ftypes.Location{{StartLine: 87, EndLine: 93}}}, + {Name: "framework", Version: "0.1.0", Locations: []ftypes.Location{{StartLine: 80, EndLine: 86}}}, + {Name: "docutils", Version: "0.14", Locations: []ftypes.Location{{StartLine: 72, EndLine: 79}}}, + {Name: "djangorestframework", Version: "3.9.3", Locations: []ftypes.Location{{StartLine: 64, EndLine: 71}}}, + {Name: "django", Version: "2.2", Locations: []ftypes.Location{{StartLine: 56, EndLine: 63}}}, + {Name: "colorama", Version: "0.3.9", Locations: []ftypes.Location{{StartLine: 49, EndLine: 55}}}, + {Name: "chardet", Version: "3.0.4", Locations: []ftypes.Location{{StartLine: 42, EndLine: 48}}}, + {Name: "certifi", Version: "2019.3.9", Locations: []ftypes.Location{{StartLine: 35, EndLine: 41}}}, + {Name: "botocore", Version: "1.12.137", Locations: []ftypes.Location{{StartLine: 27, EndLine: 34}}}, + {Name: "awscli", Version: "1.16.147", Locations: []ftypes.Location{{StartLine: 19, EndLine: 26}}}, + } +) diff --git a/pkg/dependency/parser/python/pipenv/testdata/Pipfile_django.lock b/pkg/dependency/parser/python/pipenv/testdata/Pipfile_django.lock new file mode 100644 index 000000000000..bc520cd11534 --- /dev/null +++ b/pkg/dependency/parser/python/pipenv/testdata/Pipfile_django.lock @@ -0,0 +1,104 @@ +{ + "_meta": { + "hash": { + "sha256": "3072b1f87376eaf80e623fe275f79051cae6232f7e458a281bf2723853149d91" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" + ], + "version": "==2019.3.9" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "django": { + "hashes": [ + "sha256:7c3543e4fb070d14e10926189a7fcf42ba919263b7473dceaefce34d54e8a119", + "sha256:a2814bffd1f007805b19194eb0b9a331933b82bd5da1c3ba3d7b7ba16e06dc4b" + ], + "index": "pypi", + "version": "==2.2" + }, + "djangorestframework": { + "hashes": [ + "sha256:1d22971a5fc98becdbbad9710ca2a9148dd339f6cbea4c3ddbed2cb84bab94e1", + "sha256:2884763160b997073ff1e937bd820a69d23978902a3ccd0ba53a217e196239f0" + ], + "index": "pypi", + "version": "==3.9.3" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "pytz": { + "hashes": [ + "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", + "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + ], + "version": "==2019.1" + }, + "pyyaml": { + "hashes": [ + "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", + "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", + "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", + "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", + "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", + "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", + "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", + "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", + "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", + "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", + "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" + ], + "index": "pypi", + "version": "==5.1" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "sqlparse": { + "hashes": [ + "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", + "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" + ], + "version": "==0.3.0" + }, + "urllib3": { + "hashes": [ + "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", + "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" + ], + "version": "==1.24.2" + } + }, + "develop": {} +} diff --git a/pkg/dependency/parser/python/pipenv/testdata/Pipfile_many.lock b/pkg/dependency/parser/python/pipenv/testdata/Pipfile_many.lock new file mode 100644 index 000000000000..38605f921ab7 --- /dev/null +++ b/pkg/dependency/parser/python/pipenv/testdata/Pipfile_many.lock @@ -0,0 +1,247 @@ +{ + "_meta": { + "hash": { + "sha256": "6a0046c55fa1d91553ca88f7098f5e382f4954eaa7d48ce15d3f9135f4d6d5fe" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "awscli": { + "hashes": [ + "sha256:2ed95e925c5d867ac02086a0721cdf19bae581b6b6dffd1d5cfc4b1c54b2a21b", + "sha256:3c53a97d817798287e5468d5dd57acc3b9a8b02f495a32f80582f6b323294c20" + ], + "index": "pypi", + "version": "==1.16.147" + }, + "botocore": { + "hashes": [ + "sha256:0d95794f6b1239c75e2c5f966221bcd4b68020fddb5676f757531eedbb612ed8", + "sha256:3213cf48cf2ceee10fc3b93221f2cd1c38521cca7584f547d5c086213cc60f35" + ], + "index": "pypi", + "version": "==1.12.137" + }, + "certifi": { + "hashes": [ + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" + ], + "version": "==2019.3.9" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "colorama": { + "hashes": [ + "sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", + "sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1" + ], + "version": "==0.3.9" + }, + "django": { + "hashes": [ + "sha256:7c3543e4fb070d14e10926189a7fcf42ba919263b7473dceaefce34d54e8a119", + "sha256:a2814bffd1f007805b19194eb0b9a331933b82bd5da1c3ba3d7b7ba16e06dc4b" + ], + "index": "pypi", + "version": "==2.2" + }, + "djangorestframework": { + "hashes": [ + "sha256:1d22971a5fc98becdbbad9710ca2a9148dd339f6cbea4c3ddbed2cb84bab94e1", + "sha256:2884763160b997073ff1e937bd820a69d23978902a3ccd0ba53a217e196239f0" + ], + "index": "pypi", + "version": "==3.9.3" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "version": "==0.14" + }, + "framework": { + "hashes": [ + "sha256:a988297ade5c38bc58f4afc02b1dc075ad0f6dae2cbd1be6342067aaaeee532d" + ], + "index": "pypi", + "version": "==0.1.0" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "jinja2": { + "hashes": [ + "sha256:065c4f02ebe7f7cf559e49ee5a95fb800a9e4528727aec6f24402a5374c65013", + "sha256:14dd6caf1527abb21f08f86c784eac40853ba93edb79552aa1e4b8aef1b61c7b" + ], + "index": "pypi", + "version": "==2.10.1" + }, + "jmespath": { + "hashes": [ + "sha256:3720a4b1bd659dd2eecad0666459b9788813e032b83e7ba58578e48254e0a0e6", + "sha256:bde2aef6f44302dfb30320115b17d030798de8c4110e28d5cf6cf91a7a31074c" + ], + "version": "==0.9.4" + }, + "markupsafe": { + "hashes": [ + "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473", + "sha256:09027a7803a62ca78792ad89403b1b7a73a01c8cb65909cd876f7fcebd79b161", + "sha256:09c4b7f37d6c648cb13f9230d847adf22f8171b1ccc4d5682398e77f40309235", + "sha256:1027c282dad077d0bae18be6794e6b6b8c91d58ed8a8d89a89d59693b9131db5", + "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff", + "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b", + "sha256:43a55c2930bbc139570ac2452adf3d70cdbb3cfe5912c71cdce1c2c6bbd9c5d1", + "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e", + "sha256:500d4957e52ddc3351cabf489e79c91c17f6e0899158447047588650b5e69183", + "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66", + "sha256:62fe6c95e3ec8a7fad637b7f3d372c15ec1caa01ab47926cfdf7a75b40e0eac1", + "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1", + "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e", + "sha256:79855e1c5b8da654cf486b830bd42c06e8780cea587384cf6545b7d9ac013a0b", + "sha256:7c1699dfe0cf8ff607dbdcc1e9b9af1755371f92a68f706051cc8c37d447c905", + "sha256:88e5fcfb52ee7b911e8bb6d6aa2fd21fbecc674eadd44118a9cc3863f938e735", + "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d", + "sha256:98c7086708b163d425c67c7a91bad6e466bb99d797aa64f965e9d25c12111a5e", + "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d", + "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c", + "sha256:ade5e387d2ad0d7ebf59146cc00c8044acbd863725f887353a10df825fc8ae21", + "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2", + "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5", + "sha256:b2051432115498d3562c084a49bba65d97cf251f5a331c64a12ee7e04dacc51b", + "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6", + "sha256:c8716a48d94b06bb3b2524c2b77e055fb313aeb4ea620c8dd03a105574ba704f", + "sha256:cd5df75523866410809ca100dc9681e301e3c27567cf498077e8551b6d20e42f", + "sha256:e249096428b3ae81b08327a63a485ad0878de3fb939049038579ac0ef61e17e7" + ], + "version": "==1.1.1" + }, + "pyasn1": { + "hashes": [ + "sha256:da2420fe13a9452d8ae97a0e478adde1dee153b11ba832a95b223a2ba01c10f7", + "sha256:da6b43a8c9ae93bc80e2739efb38cc776ba74a886e3e9318d65fe81a8b8a2c6e" + ], + "index": "pypi", + "version": "==0.4.5" + }, + "python-dateutil": { + "hashes": [ + "sha256:7e6584c74aeed623791615e26efd690f29817a27c73085b78e4bad02493df2fb", + "sha256:c89805f6f4d64db21ed966fda138f8a5ed7a4fdbc1a8ee329ce1b74e3c74da9e" + ], + "index": "pypi", + "version": "==2.8.0" + }, + "pytz": { + "hashes": [ + "sha256:303879e36b721603cc54604edcac9d20401bdbe31e1e4fdee5b9f98d5d31dfda", + "sha256:d747dd3d23d77ef44c6a3526e274af6efeb0a6f1afd5a69ba4d5be4098c8e141" + ], + "version": "==2019.1" + }, + "pyyaml": { + "hashes": [ + "sha256:3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", + "sha256:3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", + "sha256:40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", + "sha256:558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", + "sha256:a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", + "sha256:aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", + "sha256:bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", + "sha256:d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", + "sha256:d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", + "sha256:e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", + "sha256:e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531" + ], + "index": "pypi", + "version": "==3.13" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "rsa": { + "hashes": [ + "sha256:25df4e10c263fb88b5ace923dd84bf9aa7f5019687b5e55382ffcdb8bede9db5", + "sha256:43f682fea81c452c98d09fc316aae12de6d30c4b5c84226642cf8f8fd1c93abd" + ], + "version": "==3.4.2" + }, + "s3transfer": { + "hashes": [ + "sha256:7b9ad3213bff7d357f888e0fab5101b56fa1a0548ee77d121c3a3dbfbef4cb2e", + "sha256:f23d5cb7d862b104401d9021fc82e5fa0e0cf57b7660a1331425aab0c691d021" + ], + "version": "==0.2.0" + }, + "simplejson": { + "hashes": [ + "sha256:067a7177ddfa32e1483ba5169ebea1bc2ea27f224853211ca669325648ca5642", + "sha256:2fc546e6af49fb45b93bbe878dea4c48edc34083729c0abd09981fe55bdf7f91", + "sha256:354fa32b02885e6dae925f1b5bbf842c333c1e11ea5453ddd67309dc31fdb40a", + "sha256:37e685986cf6f8144607f90340cff72d36acf654f3653a6c47b84c5c38d00df7", + "sha256:3af610ee72efbe644e19d5eaad575c73fb83026192114e5f6719f4901097fce2", + "sha256:3b919fc9cf508f13b929a9b274c40786036b31ad28657819b3b9ba44ba651f50", + "sha256:3dd289368bbd064974d9a5961101f080e939cbe051e6689a193c99fb6e9ac89b", + "sha256:6c3258ffff58712818a233b9737fe4be943d306c40cf63d14ddc82ba563f483a", + "sha256:75e3f0b12c28945c08f54350d91e624f8dd580ab74fd4f1bbea54bc6b0165610", + "sha256:b1f329139ba647a9548aa05fb95d046b4a677643070dc2afc05fa2e975d09ca5", + "sha256:ee9625fc8ee164902dfbb0ff932b26df112da9f871c32f0f9c1bcf20c350fe2a", + "sha256:fb2530b53c28f0d4d84990e945c2ebb470edb469d63e389bf02ff409012fe7c5" + ], + "index": "pypi", + "version": "==3.16.0" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "index": "pypi", + "version": "==1.12.0" + }, + "sqlparse": { + "hashes": [ + "sha256:40afe6b8d4b1117e7dff5504d7a8ce07d9a1b15aeeade8a2d10f130a834f8177", + "sha256:7c3dca29c022744e95b547e867cee89f4fce4373f3549ccd8797d8eb52cdb873" + ], + "version": "==0.3.0" + }, + "urllib3": { + "hashes": [ + "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", + "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" + ], + "markers": "python_version >= '3.4'", + "version": "==1.24.2" + } + }, + "develop": {} +} diff --git a/pkg/dependency/parser/python/pipenv/testdata/Pipfile_normal.lock b/pkg/dependency/parser/python/pipenv/testdata/Pipfile_normal.lock new file mode 100644 index 000000000000..7f2f7a683dfc --- /dev/null +++ b/pkg/dependency/parser/python/pipenv/testdata/Pipfile_normal.lock @@ -0,0 +1,74 @@ +{ + "_meta": { + "hash": { + "sha256": "5d17fda061a57afd47f9dbf5f74aa3803c28e73fa8fa247425ba76549bc30804" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.7" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" + ], + "version": "==2019.3.9" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "pyyaml": { + "hashes": [ + "sha256:1adecc22f88d38052fb787d959f003811ca858b799590a5eaa70e63dca50308c", + "sha256:436bc774ecf7c103814098159fbb84c2715d25980175292c648f2da143909f95", + "sha256:460a5a4248763f6f37ea225d19d5c205677d8d525f6a83357ca622ed541830c2", + "sha256:5a22a9c84653debfbf198d02fe592c176ea548cccce47553f35f466e15cf2fd4", + "sha256:7a5d3f26b89d688db27822343dfa25c599627bc92093e788956372285c6298ad", + "sha256:9372b04a02080752d9e6f990179a4ab840227c6e2ce15b95e1278456664cf2ba", + "sha256:a5dcbebee834eaddf3fa7366316b880ff4062e4bcc9787b78c7fbb4a26ff2dd1", + "sha256:aee5bab92a176e7cd034e57f46e9df9a9862a71f8f37cad167c6fc74c65f5b4e", + "sha256:c51f642898c0bacd335fc119da60baae0824f2cde95b0330b56c0553439f0673", + "sha256:c68ea4d3ba1705da1e0d85da6684ac657912679a649e8868bd850d2c299cce13", + "sha256:e23d0cc5299223dcc37885dae624f382297717e459ea24053709675a976a3e19" + ], + "index": "pypi", + "version": "==5.1" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:4c291ca23bbb55c76518905869ef34bdd5f0e46af7afe6861e8375643ffee1a0", + "sha256:9a247273df709c4fedb38c711e44292304f73f39ab01beda9f6b9fc375669ac3" + ], + "version": "==1.24.2" + } + }, + "develop": {} +} diff --git a/pkg/dependency/parser/python/poetry/parse.go b/pkg/dependency/parser/python/poetry/parse.go new file mode 100644 index 000000000000..54b6bc6b0220 --- /dev/null +++ b/pkg/dependency/parser/python/poetry/parse.go @@ -0,0 +1,155 @@ +package poetry + +import ( + "context" + "slices" + + "github.com/BurntSushi/toml" + "golang.org/x/xerrors" + + version "github.com/aquasecurity/go-pep440-version" + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/python" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type Lockfile struct { + Packages []struct { + Category string `toml:"category"` + Groups []string `toml:"groups"` + Description string `toml:"description"` + Marker string `toml:"marker,omitempty"` + Name string `toml:"name"` + Optional bool `toml:"optional"` + PythonVersions string `toml:"python-versions"` + Version string `toml:"version"` + Dependencies map[string]any `toml:"dependencies"` + Metadata any + } `toml:"package"` +} + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("poetry"), + } +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockfile Lockfile + if _, err := toml.NewDecoder(r).Decode(&lockfile); err != nil { + return nil, nil, xerrors.Errorf("failed to decode poetry.lock: %w", err) + } + + // Keep all installed versions + pkgVersions := p.parseVersions(lockfile) + + var pkgs []ftypes.Package + var deps []ftypes.Dependency + for _, pkg := range lockfile.Packages { + pkgID := packageID(pkg.Name, pkg.Version) + pkgs = append(pkgs, ftypes.Package{ + ID: pkgID, + Name: pkg.Name, + Version: pkg.Version, + // TODO upgrade logic for working with groups + // Mark only: + // - `category = "dev"` + // - groups without `main`. e.g. `groups = ["dev"]` + Dev: pkg.Category == "dev" || (len(pkg.Groups) > 0 && !slices.Contains(pkg.Groups, "main")), + }) + + dependsOn := p.parseDependencies(pkg.Dependencies, pkgVersions) + if len(dependsOn) != 0 { + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: dependsOn, + }) + } + } + return pkgs, deps, nil +} + +// parseVersions stores all installed versions of packages for use in dependsOn +// as the dependencies of packages use version range. +func (p *Parser) parseVersions(lockfile Lockfile) map[string][]string { + pkgVersions := make(map[string][]string) + for _, pkg := range lockfile.Packages { + if pkg.Category == "dev" { + continue + } + if vers, ok := pkgVersions[pkg.Name]; ok { + pkgVersions[pkg.Name] = append(vers, pkg.Version) + } else { + pkgVersions[pkg.Name] = []string{pkg.Version} + } + } + return pkgVersions +} + +func (p *Parser) parseDependencies(deps map[string]any, pkgVersions map[string][]string) []string { + var dependsOn []string + for name, versRange := range deps { + if dep, err := p.parseDependency(name, versRange, pkgVersions); err != nil { + p.logger.Debug("Failed to parse poetry dependency", log.Err(err)) + } else if dep != "" { + dependsOn = append(dependsOn, dep) + } + } + slices.Sort(dependsOn) + return dependsOn +} + +func (p *Parser) parseDependency(name string, versRange any, pkgVersions map[string][]string) (string, error) { + name = python.NormalizePkgName(name, true) + vers, ok := pkgVersions[name] + if !ok { + return "", xerrors.Errorf("no version found for %q", name) + } + + for _, ver := range vers { + var vRange string + + switch r := versRange.(type) { + case string: + vRange = r + case map[string]any: + for k, v := range r { + if k == "version" { + vRange = v.(string) + } + } + } + + if matched, err := matchVersion(ver, vRange); err != nil { + return "", xerrors.Errorf("failed to match version for %s: %w", name, err) + } else if matched { + return packageID(name, ver), nil + } + } + return "", xerrors.Errorf("no matched version found for %q", name) +} + +// matchVersion checks if the package version satisfies the given constraint. +func matchVersion(currentVersion, constraint string) (bool, error) { + v, err := version.Parse(currentVersion) + if err != nil { + return false, xerrors.Errorf("python version error (%s): %s", currentVersion, err) + } + + c, err := version.NewSpecifiers(constraint, version.WithPreRelease(true)) + if err != nil { + return false, xerrors.Errorf("python constraint error (%s): %s", constraint, err) + } + + return c.Check(v), nil +} + +func packageID(name, ver string) string { + return dependency.ID(ftypes.Poetry, name, ver) +} diff --git a/pkg/dependency/parser/python/poetry/parse_test.go b/pkg/dependency/parser/python/poetry/parse_test.go new file mode 100644 index 000000000000..1b28c69bea10 --- /dev/null +++ b/pkg/dependency/parser/python/poetry/parse_test.go @@ -0,0 +1,129 @@ +package poetry + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + file string + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + wantErr assert.ErrorAssertionFunc + }{ + { + name: "normal", + file: "testdata/poetry_normal.lock", + wantPkgs: poetryNormal, + wantErr: assert.NoError, + }, + { + name: "flask", + file: "testdata/poetry_flask.lock", + wantPkgs: poetryFlask, + wantDeps: poetryFlaskDeps, + wantErr: assert.NoError, + }, + { + name: "flask + poetry v2", + file: "testdata/poetry_v2_flask.lock", + wantPkgs: poetryFlask, + wantDeps: poetryV2FlaskDeps, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + defer f.Close() + + p := NewParser() + gotPkgs, gotDeps, err := p.Parse(t.Context(), f) + if !tt.wantErr(t, err, fmt.Sprintf("Parse(%v)", tt.file)) { + return + } + assert.Equalf(t, tt.wantPkgs, gotPkgs, "Parse(%v)", tt.file) + assert.Equalf(t, tt.wantDeps, gotDeps, "Parse(%v)", tt.file) + }) + } +} + +func TestParseDependency(t *testing.T) { + tests := []struct { + name string + packageName string + versionRange any + pkgsVersions map[string][]string + want string + wantErr string + }{ + { + name: "handle package name", + packageName: "Test_project.Name", + versionRange: "*", + pkgsVersions: map[string][]string{ + "test-project-name": {"1.0.0"}, + }, + want: "test-project-name@1.0.0", + }, + { + name: "version range as string", + packageName: "test", + versionRange: ">=1.0.0", + pkgsVersions: map[string][]string{ + "test": {"2.0.0"}, + }, + want: "test@2.0.0", + }, + { + name: "version range == *", + packageName: "test", + versionRange: "*", + pkgsVersions: map[string][]string{ + "test": {"3.0.0"}, + }, + want: "test@3.0.0", + }, + { + name: "version range as json", + packageName: "test", + versionRange: map[string]any{ + "version": ">=4.8.3", + "markers": "python_version < \"3.8\"", + }, + pkgsVersions: map[string][]string{ + "test": {"5.0.0"}, + }, + want: "test@5.0.0", + }, + { + name: "pkgsVersions doesn't contain required version", + packageName: "test", + versionRange: ">=1.0.0", + pkgsVersions: make(map[string][]string), + wantErr: "no version found", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewParser().parseDependency(tt.packageName, tt.versionRange, tt.pkgsVersions) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/python/poetry/parse_testcase.go b/pkg/dependency/parser/python/poetry/parse_testcase.go new file mode 100644 index 000000000000..836e208d759b --- /dev/null +++ b/pkg/dependency/parser/python/poetry/parse_testcase.go @@ -0,0 +1,68 @@ +package poetry + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +var ( + // docker run --name poetry --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh + // apk add curl + // curl -sSL https://install.python-poetry.org | POETRY_VERSION=1.1.7 python3 - + // export PATH=/root/.local/bin:$PATH + // poetry new normal && cd normal + // poetry add pypi@2.1 + // poetry show -a | awk '{gsub(/\(!\)/, ""); printf("{ID: \""$1"@"$2"\", Name: \""$1"\", Version: \""$2"\"},\n") }' + poetryNormal = []ftypes.Package{ + {ID: "pypi@2.1", Name: "pypi", Version: "2.1"}, + } + + // docker run --name poetry --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh + // apk add curl + // curl -sSL https://install.python-poetry.org | POETRY_VERSION=1.1.7 python3 - + // export PATH=/root/.local/bin:$PATH + // poetry new web && cd web + // poetry add pluggy@0.13.1 -E pre-commit,tox + // poetry add flask@1.0.3 + // poetry add pytest@5.4.3 --dev + // poetry show -a | awk '{gsub(/\(!\)/, ""); printf("{ID: \""$1"@"$2"\", Name: \""$1"\", Version: \""$2"\"},\n") }' + // mark dev deps + poetryFlask = []ftypes.Package{ + {ID: "atomicwrites@1.4.1", Name: "atomicwrites", Version: "1.4.1", Dev: true}, + {ID: "attrs@25.1.0", Name: "attrs", Version: "25.1.0", Dev: true}, + {ID: "click@8.1.8", Name: "click", Version: "8.1.8"}, + {ID: "colorama@0.4.6", Name: "colorama", Version: "0.4.6"}, + {ID: "flask@1.0.3", Name: "flask", Version: "1.0.3"}, + {ID: "itsdangerous@2.2.0", Name: "itsdangerous", Version: "2.2.0"}, + {ID: "jinja2@3.1.5", Name: "jinja2", Version: "3.1.5"}, + {ID: "markupsafe@3.0.2", Name: "markupsafe", Version: "3.0.2"}, + {ID: "more-itertools@10.6.0", Name: "more-itertools", Version: "10.6.0", Dev: true}, + {ID: "packaging@24.2", Name: "packaging", Version: "24.2", Dev: true}, + {ID: "pluggy@0.13.1", Name: "pluggy", Version: "0.13.1", Dev: false}, + {ID: "py@1.11.0", Name: "py", Version: "1.11.0", Dev: true}, + {ID: "pytest@5.4.3", Name: "pytest", Version: "5.4.3", Dev: true}, + {ID: "wcwidth@0.2.13", Name: "wcwidth", Version: "0.2.13", Dev: true}, + {ID: "werkzeug@3.1.3", Name: "werkzeug", Version: "3.1.3"}, + } + + // cat poetry.lock | grep "\[package.dependencies\]" -B 3 -A 8 - it might help to complete this slice + poetryFlaskDeps = []ftypes.Dependency{ + {ID: "click@8.1.8", DependsOn: []string{"colorama@0.4.6"}}, + {ID: "flask@1.0.3", DependsOn: []string{"click@8.1.8", "itsdangerous@2.2.0", "jinja2@3.1.5", "werkzeug@3.1.3"}}, + {ID: "jinja2@3.1.5", DependsOn: []string{"markupsafe@3.0.2"}}, + {ID: "pytest@5.4.3", DependsOn: []string{"colorama@0.4.6", "pluggy@0.13.1"}}, + {ID: "werkzeug@3.1.3", DependsOn: []string{"markupsafe@3.0.2"}}, + } + + // use instruction above with `POETRY_VERSION=2.0.1` + + poetryV2FlaskDeps = []ftypes.Dependency{ + {ID: "click@8.1.8", DependsOn: []string{"colorama@0.4.6"}}, + {ID: "flask@1.0.3", DependsOn: []string{"click@8.1.8", "itsdangerous@2.2.0", "jinja2@3.1.5", "werkzeug@3.1.3"}}, + {ID: "jinja2@3.1.5", DependsOn: []string{"markupsafe@3.0.2"}}, + { + ID: "pytest@5.4.3", DependsOn: []string{ + "atomicwrites@1.4.1", "attrs@25.1.0", "colorama@0.4.6", "more-itertools@10.6.0", "packaging@24.2", + "pluggy@0.13.1", "py@1.11.0", "wcwidth@0.2.13", + }, + }, + {ID: "werkzeug@3.1.3", DependsOn: []string{"markupsafe@3.0.2"}}, + } +) diff --git a/pkg/dependency/parser/python/poetry/testdata/poetry_flask.lock b/pkg/dependency/parser/python/poetry/testdata/poetry_flask.lock new file mode 100644 index 000000000000..8f9fa7acb805 --- /dev/null +++ b/pkg/dependency/parser/python/poetry/testdata/poetry_flask.lock @@ -0,0 +1,192 @@ +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "25.1.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=3.8" + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist", "pytest (>=4.3.0)"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest-mypy-plugins", "pytest-xdist", "pytest (>=4.3.0)"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest-mypy-plugins", "pytest-xdist", "pytest (>=4.3.0)"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest-mypy-plugins", "pytest-xdist", "pytest (>=4.3.0)"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" + +[[package]] +name = "flask" +version = "1.0.3" +description = "A simple framework for building complex web applications." +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +click = ">=5.1" +itsdangerous = ">=0.24" +Jinja2 = ">=2.10" +Werkzeug = ">=0.14" + +[package.extras] +dev = ["pytest (>=3)", "coverage", "tox", "sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet"] +docs = ["sphinx", "pallets-sphinx-themes", "sphinxcontrib-log-cabinet"] +dotenv = ["python-dotenv"] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.9" + +[[package]] +name = "more-itertools" +version = "10.6.0" +description = "More routines for operating on iterables, beyond itertools" +category = "dev" +optional = false +python-versions = ">=3.9" + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "pytest" +version = "5.4.3" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=17.4.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.extras] +checkqa-mypy = ["mypy (==v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=3.9" + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "30b62a7c783f95ec5ba20bd15a62e102cdb09e01410979007ffbf1a80a22e2ca" + +[metadata.files] +atomicwrites = [] +attrs = [] +click = [] +colorama = [] +flask = [] +itsdangerous = [] +jinja2 = [] +markupsafe = [] +more-itertools = [] +packaging = [] +pluggy = [] +py = [] +pytest = [] +wcwidth = [] +werkzeug = [] diff --git a/pkg/dependency/parser/python/poetry/testdata/poetry_normal.lock b/pkg/dependency/parser/python/poetry/testdata/poetry_normal.lock new file mode 100644 index 000000000000..0f814daa20a9 --- /dev/null +++ b/pkg/dependency/parser/python/poetry/testdata/poetry_normal.lock @@ -0,0 +1,17 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "pypi" +version = "2.1" +description = "PyPI is the Python Package Index at http://pypi.org/" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pypi-2.1.tar.gz", hash = "sha256:f014c87f871c2143ff613d9d13a59cc8ab8309b048754c27da8c58b21de2c10d"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "8fe1a16a0f2018499bedde14bd5c44aa7d9540b90c1fe0d53610a62f22e4f612" diff --git a/pkg/dependency/parser/python/poetry/testdata/poetry_v2_flask.lock b/pkg/dependency/parser/python/poetry/testdata/poetry_v2_flask.lock new file mode 100644 index 000000000000..bce75f91bfe6 --- /dev/null +++ b/pkg/dependency/parser/python/poetry/testdata/poetry_v2_flask.lock @@ -0,0 +1,297 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] + +[[package]] +name = "attrs" +version = "25.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "flask" +version = "1.0.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "Flask-1.0.3-py2.py3-none-any.whl", hash = "sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"}, + {file = "Flask-1.0.3.tar.gz", hash = "sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3"}, +] + +[package.dependencies] +click = ">=5.1" +itsdangerous = ">=0.24" +Jinja2 = ">=2.10" +Werkzeug = ">=0.14" + +[package.extras] +dev = ["coverage", "pallets-sphinx-themes", "pytest (>=3)", "sphinx", "sphinxcontrib-log-cabinet", "tox"] +docs = ["pallets-sphinx-themes", "sphinx", "sphinxcontrib-log-cabinet"] +dotenv = ["python-dotenv"] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "more-itertools" +version = "10.6.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"}, + {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main", "dev"] +files = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pytest" +version = "5.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=17.4.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.extras] +checkqa-mypy = ["mypy (==v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9" +content-hash = "f017959f972a877a3d26e05b3c990579b5ed139f6496eef716b76457872d58f3" diff --git a/pkg/dependency/parser/python/pylock/parse.go b/pkg/dependency/parser/python/pylock/parse.go new file mode 100644 index 000000000000..a103c6780320 --- /dev/null +++ b/pkg/dependency/parser/python/pylock/parse.go @@ -0,0 +1,98 @@ +package pylock + +import ( + "sort" + + "github.com/BurntSushi/toml" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/python" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type Pylock struct { + Packages []Package `toml:"packages"` +} + +type Package struct { + Name string `toml:"name"` + Version string `toml:"version"` + Dependencies []Dependency `toml:"dependencies"` +} + +type Dependency struct { + Name string `toml:"name"` + Version string `toml:"version"` +} + +// Parser parses pylock.toml defined in PEP 751. +// https://peps.python.org/pep-0751 +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lock Pylock + if _, err := toml.NewDecoder(r).Decode(&lock); err != nil { + return nil, nil, xerrors.Errorf("failed to decode pylock.toml: %w", err) + } + + pkgs := make(map[string]ftypes.Package) + deps := make(map[string][]string) + + for _, pkg := range lock.Packages { + normalizedPkgName := python.NormalizePkgName(pkg.Name, true) + pkgID := packageID(normalizedPkgName, pkg.Version) + + pkgs[pkgID] = ftypes.Package{ + ID: pkgID, + Name: normalizedPkgName, + Version: pkg.Version, + } + + var dependsOn []string + for _, dep := range pkg.Dependencies { + depName := python.NormalizePkgName(dep.Name, true) + depID := packageID(depName, dep.Version) + dependsOn = append(dependsOn, depID) + } + if len(dependsOn) > 0 { + sort.Strings(dependsOn) + deps[pkgID] = dependsOn + } + } + + depSlice := lo.MapToSlice(deps, func(pkgID string, dependsOn []string) ftypes.Dependency { + if _, ok := pkgs[pkgID]; !ok { + return ftypes.Dependency{} + } + + // Filter out dependencies that are not in the package list + var dependsOnIDs []string + for _, depID := range dependsOn { + if _, ok := pkgs[depID]; ok { + dependsOnIDs = append(dependsOnIDs, depID) + } + } + + return ftypes.Dependency{ + ID: pkgID, + DependsOn: dependsOnIDs, + } + }) + + pkgSlice := lo.Values(pkgs) + sort.Sort(ftypes.Packages(pkgSlice)) + sort.Sort(ftypes.Dependencies(depSlice)) + + return pkgSlice, depSlice, nil +} + +func packageID(name, ver string) string { + return dependency.ID(ftypes.PyLock, name, ver) +} diff --git a/pkg/dependency/parser/python/pylock/parse_test.go b/pkg/dependency/parser/python/pylock/parse_test.go new file mode 100644 index 000000000000..a742a410ea76 --- /dev/null +++ b/pkg/dependency/parser/python/pylock/parse_test.go @@ -0,0 +1,102 @@ +package pylock_test + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/pylock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + file string + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + wantErr assert.ErrorAssertionFunc + }{ + { + name: "happy path", + file: "testdata/pylock.toml", + wantPkgs: []ftypes.Package{ + { + ID: "attrs@25.4.0", + Name: "attrs", + Version: "25.4.0", + }, + { + ID: "certifi@2025.10.5", + Name: "certifi", + Version: "2025.10.5", + }, + { + ID: "charset-normalizer@3.4.3", + Name: "charset-normalizer", + Version: "3.4.3", + }, + { + ID: "ham@3.0.0", + Name: "ham", + Version: "3.0.0", + }, + { + ID: "idna@3.10", + Name: "idna", + Version: "3.10", + }, + { + ID: "requests@2.32.5", + Name: "requests", + Version: "2.32.5", + }, + { + ID: "spam@1.0.0", + Name: "spam", + Version: "1.0.0", + }, + { + ID: "spam@1.1.0", + Name: "spam", + Version: "1.1.0", + }, + { + ID: "urllib3@2.5.0", + Name: "urllib3", + Version: "2.5.0", + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "ham@3.0.0", + DependsOn: []string{"spam@1.1.0"}, + }, + }, + wantErr: assert.NoError, + }, + { + name: "sad path", + file: "testdata/sad.toml", + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + defer f.Close() + + p := pylock.NewParser() + gotPkgs, gotDeps, err := p.Parse(f) + if !tt.wantErr(t, err, fmt.Sprintf("Parse(%v)", tt.file)) { + return + } + assert.Equalf(t, tt.wantPkgs, gotPkgs, "Parse(%v)", tt.file) + assert.Equalf(t, tt.wantDeps, gotDeps, "Parse(%v)", tt.file) + }) + } +} diff --git a/pkg/dependency/parser/python/pylock/testdata/pylock.toml b/pkg/dependency/parser/python/pylock/testdata/pylock.toml new file mode 100644 index 000000000000..49580d6b60f4 --- /dev/null +++ b/pkg/dependency/parser/python/pylock/testdata/pylock.toml @@ -0,0 +1,109 @@ +lock-version = "1.0" +created-by = "pip" + +[[packages]] +name = "attrs" +version = "25.4.0" + +[[packages.wheels]] +name = "attrs-25.4.0-py3-none-any.whl" +url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl" + +[packages.wheels.hashes] +sha256 = "adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373" + +[[packages]] +name = "certifi" +version = "2025.10.5" + +[[packages.wheels]] +name = "certifi-2025.10.5-py3-none-any.whl" +url = "https://files.pythonhosted.org/packages/e4/37/af0d2ef3967ac0d6113837b44a4f0bfe1328c2b9763bd5b1744520e5cfed/certifi-2025.10.5-py3-none-any.whl" + +[packages.wheels.hashes] +sha256 = "0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de" + +[[packages]] +name = "charset-normalizer" +version = "3.4.3" + +[[packages.wheels]] +name = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" +url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" + +[packages.wheels.hashes] +sha256 = "0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07" + +[[packages]] +name = "idna" +version = "3.10" + +[[packages.wheels]] +name = "idna-3.10-py3-none-any.whl" +url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl" + +[packages.wheels.hashes] +sha256 = "946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" + +[[packages]] +name = "requests" +version = "2.32.5" + +[[packages.wheels]] +name = "requests-2.32.5-py3-none-any.whl" +url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl" + +[packages.wheels.hashes] +sha256 = "2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6" + +[[packages]] +name = "urllib3" +version = "2.5.0" + +[[packages.wheels]] +name = "urllib3-2.5.0-py3-none-any.whl" +url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl" + +[packages.wheels.hashes] +sha256 = "e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" + +# Packages with same name but different versions for different environments +[[packages]] +name = "spam" +version = "1.0.0" +marker = "sys_platform == 'linux'" + +[[packages.wheels]] +name = "spam-1.0.0-py3-none-any.whl" +url = "https://example.invalid/files/spam-1.0.0-py3-none-any.whl" + +[packages.wheels.hashes] +sha256 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + +[[packages]] +name = "spam" +version = "1.1.0" +marker = "sys_platform == 'win32'" + +[[packages.wheels]] +name = "spam-1.1.0-py3-none-any.whl" +url = "https://example.invalid/files/spam-1.1.0-py3-none-any.whl" + +[packages.wheels.hashes] +sha256 = "feebdaedfeebdaedfeebdaedfeebdaedfeebdaedfeebdaedfeebdaedfeebdaed" + +[[packages]] +name = "ham" +version = "3.0.0" +marker = "sys_platform == 'linux'" +dependencies = [ + { name = "spam", version = "1.0.0" } +] + +[[packages]] +name = "ham" +version = "3.0.0" +marker = "sys_platform == 'win32'" +dependencies = [ + { name = "spam", version = "1.1.0" } +] diff --git a/pkg/dependency/parser/python/pylock/testdata/sad.toml b/pkg/dependency/parser/python/pylock/testdata/sad.toml new file mode 100644 index 000000000000..558ed37d93c5 --- /dev/null +++ b/pkg/dependency/parser/python/pylock/testdata/sad.toml @@ -0,0 +1 @@ +[ diff --git a/pkg/dependency/parser/python/pyproject/pyproject.go b/pkg/dependency/parser/python/pyproject/pyproject.go new file mode 100644 index 000000000000..bb2b87bceb72 --- /dev/null +++ b/pkg/dependency/parser/python/pyproject/pyproject.go @@ -0,0 +1,94 @@ +package pyproject + +import ( + "io" + "strings" + + "github.com/BurntSushi/toml" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/python" + "github.com/aquasecurity/trivy/pkg/set" +) + +type PyProject struct { + Tool Tool `toml:"tool"` + Project Project `toml:"project"` +} + +type Project struct { + Dependencies Dependencies `toml:"dependencies"` +} + +type Tool struct { + Poetry Poetry `toml:"poetry"` +} + +type Poetry struct { + Dependencies Dependencies `toml:"dependencies"` + Groups map[string]Group `toml:"group"` +} + +type Group struct { + Dependencies Dependencies `toml:"dependencies"` +} + +type Dependencies struct { + set.Set[string] +} + +// MainDeps returns set of main deps +// `poetry` only uses 1 list of main dependencies +// `project.dependencies` (first priority) or `tool.poetry.dependencies` (if `project.dependencies` is missing) +func (p PyProject) MainDeps() set.Set[string] { + deps := set.New[string]() + if p.Project.Dependencies.Set != nil { + deps.Append(p.Project.Dependencies.Items()...) + } else if p.Tool.Poetry.Dependencies.Set != nil { + deps.Append(p.Tool.Poetry.Dependencies.Items()...) + } + return deps +} + +func (d *Dependencies) UnmarshalTOML(data any) error { + switch deps := data.(type) { + case map[string]any: // For Poetry v1 + d.Set = set.New[string](lo.MapToSlice(deps, func(pkgName string, _ any) string { + return python.NormalizePkgName(pkgName, true) + })...) + case []any: // For Poetry v2 + d.Set = set.New[string]() + for i := range deps { + dep, ok := deps[i].(string) + if !ok { + return xerrors.Errorf("dependencies must be string, but got: %T", deps[i]) + } + // There are some formats: + // e.g. `Flask == 1.1.4`, `Flask==1.1.4`, `Flask(>= 1.0.0)`, `pluggy[pre-commit,tox] (==0.13.1)`, etc. + dep = strings.NewReplacer(">", " ", "<", " ", "=", " ", "(", " ", "[", " ").Replace(dep) + d.Set.Append(strings.Fields(dep)[0]) // Save only name + } + default: + return xerrors.Errorf("dependencies must be map, but got: %T", data) + } + + return nil +} + +// Parser parses pyproject.toml defined in PEP518. +// https://peps.python.org/pep-0518/ +type Parser struct { +} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(r io.Reader) (PyProject, error) { + var conf PyProject + if _, err := toml.NewDecoder(r).Decode(&conf); err != nil { + return PyProject{}, xerrors.Errorf("toml decode error: %w", err) + } + return conf, nil +} diff --git a/pkg/dependency/parser/python/pyproject/pyproject_test.go b/pkg/dependency/parser/python/pyproject/pyproject_test.go new file mode 100644 index 000000000000..5f9eb46addc1 --- /dev/null +++ b/pkg/dependency/parser/python/pyproject/pyproject_test.go @@ -0,0 +1,94 @@ +package pyproject_test + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/pyproject" + "github.com/aquasecurity/trivy/pkg/set" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + file string + want pyproject.PyProject + wantErr assert.ErrorAssertionFunc + }{ + { + name: "happy path", + file: "testdata/happy.toml", + want: pyproject.PyProject{ + Tool: pyproject.Tool{ + Poetry: pyproject.Poetry{ + Dependencies: pyproject.Dependencies{ + Set: set.New[string]("flask", "python", "requests", "virtualenv"), + }, + Groups: map[string]pyproject.Group{ + "dev": { + Dependencies: pyproject.Dependencies{ + Set: set.New[string]("pytest"), + }, + }, + "lint": { + Dependencies: pyproject.Dependencies{ + Set: set.New[string]("ruff"), + }, + }, + }, + }, + }, + }, + wantErr: assert.NoError, + }, + { + name: "happy path v2", + file: "testdata/happy_v2.toml", + want: pyproject.PyProject{ + Project: pyproject.Project{ + Dependencies: pyproject.Dependencies{ + Set: set.New[string]("check-wheel-contents", "flask", "pluggy"), + }, + }, + Tool: pyproject.Tool{ + Poetry: pyproject.Poetry{ + Dependencies: pyproject.Dependencies{ + Set: set.New[string]("annotated-types", "python"), + }, + Groups: map[string]pyproject.Group{ + "dev": { + Dependencies: pyproject.Dependencies{ + Set: set.New[string]("pytest"), + }, + }, + }, + }, + }, + }, + wantErr: assert.NoError, + }, + { + name: "sad path", + file: "testdata/sad.toml", + wantErr: assert.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + defer f.Close() + + p := &pyproject.Parser{} + got, err := p.Parse(f) + if !tt.wantErr(t, err, fmt.Sprintf("Parse(%v)", tt.file)) { + return + } + assert.Equalf(t, tt.want, got, "Parse(%v)", tt.file) + }) + } +} diff --git a/pkg/dependency/parser/python/pyproject/testdata/happy.toml b/pkg/dependency/parser/python/pyproject/testdata/happy.toml new file mode 100644 index 000000000000..6fa732721162 --- /dev/null +++ b/pkg/dependency/parser/python/pyproject/testdata/happy.toml @@ -0,0 +1,26 @@ +[tool.poetry] +name = "example" +version = "0.1.0" +description = "My Hello World Example" + +[tool.poetry.dependencies] +python = "^3.9" +flask = "^1.0" +requests = {version = "2.28.1", optional = true} +virtualenv = [ + { version = "^20.4.3,!=20.4.5,!=20.4.6" }, + { version = "<20.16.6", markers = "sys_platform == 'win32' and python_version == '3.9'" }, +] + +[tool.poetry.dev-dependencies] + +[tool.poetry.group.dev.dependencies] +pytest = "8.3.4" + + +[tool.poetry.group.lint.dependencies] +ruff = "0.8.3" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pkg/dependency/parser/python/pyproject/testdata/happy_v2.toml b/pkg/dependency/parser/python/pyproject/testdata/happy_v2.toml new file mode 100644 index 000000000000..f9656f265c74 --- /dev/null +++ b/pkg/dependency/parser/python/pyproject/testdata/happy_v2.toml @@ -0,0 +1,25 @@ +[project] +name = "example" +version = "0.1.0" +description = "" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "flask(>=1.0,<2.0)", + "check-wheel-contents==0.6.1", + "pluggy[pre-commit,tox] (==0.13.1)", +] + +[tool.poetry.dependencies] +python = "^3.12" +annotated-types = "==0.7.0" + +[tool.poetry.group.dev.dependencies] +pytest = "8.3.4" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pkg/dependency/parser/python/pyproject/testdata/sad.toml b/pkg/dependency/parser/python/pyproject/testdata/sad.toml new file mode 100644 index 000000000000..8e2f0bef135b --- /dev/null +++ b/pkg/dependency/parser/python/pyproject/testdata/sad.toml @@ -0,0 +1 @@ +[ \ No newline at end of file diff --git a/pkg/dependency/parser/python/python.go b/pkg/dependency/parser/python/python.go new file mode 100644 index 000000000000..ec60ce6832a8 --- /dev/null +++ b/pkg/dependency/parser/python/python.go @@ -0,0 +1,22 @@ +package python + +import ( + "regexp" + "strings" +) + +var normalizePkgNameRegexp = regexp.MustCompile(`[-_.]+`) + +// NormalizePkgName normalizes the package name based on pep-0503 (with the option to disable conversion to lowercase). +// cf. https://peps.python.org/pep-0503/#normalized-names: +// The name should be lowercased with all runs of the characters ., -, or _ replaced with a single - character. +func NormalizePkgName(name string, inLowerCase bool) string { + name = normalizePkgNameRegexp.ReplaceAllString(name, "-") + + // pep-0503 requires that all packages names MUST be lowercase. + // But there are cases where the original case should be preserved (e.g. dist-info dir names). + if inLowerCase { + name = strings.ToLower(name) + } + return name +} diff --git a/pkg/dependency/parser/python/python_test.go b/pkg/dependency/parser/python/python_test.go new file mode 100644 index 000000000000..83d8b2213da4 --- /dev/null +++ b/pkg/dependency/parser/python/python_test.go @@ -0,0 +1,54 @@ +package python_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/python" +) + +func Test_NormalizePkgName(t *testing.T) { + tests := []struct { + pkgName string + lowerCase bool + expected string + }{ + { + pkgName: "SecretStorage", + lowerCase: true, + expected: "secretstorage", + }, + { + pkgName: "SecretStorage", + lowerCase: false, + expected: "SecretStorage", + }, + { + pkgName: "pywin32-ctypes", + lowerCase: true, + expected: "pywin32-ctypes", + }, + { + pkgName: "jaraco.classes", + lowerCase: true, + expected: "jaraco-classes", + }, + { + pkgName: "green_gdk", + lowerCase: true, + expected: "green-gdk", + }, + { + pkgName: "foo--bar__baz", + lowerCase: true, + expected: "foo-bar-baz", + }, + } + + for _, tt := range tests { + t.Run(tt.pkgName, func(t *testing.T) { + assert.Equal(t, tt.expected, python.NormalizePkgName(tt.pkgName, tt.lowerCase)) + }) + } +} diff --git a/pkg/dependency/parser/python/uv/parse.go b/pkg/dependency/parser/python/uv/parse.go new file mode 100644 index 000000000000..bae90a025b58 --- /dev/null +++ b/pkg/dependency/parser/python/uv/parse.go @@ -0,0 +1,189 @@ +package uv + +import ( + "context" + "sort" + + "github.com/BurntSushi/toml" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/set" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type Lock struct { + Packages []Package `toml:"package"` +} + +func (l Lock) packages() map[string]Package { + return lo.SliceToMap(l.Packages, func(pkg Package) (string, Package) { + return pkg.Name, pkg + }) +} + +func prodDeps(root Package, packages map[string]Package) set.Set[string] { + visited := set.New[string]() + walkPackageDeps(root, packages, visited) + return visited +} + +func walkPackageDeps(pkg Package, packages map[string]Package, visited set.Set[string]) { + if visited.Contains(pkg.Name) { + return + } + visited.Append(pkg.Name) + for depName := range pkg.nonDevDeps().Iter() { + depPkg, exists := packages[depName] + if !exists { + continue + } + walkPackageDeps(depPkg, packages, visited) + } +} + +func (l Lock) root() (Package, error) { + var pkgs []Package + for _, pkg := range l.Packages { + if pkg.isRoot() { + pkgs = append(pkgs, pkg) + } + } + + // lock file must include root package + // cf. https://github.com/astral-sh/uv/blob/f80ddf10b63c3e7b421ca4658e63f97db1e0378c/crates/uv/src/commands/project/lock.rs#L933-L936 + if len(pkgs) != 1 { + return Package{}, xerrors.New("uv lockfile must contain 1 root package") + } + + return pkgs[0], nil +} + +type Package struct { + Name string `toml:"name"` + Version string `toml:"version"` + Source Source `toml:"source"` + Dependencies Dependencies `toml:"dependencies"` + DevDependencies map[string]Dependencies `toml:"dev-dependencies"` + OptionalDependencies map[string]Dependencies `toml:"optional-dependencies"` +} + +func (p Package) directDeps() set.Set[string] { + deps := p.nonDevDeps() + for _, groupDeps := range p.DevDependencies { + deps.Append(groupDeps.toSet().Items()...) + + } + return deps +} + +func (p Package) nonDevDeps() set.Set[string] { + deps := p.Dependencies.toSet() + for _, groupDeps := range p.OptionalDependencies { + deps.Append(groupDeps.toSet().Items()...) + } + return deps +} + +type Dependencies []struct { + Name string `toml:"name"` +} + +func (d Dependencies) toSet() set.Set[string] { + deps := set.New[string]() + for _, dep := range d { + deps.Append(dep.Name) + } + return deps +} + +// https://github.com/astral-sh/uv/blob/f7d647e81d7e1e3be189324b06024ed2057168e6/crates/uv-resolver/src/lock/mod.rs#L572-L579 +func (p Package) isRoot() bool { + return p.Source.Editable == "." || p.Source.Virtual == "." +} + +type Source struct { + Editable string `toml:"editable"` + Virtual string `toml:"virtual"` +} + +type Dependency struct { + Name string `toml:"name"` +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lock Lock + if _, err := toml.NewDecoder(r).Decode(&lock); err != nil { + return nil, nil, xerrors.Errorf("failed to decode uv lock file: %w", err) + } + + rootPackage, err := lock.root() + if err != nil { + return nil, nil, err + } + + packages := lock.packages() + directDeps := rootPackage.directDeps() + + // Since each lockfile contains a root package with a list of direct dependencies, + // we can identify all production dependencies by traversing the dependency graph + // and collecting all the dependencies that are reachable from the root. + prodDeps := prodDeps(rootPackage, packages) + + var ( + pkgs ftypes.Packages + deps ftypes.Dependencies + ) + + for _, pkg := range lock.Packages { + pkgID := packageID(pkg.Name, pkg.Version) + relationship := ftypes.RelationshipIndirect + if pkg.isRoot() { + relationship = ftypes.RelationshipRoot + } else if directDeps.Contains(pkg.Name) { + relationship = ftypes.RelationshipDirect + } + + pkgs = append(pkgs, ftypes.Package{ + ID: pkgID, + Name: pkg.Name, + Version: pkg.Version, + Relationship: relationship, + Dev: !prodDeps.Contains(pkg.Name), + }) + + dependsOn := make([]string, 0, len(pkg.Dependencies)) + + for depName := range pkg.directDeps().Iter() { + depPkg, exists := packages[depName] + if !exists { + continue + } + dependsOn = append(dependsOn, packageID(depName, depPkg.Version)) + } + + if len(dependsOn) > 0 { + sort.Strings(dependsOn) + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: dependsOn, + }) + } + } + + sort.Sort(pkgs) + sort.Sort(deps) + return pkgs, deps, nil +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.Uv, name, version) +} diff --git a/pkg/dependency/parser/python/uv/parse_test.go b/pkg/dependency/parser/python/uv/parse_test.go new file mode 100644 index 000000000000..fcf2e07ae453 --- /dev/null +++ b/pkg/dependency/parser/python/uv/parse_test.go @@ -0,0 +1,56 @@ +package uv + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + file string + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + wantErr string + }{ + { + name: "normal", + file: "testdata/uv_normal.lock", + wantPkgs: uvNormal, + wantDeps: uvNormalDeps, + }, + { + name: "lockfile without root", + file: "testdata/uv_without_root.lock", + wantErr: "uv lockfile must contain 1 root package", + }, + { + name: "multiple roots", + file: "testdata/uv_multiple_roots.lock", + wantErr: "uv lockfile must contain 1 root package", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + defer f.Close() + + p := NewParser() + gotPkgs, gotDeps, err := p.Parse(t.Context(), f) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantPkgs, gotPkgs) + assert.Equal(t, tt.wantDeps, gotDeps) + }) + } +} diff --git a/pkg/dependency/parser/python/uv/parse_testcase.go b/pkg/dependency/parser/python/uv/parse_testcase.go new file mode 100644 index 000000000000..ec3203c5cd9d --- /dev/null +++ b/pkg/dependency/parser/python/uv/parse_testcase.go @@ -0,0 +1,52 @@ +package uv + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +var ( + // docker run --name uv --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh + // wget -qO- https://github.com/astral-sh/uv/releases/download/0.5.8/uv-installer.sh | sh + // source $HOME/.local/bin/env + // uv init normal && cd normal + // uv add requests==2.32.0 + // uv add --group dev pytest==8.3.4 + // uv add httpx==0.28.1 --extra socks + // uv add orjson==3.10.12 --optional json + // apk add jq + // uv pip list --format json |jq -c 'sort_by(.name) | .[] | {"ID": (.name + "@" + .version), "Name": .name, "Version": .version}' | sed 's/$/,/' | sed 's/\"\([^"]*\)\":/\1:/g' + + // add a root project + // fill in the relationships between the packages + uvNormal = []ftypes.Package{ + {ID: "normal@0.1.0", Name: "normal", Version: "0.1.0", Relationship: ftypes.RelationshipRoot}, + {ID: "httpx@0.28.1", Name: "httpx", Version: "0.28.1", Relationship: ftypes.RelationshipDirect}, + {ID: "orjson@3.10.12", Name: "orjson", Version: "3.10.12", Relationship: ftypes.RelationshipDirect}, + {ID: "pytest@8.3.4", Name: "pytest", Version: "8.3.4", Relationship: ftypes.RelationshipDirect, Dev: true}, + {ID: "requests@2.32.0", Name: "requests", Version: "2.32.0", Relationship: ftypes.RelationshipDirect}, + {ID: "anyio@4.7.0", Name: "anyio", Version: "4.7.0", Relationship: ftypes.RelationshipIndirect}, + {ID: "certifi@2024.12.14", Name: "certifi", Version: "2024.12.14", Relationship: ftypes.RelationshipIndirect}, + {ID: "charset-normalizer@3.4.0", Name: "charset-normalizer", Version: "3.4.0", Relationship: ftypes.RelationshipIndirect}, + {ID: "colorama@0.4.6", Name: "colorama", Version: "0.4.6", Relationship: ftypes.RelationshipIndirect, Dev: true}, + {ID: "exceptiongroup@1.2.2", Name: "exceptiongroup", Version: "1.2.2", Relationship: ftypes.RelationshipIndirect}, + {ID: "h11@0.14.0", Name: "h11", Version: "0.14.0", Relationship: ftypes.RelationshipIndirect}, + {ID: "httpcore@1.0.7", Name: "httpcore", Version: "1.0.7", Relationship: ftypes.RelationshipIndirect}, + {ID: "idna@3.10", Name: "idna", Version: "3.10", Relationship: ftypes.RelationshipIndirect}, + {ID: "iniconfig@2.0.0", Name: "iniconfig", Version: "2.0.0", Relationship: ftypes.RelationshipIndirect, Dev: true}, + {ID: "packaging@24.2", Name: "packaging", Version: "24.2", Relationship: ftypes.RelationshipIndirect, Dev: true}, + {ID: "pluggy@1.5.0", Name: "pluggy", Version: "1.5.0", Relationship: ftypes.RelationshipIndirect, Dev: true}, + {ID: "sniffio@1.3.1", Name: "sniffio", Version: "1.3.1", Relationship: ftypes.RelationshipIndirect}, + {ID: "socksio@1.0.0", Name: "socksio", Version: "1.0.0", Relationship: ftypes.RelationshipIndirect}, + {ID: "tomli@2.2.1", Name: "tomli", Version: "2.2.1", Relationship: ftypes.RelationshipIndirect, Dev: true}, + {ID: "typing-extensions@4.12.2", Name: "typing-extensions", Version: "4.12.2", Relationship: ftypes.RelationshipIndirect}, + {ID: "urllib3@2.2.3", Name: "urllib3", Version: "2.2.3", Relationship: ftypes.RelationshipIndirect}, + } + + // add a root project + uvNormalDeps = []ftypes.Dependency{ + {ID: "anyio@4.7.0", DependsOn: []string{"exceptiongroup@1.2.2", "idna@3.10", "sniffio@1.3.1", "typing-extensions@4.12.2"}}, + {ID: "httpcore@1.0.7", DependsOn: []string{"certifi@2024.12.14", "h11@0.14.0"}}, + {ID: "httpx@0.28.1", DependsOn: []string{"anyio@4.7.0", "certifi@2024.12.14", "httpcore@1.0.7", "idna@3.10", "socksio@1.0.0"}}, + {ID: "normal@0.1.0", DependsOn: []string{"httpx@0.28.1", "orjson@3.10.12", "pytest@8.3.4", "requests@2.32.0"}}, + {ID: "pytest@8.3.4", DependsOn: []string{"colorama@0.4.6", "exceptiongroup@1.2.2", "iniconfig@2.0.0", "packaging@24.2", "pluggy@1.5.0", "tomli@2.2.1"}}, + {ID: "requests@2.32.0", DependsOn: []string{"certifi@2024.12.14", "charset-normalizer@3.4.0", "idna@3.10", "urllib3@2.2.3"}}, + } +) diff --git a/pkg/dependency/parser/python/uv/testdata/uv_multiple_roots.lock b/pkg/dependency/parser/python/uv/testdata/uv_multiple_roots.lock new file mode 100644 index 000000000000..a0ccda991cb7 --- /dev/null +++ b/pkg/dependency/parser/python/uv/testdata/uv_multiple_roots.lock @@ -0,0 +1,30 @@ +version = 1 +requires-python = ">=3.11" + +[[package]] +name = "asyncio" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767 }, +] + +[[package]] +name = "foo" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "asyncio" }, +] + +[package.metadata] +requires-dist = [{ name = "asyncio", specifier = "==3.4.3" }] + +[[package]] +name = "bar" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "asyncio" }, +] \ No newline at end of file diff --git a/pkg/dependency/parser/python/uv/testdata/uv_normal.lock b/pkg/dependency/parser/python/uv/testdata/uv_normal.lock new file mode 100644 index 000000000000..04c63ff4b687 --- /dev/null +++ b/pkg/dependency/parser/python/uv/testdata/uv_normal.lock @@ -0,0 +1,411 @@ +version = 1 +requires-python = ">=3.9" + +[[package]] +name = "anyio" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/40/318e58f669b1a9e00f5c4453910682e2d9dd594334539c7b7817dabb765f/anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48", size = 177076 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/7a/4daaf3b6c08ad7ceffea4634ec206faeff697526421c20f07628c7372156/anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352", size = 93052 }, +] + +[[package]] +name = "certifi" +version = "2024.12.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, + { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, + { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, + { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, + { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, + { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, + { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, + { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, + { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, + { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, + { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, + { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, + { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, + { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, + { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, + { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, + { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, + { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, + { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, + { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, + { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, + { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, + { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, + { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, + { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, + { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, + { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, + { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "h11" +version = "0.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, +] + +[package.optional-dependencies] +socks = [ + { name = "socksio" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "normal" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "httpx", extra = ["socks"] }, + { name = "requests" }, +] + +[package.optional-dependencies] +json = [ + { name = "orjson" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "httpx", extras = ["socks"], specifier = "==0.28.1" }, + { name = "orjson", marker = "extra == 'json'", specifier = "==3.10.12" }, + { name = "requests", specifier = "==2.32.0" }, +] + +[package.metadata.requires-dev] +dev = [{ name = "pytest", specifier = "==8.3.4" }] + +[[package]] +name = "orjson" +version = "3.10.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/04/bb9f72987e7f62fb591d6c880c0caaa16238e4e530cbc3bdc84a7372d75f/orjson-3.10.12.tar.gz", hash = "sha256:0a78bbda3aea0f9f079057ee1ee8a1ecf790d4f1af88dd67493c6b8ee52506ff", size = 5438647 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/d2/78652b67f86d093dca984ce3fa5bf819ee1462627da83e7d0b784a9a7c45/orjson-3.10.12-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ece01a7ec71d9940cc654c482907a6b65df27251255097629d0dea781f255c6d", size = 248688 }, + { url = "https://files.pythonhosted.org/packages/70/cb/f8b6a52f3bc724edf8a62d8d1d8ee17cf19d6ae1cac89f077f0e7c30f396/orjson-3.10.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c34ec9aebc04f11f4b978dd6caf697a2df2dd9b47d35aa4cc606cabcb9df69d7", size = 136952 }, + { url = "https://files.pythonhosted.org/packages/a6/43/c55700df9814545bc8c35d87395ec4b9ee473a3c1f5ed72f8d3ad0298ee9/orjson-3.10.12-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd6ec8658da3480939c79b9e9e27e0db31dffcd4ba69c334e98c9976ac29140e", size = 149089 }, + { url = "https://files.pythonhosted.org/packages/07/da/e7e7d73bd971710b736fbd8330b8830c5fa4fc0ac003b31af61f03b26dfc/orjson-3.10.12-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f17e6baf4cf01534c9de8a16c0c611f3d94925d1701bf5f4aff17003677d8ced", size = 140479 }, + { url = "https://files.pythonhosted.org/packages/08/49/c9dfddba56ff24eecfacf2f01a76cae4d249ac2995b1359bf63a74b1b318/orjson-3.10.12-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6402ebb74a14ef96f94a868569f5dccf70d791de49feb73180eb3c6fda2ade56", size = 156564 }, + { url = "https://files.pythonhosted.org/packages/96/df/174d2eff227dc23b4540a0c2efa6ec8fe406c442c4b7f0f556242f026d1f/orjson-3.10.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0000758ae7c7853e0a4a6063f534c61656ebff644391e1f81698c1b2d2fc8cd2", size = 131282 }, + { url = "https://files.pythonhosted.org/packages/6a/96/8628c53a52e2a0a1ee861d809092df72aabbd312c71de9ad6d49e2c039ab/orjson-3.10.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:888442dcee99fd1e5bd37a4abb94930915ca6af4db50e23e746cdf4d1e63db13", size = 139764 }, + { url = "https://files.pythonhosted.org/packages/38/17/08becb49e59e7bb7b29dc1dad19bc0c48635e627ee27e60eb5b64efcf7b1/orjson-3.10.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c1f7a3ce79246aa0e92f5458d86c54f257fb5dfdc14a192651ba7ec2c00f8a05", size = 131913 }, + { url = "https://files.pythonhosted.org/packages/2a/05/f32acc2500e3fafee9445eb8b2a6ff19c4641035e6059c6c8d7bdb3abc9e/orjson-3.10.12-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:802a3935f45605c66fb4a586488a38af63cb37aaad1c1d94c982c40dcc452e85", size = 415782 }, + { url = "https://files.pythonhosted.org/packages/06/03/6cc740d998d8bb60e75d4b7e228d18964475239ac842cc1865d49d092545/orjson-3.10.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1da1ef0113a2be19bb6c557fb0ec2d79c92ebd2fed4cfb1b26bab93f021fb885", size = 142383 }, + { url = "https://files.pythonhosted.org/packages/f8/30/39cac82547fe021615376245c558b216d3ae8c99bd6b2274f312e49f1c94/orjson-3.10.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7a3273e99f367f137d5b3fecb5e9f45bcdbfac2a8b2f32fbc72129bbd48789c2", size = 130661 }, + { url = "https://files.pythonhosted.org/packages/95/29/c6837f4fc1eaa742eaf5abcd767ab6805493f44fe1f72b37c1743706c1d8/orjson-3.10.12-cp310-none-win32.whl", hash = "sha256:475661bf249fd7907d9b0a2a2421b4e684355a77ceef85b8352439a9163418c3", size = 143625 }, + { url = "https://files.pythonhosted.org/packages/f6/62/c6b955f2144421108fa441b5471e1d5f8654a7df9840b261106e04d5d15c/orjson-3.10.12-cp310-none-win_amd64.whl", hash = "sha256:87251dc1fb2b9e5ab91ce65d8f4caf21910d99ba8fb24b49fd0c118b2362d509", size = 135075 }, + { url = "https://files.pythonhosted.org/packages/d3/48/7c3cd094488f5a3bc58488555244609a8c4d105bc02f2b77e509debf0450/orjson-3.10.12-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a734c62efa42e7df94926d70fe7d37621c783dea9f707a98cdea796964d4cf74", size = 248687 }, + { url = "https://files.pythonhosted.org/packages/ff/90/e55f0e25c7fdd1f82551fe787f85df6f378170caca863c04c810cd8f2730/orjson-3.10.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:750f8b27259d3409eda8350c2919a58b0cfcd2054ddc1bd317a643afc646ef23", size = 136953 }, + { url = "https://files.pythonhosted.org/packages/2a/b3/109c020cf7fee747d400de53b43b183ca9d3ebda3906ad0b858eb5479718/orjson-3.10.12-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb52c22bfffe2857e7aa13b4622afd0dd9d16ea7cc65fd2bf318d3223b1b6252", size = 149090 }, + { url = "https://files.pythonhosted.org/packages/96/d4/35c0275dc1350707d182a1b5da16d1184b9439848060af541285407f18f9/orjson-3.10.12-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:440d9a337ac8c199ff8251e100c62e9488924c92852362cd27af0e67308c16ef", size = 140480 }, + { url = "https://files.pythonhosted.org/packages/3b/79/f863ff460c291ad2d882cc3b580cc444bd4ec60c9df55f6901e6c9a3f519/orjson-3.10.12-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9e15c06491c69997dfa067369baab3bf094ecb74be9912bdc4339972323f252", size = 156564 }, + { url = "https://files.pythonhosted.org/packages/98/7e/8d5835449ddd873424ee7b1c4ba73a0369c1055750990d824081652874d6/orjson-3.10.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:362d204ad4b0b8724cf370d0cd917bb2dc913c394030da748a3bb632445ce7c4", size = 131279 }, + { url = "https://files.pythonhosted.org/packages/46/f5/d34595b6d7f4f984c6fef289269a7f98abcdc2445ebdf90e9273487dda6b/orjson-3.10.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2b57cbb4031153db37b41622eac67329c7810e5f480fda4cfd30542186f006ae", size = 139764 }, + { url = "https://files.pythonhosted.org/packages/b3/5b/ee6e9ddeab54a7b7806768151c2090a2d36025bc346a944f51cf172ef7f7/orjson-3.10.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:165c89b53ef03ce0d7c59ca5c82fa65fe13ddf52eeb22e859e58c237d4e33b9b", size = 131915 }, + { url = "https://files.pythonhosted.org/packages/c4/45/febee5951aef6db5cd8cdb260548101d7ece0ca9d4ddadadf1766306b7a4/orjson-3.10.12-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5dee91b8dfd54557c1a1596eb90bcd47dbcd26b0baaed919e6861f076583e9da", size = 415783 }, + { url = "https://files.pythonhosted.org/packages/27/a5/5a8569e49f3a6c093bee954a3de95062a231196f59e59df13a48e2420081/orjson-3.10.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:77a4e1cfb72de6f905bdff061172adfb3caf7a4578ebf481d8f0530879476c07", size = 142387 }, + { url = "https://files.pythonhosted.org/packages/6e/05/02550fb38c5bf758f3994f55401233a2ef304e175f473f2ac6dbf464cc8b/orjson-3.10.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:038d42c7bc0606443459b8fe2d1f121db474c49067d8d14c6a075bbea8bf14dd", size = 130664 }, + { url = "https://files.pythonhosted.org/packages/8c/f4/ba31019d0646ce51f7ac75af6dabf98fd89dbf8ad87a9086da34710738e7/orjson-3.10.12-cp311-none-win32.whl", hash = "sha256:03b553c02ab39bed249bedd4abe37b2118324d1674e639b33fab3d1dafdf4d79", size = 143623 }, + { url = "https://files.pythonhosted.org/packages/83/fe/babf08842b989acf4c46103fefbd7301f026423fab47e6f3ba07b54d7837/orjson-3.10.12-cp311-none-win_amd64.whl", hash = "sha256:8b8713b9e46a45b2af6b96f559bfb13b1e02006f4242c156cbadef27800a55a8", size = 135074 }, + { url = "https://files.pythonhosted.org/packages/a1/2f/989adcafad49afb535da56b95d8f87d82e748548b2a86003ac129314079c/orjson-3.10.12-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53206d72eb656ca5ac7d3a7141e83c5bbd3ac30d5eccfe019409177a57634b0d", size = 248678 }, + { url = "https://files.pythonhosted.org/packages/69/b9/8c075e21a50c387649db262b618ebb7e4d40f4197b949c146fc225dd23da/orjson-3.10.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac8010afc2150d417ebda810e8df08dd3f544e0dd2acab5370cfa6bcc0662f8f", size = 136763 }, + { url = "https://files.pythonhosted.org/packages/87/d3/78edf10b4ab14c19f6d918cf46a145818f4aca2b5a1773c894c5490d3a4c/orjson-3.10.12-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed459b46012ae950dd2e17150e838ab08215421487371fa79d0eced8d1461d70", size = 149137 }, + { url = "https://files.pythonhosted.org/packages/16/81/5db8852bdf990a0ddc997fa8f16b80895b8cc77c0fe3701569ed2b4b9e78/orjson-3.10.12-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dcb9673f108a93c1b52bfc51b0af422c2d08d4fc710ce9c839faad25020bb69", size = 140567 }, + { url = "https://files.pythonhosted.org/packages/fa/a6/9ce1e3e3db918512efadad489630c25841eb148513d21dab96f6b4157fa1/orjson-3.10.12-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22a51ae77680c5c4652ebc63a83d5255ac7d65582891d9424b566fb3b5375ee9", size = 156620 }, + { url = "https://files.pythonhosted.org/packages/47/d4/05133d6bea24e292d2f7628b1e19986554f7d97b6412b3e51d812e38db2d/orjson-3.10.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:910fdf2ac0637b9a77d1aad65f803bac414f0b06f720073438a7bd8906298192", size = 131555 }, + { url = "https://files.pythonhosted.org/packages/b9/7a/b3fbffda8743135c7811e95dc2ab7cdbc5f04999b83c2957d046f1b3fac9/orjson-3.10.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:24ce85f7100160936bc2116c09d1a8492639418633119a2224114f67f63a4559", size = 139743 }, + { url = "https://files.pythonhosted.org/packages/b5/13/95bbcc9a6584aa083da5ce5004ce3d59ea362a542a0b0938d884fd8790b6/orjson-3.10.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a76ba5fc8dd9c913640292df27bff80a685bed3a3c990d59aa6ce24c352f8fc", size = 131733 }, + { url = "https://files.pythonhosted.org/packages/e8/29/dddbb2ea6e7af426fcc3da65a370618a88141de75c6603313d70768d1df1/orjson-3.10.12-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ff70ef093895fd53f4055ca75f93f047e088d1430888ca1229393a7c0521100f", size = 415788 }, + { url = "https://files.pythonhosted.org/packages/53/df/4aea59324ac539975919b4705ee086aced38e351a6eb3eea0f5071dd5661/orjson-3.10.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:f4244b7018b5753ecd10a6d324ec1f347da130c953a9c88432c7fbc8875d13be", size = 142347 }, + { url = "https://files.pythonhosted.org/packages/55/55/a52d83d7c49f8ff44e0daab10554490447d6c658771569e1c662aa7057fe/orjson-3.10.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:16135ccca03445f37921fa4b585cff9a58aa8d81ebcb27622e69bfadd220b32c", size = 130829 }, + { url = "https://files.pythonhosted.org/packages/a1/8b/b1beb1624dd4adf7d72e2d9b73c4b529e7851c0c754f17858ea13e368b33/orjson-3.10.12-cp312-none-win32.whl", hash = "sha256:2d879c81172d583e34153d524fcba5d4adafbab8349a7b9f16ae511c2cee8708", size = 143659 }, + { url = "https://files.pythonhosted.org/packages/13/91/634c9cd0bfc6a857fc8fab9bf1a1bd9f7f3345e0d6ca5c3d4569ceb6dcfa/orjson-3.10.12-cp312-none-win_amd64.whl", hash = "sha256:fc23f691fa0f5c140576b8c365bc942d577d861a9ee1142e4db468e4e17094fb", size = 135221 }, + { url = "https://files.pythonhosted.org/packages/1b/bb/3f560735f46fa6f875a9d7c4c2171a58cfb19f56a633d5ad5037a924f35f/orjson-3.10.12-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:47962841b2a8aa9a258b377f5188db31ba49af47d4003a32f55d6f8b19006543", size = 248662 }, + { url = "https://files.pythonhosted.org/packages/a3/df/54817902350636cc9270db20486442ab0e4db33b38555300a1159b439d16/orjson-3.10.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6334730e2532e77b6054e87ca84f3072bee308a45a452ea0bffbbbc40a67e296", size = 126055 }, + { url = "https://files.pythonhosted.org/packages/2e/77/55835914894e00332601a74540840f7665e81f20b3e2b9a97614af8565ed/orjson-3.10.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:accfe93f42713c899fdac2747e8d0d5c659592df2792888c6c5f829472e4f85e", size = 131507 }, + { url = "https://files.pythonhosted.org/packages/33/9e/b91288361898e3158062a876b5013c519a5d13e692ac7686e3486c4133ab/orjson-3.10.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a7974c490c014c48810d1dede6c754c3cc46598da758c25ca3b4001ac45b703f", size = 131686 }, + { url = "https://files.pythonhosted.org/packages/b2/15/08ce117d60a4d2d3fd24e6b21db463139a658e9f52d22c9c30af279b4187/orjson-3.10.12-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3f250ce7727b0b2682f834a3facff88e310f52f07a5dcfd852d99637d386e79e", size = 415710 }, + { url = "https://files.pythonhosted.org/packages/71/af/c09da5ed58f9c002cf83adff7a4cdf3e6cee742aa9723395f8dcdb397233/orjson-3.10.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:f31422ff9486ae484f10ffc51b5ab2a60359e92d0716fcce1b3593d7bb8a9af6", size = 142305 }, + { url = "https://files.pythonhosted.org/packages/17/d1/8612038d44f33fae231e9ba480d273bac2b0383ce9e77cb06bede1224ae3/orjson-3.10.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5f29c5d282bb2d577c2a6bbde88d8fdcc4919c593f806aac50133f01b733846e", size = 130815 }, + { url = "https://files.pythonhosted.org/packages/67/2c/d5f87834be3591555cfaf9aecdf28f480a6f0b4afeaac53bad534bf9518f/orjson-3.10.12-cp313-none-win32.whl", hash = "sha256:f45653775f38f63dc0e6cd4f14323984c3149c05d6007b58cb154dd080ddc0dc", size = 143664 }, + { url = "https://files.pythonhosted.org/packages/6a/05/7d768fa3ca23c9b3e1e09117abeded1501119f1d8de0ab722938c91ab25d/orjson-3.10.12-cp313-none-win_amd64.whl", hash = "sha256:229994d0c376d5bdc91d92b3c9e6be2f1fbabd4cc1b59daae1443a46ee5e9825", size = 134944 }, + { url = "https://files.pythonhosted.org/packages/01/50/5a52cfe65fc70e7b843cbe143b850313095d4e45f99aeb278b4b3691f3cf/orjson-3.10.12-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:f29de3ef71a42a5822765def1febfb36e0859d33abf5c2ad240acad5c6a1b78d", size = 249130 }, + { url = "https://files.pythonhosted.org/packages/af/42/adb00fc60890e57ee6022257d70d9a20dfd28bfd955401e2d02a60192226/orjson-3.10.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de365a42acc65d74953f05e4772c974dad6c51cfc13c3240899f534d611be967", size = 136784 }, + { url = "https://files.pythonhosted.org/packages/f5/f6/5ef130a2e28a0c633c82da67224212fa84b17f93d9d768c255f389d66641/orjson-3.10.12-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91a5a0158648a67ff0004cb0df5df7dcc55bfc9ca154d9c01597a23ad54c8d0c", size = 148936 }, + { url = "https://files.pythonhosted.org/packages/54/80/f40805bb094d3470bcfca8d30ae9f4606bb90c809190fee18c17a4653956/orjson-3.10.12-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c47ce6b8d90fe9646a25b6fb52284a14ff215c9595914af63a5933a49972ce36", size = 140291 }, + { url = "https://files.pythonhosted.org/packages/b4/0a/c91265a075af28fcb118a9a690dccc0f31c52dd7f486f5cc73aa6f9a69b4/orjson-3.10.12-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0eee4c2c5bfb5c1b47a5db80d2ac7aaa7e938956ae88089f098aff2c0f35d5d8", size = 156335 }, + { url = "https://files.pythonhosted.org/packages/6f/1f/c8fc64b25e2e4a5fa0fb514a0325719e99461c088a3b659954a57b8c1f3c/orjson-3.10.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:35d3081bbe8b86587eb5c98a73b97f13d8f9fea685cf91a579beddacc0d10566", size = 131118 }, + { url = "https://files.pythonhosted.org/packages/b3/f7/f5dba457bbc6aaf8ebd0131a17eb672835d2ea31f763d1462c191902bc10/orjson-3.10.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:73c23a6e90383884068bc2dba83d5222c9fcc3b99a0ed2411d38150734236755", size = 139573 }, + { url = "https://files.pythonhosted.org/packages/85/03/87091a1c831076e8d6ed0be19d30bd837d3c27e29f43b6b2dc23efee3d29/orjson-3.10.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5472be7dc3269b4b52acba1433dac239215366f89dc1d8d0e64029abac4e714e", size = 131760 }, + { url = "https://files.pythonhosted.org/packages/53/70/956ca7999fe43b5e2ca51248f732850acaaf00f8fa5b8f7924bc504157bb/orjson-3.10.12-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:7319cda750fca96ae5973efb31b17d97a5c5225ae0bc79bf5bf84df9e1ec2ab6", size = 415605 }, + { url = "https://files.pythonhosted.org/packages/ca/7f/4f49c1a9da03e383198c0fa418cc4262d01d70931742dfc504c2a495ed7b/orjson-3.10.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:74d5ca5a255bf20b8def6a2b96b1e18ad37b4a122d59b154c458ee9494377f80", size = 142208 }, + { url = "https://files.pythonhosted.org/packages/81/d9/0216c950e295b1c493510ef546b80328dc06e2ae4a82bc693386e4cdebeb/orjson-3.10.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ff31d22ecc5fb85ef62c7d4afe8301d10c558d00dd24274d4bbe464380d3cd69", size = 130495 }, + { url = "https://files.pythonhosted.org/packages/1a/d0/70d97b583d628a307d4bc1f96f7ab6cf137e32849c06ea03d4337ad19e14/orjson-3.10.12-cp39-none-win32.whl", hash = "sha256:c22c3ea6fba91d84fcb4cda30e64aff548fcf0c44c876e681f47d61d24b12e6b", size = 143467 }, + { url = "https://files.pythonhosted.org/packages/ec/a3/725e4cac70d2a88f5fc4f9eed451e12d594063823feb931c30ac1394d26f/orjson-3.10.12-cp39-none-win_amd64.whl", hash = "sha256:be604f60d45ace6b0b33dd990a66b4526f1a7a186ac411c942674625456ca548", size = 134927 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "requests" +version = "2.32.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/a2/423f4d16d6934ef502f10ad56673719dd4345e656aedbd6687ccc359ffc5/requests-2.32.0.tar.gz", hash = "sha256:fa5490319474c82ef1d2c9bc459d3652e3ae4ef4c4ebdd18a21145a47ca4b6b8", size = 114373 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/e8/09e8d662a9675a4e4f5dd7a8e6127b463a091d2703ed931a64aa66d00065/requests-2.32.0-py3-none-any.whl", hash = "sha256:f2c3881dddb70d056c5bd7600a4fae312b2a300e39be6a118d30b90bd27262b5", size = 63695 }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, +] + +[[package]] +name = "socksio" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] \ No newline at end of file diff --git a/pkg/dependency/parser/python/uv/testdata/uv_without_root.lock b/pkg/dependency/parser/python/uv/testdata/uv_without_root.lock new file mode 100644 index 000000000000..a105e74f266a --- /dev/null +++ b/pkg/dependency/parser/python/uv/testdata/uv_without_root.lock @@ -0,0 +1,11 @@ +version = 1 +requires-python = ">=3.11" + +[[package]] +name = "asyncio" +version = "3.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767 }, +] \ No newline at end of file diff --git a/pkg/dependency/parser/ruby/bundler/parse.go b/pkg/dependency/parser/ruby/bundler/parse.go new file mode 100644 index 000000000000..5c2ac6b905c6 --- /dev/null +++ b/pkg/dependency/parser/ruby/bundler/parse.go @@ -0,0 +1,143 @@ +package bundler + +import ( + "bufio" + "context" + "sort" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + pkgs := make(map[string]ftypes.Package) + var dependsOn, directDeps []string + var deps []ftypes.Dependency + var pkgID string + + lineNum := 1 + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + + // Parse dependencies + if countLeadingSpace(line) == 4 { + if len(dependsOn) > 0 { + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: dependsOn, + }) + } + dependsOn = make([]string, 0) // re-initialize + line = strings.TrimSpace(line) + s := strings.Fields(line) + if len(s) != 2 { + continue + } + version := strings.Trim(s[1], "()") // drop parentheses + version = strings.SplitN(version, "-", 2)[0] // drop platform (e.g. 1.13.6-x86_64-linux => 1.13.6) + name := s[0] + pkgID = packageID(name, version) + pkgs[name] = ftypes.Package{ + ID: pkgID, + Name: name, + Version: version, + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: lineNum, + EndLine: lineNum, + }, + }, + } + } + // Parse dependency graph + if countLeadingSpace(line) == 6 { + line = strings.TrimSpace(line) + s := strings.Fields(line) + dependsOn = append(dependsOn, s[0]) // store name only for now + } + lineNum++ + + // Parse direct dependencies + if line == "DEPENDENCIES" { + directDeps = parseDirectDeps(scanner) + } + } + // append last dependency (if any) + if len(dependsOn) > 0 { + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: dependsOn, + }) + } + + // Identify which are direct dependencies + for _, d := range directDeps { + if l, ok := pkgs[d]; ok { + l.Relationship = ftypes.RelationshipDirect + pkgs[d] = l + } + } + + for i, dep := range deps { + dependsOn = make([]string, 0) + for _, pkgName := range dep.DependsOn { + if pkg, ok := pkgs[pkgName]; ok { + dependsOn = append(dependsOn, packageID(pkgName, pkg.Version)) + } + } + deps[i].DependsOn = dependsOn + } + if err := scanner.Err(); err != nil { + return nil, nil, xerrors.Errorf("scan error: %w", err) + } + + pkgSlice := lo.Values(pkgs) + sort.Sort(ftypes.Packages(pkgSlice)) + return pkgSlice, deps, nil +} + +func countLeadingSpace(line string) int { + i := 0 + for _, runeValue := range line { + if runeValue != ' ' { + break + } + i++ + } + return i +} + +// Parse "DEPENDENCIES" +func parseDirectDeps(scanner *bufio.Scanner) []string { + var deps []string + for scanner.Scan() { + line := scanner.Text() + if countLeadingSpace(line) != 2 { + // Reach another section + break + } + ss := strings.Fields(line) + if len(ss) == 0 { + continue + } + deps = append(deps, ss[0]) + } + return deps +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.Bundler, name, version) +} diff --git a/pkg/dependency/parser/ruby/bundler/parse_test.go b/pkg/dependency/parser/ruby/bundler/parse_test.go new file mode 100644 index 000000000000..6d904e2248de --- /dev/null +++ b/pkg/dependency/parser/ruby/bundler/parse_test.go @@ -0,0 +1,280 @@ +package bundler_test + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/ruby/bundler" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + NormalPkgs = []ftypes.Package{ + { + ID: "dotenv@2.7.2", + Name: "dotenv", + Version: "2.7.2", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 6, + EndLine: 6, + }, + }, + }, + { + ID: "faker@1.9.3", + Name: "faker", + Version: "1.9.3", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 7, + EndLine: 7, + }, + }, + }, + { + ID: "pry@0.12.2", + Name: "pry", + Version: "0.12.2", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 12, + EndLine: 12, + }, + }, + }, + { + ID: "coderay@1.1.2", + Name: "coderay", + Version: "1.1.2", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + { + ID: "concurrent-ruby@1.1.5", + Name: "concurrent-ruby", + Version: "1.1.5", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + ID: "i18n@1.6.0", + Name: "i18n", + Version: "1.6.0", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 9, + EndLine: 9, + }, + }, + }, + { + ID: "method_source@0.9.2", + Name: "method_source", + Version: "0.9.2", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 11, + EndLine: 11, + }, + }, + }, + } + NormalDeps = []ftypes.Dependency{ + { + ID: "faker@1.9.3", + DependsOn: []string{"i18n@1.6.0"}, + }, + { + ID: "i18n@1.6.0", + DependsOn: []string{"concurrent-ruby@1.1.5"}, + }, + { + ID: "pry@0.12.2", + DependsOn: []string{ + "coderay@1.1.2", + "method_source@0.9.2", + }, + }, + } + Bundler2Pkgs = []ftypes.Package{ + { + ID: "dotenv@2.7.6", + Name: "dotenv", + Version: "2.7.6", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 6, + EndLine: 6, + }, + }, + }, + { + ID: "faker@2.21.0", + Name: "faker", + Version: "2.21.0", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 7, + EndLine: 7, + }, + }, + }, + { + ID: "json@2.6.2", + Name: "json", + Version: "2.6.2", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 11, + EndLine: 11, + }, + }, + }, + { + ID: "pry@0.14.1", + Name: "pry", + Version: "0.14.1", + Relationship: ftypes.RelationshipDirect, + Locations: []ftypes.Location{ + { + StartLine: 13, + EndLine: 13, + }, + }, + }, + { + ID: "coderay@1.1.3", + Name: "coderay", + Version: "1.1.3", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + { + ID: "concurrent-ruby@1.1.10", + Name: "concurrent-ruby", + Version: "1.1.10", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + ID: "i18n@1.10.0", + Name: "i18n", + Version: "1.10.0", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 9, + EndLine: 9, + }, + }, + }, + { + ID: "method_source@1.0.0", + Name: "method_source", + Version: "1.0.0", + Relationship: ftypes.RelationshipIndirect, + Locations: []ftypes.Location{ + { + StartLine: 12, + EndLine: 12, + }, + }, + }, + } + Bundler2Deps = []ftypes.Dependency{ + { + ID: "faker@2.21.0", + DependsOn: []string{"i18n@1.10.0"}, + }, + { + ID: "i18n@1.10.0", + DependsOn: []string{"concurrent-ruby@1.1.10"}, + }, + { + ID: "pry@0.14.1", + DependsOn: []string{ + "coderay@1.1.3", + "method_source@1.0.0", + }, + }, + } +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + file string + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + wantErr assert.ErrorAssertionFunc + }{ + { + name: "normal", + file: "testdata/Gemfile_normal.lock", + wantPkgs: NormalPkgs, + wantDeps: NormalDeps, + wantErr: assert.NoError, + }, + { + name: "bundler2", + file: "testdata/Gemfile_bundler2.lock", + wantPkgs: Bundler2Pkgs, + wantDeps: Bundler2Deps, + wantErr: assert.NoError, + }, + { + name: "malformed", + file: "testdata/Gemfile_malformed.lock", + wantPkgs: []ftypes.Package{}, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + defer f.Close() + + p := &bundler.Parser{} + gotPkgs, gotDeps, err := p.Parse(t.Context(), f) + if !tt.wantErr(t, err, fmt.Sprintf("Parse(%v)", tt.file)) { + return + } + assert.Equalf(t, tt.wantPkgs, gotPkgs, "Parse(%v)", tt.file) + assert.Equalf(t, tt.wantDeps, gotDeps, "Parse(%v)", tt.file) + }) + } +} diff --git a/pkg/dependency/parser/ruby/bundler/testdata/Gemfile_bundler2.lock b/pkg/dependency/parser/ruby/bundler/testdata/Gemfile_bundler2.lock new file mode 100644 index 000000000000..55c0d8370cf3 --- /dev/null +++ b/pkg/dependency/parser/ruby/bundler/testdata/Gemfile_bundler2.lock @@ -0,0 +1,27 @@ +GEM + remote: https://rubygems.org/ + specs: + coderay (1.1.3) + concurrent-ruby (1.1.10) + dotenv (2.7.6) + faker (2.21.0) + i18n (>= 1.8.11, < 2) + i18n (1.10.0) + concurrent-ruby (~> 1.0) + json (2.6.2) + method_source (1.0.0) + pry (0.14.1) + coderay (~> 1.1) + method_source (~> 1.0) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + dotenv (~> 2.7) + faker (~> 2.21) + json (~> 2.6) + pry (~> 0.14.1) + +BUNDLED WITH + 2.3.7 \ No newline at end of file diff --git a/pkg/dependency/parser/ruby/bundler/testdata/Gemfile_malformed.lock b/pkg/dependency/parser/ruby/bundler/testdata/Gemfile_malformed.lock new file mode 100644 index 000000000000..eca77f35437e --- /dev/null +++ b/pkg/dependency/parser/ruby/bundler/testdata/Gemfile_malformed.lock @@ -0,0 +1 @@ +malformed \ No newline at end of file diff --git a/pkg/dependency/parser/ruby/bundler/testdata/Gemfile_normal.lock b/pkg/dependency/parser/ruby/bundler/testdata/Gemfile_normal.lock new file mode 100644 index 000000000000..d213f9f62322 --- /dev/null +++ b/pkg/dependency/parser/ruby/bundler/testdata/Gemfile_normal.lock @@ -0,0 +1,25 @@ +GEM + remote: https://rubygems.org/ + specs: + coderay (1.1.2) + concurrent-ruby (1.1.5) + dotenv (2.7.2) + faker (1.9.3) + i18n (>= 0.7) + i18n (1.6.0) + concurrent-ruby (~> 1.0) + method_source (0.9.2) + pry (0.12.2) + coderay (~> 1.1.0) + method_source (~> 0.9.0) + +PLATFORMS + ruby + +DEPENDENCIES + dotenv (~> 2.7) + faker (~> 1.9) + pry (~> 0.12.2) + +BUNDLED WITH + 1.17.2 diff --git a/pkg/dependency/parser/ruby/gemspec/parse.go b/pkg/dependency/parser/ruby/gemspec/parse.go new file mode 100644 index 000000000000..4ff65c96a649 --- /dev/null +++ b/pkg/dependency/parser/ruby/gemspec/parse.go @@ -0,0 +1,137 @@ +package gemspec + +import ( + "bufio" + "context" + "fmt" + "regexp" + "strings" + + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +const specNewStr = "Gem::Specification.new" + +var ( + // Capture the variable name + // e.g. Gem::Specification.new do |s| + // => s + newVarRegexp = regexp.MustCompile(`\|(?P.*)\|`) + + // Capture the value of "name" + // e.g. s.name = "async".freeze + // => "async".freeze + nameRegexp = regexp.MustCompile(`\.name\s*=\s*(?P\S+)`) + + // Capture the value of "version" + // e.g. s.version = "1.2.3" + // => "1.2.3" + versionRegexp = regexp.MustCompile(`\.version\s*=\s*(?P\S+)`) + + // Capture the value of "license" + // e.g. s.license = "MIT" + // => "MIT" + licenseRegexp = regexp.MustCompile(`\.license\s*=\s*(?P\S+)`) + + // Capture the value of "licenses" + // e.g. s.license = ["MIT".freeze, "BSDL".freeze] + // => "MIT".freeze, "BSDL".freeze + licensesRegexp = regexp.MustCompile(`\.licenses\s*=\s*\[(?P.+)\]`) +) + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) (pkgs []ftypes.Package, deps []ftypes.Dependency, err error) { + var newVar, name, version, license string + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if strings.Contains(line, specNewStr) { + newVar = findSubString(newVarRegexp, line, "var") + } + + if newVar == "" { + continue + } + + // Capture name, version, license, and licenses + switch { + case strings.HasPrefix(line, fmt.Sprintf("%s.name", newVar)): + // https://guides.rubygems.org/specification-reference/#name + name = findSubString(nameRegexp, line, "name") + name = trim(name) + case strings.HasPrefix(line, fmt.Sprintf("%s.version", newVar)): + // https://guides.rubygems.org/specification-reference/#version + version = findSubString(versionRegexp, line, "version") + version = trim(version) + case strings.HasPrefix(line, fmt.Sprintf("%s.licenses", newVar)): + // https://guides.rubygems.org/specification-reference/#licenses= + license = findSubString(licensesRegexp, line, "licenses") + license = parseLicenses(license) + case strings.HasPrefix(line, fmt.Sprintf("%s.license", newVar)): + // https://guides.rubygems.org/specification-reference/#license= + license = findSubString(licenseRegexp, line, "license") + license = trim(license) + } + + // No need to iterate the loop anymore + if name != "" && version != "" && license != "" { + break + } + } + if err := scanner.Err(); err != nil { + return nil, nil, xerrors.Errorf("failed to parse gemspec: %w", err) + } + + if name == "" || version == "" { + return nil, nil, xerrors.New("failed to parse gemspec") + } + + return []ftypes.Package{ + { + Name: name, + Version: version, + Licenses: licensing.SplitLicenses(license), + }, + }, nil, nil +} + +func findSubString(re *regexp.Regexp, line, name string) string { + m := re.FindStringSubmatch(line) + if m == nil { + return "" + } + return m[re.SubexpIndex(name)] +} + +// Trim single quotes, double quotes and ".freeze" +// e.g. "async".freeze => async +func trim(s string) string { + s = strings.TrimSpace(s) + s = strings.TrimSuffix(s, ".freeze") + return strings.Trim(s, `'"`) +} + +func parseLicenses(s string) string { + // e.g. `"Ruby".freeze, "BSDL".freeze` + // => {"\"Ruby\".freeze", "\"BSDL\".freeze"} + ss := strings.SplitSeq(s, ",") + + // e.g. {"\"Ruby\".freeze", "\"BSDL\".freeze"} + // => {"Ruby", "BSDL"} + var licenses []string + for l := range ss { + licenses = append(licenses, trim(l)) + } + + return strings.Join(licenses, ", ") +} diff --git a/pkg/dependency/parser/ruby/gemspec/parse_test.go b/pkg/dependency/parser/ruby/gemspec/parse_test.go new file mode 100644 index 000000000000..92d7c58015e2 --- /dev/null +++ b/pkg/dependency/parser/ruby/gemspec/parse_test.go @@ -0,0 +1,92 @@ +package gemspec_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/ruby/gemspec" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []ftypes.Package + wantErr string + }{ + { + name: "happy", + inputFile: "testdata/normal00.gemspec", + want: []ftypes.Package{ + { + Name: "rake", + Version: "13.0.3", + Licenses: []string{"MIT"}, + }, + }, + }, + { + name: "another variable name", + inputFile: "testdata/normal01.gemspec", + want: []ftypes.Package{ + { + Name: "async", + Version: "1.25.0", + }, + }, + }, + { + name: "license", + inputFile: "testdata/license.gemspec", + want: []ftypes.Package{ + { + Name: "async", + Version: "1.25.0", + Licenses: []string{"MIT"}, + }, + }, + }, + { + name: "multiple licenses", + inputFile: "testdata/multiple_licenses.gemspec", + want: []ftypes.Package{ + { + Name: "test-unit", + Version: "3.3.7", + Licenses: []string{ + "Ruby", + "BSDL", + "PSFL", + }, + }, + }, + }, + { + name: "malformed variable name", + inputFile: "testdata/malformed00.gemspec", + wantErr: "failed to parse gemspec", + }, + { + name: "missing version", + inputFile: "testdata/malformed01.gemspec", + wantErr: "failed to parse gemspec", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + got, _, err := gemspec.NewParser().Parse(t.Context(), f) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/dependency/parser/ruby/gemspec/testdata/license.gemspec b/pkg/dependency/parser/ruby/gemspec/testdata/license.gemspec new file mode 100644 index 000000000000..ef7dbe77c65a --- /dev/null +++ b/pkg/dependency/parser/ruby/gemspec/testdata/license.gemspec @@ -0,0 +1,10 @@ +# -*- encoding: utf-8 -*- +# ... REDACTED ... + +Gem::Specification.new do |spec| + spec.name = "async".freeze + spec.version = "1.25.0" + spec.license = "MIT" + + # ... REDACTED ... +end diff --git a/pkg/dependency/parser/ruby/gemspec/testdata/malformed00.gemspec b/pkg/dependency/parser/ruby/gemspec/testdata/malformed00.gemspec new file mode 100644 index 000000000000..49b890354fb5 --- /dev/null +++ b/pkg/dependency/parser/ruby/gemspec/testdata/malformed00.gemspec @@ -0,0 +1,10 @@ +# -*- encoding: utf-8 -*- +# ... REDACTED ... + +# Wrong attribute value assignment. +Gem::Specification.new do |spec| + s.name = "async".freeze + s.version = "1.25.0" + + # ... REDACTED ... +end diff --git a/pkg/dependency/parser/ruby/gemspec/testdata/malformed01.gemspec b/pkg/dependency/parser/ruby/gemspec/testdata/malformed01.gemspec new file mode 100644 index 000000000000..253ba4888881 --- /dev/null +++ b/pkg/dependency/parser/ruby/gemspec/testdata/malformed01.gemspec @@ -0,0 +1,10 @@ +# -*- encoding: utf-8 -*- +# ... REDACTED ... + +# Missing version attribute. + +Gem::Specification.new do |s| + s.name = "async".freeze + + # ... REDACTED ... +end diff --git a/pkg/dependency/parser/ruby/gemspec/testdata/multiple_licenses.gemspec b/pkg/dependency/parser/ruby/gemspec/testdata/multiple_licenses.gemspec new file mode 100644 index 000000000000..999ab6129df8 --- /dev/null +++ b/pkg/dependency/parser/ruby/gemspec/testdata/multiple_licenses.gemspec @@ -0,0 +1,41 @@ +# -*- encoding: utf-8 -*- +# stub: test-unit 3.3.7 ruby lib + +Gem::Specification.new do |s| + s.name = "test-unit".freeze + s.version = "3.3.7" + + s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= + s.metadata = { "source_code_uri" => "https://github.com/test-unit/test-unit" } if s.respond_to? :metadata= + s.require_paths = ["lib".freeze] + s.authors = ["Kouhei Sutou".freeze, "Haruka Yoshihara".freeze] + s.date = "2020-11-18" + s.description = "test-unit (Test::Unit) is unit testing framework for Ruby, based on xUnit\nprinciples. These were originally designed by Kent Beck, creator of extreme\nprogramming software development methodology, for Smalltalk's SUnit. It allows\nwriting tests, checking results and automated testing in Ruby.\n".freeze + s.email = ["kou@cozmixng.org".freeze, "yoshihara@clear-code.com".freeze] + s.homepage = "http://test-unit.github.io/".freeze + s.licenses = ["Ruby".freeze,"BSDL".freeze, "PSFL".freeze] + s.rubygems_version = "3.2.22".freeze + s.summary = "An xUnit family unit testing framework for Ruby.".freeze + + s.installed_by_version = "3.2.22" if s.respond_to? :installed_by_version + + if s.respond_to? :specification_version then + s.specification_version = 4 + end + + if s.respond_to? :add_runtime_dependency then + s.add_runtime_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + else + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + end +end diff --git a/pkg/dependency/parser/ruby/gemspec/testdata/normal00.gemspec b/pkg/dependency/parser/ruby/gemspec/testdata/normal00.gemspec new file mode 100644 index 000000000000..afb9e85b0aa2 --- /dev/null +++ b/pkg/dependency/parser/ruby/gemspec/testdata/normal00.gemspec @@ -0,0 +1,26 @@ +# -*- encoding: utf-8 -*- +# stub: rake 13.0.3 ruby lib + +Gem::Specification.new do |s| + s.name = "rake".freeze + s.version = "13.0.3" + + s.required_rubygems_version = Gem::Requirement.new(">= 1.3.2".freeze) if s.respond_to? :required_rubygems_version= + s.metadata = { "bug_tracker_uri" => "https://github.com/ruby/rake/issues", "changelog_uri" => "https://github.com/ruby/rake/blob/v13.0.3/History.rdoc", "documentation_uri" => "https://ruby.github.io/rake", "source_code_uri" => "https://github.com/ruby/rake/tree/v13.0.3" } if s.respond_to? :metadata= + s.require_paths = ["lib".freeze] + s.authors = ["Hiroshi SHIBATA".freeze, "Eric Hodel".freeze, "Jim Weirich".freeze] + s.bindir = "exe".freeze + s.date = "2020-12-21" + s.description = "Rake is a Make-like program implemented in Ruby. Tasks and dependencies are\nspecified in standard Ruby syntax.\nRake has the following features:\n * Rakefiles (rake's version of Makefiles) are completely defined in standard Ruby syntax.\n No XML files to edit. No quirky Makefile syntax to worry about (is that a tab or a space?)\n * Users can specify tasks with prerequisites.\n * Rake supports rule patterns to synthesize implicit tasks.\n * Flexible FileLists that act like arrays but know about manipulating file names and paths.\n * Supports parallel execution of tasks.\n".freeze + s.email = ["hsbt@ruby-lang.org".freeze, "drbrain@segment7.net".freeze, "".freeze] + s.executables = ["rake".freeze] + s.files = ["exe/rake".freeze] + s.homepage = "https://github.com/ruby/rake".freeze + s.licenses = ["MIT".freeze] + s.rdoc_options = ["--main".freeze, "README.rdoc".freeze] + s.required_ruby_version = Gem::Requirement.new(">= 2.2".freeze) + s.rubygems_version = "3.2.22".freeze + s.summary = "Rake is a Make-like program implemented in Ruby".freeze + + s.installed_by_version = "3.2.22" if s.respond_to? :installed_by_version +end diff --git a/pkg/dependency/parser/ruby/gemspec/testdata/normal01.gemspec b/pkg/dependency/parser/ruby/gemspec/testdata/normal01.gemspec new file mode 100644 index 000000000000..ae964c425713 --- /dev/null +++ b/pkg/dependency/parser/ruby/gemspec/testdata/normal01.gemspec @@ -0,0 +1,10 @@ +# -*- encoding: utf-8 -*- +# ... REDACTED ... + +Gem::Specification.new do |spec| + spec.name = "async".freeze # comment + spec.version = "1.25.0" + spec.licenses = "MIT" # invalid + + # ... REDACTED ... +end diff --git a/pkg/dependency/parser/rust/binary/parse.go b/pkg/dependency/parser/rust/binary/parse.go new file mode 100644 index 000000000000..1db56b534c24 --- /dev/null +++ b/pkg/dependency/parser/rust/binary/parse.go @@ -0,0 +1,82 @@ +// Detects dependencies from Rust binaries built with https://github.com/rust-secure-code/cargo-auditable +package binary + +import ( + "context" + + rustaudit "github.com/rust-secure-code/go-rustaudit" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +var ( + ErrUnrecognizedExe = xerrors.New("unrecognized executable format") + ErrNonRustBinary = xerrors.New("non Rust auditable binary") +) + +// convertError detects rustaudit.ErrUnknownFileFormat and convert to +// ErrUnrecognizedExe and convert rustaudit.ErrNoRustDepInfo to ErrNonRustBinary +func convertError(err error) error { + if err == rustaudit.ErrUnknownFileFormat { + return ErrUnrecognizedExe + } + if err == rustaudit.ErrNoRustDepInfo { + return ErrNonRustBinary + } + + return err +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +// Parse scans files to try to report Rust crates and version injected into Rust binaries +// via https://github.com/rust-secure-code/cargo-auditable +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + info, err := rustaudit.GetDependencyInfo(r) + if err != nil { + return nil, nil, convertError(err) + } + + var pkgs []ftypes.Package + var deps []ftypes.Dependency + for _, pkg := range info.Packages { + if pkg.Kind != rustaudit.Runtime { + continue + } + pkgID := packageID(pkg.Name, pkg.Version) + pkgs = append(pkgs, ftypes.Package{ + ID: pkgID, + Name: pkg.Name, + Version: pkg.Version, + Relationship: lo.Ternary(pkg.Root, ftypes.RelationshipRoot, ftypes.RelationshipUnknown), // TODO: Determine the direct dependencies by checking the dependencies of the root crate + }) + + var childDeps []string + for _, dep_idx := range pkg.Dependencies { + dep := info.Packages[dep_idx] + if dep.Kind == rustaudit.Runtime { + childDeps = append(childDeps, packageID(dep.Name, dep.Version)) + } + } + if len(childDeps) > 0 { + deps = append(deps, ftypes.Dependency{ + ID: pkgID, + DependsOn: childDeps, + }) + } + } + + return pkgs, deps, nil +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.RustBinary, name, version) +} diff --git a/pkg/dependency/parser/rust/binary/parse_test.go b/pkg/dependency/parser/rust/binary/parse_test.go new file mode 100644 index 000000000000..f17c59ec45d6 --- /dev/null +++ b/pkg/dependency/parser/rust/binary/parse_test.go @@ -0,0 +1,89 @@ +package binary_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/rust/binary" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +// Test binaries generated from cargo-auditable test fixture +// https://github.com/rust-secure-code/cargo-auditable/tree/6b77151/cargo-auditable/tests/fixtures/workspace +var ( + pkgs = []ftypes.Package{ + { + ID: "crate_with_features@0.1.0", + Name: "crate_with_features", + Version: "0.1.0", + Relationship: ftypes.RelationshipRoot, + }, + { + ID: "library_crate@0.1.0", + Name: "library_crate", + Version: "0.1.0", + Relationship: ftypes.RelationshipUnknown, + }, + } + + deps = []ftypes.Dependency{ + { + ID: "crate_with_features@0.1.0", + DependsOn: []string{"library_crate@0.1.0"}, + }, + } +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []ftypes.Package + wantDeps []ftypes.Dependency + wantErr string + }{ + { + name: "ELF", + inputFile: "testdata/test.elf", + want: pkgs, + wantDeps: deps, + }, + { + name: "PE", + inputFile: "testdata/test.exe", + want: pkgs, + wantDeps: deps, + }, + { + name: "Mach-O", + inputFile: "testdata/test.macho", + want: pkgs, + wantDeps: deps, + }, + { + name: "sad path", + inputFile: "testdata/dummy", + wantErr: "unrecognized executable format", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + got, gotDeps, err := binary.NewParser().Parse(t.Context(), f) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.wantDeps, gotDeps) + }) + } +} diff --git a/pkg/dependency/parser/rust/binary/testdata/dummy b/pkg/dependency/parser/rust/binary/testdata/dummy new file mode 100644 index 000000000000..26bf640459d3 --- /dev/null +++ b/pkg/dependency/parser/rust/binary/testdata/dummy @@ -0,0 +1 @@ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/pkg/dependency/parser/rust/binary/testdata/test.elf b/pkg/dependency/parser/rust/binary/testdata/test.elf new file mode 100644 index 000000000000..cadfee87b008 Binary files /dev/null and b/pkg/dependency/parser/rust/binary/testdata/test.elf differ diff --git a/pkg/dependency/parser/rust/binary/testdata/test.exe b/pkg/dependency/parser/rust/binary/testdata/test.exe new file mode 100644 index 000000000000..6a0019bd40a0 Binary files /dev/null and b/pkg/dependency/parser/rust/binary/testdata/test.exe differ diff --git a/pkg/dependency/parser/rust/binary/testdata/test.macho b/pkg/dependency/parser/rust/binary/testdata/test.macho new file mode 100644 index 000000000000..09271be2a41e Binary files /dev/null and b/pkg/dependency/parser/rust/binary/testdata/test.macho differ diff --git a/pkg/dependency/parser/rust/cargo/naive_pkg_parser.go b/pkg/dependency/parser/rust/cargo/naive_pkg_parser.go new file mode 100644 index 000000000000..88f037c32b12 --- /dev/null +++ b/pkg/dependency/parser/rust/cargo/naive_pkg_parser.go @@ -0,0 +1,72 @@ +package cargo + +import ( + "bufio" + "fmt" + "io" + "strings" +) + +type pkgPosition struct { + start int + end int +} +type minPkg struct { + name string + version string + position pkgPosition +} + +func (pkg *minPkg) setEndPositionIfEmpty(n int) { + if pkg.position.end == 0 { + pkg.position.end = n + } +} + +type naivePkgParser struct { + r io.Reader +} + +func (parser *naivePkgParser) parse() map[string]pkgPosition { + var currentPkg = minPkg{} + var idx = make(map[string]pkgPosition, 0) + + scanner := bufio.NewScanner(parser.r) + lineNum := 1 + for scanner.Scan() { + line := scanner.Text() + switch { + case strings.HasPrefix(strings.TrimSpace(line), "["): + if currentPkg.name != "" { + pkgId := packageID(currentPkg.name, currentPkg.version) + currentPkg.setEndPositionIfEmpty(lineNum - 1) + idx[pkgId] = currentPkg.position + } + currentPkg = minPkg{} + currentPkg.position.start = lineNum + + case strings.HasPrefix(strings.TrimSpace(line), "name ="): + currentPkg.name = propertyValue(line) + case strings.HasPrefix(strings.TrimSpace(line), "version ="): + currentPkg.version = propertyValue(line) + case strings.TrimSpace(line) == "": + currentPkg.setEndPositionIfEmpty(lineNum - 1) + } + + lineNum++ + } + // add last item + if currentPkg.name != "" { + pkgId := fmt.Sprintf("%s@%s", currentPkg.name, currentPkg.version) + currentPkg.setEndPositionIfEmpty(lineNum - 1) + idx[pkgId] = currentPkg.position + } + return idx +} +func propertyValue(line string) string { + parts := strings.Split(line, "=") + if len(parts) == 2 { + return strings.Trim(parts[1], ` "`) + } + return "" +} diff --git a/pkg/dependency/parser/rust/cargo/parse.go b/pkg/dependency/parser/rust/cargo/parse.go new file mode 100644 index 000000000000..81b92165ca97 --- /dev/null +++ b/pkg/dependency/parser/rust/cargo/parse.go @@ -0,0 +1,131 @@ +package cargo + +import ( + "context" + "io" + "sort" + "strings" + + "github.com/BurntSushi/toml" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type cargoPkg struct { + Name string `toml:"name"` + Version string `toml:"version"` + Source string `toml:"source,omitempty"` + Dependencies []string `toml:"dependencies,omitempty"` +} +type Lockfile struct { + Packages []cargoPkg `toml:"package"` +} + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("cargo"), + } +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockfile Lockfile + decoder := toml.NewDecoder(r) + if _, err := decoder.Decode(&lockfile); err != nil { + return nil, nil, xerrors.Errorf("decode error: %w", err) + } + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return nil, nil, xerrors.Errorf("seek error: %w", err) + } + + // naive parser to get line numbers by package from lock file + pkgParser := naivePkgParser{r: r} + lineNumIdx := pkgParser.parse() + + // We need to get version for unique dependencies for lockfile v3 from lockfile.Packages + pkgMap := lo.SliceToMap(lockfile.Packages, func(pkg cargoPkg) (string, cargoPkg) { + return pkg.Name, pkg + }) + + var pkgs ftypes.Packages + var deps ftypes.Dependencies + for _, lpkg := range lockfile.Packages { + pkgID := packageID(lpkg.Name, lpkg.Version) + pkg := ftypes.Package{ + ID: pkgID, + Name: lpkg.Name, + Version: lpkg.Version, + } + if pos, ok := lineNumIdx[pkgID]; ok { + pkg.Locations = []ftypes.Location{ + { + StartLine: pos.start, + EndLine: pos.end, + }, + } + } + + pkgs = append(pkgs, pkg) + dep := p.parseDependencies(pkgID, lpkg, pkgMap) + if dep != nil { + deps = append(deps, *dep) + } + } + sort.Sort(pkgs) + sort.Sort(deps) + return pkgs, deps, nil +} +func (p *Parser) parseDependencies(pkgId string, pkg cargoPkg, pkgs map[string]cargoPkg) *ftypes.Dependency { + var dependOn []string + + for _, pkgDep := range pkg.Dependencies { + /* + Dependency entries look like: + old Cargo.lock - https://github.com/rust-lang/cargo/blob/46bac2dc448ab12fe0f182bee8d35cc804d9a6af/tests/testsuite/lockfile_compat.rs#L48-L50 + "unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" + new Cargo.lock -https://github.com/rust-lang/cargo/blob/46bac2dc448ab12fe0f182bee8d35cc804d9a6af/tests/testsuite/lockfile_compat.rs#L39-L41 + "unsafe-any" - if lock file contains only 1 version of dependency + "unsafe-any 0.4.2" if lock file contains more than 1 version of dependency + */ + fields := strings.Fields(pkgDep) + switch len(fields) { + // unique dependency in new lock file + case 1: + name := fields[0] + version, ok := pkgs[name] + if !ok { + p.logger.Debug("Cannot find version", log.String("name", name)) + continue + } + dependOn = append(dependOn, packageID(name, version.Version)) + // 2: non-unique dependency in new lock file + // 3: old lock file + case 2, 3: + dependOn = append(dependOn, packageID(fields[0], fields[1])) + default: + p.logger.Debug("Wrong dependency format", log.String("dep", pkgDep)) + continue + } + } + if len(dependOn) > 0 { + sort.Strings(dependOn) + return &ftypes.Dependency{ + ID: pkgId, + DependsOn: dependOn, + } + } + return nil +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.Cargo, name, version) +} diff --git a/pkg/dependency/parser/rust/cargo/parse_test.go b/pkg/dependency/parser/rust/cargo/parse_test.go new file mode 100644 index 000000000000..c973d3c1d72d --- /dev/null +++ b/pkg/dependency/parser/rust/cargo/parse_test.go @@ -0,0 +1,401 @@ +package cargo + +import ( + "fmt" + "os" + "path" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + cargoNormalPkgs = []ftypes.Package{ + { + ID: "normal@0.1.0", + Name: "normal", + Version: "0.1.0", + Locations: []ftypes.Location{ + { + StartLine: 8, + EndLine: 13, + }, + }, + }, + { + ID: "libc@0.2.54", + Name: "libc", + Version: "0.2.54", + Locations: []ftypes.Location{ + { + StartLine: 3, + EndLine: 6, + }, + }, + }, + { + ID: "typemap@0.3.3", + Name: "typemap", + Version: "0.3.3", + Locations: []ftypes.Location{ + { + StartLine: 20, + EndLine: 26, + }, + }, + }, + { + ID: "url@1.7.2", + Name: "url", + Version: "1.7.2", + Locations: []ftypes.Location{ + { + StartLine: 43, + EndLine: 51, + }, + }, + }, + { + ID: "unsafe-any@0.4.2", + Name: "unsafe-any", + Version: "0.4.2", + Locations: []ftypes.Location{ + { + StartLine: 15, + EndLine: 18, + }, + }, + }, + { + ID: "matches@0.1.8", + Name: "matches", + Version: "0.1.8", + Locations: []ftypes.Location{ + { + StartLine: 33, + EndLine: 36, + }, + }, + }, + { + ID: "idna@0.1.5", + Name: "idna", + Version: "0.1.5", + Locations: []ftypes.Location{ + { + StartLine: 28, + EndLine: 31, + }, + }, + }, + { + ID: "percent-encoding@1.0.1", + Name: "percent-encoding", + Version: "1.0.1", + Locations: []ftypes.Location{ + { + StartLine: 38, + EndLine: 41, + }, + }, + }, + } + cargoNormalDeps = []ftypes.Dependency{ + { + ID: "normal@0.1.0", + DependsOn: []string{"libc@0.2.54"}, + }, + { + ID: "typemap@0.3.3", + DependsOn: []string{"unsafe-any@0.4.2"}, + }, + { + ID: "url@1.7.2", + DependsOn: []string{ + "idna@0.1.5", + "matches@0.1.8", + "percent-encoding@1.0.1", + }, + }, + } + cargoMixedPkgs = []ftypes.Package{ + { + ID: "normal@0.1.0", + Name: "normal", + Version: "0.1.0", + Locations: []ftypes.Location{ + { + StartLine: 17, + EndLine: 22, + }, + }, + }, + { + ID: "libc@0.2.54", + Name: "libc", + Version: "0.2.54", + Locations: []ftypes.Location{ + { + StartLine: 3, + EndLine: 6, + }, + }, + }, + { + ID: "typemap@0.3.3", + Name: "typemap", + Version: "0.3.3", + Locations: []ftypes.Location{ + { + StartLine: 55, + EndLine: 61, + }, + }, + }, + { + ID: "url@1.7.2", + Name: "url", + Version: "1.7.2", + Locations: []ftypes.Location{ + { + StartLine: 26, + EndLine: 34, + }, + }, + }, + { + ID: "unsafe-any@0.4.2", + Name: "unsafe-any", + Version: "0.4.2", + Locations: []ftypes.Location{ + { + StartLine: 9, + EndLine: 12, + }, + }, + }, + { + ID: "matches@0.1.8", + Name: "matches", + Version: "0.1.8", + Locations: []ftypes.Location{ + { + StartLine: 41, + EndLine: 44, + }, + }, + }, + { + ID: "idna@0.1.5", + Name: "idna", + Version: "0.1.5", + Locations: []ftypes.Location{ + { + StartLine: 36, + EndLine: 39, + }, + }, + }, + { + ID: "percent-encoding@1.0.1", + Name: "percent-encoding", + Version: "1.0.1", + Locations: []ftypes.Location{ + { + StartLine: 46, + EndLine: 49, + }, + }, + }, + } + cargoV3Pkgs = []ftypes.Package{ + { + ID: "aho-corasick@0.7.20", + Name: "aho-corasick", + Version: "0.7.20", + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 12, + }, + }, + }, + { + ID: "app@0.1.0", + Name: "app", + Version: "0.1.0", + Locations: []ftypes.Location{ + { + StartLine: 14, + EndLine: 21, + }, + }, + }, + { + ID: "libc@0.2.140", + Name: "libc", + Version: "0.2.140", + Locations: []ftypes.Location{ + { + StartLine: 23, + EndLine: 27, + }, + }, + }, + { + ID: "memchr@1.0.2", + Name: "memchr", + Version: "1.0.2", + Locations: []ftypes.Location{ + { + StartLine: 29, + EndLine: 36, + }, + }, + }, + { + ID: "memchr@2.5.0", + Name: "memchr", + Version: "2.5.0", + Locations: []ftypes.Location{ + { + StartLine: 38, + EndLine: 42, + }, + }, + }, + { + ID: "regex@1.7.3", + Name: "regex", + Version: "1.7.3", + Locations: []ftypes.Location{ + { + StartLine: 44, + EndLine: 53, + }, + }, + }, + { + ID: "regex-syntax@0.5.6", + Name: "regex-syntax", + Version: "0.5.6", + Locations: []ftypes.Location{ + { + StartLine: 55, + EndLine: 62, + }, + }, + }, + { + ID: "regex-syntax@0.6.29", + Name: "regex-syntax", + Version: "0.6.29", + Locations: []ftypes.Location{ + { + StartLine: 64, + EndLine: 68, + }, + }, + }, + { + ID: "ucd-util@0.1.10", + Name: "ucd-util", + Version: "0.1.10", + Locations: []ftypes.Location{ + { + StartLine: 70, + EndLine: 74, + }, + }, + }, + } + + cargoV3Deps = []ftypes.Dependency{ + { + ID: "aho-corasick@0.7.20", + DependsOn: []string{"memchr@2.5.0"}, + }, + { + ID: "app@0.1.0", + DependsOn: []string{ + "memchr@1.0.2", + "regex-syntax@0.5.6", + "regex@1.7.3", + }, + }, + { + ID: "memchr@1.0.2", + DependsOn: []string{"libc@0.2.140"}, + }, + { + ID: "regex@1.7.3", + DependsOn: []string{ + "aho-corasick@0.7.20", + "memchr@2.5.0", + "regex-syntax@0.6.29", + }, + }, + { + ID: "regex-syntax@0.5.6", + DependsOn: []string{"ucd-util@0.1.10"}, + }, + } +) + +func TestParse(t *testing.T) { + vectors := []struct { + file string // Test input file + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + wantErr assert.ErrorAssertionFunc + }{ + { + file: "testdata/cargo_normal.lock", + wantPkgs: cargoNormalPkgs, + wantDeps: cargoNormalDeps, + wantErr: assert.NoError, + }, + { + file: "testdata/cargo_mixed.lock", + wantPkgs: cargoMixedPkgs, + wantDeps: cargoNormalDeps, + wantErr: assert.NoError, + }, + { + file: "testdata/cargo_v3.lock", + wantPkgs: cargoV3Pkgs, + wantDeps: cargoV3Deps, + wantErr: assert.NoError, + }, + { + file: "testdata/cargo_invalid.lock", + wantErr: assert.Error, + }, + } + + for _, v := range vectors { + t.Run(path.Base(v.file), func(t *testing.T) { + f, err := os.Open(v.file) + require.NoError(t, err) + + gotPkgs, gotDeps, err := NewParser().Parse(t.Context(), f) + + if !v.wantErr(t, err, fmt.Sprintf("Parse(%v)", v.file)) { + return + } + + if err != nil { + return + } + + sort.Sort(ftypes.Packages(v.wantPkgs)) + sort.Sort(ftypes.Dependencies(v.wantDeps)) + assert.Equalf(t, v.wantPkgs, gotPkgs, "Parse libraries(%v)", v.file) + assert.Equalf(t, v.wantDeps, gotDeps, "Parse dependencies(%v)", v.file) + }) + } +} diff --git a/pkg/dependency/parser/rust/cargo/testdata/cargo_invalid.lock b/pkg/dependency/parser/rust/cargo/testdata/cargo_invalid.lock new file mode 100644 index 000000000000..a0690b395f18 --- /dev/null +++ b/pkg/dependency/parser/rust/cargo/testdata/cargo_invalid.lock @@ -0,0 +1,34 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "libc" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "normal" +version = +dependencies = [ + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)" = "c6785aa7dd976f5fbf3b71cfd9cd49d7f783c1ff565a858d71031c6c313aa5c6" diff --git a/pkg/dependency/parser/rust/cargo/testdata/cargo_mixed.lock b/pkg/dependency/parser/rust/cargo/testdata/cargo_mixed.lock new file mode 100644 index 000000000000..2c0cc23176d1 --- /dev/null +++ b/pkg/dependency/parser/rust/cargo/testdata/cargo_mixed.lock @@ -0,0 +1,61 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "libc" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" + + +[[package]] +name = "unsafe-any" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + + + + +[[package]] +name = "normal" +version = "0.1.0" +dependencies = [ + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", +] + +# Extra comments + +[[package]] +name = "url" +version = """1.7.2""" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + + +[metadata] +"checksum libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)" = "c6785aa7dd976f5fbf3b71cfd9cd49d7f783c1ff565a858d71031c6c313aa5c6" + +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] diff --git a/pkg/dependency/parser/rust/cargo/testdata/cargo_normal.lock b/pkg/dependency/parser/rust/cargo/testdata/cargo_normal.lock new file mode 100644 index 000000000000..76ecc0abe5bc --- /dev/null +++ b/pkg/dependency/parser/rust/cargo/testdata/cargo_normal.lock @@ -0,0 +1,54 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "libc" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "normal" +version = "0.1.0" +dependencies = [ + "libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "unsafe-any" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "typemap" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "unsafe-any 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "idna" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "matches" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "percent-encoding" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "url" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "idna 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", + "matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)", + "percent-encoding 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[metadata] +"checksum libc 0.2.54 (registry+https://github.com/rust-lang/crates.io-index)" = "c6785aa7dd976f5fbf3b71cfd9cd49d7f783c1ff565a858d71031c6c313aa5c6" diff --git a/pkg/dependency/parser/rust/cargo/testdata/cargo_v3.lock b/pkg/dependency/parser/rust/cargo/testdata/cargo_v3.lock new file mode 100644 index 000000000000..a366dbdea056 --- /dev/null +++ b/pkg/dependency/parser/rust/cargo/testdata/cargo_v3.lock @@ -0,0 +1,74 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr 2.5.0", +] + +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "memchr 1.0.2", + "regex", + "regex-syntax 0.5.6", +] + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "aho-corasick", + "memchr 2.5.0", + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "ucd-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003" \ No newline at end of file diff --git a/pkg/dependency/parser/sbt/lockfile/parse.go b/pkg/dependency/parser/sbt/lockfile/parse.go new file mode 100644 index 000000000000..ee1f9f1e911d --- /dev/null +++ b/pkg/dependency/parser/sbt/lockfile/parse.go @@ -0,0 +1,62 @@ +package lockfile + +import ( + "context" + "slices" + "sort" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +// lockfile format defined at: https://stringbean.github.io/sbt-dependency-lock/file-formats/version-1.html +type sbtLockfile struct { + Version int `json:"lockVersion"` + Dependencies []sbtLockfileDependency `json:"dependencies"` +} + +type sbtLockfileDependency struct { + Organization string `json:"org"` + Name string `json:"name"` + Version string `json:"version"` + Configurations []string `json:"configurations"` + xjson.Location +} + +type Parser struct{} + +func NewParser() *Parser { + return &Parser{} +} + +func (Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockfile sbtLockfile + if err := xjson.UnmarshalRead(r, &lockfile); err != nil { + return nil, nil, xerrors.Errorf("JSON decoding failed: %w", err) + } + + var libraries ftypes.Packages + + for _, dep := range lockfile.Dependencies { + if slices.ContainsFunc(dep.Configurations, isIncludedConfig) { + name := dep.Organization + ":" + dep.Name + libraries = append(libraries, ftypes.Package{ + ID: dependency.ID(ftypes.Sbt, name, dep.Version), + Name: name, + Version: dep.Version, + Locations: []ftypes.Location{ftypes.Location(dep.Location)}, + }) + } + } + + sort.Sort(libraries) + return libraries, nil, nil +} + +func isIncludedConfig(config string) bool { + return config == "compile" || config == "runtime" +} diff --git a/pkg/dependency/parser/sbt/lockfile/parse_test.go b/pkg/dependency/parser/sbt/lockfile/parse_test.go new file mode 100644 index 000000000000..0450fe0257f1 --- /dev/null +++ b/pkg/dependency/parser/sbt/lockfile/parse_test.go @@ -0,0 +1,77 @@ +package lockfile + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []ftypes.Package + }{ + { + name: "v1 happy path", + inputFile: "testdata/v1_happy.sbt.lock", + want: []ftypes.Package{ + { + ID: "org.apache.commons:commons-lang3:3.9", + Name: "org.apache.commons:commons-lang3", + Version: "3.9", + Locations: []ftypes.Location{ + { + StartLine: 10, + EndLine: 25, + }, + }, + }, + { + ID: "org.scala-lang:scala-library:2.12.10", + Name: "org.scala-lang:scala-library", + Version: "2.12.10", + Locations: []ftypes.Location{ + { + StartLine: 26, + EndLine: 41, + }, + }, + }, + { + ID: "org.typelevel:cats-core_2.12:2.9.0", + Name: "org.typelevel:cats-core_2.12", + Version: "2.9.0", + Locations: []ftypes.Location{ + { + StartLine: 42, + EndLine: 57, + }, + }, + }, + }, + }, + { + name: "empty", + inputFile: "testdata/empty.sbt.lock", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := NewParser() + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + libs, _, err := parser.Parse(t.Context(), f) + require.NoError(t, err) + + assert.Equal(t, tt.want, libs) + }) + } +} diff --git a/pkg/dependency/parser/sbt/lockfile/testdata/empty.sbt.lock b/pkg/dependency/parser/sbt/lockfile/testdata/empty.sbt.lock new file mode 100644 index 000000000000..6125547882da --- /dev/null +++ b/pkg/dependency/parser/sbt/lockfile/testdata/empty.sbt.lock @@ -0,0 +1,10 @@ +{ + "lockVersion": 1, + "timestamp": "2024-06-05T13:41:10.992Z", + "configurations": [ + "compile", + "runtime", + "test" + ], + "dependencies": [] +} \ No newline at end of file diff --git a/pkg/dependency/parser/sbt/lockfile/testdata/v1_happy.sbt.lock b/pkg/dependency/parser/sbt/lockfile/testdata/v1_happy.sbt.lock new file mode 100644 index 000000000000..0dcba8ba09c6 --- /dev/null +++ b/pkg/dependency/parser/sbt/lockfile/testdata/v1_happy.sbt.lock @@ -0,0 +1,73 @@ +{ + "lockVersion": 1, + "timestamp": "2024-06-05T13:41:10.992Z", + "configurations": [ + "compile", + "runtime", + "test" + ], + "dependencies": [ + { + "org": "org.apache.commons", + "name": "commons-lang3", + "version": "3.9", + "artifacts": [ + { + "name": "commons-lang3.jar", + "hash": "sha1:0122c7cee69b53ed4a7681c03d4ee4c0e2765da5" + } + ], + "configurations": [ + "test", + "compile", + "runtime" + ] + }, + { + "org": "org.scala-lang", + "name": "scala-library", + "version": "2.12.10", + "artifacts": [ + { + "name": "scala-library.jar", + "hash": "sha1:3509860bc2e5b3da001ed45aca94ffbe5694dbda" + } + ], + "configurations": [ + "test", + "compile", + "runtime" + ] + }, + { + "org" : "org.typelevel", + "name" : "cats-core_2.12", + "version" : "2.9.0", + "artifacts" : [ + { + "name" : "cats-core_2.12.jar", + "hash" : "sha1:844f21541d1809008586fbc1172dc02c96476639" + } + ], + "configurations" : [ + "compile", + "runtime", + "test" + ] + }, + { + "org" : "org.scalatest", + "name" : "scalatest-core_2.13", + "version" : "3.2.15", + "artifacts" : [ + { + "name" : "scalatest-core_2.13.jar", + "hash" : "sha1:231d1f4049a9fa4bd65c17b806a58180b9f4abe1" + } + ], + "configurations" : [ + "test" + ] + } + ] +} \ No newline at end of file diff --git a/pkg/dependency/parser/swift/cocoapods/parse.go b/pkg/dependency/parser/swift/cocoapods/parse.go new file mode 100644 index 000000000000..48195f66faf3 --- /dev/null +++ b/pkg/dependency/parser/swift/cocoapods/parse.go @@ -0,0 +1,118 @@ +package cocoapods + +import ( + "context" + "sort" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/utils" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("cocoapods"), + } +} + +type lockFile struct { + Pods []any `yaml:"PODS"` // pod can be string or map[string]interface{} +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + lock := &lockFile{} + decoder := yaml.NewDecoder(r) + if err := decoder.Decode(&lock); err != nil { + return nil, nil, xerrors.Errorf("failed to decode cocoapods lock file: %s", err.Error()) + } + + parsedDeps := make(map[string]ftypes.Package) // dependency name => Package + directDeps := make(map[string][]string) // dependency name => slice of child dependency names + for _, pod := range lock.Pods { + switch dep := pod.(type) { + case string: // dependency with version number + pkg, err := parseDep(dep) + if err != nil { + p.logger.Debug("Dependency parse error", log.Err(err)) + continue + } + parsedDeps[pkg.Name] = pkg + case map[string]any: + for dep, childDeps := range dep { + pkg, err := parseDep(dep) + if err != nil { + p.logger.Debug("Dependency parse error", log.Err(err)) + continue + } + parsedDeps[pkg.Name] = pkg + + children, ok := childDeps.([]any) + if !ok { + return nil, nil, xerrors.Errorf("invalid value of cocoapods direct dependency: %q", childDeps) + } + + for _, childDep := range children { + s, ok := childDep.(string) + if !ok { + return nil, nil, xerrors.Errorf("must be string: %q", childDep) + } + directDeps[pkg.Name] = append(directDeps[pkg.Name], strings.Fields(s)[0]) + } + } + } + } + + var deps ftypes.Dependencies + for dep, childDeps := range directDeps { + var dependsOn []string + // find versions for child dependencies + for _, childDep := range childDeps { + dependsOn = append(dependsOn, packageID(childDep, parsedDeps[childDep].Version)) + } + deps = append(deps, ftypes.Dependency{ + ID: parsedDeps[dep].ID, + DependsOn: dependsOn, + }) + } + + sort.Sort(deps) + return utils.UniquePackages(lo.Values(parsedDeps)), deps, nil +} + +func parseDep(dep string) (ftypes.Package, error) { + // dep example: + // 'AppCenter (4.2.0)' + // direct dep examples: + // 'AppCenter/Core' + // 'AppCenter/Analytics (= 4.2.0)' + // 'AppCenter/Analytics (-> 4.2.0)' + ss := strings.Split(dep, " (") + if len(ss) != 2 { + return ftypes.Package{}, xerrors.Errorf("Unable to determine cocoapods dependency: %q", dep) + } + + name := ss[0] + version := strings.Trim(strings.TrimSpace(ss[1]), "()") + pkg := ftypes.Package{ + ID: packageID(name, version), + Name: name, + Version: version, + } + + return pkg, nil +} + +func packageID(name, version string) string { + return dependency.ID(ftypes.Cocoapods, name, version) +} diff --git a/pkg/dependency/parser/swift/cocoapods/parse_test.go b/pkg/dependency/parser/swift/cocoapods/parse_test.go new file mode 100644 index 000000000000..35dec6076cad --- /dev/null +++ b/pkg/dependency/parser/swift/cocoapods/parse_test.go @@ -0,0 +1,96 @@ +package cocoapods_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/swift/cocoapods" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + inputFile string // Test input file + wantPkgs []ftypes.Package + wantDeps []ftypes.Dependency + }{ + { + name: "happy path", + inputFile: "testdata/happy.lock", + wantPkgs: []ftypes.Package{ + { + ID: "AppCenter@4.2.0", + Name: "AppCenter", + Version: "4.2.0", + }, + { + ID: "AppCenter/Analytics@4.2.0", + Name: "AppCenter/Analytics", + Version: "4.2.0", + }, + { + ID: "AppCenter/Core@4.2.0", + Name: "AppCenter/Core", + Version: "4.2.0", + }, + { + ID: "AppCenter/Crashes@4.2.0", + Name: "AppCenter/Crashes", + Version: "4.2.0", + }, + { + ID: "KeychainAccess@4.2.1", + Name: "KeychainAccess", + Version: "4.2.1", + }, + }, + wantDeps: []ftypes.Dependency{ + { + ID: "AppCenter/Analytics@4.2.0", + DependsOn: []string{ + "AppCenter/Core@4.2.0", + }, + }, + { + ID: "AppCenter/Crashes@4.2.0", + DependsOn: []string{ + "AppCenter/Core@4.2.0", + }, + }, + { + ID: "AppCenter@4.2.0", + DependsOn: []string{ + "AppCenter/Analytics@4.2.0", + "AppCenter/Crashes@4.2.0", + }, + }, + }, + }, + { + name: "happy path. lock file without dependencies", + inputFile: "testdata/empty.lock", + }, + { + name: "sad path. wrong dep format", + inputFile: "testdata/sad.lock", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + gotPkgs, gotDeps, err := cocoapods.NewParser().Parse(t.Context(), f) + require.NoError(t, err) + + assert.Equal(t, tt.wantPkgs, gotPkgs) + assert.Equal(t, tt.wantDeps, gotDeps) + }) + } +} diff --git a/pkg/dependency/parser/swift/cocoapods/testdata/empty.lock b/pkg/dependency/parser/swift/cocoapods/testdata/empty.lock new file mode 100644 index 000000000000..142864b62d23 --- /dev/null +++ b/pkg/dependency/parser/swift/cocoapods/testdata/empty.lock @@ -0,0 +1 @@ +COCOAPODS: 1.11.2 \ No newline at end of file diff --git a/pkg/dependency/parser/swift/cocoapods/testdata/happy.lock b/pkg/dependency/parser/swift/cocoapods/testdata/happy.lock new file mode 100644 index 000000000000..65600c35476c --- /dev/null +++ b/pkg/dependency/parser/swift/cocoapods/testdata/happy.lock @@ -0,0 +1,12 @@ +PODS: + - AppCenter (4.2.0): + - AppCenter/Analytics (= 4.2.0) + - AppCenter/Crashes (= 4.2.0) + - AppCenter/Analytics (4.2.0): + - AppCenter/Core + - AppCenter/Core (4.2.0) + - AppCenter/Crashes (4.2.0): + - AppCenter/Core + - KeychainAccess (4.2.1) + +COCOAPODS: 1.11.2 \ No newline at end of file diff --git a/pkg/dependency/parser/swift/cocoapods/testdata/sad.lock b/pkg/dependency/parser/swift/cocoapods/testdata/sad.lock new file mode 100644 index 000000000000..b0be368257b0 --- /dev/null +++ b/pkg/dependency/parser/swift/cocoapods/testdata/sad.lock @@ -0,0 +1,4 @@ +PODS: + - AppCenter = 4.2.0 + +COCOAPODS: 1.11.2 \ No newline at end of file diff --git a/pkg/dependency/parser/swift/swift/parse.go b/pkg/dependency/parser/swift/swift/parse.go new file mode 100644 index 000000000000..0124387a913a --- /dev/null +++ b/pkg/dependency/parser/swift/swift/parse.go @@ -0,0 +1,77 @@ +package swift + +import ( + "context" + "sort" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +// Parser is a parser for Package.resolved files +type Parser struct { + logger *log.Logger +} + +func NewParser() *Parser { + return &Parser{ + logger: log.WithPrefix("swift"), + } +} + +func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { + var lockFile LockFile + if err := xjson.UnmarshalRead(r, &lockFile); err != nil { + return nil, nil, xerrors.Errorf("decode error: %w", err) + } + + var pkgs ftypes.Packages + pins := lockFile.Object.Pins + if lockFile.Version > 1 { + pins = lockFile.Pins + } + for _, pin := range pins { + name := pkgName(pin, lockFile.Version) + + // Skip packages for which we cannot resolve the version + if pin.State.Version == "" && pin.State.Branch == "" { + p.logger.Warn("Unable to resolve. Both the version and branch fields are empty.", log.String("name", name)) + continue + } + + // A Pin can be resolved using `branch` without `version`. + // e.g. https://github.com/element-hq/element-ios/blob/6a9bcc88ea37147efba8f0a7bcf3ec187f4a4011/Riot.xcworkspace/xcshareddata/swiftpm/Package.resolved#L84-L92 + version := lo.Ternary(pin.State.Version != "", pin.State.Version, pin.State.Branch) + + pkgs = append(pkgs, ftypes.Package{ + ID: dependency.ID(ftypes.Swift, name, version), + Name: name, + Version: version, + Locations: []ftypes.Location{ftypes.Location(pin.Location)}, + }) + } + sort.Sort(pkgs) + return pkgs, nil, nil +} + +func pkgName(pin Pin, lockVersion int) string { + // Package.resolved v1 uses `RepositoryURL` + // v2 uses `Location` + name := pin.RepositoryURL + if lockVersion > 1 { + name = pin.Loc + } + // Swift uses `https://github.com//.git format + // `.git` suffix can be omitted (take a look happy test) + // Remove `https://` and `.git` to fit the same format + name = strings.TrimPrefix(name, "https://") + name = strings.TrimSuffix(name, ".git") + return name +} diff --git a/pkg/dependency/parser/swift/swift/parse_test.go b/pkg/dependency/parser/swift/swift/parse_test.go new file mode 100644 index 000000000000..199f70ad4d14 --- /dev/null +++ b/pkg/dependency/parser/swift/swift/parse_test.go @@ -0,0 +1,103 @@ +package swift + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + inputFile string + want []ftypes.Package + }{ + // docker run -it --rm swift@sha256:3c62ac97506ecf19ca15e4db57d7930e6a71559b23b19aa57e13d380133a54db + // mkdir app && cd app + // swift package init + // ## add new deps: ## + // sed -i 's/"1.0.0")/"1.0.0")\n.package(url: "https:\/\/github.com\/ReactiveCocoa\/ReactiveSwift", from: "7.0.0"),\n.package(url: "https:\/\/github.com\/Quick\/Nimble", .exact("9.2.1"))/' Package.swift + // swift package update + { + name: "happy path v1", + inputFile: "testdata/happy-v1-Package.resolved", + want: []ftypes.Package{ + { + ID: "github.com/Quick/Nimble@9.2.1", + Name: "github.com/Quick/Nimble", + Version: "9.2.1", + Locations: []ftypes.Location{{StartLine: 4, EndLine: 12}}, + }, + { + ID: "github.com/ReactiveCocoa/ReactiveSwift@7.1.1", + Name: "github.com/ReactiveCocoa/ReactiveSwift", + Version: "7.1.1", + Locations: []ftypes.Location{{StartLine: 13, EndLine: 21}}, + }, + }, + }, + { + name: "happy path v2", + inputFile: "testdata/happy-v2-Package.resolved", + want: []ftypes.Package{ + { + ID: "github.com/Quick/Nimble@9.2.1", + Name: "github.com/Quick/Nimble", + Version: "9.2.1", + Locations: []ftypes.Location{{StartLine: 21, EndLine: 29}}, + }, + { + ID: "github.com/Quick/Quick@7.2.0", + Name: "github.com/Quick/Quick", + Version: "7.2.0", + Locations: []ftypes.Location{{StartLine: 30, EndLine: 38}}, + }, + { + ID: "github.com/ReactiveCocoa/ReactiveSwift@7.1.1", + Name: "github.com/ReactiveCocoa/ReactiveSwift", + Version: "7.1.1", + Locations: []ftypes.Location{{StartLine: 39, EndLine: 47}}, + }, + { + ID: "github.com/element-hq/swift-ogg@0.0.1", + Name: "github.com/element-hq/swift-ogg", + Version: "0.0.1", + Locations: []ftypes.Location{{StartLine: 48, EndLine: 56}}, + }, + { + ID: "github.com/mattgallagher/CwlCatchException@2.1.2", + Name: "github.com/mattgallagher/CwlCatchException", + Version: "2.1.2", + Locations: []ftypes.Location{{StartLine: 3, EndLine: 11}}, + }, + { + ID: "github.com/mattgallagher/CwlPreconditionTesting@2.1.2", + Name: "github.com/mattgallagher/CwlPreconditionTesting", + Version: "2.1.2", + Locations: []ftypes.Location{{StartLine: 12, EndLine: 20}}, + }, + }, + }, + { + name: "empty", + inputFile: "testdata/empty-Package.resolved", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := NewParser() + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + gotPkgs, _, err := parser.Parse(t.Context(), f) + require.NoError(t, err) + assert.Equal(t, tt.want, gotPkgs) + }) + } +} diff --git a/pkg/dependency/parser/swift/swift/testdata/empty-Package.resolved b/pkg/dependency/parser/swift/swift/testdata/empty-Package.resolved new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/pkg/dependency/parser/swift/swift/testdata/empty-Package.resolved @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkg/dependency/parser/swift/swift/testdata/happy-v1-Package.resolved b/pkg/dependency/parser/swift/swift/testdata/happy-v1-Package.resolved new file mode 100644 index 000000000000..cb15135d4208 --- /dev/null +++ b/pkg/dependency/parser/swift/swift/testdata/happy-v1-Package.resolved @@ -0,0 +1,25 @@ +{ + "object": { + "pins": [ + { + "package": "Nimble", + "repositoryURL": "https://github.com/Quick/Nimble.git", + "state": { + "branch": null, + "revision": "c93f16c25af5770f0d3e6af27c9634640946b068", + "version": "9.2.1" + } + }, + { + "package": "ReactiveSwift", + "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift", + "state": { + "branch": null, + "revision": "40c465af19b993344e84355c00669ba2022ca3cd", + "version": "7.1.1" + } + } + ] + }, + "version": 1 +} \ No newline at end of file diff --git a/pkg/dependency/parser/swift/swift/testdata/happy-v2-Package.resolved b/pkg/dependency/parser/swift/swift/testdata/happy-v2-Package.resolved new file mode 100644 index 000000000000..d3a189b88337 --- /dev/null +++ b/pkg/dependency/parser/swift/swift/testdata/happy-v2-Package.resolved @@ -0,0 +1,59 @@ +{ + "pins" : [ + { + "identity" : "cwlcatchexception", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlCatchException.git", + "state" : { + "revision" : "3b123999de19bf04905bc1dfdb76f817b0f2cc00", + "version" : "2.1.2" + } + }, + { + "identity" : "cwlpreconditiontesting", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state" : { + "revision" : "a23ded2c91df9156628a6996ab4f347526f17b6b", + "version" : "2.1.2" + } + }, + { + "identity" : "nimble", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Nimble.git", + "state" : { + "revision" : "c93f16c25af5770f0d3e6af27c9634640946b068", + "version" : "9.2.1" + } + }, + { + "identity" : "quick", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Quick/Quick.git", + "state" : { + "revision" : "494eff9ad74a37047782b0d5d8d84c7ff49a60e4", + "version" : "7.2.0" + } + }, + { + "identity" : "reactiveswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/ReactiveCocoa/ReactiveSwift", + "state" : { + "revision" : "40c465af19b993344e84355c00669ba2022ca3cd", + "version" : "7.1.1" + } + }, + { + "identity" : "swift-ogg", + "kind" : "remoteSourceControl", + "location" : "https://github.com/element-hq/swift-ogg", + "state" : { + "branch" : "0.0.1", + "revision" : "e9a9e7601da662fd8b97d93781ff5c60b4becf88" + } + } + ], + "version" : 2 +} diff --git a/pkg/dependency/parser/swift/swift/types.go b/pkg/dependency/parser/swift/swift/types.go new file mode 100644 index 000000000000..31efff1f5fe5 --- /dev/null +++ b/pkg/dependency/parser/swift/swift/types.go @@ -0,0 +1,29 @@ +package swift + +import ( + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type LockFile struct { + Object Object `json:"object"` + Pins []Pin `json:"pins"` + Version int `json:"version"` +} + +type Object struct { + Pins []Pin `json:"pins"` +} + +type Pin struct { + Package string `json:"package"` + RepositoryURL string `json:"repositoryURL"` // Package.revision v1 + Loc string `json:"location"` // Package.revision v2 + State State `json:"state"` + xjson.Location +} + +type State struct { + Branch string `json:"branch"` + Revision string `json:"revision"` + Version string `json:"version"` +} diff --git a/pkg/dependency/parser/utils/utils.go b/pkg/dependency/parser/utils/utils.go new file mode 100644 index 000000000000..2e494d1d490b --- /dev/null +++ b/pkg/dependency/parser/utils/utils.go @@ -0,0 +1,53 @@ +package utils + +import ( + "fmt" + "maps" + "sort" + + "github.com/samber/lo" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func UniquePackages(pkgs []ftypes.Package) []ftypes.Package { + if len(pkgs) == 0 { + return nil + } + unique := make(map[string]ftypes.Package) + for _, pkg := range pkgs { + identifier := fmt.Sprintf("%s@%s", pkg.Name, pkg.Version) + if l, ok := unique[identifier]; !ok { + unique[identifier] = pkg + } else { + // There are times when we get 2 same packages as root and dev dependencies. + // https://github.com/aquasecurity/trivy/issues/5532 + // In these cases, we need to mark the dependency as a root dependency. + if !pkg.Dev { + l.Dev = pkg.Dev + unique[identifier] = l + } + + if len(pkg.Locations) > 0 { + // merge locations + l.Locations = append(l.Locations, pkg.Locations...) + sort.Sort(l.Locations) + unique[identifier] = l + } + } + } + pkgSlice := lo.Values(unique) + sort.Sort(ftypes.Packages(pkgSlice)) + + return pkgSlice +} + +func MergeMaps(parent, child map[string]string) map[string]string { + if parent == nil { + return child + } + // Clone parent map to avoid shadow overwrite + newParent := maps.Clone(parent) + maps.Copy(newParent, child) + return newParent +} diff --git a/pkg/dependency/parser/utils/utils_test.go b/pkg/dependency/parser/utils/utils_test.go new file mode 100644 index 000000000000..0610fee0ff38 --- /dev/null +++ b/pkg/dependency/parser/utils/utils_test.go @@ -0,0 +1,119 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestUniqueLibraries(t *testing.T) { + tests := []struct { + name string + pkgs []ftypes.Package + wantPkgs []ftypes.Package + }{ + { + name: "happy path merge locations", + pkgs: []ftypes.Package{ + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Locations: []ftypes.Location{ + { + StartLine: 10, + EndLine: 14, + }, + }, + }, + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Locations: []ftypes.Location{ + { + StartLine: 24, + EndLine: 30, + }, + }, + }, + }, + wantPkgs: []ftypes.Package{ + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Locations: []ftypes.Location{ + { + StartLine: 10, + EndLine: 14, + }, + { + StartLine: 24, + EndLine: 30, + }, + }, + }, + }, + }, + { + name: "happy path Dev and Root deps", + pkgs: []ftypes.Package{ + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Dev: true, + }, + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Dev: false, + }, + }, + wantPkgs: []ftypes.Package{ + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Dev: false, + }, + }, + }, + { + name: "happy path Root and Dev deps", + pkgs: []ftypes.Package{ + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Dev: false, + }, + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Dev: true, + }, + }, + wantPkgs: []ftypes.Package{ + { + ID: "asn1@0.2.6", + Name: "asn1", + Version: "0.2.6", + Dev: false, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotPkgs := UniquePackages(tt.pkgs) + require.Equal(t, tt.wantPkgs, gotPkgs) + }) + } +} diff --git a/pkg/detector/library/compare/bitnami/compare.go b/pkg/detector/library/compare/bitnami/compare.go new file mode 100644 index 000000000000..f3f62a7194d1 --- /dev/null +++ b/pkg/detector/library/compare/bitnami/compare.go @@ -0,0 +1,32 @@ +package bitnami + +import ( + version "github.com/bitnami/go-version/pkg/version" + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare" +) + +// Comparer represents a comparer for Bitnami +type Comparer struct{} + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func (n Comparer) IsVulnerable(ver string, advisory dbTypes.Advisory) bool { + return compare.IsVulnerable(ver, advisory, n.matchVersion) +} + +// matchVersion checks if the package version satisfies the given constraint. +func (n Comparer) matchVersion(currentVersion, constraint string) (bool, error) { + v, err := version.Parse(currentVersion) + if err != nil { + return false, xerrors.Errorf("bitnami version error (%s): %s", currentVersion, err) + } + + c, err := version.NewConstraints(constraint) + if err != nil { + return false, xerrors.Errorf("bitnami constraint error (%s): %s", constraint, err) + } + + return c.Check(v), nil +} diff --git a/pkg/detector/library/compare/bitnami/compare_test.go b/pkg/detector/library/compare/bitnami/compare_test.go new file mode 100644 index 000000000000..fda9c5e5f0e0 --- /dev/null +++ b/pkg/detector/library/compare/bitnami/compare_test.go @@ -0,0 +1,141 @@ +package bitnami_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/bitnami" +) + +func TestBitnamiComparer_IsVulnerable(t *testing.T) { + type args struct { + currentVersion string + advisory types.Advisory + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "not vulnerable", + args: args{ + currentVersion: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{"<1.2.3"}, + }, + }, + want: false, + }, + { + name: "vulnerable", + args: args{ + currentVersion: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{"<=1.2.3"}, + }, + }, + want: true, + }, + { + name: "patched", + args: args{ + currentVersion: "1.2.3", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3"}, + }, + }, + want: false, + }, + { + name: "unaffected", + args: args{ + currentVersion: "1.2.3", + advisory: types.Advisory{ + UnaffectedVersions: []string{"=1.2.3"}, + }, + }, + want: false, + }, + { + name: "vulnerable based on patched & unaffected versions", + args: args{ + currentVersion: "1.2.3", + advisory: types.Advisory{ + UnaffectedVersions: []string{"=1.2.0"}, + PatchedVersions: []string{">=1.2.4"}, + }, + }, + want: true, + }, + { + name: "patched with revision on current version", + args: args{ + currentVersion: "1.2.3-1", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3"}, + }, + }, + want: false, + }, + { + name: "vulnerable with revision on current version", + args: args{ + currentVersion: "1.2.3-1", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.4"}, + }, + }, + want: true, + }, + { + name: "patched with revision on patch", + args: args{ + currentVersion: "1.2.4", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3-1"}, + }, + }, + want: false, + }, + { + name: "vulnerable with revision on patch", + args: args{ + currentVersion: "1.2.3", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3-1"}, + }, + }, + want: true, + }, + { + name: "patched with revisions on both current and patch", + args: args{ + currentVersion: "1.2.4-2", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3-1"}, + }, + }, + want: false, + }, + { + name: "vulnerable with revision on both current and patch", + args: args{ + currentVersion: "1.2.3-0", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3-1"}, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + b := bitnami.Comparer{} + got := b.IsVulnerable(tt.args.currentVersion, tt.args.advisory) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/compare/compare.go b/pkg/detector/library/compare/compare.go new file mode 100644 index 000000000000..a7b7dffbcbf3 --- /dev/null +++ b/pkg/detector/library/compare/compare.go @@ -0,0 +1,77 @@ +package compare + +import ( + "slices" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/go-version/pkg/version" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +// Comparer is an interface for version comparison +type Comparer interface { + IsVulnerable(currentVersion string, advisory dbTypes.Advisory) bool +} + +type matchVersion func(currentVersion, constraint string) (bool, error) + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func IsVulnerable(pkgVer string, advisory dbTypes.Advisory, match matchVersion) bool { + // If one of vulnerable/patched versions is empty, we should detect it anyway. + if slices.Contains(append(advisory.VulnerableVersions, advisory.PatchedVersions...), "") { + return true + } + var matched bool + var err error + + if len(advisory.VulnerableVersions) != 0 { + matched, err = match(pkgVer, strings.Join(advisory.VulnerableVersions, " || ")) + if err != nil { + log.Warn("Version matching error", log.Err(err)) + return false + } else if !matched { + // the version is not vulnerable + return false + } + } + + secureVersions := append(advisory.PatchedVersions, advisory.UnaffectedVersions...) + if len(secureVersions) == 0 { + // the version matches vulnerable versions and patched/unaffected versions are not provided + // or all values are empty + return matched + } + + matched, err = match(pkgVer, strings.Join(secureVersions, " || ")) + if err != nil { + log.Warn("Version matching error", log.Err(err)) + return false + } + return !matched +} + +// GenericComparer represents a comparer for semver-like versioning +type GenericComparer struct{} + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func (v GenericComparer) IsVulnerable(ver string, advisory dbTypes.Advisory) bool { + return IsVulnerable(ver, advisory, v.matchVersion) +} + +// matchVersion checks if the package version satisfies the given constraint. +func (v GenericComparer) matchVersion(currentVersion, constraint string) (bool, error) { + ver, err := version.Parse(currentVersion) + if err != nil { + return false, xerrors.Errorf("version error (%s): %s", currentVersion, err) + } + + c, err := version.NewConstraints(constraint) + if err != nil { + return false, xerrors.Errorf("constraint error (%s): %s", currentVersion, err) + } + + return c.Check(ver), nil +} diff --git a/pkg/detector/library/compare/compare_test.go b/pkg/detector/library/compare/compare_test.go new file mode 100644 index 000000000000..3c1502e8fdda --- /dev/null +++ b/pkg/detector/library/compare/compare_test.go @@ -0,0 +1,116 @@ +package compare_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare" +) + +func TestGenericComparer_IsVulnerable(t *testing.T) { + type args struct { + ver string + advisory types.Advisory + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "happy path", + args: args{ + ver: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{"<=1.0"}, + PatchedVersions: []string{">=1.1"}, + }, + }, + }, + { + name: "no patch", + args: args{ + ver: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{"<=99.999.99999"}, + PatchedVersions: []string{"<0.0.0"}, + }, + }, + want: true, + }, + { + name: "pre-release", + args: args{ + ver: "1.2.2-alpha", + advisory: types.Advisory{ + VulnerableVersions: []string{"<=1.2.2"}, + PatchedVersions: []string{">=1.2.2"}, + }, + }, + want: true, + }, + { + name: "multiple constraints", + args: args{ + ver: "2.0.0", + advisory: types.Advisory{ + VulnerableVersions: []string{">=1.7.0 <1.7.16", ">=1.8.0 <1.8.8", ">=2.0.0 <2.0.8", ">=3.0.0-beta.1 <3.0.0-beta.7"}, + PatchedVersions: []string{">=3.0.0-beta.7", ">=2.0.8 <3.0.0-beta.1", ">=1.8.8 <2.0.0", ">=1.7.16 <1.8.0"}, + }, + }, + want: true, + }, + { + name: "invalid version", + args: args{ + ver: "1.2..4", + advisory: types.Advisory{ + VulnerableVersions: []string{"<1.0.0"}, + }, + }, + want: false, + }, + { + name: "improper constraint", + args: args{ + ver: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{"*"}, + PatchedVersions: nil, + }, + }, + want: false, + }, + { + name: "empty patched version", + args: args{ + ver: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{"<=99.999.99999"}, + PatchedVersions: []string{""}, + }, + }, + want: true, + }, + { + name: "empty vulnerable & patched version", + args: args{ + ver: "1.2.3", + advisory: types.Advisory{ + VulnerableVersions: []string{""}, + PatchedVersions: []string{""}, + }, + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := compare.GenericComparer{} + got := v.IsVulnerable(tt.args.ver, tt.args.advisory) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/compare/maven/compare.go b/pkg/detector/library/compare/maven/compare.go new file mode 100644 index 000000000000..d36156dce956 --- /dev/null +++ b/pkg/detector/library/compare/maven/compare.go @@ -0,0 +1,32 @@ +package maven + +import ( + version "github.com/masahiro331/go-mvn-version" + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare" +) + +// Comparer represents a comparer for maven +type Comparer struct{} + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func (n Comparer) IsVulnerable(ver string, advisory dbTypes.Advisory) bool { + return compare.IsVulnerable(ver, advisory, n.matchVersion) +} + +// matchVersion checks if the package version satisfies the given constraint. +func (n Comparer) matchVersion(currentVersion, constraint string) (bool, error) { + v, err := version.NewVersion(currentVersion) + if err != nil { + return false, xerrors.Errorf("maven version error (%s): %s", currentVersion, err) + } + + c, err := version.NewComparer(constraint) + if err != nil { + return false, xerrors.Errorf("maven constraint error (%s): %s", constraint, err) + } + + return c.Check(v), nil +} diff --git a/pkg/detector/library/compare/maven/compare_test.go b/pkg/detector/library/compare/maven/compare_test.go new file mode 100644 index 000000000000..3335ca978b30 --- /dev/null +++ b/pkg/detector/library/compare/maven/compare_test.go @@ -0,0 +1,117 @@ +package maven_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/maven" +) + +func TestComparer_IsVulnerable(t *testing.T) { + type args struct { + currentVersion string + advisory dbTypes.Advisory + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "happy path", + args: args{ + currentVersion: "1.0.0", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=1.0"}, + PatchedVersions: []string{">=1.1"}, + }, + }, + want: true, + }, + { + name: "final release", + args: args{ + currentVersion: "1.2.3.final", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<1.2.3"}, + PatchedVersions: []string{"1.2.3"}, + }, + }, + want: false, + }, + { + name: "pre-release", + args: args{ + currentVersion: "1.2.3-a1", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<1.2.3"}, + PatchedVersions: []string{">=1.2.3"}, + }, + }, + want: true, + }, + { + name: "multiple constraints", + args: args{ + currentVersion: "2.0.0", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{">=1.7.0 <1.7.16", ">=1.8.0 <1.8.8", ">=2.0.0 <2.0.8", ">=3.0.0-beta.1 <3.0.0-beta.7"}, + PatchedVersions: []string{">=3.0.0-beta.7", ">=2.0.8 <3.0.0-beta.1", ">=1.8.8 <2.0.0", ">=1.7.16 <1.8.0"}, + }, + }, + want: true, + }, + { + name: "version requirements", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"(,1.2.3]"}, + PatchedVersions: []string{"1.2.4"}, + }, + }, + want: true, + }, + { + name: "version soft requirements happy", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"1.2.3"}, + PatchedVersions: []string{"1.2.4"}, + }, + }, + want: true, + }, + { + name: "version soft requirements", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"1.2.2"}, + PatchedVersions: []string{"1.2.4"}, + }, + }, + want: false, + }, + { + name: "invalid constraint", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{`<1.0\.0`}, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := maven.Comparer{} + got := c.IsVulnerable(tt.args.currentVersion, tt.args.advisory) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/compare/npm/compare.go b/pkg/detector/library/compare/npm/compare.go new file mode 100644 index 000000000000..f3d5712e2cfa --- /dev/null +++ b/pkg/detector/library/compare/npm/compare.go @@ -0,0 +1,32 @@ +package npm + +import ( + "golang.org/x/xerrors" + + npm "github.com/aquasecurity/go-npm-version/pkg" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare" +) + +// Comparer represents a comparer for npm +type Comparer struct{} + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func (n Comparer) IsVulnerable(ver string, advisory dbTypes.Advisory) bool { + return compare.IsVulnerable(ver, advisory, n.MatchVersion) +} + +// MatchVersion checks if the package version satisfies the given constraint. +func (n Comparer) MatchVersion(currentVersion, constraint string) (bool, error) { + v, err := npm.NewVersion(currentVersion) + if err != nil { + return false, xerrors.Errorf("npm version error (%s): %s", currentVersion, err) + } + + c, err := npm.NewConstraints(constraint, npm.WithPreRelease(true)) + if err != nil { + return false, xerrors.Errorf("npm constraint error (%s): %s", constraint, err) + } + + return c.Check(v), nil +} diff --git a/pkg/detector/library/compare/npm/compare_test.go b/pkg/detector/library/compare/npm/compare_test.go new file mode 100644 index 000000000000..fab726e424f8 --- /dev/null +++ b/pkg/detector/library/compare/npm/compare_test.go @@ -0,0 +1,153 @@ +package npm_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/npm" +) + +func TestNpmComparer_IsVulnerable(t *testing.T) { + type args struct { + currentVersion string + advisory dbTypes.Advisory + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "happy path", + args: args{ + currentVersion: "1.0.0", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=1.0"}, + PatchedVersions: []string{">=1.1"}, + }, + }, + want: true, + }, + { + name: "prerelease", + args: args{ + currentVersion: "1.45.1-lts.1", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{">=1.4.4-lts.1, <2.0.0"}, + PatchedVersions: []string{"2.0.0"}, + }, + }, + want: true, + }, + { + name: "no patch", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=99.999.99999"}, + PatchedVersions: []string{"<0.0.0"}, + }, + }, + want: true, + }, + { + name: "no patch with wildcard", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"*"}, + PatchedVersions: nil, + }, + }, + want: true, + }, + { + name: "pre-release", + args: args{ + currentVersion: "1.2.3-alpha", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=1.2.2"}, + PatchedVersions: []string{">=1.2.2"}, + }, + }, + want: false, + }, + { + name: "multiple constraints", + args: args{ + currentVersion: "2.0.0", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{ + ">=1.7.0 <1.7.16", ">=1.8.0 <1.8.8", ">=2.0.0 <2.0.8", ">=3.0.0-beta.1 <3.0.0-beta.7", + }, + PatchedVersions: []string{ + ">=3.0.0-beta.7", ">=2.0.8 <3.0.0-beta.1", ">=1.8.8 <2.0.0", ">=1.7.16 <1.8.0", + }, + }, + }, + want: true, + }, + { + name: "x", + args: args{ + currentVersion: "2.0.1", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"2.0.x", "2.1.x"}, + PatchedVersions: []string{">=2.2.x"}, + }, + }, + want: true, + }, + { + name: "exact versions", + args: args{ + currentVersion: "2.1.0-M1", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"2.1.0-M1", "2.1.0-M2"}, + PatchedVersions: []string{">=2.1.0"}, + }, + }, + want: true, + }, + { + name: "caret", + args: args{ + currentVersion: "2.0.18", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<2.0.18", "<3.0.16"}, + PatchedVersions: []string{"^2.0.18", "^3.0.16"}, + }, + }, + want: false, + }, + { + name: "invalid version", + args: args{ + currentVersion: "1.2..4", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<1.0.0"}, + }, + }, + want: false, + }, + { + name: "invalid constraint", + args: args{ + currentVersion: "1.2.4", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"!1.0.0"}, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := npm.Comparer{} + got := c.IsVulnerable(tt.args.currentVersion, tt.args.advisory) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/compare/pep440/compare.go b/pkg/detector/library/compare/pep440/compare.go new file mode 100644 index 000000000000..8c88518a9d2f --- /dev/null +++ b/pkg/detector/library/compare/pep440/compare.go @@ -0,0 +1,32 @@ +package pep440 + +import ( + "golang.org/x/xerrors" + + version "github.com/aquasecurity/go-pep440-version" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare" +) + +// Comparer represents a comparer for PEP 440 +type Comparer struct{} + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func (n Comparer) IsVulnerable(ver string, advisory dbTypes.Advisory) bool { + return compare.IsVulnerable(ver, advisory, n.matchVersion) +} + +// matchVersion checks if the package version satisfies the given constraint. +func (n Comparer) matchVersion(currentVersion, constraint string) (bool, error) { + v, err := version.Parse(currentVersion) + if err != nil { + return false, xerrors.Errorf("python version error (%s): %s", currentVersion, err) + } + + c, err := version.NewSpecifiers(constraint, version.WithPreRelease(true)) + if err != nil { + return false, xerrors.Errorf("python constraint error (%s): %s", constraint, err) + } + + return c.Check(v), nil +} diff --git a/pkg/detector/library/compare/pep440/compare_test.go b/pkg/detector/library/compare/pep440/compare_test.go new file mode 100644 index 000000000000..7ab251e417b0 --- /dev/null +++ b/pkg/detector/library/compare/pep440/compare_test.go @@ -0,0 +1,116 @@ +package pep440_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/pep440" +) + +func TestPep440Comparer_IsVulnerable(t *testing.T) { + type args struct { + currentVersion string + advisory dbTypes.Advisory + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "happy path", + args: args{ + currentVersion: "1.0.0", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=1.0"}, + PatchedVersions: []string{">=1.1"}, + }, + }, + want: true, + }, + { + name: "no patch", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=99.999.99999"}, + PatchedVersions: []string{"<0.0.0"}, + }, + }, + want: true, + }, + { + name: "no patch with wildcard", + args: args{ + currentVersion: "1.2.3", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"*"}, + PatchedVersions: nil, + }, + }, + want: true, + }, + { + name: "pre-release", + args: args{ + currentVersion: "1.2.3a1", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<=1.2.2"}, + PatchedVersions: []string{">=1.2.2"}, + }, + }, + want: false, + }, + { + name: "multiple constraints", + args: args{ + currentVersion: "2.0.0", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{">=1.7.0 <1.7.16", ">=1.8.0 <1.8.8", ">=2.0.0 <2.0.8", ">=3.0.0b1 <3.0.0b7"}, + PatchedVersions: []string{">=3.0.0b7", ">=2.0.8 <3.0.0b1", ">=1.8.8 <2.0.0", ">=1.7.16 <1.8.0"}, + }, + }, + want: true, + }, + { + name: "exact versions", + args: args{ + currentVersion: "2.1.0.post1", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"2.1.0.post1", "2.0.0"}, + PatchedVersions: []string{">=2.1.1"}, + }, + }, + want: true, + }, + { + name: "invalid version", + args: args{ + currentVersion: "1.2..4", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"<1.0.0"}, + }, + }, + want: false, + }, + { + name: "invalid constraint", + args: args{ + currentVersion: "1.2.4", + advisory: dbTypes.Advisory{ + VulnerableVersions: []string{"!1.0.0"}, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := pep440.Comparer{} + got := c.IsVulnerable(tt.args.currentVersion, tt.args.advisory) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/compare/rubygems/compare.go b/pkg/detector/library/compare/rubygems/compare.go new file mode 100644 index 000000000000..ccb84cb8f83e --- /dev/null +++ b/pkg/detector/library/compare/rubygems/compare.go @@ -0,0 +1,32 @@ +package rubygems + +import ( + "golang.org/x/xerrors" + + "github.com/aquasecurity/go-gem-version" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare" +) + +// Comparer represents a comparer for RubyGems +type Comparer struct{} + +// IsVulnerable checks if the package version is vulnerable to the advisory. +func (r Comparer) IsVulnerable(ver string, advisory dbTypes.Advisory) bool { + return compare.IsVulnerable(ver, advisory, r.matchVersion) +} + +// matchVersion checks if the package version satisfies the given constraint. +func (r Comparer) matchVersion(currentVersion, constraint string) (bool, error) { + v, err := gem.NewVersion(currentVersion) + if err != nil { + return false, xerrors.Errorf("RubyGems version error (%s): %s", currentVersion, err) + } + + c, err := gem.NewConstraints(constraint) + if err != nil { + return false, xerrors.Errorf("RubyGems constraint error (%s): %s", constraint, err) + } + + return c.Check(v), nil +} diff --git a/pkg/detector/library/compare/rubygems/compare_test.go b/pkg/detector/library/compare/rubygems/compare_test.go new file mode 100644 index 000000000000..53dbde266352 --- /dev/null +++ b/pkg/detector/library/compare/rubygems/compare_test.go @@ -0,0 +1,102 @@ +package rubygems_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/rubygems" +) + +func TestRubyGemsComparer_IsVulnerable(t *testing.T) { + type args struct { + currentVersion string + advisory types.Advisory + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "happy path", + args: args{ + currentVersion: "1.2.3", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.0"}, + }, + }, + want: false, + }, + { + name: "pre-release", + args: args{ + currentVersion: "1.2.3.a", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3"}, + }, + }, + want: true, + }, + { + name: "pre-release without dot", + args: args{ + currentVersion: "4.1a", + advisory: types.Advisory{ + UnaffectedVersions: []string{"< 4.2b1"}, + }, + }, + want: false, + }, + { + // https://github.com/aquasecurity/trivy/issues/108 + name: "hyphen", + args: args{ + currentVersion: "1.9.25-x86-mingw32", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.9.24"}, + }, + }, + want: false, + }, + { + // https://github.com/aquasecurity/trivy/issues/108 + name: "pessimistic", + args: args{ + currentVersion: "1.8.6-java", + advisory: types.Advisory{ + PatchedVersions: []string{"~> 1.5.5", "~> 1.6.8", ">= 1.7.7"}, + }, + }, + want: false, + }, + { + name: "invalid version", + args: args{ + currentVersion: "1.2..4", + advisory: types.Advisory{ + PatchedVersions: []string{">=1.2.3"}, + }, + }, + want: false, + }, + { + name: "invalid constraint", + args: args{ + currentVersion: "1.2.4", + advisory: types.Advisory{ + PatchedVersions: []string{"!1.2.0"}, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := rubygems.Comparer{} + got := r.IsVulnerable(tt.args.currentVersion, tt.args.advisory) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/detect.go b/pkg/detector/library/detect.go new file mode 100644 index 000000000000..e85db70769b0 --- /dev/null +++ b/pkg/detector/library/detect.go @@ -0,0 +1,50 @@ +package library + +import ( + "context" + + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Detect scans language-specific packages and returns vulnerabilities. +func Detect(ctx context.Context, libType ftypes.LangType, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + driver, ok := NewDriver(libType) + if !ok { + return nil, nil + } + + vulns, err := detect(ctx, driver, pkgs) + if err != nil { + return nil, xerrors.Errorf("failed to scan %s vulnerabilities: %w", driver.Type(), err) + } + + return vulns, nil +} + +func detect(ctx context.Context, driver Driver, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + var vulnerabilities []types.DetectedVulnerability + for _, pkg := range pkgs { + if pkg.Version == "" { + log.DebugContext(ctx, "Skipping vulnerability scan as no version is detected for the package", + log.String("name", pkg.Name)) + continue + } + vulns, err := driver.DetectVulnerabilities(pkg.ID, pkg.Name, pkg.Version) + if err != nil { + return nil, xerrors.Errorf("failed to detect %s vulnerabilities: %w", driver.Type(), err) + } + + for i := range vulns { + vulns[i].Layer = pkg.Layer + vulns[i].PkgPath = pkg.FilePath + vulns[i].PkgIdentifier = pkg.Identifier + } + vulnerabilities = append(vulnerabilities, vulns...) + } + + return vulnerabilities, nil +} diff --git a/pkg/detector/library/driver.go b/pkg/detector/library/driver.go new file mode 100644 index 000000000000..a35e6ae14470 --- /dev/null +++ b/pkg/detector/library/driver.go @@ -0,0 +1,166 @@ +package library + +import ( + "fmt" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/ecosystem" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/detector/library/compare" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/bitnami" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/maven" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/npm" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/pep440" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/rubygems" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +// NewDriver returns a driver according to the library type +func NewDriver(libType ftypes.LangType) (Driver, bool) { + var eco ecosystem.Type + var comparer compare.Comparer + + switch libType { + case ftypes.Bundler, ftypes.GemSpec: + eco = ecosystem.RubyGems + comparer = rubygems.Comparer{} + case ftypes.RustBinary, ftypes.Cargo: + eco = ecosystem.Cargo + comparer = compare.GenericComparer{} + case ftypes.Composer, ftypes.ComposerVendor: + eco = ecosystem.Composer + comparer = compare.GenericComparer{} + case ftypes.GoBinary, ftypes.GoModule: + eco = ecosystem.Go + comparer = compare.GenericComparer{} + case ftypes.Jar, ftypes.Pom, ftypes.Gradle, ftypes.Sbt: + eco = ecosystem.Maven + comparer = maven.Comparer{} + case ftypes.Npm, ftypes.Yarn, ftypes.Pnpm, ftypes.Bun, ftypes.NodePkg, ftypes.JavaScript: + eco = ecosystem.Npm + comparer = npm.Comparer{} + case ftypes.NuGet, ftypes.DotNetCore, ftypes.PackagesProps: + eco = ecosystem.NuGet + comparer = compare.GenericComparer{} + case ftypes.Pipenv, ftypes.Poetry, ftypes.Pip, ftypes.PythonPkg, ftypes.Uv: + eco = ecosystem.Pip + comparer = pep440.Comparer{} + case ftypes.Pub: + eco = ecosystem.Pub + comparer = compare.GenericComparer{} + case ftypes.Hex: + eco = ecosystem.Erlang + comparer = compare.GenericComparer{} + case ftypes.Conan: + eco = ecosystem.Conan + // Only semver can be used for version ranges + // https://docs.conan.io/en/latest/versioning/version_ranges.html + comparer = compare.GenericComparer{} + case ftypes.Swift: + // Swift uses semver + // https://www.swift.org/package-manager/#importing-dependencies + eco = ecosystem.Swift + comparer = compare.GenericComparer{} + case ftypes.Cocoapods: + // CocoaPods uses RubyGems version specifiers + // https://guides.cocoapods.org/making/making-a-cocoapod.html#cocoapods-versioning-specifics + eco = ecosystem.Cocoapods + comparer = rubygems.Comparer{} + case ftypes.CondaPkg, ftypes.CondaEnv: + log.Warn("Conda package is supported for SBOM, not for vulnerability scanning") + return Driver{}, false + case ftypes.Bitnami: + eco = ecosystem.Bitnami + comparer = bitnami.Comparer{} + case ftypes.K8sUpstream: + eco = ecosystem.Kubernetes + comparer = compare.GenericComparer{} + case ftypes.Julia: + eco = ecosystem.Julia + comparer = compare.GenericComparer{} + default: + log.Warn("The library type is not supported for vulnerability scanning", + log.String("type", string(libType))) + return Driver{}, false + } + return Driver{ + ecosystem: eco, + comparer: comparer, + dbc: db.Config{}, + }, true +} + +// Driver represents security advisories for each programming language +type Driver struct { + ecosystem ecosystem.Type + comparer compare.Comparer + dbc db.Config +} + +// Type returns the driver ecosystem +func (d *Driver) Type() string { + return string(d.ecosystem) +} + +// DetectVulnerabilities scans buckets with the prefix according to the ecosystem. +// If "ecosystem" is pip, it looks for buckets with "pip::" and gets security advisories from those buckets. +// It allows us to add a new data source with the ecosystem prefix (e.g. pip::new-data-source) +// and detect vulnerabilities without specifying a specific bucket name. +func (d *Driver) DetectVulnerabilities(pkgID, pkgName, pkgVer string) ([]types.DetectedVulnerability, error) { + // e.g. "pip::", "npm::" + prefix := fmt.Sprintf("%s::", d.ecosystem) + advisories, err := d.dbc.GetAdvisories(prefix, vulnerability.NormalizePkgName(d.ecosystem, pkgName)) + if err != nil { + return nil, xerrors.Errorf("failed to get %s advisories: %w", d.ecosystem, err) + } + + var vulns []types.DetectedVulnerability + for _, adv := range advisories { + if !d.comparer.IsVulnerable(pkgVer, adv) { + continue + } + + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + VendorIDs: adv.VendorIDs, // Any vendors have specific IDs, e.g. GHSA, JLSEC + PkgID: pkgID, + PkgName: pkgName, + InstalledVersion: pkgVer, + FixedVersion: createFixedVersions(adv), + DataSource: adv.DataSource, + Custom: adv.Custom, + } + vulns = append(vulns, vuln) + } + + return vulns, nil +} + +func createFixedVersions(advisory dbTypes.Advisory) string { + if len(advisory.PatchedVersions) != 0 { + return joinFixedVersions(advisory.PatchedVersions) + } + + var fixedVersions []string + for _, version := range advisory.VulnerableVersions { + for s := range strings.SplitSeq(version, ",") { + s = strings.TrimSpace(s) + if !strings.HasPrefix(s, "<=") && strings.HasPrefix(s, "<") { + s = strings.TrimPrefix(s, "<") + fixedVersions = append(fixedVersions, strings.TrimSpace(s)) + } + } + } + return joinFixedVersions(fixedVersions) +} + +func joinFixedVersions(fixedVersions []string) string { + return strings.Join(lo.Uniq(fixedVersions), ", ") +} diff --git a/pkg/detector/library/driver_test.go b/pkg/detector/library/driver_test.go new file mode 100644 index 000000000000..cfd8681d56c9 --- /dev/null +++ b/pkg/detector/library/driver_test.go @@ -0,0 +1,263 @@ +package library_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/detector/library" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestDriver_Detect(t *testing.T) { + type args struct { + pkgName string + pkgVer string + } + tests := []struct { + name string + fixtures []string + libType ftypes.LangType + args args + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/php.yaml", + "testdata/fixtures/data-source.yaml", + }, + libType: ftypes.Composer, + args: args{ + pkgName: "symfony/symfony", + pkgVer: "4.2.6", + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-10909", + PkgName: "symfony/symfony", + InstalledVersion: "4.2.6", + FixedVersion: "4.2.7", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.GLAD, + Name: "GitLab Advisory Database Community", + URL: "https://gitlab.com/gitlab-org/advisories-community", + }, + }, + }, + }, + { + name: "case-sensitive go package", + fixtures: []string{ + "testdata/fixtures/go.yaml", + "testdata/fixtures/data-source.yaml", + }, + libType: ftypes.GoModule, + args: args{ + pkgName: "github.com/Masterminds/vcs", + pkgVer: "v1.13.1", + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-21235", + VendorIDs: []string{ + "GHSA-6635-c626-vj4r", + }, + PkgName: "github.com/Masterminds/vcs", + InstalledVersion: "v1.13.1", + FixedVersion: "v1.13.2", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.GLAD, + Name: "GitLab Advisory Database Community", + URL: "https://gitlab.com/gitlab-org/advisories-community", + }, + }, + }, + }, + { + name: "julia package", + fixtures: []string{ + "testdata/fixtures/julia.yaml", + "testdata/fixtures/data-source.yaml", + }, + libType: ftypes.Julia, + args: args{ + pkgName: "HTTP", + pkgVer: "1.10.16", + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2025-52479", + PkgName: "HTTP", + InstalledVersion: "1.10.16", + FixedVersion: "1.10.17", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Julia, + Name: "Julia Ecosystem Security Advisories", + URL: "https://github.com/JuliaLang/SecurityAdvisories.jl", + }, + VendorIDs: []string{ + "JLSEC-2025-1", + }, + }, + }, + }, + { + name: "non-prefixed buckets", + fixtures: []string{"testdata/fixtures/php-without-prefix.yaml"}, + libType: ftypes.Composer, + args: args{ + pkgName: "symfony/symfony", + pkgVer: "4.2.6", + }, + want: nil, + }, + { + name: "no patched versions in the advisory", + fixtures: []string{ + "testdata/fixtures/php.yaml", + "testdata/fixtures/data-source.yaml", + }, + libType: ftypes.Composer, + args: args{ + pkgName: "symfony/symfony", + pkgVer: "4.4.6", + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-5275", + PkgName: "symfony/symfony", + InstalledVersion: "4.4.6", + FixedVersion: "4.4.7", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.PhpSecurityAdvisories, + Name: "PHP Security Advisories Database", + URL: "https://github.com/FriendsOfPHP/security-advisories", + }, + }, + }, + }, + { + name: "no vulnerable versions in the advisory", + fixtures: []string{ + "testdata/fixtures/ruby.yaml", + "testdata/fixtures/data-source.yaml", + }, + libType: ftypes.Bundler, + args: args{ + pkgName: "activesupport", + pkgVer: "4.1.1", + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2015-3226", + PkgName: "activesupport", + InstalledVersion: "4.1.1", + FixedVersion: ">= 4.2.2, ~> 4.1.11", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RubySec, + Name: "Ruby Advisory Database", + URL: "https://github.com/rubysec/ruby-advisory-db", + }, + }, + }, + }, + { + name: "no vulnerability", + fixtures: []string{"testdata/fixtures/php.yaml"}, + libType: ftypes.Composer, + args: args{ + pkgName: "symfony/symfony", + pkgVer: "4.4.7", + }, + }, + { + name: "malformed JSON", + fixtures: []string{"testdata/fixtures/invalid-type.yaml"}, + libType: ftypes.Composer, + args: args{ + pkgName: "symfony/symfony", + pkgVer: "5.1.5", + }, + wantErr: "json unmarshal error", + }, + { + name: "duplicated version in advisory", + fixtures: []string{ + "testdata/fixtures/pip.yaml", + "testdata/fixtures/data-source.yaml", + }, + libType: ftypes.PythonPkg, + args: args{ + pkgName: "Django", + pkgVer: "4.2.1", + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2023-36053", + PkgName: "Django", + InstalledVersion: "4.2.1", + FixedVersion: "4.2.3", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.GHSA, + Name: "GitHub Security Advisory Pip", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip", + }, + }, + }, + }, + { + name: "Custom data for vulnerability", + fixtures: []string{ + "testdata/fixtures/go-custom-data.yaml", + "testdata/fixtures/data-source.yaml", + }, + libType: ftypes.GoBinary, + args: args{ + pkgName: "github.com/docker/docker", + pkgVer: "23.0.14", + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "GHSA-v23v-6jw2-98fq", + PkgName: "github.com/docker/docker", + InstalledVersion: "23.0.14", + FixedVersion: "23.0.15, 26.1.5, 27.1.1, 25.0.6", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.GHSA, + Name: "GitHub Security Advisory Go", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago", + }, + Custom: map[string]any{"Severity": 2.0}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize DB + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + driver, ok := library.NewDriver(tt.libType) + require.True(t, ok) + + got, err := driver.DetectVulnerabilities("", tt.args.pkgName, tt.args.pkgVer) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + // Compare + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/library/testdata/fixtures/data-source.yaml b/pkg/detector/library/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..c61c24e88285 --- /dev/null +++ b/pkg/detector/library/testdata/fixtures/data-source.yaml @@ -0,0 +1,37 @@ +- bucket: data-source + pairs: + - key: go::GitLab Advisory Database Community + value: + ID: "glad" + Name: "GitLab Advisory Database Community" + URL: "https://gitlab.com/gitlab-org/advisories-community" + - key: composer::GitLab Advisory Database Community + value: + ID: "glad" + Name: "GitLab Advisory Database Community" + URL: "https://gitlab.com/gitlab-org/advisories-community" + - key: composer::php-security-advisories + value: + ID: "php-security-advisories" + Name: "PHP Security Advisories Database" + URL: "https://github.com/FriendsOfPHP/security-advisories" + - key: rubygems::ruby-advisory-db + value: + ID: "ruby-advisory-db" + Name: "Ruby Advisory Database" + URL: "https://github.com/rubysec/ruby-advisory-db" + - key: "pip::GitHub Security Advisory Pip" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Pip" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip" + - key: "go::GitHub Security Advisory Go" + value: + ID: "ghsa" + Name: "GitHub Security Advisory Go" + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Ago" + - key: "julia::Julia Ecosystem Security Advisories" + value: + ID: "julia" + Name: "Julia Ecosystem Security Advisories" + URL: "https://github.com/JuliaLang/SecurityAdvisories.jl" diff --git a/pkg/detector/library/testdata/fixtures/go-custom-data.yaml b/pkg/detector/library/testdata/fixtures/go-custom-data.yaml new file mode 100644 index 000000000000..aea7b8c7cd01 --- /dev/null +++ b/pkg/detector/library/testdata/fixtures/go-custom-data.yaml @@ -0,0 +1,18 @@ +- bucket: "go::GitHub Security Advisory Go" + pairs: + - bucket: github.com/docker/docker + pairs: + - key: "GHSA-v23v-6jw2-98fq" + value: + PatchedVersions: + - "23.0.15" + - "26.1.5" + - "27.1.1" + - "25.0.6" + VulnerableVersions: + - ">=19.03.0, <23.0.15" + - ">=26.0.0, <26.1.5" + - ">=27.0.0, <27.1.1" + - ">=24.0.0, <25.0.6" + Custom: + Severity: 2 \ No newline at end of file diff --git a/pkg/detector/library/testdata/fixtures/go.yaml b/pkg/detector/library/testdata/fixtures/go.yaml new file mode 100644 index 000000000000..9e2a9d276a94 --- /dev/null +++ b/pkg/detector/library/testdata/fixtures/go.yaml @@ -0,0 +1,12 @@ +- bucket: "go::GitLab Advisory Database Community" + pairs: + - bucket: github.com/Masterminds/vcs + pairs: + - key: CVE-2022-21235 + value: + PatchedVersions: + - v1.13.2 + VulnerableVersions: + - "= 4.2.0, < 4.2.7" +- bucket: php-security-advisories + pairs: + - bucket: symfony/symfony + pairs: + - key: CVE-2020-5275 + value: + VulnerableVersions: + - ">= 4.4.0, < 4.4.7" \ No newline at end of file diff --git a/pkg/detector/library/testdata/fixtures/php.yaml b/pkg/detector/library/testdata/fixtures/php.yaml new file mode 100644 index 000000000000..468b9872f523 --- /dev/null +++ b/pkg/detector/library/testdata/fixtures/php.yaml @@ -0,0 +1,18 @@ +- bucket: "composer::GitLab Advisory Database Community" + pairs: + - bucket: symfony/symfony + pairs: + - key: CVE-2019-10909 + value: + PatchedVersions: + - 4.2.7 + VulnerableVersions: + - ">= 4.2.0, < 4.2.7" +- bucket: "composer::php-security-advisories" + pairs: + - bucket: symfony/symfony + pairs: + - key: CVE-2020-5275 + value: + VulnerableVersions: + - ">= 4.4.0, < 4.4.7" \ No newline at end of file diff --git a/pkg/detector/library/testdata/fixtures/pip.yaml b/pkg/detector/library/testdata/fixtures/pip.yaml new file mode 100644 index 000000000000..f39357e16fb6 --- /dev/null +++ b/pkg/detector/library/testdata/fixtures/pip.yaml @@ -0,0 +1,18 @@ +- bucket: "pip::GitHub Security Advisory Pip" + pairs: + - bucket: Django + pairs: + - key: CVE-2023-36053 + value: + PatchedVersions: + - 4.2.3 + VulnerableVersions: + - < 4.2.3 + - bucket: django + pairs: + - key: CVE-2023-36053 + value: + PatchedVersions: + - 4.2.3 + VulnerableVersions: + - < 4.2.3 diff --git a/pkg/detector/library/testdata/fixtures/ruby.yaml b/pkg/detector/library/testdata/fixtures/ruby.yaml new file mode 100644 index 000000000000..c3305ed7ad5f --- /dev/null +++ b/pkg/detector/library/testdata/fixtures/ruby.yaml @@ -0,0 +1,11 @@ +- bucket: "rubygems::ruby-advisory-db" + pairs: + - bucket: activesupport + pairs: + - key: CVE-2015-3226 + value: + PatchedVersions: + - ">= 4.2.2" + - "~> 4.1.11" + UnaffectedVersions: + - "< 4.1.0" \ No newline at end of file diff --git a/pkg/detector/ospkg/alma/alma.go b/pkg/detector/ospkg/alma/alma.go new file mode 100644 index 000000000000..feb33baaf268 --- /dev/null +++ b/pkg/detector/ospkg/alma/alma.go @@ -0,0 +1,109 @@ +package alma + +import ( + "context" + "strings" + "time" + + version "github.com/knqyf263/go-rpm-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/alma" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + eolDates = map[string]time.Time{ + // Source: + // https://endoflife.date/almalinux + "8": time.Date(2029, 3, 1, 23, 59, 59, 0, time.UTC), + "9": time.Date(2032, 5, 31, 23, 59, 59, 0, time.UTC), + "10": time.Date(2035, 5, 31, 23, 59, 59, 0, time.UTC), + } +) + +// Scanner implements the AlmaLinux scanner +type Scanner struct { + vs *alma.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{ + vs: alma.NewVulnSrc(), + } +} + +// Detect vulnerabilities in package using AlmaLinux scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + osVer = osver.Major(osVer) + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + var skipPkgs []string + for _, pkg := range pkgs { + if strings.Contains(pkg.Release, ".module_el") && pkg.Modularitylabel == "" { + skipPkgs = append(skipPkgs, pkg.Name) + continue + } + pkgName := addModularNamespace(pkg.Name, pkg.Modularitylabel) + advisories, err := s.vs.Get(db.GetParams{ + Release: osVer, + PkgName: pkgName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get AlmaLinux advisories: %w", err) + } + + installed := utils.FormatVersion(pkg) + installedVersion := version.NewVersion(installed) + for _, adv := range advisories { + fixedVersion := version.NewVersion(adv.FixedVersion) + if installedVersion.LessThan(fixedVersion) { + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: installed, + FixedVersion: fixedVersion.String(), + PkgIdentifier: pkg.Identifier, + Layer: pkg.Layer, + DataSource: adv.DataSource, + Custom: adv.Custom, + } + vulns = append(vulns, vuln) + } + } + } + if len(skipPkgs) > 0 { + log.InfoContext(ctx, "Skipped detection of the packages because modular packages cannot be detected correctly due to a bug in AlmaLinux. See also: https://bugs.almalinux.org/view.php?id=173", + log.Any("packages", skipPkgs)) + } + + return vulns, nil +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + return osver.Supported(ctx, eolDates, osFamily, osver.Major(osVer)) +} + +func addModularNamespace(name, label string) string { + // e.g. npm, nodejs:12:8030020201124152102:229f0a1c => nodejs:12::npm + var count int + for i, r := range label { + if r == ':' { + count++ + } + if count == 2 { + return label[:i] + "::" + name + } + } + return name +} diff --git a/pkg/detector/ospkg/alma/alma_test.go b/pkg/detector/ospkg/alma/alma_test.go new file mode 100644 index 000000000000..77d563100d46 --- /dev/null +++ b/pkg/detector/ospkg/alma/alma_test.go @@ -0,0 +1,223 @@ +package alma_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/alma" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/alma.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "8.4", + pkgs: []ftypes.Package{ + { + Name: "python3-libs", + Epoch: 0, + Version: "3.6.8", + Release: "36.el8.alma", + Arch: "x86_64", + SrcName: "python3", + SrcEpoch: 0, + SrcVersion: "3.6.8", + SrcRelease: "36.el8.alma", + Modularitylabel: "", + Licenses: []string{"Python"}, + Layer: ftypes.Layer{}, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "python3-libs", + VulnerabilityID: "CVE-2020-26116", + InstalledVersion: "3.6.8-36.el8.alma", + FixedVersion: "3.6.8-37.el8.alma", + Layer: ftypes.Layer{}, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Alma, + Name: "AlmaLinux Product Errata", + URL: "https://errata.almalinux.org/", + }, + }, + }, + }, + { + name: "skip modular package", + fixtures: []string{ + "testdata/fixtures/modular.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "8.4", + pkgs: []ftypes.Package{ + { + Name: "nginx", + Epoch: 1, + Version: "1.14.1", + Release: "8.module_el8.3.0+2165+af250afe.alma", + Arch: "x86_64", + SrcName: "nginx", + SrcEpoch: 1, + SrcVersion: "1.14.1", + SrcRelease: "8.module_el8.3.0+2165+af250afe.alma", + Modularitylabel: "", // ref: https://bugs.almalinux.org/view.php?id=173 , https://github.com/aquasecurity/trivy/issues/2342#issuecomment-1158459628 + Licenses: []string{"BSD"}, + Layer: ftypes.Layer{}, + }, + }, + }, + want: nil, + }, + { + name: "modular package", + fixtures: []string{ + "testdata/fixtures/modular.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "8.6", + pkgs: []ftypes.Package{ + { + Name: "httpd", + Epoch: 0, + Version: "2.4.37", + Release: "46.module_el8.6.0+2872+fe0ff7aa.1.alma", + Arch: "x86_64", + SrcName: "httpd", + SrcEpoch: 0, + SrcVersion: "2.4.37", + SrcRelease: "46.module_el8.6.0+2872+fe0ff7aa.1.alma", + Modularitylabel: "httpd:2.4:8060020220510105858:9edba152", + Licenses: []string{"ASL 2.0"}, + Layer: ftypes.Layer{}, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "httpd", + VulnerabilityID: "CVE-2020-35452", + InstalledVersion: "2.4.37-46.module_el8.6.0+2872+fe0ff7aa.1.alma", + FixedVersion: "2.4.37-47.module_el8.6.0+2872+fe0ff7aa.1.alma", + Layer: ftypes.Layer{}, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Alma, + Name: "AlmaLinux Product Errata", + URL: "https://errata.almalinux.org/", + }, + }, + }, + }, + { + name: "Get returns an error", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "8.4", + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.5-12", + SrcName: "jq", + SrcVersion: "1.5-12", + }, + }, + }, + wantErr: "failed to get AlmaLinux advisories", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := alma.NewScanner() + got, err := s.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsSupportedVersion(t *testing.T) { + type args struct { + osFamily ftypes.OSType + osVer string + } + tests := []struct { + name string + now time.Time + args args + want bool + }{ + { + name: "alma 8.4", + now: time.Date(2019, 3, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "alma", + osVer: "8.4", + }, + want: true, + }, + { + name: "alma 8.4 with EOL", + now: time.Date(2030, 1, 1, 0, 0, 0, 0, time.UTC), + args: args{ + osFamily: "alma", + osVer: "8.4", + }, + want: false, + }, + { + name: "latest", + now: time.Date(2019, 5, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "alma", + osVer: "999", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), tt.now) + s := alma.NewScanner() + got := s.IsSupportedVersion(ctx, tt.args.osFamily, tt.args.osVer) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/alma/testdata/fixtures/alma.yaml b/pkg/detector/ospkg/alma/testdata/fixtures/alma.yaml new file mode 100644 index 000000000000..af3043c75faa --- /dev/null +++ b/pkg/detector/ospkg/alma/testdata/fixtures/alma.yaml @@ -0,0 +1,10 @@ +- bucket: alma 8 + pairs: + - bucket: python3-libs + pairs: + - key: CVE-2019-16935 + value: + FixedVersion: "3.6.8-31.el8.alma" + - key: CVE-2020-26116 + value: + FixedVersion: "3.6.8-37.el8.alma" \ No newline at end of file diff --git a/pkg/detector/ospkg/alma/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/alma/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..46e77ad435cb --- /dev/null +++ b/pkg/detector/ospkg/alma/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: alma 8 + value: + ID: "alma" + Name: "AlmaLinux Product Errata" + URL: "https://errata.almalinux.org/" \ No newline at end of file diff --git a/pkg/detector/ospkg/alma/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/alma/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..a9cbeec8e062 --- /dev/null +++ b/pkg/detector/ospkg/alma/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: alma 8 + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar \ No newline at end of file diff --git a/pkg/detector/ospkg/alma/testdata/fixtures/modular.yaml b/pkg/detector/ospkg/alma/testdata/fixtures/modular.yaml new file mode 100644 index 000000000000..14a9112e9d54 --- /dev/null +++ b/pkg/detector/ospkg/alma/testdata/fixtures/modular.yaml @@ -0,0 +1,12 @@ +- bucket: alma 8 + pairs: + - bucket: nginx:1.14::nginx + pairs: + - key: CVE-2019-9511 + value: + FixedVersion: "1:1.14.1-9.module_el8.3.0+2165+af250afe.alma" + - bucket: httpd:2.4::httpd + pairs: + - key: CVE-2020-35452 + value: + FixedVersion: "2.4.37-47.module_el8.6.0+2872+fe0ff7aa.1.alma" \ No newline at end of file diff --git a/pkg/detector/ospkg/alpine/alpine.go b/pkg/detector/ospkg/alpine/alpine.go new file mode 100644 index 000000000000..86239c6f4a82 --- /dev/null +++ b/pkg/detector/ospkg/alpine/alpine.go @@ -0,0 +1,176 @@ +package alpine + +import ( + "context" + "strings" + "time" + + version "github.com/knqyf263/go-apk-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/alpine" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +var eolDates = map[string]time.Time{ + "2.0": time.Date(2012, 4, 1, 23, 59, 59, 0, time.UTC), + "2.1": time.Date(2012, 11, 1, 23, 59, 59, 0, time.UTC), + "2.2": time.Date(2013, 5, 1, 23, 59, 59, 0, time.UTC), + "2.3": time.Date(2013, 11, 1, 23, 59, 59, 0, time.UTC), + "2.4": time.Date(2014, 5, 1, 23, 59, 59, 0, time.UTC), + "2.5": time.Date(2014, 11, 1, 23, 59, 59, 0, time.UTC), + "2.6": time.Date(2015, 5, 1, 23, 59, 59, 0, time.UTC), + "2.7": time.Date(2015, 11, 1, 23, 59, 59, 0, time.UTC), + "3.0": time.Date(2016, 5, 1, 23, 59, 59, 0, time.UTC), + "3.1": time.Date(2016, 11, 1, 23, 59, 59, 0, time.UTC), + "3.2": time.Date(2017, 5, 1, 23, 59, 59, 0, time.UTC), + "3.3": time.Date(2017, 11, 1, 23, 59, 59, 0, time.UTC), + "3.4": time.Date(2018, 5, 1, 23, 59, 59, 0, time.UTC), + "3.5": time.Date(2018, 11, 1, 23, 59, 59, 0, time.UTC), + "3.6": time.Date(2019, 5, 1, 23, 59, 59, 0, time.UTC), + "3.7": time.Date(2019, 11, 1, 23, 59, 59, 0, time.UTC), + "3.8": time.Date(2020, 5, 1, 23, 59, 59, 0, time.UTC), + "3.9": time.Date(2020, 11, 1, 23, 59, 59, 0, time.UTC), + "3.10": time.Date(2021, 5, 1, 23, 59, 59, 0, time.UTC), + "3.11": time.Date(2021, 11, 1, 23, 59, 59, 0, time.UTC), + "3.12": time.Date(2022, 5, 1, 23, 59, 59, 0, time.UTC), + "3.13": time.Date(2022, 11, 1, 23, 59, 59, 0, time.UTC), + "3.14": time.Date(2023, 5, 1, 23, 59, 59, 0, time.UTC), + "3.15": time.Date(2023, 11, 1, 23, 59, 59, 0, time.UTC), + "3.16": time.Date(2024, 5, 23, 23, 59, 59, 0, time.UTC), + "3.17": time.Date(2024, 11, 22, 23, 59, 59, 0, time.UTC), + "3.18": time.Date(2025, 5, 9, 23, 59, 59, 0, time.UTC), + "3.19": time.Date(2025, 11, 1, 23, 59, 59, 0, time.UTC), + "3.20": time.Date(2026, 4, 1, 23, 59, 59, 0, time.UTC), + "3.21": time.Date(2026, 12, 5, 23, 59, 59, 0, time.UTC), + "3.22": time.Date(2027, 4, 30, 23, 59, 59, 0, time.UTC), + "3.23": time.Date(2027, 11, 1, 23, 59, 59, 0, time.UTC), + "edge": time.Date(9999, 1, 1, 0, 0, 0, 0, time.UTC), +} + +// Scanner implements the Alpine scanner +type Scanner struct { + vs alpine.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{ + vs: alpine.NewVulnSrc(), + } +} + +// Detect vulnerabilities in package using Alpine scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, repo *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + osVer = osver.Minor(osVer) + repoRelease := s.repoRelease(repo) + + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.String("repository", repoRelease), log.Int("pkg_num", len(pkgs))) + + stream := osVer + if repoRelease != "" && osVer != repoRelease { + // Prefer the repository release. Use OS version only when the repository is not detected. + stream = repoRelease + if repoRelease != "edge" { // TODO: we should detect the current edge version. + log.WarnContext(ctx, "Mixing Alpine versions is unsupported", + log.String("os", osVer), log.String("repository", repoRelease)) + } + } + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + srcName := pkg.SrcName + if srcName == "" { + srcName = pkg.Name + } + advisories, err := s.vs.Get(db.GetParams{ + Release: stream, + PkgName: srcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get alpine advisories: %w", err) + } + + sourceVersion, err := version.NewVersion(utils.FormatSrcVersion(pkg)) + if err != nil { + log.DebugContext(ctx, "Failed to parse the installed package version", log.Err(err)) + continue + } + + for _, adv := range advisories { + if !s.isVulnerable(ctx, sourceVersion, adv) { + continue + } + vulns = append(vulns, types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: utils.FormatVersion(pkg), + FixedVersion: adv.FixedVersion, + Layer: pkg.Layer, + PkgIdentifier: pkg.Identifier, + Custom: adv.Custom, + DataSource: adv.DataSource, + }) + } + } + return vulns, nil +} + +func (s *Scanner) isVulnerable(ctx context.Context, installedVersion version.Version, adv dbTypes.Advisory) bool { + // This logic is for unfixed vulnerabilities, but Trivy DB doesn't have advisories for unfixed vulnerabilities for now + // because Alpine just provides potentially vulnerable packages. It will cause a lot of false positives. + // This is for Aqua commercial products. + if adv.AffectedVersion != "" { + // AffectedVersion means which version introduced this vulnerability. + affectedVersion, err := version.NewVersion(adv.AffectedVersion) + if err != nil { + log.DebugContext(ctx, "Failed to parse the affected package version", + log.String("version", adv.AffectedVersion), log.Err(err)) + return false + } + if affectedVersion.GreaterThan(installedVersion) { + return false + } + } + + // This logic is also for unfixed vulnerabilities. + if adv.FixedVersion == "" { + // It means the unfixed vulnerability + return true + } + + // Compare versions for fixed vulnerabilities + fixedVersion, err := version.NewVersion(adv.FixedVersion) + if err != nil { + log.DebugContext(ctx, "Failed to parse the fixed version", + log.String("version", adv.FixedVersion), log.Err(err)) + return false + } + + // It means the fixed vulnerability + return installedVersion.LessThan(fixedVersion) +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + return osver.Supported(ctx, eolDates, osFamily, osver.Minor(osVer)) +} + +func (s *Scanner) repoRelease(repo *ftypes.Repository) string { + if repo == nil { + return "" + } + release := repo.Release + if strings.Count(release, ".") > 1 { + release = release[:strings.LastIndex(release, ".")] + } + return release +} diff --git a/pkg/detector/ospkg/alpine/alpine_test.go b/pkg/detector/ospkg/alpine/alpine_test.go new file mode 100644 index 000000000000..8f66d48d841d --- /dev/null +++ b/pkg/detector/ospkg/alpine/alpine_test.go @@ -0,0 +1,334 @@ +package alpine_test + +import ( + "sort" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/alpine" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + repo *ftypes.Repository + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/alpine.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.10.2", + pkgs: []ftypes.Package{ + { + Name: "ansible", + Version: "2.6.4", + SrcName: "ansible", + SrcVersion: "2.6.4", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + Name: "invalid", + Version: "invalid", // skipped + SrcName: "invalid", + SrcVersion: "invalid", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "ansible", + VulnerabilityID: "CVE-2019-10217", + InstalledVersion: "2.6.4", + FixedVersion: "2.8.4-r0", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Alpine, + Name: "Alpine Secdb", + URL: "https://secdb.alpinelinux.org/", + }, + }, + { + PkgName: "ansible", + VulnerabilityID: "CVE-2021-20191", + InstalledVersion: "2.6.4", + FixedVersion: "", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Alpine, + Name: "Alpine Secdb", + URL: "https://secdb.alpinelinux.org/", + }, + }, + }, + }, + { + name: "contain rc", + fixtures: []string{ + "testdata/fixtures/alpine.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.10", + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "jq", + VulnerabilityID: "CVE-2020-1234", + InstalledVersion: "1.6-r0", + FixedVersion: "1.6-r1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Alpine, + Name: "Alpine Secdb", + URL: "https://secdb.alpinelinux.org/", + }, + }, + }, + }, + { + name: "contain pre", + fixtures: []string{ + "testdata/fixtures/alpine.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.10", + pkgs: []ftypes.Package{ + { + Name: "test", + Version: "0.1.0_alpha", + SrcName: "test-src", + SrcVersion: "0.1.0_alpha", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2030-0002", + PkgName: "test", + InstalledVersion: "0.1.0_alpha", + FixedVersion: "0.1.0_alpha2", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Alpine, + Name: "Alpine Secdb", + URL: "https://secdb.alpinelinux.org/", + }, + }, + }, + }, + { + name: "repository is newer than OS version", + fixtures: []string{ + "testdata/fixtures/alpine.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.9.3", + repo: &ftypes.Repository{ + Family: ftypes.Alpine, + Release: "3.10", + }, + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "jq", + VulnerabilityID: "CVE-2020-1234", + InstalledVersion: "1.6-r0", + FixedVersion: "1.6-r1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Alpine, + Name: "Alpine Secdb", + URL: "https://secdb.alpinelinux.org/", + }, + }, + }, + }, + { + name: "Get returns an error", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.10.2", + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + wantErr: "failed to get alpine advisories", + }, + { + name: "No src name", + fixtures: []string{ + "testdata/fixtures/alpine.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.9.3", + repo: &ftypes.Repository{ + Family: ftypes.Alpine, + Release: "3.10", + }, + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcVersion: "1.6-r0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "jq", + VulnerabilityID: "CVE-2020-1234", + InstalledVersion: "1.6-r0", + FixedVersion: "1.6-r1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Alpine, + Name: "Alpine Secdb", + URL: "https://secdb.alpinelinux.org/", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := alpine.NewScanner() + got, err := s.Detect(t.Context(), tt.args.osVer, tt.args.repo, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + sort.Slice(got, func(i, j int) bool { + return got[i].VulnerabilityID < got[j].VulnerabilityID + }) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsSupportedVersion(t *testing.T) { + type args struct { + osFamily ftypes.OSType + osVer string + } + tests := []struct { + name string + now time.Time + args args + want bool + }{ + { + name: "alpine 3.6", + now: time.Date(2019, 3, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "alpine", + osVer: "3.6", + }, + want: true, + }, + { + name: "alpine 3.6 with EOL", + now: time.Date(2019, 5, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "alpine", + osVer: "3.6.5", + }, + want: false, + }, + { + name: "alpine 3.9", + now: time.Date(2019, 5, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "alpine", + osVer: "3.9.0", + }, + want: true, + }, + { + name: "alpine 3.10", + now: time.Date(2019, 5, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "alpine", + osVer: "3.10", + }, + want: true, + }, + { + name: "unknown", + now: time.Date(2019, 5, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "alpine", + osVer: "unknown", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), tt.now) + s := alpine.NewScanner() + got := s.IsSupportedVersion(ctx, tt.args.osFamily, tt.args.osVer) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/alpine/testdata/fixtures/alpine.yaml b/pkg/detector/ospkg/alpine/testdata/fixtures/alpine.yaml new file mode 100644 index 000000000000..810578368790 --- /dev/null +++ b/pkg/detector/ospkg/alpine/testdata/fixtures/alpine.yaml @@ -0,0 +1,39 @@ +- bucket: alpine 3.10 + pairs: + - bucket: ansible + pairs: + - key: CVE-2018-10875 + value: + FixedVersion: "2.6.3-r0" + - key: CVE-2019-10217 + value: + FixedVersion: "2.8.4-r0" + - key: CVE-2020-1740 + value: + FixedVersion: "" + AffectedVersion: "2.6.5" + - key: CVE-2021-20191 + value: + FixedVersion: "" + - key: CVE-2019-INVALID + value: + FixedVersion: "invalid" + - bucket: jq + pairs: + - key: CVE-2016-4074 + value: + FixedVersion: "1.6_rc1-r0" + - key: CVE-2019-9999 + value: + FixedVersion: "1.6_rc2" + - key: CVE-2020-1234 + value: + FixedVersion: "1.6-r1" + - bucket: test-src + pairs: + - key: CVE-2030-0001 + value: + FixedVersion: "0.1.0_alpha_pre2" + - key: CVE-2030-0002 + value: + FixedVersion: "0.1.0_alpha2" diff --git a/pkg/detector/ospkg/alpine/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/alpine/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..ca781d2ee96d --- /dev/null +++ b/pkg/detector/ospkg/alpine/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: alpine 3.10 + value: + ID: "alpine" + Name: "Alpine Secdb" + URL: "https://secdb.alpinelinux.org/" \ No newline at end of file diff --git a/pkg/detector/ospkg/alpine/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/alpine/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..0250feaf958b --- /dev/null +++ b/pkg/detector/ospkg/alpine/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: alpine 3.10 + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/amazon/amazon.go b/pkg/detector/ospkg/amazon/amazon.go new file mode 100644 index 000000000000..681cdb672be0 --- /dev/null +++ b/pkg/detector/ospkg/amazon/amazon.go @@ -0,0 +1,114 @@ +package amazon + +import ( + "context" + "strings" + "time" + + version "github.com/knqyf263/go-deb-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/amazon" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + eolDates = map[string]time.Time{ + // https://aws.amazon.com/jp/blogs/aws/update-on-amazon-linux-ami-end-of-life/ + "1": time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC), + // https://aws.amazon.com/amazon-linux-2/faqs/?nc1=h_ls + "2": time.Date(2026, 6, 30, 23, 59, 59, 0, time.UTC), + // Amazon Linux 2022 was renamed to 2023. AL2022 is not currently supported. + "2023": time.Date(2028, 3, 15, 23, 59, 59, 0, time.UTC), + } +) + +// Scanner to scan amazon vulnerabilities +type Scanner struct { + ac amazon.VulnSrc +} + +// NewScanner is the factory method to return Amazon scanner +func NewScanner() *Scanner { + return &Scanner{ + ac: amazon.NewVulnSrc(), + } +} + +// Detect scans the packages using amazon scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + osVer = strings.Fields(osVer)[0] + // The format `2023.xxx.xxxx` can be used. + osVer = osver.Major(osVer) + if osVer != "2" && osVer != "2022" && osVer != "2023" { + osVer = "1" + } + + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + advisories, err := s.ac.Get(db.GetParams{ + Release: osVer, + PkgName: pkg.Name, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get amazon advisories: %w", err) + } + + installed := utils.FormatVersion(pkg) + if installed == "" { + continue + } + + installedVersion, err := version.NewVersion(installed) + if err != nil { + log.DebugContext(ctx, "Failed to parse the installed package version", + log.String("version", installed), log.Err(err)) + continue + } + + for _, adv := range advisories { + fixedVersion, err := version.NewVersion(adv.FixedVersion) + if err != nil { + log.DebugContext(ctx, "Failed to parse the fixed version", + log.String("version", adv.FixedVersion), log.Err(err)) + continue + } + + if installedVersion.LessThan(fixedVersion) { + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: installed, + FixedVersion: adv.FixedVersion, + PkgIdentifier: pkg.Identifier, + Layer: pkg.Layer, + Custom: adv.Custom, + DataSource: adv.DataSource, + } + vulns = append(vulns, vuln) + } + } + } + return vulns, nil +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + osVer = strings.Fields(osVer)[0] + // The format `2023.xxx.xxxx` can be used. + osVer = osver.Major(osVer) + if osVer != "2" && osVer != "2022" && osVer != "2023" { + osVer = "1" + } + + return osver.Supported(ctx, eolDates, osFamily, osVer) +} diff --git a/pkg/detector/ospkg/amazon/amazon_test.go b/pkg/detector/ospkg/amazon/amazon_test.go new file mode 100644 index 000000000000..f09eaa87ab1c --- /dev/null +++ b/pkg/detector/ospkg/amazon/amazon_test.go @@ -0,0 +1,256 @@ +package amazon_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/amazon" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "amazon linux 1", + fixtures: []string{ + "testdata/fixtures/amazon.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "1.2", + pkgs: []ftypes.Package{ + { + Name: "bind", + Epoch: 32, + Version: "9.8.2", + Release: "0.68.rc1.85.amzn1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "bind", + VulnerabilityID: "CVE-2020-8625", + InstalledVersion: "32:9.8.2-0.68.rc1.85.amzn1", + FixedVersion: "32:9.8.2-0.68.rc1.86.amzn1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Amazon, + Name: "Amazon Linux Security Center", + URL: "https://alas.aws.amazon.com/", + }, + }, + }, + }, + { + name: "amazon linux 2", + fixtures: []string{ + "testdata/fixtures/amazon.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "2", + pkgs: []ftypes.Package{ + { + Name: "bash", + Version: "4.2.45", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "bash", + VulnerabilityID: "CVE-2019-9924", + InstalledVersion: "4.2.45", + FixedVersion: "4.2.46-34.amzn2", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Amazon, + Name: "Amazon Linux Security Center", + URL: "https://alas.aws.amazon.com/", + }, + }, + }, + }, + { + name: "amazon linux 2023", + fixtures: []string{ + "testdata/fixtures/amazon.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "2023.3.20240304", + pkgs: []ftypes.Package{ + { + Name: "protobuf", + Version: "3.14.0-7.amzn2023.0.3", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "protobuf", + VulnerabilityID: "CVE-2022-1941", + InstalledVersion: "3.14.0-7.amzn2023.0.3", + FixedVersion: "3.19.6-1.amzn2023.0.1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Amazon, + Name: "Amazon Linux Security Center", + URL: "https://alas.aws.amazon.com/", + }, + }, + }, + }, + { + name: "empty version", + fixtures: []string{ + "testdata/fixtures/amazon.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "2", + pkgs: []ftypes.Package{ + { + Name: "bash", + }, + }, + }, + }, + { + name: "Get returns an error", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "1", + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + wantErr: "failed to get amazon advisories", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := amazon.NewScanner() + got, err := s.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsSupportedVersion(t *testing.T) { + type args struct { + osFamily ftypes.OSType + osVer string + } + tests := []struct { + name string + now time.Time + args args + want bool + }{ + { + name: "amazon linux 1", + now: time.Date(2022, 5, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "amazon", + osVer: "1", + }, + want: true, + }, + { + name: "amazon linux 1 EOL", + now: time.Date(2024, 5, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "amazon", + osVer: "1", + }, + want: false, + }, + { + name: "amazon linux 2", + now: time.Date(2020, 12, 1, 0, 0, 0, 0, time.UTC), + args: args{ + osFamily: "amazon", + osVer: "2", + }, + want: true, + }, + { + name: "amazon linux 2022", + now: time.Date(2020, 12, 1, 0, 0, 0, 0, time.UTC), + args: args{ + osFamily: "amazon", + osVer: "2022", + }, + want: true, + }, + { + name: "amazon linux 2023", + now: time.Date(2020, 12, 1, 0, 0, 0, 0, time.UTC), + args: args{ + osFamily: "amazon", + osVer: "2023", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), tt.now) + s := amazon.NewScanner() + got := s.IsSupportedVersion(ctx, tt.args.osFamily, tt.args.osVer) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/amazon/testdata/fixtures/amazon.yaml b/pkg/detector/ospkg/amazon/testdata/fixtures/amazon.yaml new file mode 100644 index 000000000000..fec6ad03d60b --- /dev/null +++ b/pkg/detector/ospkg/amazon/testdata/fixtures/amazon.yaml @@ -0,0 +1,28 @@ +- bucket: amazon linux 1 + pairs: + - bucket: bind + pairs: + - key: CVE-2020-8625 + value: + FixedVersion: "32:9.8.2-0.68.rc1.86.amzn1" +- bucket: amazon linux 2 + pairs: + - bucket: bash + pairs: + - key: CVE-2019-9924 + value: + FixedVersion: "4.2.46-34.amzn2" +- bucket: amazon linux 2022 + pairs: + - bucket: log4j + pairs: + - key: CVE-2021-44228 + value: + FixedVersion: "2.15.0-1.amzn2022.0.1" +- bucket: amazon linux 2023 + pairs: + - bucket: protobuf + pairs: + - key: CVE-2022-1941 + value: + FixedVersion: "3.19.6-1.amzn2023.0.1" \ No newline at end of file diff --git a/pkg/detector/ospkg/amazon/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/amazon/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..6c1d3e2c339a --- /dev/null +++ b/pkg/detector/ospkg/amazon/testdata/fixtures/data-source.yaml @@ -0,0 +1,22 @@ +- bucket: data-source + pairs: + - key: amazon linux 1 + value: + ID: "amazon" + Name: "Amazon Linux Security Center" + URL: "https://alas.aws.amazon.com/" + - key: amazon linux 2 + value: + ID: "amazon" + Name: "Amazon Linux Security Center" + URL: "https://alas.aws.amazon.com/" + - key: amazon linux 2022 + value: + ID: "amazon" + Name: "Amazon Linux Security Center" + URL: "https://alas.aws.amazon.com/" + - key: amazon linux 2023 + value: + ID: "amazon" + Name: "Amazon Linux Security Center" + URL: "https://alas.aws.amazon.com/" \ No newline at end of file diff --git a/pkg/detector/ospkg/amazon/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/amazon/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..073f4391cf87 --- /dev/null +++ b/pkg/detector/ospkg/amazon/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: amazon linux 1 + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar \ No newline at end of file diff --git a/pkg/detector/ospkg/azure/azure.go b/pkg/detector/ospkg/azure/azure.go new file mode 100644 index 000000000000..37e34bda7cf9 --- /dev/null +++ b/pkg/detector/ospkg/azure/azure.go @@ -0,0 +1,91 @@ +package azure + +import ( + "context" + + version "github.com/knqyf263/go-rpm-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/azure" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Scanner implements the CBL-Mariner scanner +type Scanner struct { + vs azure.VulnSrc +} + +// NewScanner is the factory method for Scanner +func newScanner(distribution azure.Distribution) *Scanner { + return &Scanner{ + vs: azure.NewVulnSrc(distribution), + } +} + +func NewAzureScanner() *Scanner { + return newScanner(azure.Azure) +} + +func NewMarinerScanner() *Scanner { + return newScanner(azure.Mariner) +} + +// Detect vulnerabilities in package using CBL-Mariner scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + // e.g. 1.0.20210127 + osVer = osver.Minor(osVer) + + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + // Azure Linux OVAL contains source package names only. + advisories, err := s.vs.Get(db.GetParams{ + Release: osVer, + PkgName: pkg.SrcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get Azure Linux advisories: %w", err) + } + + sourceVersion := version.NewVersion(utils.FormatSrcVersion(pkg)) + + for _, adv := range advisories { + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgName: pkg.Name, + InstalledVersion: utils.FormatVersion(pkg), + PkgIdentifier: pkg.Identifier, + Layer: pkg.Layer, + DataSource: adv.DataSource, + } + + // Unpatched vulnerabilities + if adv.FixedVersion == "" { + vulns = append(vulns, vuln) + continue + } + + // Patched vulnerabilities + fixedVersion := version.NewVersion(adv.FixedVersion) + if sourceVersion.LessThan(fixedVersion) { + vuln.FixedVersion = fixedVersion.String() + vulns = append(vulns, vuln) + } + } + } + + return vulns, nil +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool { + // EOL is not in public at the moment. + return true +} diff --git a/pkg/detector/ospkg/azure/azure_test.go b/pkg/detector/ospkg/azure/azure_test.go new file mode 100644 index 000000000000..1436a7a5e4e1 --- /dev/null +++ b/pkg/detector/ospkg/azure/azure_test.go @@ -0,0 +1,197 @@ +package azure_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + azurevs "github.com/aquasecurity/trivy-db/pkg/vulnsrc/azure" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/azure" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + dist azurevs.Distribution + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path 1.0 SrcName and Name are different", + fixtures: []string{ + "testdata/fixtures/azure.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + dist: azurevs.Mariner, + osVer: "1.0", + pkgs: []ftypes.Package{ + { + Name: "bind-utils", + Epoch: 0, + Version: "9.16.14", + Release: "1.cm1", + Arch: "aarch64", + SrcName: "bind", + SrcEpoch: 0, + SrcVersion: "9.16.14", + SrcRelease: "1.cm1", + Licenses: []string{"ISC"}, + Layer: ftypes.Layer{}, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "bind-utils", + VulnerabilityID: "CVE-2019-6470", + InstalledVersion: "9.16.14-1.cm1", + FixedVersion: "9.16.15-1.cm1", + Layer: ftypes.Layer{}, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.CBLMariner, + Name: "CBL-Mariner Vulnerability Data", + URL: "https://github.com/microsoft/CBL-MarinerVulnerabilityData", + }, + }, + }, + }, + { + name: "happy path 2.0", + fixtures: []string{ + "testdata/fixtures/azure.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + dist: azurevs.Mariner, + osVer: "2.0", + pkgs: []ftypes.Package{ + { + Name: "vim", + Epoch: 0, + Version: "8.2.4081", + Release: "1.cm1", + Arch: "aarch64", + SrcName: "vim", + SrcEpoch: 0, + SrcVersion: "8.2.4081", + SrcRelease: "1.cm1", + Licenses: []string{"Vim"}, + Layer: ftypes.Layer{}, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "vim", + VulnerabilityID: "CVE-2022-0261", + InstalledVersion: "8.2.4081-1.cm1", + Layer: ftypes.Layer{}, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.CBLMariner, + Name: "CBL-Mariner Vulnerability Data", + URL: "https://github.com/microsoft/CBL-MarinerVulnerabilityData", + }, + }, + }, + }, + { + name: "happy path 3.0", + fixtures: []string{ + "testdata/fixtures/azure.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + dist: azurevs.Azure, + osVer: "3.0", + pkgs: []ftypes.Package{ + { + Name: "php", + Epoch: 0, + Version: "8.3.6", + Release: "1.azl3", + Arch: "aarch64", + SrcName: "php", + SrcEpoch: 0, + SrcVersion: "8.3.6", + SrcRelease: "1.azl3", + Licenses: []string{"Php"}, + Layer: ftypes.Layer{}, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "php", + VulnerabilityID: "CVE-2024-2408", + InstalledVersion: "8.3.6-1.azl3", + FixedVersion: "8.3.8-1.azl3", + Layer: ftypes.Layer{}, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.AzureLinux, + Name: "Azure Linux Vulnerability Data", + URL: "https://github.com/microsoft/AzureLinuxVulnerabilityData", + }, + }, + }, + }, + { + name: "broken advisory", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + dist: azurevs.Mariner, + osVer: "1.0", + pkgs: []ftypes.Package{ + { + Name: "bind-utils", + Epoch: 0, + Version: "9.16.14", + Release: "1.cm1", + Arch: "aarch64", + SrcName: "bind", + SrcEpoch: 0, + SrcVersion: "9.16.14", + SrcRelease: "1.cm1", + Licenses: []string{"ISC"}, + Layer: ftypes.Layer{}, + }, + }, + }, + wantErr: "failed to get Azure Linux advisories", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := azure.NewAzureScanner() + if tt.args.dist == azurevs.Mariner { + s = azure.NewMarinerScanner() + } + got, err := s.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/azure/testdata/fixtures/azure.yaml b/pkg/detector/ospkg/azure/testdata/fixtures/azure.yaml new file mode 100644 index 000000000000..f9829e17ad41 --- /dev/null +++ b/pkg/detector/ospkg/azure/testdata/fixtures/azure.yaml @@ -0,0 +1,24 @@ +- bucket: CBL-Mariner 1.0 + pairs: + - bucket: bind + pairs: + - key: CVE-2020-8618 + value: + FixedVersion: 0:9.16.3-2.cm1 + - key: CVE-2019-6470 + value: + FixedVersion: 0:9.16.15-1.cm1 + +- bucket: CBL-Mariner 2.0 + pairs: + - bucket: vim + pairs: + - key: CVE-2022-0261 + +- bucket: Azure Linux 3.0 + pairs: + - bucket: php + pairs: + - key: CVE-2024-2408 + value: + FixedVersion: 8.3.8-1.azl3 diff --git a/pkg/detector/ospkg/azure/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/azure/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..7c9f386f157d --- /dev/null +++ b/pkg/detector/ospkg/azure/testdata/fixtures/data-source.yaml @@ -0,0 +1,21 @@ +- bucket: data-source + pairs: + - key: CBL-Mariner 1.0 + value: + ID: "cbl-mariner" + Name: "CBL-Mariner Vulnerability Data" + URL: "https://github.com/microsoft/CBL-MarinerVulnerabilityData" +- bucket: data-source + pairs: + - key: CBL-Mariner 2.0 + value: + ID: "cbl-mariner" + Name: "CBL-Mariner Vulnerability Data" + URL: "https://github.com/microsoft/CBL-MarinerVulnerabilityData" +- bucket: data-source + pairs: + - key: Azure Linux 3.0 + value: + ID: "azure" + Name: "Azure Linux Vulnerability Data" + URL: "https://github.com/microsoft/AzureLinuxVulnerabilityData" diff --git a/pkg/detector/ospkg/azure/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/azure/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..84273b060928 --- /dev/null +++ b/pkg/detector/ospkg/azure/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: CBL-Mariner 1.0 + pairs: + - bucket: bind + pairs: + - key: CVE-2021-25219 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/bottlerocket/bottlerocket.go b/pkg/detector/ospkg/bottlerocket/bottlerocket.go new file mode 100644 index 000000000000..9dcd6ac43383 --- /dev/null +++ b/pkg/detector/ospkg/bottlerocket/bottlerocket.go @@ -0,0 +1,28 @@ +package bottlerocket + +import ( + "context" + + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Scanner implements the Bottlerocket scanner +type Scanner struct { +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{} +} + +func (s *Scanner) Detect(ctx context.Context, _ string, _ *ftypes.Repository, _ []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Vulnerability detection of Bottlerocket packages is currently not supported.") + return nil, nil +} + +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + return osver.Supported(ctx, nil, osFamily, osver.Minor(osVer)) +} diff --git a/pkg/detector/ospkg/chainguard/chainguard.go b/pkg/detector/ospkg/chainguard/chainguard.go new file mode 100644 index 000000000000..be7d5802b6e3 --- /dev/null +++ b/pkg/detector/ospkg/chainguard/chainguard.go @@ -0,0 +1,93 @@ +package chainguard + +import ( + "context" + + version "github.com/knqyf263/go-apk-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/chainguard" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Scanner implements the Chainguard scanner +type Scanner struct { + vs chainguard.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{ + vs: chainguard.NewVulnSrc(), + } +} + +// Detect vulnerabilities in package using Chainguard scanner +func (s *Scanner) Detect(ctx context.Context, _ string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Detecting Chainguard vulnerabilities...", log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + srcName := pkg.SrcName + if srcName == "" { + srcName = pkg.Name + } + advisories, err := s.vs.Get(db.GetParams{ + PkgName: srcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get Chainguard advisories: %w", err) + } + + installed := utils.FormatVersion(pkg) + installedVersion, err := version.NewVersion(installed) + if err != nil { + log.DebugContext(ctx, "Failed to parse the installed package version", log.Err(err)) + continue + } + + for _, adv := range advisories { + if !s.isVulnerable(ctx, installedVersion, adv) { + continue + } + vulns = append(vulns, types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: installed, + FixedVersion: adv.FixedVersion, + Layer: pkg.Layer, + PkgIdentifier: pkg.Identifier, + Custom: adv.Custom, + DataSource: adv.DataSource, + }) + } + } + return vulns, nil +} + +func (s *Scanner) isVulnerable(ctx context.Context, installedVersion version.Version, adv dbTypes.Advisory) bool { + // Compare versions for fixed vulnerabilities + fixedVersion, err := version.NewVersion(adv.FixedVersion) + if err != nil { + log.DebugContext(ctx, "Failed to parse the fixed version", + log.String("version", adv.FixedVersion), log.Err(err)) + return false + } + + // It means the fixed vulnerability + return installedVersion.LessThan(fixedVersion) +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool { + // Chainguard doesn't have versions, so there is no case where a given input yields a + // result of an unsupported Chainguard version. + + return true +} diff --git a/pkg/detector/ospkg/chainguard/chainguard_test.go b/pkg/detector/ospkg/chainguard/chainguard_test.go new file mode 100644 index 000000000000..f92d46d0c83f --- /dev/null +++ b/pkg/detector/ospkg/chainguard/chainguard_test.go @@ -0,0 +1,210 @@ +package chainguard_test + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/chainguard" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + repo *ftypes.Repository + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/chainguard.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "ansible", + Version: "2.6.4", + SrcName: "ansible", + SrcVersion: "2.6.4", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + Name: "invalid", + Version: "invalid", // skipped + SrcName: "invalid", + SrcVersion: "invalid", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "ansible", + VulnerabilityID: "CVE-2019-10217", + InstalledVersion: "2.6.4", + FixedVersion: "2.8.4-r0", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Chainguard, + Name: "Chainguard Secdb", + URL: "https://packages.cgr.dev/chainguard/security.json", + }, + }, + }, + }, + { + name: "contain rc", + fixtures: []string{ + "testdata/fixtures/chainguard.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "jq", + VulnerabilityID: "CVE-2020-1234", + InstalledVersion: "1.6-r0", + FixedVersion: "1.6-r1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Chainguard, + Name: "Chainguard Secdb", + URL: "https://packages.cgr.dev/chainguard/security.json", + }, + }, + }, + }, + { + name: "contain pre", + fixtures: []string{ + "testdata/fixtures/chainguard.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "test", + Version: "0.1.0_alpha", + SrcName: "test-src", + SrcVersion: "0.1.0_alpha", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2030-0002", + PkgName: "test", + InstalledVersion: "0.1.0_alpha", + FixedVersion: "0.1.0_alpha2", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Chainguard, + Name: "Chainguard Secdb", + URL: "https://packages.cgr.dev/chainguard/security.json", + }, + }, + }, + }, + { + name: "Get returns an error", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + wantErr: "failed to get Chainguard advisories", + }, + { + name: "No src name", + fixtures: []string{ + "testdata/fixtures/chainguard.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + repo: &ftypes.Repository{ + Family: ftypes.Chainguard, + Release: "3.10", + }, + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcVersion: "1.6-r0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "jq", + VulnerabilityID: "CVE-2020-1234", + InstalledVersion: "1.6-r0", + FixedVersion: "1.6-r1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Chainguard, + Name: "Chainguard Secdb", + URL: "https://packages.cgr.dev/chainguard/security.json", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := chainguard.NewScanner() + got, err := s.Detect(t.Context(), "", tt.args.repo, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + sort.Slice(got, func(i, j int) bool { + return got[i].VulnerabilityID < got[j].VulnerabilityID + }) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/chainguard/testdata/fixtures/chainguard.yaml b/pkg/detector/ospkg/chainguard/testdata/fixtures/chainguard.yaml new file mode 100644 index 000000000000..c6015908d392 --- /dev/null +++ b/pkg/detector/ospkg/chainguard/testdata/fixtures/chainguard.yaml @@ -0,0 +1,39 @@ +- bucket: chainguard + pairs: + - bucket: ansible + pairs: + - key: CVE-2018-10875 + value: + FixedVersion: "2.6.3-r0" + - key: CVE-2019-10217 + value: + FixedVersion: "2.8.4-r0" + - key: CVE-2020-1740 + value: + FixedVersion: "" + AffectedVersion: "2.6.5" + - key: CVE-2021-20191 + value: + FixedVersion: "" + - key: CVE-2019-INVALID + value: + FixedVersion: "invalid" + - bucket: jq + pairs: + - key: CVE-2016-4074 + value: + FixedVersion: "1.6_rc1-r0" + - key: CVE-2019-9999 + value: + FixedVersion: "1.6_rc2" + - key: CVE-2020-1234 + value: + FixedVersion: "1.6-r1" + - bucket: test-src + pairs: + - key: CVE-2030-0001 + value: + FixedVersion: "0.1.0_alpha_pre2" + - key: CVE-2030-0002 + value: + FixedVersion: "0.1.0_alpha2" diff --git a/pkg/detector/ospkg/chainguard/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/chainguard/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..b29a5f0eb011 --- /dev/null +++ b/pkg/detector/ospkg/chainguard/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: chainguard + value: + ID: "chainguard" + Name: "Chainguard Secdb" + URL: "https://packages.cgr.dev/chainguard/security.json" \ No newline at end of file diff --git a/pkg/detector/ospkg/chainguard/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/chainguard/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..b2fd595eb9b7 --- /dev/null +++ b/pkg/detector/ospkg/chainguard/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: chainguard + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/coreos/coreos.go b/pkg/detector/ospkg/coreos/coreos.go new file mode 100644 index 000000000000..6c98d550b90c --- /dev/null +++ b/pkg/detector/ospkg/coreos/coreos.go @@ -0,0 +1,28 @@ +package coreos + +import ( + "context" + + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Scanner implements the CoreOS scanner +type Scanner struct { +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{} +} + +func (s *Scanner) Detect(ctx context.Context, _ string, _ *ftypes.Repository, _ []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Vulnerability detection of CoreOS packages is currently not supported.") + return nil, nil +} + +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + return osver.Supported(ctx, nil, osFamily, osver.Minor(osVer)) +} diff --git a/pkg/detector/ospkg/debian/debian.go b/pkg/detector/ospkg/debian/debian.go new file mode 100644 index 000000000000..a24d7fb6e4c8 --- /dev/null +++ b/pkg/detector/ospkg/debian/debian.go @@ -0,0 +1,128 @@ +package debian + +import ( + "context" + "time" + + version "github.com/knqyf263/go-deb-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/debian" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + eolDates = map[string]time.Time{ + "1.1": time.Date(1997, 6, 5, 23, 59, 59, 0, time.UTC), + "1.2": time.Date(1998, 6, 5, 23, 59, 59, 0, time.UTC), + "1.3": time.Date(1999, 3, 9, 23, 59, 59, 0, time.UTC), + "2.0": time.Date(2000, 3, 9, 23, 59, 59, 0, time.UTC), + "2.1": time.Date(2000, 10, 30, 23, 59, 59, 0, time.UTC), + "2.2": time.Date(2003, 7, 30, 23, 59, 59, 0, time.UTC), + "3.0": time.Date(2006, 6, 30, 23, 59, 59, 0, time.UTC), + "3.1": time.Date(2008, 3, 30, 23, 59, 59, 0, time.UTC), + "4.0": time.Date(2010, 2, 15, 23, 59, 59, 0, time.UTC), + "5.0": time.Date(2012, 2, 6, 23, 59, 59, 0, time.UTC), + // LTS + "6.0": time.Date(2016, 2, 29, 23, 59, 59, 0, time.UTC), + "7": time.Date(2018, 5, 31, 23, 59, 59, 0, time.UTC), + "8": time.Date(2020, 6, 30, 23, 59, 59, 0, time.UTC), + "9": time.Date(2022, 6, 30, 23, 59, 59, 0, time.UTC), + "10": time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC), + "11": time.Date(2026, 8, 14, 23, 59, 59, 0, time.UTC), + "12": time.Date(2028, 6, 10, 23, 59, 59, 0, time.UTC), + "13": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC), + } +) + +// Scanner implements the Debian scanner +type Scanner struct { + vs debian.VulnSrc +} + +// NewScanner is the factory method to return Scanner +func NewScanner() *Scanner { + return &Scanner{ + vs: debian.NewVulnSrc(), + } +} + +// Detect scans and return vulnerabilities using Debian scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + osVer = osver.Major(osVer) + + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + sourceVersion, err := version.NewVersion(utils.FormatSrcVersion(pkg)) + if err != nil { + log.DebugContext(ctx, "Installed package version error", log.Err(err)) + continue + } + + advisories, err := s.vs.Get(db.GetParams{ + Release: osVer, + PkgName: pkg.SrcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get debian advisories: %w", err) + } + + for _, adv := range advisories { + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + VendorIDs: adv.VendorIDs, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: utils.FormatVersion(pkg), + FixedVersion: adv.FixedVersion, + PkgIdentifier: pkg.Identifier, + Status: adv.Status, + Layer: pkg.Layer, + Custom: adv.Custom, + DataSource: adv.DataSource, + } + + if adv.Severity != dbTypes.SeverityUnknown { + // Package-specific severity + vuln.SeveritySource = vulnerability.Debian + vuln.Vulnerability = dbTypes.Vulnerability{ + Severity: adv.Severity.String(), + } + } + + // It means unfixed vulnerability. We don't have to compare versions. + if adv.FixedVersion == "" { + vulns = append(vulns, vuln) + continue + } + + var fixedVersion version.Version + fixedVersion, err = version.NewVersion(adv.FixedVersion) + if err != nil { + log.DebugContext(ctx, "Failed to parse the fixed version", + log.String("version", adv.FixedVersion), log.Err(err)) + continue + } + + if sourceVersion.LessThan(fixedVersion) { + vulns = append(vulns, vuln) + } + } + } + return vulns, nil +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + return osver.Supported(ctx, eolDates, osFamily, osver.Major(osVer)) +} diff --git a/pkg/detector/ospkg/debian/debian_test.go b/pkg/detector/ospkg/debian/debian_test.go new file mode 100644 index 000000000000..b038c359890a --- /dev/null +++ b/pkg/detector/ospkg/debian/debian_test.go @@ -0,0 +1,180 @@ +package debian_test + +import ( + "sort" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/debian" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/debian.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "9.1", + pkgs: []ftypes.Package{ + { + Name: "htpasswd", + Version: "2.4.24", + SrcName: "apache2", + SrcVersion: "2.4.24", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "htpasswd", + VulnerabilityID: "CVE-2020-11985", + VendorIDs: []string{"DSA-4884-1"}, + InstalledVersion: "2.4.24", + FixedVersion: "2.4.25-1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Debian, + Name: "Debian Security Tracker", + URL: "https://salsa.debian.org/security-tracker-team/security-tracker", + }, + }, + { + PkgName: "htpasswd", + VulnerabilityID: "CVE-2021-31618", + InstalledVersion: "2.4.24", + Status: dbTypes.StatusWillNotFix, + SeveritySource: vulnerability.Debian, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityMedium.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Debian, + Name: "Debian Security Tracker", + URL: "https://salsa.debian.org/security-tracker-team/security-tracker", + }, + }, + }, + }, + { + name: "invalid bucket", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "9.1", + pkgs: []ftypes.Package{ + { + Name: "htpasswd", + Version: "2.4.24", + SrcName: "apache2", + SrcVersion: "2.4.24", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + wantErr: "failed to get debian advisories", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := debian.NewScanner() + got, err := s.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + sort.Slice(got, func(i, j int) bool { + return got[i].VulnerabilityID < got[j].VulnerabilityID + }) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsSupportedVersion(t *testing.T) { + type args struct { + osFamily ftypes.OSType + osVer string + } + tests := []struct { + name string + now time.Time + args args + want bool + }{ + { + name: "debian 7", + now: time.Date(2018, 3, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "debian", + osVer: "7", + }, + want: true, + }, + { + name: "debian 8 EOL", + now: time.Date(2020, 7, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "debian", + osVer: "8.2", + }, + want: false, + }, + { + name: "latest", + now: time.Date(2020, 7, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "debian", + osVer: "999", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), tt.now) + s := debian.NewScanner() + got := s.IsSupportedVersion(ctx, tt.args.osFamily, tt.args.osVer) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/debian/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/debian/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..dbce04f64048 --- /dev/null +++ b/pkg/detector/ospkg/debian/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: debian 9 + value: + ID: "debian" + Name: "Debian Security Tracker" + URL: "https://salsa.debian.org/security-tracker-team/security-tracker" \ No newline at end of file diff --git a/pkg/detector/ospkg/debian/testdata/fixtures/debian.yaml b/pkg/detector/ospkg/debian/testdata/fixtures/debian.yaml new file mode 100644 index 000000000000..903f7e46e54b --- /dev/null +++ b/pkg/detector/ospkg/debian/testdata/fixtures/debian.yaml @@ -0,0 +1,17 @@ +- bucket: debian 9 + pairs: + - bucket: apache2 + pairs: + - key: CVE-2012-3499 + value: + FixedVersion: "2.2.22-13" + - key: CVE-2020-11985 + value: + FixedVersion: "2.4.25-1" + VendorIDs: + - DSA-4884-1 + - key: CVE-2021-31618 + value: + FixedVersion: "" + Severity: 2 + Status: 5 diff --git a/pkg/detector/ospkg/debian/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/debian/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..32ff0c70ab1c --- /dev/null +++ b/pkg/detector/ospkg/debian/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: debian 9 + pairs: + - bucket: apache2 + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar \ No newline at end of file diff --git a/pkg/detector/ospkg/detect.go b/pkg/detector/ospkg/detect.go new file mode 100644 index 000000000000..8f4cff7cf8ed --- /dev/null +++ b/pkg/detector/ospkg/detect.go @@ -0,0 +1,140 @@ +package ospkg + +import ( + "context" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/detector/ospkg/alma" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/alpine" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/amazon" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/azure" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/bottlerocket" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/chainguard" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/coreos" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/debian" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/driver" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/echo" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/minimos" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/oracle" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/photon" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/redhat" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/rocky" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/seal" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/suse" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/ubuntu" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/wolfi" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Detector detects OS package vulnerabilities. +type Detector struct { + target types.ScanTarget + driver driver.Driver +} + +var ( + // ErrUnsupportedOS defines error for unsupported OS + ErrUnsupportedOS = xerrors.New("unsupported os") + + drivers = map[ftypes.OSType]driver.Driver{ + ftypes.Alpine: alpine.NewScanner(), + ftypes.Alma: alma.NewScanner(), + ftypes.Amazon: amazon.NewScanner(), + ftypes.Azure: azure.NewAzureScanner(), + ftypes.Bottlerocket: bottlerocket.NewScanner(), + ftypes.CBLMariner: azure.NewMarinerScanner(), + ftypes.Debian: debian.NewScanner(), + ftypes.Ubuntu: ubuntu.NewScanner(), + ftypes.RedHat: redhat.NewScanner(), + ftypes.CentOS: redhat.NewScanner(), + ftypes.Rocky: rocky.NewScanner(), + ftypes.Oracle: oracle.NewScanner(), + ftypes.OpenSUSETumbleweed: suse.NewScanner(suse.OpenSUSETumbleweed), + ftypes.OpenSUSELeap: suse.NewScanner(suse.OpenSUSE), + ftypes.SLES: suse.NewScanner(suse.SUSEEnterpriseLinux), + ftypes.SLEMicro: suse.NewScanner(suse.SUSEEnterpriseLinuxMicro), + ftypes.Photon: photon.NewScanner(), + ftypes.Wolfi: wolfi.NewScanner(), + ftypes.Chainguard: chainguard.NewScanner(), + ftypes.Echo: echo.NewScanner(), + ftypes.MinimOS: minimos.NewScanner(), + ftypes.CoreOS: coreos.NewScanner(), + } + + // providers dynamically generate drivers based on package information + // and environment detection. They are tried before standard OS-specific drivers. + providers = []driver.Provider{ + rootio.Provider, + seal.Provider, + } +) + +// NewDetector creates a new Detector for the given scan target +func NewDetector(target types.ScanTarget) (*Detector, error) { + drv, err := newDriver(target.OS.Family, target.Packages) + if err != nil { + return nil, err + } + return &Detector{ + target: target, + driver: drv, + }, nil +} + +// Detect detects the vulnerabilities +func (d *Detector) Detect(ctx context.Context) ([]types.DetectedVulnerability, bool, error) { + ctx = log.WithContextPrefix(ctx, string(d.target.OS.Family)) + + eosl := !d.driver.IsSupportedVersion(ctx, d.target.OS.Family, d.target.OS.Name) + + filteredPkgs := filterPkgs(ctx, d.target.Packages) + vulns, err := d.driver.Detect(ctx, d.target.OS.Name, d.target.Repository, filteredPkgs) + if err != nil { + return nil, false, xerrors.Errorf("failed detection: %w", err) + } + + return vulns, eosl, nil +} + +// filterPkgs filters out packages that should not be scanned: +// - gpg-pubkey: doesn't use the correct version +// - Third-party packages: not covered by official OS security advisories +func filterPkgs(ctx context.Context, pkgs []ftypes.Package) []ftypes.Package { + var skipped []string + filtered := lo.Filter(pkgs, func(pkg ftypes.Package, _ int) bool { + if pkg.Name == "gpg-pubkey" { + return false + } + if pkg.Repository.Class == ftypes.RepositoryClassThirdParty { + skipped = append(skipped, pkg.Name) + return false + } + return true + }) + if len(skipped) > 0 { + log.DebugContext(ctx, "Skipping third-party packages", log.Any("packages", skipped)) + } + return filtered +} + +func newDriver(osFamily ftypes.OSType, pkgs []ftypes.Package) (driver.Driver, error) { + // Try providers first + for _, provider := range providers { + if d := provider(osFamily, pkgs); d != nil { + return d, nil + } + } + + // Fall back to standard drivers + if d, ok := drivers[osFamily]; ok { + return d, nil + } + + log.Warn("Unsupported os", log.String("family", string(osFamily))) + return nil, ErrUnsupportedOS +} diff --git a/pkg/detector/ospkg/detect_test.go b/pkg/detector/ospkg/detect_test.go new file mode 100644 index 000000000000..b78e01be7b14 --- /dev/null +++ b/pkg/detector/ospkg/detect_test.go @@ -0,0 +1,156 @@ +package ospkg_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/detector/ospkg" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +type mockDriver struct { + receivedPkgs []ftypes.Package +} + +func (m *mockDriver) Detect(_ context.Context, _ string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + m.receivedPkgs = pkgs + return nil, nil +} + +func (m *mockDriver) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool { + return true +} + +func TestDetector_Detect(t *testing.T) { + tests := []struct { + name string + target types.ScanTarget + wantPkgs []ftypes.Package + }{ + { + name: "filter out gpg-pubkey package", + target: types.ScanTarget{ + OS: ftypes.OS{ + Family: ftypes.CentOS, + Name: "7", + }, + Packages: []ftypes.Package{ + {Name: "vim"}, + {Name: "gpg-pubkey"}, + }, + }, + wantPkgs: []ftypes.Package{ + {Name: "vim"}, + }, + }, + { + name: "filter out third-party packages", + target: types.ScanTarget{ + OS: ftypes.OS{ + Family: ftypes.CentOS, + Name: "7", + }, + Packages: []ftypes.Package{ + { + Name: "vim", + Repository: ftypes.PackageRepository{ + Class: ftypes.RepositoryClassOfficial, + }, + }, + { + Name: "php", + Repository: ftypes.PackageRepository{ + Class: ftypes.RepositoryClassThirdParty, + }, + }, + }, + }, + wantPkgs: []ftypes.Package{ + { + Name: "vim", + Repository: ftypes.PackageRepository{ + Class: ftypes.RepositoryClassOfficial, + }, + }, + }, + }, + { + name: "filter out both gpg-pubkey and third-party packages", + target: types.ScanTarget{ + OS: ftypes.OS{ + Family: ftypes.CentOS, + Name: "7", + }, + Packages: []ftypes.Package{ + { + Name: "vim", + Repository: ftypes.PackageRepository{ + Class: ftypes.RepositoryClassOfficial, + }, + }, + {Name: "gpg-pubkey"}, + { + Name: "php", + Repository: ftypes.PackageRepository{ + Class: ftypes.RepositoryClassThirdParty, + }, + }, + }, + }, + wantPkgs: []ftypes.Package{ + { + Name: "vim", + Repository: ftypes.PackageRepository{ + Class: ftypes.RepositoryClassOfficial, + }, + }, + }, + }, + { + name: "keep packages with unknown repository class", + target: types.ScanTarget{ + OS: ftypes.OS{ + Family: ftypes.CentOS, + Name: "7", + }, + Packages: []ftypes.Package{ + { + Name: "vim", + Repository: ftypes.PackageRepository{ + Class: ftypes.RepositoryClassUnknown, + }, + }, + { + Name: "curl", + }, + }, + }, + wantPkgs: []ftypes.Package{ + { + Name: "vim", + Repository: ftypes.PackageRepository{ + Class: ftypes.RepositoryClassUnknown, + }, + }, + { + Name: "curl", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockDrv := &mockDriver{} + d := ospkg.NewTestDetector(tt.target, mockDrv) + + _, _, err := d.Detect(t.Context()) + require.NoError(t, err) + assert.Equal(t, tt.wantPkgs, mockDrv.receivedPkgs) + }) + } +} diff --git a/pkg/detector/ospkg/driver/driver.go b/pkg/detector/ospkg/driver/driver.go new file mode 100644 index 000000000000..f693bd62c166 --- /dev/null +++ b/pkg/detector/ospkg/driver/driver.go @@ -0,0 +1,17 @@ +package driver + +import ( + "context" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Driver defines operations for OS package scan +type Driver interface { + Detect(context.Context, string, *ftypes.Repository, []ftypes.Package) ([]types.DetectedVulnerability, error) + IsSupportedVersion(context.Context, ftypes.OSType, string) bool +} + +// Provider creates a specialized driver based on the environment +type Provider func(osFamily ftypes.OSType, pkgs []ftypes.Package) Driver diff --git a/pkg/detector/ospkg/echo/echo.go b/pkg/detector/ospkg/echo/echo.go new file mode 100644 index 000000000000..1b68f27a01c5 --- /dev/null +++ b/pkg/detector/ospkg/echo/echo.go @@ -0,0 +1,83 @@ +package echo + +import ( + "context" + + version "github.com/knqyf263/go-deb-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + echoDb "github.com/aquasecurity/trivy-db/pkg/vulnsrc/echo" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +type Scanner struct { + vs echoDb.VulnSrc +} + +func NewScanner() *Scanner { + return &Scanner{ + vs: echoDb.NewVulnSrc(), + } +} + +func (s *Scanner) Detect(ctx context.Context, _ string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Detecting vulnerabilities...", log.Int("pkg_num", len(pkgs))) + var detectedVulns []types.DetectedVulnerability + for _, pkg := range pkgs { + advisories, err := s.vs.Get(db.GetParams{ + PkgName: pkg.SrcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get echo advisories: %w", err) + } + formattedInstalledVersion := utils.FormatSrcVersion(pkg) + installedVersion, err := version.NewVersion(formattedInstalledVersion) + if err != nil { + return nil, xerrors.Errorf("failed to parse installed version: %w", err) + } + for _, advisory := range advisories { + vuln := types.DetectedVulnerability{ + PkgID: pkg.ID, + VulnerabilityID: advisory.VulnerabilityID, + InstalledVersion: formattedInstalledVersion, + FixedVersion: advisory.FixedVersion, + PkgName: pkg.Name, + PkgIdentifier: pkg.Identifier, + Status: advisory.Status, + Layer: pkg.Layer, + Custom: advisory.Custom, + DataSource: advisory.DataSource, + } + + if advisory.Severity != dbTypes.SeverityUnknown { + vuln.SeveritySource = vulnerability.Echo + vuln.Vulnerability = dbTypes.Vulnerability{ + Severity: advisory.Severity.String(), + } + } + + if advisory.FixedVersion != "" { + fixedVersion, err := version.NewVersion(advisory.FixedVersion) + if err != nil { + return nil, xerrors.Errorf("failed to parse fixed version: %w", err) + } + if !installedVersion.LessThan(fixedVersion) { + continue + } + } + detectedVulns = append(detectedVulns, vuln) + } + } + return detectedVulns, nil +} + +// Echo is a rolling distro, meaning there are no versions, and therefor no need to check the version +func (s *Scanner) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool { + return true +} diff --git a/pkg/detector/ospkg/echo/echo_test.go b/pkg/detector/ospkg/echo/echo_test.go new file mode 100644 index 000000000000..238a3f18810f --- /dev/null +++ b/pkg/detector/ospkg/echo/echo_test.go @@ -0,0 +1,269 @@ +package echo + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + want []types.DetectedVulnerability + wantErr string + fixtures []string + }{ + { + name: "happy path - detect vulnerabilities", + fixtures: []string{ + "testdata/fixtures/echo.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + ID: "echo", + Name: "echo", + Version: "1.0.0", + SrcName: "echo", + SrcVersion: "1.0.0", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + ID: "python3", + Name: "python3", + Version: "3.6.8", + SrcName: "python3", + SrcVersion: "3.6.8", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + ID: "apache2", + Name: "htpasswd", + SrcName: "apache2", + Version: "2.4.24", + SrcVersion: "2.4.24", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-11985", + PkgID: "apache2", + InstalledVersion: "2.4.24", + FixedVersion: "2.4.25-1", + PkgName: "htpasswd", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: "LOW", + }, + SeveritySource: vulnerability.Echo, + DataSource: &dbTypes.DataSource{ + ID: "echo", + Name: "Echo", + URL: "https://advisory.echohq.com/data.json", + }, + }, + { + VulnerabilityID: "CVE-2020-26116", + PkgID: "python3", + InstalledVersion: "3.6.8", + FixedVersion: "3.6.9", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: "MEDIUM", + }, + PkgName: "python3", + SeveritySource: vulnerability.Echo, + DataSource: &dbTypes.DataSource{ + ID: "echo", + Name: "Echo", + URL: "https://advisory.echohq.com/data.json", + }, + }, + { + VulnerabilityID: "CVE-2021-11111", + PkgID: "apache2", + InstalledVersion: "2.4.24", + FixedVersion: "2.4.25-1", + PkgName: "htpasswd", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: "echo", + Name: "Echo", + URL: "https://advisory.echohq.com/data.json", + }, + }, + { + VulnerabilityID: "CVE-2021-11113", + PkgID: "apache2", + InstalledVersion: "2.4.24", + FixedVersion: "", + PkgName: "htpasswd", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: "echo", + Name: "Echo", + URL: "https://advisory.echohq.com/data.json", + }, + }, + }, + }, + { + name: "happy path - package with release", + fixtures: []string{ + "testdata/fixtures/echo.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + ID: "nginx", + Name: "nginx", + Version: "1.14.2", + SrcName: "nginx", + SrcVersion: "1.14.2", + Release: "1ubuntu1", + SrcRelease: "1ubuntu1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + ID: "apache2", + Name: "apache2", + SrcName: "apache2", + Version: "2.4.24", + SrcVersion: "2.4.24", + Release: "2", + SrcRelease: "2", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-11985", + PkgID: "apache2", + InstalledVersion: "2.4.24-2", + FixedVersion: "2.4.25-1", + PkgName: "apache2", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: "LOW", + }, + SeveritySource: vulnerability.Echo, + DataSource: &dbTypes.DataSource{ + ID: "echo", + Name: "Echo", + URL: "https://advisory.echohq.com/data.json", + }, + }, + { + VulnerabilityID: "CVE-2021-11111", + PkgID: "apache2", + InstalledVersion: "2.4.24-2", + FixedVersion: "2.4.25-1", + PkgName: "apache2", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: "echo", + Name: "Echo", + URL: "https://advisory.echohq.com/data.json", + }, + }, + + { + VulnerabilityID: "CVE-2021-11113", + PkgID: "apache2", + InstalledVersion: "2.4.24-2", + FixedVersion: "", + PkgName: "apache2", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: "echo", + Name: "Echo", + URL: "https://advisory.echohq.com/data.json", + }, + }, + }, + }, + { + name: "happy path - no matching packages", + args: args{ + pkgs: []ftypes.Package{ + {ID: "echo", Version: "1.0.0", SrcVersion: "1.0.0", SrcName: "echo"}, + }, + }, + want: nil, + }, + { + name: "sad path - invalid", + fixtures: []string{ + "testdata/fixtures/echo.yaml", + "testdata/fixtures/invalid.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + {SrcName: "apache2", Version: "1.0.0", SrcVersion: "1.0.0"}, + }, + }, + wantErr: "failed to get echo advisories", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := NewScanner() + got, err := s.Detect(t.Context(), "", nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + sort.Slice(got, func(i, j int) bool { + return got[i].VulnerabilityID < got[j].VulnerabilityID + }) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/echo/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/echo/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..e2789991485d --- /dev/null +++ b/pkg/detector/ospkg/echo/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: echo + value: + ID: "echo" + Name: "Echo" + URL: "https://advisory.echohq.com/data.json" diff --git a/pkg/detector/ospkg/echo/testdata/fixtures/echo.yaml b/pkg/detector/ospkg/echo/testdata/fixtures/echo.yaml new file mode 100644 index 000000000000..f4d5c9adc203 --- /dev/null +++ b/pkg/detector/ospkg/echo/testdata/fixtures/echo.yaml @@ -0,0 +1,22 @@ +- bucket: echo + pairs: + - bucket: python3 + pairs: + - key: CVE-2020-26116 + value: + FixedVersion: "3.6.9" + Severity: 2 + - bucket: apache2 + pairs: + - key: CVE-2020-11985 + value: + FixedVersion: "2.4.25-1" + Severity: 1 + - key: CVE-2021-11111 + value: + FixedVersion: "2.4.25-1" + - key: CVE-2021-11112 + value: + FixedVersion: "0" + - key: CVE-2021-11113 + value: diff --git a/pkg/detector/ospkg/echo/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/echo/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..59ec8ad98a14 --- /dev/null +++ b/pkg/detector/ospkg/echo/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: echo + pairs: + - bucket: apache2 + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/export_test.go b/pkg/detector/ospkg/export_test.go new file mode 100644 index 000000000000..d8bf3db25a04 --- /dev/null +++ b/pkg/detector/ospkg/export_test.go @@ -0,0 +1,14 @@ +package ospkg + +import ( + "github.com/aquasecurity/trivy/pkg/detector/ospkg/driver" + "github.com/aquasecurity/trivy/pkg/types" +) + +// NewTestDetector creates a Detector with custom target and driver for testing. +func NewTestDetector(target types.ScanTarget, drv driver.Driver) *Detector { + return &Detector{ + target: target, + driver: drv, + } +} diff --git a/pkg/detector/ospkg/minimos/minimos.go b/pkg/detector/ospkg/minimos/minimos.go new file mode 100644 index 000000000000..922761e2ea28 --- /dev/null +++ b/pkg/detector/ospkg/minimos/minimos.go @@ -0,0 +1,94 @@ +package minimos + +import ( + "context" + + version "github.com/knqyf263/go-apk-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/minimos" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Scanner implements the MinimOS scanner +type Scanner struct { + vs minimos.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{ + vs: minimos.NewVulnSrc(), + } +} + +// Detect vulnerabilities in package using MinimOS scanner +func (s *Scanner) Detect(ctx context.Context, _ string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Detecting MinimOS vulnerabilities...", log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + srcName := pkg.SrcName + if srcName == "" { + srcName = pkg.Name + } + advisories, err := s.vs.Get(db.GetParams{ + PkgName: srcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get MinimOS advisories: %w", err) + } + + installed := utils.FormatVersion(pkg) + installedVersion, err := version.NewVersion(installed) + if err != nil { + log.DebugContext(ctx, "Failed to parse the installed package version", + log.String("version", installed), log.Err(err)) + continue + } + + for _, adv := range advisories { + if !s.isVulnerable(ctx, installedVersion, adv) { + continue + } + vulns = append(vulns, types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: installed, + FixedVersion: adv.FixedVersion, + Layer: pkg.Layer, + PkgIdentifier: pkg.Identifier, + Custom: adv.Custom, + DataSource: adv.DataSource, + }) + } + } + return vulns, nil +} + +func (s *Scanner) isVulnerable(ctx context.Context, installedVersion version.Version, adv dbTypes.Advisory) bool { + // Compare versions for fixed vulnerabilities + fixedVersion, err := version.NewVersion(adv.FixedVersion) + if err != nil { + log.DebugContext(ctx, "Failed to parse the fixed version", + log.String("version", adv.FixedVersion), log.Err(err)) + return false + } + + // It means the fixed vulnerability + return installedVersion.LessThan(fixedVersion) +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool { + // MinimOS doesn't have versions, so there is no case where a given input yields a + // result of an unsupported MinimOS version. + + return true +} diff --git a/pkg/detector/ospkg/minimos/minimos_test.go b/pkg/detector/ospkg/minimos/minimos_test.go new file mode 100644 index 000000000000..99c9a1438f21 --- /dev/null +++ b/pkg/detector/ospkg/minimos/minimos_test.go @@ -0,0 +1,210 @@ +package minimos_test + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/minimos" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + repo *ftypes.Repository + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/minimos.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "ansible", + Version: "2.6.4", + SrcName: "ansible", + SrcVersion: "2.6.4", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + Name: "invalid", + Version: "invalid", // skipped + SrcName: "invalid", + SrcVersion: "invalid", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "ansible", + VulnerabilityID: "CVE-2019-10217", + InstalledVersion: "2.6.4", + FixedVersion: "2.8.4-r0", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.MinimOS, + Name: "MinimOS Secdb", + URL: "https://packages.mini.dev/advisories/secdb/security.json", + }, + }, + }, + }, + { + name: "contain rc", + fixtures: []string{ + "testdata/fixtures/minimos.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "jq", + VulnerabilityID: "CVE-2020-1234", + InstalledVersion: "1.6-r0", + FixedVersion: "1.6-r1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.MinimOS, + Name: "MinimOS Secdb", + URL: "https://packages.mini.dev/advisories/secdb/security.json", + }, + }, + }, + }, + { + name: "contain pre", + fixtures: []string{ + "testdata/fixtures/minimos.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "test", + Version: "0.1.0_alpha", + SrcName: "test-src", + SrcVersion: "0.1.0_alpha", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2030-0002", + PkgName: "test", + InstalledVersion: "0.1.0_alpha", + FixedVersion: "0.1.0_alpha2", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.MinimOS, + Name: "MinimOS Secdb", + URL: "https://packages.mini.dev/advisories/secdb/security.json", + }, + }, + }, + }, + { + name: "Get returns an error", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + wantErr: "failed to get MinimOS advisories", + }, + { + name: "No src name", + fixtures: []string{ + "testdata/fixtures/minimos.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + repo: &ftypes.Repository{ + Family: ftypes.MinimOS, + Release: "3.10", + }, + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcVersion: "1.6-r0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "jq", + VulnerabilityID: "CVE-2020-1234", + InstalledVersion: "1.6-r0", + FixedVersion: "1.6-r1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.MinimOS, + Name: "MinimOS Secdb", + URL: "https://packages.mini.dev/advisories/secdb/security.json", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := minimos.NewScanner() + got, err := s.Detect(t.Context(), "", tt.args.repo, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + sort.Slice(got, func(i, j int) bool { + return got[i].VulnerabilityID < got[j].VulnerabilityID + }) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/minimos/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/minimos/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..86505400a2c5 --- /dev/null +++ b/pkg/detector/ospkg/minimos/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: minimos + value: + ID: "minimos" + Name: "MinimOS Secdb" + URL: "https://packages.mini.dev/advisories/secdb/security.json" \ No newline at end of file diff --git a/pkg/detector/ospkg/minimos/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/minimos/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..fefa844fc4bb --- /dev/null +++ b/pkg/detector/ospkg/minimos/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: minimos + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/minimos/testdata/fixtures/minimos.yaml b/pkg/detector/ospkg/minimos/testdata/fixtures/minimos.yaml new file mode 100644 index 000000000000..d7d1c814d8a4 --- /dev/null +++ b/pkg/detector/ospkg/minimos/testdata/fixtures/minimos.yaml @@ -0,0 +1,35 @@ +- bucket: minimos + pairs: + - bucket: ansible + pairs: + - key: CVE-2018-10875 + value: + FixedVersion: "2.6.3-r0" + - key: CVE-2019-10217 + value: + FixedVersion: "2.8.4-r0" + - key: CVE-2021-20191 + value: + FixedVersion: "" + - key: CVE-2019-INVALID + value: + FixedVersion: "invalid" + - bucket: jq + pairs: + - key: CVE-2016-4074 + value: + FixedVersion: "1.6_rc1-r0" + - key: CVE-2019-9999 + value: + FixedVersion: "1.6_rc2" + - key: CVE-2020-1234 + value: + FixedVersion: "1.6-r1" + - bucket: test-src + pairs: + - key: CVE-2030-0001 + value: + FixedVersion: "0.1.0_alpha_pre2" + - key: CVE-2030-0002 + value: + FixedVersion: "0.1.0_alpha2" diff --git a/pkg/detector/ospkg/oracle/oracle.go b/pkg/detector/ospkg/oracle/oracle.go new file mode 100644 index 000000000000..ac38252f3843 --- /dev/null +++ b/pkg/detector/ospkg/oracle/oracle.go @@ -0,0 +1,95 @@ +package oracle + +import ( + "context" + "time" + + version "github.com/knqyf263/go-rpm-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + oracleoval "github.com/aquasecurity/trivy-db/pkg/vulnsrc/oracle-oval" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + eolDates = map[string]time.Time{ + // Source: + // https://www.oracle.com/a/ocom/docs/elsp-lifetime-069338.pdf + // https://community.oracle.com/docs/DOC-917964 + "3": time.Date(2011, 12, 31, 23, 59, 59, 0, time.UTC), + "4": time.Date(2013, 12, 31, 23, 59, 59, 0, time.UTC), + "5": time.Date(2017, 12, 31, 23, 59, 59, 0, time.UTC), + "6": time.Date(2021, 3, 21, 23, 59, 59, 0, time.UTC), + "7": time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC), + "8": time.Date(2029, 7, 18, 23, 59, 59, 0, time.UTC), + "9": time.Date(2032, 7, 18, 23, 59, 59, 0, time.UTC), + } +) + +// Scanner implements oracle vulnerability scanner +type Scanner struct { + vs *oracleoval.VulnSrc +} + +// NewScanner is the factory method to return oracle vulnerabilities +func NewScanner() *Scanner { + return &Scanner{ + vs: oracleoval.NewVulnSrc(), + } +} + +// Detect scans and return vulnerability in Oracle scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + osVer = osver.Major(osVer) + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + advisories, err := s.vs.Get(db.GetParams{ + Release: osVer, + PkgName: pkg.Name, + Arch: pkg.Arch, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get Oracle Linux advisory: %w", err) + } + + installed := utils.FormatVersion(pkg) + installedVersion := version.NewVersion(installed) + for _, adv := range advisories { + // We need to use only advisories from the same flavor as the package flavors. + // See more in https://github.com/aquasecurity/trivy/issues/1967 + if oracleoval.PackageFlavor(adv.FixedVersion) != oracleoval.PackageFlavor(pkg.Release) { + continue + } + + fixedVersion := version.NewVersion(adv.FixedVersion) + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: installed, + PkgIdentifier: pkg.Identifier, + Layer: pkg.Layer, + Custom: adv.Custom, + DataSource: adv.DataSource, + } + if installedVersion.LessThan(fixedVersion) { + vuln.FixedVersion = adv.FixedVersion + vulns = append(vulns, vuln) + } + } + } + return vulns, nil +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + return osver.Supported(ctx, eolDates, osFamily, osver.Major(osVer)) +} diff --git a/pkg/detector/ospkg/oracle/oracle_test.go b/pkg/detector/ospkg/oracle/oracle_test.go new file mode 100644 index 000000000000..0ecd4b2c0bf4 --- /dev/null +++ b/pkg/detector/ospkg/oracle/oracle_test.go @@ -0,0 +1,351 @@ +package oracle + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_IsSupportedVersion(t *testing.T) { + tests := map[string]struct { + now time.Time + osFamily ftypes.OSType + osVersion string + expected bool + }{ + "oracle3": { + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + osFamily: "oracle", + osVersion: "3", + expected: false, + }, + "oracle4": { + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + osFamily: "oracle", + osVersion: "4", + expected: false, + }, + "oracle5": { + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + osFamily: "oracle", + osVersion: "5", + expected: false, + }, + "oracle6": { + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + osFamily: "oracle", + osVersion: "6", + expected: true, + }, + "oracle7": { + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + osFamily: "oracle", + osVersion: "7", + expected: true, + }, + "oracle7.6": { + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + osFamily: "oracle", + osVersion: "7.6", + expected: true, + }, + "oracle8": { + now: time.Date(2029, 7, 18, 23, 59, 58, 59, time.UTC), + osFamily: "oracle", + osVersion: "8", + expected: true, + }, + "oracle8-same-time": { + now: time.Date(2029, 7, 18, 23, 59, 59, 0, time.UTC), + osFamily: "oracle", + osVersion: "8", + expected: false, + }, + "latest": { + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + osFamily: "oracle", + osVersion: "latest", + expected: true, + }, + } + + for testName, tt := range tests { + s := NewScanner() + t.Run(testName, func(t *testing.T) { + ctx := clock.With(t.Context(), tt.now) + actual := s.IsSupportedVersion(ctx, tt.osFamily, tt.osVersion) + if actual != tt.expected { + t.Errorf("[%s] got %v, want %v", testName, actual, tt.expected) + } + }) + } +} + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "detected", + fixtures: []string{ + "testdata/fixtures/oracle7.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "7", + pkgs: []ftypes.Package{ + { + Name: "curl", + Version: "7.29.0", + Release: "59.0.1.el7", + Arch: "x86_64", + SrcName: "curl", + SrcVersion: "7.29.0", + SrcRelease: "59.0.1.el7", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-8177", + PkgName: "curl", + InstalledVersion: "7.29.0-59.0.1.el7", + FixedVersion: "7.29.0-59.0.1.el7_9.1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.OracleOVAL, + Name: "Oracle Linux OVAL definitions", + URL: "https://linux.oracle.com/security/oval/", + }, + }, + }, + }, + { + name: "different fixed versions for different arches", + fixtures: []string{ + "testdata/fixtures/oracle7.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "7", + pkgs: []ftypes.Package{ + { + Name: "rsyslog", + Version: "8.24.0", + Release: "57.0.0.el7", + Arch: "x86_64", + SrcName: "rsyslog", + SrcVersion: "8.24.0", + SrcRelease: "57.0.0.el7", + }, + { + Name: "rsyslog", + Version: "8.24.0", + Release: "57.0.0.el7", + Arch: "aarch64", + SrcName: "rsyslog", + SrcVersion: "8.24.0", + SrcRelease: "57.0.0.el7", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-24903", + PkgName: "rsyslog", + InstalledVersion: "8.24.0-57.0.0.el7", + FixedVersion: "8.24.0-57.0.1.el7_9.3", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.OracleOVAL, + Name: "Oracle Linux OVAL definitions", + URL: "https://linux.oracle.com/security/oval/", + }, + }, + { + VulnerabilityID: "CVE-2022-24903", + PkgName: "rsyslog", + InstalledVersion: "8.24.0-57.0.0.el7", + FixedVersion: "8.24.0-57.0.4.el7_9.3", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.OracleOVAL, + Name: "Oracle Linux OVAL definitions", + URL: "https://linux.oracle.com/security/oval/", + }, + }, + }, + }, + { + name: "without ksplice", + fixtures: []string{ + "testdata/fixtures/oracle7.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "7", + pkgs: []ftypes.Package{ + { + Name: "glibc", + Version: "2.17", + Release: "317.0.1.el7", + Arch: "x86_64", + SrcName: "glibc", + SrcVersion: "2.17", + SrcRelease: "317.0.1.el7", + }, + }, + }, + want: nil, + }, + { + name: "the installed version has ksplice2", + fixtures: []string{ + "testdata/fixtures/oracle7.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "7", + pkgs: []ftypes.Package{ + { + Name: "glibc", + Epoch: 2, + Version: "2.28", + Release: "151.0.1.ksplice2.el8", + Arch: "x86_64", + SrcEpoch: 2, + SrcName: "glibc", + SrcVersion: "2.28", + SrcRelease: "151.0.1.ksplice2.el8", + }, + }, + }, + want: nil, + }, + { + name: "with ksplice", + fixtures: []string{ + "testdata/fixtures/oracle7.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "7", + pkgs: []ftypes.Package{ + { + Name: "glibc", + Epoch: 2, + Version: "2.17", + Release: "156.ksplice1.el7", + Arch: "x86_64", + SrcEpoch: 2, + SrcName: "glibc", + SrcVersion: "2.17", + SrcRelease: "156.ksplice1.el7", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2017-1000364", + PkgName: "glibc", + InstalledVersion: "2:2.17-156.ksplice1.el7", + FixedVersion: "2:2.17-157.ksplice1.el7_3.4", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.OracleOVAL, + Name: "Oracle Linux OVAL definitions", + URL: "https://linux.oracle.com/security/oval/", + }, + }, + }, + }, + { + name: "with fips", + fixtures: []string{ + "testdata/fixtures/oracle7.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "7", + pkgs: []ftypes.Package{ + { + Name: "gnutls", + Epoch: 10, + Version: "3.6.15", + Release: "4.0.1.el8_fips", + Arch: "x86_64", + SrcEpoch: 2, + SrcName: "gnutls", + SrcVersion: "3.6.15", + SrcRelease: "4.0.1.el8_fips", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2021-20232", + PkgName: "gnutls", + InstalledVersion: "10:3.6.15-4.0.1.el8_fips", + FixedVersion: "10:3.6.16-4.0.1.el8_fips", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.OracleOVAL, + Name: "Oracle Linux OVAL definitions", + URL: "https://linux.oracle.com/security/oval/", + }, + }, + }, + }, + { + name: "malformed", + fixtures: []string{ + "testdata/fixtures/invalid-type.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "7", + pkgs: []ftypes.Package{ + { + Name: "curl", + Version: "7.29.0", + Release: "59.0.1.el7", + Arch: "x86_64", + SrcName: "curl", + SrcVersion: "7.29.0", + SrcRelease: "59.0.1.el7", + }, + }, + }, + wantErr: "failed to get Oracle Linux advisory", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := NewScanner() + got, err := s.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/oracle/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/oracle/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..996b4c870c39 --- /dev/null +++ b/pkg/detector/ospkg/oracle/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: Oracle Linux 7 + value: + ID: "oracle-oval" + Name: "Oracle Linux OVAL definitions" + URL: "https://linux.oracle.com/security/oval/" \ No newline at end of file diff --git a/pkg/detector/ospkg/oracle/testdata/fixtures/invalid-type.yaml b/pkg/detector/ospkg/oracle/testdata/fixtures/invalid-type.yaml new file mode 100644 index 000000000000..9b6ca8c40e3c --- /dev/null +++ b/pkg/detector/ospkg/oracle/testdata/fixtures/invalid-type.yaml @@ -0,0 +1,9 @@ +- bucket: Oracle Linux 7 + pairs: + - bucket: curl + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/oracle/testdata/fixtures/oracle7.yaml b/pkg/detector/ospkg/oracle/testdata/fixtures/oracle7.yaml new file mode 100644 index 000000000000..2b5e1e4e3c39 --- /dev/null +++ b/pkg/detector/ospkg/oracle/testdata/fixtures/oracle7.yaml @@ -0,0 +1,46 @@ +- bucket: Oracle Linux 7 + pairs: + - bucket: curl + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: "7.29.0-59.0.1.el7_9.1" + Entries: + - FixedVersion: "7.29.0-59.0.1.el7_9.1" + Arches: + - x86_64 + - aarch64 + - bucket: glibc + pairs: + - key: CVE-2017-1000364 + value: + FixedVersion: "2:2.17-157.ksplice1.el7_3.4" + Entries: + - FixedVersion: "2:2.17-157.ksplice1.el7_3.4" + Arches: + - x86_64 + - aarch64 + - bucket: gnutls + pairs: + - key: CVE-2021-20232 + value: + FixedVersion: "3.6.16-4.el8" + Entries: + - FixedVersion: "10:3.6.16-4.0.1.el8_fips" + Arches: + - x86_64 + - FixedVersion: "3.6.16-4.el8" + Arches: + - x86_64 + - bucket: rsyslog + pairs: + - key: CVE-2022-24903 + value: + FixedVersion: "8.24.0-57.0.1.el7_9.3" + Entries: + - FixedVersion: "8.24.0-57.0.1.el7_9.3" + Arches: + - x86_64 + - FixedVersion: "8.24.0-57.0.4.el7_9.3" + Arches: + - aarch64 diff --git a/pkg/detector/ospkg/photon/photon.go b/pkg/detector/ospkg/photon/photon.go new file mode 100644 index 000000000000..ec8e6225a7c8 --- /dev/null +++ b/pkg/detector/ospkg/photon/photon.go @@ -0,0 +1,84 @@ +package photon + +import ( + "context" + "time" + + version "github.com/knqyf263/go-rpm-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/photon" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + eolDates = map[string]time.Time{ + "1.0": time.Date(2022, 2, 28, 23, 59, 59, 0, time.UTC), + "2.0": time.Date(2022, 12, 31, 23, 59, 59, 0, time.UTC), + "3.0": time.Date(2024, 3, 31, 23, 59, 59, 0, time.UTC), + "4.0": time.Date(2026, 3, 31, 23, 59, 59, 0, time.UTC), + // The following version don't have the EOL dates yet. + // https://blogs.vmware.com/cloud-foundation/2023/05/02/announcing-photon-os-5-0-general-availability/ + "5.0": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC), + } +) + +// Scanner implements the Photon scanner +type Scanner struct { + vs photon.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{ + vs: photon.NewVulnSrc(), + } +} + +// Detect scans and returns vulnerabilities using photon scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + advisories, err := s.vs.Get(db.GetParams{ + Release: osVer, + PkgName: pkg.SrcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get Photon Linux advisory: %w", err) + } + + installed := utils.FormatVersion(pkg) + installedVersion := version.NewVersion(installed) + for _, adv := range advisories { + fixedVersion := version.NewVersion(adv.FixedVersion) + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: installed, + PkgIdentifier: pkg.Identifier, + Layer: pkg.Layer, + Custom: adv.Custom, + DataSource: adv.DataSource, + } + if installedVersion.LessThan(fixedVersion) { + vuln.FixedVersion = adv.FixedVersion + vulns = append(vulns, vuln) + } + } + } + return vulns, nil +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + return osver.Supported(ctx, eolDates, osFamily, osVer) +} diff --git a/pkg/detector/ospkg/photon/photon_test.go b/pkg/detector/ospkg/photon/photon_test.go new file mode 100644 index 000000000000..d5a6ad7bbea1 --- /dev/null +++ b/pkg/detector/ospkg/photon/photon_test.go @@ -0,0 +1,155 @@ +package photon_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/photon" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/photon.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "1.0", + pkgs: []ftypes.Package{ + { + Name: "PyYAML", + Version: "3.12", + Release: "4.ph1", + SrcName: "PyYAML", + SrcVersion: "3.12", + SrcRelease: "4.ph1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "PyYAML", + VulnerabilityID: "CVE-2020-1747", + InstalledVersion: "3.12-4.ph1", + FixedVersion: "3.12-5.ph1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Photon, + Name: "Photon OS CVE metadata", + URL: "https://packages.broadcom.com/photon/photon_cve_metadata/", + }, + }, + }, + }, + { + name: "invalid bucket", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "1.0", + pkgs: []ftypes.Package{ + { + Name: "PyYAML", + Version: "3.12", + SrcName: "PyYAML", + SrcVersion: "3.12", + }, + }, + }, + wantErr: "failed to get Photon Linux advisory", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := photon.NewScanner() + got, err := s.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsSupportedVersion(t *testing.T) { + type args struct { + osFamily ftypes.OSType + osVer string + } + tests := []struct { + name string + now time.Time + args args + want bool + }{ + { + name: "photon 1.0", + now: time.Date(2022, 1, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "photon", + osVer: "1.0", + }, + want: true, + }, + { + name: "photon 1.0 EOL", + now: time.Date(2022, 3, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "photon", + osVer: "1.0", + }, + want: false, + }, + { + name: "latest", + now: time.Date(2022, 1, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "photon", + osVer: "999.0", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), tt.now) + s := photon.NewScanner() + got := s.IsSupportedVersion(ctx, tt.args.osFamily, tt.args.osVer) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/photon/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/photon/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..fe7b27cdff10 --- /dev/null +++ b/pkg/detector/ospkg/photon/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: Photon OS 1.0 + value: + ID: "photon" + Name: "Photon OS CVE metadata" + URL: "https://packages.broadcom.com/photon/photon_cve_metadata/" \ No newline at end of file diff --git a/pkg/detector/ospkg/photon/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/photon/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..43af3dde0b04 --- /dev/null +++ b/pkg/detector/ospkg/photon/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: Photon OS 1.0 + pairs: + - bucket: PyYAML + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar \ No newline at end of file diff --git a/pkg/detector/ospkg/photon/testdata/fixtures/photon.yaml b/pkg/detector/ospkg/photon/testdata/fixtures/photon.yaml new file mode 100644 index 000000000000..665dd8331965 --- /dev/null +++ b/pkg/detector/ospkg/photon/testdata/fixtures/photon.yaml @@ -0,0 +1,10 @@ +- bucket: Photon OS 1.0 + pairs: + - bucket: PyYAML + pairs: + - key: CVE-2017-18342 + value: + FixedVersion: "3.12-3.ph1" + - key: CVE-2020-1747 + value: + FixedVersion: "3.12-5.ph1" diff --git a/pkg/detector/ospkg/redhat/redhat.go b/pkg/detector/ospkg/redhat/redhat.go new file mode 100644 index 000000000000..8f349f445ec5 --- /dev/null +++ b/pkg/detector/ospkg/redhat/redhat.go @@ -0,0 +1,207 @@ +package redhat + +import ( + "context" + "fmt" + "regexp" + "slices" + "sort" + "time" + + version "github.com/knqyf263/go-rpm-version" + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + redhat "github.com/aquasecurity/trivy-db/pkg/vulnsrc/redhat-oval" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +var ( + defaultContentSets = map[string][]string{ + "6": { + "rhel-6-server-rpms", + "rhel-6-server-extras-rpms", + }, + "7": { + "rhel-7-server-rpms", + "rhel-7-server-extras-rpms", + }, + "8": { + "rhel-8-for-x86_64-baseos-rpms", + "rhel-8-for-x86_64-appstream-rpms", + }, + "9": { + "rhel-9-for-x86_64-baseos-rpms", + "rhel-9-for-x86_64-appstream-rpms", + }, + } + redhatEOLDates = map[string]time.Time{ + "4": time.Date(2017, 5, 31, 23, 59, 59, 0, time.UTC), + "5": time.Date(2020, 11, 30, 23, 59, 59, 0, time.UTC), + "6": time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC), + // N/A + "7": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC), + "8": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC), + "9": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC), + "10": time.Date(3000, 1, 1, 23, 59, 59, 0, time.UTC), + } + centosEOLDates = map[string]time.Time{ + "3": time.Date(2010, 10, 31, 23, 59, 59, 0, time.UTC), + "4": time.Date(2012, 2, 29, 23, 59, 59, 0, time.UTC), + "5": time.Date(2017, 3, 31, 23, 59, 59, 0, time.UTC), + "6": time.Date(2020, 11, 30, 23, 59, 59, 0, time.UTC), + "7": time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC), + "8": time.Date(2021, 12, 31, 23, 59, 59, 0, time.UTC), + } +) + +// Scanner implements the RedHat scanner +type Scanner struct { + vs redhat.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{ + vs: redhat.NewVulnSrc(), + } +} + +// Detect scans and returns redhat vulnerabilities +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + osVer = osver.Major(osVer) + log.InfoContext(ctx, "Detecting RHEL/CentOS vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + detectedVulns, err := s.detect(osVer, pkg) + if err != nil { + return nil, xerrors.Errorf("redhat vulnerability detection error: %w", err) + } + vulns = append(vulns, detectedVulns...) + } + return vulns, nil +} + +func (s *Scanner) detect(osVer string, pkg ftypes.Package) ([]types.DetectedVulnerability, error) { + // For Red Hat OVAL v2 containing only binary package names + pkgName := addModularNamespace(pkg.Name, pkg.Modularitylabel) + + var contentSets []string + var nvr string + if pkg.BuildInfo == nil { + contentSets = defaultContentSets[osVer] + } else { + contentSets = pkg.BuildInfo.ContentSets + nvr = fmt.Sprintf("%s-%s", pkg.BuildInfo.Nvr, pkg.BuildInfo.Arch) + } + + // Clean content sets from generic suffixes (__8, __9, etc.) + contentSets = cleanContentSets(contentSets) + + advisories, err := s.vs.Get(pkgName, contentSets, []string{nvr}) + if err != nil { + return nil, xerrors.Errorf("failed to get Red Hat advisories: %w", err) + } + + // Choose the latest fixed version for each CVE-ID (empty for unpatched vulns). + // Take the single RHSA-ID with the latest fixed version (for patched vulns). + uniqAdvisories := make(map[string]dbTypes.Advisory) + for _, adv := range advisories { + // If Arches for advisory are empty or pkg.Arch is "noarch", then any Arches are affected + if len(adv.Arches) != 0 && pkg.Arch != "noarch" { + if !slices.Contains(adv.Arches, pkg.Arch) { + continue + } + } + + if a, ok := uniqAdvisories[adv.VulnerabilityID]; ok { + if version.NewVersion(a.FixedVersion).LessThan(version.NewVersion(adv.FixedVersion)) { + uniqAdvisories[adv.VulnerabilityID] = adv + } + } else { + uniqAdvisories[adv.VulnerabilityID] = adv + } + } + + var vulns []types.DetectedVulnerability + for _, adv := range uniqAdvisories { + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + VendorIDs: adv.VendorIDs, // Will be empty for unpatched vulnerabilities + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: utils.FormatVersion(pkg), + FixedVersion: version.NewVersion(adv.FixedVersion).String(), // Will be empty for unpatched vulnerabilities + PkgIdentifier: pkg.Identifier, + Status: adv.Status, + Layer: pkg.Layer, + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: adv.Severity.String(), + }, + Custom: adv.Custom, + } + + // Keep unpatched and affected vulnerabilities + if adv.FixedVersion == "" || version.NewVersion(vuln.InstalledVersion).LessThan(version.NewVersion(adv.FixedVersion)) { + vulns = append(vulns, vuln) + } + } + + sort.Slice(vulns, func(i, j int) bool { + return vulns[i].VulnerabilityID < vulns[j].VulnerabilityID + }) + return vulns, nil +} + +// IsSupportedVersion checks is OSFamily can be scanned with Redhat scanner +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + osVer = osver.Major(osVer) + if osFamily == ftypes.CentOS { + return osver.Supported(ctx, centosEOLDates, osFamily, osVer) + } + + return osver.Supported(ctx, redhatEOLDates, osFamily, osVer) +} + +func addModularNamespace(name, label string) string { + // e.g. npm, nodejs:12:8030020201124152102:229f0a1c => nodejs:12::npm + var count int + for i, r := range label { + if r == ':' { + count++ + } + if count == 2 { + return label[:i] + "::" + name + } + } + return name +} + +// Match generic version suffixes like "__8", "__9", "__10", but preserve EUS suffixes like "__9_DOT_2". +// Examples: +// - Matches: "repo__8", "repo__10" +// - Does not match: "repo__9_DOT_2", "repo__10_DOT_1" +var genericSuffixPattern = regexp.MustCompile(`__\d+$`) + +// cleanContentSets removes generic suffixes like "__8" from content sets. +// These are Red Hat image build artifacts and not valid repository names. +// Examples: +// +// Input: []string{"repo__8", "repo__9_DOT_2", "repo__10"} +// Output: []string{"repo", "repo__9_DOT_2", "repo"} +// +// cf. https://github.com/aquasecurity/trivy-db/issues/435 +func cleanContentSets(contentSets []string) []string { + return xslices.Map(contentSets, func(cs string) string { + return genericSuffixPattern.ReplaceAllString(cs, "") + }) +} diff --git a/pkg/detector/ospkg/redhat/redhat_test.go b/pkg/detector/ospkg/redhat/redhat_test.go new file mode 100644 index 000000000000..3f4e97d8974e --- /dev/null +++ b/pkg/detector/ospkg/redhat/redhat_test.go @@ -0,0 +1,534 @@ +package redhat_test + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/redhat" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, false) + os.Exit(m.Run()) +} + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + fixtures []string + args args + want []types.DetectedVulnerability + wantErr bool + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/redhat.yaml", + "testdata/fixtures/cpe.yaml", + }, + args: args{ + osVer: "7.6", + pkgs: []ftypes.Package{ + { + Name: "vim-minimal", + Version: "7.4.160", + Release: "5.el7", + Epoch: 2, + Arch: "x86_64", + SrcName: "vim", + SrcVersion: "7.4.160", + SrcRelease: "5.el7", + SrcEpoch: 2, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + BuildInfo: &ftypes.BuildInfo{ + ContentSets: []string{"rhel-7-server-rpms"}, + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2017-5953", + PkgName: "vim-minimal", + InstalledVersion: "2:7.4.160-5.el7", + Status: dbTypes.StatusWillNotFix, + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + VulnerabilityID: "CVE-2019-12735", + VendorIDs: []string{ + "RHSA-2019:1619", + }, + PkgName: "vim-minimal", + InstalledVersion: "2:7.4.160-5.el7", + FixedVersion: "2:7.4.160-6.el7_6", + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + { + name: "happy path: multiple RHSA-IDs", + fixtures: []string{ + "testdata/fixtures/redhat.yaml", + "testdata/fixtures/cpe.yaml", + }, + args: args{ + osVer: "7.5", + pkgs: []ftypes.Package{ + { + Name: "nss", + Version: "3.36.0", + Release: "7.1.el7_6", + Epoch: 0, + Arch: "x86_64", + SrcName: "nss", + SrcVersion: "3.36.0", + SrcRelease: "7.4.160", + SrcEpoch: 0, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + BuildInfo: &ftypes.BuildInfo{ + ContentSets: []string{"rhel-7-server-rpms"}, + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-17007", + VendorIDs: []string{ + "RHSA-2021:0876", + }, + PkgName: "nss", + InstalledVersion: "3.36.0-7.1.el7_6", + FixedVersion: "3.36.0-9.el7_6", + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityMedium.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + VulnerabilityID: "CVE-2020-12403", + VendorIDs: []string{ + "RHSA-2021:0538", + }, + PkgName: "nss", + InstalledVersion: "3.36.0-7.1.el7_6", + FixedVersion: "3.53.1-17.el7_3", + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + { + name: "happy path: CVE-ID and RHSA-ID for same vulnerability", + fixtures: []string{ + "testdata/fixtures/redhat.yaml", + "testdata/fixtures/cpe.yaml", + }, + args: args{ + osVer: "8.3", + pkgs: []ftypes.Package{ + { + Name: "expat", + Version: "2.2.5", + Release: "16.el8_10", + Epoch: 0, + Arch: "x86_64", + SrcName: "expat", + SrcVersion: "2.2.5", + SrcRelease: "16.el8_10", + SrcEpoch: 0, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + BuildInfo: &ftypes.BuildInfo{ + ContentSets: []string{"rhel-8-for-x86_64-baseos-rpms"}, + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2024-45490", + VendorIDs: []string{ + "RHSA-2024:6989-3", + }, + PkgName: "expat", + InstalledVersion: "2.2.5-16.el8_10", + FixedVersion: "2.2.5-18.el8_10", + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityMedium.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + { + name: "happy path: package without architecture", + fixtures: []string{ + "testdata/fixtures/redhat.yaml", + "testdata/fixtures/cpe.yaml", + }, + args: args{ + osVer: "7.6", + pkgs: []ftypes.Package{ + { + Name: "kernel-headers", + Version: "3.10.0-1127.19", + Release: "1.el7", + Epoch: 0, + Arch: "noarch", + SrcName: "kernel-headers", + SrcVersion: "3.10.0-1127.19", + SrcRelease: "1.el7", + SrcEpoch: 0, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + BuildInfo: &ftypes.BuildInfo{ + ContentSets: []string{"rhel-7-server-rpms"}, + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2016-5195", + VendorIDs: []string{ + "RHSA-2017:0372", + }, + PkgName: "kernel-headers", + InstalledVersion: "3.10.0-1127.19-1.el7", + FixedVersion: "4.5.0-15.2.1.el7", + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + { + name: "happy path: advisories have different arches", + fixtures: []string{ + "testdata/fixtures/redhat.yaml", + "testdata/fixtures/cpe.yaml", + }, + args: args{ + osVer: "7.6", + pkgs: []ftypes.Package{ + { + Name: "kernel-headers", + Version: "3.10.0-326.36", + Release: "3.el7", + Epoch: 0, + Arch: "x86_64", + SrcName: "kernel-headers", + SrcVersion: "3.10.0-326.36", + SrcRelease: "3.el7", + SrcEpoch: 0, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + BuildInfo: &ftypes.BuildInfo{ + ContentSets: []string{"rhel-7-server-rpms"}, + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2016-5195", + VendorIDs: []string{ + "RHSA-2016:2098", + }, + PkgName: "kernel-headers", + InstalledVersion: "3.10.0-326.36-3.el7", + FixedVersion: "3.10.0-327.36.3.el7", + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + { + name: "no build info", + fixtures: []string{ + "testdata/fixtures/redhat.yaml", + "testdata/fixtures/cpe.yaml", + }, + args: args{ + osVer: "8.3", + pkgs: []ftypes.Package{ + { + Name: "vim-minimal", + Version: "7.4.160", + Release: "5.el8", + Epoch: 2, + Arch: "x86_64", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-12735", + VendorIDs: []string{ + "RHSA-2019:1619", + }, + PkgName: "vim-minimal", + InstalledVersion: "2:7.4.160-5.el8", + FixedVersion: "2:7.4.160-7.el8_7", + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityMedium.String(), + }, + }, + }, + }, + { + name: "modular packages", + fixtures: []string{ + "testdata/fixtures/redhat.yaml", + "testdata/fixtures/cpe.yaml", + }, + args: args{ + osVer: "8.3", + pkgs: []ftypes.Package{ + { + Name: "php", + Version: "7.2.10", + Release: "1.module_el8.2.0+313+b04d0a66", + Arch: "x86_64", + SrcName: "php", + SrcVersion: "7.2.10", + SrcRelease: "1.module_el8.2.0+313+b04d0a66", + Modularitylabel: "php:7.2:8020020200507003613:2c7ca891", + Layer: ftypes.Layer{ + DiffID: "sha256:3e968ecc016e1b9aa19023798229bf2d25c813d1bf092533f38b056aff820524", + }, + BuildInfo: &ftypes.BuildInfo{ + Nvr: "ubi8-init-container-8.0-7", + Arch: "x86_64", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-11043", + VendorIDs: []string{ + "RHSA-2020:0322", + }, + PkgName: "php", + InstalledVersion: "7.2.10-1.module_el8.2.0+313+b04d0a66", + FixedVersion: "7.2.11-1.1.module+el8.0.0+4664+17bd8d65", + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:3e968ecc016e1b9aa19023798229bf2d25c813d1bf092533f38b056aff820524", + }, + }, + }, + }, + { + name: "broken value", + fixtures: []string{ + "testdata/fixtures/invalid-type.yaml", + "testdata/fixtures/cpe.yaml", + }, + args: args{ + osVer: "7", + pkgs: []ftypes.Package{ + { + Name: "nss", + Version: "3.36.0", + Release: "7.1.el7_6", + Arch: "x86_64", + BuildInfo: &ftypes.BuildInfo{ + ContentSets: []string{"rhel-7-server-rpms"}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "content sets with invalid suffix", + fixtures: []string{ + "testdata/fixtures/redhat.yaml", + "testdata/fixtures/cpe.yaml", + }, + args: args{ + osVer: "8.3", + pkgs: []ftypes.Package{ + { + Name: "vim-minimal", + Version: "7.4.160", + Release: "5.el8", + Epoch: 2, + Arch: "x86_64", + SrcName: "vim", + SrcVersion: "7.4.160", + SrcRelease: "5.el8", + SrcEpoch: 2, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + BuildInfo: &ftypes.BuildInfo{ + ContentSets: []string{ + "rhel-8-for-x86_64-baseos-rpms__8", + "rhel-8-for-x86_64-appstream-rpms__8", + }, + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-12735", + VendorIDs: []string{ + "RHSA-2019:1619", + }, + PkgName: "vim-minimal", + InstalledVersion: "2:7.4.160-5.el8", + FixedVersion: "2:7.4.160-7.el8_7", + SeveritySource: vulnerability.RedHat, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityMedium.String(), + }, + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dbtest.InitDB(t, tt.fixtures) + defer func() { _ = dbtest.Close() }() + + s := redhat.NewScanner() + got, err := s.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + require.Equal(t, tt.wantErr, err != nil, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsSupportedVersion(t *testing.T) { + type args struct { + osFamily ftypes.OSType + osVer string + } + tests := []struct { + name string + now time.Time + args args + want bool + }{ + { + name: "centos 6", + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "centos", + osVer: "6.8", + }, + want: true, + }, + { + name: "centos 6 EOL", + now: time.Date(2020, 12, 1, 0, 0, 0, 0, time.UTC), + args: args{ + osFamily: "centos", + osVer: "6.7", + }, + want: false, + }, + { + name: "two dots", + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "centos", + osVer: "8.0.1", + }, + want: true, + }, + { + name: "rhel 8", + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "redhat", + osVer: "8.0", + }, + want: true, + }, + { + name: "latest", + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "redhat", + osVer: "999.0", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), tt.now) + s := redhat.NewScanner() + got := s.IsSupportedVersion(ctx, tt.args.osFamily, tt.args.osVer) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/redhat/testdata/fixtures/cpe.yaml b/pkg/detector/ospkg/redhat/testdata/fixtures/cpe.yaml new file mode 100644 index 000000000000..806e6ea62523 --- /dev/null +++ b/pkg/detector/ospkg/redhat/testdata/fixtures/cpe.yaml @@ -0,0 +1,32 @@ +- bucket: Red Hat CPE + pairs: + - bucket: repository + pairs: + - key: "rhel-8-for-x86_64-baseos-rpms" + value: + - 2 + - 4 + - key: "3scale-amp-2-rpms-for-rhel-8-x86_64-debug-rpms" + value: + - 4 + - key: "rhel-7-server-rpms" + value: + - 0 + - bucket: nvr + pairs: + - key: "ubi8-init-container-8.0-7-x86_64" + value: + - 2 + - 3 + - bucket: cpe + pairs: + - key: "0" + value: "cpe:/o:redhat:enterprise_linux:7::server" + - key: "1" + value: "cpe:/o:redhat:enterprise_linux:7::client" + - key: "2" + value: "cpe:/a:redhat:enterprise_linux:8" + - key: "3" + value: "cpe:/a:redhat:enterprise_linux:8::appstream" + - key: "4" + value: "cpe:/o:redhat:enterprise_linux:8::baseos" diff --git a/pkg/detector/ospkg/redhat/testdata/fixtures/invalid-type.yaml b/pkg/detector/ospkg/redhat/testdata/fixtures/invalid-type.yaml new file mode 100644 index 000000000000..1c9ed1eeaa2f --- /dev/null +++ b/pkg/detector/ospkg/redhat/testdata/fixtures/invalid-type.yaml @@ -0,0 +1,7 @@ +- bucket: Red Hat + pairs: + - bucket: nss + pairs: + - key: RHSA-2021:0538 + value: + Entries: broken \ No newline at end of file diff --git a/pkg/detector/ospkg/redhat/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/redhat/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..095230aa7d7a --- /dev/null +++ b/pkg/detector/ospkg/redhat/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: Red Hat Enterprise Linux 6 + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/redhat/testdata/fixtures/redhat.yaml b/pkg/detector/ospkg/redhat/testdata/fixtures/redhat.yaml new file mode 100644 index 000000000000..74f6805ad052 --- /dev/null +++ b/pkg/detector/ospkg/redhat/testdata/fixtures/redhat.yaml @@ -0,0 +1,170 @@ +- bucket: Red Hat + pairs: + - bucket: vim-minimal + pairs: + - key: RHSA-2019:1619 + value: + Entries: + - FixedVersion: 2:7.4.160-6.el7_6 + Affected: + - 0 + - 1 + Cves: + - ID: CVE-2019-12735 + Severity: 3 + - FixedVersion: 2:7.4.160-7.el8_7 + Affected: + - 2 + - 3 + Cves: + - ID: CVE-2019-12735 + Severity: 2 + - key: CVE-2017-5953 + value: + Entries: + - FixedVersion: "" + Status: 5 + Affected: + - 0 + - 1 + Cves: + - Severity: 1 + - bucket: nss + pairs: + - key: RHSA-2021:0538 + value: + Entries: + - FixedVersion: 0:3.53.1-17.el7_3 + Affected: + - 0 + - 1 + Cves: + - ID: CVE-2020-12403 + Severity: 3 + - key: RHSA-2021:0876 + value: + Entries: + - FixedVersion: 0:3.36.0-9.el7_6 + Affected: + - 0 + - 1 + Cves: + - ID: CVE-2019-17007 + Severity: 2 + - ID: CVE-2020-12403 + Severity: 3 + - bucket: "php:7.2::php" + pairs: + - key: RHSA-2020:0322 + value: + Entries: + - FixedVersion: "0:7.2.11-1.1.module+el8.0.0+4664+17bd8d65" + Affected: + - 2 + - 3 + Cves: + - ID: CVE-2019-11043 + Severity: 4 + - bucket: php + pairs: + - key: CVE-2006-4023 + value: + Entries: + - FixedVersion: "" + Affected: + - 0 + - 1 + Cves: + - Severity: 1 + - bucket: kernel-headers + pairs: + - key: RHSA-2016:2098 + value: + Entries: + - FixedVersion: 0:3.10.0-327.36.3.el7 + Affected: + - 0 + - 1 + Cves: + - ID: CVE-2016-5195 + Severity: 3 + Arches: + - ppc64 + - ppc64le + - s390x + - x86_64 + - key: RHSA-2017:0372 + value: + Entries: + - FixedVersion: 0:4.5.0-15.2.1.el7 + Affected: + - 0 + - 1 + Cves: + - ID: CVE-2016-5195 + Severity: 3 + Arches: + - aarch64 + - bucket: expat + pairs: + - key: RHSA-2024:6989-2 # created for test only + value: + Entries: + - FixedVersion: 0:2.2.5-17.el8_10 + Affected: + - 4 + Arches: + - x86_64 + Cves: + - ID: CVE-2024-45490 + Severity: 2 + - key: RHSA-2024:6989-3 # created for test only + value: + Entries: + - FixedVersion: 0:2.2.5-18.el8_10 + Affected: + - 4 + Arches: + - x86_64 + Cves: + - ID: CVE-2024-45490 + Severity: 2 + - key: CVE-2024-45490 + value: + Entries: + - Affected: + - 4 + Cves: + - Severity: 2 + Status: 5 + - key: CVE-2024-45491 + value: + Entries: + - Affected: + - 4 + Cves: + - Severity: 2 + Status: 5 + - key: RHSA-2024:6989 + value: + Entries: + - FixedVersion: 0:2.2.5-15.el8_10 + Affected: + - 4 + Arches: + - x86_64 + Cves: + - ID: CVE-2024-45490 + Severity: 2 + - ID: CVE-2024-45491 + Severity: 2 + - ID: CVE-2024-45492 + Severity: 2 + - key: CVE-2024-45492 + value: + Entries: + - Affected: + - 4 + Cves: + - Severity: 2 + Status: 5 \ No newline at end of file diff --git a/pkg/detector/ospkg/rocky/rocky.go b/pkg/detector/ospkg/rocky/rocky.go new file mode 100644 index 000000000000..7cb7b3a25513 --- /dev/null +++ b/pkg/detector/ospkg/rocky/rocky.go @@ -0,0 +1,100 @@ +package rocky + +import ( + "context" + "time" + + version "github.com/knqyf263/go-rpm-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/rocky" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + eolDates = map[string]time.Time{ + // Source: + // https://endoflife.date/rocky-linux + "8": time.Date(2029, 5, 31, 23, 59, 59, 0, time.UTC), + "9": time.Date(2032, 5, 31, 23, 59, 59, 0, time.UTC), + } +) + +// Scanner implements the Rocky Linux scanner +type Scanner struct { + vs *rocky.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{ + vs: rocky.NewVulnSrc(), + } +} + +// Detect vulnerabilities in package using Rocky Linux scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + osVer = osver.Major(osVer) + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + pkgName := addModularNamespace(pkg.Name, pkg.Modularitylabel) + advisories, err := s.vs.Get(db.GetParams{ + Release: osVer, + PkgName: pkgName, + Arch: pkg.Arch, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get Rocky Linux advisories: %w", err) + } + + installed := utils.FormatVersion(pkg) + installedVersion := version.NewVersion(installed) + + for _, adv := range advisories { + fixedVersion := version.NewVersion(adv.FixedVersion) + if installedVersion.LessThan(fixedVersion) { + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: installed, + FixedVersion: fixedVersion.String(), + PkgIdentifier: pkg.Identifier, + Layer: pkg.Layer, + DataSource: adv.DataSource, + Custom: adv.Custom, + } + vulns = append(vulns, vuln) + } + } + } + + return vulns, nil +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + return osver.Supported(ctx, eolDates, osFamily, osver.Major(osVer)) +} + +func addModularNamespace(name, label string) string { + // e.g. npm, nodejs:12:8030020201124152102:229f0a1c => nodejs:12::npm + var count int + for i, r := range label { + if r == ':' { + count++ + } + if count == 2 { + return label[:i] + "::" + name + } + } + return name +} diff --git a/pkg/detector/ospkg/rocky/rocky_test.go b/pkg/detector/ospkg/rocky/rocky_test.go new file mode 100644 index 000000000000..9a4ef487f8f7 --- /dev/null +++ b/pkg/detector/ospkg/rocky/rocky_test.go @@ -0,0 +1,196 @@ +package rocky_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/rocky" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/rocky.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "8.5", + pkgs: []ftypes.Package{ + { + Name: "bpftool", + Epoch: 0, + Version: "4.18.0", + Release: "348.el8.0.3", + Arch: "aarch64", + SrcName: "kernel", + SrcEpoch: 0, + SrcVersion: "4.18.0", + SrcRelease: "348.el8.0.3", + Modularitylabel: "", + Licenses: []string{"GPLv2"}, + Layer: ftypes.Layer{}, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "bpftool", + VulnerabilityID: "CVE-2021-20317", + InstalledVersion: "4.18.0-348.el8.0.3", + FixedVersion: "5.18.0-348.2.1.el8_5", + Layer: ftypes.Layer{}, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Rocky, + Name: "Rocky Linux updateinfo", + URL: "https://download.rockylinux.org/pub/rocky/", + }, + }, + }, + }, + { + name: "modular package", + fixtures: []string{ + "testdata/fixtures/modular.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "8.5", + pkgs: []ftypes.Package{ + { + Name: "nginx", + Epoch: 1, + Version: "1.16.1", + Release: "2.module+el8.4.0+543+efbf198b.0", + Arch: "x86_64", + SrcName: "nginx", + SrcEpoch: 1, + SrcVersion: "1.16.1", + SrcRelease: "2.module+el8.4.0+543+efbf198b.0", + Modularitylabel: "nginx:1.16:8040020210610090125:9f9e2e7e", + Licenses: []string{"BSD"}, + Layer: ftypes.Layer{}, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "nginx", + VulnerabilityID: "CVE-2021-23017", + InstalledVersion: "1:1.16.1-2.module+el8.4.0+543+efbf198b.0", + FixedVersion: "1:1.16.1-2.module+el8.4.0+543+efbf198b.1", + Layer: ftypes.Layer{}, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Rocky, + Name: "Rocky Linux updateinfo", + URL: "https://download.rockylinux.org/pub/rocky/", + }, + }, + }, + }, + { + name: "Get returns an error", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "8.5", + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.5-12", + SrcName: "jq", + SrcVersion: "1.5-12", + }, + }, + }, + wantErr: "failed to get Rocky Linux advisories", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := rocky.NewScanner() + got, err := s.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsSupportedVersion(t *testing.T) { + type args struct { + osFamily ftypes.OSType + osVer string + } + tests := []struct { + name string + now time.Time + args args + want bool + }{ + { + name: "rocky 8.5", + now: time.Date(2019, 3, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "rocky", + osVer: "8.5", + }, + want: true, + }, + { + name: "rocky 8.5 with EOL", + now: time.Date(2029, 6, 1, 0, 0, 0, 0, time.UTC), + args: args{ + osFamily: "rocky", + osVer: "8.5", + }, + want: false, + }, + { + name: "latest", + now: time.Date(2019, 5, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "rocky", + osVer: "999.0", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), tt.now) + s := rocky.NewScanner() + got := s.IsSupportedVersion(ctx, tt.args.osFamily, tt.args.osVer) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/rocky/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/rocky/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..67713f60e158 --- /dev/null +++ b/pkg/detector/ospkg/rocky/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: rocky 8 + value: + ID: "rocky" + Name: "Rocky Linux updateinfo" + URL: "https://download.rockylinux.org/pub/rocky/" \ No newline at end of file diff --git a/pkg/detector/ospkg/rocky/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/rocky/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..883c443e8520 --- /dev/null +++ b/pkg/detector/ospkg/rocky/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: rocky 8 + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar \ No newline at end of file diff --git a/pkg/detector/ospkg/rocky/testdata/fixtures/modular.yaml b/pkg/detector/ospkg/rocky/testdata/fixtures/modular.yaml new file mode 100644 index 000000000000..3429a8dc11fc --- /dev/null +++ b/pkg/detector/ospkg/rocky/testdata/fixtures/modular.yaml @@ -0,0 +1,13 @@ +- bucket: rocky 8 + pairs: + - bucket: "nginx:1.16::nginx" + pairs: + - key: CVE-2021-23017 + value: + FixedVersion: "1:1.16.1-2.module+el8.4.0+543+efbf198b.1" + Entries: + - FixedVersion: "1:1.16.1-2.module+el8.4.0+543+efbf198b.1" + Arches: + - "x86_64" + VendorIDs: + - "RLSA-2021:2258" diff --git a/pkg/detector/ospkg/rocky/testdata/fixtures/rocky.yaml b/pkg/detector/ospkg/rocky/testdata/fixtures/rocky.yaml new file mode 100644 index 000000000000..3956f0a1632d --- /dev/null +++ b/pkg/detector/ospkg/rocky/testdata/fixtures/rocky.yaml @@ -0,0 +1,18 @@ +- bucket: rocky 8 + pairs: + - bucket: bpftool + pairs: + - key: CVE-2021-20317 + value: + FixedVersion: "4.18.0-348.2.1.el8_5" + Entries: + - FixedVersion: "4.18.0-348.2.1.el8_5" + Arches: + - "x86_64" + VendorIDs: + - "RLSA-2021:4647" + - FixedVersion: "5.18.0-348.2.1.el8_5" + Arches: + - "aarch64" + VendorIDs: + - "RLSA-2021:4647" \ No newline at end of file diff --git a/pkg/detector/ospkg/rootio/export_test.go b/pkg/detector/ospkg/rootio/export_test.go new file mode 100644 index 000000000000..1f17873698de --- /dev/null +++ b/pkg/detector/ospkg/rootio/export_test.go @@ -0,0 +1,14 @@ +package rootio + +import ( + "context" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" +) + +// Bridge to expose rootio scanner internals to tests in the rootio_test package. + +// IsVulnerable exports isVulnerable for testing. +func (s *Scanner) IsVulnerable(ctx context.Context, installedVersion string, adv dbTypes.Advisory) bool { + return s.isVulnerable(ctx, installedVersion, adv) +} diff --git a/pkg/detector/ospkg/rootio/provider.go b/pkg/detector/ospkg/rootio/provider.go new file mode 100644 index 000000000000..cda02142aca2 --- /dev/null +++ b/pkg/detector/ospkg/rootio/provider.go @@ -0,0 +1,46 @@ +package rootio + +import ( + "regexp" + + "github.com/aquasecurity/trivy/pkg/detector/ospkg/driver" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/scan/utils" +) + +var ( + // debianRootIOPattern matches Debian/Ubuntu Root.io version pattern: .root.io + debianRootIOPattern = regexp.MustCompile(`\.root\.io`) + // alpineRootIOPattern matches Alpine Root.io version pattern: -r\d007\d (e.g., -r10071, -r20072) + alpineRootIOPattern = regexp.MustCompile(`-r\d007\d`) +) + +// Provider creates a Root.io driver if Root.io packages are detected +func Provider(osFamily ftypes.OSType, pkgs []ftypes.Package) driver.Driver { + if !isRootIOEnvironment(osFamily, pkgs) { + return nil + } + return NewScanner(osFamily) +} + +// isRootIOEnvironment detects if the environment is Root.io based on package suffixes +func isRootIOEnvironment(osFamily ftypes.OSType, pkgs []ftypes.Package) bool { + switch osFamily { + case ftypes.Debian, ftypes.Ubuntu: + return hasPackageWithPattern(pkgs, debianRootIOPattern) + case ftypes.Alpine: + return hasPackageWithPattern(pkgs, alpineRootIOPattern) + default: + return false + } +} + +// hasPackageWithPattern checks if any package version matches the specified pattern +func hasPackageWithPattern(pkgs []ftypes.Package, pattern *regexp.Regexp) bool { + for _, pkg := range pkgs { + if pattern.MatchString(utils.FormatVersion(pkg)) { + return true + } + } + return false +} diff --git a/pkg/detector/ospkg/rootio/provider_test.go b/pkg/detector/ospkg/rootio/provider_test.go new file mode 100644 index 000000000000..7aa47b32cbbb --- /dev/null +++ b/pkg/detector/ospkg/rootio/provider_test.go @@ -0,0 +1,118 @@ +package rootio_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestProvider(t *testing.T) { + tests := []struct { + name string + osFamily ftypes.OSType + pkgs []ftypes.Package + want bool // true if driver should be returned, false if nil + }{ + { + name: "Debian with .root.io package", + osFamily: ftypes.Debian, + pkgs: []ftypes.Package{ + {Name: "libc6", Version: "2.31", Release: "13+deb11u4.root.io"}, + {Name: "bash", Version: "5.1-2+deb11u1"}, + }, + want: true, + }, + { + name: "Ubuntu with .root.io package", + osFamily: ftypes.Ubuntu, + pkgs: []ftypes.Package{ + {Name: "libc6", Version: "2.31-0ubuntu9.9.root.io"}, + {Name: "bash", Version: "5.1-6ubuntu1"}, + }, + want: true, + }, + { + name: "Alpine with Root.io pattern package", + osFamily: ftypes.Alpine, + pkgs: []ftypes.Package{ + {Name: "musl", Version: "1.2.3-r10071"}, + {Name: "busybox", Version: "1.35.0-r17"}, + }, + want: true, + }, + { + name: "Debian without .root.io package", + osFamily: ftypes.Debian, + pkgs: []ftypes.Package{ + {Name: "libc6", Version: "2.31-13+deb11u4"}, + {Name: "bash", Version: "5.1-2+deb11u1"}, + }, + want: false, + }, + { + name: "Ubuntu without .root.io package", + osFamily: ftypes.Ubuntu, + pkgs: []ftypes.Package{ + {Name: "libc6", Version: "2.31-0ubuntu9.9"}, + {Name: "bash", Version: "5.1-6ubuntu1"}, + }, + want: false, + }, + { + name: "Alpine without Root.io pattern package", + osFamily: ftypes.Alpine, + pkgs: []ftypes.Package{ + {Name: "musl", Version: "1.2.3-r0"}, + {Name: "busybox", Version: "1.35.0-r17"}, + }, + want: false, + }, + { + name: "Unsupported OS family", + osFamily: ftypes.RedHat, + pkgs: []ftypes.Package{ + {Name: "glibc", Version: "2.28-151.el8.root.io"}, + }, + want: false, + }, + { + name: "Empty package list", + osFamily: ftypes.Debian, + pkgs: []ftypes.Package{}, + want: false, + }, + { + name: "Multiple .root.io packages", + osFamily: ftypes.Debian, + pkgs: []ftypes.Package{ + {Name: "libc6", Version: "2.31-13+deb11u4.root.io"}, + {Name: "openssl", Version: "1.1.1n-0+deb11u3.root.io"}, + }, + want: true, + }, + { + name: "Multiple Alpine Root.io pattern packages", + osFamily: ftypes.Alpine, + pkgs: []ftypes.Package{ + {Name: "musl", Version: "1.2.3-r20072"}, + {Name: "openssl", Version: "1.1.1t-r10071"}, + }, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + driver := rootio.Provider(tt.osFamily, tt.pkgs) + if tt.want { + require.NotNil(t, driver, "Provider should return a driver for Root.io environment") + } else { + assert.Nil(t, driver, "Provider should return nil for non-Root.io environment") + } + }) + } +} diff --git a/pkg/detector/ospkg/rootio/rootio.go b/pkg/detector/ospkg/rootio/rootio.go new file mode 100644 index 000000000000..3f3f9d449e20 --- /dev/null +++ b/pkg/detector/ospkg/rootio/rootio.go @@ -0,0 +1,155 @@ +package rootio + +import ( + "context" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/rootio" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Scanner implements the Root.io scanner +type Scanner struct { + comparer version.Comparer + vsg rootio.VulnSrcGetter + versionTrimmer func(string) string + logger *log.Logger +} + +// NewScanner is the factory method for Scanner +func NewScanner(baseOS ftypes.OSType) *Scanner { + var comparer version.Comparer + var vsg rootio.VulnSrcGetter + var versionTrimmer func(string) string + + switch baseOS { + case ftypes.Debian: + comparer = version.NewDEBComparer() + vsg = rootio.NewVulnSrcGetter(vulnerability.Debian) + versionTrimmer = version.Major + case ftypes.Ubuntu: + comparer = version.NewDEBComparer() + vsg = rootio.NewVulnSrcGetter(vulnerability.Ubuntu) + versionTrimmer = version.Minor + case ftypes.Alpine: + comparer = version.NewAPKComparer() + vsg = rootio.NewVulnSrcGetter(vulnerability.Alpine) + versionTrimmer = version.Minor + default: + // Should never happen as it's validated in the provider + comparer = version.NewDEBComparer() + vsg = rootio.NewVulnSrcGetter(vulnerability.Debian) + versionTrimmer = version.Major + } + + return &Scanner{ + comparer: comparer, + vsg: vsg, + versionTrimmer: versionTrimmer, + logger: log.WithPrefix("rootio"), + } +} + +// Detect vulnerabilities in package using Root.io scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + // Trim patch/minor part of osVer. + // e.g. "12.0.1" -> "12" (Debian), "24.04.1" -> "24.04" (Ubuntu), "3.17.2" -> "3.17" (Alpine) + osVer = s.versionTrimmer(osVer) + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + srcName := pkg.SrcName + if srcName == "" { + srcName = pkg.Name + } + + advisories, err := s.vsg.Get(db.GetParams{ + Release: osVer, + PkgName: srcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get Root.io advisories: %w", err) + } + + for _, adv := range advisories { + if !s.isVulnerable(ctx, utils.FormatSrcVersion(pkg), adv) { + continue + } + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: utils.FormatVersion(pkg), + FixedVersion: strings.Join(adv.PatchedVersions, ", "), + Layer: pkg.Layer, + PkgIdentifier: pkg.Identifier, + Custom: adv.Custom, + DataSource: adv.DataSource, + } + + // We add the severity from the base OS, so we need to keep the severity level from the base OS (as SeveritySource). + if adv.Severity != dbTypes.SeverityUnknown { + vuln.Vulnerability = dbTypes.Vulnerability{ + Severity: adv.Severity.String(), + } + + vuln.SeveritySource = adv.DataSource.ID + } + + vulns = append(vulns, vuln) + } + } + return vulns, nil +} + +func (s *Scanner) isVulnerable(ctx context.Context, installedVersion string, adv dbTypes.Advisory) bool { + // Handle unfixed vulnerabilities + if len(adv.VulnerableVersions) == 0 { + // If no vulnerable versions are specified, it means the package is always vulnerable + return true + } + + // For fixed vulnerabilities, check if installed version satisfies the constraint + return s.checkConstraints(ctx, installedVersion, adv.VulnerableVersions) +} + +func (s *Scanner) checkConstraints(ctx context.Context, installedVersion string, constraintsStr []string) bool { + if installedVersion == "" { + return false + } + + for _, constraintStr := range constraintsStr { + constraints, err := version.NewConstraints(constraintStr, s.comparer) + if err != nil { + s.logger.DebugContext(ctx, "Failed to parse constraints", + log.String("constraints", constraintStr), log.Err(err)) + return false + } + + if satisfied, err := constraints.Check(installedVersion); err != nil { + s.logger.DebugContext(ctx, "Failed to check version constraints", + log.String("version", installedVersion), + log.String("constraints", constraintStr), log.Err(err)) + return false + } else if satisfied { + return true + } + } + return false +} + +// IsSupportedVersion checks if the version is supported. +// Root.io creates fixes for EOL distributions, so we assume all versions are supported. +func (s *Scanner) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool { + return true +} diff --git a/pkg/detector/ospkg/rootio/rootio_test.go b/pkg/detector/ospkg/rootio/rootio_test.go new file mode 100644 index 000000000000..e56deca61893 --- /dev/null +++ b/pkg/detector/ospkg/rootio/rootio_test.go @@ -0,0 +1,322 @@ +package rootio_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/rootio" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + baseOS ftypes.OSType + fixtures []string + args args + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "Debian scanner", + baseOS: ftypes.Debian, + fixtures: []string{ + "testdata/fixtures/rootio.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "12", + pkgs: []ftypes.Package{ + { + Name: "openssl", + Version: "3.0.15-1~deb12u1.root.io.0", + SrcName: "openssl", + SrcVersion: "3.0.15-1~deb12u1.root.io.0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "openssl", + VulnerabilityID: "CVE-2024-13176", // Debian and Root.io contain this CVE + InstalledVersion: "3.0.15-1~deb12u1.root.io.0", + FixedVersion: "3.0.15-1~deb12u1.root.io.1, 3.0.16-1~deb12u1", + SeveritySource: vulnerability.RootIO, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RootIO, + BaseID: vulnerability.Debian, + Name: "Root.io Security Patches (debian)", + URL: "https://api.root.io/external/patch_feed", + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityMedium.String(), + }, + }, + { + PkgName: "openssl", + VulnerabilityID: "CVE-2025-27587", // Debian only contains this CVE + InstalledVersion: "3.0.15-1~deb12u1.root.io.0", + FixedVersion: "3.0.16-1~deb12u1", + SeveritySource: vulnerability.Debian, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Debian, + Name: "Debian Security Tracker", + URL: "https://salsa.debian.org/security-tracker-team/security-tracker", + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, + }, + }, + { + name: "Ubuntu scanner", + baseOS: ftypes.Ubuntu, + fixtures: []string{ + "testdata/fixtures/rootio.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "20.04", + pkgs: []ftypes.Package{ + { + Name: "nginx", + Version: "1.22.1-9+deb12u2.root.io.0", + SrcName: "nginx", + SrcVersion: "1.22.1-9+deb12u2.root.io.0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "nginx", + VulnerabilityID: "CVE-2023-44487", + InstalledVersion: "1.22.1-9+deb12u2.root.io.0", + FixedVersion: "1.22.1-9+deb12u2.root.io.1", + SeveritySource: vulnerability.RootIO, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RootIO, + BaseID: vulnerability.Ubuntu, + Name: "Root.io Security Patches (ubuntu)", + URL: "https://api.root.io/external/patch_feed", + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + }, + }, + { + name: "Alpine scanner", + baseOS: ftypes.Alpine, + fixtures: []string{ + "testdata/fixtures/rootio.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.19.3", + pkgs: []ftypes.Package{ + { + Name: "less", + Version: "643-r00072", + SrcName: "less", + SrcVersion: "643-r00072", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "less", + VulnerabilityID: "CVE-2024-32487", + InstalledVersion: "643-r00072", + FixedVersion: "643-r10072", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RootIO, + BaseID: vulnerability.Alpine, + Name: "Root.io Security Patches (alpine)", + URL: "https://api.root.io/external/patch_feed", + }, + }, + }, + }, + { + name: "Get returns an error", + baseOS: ftypes.Alpine, + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.20", + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.5-12", + SrcName: "jq", + SrcVersion: "1.5-12", + }, + }, + }, + wantErr: "failed to get Root.io advisories", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + scanner := rootio.NewScanner(tt.baseOS) + got, err := scanner.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsVulnerable(t *testing.T) { + tests := []struct { + name string + installedVersion string + vulnerableRanges []string + want bool + }{ + { + name: "Installed vulnerable vendor version. There is no fix", + installedVersion: "1.0.0", + vulnerableRanges: []string{}, + want: true, + }, + { + name: "Installed vulnerable vendor version, fix by vendor", + installedVersion: "1.0.0", + vulnerableRanges: []string{ + "<1.0.0-2", + }, + want: true, + }, + { + name: "Installed non-vulnerable vendor version, fix by vendor", + installedVersion: "1.0.0-2", + vulnerableRanges: []string{ + "<1.0.0-2", + }, + want: false, + }, + { + name: "Installed vulnerable vendor version, fix by root.io (root.io version)", + installedVersion: "1.0.0-2", + vulnerableRanges: []string{ + "<1.0.0-2.root.io", + }, + want: true, + }, + { + name: "Installed non-vulnerable vendor version, fix by root.io (root.io version)", + installedVersion: "1.0.0-3", + vulnerableRanges: []string{ + "<1.0.0-2.root.io", + }, + want: false, + }, + { + name: "Installed vulnerable vendor version, fix by root.io (root.io + vendor versions)", + installedVersion: "1.0.0-2", + vulnerableRanges: []string{ + "<1.0.0-2.root.io", + ">=1.0.0-2 <1.0.0-3", + }, + want: true, + }, + { + name: "Installed non-vulnerable vendor version, fix by root.io (root.io + vendor versions)", + installedVersion: "1.0.0-3", + vulnerableRanges: []string{ + "<1.0.0-2.root.io", + ">=1.0.0-2 <1.0.0-3", + }, + want: false, + }, + { + name: "Installed vulnerable root.io version, fix by root.io", + installedVersion: "1.0.0-1.root.io", + vulnerableRanges: []string{ + "<1.0.0-2.root.io", + }, + want: true, + }, + { + name: "Installed non-vulnerable root.io version, fix by root.io", + installedVersion: "1.0.0-2.root.io", + vulnerableRanges: []string{ + "<1.0.0-2.root.io", + }, + want: false, + }, + { + name: "Installed vulnerable root.io version, fix by vendor", + installedVersion: "1.0.0-1.root.io", + vulnerableRanges: []string{ + "<1.0.0-2", + }, + want: true, + }, + { + name: "Installed non-vulnerable root.io version, fix by vendor", + installedVersion: "1.0.0-2.root.io", + vulnerableRanges: []string{ + "<1.0.0-1", + }, + want: false, + }, + { + name: "Installed vulnerable root.io version, fix by root.io (root.io + vendor versions)", + installedVersion: "1.0.0-1.root.io", + vulnerableRanges: []string{ + "<1.0.0-2.root.io", + ">=1.0.0-2 <1.0.0-2", + }, + want: true, + }, + { + name: "Installed non-vulnerable root.io version, fix by root.io (root.io + vendor versions)", + installedVersion: "1.0.0-2.root.io", + vulnerableRanges: []string{ + "<1.0.0-2.root.io", + ">=1.0.0-2 <1.0.0-2", + }, + want: false, + }, + { + name: "Installed non-vulnerable root.io version, fix by root.io (root.io + root.io + vendor versions)", + installedVersion: "1.0.0-2.root.io", + vulnerableRanges: []string{ + "<1.0.0-2.root.io", + ">1.0.0-2.root.io <1.0.0-2", + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scanner := rootio.NewScanner(ftypes.Debian) + vulnerable := scanner.IsVulnerable(t.Context(), tt.installedVersion, dbTypes.Advisory{VulnerableVersions: tt.vulnerableRanges}) + require.Equal(t, tt.want, vulnerable) + }) + } +} diff --git a/pkg/detector/ospkg/rootio/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/rootio/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..e863ef4d7cbd --- /dev/null +++ b/pkg/detector/ospkg/rootio/testdata/fixtures/data-source.yaml @@ -0,0 +1,25 @@ +- bucket: data-source + pairs: + - key: debian 12 + value: + ID: "debian" + Name: "Debian Security Tracker" + URL: "https://salsa.debian.org/security-tracker-team/security-tracker" + - key: root.io debian 12 + value: + ID: "rootio" + BaseID: "debian" + Name: "Root.io Security Patches (debian)" + URL: "https://api.root.io/external/patch_feed" + - key: root.io ubuntu 20.04 + value: + ID: "rootio" + BaseID: "ubuntu" + Name: "Root.io Security Patches (ubuntu)" + URL: "https://api.root.io/external/patch_feed" + - key: root.io alpine 3.19 + value: + ID: "rootio" + BaseID: "alpine" + Name: "Root.io Security Patches (alpine)" + URL: "https://api.root.io/external/patch_feed" \ No newline at end of file diff --git a/pkg/detector/ospkg/rootio/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/rootio/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..8e30fd71ac8d --- /dev/null +++ b/pkg/detector/ospkg/rootio/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: root.io alpine 3.20 + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar \ No newline at end of file diff --git a/pkg/detector/ospkg/rootio/testdata/fixtures/rootio.yaml b/pkg/detector/ospkg/rootio/testdata/fixtures/rootio.yaml new file mode 100644 index 000000000000..13b1e2fa74da --- /dev/null +++ b/pkg/detector/ospkg/rootio/testdata/fixtures/rootio.yaml @@ -0,0 +1,52 @@ +- bucket: debian 12 + pairs: + - bucket: openssl + pairs: + - key: CVE-2024-13176 + value: + VulnerableVersions: + - "<3.0.16-1~deb12u1" + PatchedVersions: + - "3.0.16-1~deb12u1" + Severity: 2 + - key: CVE-2025-27587 + value: + VulnerableVersions: + - "<3.0.16-1~deb12u1" + PatchedVersions: + - "3.0.16-1~deb12u1" + Severity: 1 +- bucket: root.io debian 12 + pairs: + - bucket: openssl + pairs: + - key: CVE-2024-13176 + value: + VulnerableVersions: + - "<3.0.15-1~deb12u1.root.io.1" + - ">3.0.15-1~deb12u1.root.io.1 <3.0.16-1~deb12u1" + PatchedVersions: + - "3.0.15-1~deb12u1.root.io.1" + - "3.0.16-1~deb12u1" + Severity: 2 +- bucket: root.io ubuntu 20.04 + pairs: + - bucket: nginx + pairs: + - key: CVE-2023-44487 + value: + VulnerableVersions: + - "<1.22.1-9+deb12u2.root.io.1" + PatchedVersions: + - "1.22.1-9+deb12u2.root.io.1" + Severity: 3 +- bucket: root.io alpine 3.19 + pairs: + - bucket: less + pairs: + - key: CVE-2024-32487 + value: + VulnerableVersions: + - "<643-r10072" + PatchedVersions: + - "643-r10072" \ No newline at end of file diff --git a/pkg/detector/ospkg/seal/provider.go b/pkg/detector/ospkg/seal/provider.go new file mode 100644 index 000000000000..c4c0a6da4241 --- /dev/null +++ b/pkg/detector/ospkg/seal/provider.go @@ -0,0 +1,29 @@ +package seal + +import ( + "slices" + + "github.com/aquasecurity/trivy/pkg/detector/ospkg/driver" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/set" +) + +var ( + supportedOSFamilies = set.New( + ftypes.Alpine, + ftypes.CBLMariner, + ftypes.CentOS, + ftypes.RedHat, + ftypes.Debian, + ftypes.Oracle, + ftypes.Ubuntu, + ) +) + +// Provider creates a Root.io driver if Root.io packages are detected +func Provider(osFamily ftypes.OSType, pkgs []ftypes.Package) driver.Driver { + if supportedOSFamilies.Contains(osFamily) && slices.ContainsFunc(pkgs, sealPkg) { + return NewScanner(osFamily) + } + return nil +} diff --git a/pkg/detector/ospkg/seal/provider_test.go b/pkg/detector/ospkg/seal/provider_test.go new file mode 100644 index 000000000000..634a056697a6 --- /dev/null +++ b/pkg/detector/ospkg/seal/provider_test.go @@ -0,0 +1,81 @@ +package seal_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/detector/ospkg/seal" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestProvider(t *testing.T) { + tests := []struct { + name string + osFamily ftypes.OSType + pkgs []ftypes.Package + want bool // true if driver should be returned, false if nil + }{ + { + name: "returns driver when package name starts with seal", + osFamily: ftypes.Debian, + pkgs: []ftypes.Package{ + {Name: "seal-agent", Version: "1.0.0"}, + {Name: "bash", Version: "5.1"}, + }, + want: true, + }, + { + name: "returns driver when src name starts with seal", + osFamily: ftypes.Ubuntu, + pkgs: []ftypes.Package{ + {Name: "libssl", SrcName: "seal-ssl", Version: "1.2.3"}, + {Name: "curl", Version: "7.81.0"}, + }, + want: true, + }, + { + name: "returns nil when no seal packages present", + osFamily: ftypes.Alpine, + pkgs: []ftypes.Package{ + {Name: "musl", Version: "1.2.3"}, + {Name: "busybox", Version: "1.36.1"}, + }, + want: false, + }, + { + name: "returns nil for empty package list", + osFamily: ftypes.Debian, + pkgs: []ftypes.Package{}, + want: false, + }, + { + name: "case-insensitive: Seal prefix matched", + osFamily: ftypes.Ubuntu, + pkgs: []ftypes.Package{ + {Name: "Seal-agent", Version: "2.0.0"}, + }, + want: true, + }, + { + name: "returns nil for unsupported OS family even with seal package", + osFamily: ftypes.Fedora, + pkgs: []ftypes.Package{ + {Name: "seal-agent", Version: "1.0.0"}, + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := seal.Provider(tt.osFamily, tt.pkgs) + if tt.want { + require.NotNil(t, d, "expected a non-nil driver when seal package is present") + } else { + assert.Nil(t, d, "expected nil driver when no seal package is present") + } + }) + } +} diff --git a/pkg/detector/ospkg/seal/seal.go b/pkg/detector/ospkg/seal/seal.go new file mode 100644 index 000000000000..65d64f0614e5 --- /dev/null +++ b/pkg/detector/ospkg/seal/seal.go @@ -0,0 +1,203 @@ +package seal + +import ( + "cmp" + "context" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/ecosystem" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/seal" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/alpine" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/azure" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/debian" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/driver" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/oracle" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/redhat" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/ubuntu" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Scanner implements the Seal scanner +type Scanner struct { + comparer version.Comparer + scanner driver.Driver + vsg seal.VulnSrcGetter + logger *log.Logger +} + +// NewScanner is the factory method for Scanner +func NewScanner(baseOS ftypes.OSType) *Scanner { + var scanner driver.Driver + var comparer version.Comparer + var vsg seal.VulnSrcGetter + + switch baseOS { + case ftypes.Alpine: + scanner = alpine.NewScanner() + comparer = version.NewAPKComparer() + vsg = seal.NewVulnSrcGetter(ecosystem.Alpine) + case ftypes.CBLMariner: + scanner = azure.NewMarinerScanner() + comparer = version.NewRPMComparer() + vsg = seal.NewVulnSrcGetter(ecosystem.RedHat) + case ftypes.CentOS, ftypes.RedHat: + scanner = redhat.NewScanner() + comparer = version.NewRPMComparer() + vsg = seal.NewVulnSrcGetter(ecosystem.RedHat) + case ftypes.Debian: + scanner = debian.NewScanner() + comparer = version.NewDEBComparer() + vsg = seal.NewVulnSrcGetter(ecosystem.Debian) + case ftypes.Oracle: + scanner = oracle.NewScanner() + comparer = version.NewRPMComparer() + vsg = seal.NewVulnSrcGetter(ecosystem.RedHat) + case ftypes.Ubuntu: + scanner = ubuntu.NewScanner() + comparer = version.NewDEBComparer() + vsg = seal.NewVulnSrcGetter(ecosystem.Debian) + default: + // Should never happen as it's validated in the provider + scanner = debian.NewScanner() + comparer = version.NewDEBComparer() + vsg = seal.NewVulnSrcGetter(ecosystem.Debian) + } + + return &Scanner{ + scanner: scanner, + comparer: comparer, + vsg: vsg, + logger: log.WithPrefix("seal"), + } +} + +// Detect vulnerabilities in package using Seal scanner +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + var baseOSPkgs ftypes.Packages + for _, pkg := range pkgs { + // Keep non-Seal packages to scan them with the base OS scanner later + if !sealPkg(pkg) { + baseOSPkgs = append(baseOSPkgs, pkg) + continue + } + + srcName := cmp.Or(pkg.SrcName, pkg.Name) + srcRelease := lo.Ternary(pkg.SrcName != "", pkg.SrcRelease, pkg.Release) + + advisories, err := s.vsg.Get(db.GetParams{ + // Detect release version from pkg Release (`el` version for RPM-based distros, empty for other distros). + // cf. https://github.com/aquasecurity/vuln-list-update/pull/364#issuecomment-3244733993 + Release: releaseVersion(srcRelease), + PkgName: srcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get Seal advisories: %w", err) + } + + for _, adv := range advisories { + if !s.isVulnerable(ctx, utils.FormatSrcVersion(pkg), adv) { + continue + } + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: utils.FormatVersion(pkg), + FixedVersion: strings.Join(adv.PatchedVersions, ", "), + Layer: pkg.Layer, + PkgIdentifier: pkg.Identifier, + Custom: adv.Custom, + DataSource: adv.DataSource, + } + + if adv.Severity != dbTypes.SeverityUnknown { + // Package-specific severity + vuln.SeveritySource = adv.DataSource.BaseID + vuln.Vulnerability = dbTypes.Vulnerability{ + Severity: adv.Severity.String(), + } + } + + vulns = append(vulns, vuln) + } + } + + // Detect vulns for baseOS packages. + baseOSVulns, err := s.scanner.Detect(ctx, osVer, nil, baseOSPkgs) + if err != nil { + return nil, xerrors.Errorf("failed to detect vulnerabilities with base OS scanner: %w", err) + } + + return append(baseOSVulns, vulns...), nil +} + +func (s *Scanner) isVulnerable(ctx context.Context, installedVersion string, adv dbTypes.Advisory) bool { + // Handle unfixed vulnerabilities + if len(adv.VulnerableVersions) == 0 { + // If no vulnerable versions are specified, it means the package is always vulnerable + return true + } + + // For fixed vulnerabilities, check if installed version satisfies the constraint + return s.checkConstraints(ctx, installedVersion, adv.VulnerableVersions) +} + +func (s *Scanner) checkConstraints(ctx context.Context, installedVersion string, constraintsStr []string) bool { + if installedVersion == "" { + return false + } + + for _, constraintStr := range constraintsStr { + constraints, err := version.NewConstraints(constraintStr, s.comparer) + if err != nil { + s.logger.DebugContext(ctx, "Failed to parse constraints", + log.String("constraints", constraintStr), log.Err(err)) + return false + } + + if satisfied, err := constraints.Check(installedVersion); err != nil { + s.logger.DebugContext(ctx, "Failed to check version constraints", + log.String("version", installedVersion), + log.String("constraints", constraintStr), log.Err(err)) + return false + } else if satisfied { + return true + } + } + return false +} + +// IsSupportedVersion checks if the version is supported. +// Seal creates fixes for EOL distributions, so we assume all versions are supported. +func (s *Scanner) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool { + return true +} + +func sealPkg(pkg ftypes.Package) bool { + // Seal packages start with "seal-" + return strings.HasPrefix(strings.ToLower(pkg.Name), "seal-") || strings.HasPrefix(strings.ToLower(pkg.SrcName), "seal-") +} + +func releaseVersion(release string) string { + // Seal takes RedHat version from `el` version of package. + // cf. https://github.com/aquasecurity/vuln-list-update/pull/364#issuecomment-3244733993 + if !strings.Contains(release, "el") { + return "" + } + _, osVer, _ := strings.Cut(release, "el") // Remove `el` and text before. + osVer, _, _ = strings.Cut(osVer, "_") // Remove `_.*` suffix (e.g. `9.el10_0.1`) + osVer, _, _ = strings.Cut(osVer, "+") // Remove `+.*` suffix (e.g. `3.el8+sp1`) + return osVer +} diff --git a/pkg/detector/ospkg/seal/seal_test.go b/pkg/detector/ospkg/seal/seal_test.go new file mode 100644 index 000000000000..7c0a8a9dc8ea --- /dev/null +++ b/pkg/detector/ospkg/seal/seal_test.go @@ -0,0 +1,238 @@ +package seal_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/seal" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + baseOS ftypes.OSType + fixtures []string + args args + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "Debian scanner", + baseOS: ftypes.Debian, + fixtures: []string{ + "testdata/fixtures/seal.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "12", + pkgs: []ftypes.Package{ + { + Name: "seal-wget", + Version: "1.21", + Release: "1+deb11u1", + SrcName: "seal-wget", + SrcVersion: "1.21", + SrcRelease: "1+deb11u1", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "seal-wget", + VulnerabilityID: "CVE-2024-10524", + InstalledVersion: "1.21-1+deb11u1", + FixedVersion: "1.21-1+deb11u1+sp999", + DataSource: &dbTypes.DataSource{ + ID: "seal", + Name: "Seal Security Database", + URL: "http://vulnfeed.sealsecurity.io/v1/osv/renamed/vulnerabilities.zip", + BaseID: "debian", + }, + }, + }, + }, + { + name: "Ubuntu scanner", + baseOS: ftypes.Ubuntu, + fixtures: []string{ + "testdata/fixtures/seal.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "22.04", + pkgs: []ftypes.Package{ + { + Name: "seal-wget", + Version: "1.21", + Release: "1+deb11u1", + SrcName: "seal-wget", + SrcVersion: "1.21", + SrcRelease: "1+deb11u1", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "seal-wget", + VulnerabilityID: "CVE-2024-10524", + InstalledVersion: "1.21-1+deb11u1", + FixedVersion: "1.21-1+deb11u1+sp999", + DataSource: &dbTypes.DataSource{ + ID: "seal", + Name: "Seal Security Database", + URL: "http://vulnfeed.sealsecurity.io/v1/osv/renamed/vulnerabilities.zip", + BaseID: "debian", + }, + }, + }, + }, + { + name: "Alpine scanner", + baseOS: ftypes.Alpine, + fixtures: []string{ + "testdata/fixtures/seal.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.21.5", + pkgs: []ftypes.Package{ + { + Name: "seal-zlib", + Version: "1.2.8-r2", + SrcName: "seal-zlib", + SrcVersion: "1.2.8-r2", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "seal-zlib", + VulnerabilityID: "CVE-2023-6992", + InstalledVersion: "1.2.8-r2", + FixedVersion: "1.2.8-r25341999", + DataSource: &dbTypes.DataSource{ + ID: "seal", + Name: "Seal Security Database", + URL: "http://vulnfeed.sealsecurity.io/v1/osv/renamed/vulnerabilities.zip", + BaseID: "alpine", + }, + }, + }, + }, + { + name: "RedHat scanner", + baseOS: ftypes.RedHat, + fixtures: []string{ + "testdata/fixtures/seal.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "9.3", + pkgs: []ftypes.Package{ + { + Name: "seal-wget", + Version: "1.12", + Release: "10.el6", + SrcName: "seal-wget", + SrcVersion: "1.12", + SrcRelease: "10.el6", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "seal-wget", + VulnerabilityID: "CVE-2024-10524", + InstalledVersion: "1.12-10.el6", + FixedVersion: "1.12-10.el6+sp999", + DataSource: &dbTypes.DataSource{ + ID: "seal", + Name: "Seal Security Database", + URL: "http://vulnfeed.sealsecurity.io/v1/osv/renamed/vulnerabilities.zip", + BaseID: "redhat", + }, + }, + }, + }, + { + name: "CentOS scanner", + baseOS: ftypes.CentOS, + fixtures: []string{ + "testdata/fixtures/seal.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "8.9", + pkgs: []ftypes.Package{ + { + Name: "seal-wget", + Version: "1.12", + Release: "10.el6", + SrcName: "seal-wget", + SrcVersion: "1.12", + SrcRelease: "10.el6", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "seal-wget", + VulnerabilityID: "CVE-2024-10524", + InstalledVersion: "1.12-10.el6", + FixedVersion: "1.12-10.el6+sp999", + DataSource: &dbTypes.DataSource{ + ID: "seal", + Name: "Seal Security Database", + URL: "http://vulnfeed.sealsecurity.io/v1/osv/renamed/vulnerabilities.zip", + BaseID: "redhat", + }, + }, + }, + }, + { + name: "Get returns an error", + baseOS: ftypes.Alpine, + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "3.20", + pkgs: []ftypes.Package{ + { + Name: "seal-jq", + Version: "1.5-12", + SrcName: "seal-jq", + SrcVersion: "1.5-12", + }, + }, + }, + wantErr: "failed to get Seal advisories", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer dbtest.Close() + + scanner := seal.NewScanner(tt.baseOS) + got, err := scanner.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/seal/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/seal/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..74ecb4abee01 --- /dev/null +++ b/pkg/detector/ospkg/seal/testdata/fixtures/data-source.yaml @@ -0,0 +1,25 @@ +- bucket: data-source + pairs: + - key: debian 12 + value: + ID: "debian" + Name: "Debian Security Tracker" + URL: "https://salsa.debian.org/security-tracker-team/security-tracker" + - key: seal debian + value: + ID: "seal" + Name: "Seal Security Database" + URL: "http://vulnfeed.sealsecurity.io/v1/osv/renamed/vulnerabilities.zip" + BaseID: "debian" + - key: seal alpine + value: + ID: "seal" + Name: "Seal Security Database" + URL: "http://vulnfeed.sealsecurity.io/v1/osv/renamed/vulnerabilities.zip" + BaseID: "alpine" + - key: seal Red Hat 6 + value: + ID: "seal" + Name: "Seal Security Database" + URL: "http://vulnfeed.sealsecurity.io/v1/osv/renamed/vulnerabilities.zip" + BaseID: "redhat" \ No newline at end of file diff --git a/pkg/detector/ospkg/seal/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/seal/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..726949d3c22f --- /dev/null +++ b/pkg/detector/ospkg/seal/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: seal alpine + pairs: + - bucket: seal-jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/seal/testdata/fixtures/seal.yaml b/pkg/detector/ospkg/seal/testdata/fixtures/seal.yaml new file mode 100644 index 000000000000..c2b67fd7cfb0 --- /dev/null +++ b/pkg/detector/ospkg/seal/testdata/fixtures/seal.yaml @@ -0,0 +1,41 @@ +- bucket: debian 12 + pairs: + - bucket: openssl + pairs: + - key: CVE-2025-27587 + value: + VulnerableVersions: + - "<3.0.16-1~deb12u1" + PatchedVersions: + - "3.0.16-1~deb12u1" + Severity: 1 +- bucket: seal debian + pairs: + - bucket: seal-wget + pairs: + - key: CVE-2024-10524 + value: + VulnerableVersions: + - ">=1.21-1+deb11u1, <1.21-1+deb11u1+sp999" + PatchedVersions: + - "1.21-1+deb11u1+sp999" +- bucket: seal alpine + pairs: + - bucket: seal-zlib + pairs: + - key: CVE-2023-6992 + value: + VulnerableVersions: + - ">=1.2.8-r2, <1.2.8-r25341999" + PatchedVersions: + - "1.2.8-r25341999" +- bucket: seal Red Hat 6 + pairs: + - bucket: seal-wget + pairs: + - key: CVE-2024-10524 + value: + VulnerableVersions: + - ">=1.12-10.el6, <1.12-10.el6+sp999" + PatchedVersions: + - "1.12-10.el6+sp999" \ No newline at end of file diff --git a/pkg/detector/ospkg/suse/suse.go b/pkg/detector/ospkg/suse/suse.go new file mode 100644 index 000000000000..9eef46532109 --- /dev/null +++ b/pkg/detector/ospkg/suse/suse.go @@ -0,0 +1,171 @@ +package suse + +import ( + "context" + "time" + + version "github.com/knqyf263/go-rpm-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + susecvrf "github.com/aquasecurity/trivy-db/pkg/vulnsrc/suse-cvrf" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + slesEolDates = map[string]time.Time{ + // Source: https://www.suse.com/lifecycle/ + "10": time.Date(2007, 12, 31, 23, 59, 59, 0, time.UTC), + "10.1": time.Date(2008, 11, 30, 23, 59, 59, 0, time.UTC), + "10.2": time.Date(2010, 4, 11, 23, 59, 59, 0, time.UTC), + "10.3": time.Date(2011, 10, 11, 23, 59, 59, 0, time.UTC), + "10.4": time.Date(2013, 7, 31, 23, 59, 59, 0, time.UTC), + "11": time.Date(2010, 12, 31, 23, 59, 59, 0, time.UTC), + "11.1": time.Date(2012, 8, 31, 23, 59, 59, 0, time.UTC), + "11.2": time.Date(2014, 1, 31, 23, 59, 59, 0, time.UTC), + "11.3": time.Date(2016, 1, 31, 23, 59, 59, 0, time.UTC), + "11.4": time.Date(2019, 3, 31, 23, 59, 59, 0, time.UTC), + "12": time.Date(2016, 6, 30, 23, 59, 59, 0, time.UTC), + "12.1": time.Date(2017, 5, 31, 23, 59, 59, 0, time.UTC), + "12.2": time.Date(2018, 3, 31, 23, 59, 59, 0, time.UTC), + "12.3": time.Date(2019, 1, 30, 23, 59, 59, 0, time.UTC), + "12.4": time.Date(2020, 6, 30, 23, 59, 59, 0, time.UTC), + "12.5": time.Date(2024, 10, 31, 23, 59, 59, 0, time.UTC), + "15": time.Date(2019, 12, 31, 23, 59, 59, 0, time.UTC), + "15.1": time.Date(2021, 1, 31, 23, 59, 59, 0, time.UTC), + "15.2": time.Date(2021, 12, 31, 23, 59, 59, 0, time.UTC), + "15.3": time.Date(2022, 12, 31, 23, 59, 59, 0, time.UTC), + "15.4": time.Date(2023, 12, 31, 23, 59, 59, 0, time.UTC), + "15.5": time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC), + "15.6": time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC), + "15.7": time.Date(2031, 7, 31, 23, 59, 59, 0, time.UTC), + "16.0": time.Date(2027, 11, 30, 23, 59, 59, 0, time.UTC), + "16.1": time.Date(2028, 11, 30, 23, 59, 59, 0, time.UTC), + } + slemicroEolDates = map[string]time.Time{ + // Source: https://www.suse.com/lifecycle/ + "5.0": time.Date(2022, 3, 31, 23, 59, 59, 0, time.UTC), + "5.1": time.Date(2025, 10, 31, 23, 59, 59, 0, time.UTC), + "5.2": time.Date(2026, 4, 30, 23, 59, 59, 0, time.UTC), + "5.3": time.Date(2026, 10, 30, 23, 59, 59, 0, time.UTC), + "5.4": time.Date(2027, 4, 30, 23, 59, 59, 0, time.UTC), + "5.5": time.Date(2027, 10, 31, 23, 59, 59, 0, time.UTC), + "6.0": time.Date(2028, 6, 30, 23, 59, 59, 0, time.UTC), + "6.1": time.Date(2028, 11, 30, 23, 59, 59, 0, time.UTC), + // 6.2 will be released late 2025 + // "6.2": time.Date(2029, 11, 30, 23, 59, 59, 0, time.UTC), + } + + opensuseEolDates = map[string]time.Time{ + // Source: https://en.opensuse.org/Lifetime + "42.1": time.Date(2017, 5, 17, 23, 59, 59, 0, time.UTC), + "42.2": time.Date(2018, 1, 26, 23, 59, 59, 0, time.UTC), + "42.3": time.Date(2019, 6, 30, 23, 59, 59, 0, time.UTC), + "15.0": time.Date(2019, 12, 3, 23, 59, 59, 0, time.UTC), + "15.1": time.Date(2020, 11, 30, 23, 59, 59, 0, time.UTC), + "15.2": time.Date(2021, 11, 30, 23, 59, 59, 0, time.UTC), + "15.3": time.Date(2022, 11, 30, 23, 59, 59, 0, time.UTC), + "15.4": time.Date(2023, 11, 30, 23, 59, 59, 0, time.UTC), + "15.5": time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC), + "15.6": time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC), + "16.0": time.Date(2027, 10, 31, 23, 59, 59, 0, time.UTC), + } +) + +// Type defines SUSE type +type Type int + +const ( + // SUSEEnterpriseLinux is Linux Enterprise version + SUSEEnterpriseLinux Type = iota + // SUSE Linux Enterprise Micro is the micro series + SUSEEnterpriseLinuxMicro + // OpenSUSE for open versions + OpenSUSE + OpenSUSETumbleweed +) + +// Scanner implements the SUSE scanner +type Scanner struct { + vs susecvrf.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner(t Type) *Scanner { + switch t { + case SUSEEnterpriseLinux: + return &Scanner{ + vs: susecvrf.NewVulnSrc(susecvrf.SUSEEnterpriseLinux), + } + case SUSEEnterpriseLinuxMicro: + return &Scanner{ + vs: susecvrf.NewVulnSrc(susecvrf.SUSEEnterpriseLinuxMicro), + } + case OpenSUSE: + return &Scanner{ + vs: susecvrf.NewVulnSrc(susecvrf.OpenSUSE), + } + case OpenSUSETumbleweed: + return &Scanner{ + vs: susecvrf.NewVulnSrc(susecvrf.OpenSUSETumbleweed), + } + } + return nil +} + +// Detect scans and returns the vulnerabilities +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + advisories, err := s.vs.Get(db.GetParams{ + Release: osVer, + PkgName: pkg.Name, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get SUSE advisory: %w", err) + } + + installed := utils.FormatVersion(pkg) + installedVersion := version.NewVersion(installed) + for _, adv := range advisories { + fixedVersion := version.NewVersion(adv.FixedVersion) + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: installed, + PkgIdentifier: pkg.Identifier, + Layer: pkg.Layer, + Custom: adv.Custom, + DataSource: adv.DataSource, + } + if installedVersion.LessThan(fixedVersion) { + vuln.FixedVersion = adv.FixedVersion + vulns = append(vulns, vuln) + } + } + } + return vulns, nil +} + +// IsSupportedVersion checks if OSFamily can be scanned using SUSE scanner +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + if osFamily == ftypes.SLES { + return osver.Supported(ctx, slesEolDates, osFamily, osVer) + } + if osFamily == ftypes.SLEMicro { + return osver.Supported(ctx, slemicroEolDates, osFamily, osVer) + } + // tumbleweed is a rolling release, it has no version and no eol + if osFamily == ftypes.OpenSUSETumbleweed { + return true + } + return osver.Supported(ctx, opensuseEolDates, osFamily, osVer) +} diff --git a/pkg/detector/ospkg/suse/suse_test.go b/pkg/detector/ospkg/suse/suse_test.go new file mode 100644 index 000000000000..f1067a124b56 --- /dev/null +++ b/pkg/detector/ospkg/suse/suse_test.go @@ -0,0 +1,291 @@ +package suse_test + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/suse" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + distribution suse.Type + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/suse.yaml", + "testdata/fixtures/data-source.yaml", + }, + distribution: suse.OpenSUSE, + args: args{ + osVer: "15.3", + pkgs: []ftypes.Package{ + { + Name: "postgresql", + Version: "13", + Release: "4.6.6", + SrcName: "postgresql", + SrcVersion: "13", + SrcRelease: "4.6.6", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "postgresql", + VulnerabilityID: "SUSE-SU-2021:0175-1", + InstalledVersion: "13-4.6.6", + FixedVersion: "13-4.6.7", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.SuseCVRF, + Name: "SUSE CVRF", + URL: "https://ftp.suse.com/pub/projects/security/cvrf/", + }, + }, + }, + }, + { + name: "happy path: tumbleweed", + fixtures: []string{ + "testdata/fixtures/tumbleweed.yaml", + "testdata/fixtures/data-source.yaml", + }, + distribution: suse.OpenSUSETumbleweed, + args: args{ + osVer: "", + pkgs: []ftypes.Package{ + { + Name: "singularity-ce", + Version: "4.1.3", + Release: "1.0", + SrcName: "postgresql", + SrcVersion: "4.1.3", + SrcRelease: "1.1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "singularity-ce", + VulnerabilityID: "openSUSE-SU-2024:14059-1", + InstalledVersion: "4.1.3-1.0", + FixedVersion: "4.1.3-1.1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.SuseCVRF, + Name: "SUSE CVRF", + URL: "https://ftp.suse.com/pub/projects/security/cvrf/", + }, + }, + }, + }, + { + name: "happy path: suse sle 15sp3", + fixtures: []string{ + "testdata/fixtures/suse.yaml", + "testdata/fixtures/data-source.yaml", + }, + distribution: suse.SUSEEnterpriseLinux, + args: args{ + osVer: "15.3", + pkgs: []ftypes.Package{ + { + Name: "libopenssl1_1", + Version: "1.1.1d", + Release: "150200.11.47.1", + SrcName: "libopenssl1_1", + SrcVersion: "1.1.1d", + SrcRelease: "150200.11.47.1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "libopenssl1_1", + VulnerabilityID: "SUSE-SU-2022:2251-1", + InstalledVersion: "1.1.1d-150200.11.47.1", + FixedVersion: "1.1.1d-150200.11.48.1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.SuseCVRF, + Name: "SUSE CVRF", + URL: "https://ftp.suse.com/pub/projects/security/cvrf/", + }, + }, + }, + }, + { + name: "happy path: suse sle micro 15.3", + fixtures: []string{ + "testdata/fixtures/suse.yaml", + "testdata/fixtures/data-source.yaml", + }, + distribution: suse.SUSEEnterpriseLinuxMicro, + args: args{ + osVer: "5.3", + pkgs: []ftypes.Package{ + { + Name: "libopenssl1_1", + Version: "1.1.1l", + Release: "150400.7.21.1", + SrcName: "libopenssl1_1", + SrcVersion: "1.1.1l", + SrcRelease: "150400.7.21.1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "libopenssl1_1", + VulnerabilityID: "SUSE-SU-2023:0311-1", + InstalledVersion: "1.1.1l-150400.7.21.1", + FixedVersion: "1.1.1l-150400.7.22.1", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.SuseCVRF, + Name: "SUSE CVRF", + URL: "https://ftp.suse.com/pub/projects/security/cvrf/", + }, + }, + }, + }, + { + name: "broken bucket", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + distribution: suse.SUSEEnterpriseLinux, + args: args{ + osVer: "15.3", + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + wantErr: "failed to get SUSE advisory", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := suse.NewScanner(tt.distribution) + got, err := s.Detect(t.Context(), tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsSupportedVersion(t *testing.T) { + type args struct { + osFamily ftypes.OSType + osVer string + } + tests := []struct { + name string + now time.Time + distribution suse.Type + args args + want bool + }{ + { + name: "opensuse-tumbleweed", + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "opensuse-tumbleweed", + osVer: "", + }, + distribution: suse.OpenSUSETumbleweed, + want: true, + }, + { + name: "opensuse-leap42.3", + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "opensuse-leap", + osVer: "42.3", + }, + distribution: suse.OpenSUSE, + want: true, + }, + { + name: "sles12.3", + now: time.Date(2019, 5, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "sles", + osVer: "12.3", + }, + distribution: suse.SUSEEnterpriseLinux, + want: false, + }, + { + name: "latest", + now: time.Date(2019, 5, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "opensuse-leap", + osVer: "999.0", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), tt.now) + s := suse.NewScanner(tt.distribution) + got := s.IsSupportedVersion(ctx, tt.args.osFamily, tt.args.osVer) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/suse/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/suse/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..4e9ddde802d5 --- /dev/null +++ b/pkg/detector/ospkg/suse/testdata/fixtures/data-source.yaml @@ -0,0 +1,22 @@ +- bucket: data-source + pairs: + - key: openSUSE Tumbleweed + value: + ID: "suse-cvrf" + Name: "SUSE CVRF" + URL: "https://ftp.suse.com/pub/projects/security/cvrf/" + - key: openSUSE Leap 15.3 + value: + ID: "suse-cvrf" + Name: "SUSE CVRF" + URL: "https://ftp.suse.com/pub/projects/security/cvrf/" + - key: SUSE Linux Enterprise 15.3 + value: + ID: "suse-cvrf" + Name: "SUSE CVRF" + URL: "https://ftp.suse.com/pub/projects/security/cvrf/" + - key: SUSE Linux Enterprise Micro 5.3 + value: + ID: "suse-cvrf" + Name: "SUSE CVRF" + URL: "https://ftp.suse.com/pub/projects/security/cvrf/" diff --git a/pkg/detector/ospkg/suse/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/suse/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..9a2e176c166c --- /dev/null +++ b/pkg/detector/ospkg/suse/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: SUSE Linux Enterprise 15.3 + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/suse/testdata/fixtures/suse.yaml b/pkg/detector/ospkg/suse/testdata/fixtures/suse.yaml new file mode 100644 index 000000000000..027e56673d0e --- /dev/null +++ b/pkg/detector/ospkg/suse/testdata/fixtures/suse.yaml @@ -0,0 +1,30 @@ +- bucket: openSUSE Leap 15.3 + pairs: + - bucket: postgresql + pairs: + - key: SUSE-SU-2021:0175-1 + value: + FixedVersion: "13-4.6.7" + - key: CVE-2021-0001 + value: + FixedVersion: "" +- bucket: SUSE Linux Enterprise 15.3 + pairs: + - bucket: libopenssl1_1 + pairs: + - key: "SUSE-SU-2022:2251-1" + value: + FixedVersion: 1.1.1d-150200.11.48.1 + - bucket: openssl-1_1 + pairs: + - key: "SUSE-SU-2022:2251-1" + value: + FixedVersion: 1.1.1d-150200.11.48.1 +- bucket: SUSE Linux Enterprise Micro 5.3 + pairs: + - bucket: libopenssl1_1 + pairs: + - key: "SUSE-SU-2023:0311-1" + value: + FixedVersion: 1.1.1l-150400.7.22.1 + diff --git a/pkg/detector/ospkg/suse/testdata/fixtures/tumbleweed.yaml b/pkg/detector/ospkg/suse/testdata/fixtures/tumbleweed.yaml new file mode 100644 index 000000000000..2912dda8a711 --- /dev/null +++ b/pkg/detector/ospkg/suse/testdata/fixtures/tumbleweed.yaml @@ -0,0 +1,7 @@ +- bucket: openSUSE Tumbleweed + pairs: + - bucket: singularity-ce + pairs: + - key: openSUSE-SU-2024:14059-1 + value: + FixedVersion: "4.1.3-1.1" diff --git a/pkg/detector/ospkg/ubuntu/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/ubuntu/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..b5c8f895fd21 --- /dev/null +++ b/pkg/detector/ospkg/ubuntu/testdata/fixtures/data-source.yaml @@ -0,0 +1,12 @@ +- bucket: data-source + pairs: + - key: ubuntu 20.04 + value: + ID: "ubuntu" + Name: "Ubuntu CVE Tracker" + URL: "https://git.launchpad.net/ubuntu-cve-tracker" + - key: ubuntu 21.04 + value: + ID: "ubuntu" + Name: "Ubuntu CVE Tracker" + URL: "https://git.launchpad.net/ubuntu-cve-tracker" \ No newline at end of file diff --git a/pkg/detector/ospkg/ubuntu/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/ubuntu/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..29fcd05cbb99 --- /dev/null +++ b/pkg/detector/ospkg/ubuntu/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: ubuntu 21.04 + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/ubuntu/testdata/fixtures/ubuntu.yaml b/pkg/detector/ospkg/ubuntu/testdata/fixtures/ubuntu.yaml new file mode 100644 index 000000000000..bcb207cd8e26 --- /dev/null +++ b/pkg/detector/ospkg/ubuntu/testdata/fixtures/ubuntu.yaml @@ -0,0 +1,20 @@ +- bucket: ubuntu 19.04 + pairs: + - bucket: wpa + pairs: + - key: CVE-2019-9243 + value: + FixedVersion: "" +- bucket: ubuntu 20.04 + pairs: + - bucket: wpa + pairs: + - key: CVE-2021-27803 + value: + FixedVersion: "2:2.9-1ubuntu4.3" + - key: CVE-2019-9243 + value: + FixedVersion: "" + - key: CVE-2016-4476 + value: + FixedVersion: "2.4-0ubuntu10" diff --git a/pkg/detector/ospkg/ubuntu/ubuntu.go b/pkg/detector/ospkg/ubuntu/ubuntu.go new file mode 100644 index 000000000000..b18a1326cbfb --- /dev/null +++ b/pkg/detector/ospkg/ubuntu/ubuntu.go @@ -0,0 +1,179 @@ +package ubuntu + +import ( + "context" + "strings" + "time" + + version "github.com/knqyf263/go-deb-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/ubuntu" + "github.com/aquasecurity/trivy/pkg/clock" + osver "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + eolDates = map[string]time.Time{ + "4.10": time.Date(2006, 4, 30, 23, 59, 59, 0, time.UTC), + "5.04": time.Date(2006, 10, 31, 23, 59, 59, 0, time.UTC), + "5.10": time.Date(2007, 4, 13, 23, 59, 59, 0, time.UTC), + "6.06": time.Date(2011, 6, 1, 23, 59, 59, 0, time.UTC), + "6.10": time.Date(2008, 4, 25, 23, 59, 59, 0, time.UTC), + "7.04": time.Date(2008, 10, 19, 23, 59, 59, 0, time.UTC), + "7.10": time.Date(2009, 4, 18, 23, 59, 59, 0, time.UTC), + "8.04": time.Date(2013, 5, 9, 23, 59, 59, 0, time.UTC), + "8.10": time.Date(2010, 4, 30, 23, 59, 59, 0, time.UTC), + "9.04": time.Date(2010, 10, 23, 23, 59, 59, 0, time.UTC), + "9.10": time.Date(2011, 4, 29, 23, 59, 59, 0, time.UTC), + "10.04": time.Date(2015, 4, 29, 23, 59, 59, 0, time.UTC), + "10.10": time.Date(2012, 4, 10, 23, 59, 59, 0, time.UTC), + "11.04": time.Date(2012, 10, 28, 23, 59, 59, 0, time.UTC), + "11.10": time.Date(2013, 5, 9, 23, 59, 59, 0, time.UTC), + "12.04": time.Date(2019, 4, 26, 23, 59, 59, 0, time.UTC), + "12.04-ESM": time.Date(2019, 4, 28, 23, 59, 59, 0, time.UTC), + "12.10": time.Date(2014, 5, 16, 23, 59, 59, 0, time.UTC), + "13.04": time.Date(2014, 1, 27, 23, 59, 59, 0, time.UTC), + "13.10": time.Date(2014, 7, 17, 23, 59, 59, 0, time.UTC), + "14.04": time.Date(2022, 4, 25, 23, 59, 59, 0, time.UTC), + "14.04-ESM": time.Date(2024, 4, 25, 23, 59, 59, 0, time.UTC), + "14.10": time.Date(2015, 7, 23, 23, 59, 59, 0, time.UTC), + "15.04": time.Date(2016, 1, 23, 23, 59, 59, 0, time.UTC), + "15.10": time.Date(2016, 7, 22, 23, 59, 59, 0, time.UTC), + "16.04": time.Date(2021, 4, 21, 23, 59, 59, 0, time.UTC), + "16.04-ESM": time.Date(2026, 4, 29, 23, 59, 59, 0, time.UTC), + "16.10": time.Date(2017, 7, 20, 23, 59, 59, 0, time.UTC), + "17.04": time.Date(2018, 1, 13, 23, 59, 59, 0, time.UTC), + "17.10": time.Date(2018, 7, 19, 23, 59, 59, 0, time.UTC), + "18.04": time.Date(2023, 5, 31, 23, 59, 59, 0, time.UTC), + "18.04-ESM": time.Date(2028, 3, 31, 23, 59, 59, 0, time.UTC), + "18.10": time.Date(2019, 7, 18, 23, 59, 59, 0, time.UTC), + "19.04": time.Date(2020, 1, 18, 23, 59, 59, 0, time.UTC), + "19.10": time.Date(2020, 7, 17, 23, 59, 59, 0, time.UTC), + "20.04": time.Date(2025, 5, 31, 23, 59, 59, 0, time.UTC), + "20.04-ESM": time.Date(2030, 4, 30, 23, 59, 59, 0, time.UTC), + "20.10": time.Date(2021, 7, 22, 23, 59, 59, 0, time.UTC), + "21.04": time.Date(2022, 1, 20, 23, 59, 59, 0, time.UTC), + "21.10": time.Date(2022, 7, 14, 23, 59, 59, 0, time.UTC), + "22.04": time.Date(2027, 4, 23, 23, 59, 59, 0, time.UTC), + "22.10": time.Date(2023, 7, 20, 23, 59, 59, 0, time.UTC), + "23.04": time.Date(2024, 1, 20, 23, 59, 59, 0, time.UTC), + "23.10": time.Date(2024, 6, 30, 23, 59, 59, 0, time.UTC), + "24.04": time.Date(2034, 3, 31, 23, 59, 59, 0, time.UTC), + "24.10": time.Date(2025, 7, 9, 23, 59, 59, 0, time.UTC), + "25.04": time.Date(2026, 1, 16, 23, 59, 59, 0, time.UTC), + "25.10": time.Date(2026, 7, 1, 23, 59, 59, 0, time.UTC), + } +) + +type Option func(*Scanner) + +// WithEOLDates takes eol dates for testability +func WithEOLDates(dates map[string]time.Time) Option { + return func(s *Scanner) { + s.eolDates = dates + } +} + +// Scanner implements the Ubuntu scanner +type Scanner struct { + eolDates map[string]time.Time + vs ubuntu.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner(opts ...Option) *Scanner { + s := &Scanner{ + eolDates: eolDates, + vs: ubuntu.NewVulnSrc(), + } + + for _, opt := range opts { + opt(s) + } + return s +} + +// Detect scans and returns the vulnerabilities +func (s *Scanner) Detect(ctx context.Context, osVer string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Detecting vulnerabilities...", log.String("os_version", osVer), + log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + osVer = s.versionFromEolDates(ctx, osVer) + advisories, err := s.vs.Get(db.GetParams{ + Release: osVer, + PkgName: pkg.SrcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get Ubuntu advisories: %w", err) + } + + sourceVersion, err := version.NewVersion(utils.FormatSrcVersion(pkg)) + if err != nil { + log.DebugContext(ctx, "Failed to parse the installed package version", log.Err(err)) + continue + } + + for _, adv := range advisories { + vuln := types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: utils.FormatVersion(pkg), + FixedVersion: adv.FixedVersion, + PkgIdentifier: pkg.Identifier, + Layer: pkg.Layer, + Custom: adv.Custom, + DataSource: adv.DataSource, + } + + if adv.FixedVersion == "" { + vulns = append(vulns, vuln) + continue + } + + fixedVersion, err := version.NewVersion(adv.FixedVersion) + if err != nil { + log.DebugContext(ctx, "Failed to parse the fixed version", + log.String("version", adv.FixedVersion), log.Err(err)) + continue + } + + if sourceVersion.LessThan(fixedVersion) { + vulns = append(vulns, vuln) + } + } + } + return vulns, nil +} + +// IsSupportedVersion checks is OSFamily can be scanned using Ubuntu scanner +func (s *Scanner) IsSupportedVersion(ctx context.Context, osFamily ftypes.OSType, osVer string) bool { + osVer = s.versionFromEolDates(ctx, osVer) + return osver.Supported(ctx, s.eolDates, osFamily, osVer) +} + +// versionFromEolDates checks if actual (not ESM) version is not outdated +func (s *Scanner) versionFromEolDates(ctx context.Context, osVer string) string { + if _, ok := s.eolDates[osVer]; ok { + return osVer + } + + // if base version (not ESM) is still actual + // we need to use this version + // e.g. Ubuntu doesn't have vulnerabilities for `18.04-ESM`, because `18.04` is not outdated + // then we need to get vulnerabilities for `18.04` + // if `18.04` is outdated - we need to use `18.04-ESM` (we will return error until we add `18.04-ESM` to eolDates) + ver := strings.TrimRight(osVer, "-ESM") + if eol, ok := s.eolDates[ver]; ok && clock.Now(ctx).Before(eol) { + return ver + } + return osVer +} diff --git a/pkg/detector/ospkg/ubuntu/ubuntu_test.go b/pkg/detector/ospkg/ubuntu/ubuntu_test.go new file mode 100644 index 000000000000..2cf331e9ab16 --- /dev/null +++ b/pkg/detector/ospkg/ubuntu/ubuntu_test.go @@ -0,0 +1,306 @@ +package ubuntu_test + +import ( + "bytes" + "sort" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/ubuntu" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +var testEOLDates = map[string]time.Time{ + "12.04": time.Date(2019, 4, 26, 23, 59, 59, 0, time.UTC), + "12.04-ESM": time.Date(2019, 4, 28, 23, 59, 59, 0, time.UTC), + "18.04": time.Date(2023, 5, 31, 23, 59, 59, 0, time.UTC), + "18.04-ESM": time.Date(2028, 3, 31, 23, 59, 59, 0, time.UTC), + "19.04": time.Date(2020, 1, 18, 23, 59, 59, 0, time.UTC), + "20.04": time.Date(2025, 5, 31, 23, 59, 59, 0, time.UTC), + "21.04": time.Date(2022, 1, 20, 23, 59, 59, 0, time.UTC), +} + +func TestScanner_Detect(t *testing.T) { + type args struct { + osVer string + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/ubuntu.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "20.04", + pkgs: []ftypes.Package{ + { + Name: "wpa", + Version: "2.9", + SrcName: "wpa", + SrcVersion: "2.9", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "wpa", + VulnerabilityID: "CVE-2019-9243", + InstalledVersion: "2.9", + FixedVersion: "", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Ubuntu, + Name: "Ubuntu CVE Tracker", + URL: "https://git.launchpad.net/ubuntu-cve-tracker", + }, + }, + { + PkgName: "wpa", + VulnerabilityID: "CVE-2021-27803", + InstalledVersion: "2.9", + FixedVersion: "2:2.9-1ubuntu4.3", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Ubuntu, + Name: "Ubuntu CVE Tracker", + URL: "https://git.launchpad.net/ubuntu-cve-tracker", + }, + }, + }, + }, + { + name: "ubuntu 20.04-ESM. 20.04 is not outdated", + fixtures: []string{ + "testdata/fixtures/ubuntu.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "20.04-ESM", + pkgs: []ftypes.Package{ + { + Name: "wpa", + Version: "2.9", + SrcName: "wpa", + SrcVersion: "2.9", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "wpa", + VulnerabilityID: "CVE-2019-9243", + InstalledVersion: "2.9", + FixedVersion: "", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Ubuntu, + Name: "Ubuntu CVE Tracker", + URL: "https://git.launchpad.net/ubuntu-cve-tracker", + }, + }, + { + PkgName: "wpa", + VulnerabilityID: "CVE-2021-27803", + InstalledVersion: "2.9", + FixedVersion: "2:2.9-1ubuntu4.3", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Ubuntu, + Name: "Ubuntu CVE Tracker", + URL: "https://git.launchpad.net/ubuntu-cve-tracker", + }, + }, + }, + }, + { + name: "ubuntu 19.04-ESM, 19.04 is outdated", // Use 19.04-ESM for testing, although it doesn't exist + fixtures: []string{ + "testdata/fixtures/ubuntu.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "19.04-ESM", + pkgs: []ftypes.Package{ + { + Name: "wpa", + Version: "2.9", + SrcName: "wpa", + SrcVersion: "2.9", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + }, + { + name: "broken bucket", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + osVer: "21.04", + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + wantErr: "failed to get Ubuntu advisories", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), time.Date(2024, 4, 1, 0, 0, 0, 0, time.UTC)) + + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := ubuntu.NewScanner(ubuntu.WithEOLDates(testEOLDates)) + got, err := s.Detect(ctx, tt.args.osVer, nil, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + sort.Slice(got, func(i, j int) bool { + return got[i].VulnerabilityID < got[j].VulnerabilityID + }) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestScanner_IsSupportedVersion(t *testing.T) { + type args struct { + osFamily ftypes.OSType + osVer string + } + tests := []struct { + name string + now time.Time + args args + want bool + wantLog string + }{ + { + name: "ubuntu 12.04 eol ends", + now: time.Date(2019, 3, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "ubuntu", + osVer: "12.04", + }, + want: true, + }, + { + name: "ubuntu 12.04", + now: time.Date(2019, 4, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "ubuntu", + osVer: "12.04", + }, + want: false, + }, + { + name: "ubuntu 18.04 ESM and 18.04 are outdated", + now: time.Date(2030, 4, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "ubuntu", + osVer: "18.04-ESM", + }, + want: false, + }, + { + name: "ubuntu 18.04 ESM. Only 18.04 is outdated", + now: time.Date(2027, 4, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "ubuntu", + osVer: "18.04-ESM", + }, + want: true, + }, + { + name: "ubuntu 20.04 ESM. 20.04 is not outdated, 20.04 ESM not added in EOL dates", + now: time.Date(2022, 4, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "ubuntu", + osVer: "20.04-ESM", + }, + want: true, + }, + { + name: "ubuntu 20.04 ESM. 20.04 is outdated, 20.04 ESM not added in EOL dates", + now: time.Date(2030, 4, 31, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "ubuntu", + osVer: "20.04-ESM", + }, + want: true, + wantLog: "This OS version is not on the EOL list\tfamily=\"ubuntu\" version=\"20.04-ESM\"", + }, + { + name: "latest", + now: time.Date(2019, 5, 2, 23, 59, 59, 0, time.UTC), + args: args{ + osFamily: "ubuntu", + osVer: "99.04", + }, + want: true, + wantLog: "This OS version is not on the EOL list\tfamily=\"ubuntu\" version=\"99.04\"", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := bytes.NewBuffer(nil) + logger := log.New(log.NewHandler(out, &log.Options{Level: log.LevelInfo})) + log.SetDefault(logger) + + ctx := clock.With(t.Context(), tt.now) + s := ubuntu.NewScanner(ubuntu.WithEOLDates(testEOLDates)) + got := s.IsSupportedVersion(ctx, tt.args.osFamily, tt.args.osVer) + assert.Equal(t, tt.want, got) + + if out.Len() > 0 && tt.wantLog == "" { + t.Errorf("IsSupportedVersion() logs not expected. Found logs: %s", out.String()) + return + } + assert.Contains(t, out.String(), tt.wantLog) + }) + } +} diff --git a/pkg/detector/ospkg/version/compare.go b/pkg/detector/ospkg/version/compare.go new file mode 100644 index 000000000000..768691678865 --- /dev/null +++ b/pkg/detector/ospkg/version/compare.go @@ -0,0 +1,88 @@ +package version + +import ( + apkver "github.com/knqyf263/go-apk-version" + debver "github.com/knqyf263/go-deb-version" + rpmver "github.com/knqyf263/go-rpm-version" + "golang.org/x/xerrors" +) + +// Comparer defines the interface for version comparison +type Comparer interface { + Compare(version1, version2 string) (int, error) +} + +// DEBComparer implements Comparer for Debian/Ubuntu packages +type DEBComparer struct{} + +// NewDEBComparer creates a new DEB version comparer +func NewDEBComparer() *DEBComparer { + return &DEBComparer{} +} + +// Compare compares two Debian package versions +// Returns: +// - positive if version1 > version2 +// - negative if version1 < version2 +// - zero if version1 == version2 +func (c *DEBComparer) Compare(version1, version2 string) (int, error) { + v1, err := debver.NewVersion(version1) + if err != nil { + return 0, err + } + + v2, err := debver.NewVersion(version2) + if err != nil { + return 0, err + } + + return v1.Compare(v2), nil +} + +// APKComparer implements Comparer for Alpine packages +type APKComparer struct{} + +// NewAPKComparer creates a new APK version comparer +func NewAPKComparer() *APKComparer { + return &APKComparer{} +} + +// Compare compares two Alpine package versions +// Returns: +// - positive if version1 > version2 +// - negative if version1 < version2 +// - zero if version1 == version2 +func (c *APKComparer) Compare(version1, version2 string) (int, error) { + v1, err := apkver.NewVersion(version1) + if err != nil { + return 0, xerrors.Errorf("failed to parse apk %q version: %w", version1, err) + } + + v2, err := apkver.NewVersion(version2) + if err != nil { + return 0, xerrors.Errorf("failed to parse apk %q version: %w", version2, err) + } + + return v1.Compare(v2), nil +} + +// RPMComparer implements Comparer for RedHat packages +type RPMComparer struct{} + +// NewRPMComparer creates a new RedHat version comparer +func NewRPMComparer() *RPMComparer { + return &RPMComparer{} +} + +// Compare compares two RedHat package versions +// Returns: +// - positive if version1 > version2 +// - negative if version1 < version2 +// - zero if version1 == version2 +func (c *RPMComparer) Compare(version1, version2 string) (int, error) { + v1 := rpmver.NewVersion(version1) + + v2 := rpmver.NewVersion(version2) + + return v1.Compare(v2), nil +} diff --git a/pkg/detector/ospkg/version/compare_test.go b/pkg/detector/ospkg/version/compare_test.go new file mode 100644 index 000000000000..81de1fcda4f2 --- /dev/null +++ b/pkg/detector/ospkg/version/compare_test.go @@ -0,0 +1,152 @@ +package version_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" +) + +func TestDEBComparer_Compare(t *testing.T) { + tests := []struct { + name string + version1 string + version2 string + want int + wantErr bool + }{ + { + name: "equal versions", + version1: "1.2.3", + version2: "1.2.3", + want: 0, + }, + { + name: "version1 greater", + version1: "1.2.4", + version2: "1.2.3", + want: 1, + }, + { + name: "version1 less", + version1: "1.2.2", + version2: "1.2.3", + want: -1, + }, + { + name: "with debian revision - equal base, different revision", + version1: "1.2.3-1", + version2: "1.2.3-2", + want: -1, + }, + { + name: "with epoch - different epoch", + version1: "1:1.2.3", + version2: "2:1.2.3", + want: -1, + }, + { + name: "with epoch and revision", + version1: "1:1.2.3-1", + version2: "1:1.2.3-2", + want: -1, + }, + { + name: "ubuntu specific version", + version1: "1.2.3-1ubuntu1", + version2: "1.2.3-1ubuntu2", + want: -1, + }, + { + name: "invalid version1", + version1: "invalid_version", + version2: "1.2.3", + wantErr: true, + }, + { + name: "invalid version2", + version1: "1.2.3", + version2: "invalid_version", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := version.NewDEBComparer() + got, err := c.Compare(tt.version1, tt.version2) + + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestAPKComparer_Compare(t *testing.T) { + tests := []struct { + name string + version1 string + version2 string + want int + wantErr bool + }{ + { + name: "equal versions", + version1: "1.2.3", + version2: "1.2.3", + want: 0, + }, + { + name: "version1 greater", + version1: "1.2.4", + version2: "1.2.3", + want: 1, + }, + { + name: "version1 less", + version1: "1.2.2", + version2: "1.2.3", + want: -1, + }, + { + name: "with alpine revision - equal base, different revision", + version1: "1.2.3-r0", + version2: "1.2.3-r1", + want: -1, + }, + { + name: "pre-release versions", + version1: "1.2.3_pre1", + version2: "1.2.3", + want: -1, + }, + { + name: "complex alpine version", + version1: "1.2.3-r0", + version2: "1.2.3_pre1-r0", + want: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := version.NewAPKComparer() + got, err := c.Compare(tt.version1, tt.version2) + + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/version/constraint.go b/pkg/detector/ospkg/version/constraint.go new file mode 100644 index 000000000000..520d9ef0d6d6 --- /dev/null +++ b/pkg/detector/ospkg/version/constraint.go @@ -0,0 +1,195 @@ +package version + +import ( + "regexp" + "strings" + + "golang.org/x/xerrors" +) + +// operatorFunc defines the signature for constraint operator functions +type operatorFunc func(v, c string, comparer Comparer) (bool, error) + +// constraintOperators maps operator strings to their corresponding functions +var constraintOperators = map[string]operatorFunc{ + "": constraintEqual, + "=": constraintEqual, + "==": constraintEqual, + "!=": constraintNotEqual, + ">": constraintGreaterThan, + "<": constraintLessThan, + ">=": constraintGreaterThanEqual, + "<=": constraintLessThanEqual, +} + +// constraintRegex matches constraint patterns like ">=1.2.3", "<2.0.0", "==1.0.0" +// Version can contain numbers, dots, hyphens, plus signs, tildes, colons, and alphanumeric characters +var constraintRegex = regexp.MustCompile(`^(>=|<=|>|<|==|!=|=)?\s*([0-9]+[0-9a-zA-Z.\-+~:_]*)$`) + +// constraint represents a single version constraint +type constraint struct { + version string + operator operatorFunc + original string +} + +// Constraints represents a collection of constraints with a comparer +type Constraints struct { + constraints []*constraint + comparer Comparer +} + +// NewConstraints creates a new Constraints from a constraint string and comparer +// Multiple constraints can be separated by commas or spaces +func NewConstraints(constraints string, comparer Comparer) (*Constraints, error) { + if constraints == "" { + return nil, xerrors.New("constraints string is empty") + } + + var cs []*constraint + constraintList := splitConstraints(constraints) + for _, constraintStr := range constraintList { + constraintStr = strings.TrimSpace(constraintStr) + if constraintStr == "" { + continue + } + + c, err := newConstraint(constraintStr) + if err != nil { + return nil, xerrors.Errorf("invalid constraint '%s': %w", constraintStr, err) + } + + cs = append(cs, c) + } + + return &Constraints{ + constraints: cs, + comparer: comparer, + }, nil +} + +// splitConstraints splits constraint string by comma or space, preferring comma +func splitConstraints(constraints string) []string { + // If contains comma, split by comma + if strings.Contains(constraints, ",") { + return strings.Split(constraints, ",") + } + // Otherwise, split by spaces + return strings.Fields(constraints) +} + +// newConstraint creates a new constraint from a constraint string +func newConstraint(constraintStr string) (*constraint, error) { + constraintStr = strings.TrimSpace(constraintStr) + matches := constraintRegex.FindStringSubmatch(constraintStr) + if len(matches) != 3 { + return nil, xerrors.Errorf("invalid constraint format: %s", constraintStr) + } + + op := matches[1] + version := strings.TrimSpace(matches[2]) + + operator, ok := constraintOperators[op] + if !ok { + return nil, xerrors.Errorf("unsupported operator: %s", op) + } + + return &constraint{ + version: version, + operator: operator, + original: constraintStr, + }, nil +} + +// Check returns true if the given version satisfies the constraint +func (c *constraint) check(version string, comparer Comparer) (bool, error) { + return c.operator(version, c.version, comparer) +} + +// String returns the original constraint string +func (c *constraint) String() string { + return c.original +} + +// Check returns true if the given version satisfies any of the constraints +// Multiple constraints are combined with AND logic +func (cs *Constraints) Check(version string) (bool, error) { + if version == "" { + return false, xerrors.New("version is empty") + } + + if len(cs.constraints) == 0 { + return false, xerrors.New("no constraints specified") + } + + for _, c := range cs.constraints { + satisfied, err := c.check(version, cs.comparer) + if err != nil { + return false, err + } + if !satisfied { + return false, nil + } + } + + return true, nil +} + +// String returns the string representation of constraints +func (cs *Constraints) String() string { + var strs []string + for _, c := range cs.constraints { + strs = append(strs, c.String()) + } + return strings.Join(strs, ", ") +} + +// Constraint operator functions + +func constraintEqual(v, c string, comparer Comparer) (bool, error) { + result, err := comparer.Compare(v, c) + if err != nil { + return false, err + } + return result == 0, nil +} + +func constraintNotEqual(v, c string, comparer Comparer) (bool, error) { + result, err := comparer.Compare(v, c) + if err != nil { + return false, err + } + return result != 0, nil +} + +func constraintGreaterThan(v, c string, comparer Comparer) (bool, error) { + result, err := comparer.Compare(v, c) + if err != nil { + return false, err + } + return result > 0, nil +} + +func constraintLessThan(v, c string, comparer Comparer) (bool, error) { + result, err := comparer.Compare(v, c) + if err != nil { + return false, err + } + return result < 0, nil +} + +func constraintGreaterThanEqual(v, c string, comparer Comparer) (bool, error) { + result, err := comparer.Compare(v, c) + if err != nil { + return false, err + } + return result >= 0, nil +} + +func constraintLessThanEqual(v, c string, comparer Comparer) (bool, error) { + result, err := comparer.Compare(v, c) + if err != nil { + return false, err + } + return result <= 0, nil +} diff --git a/pkg/detector/ospkg/version/constraint_test.go b/pkg/detector/ospkg/version/constraint_test.go new file mode 100644 index 000000000000..057581a78542 --- /dev/null +++ b/pkg/detector/ospkg/version/constraint_test.go @@ -0,0 +1,320 @@ +package version_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/detector/ospkg/version" +) + +func TestNewConstraints(t *testing.T) { + tests := []struct { + name string + constraints string + wantErr bool + }{ + { + name: "empty constraint returns error", + constraints: "", + wantErr: true, + }, + { + name: "single constraint with operator", + constraints: ">=1.2.3", + wantErr: false, + }, + { + name: "single constraint without operator", + constraints: "1.2.3", + wantErr: false, + }, + { + name: "multiple constraints with comma", + constraints: ">=1.2.3, <2.0.0", + wantErr: false, + }, + { + name: "multiple constraints with space", + constraints: ">=1.2.3 <2.0.0", + wantErr: false, + }, + { + name: "mixed operators", + constraints: ">1.0.0, <=2.0.0, ==1.5.0", + wantErr: false, + }, + { + name: "invalid constraint format", + constraints: ">>>1.2.3", + wantErr: true, + }, + { + name: "constraints with extra spaces", + constraints: " >=1.2.3 , <2.0.0 ", + wantErr: false, + }, + { + name: "not equal operator", + constraints: "!=1.2.3", + wantErr: false, + }, + { + name: "equal operator variations", + constraints: "=1.2.3, ==1.2.4", + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := version.NewConstraints(tt.constraints, version.NewDEBComparer()) + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} + +func TestConstraints_Check(t *testing.T) { + tests := []struct { + name string + constraints string + version string + want bool + wantErr bool + }{ + { + name: "empty version returns error", + constraints: ">=1.2.3", + version: "", + wantErr: true, + }, + { + name: "equal constraint satisfied", + constraints: "1.2.3", + version: "1.2.3", + want: true, + }, + { + name: "equal constraint not satisfied", + constraints: "1.2.3", + version: "1.2.4", + want: false, + }, + { + name: "greater than satisfied", + constraints: ">1.2.3", + version: "1.2.4", + want: true, + }, + { + name: "greater than not satisfied", + constraints: ">1.2.3", + version: "1.2.3", + want: false, + }, + { + name: "greater than not satisfied (lower)", + constraints: ">1.2.3", + version: "1.2.2", + want: false, + }, + { + name: "greater than or equal satisfied (equal)", + constraints: ">=1.2.3", + version: "1.2.3", + want: true, + }, + { + name: "greater than or equal satisfied (greater)", + constraints: ">=1.2.3", + version: "1.2.4", + want: true, + }, + { + name: "less than satisfied", + constraints: "<2.0.0", + version: "1.9.9", + want: true, + }, + { + name: "less than not satisfied", + constraints: "<2.0.0", + version: "2.0.0", + want: false, + }, + { + name: "less than or equal satisfied (equal)", + constraints: "<=2.0.0", + version: "2.0.0", + want: true, + }, + { + name: "less than or equal satisfied (less)", + constraints: "<=2.0.0", + version: "1.9.9", + want: true, + }, + { + name: "not equal satisfied", + constraints: "!=1.2.3", + version: "1.2.4", + want: true, + }, + { + name: "not equal not satisfied", + constraints: "!=1.2.3", + version: "1.2.3", + want: false, + }, + { + name: "multiple constraints AND logic (before first constraint)", + constraints: ">=1.0.0, <2.0.0", + version: "0.9.0", + want: false, + }, + { + name: "multiple constraints AND logic (after second constraint)", + constraints: ">=1.0.0, <2.0.0", + version: "2.1.0", + want: false, + }, + { + name: "multiple constraints AND logic (satisfied)", + constraints: ">=1.0.0, <2.0.0", + version: "1.5.0", + want: true, + }, + { + name: "range constraint (satisfied)", + constraints: ">=1.2.3, <2.0.0", + version: "1.5.0", + want: true, + }, + { + name: "multiple constraints with space separator", + constraints: ">=1.2.3 <2.0.0", + version: "1.5.0", + want: true, + }, + { + name: "debian version with revision", + constraints: ">=1.2.3-1", + version: "1.2.3-2", + want: true, + }, + { + name: "debian version with epoch", + constraints: ">=1:1.2.3", + version: "1:1.2.4", + want: true, + }, + { + name: "debian version with epoch and revision", + constraints: ">=1:1.2.3-1", + version: "1:1.2.3-2", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + comparer := version.NewDEBComparer() + constraints, err := version.NewConstraints(tt.constraints, comparer) + require.NoError(t, err) + + got, err := constraints.Check(tt.version) + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestConstraints_String(t *testing.T) { + tests := []struct { + name string + constraints string + want string + }{ + { + name: "single constraint", + constraints: ">=1.2.3", + want: ">=1.2.3", + }, + { + name: "multiple constraints", + constraints: ">=1.2.3, <2.0.0", + want: ">=1.2.3, <2.0.0", + }, + { + name: "constraints with extra spaces", + constraints: " >=1.2.3 , <2.0.0 ", + want: ">=1.2.3, <2.0.0", + }, + { + name: "space separated constraints", + constraints: ">=1.2.3 <2.0.0", + want: ">=1.2.3, <2.0.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + comparer := version.NewDEBComparer() + constraints, err := version.NewConstraints(tt.constraints, comparer) + require.NoError(t, err) + + got := constraints.String() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestConstraints_CheckWithAPKComparer(t *testing.T) { + tests := []struct { + name string + constraints string + version string + want bool + }{ + { + name: "alpine version comparison", + constraints: ">=1.2.3-r0", + version: "1.2.3-r1", + want: true, + }, + { + name: "alpine version with pre-release", + constraints: "<1.2.3_pre1", + version: "1.2.2", + want: true, + }, + { + name: "alpine version range", + constraints: ">=1.2.3-r0, <2.0.0", + version: "1.5.0-r2", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + comparer := version.NewAPKComparer() + constraints, err := version.NewConstraints(tt.constraints, comparer) + require.NoError(t, err) + + got, err := constraints.Check(tt.version) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/detector/ospkg/version/version.go b/pkg/detector/ospkg/version/version.go new file mode 100644 index 000000000000..8ed344f1f3ee --- /dev/null +++ b/pkg/detector/ospkg/version/version.go @@ -0,0 +1,39 @@ +package version + +import ( + "context" + "strings" + "time" + + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +// Major returns the major version +// e.g. 8.1 => 8 +func Major(osVer string) string { + osVer, _, _ = strings.Cut(osVer, ".") + return osVer +} + +// Minor returns the major and minor version +// e.g. 3.17.2 => 3.17 +func Minor(osVer string) string { + major, s, ok := strings.Cut(osVer, ".") + if !ok { + return osVer + } + minor, _, _ := strings.Cut(s, ".") + return major + "." + minor +} + +func Supported(ctx context.Context, eolDates map[string]time.Time, osFamily ftypes.OSType, osVer string) bool { + eol, ok := eolDates[osVer] + if !ok { + log.Warn("This OS version is not on the EOL list", + log.String("family", string(osFamily)), log.String("version", osVer)) + return true // can be the latest version + } + return clock.Now(ctx).Before(eol) +} diff --git a/pkg/detector/ospkg/wolfi/testdata/fixtures/data-source.yaml b/pkg/detector/ospkg/wolfi/testdata/fixtures/data-source.yaml new file mode 100644 index 000000000000..476d6744851d --- /dev/null +++ b/pkg/detector/ospkg/wolfi/testdata/fixtures/data-source.yaml @@ -0,0 +1,7 @@ +- bucket: data-source + pairs: + - key: wolfi + value: + ID: "wolfi" + Name: "Wolfi Secdb" + URL: "https://packages.wolfi.dev/os/security.json" \ No newline at end of file diff --git a/pkg/detector/ospkg/wolfi/testdata/fixtures/invalid.yaml b/pkg/detector/ospkg/wolfi/testdata/fixtures/invalid.yaml new file mode 100644 index 000000000000..683c60727f2d --- /dev/null +++ b/pkg/detector/ospkg/wolfi/testdata/fixtures/invalid.yaml @@ -0,0 +1,9 @@ +- bucket: wolfi + pairs: + - bucket: jq + pairs: + - key: CVE-2020-8177 + value: + FixedVersion: + - foo + - bar diff --git a/pkg/detector/ospkg/wolfi/testdata/fixtures/wolfi.yaml b/pkg/detector/ospkg/wolfi/testdata/fixtures/wolfi.yaml new file mode 100644 index 000000000000..5df51800a08d --- /dev/null +++ b/pkg/detector/ospkg/wolfi/testdata/fixtures/wolfi.yaml @@ -0,0 +1,39 @@ +- bucket: wolfi + pairs: + - bucket: ansible + pairs: + - key: CVE-2018-10875 + value: + FixedVersion: "2.6.3-r0" + - key: CVE-2019-10217 + value: + FixedVersion: "2.8.4-r0" + - key: CVE-2020-1740 + value: + FixedVersion: "" + AffectedVersion: "2.6.5" + - key: CVE-2021-20191 + value: + FixedVersion: "" + - key: CVE-2019-INVALID + value: + FixedVersion: "invalid" + - bucket: jq + pairs: + - key: CVE-2016-4074 + value: + FixedVersion: "1.6_rc1-r0" + - key: CVE-2019-9999 + value: + FixedVersion: "1.6_rc2" + - key: CVE-2020-1234 + value: + FixedVersion: "1.6-r1" + - bucket: test-src + pairs: + - key: CVE-2030-0001 + value: + FixedVersion: "0.1.0_alpha_pre2" + - key: CVE-2030-0002 + value: + FixedVersion: "0.1.0_alpha2" diff --git a/pkg/detector/ospkg/wolfi/wolfi.go b/pkg/detector/ospkg/wolfi/wolfi.go new file mode 100644 index 000000000000..18884468fb41 --- /dev/null +++ b/pkg/detector/ospkg/wolfi/wolfi.go @@ -0,0 +1,94 @@ +package wolfi + +import ( + "context" + + version "github.com/knqyf263/go-apk-version" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/wolfi" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Scanner implements the Wolfi scanner +type Scanner struct { + vs wolfi.VulnSrc +} + +// NewScanner is the factory method for Scanner +func NewScanner() *Scanner { + return &Scanner{ + vs: wolfi.NewVulnSrc(), + } +} + +// Detect vulnerabilities in package using Wolfi scanner +func (s *Scanner) Detect(ctx context.Context, _ string, _ *ftypes.Repository, pkgs []ftypes.Package) ([]types.DetectedVulnerability, error) { + log.InfoContext(ctx, "Detecting vulnerabilities...", log.Int("pkg_num", len(pkgs))) + + var vulns []types.DetectedVulnerability + for _, pkg := range pkgs { + srcName := pkg.SrcName + if srcName == "" { + srcName = pkg.Name + } + advisories, err := s.vs.Get(db.GetParams{ + PkgName: srcName, + }) + if err != nil { + return nil, xerrors.Errorf("failed to get Wolfi advisories: %w", err) + } + + installed := utils.FormatVersion(pkg) + installedVersion, err := version.NewVersion(installed) + if err != nil { + log.DebugContext(ctx, "Failed to parse the installed package version", + log.String("version", installed), log.Err(err)) + continue + } + + for _, adv := range advisories { + if !s.isVulnerable(ctx, installedVersion, adv) { + continue + } + vulns = append(vulns, types.DetectedVulnerability{ + VulnerabilityID: adv.VulnerabilityID, + PkgID: pkg.ID, + PkgName: pkg.Name, + InstalledVersion: installed, + FixedVersion: adv.FixedVersion, + Layer: pkg.Layer, + PkgIdentifier: pkg.Identifier, + Custom: adv.Custom, + DataSource: adv.DataSource, + }) + } + } + return vulns, nil +} + +func (s *Scanner) isVulnerable(ctx context.Context, installedVersion version.Version, adv dbTypes.Advisory) bool { + // Compare versions for fixed vulnerabilities + fixedVersion, err := version.NewVersion(adv.FixedVersion) + if err != nil { + log.DebugContext(ctx, "Failed to parse the fixed version", + log.String("version", adv.FixedVersion), log.Err(err)) + return false + } + + // It means the fixed vulnerability + return installedVersion.LessThan(fixedVersion) +} + +// IsSupportedVersion checks if the version is supported. +func (s *Scanner) IsSupportedVersion(_ context.Context, _ ftypes.OSType, _ string) bool { + // Wolfi doesn't have versions, so there is no case where a given input yields a + // result of an unsupported Wolfi version. + + return true +} diff --git a/pkg/detector/ospkg/wolfi/wolfi_test.go b/pkg/detector/ospkg/wolfi/wolfi_test.go new file mode 100644 index 000000000000..6ebd6c03d1bd --- /dev/null +++ b/pkg/detector/ospkg/wolfi/wolfi_test.go @@ -0,0 +1,210 @@ +package wolfi_test + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/detector/ospkg/wolfi" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanner_Detect(t *testing.T) { + type args struct { + repo *ftypes.Repository + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + fixtures []string + want []types.DetectedVulnerability + wantErr string + }{ + { + name: "happy path", + fixtures: []string{ + "testdata/fixtures/wolfi.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "ansible", + Version: "2.6.4", + SrcName: "ansible", + SrcVersion: "2.6.4", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + { + Name: "invalid", + Version: "invalid", // skipped + SrcName: "invalid", + SrcVersion: "invalid", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "ansible", + VulnerabilityID: "CVE-2019-10217", + InstalledVersion: "2.6.4", + FixedVersion: "2.8.4-r0", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Wolfi, + Name: "Wolfi Secdb", + URL: "https://packages.wolfi.dev/os/security.json", + }, + }, + }, + }, + { + name: "contain rc", + fixtures: []string{ + "testdata/fixtures/wolfi.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "jq", + VulnerabilityID: "CVE-2020-1234", + InstalledVersion: "1.6-r0", + FixedVersion: "1.6-r1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Wolfi, + Name: "Wolfi Secdb", + URL: "https://packages.wolfi.dev/os/security.json", + }, + }, + }, + }, + { + name: "contain pre", + fixtures: []string{ + "testdata/fixtures/wolfi.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "test", + Version: "0.1.0_alpha", + SrcName: "test-src", + SrcVersion: "0.1.0_alpha", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + }, + }, + want: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2030-0002", + PkgName: "test", + InstalledVersion: "0.1.0_alpha", + FixedVersion: "0.1.0_alpha2", + Layer: ftypes.Layer{ + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Wolfi, + Name: "Wolfi Secdb", + URL: "https://packages.wolfi.dev/os/security.json", + }, + }, + }, + }, + { + name: "Get returns an error", + fixtures: []string{ + "testdata/fixtures/invalid.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcName: "jq", + SrcVersion: "1.6-r0", + }, + }, + }, + wantErr: "failed to get Wolfi advisories", + }, + { + name: "No src name", + fixtures: []string{ + "testdata/fixtures/wolfi.yaml", + "testdata/fixtures/data-source.yaml", + }, + args: args{ + repo: &ftypes.Repository{ + Family: ftypes.Wolfi, + Release: "3.10", + }, + pkgs: []ftypes.Package{ + { + Name: "jq", + Version: "1.6-r0", + SrcVersion: "1.6-r0", + }, + }, + }, + want: []types.DetectedVulnerability{ + { + PkgName: "jq", + VulnerabilityID: "CVE-2020-1234", + InstalledVersion: "1.6-r0", + FixedVersion: "1.6-r1", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.Wolfi, + Name: "Wolfi Secdb", + URL: "https://packages.wolfi.dev/os/security.json", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + s := wolfi.NewScanner() + got, err := s.Detect(t.Context(), "", tt.args.repo, tt.args.pkgs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + sort.Slice(got, func(i, j int) bool { + return got[i].VulnerabilityID < got[j].VulnerabilityID + }) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/digest/digest.go b/pkg/digest/digest.go new file mode 100644 index 000000000000..f2b69407cab3 --- /dev/null +++ b/pkg/digest/digest.go @@ -0,0 +1,90 @@ +package digest + +import ( + "crypto/sha1" // nolint + "crypto/sha256" + "encoding/hex" + "fmt" + "hash" + "io" + "strings" + + "golang.org/x/xerrors" +) + +type Algorithm string + +func (a Algorithm) String() string { + return string(a) +} + +// supported digest types +const ( + SHA1 Algorithm = "sha1" // sha1 with hex encoding (lower case only) + SHA256 Algorithm = "sha256" // sha256 with hex encoding (lower case only) + SHA512 Algorithm = "sha512" + MD5 Algorithm = "md5" // md5 with hex encoding (lower case only) +) + +// Digest allows simple protection of hex formatted digest strings, prefixed by their algorithm. +// +// The following is an example of the contents of Digest types: +// +// sha256:7173b809ca12ec5dee4506cd86be934c4596dd234ee82c0662eac04a8c2c71dc +type Digest string + +// NewDigest returns a Digest from alg and a hash.Hash object. +func NewDigest(alg Algorithm, h hash.Hash) Digest { + return Digest(fmt.Sprintf("%s:%x", alg, h.Sum(nil))) +} + +// NewDigestFromString returns a Digest from alg and a string. +func NewDigestFromString(alg Algorithm, h string) Digest { + return Digest(fmt.Sprintf("%s:%s", alg, h)) +} + +func (d Digest) Algorithm() Algorithm { + return Algorithm(d[:d.sepIndex()]) +} + +func (d Digest) Encoded() string { + return string(d[d.sepIndex()+1:]) +} + +func (d Digest) String() string { + return string(d) +} + +func (d Digest) sepIndex() int { + i := max(strings.Index(string(d), ":"), 0) + return i +} + +func CalcSHA1(r io.ReadSeeker) (Digest, error) { + defer r.Seek(0, io.SeekStart) + + h := sha1.New() // nolint + if _, err := io.Copy(h, r); err != nil { + return "", xerrors.Errorf("unable to calculate sha1 digest: %w", err) + } + + return NewDigest(SHA1, h), nil +} + +// CalcSHA256 calculates the SHA256 hash of the given data +func CalcSHA256(data []byte) Digest { + h := sha256.Sum256(data) + return NewDigestFromString(SHA256, hex.EncodeToString(h[:])) +} + +// CalcSHA256FromReader calculates the SHA256 hash from a reader +func CalcSHA256FromReader(r io.ReadSeeker) (Digest, error) { + defer r.Seek(0, io.SeekStart) + + h := sha256.New() + if _, err := io.Copy(h, r); err != nil { + return "", xerrors.Errorf("unable to calculate sha256 digest: %w", err) + } + + return NewDigest(SHA256, h), nil +} diff --git a/pkg/downloader/download.go b/pkg/downloader/download.go new file mode 100644 index 000000000000..f47d0c981739 --- /dev/null +++ b/pkg/downloader/download.go @@ -0,0 +1,194 @@ +package downloader + +import ( + "cmp" + "context" + "errors" + "maps" + "net/http" + "net/url" + "os" + "strings" + "time" + + "github.com/google/go-github/v62/github" + "github.com/hashicorp/go-getter" + "github.com/samber/lo" + "golang.org/x/xerrors" + + xhttp "github.com/aquasecurity/trivy/pkg/x/http" + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +var ErrSkipDownload = errors.New("skip download") + +type Options struct { + Insecure bool + Auth Auth + ETag string + ClientMode getter.ClientMode +} + +type Auth struct { + Username string + Password string + Token string +} + +// DownloadToTempDir downloads the configured source to a temp dir. +func DownloadToTempDir(ctx context.Context, src string, opts Options) (string, error) { + tempDir, err := xos.MkdirTemp("", "download-") + if err != nil { + return "", xerrors.Errorf("failed to create a temp dir: %w", err) + } + + pwd, err := os.Getwd() + if err != nil { + return "", xerrors.Errorf("unable to get the current dir: %w", err) + } + + if _, err = Download(ctx, src, tempDir, pwd, opts); err != nil { + return "", xerrors.Errorf("download error: %w", err) + } + + return tempDir, nil +} + +// Download downloads the configured source to the destination. +func Download(ctx context.Context, src, dst, pwd string, opts Options) (string, error) { + // go-getter doesn't allow the dst directory already exists if the src is directory. + _ = os.RemoveAll(dst) + + var clientOpts []getter.ClientOption + if opts.Insecure { + clientOpts = append(clientOpts, getter.WithInsecure()) + } + + // Clone the global map so that it will not be accessed concurrently. + getters := maps.Clone(getter.Getters) + + // Overwrite the file getter so that a file will be copied + getters["file"] = &getter.FileGetter{Copy: true} + + // Since "httpGetter" is a global pointer and the state is shared, + // once it is executed without "WithInsecure()", + // it cannot enable WithInsecure() afterwards because its state is preserved. + // Therefore, we need to create a new "HttpGetter" instance every time. + // cf. https://github.com/hashicorp/go-getter/blob/5a63fd9c0d5b8da8a6805e8c283f46f0dacb30b3/get.go#L63-L65 + transport := NewCustomTransport(opts) + httpGetter := &getter.HttpGetter{ + Netrc: true, + Client: &http.Client{ + Transport: transport, + Timeout: time.Minute * 5, + }, + } + getters["http"] = httpGetter + getters["https"] = httpGetter + + // Build the client + client := &getter.Client{ + Ctx: ctx, + Src: src, + Dst: dst, + Pwd: pwd, + Getters: getters, + Mode: lo.Ternary(opts.ClientMode == 0, getter.ClientModeAny, opts.ClientMode), + Options: clientOpts, + } + + if err := client.Get(); err != nil { + return "", xerrors.Errorf("failed to download %s: %w", src, err) + } + + return transport.newETag, nil +} + +type CustomTransport struct { + insecure bool + auth Auth + cachedETag string + newETag string +} + +func NewCustomTransport(opts Options) *CustomTransport { + return &CustomTransport{ + insecure: opts.Insecure, + auth: opts.Auth, + cachedETag: opts.ETag, + } +} + +func (t *CustomTransport) RoundTrip(req *http.Request) (*http.Response, error) { + if t.cachedETag != "" { + req.Header.Set("If-None-Match", t.cachedETag) + } + if t.auth.Token != "" { + req.Header.Set("Authorization", "Bearer "+t.auth.Token) + } else if t.auth.Username != "" || t.auth.Password != "" { + req.SetBasicAuth(t.auth.Username, t.auth.Password) + } + + transport := xhttp.RoundTripper(req.Context(), xhttp.WithInsecure(t.insecure)) + if req.URL.Host == "github.com" { + transport = NewGitHubTransport(req.URL, t.auth.Token) + } + + res, err := transport.RoundTrip(req) + if err != nil { + return nil, xerrors.Errorf("failed to round trip: %w", err) + } + + switch res.StatusCode { + case http.StatusOK, http.StatusPartialContent: + // Update the ETag + t.newETag = res.Header.Get("ETag") + case http.StatusNotModified: + return nil, ErrSkipDownload + } + + return res, nil +} + +func NewGitHubTransport(u *url.URL, token string) http.RoundTripper { + client := newGitHubClient(token) + ss := strings.SplitN(u.Path, "/", 4) + if len(ss) < 4 || strings.HasPrefix(ss[3], "archive/") || strings.HasPrefix(ss[3], "releases/") || + strings.HasPrefix(ss[3], "tags/") { + // Use the default transport from go-github for authentication + return client.Client().Transport + } + + return &GitHubContentTransport{ + owner: ss[1], + repo: ss[2], + filePath: ss[3], + client: client, + } +} + +// GitHubContentTransport is a round tripper for downloading the GitHub content. +type GitHubContentTransport struct { + owner string + repo string + filePath string + client *github.Client +} + +// RoundTrip calls the GitHub API to download the content. +func (t *GitHubContentTransport) RoundTrip(req *http.Request) (*http.Response, error) { + _, res, err := t.client.Repositories.DownloadContents(req.Context(), t.owner, t.repo, t.filePath, nil) + if err != nil { + return nil, xerrors.Errorf("failed to get the file content: %w", err) + } + return res.Response, nil +} + +func newGitHubClient(token string) *github.Client { + client := github.NewClient(xhttp.Client()) + token = cmp.Or(token, os.Getenv("GITHUB_TOKEN")) + if token != "" { + client = client.WithAuthToken(token) + } + return client +} diff --git a/pkg/downloader/downloader_test.go b/pkg/downloader/downloader_test.go new file mode 100644 index 000000000000..14eb23caf9c7 --- /dev/null +++ b/pkg/downloader/downloader_test.go @@ -0,0 +1,68 @@ +package downloader_test + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/downloader" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" +) + +func TestDownload(t *testing.T) { + // Set up a test server with a self-signed certificate + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write([]byte("test content")) + assert.NoError(t, err) + })) + defer server.Close() + + tests := []struct { + name string + insecure bool + wantErr bool + }{ + { + "Secure (should fail)", + false, + true, + }, + { + "Insecure (should succeed)", + true, + false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up the destination path + dst := t.TempDir() + + // Configure the HTTP transport with the insecure option + ctx := xhttp.WithTransport(t.Context(), xhttp.NewTransport(xhttp.Options{ + Insecure: tt.insecure, + })) + + // Execute the download + _, err := downloader.Download(ctx, server.URL, dst, "", downloader.Options{ + Insecure: tt.insecure, + }) + + if tt.wantErr { + assert.Error(t, err) + return + } + require.NoError(t, err) + + // Check the content of the downloaded file + content, err := os.ReadFile(dst) + require.NoError(t, err) + assert.Equal(t, "test content", string(content)) + }) + } +} diff --git a/pkg/extension/hook.go b/pkg/extension/hook.go new file mode 100644 index 000000000000..ebd72f07aaba --- /dev/null +++ b/pkg/extension/hook.go @@ -0,0 +1,162 @@ +package extension + +import ( + "context" + "sort" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/types" +) + +var hooks = make(map[string]Hook) + +func RegisterHook(s Hook) { + // Avoid duplication + hooks[s.Name()] = s +} + +func DeregisterHook(name string) { + delete(hooks, name) +} + +// Hook is an interface that defines the methods for a hook. +type Hook interface { + // Name returns the name of the extension. + Name() string +} + +// RunHook is a extension that is called before and after all the processes. +type RunHook interface { + Hook + + // PreRun is called before all the processes. + PreRun(ctx context.Context, opts flag.Options) error + + // PostRun is called after all the processes. + PostRun(ctx context.Context, opts flag.Options) error +} + +// ScanHook is a extension that is called before and after the scan. +type ScanHook interface { + Hook + + // PreScan is called before the scan. It can modify the scan target. + // It may be called on the server side in client/server mode. + PreScan(ctx context.Context, target *types.ScanTarget, opts types.ScanOptions) error + + // PostScan is called after the scan. It can modify the results. + // It may be called on the server side in client/server mode. + // NOTE: Wasm modules cannot directly modify the passed results, + // so it returns a copy of the results. + PostScan(ctx context.Context, results types.Results) (types.Results, error) +} + +// ReportHook is a extension that is called before and after the report is written. +type ReportHook interface { + Hook + + // PreReport is called before the report is written. + // It can modify the report. It is called on the client side. + PreReport(ctx context.Context, report *types.Report, opts flag.Options) error + + // PostReport is called after the report is written. + // It can modify the report. It is called on the client side. + PostReport(ctx context.Context, report *types.Report, opts flag.Options) error +} + +func PreRun(ctx context.Context, opts flag.Options) error { + for _, e := range Hooks() { + h, ok := e.(RunHook) + if !ok { + continue + } + if err := h.PreRun(ctx, opts); err != nil { + return xerrors.Errorf("%s pre run error: %w", e.Name(), err) + } + } + return nil +} + +// PostRun is a hook that is called after all the processes. +func PostRun(ctx context.Context, opts flag.Options) error { + for _, e := range Hooks() { + h, ok := e.(RunHook) + if !ok { + continue + } + if err := h.PostRun(ctx, opts); err != nil { + return xerrors.Errorf("%s post run error: %w", e.Name(), err) + } + } + return nil +} + +// PreScan is a hook that is called before the scan. +func PreScan(ctx context.Context, target *types.ScanTarget, options types.ScanOptions) error { + for _, e := range Hooks() { + h, ok := e.(ScanHook) + if !ok { + continue + } + if err := h.PreScan(ctx, target, options); err != nil { + return xerrors.Errorf("%s pre scan error: %w", e.Name(), err) + } + } + return nil +} + +// PostScan is a hook that is called after the scan. +func PostScan(ctx context.Context, results types.Results) (types.Results, error) { + var err error + for _, e := range Hooks() { + h, ok := e.(ScanHook) + if !ok { + continue + } + results, err = h.PostScan(ctx, results) + if err != nil { + return nil, xerrors.Errorf("%s post scan error: %w", e.Name(), err) + } + } + return results, nil +} + +// PreReport is a hook that is called before the report is written. +func PreReport(ctx context.Context, report *types.Report, opts flag.Options) error { + for _, e := range Hooks() { + h, ok := e.(ReportHook) + if !ok { + continue + } + if err := h.PreReport(ctx, report, opts); err != nil { + return xerrors.Errorf("%s pre report error: %w", e.Name(), err) + } + } + return nil +} + +// PostReport is a hook that is called after the report is written. +func PostReport(ctx context.Context, report *types.Report, opts flag.Options) error { + for _, e := range Hooks() { + h, ok := e.(ReportHook) + if !ok { + continue + } + if err := h.PostReport(ctx, report, opts); err != nil { + return xerrors.Errorf("%s post report error: %w", e.Name(), err) + } + } + return nil +} + +// Hooks returns the list of hooks. +func Hooks() []Hook { + hooks := lo.Values(hooks) + sort.Slice(hooks, func(i, j int) bool { + return hooks[i].Name() < hooks[j].Name() + }) + return hooks +} diff --git a/pkg/extension/hook_test.go b/pkg/extension/hook_test.go new file mode 100644 index 000000000000..c90828f585aa --- /dev/null +++ b/pkg/extension/hook_test.go @@ -0,0 +1,278 @@ +package extension_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/internal/hooktest" + "github.com/aquasecurity/trivy/pkg/extension" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestPostScan(t *testing.T) { + tests := []struct { + name string + results types.Results + want types.Results + wantErr bool + }{ + { + name: "happy path", + results: types.Results{ + { + Target: "test", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0001", + PkgName: "musl", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: "CRITICAL", + }, + }, + }, + }, + }, + want: types.Results{ + { + Target: "test", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0001", + PkgName: "musl", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: "CRITICAL", + References: []string{ + "https://example.com/post-scan", + }, + }, + }, + }, + }, + }, + }, + { + name: "sad path", + results: types.Results{ + { + Target: "bad", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize the test hook + hooktest.Init(t) + + results, err := extension.PostScan(t.Context(), tt.results) + require.Equal(t, tt.wantErr, err != nil) + assert.Equal(t, tt.want, results) + }) + } +} + +func TestPreScan(t *testing.T) { + tests := []struct { + name string + target *types.ScanTarget + options types.ScanOptions + wantErr bool + }{ + { + name: "happy path", + target: &types.ScanTarget{ + Name: "test", + }, + wantErr: false, + }, + { + name: "sad path", + target: &types.ScanTarget{ + Name: "bad-pre", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize the test hook + hooktest.Init(t) + + err := extension.PreScan(t.Context(), tt.target, tt.options) + require.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func TestPreRun(t *testing.T) { + tests := []struct { + name string + opts flag.Options + wantErr bool + }{ + { + name: "happy path", + opts: flag.Options{}, + wantErr: false, + }, + { + name: "sad path", + opts: flag.Options{ + GlobalOptions: flag.GlobalOptions{ + ConfigFile: "bad-config", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize the test hook + hooktest.Init(t) + + err := extension.PreRun(t.Context(), tt.opts) + require.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func TestPostRun(t *testing.T) { + tests := []struct { + name string + opts flag.Options + wantErr bool + }{ + { + name: "happy path", + opts: flag.Options{}, + wantErr: false, + }, + { + name: "sad path", + opts: flag.Options{ + GlobalOptions: flag.GlobalOptions{ + ConfigFile: "bad-config", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize the test extension + hooktest.Init(t) + + err := extension.PostRun(t.Context(), tt.opts) + require.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func TestPreReport(t *testing.T) { + tests := []struct { + name string + report *types.Report + opts flag.Options + wantTitle string + wantErr bool + }{ + { + name: "happy path", + report: &types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0001", + }, + }, + }, + }, + }, + wantTitle: "Modified by pre-report hook", + wantErr: false, + }, + { + name: "sad path", + report: &types.Report{ + ArtifactName: "bad-report", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize the test hook + hooktest.Init(t) + + err := extension.PreReport(t.Context(), tt.report, tt.opts) + if tt.wantErr { + require.Error(t, err) + return + } + + require.Len(t, tt.report.Results, 1) + require.Len(t, tt.report.Results[0].Vulnerabilities, 1) + assert.Equal(t, tt.wantTitle, tt.report.Results[0].Vulnerabilities[0].Title) + }) + } +} + +func TestPostReport(t *testing.T) { + tests := []struct { + name string + report *types.Report + opts flag.Options + wantDescription string + wantErr bool + }{ + { + name: "happy path", + report: &types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0001", + }, + }, + }, + }, + }, + wantDescription: "Modified by post-report hook", + wantErr: false, + }, + { + name: "sad path", + report: &types.Report{ + ArtifactName: "bad-report", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize the test hook + hooktest.Init(t) + + err := extension.PostReport(t.Context(), tt.report, tt.opts) + if tt.wantErr { + require.Error(t, err) + return + } + + require.Len(t, tt.report.Results, 1) + require.Len(t, tt.report.Results[0].Vulnerabilities, 1) + assert.Equal(t, tt.wantDescription, tt.report.Results[0].Vulnerabilities[0].Description) + }) + } +} diff --git a/pkg/fanal/analyzer/all/import.go b/pkg/fanal/analyzer/all/import.go new file mode 100644 index 000000000000..f4b2ff904eb1 --- /dev/null +++ b/pkg/fanal/analyzer/all/import.go @@ -0,0 +1,56 @@ +package all + +import ( + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/buildinfo" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/all" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/executable" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/imgconf/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/imgconf/dockerfile" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/imgconf/secret" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/c/conan" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/conda/environment" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/conda/meta" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/dart/pub" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/dotnet/deps" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/dotnet/nuget" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/dotnet/packagesprops" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/elixir/mix" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/golang/binary" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/golang/mod" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/java/gradle" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/java/jar" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/java/pom" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/java/sbt" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/julia/pkg" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/bun" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/npm" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/pkg" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/pnpm" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/yarn" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/php/composer" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/packaging" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/pip" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/pipenv" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/poetry" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/uv" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/bundler" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/gemspec" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/rust/binary" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/rust/cargo" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/swift/cocoapods" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/swift/swift" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/licensing" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/alpine" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/amazonlinux" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/debian" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/redhatbase" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/release" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/ubuntu" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/bottlerocket_inventory" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/dpkg" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/rpm" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/repo/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/sbom" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret" +) diff --git a/pkg/fanal/analyzer/analyzer.go b/pkg/fanal/analyzer/analyzer.go new file mode 100644 index 000000000000..2b0afb8ab486 --- /dev/null +++ b/pkg/fanal/analyzer/analyzer.go @@ -0,0 +1,627 @@ +package analyzer + +import ( + "context" + "errors" + "io/fs" + "os" + "regexp" + "slices" + "sort" + "strings" + "sync" + + "github.com/samber/lo" + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" + "golang.org/x/xerrors" + + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/misconf" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +var ( + analyzers = make(map[Type]analyzer) + postAnalyzers = make(map[Type]postAnalyzerInitialize) + + // ErrUnknownOS occurs when unknown OS is analyzed. + ErrUnknownOS = xerrors.New("unknown OS") + // ErrPkgAnalysis occurs when the analysis of packages is failed. + ErrPkgAnalysis = xerrors.New("failed to analyze packages") + // ErrNoPkgsDetected occurs when the required files for an OS package manager are not detected + ErrNoPkgsDetected = xerrors.New("no packages detected") +) + +////////////////////// +// Analyzer options // +////////////////////// + +// AnalyzerOptions is used to initialize analyzers +type AnalyzerOptions struct { + Group Group + Parallel int + FilePatterns []string + DisabledAnalyzers []Type + DetectionPriority types.DetectionPriority + MisconfScannerOption misconf.ScannerOption + SecretScannerOption SecretScannerOption + LicenseScannerOption LicenseScannerOption +} + +type SecretScannerOption struct { + ConfigPath string +} + +type LicenseScannerOption struct { + // Use license classifier to get better results though the classification is expensive. + Full bool + ClassifierConfidenceLevel float64 +} + +//////////////// +// Interfaces // +//////////////// + +// Initializer represents analyzers that need to take parameters from users +type Initializer interface { + Init(AnalyzerOptions) error +} + +type analyzer interface { + Type() Type + Version() int + Analyze(ctx context.Context, input AnalysisInput) (*AnalysisResult, error) + Required(filePath string, info os.FileInfo) bool +} + +type PostAnalyzer interface { + Type() Type + Version() int + PostAnalyze(ctx context.Context, input PostAnalysisInput) (*AnalysisResult, error) + Required(filePath string, info os.FileInfo) bool +} + +//////////////////// +// Analyzer group // +//////////////////// + +type Group string + +const GroupBuiltin Group = "builtin" + +func RegisterAnalyzer(analyzer analyzer) { + if _, ok := analyzers[analyzer.Type()]; ok { + log.Fatal("Analyzer is registered twice", log.String("type", string(analyzer.Type()))) + } + analyzers[analyzer.Type()] = analyzer +} + +type postAnalyzerInitialize func(options AnalyzerOptions) (PostAnalyzer, error) + +func RegisterPostAnalyzer(t Type, initializer postAnalyzerInitialize) { + if _, ok := postAnalyzers[t]; ok { + log.Fatal("Analyzer is registered twice", log.String("type", string(t))) + } + postAnalyzers[t] = initializer +} + +// DeregisterAnalyzer is mainly for testing +func DeregisterAnalyzer(t Type) { + delete(analyzers, t) +} + +// CustomGroup returns a group name for custom analyzers +// This is mainly intended to be used in Aqua products. +type CustomGroup interface { + Group() Group +} + +// StaticPathAnalyzer is an interface for analyzers that can specify static file paths +// instead of traversing the entire filesystem. +type StaticPathAnalyzer interface { + // StaticPaths returns a list of static file paths to analyze + StaticPaths() []string +} + +type Opener func() (xio.ReadSeekCloserAt, error) + +type AnalyzerGroup struct { + logger *log.Logger + analyzers []analyzer + postAnalyzers []PostAnalyzer + filePatterns map[Type]FilePatterns + detectionPriority types.DetectionPriority +} + +/////////////////////////// +// Analyzer input/output // +/////////////////////////// + +type AnalysisInput struct { + Dir string + FilePath string + Info os.FileInfo + Content xio.ReadSeekerAt + + Options AnalysisOptions +} + +type PostAnalysisInput struct { + FS fs.FS + FilePatterns FilePatterns + Options AnalysisOptions +} + +type FilePatterns []*regexp.Regexp + +func (f FilePatterns) Match(filePath string) bool { + for _, pattern := range f { + if pattern.MatchString(filePath) { + return true + } + } + return false +} + +type AnalysisOptions struct { + Offline bool + FileChecksum bool +} + +type AnalysisResult struct { + m sync.Mutex + OS types.OS + Repository *types.Repository + PackageInfos []types.PackageInfo + Applications []types.Application + Misconfigurations []types.Misconfiguration + Secrets []types.Secret + Licenses []types.LicenseFile + SystemInstalledFiles []string // A list of files installed by OS package manager + + // Digests contains SHA-256 digests of unpackaged files + // used to search for SBOM attestation. + Digests map[string]string + + // For Red Hat + BuildInfo *types.BuildInfo + + // CustomResources hold analysis results from custom analyzers. + // It is for extensibility and not used in OSS. + CustomResources []types.CustomResource +} + +func NewAnalysisResult() *AnalysisResult { + result := new(AnalysisResult) + return result +} + +func (r *AnalysisResult) isEmpty() bool { + return lo.IsEmpty(r.OS) && r.Repository == nil && len(r.PackageInfos) == 0 && len(r.Applications) == 0 && + len(r.Misconfigurations) == 0 && len(r.Secrets) == 0 && len(r.Licenses) == 0 && len(r.SystemInstalledFiles) == 0 && + r.BuildInfo == nil && len(r.Digests) == 0 && len(r.CustomResources) == 0 +} + +func (r *AnalysisResult) Sort() { + // OS packages + sort.Slice(r.PackageInfos, func(i, j int) bool { + return r.PackageInfos[i].FilePath < r.PackageInfos[j].FilePath + }) + + for _, pi := range r.PackageInfos { + sort.Sort(pi.Packages) + } + + // Language-specific packages + sort.Slice(r.Applications, func(i, j int) bool { + if r.Applications[i].FilePath != r.Applications[j].FilePath { + return r.Applications[i].FilePath < r.Applications[j].FilePath + } + return r.Applications[i].Type < r.Applications[j].Type + }) + + for _, app := range r.Applications { + sort.Sort(app.Packages) + } + + // Custom resources + sort.Slice(r.CustomResources, func(i, j int) bool { + return r.CustomResources[i].FilePath < r.CustomResources[j].FilePath + }) + + // Misconfigurations + sort.Slice(r.Misconfigurations, func(i, j int) bool { + if r.Misconfigurations[i].FileType != r.Misconfigurations[j].FileType { + return r.Misconfigurations[i].FileType < r.Misconfigurations[j].FileType + } + return r.Misconfigurations[i].FilePath < r.Misconfigurations[j].FilePath + }) + + // Secrets + sort.Slice(r.Secrets, func(i, j int) bool { + return r.Secrets[i].FilePath < r.Secrets[j].FilePath + }) + for _, sec := range r.Secrets { + sort.Slice(sec.Findings, func(i, j int) bool { + if sec.Findings[i].RuleID != sec.Findings[j].RuleID { + return sec.Findings[i].RuleID < sec.Findings[j].RuleID + } + return sec.Findings[i].StartLine < sec.Findings[j].StartLine + }) + } + + // License files + sort.Slice(r.Licenses, func(i, j int) bool { + if r.Licenses[i].Type == r.Licenses[j].Type { + if r.Licenses[i].FilePath == r.Licenses[j].FilePath { + return r.Licenses[i].Layer.DiffID < r.Licenses[j].Layer.DiffID + } + return r.Licenses[i].FilePath < r.Licenses[j].FilePath + } + + return r.Licenses[i].Type < r.Licenses[j].Type + }) +} + +func (r *AnalysisResult) Merge(newResult *AnalysisResult) { + if newResult == nil || newResult.isEmpty() { + return + } + + // this struct is accessed by multiple goroutines + r.m.Lock() + defer r.m.Unlock() + + r.OS.Merge(newResult.OS) + + if newResult.Repository != nil { + r.Repository = newResult.Repository + } + + if len(newResult.PackageInfos) > 0 { + r.PackageInfos = append(r.PackageInfos, newResult.PackageInfos...) + } + + if len(newResult.Applications) > 0 { + normalizeApplicationsLicenses(newResult.Applications) + r.Applications = append(r.Applications, newResult.Applications...) + } + + // Merge SHA-256 digests of unpackaged files + if newResult.Digests != nil { + r.Digests = lo.Assign(r.Digests, newResult.Digests) + } + + r.Misconfigurations = append(r.Misconfigurations, newResult.Misconfigurations...) + r.Secrets = append(r.Secrets, newResult.Secrets...) + r.Licenses = append(r.Licenses, newResult.Licenses...) + r.SystemInstalledFiles = append(r.SystemInstalledFiles, newResult.SystemInstalledFiles...) + + if newResult.BuildInfo != nil { + if r.BuildInfo == nil { + r.BuildInfo = newResult.BuildInfo + } else { + // We don't need to merge build info here + // because there is theoretically only one file about build info in each layer. + if newResult.BuildInfo.Nvr != "" || newResult.BuildInfo.Arch != "" { + r.BuildInfo.Nvr = newResult.BuildInfo.Nvr + r.BuildInfo.Arch = newResult.BuildInfo.Arch + } + if len(newResult.BuildInfo.ContentSets) > 0 { + r.BuildInfo.ContentSets = newResult.BuildInfo.ContentSets + } + } + } + + r.CustomResources = append(r.CustomResources, newResult.CustomResources...) +} + +// setAnalyzedBy sets the AnalyzedBy field for all packages in the result. +func (r *AnalysisResult) setAnalyzedBy(analyzerType Type) { + if r == nil { + return + } + for i := range r.PackageInfos { + for j := range r.PackageInfos[i].Packages { + r.PackageInfos[i].Packages[j].AnalyzedBy = analyzerType + } + } + for i := range r.Applications { + for j := range r.Applications[i].Packages { + r.Applications[i].Packages[j].AnalyzedBy = analyzerType + } + } +} + +// analyze runs the analyzer and sets AnalyzedBy on the result. +func (ag AnalyzerGroup) analyze(ctx context.Context, a analyzer, input AnalysisInput) (*AnalysisResult, error) { + result, err := a.Analyze(ctx, input) + if err != nil { + return nil, err + } + result.setAnalyzedBy(a.Type()) + return result, nil +} + +// postAnalyze runs the post-analyzer and sets AnalyzedBy on the result. +func (ag AnalyzerGroup) postAnalyze(ctx context.Context, a PostAnalyzer, input PostAnalysisInput) (*AnalysisResult, error) { + result, err := a.PostAnalyze(ctx, input) + if err != nil { + return nil, err + } + result.setAnalyzedBy(a.Type()) + return result, nil +} + +func belongToGroup(groupName Group, analyzerType Type, disabledAnalyzers []Type, analyzer any) bool { + if slices.Contains(disabledAnalyzers, analyzerType) { + return false + } + + analyzerGroupName := GroupBuiltin + if cg, ok := analyzer.(CustomGroup); ok { + analyzerGroupName = cg.Group() + } + if analyzerGroupName != groupName { + return false + } + + return true +} + +func normalizeApplicationsLicenses(applications []types.Application) { + for i, app := range applications { + for j, pkg := range app.Packages { + applications[i].Packages[j].Licenses = licensing.NormalizeLicenses(pkg.Licenses) + } + } +} + +const separator = ":" + +func NewAnalyzerGroup(opts AnalyzerOptions) (AnalyzerGroup, error) { + groupName := opts.Group + if groupName == "" { + groupName = GroupBuiltin + } + + group := AnalyzerGroup{ + logger: log.WithPrefix("analyzer"), + filePatterns: make(map[Type]FilePatterns), + detectionPriority: opts.DetectionPriority, + } + for _, p := range opts.FilePatterns { + // e.g. "dockerfile:my_dockerfile_*" + s := strings.SplitN(p, separator, 2) + if len(s) != 2 { + return group, xerrors.Errorf("invalid file pattern (%s) expected format: \"fileType:regexPattern\" e.g. \"dockerfile:my_dockerfile_*\"", p) + } + + fileType, pattern := s[0], s[1] + r, err := regexp.Compile(pattern) + if err != nil { + return group, xerrors.Errorf("invalid file regexp (%s): %w", p, err) + } + + group.filePatterns[Type(fileType)] = append(group.filePatterns[Type(fileType)], r) + } + + for analyzerType, a := range analyzers { + if !belongToGroup(groupName, analyzerType, opts.DisabledAnalyzers, a) { + continue + } + // Initialize only scanners that have Init() + if ini, ok := a.(Initializer); ok { + if err := ini.Init(opts); err != nil { + return AnalyzerGroup{}, xerrors.Errorf("analyzer initialization error: %w", err) + } + } + group.analyzers = append(group.analyzers, a) + } + + for analyzerType, init := range postAnalyzers { + a, err := init(opts) + if err != nil { + return AnalyzerGroup{}, xerrors.Errorf("post-analyzer init error: %w", err) + } + if !belongToGroup(groupName, analyzerType, opts.DisabledAnalyzers, a) { + continue + } + group.postAnalyzers = append(group.postAnalyzers, a) + } + + return group, nil +} + +type Versions struct { + Analyzers map[string]int + PostAnalyzers map[string]int +} + +// AnalyzerVersions returns analyzer version identifier used for cache keys. +func (ag AnalyzerGroup) AnalyzerVersions() Versions { + analyzerVersions := make(map[string]int) + for _, a := range ag.analyzers { + analyzerVersions[string(a.Type())] = a.Version() + } + postAnalyzerVersions := make(map[string]int) + for _, a := range ag.postAnalyzers { + postAnalyzerVersions[string(a.Type())] = a.Version() + } + return Versions{ + Analyzers: analyzerVersions, + PostAnalyzers: postAnalyzerVersions, + } +} + +// AnalyzeFile determines which files are required by the analyzers based on the file name and attributes, +// and passes only those files to the analyzer for analysis. +// This function may be called concurrently and must be thread-safe. +func (ag AnalyzerGroup) AnalyzeFile(ctx context.Context, eg *errgroup.Group, limit *semaphore.Weighted, result *AnalysisResult, + dir, filePath string, info os.FileInfo, opener Opener, disabled []Type, opts AnalysisOptions) error { + if info.IsDir() { + return nil + } + + // filepath extracted from tar file doesn't have the prefix "/" + cleanPath := strings.TrimLeft(filePath, "/") + + for _, a := range ag.analyzers { + // Skip disabled analyzers + if slices.Contains(disabled, a.Type()) { + continue + } + + if !ag.filePatterns[a.Type()].Match(cleanPath) && !a.Required(cleanPath, info) { + continue + } + rc, err := opener() + if errors.Is(err, fs.ErrPermission) { + ag.logger.Debug("Permission error", log.FilePath(filePath)) + break + } else if err != nil { + return xerrors.Errorf("unable to open %s: %w", filePath, err) + } + + if err = limit.Acquire(ctx, 1); err != nil { + return xerrors.Errorf("semaphore acquire: %w", err) + } + + eg.Go(func() error { + defer limit.Release(1) + defer rc.Close() + + ret, analyzeErr := ag.analyze(ctx, a, AnalysisInput{ + Dir: dir, + FilePath: filePath, + Info: info, + Content: rc, + Options: opts, + }) + if analyzeErr != nil { + switch { + case errors.Is(analyzeErr, fos.AnalyzeOSError): + // The OS could not be detected. + case errors.Is(analyzeErr, context.DeadlineExceeded): + return xerrors.Errorf("analyzer timed out: %w", analyzeErr) + default: + ag.logger.Debug("Analysis error", log.Err(err)) + return nil + } + } + result.Merge(ret) + return nil + }) + } + + return nil +} + +// RequiredPostAnalyzers returns a list of analyzer types that require the given file. +func (ag AnalyzerGroup) RequiredPostAnalyzers(filePath string, info os.FileInfo) []Type { + if info.IsDir() { + return nil + } + var postAnalyzerTypes []Type + for _, a := range ag.postAnalyzers { + if ag.filePatterns[a.Type()].Match(filePath) || a.Required(filePath, info) { + postAnalyzerTypes = append(postAnalyzerTypes, a.Type()) + } + } + return postAnalyzerTypes +} + +// PostAnalyze passes a virtual filesystem containing only required files +// and passes it to the respective post-analyzer. +// The obtained results are merged into the "result". +// This function may be called concurrently and must be thread-safe. +func (ag AnalyzerGroup) PostAnalyze(ctx context.Context, compositeFS *CompositeFS, result *AnalysisResult, + opts AnalysisOptions) error { + for _, a := range ag.postAnalyzers { + fsys, ok := compositeFS.Get(a.Type()) + if !ok { + continue + } + + skippedFiles := result.SystemInstalledFiles + if ag.detectionPriority == types.PriorityComprehensive { + // If the detection priority is comprehensive, system files installed by the OS package manager will not be skipped. + // It can lead to false positives and duplicates, but it may be necessary to detect all possible vulnerabilities. + skippedFiles = nil + } + for _, app := range result.Applications { + skippedFiles = append(skippedFiles, app.FilePath) + for _, pkg := range app.Packages { + // The analysis result could contain packages listed in SBOM. + // The files of those packages don't have to be analyzed. + // This is especially helpful for expensive post-analyzers such as the JAR analyzer. + if pkg.FilePath != "" { + skippedFiles = append(skippedFiles, pkg.FilePath) + } + } + } + + filteredFS, err := fsys.Filter(skippedFiles) + if err != nil { + return xerrors.Errorf("unable to filter filesystem: %w", err) + } + + res, err := ag.postAnalyze(ctx, a, PostAnalysisInput{ + FS: filteredFS, + FilePatterns: ag.filePatterns[a.Type()], + Options: opts, + }) + if err != nil { + return xerrors.Errorf("post analysis error: %w", err) + } + result.Merge(res) + } + return nil +} + +// PostAnalyzerFS returns a composite filesystem that contains multiple filesystems for each post-analyzer +func (ag AnalyzerGroup) PostAnalyzerFS() (*CompositeFS, error) { + return NewCompositeFS() +} + +// StaticPaths collects static paths from all enabled analyzers +// It returns the collected paths and a boolean indicating if all enabled analyzers implement StaticPathAnalyzer +func (ag AnalyzerGroup) StaticPaths(disabled []Type) ([]string, bool) { + var paths []string + + type analyzerType interface{ Type() Type } + allAnalyzers := append( + xslices.Map(ag.analyzers, func(a analyzer) analyzerType { return a }), + xslices.Map(ag.postAnalyzers, func(a PostAnalyzer) analyzerType { return a })..., + ) + + for _, a := range allAnalyzers { + // Skip disabled analyzers + if slices.Contains(disabled, a.Type()) { + continue + } + + // We can't be sure that the file pattern uses a static path. + // So we don't need to use `StaticPath` logic if any enabled analyzer has a file pattern. + if _, ok := ag.filePatterns[a.Type()]; ok { + return nil, false + } + + // If any analyzer doesn't implement StaticPathAnalyzer, return false + staticPathAnalyzer, ok := a.(StaticPathAnalyzer) + if !ok { + return nil, false + } + + // Collect paths from StaticPathAnalyzer + paths = append(paths, staticPathAnalyzer.StaticPaths()...) + } + + // Remove duplicates + return lo.Uniq(paths), true +} diff --git a/pkg/fanal/analyzer/analyzer_test.go b/pkg/fanal/analyzer/analyzer_test.go new file mode 100644 index 000000000000..2274dc582d15 --- /dev/null +++ b/pkg/fanal/analyzer/analyzer_test.go @@ -0,0 +1,866 @@ +package analyzer_test + +import ( + "os" + "slices" + "sync" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/javadb" + "github.com/aquasecurity/trivy/pkg/mapfs" + xio "github.com/aquasecurity/trivy/pkg/x/io" + + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/imgconf/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/java/jar" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/poetry" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/bundler" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/alpine" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/ubuntu" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/dpkg" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/repo/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/handler/all" + _ "modernc.org/sqlite" +) + +func TestAnalysisResult_Merge(t *testing.T) { + type fields struct { + m sync.Mutex + OS types.OS + PackageInfos []types.PackageInfo + Applications []types.Application + } + type args struct { + new *analyzer.AnalysisResult + } + tests := []struct { + name string + fields fields + args args + want analyzer.AnalysisResult + }{ + { + name: "happy path", + fields: fields{ + OS: types.OS{ + Family: types.Debian, + Name: "9.8", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/libc", + Packages: types.Packages{ + { + Name: "libc", + Version: "1.2.3", + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: "bundler", + FilePath: "app/Gemfile.lock", + Packages: types.Packages{ + { + Name: "rails", + Version: "5.0.0", + }, + }, + }, + }, + }, + args: args{ + new: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/openssl", + Packages: types.Packages{ + { + Name: "openssl", + Version: "1.1.1", + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: "bundler", + FilePath: "app2/Gemfile.lock", + Packages: types.Packages{ + { + Name: "nokogiri", + Version: "1.0.0", + }, + }, + }, + }, + }, + }, + want: analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Debian, + Name: "9.8", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/libc", + Packages: types.Packages{ + { + Name: "libc", + Version: "1.2.3", + }, + }, + }, + { + FilePath: "var/lib/dpkg/status.d/openssl", + Packages: types.Packages{ + { + Name: "openssl", + Version: "1.1.1", + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: "bundler", + FilePath: "app/Gemfile.lock", + Packages: types.Packages{ + { + Name: "rails", + Version: "5.0.0", + }, + }, + }, + { + Type: "bundler", + FilePath: "app2/Gemfile.lock", + Packages: types.Packages{ + { + Name: "nokogiri", + Version: "1.0.0", + }, + }, + }, + }, + }, + }, + { + name: "redhat must be replaced with oracle", + fields: fields{ + OS: types.OS{ + Family: types.RedHat, // this must be overwritten + Name: "8.0", + }, + }, + args: args{ + new: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Oracle, + Name: "8.0", + }, + }, + }, + want: analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Oracle, + Name: "8.0", + }, + }, + }, + { + name: "debian must be replaced with ubuntu", + fields: fields{ + OS: types.OS{ + Family: types.Debian, // this must be overwritten + Name: "9.0", + }, + }, + args: args{ + new: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Ubuntu, + Name: "18.04", + }, + }, + }, + want: analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Ubuntu, + Name: "18.04", + }, + }, + }, + { + name: "merge extended flag", + fields: fields{ + // This must be overwritten + OS: types.OS{ + Family: types.Ubuntu, + Name: "16.04", + }, + }, + args: args{ + new: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Ubuntu, + Extended: true, + }, + }, + }, + want: analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Ubuntu, + Name: "16.04", + Extended: true, + }, + }, + }, + { + name: "alpine OS needs to be extended with apk repositories", + fields: fields{ + OS: types.OS{ + Family: types.Alpine, + Name: "3.15.3", + }, + }, + args: args{ + new: &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: types.Alpine, + Release: "edge", + }, + }, + }, + want: analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Alpine, + Name: "3.15.3", + }, + Repository: &types.Repository{ + Family: types.Alpine, + Release: "edge", + }, + }, + }, + { + name: "alpine must not be replaced with oracle", + fields: fields{ + OS: types.OS{ + Family: types.Alpine, // this must not be overwritten + Name: "3.11", + }, + }, + args: args{ + new: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Oracle, + Name: "8.0", + }, + }, + }, + want: analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Alpine, // this must not be overwritten + Name: "3.11", + }, + }, + }, + { + name: "normalize licenses for PackageInfos and Applications", + args: args{ + new: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: "gomod", + FilePath: "go.mod", + Packages: types.Packages{ + { + Name: "github.com/example/package", + Version: "v1.0.0", + Licenses: []string{ + "", + "BSD", + "GPL-2", + "GPL-2.0", + }, + }, + }, + }, + { + Type: "gomod", + FilePath: "empty-license/go.mod", + Packages: types.Packages{ + { + Name: "github.com/empty/license", + Version: "v1.0.0", + Licenses: []string{ + "", + }, + }, + }, + }, + }, + }, + }, + want: analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: "gomod", + FilePath: "go.mod", + Packages: types.Packages{ + { + Name: "github.com/example/package", + Version: "v1.0.0", + Licenses: []string{ + "BSD-3-Clause", + "GPL-2.0-only", + }, + }, + }, + }, + { + Type: "gomod", + FilePath: "empty-license/go.mod", + Packages: types.Packages{ + { + Name: "github.com/empty/license", + Version: "v1.0.0", + Licenses: nil, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := analyzer.AnalysisResult{ + OS: tt.fields.OS, + PackageInfos: tt.fields.PackageInfos, + Applications: tt.fields.Applications, + } + r.Merge(tt.args.new) + assert.Equal(t, tt.want, r) + }) + } +} + +func TestAnalyzerGroup_AnalyzeFile(t *testing.T) { + type args struct { + filePath string + testFilePath string + disabledAnalyzers []analyzer.Type + filePatterns []string + } + tests := []struct { + name string + args args + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path with os analyzer", + args: args{ + filePath: "/etc/alpine-release", + testFilePath: "testdata/etc/alpine-release", + }, + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: "alpine", + Name: "3.11.6", + }, + }, + }, + { + name: "happy path with disabled os analyzer", + args: args{ + filePath: "/etc/alpine-release", + testFilePath: "testdata/etc/alpine-release", + disabledAnalyzers: []analyzer.Type{analyzer.TypeAlpine}, + }, + want: &analyzer.AnalysisResult{}, + }, + { + name: "happy path with package analyzer", + args: args{ + filePath: "/lib/apk/db/installed", + testFilePath: "testdata/lib/apk/db/installed", + }, + want: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "/lib/apk/db/installed", + Packages: types.Packages{ + { + ID: "musl@1.1.24-r2", + Name: "musl", + Version: "1.1.24-r2", + SrcName: "musl", + SrcVersion: "1.1.24-r2", + Licenses: []string{"MIT"}, + Maintainer: "Timo Teräs ", + Arch: "x86_64", + Digest: "sha1:cb2316a189ebee5282c4a9bd98794cc2477a74c6", + InstalledFiles: []string{ + "lib/libc.musl-x86_64.so.1", + "lib/ld-musl-x86_64.so.1", + }, + AnalyzedBy: analyzer.TypeApk, + }, + }, + }, + }, + SystemInstalledFiles: []string{ + "lib/libc.musl-x86_64.so.1", + "lib/ld-musl-x86_64.so.1", + }, + }, + }, + { + name: "happy path with disabled package analyzer", + args: args{ + filePath: "/lib/apk/db/installed", + testFilePath: "testdata/lib/apk/db/installed", + disabledAnalyzers: []analyzer.Type{analyzer.TypeApk}, + }, + want: &analyzer.AnalysisResult{}, + }, + { + name: "happy path with library analyzer", + args: args{ + filePath: "/app/Gemfile.lock", + testFilePath: "testdata/app/Gemfile.lock", + }, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: "bundler", + FilePath: "/app/Gemfile.lock", + Packages: types.Packages{ + { + ID: "actioncable@5.2.3", + Name: "actioncable", + Version: "5.2.3", + Indirect: false, + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "actionpack@5.2.3", + }, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "actionpack@5.2.3", + Name: "actionpack", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 6, + EndLine: 6, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + }, + }, + }, + }, + }, + { + name: "happy path with invalid os information", + args: args{ + filePath: "/etc/lsb-release", + testFilePath: "testdata/etc/hostname", + }, + want: &analyzer.AnalysisResult{}, + }, + { + name: "happy path with a directory", + args: args{ + filePath: "/etc/lsb-release", + testFilePath: "testdata/etc", + }, + want: &analyzer.AnalysisResult{}, + }, + { + name: "happy path with library analyzer file pattern regex", + args: args{ + filePath: "/app/Gemfile-dev.lock", + testFilePath: "testdata/app/Gemfile.lock", + filePatterns: []string{"bundler:Gemfile(-.*)?\\.lock"}, + }, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: "bundler", + FilePath: "/app/Gemfile-dev.lock", + Packages: types.Packages{ + { + ID: "actioncable@5.2.3", + Name: "actioncable", + Version: "5.2.3", + Indirect: false, + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "actionpack@5.2.3", + }, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "actionpack@5.2.3", + Name: "actionpack", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 6, + EndLine: 6, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + }, + }, + }, + }, + }, + { + name: "ignore permission error", + args: args{ + filePath: "/etc/alpine-release", + testFilePath: "testdata/no-permission", + }, + want: &analyzer.AnalysisResult{}, + }, + { + name: "sad path with opener error", + args: args{ + filePath: "/lib/apk/db/installed", + testFilePath: "testdata/error", + }, + wantErr: "unable to open /lib/apk/db/installed", + }, + { + name: "sad path with broken file pattern regex", + args: args{ + filePath: "/app/Gemfile-dev.lock", + testFilePath: "testdata/app/Gemfile.lock", + filePatterns: []string{"bundler:Gemfile(-.*?\\.lock"}, + }, + wantErr: "error parsing regexp", + }, + { + name: "sad path with broken file pattern", + args: args{ + filePath: "/app/Gemfile-dev.lock", + testFilePath: "testdata/app/Gemfile.lock", + filePatterns: []string{"Gemfile(-.*)?\\.lock"}, + }, + wantErr: "invalid file pattern", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + eg, ctx := errgroup.WithContext(t.Context()) + limit := semaphore.NewWeighted(3) + + got := new(analyzer.AnalysisResult) + a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{ + FilePatterns: tt.args.filePatterns, + DisabledAnalyzers: tt.args.disabledAnalyzers, + }) + if err != nil && tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + info, err := os.Stat(tt.args.testFilePath) + require.NoError(t, err) + + err = a.AnalyzeFile(ctx, eg, limit, got, "", tt.args.filePath, info, + func() (xio.ReadSeekCloserAt, error) { + switch tt.args.testFilePath { + case "testdata/error": + return nil, xerrors.New("error") + case "testdata/no-permission": + os.Chmod(tt.args.testFilePath, 0o000) + t.Cleanup(func() { + os.Chmod(tt.args.testFilePath, 0o644) + }) + } + return os.Open(tt.args.testFilePath) + }, + nil, analyzer.AnalysisOptions{}, + ) + + egErr := eg.Wait() + require.NoError(t, egErr) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestAnalyzerGroup_PostAnalyze(t *testing.T) { + tests := []struct { + name string + dir string + analyzerType analyzer.Type + filePatterns []string + want *analyzer.AnalysisResult + }{ + { + name: "jars with invalid jar", + dir: "testdata/post-apps/jar/", + analyzerType: analyzer.TypeJar, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Jar, + FilePath: "testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar", + Packages: types.Packages{ + { + Name: "com.fasterxml.jackson.core:jackson-annotations", + Version: "2.15.0-rc2", + FilePath: "testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar", + AnalyzedBy: analyzer.TypeJar, + }, + }, + }, + }, + }, + }, + { + name: "poetry files with file from pattern and invalid file", + dir: "testdata/post-apps/poetry/", + filePatterns: []string{ + "poetry:poetry-pattern.lock", + }, + analyzerType: analyzer.TypePoetry, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Poetry, + FilePath: "testdata/post-apps/poetry/happy/poetry-pattern.lock", + Packages: types.Packages{ + { + ID: "certifi@2022.12.7", + Name: "certifi", + Version: "2022.12.7", + AnalyzedBy: analyzer.TypePoetry, + }, + }, + }, + { + Type: types.Poetry, + FilePath: "testdata/post-apps/poetry/happy/poetry.lock", + Packages: types.Packages{ + { + ID: "certifi@2022.12.7", + Name: "certifi", + Version: "2022.12.7", + AnalyzedBy: analyzer.TypePoetry, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{ + FilePatterns: tt.filePatterns, + }) + require.NoError(t, err) + + // Create a virtual filesystem + composite, err := analyzer.NewCompositeFS() + require.NoError(t, err) + + mfs := mapfs.New() + require.NoError(t, mfs.CopyFilesUnder(tt.dir)) + composite.Set(tt.analyzerType, mfs) + + if tt.analyzerType == analyzer.TypeJar { + // init java-trivy-db with skip update + repo, err := name.NewTag(javadb.DefaultGHCRRepository) + require.NoError(t, err) + javadb.Init("./language/java/jar/testdata", []name.Reference{repo}, true, false, types.RegistryOptions{Insecure: false}) + } + + ctx := t.Context() + got := new(analyzer.AnalysisResult) + err = a.PostAnalyze(ctx, composite, got, analyzer.AnalysisOptions{}) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestAnalyzerGroup_AnalyzerVersions(t *testing.T) { + tests := []struct { + name string + disabled []analyzer.Type + want analyzer.Versions + }{ + { + name: "happy path", + disabled: []analyzer.Type{}, + want: analyzer.Versions{ + Analyzers: map[string]int{ + "alpine": 1, + "apk-repo": 1, + "apk": 3, + "bundler": 1, + "dpkg-license": 1, + "ubuntu": 1, + "ubuntu-esm": 1, + }, + PostAnalyzers: map[string]int{ + "dpkg": 6, + "jar": 1, + "poetry": 1, + }, + }, + }, + { + name: "disable analyzers", + disabled: []analyzer.Type{ + analyzer.TypeAlpine, + analyzer.TypeApkRepo, + analyzer.TypeDpkg, + analyzer.TypeDpkgLicense, + analyzer.TypeUbuntu, + analyzer.TypeUbuntuESM, + analyzer.TypeJar, + }, + want: analyzer.Versions{ + Analyzers: map[string]int{ + "apk": 3, + "bundler": 1, + }, + PostAnalyzers: map[string]int{ + "poetry": 1, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{ + DisabledAnalyzers: tt.disabled, + }) + require.NoError(t, err) + got := a.AnalyzerVersions() + assert.Equal(t, tt.want, got) + }) + } +} + +// TestAnalyzerGroup_StaticPaths tests the StaticPaths method of AnalyzerGroup +func TestAnalyzerGroup_StaticPaths(t *testing.T) { + tests := []struct { + name string + disabledAnalyzers []analyzer.Type + filePatterns []string + want []string + wantAllStatic bool + }{ + { + name: "all analyzers including post-analyzers implement StaticPathAnalyzer", + disabledAnalyzers: []analyzer.Type{ + analyzer.TypeApkCommand, analyzer.TypeJar, analyzer.TypePoetry, analyzer.TypeBundler, + }, + want: []string{ + "etc/apk/repositories", + "etc/lsb-release", + "lib/apk/db/installed", + "usr/lib/apk/db/installed", + "etc/alpine-release", + + "usr/share/doc/", + "var/lib/dpkg/status", + "var/lib/dpkg/status.d/", + "var/lib/dpkg/available", + "var/lib/dpkg/info/", + "var/lib/ubuntu-advantage/status.json", + }, + wantAllStatic: true, + }, + { + name: "all analyzers implement StaticPathAnalyzer, but there is file pattern", + disabledAnalyzers: []analyzer.Type{ + analyzer.TypeApkCommand, analyzer.TypeJar, analyzer.TypePoetry, analyzer.TypeBundler, + }, + filePatterns: []string{ + "alpine:etc/alpine-release-custom", + }, + want: []string{}, + wantAllStatic: false, + }, + { + name: "some analyzers don't implement StaticPathAnalyzer", + want: []string{}, + wantAllStatic: false, + }, + { + name: "disable all analyzers", + disabledAnalyzers: slices.Concat(analyzer.TypeOSes, analyzer.TypeLanguages), + want: []string{}, + wantAllStatic: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a new analyzer group + a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{ + FilePatterns: tt.filePatterns, + }) + require.NoError(t, err) + + // Get static paths + gotPaths, gotAllStatic := a.StaticPaths(tt.disabledAnalyzers) + + // Check if all analyzers implement StaticPathAnalyzer + assert.Equal(t, tt.wantAllStatic, gotAllStatic) + + // Check paths + assert.ElementsMatch(t, tt.want, gotPaths) + }) + } +} diff --git a/pkg/fanal/analyzer/buildinfo/content_manifest.go b/pkg/fanal/analyzer/buildinfo/content_manifest.go new file mode 100644 index 000000000000..8b7a0feabe6c --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/content_manifest.go @@ -0,0 +1,69 @@ +package buildinfo + +import ( + "context" + "encoding/json" + "os" + "path/filepath" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/set" +) + +func init() { + analyzer.RegisterAnalyzer(&contentManifestAnalyzer{}) +} + +var contentSetsDirs = set.New[string]( + "root/buildinfo/content_manifests/", + "usr/share/buildinfo/", // for RHCOS +) + +const contentManifestAnalyzerVersion = 1 + +type contentManifest struct { + ContentSets []string `json:"content_sets"` +} + +// For Red Hat products +type contentManifestAnalyzer struct{} + +func (a contentManifestAnalyzer) Analyze(_ context.Context, target analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + var manifest contentManifest + if err := json.NewDecoder(target.Content).Decode(&manifest); err != nil { + return nil, xerrors.Errorf("invalid content manifest: %w", err) + } + + if len(manifest.ContentSets) == 0 { + return nil, nil + } + + return &analyzer.AnalysisResult{ + BuildInfo: &types.BuildInfo{ + ContentSets: manifest.ContentSets, + }, + }, nil +} + +func (a contentManifestAnalyzer) Required(filePath string, _ os.FileInfo) bool { + dir, file := filepath.Split(filepath.ToSlash(filePath)) + if !contentSetsDirs.Contains(dir) { + return false + } + return filepath.Ext(file) == ".json" +} + +func (a contentManifestAnalyzer) Type() analyzer.Type { + return analyzer.TypeRedHatContentManifestType +} + +func (a contentManifestAnalyzer) Version() int { + return contentManifestAnalyzerVersion +} + +func (a contentManifestAnalyzer) StaticPaths() []string { + return contentSetsDirs.Items() +} diff --git a/pkg/fanal/analyzer/buildinfo/content_manifest_test.go b/pkg/fanal/analyzer/buildinfo/content_manifest_test.go new file mode 100644 index 000000000000..004998e229f7 --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/content_manifest_test.go @@ -0,0 +1,101 @@ +package buildinfo + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_contentManifestAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + input string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + input: "testdata/content_manifests/ubi8-minimal-container-8.5-218.json", + want: &analyzer.AnalysisResult{ + BuildInfo: &types.BuildInfo{ + ContentSets: []string{ + "rhel-8-for-x86_64-baseos-rpms", + "rhel-8-for-x86_64-appstream-rpms", + }, + }, + }, + }, + { + name: "happy path for non-contentSets file", + input: "testdata/content_manifests/sbom-purl.json", + want: nil, + }, + { + name: "broken json", + input: "testdata/content_manifests/broken.json", + wantErr: "invalid content manifest", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.input) + require.NoError(t, err) + defer f.Close() + + a := contentManifestAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.input, + Content: f, + }) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_contentManifestAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path root dir", + filePath: "root/buildinfo/content_manifests/nodejs-12-container-1-66.json", + want: true, + }, + { + name: "happy path usr dir", + filePath: "usr/share/buildinfo/nodejs-12-container-1-66.json", + want: true, + }, + { + name: "sad path wrong dir", + filePath: "foo/bar/nodejs-12-container-1-66.json", + want: false, + }, + { + name: "sad path wrong extension", + filePath: "root/buildinfo/content_manifests/nodejs-12-container-1-66.xml", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := contentManifestAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/buildinfo/dockerfile.go b/pkg/fanal/analyzer/buildinfo/dockerfile.go new file mode 100644 index 000000000000..a4e5baded691 --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/dockerfile.go @@ -0,0 +1,164 @@ +package buildinfo + +import ( + "context" + "os" + "path/filepath" + "strings" + + "github.com/moby/buildkit/frontend/dockerfile/instructions" + "github.com/moby/buildkit/frontend/dockerfile/parser" + "github.com/moby/buildkit/frontend/dockerfile/shell" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&dockerfileAnalyzer{}) +} + +const dockerfileAnalyzerVersion = 1 + +// For Red Hat products +type dockerfileAnalyzer struct{} + +func (a dockerfileAnalyzer) Analyze(_ context.Context, target analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + // ported from https://github.com/moby/buildkit/blob/e83d79a51fb49aeb921d8a2348ae14a58701c98c/frontend/dockerfile/dockerfile2llb/convert.go#L88-L89 + dockerfile, err := parser.Parse(target.Content) + if err != nil { + return nil, xerrors.Errorf("dockerfile parse error: %w", err) + } + + stages, metaArgs, err := instructions.Parse(dockerfile.AST, nil) + if err != nil { + return nil, xerrors.Errorf("instruction parse error: %w", err) + } + + var args []instructions.KeyValuePairOptional + for _, cmd := range metaArgs { + for _, metaArg := range cmd.Args { + args = append(args, setKVValue(metaArg, nil)) + } + } + + shlex := shell.NewLex(dockerfile.EscapeToken) + envs := metaArgsToEnvGetter(args) + var component, arch string + for _, st := range stages { + for _, cmd := range st.Commands { + switch c := cmd.(type) { + case *instructions.EnvCommand: + envs.addKeyValuePairsToEnvGetter(c.Env) + case *instructions.LabelCommand: + for _, kvp := range c.Labels { + workResult, err := shlex.ProcessWordWithMatches(kvp.Key, envs) + if err != nil { + return nil, xerrors.Errorf("unable to evaluate the label '%s': %w", kvp.Key, err) + } + + key := strings.ToLower(workResult.Result) + switch key { + case "com.redhat.component", "bzcomponent": + workResult, err = shlex.ProcessWordWithMatches(kvp.Value, envs) + component = workResult.Result + case "architecture": + workResult, err = shlex.ProcessWordWithMatches(kvp.Value, envs) + arch = workResult.Result + } + + if err != nil { + return nil, xerrors.Errorf("failed to process the label '%s': %w", key, err) + } + } + } + } + } + + if component == "" { + return nil, xerrors.New("no component found") + } else if arch == "" { + return nil, xerrors.New("no arch found") + } + + return &analyzer.AnalysisResult{ + BuildInfo: &types.BuildInfo{ + Nvr: component + "-" + parseVersion(target.FilePath), + Arch: arch, + }, + }, nil +} + +func (a dockerfileAnalyzer) Required(filePath string, _ os.FileInfo) bool { + dir, file := filepath.Split(filepath.ToSlash(filePath)) + if dir != "root/buildinfo/" { + return false + } + return strings.HasPrefix(file, "Dockerfile") +} + +func (a dockerfileAnalyzer) Type() analyzer.Type { + return analyzer.TypeRedHatDockerfileType +} + +func (a dockerfileAnalyzer) Version() int { + return dockerfileAnalyzerVersion +} + +// parseVersion parses version from a file name +func parseVersion(nvr string) string { + releaseIndex := strings.LastIndex(nvr, "-") + if releaseIndex < 0 { + return "" + } + versionIndex := strings.LastIndex(nvr[:releaseIndex], "-") + version := nvr[versionIndex+1:] + return version +} + +type envGetter struct { + m map[string]string +} + +func (e *envGetter) addKeyValuePairsToEnvGetter(kvp instructions.KeyValuePairs) { + if e.m == nil { + e.m = make(map[string]string) + } + + for _, kv := range kvp { + e.m[kv.Key] = kv.Value + } +} + +func metaArgsToEnvGetter(metaArgs []instructions.KeyValuePairOptional) *envGetter { + env := &envGetter{ + m: make(map[string]string), + } + + for _, kv := range metaArgs { + env.m[kv.Key] = kv.ValueString() + } + return env +} + +func (e *envGetter) Get(key string) (string, bool) { + v, ok := e.m[key] + return v, ok +} + +func (e *envGetter) Keys() []string { + return lo.Keys(e.m) +} + +func setKVValue(kvpo instructions.KeyValuePairOptional, values map[string]string) instructions.KeyValuePairOptional { + if v, ok := values[kvpo.Key]; ok { + kvpo.Value = &v + } + return kvpo +} + +func (a dockerfileAnalyzer) StaticPaths() []string { + return []string{"root/buildinfo"} +} diff --git a/pkg/fanal/analyzer/buildinfo/dockerfile_test.go b/pkg/fanal/analyzer/buildinfo/dockerfile_test.go new file mode 100644 index 000000000000..f653bf976e6b --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/dockerfile_test.go @@ -0,0 +1,94 @@ +package buildinfo + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_dockerfileAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "com.redhat.component", + inputFile: "testdata/dockerfile/Dockerfile-ubi8-8.3-227", + want: &analyzer.AnalysisResult{ + BuildInfo: &types.BuildInfo{ + Nvr: "ubi8-container-8.3-227", + Arch: "x86_64", + }, + }, + }, + { + name: "BZcomponent", + inputFile: "testdata/dockerfile/Dockerfile-jboss-base-7-base-1.1-3", + want: &analyzer.AnalysisResult{ + BuildInfo: &types.BuildInfo{ + Nvr: "jboss-base-7-docker-1.1-3", + Arch: "x86_64", + }, + }, + }, + { + name: "missing architecture", + inputFile: "testdata/dockerfile/Dockerfile.sad", + wantErr: "no arch found", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := dockerfileAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + if tt.wantErr != "" { + require.Error(t, err) + assert.Equal(t, tt.wantErr, err.Error()) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_dockerfileAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path", + filePath: "root/buildinfo/Dockerfile-ubi8-8.3-227", + want: true, + }, + { + name: "sad path", + filePath: "app/Dockerfile-ubi8-8.3-227", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := dockerfileAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/buildinfo/testdata/content_manifests/broken.json b/pkg/fanal/analyzer/buildinfo/testdata/content_manifests/broken.json new file mode 100644 index 000000000000..86a410dd1d33 --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/testdata/content_manifests/broken.json @@ -0,0 +1 @@ +broken \ No newline at end of file diff --git a/pkg/fanal/analyzer/buildinfo/testdata/content_manifests/sbom-purl.json b/pkg/fanal/analyzer/buildinfo/testdata/content_manifests/sbom-purl.json new file mode 100644 index 000000000000..8e73ade1d8f3 --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/testdata/content_manifests/sbom-purl.json @@ -0,0 +1,9 @@ +{ + "image_contents": { + "dependencies": [ + { + "purl": "pkg:rpm/redhat/zstd@1.5.1-2.el9?arch=src&checksum=sha256:f1ddea14d19746b867e69b48d128dd9c2d3e8cc021a5ea7b0674b48356ad3341&repository_id=rhel-9-base-source" + } + ] + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/buildinfo/testdata/content_manifests/ubi8-minimal-container-8.5-218.json b/pkg/fanal/analyzer/buildinfo/testdata/content_manifests/ubi8-minimal-container-8.5-218.json new file mode 100644 index 000000000000..8653f337011c --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/testdata/content_manifests/ubi8-minimal-container-8.5-218.json @@ -0,0 +1,12 @@ +{ + "metadata": { + "icm_version": 1, + "icm_spec": "https://raw.githubusercontent.com/containerbuildsystem/atomic-reactor/master/atomic_reactor/schemas/content_manifest.json", + "image_layer_index": 1 + }, + "content_sets": [ + "rhel-8-for-x86_64-baseos-rpms", + "rhel-8-for-x86_64-appstream-rpms" + ], + "image_contents": [] +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/buildinfo/testdata/dockerfile/Dockerfile-jboss-base-7-base-1.1-3 b/pkg/fanal/analyzer/buildinfo/testdata/dockerfile/Dockerfile-jboss-base-7-base-1.1-3 new file mode 100644 index 000000000000..4f9f92911b21 --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/testdata/dockerfile/Dockerfile-jboss-base-7-base-1.1-3 @@ -0,0 +1,35 @@ +# registry.redhat.io/openshift3/metrics-cassandra:3.1.0 +FROM 82ad5fa11820c2889c60f7f748d67aab04400700c581843db0d1e68735327443 +MAINTAINER JBoss Cloud Enablement Feedback + +ENV base jboss-base + +LABEL BZComponent="${base}-7-docker" \ + Architecture="x86_64" \ + Name="jboss-base-7/base" \ + Version="1.1" \ + Release="3" + +# Explicitly set the $HOME env variable so it can be referenced in Dockerfiles +ENV HOME /home/jboss + +ADD jboss.repo /etc/yum.repos.d/jboss.repo + +# Install unzip and tar package which is required to unpack product distributions +# Cleanup the YUM metadata +RUN yum -y --disablerepo \* --enablerepo=jboss install yum-utils unzip tar && \ + yum clean all + +RUN rm /etc/yum.repos.d/jboss.repo + +# Create a user and group used to launch processes +# We use the ID 185 fot the group as well as for the user. +# This ID is registered static ID for the JBoss EAP product +# on RHEL which makes it safe to use. +RUN groupadd -r jboss -g 185 && useradd -u 185 -r -g jboss -m -d /home/jboss -s /sbin/nologin -c "JBoss user" jboss + +# Set the working directory to jboss' user home directory +WORKDIR /home/jboss + +# Specify the user which should be used to execute all commands below +USER jboss diff --git a/pkg/fanal/analyzer/buildinfo/testdata/dockerfile/Dockerfile-ubi8-8.3-227 b/pkg/fanal/analyzer/buildinfo/testdata/dockerfile/Dockerfile-ubi8-8.3-227 new file mode 100644 index 000000000000..910ae14e0577 --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/testdata/dockerfile/Dockerfile-ubi8-8.3-227 @@ -0,0 +1,30 @@ +FROM sha256:4224eead35ea350b4b9d4ac67550e92efb9a50d3855cb3381469fe4c7e3f2053 + +LABEL maintainer="Red Hat, Inc." + +LABEL com.redhat.component="ubi8-container" \ + name="ubi8" \ + version="8.3" + +#label for EULA +LABEL com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" + +#labels for container catalog +LABEL summary="Provides the latest release of Red Hat Universal Base Image 8." +LABEL description="The Universal Base Image is designed and engineered to be the base layer for all of your containerized applications, middleware and utilities. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly." +LABEL io.k8s.display-name="Red Hat Universal Base Image 8" +LABEL io.openshift.expose-services="" +LABEL io.openshift.tags="base rhel8" + +ENV container oci +ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +ENV arch x86_64 + +CMD ["/bin/bash"] + +RUN rm -rf /var/log/* +#rhbz 1609043 +RUN mkdir -p /var/log/rhsm + +ADD ubi8-container-8.3-227.json /root/buildinfo/content_manifests/ubi8-container-8.3-227.json +LABEL "release"="227" "distribution-scope"="public" "vendor"="Red Hat, Inc." "build-date"="2020-12-10T01:59:40.343735" "architecture"=$arch "vcs-type"="git" "vcs-ref"="3652f52021079930cba3bf90d27d9f191b18115b" "com.redhat.build-host"="cpt-1002.osbs.prod.upshift.rdu2.redhat.com" "io.k8s.description"="The Universal Base Image is designed and engineered to be the base layer for all of your containerized applications, middleware and utilities. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly." "url"="https://access.redhat.com/containers/#/registry.access.redhat.com/ubi8/images/8.3-227" diff --git a/pkg/fanal/analyzer/buildinfo/testdata/dockerfile/Dockerfile.sad b/pkg/fanal/analyzer/buildinfo/testdata/dockerfile/Dockerfile.sad new file mode 100644 index 000000000000..96e596df5c6c --- /dev/null +++ b/pkg/fanal/analyzer/buildinfo/testdata/dockerfile/Dockerfile.sad @@ -0,0 +1,28 @@ +FROM sha256:4224eead35ea350b4b9d4ac67550e92efb9a50d3855cb3381469fe4c7e3f2053 + +LABEL maintainer="Red Hat, Inc." + +LABEL com.redhat.component="ubi8-container" \ + name="ubi8" \ + version="8.3" + +#label for EULA +LABEL com.redhat.license_terms="https://www.redhat.com/en/about/red-hat-end-user-license-agreements#UBI" + +#labels for container catalog +LABEL summary="Provides the latest release of Red Hat Universal Base Image 8." +LABEL description="The Universal Base Image is designed and engineered to be the base layer for all of your containerized applications, middleware and utilities. This base image is freely redistributable, but Red Hat only supports Red Hat technologies through subscriptions for Red Hat products. This image is maintained by Red Hat and updated regularly." +LABEL io.k8s.display-name="Red Hat Universal Base Image 8" +LABEL io.openshift.expose-services="" +LABEL io.openshift.tags="base rhel8" + +ENV container oci +ENV PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +CMD ["/bin/bash"] + +RUN rm -rf /var/log/* +#rhbz 1609043 +RUN mkdir -p /var/log/rhsm + +ADD ubi8-container-8.3-227.json /root/buildinfo/content_manifests/ubi8-container-8.3-227.json diff --git a/pkg/fanal/analyzer/config/all/import.go b/pkg/fanal/analyzer/config/all/import.go new file mode 100644 index 000000000000..3b8fd440cf20 --- /dev/null +++ b/pkg/fanal/analyzer/config/all/import.go @@ -0,0 +1,15 @@ +package all + +import ( + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/ansible" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/azurearm" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/cloudformation" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/dockerfile" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/helm" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/json" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/k8s" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/terraform" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/terraformplan/json" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/terraformplan/snapshot" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/yaml" +) diff --git a/pkg/fanal/analyzer/config/ansible/ansible.go b/pkg/fanal/analyzer/config/ansible/ansible.go new file mode 100644 index 000000000000..069822976c85 --- /dev/null +++ b/pkg/fanal/analyzer/config/ansible/ansible.go @@ -0,0 +1,37 @@ +package ansible + +import ( + "os" + "path/filepath" + "slices" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + version = 1 + analyzerType = analyzer.TypeAnsible +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newAnsibleConfigAnalyzer) +} + +type ansibleConfigAnalyzer struct { + *config.Analyzer +} + +func newAnsibleConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeAnsible, opts) + if err != nil { + return nil, err + } + return &ansibleConfigAnalyzer{Analyzer: a}, nil +} + +func (a *ansibleConfigAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Base(filePath) == "ansible.cfg" || + slices.Contains([]string{"", ".yml", ".yaml", ".json", ".ini"}, filepath.Ext(filePath)) +} diff --git a/pkg/fanal/analyzer/config/ansible/ansible_test.go b/pkg/fanal/analyzer/config/ansible/ansible_test.go new file mode 100644 index 000000000000..60befbcf235e --- /dev/null +++ b/pkg/fanal/analyzer/config/ansible/ansible_test.go @@ -0,0 +1,61 @@ +package ansible + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" +) + +func Test_ansibleConfigAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "yaml", + filePath: "test.yaml", + want: true, + }, + { + name: "yml", + filePath: "test.yml", + want: true, + }, + { + name: "json", + filePath: "test.json", + want: true, + }, + { + name: "init", + filePath: "test.ini", + want: true, + }, + { + name: "without extension", + filePath: "test", + want: true, + }, + { + name: "config file", + filePath: "ansible.cfg", + want: true, + }, + { + name: "just cfg", + filePath: "test.cfg", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newAnsibleConfigAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + assert.Equal(t, tt.want, a.Required(tt.filePath, nil)) + }) + } +} diff --git a/pkg/fanal/analyzer/config/azurearm/azurearm.go b/pkg/fanal/analyzer/config/azurearm/azurearm.go new file mode 100644 index 000000000000..455894a715b6 --- /dev/null +++ b/pkg/fanal/analyzer/config/azurearm/azurearm.go @@ -0,0 +1,42 @@ +package azurearm + +import ( + "os" + "path/filepath" + "strings" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + version = 1 + analyzerType = analyzer.TypeAzureARM +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newAzureARMConfigAnalyzer) +} + +// azureARMConfigAnalyzer is an analyzer for detecting misconfigurations in Azure ARM templates. +// It embeds config.Analyzer so it can implement analyzer.PostAnalyzer. +type azureARMConfigAnalyzer struct { + *config.Analyzer +} + +func newAzureARMConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeAzureARM, opts) + if err != nil { + return nil, err + } + return &azureARMConfigAnalyzer{Analyzer: a}, nil +} + +// Required overrides config.Analyzer.Required() and check if the given file is JSON. +func (a *azureARMConfigAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Ext(filePath) == ".json" && + // skip CreateUiDefinition + // https://learn.microsoft.com/en-us/azure/azure-resource-manager/managed-applications/create-uidefinition-overview + strings.ToLower(filepath.Base(filePath)) != "createuidefinition.json" +} diff --git a/pkg/fanal/analyzer/config/azurearm/azurearm_test.go b/pkg/fanal/analyzer/config/azurearm/azurearm_test.go new file mode 100644 index 000000000000..ed1fc105021a --- /dev/null +++ b/pkg/fanal/analyzer/config/azurearm/azurearm_test.go @@ -0,0 +1,46 @@ +package azurearm + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" +) + +func Test_azureARMConfigAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "json", + filePath: "test.json", + want: true, + }, + { + name: "yaml", + filePath: "test.yaml", + want: false, + }, + { + name: "CreateUiDefinition", + filePath: "CreateUiDefinition.json", + want: false, + }, + { + name: "CreateUiDefinition lowercase", + filePath: "createuidefinition.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newAzureARMConfigAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + assert.Equal(t, tt.want, a.Required(tt.filePath, nil)) + }) + } +} diff --git a/pkg/fanal/analyzer/config/cloudformation/cloudformation.go b/pkg/fanal/analyzer/config/cloudformation/cloudformation.go new file mode 100644 index 000000000000..02f281cbbb8a --- /dev/null +++ b/pkg/fanal/analyzer/config/cloudformation/cloudformation.go @@ -0,0 +1,30 @@ +package cloudformation + +import ( + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + analyzerType = analyzer.TypeCloudFormation + version = 1 +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newCloudFormationConfigAnalyzer) +} + +// cloudFormationConfigAnalyzer is an analyzer for detecting misconfigurations in CloudFormation files. +// It embeds config.Analyzer so it can implement analyzer.PostAnalyzer. +type cloudFormationConfigAnalyzer struct { + *config.Analyzer +} + +func newCloudFormationConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeCloudFormation, opts) + if err != nil { + return nil, err + } + return &cloudFormationConfigAnalyzer{Analyzer: a}, nil +} diff --git a/pkg/fanal/analyzer/config/config.go b/pkg/fanal/analyzer/config/config.go new file mode 100644 index 000000000000..020ae1ae3562 --- /dev/null +++ b/pkg/fanal/analyzer/config/config.go @@ -0,0 +1,64 @@ +package config + +import ( + "context" + "os" + "path/filepath" + + "golang.org/x/xerrors" + "k8s.io/utils/strings/slices" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/iac/detection" + "github.com/aquasecurity/trivy/pkg/misconf" +) + +var ( + _ analyzer.PostAnalyzer = (*Analyzer)(nil) + + requiredExts = []string{".json", ".yaml", ".yml", ".tfvars"} +) + +// Analyzer represents an analyzer for config files, +// which is embedded into each config analyzer such as Kubernetes. +type Analyzer struct { + typ analyzer.Type + version int + scanner *misconf.Scanner +} + +func NewAnalyzer(t analyzer.Type, version int, fileType detection.FileType, opts analyzer.AnalyzerOptions) (*Analyzer, error) { + s, err := misconf.NewScanner(fileType, opts.MisconfScannerOption) + if err != nil { + return nil, xerrors.Errorf("%s scanner init error: %w", t, err) + } + return &Analyzer{ + typ: t, + version: version, + scanner: s, + }, nil +} + +// PostAnalyze performs configuration analysis on the input filesystem and detect misconfigurations. +func (a *Analyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + misconfs, err := a.scanner.Scan(ctx, input.FS) + if err != nil { + return nil, xerrors.Errorf("%s scan error: %w", a.typ, err) + } + return &analyzer.AnalysisResult{Misconfigurations: misconfs}, nil +} + +// Required checks if the given file path has one of the required file extensions. +func (a *Analyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredExts, filepath.Ext(filePath)) +} + +// Type returns the analyzer type of the current Analyzer instance. +func (a *Analyzer) Type() analyzer.Type { + return a.typ +} + +// Version returns the version of the current Analyzer instance. +func (a *Analyzer) Version() int { + return a.version +} diff --git a/pkg/fanal/analyzer/config/config_test.go b/pkg/fanal/analyzer/config/config_test.go new file mode 100644 index 000000000000..f3fb893c8136 --- /dev/null +++ b/pkg/fanal/analyzer/config/config_test.go @@ -0,0 +1,106 @@ +package config_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/detection" + "github.com/aquasecurity/trivy/pkg/misconf" +) + +func TestAnalyzer_PostAnalyze(t *testing.T) { + type fields struct { + typ analyzer.Type + fileType detection.FileType + opts analyzer.AnalyzerOptions + } + tests := []struct { + name string + fields fields + dir string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "dockerfile", + fields: fields{ + typ: analyzer.TypeDockerfile, + fileType: detection.FileTypeDockerfile, + opts: analyzer.AnalyzerOptions{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"testdata/rego"}, + DisableEmbeddedPolicies: true, + }, + }, + }, + dir: "testdata/src", + want: &analyzer.AnalysisResult{ + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.Dockerfile, + FilePath: "Dockerfile", + Successes: types.MisconfResults{ + types.MisconfResult{ + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Dockerfile Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + { + name: "non-existent dir", + fields: fields{ + typ: analyzer.TypeDockerfile, + fileType: detection.FileTypeDockerfile, + opts: analyzer.AnalyzerOptions{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"testdata/rego"}, + DisableEmbeddedPolicies: true, + }, + }, + }, + dir: "testdata/non-existent", + wantErr: testutil.ErrNotExist, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := config.NewAnalyzer(tt.fields.typ, 0, tt.fields.fileType, tt.fields.opts) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/config/dockerfile/docker.go b/pkg/fanal/analyzer/config/dockerfile/docker.go new file mode 100644 index 000000000000..0c4463dd528e --- /dev/null +++ b/pkg/fanal/analyzer/config/dockerfile/docker.go @@ -0,0 +1,54 @@ +package dockerfile + +import ( + "os" + "path/filepath" + "strings" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + version = 1 + analyzerType = analyzer.TypeDockerfile +) + +var requiredFiles = []string{"Dockerfile", "Containerfile"} + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newDockerfileConfigAnalyzer) +} + +// dockerConfigAnalyzer is an analyzer for detecting misconfigurations in Dockerfiles. +// It embeds config.Analyzer so it can implement analyzer.PostAnalyzer. +type dockerConfigAnalyzer struct { + *config.Analyzer +} + +func newDockerfileConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeDockerfile, opts) + if err != nil { + return nil, err + } + return &dockerConfigAnalyzer{Analyzer: a}, nil +} + +// Required does a case-insensitive check for filePath and returns true if +// filePath equals/startsWith/hasExtension requiredFiles +// It overrides config.Analyzer.Required(). +func (a *dockerConfigAnalyzer) Required(filePath string, _ os.FileInfo) bool { + base := filepath.Base(filePath) + ext := filepath.Ext(base) + for _, file := range requiredFiles { + if strings.EqualFold(base, file+ext) { + return true + } + if strings.EqualFold(ext, "."+file) { + return true + } + } + + return false +} diff --git a/pkg/fanal/analyzer/config/dockerfile/docker_test.go b/pkg/fanal/analyzer/config/dockerfile/docker_test.go new file mode 100644 index 000000000000..1a791be8bff1 --- /dev/null +++ b/pkg/fanal/analyzer/config/dockerfile/docker_test.go @@ -0,0 +1,73 @@ +package dockerfile + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_dockerConfigAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "dockerfile", + filePath: "dockerfile", + want: true, + }, + { + name: "Dockerfile", + filePath: "Dockerfile", + want: true, + }, + { + name: "Dockerfile with ext", + filePath: "Dockerfile.build", + want: true, + }, + { + name: "dockerfile as ext", + filePath: "build.dockerfile", + want: true, + }, + { + name: "Dockerfile in dir", + filePath: "docker/Dockerfile", + want: true, + }, + { + name: "Dockerfile as prefix", + filePath: "Dockerfilebuild", + want: false, + }, + { + name: "Dockerfile as suffix", + filePath: "buildDockerfile", + want: false, + }, + { + name: "Dockerfile as prefix with ext", + filePath: "Dockerfilebuild.sh", + want: false, + }, + { + name: "Dockerfile as suffix with ext", + filePath: "buildDockerfile.sh", + want: false, + }, + { + name: "json", + filePath: "deployment.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := dockerConfigAnalyzer{} + got := s.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/config/helm/helm.go b/pkg/fanal/analyzer/config/helm/helm.go new file mode 100644 index 000000000000..42542ce6c835 --- /dev/null +++ b/pkg/fanal/analyzer/config/helm/helm.go @@ -0,0 +1,60 @@ +package helm + +import ( + "os" + "path/filepath" + "strings" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + analyzerType = analyzer.TypeHelm + version = 1 + maxTarSize = 209_715_200 // 200MB +) + +var acceptedExts = []string{".tpl", ".json", ".yml", ".yaml", ".tar", ".tgz", ".tar.gz"} + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newHelmConfigAnalyzer) +} + +// helmConfigAnalyzer is an analyzer for detecting misconfigurations in Helm charts. +// It embeds config.Analyzer so it can implement analyzer.PostAnalyzer. +type helmConfigAnalyzer struct { + *config.Analyzer +} + +func newHelmConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeHelm, opts) + if err != nil { + return nil, err + } + return &helmConfigAnalyzer{Analyzer: a}, nil +} + +// Required overrides config.Analyzer.Required() and checks if the given file is a Helm chart. +func (*helmConfigAnalyzer) Required(filePath string, info os.FileInfo) bool { + if info.Size() > maxTarSize { + // tarball is too big to be Helm chart - move on + return false + } + + for _, acceptable := range acceptedExts { + if strings.HasSuffix(strings.ToLower(filePath), acceptable) { + return true + } + } + + name := filepath.Base(filePath) + for _, acceptable := range []string{"Chart.yaml", ".helmignore"} { + if strings.EqualFold(name, acceptable) { + return true + } + } + + return false +} diff --git a/pkg/fanal/analyzer/config/helm/helm_test.go b/pkg/fanal/analyzer/config/helm/helm_test.go new file mode 100644 index 000000000000..982020769f50 --- /dev/null +++ b/pkg/fanal/analyzer/config/helm/helm_test.go @@ -0,0 +1,75 @@ +package helm + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_helmConfigAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "yaml", + filePath: "Chart.yaml", + want: true, + }, + { + name: "yaml - shorthand", + filePath: "templates/deployment.yml", + want: true, + }, + { + name: "tpl", + filePath: "templates/_helpers.tpl", + want: true, + }, + { + name: "json", + filePath: "values.json", + want: true, + }, + { + name: "NOTES.txt", + filePath: "templates/NOTES.txt", + want: false, + }, + { + name: ".helmignore", + filePath: ".helmignore", + want: true, + }, + { + name: "testchart.tgz", + filePath: "testchart.tgz", + want: true, + }, + { + name: "testchart.tar.gz", + filePath: "testchart.tar.gz", + want: true, + }, + { + name: "nope.tgz", + filePath: "nope.tgz", + want: true, // it's a tarball after all + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := helmConfigAnalyzer{} + + // Create a dummy file info + info, err := os.Stat("./helm_test.go") + require.NoError(t, err) + + got := s.Required(tt.filePath, info) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/config/json/json.go b/pkg/fanal/analyzer/config/json/json.go new file mode 100644 index 000000000000..c2a6eaa73d63 --- /dev/null +++ b/pkg/fanal/analyzer/config/json/json.go @@ -0,0 +1,36 @@ +package json + +import ( + "os" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + analyzerType = analyzer.TypeJSON + version = 1 +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newJSONConfigAnalyzer) +} + +// jsonConfigAnalyzer analyzes JSON files +type jsonConfigAnalyzer struct { + *config.Analyzer +} + +func newJSONConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeJSON, opts) + if err != nil { + return nil, err + } + return &jsonConfigAnalyzer{Analyzer: a}, nil +} + +func (*jsonConfigAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Ext(filePath) == ".json" +} diff --git a/pkg/fanal/analyzer/config/k8s/k8s.go b/pkg/fanal/analyzer/config/k8s/k8s.go new file mode 100644 index 000000000000..dabdf41a990b --- /dev/null +++ b/pkg/fanal/analyzer/config/k8s/k8s.go @@ -0,0 +1,30 @@ +package k8s + +import ( + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + analyzerType = analyzer.TypeKubernetes + version = 1 +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newKubernetesConfigAnalyzer) +} + +// kubernetesConfigAnalyzer is an analyzer for detecting misconfigurations in Kubernetes config files. +// It embeds config.Analyzer so it can implement analyzer.PostAnalyzer. +type kubernetesConfigAnalyzer struct { + *config.Analyzer +} + +func newKubernetesConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeKubernetes, opts) + if err != nil { + return nil, err + } + return &kubernetesConfigAnalyzer{Analyzer: a}, nil +} diff --git a/pkg/fanal/analyzer/config/terraform/terraform.go b/pkg/fanal/analyzer/config/terraform/terraform.go new file mode 100644 index 000000000000..14b683742413 --- /dev/null +++ b/pkg/fanal/analyzer/config/terraform/terraform.go @@ -0,0 +1,37 @@ +package terraform + +import ( + "os" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + analyzerType = analyzer.TypeTerraform + version = 1 +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newTerraformConfigAnalyzer) +} + +// terraformConfigAnalyzer is an analyzer for detecting misconfigurations in Terraform files. +// It embeds config.Analyzer so it can implement analyzer.PostAnalyzer. +type terraformConfigAnalyzer struct { + *config.Analyzer +} + +func newTerraformConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeTerraform, opts) + if err != nil { + return nil, err + } + return &terraformConfigAnalyzer{Analyzer: a}, nil +} + +// Required overrides config.Analyzer.Required() and checks if the given file is a Terraform file. +func (*terraformConfigAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return detection.IsTerraformFile(filePath) +} diff --git a/pkg/fanal/analyzer/config/terraform/terraform_test.go b/pkg/fanal/analyzer/config/terraform/terraform_test.go new file mode 100644 index 000000000000..988a857777cf --- /dev/null +++ b/pkg/fanal/analyzer/config/terraform/terraform_test.go @@ -0,0 +1,58 @@ +package terraform + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfigAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "tf", + filePath: "/path/to/main.tf", + want: true, + }, + { + name: "tf.json", + filePath: "/path/to/main.tf.json", + want: true, + }, + { + name: "tfvars", + filePath: "/path/to/some.tfvars", + want: true, + }, + { + name: "json", + filePath: "/path/to/some.json", + want: false, + }, + { + name: "hcl", + filePath: "/path/to/main.hcl", + want: false, + }, + { + name: "yaml", + filePath: "deployment.yaml", + want: false, + }, + { + name: "manifest snapshot file", + filePath: ".terraform/modules/modules.json", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := terraformConfigAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/config/terraformplan/json/json.go b/pkg/fanal/analyzer/config/terraformplan/json/json.go new file mode 100644 index 000000000000..f0cb2c518549 --- /dev/null +++ b/pkg/fanal/analyzer/config/terraformplan/json/json.go @@ -0,0 +1,44 @@ +package terraformplan + +import ( + "os" + "path/filepath" + + "k8s.io/utils/strings/slices" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + analyzerType = analyzer.TypeTerraformPlanJSON + version = 1 +) + +var requiredExts = []string{ + ".json", +} + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newTerraformPlanJSONConfigAnalyzer) +} + +// terraformPlanConfigAnalyzer is an analyzer for detecting misconfigurations in Terraform Plan files in JSON format. +// It embeds config.Analyzer so it can implement analyzer.PostAnalyzer. +type terraformPlanConfigAnalyzer struct { + *config.Analyzer +} + +func newTerraformPlanJSONConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeTerraformPlanJSON, opts) + if err != nil { + return nil, err + } + return &terraformPlanConfigAnalyzer{Analyzer: a}, nil +} + +// Required overrides config.Analyzer.Required() and checks if the given file is a Terraform Plan file in JSON format. +func (*terraformPlanConfigAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredExts, filepath.Ext(filePath)) +} diff --git a/pkg/fanal/analyzer/config/terraformplan/json/json_test.go b/pkg/fanal/analyzer/config/terraformplan/json/json_test.go new file mode 100644 index 000000000000..0e0ec3d2e9fe --- /dev/null +++ b/pkg/fanal/analyzer/config/terraformplan/json/json_test.go @@ -0,0 +1,38 @@ +package terraformplan + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfigAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path", + filePath: "/path/to/tfplan.json", + want: true, + }, + { + name: "hcl", + filePath: "/path/to/main.hcl", + want: false, + }, + { + name: "yaml", + filePath: "deployment.yaml", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := terraformPlanConfigAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot.go b/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot.go new file mode 100644 index 000000000000..5bac997e6a11 --- /dev/null +++ b/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot.go @@ -0,0 +1,37 @@ +package terraformplan + +import ( + "os" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + analyzerType = analyzer.TypeTerraformPlanSnapshot + version = 1 +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newTerraformPlanSnapshotConfigAnalyzer) +} + +// terraformPlanConfigAnalyzer is an analyzer for detecting misconfigurations in Terraform Plan files in snapshot format. +// It embeds config.Analyzer so it can implement analyzer.PostAnalyzer. +type terraformPlanConfigAnalyzer struct { + *config.Analyzer +} + +func newTerraformPlanSnapshotConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeTerraformPlanSnapshot, opts) + if err != nil { + return nil, err + } + return &terraformPlanConfigAnalyzer{Analyzer: a}, nil +} + +func (*terraformPlanConfigAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Ext(filePath) == ".tfplan" || filepath.Base(filePath) == "tfplan" +} diff --git a/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot_test.go b/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot_test.go new file mode 100644 index 000000000000..22bd69e5f357 --- /dev/null +++ b/pkg/fanal/analyzer/config/terraformplan/snapshot/snapshot_test.go @@ -0,0 +1,38 @@ +package terraformplan + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfigAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "tfplan as extension", + filePath: "/path/to/test.tfplan", + want: true, + }, + { + name: "without extension", + filePath: "/path/to/tfplan", + want: true, + }, + { + name: "bad path", + filePath: "/path/to/mytfplan.txt", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := terraformPlanConfigAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/config/testdata/rego/policy.rego b/pkg/fanal/analyzer/config/testdata/rego/policy.rego new file mode 100644 index 000000000000..ab4daa223c8b --- /dev/null +++ b/pkg/fanal/analyzer/config/testdata/rego/policy.rego @@ -0,0 +1,32 @@ +package user.something + +__rego_metadata__ := { + "id": "TEST001", + "avd_id": "AVD-TEST-0001", + "title": "Test policy", + "short_code": "no-buckets", + "severity": "LOW", + "description": "This is a test policy.", + "recommended_actions": "Have a cup of tea.", + "url": "https://trivy.dev/", +} + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", object.get(metadata, "StartLine", 0)), + "endline": object.get(metadata, "endline", object.get(metadata, "EndLine", 0)), + "filepath": object.get(metadata, "filepath", object.get(metadata, "Path", "")), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + cmd := input.stages[_][_] + res := result("No commands allowed!", cmd) +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/config/testdata/src/Dockerfile b/pkg/fanal/analyzer/config/testdata/src/Dockerfile new file mode 100644 index 000000000000..6d4b4c49ea2e --- /dev/null +++ b/pkg/fanal/analyzer/config/testdata/src/Dockerfile @@ -0,0 +1 @@ +FROM ubuntu \ No newline at end of file diff --git a/pkg/fanal/analyzer/config/yaml/yaml.go b/pkg/fanal/analyzer/config/yaml/yaml.go new file mode 100644 index 000000000000..f8b5569f5bed --- /dev/null +++ b/pkg/fanal/analyzer/config/yaml/yaml.go @@ -0,0 +1,36 @@ +package yaml + +import ( + "os" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +const ( + analyzerType = analyzer.TypeYAML + version = 1 +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzerType, newYAMLConfigAnalyzer) +} + +// yamlConfigAnalyzer analyzes YAML files +type yamlConfigAnalyzer struct { + *config.Analyzer +} + +func newYAMLConfigAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + a, err := config.NewAnalyzer(analyzerType, version, detection.FileTypeYAML, opts) + if err != nil { + return nil, err + } + return &yamlConfigAnalyzer{Analyzer: a}, nil +} + +func (*yamlConfigAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Ext(filePath) == ".yaml" || filepath.Ext(filePath) == ".yml" +} diff --git a/pkg/fanal/analyzer/config_analyzer.go b/pkg/fanal/analyzer/config_analyzer.go new file mode 100644 index 000000000000..cddff6412e6a --- /dev/null +++ b/pkg/fanal/analyzer/config_analyzer.go @@ -0,0 +1,124 @@ +package analyzer + +import ( + "context" + "slices" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/misconf" +) + +var configAnalyzerConstructors = make(map[Type]configAnalyzerConstructor) + +type configAnalyzerConstructor func(ConfigAnalyzerOptions) (ConfigAnalyzer, error) + +// RegisterConfigAnalyzer adds a constructor of config analyzer +func RegisterConfigAnalyzer(t Type, init configAnalyzerConstructor) { + configAnalyzerConstructors[t] = init +} + +// DeregisterConfigAnalyzer is mainly for testing +func DeregisterConfigAnalyzer(t Type) { + delete(configAnalyzerConstructors, t) +} + +// ConfigAnalyzer defines an interface for analyzer of container image config +type ConfigAnalyzer interface { + Type() Type + Version() int + Analyze(ctx context.Context, input ConfigAnalysisInput) (*ConfigAnalysisResult, error) + Required(osFound types.OS) bool +} + +// ConfigAnalyzerOptions is used to initialize config analyzers +type ConfigAnalyzerOptions struct { + FilePatterns []string + DisabledAnalyzers []Type + MisconfScannerOption misconf.ScannerOption + SecretScannerOption SecretScannerOption +} + +type ConfigAnalysisInput struct { + OS types.OS + Config *v1.ConfigFile +} + +type ConfigAnalysisResult struct { + Misconfiguration *types.Misconfiguration + Secret *types.Secret + HistoryPackages types.Packages +} + +func (r *ConfigAnalysisResult) Merge(newResult *ConfigAnalysisResult) { + if newResult == nil { + return + } + if newResult.Misconfiguration != nil { + r.Misconfiguration = newResult.Misconfiguration + } + if newResult.Secret != nil { + r.Secret = newResult.Secret + } + if newResult.HistoryPackages != nil { + r.HistoryPackages = newResult.HistoryPackages + } +} + +type ConfigAnalyzerGroup struct { + configAnalyzers []ConfigAnalyzer +} + +func NewConfigAnalyzerGroup(opts ConfigAnalyzerOptions) (ConfigAnalyzerGroup, error) { + var g ConfigAnalyzerGroup + for t, newConfigAnalyzer := range configAnalyzerConstructors { + // Skip the handler if it is disabled + if slices.Contains(opts.DisabledAnalyzers, t) { + continue + } + a, err := newConfigAnalyzer(opts) + if err != nil { + return ConfigAnalyzerGroup{}, xerrors.Errorf("config analyzer %s initialize error: %w", t, err) + } + + g.configAnalyzers = append(g.configAnalyzers, a) + } + + return g, nil +} + +// AnalyzerVersions returns analyzer version identifier used for cache keys. +func (ag *ConfigAnalyzerGroup) AnalyzerVersions() Versions { + versions := make(map[string]int) + for _, ca := range ag.configAnalyzers { + versions[string(ca.Type())] = ca.Version() + } + return Versions{ + Analyzers: versions, + } +} + +func (ag *ConfigAnalyzerGroup) AnalyzeImageConfig(ctx context.Context, targetOS types.OS, config *v1.ConfigFile) *ConfigAnalysisResult { + input := ConfigAnalysisInput{ + OS: targetOS, + Config: config, + } + result := new(ConfigAnalysisResult) + for _, a := range ag.configAnalyzers { + if !a.Required(targetOS) { + continue + } + + r, err := a.Analyze(ctx, input) + if err != nil { + log.Debug("Image config analysis error", log.Err(err)) + continue + } + + result.Merge(r) + } + return result +} diff --git a/pkg/fanal/analyzer/config_analyzer_test.go b/pkg/fanal/analyzer/config_analyzer_test.go new file mode 100644 index 000000000000..91a386f35eda --- /dev/null +++ b/pkg/fanal/analyzer/config_analyzer_test.go @@ -0,0 +1,164 @@ +package analyzer_test + +import ( + "context" + "errors" + "os" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type mockConfigAnalyzer struct{} + +func newMockConfigAnalyzer(_ analyzer.ConfigAnalyzerOptions) (analyzer.ConfigAnalyzer, error) { + return mockConfigAnalyzer{}, nil +} + +func (mockConfigAnalyzer) Required(targetOS types.OS) bool { + return targetOS.Family == "alpine" +} + +func (mockConfigAnalyzer) Analyze(_ context.Context, input analyzer.ConfigAnalysisInput) (*analyzer.ConfigAnalysisResult, error) { + if input.Config == nil { + return nil, errors.New("error") + } + return &analyzer.ConfigAnalysisResult{ + HistoryPackages: types.Packages{ + { + Name: "musl", + Version: "1.1.24-r2", + }, + }, + }, nil +} + +func (mockConfigAnalyzer) Type() analyzer.Type { + return analyzer.Type("test") +} + +func (mockConfigAnalyzer) Version() int { + return 1 +} + +func TestMain(m *testing.M) { + mock := mockConfigAnalyzer{} + analyzer.RegisterConfigAnalyzer(mock.Type(), newMockConfigAnalyzer) + defer analyzer.DeregisterConfigAnalyzer(mock.Type()) + os.Exit(m.Run()) +} + +func TestAnalyzeConfig(t *testing.T) { + type args struct { + targetOS types.OS + config *v1.ConfigFile + disabledAnalyzers []analyzer.Type + filePatterns []string + } + tests := []struct { + name string + args args + want *analyzer.ConfigAnalysisResult + }{ + { + name: "happy path", + args: args{ + targetOS: types.OS{ + Family: "alpine", + Name: "3.11.6", + }, + config: &v1.ConfigFile{ + OS: "linux", + }, + }, + want: &analyzer.ConfigAnalysisResult{ + HistoryPackages: []types.Package{ + { + Name: "musl", + Version: "1.1.24-r2", + }, + }, + }, + }, + { + name: "non-target OS", + args: args{ + targetOS: types.OS{ + Family: "debian", + Name: "9.2", + }, + config: &v1.ConfigFile{ + OS: "linux", + }, + }, + want: &analyzer.ConfigAnalysisResult{}, + }, + { + name: "Analyze returns an error", + args: args{ + targetOS: types.OS{ + Family: "alpine", + Name: "3.11.6", + }, + }, + want: &analyzer.ConfigAnalysisResult{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := analyzer.NewConfigAnalyzerGroup(analyzer.ConfigAnalyzerOptions{ + FilePatterns: tt.args.filePatterns, + DisabledAnalyzers: tt.args.disabledAnalyzers, + }) + require.NoError(t, err) + got := a.AnalyzeImageConfig(t.Context(), tt.args.targetOS, tt.args.config) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestConfigAnalyzerGroup_AnalyzerVersions(t *testing.T) { + tests := []struct { + name string + disabled []analyzer.Type + want analyzer.Versions + }{ + { + name: "happy path", + disabled: []analyzer.Type{}, + want: analyzer.Versions{ + Analyzers: map[string]int{ + "apk-command": 1, + "test": 1, + }, + }, + }, + { + name: "disable analyzers", + disabled: []analyzer.Type{ + analyzer.TypeAlpine, + analyzer.TypeApkCommand, + }, + want: analyzer.Versions{ + Analyzers: map[string]int{ + "test": 1, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := analyzer.NewConfigAnalyzerGroup(analyzer.ConfigAnalyzerOptions{ + DisabledAnalyzers: tt.disabled, + }) + require.NoError(t, err) + got := a.AnalyzerVersions() + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/const.go b/pkg/fanal/analyzer/const.go new file mode 100644 index 000000000000..9cdcc6b7e079 --- /dev/null +++ b/pkg/fanal/analyzer/const.go @@ -0,0 +1,274 @@ +package analyzer + +import ( + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/detection" +) + +// Type is an alias for types.AnalyzerType for backward compatibility +type Type = types.AnalyzerType + +const ( + // ====== + // OS + // ====== + TypeOSRelease Type = "os-release" + TypeAlpine Type = "alpine" + TypeAmazon Type = "amazon" + TypeAzure Type = "azurelinux" + TypeCBLMariner Type = "cbl-mariner" + TypeDebian Type = "debian" + TypePhoton Type = "photon" + TypeCentOS Type = "centos" + TypeRocky Type = "rocky" + TypeAlma Type = "alma" + TypeFedora Type = "fedora" + TypeOracle Type = "oracle" + TypeRedHatBase Type = "redhat" + TypeSUSE Type = "suse" + TypeUbuntu Type = "ubuntu" + TypeUbuntuESM Type = "ubuntu-esm" + TypeCoreOS Type = "coreos" + + // OS Package + TypeApk Type = "apk" + TypeBottlerocketInventory Type = "bottlerocket-inventory" + TypeDpkg Type = "dpkg" + TypeDpkgLicense Type = "dpkg-license" // For analyzing licenses + TypeRpm Type = "rpm" + TypeRpmArchive Type = "rpm-archive" + TypeRpmqa Type = "rpmqa" + + // OS Package Repository + TypeApkRepo Type = "apk-repo" + + // ============================ + // Programming Language Package + // ============================ + + // Ruby + TypeBundler Type = "bundler" + TypeGemSpec Type = "gemspec" + + // Rust + TypeRustBinary Type = "rustbinary" + TypeCargo Type = "cargo" + + // PHP + TypeComposer Type = "composer" + TypeComposerVendor Type = "composer-vendor" + + // Java + TypeJar Type = "jar" + TypePom Type = "pom" + TypeGradleLock Type = "gradle-lockfile" + TypeSbtLock Type = "sbt-lockfile" + + // Node.js + TypeNpmPkgLock Type = "npm" + TypeNodePkg Type = "node-pkg" + TypeYarn Type = "yarn" + TypePnpm Type = "pnpm" + TypeBun Type = "bun" + + // .NET + TypeNuget Type = "nuget" + TypeDotNetCore Type = "dotnet-core" + TypePackagesProps Type = "packages-props" + + // Conda + TypeCondaPkg Type = "conda-pkg" + TypeCondaEnv Type = "conda-environment" + + // Python + TypePythonPkg Type = "python-pkg" + TypePythonPkgEgg Type = "python-egg" + TypePip Type = "pip" + TypePipenv Type = "pipenv" + TypePoetry Type = "poetry" + TypeUv Type = "uv" + + // Go + TypeGoBinary Type = "gobinary" + TypeGoMod Type = "gomod" + + // C/C++ + TypeConanLock Type = "conan-lock" + + // Elixir + TypeMixLock Type = "mix-lock" + + // Swift + TypeSwift Type = "swift" + TypeCocoaPods Type = "cocoapods" + + // Dart + TypePubSpecLock Type = "pubspec-lock" + + // Julia + TypeJulia Type = "julia" + + // ============ + // Non-packaged + // ============ + TypeExecutable Type = "executable" + TypeSBOM Type = "sbom" + + // ============ + // Image Config + // ============ + TypeApkCommand Type = "apk-command" + TypeHistoryDockerfile Type = "history-dockerfile" + TypeImageConfigSecret Type = "image-config-secret" + + // ================= + // Structured Config + // ================= + TypeAzureARM Type = Type(detection.FileTypeAzureARM) + TypeCloudFormation Type = Type(detection.FileTypeCloudFormation) + TypeDockerfile Type = Type(detection.FileTypeDockerfile) + TypeHelm Type = Type(detection.FileTypeHelm) + TypeKubernetes Type = Type(detection.FileTypeKubernetes) + TypeTerraform Type = Type(detection.FileTypeTerraform) + TypeTerraformPlanJSON Type = Type(detection.FileTypeTerraformPlanJSON) + TypeTerraformPlanSnapshot Type = Type(detection.FileTypeTerraformPlanSnapshot) + TypeYAML Type = Type(detection.FileTypeYAML) + TypeJSON Type = Type(detection.FileTypeJSON) + TypeAnsible Type = Type(detection.FileTypeAnsible) + + // ======== + // License + // ======== + TypeLicenseFile Type = "license-file" + + // ======== + // Secrets + // ======== + TypeSecret Type = "secret" + + // ======= + // Red Hat + // ======= + TypeRedHatContentManifestType Type = "redhat-content-manifest" + TypeRedHatDockerfileType Type = "redhat-dockerfile" +) + +var ( + // TypeOSes has all OS-related analyzers + TypeOSes = []Type{ + TypeOSRelease, + TypeAlpine, + TypeAmazon, + TypeCBLMariner, + TypeCoreOS, + TypeDebian, + TypePhoton, + TypeCentOS, + TypeRocky, + TypeAlma, + TypeFedora, + TypeOracle, + TypeRedHatBase, + TypeSUSE, + TypeUbuntu, + TypeUbuntuESM, + TypeApk, + TypeBottlerocketInventory, + TypeDpkg, + TypeDpkgLicense, + TypeRpm, + TypeRpmqa, + TypeApkRepo, + TypeApkCommand, + } + + // TypeLanguages has all language analyzers + TypeLanguages = []Type{ + TypeBundler, + TypeGemSpec, + TypeCargo, + TypeComposer, + TypeComposerVendor, + TypeJar, + TypePom, + TypeGradleLock, + TypeSbtLock, + TypeNpmPkgLock, + TypeNodePkg, + TypeYarn, + TypePnpm, + TypeBun, + TypeNuget, + TypeDotNetCore, + TypePackagesProps, + TypeCondaPkg, + TypeCondaEnv, + TypePythonPkg, + TypePythonPkgEgg, + TypePip, + TypePipenv, + TypePoetry, + TypeUv, + TypeGoBinary, + TypeGoMod, + TypeRustBinary, + TypeConanLock, + TypeCocoaPods, + TypeSwift, + TypePubSpecLock, + TypeMixLock, + TypeJulia, + TypeSBOM, + } + + // TypeLockfiles has all lock file analyzers + TypeLockfiles = []Type{ + TypeBundler, + TypeNpmPkgLock, + TypeYarn, + TypePnpm, + TypeBun, + TypePip, + TypePipenv, + TypePoetry, + TypeUv, + TypeGoMod, + TypePom, + TypeConanLock, + TypeGradleLock, + TypeSbtLock, + TypeCocoaPods, + TypeSwift, + TypePubSpecLock, + TypeMixLock, + TypeCondaEnv, + TypeComposer, + } + + // TypeIndividualPkgs has all analyzers for individual packages + TypeIndividualPkgs = []Type{ + TypeGemSpec, + TypeNodePkg, + TypeCondaPkg, + TypePythonPkg, + TypeGoBinary, + TypeJar, + TypeRustBinary, + TypeComposerVendor, + } + + // TypeConfigFiles has all config file analyzers + TypeConfigFiles = []Type{ + TypeAzureARM, + TypeCloudFormation, + TypeDockerfile, + TypeHelm, + TypeKubernetes, + TypeTerraform, + TypeTerraformPlanJSON, + TypeTerraformPlanSnapshot, + TypeYAML, + TypeJSON, + TypeAnsible, + } +) diff --git a/pkg/fanal/analyzer/executable/executable.go b/pkg/fanal/analyzer/executable/executable.go new file mode 100644 index 000000000000..9f8aae7d34a9 --- /dev/null +++ b/pkg/fanal/analyzer/executable/executable.go @@ -0,0 +1,53 @@ +package executable + +import ( + "context" + "os" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/utils" +) + +func init() { + analyzer.RegisterAnalyzer(&executableAnalyzer{}) +} + +const version = 1 + +// executableAnalyzer calculates SHA-256 for each binary not managed by package managers (called unpackaged binaries) +// so that it can search for SBOM attestation in post-handler. +type executableAnalyzer struct{} + +func (a executableAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + // Skip non-binaries + isBinary, err := utils.IsBinary(input.Content, input.Info.Size()) + if !isBinary || err != nil { + return nil, nil + } + + dig, err := digest.CalcSHA256FromReader(input.Content) + if err != nil { + return nil, xerrors.Errorf("sha256 error: %w", err) + } + + return &analyzer.AnalysisResult{ + Digests: map[string]string{ + input.FilePath: dig.String(), + }, + }, nil +} + +func (a executableAnalyzer) Required(_ string, fileInfo os.FileInfo) bool { + return utils.IsExecutable(fileInfo) +} + +func (a executableAnalyzer) Type() analyzer.Type { + return analyzer.TypeExecutable +} + +func (a executableAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/executable/executable_test.go b/pkg/fanal/analyzer/executable/executable_test.go new file mode 100644 index 000000000000..161372d6b8a4 --- /dev/null +++ b/pkg/fanal/analyzer/executable/executable_test.go @@ -0,0 +1,53 @@ +package executable + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" +) + +func Test_executableAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + filePath string + want *analyzer.AnalysisResult + }{ + { + name: "binary", + filePath: "testdata/binary", + want: &analyzer.AnalysisResult{ + Digests: map[string]string{ + "testdata/binary": "sha256:9f64a747e1b97f131fabb6b447296c9b6f0201e79fb3c5356e6c77e89b6a806a", + }, + }, + }, + { + name: "text", + filePath: "testdata/hello.txt", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.filePath) + require.NoError(t, err) + defer f.Close() + + stat, err := f.Stat() + require.NoError(t, err) + + a := executableAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.filePath, + Content: f, + Info: stat, + }) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/executable/testdata/binary b/pkg/fanal/analyzer/executable/testdata/binary new file mode 100644 index 000000000000..82090ee2cbea --- /dev/null +++ b/pkg/fanal/analyzer/executable/testdata/binary @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pkg/fanal/analyzer/executable/testdata/hello.txt b/pkg/fanal/analyzer/executable/testdata/hello.txt new file mode 100644 index 000000000000..ce013625030b --- /dev/null +++ b/pkg/fanal/analyzer/executable/testdata/hello.txt @@ -0,0 +1 @@ +hello diff --git a/pkg/fanal/analyzer/fs.go b/pkg/fanal/analyzer/fs.go new file mode 100644 index 000000000000..6f367ce43fea --- /dev/null +++ b/pkg/fanal/analyzer/fs.go @@ -0,0 +1,102 @@ +package analyzer + +import ( + "errors" + "io" + "io/fs" + "os" + "path" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/mapfs" + xos "github.com/aquasecurity/trivy/pkg/x/os" + "github.com/aquasecurity/trivy/pkg/x/sync" +) + +// CompositeFS contains multiple filesystems for post-analyzers +type CompositeFS struct { + dir string + files *sync.Map[Type, *mapfs.FS] +} + +func NewCompositeFS() (*CompositeFS, error) { + tmpDir, err := xos.MkdirTemp("", "analyzer-composite-") + if err != nil { + return nil, xerrors.Errorf("unable to create temporary directory: %w", err) + } + + return &CompositeFS{ + dir: tmpDir, + files: new(sync.Map[Type, *mapfs.FS]), + }, nil +} + +// CopyFileToTemp takes a file path and information, opens the file, copies its contents to a temporary file +func (c *CompositeFS) CopyFileToTemp(opener Opener, _ os.FileInfo) (string, error) { + // Create a temporary file to which the file in the layer will be copied + // so that all the files will not be loaded into memory + f, err := xos.CreateTemp(c.dir, "analyzer-file-") + if err != nil { + return "", xerrors.Errorf("create temp error: %w", err) + } + defer f.Close() + + // Open a file in the layer + r, err := opener() + if err != nil { + return "", xerrors.Errorf("file open error: %w", err) + } + defer r.Close() + + // Copy file content into the temporary file + if _, err = io.Copy(f, r); err != nil { + return "", xerrors.Errorf("copy error: %w", err) + } + + // Use 0600 instead of file permissions to avoid errors when a file uses incorrect permissions (e.g. 0044). + if err = os.Chmod(f.Name(), 0o600); err != nil { + return "", xerrors.Errorf("chmod error: %w", err) + } + + return f.Name(), nil +} + +// CreateLink creates a link in the virtual filesystem that corresponds to a real file. +// The linked virtual file will have the same path as the real file path provided. +func (c *CompositeFS) CreateLink(analyzerTypes []Type, rootDir, virtualPath, realPath string) error { + // Create fs.FS for each post-analyzer that wants to analyze the current file + for _, t := range analyzerTypes { + // Since filesystem scanning may require access outside the specified path, (e.g. Terraform modules) + // it allows "../" access with "WithUnderlyingRoot". + var opts []mapfs.Option + if rootDir != "" { + opts = append(opts, mapfs.WithUnderlyingRoot(rootDir)) + } + mfs, _ := c.files.LoadOrStore(t, mapfs.New(opts...)) + if d := path.Dir(virtualPath); d != "." { + if err := mfs.MkdirAll(d, os.ModePerm); err != nil && !errors.Is(err, fs.ErrExist) { + return xerrors.Errorf("mapfs mkdir error: %w", err) + } + } + if err := mfs.WriteFile(virtualPath, realPath); err != nil { + return xerrors.Errorf("mapfs write error: %w", err) + } + } + return nil +} + +// Set sets the fs.FS for the specified post-analyzer +func (c *CompositeFS) Set(t Type, mfs *mapfs.FS) { + c.files.Store(t, mfs) +} + +// Get returns the fs.FS for the specified post-analyzer +func (c *CompositeFS) Get(t Type) (*mapfs.FS, bool) { + return c.files.Load(t) +} + +// Cleanup removes the temporary directory +func (c *CompositeFS) Cleanup() error { + return os.RemoveAll(c.dir) +} diff --git a/pkg/fanal/analyzer/imgconf/apk/apk.go b/pkg/fanal/analyzer/imgconf/apk/apk.go new file mode 100644 index 000000000000..64302ba9af2f --- /dev/null +++ b/pkg/fanal/analyzer/imgconf/apk/apk.go @@ -0,0 +1,293 @@ +package apk + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + builtinos "os" + "sort" + "strings" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/set" +) + +const ( + envApkIndexArchiveURL = "FANAL_APK_INDEX_ARCHIVE_URL" + analyzerVersion = 1 +) + +var defaultApkIndexArchiveURL = "https://raw.githubusercontent." + + "com/knqyf263/apkIndex-archive/master/alpine/v%s/main/x86_64/history.json" + +func init() { + analyzer.RegisterConfigAnalyzer(analyzer.TypeApkCommand, newAlpineCmdAnalyzer) +} + +type alpineCmdAnalyzer struct { + apkIndexArchiveURL string +} + +func newAlpineCmdAnalyzer(_ analyzer.ConfigAnalyzerOptions) (analyzer.ConfigAnalyzer, error) { + apkIndexArchiveURL := defaultApkIndexArchiveURL + if builtinos.Getenv(envApkIndexArchiveURL) != "" { + apkIndexArchiveURL = builtinos.Getenv(envApkIndexArchiveURL) + } + return alpineCmdAnalyzer{apkIndexArchiveURL: apkIndexArchiveURL}, nil +} + +type apkIndex struct { + Package map[string]archive + Provide provide +} + +type archive struct { + Origin string + Versions version + Dependencies []string + Provides []string +} + +type provide struct { + SO map[string]apk // package which provides the shared object + Package map[string]apk // package which provides the package +} + +type apk struct { + Package string + Versions version +} + +type version map[string]int + +func (a alpineCmdAnalyzer) Analyze(_ context.Context, input analyzer.ConfigAnalysisInput) (*analyzer.ConfigAnalysisResult, error) { + if input.Config == nil { + return nil, nil + } + var apkIndexArchive *apkIndex + var err error + if apkIndexArchive, err = a.fetchApkIndexArchive(input.OS); err != nil { + log.Println(err) + return nil, xerrors.Errorf("failed to fetch apk index archive: %w", err) + } + + pkgs := a.parseConfig(apkIndexArchive, input.Config) + if len(pkgs) == 0 { + return nil, nil + } + + return &analyzer.ConfigAnalysisResult{ + HistoryPackages: pkgs, + }, nil +} +func (a alpineCmdAnalyzer) fetchApkIndexArchive(targetOS types.OS) (*apkIndex, error) { + // 3.9.3 => 3.9 + osVer := targetOS.Name + if strings.Count(osVer, ".") > 1 { + osVer = osVer[:strings.LastIndex(osVer, ".")] + } + + url := fmt.Sprintf(a.apkIndexArchiveURL, osVer) + var reader io.Reader + if strings.HasPrefix(url, "file://") { + var err error + reader, err = builtinos.Open(strings.TrimPrefix(url, "file://")) + if err != nil { + return nil, xerrors.Errorf("failed to read APKINDEX archive file: %w", err) + } + defer reader.(*builtinos.File).Close() + } else { + // nolint + resp, err := http.Get(url) + if err != nil { + return nil, xerrors.Errorf("failed to fetch APKINDEX archive: %w", err) + } + defer resp.Body.Close() + reader = resp.Body + } + apkIndexArchive := &apkIndex{} + if err := json.NewDecoder(reader).Decode(apkIndexArchive); err != nil { + return nil, xerrors.Errorf("failed to decode APKINDEX JSON: %w", err) + } + + return apkIndexArchive, nil +} + +func (a alpineCmdAnalyzer) parseConfig(apkIndexArchive *apkIndex, config *v1.ConfigFile) (packages []types.Package) { + envs := make(map[string]string) + for _, env := range config.Config.Env { + index := strings.Index(env, "=") + envs["$"+env[:index]] = env[index+1:] + } + + uniqPkgs := make(map[string]types.Package) + for _, history := range config.History { + pkgs := a.parseCommand(history.CreatedBy, envs) + pkgs = a.resolveDependencies(apkIndexArchive, pkgs) + results := a.guessVersion(apkIndexArchive, pkgs, history.Created.Time) + for _, result := range results { + result.Identifier.UID = dependency.UID("", result) + uniqPkgs[result.Name] = result + } + } + + return lo.Values(uniqPkgs) +} + +func (a alpineCmdAnalyzer) parseCommand(command string, envs map[string]string) (pkgs []string) { + if strings.Contains(command, "#(nop)") { + return nil + } + + command = strings.TrimPrefix(command, "/bin/sh -c") + var commands []string + for cmd := range strings.SplitSeq(command, "&&") { + for c := range strings.SplitSeq(cmd, ";") { + commands = append(commands, strings.TrimSpace(c)) + } + } + for _, cmd := range commands { + if !strings.HasPrefix(cmd, "apk") { + continue + } + + var add bool + for field := range strings.FieldsSeq(cmd) { + switch { + case strings.HasPrefix(field, "-") || strings.HasPrefix(field, "."): + continue + case field == "add": + add = true + case add: + if strings.HasPrefix(field, "$") { + pkgs = append(pkgs, strings.Fields(envs[field])...) + continue + } + pkgs = append(pkgs, field) + } + } + } + return pkgs +} +func (a alpineCmdAnalyzer) resolveDependencies(apkIndexArchive *apkIndex, originalPkgs []string) (pkgs []string) { + uniqPkgs := set.New[string]() + for _, pkgName := range originalPkgs { + if uniqPkgs.Contains(pkgName) { + continue + } + + seenPkgs := set.New[string]() + for _, p := range a.resolveDependency(apkIndexArchive, pkgName, seenPkgs) { + uniqPkgs.Append(p) + } + } + return uniqPkgs.Items() +} + +func (a alpineCmdAnalyzer) resolveDependency(apkIndexArchive *apkIndex, pkgName string, + seenPkgs set.Set[string]) (pkgNames []string) { + pkg, ok := apkIndexArchive.Package[pkgName] + if !ok { + return nil + } + if seenPkgs.Contains(pkgName) { + return nil + } + seenPkgs.Append(pkgName) + + pkgNames = append(pkgNames, pkgName) + for _, dependency := range pkg.Dependencies { + // sqlite-libs=3.26.0-r3 => sqlite-libs + dependency, _, _ = strings.Cut(dependency, "=") + + if strings.HasPrefix(dependency, "so:") { + soProvidePkg := apkIndexArchive.Provide.SO[dependency[3:]].Package + pkgNames = append(pkgNames, a.resolveDependency(apkIndexArchive, soProvidePkg, seenPkgs)...) + continue + } else if strings.HasPrefix(dependency, "pc:") || strings.HasPrefix(dependency, "cmd:") { + continue + } + pkgProvidePkg, ok := apkIndexArchive.Provide.Package[dependency] + if ok { + pkgNames = append(pkgNames, a.resolveDependency(apkIndexArchive, pkgProvidePkg.Package, seenPkgs)...) + continue + } + pkgNames = append(pkgNames, a.resolveDependency(apkIndexArchive, dependency, seenPkgs)...) + } + return pkgNames +} + +type historyVersion struct { + Version string + BuiltAt int +} + +func (a alpineCmdAnalyzer) guessVersion(apkIndexArchive *apkIndex, originalPkgs []string, + createdAt time.Time) (pkgs []types.Package) { + for _, pkg := range originalPkgs { + archive, ok := apkIndexArchive.Package[pkg] + if !ok { + continue + } + + var historyVersions []historyVersion + for version, builtAt := range archive.Versions { + historyVersions = append(historyVersions, historyVersion{ + Version: version, + BuiltAt: builtAt, + }) + } + sort.Slice(historyVersions, func(i, j int) bool { + return historyVersions[i].BuiltAt < historyVersions[j].BuiltAt + }) + + createdUnix := int(createdAt.Unix()) + var candidateVersion string + for _, historyVersion := range historyVersions { + if historyVersion.BuiltAt <= createdUnix { + candidateVersion = historyVersion.Version + } else if createdUnix < historyVersion.BuiltAt { + break + } + } + if candidateVersion == "" { + continue + } + + pkgs = append(pkgs, types.Package{ + Name: pkg, + Version: candidateVersion, + }) + + // Add origin package name + if archive.Origin != "" && archive.Origin != pkg { + pkgs = append(pkgs, types.Package{ + Name: archive.Origin, + Version: candidateVersion, + }) + } + } + return pkgs +} + +func (a alpineCmdAnalyzer) Required(targetOS types.OS) bool { + return targetOS.Family == types.Alpine +} + +func (a alpineCmdAnalyzer) Type() analyzer.Type { + return analyzer.TypeApkCommand +} + +func (a alpineCmdAnalyzer) Version() int { + return analyzerVersion +} diff --git a/pkg/fanal/analyzer/imgconf/apk/apk_test.go b/pkg/fanal/analyzer/imgconf/apk/apk_test.go new file mode 100644 index 000000000000..f03faf4522b6 --- /dev/null +++ b/pkg/fanal/analyzer/imgconf/apk/apk_test.go @@ -0,0 +1,1713 @@ +package apk + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "reflect" + "sort" + "testing" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/kylelemons/godebug/pretty" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/set" +) + +var ( + oldAlpineConfig = &v1.ConfigFile{ + Architecture: "amd64", + Container: "f5b08762ace1af069127a337579acd51c415b919d736e6615b453a3c6fbf260d", + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 53, 798628678, time.UTC), + }, + DockerVersion: "17.06.2-ce", + History: []v1.History{ + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 11, 22, 19, 38, 885299940, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ADD file:49f9e47e678d868d5b023482aa8dded71276a241a665c4f8b55ca77269321b34 in / ", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 11, 22, 19, 39, 58628442, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 12, 1, 26, 59, 951316015, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 12, 1, 27, 1, 470388635, time.UTC), + }, + CreatedBy: "/bin/sh -c apk add --no-cache --virtual .persistent-deps \t\tca-certificates \t\tcurl \t\ttar \t\txz \t\tlibressl", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 12, 1, 27, 2, 432381785, time.UTC), + }, + CreatedBy: "/bin/sh -c set -x \t&& addgroup -g 82 -S www-data \t&& adduser -u 82 -D -S -G www-data www-data", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 12, 1, 27, 2, 715120309, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_INI_DIR=/usr/local/etc/php", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 12, 1, 27, 3, 655421341, time.UTC), + }, + CreatedBy: "/bin/sh -c mkdir -p $PHP_INI_DIR/conf.d", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 12, 1, 27, 3, 931799562, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 12, 1, 27, 4, 210945499, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 12, 1, 27, 4, 523116501, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.September, 12, 1, 27, 4, 795176159, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV GPG_KEYS=1729F83938DA44E27BA0F4D3DBDB397470D12172 B1B44D8F021E4E2D6021E995DC9FF8D3EE5AF27F", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 2, 18, 415761689, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_VERSION=7.2.11", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 2, 18, 599097853, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_URL=https://secure.php.net/get/php-7.2.11.tar.xz/from/this/mirror PHP_ASC_URL=https://secure.php.net/get/php-7.2.11.tar.xz.asc/from/this/mirror", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 2, 18, 782890412, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_SHA256=da1a705c0bc46410e330fc6baa967666c8cd2985378fb9707c01a8e33b01d985 PHP_MD5=", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 2, 22, 795846753, time.UTC), + }, + CreatedBy: "/bin/sh -c set -xe; \t\tapk add --no-cache --virtual .fetch-deps \t\tgnupg \t\twget \t; \t\tmkdir -p /usr/src; \tcd /usr/src; \t\twget -O php.tar.xz \"$PHP_URL\"; \t\tif [ -n \"$PHP_SHA256\" ]; then \t\techo \"$PHP_SHA256 *php.tar.xz\" | sha256sum -c -; \tfi; \tif [ -n \"$PHP_MD5\" ]; then \t\techo \"$PHP_MD5 *php.tar.xz\" | md5sum -c -; \tfi; \t\tif [ -n \"$PHP_ASC_URL\" ]; then \t\twget -O php.tar.xz.asc \"$PHP_ASC_URL\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \t\tfor key in $GPG_KEYS; do \t\t\tgpg --keyserver ha.pool.sks-keyservers.net --recv-keys \"$key\"; \t\tdone; \t\tgpg --batch --verify php.tar.xz.asc php.tar.xz; \t\tcommand -v gpgconf > /dev/null && gpgconf --kill all; \t\trm -rf \"$GNUPGHOME\"; \tfi; \t\tapk del .fetch-deps", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 2, 23, 71406376, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) COPY file:207c686e3fed4f71f8a7b245d8dcae9c9048d276a326d82b553c12a90af0c0ca in /usr/local/bin/ ", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 7, 13, 93396680, time.UTC), + }, + CreatedBy: "/bin/sh -c set -xe \t&& apk add --no-cache --virtual .build-deps \t\t$PHPIZE_DEPS \t\tcoreutils \t\tcurl-dev \t\tlibedit-dev \t\tlibressl-dev \t\tlibsodium-dev \t\tlibxml2-dev \t\tsqlite-dev \t\t&& export CFLAGS=\"$PHP_CFLAGS\" \t\tCPPFLAGS=\"$PHP_CPPFLAGS\" \t\tLDFLAGS=\"$PHP_LDFLAGS\" \t&& docker-php-source extract \t&& cd /usr/src/php \t&& gnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\" \t&& ./configure \t\t--build=\"$gnuArch\" \t\t--with-config-file-path=\"$PHP_INI_DIR\" \t\t--with-config-file-scan-dir=\"$PHP_INI_DIR/conf.d\" \t\t\t\t--enable-option-checking=fatal \t\t\t\t--with-mhash \t\t\t\t--enable-ftp \t\t--enable-mbstring \t\t--enable-mysqlnd \t\t--with-sodium=shared \t\t\t\t--with-curl \t\t--with-libedit \t\t--with-openssl \t\t--with-zlib \t\t\t\t$(test \"$gnuArch\" = 's390x-linux-gnu' && echo '--without-pcre-jit') \t\t\t\t$PHP_EXTRA_CONFIGURE_ARGS \t&& make -j \"$(nproc)\" \t&& make install \t&& { find /usr/local/bin /usr/local/sbin -type f -perm +0111 -exec strip --strip-all '{}' + || true; } \t&& make clean \t\t&& cp -v php.ini-* \"$PHP_INI_DIR/\" \t\t&& cd / \t&& docker-php-source delete \t\t&& runDeps=\"$( \t\tscanelf --needed --nobanner --format '%n#p' --recursive /usr/local \t\t\t| tr ',' '\\n' \t\t\t| sort -u \t\t\t| awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' \t)\" \t&& apk add --no-cache --virtual .php-rundeps $runDeps \t\t&& apk del .build-deps \t\t&& pecl update-channels \t&& rm -rf /tmp/pear ~/.pearrc", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 7, 13, 722586262, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) COPY multi:2cdcedabcf5a3b9ae610fab7848e94bc2f64b4d85710d55fd6f79e44dacf73d8 in /usr/local/bin/ ", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 7, 14, 618087104, time.UTC), + }, + CreatedBy: "/bin/sh -c docker-php-ext-enable sodium", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 7, 14, 826981756, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENTRYPOINT [\"docker-php-entrypoint\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 19, 7, 15, 10831572, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) CMD [\"php\" \"-a\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 21, 919735971, time.UTC), + }, + CreatedBy: "/bin/sh -c apk --no-cache add git subversion openssh mercurial tini bash patch", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 22, 611763893, time.UTC), + }, + CreatedBy: "/bin/sh -c echo \"memory_limit=-1\" > \"$PHP_INI_DIR/conf.d/memory-limit.ini\" && echo \"date.timezone=${PHP_TIMEZONE:-UTC}\" > \"$PHP_INI_DIR/conf.d/date_timezone.ini\"", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 50, 224278478, time.UTC), + }, + CreatedBy: "/bin/sh -c apk add --no-cache --virtual .build-deps zlib-dev && docker-php-ext-install zip && runDeps=\"$( scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions | tr ',' '\\n' | sort -u | awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' )\" && apk add --virtual .composer-phpext-rundeps $runDeps && apk del .build-deps", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 50, 503010161, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV COMPOSER_ALLOW_SUPERUSER=1", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 50, 775378559, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV COMPOSER_HOME=/tmp", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 51, 35012363, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENV COMPOSER_VERSION=1.7.2", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 52, 491402624, time.UTC), + }, + CreatedBy: "/bin/sh -c curl --silent --fail --location --retry 3 --output /tmp/installer.php --url https://raw.githubusercontent.com/composer/getcomposer.org/b107d959a5924af895807021fcef4ffec5a76aa9/web/installer && php -r \" \\$signature = '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061'; \\$hash = hash('SHA384', file_get_contents('/tmp/installer.php')); if (!hash_equals(\\$signature, \\$hash)) { unlink('/tmp/installer.php'); echo 'Integrity check failed, installer is either corrupt or worse.' . PHP_EOL; exit(1); }\" && php /tmp/installer.php --no-ansi --install-dir=/usr/bin --filename=composer --version=${COMPOSER_VERSION} && composer --ansi --version --no-interaction && rm -rf /tmp/* /tmp/.htaccess", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 52, 948859545, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) COPY file:295943a303e8f27de4302b6aa3687bce4b1d1392335efaaab9ecd37bec5ab4c5 in /docker-entrypoint.sh ", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 53, 295399872, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) WORKDIR /app", + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 53, 582920705, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) ENTRYPOINT [\"/bin/sh\" \"/docker-entrypoint.sh\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{ + Time: time.Date(2018, time.October, 15, 21, 28, 53, 798628678, time.UTC), + }, + CreatedBy: "/bin/sh -c #(nop) CMD [\"composer\"]", + EmptyLayer: true, + }, + }, + OS: "linux", + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888", + }, + { + Algorithm: "sha256", + Hex: "0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + { + Algorithm: "sha256", + Hex: "9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303", + }, + { + Algorithm: "sha256", + Hex: "dc00fbef458ad3204bbb548e2d766813f593d857b845a940a0de76aed94c94d1", + }, + { + Algorithm: "sha256", + Hex: "5cb2a5009179b1e78ecfef81a19756328bb266456cf9a9dbbcf9af8b83b735f0", + }, + { + Algorithm: "sha256", + Hex: "9bdb2c849099a99c8ab35f6fd7469c623635e8f4479a0a5a3df61e22bae509f6", + }, + { + Algorithm: "sha256", + Hex: "6408527580eade39c2692dbb6b0f6a9321448d06ea1c2eef06bb7f37da9c5013", + }, + { + Algorithm: "sha256", + Hex: "83abef706f5ae199af65d1c13d737d0eb36219f0d18e36c6d8ff06159df39a63", + }, + { + Algorithm: "sha256", + Hex: "c03283c257abd289a30b4f5e9e1345da0e9bfdc6ca398ee7e8fac6d2c1456227", + }, + { + Algorithm: "sha256", + Hex: "2da3602d664dd3f71fae83cbc566d4e80b432c6ee8bb4efd94c8e85122f503d4", + }, + { + Algorithm: "sha256", + Hex: "82c59ac8ee582542648e634ca5aff9a464c68ff8a054f105a58689fb52209e34", + }, + { + Algorithm: "sha256", + Hex: "2f4a5c9187c249834ebc28783bd3c65bdcbacaa8baa6620ddaa27846dd3ef708", + }, + { + Algorithm: "sha256", + Hex: "6ca56f561e677ae06c3bc87a70792642d671a4416becb9a101577c1a6e090e36", + }, + { + Algorithm: "sha256", + Hex: "154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + }, + { + Algorithm: "sha256", + Hex: "b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{"composer"}, + Entrypoint: []string{ + "/bin/sh", + "/docker-entrypoint.sh", + }, + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c", + "PHP_INI_DIR=/usr/local/etc/php", + "PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2", + "PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2", + "PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie", + "GPG_KEYS=1729F83938DA44E27BA0F4D3DBDB397470D12172 B1B44D8F021E4E2D6021E995DC9FF8D3EE5AF27F", + "PHP_VERSION=7.2.11", + "PHP_URL=https://secure.php.net/get/php-7.2.11.tar.xz/from/this/mirror", + "PHP_ASC_URL=https://secure.php.net/get/php-7.2.11.tar.xz.asc/from/this/mirror", + "PHP_SHA256=da1a705c0bc46410e330fc6baa967666c8cd2985378fb9707c01a8e33b01d985", + "PHP_MD5=", + "COMPOSER_ALLOW_SUPERUSER=1", + "COMPOSER_HOME=/tmp", + "COMPOSER_VERSION=1.7.2", + }, + Image: "sha256:ad8c55ed62ca1f439bd600c7251de347926ca901ab7f52a93d8fba743ef397c6", + WorkingDir: "/app", + ArgsEscaped: true, + }, + } + + alpineConfig = &v1.ConfigFile{ + Architecture: "amd64", + Container: "47d9d33b3d5abb0316dba1a0bfcbc12a6fa88d98ad30170c41d30718003de82e", + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 20, 331457195, time.UTC)}, + DockerVersion: "18.06.1-ce", + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 0, 7, 3, 358250803, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6ae03397b99ea77f2e9ee901c5c59e59f76f93adbb4035913 in / ", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 0, 7, 3, 510395965, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 43, 80069360, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 44, 655269947, time.UTC)}, + CreatedBy: "/bin/sh -c apk add --no-cache \t\tca-certificates \t\tcurl \t\ttar \t\txz \t\topenssl", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 45, 787769041, time.UTC)}, + CreatedBy: "/bin/sh -c set -x \t&& addgroup -g 82 -S www-data \t&& adduser -u 82 -D -S -G www-data www-data", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 46, 47800659, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_INI_DIR=/usr/local/etc/php", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 47, 131691293, time.UTC)}, + CreatedBy: "/bin/sh -c set -eux; \tmkdir -p \"$PHP_INI_DIR/conf.d\"; \t[ ! -d /var/www/html ]; \tmkdir -p /var/www/html; \tchown www-data:www-data /var/www/html; \tchmod 777 /var/www/html", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 47, 360137598, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 47, 624002469, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 47, 823552655, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 48, 90975339, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV GPG_KEYS=CBAF69F173A0FEA4B537F470D66C9593118BCCB6 F38252826ACD957EF380D39F2F7956BC5DA04B5D", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 48, 311134986, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_VERSION=7.3.5", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 48, 546724822, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_URL=https://www.php.net/get/php-7.3.5.tar.xz/from/this/mirror PHP_ASC_URL=https://www.php.net/get/php-7.3.5.tar.xz.asc/from/this/mirror", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 48, 787069773, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_SHA256=e1011838a46fd4a195c8453b333916622d7ff5bce4aca2d9d99afac142db2472 PHP_MD5=", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 54, 588915046, time.UTC)}, + CreatedBy: "/bin/sh -c set -xe; \t\tapk add --no-cache --virtual .fetch-deps \t\tgnupg \t\twget \t; \t\tmkdir -p /usr/src; \tcd /usr/src; \t\twget -O php.tar.xz \"$PHP_URL\"; \t\tif [ -n \"$PHP_SHA256\" ]; then \t\techo \"$PHP_SHA256 *php.tar.xz\" | sha256sum -c -; \tfi; \tif [ -n \"$PHP_MD5\" ]; then \t\techo \"$PHP_MD5 *php.tar.xz\" | md5sum -c -; \tfi; \t\tif [ -n \"$PHP_ASC_URL\" ]; then \t\twget -O php.tar.xz.asc \"$PHP_ASC_URL\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \t\tfor key in $GPG_KEYS; do \t\t\tgpg --batch --keyserver ha.pool.sks-keyservers.net --recv-keys \"$key\"; \t\tdone; \t\tgpg --batch --verify php.tar.xz.asc php.tar.xz; \t\tcommand -v gpgconf > /dev/null && gpgconf --kill all; \t\trm -rf \"$GNUPGHOME\"; \tfi; \t\tapk del --no-network .fetch-deps", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 4, 54, 868883630, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY file:ce57c04b70896f77cc11eb2766417d8a1240fcffe5bba92179ec78c458844110 in /usr/local/bin/ ", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 12, 28, 585346378, time.UTC)}, + CreatedBy: "/bin/sh -c set -xe \t&& apk add --no-cache --virtual .build-deps \t\t$PHPIZE_DEPS \t\targon2-dev \t\tcoreutils \t\tcurl-dev \t\tlibedit-dev \t\tlibsodium-dev \t\tlibxml2-dev \t\topenssl-dev \t\tsqlite-dev \t\t&& export CFLAGS=\"$PHP_CFLAGS\" \t\tCPPFLAGS=\"$PHP_CPPFLAGS\" \t\tLDFLAGS=\"$PHP_LDFLAGS\" \t&& docker-php-source extract \t&& cd /usr/src/php \t&& gnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\" \t&& ./configure \t\t--build=\"$gnuArch\" \t\t--with-config-file-path=\"$PHP_INI_DIR\" \t\t--with-config-file-scan-dir=\"$PHP_INI_DIR/conf.d\" \t\t\t\t--enable-option-checking=fatal \t\t\t\t--with-mhash \t\t\t\t--enable-ftp \t\t--enable-mbstring \t\t--enable-mysqlnd \t\t--with-password-argon2 \t\t--with-sodium=shared \t\t\t\t--with-curl \t\t--with-libedit \t\t--with-openssl \t\t--with-zlib \t\t\t\t$(test \"$gnuArch\" = 's390x-linux-gnu' && echo '--without-pcre-jit') \t\t\t\t$PHP_EXTRA_CONFIGURE_ARGS \t&& make -j \"$(nproc)\" \t&& find -type f -name '*.a' -delete \t&& make install \t&& { find /usr/local/bin /usr/local/sbin -type f -perm +0111 -exec strip --strip-all '{}' + || true; } \t&& make clean \t\t&& cp -v php.ini-* \"$PHP_INI_DIR/\" \t\t&& cd / \t&& docker-php-source delete \t\t&& runDeps=\"$( \t\tscanelf --needed --nobanner --format '%n#p' --recursive /usr/local \t\t\t| tr ',' '\\n' \t\t\t| sort -u \t\t\t| awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' \t)\" \t&& apk add --no-cache $runDeps \t\t&& apk del --no-network .build-deps \t\t&& pecl update-channels \t&& rm -rf /tmp/pear ~/.pearrc", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 12, 29, 98563791, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY multi:03970f7b3773444b9f7f244f89d3ceeb4253ac6599f0ba0a4c0306c5bf7d1b9b in /usr/local/bin/ ", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 12, 30, 99974579, time.UTC)}, + CreatedBy: "/bin/sh -c docker-php-ext-enable sodium", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 12, 30, 266754534, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENTRYPOINT [\"docker-php-entrypoint\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 3, 12, 30, 414982715, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"php\" \"-a\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 12, 574223281, time.UTC)}, + CreatedBy: "/bin/sh -c apk add --no-cache --virtual .composer-rundeps git subversion openssh mercurial tini bash patch make zip unzip coreutils && apk add --no-cache --virtual .build-deps zlib-dev libzip-dev && docker-php-ext-configure zip --with-libzip && docker-php-ext-install -j$(getconf _NPROCESSORS_ONLN) zip opcache && runDeps=\"$( scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions | tr ',' '\\n' | sort -u | awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' )\" && apk add --no-cache --virtual .composer-phpext-rundeps $runDeps && apk del .build-deps && printf \"# composer php cli ini settings\\ndate.timezone=UTC\\nmemory_limit=-1\\nopcache.enable_cli=1\\n\" > $PHP_INI_DIR/php-cli.ini", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 12, 831274473, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV COMPOSER_ALLOW_SUPERUSER=1", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 13, 3330711, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV COMPOSER_HOME=/tmp", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 18, 503381656, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV COMPOSER_VERSION=1.7.3", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 19, 619504049, time.UTC)}, + CreatedBy: "/bin/sh -c curl --silent --fail --location --retry 3 --output /tmp/installer.php --url https://raw.githubusercontent.com/composer/getcomposer.org/cb19f2aa3aeaa2006c0cd69a7ef011eb31463067/web/installer && php -r \" \\$signature = '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5'; \\$hash = hash('sha384', file_get_contents('/tmp/installer.php')); if (!hash_equals(\\$signature, \\$hash)) { unlink('/tmp/installer.php'); echo 'Integrity check failed, installer is either corrupt or worse.' . PHP_EOL; exit(1); }\" && php /tmp/installer.php --no-ansi --install-dir=/usr/bin --filename=composer --version=${COMPOSER_VERSION} && composer --ansi --version --no-interaction && rm -f /tmp/installer.php", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 19, 803213107, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY file:0bcb2d1c76549e38469db832f5bcfcb4c538b26748a9d4246cc64f35a23280d0 in /docker-entrypoint.sh ", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 19, 987396089, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) WORKDIR /app", + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 20, 159217819, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENTRYPOINT [\"/bin/sh\" \"/docker-entrypoint.sh\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, time.May, 11, 5, 10, 20, 331457195, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"composer\"]", + EmptyLayer: true, + }, + }, + OS: "linux", + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81", + }, + { + Algorithm: "sha256", + Hex: "3575e617b5f4845d72ac357ea1712be9037c1f73e8893fa4a5b887be964f8f59", + }, + { + Algorithm: "sha256", + Hex: "414e112bbb2c35bef0e76708e87a68b521a011a1941fe6d062e30da800c69d1f", + }, + { + Algorithm: "sha256", + Hex: "21f626200b4c7decb2150402d3b801a886ef9dab022d11478eb3240b2a1bb175", + }, + { + Algorithm: "sha256", + Hex: "64a9089492da43bf6f8f3b3b45aafee7d71f1dfd6464477e27b43b4dbe1da341", + }, + { + Algorithm: "sha256", + Hex: "c60e74b6df1608ee7a080978a9f5eddce48dd4d7366b65a5ec00c6e96deabfae", + }, + { + Algorithm: "sha256", + Hex: "489ab25ac6f9d77b5868493bfccc72bcbfaa85d8f393cdd21f3a6cb6e0256c15", + }, + { + Algorithm: "sha256", + Hex: "5a8c7d3402d369f0f5838b74da5c2bd3eaa64c6bbd8d8e11d7ec0affb074c276", + }, + { + Algorithm: "sha256", + Hex: "fe6bde799f85946dbed35f5f614532d68a9f8b62f3f42ae9164740c3d0a6296a", + }, + { + Algorithm: "sha256", + Hex: "40dd29f574f814717669b34efc4ae527a3af0829a2cccb9ec4f077a8cb2766cc", + }, + { + Algorithm: "sha256", + Hex: "0d5d3c0e6691d3c6d24dc782de33d64d490226c503414da0df93b8f605f93da5", + }, + { + Algorithm: "sha256", + Hex: "41467c77644ee108b8ef3e89db7f235ebb720ed4a4041bf746d7342193e6bc7d", + }, + { + Algorithm: "sha256", + Hex: "6a64ec219cdeecfe63aac5b7f43fb3cb6651c6b1a02ebbde6deeabf8a7e3b345", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{"composer"}, + Entrypoint: []string{ + "/bin/sh", + "/docker-entrypoint.sh", + }, + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c", + "PHP_INI_DIR=/usr/local/etc/php", + "PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2", + "PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2", + "PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie", + "GPG_KEYS=CBAF69F173A0FEA4B537F470D66C9593118BCCB6 F38252826ACD957EF380D39F2F7956BC5DA04B5D", + "PHP_VERSION=7.3.5", + "PHP_URL=https://www.php.net/get/php-7.3.5.tar.xz/from/this/mirror", + "PHP_ASC_URL=https://www.php.net/get/php-7.3.5.tar.xz.asc/from/this/mirror", + "PHP_SHA256=e1011838a46fd4a195c8453b333916622d7ff5bce4aca2d9d99afac142db2472", + "PHP_MD5=", + "COMPOSER_ALLOW_SUPERUSER=1", + "COMPOSER_HOME=/tmp", + "COMPOSER_VERSION=1.7.3", + }, + Image: "sha256:45a1f30c00e614b0d90bb2a24affba0a304ff27660ad4717987fefe067cadec8", + WorkingDir: "/app", + ArgsEscaped: true, + }, + } + + wantPkgs = []types.Package{ + { + Name: "acl", + Version: "2.2.52-r5", + Identifier: types.PkgIdentifier{ + UID: "784f131cd326111", + }, + }, + { + Name: "apr", + Version: "1.6.5-r0", + Identifier: types.PkgIdentifier{ + UID: "1e7a6d0bda73a74f", + }, + }, + { + Name: "apr-util", + Version: "1.6.1-r5", + Identifier: types.PkgIdentifier{ + UID: "a2c929c03d9ad61a", + }, + }, + { + Name: "argon2", + Version: "20171227-r1", + Identifier: types.PkgIdentifier{ + UID: "f1f015346e9d54db", + }, + }, + { + Name: "argon2-dev", + Version: "20171227-r1", + Identifier: types.PkgIdentifier{ + UID: "dd027c90469eaea2", + }, + }, + { + Name: "argon2-libs", + Version: "20171227-r1", + Identifier: types.PkgIdentifier{ + UID: "c18902624988b224", + }, + }, + { + Name: "attr", + Version: "2.4.47-r7", + Identifier: types.PkgIdentifier{ + UID: "88e3c95b0bd83fe", + }, + }, + { + Name: "autoconf", + Version: "2.69-r2", + Identifier: types.PkgIdentifier{ + UID: "9282eebaa2edb18e", + }, + }, + { + Name: "bash", + Version: "4.4.19-r1", + Identifier: types.PkgIdentifier{ + UID: "b9623518df2580d7", + }, + }, + { + Name: "binutils", + Version: "2.31.1-r2", + Identifier: types.PkgIdentifier{ + UID: "6eb2b9ef787d20e5", + }, + }, + { + Name: "busybox", + Version: "1.29.3-r10", + Identifier: types.PkgIdentifier{ + UID: "6d4fece8eb9aed1b", + }, + }, + { + Name: "bzip2", + Version: "1.0.6-r6", + Identifier: types.PkgIdentifier{ + UID: "f10a7652e98de81", + }, + }, + { + Name: "ca-certificates", + Version: "20190108-r0", + Identifier: types.PkgIdentifier{ + UID: "78b6dea410b11547", + }, + }, + { + Name: "coreutils", + Version: "8.30-r0", + Identifier: types.PkgIdentifier{ + UID: "a65f04a5f1682ef3", + }, + }, + { + Name: "curl", + Version: "7.64.0-r1", + Identifier: types.PkgIdentifier{ + UID: "653b9f8ab041d5ac", + }, + }, + { + Name: "curl-dev", + Version: "7.64.0-r1", + Identifier: types.PkgIdentifier{ + UID: "cdaa10b4d0045df", + }, + }, + { + Name: "cyrus-sasl", + Version: "2.1.27-r1", + Identifier: types.PkgIdentifier{ + UID: "11f463e17f11fc11", + }, + }, + { + Name: "db", + Version: "5.3.28-r1", + Identifier: types.PkgIdentifier{ + UID: "3c96ed610406070f", + }, + }, + { + Name: "dpkg", + Version: "1.19.2-r0", + Identifier: types.PkgIdentifier{ + UID: "291cdf92161b7a35", + }, + }, + { + Name: "dpkg-dev", + Version: "1.19.2-r0", + Identifier: types.PkgIdentifier{ + UID: "c87dd7f90913b9c0", + }, + }, + { + Name: "expat", + Version: "2.2.6-r0", + Identifier: types.PkgIdentifier{ + UID: "c17cef592b4cd1ac", + }, + }, + { + Name: "file", + Version: "5.36-r0", + Identifier: types.PkgIdentifier{ + UID: "e9eac8d2344654b6", + }, + }, + { + Name: "g++", + Version: "8.3.0-r0", + Identifier: types.PkgIdentifier{ + UID: "b5a17a376ce78648", + }, + }, + { + Name: "gcc", + Version: "8.3.0-r0", + Identifier: types.PkgIdentifier{ + UID: "e0028b3f4edb10d0", + }, + }, + { + Name: "gdbm", + Version: "1.13-r1", + Identifier: types.PkgIdentifier{ + UID: "ddf8257d2b4ffc7b", + }, + }, + { + Name: "git", + Version: "2.20.1-r0", + Identifier: types.PkgIdentifier{ + UID: "7df769c499baac3e", + }, + }, + { + Name: "gmp", + Version: "6.1.2-r1", + Identifier: types.PkgIdentifier{ + UID: "9543ab8b3ef71c6b", + }, + }, + { + Name: "gnupg", + Version: "2.2.12-r0", + Identifier: types.PkgIdentifier{ + UID: "af5a8477a7bb8a39", + }, + }, + { + Name: "gnutls", + Version: "3.6.7-r0", + Identifier: types.PkgIdentifier{ + UID: "7b8292fb2158b405", + }, + }, + { + Name: "isl", + Version: "0.18-r0", + Identifier: types.PkgIdentifier{ + UID: "ff5808fa3be09223", + }, + }, + { + Name: "libacl", + Version: "2.2.52-r5", + Identifier: types.PkgIdentifier{ + UID: "e1110bb9fa71e9b6", + }, + }, + { + Name: "libassuan", + Version: "2.5.1-r0", + Identifier: types.PkgIdentifier{ + UID: "5c27356bfc0c8063", + }, + }, + { + Name: "libatomic", + Version: "8.3.0-r0", + Identifier: types.PkgIdentifier{ + UID: "9c448437636ec536", + }, + }, + { + Name: "libattr", + Version: "2.4.47-r7", + Identifier: types.PkgIdentifier{ + UID: "58d4cb13b94c427c", + }, + }, + { + Name: "libbz2", + Version: "1.0.6-r6", + Identifier: types.PkgIdentifier{ + UID: "b88167f64940af66", + }, + }, + { + Name: "libc-dev", + Version: "0.7.1-r0", + Identifier: types.PkgIdentifier{ + UID: "804732077a4c662b", + }, + }, + { + Name: "libcap", + Version: "2.26-r0", + Identifier: types.PkgIdentifier{ + UID: "9fe6eb7eda727396", + }, + }, + { + Name: "libcrypto1.1", + Version: "1.1.1b-r1", + Identifier: types.PkgIdentifier{ + UID: "68da5e7990c8780c", + }, + }, + { + Name: "libcurl", + Version: "7.64.0-r1", + Identifier: types.PkgIdentifier{ + UID: "49e0a68f77f67462", + }, + }, + { + Name: "libedit", + Version: "20181209.3.1-r0", + Identifier: types.PkgIdentifier{ + UID: "cb42283699ac3423", + }, + }, + { + Name: "libedit-dev", + Version: "20181209.3.1-r0", + Identifier: types.PkgIdentifier{ + UID: "e02c3a224e18a6b2", + }, + }, + { + Name: "libffi", + Version: "3.2.1-r6", + Identifier: types.PkgIdentifier{ + UID: "68833f89f34bd7ec", + }, + }, + { + Name: "libgcc", + Version: "8.3.0-r0", + Identifier: types.PkgIdentifier{ + UID: "27519ea7a3464bc0", + }, + }, + { + Name: "libgcrypt", + Version: "1.8.4-r0", + Identifier: types.PkgIdentifier{ + UID: "f05eadedb8dc0151", + }, + }, + { + Name: "libgomp", + Version: "8.3.0-r0", + Identifier: types.PkgIdentifier{ + UID: "543d8918635c52d6", + }, + }, + { + Name: "libgpg-error", + Version: "1.33-r0", + Identifier: types.PkgIdentifier{ + UID: "db991adc17654512", + }, + }, + { + Name: "libksba", + Version: "1.3.5-r0", + Identifier: types.PkgIdentifier{ + UID: "2bdff3fcdb38fcc9", + }, + }, + { + Name: "libldap", + Version: "2.4.47-r2", + Identifier: types.PkgIdentifier{ + UID: "7d1e18d46af8e64d", + }, + }, + { + Name: "libmagic", + Version: "5.36-r0", + Identifier: types.PkgIdentifier{ + UID: "8de0dc2316c7f08c", + }, + }, + { + Name: "libsasl", + Version: "2.1.27-r1", + Identifier: types.PkgIdentifier{ + UID: "32ade945216e13cb", + }, + }, + { + Name: "libsodium", + Version: "1.0.16-r0", + Identifier: types.PkgIdentifier{ + UID: "7510915bba932b1b", + }, + }, + { + Name: "libsodium-dev", + Version: "1.0.16-r0", + Identifier: types.PkgIdentifier{ + UID: "df3b47abf3f1411f", + }, + }, + { + Name: "libssh2", + Version: "1.8.2-r0", + Identifier: types.PkgIdentifier{ + UID: "7cf64ca05155ca9c", + }, + }, + { + Name: "libssh2-dev", + Version: "1.8.2-r0", + Identifier: types.PkgIdentifier{ + UID: "8afd5b832f1d474c", + }, + }, + { + Name: "libssl1.1", + Version: "1.1.1b-r1", + Identifier: types.PkgIdentifier{ + UID: "b35faf606cddd965", + }, + }, + { + Name: "libstdc++", + Version: "8.3.0-r0", + Identifier: types.PkgIdentifier{ + UID: "e56259710bdc7ded", + }, + }, + { + Name: "libtasn1", + Version: "4.13-r0", + Identifier: types.PkgIdentifier{ + UID: "d78c8e47a85a4185", + }, + }, + { + Name: "libunistring", + Version: "0.9.10-r0", + Identifier: types.PkgIdentifier{ + UID: "5f31755a4db496df", + }, + }, + { + Name: "libuuid", + Version: "2.33-r0", + Identifier: types.PkgIdentifier{ + UID: "af33ae09a75e4ee2", + }, + }, + { + Name: "libxml2", + Version: "2.9.9-r1", + Identifier: types.PkgIdentifier{ + UID: "f772ed5552f4248f", + }, + }, + { + Name: "libxml2-dev", + Version: "2.9.9-r1", + Identifier: types.PkgIdentifier{ + UID: "3aa56b4d41995ecc", + }, + }, + { + Name: "lz4", + Version: "1.8.3-r2", + Identifier: types.PkgIdentifier{ + UID: "16fe8f309f000a", + }, + }, + { + Name: "lz4-libs", + Version: "1.8.3-r2", + Identifier: types.PkgIdentifier{ + UID: "f1cc72d3a4f0e3fa", + }, + }, + { + Name: "m4", + Version: "1.4.18-r1", + Identifier: types.PkgIdentifier{ + UID: "279041efb6311a55", + }, + }, + { + Name: "make", + Version: "4.2.1-r2", + Identifier: types.PkgIdentifier{ + UID: "aedc2f116a0a656", + }, + }, + { + Name: "mercurial", + Version: "4.9.1-r0", + Identifier: types.PkgIdentifier{ + UID: "dd8db352af0fe45d", + }, + }, + { + Name: "mpc1", + Version: "1.0.3-r1", + Identifier: types.PkgIdentifier{ + UID: "4ac00bb3c9d7b863", + }, + }, + { + Name: "mpfr3", + Version: "3.1.5-r1", + Identifier: types.PkgIdentifier{ + UID: "7351997d7d25f69a", + }, + }, + { + Name: "musl", + Version: "1.1.20-r4", + Identifier: types.PkgIdentifier{ + UID: "8e5756f96b3b5f6", + }, + }, + { + Name: "musl-dev", + Version: "1.1.20-r4", + Identifier: types.PkgIdentifier{ + UID: "2232888b0c99c2d", + }, + }, + { + Name: "ncurses", + Version: "6.1_p20190105-r0", + Identifier: types.PkgIdentifier{ + UID: "df982c6c8f287e6a", + }, + }, + { + Name: "ncurses-dev", + Version: "6.1_p20190105-r0", + Identifier: types.PkgIdentifier{ + UID: "80932f0ecaf2d5f8", + }, + }, + { + Name: "ncurses-libs", + Version: "6.1_p20190105-r0", + Identifier: types.PkgIdentifier{ + UID: "d8410f98ecc55ce4", + }, + }, + { + Name: "ncurses-terminfo", + Version: "6.1_p20190105-r0", + Identifier: types.PkgIdentifier{ + UID: "c60fc6e5a37d8a95", + }, + }, + { + Name: "ncurses-terminfo-base", + Version: "6.1_p20190105-r0", + Identifier: types.PkgIdentifier{ + UID: "c2beca25e6a5371f", + }, + }, + { + Name: "nettle", + Version: "3.4.1-r0", + Identifier: types.PkgIdentifier{ + UID: "96dcec63030bedbb", + }, + }, + { + Name: "nghttp2", + Version: "1.35.1-r0", + Identifier: types.PkgIdentifier{ + UID: "ba6c36de650ae267", + }, + }, + { + Name: "nghttp2-dev", + Version: "1.35.1-r0", + Identifier: types.PkgIdentifier{ + UID: "db9600175e13927", + }, + }, + { + Name: "nghttp2-libs", + Version: "1.35.1-r0", + Identifier: types.PkgIdentifier{ + UID: "66cd303081642354", + }, + }, + { + Name: "npth", + Version: "1.6-r0", + Identifier: types.PkgIdentifier{ + UID: "4a8c2366f7da081d", + }, + }, + { + Name: "openldap", + Version: "2.4.47-r2", + Identifier: types.PkgIdentifier{ + UID: "4e116d327ed660e7", + }, + }, + { + Name: "openssh", + Version: "7.9_p1-r5", + Identifier: types.PkgIdentifier{ + UID: "27574c5b357bd209", + }, + }, + { + Name: "openssh-client", + Version: "7.9_p1-r5", + Identifier: types.PkgIdentifier{ + UID: "4d095f61f69debef", + }, + }, + { + Name: "openssh-keygen", + Version: "7.9_p1-r5", + Identifier: types.PkgIdentifier{ + UID: "12cb2bcb1f6c2295", + }, + }, + { + Name: "openssh-server", + Version: "7.9_p1-r5", + Identifier: types.PkgIdentifier{ + UID: "dee48c5c90bff0d6", + }, + }, + { + Name: "openssh-server-common", + Version: "7.9_p1-r5", + Identifier: types.PkgIdentifier{ + UID: "ad06b8d442f8a162", + }, + }, + { + Name: "openssh-sftp-server", + Version: "7.9_p1-r5", + Identifier: types.PkgIdentifier{ + UID: "e0bc3d8e794f06c8", + }, + }, + { + Name: "openssl", + Version: "1.1.1b-r1", + Identifier: types.PkgIdentifier{ + UID: "1c8ae81a9b60513c", + }, + }, + { + Name: "openssl-dev", + Version: "1.1.1b-r1", + Identifier: types.PkgIdentifier{ + UID: "c6549ade045edac", + }, + }, + { + Name: "p11-kit", + Version: "0.23.14-r0", + Identifier: types.PkgIdentifier{ + UID: "4f77fc5f062368fc", + }, + }, + { + Name: "patch", + Version: "2.7.6-r4", + Identifier: types.PkgIdentifier{ + UID: "49b5c14cbee185b7", + }, + }, + { + Name: "pcre2", + Version: "10.32-r1", + Identifier: types.PkgIdentifier{ + UID: "c3733a676cf271ba", + }, + }, + { + Name: "perl", + Version: "5.26.3-r0", + Identifier: types.PkgIdentifier{ + UID: "10e2893a9ea288e", + }, + }, + { + Name: "pinentry", + Version: "1.1.0-r0", + Identifier: types.PkgIdentifier{ + UID: "783866fca9a015bd", + }, + }, + { + Name: "pkgconf", + Version: "1.6.0-r0", + Identifier: types.PkgIdentifier{ + UID: "33d19ebaf35432b0", + }, + }, + { + Name: "python2", + Version: "2.7.16-r1", + Identifier: types.PkgIdentifier{ + UID: "a839b2c9a5f9ba73", + }, + }, + { + Name: "re2c", + Version: "1.1.1-r0", + Identifier: types.PkgIdentifier{ + UID: "ec7d28a39ed7dfb6", + }, + }, + { + Name: "readline", + Version: "7.0.003-r1", + Identifier: types.PkgIdentifier{ + UID: "7d4d5810d005452c", + }, + }, + { + Name: "serf", + Version: "1.3.9-r5", + Identifier: types.PkgIdentifier{ + UID: "7f23c377c19eff2f", + }, + }, + { + Name: "sqlite", + Version: "3.26.0-r3", + Identifier: types.PkgIdentifier{ + UID: "1eab4ef4d3ea8c3c", + }, + }, + { + Name: "sqlite-dev", + Version: "3.26.0-r3", + Identifier: types.PkgIdentifier{ + UID: "4dcddd5956410d59", + }, + }, + { + Name: "sqlite-libs", + Version: "3.26.0-r3", + Identifier: types.PkgIdentifier{ + UID: "9790a2922ebbad67", + }, + }, + { + Name: "subversion", + Version: "1.11.1-r0", + Identifier: types.PkgIdentifier{ + UID: "9de851271909a16", + }, + }, + { + Name: "subversion-libs", + Version: "1.11.1-r0", + Identifier: types.PkgIdentifier{ + UID: "f55079ee39f32296", + }, + }, + { + Name: "tar", + Version: "1.32-r0", + Identifier: types.PkgIdentifier{ + UID: "ee612e956fb56928", + }, + }, + { + Name: "unzip", + Version: "6.0-r4", + Identifier: types.PkgIdentifier{ + UID: "a0da1ecf3082e04", + }, + }, + { + Name: "util-linux", + Version: "2.33-r0", + Identifier: types.PkgIdentifier{ + UID: "1405b9526350c651", + }, + }, + { + Name: "wget", + Version: "1.20.3-r0", + Identifier: types.PkgIdentifier{ + UID: "a5b83a24d7129300", + }, + }, + { + Name: "xz", + Version: "5.2.4-r0", + Identifier: types.PkgIdentifier{ + UID: "253d1bd8b99d6062", + }, + }, + { + Name: "xz-libs", + Version: "5.2.4-r0", + Identifier: types.PkgIdentifier{ + UID: "a42777c05ddb55f3", + }, + }, + { + Name: "zip", + Version: "3.0-r7", + Identifier: types.PkgIdentifier{ + UID: "2039aba6424806a4", + }, + }, + { + Name: "zlib", + Version: "1.2.11-r1", + Identifier: types.PkgIdentifier{ + UID: "b742ac29b1f34e", + }, + }, + { + Name: "zlib-dev", + Version: "1.2.11-r1", + Identifier: types.PkgIdentifier{ + UID: "e9a8669a86602c9d", + }, + }, + } +) + +func TestAnalyze(t *testing.T) { + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, _ *http.Request) { + content, err := os.ReadFile("testdata/history_v3.9.json") + if err != nil { + http.Error(res, err.Error(), http.StatusInternalServerError) + return + } + res.WriteHeader(http.StatusOK) + res.Write(content) + return + })) + defer testServer.Close() + + type args struct { + targetOS types.OS + config *v1.ConfigFile + } + var tests = map[string]struct { + args args + apkIndexArchivePath string + want types.Packages + }{ + "old": { + args: args{ + targetOS: types.OS{ + Family: "alpine", + Name: "3.9.1", + }, + config: oldAlpineConfig, + }, + apkIndexArchivePath: "file://testdata/history_v%s.json", + want: nil, + }, + "new": { + args: args{ + targetOS: types.OS{ + Family: "alpine", + Name: "3.9.1", + }, + config: alpineConfig, + }, + apkIndexArchivePath: "file://testdata/history_v%s.json", + want: wantPkgs, + }, + "https": { + args: args{ + targetOS: types.OS{ + Family: "alpine", + Name: "3.9.1", + }, + config: alpineConfig, + }, + apkIndexArchivePath: testServer.URL + "/%v", + want: wantPkgs, + }, + } + for testName, v := range tests { + t.Run(testName, func(t *testing.T) { + t.Setenv(envApkIndexArchiveURL, v.apkIndexArchivePath) + a, err := newAlpineCmdAnalyzer(analyzer.ConfigAnalyzerOptions{}) + require.NoError(t, err) + result, err := a.Analyze(t.Context(), analyzer.ConfigAnalysisInput{ + OS: v.args.targetOS, + Config: v.args.config, + }) + require.NoError(t, err) + + got := lo.FromPtr(result) + sort.Sort(got.HistoryPackages) + assert.Equal(t, v.want, got.HistoryPackages) + }) + } +} + +func TestParseCommand(t *testing.T) { + var tests = map[string]struct { + command string + envs map[string]string + expected []string + }{ + "no package": { + command: "/bin/sh -c #(nop) ADD file:49f9e47e678d868d5b023482aa8dded71276a241a665c4f8b55ca77269321b34 in / ", + envs: nil, + expected: nil, + }, + "no-cache": { + command: "/bin/sh -c apk add --no-cache --virtual .persistent-deps \t\tca-certificates \t\tcurl \t\ttar \t\txz \t\tlibressl", + envs: nil, + expected: []string{ + "ca-certificates", + "curl", + "tar", + "xz", + "libressl", + }, + }, + // TODO: support $runDeps + "joined by &&": { + command: `/bin/sh -c apk add --no-cache --virtual .build-deps zlib-dev && docker-php-ext-install zip && runDeps=\"$( scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions | tr ',' '\\n' | sort -u | awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' )\" && apk add --virtual .composer-phpext-rundeps $runDeps && apk del .build-deps`, + envs: nil, + expected: []string{"zlib-dev"}, + }, + "joined by ;": { + command: "/bin/sh -c set -xe; \t\tapk add --no-cache --virtual .fetch-deps \t\tgnupg \t\twget \t; \t\tmkdir -p /usr/src; \tcd /usr/src; \t\twget -O php.tar.xz \"$PHP_URL\"; \t\tif [ -n \"$PHP_SHA256\" ]; then \t\techo \"$PHP_SHA256 *php.tar.xz\" | sha256sum -c -; \tfi; \tif [ -n \"$PHP_MD5\" ]; then \t\techo \"$PHP_MD5 *php.tar.xz\" | md5sum -c -; \tfi; \t\tif [ -n \"$PHP_ASC_URL\" ]; then \t\twget -O php.tar.xz.asc \"$PHP_ASC_URL\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \t\tfor key in $GPG_KEYS; do \t\t\tgpg --keyserver ha.pool.sks-keyservers.net --recv-keys \"$key\"; \t\tdone; \t\tgpg --batch --verify php.tar.xz.asc php.tar.xz; \t\tcommand -v gpgconf > /dev/null && gpgconf --kill all; \t\trm -rf \"$GNUPGHOME\"; \tfi; \t\tapk del .fetch-deps", + envs: nil, + expected: []string{ + "gnupg", + "wget", + }, + }, + "ENV": { + command: "/bin/sh -c set -xe \t&& apk add --no-cache --virtual .build-deps \t\t$PHPIZE_DEPS \t\tcoreutils \t\tcurl-dev \t\tlibedit-dev \t\tlibressl-dev \t\tlibsodium-dev \t\tlibxml2-dev \t\tsqlite-dev", + envs: map[string]string{ + "$PHPIZE_DEPS": "autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c", + }, + expected: []string{ + "autoconf", + "dpkg-dev", + "dpkg", + "file", + "g++", + "gcc", + "libc-dev", + "make", + "pkgconf", + "re2c", + "coreutils", + "curl-dev", + "libedit-dev", + "libressl-dev", + "libsodium-dev", + "libxml2-dev", + "sqlite-dev", + }, + }, + } + analyzer := alpineCmdAnalyzer{} + for testName, v := range tests { + actual := analyzer.parseCommand(v.command, v.envs) + assert.Equal(t, v.expected, actual, "[%s]\n%s", testName, pretty.Compare(v.expected, actual)) + } +} + +func TestResolveDependency(t *testing.T) { + var tests = map[string]struct { + pkgName string + apkIndexArchivePath string + want set.Set[string] + }{ + "low": { + pkgName: "libblkid", + apkIndexArchivePath: "testdata/history_v3.9.json", + want: set.New( + "libblkid", + "libuuid", + "musl", + ), + }, + "medium": { + pkgName: "libgcab", + apkIndexArchivePath: "testdata/history_v3.9.json", + want: set.New( + "busybox", + "libblkid", + "libuuid", + "musl", + "libmount", + "pcre", + "glib", + "libgcab", + "libintl", + "zlib", + "libffi", + ), + }, + "high": { + pkgName: "postgresql", + apkIndexArchivePath: "testdata/history_v3.9.json", + want: set.New( + "busybox", + "ncurses-terminfo-base", + "ncurses-terminfo", + "libedit", + "db", + "libsasl", + "libldap", + "libpq", + "postgresql-client", + "tzdata", + "libxml2", + "postgresql", + "musl", + "libcrypto1.1", + "libssl1.1", + "ncurses-libs", + "zlib", + ), + }, + "package alias": { + pkgName: "sqlite-dev", + apkIndexArchivePath: "testdata/history_v3.9.json", + want: set.New( + "sqlite-dev", + "sqlite-libs", + "pkgconf", // pkgconfig => pkgconf + "musl", + ), + }, + "circular dependencies": { + pkgName: "nodejs", + apkIndexArchivePath: "testdata/history_v3.7.json", + want: set.New( + "busybox", + "c-ares", + "ca-certificates", + "http-parser", + "libcrypto1.0", + "libgcc", + "libressl2.6-libcrypto", + "libssl1.0", + "libstdc++", + "libuv", + "musl", + "nodejs", + "nodejs-npm", + "zlib", + ), + }, + } + analyzer := alpineCmdAnalyzer{} + for testName, v := range tests { + f, err := os.Open(v.apkIndexArchivePath) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + apkIndexArchive := &apkIndex{} + if err = json.NewDecoder(f).Decode(&apkIndexArchive); err != nil { + t.Fatalf("unexpected error: %s", err) + } + circularDependencyCheck := set.New[string]() + pkgs := analyzer.resolveDependency(apkIndexArchive, v.pkgName, circularDependencyCheck) + got := set.New(pkgs...) + assert.Equal(t, v.want, got, testName) + } +} + +func TestGuessVersion(t *testing.T) { + var tests = map[string]struct { + apkIndexArchive *apkIndex + pkgs []string + createdAt time.Time + expected []types.Package + }{ + "normal": { + apkIndexArchive: &apkIndex{ + Package: map[string]archive{ + "busybox": { + Versions: map[string]int{ + "1.24.2-r0": 100, + "1.24.2-r1": 200, + "1.24.2-r2": 300, + }, + }, + }, + }, + pkgs: []string{"busybox"}, + createdAt: time.Unix(200, 0), + expected: []types.Package{ + { + Name: "busybox", + Version: "1.24.2-r1", + }, + }, + }, + "unmatched version": { + apkIndexArchive: &apkIndex{ + Package: map[string]archive{ + "busybox": { + Versions: map[string]int{ + "1.24.2-r0": 100, + "1.24.2-r1": 200, + "1.24.2-r2": 300, + }, + }, + }, + }, + pkgs: []string{"busybox"}, + createdAt: time.Unix(50, 0), + expected: nil, + }, + "unmatched package": { + apkIndexArchive: &apkIndex{ + Package: map[string]archive{ + "busybox": { + Versions: map[string]int{ + "1.24.2-r0": 100, + "1.24.2-r1": 200, + "1.24.2-r2": 300, + }, + }, + }, + }, + pkgs: []string{ + "busybox", + "openssl", + }, + createdAt: time.Unix(200, 0), + expected: []types.Package{ + { + Name: "busybox", + Version: "1.24.2-r1", + }, + }, + }, + "origin": { + apkIndexArchive: &apkIndex{ + Package: map[string]archive{ + "sqlite-dev": { + Versions: map[string]int{ + "3.26.0-r0": 100, + "3.26.0-r1": 200, + "3.26.0-r2": 300, + "3.26.0-r3": 400, + }, + Origin: "sqlite", + }, + }, + }, + pkgs: []string{"sqlite-dev"}, + createdAt: time.Unix(500, 0), + expected: []types.Package{ + { + Name: "sqlite-dev", + Version: "3.26.0-r3", + }, + { + Name: "sqlite", + Version: "3.26.0-r3", + }, + }, + }, + } + analyzer := alpineCmdAnalyzer{} + for testName, v := range tests { + actual := analyzer.guessVersion(v.apkIndexArchive, v.pkgs, v.createdAt) + if !reflect.DeepEqual(v.expected, actual) { + t.Errorf("[%s]\n%s", testName, pretty.Compare(v.expected, actual)) + } + } +} diff --git a/pkg/fanal/analyzer/imgconf/apk/testdata/history_v3.7.json b/pkg/fanal/analyzer/imgconf/apk/testdata/history_v3.7.json new file mode 100644 index 000000000000..7995b3babf98 --- /dev/null +++ b/pkg/fanal/analyzer/imgconf/apk/testdata/history_v3.7.json @@ -0,0 +1,78106 @@ +{ + "package": { + "sox-doc": { + "versions": { + "14.4.2-r0": 1510074687 + }, + "origin": "sox" + }, + "alsa-utils-dbg": { + "versions": { + "1.1.4-r0": 1509468852 + }, + "origin": "alsa-utils", + "dependencies": [ + "dialog" + ] + }, + "perl-xml-sax-base": { + "versions": { + "1.09-r0": 1509475899 + }, + "origin": "perl-xml-sax-base", + "dependencies": [ + "perl" + ] + }, + "flite": { + "versions": { + "2.0.0-r0": 1509481030 + }, + "origin": "flite", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libflite.so.1=2.0.0", + "so:libflite_cmu_grapheme_lang.so.1=2.0.0", + "so:libflite_cmu_grapheme_lex.so.1=2.0.0", + "so:libflite_cmu_indic_lang.so.1=2.0.0", + "so:libflite_cmu_indic_lex.so.1=2.0.0", + "so:libflite_cmu_time_awb.so.1=2.0.0", + "so:libflite_cmu_us_awb.so.1=2.0.0", + "so:libflite_cmu_us_kal.so.1=2.0.0", + "so:libflite_cmu_us_kal16.so.1=2.0.0", + "so:libflite_cmu_us_rms.so.1=2.0.0", + "so:libflite_cmu_us_slt.so.1=2.0.0", + "so:libflite_cmulex.so.1=2.0.0", + "so:libflite_usenglish.so.1=2.0.0", + "cmd:flite", + "cmd:flite_cmu_time_awb", + "cmd:flite_cmu_us_awb", + "cmd:flite_cmu_us_kal", + "cmd:flite_cmu_us_kal16", + "cmd:flite_cmu_us_rms", + "cmd:flite_cmu_us_slt", + "cmd:flite_time" + ] + }, + "py3-twitter": { + "versions": { + "3.1-r2": 1509552771 + }, + "origin": "py-twitter", + "dependencies": [ + "py3-future", + "py3-requests", + "py3-requests-oauthlib", + "python3" + ] + }, + "kamailio-sqlite": { + "versions": { + "5.0.7-r0": 1532960874 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "kamailio-db", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0", + "so:libsrdb1.so.1" + ] + }, + "swig": { + "versions": { + "3.0.12-r1": 1509468194 + }, + "origin": "swig", + "dependencies": [ + "guile", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpcre.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:ccache-swig", + "cmd:swig" + ] + }, + "py-chardet": { + "versions": { + "3.0.4-r0": 1509481686 + }, + "origin": "py-chardet" + }, + "openldap-mqtt": { + "versions": { + "2.4.45-r3": 1510258132, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "openldap", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libmosquitto.so.1" + ] + }, + "giblib-doc": { + "versions": { + "1.2.4-r8": 1509470528 + }, + "origin": "giblib" + }, + "perl-test-manifest-doc": { + "versions": { + "2.02-r0": 1509471891 + }, + "origin": "perl-test-manifest" + }, + "xf86-video-s3": { + "versions": { + "0.6.5-r8": 1510074849 + }, + "origin": "xf86-video-s3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "cryptsetup-libs": { + "versions": { + "1.7.5-r1": 1510261295 + }, + "origin": "cryptsetup", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libdevmapper.so.1.02", + "so:libuuid.so.1" + ], + "provides": [ + "so:libcryptsetup.so.4=4.7.0" + ] + }, + "protobuf-vim": { + "versions": { + "3.4.1-r1": 1510846093 + }, + "origin": "protobuf" + }, + "lua5.3-posix": { + "versions": { + "33.4.0-r0": 1509468347 + }, + "origin": "lua-posix", + "dependencies": [ + "lua5.3-bitlib<26", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-archive-zip": { + "versions": { + "1.59-r0": 1509483548 + }, + "origin": "perl-archive-zip", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:crc32" + ] + }, + "txt2man": { + "versions": { + "1.6.0-r0": 1509475656 + }, + "origin": "txt2man", + "dependencies": [ + "gawk" + ], + "provides": [ + "cmd:bookman", + "cmd:src2man", + "cmd:txt2man" + ] + }, + "freshclam": { + "versions": { + "0.100.3-r0": 1555508238 + }, + "origin": "clamav", + "dependencies": [ + "logrotate", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libclamav.so.7", + "so:libcrypto.so.42", + "so:libz.so.1" + ], + "provides": [ + "cmd:freshclam" + ] + }, + "enchant-doc": { + "versions": { + "1.6.0-r12": 1509477682 + }, + "origin": "enchant" + }, + "perl-log-any-doc": { + "versions": { + "1.049-r0": 1509470556 + }, + "origin": "perl-log-any" + }, + "hwdata-usb": { + "versions": { + "0.305-r0": 1509468678 + }, + "origin": "hwdata" + }, + "libldap": { + "versions": { + "2.4.45-r3": 1510258131, + "2.4.46-r0": 1565073937, + "2.4.48-r0": 1566900514 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libsasl2.so.3", + "so:libssl.so.44" + ], + "provides": [ + "so:liblber-2.4.so.2=2.10.11", + "so:libldap-2.4.so.2=2.10.11", + "so:libldap_r-2.4.so.2=2.10.11" + ] + }, + "f2fs-tools-dev": { + "versions": { + "1.6.1-r0": 1509483523 + }, + "origin": "f2fs-tools", + "dependencies": [ + "f2fs-tools-libs=1.6.1-r0" + ] + }, + "libxscrnsaver-dev": { + "versions": { + "1.2.2-r1": 1509480773 + }, + "origin": "libxscrnsaver", + "dependencies": [ + "scrnsaverproto", + "libxext-dev", + "libx11-dev", + "libxscrnsaver=1.2.2-r1", + "pc:scrnsaverproto", + "pc:x11", + "pc:xext", + "pkgconfig" + ], + "provides": [ + "pc:xscrnsaver=1.2.2" + ] + }, + "lua5.3-cqueues": { + "versions": { + "20171014-r0": 1510619111 + }, + "origin": "lua-cqueues", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "samba-dc": { + "versions": { + "4.7.6-r3": 1555491788 + }, + "origin": "samba", + "dependencies": [ + "samba-common=4.7.6-r3", + "samba-server=4.7.6-r3", + "samba-winbind=4.7.6-r3", + "py-samba=4.7.6-r3", + "so:libMESSAGING-samba4.so", + "so:libasn1util-samba4.so", + "so:libauth-samba4.so", + "so:libauth4-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcliauth-samba4.so", + "so:libcluster-samba4.so", + "so:libcmdline-credentials-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdsdb-module-samba4.so", + "so:libevents-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr.so.0", + "so:libnetif-samba4.so", + "so:libpopt.so.0", + "so:libposix-eadb-samba4.so", + "so:libprocess-model-samba4.so", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libservice-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbd-shim-samba4.so", + "so:libtalloc.so.2", + "so:libtdb-wrap-samba4.so", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so" + ], + "provides": [ + "so:libsmbpasswdparser-samba4.so=0", + "cmd:samba", + "cmd:samba-tool", + "cmd:samba_dnsupdate", + "cmd:samba_kcc", + "cmd:samba_spnupdate", + "cmd:samba_upgradedns" + ] + }, + "upower": { + "versions": { + "0.99.6-r0": 1510068214 + }, + "origin": "upower", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libupower-glib.so.3=3.0.1", + "cmd:upower" + ] + }, + "xinput": { + "versions": { + "1.6.2-r0": 1509490860 + }, + "origin": "xinput", + "dependencies": [ + "so:libX11.so.6", + "so:libXi.so.6", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xinput" + ] + }, + "py2-httplib2": { + "versions": { + "0.10.3-r0": 1509476699 + }, + "origin": "py-httplib2", + "dependencies": [ + "python2" + ] + }, + "makedepend-doc": { + "versions": { + "1.0.5-r1": 1509466284 + }, + "origin": "makedepend" + }, + "sems": { + "versions": { + "1.6.0-r6": 1510260897 + }, + "origin": "sems", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libevent_pthreads-2.1.so.6", + "so:libgcc_s.so.1", + "so:libsamplerate.so.0", + "so:libspandsp.so.2", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:sems", + "cmd:sems-get-callproperties", + "cmd:sems-list-active-calls", + "cmd:sems-list-calls", + "cmd:sems-list-finished-calls", + "cmd:sems-logfile-callextract", + "cmd:sems-sbc-get-activeprofile", + "cmd:sems-sbc-get-regex-map-names", + "cmd:sems-sbc-list-profiles", + "cmd:sems-sbc-load-profile", + "cmd:sems-sbc-reload-profile", + "cmd:sems-sbc-reload-profiles", + "cmd:sems-sbc-set-activeprofile", + "cmd:sems-sbc-set-regex-map", + "cmd:sems-stats" + ] + }, + "zfs-utils-py": { + "versions": { + "0.7.3-r0": 1509490074 + }, + "origin": "zfs", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:arc_summary.py", + "cmd:arcstat.py", + "cmd:dbufstat.py" + ] + }, + "dahdi-linux": { + "versions": { + "2.11.1-r0": 1509476068 + }, + "origin": "dahdi-linux" + }, + "libnet-doc": { + "versions": { + "1.1.6-r2": 1509481429 + }, + "origin": "libnet" + }, + "weechat": { + "versions": { + "1.9.1-r1": 1509530223 + }, + "origin": "weechat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libgcrypt.so.20", + "so:libgnutls.so.30", + "so:libncursesw.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:weechat", + "cmd:weechat-curses" + ] + }, + "libmspack-dev": { + "versions": { + "0.8_alpha-r0": 1543321986 + }, + "origin": "libmspack", + "dependencies": [ + "libmspack=0.8_alpha-r0", + "pkgconfig" + ] + }, + "ssmtp-doc": { + "versions": { + "2.64-r12": 1510260822 + }, + "origin": "ssmtp" + }, + "abiword-plugin-sdw": { + "versions": { + "3.0.2-r1": 1510073368 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "xf86-video-glint": { + "versions": { + "1.2.9-r0": 1510073704 + }, + "origin": "xf86-video-glint", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "xcb-util-keysyms": { + "versions": { + "0.4.0-r1": 1509473915 + }, + "origin": "xcb-util-keysyms", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-keysyms.so.1=1.0.0" + ] + }, + "recordmydesktop-doc": { + "versions": { + "0.3.8.1-r2": 1510075368 + }, + "origin": "recordmydesktop" + }, + "xf86-input-synaptics-doc": { + "versions": { + "1.9.0-r1": 1510073649 + }, + "origin": "xf86-input-synaptics" + }, + "gtkglext": { + "versions": { + "1.2.0-r11": 1510075507 + }, + "origin": "gtkglext", + "dependencies": [ + "so:libGL.so.1", + "so:libGLU.so.1", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangox-1.0.so.0" + ], + "provides": [ + "so:libgdkglext-x11-1.0.so.0=0.0.0", + "so:libgtkglext-x11-1.0.so.0=0.0.0" + ] + }, + "py2-itsdangerous": { + "versions": { + "0.24-r3": 1509551858 + }, + "origin": "py-itsdangerous", + "dependencies": [ + "python2" + ] + }, + "libass": { + "versions": { + "0.13.7-r0": 1509480356 + }, + "origin": "libass", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libfribidi.so.0" + ], + "provides": [ + "so:libass.so.9=9.0.1" + ] + }, + "xfce4-mixer": { + "versions": { + "4.11.0-r2": 1510069972 + }, + "origin": "xfce4-mixer", + "dependencies": [ + "hicolor-icon-theme", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdbus-glib-1.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-0.10.so.0", + "so:libgstinterfaces-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libkeybinder.so.0", + "so:libunique-1.0.so.0", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:xfce4-mixer" + ] + }, + "perl-class-returnvalue": { + "versions": { + "0.55-r1": 1510564545 + }, + "origin": "perl-class-returnvalue", + "dependencies": [ + "perl", + "perl-devel-stacktrace" + ] + }, + "py2-libxml2": { + "versions": { + "2.9.8-r1": 1540398579 + }, + "origin": "libxml2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libxml2.so.2" + ] + }, + "dpaste": { + "versions": { + "0.6-r0": 1509477739 + }, + "origin": "sprunge", + "dependencies": [ + "curl" + ], + "provides": [ + "cmd:dpaste" + ] + }, + "py3-purl": { + "versions": { + "1.3.1-r0": 1509483542 + }, + "origin": "py-purl", + "dependencies": [ + "py3-six", + "python3" + ] + }, + "perl-data-ical": { + "versions": { + "0.21-r1": 1510588973 + }, + "origin": "perl-data-ical", + "dependencies": [ + "perl-test-nowarnings", + "perl-test-warn", + "perl-text-vfile-asdata", + "perl-class-returnvalue", + "perl-test-longstring", + "perl-class-accessor", + "perl-file-slurp" + ] + }, + "uwsgi-lua": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:liblua-5.2.so.0" + ] + }, + "perl-path-class-doc": { + "versions": { + "0.37-r0": 1509480564 + }, + "origin": "perl-path-class" + }, + "unbound-migrate": { + "versions": { + "1.6.7-r1": 1510588259 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root" + ], + "provides": [ + "cmd:migrate-dnscache-to-unbound" + ] + }, + "nrpe": { + "versions": { + "2.15-r7": 1510259028 + }, + "origin": "nrpe", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:nrpe" + ] + }, + "xev-doc": { + "versions": { + "1.2.2-r0": 1509491060 + }, + "origin": "xev" + }, + "the_silver_searcher-zsh-completion": { + "versions": { + "2.1.0-r2": 1510831170 + }, + "origin": "the_silver_searcher" + }, + "dhclient": { + "versions": { + "4.3.5-r0": 1509496513 + }, + "origin": "dhcp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "cmd:dhclient", + "cmd:dhclient-script" + ] + }, + "acf-lvm2": { + "versions": { + "0.7.0-r2": 1510072195 + }, + "origin": "acf-lvm2", + "dependencies": [ + "acf-core", + "lvm2" + ] + }, + "clutter": { + "versions": { + "1.26.2-r1": 1510073009 + }, + "origin": "clutter", + "dependencies": [ + "so:libX11.so.6", + "so:libXcomposite.so.1", + "so:libXdamage.so.1", + "so:libXi.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo-gobject.so.2", + "so:libcairo.so.2", + "so:libcogl-pango.so.20", + "so:libcogl-path.so.20", + "so:libcogl.so.20", + "so:libfontconfig.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjson-glib-1.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0" + ], + "provides": [ + "so:libclutter-1.0.so.0=0.2600.2" + ] + }, + "asterisk-doc": { + "versions": { + "15.6.1-r0": 1537795340, + "15.6.2-r0": 1568705002 + }, + "origin": "asterisk" + }, + "wpa_supplicant": { + "versions": { + "2.6-r9": 1534860111, + "2.6-r10": 1559719897, + "2.6-r11": 1568726389 + }, + "origin": "wpa_supplicant", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libdbus-1.so.3", + "so:libnl-3.so.200", + "so:libnl-genl-3.so.200", + "so:libpcsclite.so.1", + "so:libssl.so.44" + ], + "provides": [ + "cmd:eapol_test", + "cmd:wpa_cli", + "cmd:wpa_passphrase", + "cmd:wpa_supplicant" + ] + }, + "pacman": { + "versions": { + "5.0.2-r1": 1510588357 + }, + "origin": "pacman", + "dependencies": [ + "so:libarchive.so.13", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libgpgme.so.11", + "so:libintl.so.8" + ], + "provides": [ + "so:libalpm.so.10=10.0.2", + "cmd:cleanupdelta", + "cmd:makepkg", + "cmd:makepkg-template", + "cmd:pacman", + "cmd:pacman-db-upgrade", + "cmd:pacman-key", + "cmd:pacman-optimize", + "cmd:pacsort", + "cmd:pactree", + "cmd:pkgdelta", + "cmd:repo-add", + "cmd:repo-elephant", + "cmd:repo-remove", + "cmd:testpkg", + "cmd:vercmp" + ] + }, + "gconf-dev": { + "versions": { + "3.2.6-r1": 1510928342 + }, + "origin": "gconf", + "dependencies": [ + "libxml2-dev", + "gtk+3.0-dev", + "polkit-dev", + "orbit2-dev", + "gconf=3.2.6-r1", + "pc:ORBit-2.0", + "pc:gio-2.0", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gconf-2.0=3.2.6" + ] + }, + "eggdbus-doc": { + "versions": { + "0.6-r5": 1509469812 + }, + "origin": "eggdbus" + }, + "xmodmap": { + "versions": { + "1.0.9-r2": 1509473722 + }, + "origin": "xmodmap", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xmodmap" + ] + }, + "subversion-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503664 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "libxfce4ui-lang": { + "versions": { + "4.12.1-r3": 1510067994 + }, + "origin": "libxfce4ui" + }, + "rxvt-unicode-doc": { + "versions": { + "9.22-r2": 1509476789 + }, + "origin": "rxvt-unicode" + }, + "libxfce4util": { + "versions": { + "4.12.1-r4": 1509468547 + }, + "origin": "libxfce4util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libxfce4util.so.7=7.0.0", + "cmd:xfce4-kiosk-query" + ] + }, + "gperf-doc": { + "versions": { + "3.1-r1": 1509462059 + }, + "origin": "gperf" + }, + "abiword-plugin-gdict": { + "versions": { + "3.0.2-r1": 1510073360 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0" + ] + }, + "haserl-doc": { + "versions": { + "0.9.35-r1": 1509468292 + }, + "origin": "haserl" + }, + "py2-vobject": { + "versions": { + "0.9.5-r2": 1510832515 + }, + "origin": "py-vobject", + "dependencies": [ + "python2", + "py2-icu", + "py2-dateutil" + ] + }, + "python2-tests": { + "versions": { + "2.7.15-r2": 1534944322 + }, + "origin": "python2", + "provides": [ + "python-tests=2.7.15-r2" + ] + }, + "xhost": { + "versions": { + "1.0.7-r1": 1509483059 + }, + "origin": "xhost", + "dependencies": [ + "so:libX11.so.6", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xhost" + ] + }, + "seabios-bin": { + "versions": { + "1.10.3-r0": 1510068884 + }, + "origin": "seabios", + "dependencies": [ + "seabios-bin=1.10.3-r0", + "seavgabios-bin=1.10.3-r0" + ] + }, + "perl-protocol-websocket-doc": { + "versions": { + "0.20-r0": 1509474790 + }, + "origin": "perl-protocol-websocket" + }, + "python2-dev": { + "versions": { + "2.7.15-r2": 1534944322 + }, + "origin": "python2", + "dependencies": [ + "pkgconfig", + "python2=2.7.15-r2" + ], + "provides": [ + "python-dev=2.7.15-r2", + "pc:python-2.7=2.7", + "pc:python2=2.7", + "pc:python=2.7", + "cmd:python-config", + "cmd:python2-config", + "cmd:python2.7-config" + ] + }, + "dovecot-fts-lucene": { + "versions": { + "2.2.36.3-r0": 1554102391, + "2.2.36.4-r0": 1567075099 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot", + "so:libc.musl-x86_64.so.1", + "so:libclucene-core.so.1", + "so:libclucene-shared.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:lib21_fts_lucene_plugin.so=0" + ] + }, + "xdg-utils-doc": { + "versions": { + "1.1.2-r0": 1510842481 + }, + "origin": "xdg-utils" + }, + "xf86-input-libinput-doc": { + "versions": { + "0.26.0-r0": 1510303212 + }, + "origin": "xf86-input-libinput" + }, + "pptpd": { + "versions": { + "1.4.0-r1": 1509485779 + }, + "origin": "pptpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bcrelay", + "cmd:pptpctrl", + "cmd:pptpd" + ] + }, + "nagios-plugins-ifoperstatus": { + "versions": { + "2.2.1-r3": 1510288495 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins" + ] + }, + "nagios-plugins-openrc": { + "versions": { + "2.2.1-r3": 1510288499 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "openrc", + "sudo" + ] + }, + "py2-virtualenv": { + "versions": { + "15.1.0-r0": 1512030542 + }, + "origin": "py-virtualenv", + "dependencies": [ + "py2-pip", + "python2" + ], + "provides": [ + "cmd:virtualenv" + ] + }, + "shared-mime-info-lang": { + "versions": { + "1.9-r0": 1509464467 + }, + "origin": "shared-mime-info" + }, + "lame-dev": { + "versions": { + "3.100-r0": 1510088509 + }, + "origin": "lame", + "dependencies": [ + "lame=3.100-r0" + ] + }, + "libexecinfo": { + "versions": { + "1.1-r0": 1509491294 + }, + "origin": "libexecinfo", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libexecinfo.so.1=1" + ] + }, + "rtorrent-doc": { + "versions": { + "0.9.6-r2": 1509492667 + }, + "origin": "rtorrent" + }, + "py-gobject3": { + "versions": { + "3.24.1-r2": 1509481893 + }, + "origin": "py-gobject3" + }, + "libechonest-dev": { + "versions": { + "2.3.1-r0": 1510075923 + }, + "origin": "libechonest", + "dependencies": [ + "qjson-dev", + "qt-dev", + "libechonest=2.3.1-r0", + "pc:QJson", + "pc:QtCore", + "pc:QtNetwork", + "pkgconfig" + ], + "provides": [ + "pc:libechonest=2.3.1" + ] + }, + "herbstluftwm-zsh-completion": { + "versions": { + "0.7.0-r1": 1509551853 + }, + "origin": "herbstluftwm" + }, + "kamailio-db": { + "versions": { + "5.0.7-r0": 1532960874 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libsrdb1.so.1", + "so:libsrdb2.so.1", + "so:libsrutils.so.1", + "so:libtrie.so.1" + ] + }, + "lua-lzmq": { + "versions": { + "0.4.4-r0": 1509475920 + }, + "origin": "lua-lzmq" + }, + "boost-filesystem": { + "versions": { + "1.62.0-r5": 1509465874 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.62.0", + "so:libboost_system.so.1.62.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_filesystem-mt.so.1.62.0=1.62.0", + "so:libboost_filesystem.so.1.62.0=1.62.0" + ] + }, + "pm-utils-dev": { + "versions": { + "1.4.1-r0": 1509491018 + }, + "origin": "pm-utils", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:pm-utils=1.4.1" + ] + }, + "zeromq": { + "versions": { + "4.2.5-r0": 1549279390, + "4.2.5-r1": 1563908217 + }, + "origin": "zeromq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libzmq.so.5" + ], + "provides": [ + "cmd:curve_keygen" + ] + }, + "lua5.1-libs": { + "versions": { + "5.1.5-r3": 1509462458 + }, + "origin": "lua5.1", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblua.so.5=5.1.4" + ] + }, + "farstream0.1": { + "versions": { + "0.1.2-r2": 1510933114 + }, + "origin": "farstream0.1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstnetbuffer-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgstrtp-0.10.so.0", + "so:libnice.so.10" + ], + "provides": [ + "so:libfarstream-0.1.so.0=0.0.1" + ] + }, + "mqtt-exec": { + "versions": { + "0.4-r1": 1531919011 + }, + "origin": "mqtt-exec", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ], + "provides": [ + "cmd:mqtt-exec" + ] + }, + "perl-test-refcount": { + "versions": { + "0.08-r0": 1509477781 + }, + "origin": "perl-test-refcount" + }, + "libxp-doc": { + "versions": { + "1.0.3-r0": 1509494901 + }, + "origin": "libxp" + }, + "acf-alpine-baselayout": { + "versions": { + "0.13.2-r0": 1530045059 + }, + "origin": "acf-alpine-baselayout", + "dependencies": [ + "acf-core", + "lua-json4", + "lua-posix" + ] + }, + "xorg-server-xephyr": { + "versions": { + "1.19.5-r1": 1540838460 + }, + "origin": "xorg-server", + "dependencies": [ + "so:libGL.so.1", + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.0.0", + "so:libepoxy.so.0", + "so:libpixman-1.so.0", + "so:libudev.so.1", + "so:libxcb-icccm.so.4", + "so:libxcb-image.so.0", + "so:libxcb-keysyms.so.1", + "so:libxcb-randr.so.0", + "so:libxcb-render-util.so.0", + "so:libxcb-render.so.0", + "so:libxcb-shape.so.0", + "so:libxcb-shm.so.0", + "so:libxcb-util.so.1", + "so:libxcb-xkb.so.1", + "so:libxcb-xv.so.0", + "so:libxcb.so.1", + "so:libxshmfence.so.1" + ], + "provides": [ + "cmd:Xephyr" + ] + }, + "qemu-cris": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-cris" + ] + }, + "docbook-xml": { + "versions": { + "4.5-r5": 1509459373 + }, + "origin": "docbook-xml", + "dependencies": [ + "libxml2-utils", + "/bin/sh" + ] + }, + "ruby-net-telnet": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ] + }, + "abcde-doc": { + "versions": { + "2.8.1-r1": 1509551899 + }, + "origin": "abcde" + }, + "perl-parse-syslog": { + "versions": { + "1.10-r2": 1509479748 + }, + "origin": "perl-parse-syslog", + "dependencies": [ + "perl" + ] + }, + "libatasmart-doc": { + "versions": { + "0.19-r1": 1509482228 + }, + "origin": "libatasmart" + }, + "samba-heimdal-libs": { + "versions": { + "4.7.6-r3": 1555491787 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2" + ], + "provides": [ + "so:libasn1-samba4.so.8=8.0.0", + "so:libgssapi-samba4.so.2=2.0.0", + "so:libhcrypto-samba4.so.5=5.0.1", + "so:libheimbase-samba4.so.1=1.0.0", + "so:libheimntlm-samba4.so.1=1.0.1", + "so:libhx509-samba4.so.5=5.0.0", + "so:libkrb5-samba4.so.26=26.0.0", + "so:libroken-samba4.so.19=19.0.1", + "so:libwind-samba4.so.0=0.0.0" + ] + }, + "libmpeg2": { + "versions": { + "0.5.1-r7": 1509473558 + }, + "origin": "libmpeg2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmpeg2.so.0=0.1.0", + "so:libmpeg2convert.so.0=0.0.0", + "cmd:corrupt_mpeg2", + "cmd:extract_mpeg2" + ] + }, + "libcroco": { + "versions": { + "0.6.12-r0": 1509467188, + "0.6.12-r1": 1563788970 + }, + "origin": "libcroco", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libcroco-0.6.so.3=3.0.1", + "cmd:csslint-0.6" + ] + }, + "aspell-ru": { + "versions": { + "0.99f7-r1": 1509472871 + }, + "origin": "aspell-ru" + }, + "lxterminal": { + "versions": { + "0.3.0-r2": 1510071921 + }, + "origin": "lxterminal", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libvte.so.9" + ], + "provides": [ + "cmd:lxterminal" + ] + }, + "figlet-doc": { + "versions": { + "2.2.5-r0": 1509480743 + }, + "origin": "figlet" + }, + "libguess": { + "versions": { + "1.2-r0": 1509494184 + }, + "origin": "libguess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libguess.so.1=1.0.0" + ] + }, + "xfce4-power-manager-lang": { + "versions": { + "1.6.0-r2": 1510068245 + }, + "origin": "xfce4-power-manager", + "dependencies": [ + "polkit" + ] + }, + "perl-test-longstring-doc": { + "versions": { + "0.15-r0": 1509470425 + }, + "origin": "perl-test-longstring" + }, + "py3-mock": { + "versions": { + "2.0.0-r3": 1509493400 + }, + "origin": "py-mock", + "dependencies": [ + "py3-pbr", + "py3-six", + "python3" + ] + }, + "py-gobject": { + "versions": { + "2.28.6-r6": 1509471051 + }, + "origin": "py-gobject", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0" + ], + "provides": [ + "so:libpyglib-2.0-python.so.0=0.0.0", + "cmd:pygobject-codegen-2.0" + ] + }, + "py2-cparser": { + "versions": { + "2.18-r0": 1509483348 + }, + "origin": "py-cparser", + "dependencies": [ + "python2" + ] + }, + "mailx-doc": { + "versions": { + "8.1.1-r1": 1509494503 + }, + "origin": "mailx" + }, + "py2-jwt": { + "versions": { + "1.5.0-r1": 1509552755 + }, + "origin": "py-jwt", + "dependencies": [ + "python2" + ] + }, + "syslog-ng": { + "versions": { + "3.9.1-r3": 1510259404 + }, + "origin": "syslog-ng", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libevtlog.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libhiredis.so.0.13", + "so:libpcre.so.1", + "so:libpython2.7.so.1.0", + "so:libssl.so.44" + ], + "provides": [ + "so:libsyslog-ng-3.9.so.0=0.0.0", + "cmd:dqtool", + "cmd:loggen", + "cmd:pdbtool", + "cmd:syslog-ng", + "cmd:syslog-ng-ctl", + "cmd:update-patterndb" + ] + }, + "xproto-doc": { + "versions": { + "7.0.31-r1": 1509461805 + }, + "origin": "xproto" + }, + "sems-voicebox": { + "versions": { + "1.6.0-r6": 1510260897 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "perl-json": { + "versions": { + "2.97000-r0": 1511989778 + }, + "origin": "perl-json" + }, + "boost-serialization": { + "versions": { + "1.62.0-r5": 1509465879 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_serialization-mt.so.1.62.0=1.62.0", + "so:libboost_serialization.so.1.62.0=1.62.0" + ] + }, + "gvfs-fuse": { + "versions": { + "1.34.1-r0": 1511430258, + "1.34.1-r1": 1563787224 + }, + "origin": "gvfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgvfscommon.so" + ] + }, + "mesa-gbm": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libexpat.so.1", + "so:libwayland-client.so.0", + "so:libwayland-server.so.0" + ], + "provides": [ + "so:libgbm.so.1=1.0.0" + ] + }, + "dejagnu-doc": { + "versions": { + "1.6-r1": 1509491945 + }, + "origin": "dejagnu" + }, + "perl-data-uuid-doc": { + "versions": { + "1.221-r1": 1509477323 + }, + "origin": "perl-data-uuid" + }, + "lua5.3-rex-pcre": { + "versions": { + "2.9.0-r0": 1510619366 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ] + }, + "lighttpd-dbg": { + "versions": { + "1.4.48-r0": 1511925915 + }, + "origin": "lighttpd" + }, + "zlib": { + "versions": { + "1.2.11-r1": 1509456391 + }, + "origin": "zlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libz.so.1=1.2.11" + ] + }, + "perl-control-x10-doc": { + "versions": { + "2.09-r1": 1509491053 + }, + "origin": "perl-control-x10" + }, + "openldap-overlay-memberof": { + "versions": { + "2.4.45-r3": 1510258134, + "2.4.46-r0": 1565073940, + "2.4.48-r0": 1566900517 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "apache2-mod-wsgi": { + "versions": { + "4.5.22-r0": 1511871607 + }, + "origin": "apache2-mod-wsgi", + "dependencies": [ + "apache2", + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "py-hgtools": { + "versions": { + "6.5.2-r0": 1509490584 + }, + "origin": "py-hgtools" + }, + "dbus-libs": { + "versions": { + "1.10.24-r0": 1509465101, + "1.10.28-r0": 1560765707 + }, + "origin": "dbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdbus-1.so.3=3.14.16" + ] + }, + "libvpx-utils": { + "versions": { + "1.6.1-r0": 1509480734 + }, + "origin": "libvpx", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libvpx.so.4" + ], + "provides": [ + "cmd:vpxdec", + "cmd:vpxenc" + ] + }, + "libmodplug-dev": { + "versions": { + "0.8.9.0-r0": 1509489448 + }, + "origin": "libmodplug", + "dependencies": [ + "libmodplug=0.8.9.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmodplug=0.8.9.0" + ] + }, + "libedit-doc": { + "versions": { + "20170329.3.1-r3": 1509461725 + }, + "origin": "libedit" + }, + "consolekit2-dev": { + "versions": { + "1.2.0-r2": 1510067529 + }, + "origin": "consolekit2", + "dependencies": [ + "consolekit2=1.2.0-r2", + "pc:dbus-1", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:ck-connector=1.2.0", + "pc:libconsolekit=1.2.0" + ] + }, + "abiword": { + "versions": { + "3.0.2-r1": 1510073371 + }, + "origin": "abiword", + "dependencies": [ + "so:libX11.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libenchant.so.1", + "so:libfontconfig.so.1", + "so:libfribidi.so.0", + "so:libgcc_s.so.1", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgoffice-0.10.so.10", + "so:libgsf-1.so.114", + "so:libgtk-3.so.0", + "so:libjpeg.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpng16.so.16", + "so:librsvg-2.so.2", + "so:libstdc++.so.6", + "so:libwv-1.2.so.4", + "so:libxml2.so.2", + "so:libxslt.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libabiword-3.0.so=0", + "cmd:abiword" + ] + }, + "libtool": { + "versions": { + "2.4.6-r4": 1509456826 + }, + "origin": "libtool", + "dependencies": [ + "bash", + "libltdl" + ], + "provides": [ + "cmd:libtool", + "cmd:libtoolize" + ] + }, + "tiff": { + "versions": { + "4.0.10-r0": 1544169734, + "4.0.10-r1": 1566982294, + "4.0.10-r2": 1572815888 + }, + "origin": "tiff", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:libz.so.1" + ], + "provides": [ + "so:libtiff.so.5=5.4.0" + ] + }, + "nginx-mod-http-set-misc": { + "versions": { + "1.12.2-r4": 1542814448 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "nginx-mod-devel-kit", + "so:libc.musl-x86_64.so.1" + ] + }, + "openntpd": { + "versions": { + "6.2_p3-r0": 1510618938 + }, + "origin": "openntpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtls.so.16" + ], + "provides": [ + "cmd:ntpctl", + "cmd:ntpd" + ] + }, + "librsync-dev": { + "versions": { + "2.0.0-r1": 1509472924 + }, + "origin": "librsync", + "dependencies": [ + "librsync=2.0.0-r1" + ] + }, + "quagga-dbg": { + "versions": { + "1.2.4-r0": 1519133995 + }, + "origin": "quagga", + "dependencies": [ + "iproute2" + ] + }, + "unbound-dbg": { + "versions": { + "1.6.7-r1": 1510588258 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root" + ] + }, + "py2-jinja2": { + "versions": { + "2.9.6-r0": 1509476511 + }, + "origin": "py-jinja2", + "dependencies": [ + "py2-markupsafe", + "python2" + ] + }, + "audacious-plugins-lang": { + "versions": { + "3.9-r0": 1510288540 + }, + "origin": "audacious-plugins", + "dependencies": [ + "audacious" + ] + }, + "lua-json4": { + "versions": { + "1.0.0-r2": 1509468372 + }, + "origin": "lua-json4" + }, + "mariadb-test": { + "versions": { + "10.1.38-r1": 1551187991, + "10.1.40-r0": 1560354888, + "10.1.41-r0": 1565163112 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:my_safe_process", + "cmd:mysql-test", + "cmd:mysql_client_test" + ] + }, + "py3-requests": { + "versions": { + "2.18.4-r0": 1509483041 + }, + "origin": "py-requests", + "dependencies": [ + "py3-chardet", + "py3-idna", + "py3-certifi", + "py3-urllib3", + "python3" + ] + }, + "eudev-doc": { + "versions": { + "3.2.4-r1": 1509466081 + }, + "origin": "eudev" + }, + "libmcrypt-dev": { + "versions": { + "2.5.8-r7": 1509493845 + }, + "origin": "libmcrypt", + "dependencies": [ + "libmcrypt=2.5.8-r7" + ], + "provides": [ + "cmd:libmcrypt-config" + ] + }, + "tinyxml": { + "versions": { + "2.6.2-r1": 1509496005 + }, + "origin": "tinyxml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libtinyxml.so.0=0.2.6.2" + ] + }, + "font-bh-100dpi": { + "versions": { + "1.0.3-r0": 1509491623 + }, + "origin": "font-bh-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "libxau-doc": { + "versions": { + "1.0.8-r2": 1509461811 + }, + "origin": "libxau" + }, + "a52dec-dev": { + "versions": { + "0.7.4-r6": 1510072174 + }, + "origin": "a52dec", + "dependencies": [ + "a52dec=0.7.4-r6" + ] + }, + "perl-data-guid": { + "versions": { + "0.049-r0": 1509479285 + }, + "origin": "perl-data-guid", + "dependencies": [ + "perl-sub-install", + "perl-sub-exporter", + "perl-data-uuid" + ] + }, + "perl-test2-plugin-nowarnings": { + "versions": { + "0.06-r0": 1510588886 + }, + "origin": "perl-test2-plugin-nowarnings", + "dependencies": [ + "perl-ipc-run3", + "perl-test2-suite", + "perl-test-simple" + ] + }, + "bigreqsproto-doc": { + "versions": { + "1.1.2-r3": 1509470423 + }, + "origin": "bigreqsproto" + }, + "lua5.2-dbi-mysql": { + "versions": { + "0.6-r1": 1511483402 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "libaio-dev": { + "versions": { + "0.3.110-r1": 1509471437 + }, + "origin": "libaio", + "dependencies": [ + "libaio=0.3.110-r1" + ] + }, + "py3-asn1-modules": { + "versions": { + "0.2.1-r0": 1511889298 + }, + "origin": "py-asn1-modules", + "dependencies": [ + "py3-openssl", + "py3-asn1" + ] + }, + "libunique3-dev": { + "versions": { + "3.0.2-r0": 1510069893 + }, + "origin": "libunique3", + "dependencies": [ + "libunique3=3.0.2-r0", + "pc:gtk+-3.0", + "pkgconfig" + ], + "provides": [ + "pc:unique-3.0=3.0.2" + ] + }, + "qv4l2": { + "versions": { + "1.12.5-r1": 1510072043 + }, + "origin": "v4l-utils", + "dependencies": [ + "so:libGL.so.1", + "so:libQtCore.so.4", + "so:libQtGui.so.4", + "so:libQtOpenGL.so.4", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libv4l2.so.0", + "so:libv4lconvert.so.0" + ], + "provides": [ + "cmd:qv4l2" + ] + }, + "znc-modpython": { + "versions": { + "1.7.1-r0": 1531900839, + "1.7.1-r1": 1565877330 + }, + "origin": "znc", + "dependencies": [ + "znc", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpython3.6m.so.1.0", + "so:libstdc++.so.6" + ] + }, + "rtpproxy-doc": { + "versions": { + "2.0.0-r4": 1509492566 + }, + "origin": "rtpproxy" + }, + "libassuan-doc": { + "versions": { + "2.4.4-r0": 1512029943 + }, + "origin": "libassuan" + }, + "libsamplerate": { + "versions": { + "0.1.9-r0": 1509473266 + }, + "origin": "libsamplerate", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsamplerate.so.0=0.1.8", + "cmd:sndfile-resample" + ] + }, + "lua5.1-rex-pcre": { + "versions": { + "2.9.0-r0": 1510619366 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ] + }, + "gnokii-smsd": { + "versions": { + "0.6.31-r6": 1510069849 + }, + "origin": "gnokii", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnokii.so.7", + "so:libintl.so.8" + ], + "provides": [ + "cmd:smsd" + ] + }, + "perl-libwww-doc": { + "versions": { + "6.29-r0": 1510038531 + }, + "origin": "perl-libwww" + }, + "libjpeg": { + "versions": { + "8-r6": 1509469919 + }, + "origin": "jpeg", + "dependencies": [ + "libjpeg-turbo" + ] + }, + "perl-term-readkey-doc": { + "versions": { + "2.37-r1": 1509494715 + }, + "origin": "perl-term-readkey" + }, + "x264-libs": { + "versions": { + "20170930-r0": 1509473602 + }, + "origin": "x264", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libx264.so.148=148" + ] + }, + "wireless-tools": { + "versions": { + "30_pre9-r0": 1509479803 + }, + "origin": "wireless-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ifrename", + "cmd:iwconfig", + "cmd:iwevent", + "cmd:iwgetid", + "cmd:iwlist", + "cmd:iwpriv", + "cmd:iwspy" + ] + }, + "rhash-libs": { + "versions": { + "1.3.5-r1": 1509461595 + }, + "origin": "rhash", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:librhash.so.0=0" + ] + }, + "libmowgli": { + "versions": { + "2.0.0-r0": 1509491365 + }, + "origin": "libmowgli", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmowgli-2.so.0=0.0.0" + ] + }, + "diffutils-doc": { + "versions": { + "3.6-r0": 1509459510 + }, + "origin": "diffutils" + }, + "openldap": { + "versions": { + "2.4.45-r3": 1510258136, + "2.4.46-r0": 1565073941, + "2.4.48-r0": 1566900518 + }, + "origin": "openldap", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2", + "so:libltdl.so.7", + "so:libsasl2.so.3", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:slapacl", + "cmd:slapadd", + "cmd:slapauth", + "cmd:slapcat", + "cmd:slapd", + "cmd:slapdn", + "cmd:slapindex", + "cmd:slappasswd", + "cmd:slapschema", + "cmd:slaptest" + ] + }, + "java-common": { + "versions": { + "0.1-r0": 1509482167 + }, + "origin": "java-common", + "dependencies": [ + "/bin/sh" + ] + }, + "perl-fcgi": { + "versions": { + "0.78-r1": 1509474656 + }, + "origin": "perl-fcgi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-db": { + "versions": { + "0.55-r1": 1509477765 + }, + "origin": "perl-db", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ] + }, + "py3-curl": { + "versions": { + "7.43.0-r4": 1510288123 + }, + "origin": "py-curl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libpython3.6m.so.1.0" + ] + }, + "perl-javascript-minifier-doc": { + "versions": { + "1.14-r0": 1509482419 + }, + "origin": "perl-javascript-minifier" + }, + "netcat-openbsd-doc": { + "versions": { + "1.130-r1": 1509481636 + }, + "origin": "netcat-openbsd" + }, + "fprobe-ulog-doc": { + "versions": { + "1.2-r3": 1509496377 + }, + "origin": "fprobe-ulog" + }, + "libexif-dev": { + "versions": { + "0.6.21-r2": 1539006610 + }, + "origin": "libexif", + "dependencies": [ + "libexif=0.6.21-r2", + "pkgconfig" + ], + "provides": [ + "pc:libexif=0.6.21" + ] + }, + "py2-oauth2client": { + "versions": { + "4.1.2-r1": 1509551798 + }, + "origin": "py-oauth2client", + "dependencies": [ + "py2asn1", + "py2httplib2", + "py2asn1-modules", + "py2rsa", + "py2six", + "python2" + ] + }, + "libsodium": { + "versions": { + "1.0.15-r0": 1509474812 + }, + "origin": "libsodium", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsodium.so.23=23.0.0" + ] + }, + "indent": { + "versions": { + "2.2.11-r1": 1509496390 + }, + "origin": "indent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8" + ], + "provides": [ + "cmd:indent", + "cmd:texinfo2man" + ] + }, + "gtk+-dev": { + "versions": { + "2.24.31-r0": 1512030427 + }, + "origin": "gtk+", + "dependencies": [ + "gtk+2.0-dev" + ] + }, + "py3-redis": { + "versions": { + "2.10.5-r1": 1509552807 + }, + "origin": "py-redis", + "dependencies": [ + "python3" + ] + }, + "nagios-plugins-nt": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-twisted": { + "versions": { + "17.1.0-r0": 1509493298 + }, + "origin": "py-twisted", + "dependencies": [ + "py-crypto", + "py-zope-interface", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:cftp", + "cmd:ckeygen", + "cmd:conch", + "cmd:mailmail", + "cmd:pyhtmlizer", + "cmd:tkconch", + "cmd:trial", + "cmd:twist", + "cmd:twistd" + ] + }, + "lua-soap": { + "versions": { + "3.0-r0": 1509494668 + }, + "origin": "lua-soap", + "dependencies": [ + "lua-expat", + "lua-socket" + ] + }, + "pidgin-sipe-lang": { + "versions": { + "1.22.1-r0": 1510931371 + }, + "origin": "pidgin-sipe" + }, + "graphviz-graphs": { + "versions": { + "2.40.1-r0": 1510066920 + }, + "origin": "graphviz" + }, + "nginx-mod-devel-kit": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "acf-squid": { + "versions": { + "0.11.0-r2": 1510076156 + }, + "origin": "acf-squid", + "dependencies": [ + "acf-core", + "squid" + ] + }, + "py-pep8": { + "versions": { + "1.7.0-r0": 1509492633 + }, + "origin": "py-pep8", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:pep8" + ] + }, + "obex-data-server": { + "versions": { + "0.4.6-r4": 1510071752 + }, + "origin": "obex-data-server", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libdbus-glib-1.so.2", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libopenobex.so.2", + "so:libusb-0.1.so.4" + ], + "provides": [ + "cmd:obex-data-server" + ] + }, + "lua-cmsgpack": { + "versions": { + "0.4.0-r0": 1509493359 + }, + "origin": "lua-cmsgpack", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.3-bitlib": { + "versions": { + "5.3.0-r0": 1509468310 + }, + "origin": "lua-bitlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "logtail": { + "versions": { + "3.21-r0": 1509490917 + }, + "origin": "logtail", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:logtail" + ] + }, + "boost-python": { + "versions": { + "1.62.0-r5": 1509465878 + }, + "origin": "boost", + "dependencies": [ + "python", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "so:libboost_python-mt.so.1.62.0=1.62.0", + "so:libboost_python.so.1.62.0=1.62.0" + ] + }, + "weechat-lua": { + "versions": { + "1.9.1-r1": 1509530223 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ] + }, + "sysklogd-doc": { + "versions": { + "1.5.1-r1": 1511795688 + }, + "origin": "sysklogd" + }, + "xorg-server-xnest": { + "versions": { + "1.19.5-r1": 1540838460 + }, + "origin": "xorg-server", + "dependencies": [ + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXext.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.0.0", + "so:libpixman-1.so.0" + ], + "provides": [ + "cmd:Xnest" + ] + }, + "libgnome-doc": { + "versions": { + "2.32.1-r7": 1510931516 + }, + "origin": "libgnome" + }, + "gtk-xfce-engine": { + "versions": { + "3.2.0-r2": 1510920446 + }, + "origin": "gtk-xfce-engine" + }, + "postgresql-dev": { + "versions": { + "10.7-r0": 1554274195, + "10.8-r0": 1557668011, + "10.9-r0": 1562225611, + "10.10-r0": 1565683434 + }, + "origin": "postgresql", + "dependencies": [ + "libressl-dev", + "libpq=10.10-r0", + "pkgconfig", + "postgresql-libs=10.10-r0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "pc:libecpg=10.10", + "pc:libecpg_compat=10.10", + "pc:libpgtypes=10.10", + "pc:libpq=10.10", + "cmd:ecpg", + "cmd:pg_config" + ] + }, + "gnumeric-dev": { + "versions": { + "1.12.36-r0": 1511455615 + }, + "origin": "gnumeric", + "dependencies": [ + "gnumeric=1.12.36-r0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gtk+-3.0", + "pc:libgoffice-0.10", + "pc:libgsf-1", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libspreadsheet-1.12=1.12.36" + ] + }, + "guile-doc": { + "versions": { + "2.0.14-r0": 1509468174 + }, + "origin": "guile" + }, + "x265-dev": { + "versions": { + "2.5-r0": 1509482548 + }, + "origin": "x265", + "dependencies": [ + "pkgconfig", + "x265=2.5-r0" + ], + "provides": [ + "pc:x265=2.5" + ] + }, + "wv-doc": { + "versions": { + "1.2.9-r3": 1509483982 + }, + "origin": "wv" + }, + "pptpd-doc": { + "versions": { + "1.4.0-r1": 1509485776 + }, + "origin": "pptpd" + }, + "duply": { + "versions": { + "2.0.3-r0": 1510588387 + }, + "origin": "duply", + "dependencies": [ + "duplicity", + "bash" + ], + "provides": [ + "cmd:duply" + ] + }, + "xmlto-doc": { + "versions": { + "0.0.28-r2": 1509461795 + }, + "origin": "xmlto" + }, + "libvirt-xen": { + "versions": { + "3.9.0-r1": 1510088352, + "5.5.0-r0": 1562165540 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=5.5.0-r0", + "libvirt-common-drivers=5.5.0-r0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libvirt.so.0", + "so:libxenlight.so.4.9", + "so:libxenstore.so.3.0", + "so:libxentoollog.so.1", + "so:libxlutil.so.4.9", + "so:libxml2.so.2" + ] + }, + "dri3proto-doc": { + "versions": { + "1.0-r2": 1509466244 + }, + "origin": "dri3proto" + }, + "backuppc": { + "versions": { + "3.3.2-r0": 1509492572 + }, + "origin": "backuppc", + "dependencies": [ + "perl", + "busybox", + "rsync", + "perl-archive-zip", + "perl-io-compress", + "perl-libwww", + "perl-file-rsync", + "samba-client" + ] + }, + "libsigc++": { + "versions": { + "2.10.0-r1": 1509472940 + }, + "origin": "libsigc++", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libsigc-2.0.so.0=0.0.0" + ] + }, + "bdftopcf-doc": { + "versions": { + "1.0.5-r1": 1509470411 + }, + "origin": "bdftopcf" + }, + "joe": { + "versions": { + "4.5-r0": 1509494570 + }, + "origin": "joe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:jmacs", + "cmd:joe", + "cmd:jpico", + "cmd:jstar", + "cmd:rjoe" + ] + }, + "rest-doc": { + "versions": { + "0.8.1-r0": 1509492761 + }, + "origin": "rest" + }, + "opensp-lang": { + "versions": { + "1.5.2-r0": 1509488685 + }, + "origin": "opensp" + }, + "libxpm": { + "versions": { + "3.5.12-r0": 1509469832 + }, + "origin": "libxpm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXpm.so.4=4.11.0", + "cmd:cxpm", + "cmd:sxpm" + ] + }, + "perl-test-script": { + "versions": { + "1.23-r0": 1510588920 + }, + "origin": "perl-test-script", + "dependencies": [ + "perl-ipc-run3", + "perl-probe-perl", + "perl-test-simple" + ] + }, + "perl-file-remove-doc": { + "versions": { + "1.57-r0": 1510352929 + }, + "origin": "perl-file-remove" + }, + "perl-crypt-rijndael": { + "versions": { + "1.11-r4": 1509471904 + }, + "origin": "perl-crypt-rijndael", + "dependencies": [ + "perl-test-manifest", + "so:libc.musl-x86_64.so.1" + ] + }, + "xfce4-screenshooter": { + "versions": { + "1.8.2-r1": 1510074738 + }, + "origin": "xfce4-screenshooter", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libexo-1.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libsoup-2.4.so.1", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:xfce4-screenshooter" + ] + }, + "iniparser-dev": { + "versions": { + "4.0-r1": 1509486557 + }, + "origin": "iniparser", + "dependencies": [ + "iniparser=4.0-r1" + ] + }, + "grub-doc": { + "versions": { + "2.02-r3": 1509495739 + }, + "origin": "grub" + }, + "perl-html-formattext-withlinks-andtables-doc": { + "versions": { + "0.07-r0": 1509481579 + }, + "origin": "perl-html-formattext-withlinks-andtables" + }, + "lua5.2-posixtz": { + "versions": { + "0.5-r1": 1509479783 + }, + "origin": "lua-posixtz", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "s6-dns-dev": { + "versions": { + "2.2.0.1-r0": 1509480281 + }, + "origin": "s6-dns", + "dependencies": [ + "s6-dns=2.2.0.1-r0" + ] + }, + "gsm-dev": { + "versions": { + "1.0.16-r0": 1509473211 + }, + "origin": "gsm", + "dependencies": [ + "gsm=1.0.16-r0" + ] + }, + "perl-html-rewriteattributes-doc": { + "versions": { + "0.05-r1": 1510564566 + }, + "origin": "perl-html-rewriteattributes" + }, + "lua5.1-discount": { + "versions": { + "1.2.10.1-r4": 1509479788 + }, + "origin": "lua-discount", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libconfig++": { + "versions": { + "1.5-r3": 1509706433 + }, + "origin": "libconfig", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libconfig++.so.9=9.2.0" + ] + }, + "perl-http-body-doc": { + "versions": { + "1.17-r0": 1509481767 + }, + "origin": "perl-http-body" + }, + "gtk+2.0-doc": { + "versions": { + "2.24.31-r0": 1510066781 + }, + "origin": "gtk+2.0" + }, + "grub": { + "versions": { + "2.02-r3": 1509495739 + }, + "origin": "grub", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02", + "so:libfreetype.so.6", + "so:liblzma.so.5" + ], + "provides": [ + "cmd:grub-bios-setup", + "cmd:grub-editenv", + "cmd:grub-file", + "cmd:grub-fstest", + "cmd:grub-glue-efi", + "cmd:grub-install", + "cmd:grub-kbdcomp", + "cmd:grub-macbless", + "cmd:grub-menulst2cfg", + "cmd:grub-mkconfig", + "cmd:grub-mkfont", + "cmd:grub-mkimage", + "cmd:grub-mklayout", + "cmd:grub-mknetdir", + "cmd:grub-mkpasswd-pbkdf2", + "cmd:grub-mkrelpath", + "cmd:grub-mkrescue", + "cmd:grub-mkstandalone", + "cmd:grub-ofpathname", + "cmd:grub-probe", + "cmd:grub-reboot", + "cmd:grub-render-label", + "cmd:grub-script-check", + "cmd:grub-set-default", + "cmd:grub-sparc64-setup", + "cmd:grub-syslinux2cfg" + ] + }, + "xl2tpd": { + "versions": { + "1.3.10-r0": 1509490847 + }, + "origin": "xl2tpd", + "dependencies": [ + "ppp-l2tp", + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:pfc", + "cmd:xl2tpd", + "cmd:xl2tpd-control" + ] + }, + "py-tdb": { + "versions": { + "1.3.15-r0": 1509486504 + }, + "origin": "tdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libtdb.so.1" + ] + }, + "pigz-doc": { + "versions": { + "2.3.4-r2": 1509494286 + }, + "origin": "pigz" + }, + "open-iscsi": { + "versions": { + "2.0.874-r0": 1509494139 + }, + "origin": "open-iscsi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libisns.so.0", + "so:libmount.so.1" + ], + "provides": [ + "cmd:iscsi-iname", + "cmd:iscsi_discovery", + "cmd:iscsiadm", + "cmd:iscsid", + "cmd:iscsistart", + "cmd:iscsiuio" + ] + }, + "acf-dnscache": { + "versions": { + "0.6.0-r2": 1510074047 + }, + "origin": "acf-dnscache", + "dependencies": [ + "acf-core", + "lua-posix", + "dnscache" + ] + }, + "ldb": { + "versions": { + "1.3.0-r1": 1534939012 + }, + "origin": "ldb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent.so.0" + ], + "provides": [ + "so:libldb.so.1=1.3.0" + ] + }, + "xmlrpc-c-client++": { + "versions": { + "1.39.11-r0": 1509482035 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libxmlrpc++.so.8", + "so:libxmlrpc.so.3", + "so:libxmlrpc_client.so.3", + "so:libxmlrpc_packetsocket.so.8", + "so:libxmlrpc_util++.so.8", + "so:libxmlrpc_util.so.3" + ], + "provides": [ + "so:libxmlrpc_client++.so.8=8.39" + ] + }, + "mercurial-zsh-completion": { + "versions": { + "4.5.2-r0": 1532937171, + "5.4.2-r1": 1522503664, + "4.5.2-r1": 1563792023 + }, + "origin": "mercurial", + "dependencies": [ + "zsh" + ] + }, + "pixman-dev": { + "versions": { + "0.34.0-r3": 1509545247 + }, + "origin": "pixman", + "dependencies": [ + "pixman=0.34.0-r3", + "pkgconfig" + ], + "provides": [ + "pc:pixman-1=0.34.0" + ] + }, + "kamailio-unixodbc": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2", + "so:libsrdb1.so.1" + ] + }, + "bison": { + "versions": { + "3.0.4-r0": 1509456687 + }, + "origin": "bison", + "dependencies": [ + "m4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bison", + "cmd:yacc" + ] + }, + "squid-lang-sl": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-html-formattext-withlinks-doc": { + "versions": { + "0.15-r0": 1509481572 + }, + "origin": "perl-html-formattext-withlinks" + }, + "bwm-ng-doc": { + "versions": { + "0.6.1-r3": 1509475294 + }, + "origin": "bwm-ng" + }, + "lua-posixtz": { + "versions": { + "0.5-r1": 1509479785 + }, + "origin": "lua-posixtz" + }, + "libxtst-doc": { + "versions": { + "1.2.3-r1": 1509466038 + }, + "origin": "libxtst" + }, + "libverto-dev": { + "versions": { + "0.3.0-r0": 1509469586 + }, + "origin": "libverto", + "dependencies": [ + "libverto-glib=0.3.0-r0", + "libverto-libev=0.3.0-r0", + "libverto-libevent=0.3.0-r0", + "libverto=0.3.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libverto-glib=0.3.0", + "pc:libverto-libev=0.3.0", + "pc:libverto-libevent=0.3.0", + "pc:libverto=0.3.0" + ] + }, + "perl-www-robotrules-doc": { + "versions": { + "6.02-r1": 1509464407 + }, + "origin": "perl-www-robotrules" + }, + "nagios-plugins-uptime": { + "versions": { + "2.2.1-r3": 1510288498 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "hexchat-lang": { + "versions": { + "2.12.4-r1": 1510260816 + }, + "origin": "hexchat" + }, + "libcrystalhd-dev": { + "versions": { + "20130708-r2": 1509493202 + }, + "origin": "libcrystalhd", + "dependencies": [ + "libcrystalhd=20130708-r2" + ] + }, + "gtkglext-dev": { + "versions": { + "1.2.0-r11": 1510075506 + }, + "origin": "gtkglext", + "dependencies": [ + "gtk+2.0-dev", + "mesa-dev", + "libice-dev", + "libxxf86vm-dev", + "libxi-dev", + "libx11-dev", + "libxt-dev", + "glu-dev", + "pangox-compat-dev", + "gtkglext=1.2.0-r11", + "pc:gdk-2.0", + "pc:gmodule-2.0", + "pc:gtk+-2.0", + "pc:pango", + "pc:pangox", + "pkgconfig" + ], + "provides": [ + "pc:gdkglext-1.0=1.2.0", + "pc:gdkglext-x11-1.0=1.2.0", + "pc:gtkglext-1.0=1.2.0", + "pc:gtkglext-x11-1.0=1.2.0" + ] + }, + "atk-doc": { + "versions": { + "2.26.1-r1": 1509464672 + }, + "origin": "atk" + }, + "perl-file-sharedir-install-doc": { + "versions": { + "0.11-r0": 1511889856 + }, + "origin": "perl-file-sharedir-install" + }, + "py3-flask": { + "versions": { + "0.12.2-r1": 1509551869 + }, + "origin": "py-flask", + "dependencies": [ + "py3-click", + "py3-itsdangerous", + "py3-jinja2", + "py3-werkzeug", + "python3" + ], + "provides": [ + "cmd:flask" + ] + }, + "xfce4-battery-plugin": { + "versions": { + "1.0.5-r2": 1510072895 + }, + "origin": "xfce4-battery-plugin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7" + ] + }, + "docbook2x": { + "versions": { + "0.8.8-r6": 1509489153 + }, + "origin": "docbook2x", + "dependencies": [ + "texinfo", + "openjade", + "docbook-xml", + "docbook-xsl", + "perl-xml-sax", + "libxslt", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:db2x_manxml", + "cmd:db2x_texixml", + "cmd:db2x_xsltproc", + "cmd:docbook2x-man", + "cmd:docbook2x-texi", + "cmd:sgml2xml-isoent", + "cmd:utf8trans" + ] + }, + "libproxy": { + "versions": { + "0.4.15-r0": 1509468258 + }, + "origin": "libproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libproxy.so.1=1.0.0" + ] + }, + "xfce4-appfinder": { + "versions": { + "4.12.0-r0": 1510074536 + }, + "origin": "xfce4-appfinder", + "dependencies": [ + "hicolor-icon-theme", + "so:libc.musl-x86_64.so.1", + "so:libgarcon-1.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:xfce4-appfinder", + "cmd:xfrun4" + ] + }, + "orbit2-dev": { + "versions": { + "2.14.19-r3": 1510928299 + }, + "origin": "orbit2", + "dependencies": [ + "glib-dev", + "libidl-dev", + "orbit2=2.14.19-r3", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pc:libIDL-2.0", + "pkgconfig" + ], + "provides": [ + "pc:ORBit-2.0=2.14.19", + "pc:ORBit-CosNaming-2.0=2.14.19", + "pc:ORBit-idl-2.0=2.14.19", + "pc:ORBit-imodule-2.0=2.14.19", + "cmd:orbit2-config" + ] + }, + "perl-carp-clan-doc": { + "versions": { + "6.06-r1": 1509473079 + }, + "origin": "perl-carp-clan" + }, + "mii-tool": { + "versions": { + "1.60_git20140218-r1": 1509492734 + }, + "origin": "net-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mii-tool" + ] + }, + "iaxmodem-doc": { + "versions": { + "1.3.0-r0": 1509494109 + }, + "origin": "iaxmodem" + }, + "openresolv": { + "versions": { + "3.9.0-r0": 1509476551 + }, + "origin": "openresolv", + "provides": [ + "cmd:resolvconf" + ] + }, + "kamailio-jsdt": { + "versions": { + "5.0.7-r0": 1532960877 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1" + ] + }, + "b43-fwcutter": { + "versions": { + "019-r0": 1509495309 + }, + "origin": "b43-fwcutter", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:b43-fwcutter" + ] + }, + "perl-http-date-doc": { + "versions": { + "6.02-r1": 1509464355 + }, + "origin": "perl-http-date" + }, + "avahi-tools": { + "versions": { + "0.6.32-r4": 1509465447, + "0.6.32-r5": 1563345619 + }, + "origin": "avahi", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libintl.so.8" + ], + "provides": [ + "cmd:avahi-browse", + "cmd:avahi-browse-domains", + "cmd:avahi-publish", + "cmd:avahi-publish-address", + "cmd:avahi-publish-service", + "cmd:avahi-resolve", + "cmd:avahi-resolve-address", + "cmd:avahi-resolve-host-name", + "cmd:avahi-set-host-name" + ] + }, + "bridge-utils": { + "versions": { + "1.6-r0": 1509491171 + }, + "origin": "bridge-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:brctl" + ] + }, + "thunar-vcs-plugin-svn": { + "versions": { + "0.1.4-r6": 1510314591 + }, + "origin": "thunar-vcs-plugin", + "dependencies": [ + "so:libapr-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libsvn_client-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_subr-1.so.0", + "so:libsvn_wc-1.so.0", + "so:libxfce4util.so.7" + ] + }, + "perl-sub-install-doc": { + "versions": { + "0.928-r0": 1509473962 + }, + "origin": "perl-sub-install" + }, + "db-doc": { + "versions": { + "5.3.28-r0": 1509469314 + }, + "origin": "db" + }, + "perl-html-mason-psgihandler": { + "versions": { + "0.53-r0": 1510588962 + }, + "origin": "perl-html-mason-psgihandler", + "dependencies": [ + "perl-cgi-psgi", + "perl-html-mason" + ] + }, + "librrd-th": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpng16.so.16", + "so:libxml2.so.2" + ], + "provides": [ + "so:librrd_th.so.4=4.3.5" + ] + }, + "gdl-doc": { + "versions": { + "3.22.0-r0": 1510074987 + }, + "origin": "gdl" + }, + "py2-paramiko": { + "versions": { + "2.4.2-r0": 1551364326 + }, + "origin": "py-paramiko", + "dependencies": [ + "py2-asn1", + "py2-cryptography", + "py2-bcrypt", + "py2-pynacl", + "python2" + ] + }, + "xf86-video-fbdev-doc": { + "versions": { + "0.4.4-r5": 1510069870 + }, + "origin": "xf86-video-fbdev" + }, + "libxv": { + "versions": { + "1.0.11-r1": 1509467171 + }, + "origin": "libxv", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXv.so.1=1.0.0" + ] + }, + "bdftopcf": { + "versions": { + "1.0.5-r1": 1509470412 + }, + "origin": "bdftopcf", + "dependencies": [ + "so:libXfont.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bdftopcf" + ] + }, + "libxshmfence": { + "versions": { + "1.2-r2": 1509466255 + }, + "origin": "libxshmfence", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxshmfence.so.1=1.0.0" + ] + }, + "yasm-dev": { + "versions": { + "1.3.0-r1": 1509473588 + }, + "origin": "yasm" + }, + "perl-capture-tiny": { + "versions": { + "0.46-r0": 1509485667 + }, + "origin": "perl-capture-tiny" + }, + "xfdesktop": { + "versions": { + "4.12.4-r0": 1510074490 + }, + "origin": "xfdesktop", + "dependencies": [ + "hicolor-icon-theme", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libexo-1.so.0", + "so:libgarcon-1.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libnotify.so.4", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libthunarx-2.so.0", + "so:libwnck-1.so.22", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:xfdesktop", + "cmd:xfdesktop-settings" + ] + }, + "imap-dev": { + "versions": { + "2007f-r7": 1510258915 + }, + "origin": "imap", + "dependencies": [ + "c-client=2007f-r7" + ] + }, + "xf86-video-chips": { + "versions": { + "1.2.7-r0": 1510074064 + }, + "origin": "xf86-video-chips", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libpcap-dev": { + "versions": { + "1.8.1-r1": 1509475964 + }, + "origin": "libpcap", + "dependencies": [ + "libpcap=1.8.1-r1" + ], + "provides": [ + "cmd:pcap-config" + ] + }, + "perl-file-rsync-doc": { + "versions": { + "0.74-r1": 1509473504 + }, + "origin": "perl-file-rsync" + }, + "opus-doc": { + "versions": { + "1.2.1-r1": 1509468917 + }, + "origin": "opus" + }, + "gvfs-mtp": { + "versions": { + "1.34.1-r0": 1511430258, + "1.34.1-r1": 1563787224 + }, + "origin": "gvfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libgvfscommon.so", + "so:libgvfsdaemon.so", + "so:libintl.so.8", + "so:libmtp.so.9", + "so:libusb-1.0.so.0" + ] + }, + "unbound-libs": { + "versions": { + "1.6.7-r1": 1510588258 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libevent-2.1.so.6", + "so:libssl.so.44" + ], + "provides": [ + "so:libunbound.so.2=2.5.6" + ] + }, + "libqrencode-dev": { + "versions": { + "4.0.0-r0": 1509482399 + }, + "origin": "libqrencode", + "dependencies": [ + "libqrencode=4.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libqrencode=4.0.0" + ] + }, + "cyrus-sasl-gs2": { + "versions": { + "2.1.26-r11": 1510258044 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgssapi.so.3" + ] + }, + "alsa-lib-dbg": { + "versions": { + "1.1.4.1-r2": 1509468675 + }, + "origin": "alsa-lib" + }, + "lua5.1-gversion.lua": { + "versions": { + "0.2.0-r1": 1509475305 + }, + "origin": "lua-gversion", + "dependencies": [ + "lua5.1" + ] + }, + "lua-imlib2": { + "versions": { + "0.1-r1": 1509495907 + }, + "origin": "lua-imlib2", + "dependencies": [ + "so:libImlib2.so.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-exception-class-doc": { + "versions": { + "1.43-r0": 1509470597 + }, + "origin": "perl-exception-class" + }, + "libnfs": { + "versions": { + "2.0.0-r0": 1509493089 + }, + "origin": "libnfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnfs.so.11=11.0.0", + "cmd:nfs-cat", + "cmd:nfs-cp", + "cmd:nfs-ls" + ] + }, + "acf-db": { + "versions": { + "0.2.1-r2": 1510068617 + }, + "origin": "acf-db", + "dependencies": [ + "acf-core", + "acf-db-lib", + "acf-db-lib=0.2.1-r2" + ] + }, + "abiword-plugin-openxml": { + "versions": { + "3.0.2-r1": 1510073365 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "openvpn-doc": { + "versions": { + "2.4.4-r1": 1510259338 + }, + "origin": "openvpn" + }, + "device-mapper-libs": { + "versions": { + "2.02.175-r0": 1509462402 + }, + "origin": "lvm2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdevmapper.so.1.02=1.02" + ] + }, + "perl-data-page-pageset": { + "versions": { + "1.02-r1": 1509493853 + }, + "origin": "perl-data-page-pageset", + "dependencies": [ + "perl-data-page", + "perl-test-exception", + "perl-class-accessor" + ] + }, + "spice-glib": { + "versions": { + "0.34-r1": 1510260978 + }, + "origin": "spice-gtk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcelt051.so.0", + "so:libcrypto.so.42", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstapp-1.0.so.0", + "so:libgstaudio-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgstvideo-1.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:liblz4.so.1", + "so:libphodav-2.0.so.0", + "so:libpixman-1.so.0", + "so:libsasl2.so.3", + "so:libsoup-2.4.so.1", + "so:libssl.so.44", + "so:libusb-1.0.so.0", + "so:libusbredirhost.so.1", + "so:libusbredirparser.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libspice-client-glib-2.0.so.8=8.6.0", + "so:libspice-controller.so.0=0.0.0" + ] + }, + "npth": { + "versions": { + "1.5-r1": 1509472897 + }, + "origin": "npth", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnpth.so.0=0.1.1" + ] + }, + "kamailio-redis": { + "versions": { + "5.0.7-r0": 1532960877 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.13" + ] + }, + "attr-dev": { + "versions": { + "2.4.47-r6": 1509457000 + }, + "origin": "attr", + "dependencies": [ + "libattr=2.4.47-r6" + ] + }, + "freeradius-client": { + "versions": { + "1.1.7-r1": 1509488300 + }, + "origin": "freeradius-client", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfreeradius-client.so.2=2.0.0", + "cmd:login.radius", + "cmd:radacct", + "cmd:radembedded", + "cmd:radexample", + "cmd:radiusclient", + "cmd:radlogin", + "cmd:radstatus" + ] + }, + "uwsgi-graylog2": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ] + }, + "perl-net-smtp-tls-butmaintained": { + "versions": { + "0.24-r0": 1509494940 + }, + "origin": "perl-net-smtp-tls-butmaintained", + "dependencies": [ + "perl-net-ssleay", + "perl-io-socket-ssl", + "perl-digest-hmac" + ] + }, + "perl-gdtextutil-doc": { + "versions": { + "0.86-r0": 1509495188 + }, + "origin": "perl-gdtextutil" + }, + "smem-doc": { + "versions": { + "1.4-r1": 1509492706 + }, + "origin": "smem" + }, + "libiptcdata-doc": { + "versions": { + "1.0.4-r2": 1509496042 + }, + "origin": "libiptcdata" + }, + "thin-provisioning-tools-doc": { + "versions": { + "0.7.1-r0": 1509491569 + }, + "origin": "thin-provisioning-tools" + }, + "boost": { + "versions": { + "1.62.0-r5": 1509465883 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_filesystem-mt.so.1.62.0", + "so:libboost_filesystem.so.1.62.0", + "so:libboost_regex-mt.so.1.62.0", + "so:libboost_regex.so.1.62.0", + "so:libboost_system-mt.so.1.62.0", + "so:libboost_system.so.1.62.0", + "so:libboost_thread-mt.so.1.62.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libboost_atomic-mt.so.1.62.0=1.62.0", + "so:libboost_chrono-mt.so.1.62.0=1.62.0", + "so:libboost_chrono.so.1.62.0=1.62.0", + "so:libboost_container-mt.so.1.62.0=1.62.0", + "so:libboost_container.so.1.62.0=1.62.0", + "so:libboost_context-mt.so.1.62.0=1.62.0", + "so:libboost_coroutine-mt.so.1.62.0=1.62.0", + "so:libboost_coroutine.so.1.62.0=1.62.0", + "so:libboost_locale-mt.so.1.62.0=1.62.0", + "so:libboost_log-mt.so.1.62.0=1.62.0", + "so:libboost_log.so.1.62.0=1.62.0", + "so:libboost_log_setup-mt.so.1.62.0=1.62.0", + "so:libboost_log_setup.so.1.62.0=1.62.0", + "so:libboost_timer-mt.so.1.62.0=1.62.0", + "so:libboost_timer.so.1.62.0=1.62.0", + "so:libboost_type_erasure-mt.so.1.62.0=1.62.0", + "so:libboost_type_erasure.so.1.62.0=1.62.0", + "cmd:bcp", + "cmd:bjam", + "cmd:pyste.py" + ] + }, + "xe-guest-utilities": { + "versions": { + "6.1.0-r2": 1509492111 + }, + "origin": "xe-guest-utilities", + "provides": [ + "cmd:xe-daemon", + "cmd:xe-linux-distribution", + "cmd:xe-update-guest-attrs", + "cmd:xenstore", + "cmd:xenstore-chmod", + "cmd:xenstore-exists", + "cmd:xenstore-list", + "cmd:xenstore-ls", + "cmd:xenstore-read", + "cmd:xenstore-rm", + "cmd:xenstore-write" + ] + }, + "qt-assistant": { + "versions": { + "4.8.7-r8": 1510287208 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtGui.so.4", + "so:libQtHelp.so.4", + "so:libQtNetwork.so.4", + "so:libQtSql.so.4", + "so:libQtWebKit.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:assistant" + ] + }, + "py2-asn1-modules": { + "versions": { + "0.2.1-r0": 1511889299 + }, + "origin": "py-asn1-modules", + "dependencies": [ + "py2-openssl", + "py2-asn1" + ] + }, + "lua5.1-sqlite": { + "versions": { + "0.9.4-r2": 1509482115 + }, + "origin": "lua-sqlite", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "audacious-doc": { + "versions": { + "3.9-r0": 1510072524 + }, + "origin": "audacious" + }, + "apache2-ssl": { + "versions": { + "2.4.39-r0": 1554306882, + "2.4.41-r0": 1566292328 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "libressl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "jemalloc": { + "versions": { + "5.0.1-r0": 1509482444 + }, + "origin": "jemalloc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libjemalloc.so.2=2" + ] + }, + "qt-odbc": { + "versions": { + "4.8.7-r8": 1510287208 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libodbc.so.2", + "so:libstdc++.so.6" + ] + }, + "swig-doc": { + "versions": { + "3.0.12-r1": 1509468194 + }, + "origin": "swig" + }, + "py-flask-script": { + "versions": { + "2.0.5-r1": 1509552748 + }, + "origin": "py-flask-script", + "dependencies": [ + "python2", + "py-flask" + ] + }, + "apg": { + "versions": { + "2.2.3-r3": 1509489481 + }, + "origin": "apg", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:apg", + "cmd:apgbfm" + ] + }, + "openssh": { + "versions": { + "7.5_p1-r10": 1551712288 + }, + "origin": "openssh", + "dependencies": [ + "openssh-client", + "openssh-sftp-server", + "openssh-server", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "tdb-libs": { + "versions": { + "1.3.15-r0": 1509486504 + }, + "origin": "tdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtdb.so.1=1.3.15" + ] + }, + "perl-locale-maketext-lexicon-doc": { + "versions": { + "1.00-r0": 1509494303 + }, + "origin": "perl-locale-maketext-lexicon" + }, + "libxt-dev": { + "versions": { + "1.1.5-r1": 1509466276 + }, + "origin": "libxt", + "dependencies": [ + "libsm-dev", + "libxt=1.1.5-r1", + "pc:ice", + "pc:sm", + "pc:x11", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xt=1.1.5" + ] + }, + "perl-io-html": { + "versions": { + "1.001-r1": 1509464368 + }, + "origin": "perl-io-html" + }, + "perl-net-smtp-ssl-doc": { + "versions": { + "1.04-r0": 1509494478 + }, + "origin": "perl-net-smtp-ssl" + }, + "paxctl": { + "versions": { + "0.9-r0": 1509495226 + }, + "origin": "paxctl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:paxctl" + ] + }, + "xtrans-doc": { + "versions": { + "1.3.5-r1": 1509461953 + }, + "origin": "xtrans" + }, + "aspell-uk": { + "versions": { + "1.4.0-r1": 1509494404 + }, + "origin": "aspell-uk" + }, + "libbonobo": { + "versions": { + "2.32.1-r6": 1510931484 + }, + "origin": "libbonobo", + "dependencies": [ + "so:libORBit-2.so.0", + "so:libORBitCosNaming-2.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libintl.so.8", + "so:libxml2.so.2" + ], + "provides": [ + "so:libbonobo-2.so.0=0.0.0", + "so:libbonobo-activation.so.4=4.0.0", + "cmd:activation-client", + "cmd:bonobo-activation-sysconf", + "cmd:bonobo-slay" + ] + }, + "perl-mailtools-doc": { + "versions": { + "2.19-r0": 1511989744 + }, + "origin": "perl-mailtools" + }, + "py-mock": { + "versions": { + "2.0.0-r3": 1509493402 + }, + "origin": "py-mock", + "dependencies": [ + "py-pbr", + "py-six" + ] + }, + "glib-dbg": { + "versions": { + "2.54.2-r0": 1509911132, + "2.54.2-r1": 1560764732 + }, + "origin": "glib" + }, + "libgcrypt-doc": { + "versions": { + "1.8.3-r0": 1529408085, + "1.8.3-r1": 1564327775, + "1.8.3-r2": 1574265351 + }, + "origin": "libgcrypt" + }, + "libffi-doc": { + "versions": { + "3.2.1-r4": 1509458692 + }, + "origin": "libffi" + }, + "linux-firmware": { + "versions": { + "20171121-r0": 1511250555 + }, + "origin": "linux-firmware" + }, + "parole-dev": { + "versions": { + "0.9.2-r1": 1510075084 + }, + "origin": "parole", + "dependencies": [ + "libxfce4ui-dev", + "libxfce4util-dev", + "intltool", + "gstreamer-dev", + "bash", + "gst-plugins-base-dev", + "libsm-dev", + "taglib-dev", + "libnotify-dev" + ] + }, + "elinks": { + "versions": { + "0.13-r4": 1510261265 + }, + "origin": "elinks", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libexpat.so.1", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:elinks" + ] + }, + "mariadb-doc": { + "versions": { + "10.1.38-r1": 1551187988, + "10.1.40-r0": 1560354885, + "10.1.41-r0": 1565163109 + }, + "origin": "mariadb" + }, + "ttf-dejavu": { + "versions": { + "2.37-r0": 1509482655 + }, + "origin": "ttf-dejavu", + "dependencies": [ + "fontconfig", + "encodings", + "mkfontdir", + "mkfontscale" + ] + }, + "perl-test-warn": { + "versions": { + "0.32-r0": 1509470538 + }, + "origin": "perl-test-warn", + "dependencies": [ + "perl-sub-uplevel" + ] + }, + "mpt-status-doc": { + "versions": { + "1.2.0-r0": 1509488649 + }, + "origin": "mpt-status" + }, + "gnutls-doc": { + "versions": { + "3.6.1-r0": 1509465348 + }, + "origin": "gnutls" + }, + "ttf-freefont": { + "versions": { + "20120503-r0": 1509491022 + }, + "origin": "ttf-freefont", + "dependencies": [ + "fontconfig", + "encodings", + "mkfontdir", + "mkfontscale" + ] + }, + "perl-data-optlist": { + "versions": { + "0.110-r0": 1509473976 + }, + "origin": "perl-data-optlist", + "dependencies": [ + "perl", + "perl-params-util", + "perl-sub-install" + ] + }, + "elfutils-libelf": { + "versions": { + "0.168-r1": 1509477711 + }, + "origin": "elfutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libelf.so.1=0" + ] + }, + "screen": { + "versions": { + "4.6.2-r0": 1509492558 + }, + "origin": "screen", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:screen", + "cmd:screen-4.6.2" + ] + }, + "libiec61883": { + "versions": { + "1.2.0-r1": 1509470083 + }, + "origin": "libiec61883", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libraw1394.so.11" + ], + "provides": [ + "so:libiec61883.so.0=0.1.1" + ] + }, + "ruby-dev": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "gmp-dev", + "pkgconfig", + "ruby-libs=2.4.6-r0" + ], + "provides": [ + "pc:ruby-2.4=2.4.0" + ] + }, + "bacula": { + "versions": { + "9.0.5-r3": 1512992127 + }, + "origin": "bacula", + "dependencies": [ + "/bin/sh", + "so:libbac-9.0.5.so", + "so:libbaccfg-9.0.5.so", + "so:libbacfind-9.0.5.so", + "so:libbacsd-9.0.5.so", + "so:libbacsql-9.0.5.so", + "so:libc.musl-x86_64.so.1", + "so:liblzo2.so.2", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:bacula-dir", + "cmd:bacula-sd", + "cmd:bbconsjson", + "cmd:bcopy", + "cmd:bdirjson", + "cmd:bextract", + "cmd:bfdjson", + "cmd:bls", + "cmd:bregex", + "cmd:bscan", + "cmd:bsdjson", + "cmd:bsmtp", + "cmd:btape", + "cmd:bwild", + "cmd:dbcheck" + ] + }, + "imagemagick-c++": { + "versions": { + "7.0.7.11-r1": 1510748307 + }, + "origin": "imagemagick", + "dependencies": [ + "so:libMagickCore-7.Q16HDRI.so.4", + "so:libMagickWand-7.Q16HDRI.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libMagick++-7.Q16HDRI.so.3=3.0.0" + ] + }, + "pjsua": { + "versions": { + "2.5.5-r3": 1510258881 + }, + "origin": "pjproject", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpj.so.2", + "so:libpjlib-util.so.2", + "so:libpjmedia.so.2", + "so:libpjsip-simple.so.2", + "so:libpjsip.so.2", + "so:libpjsua.so.2" + ], + "provides": [ + "cmd:pjsua" + ] + }, + "libraw": { + "versions": { + "0.18.6-r0": 1514450639 + }, + "origin": "libraw", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgomp.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libraw.so.16=16.0.0", + "so:libraw_r.so.16=16.0.0", + "cmd:4channels", + "cmd:dcraw_emu", + "cmd:dcraw_half", + "cmd:half_mt", + "cmd:mem_image", + "cmd:multirender_test", + "cmd:postprocessing_benchmark", + "cmd:raw-identify", + "cmd:simple_dcraw", + "cmd:unprocessed_raw" + ] + }, + "xgamma": { + "versions": { + "1.0.6-r0": 1509481750 + }, + "origin": "xgamma", + "dependencies": [ + "so:libX11.so.6", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xgamma" + ] + }, + "py3-alabaster": { + "versions": { + "0.7.10-r0": 1509476479 + }, + "origin": "py-alabaster", + "dependencies": [ + "python3" + ] + }, + "lua-lustache": { + "versions": { + "1.3.1-r1": 1509472928 + }, + "origin": "lua-lustache" + }, + "iperf-doc": { + "versions": { + "2.0.9-r1": 1509482124 + }, + "origin": "iperf" + }, + "perl-gdgraph": { + "versions": { + "1.54-r0": 1509496365 + }, + "origin": "perl-gdgraph", + "dependencies": [ + "perl-gdtextutil", + "perl-gd" + ] + }, + "libpq": { + "versions": { + "10.7-r0": 1554274198, + "10.8-r0": 1557668015, + "10.9-r0": 1562225614, + "10.10-r0": 1565683438 + }, + "origin": "postgresql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libldap_r-2.4.so.2", + "so:libssl.so.44" + ], + "provides": [ + "so:libpq.so.5=5.10" + ] + }, + "py-paramiko-demos": { + "versions": { + "2.4.2-r0": 1551364324 + }, + "origin": "py-paramiko", + "dependencies": [ + "py-asn1", + "py-cryptography", + "py-bcrypt", + "py-pynacl" + ] + }, + "py-tornado": { + "versions": { + "4.5.2-r1": 1509551890 + }, + "origin": "py-tornado" + }, + "xinit-doc": { + "versions": { + "1.3.4-r1": 1509473740 + }, + "origin": "xinit" + }, + "gc-doc": { + "versions": { + "7.6.0-r1": 1509467220 + }, + "origin": "gc" + }, + "libpng": { + "versions": { + "1.6.37-r0": 1557132258 + }, + "origin": "libpng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libpng16.so.16=16.37.0" + ] + }, + "curl-dbg": { + "versions": { + "7.61.1-r2": 1551780208, + "7.61.1-r3": 1568722562 + }, + "origin": "curl", + "dependencies": [ + "ca-certificates" + ] + }, + "sipsak-dbg": { + "versions": { + "0.9.6-r9": 1510260059 + }, + "origin": "sipsak" + }, + "kbd-misc": { + "versions": { + "2.0.4-r2": 1510922530 + }, + "origin": "kbd" + }, + "dnssec-root": { + "versions": { + "20100715-r3": 1510588189 + }, + "origin": "dnssec-root" + }, + "shorewall": { + "versions": { + "5.1.8-r0": 1509481282 + }, + "origin": "shorewall", + "dependencies": [ + "shorewall-core", + "perl", + "iptables", + "iproute2" + ] + }, + "perl-class-accessor": { + "versions": { + "0.34-r0": 1509474098 + }, + "origin": "perl-class-accessor", + "dependencies": [ + "perl" + ] + }, + "herbstluftwm-bash-completion": { + "versions": { + "0.7.0-r1": 1509551853 + }, + "origin": "herbstluftwm" + }, + "xfce4-session-doc": { + "versions": { + "4.12.1-r2": 1510074574 + }, + "origin": "xfce4-session" + }, + "pngcrush": { + "versions": { + "1.8.13-r0": 1509482684 + }, + "origin": "pngcrush", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpng16.so.16", + "so:libz.so.1" + ], + "provides": [ + "cmd:pngcrush" + ] + }, + "lua5.1-ldap": { + "versions": { + "1.2.3-r5": 1509485733 + }, + "origin": "lua-ldap", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "libarchive-doc": { + "versions": { + "3.3.2-r2": 1510258000, + "3.3.3-r0": 1566312165, + "3.3.3-r1": 1572677952 + }, + "origin": "libarchive" + }, + "libburn": { + "versions": { + "1.4.8-r0": 1509462200 + }, + "origin": "libburn", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libburn.so.4=4.101.0", + "cmd:cdrskin" + ] + }, + "clamav-lib": { + "versions": { + "0.100.3-r0": 1555508237 + }, + "origin": "clamav", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libclammspack.so.0", + "so:libcrypto.so.42", + "so:libpcre.so.1", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:libclamav.so.7=7.1.2" + ] + }, + "lua-xctrl": { + "versions": { + "2015.04.10-r2": 1509475304 + }, + "origin": "lua-xctrl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "apr-util-dbd_pgsql": { + "versions": { + "1.6.1-r1": 1510285059 + }, + "origin": "apr-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "xen-doc": { + "versions": { + "4.9.4-r0": 1551280495, + "4.9.4-r1": 1558103152 + }, + "origin": "xen" + }, + "libglade-doc": { + "versions": { + "2.6.4-r14": 1510071791 + }, + "origin": "libglade" + }, + "speexdsp-doc": { + "versions": { + "1.2_rc3-r4": 1509473232 + }, + "origin": "speexdsp" + }, + "lua-feedparser-common": { + "versions": { + "0.71-r0": 1509485573 + }, + "origin": "lua-feedparser" + }, + "libnftnl": { + "versions": { + "1.0.8-r1": 1509909171 + }, + "origin": "libnftnl" + }, + "cogl-dev": { + "versions": { + "1.22.2-r0": 1510072960 + }, + "origin": "cogl", + "dependencies": [ + "glib-dev", + "mesa-dev", + "libdrm-dev", + "libxdamage-dev", + "libxcomposite-dev", + "libxrandr-dev", + "gdk-pixbuf-dev", + "pango-dev", + "cairo-dev", + "gobject-introspection-dev", + "libxext-dev", + "eudev-dev", + "cogl=1.22.2-r0", + "pc:cairo>=1.10", + "pc:egl", + "pc:gbm", + "pc:gdk-pixbuf-2.0>=2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:libdrm", + "pc:x11", + "pc:xcomposite>=0.4", + "pc:xdamage", + "pc:xext", + "pc:xfixes>=3", + "pc:xrandr>=1.2", + "pkgconfig" + ], + "provides": [ + "pc:cogl-1.0=1.22.2", + "pc:cogl-2.0-experimental=2.0.0", + "pc:cogl-gl-1.0=1.22.2", + "pc:cogl-gles2-1.0=1.22.2", + "pc:cogl-gles2-2.0-experimental=2.0.0", + "pc:cogl-pango-1.0=1.22.2", + "pc:cogl-pango-2.0-experimental=1.22.2", + "pc:cogl-path-1.0=1.22.2", + "pc:cogl-path-2.0-experimental=1.22.2" + ] + }, + "freeswitch-sounds-fr-fr-sibylle-8000": { + "versions": { + "0.1.3-r0": 1509482708 + }, + "origin": "freeswitch-sounds-fr-fr-sibylle-8000" + }, + "freeswitch-dbg": { + "versions": { + "1.6.19-r0": 1510585356 + }, + "origin": "freeswitch" + }, + "openvswitch-doc": { + "versions": { + "2.8.1-r2": 1512030502 + }, + "origin": "openvswitch" + }, + "py2-pbr": { + "versions": { + "3.1.1-r0": 1509480855 + }, + "origin": "py-pbr", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:pbr" + ] + }, + "protobuf-c-dev": { + "versions": { + "1.3.0-r3": 1510846239 + }, + "origin": "protobuf-c", + "dependencies": [ + "pkgconfig", + "protobuf-c=1.3.0-r3" + ], + "provides": [ + "pc:libprotobuf-c=1.3.0" + ] + }, + "libxml2-dbg": { + "versions": { + "2.9.8-r1": 1540398579 + }, + "origin": "libxml2" + }, + "goaccess": { + "versions": { + "1.2-r1": 1509480167 + }, + "origin": "goaccess", + "dependencies": [ + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:goaccess" + ] + }, + "dosfstools": { + "versions": { + "4.1-r1": 1509495036 + }, + "origin": "dosfstools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dosfsck", + "cmd:dosfslabel", + "cmd:fatlabel", + "cmd:fsck.fat", + "cmd:fsck.msdos", + "cmd:fsck.vfat", + "cmd:mkdosfs", + "cmd:mkfs.fat", + "cmd:mkfs.msdos", + "cmd:mkfs.vfat" + ] + }, + "py3-imagesize": { + "versions": { + "0.7.1-r2": 1509474092 + }, + "origin": "py-imagesize", + "dependencies": [ + "python3" + ] + }, + "lua5.3-sql-odbc": { + "versions": { + "2.3.5-r1": 1509488833 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "printproto-doc": { + "versions": { + "1.0.5-r2": 1509494090 + }, + "origin": "printproto" + }, + "libsrtp-dev": { + "versions": { + "1.5.4-r0": 1509473355 + }, + "origin": "libsrtp", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:libsrtp=1.5.4" + ] + }, + "nikto": { + "versions": { + "2.1.5-r2": 1509489224 + }, + "origin": "nikto", + "dependencies": [ + "perl", + "nmap", + "libressl" + ], + "provides": [ + "cmd:nikto.pl" + ] + }, + "cciss_vol_status-doc": { + "versions": { + "1.12-r0": 1509493251 + }, + "origin": "cciss_vol_status" + }, + "gcr": { + "versions": { + "3.20.0-r1": 1510073693 + }, + "origin": "gcr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgcrypt.so.20", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libp11-kit.so.0", + "so:libpango-1.0.so.0" + ], + "provides": [ + "so:libgck-1.so.0=0.0.0", + "so:libgcr-base-3.so.1=1.0.0", + "so:libgcr-ui-3.so.1=1.0.0", + "cmd:gcr-viewer" + ] + }, + "lz4-libs": { + "versions": { + "1.8.0-r1": 1509461405 + }, + "origin": "lz4", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblz4.so.1=1.8.0" + ] + }, + "speex-dev": { + "versions": { + "1.2.0-r0": 1509473244 + }, + "origin": "speex", + "dependencies": [ + "pkgconfig", + "speex=1.2.0-r0" + ], + "provides": [ + "pc:speex=1.2.0" + ] + }, + "gradm-doc": { + "versions": { + "3.1.201607172312-r0": 1509494154 + }, + "origin": "gradm" + }, + "xsetroot-doc": { + "versions": { + "1.1.1-r1": 1509490722 + }, + "origin": "xsetroot" + }, + "acf-openldap": { + "versions": { + "1.0.1-r4": 1510072538 + }, + "origin": "acf-openldap", + "dependencies": [ + "acf-core", + "openldap", + "openldap-back-bdb" + ] + }, + "perl-www-robotrules": { + "versions": { + "6.02-r1": 1509464408 + }, + "origin": "perl-www-robotrules", + "dependencies": [ + "perl", + "perl-uri" + ] + }, + "ulogd-mysql": { + "versions": { + "2.0.5-r4": 1509480395 + }, + "origin": "ulogd", + "dependencies": [ + "ulogd", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "gst-plugins-base0.10-lang": { + "versions": { + "0.10.36-r3": 1510068368 + }, + "origin": "gst-plugins-base0.10" + }, + "tango-icon-theme": { + "versions": { + "0.8.90-r4": 1510365697 + }, + "origin": "tango-icon-theme" + }, + "qemu-x86_64": { + "versions": { + "2.10.1-r3": 1519746244 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-x86_64" + ] + }, + "clamav-libunrar": { + "versions": { + "0.100.3-r0": 1555508237 + }, + "origin": "clamav", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libclamunrar.so.7=7.1.2", + "so:libclamunrar_iface.so.7=7.1.2" + ] + }, + "perl-getopt-long-doc": { + "versions": { + "2.50-r0": 1509491104 + }, + "origin": "perl-getopt-long" + }, + "dhcpcd-doc": { + "versions": { + "6.11.5-r1": 1509494692 + }, + "origin": "dhcpcd" + }, + "py-backports_abc": { + "versions": { + "0.4-r3": 1509552782 + }, + "origin": "py-backports_abc" + }, + "dovecot-pigeonhole-plugin-ldap": { + "versions": { + "2.2.36.3-r0": 1554102387, + "2.2.36.4-r0": 1567075098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot-pigeonhole-plugin", + "dovecot-ldap", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "py3-flask-oauthlib": { + "versions": { + "0.9.3-r2": 1509552815 + }, + "origin": "py-flask-oauthlib", + "dependencies": [ + "py3-flask", + "py3-requests-oauthlib", + "python3" + ] + }, + "sems-early_announce": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "perl-test-without-module-doc": { + "versions": { + "0.20-r0": 1509481284 + }, + "origin": "perl-test-without-module" + }, + "libsndfile-dev": { + "versions": { + "1.0.28-r4": 1548491633, + "1.0.28-r5": 1563345857 + }, + "origin": "libsndfile", + "dependencies": [ + "flac-dev", + "libvorbis-dev", + "libogg-dev", + "libsndfile=1.0.28-r5", + "pkgconfig" + ], + "provides": [ + "pc:sndfile=1.0.28" + ] + }, + "xsetroot": { + "versions": { + "1.1.1-r1": 1509490722 + }, + "origin": "xsetroot", + "dependencies": [ + "so:libX11.so.6", + "so:libXcursor.so.1", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xsetroot" + ] + }, + "nagios-plugins-dhcp": { + "versions": { + "2.2.1-r3": 1510288494 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "yajl-tools": { + "versions": { + "2.1.0-r0": 1509475316 + }, + "origin": "yajl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:json_reformat", + "cmd:json_verify" + ] + }, + "libxvmc-doc": { + "versions": { + "1.0.10-r1": 1509467179 + }, + "origin": "libxvmc" + }, + "py-configshell": { + "versions": { + "1.1_p19-r2": 1509493963 + }, + "origin": "py-configshell", + "dependencies": [ + "python2", + "py-six", + "py-urwid", + "py-parsing" + ] + }, + "libgcrypt-dev": { + "versions": { + "1.8.3-r0": 1529408084, + "1.8.3-r1": 1564327775, + "1.8.3-r2": 1574265351 + }, + "origin": "libgcrypt", + "dependencies": [ + "libgpg-error-dev", + "libgcrypt=1.8.3-r2" + ], + "provides": [ + "cmd:libgcrypt-config" + ] + }, + "perl-convert-tnef-doc": { + "versions": { + "0.18-r0": 1509470831 + }, + "origin": "perl-convert-tnef" + }, + "perl-stream-buffered-doc": { + "versions": { + "0.02-r1": 1510564489 + }, + "origin": "perl-stream-buffered" + }, + "libgsasl-dev": { + "versions": { + "1.8.0-r2": 1509493437 + }, + "origin": "libgsasl", + "dependencies": [ + "pkgconfig", + "libgsasl=1.8.0-r2" + ], + "provides": [ + "pc:libgsasl=1.8.0" + ] + }, + "lcms2-doc": { + "versions": { + "2.8-r1": 1509464952 + }, + "origin": "lcms2" + }, + "distcc": { + "versions": { + "3.1-r12": 1509493045 + }, + "origin": "distcc", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:distcc", + "cmd:distccd", + "cmd:distccmon-text", + "cmd:lsdistcc" + ] + }, + "py-gobject-doc": { + "versions": { + "2.28.6-r6": 1509471051 + }, + "origin": "py-gobject" + }, + "squid-lang-et": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-importer": { + "versions": { + "0.024-r0": 1509481615 + }, + "origin": "perl-importer", + "dependencies": [ + "perl" + ] + }, + "libgss-doc": { + "versions": { + "0.1.5-r1": 1509493415 + }, + "origin": "libgss" + }, + "transmission-cli": { + "versions": { + "2.92-r8": 1510932986 + }, + "origin": "transmission", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libevent-2.1.so.6", + "so:libintl.so.8", + "so:libz.so.1" + ], + "provides": [ + "cmd:transmission-cli", + "cmd:transmission-create", + "cmd:transmission-edit", + "cmd:transmission-remote", + "cmd:transmission-show" + ] + }, + "gnupg": { + "versions": { + "2.2.3-r1": 1528897438 + }, + "origin": "gnupg", + "dependencies": [ + "pinentry", + "so:libassuan.so.0", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libgnutls.so.30", + "so:libgpg-error.so.0", + "so:libksba.so.8", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libnpth.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:addgnupghome", + "cmd:applygnupgdefaults", + "cmd:dirmngr", + "cmd:dirmngr-client", + "cmd:gpg", + "cmd:gpg-agent", + "cmd:gpg-connect-agent", + "cmd:gpg2", + "cmd:gpgconf", + "cmd:gpgparsemail", + "cmd:gpgscm", + "cmd:gpgsm", + "cmd:gpgtar", + "cmd:gpgv", + "cmd:gpgv2", + "cmd:kbxutil", + "cmd:watchgnupg" + ] + }, + "kamailio-ims": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libkamailio_ims.so.0", + "so:libsrdb1.so.1", + "so:libsrutils.so.1", + "so:libxml2.so.2" + ] + }, + "py2-gflags": { + "versions": { + "3.1.1-r1": 1509551806 + }, + "origin": "py-gflags", + "dependencies": [ + "python2" + ] + }, + "hunspell-pt": { + "versions": { + "20170814-r0": 1511168470 + }, + "origin": "hunspell-pt" + }, + "uwsgi-transformation_offload": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "tcl": { + "versions": { + "8.6.7-r0": 1509460298 + }, + "origin": "tcl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libtcl8.6.so=0", + "cmd:tclsh", + "cmd:tclsh8.6" + ] + }, + "perl-file-sharedir": { + "versions": { + "1.104-r0": 1510564486 + }, + "origin": "perl-file-sharedir", + "dependencies": [ + "perl", + "perl-class-inspector" + ] + }, + "perl-digest-md5": { + "versions": { + "2.55-r2": 1509482652 + }, + "origin": "perl-digest-md5", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-email-date-format": { + "versions": { + "1.005-r0": 1509489452 + }, + "origin": "perl-email-date-format" + }, + "mdadm-udev": { + "versions": { + "4.0-r0": 1509476488 + }, + "origin": "mdadm" + }, + "bluez-meshctl": { + "versions": { + "5.47-r3": 1510069788 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0", + "so:libjson-c.so.2", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:meshctl" + ] + }, + "orc-dev": { + "versions": { + "0.4.27-r0": 1509469955 + }, + "origin": "orc", + "dependencies": [ + "orc=0.4.27-r0", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:liborc-0.4.so.0", + "so:liborc-test-0.4.so.0" + ], + "provides": [ + "pc:orc-0.4=0.4.27", + "cmd:orc-bugreport" + ] + }, + "perl-class-returnvalue-doc": { + "versions": { + "0.55-r1": 1510564545 + }, + "origin": "perl-class-returnvalue" + }, + "lxc-dev": { + "versions": { + "2.1.1-r3": 1533578832 + }, + "origin": "lxc", + "dependencies": [ + "libcap-dev", + "lxc-libs=2.1.1-r3", + "pkgconfig" + ], + "provides": [ + "pc:lxc=2.1.1", + "cmd:lxc-update-config" + ] + }, + "libtxc_dxtn": { + "versions": { + "1.0.1-r5": 1510075567 + }, + "origin": "libtxc_dxtn", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtxc_dxtn.so=0" + ] + }, + "perl-xml-sax-base-doc": { + "versions": { + "1.09-r0": 1509475899 + }, + "origin": "perl-xml-sax-base" + }, + "py3-jwt": { + "versions": { + "1.5.0-r1": 1509552755 + }, + "origin": "py-jwt", + "dependencies": [ + "python3" + ] + }, + "perl-data-optlist-doc": { + "versions": { + "0.110-r0": 1509473973 + }, + "origin": "perl-data-optlist" + }, + "xcalib": { + "versions": { + "0.8-r0": 1509494572 + }, + "origin": "xcalib", + "dependencies": [ + "so:libX11.so.6", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xcalib" + ] + }, + "lcms2-dev": { + "versions": { + "2.8-r1": 1509464951 + }, + "origin": "lcms2", + "dependencies": [ + "libjpeg-turbo-dev", + "tiff-dev", + "zlib-dev", + "lcms2=2.8-r1", + "pkgconfig" + ], + "provides": [ + "pc:lcms2=2.8" + ] + }, + "binutils-libs": { + "versions": { + "2.30-r1": 1527754981, + "2.30-r2": 1565788943 + }, + "origin": "binutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libbfd-2.30.so=0", + "so:libopcodes-2.30.so=0" + ] + }, + "mesa-dri-swrast": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "mpg123-doc": { + "versions": { + "1.25.7-r0": 1509707456 + }, + "origin": "mpg123" + }, + "libshout": { + "versions": { + "2.4.1-r1": 1510068384 + }, + "origin": "libshout", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libspeex.so.1", + "so:libtheora.so.0", + "so:libvorbis.so.0" + ], + "provides": [ + "so:libshout.so.3=3.2.0" + ] + }, + "libstdc++": { + "versions": { + "6.4.0-r5": 1509458072 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libstdc++.so.6=6.0.22" + ] + }, + "perl-ipc-run3": { + "versions": { + "0.048-r0": 1509481629 + }, + "origin": "perl-ipc-run3", + "dependencies": [ + "perl" + ] + }, + "bluez-cups": { + "versions": { + "5.47-r3": 1510069787 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0" + ] + }, + "ebtables-doc": { + "versions": { + "2.0.10.4-r2": 1509491244 + }, + "origin": "ebtables" + }, + "lua5.2-ldap": { + "versions": { + "1.2.3-r5": 1509485733 + }, + "origin": "lua-ldap", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "py-asn1-modules": { + "versions": { + "0.2.1-r0": 1511889299 + }, + "origin": "py-asn1-modules" + }, + "lua-sql-mysql": { + "versions": { + "2.3.5-r1": 1509488833 + }, + "origin": "lua-sql" + }, + "py3-ecdsa": { + "versions": { + "0.13-r5": 1509494955 + }, + "origin": "py-ecdsa", + "dependencies": [ + "py3-crypto" + ] + }, + "libfetch": { + "versions": { + "2.33-r2": 1509490729 + }, + "origin": "libfetch", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfetch.so=0" + ] + }, + "perl-sub-uplevel": { + "versions": { + "0.2800-r0": 1509470532 + }, + "origin": "perl-sub-uplevel", + "dependencies": [ + "perl" + ] + }, + "perl-html-format": { + "versions": { + "2.11-r0": 1509481566 + }, + "origin": "perl-html-format", + "dependencies": [ + "perl-html-tree", + "perl-font-afm" + ] + }, + "freeswitch-sample-config": { + "versions": { + "1.6.19-r0": 1510585357 + }, + "origin": "freeswitch", + "dependencies": [ + "freeswitch-timezones" + ] + }, + "gobject-introspection-dev": { + "versions": { + "1.54.1-r0": 1509464659 + }, + "origin": "gobject-introspection", + "dependencies": [ + "python2", + "glib-dev", + "libffi-dev", + "cairo-dev", + "libtool", + "gobject-introspection=1.54.1-r0", + "pc:glib-2.0", + "pc:gmodule-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:libffi", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgio-2.0.so.0", + "so:libgirepository-1.0.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "pc:gobject-introspection-1.0=1.54.1", + "pc:gobject-introspection-no-export-1.0=1.54.1", + "cmd:g-ir-annotation-tool", + "cmd:g-ir-compiler", + "cmd:g-ir-generate", + "cmd:g-ir-inspect", + "cmd:g-ir-scanner" + ] + }, + "gdk-pixbuf-lang": { + "versions": { + "2.36.10-r0": 1509465514 + }, + "origin": "gdk-pixbuf", + "dependencies": [ + "shared-mime-info" + ] + }, + "py-rsa": { + "versions": { + "3.4.2-r2": 1509551794 + }, + "origin": "py-rsa", + "dependencies": [ + "py-asn1" + ] + }, + "unrar": { + "versions": { + "5.5.8-r0": 1509468421 + }, + "origin": "unrar", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:unrar" + ] + }, + "halberd": { + "versions": { + "0.2.4-r1": 1509483053 + }, + "origin": "halberd", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:halberd" + ] + }, + "squid-lang-sr": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "nagios-plugins-procs": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-xml-simple": { + "versions": { + "2.24-r0": 1509477314 + }, + "origin": "perl-xml-simple", + "dependencies": [ + "perl-xml-parser", + "perl" + ] + }, + "perl-html-formattext-withlinks": { + "versions": { + "0.15-r0": 1509481574 + }, + "origin": "perl-html-formattext-withlinks", + "dependencies": [ + "perl-html-format", + "perl-uri", + "perl-html-tree" + ] + }, + "gc-dev": { + "versions": { + "7.6.0-r1": 1509467220 + }, + "origin": "gc", + "dependencies": [ + "gc=7.6.0-r1", + "libgc++=7.6.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:bdw-gc=7.6.0" + ] + }, + "perl-xml-parser-doc": { + "versions": { + "2.44-r4": 1509464445 + }, + "origin": "perl-xml-parser" + }, + "parole": { + "versions": { + "0.9.2-r1": 1510075086 + }, + "origin": "parole", + "dependencies": [ + "gst-plugins-good", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-1.0.so.0", + "so:libgstpbutils-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgsttag-1.0.so.0", + "so:libgstvideo-1.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libnotify.so.4", + "so:libtag_c.so.0", + "so:libxfce4ui-2.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:parole" + ] + }, + "s6-doc": { + "versions": { + "2.6.1.1-r0": 1510364940 + }, + "origin": "s6" + }, + "pangox-compat-dev": { + "versions": { + "0.0.2-r0": 1509482459 + }, + "origin": "pangox-compat", + "dependencies": [ + "pango-dev", + "libice-dev", + "pangox-compat=0.0.2-r0", + "pc:pango", + "pkgconfig" + ], + "provides": [ + "pc:pangox=1.30" + ] + }, + "libressl2.6-libssl": { + "versions": { + "2.6.5-r0": 1529043883 + }, + "origin": "libressl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ], + "provides": [ + "so:libssl.so.44=44.0.1" + ] + }, + "mosquitto": { + "versions": { + "1.4.15-r0": 1520176488, + "1.4.15-r2": 1563346324 + }, + "origin": "mosquitto", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libuuid.so.1", + "so:libwebsockets.so.12" + ], + "provides": [ + "cmd:mosquitto", + "cmd:mosquitto_passwd" + ] + }, + "libfdisk": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libfdisk.so.1=1.1.0" + ] + }, + "py3-funcsigs": { + "versions": { + "1.0.2-r1": 1509485907 + }, + "origin": "py-funcsigs", + "dependencies": [ + "python3" + ] + }, + "perl-list-someutils-doc": { + "versions": { + "0.53-r0": 1509474586 + }, + "origin": "perl-list-someutils" + }, + "kamailio-dbg": { + "versions": { + "5.0.7-r0": 1532960874 + }, + "origin": "kamailio" + }, + "libusb-compat": { + "versions": { + "0.1.5-r3": 1509473938 + }, + "origin": "libusb-compat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libusb-0.1.so.4=4.4.4" + ] + }, + "lzip-doc": { + "versions": { + "1.19-r1": 1509459870 + }, + "origin": "lzip" + }, + "wavpack-doc": { + "versions": { + "5.1.0-r3": 1548940101, + "5.1.0-r4": 1566809779 + }, + "origin": "wavpack" + }, + "perl-try-tiny-doc": { + "versions": { + "0.22-r1": 1509464413 + }, + "origin": "perl-try-tiny" + }, + "perl-html-parser-doc": { + "versions": { + "3.72-r2": 1509464394 + }, + "origin": "perl-html-parser" + }, + "attr-doc": { + "versions": { + "2.4.47-r6": 1509457001 + }, + "origin": "attr" + }, + "cifs-utils-dev": { + "versions": { + "6.7-r1": 1509475350 + }, + "origin": "cifs-utils" + }, + "perl-module-versions-report-doc": { + "versions": { + "1.06-r0": 1509495045 + }, + "origin": "perl-module-versions-report" + }, + "libguess-dev": { + "versions": { + "1.2-r0": 1509494181 + }, + "origin": "libguess", + "dependencies": [ + "libguess=1.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libguess=1.2" + ] + }, + "lua5.3-stringy": { + "versions": { + "0.4.0-r5": 1509480103 + }, + "origin": "lua-stringy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "pgtcl": { + "versions": { + "2.1.0-r0": 1509489163 + }, + "origin": "pgtcl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "perl-xml-rss-doc": { + "versions": { + "1.59-r1": 1511389028 + }, + "origin": "perl-xml-rss" + }, + "asciidoc-doc": { + "versions": { + "8.6.10-r0": 1509461782 + }, + "origin": "asciidoc" + }, + "libvirt-common-drivers": { + "versions": { + "3.9.0-r1": 1510088352, + "5.5.0-r0": 1562165541 + }, + "origin": "libvirt", + "dependencies": [ + "lvm2", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libnetcf.so.1", + "so:libpcap.so.1", + "so:libpciaccess.so.0", + "so:libudev.so.1", + "so:libvirt.so.0", + "so:libxml2.so.2" + ] + }, + "libssh2": { + "versions": { + "1.8.2-r0": 1553682025, + "1.9.0-r0": 1571235284, + "1.9.0-r1": 1571996777 + }, + "origin": "libssh2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libz.so.1" + ], + "provides": [ + "so:libssh2.so.1=1.0.1" + ] + }, + "perl-role-basic": { + "versions": { + "0.13-r0": 1509468428 + }, + "origin": "perl-role-basic" + }, + "haveged": { + "versions": { + "1.9.2-r0": 1511893694 + }, + "origin": "haveged", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhavege.so.1=1.1.0", + "cmd:haveged" + ] + }, + "perl-test-deep-doc": { + "versions": { + "1.126-r0": 1509468479 + }, + "origin": "perl-test-deep" + }, + "perl-want-doc": { + "versions": { + "0.29-r1": 1509477485 + }, + "origin": "perl-want" + }, + "perl-text-template": { + "versions": { + "1.46-r0": 1509491534 + }, + "origin": "perl-text-template", + "dependencies": [ + "perl" + ] + }, + "perl-proc-wait3": { + "versions": { + "0.05-r1": 1509482407 + }, + "origin": "perl-proc-wait3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "keyutils-libs": { + "versions": { + "1.5.10-r0": 1509469695 + }, + "origin": "keyutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libkeyutils.so.1=1.6" + ] + }, + "samba-test": { + "versions": { + "4.7.6-r3": 1555491789 + }, + "origin": "samba", + "dependencies": [ + "so:libMESSAGING-SEND-samba4.so", + "so:libMESSAGING-samba4.so", + "so:libaddns-samba4.so", + "so:libasn1-samba4.so.8", + "so:libasn1util-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libcluster-samba4.so", + "so:libcmdline-credentials-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libdcerpc.so.0", + "so:libdnsserver-common-samba4.so", + "so:libdsdb-module-samba4.so", + "so:libevents-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgnutls.so.30", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:liblibsmb-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnetapi.so.0", + "so:libnetif-samba4.so", + "so:libpopt.so.0", + "so:libregistry-samba4.so", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-net-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libshares-samba4.so", + "so:libsmbclient-raw-samba4.so", + "so:libsmbclient.so.0", + "so:libsmbconf.so.0", + "so:libsmbpasswdparser-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtdb-wrap-samba4.so", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so", + "so:libutil-reg-samba4.so", + "so:libutil-tdb-samba4.so", + "so:libwbclient.so.0", + "so:libwinbind-client-samba4.so" + ], + "provides": [ + "so:libdlz-bind9-for-torture-samba4.so=0", + "so:libtorture-samba4.so=0", + "cmd:gentest", + "cmd:locktest", + "cmd:masktest", + "cmd:ndrdump", + "cmd:smbtorture" + ] + }, + "freeswitch-sangoma": { + "versions": { + "1.6.19-r0": 1510585357 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeswitch.so.1", + "so:libsngtc_node.so" + ] + }, + "open-iscsi-doc": { + "versions": { + "2.0.874-r0": 1509494138 + }, + "origin": "open-iscsi" + }, + "alsa-lib": { + "versions": { + "1.1.4.1-r2": 1509468675 + }, + "origin": "alsa-lib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libasound.so.2=2.0.0", + "cmd:aserver" + ] + }, + "gconf": { + "versions": { + "3.2.6-r1": 1510928343 + }, + "origin": "gconf", + "dependencies": [ + "so:libORBit-2.so.0", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libintl.so.8", + "so:libpolkit-gobject-1.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgconf-2.so.4=4.1.5", + "cmd:gconf-merge-tree", + "cmd:gconftool-2", + "cmd:gsettings-data-convert", + "cmd:gsettings-schema-convert" + ] + }, + "ircservices": { + "versions": { + "5.1.24-r4": 1509481224 + }, + "origin": "ircservices", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ircservices", + "cmd:ircservices-chk", + "cmd:ircservices-convert-db" + ] + }, + "faac-doc": { + "versions": { + "1.28-r11": 1509470017 + }, + "origin": "faac" + }, + "xrdb": { + "versions": { + "1.1.0-r2": 1509473736 + }, + "origin": "xrdb", + "dependencies": [ + "mcpp", + "so:libX11.so.6", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xrdb" + ] + }, + "mariadb-dev": { + "versions": { + "10.1.38-r1": 1551187990, + "10.1.40-r0": 1560354886, + "10.1.41-r0": 1565163110 + }, + "origin": "mariadb", + "dependencies": [ + "libressl-dev", + "zlib-dev", + "mariadb-client-libs=10.1.41-r0", + "mariadb-libs=10.1.41-r0" + ], + "provides": [ + "mysql-dev=10.1.41-r0", + "cmd:mysql_config" + ] + }, + "qemu-system-sh4eb": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-sh4eb" + ] + }, + "ircii-doc": { + "versions": { + "20111115-r4": 1509492740 + }, + "origin": "ircii" + }, + "perl-mime-tools": { + "versions": { + "5.509-r1": 1509469901 + }, + "origin": "perl-mime-tools", + "dependencies": [ + "perl", + "perl-io-stringy", + "perl-mailtools", + "perl-convert-binhex" + ] + }, + "tinyproxy-doc": { + "versions": { + "1.8.4-r3": 1509491455 + }, + "origin": "tinyproxy" + }, + "fish-tools": { + "versions": { + "2.6.0-r2": 1511486201 + }, + "origin": "fish", + "dependencies": [ + "fish", + "python" + ] + }, + "perl-convert-binhex": { + "versions": { + "1.125-r0": 1509469888 + }, + "origin": "perl-convert-binhex", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:binhex.pl", + "cmd:debinhex.pl" + ] + }, + "sudo": { + "versions": { + "1.8.21_p2-r1": 1509459675 + }, + "origin": "sudo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:group_file.so=0", + "so:libsudo_noexec.so=0", + "so:libsudo_util.so.0=0.0.0", + "so:sudoers.so=0", + "so:system_group.so=0", + "cmd:sudo", + "cmd:sudoedit", + "cmd:sudoreplay", + "cmd:visudo" + ] + }, + "xmlrpc-c-doc": { + "versions": { + "1.39.11-r0": 1509482031 + }, + "origin": "xmlrpc-c" + }, + "openvpn-auth-pam": { + "versions": { + "2.4.4-r1": 1510259339 + }, + "origin": "openvpn", + "dependencies": [ + "iproute2", + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0" + ] + }, + "aspell-dev": { + "versions": { + "0.60.6.1-r12": 1509472867, + "0.60.6.1-r13": 1572541141 + }, + "origin": "aspell", + "dependencies": [ + "aspell-utils", + "aspell-libs=0.60.6.1-r13" + ], + "provides": [ + "cmd:pspell-config" + ] + }, + "sems-conference": { + "versions": { + "1.6.0-r6": 1510260895 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "avahi-ui-gtk3": { + "versions": { + "0.6.31-r7": 1510076323 + }, + "origin": "avahi-ui", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-glib.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libgdk-3.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libavahi-ui-gtk3.so.0=0.1.4" + ] + }, + "xset": { + "versions": { + "1.2.3-r1": 1509481585 + }, + "origin": "xset", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xset" + ] + }, + "vanessa_logger": { + "versions": { + "0.0.10-r0": 1509481344 + }, + "origin": "vanessa_logger", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libvanessa_logger.so.0=0.0.5", + "cmd:vanessa_logger_sample" + ] + }, + "libmpeg2-doc": { + "versions": { + "0.5.1-r7": 1509473556 + }, + "origin": "libmpeg2" + }, + "mcookie": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux", + "dependencies": [ + "findmnt", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mcookie" + ] + }, + "antiword": { + "versions": { + "0.37-r3": 1509476493 + }, + "origin": "antiword", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:antiword" + ] + }, + "e2fsprogs": { + "versions": { + "1.43.7-r0": 1509469275, + "1.43.7-r1": 1571322245 + }, + "origin": "e2fsprogs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libe2p.so.2", + "so:libext2fs.so.2", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:e2fsck", + "cmd:fsck.ext2", + "cmd:fsck.ext3", + "cmd:fsck.ext4", + "cmd:mke2fs", + "cmd:mkfs.ext2", + "cmd:mkfs.ext3", + "cmd:mkfs.ext4" + ] + }, + "libwebsockets": { + "versions": { + "2.4.0-r1": 1510258055 + }, + "origin": "libwebsockets", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:libwebsockets.so.12=12" + ] + }, + "perl-mail-clamav": { + "versions": { + "0.29-r12": 1509489512 + }, + "origin": "perl-mail-clamav", + "dependencies": [ + "perl", + "clamav", + "so:libc.musl-x86_64.so.1", + "so:libclamav.so.7" + ] + }, + "iwlwifi-1000-ucode": { + "versions": { + "39.31.5.1-r0": 1509483630 + }, + "origin": "iwlwifi-1000-ucode" + }, + "libmemcached-libs": { + "versions": { + "1.0.18-r2": 1520366184 + }, + "origin": "libmemcached", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libsasl2.so.3", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libhashkit.so.2=2.0.0", + "so:libmemcached.so.11=11.0.0", + "so:libmemcachedprotocol.so.0=0.0.0", + "so:libmemcachedutil.so.2=2.0.0" + ] + }, + "perl-test-mocktime": { + "versions": { + "0.15-r0": 1509475511 + }, + "origin": "perl-test-mocktime" + }, + "libev-doc": { + "versions": { + "4.24-r0": 1509469569 + }, + "origin": "libev" + }, + "graphviz-doc": { + "versions": { + "2.40.1-r0": 1510066917 + }, + "origin": "graphviz" + }, + "bluez-firmware": { + "versions": { + "1.2-r0": 1509492592 + }, + "origin": "bluez-firmware" + }, + "spice-gtk-doc": { + "versions": { + "0.34-r1": 1510260978 + }, + "origin": "spice-gtk" + }, + "irqbalance-doc": { + "versions": { + "1.2.0-r1": 1509492255 + }, + "origin": "irqbalance" + }, + "minizip": { + "versions": { + "1.2.11-r0": 1509495062 + }, + "origin": "minizip", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libminizip.so.1=1.0.0" + ] + }, + "xfsprogs-doc": { + "versions": { + "4.14.0-r1": 1528279973 + }, + "origin": "xfsprogs" + }, + "iscsi-scst": { + "versions": { + "2.2.1-r2": 1509482428 + }, + "origin": "iscsi-scst", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:iscsi-scst-adm", + "cmd:iscsi-scstd" + ] + }, + "asterisk-pgsql": { + "versions": { + "15.6.1-r0": 1537795340, + "15.6.2-r0": 1568705002 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "ebtables": { + "versions": { + "2.0.10.4-r2": 1509491245 + }, + "origin": "ebtables", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libebt_802_3.so=0", + "so:libebt_among.so=0", + "so:libebt_arp.so=0", + "so:libebt_arpreply.so=0", + "so:libebt_ip.so=0", + "so:libebt_ip6.so=0", + "so:libebt_limit.so=0", + "so:libebt_log.so=0", + "so:libebt_mark.so=0", + "so:libebt_mark_m.so=0", + "so:libebt_nat.so=0", + "so:libebt_nflog.so=0", + "so:libebt_pkttype.so=0", + "so:libebt_redirect.so=0", + "so:libebt_standard.so=0", + "so:libebt_stp.so=0", + "so:libebt_ulog.so=0", + "so:libebt_vlan.so=0", + "so:libebtable_broute.so=0", + "so:libebtable_filter.so=0", + "so:libebtable_nat.so=0", + "so:libebtc.so=0", + "cmd:ebtables", + "cmd:ebtables-restore", + "cmd:ebtables-save" + ] + }, + "apr-dev": { + "versions": { + "1.6.3-r0": 1509476182 + }, + "origin": "apr", + "dependencies": [ + "apr", + "util-linux-dev", + "bash", + "pkgconfig" + ], + "provides": [ + "pc:apr-1=1.6.3", + "cmd:apr-1-config" + ] + }, + "py-django-haystack": { + "versions": { + "2.5.0-r0": 1509477805 + }, + "origin": "py-django-haystack", + "dependencies": [ + "py-django" + ] + }, + "netcf-dev": { + "versions": { + "0.2.8-r4": 1509491200 + }, + "origin": "netcf", + "dependencies": [ + "netcf-libs=0.2.8-r4", + "pkgconfig" + ], + "provides": [ + "pc:netcf=0.2.8" + ] + }, + "dahdi-linux-dev": { + "versions": { + "2.11.1-r0": 1509476067 + }, + "origin": "dahdi-linux" + }, + "cpufreqd-doc": { + "versions": { + "2.4.2-r4": 1509492586 + }, + "origin": "cpufreqd" + }, + "ldb-tools": { + "versions": { + "1.3.0-r1": 1534939012 + }, + "origin": "ldb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldb.so.1", + "so:libpopt.so.0", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libldb-cmdline.so=0", + "cmd:ldbadd", + "cmd:ldbdel", + "cmd:ldbedit", + "cmd:ldbmodify", + "cmd:ldbrename", + "cmd:ldbsearch" + ] + }, + "jfsutils-doc": { + "versions": { + "1.1.15-r1": 1509482133 + }, + "origin": "jfsutils" + }, + "dbus-dev": { + "versions": { + "1.10.24-r0": 1509465101, + "1.10.28-r0": 1560765707 + }, + "origin": "dbus", + "dependencies": [ + "util-linux-dev", + "dbus-libs=1.10.28-r0", + "pkgconfig" + ], + "provides": [ + "pc:dbus-1=1.10.28" + ] + }, + "libcap": { + "versions": { + "2.25-r1": 1509459572 + }, + "origin": "libcap", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcap.so.2=2.25", + "cmd:capsh", + "cmd:getcap", + "cmd:getpcaps", + "cmd:setcap" + ] + }, + "perl-test-nowarnings-doc": { + "versions": { + "1.04-r1": 1509468472 + }, + "origin": "perl-test-nowarnings" + }, + "pcre-doc": { + "versions": { + "8.41-r1": 1509464277 + }, + "origin": "pcre" + }, + "vblade-doc": { + "versions": { + "23-r0": 1509493237 + }, + "origin": "vblade" + }, + "lua5.3-dbi-sqlite3": { + "versions": { + "0.6-r1": 1511483403 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "openldap-overlay-syncprov": { + "versions": { + "2.4.45-r3": 1510258135, + "2.4.46-r0": 1565073941, + "2.4.48-r0": 1566900518 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "gpgme-doc": { + "versions": { + "1.9.0-r1": 1510588163 + }, + "origin": "gpgme" + }, + "libao-dev": { + "versions": { + "1.2.0-r2": 1543926072 + }, + "origin": "libao", + "dependencies": [ + "libao=1.2.0-r2", + "pkgconfig" + ], + "provides": [ + "pc:ao=1.2.0" + ] + }, + "xf86-video-nouveau": { + "versions": { + "1.0.15-r0": 1510074882 + }, + "origin": "xf86-video-nouveau", + "dependencies": [ + "mesa-dri-nouveau", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_nouveau.so.2", + "so:libudev.so.1" + ] + }, + "kamailio-utils": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libsrdb1.so.1", + "so:libxml2.so.2" + ] + }, + "libspf2-tools": { + "versions": { + "1.2.10-r2": 1509491332 + }, + "origin": "libspf2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libspf2.so.2" + ], + "provides": [ + "cmd:spfd", + "cmd:spfquery" + ] + }, + "resourceproto-doc": { + "versions": { + "1.2.0-r2": 1509468557 + }, + "origin": "resourceproto" + }, + "kamailio-outbound": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "xscreensaver-extras": { + "versions": { + "5.36-r0": 1510074354 + }, + "origin": "xscreensaver", + "dependencies": [ + "bc", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXmu.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgdk_pixbuf_xlib-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libjpeg.so.8" + ] + }, + "email-doc": { + "versions": { + "3.1.4-r4": 1510260519 + }, + "origin": "email" + }, + "live-media": { + "versions": { + "2017.10.28-r0": 1510046892 + }, + "origin": "live-media", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libBasicUsageEnvironment.so.1=1.0.0", + "so:libUsageEnvironment.so.3=3.1.0", + "so:libgroupsock.so.8=8.1.1", + "so:libliveMedia.so.61=61.0.0" + ] + }, + "netcf": { + "versions": { + "0.2.8-r4": 1509491202 + }, + "origin": "netcf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetcf.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:ncftool" + ] + }, + "libsodium-dev": { + "versions": { + "1.0.15-r0": 1509474812 + }, + "origin": "libsodium", + "dependencies": [ + "libsodium=1.0.15-r0", + "pkgconfig" + ], + "provides": [ + "pc:libsodium=1.0.15" + ] + }, + "imap": { + "versions": { + "2007f-r7": 1510258915 + }, + "origin": "imap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:imapd", + "cmd:ipop2d", + "cmd:ipop3d" + ] + }, + "gnats": { + "versions": { + "4.2.0-r3": 1509496000 + }, + "origin": "gnats", + "dependencies": [ + "postfix", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:edit-pr", + "cmd:getclose", + "cmd:install-sid", + "cmd:query-pr", + "cmd:send-pr" + ] + }, + "open-isns-dev": { + "versions": { + "0.97-r2": 1510259303 + }, + "origin": "open-isns", + "dependencies": [ + "open-isns-lib=0.97-r2" + ] + }, + "kamailio-presence": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libsrdb1.so.1", + "so:libsrutils.so.1", + "so:libxml2.so.2" + ] + }, + "mkfontscale-doc": { + "versions": { + "1.1.2-r1": 1509469865 + }, + "origin": "mkfontscale" + }, + "acf-lib-lua5.1": { + "versions": { + "0.10.1-r0": 1510068294 + }, + "origin": "acf-lib" + }, + "freeswitch-dev": { + "versions": { + "1.6.19-r0": 1510585356 + }, + "origin": "freeswitch", + "dependencies": [ + "freeswitch-freetdm=1.6.19-r0", + "freeswitch=1.6.19-r0", + "pkgconfig" + ], + "provides": [ + "pc:freeswitch=1.6.19", + "pc:freetdm=0.1" + ] + }, + "perl-datetime-format-w3cdtf": { + "versions": { + "0.07-r0": 1510859374 + }, + "origin": "perl-datetime-format-w3cdtf", + "dependencies": [ + "perl", + "perl-datetime" + ] + }, + "xfce4-settings": { + "versions": { + "4.12.1-r0": 1510303246 + }, + "origin": "xfce4-settings", + "dependencies": [ + "so:libX11.so.6", + "so:libXcursor.so.1", + "so:libXi.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libexo-1.so.0", + "so:libfontconfig.so.1", + "so:libgarcon-1.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libnotify.so.4", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libupower-glib.so.3", + "so:libxfce4kbd-private-2.so.0", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2", + "so:libxklavier.so.16" + ], + "provides": [ + "cmd:xfce4-accessibility-settings", + "cmd:xfce4-appearance-settings", + "cmd:xfce4-display-settings", + "cmd:xfce4-keyboard-settings", + "cmd:xfce4-mime-settings", + "cmd:xfce4-mouse-settings", + "cmd:xfce4-settings-editor", + "cmd:xfce4-settings-manager", + "cmd:xfsettingsd" + ] + }, + "lua5.3-json4": { + "versions": { + "1.0.0-r2": 1509468370 + }, + "origin": "lua-json4" + }, + "lua5.3-cjson": { + "versions": { + "2.1.0-r7": 1509462474 + }, + "origin": "lua-cjson", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "haserl-lua5.1": { + "versions": { + "0.9.35-r1": 1509468298 + }, + "origin": "haserl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ], + "provides": [ + "cmd:haserl-lua5.1" + ] + }, + "aumix": { + "versions": { + "2.9.1-r6": 1509476722 + }, + "origin": "aumix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:aumix", + "cmd:mute", + "cmd:xaumix" + ] + }, + "py2-phonenumbers": { + "versions": { + "8.8.6-r0": 1511018972 + }, + "origin": "py-phonenumbers", + "dependencies": [ + "python2" + ] + }, + "py3-ply": { + "versions": { + "3.11-r0": 1556865249 + }, + "origin": "py3-ply", + "dependencies": [ + "python3" + ] + }, + "userspace-rcu-doc": { + "versions": { + "0.10.0-r0": 1509479772 + }, + "origin": "userspace-rcu" + }, + "yajl-dev": { + "versions": { + "2.1.0-r0": 1509475309 + }, + "origin": "yajl", + "dependencies": [ + "pkgconfig", + "yajl=2.1.0-r0" + ], + "provides": [ + "pc:yajl=2.1.0" + ] + }, + "xfwm4-themes": { + "versions": { + "4.10.0-r0": 1510074462 + }, + "origin": "xfwm4-themes", + "dependencies": [ + "xfwm4" + ] + }, + "garcon": { + "versions": { + "0.6.1-r1": 1510068108 + }, + "origin": "garcon", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-3.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libxfce4ui-1.so.0", + "so:libxfce4ui-2.so.0", + "so:libxfce4util.so.7" + ], + "provides": [ + "so:libgarcon-1.so.0=0.0.0", + "so:libgarcon-gtk2-1.so.0=0.0.0", + "so:libgarcon-gtk3-1.so.0=0.0.0" + ] + }, + "libxfce4ui-doc": { + "versions": { + "4.12.1-r3": 1510067993 + }, + "origin": "libxfce4ui" + }, + "libice-dev": { + "versions": { + "1.0.9-r2": 1509465974 + }, + "origin": "libice", + "dependencies": [ + "libice=1.0.9-r2", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:ice=1.0.9" + ] + }, + "libraw1394-doc": { + "versions": { + "2.1.2-r0": 1509470073 + }, + "origin": "libraw1394" + }, + "inotify-tools-dev": { + "versions": { + "3.14-r2": 1509475672 + }, + "origin": "inotify-tools", + "dependencies": [ + "inotify-tools=3.14-r2" + ] + }, + "lua5.3-filesystem": { + "versions": { + "1.7.0.2-r0": 1511049451 + }, + "origin": "lua-filesystem", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "sipsak": { + "versions": { + "0.9.6-r9": 1510260059 + }, + "origin": "sipsak", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libcrypto.so.42" + ], + "provides": [ + "cmd:sipsak" + ] + }, + "nano-doc": { + "versions": { + "2.9.1-r0": 1512032032 + }, + "origin": "nano" + }, + "iftop-doc": { + "versions": { + "0.17-r7": 1509489487 + }, + "origin": "iftop" + }, + "pciutils-dev": { + "versions": { + "3.5.6-r0": 1511165900 + }, + "origin": "pciutils", + "dependencies": [ + "pciutils-libs=3.5.6-r0", + "pkgconfig" + ], + "provides": [ + "pc:libpci=3.5.6" + ] + }, + "wayland-libs-client": { + "versions": { + "1.14.0-r2": 1510066936 + }, + "origin": "wayland", + "dependencies": [ + "wayland-libs-client", + "wayland-libs-cursor", + "wayland-libs-server", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6" + ], + "provides": [ + "so:libwayland-client.so.0=0.3.0" + ] + }, + "py-phonenumbers": { + "versions": { + "8.8.6-r0": 1511018982 + }, + "origin": "py-phonenumbers" + }, + "perl-module-refresh-doc": { + "versions": { + "0.17-r1": 1510564499 + }, + "origin": "perl-module-refresh" + }, + "xvidcore-dev": { + "versions": { + "1.3.4-r0": 1509480928 + }, + "origin": "xvidcore", + "dependencies": [ + "xvidcore=1.3.4-r0" + ] + }, + "iceauth": { + "versions": { + "1.0.7-r0": 1509482662 + }, + "origin": "iceauth", + "dependencies": [ + "so:libICE.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:iceauth" + ] + }, + "xcb-util-renderutil": { + "versions": { + "0.3.9-r1": 1509473920 + }, + "origin": "xcb-util-renderutil", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-render.so.0", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-render-util.so.0=0.0.0" + ] + }, + "libverto-libev": { + "versions": { + "0.3.0-r0": 1509469586 + }, + "origin": "libverto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libev.so.4", + "so:libverto.so.1" + ], + "provides": [ + "so:libverto-libev.so.1=1.0.0" + ] + }, + "mtu": { + "versions": { + "1.5-r1": 1509474487 + }, + "origin": "pingu", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mtu" + ] + }, + "perl-extutils-helpers": { + "versions": { + "0.026-r0": 1509474753 + }, + "origin": "perl-extutils-helpers" + }, + "socat": { + "versions": { + "1.7.3.2-r3": 1510260539 + }, + "origin": "socat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libreadline.so.7", + "so:libssl.so.44" + ], + "provides": [ + "cmd:filan", + "cmd:procan", + "cmd:socat" + ] + }, + "nagios-plugins-dummy": { + "versions": { + "2.2.1-r3": 1510288495 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-io-doc": { + "versions": { + "1.25-r4": 1509483995 + }, + "origin": "perl-io" + }, + "alpine-base": { + "versions": { + "3.7.3-r0": 1551903503 + }, + "origin": "alpine-base", + "dependencies": [ + "alpine-baselayout", + "alpine-conf", + "apk-tools", + "busybox", + "busybox-suid", + "busybox-initscripts", + "openrc", + "libc-utils", + "alpine-keys" + ] + }, + "gigolo-doc": { + "versions": { + "0.4.2-r0": 1510076413 + }, + "origin": "gigolo" + }, + "aumix-doc": { + "versions": { + "2.9.1-r6": 1509476721 + }, + "origin": "aumix" + }, + "libjpeg-turbo-dev": { + "versions": { + "1.5.3-r2": 1537879742, + "1.5.3-r3": 1563792633 + }, + "origin": "libjpeg-turbo", + "dependencies": [ + "libjpeg-turbo=1.5.3-r3", + "pkgconfig" + ], + "provides": [ + "pc:libjpeg=1.5.3", + "pc:libturbojpeg=1.5.3" + ] + }, + "nettle": { + "versions": { + "3.3-r0": 1509465116 + }, + "origin": "nettle", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10" + ], + "provides": [ + "so:libhogweed.so.4=4.3", + "so:libnettle.so.6=6.3" + ] + }, + "drill": { + "versions": { + "1.6.17-r6": 1510258937 + }, + "origin": "ldns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libldns.so.1" + ], + "provides": [ + "cmd:drill" + ] + }, + "krb5-libs": { + "versions": { + "1.15.4-r0": 1548491443 + }, + "origin": "krb5", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libcrypto.so.42", + "so:libkeyutils.so.1", + "so:libssl.so.44", + "so:libverto.so.1" + ], + "provides": [ + "so:libgssapi_krb5.so.2=2.2", + "so:libgssrpc.so.4=4.2", + "so:libk5crypto.so.3=3.1", + "so:libkadm5clnt_mit.so.11=11.0", + "so:libkadm5srv_mit.so.11=11.0", + "so:libkdb5.so.8=8.0", + "so:libkrad.so.0=0.0", + "so:libkrb5.so.3=3.3", + "so:libkrb5support.so.0=0.1" + ] + }, + "perl-module-build-doc": { + "versions": { + "0.4224-r0": 1509459456 + }, + "origin": "perl-module-build" + }, + "python3-wininst": { + "versions": { + "3.6.8-r0": 1555928809, + "3.6.9-r0": 1571233995, + "3.6.9-r1": 1571314639 + }, + "origin": "python3" + }, + "libshout-dev": { + "versions": { + "2.4.1-r1": 1510068381 + }, + "origin": "libshout", + "dependencies": [ + "speex-dev", + "libshout=2.4.1-r1", + "pc:ogg", + "pc:speex", + "pc:theora", + "pc:vorbis", + "pkgconfig" + ], + "provides": [ + "pc:shout=2.4.1" + ] + }, + "perl-locale-maketext-fuzzy-doc": { + "versions": { + "0.11-r1": 1510848491 + }, + "origin": "perl-locale-maketext-fuzzy" + }, + "lua5.1-posixtz": { + "versions": { + "0.5-r1": 1509479783 + }, + "origin": "lua-posixtz", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-mail-spamassassin": { + "versions": { + "3.4.1-r8": 1509517413, + "3.4.1-r2": 1509517006 + }, + "origin": "perl-mail-spamassassin", + "dependencies": [ + "perl-mail-dkim", + "perl-netaddr-ip", + "perl-digest-sha1", + "perl-html-parser", + "perl-net-dns", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sa-awl", + "cmd:sa-check_spamd", + "cmd:sa-compile", + "cmd:sa-learn", + "cmd:sa-update", + "cmd:spamassassin", + "cmd:spamc", + "cmd:spamd" + ] + }, + "boost-wave": { + "versions": { + "1.62.0-r5": 1509465882 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.62.0", + "so:libboost_thread-mt.so.1.62.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_wave-mt.so.1.62.0=1.62.0" + ] + }, + "mdadm-misc": { + "versions": { + "4.0-r0": 1509476489 + }, + "origin": "mdadm", + "dependencies": [ + "mdadm", + "bash" + ], + "provides": [ + "cmd:handle-mdadm-events", + "cmd:mdcheck" + ] + }, + "cgit": { + "versions": { + "1.1-r3": 1533395015 + }, + "origin": "cgit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:liblua-5.3.so.0", + "so:libz.so.1" + ] + }, + "py2-singledispatch": { + "versions": { + "3.4.0.3-r1": 1509552750 + }, + "origin": "py2-singledispatch", + "dependencies": [ + "python2" + ], + "provides": [ + "py-singledispatch" + ] + }, + "openjade-libs": { + "versions": { + "1.3.2-r5": 1509488726 + }, + "origin": "openjade", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libogrove.so.0=0.0.1", + "so:libospgrove.so.0=0.0.1", + "so:libostyle.so.0=0.0.1" + ] + }, + "nss-dev": { + "versions": { + "3.34.1-r0": 1511890215 + }, + "origin": "nss", + "dependencies": [ + "nss", + "nspr-dev", + "pc:nspr>=3.34.1", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libnspr4.so", + "so:libnssutil3.so", + "so:libplc4.so", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgtest1.so.34=34", + "so:libnsssysinit.so=34", + "pc:mozilla-nss=3.34.1", + "pc:nss-softokn=3.34.1", + "pc:nss-util=3.34.1", + "pc:nss=3.34.1", + "cmd:nss-config" + ] + }, + "perl-sub-exporter-progressive": { + "versions": { + "0.001013-r0": 1509473996 + }, + "origin": "perl-sub-exporter-progressive" + }, + "libxdg-basedir-dev": { + "versions": { + "1.2.0-r0": 1509494298 + }, + "origin": "libxdg-basedir", + "dependencies": [ + "libxdg-basedir=1.2.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libxdg-basedir=1.2.0" + ] + }, + "sdl_mixer": { + "versions": { + "1.2.12-r1": 1510075316 + }, + "origin": "sdl_mixer", + "dependencies": [ + "so:libSDL-1.2.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL_mixer-1.2.so.0=0.12.0" + ] + }, + "speex-tools": { + "versions": { + "1.2.0-r0": 1509473247 + }, + "origin": "speex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libspeex.so.1", + "so:libspeexdsp.so.1" + ], + "provides": [ + "cmd:speexdec", + "cmd:speexenc" + ] + }, + "ruby-bigdecimal": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ] + }, + "libx11": { + "versions": { + "1.6.6-r0": 1538999955 + }, + "origin": "libx11", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libX11-xcb.so.1=1.0.0", + "so:libX11.so.6=6.3.0" + ] + }, + "perl-gdgraph-doc": { + "versions": { + "1.54-r0": 1509496362 + }, + "origin": "perl-gdgraph" + }, + "avahi-dev": { + "versions": { + "0.6.32-r4": 1509465445, + "0.6.32-r5": 1563345619 + }, + "origin": "avahi", + "dependencies": [ + "gdbm-dev", + "avahi-compat-howl=0.6.32-r5", + "avahi-compat-libdns_sd=0.6.32-r5", + "avahi-glib=0.6.32-r5", + "avahi-libs=0.6.32-r5", + "avahi=0.6.32-r5", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:avahi-client=0.6.32", + "pc:avahi-compat-howl=0.9.8", + "pc:avahi-compat-libdns_sd=0.6.32", + "pc:avahi-core=0.6.32", + "pc:avahi-glib=0.6.32", + "pc:avahi-gobject=0.6.32", + "pc:howl=0.9.8", + "pc:libdns_sd=0.6.32" + ] + }, + "gnome-vfs-lang": { + "versions": { + "2.24.4-r6": 1510931439 + }, + "origin": "gnome-vfs", + "dependencies": [ + "gnome-mime-data" + ] + }, + "libmatroska": { + "versions": { + "1.4.8-r0": 1509479839 + }, + "origin": "libmatroska", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libebml.so.4", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libmatroska.so.6=6.0.0" + ] + }, + "collectd-lua": { + "versions": { + "5.7.2-r0": 1510069653 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ] + }, + "gsm-tools": { + "versions": { + "1.0.16-r0": 1509473212 + }, + "origin": "gsm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgsm.so.1" + ], + "provides": [ + "cmd:tcat", + "cmd:toast", + "cmd:untoast" + ] + }, + "darkice": { + "versions": { + "1.3-r0": 1509470067 + }, + "origin": "darkice", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libfaac.so.0", + "so:libgcc_s.so.1", + "so:libmp3lame.so.0", + "so:libogg.so.0", + "so:libstdc++.so.6", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2" + ], + "provides": [ + "cmd:darkice" + ] + }, + "sysfsutils": { + "versions": { + "2.1.0-r8": 1509475366 + }, + "origin": "sysfsutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsysfs.so.2=2.0.1", + "cmd:dlist_test", + "cmd:get_device", + "cmd:get_driver", + "cmd:get_module", + "cmd:systool" + ] + }, + "librevenge-dev": { + "versions": { + "0.0.4-r0": 1509495165 + }, + "origin": "librevenge", + "dependencies": [ + "boost-dev", + "zlib-dev", + "cppunit-dev", + "librevenge=0.0.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:librevenge-0.0=0.0.4", + "pc:librevenge-generators-0.0=0.0.4", + "pc:librevenge-stream-0.0=0.0.4" + ] + }, + "freeradius-rest": { + "versions": { + "3.0.15-r4": 1556202797, + "3.0.15-r5": 1566310605 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libjson-c.so.2" + ], + "provides": [ + "so:rlm_rest.so=0" + ] + }, + "postfix-policyd-spf-perl": { + "versions": { + "2.007-r2": 1509494151 + }, + "origin": "postfix-policyd-spf-perl", + "dependencies": [ + "perl", + "perl-mail-spf", + "perl-netaddr-ip" + ], + "provides": [ + "cmd:postfix-policyd-spf-perl" + ] + }, + "libmicrohttpd": { + "versions": { + "0.9.57-r0": 1512032047 + }, + "origin": "libmicrohttpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30" + ], + "provides": [ + "so:libmicrohttpd.so.12=12.45.0" + ] + }, + "perl-net-snmp": { + "versions": { + "6.0.1-r2": 1509471907 + }, + "origin": "perl-net-snmp", + "dependencies": [ + "perl-crypt-des", + "perl-crypt-rijndael", + "perl-digest-sha1" + ], + "provides": [ + "cmd:snmpkey" + ] + }, + "boost-random": { + "versions": { + "1.62.0-r5": 1509465878 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.62.0", + "so:libboost_system.so.1.62.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_random-mt.so.1.62.0=1.62.0", + "so:libboost_random.so.1.62.0=1.62.0" + ] + }, + "libmnl-dev": { + "versions": { + "1.0.4-r0": 1509469217 + }, + "origin": "libmnl", + "dependencies": [ + "linux-headers", + "libmnl=1.0.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmnl=1.0.4" + ] + }, + "xgnokii": { + "versions": { + "0.6.31-r6": 1510069847 + }, + "origin": "gnokii", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgnokii.so.7", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:xgnokii" + ] + }, + "ninja-doc": { + "versions": { + "1.8.2-r0": 1509475826 + }, + "origin": "ninja" + }, + "rtpproxy": { + "versions": { + "2.0.0-r4": 1509492569 + }, + "origin": "rtpproxy", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rtpproxy" + ] + }, + "xf86-input-keyboard": { + "versions": { + "1.9.0-r0": 1510074947 + }, + "origin": "xf86-input-keyboard", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "xrefresh-doc": { + "versions": { + "1.0.5-r0": 1509494698 + }, + "origin": "xrefresh" + }, + "cups": { + "versions": { + "2.2.10-r0": 1549287810, + "2.2.12-r0": 1566207598 + }, + "origin": "cups", + "dependencies": [ + "cups-client", + "poppler-utils", + "libressl", + "dbus", + "/bin/sh", + "cups-client=2.2.12-r0", + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libcupsimage.so.2", + "so:libdbus-1.so.3", + "so:libgcc_s.so.1", + "so:libpaper.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:cupsd", + "cmd:cupsfilter" + ] + }, + "distcc-pump": { + "versions": { + "3.1-r12": 1509493044 + }, + "origin": "distcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:pump" + ] + }, + "perl-digest-md5-doc": { + "versions": { + "2.55-r2": 1509482651 + }, + "origin": "perl-digest-md5" + }, + "font-bh-type1": { + "versions": { + "1.0.3-r0": 1509494001 + }, + "origin": "font-bh-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "weechat-ruby": { + "versions": { + "1.9.1-r1": 1509530223 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.4" + ] + }, + "goffice": { + "versions": { + "0.10.36-r0": 1511455403 + }, + "origin": "goffice", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:librsvg-2.so.2", + "so:libxml2.so.2", + "so:libxslt.so.1" + ], + "provides": [ + "so:libgoffice-0.10.so.10=10.0.36" + ] + }, + "py2-mako": { + "versions": { + "1.0.7-r0": 1509468232 + }, + "origin": "py-mako", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:mako-render" + ] + }, + "libssh2-doc": { + "versions": { + "1.8.2-r0": 1553682025, + "1.9.0-r0": 1571235284, + "1.9.0-r1": 1571996777 + }, + "origin": "libssh2" + }, + "py-zope-interface": { + "versions": { + "4.3.2-r1": 1509493285 + }, + "origin": "py-zope-interface" + }, + "libvdpau-doc": { + "versions": { + "1.1.1-r1": 1509467160 + }, + "origin": "libvdpau" + }, + "tinydns": { + "versions": { + "1.05-r47": 1509488767 + }, + "origin": "djbdns", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tinydns", + "cmd:tinydns-conf", + "cmd:tinydns-data", + "cmd:tinydns-edit", + "cmd:tinydns-get" + ] + }, + "perl-socket-doc": { + "versions": { + "2.024-r2": 1509484006 + }, + "origin": "perl-socket" + }, + "nghttp2": { + "versions": { + "1.28.0-r0": 1511922922, + "1.39.2-r0": 1568186892 + }, + "origin": "nghttp2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libcrypto.so.42", + "so:libev.so.4", + "so:libgcc_s.so.1", + "so:libnghttp2.so.14", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:h2load", + "cmd:nghttp", + "cmd:nghttpd", + "cmd:nghttpx" + ] + }, + "sdl_image-dev": { + "versions": { + "1.2.12-r3": 1510067913, + "1.2.12-r4": 1574265384 + }, + "origin": "sdl_image", + "dependencies": [ + "pc:sdl>=1.2.10", + "pkgconfig", + "sdl_image=1.2.12-r4" + ], + "provides": [ + "pc:SDL_image=1.2.12" + ] + }, + "dwm": { + "versions": { + "6.1-r2": 1509494032 + }, + "origin": "dwm", + "dependencies": [ + "dmenu", + "st", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1" + ], + "provides": [ + "cmd:dwm" + ] + }, + "xf86dgaproto": { + "versions": { + "2.1-r4": 1509473781 + }, + "origin": "xf86dgaproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xf86dgaproto=2.1" + ] + }, + "device-mapper-event-libs": { + "versions": { + "2.02.175-r0": 1509462402 + }, + "origin": "lvm2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "so:libdevmapper-event.so.1.02=1.02" + ] + }, + "recordmydesktop": { + "versions": { + "0.3.8.1-r2": 1510075370 + }, + "origin": "recordmydesktop", + "dependencies": [ + "so:libX11.so.6", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libtheora.so.0", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:recordmydesktop" + ] + }, + "mosquitto-doc": { + "versions": { + "1.4.15-r0": 1520176488, + "1.4.15-r2": 1563346323 + }, + "origin": "mosquitto" + }, + "cciss_vol_status": { + "versions": { + "1.12-r0": 1509493252 + }, + "origin": "cciss_vol_status", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cciss_vol_status" + ] + }, + "makekit": { + "versions": { + "0.2-r0": 1509494663 + }, + "origin": "makekit", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:makekit", + "cmd:mkdash" + ] + }, + "libmpeg2-dev": { + "versions": { + "0.5.1-r7": 1509473556 + }, + "origin": "libmpeg2", + "dependencies": [ + "libmpeg2=0.5.1-r7", + "pkgconfig" + ], + "provides": [ + "pc:libmpeg2=0.5.1", + "pc:libmpeg2convert=0.5.1" + ] + }, + "rsnapshot": { + "versions": { + "1.4.2-r0": 1509480276 + }, + "origin": "rsnapshot", + "dependencies": [ + "perl", + "rsync", + "openssh-client" + ], + "provides": [ + "cmd:rsnapshot", + "cmd:rsnapshot-diff" + ] + }, + "perl-params-classify": { + "versions": { + "0.015-r0": 1509470455 + }, + "origin": "perl-params-classify", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libedit-dev": { + "versions": { + "20170329.3.1-r3": 1509461724 + }, + "origin": "libedit", + "dependencies": [ + "ncurses-dev", + "libedit=20170329.3.1-r3", + "pkgconfig" + ], + "provides": [ + "pc:libedit=3.1" + ] + }, + "file": { + "versions": { + "5.32-r0": 1509461605, + "5.32-r1": 1563340161, + "5.32-r2": 1572348414 + }, + "origin": "file", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmagic.so.1" + ], + "provides": [ + "cmd:file" + ] + }, + "xfce4-screenshooter-lang": { + "versions": { + "1.8.2-r1": 1510074733 + }, + "origin": "xfce4-screenshooter" + }, + "libxxf86misc": { + "versions": { + "1.0.3-r1": 1509473661 + }, + "origin": "libxxf86misc", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXxf86misc.so.1=1.1.0" + ] + }, + "cyrus-sasl-doc": { + "versions": { + "2.1.26-r11": 1510258044 + }, + "origin": "cyrus-sasl" + }, + "libetpan-dev": { + "versions": { + "1.8-r2": 1509721604 + }, + "origin": "libetpan", + "dependencies": [ + "cyrus-sasl-dev", + "db-dev", + "libetpan=1.8-r2" + ], + "provides": [ + "cmd:libetpan-config" + ] + }, + "radvd-doc": { + "versions": { + "2.16-r0": 1509493025 + }, + "origin": "radvd" + }, + "perl-list-moreutils": { + "versions": { + "0.419-r1": 1509474021 + }, + "origin": "perl-list-moreutils", + "dependencies": [ + "perl-exporter-tiny" + ] + }, + "perl-html-tagset-doc": { + "versions": { + "3.20-r1": 1509464386 + }, + "origin": "perl-html-tagset" + }, + "libdrm-dev": { + "versions": { + "2.4.88-r0": 1510649882 + }, + "origin": "libdrm", + "dependencies": [ + "linux-headers", + "libdrm=2.4.88-r0", + "pkgconfig" + ], + "provides": [ + "pc:libdrm=2.4.88", + "pc:libdrm_amdgpu=2.4.88", + "pc:libdrm_freedreno=2.4.88", + "pc:libdrm_intel=2.4.88", + "pc:libdrm_nouveau=2.4.88", + "pc:libdrm_radeon=2.4.88", + "pc:libkms=1.0.0" + ] + }, + "perl-test-tcp-doc": { + "versions": { + "2.14-r0": 1509481784 + }, + "origin": "perl-test-tcp" + }, + "nginx-mod-http-geoip": { + "versions": { + "1.12.2-r4": 1542814446 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.2-doc": { + "versions": { + "5.2.4-r4": 1509459694 + }, + "origin": "lua5.2" + }, + "charybdis-doc": { + "versions": { + "3.5.5-r3": 1510260651 + }, + "origin": "charybdis" + }, + "perl-symbol-global-name": { + "versions": { + "0.05-r1": 1510564552 + }, + "origin": "perl-symbol-global-name" + }, + "perl-test-pod-doc": { + "versions": { + "1.51-r1": 1509461791 + }, + "origin": "perl-test-pod" + }, + "tinyxml-dev": { + "versions": { + "2.6.2-r1": 1509496004 + }, + "origin": "tinyxml", + "dependencies": [ + "tinyxml=2.6.2-r1" + ] + }, + "openvswitch-dev": { + "versions": { + "2.8.1-r2": 1512030506 + }, + "origin": "openvswitch", + "dependencies": [ + "libressl-dev", + "pkgconfig" + ], + "provides": [ + "pc:libofproto=2.8.1", + "pc:libopenvswitch=2.8.1", + "pc:libovsdb=2.8.1", + "pc:libsflow=2.8.1" + ] + }, + "asciidoc": { + "versions": { + "8.6.10-r0": 1509461782 + }, + "origin": "asciidoc", + "dependencies": [ + "python2", + "libxml2-utils", + "docbook-xsl" + ], + "provides": [ + "cmd:a2x", + "cmd:a2x.py", + "cmd:asciidoc", + "cmd:asciidoc.py" + ] + }, + "swish-e-doc": { + "versions": { + "2.4.7-r8": 1509483302 + }, + "origin": "swish-e" + }, + "perl-gdtextutil": { + "versions": { + "0.86-r0": 1509495191 + }, + "origin": "perl-gdtextutil", + "dependencies": [ + "perl-gd" + ] + }, + "thunar-volman-lang": { + "versions": { + "0.8.1-r2": 1510075201 + }, + "origin": "thunar-volman" + }, + "dansguardian": { + "versions": { + "2.12.0.3-r4": 1509494015 + }, + "origin": "dansguardian", + "dependencies": [ + "logrotate", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpcreposix.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:dansguardian" + ] + }, + "openldap-passwd-pbkdf2": { + "versions": { + "2.4.45-r3": 1510258132, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "openldap", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "libxcb-dev": { + "versions": { + "1.12-r1": 1509461874 + }, + "origin": "libxcb", + "dependencies": [ + "libxau-dev", + "xcb-proto", + "libxcb=1.12-r1", + "pc:pthread-stubs", + "pc:xau>=0.99.2", + "pc:xdmcp", + "pkgconfig" + ], + "provides": [ + "pc:xcb-composite=1.12", + "pc:xcb-damage=1.12", + "pc:xcb-dpms=1.12", + "pc:xcb-dri2=1.12", + "pc:xcb-dri3=1.12", + "pc:xcb-glx=1.12", + "pc:xcb-present=1.12", + "pc:xcb-randr=1.12", + "pc:xcb-record=1.12", + "pc:xcb-render=1.12", + "pc:xcb-res=1.12", + "pc:xcb-screensaver=1.12", + "pc:xcb-shape=1.12", + "pc:xcb-shm=1.12", + "pc:xcb-sync=1.12", + "pc:xcb-xf86dri=1.12", + "pc:xcb-xfixes=1.12", + "pc:xcb-xinerama=1.12", + "pc:xcb-xinput=1.12", + "pc:xcb-xkb=1.12", + "pc:xcb-xtest=1.12", + "pc:xcb-xv=1.12", + "pc:xcb-xvmc=1.12", + "pc:xcb=1.12" + ] + }, + "xclip": { + "versions": { + "0.13-r0": 1509495879 + }, + "origin": "xclip", + "dependencies": [ + "so:libX11.so.6", + "so:libXmu.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xclip", + "cmd:xclip-copyfile", + "cmd:xclip-cutfile", + "cmd:xclip-pastefile" + ] + }, + "qt-x11": { + "versions": { + "4.8.7-r8": 1510287209 + }, + "origin": "qt", + "dependencies": [ + "hicolor-icon-theme", + "so:libGL.so.1", + "so:libICE.so.6", + "so:libQtCore.so.4", + "so:libQtDBus.so.4", + "so:libQtNetwork.so.4", + "so:libQtScript.so.4", + "so:libQtSql.so.4", + "so:libQtXml.so.4", + "so:libQtXmlPatterns.so.4", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libjpeg.so.8", + "so:libmng.so.2", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libtiff.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libQt3Support.so.4=4.8.7", + "so:libQtCLucene.so.4=4.8.7", + "so:libQtDeclarative.so.4=4.8.7", + "so:libQtDesigner.so.4=4.8.7", + "so:libQtDesignerComponents.so.4=4.8.7", + "so:libQtGui.so.4=4.8.7", + "so:libQtHelp.so.4=4.8.7", + "so:libQtMultimedia.so.4=4.8.7", + "so:libQtOpenGL.so.4=4.8.7", + "so:libQtScriptTools.so.4=4.8.7", + "so:libQtSvg.so.4=4.8.7", + "cmd:qdbusviewer", + "cmd:qmlplugindump", + "cmd:qmlviewer" + ] + }, + "qpdf-doc": { + "versions": { + "7.0.0-r0": 1509482355 + }, + "origin": "qpdf" + }, + "gtk-engines-lang": { + "versions": { + "2.21.0-r2": 1510071720 + }, + "origin": "gtk-engines", + "dependencies": [ + "gtk-engines-clearlooks", + "gtk-engines-crux", + "gtk-engines-industrial", + "gtk-engines-mist", + "gtk-engines-redmond", + "gtk-engines-thinice" + ] + }, + "enca": { + "versions": { + "1.19-r1": 1509480335 + }, + "origin": "enca", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libenca.so.0=0.5.1", + "cmd:enca", + "cmd:enconv" + ] + }, + "py-flup": { + "versions": { + "1.0.2-r0": 1509489931 + }, + "origin": "py-flup", + "dependencies": [ + "python2" + ] + }, + "xfce4-skel": { + "versions": { + "4.12.0-r1": 1510920458 + }, + "origin": "xfce4", + "dependencies": [ + "exo>=0.10.3", + "garcon>=0.4.0", + "gtk-xfce-engine>=3.2.0", + "libxfce4ui>=4.12.0", + "libxfce4util>=4.12.0", + "thunar>=1.6.6", + "ttf-dejavu", + "xfce4-appfinder>=4.12.0", + "xfce4-mixer", + "xfce4-panel>=4.12.0", + "xfce4-power-manager>=1.4.3", + "xfce4-session>=4.12.0", + "xfce4-settings>=4.12.0", + "xfce4-terminal", + "xfconf>=4.12.0", + "xfdesktop>=4.12.0", + "xfwm4>=4.12.0" + ] + }, + "ninja": { + "versions": { + "1.8.2-r0": 1509475826 + }, + "origin": "ninja", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:ninja" + ] + }, + "openldap-back-shell": { + "versions": { + "2.4.45-r3": 1510258133, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "sfcapd": { + "versions": { + "1.6.15-r0": 1509494471, + "1.6.15-r1": 1574265426 + }, + "origin": "nfdump", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnfdump-1.6.15.so" + ], + "provides": [ + "cmd:sfcapd" + ] + }, + "py-pygments-doc": { + "versions": { + "2.2.0-r0": 1509493371 + }, + "origin": "py-pygments" + }, + "perl-net-libidn-doc": { + "versions": { + "0.12-r3": 1509468971 + }, + "origin": "perl-net-libidn" + }, + "jwm-lang": { + "versions": { + "2.3.7-r0": 1510075431 + }, + "origin": "jwm" + }, + "py-fuse": { + "versions": { + "0.2.1-r1": 1509493774 + }, + "origin": "py-fuse", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2", + "so:libpython2.7.so.1.0" + ] + }, + "perl-inline-c-doc": { + "versions": { + "0.76-r0": 1509480892 + }, + "origin": "perl-inline-c" + }, + "openvpn-auth-ldap": { + "versions": { + "2.0.3-r6": 1509492316 + }, + "origin": "openvpn-auth-ldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libobjc.so.4" + ], + "provides": [ + "so:openvpn-auth-ldap.so=0" + ] + }, + "libcddb": { + "versions": { + "1.3.2-r2": 1509473612 + }, + "origin": "libcddb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcddb.so.2=2.2.3", + "cmd:cddb_query" + ] + }, + "xfce4-terminal": { + "versions": { + "0.6.3-r1": 1510074621 + }, + "origin": "xfce4-terminal", + "dependencies": [ + "startup-notification", + "hicolor-icon-theme", + "so:libX11.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libvte.so.9", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7" + ], + "provides": [ + "cmd:xfce4-terminal" + ] + }, + "uvncrepeater": { + "versions": { + "014-r6": 1509491342 + }, + "origin": "uvncrepeater", + "dependencies": [ + "openrc>=0.6", + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:repeater" + ] + }, + "openldap-overlay-proxycache": { + "versions": { + "2.4.45-r3": 1510258135, + "2.4.46-r0": 1565073940, + "2.4.48-r0": 1566900517 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "mesa-gles": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglapi.so.0" + ], + "provides": [ + "so:libGLESv1_CM.so.1=1.1.0", + "so:libGLESv2.so.2=2.0.0" + ] + }, + "perl-email-address-list-doc": { + "versions": { + "0.05-r2": 1510564503 + }, + "origin": "perl-email-address-list" + }, + "lua-mosquitto": { + "versions": { + "0.2-r1": 1509496428 + }, + "origin": "lua-mosquitto" + }, + "aspell-doc": { + "versions": { + "0.60.6.1-r12": 1509472868, + "0.60.6.1-r13": 1572541141 + }, + "origin": "aspell" + }, + "acf-skins": { + "versions": { + "0.6.0-r1": 1510068296 + }, + "origin": "acf-skins" + }, + "xfce4-whiskermenu-plugin": { + "versions": { + "1.7.3-r0": 1510073069 + }, + "origin": "xfce4-whiskermenu-plugin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libexo-1.so.0", + "so:libgarcon-1.so.0", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libstdc++.so.6", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7" + ], + "provides": [ + "cmd:xfce4-popup-whiskermenu" + ] + }, + "protobuf-dev": { + "versions": { + "3.4.1-r1": 1510846093 + }, + "origin": "protobuf", + "dependencies": [ + "zlib-dev", + "pkgconfig", + "protobuf=3.4.1-r1" + ], + "provides": [ + "pc:protobuf-lite=3.4.1", + "pc:protobuf=3.4.1" + ] + }, + "tlsdate-doc": { + "versions": { + "0.0.13-r5": 1510259209 + }, + "origin": "tlsdate" + }, + "spawn-fcgi": { + "versions": { + "1.6.4-r3": 1509552746 + }, + "origin": "spawn-fcgi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:spawn-fcgi" + ] + }, + "perl-module-build-tiny": { + "versions": { + "0.039-r0": 1509474782 + }, + "origin": "perl-module-build-tiny", + "dependencies": [ + "perl-extutils-installpaths", + "perl-extutils-config", + "perl-extutils-helpers", + "perl-test-harness" + ] + }, + "dahdi-linux-hardened-dev": { + "versions": { + "4.9.65-r1": 1511798670 + }, + "origin": "dahdi-linux-hardened", + "dependencies": [ + "dahdi-linux-dev" + ] + }, + "lua-penlight-shared": { + "versions": { + "1.5.4-r0": 1509485590 + }, + "origin": "lua-penlight" + }, + "lua-alt-getopt": { + "versions": { + "0.7-r8": 1509480081 + }, + "origin": "lua-alt-getopt" + }, + "asterisk-sample-config": { + "versions": { + "15.6.1-r0": 1537795342, + "15.6.2-r0": 1568705004 + }, + "origin": "asterisk" + }, + "lua5.1-lyaml": { + "versions": { + "6.1.3-r1": 1509473484 + }, + "origin": "lua-lyaml", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libyaml-0.so.2" + ] + }, + "heimdal-libs": { + "versions": { + "7.4.0-r2": 1514545901, + "7.4.0-r3": 1559659765, + "7.4.0-r4": 1562862336 + }, + "origin": "heimdal", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libdb-5.3.so", + "so:libreadline.so.7", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libasn1.so.8=8.0.0", + "so:libgssapi.so.3=3.0.0", + "so:libhcrypto.so.4=4.1.0", + "so:libhdb.so.9=9.2.0", + "so:libheimbase.so.1=1.0.0", + "so:libheimntlm.so.0=0.1.0", + "so:libhx509.so.5=5.0.0", + "so:libkadm5clnt.so.7=7.0.1", + "so:libkadm5srv.so.8=8.0.1", + "so:libkafs.so.0=0.5.1", + "so:libkdc.so.2=2.0.0", + "so:libkrb5.so.26=26.0.0", + "so:libotp.so.0=0.1.5", + "so:libroken.so.18=18.1.0", + "so:libsl.so.0=0.2.1", + "so:libwind.so.0=0.0.0", + "so:windc.so.0=0.0.0", + "cmd:digest-service", + "cmd:kdigest", + "cmd:string2key", + "cmd:verify_krb5_conf" + ] + }, + "vala-doc": { + "versions": { + "0.36.4-r0": 1509480647 + }, + "origin": "vala" + }, + "samba-client-libs": { + "versions": { + "4.7.6-r3": 1555491787 + }, + "origin": "samba", + "dependencies": [ + "so:libaddns-samba4.so", + "so:libasn1util-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libinterfaces-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-reg-samba4.so" + ], + "provides": [ + "so:libcli-ldap-samba4.so=0", + "so:libcmdline-credentials-samba4.so=0", + "so:libdcerpc.so.0=0.0.1", + "so:libdsdb-garbage-collect-tombstones-samba4.so=0", + "so:libevents-samba4.so=0", + "so:libhttp-samba4.so=0", + "so:libnetif-samba4.so=0", + "so:libregistry-samba4.so=0", + "so:libsmbclient-raw-samba4.so=0" + ] + }, + "postgresql-libs": { + "versions": { + "10.7-r0": 1554274198, + "10.8-r0": 1557668015, + "10.9-r0": 1562225614, + "10.10-r0": 1565683438 + }, + "origin": "postgresql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:libecpg.so.6=6.10", + "so:libecpg_compat.so.3=3.10", + "so:libpgtypes.so.3=3.10" + ] + }, + "py-google-api-python-client": { + "versions": { + "1.4.2-r1": 1509551801 + }, + "origin": "py-google-api-python-client", + "dependencies": [ + "python2", + "py-httplib2", + "py-oauth2client", + "py-uritemplate", + "py-six" + ] + }, + "libwnck-lang": { + "versions": { + "2.31.0-r5": 1510068129 + }, + "origin": "libwnck" + }, + "freeradius-client-dev": { + "versions": { + "1.1.7-r1": 1509488299 + }, + "origin": "freeradius-client", + "dependencies": [ + "freeradius-client=1.1.7-r1" + ] + }, + "xdm": { + "versions": { + "1.1.11-r5": 1509480595 + }, + "origin": "xdm", + "dependencies": [ + "sessreg", + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXaw.so.7", + "so:libXdmcp.so.6", + "so:libXext.so.6", + "so:libXmu.so.6", + "so:libXpm.so.4", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0" + ], + "provides": [ + "cmd:xdm" + ] + }, + "net-snmp-libs": { + "versions": { + "5.7.3-r10": 1510259616 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ], + "provides": [ + "so:libnetsnmp.so.30=30.0.3" + ] + }, + "mdocml-soelim": { + "versions": { + "1.14.3-r0": 1509459639 + }, + "origin": "mdocml", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:soelim" + ] + }, + "valgrind": { + "versions": { + "3.12.0-r1": 1509494841 + }, + "origin": "valgrind", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:callgrind_annotate", + "cmd:callgrind_control", + "cmd:cg_annotate", + "cmd:cg_diff", + "cmd:cg_merge", + "cmd:ms_print", + "cmd:valgrind", + "cmd:valgrind-di-server", + "cmd:valgrind-listener", + "cmd:vgdb" + ] + }, + "perl-test-needs": { + "versions": { + "0.002005-r0": 1509481592 + }, + "origin": "perl-test-needs" + }, + "xmlrpc-c-client": { + "versions": { + "1.39.11-r0": 1509482033 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libxmlrpc.so.3", + "so:libxmlrpc_util.so.3" + ], + "provides": [ + "so:libxmlrpc_client.so.3=3.39" + ] + }, + "tk-doc": { + "versions": { + "8.6.6-r1": 1509462114 + }, + "origin": "tk" + }, + "squid-lang-ms": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "py-gtk-demo": { + "versions": { + "2.24.0-r14": 1510071866 + }, + "origin": "py-gtk", + "dependencies": [ + "py-gtk" + ], + "provides": [ + "cmd:pygtk-demo" + ] + }, + "luajit": { + "versions": { + "2.1.0_beta3-r0": 1509474651 + }, + "origin": "luajit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libluajit-5.1.so.2=2.1.0", + "cmd:luajit", + "cmd:luajit-2.1.0-beta3" + ] + }, + "libnotify-doc": { + "versions": { + "0.7.7-r0": 1510068016 + }, + "origin": "libnotify" + }, + "openipmi-lanserv": { + "versions": { + "2.0.22-r1": 1510260044 + }, + "origin": "openipmi", + "dependencies": [ + "so:libOpenIPMIposix.so.0", + "so:libOpenIPMIutils.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpopt.so.0" + ], + "provides": [ + "so:libIPMIlanserv.so.0=0.0.1", + "cmd:ipmi_sim", + "cmd:ipmilan", + "cmd:sdrcomp" + ] + }, + "shorewall6-doc": { + "versions": { + "5.1.8-r0": 1509489476 + }, + "origin": "shorewall6" + }, + "ttf-linux-libertine": { + "versions": { + "5.3.0-r0": 1509469876 + }, + "origin": "ttf-linux-libertine", + "dependencies": [ + "fontconfig", + "encodings", + "mkfontdir", + "mkfontscale" + ] + }, + "acf-clamsmtp": { + "versions": { + "0.6.0-r2": 1510075831 + }, + "origin": "acf-clamsmtp", + "dependencies": [ + "acf-core", + "clamsmtp" + ] + }, + "xf86-video-sunleo-doc": { + "versions": { + "1.2.2-r0": 1510074826 + }, + "origin": "xf86-video-sunleo" + }, + "vanessa_socket-dev": { + "versions": { + "0.0.13-r0": 1511165896 + }, + "origin": "vanessa_socket", + "dependencies": [ + "popt-dev", + "vanessa_logger-dev", + "pkgconfig", + "vanessa_socket=0.0.13-r0" + ], + "provides": [ + "pc:vanessa-socket=0.0.13" + ] + }, + "musl-dev": { + "versions": { + "1.1.18-r3": 1518031143, + "1.1.18-r4": 1565163296 + }, + "origin": "musl", + "dependencies": [ + "!uclibc-dev", + "musl=1.1.18-r4" + ] + }, + "postgresql": { + "versions": { + "10.7-r0": 1554274200, + "10.8-r0": 1557668017, + "10.9-r0": 1562225615, + "10.10-r0": 1565683440 + }, + "origin": "postgresql", + "dependencies": [ + "postgresql-client", + "tzdata", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libldap-2.4.so.2", + "so:libpq.so.5", + "so:libssl.so.44", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:initdb", + "cmd:pg_archivecleanup", + "cmd:pg_controldata", + "cmd:pg_ctl", + "cmd:pg_resetwal", + "cmd:pg_rewind", + "cmd:pg_test_fsync", + "cmd:pg_test_timing", + "cmd:pg_upgrade", + "cmd:pg_waldump", + "cmd:pgbench", + "cmd:postgres", + "cmd:postmaster" + ] + }, + "xautolock-doc": { + "versions": { + "2.2-r3": 1510072540 + }, + "origin": "xautolock" + }, + "libxt": { + "versions": { + "1.1.5-r1": 1509466278 + }, + "origin": "libxt", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXt.so.6=6.0.0" + ] + }, + "xf86-input-evdev": { + "versions": { + "2.10.5-r0": 1510074957 + }, + "origin": "xf86-input-evdev", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libevdev.so.2", + "so:libmtdev.so.1", + "so:libudev.so.1" + ] + }, + "linux-pam-dev": { + "versions": { + "1.3.0-r0": 1509473196 + }, + "origin": "linux-pam", + "dependencies": [ + "gettext-dev", + "linux-pam=1.3.0-r0" + ] + }, + "xf86-video-i740-doc": { + "versions": { + "1.3.6-r0": 1510074897 + }, + "origin": "xf86-video-i740" + }, + "vlan": { + "versions": { + "2.2-r0": 1529583317 + }, + "origin": "vlan" + }, + "py-unidecode": { + "versions": { + "0.04.21-r0": 1509493258 + }, + "origin": "py-unidecode", + "dependencies": [ + "py3-unidecode", + "py3-unidecode=0.04.21-r0" + ] + }, + "nrpe-plugin": { + "versions": { + "2.15-r7": 1510259028 + }, + "origin": "nrpe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libssl.so.44" + ] + }, + "py2-lxml": { + "versions": { + "4.1.1-r0": 1510088208 + }, + "origin": "py-lxml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libpython2.7.so.1.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ] + }, + "gst-plugins-bad0.10": { + "versions": { + "0.10.23-r7": 1510287376 + }, + "origin": "gst-plugins-bad0.10", + "dependencies": [ + "so:libSDL-1.2.so.0", + "so:libX11.so.6", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcrypto.so.42", + "so:libdc1394.so.22", + "so:libfaac.so.0", + "so:libfaad.so.2", + "so:libflite.so.1", + "so:libflite_cmu_us_kal.so.1", + "so:libflite_cmulex.so.1", + "so:libflite_usenglish.so.1", + "so:libgcc_s.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsm.so.1", + "so:libgstapp-0.10.so.0", + "so:libgstaudio-0.10.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstcontroller-0.10.so.0", + "so:libgstfft-0.10.so.0", + "so:libgstinterfaces-0.10.so.0", + "so:libgstpbutils-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgstrtp-0.10.so.0", + "so:libgstsdp-0.10.so.0", + "so:libgsttag-0.10.so.0", + "so:libgstvideo-0.10.so.0", + "so:libintl.so.8", + "so:libjasper.so.4", + "so:libmms.so.0", + "so:liborc-0.4.so.0", + "so:libpng16.so.16", + "so:librsvg-2.so.2", + "so:libstdc++.so.6", + "so:libvpx.so.4", + "so:libxvidcore.so.4" + ], + "provides": [ + "so:libgstbasecamerabinsrc-0.10.so.23=23.0.0", + "so:libgstbasevideo-0.10.so.23=23.0.0", + "so:libgstcodecparsers-0.10.so.23=23.0.0", + "so:libgstphotography-0.10.so.23=23.0.0", + "so:libgstsignalprocessor-0.10.so.23=23.0.0" + ] + }, + "sed": { + "versions": { + "4.4-r1": 1509462308 + }, + "origin": "sed", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sed" + ] + }, + "lua5.1-lub": { + "versions": { + "1.1.0-r1": 1509475703 + }, + "origin": "lua-lub", + "dependencies": [ + "lua5.1", + "lua5.1-filesystem" + ] + }, + "pangomm-dev": { + "versions": { + "2.40.1-r0": 1509473003 + }, + "origin": "pangomm", + "dependencies": [ + "pango-dev", + "glibmm-dev", + "cairomm-dev", + "pangomm=2.40.1-r0", + "pc:cairomm-1.0>=1.2.2", + "pc:glibmm-2.4>=2.48.0", + "pc:pangocairo>=1.38.0", + "pkgconfig" + ], + "provides": [ + "pc:pangomm-1.4=2.40.1" + ] + }, + "libdvbcsa": { + "versions": { + "1.1.0-r0": 1509488846 + }, + "origin": "libdvbcsa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdvbcsa.so.1=1.0.1" + ] + }, + "json-glib": { + "versions": { + "1.2.8-r0": 1509480681 + }, + "origin": "json-glib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libjson-glib-1.0.so.0=0.200.8" + ] + }, + "py2-bluez": { + "versions": { + "0.22-r1": 1510075575 + }, + "origin": "py-bluez", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "lua-aports": { + "versions": { + "0.7.0-r0": 1509462496 + }, + "origin": "lua-aports", + "dependencies": [ + "lua5.2-cjson", + "lua5.2-filesystem", + "lua5.2-optarg", + "lua5.2" + ], + "provides": [ + "cmd:ap", + "cmd:buildrepo" + ] + }, + "recode-doc": { + "versions": { + "3.6-r1": 1509485761 + }, + "origin": "recode" + }, + "logcheck": { + "versions": { + "1.3.18-r0": 1509468631 + }, + "origin": "logcheck", + "dependencies": [ + "lockfile-progs", + "/bin/sh" + ], + "provides": [ + "cmd:logcheck", + "cmd:logcheck-test", + "cmd:logtail", + "cmd:logtail2" + ] + }, + "libsigc++-dev": { + "versions": { + "2.10.0-r1": 1509472940 + }, + "origin": "libsigc++", + "dependencies": [ + "libsigc++=2.10.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:sigc++-2.0=2.10.0" + ] + }, + "perl-term-table": { + "versions": { + "0.012-r0": 1509631261 + }, + "origin": "perl-term-table", + "dependencies": [ + "perl-importer" + ] + }, + "uwsgi-router_expires": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libnl3-dev": { + "versions": { + "3.2.28-r1": 1509475886 + }, + "origin": "libnl3", + "dependencies": [ + "libnl3-cli=3.2.28-r1", + "libnl3=3.2.28-r1", + "pkgconfig" + ], + "provides": [ + "pc:libnl-3.0=3.2.28", + "pc:libnl-cli-3.0=3.2.28", + "pc:libnl-genl-3.0=3.2.28", + "pc:libnl-idiag-3.0=3.2.28", + "pc:libnl-nf-3.0=3.2.28", + "pc:libnl-route-3.0=3.2.28", + "pc:libnl-xfrm-3.0=3.2.28" + ] + }, + "flex-libs": { + "versions": { + "2.6.4-r1": 1509456704 + }, + "origin": "flex", + "dependencies": [ + "m4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfl.so.2=2.0.0" + ] + }, + "py-pgen": { + "versions": { + "2.7.10-r0": 1509484067 + }, + "origin": "py-pgen", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pgen" + ] + }, + "partimage-doc": { + "versions": { + "0.6.9-r4": 1510260489 + }, + "origin": "partimage" + }, + "libunique-dev": { + "versions": { + "1.1.6-r6": 1510069913 + }, + "origin": "libunique", + "dependencies": [ + "libunique=1.1.6-r6", + "pc:gtk+-2.0", + "pkgconfig" + ], + "provides": [ + "pc:unique-1.0=1.1.6" + ] + }, + "qextserialport": { + "versions": { + "1.2_rc1-r0": 1510075401 + }, + "origin": "qextserialport", + "dependencies": [ + "so:libQtCore.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libqextserialport.so.1=1.2.0" + ] + }, + "lua5.1-evdev": { + "versions": { + "2.2.1-r1": 1509488870 + }, + "origin": "lua-evdev", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "fprobe": { + "versions": { + "1.1-r7": 1509481826 + }, + "origin": "fprobe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:fprobe" + ] + }, + "darkhttpd-openrc": { + "versions": { + "1.12-r2": 1526305370 + }, + "origin": "darkhttpd" + }, + "st-dbg": { + "versions": { + "0.7-r1": 1509494024 + }, + "origin": "st", + "dependencies": [ + "ncurses-terminfo" + ] + }, + "rtapd": { + "versions": { + "1.7-r6": 1509491640 + }, + "origin": "rtapd", + "dependencies": [ + "rtnppd", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "cmd:rtapd", + "cmd:vsnppd" + ] + }, + "libbluray-dev": { + "versions": { + "1.0.0-r0": 1509488856 + }, + "origin": "libbluray", + "dependencies": [ + "libbluray=1.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libbluray=1.0.0" + ] + }, + "perl-xml-parser": { + "versions": { + "2.44-r4": 1509464446 + }, + "origin": "perl-xml-parser", + "dependencies": [ + "perl-libwww", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "perl-net-telnet-doc": { + "versions": { + "3.04-r0": 1509477768 + }, + "origin": "perl-net-telnet" + }, + "perl-exporter-tiny": { + "versions": { + "1.000000-r0": 1509468499 + }, + "origin": "perl-exporter-tiny" + }, + "lua5.1-feedparser": { + "versions": { + "0.71-r0": 1509485576 + }, + "origin": "lua-feedparser", + "dependencies": [ + "lua5.1-expat", + "lua-feedparser-common", + "lua-feedparser-common=0.71-r0" + ] + }, + "su-exec": { + "versions": { + "0.2-r0": 1509483490 + }, + "origin": "su-exec", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:su-exec" + ] + }, + "sshpass": { + "versions": { + "1.06-r0": 1509494949 + }, + "origin": "sshpass", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sshpass" + ] + }, + "xf86-video-qxl": { + "versions": { + "0.1.5-r3": 1510073032 + }, + "origin": "xf86-video-qxl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "ristretto": { + "versions": { + "0.8.2-r0": 1510072650 + }, + "origin": "ristretto", + "dependencies": [ + "desktop-file-utils", + "hicolor-icon-theme", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdbus-glib-1.so.2", + "so:libexif.so.12", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:ristretto" + ] + }, + "cppunit-dev": { + "versions": { + "1.14.0-r0": 1509481255 + }, + "origin": "cppunit", + "dependencies": [ + "cppunit=1.14.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:cppunit=1.14.0" + ] + }, + "py-urwid": { + "versions": { + "1.3.1-r2": 1509493957 + }, + "origin": "py-urwid" + }, + "dpkg-doc": { + "versions": { + "1.18.24-r0": 1509494230 + }, + "origin": "dpkg" + }, + "icu-dev": { + "versions": { + "59.1-r1": 1509464843 + }, + "origin": "icu", + "dependencies": [ + "icu-libs=59.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:icu-i18n=59.1", + "pc:icu-io=59.1", + "pc:icu-uc=59.1", + "cmd:icu-config" + ] + }, + "gigolo": { + "versions": { + "0.4.2-r0": 1510076414 + }, + "origin": "gigolo", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:gigolo" + ] + }, + "py-sphinxcontrib-websupport": { + "versions": { + "1.0.1-r3": 1509493320 + }, + "origin": "py-sphinxcontrib-websupport" + }, + "perl-universal-require-doc": { + "versions": { + "0.13-r0": 1509469904 + }, + "origin": "perl-universal-require" + }, + "nginx-mod-http-fancyindex": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "boost-unit_test_framework": { + "versions": { + "1.62.0-r5": 1509465881 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_unit_test_framework-mt.so.1.62.0=1.62.0", + "so:libboost_unit_test_framework.so.1.62.0=1.62.0" + ] + }, + "scstadmin": { + "versions": { + "2.2.0-r2": 1509492547 + }, + "origin": "scstadmin", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:scstadmin" + ] + }, + "libxfont2": { + "versions": { + "2.0.3-r0": 1511975683 + }, + "origin": "libxfont2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfontenc.so.1", + "so:libfreetype.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libXfont2.so.2=2.0.0" + ] + }, + "qemu-i386": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-i386" + ] + }, + "cunit-dev": { + "versions": { + "2.1.3-r1": 1509466104 + }, + "origin": "cunit", + "dependencies": [ + "cunit=2.1.3-r1", + "pkgconfig" + ] + }, + "libvorbis-doc": { + "versions": { + "1.3.6-r2": 1549615971 + }, + "origin": "libvorbis" + }, + "motif": { + "versions": { + "2.3.4-r2": 1509495442 + }, + "origin": "motif", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1" + ], + "provides": [ + "so:libMrm.so.4=4.0.4", + "so:libUil.so.4=4.0.4", + "so:libXm.so.4=4.0.4", + "cmd:uil" + ] + }, + "lua-feedparser": { + "versions": { + "0.71-r0": 1509485586 + }, + "origin": "lua-feedparser" + }, + "spl-hardened-dev": { + "versions": { + "4.9.65-r1": 1511798817 + }, + "origin": "spl-hardened", + "dependencies": [ + "linux-hardened-dev=4.9.65-r1" + ] + }, + "exo-doc": { + "versions": { + "0.11.5-r0": 1510068072 + }, + "origin": "exo" + }, + "lua-uuid": { + "versions": { + "2012.05-r1": 1509489738 + }, + "origin": "lua-uuid", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ] + }, + "libcanberra-gtk3": { + "versions": { + "0.30-r1": 1510071976 + }, + "origin": "libcanberra", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcanberra.so.0", + "so:libgdk-3.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0" + ], + "provides": [ + "so:libcanberra-gtk3.so.0=0.1.9", + "cmd:canberra-gtk-play" + ] + }, + "lua5.2-curl": { + "versions": { + "0.3.7-r1": 1509493017 + }, + "origin": "lua-curl", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "help2man-doc": { + "versions": { + "1.47.5-r0": 1509456689 + }, + "origin": "help2man" + }, + "sysklogd": { + "versions": { + "1.5.1-r1": 1511795688 + }, + "origin": "sysklogd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:klogd", + "cmd:syslogd" + ] + }, + "perl-test-nowarnings": { + "versions": { + "1.04-r1": 1509468472 + }, + "origin": "perl-test-nowarnings", + "dependencies": [ + "perl", + "perl-test-simple" + ] + }, + "perl-file-copy-recursive": { + "versions": { + "0.38-r1": 1510855671 + }, + "origin": "perl-file-copy-recursive" + }, + "at-spi2-atk-dev": { + "versions": { + "2.26.1-r0": 1509466060 + }, + "origin": "at-spi2-atk", + "dependencies": [ + "glib-dev", + "dbus-dev", + "atk-dev", + "at-spi2-core-dev", + "at-spi2-atk=2.26.1-r0", + "pc:atspi-2", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:atk-bridge-2.0=2.26.1" + ] + }, + "perl-io-async": { + "versions": { + "0.69-r0": 1509480542 + }, + "origin": "perl-io-async", + "dependencies": [ + "perl-struct-dumb", + "perl-future" + ] + }, + "xf86-video-xgixp": { + "versions": { + "1.8.1-r8": 1510069923 + }, + "origin": "xf86-video-xgixp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libcdio-doc": { + "versions": { + "0.94-r0": 1509473626 + }, + "origin": "libcdio" + }, + "check-doc": { + "versions": { + "0.12.0-r1": 1509461855 + }, + "origin": "check" + }, + "squid-lang-da": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "freeswitch-flite": { + "versions": { + "1.6.19-r0": 1510585357 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libflite.so.1", + "so:libflite_cmu_us_awb.so.1", + "so:libflite_cmu_us_kal.so.1", + "so:libflite_cmu_us_kal16.so.1", + "so:libflite_cmu_us_rms.so.1", + "so:libflite_cmu_us_slt.so.1", + "so:libflite_usenglish.so.1", + "so:libfreeswitch.so.1" + ] + }, + "bacula-libs": { + "versions": { + "9.0.5-r3": 1512992127 + }, + "origin": "bacula", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:bpipe-fd.so=0", + "so:libbac-9.0.5.so=0", + "so:libbaccfg-9.0.5.so=0", + "so:libbacfind-9.0.5.so=0", + "so:libbacsd-9.0.5.so=0", + "so:libbacsql-9.0.5.so=0" + ] + }, + "unrar-doc": { + "versions": { + "5.5.8-r0": 1509468420 + }, + "origin": "unrar" + }, + "py3-pygments": { + "versions": { + "2.2.0-r0": 1509493370 + }, + "origin": "py-pygments", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:pygmentize-3" + ] + }, + "libdvbpsi-dev": { + "versions": { + "1.3.1-r0": 1511168477 + }, + "origin": "libdvbpsi", + "dependencies": [ + "libdvbpsi=1.3.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libdvbpsi=1.3.1" + ] + }, + "alpine-mirrors": { + "versions": { + "3.5.6-r0": 1511354854 + }, + "origin": "alpine-mirrors" + }, + "perl-crypt-rijndael-doc": { + "versions": { + "1.11-r4": 1509471903 + }, + "origin": "perl-crypt-rijndael" + }, + "wxgtk2.8-dev": { + "versions": { + "2.8.12.1-r4": 1510928414 + }, + "origin": "wxgtk2.8", + "dependencies": [ + "gtk+2.0-dev", + "zlib-dev", + "tiff-dev", + "libpng-dev", + "libjpeg-turbo-dev", + "expat-dev", + "libsm-dev", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libwx_baseu-2.8.so.0", + "so:libwx_baseu_xml-2.8.so.0", + "wxgtk2.8-base=2.8.12.1-r4", + "wxgtk2.8-media=2.8.12.1-r4", + "wxgtk2.8=2.8.12.1-r4" + ], + "provides": [ + "cmd:wxrc", + "cmd:wxrc-2.8" + ] + }, + "cyrus-sasl-ntlm": { + "versions": { + "2.1.26-r11": 1510258044 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "py3-unidecode": { + "versions": { + "0.04.21-r0": 1509493257 + }, + "origin": "py-unidecode", + "dependencies": [ + "py3-unidecode", + "python3" + ], + "provides": [ + "cmd:unidecode-3" + ] + }, + "abiword-plugin-mswrite": { + "versions": { + "3.0.2-r1": 1510073364 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "gvfs-smb": { + "versions": { + "1.34.1-r0": 1511430258, + "1.34.1-r1": 1563787224 + }, + "origin": "gvfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgvfscommon.so", + "so:libgvfsdaemon.so", + "so:libintl.so.8", + "so:libsmbclient.so.0" + ] + }, + "avahi-libs": { + "versions": { + "0.6.32-r4": 1509465448, + "0.6.32-r5": 1563345619 + }, + "origin": "avahi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libintl.so.8" + ], + "provides": [ + "so:libavahi-client.so.3=3.2.9", + "so:libavahi-common.so.3=3.5.3" + ] + }, + "libiptcdata": { + "versions": { + "1.0.4-r2": 1509496044 + }, + "origin": "libiptcdata", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libiptcdata.so.0=0.3.3", + "cmd:iptc" + ] + }, + "p7zip-doc": { + "versions": { + "16.02-r3": 1533742283 + }, + "origin": "p7zip" + }, + "trac": { + "versions": { + "1.2.2-r0": 1509491441 + }, + "origin": "trac", + "dependencies": [ + "python2", + "py-setuptools", + "py-genshi", + "/bin/sh" + ], + "provides": [ + "cmd:trac-admin", + "cmd:tracd" + ] + }, + "glew": { + "versions": { + "2.1.0-r0": 1510068629 + }, + "origin": "glew", + "dependencies": [ + "so:libGL.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libGLEW.so.2.1=2.1.0" + ] + }, + "collectd-libs": { + "versions": { + "5.7.2-r0": 1510069642 + }, + "origin": "collectd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20" + ], + "provides": [ + "so:libcollectdclient.so.1=1.0.0" + ] + }, + "acpi": { + "versions": { + "1.7-r2": 1509787633 + }, + "origin": "acpi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:acpi" + ] + }, + "py-templayer-doc": { + "versions": { + "1.5.1-r3": 1509489478 + }, + "origin": "py-templayer", + "dependencies": [ + "python2" + ] + }, + "perl-type-tiny": { + "versions": { + "1.000006-r0": 1509468523 + }, + "origin": "perl-type-tiny", + "dependencies": [ + "perl-exporter-tiny" + ] + }, + "rtpproxy-debug": { + "versions": { + "2.0.0-r4": 1509492567 + }, + "origin": "rtpproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rtpproxy_debug" + ] + }, + "perl-mime-base64": { + "versions": { + "3.15-r4": 1509491805 + }, + "origin": "perl-mime-base64", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "clamav-scanner": { + "versions": { + "0.100.3-r0": 1555508238 + }, + "origin": "clamav", + "dependencies": [ + "freshclam", + "so:libc.musl-x86_64.so.1", + "so:libclamav.so.7", + "so:libcrypto.so.42", + "so:libz.so.1" + ], + "provides": [ + "cmd:clambc", + "cmd:clamscan", + "cmd:sigtool" + ] + }, + "xfce4-notes-plugin-lang": { + "versions": { + "1.8.1-r0": 1510074768 + }, + "origin": "xfce4-notes-plugin" + }, + "mosh-doc": { + "versions": { + "1.3.2-r3": 1510846214 + }, + "origin": "mosh" + }, + "ntop-doc": { + "versions": { + "5.0.1-r11": 1510314558 + }, + "origin": "ntop" + }, + "quvi-doc": { + "versions": { + "0.9.5-r4": 1509495084 + }, + "origin": "quvi" + }, + "xrdp-dev": { + "versions": { + "0.9.2-r3": 1510260212 + }, + "origin": "xrdp", + "dependencies": [ + "pkgconfig", + "xrdp=0.9.2-r3" + ], + "provides": [ + "pc:libpainter=0.1.1", + "pc:rfxcodec=0.1.1", + "pc:xrdp=0.9.2" + ] + }, + "a52dec-doc": { + "versions": { + "0.7.4-r6": 1510072175 + }, + "origin": "a52dec" + }, + "py2-curl": { + "versions": { + "7.43.0-r4": 1510288123 + }, + "origin": "py-curl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libpython2.7.so.1.0" + ] + }, + "libiec61883-doc": { + "versions": { + "1.2.0-r1": 1509470082 + }, + "origin": "libiec61883" + }, + "wv": { + "versions": { + "1.2.9-r3": 1509483983 + }, + "origin": "wv", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libwv-1.2.so.4=4.0.5", + "cmd:wvAbw", + "cmd:wvCleanLatex", + "cmd:wvConvert", + "cmd:wvDVI", + "cmd:wvDocBook", + "cmd:wvHtml", + "cmd:wvLatex", + "cmd:wvMime", + "cmd:wvPDF", + "cmd:wvPS", + "cmd:wvRTF", + "cmd:wvSummary", + "cmd:wvText", + "cmd:wvVersion", + "cmd:wvWare", + "cmd:wvWml" + ] + }, + "nspr-dev": { + "versions": { + "4.17-r0": 1509479384 + }, + "origin": "nspr", + "dependencies": [ + "nspr", + "pkgconfig" + ], + "provides": [ + "pc:nspr=4.17.0", + "cmd:nspr-config" + ] + }, + "py-gdbm": { + "versions": { + "2.7.15-r2": 1534944323 + }, + "origin": "python2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "python-gdbm=2.7.15-r2" + ] + }, + "lua5.2-augeas": { + "versions": { + "0.1.2-r3": 1509473122 + }, + "origin": "lua-augeas", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1" + ] + }, + "bacula-pgsql": { + "versions": { + "9.0.5-r3": 1512992127 + }, + "origin": "bacula", + "dependencies": [ + "bacula", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:libbaccats-postgresql-9.0.5.so=0" + ] + }, + "ipset-doc": { + "versions": { + "6.34-r1": 1537515181 + }, + "origin": "ipset" + }, + "libshout-doc": { + "versions": { + "2.4.1-r1": 1510068379 + }, + "origin": "libshout" + }, + "py3-openssl": { + "versions": { + "17.5.0-r0": 1548491340 + }, + "origin": "py-openssl", + "dependencies": [ + "py3-cryptography", + "py3-six", + "python3" + ] + }, + "mutt-doc": { + "versions": { + "1.10.1-r0": 1532446898 + }, + "origin": "mutt" + }, + "nss-static": { + "versions": { + "3.34.1-r0": 1511890215 + }, + "origin": "nss" + }, + "clang-analyzer": { + "versions": { + "5.0.0-r0": 1510683247 + }, + "origin": "clang", + "dependencies": [ + "clang=5.0.0-r0", + "perl", + "python2" + ], + "provides": [ + "cmd:scan-build", + "cmd:scan-view" + ] + }, + "squid-lang-fa": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-test-sharedfork-doc": { + "versions": { + "0.35-r0": 1509481778 + }, + "origin": "perl-test-sharedfork" + }, + "dhcp": { + "versions": { + "4.3.5-r0": 1509496514 + }, + "origin": "dhcp", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "cmd:dhcpd", + "cmd:omshell" + ] + }, + "perl-dbd-sqlite-dev": { + "versions": { + "1.54-r1": 1509477495 + }, + "origin": "perl-dbd-sqlite" + }, + "libxdg-basedir": { + "versions": { + "1.2.0-r0": 1509494299 + }, + "origin": "libxdg-basedir", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxdg-basedir.so.1=1.2.0" + ] + }, + "openldap-back-sql": { + "versions": { + "2.4.45-r3": 1510258133, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2", + "so:libodbc.so.2" + ] + }, + "qt-tds": { + "versions": { + "4.8.7-r8": 1510287209 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libsybdb.so.5" + ] + }, + "json-c": { + "versions": { + "0.12.1-r1": 1509466321 + }, + "origin": "json-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libjson-c.so.2=2.0.2" + ] + }, + "qemu-lang": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu" + }, + "oniguruma-dev": { + "versions": { + "6.6.1-r0": 1509470649 + }, + "origin": "oniguruma", + "dependencies": [ + "oniguruma=6.6.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:oniguruma=6.6.1", + "cmd:onig-config" + ] + }, + "djbdns-doc": { + "versions": { + "1.05-r47": 1509488768 + }, + "origin": "djbdns" + }, + "glade-doc": { + "versions": { + "3.20.1-r0": 1510073638 + }, + "origin": "glade" + }, + "atk-lang": { + "versions": { + "2.26.1-r1": 1509464672 + }, + "origin": "atk" + }, + "gtk+3.0-lang": { + "versions": { + "3.22.21-r0": 1510067779 + }, + "origin": "gtk+3.0", + "dependencies": [ + "shared-mime-info", + "gtk-update-icon-cache" + ] + }, + "jwhois-doc": { + "versions": { + "4.0-r4": 1509493813 + }, + "origin": "jwhois" + }, + "ntop": { + "versions": { + "5.0.1-r11": 1510314558 + }, + "origin": "ntop", + "dependencies": [ + "/bin/sh", + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgdbm.so.4", + "so:libpcap.so.1", + "so:libpython2.7.so.1.0", + "so:librrd_th.so.4", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:libnetflowPlugin-5.0.1.so=0", + "so:libntop-5.0.1.so=0", + "so:libntopreport-5.0.1.so=0", + "so:librrdPlugin-5.0.1.so=0", + "so:libsflowPlugin-5.0.1.so=0", + "cmd:ntop", + "cmd:ntop-update-geoip-db" + ] + }, + "gstreamer0.10-tools": { + "versions": { + "0.10.36-r2": 1509470983 + }, + "origin": "gstreamer0.10", + "dependencies": [ + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-0.10.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:gst-feedback", + "cmd:gst-feedback-0.10", + "cmd:gst-inspect", + "cmd:gst-inspect-0.10", + "cmd:gst-launch", + "cmd:gst-launch-0.10", + "cmd:gst-typefind", + "cmd:gst-typefind-0.10", + "cmd:gst-xmlinspect", + "cmd:gst-xmlinspect-0.10", + "cmd:gst-xmllaunch", + "cmd:gst-xmllaunch-0.10" + ] + }, + "libspf2-dev": { + "versions": { + "1.2.10-r2": 1509491330 + }, + "origin": "libspf2", + "dependencies": [ + "libspf2=1.2.10-r2" + ] + }, + "libsexy-dev": { + "versions": { + "0.1.11-r7": 1510072497 + }, + "origin": "libsexy", + "dependencies": [ + "libsexy=0.1.11-r7", + "pkgconfig" + ], + "provides": [ + "pc:libsexy=0.1.11" + ] + }, + "net-snmp-perl": { + "versions": { + "5.7.3-r10": 1510259617 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.30", + "so:libnetsnmpagent.so.30", + "so:libnetsnmptrapd.so.30" + ], + "provides": [ + "cmd:mib2c", + "cmd:mib2c-update", + "cmd:net-snmp-cert", + "cmd:snmp-bridge-mib", + "cmd:traptoemail" + ] + }, + "xfce4-cpufreq-plugin-lang": { + "versions": { + "1.1.3-r2": 1510074004 + }, + "origin": "xfce4-cpufreq-plugin" + }, + "qemu-sparc64": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sparc64" + ] + }, + "websocket++": { + "versions": { + "0.7.0-r0": 1509491258 + }, + "origin": "websocket++" + }, + "nss": { + "versions": { + "3.34.1-r0": 1511890216 + }, + "origin": "nss", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libnspr4.so", + "so:libplc4.so", + "so:libplds4.so", + "so:libsqlite3.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libfreebl3.so=34", + "so:libgtestutil.so.34=34", + "so:libnss3.so=34", + "so:libnssckbi.so=34", + "so:libnssdbm3.so=34", + "so:libnssutil3.so=34", + "so:libsmime3.so=34", + "so:libsoftokn3.so=34", + "so:libssl3.so=34" + ] + }, + "orage-doc": { + "versions": { + "4.12.1-r1": 1510073565 + }, + "origin": "orage" + }, + "openssh-keysign": { + "versions": { + "7.5_p1-r10": 1551712288 + }, + "origin": "openssh", + "dependencies": [ + "openssh-client", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libz.so.1" + ] + }, + "ppp-radius": { + "versions": { + "2.4.7-r5": 1509480128 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "nettle-dev": { + "versions": { + "3.3-r0": 1509465113 + }, + "origin": "nettle", + "dependencies": [ + "gmp-dev", + "nettle=3.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:hogweed=3.3", + "pc:nettle=3.3" + ] + }, + "gst-libav": { + "versions": { + "1.12.3-r0": 1510072163 + }, + "origin": "gst-libav", + "dependencies": [ + "so:libavcodec.so.57", + "so:libavfilter.so.6", + "so:libavformat.so.57", + "so:libavutil.so.55", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-1.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstpbutils-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgstvideo-1.0.so.0" + ] + }, + "liboil-dev": { + "versions": { + "0.3.17-r5": 1509471014 + }, + "origin": "liboil", + "dependencies": [ + "liboil=0.3.17-r5", + "pkgconfig" + ], + "provides": [ + "pc:liboil-0.3=0.3.17" + ] + }, + "thunar-doc": { + "versions": { + "1.6.12-r0": 1510072625 + }, + "origin": "thunar" + }, + "libgpg-error-dev": { + "versions": { + "1.27-r1": 1509459211 + }, + "origin": "libgpg-error", + "dependencies": [ + "libgpg-error=1.27-r1" + ], + "provides": [ + "cmd:gpg-error-config" + ] + }, + "halberd-doc": { + "versions": { + "0.2.4-r1": 1509483052 + }, + "origin": "halberd" + }, + "wxgtk2.8-base": { + "versions": { + "2.8.12.1-r4": 1510928424 + }, + "origin": "wxgtk2.8", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libwx_baseu-2.8.so.0=0.8.0", + "so:libwx_baseu_net-2.8.so.0=0.8.0", + "so:libwx_baseu_xml-2.8.so.0=0.8.0" + ] + }, + "phodav-lang": { + "versions": { + "2.2-r0": 1509492231 + }, + "origin": "phodav" + }, + "perl-io-multiplex": { + "versions": { + "1.16-r0": 1509479741 + }, + "origin": "perl-io-multiplex", + "dependencies": [ + "perl" + ] + }, + "asterisk-curl": { + "versions": { + "15.6.1-r0": 1537795343, + "15.6.2-r0": 1568705005 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "libatomic": { + "versions": { + "6.4.0-r5": 1509458071 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libatomic.so.1=1.2.0" + ] + }, + "perl-tree-dag_node-doc": { + "versions": { + "1.29-r0": 1509494494 + }, + "origin": "perl-tree-dag_node" + }, + "wipe": { + "versions": { + "2.3.1-r0": 1509496532 + }, + "origin": "wipe", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:wipe" + ] + }, + "at-spi2-core-dbg": { + "versions": { + "2.26.2-r0": 1509466052 + }, + "origin": "at-spi2-core" + }, + "perl-text-password-pronounceable-doc": { + "versions": { + "0.30-r1": 1510564569 + }, + "origin": "perl-text-password-pronounceable" + }, + "chrony-doc": { + "versions": { + "3.2-r2": 1518706488 + }, + "origin": "chrony" + }, + "gucharmap-lang": { + "versions": { + "3.18.2-r0": 1510074174 + }, + "origin": "gucharmap" + }, + "cairomm-dev": { + "versions": { + "1.12.2-r0": 1509472990 + }, + "origin": "cairomm", + "dependencies": [ + "libsigc++-dev", + "cairomm=1.12.2-r0", + "pc:cairo-ft", + "pc:cairo-pdf", + "pc:cairo-png", + "pc:cairo-ps", + "pc:cairo-svg", + "pc:cairo-xlib", + "pc:cairo-xlib-xrender", + "pc:cairo>=1.10.0", + "pc:sigc++-2.0>=2.5.1", + "pkgconfig" + ], + "provides": [ + "pc:cairomm-1.0=1.12.2", + "pc:cairomm-ft-1.0=1.12.2", + "pc:cairomm-pdf-1.0=1.12.2", + "pc:cairomm-png-1.0=1.12.2", + "pc:cairomm-ps-1.0=1.12.2", + "pc:cairomm-svg-1.0=1.12.2", + "pc:cairomm-xlib-1.0=1.12.2", + "pc:cairomm-xlib-xrender-1.0=1.12.2" + ] + }, + "mpeg2dec": { + "versions": { + "0.5.1-r7": 1509473557 + }, + "origin": "libmpeg2", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libc.musl-x86_64.so.1", + "so:libmpeg2.so.0", + "so:libmpeg2convert.so.0" + ], + "provides": [ + "cmd:mpeg2dec" + ] + }, + "task": { + "versions": { + "2.5.1-r0": 1509491757 + }, + "origin": "task", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:task" + ] + }, + "py-openssl": { + "versions": { + "17.5.0-r0": 1548491341 + }, + "origin": "py-openssl", + "dependencies": [ + "py-cryptography", + "py-six" + ] + }, + "tree-doc": { + "versions": { + "1.7.0-r1": 1509489739 + }, + "origin": "tree" + }, + "perl-convert-tnef": { + "versions": { + "0.18-r0": 1509470833 + }, + "origin": "perl-convert-tnef", + "dependencies": [ + "perl" + ] + }, + "lua5.1-cqueues": { + "versions": { + "20171014-r0": 1510619110 + }, + "origin": "lua-cqueues", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "sfic-doc": { + "versions": { + "0.1.7-r6": 1509492420 + }, + "origin": "sfic" + }, + "py3-wtforms": { + "versions": { + "2.1-r0": 1509476540 + }, + "origin": "py-wtforms", + "dependencies": [ + "python3" + ] + }, + "sparsehash-doc": { + "versions": { + "2.0.3-r0": 1509489125 + }, + "origin": "sparsehash" + }, + "perl-regexp-common-doc": { + "versions": { + "2016010801-r0": 1509485697 + }, + "origin": "perl-regexp-common" + }, + "perl-io-tty-doc": { + "versions": { + "1.12-r4": 1509483927 + }, + "origin": "perl-io-tty" + }, + "mp3splt-gtk-doc": { + "versions": { + "0.9.2-r2": 1510070622 + }, + "origin": "mp3splt-gtk" + }, + "gtksourceview2-doc": { + "versions": { + "2.10.5-r0": 1510067575 + }, + "origin": "gtksourceview2" + }, + "collectd-utils": { + "versions": { + "5.7.2-r0": 1510069644 + }, + "origin": "collectd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcollectdclient.so.1" + ], + "provides": [ + "cmd:collectd-nagios", + "cmd:collectd-tg", + "cmd:collectdctl" + ] + }, + "xcb-util-wm": { + "versions": { + "0.4.1-r1": 1509473928 + }, + "origin": "xcb-util-wm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-ewmh.so.2=2.0.0", + "so:libxcb-icccm.so.4=4.0.0" + ] + }, + "freeradius-unixodbc": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310604 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ], + "provides": [ + "freeradius3-unixodbc=3.0.15-r5", + "so:rlm_sql_unixodbc.so=0" + ] + }, + "gd-dev": { + "versions": { + "2.2.5-r3": 1554728246 + }, + "origin": "gd", + "dependencies": [ + "gd", + "perl", + "libgd=2.2.5-r3", + "pkgconfig" + ], + "provides": [ + "pc:gdlib=2.2.5", + "cmd:bdftogd", + "cmd:gdlib-config" + ] + }, + "perl-lwp-protocol-https": { + "versions": { + "6.06-r1": 1509495026 + }, + "origin": "perl-lwp-protocol-https", + "dependencies": [ + "perl-libwww", + "perl-io-socket-ssl", + "perl-mozilla-ca", + "perl-net-http", + "perl-net-ssleay" + ] + }, + "sessreg": { + "versions": { + "1.1.1-r0": 1509480574 + }, + "origin": "sessreg", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sessreg" + ] + }, + "quagga": { + "versions": { + "1.2.4-r0": 1519133995 + }, + "origin": "quagga", + "dependencies": [ + "iproute2", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libnetsnmp.so.30", + "so:libnetsnmpagent.so.30", + "so:libreadline.so.7" + ], + "provides": [ + "quagga-nhrp=1.2.4", + "so:libfpm_pb.so.0=0.0.0", + "so:libospf.so.0=0.0.0", + "so:libospfapiclient.so.0=0.0.0", + "so:libquagga_pb.so.0=0.0.0", + "so:libzebra.so.1=1.0.0", + "cmd:bgp_btoa", + "cmd:bgpd", + "cmd:isisd", + "cmd:nhrpd", + "cmd:ospf6d", + "cmd:ospfclient", + "cmd:ospfd", + "cmd:pimd", + "cmd:ripd", + "cmd:ripngd", + "cmd:test_igmpv3_join", + "cmd:vtysh", + "cmd:watchquagga", + "cmd:zebra" + ] + }, + "libxp": { + "versions": { + "1.0.3-r0": 1509494902 + }, + "origin": "libxp", + "dependencies": [ + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXp.so.6=6.2.0" + ] + }, + "spice-gtk-dev": { + "versions": { + "0.34-r1": 1510260978 + }, + "origin": "spice-gtk", + "dependencies": [ + "gtk+3.0-dev", + "celt051-dev", + "polkit-dev", + "libxrandr-dev", + "libressl-dev", + "cyrus-sasl-dev", + "gst-plugins-base-dev", + "libjpeg-turbo-dev", + "zlib-dev", + "acl-dev", + "lz4-dev", + "pc:glib-2.0", + "pc:gtk+-3.0>=3.12", + "pc:openssl", + "pc:pixman-1>=0.17.7", + "pc:spice-protocol", + "pkgconfig", + "spice-glib=0.34-r1", + "spice-gtk=0.34-r1" + ], + "provides": [ + "pc:spice-client-glib-2.0=0.34", + "pc:spice-client-gtk-3.0=0.34", + "pc:spice-controller=0.34" + ] + }, + "weechat-aspell": { + "versions": { + "1.9.1-r1": 1509530222 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libaspell.so.15", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-file-slurp": { + "versions": { + "9999.19-r0": 1509474124 + }, + "origin": "perl-file-slurp" + }, + "xfburn": { + "versions": { + "0.5.4-r0": 1510074795 + }, + "origin": "xfburn", + "dependencies": [ + "desktop-file-utils", + "hicolor-icon-theme", + "so:libburn.so.4", + "so:libc.musl-x86_64.so.1", + "so:libexo-1.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libisofs.so.6", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7" + ], + "provides": [ + "cmd:xfburn" + ] + }, + "perl-dbi": { + "versions": { + "1.637-r0": 1509477478 + }, + "origin": "perl-dbi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dbilogstrip", + "cmd:dbiprof", + "cmd:dbiproxy" + ] + }, + "lxsession-doc": { + "versions": { + "0.5.3-r0": 1510073918 + }, + "origin": "lxsession" + }, + "gst-plugins-base": { + "versions": { + "1.12.3-r0": 1510070598 + }, + "origin": "gst-plugins-base", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcdda_interface.so.0", + "so:libcdda_paranoia.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstnet-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libintl.so.8", + "so:libogg.so.0", + "so:libopus.so.0", + "so:liborc-0.4.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libtheoradec.so.1", + "so:libtheoraenc.so.1", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libgstallocators-1.0.so.0=0.1203.0", + "so:libgstapp-1.0.so.0=0.1203.0", + "so:libgstaudio-1.0.so.0=0.1203.0", + "so:libgstfft-1.0.so.0=0.1203.0", + "so:libgstpbutils-1.0.so.0=0.1203.0", + "so:libgstriff-1.0.so.0=0.1203.0", + "so:libgstrtp-1.0.so.0=0.1203.0", + "so:libgstrtsp-1.0.so.0=0.1203.0", + "so:libgstsdp-1.0.so.0=0.1203.0", + "so:libgsttag-1.0.so.0=0.1203.0", + "so:libgstvideo-1.0.so.0=0.1203.0", + "cmd:gst-device-monitor-1.0", + "cmd:gst-discoverer-1.0", + "cmd:gst-play-1.0" + ] + }, + "xextproto-doc": { + "versions": { + "7.3.0-r2": 1509461946 + }, + "origin": "xextproto" + }, + "perl-date-format": { + "versions": { + "2.30-r0": 1510564547 + }, + "origin": "perl-date-format", + "dependencies": [ + "perl" + ] + }, + "perl-sub-name": { + "versions": { + "0.21-r1": 1509474063 + }, + "origin": "perl-sub-name", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py3-vobject": { + "versions": { + "0.9.5-r2": 1510832515 + }, + "origin": "py-vobject", + "dependencies": [ + "python3", + "py3-icu", + "py3-dateutil" + ] + }, + "krb5-server-ldap": { + "versions": { + "1.15.4-r0": 1548491443 + }, + "origin": "krb5", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libgssrpc.so.4", + "so:libkadm5srv_mit.so.11", + "so:libkdb5.so.8", + "so:libkrb5.so.3", + "so:libkrb5support.so.0", + "so:libldap-2.4.so.2" + ], + "provides": [ + "so:libkdb_ldap.so.1=1.0" + ] + }, + "zeromq-dev": { + "versions": { + "4.2.5-r0": 1549279389, + "4.2.5-r1": 1563908216 + }, + "origin": "zeromq", + "dependencies": [ + "libzmq=4.2.5-r1", + "pkgconfig" + ], + "provides": [ + "pc:libzmq=4.2.5" + ] + }, + "lua5.3-feedparser": { + "versions": { + "0.71-r0": 1509485583 + }, + "origin": "lua-feedparser", + "dependencies": [ + "lua5.3-expat", + "lua-feedparser-common", + "lua-feedparser-common=0.71-r0" + ] + }, + "lua-sec": { + "versions": { + "0.6-r3": 1510260656 + }, + "origin": "lua-sec" + }, + "sdl2_ttf-dev": { + "versions": { + "2.0.14-r1": 1510310960 + }, + "origin": "sdl2_ttf", + "dependencies": [ + "freetype-dev", + "sdl2-dev", + "pc:sdl2>=2.0.0", + "pkgconfig", + "sdl2_ttf=2.0.14-r1" + ], + "provides": [ + "pc:SDL2_ttf=2.0.14" + ] + }, + "glade3-dev": { + "versions": { + "3.8.5-r4": 1510067963 + }, + "origin": "glade3", + "dependencies": [ + "glade3=3.8.5-r4", + "pc:gtk+-2.0>=2.14.0", + "pc:libxml-2.0>=2.4.0", + "pkgconfig" + ], + "provides": [ + "pc:gladeui-1.0=3.8.5" + ] + }, + "fltk": { + "versions": { + "1.3.4-r0": 1510072239 + }, + "origin": "fltk", + "dependencies": [ + "so:libGL.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXft.so.2", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libfltk.so.1.3=1.3", + "so:libfltk_forms.so.1.3=1.3", + "so:libfltk_gl.so.1.3=1.3", + "so:libfltk_images.so.1.3=1.3" + ] + }, + "ncftp-bookmarks": { + "versions": { + "3.2.6-r1": 1509472912 + }, + "origin": "ncftp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncftp.so.3", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:ncftpbookmarks" + ] + }, + "nfdump": { + "versions": { + "1.6.15-r0": 1509494473, + "1.6.15-r1": 1574265426 + }, + "origin": "nfdump", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnfdump-1.6.15.so=0", + "cmd:nfanon", + "cmd:nfcapd", + "cmd:nfdump", + "cmd:nfexpire", + "cmd:nfreplay" + ] + }, + "libxkbfile-dev": { + "versions": { + "1.0.9-r2": 1509473691 + }, + "origin": "libxkbfile", + "dependencies": [ + "libxkbfile=1.0.9-r2", + "pc:kbproto", + "pc:x11", + "pkgconfig" + ], + "provides": [ + "pc:xkbfile=1.0.9" + ] + }, + "p11-kit-trust": { + "versions": { + "0.23.2-r2": 1509465179 + }, + "origin": "p11-kit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtasn1.so.6" + ] + }, + "uwsgi-router_metrics": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "mousepad-lang": { + "versions": { + "0.4.0-r2": 1510073843 + }, + "origin": "mousepad", + "dependencies": [ + "desktop-file-utils" + ] + }, + "glibmm-dev": { + "versions": { + "2.50.1-r0": 1509472979 + }, + "origin": "glibmm", + "dependencies": [ + "libsigc++-dev", + "glibmm=2.50.1-r0", + "pc:gio-2.0", + "pc:gobject-2.0", + "pc:sigc++-2.0", + "pkgconfig" + ], + "provides": [ + "pc:giomm-2.4=2.50.1", + "pc:glibmm-2.4=2.50.1" + ] + }, + "iasl": { + "versions": { + "20170303-r0": 1510068816 + }, + "origin": "acpica", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:iasl" + ] + }, + "py2-redis": { + "versions": { + "2.10.5-r1": 1509552806 + }, + "origin": "py-redis", + "dependencies": [ + "python2" + ] + }, + "make-doc": { + "versions": { + "4.2.1-r0": 1509458174 + }, + "origin": "make" + }, + "perl-dbix-dbschema-doc": { + "versions": { + "0.39-r0": 1509477489 + }, + "origin": "perl-dbix-dbschema" + }, + "json-glib-doc": { + "versions": { + "1.2.8-r0": 1509480681 + }, + "origin": "json-glib" + }, + "spice-gtk": { + "versions": { + "0.34-r1": 1510260978 + }, + "origin": "spice-gtk", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libepoxy.so.0", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libspice-client-glib-2.0.so.8" + ], + "provides": [ + "so:libspice-client-gtk-3.0.so.5=5.0.0" + ] + }, + "parallel": { + "versions": { + "20171122-r0": 1511894075 + }, + "origin": "parallel", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:env_parallel", + "cmd:env_parallel.ash", + "cmd:env_parallel.bash", + "cmd:env_parallel.csh", + "cmd:env_parallel.dash", + "cmd:env_parallel.fish", + "cmd:env_parallel.ksh", + "cmd:env_parallel.pdksh", + "cmd:env_parallel.sh", + "cmd:env_parallel.tcsh", + "cmd:env_parallel.zsh", + "cmd:niceload", + "cmd:parallel", + "cmd:parcat", + "cmd:sem", + "cmd:sql" + ] + }, + "uwsgi-cache": { + "versions": { + "2.0.17-r0": 1522154653 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "liblockfile-doc": { + "versions": { + "1.14-r0": 1511990622 + }, + "origin": "liblockfile" + }, + "acf-chrony": { + "versions": { + "0.8.0-r2": 1510074744 + }, + "origin": "acf-chrony", + "dependencies": [ + "acf-core", + "lua-posix", + "chrony" + ] + }, + "squid-lang-hu": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "lua-cjson": { + "versions": { + "2.1.0-r7": 1509462475 + }, + "origin": "lua-cjson" + }, + "json-glib-lang": { + "versions": { + "1.2.8-r0": 1509480681 + }, + "origin": "json-glib" + }, + "apache2-utils": { + "versions": { + "2.4.39-r0": 1554306882, + "2.4.41-r0": 1566292328 + }, + "origin": "apache2", + "dependencies": [ + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:ab", + "cmd:checkgid", + "cmd:dbmmanage", + "cmd:htcacheclean", + "cmd:htdbm", + "cmd:htdigest", + "cmd:htpasswd", + "cmd:httxt2dbm", + "cmd:logresolve", + "cmd:rotatelogs" + ] + }, + "openjade-doc": { + "versions": { + "1.3.2-r5": 1509488728 + }, + "origin": "openjade" + }, + "xf86-video-apm-doc": { + "versions": { + "1.2.5-r8": 1510070494 + }, + "origin": "xf86-video-apm" + }, + "perl-convert-binhex-doc": { + "versions": { + "1.125-r0": 1509469886 + }, + "origin": "perl-convert-binhex" + }, + "libfprint": { + "versions": { + "0.7.0-r0": 1509486009 + }, + "origin": "libfprint", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libnss3.so", + "so:libpixman-1.so.0", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libfprint.so.0=0.0.0" + ] + }, + "zfs-vanilla-dev": { + "versions": { + "4.9.161-r0": 1551780615, + "4.9.182-r0": 1560866659 + }, + "origin": "zfs-vanilla", + "dependencies": [ + "glib-dev", + "e2fsprogs-dev", + "util-linux-dev", + "libtirpc-dev", + "linux-vanilla-dev=4.9.182-r0", + "spl-vanilla-dev" + ] + }, + "sipcalc-doc": { + "versions": { + "1.1.6-r0": 1509492398 + }, + "origin": "sipcalc" + }, + "xfce4-panel": { + "versions": { + "4.12.1-r1": 1510068175 + }, + "origin": "xfce4-panel", + "dependencies": [ + "hicolor-icon-theme", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libexo-1.so.0", + "so:libgarcon-1.so.0", + "so:libgarcon-gtk2-1.so.0", + "so:libgdk-3.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libwnck-1.so.22", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "so:libxfce4panel-1.0.so.4=4.0.0", + "so:libxfce4panel-2.0.so.4=4.0.0", + "cmd:xfce4-panel", + "cmd:xfce4-popup-applicationsmenu", + "cmd:xfce4-popup-directorymenu", + "cmd:xfce4-popup-windowmenu" + ] + }, + "postfix-sqlite": { + "versions": { + "3.2.4-r1": 1510285340 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:postfix-sqlite.so=0" + ] + }, + "perl-net-telnet": { + "versions": { + "3.04-r0": 1509477772 + }, + "origin": "perl-net-telnet" + }, + "perl-module-metadata-doc": { + "versions": { + "1.000033-r0": 1509468434 + }, + "origin": "perl-module-metadata" + }, + "qemu-system-ppc": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-ppc" + ] + }, + "perl-probe-perl-doc": { + "versions": { + "0.03-r0": 1509482463 + }, + "origin": "perl-probe-perl" + }, + "lua-penlight-doc": { + "versions": { + "1.5.4-r0": 1509485590 + }, + "origin": "lua-penlight" + }, + "audacious-lang": { + "versions": { + "3.9-r0": 1510072525 + }, + "origin": "audacious" + }, + "perl-db-doc": { + "versions": { + "0.55-r1": 1509477765 + }, + "origin": "perl-db" + }, + "kamailio-json": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libjson-c.so.2", + "so:libsrutils.so.1" + ] + }, + "rsnapshot-doc": { + "versions": { + "1.4.2-r0": 1509480273 + }, + "origin": "rsnapshot" + }, + "postfix-mysql": { + "versions": { + "3.2.4-r1": 1510285340 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ], + "provides": [ + "so:postfix-mysql.so=0" + ] + }, + "nagios-plugins-disk": { + "versions": { + "2.2.1-r3": 1510288494 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-git-svn": { + "versions": { + "2.15.3-r0": 1540401292, + "2.15.4-r0": 1576015189 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0" + ] + }, + "rsyslog": { + "versions": { + "8.31.0-r0": 1512031983, + "8.31.0-r1": 1571767020 + }, + "origin": "rsyslog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libestr.so.0", + "so:libfastjson.so.4", + "so:libgcrypt.so.20", + "so:liblogging-stdlog.so.0", + "so:libnet.so.1", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:rsyslogd" + ] + }, + "libatasmart-dev": { + "versions": { + "0.19-r1": 1509482228 + }, + "origin": "libatasmart", + "dependencies": [ + "eudev-dev", + "libatasmart=0.19-r1", + "pkgconfig" + ], + "provides": [ + "pc:libatasmart=0.19" + ] + }, + "xbitmaps": { + "versions": { + "1.1.1-r2": 1509490716 + }, + "origin": "xbitmaps", + "dependencies": [ + "util-macros" + ] + }, + "sdl2_mixer-dev": { + "versions": { + "2.0.2-r0": 1511859191 + }, + "origin": "sdl2_mixer", + "dependencies": [ + "pc:sdl2>=2.0.7", + "pkgconfig", + "sdl2_mixer=2.0.2-r0" + ], + "provides": [ + "pc:SDL2_mixer=2.0.2" + ] + }, + "orc": { + "versions": { + "0.4.27-r0": 1509469955 + }, + "origin": "orc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liborc-0.4.so.0=0.27.0", + "so:liborc-test-0.4.so.0=0.27.0" + ] + }, + "qemu-sparc32plus": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sparc32plus" + ] + }, + "libnetfilter_log": { + "versions": { + "1.0.1-r2": 1509480369 + }, + "origin": "libnetfilter_log", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnfnetlink.so.0" + ], + "provides": [ + "so:libnetfilter_log.so.1=1.1.0", + "so:libnetfilter_log_libipulog.so.1=1.0.0" + ] + }, + "lua5.1-doc": { + "versions": { + "5.1.5-r3": 1509462457 + }, + "origin": "lua5.1" + }, + "libnice": { + "versions": { + "0.1.14-r2": 1510931344 + }, + "origin": "libnice", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libnice.so.10=10.7.0", + "cmd:sdp-example", + "cmd:simple-example", + "cmd:stunbdc", + "cmd:stund", + "cmd:threaded-example" + ] + }, + "libavc1394-dev": { + "versions": { + "0.5.4-r1": 1509489637 + }, + "origin": "libavc1394", + "dependencies": [ + "libraw1394-dev", + "libavc1394=0.5.4-r1", + "pc:libraw1394", + "pkgconfig" + ], + "provides": [ + "pc:libavc1394=0.5.4" + ] + }, + "re2c": { + "versions": { + "1.0.2-r0": 1509475772 + }, + "origin": "re2c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:re2c" + ] + }, + "c-ares": { + "versions": { + "1.13.0-r0": 1509461299 + }, + "origin": "c-ares", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcares.so.2=2.2.0" + ] + }, + "perl-html-quoted": { + "versions": { + "0.04-r0": 1511989791 + }, + "origin": "perl-html-quoted", + "dependencies": [ + "perl", + "perl-html-parser" + ] + }, + "vte-lang": { + "versions": { + "0.28.2-r13": 1510071902 + }, + "origin": "vte" + }, + "apache2-mod-wsgi-doc": { + "versions": { + "4.5.22-r0": 1511871607 + }, + "origin": "apache2-mod-wsgi" + }, + "xfontsel": { + "versions": { + "1.0.5-r1": 1509481103 + }, + "origin": "xfontsel", + "dependencies": [ + "so:libX11.so.6", + "so:libXaw.so.7", + "so:libXmu.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xfontsel" + ] + }, + "mosh-server": { + "versions": { + "1.3.2-r3": 1510846214 + }, + "origin": "mosh", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libprotobuf.so.14", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:mosh-server" + ] + }, + "gummiboot": { + "versions": { + "48.1-r0": 1509483314 + }, + "origin": "gummiboot", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:gummiboot" + ] + }, + "speex": { + "versions": { + "1.2.0-r0": 1509473248 + }, + "origin": "speex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libspeex.so.1=1.5.1" + ] + }, + "perl-javascript-minifier": { + "versions": { + "1.14-r0": 1509482423 + }, + "origin": "perl-javascript-minifier" + }, + "tinc-doc": { + "versions": { + "1.0.35-r0": 1549268950 + }, + "origin": "tinc" + }, + "scrot": { + "versions": { + "0.8.13-r0": 1509481275 + }, + "origin": "scrot", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgiblib.so.1" + ], + "provides": [ + "cmd:scrot" + ] + }, + "font-dec-misc": { + "versions": { + "1.0.3-r0": 1509491348 + }, + "origin": "font-dec-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "kamailio-tls": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4" + ] + }, + "atk-dev": { + "versions": { + "2.26.1-r1": 1509464672 + }, + "origin": "atk", + "dependencies": [ + "atk=2.26.1-r1", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:atk=2.26.1" + ] + }, + "atf-dev": { + "versions": { + "0.21-r1": 1509459730 + }, + "origin": "atf", + "dependencies": [ + "atf=0.21-r1", + "pkgconfig" + ], + "provides": [ + "pc:atf-c++=0.21", + "pc:atf-c=0.21", + "pc:atf-sh=0.21" + ] + }, + "lz4": { + "versions": { + "1.8.0-r1": 1509461406 + }, + "origin": "lz4", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lz4", + "cmd:lz4c", + "cmd:lz4cat", + "cmd:unlz4" + ] + }, + "freeradius-eap": { + "versions": { + "3.0.15-r4": 1556202797, + "3.0.15-r5": 1566310604 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libfreeradius-eap.so", + "so:libfreeradius-radius.so", + "so:libfreeradius-server.so", + "so:libssl.so.44", + "so:libtalloc.so.2" + ], + "provides": [ + "freeradius3-eap=3.0.15-r5", + "so:rlm_eap.so=0", + "so:rlm_eap_fast.so=0", + "so:rlm_eap_gtc.so=0", + "so:rlm_eap_leap.so=0", + "so:rlm_eap_md5.so=0", + "so:rlm_eap_mschapv2.so=0", + "so:rlm_eap_peap.so=0", + "so:rlm_eap_pwd.so=0", + "so:rlm_eap_sim.so=0", + "so:rlm_eap_tls.so=0", + "so:rlm_eap_ttls.so=0", + "cmd:radeapclient" + ] + }, + "glib-networking": { + "versions": { + "2.54.1-r0": 1509477723 + }, + "origin": "glib-networking", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libp11-kit.so.0", + "so:libproxy.so.1" + ] + }, + "py3-psycopg2": { + "versions": { + "2.7.3.2-r0": 1510619592 + }, + "origin": "py-psycopg2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5", + "so:libpython3.6m.so.1.0" + ] + }, + "pciutils-libs": { + "versions": { + "3.5.6-r0": 1511165900 + }, + "origin": "pciutils", + "dependencies": [ + "hwdata-pci", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpci.so.3=3.5.6" + ] + }, + "lua-doc": { + "versions": { + "5.1.5-r4": 1509471877 + }, + "origin": "lua", + "dependencies": [ + "lua5.2-doc" + ] + }, + "iwlwifi-6000-ucode-doc": { + "versions": { + "9.221.4.1-r0": 1509495918 + }, + "origin": "iwlwifi-6000-ucode" + }, + "font-adobe-100dpi": { + "versions": { + "1.0.3-r0": 1509470417 + }, + "origin": "font-adobe-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "libfastjson-dev": { + "versions": { + "0.99.7-r0": 1509486266 + }, + "origin": "libfastjson", + "dependencies": [ + "libfastjson=0.99.7-r0", + "pkgconfig" + ], + "provides": [ + "pc:libfastjson=0.99.7" + ] + }, + "perl-text-csv": { + "versions": { + "1.20-r1": 1509488639 + }, + "origin": "perl-text-csv", + "dependencies": [ + "perl" + ] + }, + "lua-graphviz": { + "versions": { + "2.40.1-r0": 1510066918 + }, + "origin": "graphviz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcgraph.so.6", + "so:libgvc.so.6", + "so:liblua-5.2.so.0" + ] + }, + "libxcomposite": { + "versions": { + "0.4.4-r1": 1509465984 + }, + "origin": "libxcomposite", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXcomposite.so.1=1.0.0" + ] + }, + "haproxy-doc": { + "versions": { + "1.7.9-r1": 1510261284 + }, + "origin": "haproxy" + }, + "libgc++": { + "versions": { + "7.6.0-r1": 1509467221 + }, + "origin": "gc", + "dependencies": [ + "so:libgc.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgccpp.so.1=1.0.3" + ] + }, + "avahi-compat-howl": { + "versions": { + "0.6.32-r4": 1509465448, + "0.6.32-r5": 1563345619 + }, + "origin": "avahi", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhowl.so.0=0.0.0" + ] + }, + "libgnat": { + "versions": { + "6.4.0-r5": 1509458080 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libgnarl-6.so=0", + "so:libgnat-6.so=0" + ] + }, + "xfconf": { + "versions": { + "4.12.1-r2": 1510067934 + }, + "origin": "xfconf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libxfce4util.so.7" + ], + "provides": [ + "so:libxfconf-0.so.2=2.0.0", + "cmd:xfconf-query" + ] + }, + "uwsgi-stats_pusher_socket": { + "versions": { + "2.0.17-r0": 1522154659 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "kbd-legacy": { + "versions": { + "2.0.4-r2": 1510922529 + }, + "origin": "kbd" + }, + "perl-params-util": { + "versions": { + "1.07-r5": 1509473959 + }, + "origin": "perl-params-util", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-test-without-module": { + "versions": { + "0.20-r0": 1509481285 + }, + "origin": "perl-test-without-module" + }, + "glu-dev": { + "versions": { + "9.0.0-r3": 1510067878 + }, + "origin": "glu", + "dependencies": [ + "mesa-dev", + "glu=9.0.0-r3", + "pc:gl", + "pkgconfig" + ], + "provides": [ + "pc:glu=9.0.0" + ] + }, + "the_silver_searcher-doc": { + "versions": { + "2.1.0-r2": 1510831170 + }, + "origin": "the_silver_searcher" + }, + "cdparanoia-doc": { + "versions": { + "10.2-r7": 1509471029 + }, + "origin": "cdparanoia" + }, + "abiword-plugin-hrtext": { + "versions": { + "3.0.2-r1": 1510073361 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "sfdisk": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfdisk.so.1", + "so:libncursesw.so.6", + "so:libsmartcols.so.1" + ], + "provides": [ + "cmd:sfdisk" + ] + }, + "samba-common-tools": { + "versions": { + "4.7.6-r3": 1555491786 + }, + "origin": "samba", + "dependencies": [ + "so:libCHARSET3-samba4.so", + "so:libaddns-samba4.so", + "so:libads-samba4.so", + "so:libauth-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-smb-common-samba4.so", + "so:libcli-spoolss-samba4.so", + "so:libcliauth-samba4.so", + "so:libcom_err.so.2", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libgssapi-samba4.so.2", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldap-2.4.so.2", + "so:liblibcli-lsa3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnetapi.so.0", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libprinting-migrate-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-modules-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libserver-role-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbd-shim-samba4.so", + "so:libsmbldap.so.2", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so", + "so:libtrusts-util-samba4.so", + "so:libutil-reg-samba4.so", + "so:libutil-tdb-samba4.so", + "so:libwbclient.so.0" + ], + "provides": [ + "so:libgpo-samba4.so=0", + "so:libnet-keytab-samba4.so=0", + "cmd:net", + "cmd:pdbedit", + "cmd:profiles", + "cmd:smbcontrol", + "cmd:smbpasswd", + "cmd:testparm" + ] + }, + "perl-test2-suite-doc": { + "versions": { + "0.000083-r0": 1510564541 + }, + "origin": "perl-test2-suite" + }, + "collectd-redis": { + "versions": { + "5.7.2-r0": 1510069651 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.13" + ] + }, + "opensmtpd-doc": { + "versions": { + "6.0.2p1-r7": 1510259021 + }, + "origin": "opensmtpd" + }, + "glew-dev": { + "versions": { + "2.1.0-r0": 1510068629 + }, + "origin": "glew", + "dependencies": [ + "libxmu-dev", + "libxi-dev", + "mesa-dev", + "glu-dev", + "glew=2.1.0-r0", + "pc:glu", + "pkgconfig" + ], + "provides": [ + "pc:glew=2.1.0" + ] + }, + "lxdm-lang": { + "versions": { + "0.5.3-r1": 1510070032 + }, + "origin": "lxdm", + "dependencies": [ + "bash" + ] + }, + "sems-pin_collect": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "sems-ivr" + ] + }, + "qemu-system-or1k": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-or1k" + ] + }, + "libebml-dev": { + "versions": { + "1.3.5-r0": 1509479829, + "1.3.5-r1": 1564417272 + }, + "origin": "libebml", + "dependencies": [ + "libebml=1.3.5-r1", + "pkgconfig" + ], + "provides": [ + "pc:libebml=1.3.5" + ] + }, + "libressl": { + "versions": { + "2.6.5-r0": 1529043917 + }, + "origin": "libressl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libtls.so.16" + ], + "provides": [ + "cmd:ocspcheck", + "cmd:openssl" + ] + }, + "ffmpeg-doc": { + "versions": { + "3.4-r1": 1510072147 + }, + "origin": "ffmpeg" + }, + "task-doc": { + "versions": { + "2.5.1-r0": 1509491754 + }, + "origin": "task" + }, + "truecrypt": { + "versions": { + "7.1a-r3": 1510928472 + }, + "origin": "truecrypt", + "dependencies": [ + "device-mapper", + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libwx_baseu-2.8.so.0", + "so:libwx_gtk2u_adv-2.8.so.0", + "so:libwx_gtk2u_core-2.8.so.0" + ], + "provides": [ + "cmd:truecrypt" + ] + }, + "perl-test-file-sharedir": { + "versions": { + "1.001002-r0": 1510855690 + }, + "origin": "perl-test-file-sharedir", + "dependencies": [ + "perl-class-tiny", + "perl-file-sharedir", + "perl-file-copy-recursive", + "perl-path-tiny", + "perl-scope-guard" + ] + }, + "lua5.1-md5": { + "versions": { + "1.2-r3": 1509468352 + }, + "origin": "lua-md5", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "terminus-font-doc": { + "versions": { + "4.46-r0": 1509491616 + }, + "origin": "terminus-font" + }, + "squid-lang-sv": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865670 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "fcgi++": { + "versions": { + "2.4.0-r8": 1510330998 + }, + "origin": "fcgi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfcgi.so.0", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libfcgi++.so.0=0.0.0" + ] + }, + "ngrep-dbg": { + "versions": { + "1.45-r4": 1509489071 + }, + "origin": "ngrep" + }, + "boost-wserialization": { + "versions": { + "1.62.0-r5": 1509465882 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_serialization-mt.so.1.62.0", + "so:libboost_serialization.so.1.62.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_wserialization-mt.so.1.62.0=1.62.0", + "so:libboost_wserialization.so.1.62.0=1.62.0" + ] + }, + "squid-lang-ja": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "xf86-video-openchrome-doc": { + "versions": { + "0.6.0-r1": 1510073043 + }, + "origin": "xf86-video-openchrome" + }, + "font-screen-cyrillic": { + "versions": { + "1.0.4-r0": 1509495009 + }, + "origin": "font-screen-cyrillic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "freeswitch-pgsql": { + "versions": { + "1.6.19-r0": 1510585357 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeswitch.so.1", + "so:libpq.so.5" + ] + }, + "arpwatch-ethercodes": { + "versions": { + "2.1a15-r16": 1510075585 + }, + "origin": "arpwatch" + }, + "libnetfilter_cthelper": { + "versions": { + "1.0.0-r0": 1509469239 + }, + "origin": "libnetfilter_cthelper", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0" + ], + "provides": [ + "so:libnetfilter_cthelper.so.0=0.0.0" + ] + }, + "ferm": { + "versions": { + "2.4.1-r0": 1509481745 + }, + "origin": "ferm", + "dependencies": [ + "perl", + "iptables" + ], + "provides": [ + "cmd:ferm", + "cmd:import-ferm" + ] + }, + "cvechecker-doc": { + "versions": { + "3.8-r1": 1510072447 + }, + "origin": "cvechecker" + }, + "libtheora-doc": { + "versions": { + "1.1.1-r13": 1510068272 + }, + "origin": "libtheora" + }, + "perl-http-daemon": { + "versions": { + "6.01-r1": 1509464384 + }, + "origin": "perl-http-daemon", + "dependencies": [ + "perl", + "perl-http-date", + "perl-http-message" + ] + }, + "py2-bcrypt": { + "versions": { + "3.1.4-r0": 1509474481 + }, + "origin": "py-bcrypt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "py-redis": { + "versions": { + "2.10.5-r1": 1509552807 + }, + "origin": "py-redis" + }, + "alsa-lib-dev": { + "versions": { + "1.1.4.1-r2": 1509468674 + }, + "origin": "alsa-lib", + "dependencies": [ + "alsa-lib=1.1.4.1-r2", + "pkgconfig" + ], + "provides": [ + "pc:alsa=1.1.4.1" + ] + }, + "mercurial-vim": { + "versions": { + "4.5.2-r0": 1532937170, + "4.5.2-r1": 1563792023 + }, + "origin": "mercurial" + }, + "py2-six": { + "versions": { + "1.11.0-r0": 1509465546 + }, + "origin": "py-six", + "dependencies": [ + "python2" + ] + }, + "perl-date-extract": { + "versions": { + "0.06-r1": 1510861954 + }, + "origin": "perl-date-extract", + "dependencies": [ + "perl-datetime-format-natural", + "perl-class-data-inheritable", + "perl-test-mocktime" + ] + }, + "harfbuzz": { + "versions": { + "1.6.3-r0": 1509464873 + }, + "origin": "harfbuzz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libglib-2.0.so.0", + "so:libgraphite2.so.3" + ], + "provides": [ + "so:libharfbuzz.so.0=0.10600.3" + ] + }, + "rgb-doc": { + "versions": { + "1.0.6-r0": 1509486278 + }, + "origin": "rgb" + }, + "perl-module-util-doc": { + "versions": { + "1.09-r0": 1509485616 + }, + "origin": "perl-module-util" + }, + "freeswitch-sounds-music-8000": { + "versions": { + "1.0.8-r1": 1509490954 + }, + "origin": "freeswitch-sounds-music-8000" + }, + "findutils": { + "versions": { + "4.6.0-r0": 1509481488 + }, + "origin": "findutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:find", + "cmd:locate", + "cmd:updatedb", + "cmd:xargs" + ] + }, + "setxkbmap": { + "versions": { + "1.3.1-r0": 1509496554 + }, + "origin": "setxkbmap", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libxkbfile.so.1" + ], + "provides": [ + "cmd:setxkbmap" + ] + }, + "pwgen-doc": { + "versions": { + "2.08-r0": 1509491100 + }, + "origin": "pwgen" + }, + "liboil-doc": { + "versions": { + "0.3.17-r5": 1509471015 + }, + "origin": "liboil" + }, + "cairo": { + "versions": { + "1.14.10-r0": 1509464621 + }, + "origin": "cairo", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libxcb-render.so.0", + "so:libxcb-shm.so.0", + "so:libxcb.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libcairo-script-interpreter.so.2=2.11400.10", + "so:libcairo.so.2=2.11400.10" + ] + }, + "acf-tinydns": { + "versions": { + "0.11.0-r2": 1510075935 + }, + "origin": "acf-tinydns", + "dependencies": [ + "acf-core", + "tinydns" + ] + }, + "less": { + "versions": { + "520-r0": 1509496059 + }, + "origin": "less", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:less", + "cmd:lessecho", + "cmd:lesskey" + ] + }, + "email2trac": { + "versions": { + "2.5.0-r1": 1509492118 + }, + "origin": "email2trac", + "dependencies": [ + "trac", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:delete_spam", + "cmd:email2trac", + "cmd:run_email2trac" + ] + }, + "spamassassin-compiler": { + "versions": { + "3.4.1-r8": 1509517412 + }, + "origin": "spamassassin", + "dependencies": [ + "re2c", + "gcc", + "perl-dev", + "perl-mail-spamassassin" + ], + "provides": [ + "cmd:sa-compile" + ] + }, + "alpine-conf": { + "versions": { + "3.7.0-r1": 1520242790 + }, + "origin": "alpine-conf", + "dependencies": [ + "openrc>0.13", + "busybox>=1.26.1-r3", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lbu", + "cmd:lbu_commit", + "cmd:lbu_exclude", + "cmd:lbu_include", + "cmd:lbu_status", + "cmd:lbu_update", + "cmd:setup-acf", + "cmd:setup-alpine", + "cmd:setup-apkcache", + "cmd:setup-apkrepos", + "cmd:setup-bootable", + "cmd:setup-disk", + "cmd:setup-dns", + "cmd:setup-gparted-desktop", + "cmd:setup-hostname", + "cmd:setup-interfaces", + "cmd:setup-keymap", + "cmd:setup-lbu", + "cmd:setup-mta", + "cmd:setup-ntp", + "cmd:setup-proxy", + "cmd:setup-sshd", + "cmd:setup-timezone", + "cmd:setup-xen-dom0", + "cmd:setup-xorg-base", + "cmd:uniso", + "cmd:update-conf", + "cmd:update-kernel" + ] + }, + "perl-dbd-odbc": { + "versions": { + "1.56-r1": 1509493770 + }, + "origin": "perl-dbd-odbc", + "dependencies": [ + "perl", + "perl-dbi", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "finch": { + "versions": { + "2.12.0-r2": 1510069756 + }, + "origin": "pidgin", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libpanelw.so.6", + "so:libpurple.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgnt.so.0=0.8.10", + "cmd:finch" + ] + }, + "xdm-doc": { + "versions": { + "1.1.11-r5": 1509480595 + }, + "origin": "xdm" + }, + "uwsgi-logfile": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "boost-prg_exec_monitor": { + "versions": { + "1.62.0-r5": 1509465876 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_prg_exec_monitor-mt.so.1.62.0=1.62.0", + "so:libboost_prg_exec_monitor.so.1.62.0=1.62.0" + ] + }, + "abiword-plugin-paint": { + "versions": { + "3.0.2-r1": 1510073366 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "libxxf86vm-doc": { + "versions": { + "1.1.4-r1": 1509466237 + }, + "origin": "libxxf86vm" + }, + "lua5.2-inspect": { + "versions": { + "3.1.0-r1": 1509491026 + }, + "origin": "lua-inspect", + "dependencies": [ + "lua5.2" + ] + }, + "perl-cgi-doc": { + "versions": { + "4.36-r0": 1509470550 + }, + "origin": "perl-cgi" + }, + "openssh-server-pam": { + "versions": { + "7.5_p1-r10": 1551712288 + }, + "origin": "openssh", + "dependencies": [ + "openssh-keygen", + "openssh-server-common", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpam.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:sshd" + ] + }, + "usbredir-server": { + "versions": { + "0.7.1-r0": 1509707469 + }, + "origin": "usbredir", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirhost.so.1" + ], + "provides": [ + "cmd:usbredirserver" + ] + }, + "perl-mozilla-ca-doc": { + "versions": { + "20160104-r0": 1509494407 + }, + "origin": "perl-mozilla-ca" + }, + "nagios-plugins-http": { + "versions": { + "2.2.1-r3": 1510288495 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "tftp-hpa-doc": { + "versions": { + "5.2-r2": 1509469060 + }, + "origin": "tftp-hpa" + }, + "mesa-dri-intel": { + "versions": { + "17.2.4-r1": 1510741948 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_intel.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "lockfile-progs": { + "versions": { + "0.1.17-r0": 1509468625 + }, + "origin": "lockfile-progs", + "dependencies": [ + "liblockfile", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lockfile-check", + "cmd:lockfile-create", + "cmd:lockfile-remove", + "cmd:lockfile-touch", + "cmd:mail-lock", + "cmd:mail-touchlock", + "cmd:mail-unlock" + ] + }, + "abi-compliance-checker": { + "versions": { + "2.2-r0": 1510075237 + }, + "origin": "abi-compliance-checker", + "dependencies": [ + "perl", + "build-base" + ], + "provides": [ + "cmd:abi-compliance-checker" + ] + }, + "pingu": { + "versions": { + "1.5-r1": 1509474488 + }, + "origin": "pingu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libev.so.4" + ], + "provides": [ + "cmd:pingu", + "cmd:pinguctl" + ] + }, + "lua5.2-b64": { + "versions": { + "0.1-r1": 1509475856 + }, + "origin": "lua5.2-b64", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "talloc": { + "versions": { + "2.1.10-r0": 1509466375 + }, + "origin": "talloc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtalloc.so.2=2.1.10" + ] + }, + "aria2-bash-completion": { + "versions": { + "1.33.1-r1": 1548941587 + }, + "origin": "aria2", + "dependencies": [ + "ca-certificates" + ] + }, + "lua5.2-ossl": { + "versions": { + "20171028-r1": 1510260842 + }, + "origin": "lua-ossl", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "uwsgi-router_basicauth": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-gv": { + "versions": { + "2.40.1-r0": 1510066918 + }, + "origin": "graphviz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcgraph.so.6", + "so:libgcc_s.so.1", + "so:libgvc.so.6", + "so:libstdc++.so.6" + ] + }, + "lua5.1-inspect": { + "versions": { + "3.1.0-r1": 1509491025 + }, + "origin": "lua-inspect", + "dependencies": [ + "lua5.1" + ] + }, + "wayland-libs-cursor": { + "versions": { + "1.14.0-r2": 1510066936 + }, + "origin": "wayland", + "dependencies": [ + "wayland-libs-client", + "wayland-libs-cursor", + "wayland-libs-server", + "so:libc.musl-x86_64.so.1", + "so:libwayland-client.so.0" + ], + "provides": [ + "so:libwayland-cursor.so.0=0.0.0" + ] + }, + "opusfile": { + "versions": { + "0.10-r0": 1512032061 + }, + "origin": "opusfile", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libogg.so.0", + "so:libopus.so.0", + "so:libssl.so.44" + ], + "provides": [ + "so:libopusfile.so.0=0.4.3", + "so:libopusurl.so.0=0.4.3" + ] + }, + "opusfile-doc": { + "versions": { + "0.10-r0": 1512032061 + }, + "origin": "opusfile" + }, + "gmp-doc": { + "versions": { + "6.1.2-r1": 1509456916 + }, + "origin": "gmp" + }, + "perl-io-tty": { + "versions": { + "1.12-r4": 1509483928 + }, + "origin": "perl-io-tty", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "git-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503663 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "libgnomekbd-lang": { + "versions": { + "3.6.0-r0": 1510933018 + }, + "origin": "libgnomekbd" + }, + "cups-filters-libs": { + "versions": { + "1.17.9-r0": 1510075884 + }, + "origin": "cups-filters", + "dependencies": [ + "poppler-utils", + "bc", + "ttf-freefont", + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libdbus-1.so.3", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libtiff.so.5" + ], + "provides": [ + "so:libcupsfilters.so.1=1.0.0", + "so:libfontembed.so.1=1.0.0" + ] + }, + "xfce4-xkb-plugin-lang": { + "versions": { + "0.7.1-r0": 1510074126 + }, + "origin": "xfce4-xkb-plugin" + }, + "libltdl": { + "versions": { + "2.4.6-r4": 1509456826 + }, + "origin": "libtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libltdl.so.7=7.3.1" + ] + }, + "dialog-doc": { + "versions": { + "1.3.20170509-r0": 1509468657 + }, + "origin": "dialog" + }, + "sdl_mixer-dev": { + "versions": { + "1.2.12-r1": 1510075315 + }, + "origin": "sdl_mixer", + "dependencies": [ + "pc:sdl>=1.2.10", + "pkgconfig", + "sdl_mixer=1.2.12-r1" + ], + "provides": [ + "pc:SDL_mixer=1.2.12" + ] + }, + "libmaa": { + "versions": { + "1.3.2-r0": 1509489138 + }, + "origin": "libmaa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmaa.so.3=3.0.0" + ] + }, + "gtk+2.0-lang": { + "versions": { + "2.24.31-r0": 1510066781 + }, + "origin": "gtk+2.0", + "dependencies": [ + "shared-mime-info", + "gtk-update-icon-cache" + ] + }, + "imake": { + "versions": { + "1.0.7-r1": 1509476556 + }, + "origin": "imake", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ccmakedep", + "cmd:cleanlinks", + "cmd:imake", + "cmd:makeg", + "cmd:mergelib", + "cmd:mkdirhier", + "cmd:mkhtmlindex", + "cmd:revpath", + "cmd:xmkmf" + ] + }, + "perl-proc-wait3-doc": { + "versions": { + "0.05-r1": 1509482406 + }, + "origin": "perl-proc-wait3" + }, + "libxxf86dga": { + "versions": { + "1.1.4-r1": 1509473787 + }, + "origin": "libxxf86dga", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXxf86dga.so.1=1.0.0" + ] + }, + "tftp-hpa": { + "versions": { + "5.2-r2": 1509469065 + }, + "origin": "tftp-hpa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:in.tftpd", + "cmd:tftp" + ] + }, + "xfce4-xkb-plugin": { + "versions": { + "0.7.1-r0": 1510074131 + }, + "origin": "xfce4-xkb-plugin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgarcon-1.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:librsvg-2.so.2", + "so:libwnck-1.so.22", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxklavier.so.16" + ] + }, + "bind-tools": { + "versions": { + "9.11.6_p1-r1": 1556872768, + "9.11.8-r0": 1561323229 + }, + "origin": "bind", + "dependencies": [ + "so:libbind9.so.161", + "so:libc.musl-x86_64.so.1", + "so:libdns.so.1106", + "so:libirs.so.161", + "so:libisc.so.1100", + "so:libisccfg.so.163", + "so:liblwres.so.161" + ], + "provides": [ + "cmd:delv", + "cmd:dig", + "cmd:dnssec-dsfromkey", + "cmd:dnssec-importkey", + "cmd:dnssec-keyfromlabel", + "cmd:dnssec-keygen", + "cmd:dnssec-revoke", + "cmd:dnssec-settime", + "cmd:dnssec-signzone", + "cmd:dnssec-verify", + "cmd:host", + "cmd:nslookup", + "cmd:nsupdate" + ] + }, + "igmpproxy": { + "versions": { + "0.1-r5": 1509489518 + }, + "origin": "igmpproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:igmpproxy" + ] + }, + "libevdev": { + "versions": { + "1.5.7-r1": 1509475753 + }, + "origin": "libevdev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libevdev.so.2=2.1.19", + "cmd:libevdev-tweak-device", + "cmd:mouse-dpi-tool", + "cmd:touchpad-edge-detector" + ] + }, + "jemalloc-dev": { + "versions": { + "5.0.1-r0": 1509482444 + }, + "origin": "jemalloc", + "dependencies": [ + "jemalloc=5.0.1-r0", + "pkgconfig" + ] + }, + "libnih-doc": { + "versions": { + "1.0.3-r4": 1509473673 + }, + "origin": "libnih" + }, + "apg-doc": { + "versions": { + "2.2.3-r3": 1509489481 + }, + "origin": "apg" + }, + "uriparser": { + "versions": { + "0.8.4-r0": 1509494964 + }, + "origin": "uriparser", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liburiparser.so.1=1.0.20", + "cmd:uriparse" + ] + }, + "lua5.1-augeas": { + "versions": { + "0.1.2-r3": 1509473119 + }, + "origin": "lua-augeas", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1" + ] + }, + "libdvbpsi": { + "versions": { + "1.3.1-r0": 1511168477 + }, + "origin": "libdvbpsi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdvbpsi.so.10=10.0.0" + ] + }, + "uwsgi-dummy": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "taskd-pki": { + "versions": { + "1.1.0-r4": 1509491727 + }, + "origin": "taskd", + "dependencies": [ + "taskd", + "gnutls-utils" + ] + }, + "ncurses-terminfo": { + "versions": { + "6.0_p20171125-r1": 1534862982 + }, + "origin": "ncurses", + "dependencies": [ + "ncurses-terminfo-base", + "ncurses-terminfo-base=6.0_p20171125-r1" + ] + }, + "netcat-openbsd": { + "versions": { + "1.130-r1": 1509481636 + }, + "origin": "netcat-openbsd", + "dependencies": [ + "so:libbsd.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:nc" + ] + }, + "krb5-dev": { + "versions": { + "1.15.4-r0": 1548491443 + }, + "origin": "krb5", + "dependencies": [ + "e2fsprogs-dev", + "krb5-libs=1.15.4-r0", + "krb5-server-ldap=1.15.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:gssrpc=1.15.4", + "pc:kadm-client=1.15.4", + "pc:kadm-server=1.15.4", + "pc:kdb=1.15.4", + "pc:krb5-gssapi=1.15.4", + "pc:krb5=1.15.4", + "pc:mit-krb5-gssapi=1.15.4", + "pc:mit-krb5=1.15.4", + "cmd:krb5-config" + ] + }, + "pangomm": { + "versions": { + "2.40.1-r0": 1509473003 + }, + "origin": "pangomm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairomm-1.0.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libglibmm-2.4.so.1", + "so:libgobject-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libpangomm-1.4.so.1=1.0.30" + ] + }, + "nagios-plugins-ssh": { + "versions": { + "2.2.1-r3": 1510288498 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "zfs-scripts": { + "versions": { + "0.7.3-r0": 1509490074 + }, + "origin": "zfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libzfs.so.2" + ] + }, + "perl-file-slurp-doc": { + "versions": { + "9999.19-r0": 1509474123 + }, + "origin": "perl-file-slurp" + }, + "pmacct": { + "versions": { + "1.6.2-r0": 1509494257 + }, + "origin": "pmacct", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjansson.so.4", + "so:libmysqlclient.so.18", + "so:libpcap.so.1", + "so:libpq.so.5", + "so:libsqlite3.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:nfacctd", + "cmd:pmacct", + "cmd:pmacctd", + "cmd:pmbgpd", + "cmd:pmbmpd", + "cmd:pmtelemetryd", + "cmd:sfacctd" + ] + }, + "qemu-gtk": { + "versions": { + "2.10.1-r3": 1519746244 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libX11.so.6", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libsnappy.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-x86_64-gtk" + ] + }, + "umurmur-doc": { + "versions": { + "0.2.17-r1": 1510846249 + }, + "origin": "umurmur" + }, + "byacc": { + "versions": { + "20170709-r0": 1510310594 + }, + "origin": "byacc", + "dependencies": [ + "!bison", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:yacc" + ] + }, + "qemu-system-mips": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-mips" + ] + }, + "libmaxminddb": { + "versions": { + "1.3.1-r0": 1511893707 + }, + "origin": "libmaxminddb", + "dependencies": [ + "curl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmaxminddb.so.0=0.0.7", + "cmd:mmdblookup" + ] + }, + "xf86driproto": { + "versions": { + "2.1.1-r3": 1509473931 + }, + "origin": "xf86driproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xf86driproto=2.1.1" + ] + }, + "qemu-ppc": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-ppc" + ] + }, + "openldap-back-sock": { + "versions": { + "2.4.45-r3": 1510258133, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "perl-test-tcp": { + "versions": { + "2.14-r0": 1509481787 + }, + "origin": "perl-test-tcp", + "dependencies": [ + "perl-test-sharedfork" + ] + }, + "ed": { + "versions": { + "1.14.2-r2": 1509465946 + }, + "origin": "ed", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ed", + "cmd:red" + ] + }, + "perl-server-starter-doc": { + "versions": { + "0.15-r1": 1510564664 + }, + "origin": "perl-server-starter" + }, + "perl-eval-closure": { + "versions": { + "0.14-r0": 1509481599 + }, + "origin": "perl-eval-closure", + "dependencies": [ + "perl-try-tiny" + ] + }, + "libmp3splt": { + "versions": { + "0.9.2-r0": 1509475939 + }, + "origin": "libmp3splt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libid3tag.so.0", + "so:libltdl.so.7", + "so:libmad.so.0", + "so:libogg.so.0", + "so:libpcre.so.1", + "so:libvorbis.so.0", + "so:libvorbisfile.so.3" + ], + "provides": [ + "so:libmp3splt.so.0=0.0.9" + ] + }, + "libhistory": { + "versions": { + "7.0.003-r0": 1509456789 + }, + "origin": "readline", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhistory.so.7=7.0" + ] + }, + "tarsnap-doc": { + "versions": { + "1.0.39-r2": 1510258955 + }, + "origin": "tarsnap" + }, + "libpaper": { + "versions": { + "1.1.24-r3": 1509465362 + }, + "origin": "libpaper", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpaper.so.1=1.1.2", + "cmd:paperconf", + "cmd:paperconfig" + ] + }, + "glproto": { + "versions": { + "1.4.17-r2": 1509466241 + }, + "origin": "glproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:glproto=1.4.17" + ] + }, + "cairomm-doc": { + "versions": { + "1.12.2-r0": 1509472990 + }, + "origin": "cairomm" + }, + "perl-cps": { + "versions": { + "0.18-r0": 1509477800 + }, + "origin": "perl-cps", + "dependencies": [ + "perl-future" + ] + }, + "ncurses-libs": { + "versions": { + "6.0_p20171125-r1": 1534862983 + }, + "origin": "ncurses", + "dependencies": [ + "ncurses-terminfo-base", + "ncurses-terminfo=6.0_p20171125-r1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "ncurses-widec-libs=6.0_p20171125-r1", + "so:libformw.so.6=6.0", + "so:libmenuw.so.6=6.0", + "so:libncursesw.so.6=6.0", + "so:libpanelw.so.6=6.0" + ] + }, + "perl-module-install": { + "versions": { + "1.18-r0": 1510352967 + }, + "origin": "perl-module-install" + }, + "openrc-dev": { + "versions": { + "0.24.1-r4": 1511887834 + }, + "origin": "openrc", + "dependencies": [ + "openrc=0.24.1-r4", + "pkgconfig" + ], + "provides": [ + "pc:einfo=0.24.1", + "pc:openrc=0.24.1" + ] + }, + "iw-doc": { + "versions": { + "4.9-r0": 1509494921 + }, + "origin": "iw" + }, + "libcurl": { + "versions": { + "7.61.1-r2": 1551780214, + "7.61.1-r3": 1568722567 + }, + "origin": "curl", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssh2.so.1", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:libcurl.so.4=4.5.0" + ] + }, + "perl-socket-getaddrinfo": { + "versions": { + "0.20-r2": 1509488747 + }, + "origin": "perl-socket-getaddrinfo", + "dependencies": [ + "perl-extutils-cchecker" + ], + "provides": [ + "cmd:getaddrinfo", + "cmd:getnameinfo" + ] + }, + "mosquitto-libs++": { + "versions": { + "1.4.15-r0": 1520176488, + "1.4.15-r2": 1563346323 + }, + "origin": "mosquitto", + "dependencies": [ + "so:libmosquitto.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libmosquittopp.so.1=1" + ] + }, + "perl-lwp-mediatypes-doc": { + "versions": { + "6.02-r1": 1509464357 + }, + "origin": "perl-lwp-mediatypes" + }, + "gst-plugins-good": { + "versions": { + "1.12.3-r0": 1510075045 + }, + "origin": "gst-plugins-good", + "dependencies": [ + "so:libFLAC.so.8", + "so:libX11.so.6", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libavc1394.so.0", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdv.so.4", + "so:libgcc_s.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstallocators-1.0.so.0", + "so:libgstaudio-1.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstfft-1.0.so.0", + "so:libgstnet-1.0.so.0", + "so:libgstpbutils-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgstriff-1.0.so.0", + "so:libgstrtp-1.0.so.0", + "so:libgstrtsp-1.0.so.0", + "so:libgstsdp-1.0.so.0", + "so:libgsttag-1.0.so.0", + "so:libgstvideo-1.0.so.0", + "so:libgudev-1.0.so.0", + "so:libiec61883.so.0", + "so:libintl.so.8", + "so:libjack.so.0", + "so:libjpeg.so.8", + "so:liborc-0.4.so.0", + "so:libpng16.so.16", + "so:libraw1394.so.11", + "so:librom1394.so.0", + "so:libshout.so.3", + "so:libsoup-2.4.so.1", + "so:libspeex.so.1", + "so:libstdc++.so.6", + "so:libtag.so.1", + "so:libv4l2.so.0", + "so:libvpx.so.4", + "so:libwavpack.so.1", + "so:libz.so.1" + ] + }, + "py-bcrypt": { + "versions": { + "3.1.4-r0": 1509474481 + }, + "origin": "py-bcrypt" + }, + "ppp-minconn": { + "versions": { + "2.4.7-r5": 1509480139 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua-pty": { + "versions": { + "1.2.1-r1": 1509496424 + }, + "origin": "lua-pty" + }, + "encodings": { + "versions": { + "1.0.4-r1": 1509469869 + }, + "origin": "encodings" + }, + "abiword-plugin-babelfish": { + "versions": { + "3.0.2-r1": 1510073356 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "liboping": { + "versions": { + "1.10.0-r0": 1509482706 + }, + "origin": "liboping", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "so:liboping.so.0=0.3.0", + "cmd:noping", + "cmd:oping" + ] + }, + "uwsgi-emperor_pg": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "perl-text-wikiformat-doc": { + "versions": { + "0.81-r0": 1509482360 + }, + "origin": "perl-text-wikiformat" + }, + "libice": { + "versions": { + "1.0.9-r2": 1509465974 + }, + "origin": "libice", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libICE.so.6=6.3.0" + ] + }, + "pstree-doc": { + "versions": { + "2.39-r0": 1509493760 + }, + "origin": "pstree" + }, + "postgresql-plpython2": { + "versions": { + "10.7-r0": 1554274199, + "10.8-r0": 1557668015, + "10.9-r0": 1562225614, + "10.10-r0": 1565683439 + }, + "origin": "postgresql", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "pinentry-gtk": { + "versions": { + "1.0.0-r0": 1510587943 + }, + "origin": "pinentry", + "dependencies": [ + "/bin/sh", + "so:libassuan.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgpg-error.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:pinentry-gtk-2" + ] + }, + "libxp-dev": { + "versions": { + "1.0.3-r0": 1509494901 + }, + "origin": "libxp", + "dependencies": [ + "libx11-dev", + "libxext-dev", + "libxau-dev", + "printproto", + "libxp=1.0.3-r0", + "pc:printproto", + "pc:x11", + "pc:xau", + "pc:xext", + "pkgconfig" + ], + "provides": [ + "pc:xp=1.0.3" + ] + }, + "orbit2-doc": { + "versions": { + "2.14.19-r3": 1510928302 + }, + "origin": "orbit2" + }, + "asterisk-cdr-mysql": { + "versions": { + "15.6.1-r0": 1537795343, + "15.6.2-r0": 1568705005 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "neon-dev": { + "versions": { + "0.30.2-r2": 1510285201 + }, + "origin": "neon", + "dependencies": [ + "expat-dev", + "libressl-dev", + "zlib-dev", + "neon=0.30.2-r2", + "pkgconfig" + ], + "provides": [ + "pc:neon=0.30.2", + "cmd:neon-config" + ] + }, + "perl-yaml-syck-doc": { + "versions": { + "1.30-r1": 1509461787 + }, + "origin": "perl-yaml-syck" + }, + "unifont-doc": { + "versions": { + "9.0.06-r0": 1509491373 + }, + "origin": "unifont" + }, + "sems-gsm": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libgsm.so.1" + ] + }, + "pcsc-lite": { + "versions": { + "1.8.22-r0": 1509474498 + }, + "origin": "pcsc-lite", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ], + "provides": [ + "cmd:pcsc-spy", + "cmd:pcscd" + ] + }, + "multipath-tools": { + "versions": { + "0.7.4-r0": 1510816244 + }, + "origin": "multipath-tools", + "dependencies": [ + "eudev", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02", + "so:libjson-c.so.2", + "so:libreadline.so.7", + "so:libudev.so.1", + "so:liburcu.so.6" + ], + "provides": [ + "so:libdmmp.so.0.1.0=0.1.0", + "so:libmpathcmd.so.0=0", + "so:libmpathpersist.so.0=0", + "so:libmultipath.so.0=0", + "cmd:kpartx", + "cmd:mpathpersist", + "cmd:multipath", + "cmd:multipathd" + ] + }, + "scons": { + "versions": { + "3.0.0-r1": 1510136398 + }, + "origin": "scons", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:scons", + "cmd:scons-3.0.0", + "cmd:scons-configure-cache", + "cmd:scons-configure-cache-3.0.0", + "cmd:scons-time", + "cmd:scons-time-3.0.0", + "cmd:sconsign", + "cmd:sconsign-3.0.0" + ] + }, + "nginx-mod-http-upstream-fair": { + "versions": { + "1.12.2-r4": 1542814448 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.3-curl": { + "versions": { + "0.3.7-r1": 1509493018 + }, + "origin": "lua-curl", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "libxfixes": { + "versions": { + "5.0.3-r1": 1509464685 + }, + "origin": "libxfixes", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXfixes.so.3=3.1.0" + ] + }, + "gtksourceview2-dev": { + "versions": { + "2.10.5-r0": 1510067575 + }, + "origin": "gtksourceview2", + "dependencies": [ + "gtk+2.0-dev", + "libxml2-dev", + "gtksourceview2=2.10.5-r0", + "pc:gtk+-2.0>=2.12.0", + "pc:libxml-2.0>=2.5.0", + "pkgconfig" + ], + "provides": [ + "pc:gtksourceview-2.0=2.10.5" + ] + }, + "libxvmc-dev": { + "versions": { + "1.0.10-r1": 1509467179 + }, + "origin": "libxvmc", + "dependencies": [ + "libxext-dev", + "libxvmc=1.0.10-r1", + "pc:videoproto", + "pc:x11", + "pc:xext", + "pc:xproto", + "pc:xv", + "pkgconfig" + ], + "provides": [ + "pc:xvmc=1.0.10" + ] + }, + "libsm-dev": { + "versions": { + "1.2.2-r1": 1509466262 + }, + "origin": "libsm", + "dependencies": [ + "libsm=1.2.2-r1", + "pc:ice", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:sm=1.2.2" + ] + }, + "lua-lub": { + "versions": { + "1.1.0-r1": 1509475705 + }, + "origin": "lua-lub" + }, + "libelf": { + "versions": { + "0.8.13-r3": 1509468229 + }, + "origin": "libelf", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libelf.so.0=0" + ] + }, + "iproute2": { + "versions": { + "4.13.0-r0": 1509481070 + }, + "origin": "iproute2", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libelf.so.0", + "so:libxtables.so.12" + ], + "provides": [ + "cmd:bridge", + "cmd:ctstat", + "cmd:genl", + "cmd:ifcfg", + "cmd:ifstat", + "cmd:ip", + "cmd:lnstat", + "cmd:nstat", + "cmd:routef", + "cmd:routel", + "cmd:rtacct", + "cmd:rtmon", + "cmd:rtpr", + "cmd:rtstat", + "cmd:ss", + "cmd:tc" + ] + }, + "dwm-doc": { + "versions": { + "6.1-r2": 1509494031 + }, + "origin": "dwm" + }, + "gtksourceview2-lang": { + "versions": { + "2.10.5-r0": 1510067576 + }, + "origin": "gtksourceview2" + }, + "libcom_err": { + "versions": { + "1.43.7-r0": 1509469274, + "1.43.7-r1": 1571322243 + }, + "origin": "e2fsprogs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcom_err.so.2=2.1" + ] + }, + "openssh-keygen": { + "versions": { + "7.5_p1-r10": 1551712287 + }, + "origin": "openssh", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ], + "provides": [ + "cmd:ssh-keygen" + ] + }, + "openbox-kde": { + "versions": { + "3.6.1-r1": 1510073886 + }, + "origin": "openbox", + "provides": [ + "cmd:openbox-kde-session" + ] + }, + "libxmu": { + "versions": { + "1.1.2-r1": 1509473711 + }, + "origin": "libxmu", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXmu.so.6=6.2.0", + "so:libXmuu.so.1=1.0.0" + ] + }, + "xf86-video-sis-doc": { + "versions": { + "0.10.9-r0": 1510075907 + }, + "origin": "xf86-video-sis" + }, + "libgnomekbd": { + "versions": { + "3.6.0-r0": 1510933020 + }, + "origin": "libgnomekbd", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libxklavier.so.16" + ], + "provides": [ + "so:libgnomekbd.so.8=8.0.0", + "so:libgnomekbdui.so.8=8.0.0", + "cmd:gkbd-keyboard-display" + ] + }, + "perl-dbd-pg": { + "versions": { + "3.7.0-r0": 1509491419 + }, + "origin": "perl-dbd-pg", + "dependencies": [ + "perl-dbi", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "samba-libnss-winbind": { + "versions": { + "4.7.6-r3": 1555491787 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libwbclient.so.0", + "so:libwinbind-client-samba4.so" + ], + "provides": [ + "so:libnss_winbind.so.2=2", + "so:libnss_wins.so.2=2" + ] + }, + "cvs-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503663 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "py-asn1": { + "versions": { + "0.4.2-r0": 1511889292 + }, + "origin": "py-asn1" + }, + "smem": { + "versions": { + "1.4-r1": 1509492708 + }, + "origin": "smem", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:smem" + ] + }, + "py2-babel": { + "versions": { + "2.5.1-r0": 1509490095 + }, + "origin": "py-babel", + "dependencies": [ + "py2-tz", + "python2" + ], + "provides": [ + "cmd:pybabel" + ] + }, + "lua5.2-discount": { + "versions": { + "1.2.10.1-r4": 1509479788 + }, + "origin": "lua-discount", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "fftw-dev": { + "versions": { + "3.3.6p2-r0": 1509468829 + }, + "origin": "fftw", + "dependencies": [ + "fftw-double-libs=3.3.6p2-r0", + "fftw-long-double-libs=3.3.6p2-r0", + "fftw-single-libs=3.3.6p2-r0", + "pkgconfig" + ] + }, + "collectd-nginx": { + "versions": { + "5.7.2-r0": 1510069645 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "py-cliapp": { + "versions": { + "1.20150829-r1": 1509552707 + }, + "origin": "py-cliapp", + "dependencies": [ + "python2" + ] + }, + "libxscrnsaver": { + "versions": { + "1.2.2-r1": 1509480774 + }, + "origin": "libxscrnsaver", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXss.so.1=1.0.0" + ] + }, + "font-util-dev": { + "versions": { + "1.3.1-r2": 1509470386 + }, + "origin": "font-util", + "dependencies": [ + "font-util", + "pkgconfig" + ], + "provides": [ + "pc:fontutil=1.3.1" + ] + }, + "subversion": { + "versions": { + "1.9.7-r0": 1510314505, + "1.9.12-r0": 1565086192 + }, + "origin": "subversion", + "dependencies": [ + "/bin/sh", + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libsasl2.so.3", + "so:libsvn_client-1.so.0", + "so:libsvn_delta-1.so.0", + "so:libsvn_diff-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_fs_fs-1.so.0", + "so:libsvn_ra-1.so.0", + "so:libsvn_ra_svn-1.so.0", + "so:libsvn_repos-1.so.0", + "so:libsvn_subr-1.so.0", + "so:libsvn_wc-1.so.0" + ], + "provides": [ + "cmd:svn", + "cmd:svnadmin", + "cmd:svnbench", + "cmd:svndumpfilter", + "cmd:svnfsfs", + "cmd:svnlook", + "cmd:svnmucc", + "cmd:svnrdump", + "cmd:svnserve", + "cmd:svnsync", + "cmd:svnversion" + ] + }, + "perl-clone-doc": { + "versions": { + "0.39-r1": 1509477481 + }, + "origin": "perl-clone" + }, + "perl-net-async-http-doc": { + "versions": { + "0.40-r0": 1509480555 + }, + "origin": "perl-net-async-http" + }, + "openpgm": { + "versions": { + "5.2.122-r1": 1509475722 + }, + "origin": "openpgm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpgm-5.2.so.0=0.0.122" + ] + }, + "sems-dsm": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "meson": { + "versions": { + "0.43.0-r0": 1509475829 + }, + "origin": "meson", + "dependencies": [ + "python3", + "ninja" + ], + "provides": [ + "cmd:meson", + "cmd:mesonconf", + "cmd:mesonintrospect", + "cmd:mesontest", + "cmd:wraptool" + ] + }, + "rdiff-backup": { + "versions": { + "1.2.8-r2": 1509483273 + }, + "origin": "rdiff-backup", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:librsync.so.2" + ], + "provides": [ + "cmd:rdiff-backup", + "cmd:rdiff-backup-statistics" + ] + }, + "fetchmailconf": { + "versions": { + "6.3.26-r12": 1510261418 + }, + "origin": "fetchmail", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:fetchmailconf" + ] + }, + "mariadb-client-libs": { + "versions": { + "10.1.38-r1": 1551187990, + "10.1.40-r0": 1560354887, + "10.1.41-r0": 1565163111 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:libmysqlclient.so.18=18.0.0" + ] + }, + "ack-doc": { + "versions": { + "2.18-r0": 1510073016 + }, + "origin": "ack" + }, + "s6-linux-init": { + "versions": { + "0.3.1.0-r0": 1509492623 + }, + "origin": "s6-linux-init", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.6" + ], + "provides": [ + "cmd:s6-halt", + "cmd:s6-linux-init-maker", + "cmd:s6-poweroff", + "cmd:s6-reboot" + ] + }, + "imagemagick-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503664 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "hdparm-doc": { + "versions": { + "9.52-r0": 1509479800 + }, + "origin": "hdparm" + }, + "itstool": { + "versions": { + "2.0.4-r3": 1511265097 + }, + "origin": "itstool", + "dependencies": [ + "py3-libxml2", + "python3" + ], + "provides": [ + "cmd:itstool" + ] + }, + "termrec-dev": { + "versions": { + "0.17-r1": 1509491604 + }, + "origin": "termrec", + "dependencies": [ + "termrec=0.17-r1" + ] + }, + "py3-bcrypt": { + "versions": { + "3.1.4-r0": 1509474481 + }, + "origin": "py-bcrypt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "perl-yaml-libyaml": { + "versions": { + "0.65-r2": 1509494322 + }, + "origin": "perl-yaml-libyaml", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "perl-yaml-xs=0.65-r2" + ] + }, + "font-util-doc": { + "versions": { + "1.3.1-r2": 1509470387 + }, + "origin": "font-util" + }, + "perl-regexp-common-net-cidr": { + "versions": { + "0.02-r1": 1510564550 + }, + "origin": "perl-regexp-common-net-cidr", + "dependencies": [ + "perl", + "perl-regexp-common" + ] + }, + "loudmouth": { + "versions": { + "1.5.3-r0": 1509490879 + }, + "origin": "loudmouth", + "dependencies": [ + "so:libasyncns.so.0", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libgobject-2.0.so.0", + "so:libidn.so.11", + "so:libintl.so.8" + ], + "provides": [ + "so:libloudmouth-1.so.0=0.1.0" + ] + }, + "ddate-doc": { + "versions": { + "0.2.2-r0": 1509492724 + }, + "origin": "ddate" + }, + "perl-test-pod": { + "versions": { + "1.51-r1": 1509461792 + }, + "origin": "perl-test-pod" + }, + "libmtp-dev": { + "versions": { + "1.1.14-r0": 1509481648 + }, + "origin": "libmtp", + "dependencies": [ + "libusb-compat-dev", + "libmtp=1.1.14-r0", + "pc:libusb-1.0", + "pkgconfig" + ], + "provides": [ + "pc:libmtp=1.1.14" + ] + }, + "perl-gd-doc": { + "versions": { + "2.67-r0": 1511805778 + }, + "origin": "perl-gd" + }, + "bitlbee-otr": { + "versions": { + "3.5.1-r1": 1510259355 + }, + "origin": "bitlbee", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libotr.so.5" + ] + }, + "thunar-dev": { + "versions": { + "1.6.12-r0": 1510072625 + }, + "origin": "thunar", + "dependencies": [ + "gtk+2.0-dev", + "glib-dev", + "exo-dev", + "pc:gio-2.0", + "pc:gtk+-2.0", + "pkgconfig", + "thunar=1.6.12-r0" + ], + "provides": [ + "pc:thunarx-2=1.6.12" + ] + }, + "xvinfo": { + "versions": { + "1.1.3-r0": 1509492435 + }, + "origin": "xvinfo", + "dependencies": [ + "so:libX11.so.6", + "so:libXv.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xvinfo" + ] + }, + "perl-cgi-fast-doc": { + "versions": { + "2.13-r0": 1511894072 + }, + "origin": "perl-cgi-fast" + }, + "gnome-doc-utils-lang": { + "versions": { + "0.20.10-r2": 1509627733 + }, + "origin": "gnome-doc-utils", + "dependencies": [ + "python2", + "docbook-xml", + "rarian", + "py2-libxml2", + "libxslt" + ] + }, + "perl-module-metadata": { + "versions": { + "1.000033-r0": 1509468436 + }, + "origin": "perl-module-metadata" + }, + "cracklib-words": { + "versions": { + "20080507-r2": 1509486317 + }, + "origin": "cracklib-words" + }, + "mlmmj-doc": { + "versions": { + "1.3.0-r0": 1509492628 + }, + "origin": "mlmmj" + }, + "krb5": { + "versions": { + "1.15.4-r0": 1548491443 + }, + "origin": "krb5", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libgssapi_krb5.so.2", + "so:libk5crypto.so.3", + "so:libkadm5clnt_mit.so.11", + "so:libkadm5srv_mit.so.11", + "so:libkdb5.so.8", + "so:libkrb5.so.3", + "so:libkrb5support.so.0", + "so:libss.so.2" + ], + "provides": [ + "cmd:gss-client", + "cmd:k5srvutil", + "cmd:kadmin", + "cmd:kdestroy", + "cmd:kinit", + "cmd:klist", + "cmd:kpasswd", + "cmd:ksu", + "cmd:kswitch", + "cmd:ktutil", + "cmd:kvno", + "cmd:sim_client", + "cmd:uuclient" + ] + }, + "xkbcomp-doc": { + "versions": { + "1.4.0-r2": 1509473697 + }, + "origin": "xkbcomp" + }, + "libfontenc": { + "versions": { + "1.1.3-r2": 1509469860 + }, + "origin": "libfontenc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libfontenc.so.1=1.0.0" + ] + }, + "texinfo": { + "versions": { + "6.5-r0": 1509456625 + }, + "origin": "texinfo", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:info", + "cmd:install-info", + "cmd:makeinfo", + "cmd:pdftexi2dvi", + "cmd:pod2texi", + "cmd:texi2any", + "cmd:texi2dvi", + "cmd:texi2pdf", + "cmd:texindex" + ] + }, + "ip6tables": { + "versions": { + "1.6.1-r1": 1509469980 + }, + "origin": "iptables", + "dependencies": [ + "iptables", + "iptables=1.6.1-r1", + "so:libc.musl-x86_64.so.1", + "so:libxtables.so.12" + ] + }, + "perl-json-maybexs": { + "versions": { + "1.003009-r0": 1510352918 + }, + "origin": "perl-json-maybexs", + "dependencies": [ + "perl-cpanel-json-xs" + ] + }, + "font-micro-misc": { + "versions": { + "1.0.3-r0": 1509494115 + }, + "origin": "font-micro-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "clutter-dev": { + "versions": { + "1.26.2-r1": 1510073008 + }, + "origin": "clutter", + "dependencies": [ + "gdk-pixbuf-dev", + "json-glib-dev", + "atk-dev", + "pango-dev", + "mesa-dev", + "libxcomposite-dev", + "libxi-dev", + "cairo-dev", + "cogl-dev", + "clutter=1.26.2-r1", + "pc:atk", + "pc:atk>=2.5.3", + "pc:cairo-gobject>=1.14.0", + "pc:cogl-1.0>=1.21.2", + "pc:cogl-pango-1.0", + "pc:cogl-path-1.0", + "pc:gio-2.0>=2.44.0", + "pc:json-glib-1.0>=0.12.0", + "pc:pangocairo>=1.30", + "pc:pangoft2", + "pc:x11", + "pc:xcomposite>=0.4", + "pc:xdamage", + "pc:xext", + "pc:xi", + "pkgconfig" + ], + "provides": [ + "pc:cally-1.0=1.26.2", + "pc:clutter-1.0=1.26.2", + "pc:clutter-cogl-1.0=1.26.2", + "pc:clutter-egl-1.0=1.26.2", + "pc:clutter-glx-1.0=1.26.2", + "pc:clutter-x11-1.0=1.26.2" + ] + }, + "xdriinfo": { + "versions": { + "1.0.5-r0": 1510074965 + }, + "origin": "xdriinfo", + "dependencies": [ + "so:libGL.so.1", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xdriinfo" + ] + }, + "py-libxml2": { + "versions": { + "2.9.8-r1": 1540398579 + }, + "origin": "libxml2" + }, + "bash-doc": { + "versions": { + "4.4.19-r1": 1518031135 + }, + "origin": "bash" + }, + "collectd-dev": { + "versions": { + "5.7.2-r0": 1510069641 + }, + "origin": "collectd", + "dependencies": [ + "collectd-libs=5.7.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcollectdclient=5.7.2" + ] + }, + "postgresql-plpython2-contrib": { + "versions": { + "10.7-r0": 1554274199, + "10.8-r0": 1557668016, + "10.9-r0": 1562225614, + "10.10-r0": 1565683439 + }, + "origin": "postgresql", + "dependencies": [ + "postgresql-plpython2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "znc-modperl": { + "versions": { + "1.7.1-r0": 1531900839, + "1.7.1-r1": 1565877330 + }, + "origin": "znc", + "dependencies": [ + "znc", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libperl.so", + "so:libstdc++.so.6" + ] + }, + "uwsgi-alarm_curl": { + "versions": { + "2.0.17-r0": 1522154653 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "pacman-lang": { + "versions": { + "5.0.2-r1": 1510588357 + }, + "origin": "pacman" + }, + "acf-postfix": { + "versions": { + "0.10.0-r2": 1510076433 + }, + "origin": "acf-postfix", + "dependencies": [ + "acf-core", + "postfix" + ] + }, + "pllua": { + "versions": { + "1.1.0-r0": 1510075359 + }, + "origin": "pllua", + "dependencies": [ + "postgresql", + "so:libc.musl-x86_64.so.1", + "so:libluajit-5.1.so.2" + ] + }, + "gperf": { + "versions": { + "3.1-r1": 1509462059 + }, + "origin": "gperf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:gperf" + ] + }, + "apk-tools-static": { + "versions": { + "2.10.1-r0": 1536582152 + }, + "origin": "apk-tools", + "provides": [ + "cmd:apk.static" + ] + }, + "joe-doc": { + "versions": { + "4.5-r0": 1509494570 + }, + "origin": "joe" + }, + "consolekit2-doc": { + "versions": { + "1.2.0-r2": 1510067530 + }, + "origin": "consolekit2" + }, + "libnetfilter_log-dev": { + "versions": { + "1.0.1-r2": 1509480367 + }, + "origin": "libnetfilter_log", + "dependencies": [ + "linux-headers", + "libnetfilter_log=1.0.1-r2", + "pc:libnfnetlink", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_log=1.0.1" + ] + }, + "libdvdcss-dev": { + "versions": { + "1.4.0-r1": 1509480288 + }, + "origin": "libdvdcss", + "dependencies": [ + "libdvdcss=1.4.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:libdvdcss=1.4.0" + ] + }, + "videoproto-doc": { + "versions": { + "2.3.3-r1": 1509467164 + }, + "origin": "videoproto" + }, + "py2-crypto": { + "versions": { + "2.6.1-r2": 1509483003 + }, + "origin": "py-crypto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libpython2.7.so.1.0" + ] + }, + "s6": { + "versions": { + "2.6.1.1-r0": 1510364940 + }, + "origin": "s6", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libexecline.so.2.3", + "so:libskarnet.so.2.6" + ], + "provides": [ + "so:libs6.so.2.6=2.6.1.1", + "cmd:s6-accessrules-cdb-from-fs", + "cmd:s6-accessrules-fs-from-cdb", + "cmd:s6-applyuidgid", + "cmd:s6-cleanfifodir", + "cmd:s6-connlimit", + "cmd:s6-envdir", + "cmd:s6-envuidgid", + "cmd:s6-fdholder-daemon", + "cmd:s6-fdholder-delete", + "cmd:s6-fdholder-deletec", + "cmd:s6-fdholder-getdump", + "cmd:s6-fdholder-getdumpc", + "cmd:s6-fdholder-list", + "cmd:s6-fdholder-listc", + "cmd:s6-fdholder-retrieve", + "cmd:s6-fdholder-retrievec", + "cmd:s6-fdholder-setdump", + "cmd:s6-fdholder-setdumpc", + "cmd:s6-fdholder-store", + "cmd:s6-fdholder-storec", + "cmd:s6-fdholder-transferdump", + "cmd:s6-fdholder-transferdumpc", + "cmd:s6-fdholderd", + "cmd:s6-fghack", + "cmd:s6-ftrig-listen", + "cmd:s6-ftrig-listen1", + "cmd:s6-ftrig-notify", + "cmd:s6-ftrig-wait", + "cmd:s6-ftrigrd", + "cmd:s6-ioconnect", + "cmd:s6-ipcclient", + "cmd:s6-ipcserver", + "cmd:s6-ipcserver-access", + "cmd:s6-ipcserver-socketbinder", + "cmd:s6-ipcserverd", + "cmd:s6-log", + "cmd:s6-mkfifodir", + "cmd:s6-notifyoncheck", + "cmd:s6-setlock", + "cmd:s6-setsid", + "cmd:s6-setuidgid", + "cmd:s6-softlimit", + "cmd:s6-sudo", + "cmd:s6-sudoc", + "cmd:s6-sudod", + "cmd:s6-supervise", + "cmd:s6-svc", + "cmd:s6-svlisten", + "cmd:s6-svlisten1", + "cmd:s6-svok", + "cmd:s6-svscan", + "cmd:s6-svscanctl", + "cmd:s6-svstat", + "cmd:s6-svwait", + "cmd:s6-tai64n", + "cmd:s6-tai64nlocal", + "cmd:s6lockd", + "cmd:ucspilogd" + ] + }, + "perl-net-ip-doc": { + "versions": { + "1.26-r0": 1509469033 + }, + "origin": "perl-net-ip" + }, + "xfce4-vala": { + "versions": { + "4.10.3-r7": 1510070641 + }, + "origin": "xfce4-vala", + "dependencies": [ + "exo-dev", + "libxfce4util-dev", + "libxfce4ui-dev", + "garcon-dev", + "xfce4-panel-dev", + "xfconf-dev", + "vala" + ] + }, + "libxi-doc": { + "versions": { + "1.7.9-r1": 1509466011 + }, + "origin": "libxi" + }, + "samba-server-libs": { + "versions": { + "4.7.6-r3": 1555491788 + }, + "origin": "samba", + "dependencies": [ + "so:libMESSAGING-SEND-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libldap-2.4.so.2", + "so:libmessages-dgm-samba4.so", + "so:libmessages-util-samba4.so", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbldap.so.2", + "so:libsmbldaphelper-samba4.so", + "so:libtalloc-report-samba4.so", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-tdb-samba4.so" + ], + "provides": [ + "so:libMESSAGING-samba4.so=0", + "so:libdcerpc-samba4.so=0", + "so:libidmap-samba4.so=0", + "so:libnon-posix-acls-samba4.so=0", + "so:libnss-info-samba4.so=0" + ] + }, + "apcupsd": { + "versions": { + "3.14.14-r0": 1509495263 + }, + "origin": "apcupsd", + "dependencies": [ + "util-linux", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "cmd:apcaccess", + "cmd:apctest", + "cmd:apcupsd", + "cmd:smtp" + ] + }, + "dovecot-sqlite": { + "versions": { + "2.2.36.3-r0": 1554102390, + "2.2.36.4-r0": 1567075098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot-sql", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libdriver_sqlite.so=0" + ] + }, + "perl-config-grammar": { + "versions": { + "1.12-r0": 1509483936 + }, + "origin": "perl-config-grammar" + }, + "faac": { + "versions": { + "1.28-r11": 1509470018 + }, + "origin": "faac", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libfaac.so.0=0.0.0", + "cmd:faac" + ] + }, + "perl-test-needs-doc": { + "versions": { + "0.002005-r0": 1509481590 + }, + "origin": "perl-test-needs" + }, + "libinput": { + "versions": { + "1.9.2-r0": 1511276853 + }, + "origin": "libinput", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libevdev.so.2", + "so:libinput.so.10", + "so:libudev.so.1" + ], + "provides": [ + "cmd:libinput", + "cmd:libinput-debug-events", + "cmd:libinput-list-devices" + ] + }, + "uwsgi-python3": { + "versions": { + "2.0.17-r0": 1522154661 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "clamav": { + "versions": { + "0.100.3-r0": 1555508238 + }, + "origin": "clamav", + "dependencies": [ + "clamav-scanner", + "clamav-daemon", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libclammspack.so.0=0.1.0" + ] + }, + "zfs-dracut": { + "versions": { + "0.7.3-r0": 1509490074 + }, + "origin": "zfs" + }, + "xtables-addons": { + "versions": { + "2.11-r1": 1509469988 + }, + "origin": "xtables-addons", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxtables.so.12" + ], + "provides": [ + "so:libxt_ACCOUNT_cl.so.0=0.0.0", + "cmd:iptaccount" + ] + }, + "lua5.3-ldbus": { + "versions": { + "20150430-r2": 1509492577 + }, + "origin": "lua-ldbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ] + }, + "glamor-egl-dev": { + "versions": { + "0.6.0-r3": 1510068308 + }, + "origin": "glamor-egl", + "dependencies": [ + "mesa-dev", + "glamor-egl=0.6.0-r3", + "pkgconfig" + ], + "provides": [ + "pc:glamor-egl=0.6.0", + "pc:glamor=0.6.0" + ] + }, + "libcdio-paranoia-dev": { + "versions": { + "0.94_p1-r0": 1509473650 + }, + "origin": "libcdio-paranoia", + "dependencies": [ + "libcdio-paranoia=0.94_p1-r0", + "pc:libcdio", + "pkgconfig" + ] + }, + "which": { + "versions": { + "2.21-r1": 1509485572 + }, + "origin": "which", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:which" + ] + }, + "perl-want": { + "versions": { + "0.29-r1": 1509477486 + }, + "origin": "perl-want", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "iso-codes-dev": { + "versions": { + "3.75-r0": 1509466021 + }, + "origin": "iso-codes", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:iso-codes=3.75" + ] + }, + "acf-freeswitch-vmail": { + "versions": { + "0.6.2-r1": 1510585378 + }, + "origin": "acf-freeswitch-vmail", + "dependencies": [ + "acf-core", + "lua-sql-sqlite3", + "lua-socket", + "freeswitch", + "/bin/sh" + ] + }, + "perl-datetime-timezone": { + "versions": { + "2.15-r0": 1511251871 + }, + "origin": "perl-datetime-timezone", + "dependencies": [ + "perl-class-singleton", + "perl-params-validationcompiler", + "perl-namespace-autoclean", + "perl-try-tiny", + "perl-module-runtime", + "perl-specio" + ] + }, + "libnjb-dev": { + "versions": { + "2.2.7-r4": 1509481678 + }, + "origin": "libnjb", + "dependencies": [ + "libusb-compat-dev", + "libnjb=2.2.7-r4", + "pkgconfig" + ], + "provides": [ + "pc:libnjb=2.2.7" + ] + }, + "nagios-plugins-radius": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libfreeradius-client.so.2" + ] + }, + "rpcbind-doc": { + "versions": { + "0.2.4-r0": 1509488291 + }, + "origin": "rpcbind" + }, + "exiftool": { + "versions": { + "10.55-r0": 1509495133 + }, + "origin": "perl-image-exiftool", + "dependencies": [ + "perl-image-exiftool" + ], + "provides": [ + "cmd:exiftool" + ] + }, + "font-cronyx-cyrillic": { + "versions": { + "1.0.3-r0": 1509496607 + }, + "origin": "font-cronyx-cyrillic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "py-ttystatus": { + "versions": { + "0.23-r1": 1509552708 + }, + "origin": "py-ttystatus", + "dependencies": [ + "python2" + ] + }, + "linux-vanilla-dev": { + "versions": { + "4.9.161-r0": 1551779586, + "4.9.182-r0": 1560866037 + }, + "origin": "linux-vanilla", + "dependencies": [ + "gmp-dev", + "bash", + "perl", + "so:libc.musl-x86_64.so.1", + "so:libelf.so.1" + ] + }, + "libvirt-dev": { + "versions": { + "3.9.0-r1": 1510088351, + "5.5.0-r0": 1562165539 + }, + "origin": "libvirt", + "dependencies": [ + "libtirpc-dev", + "libvirt-client=5.5.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libvirt-admin=5.5.0", + "pc:libvirt-lxc=5.5.0", + "pc:libvirt-qemu=5.5.0", + "pc:libvirt=5.5.0" + ] + }, + "ntfs-3g-libs": { + "versions": { + "2017.3.23-r1": 1509489438 + }, + "origin": "ntfs-3g", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libntfs-3g.so.88=88.0.0" + ] + }, + "squid-lang-id": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "flashcache-utils": { + "versions": { + "3.1.3-r0": 1509495925 + }, + "origin": "flashcache-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:flashcache_create", + "cmd:flashcache_destroy", + "cmd:flashcache_load", + "cmd:flashcache_setioctl", + "cmd:get_agsize" + ] + }, + "perl-net-cidr-lite": { + "versions": { + "0.21-r2": 1509496072 + }, + "origin": "perl-net-cidr-lite", + "dependencies": [ + "perl" + ] + }, + "lxsession-lang": { + "versions": { + "0.5.3-r0": 1510073919 + }, + "origin": "lxsession" + }, + "ncurses-terminfo-base": { + "versions": { + "6.0_p20171125-r1": 1534862982 + }, + "origin": "ncurses" + }, + "xf86-video-intel": { + "versions": { + "2.99.917_git20170325-r0": 1510076035 + }, + "origin": "xf86-video-intel", + "dependencies": [ + "mesa-dri-intel", + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXv.so.1", + "so:libXvMC.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_intel.so.1", + "so:libpciaccess.so.0", + "so:libpixman-1.so.0", + "so:libudev.so.1", + "so:libxcb-dri2.so.0", + "so:libxcb-util.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libI810XvMC.so.1=1.0.0", + "so:libIntelXvMC.so.1=1.0.0" + ] + }, + "gummiboot-doc": { + "versions": { + "48.1-r0": 1509483313 + }, + "origin": "gummiboot" + }, + "uwsgi-rrdtool": { + "versions": { + "2.0.17-r0": 1522154658 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "rarian-dev": { + "versions": { + "0.8.1-r6": 1509465523 + }, + "origin": "rarian", + "dependencies": [ + "pkgconfig", + "rarian=0.8.1-r6" + ], + "provides": [ + "pc:rarian=0.8.1", + "cmd:rarian-sk-config", + "cmd:scrollkeeper-config" + ] + }, + "net-snmp-tools": { + "versions": { + "5.7.3-r10": 1510259617 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libnetsnmp.so.30", + "so:libnetsnmpagent.so.30" + ], + "provides": [ + "cmd:agentxtrap", + "cmd:encode_keychange", + "cmd:net-snmp-create-v3-user", + "cmd:snmpbulkget", + "cmd:snmpbulkwalk", + "cmd:snmpconf", + "cmd:snmpdelta", + "cmd:snmpdf", + "cmd:snmpget", + "cmd:snmpgetnext", + "cmd:snmpinform", + "cmd:snmpnetstat", + "cmd:snmpset", + "cmd:snmpstatus", + "cmd:snmptable", + "cmd:snmptest", + "cmd:snmptranslate", + "cmd:snmptrap", + "cmd:snmpusm", + "cmd:snmpvacm", + "cmd:snmpwalk" + ] + }, + "py-nose": { + "versions": { + "1.3.7-r2": 1509552700 + }, + "origin": "py-nose" + }, + "icon-naming-utils": { + "versions": { + "0.8.90-r2": 1509477317 + }, + "origin": "icon-naming-utils", + "dependencies": [ + "perl-xml-simple", + "pkgconfig" + ], + "provides": [ + "pc:icon-naming-utils=0.8.90" + ] + }, + "xfsprogs": { + "versions": { + "4.14.0-r1": 1528279973 + }, + "origin": "xfsprogs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:fsck.xfs", + "cmd:mkfs.xfs", + "cmd:xfs_repair" + ] + }, + "mailx": { + "versions": { + "8.1.1-r1": 1509494506 + }, + "origin": "mailx", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mail" + ] + }, + "xcb-util-renderutil-dev": { + "versions": { + "0.3.9-r1": 1509473920 + }, + "origin": "xcb-util-renderutil", + "dependencies": [ + "xcb-util-dev", + "pc:xcb-render", + "pkgconfig", + "xcb-util-renderutil=0.3.9-r1" + ], + "provides": [ + "pc:xcb-renderutil=0.3.9" + ] + }, + "libcddb-dev": { + "versions": { + "1.3.2-r2": 1509473611 + }, + "origin": "libcddb", + "dependencies": [ + "libcddb=1.3.2-r2", + "pkgconfig" + ], + "provides": [ + "pc:libcddb=1.3.2" + ] + }, + "curl": { + "versions": { + "7.61.1-r2": 1551780214, + "7.61.1-r3": 1568722567 + }, + "origin": "curl", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libz.so.1" + ], + "provides": [ + "cmd:curl" + ] + }, + "slang-dev": { + "versions": { + "2.3.1a-r0": 1509476135 + }, + "origin": "slang", + "dependencies": [ + "pkgconfig", + "slang=2.3.1a-r0" + ], + "provides": [ + "pc:slang=2.3.1" + ] + }, + "xf86-video-amdgpu-doc": { + "versions": { + "1.4.0-r0": 1510075099 + }, + "origin": "xf86-video-amdgpu" + }, + "ssh-getkey-ldap": { + "versions": { + "0.1.2-r0": 1509491919 + }, + "origin": "ssh-getkey-ldap", + "dependencies": [ + "lua", + "lua-ldap", + "/bin/sh" + ], + "provides": [ + "cmd:ssh-getkey-ldap" + ] + }, + "perl-datetime": { + "versions": { + "1.44-r0": 1510859303 + }, + "origin": "perl-datetime", + "dependencies": [ + "perl-datetime-locale", + "perl-try-tiny", + "perl-dist-checkconflicts", + "perl-params-validationcompiler", + "perl-datetime-timezone", + "perl-namespace-autoclean", + "perl-specio", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-dbd-odbc-doc": { + "versions": { + "1.56-r1": 1509493769 + }, + "origin": "perl-dbd-odbc" + }, + "perl-authen-sasl": { + "versions": { + "2.16-r1": 1510352971 + }, + "origin": "perl-authen-sasl", + "dependencies": [ + "perl", + "perl-digest-hmac" + ] + }, + "wget": { + "versions": { + "1.20.3-r0": 1554724053 + }, + "origin": "wget", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:wget" + ] + }, + "postgresql-plpython3-contrib": { + "versions": { + "10.7-r0": 1554274200, + "10.8-r0": 1557668016, + "10.9-r0": 1562225615, + "10.10-r0": 1565683439 + }, + "origin": "postgresql", + "dependencies": [ + "postgresql-plpython3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "thunar-archive-plugin": { + "versions": { + "0.3.1-r1": 1510075231 + }, + "origin": "thunar-archive-plugin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libthunarx-2.so.0" + ] + }, + "directfb-doc": { + "versions": { + "1.7.7-r1": 1509488975 + }, + "origin": "directfb" + }, + "libxres": { + "versions": { + "1.2.0-r0": 1509468563 + }, + "origin": "libxres", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXRes.so.1=1.0.0" + ] + }, + "xauth-doc": { + "versions": { + "1.0.10-r1": 1509473717 + }, + "origin": "xauth" + }, + "lua-rex": { + "versions": { + "2.9.0-r0": 1510619366 + }, + "origin": "lua-rex", + "dependencies": [ + "lua-rex-pcre", + "lua-rex-posix" + ] + }, + "abiword-plugin-urldict": { + "versions": { + "3.0.2-r1": 1510073369 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "perl-data-ical-doc": { + "versions": { + "0.21-r1": 1510588973 + }, + "origin": "perl-data-ical" + }, + "py2-chardet": { + "versions": { + "3.0.4-r0": 1509481684 + }, + "origin": "py-chardet", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:chardetect" + ] + }, + "nagios-plugins-mrtg": { + "versions": { + "2.2.1-r3": 1510288496 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "jq-doc": { + "versions": { + "1.5-r5": 1525102934 + }, + "origin": "jq" + }, + "lua-stdlib-doc": { + "versions": { + "41.2.0-r0": 1509495239 + }, + "origin": "lua-stdlib" + }, + "libcap-ng-utils": { + "versions": { + "0.7.8-r1": 1509461311 + }, + "origin": "libcap-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0" + ], + "provides": [ + "cmd:captest", + "cmd:filecap", + "cmd:netcap", + "cmd:pscap" + ] + }, + "perl-scope-upper-doc": { + "versions": { + "0.30-r0": 1509952234 + }, + "origin": "perl-scope-upper" + }, + "squid-lang-sk": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "apache2-proxy-html": { + "versions": { + "2.4.39-r0": 1554306881, + "2.4.41-r0": 1566292328 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ] + }, + "py-urllib3": { + "versions": { + "1.22-r0": 1509483033 + }, + "origin": "py-urllib3" + }, + "dvgrab-doc": { + "versions": { + "3.5-r3": 1510075828 + }, + "origin": "dvgrab" + }, + "py-mako": { + "versions": { + "1.0.7-r0": 1509468234 + }, + "origin": "py-mako" + }, + "perl": { + "versions": { + "5.26.3-r0": 1543940998 + }, + "origin": "perl", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libperl.so=0", + "cmd:perl", + "cmd:perl5.26.3", + "cmd:perldoc", + "cmd:pod2html", + "cmd:pod2man", + "cmd:pod2text", + "cmd:pod2usage", + "cmd:podchecker", + "cmd:podselect" + ] + }, + "rrdcollect-doc": { + "versions": { + "0.2.10-r1": 1509492720 + }, + "origin": "rrdcollect" + }, + "libva": { + "versions": { + "1.8.2-r0": 1510071998 + }, + "origin": "libva", + "dependencies": [ + "so:libGL.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2" + ], + "provides": [ + "so:libva-drm.so.1=1.4000.0", + "so:libva-egl.so.1=1.4000.0", + "so:libva-glx.so.1=1.4000.0", + "so:libva-tpi.so.1=1.4000.0", + "so:libva-x11.so.1=1.4000.0", + "so:libva.so.1=1.4000.0" + ] + }, + "abiword-plugin-opml": { + "versions": { + "3.0.2-r1": 1510073365 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "wayland-dev": { + "versions": { + "1.14.0-r2": 1510066935 + }, + "origin": "wayland", + "dependencies": [ + "libffi-dev", + "expat-dev", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libxml2.so.2", + "wayland-libs-client=1.14.0-r2", + "wayland-libs-cursor=1.14.0-r2", + "wayland-libs-server=1.14.0-r2" + ], + "provides": [ + "pc:wayland-client=1.14.0", + "pc:wayland-cursor=1.14.0", + "pc:wayland-scanner=1.14.0", + "pc:wayland-server=1.14.0", + "cmd:wayland-scanner" + ] + }, + "rest-dev": { + "versions": { + "0.8.1-r0": 1509492761 + }, + "origin": "rest", + "dependencies": [ + "libsoup-dev", + "gobject-introspection-dev", + "pc:glib-2.0", + "pc:libsoup-2.4", + "pc:libxml-2.0", + "pkgconfig", + "rest=0.8.1-r0" + ], + "provides": [ + "pc:rest-0.7=0.8.1", + "pc:rest-extras-0.7=0.8.1" + ] + }, + "liblockfile-dev": { + "versions": { + "1.14-r0": 1511990622 + }, + "origin": "liblockfile" + }, + "libbluray": { + "versions": { + "1.0.0-r0": 1509488856 + }, + "origin": "libbluray", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libbluray.so.2=2.0.0", + "cmd:bd_info" + ] + }, + "mupdf-tools": { + "versions": { + "1.13.0-r0": 1533746007 + }, + "origin": "mupdf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmupdf.so.0", + "so:libmupdfthird.so.0" + ], + "provides": [ + "cmd:mjsgen", + "cmd:mujstest", + "cmd:muraster", + "cmd:mutool" + ] + }, + "freeswitch-snmp": { + "versions": { + "1.6.19-r0": 1510585357 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeswitch.so.1", + "so:libnetsnmp.so.30", + "so:libnetsnmpagent.so.30" + ] + }, + "gtk-engines-clearlooks": { + "versions": { + "2.21.0-r2": 1510071722 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "gvfs-gphoto2": { + "versions": { + "1.34.1-r0": 1511430258, + "1.34.1-r1": 1563787224 + }, + "origin": "gvfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgphoto2.so.6", + "so:libgphoto2_port.so.12", + "so:libgudev-1.0.so.0", + "so:libgvfscommon.so", + "so:libgvfsdaemon.so", + "so:libintl.so.8" + ] + }, + "cmph-dev": { + "versions": { + "2.0-r1": 1509482577 + }, + "origin": "cmph", + "dependencies": [ + "libcmph=2.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:cmph=2.0" + ] + }, + "clamsmtp": { + "versions": { + "1.10-r15": 1509468381 + }, + "origin": "clamsmtp", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:clamsmtpd" + ] + }, + "py-asn1crypto": { + "versions": { + "0.23.0-r0": 1509476391 + }, + "origin": "py-asn1crypto" + }, + "freetype-dev": { + "versions": { + "2.8.1-r4": 1549555370 + }, + "origin": "freetype", + "dependencies": [ + "freetype=2.8.1-r4", + "pc:libpng", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:freetype2=21.0.15", + "cmd:freetype-config" + ] + }, + "spandsp": { + "versions": { + "0.0.6-r0": 1509481507 + }, + "origin": "spandsp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtiff.so.5" + ], + "provides": [ + "so:libspandsp.so.2=2.0.0" + ] + }, + "opensp-doc": { + "versions": { + "1.5.2-r0": 1509488683 + }, + "origin": "opensp" + }, + "dosfstools-doc": { + "versions": { + "4.1-r1": 1509495035 + }, + "origin": "dosfstools" + }, + "libssh2-dev": { + "versions": { + "1.8.2-r0": 1553682023, + "1.9.0-r0": 1571235283, + "1.9.0-r1": 1571996776 + }, + "origin": "libssh2", + "dependencies": [ + "libssh2=1.9.0-r1", + "pc:libcrypto", + "pc:libssl", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:libssh2=1.9.0" + ] + }, + "phodav-dev": { + "versions": { + "2.2-r0": 1509492229 + }, + "origin": "phodav", + "dependencies": [ + "pc:libsoup-2.4", + "phodav=2.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libphodav-2.0=2.2" + ] + }, + "xscreensaver-gl-extras": { + "versions": { + "5.36-r0": 1510074351 + }, + "origin": "xscreensaver", + "dependencies": [ + "bc", + "so:libGL.so.1", + "so:libGLU.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXmu.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgdk_pixbuf_xlib-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "cmd:xscreensaver-gl-helper" + ] + }, + "mesa": { + "versions": { + "17.2.4-r1": 1510741949 + }, + "origin": "mesa" + }, + "spawn-fcgi-doc": { + "versions": { + "1.6.4-r3": 1509552746 + }, + "origin": "spawn-fcgi" + }, + "py2-munkres": { + "versions": { + "1.0.12-r0": 1509468388 + }, + "origin": "py-munkres", + "dependencies": [ + "python2" + ] + }, + "freetype": { + "versions": { + "2.8.1-r4": 1549555370 + }, + "origin": "freetype", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libpng16.so.16", + "so:libz.so.1" + ], + "provides": [ + "so:libfreetype.so.6=6.15.0" + ] + }, + "acf-apk-tools": { + "versions": { + "0.11.0-r1": 1510073922 + }, + "origin": "acf-apk-tools", + "dependencies": [ + "acf-core", + "lua-posix", + "apk-tools" + ] + }, + "lua5.2-lustache": { + "versions": { + "1.3.1-r1": 1509472927 + }, + "origin": "lua-lustache", + "dependencies": [ + "lua-lustache-common", + "lua5.2", + "lua-lustache-common=1.3.1-r1" + ] + }, + "freeglut-dev": { + "versions": { + "3.0.0-r0": 1510071760 + }, + "origin": "freeglut", + "dependencies": [ + "mesa-dev", + "libx11-dev", + "libice-dev", + "libxxf86vm-dev", + "libxi-dev", + "glu-dev", + "freeglut=3.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:freeglut=3.0.0" + ] + }, + "p7zip": { + "versions": { + "16.02-r3": 1533742283 + }, + "origin": "p7zip", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:7z", + "cmd:7za", + "cmd:7zr", + "cmd:p7zip" + ] + }, + "quazip": { + "versions": { + "0.7.3-r0": 1510076335 + }, + "origin": "quazip", + "dependencies": [ + "so:libQtCore.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libquazip.so.1=1.0.0" + ] + }, + "kamailio-mysql": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "kamailio-db", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18", + "so:libsrdb1.so.1", + "so:libsrdb2.so.1" + ] + }, + "kamailio-doc": { + "versions": { + "5.0.7-r0": 1532960867 + }, + "origin": "kamailio" + }, + "perl-html-mason": { + "versions": { + "1.58-r0": 1509470629 + }, + "origin": "perl-html-mason", + "dependencies": [ + "perl-cgi", + "perl-cache-cache", + "perl-log-any", + "perl-html-parser", + "perl-class-container", + "perl-params-validate", + "perl-exception-class" + ], + "provides": [ + "cmd:convert0.6.README", + "cmd:convert0.6.pl", + "cmd:convert0.8.README", + "cmd:convert0.8.pl", + "cmd:mason.pl" + ] + }, + "lua-microlight": { + "versions": { + "1.1.1-r2": 1509480790 + }, + "origin": "lua-microlight" + }, + "xfce4-screenshooter-doc": { + "versions": { + "1.8.2-r1": 1510074729 + }, + "origin": "xfce4-screenshooter" + }, + "abiword-plugin-passepartout": { + "versions": { + "3.0.2-r1": 1510073366 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "lksctp-tools": { + "versions": { + "1.0.17-r0": 1509492330 + }, + "origin": "lksctp-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsctp.so.1=1.0.17", + "cmd:checksctp", + "cmd:sctp_darn", + "cmd:sctp_status", + "cmd:sctp_test", + "cmd:withsctp" + ] + }, + "lua5.2-sql-odbc": { + "versions": { + "2.3.5-r1": 1509488831 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "py3-pbr": { + "versions": { + "3.1.1-r0": 1509480860 + }, + "origin": "py-pbr", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:pbr" + ] + }, + "check": { + "versions": { + "0.12.0-r1": 1509461855 + }, + "origin": "check", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcheck.so.0=0.0.0", + "cmd:checkmk" + ] + }, + "py2-uritemplate.py": { + "versions": { + "3.0.0-r0": 1509486307 + }, + "origin": "py-uritemplate", + "dependencies": [ + "python2" + ] + }, + "lsyncd-doc": { + "versions": { + "2.2.2-r0": 1509493062 + }, + "origin": "lsyncd" + }, + "libisoburn": { + "versions": { + "1.4.8-r0": 1509462218 + }, + "origin": "libisoburn", + "dependencies": [ + "so:libburn.so.4", + "so:libc.musl-x86_64.so.1", + "so:libedit.so.0", + "so:libisofs.so.6" + ], + "provides": [ + "so:libisoburn.so.1=1.105.0" + ] + }, + "uwsgi-gevent3": { + "versions": { + "2.0.17-r0": 1522154661 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "squid-lang-nl": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libcdio-paranoia": { + "versions": { + "0.94_p1-r0": 1509473651 + }, + "origin": "libcdio-paranoia", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcdio.so.16" + ], + "provides": [ + "so:libcdio_cdda.so.2=2.0.0", + "so:libcdio_paranoia.so.2=2.0.0", + "cmd:cd-paranoia" + ] + }, + "s6-linux-utils": { + "versions": { + "2.4.0.1-r0": 1509468269 + }, + "origin": "s6-linux-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.6" + ], + "provides": [ + "cmd:s6-chroot", + "cmd:s6-devd", + "cmd:s6-fillurandompool", + "cmd:s6-freeramdisk", + "cmd:s6-hostname", + "cmd:s6-logwatch", + "cmd:s6-mount", + "cmd:s6-pivotchroot", + "cmd:s6-ps", + "cmd:s6-swapoff", + "cmd:s6-swapon", + "cmd:s6-uevent-listener", + "cmd:s6-uevent-spawner", + "cmd:s6-umount" + ] + }, + "perl-mail-spf-doc": { + "versions": { + "2.9.0-r2": 1509491249 + }, + "origin": "perl-mail-spf" + }, + "squid-lang-ro": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "py2-twitter": { + "versions": { + "3.1-r2": 1509552772 + }, + "origin": "py-twitter", + "dependencies": [ + "py2-future", + "py2-requests", + "py2-requests-oauthlib", + "python2" + ] + }, + "lua5.2-stdlib": { + "versions": { + "41.2.0-r0": 1509495244 + }, + "origin": "lua-stdlib" + }, + "libxt-doc": { + "versions": { + "1.1.5-r1": 1509466278 + }, + "origin": "libxt" + }, + "grub-bios": { + "versions": { + "2.02-r3": 1509495739 + }, + "origin": "grub", + "dependencies": [ + "grub" + ] + }, + "squark-dbg": { + "versions": { + "0.6.1-r1": 1509492120 + }, + "origin": "squark", + "dependencies": [ + "haserl" + ] + }, + "awstats-doc": { + "versions": { + "7.6-r2": 1515159956 + }, + "origin": "awstats" + }, + "lua-mqtt-publish": { + "versions": { + "0.1-r0": 1509462507 + }, + "origin": "lua-mqtt-publish" + }, + "font-util": { + "versions": { + "1.3.1-r2": 1509470387 + }, + "origin": "font-util", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bdftruncate", + "cmd:ucs2any" + ] + }, + "thin-provisioning-tools": { + "versions": { + "0.7.1-r0": 1509491570 + }, + "origin": "thin-provisioning-tools", + "dependencies": [ + "expat", + "boost", + "libaio", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:cache_check", + "cmd:cache_dump", + "cmd:cache_metadata_size", + "cmd:cache_repair", + "cmd:cache_restore", + "cmd:cache_writeback", + "cmd:era_check", + "cmd:era_dump", + "cmd:era_invalidate", + "cmd:era_restore", + "cmd:pdata_tools", + "cmd:thin_check", + "cmd:thin_delta", + "cmd:thin_dump", + "cmd:thin_ls", + "cmd:thin_metadata_size", + "cmd:thin_repair", + "cmd:thin_restore", + "cmd:thin_rmap" + ] + }, + "xrdb-doc": { + "versions": { + "1.1.0-r2": 1509473735 + }, + "origin": "xrdb" + }, + "perl-mro-compat": { + "versions": { + "0.13-r0": 1509481603 + }, + "origin": "perl-mro-compat" + }, + "jack-dbus": { + "versions": { + "1.9.10-r5": 1509473319 + }, + "origin": "jack", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libexpat.so.1", + "so:libjackserver.so.0" + ], + "provides": [ + "cmd:jackdbus" + ] + }, + "freeradius-mysql": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310603 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius-sql=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ], + "provides": [ + "freeradius3-mysql=3.0.15-r5", + "so:rlm_sql_mysql.so=0" + ] + }, + "tdb": { + "versions": { + "1.3.15-r0": 1509486505 + }, + "origin": "tdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtdb.so.1" + ], + "provides": [ + "cmd:tdbbackup", + "cmd:tdbdump", + "cmd:tdbrestore", + "cmd:tdbtool" + ] + }, + "libdbi": { + "versions": { + "0.9.0-r0": 1509474455 + }, + "origin": "libdbi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdbi.so.1=1.1.0" + ] + }, + "libgit2-dev": { + "versions": { + "0.25.1-r4": 1510288231 + }, + "origin": "libgit2", + "dependencies": [ + "curl-dev", + "libssh2-dev", + "libgit2=0.25.1-r4", + "pc:openssl", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:libgit2=0.25.1" + ] + }, + "qemu": { + "versions": { + "2.10.1-r3": 1519746244 + }, + "origin": "qemu", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libcap.so.2", + "so:libglib-2.0.so.0" + ], + "provides": [ + "cmd:ivshmem-client", + "cmd:ivshmem-server", + "cmd:qemu-nios2", + "cmd:virtfs-proxy-helper" + ] + }, + "keybinder-dev": { + "versions": { + "0.3.0-r1": 1510069940 + }, + "origin": "keybinder", + "dependencies": [ + "gtk+2.0-dev", + "libxext-dev", + "keybinder=0.3.0-r1", + "pc:gtk+-2.0", + "pkgconfig" + ], + "provides": [ + "pc:keybinder=0.3.0" + ] + }, + "dovecot-pigeonhole-plugin": { + "versions": { + "2.2.36.3-r0": 1554102388, + "2.2.36.4-r0": 1567075098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot", + "so:libc.musl-x86_64.so.1", + "so:libdovecot-lda.so.0", + "so:libdovecot-login.so.0", + "so:libdovecot-storage.so.0", + "so:libdovecot.so.0" + ], + "provides": [ + "so:lib90_sieve_plugin.so=0", + "so:lib95_imap_sieve_plugin.so=0", + "so:libdovecot-sieve.so.0=0.0.0", + "cmd:sieve-dump", + "cmd:sieve-filter", + "cmd:sieve-test", + "cmd:sievec" + ] + }, + "vala": { + "versions": { + "0.36.4-r0": 1509480648 + }, + "origin": "vala", + "dependencies": [ + "glib-dev", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libvala-0.36.so.0=0.0.0", + "pc:libvala-0.36=0.36.4", + "pc:vapigen-0.36=0.36.4", + "pc:vapigen=0.36.4", + "cmd:vala", + "cmd:vala-0.36", + "cmd:vala-gen-introspect", + "cmd:vala-gen-introspect-0.36", + "cmd:valac", + "cmd:valac-0.36", + "cmd:vapicheck", + "cmd:vapicheck-0.36", + "cmd:vapigen", + "cmd:vapigen-0.36" + ] + }, + "usbredir-doc": { + "versions": { + "0.7.1-r0": 1509707469 + }, + "origin": "usbredir" + }, + "lua5.3-expat": { + "versions": { + "1.3.0-r2": 1509483370 + }, + "origin": "lua-expat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "wipe-doc": { + "versions": { + "2.3.1-r0": 1509496530 + }, + "origin": "wipe" + }, + "nodejs": { + "versions": { + "8.9.3-r1": 1522616959 + }, + "origin": "nodejs", + "dependencies": [ + "ca-certificates", + "nodejs-npm=8.9.3-r1", + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libcrypto.so.1.0.0", + "so:libgcc_s.so.1", + "so:libhttp_parser.so.2.7.1", + "so:libssl.so.1.0.0", + "so:libstdc++.so.6", + "so:libuv.so.1", + "so:libz.so.1" + ], + "provides": [ + "nodejs-lts=8.9.3", + "cmd:node" + ] + }, + "celt051-dev": { + "versions": { + "0.5.1.3-r0": 1509475636 + }, + "origin": "celt051", + "dependencies": [ + "celt051=0.5.1.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:celt051=0.5.1.3" + ] + }, + "libmicrohttpd-doc": { + "versions": { + "0.9.57-r0": 1512032047 + }, + "origin": "libmicrohttpd" + }, + "pdnsd": { + "versions": { + "1.2.9a-r5": 1509492602 + }, + "origin": "pdnsd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pdnsd", + "cmd:pdnsd-ctl" + ] + }, + "xf86-video-openchrome": { + "versions": { + "0.6.0-r1": 1510073044 + }, + "origin": "xf86-video-openchrome", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libXvMC.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2" + ], + "provides": [ + "so:libchromeXvMC.so.1=1.0.0", + "so:libchromeXvMCPro.so.1=1.0.0" + ] + }, + "libxdmcp": { + "versions": { + "1.1.2-r4": 1509461845 + }, + "origin": "libxdmcp", + "dependencies": [ + "so:libbsd.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXdmcp.so.6=6.0.0" + ] + }, + "perl-test-leaktrace": { + "versions": { + "0.15-r3": 1509474017 + }, + "origin": "perl-test-leaktrace", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "cdrkit-doc": { + "versions": { + "1.1.11-r2": 1509482677 + }, + "origin": "cdrkit" + }, + "openpgm-dev": { + "versions": { + "5.2.122-r1": 1509475721 + }, + "origin": "openpgm", + "dependencies": [ + "openpgm=5.2.122-r1", + "pkgconfig" + ], + "provides": [ + "pc:openpgm-5.2=5.2.122" + ] + }, + "libvncserver-dev": { + "versions": { + "0.9.11-r2": 1533743207, + "0.9.11-r3": 1572818864 + }, + "origin": "libvncserver", + "dependencies": [ + "libgcrypt-dev", + "libjpeg-turbo-dev", + "gnutls-dev", + "libpng-dev", + "libice-dev", + "libx11-dev", + "libxdamage-dev", + "libxext-dev", + "libxfixes-dev", + "libxi-dev", + "libxinerama-dev", + "libxrandr-dev", + "libxtst-dev", + "libvncserver=0.9.11-r3", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:libvncclient=0.9.11", + "pc:libvncserver=0.9.11", + "cmd:libvncserver-config" + ] + }, + "sqlite": { + "versions": { + "3.25.3-r0": 1548491335, + "3.25.3-r1": 1563951530, + "3.25.3-r2": 1571590443 + }, + "origin": "sqlite", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:sqlite3" + ] + }, + "py-django": { + "versions": { + "1.11.20-r0": 1551365039, + "1.11.21-r0": 1561497065, + "1.11.22-r0": 1563783120, + "1.11.23-r0": 1565087802 + }, + "origin": "py-django", + "dependencies": [ + "py3-django=1.11.23-r0" + ] + }, + "py-gnome-bonobo": { + "versions": { + "2.28.1-r5": 1510933055 + }, + "origin": "py-gnome", + "dependencies": [ + "py-gtk", + "py-gnome-gnomecanvas" + ] + }, + "py2-mccabe": { + "versions": { + "0.6.1-r1": 1511661234 + }, + "origin": "py-mccabe", + "dependencies": [ + "python2" + ] + }, + "rhash-dev": { + "versions": { + "1.3.5-r1": 1509461595 + }, + "origin": "rhash", + "dependencies": [ + "rhash-libs=1.3.5-r1" + ] + }, + "hexchat-python": { + "versions": { + "2.12.4-r1": 1510260816 + }, + "origin": "hexchat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libpython2.7.so.1.0" + ] + }, + "coreutils": { + "versions": { + "8.28-r0": 1509462378 + }, + "origin": "coreutils", + "dependencies": [ + "/bin/sh", + "so:libacl.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:[", + "cmd:b2sum", + "cmd:base32", + "cmd:base64", + "cmd:basename", + "cmd:cat", + "cmd:chcon", + "cmd:chgrp", + "cmd:chmod", + "cmd:chown", + "cmd:chroot", + "cmd:cksum", + "cmd:comm", + "cmd:cp", + "cmd:csplit", + "cmd:cut", + "cmd:date", + "cmd:dd", + "cmd:df", + "cmd:dir", + "cmd:dircolors", + "cmd:dirname", + "cmd:du", + "cmd:echo", + "cmd:env", + "cmd:expand", + "cmd:expr", + "cmd:factor", + "cmd:false", + "cmd:fmt", + "cmd:fold", + "cmd:groups", + "cmd:head", + "cmd:hostid", + "cmd:id", + "cmd:install", + "cmd:join", + "cmd:link", + "cmd:ln", + "cmd:logname", + "cmd:ls", + "cmd:md5sum", + "cmd:mkdir", + "cmd:mkfifo", + "cmd:mknod", + "cmd:mktemp", + "cmd:mv", + "cmd:nice", + "cmd:nl", + "cmd:nohup", + "cmd:nproc", + "cmd:numfmt", + "cmd:od", + "cmd:paste", + "cmd:pathchk", + "cmd:pinky", + "cmd:pr", + "cmd:printenv", + "cmd:printf", + "cmd:ptx", + "cmd:pwd", + "cmd:readlink", + "cmd:realpath", + "cmd:rm", + "cmd:rmdir", + "cmd:runcon", + "cmd:seq", + "cmd:sha1sum", + "cmd:sha224sum", + "cmd:sha256sum", + "cmd:sha384sum", + "cmd:sha512sum", + "cmd:shred", + "cmd:shuf", + "cmd:sleep", + "cmd:sort", + "cmd:split", + "cmd:stat", + "cmd:stdbuf", + "cmd:stty", + "cmd:sum", + "cmd:sync", + "cmd:tac", + "cmd:tail", + "cmd:tee", + "cmd:test", + "cmd:timeout", + "cmd:touch", + "cmd:tr", + "cmd:true", + "cmd:truncate", + "cmd:tsort", + "cmd:tty", + "cmd:uname", + "cmd:unexpand", + "cmd:uniq", + "cmd:unlink", + "cmd:users", + "cmd:vdir", + "cmd:wc", + "cmd:who", + "cmd:whoami", + "cmd:yes" + ] + }, + "dovecot-sql": { + "versions": { + "2.2.36.3-r0": 1554102389, + "2.2.36.4-r0": 1567075098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot", + "so:libc.musl-x86_64.so.1", + "so:libdovecot.so.0" + ], + "provides": [ + "so:libdovecot-sql.so.0=0.0.0" + ] + }, + "rabbitmq-c-doc": { + "versions": { + "0.8.0-r3": 1510260051 + }, + "origin": "rabbitmq-c" + }, + "damageproto-doc": { + "versions": { + "1.2.1-r3": 1509464676 + }, + "origin": "damageproto" + }, + "py3-mako": { + "versions": { + "1.0.7-r0": 1509468233 + }, + "origin": "py-mako", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:mako-render" + ] + }, + "umix": { + "versions": { + "1.0.2-r7": 1509480174 + }, + "origin": "umix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:umix" + ] + }, + "nfs-utils": { + "versions": { + "2.1.1-r4": 1509492107 + }, + "origin": "nfs-utils", + "dependencies": [ + "rpcbind", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcom_err.so.2", + "so:libdevmapper.so.1.02", + "so:libevent-2.1.so.6", + "so:libgssapi_krb5.so.2", + "so:libkeyutils.so.1", + "so:libkrb5.so.3", + "so:libmount.so.1", + "so:libnfsidmap.so.0", + "so:libsqlite3.so.0", + "so:libtirpc.so.3" + ], + "provides": [ + "cmd:blkmapd", + "cmd:exportfs", + "cmd:mount.nfs", + "cmd:mount.nfs4", + "cmd:mountstats", + "cmd:nfsdcltrack", + "cmd:nfsidmap", + "cmd:nfsiostat", + "cmd:nfsstat", + "cmd:osd_login", + "cmd:rpc.gssd", + "cmd:rpc.idmapd", + "cmd:rpc.mountd", + "cmd:rpc.nfsd", + "cmd:rpc.statd", + "cmd:rpcdebug", + "cmd:showmount", + "cmd:sm-notify", + "cmd:start-statd", + "cmd:umount.nfs", + "cmd:umount.nfs4" + ] + }, + "xf86-video-glint-doc": { + "versions": { + "1.2.9-r0": 1510073703 + }, + "origin": "xf86-video-glint" + }, + "nodejs-npm": { + "versions": { + "8.9.3-r1": 1522616958 + }, + "origin": "nodejs", + "dependencies": [ + "nodejs" + ], + "provides": [ + "nodejs-current-npm", + "cmd:npm" + ] + }, + "openjpeg-tools": { + "versions": { + "2.3.0-r2": 1552584602 + }, + "origin": "openjpeg", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblcms2.so.2", + "so:libopenjp2.so.7", + "so:libpng16.so.16", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:opj_compress", + "cmd:opj_decompress", + "cmd:opj_dump" + ] + }, + "perl-datetime-format-mail-doc": { + "versions": { + "0.403-r0": 1511386193 + }, + "origin": "perl-datetime-format-mail" + }, + "cdrkit": { + "versions": { + "1.1.11-r2": 1509482679 + }, + "origin": "cdrkit", + "dependencies": [ + "file", + "bzip2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:cdda2mp3", + "cmd:cdda2ogg", + "cmd:cdda2wav", + "cmd:cdrecord", + "cmd:devdump", + "cmd:dirsplit", + "cmd:genisoimage", + "cmd:icedax", + "cmd:isodebug", + "cmd:isodump", + "cmd:isoinfo", + "cmd:isovfy", + "cmd:mkhybrid", + "cmd:mkisofs", + "cmd:netscsid", + "cmd:pitchplay", + "cmd:readcd", + "cmd:readmult", + "cmd:readom", + "cmd:wodim" + ] + }, + "udisks2": { + "versions": { + "2.6.5-r0": 1510075153 + }, + "origin": "udisks2", + "dependencies": [ + "so:libacl.so.1", + "so:libatasmart.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libpolkit-agent-1.so.0", + "so:libpolkit-gobject-1.so.0", + "so:libudisks2.so.0" + ], + "provides": [ + "cmd:udisksctl", + "cmd:umount.udisks2" + ] + }, + "lua5.2-posix": { + "versions": { + "33.4.0-r0": 1509468345 + }, + "origin": "lua-posix", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "patchwork-doc": { + "versions": { + "1.1.3-r0": 1509474237, + "1.1.3-r1": 1562223418 + }, + "origin": "patchwork" + }, + "qt-doc": { + "versions": { + "4.8.7-r8": 1510287208 + }, + "origin": "qt" + }, + "py3-markupsafe": { + "versions": { + "1.0-r0": 1509476743 + }, + "origin": "py-markupsafe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "py-django-contact-form": { + "versions": { + "1.4.2-r0": 1509489456 + }, + "origin": "py-django-contact-form", + "dependencies": [ + "py-django" + ] + }, + "xdg-utils": { + "versions": { + "1.1.2-r0": 1510842482 + }, + "origin": "xdg-utils", + "dependencies": [ + "xset", + "xprop" + ], + "provides": [ + "cmd:xdg-desktop-icon", + "cmd:xdg-desktop-menu", + "cmd:xdg-email", + "cmd:xdg-icon-resource", + "cmd:xdg-mime", + "cmd:xdg-open", + "cmd:xdg-screensaver", + "cmd:xdg-settings" + ] + }, + "opensp-dev": { + "versions": { + "1.5.2-r0": 1509488681 + }, + "origin": "opensp", + "dependencies": [ + "opensp=1.5.2-r0" + ] + }, + "openssh-server-common": { + "versions": { + "7.5_p1-r10": 1551712288 + }, + "origin": "openssh" + }, + "dvgrab": { + "versions": { + "3.5-r3": 1510075829 + }, + "origin": "dvgrab", + "dependencies": [ + "so:libavc1394.so.0", + "so:libc.musl-x86_64.so.1", + "so:libdv.so.4", + "so:libgcc_s.so.1", + "so:libiec61883.so.0", + "so:libjpeg.so.8", + "so:libraw1394.so.11", + "so:librom1394.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:dvgrab" + ] + }, + "samba-pidl": { + "versions": { + "4.7.6-r3": 1555491789 + }, + "origin": "samba", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:pidl" + ] + }, + "s6-rc-doc": { + "versions": { + "0.2.1.2-r0": 1509492619 + }, + "origin": "s6-rc" + }, + "libsmartcols": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsmartcols.so.1=1.1.0" + ] + }, + "ack": { + "versions": { + "2.18-r0": 1510073016 + }, + "origin": "ack", + "dependencies": [ + "perl-file-next", + "perl" + ], + "provides": [ + "cmd:ack" + ] + }, + "squid-lang-hy": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "smartmontools": { + "versions": { + "6.6-r0": 1510573358 + }, + "origin": "smartmontools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:smartctl", + "cmd:smartd", + "cmd:update-smart-drivedb" + ] + }, + "kbproto": { + "versions": { + "1.0.7-r2": 1509461961 + }, + "origin": "kbproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:kbproto=1.0.7" + ] + }, + "libxxf86misc-dev": { + "versions": { + "1.0.3-r1": 1509473661 + }, + "origin": "libxxf86misc", + "dependencies": [ + "libxxf86misc=1.0.3-r1", + "pc:x11", + "pc:xext", + "pc:xf86miscproto", + "pkgconfig" + ], + "provides": [ + "pc:xxf86misc=1.0.3" + ] + }, + "uwsgi-carbon": { + "versions": { + "2.0.17-r0": 1522154653 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libmms-dev": { + "versions": { + "0.6.4-r0": 1509480979 + }, + "origin": "libmms", + "dependencies": [ + "libmms=0.6.4-r0", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libmms=0.6.4" + ] + }, + "gitolite": { + "versions": { + "3.6.11-r0": 1548491616 + }, + "origin": "gitolite", + "dependencies": [ + "git", + "perl", + "/bin/sh" + ] + }, + "py3-gflags": { + "versions": { + "3.1.1-r1": 1509551804 + }, + "origin": "py-gflags", + "dependencies": [ + "python3" + ] + }, + "py-pylast": { + "versions": { + "1.9.0-r0": 1509491031 + }, + "origin": "py-pylast" + }, + "perl-test-mockrandom": { + "versions": { + "1.01-r1": 1510855674 + }, + "origin": "perl-test-mockrandom" + }, + "lua-sql-odbc": { + "versions": { + "2.3.5-r1": 1509488835 + }, + "origin": "lua-sql" + }, + "ifupdown": { + "versions": { + "0.7.53.1-r1": 1509494558 + }, + "origin": "ifupdown", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ifdown", + "cmd:ifquery", + "cmd:ifup" + ] + }, + "perl-sub-info-doc": { + "versions": { + "0.002-r0": 1509481619 + }, + "origin": "perl-sub-info" + }, + "dkimproxy-doc": { + "versions": { + "1.4.1-r5": 1509517849 + }, + "origin": "dkimproxy" + }, + "zlib-dev": { + "versions": { + "1.2.11-r1": 1509456390 + }, + "origin": "zlib", + "dependencies": [ + "pkgconfig", + "zlib=1.2.11-r1" + ], + "provides": [ + "pc:zlib=1.2.11" + ] + }, + "bacula-mysql": { + "versions": { + "9.0.5-r3": 1512992127 + }, + "origin": "bacula", + "dependencies": [ + "bacula", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ], + "provides": [ + "so:libbaccats-mysql-9.0.5.so=0" + ] + }, + "libnl3-cli": { + "versions": { + "3.2.28-r1": 1509475887 + }, + "origin": "libnl3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnl-3.so.200", + "so:libnl-genl-3.so.200", + "so:libnl-idiag-3.so.200", + "so:libnl-nf-3.so.200", + "so:libnl-route-3.so.200" + ], + "provides": [ + "so:libnl-cli-3.so.200=200.23.0", + "cmd:genl-ctrl-list", + "cmd:idiag-socket-details", + "cmd:nf-ct-add", + "cmd:nf-ct-list", + "cmd:nf-exp-add", + "cmd:nf-exp-delete", + "cmd:nf-exp-list", + "cmd:nf-log", + "cmd:nf-monitor", + "cmd:nf-queue", + "cmd:nl-addr-add", + "cmd:nl-addr-delete", + "cmd:nl-addr-list", + "cmd:nl-class-add", + "cmd:nl-class-delete", + "cmd:nl-class-list", + "cmd:nl-classid-lookup", + "cmd:nl-cls-add", + "cmd:nl-cls-delete", + "cmd:nl-cls-list", + "cmd:nl-fib-lookup", + "cmd:nl-link-enslave", + "cmd:nl-link-ifindex2name", + "cmd:nl-link-list", + "cmd:nl-link-name2ifindex", + "cmd:nl-link-release", + "cmd:nl-link-set", + "cmd:nl-link-stats", + "cmd:nl-list-caches", + "cmd:nl-list-sockets", + "cmd:nl-monitor", + "cmd:nl-neigh-add", + "cmd:nl-neigh-delete", + "cmd:nl-neigh-list", + "cmd:nl-neightbl-list", + "cmd:nl-pktloc-lookup", + "cmd:nl-qdisc-add", + "cmd:nl-qdisc-delete", + "cmd:nl-qdisc-list", + "cmd:nl-route-add", + "cmd:nl-route-delete", + "cmd:nl-route-get", + "cmd:nl-route-list", + "cmd:nl-rule-list", + "cmd:nl-tctree-list", + "cmd:nl-util-addr" + ] + }, + "asterisk-mobile": { + "versions": { + "15.6.1-r0": 1537795342, + "15.6.2-r0": 1568705004 + }, + "origin": "asterisk", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "xfce4-terminal-lang": { + "versions": { + "0.6.3-r1": 1510074617 + }, + "origin": "xfce4-terminal", + "dependencies": [ + "startup-notification", + "hicolor-icon-theme" + ] + }, + "nano-syntax": { + "versions": { + "2.9.1-r0": 1512032032 + }, + "origin": "nano" + }, + "xfce4-wavelan-plugin": { + "versions": { + "0.6.0-r0": 1510072790 + }, + "origin": "xfce4-wavelan-plugin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-3.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libxfce4panel-2.0.so.4", + "so:libxfce4ui-2.so.0", + "so:libxfce4util.so.7" + ] + }, + "rsync": { + "versions": { + "3.1.3-r0": 1521547982 + }, + "origin": "rsync", + "dependencies": [ + "so:libacl.so.1", + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:rsync" + ] + }, + "mcpp": { + "versions": { + "2.7.2-r1": 1509473731 + }, + "origin": "mcpp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmcpp.so.0" + ], + "provides": [ + "cmd:mcpp" + ] + }, + "squid-lang-pl": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libvdpau-dev": { + "versions": { + "1.1.1-r1": 1509467159 + }, + "origin": "libvdpau", + "dependencies": [ + "libvdpau=1.1.1-r1", + "pc:x11", + "pkgconfig" + ], + "provides": [ + "pc:vdpau=1.1.1" + ] + }, + "font-bh-75dpi": { + "versions": { + "1.0.3-r0": 1509496603 + }, + "origin": "font-bh-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "uwsgi-stats_pusher_file": { + "versions": { + "2.0.17-r0": 1522154659 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "imagemagick": { + "versions": { + "7.0.7.11-r1": 1510748307 + }, + "origin": "imagemagick", + "dependencies": [ + "so:libMagickCore-7.Q16HDRI.so.4", + "so:libMagickWand-7.Q16HDRI.so.4", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgomp.so.1", + "so:libgs.so.9", + "so:libjpeg.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpng16.so.16", + "so:librsvg-2.so.2", + "so:libtiff.so.5", + "so:libwebp.so.7", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:animate", + "cmd:compare", + "cmd:composite", + "cmd:conjure", + "cmd:convert", + "cmd:display", + "cmd:identify", + "cmd:import", + "cmd:magick", + "cmd:magick-script", + "cmd:mogrify", + "cmd:montage", + "cmd:stream" + ] + }, + "libxi": { + "versions": { + "1.7.9-r1": 1509466011 + }, + "origin": "libxi", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXi.so.6=6.1.0" + ] + }, + "squid-lang-cs": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "openobex": { + "versions": { + "1.7.2-r1": 1510071736 + }, + "origin": "openobex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libopenobex.so.2=1.7.2", + "cmd:obex-check-device" + ] + }, + "rtmpdump": { + "versions": { + "2.4_git20160909-r3": 1510259117 + }, + "origin": "rtmpdump", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librtmp.so.1" + ], + "provides": [ + "cmd:rtmpdump", + "cmd:rtmpgw", + "cmd:rtmpsrv", + "cmd:rtmpsuck" + ] + }, + "perl-digest-sha1-doc": { + "versions": { + "2.13-r9": 1509468920 + }, + "origin": "perl-digest-sha1" + }, + "xcb-util": { + "versions": { + "0.4.0-r1": 1509464530 + }, + "origin": "xcb-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-util.so.1=1.0.0" + ] + }, + "ix": { + "versions": { + "0.6-r0": 1509477743 + }, + "origin": "sprunge", + "dependencies": [ + "curl" + ], + "provides": [ + "cmd:ix" + ] + }, + "perl-io-socket-ssl": { + "versions": { + "2.048-r1": 1509474731 + }, + "origin": "perl-io-socket-ssl", + "dependencies": [ + "ca-certificates", + "perl-net-libidn", + "perl-net-ssleay" + ] + }, + "py-docutils": { + "versions": { + "0.13.1-r0": 1509486248 + }, + "origin": "py-docutils", + "dependencies": [ + "py3-docutils", + "py3-docutils=0.13.1-r0" + ] + }, + "xbacklight": { + "versions": { + "1.2.1-r0": 1509491142 + }, + "origin": "xbacklight", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-randr.so.0", + "so:libxcb-util.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "cmd:xbacklight" + ] + }, + "xfce4-mixer-doc": { + "versions": { + "4.11.0-r2": 1510069969 + }, + "origin": "xfce4-mixer" + }, + "perl-net-dns-resolver-mock-doc": { + "versions": { + "1.20170814-r0": 1509469053 + }, + "origin": "perl-net-dns-resolver-mock" + }, + "py2-requests-oauthlib": { + "versions": { + "0.8.0-r1": 1509552764 + }, + "origin": "py-requests-oauthlib", + "dependencies": [ + "py2-oauthlib", + "py2-requests", + "python2" + ] + }, + "libwnck": { + "versions": { + "2.31.0-r5": 1510068129 + }, + "origin": "libwnck", + "dependencies": [ + "so:libX11.so.6", + "so:libXRes.so.1", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libstartup-notification-1.so.0" + ], + "provides": [ + "so:libwnck-1.so.22=22.4.0" + ] + }, + "asterisk-dahdi": { + "versions": { + "15.6.1-r0": 1537795343, + "15.6.2-r0": 1568705005 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpri.so.1.4", + "so:libtonezone.so.2" + ] + }, + "libxcb": { + "versions": { + "1.12-r1": 1509461940 + }, + "origin": "libxcb", + "dependencies": [ + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxcb-composite.so.0=0.0.0", + "so:libxcb-damage.so.0=0.0.0", + "so:libxcb-dpms.so.0=0.0.0", + "so:libxcb-dri2.so.0=0.0.0", + "so:libxcb-dri3.so.0=0.0.0", + "so:libxcb-glx.so.0=0.0.0", + "so:libxcb-present.so.0=0.0.0", + "so:libxcb-randr.so.0=0.1.0", + "so:libxcb-record.so.0=0.0.0", + "so:libxcb-render.so.0=0.0.0", + "so:libxcb-res.so.0=0.0.0", + "so:libxcb-screensaver.so.0=0.0.0", + "so:libxcb-shape.so.0=0.0.0", + "so:libxcb-shm.so.0=0.0.0", + "so:libxcb-sync.so.1=1.0.0", + "so:libxcb-xf86dri.so.0=0.0.0", + "so:libxcb-xfixes.so.0=0.0.0", + "so:libxcb-xinerama.so.0=0.0.0", + "so:libxcb-xinput.so.0=0.1.0", + "so:libxcb-xkb.so.1=1.0.0", + "so:libxcb-xtest.so.0=0.0.0", + "so:libxcb-xv.so.0=0.0.0", + "so:libxcb-xvmc.so.0=0.0.0", + "so:libxcb.so.1=1.1.0" + ] + }, + "libunistring-dev": { + "versions": { + "0.9.7-r0": 1509458997 + }, + "origin": "libunistring", + "dependencies": [ + "libunistring=0.9.7-r0" + ] + }, + "py2-psycopg2": { + "versions": { + "2.7.3.2-r0": 1510619591 + }, + "origin": "py-psycopg2", + "dependencies": [ + "py-egenix-mx-base", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5", + "so:libpython2.7.so.1.0" + ] + }, + "freeradius-radclient": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310604 + }, + "origin": "freeradius", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeradius-radius.so", + "so:libtalloc.so.2" + ], + "provides": [ + "freeradius3-radclient=3.0.15-r5", + "cmd:radclient" + ] + }, + "py-virtualenv": { + "versions": { + "15.1.0-r0": 1512030542 + }, + "origin": "py-virtualenv" + }, + "lxterminal-lang": { + "versions": { + "0.3.0-r2": 1510071921 + }, + "origin": "lxterminal" + }, + "gst-plugins-base0.10": { + "versions": { + "0.10.36-r3": 1510068368 + }, + "origin": "gst-plugins-base0.10", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstcontroller-0.10.so.0", + "so:libgstdataprotocol-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libintl.so.8", + "so:libogg.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libtheoradec.so.1", + "so:libtheoraenc.so.1", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libgstapp-0.10.so.0=0.25.0", + "so:libgstaudio-0.10.so.0=0.25.0", + "so:libgstcdda-0.10.so.0=0.25.0", + "so:libgstfft-0.10.so.0=0.25.0", + "so:libgstinterfaces-0.10.so.0=0.25.0", + "so:libgstnetbuffer-0.10.so.0=0.25.0", + "so:libgstpbutils-0.10.so.0=0.25.0", + "so:libgstriff-0.10.so.0=0.25.0", + "so:libgstrtp-0.10.so.0=0.25.0", + "so:libgstrtsp-0.10.so.0=0.25.0", + "so:libgstsdp-0.10.so.0=0.25.0", + "so:libgsttag-0.10.so.0=0.25.0", + "so:libgstvideo-0.10.so.0=0.25.0", + "cmd:gst-discoverer-0.10", + "cmd:gst-visualise-0.10" + ] + }, + "pmacct-doc": { + "versions": { + "1.6.2-r0": 1509494257 + }, + "origin": "pmacct" + }, + "perl-date-calc-doc": { + "versions": { + "6.4-r1": 1509485624 + }, + "origin": "perl-date-calc" + }, + "lua5.2-filesystem": { + "versions": { + "1.7.0.2-r0": 1511049451 + }, + "origin": "lua-filesystem", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "libresample": { + "versions": { + "0.1.3-r1": 1509481513 + }, + "origin": "libresample" + }, + "perl-mail-dkim-doc": { + "versions": { + "0.44-r0": 1509516995 + }, + "origin": "perl-mail-dkim" + }, + "py3-parsing": { + "versions": { + "2.2.0-r0": 1509475644 + }, + "origin": "py-parsing", + "dependencies": [ + "python3" + ] + }, + "py3-lockfile": { + "versions": { + "0.12.2-r0": 1511197215 + }, + "origin": "py-lockfile", + "dependencies": [ + "python3" + ] + }, + "fts-dev": { + "versions": { + "1.2.7-r0": 1509481205 + }, + "origin": "fts", + "dependencies": [ + "fts=1.2.7-r0", + "pkgconfig" + ], + "provides": [ + "pc:libfts=1.2.7", + "pc:musl-fts=1.2.7" + ] + }, + "perl-class-data-inheritable-doc": { + "versions": { + "0.08-r0": 1509470593 + }, + "origin": "perl-class-data-inheritable" + }, + "lutok-dev": { + "versions": { + "0.4-r1": 1509459744 + }, + "origin": "lutok", + "dependencies": [ + "lua5.2-dev", + "lutok=0.4-r1", + "pkgconfig" + ], + "provides": [ + "pc:lutok=0.4" + ] + }, + "util-linux-doc": { + "versions": { + "2.31.1-r0": 1541506293 + }, + "origin": "util-linux" + }, + "apr-util": { + "versions": { + "1.6.1-r1": 1510285059 + }, + "origin": "apr-util", + "dependencies": [ + "so:libapr-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libexpat.so.1" + ], + "provides": [ + "so:libaprutil-1.so.0=0.6.1" + ] + }, + "py2-parsing": { + "versions": { + "2.2.0-r0": 1509475642 + }, + "origin": "py-parsing", + "dependencies": [ + "python2" + ] + }, + "py-django-extra-views": { + "versions": { + "0.6.4-r0": 1509493857 + }, + "origin": "py-django-extra-views", + "dependencies": [ + "py-django" + ] + }, + "libxinerama": { + "versions": { + "1.1.3-r1": 1509468244 + }, + "origin": "libxinerama", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXinerama.so.1=1.0.0" + ] + }, + "iproute2-bash-completion": { + "versions": { + "4.13.0-r0": 1509481070 + }, + "origin": "iproute2" + }, + "cmph": { + "versions": { + "2.0-r1": 1509482578 + }, + "origin": "cmph", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcmph.so.0" + ], + "provides": [ + "cmd:cmph" + ] + }, + "postfix-pcre": { + "versions": { + "3.2.4-r1": 1510285340 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ], + "provides": [ + "so:postfix-pcre.so=0" + ] + }, + "libacl": { + "versions": { + "2.2.52-r3": 1509459587 + }, + "origin": "acl", + "dependencies": [ + "so:libattr.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libacl.so.1=1.1.0" + ] + }, + "dovecot-dev": { + "versions": { + "2.2.36.3-r0": 1554102386, + "2.2.36.4-r0": 1567075097 + }, + "origin": "dovecot" + }, + "gsl": { + "versions": { + "2.4-r0": 1509493183 + }, + "origin": "gsl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgsl.so.23=23.0.0", + "so:libgslcblas.so.0=0.0.0", + "cmd:gsl-histogram", + "cmd:gsl-randist" + ] + }, + "jq-dev": { + "versions": { + "1.5-r5": 1525102934 + }, + "origin": "jq", + "dependencies": [ + "jq=1.5-r5" + ] + }, + "mc": { + "versions": { + "4.8.20-r0": 1511871582 + }, + "origin": "mc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libssh2.so.1" + ], + "provides": [ + "cmd:mc", + "cmd:mcdiff", + "cmd:mcedit", + "cmd:mcview" + ] + }, + "goaccess-doc": { + "versions": { + "1.2-r1": 1509480167 + }, + "origin": "goaccess" + }, + "openldap-back-meta": { + "versions": { + "2.4.45-r3": 1510258133, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "gnuchess-doc": { + "versions": { + "6.2.5-r0": 1509496082 + }, + "origin": "gnuchess" + }, + "perl-mime-types": { + "versions": { + "2.14-r0": 1511188636 + }, + "origin": "perl-mime-types" + }, + "collectd-mqtt": { + "versions": { + "5.7.2-r0": 1510069654 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ] + }, + "perl-extutils-cchecker": { + "versions": { + "0.10-r0": 1511889284 + }, + "origin": "perl-extutils-cchecker", + "dependencies": [ + "perl-test-exception" + ] + }, + "glfw": { + "versions": { + "3.2.1-r2": 1510068481 + }, + "origin": "glfw", + "dependencies": [ + "so:libX11.so.6", + "so:libXcursor.so.1", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libglfw.so.3=3.2" + ] + }, + "lua-openrc": { + "versions": { + "0.2-r2": 1509492205 + }, + "origin": "lua-openrc" + }, + "varnish-dbg": { + "versions": { + "5.2.1-r0": 1511265090 + }, + "origin": "varnish", + "dependencies": [ + "gcc", + "libc-dev", + "libgcc" + ] + }, + "logrotate-doc": { + "versions": { + "3.13.0-r0": 1509473542 + }, + "origin": "logrotate" + }, + "gnokii-smsd-mysql": { + "versions": { + "0.6.31-r6": 1510069852 + }, + "origin": "gnokii", + "dependencies": [ + "gnokii-smsd", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "nagios-plugins-load": { + "versions": { + "2.2.1-r3": 1510288496 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "ipset": { + "versions": { + "6.34-r1": 1537515181 + }, + "origin": "ipset", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0" + ], + "provides": [ + "so:libipset.so.3=3.7.0", + "cmd:ipset" + ] + }, + "qemu-hppa": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-hppa" + ] + }, + "libx11-dev": { + "versions": { + "1.6.6-r0": 1538999940 + }, + "origin": "libx11", + "dependencies": [ + "libxcb-dev", + "xextproto", + "xf86bigfontproto-dev", + "xtrans", + "inputproto", + "libx11=1.6.6-r0", + "pc:kbproto", + "pc:xcb", + "pc:xcb>=1.11.1", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:x11-xcb=1.6.6", + "pc:x11=1.6.6" + ] + }, + "samba-winbind": { + "versions": { + "4.7.6-r3": 1555491787 + }, + "origin": "samba", + "dependencies": [ + "samba-common=4.7.6-r3", + "so:libMESSAGING-samba4.so", + "so:libads-samba4.so", + "so:libasn1util-samba4.so", + "so:libauth-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-ldap-common-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libdcerpc-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libidmap-samba4.so", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldap-2.4.so.2", + "so:liblibcli-lsa3-samba4.so", + "so:liblibcli-netlogon3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnpa-tstream-samba4.so", + "so:libnss-info-samba4.so", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb-common-samba4.so", + "so:libsecrets3-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-shim-samba4.so", + "so:libsmbldap.so.2", + "so:libsocket-blocking-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so", + "so:libtrusts-util-samba4.so", + "so:libutil-tdb-samba4.so" + ], + "provides": [ + "cmd:winbindd" + ] + }, + "font-mutt-misc": { + "versions": { + "1.0.3-r0": 1509496611 + }, + "origin": "font-mutt-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "oidentd-doc": { + "versions": { + "2.0.8-r5": 1509483894 + }, + "origin": "oidentd" + }, + "libmagic": { + "versions": { + "5.32-r0": 1509461605, + "5.32-r1": 1563340161, + "5.32-r2": 1572348414 + }, + "origin": "file", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmagic.so.1=1.0.0" + ] + }, + "db-c++": { + "versions": { + "5.3.28-r0": 1509469316 + }, + "origin": "db", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libdb_cxx-5.3.so=0" + ] + }, + "policyd-spf-fs": { + "versions": { + "23-r3": 1509491339 + }, + "origin": "policyd-spf-fs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libspf2.so.2" + ], + "provides": [ + "cmd:policyd-spf-fs" + ] + }, + "lockfile-progs-doc": { + "versions": { + "0.1.17-r0": 1509468621 + }, + "origin": "lockfile-progs" + }, + "libxau": { + "versions": { + "1.0.8-r2": 1509461812 + }, + "origin": "libxau", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXau.so.6=6.0.0" + ] + }, + "readline-doc": { + "versions": { + "7.0.003-r0": 1509456789 + }, + "origin": "readline" + }, + "perl-module-refresh": { + "versions": { + "0.17-r1": 1510564499 + }, + "origin": "perl-module-refresh" + }, + "linux-pam-doc": { + "versions": { + "1.3.0-r0": 1509473197 + }, + "origin": "linux-pam" + }, + "adwaita-icon-theme": { + "versions": { + "3.26.0-r0": 1510076149 + }, + "origin": "adwaita-icon-theme" + }, + "sox-dev": { + "versions": { + "14.4.2-r0": 1510074686 + }, + "origin": "sox", + "dependencies": [ + "pkgconfig", + "sox=14.4.2-r0" + ], + "provides": [ + "pc:sox=14.4.2" + ] + }, + "libraw1394-dev": { + "versions": { + "2.1.2-r0": 1509470072 + }, + "origin": "libraw1394", + "dependencies": [ + "libraw1394=2.1.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libraw1394=2.1.2" + ] + }, + "ilbc": { + "versions": { + "0.0.1-r0": 1510584810 + }, + "origin": "ilbc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libilbc.so.0=0.0.2" + ] + }, + "recode": { + "versions": { + "3.6-r1": 1509485764 + }, + "origin": "recode", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:librecode.so.0=0.0.0", + "cmd:recode" + ] + }, + "hunspell-doc": { + "versions": { + "1.6.2-r1": 1509477668 + }, + "origin": "hunspell" + }, + "perl-ipc-sharelite-doc": { + "versions": { + "0.17-r2": 1509469072 + }, + "origin": "perl-ipc-sharelite" + }, + "yasm-doc": { + "versions": { + "1.3.0-r1": 1509473588 + }, + "origin": "yasm" + }, + "umix-doc": { + "versions": { + "1.0.2-r7": 1509480173 + }, + "origin": "umix" + }, + "parted-doc": { + "versions": { + "3.2-r6": 1509482206 + }, + "origin": "parted" + }, + "postgresql-bdr": { + "versions": { + "9.4.10_p1-r3": 1510259287 + }, + "origin": "postgresql-bdr", + "dependencies": [ + "bash", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "postgresql", + "so:postgresql-bdr:libecpg.so.6=6.6", + "so:postgresql-bdr:libecpg_compat.so.3=3.6", + "so:postgresql-bdr:libpgtypes.so.3=3.5", + "cmd:createlang", + "cmd:droplang", + "cmd:ecpg", + "cmd:initdb", + "cmd:pg_config", + "cmd:pg_controldata", + "cmd:pg_ctl", + "cmd:pg_receivexlog", + "cmd:pg_resetxlog", + "cmd:postgres", + "cmd:postmaster" + ] + }, + "djbdns-common": { + "versions": { + "1.05-r47": 1509488767 + }, + "origin": "djbdns", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dnsip", + "cmd:dnsqr" + ] + }, + "perl-http-date": { + "versions": { + "6.02-r1": 1509464355 + }, + "origin": "perl-http-date", + "dependencies": [ + "perl" + ] + }, + "py2-simplejson": { + "versions": { + "3.8.2-r2": 1509494750 + }, + "origin": "py-simplejson", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "open-isns-doc": { + "versions": { + "0.97-r2": 1510259303 + }, + "origin": "open-isns" + }, + "py3-cairo-dev": { + "versions": { + "1.10.0-r0": 1509481861 + }, + "origin": "py3-cairo", + "dependencies": [ + "pc:cairo", + "pkgconfig" + ], + "provides": [ + "pc:py3cairo=1.10.0" + ] + }, + "perl-json-doc": { + "versions": { + "2.97000-r0": 1511989778 + }, + "origin": "perl-json" + }, + "boost-iostreams": { + "versions": { + "1.62.0-r5": 1509465875 + }, + "origin": "boost", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libboost_iostreams-mt.so.1.62.0=1.62.0", + "so:libboost_iostreams.so.1.62.0=1.62.0" + ] + }, + "lua5.3": { + "versions": { + "5.3.5-r2": 1557163184 + }, + "origin": "lua5.3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblinenoise.so.0", + "so:liblua-5.3.so.0" + ], + "provides": [ + "cmd:lua5.3", + "cmd:luac5.3" + ] + }, + "udisks": { + "versions": { + "1.0.5-r3": 1510073404 + }, + "origin": "udisks", + "dependencies": [ + "so:libatasmart.so.4", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libdevmapper.so.1.02", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libparted.so.2", + "so:libpolkit-gobject-1.so.0", + "so:libsgutils2.so.2", + "so:libudev.so.1" + ], + "provides": [ + "cmd:udisks", + "cmd:udisks-tcp-bridge", + "cmd:umount.udisks" + ] + }, + "libmikmod-dev": { + "versions": { + "3.3.11.1-r0": 1509481761 + }, + "origin": "libmikmod", + "dependencies": [ + "libmikmod=3.3.11.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmikmod=3.3.11", + "cmd:libmikmod-config" + ] + }, + "perl-class-singleton": { + "versions": { + "1.5-r0": 1509485598 + }, + "origin": "perl-class-singleton" + }, + "xbanish-doc": { + "versions": { + "1.5-r0": 1509489791 + }, + "origin": "xbanish" + }, + "perl-device-serialport-doc": { + "versions": { + "1.04-r10": 1509489402 + }, + "origin": "perl-device-serialport" + }, + "perl-ipc-sharelite": { + "versions": { + "0.17-r2": 1509469073 + }, + "origin": "perl-ipc-sharelite", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.1-posix": { + "versions": { + "33.4.0-r0": 1509468343 + }, + "origin": "lua-posix", + "dependencies": [ + "lua5.1-bitlib<26", + "so:libc.musl-x86_64.so.1" + ] + }, + "gtkglext-doc": { + "versions": { + "1.2.0-r11": 1510075507 + }, + "origin": "gtkglext" + }, + "xf86-video-savage-doc": { + "versions": { + "2.3.9-r0": 1510076049 + }, + "origin": "xf86-video-savage" + }, + "jfsutils": { + "versions": { + "1.1.15-r1": 1509482135 + }, + "origin": "jfsutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:fsck.jfs", + "cmd:jfs_debugfs", + "cmd:jfs_fsck", + "cmd:jfs_fscklog", + "cmd:jfs_logdump", + "cmd:jfs_mkfs", + "cmd:jfs_tune", + "cmd:mkfs.jfs" + ] + }, + "ctags-doc": { + "versions": { + "5.8-r5": 1509491649 + }, + "origin": "ctags" + }, + "dropbear-openrc": { + "versions": { + "2018.76-r2": 1537439117 + }, + "origin": "dropbear" + }, + "pax-utils": { + "versions": { + "1.2.2-r1": 1509459679 + }, + "origin": "pax-utils", + "dependencies": [ + "scanelf", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2" + ], + "provides": [ + "cmd:dumpelf", + "cmd:pspax", + "cmd:scanmacho", + "cmd:symtree" + ] + }, + "asterisk-srtp": { + "versions": { + "15.6.1-r0": 1537795343, + "15.6.2-r0": 1568705005 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "geeqie": { + "versions": { + "1.3-r1": 1510075967 + }, + "origin": "geeqie", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libexiv2.so.14", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:liblua.so.5", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libstdc++.so.6", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:geeqie" + ] + }, + "socat-doc": { + "versions": { + "1.7.3.2-r3": 1510260539 + }, + "origin": "socat" + }, + "lua5.3-struct": { + "versions": { + "0.2-r2": 1509494933 + }, + "origin": "lua-struct", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "tolua++": { + "versions": { + "1.0.93-r2": 1509479805 + }, + "origin": "tolua++", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ], + "provides": [ + "cmd:tolua++" + ] + }, + "libgudev": { + "versions": { + "230-r2": 1509470859 + }, + "origin": "libgudev", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libudev.so.1" + ], + "provides": [ + "so:libgudev-1.0.so.0=0.2.0" + ] + }, + "openldap-overlay-retcode": { + "versions": { + "2.4.45-r3": 1510258135, + "2.4.46-r0": 1565073941, + "2.4.48-r0": 1566900518 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "snappy-dev": { + "versions": { + "1.1.4-r2": 1509483342 + }, + "origin": "snappy", + "dependencies": [ + "pkgconfig", + "snappy=1.1.4-r2" + ], + "provides": [ + "pc:snappy=1.1.4" + ] + }, + "libpaper-doc": { + "versions": { + "1.1.24-r3": 1509465360 + }, + "origin": "libpaper" + }, + "popt-doc": { + "versions": { + "1.16-r7": 1509462411 + }, + "origin": "popt" + }, + "collectd-postgresql": { + "versions": { + "5.7.2-r0": 1510069646 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "perl-net-dns": { + "versions": { + "1.13-r0": 1509469031 + }, + "origin": "perl-net-dns", + "dependencies": [ + "perl" + ] + }, + "gettext-dev": { + "versions": { + "0.19.8.1-r1": 1509459196 + }, + "origin": "gettext", + "dependencies": [ + "gettext-asprintf=0.19.8.1-r1", + "gettext-libs=0.19.8.1-r1", + "gettext=0.19.8.1-r1", + "libintl=0.19.8.1-r1" + ] + }, + "gtk2-xfce-engine": { + "versions": { + "3.2.0-r2": 1510920446 + }, + "origin": "gtk-xfce-engine", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "pianobar": { + "versions": { + "2017.08.30-r0": 1510074140 + }, + "origin": "pianobar", + "dependencies": [ + "so:libao.so.4", + "so:libavcodec.so.57", + "so:libavfilter.so.6", + "so:libavformat.so.57", + "so:libavutil.so.55", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libgcrypt.so.20", + "so:libjson-c.so.2" + ], + "provides": [ + "cmd:pianobar" + ] + }, + "audit-dev": { + "versions": { + "2.7.7-r1": 1509491478 + }, + "origin": "audit", + "dependencies": [ + "linux-headers", + "audit-libs=2.7.7-r1" + ] + }, + "glib-networking-lang": { + "versions": { + "2.54.1-r0": 1509477723 + }, + "origin": "glib-networking", + "dependencies": [ + "ca-certificates" + ] + }, + "bluez-hid2hci": { + "versions": { + "5.47-r3": 1510069787 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ] + }, + "s6-linux-init-dev": { + "versions": { + "0.3.1.0-r0": 1509492622 + }, + "origin": "s6-linux-init" + }, + "perl-path-class": { + "versions": { + "0.37-r0": 1509480566 + }, + "origin": "perl-path-class" + }, + "hicolor-icon-theme": { + "versions": { + "0.17-r0": 1509465966 + }, + "origin": "hicolor-icon-theme" + }, + "wayland": { + "versions": { + "1.14.0-r2": 1510066937 + }, + "origin": "wayland", + "dependencies": [ + "wayland-libs-client", + "wayland-libs-cursor", + "wayland-libs-server" + ] + }, + "python2": { + "versions": { + "2.7.15-r2": 1534944323 + }, + "origin": "python2", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libexpat.so.1", + "so:libffi.so.6", + "so:libgdbm_compat.so.4", + "so:libncursesw.so.6", + "so:libpanelw.so.6", + "so:libreadline.so.7", + "so:libsqlite3.so.0", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "python=2.7.15-r2", + "so:libpython2.7.so.1.0=1.0", + "cmd:idle", + "cmd:pydoc", + "cmd:python", + "cmd:python2", + "cmd:python2.7", + "cmd:smtpd.py" + ] + }, + "perl-capture-tiny-doc": { + "versions": { + "0.46-r0": 1509485666 + }, + "origin": "perl-capture-tiny" + }, + "perl-text-soundex": { + "versions": { + "3.05-r0": 1509470837 + }, + "origin": "perl-text-soundex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-sphinx": { + "versions": { + "1.6.5-r0": 1509480687 + }, + "origin": "py-sphinx", + "dependencies": [ + "make", + "py2-docutils", + "py2-jinja2", + "py2-pygments", + "py2-six", + "py2-sphinx_rtd_theme", + "py2-alabaster<0.8", + "py2-babel", + "py2-snowballstemmer", + "py2-imagesize", + "py2-requests", + "py2-sphinxcontrib-websupport", + "py2-typing", + "python2" + ], + "provides": [ + "cmd:sphinx-apidoc-2", + "cmd:sphinx-autogen-2", + "cmd:sphinx-build-2", + "cmd:sphinx-quickstart-2" + ] + }, + "libattr": { + "versions": { + "2.4.47-r6": 1509457001 + }, + "origin": "attr", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libattr.so.1=1.1.0" + ] + }, + "qemu-system-microblaze": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-microblaze" + ] + }, + "libunwind-doc": { + "versions": { + "1.2.1-r1": 1509493800 + }, + "origin": "libunwind" + }, + "libksba-doc": { + "versions": { + "1.3.5-r0": 1509472891 + }, + "origin": "libksba" + }, + "lua-dbi": { + "versions": { + "0.6-r1": 1511483403 + }, + "origin": "lua-dbi" + }, + "libuuid": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libuuid.so.1=1.3.0" + ] + }, + "qemu-system-mipsel": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-mipsel" + ] + }, + "lua-hashids": { + "versions": { + "1.0.6-r1": 1509480766 + }, + "origin": "lua-hashids" + }, + "libgpg-error-doc": { + "versions": { + "1.27-r1": 1509459212 + }, + "origin": "libgpg-error" + }, + "perl-cache-simple-timedexpiry": { + "versions": { + "0.27-r1": 1510564524 + }, + "origin": "perl-cache-simple-timedexpiry", + "dependencies": [ + "perl" + ] + }, + "libatomic_ops-dev": { + "versions": { + "7.4.8-r0": 1509878778 + }, + "origin": "libatomic_ops", + "dependencies": [ + "libatomic_ops=7.4.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:atomic_ops=7.4.8" + ] + }, + "rest": { + "versions": { + "0.8.1-r0": 1509492761 + }, + "origin": "rest", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libsoup-2.4.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "so:librest-0.7.so.0=0.0.0", + "so:librest-extras-0.7.so.0=0.0.0" + ] + }, + "elinks-doc": { + "versions": { + "0.13-r4": 1510261265 + }, + "origin": "elinks" + }, + "tmux": { + "versions": { + "2.6-r0": 1509469998 + }, + "origin": "tmux", + "dependencies": [ + "ncurses-terminfo", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:tmux" + ] + }, + "pacman-doc": { + "versions": { + "5.0.2-r1": 1510588357 + }, + "origin": "pacman" + }, + "libxxf86misc-doc": { + "versions": { + "1.0.3-r1": 1509473661 + }, + "origin": "libxxf86misc" + }, + "clang-libs": { + "versions": { + "5.0.0-r0": 1510683247 + }, + "origin": "clang", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libclang.so.5.0=5.0" + ] + }, + "libdvdcss": { + "versions": { + "1.4.0-r1": 1509480289 + }, + "origin": "libdvdcss", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdvdcss.so.2=2.2.0" + ] + }, + "libevdev-doc": { + "versions": { + "1.5.7-r1": 1509475753 + }, + "origin": "libevdev" + }, + "bluez-deprecated": { + "versions": { + "5.47-r3": 1510069787 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libreadline.so.7", + "so:libudev.so.1" + ], + "provides": [ + "cmd:ciptool", + "cmd:gatttool", + "cmd:hciattach", + "cmd:hciconfig", + "cmd:hcidump", + "cmd:hcitool", + "cmd:rfcomm", + "cmd:sdptool" + ] + }, + "squashfs-tools": { + "versions": { + "4.3-r4": 1511196129 + }, + "origin": "squashfs-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblz4.so.1", + "so:liblzma.so.5", + "so:liblzo2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:mksquashfs", + "cmd:unsquashfs" + ] + }, + "mailcap": { + "versions": { + "2.1.48-r0": 1509474793 + }, + "origin": "mailcap" + }, + "bmd-tools": { + "versions": { + "1.0.2-r0": 1527191317 + }, + "origin": "bmd-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "cmd:bmd-extractfw", + "cmd:bmd-streamer" + ] + }, + "lang": { + "versions": { + "0.1-r0": 1509480915 + }, + "origin": "lang" + }, + "acf-freeswitch": { + "versions": { + "0.8.0-r2": 1510585376 + }, + "origin": "acf-freeswitch", + "dependencies": [ + "acf-core", + "freeswitch", + "lua-xml" + ] + }, + "slibtool": { + "versions": { + "0.5.17-r1": 1510843235 + }, + "origin": "slibtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:clibtool", + "cmd:clibtool-shared", + "cmd:clibtool-static", + "cmd:dlibtool", + "cmd:dlibtool-shared", + "cmd:dlibtool-static", + "cmd:slibtool", + "cmd:slibtool-shared", + "cmd:slibtool-static" + ] + }, + "libdaemon": { + "versions": { + "0.14-r2": 1509465393 + }, + "origin": "libdaemon", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdaemon.so.0=0.5.0" + ] + }, + "libgnome": { + "versions": { + "2.32.1-r7": 1510931516 + }, + "origin": "libgnome", + "dependencies": [ + "/bin/sh", + "so:libORBit-2.so.0", + "so:libbonobo-2.so.0", + "so:libbonobo-activation.so.4", + "so:libc.musl-x86_64.so.1", + "so:libcanberra.so.0", + "so:libgconf-2.so.4", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnomevfs-2.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libintl.so.8", + "so:libpopt.so.0" + ], + "provides": [ + "so:libgnome-2.so.0=0.3200.1", + "cmd:gnome-open" + ] + }, + "ghostscript-gtk": { + "versions": { + "9.26-r2": 1554362768, + "9.26-r3": 1565700439, + "9.26-r4": 1571234493 + }, + "origin": "ghostscript", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgs.so.9", + "so:libgtk-3.so.0" + ], + "provides": [ + "cmd:gsx" + ] + }, + "perl-net-openssh-doc": { + "versions": { + "0.74-r0": 1509473012 + }, + "origin": "perl-net-openssh" + }, + "perl-xml-namespacesupport": { + "versions": { + "1.12-r0": 1509475902 + }, + "origin": "perl-xml-namespacesupport", + "dependencies": [ + "perl" + ] + }, + "cmake-doc": { + "versions": { + "3.9.5-r0": 1509907172 + }, + "origin": "cmake" + }, + "varnish": { + "versions": { + "5.2.1-r0": 1511265090 + }, + "origin": "varnish", + "dependencies": [ + "gcc", + "libc-dev", + "libgcc", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libedit.so.0", + "so:libexecinfo.so.1", + "so:libncursesw.so.6", + "so:libpcre.so.1", + "so:libvarnishapi.so.1" + ], + "provides": [ + "cmd:varnishadm", + "cmd:varnishd", + "cmd:varnishhist", + "cmd:varnishlog", + "cmd:varnishncsa", + "cmd:varnishstat", + "cmd:varnishtest", + "cmd:varnishtop" + ] + }, + "jsoncpp": { + "versions": { + "1.8.1-r1": 1510311447 + }, + "origin": "jsoncpp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libjsoncpp.so.0=0.0.0" + ] + }, + "eggdrop-gseen": { + "versions": { + "1.6.21-r2": 1509491904 + }, + "origin": "eggdrop", + "dependencies": [ + "tcl", + "so:libc.musl-x86_64.so.1", + "so:libtcl8.6.so" + ] + }, + "pinentry-doc": { + "versions": { + "1.0.0-r0": 1510587941 + }, + "origin": "pinentry" + }, + "lua5.2-lyaml": { + "versions": { + "6.1.3-r1": 1509473485 + }, + "origin": "lua-lyaml", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libyaml-0.so.2" + ] + }, + "perl-package-stash-xs-doc": { + "versions": { + "0.28-r4": 1509474037 + }, + "origin": "perl-package-stash-xs" + }, + "py3-cryptography": { + "versions": { + "2.0.3-r1": 1510288190, + "2.1.4-r0": 1559543208 + }, + "origin": "py-cryptography", + "dependencies": [ + "py3-cffi", + "py3-idna", + "py3-asn1crypto", + "py3-six", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpython3.6m.so.1.0", + "so:libssl.so.44" + ] + }, + "libarchive": { + "versions": { + "3.3.2-r2": 1510258000, + "3.3.3-r0": 1566312165, + "3.3.3-r1": 1572677952 + }, + "origin": "libarchive", + "dependencies": [ + "so:libacl.so.1", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:liblz4.so.1", + "so:liblzma.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libarchive.so.13=13.3.3" + ] + }, + "lzo-doc": { + "versions": { + "2.10-r2": 1509462188 + }, + "origin": "lzo" + }, + "vim-doc": { + "versions": { + "8.0.1359-r0": 1512029967, + "8.0.1359-r1": 1559759710, + "8.0.1359-r2": 1561188677 + }, + "origin": "vim" + }, + "brlaser-doc": { + "versions": { + "3-r0": 1510075894 + }, + "origin": "brlaser" + }, + "xf86-input-mouse-dev": { + "versions": { + "1.9.2-r0": 1510074097 + }, + "origin": "xf86-input-mouse", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xorg-mouse=1.9.2" + ] + }, + "avahi-ui-dev": { + "versions": { + "0.6.31-r7": 1510076322 + }, + "origin": "avahi-ui", + "dependencies": [ + "gtk+3.0-dev", + "gtk+2.0-dev", + "gdbm-dev", + "avahi-dev", + "avahi-ui-gtk3=0.6.31-r7", + "avahi-ui=0.6.31-r7", + "pc:avahi-client", + "pc:avahi-glib", + "pc:gtk+-2.0", + "pc:gtk+-3.0", + "pkgconfig" + ], + "provides": [ + "pc:avahi-ui-gtk3=0.6.31", + "pc:avahi-ui=0.6.31" + ] + }, + "libxfixes-doc": { + "versions": { + "5.0.3-r1": 1509464684 + }, + "origin": "libxfixes" + }, + "libxcomposite-dev": { + "versions": { + "0.4.4-r1": 1509465984 + }, + "origin": "libxcomposite", + "dependencies": [ + "libxext-dev", + "libxcomposite=0.4.4-r1", + "pc:compositeproto>=0.4", + "pc:x11", + "pc:xfixes", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xcomposite=0.4.4" + ] + }, + "dnsmasq-dnssec": { + "versions": { + "2.78-r3": 1540537793 + }, + "origin": "dnsmasq", + "dependencies": [ + "!dnsmasq", + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libhogweed.so.4", + "so:libnettle.so.6" + ], + "provides": [ + "cmd:dnsmasq" + ] + }, + "perl-hash-multivalue": { + "versions": { + "0.16-r0": 1511871381 + }, + "origin": "perl-hash-multivalue", + "dependencies": [ + "perl" + ] + }, + "lua5.3-rex-posix": { + "versions": { + "2.9.0-r0": 1510619366 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "bind-doc": { + "versions": { + "9.11.6_p1-r1": 1556872767, + "9.11.8-r0": 1561323229 + }, + "origin": "bind" + }, + "xdpyinfo": { + "versions": { + "1.3.2-r0": 1509491068 + }, + "origin": "xdpyinfo", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXi.so.6", + "so:libXtst.so.6", + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "cmd:xdpyinfo" + ] + }, + "installkernel": { + "versions": { + "3.5-r0": 1509477809 + }, + "origin": "installkernel", + "provides": [ + "cmd:installkernel" + ] + }, + "eudev": { + "versions": { + "3.2.4-r1": 1509466081 + }, + "origin": "eudev", + "dependencies": [ + "udev-init-scripts", + "eudev-libs=3.2.4-r1", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libkmod.so.2" + ], + "provides": [ + "udev=176", + "cmd:setup-udev", + "cmd:udevadm", + "cmd:udevd" + ] + }, + "libpcre2-16": { + "versions": { + "10.30-r0": 1509461774 + }, + "origin": "pcre2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre2-16.so.0=0.6.0" + ] + }, + "font-bitstream-75dpi": { + "versions": { + "1.0.3-r0": 1509480923 + }, + "origin": "font-bitstream-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "rxvt-unicode-terminfo": { + "versions": { + "9.22-r2": 1509476789 + }, + "origin": "rxvt-unicode", + "dependencies": [ + "rxvt-unicode-terminfo" + ] + }, + "gphoto2-lang": { + "versions": { + "2.5.14-r0": 1509493216 + }, + "origin": "gphoto2" + }, + "gvfs-lang": { + "versions": { + "1.34.1-r0": 1511430257, + "1.34.1-r1": 1563787223 + }, + "origin": "gvfs" + }, + "git-cvs": { + "versions": { + "2.15.3-r0": 1540401293, + "2.15.4-r0": 1576015190 + }, + "origin": "git", + "dependencies": [ + "perl", + "perl-git=2.15.4-r0", + "cvs", + "perl-dbd-sqlite" + ], + "provides": [ + "cmd:git-cvsserver" + ] + }, + "ulogd-doc": { + "versions": { + "2.0.5-r4": 1509480398 + }, + "origin": "ulogd" + }, + "perl-dist-checkconflicts": { + "versions": { + "0.11-r0": 1509474029 + }, + "origin": "perl-dist-checkconflicts", + "dependencies": [ + "perl-list-moreutils", + "perl-module-runtime" + ] + }, + "libxres-doc": { + "versions": { + "1.2.0-r0": 1509468563 + }, + "origin": "libxres" + }, + "gst-plugins-bad-doc": { + "versions": { + "1.12.3-r2": 1512039893 + }, + "origin": "gst-plugins-bad" + }, + "xtables-addons-doc": { + "versions": { + "2.11-r1": 1509469987 + }, + "origin": "xtables-addons" + }, + "libc6-compat": { + "versions": { + "1.1.18-r3": 1518031143, + "1.1.18-r4": 1565163296 + }, + "origin": "musl", + "dependencies": [ + "musl=1.1.18-r4" + ] + }, + "py3-babel": { + "versions": { + "2.5.1-r0": 1509490097 + }, + "origin": "py-babel", + "dependencies": [ + "py3-tz", + "python3" + ], + "provides": [ + "cmd:pybabel" + ] + }, + "openbox-gnome": { + "versions": { + "3.6.1-r1": 1510073885 + }, + "origin": "openbox", + "dependencies": [ + "so:libX11.so.6", + "so:libXau.so.6", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "cmd:gdm-control", + "cmd:gnome-panel-control", + "cmd:openbox-gnome-session" + ] + }, + "libffi": { + "versions": { + "3.2.1-r4": 1509458693 + }, + "origin": "libffi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libffi.so.6=6.0.4" + ] + }, + "m4": { + "versions": { + "1.4.18-r0": 1509456654 + }, + "origin": "m4", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:m4" + ] + }, + "lua5.3-sircbot": { + "versions": { + "0.4-r1": 1509492359 + }, + "origin": "sircbot", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "abiword-plugin-openwriter": { + "versions": { + "3.0.2-r1": 1510073365 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "lua5.1-dbi-mysql": { + "versions": { + "0.6-r1": 1511483402 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "libetpan-doc": { + "versions": { + "1.8-r2": 1509721604 + }, + "origin": "libetpan" + }, + "argp-standalone": { + "versions": { + "1.3-r2": 1509477695 + }, + "origin": "argp-standalone" + }, + "graphviz": { + "versions": { + "2.40.1-r0": 1510066920 + }, + "origin": "graphviz", + "dependencies": [ + "/bin/sh", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libltdl.so.7", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libcdt.so.5=5.0.0", + "so:libcgraph.so.6=6.0.0", + "so:libgvc.so.6=6.0.0", + "so:libgvplugin_core.so.6=6.0.0", + "so:libgvplugin_dot_layout.so.6=6.0.0", + "so:libgvplugin_neato_layout.so.6=6.0.0", + "so:libgvplugin_pango.so.6=6.0.0", + "so:libgvplugin_xlib.so.6=6.0.0", + "so:libgvpr.so.2=2.0.0", + "so:liblab_gamut.so.1=1.0.0", + "so:libpathplan.so.4=4.0.0", + "so:libxdot.so.4=4.0.0", + "cmd:acyclic", + "cmd:bcomps", + "cmd:ccomps", + "cmd:circo", + "cmd:cluster", + "cmd:dijkstra", + "cmd:dot", + "cmd:dot2gxl", + "cmd:dot_builtins", + "cmd:dotty", + "cmd:edgepaint", + "cmd:fdp", + "cmd:gc", + "cmd:gml2gv", + "cmd:graphml2gv", + "cmd:gv2gml", + "cmd:gv2gxl", + "cmd:gvcolor", + "cmd:gvgen", + "cmd:gvmap", + "cmd:gvmap.sh", + "cmd:gvpack", + "cmd:gvpr", + "cmd:gxl2dot", + "cmd:gxl2gv", + "cmd:lneato", + "cmd:mm2gv", + "cmd:neato", + "cmd:nop", + "cmd:osage", + "cmd:patchwork", + "cmd:prune", + "cmd:sccmap", + "cmd:sfdp", + "cmd:tred", + "cmd:twopi", + "cmd:unflatten", + "cmd:vimdot" + ] + }, + "fontconfig-doc": { + "versions": { + "2.12.6-r0": 1509462083 + }, + "origin": "fontconfig" + }, + "dahdi-tools-doc": { + "versions": { + "2.11.1-r0": 1509476155 + }, + "origin": "dahdi-tools" + }, + "acf-nsd": { + "versions": { + "0.0.1-r0": 1510073459 + }, + "origin": "acf-nsd", + "dependencies": [ + "acf-core", + "nsd" + ] + }, + "libvncserver": { + "versions": { + "0.9.11-r2": 1533743207, + "0.9.11-r3": 1572818864 + }, + "origin": "libvncserver", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libz.so.1" + ], + "provides": [ + "so:libvncclient.so.1=1.0.0", + "so:libvncserver.so.1=1.0.0" + ] + }, + "lua5.1-pty": { + "versions": { + "1.2.1-r1": 1509496423 + }, + "origin": "lua-pty", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "py3-newt": { + "versions": { + "0.52.20-r0": 1509476145 + }, + "origin": "newt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnewt.so.0.52" + ] + }, + "mtools-doc": { + "versions": { + "4.0.18-r2": 1509462431 + }, + "origin": "mtools" + }, + "lksctp-tools-dev": { + "versions": { + "1.0.17-r0": 1509492326 + }, + "origin": "lksctp-tools", + "dependencies": [ + "lksctp-tools=1.0.17-r0", + "pkgconfig" + ], + "provides": [ + "pc:libsctp=1.0.17" + ] + }, + "xf86vidmodeproto": { + "versions": { + "2.3.1-r3": 1509466231 + }, + "origin": "xf86vidmodeproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xf86vidmodeproto=2.3.1" + ] + }, + "lua5.3-sec": { + "versions": { + "0.6-r3": 1510260656 + }, + "origin": "lua-sec", + "dependencies": [ + "lua5.3-socket", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "exo-lang": { + "versions": { + "0.11.5-r0": 1510068072 + }, + "origin": "exo", + "dependencies": [ + "hicolor-icon-theme" + ] + }, + "transmission-lang": { + "versions": { + "2.92-r8": 1510932986 + }, + "origin": "transmission" + }, + "mpt-status": { + "versions": { + "1.2.0-r0": 1509488650 + }, + "origin": "mpt-status", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mpt-status" + ] + }, + "perl-class-accessor-doc": { + "versions": { + "0.34-r0": 1509474097 + }, + "origin": "perl-class-accessor" + }, + "nfs-utils-dbg": { + "versions": { + "2.1.1-r4": 1509492107 + }, + "origin": "nfs-utils", + "dependencies": [ + "rpcbind" + ] + }, + "lua5.3-crypto": { + "versions": { + "0.3.2-r5": 1510261401 + }, + "origin": "lua-crypto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "uwsgi-emperor_zeromq": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libzmq.so.5" + ] + }, + "sdl2_image-dev": { + "versions": { + "2.0.2-r1": 1528280868, + "2.0.5-r0": 1568803593 + }, + "origin": "sdl2_image", + "dependencies": [ + "pc:sdl2>=2.0.8", + "pkgconfig", + "sdl2_image=2.0.5-r0" + ], + "provides": [ + "pc:SDL2_image=2.0.5" + ] + }, + "uwsgi-transformation_chunked": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libxcursor-dev": { + "versions": { + "1.1.15-r0": 1511975691 + }, + "origin": "libxcursor", + "dependencies": [ + "libxcursor=1.1.15-r0", + "pc:x11", + "pc:xfixes", + "pc:xproto", + "pc:xrender", + "pkgconfig" + ], + "provides": [ + "pc:xcursor=1.1.15" + ] + }, + "py-dbus": { + "versions": { + "1.2.0-r2": 1509465384 + }, + "origin": "py-dbus", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libglib-2.0.so.0" + ] + }, + "libxvmc": { + "versions": { + "1.0.10-r1": 1509467179 + }, + "origin": "libxvmc", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXvMC.so.1=1.0.0", + "so:libXvMCW.so.1=1.0.0" + ] + }, + "libxslt-doc": { + "versions": { + "1.1.31-r1": 1555487793, + "1.1.31-r2": 1572541164 + }, + "origin": "libxslt" + }, + "expect-doc": { + "versions": { + "5.45-r4": 1509489557 + }, + "origin": "expect" + }, + "gvfs-afp": { + "versions": { + "1.34.1-r0": 1511430257, + "1.34.1-r1": 1563787223 + }, + "origin": "gvfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgvfscommon.so", + "so:libgvfsdaemon.so", + "so:libintl.so.8" + ] + }, + "haproxy-systemd-wrapper": { + "versions": { + "1.7.9-r1": 1510261284 + }, + "origin": "haproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:haproxy-systemd-wrapper" + ] + }, + "nginx-mod-http-image-filter": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ] + }, + "xproto": { + "versions": { + "7.0.31-r1": 1509461806 + }, + "origin": "xproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xproto=7.0.31" + ] + }, + "font-cursor-misc": { + "versions": { + "1.0.3-r1": 1509473684 + }, + "origin": "font-cursor-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig", + "util-macros" + ] + }, + "atf-doc": { + "versions": { + "0.21-r1": 1509459730 + }, + "origin": "atf" + }, + "glibmm-doc": { + "versions": { + "2.50.1-r0": 1509472979 + }, + "origin": "glibmm" + }, + "imake-doc": { + "versions": { + "1.0.7-r1": 1509476556 + }, + "origin": "imake" + }, + "lua5.3-doc": { + "versions": { + "5.3.5-r2": 1557163184 + }, + "origin": "lua5.3" + }, + "setup-box-doc": { + "versions": { + "1.0.1-r0": 1509490944 + }, + "origin": "setup-box", + "dependencies": [ + "jq" + ] + }, + "libical-dev": { + "versions": { + "2.0.0-r2": 1509468578 + }, + "origin": "libical", + "dependencies": [ + "libical=2.0.0-r2", + "pkgconfig" + ], + "provides": [ + "pc:libical=2.0" + ] + }, + "abcde": { + "versions": { + "2.8.1-r1": 1509551899 + }, + "origin": "abcde", + "dependencies": [ + "bash", + "cd-discid", + "py-eyed3" + ], + "provides": [ + "cmd:abcde", + "cmd:abcde-musicbrainz-tool", + "cmd:cddb-tool" + ] + }, + "perl-module-install-doc": { + "versions": { + "1.18-r0": 1510352967 + }, + "origin": "perl-module-install" + }, + "libedit": { + "versions": { + "20170329.3.1-r3": 1509461725 + }, + "origin": "libedit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "so:libedit.so.0=0.0.56" + ] + }, + "startup-notification": { + "versions": { + "0.12-r3": 1509468554 + }, + "origin": "startup-notification", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libxcb-util.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libstartup-notification-1.so.0=0.0.0" + ] + }, + "hwdata-pnp": { + "versions": { + "0.305-r0": 1509468678 + }, + "origin": "hwdata" + }, + "perl-error-doc": { + "versions": { + "0.17025-r0": 1509461798 + }, + "origin": "perl-error" + }, + "mariadb-common": { + "versions": { + "10.1.38-r1": 1551187990, + "10.1.40-r0": 1560354886, + "10.1.41-r0": 1565163111 + }, + "origin": "mariadb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "hylafax": { + "versions": { + "6.0.6-r4": 1539067243 + }, + "origin": "hylafax", + "dependencies": [ + "ghostscript", + "bash", + "tiff-tools", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libtiff.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libhylafax-6.0.so.6=6", + "cmd:choptest", + "cmd:cqtest", + "cmd:dialtest", + "cmd:edit-faxcover", + "cmd:faxabort", + "cmd:faxaddmodem", + "cmd:faxadduser", + "cmd:faxalter", + "cmd:faxanswer", + "cmd:faxconfig", + "cmd:faxcover", + "cmd:faxcron", + "cmd:faxdeluser", + "cmd:faxgetty", + "cmd:faxinfo", + "cmd:faxlock", + "cmd:faxmail", + "cmd:faxmodem", + "cmd:faxmsg", + "cmd:faxq", + "cmd:faxqclean", + "cmd:faxquit", + "cmd:faxrm", + "cmd:faxsend", + "cmd:faxsetup", + "cmd:faxsetup.bsdi", + "cmd:faxsetup.irix", + "cmd:faxsetup.linux", + "cmd:faxstat", + "cmd:faxstate", + "cmd:faxwatch", + "cmd:hfaxd", + "cmd:hylafax", + "cmd:lockname", + "cmd:ondelay", + "cmd:pagesend", + "cmd:probemodem", + "cmd:recvstats", + "cmd:sendfax", + "cmd:sendpage", + "cmd:tagtest", + "cmd:textfmt", + "cmd:tiffcheck", + "cmd:tsitest", + "cmd:typetest", + "cmd:xferfaxstats" + ] + }, + "lttng-ust-doc": { + "versions": { + "2.10.0-r1": 1509492304 + }, + "origin": "lttng-ust" + }, + "perl-cpanel-json-xs": { + "versions": { + "3.0239-r0": 1510352915 + }, + "origin": "perl-cpanel-json-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cpanel_json_xs" + ] + }, + "iputils": { + "versions": { + "20121221-r8": 1510259681 + }, + "origin": "iputils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.42" + ], + "provides": [ + "cmd:arping", + "cmd:clockdiff", + "cmd:ipg", + "cmd:ping", + "cmd:ping6", + "cmd:rarpd", + "cmd:rdisc", + "cmd:tftpd", + "cmd:tracepath", + "cmd:tracepath6", + "cmd:traceroute6" + ] + }, + "kamailio-memcached": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libmemcached.so.11" + ] + }, + "libproxy-dev": { + "versions": { + "0.4.15-r0": 1509468257 + }, + "origin": "libproxy", + "dependencies": [ + "zlib-dev", + "libproxy=0.4.15-r0", + "pkgconfig" + ], + "provides": [ + "pc:libproxy-1.0=0.4.15" + ] + }, + "flac": { + "versions": { + "1.3.2-r0": 1509473288 + }, + "origin": "flac", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libogg.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libFLAC++.so.6=6.3.0", + "so:libFLAC.so.8=8.3.0", + "cmd:flac", + "cmd:metaflac" + ] + }, + "cd-discid-doc": { + "versions": { + "1.4-r2": 1509551854 + }, + "origin": "cd-discid" + }, + "gsl-dev": { + "versions": { + "2.4-r0": 1509493183 + }, + "origin": "gsl", + "dependencies": [ + "gsl=2.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:gsl=2.4", + "cmd:gsl-config" + ] + }, + "py-paramiko-doc": { + "versions": { + "2.4.2-r0": 1551364324 + }, + "origin": "py-paramiko", + "dependencies": [ + "py-asn1", + "py-cryptography", + "py-bcrypt", + "py-pynacl" + ] + }, + "inputproto": { + "versions": { + "2.3.2-r1": 1509461957 + }, + "origin": "inputproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:inputproto=2.3.2" + ] + }, + "eggdbus-dev": { + "versions": { + "0.6-r5": 1509469812 + }, + "origin": "eggdbus", + "dependencies": [ + "eggdbus=0.6-r5", + "pc:gio-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:eggdbus-1=0.6" + ] + }, + "ethtool": { + "versions": { + "4.13-r0": 1509492545 + }, + "origin": "ethtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ethtool" + ] + }, + "rtmpdump-dev": { + "versions": { + "2.4_git20160909-r3": 1510259117 + }, + "origin": "rtmpdump", + "dependencies": [ + "zlib-dev", + "libressl-dev", + "librtmp=2.4_git20160909-r3", + "pc:libcrypto", + "pc:libssl", + "pkgconfig" + ], + "provides": [ + "pc:librtmp=v2.4" + ] + }, + "openrc-doc": { + "versions": { + "0.24.1-r4": 1511887834 + }, + "origin": "openrc" + }, + "lynx": { + "versions": { + "2.8.8_p2-r6": 1510842477 + }, + "origin": "lynx", + "dependencies": [ + "gzip", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:lynx" + ] + }, + "collectd-write_http": { + "versions": { + "5.7.2-r0": 1510069644 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libyajl.so.2" + ] + }, + "cd-discid": { + "versions": { + "1.4-r2": 1509551855 + }, + "origin": "cd-discid", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cd-discid" + ] + }, + "perl-file-temp-doc": { + "versions": { + "0.2304-r0": 1509495372 + }, + "origin": "perl-file-temp" + }, + "uwsgi-cheaper_backlog2": { + "versions": { + "2.0.17-r0": 1522154653 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "aoetools": { + "versions": { + "37-r0": 1509477686 + }, + "origin": "aoetools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:aoe-discover", + "cmd:aoe-flush", + "cmd:aoe-interfaces", + "cmd:aoe-mkdevs", + "cmd:aoe-mkshelf", + "cmd:aoe-revalidate", + "cmd:aoe-sancheck", + "cmd:aoe-stat", + "cmd:aoe-version", + "cmd:aoecfg", + "cmd:aoeping", + "cmd:coraid-update" + ] + }, + "nagios-plugins-dig": { + "versions": { + "2.2.1-r3": 1510288494 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "bind-tools", + "so:libc.musl-x86_64.so.1" + ] + }, + "libpcap": { + "versions": { + "1.8.1-r1": 1509475964 + }, + "origin": "libpcap", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcap.so.1=1.8.1" + ] + }, + "xfce4-cpugraph-plugin": { + "versions": { + "1.0.5-r1": 1510074087 + }, + "origin": "xfce4-cpugraph-plugin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7" + ] + }, + "sdl2": { + "versions": { + "2.0.7-r3": 1511364667, + "2.0.10-r0": 1564135866 + }, + "origin": "sdl2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL2-2.0.so.0=0.10.0" + ] + }, + "lua-bitlib": { + "versions": { + "5.3.0-r0": 1509468313 + }, + "origin": "lua-bitlib" + }, + "acf-freeradius3": { + "versions": { + "0.3.1-r0": 1510072745 + }, + "origin": "acf-freeradius3", + "dependencies": [ + "acf-core", + "freeradius>3" + ] + }, + "acf-core": { + "versions": { + "0.21.1-r0": 1510068297 + }, + "origin": "acf-core", + "dependencies": [ + "acf-jquery", + "acf-lib", + "acf-skins", + "haserl-lua5.2", + "lua5.2", + "lua5.2-posix", + "lua5.2-md5", + "lua5.2-json4", + "lua5.2-subprocess", + "/bin/sh" + ], + "provides": [ + "cmd:acf-cli", + "cmd:acfpasswd" + ] + }, + "rlog-dev": { + "versions": { + "1.4-r4": 1509475979 + }, + "origin": "rlog", + "dependencies": [ + "pkgconfig", + "rlog=1.4-r4" + ], + "provides": [ + "pc:librlog=1.4" + ] + }, + "qemu-mips": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mips" + ] + }, + "lua5.1-sircbot": { + "versions": { + "0.4-r1": 1509492353 + }, + "origin": "sircbot", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py-future": { + "versions": { + "0.15.2-r4": 1509491798 + }, + "origin": "py-future" + }, + "libisoburn-doc": { + "versions": { + "1.4.8-r0": 1509462217 + }, + "origin": "libisoburn" + }, + "dtach-doc": { + "versions": { + "0.9-r0": 1509475624 + }, + "origin": "dtach" + }, + "collectd-apache": { + "versions": { + "5.7.2-r0": 1510069645 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "nginx-mod-http-headers-more": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "xl2tpd-doc": { + "versions": { + "1.3.10-r0": 1509490846 + }, + "origin": "xl2tpd" + }, + "py3-paramiko": { + "versions": { + "2.4.2-r0": 1551364325 + }, + "origin": "py-paramiko", + "dependencies": [ + "py3-asn1", + "py3-cryptography", + "py3-bcrypt", + "py3-pynacl", + "python3" + ] + }, + "perl-lwp-protocol-https-doc": { + "versions": { + "6.06-r1": 1509495025 + }, + "origin": "perl-lwp-protocol-https" + }, + "perl-extutils-installpaths": { + "versions": { + "0.011-r0": 1509474747 + }, + "origin": "perl-extutils-installpaths", + "dependencies": [ + "perl-extutils-config" + ] + }, + "libwebp-doc": { + "versions": { + "0.6.0-r1": 1509473038 + }, + "origin": "libwebp" + }, + "iwlwifi-5150-ucode": { + "versions": { + "8.24.2.2-r1": 1509475699 + }, + "origin": "iwlwifi-5150-ucode" + }, + "nagios-plugins-snmp": { + "versions": { + "2.2.1-r3": 1510288498 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "net-snmp-tools", + "so:libc.musl-x86_64.so.1" + ] + }, + "xdelta3": { + "versions": { + "3.0.11-r0": 1509491097 + }, + "origin": "xdelta3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xdelta3" + ] + }, + "nsd": { + "versions": { + "4.1.16-r1": 1510260999 + }, + "origin": "nsd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libevent-2.1.so.6", + "so:libssl.so.44" + ], + "provides": [ + "cmd:nsd", + "cmd:nsd-checkconf", + "cmd:nsd-checkzone", + "cmd:nsd-control", + "cmd:nsd-control-setup" + ] + }, + "orc-compiler": { + "versions": { + "0.4.27-r0": 1509469955 + }, + "origin": "orc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liborc-0.4.so.0" + ], + "provides": [ + "cmd:orcc" + ] + }, + "protobuf-c": { + "versions": { + "1.3.0-r3": 1510846239 + }, + "origin": "protobuf-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libprotobuf.so.14", + "so:libprotoc.so.14", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libprotobuf-c.so.1=1.0.0", + "cmd:protoc-c", + "cmd:protoc-gen-c" + ] + }, + "nload-doc": { + "versions": { + "0.7.4-r3": 1509496066 + }, + "origin": "nload" + }, + "gstreamer0.10-lang": { + "versions": { + "0.10.36-r2": 1509470984 + }, + "origin": "gstreamer0.10" + }, + "postfix-doc": { + "versions": { + "3.2.4-r1": 1510285340 + }, + "origin": "postfix" + }, + "irqbalance": { + "versions": { + "1.2.0-r1": 1509492256 + }, + "origin": "irqbalance", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:irqbalance", + "cmd:irqbalance-ui" + ] + }, + "perl-test-without-doc": { + "versions": { + "0.10-r0": 1509496582 + }, + "origin": "perl-test-without" + }, + "py-curl": { + "versions": { + "7.43.0-r4": 1510288123 + }, + "origin": "py-curl" + }, + "newt-doc": { + "versions": { + "0.52.20-r0": 1509476145 + }, + "origin": "newt" + }, + "lua5.1-mqtt-publish": { + "versions": { + "0.1-r0": 1509462497 + }, + "origin": "lua-mqtt-publish", + "dependencies": [ + "lua5.1-mosquitto" + ] + }, + "py-mysqldb": { + "versions": { + "1.2.5-r0": 1509492445 + }, + "origin": "py-mysqldb", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18", + "so:libpython2.7.so.1.0" + ] + }, + "perl-text-template-doc": { + "versions": { + "1.46-r0": 1509491527 + }, + "origin": "perl-text-template" + }, + "pcmciautils": { + "versions": { + "018-r1": 1509482232 + }, + "origin": "pcmciautils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lspcmcia", + "cmd:pccardctl" + ] + }, + "perl-inline": { + "versions": { + "0.80-r0": 1509489231 + }, + "origin": "perl-inline", + "dependencies": [ + "perl" + ] + }, + "perl-encode-locale": { + "versions": { + "1.05-r1": 1509464360 + }, + "origin": "perl-encode-locale", + "dependencies": [ + "perl" + ] + }, + "nginx-doc": { + "versions": { + "1.12.2-r4": 1542814446 + }, + "origin": "nginx" + }, + "dnstop-doc": { + "versions": { + "20140915-r3": 1509485931 + }, + "origin": "dnstop" + }, + "perl-namespace-clean": { + "versions": { + "0.27-r0": 1509474076 + }, + "origin": "perl-namespace-clean", + "dependencies": [ + "perl-package-stash", + "perl-sub-name", + "perl-sub-identify", + "perl-b-hooks-endofscope" + ] + }, + "xfce4-volumed": { + "versions": { + "0.1.13-r1": 1510072212 + }, + "origin": "xfce4-volumed", + "dependencies": [ + "xfce4-mixer", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-0.10.so.0", + "so:libgstinterfaces-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libkeybinder.so.0", + "so:libnotify.so.4", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:xfce4-volumed" + ] + }, + "perl-extutils-cchecker-doc": { + "versions": { + "0.10-r0": 1511889284 + }, + "origin": "perl-extutils-cchecker" + }, + "transmission-daemon": { + "versions": { + "2.92-r8": 1510932986 + }, + "origin": "transmission", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libevent-2.1.so.6", + "so:libintl.so.8", + "so:libz.so.1" + ], + "provides": [ + "cmd:transmission-daemon" + ] + }, + "qca-dev": { + "versions": { + "2.1.3-r4": 1510288320 + }, + "origin": "qca", + "dependencies": [ + "qt-dev", + "pc:QtCore", + "pkgconfig", + "qca=2.1.3-r4" + ], + "provides": [ + "pc:qca2=2.1.3" + ] + }, + "knock": { + "versions": { + "0.7-r1": 1509480798 + }, + "origin": "knock", + "dependencies": [ + "iptables", + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:knock", + "cmd:knockd" + ] + }, + "libnice-gstreamer": { + "versions": { + "0.1.14-r2": 1510931344 + }, + "origin": "libnice", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libnice.so.10" + ] + }, + "perl-encode-utils": { + "versions": { + "2.93-r0": 1511889458 + }, + "origin": "perl-encode", + "dependencies": [ + "perl-encode", + "perl" + ], + "provides": [ + "perl-encode-piconv", + "cmd:encguess", + "cmd:piconv" + ] + }, + "libxml2-dev": { + "versions": { + "2.9.8-r1": 1540398579 + }, + "origin": "libxml2", + "dependencies": [ + "zlib-dev", + "libxml2=2.9.8-r1", + "pkgconfig" + ], + "provides": [ + "pc:libxml-2.0=2.9.8", + "cmd:xml2-config" + ] + }, + "lua-sec-doc": { + "versions": { + "0.6-r3": 1510260656 + }, + "origin": "lua-sec" + }, + "lftp": { + "versions": { + "4.8.3-r1": 1510259189 + }, + "origin": "lftp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libncursesw.so.6", + "so:libreadline.so.7", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:cmd-mirror.so=0", + "so:cmd-sleep.so=0", + "so:cmd-torrent.so=0", + "so:liblftp-jobs.so.0=0.0.0", + "so:liblftp-network.so=0", + "so:liblftp-pty.so=0", + "so:liblftp-tasks.so.0=0.0.0", + "so:proto-file.so=0", + "so:proto-fish.so=0", + "so:proto-ftp.so=0", + "so:proto-http.so=0", + "so:proto-sftp.so=0", + "cmd:lftp", + "cmd:lftpget" + ] + }, + "slony1": { + "versions": { + "2.2.6-r0": 1509491940 + }, + "origin": "slony1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "cmd:slon", + "cmd:slonik", + "cmd:slony_logshipper" + ] + }, + "perl-text-autoformat-doc": { + "versions": { + "1.669004-r0": 1509475841 + }, + "origin": "perl-text-autoformat" + }, + "lua5.2-penlight": { + "versions": { + "1.5.4-r0": 1509485591 + }, + "origin": "lua-penlight", + "dependencies": [ + "lua-penlight-shared", + "lua5.2-filesystem", + "lua-penlight-shared=1.5.4-r0" + ] + }, + "linux-headers": { + "versions": { + "4.4.6-r2": 1509457104 + }, + "origin": "linux-headers" + }, + "strace": { + "versions": { + "4.19-r0": 1509477631 + }, + "origin": "strace", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:strace", + "cmd:strace-graph", + "cmd:strace-log-merge" + ] + }, + "x264": { + "versions": { + "20170930-r0": 1509473602 + }, + "origin": "x264", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:x264" + ] + }, + "valgrind-dev": { + "versions": { + "3.12.0-r1": 1509494839 + }, + "origin": "valgrind", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:valgrind=3.12.0" + ] + }, + "uwsgi-spooler": { + "versions": { + "2.0.17-r0": 1522154659 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libsexy-doc": { + "versions": { + "0.1.11-r7": 1510072498 + }, + "origin": "libsexy" + }, + "hostapd-doc": { + "versions": { + "2.6-r3": 1510261213, + "2.6-r4": 1559723195, + "2.6-r5": 1559724985, + "2.6-r6": 1568724303 + }, + "origin": "hostapd" + }, + "perl-net-rblclient-doc": { + "versions": { + "0.5-r2": 1509479744 + }, + "origin": "perl-net-rblclient" + }, + "xen-dev": { + "versions": { + "4.9.4-r0": 1551280495, + "4.9.4-r1": 1558103152 + }, + "origin": "xen", + "dependencies": [ + "libressl-dev", + "python2-dev", + "e2fsprogs-dev", + "gettext", + "zlib-dev", + "ncurses-dev", + "dev86", + "texinfo", + "perl", + "pciutils-dev", + "glib-dev", + "yajl-dev", + "libnl3-dev", + "spice-dev", + "gnutls-dev", + "curl-dev", + "libaio-dev", + "lzo-dev", + "xz-dev", + "util-linux-dev", + "e2fsprogs-dev", + "linux-headers", + "argp-standalone", + "perl-dev", + "xen-libs=4.9.4-r1" + ] + }, + "rrsync": { + "versions": { + "3.1.3-r0": 1521547982 + }, + "origin": "rsync", + "dependencies": [ + "rsync", + "perl" + ], + "provides": [ + "cmd:rrsync" + ] + }, + "libcanberra-gtk2": { + "versions": { + "0.30-r1": 1510071976 + }, + "origin": "libcanberra", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcanberra.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ], + "provides": [ + "so:libcanberra-gtk.so.0=0.1.9" + ] + }, + "uwsgi-fastrouter": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "nfprofile": { + "versions": { + "1.6.15-r0": 1509494469, + "1.6.15-r1": 1574265426 + }, + "origin": "nfdump", + "dependencies": [ + "nfdump", + "so:libc.musl-x86_64.so.1", + "so:libnfdump-1.6.15.so", + "so:librrd.so.4" + ], + "provides": [ + "cmd:nfprofile" + ] + }, + "perl-package-anon": { + "versions": { + "0.05-r4": 1509489338 + }, + "origin": "perl-package-anon", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "bluez-dev": { + "versions": { + "5.47-r3": 1510069786 + }, + "origin": "bluez", + "dependencies": [ + "bluez-libs=5.47-r3", + "pkgconfig" + ], + "provides": [ + "pc:bluez=5.47" + ] + }, + "qemu-system-unicore32": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-unicore32" + ] + }, + "nagios-plugins-rpc": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins" + ] + }, + "perl-b-hooks-endofscope": { + "versions": { + "0.21-r0": 1509474011 + }, + "origin": "perl-b-hooks-endofscope", + "dependencies": [ + "perl-sub-exporter-progressive", + "perl-module-runtime", + "perl-module-implementation", + "perl-variable-magic" + ] + }, + "acf-dhcp": { + "versions": { + "0.9.1-r0": 1510076340 + }, + "origin": "acf-dhcp", + "dependencies": [ + "acf-core", + "dhcp" + ] + }, + "perl-image-exiftool": { + "versions": { + "10.55-r0": 1509495133 + }, + "origin": "perl-image-exiftool", + "dependencies": [ + "perl" + ] + }, + "perl-file-rsync": { + "versions": { + "0.74-r1": 1509473505 + }, + "origin": "perl-file-rsync", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "binutils": { + "versions": { + "2.30-r1": 1527754982, + "2.30-r2": 1565788944 + }, + "origin": "binutils", + "dependencies": [ + "so:libbfd-2.30.so", + "so:libc.musl-x86_64.so.1", + "so:libopcodes-2.30.so", + "so:libz.so.1" + ], + "provides": [ + "cmd:addr2line", + "cmd:ar", + "cmd:as", + "cmd:c++filt", + "cmd:dwp", + "cmd:elfedit", + "cmd:gprof", + "cmd:ld", + "cmd:ld.bfd", + "cmd:nm", + "cmd:objcopy", + "cmd:objdump", + "cmd:ranlib", + "cmd:readelf", + "cmd:size", + "cmd:strings", + "cmd:strip" + ] + }, + "fail2ban": { + "versions": { + "0.10.1-r0": 1509473544 + }, + "origin": "fail2ban", + "dependencies": [ + "python2", + "iptables", + "ip6tables", + "logrotate" + ], + "provides": [ + "cmd:fail2ban-client", + "cmd:fail2ban-python", + "cmd:fail2ban-regex", + "cmd:fail2ban-server", + "cmd:fail2ban-testcases" + ] + }, + "libgnomekbd-dev": { + "versions": { + "3.6.0-r0": 1510933015 + }, + "origin": "libgnomekbd", + "dependencies": [ + "gtk+3.0-dev", + "gconf-dev", + "libxklavier-dev", + "libxml2-dev", + "gobject-introspection-dev", + "libxcursor-dev", + "libxcomposite-dev", + "libxi-dev", + "libxau-dev", + "libxdmcp-dev", + "libxext-dev", + "libxcb-dev", + "libgnomekbd=3.6.0-r0", + "pc:gdk-3.0>=2.91.7", + "pc:gmodule-export-2.0", + "pc:libxklavier>=5.2", + "pkgconfig" + ], + "provides": [ + "pc:libgnomekbd=3.6.0", + "pc:libgnomekbdui=3.6.0" + ] + }, + "zfs-libs": { + "versions": { + "0.7.3-r0": 1509490074 + }, + "origin": "zfs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libtirpc.so.3", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libnvpair.so.1=1.0.1", + "so:libuutil.so.1=1.0.1", + "so:libzfs.so.2=2.0.0", + "so:libzfs_core.so.1=1.0.0", + "so:libzpool.so.2=2.0.0" + ] + }, + "libgnome-lang": { + "versions": { + "2.32.1-r7": 1510931516 + }, + "origin": "libgnome" + }, + "ser2net-doc": { + "versions": { + "3.4-r1": 1509492427 + }, + "origin": "ser2net" + }, + "perl-sub-install": { + "versions": { + "0.928-r0": 1509473967 + }, + "origin": "perl-sub-install", + "dependencies": [ + "perl" + ] + }, + "py-dbus-dev": { + "versions": { + "1.2.0-r2": 1509465380 + }, + "origin": "py-dbus", + "dependencies": [ + "py-dbus", + "pc:dbus-1>=1.0", + "pkgconfig" + ], + "provides": [ + "pc:dbus-python=1.2.0" + ] + }, + "xf86-input-mouse-doc": { + "versions": { + "1.9.2-r0": 1510074096 + }, + "origin": "xf86-input-mouse" + }, + "s6-dns-doc": { + "versions": { + "2.2.0.1-r0": 1509480281 + }, + "origin": "s6-dns" + }, + "nettle-utils": { + "versions": { + "3.3-r0": 1509465115 + }, + "origin": "nettle", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libhogweed.so.4", + "so:libnettle.so.6" + ], + "provides": [ + "cmd:nettle-hash", + "cmd:nettle-lfib-stream", + "cmd:nettle-pbkdf2", + "cmd:pkcs1-conv", + "cmd:sexp-conv" + ] + }, + "xmlrpc-c-dev": { + "versions": { + "1.39.11-r0": 1509482028 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "libxml2-dev", + "xmlrpc-c++=1.39.11-r0", + "xmlrpc-c-abyss=1.39.11-r0", + "xmlrpc-c-client++=1.39.11-r0", + "xmlrpc-c-client=1.39.11-r0", + "xmlrpc-c=1.39.11-r0" + ], + "provides": [ + "cmd:xmlrpc-c-config" + ] + }, + "perl-boolean-doc": { + "versions": { + "0.46-r0": 1509485610 + }, + "origin": "perl-boolean" + }, + "libmicrohttpd-dev": { + "versions": { + "0.9.57-r0": 1512032047 + }, + "origin": "libmicrohttpd", + "dependencies": [ + "libmicrohttpd=0.9.57-r0", + "pc:gnutls", + "pkgconfig" + ], + "provides": [ + "pc:libmicrohttpd=0.9.57" + ] + }, + "at-spi2-core-dev": { + "versions": { + "2.26.2-r0": 1509466052 + }, + "origin": "at-spi2-core", + "dependencies": [ + "dbus-dev", + "glib-dev", + "libxtst-dev", + "at-spi2-core=2.26.2-r0", + "pc:dbus-1", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:atspi-2=2.26.2" + ] + }, + "lua-discount": { + "versions": { + "1.2.10.1-r4": 1509479789 + }, + "origin": "lua-discount" + }, + "perl-class-container-doc": { + "versions": { + "0.12-r1": 1509470590 + }, + "origin": "perl-class-container" + }, + "ncurses5-widec-libs": { + "versions": { + "5.9-r1": 1509483663 + }, + "origin": "ncurses5", + "dependencies": [ + "ncurses-terminfo-base", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libformw.so.5=5.9", + "so:libmenuw.so.5=5.9", + "so:libncursesw.so.5=5.9", + "so:libpanelw.so.5=5.9" + ] + }, + "ppp-daemon": { + "versions": { + "2.4.7-r5": 1509480141 + }, + "origin": "ppp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:poff", + "cmd:pon", + "cmd:pppd", + "cmd:pppdump", + "cmd:pppstats" + ] + }, + "qemu-sh4": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sh4" + ] + }, + "perl-list-someutils": { + "versions": { + "0.53-r0": 1509474588 + }, + "origin": "perl-list-someutils", + "dependencies": [ + "perl-module-implementation", + "perl-list-someutils-xs" + ] + }, + "libmikmod": { + "versions": { + "3.3.11.1-r0": 1509481761 + }, + "origin": "libmikmod", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmikmod.so.3=3.3.0" + ] + }, + "bacula-doc": { + "versions": { + "9.0.5-r3": 1512992127 + }, + "origin": "bacula" + }, + "acl-dev": { + "versions": { + "2.2.52-r3": 1509459586 + }, + "origin": "acl", + "dependencies": [ + "libacl=2.2.52-r3" + ] + }, + "squid-doc": { + "versions": { + "3.5.27-r0": 1519824030, + "3.5.27-r1": 1562865666 + }, + "origin": "squid" + }, + "py3-jinja2": { + "versions": { + "2.9.6-r0": 1509476513 + }, + "origin": "py-jinja2", + "dependencies": [ + "py3-markupsafe", + "python3" + ] + }, + "poppler-qt4-dev": { + "versions": { + "0.56.0-r0": 1510071692 + }, + "origin": "poppler-qt4", + "dependencies": [ + "jpeg-dev", + "cairo-dev", + "libxml2-dev", + "fontconfig-dev", + "qt-dev", + "poppler-dev>=0.56.0", + "lcms2-dev", + "openjpeg-dev", + "pkgconfig", + "poppler-qt4=0.56.0-r0" + ], + "provides": [ + "pc:poppler-qt4=0.56.0" + ] + }, + "heimdal-dev": { + "versions": { + "7.4.0-r2": 1514545901, + "7.4.0-r3": 1559659765, + "7.4.0-r4": 1562862336 + }, + "origin": "heimdal", + "dependencies": [ + "libressl-dev", + "e2fsprogs-dev", + "db-dev", + "heimdal-libs=7.4.0-r4", + "pkgconfig" + ], + "provides": [ + "pc:heimdal-gssapi=7.4.0", + "pc:heimdal-kadm-client=7.4.0", + "pc:heimdal-kadm-server=7.4.0", + "pc:heimdal-krb5=7.4.0", + "pc:kadm-client=7.4.0", + "pc:kadm-server=7.4.0", + "pc:kafs=7.4.0", + "pc:krb5-gssapi=7.4.0", + "pc:krb5=7.4.0", + "cmd:krb5-config" + ] + }, + "which-doc": { + "versions": { + "2.21-r1": 1509485571 + }, + "origin": "which" + }, + "mesa-libwayland-egl": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libwayland-egl.so.1=1.0.0" + ] + }, + "libbz2": { + "versions": { + "1.0.6-r6": 1509456387, + "1.0.6-r7": 1562268494 + }, + "origin": "bzip2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libbz2.so.1=1.0.6" + ] + }, + "qemu-guest-agent": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "cmd:qemu-ga" + ] + }, + "xfce4-whiskermenu-plugin-doc": { + "versions": { + "1.7.3-r0": 1510073068 + }, + "origin": "xfce4-whiskermenu-plugin" + }, + "openbox-dev": { + "versions": { + "3.6.1-r1": 1510073884 + }, + "origin": "openbox", + "dependencies": [ + "libxcursor-dev", + "libxrandr-dev", + "libxinerama-dev", + "startup-notification-dev", + "openbox-libs=3.6.1-r1", + "pc:glib-2.0", + "pc:imlib2", + "pc:librsvg-2.0", + "pc:libxml-2.0", + "pc:pangoxft", + "pc:xft", + "pkgconfig" + ], + "provides": [ + "pc:obrender-3.5=3.6", + "pc:obt-3.5=3.6" + ] + }, + "perl-crypt-openssl-rsa-doc": { + "versions": { + "0.28-r9": 1510261379 + }, + "origin": "perl-crypt-openssl-rsa" + }, + "libass-dev": { + "versions": { + "0.13.7-r0": 1509480355 + }, + "origin": "libass", + "dependencies": [ + "enca-dev", + "fontconfig-dev", + "fribidi-dev", + "freetype-dev", + "libass=0.13.7-r0", + "pc:fontconfig>=2.10.92", + "pc:freetype2>=9.10.3", + "pc:fribidi>=0.19.0", + "pkgconfig" + ], + "provides": [ + "pc:libass=0.13.7" + ] + }, + "lm_sensors-dev": { + "versions": { + "3.4.0-r4": 1509475442 + }, + "origin": "lm_sensors", + "dependencies": [ + "lm_sensors=3.4.0-r4" + ] + }, + "kamailio-carrierroute": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libsrdb1.so.1", + "so:libtrie.so.1" + ] + }, + "perl-parse-recdescent-doc": { + "versions": { + "1.967015-r0": 1509489503 + }, + "origin": "perl-parse-recdescent" + }, + "sems-g722": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libspandsp.so.2" + ] + }, + "libnih-dev": { + "versions": { + "1.0.3-r4": 1509473672 + }, + "origin": "libnih", + "dependencies": [ + "dbus-dev", + "expat-dev", + "libnih=1.0.3-r4", + "pkgconfig" + ], + "provides": [ + "pc:libnih-dbus=1.0.3", + "pc:libnih=1.0.3" + ] + }, + "py2-certifi": { + "versions": { + "2017.7.27.1-r1": 1509551895 + }, + "origin": "py-certifi", + "dependencies": [ + "python2" + ] + }, + "libsrtp": { + "versions": { + "1.5.4-r0": 1509473356 + }, + "origin": "libsrtp" + }, + "fprobe-ulog": { + "versions": { + "1.2-r3": 1509496379 + }, + "origin": "fprobe-ulog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetfilter_log.so.1", + "so:libnfnetlink.so.0" + ], + "provides": [ + "cmd:fprobe-ulog" + ] + }, + "perl-astro-suntime": { + "versions": { + "0.06-r0": 1509493236 + }, + "origin": "perl-astro-suntime", + "dependencies": [ + "perl" + ] + }, + "nginx-mod-mail": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "py3-chardet": { + "versions": { + "3.0.4-r0": 1509481686 + }, + "origin": "py-chardet", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:chardetect" + ] + }, + "musl-dbg": { + "versions": { + "1.1.18-r3": 1518031143, + "1.1.18-r4": 1565163296 + }, + "origin": "musl" + }, + "audacious-dev": { + "versions": { + "3.9-r0": 1510072524 + }, + "origin": "audacious", + "dependencies": [ + "gtk+2.0-dev", + "dbus-glib-dev", + "audacious=3.9-r0", + "pkgconfig" + ], + "provides": [ + "pc:audacious=3.9" + ] + }, + "lua5.2-maxminddb": { + "versions": { + "0.1-r1": 1509496571 + }, + "origin": "lua-maxminddb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmaxminddb.so.0" + ] + }, + "xfce4-xkb-plugin-doc": { + "versions": { + "0.7.1-r0": 1510074121 + }, + "origin": "xfce4-xkb-plugin" + }, + "dpkg-dev": { + "versions": { + "1.18.24-r0": 1509494230 + }, + "origin": "dpkg", + "dependencies": [ + "perl", + "pkgconfig" + ], + "provides": [ + "pc:libdpkg=1.18.24", + "cmd:dpkg-architecture", + "cmd:dpkg-buildpackage", + "cmd:dpkg-checkbuilddeps", + "cmd:dpkg-distaddfile", + "cmd:dpkg-genchanges", + "cmd:dpkg-gencontrol", + "cmd:dpkg-gensymbols", + "cmd:dpkg-name", + "cmd:dpkg-scanpackages", + "cmd:dpkg-scansources", + "cmd:dpkg-shlibdeps", + "cmd:dpkg-source", + "cmd:dpkg-vendor" + ] + }, + "putty": { + "versions": { + "0.71-r1": 1554726628 + }, + "origin": "putty", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:plink", + "cmd:pscp", + "cmd:psftp", + "cmd:puttygen" + ] + }, + "ttf-liberation": { + "versions": { + "2.00.1-r1": 1509491425 + }, + "origin": "ttf-liberation", + "dependencies": [ + "fontconfig" + ] + }, + "gptfdisk-doc": { + "versions": { + "1.0.3-r0": 1509495223 + }, + "origin": "gptfdisk" + }, + "gtk-engines-dev": { + "versions": { + "2.21.0-r2": 1510071721 + }, + "origin": "gtk-engines", + "dependencies": [ + "pc:gtk+-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gtk-engines-2=2.21.0" + ] + }, + "fftw-single-libs": { + "versions": { + "3.3.6p2-r0": 1509468830 + }, + "origin": "fftw", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfftw3f.so.3=3.5.6", + "so:libfftw3f_threads.so.3=3.5.6" + ] + }, + "libmowgli-dev": { + "versions": { + "2.0.0-r0": 1509491362 + }, + "origin": "libmowgli", + "dependencies": [ + "libmowgli=2.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmowgli-2=2.0.0" + ] + }, + "galculator": { + "versions": { + "2.1.4-r0": 1510075355 + }, + "origin": "galculator", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0" + ], + "provides": [ + "cmd:galculator" + ] + }, + "altermime-doc": { + "versions": { + "0.3.11-r0": 1509481329 + }, + "origin": "altermime" + }, + "xcb-util-dev": { + "versions": { + "0.4.0-r1": 1509464530 + }, + "origin": "xcb-util", + "dependencies": [ + "libxcb-dev", + "util-macros", + "pkgconfig", + "xcb-util=0.4.0-r1" + ], + "provides": [ + "pc:xcb-atom=0.4.0", + "pc:xcb-aux=0.4.0", + "pc:xcb-event=0.4.0", + "pc:xcb-util=0.4.0" + ] + }, + "encfs": { + "versions": { + "1.8.1-r7": 1510258715 + }, + "origin": "encfs", + "dependencies": [ + "fuse", + "so:libboost_serialization-mt.so.1.62.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libfuse.so.2", + "so:libgcc_s.so.1", + "so:librlog.so.5", + "so:libssl.so.44", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libencfs.so.6=6.0.2", + "cmd:encfs", + "cmd:encfsctl", + "cmd:encfssh" + ] + }, + "py-libmount": { + "versions": { + "2.31.1-r0": 1541506295 + }, + "origin": "util-linux", + "dependencies": [ + "findmnt", + "so:libc.musl-x86_64.so.1", + "so:libmount.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "libxmu-dev": { + "versions": { + "1.1.2-r1": 1509473711 + }, + "origin": "libxmu", + "dependencies": [ + "util-linux-dev", + "libxmu=1.1.2-r1", + "pc:x11", + "pc:xext", + "pc:xproto", + "pc:xt", + "pkgconfig" + ], + "provides": [ + "pc:xmu=1.1.2", + "pc:xmuu=1.1.2" + ] + }, + "bridge": { + "versions": { + "1.5-r3": 1509480112 + }, + "origin": "bridge" + }, + "atkmm-doc": { + "versions": { + "2.24.2-r0": 1509483360 + }, + "origin": "atkmm" + }, + "cairo-tools": { + "versions": { + "1.14.10-r0": 1509464621 + }, + "origin": "cairo", + "dependencies": [ + "so:libX11.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo-script-interpreter.so.2", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libpixman-1.so.0", + "so:libxcb-render.so.0", + "so:libxcb.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:cairo-sphinx", + "cmd:cairo-trace" + ] + }, + "py-cffi": { + "versions": { + "1.10.0-r0": 1509476375 + }, + "origin": "py-cffi" + }, + "shorewall-core": { + "versions": { + "5.1.8-r0": 1509477808 + }, + "origin": "shorewall-core", + "provides": [ + "cmd:shorewall" + ] + }, + "uwsgi-syslog": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-yaml-tiny-doc": { + "versions": { + "1.70-r1": 1510352923 + }, + "origin": "perl-yaml-tiny" + }, + "imagemagick-dev": { + "versions": { + "7.0.7.11-r1": 1510748307 + }, + "origin": "imagemagick", + "dependencies": [ + "imagemagick-c++=7.0.7.11-r1", + "imagemagick-libs=7.0.7.11-r1", + "pkgconfig" + ], + "provides": [ + "pc:ImageMagick-7.Q16HDRI=7.0.7", + "pc:ImageMagick=7.0.7", + "pc:Magick++-7.Q16HDRI=7.0.7", + "pc:Magick++=7.0.7", + "pc:MagickCore-7.Q16HDRI=7.0.7", + "pc:MagickCore=7.0.7", + "pc:MagickWand-7.Q16HDRI=7.0.7", + "pc:MagickWand=7.0.7", + "cmd:Magick++-config", + "cmd:MagickCore-config", + "cmd:MagickWand-config" + ] + }, + "gpgmepp": { + "versions": { + "1.9.0-r1": 1510588164 + }, + "origin": "gpgme", + "dependencies": [ + "gnupg", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgpgme.so.11", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgpgmepp.so.6=6.4.0" + ] + }, + "perl-file-copy-recursive-doc": { + "versions": { + "0.38-r1": 1510855671 + }, + "origin": "perl-file-copy-recursive" + }, + "cogl": { + "versions": { + "1.22.2-r0": 1510072961 + }, + "origin": "cogl", + "dependencies": [ + "so:libEGL.so.1", + "so:libX11.so.6", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdrm.so.2", + "so:libgbm.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0" + ], + "provides": [ + "so:libcogl-gles2.so.20=20.4.2", + "so:libcogl-pango.so.20=20.4.2", + "so:libcogl-path.so.20=20.4.2", + "so:libcogl.so.20=20.4.2" + ] + }, + "libidn-doc": { + "versions": { + "1.33-r1": 1509468968 + }, + "origin": "libidn" + }, + "xfsprogs-libs": { + "versions": { + "4.14.0-r1": 1528279973 + }, + "origin": "xfsprogs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhandle.so.1=1.0.3" + ] + }, + "libxkbui": { + "versions": { + "1.0.2-r8": 1509473778 + }, + "origin": "libxkbui", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libxkbfile.so.1" + ], + "provides": [ + "so:libxkbui.so.1=1.0.0" + ] + }, + "lxc-bash-completion": { + "versions": { + "2.1.1-r3": 1533578833 + }, + "origin": "lxc" + }, + "jemalloc-doc": { + "versions": { + "5.0.1-r0": 1509482444 + }, + "origin": "jemalloc" + }, + "busybox-suid": { + "versions": { + "1.27.2-r11": 1528276162 + }, + "origin": "busybox", + "dependencies": [ + "busybox", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bbsuid" + ] + }, + "git-email": { + "versions": { + "2.15.3-r0": 1540401293, + "2.15.4-r0": 1576015190 + }, + "origin": "git", + "dependencies": [ + "perl", + "perl-git=2.15.4-r0", + "perl-net-smtp-ssl", + "perl-authen-sasl", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libpcre2-8.so.0", + "so:libssl.so.44", + "so:libz.so.1" + ] + }, + "audacious-plugins-dbg": { + "versions": { + "3.9-r0": 1510288541 + }, + "origin": "audacious-plugins", + "dependencies": [ + "audacious" + ] + }, + "appstream-glib-lang": { + "versions": { + "0.6.3-r0": 1510067862 + }, + "origin": "appstream-glib" + }, + "dhcpcd-ui": { + "versions": { + "0.7.5-r1": 1510073428 + }, + "origin": "dhcpcd-ui", + "dependencies": [ + "dhcpcd-dbus", + "hicolor-icon-theme", + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libnotify.so.4" + ], + "provides": [ + "cmd:dhcpcd-gtk", + "cmd:dhcpcd-online" + ] + }, + "cdw": { + "versions": { + "0.8.1-r0": 1509491773 + }, + "origin": "cdw", + "dependencies": [ + "cdrkit", + "so:libburn.so.4", + "so:libc.musl-x86_64.so.1", + "so:libcdio.so.16", + "so:libformw.so.6", + "so:libintl.so.8", + "so:libiso9660.so.10", + "so:libmenuw.so.6", + "so:libncursesw.so.6", + "so:libpanelw.so.6" + ], + "provides": [ + "cmd:cdw" + ] + }, + "perl-sub-identify": { + "versions": { + "0.14-r1": 1509474068 + }, + "origin": "perl-sub-identify", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py-sphinx": { + "versions": { + "1.6.5-r0": 1509480690 + }, + "origin": "py-sphinx", + "dependencies": [ + "py3-sphinx", + "py3-sphinx=1.6.5-r0" + ] + }, + "py-django-registration": { + "versions": { + "1.0-r1": 1509474180 + }, + "origin": "py-django-registration", + "dependencies": [ + "python2" + ] + }, + "gettext-static": { + "versions": { + "0.19.8.1-r1": 1509459195 + }, + "origin": "gettext" + }, + "a2ps": { + "versions": { + "4.14-r7": 1510365670 + }, + "origin": "a2ps", + "dependencies": [ + "ghostscript", + "imagemagick", + "perl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:a2ps", + "cmd:card", + "cmd:composeglyphs", + "cmd:fixnt", + "cmd:fixps", + "cmd:ogonkify", + "cmd:pdiff", + "cmd:psmandup", + "cmd:psset", + "cmd:texi2dvi4a2ps" + ] + }, + "vanessa_socket": { + "versions": { + "0.0.13-r0": 1511165897 + }, + "origin": "vanessa_socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0", + "so:libvanessa_logger.so.0" + ], + "provides": [ + "so:libvanessa_socket.so.2=2.1.0", + "cmd:vanessa_socket_pipe" + ] + }, + "perl-test-warnings-doc": { + "versions": { + "0.026-r0": 1509468491 + }, + "origin": "perl-test-warnings" + }, + "gstreamer-tools": { + "versions": { + "1.12.3-r0": 1509470920 + }, + "origin": "gstreamer", + "dependencies": [ + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:gst-inspect-1.0", + "cmd:gst-launch-1.0", + "cmd:gst-stats-1.0", + "cmd:gst-typefind-1.0" + ] + }, + "openssl-doc": { + "versions": { + "1.0.2r-r0": 1552814666, + "1.0.2t-r0": 1568300411 + }, + "origin": "openssl" + }, + "perl-type-tiny-doc": { + "versions": { + "1.000006-r0": 1509468522 + }, + "origin": "perl-type-tiny" + }, + "libevent-dev": { + "versions": { + "2.1.8-r2": 1510258602 + }, + "origin": "libevent", + "dependencies": [ + "python2", + "libevent=2.1.8-r2", + "pkgconfig" + ], + "provides": [ + "cmd:event_rpcgen.py" + ] + }, + "gradm": { + "versions": { + "3.1.201607172312-r0": 1509494156 + }, + "origin": "gradm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:gradm", + "cmd:grlearn" + ] + }, + "patchwork-mysql": { + "versions": { + "1.1.3-r0": 1509474238, + "1.1.3-r1": 1562223418 + }, + "origin": "patchwork", + "dependencies": [ + "py-mysqldb" + ] + }, + "lua5.2-subprocess": { + "versions": { + "0.0.20141229-r2": 1509468275 + }, + "origin": "lua-subprocess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libdnet-dev": { + "versions": { + "1.12-r7": 1509490804 + }, + "origin": "libdnet", + "dependencies": [ + "libdnet=1.12-r7" + ], + "provides": [ + "cmd:dnet-config" + ] + }, + "kamailio-cpl": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libsrdb1.so.1", + "so:libxml2.so.2" + ] + }, + "gnutls-dbg": { + "versions": { + "3.6.1-r0": 1509465331 + }, + "origin": "gnutls" + }, + "perl-package-stash-xs": { + "versions": { + "0.28-r4": 1509474038 + }, + "origin": "perl-package-stash-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py-jinja2-doc": { + "versions": { + "2.9.6-r0": 1509476514 + }, + "origin": "py-jinja2" + }, + "dbus": { + "versions": { + "1.10.24-r0": 1509465101, + "1.10.28-r0": 1560765707 + }, + "origin": "dbus", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libexpat.so.1" + ], + "provides": [ + "cmd:dbus-cleanup-sockets", + "cmd:dbus-daemon", + "cmd:dbus-monitor", + "cmd:dbus-run-session", + "cmd:dbus-send", + "cmd:dbus-test-tool", + "cmd:dbus-update-activation-environment", + "cmd:dbus-uuidgen" + ] + }, + "perl-config-inifiles-doc": { + "versions": { + "2.94-r0": 1509489531 + }, + "origin": "perl-config-inifiles" + }, + "udisks2-doc": { + "versions": { + "2.6.5-r0": 1510075149 + }, + "origin": "udisks2" + }, + "avahi-compat-libdns_sd": { + "versions": { + "0.6.32-r4": 1509465449, + "0.6.32-r5": 1563345619 + }, + "origin": "avahi", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdns_sd.so.1=1.0.0" + ] + }, + "perl-test-fatal-doc": { + "versions": { + "0.014-r1": 1509464416 + }, + "origin": "perl-test-fatal" + }, + "openldap-overlay-rwm": { + "versions": { + "2.4.45-r3": 1510258135, + "2.4.46-r0": 1565073941, + "2.4.48-r0": 1566900518 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "parallel-doc": { + "versions": { + "20171122-r0": 1511894075 + }, + "origin": "parallel" + }, + "perl-xml-namespacesupport-doc": { + "versions": { + "1.12-r0": 1509475901 + }, + "origin": "perl-xml-namespacesupport" + }, + "lxsession": { + "versions": { + "0.5.3-r0": 1510073920 + }, + "origin": "lxsession", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpolkit-agent-1.so.0", + "so:libpolkit-gobject-1.so.0", + "so:libunique-1.0.so.0" + ], + "provides": [ + "cmd:lxclipboard", + "cmd:lxlock", + "cmd:lxpolkit", + "cmd:lxsession", + "cmd:lxsession-db", + "cmd:lxsession-default", + "cmd:lxsession-default-apps", + "cmd:lxsession-default-terminal", + "cmd:lxsession-edit", + "cmd:lxsession-logout", + "cmd:lxsession-xdg-autostart", + "cmd:lxsettings-daemon" + ] + }, + "perl-package-anon-doc": { + "versions": { + "0.05-r4": 1509489338 + }, + "origin": "perl-package-anon" + }, + "gnokii": { + "versions": { + "0.6.31-r6": 1510069857 + }, + "origin": "gnokii", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgnokii.so.7", + "so:libintl.so.8" + ], + "provides": [ + "cmd:gnokii", + "cmd:gnokiid", + "cmd:mgnokiidev" + ] + }, + "tumbler-dev": { + "versions": { + "0.2.0-r0": 1510075180 + }, + "origin": "tumbler", + "dependencies": [ + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pkgconfig", + "tumbler=0.2.0-r0" + ], + "provides": [ + "pc:tumbler-1=0.2.0" + ] + }, + "keybinder-doc": { + "versions": { + "0.3.0-r1": 1510069942 + }, + "origin": "keybinder" + }, + "gross-doc": { + "versions": { + "1.0.2-r11": 1509482386 + }, + "origin": "gross" + }, + "samba-common-server-libs": { + "versions": { + "4.7.6-r3": 1555491786 + }, + "origin": "samba", + "dependencies": [ + "so:libCHARSET3-samba4.so", + "so:libacl.so.1", + "so:libaddns-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcli-spoolss-samba4.so", + "so:libcliauth-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libcups.so.2", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libevents-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libgssapi-samba4.so.2", + "so:libiov-buf-samba4.so", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libldb.so.1", + "so:liblibcli-lsa3-samba4.so", + "so:liblibcli-netlogon3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmessages-dgm-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libreplace-samba4.so", + "so:libsamba-cluster-support-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-shim-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so", + "so:libtrusts-util-samba4.so", + "so:libutil-cmdline-samba4.so", + "so:libutil-reg-samba4.so", + "so:libutil-setid-samba4.so", + "so:libutil-tdb-samba4.so", + "so:libwbclient.so.0" + ], + "provides": [ + "so:libads-samba4.so=0", + "so:libauth-samba4.so=0", + "so:libdfs-server-ad-samba4.so=0", + "so:libnetapi.so.0=0", + "so:libnpa-tstream-samba4.so=0", + "so:libprinting-migrate-samba4.so=0", + "so:libsmbd-base-samba4.so=0", + "so:libsmbd-conn-samba4.so=0", + "so:libsmbldap.so.2=2", + "so:libsmbldaphelper-samba4.so=0" + ] + }, + "tcpproxy": { + "versions": { + "2.0.0_beta15-r4": 1509491651 + }, + "origin": "tcpproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tcpproxy" + ] + }, + "qt-webkit": { + "versions": { + "4.8.7-r8": 1510287209 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtDeclarative.so.4", + "so:libQtGui.so.4", + "so:libQtNetwork.so.4", + "so:libX11.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libsqlite3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libQtWebKit.so.4=4.9.4" + ] + }, + "libasyncns-dev": { + "versions": { + "0.8-r0": 1509490868 + }, + "origin": "libasyncns", + "dependencies": [ + "libasyncns=0.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:libasyncns=0.8" + ] + }, + "cloog": { + "versions": { + "0.18.4-r1": 1509473493 + }, + "origin": "cloog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libisl.so.15" + ], + "provides": [ + "so:libcloog-isl.so.4=4.0.0", + "cmd:cloog" + ] + }, + "fixesproto-doc": { + "versions": { + "5.0-r2": 1509464679 + }, + "origin": "fixesproto" + }, + "font-misc-misc": { + "versions": { + "1.1.2-r1": 1509473680 + }, + "origin": "font-misc-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig", + "util-macros" + ] + }, + "qemu-system-s390x": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-s390x" + ] + }, + "s6-rc": { + "versions": { + "0.2.1.2-r0": 1509492619 + }, + "origin": "s6-rc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexecline.so.2.3", + "so:libs6.so.2.6", + "so:libskarnet.so.2.6" + ], + "provides": [ + "so:libs6rc.so.0.2=0.2.1.2", + "cmd:s6-rc", + "cmd:s6-rc-bundle", + "cmd:s6-rc-compile", + "cmd:s6-rc-db", + "cmd:s6-rc-dryrun", + "cmd:s6-rc-init", + "cmd:s6-rc-update" + ] + }, + "open-isns": { + "versions": { + "0.97-r2": 1510259304 + }, + "origin": "open-isns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libisns.so.0" + ], + "provides": [ + "cmd:isnsadm", + "cmd:isnsd", + "cmd:isnsdd" + ] + }, + "qextserialport-dev": { + "versions": { + "1.2_rc1-r0": 1510075399 + }, + "origin": "qextserialport", + "dependencies": [ + "qt-dev", + "qextserialport=1.2_rc1-r0" + ] + }, + "libusb-compat-dev": { + "versions": { + "0.1.5-r3": 1509473935 + }, + "origin": "libusb-compat", + "dependencies": [ + "libusb-dev", + "libusb-compat=0.1.5-r3", + "pc:libusb-1.0", + "pkgconfig" + ], + "provides": [ + "pc:libusb=0.1.12", + "cmd:libusb-config" + ] + }, + "xmlto": { + "versions": { + "0.0.28-r2": 1509461795 + }, + "origin": "xmlto", + "dependencies": [ + "libxslt", + "perl-yaml-syck", + "perl-test-pod", + "bash", + "docbook-xsl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xmlif", + "cmd:xmlto" + ] + }, + "hunspell-pt-br": { + "versions": { + "20131017-r0": 1509494665 + }, + "origin": "hunspell-pt-br" + }, + "giblib": { + "versions": { + "1.2.4-r8": 1509470529 + }, + "origin": "giblib", + "dependencies": [ + "so:libImlib2.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgiblib.so.1=1.0.6" + ] + }, + "uwsgi-ping": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "luajit-doc": { + "versions": { + "2.1.0_beta3-r0": 1509474650 + }, + "origin": "luajit" + }, + "unzip": { + "versions": { + "6.0-r3": 1534926377 + }, + "origin": "unzip", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:funzip", + "cmd:unzip", + "cmd:unzipsfx", + "cmd:zipgrep", + "cmd:zipinfo" + ] + }, + "weechat-dev": { + "versions": { + "1.9.1-r1": 1509530222 + }, + "origin": "weechat", + "dependencies": [ + "cmake", + "libintl", + "ncurses-dev", + "gnutls-dev", + "libgcrypt-dev", + "curl-dev", + "aspell-dev", + "lua-dev", + "perl-dev", + "python2-dev", + "ruby-dev", + "zlib-dev", + "pkgconfig" + ], + "provides": [ + "pc:weechat=1.9.1" + ] + }, + "mpg123": { + "versions": { + "1.25.7-r0": 1509707456 + }, + "origin": "mpg123", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmpg123.so.0=0.44.6", + "so:libout123.so.0=0.2.1", + "cmd:mpg123", + "cmd:mpg123-id3dump", + "cmd:mpg123-strip", + "cmd:out123" + ] + }, + "unixodbc": { + "versions": { + "2.3.4-r2": 1509469610 + }, + "origin": "unixodbc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "so:libodbc.so.2=2.0.0", + "so:libodbccr.so.2=2.0.0", + "so:libodbcinst.so.2=2.0.0", + "cmd:dltest", + "cmd:isql", + "cmd:iusql", + "cmd:odbc_config", + "cmd:odbcinst", + "cmd:slencheck" + ] + }, + "py2-docutils": { + "versions": { + "0.13.1-r0": 1509486243 + }, + "origin": "py-docutils", + "dependencies": [ + "py2-pillow", + "py2-roman", + "python2" + ], + "provides": [ + "cmd:rst2html-2", + "cmd:rst2html5-2", + "cmd:rst2latex-2", + "cmd:rst2man-2", + "cmd:rst2odt-2", + "cmd:rst2odt_prepstyles-2", + "cmd:rst2pseudoxml-2", + "cmd:rst2s5-2", + "cmd:rst2xetex-2", + "cmd:rst2xml-2", + "cmd:rstpep2html-2" + ] + }, + "xf86-video-nv": { + "versions": { + "2.1.21-r0": 1510075414 + }, + "origin": "xf86-video-nv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py-boto-doc": { + "versions": { + "2.47.0-r0": 1509472876 + }, + "origin": "py-boto", + "dependencies": [ + "python2" + ] + }, + "mtr-gtk": { + "versions": { + "0.92-r0": 1510072768 + }, + "origin": "mtr", + "dependencies": [ + "ncurses", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:mtr-gtk", + "cmd:mtr-packet-gtk" + ] + }, + "libxdamage": { + "versions": { + "1.1.4-r1": 1509464692 + }, + "origin": "libxdamage", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXdamage.so.1=1.1.0" + ] + }, + "uwsgi-logcrypto": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "pango-dev": { + "versions": { + "1.40.14-r1": 1541519450 + }, + "origin": "pango", + "dependencies": [ + "pango=1.40.14-r1", + "pc:cairo", + "pc:fontconfig", + "pc:freetype2", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:harfbuzz", + "pc:xft", + "pkgconfig" + ], + "provides": [ + "pc:pango=1.40.14", + "pc:pangocairo=1.40.14", + "pc:pangoft2=1.40.14", + "pc:pangoxft=1.40.14" + ] + }, + "xsetmode": { + "versions": { + "1.0.0-r4": 1509490726 + }, + "origin": "xsetmode", + "dependencies": [ + "so:libX11.so.6", + "so:libXi.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xsetmode" + ] + }, + "lua-sql-postgres": { + "versions": { + "2.3.5-r1": 1509488834 + }, + "origin": "lua-sql" + }, + "lua-curl": { + "versions": { + "0.3.7-r1": 1509493018 + }, + "origin": "lua-curl" + }, + "py-django-oscar": { + "versions": { + "1.5.1-r0": 1512067699 + }, + "origin": "py-django-oscar", + "dependencies": [ + "py-babel", + "py-django", + "py-django-extra-views", + "py-django-haystack", + "py-django-phonenumber-field", + "py-django-sorl-thumbnail", + "py-django-tables2", + "py-django-treebeard", + "py-django-widget-tweaks", + "py-factory-boy", + "py-mock", + "py-pillow", + "py-purl", + "py-unidecode" + ] + }, + "perl-module-pluggable-doc": { + "versions": { + "5.2-r0": 1509483532 + }, + "origin": "perl-module-pluggable" + }, + "libunique3-doc": { + "versions": { + "3.0.2-r0": 1510069893 + }, + "origin": "libunique3" + }, + "gst-plugins-bad0.10-lang": { + "versions": { + "0.10.23-r7": 1510287373 + }, + "origin": "gst-plugins-bad0.10" + }, + "exiv2": { + "versions": { + "0.25-r0": 1509475473 + }, + "origin": "exiv2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libexiv2.so.14=14.0.0", + "cmd:exiv2" + ] + }, + "quvi": { + "versions": { + "0.9.5-r4": 1509495086 + }, + "origin": "quvi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libglib-2.0.so.0", + "so:libintl.so.8", + "so:libquvi-0.9-0.9.4.so" + ], + "provides": [ + "cmd:quvi" + ] + }, + "libxml2": { + "versions": { + "2.9.8-r1": 1540398580 + }, + "origin": "libxml2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libxml2.so.2=2.9.8" + ] + }, + "uwsgi-router_redirect": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libexecinfo-dev": { + "versions": { + "1.1-r0": 1509491293 + }, + "origin": "libexecinfo", + "dependencies": [ + "libexecinfo=1.1-r0" + ] + }, + "pigz": { + "versions": { + "2.3.4-r2": 1509494287 + }, + "origin": "pigz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:pigz", + "cmd:unpigz" + ] + }, + "mesa-dri-ati": { + "versions": { + "17.2.4-r1": 1510741945 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_intel.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libxcb-dri2.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb-xfixes.so.0", + "so:libxcb.so.1", + "so:libxshmfence.so.1", + "so:libz.so.1" + ] + }, + "hypermail": { + "versions": { + "2.3.0-r4": 1509494653 + }, + "origin": "hypermail", + "dependencies": [ + "lua", + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libpcre.so.1" + ], + "provides": [ + "cmd:hypermail", + "cmd:mdir2mbox.lua", + "cmd:msgarchive", + "cmd:rdmsg" + ] + }, + "subunit-dev": { + "versions": { + "1.2.0-r0": 1509481263 + }, + "origin": "subunit", + "dependencies": [ + "pkgconfig", + "subunit-libs=1.2.0-r0" + ], + "provides": [ + "pc:libcppunit_subunit=1.2.0", + "pc:libsubunit=1.2.0" + ] + }, + "pm-utils-doc": { + "versions": { + "1.4.1-r0": 1509491019 + }, + "origin": "pm-utils" + }, + "py-django-tables2": { + "versions": { + "1.16.0-r0": 1511954172 + }, + "origin": "py-django-tables2", + "dependencies": [ + "py-django", + "py-six" + ] + }, + "perl-extutils-installpaths-doc": { + "versions": { + "0.011-r0": 1509474744 + }, + "origin": "perl-extutils-installpaths" + }, + "libmtp": { + "versions": { + "1.1.14-r0": 1509481648 + }, + "origin": "libmtp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libmtp.so.9=9.3.0" + ] + }, + "clutter-lang": { + "versions": { + "1.26.2-r1": 1510073009 + }, + "origin": "clutter" + }, + "stalonetray": { + "versions": { + "0.8.3-r0": 1509489786 + }, + "origin": "stalonetray", + "dependencies": [ + "so:libX11.so.6", + "so:libXpm.so.4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:stalonetray" + ] + }, + "perl-net-snpp": { + "versions": { + "1.17-r2": 1509489394 + }, + "origin": "perl-net-snpp", + "dependencies": [ + "perl" + ] + }, + "py-urlnorm": { + "versions": { + "1.1.2-r1": 1509552774 + }, + "origin": "py-urlnorm", + "dependencies": [ + "python2" + ] + }, + "xcb-util-image-dev": { + "versions": { + "0.4.0-r1": 1509473909 + }, + "origin": "xcb-util-image", + "dependencies": [ + "xcb-util-dev", + "pc:xcb-shm", + "pkgconfig", + "xcb-util-image=0.4.0-r1" + ], + "provides": [ + "pc:xcb-image=0.4.0" + ] + }, + "eudev-libs": { + "versions": { + "3.2.4-r1": 1509466081 + }, + "origin": "eudev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libudev.so.1=1.6.3" + ] + }, + "doxygen": { + "versions": { + "1.8.13-r1": 1509465011 + }, + "origin": "doxygen", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:doxygen" + ] + }, + "perl-cgi-emulate-psgi": { + "versions": { + "0.23-r0": 1509493702 + }, + "origin": "perl-cgi-emulate-psgi", + "dependencies": [ + "perl-http-message", + "perl-cgi" + ] + }, + "mesa-gl": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libexpat.so.1", + "so:libglapi.so.0", + "so:libxcb-dri2.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-glx.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb.so.1", + "so:libxshmfence.so.1" + ], + "provides": [ + "so:libGL.so.1=1.2.0" + ] + }, + "imagemagick-doc": { + "versions": { + "7.0.7.11-r1": 1510748306 + }, + "origin": "imagemagick" + }, + "amavisd-new": { + "versions": { + "2.11.0-r2": 1509517843 + }, + "origin": "amavisd-new", + "dependencies": [ + "sed", + "file", + "perl", + "perl-archive-zip", + "perl-carp", + "perl-convert-tnef", + "perl-compress-raw-zlib", + "perl-convert-uulib", + "perl-digest-md5", + "perl-io", + "perl-exporter", + "perl-io-compress", + "perl-io-stringy", + "perl-mime-tools", + "perl-mailtools", + "perl-socket", + "perl-net-libidn", + "perl-net-server", + "perl-time-hires", + "perl-unix-syslog", + "perl-db", + "perl-mail-dkim", + "/bin/sh" + ], + "provides": [ + "cmd:amavisd", + "cmd:amavisd-nanny", + "cmd:amavisd-release" + ] + }, + "py2-click": { + "versions": { + "6.7-r2": 1509476498 + }, + "origin": "py-click", + "dependencies": [ + "python2" + ] + }, + "dillo-doc": { + "versions": { + "3.0.5-r4": 1510259043 + }, + "origin": "dillo" + }, + "samba-initscript": { + "versions": { + "4.7.6-r3": 1555491786 + }, + "origin": "samba" + }, + "libusb": { + "versions": { + "1.0.21-r0": 1509470806 + }, + "origin": "libusb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libusb-1.0.so.0=0.1.0" + ] + }, + "py2-django": { + "versions": { + "1.11.20-r0": 1551365028, + "1.11.21-r0": 1561497056, + "1.11.22-r0": 1563783110, + "1.11.23-r0": 1565087790 + }, + "origin": "py-django", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:django-admin-2" + ] + }, + "eggdrop-doc": { + "versions": { + "1.6.21-r2": 1509491901 + }, + "origin": "eggdrop" + }, + "apache-mod-auth-kerb": { + "versions": { + "5.4-r4": 1509495855 + }, + "origin": "apache-mod-auth-kerb", + "dependencies": [ + "apache2", + "heimdal", + "so:libc.musl-x86_64.so.1", + "so:libgssapi.so.3" + ] + }, + "eggdbus": { + "versions": { + "0.6-r5": 1509469812 + }, + "origin": "eggdbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libeggdbus-1.so.0=0.0.0", + "cmd:eggdbus-binding-tool", + "cmd:eggdbus-glib-genmarshal" + ] + }, + "perl-io-stringy": { + "versions": { + "2.111-r0": 1509469881 + }, + "origin": "perl-io-stringy", + "dependencies": [ + "perl" + ] + }, + "lftp-doc": { + "versions": { + "4.8.3-r1": 1510259189 + }, + "origin": "lftp" + }, + "gnome-common": { + "versions": { + "3.18.0-r1": 1509481864 + }, + "origin": "gnome-common", + "provides": [ + "cmd:gnome-autogen.sh" + ] + }, + "wxgtk2.8-media": { + "versions": { + "2.8.12.1-r4": 1510928422 + }, + "origin": "wxgtk2.8", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstinterfaces-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libstdc++.so.6", + "so:libwx_baseu-2.8.so.0", + "so:libwx_gtk2u_core-2.8.so.0" + ], + "provides": [ + "so:libwx_gtk2u_media-2.8.so.0=0.8.0" + ] + }, + "automake": { + "versions": { + "1.15.1-r0": 1509456977 + }, + "origin": "automake", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:aclocal", + "cmd:aclocal-1.15", + "cmd:automake", + "cmd:automake-1.15" + ] + }, + "gtest": { + "versions": { + "1.8.0-r1": 1509475787 + }, + "origin": "gtest", + "dependencies": [ + "libgcc", + "bash", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgtest.so=0", + "so:libgtest_main.so=0" + ] + }, + "qemu-system-sh4": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-sh4" + ] + }, + "perl-file-tail-doc": { + "versions": { + "1.3-r1": 1511889532 + }, + "origin": "perl-file-tail" + }, + "py2-urwid": { + "versions": { + "1.3.1-r2": 1509493953 + }, + "origin": "py-urwid", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "font-isas-misc": { + "versions": { + "1.0.3-r0": 1509488868 + }, + "origin": "font-isas-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-time-hires": { + "versions": { + "1.9746-r0": 1509484014 + }, + "origin": "perl-time-hires", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-future": { + "versions": { + "0.37-r0": 1511989972 + }, + "origin": "perl-future" + }, + "libgsf-lang": { + "versions": { + "1.14.41-r0": 1509467200 + }, + "origin": "libgsf" + }, + "minicom": { + "versions": { + "2.7.1-r0": 1509485711 + }, + "origin": "minicom", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:ascii-xfr", + "cmd:minicom", + "cmd:runscript", + "cmd:xminicom" + ] + }, + "mosh-bash-completion": { + "versions": { + "1.3.2-r3": 1510846214 + }, + "origin": "mosh" + }, + "fping-doc": { + "versions": { + "4.0-r0": 1509483914 + }, + "origin": "fping" + }, + "libunique-doc": { + "versions": { + "1.1.6-r6": 1510069913 + }, + "origin": "libunique" + }, + "libpurple-oscar": { + "versions": { + "2.12.0-r2": 1510069753 + }, + "origin": "pidgin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libaim.so=0", + "so:libicq.so=0", + "so:liboscar.so.0=0.0.0" + ] + }, + "libressl-doc": { + "versions": { + "2.6.5-r0": 1529043911 + }, + "origin": "libressl" + }, + "postgresql-bdr-extension-doc": { + "versions": { + "1.0.2-r1": 1510259297 + }, + "origin": "postgresql-bdr-extension" + }, + "libxklavier-doc": { + "versions": { + "5.4-r2": 1509475733 + }, + "origin": "libxklavier" + }, + "qpdf-fix-qdf": { + "versions": { + "7.0.0-r0": 1509482355 + }, + "origin": "qpdf", + "dependencies": [ + "qpdf", + "perl" + ], + "provides": [ + "cmd:fix-qdf" + ] + }, + "nagios-plugins-pgsql": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "irssi-dev": { + "versions": { + "1.0.6-r0": 1519052408, + "1.0.8-r0": 1562236916 + }, + "origin": "irssi" + }, + "mesa-osmesa": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libOSMesa.so.8=8.0.0" + ] + }, + "yasm": { + "versions": { + "1.3.0-r1": 1509473589 + }, + "origin": "yasm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:vsyasm", + "cmd:yasm", + "cmd:ytasm" + ] + }, + "libnet": { + "versions": { + "1.1.6-r2": 1509481431 + }, + "origin": "libnet", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnet.so.1=1.7.0" + ] + }, + "perl-config-grammar-doc": { + "versions": { + "1.12-r0": 1509483934 + }, + "origin": "perl-config-grammar" + }, + "libsm-doc": { + "versions": { + "1.2.2-r1": 1509466262 + }, + "origin": "libsm" + }, + "gvfs-cdda": { + "versions": { + "1.34.1-r0": 1511430258, + "1.34.1-r1": 1563787223 + }, + "origin": "gvfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcdio.so.16", + "so:libcdio_cdda.so.2", + "so:libcdio_paranoia.so.2", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libgvfscommon.so", + "so:libgvfsdaemon.so", + "so:libintl.so.8" + ] + }, + "perl-regexp-ipv6-doc": { + "versions": { + "0.03-r0": 1509470471 + }, + "origin": "perl-regexp-ipv6" + }, + "lua5.2-gversion.lua": { + "versions": { + "0.2.0-r1": 1509475305 + }, + "origin": "lua-gversion", + "dependencies": [ + "lua5.2" + ] + }, + "st-doc": { + "versions": { + "0.7-r1": 1509494022 + }, + "origin": "st" + }, + "boost-thread": { + "versions": { + "1.62.0-r5": 1509465881 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.62.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_thread-mt.so.1.62.0=1.62.0" + ] + }, + "xfce4-appfinder-lang": { + "versions": { + "4.12.0-r0": 1510074532 + }, + "origin": "xfce4-appfinder", + "dependencies": [ + "hicolor-icon-theme" + ] + }, + "gmp-dev": { + "versions": { + "6.1.2-r1": 1509456917 + }, + "origin": "gmp", + "dependencies": [ + "gmp=6.1.2-r1", + "libgmpxx=6.1.2-r1" + ] + }, + "patchwork": { + "versions": { + "1.1.3-r0": 1509474240, + "1.1.3-r1": 1562223418 + }, + "origin": "patchwork", + "dependencies": [ + "py-django", + "py-django-registration", + "git", + "py-psycopg2" + ] + }, + "mrtg": { + "versions": { + "2.17.4-r4": 1509482698 + }, + "origin": "mrtg", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ], + "provides": [ + "cmd:cfgmaker", + "cmd:indexmaker", + "cmd:mrtg", + "cmd:mrtg-traffic-sum", + "cmd:rateup" + ] + }, + "ncurses5": { + "versions": { + "5.9-r1": 1509483668 + }, + "origin": "ncurses5" + }, + "scrot-doc": { + "versions": { + "0.8.13-r0": 1509481270 + }, + "origin": "scrot" + }, + "squid-lang-oc": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "lua5.3-posixtz": { + "versions": { + "0.5-r1": 1509479784 + }, + "origin": "lua-posixtz", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "opennhrp-doc": { + "versions": { + "0.14.1-r6": 1509490848 + }, + "origin": "opennhrp" + }, + "perl-parse-syslog-doc": { + "versions": { + "1.10-r2": 1509479747 + }, + "origin": "perl-parse-syslog" + }, + "perl-net-ssleay": { + "versions": { + "1.82-r0": 1510260115 + }, + "origin": "perl-net-ssleay", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "pcsc-lite-libs": { + "versions": { + "1.8.22-r0": 1509474498 + }, + "origin": "pcsc-lite", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcsclite.so.1=1.0.0", + "so:libpcscspy.so.0=0.0.0" + ] + }, + "android-tools-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503663 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "lua5.2-xml": { + "versions": { + "130610-r5": 1509482647 + }, + "origin": "lua-xml", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "libsecret-doc": { + "versions": { + "0.18.5-r0": 1509483961 + }, + "origin": "libsecret" + }, + "polkit-dev": { + "versions": { + "0.105-r8": 1551780676, + "0.105-r9": 1563792450, + "0.105-r10": 1567523133 + }, + "origin": "polkit", + "dependencies": [ + "eggdbus-dev", + "dbus-glib-dev", + "pc:gio-2.0>=2.18", + "pc:glib-2.0>=2.18", + "pkgconfig", + "polkit=0.105-r10" + ], + "provides": [ + "pc:polkit-agent-1=0.105", + "pc:polkit-backend-1=0.105", + "pc:polkit-gobject-1=0.105" + ] + }, + "cloog-dev": { + "versions": { + "0.18.4-r1": 1509473492 + }, + "origin": "cloog", + "dependencies": [ + "gmp-dev", + "isl-dev", + "cloog=0.18.4-r1", + "pkgconfig" + ], + "provides": [ + "pc:cloog-isl=0.18.4" + ] + }, + "xf86bigfontproto": { + "versions": { + "1.2.0-r5": 1509461949 + }, + "origin": "xf86bigfontproto" + }, + "libvirt-doc": { + "versions": { + "3.9.0-r1": 1510088351, + "5.5.0-r0": 1562165539 + }, + "origin": "libvirt" + }, + "phodav": { + "versions": { + "2.2-r0": 1509492235 + }, + "origin": "phodav", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libsoup-2.4.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "so:libphodav-2.0.so.0=0.0.0" + ] + }, + "tpaste": { + "versions": { + "0.6-r0": 1509477736 + }, + "origin": "sprunge", + "dependencies": [ + "curl" + ], + "provides": [ + "cmd:tpaste" + ] + }, + "perl-params-validate-doc": { + "versions": { + "1.28-r0": 1509470588 + }, + "origin": "perl-params-validate" + }, + "perl-authen-sasl-doc": { + "versions": { + "2.16-r1": 1510352971 + }, + "origin": "perl-authen-sasl" + }, + "figlet": { + "versions": { + "2.2.5-r0": 1509480744 + }, + "origin": "figlet", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chkfont", + "cmd:figlet", + "cmd:figlist", + "cmd:showfigfonts" + ] + }, + "perl-xml-rss": { + "versions": { + "1.59-r1": 1511389029 + }, + "origin": "perl-xml-rss", + "dependencies": [ + "perl-xml-parser", + "perl-html-parser", + "perl-datetime-format-mail", + "perl-datetime-format-w3cdtf", + "perl-datetime" + ] + }, + "perl-test-exception-doc": { + "versions": { + "0.43-r0": 1509473072 + }, + "origin": "perl-test-exception" + }, + "unixodbc-dev": { + "versions": { + "2.3.4-r2": 1509469609 + }, + "origin": "unixodbc", + "dependencies": [ + "unixodbc=2.3.4-r2" + ] + }, + "unzip-doc": { + "versions": { + "6.0-r3": 1534926377 + }, + "origin": "unzip" + }, + "sntpc": { + "versions": { + "0.9-r6": 1509492306 + }, + "origin": "sntpc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sntpc" + ] + }, + "zip": { + "versions": { + "3.0-r4": 1509457025 + }, + "origin": "zip", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:zip", + "cmd:zipcloak", + "cmd:zipnote", + "cmd:zipsplit" + ] + }, + "twm": { + "versions": { + "1.0.9-r2": 1509493988 + }, + "origin": "twm", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXmu.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:twm" + ] + }, + "pyflakes": { + "versions": { + "1.6.0-r0": 1509492637 + }, + "origin": "pyflakes", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:pyflakes" + ] + }, + "nginx-mod-http-xslt-filter": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ] + }, + "gsl-doc": { + "versions": { + "2.4-r0": 1509493183 + }, + "origin": "gsl" + }, + "open-vm-tools-dev": { + "versions": { + "10.1.15-r0": 1510922342 + }, + "origin": "open-vm-tools", + "dependencies": [ + "open-vm-tools=10.1.15-r0", + "pkgconfig" + ], + "provides": [ + "pc:libDeployPkg=10.1.15", + "pc:vmguestlib=10.1.15" + ] + }, + "ipset-dev": { + "versions": { + "6.34-r1": 1537515181 + }, + "origin": "ipset", + "dependencies": [ + "libmnl-dev", + "ipset=6.34-r1", + "pc:libmnl>=1", + "pkgconfig" + ], + "provides": [ + "pc:libipset=6.34" + ] + }, + "libepoxy": { + "versions": { + "1.4.3-r1": 1510067311 + }, + "origin": "libepoxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libepoxy.so.0=0.0.0" + ] + }, + "execline-doc": { + "versions": { + "2.3.0.3-r0": 1509488629 + }, + "origin": "execline" + }, + "perl-test-identity-doc": { + "versions": { + "0.01-r0": 1509477787 + }, + "origin": "perl-test-identity" + }, + "libdc1394-doc": { + "versions": { + "2.2.5-r1": 1509480970 + }, + "origin": "libdc1394" + }, + "perl-class-method-modifiers-doc": { + "versions": { + "2.12-r0": 1509479362 + }, + "origin": "perl-class-method-modifiers" + }, + "enca-doc": { + "versions": { + "1.19-r1": 1509480334 + }, + "origin": "enca" + }, + "orbit2": { + "versions": { + "2.14.19-r3": 1510928305 + }, + "origin": "orbit2", + "dependencies": [ + "so:libIDL-2.so.0", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libORBit-2.so.0=0.1.0", + "so:libORBit-imodule-2.so.0=0.0.0", + "so:libORBitCosNaming-2.so.0=0.1.0", + "cmd:ior-decode-2", + "cmd:linc-cleanup-sockets", + "cmd:orbit-idl-2", + "cmd:typelib-dump" + ] + }, + "awstats": { + "versions": { + "7.6-r2": 1515159956 + }, + "origin": "awstats", + "dependencies": [ + "perl", + "perl-uri" + ], + "provides": [ + "cmd:awstats_buildstaticpages.pl", + "cmd:awstats_configure.pl", + "cmd:awstats_exportlib.pl", + "cmd:awstats_updateall.pl", + "cmd:geoip_generator.pl", + "cmd:logresolvemerge.pl", + "cmd:maillogconvert.pl", + "cmd:urlaliasbuilder.pl" + ] + }, + "lxc-bridge": { + "versions": { + "2.1.1-r3": 1533578833 + }, + "origin": "lxc", + "dependencies": [ + "dnsmasq" + ] + }, + "soxr-doc": { + "versions": { + "0.1.2-r0": 1509492272 + }, + "origin": "soxr" + }, + "clang-doc": { + "versions": { + "5.0.0-r0": 1510683246 + }, + "origin": "clang" + }, + "perl-posix-strftime-compiler-doc": { + "versions": { + "0.41-r0": 1509481797 + }, + "origin": "perl-posix-strftime-compiler" + }, + "perl-datetime-format-w3cdtf-doc": { + "versions": { + "0.07-r0": 1510859373 + }, + "origin": "perl-datetime-format-w3cdtf" + }, + "nspr": { + "versions": { + "4.17-r0": 1509479384 + }, + "origin": "nspr", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnspr4.so=0", + "so:libplc4.so=0", + "so:libplds4.so=0" + ] + }, + "perl-html-scrubber-doc": { + "versions": { + "0.11-r0": 1509481322 + }, + "origin": "perl-html-scrubber" + }, + "tsocks": { + "versions": { + "1.8_beta5-r0": 1509496537 + }, + "origin": "tsocks", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtsocks.so.1.8=1.8", + "cmd:tsocks" + ] + }, + "alpine-doc": { + "versions": { + "2.21-r1": 1510259111 + }, + "origin": "alpine" + }, + "vsftpd-doc": { + "versions": { + "3.0.3-r4": 1510260062 + }, + "origin": "vsftpd" + }, + "rtnppd": { + "versions": { + "1.7b-r8": 1509491629 + }, + "origin": "rtnppd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rtnppd", + "cmd:testq", + "cmd:tnpppage" + ] + }, + "font-bitstream-type1": { + "versions": { + "1.0.3-r0": 1509489238 + }, + "origin": "font-bitstream-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "libtheora-dev": { + "versions": { + "1.1.1-r13": 1510068272 + }, + "origin": "libtheora", + "dependencies": [ + "libogg-dev", + "libtheora=1.1.1-r13", + "pc:ogg>=1.1", + "pkgconfig" + ], + "provides": [ + "pc:theora=1.1.1", + "pc:theoradec=1.1.1", + "pc:theoraenc=1.1.1" + ] + }, + "libmilter": { + "versions": { + "1.0.2-r5": 1509479369 + }, + "origin": "libmilter", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmilter.so.1.0.2=1.0.2" + ] + }, + "samba-libs": { + "versions": { + "4.7.6-r3": 1555491790 + }, + "origin": "samba", + "dependencies": [ + "so:libasn1-samba4.so.8", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcom_err.so.2", + "so:libgnutls.so.30", + "so:libgssapi-samba4.so.2", + "so:libkrb5-samba4.so.26", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libldb.so.1", + "so:libpopt.so.0", + "so:libreplace-samba4.so", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent.so.0", + "so:libwbclient.so.0", + "so:libz.so.1" + ], + "provides": [ + "so:libCHARSET3-samba4.so=0", + "so:libaddns-samba4.so=0", + "so:libasn1util-samba4.so=0", + "so:libauthkrb5-samba4.so=0", + "so:libcli-cldap-samba4.so=0", + "so:libcli-ldap-common-samba4.so=0", + "so:libcli-nbt-samba4.so=0", + "so:libcli-smb-common-samba4.so=0", + "so:libcliauth-samba4.so=0", + "so:libcmocka-samba4.so=0", + "so:libcommon-auth-samba4.so=0", + "so:libdbwrap-samba4.so=0", + "so:libflag-mapping-samba4.so=0", + "so:libgenrand-samba4.so=0", + "so:libgensec-samba4.so=0", + "so:libgse-samba4.so=0", + "so:libinterfaces-samba4.so=0", + "so:libiov-buf-samba4.so=0", + "so:libkrb5samba-samba4.so=0", + "so:libldbsamba-samba4.so=0", + "so:libmessages-dgm-samba4.so=0", + "so:libmessages-util-samba4.so=0", + "so:libmsghdr-samba4.so=0", + "so:libndr-krb5pac.so.0=0.0.1", + "so:libndr-nbt.so.0=0.0.1", + "so:libndr-samba-samba4.so=0", + "so:libndr-standard.so.0=0.0.1", + "so:libndr.so.0=0.1.0", + "so:libpopt-samba3-samba4.so=0", + "so:libsamba-cluster-support-samba4.so=0", + "so:libsamba-credentials.so.0=0.0.1", + "so:libsamba-debug-samba4.so=0", + "so:libsamba-errors.so.1=1", + "so:libsamba-hostconfig.so.0=0.0.1", + "so:libsamba-modules-samba4.so=0", + "so:libsamba-security-samba4.so=0", + "so:libsamba-sockets-samba4.so=0", + "so:libsamba-util.so.0=0.0.1", + "so:libsamba3-util-samba4.so=0", + "so:libsamdb-common-samba4.so=0", + "so:libsamdb.so.0=0.0.1", + "so:libsecrets3-samba4.so=0", + "so:libserver-id-db-samba4.so=0", + "so:libserver-role-samba4.so=0", + "so:libsmb-transport-samba4.so=0", + "so:libsmbconf.so.0=0", + "so:libsmbd-shim-samba4.so=0", + "so:libsocket-blocking-samba4.so=0", + "so:libsys-rw-samba4.so=0", + "so:libtalloc-report-samba4.so=0", + "so:libtdb-wrap-samba4.so=0", + "so:libtevent-util.so.0=0.0.1", + "so:libtime-basic-samba4.so=0", + "so:libutil-cmdline-samba4.so=0", + "so:libutil-reg-samba4.so=0", + "so:libutil-setid-samba4.so=0", + "so:libutil-tdb-samba4.so=0" + ] + }, + "gtk3-xfce-engine": { + "versions": { + "3.2.0-r2": 1510920446 + }, + "origin": "gtk-xfce-engine", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0" + ] + }, + "qemu-armeb": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-armeb" + ] + }, + "lua5.2-optarg": { + "versions": { + "0.2-r1": 1509462490 + }, + "origin": "lua-optarg", + "dependencies": [ + "lua5.2" + ] + }, + "acf-fetchmail": { + "versions": { + "0.9.0-r2": 1510076434 + }, + "origin": "acf-fetchmail", + "dependencies": [ + "acf-core", + "fetchmail" + ] + }, + "asterisk-alsa": { + "versions": { + "15.6.1-r0": 1537795343, + "15.6.2-r0": 1568705005 + }, + "origin": "asterisk", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.2-libs": { + "versions": { + "5.2.4-r4": 1509459694 + }, + "origin": "lua5.2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblua-5.2.so.0=0.0.0" + ] + }, + "perl-file-slurp-tiny": { + "versions": { + "0.004-r0": 1509494489 + }, + "origin": "perl-file-slurp-tiny" + }, + "perl-devel-checklib": { + "versions": { + "1.11-r1": 1509489315 + }, + "origin": "perl-devel-checklib", + "provides": [ + "cmd:use-devel-checklib" + ] + }, + "myrepos": { + "versions": { + "1.20180726-r0": 1534931445 + }, + "origin": "myrepos", + "dependencies": [ + "perl", + "git" + ], + "provides": [ + "cmd:mr" + ] + }, + "libnetfilter_queue": { + "versions": { + "1.0.2-r0": 1509469244 + }, + "origin": "libnetfilter_queue", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnfnetlink.so.0" + ], + "provides": [ + "so:libnetfilter_queue.so.1=1.3.0" + ] + }, + "neon-doc": { + "versions": { + "0.30.2-r2": 1510285201 + }, + "origin": "neon" + }, + "libao-doc": { + "versions": { + "1.2.0-r2": 1543926072 + }, + "origin": "libao" + }, + "perl-unix-syslog": { + "versions": { + "1.1-r9": 1509484018 + }, + "origin": "perl-unix-syslog", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "libnetfilter_acct": { + "versions": { + "1.0.3-r0": 1509480361 + }, + "origin": "libnetfilter_acct", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0" + ], + "provides": [ + "so:libnetfilter_acct.so.1=1.0.0" + ] + }, + "collectd-python": { + "versions": { + "5.7.2-r0": 1510069648 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "acf-gross": { + "versions": { + "0.6.0-r2": 1510075435 + }, + "origin": "acf-gross", + "dependencies": [ + "acf-core", + "gross" + ] + }, + "nginx-mod-stream": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "scstadmin-doc": { + "versions": { + "2.2.0-r2": 1509492547 + }, + "origin": "scstadmin" + }, + "perl-html-parser": { + "versions": { + "3.72-r2": 1509464394 + }, + "origin": "perl-html-parser", + "dependencies": [ + "perl-html-tagset", + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-gobject3": { + "versions": { + "3.24.1-r2": 1509481892 + }, + "origin": "py-gobject3", + "dependencies": [ + "py2-cairo", + "so:libc.musl-x86_64.so.1", + "so:libcairo-gobject.so.2", + "so:libcairo.so.2", + "so:libffi.so.6", + "so:libgirepository-1.0.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ] + }, + "perl-package-stash-doc": { + "versions": { + "0.37-r0": 1509474043 + }, + "origin": "perl-package-stash" + }, + "libxaw-doc": { + "versions": { + "1.0.13-r0": 1509480584 + }, + "origin": "libxaw" + }, + "libxext": { + "versions": { + "1.3.3-r2": 1509464538 + }, + "origin": "libxext", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXext.so.6=6.4.0" + ] + }, + "fping": { + "versions": { + "4.0-r0": 1509483914 + }, + "origin": "fping", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:fping" + ] + }, + "py2-sphinx_rtd_theme": { + "versions": { + "0.2.4-r0": 1509469909 + }, + "origin": "py-sphinx_rtd_theme", + "dependencies": [ + "python2" + ] + }, + "libmcrypt": { + "versions": { + "2.5.8-r7": 1509493846 + }, + "origin": "libmcrypt", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmcrypt.so.4=4.4.8" + ] + }, + "pingu-doc": { + "versions": { + "1.5-r1": 1509474485 + }, + "origin": "pingu" + }, + "perl-eval-closure-doc": { + "versions": { + "0.14-r0": 1509481597 + }, + "origin": "perl-eval-closure" + }, + "libcdio-paranoia-doc": { + "versions": { + "0.94_p1-r0": 1509473651 + }, + "origin": "libcdio-paranoia" + }, + "imlib2-doc": { + "versions": { + "1.4.10-r0": 1509470515 + }, + "origin": "imlib2" + }, + "abiword-plugin-bmp": { + "versions": { + "3.0.2-r1": 1510073356 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpng16.so.16", + "so:libstdc++.so.6" + ] + }, + "open-vm-tools-gtk": { + "versions": { + "10.1.15-r0": 1510922342 + }, + "origin": "open-vm-tools", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXi.so.6", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libXtst.so.6", + "so:libatkmm-1.6.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairomm-1.0.so.1", + "so:libfuse.so.2", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdkmm-2.4.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libglibmm-2.4.so.1", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libgtkmm-2.4.so.1", + "so:libhgfs.so.0", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6", + "so:libtirpc.so.3", + "so:libvmtools.so.0" + ], + "provides": [ + "cmd:vmware-user-suid-wrapper", + "cmd:vmware-vmblock-fuse" + ] + }, + "coova-chilli-doc": { + "versions": { + "1.3.2-r2": 1510287299 + }, + "origin": "coova-chilli" + }, + "opusfile-dev": { + "versions": { + "0.10-r0": 1512032060 + }, + "origin": "opusfile", + "dependencies": [ + "libogg-dev", + "libressl-dev", + "opus-dev", + "opusfile=0.10-r0", + "pc:ogg>=1.3", + "pc:openssl", + "pc:opus>=1.0.1", + "pkgconfig" + ], + "provides": [ + "pc:opusfile=0.10", + "pc:opusurl=0.10" + ] + }, + "libunique": { + "versions": { + "1.1.6-r6": 1510069913 + }, + "origin": "libunique", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libdbus-glib-1.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ], + "provides": [ + "so:libunique-1.0.so.0=0.100.6" + ] + }, + "pam-pgsql": { + "versions": { + "0.7.3.2-r0": 1509494688 + }, + "origin": "pam-pgsql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libpam.so.0", + "so:libpq.so.5" + ] + }, + "squid-lang-uz": { + "versions": { + "3.5.27-r0": 1519824033, + "3.5.27-r1": 1562865670 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-net-openssh": { + "versions": { + "0.74-r0": 1509473012 + }, + "origin": "perl-net-openssh" + }, + "libbsd-doc": { + "versions": { + "0.8.6-r1": 1509461838 + }, + "origin": "libbsd" + }, + "perl-dbd-mysql": { + "versions": { + "4.043-r0": 1509489325 + }, + "origin": "perl-dbd-mysql", + "dependencies": [ + "perl", + "perl-dbi", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "skalibs": { + "versions": { + "2.6.1.0-r0": 1510364878 + }, + "origin": "skalibs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libskarnet.so.2.6=2.6.1.0" + ] + }, + "jwm": { + "versions": { + "2.3.7-r0": 1510075432 + }, + "origin": "jwm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libXmu.so.6", + "so:libXpm.so.4", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:librsvg-2.so.2" + ], + "provides": [ + "cmd:jwm" + ] + }, + "nginx": { + "versions": { + "1.12.2-r4": 1542814448 + }, + "origin": "nginx", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpcre.so.1", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:nginx" + ] + }, + "libunwind-dbg": { + "versions": { + "1.2.1-r1": 1509493800 + }, + "origin": "libunwind" + }, + "zonenotify": { + "versions": { + "0.1-r3": 1509489771 + }, + "origin": "zonenotify", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:zonenotify" + ] + }, + "ltrace": { + "versions": { + "0.7.3-r1": 1509477339 + }, + "origin": "ltrace", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libelf.so.0" + ], + "provides": [ + "cmd:ltrace" + ] + }, + "libxi-dev": { + "versions": { + "1.7.9-r1": 1509466010 + }, + "origin": "libxi", + "dependencies": [ + "libxi=1.7.9-r1", + "pc:inputproto", + "pc:x11", + "pc:xext", + "pc:xfixes", + "pkgconfig" + ], + "provides": [ + "pc:xi=1.7.9" + ] + }, + "perl-test-simple": { + "versions": { + "1.302118-r0": 1512052687 + }, + "origin": "perl-test-simple", + "provides": [ + "perl-test-tester" + ] + }, + "libnih": { + "versions": { + "1.0.3-r4": 1509473674 + }, + "origin": "libnih", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libexpat.so.1" + ], + "provides": [ + "so:libnih-dbus.so.1=1.0.0", + "so:libnih.so.1=1.0.0", + "cmd:nih-dbus-tool" + ] + }, + "djbdns": { + "versions": { + "1.05-r47": 1509488768 + }, + "origin": "djbdns", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:axfr-get", + "cmd:axfrdns", + "cmd:axfrdns-conf", + "cmd:dnsfilter", + "cmd:dnsipq", + "cmd:dnsmx", + "cmd:dnsname", + "cmd:dnsq", + "cmd:dnstrace", + "cmd:dnstracesort", + "cmd:dnstxt", + "cmd:pickdns", + "cmd:pickdns-conf", + "cmd:pickdns-data", + "cmd:random-ip", + "cmd:rbldns", + "cmd:rbldns-conf", + "cmd:rbldns-data", + "cmd:walldns", + "cmd:walldns-conf" + ] + }, + "boost-system": { + "versions": { + "1.62.0-r5": 1509465880 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libboost_system-mt.so.1.62.0=1.62.0", + "so:libboost_system.so.1.62.0=1.62.0" + ] + }, + "open-isns-lib": { + "versions": { + "0.97-r2": 1510259304 + }, + "origin": "open-isns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ], + "provides": [ + "so:libisns.so.0=0" + ] + }, + "file-dev": { + "versions": { + "5.32-r0": 1509461604, + "5.32-r1": 1563340161, + "5.32-r2": 1572348414 + }, + "origin": "file", + "dependencies": [ + "libmagic=5.32-r2" + ] + }, + "py3-icu": { + "versions": { + "1.9.6-r3": 1510832509 + }, + "origin": "py-icu", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libicui18n.so.59", + "so:libicuuc.so.59", + "so:libpython3.6m.so.1.0", + "so:libstdc++.so.6" + ] + }, + "acf-heimdal": { + "versions": { + "0.6.0-r2": 1510072579 + }, + "origin": "acf-heimdal", + "dependencies": [ + "acf-core", + "heimdal" + ] + }, + "perl-module-pluggable": { + "versions": { + "5.2-r0": 1509483535 + }, + "origin": "perl-module-pluggable" + }, + "xz-doc": { + "versions": { + "5.2.3-r1": 1509459905 + }, + "origin": "xz" + }, + "jasper-dev": { + "versions": { + "2.0.14-r0": 1509475517 + }, + "origin": "jasper", + "dependencies": [ + "jasper-libs=2.0.14-r0", + "pkgconfig" + ], + "provides": [ + "pc:jasper=2.0.14" + ] + }, + "xf86-video-s3virge-doc": { + "versions": { + "1.10.7-r1": 1510074189 + }, + "origin": "xf86-video-s3virge" + }, + "qca-doc": { + "versions": { + "2.1.3-r4": 1510288320 + }, + "origin": "qca" + }, + "i3lock-doc": { + "versions": { + "2.9.1-r0": 1509491492 + }, + "origin": "i3lock" + }, + "lvm2-extra": { + "versions": { + "2.02.175-r0": 1509462403 + }, + "origin": "lvm2", + "dependencies": [ + "bash", + "coreutils" + ], + "provides": [ + "cmd:blkdeactivate", + "cmd:fsadm", + "cmd:lvmconf", + "cmd:lvmdump" + ] + }, + "py2-gnutls": { + "versions": { + "3.1.1-r0": 1509489166 + }, + "origin": "py-gnutls" + }, + "lua5.2-sql-mysql": { + "versions": { + "2.3.5-r1": 1509488830 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "perl-test-without": { + "versions": { + "0.10-r0": 1509496584 + }, + "origin": "perl-test-without" + }, + "newsbeuter-doc": { + "versions": { + "2.9-r6": 1510288208 + }, + "origin": "newsbeuter" + }, + "perl-x10": { + "versions": { + "0.04-r0": 1509494415 + }, + "origin": "perl-x10", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:x10client", + "cmd:x10server" + ] + }, + "libbonobo-doc": { + "versions": { + "2.32.1-r6": 1510931479 + }, + "origin": "libbonobo" + }, + "bonding": { + "versions": { + "2.6-r3": 1509480115 + }, + "origin": "bonding" + }, + "py-django-simple-captcha": { + "versions": { + "0.4.3-r1": 1509491076 + }, + "origin": "py-django-simple-captcha", + "dependencies": [ + "python2" + ] + }, + "perl-locale-maketext-fuzzy": { + "versions": { + "0.11-r1": 1510848492 + }, + "origin": "perl-locale-maketext-fuzzy", + "dependencies": [ + "perl" + ] + }, + "ldns-doc": { + "versions": { + "1.6.17-r6": 1510258937 + }, + "origin": "ldns" + }, + "lttng-ust-dev": { + "versions": { + "2.10.0-r1": 1509492304 + }, + "origin": "lttng-ust", + "dependencies": [ + "lttng-ust=2.10.0-r1", + "pc:liburcu-bp", + "pkgconfig" + ], + "provides": [ + "pc:lttng-ust=2.10.0" + ] + }, + "frotz": { + "versions": { + "2.44-r0": 1509491524 + }, + "origin": "frotz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:frotz" + ] + }, + "dkimproxy": { + "versions": { + "1.4.1-r5": 1509517849 + }, + "origin": "dkimproxy", + "dependencies": [ + "perl-mail-dkim", + "perl-net-server", + "perl-error", + "/bin/sh" + ], + "provides": [ + "cmd:dkim_responder", + "cmd:dkimproxy.in", + "cmd:dkimproxy.out" + ] + }, + "acl": { + "versions": { + "2.2.52-r3": 1509459588 + }, + "origin": "acl", + "dependencies": [ + "so:libacl.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chacl", + "cmd:getfacl", + "cmd:setfacl" + ] + }, + "pidgin-doc": { + "versions": { + "2.12.0-r2": 1510069749 + }, + "origin": "pidgin" + }, + "squid-lang-th": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865670 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "powertop": { + "versions": { + "2.9-r1": 1509552798 + }, + "origin": "powertop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libnl.so.1", + "so:libpci.so.3", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:powertop" + ] + }, + "perl-crypt-des": { + "versions": { + "2.07-r4": 1509471889 + }, + "origin": "perl-crypt-des", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-test-mockobject": { + "versions": { + "1.20161202-r0": 1509489412 + }, + "origin": "perl-test-mockobject", + "dependencies": [ + "perl" + ] + }, + "perl-cgi-session-doc": { + "versions": { + "4.48-r0": 1509475912 + }, + "origin": "perl-cgi-session" + }, + "gd": { + "versions": { + "2.2.5-r3": 1554728246 + }, + "origin": "gd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ], + "provides": [ + "cmd:annotate", + "cmd:gd2copypal", + "cmd:gd2togif", + "cmd:gd2topng", + "cmd:gdcmpgif", + "cmd:gdparttopng", + "cmd:gdtopng", + "cmd:giftogd2", + "cmd:pngtogd", + "cmd:pngtogd2", + "cmd:webpng" + ] + }, + "py-django-pipeline": { + "versions": { + "1.3.25-r0": 1509475944 + }, + "origin": "py-django-pipeline", + "dependencies": [ + "python2" + ] + }, + "ghostscript": { + "versions": { + "9.26-r2": 1554362768, + "9.26-r3": 1565700440, + "9.26-r4": 1571234493 + }, + "origin": "ghostscript", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libcupsimage.so.2", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libjbig2dec.so.0", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libpng16.so.16", + "so:libtiff.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libgs.so.9=9.26", + "so:libijs-0.35.so=0", + "cmd:dvipdf", + "cmd:eps2eps", + "cmd:gs", + "cmd:gsbj", + "cmd:gsc", + "cmd:gsdj", + "cmd:gsdj500", + "cmd:gslj", + "cmd:gslp", + "cmd:gsnd", + "cmd:ijs_client_example", + "cmd:ijs_server_example", + "cmd:lprsetup.sh", + "cmd:pdf2dsc", + "cmd:pdf2ps", + "cmd:pf2afm", + "cmd:pfbtopfa", + "cmd:pphs", + "cmd:printafm", + "cmd:ps2ascii", + "cmd:ps2epsi", + "cmd:ps2pdf", + "cmd:ps2pdf12", + "cmd:ps2pdf13", + "cmd:ps2pdf14", + "cmd:ps2pdfwr", + "cmd:ps2ps", + "cmd:ps2ps2", + "cmd:unix-lpr.sh" + ] + }, + "gvfs-avahi": { + "versions": { + "1.34.1-r0": 1511430258, + "1.34.1-r1": 1563787223 + }, + "origin": "gvfs", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-glib.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgvfscommon.so", + "so:libgvfsdaemon.so", + "so:libintl.so.8" + ] + }, + "libmaa-dev": { + "versions": { + "1.3.2-r0": 1509489137 + }, + "origin": "libmaa", + "dependencies": [ + "libmaa=1.3.2-r0" + ] + }, + "libnetfilter_conntrack-dev": { + "versions": { + "1.0.6-r0": 1509469225 + }, + "origin": "libnetfilter_conntrack", + "dependencies": [ + "libnetfilter_conntrack=1.0.6-r0", + "pc:libnfnetlink", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_conntrack=1.0.6" + ] + }, + "xcb-util-wm-dev": { + "versions": { + "0.4.1-r1": 1509473927 + }, + "origin": "xcb-util-wm", + "dependencies": [ + "xcb-util-dev", + "pkgconfig", + "xcb-util-wm=0.4.1-r1" + ], + "provides": [ + "pc:xcb-ewmh=0.4.1", + "pc:xcb-icccm=0.4.1" + ] + }, + "uwsgi-logpipe": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "abiword-plugin-hancom": { + "versions": { + "3.0.2-r1": 1510073361 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "imlib2": { + "versions": { + "1.4.10-r0": 1509470515 + }, + "origin": "imlib2", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libgif.so.7", + "so:libid3tag.so.0", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libtiff.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libImlib2.so.1=1.4.10", + "cmd:imlib2_bumpmap", + "cmd:imlib2_colorspace", + "cmd:imlib2_conv", + "cmd:imlib2_grab", + "cmd:imlib2_poly", + "cmd:imlib2_show", + "cmd:imlib2_test", + "cmd:imlib2_view" + ] + }, + "lua5.2-dbi-sqlite3": { + "versions": { + "0.6-r1": 1511483402 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "iperf3": { + "versions": { + "3.2-r0": 1509475696 + }, + "origin": "iperf3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libiperf.so.0=0.0.0", + "cmd:iperf3" + ] + }, + "py3-six": { + "versions": { + "1.11.0-r0": 1509465545 + }, + "origin": "py-six", + "dependencies": [ + "python3" + ] + }, + "py3-crypto": { + "versions": { + "2.6.1-r2": 1509483001 + }, + "origin": "py-crypto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libpython3.6m.so.1.0" + ] + }, + "nagios-plugins-all": { + "versions": { + "2.2.1-r3": 1510288499 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins-breeze", + "nagios-plugins-by_ssh", + "nagios-plugins-cluster", + "nagios-plugins-dbi", + "nagios-plugins-dhcp", + "nagios-plugins-dig", + "nagios-plugins-disk_smb", + "nagios-plugins-disk", + "nagios-plugins-dns", + "nagios-plugins-dummy", + "nagios-plugins-file_age", + "nagios-plugins-fping", + "nagios-plugins-hpjd", + "nagios-plugins-http", + "nagios-plugins-icmp", + "nagios-plugins-ide_smart", + "nagios-plugins-ifoperstatus", + "nagios-plugins-ifstatus", + "nagios-plugins-ircd", + "nagios-plugins-ldap", + "nagios-plugins-load", + "nagios-plugins-log", + "nagios-plugins-mailq", + "nagios-plugins-mrtgtraf", + "nagios-plugins-mrtg", + "nagios-plugins-mysql", + "nagios-plugins-nagios", + "nagios-plugins-ntp", + "nagios-plugins-nt", + "nagios-plugins-nwstat", + "nagios-plugins-overcr", + "nagios-plugins-pgsql", + "nagios-plugins-ping", + "nagios-plugins-procs", + "nagios-plugins-radius", + "nagios-plugins-real", + "nagios-plugins-rpc", + "nagios-plugins-sensors", + "nagios-plugins-smtp", + "nagios-plugins-snmp", + "nagios-plugins-ssh", + "nagios-plugins-swap", + "nagios-plugins-time", + "nagios-plugins-ups", + "nagios-plugins-uptime", + "nagios-plugins-users", + "nagios-plugins-wave", + "nagios-plugins-openrc", + "nagios-plugins-tcp" + ] + }, + "libxslt-dev": { + "versions": { + "1.1.31-r1": 1555487793, + "1.1.31-r2": 1572541164 + }, + "origin": "libxslt", + "dependencies": [ + "libxslt=1.1.31-r2", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libexslt=0.8.19", + "pc:libxslt=1.1.31", + "cmd:xslt-config" + ] + }, + "lxc-doc": { + "versions": { + "2.1.1-r3": 1533578832 + }, + "origin": "lxc" + }, + "tcpflow": { + "versions": { + "1.4.5-r3": 1510260713, + "1.5.0-r1": 1562596229 + }, + "origin": "tcpflow", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.0.0", + "so:libgcc_s.so.1", + "so:libpcap.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:tcpflow" + ] + }, + "hexchat-doc": { + "versions": { + "2.12.4-r1": 1510260816 + }, + "origin": "hexchat" + }, + "uwsgi-transformation_gzip": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "freeradius-redis": { + "versions": { + "3.0.15-r4": 1556202797, + "3.0.15-r5": 1566310605 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.13" + ], + "provides": [ + "so:rlm_redis.so=0", + "so:rlm_rediswho.so=0" + ] + }, + "gtkspell-lang": { + "versions": { + "2.0.16-r6": 1510069682 + }, + "origin": "gtkspell" + }, + "libtorrent": { + "versions": { + "0.13.6-r3": 1510259942 + }, + "origin": "libtorrent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libtorrent.so.19=19.0.0" + ] + }, + "lua-rex-pcre": { + "versions": { + "2.9.0-r0": 1510619366 + }, + "origin": "lua-rex" + }, + "perl-net-server": { + "versions": { + "2.009-r0": 1509479737 + }, + "origin": "perl-net-server", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:net-server" + ] + }, + "perl-namespace-clean-doc": { + "versions": { + "0.27-r0": 1509474074 + }, + "origin": "perl-namespace-clean" + }, + "kmod-doc": { + "versions": { + "24-r0": 1509462316 + }, + "origin": "kmod" + }, + "tzdata-doc": { + "versions": { + "2019a-r0": 1556626900, + "2019b-r0": 1566548308, + "2019c-r0": 1571319267 + }, + "origin": "tzdata" + }, + "gst-plugins-good0.10-lang": { + "versions": { + "0.10.31-r0": 1510068452 + }, + "origin": "gst-plugins-good0.10" + }, + "perl-net-server-doc": { + "versions": { + "2.009-r0": 1509479736 + }, + "origin": "perl-net-server" + }, + "uwsgi-ugreen": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "mcabber-dev": { + "versions": { + "1.1.0-r0": 1510588318 + }, + "origin": "mcabber", + "dependencies": [ + "pc:glib-2.0", + "pc:gmodule-2.0", + "pc:loudmouth-1.0", + "pkgconfig" + ], + "provides": [ + "pc:mcabber=1.1.0" + ] + }, + "uwsgi-transformation_tofile": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-io-multiplex-doc": { + "versions": { + "1.16-r0": 1509479739 + }, + "origin": "perl-io-multiplex" + }, + "perl-ldap": { + "versions": { + "0.65-r1": 1510564476 + }, + "origin": "perl-ldap", + "dependencies": [ + "perl-convert-asn1" + ], + "provides": [ + "perl-net-ldap" + ] + }, + "libnice-doc": { + "versions": { + "0.1.14-r2": 1510931344 + }, + "origin": "libnice" + }, + "libunistring-doc": { + "versions": { + "0.9.7-r0": 1509458999 + }, + "origin": "libunistring" + }, + "libwebsockets-doc": { + "versions": { + "2.4.0-r1": 1510258054 + }, + "origin": "libwebsockets" + }, + "gcr-doc": { + "versions": { + "3.20.0-r1": 1510073693 + }, + "origin": "gcr" + }, + "libmodplug": { + "versions": { + "0.8.9.0-r0": 1509489449 + }, + "origin": "libmodplug", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libmodplug.so.1=1.0.0" + ] + }, + "at-spi2-core-lang": { + "versions": { + "2.26.2-r0": 1509466052 + }, + "origin": "at-spi2-core" + }, + "v4l-utils-dvbv5": { + "versions": { + "1.12.5-r1": 1510072044 + }, + "origin": "v4l-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libudev.so.1" + ], + "provides": [ + "so:libdvbv5.so.0=0.0.0", + "cmd:dvb-fe-tool", + "cmd:dvb-format-convert", + "cmd:dvbv5-daemon", + "cmd:dvbv5-scan", + "cmd:dvbv5-zap" + ] + }, + "ncdu": { + "versions": { + "1.12-r0": 1509495341 + }, + "origin": "ncdu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:ncdu" + ] + }, + "qemu-microblazeel": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-microblazeel" + ] + }, + "libvirt-uml": { + "versions": { + "3.9.0-r1": 1510088352, + "5.5.0-r0": 1562165540 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=5.5.0-r0", + "libvirt-common-drivers=5.5.0-r0" + ] + }, + "xf86-video-xgixp-doc": { + "versions": { + "1.8.1-r8": 1510069922 + }, + "origin": "xf86-video-xgixp" + }, + "sems-jsonrpc": { + "versions": { + "1.6.0-r6": 1510260897 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libev.so.4", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "py3-zope-interface": { + "versions": { + "4.3.2-r1": 1509493283 + }, + "origin": "py-zope-interface", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "py3-oauthlib": { + "versions": { + "2.0.6-r1": 1509552760 + }, + "origin": "py-oauthlib", + "dependencies": [ + "py3-crypto", + "py3-jwt", + "python3" + ] + }, + "gnome-vfs-doc": { + "versions": { + "2.24.4-r6": 1510931437 + }, + "origin": "gnome-vfs" + }, + "perl-cgi-psgi": { + "versions": { + "0.15-r1": 1510564479 + }, + "origin": "perl-cgi-psgi", + "dependencies": [ + "perl-cgi" + ] + }, + "augeas-tests": { + "versions": { + "1.9.0-r3": 1511899746 + }, + "origin": "augeas" + }, + "spl": { + "versions": { + "0.7.1-r0": 1509492193 + }, + "origin": "spl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:splat", + "cmd:splslab.py" + ] + }, + "uwsgi-router_redis": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "gtkspell-doc": { + "versions": { + "2.0.16-r6": 1510069681 + }, + "origin": "gtkspell" + }, + "ruby-augeas": { + "versions": { + "0.5.0-r3": 1509482368 + }, + "origin": "ruby-augeas", + "dependencies": [ + "ruby", + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.4" + ] + }, + "libcue": { + "versions": { + "2.1.0-r0": 1509494076 + }, + "origin": "libcue", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcue.so.2=2.1.0" + ] + }, + "perl-module-scandeps": { + "versions": { + "1.24-r0": 1510352936 + }, + "origin": "perl-module-scandeps", + "provides": [ + "cmd:scandeps.pl" + ] + }, + "libsamplerate-dev": { + "versions": { + "0.1.9-r0": 1509473264 + }, + "origin": "libsamplerate", + "dependencies": [ + "libsamplerate=0.1.9-r0", + "pkgconfig" + ], + "provides": [ + "pc:samplerate=0.1.9" + ] + }, + "dovecot-fts-solr": { + "versions": { + "2.2.36.3-r0": 1554102391, + "2.2.36.4-r0": 1567075099 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot", + "so:lib20_fts_plugin.so", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ], + "provides": [ + "so:lib21_fts_solr_plugin.so=0" + ] + }, + "linenoise-dev": { + "versions": { + "1.0-r1": 1509462460 + }, + "origin": "linenoise", + "dependencies": [ + "linenoise=1.0-r1" + ] + }, + "fakeroot": { + "versions": { + "1.21-r1": 1509459623 + }, + "origin": "fakeroot", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfakeroot-0.so=0", + "cmd:faked", + "cmd:fakeroot" + ] + }, + "poppler-doc": { + "versions": { + "0.56.0-r0": 1509465066, + "0.56.0-r1": 1569334548 + }, + "origin": "poppler" + }, + "perl-canary-stability": { + "versions": { + "2012-r0": 1509474458 + }, + "origin": "perl-canary-stability" + }, + "libnice-gstreamer0.10": { + "versions": { + "0.1.14-r2": 1510931344 + }, + "origin": "libnice", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libnice.so.10" + ] + }, + "lua5.2-pty": { + "versions": { + "1.2.1-r1": 1509496423 + }, + "origin": "lua-pty", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "cracklib": { + "versions": { + "2.9.6-r0": 1509485917 + }, + "origin": "cracklib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcrack.so.2=2.9.0", + "cmd:cracklib-check", + "cmd:cracklib-format", + "cmd:cracklib-packer", + "cmd:cracklib-unpacker", + "cmd:create-cracklib-dict" + ] + }, + "gnu-efi": { + "versions": { + "3.0.4-r1": 1509482584 + }, + "origin": "gnu-efi" + }, + "mkfontdir-doc": { + "versions": { + "1.0.7-r1": 1509469874 + }, + "origin": "mkfontdir" + }, + "opus": { + "versions": { + "1.2.1-r1": 1509468917 + }, + "origin": "opus", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libopus.so.0=0.6.1" + ] + }, + "lua5.1-sql-postgres": { + "versions": { + "2.3.5-r1": 1509488829 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "rdiff-backup-doc": { + "versions": { + "1.2.8-r2": 1509483271 + }, + "origin": "rdiff-backup" + }, + "collectd-virt": { + "versions": { + "5.7.2-r0": 1510069650 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libvirt.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "collectd-libvirt" + ] + }, + "perl-encode-locale-doc": { + "versions": { + "1.05-r1": 1509464360 + }, + "origin": "perl-encode-locale" + }, + "perl-symbol-global-name-doc": { + "versions": { + "0.05-r1": 1510564552 + }, + "origin": "perl-symbol-global-name" + }, + "zfs-doc": { + "versions": { + "0.7.3-r0": 1509490073 + }, + "origin": "zfs" + }, + "nfdump-dbg": { + "versions": { + "1.6.15-r0": 1509494467, + "1.6.15-r1": 1574265426 + }, + "origin": "nfdump" + }, + "augeas-dev": { + "versions": { + "1.9.0-r3": 1511899746 + }, + "origin": "augeas", + "dependencies": [ + "augeas-libs=1.9.0-r3", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:augeas=1.9.0" + ] + }, + "xf86-input-libinput": { + "versions": { + "0.26.0-r0": 1510303212 + }, + "origin": "xf86-input-libinput", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libinput.so.10" + ] + }, + "qemu-mipsel": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mipsel" + ] + }, + "perl-tree-simple-doc": { + "versions": { + "1.18-r1": 1509494481 + }, + "origin": "perl-tree-simple" + }, + "glade3-doc": { + "versions": { + "3.8.5-r4": 1510067963 + }, + "origin": "glade3" + }, + "shorewall-doc": { + "versions": { + "5.1.8-r0": 1509481282 + }, + "origin": "shorewall" + }, + "acf-weblog": { + "versions": { + "0.11.1-r1": 1510074829 + }, + "origin": "acf-weblog", + "dependencies": [ + "acf-core", + "lua-sql-postgres", + "wget", + "postgresql-client", + "lua-subprocess", + "/bin/sh" + ], + "provides": [ + "cmd:acf-weblog-update-schema" + ] + }, + "pidgin-dev": { + "versions": { + "2.12.0-r2": 1510069747 + }, + "origin": "pidgin", + "dependencies": [ + "finch=2.12.0-r2", + "libpurple=2.12.0-r2", + "pc:glib-2.0", + "pc:gtk+-2.0", + "pkgconfig" + ], + "provides": [ + "pc:finch=2.12.0", + "pc:gnt=2.12.0", + "pc:pidgin=2.12.0", + "pc:purple=2.12.0" + ] + }, + "herbstluftwm-doc": { + "versions": { + "0.7.0-r1": 1509551853 + }, + "origin": "herbstluftwm" + }, + "font-daewoo-misc": { + "versions": { + "1.0.3-r0": 1509495332 + }, + "origin": "font-daewoo-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "bind-libs": { + "versions": { + "9.11.6_p1-r1": 1556872767, + "9.11.8-r0": 1561323229 + }, + "origin": "bind", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libbind9.so.161=161.0.2", + "so:libdns.so.1106=1106.0.1", + "so:libirs.so.161=161.0.0", + "so:libisc.so.1100=1100.2.0", + "so:libisccc.so.161=161.0.1", + "so:libisccfg.so.163=163.0.1", + "so:liblwres.so.161=161.0.1" + ] + }, + "perl-cache-cache": { + "versions": { + "1.08-r0": 1509469202 + }, + "origin": "perl-cache-cache", + "dependencies": [ + "perl-error", + "perl-digest-sha1" + ] + }, + "freeradius-perl": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310604 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "perl", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ], + "provides": [ + "freeradius3-perl=3.0.15-r5", + "so:rlm_perl.so=0" + ] + }, + "uwsgi-emperor_amqp": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "qpdf-libs": { + "versions": { + "7.0.0-r0": 1509482355 + }, + "origin": "qpdf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libjpeg.so.8", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libqpdf.so.18=18.1.0" + ] + }, + "libspf2": { + "versions": { + "1.2.10-r2": 1509491334 + }, + "origin": "libspf2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libspf2.so.2=2.1.0" + ] + }, + "sdl2_mixer": { + "versions": { + "2.0.2-r0": 1511859191 + }, + "origin": "sdl2_mixer", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL2_mixer-2.0.so.0=0.2.0" + ] + }, + "perl-digest-sha1": { + "versions": { + "2.13-r9": 1509468921 + }, + "origin": "perl-digest-sha1", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "openldap-overlay-unique": { + "versions": { + "2.4.45-r3": 1510258136, + "2.4.46-r0": 1565073941, + "2.4.48-r0": 1566900518 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "parole-lang": { + "versions": { + "0.9.2-r1": 1510075085 + }, + "origin": "parole", + "dependencies": [ + "gst-plugins-good" + ] + }, + "cvs-doc": { + "versions": { + "1.11.23-r0": 1509481179 + }, + "origin": "cvs" + }, + "py-purl": { + "versions": { + "1.3.1-r0": 1509483543 + }, + "origin": "py-purl", + "dependencies": [ + "py-six" + ] + }, + "freeglut": { + "versions": { + "3.0.0-r0": 1510071762 + }, + "origin": "freeglut", + "dependencies": [ + "so:libGL.so.1", + "so:libX11.so.6", + "so:libXi.so.6", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libglut.so.3=3.10.0" + ] + }, + "mesa-egl": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libexpat.so.1", + "so:libgbm.so.1", + "so:libwayland-client.so.0", + "so:libwayland-server.so.0", + "so:libxcb-dri2.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb-xfixes.so.0", + "so:libxcb.so.1", + "so:libxshmfence.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libEGL.so.1=1.0.0" + ] + }, + "py-newt": { + "versions": { + "0.52.20-r0": 1509476145 + }, + "origin": "newt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnewt.so.0.52" + ] + }, + "py3-ipaddress": { + "versions": { + "1.0.18-r1": 1509480832 + }, + "origin": "py-ipaddress", + "dependencies": [ + "python3" + ] + }, + "wv-dev": { + "versions": { + "1.2.9-r3": 1509483981 + }, + "origin": "wv", + "dependencies": [ + "pc:libgsf-1>=1.13.0", + "pkgconfig", + "wv=1.2.9-r3" + ], + "provides": [ + "pc:wv-1.0=1.2.9" + ] + }, + "font-bh-ttf": { + "versions": { + "1.0.3-r0": 1509491291 + }, + "origin": "font-bh-ttf", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-universal-require": { + "versions": { + "0.13-r0": 1509469905 + }, + "origin": "perl-universal-require", + "dependencies": [ + "perl" + ] + }, + "exo": { + "versions": { + "0.11.5-r0": 1510068072 + }, + "origin": "exo", + "dependencies": [ + "hicolor-icon-theme", + "so:libX11.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxfce4ui-2.so.0", + "so:libxfce4util.so.7" + ], + "provides": [ + "so:libexo-1.so.0=0.1.0", + "so:libexo-2.so.0=0.1.0", + "cmd:exo-csource", + "cmd:exo-desktop-item-edit", + "cmd:exo-open", + "cmd:exo-preferred-applications" + ] + }, + "perl-sys-mmap": { + "versions": { + "0.19-r1": 1509489718 + }, + "origin": "perl-sys-mmap", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-gnome-gnomevfs": { + "versions": { + "2.28.1-r5": 1510933056 + }, + "origin": "py-gnome", + "dependencies": [ + "gnome-vfs", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgnomevfs-2.so.0", + "so:libgobject-2.0.so.0", + "so:libpython2.7.so.1.0" + ] + }, + "linux-hardened-dev": { + "versions": { + "4.9.65-r1": 1511798414 + }, + "origin": "linux-hardened", + "dependencies": [ + "gmp-dev", + "bash", + "so:libc.musl-x86_64.so.1", + "so:libelf.so.1", + "so:libstdc++.so.6" + ] + }, + "boost-date_time": { + "versions": { + "1.62.0-r5": 1509465874 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_date_time-mt.so.1.62.0=1.62.0", + "so:libboost_date_time.so.1.62.0=1.62.0" + ] + }, + "ruby-rdoc": { + "versions": { + "2.4.6-r0": 1557166823 + }, + "origin": "ruby", + "dependencies": [ + "ruby", + "ruby-json", + "ruby-io-console" + ], + "provides": [ + "cmd:rdoc", + "cmd:ri" + ] + }, + "libnl": { + "versions": { + "1.1.4-r0": 1509473008 + }, + "origin": "libnl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnl.so.1=1.1.4" + ] + }, + "libasyncns": { + "versions": { + "0.8-r0": 1509490869 + }, + "origin": "libasyncns", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libasyncns.so.0=0.3.1" + ] + }, + "swatch-doc": { + "versions": { + "3.2.4-r4": 1510564574 + }, + "origin": "swatch" + }, + "serf": { + "versions": { + "1.3.9-r3": 1510314338 + }, + "origin": "serf", + "dependencies": [ + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:libserf-1.so.1=1.3.0" + ] + }, + "libtirpc-doc": { + "versions": { + "1.0.1-r2": 1509469767 + }, + "origin": "libtirpc" + }, + "perl-compress-raw-zlib": { + "versions": { + "2.076-r0": 1511795062 + }, + "origin": "perl-compress-raw-zlib", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "cvechecker": { + "versions": { + "3.8-r1": 1510072447 + }, + "origin": "cvechecker", + "dependencies": [ + "gawk", + "wget", + "libxslt", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libconfig.so.9", + "so:libmysqlclient.so.18", + "so:libsqlite3.so.0" + ], + "provides": [ + "cmd:cvechecker", + "cmd:cvegenversdat", + "cmd:cvereport", + "cmd:cverules", + "cmd:pullcves" + ] + }, + "libvorbis-dev": { + "versions": { + "1.3.6-r2": 1549615971 + }, + "origin": "libvorbis", + "dependencies": [ + "libvorbis=1.3.6-r2", + "pc:ogg", + "pkgconfig" + ], + "provides": [ + "pc:vorbis=1.3.6", + "pc:vorbisenc=1.3.6", + "pc:vorbisfile=1.3.6" + ] + }, + "augeas-libs": { + "versions": { + "1.9.0-r3": 1511899746 + }, + "origin": "augeas", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "so:libaugeas.so.0=0.23.0", + "so:libfa.so.1=1.4.5" + ] + }, + "audit-libs": { + "versions": { + "2.7.7-r1": 1509491479 + }, + "origin": "audit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0" + ], + "provides": [ + "so:libaudit.so.1=1.0.0", + "so:libauparse.so.0=0.0.0" + ] + }, + "libgcrypt": { + "versions": { + "1.8.3-r0": 1529408085, + "1.8.3-r1": 1564327775, + "1.8.3-r2": 1574265351 + }, + "origin": "libgcrypt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgpg-error.so.0" + ], + "provides": [ + "so:libgcrypt.so.20=20.2.3", + "cmd:dumpsexp", + "cmd:hmac256", + "cmd:mpicalc" + ] + }, + "rrdtool": { + "versions": { + "1.5.6-r3": 1512296350 + }, + "origin": "rrdtool", + "dependencies": [ + "font-sony-misc", + "so:libc.musl-x86_64.so.1", + "so:librrd.so.4" + ], + "provides": [ + "cmd:rrdtool" + ] + }, + "perl-data-page-pageset-doc": { + "versions": { + "1.02-r1": 1509493852 + }, + "origin": "perl-data-page-pageset" + }, + "ipvsadm": { + "versions": { + "1.29-r0": 1509494682 + }, + "origin": "ipvsadm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnl.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:ipvsadm", + "cmd:ipvsadm-restore", + "cmd:ipvsadm-save" + ] + }, + "openssh-sftp-server": { + "versions": { + "7.5_p1-r10": 1551712288 + }, + "origin": "openssh", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "squid-lang-tr": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865670 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "ngircd": { + "versions": { + "24-r2": 1510259315 + }, + "origin": "ngircd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpam.so.0", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:ngircd" + ] + }, + "chrpath": { + "versions": { + "0.16-r1": 1509466380 + }, + "origin": "chrpath", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chrpath" + ] + }, + "sdl2_image": { + "versions": { + "2.0.2-r1": 1528280868, + "2.0.5-r0": 1568803593 + }, + "origin": "sdl2_image", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL2_image-2.0.so.0=0.2.3" + ] + }, + "bluez": { + "versions": { + "5.47-r3": 1510069788 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:bluemoon", + "cmd:bluetoothctl", + "cmd:bluez-simple-agent", + "cmd:btattach", + "cmd:hex2hcd", + "cmd:l2ping", + "cmd:l2test", + "cmd:mpris-proxy", + "cmd:rctest" + ] + }, + "libsecret": { + "versions": { + "0.18.5-r0": 1509483964 + }, + "origin": "libsecret", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libsecret-1.so.0=0.0.0", + "cmd:secret-tool" + ] + }, + "ortp-dev": { + "versions": { + "0.25.0-r0": 1509479727 + }, + "origin": "ortp", + "dependencies": [ + "ortp=0.25.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:ortp=0.25.0" + ] + }, + "perl-business-hours-doc": { + "versions": { + "0.12-r1": 1511889850 + }, + "origin": "perl-business-hours" + }, + "open-lldp-dev": { + "versions": { + "0.9.46-r3": 1510075776 + }, + "origin": "open-lldp", + "dependencies": [ + "open-lldp=0.9.46-r3", + "pkgconfig" + ], + "provides": [ + "pc:liblldp_clif=1.0.0", + "pc:lldpad=0.9.46" + ] + }, + "perl-test-failwarnings": { + "versions": { + "0.008-r1": 1510855678 + }, + "origin": "perl-test-failwarnings" + }, + "libcanberra": { + "versions": { + "0.30-r1": 1510071977 + }, + "origin": "libcanberra", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libltdl.so.7", + "so:libvorbisfile.so.3" + ], + "provides": [ + "so:libcanberra.so.0=0.2.5" + ] + }, + "libatomic_ops-doc": { + "versions": { + "7.4.8-r0": 1509878778 + }, + "origin": "libatomic_ops" + }, + "perl-utils": { + "versions": { + "5.26.3-r0": 1543940997 + }, + "origin": "perl", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:corelist", + "cmd:cpan", + "cmd:encguess", + "cmd:h2ph", + "cmd:instmodsh", + "cmd:json_pp", + "cmd:libnetcfg", + "cmd:perlbug", + "cmd:perlthanks", + "cmd:piconv", + "cmd:pl2pm", + "cmd:prove", + "cmd:ptar", + "cmd:ptardiff", + "cmd:ptargrep", + "cmd:shasum", + "cmd:splain", + "cmd:zipdetails" + ] + }, + "gst-plugins-bad0.10-doc": { + "versions": { + "0.10.23-r7": 1510287374 + }, + "origin": "gst-plugins-bad0.10" + }, + "openbox": { + "versions": { + "3.6.1-r1": 1510073886 + }, + "origin": "openbox", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libXcursor.so.1", + "so:libXext.so.6", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libintl.so.8", + "so:libobrender.so.32", + "so:libobt.so.2", + "so:libstartup-notification-1.so.0" + ], + "provides": [ + "cmd:obxprop", + "cmd:openbox", + "cmd:openbox-session", + "cmd:setlayout" + ] + }, + "libraw-dev": { + "versions": { + "0.18.6-r0": 1514450639 + }, + "origin": "libraw", + "dependencies": [ + "libraw=0.18.6-r0", + "pkgconfig" + ], + "provides": [ + "pc:libraw=0.18.6", + "pc:libraw_r=0.18.6" + ] + }, + "py-oauthlib": { + "versions": { + "2.0.6-r1": 1509552760 + }, + "origin": "py-oauthlib", + "dependencies": [ + "py-crypto", + "py-jwt" + ] + }, + "xfce4-dev-tools": { + "versions": { + "4.12.0-r0": 1510073856 + }, + "origin": "xfce4-dev-tools", + "dependencies": [ + "automake", + "autoconf", + "make", + "intltool", + "pkgconfig", + "libtool", + "gtk-doc", + "glib-dev", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "cmd:xdt-autogen", + "cmd:xdt-commit", + "cmd:xdt-csource" + ] + }, + "aconf-mod-network": { + "versions": { + "0.6.5-r0": 1510073709 + }, + "origin": "aconf", + "dependencies": [ + "aconf" + ] + }, + "py-gnome-libgnome": { + "versions": { + "2.28.1-r5": 1510933056 + }, + "origin": "py-gnome", + "dependencies": [ + "py-gtk", + "py-gnome-bonobo", + "py-gnome-gnomecanvas", + "py-gnome-gnomevfs", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgnome-2.so.0", + "so:libgobject-2.0.so.0", + "so:libpopt.so.0", + "so:libpython2.7.so.1.0" + ] + }, + "aspell-utils": { + "versions": { + "0.60.6.1-r12": 1509472867, + "0.60.6.1-r13": 1572541141 + }, + "origin": "aspell", + "dependencies": [ + "aspell", + "perl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:aspell-import", + "cmd:precat", + "cmd:preunzip", + "cmd:prezip", + "cmd:prezip-bin", + "cmd:run-with-aspell", + "cmd:word-list-compress" + ] + }, + "openldap-back-relay": { + "versions": { + "2.4.45-r3": 1510258133, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "lua5.1-cjson": { + "versions": { + "2.1.0-r7": 1509462473 + }, + "origin": "lua-cjson", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-sys-mmap-doc": { + "versions": { + "0.19-r1": 1509489717 + }, + "origin": "perl-sys-mmap" + }, + "libssh": { + "versions": { + "0.7.6-r0": 1540400366 + }, + "origin": "libssh", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libz.so.1" + ], + "provides": [ + "so:libssh.so.4=4.4.3", + "so:libssh_threads.so.4=4.4.3" + ] + }, + "py-lxml": { + "versions": { + "4.1.1-r0": 1510088209 + }, + "origin": "py-lxml" + }, + "font-winitzki-cyrillic": { + "versions": { + "1.0.3-r0": 1509477635 + }, + "origin": "font-winitzki-cyrillic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-cache-cache-doc": { + "versions": { + "1.08-r0": 1509469199 + }, + "origin": "perl-cache-cache" + }, + "krb5-conf": { + "versions": { + "1.0-r1": 1509469246 + }, + "origin": "krb5-conf" + }, + "perl-crypt-ssleay-doc": { + "versions": { + "0.72-r7": 1510260987 + }, + "origin": "perl-crypt-ssleay" + }, + "libvirt-static": { + "versions": { + "3.9.0-r1": 1510088351, + "5.5.0-r0": 1562165539 + }, + "origin": "libvirt", + "dependencies": [ + "lvm2" + ] + }, + "celt051": { + "versions": { + "0.5.1.3-r0": 1509475638 + }, + "origin": "celt051", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0" + ], + "provides": [ + "so:libcelt051.so.0=0.0.0", + "cmd:celtdec051", + "cmd:celtenc051" + ] + }, + "clamav-doc": { + "versions": { + "0.100.3-r0": 1555508237 + }, + "origin": "clamav" + }, + "net-snmp": { + "versions": { + "5.7.3-r10": 1510259617 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.30", + "so:libnetsnmpagent.so.30", + "so:libnetsnmpmibs.so.30", + "so:libnetsnmptrapd.so.30" + ], + "provides": [ + "cmd:snmpd", + "cmd:snmptrapd" + ] + }, + "perl-pathtools-doc": { + "versions": { + "3.62-r3": 1509488596 + }, + "origin": "perl-pathtools" + }, + "perl-mail-domainkeys-doc": { + "versions": { + "1.0-r2": 1509494035 + }, + "origin": "perl-mail-domainkeys" + }, + "perl-cgi-emulate-psgi-doc": { + "versions": { + "0.23-r0": 1509493702 + }, + "origin": "perl-cgi-emulate-psgi" + }, + "eudev-dev": { + "versions": { + "3.2.4-r1": 1509466081 + }, + "origin": "eudev", + "dependencies": [ + "eudev=3.2.4-r1", + "pkgconfig" + ], + "provides": [ + "pc:libudev=220", + "pc:udev=220" + ] + }, + "xfce4-power-manager-doc": { + "versions": { + "1.6.0-r2": 1510068241 + }, + "origin": "xfce4-power-manager" + }, + "uwsgi-rpc": { + "versions": { + "2.0.17-r0": 1522154658 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-html-formattext-withlinks-andtables": { + "versions": { + "0.07-r0": 1509481579 + }, + "origin": "perl-html-formattext-withlinks-andtables", + "dependencies": [ + "perl-html-formattext-withlinks" + ] + }, + "perl-list-utilsby-doc": { + "versions": { + "0.10-r0": 1509488642 + }, + "origin": "perl-list-utilsby" + }, + "mpfr3-doc": { + "versions": { + "3.1.5-r1": 1509457049 + }, + "origin": "mpfr3" + }, + "sc-doc": { + "versions": { + "7.16-r4": 1509495483 + }, + "origin": "sc" + }, + "db-utils": { + "versions": { + "5.3.28-r0": 1509469315 + }, + "origin": "db", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ], + "provides": [ + "cmd:db_archive", + "cmd:db_checkpoint", + "cmd:db_deadlock", + "cmd:db_dump", + "cmd:db_hotbackup", + "cmd:db_load", + "cmd:db_log_verify", + "cmd:db_printlog", + "cmd:db_recover", + "cmd:db_replicate", + "cmd:db_stat", + "cmd:db_tuner", + "cmd:db_upgrade", + "cmd:db_verify" + ] + }, + "lua-posix-doc": { + "versions": { + "33.4.0-r0": 1509468341 + }, + "origin": "lua-posix" + }, + "xauth": { + "versions": { + "1.0.10-r1": 1509473717 + }, + "origin": "xauth", + "dependencies": [ + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXext.so.6", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xauth" + ] + }, + "perl-pathtools": { + "versions": { + "3.62-r3": 1509488596 + }, + "origin": "perl-pathtools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libgd": { + "versions": { + "2.2.5-r3": 1554728246 + }, + "origin": "gd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libwebp.so.7", + "so:libz.so.1" + ], + "provides": [ + "so:libgd.so.3=3.0.5" + ] + }, + "qemu-system-m68k": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-m68k" + ] + }, + "iso-codes": { + "versions": { + "3.75-r0": 1509466022 + }, + "origin": "iso-codes" + }, + "perl-test-inter-doc": { + "versions": { + "1.06-r0": 1509485921 + }, + "origin": "perl-test-inter" + }, + "libobjc": { + "versions": { + "6.4.0-r5": 1509458075 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libobjc.so.4=4.0.0" + ] + }, + "perl-net-http": { + "versions": { + "6.09-r1": 1509464378 + }, + "origin": "perl-net-http", + "dependencies": [ + "perl", + "perl-uri" + ] + }, + "perl-text-password-pronounceable": { + "versions": { + "0.30-r1": 1510564569 + }, + "origin": "perl-text-password-pronounceable", + "dependencies": [ + "perl" + ] + }, + "sems-webconference": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "sems-xmlrpc2di", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "tumbler-doc": { + "versions": { + "0.2.0-r0": 1510075180 + }, + "origin": "tumbler" + }, + "qemu-sparc": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sparc" + ] + }, + "linux-virthardened": { + "versions": { + "4.9.65-r1": 1511798433 + }, + "origin": "linux-hardened", + "dependencies": [ + "mkinitfs" + ], + "provides": [ + "linux-virtgrsec=4.9.65-r1" + ] + }, + "distcc-doc": { + "versions": { + "3.1-r12": 1509493043 + }, + "origin": "distcc" + }, + "libvirt-qemu": { + "versions": { + "3.9.0-r1": 1510088351, + "5.5.0-r0": 1562165540 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=5.5.0-r0", + "libvirt-common-drivers=5.5.0-r0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libintl.so.8", + "so:libvirt.so.0", + "so:libxml2.so.2" + ] + }, + "pcre-dev": { + "versions": { + "8.41-r1": 1509464276 + }, + "origin": "pcre", + "dependencies": [ + "libpcre16=8.41-r1", + "libpcre32=8.41-r1", + "libpcrecpp=8.41-r1", + "pcre=8.41-r1", + "pkgconfig" + ], + "provides": [ + "pc:libpcre16=8.41", + "pc:libpcre32=8.41", + "pc:libpcre=8.41", + "pc:libpcrecpp=8.41", + "pc:libpcreposix=8.41", + "cmd:pcre-config" + ] + }, + "orc-doc": { + "versions": { + "0.4.27-r0": 1509469955 + }, + "origin": "orc" + }, + "build-base": { + "versions": { + "0.5-r0": 1509458198 + }, + "origin": "build-base", + "dependencies": [ + "binutils", + "gcc", + "g++", + "make", + "libc-dev", + "fortify-headers" + ] + }, + "expect-dev": { + "versions": { + "5.45-r4": 1509489556 + }, + "origin": "expect", + "dependencies": [ + "tcl-dev" + ] + }, + "postgresql-bdr-contrib": { + "versions": { + "9.4.10_p1-r3": 1510259286 + }, + "origin": "postgresql-bdr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:oid2name", + "cmd:pg_archivecleanup", + "cmd:pg_standby", + "cmd:pg_test_fsync", + "cmd:pg_test_timing", + "cmd:pg_upgrade", + "cmd:pg_xlogdump", + "cmd:pgbench", + "cmd:vacuumlo" + ] + }, + "docs": { + "versions": { + "0.2-r0": 1509492704 + }, + "origin": "docs", + "dependencies": [ + "man" + ] + }, + "appstream-glib-builder": { + "versions": { + "0.6.3-r0": 1510067862 + }, + "origin": "appstream-glib", + "dependencies": [ + "so:libappstream-glib.so.8", + "so:libarchive.so.13", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjson-glib-1.0.so.0", + "so:libpango-1.0.so.0" + ], + "provides": [ + "so:libappstream-builder.so.8=8.0.10", + "cmd:appstream-builder" + ] + }, + "xf86-video-nouveau-doc": { + "versions": { + "1.0.15-r0": 1510074881 + }, + "origin": "xf86-video-nouveau" + }, + "asciidoc-vim": { + "versions": { + "8.6.10-r0": 1509461782 + }, + "origin": "asciidoc" + }, + "perl-crypt-eksblowfish": { + "versions": { + "0.009-r4": 1509470469 + }, + "origin": "perl-crypt-eksblowfish", + "dependencies": [ + "perl-class-mix", + "so:libc.musl-x86_64.so.1" + ] + }, + "libetpan": { + "versions": { + "1.8-r2": 1509721604 + }, + "origin": "libetpan", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:libgnutls.so.30", + "so:libsasl2.so.3", + "so:libz.so.1" + ], + "provides": [ + "so:libetpan.so.20=20.1.0" + ] + }, + "mupdf": { + "versions": { + "1.13.0-r0": 1533746007 + }, + "origin": "mupdf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libharfbuzz.so.0", + "so:libjbig2dec.so.0", + "so:libjpeg.so.8", + "so:libopenjp2.so.7", + "so:libz.so.1" + ], + "provides": [ + "so:libmupdf.so.0=0", + "so:libmupdfthird.so.0=0" + ] + }, + "ltrace-doc": { + "versions": { + "0.7.3-r1": 1509477339 + }, + "origin": "ltrace" + }, + "mpfr3": { + "versions": { + "3.1.5-r1": 1509457050 + }, + "origin": "mpfr3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10" + ], + "provides": [ + "so:libmpfr.so.4=4.1.5" + ] + }, + "uwsgi-transformation_template": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "psqlodbc": { + "versions": { + "09.06.0410-r0": 1509494071 + }, + "origin": "psqlodbc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libodbcinst.so.2", + "so:libpq.so.5" + ], + "provides": [ + "so:psqlodbca.so=0", + "so:psqlodbcw.so=0" + ] + }, + "isl": { + "versions": { + "0.18-r0": 1509456967 + }, + "origin": "isl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10" + ], + "provides": [ + "so:libisl.so.15=15.3.0" + ] + }, + "dovecot-pigeonhole-plugin-extdata": { + "versions": { + "2.2.36.3-r0": 1554102387, + "2.2.36.4-r0": 1567075098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot-pigeonhole-plugin", + "so:libc.musl-x86_64.so.1" + ] + }, + "attr": { + "versions": { + "2.4.47-r6": 1509457002 + }, + "origin": "attr", + "dependencies": [ + "so:libattr.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:attr", + "cmd:getfattr", + "cmd:setfattr" + ] + }, + "libdnet-doc": { + "versions": { + "1.12-r7": 1509490804 + }, + "origin": "libdnet" + }, + "tcpdump": { + "versions": { + "4.9.2-r1": 1510260551 + }, + "origin": "tcpdump", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:tcpdump" + ] + }, + "ngircd-doc": { + "versions": { + "24-r2": 1510259315 + }, + "origin": "ngircd" + }, + "libogg-dev": { + "versions": { + "1.3.3-r1": 1511046403 + }, + "origin": "libogg", + "dependencies": [ + "libogg=1.3.3-r1", + "pkgconfig" + ], + "provides": [ + "pc:ogg=1.3.3" + ] + }, + "tmux-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503664 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "libxinerama-doc": { + "versions": { + "1.1.3-r1": 1509468244 + }, + "origin": "libxinerama" + }, + "python2-dbg": { + "versions": { + "2.7.15-r2": 1534944322 + }, + "origin": "python2" + }, + "fetchmail": { + "versions": { + "6.3.26-r12": 1510261418 + }, + "origin": "fetchmail", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:fetchmail" + ] + }, + "lua5.3-evdev": { + "versions": { + "2.2.1-r1": 1509488870 + }, + "origin": "lua-evdev", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "ppp-passprompt": { + "versions": { + "2.4.7-r5": 1509480136 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "kamailio-xml": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libsrdb1.so.1", + "so:libxml2.so.2" + ] + }, + "perl-role-tiny": { + "versions": { + "2.000005-r0": 1509481549 + }, + "origin": "perl-role-tiny" + }, + "perl-html-rewriteattributes": { + "versions": { + "0.05-r1": 1510564566 + }, + "origin": "perl-html-rewriteattributes", + "dependencies": [ + "perl-html-tagset", + "perl-uri", + "perl-html-parser" + ] + }, + "termrec": { + "versions": { + "0.17-r1": 1509491605 + }, + "origin": "termrec", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:liblzma.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libtty.so.0=0.0.0", + "cmd:proxyrec", + "cmd:termcat", + "cmd:termplay", + "cmd:termrec", + "cmd:termtime" + ] + }, + "py-hoedown": { + "versions": { + "0.2.3-r1": 1509552821 + }, + "origin": "py-hoedown", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:hoedownpy" + ] + }, + "lz4-dev": { + "versions": { + "1.8.0-r1": 1509461403 + }, + "origin": "lz4", + "dependencies": [ + "lz4-libs=1.8.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:liblz4=1.8.0" + ] + }, + "libgsf-dev": { + "versions": { + "1.14.41-r0": 1509467200 + }, + "origin": "libgsf", + "dependencies": [ + "bzip2-dev", + "libgsf=1.14.41-r0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libgsf-1=1.14.41" + ] + }, + "taglib-dev": { + "versions": { + "1.11.1-r0": 1509489612 + }, + "origin": "taglib", + "dependencies": [ + "pkgconfig", + "taglib=1.11.1-r0" + ], + "provides": [ + "pc:taglib=1.11.1", + "pc:taglib_c=1.11.1", + "cmd:taglib-config" + ] + }, + "xf86-video-vesa": { + "versions": { + "2.3.4-r2": 1510074819 + }, + "origin": "xf86-video-vesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gigolo-lang": { + "versions": { + "0.4.2-r0": 1510076413 + }, + "origin": "gigolo" + }, + "mariadb": { + "versions": { + "10.1.38-r1": 1551187996, + "10.1.40-r0": 1560354892, + "10.1.41-r0": 1565163116 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common", + "/bin/sh", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libncursesw.so.6", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:aria_chk", + "cmd:aria_dump_log", + "cmd:aria_ftdump", + "cmd:aria_pack", + "cmd:aria_read_log", + "cmd:innochecksum", + "cmd:mariabackup", + "cmd:mbstream", + "cmd:msql2mysql", + "cmd:my_print_defaults", + "cmd:myisamchk", + "cmd:myisamlog", + "cmd:myisampack", + "cmd:mysql_client_test_embedded", + "cmd:mysql_convert_table_format", + "cmd:mysql_embedded", + "cmd:mysql_install_db", + "cmd:mysql_plugin", + "cmd:mysql_secure_installation", + "cmd:mysql_setpermission", + "cmd:mysql_tzinfo_to_sql", + "cmd:mysql_upgrade", + "cmd:mysql_zap", + "cmd:mysqlbinlog", + "cmd:mysqld", + "cmd:mysqld_multi", + "cmd:mysqld_safe", + "cmd:mysqld_safe_helper", + "cmd:mysqlhotcopy", + "cmd:mysqlslap", + "cmd:mysqltest", + "cmd:mysqltest_embedded", + "cmd:mytop", + "cmd:perror", + "cmd:replace", + "cmd:resolve_stack_dump", + "cmd:resolveip", + "cmd:wsrep_sst_mariabackup", + "cmd:wsrep_sst_mysqldump", + "cmd:wsrep_sst_rsync", + "cmd:wsrep_sst_rsync_wan", + "cmd:wsrep_sst_xtrabackup", + "cmd:wsrep_sst_xtrabackup-v2" + ] + }, + "postgresql-plperl-contrib": { + "versions": { + "10.7-r0": 1554274199, + "10.8-r0": 1557668015, + "10.9-r0": 1562225614, + "10.10-r0": 1565683439 + }, + "origin": "postgresql", + "dependencies": [ + "postgresql-plperl", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "git-p4": { + "versions": { + "2.15.3-r0": 1540401293, + "2.15.4-r0": 1576015190 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0", + "git-fast-import=2.15.4-r0" + ] + }, + "perl-sub-identify-doc": { + "versions": { + "0.14-r1": 1509474067 + }, + "origin": "perl-sub-identify" + }, + "py-rrd": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:librrd.so.4" + ] + }, + "perl-sub-exporter-doc": { + "versions": { + "0.987-r0": 1509473981 + }, + "origin": "perl-sub-exporter" + }, + "razor": { + "versions": { + "2.85-r6": 1509491111 + }, + "origin": "razor", + "dependencies": [ + "perl", + "perl-digest-sha1", + "perl-getopt-long", + "perl-uri", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:razor-admin", + "cmd:razor-check", + "cmd:razor-client", + "cmd:razor-report", + "cmd:razor-revoke" + ] + }, + "libfetch-doc": { + "versions": { + "2.33-r2": 1509490729 + }, + "origin": "libfetch" + }, + "guile-libs": { + "versions": { + "2.0.14-r0": 1509468174 + }, + "origin": "guile", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgc.so.1", + "so:libgmp.so.10", + "so:libltdl.so.7", + "so:libunistring.so.2" + ], + "provides": [ + "so:libguile-2.0.so.22=22.8.1" + ] + }, + "p11-kit-doc": { + "versions": { + "0.23.2-r2": 1509465178 + }, + "origin": "p11-kit" + }, + "flex-doc": { + "versions": { + "2.6.4-r1": 1509456703 + }, + "origin": "flex" + }, + "libee": { + "versions": { + "0.4.1-r0": 1509486165 + }, + "origin": "libee", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libestr.so.0" + ], + "provides": [ + "so:libee.so.0=0.0.0", + "cmd:libee-convert" + ] + }, + "zmap-doc": { + "versions": { + "2.1.1-r1": 1510310598 + }, + "origin": "zmap" + }, + "qt-postgresql": { + "versions": { + "4.8.7-r8": 1510287209 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpq.so.5", + "so:libstdc++.so.6" + ] + }, + "diffutils": { + "versions": { + "3.6-r0": 1509459511 + }, + "origin": "diffutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cmp", + "cmd:diff", + "cmd:diff3", + "cmd:sdiff" + ] + }, + "ruby-test-unit": { + "versions": { + "2.4.6-r0": 1557166823 + }, + "origin": "ruby", + "dependencies": [ + "ruby", + "ruby-power_assert" + ] + }, + "imlib2-dev": { + "versions": { + "1.4.10-r0": 1509470514 + }, + "origin": "imlib2", + "dependencies": [ + "freetype-dev", + "libxext-dev", + "libsm-dev", + "imlib2=1.4.10-r0", + "pkgconfig" + ], + "provides": [ + "pc:imlib2=1.4.10", + "cmd:imlib2-config" + ] + }, + "json-glib-dev": { + "versions": { + "1.2.8-r0": 1509480681 + }, + "origin": "json-glib", + "dependencies": [ + "json-glib=1.2.8-r0", + "pc:gio-2.0", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjson-glib-1.0.so.0" + ], + "provides": [ + "pc:json-glib-1.0=1.2.8", + "cmd:json-glib-format", + "cmd:json-glib-validate" + ] + }, + "xorg-cf-files": { + "versions": { + "1.0.6-r0": 1509488821 + }, + "origin": "xorg-cf-files" + }, + "darkstat-doc": { + "versions": { + "3.0.719-r0": 1509486285 + }, + "origin": "darkstat" + }, + "acf-dnsmasq": { + "versions": { + "0.7.1-r0": 1510072792 + }, + "origin": "acf-dnsmasq", + "dependencies": [ + "acf-core", + "dnsmasq" + ] + }, + "perl-cpanel-json-xs-doc": { + "versions": { + "3.0239-r0": 1510352915 + }, + "origin": "perl-cpanel-json-xs" + }, + "farstream-dev": { + "versions": { + "0.2.8-r2": 1510931394 + }, + "origin": "farstream", + "dependencies": [ + "libnice-dev", + "gstreamer-dev", + "gst-plugins-base-dev", + "farstream=0.2.8-r2", + "pc:gstreamer-1.0", + "pc:gstreamer-base-1.0", + "pkgconfig" + ], + "provides": [ + "pc:farstream-0.2=0.2.8" + ] + }, + "perl-text-wrapper-doc": { + "versions": { + "1.04-r0": 1509490810 + }, + "origin": "perl-text-wrapper" + }, + "nagios-plugins-ping": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua-pc": { + "versions": { + "1.0.0-r9": 1509480094 + }, + "origin": "lua-pc", + "dependencies": [ + "lua5.1-pc" + ] + }, + "perl-extutils-config-doc": { + "versions": { + "0.008-r0": 1509474734 + }, + "origin": "perl-extutils-config" + }, + "libasr": { + "versions": { + "1.0.2-r6": 1510258974 + }, + "origin": "libasr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ], + "provides": [ + "so:libasr.so.0=0.0.2" + ] + }, + "lutok": { + "versions": { + "0.4-r1": 1509459745 + }, + "origin": "lutok", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:liblua-5.2.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:liblutok.so.3=3.0.0" + ] + }, + "rtmpdump-doc": { + "versions": { + "2.4_git20160909-r3": 1510259117 + }, + "origin": "rtmpdump" + }, + "ncftp": { + "versions": { + "3.2.6-r1": 1509472913 + }, + "origin": "ncftp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libncftp.so.3=3", + "cmd:ncftp", + "cmd:ncftpbatch", + "cmd:ncftpget", + "cmd:ncftpls", + "cmd:ncftpput", + "cmd:ncftpspooler" + ] + }, + "mosquitto-dev": { + "versions": { + "1.4.15-r0": 1520176487, + "1.4.15-r2": 1563346323 + }, + "origin": "mosquitto", + "dependencies": [ + "mosquitto-libs++=1.4.15-r2", + "mosquitto-libs=1.4.15-r2" + ] + }, + "abiword-plugin-epub": { + "versions": { + "3.0.2-r1": 1510073359 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libgtk-3.so.0", + "so:libstdc++.so.6" + ] + }, + "subunit": { + "versions": { + "1.2.0-r0": 1509481265 + }, + "origin": "subunit", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:subunit-1to2", + "cmd:subunit-2to1", + "cmd:subunit-diff", + "cmd:subunit-filter", + "cmd:subunit-ls", + "cmd:subunit-notify", + "cmd:subunit-output", + "cmd:subunit-stats", + "cmd:subunit-tags", + "cmd:subunit2csv", + "cmd:subunit2disk", + "cmd:subunit2gtk", + "cmd:subunit2junitxml", + "cmd:subunit2pyunit", + "cmd:tap2subunit" + ] + }, + "apr-util-ldap": { + "versions": { + "1.6.1-r1": 1510285059 + }, + "origin": "apr-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldap-2.4.so.2" + ] + }, + "conntrack-tools-doc": { + "versions": { + "1.4.4-r0": 1509469776 + }, + "origin": "conntrack-tools" + }, + "gettext": { + "versions": { + "0.19.8.1-r1": 1509459197 + }, + "origin": "gettext", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgomp.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libunistring.so.2", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgettextlib-0.19.8.1.so=0", + "so:libgettextsrc-0.19.8.1.so=0", + "cmd:autopoint", + "cmd:envsubst", + "cmd:gettext", + "cmd:gettext.sh", + "cmd:gettextize", + "cmd:msgattrib", + "cmd:msgcat", + "cmd:msgcmp", + "cmd:msgcomm", + "cmd:msgconv", + "cmd:msgen", + "cmd:msgexec", + "cmd:msgfilter", + "cmd:msgfmt", + "cmd:msggrep", + "cmd:msginit", + "cmd:msgmerge", + "cmd:msgunfmt", + "cmd:msguniq", + "cmd:ngettext", + "cmd:recode-sr-latin", + "cmd:xgettext" + ] + }, + "bsd-compat-headers": { + "versions": { + "0.7.1-r0": 1509458195 + }, + "origin": "libc-dev" + }, + "lua5.3-penlight": { + "versions": { + "1.5.4-r0": 1509485591 + }, + "origin": "lua-penlight", + "dependencies": [ + "lua-penlight-shared", + "lua5.3-filesystem", + "lua-penlight-shared=1.5.4-r0" + ] + }, + "lua5.3-microlight": { + "versions": { + "1.1.1-r2": 1509480787 + }, + "origin": "lua-microlight" + }, + "p11-kit": { + "versions": { + "0.23.2-r2": 1509465179 + }, + "origin": "p11-kit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libtasn1.so.6" + ], + "provides": [ + "so:libp11-kit.so.0=0.1.0", + "cmd:p11-kit", + "cmd:trust" + ] + }, + "ntfs-3g-progs": { + "versions": { + "2017.3.23-r1": 1509489438 + }, + "origin": "ntfs-3g", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libntfs-3g.so.88", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:mkntfs", + "cmd:ntfs-3g.probe", + "cmd:ntfscat", + "cmd:ntfsclone", + "cmd:ntfscluster", + "cmd:ntfscmp", + "cmd:ntfscp", + "cmd:ntfsfix", + "cmd:ntfsinfo", + "cmd:ntfslabel", + "cmd:ntfsls", + "cmd:ntfsresize", + "cmd:ntfsundelete" + ] + }, + "giflib-dev": { + "versions": { + "5.1.4-r1": 1509470478 + }, + "origin": "giflib", + "dependencies": [ + "giflib=5.1.4-r1" + ] + }, + "cyrus-sasl-digestmd5": { + "versions": { + "2.1.26-r11": 1510258045 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "unbound-dev": { + "versions": { + "1.6.7-r1": 1510588257 + }, + "origin": "unbound", + "dependencies": [ + "libressl-dev", + "expat-dev", + "ldns-dev", + "libevent-dev", + "unbound-libs=1.6.7-r1" + ] + }, + "fprobe-doc": { + "versions": { + "1.1-r7": 1509481825 + }, + "origin": "fprobe" + }, + "c-client": { + "versions": { + "2007f-r7": 1510258915 + }, + "origin": "imap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "so:libc-client.so.1=1.0.0" + ] + }, + "haserl": { + "versions": { + "0.9.35-r1": 1509468300 + }, + "origin": "haserl", + "dependencies": [ + "haserl-lua5.3", + "haserl-lua5.2", + "haserl-lua5.1", + "haserl-lua5.3=0.9.35-r1" + ] + }, + "lua5.1-rex-posix": { + "versions": { + "2.9.0-r0": 1510619366 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "ppp-dev": { + "versions": { + "2.4.7-r5": 1509480124 + }, + "origin": "ppp" + }, + "jack": { + "versions": { + "1.9.10-r5": 1509473320 + }, + "origin": "jack", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libgcc_s.so.1", + "so:libsamplerate.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libjack.so.0=0.1.0", + "so:libjacknet.so.0=0.1.0", + "so:libjackserver.so.0=0.1.0", + "cmd:jackd" + ] + }, + "abiword-plugin-wml": { + "versions": { + "3.0.2-r1": 1510073370 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "qemu-mips64": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mips64" + ] + }, + "perl-compress-raw-bzip2": { + "versions": { + "2.074-r1": 1509483998 + }, + "origin": "perl-compress-raw-bzip2", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "e2fsprogs-libs": { + "versions": { + "1.43.7-r0": 1509469274, + "1.43.7-r1": 1571322244 + }, + "origin": "e2fsprogs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2" + ], + "provides": [ + "so:libe2p.so.2=2.3", + "so:libext2fs.so.2=2.4", + "so:libss.so.2=2.0" + ] + }, + "pwclient": { + "versions": { + "1.1.3-r0": 1509474240, + "1.1.3-r1": 1562223418 + }, + "origin": "patchwork", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:pwclient" + ] + }, + "iceauth-doc": { + "versions": { + "1.0.7-r0": 1509482661 + }, + "origin": "iceauth" + }, + "glm-dev": { + "versions": { + "0.9.8.5-r0": 1509494918 + }, + "origin": "glm", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:glm=0.9.8" + ] + }, + "libepoxy-dev": { + "versions": { + "1.4.3-r1": 1510067311 + }, + "origin": "libepoxy", + "dependencies": [ + "libx11-dev", + "mesa-dev", + "libepoxy=1.4.3-r1", + "pkgconfig" + ], + "provides": [ + "pc:epoxy=1.4.3" + ] + }, + "ipsec-tools-doc": { + "versions": { + "0.8.2-r6": 1510261509 + }, + "origin": "ipsec-tools" + }, + "xf86-video-siliconmotion": { + "versions": { + "1.7.9-r0": 1510074838 + }, + "origin": "xf86-video-siliconmotion", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py-requests": { + "versions": { + "2.18.4-r0": 1509483043 + }, + "origin": "py-requests", + "dependencies": [ + "py-chardet", + "py-idna", + "py-certifi", + "py-urllib3" + ] + }, + "perl-mail-clamav-doc": { + "versions": { + "0.29-r12": 1509489511 + }, + "origin": "perl-mail-clamav" + }, + "glamor-egl": { + "versions": { + "0.6.0-r3": 1510068308 + }, + "origin": "glamor-egl", + "dependencies": [ + "so:libEGL.so.1", + "so:libGL.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgbm.so.1" + ], + "provides": [ + "so:libglamor.so.0=0.0.0" + ] + }, + "lua-expat": { + "versions": { + "1.3.0-r2": 1509483373 + }, + "origin": "lua-expat" + }, + "s6-linux-init-doc": { + "versions": { + "0.3.1.0-r0": 1509492622 + }, + "origin": "s6-linux-init" + }, + "libdnet": { + "versions": { + "1.12-r7": 1509490804 + }, + "origin": "libdnet", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdnet.so.1=1.0.1", + "cmd:dnet" + ] + }, + "pssh-doc": { + "versions": { + "2.3.1-r1": 1509494676 + }, + "origin": "pssh" + }, + "py-genshi": { + "versions": { + "0.7-r0": 1509491435 + }, + "origin": "py-genshi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "libidn": { + "versions": { + "1.33-r1": 1509468968 + }, + "origin": "libidn", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libidn.so.11=11.6.16", + "cmd:idn" + ] + }, + "tiff-tools": { + "versions": { + "4.0.10-r0": 1544169734, + "4.0.10-r1": 1566982293, + "4.0.10-r2": 1572815888 + }, + "origin": "tiff", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:fax2ps", + "cmd:fax2tiff", + "cmd:pal2rgb", + "cmd:ppm2tiff", + "cmd:raw2tiff", + "cmd:tiff2bw", + "cmd:tiff2pdf", + "cmd:tiff2ps", + "cmd:tiff2rgba", + "cmd:tiffcmp", + "cmd:tiffcp", + "cmd:tiffcrop", + "cmd:tiffdither", + "cmd:tiffdump", + "cmd:tiffinfo", + "cmd:tiffmedian", + "cmd:tiffset", + "cmd:tiffsplit" + ] + }, + "talloc-doc": { + "versions": { + "2.1.10-r0": 1509466375 + }, + "origin": "talloc" + }, + "lxterminal-doc": { + "versions": { + "0.3.0-r2": 1510071920 + }, + "origin": "lxterminal" + }, + "py2-libvirt": { + "versions": { + "3.9.0-r0": 1510088408 + }, + "origin": "py-libvirt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libvirt-lxc.so.0", + "so:libvirt-qemu.so.0", + "so:libvirt.so.0" + ] + }, + "gst-plugins-ugly0.10-lang": { + "versions": { + "0.10.19-r2": 1510072192 + }, + "origin": "gst-plugins-ugly0.10" + }, + "py3-pynacl": { + "versions": { + "1.2.0-r0": 1510088478 + }, + "origin": "py-pynacl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "py-jinja2-vim": { + "versions": { + "2.9.6-r0": 1509476515 + }, + "origin": "py-jinja2", + "dependencies": [ + "vim" + ] + }, + "liblockfile": { + "versions": { + "1.14-r0": 1511990622 + }, + "origin": "liblockfile", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dotlockfile" + ] + }, + "abiword-plugin-applix": { + "versions": { + "3.0.2-r1": 1510073356 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "ca-certificates": { + "versions": { + "20171114-r0": 1510690408, + "20190108-r0": 1558960537 + }, + "origin": "ca-certificates", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ], + "provides": [ + "cmd:c_rehash", + "cmd:update-ca-certificates" + ] + }, + "libblkid": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libblkid.so.1=1.1.0" + ] + }, + "py2-asn1": { + "versions": { + "0.4.2-r0": 1511889292 + }, + "origin": "py-asn1", + "dependencies": [ + "python2" + ] + }, + "py2-backports.ssl_match_hostname": { + "versions": { + "3.5.0.1-r1": 1509552713 + }, + "origin": "py2-backports.ssl_match_hostname", + "dependencies": [ + "python2" + ], + "provides": [ + "py-backports.ssl_match_hostname" + ] + }, + "swatch": { + "versions": { + "3.2.4-r4": 1510564578 + }, + "origin": "swatch", + "dependencies": [ + "perl", + "perl-date-calc", + "perl-date-format", + "perl-date-manip", + "perl-file-tail", + "perl-carp-clan" + ], + "provides": [ + "cmd:swatchdog" + ] + }, + "lua5.1-alt-getopt": { + "versions": { + "0.7-r8": 1509480073 + }, + "origin": "lua-alt-getopt" + }, + "unionfs-fuse": { + "versions": { + "1.0-r0": 1509489291 + }, + "origin": "unionfs-fuse", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2" + ], + "provides": [ + "cmd:mount.unionfs", + "cmd:unionfs", + "cmd:unionfsctl" + ] + }, + "lua-sql": { + "versions": { + "2.3.5-r1": 1509488835 + }, + "origin": "lua-sql" + }, + "gcc-objc": { + "versions": { + "6.4.0-r5": 1509458075 + }, + "origin": "gcc", + "dependencies": [ + "libc-dev", + "gcc=6.4.0-r5", + "libobjc=6.4.0-r5", + "libobjc=6.4.0-r5" + ] + }, + "py2-country": { + "versions": { + "17.9.23-r0": 1509493917 + }, + "origin": "py-country", + "dependencies": [ + "python2" + ] + }, + "perl-io-async-doc": { + "versions": { + "0.69-r0": 1509480539 + }, + "origin": "perl-io-async" + }, + "libevdev-dev": { + "versions": { + "1.5.7-r1": 1509475753 + }, + "origin": "libevdev", + "dependencies": [ + "libevdev=1.5.7-r1", + "pkgconfig" + ], + "provides": [ + "pc:libevdev=1.5.7" + ] + }, + "gpgme-dev": { + "versions": { + "1.9.0-r1": 1510588162 + }, + "origin": "gpgme", + "dependencies": [ + "libgpg-error-dev", + "libassuan-dev", + "gpgme=1.9.0-r1", + "gpgmepp=1.9.0-r1" + ], + "provides": [ + "cmd:gpgme-config" + ] + }, + "py-gamin": { + "versions": { + "0.1.10-r10": 1509481666 + }, + "origin": "gamin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgamin-1.so.0" + ] + }, + "sdl-doc": { + "versions": { + "1.2.15-r7": 1510067900, + "1.2.15-r8": 1565787130, + "1.2.15-r9": 1574265376 + }, + "origin": "sdl" + }, + "libnotify-dev": { + "versions": { + "0.7.7-r0": 1510068016 + }, + "origin": "libnotify", + "dependencies": [ + "gdk-pixbuf-dev", + "glib-dev", + "dbus-dev", + "libnotify=0.7.7-r0", + "pc:gdk-pixbuf-2.0", + "pc:gio-2.0>=2.26.0", + "pc:glib-2.0>=2.26.0", + "pkgconfig" + ], + "provides": [ + "pc:libnotify=0.7.7" + ] + }, + "kamailio": { + "versions": { + "5.0.7-r0": 1532960877 + }, + "origin": "kamailio", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libkamailio_ims.so.0=0.1", + "so:libprint.so.1=1.2", + "so:libsrdb1.so.1=1.0", + "so:libsrdb2.so.1=1.0", + "so:libsrutils.so.1=1.0", + "so:libtrie.so.1=1.0", + "cmd:kamailio", + "cmd:kamcmd", + "cmd:kamctl", + "cmd:kamdbctl" + ] + }, + "libgcab": { + "versions": { + "0.7-r1": 1510067823 + }, + "origin": "libgcab", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libz.so.1" + ], + "provides": [ + "so:libgcab-1.0.so.0=0.0.0", + "cmd:gcab" + ] + }, + "findmnt": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libmount.so.1", + "so:libsmartcols.so.1" + ], + "provides": [ + "cmd:findmnt" + ] + }, + "squid-lang-uk": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865670 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "dropbear": { + "versions": { + "2018.76-r2": 1537439117 + }, + "origin": "dropbear", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:dropbear", + "cmd:dropbearkey" + ] + }, + "nagios-plugins-smtp": { + "versions": { + "2.2.1-r3": 1510288498 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "linenoise": { + "versions": { + "1.0-r1": 1509462461 + }, + "origin": "linenoise", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblinenoise.so.0=0.0.0" + ] + }, + "icecast-doc": { + "versions": { + "2.4.4-r0": 1541491284 + }, + "origin": "icecast" + }, + "py3-munkres": { + "versions": { + "1.0.12-r0": 1509468389 + }, + "origin": "py-munkres", + "dependencies": [ + "python3" + ] + }, + "collectd-perl": { + "versions": { + "5.7.2-r0": 1510069643 + }, + "origin": "collectd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "mesa-dri-freedreno": { + "versions": { + "17.2.4-r1": 1510741946 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "perl-io-captureoutput-doc": { + "versions": { + "1.1104-r0": 1509489308 + }, + "origin": "perl-io-captureoutput" + }, + "tig": { + "versions": { + "2.3.0-r0": 1511889467 + }, + "origin": "tig", + "dependencies": [ + "git", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:tig" + ] + }, + "libgsf": { + "versions": { + "1.14.41-r0": 1509467200 + }, + "origin": "libgsf", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libgsf-1.so.114=114.0.41", + "cmd:gsf", + "cmd:gsf-vba-dump" + ] + }, + "krb5-pkinit": { + "versions": { + "1.15.4-r0": 1548491443 + }, + "origin": "krb5", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libcrypto.so.42", + "so:libk5crypto.so.3", + "so:libkrb5.so.3", + "so:libkrb5support.so.0" + ] + }, + "perl-datetime-format-natural-doc": { + "versions": { + "1.05-r1": 1511384618 + }, + "origin": "perl-datetime-format-natural" + }, + "libxrandr": { + "versions": { + "1.5.1-r1": 1509466001 + }, + "origin": "libxrandr", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXrandr.so.2=2.2.0" + ] + }, + "net-snmp-gui": { + "versions": { + "5.7.3-r10": 1510259617 + }, + "origin": "net-snmp", + "dependencies": [ + "perl-net-snmp" + ], + "provides": [ + "cmd:tkmib" + ] + }, + "nsd-dbg": { + "versions": { + "4.1.16-r1": 1510260999 + }, + "origin": "nsd" + }, + "perl-apache-session-doc": { + "versions": { + "1.93-r0": 1509491309 + }, + "origin": "perl-apache-session" + }, + "confuse-doc": { + "versions": { + "3.2.1-r0": 1509471872 + }, + "origin": "confuse" + }, + "perl-mailtools": { + "versions": { + "2.19-r0": 1511989745 + }, + "origin": "perl-mailtools", + "dependencies": [ + "perl-timedate" + ], + "provides": [ + "perl-mail-tools=2.19" + ] + }, + "gtk+2.0-dev": { + "versions": { + "2.24.31-r0": 1510066780 + }, + "origin": "gtk+2.0", + "dependencies": [ + "atk-dev", + "cairo-dev", + "intltool", + "libxdamage-dev", + "pango-dev", + "gtk+2.0=2.24.31-r0", + "pc:atk", + "pc:cairo", + "pc:gdk-pixbuf-2.0", + "pc:gio-2.0", + "pc:pango", + "pc:pangocairo", + "pc:pangoft2", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0" + ], + "provides": [ + "pc:gail=2.24.31", + "pc:gdk-2.0=2.24.31", + "pc:gdk-x11-2.0=2.24.31", + "pc:gtk+-2.0=2.24.31", + "pc:gtk+-unix-print-2.0=2.24.31", + "pc:gtk+-x11-2.0=2.24.31", + "cmd:gtk-builder-convert", + "cmd:gtk-demo" + ] + }, + "py-cliapp-doc": { + "versions": { + "1.20150829-r1": 1509552707 + }, + "origin": "py-cliapp" + }, + "perl-timedate-doc": { + "versions": { + "2.30-r1": 1509469036 + }, + "origin": "perl-timedate" + }, + "ntfs-3g-doc": { + "versions": { + "2017.3.23-r1": 1509489437 + }, + "origin": "ntfs-3g" + }, + "libogg-doc": { + "versions": { + "1.3.3-r1": 1511046403 + }, + "origin": "libogg" + }, + "cmocka-dev": { + "versions": { + "1.1.1-r1": 1509476797 + }, + "origin": "cmocka", + "dependencies": [ + "cmocka=1.1.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:cmocka=1.1.1" + ] + }, + "perl-canary-stability-doc": { + "versions": { + "2012-r0": 1509474458 + }, + "origin": "perl-canary-stability" + }, + "acf-samba": { + "versions": { + "0.10.0-r2": 1510069863 + }, + "origin": "acf-samba", + "dependencies": [ + "acf-core", + "samba" + ] + }, + "docbook2x-doc": { + "versions": { + "0.8.8-r6": 1509489152 + }, + "origin": "docbook2x" + }, + "boost-math": { + "versions": { + "1.62.0-r5": 1509465876 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_math_c99-mt.so.1.62.0=1.62.0", + "so:libboost_math_c99.so.1.62.0=1.62.0", + "so:libboost_math_c99f-mt.so.1.62.0=1.62.0", + "so:libboost_math_c99f.so.1.62.0=1.62.0", + "so:libboost_math_c99l-mt.so.1.62.0=1.62.0", + "so:libboost_math_c99l.so.1.62.0=1.62.0", + "so:libboost_math_tr1-mt.so.1.62.0=1.62.0", + "so:libboost_math_tr1.so.1.62.0=1.62.0", + "so:libboost_math_tr1f-mt.so.1.62.0=1.62.0", + "so:libboost_math_tr1f.so.1.62.0=1.62.0", + "so:libboost_math_tr1l-mt.so.1.62.0=1.62.0", + "so:libboost_math_tr1l.so.1.62.0=1.62.0" + ] + }, + "py3-bluez": { + "versions": { + "0.22-r1": 1510075577 + }, + "origin": "py-bluez", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "qpdf-dev": { + "versions": { + "7.0.0-r0": 1509482354 + }, + "origin": "qpdf", + "dependencies": [ + "pc:libjpeg", + "pc:zlib", + "pkgconfig", + "qpdf-libs=7.0.0-r0" + ], + "provides": [ + "pc:libqpdf=7.0.0" + ] + }, + "acf-snort": { + "versions": { + "0.8.0-r2": 1510848488 + }, + "origin": "acf-snort", + "dependencies": [ + "acf-core", + "snort" + ] + }, + "perl-class-load-doc": { + "versions": { + "0.23-r0": 1509494276 + }, + "origin": "perl-class-load" + }, + "openipmi": { + "versions": { + "2.0.22-r1": 1510260044 + }, + "origin": "openipmi", + "dependencies": [ + "so:libOpenIPMI.so.0", + "so:libOpenIPMIcmdlang.so.0", + "so:libOpenIPMIglib.so.0", + "so:libOpenIPMIposix.so.0", + "so:libOpenIPMIui.so.1", + "so:libOpenIPMIutils.so.0", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libncursesw.so.6", + "so:libnetsnmp.so.30" + ], + "provides": [ + "cmd:ipmi_ui", + "cmd:ipmicmd", + "cmd:ipmish", + "cmd:openipmi_eventd", + "cmd:openipmicmd", + "cmd:openipmish", + "cmd:rmcp_ping", + "cmd:solterm" + ] + }, + "lua5.3-lzlib": { + "versions": { + "0.4.3-r0": 1509494170 + }, + "origin": "lua-lzlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ] + }, + "py2-werkzeug": { + "versions": { + "0.11.15-r2": 1509551863 + }, + "origin": "py-werkzeug", + "dependencies": [ + "python2" + ] + }, + "ksymoops-doc": { + "versions": { + "2.4.11-r7": 1509489729 + }, + "origin": "ksymoops" + }, + "perl-http-message": { + "versions": { + "6.13-r0": 1511871374 + }, + "origin": "perl-http-message", + "dependencies": [ + "perl-lwp-mediatypes", + "perl-encode-locale", + "perl-http-date", + "perl-uri", + "perl-io-html" + ] + }, + "lua-iconv": { + "versions": { + "7-r1": 1509493324 + }, + "origin": "lua-iconv" + }, + "kamailio-ev": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libev.so.4" + ] + }, + "fastjar-doc": { + "versions": { + "0.98-r2": 1509490598 + }, + "origin": "fastjar" + }, + "perl-encode-hanextra-doc": { + "versions": { + "0.23-r2": 1509489060 + }, + "origin": "perl-encode-hanextra" + }, + "gobject-introspection": { + "versions": { + "1.54.1-r0": 1509464659 + }, + "origin": "gobject-introspection", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libgirepository-1.0.so.1=1.0.0" + ] + }, + "libnfs-dev": { + "versions": { + "2.0.0-r0": 1509493088 + }, + "origin": "libnfs", + "dependencies": [ + "libnfs=2.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnfs=2.0.0" + ] + }, + "grep-doc": { + "versions": { + "3.1-r0": 1509468224 + }, + "origin": "grep" + }, + "lua5.2-struct": { + "versions": { + "0.2-r2": 1509494933 + }, + "origin": "lua-struct", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "expect": { + "versions": { + "5.45-r4": 1509489557 + }, + "origin": "expect", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtcl8.6.so" + ], + "provides": [ + "so:libexpect5.45.so=0", + "cmd:autoexpect", + "cmd:autopasswd", + "cmd:cryptdir", + "cmd:decryptdir", + "cmd:dislocate", + "cmd:expect", + "cmd:ftp-rfc", + "cmd:kibitz", + "cmd:lpunlock", + "cmd:mkpasswd", + "cmd:multixterm", + "cmd:passmass", + "cmd:rftp", + "cmd:rlogin-cwd", + "cmd:timed-read", + "cmd:timed-run", + "cmd:tknewsbiff", + "cmd:tkpasswd", + "cmd:unbuffer", + "cmd:weather", + "cmd:xkibitz", + "cmd:xpstat" + ] + }, + "apk-cron": { + "versions": { + "1.0-r0": 1509494401 + }, + "origin": "apk-cron", + "dependencies": [ + "apk-tools" + ] + }, + "acpi-doc": { + "versions": { + "1.7-r2": 1509787633 + }, + "origin": "acpi" + }, + "ruby-json": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "ruby", + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.4" + ] + }, + "cunit": { + "versions": { + "2.1.3-r1": 1509466104 + }, + "origin": "cunit", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcunit.so.1=1.0.1" + ] + }, + "py3-lxc": { + "versions": { + "2.1.1-r3": 1533578833 + }, + "origin": "lxc", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:liblxc.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "bind": { + "versions": { + "9.11.6_p1-r1": 1556872768, + "9.11.8-r0": 1561323229 + }, + "origin": "bind", + "dependencies": [ + "/bin/sh", + "so:libbind9.so.161", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.42", + "so:libdns.so.1106", + "so:libisc.so.1100", + "so:libisccc.so.161", + "so:libisccfg.so.163", + "so:liblwres.so.161", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:arpaname", + "cmd:ddns-confgen", + "cmd:genrandom", + "cmd:isc-config.sh", + "cmd:isc-hmac-fixup", + "cmd:lwresd", + "cmd:mdig", + "cmd:named", + "cmd:named-checkconf", + "cmd:named-checkzone", + "cmd:named-compilezone", + "cmd:named-journalprint", + "cmd:named-rrchecker", + "cmd:nsec3hash", + "cmd:rndc", + "cmd:rndc-confgen", + "cmd:tsig-keygen" + ] + }, + "spandsp-dev": { + "versions": { + "0.0.6-r0": 1509481503 + }, + "origin": "spandsp", + "dependencies": [ + "tiff-dev", + "pkgconfig", + "spandsp=0.0.6-r0" + ], + "provides": [ + "pc:spandsp=0.0.6" + ] + }, + "xgamma-doc": { + "versions": { + "1.0.6-r0": 1509481749 + }, + "origin": "xgamma" + }, + "lttng-ust": { + "versions": { + "2.10.0-r1": 1509492305 + }, + "origin": "lttng-ust", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liburcu-bp.so.6", + "so:liburcu-cds.so.6" + ], + "provides": [ + "so:liblttng-ust-ctl.so.4=4.0.0", + "so:liblttng-ust-cyg-profile-fast.so.0=0.0.0", + "so:liblttng-ust-cyg-profile.so.0=0.0.0", + "so:liblttng-ust-dl.so.0=0.0.0", + "so:liblttng-ust-fd.so.0=0.0.0", + "so:liblttng-ust-fork.so.0=0.0.0", + "so:liblttng-ust-libc-wrapper.so.0=0.0.0", + "so:liblttng-ust-pthread-wrapper.so.0=0.0.0", + "so:liblttng-ust-tracepoint.so.0=0.0.0", + "so:liblttng-ust.so.0=0.0.0", + "cmd:lttng-gen-tp" + ] + }, + "squid-lang-ru": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "xf86-video-rendition-doc": { + "versions": { + "4.2.6-r1": 1510074857 + }, + "origin": "xf86-video-rendition" + }, + "iniparser": { + "versions": { + "4.0-r1": 1509486559 + }, + "origin": "iniparser", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libiniparser.so.0=0" + ] + }, + "graphviz-dev": { + "versions": { + "2.40.1-r0": 1510066916 + }, + "origin": "graphviz", + "dependencies": [ + "zlib-dev", + "libpng-dev", + "libjpeg-turbo-dev", + "expat-dev", + "fontconfig-dev", + "libsm-dev", + "libxext-dev", + "cairo-dev", + "pango-dev", + "librsvg-dev", + "gmp-dev", + "freetype-dev", + "graphviz=2.40.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcdt=2.40.1", + "pc:libcgraph=2.40.1", + "pc:libgvc=2.40.1", + "pc:libgvpr=2.40.1", + "pc:liblab_gamut=2.40.1", + "pc:libpathplan=2.40.1", + "pc:libxdot=2.40.1" + ] + }, + "perl-ipc-run3-doc": { + "versions": { + "0.048-r0": 1509481624 + }, + "origin": "perl-ipc-run3" + }, + "libmad-dev": { + "versions": { + "0.15.1b-r7": 1509473572, + "0.15.1b-r8": 1565784570 + }, + "origin": "libmad", + "dependencies": [ + "libmad=0.15.1b-r8", + "pkgconfig" + ], + "provides": [ + "pc:mad=0.15.0b" + ] + }, + "libice-doc": { + "versions": { + "1.0.9-r2": 1509465974 + }, + "origin": "libice" + }, + "clucene-contribs": { + "versions": { + "2.3.3.4-r5": 1509495542 + }, + "origin": "clucene", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libclucene-core.so.1", + "so:libclucene-shared.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libclucene-contribs-lib.so.1=2.3.3.4" + ] + }, + "pwgen": { + "versions": { + "2.08-r0": 1509491101 + }, + "origin": "pwgen", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pwgen" + ] + }, + "udisks-doc": { + "versions": { + "1.0.5-r3": 1510073402 + }, + "origin": "udisks" + }, + "lxc": { + "versions": { + "2.1.1-r3": 1533578833 + }, + "origin": "lxc", + "dependencies": [ + "gzip", + "so:libc.musl-x86_64.so.1", + "so:liblxc.so.1" + ], + "provides": [ + "cmd:init.lxc", + "cmd:init.lxc.static", + "cmd:lxc-attach", + "cmd:lxc-autostart", + "cmd:lxc-cgroup", + "cmd:lxc-checkconfig", + "cmd:lxc-checkpoint", + "cmd:lxc-config", + "cmd:lxc-console", + "cmd:lxc-copy", + "cmd:lxc-create", + "cmd:lxc-destroy", + "cmd:lxc-device", + "cmd:lxc-execute", + "cmd:lxc-freeze", + "cmd:lxc-info", + "cmd:lxc-ls", + "cmd:lxc-monitor", + "cmd:lxc-snapshot", + "cmd:lxc-start", + "cmd:lxc-stop", + "cmd:lxc-top", + "cmd:lxc-unfreeze", + "cmd:lxc-unshare", + "cmd:lxc-usernsexec", + "cmd:lxc-wait" + ] + }, + "busybox": { + "versions": { + "1.27.2-r11": 1528276162 + }, + "origin": "busybox", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "/bin/sh", + "cmd:busybox", + "cmd:sh" + ] + }, + "libdbi-dev": { + "versions": { + "0.9.0-r0": 1509474454 + }, + "origin": "libdbi", + "dependencies": [ + "libdbi=0.9.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:dbi=0.9.0" + ] + }, + "rsyslog-dbg": { + "versions": { + "8.31.0-r0": 1512031983, + "8.31.0-r1": 1571767020 + }, + "origin": "rsyslog" + }, + "mp3splt": { + "versions": { + "2.6.2-r0": 1509488740 + }, + "origin": "mp3splt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmp3splt.so.0" + ], + "provides": [ + "cmd:mp3splt", + "cmd:oggsplt" + ] + }, + "gross-dev": { + "versions": { + "1.0.2-r11": 1509482386 + }, + "origin": "gross" + }, + "parted-dev": { + "versions": { + "3.2-r6": 1509482205 + }, + "origin": "parted", + "dependencies": [ + "parted=3.2-r6", + "pkgconfig" + ], + "provides": [ + "pc:libparted=3.2" + ] + }, + "cairo-dev": { + "versions": { + "1.14.10-r0": 1509464620 + }, + "origin": "cairo", + "dependencies": [ + "fontconfig-dev", + "freetype-dev", + "libxrender-dev", + "pixman-dev", + "xcb-util-dev", + "libxext-dev", + "cairo-tools", + "cairo-gobject=1.14.10-r0", + "cairo=1.14.10-r0", + "pc:fontconfig>=2.2.95", + "pc:freetype2>=9.7.3", + "pc:glib-2.0>=2.14", + "pc:gobject-2.0", + "pc:libpng", + "pc:pixman-1>=0.30.0", + "pc:x11", + "pc:xcb-render>=1.6", + "pc:xcb-shm", + "pc:xcb>=1.6", + "pc:xext", + "pc:xrender>=0.6", + "pkgconfig" + ], + "provides": [ + "pc:cairo-fc=1.14.10", + "pc:cairo-ft=1.14.10", + "pc:cairo-gobject=1.14.10", + "pc:cairo-pdf=1.14.10", + "pc:cairo-png=1.14.10", + "pc:cairo-ps=1.14.10", + "pc:cairo-script=1.14.10", + "pc:cairo-svg=1.14.10", + "pc:cairo-tee=1.14.10", + "pc:cairo-xcb-shm=1.14.10", + "pc:cairo-xcb=1.14.10", + "pc:cairo-xlib-xrender=1.14.10", + "pc:cairo-xlib=1.14.10", + "pc:cairo=1.14.10" + ] + }, + "xfce4-taskmanager-lang": { + "versions": { + "1.2.0-r0": 1510074646 + }, + "origin": "xfce4-taskmanager" + }, + "clamav-dev": { + "versions": { + "0.100.3-r0": 1555508237 + }, + "origin": "clamav", + "dependencies": [ + "libressl-dev", + "clamav-lib=0.100.3-r0", + "clamav-libunrar=0.100.3-r0", + "clamav=0.100.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:libclamav=0.100.3", + "cmd:clamav-config" + ] + }, + "tinyproxy": { + "versions": { + "1.8.4-r3": 1509491456 + }, + "origin": "tinyproxy", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tinyproxy" + ] + }, + "qemu-system-sparc64": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-sparc64" + ] + }, + "py3-urwid": { + "versions": { + "1.3.1-r2": 1509493955 + }, + "origin": "py-urwid", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "cramfs": { + "versions": { + "1.1-r2": 1509474501 + }, + "origin": "cramfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:cramfsck", + "cmd:mkcramfs" + ] + }, + "cunit-doc": { + "versions": { + "2.1.3-r1": 1509466104 + }, + "origin": "cunit" + }, + "libsexy": { + "versions": { + "0.1.11-r7": 1510072499 + }, + "origin": "libsexy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libsexy.so.2=2.0.4" + ] + }, + "libcap-ng-dev": { + "versions": { + "0.7.8-r1": 1509461309 + }, + "origin": "libcap-ng", + "dependencies": [ + "linux-headers", + "libcap-ng=0.7.8-r1", + "pkgconfig" + ], + "provides": [ + "pc:libcap-ng=0.7.8" + ] + }, + "unbound": { + "versions": { + "1.6.7-r1": 1510588259 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libevent-2.1.so.6", + "so:libexpat.so.1", + "so:libssl.so.44", + "so:libunbound.so.2" + ], + "provides": [ + "cmd:unbound", + "cmd:unbound-anchor", + "cmd:unbound-checkconf", + "cmd:unbound-control", + "cmd:unbound-control-setup", + "cmd:unbound-host" + ] + }, + "lua5.1-filesystem": { + "versions": { + "1.7.0.2-r0": 1511049451 + }, + "origin": "lua-filesystem", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "alpine-sdk": { + "versions": { + "0.5-r0": 1509462433 + }, + "origin": "alpine-sdk", + "dependencies": [ + "abuild", + "build-base", + "git", + "squashfs-tools", + "xorriso", + "acct", + "mkinitfs", + "mtools" + ] + }, + "perl-extutils-config": { + "versions": { + "0.008-r0": 1509474738 + }, + "origin": "perl-extutils-config" + }, + "cups-filters-doc": { + "versions": { + "1.17.9-r0": 1510075884 + }, + "origin": "cups-filters" + }, + "ghostscript-dbg": { + "versions": { + "9.26-r2": 1554362767, + "9.26-r3": 1565700439, + "9.26-r4": 1571234492 + }, + "origin": "ghostscript" + }, + "libatasmart": { + "versions": { + "0.19-r1": 1509482228 + }, + "origin": "libatasmart", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ], + "provides": [ + "so:libatasmart.so.4=4.0.5", + "cmd:skdump", + "cmd:sktest" + ] + }, + "lua5.1-stringy": { + "versions": { + "0.4.0-r5": 1509480099 + }, + "origin": "lua-stringy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-sub-exporter": { + "versions": { + "0.987-r0": 1509473987 + }, + "origin": "perl-sub-exporter", + "dependencies": [ + "perl-data-optlist", + "perl-sub-install", + "perl-params-util" + ] + }, + "gparted": { + "versions": { + "0.30.0-r0": 1510073982 + }, + "origin": "gparted", + "dependencies": [ + "e2fsprogs", + "ntfs-3g-progs", + "so:libatkmm-1.6.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdkmm-2.4.so.1", + "so:libglib-2.0.so.0", + "so:libglibmm-2.4.so.1", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libgtkmm-2.4.so.1", + "so:libintl.so.8", + "so:libpangomm-1.4.so.1", + "so:libparted-fs-resize.so.0", + "so:libparted.so.2", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:gparted", + "cmd:gpartedbin" + ] + }, + "py-gobject-dev": { + "versions": { + "2.28.6-r6": 1509471051 + }, + "origin": "py-gobject", + "dependencies": [ + "pc:gobject-2.0", + "pc:libffi", + "pkgconfig", + "py-gobject=2.28.6-r6" + ], + "provides": [ + "pc:pygobject-2.0=2.28.6" + ] + }, + "sshfs": { + "versions": { + "2.10-r2": 1509489736 + }, + "origin": "sshfs", + "dependencies": [ + "openssh-client", + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2", + "so:libglib-2.0.so.0" + ], + "provides": [ + "cmd:sshfs" + ] + }, + "ghostscript-doc": { + "versions": { + "9.26-r2": 1554362767, + "9.26-r3": 1565700439, + "9.26-r4": 1571234492 + }, + "origin": "ghostscript" + }, + "tiff-dev": { + "versions": { + "4.0.10-r0": 1544169734, + "4.0.10-r1": 1566982293, + "4.0.10-r2": 1572815888 + }, + "origin": "tiff", + "dependencies": [ + "zlib-dev", + "libjpeg-turbo-dev", + "pkgconfig", + "tiff=4.0.10-r2" + ], + "provides": [ + "pc:libtiff-4=4.0.10" + ] + }, + "qjson-dev": { + "versions": { + "0.9.0-r0": 1510075392 + }, + "origin": "qjson", + "dependencies": [ + "pc:QtCore", + "pkgconfig", + "qjson=0.9.0-r0" + ], + "provides": [ + "pc:QJson=0.9.0" + ] + }, + "haproxy": { + "versions": { + "1.7.9-r1": 1510261284 + }, + "origin": "haproxy", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:liblua-5.3.so.0", + "so:libpcre.so.1", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:haproxy" + ] + }, + "xscreensaver-lang": { + "versions": { + "5.36-r0": 1510074356 + }, + "origin": "xscreensaver", + "dependencies": [ + "bc" + ] + }, + "wireless-tools-doc": { + "versions": { + "30_pre9-r0": 1509479802 + }, + "origin": "wireless-tools" + }, + "perl-filesys-notify-simple-doc": { + "versions": { + "0.12-r0": 1510588883 + }, + "origin": "perl-filesys-notify-simple" + }, + "kmod-bash-completion": { + "versions": { + "24-r0": 1509462317 + }, + "origin": "kmod" + }, + "squid-lang-bg": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "resourceproto": { + "versions": { + "1.2.0-r2": 1509468557 + }, + "origin": "resourceproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:resourceproto=1.2.0" + ] + }, + "glew-doc": { + "versions": { + "2.1.0-r0": 1510068629 + }, + "origin": "glew" + }, + "py3-simplejson": { + "versions": { + "3.8.2-r2": 1509494752 + }, + "origin": "py-simplejson", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "memcached": { + "versions": { + "1.5.6-r0": 1520365888 + }, + "origin": "memcached", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libsasl2.so.3", + "so:libseccomp.so.2" + ], + "provides": [ + "cmd:memcached" + ] + }, + "fcgi": { + "versions": { + "2.4.0-r8": 1510330999 + }, + "origin": "fcgi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfcgi.so.0=0.0.0", + "cmd:cgi-fcgi" + ] + }, + "uwsgi-xslt": { + "versions": { + "2.0.17-r0": 1522154661 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2", + "so:libxslt.so.1" + ] + }, + "py3-virtualenv": { + "versions": { + "15.1.0-r0": 1512030542 + }, + "origin": "py-virtualenv", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:virtualenv" + ] + }, + "libotr3": { + "versions": { + "3.2.1-r4": 1509481193 + }, + "origin": "libotr3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20" + ], + "provides": [ + "so:libotr.so.2=2.2.1" + ] + }, + "cython": { + "versions": { + "0.27.2-r0": 1509484225 + }, + "origin": "cython", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:cygdb", + "cmd:cython", + "cmd:cythonize" + ] + }, + "uwsgi-notfound": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "openldap-overlay-auditlog": { + "versions": { + "2.4.45-r3": 1510258134, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "shared-mime-info-doc": { + "versions": { + "1.9-r0": 1509464467 + }, + "origin": "shared-mime-info" + }, + "lua5.2-hashids": { + "versions": { + "1.0.6-r1": 1509480766 + }, + "origin": "lua-hashids", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "xvidcore": { + "versions": { + "1.3.4-r0": 1509480933 + }, + "origin": "xvidcore", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxvidcore.so.4=4.3" + ] + }, + "bind-dnssec-tools": { + "versions": { + "9.11.6_p1-r1": 1556872768, + "9.11.8-r0": 1561323229 + }, + "origin": "bind", + "dependencies": [ + "py3-bind=9.11.8-r0" + ], + "provides": [ + "cmd:dnssec-checkds", + "cmd:dnssec-coverage", + "cmd:dnssec-keymgr" + ] + }, + "xf86-input-vmmouse-doc": { + "versions": { + "13.1.0-r3": 1510074937 + }, + "origin": "xf86-input-vmmouse" + }, + "mosquitto-clients": { + "versions": { + "1.4.15-r0": 1520176488, + "1.4.15-r2": 1563346324 + }, + "origin": "mosquitto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ], + "provides": [ + "cmd:mosquitto_pub", + "cmd:mosquitto_sub" + ] + }, + "gnupg-doc": { + "versions": { + "2.2.3-r1": 1528897438 + }, + "origin": "gnupg" + }, + "xf86-video-tdfx-doc": { + "versions": { + "1.4.7-r0": 1510075838 + }, + "origin": "xf86-video-tdfx" + }, + "unfs3-doc": { + "versions": { + "0.9.22-r4": 1509496016 + }, + "origin": "unfs3" + }, + "glfw-dev": { + "versions": { + "3.2.1-r2": 1510068480 + }, + "origin": "glfw", + "dependencies": [ + "libxinerama-dev", + "linux-headers", + "mesa-dev", + "glfw=3.2.1-r2", + "pc:x11", + "pc:xcursor", + "pc:xinerama", + "pc:xrandr", + "pc:xxf86vm", + "pkgconfig" + ], + "provides": [ + "pc:glfw3=3.2.1" + ] + }, + "apkbuild-gem-resolver": { + "versions": { + "3.1.0-r4": 1527832619, + "3.1.0-r5": 1572356049 + }, + "origin": "abuild", + "dependencies": [ + "ruby", + "ruby-augeas" + ], + "provides": [ + "cmd:apkbuild-gem-resolver" + ] + }, + "python2-doc": { + "versions": { + "2.7.15-r2": 1534944322 + }, + "origin": "python2" + }, + "seavgabios-bin": { + "versions": { + "1.10.3-r0": 1510068884 + }, + "origin": "seabios", + "dependencies": [ + "seabios-bin=1.10.3-r0", + "seavgabios-bin=1.10.3-r0" + ] + }, + "rgb": { + "versions": { + "1.0.6-r0": 1509486279 + }, + "origin": "rgb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:showrgb" + ] + }, + "gamin": { + "versions": { + "0.1.10-r10": 1509481667 + }, + "origin": "gamin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "so:libfam.so.0=0.0.0", + "so:libgamin-1.so.0=0.1.10" + ] + }, + "py-icu": { + "versions": { + "1.9.6-r3": 1510832509 + }, + "origin": "py-icu" + }, + "nss-tools": { + "versions": { + "3.34.1-r0": 1511890216 + }, + "origin": "nss", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnspr4.so", + "so:libnss3.so", + "so:libnssutil3.so", + "so:libplc4.so", + "so:libplds4.so", + "so:libsmime3.so", + "so:libz.so.1" + ], + "provides": [ + "cmd:certutil", + "cmd:cmsutil", + "cmd:crlutil", + "cmd:modutil", + "cmd:pk12util", + "cmd:shlibsign", + "cmd:signtool", + "cmd:signver", + "cmd:ssltap" + ] + }, + "perl-net-dns-resolver-programmable": { + "versions": { + "0.003-r2": 1509475480 + }, + "origin": "perl-net-dns-resolver-programmable", + "dependencies": [ + "perl", + "perl-net-dns" + ] + }, + "nload": { + "versions": { + "0.7.4-r3": 1509496066 + }, + "origin": "nload", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libformw.so.6", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:nload" + ] + }, + "iptables": { + "versions": { + "1.6.1-r1": 1509469980 + }, + "origin": "iptables", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnftnl.so.7" + ], + "provides": [ + "so:libip4tc.so.0=0.1.0", + "so:libip6tc.so.0=0.1.0", + "so:libipq.so.0=0.0.0", + "so:libiptc.so.0=0.0.0", + "so:libxtables.so.12=12.0.0", + "cmd:arptables-compat", + "cmd:ebtables-compat", + "cmd:iptables", + "cmd:iptables-compat", + "cmd:iptables-compat-restore", + "cmd:iptables-compat-save", + "cmd:iptables-restore", + "cmd:iptables-restore-translate", + "cmd:iptables-save", + "cmd:iptables-translate", + "cmd:xtables-compat-multi", + "cmd:xtables-multi" + ] + }, + "libnfsidmap-ldap": { + "versions": { + "0.25-r1": 1509492066 + }, + "origin": "libnfsidmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "augeas-static": { + "versions": { + "1.9.0-r3": 1511899746 + }, + "origin": "augeas" + }, + "py3-werkzeug": { + "versions": { + "0.11.15-r2": 1509551864 + }, + "origin": "py-werkzeug", + "dependencies": [ + "python3" + ] + }, + "dovecot-pgsql": { + "versions": { + "2.2.36.3-r0": 1554102389, + "2.2.36.4-r0": 1567075098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot-sql", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:libdriver_pgsql.so=0" + ] + }, + "squid-lang-it": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-crypt-ssleay": { + "versions": { + "0.72-r7": 1510260987 + }, + "origin": "perl-crypt-ssleay", + "dependencies": [ + "perl-path-class", + "perl-try-tiny", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "neon": { + "versions": { + "0.30.2-r2": 1510285202 + }, + "origin": "neon", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libexpat.so.1", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:libneon.so.27=27.3.2" + ] + }, + "at-spi2-atk": { + "versions": { + "2.26.1-r0": 1509466060 + }, + "origin": "at-spi2-atk", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libatspi.so.0", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libatk-bridge-2.0.so.0=0.0.0" + ] + }, + "xf86-video-s3-doc": { + "versions": { + "0.6.5-r8": 1510074848 + }, + "origin": "xf86-video-s3" + }, + "clamav-daemon": { + "versions": { + "0.100.3-r0": 1555508237 + }, + "origin": "clamav", + "dependencies": [ + "freshclam", + "logrotate", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libclamav.so.7", + "so:libcrypto.so.42", + "so:libfts.so.0", + "so:libncursesw.so.6", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:clamconf", + "cmd:clamd", + "cmd:clamdscan", + "cmd:clamdtop" + ] + }, + "lua5.1-sec": { + "versions": { + "0.6-r3": 1510260656 + }, + "origin": "lua-sec", + "dependencies": [ + "lua5.1-socket", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "libgcab-dev": { + "versions": { + "0.7-r1": 1510067822 + }, + "origin": "libgcab", + "dependencies": [ + "gettext-dev", + "libgcab=0.7-r1", + "pc:gio-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libgcab-1.0=0.7" + ] + }, + "spice-gtk-lang": { + "versions": { + "0.34-r1": 1510260978 + }, + "origin": "spice-gtk" + }, + "mesa-glapi": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libglapi.so.0=0.0.0" + ] + }, + "gnumeric-lang": { + "versions": { + "1.12.36-r0": 1511455617 + }, + "origin": "gnumeric" + }, + "gcc-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503663 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "apkbuild-cpan": { + "versions": { + "3.1.0-r4": 1527832619, + "3.1.0-r5": 1572356049 + }, + "origin": "abuild", + "dependencies": [ + "perl", + "perl-libwww", + "perl-json", + "perl-module-build-tiny" + ], + "provides": [ + "cmd:apkbuild-cpan" + ] + }, + "stfl": { + "versions": { + "0.24-r2": 1509491486 + }, + "origin": "stfl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libstfl.so.0=0.24" + ] + }, + "dmenu": { + "versions": { + "4.7-r0": 1509494019 + }, + "origin": "dmenu", + "dependencies": [ + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1" + ], + "provides": [ + "cmd:dmenu", + "cmd:dmenu_path", + "cmd:dmenu_run", + "cmd:stest" + ] + }, + "bcache-tools-doc": { + "versions": { + "1.0.8-r1": 1509495228 + }, + "origin": "bcache-tools" + }, + "gpicview": { + "versions": { + "0.2.5-r0": 1510074707 + }, + "origin": "gpicview", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8" + ], + "provides": [ + "cmd:gpicview" + ] + }, + "openjpeg-dev": { + "versions": { + "2.3.0-r2": 1552584602 + }, + "origin": "openjpeg", + "dependencies": [ + "openjpeg=2.3.0-r2", + "pkgconfig" + ], + "provides": [ + "pc:libopenjp2=2.3.0" + ] + }, + "git-gui": { + "versions": { + "2.15.3-r0": 1540401295, + "2.15.4-r0": 1576015196 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0", + "tcl", + "tk" + ] + }, + "iaxmodem": { + "versions": { + "1.3.0-r0": 1509494110 + }, + "origin": "iaxmodem", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:iaxmodem" + ] + }, + "e2fsprogs-dev": { + "versions": { + "1.43.7-r0": 1509469274, + "1.43.7-r1": 1571322241 + }, + "origin": "e2fsprogs", + "dependencies": [ + "util-linux-dev", + "e2fsprogs-libs=1.43.7-r1", + "libcom_err=1.43.7-r1", + "pkgconfig" + ], + "provides": [ + "pc:com_err=1.43.7", + "pc:e2p=1.43.7", + "pc:ext2fs=1.43.7", + "pc:ss=1.43.7", + "cmd:compile_et", + "cmd:mk_cmds" + ] + }, + "vsftpd": { + "versions": { + "3.0.3-r4": 1510260062 + }, + "origin": "vsftpd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.42", + "so:libpam.so.0", + "so:libssl.so.44" + ], + "provides": [ + "cmd:vsftpd" + ] + }, + "squid-lang-az": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865666 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-class-accessor-chained-doc": { + "versions": { + "0.01-r0": 1509474101 + }, + "origin": "perl-class-accessor-chained" + }, + "swfdec-mozilla": { + "versions": { + "0.9.2-r0": 1510075257 + }, + "origin": "swfdec-mozilla", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libswfdec-0.9.so.2", + "so:libswfdec-gtk-0.9.so.2" + ] + }, + "libtasn1": { + "versions": { + "4.12-r3": 1519805758, + "4.12-r4": 1563955390 + }, + "origin": "libtasn1", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtasn1.so.6=6.5.4", + "cmd:asn1Coding", + "cmd:asn1Decoding", + "cmd:asn1Parser" + ] + }, + "collectd-mysql": { + "versions": { + "5.7.2-r0": 1510069647 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "rp-pppoe": { + "versions": { + "3.12-r1": 1541067520 + }, + "origin": "rp-pppoe", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pppoe", + "cmd:pppoe-connect", + "cmd:pppoe-relay", + "cmd:pppoe-server", + "cmd:pppoe-setup", + "cmd:pppoe-sniff", + "cmd:pppoe-start", + "cmd:pppoe-status", + "cmd:pppoe-stop" + ] + }, + "perl-test-harness-doc": { + "versions": { + "3.39-r0": 1509474771 + }, + "origin": "perl-test-harness" + }, + "mcabber-doc": { + "versions": { + "1.1.0-r0": 1510588319 + }, + "origin": "mcabber" + }, + "libgomp": { + "versions": { + "6.4.0-r5": 1509458072 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgomp.so.1=1.0.0" + ] + }, + "libdv": { + "versions": { + "1.0.0-r3": 1510068410 + }, + "origin": "libdv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdv.so.4=4.0.3" + ] + }, + "perl-class-method-modifiers": { + "versions": { + "2.12-r0": 1509479364 + }, + "origin": "perl-class-method-modifiers" + }, + "clucene": { + "versions": { + "2.3.3.4-r5": 1509495542 + }, + "origin": "clucene", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libclucene-core.so.1=2.3.3.4", + "so:libclucene-shared.so.1=2.3.3.4" + ] + }, + "iw": { + "versions": { + "4.9-r0": 1509494921 + }, + "origin": "iw", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnl-3.so.200", + "so:libnl-genl-3.so.200" + ], + "provides": [ + "cmd:iw" + ] + }, + "lua5.2-md5": { + "versions": { + "1.2-r3": 1509468355 + }, + "origin": "lua-md5", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "uwsgi": { + "versions": { + "2.0.17-r0": 1522154661 + }, + "origin": "uwsgi", + "dependencies": [ + "mailcap", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.42", + "so:libjansson.so.4", + "so:libpcre.so.1", + "so:libssl.so.44", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:uwsgi" + ] + }, + "libvirt-client": { + "versions": { + "3.9.0-r1": 1510088351, + "5.5.0-r0": 1562165539 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt", + "pm-utils", + "gnutls-utils", + "netcat-openbsd", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libcurl.so.4", + "so:libdbus-1.so.3", + "so:libdevmapper.so.1.02", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libintl.so.8", + "so:libnl-3.so.200", + "so:libreadline.so.7", + "so:libsasl2.so.3", + "so:libtirpc.so.3", + "so:libxml2.so.2", + "so:libyajl.so.2" + ], + "provides": [ + "so:libvirt-admin.so.0=0.5005.0", + "so:libvirt-lxc.so.0=0.5005.0", + "so:libvirt-qemu.so.0=0.5005.0", + "so:libvirt.so.0=0.5005.0", + "cmd:virsh", + "cmd:virt-admin", + "cmd:virt-host-validate", + "cmd:virt-login-shell", + "cmd:virt-pki-validate", + "cmd:virt-xml-validate" + ] + }, + "ncurses5-libs": { + "versions": { + "5.9-r1": 1509483665 + }, + "origin": "ncurses5", + "dependencies": [ + "ncurses-terminfo-base", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libform.so.5=5.9", + "so:libmenu.so.5=5.9", + "so:libncurses.so.5=5.9", + "so:libpanel.so.5=5.9" + ] + }, + "llvm5-static": { + "versions": { + "5.0.0-r0": 1510682601 + }, + "origin": "llvm5", + "provides": [ + "llvm-static=5.0.0-r0" + ] + }, + "pkgconf-doc": { + "versions": { + "1.3.10-r0": 1509459813 + }, + "origin": "pkgconf" + }, + "ffmpeg": { + "versions": { + "3.4-r1": 1510072148 + }, + "origin": "ffmpeg", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libavcodec.so.57", + "so:libavdevice.so.57", + "so:libavfilter.so.6", + "so:libavformat.so.57", + "so:libavresample.so.3", + "so:libavutil.so.55", + "so:libc.musl-x86_64.so.1", + "so:libpostproc.so.54", + "so:libswresample.so.2", + "so:libswscale.so.4" + ], + "provides": [ + "cmd:ffmpeg", + "cmd:ffplay", + "cmd:ffprobe", + "cmd:ffserver", + "cmd:qt-faststart" + ] + }, + "perl-digest-hmac-doc": { + "versions": { + "1.03-r0": 1509468923 + }, + "origin": "perl-digest-hmac" + }, + "qemu-system-arm": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libstdc++.so.6", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-arm" + ] + }, + "libc-dev": { + "versions": { + "0.7.1-r0": 1509458196 + }, + "origin": "libc-dev", + "dependencies": [ + "musl-dev" + ] + }, + "collectd": { + "versions": { + "5.7.2-r0": 1510069656 + }, + "origin": "collectd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libltdl.so.7", + "so:libyajl.so.2" + ], + "provides": [ + "cmd:collectd", + "cmd:collectdmon" + ] + }, + "gstreamer0.10": { + "versions": { + "0.10.36-r2": 1509470985 + }, + "origin": "gstreamer0.10", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgstbase-0.10.so.0=0.30.0", + "so:libgstcheck-0.10.so.0=0.30.0", + "so:libgstcontroller-0.10.so.0=0.30.0", + "so:libgstdataprotocol-0.10.so.0=0.30.0", + "so:libgstnet-0.10.so.0=0.30.0", + "so:libgstreamer-0.10.so.0=0.30.0" + ] + }, + "libverto-glib": { + "versions": { + "0.3.0-r0": 1509469586 + }, + "origin": "libverto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libverto.so.1" + ], + "provides": [ + "so:libverto-glib.so.1=1.0.0" + ] + }, + "py3-tz": { + "versions": { + "2017.3-r0": 1510732132 + }, + "origin": "py-tz", + "dependencies": [ + "python3" + ] + }, + "perl-email-address": { + "versions": { + "1.908-r0": 1509491037, + "1.912-r0": 1559738452 + }, + "origin": "perl-email-address", + "dependencies": [ + "perl", + "perl-capture-tiny" + ] + }, + "gnumeric": { + "versions": { + "1.12.36-r0": 1511455617 + }, + "origin": "gnumeric", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgoffice-0.10.so.10", + "so:libgsf-1.so.114", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libspreadsheet-1.12.36.so=0", + "cmd:gnumeric", + "cmd:gnumeric-1.12.36", + "cmd:ssconvert", + "cmd:ssdiff", + "cmd:ssgrep", + "cmd:ssindex" + ] + }, + "perl-regexp-common-net-cidr-doc": { + "versions": { + "0.02-r1": 1510564550 + }, + "origin": "perl-regexp-common-net-cidr" + }, + "acf-iproute2-qos": { + "versions": { + "0.4.0-r2": 1510073019 + }, + "origin": "acf-iproute2-qos", + "dependencies": [ + "acf-core", + "iproute2-qos", + "acf-alpine-baselayout>=0.5.7" + ] + }, + "libssh2-dbg": { + "versions": { + "1.8.2-r0": 1553682023, + "1.9.0-r0": 1571235283, + "1.9.0-r1": 1571996776 + }, + "origin": "libssh2" + }, + "nfdump-doc": { + "versions": { + "1.6.15-r0": 1509494465, + "1.6.15-r1": 1574265425 + }, + "origin": "nfdump" + }, + "guile-dev": { + "versions": { + "2.0.14-r0": 1509468173 + }, + "origin": "guile", + "dependencies": [ + "guile", + "gc-dev", + "guile-libs=2.0.14-r0", + "pkgconfig" + ], + "provides": [ + "pc:guile-2.0=2.0.14", + "cmd:guile-config" + ] + }, + "libvirt-vbox": { + "versions": { + "3.9.0-r1": 1510088352, + "5.5.0-r0": 1562165540 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=5.5.0-r0", + "libvirt-common-drivers=5.5.0-r0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libvirt.so.0", + "so:libxml2.so.2" + ] + }, + "c-ares-doc": { + "versions": { + "1.13.0-r0": 1509461299 + }, + "origin": "c-ares" + }, + "cython-dev": { + "versions": { + "0.27.2-r0": 1509484225 + }, + "origin": "cython", + "dependencies": [ + "python2-dev", + "py-pgen", + "cython" + ] + }, + "bind-dev": { + "versions": { + "9.11.6_p1-r1": 1556872767, + "9.11.8-r0": 1561323229 + }, + "origin": "bind", + "dependencies": [ + "bind-libs=9.11.8-r0" + ], + "provides": [ + "cmd:bind9-config" + ] + }, + "xf86-video-nv-doc": { + "versions": { + "2.1.21-r0": 1510075413 + }, + "origin": "xf86-video-nv" + }, + "perl-time-modules-doc": { + "versions": { + "2013.0912-r0": 1509493223 + }, + "origin": "perl-time-modules" + }, + "squid-lang-he": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-convert-asn1": { + "versions": { + "0.27-r0": 1509477731 + }, + "origin": "perl-convert-asn1" + }, + "json-c-dev": { + "versions": { + "0.12.1-r1": 1509466321 + }, + "origin": "json-c", + "dependencies": [ + "json-c=0.12.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:json-c=0.12.1" + ] + }, + "vde2-libs": { + "versions": { + "2.3.2-r8": 1510260476 + }, + "origin": "vde2", + "dependencies": [ + "libressl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libvdehist.so.0=0.0.1", + "so:libvdemgmt.so.0=0.0.1", + "so:libvdeplug.so.3=3.0.1", + "so:libvdesnmp.so.0=0.0.1" + ] + }, + "py-sphinx_rtd_theme": { + "versions": { + "0.2.4-r0": 1509469913 + }, + "origin": "py-sphinx_rtd_theme" + }, + "iproute2-qos": { + "versions": { + "0.5-r1": 1509489558 + }, + "origin": "iproute2-qos", + "dependencies": [ + "iproute2" + ], + "provides": [ + "cmd:setup-qos" + ] + }, + "loudmouth-dev": { + "versions": { + "1.5.3-r0": 1509490879 + }, + "origin": "loudmouth", + "dependencies": [ + "pkgconfig", + "gnutls-dev", + "libidn-dev", + "libasyncns-dev", + "check-dev", + "autoconf", + "loudmouth=1.5.3-r0", + "pc:glib-2.0" + ], + "provides": [ + "pc:loudmouth-1.0=1.5.3" + ] + }, + "the_silver_searcher-bash-completion": { + "versions": { + "2.1.0-r2": 1510831170 + }, + "origin": "the_silver_searcher" + }, + "libpurple-bonjour": { + "versions": { + "2.12.0-r2": 1510069751 + }, + "origin": "pidgin", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-glib.so.1", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libintl.so.8", + "so:libxml2.so.2" + ], + "provides": [ + "so:libbonjour.so=0" + ] + }, + "lua5.1-openrc": { + "versions": { + "0.2-r2": 1509492195 + }, + "origin": "lua-openrc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librc.so.1" + ] + }, + "flite-dev": { + "versions": { + "2.0.0-r0": 1509481028 + }, + "origin": "flite", + "dependencies": [ + "flite=2.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:flite=2.0.0" + ] + }, + "giflib-doc": { + "versions": { + "5.1.4-r1": 1509470479 + }, + "origin": "giflib" + }, + "vde2-dev": { + "versions": { + "2.3.2-r8": 1510260476 + }, + "origin": "vde2", + "dependencies": [ + "pkgconfig", + "vde2-libs=2.3.2-r8" + ], + "provides": [ + "pc:vdehist=2.3.2", + "pc:vdemgmt=2.3.2", + "pc:vdeplug=2.3.2", + "pc:vdesnmp=2.3.2" + ] + }, + "fluxbox": { + "versions": { + "1.3.7-r0": 1509491166 + }, + "origin": "fluxbox", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libXpm.so.4", + "so:libXrandr.so.2", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:fbrun", + "cmd:fbsetbg", + "cmd:fbsetroot", + "cmd:fluxbox", + "cmd:fluxbox-generate_menu", + "cmd:fluxbox-remote", + "cmd:fluxbox-update_configs", + "cmd:startfluxbox" + ] + }, + "gmime-doc": { + "versions": { + "2.6.20-r0": 1510588181 + }, + "origin": "gmime" + }, + "xinput-doc": { + "versions": { + "1.6.2-r0": 1509490859 + }, + "origin": "xinput" + }, + "run-parts-doc": { + "versions": { + "4.8.2-r0": 1509470087 + }, + "origin": "run-parts" + }, + "lua5.3-lustache": { + "versions": { + "1.3.1-r1": 1509472928 + }, + "origin": "lua-lustache", + "dependencies": [ + "lua-lustache-common", + "lua5.3", + "lua-lustache-common=1.3.1-r1" + ] + }, + "py-mccabe": { + "versions": { + "0.6.1-r1": 1511661235 + }, + "origin": "py-mccabe" + }, + "samba-winbind-clients": { + "versions": { + "4.7.6-r3": 1555491787 + }, + "origin": "samba", + "dependencies": [ + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcliauth-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsmbconf.so.0", + "so:libtalloc.so.2", + "so:libwbclient.so.0", + "so:libwinbind-client-samba4.so" + ], + "provides": [ + "cmd:ntlm_auth", + "cmd:wbinfo" + ] + }, + "findutils-doc": { + "versions": { + "4.6.0-r0": 1509481487 + }, + "origin": "findutils" + }, + "d-feet": { + "versions": { + "0.3.12-r0": 1510075861 + }, + "origin": "d-feet", + "dependencies": [ + "python2", + "py-gobject3" + ], + "provides": [ + "cmd:d-feet" + ] + }, + "snort-dev": { + "versions": { + "2.9.11-r0": 1510848485 + }, + "origin": "snort", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:snort=2.9.11", + "pc:snort_output=2.9.11", + "pc:snort_preproc=2.9.11" + ] + }, + "perl-mro-compat-doc": { + "versions": { + "0.13-r0": 1509481602 + }, + "origin": "perl-mro-compat" + }, + "nftables-doc": { + "versions": { + "0.8-r0": 1509496566 + }, + "origin": "nftables" + }, + "redis": { + "versions": { + "4.0.11-r0": 1533890747, + "4.0.14-r0": 1564477414 + }, + "origin": "redis", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:redis-benchmark", + "cmd:redis-check-aof", + "cmd:redis-check-rdb", + "cmd:redis-cli", + "cmd:redis-sentinel", + "cmd:redis-server" + ] + }, + "qemu-microblaze": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-microblaze" + ] + }, + "powertop-doc": { + "versions": { + "2.9-r1": 1509552798 + }, + "origin": "powertop" + }, + "wayland-libs-server": { + "versions": { + "1.14.0-r2": 1510066937 + }, + "origin": "wayland", + "dependencies": [ + "wayland-libs-client", + "wayland-libs-cursor", + "wayland-libs-server", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6" + ], + "provides": [ + "so:libwayland-server.so.0=0.1.0" + ] + }, + "tk": { + "versions": { + "8.6.6-r1": 1509462120 + }, + "origin": "tk", + "dependencies": [ + "so:libX11.so.6", + "so:libXft.so.2", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libtcl8.6.so" + ], + "provides": [ + "so:libtk8.6.so=0", + "cmd:wish", + "cmd:wish8.6" + ] + }, + "perl-time-hires-doc": { + "versions": { + "1.9746-r0": 1509484014 + }, + "origin": "perl-time-hires" + }, + "libxau-dev": { + "versions": { + "1.0.8-r2": 1509461811 + }, + "origin": "libxau", + "dependencies": [ + "libxau=1.0.8-r2", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xau=1.0.8" + ] + }, + "asterisk-sounds-moh": { + "versions": { + "15.6.1-r0": 1537795342, + "15.6.2-r0": 1568705004 + }, + "origin": "asterisk" + }, + "collectd-iptables": { + "versions": { + "5.7.2-r0": 1510069649 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libip4tc.so.0", + "so:libip6tc.so.0" + ] + }, + "pgpool": { + "versions": { + "3.7.0-r0": 1511650825 + }, + "origin": "pgpool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:libpcp.so.1=1.0.0", + "cmd:pcp_attach_node", + "cmd:pcp_detach_node", + "cmd:pcp_node_count", + "cmd:pcp_node_info", + "cmd:pcp_pool_status", + "cmd:pcp_proc_count", + "cmd:pcp_proc_info", + "cmd:pcp_promote_node", + "cmd:pcp_recovery_node", + "cmd:pcp_stop_pgpool", + "cmd:pcp_watchdog_info", + "cmd:pg_md5", + "cmd:pgpool", + "cmd:pgpool_setup", + "cmd:watchdog_setup" + ] + }, + "gst-plugins-bad-dev": { + "versions": { + "1.12.3-r2": 1512039893 + }, + "origin": "gst-plugins-bad", + "dependencies": [ + "gst-plugins-bad=1.12.3-r2", + "pc:gstreamer-1.0", + "pc:gstreamer-base-1.0", + "pc:gstreamer-video-1.0", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-bad-allocators-1.0=1.12.3", + "pc:gstreamer-bad-audio-1.0=1.12.3", + "pc:gstreamer-bad-base-1.0=1.12.3", + "pc:gstreamer-bad-video-1.0=1.12.3", + "pc:gstreamer-codecparsers-1.0=1.12.3", + "pc:gstreamer-gl-1.0=1.12.3", + "pc:gstreamer-insertbin-1.0=1.12.3", + "pc:gstreamer-mpegts-1.0=1.12.3", + "pc:gstreamer-player-1.0=1.12.3", + "pc:gstreamer-plugins-bad-1.0=1.12.3" + ] + }, + "mosh": { + "versions": { + "1.3.2-r3": 1510846214 + }, + "origin": "mosh", + "dependencies": [ + "mosh-client", + "mosh-server" + ], + "provides": [ + "cmd:mosh" + ] + }, + "exo-dev": { + "versions": { + "0.11.5-r0": 1510068072 + }, + "origin": "exo", + "dependencies": [ + "exo=0.11.5-r0", + "pc:gtk+-2.0", + "pc:gtk+-3.0", + "pc:libxfce4util-1.0", + "pkgconfig" + ], + "provides": [ + "pc:exo-1=0.11.5", + "pc:exo-2=0.11.5" + ] + }, + "freeradius": { + "versions": { + "3.0.15-r4": 1556202797, + "3.0.15-r5": 1566310605 + }, + "origin": "freeradius", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libfreeradius-dhcp.so", + "so:libfreeradius-radius.so", + "so:libfreeradius-server.so", + "so:libgdbm.so.4", + "so:libpcap.so.1", + "so:libreadline.so.7", + "so:libssl.so.44", + "so:libtalloc.so.2" + ], + "provides": [ + "freeradius3=3.0.15-r5", + "so:proto_dhcp.so=0", + "so:proto_vmps.so=0", + "so:rlm_always.so=0", + "so:rlm_attr_filter.so=0", + "so:rlm_cache.so=0", + "so:rlm_cache_rbtree.so=0", + "so:rlm_chap.so=0", + "so:rlm_counter.so=0", + "so:rlm_cram.so=0", + "so:rlm_date.so=0", + "so:rlm_detail.so=0", + "so:rlm_dhcp.so=0", + "so:rlm_digest.so=0", + "so:rlm_dynamic_clients.so=0", + "so:rlm_exec.so=0", + "so:rlm_expiration.so=0", + "so:rlm_expr.so=0", + "so:rlm_files.so=0", + "so:rlm_ippool.so=0", + "so:rlm_linelog.so=0", + "so:rlm_logintime.so=0", + "so:rlm_mschap.so=0", + "so:rlm_otp.so=0", + "so:rlm_pap.so=0", + "so:rlm_passwd.so=0", + "so:rlm_preprocess.so=0", + "so:rlm_radutmp.so=0", + "so:rlm_realm.so=0", + "so:rlm_replicate.so=0", + "so:rlm_soh.so=0", + "so:rlm_sometimes.so=0", + "so:rlm_test.so=0", + "so:rlm_unix.so=0", + "so:rlm_unpack.so=0", + "so:rlm_utf8.so=0", + "so:rlm_wimax.so=0", + "cmd:dhcpclient", + "cmd:map_unit", + "cmd:rad_counter", + "cmd:radattr", + "cmd:radcrypt", + "cmd:raddebug", + "cmd:radiusd", + "cmd:radlast", + "cmd:radmin", + "cmd:radsniff", + "cmd:radsqlrelay", + "cmd:radtest", + "cmd:radwho", + "cmd:radzap", + "cmd:rlm_ippool_tool", + "cmd:smbencrypt" + ] + }, + "gpsd": { + "versions": { + "3.16-r3": 1510311401 + }, + "origin": "gpsd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgps.so.22=22.0.0", + "cmd:gpsctl", + "cmd:gpsd", + "cmd:gpsdctl", + "cmd:gpsdecode", + "cmd:gpspipe" + ] + }, + "s6-rc-dev": { + "versions": { + "0.2.1.2-r0": 1509492618 + }, + "origin": "s6-rc", + "dependencies": [ + "s6-rc=0.2.1.2-r0" + ] + }, + "libglade": { + "versions": { + "2.6.4-r14": 1510071791 + }, + "origin": "libglade", + "dependencies": [ + "libxml2-utils", + "/bin/sh", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libglade-2.0.so.0=0.0.7", + "cmd:libglade-convert" + ] + }, + "libdaemon-dev": { + "versions": { + "0.14-r2": 1509465393 + }, + "origin": "libdaemon", + "dependencies": [ + "libdaemon=0.14-r2", + "pkgconfig" + ], + "provides": [ + "pc:libdaemon=0.14" + ] + }, + "xf86-video-ati": { + "versions": { + "7.10.0-r1": 1511981941 + }, + "origin": "xf86-video-ati", + "dependencies": [ + "mesa-dri-ati", + "so:libc.musl-x86_64.so.1", + "so:libdrm_radeon.so.1", + "so:libpciaccess.so.0", + "so:libudev.so.1" + ] + }, + "lua5.1-expat": { + "versions": { + "1.3.0-r2": 1509483364 + }, + "origin": "lua-expat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "e2fsprogs-extra": { + "versions": { + "1.43.7-r0": 1509469275, + "1.43.7-r1": 1571322245 + }, + "origin": "e2fsprogs", + "dependencies": [ + "e2fsprogs", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libe2p.so.2", + "so:libext2fs.so.2", + "so:libss.so.2", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:badblocks", + "cmd:chattr", + "cmd:debugfs", + "cmd:dumpe2fs", + "cmd:e2freefrag", + "cmd:e2image", + "cmd:e2label", + "cmd:e2undo", + "cmd:e4crypt", + "cmd:e4defrag", + "cmd:filefrag", + "cmd:logsave", + "cmd:lsattr", + "cmd:mklost+found", + "cmd:resize2fs", + "cmd:tune2fs" + ] + }, + "libxfce4util-doc": { + "versions": { + "4.12.1-r4": 1509468547 + }, + "origin": "libxfce4util" + }, + "bitlbee-dev": { + "versions": { + "3.5.1-r1": 1510259355 + }, + "origin": "bitlbee", + "dependencies": [ + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:bitlbee=3.5.1" + ] + }, + "xrandr": { + "versions": { + "1.5.0-r1": 1509491917 + }, + "origin": "xrandr", + "dependencies": [ + "so:libX11.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xrandr" + ] + }, + "libiptcdata-dev": { + "versions": { + "1.0.4-r2": 1509496039 + }, + "origin": "libiptcdata", + "dependencies": [ + "libiptcdata=1.0.4-r2", + "pkgconfig" + ], + "provides": [ + "pc:libiptcdata=1.0.4" + ] + }, + "swish-e": { + "versions": { + "2.4.7-r8": 1509483303 + }, + "origin": "swish-e", + "dependencies": [ + "perl-html-parser", + "perl-html-tagset", + "perl-libwww", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libswish-e.so.2=2.0.0", + "cmd:swish-e", + "cmd:swish-filter-test" + ] + }, + "zfs-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503664 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "postgresql-bdr-dev": { + "versions": { + "9.4.10_p1-r3": 1510259283 + }, + "origin": "postgresql-bdr", + "dependencies": [ + "readline-dev", + "libressl-dev", + "zlib-dev", + "libxml2-dev", + "pkgconfig", + "postgresql-bdr=9.4.10_p1-r3" + ], + "provides": [ + "pc:libecpg=9.4.10", + "pc:libecpg_compat=9.4.10", + "pc:libpgtypes=9.4.10", + "pc:libpq=9.4.10" + ] + }, + "py-markupsafe": { + "versions": { + "1.0-r0": 1509476745 + }, + "origin": "py-markupsafe" + }, + "orage-lang": { + "versions": { + "4.12.1-r1": 1510073567 + }, + "origin": "orage", + "dependencies": [ + "hicolor-icon-theme" + ] + }, + "readline-dev": { + "versions": { + "7.0.003-r0": 1509456788 + }, + "origin": "readline", + "dependencies": [ + "libhistory=7.0.003-r0", + "readline=7.0.003-r0" + ] + }, + "ruby": { + "versions": { + "2.4.6-r0": 1557166824 + }, + "origin": "ruby", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.4" + ], + "provides": [ + "cmd:erb", + "cmd:gem", + "cmd:ruby" + ] + }, + "freeradius-postgresql": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310604 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius-sql=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "freeradius3-postgresql=3.0.15-r5", + "so:rlm_sql_postgresql.so=0" + ] + }, + "ethtool-doc": { + "versions": { + "4.13-r0": 1509492545 + }, + "origin": "ethtool" + }, + "libnet-dev": { + "versions": { + "1.1.6-r2": 1509481427 + }, + "origin": "libnet", + "dependencies": [ + "libnet=1.1.6-r2" + ], + "provides": [ + "cmd:libnet-config" + ] + }, + "libiec61883-dev": { + "versions": { + "1.2.0-r1": 1509470082 + }, + "origin": "libiec61883", + "dependencies": [ + "libraw1394-dev", + "libiec61883=1.2.0-r1", + "pc:libraw1394", + "pkgconfig" + ], + "provides": [ + "pc:libiec61883=1.2.0" + ] + }, + "py-egenix-mx-base-dev": { + "versions": { + "3.2.9-r0": 1509474195 + }, + "origin": "py-egenix-mx-base" + }, + "perl-device-serialport": { + "versions": { + "1.04-r10": 1509489402 + }, + "origin": "perl-device-serialport", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:modemtest" + ] + }, + "perl-date-manip-doc": { + "versions": { + "6.60-r0": 1509492186 + }, + "origin": "perl-date-manip" + }, + "openldap-overlay-sssvlv": { + "versions": { + "2.4.45-r3": 1510258135, + "2.4.46-r0": 1565073941, + "2.4.48-r0": 1566900518 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "rrdtool-doc": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool" + }, + "libnetfilter_cttimeout": { + "versions": { + "1.0.0-r0": 1509469233 + }, + "origin": "libnetfilter_cttimeout", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0" + ], + "provides": [ + "so:libnetfilter_cttimeout.so.1=1.0.0" + ] + }, + "tslib-dev": { + "versions": { + "1.14-r0": 1510573237 + }, + "origin": "tslib", + "dependencies": [ + "pkgconfig", + "tslib=1.14-r0" + ], + "provides": [ + "pc:tslib=1.14" + ] + }, + "py-httplib2": { + "versions": { + "0.10.3-r0": 1509476702 + }, + "origin": "py-httplib2" + }, + "perl-crypt-openssl-rsa": { + "versions": { + "0.28-r9": 1510261379 + }, + "origin": "perl-crypt-openssl-rsa", + "dependencies": [ + "perl", + "perl-crypt-openssl-random", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "libxv-doc": { + "versions": { + "1.0.11-r1": 1509467171 + }, + "origin": "libxv" + }, + "menu-cache-dev": { + "versions": { + "0.5.1-r1": 1509469820 + }, + "origin": "menu-cache", + "dependencies": [ + "menu-cache=0.5.1-r1", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libmenu-cache=0.5.1" + ] + }, + "squark-doc": { + "versions": { + "0.6.1-r1": 1509492123 + }, + "origin": "squark" + }, + "file-doc": { + "versions": { + "5.32-r0": 1509461605, + "5.32-r1": 1563340161, + "5.32-r2": 1572348414 + }, + "origin": "file" + }, + "py-twisted-doc": { + "versions": { + "17.1.0-r0": 1509493297 + }, + "origin": "py-twisted" + }, + "gucharmap": { + "versions": { + "3.18.2-r0": 1510074175 + }, + "origin": "gucharmap", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0" + ], + "provides": [ + "so:libgucharmap_2_90.so.7=7.0.0", + "cmd:charmap", + "cmd:gnome-character-map", + "cmd:gucharmap" + ] + }, + "gcalcli": { + "versions": { + "3.4.0-r1": 1509551848 + }, + "origin": "gcalcli", + "dependencies": [ + "python3", + "py-google-api-python-client", + "py-dateutil", + "py-gflags", + "py-vobject" + ], + "provides": [ + "cmd:gcalcli" + ] + }, + "gtk+3.0": { + "versions": { + "3.22.21-r0": 1510067781 + }, + "origin": "gtk+3.0", + "dependencies": [ + "shared-mime-info", + "gtk-update-icon-cache", + "/bin/sh", + "so:libX11.so.6", + "so:libXcomposite.so.1", + "so:libXcursor.so.1", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXi.so.6", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libatk-1.0.so.0", + "so:libatk-bridge-2.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo-gobject.so.2", + "so:libcairo.so.2", + "so:libcups.so.2", + "so:libepoxy.so.0", + "so:libfontconfig.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libharfbuzz.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0" + ], + "provides": [ + "so:libgailutil-3.so.0=0.0.0", + "so:libgdk-3.so.0=0.2200.21", + "so:libgtk-3.so.0=0.2200.21", + "cmd:gtk-builder-tool", + "cmd:gtk-encode-symbolic-svg", + "cmd:gtk-launch", + "cmd:gtk-query-immodules-3.0", + "cmd:gtk-query-settings", + "cmd:gtk3-demo", + "cmd:gtk3-demo-application", + "cmd:gtk3-icon-browser", + "cmd:gtk3-widget-factory" + ] + }, + "hwdata": { + "versions": { + "0.305-r0": 1509468678 + }, + "origin": "hwdata", + "dependencies": [ + "hwdata-usb", + "hwdata-pci", + "hwdata-pnp", + "hwdata-oui" + ] + }, + "gamin-dev": { + "versions": { + "0.1.10-r10": 1509481666 + }, + "origin": "gamin", + "dependencies": [ + "gamin=0.1.10-r10", + "pkgconfig" + ], + "provides": [ + "pc:gamin=0.1.10" + ] + }, + "ulogd": { + "versions": { + "2.0.5-r4": 1509480400 + }, + "origin": "ulogd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnetfilter_acct.so.1", + "so:libnetfilter_conntrack.so.3", + "so:libnetfilter_log.so.1", + "so:libnfnetlink.so.0" + ], + "provides": [ + "cmd:ulogd" + ] + }, + "damageproto": { + "versions": { + "1.2.1-r3": 1509464676 + }, + "origin": "damageproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:damageproto=1.2.1" + ] + }, + "ncurses": { + "versions": { + "6.0_p20171125-r1": 1534862983 + }, + "origin": "ncurses", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:captoinfo", + "cmd:clear", + "cmd:infocmp", + "cmd:infotocap", + "cmd:reset", + "cmd:tabs", + "cmd:tic", + "cmd:toe", + "cmd:tput", + "cmd:tset" + ] + }, + "mcabber": { + "versions": { + "1.1.0-r0": 1510588320 + }, + "origin": "mcabber", + "dependencies": [ + "beep", + "so:libc.musl-x86_64.so.1", + "so:libenchant.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgpgme.so.11", + "so:libidn.so.11", + "so:libloudmouth-1.so.0", + "so:libncursesw.so.6", + "so:libotr.so.2", + "so:libpanelw.so.6" + ], + "provides": [ + "cmd:mcabber" + ] + }, + "geeqie-doc": { + "versions": { + "1.3-r1": 1510075966 + }, + "origin": "geeqie" + }, + "lua5.1-json4": { + "versions": { + "1.0.0-r2": 1509468366 + }, + "origin": "lua-json4" + }, + "graphviz-gtk": { + "versions": { + "2.40.1-r0": 1510066919 + }, + "origin": "graphviz", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libgvc.so.6", + "so:librsvg-2.so.2" + ], + "provides": [ + "so:libgvplugin_gdk.so.6=6.0.0", + "so:libgvplugin_gtk.so.6=6.0.0", + "so:libgvplugin_rsvg.so.6=6.0.0" + ] + }, + "kamailio-postgres": { + "versions": { + "5.0.7-r0": 1532960874 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "kamailio-db", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5", + "so:libsrdb1.so.1", + "so:libsrdb2.so.1" + ] + }, + "postgresql-bdr-extension": { + "versions": { + "1.0.2-r1": 1510259297 + }, + "origin": "postgresql-bdr-extension", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:bdr_dump", + "cmd:bdr_init_copy", + "cmd:bdr_initial_load" + ] + }, + "py3-httplib2": { + "versions": { + "0.10.3-r0": 1509476701 + }, + "origin": "py-httplib2", + "dependencies": [ + "python3" + ] + }, + "py-templayer": { + "versions": { + "1.5.1-r3": 1509489479 + }, + "origin": "py-templayer", + "dependencies": [ + "python2" + ] + }, + "acf-mariadb": { + "versions": { + "0.2.0-r2": 1510068620 + }, + "origin": "acf-mariadb", + "dependencies": [ + "acf-core", + "mariadb", + "mariadb-client", + "lua-sql-mysql", + "acf-db-lib" + ] + }, + "uwsgi-msgpack": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "liboping-dev": { + "versions": { + "1.10.0-r0": 1509482706 + }, + "origin": "liboping", + "dependencies": [ + "liboping=1.10.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:liboping=1.10.0" + ] + }, + "speexdsp-dev": { + "versions": { + "1.2_rc3-r4": 1509473229 + }, + "origin": "speexdsp", + "dependencies": [ + "pkgconfig", + "speexdsp=1.2_rc3-r4" + ] + }, + "varnish-doc": { + "versions": { + "5.2.1-r0": 1511265090 + }, + "origin": "varnish" + }, + "libarchive-dev": { + "versions": { + "3.3.2-r2": 1510258000, + "3.3.3-r0": 1566312164, + "3.3.3-r1": 1572677952 + }, + "origin": "libarchive", + "dependencies": [ + "libarchive=3.3.3-r1", + "pkgconfig" + ], + "provides": [ + "pc:libarchive=3.3.3" + ] + }, + "e2fsprogs-doc": { + "versions": { + "1.43.7-r0": 1509469274, + "1.43.7-r1": 1571322243 + }, + "origin": "e2fsprogs" + }, + "libmp3splt-dev": { + "versions": { + "0.9.2-r0": 1509475937 + }, + "origin": "libmp3splt", + "dependencies": [ + "pcre-dev", + "libogg-dev", + "libmad-dev", + "libvorbis-dev", + "libid3tag-dev", + "libtool", + "libmp3splt=0.9.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmp3splt=0.9.2" + ] + }, + "apache-mod-auth-ntlm-winbind": { + "versions": { + "0.1-r6": 1509494593 + }, + "origin": "apache-mod-auth-ntlm-winbind", + "dependencies": [ + "apache2", + "samba", + "samba-winbind", + "so:libc.musl-x86_64.so.1" + ] + }, + "mt-st-doc": { + "versions": { + "1.1-r4": 1509496047 + }, + "origin": "mt-st" + }, + "xfce4-taskmanager": { + "versions": { + "1.2.0-r0": 1510074648 + }, + "origin": "xfce4-taskmanager", + "dependencies": [ + "so:libX11.so.6", + "so:libXmu.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:xfce4-taskmanager" + ] + }, + "lua5.3-ldap": { + "versions": { + "1.2.3-r5": 1509485733 + }, + "origin": "lua-ldap", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "perl-encode-dev": { + "versions": { + "2.93-r0": 1511889457 + }, + "origin": "perl-encode", + "dependencies": [ + "perl-encode-utils" + ], + "provides": [ + "cmd:enc2xs" + ] + }, + "libmnl": { + "versions": { + "1.0.4-r0": 1509469219 + }, + "origin": "libmnl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmnl.so.0=0.2.0" + ] + }, + "zlib-doc": { + "versions": { + "1.2.11-r1": 1509456390 + }, + "origin": "zlib" + }, + "proxychains-ng": { + "versions": { + "4.12-r0": 1509482460 + }, + "origin": "proxychains-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libproxychains4.so=0", + "cmd:proxychains", + "cmd:proxychains4" + ] + }, + "libvdpau": { + "versions": { + "1.1.1-r1": 1509467160 + }, + "origin": "libvdpau", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libvdpau.so.1=1.0.0" + ] + }, + "sysfsutils-doc": { + "versions": { + "2.1.0-r8": 1509475365 + }, + "origin": "sysfsutils" + }, + "libxv-dev": { + "versions": { + "1.0.11-r1": 1509467170 + }, + "origin": "libxv", + "dependencies": [ + "libxv=1.0.11-r1", + "pc:videoproto", + "pc:x11", + "pc:xext", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xv=1.0.11" + ] + }, + "ccache": { + "versions": { + "3.3.4-r0": 1509494162 + }, + "origin": "ccache", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:ccache" + ] + }, + "nmap-nping": { + "versions": { + "7.60-r2": 1510261467, + "7.60-r3": 1572297245 + }, + "origin": "nmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libpcap.so.1", + "so:libssl.so.44", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:nping" + ] + }, + "fluxbox-doc": { + "versions": { + "1.3.7-r0": 1509491166 + }, + "origin": "fluxbox" + }, + "py-idna": { + "versions": { + "2.6-r0": 1509476382 + }, + "origin": "py-idna" + }, + "collectd-curl": { + "versions": { + "5.7.2-r0": 1510069644 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libxml2.so.2", + "so:libyajl.so.2" + ] + }, + "aria2-doc": { + "versions": { + "1.33.1-r1": 1548941587 + }, + "origin": "aria2" + }, + "xf86-video-sis": { + "versions": { + "0.10.9-r0": 1510075908 + }, + "origin": "xf86-video-sis", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "net-snmp-doc": { + "versions": { + "5.7.3-r10": 1510259616 + }, + "origin": "net-snmp" + }, + "kamailio-lua": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ] + }, + "perl-date-extract-doc": { + "versions": { + "0.06-r1": 1510861954 + }, + "origin": "perl-date-extract" + }, + "acf-openssl": { + "versions": { + "0.10.1-r3": 1510070643 + }, + "origin": "acf-openssl", + "dependencies": [ + "acf-core", + "libressl" + ] + }, + "mlmmj": { + "versions": { + "1.3.0-r0": 1509492629 + }, + "origin": "mlmmj", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mlmmj-bounce", + "cmd:mlmmj-list", + "cmd:mlmmj-maintd", + "cmd:mlmmj-make-ml", + "cmd:mlmmj-make-ml.sh", + "cmd:mlmmj-process", + "cmd:mlmmj-receive", + "cmd:mlmmj-recieve", + "cmd:mlmmj-send", + "cmd:mlmmj-sub", + "cmd:mlmmj-unsub" + ] + }, + "musl-utils": { + "versions": { + "1.1.18-r3": 1518031143, + "1.1.18-r4": 1565163296 + }, + "origin": "musl", + "dependencies": [ + "!uclibc-utils", + "scanelf", + "musl=1.1.18-r4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:getconf", + "cmd:getent", + "cmd:iconv", + "cmd:ldconfig" + ] + }, + "lua-subprocess": { + "versions": { + "0.0.20141229-r2": 1509468278 + }, + "origin": "lua-subprocess" + }, + "uwsgi-router_cache": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "patch-doc": { + "versions": { + "2.7.5-r2": 1519825694, + "2.7.5-r3": 1563906800, + "2.7.6-r0": 1565250814 + }, + "origin": "patch" + }, + "libsecret-dev": { + "versions": { + "0.18.5-r0": 1509483959 + }, + "origin": "libsecret", + "dependencies": [ + "glib-dev", + "libsecret=0.18.5-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libsecret-1=0.18.5", + "pc:libsecret-unstable=0.18.5" + ] + }, + "gengetopt": { + "versions": { + "2.22.6-r2": 1509488509 + }, + "origin": "gengetopt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:gengetopt" + ] + }, + "npth-dev": { + "versions": { + "1.5-r1": 1509472897 + }, + "origin": "npth", + "dependencies": [ + "npth=1.5-r1" + ], + "provides": [ + "cmd:npth-config" + ] + }, + "lua5.3-mqtt-publish": { + "versions": { + "0.1-r0": 1509462505 + }, + "origin": "lua-mqtt-publish", + "dependencies": [ + "lua5.3-mosquitto" + ] + }, + "heimdal": { + "versions": { + "7.4.0-r2": 1514545901, + "7.4.0-r3": 1559659765, + "7.4.0-r4": 1562862337 + }, + "origin": "heimdal", + "dependencies": [ + "krb5-conf", + "so:libasn1.so.8", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libgssapi.so.3", + "so:libhcrypto.so.4", + "so:libhdb.so.9", + "so:libheimbase.so.1", + "so:libheimntlm.so.0", + "so:libhx509.so.5", + "so:libkadm5clnt.so.7", + "so:libkadm5srv.so.8", + "so:libkafs.so.0", + "so:libkdc.so.2", + "so:libkrb5.so.26", + "so:libotp.so.0", + "so:libreadline.so.7", + "so:libroken.so.18", + "so:libsl.so.0", + "so:libwind.so.0" + ], + "provides": [ + "cmd:afslog", + "cmd:bsearch", + "cmd:gsstool", + "cmd:heimdal", + "cmd:heimtools", + "cmd:hprop", + "cmd:hpropd", + "cmd:hxtool", + "cmd:idn-lookup", + "cmd:iprop-log", + "cmd:ipropd-master", + "cmd:ipropd-slave", + "cmd:kadmin", + "cmd:kadmind", + "cmd:kcm", + "cmd:kdc", + "cmd:kdestroy", + "cmd:kf", + "cmd:kfd", + "cmd:kgetcred", + "cmd:kimpersonate", + "cmd:kinit", + "cmd:klist", + "cmd:kpasswd", + "cmd:kpasswdd", + "cmd:kstash", + "cmd:kswitch", + "cmd:ktutil", + "cmd:otp", + "cmd:otpprint", + "cmd:pagsh", + "cmd:su" + ] + }, + "screen-doc": { + "versions": { + "4.6.2-r0": 1509492558 + }, + "origin": "screen" + }, + "perl-module-runtime-doc": { + "versions": { + "0.014-r1": 1509470561 + }, + "origin": "perl-module-runtime" + }, + "gnats-doc": { + "versions": { + "4.2.0-r3": 1509495999 + }, + "origin": "gnats" + }, + "libmng": { + "versions": { + "2.0.3-r1": 1509480914 + }, + "origin": "libmng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libmng.so.2=2.0.2" + ] + }, + "graphite2-dev": { + "versions": { + "1.3.10-r0": 1509464858 + }, + "origin": "graphite2", + "dependencies": [ + "freetype-dev", + "graphite2=1.3.10-r0", + "pkgconfig" + ], + "provides": [ + "pc:graphite2=3.0.1" + ] + }, + "cgit-doc": { + "versions": { + "1.1-r3": 1533395015 + }, + "origin": "cgit" + }, + "pianobar-doc": { + "versions": { + "2017.08.30-r0": 1510074140 + }, + "origin": "pianobar" + }, + "xfce4-session-lang": { + "versions": { + "4.12.1-r2": 1510074577 + }, + "origin": "xfce4-session", + "dependencies": [ + "hicolor-icon-theme", + "iceauth", + "dbus-x11" + ] + }, + "eggdrop-logs2html": { + "versions": { + "1.6.21-r2": 1509491903 + }, + "origin": "eggdrop", + "dependencies": [ + "tcl", + "so:libc.musl-x86_64.so.1" + ] + }, + "libnfnetlink": { + "versions": { + "1.0.1-r1": 1509469211 + }, + "origin": "libnfnetlink", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnfnetlink.so.0=0.2.0" + ] + }, + "xscreensaver": { + "versions": { + "5.36-r0": 1510074358 + }, + "origin": "xscreensaver", + "dependencies": [ + "bc", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXinerama.so.1", + "so:libXmu.so.6", + "so:libXrandr.so.2", + "so:libXt.so.6", + "so:libXxf86vm.so.1", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgdk_pixbuf_xlib-2.0.so.0", + "so:libglade-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:xscreensaver", + "cmd:xscreensaver-command", + "cmd:xscreensaver-demo", + "cmd:xscreensaver-getimage", + "cmd:xscreensaver-getimage-file", + "cmd:xscreensaver-getimage-video", + "cmd:xscreensaver-text" + ] + }, + "py3-mccabe": { + "versions": { + "0.6.1-r1": 1511661235 + }, + "origin": "py-mccabe", + "dependencies": [ + "python3" + ] + }, + "qemu-ppc64le": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-ppc64le" + ] + }, + "xspice": { + "versions": { + "0.1.5-r3": 1510073031 + }, + "origin": "xf86-video-qxl", + "dependencies": [ + "python2", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libspice-server.so.1" + ], + "provides": [ + "cmd:Xspice" + ] + }, + "lua-ldap": { + "versions": { + "1.2.3-r5": 1509485734 + }, + "origin": "lua-ldap" + }, + "icu-libs": { + "versions": { + "59.1-r1": 1509464844 + }, + "origin": "icu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libicudata.so.59=59.1", + "so:libicui18n.so.59=59.1", + "so:libicuio.so.59=59.1", + "so:libicutest.so.59=59.1", + "so:libicutu.so.59=59.1", + "so:libicuuc.so.59=59.1" + ] + }, + "py-irc": { + "versions": { + "8.5.1-r0": 1509491083 + }, + "origin": "py-irc" + }, + "musl": { + "versions": { + "1.1.18-r3": 1518031143, + "1.1.18-r4": 1565163296 + }, + "origin": "musl", + "provides": [ + "so:libc.musl-x86_64.so.1=1" + ] + }, + "libmilter-dev": { + "versions": { + "1.0.2-r5": 1509479368 + }, + "origin": "libmilter", + "dependencies": [ + "libmilter=1.0.2-r5" + ] + }, + "perl-hash-multivalue-doc": { + "versions": { + "0.16-r0": 1511871381 + }, + "origin": "perl-hash-multivalue" + }, + "lua5.2-sqlite": { + "versions": { + "0.9.4-r2": 1509482116 + }, + "origin": "lua-sqlite", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "xclip-doc": { + "versions": { + "0.13-r0": 1509495878 + }, + "origin": "xclip" + }, + "gvfs-dav": { + "versions": { + "1.34.1-r0": 1511430258, + "1.34.1-r1": 1563787224 + }, + "origin": "gvfs", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-glib.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgvfscommon.so", + "so:libgvfsdaemon.so", + "so:libintl.so.8", + "so:libsoup-2.4.so.1", + "so:libxml2.so.2" + ] + }, + "py-django-djblets": { + "versions": { + "0.6.31-r0": 1509475948 + }, + "origin": "py-django-djblets", + "dependencies": [ + "python2", + "py-django-pipeline" + ] + }, + "irssi": { + "versions": { + "1.0.6-r0": 1519052408, + "1.0.8-r0": 1562236917 + }, + "origin": "irssi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libncursesw.so.6", + "so:libssl.so.44" + ], + "provides": [ + "cmd:irssi" + ] + }, + "mercurial-doc": { + "versions": { + "4.5.2-r0": 1532937167, + "4.5.2-r1": 1563792023 + }, + "origin": "mercurial" + }, + "s6-portable-utils-doc": { + "versions": { + "2.2.1.1-r0": 1509488754 + }, + "origin": "s6-portable-utils" + }, + "py2-yaml": { + "versions": { + "3.12-r1": 1509485804 + }, + "origin": "py-yaml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libyaml-0.so.2" + ] + }, + "tcl-dev": { + "versions": { + "8.6.7-r0": 1509460298 + }, + "origin": "tcl", + "dependencies": [ + "tcl", + "pc:zlib>=1.2.3", + "pkgconfig" + ], + "provides": [ + "pc:tcl=8.6.7" + ] + }, + "cryptsetup-doc": { + "versions": { + "1.7.5-r1": 1510261295 + }, + "origin": "cryptsetup" + }, + "sems-diameter_client": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libssl.so.44", + "so:libstdc++.so.6" + ] + }, + "st": { + "versions": { + "0.7-r1": 1509494026 + }, + "origin": "st", + "dependencies": [ + "ncurses-terminfo", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1" + ], + "provides": [ + "cmd:st" + ] + }, + "lucene++-dev": { + "versions": { + "3.0.7-r5": 1509496353 + }, + "origin": "lucene++", + "dependencies": [ + "boost-dev", + "lucene++=3.0.7-r5", + "pkgconfig" + ], + "provides": [ + "pc:liblucene++-contrib=3.0.7", + "pc:liblucene++=3.0.7" + ] + }, + "confuse-dev": { + "versions": { + "3.2.1-r0": 1509471872 + }, + "origin": "confuse", + "dependencies": [ + "confuse=3.2.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libconfuse=3.2.1" + ] + }, + "sshpass-doc": { + "versions": { + "1.06-r0": 1509494947 + }, + "origin": "sshpass" + }, + "krb5-server": { + "versions": { + "1.15.4-r0": 1548491443 + }, + "origin": "krb5", + "dependencies": [ + "libverto-libev", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libgssapi_krb5.so.2", + "so:libgssrpc.so.4", + "so:libk5crypto.so.3", + "so:libkadm5clnt_mit.so.11", + "so:libkadm5srv_mit.so.11", + "so:libkdb5.so.8", + "so:libkdb_ldap.so.1", + "so:libkrb5.so.3", + "so:libkrb5support.so.0", + "so:libss.so.2", + "so:libverto.so.1" + ], + "provides": [ + "cmd:gss-server", + "cmd:kadmin.local", + "cmd:kadmind", + "cmd:kdb5_ldap_util", + "cmd:kdb5_util", + "cmd:kprop", + "cmd:kpropd", + "cmd:kproplog", + "cmd:krb5-send-pr", + "cmd:krb5kdc", + "cmd:sclient", + "cmd:sim_server", + "cmd:sserver", + "cmd:uuserver" + ] + }, + "librtmp": { + "versions": { + "2.4_git20160909-r3": 1510259117 + }, + "origin": "rtmpdump", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:librtmp.so.1=1" + ] + }, + "lua-crypto": { + "versions": { + "0.3.2-r5": 1510261401 + }, + "origin": "lua-crypto" + }, + "aspell-en": { + "versions": { + "2017.01.22-r0": 1509496074 + }, + "origin": "aspell-en" + }, + "nagios-plugins-log": { + "versions": { + "2.2.1-r3": 1510288496 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "grep" + ] + }, + "atop-doc": { + "versions": { + "2.3.0-r1": 1509489334 + }, + "origin": "atop" + }, + "yaml": { + "versions": { + "0.1.7-r0": 1509470097 + }, + "origin": "yaml", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libyaml-0.so.2=2.0.5" + ] + }, + "newsbeuter-lang": { + "versions": { + "2.9-r6": 1510288209 + }, + "origin": "newsbeuter" + }, + "samba-dev": { + "versions": { + "4.7.6-r3": 1555491781 + }, + "origin": "samba", + "dependencies": [ + "libsmbclient=4.7.6-r3", + "libwbclient=4.7.6-r3", + "pc:ldb", + "pc:talloc", + "pc:tevent", + "pkgconfig", + "samba-client-libs=4.7.6-r3", + "samba-common-libs=4.7.6-r3", + "samba-common-server-libs=4.7.6-r3", + "samba-dc-libs=4.7.6-r3", + "samba-libnss-winbind=4.7.6-r3", + "samba-libs=4.7.6-r3" + ], + "provides": [ + "pc:dcerpc=0.0.1", + "pc:dcerpc_samr=0.0.1", + "pc:dcerpc_server=0.0.1", + "pc:ndr=0.1.0", + "pc:ndr_krb5pac=0.0.1", + "pc:ndr_nbt=0.0.1", + "pc:ndr_standard=0.0.1", + "pc:netapi=0", + "pc:samba-credentials=0.0.1", + "pc:samba-hostconfig=0.0.1", + "pc:samba-policy=0.0.1", + "pc:samba-util=0.0.1", + "pc:samdb=0.0.1", + "pc:smbclient=0.2.3", + "pc:wbclient=0.14" + ] + }, + "gnokii-doc": { + "versions": { + "0.6.31-r6": 1510069844 + }, + "origin": "gnokii" + }, + "cegui06": { + "versions": { + "0.6.2b-r14": 1510068748 + }, + "origin": "cegui06", + "dependencies": [ + "so:libGL.so.1", + "so:libGLEW.so.2.1", + "so:libGLU.so.1", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libfreetype.so.6", + "so:libgcc_s.so.1", + "so:liblua.so.5", + "so:libpcre.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libCEGUIBase-0.6.2.so=0", + "so:libCEGUIExpatParser-0.6.2.so=0", + "so:libCEGUIFalagardWRBase-0.6.2.so=0", + "so:libCEGUILuaScriptModule-0.6.2.so=0", + "so:libCEGUIOpenGLRenderer-0.6.2.so=0", + "so:libCEGUITGAImageCodec-0.6.2.so=0", + "so:libCEGUITinyXMLParser-0.6.2.so=0", + "so:libCEGUItoluapp-0.6.2.so=0" + ] + }, + "dropbear-scp": { + "versions": { + "2018.76-r2": 1537439117 + }, + "origin": "dropbear", + "dependencies": [ + "!openssh-client", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:scp" + ] + }, + "byobu-doc": { + "versions": { + "5.123-r1": 1509470002 + }, + "origin": "byobu" + }, + "fontconfig-dev": { + "versions": { + "2.12.6-r0": 1509462083 + }, + "origin": "fontconfig", + "dependencies": [ + "fontconfig=2.12.6-r0", + "pc:expat", + "pc:freetype2", + "pkgconfig" + ], + "provides": [ + "pc:fontconfig=2.12.6" + ] + }, + "mysql-bench": { + "versions": { + "10.1.38-r1": 1551187995, + "10.1.40-r0": 1560354890, + "10.1.41-r0": 1565163115 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-client" + ] + }, + "statserial": { + "versions": { + "1.1-r4": 1509491852 + }, + "origin": "statserial", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:statserial" + ] + }, + "perl-test-failwarnings-doc": { + "versions": { + "0.008-r1": 1510855678 + }, + "origin": "perl-test-failwarnings" + }, + "libburn-doc": { + "versions": { + "1.4.8-r0": 1509462200 + }, + "origin": "libburn" + }, + "gnokii-dev": { + "versions": { + "0.6.31-r6": 1510069842 + }, + "origin": "gnokii", + "dependencies": [ + "gnokii-libs=0.6.31-r6", + "pkgconfig" + ], + "provides": [ + "pc:gnokii=0.6.31", + "pc:xgnokii=1.0" + ] + }, + "libxml2-utils": { + "versions": { + "2.9.8-r1": 1540398579 + }, + "origin": "libxml2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:xmlcatalog", + "cmd:xmllint" + ] + }, + "perl-role-basic-doc": { + "versions": { + "0.13-r0": 1509468425 + }, + "origin": "perl-role-basic" + }, + "xcmsdb-doc": { + "versions": { + "1.0.5-r0": 1509494048 + }, + "origin": "xcmsdb" + }, + "py3-gunicorn": { + "versions": { + "19.7.1-r1": 1509493758 + }, + "origin": "py-gunicorn", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:gunicorn", + "cmd:gunicorn_paster" + ] + }, + "znc-extra": { + "versions": { + "1.7.1-r0": 1531900839, + "1.7.1-r1": 1565877330 + }, + "origin": "znc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "snappy-doc": { + "versions": { + "1.1.4-r2": 1509483342 + }, + "origin": "snappy" + }, + "py-tevent": { + "versions": { + "0.9.34-r0": 1511479236 + }, + "origin": "tevent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libtalloc.so.2", + "so:libtevent.so.0" + ] + }, + "static-routing": { + "versions": { + "1.0-r0": 1509491854 + }, + "origin": "static-routing" + }, + "geeqie-lang": { + "versions": { + "1.3-r1": 1510075967 + }, + "origin": "geeqie" + }, + "perl-html-format-doc": { + "versions": { + "2.11-r0": 1509481563 + }, + "origin": "perl-html-format" + }, + "ntop-dev": { + "versions": { + "5.0.1-r11": 1510314558 + }, + "origin": "ntop", + "dependencies": [ + "ntop=5.0.1-r11" + ] + }, + "libva-dev": { + "versions": { + "1.8.2-r0": 1510071998 + }, + "origin": "libva", + "dependencies": [ + "mesa-dev", + "libva=1.8.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libva-drm=0.40.0", + "pc:libva-egl=0.40.0", + "pc:libva-glx=0.40.0", + "pc:libva-tpi=0.40.0", + "pc:libva-x11=0.40.0", + "pc:libva=0.40.0" + ] + }, + "libxinerama-dev": { + "versions": { + "1.1.3-r1": 1509468244 + }, + "origin": "libxinerama", + "dependencies": [ + "libxinerama=1.1.3-r1", + "pc:x11", + "pc:xext", + "pc:xineramaproto", + "pkgconfig" + ], + "provides": [ + "pc:xinerama=1.1.3" + ] + }, + "perl-namespace-autoclean-doc": { + "versions": { + "0.28-r0": 1509474082 + }, + "origin": "perl-namespace-autoclean" + }, + "hwdata-pci": { + "versions": { + "0.305-r0": 1509468678 + }, + "origin": "hwdata" + }, + "fish-dev": { + "versions": { + "2.6.0-r2": 1511486201 + }, + "origin": "fish", + "dependencies": [ + "fish-tools" + ] + }, + "goffice-doc": { + "versions": { + "0.10.36-r0": 1511455403 + }, + "origin": "goffice" + }, + "lm_sensors-sensord": { + "versions": { + "3.4.0-r4": 1509475444 + }, + "origin": "lm_sensors", + "dependencies": [ + "bash", + "sysfsutils", + "so:libc.musl-x86_64.so.1", + "so:librrd.so.4", + "so:libsensors.so.4" + ], + "provides": [ + "cmd:sensord" + ] + }, + "tiff-doc": { + "versions": { + "4.0.10-r0": 1544169734, + "4.0.10-r1": 1566982293, + "4.0.10-r2": 1572815888 + }, + "origin": "tiff" + }, + "apcupsd-webif": { + "versions": { + "3.14.14-r0": 1509495262 + }, + "origin": "apcupsd", + "dependencies": [ + "util-linux", + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ] + }, + "lxc-lvm": { + "versions": { + "2.1.1-r3": 1533578832 + }, + "origin": "lxc", + "dependencies": [ + "lvm2", + "util-linux", + "lxc" + ] + }, + "perl-fcgi-procmanager-doc": { + "versions": { + "0.28-r0": 1511894069 + }, + "origin": "perl-fcgi-procmanager" + }, + "perl-path-tiny-doc": { + "versions": { + "0.104-r1": 1510855684 + }, + "origin": "perl-path-tiny" + }, + "keybinder": { + "versions": { + "0.3.0-r1": 1510069943 + }, + "origin": "keybinder", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ], + "provides": [ + "so:libkeybinder.so.0=0.1.0" + ] + }, + "cups-libs": { + "versions": { + "2.2.10-r0": 1549287809, + "2.2.12-r0": 1566207591 + }, + "origin": "cups", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30", + "so:libz.so.1" + ], + "provides": [ + "so:libcups.so.2=2", + "so:libcupsimage.so.2=2" + ] + }, + "perl-carp": { + "versions": { + "1.38-r0": 1509483988 + }, + "origin": "perl-carp" + }, + "llvm5-test-utils": { + "versions": { + "5.0.0-r0": 1510682602 + }, + "origin": "llvm5", + "dependencies": [ + "python2", + "py-setuptools", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "llvm-test-utils=5.0.0-r0", + "lit=0.6.0-r0", + "cmd:lit" + ] + }, + "py2-pillow": { + "versions": { + "4.3.0-r0": 1509489958 + }, + "origin": "py-pillow", + "dependencies": [ + "py2-olefile", + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libopenjp2.so.7", + "so:libpython2.7.so.1.0", + "so:libtiff.so.5", + "so:libwebp.so.7", + "so:libwebpmux.so.3", + "so:libz.so.1" + ] + }, + "razor-doc": { + "versions": { + "2.85-r6": 1509491110 + }, + "origin": "razor" + }, + "lua5.2": { + "versions": { + "5.2.4-r4": 1509459695 + }, + "origin": "lua5.2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua-5.2.so.0" + ], + "provides": [ + "cmd:lua5.2", + "cmd:luac5.2" + ] + }, + "lighttpd-mod_auth": { + "versions": { + "1.4.48-r0": 1511925915 + }, + "origin": "lighttpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libpcre2-32": { + "versions": { + "10.30-r0": 1509461774 + }, + "origin": "pcre2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre2-32.so.0=0.6.0" + ] + }, + "mtdev-dev": { + "versions": { + "1.1.5-r1": 1509475741 + }, + "origin": "mtdev", + "dependencies": [ + "linux-headers", + "mtdev=1.1.5-r1", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libmtdev.so.1" + ], + "provides": [ + "pc:mtdev=1.1.5", + "cmd:mtdev-test" + ] + }, + "freeradius-mssql": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310603 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius-sql=3.0.15-r5" + ], + "provides": [ + "freeradius3-mssql=3.0.15-r5" + ] + }, + "perl-datetime-format-mail": { + "versions": { + "0.403-r0": 1511386193 + }, + "origin": "perl-datetime-format-mail", + "dependencies": [ + "perl", + "perl-datetime", + "perl-params-validate" + ] + }, + "abiword-plugin-loadbindings": { + "versions": { + "3.0.2-r1": 1510073363 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6", + "so:libxml2.so.2" + ] + }, + "ipptool": { + "versions": { + "2.2.10-r0": 1549287809, + "2.2.12-r0": 1566207592 + }, + "origin": "cups", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2" + ], + "provides": [ + "cmd:ipptool" + ] + }, + "perl-math-round": { + "versions": { + "0.07-r0": 1509483899 + }, + "origin": "perl-math-round", + "dependencies": [ + "perl" + ] + }, + "util-linux-dev": { + "versions": { + "2.31.1-r0": 1541506293 + }, + "origin": "util-linux", + "dependencies": [ + "libblkid=2.31.1-r0", + "libfdisk=2.31.1-r0", + "libmount=2.31.1-r0", + "libsmartcols=2.31.1-r0", + "libuuid=2.31.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:blkid=2.31.1", + "pc:fdisk=2.31.1", + "pc:mount=2.31.1", + "pc:smartcols=2.31.1", + "pc:uuid=2.31.1" + ] + }, + "ppp-pppoe": { + "versions": { + "2.4.7-r5": 1509480131 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pppoe-discovery" + ] + }, + "lua5.3-md5": { + "versions": { + "1.2-r3": 1509468358 + }, + "origin": "lua-md5", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "command-not-found": { + "versions": { + "0.2-r0": 1509688078 + }, + "origin": "command-not-found" + }, + "gettext-asprintf": { + "versions": { + "0.19.8.1-r1": 1509459196 + }, + "origin": "gettext", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libasprintf.so.0=0.0.0" + ] + }, + "ccache-doc": { + "versions": { + "3.3.4-r0": 1509494162 + }, + "origin": "ccache" + }, + "xfce4-notifyd": { + "versions": { + "0.3.6-r0": 1510075459 + }, + "origin": "xfce4-notifyd", + "dependencies": [ + "hicolor-icon-theme", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libnotify.so.4", + "so:libxfce4ui-2.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:xfce4-notifyd-config" + ] + }, + "expat-doc": { + "versions": { + "2.2.5-r0": 1509908121, + "2.2.7-r0": 1561897528, + "2.2.7-r1": 1568353727, + "2.2.8-r0": 1568974114 + }, + "origin": "expat" + }, + "libmng-dev": { + "versions": { + "2.0.3-r1": 1509480913 + }, + "origin": "libmng", + "dependencies": [ + "libjpeg-turbo-dev", + "libmng=2.0.3-r1", + "pkgconfig" + ], + "provides": [ + "pc:libmng=2.0.2" + ] + }, + "ipvsadm-doc": { + "versions": { + "1.29-r0": 1509494681 + }, + "origin": "ipvsadm" + }, + "gcc": { + "versions": { + "6.4.0-r5": 1509458083 + }, + "origin": "gcc", + "dependencies": [ + "binutils", + "isl", + "libgomp=6.4.0-r5", + "libatomic=6.4.0-r5", + "libgomp=6.4.0-r5", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgmp.so.10", + "so:libisl.so.15", + "so:libmpc.so.3", + "so:libmpfr.so.4", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libcc1.so.0=0.0.0", + "so:libcilkrts.so.5=5.0.0", + "so:libitm.so.1=1.0.0", + "pc:libgcj-6=6.4.0", + "cmd:cc", + "cmd:cpp", + "cmd:gcc", + "cmd:gcc-ar", + "cmd:gcc-nm", + "cmd:gcc-ranlib", + "cmd:gcov", + "cmd:gcov-dump", + "cmd:gcov-tool", + "cmd:x86_64-alpine-linux-musl-gcc", + "cmd:x86_64-alpine-linux-musl-gcc-6.4.0", + "cmd:x86_64-alpine-linux-musl-gcc-ar", + "cmd:x86_64-alpine-linux-musl-gcc-nm", + "cmd:x86_64-alpine-linux-musl-gcc-ranlib", + "cmd:x86_64-alpine-linux-musl-gcj" + ] + }, + "libtasn1-dev": { + "versions": { + "4.12-r3": 1519805758, + "4.12-r4": 1563955390 + }, + "origin": "libtasn1", + "dependencies": [ + "libtasn1=4.12-r4", + "pkgconfig" + ], + "provides": [ + "pc:libtasn1=4.12" + ] + }, + "fish": { + "versions": { + "2.6.0-r2": 1511486202 + }, + "origin": "fish", + "dependencies": [ + "bc", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:fish", + "cmd:fish_indent", + "cmd:fish_key_reader" + ] + }, + "freeradius-sqlite": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310604 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius-sql=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ], + "provides": [ + "freeradius3-sqlite=3.0.15-r5", + "so:rlm_sql_sqlite.so=0" + ] + }, + "libotr3-tools": { + "versions": { + "3.2.1-r4": 1509481190 + }, + "origin": "libotr3", + "dependencies": [ + "libotr3", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libotr.so.2" + ], + "provides": [ + "cmd:otr_mackey", + "cmd:otr_modify", + "cmd:otr_parse", + "cmd:otr_readforge", + "cmd:otr_remac", + "cmd:otr_sesskeys" + ] + }, + "nagios-plugins-hpjd": { + "versions": { + "2.2.1-r3": 1510288495 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "collectd-ceph": { + "versions": { + "5.7.2-r0": 1510069652 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libyajl.so.2" + ] + }, + "abiword-plugin-xslfo": { + "versions": { + "3.0.2-r1": 1510073370 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "perl-datetime-doc": { + "versions": { + "1.44-r0": 1510859302 + }, + "origin": "perl-datetime" + }, + "xprop": { + "versions": { + "1.2.1-r1": 1509491073 + }, + "origin": "xprop", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xprop" + ] + }, + "libxext-doc": { + "versions": { + "1.3.3-r2": 1509464538 + }, + "origin": "libxext" + }, + "squid-lang-de": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "avahi-glib": { + "versions": { + "0.6.32-r4": 1509465447, + "0.6.32-r5": 1563345619 + }, + "origin": "avahi", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libavahi-glib.so.1=1.0.2", + "so:libavahi-gobject.so.0=0.0.4" + ] + }, + "xf86-video-tdfx": { + "versions": { + "1.4.7-r0": 1510075839 + }, + "origin": "xf86-video-tdfx", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "nagios-plugins-ldap": { + "versions": { + "2.2.1-r3": 1510288496 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libldap-2.4.so.2" + ] + }, + "nagios-plugins-icmp": { + "versions": { + "2.2.1-r3": 1510288495 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "dovecot-mysql": { + "versions": { + "2.2.36.3-r0": 1554102389, + "2.2.36.4-r0": 1567075098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot-sql", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ], + "provides": [ + "so:libdriver_mysql.so=0" + ] + }, + "boost-signals": { + "versions": { + "1.62.0-r5": 1509465880 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_signals-mt.so.1.62.0=1.62.0", + "so:libboost_signals.so.1.62.0=1.62.0" + ] + }, + "gconf-lang": { + "versions": { + "3.2.6-r1": 1510928342 + }, + "origin": "gconf" + }, + "boost-doc": { + "versions": { + "1.62.0-r5": 1509465873 + }, + "origin": "boost" + }, + "qemu-system-xtensaeb": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-xtensaeb" + ] + }, + "desktop-file-utils": { + "versions": { + "0.23-r0": 1509470841 + }, + "origin": "desktop-file-utils", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:desktop-file-edit", + "cmd:desktop-file-install", + "cmd:desktop-file-validate", + "cmd:update-desktop-database" + ] + }, + "xorriso": { + "versions": { + "1.4.8-r0": 1509462217 + }, + "origin": "libisoburn", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libisoburn.so.1" + ], + "provides": [ + "cmd:mkisofs", + "cmd:osirrox", + "cmd:xorrecord", + "cmd:xorriso", + "cmd:xorrisofs" + ] + }, + "ttf-linux-libertine-doc": { + "versions": { + "5.3.0-r0": 1509469875 + }, + "origin": "ttf-linux-libertine" + }, + "libxfont": { + "versions": { + "1.5.4-r0": 1511976122 + }, + "origin": "libxfont", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfontenc.so.1", + "so:libfreetype.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libXfont.so.1=1.4.1" + ] + }, + "avfs-dev": { + "versions": { + "1.0.5-r0": 1509496597 + }, + "origin": "avfs", + "dependencies": [ + "avfs=1.0.5-r0" + ], + "provides": [ + "cmd:avfs-config" + ] + }, + "libxrender-dev": { + "versions": { + "0.9.10-r2": 1509462094 + }, + "origin": "libxrender", + "dependencies": [ + "libxrender=0.9.10-r2", + "pc:renderproto>=0.9", + "pc:x11", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xrender=0.9.10" + ] + }, + "qemu-system-alpha": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-alpha" + ] + }, + "sendpage": { + "versions": { + "1.0.3-r5": 1509489417 + }, + "origin": "sendpage", + "dependencies": [ + "perl-mailtools", + "perl-net-snpp", + "perl-device-serialport", + "perl-dbi", + "perl-sys-hostname-long", + "perl-test-mockobject" + ], + "provides": [ + "cmd:email2page", + "cmd:sendmail2snpp", + "cmd:sendpage", + "cmd:sendpage-db", + "cmd:snpp" + ] + }, + "pound": { + "versions": { + "2.7-r4": 1510261014 + }, + "origin": "pound", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpcreposix.so.0", + "so:libssl.so.44" + ], + "provides": [ + "cmd:pound", + "cmd:poundctl" + ] + }, + "lua5.3-gversion.lua": { + "versions": { + "0.2.0-r1": 1509475306 + }, + "origin": "lua-gversion", + "dependencies": [ + "lua5.3" + ] + }, + "lua-sqlite": { + "versions": { + "0.9.4-r2": 1509482117 + }, + "origin": "lua-sqlite" + }, + "openldap-overlay-dds": { + "versions": { + "2.4.45-r3": 1510258134, + "2.4.46-r0": 1565073940, + "2.4.48-r0": 1566900517 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "perl-plack": { + "versions": { + "1.0033-r0": 1510588948 + }, + "origin": "perl-plack", + "dependencies": [ + "perl-file-sharedir", + "perl-filesys-notify-simple", + "perl-devel-stacktrace", + "perl-stream-buffered", + "perl-uri", + "perl-devel-stacktrace-ashtml", + "perl-http-body", + "perl-test-tcp", + "perl-try-tiny", + "perl-apache-logformat-compiler", + "perl-hash-multivalue", + "perl-http-message" + ], + "provides": [ + "cmd:plackup" + ] + }, + "xfce4-wavelan-plugin-lang": { + "versions": { + "0.6.0-r0": 1510072789 + }, + "origin": "xfce4-wavelan-plugin" + }, + "rabbitmq-c-dev": { + "versions": { + "0.8.0-r3": 1510260052 + }, + "origin": "rabbitmq-c", + "dependencies": [ + "libressl-dev", + "popt-dev", + "pkgconfig", + "rabbitmq-c=0.8.0-r3" + ], + "provides": [ + "pc:librabbitmq=0.8.0" + ] + }, + "libquvi-doc": { + "versions": { + "0.9.4-r3": 1509492379 + }, + "origin": "libquvi" + }, + "glib-bash-completion": { + "versions": { + "2.54.2-r0": 1509911133, + "2.54.2-r1": 1560764733 + }, + "origin": "glib" + }, + "openobex-doc": { + "versions": { + "1.7.2-r1": 1510071734 + }, + "origin": "openobex" + }, + "libatomic_ops": { + "versions": { + "7.4.8-r0": 1509878778 + }, + "origin": "libatomic_ops", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libatomic_ops.so.1=1.0.4", + "so:libatomic_ops_gpl.so.1=1.0.4" + ] + }, + "iptraf-ng": { + "versions": { + "1.1.4-r4": 1509495004 + }, + "origin": "iptraf-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libpanelw.so.6" + ], + "provides": [ + "cmd:iptraf-ng", + "cmd:rvnamed-ng" + ] + }, + "py-ipaddress": { + "versions": { + "1.0.18-r1": 1509480834 + }, + "origin": "py-ipaddress" + }, + "libgss": { + "versions": { + "0.1.5-r1": 1509493415 + }, + "origin": "libgss", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgss.so.1=1.1.0", + "cmd:gss" + ] + }, + "abuild": { + "versions": { + "3.1.0-r4": 1527832620, + "3.1.0-r5": 1572356050 + }, + "origin": "abuild", + "dependencies": [ + "fakeroot", + "sudo", + "pax-utils", + "libressl", + "apk-tools>=2.0.7-r1", + "libc-utils", + "attr", + "tar", + "pkgconf", + "patch", + "lzip", + "curl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libz.so.1" + ], + "provides": [ + "cmd:abuild", + "cmd:abuild-addgroup", + "cmd:abuild-adduser", + "cmd:abuild-apk", + "cmd:abuild-fetch", + "cmd:abuild-gzsplit", + "cmd:abuild-keygen", + "cmd:abuild-rmtemp", + "cmd:abuild-sign", + "cmd:abuild-sudo", + "cmd:abuild-tar", + "cmd:abump", + "cmd:apkgrel", + "cmd:buildlab", + "cmd:checkapk", + "cmd:newapkbuild" + ] + }, + "perl-html-tagset": { + "versions": { + "3.20-r1": 1509464387 + }, + "origin": "perl-html-tagset" + }, + "polkit-gnome": { + "versions": { + "0.105-r0": 1510075800 + }, + "origin": "polkit-gnome", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpolkit-agent-1.so.0", + "so:libpolkit-gobject-1.so.0" + ] + }, + "perl-x10-doc": { + "versions": { + "0.04-r0": 1509494413 + }, + "origin": "perl-x10" + }, + "pekwm-doc": { + "versions": { + "0.1.17-r2": 1509494547 + }, + "origin": "pekwm" + }, + "lm_sensors-doc": { + "versions": { + "3.4.0-r4": 1509475443 + }, + "origin": "lm_sensors" + }, + "grub-efi": { + "versions": { + "2.02-r3": 1509495739 + }, + "origin": "grub", + "dependencies": [ + "grub" + ] + }, + "libraw-doc": { + "versions": { + "0.18.6-r0": 1514450639 + }, + "origin": "libraw" + }, + "pcsc-lite-dev": { + "versions": { + "1.8.22-r0": 1509474497 + }, + "origin": "pcsc-lite", + "dependencies": [ + "eudev-dev", + "pcsc-lite-libs=1.8.22-r0", + "pkgconfig" + ], + "provides": [ + "pc:libpcsclite=1.8.22" + ] + }, + "libxfce4ui-gtk3": { + "versions": { + "4.12.1-r3": 1510067994 + }, + "origin": "libxfce4ui", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libpango-1.0.so.0", + "so:libstartup-notification-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "so:libxfce4kbd-private-3.so.0=0.0.0", + "so:libxfce4ui-2.so.0=0.0.0" + ] + }, + "liblcms": { + "versions": { + "1.19-r6": 1509477356 + }, + "origin": "lcms", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblcms.so.1=1.0.19" + ] + }, + "nagios-plugins-nagios": { + "versions": { + "2.2.1-r3": 1510288496 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "mcpp-doc": { + "versions": { + "2.7.2-r1": 1509473730 + }, + "origin": "mcpp" + }, + "ncurses-dev": { + "versions": { + "6.0_p20171125-r1": 1534862974 + }, + "origin": "ncurses", + "dependencies": [ + "ncurses-libs=6.0_p20171125-r1", + "pkgconfig" + ], + "provides": [ + "pc:form=6.0.20171125", + "pc:formw=6.0.20171125", + "pc:menu=6.0.20171125", + "pc:menuw=6.0.20171125", + "pc:ncurses=6.0.20171125", + "pc:ncursesw=6.0.20171125", + "pc:panel=6.0.20171125", + "pc:panelw=6.0.20171125", + "cmd:ncursesw6-config" + ] + }, + "abiword-dev": { + "versions": { + "3.0.2-r1": 1510073354 + }, + "origin": "abiword", + "dependencies": [ + "pc:cairo-fc", + "pc:cairo-pdf", + "pc:cairo-ps", + "pc:enchant>=1.2.0", + "pc:fribidi>=0.10.4", + "pc:gio-2.0", + "pc:glib-2.0>=2.6.0", + "pc:gobject-2.0>=2.6.0", + "pc:gthread-2.0>=2.6.0", + "pc:gtk+-3.0>=3.0.8", + "pc:gtk+-unix-print-3.0", + "pc:libgoffice-0.10>=0.10.0", + "pc:libgsf-1>=1.14.18", + "pc:librsvg-2.0>=2.16.0", + "pc:libxslt", + "pc:pangocairo", + "pc:wv-1.0>=1.2.0", + "pc:x11", + "pkgconfig" + ], + "provides": [ + "pc:abiword-3.0=3.0.2" + ] + }, + "liboping-doc": { + "versions": { + "1.10.0-r0": 1509482706 + }, + "origin": "liboping" + }, + "check-dev": { + "versions": { + "0.12.0-r1": 1509461855 + }, + "origin": "check", + "dependencies": [ + "check=0.12.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:check=0.12.0" + ] + }, + "libuv-dbg": { + "versions": { + "1.17.0-r0": 1511652082 + }, + "origin": "libuv" + }, + "nagios-plugins-disk_smb": { + "versions": { + "2.2.1-r3": 1510288494 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "samba-client" + ] + }, + "slang": { + "versions": { + "2.3.1a-r0": 1509476136 + }, + "origin": "slang", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ], + "provides": [ + "so:libslang.so.2=2.3.1", + "cmd:slsh" + ] + }, + "xf86-video-siliconmotion-doc": { + "versions": { + "1.7.9-r0": 1510074837 + }, + "origin": "xf86-video-siliconmotion" + }, + "v4l-utils-doc": { + "versions": { + "1.12.5-r1": 1510072042 + }, + "origin": "v4l-utils" + }, + "lzip": { + "versions": { + "1.19-r1": 1509459871 + }, + "origin": "lzip", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:lzip" + ] + }, + "libffi-dev": { + "versions": { + "3.2.1-r4": 1509458692 + }, + "origin": "libffi", + "dependencies": [ + "libffi=3.2.1-r4", + "pkgconfig" + ], + "provides": [ + "pc:libffi=3.2.1" + ] + }, + "perl-test-warn-doc": { + "versions": { + "0.32-r0": 1509470537 + }, + "origin": "perl-test-warn" + }, + "py-gunicorn": { + "versions": { + "19.7.1-r1": 1509493758 + }, + "origin": "py-gunicorn" + }, + "xf86-video-s3virge": { + "versions": { + "1.10.7-r1": 1510074190 + }, + "origin": "xf86-video-s3virge", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "skytraq-datalogger": { + "versions": { + "0.5.1-r1": 1509474459 + }, + "origin": "skytraq-datalogger", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ], + "provides": [ + "cmd:skytraq-datalogger" + ] + }, + "weechat-python": { + "versions": { + "1.9.1-r1": 1509530223 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libc.musl-x86_64.so.1" + ] + }, + "uwsgi-zabbix": { + "versions": { + "2.0.17-r0": 1522154661 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "freeradius-ldap": { + "versions": { + "3.0.15-r4": 1556202795, + "3.0.15-r5": 1566310603 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libldap-2.4.so.2" + ], + "provides": [ + "freeradius3-ldap=3.0.15-r5", + "so:rlm_ldap.so=0" + ] + }, + "rsync-doc": { + "versions": { + "3.1.3-r0": 1521547982 + }, + "origin": "rsync" + }, + "libxkbcommon-dev": { + "versions": { + "0.7.1-r1": 1509482486 + }, + "origin": "libxkbcommon", + "dependencies": [ + "libxkbcommon=0.7.1-r1", + "pc:xcb", + "pc:xcb-xkb", + "pkgconfig" + ], + "provides": [ + "pc:xkbcommon-x11=0.7.1", + "pc:xkbcommon=0.7.1" + ] + }, + "rabbitmq-c-utils": { + "versions": { + "0.8.0-r3": 1510260051 + }, + "origin": "rabbitmq-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0", + "so:librabbitmq.so.4" + ], + "provides": [ + "cmd:amqp-consume", + "cmd:amqp-declare-queue", + "cmd:amqp-delete-queue", + "cmd:amqp-get", + "cmd:amqp-publish" + ] + }, + "perl-class-tiny": { + "versions": { + "1.006-r0": 1510845710 + }, + "origin": "perl-class-tiny" + }, + "perl-list-someutils-xs-doc": { + "versions": { + "0.55-r0": 1510564670 + }, + "origin": "perl-list-someutils-xs" + }, + "xf86-video-fbdev": { + "versions": { + "0.4.4-r5": 1510069871 + }, + "origin": "xf86-video-fbdev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "sdl2-dev": { + "versions": { + "2.0.7-r3": 1511364667, + "2.0.10-r0": 1564135866 + }, + "origin": "sdl2", + "dependencies": [ + "directfb-dev", + "pkgconfig", + "sdl2=2.0.10-r0" + ], + "provides": [ + "pc:sdl2=2.0.10", + "cmd:sdl2-config" + ] + }, + "lua5.2-pc": { + "versions": { + "1.0.0-r9": 1509480088 + }, + "origin": "lua-pc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gobject-introspection-doc": { + "versions": { + "1.54.1-r0": 1509464659 + }, + "origin": "gobject-introspection" + }, + "ttf-ubuntu-font-family": { + "versions": { + "0.83-r0": 1509491420 + }, + "origin": "ttf-ubuntu-font-family" + }, + "perl-html-mason-psgihandler-doc": { + "versions": { + "0.53-r0": 1510588956 + }, + "origin": "perl-html-mason-psgihandler" + }, + "openobex-apps": { + "versions": { + "1.7.2-r1": 1510071735 + }, + "origin": "openobex", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libopenobex.so.2" + ], + "provides": [ + "cmd:ircp", + "cmd:irobex_palm3", + "cmd:irxfer", + "cmd:obex_find", + "cmd:obex_tcp", + "cmd:obex_test" + ] + }, + "perl-mime-tools-doc": { + "versions": { + "5.509-r1": 1509469900 + }, + "origin": "perl-mime-tools" + }, + "ca-certificates-doc": { + "versions": { + "20171114-r0": 1510690408, + "20190108-r0": 1558960536 + }, + "origin": "ca-certificates" + }, + "perl-b-hooks-endofscope-doc": { + "versions": { + "0.21-r0": 1509474009 + }, + "origin": "perl-b-hooks-endofscope" + }, + "guile": { + "versions": { + "2.0.14-r0": 1509468174 + }, + "origin": "guile", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libguile-2.0.so.22" + ], + "provides": [ + "cmd:guild", + "cmd:guile", + "cmd:guile-snarf", + "cmd:guile-tools" + ] + }, + "perl-encode": { + "versions": { + "2.93-r0": 1511889458 + }, + "origin": "perl-encode", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "mtools": { + "versions": { + "4.0.18-r2": 1509462432 + }, + "origin": "mtools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:amuFormat.sh", + "cmd:lz", + "cmd:mattrib", + "cmd:mbadblocks", + "cmd:mcat", + "cmd:mcd", + "cmd:mcheck", + "cmd:mclasserase", + "cmd:mcomp", + "cmd:mcopy", + "cmd:mdel", + "cmd:mdeltree", + "cmd:mdir", + "cmd:mdu", + "cmd:mformat", + "cmd:minfo", + "cmd:mkmanifest", + "cmd:mlabel", + "cmd:mmd", + "cmd:mmount", + "cmd:mmove", + "cmd:mpartition", + "cmd:mrd", + "cmd:mren", + "cmd:mshortname", + "cmd:mshowfat", + "cmd:mtools", + "cmd:mtoolstest", + "cmd:mtype", + "cmd:mxtar", + "cmd:mzip", + "cmd:tgz", + "cmd:uz" + ] + }, + "btrfs-progs-doc": { + "versions": { + "4.13.2-r0": 1509481818 + }, + "origin": "btrfs-progs" + }, + "monit": { + "versions": { + "5.24.0-r1": 1510261491, + "5.24.0-r2": 1559744492 + }, + "origin": "monit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:monit" + ] + }, + "serf-dev": { + "versions": { + "1.3.9-r3": 1510314338 + }, + "origin": "serf", + "dependencies": [ + "pc:libcrypto", + "pc:libssl", + "pkgconfig", + "serf=1.3.9-r3" + ], + "provides": [ + "pc:serf-1=1.3.9" + ] + }, + "gtest-doc": { + "versions": { + "1.8.0-r1": 1509475786 + }, + "origin": "gtest" + }, + "py3-requests-oauthlib": { + "versions": { + "0.8.0-r1": 1509552764 + }, + "origin": "py-requests-oauthlib", + "dependencies": [ + "py3-oauthlib", + "py3-requests", + "python3" + ] + }, + "libotr": { + "versions": { + "4.1.1-r1": 1509473953 + }, + "origin": "libotr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20" + ], + "provides": [ + "so:libotr.so.5=5.1.1" + ] + }, + "xorg-server-dev": { + "versions": { + "1.19.5-r1": 1540838460 + }, + "origin": "xorg-server", + "dependencies": [ + "bigreqsproto", + "compositeproto", + "damageproto", + "fixesproto", + "libepoxy-dev", + "libxfont2-dev", + "mesa-dev", + "recordproto", + "xcmiscproto", + "pc:dri2proto>=2.8", + "pc:dri3proto>=1.0", + "pc:dri>=7.8.0", + "pc:fontsproto>=2.1.3", + "pc:glproto>=1.4.17", + "pc:inputproto>=2.3", + "pc:kbproto>=1.0.3", + "pc:pciaccess>=0.12.901", + "pc:pixman-1>=0.27.2", + "pc:presentproto>=1.0", + "pc:randrproto>=1.5.0", + "pc:renderproto>=0.11", + "pc:resourceproto>=1.2.0", + "pc:scrnsaverproto>=1.1", + "pc:videoproto", + "pc:xextproto>=7.2.99.901", + "pc:xf86driproto>=2.1.0", + "pc:xineramaproto", + "pc:xproto>=7.0.31", + "pkgconfig" + ], + "provides": [ + "pc:xorg-server=1.19.5" + ] + }, + "perl-namespace-autoclean": { + "versions": { + "0.28-r0": 1509474083 + }, + "origin": "perl-namespace-autoclean", + "dependencies": [ + "perl-b-hooks-endofscope", + "perl-namespace-clean" + ] + }, + "spice-dev": { + "versions": { + "0.14.1-r2": 1548934012 + }, + "origin": "spice", + "dependencies": [ + "spice-protocol", + "pixman-dev", + "celt051-dev", + "libxinerama-dev", + "pc:gio-2.0>=2.32", + "pc:glib-2.0>=2.32", + "pc:gobject-2.0>=2.32", + "pc:openssl", + "pc:pixman-1>=0.17.7", + "pc:spice-protocol>=0.12.14", + "pkgconfig", + "spice-server=0.14.1-r2" + ], + "provides": [ + "pc:spice-server=0.14.1" + ] + }, + "links": { + "versions": { + "2.14-r2": 1510259134 + }, + "origin": "links", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:links" + ] + }, + "py-samba": { + "versions": { + "4.7.6-r3": 1555491789 + }, + "origin": "samba", + "dependencies": [ + "py-tdb", + "so:libMESSAGING-SEND-samba4.so", + "so:libMESSAGING-samba4.so", + "so:libauth4-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-ldap-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcliauth-samba4.so", + "so:libcluster-samba4.so", + "so:libcmdline-credentials-samba4.so", + "so:libdb-glue-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libdcerpc-samba4.so", + "so:libdcerpc.so.0", + "so:libdnsserver-common-samba4.so", + "so:libdsdb-garbage-collect-tombstones-samba4.so", + "so:libevents-samba4.so", + "so:libgensec-samba4.so", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:liblibsmb-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnetif-samba4.so", + "so:libposix-eadb-samba4.so", + "so:libpyldb-util.so.1", + "so:libpytalloc-util.so.2", + "so:libpython2.7.so.1.0", + "so:libregistry-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-net-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-policy.so.0", + "so:libsamba-python-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libserver-role-samba4.so", + "so:libsmbclient-raw-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbd-conn-samba4.so", + "so:libtalloc.so.2", + "so:libtdb-wrap-samba4.so", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-reg-samba4.so", + "so:libxattr-tdb-samba4.so" + ] + }, + "gettext-libs": { + "versions": { + "0.19.8.1-r1": 1509459197 + }, + "origin": "gettext", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libunistring.so.2" + ], + "provides": [ + "so:libgettextpo.so.0=0.5.4" + ] + }, + "openbox-lang": { + "versions": { + "3.6.1-r1": 1510073886 + }, + "origin": "openbox" + }, + "harfbuzz-icu": { + "versions": { + "1.6.3-r0": 1509464873 + }, + "origin": "harfbuzz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libharfbuzz.so.0", + "so:libicuuc.so.59" + ], + "provides": [ + "so:libharfbuzz-icu.so.0=0.10600.3" + ] + }, + "nagios-plugins-nwstat": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "acf-postgresql": { + "versions": { + "0.11.0-r2": 1510076158 + }, + "origin": "acf-postgresql", + "dependencies": [ + "acf-core", + "postgresql", + "acf-db-lib", + "lua-sql-postgres" + ] + }, + "dovecot-doc": { + "versions": { + "2.2.36.3-r0": 1554102386, + "2.2.36.4-r0": 1567075097 + }, + "origin": "dovecot" + }, + "cups-dev": { + "versions": { + "2.2.10-r0": 1549287809, + "2.2.12-r0": 1566207589 + }, + "origin": "cups", + "dependencies": [ + "libgcrypt-dev", + "gnutls-dev", + "zlib-dev", + "cups-libs=2.2.12-r0" + ], + "provides": [ + "cmd:cups-config" + ] + }, + "xmlrpc-c++": { + "versions": { + "1.39.11-r0": 1509482038 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libxmlrpc.so.3", + "so:libxmlrpc_abyss.so.3", + "so:libxmlrpc_server.so.3", + "so:libxmlrpc_server_abyss.so.3", + "so:libxmlrpc_util.so.3" + ], + "provides": [ + "so:libxmlrpc++.so.8=8.39", + "so:libxmlrpc_abyss++.so.8=8.39", + "so:libxmlrpc_cpp.so.8=8.39", + "so:libxmlrpc_packetsocket.so.8=8.39", + "so:libxmlrpc_server++.so.8=8.39", + "so:libxmlrpc_server_abyss++.so.8=8.39", + "so:libxmlrpc_server_cgi++.so.8=8.39", + "so:libxmlrpc_server_pstream++.so.8=8.39", + "so:libxmlrpc_util++.so.8=8.39" + ] + }, + "gdl-dev": { + "versions": { + "3.22.0-r0": 1510074987 + }, + "origin": "gdl", + "dependencies": [ + "gdl=3.22.0-r0", + "pc:gtk+-3.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gdl-3.0=3.22.0" + ] + }, + "xfce4-panel-lang": { + "versions": { + "4.12.1-r1": 1510068174 + }, + "origin": "xfce4-panel", + "dependencies": [ + "hicolor-icon-theme" + ] + }, + "libpciaccess-dev": { + "versions": { + "0.13.5-r1": 1509466088 + }, + "origin": "libpciaccess", + "dependencies": [ + "libpciaccess=0.13.5-r1", + "pkgconfig" + ], + "provides": [ + "pc:pciaccess=0.13.5" + ] + }, + "debian-archive-keyring-doc": { + "versions": { + "2017.6-r0": 1510588269 + }, + "origin": "debian-archive-keyring" + }, + "sysstat": { + "versions": { + "11.6.0-r0": 1509491781 + }, + "origin": "sysstat", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cifsiostat", + "cmd:iostat", + "cmd:mpstat", + "cmd:pidstat", + "cmd:sadf", + "cmd:sar", + "cmd:tapestat" + ] + }, + "recordproto-doc": { + "versions": { + "1.14.2-r2": 1509466032 + }, + "origin": "recordproto" + }, + "perl-lwp-mediatypes": { + "versions": { + "6.02-r1": 1509464357 + }, + "origin": "perl-lwp-mediatypes", + "dependencies": [ + "perl" + ] + }, + "logrotate": { + "versions": { + "3.13.0-r0": 1509473542 + }, + "origin": "logrotate", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:logrotate" + ] + }, + "libcue-dev": { + "versions": { + "2.1.0-r0": 1509494074 + }, + "origin": "libcue", + "dependencies": [ + "libcue=2.1.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcue=2.1.0" + ] + }, + "perl-xml-sax": { + "versions": { + "0.99-r2": 1509475907 + }, + "origin": "perl-xml-sax", + "dependencies": [ + "perl-xml-sax-base", + "perl-xml-namespacesupport", + "/bin/sh" + ] + }, + "fribidi-dev": { + "versions": { + "0.19.7-r0": 1509480343 + }, + "origin": "fribidi", + "dependencies": [ + "fribidi=0.19.7-r0", + "pkgconfig" + ], + "provides": [ + "pc:fribidi=0.19.7" + ] + }, + "rtnppd-dbg": { + "versions": { + "1.7b-r8": 1509491626 + }, + "origin": "rtnppd" + }, + "libxklavier": { + "versions": { + "5.4-r2": 1509475735 + }, + "origin": "libxklavier", + "dependencies": [ + "xkeyboard-config", + "iso-codes", + "so:libX11.so.6", + "so:libXi.so.6", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libxkbfile.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "so:libxklavier.so.16=16.4.0" + ] + }, + "clamav-db": { + "versions": { + "0.100.3-r0": 1555508238 + }, + "origin": "clamav", + "dependencies": [ + "freshclam" + ] + }, + "gst-plugins-ugly0.10": { + "versions": { + "0.10.19-r2": 1510072192 + }, + "origin": "gst-plugins-ugly0.10", + "dependencies": [ + "so:liba52.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcdio.so.16", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-0.10.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstcdda-0.10.so.0", + "so:libgstpbutils-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgstriff-0.10.so.0", + "so:libgstrtp-0.10.so.0", + "so:libgstrtsp-0.10.so.0", + "so:libgstsdp-0.10.so.0", + "so:libgsttag-0.10.so.0", + "so:libgstvideo-0.10.so.0", + "so:libintl.so.8", + "so:libmad.so.0", + "so:libmp3lame.so.0", + "so:libmpeg2.so.0", + "so:liborc-0.4.so.0", + "so:libx264.so.148" + ] + }, + "perl-cpan-meta-check-doc": { + "versions": { + "0.013-r0": 1509468484 + }, + "origin": "perl-cpan-meta-check" + }, + "libwnck-doc": { + "versions": { + "2.31.0-r5": 1510068129 + }, + "origin": "libwnck" + }, + "aconf-mod-dns-zone": { + "versions": { + "0.6.5-r0": 1510073708 + }, + "origin": "aconf", + "dependencies": [ + "aconf" + ] + }, + "perl-package-deprecationmanager-doc": { + "versions": { + "0.17-r0": 1509493029 + }, + "origin": "perl-package-deprecationmanager" + }, + "ppp-passwordfd": { + "versions": { + "2.4.7-r5": 1509480138 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-netaddr-ip": { + "versions": { + "4.079-r1": 1509485728 + }, + "origin": "perl-netaddr-ip", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "openvswitch-monitor": { + "versions": { + "2.8.1-r2": 1512030506 + }, + "origin": "openvswitch", + "dependencies": [ + "openvswitch", + "py-twisted", + "py-twisted-web2", + "py-qt" + ] + }, + "xf86-video-amdgpu": { + "versions": { + "1.4.0-r0": 1510075100 + }, + "origin": "xf86-video-amdgpu", + "dependencies": [ + "mesa-dri-ati", + "so:libc.musl-x86_64.so.1", + "so:libdrm_amdgpu.so.1", + "so:libgbm.so.1", + "so:libudev.so.1" + ] + }, + "sdl-dev": { + "versions": { + "1.2.15-r7": 1510067895, + "1.2.15-r8": 1565787129, + "1.2.15-r9": 1574265375 + }, + "origin": "sdl", + "dependencies": [ + "libx11-dev", + "pkgconfig", + "sdl=1.2.15-r9" + ], + "provides": [ + "pc:sdl=1.2.15", + "cmd:sdl-config" + ] + }, + "userspace-rcu-dev": { + "versions": { + "0.10.0-r0": 1509479771 + }, + "origin": "userspace-rcu", + "dependencies": [ + "pkgconfig", + "userspace-rcu=0.10.0-r0" + ], + "provides": [ + "pc:liburcu-bp=0.10.0", + "pc:liburcu-cds=0.10.0", + "pc:liburcu-mb=0.10.0", + "pc:liburcu-qsbr=0.10.0", + "pc:liburcu-signal=0.10.0", + "pc:liburcu=0.10.0" + ] + }, + "freeradius-python": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310604 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "python2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "freeradius3-python=3.0.15-r5", + "so:rlm_python.so=0" + ] + }, + "qemu-system-ppc64": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-ppc64" + ] + }, + "py3-idna": { + "versions": { + "2.6-r0": 1509476380 + }, + "origin": "py-idna", + "dependencies": [ + "python3" + ] + }, + "fuse": { + "versions": { + "2.9.8-r0": 1532967982 + }, + "origin": "fuse", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfuse.so.2=2.9.8", + "so:libulockmgr.so.1=1.0.1", + "cmd:fusermount", + "cmd:mount.fuse", + "cmd:ulockmgr_server" + ] + }, + "acpica-doc": { + "versions": { + "20170303-r0": 1510068815 + }, + "origin": "acpica" + }, + "cabextract-doc": { + "versions": { + "1.9-r0": 1543333648 + }, + "origin": "cabextract" + }, + "network-extras": { + "versions": { + "1.2-r0": 1509480150 + }, + "origin": "network-extras", + "dependencies": [ + "bridge", + "bonding", + "vlan", + "wpa_supplicant", + "wireless-tools", + "ppp-atm", + "ppp-chat", + "ppp-daemon", + "ppp-l2tp", + "ppp-minconn", + "ppp-passprompt", + "ppp-passwordfd", + "ppp-pppoe", + "ppp-radius", + "ppp-winbind", + "usb-modeswitch" + ] + }, + "lua5.2-cjson": { + "versions": { + "2.1.0-r7": 1509462474 + }, + "origin": "lua-cjson", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "kbd-doc": { + "versions": { + "2.0.4-r2": 1510922531 + }, + "origin": "kbd" + }, + "xz-libs": { + "versions": { + "5.2.3-r1": 1509459906 + }, + "origin": "xz", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblzma.so.5=5.2.3" + ] + }, + "libfetch-dev": { + "versions": { + "2.33-r2": 1509490729 + }, + "origin": "libfetch" + }, + "py2-pygments": { + "versions": { + "2.2.0-r0": 1509493366 + }, + "origin": "py-pygments", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:pygmentize-2" + ] + }, + "libpcre32": { + "versions": { + "8.41-r1": 1509464278 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre32.so.0=0.0.9" + ] + }, + "font-adobe-utopia-type1": { + "versions": { + "1.0.4-r0": 1509482404 + }, + "origin": "font-adobe-utopia-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "lksctp-tools-doc": { + "versions": { + "1.0.17-r0": 1509492328 + }, + "origin": "lksctp-tools" + }, + "lua5.3-optarg": { + "versions": { + "0.2-r1": 1509462490 + }, + "origin": "lua-optarg", + "dependencies": [ + "lua5.3" + ] + }, + "fts": { + "versions": { + "1.2.7-r0": 1509481205 + }, + "origin": "fts", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfts.so.0=0.0.0" + ] + }, + "slim": { + "versions": { + "1.3.6-r8": 1510067548 + }, + "origin": "slim", + "dependencies": [ + "dbus", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXmu.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libck-connector.so.0", + "so:libdbus-1.so.3", + "so:libgcc_s.so.1", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libslim.so.1.3.6=1.3.6", + "cmd:slim" + ] + }, + "xf86-video-i740": { + "versions": { + "1.3.6-r0": 1510074898 + }, + "origin": "xf86-video-i740", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "eggdrop": { + "versions": { + "1.6.21-r2": 1509491905 + }, + "origin": "eggdrop", + "dependencies": [ + "tcl", + "so:libc.musl-x86_64.so.1", + "so:libtcl8.6.so", + "so:libz.so.1" + ], + "provides": [ + "cmd:eggdrop" + ] + }, + "lua5.3-dbi-mysql": { + "versions": { + "0.6-r1": 1511483402 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "uwsgi-sslrouter": { + "versions": { + "2.0.17-r0": 1522154659 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "nagios-plugins-users": { + "versions": { + "2.2.1-r3": 1510288498 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "open-lldp-doc": { + "versions": { + "0.9.46-r3": 1510075776 + }, + "origin": "open-lldp" + }, + "freeswitch-sounds-es-mx-maria-44100": { + "versions": { + "0-r2": 1509493970 + }, + "origin": "freeswitch-sounds-es-mx-maria-44100" + }, + "a2ps-dev": { + "versions": { + "4.14-r7": 1510365669 + }, + "origin": "a2ps" + }, + "brlaser": { + "versions": { + "3-r0": 1510075895 + }, + "origin": "brlaser", + "dependencies": [ + "cups-filters", + "so:libc.musl-x86_64.so.1", + "so:libcupsimage.so.2", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "tree": { + "versions": { + "1.7.0-r1": 1509489740 + }, + "origin": "tree", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tree" + ] + }, + "kyua": { + "versions": { + "0.13-r1": 1509459786 + }, + "origin": "kyua", + "dependencies": [ + "so:libatf-c++.so.2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:liblutok.so.3", + "so:libsqlite3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:kyua" + ] + }, + "faad2-doc": { + "versions": { + "2.7-r7": 1509480953, + "2.9.0-r0": 1571918410 + }, + "origin": "faad2" + }, + "perl-devel-symdump": { + "versions": { + "2.18-r0": 1509470433 + }, + "origin": "perl-devel-symdump" + }, + "openssh-server": { + "versions": { + "7.5_p1-r10": 1551712288 + }, + "origin": "openssh", + "dependencies": [ + "openssh-keygen", + "openssh-server-common", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libz.so.1" + ], + "provides": [ + "cmd:sshd" + ] + }, + "vanessa_logger-doc": { + "versions": { + "0.0.10-r0": 1509481340 + }, + "origin": "vanessa_logger" + }, + "lighttpd": { + "versions": { + "1.4.48-r0": 1511925915 + }, + "origin": "lighttpd", + "dependencies": [ + "/bin/sh", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libev.so.4", + "so:libfam.so.0", + "so:libldap-2.4.so.2", + "so:liblua-5.3.so.0", + "so:libpcre.so.1", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:lighttpd", + "cmd:lighttpd-angel" + ] + }, + "gtk-update-icon-cache": { + "versions": { + "2.24.31-r0": 1510066781 + }, + "origin": "gtk+2.0", + "dependencies": [ + "hicolor-icon-theme", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:gtk-update-icon-cache" + ] + }, + "lua-ldbus": { + "versions": { + "20150430-r2": 1509492578 + }, + "origin": "lua-ldbus" + }, + "libmcrypt-doc": { + "versions": { + "2.5.8-r7": 1509493845 + }, + "origin": "libmcrypt" + }, + "zsh-calendar": { + "versions": { + "5.4.2-r1": 1522503663 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "po4a": { + "versions": { + "0.51-r1": 1509459569 + }, + "origin": "po4a", + "dependencies": [ + "perl", + "gettext" + ], + "provides": [ + "cmd:msguntypot", + "cmd:po4a", + "cmd:po4a-build", + "cmd:po4a-gettextize", + "cmd:po4a-normalize", + "cmd:po4a-translate", + "cmd:po4a-updatepo", + "cmd:po4aman-display-po", + "cmd:po4apod-display-po" + ] + }, + "openldap-overlay-deref": { + "versions": { + "2.4.45-r3": 1510258134, + "2.4.46-r0": 1565073940, + "2.4.48-r0": 1566900517 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "bluez-obexd": { + "versions": { + "5.47-r3": 1510069788 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0", + "so:libicalvcal.so.2" + ] + }, + "perl-apache-session": { + "versions": { + "1.93-r0": 1509491312 + }, + "origin": "perl-apache-session" + }, + "gdb": { + "versions": { + "8.0.1-r3": 1509486145 + }, + "origin": "gdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libncursesw.so.6", + "so:libreadline.so.7", + "so:libz.so.1" + ], + "provides": [ + "cmd:gcore", + "cmd:gdb" + ] + }, + "one-context": { + "versions": { + "0.5.4-r0": 1529622755 + }, + "origin": "one-context", + "dependencies": [ + "blkid" + ] + }, + "uwsgi-geoip": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-test-harness": { + "versions": { + "3.39-r0": 1509474773 + }, + "origin": "perl-test-harness" + }, + "libressl2.6-libtls": { + "versions": { + "2.6.5-r0": 1529043884 + }, + "origin": "libressl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "so:libtls.so.16=16.0.1" + ] + }, + "git-doc": { + "versions": { + "2.15.3-r0": 1540401292, + "2.15.4-r0": 1576015189 + }, + "origin": "git" + }, + "squid-lang-el": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-convert-color": { + "versions": { + "0.11-r0": 1509489725 + }, + "origin": "perl-convert-color", + "dependencies": [ + "perl-list-utilsby", + "perl-module-pluggable" + ] + }, + "ppp-atm": { + "versions": { + "2.4.7-r5": 1509480129 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.3-discount": { + "versions": { + "1.2.10.1-r4": 1509479789 + }, + "origin": "lua-discount", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "quazip-dev": { + "versions": { + "0.7.3-r0": 1510076333 + }, + "origin": "quazip", + "dependencies": [ + "quazip=0.7.3-r0" + ] + }, + "gsm": { + "versions": { + "1.0.16-r0": 1509473212 + }, + "origin": "gsm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgsm.so.1=1.0.12" + ] + }, + "ruby-google-protobuf": { + "versions": { + "3.4.1-r1": 1510846093 + }, + "origin": "protobuf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.4" + ] + }, + "pstree": { + "versions": { + "2.39-r0": 1509493762 + }, + "origin": "pstree", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pstree" + ] + }, + "perl-test-harness-utils": { + "versions": { + "3.39-r0": 1509474772 + }, + "origin": "perl-test-harness", + "dependencies": [ + "perl-test-harness", + "perl" + ], + "provides": [ + "cmd:prove" + ] + }, + "ruby-irb": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ], + "provides": [ + "cmd:irb" + ] + }, + "geoip": { + "versions": { + "1.6.11-r0": 1509474613 + }, + "origin": "geoip", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libGeoIP.so.1=1.6.11", + "cmd:geoiplookup", + "cmd:geoiplookup6" + ] + }, + "libisofs": { + "versions": { + "1.4.8-r0": 1509462207 + }, + "origin": "libisofs", + "dependencies": [ + "so:libacl.so.1", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libisofs.so.6=6.84.0" + ] + }, + "py-gnome-gconf": { + "versions": { + "2.28.1-r5": 1510933055 + }, + "origin": "py-gnome", + "dependencies": [ + "gconf", + "py-gtk", + "so:libc.musl-x86_64.so.1", + "so:libgconf-2.so.4", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpython2.7.so.1.0" + ] + }, + "cups-filters": { + "versions": { + "1.17.9-r0": 1510075884 + }, + "origin": "cups-filters", + "dependencies": [ + "poppler-utils", + "bc", + "ttf-freefont", + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-glib.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libcupsfilters.so.1", + "so:libcupsimage.so.2", + "so:libfontconfig.so.1", + "so:libfontembed.so.1", + "so:libgcc_s.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libijs-0.35.so", + "so:liblcms2.so.2", + "so:libpoppler.so.67", + "so:libqpdf.so.18", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:cups-browsed", + "cmd:driverless", + "cmd:foomatic-rip", + "cmd:ttfread" + ] + }, + "perl-crypt-x509-doc": { + "versions": { + "0.51-r0": 1509480694 + }, + "origin": "perl-crypt-x509" + }, + "libtirpc-dev": { + "versions": { + "1.0.1-r2": 1509469767 + }, + "origin": "libtirpc", + "dependencies": [ + "krb5-dev", + "bsd-compat-headers", + "libtirpc=1.0.1-r2", + "pkgconfig" + ], + "provides": [ + "pc:libtirpc=1.0.1" + ] + }, + "alpine-baselayout": { + "versions": { + "3.0.5-r2": 1510075862 + }, + "origin": "alpine-baselayout", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mkmntdirs" + ] + }, + "rdesktop": { + "versions": { + "1.8.3-r3": 1510258584 + }, + "origin": "rdesktop", + "dependencies": [ + "so:libX11.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgssglue.so.1", + "so:libsamplerate.so.0", + "so:libssl.so.44" + ], + "provides": [ + "cmd:rdesktop" + ] + }, + "snmptt": { + "versions": { + "1.3-r7": 1509492310 + }, + "origin": "snmptt", + "dependencies": [ + "perl", + "perl-config-inifiles", + "perl-list-moreutils", + "/bin/sh" + ], + "provides": [ + "cmd:snmptt", + "cmd:snmpttconvert", + "cmd:snmpttconvertmib", + "cmd:snmptthandler" + ] + }, + "paxmark": { + "versions": { + "0.11-r0": 1509457003 + }, + "origin": "paxmark", + "dependencies": [ + "attr" + ], + "provides": [ + "cmd:paxmark", + "cmd:paxmark.sh" + ] + }, + "vblade": { + "versions": { + "23-r0": 1509493239 + }, + "origin": "vblade", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:vblade", + "cmd:vbladed" + ] + }, + "tcpdump-doc": { + "versions": { + "4.9.2-r1": 1510260551 + }, + "origin": "tcpdump" + }, + "nss-pam-ldapd": { + "versions": { + "0.9.8-r0": 1509483509 + }, + "origin": "nss-pam-ldapd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2", + "so:libpam.so.0" + ], + "provides": [ + "cmd:nslcd" + ] + }, + "xen-bridge": { + "versions": { + "4.9.4-r0": 1551280495, + "4.9.4-r1": 1558103152 + }, + "origin": "xen", + "dependencies": [ + "dnsmasq" + ] + }, + "ansible-doc": { + "versions": { + "2.4.6.0-r0": 1538034317, + "2.4.6.0-r1": 1568287983 + }, + "origin": "ansible" + }, + "nghttp2-dev": { + "versions": { + "1.28.0-r0": 1511922922, + "1.39.2-r0": 1568186892 + }, + "origin": "nghttp2", + "dependencies": [ + "nghttp2-libs=1.39.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnghttp2=1.39.2" + ] + }, + "lua5.1-sql-sqlite3": { + "versions": { + "2.3.5-r1": 1509488829 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "xfce4-settings-lang": { + "versions": { + "4.12.1-r0": 1510303245 + }, + "origin": "xfce4-settings" + }, + "py2-lockfile": { + "versions": { + "0.12.2-r0": 1511197209 + }, + "origin": "py-lockfile", + "dependencies": [ + "python2" + ] + }, + "mini_httpd": { + "versions": { + "1.27-r1": 1510260671 + }, + "origin": "mini_httpd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:mini_htpasswd", + "cmd:mini_httpd" + ] + }, + "libnftnl-dev": { + "versions": { + "1.0.8-r1": 1509909171 + }, + "origin": "libnftnl", + "dependencies": [ + "libmnl-dev", + "libnftnl-libs=1.0.8-r1", + "pkgconfig" + ], + "provides": [ + "pc:libnftnl=1.0.8" + ] + }, + "git-svn": { + "versions": { + "2.15.3-r0": 1540401293, + "2.15.4-r0": 1576015190 + }, + "origin": "git", + "dependencies": [ + "perl", + "perl-git-svn=2.15.4-r0", + "perl-subversion", + "perl-term-readkey", + "so:libc.musl-x86_64.so.1", + "so:libpcre2-8.so.0", + "so:libz.so.1" + ] + }, + "py-yaml": { + "versions": { + "3.12-r1": 1509485807 + }, + "origin": "py-yaml" + }, + "squid-lang-ko": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-test-pod-coverage": { + "versions": { + "1.10-r0": 1509470445 + }, + "origin": "perl-test-pod-coverage", + "dependencies": [ + "perl", + "perl-pod-coverage", + "perl-test-pod", + "perl-devel-symdump" + ] + }, + "libsndfile": { + "versions": { + "1.0.28-r4": 1548491634, + "1.0.28-r5": 1563345857 + }, + "origin": "libsndfile", + "dependencies": [ + "so:libFLAC.so.8", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2" + ], + "provides": [ + "so:libsndfile.so.1=1.0.28", + "cmd:sndfile-cmp", + "cmd:sndfile-concat", + "cmd:sndfile-convert", + "cmd:sndfile-deinterleave", + "cmd:sndfile-info", + "cmd:sndfile-interleave", + "cmd:sndfile-metadata-get", + "cmd:sndfile-metadata-set", + "cmd:sndfile-play", + "cmd:sndfile-salvage" + ] + }, + "setxkbmap-doc": { + "versions": { + "1.3.1-r0": 1509496553 + }, + "origin": "setxkbmap" + }, + "mrtg-doc": { + "versions": { + "2.17.4-r4": 1509482696 + }, + "origin": "mrtg" + }, + "openldap-overlay-translucent": { + "versions": { + "2.4.45-r3": 1510258135, + "2.4.46-r0": 1565073941, + "2.4.48-r0": 1566900518 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "iwlwifi-5000-ucode": { + "versions": { + "8.83.5.1-r0": 1509494266 + }, + "origin": "iwlwifi-5000-ucode" + }, + "xf86-video-ark-doc": { + "versions": { + "0.7.5-r7": 1510074926 + }, + "origin": "xf86-video-ark" + }, + "tinc": { + "versions": { + "1.0.35-r0": 1549268950 + }, + "origin": "tinc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:liblzo2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:tincd" + ] + }, + "dillo": { + "versions": { + "3.0.5-r4": 1510259043 + }, + "origin": "dillo", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libfltk.so.1.3", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:dillo", + "cmd:dillo-install-hyphenation", + "cmd:dpid", + "cmd:dpidc" + ] + }, + "ez-ipupdate": { + "versions": { + "3.0.10-r9": 1509490827 + }, + "origin": "ez-ipupdate", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ez-ipupdate" + ] + }, + "acf-dansguardian": { + "versions": { + "0.8.0-r2": 1510070498 + }, + "origin": "acf-dansguardian", + "dependencies": [ + "acf-core", + "dansguardian" + ] + }, + "apache2": { + "versions": { + "2.4.39-r0": 1554306882, + "2.4.41-r0": 1566292329 + }, + "origin": "apache2", + "dependencies": [ + "/bin/sh", + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:fcgistarter", + "cmd:httpd", + "cmd:suexec" + ] + }, + "uwsgi-pty": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "librsvg-dev": { + "versions": { + "2.40.19-r0": 1510066817 + }, + "origin": "librsvg", + "dependencies": [ + "glib-dev", + "gtk+2.0-dev", + "cairo-dev", + "libcroco-dev", + "libgsf-dev", + "librsvg=2.40.19-r0", + "pc:cairo", + "pc:gdk-pixbuf-2.0", + "pc:gio-2.0", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:librsvg-2.0=2.40.19" + ] + }, + "gstreamer-lang": { + "versions": { + "1.12.3-r0": 1509470920 + }, + "origin": "gstreamer" + }, + "libpurple-xmpp": { + "versions": { + "2.12.0-r2": 1510069755 + }, + "origin": "pidgin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libidn.so.11", + "so:libintl.so.8", + "so:libsasl2.so.3", + "so:libxml2.so.2" + ], + "provides": [ + "so:libjabber.so.0=0.0.0", + "so:libxmpp.so=0" + ] + }, + "irssi-proxy": { + "versions": { + "1.0.6-r0": 1519052408, + "1.0.8-r0": 1562236916 + }, + "origin": "irssi", + "dependencies": [ + "irssi", + "so:libc.musl-x86_64.so.1" + ] + }, + "xfburn-doc": { + "versions": { + "0.5.4-r0": 1510074793 + }, + "origin": "xfburn" + }, + "pgpool-dev": { + "versions": { + "3.7.0-r0": 1511650825 + }, + "origin": "pgpool", + "dependencies": [ + "pgpool=3.7.0-r0" + ] + }, + "perl-http-message-doc": { + "versions": { + "6.13-r0": 1511871374 + }, + "origin": "perl-http-message" + }, + "asciidoctor": { + "versions": { + "1.5.6.1-r0": 1509488588 + }, + "origin": "asciidoctor", + "dependencies": [ + "ruby" + ] + }, + "lua5.3-libs": { + "versions": { + "5.3.5-r2": 1557163184 + }, + "origin": "lua5.3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblua-5.3.so.0=0.0.0" + ] + }, + "fontsproto": { + "versions": { + "2.1.3-r2": 1509470393 + }, + "origin": "fontsproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:fontsproto=2.1.3" + ] + }, + "scrnsaverproto": { + "versions": { + "1.2.2-r2": 1509473902 + }, + "origin": "scrnsaverproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:scrnsaverproto=1.2.2" + ] + }, + "lua5.3-pc": { + "versions": { + "1.0.0-r9": 1509480091 + }, + "origin": "lua-pc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-pam": { + "versions": { + "1.3.0-r0": 1509473198 + }, + "origin": "linux-pam", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpam.so.0=0.84.2", + "so:libpam_misc.so.0=0.82.1", + "so:libpamc.so.0=0.82.1", + "cmd:mkhomedir_helper", + "cmd:pam_tally", + "cmd:pam_tally2", + "cmd:pam_timestamp_check", + "cmd:unix_chkpwd", + "cmd:unix_update" + ] + }, + "xwininfo-doc": { + "versions": { + "1.1.3-r0": 1509484045 + }, + "origin": "xwininfo" + }, + "uwsgi-cgi": { + "versions": { + "2.0.17-r0": 1522154653 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-test-output-doc": { + "versions": { + "1.031-r0": 1509485673 + }, + "origin": "perl-test-output" + }, + "mcpp-libs": { + "versions": { + "2.7.2-r1": 1509473730 + }, + "origin": "mcpp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmcpp.so.0=0.3.0" + ] + }, + "perl-params-util-doc": { + "versions": { + "1.07-r5": 1509473959 + }, + "origin": "perl-params-util" + }, + "py3-urllib3": { + "versions": { + "1.22-r0": 1509483033 + }, + "origin": "py-urllib3", + "dependencies": [ + "python3" + ] + }, + "beep": { + "versions": { + "1.3-r2": 1509481228 + }, + "origin": "beep", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:beep" + ] + }, + "xfburn-lang": { + "versions": { + "0.5.4-r0": 1510074794 + }, + "origin": "xfburn", + "dependencies": [ + "desktop-file-utils", + "hicolor-icon-theme" + ] + }, + "zfs": { + "versions": { + "0.7.3-r0": 1509490075 + }, + "origin": "zfs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libnvpair.so.1", + "so:libuuid.so.1", + "so:libuutil.so.1", + "so:libzfs.so.2", + "so:libzfs_core.so.1", + "so:libzpool.so.2" + ], + "provides": [ + "cmd:fsck.zfs", + "cmd:mount.zfs", + "cmd:raidz_test", + "cmd:zdb", + "cmd:zed", + "cmd:zfs", + "cmd:zgenhostid", + "cmd:zhack", + "cmd:zinject", + "cmd:zpios", + "cmd:zpool", + "cmd:zstreamdump", + "cmd:ztest" + ] + }, + "libxpm-dev": { + "versions": { + "3.5.12-r0": 1509469832 + }, + "origin": "libxpm", + "dependencies": [ + "libxpm=3.5.12-r0", + "pc:x11", + "pkgconfig" + ], + "provides": [ + "pc:xpm=3.5.12" + ] + }, + "beep-doc": { + "versions": { + "1.3-r2": 1509481227 + }, + "origin": "beep" + }, + "postgresql-plpython3": { + "versions": { + "10.7-r0": 1554274199, + "10.8-r0": 1557668016, + "10.9-r0": 1562225614, + "10.10-r0": 1565683439 + }, + "origin": "postgresql", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "kamailio-geoip2": { + "versions": { + "5.0.7-r0": 1532960877 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libmaxminddb.so.0" + ] + }, + "pcre2-dev": { + "versions": { + "10.30-r0": 1509461772 + }, + "origin": "pcre2", + "dependencies": [ + "libedit-dev", + "zlib-dev", + "libpcre2-16=10.30-r0", + "libpcre2-32=10.30-r0", + "pcre2=10.30-r0", + "pkgconfig" + ], + "provides": [ + "pc:libpcre2-16=10.30", + "pc:libpcre2-32=10.30", + "pc:libpcre2-8=10.30", + "pc:libpcre2-posix=10.30", + "cmd:pcre2-config" + ] + }, + "perl-git": { + "versions": { + "2.15.3-r0": 1540401292, + "2.15.4-r0": 1576015190 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0", + "perl-error" + ] + }, + "sdl_image": { + "versions": { + "1.2.12-r3": 1510067914, + "1.2.12-r4": 1574265385 + }, + "origin": "sdl_image", + "dependencies": [ + "so:libSDL-1.2.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL_image-1.2.so.0=0.8.4" + ] + }, + "perl-cgi-session": { + "versions": { + "4.48-r0": 1509475912 + }, + "origin": "perl-cgi-session", + "dependencies": [ + "perl" + ] + }, + "lua5.3-dbi-postgresql": { + "versions": { + "0.6-r1": 1511483402 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "bzip2-doc": { + "versions": { + "1.0.6-r6": 1509456387, + "1.0.6-r7": 1562268494 + }, + "origin": "bzip2" + }, + "glade3": { + "versions": { + "3.8.5-r4": 1510067964 + }, + "origin": "glade3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgladeui-1.so.11=11.2.0", + "cmd:glade-3" + ] + }, + "perl-extutils-pkgconfig-doc": { + "versions": { + "1.16-r1": 1509491043 + }, + "origin": "perl-extutils-pkgconfig" + }, + "lua5.1-hashids": { + "versions": { + "1.0.6-r1": 1509480766 + }, + "origin": "lua-hashids", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "openldap-overlay-seqmod": { + "versions": { + "2.4.45-r3": 1510258135, + "2.4.46-r0": 1565073941, + "2.4.48-r0": 1566900518 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldap_r-2.4.so.2" + ] + }, + "libnjb-examples": { + "versions": { + "2.2.7-r4": 1509481679 + }, + "origin": "libnjb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libnjb.so.5", + "so:libz.so.1" + ], + "provides": [ + "cmd:njb-cursesplay", + "cmd:njb-delfile", + "cmd:njb-deltr", + "cmd:njb-dumpeax", + "cmd:njb-dumptime", + "cmd:njb-files", + "cmd:njb-fwupgrade", + "cmd:njb-getfile", + "cmd:njb-getowner", + "cmd:njb-gettr", + "cmd:njb-getusage", + "cmd:njb-handshake", + "cmd:njb-pl", + "cmd:njb-play", + "cmd:njb-playlists", + "cmd:njb-sendfile", + "cmd:njb-sendtr", + "cmd:njb-setowner", + "cmd:njb-setpbm", + "cmd:njb-settime", + "cmd:njb-tagtr", + "cmd:njb-tracks" + ] + }, + "libdvdcss-doc": { + "versions": { + "1.4.0-r1": 1509480289 + }, + "origin": "libdvdcss" + }, + "keyutils-doc": { + "versions": { + "1.5.10-r0": 1509469694 + }, + "origin": "keyutils" + }, + "xfwm4-lang": { + "versions": { + "4.12.4-r1": 1510074449 + }, + "origin": "xfwm4", + "dependencies": [ + "hicolor-icon-theme" + ] + }, + "bluez-btmon": { + "versions": { + "5.47-r3": 1510069787 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ], + "provides": [ + "cmd:btmon" + ] + }, + "libnftnl-libs": { + "versions": { + "1.0.8-r1": 1509909171 + }, + "origin": "libnftnl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjansson.so.4", + "so:libmnl.so.0" + ], + "provides": [ + "so:libnftnl.so.7=7.0.0" + ] + }, + "py3-future": { + "versions": { + "0.15.2-r4": 1509491794 + }, + "origin": "py-future", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:futurize", + "cmd:pasteurize" + ] + }, + "sc": { + "versions": { + "7.16-r4": 1509495486 + }, + "origin": "sc", + "dependencies": [ + "ncurses", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:psc", + "cmd:sc", + "cmd:scqref" + ] + }, + "perl-mime-types-doc": { + "versions": { + "2.14-r0": 1511188636 + }, + "origin": "perl-mime-types" + }, + "libproxy-bin": { + "versions": { + "0.4.15-r0": 1509468257 + }, + "origin": "libproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libproxy.so.1" + ], + "provides": [ + "cmd:proxy" + ] + }, + "libfprint-dev": { + "versions": { + "0.7.0-r0": 1509486009 + }, + "origin": "libfprint", + "dependencies": [ + "libusb-dev", + "libfprint=0.7.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libfprint=0.7.0" + ] + }, + "py2-olefile": { + "versions": { + "0.43-r2": 1509489936 + }, + "origin": "py-olefile" + }, + "lua5.2-sql-sqlite3": { + "versions": { + "2.3.5-r1": 1509488831 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "cksfv-doc": { + "versions": { + "1.3.14-r4": 1509486269 + }, + "origin": "cksfv" + }, + "perl-text-soundex-doc": { + "versions": { + "3.05-r0": 1509470836 + }, + "origin": "perl-text-soundex" + }, + "libcdio++": { + "versions": { + "0.94-r0": 1509473627 + }, + "origin": "libcdio", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcdio.so.16", + "so:libiso9660.so.10", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcdio++.so.0=0.0.2", + "so:libiso9660++.so.0=0.0.0" + ] + }, + "perl-list-allutils-doc": { + "versions": { + "0.14-r0": 1509493247 + }, + "origin": "perl-list-allutils" + }, + "py-django-phonenumber-field": { + "versions": { + "1.3.0-r0": 1509490141 + }, + "origin": "py-django-phonenumber-field", + "dependencies": [ + "py-babel", + "py-django", + "py-phonenumbers" + ] + }, + "libquvi-scripts": { + "versions": { + "0.9.20131130-r0": 1509491410 + }, + "origin": "libquvi-scripts" + }, + "py2-cffi": { + "versions": { + "1.10.0-r0": 1509476374 + }, + "origin": "py-cffi", + "dependencies": [ + "py2-cparser", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libpython2.7.so.1.0" + ] + }, + "py-jwt-cli": { + "versions": { + "1.5.0-r1": 1509552755 + }, + "origin": "py-jwt", + "dependencies": [ + "py3-jwt" + ], + "provides": [ + "cmd:pyjwt" + ] + }, + "varnish-geoip": { + "versions": { + "5.2.1-r0": 1511265090 + }, + "origin": "varnish", + "dependencies": [ + "libmaxminddb-dev" + ] + }, + "pjproject": { + "versions": { + "2.5.5-r3": 1510258881 + }, + "origin": "pjproject", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libgsm.so.1", + "so:libsamplerate.so.0", + "so:libspeex.so.1", + "so:libspeexdsp.so.1", + "so:libssl.so.44", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libg7221codec.so.2=2", + "so:libilbccodec.so.2=2", + "so:libpj.so.2=2", + "so:libpjlib-util.so.2=2", + "so:libpjmedia-audiodev.so.2=2", + "so:libpjmedia-codec.so.2=2", + "so:libpjmedia-videodev.so.2=2", + "so:libpjmedia.so.2=2", + "so:libpjnath.so.2=2", + "so:libpjsip-simple.so.2=2", + "so:libpjsip-ua.so.2=2", + "so:libpjsip.so.2=2", + "so:libpjsua.so.2=2", + "so:libpjsua2.so.2=2", + "so:libyuv.so.2=2" + ] + }, + "znc-modtcl": { + "versions": { + "1.7.1-r0": 1531900839, + "1.7.1-r1": 1565877330 + }, + "origin": "znc", + "dependencies": [ + "znc", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libtcl8.6.so" + ] + }, + "rdesktop-doc": { + "versions": { + "1.8.3-r3": 1510258584 + }, + "origin": "rdesktop" + }, + "libuv-dev": { + "versions": { + "1.17.0-r0": 1511652082 + }, + "origin": "libuv", + "dependencies": [ + "libuv=1.17.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libuv=1.17.0" + ] + }, + "galculator-doc": { + "versions": { + "2.1.4-r0": 1510075351 + }, + "origin": "galculator" + }, + "vte": { + "versions": { + "0.28.2-r13": 1510071903 + }, + "origin": "vte", + "dependencies": [ + "so:libX11.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0" + ], + "provides": [ + "so:libvte.so.9=9.2800.2", + "cmd:vte" + ] + }, + "libwebsockets-test": { + "versions": { + "2.4.0-r1": 1510258054 + }, + "origin": "libwebsockets", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:libwebsockets-test-client", + "cmd:libwebsockets-test-echo", + "cmd:libwebsockets-test-fraggle", + "cmd:libwebsockets-test-fuzxy", + "cmd:libwebsockets-test-ping", + "cmd:libwebsockets-test-server", + "cmd:libwebsockets-test-server-extpoll" + ] + }, + "lynx-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503664 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "strongswan": { + "versions": { + "5.6.3-r2": 1539005310 + }, + "origin": "strongswan", + "dependencies": [ + "iproute2", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libgmp.so.10", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libcharon.so.0=0.0.0", + "so:libradius.so.0=0.0.0", + "so:libsimaka.so.0=0.0.0", + "so:libstrongswan.so.0=0.0.0", + "so:libtls.so.0=0.0.0", + "so:libvici.so.0=0.0.0", + "cmd:charon-cmd", + "cmd:ipsec", + "cmd:pki", + "cmd:swanctl" + ] + }, + "perl-test-sharedfork": { + "versions": { + "0.35-r0": 1509481780 + }, + "origin": "perl-test-sharedfork", + "dependencies": [ + "perl", + "perl-test-requires" + ] + }, + "py-lockfile": { + "versions": { + "0.12.2-r0": 1511197215 + }, + "origin": "py-lockfile" + }, + "yaml-dev": { + "versions": { + "0.1.7-r0": 1509470097 + }, + "origin": "yaml", + "dependencies": [ + "pkgconfig", + "yaml=0.1.7-r0" + ], + "provides": [ + "pc:yaml-0.1=0.1.7" + ] + }, + "perl-module-runtime": { + "versions": { + "0.014-r1": 1509470564 + }, + "origin": "perl-module-runtime", + "dependencies": [ + "perl-params-classify", + "perl-module-build" + ] + }, + "mg-doc": { + "versions": { + "20171014-r0": 1509494079 + }, + "origin": "mg" + }, + "libavc1394": { + "versions": { + "0.5.4-r1": 1509489638 + }, + "origin": "libavc1394", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libraw1394.so.11" + ], + "provides": [ + "so:libavc1394.so.0=0.3.0", + "so:librom1394.so.0=0.3.0", + "cmd:dvcont", + "cmd:mkrfc2734", + "cmd:panelctl" + ] + }, + "libxrender": { + "versions": { + "0.9.10-r2": 1509462095 + }, + "origin": "libxrender", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXrender.so.1=1.3.0" + ] + }, + "libpcre16": { + "versions": { + "8.41-r1": 1509464278 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre16.so.0=0.2.9" + ] + }, + "qemu-system-i386": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-i386" + ] + }, + "lua5.2-lzmq": { + "versions": { + "0.4.4-r0": 1509475920 + }, + "origin": "lua-lzmq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5", + "so:libzmq.so.5" + ] + }, + "perl-scalar-list-utils": { + "versions": { + "1.49-r0": 1509485608 + }, + "origin": "perl-scalar-list-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-funcsigs": { + "versions": { + "1.0.2-r1": 1509485905 + }, + "origin": "py-funcsigs", + "dependencies": [ + "python2" + ] + }, + "re2c-doc": { + "versions": { + "1.0.2-r0": 1509475772 + }, + "origin": "re2c" + }, + "libfastjson": { + "versions": { + "0.99.7-r0": 1509486267 + }, + "origin": "libfastjson", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfastjson.so.4=4.2.0" + ] + }, + "freeradius-sql": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310603 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "freeradius3-sql=3.0.15-r5", + "so:rlm_sql.so=0", + "so:rlm_sql_null.so=0", + "so:rlm_sqlcounter.so=0", + "so:rlm_sqlippool.so=0" + ] + }, + "upower-doc": { + "versions": { + "0.99.6-r0": 1510068213 + }, + "origin": "upower" + }, + "rsyslog-snmp": { + "versions": { + "8.31.0-r0": 1512031981, + "8.31.0-r1": 1571767017 + }, + "origin": "rsyslog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.30" + ] + }, + "poppler-glib": { + "versions": { + "0.56.0-r0": 1509465067, + "0.56.0-r1": 1569334548 + }, + "origin": "poppler", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfreetype.so.6", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpoppler.so.67", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libpoppler-glib.so.8=8.9.0" + ] + }, + "garcon-lang": { + "versions": { + "0.6.1-r1": 1510068108 + }, + "origin": "garcon" + }, + "py2-ipaddress": { + "versions": { + "1.0.18-r1": 1509480833 + }, + "origin": "py-ipaddress", + "dependencies": [ + "python2" + ] + }, + "wireless-tools-dev": { + "versions": { + "30_pre9-r0": 1509479802 + }, + "origin": "wireless-tools" + }, + "pm-utils": { + "versions": { + "1.4.1-r0": 1509491020 + }, + "origin": "pm-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:on_ac_power", + "cmd:pm-is-supported", + "cmd:pm-powersave" + ] + }, + "gnutls-utils": { + "versions": { + "3.6.1-r0": 1509465348 + }, + "origin": "gnutls", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30", + "so:libtasn1.so.6" + ], + "provides": [ + "cmd:certtool", + "cmd:gnutls-cli", + "cmd:gnutls-cli-debug", + "cmd:gnutls-serv", + "cmd:ocsptool", + "cmd:p11tool", + "cmd:psktool", + "cmd:srptool" + ] + }, + "ldns-dev": { + "versions": { + "1.6.17-r6": 1510258933 + }, + "origin": "ldns", + "dependencies": [ + "libressl-dev", + "ldns=1.6.17-r6", + "pc:libcrypto", + "pkgconfig" + ], + "provides": [ + "pc:libldns=1.6.17", + "cmd:ldns-config" + ] + }, + "py-django-treebeard": { + "versions": { + "4.1.2-r0": 1509494272 + }, + "origin": "py-django-treebeard", + "dependencies": [ + "py-django" + ] + }, + "py-oauth2client": { + "versions": { + "4.1.2-r1": 1509551799 + }, + "origin": "py-oauth2client", + "dependencies": [ + "py-asn1", + "py-httplib2", + "py-asn1-modules", + "py-rsa", + "py-six" + ] + }, + "libwebp": { + "versions": { + "0.6.0-r1": 1509473039 + }, + "origin": "libwebp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libwebp.so.7=7.0.0", + "so:libwebpdecoder.so.3=3.0.0", + "so:libwebpdemux.so.2=2.0.2", + "so:libwebpmux.so.3=3.0.0" + ] + }, + "qemu-mips64el": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mips64el" + ] + }, + "opensmtpd": { + "versions": { + "6.0.2p1-r7": 1510259021 + }, + "origin": "opensmtpd", + "dependencies": [ + "!postfix", + "/bin/sh", + "so:libasr.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libdb-5.3.so", + "so:libevent-2.1.so.6", + "so:libfts.so.0", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:smtpctl", + "cmd:smtpd" + ] + }, + "sdl": { + "versions": { + "1.2.15-r7": 1510067903, + "1.2.15-r8": 1565787130, + "1.2.15-r9": 1574265376 + }, + "origin": "sdl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL-1.2.so.0=0.11.4" + ] + }, + "fltk-doc": { + "versions": { + "1.3.4-r0": 1510072236 + }, + "origin": "fltk" + }, + "dconf-dev": { + "versions": { + "0.26.0-r1": 1509492217 + }, + "origin": "dconf", + "dependencies": [ + "dconf=0.26.0-r1", + "pc:gio-2.0>=2.25.7", + "pkgconfig" + ], + "provides": [ + "pc:dconf=0.26.0" + ] + }, + "newt": { + "versions": { + "0.52.20-r0": 1509476146 + }, + "origin": "newt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0", + "so:libslang.so.2" + ], + "provides": [ + "so:libnewt.so.0.52=0.52.20", + "cmd:whiptail" + ] + }, + "spice-webdavd": { + "versions": { + "2.2-r0": 1509492233 + }, + "origin": "phodav", + "dependencies": [ + "so:libavahi-gobject.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "cmd:spice-webdavd" + ] + }, + "dmenu-doc": { + "versions": { + "4.7-r0": 1509494018 + }, + "origin": "dmenu" + }, + "libmaxminddb-dev": { + "versions": { + "1.3.1-r0": 1511893707 + }, + "origin": "libmaxminddb", + "dependencies": [ + "libmaxminddb=1.3.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmaxminddb=1.3.1" + ] + }, + "liblastfm-dev": { + "versions": { + "1.0.9-r1": 1510074386 + }, + "origin": "liblastfm", + "dependencies": [ + "qt-dev", + "libsamplerate-dev", + "fftw-dev", + "liblastfm=1.0.9-r1" + ] + }, + "poppler-utils": { + "versions": { + "0.56.0-r0": 1509465066, + "0.56.0-r1": 1569334548 + }, + "origin": "poppler", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfreetype.so.6", + "so:liblcms2.so.2", + "so:libpoppler.so.67", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:pdfdetach", + "cmd:pdffonts", + "cmd:pdfimages", + "cmd:pdfinfo", + "cmd:pdfseparate", + "cmd:pdftocairo", + "cmd:pdftohtml", + "cmd:pdftoppm", + "cmd:pdftops", + "cmd:pdftotext", + "cmd:pdfunite" + ] + }, + "liblogging-dev": { + "versions": { + "1.0.6-r0": 1509517790 + }, + "origin": "liblogging", + "dependencies": [ + "liblogging=1.0.6-r0", + "pkgconfig" + ], + "provides": [ + "pc:liblogging-stdlog=1.0.6" + ] + }, + "perl-sub-info": { + "versions": { + "0.002-r0": 1509481620 + }, + "origin": "perl-sub-info", + "dependencies": [ + "perl-importer", + "perl-test-simple" + ] + }, + "squid-lang-ar": { + "versions": { + "3.5.27-r0": 1519824030, + "3.5.27-r1": 1562865666 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-log-any": { + "versions": { + "1.049-r0": 1509470557 + }, + "origin": "perl-log-any" + }, + "audit": { + "versions": { + "2.7.7-r1": 1509491479 + }, + "origin": "audit", + "dependencies": [ + "so:libaudit.so.1", + "so:libauparse.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0" + ], + "provides": [ + "cmd:audisp-remote", + "cmd:audispd", + "cmd:auditctl", + "cmd:auditd", + "cmd:augenrules", + "cmd:aulast", + "cmd:aulastlog", + "cmd:aureport", + "cmd:ausearch", + "cmd:ausyscall", + "cmd:autrace", + "cmd:auvirt" + ] + }, + "py-six": { + "versions": { + "1.11.0-r0": 1509465546 + }, + "origin": "py-six" + }, + "duply-doc": { + "versions": { + "2.0.3-r0": 1510588387 + }, + "origin": "duply" + }, + "xf86-video-ast-doc": { + "versions": { + "1.1.5-r1": 1510074917 + }, + "origin": "xf86-video-ast" + }, + "xcmsdb": { + "versions": { + "1.0.5-r0": 1509494048 + }, + "origin": "xcmsdb", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xcmsdb" + ] + }, + "xf86bigfontproto-dev": { + "versions": { + "1.2.0-r5": 1509461949 + }, + "origin": "xf86bigfontproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xf86bigfontproto=1.2.0" + ] + }, + "perl-dbd-sqlite-doc": { + "versions": { + "1.54-r1": 1509477495 + }, + "origin": "perl-dbd-sqlite" + }, + "nasm": { + "versions": { + "2.13.01-r0": 1509464902 + }, + "origin": "nasm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:nasm", + "cmd:ndisasm" + ] + }, + "perl-exporter-tiny-doc": { + "versions": { + "1.000000-r0": 1509468499 + }, + "origin": "perl-exporter-tiny" + }, + "usbutils-doc": { + "versions": { + "009-r0": 1511890229 + }, + "origin": "usbutils" + }, + "lvm2-openrc": { + "versions": { + "2.02.175-r0": 1509462402 + }, + "origin": "lvm2", + "dependencies": [ + "lvm2-libs=2.02.175-r0" + ] + }, + "polkit-gnome-lang": { + "versions": { + "0.105-r0": 1510075797 + }, + "origin": "polkit-gnome" + }, + "py-munkres": { + "versions": { + "1.0.12-r0": 1509468390 + }, + "origin": "py-munkres" + }, + "lua5.2-sircbot": { + "versions": { + "0.4-r1": 1509492356 + }, + "origin": "sircbot", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gtk-engines-crux": { + "versions": { + "2.21.0-r2": 1510071722 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "altermime": { + "versions": { + "0.3.11-r0": 1509481330 + }, + "origin": "altermime", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:altermime" + ] + }, + "presentproto": { + "versions": { + "1.1-r1": 1509466249 + }, + "origin": "presentproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:presentproto=1.1" + ] + }, + "perl-net-snmp-doc": { + "versions": { + "6.0.1-r2": 1509471906 + }, + "origin": "perl-net-snmp" + }, + "espeak": { + "versions": { + "1.48.04-r1": 1509472851 + }, + "origin": "espeak", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libespeak.so.1=1.1.48", + "cmd:espeak" + ] + }, + "xfce4-notifyd-lang": { + "versions": { + "0.3.6-r0": 1510075457 + }, + "origin": "xfce4-notifyd", + "dependencies": [ + "hicolor-icon-theme" + ] + }, + "xfdesktop-lang": { + "versions": { + "4.12.4-r0": 1510074490 + }, + "origin": "xfdesktop", + "dependencies": [ + "hicolor-icon-theme" + ] + }, + "nagios-plugins-by_ssh": { + "versions": { + "2.2.1-r3": 1510288494 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "openssh-client", + "so:libc.musl-x86_64.so.1" + ] + }, + "libcap-dev": { + "versions": { + "2.25-r1": 1509459572 + }, + "origin": "libcap", + "dependencies": [ + "linux-headers", + "libcap=2.25-r1", + "pkgconfig" + ], + "provides": [ + "pc:libcap=2.25" + ] + }, + "htop-doc": { + "versions": { + "2.0.2-r0": 1509494928 + }, + "origin": "htop" + }, + "lua-posix": { + "versions": { + "33.4.0-r0": 1509468348 + }, + "origin": "lua-posix" + }, + "perl-test-fatal": { + "versions": { + "0.014-r1": 1509464416 + }, + "origin": "perl-test-fatal", + "dependencies": [ + "perl-try-tiny" + ] + }, + "libxtst-dev": { + "versions": { + "1.2.3-r1": 1509466038 + }, + "origin": "libxtst", + "dependencies": [ + "inputproto", + "libxtst=1.2.3-r1", + "pc:recordproto", + "pc:x11", + "pc:xext", + "pc:xextproto", + "pc:xi", + "pkgconfig" + ], + "provides": [ + "pc:xtst=1.2.3" + ] + }, + "freetds-doc": { + "versions": { + "1.00.44-r0": 1509480207 + }, + "origin": "freetds" + }, + "exiv2-dev": { + "versions": { + "0.25-r0": 1509475472 + }, + "origin": "exiv2", + "dependencies": [ + "expat-dev", + "zlib-dev", + "exiv2=0.25-r0", + "pkgconfig" + ], + "provides": [ + "pc:exiv2=0.25" + ] + }, + "setpriv": { + "versions": { + "2.31.1-r0": 1541506295 + }, + "origin": "util-linux", + "dependencies": [ + "findmnt", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0" + ], + "provides": [ + "cmd:setpriv" + ] + }, + "sipp": { + "versions": { + "3.3-r9": 1510261312 + }, + "origin": "sipp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libpcap.so.1", + "so:libsctp.so.1", + "so:libssl.so.44", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:sipp" + ] + }, + "libgphoto2-dev": { + "versions": { + "2.5.14-r0": 1509481938 + }, + "origin": "libgphoto2", + "dependencies": [ + "libexif-dev", + "libusb-dev", + "libgphoto2=2.5.14-r0", + "pc:libexif>=0.6.13", + "pkgconfig" + ], + "provides": [ + "pc:libgphoto2=2.5.14", + "pc:libgphoto2_port=0.12.0", + "cmd:gphoto2-config", + "cmd:gphoto2-port-config" + ] + }, + "jasper": { + "versions": { + "2.0.14-r0": 1509475517 + }, + "origin": "jasper", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjasper.so.4" + ], + "provides": [ + "cmd:imgcmp", + "cmd:imginfo", + "cmd:jasper" + ] + }, + "shared-mime-info": { + "versions": { + "1.9-r0": 1509464467 + }, + "origin": "shared-mime-info", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:update-mime-database" + ] + }, + "qemu-system-tricore": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-tricore" + ] + }, + "libid3tag-dev": { + "versions": { + "0.15.1b-r6": 1509470495 + }, + "origin": "libid3tag", + "dependencies": [ + "libid3tag=0.15.1b-r6", + "pkgconfig" + ], + "provides": [ + "pc:id3tag=0.15.1b" + ] + }, + "tarsnap": { + "versions": { + "1.0.39-r2": 1510258955 + }, + "origin": "tarsnap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libz.so.1" + ], + "provides": [ + "cmd:tarsnap", + "cmd:tarsnap-keygen", + "cmd:tarsnap-keymgmt", + "cmd:tarsnap-keyregen", + "cmd:tarsnap-recrypt" + ] + }, + "bitlbee": { + "versions": { + "3.5.1-r1": 1510259355 + }, + "origin": "bitlbee", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libssl.so.44" + ], + "provides": [ + "cmd:bitlbee" + ] + }, + "flake8": { + "versions": { + "3.4.1-r0": 1509492641 + }, + "origin": "flake8", + "dependencies": [ + "python2", + "py-setuptools", + "py-mccabe", + "py-pep8", + "pyflakes" + ], + "provides": [ + "cmd:flake8" + ] + }, + "perl-class-accessor-chained": { + "versions": { + "0.01-r0": 1509474101 + }, + "origin": "perl-class-accessor-chained", + "dependencies": [ + "perl-class-accessor" + ] + }, + "perl-file-next": { + "versions": { + "1.16-r0": 1509491910 + }, + "origin": "perl-file-next" + }, + "xmodmap-doc": { + "versions": { + "1.0.9-r2": 1509473722 + }, + "origin": "xmodmap" + }, + "lua-rrd": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5", + "so:librrd.so.4" + ] + }, + "asterisk-dbg": { + "versions": { + "15.6.1-r0": 1537795340, + "15.6.2-r0": 1568705001 + }, + "origin": "asterisk" + }, + "snownews-doc": { + "versions": { + "1.5.12-r6": 1510260845 + }, + "origin": "snownews" + }, + "pgtcl-doc": { + "versions": { + "2.1.0-r0": 1509489161 + }, + "origin": "pgtcl" + }, + "py3-yaml": { + "versions": { + "3.12-r1": 1509485806 + }, + "origin": "py-yaml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0", + "so:libyaml-0.so.2" + ] + }, + "lua5.2-sec": { + "versions": { + "0.6-r3": 1510260656 + }, + "origin": "lua-sec", + "dependencies": [ + "lua5.2-socket", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "libnetfilter_acct-dev": { + "versions": { + "1.0.3-r0": 1509480361 + }, + "origin": "libnetfilter_acct", + "dependencies": [ + "libnetfilter_acct=1.0.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_acct=1.0.3" + ] + }, + "indent-lang": { + "versions": { + "2.2.11-r1": 1509496389 + }, + "origin": "indent" + }, + "kamailio-websocket": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libunistring.so.2" + ] + }, + "tcl-doc": { + "versions": { + "8.6.7-r0": 1509460296 + }, + "origin": "tcl" + }, + "gst-plugins-bad-lang": { + "versions": { + "1.12.3-r2": 1512039893 + }, + "origin": "gst-plugins-bad" + }, + "py-gnutls": { + "versions": { + "3.1.1-r0": 1509489167 + }, + "origin": "py-gnutls" + }, + "rsync-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503664 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "dbus-x11": { + "versions": { + "1.10.24-r0": 1509465101, + "1.10.28-r0": 1560765707 + }, + "origin": "dbus", + "dependencies": [ + "dbus=1.10.28-r0", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ], + "provides": [ + "cmd:dbus-launch" + ] + }, + "kamailio-debugger": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libprint.so.1", + "so:libsrutils.so.1" + ] + }, + "minizip-dev": { + "versions": { + "1.2.11-r0": 1509495061 + }, + "origin": "minizip", + "dependencies": [ + "minizip=1.2.11-r0", + "pkgconfig" + ], + "provides": [ + "pc:minizip=1.2.11" + ] + }, + "sed-doc": { + "versions": { + "4.4-r1": 1509462307 + }, + "origin": "sed" + }, + "abiword-plugin-pdb": { + "versions": { + "3.0.2-r1": 1510073367 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "tlsdate": { + "versions": { + "0.0.13-r5": 1510259209 + }, + "origin": "tlsdate", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libevent-2.1.so.6", + "so:libssl.so.44" + ], + "provides": [ + "cmd:tlsdate", + "cmd:tlsdate-helper", + "cmd:tlsdated" + ] + }, + "sshfs-doc": { + "versions": { + "2.10-r2": 1509489736 + }, + "origin": "sshfs" + }, + "openldap-overlay-all": { + "versions": { + "2.4.45-r3": 1510258132, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "openldap-overlay-accesslog", + "openldap-overlay-auditlog", + "openldap-overlay-collect", + "openldap-overlay-constraint", + "openldap-overlay-dds", + "openldap-overlay-deref", + "openldap-overlay-dyngroup", + "openldap-overlay-dynlist", + "openldap-overlay-memberof", + "openldap-overlay-ppolicy", + "openldap-overlay-proxycache", + "openldap-overlay-refint", + "openldap-overlay-retcode", + "openldap-overlay-rwm", + "openldap-overlay-seqmod", + "openldap-overlay-sssvlv", + "openldap-overlay-syncprov", + "openldap-overlay-translucent", + "openldap-overlay-unique", + "openldap-overlay-valsort" + ] + }, + "uwsgi-router_memcached": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-cairo": { + "versions": { + "1.10.0-r0": 1509471034 + }, + "origin": "py2-cairo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libpython2.7.so.1.0" + ] + }, + "python3": { + "versions": { + "3.6.8-r0": 1555928809, + "3.6.9-r0": 1571233996, + "3.6.9-r1": 1571314640 + }, + "origin": "python3", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libexpat.so.1", + "so:libffi.so.6", + "so:libgdbm.so.4", + "so:libgdbm_compat.so.4", + "so:liblzma.so.5", + "so:libncursesw.so.6", + "so:libpanelw.so.6", + "so:libreadline.so.7", + "so:libsqlite3.so.0", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "py3-pip", + "so:libpython3.6m.so.1.0=1.0", + "so:libpython3.so=0", + "cmd:2to3", + "cmd:2to3-3.6", + "cmd:easy_install-3.6", + "cmd:pip3", + "cmd:pip3.6", + "cmd:pydoc3", + "cmd:pydoc3.6", + "cmd:python3", + "cmd:python3.6", + "cmd:python3.6m", + "cmd:pyvenv", + "cmd:pyvenv-3.6" + ] + }, + "zsh": { + "versions": { + "5.4.2-r1": 1522503664 + }, + "origin": "zsh", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:zsh", + "cmd:zsh-5.4.2" + ] + }, + "iwlwifi-1000-ucode-doc": { + "versions": { + "39.31.5.1-r0": 1509483628 + }, + "origin": "iwlwifi-1000-ucode" + }, + "mc-doc": { + "versions": { + "4.8.20-r0": 1511871582 + }, + "origin": "mc" + }, + "perl-datetime-locale": { + "versions": { + "1.17-r0": 1510855741 + }, + "origin": "perl-datetime-locale", + "dependencies": [ + "perl", + "perl-list-moreutils", + "perl-params-validate", + "perl-dist-checkconflicts", + "perl-cpan-meta-check", + "perl-test-fatal", + "perl-test-requires", + "perl-test-warnings", + "perl-scalar-list-utils", + "perl-params-validationcompiler", + "perl-file-sharedir", + "perl-file-sharedir-install", + "perl-namespace-autoclean" + ] + }, + "libgcab-doc": { + "versions": { + "0.7-r1": 1510067822 + }, + "origin": "libgcab" + }, + "krb5-doc": { + "versions": { + "1.15.4-r0": 1548491443 + }, + "origin": "krb5" + }, + "conky-doc": { + "versions": { + "1.10.6-r0": 1512047603 + }, + "origin": "conky" + }, + "xfconf-doc": { + "versions": { + "4.12.1-r2": 1510067933 + }, + "origin": "xfconf" + }, + "xf86-input-evdev-doc": { + "versions": { + "2.10.5-r0": 1510074955 + }, + "origin": "xf86-input-evdev" + }, + "py2-zope-interface": { + "versions": { + "4.3.2-r1": 1509493281 + }, + "origin": "py-zope-interface", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "py-werkzeug": { + "versions": { + "0.11.15-r2": 1509551864 + }, + "origin": "py-werkzeug" + }, + "libxft-dev": { + "versions": { + "2.3.2-r2": 1509462101 + }, + "origin": "libxft", + "dependencies": [ + "zlib-dev", + "libxft=2.3.2-r2", + "pc:fontconfig", + "pc:freetype2", + "pc:xproto", + "pc:xrender", + "pkgconfig" + ], + "provides": [ + "pc:xft=2.3.2" + ] + }, + "libseccomp-dev": { + "versions": { + "2.3.2-r1": 1519818661 + }, + "origin": "libseccomp", + "dependencies": [ + "linux-headers", + "libseccomp=2.3.2-r1", + "pkgconfig" + ], + "provides": [ + "pc:libseccomp=2.3.2" + ] + }, + "collectd-sensors": { + "versions": { + "5.7.2-r0": 1510069648 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libsensors.so.4" + ] + }, + "gmime": { + "versions": { + "2.6.20-r0": 1510588183 + }, + "origin": "gmime", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libz.so.1" + ], + "provides": [ + "so:libgmime-2.6.so.0=0.620.0" + ] + }, + "znc-doc": { + "versions": { + "1.7.1-r0": 1531900839, + "1.7.1-r1": 1565877330 + }, + "origin": "znc" + }, + "perl-file-slurp-tiny-doc": { + "versions": { + "0.004-r0": 1509494486 + }, + "origin": "perl-file-slurp-tiny" + }, + "libxkbfile": { + "versions": { + "1.0.9-r2": 1509473691 + }, + "origin": "libxkbfile", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxkbfile.so.1=1.0.2" + ] + }, + "polkit-lang": { + "versions": { + "0.105-r8": 1551780676, + "0.105-r9": 1563792450, + "0.105-r10": 1567523133 + }, + "origin": "polkit" + }, + "lua5.2-file-magic": { + "versions": { + "0.2-r0": 1509485935 + }, + "origin": "lua5.2-file-magic", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmagic.so.1" + ] + }, + "lua5.2-json4": { + "versions": { + "1.0.0-r2": 1509468368 + }, + "origin": "lua-json4" + }, + "ncurses-doc": { + "versions": { + "6.0_p20171125-r1": 1534862979 + }, + "origin": "ncurses" + }, + "pcsc-lite-doc": { + "versions": { + "1.8.22-r0": 1509474497 + }, + "origin": "pcsc-lite" + }, + "libavc1394-doc": { + "versions": { + "0.5.4-r1": 1509489638 + }, + "origin": "libavc1394" + }, + "libssh-dev": { + "versions": { + "0.7.6-r0": 1540400365 + }, + "origin": "libssh", + "dependencies": [ + "libssh=0.7.6-r0", + "pkgconfig" + ], + "provides": [ + "pc:libssh=0.7.6", + "pc:libssh_threads=0.7.6" + ] + }, + "syslinux-dev": { + "versions": { + "6.04_pre1-r1": 1509482642 + }, + "origin": "syslinux" + }, + "llvm5": { + "versions": { + "5.0.0-r0": 1510682603 + }, + "origin": "llvm5", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "llvm=5.0.0-r0", + "cmd:bugpoint", + "cmd:llc", + "cmd:lli", + "cmd:llvm-ar", + "cmd:llvm-as", + "cmd:llvm-bcanalyzer", + "cmd:llvm-c-test", + "cmd:llvm-cat", + "cmd:llvm-cov", + "cmd:llvm-cvtres", + "cmd:llvm-cxxdump", + "cmd:llvm-cxxfilt", + "cmd:llvm-diff", + "cmd:llvm-dis", + "cmd:llvm-dlltool", + "cmd:llvm-dsymutil", + "cmd:llvm-dwarfdump", + "cmd:llvm-dwp", + "cmd:llvm-extract", + "cmd:llvm-lib", + "cmd:llvm-link", + "cmd:llvm-lto", + "cmd:llvm-lto2", + "cmd:llvm-mc", + "cmd:llvm-mcmarkup", + "cmd:llvm-modextract", + "cmd:llvm-mt", + "cmd:llvm-nm", + "cmd:llvm-objdump", + "cmd:llvm-opt-report", + "cmd:llvm-pdbutil", + "cmd:llvm-profdata", + "cmd:llvm-ranlib", + "cmd:llvm-readelf", + "cmd:llvm-readobj", + "cmd:llvm-rtdyld", + "cmd:llvm-size", + "cmd:llvm-split", + "cmd:llvm-stress", + "cmd:llvm-strings", + "cmd:llvm-symbolizer", + "cmd:llvm-tblgen", + "cmd:llvm-xray", + "cmd:opt", + "cmd:sancov", + "cmd:sanstats", + "cmd:verify-uselistorder" + ] + }, + "xf86-input-mouse": { + "versions": { + "1.9.2-r0": 1510074098 + }, + "origin": "xf86-input-mouse", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "chrony": { + "versions": { + "3.2-r2": 1518706488 + }, + "origin": "chrony", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2" + ], + "provides": [ + "cmd:chronyc", + "cmd:chronyd" + ] + }, + "perl-cgi": { + "versions": { + "4.36-r0": 1509470551 + }, + "origin": "perl-cgi", + "dependencies": [ + "perl-html-parser" + ] + }, + "libtheora": { + "versions": { + "1.1.1-r13": 1510068273 + }, + "origin": "libtheora", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0" + ], + "provides": [ + "so:libtheora.so.0=0.3.10", + "so:libtheoradec.so.1=1.1.4", + "so:libtheoraenc.so.1=1.1.2" + ] + }, + "perl-cache-simple-timedexpiry-doc": { + "versions": { + "0.27-r1": 1510564524 + }, + "origin": "perl-cache-simple-timedexpiry" + }, + "perl-lwp-useragent-determined-doc": { + "versions": { + "1.07-r0": 1509495863 + }, + "origin": "perl-lwp-useragent-determined" + }, + "squid-lang-pt": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865669 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-test-requiresinternet": { + "versions": { + "0.05-r0": 1509464420 + }, + "origin": "perl-test-requiresinternet" + }, + "mesa-dri-vmwgfx": { + "versions": { + "17.2.4-r1": 1510741949 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "sysstat-doc": { + "versions": { + "11.6.0-r0": 1509491780 + }, + "origin": "sysstat" + }, + "mupdf-x11": { + "versions": { + "1.13.0-r0": 1533746007 + }, + "origin": "mupdf", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1", + "so:libmupdf.so.0", + "so:libmupdfthird.so.0" + ], + "provides": [ + "cmd:mupdf-x11" + ] + }, + "tslib-doc": { + "versions": { + "1.14-r0": 1510573237 + }, + "origin": "tslib" + }, + "uwsgi-curl_cron": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "perl-lwp-useragent-determined": { + "versions": { + "1.07-r0": 1509495867 + }, + "origin": "perl-lwp-useragent-determined", + "dependencies": [ + "perl", + "perl-libwww" + ] + }, + "gparted-doc": { + "versions": { + "0.30.0-r0": 1510073982 + }, + "origin": "gparted" + }, + "py2-tz": { + "versions": { + "2017.3-r0": 1510732131 + }, + "origin": "py-tz", + "dependencies": [ + "python2" + ] + }, + "libunique3": { + "versions": { + "3.0.2-r0": 1510069893 + }, + "origin": "libunique3", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk-3.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0" + ], + "provides": [ + "so:libunique-3.0.so.0=0.0.2" + ] + }, + "nagios-plugins-dns": { + "versions": { + "2.2.1-r3": 1510288494 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "bind-tools", + "so:libc.musl-x86_64.so.1" + ] + }, + "libx11-doc": { + "versions": { + "1.6.6-r0": 1538999955 + }, + "origin": "libx11" + }, + "perl-test-warnings": { + "versions": { + "0.026-r0": 1509468494 + }, + "origin": "perl-test-warnings" + }, + "asterisk-dev": { + "versions": { + "15.6.1-r0": 1537795340, + "15.6.2-r0": 1568705002 + }, + "origin": "asterisk", + "dependencies": [ + "asterisk" + ] + }, + "perl-list-moreutils-doc": { + "versions": { + "0.419-r1": 1509474021 + }, + "origin": "perl-list-moreutils" + }, + "curl-dev": { + "versions": { + "7.61.1-r2": 1551780214, + "7.61.1-r3": 1568722567 + }, + "origin": "curl", + "dependencies": [ + "libcurl=7.61.1-r3", + "pkgconfig" + ], + "provides": [ + "pc:libcurl=7.61.1", + "cmd:curl-config" + ] + }, + "squid-lang-fi": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libisoburn-dev": { + "versions": { + "1.4.8-r0": 1509462217 + }, + "origin": "libisoburn", + "dependencies": [ + "libisoburn=1.4.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:libisoburn-1=1.4.8" + ] + }, + "clucene-dev": { + "versions": { + "2.3.3.4-r5": 1509495541 + }, + "origin": "clucene", + "dependencies": [ + "zlib-dev", + "boost-dev", + "clucene-contribs=2.3.3.4-r5", + "clucene=2.3.3.4-r5", + "pkgconfig" + ], + "provides": [ + "pc:libclucene-core=2.3.3.4" + ] + }, + "ruby-doc": { + "versions": { + "2.4.6-r0": 1557166818 + }, + "origin": "ruby" + }, + "xbacklight-doc": { + "versions": { + "1.2.1-r0": 1509491141 + }, + "origin": "xbacklight" + }, + "micro-tetris": { + "versions": { + "1.2.1-r0": 1509496068 + }, + "origin": "micro-tetris", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tetris" + ] + }, + "xbanish": { + "versions": { + "1.5-r0": 1509489791 + }, + "origin": "xbanish", + "dependencies": [ + "so:libX11.so.6", + "so:libXfixes.so.3", + "so:libXi.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xbanish" + ] + }, + "libxkbui-dev": { + "versions": { + "1.0.2-r8": 1509473777 + }, + "origin": "libxkbui", + "dependencies": [ + "xproto", + "libxkbui=1.0.2-r8", + "pc:kbproto", + "pc:x11", + "pc:xt", + "pkgconfig" + ], + "provides": [ + "pc:xkbui=1.0.2" + ] + }, + "sqlite-dev": { + "versions": { + "3.25.3-r0": 1548491335, + "3.25.3-r1": 1563951530, + "3.25.3-r2": 1571590443 + }, + "origin": "sqlite", + "dependencies": [ + "pkgconfig", + "sqlite-libs=3.25.3-r2" + ], + "provides": [ + "pc:sqlite3=3.25.3" + ] + }, + "squid-lang-fr": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "popt": { + "versions": { + "1.16-r7": 1509462411 + }, + "origin": "popt", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpopt.so.0=0.0.0" + ] + }, + "sylpheed-lang": { + "versions": { + "3.6.0-r1": 1510588302 + }, + "origin": "sylpheed", + "dependencies": [ + "pinentry-gtk" + ] + }, + "glibmm": { + "versions": { + "2.50.1-r0": 1509472980 + }, + "origin": "glibmm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgiomm-2.4.so.1=1.3.0", + "so:libglibmm-2.4.so.1=1.3.0", + "so:libglibmm_generate_extra_defs-2.4.so.1=1.3.0" + ] + }, + "perl-cgi-fast": { + "versions": { + "2.13-r0": 1511894072 + }, + "origin": "perl-cgi-fast", + "dependencies": [ + "perl-cgi", + "perl-fcgi" + ] + }, + "perl-module-util": { + "versions": { + "1.09-r0": 1509485619 + }, + "origin": "perl-module-util", + "provides": [ + "cmd:pm_which" + ] + }, + "perl-socket6": { + "versions": { + "0.28-r0": 1509494721 + }, + "origin": "perl-socket6", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "lua-asn1": { + "versions": { + "2.1.0-r1": 1514491124 + }, + "origin": "lua-asn1", + "dependencies": [ + "lua-stringy" + ] + }, + "nagios-plugins-mrtgtraf": { + "versions": { + "2.2.1-r3": 1510288496 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.1-penlight": { + "versions": { + "1.5.4-r0": 1509485590 + }, + "origin": "lua-penlight", + "dependencies": [ + "lua-penlight-shared", + "lua5.1-filesystem", + "lua-penlight-shared=1.5.4-r0" + ] + }, + "gdb-doc": { + "versions": { + "8.0.1-r3": 1509486145 + }, + "origin": "gdb" + }, + "lua5.3-augeas": { + "versions": { + "0.1.2-r3": 1509473126 + }, + "origin": "lua-augeas", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1" + ] + }, + "py3-sphinx": { + "versions": { + "1.6.5-r0": 1509480689 + }, + "origin": "py-sphinx", + "dependencies": [ + "make", + "py3-docutils", + "py3-jinja2", + "py3-pygments", + "py3-six", + "py3-sphinx_rtd_theme", + "py3-alabaster<0.8", + "py3-babel", + "py3-snowballstemmer", + "py3-imagesize", + "py3-requests", + "py3-sphinxcontrib-websupport", + "python3" + ], + "provides": [ + "cmd:sphinx-apidoc-3", + "cmd:sphinx-autogen-3", + "cmd:sphinx-build-3", + "cmd:sphinx-quickstart-3" + ] + }, + "bzr-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503663 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "wpa_supplicant-doc": { + "versions": { + "2.6-r9": 1534860111, + "2.6-r10": 1559719897, + "2.6-r11": 1568726389 + }, + "origin": "wpa_supplicant" + }, + "perl-importer-doc": { + "versions": { + "0.024-r0": 1509481614 + }, + "origin": "perl-importer" + }, + "ttf-droid-nonlatin": { + "versions": { + "20121017-r0": 1509491428 + }, + "origin": "ttf-droid", + "dependencies": [ + "fontconfig" + ] + }, + "perl-http-cookies-doc": { + "versions": { + "6.04-r0": 1511871378 + }, + "origin": "perl-http-cookies" + }, + "xfontsel-doc": { + "versions": { + "1.0.5-r1": 1509481102 + }, + "origin": "xfontsel" + }, + "xf86miscproto": { + "versions": { + "0.9.3-r4": 1509473656 + }, + "origin": "xf86miscproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xf86miscproto=0.9.3" + ] + }, + "sems-mailbox": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "sems-ivr" + ] + }, + "btrfs-progs-dev": { + "versions": { + "4.13.2-r0": 1509481818 + }, + "origin": "btrfs-progs", + "dependencies": [ + "btrfs-progs-libs=4.13.2-r0" + ] + }, + "nginx-mod-http-cache-purge": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "curl-doc": { + "versions": { + "7.61.1-r2": 1551780213, + "7.61.1-r3": 1568722567 + }, + "origin": "curl" + }, + "openldap-overlay-accesslog": { + "versions": { + "2.4.45-r3": 1510258134, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "lvm2-doc": { + "versions": { + "2.02.175-r0": 1509462402 + }, + "origin": "lvm2" + }, + "fuse-dev": { + "versions": { + "2.9.8-r0": 1532967982 + }, + "origin": "fuse", + "dependencies": [ + "fuse=2.9.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:fuse=2.9.8" + ] + }, + "perl-file-sharedir-install": { + "versions": { + "0.11-r0": 1511889856 + }, + "origin": "perl-file-sharedir-install" + }, + "libsm": { + "versions": { + "1.2.2-r1": 1509466263 + }, + "origin": "libsm", + "dependencies": [ + "so:libICE.so.6", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libSM.so.6=6.0.1" + ] + }, + "py-olefile": { + "versions": { + "0.43-r2": 1509489937 + }, + "origin": "py-olefile" + }, + "xf86-video-r128": { + "versions": { + "6.10.2-r0": 1510074869 + }, + "origin": "xf86-video-r128", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "compiler-rt": { + "versions": { + "5.0.0-r0": 1510683366 + }, + "origin": "compiler-rt" + }, + "perl-netaddr-ip-doc": { + "versions": { + "4.079-r1": 1509485728 + }, + "origin": "perl-netaddr-ip" + }, + "llvm5-dev": { + "versions": { + "5.0.0-r0": 1510682602 + }, + "origin": "llvm5", + "dependencies": [ + "llvm5=5.0.0-r0", + "llvm5-libs=5.0.0-r0", + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "llvm-dev=5.0.0-r0", + "cmd:llvm-config" + ] + }, + "acf-db-lib": { + "versions": { + "0.2.1-r2": 1510068616 + }, + "origin": "acf-db", + "dependencies": [ + "acf-core", + "lua", + "acf-lib" + ] + }, + "spl-vanilla-dev": { + "versions": { + "4.9.161-r0": 1551779820, + "4.9.182-r0": 1560866250 + }, + "origin": "spl-vanilla", + "dependencies": [ + "linux-vanilla-dev=4.9.182-r0" + ] + }, + "nagios-plugins-ntp": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "collectd-bind": { + "versions": { + "5.7.2-r0": 1510069649 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libxml2.so.2" + ] + }, + "snownews-lang": { + "versions": { + "1.5.12-r6": 1510260845 + }, + "origin": "snownews", + "dependencies": [ + "ncurses" + ] + }, + "lame-doc": { + "versions": { + "3.100-r0": 1510088509 + }, + "origin": "lame" + }, + "perl-class-mix-doc": { + "versions": { + "0.005-r0": 1509470458 + }, + "origin": "perl-class-mix" + }, + "nss-pam-ldapd-doc": { + "versions": { + "0.9.8-r0": 1509483509 + }, + "origin": "nss-pam-ldapd" + }, + "libquvi": { + "versions": { + "0.9.4-r3": 1509492381 + }, + "origin": "libquvi", + "dependencies": [ + "libquvi-scripts", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libgcrypt.so.20", + "so:libglib-2.0.so.0", + "so:libgpg-error.so.0", + "so:libintl.so.8", + "so:liblua.so.5", + "so:libproxy.so.1" + ], + "provides": [ + "so:libquvi-0.9-0.9.4.so=0" + ] + }, + "aspell-de": { + "versions": { + "20030222-r1": 1509480778 + }, + "origin": "aspell-de" + }, + "motif-dev": { + "versions": { + "2.3.4-r2": 1509495434 + }, + "origin": "motif", + "dependencies": [ + "libx11-dev", + "libxft-dev", + "libxt-dev", + "libxpm-dev", + "libxext-dev", + "xbitmaps", + "motif=2.3.4-r2" + ] + }, + "zmap": { + "versions": { + "2.1.1-r1": 1510310599 + }, + "origin": "zmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libjson-c.so.2", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:zblacklist", + "cmd:zmap", + "cmd:ztee" + ] + }, + "xf86-input-synaptics": { + "versions": { + "1.9.0-r1": 1510073651 + }, + "origin": "xf86-input-synaptics", + "dependencies": [ + "so:libX11.so.6", + "so:libXi.so.6", + "so:libXtst.so.6", + "so:libc.musl-x86_64.so.1", + "so:libevdev.so.2" + ], + "provides": [ + "cmd:synclient", + "cmd:syndaemon" + ] + }, + "freeswitch-sounds-ru-RU-elena-32000": { + "versions": { + "1.0.12-r1": 1509491857 + }, + "origin": "freeswitch-sounds-ru-RU-elena-32000" + }, + "lz4-tests": { + "versions": { + "1.8.0-r1": 1509461405 + }, + "origin": "lz4" + }, + "nagios-plugins-overcr": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-module-build": { + "versions": { + "0.4224-r0": 1509459457 + }, + "origin": "perl-module-build", + "provides": [ + "cmd:config_data" + ] + }, + "lua-xctrl-doc": { + "versions": { + "2015.04.10-r2": 1509475303 + }, + "origin": "lua-xctrl" + }, + "mupdf-doc": { + "versions": { + "1.13.0-r0": 1533746007 + }, + "origin": "mupdf" + }, + "ldoc": { + "versions": { + "1.4.6-r0": 1509486331 + }, + "origin": "ldoc", + "dependencies": [ + "lua5.2-penlight", + "lua5.2" + ], + "provides": [ + "cmd:ldoc" + ] + }, + "libdc1394": { + "versions": { + "2.2.5-r1": 1509480972 + }, + "origin": "libdc1394", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libraw1394.so.11", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libdc1394.so.22=22.2.1", + "cmd:dc1394_reset_bus" + ] + }, + "f2fs-tools-doc": { + "versions": { + "1.6.1-r0": 1509483522 + }, + "origin": "f2fs-tools" + }, + "openldap-back-bdb": { + "versions": { + "2.4.45-r3": 1510258132, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "gdbm-doc": { + "versions": { + "1.13-r1": 1509458650 + }, + "origin": "gdbm" + }, + "perl-datetime-locale-doc": { + "versions": { + "1.17-r0": 1510855741 + }, + "origin": "perl-datetime-locale" + }, + "freeradius-dbg": { + "versions": { + "3.0.15-r4": 1556202795, + "3.0.15-r5": 1566310602 + }, + "origin": "freeradius" + }, + "dtc": { + "versions": { + "1.4.4-r0": 1509476729 + }, + "origin": "dtc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:convert-dtsv0", + "cmd:dtc", + "cmd:dtdiff", + "cmd:fdtdump", + "cmd:fdtget", + "cmd:fdtput" + ] + }, + "opus-dev": { + "versions": { + "1.2.1-r1": 1509468917 + }, + "origin": "opus", + "dependencies": [ + "opus=1.2.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:opus=1.2.1" + ] + }, + "debootstrap-doc": { + "versions": { + "1.0.92-r0": 1510588271 + }, + "origin": "debootstrap" + }, + "libmatroska-dev": { + "versions": { + "1.4.8-r0": 1509479839 + }, + "origin": "libmatroska", + "dependencies": [ + "libmatroska=1.4.8-r0", + "pc:libebml", + "pkgconfig" + ], + "provides": [ + "pc:libmatroska=1.4.8" + ] + }, + "collectd-ping": { + "versions": { + "5.7.2-r0": 1510069655 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:liboping.so.0" + ] + }, + "qemu-m68k": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-m68k" + ] + }, + "flex": { + "versions": { + "2.6.4-r1": 1509456704 + }, + "origin": "flex", + "dependencies": [ + "m4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:flex", + "cmd:flex++", + "cmd:lex" + ] + }, + "libvorbis": { + "versions": { + "1.3.6-r2": 1549615971 + }, + "origin": "libvorbis", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0" + ], + "provides": [ + "so:libvorbis.so.0=0.4.8", + "so:libvorbisenc.so.2=2.0.11", + "so:libvorbisfile.so.3=3.3.7" + ] + }, + "qemu-system-xtensa": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-xtensa" + ] + }, + "py2-wtforms": { + "versions": { + "2.1-r0": 1509476538 + }, + "origin": "py-wtforms", + "dependencies": [ + "python2" + ] + }, + "perl-extutils-pkgconfig": { + "versions": { + "1.16-r1": 1509491043 + }, + "origin": "perl-extutils-pkgconfig" + }, + "acf-openssh": { + "versions": { + "0.11.2-r0": 1510075988 + }, + "origin": "acf-openssh", + "dependencies": [ + "acf-core", + "openssh" + ] + }, + "gtkspell": { + "versions": { + "2.0.16-r6": 1510069682 + }, + "origin": "gtkspell", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libenchant.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libgtkspell.so.0=0.0.0" + ] + }, + "apcupsd-doc": { + "versions": { + "3.14.14-r0": 1509495261 + }, + "origin": "apcupsd" + }, + "postfix": { + "versions": { + "3.2.4-r1": 1510285341 + }, + "origin": "postfix", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libdb-5.3.so", + "so:libsasl2.so.3", + "so:libssl.so.44" + ], + "provides": [ + "so:libpostfix-dns.so=0", + "so:libpostfix-global.so=0", + "so:libpostfix-master.so=0", + "so:libpostfix-tls.so=0", + "so:libpostfix-util.so=0", + "cmd:mailq", + "cmd:newaliases", + "cmd:postalias", + "cmd:postcat", + "cmd:postconf", + "cmd:postdrop", + "cmd:postfix", + "cmd:postkick", + "cmd:postlock", + "cmd:postlog", + "cmd:postmap", + "cmd:postmulti", + "cmd:postqueue", + "cmd:postsuper", + "cmd:sendmail" + ] + }, + "sipcalc": { + "versions": { + "1.1.6-r0": 1509492402 + }, + "origin": "sipcalc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sipcalc" + ] + }, + "sysfsutils-dev": { + "versions": { + "2.1.0-r8": 1509475364 + }, + "origin": "sysfsutils", + "dependencies": [ + "sysfsutils=2.1.0-r8" + ] + }, + "lua5.3-alt-getopt": { + "versions": { + "0.7-r8": 1509480079 + }, + "origin": "lua-alt-getopt" + }, + "mod_dav_svn": { + "versions": { + "1.9.7-r0": 1510314501, + "1.9.12-r0": 1565086191 + }, + "origin": "subversion", + "dependencies": [ + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libsvn_delta-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_repos-1.so.0", + "so:libsvn_subr-1.so.0" + ] + }, + "atop": { + "versions": { + "2.3.0-r1": 1509489335 + }, + "origin": "atop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:atop", + "cmd:atop-2.3.0", + "cmd:atopacctd", + "cmd:atopsar", + "cmd:atopsar-2.3.0" + ] + }, + "usbutils": { + "versions": { + "009-r0": 1511890229 + }, + "origin": "usbutils", + "dependencies": [ + "hwdata-usb", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "pc:usbutils=009", + "cmd:lsusb", + "cmd:lsusb.py", + "cmd:usb-devices", + "cmd:usbhid-dump" + ] + }, + "apache-mod-auth-radius": { + "versions": { + "1.5.8-r3": 1509483219 + }, + "origin": "apache-mod-auth-radius", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1" + ] + }, + "aconf": { + "versions": { + "0.6.5-r0": 1510073710 + }, + "origin": "aconf", + "dependencies": [ + "lua5.2-augeas", + "lua5.2-b64", + "lua5.2-cjson", + "lua5.2-crypto", + "lua5.2-file-magic", + "lua5.2-openrc", + "lua5.2-posix", + "lua5.2-stringy", + "uwsgi", + "uwsgi-lua" + ], + "provides": [ + "cmd:aconfd" + ] + }, + "cegui06-dev": { + "versions": { + "0.6.2b-r14": 1510068747 + }, + "origin": "cegui06", + "dependencies": [ + "cegui06=0.6.2b-r14", + "pkgconfig" + ], + "provides": [ + "pc:CEGUI-OPENGL=0.6.2", + "pc:CEGUI=0.6.2" + ] + }, + "xf86-video-dummy": { + "versions": { + "0.3.8-r0": 1510072535 + }, + "origin": "xf86-video-dummy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "slim-doc": { + "versions": { + "1.3.6-r8": 1510067547 + }, + "origin": "slim" + }, + "py-pillow": { + "versions": { + "4.3.0-r0": 1509489960 + }, + "origin": "py-pillow", + "dependencies": [ + "py-olefile" + ] + }, + "perl-test-simple-doc": { + "versions": { + "1.302118-r0": 1512052687 + }, + "origin": "perl-test-simple" + }, + "py-flask-wtf": { + "versions": { + "0.14.2-r0": 1509476546 + }, + "origin": "py-flask-wtf", + "dependencies": [ + "py-flask", + "py-wtforms" + ] + }, + "font-misc-ethiopic": { + "versions": { + "1.0.3-r0": 1509479843 + }, + "origin": "font-misc-ethiopic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "py2-flask-wtf": { + "versions": { + "0.14.2-r0": 1509476545 + }, + "origin": "py-flask-wtf", + "dependencies": [ + "py2-flask", + "py2-wtforms", + "python2" + ] + }, + "librrd": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpng16.so.16", + "so:libxml2.so.2" + ], + "provides": [ + "so:librrd.so.4=4.3.5" + ] + }, + "py-boto": { + "versions": { + "2.47.0-r0": 1509472877 + }, + "origin": "py-boto", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:asadmin", + "cmd:bundle_image", + "cmd:cfadmin", + "cmd:cq", + "cmd:cwutil", + "cmd:dynamodb_dump", + "cmd:dynamodb_load", + "cmd:elbadmin", + "cmd:fetch_file", + "cmd:glacier", + "cmd:instance_events", + "cmd:kill_instance", + "cmd:launch_instance", + "cmd:list_instances", + "cmd:lss3", + "cmd:mturk", + "cmd:pyami_sendmail", + "cmd:route53", + "cmd:s3put", + "cmd:sdbadmin", + "cmd:taskadmin" + ] + }, + "ttf-droid": { + "versions": { + "20121017-r0": 1509491428 + }, + "origin": "ttf-droid", + "dependencies": [ + "fontconfig" + ] + }, + "compat-pvgrub": { + "versions": { + "1-r0": 1509476705 + }, + "origin": "compat-pvgrub", + "dependencies": [ + "/bin/sh" + ], + "provides": [ + "cmd:update-pvgrub" + ] + }, + "consolekit2": { + "versions": { + "1.2.0-r2": 1510067530 + }, + "origin": "consolekit2", + "dependencies": [ + "polkit", + "eudev", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libpolkit-gobject-1.so.0", + "so:libudev.so.1", + "so:libz.so.1" + ], + "provides": [ + "consolekit=1.2.0", + "so:libck-connector.so.0=0.0.0", + "so:libconsolekit.so.1=1.0.0", + "cmd:ck-history", + "cmd:ck-launch-session", + "cmd:ck-list-sessions", + "cmd:ck-log-system-restart", + "cmd:ck-log-system-start", + "cmd:ck-log-system-stop", + "cmd:console-kit-daemon" + ] + }, + "xinit": { + "versions": { + "1.3.4-r1": 1509473741 + }, + "origin": "xinit", + "dependencies": [ + "xauth", + "mcookie", + "xmodmap", + "xrdb", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:startx", + "cmd:xinit" + ] + }, + "libxshmfence-dev": { + "versions": { + "1.2-r2": 1509466255 + }, + "origin": "libxshmfence", + "dependencies": [ + "linux-headers", + "libxshmfence=1.2-r2", + "pkgconfig" + ], + "provides": [ + "pc:xshmfence=1.2" + ] + }, + "abiword-plugin-iscii": { + "versions": { + "3.0.2-r1": 1510073362 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "qemu-mipsn32el": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mipsn32el" + ] + }, + "run-parts": { + "versions": { + "4.8.2-r0": 1509470087 + }, + "origin": "run-parts", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:run-parts" + ] + }, + "lua5.2-mosquitto": { + "versions": { + "0.2-r1": 1509496427 + }, + "origin": "lua-mosquitto", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ] + }, + "font-misc-meltho": { + "versions": { + "1.0.3-r0": 1509494119 + }, + "origin": "font-misc-meltho", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-devel-globaldestruction": { + "versions": { + "0.14-r0": 1509475925 + }, + "origin": "perl-devel-globaldestruction", + "dependencies": [ + "perl-sub-exporter-progressive" + ] + }, + "perl-sys-hostname-long": { + "versions": { + "1.5-r0": 1509489405 + }, + "origin": "perl-sys-hostname-long", + "dependencies": [ + "perl" + ] + }, + "lua5.2-lub": { + "versions": { + "1.1.0-r1": 1509475704 + }, + "origin": "lua-lub", + "dependencies": [ + "lua5.2", + "lua5.2-filesystem" + ] + }, + "checkbashisms": { + "versions": { + "2.0.0.2-r5": 1509491256 + }, + "origin": "checkbashisms", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:checkbashisms" + ] + }, + "dropbear-ssh": { + "versions": { + "2018.76-r2": 1537439117 + }, + "origin": "dropbear", + "dependencies": [ + "dropbear-dbclient", + "!openssh-client", + "dropbear-dbclient=2018.76-r2" + ] + }, + "libdvdnav": { + "versions": { + "5.0.3-r0": 1509481834 + }, + "origin": "libdvdnav", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdvdread.so.4" + ], + "provides": [ + "so:libdvdnav.so.4=4.2.0" + ] + }, + "py-farstream0.1": { + "versions": { + "0.1.2-r2": 1510933113 + }, + "origin": "farstream0.1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfarstream-0.1.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-0.10.so.0" + ] + }, + "lua-lzlib": { + "versions": { + "0.4.3-r0": 1509494174 + }, + "origin": "lua-lzlib" + }, + "libnetfilter_queue-dev": { + "versions": { + "1.0.2-r0": 1509469244 + }, + "origin": "libnetfilter_queue", + "dependencies": [ + "libnetfilter_queue=1.0.2-r0", + "pc:libnfnetlink", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_queue=1.0.2" + ] + }, + "perl-data-uuid": { + "versions": { + "1.221-r1": 1509477323 + }, + "origin": "perl-data-uuid", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "procps-doc": { + "versions": { + "3.3.12-r3": 1509492055 + }, + "origin": "procps" + }, + "xwininfo": { + "versions": { + "1.1.3-r0": 1509484045 + }, + "origin": "xwininfo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-shape.so.0", + "so:libxcb.so.1" + ], + "provides": [ + "cmd:xwininfo" + ] + }, + "tk-dev": { + "versions": { + "8.6.6-r1": 1509462117 + }, + "origin": "tk", + "dependencies": [ + "tcl-dev", + "libx11-dev", + "libxft-dev", + "fontconfig-dev", + "pc:tcl>=8.6", + "pkgconfig" + ], + "provides": [ + "pc:tk=8.6.6" + ] + }, + "rsyslog-mysql": { + "versions": { + "8.31.0-r0": 1512031980, + "8.31.0-r1": 1571767017 + }, + "origin": "rsyslog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "openldap-clients": { + "versions": { + "2.4.45-r3": 1510258132, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900514 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libsasl2.so.3" + ], + "provides": [ + "cmd:ldapadd", + "cmd:ldapcompare", + "cmd:ldapdelete", + "cmd:ldapexop", + "cmd:ldapmodify", + "cmd:ldapmodrdn", + "cmd:ldappasswd", + "cmd:ldapsearch", + "cmd:ldapurl", + "cmd:ldapwhoami" + ] + }, + "gnome-doc-utils": { + "versions": { + "0.20.10-r2": 1509627733 + }, + "origin": "gnome-doc-utils", + "dependencies": [ + "python2", + "docbook-xml", + "rarian", + "py2-libxml2", + "libxslt" + ], + "provides": [ + "cmd:gnome-doc-prepare", + "cmd:gnome-doc-tool", + "cmd:xml2po" + ] + }, + "py-cparser": { + "versions": { + "2.18-r0": 1509483349 + }, + "origin": "py-cparser" + }, + "libidl-dev": { + "versions": { + "0.8.14-r2": 1510928272 + }, + "origin": "libidl", + "dependencies": [ + "glib-dev", + "libidl=0.8.14-r2", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libIDL-2.0=0.8.14" + ] + }, + "lcms-dev": { + "versions": { + "1.19-r6": 1509477352 + }, + "origin": "lcms", + "dependencies": [ + "liblcms=1.19-r6", + "pkgconfig" + ], + "provides": [ + "pc:lcms=1.19" + ] + }, + "daq": { + "versions": { + "2.0.6-r2": 1510839244 + }, + "origin": "daq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "so:libdaq.so.2=2.0.4", + "so:libsfbpf.so.0=0.0.1" + ] + }, + "mg": { + "versions": { + "20171014-r0": 1509494079 + }, + "origin": "mg", + "dependencies": [ + "so:libbsd.so.0", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:mg" + ] + }, + "py3-backports_abc": { + "versions": { + "0.4-r3": 1509552782 + }, + "origin": "py-backports_abc", + "dependencies": [ + "python3" + ] + }, + "libvirt-daemon": { + "versions": { + "3.9.0-r1": 1510088351, + "5.5.0-r0": 1562165539 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-client", + "bridge-utils", + "dmidecode", + "dnsmasq", + "ebtables", + "ip6tables", + "iptables", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libintl.so.8", + "so:libtirpc.so.3", + "so:libvirt-lxc.so.0", + "so:libvirt-qemu.so.0", + "so:libvirt.so.0" + ], + "provides": [ + "cmd:libvirtd" + ] + }, + "sshguard-doc": { + "versions": { + "2.0.0-r1": 1509483910 + }, + "origin": "sshguard" + }, + "iptables-dev": { + "versions": { + "1.6.1-r1": 1509469980 + }, + "origin": "iptables", + "dependencies": [ + "linux-headers", + "iptables=1.6.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:libip4tc=1.6.1", + "pc:libip6tc=1.6.1", + "pc:libipq=1.6.1", + "pc:libiptc=1.6.1", + "pc:xtables=1.6.1" + ] + }, + "acf-asterisk": { + "versions": { + "0.7.0-r2": 1510076281 + }, + "origin": "acf-asterisk", + "dependencies": [ + "acf-core", + "asterisk" + ] + }, + "perl-subversion": { + "versions": { + "1.9.7-r0": 1510314502, + "1.9.12-r0": 1565086191 + }, + "origin": "subversion", + "dependencies": [ + "so:libapr-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libsvn_client-1.so.0", + "so:libsvn_delta-1.so.0", + "so:libsvn_diff-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_ra-1.so.0", + "so:libsvn_repos-1.so.0", + "so:libsvn_subr-1.so.0", + "so:libsvn_wc-1.so.0" + ], + "provides": [ + "so:libsvn_swig_perl-1.so.0=0.0.0" + ] + }, + "execline": { + "versions": { + "2.3.0.3-r0": 1509488629 + }, + "origin": "execline", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.6" + ], + "provides": [ + "so:libexecline.so.2.3=2.3.0.3", + "cmd:background", + "cmd:backtick", + "cmd:cd", + "cmd:define", + "cmd:dollarat", + "cmd:elgetopt", + "cmd:elgetpositionals", + "cmd:elglob", + "cmd:emptyenv", + "cmd:exec", + "cmd:execlineb", + "cmd:exit", + "cmd:export", + "cmd:fdblock", + "cmd:fdclose", + "cmd:fdmove", + "cmd:fdreserve", + "cmd:fdswap", + "cmd:forbacktickx", + "cmd:foreground", + "cmd:forstdin", + "cmd:forx", + "cmd:getcwd", + "cmd:getpid", + "cmd:heredoc", + "cmd:homeof", + "cmd:if", + "cmd:ifelse", + "cmd:ifte", + "cmd:ifthenelse", + "cmd:import", + "cmd:importas", + "cmd:loopwhilex", + "cmd:multidefine", + "cmd:multisubstitute", + "cmd:pipeline", + "cmd:piperw", + "cmd:redirfd", + "cmd:runblock", + "cmd:shift", + "cmd:trap", + "cmd:tryexec", + "cmd:umask", + "cmd:unexport", + "cmd:wait", + "cmd:withstdinas" + ] + }, + "perl-class-load": { + "versions": { + "0.23-r0": 1509494278 + }, + "origin": "perl-class-load", + "dependencies": [ + "perl-data-optlist", + "perl-module-runtime", + "perl-module-implementation", + "perl-try-tiny", + "perl-namespace-clean", + "perl-package-stash" + ] + }, + "libotr3-dev": { + "versions": { + "3.2.1-r4": 1509481186 + }, + "origin": "libotr3", + "dependencies": [ + "libgcrypt-dev", + "libotr3=3.2.1-r4", + "pkgconfig" + ], + "provides": [ + "pc:libotr=3.1.0" + ] + }, + "libmikmod-doc": { + "versions": { + "3.3.11.1-r0": 1509481761 + }, + "origin": "libmikmod" + }, + "monkeysphere-doc": { + "versions": { + "0.41-r0": 1510588389 + }, + "origin": "monkeysphere" + }, + "openldap-overlay-ppolicy": { + "versions": { + "2.4.45-r3": 1510258135, + "2.4.46-r0": 1565073940, + "2.4.48-r0": 1566900517 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2", + "so:libltdl.so.7" + ] + }, + "asterisk-sounds-en": { + "versions": { + "15.6.1-r0": 1537795342, + "15.6.2-r0": 1568705004 + }, + "origin": "asterisk" + }, + "patch": { + "versions": { + "2.7.5-r2": 1519825694, + "2.7.5-r3": 1563906801, + "2.7.6-r0": 1565250814 + }, + "origin": "patch", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:patch" + ] + }, + "vimdiff": { + "versions": { + "8.0.1359-r0": 1512029968, + "8.0.1359-r1": 1559759711, + "8.0.1359-r2": 1561188677 + }, + "origin": "vim", + "dependencies": [ + "diffutils", + "vim=8.0.1359-r2" + ] + }, + "cyrus-sasl-gssapi": { + "versions": { + "2.1.26-r11": 1510258044 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgssapi.so.3" + ] + }, + "libtasn1-doc": { + "versions": { + "4.12-r3": 1519805758, + "4.12-r4": 1563955390 + }, + "origin": "libtasn1" + }, + "v86d": { + "versions": { + "0.1.10-r1": 1509491299 + }, + "origin": "v86d", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:v86d" + ] + }, + "nasm-doc": { + "versions": { + "2.13.01-r0": 1509464901 + }, + "origin": "nasm" + }, + "apache2-ldap": { + "versions": { + "2.4.39-r0": 1554306881, + "2.4.41-r0": 1566292327 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "apr-util-ldap", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "perl-regexp-common": { + "versions": { + "2016010801-r0": 1509485699 + }, + "origin": "perl-regexp-common" + }, + "acf-tcpproxy": { + "versions": { + "0.6.0-r2": 1510073457 + }, + "origin": "acf-tcpproxy", + "dependencies": [ + "acf-core", + "tcpproxy" + ] + }, + "freeglut-doc": { + "versions": { + "3.0.0-r0": 1510071761 + }, + "origin": "freeglut" + }, + "mpfr-dev": { + "versions": { + "3.1.5-r1": 1509457050 + }, + "origin": "mpfr3", + "dependencies": [ + "mpfr3=3.1.5-r1" + ] + }, + "gnupg1-doc": { + "versions": { + "1.4.23-r0": 1534431038 + }, + "origin": "gnupg1", + "provides": [ + "gnupg-doc=1.4.23-r0" + ] + }, + "samba-dc-libs": { + "versions": { + "4.7.6-r3": 1555491788 + }, + "origin": "samba", + "dependencies": [ + "samba-server=4.7.6-r3", + "samba-client=4.7.6-r3", + "samba-common-tools=4.7.6-r3", + "so:libMESSAGING-SEND-samba4.so", + "so:libMESSAGING-samba4.so", + "so:libasn1-samba4.so.8", + "so:libasn1util-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcliauth-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba4.so", + "so:libdcerpc.so.0", + "so:libdsdb-garbage-collect-tombstones-samba4.so", + "so:libevents-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgnutls.so.30", + "so:libhcrypto-samba4.so.5", + "so:libhx509-samba4.so.5", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libmessages-dgm-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnetif-samba4.so", + "so:libnpa-tstream-samba4.so", + "so:libpopt.so.0", + "so:libpython2.7.so.1.0", + "so:libreplace-samba4.so", + "so:libroken-samba4.so.19", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-net-samba4.so", + "so:libsamba-python-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsmb-transport-samba4.so", + "so:libsmbclient-raw-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libwbclient.so.0" + ], + "provides": [ + "so:libHDB-SAMBA4-samba4.so=0", + "so:libLIBWBCLIENT-OLD-samba4.so=0", + "so:libauth-unix-token-samba4.so=0", + "so:libauth4-samba4.so=0", + "so:libcluster-samba4.so=0", + "so:libdb-glue-samba4.so=0", + "so:libdcerpc-samr.so.0=0.0.1", + "so:libdcerpc-server.so.0=0.0.1", + "so:libdnsserver-common-samba4.so=0", + "so:libdsdb-module-samba4.so=0", + "so:libhdb-samba4.so.11=11.0.2", + "so:libkdc-samba4.so.2=2.0.0", + "so:libpac-samba4.so=0", + "so:libposix-eadb-samba4.so=0", + "so:libprocess-model-samba4.so=0", + "so:libsamba-policy.so.0=0.0.1", + "so:libservice-samba4.so=0", + "so:libshares-samba4.so=0" + ] + }, + "lua5.3-stdlib": { + "versions": { + "41.2.0-r0": 1509495246 + }, + "origin": "lua-stdlib" + }, + "perl-io-socket-ssl-doc": { + "versions": { + "2.048-r1": 1509474730 + }, + "origin": "perl-io-socket-ssl" + }, + "lua5.2-iconv": { + "versions": { + "7-r1": 1509493323 + }, + "origin": "lua-iconv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "kmod": { + "versions": { + "24-r0": 1509462317 + }, + "origin": "kmod", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:liblzma.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libkmod.so.2=2.3.2", + "cmd:depmod", + "cmd:insmod", + "cmd:kmod", + "cmd:lsmod", + "cmd:modinfo", + "cmd:modprobe", + "cmd:rmmod" + ] + }, + "cifs-utils": { + "versions": { + "6.7-r1": 1509475351 + }, + "origin": "cifs-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libkeyutils.so.1", + "so:libkrb5.so.3", + "so:libtalloc.so.2" + ], + "provides": [ + "cmd:cifs.upcall", + "cmd:mount.cifs" + ] + }, + "nano": { + "versions": { + "2.9.1-r0": 1512032033 + }, + "origin": "nano", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmagic.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:nano", + "cmd:rnano" + ] + }, + "libpthread-stubs": { + "versions": { + "0.3-r4": 1509461820 + }, + "origin": "libpthread-stubs", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:pthread-stubs=0.3" + ] + }, + "udev-init-scripts": { + "versions": { + "32-r1": 1509466061 + }, + "origin": "udev-init-scripts", + "dependencies": [ + "/bin/sh" + ] + }, + "perl-io-compress": { + "versions": { + "2.074-r0": 1509484002 + }, + "origin": "perl-io-compress", + "dependencies": [ + "perl", + "perl-compress-raw-bzip2", + "perl-compress-raw-zlib" + ] + }, + "v4l-utils-libs": { + "versions": { + "1.12.5-r1": 1510072045 + }, + "origin": "v4l-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8" + ], + "provides": [ + "so:libv4l1.so.0=0.0.0", + "so:libv4l2.so.0=0.0.0", + "so:libv4l2rds.so.0=0.0.0", + "so:libv4lconvert.so.0=0.0.0" + ] + }, + "pound-doc": { + "versions": { + "2.7-r4": 1510261014 + }, + "origin": "pound" + }, + "xhost-doc": { + "versions": { + "1.0.7-r1": 1509483059 + }, + "origin": "xhost" + }, + "freeswitch": { + "versions": { + "1.6.19-r0": 1510585358 + }, + "origin": "freeswitch", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libedit.so.0", + "so:libgcc_s.so.1", + "so:libilbc.so.0", + "so:libjpeg.so.8", + "so:libldns.so.1", + "so:liblua-5.2.so.0", + "so:libmp3lame.so.0", + "so:libmpg123.so.0", + "so:libncursesw.so.6", + "so:libodbc.so.2", + "so:libopus.so.0", + "so:libpcre.so.1", + "so:libportaudio.so.2", + "so:libpq.so.5", + "so:libshout.so.3", + "so:libsndfile.so.1", + "so:libspeex.so.1", + "so:libspeexdsp.so.1", + "so:libsqlite3.so.0", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libfreeswitch.so.1=1.0.0", + "cmd:freeswitch", + "cmd:fs_cli", + "cmd:fs_encode", + "cmd:fs_ivrd", + "cmd:fsxs", + "cmd:gentls_cert", + "cmd:tone2wav" + ] + }, + "libtheora-examples": { + "versions": { + "1.1.1-r13": 1510068273 + }, + "origin": "libtheora", + "dependencies": [ + "so:libSDL-1.2.so.0", + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libtheoradec.so.1", + "so:libtheoraenc.so.1", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2" + ], + "provides": [ + "cmd:dump_psnr", + "cmd:dump_video", + "cmd:encoder_example", + "cmd:player_example" + ] + }, + "ghi-doc": { + "versions": { + "1.2.0-r1": 1509552714 + }, + "origin": "ghi" + }, + "libasr-dev": { + "versions": { + "1.0.2-r6": 1510258973 + }, + "origin": "libasr", + "dependencies": [ + "libasr=1.0.2-r6" + ] + }, + "pcre2-doc": { + "versions": { + "10.30-r0": 1509461773 + }, + "origin": "pcre2" + }, + "perl-module-versions-report": { + "versions": { + "1.06-r0": 1509495045 + }, + "origin": "perl-module-versions-report", + "dependencies": [ + "perl" + ] + }, + "lighttpd-doc": { + "versions": { + "1.4.48-r0": 1511925914 + }, + "origin": "lighttpd" + }, + "perl-set-intspan": { + "versions": { + "1.19-r0": 1509480155 + }, + "origin": "perl-set-intspan" + }, + "gphoto2-doc": { + "versions": { + "2.5.14-r0": 1509493217 + }, + "origin": "gphoto2" + }, + "devicemaster-linux": { + "versions": { + "7.15-r0": 1509481943 + }, + "origin": "devicemaster-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:nslinkadmin", + "cmd:nslinkd", + "cmd:nslinkrelease", + "cmd:nslinktool" + ] + }, + "lutok-doc": { + "versions": { + "0.4-r1": 1509459744 + }, + "origin": "lutok" + }, + "xscreensaver-doc": { + "versions": { + "5.36-r0": 1510074348 + }, + "origin": "xscreensaver" + }, + "varnish-libs": { + "versions": { + "5.2.1-r0": 1511265090 + }, + "origin": "varnish", + "dependencies": [ + "gcc", + "libc-dev", + "libgcc", + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ], + "provides": [ + "so:libvarnishapi.so.1=1.0.6" + ] + }, + "libfdt": { + "versions": { + "1.4.4-r0": 1509476728 + }, + "origin": "dtc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfdt.so.1=0" + ] + }, + "xf86-video-i128-doc": { + "versions": { + "1.3.6-r8": 1510074907 + }, + "origin": "xf86-video-i128" + }, + "clang-dev": { + "versions": { + "5.0.0-r0": 1510683246 + }, + "origin": "clang", + "dependencies": [ + "clang=5.0.0-r0", + "clang-libs=5.0.0-r0" + ] + }, + "libcanberra-doc": { + "versions": { + "0.30-r1": 1510071975 + }, + "origin": "libcanberra" + }, + "spl-doc": { + "versions": { + "0.7.1-r0": 1509492193 + }, + "origin": "spl" + }, + "libwebp-tools": { + "versions": { + "0.6.0-r1": 1509473039 + }, + "origin": "libwebp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgif.so.7", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libtiff.so.5", + "so:libwebp.so.7", + "so:libwebpmux.so.3" + ], + "provides": [ + "cmd:cwebp", + "cmd:dwebp", + "cmd:gif2webp", + "cmd:img2webp", + "cmd:webpmux" + ] + }, + "compositeproto": { + "versions": { + "0.4.2-r4": 1509465978 + }, + "origin": "compositeproto", + "dependencies": [ + "fixesproto", + "pkgconfig" + ], + "provides": [ + "pc:compositeproto=0.4.2" + ] + }, + "git-subtree-doc": { + "versions": { + "2.15.3-r0": 1540401295, + "2.15.4-r0": 1576015196 + }, + "origin": "git" + }, + "libasyncns-doc": { + "versions": { + "0.8-r0": 1509490869 + }, + "origin": "libasyncns" + }, + "lua-inspect": { + "versions": { + "3.1.0-r1": 1509491027 + }, + "origin": "lua-inspect" + }, + "testdisk-doc": { + "versions": { + "7.0-r4": 1509491593 + }, + "origin": "testdisk" + }, + "lzo-dev": { + "versions": { + "2.10-r2": 1509462187 + }, + "origin": "lzo", + "dependencies": [ + "lzo=2.10-r2", + "pkgconfig" + ], + "provides": [ + "pc:lzo2=2.10" + ] + }, + "perl-io-html-doc": { + "versions": { + "1.001-r1": 1509464367 + }, + "origin": "perl-io-html" + }, + "py-django-widget-tweaks": { + "versions": { + "1.4.1-r0": 1509480739 + }, + "origin": "py-django-widget-tweaks", + "dependencies": [ + "py-django" + ] + }, + "subunit-libs": { + "versions": { + "1.2.0-r0": 1509481264 + }, + "origin": "subunit", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcppunit_subunit.so.0=0.0.0", + "so:libsubunit.so.0=0.0.0" + ] + }, + "perl-async-mergepoint": { + "versions": { + "0.04-r0": 1509494970 + }, + "origin": "perl-async-mergepoint" + }, + "xorg-cf-files-doc": { + "versions": { + "1.0.6-r0": 1509488818 + }, + "origin": "xorg-cf-files" + }, + "py2-pylast": { + "versions": { + "1.9.0-r0": 1509491031 + }, + "origin": "py-pylast", + "dependencies": [ + "python2" + ] + }, + "nginx-vim": { + "versions": { + "1.12.2-r4": 1542814446 + }, + "origin": "nginx" + }, + "dmidecode-doc": { + "versions": { + "3.1-r0": 1509482141 + }, + "origin": "dmidecode" + }, + "xen-libs": { + "versions": { + "4.9.4-r0": 1551280495, + "4.9.4-r1": 1558103152 + }, + "origin": "xen", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libext2fs.so.2", + "so:liblzma.so.5", + "so:liblzo2.so.2", + "so:libnl-3.so.200", + "so:libnl-route-3.so.200", + "so:libuuid.so.1", + "so:libyajl.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libblktapctl.so.1.0=1.0.0", + "so:libfsimage.so.1.0=1.0.0", + "so:libvhd.so.1.0=1.0.0", + "so:libxencall.so.1=1.0", + "so:libxenctrl.so.4.9=4.9.0", + "so:libxendevicemodel.so.1=1.0", + "so:libxenevtchn.so.1=1.0", + "so:libxenforeignmemory.so.1=1.1", + "so:libxengnttab.so.1=1.1", + "so:libxenguest.so.4.9=4.9.0", + "so:libxenlight.so.4.9=4.9.0", + "so:libxenstat.so.0=0.0", + "so:libxenstore.so.3.0=3.0.3", + "so:libxentoollog.so.1=1.0", + "so:libxenvchan.so.4.9=4.9.0", + "so:libxlutil.so.4.9=4.9.0" + ] + }, + "pkgconf": { + "versions": { + "1.3.10-r0": 1509459814 + }, + "origin": "pkgconf", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "pkgconfig=1", + "so:libpkgconf.so.2=2.0.0", + "cmd:pkg-config", + "cmd:pkgconf" + ] + }, + "xfce4": { + "versions": { + "4.12.0-r1": 1510920462 + }, + "origin": "xfce4", + "dependencies": [ + "exo>=0.10.3", + "garcon>=0.4.0", + "gtk-xfce-engine>=3.2.0", + "libxfce4ui>=4.12.0", + "libxfce4util>=4.12.0", + "thunar>=1.6.6", + "ttf-dejavu", + "xfce4-appfinder>=4.12.0", + "xfce4-mixer", + "xfce4-panel>=4.12.0", + "xfce4-power-manager>=1.4.3", + "xfce4-session>=4.12.0", + "xfce4-settings>=4.12.0", + "xfce4-terminal", + "xfconf>=4.12.0", + "xfdesktop>=4.12.0", + "xfwm4>=4.12.0" + ] + }, + "pidgin-lang": { + "versions": { + "2.12.0-r2": 1510069750 + }, + "origin": "pidgin" + }, + "perl-socket-getaddrinfo-doc": { + "versions": { + "0.20-r2": 1509488746 + }, + "origin": "perl-socket-getaddrinfo" + }, + "tar-doc": { + "versions": { + "1.32-r0": 1551092030 + }, + "origin": "tar" + }, + "mini_httpd-doc": { + "versions": { + "1.27-r1": 1510260670 + }, + "origin": "mini_httpd" + }, + "libvirt-lang": { + "versions": { + "3.9.0-r1": 1510088351, + "5.5.0-r0": 1562165540 + }, + "origin": "libvirt", + "dependencies": [ + "lvm2" + ] + }, + "libnice-dev": { + "versions": { + "0.1.14-r2": 1510931344 + }, + "origin": "libnice", + "dependencies": [ + "libnice=0.1.14-r2", + "pc:gio-2.0>=2.44", + "pc:glib-2.0>=2.44", + "pc:gnutls>=2.12.0", + "pc:gobject-2.0>=2.44", + "pc:gthread-2.0", + "pkgconfig" + ], + "provides": [ + "pc:nice=0.1.14" + ] + }, + "flac-doc": { + "versions": { + "1.3.2-r0": 1509473288 + }, + "origin": "flac" + }, + "man": { + "versions": { + "1.14.3-r0": 1509459637 + }, + "origin": "mdocml", + "dependencies": [ + "mdocml" + ] + }, + "smartmontools-doc": { + "versions": { + "6.6-r0": 1510573357 + }, + "origin": "smartmontools" + }, + "py-paramiko": { + "versions": { + "2.4.2-r0": 1551364326 + }, + "origin": "py-paramiko", + "dependencies": [ + "py-asn1", + "py-cryptography", + "py-bcrypt", + "py-pynacl" + ] + }, + "xfce4-clipman-plugin-lang": { + "versions": { + "1.4.2-r0": 1510073453 + }, + "origin": "xfce4-clipman-plugin" + }, + "libotr-doc": { + "versions": { + "4.1.1-r1": 1509473950 + }, + "origin": "libotr" + }, + "xfdesktop-doc": { + "versions": { + "4.12.4-r0": 1510074490 + }, + "origin": "xfdesktop" + }, + "util-linux": { + "versions": { + "2.31.1-r0": 1541506295 + }, + "origin": "util-linux", + "dependencies": [ + "findmnt", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfdisk.so.1", + "so:libmount.so.1", + "so:libncursesw.so.6", + "so:libsmartcols.so.1", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:addpart", + "cmd:agetty", + "cmd:blkdiscard", + "cmd:blockdev", + "cmd:cal", + "cmd:chcpu", + "cmd:chmem", + "cmd:chrt", + "cmd:col", + "cmd:colcrt", + "cmd:colrm", + "cmd:column", + "cmd:ctrlaltdel", + "cmd:delpart", + "cmd:dmesg", + "cmd:eject", + "cmd:fallocate", + "cmd:fdformat", + "cmd:fdisk", + "cmd:fincore", + "cmd:findfs", + "cmd:flock", + "cmd:fsck", + "cmd:fsck.cramfs", + "cmd:fsck.minix", + "cmd:fsfreeze", + "cmd:fstrim", + "cmd:getopt", + "cmd:hexdump", + "cmd:hwclock", + "cmd:i386", + "cmd:ionice", + "cmd:ipcmk", + "cmd:ipcrm", + "cmd:ipcs", + "cmd:isosize", + "cmd:ldattach", + "cmd:linux32", + "cmd:linux64", + "cmd:logger", + "cmd:look", + "cmd:losetup", + "cmd:lsblk", + "cmd:lscpu", + "cmd:lsipc", + "cmd:lslocks", + "cmd:lslogins", + "cmd:lsmem", + "cmd:lsns", + "cmd:mesg", + "cmd:mkfs", + "cmd:mkfs.bfs", + "cmd:mkfs.cramfs", + "cmd:mkfs.minix", + "cmd:mkswap", + "cmd:more", + "cmd:mount", + "cmd:mountpoint", + "cmd:namei", + "cmd:nologin", + "cmd:nsenter", + "cmd:partx", + "cmd:pivot_root", + "cmd:prlimit", + "cmd:raw", + "cmd:readprofile", + "cmd:rename", + "cmd:renice", + "cmd:resizepart", + "cmd:rev", + "cmd:rfkill", + "cmd:rtcwake", + "cmd:script", + "cmd:scriptreplay", + "cmd:setarch", + "cmd:setsid", + "cmd:setterm", + "cmd:swaplabel", + "cmd:swapoff", + "cmd:swapon", + "cmd:switch_root", + "cmd:taskset", + "cmd:ul", + "cmd:umount", + "cmd:uname26", + "cmd:unshare", + "cmd:utmpdump", + "cmd:uuidgen", + "cmd:uuidparse", + "cmd:wall", + "cmd:wdctl", + "cmd:whereis", + "cmd:wipefs", + "cmd:x86_64", + "cmd:zramctl" + ] + }, + "py-hiredis": { + "versions": { + "0.1.4-r1": 1509552775 + }, + "origin": "py-hiredis", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.13", + "so:libpython2.7.so.1.0" + ] + }, + "audit-static": { + "versions": { + "2.7.7-r1": 1509491478 + }, + "origin": "audit" + }, + "glib-lang": { + "versions": { + "2.54.2-r0": 1509911133, + "2.54.2-r1": 1560764732 + }, + "origin": "glib" + }, + "libpri-dev": { + "versions": { + "1.6.0-r0": 1509476160 + }, + "origin": "libpri", + "dependencies": [ + "libpri=1.6.0-r0" + ] + }, + "perl-crypt-openssl-random": { + "versions": { + "0.11-r4": 1510260064 + }, + "origin": "perl-crypt-openssl-random", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "aspell-fr": { + "versions": { + "0.50_p3-r1": 1509495844 + }, + "origin": "aspell-fr" + }, + "dropbear-convert": { + "versions": { + "2018.76-r2": 1537439117 + }, + "origin": "dropbear", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dropbearconvert" + ] + }, + "gzip": { + "versions": { + "1.8-r0": 1509456994 + }, + "origin": "gzip", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:gunzip", + "cmd:gzexe", + "cmd:gzip", + "cmd:uncompress", + "cmd:zcat", + "cmd:zcmp", + "cmd:zdiff", + "cmd:zegrep", + "cmd:zfgrep", + "cmd:zforce", + "cmd:zgrep", + "cmd:zless", + "cmd:zmore", + "cmd:znew" + ] + }, + "perl-switch-doc": { + "versions": { + "2.17-r0": 1509475519 + }, + "origin": "perl-switch" + }, + "libnjb": { + "versions": { + "2.2.7-r4": 1509481679 + }, + "origin": "libnjb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-0.1.so.4" + ], + "provides": [ + "so:libnjb.so.5=5.1.1" + ] + }, + "gnome-vfs-dev": { + "versions": { + "2.24.4-r6": 1510931435 + }, + "origin": "gnome-vfs", + "dependencies": [ + "gconf-dev", + "libxml2-dev", + "dbus-glib-dev", + "gamin-dev", + "gnome-vfs=2.24.4-r6", + "pc:gconf-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gnome-vfs-2.0=2.24.4", + "pc:gnome-vfs-module-2.0=2.24.4" + ] + }, + "perl-css-minifier-xs-doc": { + "versions": { + "0.09-r3": 1509489143 + }, + "origin": "perl-css-minifier-xs" + }, + "libgcj": { + "versions": { + "6.4.0-r5": 1509458076 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgmp.so.10", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libgcj-tools.so.17=17.0.0", + "so:libgcj.so.17=17.0.0", + "so:libgcj_bc.so.1=1.0.0", + "so:libgij.so.17=17.0.0", + "so:libjavamath.so=0", + "so:libjvm.so=0", + "cmd:aot-compile", + "cmd:gappletviewer", + "cmd:gc-analyze", + "cmd:gij", + "cmd:gjar", + "cmd:gjarsigner", + "cmd:gkeytool", + "cmd:gnative2ascii", + "cmd:gorbd", + "cmd:grmic", + "cmd:grmid", + "cmd:grmiregistry", + "cmd:gserialver", + "cmd:gtnameserv", + "cmd:jv-convert", + "cmd:rebuild-gcj-db" + ] + }, + "py-oauth2": { + "versions": { + "1.9.0-r0": 1509493379 + }, + "origin": "py-oauth2", + "dependencies": [ + "python2", + "py-httplib2" + ] + }, + "glib-dev": { + "versions": { + "2.54.2-r0": 1509911133, + "2.54.2-r1": 1560764732 + }, + "origin": "glib", + "dependencies": [ + "perl", + "python2", + "gettext-dev", + "zlib-dev", + "bzip2-dev", + "libffi-dev", + "util-linux-dev", + "glib=2.54.2-r1", + "pc:libpcre", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "pc:gio-2.0=2.54.2", + "pc:gio-unix-2.0=2.54.2", + "pc:glib-2.0=2.54.2", + "pc:gmodule-2.0=2.54.2", + "pc:gmodule-export-2.0=2.54.2", + "pc:gmodule-no-export-2.0=2.54.2", + "pc:gobject-2.0=2.54.2", + "pc:gthread-2.0=2.54.2", + "cmd:gdbus-codegen", + "cmd:glib-compile-resources", + "cmd:glib-genmarshal", + "cmd:glib-gettextize", + "cmd:glib-mkenums", + "cmd:gobject-query", + "cmd:gresource", + "cmd:gtester", + "cmd:gtester-report" + ] + }, + "perl-data-guid-doc": { + "versions": { + "0.049-r0": 1509479283 + }, + "origin": "perl-data-guid" + }, + "avahi-doc": { + "versions": { + "0.6.32-r4": 1509465446, + "0.6.32-r5": 1563345619 + }, + "origin": "avahi" + }, + "samba-common": { + "versions": { + "4.7.6-r3": 1555491786 + }, + "origin": "samba" + }, + "lvm2": { + "versions": { + "2.02.175-r0": 1509462403 + }, + "origin": "lvm2", + "dependencies": [ + "lvm2-libs=2.02.175-r0", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper-event.so.1.02", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "cmd:lvchange", + "cmd:lvconvert", + "cmd:lvcreate", + "cmd:lvdisplay", + "cmd:lvextend", + "cmd:lvm", + "cmd:lvmconfig", + "cmd:lvmdiskscan", + "cmd:lvmsadc", + "cmd:lvmsar", + "cmd:lvreduce", + "cmd:lvremove", + "cmd:lvrename", + "cmd:lvresize", + "cmd:lvs", + "cmd:lvscan", + "cmd:pvchange", + "cmd:pvck", + "cmd:pvcreate", + "cmd:pvdisplay", + "cmd:pvmove", + "cmd:pvremove", + "cmd:pvresize", + "cmd:pvs", + "cmd:pvscan", + "cmd:vgcfgbackup", + "cmd:vgcfgrestore", + "cmd:vgchange", + "cmd:vgck", + "cmd:vgconvert", + "cmd:vgcreate", + "cmd:vgdisplay", + "cmd:vgexport", + "cmd:vgextend", + "cmd:vgimport", + "cmd:vgimportclone", + "cmd:vgmerge", + "cmd:vgmknodes", + "cmd:vgreduce", + "cmd:vgremove", + "cmd:vgrename", + "cmd:vgs", + "cmd:vgscan", + "cmd:vgsplit" + ] + }, + "perl-net-cidr-doc": { + "versions": { + "0.17-r0": 1509486329 + }, + "origin": "perl-net-cidr" + }, + "cadaver": { + "versions": { + "0.23.3-r3": 1510310613 + }, + "origin": "cadaver", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libneon.so.27" + ], + "provides": [ + "cmd:cadaver" + ] + }, + "boost-python3": { + "versions": { + "1.62.0-r5": 1509465877 + }, + "origin": "boost", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpython3.6m.so.1.0" + ], + "provides": [ + "so:libboost_python3-mt.so.1.62.0=1.62.0", + "so:libboost_python3.so.1.62.0=1.62.0" + ] + }, + "dpkg": { + "versions": { + "1.18.24-r0": 1509494231 + }, + "origin": "dpkg", + "dependencies": [ + "xz", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:dpkg", + "cmd:dpkg-buildflags", + "cmd:dpkg-deb", + "cmd:dpkg-divert", + "cmd:dpkg-genbuildinfo", + "cmd:dpkg-maintscript-helper", + "cmd:dpkg-mergechangelogs", + "cmd:dpkg-parsechangelog", + "cmd:dpkg-query", + "cmd:dpkg-split", + "cmd:dpkg-statoverride", + "cmd:dpkg-trigger", + "cmd:update-alternatives" + ] + }, + "rrdcollect": { + "versions": { + "0.2.10-r1": 1509492721 + }, + "origin": "rrdcollect", + "dependencies": [ + "rrdtool", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rrdcollect" + ] + }, + "openipmi-dev": { + "versions": { + "2.0.22-r1": 1510260043 + }, + "origin": "openipmi", + "dependencies": [ + "openipmi-lanserv=2.0.22-r1", + "openipmi-libs=2.0.22-r1", + "pc:ncurses", + "pkgconfig" + ], + "provides": [ + "pc:OpenIPMI=2.0.22", + "pc:OpenIPMIcmdlang=2.0.22", + "pc:OpenIPMIglib=2.0.22", + "pc:OpenIPMIposix=2.0.22", + "pc:OpenIPMIpthread=2.0.22", + "pc:OpenIPMIui=2.0.22", + "pc:OpenIPMIutils=2.0.22" + ] + }, + "nginx-mod-http-redis2": { + "versions": { + "1.12.2-r4": 1542814448 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "wget-doc": { + "versions": { + "1.20.3-r0": 1554724053 + }, + "origin": "wget" + }, + "py2-openssl": { + "versions": { + "17.5.0-r0": 1548491341 + }, + "origin": "py-openssl", + "dependencies": [ + "py2-cryptography", + "py2-six", + "python2" + ] + }, + "btrfs-progs": { + "versions": { + "4.13.2-r0": 1509481819 + }, + "origin": "btrfs-progs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:liblzo2.so.2", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:btrfs", + "cmd:btrfsck", + "cmd:fsck.btrfs", + "cmd:mkfs.btrfs" + ] + }, + "python3-doc": { + "versions": { + "3.6.8-r0": 1555928808, + "3.6.9-r0": 1571233994, + "3.6.9-r1": 1571314638 + }, + "origin": "python3" + }, + "acpica": { + "versions": { + "20170303-r0": 1510068817 + }, + "origin": "acpica", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:acpibin", + "cmd:acpidump", + "cmd:acpiexamples", + "cmd:acpiexec", + "cmd:acpihelp", + "cmd:acpinames", + "cmd:acpisrc", + "cmd:acpixtract" + ] + }, + "libtorrent-dev": { + "versions": { + "0.13.6-r3": 1510259942 + }, + "origin": "libtorrent", + "dependencies": [ + "libtorrent=0.13.6-r3", + "pkgconfig" + ], + "provides": [ + "pc:libtorrent=0.13.6" + ] + }, + "collectd-lvm": { + "versions": { + "5.7.2-r0": 1510069653 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:liblvm2app.so.2.2" + ] + }, + "audacious": { + "versions": { + "3.9-r0": 1510072525 + }, + "origin": "audacious", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libaudcore.so.5=5.0.0", + "so:libaudgui.so.5=5.0.0", + "so:libaudtag.so.3=3.0.0", + "cmd:audacious", + "cmd:audtool" + ] + }, + "fcgiwrap-doc": { + "versions": { + "1.1.0-r2": 1510933068 + }, + "origin": "fcgiwrap" + }, + "udisks2-dev": { + "versions": { + "2.6.5-r0": 1510075148 + }, + "origin": "udisks2", + "dependencies": [ + "gobject-introspection-dev", + "polkit-dev", + "libatasmart-dev", + "libgudev-dev", + "acl-dev", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig", + "udisks2-libs=2.6.5-r0" + ], + "provides": [ + "pc:udisks2=2.6.5" + ] + }, + "perl-mail-imapclient": { + "versions": { + "3.39-r0": 1509494893 + }, + "origin": "perl-mail-imapclient", + "dependencies": [ + "perl-parse-recdescent" + ] + }, + "nfs-utils-doc": { + "versions": { + "2.1.1-r4": 1509492106 + }, + "origin": "nfs-utils" + }, + "libcmph": { + "versions": { + "2.0-r1": 1509482578 + }, + "origin": "cmph", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcmph.so.0=0.0.0" + ] + }, + "mksh": { + "versions": { + "56b-r0": 1509491851 + }, + "origin": "mksh", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mksh" + ] + }, + "uwsgi-redislog": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-devel-stacktrace-ashtml-doc": { + "versions": { + "0.15-r0": 1510564491 + }, + "origin": "perl-devel-stacktrace-ashtml" + }, + "patchwork-postgresql": { + "versions": { + "1.1.3-r0": 1509474239, + "1.1.3-r1": 1562223418 + }, + "origin": "patchwork", + "dependencies": [ + "py-psycopg2" + ] + }, + "supervisor": { + "versions": { + "3.3.3-r1": 1509552736 + }, + "origin": "supervisor", + "dependencies": [ + "python2", + "py-meld3", + "py-setuptools" + ], + "provides": [ + "cmd:echo_supervisord_conf", + "cmd:pidproxy", + "cmd:supervisorctl", + "cmd:supervisord" + ] + }, + "libmilter-doc": { + "versions": { + "1.0.2-r5": 1509479369 + }, + "origin": "libmilter" + }, + "py3-libxml2": { + "versions": { + "2.9.8-r1": 1540398580 + }, + "origin": "libxml2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0", + "so:libxml2.so.2" + ] + }, + "perl-test-exception": { + "versions": { + "0.43-r0": 1509473075 + }, + "origin": "perl-test-exception", + "dependencies": [ + "perl-sub-uplevel" + ] + }, + "api-sanity-checker": { + "versions": { + "1.98.7-r0": 1510075239 + }, + "origin": "api-sanity-checker", + "dependencies": [ + "perl", + "build-base" + ], + "provides": [ + "cmd:api-sanity-checker" + ] + }, + "py-crypto": { + "versions": { + "2.6.1-r2": 1509483004 + }, + "origin": "py-crypto" + }, + "xdriinfo-doc": { + "versions": { + "1.0.5-r0": 1510074964 + }, + "origin": "xdriinfo" + }, + "man-pages": { + "versions": { + "4.14-r0": 1512031990 + }, + "origin": "man-pages" + }, + "lua5.3-lyaml": { + "versions": { + "6.1.3-r1": 1509473485 + }, + "origin": "lua-lyaml", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libyaml-0.so.2" + ] + }, + "acf-unbound": { + "versions": { + "0.1.0-r2": 1510588360 + }, + "origin": "acf-unbound", + "dependencies": [ + "acf-core", + "unbound" + ] + }, + "zfs-vanilla": { + "versions": { + "4.9.161-r0": 1551780615, + "4.9.182-r0": 1560866659 + }, + "origin": "zfs-vanilla", + "dependencies": [ + "spl-vanilla", + "linux-vanilla=4.9.182-r0" + ] + }, + "rlog": { + "versions": { + "1.4-r4": 1509475981 + }, + "origin": "rlog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:librlog.so.5=5.0.0" + ] + }, + "libcdio-tools": { + "versions": { + "0.94-r0": 1509473627 + }, + "origin": "libcdio", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcddb.so.2", + "so:libcdio.so.16", + "so:libiso9660.so.10", + "so:libncursesw.so.6", + "so:libudf.so.0" + ], + "provides": [ + "cmd:cd-drive", + "cmd:cd-info", + "cmd:cd-read", + "cmd:cdda-player", + "cmd:iso-info", + "cmd:iso-read", + "cmd:mmc-tool" + ] + }, + "sg3_utils-dev": { + "versions": { + "1.42-r0": 1509482217 + }, + "origin": "sg3_utils", + "dependencies": [ + "sg3_utils=1.42-r0" + ] + }, + "samba-common-libs": { + "versions": { + "4.7.6-r3": 1555491786 + }, + "origin": "samba", + "dependencies": [ + "so:libCHARSET3-samba4.so", + "so:libasn1util-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libcom_err.so.2", + "so:libdbwrap-samba4.so", + "so:libevents-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libmessages-dgm-samba4.so", + "so:libmessages-util-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-shim-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libtalloc.so.2", + "so:libtdb-wrap-samba4.so", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-cmdline-samba4.so", + "so:libutil-setid-samba4.so", + "so:libutil-tdb-samba4.so", + "so:libwbclient.so.0" + ], + "provides": [ + "so:libMESSAGING-SEND-samba4.so=0", + "so:libcli-spoolss-samba4.so=0", + "so:libdcerpc-binding.so.0=0.0.1", + "so:libdcerpc-samba-samba4.so=0", + "so:liblibcli-lsa3-samba4.so=0", + "so:liblibcli-netlogon3-samba4.so=0", + "so:liblibsmb-samba4.so=0", + "so:libmsrpc3-samba4.so=0", + "so:libndr-samba4.so=0", + "so:libsamba-passdb.so.0=0.27.0", + "so:libtrusts-util-samba4.so=0" + ] + }, + "s6-networking": { + "versions": { + "2.3.0.2-r1": 1510260983 + }, + "origin": "s6-networking", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libs6.so.2.6", + "so:libs6dns.so.2.2", + "so:libskarnet.so.2.6", + "so:libtls.so.16" + ], + "provides": [ + "so:libs6net.so.2.3=2.3.0.2", + "so:libstls.so.2.3=2.3.0.2", + "cmd:minidentd", + "cmd:s6-clockadd", + "cmd:s6-clockview", + "cmd:s6-getservbyname", + "cmd:s6-ident-client", + "cmd:s6-sntpclock", + "cmd:s6-taiclock", + "cmd:s6-taiclockd", + "cmd:s6-tcpclient", + "cmd:s6-tcpserver", + "cmd:s6-tcpserver-access", + "cmd:s6-tcpserver4", + "cmd:s6-tcpserver4-socketbinder", + "cmd:s6-tcpserver4d", + "cmd:s6-tcpserver6", + "cmd:s6-tcpserver6-socketbinder", + "cmd:s6-tcpserver6d", + "cmd:s6-tlsc", + "cmd:s6-tlsclient", + "cmd:s6-tlsd", + "cmd:s6-tlsserver" + ] + }, + "perl-net-async-http": { + "versions": { + "0.40-r0": 1509480558 + }, + "origin": "perl-net-async-http", + "dependencies": [ + "perl-io-async", + "perl-future", + "perl-uri", + "perl-http-message", + "perl-struct-dumb" + ] + }, + "nagios-plugins": { + "versions": { + "2.2.1-r3": 1510288499 + }, + "origin": "nagios-plugins", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ] + }, + "appstream-glib": { + "versions": { + "0.6.3-r0": 1510067863 + }, + "origin": "appstream-glib", + "dependencies": [ + "so:libarchive.so.13", + "so:libc.musl-x86_64.so.1", + "so:libgcab-1.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libsoup-2.4.so.1", + "so:libuuid.so.1", + "so:libyaml-0.so.2" + ], + "provides": [ + "so:libappstream-glib.so.8=8.0.10", + "cmd:appstream-compose", + "cmd:appstream-util" + ] + }, + "font-arabic-misc": { + "versions": { + "1.0.3-r0": 1509495842 + }, + "origin": "font-arabic-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "autoconf": { + "versions": { + "2.69-r0": 1509456973 + }, + "origin": "autoconf", + "dependencies": [ + "m4", + "perl" + ], + "provides": [ + "cmd:autoconf", + "cmd:autoheader", + "cmd:autom4te", + "cmd:autoreconf", + "cmd:autoscan", + "cmd:autoupdate", + "cmd:ifnames" + ] + }, + "perl-specio": { + "versions": { + "0.36-r0": 1509481612 + }, + "origin": "perl-specio", + "dependencies": [ + "perl-test-needs", + "perl-devel-stacktrace", + "perl-eval-closure", + "perl-mro-compat", + "perl-role-tiny", + "perl-test-fatal", + "perl-module-runtime" + ] + }, + "nmap-doc": { + "versions": { + "7.60-r2": 1510261467, + "7.60-r3": 1572297244 + }, + "origin": "nmap" + }, + "rtapd-dbg": { + "versions": { + "1.7-r6": 1509491636 + }, + "origin": "rtapd", + "dependencies": [ + "rtnppd" + ] + }, + "sfic": { + "versions": { + "0.1.7-r6": 1509492421 + }, + "origin": "sfic", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtdb.so.1" + ], + "provides": [ + "cmd:sfic" + ] + }, + "abiword-plugins": { + "versions": { + "3.0.2-r1": 1510073355 + }, + "origin": "abiword", + "dependencies": [ + "abiword-plugin-applix", + "abiword-plugin-babelfish", + "abiword-plugin-bmp", + "abiword-plugin-clarisworks", + "abiword-plugin-collab", + "abiword-plugin-docbook", + "abiword-plugin-eml", + "abiword-plugin-epub", + "abiword-plugin-freetranslation", + "abiword-plugin-garble", + "abiword-plugin-gdict", + "abiword-plugin-gimp", + "abiword-plugin-google", + "abiword-plugin-hancom", + "abiword-plugin-hrtext", + "abiword-plugin-iscii", + "abiword-plugin-kword", + "abiword-plugin-latex", + "abiword-plugin-loadbindings", + "abiword-plugin-mht", + "abiword-plugin-mif", + "abiword-plugin-mswrite", + "abiword-plugin-openwriter", + "abiword-plugin-openxml", + "abiword-plugin-opml", + "abiword-plugin-paint", + "abiword-plugin-passepartout", + "abiword-plugin-pdb", + "abiword-plugin-pdf", + "abiword-plugin-presentation", + "abiword-plugin-s5", + "abiword-plugin-sdw", + "abiword-plugin-t602", + "abiword-plugin-urldict", + "abiword-plugin-wikipedia", + "abiword-plugin-wml", + "abiword-plugin-xslfo" + ] + }, + "libdvdread-doc": { + "versions": { + "5.0.3-r1": 1509480296 + }, + "origin": "libdvdread" + }, + "opennhrp": { + "versions": { + "0.14.1-r6": 1509490851 + }, + "origin": "opennhrp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2" + ], + "provides": [ + "cmd:opennhrp", + "cmd:opennhrpctl" + ] + }, + "kbproto-doc": { + "versions": { + "1.0.7-r2": 1509461960 + }, + "origin": "kbproto" + }, + "libbonobo-lang": { + "versions": { + "2.32.1-r6": 1510931481 + }, + "origin": "libbonobo" + }, + "libquvi-scripts-dev": { + "versions": { + "0.9.20131130-r0": 1509491408 + }, + "origin": "libquvi-scripts", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:libquvi-scripts-0.9=0.9.20131130" + ] + }, + "perl-class-inspector": { + "versions": { + "1.28-r1": 1510564482 + }, + "origin": "perl-class-inspector", + "dependencies": [ + "perl" + ] + }, + "busybox-initscripts": { + "versions": { + "3.1-r3": 1549616267 + }, + "origin": "busybox-initscripts", + "dependencies": [ + "busybox", + "openrc" + ] + }, + "xprop-doc": { + "versions": { + "1.2.1-r1": 1509491073 + }, + "origin": "xprop" + }, + "openldap-overlay-dyngroup": { + "versions": { + "2.4.45-r3": 1510258134, + "2.4.46-r0": 1565073940, + "2.4.48-r0": 1566900517 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "perl-io": { + "versions": { + "1.25-r4": 1509483995 + }, + "origin": "perl-io", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "ucarp": { + "versions": { + "1.5.2-r7": 1509494982 + }, + "origin": "ucarp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:ucarp" + ] + }, + "perl-pod-coverage": { + "versions": { + "0.23-r0": 1509470436 + }, + "origin": "perl-pod-coverage", + "dependencies": [ + "perl", + "perl-devel-symdump", + "perl-test-pod" + ], + "provides": [ + "cmd:pod_cover" + ] + }, + "perl-test-refcount-doc": { + "versions": { + "0.08-r0": 1509477778 + }, + "origin": "perl-test-refcount" + }, + "lua5.2-apk": { + "versions": { + "2.10.1-r0": 1536582152 + }, + "origin": "apk-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ] + }, + "libsecret-lang": { + "versions": { + "0.18.5-r0": 1509483962 + }, + "origin": "libsecret" + }, + "jwm-doc": { + "versions": { + "2.3.7-r0": 1510075431 + }, + "origin": "jwm" + }, + "libxklavier-dev": { + "versions": { + "5.4-r2": 1509475732 + }, + "origin": "libxklavier", + "dependencies": [ + "libxml2-dev", + "glib-dev", + "gettext-dev", + "libxkbfile-dev", + "libxklavier=5.4-r2", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libxklavier=5.4" + ] + }, + "mpc1-dev": { + "versions": { + "1.0.3-r1": 1509457077 + }, + "origin": "mpc1", + "dependencies": [ + "mpc1=1.0.3-r1" + ] + }, + "thunar-lang": { + "versions": { + "1.6.12-r0": 1510072625 + }, + "origin": "thunar", + "dependencies": [ + "desktop-file-utils", + "hicolor-icon-theme", + "shared-mime-info" + ] + }, + "x265": { + "versions": { + "2.5-r0": 1509482548 + }, + "origin": "x265", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libx265.so.130=130", + "cmd:x265" + ] + }, + "sic": { + "versions": { + "1.2-r0": 1509474248 + }, + "origin": "sic", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sic" + ] + }, + "qemu-doc": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu" + }, + "patchwork-uwsgi-nginx": { + "versions": { + "1.1.3-r0": 1509474238, + "1.1.3-r1": 1562223418 + }, + "origin": "patchwork", + "dependencies": [ + "nginx", + "uwsgi-python" + ] + }, + "graphicsmagick-zsh-completion": { + "versions": { + "5.4.2-r1": 1522503664 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "randrproto": { + "versions": { + "1.5.0-r2": 1509465995 + }, + "origin": "randrproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:randrproto=1.5.0" + ] + }, + "lua5.1-crypto": { + "versions": { + "0.3.2-r5": 1510261400 + }, + "origin": "lua-crypto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "py-gst0.10": { + "versions": { + "0.10.22-r0": 1510072577 + }, + "origin": "py-gst0.10", + "dependencies": [ + "py-gtk", + "py-gobject", + "pc:gstreamer-0.10", + "pc:pygobject-2.0", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-0.10.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstcontroller-0.10.so.0", + "so:libgstdataprotocol-0.10.so.0", + "so:libgstinterfaces-0.10.so.0", + "so:libgstnet-0.10.so.0", + "so:libgstpbutils-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgsttag-0.10.so.0", + "so:libgstvideo-0.10.so.0" + ], + "provides": [ + "pc:gst-python-0.10=0.10.22" + ] + }, + "ddate": { + "versions": { + "0.2.2-r0": 1509492725 + }, + "origin": "ddate", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ddate" + ] + }, + "collectd-snmp": { + "versions": { + "5.7.2-r0": 1510069643 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.30" + ] + }, + "arpon-doc": { + "versions": { + "3.0-r0": 1509495304 + }, + "origin": "arpon" + }, + "sipsak-doc": { + "versions": { + "0.9.6-r9": 1510260059 + }, + "origin": "sipsak" + }, + "xfce4-session": { + "versions": { + "4.12.1-r2": 1510074580 + }, + "origin": "xfce4-session", + "dependencies": [ + "hicolor-icon-theme", + "iceauth", + "dbus-x11", + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libwnck-1.so.22", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "so:libxfsm-4.6.so.0=0.0.0", + "cmd:startxfce4", + "cmd:xfce4-session", + "cmd:xfce4-session-logout", + "cmd:xfce4-session-settings", + "cmd:xflock4" + ] + }, + "mysql-client": { + "versions": { + "10.1.38-r1": 1551187995, + "10.1.40-r0": 1560354890, + "10.1.41-r0": 1565163115 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-client" + ] + }, + "nagios-plugins-real": { + "versions": { + "2.2.1-r3": 1510288497 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "m4-doc": { + "versions": { + "1.4.18-r0": 1509456653 + }, + "origin": "m4" + }, + "weechat-perl": { + "versions": { + "1.9.1-r1": 1509530223 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "bluez-libs": { + "versions": { + "5.47-r3": 1510069786 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libbluetooth.so.3=3.18.16" + ] + }, + "faad2-dev": { + "versions": { + "2.7-r7": 1509480952, + "2.9.0-r0": 1571918409 + }, + "origin": "faad2", + "dependencies": [ + "faad2=2.9.0-r0" + ] + }, + "xfce4-whiskermenu-plugin-lang": { + "versions": { + "1.7.3-r0": 1510073068 + }, + "origin": "xfce4-whiskermenu-plugin" + }, + "icu": { + "versions": { + "59.1-r1": 1509464844 + }, + "origin": "icu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libicui18n.so.59", + "so:libicuio.so.59", + "so:libicutu.so.59", + "so:libicuuc.so.59", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:derb", + "cmd:escapesrc", + "cmd:genbrk", + "cmd:genccode", + "cmd:gencfu", + "cmd:gencmn", + "cmd:gencnval", + "cmd:gendict", + "cmd:gennorm2", + "cmd:genrb", + "cmd:gensprep", + "cmd:icuinfo", + "cmd:icupkg", + "cmd:makeconv", + "cmd:pkgdata", + "cmd:uconv" + ] + }, + "xcmiscproto": { + "versions": { + "1.2.2-r2": 1509473766 + }, + "origin": "xcmiscproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xcmiscproto=1.2.2" + ] + }, + "spice-server": { + "versions": { + "0.14.1-r2": 1548934012 + }, + "origin": "spice", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcelt051.so.0", + "so:libcrypto.so.42", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libjpeg.so.8", + "so:libopus.so.0", + "so:libpixman-1.so.0", + "so:libsasl2.so.3", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:libspice-server.so.1=1.12.5" + ] + }, + "libbonobo-dev": { + "versions": { + "2.32.1-r6": 1510931477 + }, + "origin": "libbonobo", + "dependencies": [ + "gtk+2.0-dev", + "libidl-dev", + "orbit2-dev", + "popt-dev", + "libxml2-dev", + "libbonobo=2.32.1-r6", + "pc:ORBit-2.0", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pkgconfig" + ], + "provides": [ + "pc:bonobo-activation-2.0=2.32.1", + "pc:libbonobo-2.0=2.32.1" + ] + }, + "squid-lang-ca": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libgphoto2": { + "versions": { + "2.5.14-r0": 1509481939 + }, + "origin": "libgphoto2", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libexif.so.12", + "so:libjpeg.so.8", + "so:libltdl.so.7", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libgphoto2.so.6=6.0.0", + "so:libgphoto2_port.so.12=12.0.0" + ] + }, + "dnscache": { + "versions": { + "1.05-r47": 1509488767 + }, + "origin": "djbdns", + "dependencies": [ + "djbdns-common", + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dnscache", + "cmd:dnscache-conf" + ] + }, + "xmlindent-doc": { + "versions": { + "0.2.17-r0": 1509490829 + }, + "origin": "xmlindent" + }, + "groff-doc": { + "versions": { + "1.22.3-r2": 1509460767 + }, + "origin": "groff" + }, + "perl-yaml-tiny": { + "versions": { + "1.70-r1": 1510352923 + }, + "origin": "perl-yaml-tiny" + }, + "gstreamer-doc": { + "versions": { + "1.12.3-r0": 1509470920 + }, + "origin": "gstreamer" + }, + "fsarchiver": { + "versions": { + "0.8.2-r0": 1509485944 + }, + "origin": "fsarchiver", + "dependencies": [ + "so:libblkid.so.1", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libe2p.so.2", + "so:libext2fs.so.2", + "so:libgcrypt.so.20", + "so:liblzma.so.5", + "so:liblzo2.so.2", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:fsarchiver" + ] + }, + "rhash-doc": { + "versions": { + "1.3.5-r1": 1509461594 + }, + "origin": "rhash" + }, + "perl-text-reform-doc": { + "versions": { + "1.20-r0": 1509475836 + }, + "origin": "perl-text-reform" + }, + "lua-evdev": { + "versions": { + "2.2.1-r1": 1509488871 + }, + "origin": "lua-evdev" + }, + "twm-doc": { + "versions": { + "1.0.9-r2": 1509493987 + }, + "origin": "twm" + }, + "nmap-ncat": { + "versions": { + "7.60-r2": 1510261467, + "7.60-r3": 1572297245 + }, + "origin": "nmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpcap.so.1", + "so:libssl.so.44" + ], + "provides": [ + "cmd:ncat" + ] + }, + "samba-doc": { + "versions": { + "4.7.6-r3": 1555491786 + }, + "origin": "samba" + }, + "samba-client": { + "versions": { + "4.7.6-r3": 1555491787 + }, + "origin": "samba", + "dependencies": [ + "samba-common=4.7.6-r3", + "so:libCHARSET3-samba4.so", + "so:libaddns-samba4.so", + "so:libarchive.so.13", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcli-spoolss-samba4.so", + "so:libcliauth-samba4.so", + "so:libcmdline-credentials-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libevents-samba4.so", + "so:libformw.so.6", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:liblibcli-lsa3-samba4.so", + "so:liblibcli-netlogon3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libncursesw.so.6", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libpanelw.so.6", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libregistry-samba4.so", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb-common-samba4.so", + "so:libsecrets3-samba4.so", + "so:libserver-role-samba4.so", + "so:libsmbclient-raw-samba4.so", + "so:libsmbclient.so.0", + "so:libsmbconf.so.0", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtrusts-util-samba4.so", + "so:libutil-cmdline-samba4.so", + "so:libutil-reg-samba4.so", + "so:libutil-tdb-samba4.so" + ], + "provides": [ + "cmd:cifsdd", + "cmd:dbwrap_tool", + "cmd:findsmb", + "cmd:mvxattr", + "cmd:nmblookup", + "cmd:oLschema2ldif", + "cmd:regdiff", + "cmd:regpatch", + "cmd:regshell", + "cmd:regtree", + "cmd:rpcclient", + "cmd:samba-regedit", + "cmd:sharesec", + "cmd:smbcacls", + "cmd:smbclient", + "cmd:smbcquotas", + "cmd:smbget", + "cmd:smbprint", + "cmd:smbspool", + "cmd:smbtar", + "cmd:smbtree" + ] + }, + "gnome-vfs": { + "versions": { + "2.24.4-r6": 1510931441 + }, + "origin": "gnome-vfs", + "dependencies": [ + "gnome-mime-data", + "/bin/sh", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libfam.so.0", + "so:libgconf-2.so.4", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libgnomevfs-2.so.0=0.2400.4", + "cmd:gnomevfs-cat", + "cmd:gnomevfs-copy", + "cmd:gnomevfs-df", + "cmd:gnomevfs-info", + "cmd:gnomevfs-ls", + "cmd:gnomevfs-mkdir", + "cmd:gnomevfs-monitor", + "cmd:gnomevfs-mv", + "cmd:gnomevfs-rm" + ] + }, + "libxfce4util-lang": { + "versions": { + "4.12.1-r4": 1509468547 + }, + "origin": "libxfce4util" + }, + "automake-doc": { + "versions": { + "1.15.1-r0": 1509456977 + }, + "origin": "automake" + }, + "py-parsing": { + "versions": { + "2.2.0-r0": 1509475646 + }, + "origin": "py-parsing" + }, + "libaio": { + "versions": { + "0.3.110-r1": 1509471437 + }, + "origin": "libaio", + "provides": [ + "so:libaio.so.1=1.0.1" + ] + }, + "orage": { + "versions": { + "4.12.1-r1": 1510073570 + }, + "origin": "orage", + "dependencies": [ + "hicolor-icon-theme", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libical.so.2", + "so:libicalss.so.2", + "so:libintl.so.8", + "so:libnotify.so.4", + "so:libpango-1.0.so.0", + "so:libpopt.so.0", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4util.so.7" + ], + "provides": [ + "cmd:globaltime", + "cmd:orage", + "cmd:tz_convert" + ] + }, + "xfce4-notifyd-doc": { + "versions": { + "0.3.6-r0": 1510075458 + }, + "origin": "xfce4-notifyd" + }, + "perl-datetime-format-natural": { + "versions": { + "1.05-r1": 1511384618 + }, + "origin": "perl-datetime-format-natural", + "dependencies": [ + "perl-datetime-timezone", + "perl-clone", + "perl-params-validate", + "perl-list-moreutils", + "perl-datetime", + "perl-boolean" + ], + "provides": [ + "cmd:dateparse" + ] + }, + "yajl": { + "versions": { + "2.1.0-r0": 1509475323 + }, + "origin": "yajl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libyajl.so.2=2.1.0" + ] + }, + "perl-test-script-doc": { + "versions": { + "1.23-r0": 1510588920 + }, + "origin": "perl-test-script" + }, + "gdk-pixbuf-dev": { + "versions": { + "2.36.10-r0": 1509465514 + }, + "origin": "gdk-pixbuf", + "dependencies": [ + "gdk-pixbuf=2.36.10-r0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:libpng16", + "pkgconfig" + ], + "provides": [ + "pc:gdk-pixbuf-2.0=2.36.10", + "pc:gdk-pixbuf-xlib-2.0=2.36.10" + ] + }, + "py-libproxy": { + "versions": { + "0.4.15-r0": 1509468258 + }, + "origin": "libproxy" + }, + "perl-gd": { + "versions": { + "2.67-r0": 1511805778 + }, + "origin": "perl-gd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ], + "provides": [ + "cmd:bdf2gdfont.pl" + ] + }, + "perl-filesys-notify-simple": { + "versions": { + "0.12-r0": 1510588883 + }, + "origin": "perl-filesys-notify-simple", + "dependencies": [ + "perl" + ] + }, + "pangomm-doc": { + "versions": { + "2.40.1-r0": 1509473003 + }, + "origin": "pangomm" + }, + "gvfs-archive": { + "versions": { + "1.34.1-r0": 1511430258, + "1.34.1-r1": 1563787223 + }, + "origin": "gvfs", + "dependencies": [ + "so:libarchive.so.13", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgvfscommon.so", + "so:libgvfsdaemon.so", + "so:libintl.so.8" + ] + }, + "acf-ppp": { + "versions": { + "0.5.0-r2": 1510074011 + }, + "origin": "acf-ppp", + "dependencies": [ + "acf-core", + "ppp" + ] + }, + "ed-doc": { + "versions": { + "1.14.2-r2": 1509465946 + }, + "origin": "ed" + }, + "heimdal-doc": { + "versions": { + "7.4.0-r2": 1514545900, + "7.4.0-r3": 1559659764, + "7.4.0-r4": 1562862335 + }, + "origin": "heimdal" + }, + "abiword-plugin-latex": { + "versions": { + "3.0.2-r1": 1510073363 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "sngtc_client": { + "versions": { + "1.3.7-r1": 1509481973 + }, + "origin": "sngtc_client", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libortp.so.10" + ], + "provides": [ + "so:libsngtc_node.so=0", + "cmd:sngtc_client" + ] + }, + "rsyslog-doc": { + "versions": { + "8.31.0-r0": 1512031980, + "8.31.0-r1": 1571767017 + }, + "origin": "rsyslog" + }, + "glib-static": { + "versions": { + "2.54.2-r0": 1509911133, + "2.54.2-r1": 1560764732 + }, + "origin": "glib", + "dependencies": [ + "gettext-static" + ] + }, + "acct": { + "versions": { + "6.6.4-r0": 1509462238 + }, + "origin": "acct", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ac", + "cmd:accton", + "cmd:dump-acct", + "cmd:dump-utmp", + "cmd:last", + "cmd:lastcomm", + "cmd:sa" + ] + }, + "espeak-dev": { + "versions": { + "1.48.04-r1": 1509472850 + }, + "origin": "espeak", + "dependencies": [ + "espeak=1.48.04-r1" + ] + }, + "perl-javascript-minifier-xs-doc": { + "versions": { + "0.11-r3": 1509495049 + }, + "origin": "perl-javascript-minifier-xs" + }, + "perl-file-which": { + "versions": { + "1.22-r0": 1509495220 + }, + "origin": "perl-file-which" + }, + "dhcp-dev": { + "versions": { + "4.3.5-r0": 1509496512 + }, + "origin": "dhcp" + }, + "cdw-doc": { + "versions": { + "0.8.1-r0": 1509491772 + }, + "origin": "cdw" + }, + "abiword-plugin-s5": { + "versions": { + "3.0.2-r1": 1510073368 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "perl-scalar-list-utils-doc": { + "versions": { + "1.49-r0": 1509485607 + }, + "origin": "perl-scalar-list-utils" + }, + "liboil": { + "versions": { + "0.3.17-r5": 1509471016 + }, + "origin": "liboil", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liboil-0.3.so.0=0.3.0", + "cmd:oil-bugreport" + ] + }, + "snappy": { + "versions": { + "1.1.4-r2": 1509483343 + }, + "origin": "snappy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libsnappy.so.1=1.3.1" + ] + }, + "abiword-plugin-collab": { + "versions": { + "3.0.2-r1": 1510073357 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libgtk-3.so.0", + "so:libstdc++.so.6", + "so:libxml2.so.2" + ] + }, + "gst-ffmpeg0.10": { + "versions": { + "0.10.13-r1": 1510076394 + }, + "origin": "gst-ffmpeg0.10", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-0.10.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstpbutils-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgstvideo-0.10.so.0", + "so:liborc-0.4.so.0", + "so:libz.so.1" + ] + }, + "perl-text-vfile-asdata-doc": { + "versions": { + "0.08-r0": 1509474104 + }, + "origin": "perl-text-vfile-asdata" + }, + "linux-hardened": { + "versions": { + "4.9.65-r1": 1511798484 + }, + "origin": "linux-hardened", + "dependencies": [ + "mkinitfs", + "linux-firmware" + ], + "provides": [ + "linux-grsec=4.9.65-r1" + ] + }, + "xkbcomp": { + "versions": { + "1.4.0-r2": 1509473697 + }, + "origin": "xkbcomp", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libxkbfile.so.1" + ], + "provides": [ + "cmd:xkbcomp" + ] + }, + "cryptsetup-dev": { + "versions": { + "1.7.5-r1": 1510261295 + }, + "origin": "cryptsetup", + "dependencies": [ + "cryptsetup-libs=1.7.5-r1", + "pkgconfig" + ], + "provides": [ + "pc:libcryptsetup=1.7.5" + ] + }, + "libzdb": { + "versions": { + "3.1-r1": 1509491013 + }, + "origin": "libzdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18", + "so:libpq.so.5", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libzdb.so.11=11.0.0" + ] + }, + "taskd-doc": { + "versions": { + "1.1.0-r4": 1509491726 + }, + "origin": "taskd" + }, + "stfl-dev": { + "versions": { + "0.24-r2": 1509491482 + }, + "origin": "stfl", + "dependencies": [ + "pkgconfig", + "stfl=0.24-r2" + ], + "provides": [ + "pc:stfl=0.24" + ] + }, + "binutils-doc": { + "versions": { + "2.30-r1": 1527754981, + "2.30-r2": 1565788943 + }, + "origin": "binutils" + }, + "perl-uri": { + "versions": { + "1.71-r1": 1509464365 + }, + "origin": "perl-uri", + "dependencies": [ + "perl" + ] + }, + "perl-convert-color-doc": { + "versions": { + "0.11-r0": 1509489722 + }, + "origin": "perl-convert-color" + }, + "sqsh-doc": { + "versions": { + "2.5.16.1-r2": 1509493717 + }, + "origin": "sqsh" + }, + "py-flask-oauthlib": { + "versions": { + "0.9.3-r2": 1509552815 + }, + "origin": "py-flask-oauthlib", + "dependencies": [ + "py-flask", + "py-requests-oauthlib" + ] + }, + "gnu-efi-dev": { + "versions": { + "3.0.4-r1": 1509482583 + }, + "origin": "gnu-efi", + "dependencies": [ + "gnu-efi" + ] + }, + "cabextract": { + "versions": { + "1.9-r0": 1543333648 + }, + "origin": "cabextract", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmspack.so.0" + ], + "provides": [ + "cmd:cabextract" + ] + }, + "libxext-dev": { + "versions": { + "1.3.3-r2": 1509464538 + }, + "origin": "libxext", + "dependencies": [ + "libxau-dev", + "libxext=1.3.3-r2", + "pc:x11", + "pc:xextproto", + "pkgconfig" + ], + "provides": [ + "pc:xext=1.3.3" + ] + }, + "libart-lgpl": { + "versions": { + "2.3.21-r5": 1509475376 + }, + "origin": "libart-lgpl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libart_lgpl_2.so.2=2.3.21" + ] + }, + "perl-doc": { + "versions": { + "5.26.3-r0": 1543940996 + }, + "origin": "perl" + }, + "enchant": { + "versions": { + "1.6.0-r12": 1509477683 + }, + "origin": "enchant", + "dependencies": [ + "so:libaspell.so.15", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libhunspell-1.6.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libenchant.so.1=1.6.0", + "cmd:enchant", + "cmd:enchant-lsmod" + ] + }, + "znc": { + "versions": { + "1.7.1-r0": 1531900840, + "1.7.1-r1": 1565877331 + }, + "origin": "znc", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libicuuc.so.59", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:znc" + ] + }, + "lua5.1-sql-odbc": { + "versions": { + "2.3.5-r1": 1509488829 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "hunspell-dev": { + "versions": { + "1.6.2-r1": 1509477667 + }, + "origin": "hunspell", + "dependencies": [ + "hunspell=1.6.2-r1", + "pkgconfig" + ], + "provides": [ + "pc:hunspell=1.6.2" + ] + }, + "miniperl": { + "versions": { + "5.26.3-r0": 1543940997 + }, + "origin": "perl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:miniperl" + ] + }, + "libgfortran": { + "versions": { + "6.4.0-r5": 1509458077 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libquadmath.so.0" + ], + "provides": [ + "so:libgfortran.so.3=3.0.0" + ] + }, + "xf86-video-dummy-doc": { + "versions": { + "0.3.8-r0": 1510072534 + }, + "origin": "xf86-video-dummy" + }, + "mtr-doc": { + "versions": { + "0.92-r0": 1510072768 + }, + "origin": "mtr" + }, + "dhcpcd": { + "versions": { + "6.11.5-r1": 1509494693 + }, + "origin": "dhcpcd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dhcpcd" + ] + }, + "xvfb": { + "versions": { + "1.19.5-r1": 1540838460 + }, + "origin": "xorg-server", + "dependencies": [ + "font-misc-misc", + "font-cursor-misc", + "xkeyboard-config", + "xkbcomp", + "xinit", + "so:libGL.so.1", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.0.0", + "so:libpixman-1.so.0" + ], + "provides": [ + "cmd:Xvfb" + ] + }, + "sshguard": { + "versions": { + "2.0.0-r1": 1509483910 + }, + "origin": "sshguard", + "dependencies": [ + "iptables", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sshguard" + ] + }, + "docbook-xsl": { + "versions": { + "1.79.1-r1": 1509459379 + }, + "origin": "docbook-xsl", + "dependencies": [ + "libxml2-utils", + "libxslt", + "docbook-xml", + "/bin/sh" + ] + }, + "xf86-video-rendition": { + "versions": { + "4.2.6-r1": 1510074858 + }, + "origin": "xf86-video-rendition", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "pam-pgsql-doc": { + "versions": { + "0.7.3.2-r0": 1509494687 + }, + "origin": "pam-pgsql" + }, + "perl-business-hours": { + "versions": { + "0.12-r1": 1511889850 + }, + "origin": "perl-business-hours", + "dependencies": [ + "perl-set-intspan" + ] + }, + "font-xfree86-type1": { + "versions": { + "1.0.4-r0": 1509476739 + }, + "origin": "font-xfree86-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "c-ares-dev": { + "versions": { + "1.13.0-r0": 1509461299 + }, + "origin": "c-ares", + "dependencies": [ + "c-ares=1.13.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcares=1.13.0" + ] + }, + "aconf-doc": { + "versions": { + "0.6.5-r0": 1510073707 + }, + "origin": "aconf" + }, + "acf-iptables": { + "versions": { + "0.7.1-r2": 1510070011 + }, + "origin": "acf-iptables", + "dependencies": [ + "acf-core", + "iptables" + ] + }, + "gnutls-c++": { + "versions": { + "3.6.1-r0": 1509465349 + }, + "origin": "gnutls", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgnutlsxx.so.28=28.1.0" + ] + }, + "xsetmode-doc": { + "versions": { + "1.0.0-r4": 1509490726 + }, + "origin": "xsetmode" + }, + "lua-penlight": { + "versions": { + "1.5.4-r0": 1509485592 + }, + "origin": "lua-penlight" + }, + "lua5.2-cqueues": { + "versions": { + "20171014-r0": 1510619111 + }, + "origin": "lua-cqueues", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "compositeproto-doc": { + "versions": { + "0.4.2-r4": 1509465977 + }, + "origin": "compositeproto" + }, + "abiword-plugin-clarisworks": { + "versions": { + "3.0.2-r1": 1510073357 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "partimage": { + "versions": { + "0.6.9-r4": 1510260489 + }, + "origin": "partimage", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libnewt.so.0.52", + "so:libslang.so.2", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:partimage", + "cmd:partimaged" + ] + }, + "libcap-doc": { + "versions": { + "2.25-r1": 1509459572 + }, + "origin": "libcap" + }, + "dev86-doc": { + "versions": { + "0.16.21-r0": 1509479308 + }, + "origin": "dev86" + }, + "python3-dev": { + "versions": { + "3.6.8-r0": 1555928808, + "3.6.9-r0": 1571233994, + "3.6.9-r1": 1571314638 + }, + "origin": "python3", + "dependencies": [ + "pkgconfig", + "python3=3.6.9-r1" + ], + "provides": [ + "pc:python-3.6=3.6", + "pc:python-3.6m=3.6", + "pc:python3=3.6", + "cmd:python3-config", + "cmd:python3.6-config", + "cmd:python3.6m-config" + ] + }, + "py-nose-doc": { + "versions": { + "1.3.7-r2": 1509552699 + }, + "origin": "py-nose" + }, + "libnetfilter_cthelper-dev": { + "versions": { + "1.0.0-r0": 1509469238 + }, + "origin": "libnetfilter_cthelper", + "dependencies": [ + "libnetfilter_cthelper=1.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_cthelper=1.0.0" + ] + }, + "sylpheed": { + "versions": { + "3.6.0-r1": 1510588302 + }, + "origin": "sylpheed", + "dependencies": [ + "pinentry-gtk", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcrypto.so.42", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgpgme.so.11", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libssl.so.44" + ], + "provides": [ + "so:libsylph-0.so.1=1.2.1", + "so:libsylpheed-plugin-0.so.1=1.2.1", + "cmd:sylpheed" + ] + }, + "py-itsdangerous": { + "versions": { + "0.24-r3": 1509551859 + }, + "origin": "py-itsdangerous" + }, + "py3-phonenumbers": { + "versions": { + "8.8.6-r0": 1511018982 + }, + "origin": "py-phonenumbers", + "dependencies": [ + "python3" + ] + }, + "perl-convert-uulib-doc": { + "versions": { + "1.5-r2": 1509483418 + }, + "origin": "perl-convert-uulib" + }, + "haserl-lua5.2": { + "versions": { + "0.9.35-r1": 1509468296 + }, + "origin": "haserl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua-5.2.so.0" + ], + "provides": [ + "cmd:haserl-lua5.2" + ] + }, + "libiec61883-utils": { + "versions": { + "1.2.0-r1": 1509470083 + }, + "origin": "libiec61883", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libiec61883.so.0", + "so:libraw1394.so.11" + ], + "provides": [ + "cmd:plugctl", + "cmd:plugreport" + ] + }, + "snort-doc": { + "versions": { + "2.9.11-r0": 1510848484 + }, + "origin": "snort" + }, + "perl-try-tiny": { + "versions": { + "0.22-r1": 1509464413 + }, + "origin": "perl-try-tiny" + }, + "lua5.1-microlight": { + "versions": { + "1.1.1-r2": 1509480781 + }, + "origin": "lua-microlight" + }, + "dahdi-tools-dev": { + "versions": { + "2.11.1-r0": 1509476155 + }, + "origin": "dahdi-tools", + "dependencies": [ + "bsd-compat-headers", + "linux-headers", + "dahdi-linux-dev", + "newt-dev", + "dahdi-tools=2.11.1-r0" + ] + }, + "farstream-doc": { + "versions": { + "0.2.8-r2": 1510931394 + }, + "origin": "farstream" + }, + "libqrencode": { + "versions": { + "4.0.0-r0": 1509482400 + }, + "origin": "libqrencode", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpng16.so.16" + ], + "provides": [ + "so:libqrencode.so.4=4.0.0", + "cmd:qrencode" + ] + }, + "kamailio-snmpstats": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.30", + "so:libnetsnmpagent.so.30" + ] + }, + "librevenge": { + "versions": { + "0.0.4-r0": 1509495169 + }, + "origin": "librevenge", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:librevenge-0.0.so.0=0.0.4", + "so:librevenge-generators-0.0.so.0=0.0.4", + "so:librevenge-stream-0.0.so.0=0.0.4" + ] + }, + "libtool-doc": { + "versions": { + "2.4.6-r4": 1509456826 + }, + "origin": "libtool" + }, + "htop": { + "versions": { + "2.0.2-r0": 1509494929 + }, + "origin": "htop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:htop" + ] + }, + "libdvdread": { + "versions": { + "5.0.3-r1": 1509480296 + }, + "origin": "libdvdread", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdvdcss.so.2" + ], + "provides": [ + "so:libdvdread.so.4=4.2.0" + ] + }, + "acf-alpine-conf": { + "versions": { + "0.9.0-r3": 1510076342 + }, + "origin": "acf-alpine-conf", + "dependencies": [ + "acf-core", + "lua-posix", + "libressl" + ] + }, + "mercurial-bash-completion": { + "versions": { + "4.5.2-r0": 1532937173, + "4.5.2-r1": 1563792023 + }, + "origin": "mercurial" + }, + "py-tracing": { + "versions": { + "0.8-r1": 1509552705 + }, + "origin": "py-tracing", + "dependencies": [ + "python2" + ] + }, + "coova-chilli": { + "versions": { + "1.3.2-r2": 1510287299 + }, + "origin": "coova-chilli", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "so:libbstring.so.0=0.0.0", + "so:libchilli.so.0=0.0.0", + "cmd:chilli", + "cmd:chilli_opt", + "cmd:chilli_query", + "cmd:chilli_radconfig", + "cmd:chilli_redir", + "cmd:chilli_response", + "cmd:chilli_script" + ] + }, + "py-pynacl": { + "versions": { + "1.2.0-r0": 1510088490 + }, + "origin": "py-pynacl" + }, + "libverto-libevent": { + "versions": { + "0.3.0-r0": 1509469586 + }, + "origin": "libverto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libverto.so.1" + ], + "provides": [ + "so:libverto-libevent.so.1=1.0.0" + ] + }, + "qemu-sh4eb": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sh4eb" + ] + }, + "usb-modeswitch": { + "versions": { + "2.5.1-r0": 1509480148 + }, + "origin": "usb-modeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "cmd:usb_modeswitch" + ] + }, + "git-daemon": { + "versions": { + "2.15.3-r0": 1540401293, + "2.15.4-r0": 1576015190 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0", + "so:libc.musl-x86_64.so.1", + "so:libpcre2-8.so.0", + "so:libz.so.1" + ] + }, + "gawk": { + "versions": { + "4.2.0-r0": 1509457020 + }, + "origin": "gawk", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:awk", + "cmd:gawk", + "cmd:gawk-4.2.0" + ] + }, + "thunar-vcs-plugin-git": { + "versions": { + "0.1.4-r6": 1510314590 + }, + "origin": "thunar-vcs-plugin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libxfce4util.so.7" + ] + }, + "libsoup": { + "versions": { + "2.60.2-r0": 1509480670 + }, + "origin": "libsoup", + "dependencies": [ + "glib-networking", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libsqlite3.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libsoup-2.4.so.1=1.8.0", + "so:libsoup-gnome-2.4.so.1=1.8.0" + ] + }, + "lua5.2-bitlib": { + "versions": { + "5.3.0-r0": 1509468307 + }, + "origin": "lua-bitlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libusb-dev": { + "versions": { + "1.0.21-r0": 1509470805 + }, + "origin": "libusb", + "dependencies": [ + "libusb=1.0.21-r0", + "pkgconfig" + ], + "provides": [ + "pc:libusb-1.0=1.0.21" + ] + }, + "openvpn": { + "versions": { + "2.4.4-r1": 1510259339 + }, + "origin": "openvpn", + "dependencies": [ + "iproute2", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:liblzo2.so.2", + "so:libssl.so.44" + ], + "provides": [ + "cmd:openvpn" + ] + }, + "perl-carp-doc": { + "versions": { + "1.38-r0": 1509483987 + }, + "origin": "perl-carp" + }, + "xfce4-terminal-doc": { + "versions": { + "0.6.3-r1": 1510074612 + }, + "origin": "xfce4-terminal" + }, + "perl-crypt-eksblowfish-doc": { + "versions": { + "0.009-r4": 1509470468 + }, + "origin": "perl-crypt-eksblowfish" + }, + "lua5.1-ldbus": { + "versions": { + "20150430-r2": 1509492574 + }, + "origin": "lua-ldbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ] + }, + "gapk": { + "versions": { + "0.1-r1": 1510068290 + }, + "origin": "gapk", + "dependencies": [ + "apk-tools", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0" + ], + "provides": [ + "cmd:gapk" + ] + }, + "tzdata": { + "versions": { + "2019a-r0": 1556626900, + "2019b-r0": 1566548308, + "2019c-r0": 1571319267 + }, + "origin": "tzdata", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:posixtz", + "cmd:zdump", + "cmd:zic" + ] + }, + "uwsgi-pam": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0" + ] + }, + "perl-mail-spamassassin-doc": { + "versions": { + "3.4.1-r2": 1509517006 + }, + "origin": "perl-mail-spamassassin" + }, + "igmpproxy-doc": { + "versions": { + "0.1-r5": 1509489516 + }, + "origin": "igmpproxy" + }, + "uwsgi-cheaper_busyness": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "qca": { + "versions": { + "2.1.3-r4": 1510288320 + }, + "origin": "qca", + "dependencies": [ + "so:libQtCore.so.4", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libsasl2.so.3", + "so:libssl.so.44", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libqca.so.2=2.1.3", + "cmd:mozcerts", + "cmd:qcatool" + ] + }, + "f2fs-tools-libs": { + "versions": { + "1.6.1-r0": 1509483524 + }, + "origin": "f2fs-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libf2fs.so.0=0.0.1", + "so:libf2fs_format.so.0=0.0.1" + ] + }, + "libvirt": { + "versions": { + "3.9.0-r1": 1510088352, + "5.5.0-r0": 1562165541 + }, + "origin": "libvirt", + "dependencies": [ + "lvm2", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libparted.so.2", + "so:libtirpc.so.3", + "so:libvirt.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:virtlockd", + "cmd:virtlogd" + ] + }, + "glade-lang": { + "versions": { + "3.20.1-r0": 1510073638 + }, + "origin": "glade" + }, + "xtables-addons-hardened": { + "versions": { + "4.9.65-r1": 1511799238 + }, + "origin": "xtables-addons-hardened", + "dependencies": [ + "linux-hardened=4.9.65-r1" + ], + "provides": [ + "xtables-addons-grsec=4.9.65-r1" + ] + }, + "minicom-lang": { + "versions": { + "2.7.1-r0": 1509485710 + }, + "origin": "minicom" + }, + "font-misc-cyrillic": { + "versions": { + "1.0.3-r0": 1509494291 + }, + "origin": "font-misc-cyrillic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "apr-util-dbd_sqlite3": { + "versions": { + "1.6.1-r1": 1510285059 + }, + "origin": "apr-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "libxrender-doc": { + "versions": { + "0.9.10-r2": 1509462094 + }, + "origin": "libxrender" + }, + "libebml": { + "versions": { + "1.3.5-r0": 1509479829, + "1.3.5-r1": 1564417272 + }, + "origin": "libebml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libebml.so.4=4.0.0" + ] + }, + "squid-lang-ka": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-text-wrapper": { + "versions": { + "1.04-r0": 1509490816 + }, + "origin": "perl-text-wrapper" + }, + "libksba-dev": { + "versions": { + "1.3.5-r0": 1509472890 + }, + "origin": "libksba", + "dependencies": [ + "libksba=1.3.5-r0" + ], + "provides": [ + "cmd:ksba-config" + ] + }, + "swfdec": { + "versions": { + "0.9.2-r0": 1510073747 + }, + "origin": "swfdec", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstpbutils-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgtk-x11-2.0.so.0", + "so:liboil-0.3.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libsoup-2.4.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libswfdec-0.9.so.2=2.0.0", + "so:libswfdec-gtk-0.9.so.2=2.0.0" + ] + }, + "py2-purl": { + "versions": { + "1.3.1-r0": 1509483541 + }, + "origin": "py-purl", + "dependencies": [ + "py2-six", + "python2" + ] + }, + "sems-doc": { + "versions": { + "1.6.0-r6": 1510260895 + }, + "origin": "sems" + }, + "libburn-dev": { + "versions": { + "1.4.8-r0": 1509462199 + }, + "origin": "libburn", + "dependencies": [ + "libburn=1.4.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:libburn-1=1.4.8" + ] + }, + "postfix-ldap": { + "versions": { + "3.2.4-r1": 1510285340 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ], + "provides": [ + "so:postfix-ldap.so=0" + ] + }, + "cdparanoia-libs": { + "versions": { + "10.2-r7": 1509471029 + }, + "origin": "cdparanoia", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcdda_interface.so.0=0.10.2", + "so:libcdda_paranoia.so.0=0.10.2" + ] + }, + "axel-doc": { + "versions": { + "2.4-r1": 1509494657 + }, + "origin": "axel" + }, + "lm_sensors": { + "versions": { + "3.4.0-r4": 1509475444 + }, + "origin": "lm_sensors", + "dependencies": [ + "bash", + "sysfsutils", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsensors.so.4=4.4.0", + "cmd:fancontrol", + "cmd:isadump", + "cmd:isaset", + "cmd:pwmconfig", + "cmd:sensors" + ] + }, + "kamailio-xmpp": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "libpciaccess": { + "versions": { + "0.13.5-r1": 1509466089 + }, + "origin": "libpciaccess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpciaccess.so.0=0.11.1" + ] + }, + "perl-log-dispatch-doc": { + "versions": { + "2.67-r0": 1510953098 + }, + "origin": "perl-log-dispatch" + }, + "isl-dev": { + "versions": { + "0.18-r0": 1509456966 + }, + "origin": "isl", + "dependencies": [ + "gmp-dev", + "isl=0.18-r0", + "pkgconfig" + ], + "provides": [ + "pc:isl=0.18" + ] + }, + "openobex-dev": { + "versions": { + "1.7.2-r1": 1510071733 + }, + "origin": "openobex", + "dependencies": [ + "libusb-dev", + "openobex=1.7.2-r1", + "pc:libusb-1.0", + "pkgconfig" + ], + "provides": [ + "pc:openobex=1.7.2" + ] + }, + "ghostscript-dev": { + "versions": { + "9.26-r2": 1554362768, + "9.26-r3": 1565700439, + "9.26-r4": 1571234493 + }, + "origin": "ghostscript", + "dependencies": [ + "ghostscript=9.26-r4", + "pkgconfig" + ], + "provides": [ + "pc:ijs=0.35" + ] + }, + "perl-class-mix": { + "versions": { + "0.005-r0": 1509470460 + }, + "origin": "perl-class-mix", + "dependencies": [ + "perl-params-classify" + ] + }, + "gtk+2.0": { + "versions": { + "2.24.31-r0": 1510066782 + }, + "origin": "gtk+2.0", + "dependencies": [ + "shared-mime-info", + "gtk-update-icon-cache", + "/bin/sh", + "so:libX11.so.6", + "so:libXcomposite.so.1", + "so:libXcursor.so.1", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXi.so.6", + "so:libXrandr.so.2", + "so:libXrender.so.1", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcups.so.2", + "so:libfontconfig.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0" + ], + "provides": [ + "so:libgailutil.so.18=18.0.1", + "so:libgdk-x11-2.0.so.0=0.2400.31", + "so:libgtk-x11-2.0.so.0=0.2400.31", + "cmd:gtk-query-immodules-2.0" + ] + }, + "perl-fcgi-doc": { + "versions": { + "0.78-r1": 1509474656 + }, + "origin": "perl-fcgi" + }, + "acf-pingu": { + "versions": { + "0.4.0-r2": 1510074967 + }, + "origin": "acf-pingu", + "dependencies": [ + "acf-core", + "pingu" + ] + }, + "perl-http-body": { + "versions": { + "1.17-r0": 1509481772 + }, + "origin": "perl-http-body", + "dependencies": [ + "perl", + "perl-http-message", + "perl-uri" + ] + }, + "lua-socket": { + "versions": { + "3.0_rc1_git20160306-r2": 1509480763 + }, + "origin": "lua-socket" + }, + "freeradius-doc": { + "versions": { + "3.0.15-r4": 1556202795, + "3.0.15-r5": 1566310602 + }, + "origin": "freeradius" + }, + "perl-font-afm-doc": { + "versions": { + "1.20-r0": 1509481557 + }, + "origin": "perl-font-afm" + }, + "py-ediarpc": { + "versions": { + "0.3-r0": 1509493833 + }, + "origin": "py-ediarpc", + "dependencies": [ + "python2" + ] + }, + "py3-country": { + "versions": { + "17.9.23-r0": 1509493919 + }, + "origin": "py-country", + "dependencies": [ + "python3" + ] + }, + "dtc-dev": { + "versions": { + "1.4.4-r0": 1509476728 + }, + "origin": "dtc", + "dependencies": [ + "libfdt=1.4.4-r0" + ] + }, + "herbstluftwm": { + "versions": { + "0.7.0-r1": 1509551853 + }, + "origin": "herbstluftwm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:dmenu_run_hlwm", + "cmd:herbstclient", + "cmd:herbstluftwm" + ] + }, + "util-macros": { + "versions": { + "1.19.1-r1": 1509461801 + }, + "origin": "util-macros" + }, + "lua5.1-mosquitto": { + "versions": { + "0.2-r1": 1509496426 + }, + "origin": "lua-mosquitto", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ] + }, + "ppp-chat": { + "versions": { + "2.4.7-r5": 1509480126 + }, + "origin": "ppp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chat" + ] + }, + "recode-dev": { + "versions": { + "3.6-r1": 1509485758 + }, + "origin": "recode", + "dependencies": [ + "recode=3.6-r1" + ] + }, + "gengetopt-doc": { + "versions": { + "2.22.6-r2": 1509488507 + }, + "origin": "gengetopt" + }, + "bcache-tools": { + "versions": { + "1.0.8-r1": 1509495229 + }, + "origin": "bcache-tools", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:bcache-super-show", + "cmd:make-bcache" + ] + }, + "uwsgi-forkptyrouter": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-ecdsa": { + "versions": { + "0.13-r5": 1509494956 + }, + "origin": "py-ecdsa", + "dependencies": [ + "py2-crypto" + ] + }, + "jansson": { + "versions": { + "2.10-r0": 1509472129 + }, + "origin": "jansson", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libjansson.so.4=4.10.0" + ] + }, + "lua5.1-yaml": { + "versions": { + "1.1.2-r1": 1509488584 + }, + "origin": "lua-yaml", + "dependencies": [ + "lua5.1", + "lua5.1-lub", + "so:libc.musl-x86_64.so.1" + ] + }, + "abiword-plugin-mht": { + "versions": { + "3.0.2-r1": 1510073363 + }, + "origin": "abiword" + }, + "speedtest-cli": { + "versions": { + "1.0.7-r0": 1509540426 + }, + "origin": "speedtest-cli", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:speedtest", + "cmd:speedtest-cli" + ] + }, + "py2-dateutil": { + "versions": { + "2.6.1-r0": 1509475853 + }, + "origin": "py-dateutil", + "dependencies": [ + "py2-six", + "python2" + ] + }, + "gstreamer0.10-dev": { + "versions": { + "0.10.36-r2": 1509470980 + }, + "origin": "gstreamer0.10", + "dependencies": [ + "glib-dev", + "libxml2-dev", + "gstreamer0.10=0.10.36-r2", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-0.10=0.10.36", + "pc:gstreamer-base-0.10=0.10.36", + "pc:gstreamer-check-0.10=0.10.36", + "pc:gstreamer-controller-0.10=0.10.36", + "pc:gstreamer-dataprotocol-0.10=0.10.36", + "pc:gstreamer-net-0.10=0.10.36" + ] + }, + "lxc-templates": { + "versions": { + "2.1.1-r3": 1533578833 + }, + "origin": "lxc", + "dependencies": [ + "bash", + "tar" + ] + }, + "xf86-video-sunleo": { + "versions": { + "1.2.2-r0": 1510074827 + }, + "origin": "xf86-video-sunleo", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "glib-doc": { + "versions": { + "2.54.2-r0": 1509911132, + "2.54.2-r1": 1560764732 + }, + "origin": "glib" + }, + "b43-fwcutter-doc": { + "versions": { + "019-r0": 1509495308 + }, + "origin": "b43-fwcutter" + }, + "giflib": { + "versions": { + "5.1.4-r1": 1509470482 + }, + "origin": "giflib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgif.so.7=7.0.0" + ] + }, + "kamailio-dbtext": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "kamailio-db", + "so:libc.musl-x86_64.so.1", + "so:libsrdb1.so.1" + ] + }, + "xineramaproto": { + "versions": { + "1.2.1-r3": 1509468238 + }, + "origin": "xineramaproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xineramaproto=1.2.1" + ] + }, + "collectd-dns": { + "versions": { + "5.7.2-r0": 1510069655 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ] + }, + "xf86-video-vesa-doc": { + "versions": { + "2.3.4-r2": 1510074818 + }, + "origin": "xf86-video-vesa" + }, + "perl-net-dns-resolver-programmable-doc": { + "versions": { + "0.003-r2": 1509475479 + }, + "origin": "perl-net-dns-resolver-programmable" + }, + "gconf-doc": { + "versions": { + "3.2.6-r1": 1510928342 + }, + "origin": "gconf" + }, + "font-jis-misc": { + "versions": { + "1.0.3-r0": 1509495351 + }, + "origin": "font-jis-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "gtk-engines-thinice": { + "versions": { + "2.21.0-r2": 1510071725 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "groff": { + "versions": { + "1.22.3-r2": 1509460768 + }, + "origin": "groff", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:addftinfo", + "cmd:afmtodit", + "cmd:chem", + "cmd:eqn", + "cmd:eqn2graph", + "cmd:gdiffmk", + "cmd:glilypond", + "cmd:gperl", + "cmd:gpinyin", + "cmd:grap2graph", + "cmd:grn", + "cmd:grodvi", + "cmd:groff", + "cmd:groffer", + "cmd:grog", + "cmd:grolbp", + "cmd:grolj4", + "cmd:gropdf", + "cmd:grops", + "cmd:grotty", + "cmd:hpftodit", + "cmd:indxbib", + "cmd:lkbib", + "cmd:lookbib", + "cmd:mmroff", + "cmd:neqn", + "cmd:nroff", + "cmd:pdfmom", + "cmd:pdfroff", + "cmd:pfbtops", + "cmd:pic", + "cmd:pic2graph", + "cmd:post-grohtml", + "cmd:pre-grohtml", + "cmd:preconv", + "cmd:refer", + "cmd:roff2dvi", + "cmd:roff2html", + "cmd:roff2pdf", + "cmd:roff2ps", + "cmd:roff2text", + "cmd:roff2x", + "cmd:soelim", + "cmd:tbl", + "cmd:tfmtodit", + "cmd:troff" + ] + }, + "unbound-doc": { + "versions": { + "1.6.7-r1": 1510588258 + }, + "origin": "unbound" + }, + "nmap-scripts": { + "versions": { + "7.60-r2": 1510261467, + "7.60-r3": 1572297244 + }, + "origin": "nmap", + "dependencies": [ + "nmap-nselibs" + ] + }, + "perl-posix-strftime-compiler": { + "versions": { + "0.41-r0": 1509481800 + }, + "origin": "perl-posix-strftime-compiler", + "dependencies": [ + "tzdata" + ] + }, + "perl-cps-doc": { + "versions": { + "0.18-r0": 1509477797 + }, + "origin": "perl-cps" + }, + "mariadb-client": { + "versions": { + "10.1.38-r1": 1551187990, + "10.1.40-r0": 1560354887, + "10.1.41-r0": 1565163111 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libncursesw.so.6", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:myisam_ftdump", + "cmd:mysql", + "cmd:mysql_find_rows", + "cmd:mysql_fix_extensions", + "cmd:mysql_waitpid", + "cmd:mysqlaccess", + "cmd:mysqladmin", + "cmd:mysqlbug", + "cmd:mysqlcheck", + "cmd:mysqldump", + "cmd:mysqldumpslow", + "cmd:mysqlimport", + "cmd:mysqlshow" + ] + }, + "perl-rrd": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librrd.so.4" + ] + }, + "goffice-dev": { + "versions": { + "0.10.36-r0": 1511455403 + }, + "origin": "goffice", + "dependencies": [ + "glib-dev", + "libgsf-dev", + "cairo-dev", + "libxml2-dev", + "gtk+3.0-dev", + "librsvg-dev", + "goffice=0.10.36-r0", + "pc:cairo", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gtk+-3.0", + "pc:libgsf-1", + "pc:librsvg-2.0", + "pc:libxml-2.0", + "pc:libxslt", + "pc:pangocairo", + "pkgconfig" + ], + "provides": [ + "pc:libgoffice-0.10=0.10.36" + ] + }, + "libproc": { + "versions": { + "3.3.12-r3": 1509492055 + }, + "origin": "procps", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libprocps.so.6=6.0.0" + ] + }, + "ilbc-dev": { + "versions": { + "0.0.1-r0": 1510584810 + }, + "origin": "ilbc", + "dependencies": [ + "ilbc=0.0.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:ilbc=0.0.1" + ] + }, + "gnokii-libs": { + "versions": { + "0.6.31-r6": 1510069846 + }, + "origin": "gnokii", + "dependencies": [ + "so:libXpm.so.4", + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libical.so.2", + "so:libintl.so.8", + "so:libusb-0.1.so.4" + ], + "provides": [ + "so:libgnokii.so.7=7.0.0" + ] + }, + "git-gitweb": { + "versions": { + "2.15.3-r0": 1540401293, + "2.15.4-r0": 1576015190 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0", + "perl" + ] + }, + "lua5.1-dbi-postgresql": { + "versions": { + "0.6-r1": 1511483402 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "openssh-doc": { + "versions": { + "7.5_p1-r10": 1551712287 + }, + "origin": "openssh" + }, + "lua5.2-feedparser": { + "versions": { + "0.71-r0": 1509485579 + }, + "origin": "lua-feedparser", + "dependencies": [ + "lua5.2-expat", + "lua-feedparser-common", + "lua-feedparser-common=0.71-r0" + ] + }, + "acf-lighttpd": { + "versions": { + "0.6.0-r2": 1510072476 + }, + "origin": "acf-lighttpd", + "dependencies": [ + "acf-core", + "lighttpd" + ] + }, + "perl-socket": { + "versions": { + "2.024-r2": 1509484006 + }, + "origin": "perl-socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libdv-tools": { + "versions": { + "1.0.0-r3": 1510068409 + }, + "origin": "libdv", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdv.so.4", + "so:libgdk-x11-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ], + "provides": [ + "cmd:dubdv", + "cmd:dvconnect", + "cmd:encodedv", + "cmd:playdv" + ] + }, + "swish-e-dev": { + "versions": { + "2.4.7-r8": 1509483301 + }, + "origin": "swish-e", + "dependencies": [ + "pkgconfig", + "swish-e=2.4.7-r8" + ], + "provides": [ + "pc:swish-e=2.4.7", + "cmd:swish-config" + ] + }, + "flex-dev": { + "versions": { + "2.6.4-r1": 1509456704 + }, + "origin": "flex", + "dependencies": [ + "flex", + "flex-libs=2.6.4-r1" + ] + }, + "cppunit": { + "versions": { + "1.14.0-r0": 1509481255 + }, + "origin": "cppunit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcppunit-1.14.so.0=0.0.0", + "cmd:DllPlugInTester" + ] + }, + "perl-db_file-doc": { + "versions": { + "1.840-r1": 1509491809 + }, + "origin": "perl-db_file" + }, + "grep": { + "versions": { + "3.1-r0": 1509468224 + }, + "origin": "grep", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ], + "provides": [ + "cmd:egrep", + "cmd:fgrep", + "cmd:grep" + ] + }, + "openbox-libs": { + "versions": { + "3.6.1-r1": 1510073885 + }, + "origin": "openbox", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangoxft-1.0.so.0", + "so:librsvg-2.so.2", + "so:libxml2.so.2" + ], + "provides": [ + "so:libobrender.so.32=32.0.0", + "so:libobt.so.2=2.0.2" + ] + }, + "libcdio": { + "versions": { + "0.94-r0": 1509473628 + }, + "origin": "libcdio", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcdio.so.16=16.0.0", + "so:libiso9660.so.10=10.0.0", + "so:libudf.so.0=0.0.0" + ] + }, + "py2-mock": { + "versions": { + "2.0.0-r3": 1509493394 + }, + "origin": "py-mock", + "dependencies": [ + "py2-pbr", + "py2-six", + "py2-funcsigs", + "python2" + ] + }, + "perl-extutils-helpers-doc": { + "versions": { + "0.026-r0": 1509474753 + }, + "origin": "perl-extutils-helpers" + }, + "libdv-doc": { + "versions": { + "1.0.0-r3": 1510068409 + }, + "origin": "libdv" + }, + "ragel": { + "versions": { + "6.10-r0": 1509490669 + }, + "origin": "ragel", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:ragel" + ] + }, + "drbd-utils-doc": { + "versions": { + "9.1.1-r0": 1511795634 + }, + "origin": "drbd-utils" + }, + "xf86-video-vmware": { + "versions": { + "13.2.1-r0": 1510072912 + }, + "origin": "xf86-video-vmware", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libxatracker.so.2" + ] + }, + "ncftp-doc": { + "versions": { + "3.2.6-r1": 1509472911 + }, + "origin": "ncftp" + }, + "tevent": { + "versions": { + "0.9.34-r0": 1511479236 + }, + "origin": "tevent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libtevent.so.0=0.9.34" + ] + }, + "xf86-video-ast": { + "versions": { + "1.1.5-r1": 1510074918 + }, + "origin": "xf86-video-ast", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "ngrep-doc": { + "versions": { + "1.45-r4": 1509489072 + }, + "origin": "ngrep" + }, + "acf-mdadm": { + "versions": { + "0.5.0-r2": 1510072261 + }, + "origin": "acf-mdadm", + "dependencies": [ + "acf-core", + "mdadm" + ] + }, + "spamassassin-client": { + "versions": { + "3.4.1-r8": 1509517412 + }, + "origin": "spamassassin", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:spamc" + ] + }, + "mrxvt-doc": { + "versions": { + "0.5.4-r7": 1509469844 + }, + "origin": "mrxvt" + }, + "gst-plugins-base0.10-dev": { + "versions": { + "0.10.36-r3": 1510068367 + }, + "origin": "gst-plugins-base0.10", + "dependencies": [ + "gstreamer0.10-dev", + "gst-plugins-base0.10=0.10.36-r3", + "pc:glib-2.0", + "pc:gstreamer-0.10", + "pc:gstreamer-base-0.10", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-app-0.10=0.10.36", + "pc:gstreamer-audio-0.10=0.10.36", + "pc:gstreamer-cdda-0.10=0.10.36", + "pc:gstreamer-fft-0.10=0.10.36", + "pc:gstreamer-floatcast-0.10=0.10.36", + "pc:gstreamer-interfaces-0.10=0.10.36", + "pc:gstreamer-netbuffer-0.10=0.10.36", + "pc:gstreamer-pbutils-0.10=0.10.36", + "pc:gstreamer-plugins-base-0.10=0.10.36", + "pc:gstreamer-riff-0.10=0.10.36", + "pc:gstreamer-rtp-0.10=0.10.36", + "pc:gstreamer-rtsp-0.10=0.10.36", + "pc:gstreamer-sdp-0.10=0.10.36", + "pc:gstreamer-tag-0.10=0.10.36", + "pc:gstreamer-video-0.10=0.10.36" + ] + }, + "qt": { + "versions": { + "4.8.7-r8": 1510287210 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libQtCore.so.4=4.8.7", + "so:libQtDBus.so.4=4.8.7", + "so:libQtNetwork.so.4=4.8.7", + "so:libQtScript.so.4=4.8.7", + "so:libQtSql.so.4=4.8.7", + "so:libQtTest.so.4=4.8.7", + "so:libQtXml.so.4=4.8.7", + "so:libQtXmlPatterns.so.4=4.8.7", + "cmd:qdbus" + ] + }, + "py-factory-boy": { + "versions": { + "2.9.2-r0": 1509493824 + }, + "origin": "py-factory-boy" + }, + "font-bh-lucidatypewriter-75dpi": { + "versions": { + "1.0.3-r0": 1509494513 + }, + "origin": "font-bh-lucidatypewriter-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "lua5.2-rex-posix": { + "versions": { + "2.9.0-r0": 1510619366 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "dri2proto-doc": { + "versions": { + "2.8-r2": 1509466228 + }, + "origin": "dri2proto" + }, + "freeradius-pam": { + "versions": { + "3.0.15-r4": 1556202797, + "3.0.15-r5": 1566310604 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0" + ], + "provides": [ + "freeradius3-pam=3.0.15-r5", + "so:rlm_pam.so=0" + ] + }, + "libbsd": { + "versions": { + "0.8.6-r1": 1509461838 + }, + "origin": "libbsd", + "dependencies": [ + "musl>=1.1.16-r22", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libbsd.so.0=0.8.6" + ] + }, + "btrfs-progs-bash-completion": { + "versions": { + "4.13.2-r0": 1509481818 + }, + "origin": "btrfs-progs" + }, + "cvs": { + "versions": { + "1.11.23-r0": 1509481180 + }, + "origin": "cvs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cvs", + "cmd:cvsbug", + "cmd:rcs2log" + ] + }, + "py-webassets": { + "versions": { + "0.12.1-r1": 1509551873 + }, + "origin": "py-webassets", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:webassets" + ] + }, + "xcb-util-image": { + "versions": { + "0.4.0-r1": 1509473909 + }, + "origin": "xcb-util-image", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-shm.so.0", + "so:libxcb-util.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-image.so.0=0.0.0" + ] + }, + "randrproto-doc": { + "versions": { + "1.5.0-r2": 1509465995 + }, + "origin": "randrproto" + }, + "ngrep": { + "versions": { + "1.45-r4": 1509489072 + }, + "origin": "ngrep", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:ngrep" + ] + }, + "perl-variable-magic-doc": { + "versions": { + "0.62-r0": 1510087954 + }, + "origin": "perl-variable-magic" + }, + "multipath-tools-doc": { + "versions": { + "0.7.4-r0": 1510816244 + }, + "origin": "multipath-tools" + }, + "libidl-doc": { + "versions": { + "0.8.14-r2": 1510928272 + }, + "origin": "libidl" + }, + "postgresql-pltcl": { + "versions": { + "10.7-r0": 1554274199, + "10.8-r0": 1557668015, + "10.9-r0": 1562225614, + "10.10-r0": 1565683438 + }, + "origin": "postgresql", + "dependencies": [ + "pgtcl", + "so:libc.musl-x86_64.so.1", + "so:libtcl8.6.so" + ] + }, + "acf-kamailio": { + "versions": { + "0.10.0-r2": 1510076040 + }, + "origin": "acf-kamailio", + "dependencies": [ + "acf-core", + "kamailio", + "acf-db-lib" + ] + }, + "spice": { + "versions": { + "0.14.1-r2": 1548934012 + }, + "origin": "spice" + }, + "libgcc": { + "versions": { + "6.4.0-r5": 1509458071 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgcc_s.so.1=1" + ] + }, + "qemu-ppc64abi32": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-ppc64abi32" + ] + }, + "perl-unix-syslog-doc": { + "versions": { + "1.1-r9": 1509484017 + }, + "origin": "perl-unix-syslog" + }, + "py2-tornado": { + "versions": { + "4.5.2-r1": 1509551888 + }, + "origin": "py-tornado", + "dependencies": [ + "py2-backports_abc", + "py2-certifi", + "py2-singledispatch", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "freeswitch-sounds-ru-RU-elena-8000": { + "versions": { + "1.0.12-r1": 1509496008 + }, + "origin": "freeswitch-sounds-ru-RU-elena-8000" + }, + "apache2-lua": { + "versions": { + "2.4.39-r0": 1554306881, + "2.4.41-r0": 1566292328 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ] + }, + "libdrm": { + "versions": { + "2.4.88-r0": 1510649882 + }, + "origin": "libdrm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpciaccess.so.0" + ], + "provides": [ + "so:libdrm.so.2=2.4.0", + "so:libdrm_amdgpu.so.1=1.0.0", + "so:libdrm_freedreno.so.1=1.0.0", + "so:libdrm_intel.so.1=1.0.0", + "so:libdrm_nouveau.so.2=2.0.0", + "so:libdrm_radeon.so.1=1.0.1", + "so:libkms.so.1=1.0.0" + ] + }, + "tar": { + "versions": { + "1.32-r0": 1551092030 + }, + "origin": "tar", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tar" + ] + }, + "nodejs-doc": { + "versions": { + "8.9.3-r1": 1522616958 + }, + "origin": "nodejs" + }, + "py-enum34": { + "versions": { + "1.1.6-r2": 1509493830 + }, + "origin": "py-enum34" + }, + "portaudio-dev": { + "versions": { + "19-r1": 1509473342 + }, + "origin": "portaudio", + "dependencies": [ + "alsa-lib-dev", + "pc:alsa", + "pkgconfig", + "portaudio=19-r1" + ], + "provides": [ + "pc:portaudio-2.0=19" + ] + }, + "openjpeg": { + "versions": { + "2.3.0-r2": 1552584602 + }, + "origin": "openjpeg", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libopenjp2.so.7=2.3.0" + ] + }, + "expat-dev": { + "versions": { + "2.2.5-r0": 1509908121, + "2.2.7-r0": 1561897528, + "2.2.7-r1": 1568353727, + "2.2.8-r0": 1568974114 + }, + "origin": "expat", + "dependencies": [ + "expat=2.2.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:expat=2.2.8" + ] + }, + "perl-test-number-delta": { + "versions": { + "1.06-r0": 1509481695 + }, + "origin": "perl-test-number-delta" + }, + "openldap-overlay-valsort": { + "versions": { + "2.4.45-r3": 1510258136, + "2.4.46-r0": 1565073941, + "2.4.48-r0": 1566900518 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "rrdtool-cgi": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librrd.so.4" + ], + "provides": [ + "cmd:rrdcgi" + ] + }, + "mailcap-doc": { + "versions": { + "2.1.48-r0": 1509474792 + }, + "origin": "mailcap" + }, + "sudo-dev": { + "versions": { + "1.8.21_p2-r1": 1509459675 + }, + "origin": "sudo" + }, + "openldap-overlay-refint": { + "versions": { + "2.4.45-r3": 1510258135, + "2.4.46-r0": 1565073940, + "2.4.48-r0": 1566900517 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "mdocml": { + "versions": { + "1.14.3-r0": 1509459639 + }, + "origin": "mdocml", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libmandoc.so=0", + "cmd:demandoc", + "cmd:mandoc" + ] + }, + "msmtp-doc": { + "versions": { + "1.6.6-r2": 1510261496 + }, + "origin": "msmtp" + }, + "perdition-doc": { + "versions": { + "1.18-r10": 1510260023 + }, + "origin": "perdition" + }, + "elfutils-dev": { + "versions": { + "0.168-r1": 1509477711 + }, + "origin": "elfutils", + "dependencies": [ + "elfutils-libelf=0.168-r1" + ] + }, + "acf-dovecot": { + "versions": { + "0.6.0-r2": 1510073823 + }, + "origin": "acf-dovecot", + "dependencies": [ + "acf-core", + "dovecot" + ] + }, + "libquvi-dev": { + "versions": { + "0.9.4-r3": 1509492378 + }, + "origin": "libquvi", + "dependencies": [ + "libquvi-scripts-dev", + "curl-dev", + "lua5.1-dev", + "libquvi=0.9.4-r3", + "pkgconfig" + ], + "provides": [ + "pc:libquvi-0.9=0.9.4" + ] + }, + "dialog": { + "versions": { + "1.3.20170509-r0": 1509468657 + }, + "origin": "dialog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:dialog" + ] + }, + "xf86-video-qxl-doc": { + "versions": { + "0.1.5-r3": 1510073030 + }, + "origin": "xf86-video-qxl" + }, + "perl-dbd-sqlite": { + "versions": { + "1.54-r1": 1509477496 + }, + "origin": "perl-dbd-sqlite", + "dependencies": [ + "perl-dbi", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "nagios-plugins-fping": { + "versions": { + "2.2.1-r3": 1510288495 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "fping", + "so:libc.musl-x86_64.so.1" + ] + }, + "pango-doc": { + "versions": { + "1.40.14-r1": 1541519450 + }, + "origin": "pango" + }, + "gcr-lang": { + "versions": { + "3.20.0-r1": 1510073693 + }, + "origin": "gcr" + }, + "db-dev": { + "versions": { + "5.3.28-r0": 1509469314 + }, + "origin": "db", + "dependencies": [ + "db-c++=5.3.28-r0", + "db=5.3.28-r0" + ] + }, + "cutter": { + "versions": { + "1.04-r0": 1509475709 + }, + "origin": "cutter", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cutter" + ] + }, + "gnokii-smsd-pgsql": { + "versions": { + "0.6.31-r6": 1510069851 + }, + "origin": "gnokii", + "dependencies": [ + "gnokii-smsd", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "fftw-double-libs": { + "versions": { + "3.3.6p2-r0": 1509468831 + }, + "origin": "fftw", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfftw3.so.3=3.5.6", + "so:libfftw3_threads.so.3=3.5.6" + ] + }, + "transmission": { + "versions": { + "2.92-r8": 1510932986 + }, + "origin": "transmission", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libevent-2.1.so.6", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:transmission-gtk" + ] + }, + "lxdm": { + "versions": { + "0.5.3-r1": 1510070032 + }, + "origin": "lxdm", + "dependencies": [ + "bash", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libck-connector.so.0", + "so:libdbus-1.so.3", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libxcb.so.1" + ], + "provides": [ + "cmd:lxdm", + "cmd:lxdm-binary", + "cmd:lxdm-config" + ] + }, + "harfbuzz-dev": { + "versions": { + "1.6.3-r0": 1509464873 + }, + "origin": "harfbuzz", + "dependencies": [ + "harfbuzz-icu=1.6.3-r0", + "harfbuzz=1.6.3-r0", + "pc:glib-2.0>=2.19.1", + "pc:graphite2", + "pc:icu-uc", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfreetype.so.6", + "so:libglib-2.0.so.0", + "so:libharfbuzz.so.0" + ], + "provides": [ + "pc:harfbuzz-icu=1.6.3", + "pc:harfbuzz=1.6.3", + "cmd:hb-ot-shape-closure", + "cmd:hb-shape", + "cmd:hb-view" + ] + }, + "rabbitmq-c": { + "versions": { + "0.8.0-r3": 1510260052 + }, + "origin": "rabbitmq-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "so:librabbitmq.so.4=4.2.0" + ] + }, + "perl-list-utilsby": { + "versions": { + "0.10-r0": 1509488645 + }, + "origin": "perl-list-utilsby" + }, + "perl-php-serialization": { + "versions": { + "0.34-r1": 1509477756 + }, + "origin": "perl-php-serialization", + "dependencies": [ + "perl" + ] + }, + "lua-augeas": { + "versions": { + "0.1.2-r3": 1509473129 + }, + "origin": "lua-augeas" + }, + "a52dec": { + "versions": { + "0.7.4-r6": 1510072176 + }, + "origin": "a52dec", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liba52.so.0=0.0.0", + "cmd:a52dec", + "cmd:extract_a52" + ] + }, + "procmail": { + "versions": { + "3.22-r3": 1513159329 + }, + "origin": "procmail", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:formail", + "cmd:lockfile", + "cmd:mailstat", + "cmd:procmail" + ] + }, + "lua-xml": { + "versions": { + "130610-r5": 1509482648 + }, + "origin": "lua-xml" + }, + "vanessa_logger-dev": { + "versions": { + "0.0.10-r0": 1509481336 + }, + "origin": "vanessa_logger", + "dependencies": [ + "pkgconfig", + "vanessa_logger=0.0.10-r0" + ], + "provides": [ + "pc:vanessa-logger=0.0.10" + ] + }, + "lua5.3-sql-postgres": { + "versions": { + "2.3.5-r1": 1509488832 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "lua-ossl": { + "versions": { + "20171028-r1": 1510260842 + }, + "origin": "lua-ossl" + }, + "postgrey": { + "versions": { + "1.37-r0": 1509479750 + }, + "origin": "postgrey", + "dependencies": [ + "perl", + "perl-db", + "perl-net-dns", + "perl-net-server", + "perl-io-multiplex", + "perl-net-rblclient", + "perl-parse-syslog", + "/bin/sh" + ], + "provides": [ + "cmd:postgrey", + "cmd:postgreyreport" + ] + }, + "device-mapper": { + "versions": { + "2.02.175-r0": 1509462402 + }, + "origin": "lvm2", + "dependencies": [ + "lvm2-libs=2.02.175-r0", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "cmd:dmsetup", + "cmd:dmstats" + ] + }, + "py2-pynacl": { + "versions": { + "1.2.0-r0": 1510088490 + }, + "origin": "py-pynacl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "cracklib-dev": { + "versions": { + "2.9.6-r0": 1509485916 + }, + "origin": "cracklib", + "dependencies": [ + "cracklib=2.9.6-r0" + ] + }, + "boost-graph": { + "versions": { + "1.62.0-r5": 1509465875 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_regex-mt.so.1.62.0", + "so:libboost_regex.so.1.62.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_graph-mt.so.1.62.0=1.62.0", + "so:libboost_graph.so.1.62.0=1.62.0" + ] + }, + "rrdtool-dev": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool", + "dependencies": [ + "librrd-th=1.5.6-r3", + "librrd=1.5.6-r3", + "pkgconfig" + ], + "provides": [ + "pc:librrd=1.5.6" + ] + }, + "dbus-doc": { + "versions": { + "1.10.24-r0": 1509465101, + "1.10.28-r0": 1560765707 + }, + "origin": "dbus" + }, + "zip-doc": { + "versions": { + "3.0-r4": 1509457024 + }, + "origin": "zip" + }, + "hwdata-oui": { + "versions": { + "0.305-r0": 1509468678 + }, + "origin": "hwdata" + }, + "gtkspell-dev": { + "versions": { + "2.0.16-r6": 1510069680 + }, + "origin": "gtkspell", + "dependencies": [ + "gtk+2.0-dev", + "gtkspell=2.0.16-r6", + "pc:gtk+-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gtkspell-2.0=2.0.16" + ] + }, + "ncdu-doc": { + "versions": { + "1.12-r0": 1509495340 + }, + "origin": "ncdu" + }, + "tevent-dev": { + "versions": { + "0.9.34-r0": 1511479236 + }, + "origin": "tevent", + "dependencies": [ + "pc:talloc", + "pkgconfig", + "tevent=0.9.34-r0" + ], + "provides": [ + "pc:tevent=0.9.34" + ] + }, + "py-snowballstemmer": { + "versions": { + "1.2.1-r1": 1509489497 + }, + "origin": "py-snowballstemmer" + }, + "mrxvt": { + "versions": { + "0.5.4-r7": 1509469848 + }, + "origin": "mrxvt", + "dependencies": [ + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXpm.so.4", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libjpeg.so.8" + ], + "provides": [ + "cmd:mrxvt" + ] + }, + "openldap-dev": { + "versions": { + "2.4.45-r3": 1510258130, + "2.4.46-r0": 1565073935, + "2.4.48-r0": 1566900512 + }, + "origin": "openldap", + "dependencies": [ + "libressl-dev", + "cyrus-sasl-dev", + "util-linux-dev", + "libldap=2.4.48-r0" + ] + }, + "lua5.2-socket": { + "versions": { + "3.0_rc1_git20160306-r2": 1509480760 + }, + "origin": "lua-socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "epris": { + "versions": { + "0.2-r4": 1509482713 + }, + "origin": "epris", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgthread-2.0.so.0" + ], + "provides": [ + "cmd:epr" + ] + }, + "gnokii-lang": { + "versions": { + "0.6.31-r6": 1510069856 + }, + "origin": "gnokii" + }, + "perl-data-page": { + "versions": { + "2.02-r1": 1509493850 + }, + "origin": "perl-data-page", + "dependencies": [ + "perl-class-accessor-chained", + "perl-test-exception" + ] + }, + "lua5.1-subprocess": { + "versions": { + "0.0.20141229-r2": 1509468272 + }, + "origin": "lua-subprocess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gst-plugins-ugly": { + "versions": { + "1.12.3-r0": 1510075820 + }, + "origin": "gst-plugins-ugly", + "dependencies": [ + "so:liba52.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcdio.so.16", + "so:libdvdread.so.4", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-1.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstpbutils-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgstriff-1.0.so.0", + "so:libgstrtp-1.0.so.0", + "so:libgstrtsp-1.0.so.0", + "so:libgstsdp-1.0.so.0", + "so:libgsttag-1.0.so.0", + "so:libgstvideo-1.0.so.0", + "so:libintl.so.8", + "so:libmp3lame.so.0", + "so:libmpeg2.so.0", + "so:libmpg123.so.0", + "so:liborc-0.4.so.0", + "so:libx264.so.148" + ] + }, + "thunar": { + "versions": { + "1.6.12-r0": 1510072626 + }, + "origin": "thunar", + "dependencies": [ + "desktop-file-utils", + "hicolor-icon-theme", + "shared-mime-info", + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libexif.so.12", + "so:libexo-1.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libintl.so.8", + "so:libnotify.so.4", + "so:libpango-1.0.so.0", + "so:libpcre.so.1", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "so:libthunarx-2.so.0=0.0.0", + "cmd:Thunar", + "cmd:thunar", + "cmd:thunar-settings" + ] + }, + "ffmpeg-libs": { + "versions": { + "3.4-r1": 1510072147 + }, + "origin": "ffmpeg", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libX11.so.6", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30", + "so:libmp3lame.so.0", + "so:libopus.so.0", + "so:librtmp.so.1", + "so:libtheoradec.so.1", + "so:libtheoraenc.so.1", + "so:libv4l2.so.0", + "so:libva-drm.so.1", + "so:libva-x11.so.1", + "so:libva.so.1", + "so:libvdpau.so.1", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2", + "so:libvpx.so.4", + "so:libx264.so.148", + "so:libx265.so.130", + "so:libxcb-shape.so.0", + "so:libxcb-shm.so.0", + "so:libxcb-xfixes.so.0", + "so:libxcb.so.1", + "so:libxvidcore.so.4", + "so:libz.so.1" + ], + "provides": [ + "so:libavcodec.so.57=57.107.100", + "so:libavdevice.so.57=57.10.100", + "so:libavfilter.so.6=6.107.100", + "so:libavformat.so.57=57.83.100", + "so:libavresample.so.3=3.7.0", + "so:libavutil.so.55=55.78.100", + "so:libpostproc.so.54=54.7.100", + "so:libswresample.so.2=2.9.100", + "so:libswscale.so.4=4.8.100" + ] + }, + "librsvg": { + "versions": { + "2.40.19-r0": 1510066818 + }, + "origin": "librsvg", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcroco-0.6.so.3", + "so:libfontconfig.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:librsvg-2.so.2=2.40.19", + "cmd:rsvg-convert" + ] + }, + "asterisk-odbc": { + "versions": { + "15.6.1-r0": 1537795340, + "15.6.2-r0": 1568705002 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "ppp-l2tp": { + "versions": { + "2.4.7-r5": 1509480133 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "dtach": { + "versions": { + "0.9-r0": 1509475626 + }, + "origin": "dtach", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dtach" + ] + }, + "libseccomp": { + "versions": { + "2.3.2-r1": 1519818661 + }, + "origin": "libseccomp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libseccomp.so.2=2.3.2", + "cmd:scmp_sys_resolver" + ] + }, + "xf86-video-savage": { + "versions": { + "2.3.9-r0": 1510076050 + }, + "origin": "xf86-video-savage", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "xf86-input-keyboard-doc": { + "versions": { + "1.9.0-r0": 1510074945 + }, + "origin": "xf86-input-keyboard" + }, + "zfs-hardened": { + "versions": { + "4.9.65-r1": 1511799189 + }, + "origin": "zfs-hardened", + "dependencies": [ + "spl-hardened", + "linux-hardened=4.9.65-r1" + ], + "provides": [ + "zfs-grsec=4.9.65-r1" + ] + }, + "libressl-dev": { + "versions": { + "2.6.5-r0": 1529043884 + }, + "origin": "libressl", + "dependencies": [ + "libressl2.6-libcrypto=2.6.5-r0", + "libressl2.6-libssl=2.6.5-r0", + "libressl2.6-libtls=2.6.5-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcrypto=2.6.5", + "pc:libssl=2.6.5", + "pc:libtls=2.6.5", + "pc:openssl=2.6.5" + ] + }, + "py3-sphinx_rtd_theme": { + "versions": { + "0.2.4-r0": 1509469911 + }, + "origin": "py-sphinx_rtd_theme", + "dependencies": [ + "python3" + ] + }, + "patchutils-doc": { + "versions": { + "0.3.4-r0": 1509495751 + }, + "origin": "patchutils" + }, + "elinks-lang": { + "versions": { + "0.13-r4": 1510261265 + }, + "origin": "elinks" + }, + "audacious-dbg": { + "versions": { + "3.9-r0": 1510072525 + }, + "origin": "audacious" + }, + "perl-plack-doc": { + "versions": { + "1.0033-r0": 1510588943 + }, + "origin": "perl-plack" + }, + "font-schumacher-misc": { + "versions": { + "1.1.2-r0": 1509491122 + }, + "origin": "font-schumacher-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "xcmiscproto-doc": { + "versions": { + "1.2.2-r2": 1509473766 + }, + "origin": "xcmiscproto" + }, + "make": { + "versions": { + "4.2.1-r0": 1509458175 + }, + "origin": "make", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:make" + ] + }, + "py2-flask-oauthlib": { + "versions": { + "0.9.3-r2": 1509552814 + }, + "origin": "py-flask-oauthlib", + "dependencies": [ + "py2-flask", + "py2-requests-oauthlib", + "python2" + ] + }, + "mdadm": { + "versions": { + "4.0-r0": 1509476490 + }, + "origin": "mdadm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mdadm", + "cmd:mdmon" + ] + }, + "freeswitch-perl": { + "versions": { + "1.6.19-r0": 1510585358 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeswitch.so.1", + "so:libgcc_s.so.1", + "so:libperl.so", + "so:libstdc++.so.6" + ] + }, + "perl-mime-lite-doc": { + "versions": { + "3.030-r1": 1509495012 + }, + "origin": "perl-mime-lite" + }, + "lua-lustache-common": { + "versions": { + "1.3.1-r1": 1509472926 + }, + "origin": "lua-lustache" + }, + "gc": { + "versions": { + "7.6.0-r1": 1509467221 + }, + "origin": "gc", + "dependencies": [ + "so:libatomic_ops.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcord.so.1=1.0.3", + "so:libgc.so.1=1.0.3" + ] + }, + "upower-lang": { + "versions": { + "0.99.6-r0": 1510068213 + }, + "origin": "upower" + }, + "py-alabaster": { + "versions": { + "0.7.10-r0": 1509476479 + }, + "origin": "py-alabaster" + }, + "perl-dbi-doc": { + "versions": { + "1.637-r0": 1509477478 + }, + "origin": "perl-dbi" + }, + "lua5.3-ossl": { + "versions": { + "20171028-r1": 1510260842 + }, + "origin": "lua-ossl", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "lua-yaml": { + "versions": { + "1.1.2-r1": 1509488586 + }, + "origin": "lua-yaml" + }, + "libvirt-lxc": { + "versions": { + "3.9.0-r1": 1510088351, + "5.5.0-r0": 1562165540 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=5.5.0-r0", + "libvirt-common-drivers=5.5.0-r0", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libfuse.so.2", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libtirpc.so.3", + "so:libvirt.so.0", + "so:libxml2.so.2" + ] + }, + "mesa-dri-nouveau": { + "versions": { + "17.2.4-r1": 1510741946 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_intel.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libxcb-dri2.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb-xfixes.so.0", + "so:libxcb.so.1", + "so:libxshmfence.so.1", + "so:libz.so.1" + ] + }, + "highlight": { + "versions": { + "3.41-r0": 1512029997 + }, + "origin": "highlight", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:liblua-5.3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:highlight" + ] + }, + "tslib": { + "versions": { + "1.14-r0": 1510573238 + }, + "origin": "tslib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libts.so.0=0.8.1", + "cmd:ts_calibrate", + "cmd:ts_finddev", + "cmd:ts_harvest", + "cmd:ts_print", + "cmd:ts_print_mt", + "cmd:ts_print_raw", + "cmd:ts_test", + "cmd:ts_test_mt", + "cmd:ts_uinput", + "cmd:ts_verify" + ] + }, + "eudev-netifnames": { + "versions": { + "3.2.4-r1": 1509466081 + }, + "origin": "eudev", + "dependencies": [ + "udev-init-scripts" + ] + }, + "dovecot-ldap": { + "versions": { + "2.2.36.3-r0": 1554102390, + "2.2.36.4-r0": 1567075099 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot", + "so:libc.musl-x86_64.so.1", + "so:libdovecot.so.0", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ], + "provides": [ + "so:libdovecot-ldap.so.0=0.0.0" + ] + }, + "perl-mail-spf": { + "versions": { + "2.9.0-r2": 1509491252 + }, + "origin": "perl-mail-spf", + "dependencies": [ + "perl", + "perl-error", + "perl-net-dns", + "perl-uri", + "perl-netaddr-ip", + "perl-net-dns-resolver-programmable" + ] + }, + "perl-module-scandeps-doc": { + "versions": { + "1.24-r0": 1510352936 + }, + "origin": "perl-module-scandeps" + }, + "py-wtforms": { + "versions": { + "2.1-r0": 1509476540 + }, + "origin": "py-wtforms" + }, + "perl-email-address-doc": { + "versions": { + "1.908-r0": 1509491035, + "1.912-r0": 1559738452 + }, + "origin": "perl-email-address" + }, + "sdl2_ttf": { + "versions": { + "2.0.14-r1": 1510310961 + }, + "origin": "sdl2_ttf", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6" + ], + "provides": [ + "so:libSDL2_ttf-2.0.so.0=0.14.0" + ] + }, + "libnotify": { + "versions": { + "0.7.7-r0": 1510068017 + }, + "origin": "libnotify", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libnotify.so.4=4.0.0", + "cmd:notify-send" + ] + }, + "dnstop": { + "versions": { + "20140915-r3": 1509485932 + }, + "origin": "dnstop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:dnstop" + ] + }, + "uwsgi-symcall": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "goffice-lang": { + "versions": { + "0.10.36-r0": 1511455403 + }, + "origin": "goffice" + }, + "xfce4-battery-plugin-lang": { + "versions": { + "1.0.5-r2": 1510072891 + }, + "origin": "xfce4-battery-plugin" + }, + "sqsh": { + "versions": { + "2.5.16.1-r2": 1509493718 + }, + "origin": "sqsh", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libct.so.4", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:sqsh" + ] + }, + "xorg-server-dbg": { + "versions": { + "1.19.5-r1": 1540838459 + }, + "origin": "xorg-server", + "dependencies": [ + "font-misc-misc", + "font-cursor-misc", + "xkeyboard-config", + "xkbcomp", + "xinit" + ] + }, + "py3-nose": { + "versions": { + "1.3.7-r2": 1509552700 + }, + "origin": "py-nose", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:nosetests", + "cmd:nosetests-3.6" + ] + }, + "perl-boolean": { + "versions": { + "0.46-r0": 1509485612 + }, + "origin": "perl-boolean" + }, + "atf": { + "versions": { + "0.21-r1": 1509459731 + }, + "origin": "atf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libatf-c++.so.2=2.0.0", + "so:libatf-c.so.1=1.0.0", + "cmd:atf-sh" + ] + }, + "gtkmm-dev": { + "versions": { + "2.24.5-r0": 1510073814 + }, + "origin": "gtkmm", + "dependencies": [ + "atkmm-dev", + "gtk+2.0-dev", + "glibmm-dev", + "pangomm-dev", + "gtkmm=2.24.5-r0", + "pc:atkmm-1.6>=2.22.2", + "pc:giomm-2.4>=2.27.93", + "pc:gtk+-2.0>=2.24.0", + "pc:gtk+-unix-print-2.0", + "pc:pangomm-1.4>=2.27.1", + "pkgconfig" + ], + "provides": [ + "pc:gdkmm-2.4=2.24.5", + "pc:gtkmm-2.4=2.24.5" + ] + }, + "luajit-dev": { + "versions": { + "2.1.0_beta3-r0": 1509474649 + }, + "origin": "luajit", + "dependencies": [ + "luajit=2.1.0_beta3-r0", + "pkgconfig" + ] + }, + "libwbclient": { + "versions": { + "4.7.6-r3": 1555491787 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libreplace-samba4.so=0", + "so:libwbclient.so.0=0.14", + "so:libwinbind-client-samba4.so=0" + ] + }, + "nagios": { + "versions": { + "3.5.1-r4": 1509494205 + }, + "origin": "nagios", + "dependencies": [ + "perl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3", + "so:libperl.so" + ], + "provides": [ + "cmd:nagios", + "cmd:nagiostats" + ] + }, + "python3-dbg": { + "versions": { + "3.6.8-r0": 1555928807, + "3.6.9-r0": 1571233993, + "3.6.9-r1": 1571314638 + }, + "origin": "python3" + }, + "lua-gversion": { + "versions": { + "0.2.0-r1": 1509475306 + }, + "origin": "lua-gversion" + }, + "alsaconf": { + "versions": { + "1.1.4-r0": 1509468851 + }, + "origin": "alsa-utils", + "dependencies": [ + "alsa-utils", + "bash" + ], + "provides": [ + "cmd:alsaconf" + ] + }, + "busybox-extras": { + "versions": { + "1.27.2-r11": 1528276162 + }, + "origin": "busybox", + "dependencies": [ + "busybox", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:busybox-extras" + ] + }, + "font-vollkorn": { + "versions": { + "4.015-r0": 1509967842 + }, + "origin": "font-vollkorn" + }, + "libgmpxx": { + "versions": { + "6.1.2-r1": 1509456917 + }, + "origin": "gmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgmp.so.10", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgmpxx.so.4=4.5.2" + ] + }, + "quagga-dev": { + "versions": { + "1.2.4-r0": 1519133992 + }, + "origin": "quagga", + "dependencies": [ + "quagga=1.2.4-r0" + ] + }, + "qemu-system-microblazeel": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-microblazeel" + ] + }, + "mc-lang": { + "versions": { + "4.8.20-r0": 1511871582 + }, + "origin": "mc" + }, + "abiword-plugin-kword": { + "versions": { + "3.0.2-r1": 1510073362 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "abiword-plugin-presentation": { + "versions": { + "3.0.2-r1": 1510073367 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "dahdi-linux-hardened": { + "versions": { + "4.9.65-r1": 1511798670 + }, + "origin": "dahdi-linux-hardened", + "dependencies": [ + "dahdi-linux", + "linux-hardened=4.9.65-r1" + ], + "provides": [ + "dahdi-linux-grsec=4.9.65-r1" + ] + }, + "py2-flask": { + "versions": { + "0.12.2-r1": 1509551869 + }, + "origin": "py-flask", + "dependencies": [ + "py2-click", + "py2-itsdangerous", + "py2-jinja2", + "py2-werkzeug", + "python2" + ], + "provides": [ + "cmd:flask" + ] + }, + "lua5.2-yaml": { + "versions": { + "1.1.2-r1": 1509488585 + }, + "origin": "lua-yaml", + "dependencies": [ + "lua5.2", + "lua5.2-lub", + "so:libc.musl-x86_64.so.1" + ] + }, + "nagios-plugins-mysql": { + "versions": { + "2.2.1-r3": 1510288496 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "xfsprogs-dev": { + "versions": { + "4.14.0-r1": 1528279972 + }, + "origin": "xfsprogs", + "dependencies": [ + "xfsprogs-libs=4.14.0-r1" + ] + }, + "devicemaster-linux-hardened": { + "versions": { + "4.9.65-r1": 1511799326 + }, + "origin": "devicemaster-linux-hardened", + "dependencies": [ + "linux-hardened=4.9.65-r1" + ], + "provides": [ + "devicemaster-linux-grsec=4.9.65-r1" + ] + }, + "sgdisk": { + "versions": { + "1.0.3-r0": 1509495223 + }, + "origin": "gptfdisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpopt.so.0", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:sgdisk" + ] + }, + "git-fast-import": { + "versions": { + "2.15.3-r0": 1540401293, + "2.15.4-r0": 1576015190 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0", + "so:libc.musl-x86_64.so.1", + "so:libpcre2-8.so.0", + "so:libz.so.1" + ] + }, + "dri3proto": { + "versions": { + "1.0-r2": 1509466244 + }, + "origin": "dri3proto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:dri3proto=1.0" + ] + }, + "perl-server-starter": { + "versions": { + "0.15-r1": 1510564665 + }, + "origin": "perl-server-starter", + "dependencies": [ + "perl-proc-wait3", + "perl-list-moreutils", + "perl-scope-guard" + ], + "provides": [ + "cmd:start_server" + ] + }, + "fltk-fluid": { + "versions": { + "1.3.4-r0": 1510072238 + }, + "origin": "fltk", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libfltk.so.1.3", + "so:libfltk_forms.so.1.3", + "so:libfltk_images.so.1.3", + "so:libpng16.so.16", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:fluid" + ] + }, + "libgssglue": { + "versions": { + "0.4-r0": 1509479797 + }, + "origin": "libgssglue", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgssglue.so.1=1.0.0" + ] + }, + "appstream-glib-dev": { + "versions": { + "0.6.3-r0": 1510067860 + }, + "origin": "appstream-glib", + "dependencies": [ + "glib-dev", + "gdk-pixbuf-dev", + "appstream-glib-builder=0.6.3-r0", + "appstream-glib=0.6.3-r0", + "pc:gdk-pixbuf-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:libarchive", + "pc:libgcab-1.0", + "pc:uuid", + "pkgconfig" + ], + "provides": [ + "pc:appstream-builder=0.6.3", + "pc:appstream-glib=0.6.3" + ] + }, + "qemu-system-x86_64": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-x86_64" + ] + }, + "lua-optarg": { + "versions": { + "0.2-r1": 1509462495 + }, + "origin": "lua-optarg" + }, + "collectd-log_logstash": { + "versions": { + "5.7.2-r0": 1510069652 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libyajl.so.2" + ] + }, + "postfix-stone": { + "versions": { + "3.2.4-r1": 1510285340 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpostfix-global.so", + "so:libpostfix-util.so" + ], + "provides": [ + "cmd:fsstone", + "cmd:qmqp-sink", + "cmd:qmqp-source", + "cmd:smtp-sink", + "cmd:smtp-source" + ] + }, + "libinput-libs": { + "versions": { + "1.9.2-r0": 1511276852 + }, + "origin": "libinput", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libevdev.so.2", + "so:libmtdev.so.1", + "so:libudev.so.1" + ], + "provides": [ + "so:libinput.so.10=10.13.0" + ] + }, + "s6-linux-utils-doc": { + "versions": { + "2.4.0.1-r0": 1509468269 + }, + "origin": "s6-linux-utils" + }, + "xfce4-power-manager": { + "versions": { + "1.6.0-r2": 1510068248 + }, + "origin": "xfce4-power-manager", + "dependencies": [ + "polkit", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libnotify.so.4", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libupower-glib.so.3", + "so:libxfce4panel-2.0.so.4", + "so:libxfce4ui-2.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:xfce4-pm-helper", + "cmd:xfce4-power-manager", + "cmd:xfce4-power-manager-settings", + "cmd:xfpm-power-backlight-helper" + ] + }, + "lua5.1-struct": { + "versions": { + "0.2-r2": 1509494932 + }, + "origin": "lua-struct", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "qemu-ppc64": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-ppc64" + ] + }, + "collectd-network": { + "versions": { + "5.7.2-r0": 1510069646 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20" + ] + }, + "perl-dist-checkconflicts-doc": { + "versions": { + "0.11-r0": 1509474024 + }, + "origin": "perl-dist-checkconflicts" + }, + "ruby-libs": { + "versions": { + "2.4.6-r0": 1557166823 + }, + "origin": "ruby", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libffi.so.6", + "so:libgdbm.so.4", + "so:libgdbm_compat.so.4", + "so:libgmp.so.10", + "so:libreadline.so.7", + "so:libssl.so.44", + "so:libyaml-0.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libruby.so.2.4=2.4.6" + ] + }, + "perl-apache-logformat-compiler": { + "versions": { + "0.32-r0": 1509481806 + }, + "origin": "perl-apache-logformat-compiler", + "dependencies": [ + "perl-posix-strftime-compiler" + ] + }, + "clamsmtp-doc": { + "versions": { + "1.10-r15": 1509468380 + }, + "origin": "clamsmtp" + }, + "perl-string-shellquote": { + "versions": { + "1.04-r0": 1509494707 + }, + "origin": "perl-string-shellquote", + "provides": [ + "cmd:shell-quote" + ] + }, + "lua5.1-lustache": { + "versions": { + "1.3.1-r1": 1509472927 + }, + "origin": "lua-lustache", + "dependencies": [ + "lua-lustache-common", + "lua5.1", + "lua-lustache-common=1.3.1-r1" + ] + }, + "autoconf-doc": { + "versions": { + "2.69-r0": 1509456973 + }, + "origin": "autoconf" + }, + "perl-clone": { + "versions": { + "0.39-r1": 1509477482 + }, + "origin": "perl-clone", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.3-maxminddb": { + "versions": { + "0.1-r1": 1509496574 + }, + "origin": "lua-maxminddb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmaxminddb.so.0" + ] + }, + "perl-parse-recdescent": { + "versions": { + "1.967015-r0": 1509489504 + }, + "origin": "perl-parse-recdescent", + "dependencies": [ + "perl" + ] + }, + "fftw-doc": { + "versions": { + "3.3.6p2-r0": 1509468830 + }, + "origin": "fftw" + }, + "libxcomposite-doc": { + "versions": { + "0.4.4-r1": 1509465984 + }, + "origin": "libxcomposite" + }, + "bluez-bccmd": { + "versions": { + "5.47-r3": 1510069786 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bccmd" + ] + }, + "py3-libvirt": { + "versions": { + "3.9.0-r0": 1510088399 + }, + "origin": "py-libvirt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0", + "so:libvirt-lxc.so.0", + "so:libvirt-qemu.so.0", + "so:libvirt.so.0" + ] + }, + "openntpd-doc": { + "versions": { + "6.2_p3-r0": 1510618938 + }, + "origin": "openntpd" + }, + "snappy-dbg": { + "versions": { + "1.1.4-r2": 1509483342 + }, + "origin": "snappy" + }, + "poppler-dev": { + "versions": { + "0.56.0-r0": 1509465065, + "0.56.0-r1": 1569334548 + }, + "origin": "poppler", + "dependencies": [ + "cairo-dev", + "glib-dev", + "pc:cairo>=1.10.0", + "pc:gio-2.0>=2.41", + "pc:glib-2.0>=2.41", + "pc:gobject-2.0>=2.41", + "pkgconfig", + "poppler-glib=0.56.0-r1", + "poppler=0.56.0-r1" + ], + "provides": [ + "pc:poppler-cairo=0.56.0", + "pc:poppler-cpp=0.56.0", + "pc:poppler-glib=0.56.0", + "pc:poppler-splash=0.56.0", + "pc:poppler=0.56.0" + ] + }, + "lua5.2-openrc": { + "versions": { + "0.2-r2": 1509492199 + }, + "origin": "lua-openrc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librc.so.1" + ] + }, + "acf-openvpn": { + "versions": { + "0.11.1-r2": 1510075102 + }, + "origin": "acf-openvpn", + "dependencies": [ + "acf-core", + "openvpn" + ] + }, + "qemu-system-nios2": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-nios2" + ] + }, + "lua5.1-lzmq": { + "versions": { + "0.4.4-r0": 1509475919 + }, + "origin": "lua-lzmq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5", + "so:libzmq.so.5" + ] + }, + "kamailio-ldap": { + "versions": { + "5.0.7-r0": 1532960875 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libldap-2.4.so.2", + "so:libsrdb2.so.1" + ] + }, + "libpcrecpp": { + "versions": { + "8.41-r1": 1509464277 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpcre.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libpcrecpp.so.0=0.0.1" + ] + }, + "perl-inline-c": { + "versions": { + "0.76-r0": 1509480894 + }, + "origin": "perl-inline-c" + }, + "paxctl-doc": { + "versions": { + "0.9-r0": 1509495225 + }, + "origin": "paxctl" + }, + "lighttpd-mod_webdav": { + "versions": { + "1.4.48-r0": 1511925915 + }, + "origin": "lighttpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0", + "so:libuuid.so.1", + "so:libxml2.so.2" + ] + }, + "libssl1.0": { + "versions": { + "1.0.2r-r0": 1552814669, + "1.0.2t-r0": 1568300415 + }, + "origin": "openssl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.0.0" + ], + "provides": [ + "so:libssl.so.1.0.0=1.0.0" + ] + }, + "py-larch": { + "versions": { + "1.20131130-r1": 1509552711 + }, + "origin": "py-larch", + "dependencies": [ + "python2", + "py-tracing", + "py-cliapp", + "py-ttystatus" + ], + "provides": [ + "cmd:fsck-larch" + ] + }, + "gnokii-smsd-sqlite": { + "versions": { + "0.6.31-r6": 1510069854 + }, + "origin": "gnokii", + "dependencies": [ + "gnokii-smsd", + "so:libc.musl-x86_64.so.1" + ] + }, + "cjdns-doc": { + "versions": { + "20-r1": 1510601040 + }, + "origin": "cjdns" + }, + "avahi-ui-tools": { + "versions": { + "0.6.31-r7": 1510076323 + }, + "origin": "avahi-ui", + "dependencies": [ + "py-gtk", + "py-dbus", + "py-gdbm", + "py-avahi", + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-ui-gtk3.so.0", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:avahi-bookmarks", + "cmd:avahi-discover", + "cmd:bshell", + "cmd:bssh", + "cmd:bvnc" + ] + }, + "libsoup-doc": { + "versions": { + "2.60.2-r0": 1509480669 + }, + "origin": "libsoup" + }, + "gtk+": { + "versions": { + "2.24.31-r0": 1512030427 + }, + "origin": "gtk+", + "dependencies": [ + "gtk+2.0>=2.24.31" + ] + }, + "libnfsidmap": { + "versions": { + "0.25-r1": 1509492068 + }, + "origin": "libnfsidmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnfsidmap.so.0=0.3.0" + ] + }, + "geoip-dev": { + "versions": { + "1.6.11-r0": 1509474612 + }, + "origin": "geoip", + "dependencies": [ + "geoip=1.6.11-r0", + "pkgconfig" + ], + "provides": [ + "pc:geoip=1.6.11" + ] + }, + "uwsgi-tuntap": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "sic-doc": { + "versions": { + "1.2-r0": 1509474242 + }, + "origin": "sic" + }, + "scanelf": { + "versions": { + "1.2.2-r1": 1509459679 + }, + "origin": "pax-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:scanelf" + ] + }, + "libxfce4util-dev": { + "versions": { + "4.12.1-r4": 1509468547 + }, + "origin": "libxfce4util", + "dependencies": [ + "libxfce4util=4.12.1-r4", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libxfce4util-1.0=4.12.1" + ] + }, + "perl-digest-hmac": { + "versions": { + "1.03-r0": 1509468924 + }, + "origin": "perl-digest-hmac", + "dependencies": [ + "perl", + "perl-digest-sha1" + ] + }, + "freeswitch-timezones": { + "versions": { + "1.6.19-r0": 1510585357 + }, + "origin": "freeswitch" + }, + "unfs3": { + "versions": { + "0.9.22-r4": 1509496017 + }, + "origin": "unfs3", + "dependencies": [ + "rpcbind", + "so:libc.musl-x86_64.so.1", + "so:libfl.so.2", + "so:libtirpc.so.3" + ], + "provides": [ + "cmd:unfsd" + ] + }, + "py3-dateutil": { + "versions": { + "2.6.1-r0": 1509475854 + }, + "origin": "py-dateutil", + "dependencies": [ + "py3-six", + "python3" + ] + }, + "usbredir-dev": { + "versions": { + "0.7.1-r0": 1509707469 + }, + "origin": "usbredir", + "dependencies": [ + "libusb-dev", + "pc:libusb-1.0", + "pkgconfig", + "usbredir=0.7.1-r0" + ], + "provides": [ + "pc:libusbredirhost=0.7.1", + "pc:libusbredirparser-0.5=0.7.1" + ] + }, + "apr": { + "versions": { + "1.6.3-r0": 1509476182 + }, + "origin": "apr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libapr-1.so.0=0.6.3" + ] + }, + "perl-struct-dumb-doc": { + "versions": { + "0.07-r0": 1509480426 + }, + "origin": "perl-struct-dumb" + }, + "openresolv-doc": { + "versions": { + "3.9.0-r0": 1509476550 + }, + "origin": "openresolv" + }, + "mousepad": { + "versions": { + "0.4.0-r2": 1510073846 + }, + "origin": "mousepad", + "dependencies": [ + "desktop-file-utils", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libgtksourceview-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0" + ], + "provides": [ + "cmd:mousepad" + ] + }, + "aspell-compat": { + "versions": { + "0.60.6.1-r12": 1509472867, + "0.60.6.1-r13": 1572541141 + }, + "origin": "aspell", + "dependencies": [ + "aspell" + ], + "provides": [ + "cmd:ispell", + "cmd:spell" + ] + }, + "perl-test2-plugin-nowarnings-doc": { + "versions": { + "0.06-r0": 1510588886 + }, + "origin": "perl-test2-plugin-nowarnings" + }, + "spamassassin": { + "versions": { + "3.4.1-r8": 1509517414 + }, + "origin": "spamassassin", + "dependencies": [ + "perl-mail-spamassassin", + "curl" + ], + "provides": [ + "cmd:sa-awl", + "cmd:sa-check_spamd", + "cmd:sa-learn", + "cmd:sa-update", + "cmd:spamassassin", + "cmd:spamd" + ] + }, + "py3-snowballstemmer": { + "versions": { + "1.2.1-r1": 1509489495 + }, + "origin": "py-snowballstemmer", + "dependencies": [ + "python3" + ] + }, + "newsbeuter": { + "versions": { + "2.9-r6": 1510288209 + }, + "origin": "newsbeuter", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libjson-c.so.2", + "so:libncursesw.so.6", + "so:libsqlite3.so.0", + "so:libstdc++.so.6", + "so:libstfl.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:newsbeuter", + "cmd:podbeuter" + ] + }, + "mwm": { + "versions": { + "2.3.4-r2": 1509495433 + }, + "origin": "motif", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXm.so.4", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mwm", + "cmd:xmbind" + ] + }, + "lua-crypto-dev": { + "versions": { + "0.3.2-r5": 1510261400 + }, + "origin": "lua-crypto", + "dependencies": [ + "pc:openssl", + "pkgconfig" + ], + "provides": [ + "pc:luacrypto=0.3.1" + ] + }, + "libxcursor": { + "versions": { + "1.1.15-r0": 1511975691 + }, + "origin": "libxcursor", + "dependencies": [ + "so:libX11.so.6", + "so:libXfixes.so.3", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXcursor.so.1=1.0.2" + ] + }, + "libmount": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmount.so.1=1.1.0" + ] + }, + "tcl-tls": { + "versions": { + "1.7.10-r2": 1510260768 + }, + "origin": "tcl-tls", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "abiword-plugin-pdf": { + "versions": { + "3.0.2-r1": 1510073367 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "bigreqsproto": { + "versions": { + "1.1.2-r3": 1509470423 + }, + "origin": "bigreqsproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:bigreqsproto=1.1.2" + ] + }, + "printproto": { + "versions": { + "1.0.5-r2": 1509494090 + }, + "origin": "printproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:printproto=1.0.5" + ] + }, + "fontconfig": { + "versions": { + "2.12.6-r0": 1509462083 + }, + "origin": "fontconfig", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libfreetype.so.6" + ], + "provides": [ + "so:libfontconfig.so.1=1.10.1", + "cmd:fc-cache", + "cmd:fc-cat", + "cmd:fc-list", + "cmd:fc-match", + "cmd:fc-pattern", + "cmd:fc-query", + "cmd:fc-scan", + "cmd:fc-validate" + ] + }, + "bitchx": { + "versions": { + "1.2.1-r7": 1510260134 + }, + "origin": "bitchx", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libncursesw.so.6", + "so:libssl.so.44" + ], + "provides": [ + "cmd:BitchX", + "cmd:BitchX-1.2.1", + "cmd:scr-bx" + ] + }, + "nginx-mod-http-lua": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "nginx-mod-devel-kit", + "so:libc.musl-x86_64.so.1", + "so:libluajit-5.1.so.2" + ], + "provides": [ + "nginx-lua" + ] + }, + "lua5.3-lxc": { + "versions": { + "2.1.1-r3": 1533578833 + }, + "origin": "lxc", + "dependencies": [ + "gzip", + "so:libc.musl-x86_64.so.1", + "so:liblxc.so.1" + ] + }, + "lua5.1-ossl": { + "versions": { + "20171028-r1": 1510260842 + }, + "origin": "lua-ossl", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ] + }, + "libsoup-lang": { + "versions": { + "2.60.2-r0": 1509480670 + }, + "origin": "libsoup", + "dependencies": [ + "glib-networking" + ] + }, + "libmodplug-doc": { + "versions": { + "0.8.9.0-r0": 1509489449 + }, + "origin": "libmodplug" + }, + "perl-html-tree": { + "versions": { + "5.07-r0": 1509481555 + }, + "origin": "perl-html-tree", + "dependencies": [ + "perl-html-tagset", + "perl-html-parser" + ], + "provides": [ + "cmd:htmltree" + ] + }, + "xfconf-dev": { + "versions": { + "4.12.1-r2": 1510067932 + }, + "origin": "xfconf", + "dependencies": [ + "pc:dbus-1", + "pc:dbus-glib-1", + "pc:gobject-2.0", + "pkgconfig", + "xfconf=4.12.1-r2" + ], + "provides": [ + "pc:libxfconf-0=4.12.1" + ] + }, + "dzen-gadgets": { + "versions": { + "0.9.5-r3": 1509787635 + }, + "origin": "dzen", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dbar", + "cmd:gcpubar", + "cmd:gdbar", + "cmd:textwidth" + ] + }, + "perl-carp-clan": { + "versions": { + "6.06-r1": 1509473080 + }, + "origin": "perl-carp-clan", + "dependencies": [ + "perl", + "perl-test-exception" + ] + }, + "mdocml-dev": { + "versions": { + "1.14.3-r0": 1509459636 + }, + "origin": "mdocml" + }, + "mtr": { + "versions": { + "0.92-r0": 1510072769 + }, + "origin": "mtr", + "dependencies": [ + "ncurses", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:mtr", + "cmd:mtr-packet" + ] + }, + "perl-fcgi-procmanager": { + "versions": { + "0.28-r0": 1511894069 + }, + "origin": "perl-fcgi-procmanager", + "dependencies": [ + "perl" + ] + }, + "udisks-lang": { + "versions": { + "1.0.5-r3": 1510073403 + }, + "origin": "udisks" + }, + "sems-speex": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libspeex.so.1" + ] + }, + "libical": { + "versions": { + "2.0.0-r2": 1509468579 + }, + "origin": "libical", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libical.so.2=2.0.0", + "so:libical_cxx.so.2=2.0.0", + "so:libicalss.so.2=2.0.0", + "so:libicalss_cxx.so.2=2.0.0", + "so:libicalvcal.so.2=2.0.0" + ] + }, + "lvm2-dev": { + "versions": { + "2.02.175-r0": 1509462401 + }, + "origin": "lvm2", + "dependencies": [ + "linux-headers", + "device-mapper-event-libs=2.02.175-r0", + "device-mapper-libs=2.02.175-r0", + "lvm2-libs=2.02.175-r0", + "pc:blkid", + "pkgconfig" + ], + "provides": [ + "pc:devmapper-event=1.02.144", + "pc:devmapper=1.02.144", + "pc:lvm2app=2.2" + ] + }, + "qemu-system-sparc": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-sparc" + ] + }, + "bc": { + "versions": { + "1.07.1-r0": 1509465953 + }, + "origin": "bc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:bc", + "cmd:dc" + ] + }, + "perl-term-readkey": { + "versions": { + "2.37-r1": 1509494716 + }, + "origin": "perl-term-readkey", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "acf-clamav": { + "versions": { + "0.8.0-r2": 1510068298 + }, + "origin": "acf-clamav", + "dependencies": [ + "acf-core", + "clamav" + ] + }, + "ssmtp": { + "versions": { + "2.64-r12": 1510260822 + }, + "origin": "ssmtp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:sendmail", + "cmd:ssmtp" + ] + }, + "lua5.1-maxminddb": { + "versions": { + "0.1-r1": 1509496568 + }, + "origin": "lua-maxminddb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmaxminddb.so.0" + ] + }, + "uwsgi-signal": { + "versions": { + "2.0.17-r0": 1522154659 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-test-longstring": { + "versions": { + "0.15-r0": 1509470426 + }, + "origin": "perl-test-longstring", + "dependencies": [ + "perl" + ] + }, + "openldap-back-null": { + "versions": { + "2.4.45-r3": 1510258133, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "lua5.2-evdev": { + "versions": { + "2.2.1-r1": 1509488870 + }, + "origin": "lua-evdev", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "userspace-rcu": { + "versions": { + "0.10.0-r0": 1509479773 + }, + "origin": "userspace-rcu", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liburcu-bp.so.6=6.0.0", + "so:liburcu-cds.so.6=6.0.0", + "so:liburcu-common.so.6=6.0.0", + "so:liburcu-mb.so.6=6.0.0", + "so:liburcu-qsbr.so.6=6.0.0", + "so:liburcu-signal.so.6=6.0.0", + "so:liburcu.so.6=6.0.0" + ] + }, + "lua5.2-rex-pcre": { + "versions": { + "2.9.0-r0": 1510619366 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ] + }, + "live-media-utils": { + "versions": { + "2017.10.28-r0": 1510046892 + }, + "origin": "live-media", + "dependencies": [ + "so:libBasicUsageEnvironment.so.1", + "so:libUsageEnvironment.so.3", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgroupsock.so.8", + "so:libliveMedia.so.61", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:MPEG2TransportStreamIndexer", + "cmd:openRTSP", + "cmd:playSIP", + "cmd:registerRTSPStream", + "cmd:sapWatch", + "cmd:testAMRAudioStreamer", + "cmd:testDVVideoStreamer", + "cmd:testH264VideoStreamer", + "cmd:testH264VideoToTransportStream", + "cmd:testH265VideoStreamer", + "cmd:testH265VideoToTransportStream", + "cmd:testMKVStreamer", + "cmd:testMP3Receiver", + "cmd:testMP3Streamer", + "cmd:testMPEG1or2AudioVideoStreamer", + "cmd:testMPEG1or2ProgramToTransportStream", + "cmd:testMPEG1or2Splitter", + "cmd:testMPEG1or2VideoReceiver", + "cmd:testMPEG1or2VideoStreamer", + "cmd:testMPEG2TransportReceiver", + "cmd:testMPEG2TransportStreamTrickPlay", + "cmd:testMPEG2TransportStreamer", + "cmd:testMPEG4VideoStreamer", + "cmd:testOggStreamer", + "cmd:testOnDemandRTSPServer", + "cmd:testRTSPClient", + "cmd:testRelay", + "cmd:testReplicator", + "cmd:testWAVAudioStreamer", + "cmd:vobStreamer" + ] + }, + "openldap-overlay-constraint": { + "versions": { + "2.4.45-r3": 1510258134, + "2.4.46-r0": 1565073940, + "2.4.48-r0": 1566900517 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "perl-heap-doc": { + "versions": { + "0.80-r1": 1511989874 + }, + "origin": "perl-heap" + }, + "xcb-util-keysyms-dev": { + "versions": { + "0.4.0-r1": 1509473914 + }, + "origin": "xcb-util-keysyms", + "dependencies": [ + "xcb-util-dev", + "pkgconfig", + "xcb-util-keysyms=0.4.0-r1" + ], + "provides": [ + "pc:xcb-keysyms=0.4.0" + ] + }, + "lua5.1-sql-mysql": { + "versions": { + "2.3.5-r1": 1509488828 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "pangox-compat": { + "versions": { + "0.0.2-r0": 1509482459 + }, + "origin": "pangox-compat", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpango-1.0.so.0" + ], + "provides": [ + "so:libpangox-1.0.so.0=0.0.0" + ] + }, + "uwsgi-rawrouter": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "protobuf": { + "versions": { + "3.4.1-r1": 1510846094 + }, + "origin": "protobuf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libprotobuf-lite.so.14=14.0.0", + "so:libprotobuf.so.14=14.0.0", + "so:libprotoc.so.14=14.0.0", + "cmd:protoc" + ] + }, + "btrfs-progs-libs": { + "versions": { + "4.13.2-r0": 1509481818 + }, + "origin": "btrfs-progs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libbtrfs.so.0=0.1" + ] + }, + "thunar-vcs-plugin-lang": { + "versions": { + "0.1.4-r6": 1510314592 + }, + "origin": "thunar-vcs-plugin" + }, + "py3-rsa": { + "versions": { + "3.4.2-r2": 1509551793 + }, + "origin": "py-rsa", + "dependencies": [ + "py3-asn1", + "python3" + ], + "provides": [ + "cmd:pyrsa-decrypt", + "cmd:pyrsa-decrypt-bigfile", + "cmd:pyrsa-encrypt", + "cmd:pyrsa-encrypt-bigfile", + "cmd:pyrsa-keygen", + "cmd:pyrsa-priv2pub", + "cmd:pyrsa-sign", + "cmd:pyrsa-verify" + ] + }, + "grub-dev": { + "versions": { + "2.02-r3": 1509495739 + }, + "origin": "grub" + }, + "consolekit2-lang": { + "versions": { + "1.2.0-r2": 1510067530 + }, + "origin": "consolekit2", + "dependencies": [ + "polkit", + "eudev" + ] + }, + "vanessa_socket-doc": { + "versions": { + "0.0.13-r0": 1511165897 + }, + "origin": "vanessa_socket" + }, + "py2-nose": { + "versions": { + "1.3.7-r2": 1509552700 + }, + "origin": "py-nose", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:nosetests-2.7" + ] + }, + "chezdav": { + "versions": { + "2.2-r0": 1509492232 + }, + "origin": "phodav", + "dependencies": [ + "so:libavahi-gobject.so.0", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libphodav-2.0.so.0", + "so:libsoup-2.4.so.1" + ], + "provides": [ + "cmd:chezdav" + ] + }, + "tmux-doc": { + "versions": { + "2.6-r0": 1509469998 + }, + "origin": "tmux" + }, + "gptfdisk": { + "versions": { + "1.0.3-r0": 1509495224 + }, + "origin": "gptfdisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:cgdisk", + "cmd:fixparts", + "cmd:gdisk" + ] + }, + "enchant-dev": { + "versions": { + "1.6.0-r12": 1509477681 + }, + "origin": "enchant", + "dependencies": [ + "glib-dev", + "enchant=1.6.0-r12", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pkgconfig" + ], + "provides": [ + "pc:enchant=1.6.0" + ] + }, + "wxgtk2.8-lang": { + "versions": { + "2.8.12.1-r4": 1510928419 + }, + "origin": "wxgtk2.8" + }, + "squid-lang-lv": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-test-requiresinternet-doc": { + "versions": { + "0.05-r0": 1509464419 + }, + "origin": "perl-test-requiresinternet" + }, + "gdl-lang": { + "versions": { + "3.22.0-r0": 1510074987 + }, + "origin": "gdl" + }, + "cairomm": { + "versions": { + "1.12.2-r0": 1509472991 + }, + "origin": "cairomm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgcc_s.so.1", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcairomm-1.0.so.1=1.4.0" + ] + }, + "py2-rsa": { + "versions": { + "3.4.2-r2": 1509551794 + }, + "origin": "py-rsa", + "dependencies": [ + "py2-asn1", + "python2" + ], + "provides": [ + "cmd:pyrsa-decrypt", + "cmd:pyrsa-decrypt-bigfile", + "cmd:pyrsa-encrypt", + "cmd:pyrsa-encrypt-bigfile", + "cmd:pyrsa-keygen", + "cmd:pyrsa-priv2pub", + "cmd:pyrsa-sign", + "cmd:pyrsa-verify" + ] + }, + "font-adobe-utopia-100dpi": { + "versions": { + "1.0.4-r0": 1509477309 + }, + "origin": "font-adobe-utopia-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "py-dbus-doc": { + "versions": { + "1.2.0-r2": 1509465382 + }, + "origin": "py-dbus" + }, + "nghttp2-libs": { + "versions": { + "1.28.0-r0": 1511922922, + "1.39.2-r0": 1568186892 + }, + "origin": "nghttp2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnghttp2.so.14=14.18.0" + ] + }, + "uwsgi-router_rewrite": { + "versions": { + "2.0.17-r0": 1522154658 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "mdadm-doc": { + "versions": { + "4.0-r0": 1509476487 + }, + "origin": "mdadm" + }, + "knock-doc": { + "versions": { + "0.7-r1": 1509480796 + }, + "origin": "knock" + }, + "lcms2": { + "versions": { + "2.8-r1": 1509464953 + }, + "origin": "lcms2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblcms2.so.2=2.0.8" + ] + }, + "xfce4-session-dev": { + "versions": { + "4.12.1-r2": 1510074571 + }, + "origin": "xfce4-session", + "dependencies": [ + "libxfce4ui-dev", + "xfconf-dev", + "pc:libxfce4ui-1", + "pc:libxfconf-0", + "pkgconfig", + "xfce4-session=4.12.1-r2" + ], + "provides": [ + "pc:xfce4-session-2.0=4.12.1" + ] + }, + "kbd": { + "versions": { + "2.0.4-r2": 1510922533 + }, + "origin": "kbd", + "dependencies": [ + "kbd-misc", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chvt", + "cmd:deallocvt", + "cmd:dumpkeys", + "cmd:fgconsole", + "cmd:getkeycodes", + "cmd:kbd_mode", + "cmd:kbdinfo", + "cmd:kbdrate", + "cmd:loadkeys", + "cmd:loadunimap", + "cmd:mapscrn", + "cmd:open", + "cmd:openvt", + "cmd:psfaddtable", + "cmd:psfgettable", + "cmd:psfstriptable", + "cmd:psfxtable", + "cmd:resizecons", + "cmd:setfont", + "cmd:setkeycodes", + "cmd:setleds", + "cmd:setmetamode", + "cmd:setvtrgb", + "cmd:showconsolefont", + "cmd:showkey", + "cmd:unicode_start", + "cmd:unicode_stop" + ] + }, + "libgudev-dev": { + "versions": { + "230-r2": 1509470858 + }, + "origin": "libgudev", + "dependencies": [ + "libgudev=230-r2", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gudev-1.0=230" + ] + }, + "libexif": { + "versions": { + "0.6.21-r2": 1539006612 + }, + "origin": "libexif", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libexif.so.12=12.3.3" + ] + }, + "openipmi-doc": { + "versions": { + "2.0.22-r1": 1510260043 + }, + "origin": "openipmi" + }, + "nginx-mod-http-lua-upstream": { + "versions": { + "1.12.2-r4": 1542814448 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "nginx-mod-http-lua", + "so:libc.musl-x86_64.so.1" + ] + }, + "py3-itsdangerous": { + "versions": { + "0.24-r3": 1509551859 + }, + "origin": "py-itsdangerous", + "dependencies": [ + "python3" + ] + }, + "smokeping": { + "versions": { + "2.6.11-r3": 1510564559 + }, + "origin": "smokeping", + "dependencies": [ + "perl", + "fping", + "rrdtool", + "perl-rrd", + "perl-uri", + "perl-digest-hmac", + "perl-cgi-session", + "perl-io-tty", + "perl-libwww", + "perl-ldap", + "perl-snmp-session", + "perl-net-dns", + "perl-net-openssh", + "perl-net-snmp", + "perl-net-telnet", + "perl-fcgi", + "perl-config-grammar", + "perl-cgi", + "perl-cgi-fast", + "/bin/sh" + ], + "provides": [ + "cmd:smokeinfo", + "cmd:smokeping", + "cmd:smokeping_cgi", + "cmd:tSmoke" + ] + }, + "myrepos-doc": { + "versions": { + "1.20180726-r0": 1534931445 + }, + "origin": "myrepos" + }, + "freeswitch-freetdm": { + "versions": { + "1.6.19-r0": 1510585357 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexecinfo.so.1", + "so:libfreeswitch.so.1", + "so:libpri.so.1.4" + ], + "provides": [ + "so:libfreetdm.so.1=1.0.0" + ] + }, + "libdbi-doc": { + "versions": { + "0.9.0-r0": 1509474452 + }, + "origin": "libdbi" + }, + "qt-lang": { + "versions": { + "4.8.7-r8": 1510287210 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates" + ] + }, + "perl-exporter-doc": { + "versions": { + "5.72-r1": 1509483328 + }, + "origin": "perl-exporter" + }, + "py3-tornado": { + "versions": { + "4.5.2-r1": 1509551890 + }, + "origin": "py-tornado", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "libnl3-doc": { + "versions": { + "3.2.28-r1": 1509475886 + }, + "origin": "libnl3" + }, + "net-tools": { + "versions": { + "1.60_git20140218-r1": 1509492734 + }, + "origin": "net-tools", + "dependencies": [ + "mii-tool", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:arp", + "cmd:dnsdomainname", + "cmd:domainname", + "cmd:hostname", + "cmd:ifconfig", + "cmd:ipmaddr", + "cmd:iptunnel", + "cmd:nameif", + "cmd:netstat", + "cmd:nisdomainname", + "cmd:plipconfig", + "cmd:rarp", + "cmd:route", + "cmd:slattach", + "cmd:ypdomainname" + ] + }, + "perl-package-deprecationmanager": { + "versions": { + "0.17-r0": 1509493030 + }, + "origin": "perl-package-deprecationmanager", + "dependencies": [ + "perl-list-moreutils", + "perl-sub-install", + "perl-params-util", + "perl-test-fatal", + "perl-test-requires", + "perl-package-stash", + "perl-test-warnings", + "perl-sub-name" + ] + }, + "openssh-client": { + "versions": { + "7.5_p1-r10": 1551712287 + }, + "origin": "openssh", + "dependencies": [ + "openssh-keygen", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libz.so.1" + ], + "provides": [ + "cmd:findssl.sh", + "cmd:scp", + "cmd:sftp", + "cmd:ssh", + "cmd:ssh-add", + "cmd:ssh-agent", + "cmd:ssh-copy-id", + "cmd:ssh-keyscan", + "cmd:ssh-pkcs11-helper" + ] + }, + "setup-box": { + "versions": { + "1.0.1-r0": 1509490948 + }, + "origin": "setup-box", + "dependencies": [ + "jq" + ], + "provides": [ + "cmd:setup-box" + ] + }, + "py-bluez": { + "versions": { + "0.22-r1": 1510075579 + }, + "origin": "py-bluez" + }, + "libnfsidmap-dev": { + "versions": { + "0.25-r1": 1509492062 + }, + "origin": "libnfsidmap", + "dependencies": [ + "libnfsidmap=0.25-r1", + "pkgconfig" + ], + "provides": [ + "pc:libnfsidmap=0.25" + ] + }, + "aports-build": { + "versions": { + "1.2-r0": 1509462510 + }, + "origin": "aports-build", + "dependencies": [ + "abuild>2.20.0", + "mosquitto-clients", + "alpine-sdk", + "mqtt-exec", + "rsync", + "lua-aports", + "lua5.2-cjson", + "lua5.2-mqtt-publish", + "/bin/sh" + ], + "provides": [ + "cmd:aports-build" + ] + }, + "libdv-dev": { + "versions": { + "1.0.0-r3": 1510068408 + }, + "origin": "libdv", + "dependencies": [ + "libdv=1.0.0-r3", + "pkgconfig" + ], + "provides": [ + "pc:libdv=1.0.0" + ] + }, + "nginx-mod-http-nchan": { + "versions": { + "1.12.2-r4": 1542814448 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "nagios-plugins-breeze": { + "versions": { + "2.2.1-r3": 1510288494 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins" + ] + }, + "usb-modeswitch-udev": { + "versions": { + "2.5.1-r0": 1509480148 + }, + "origin": "usb-modeswitch", + "dependencies": [ + "tcl", + "eudev", + "usb-modeswitch=2.5.1-r0" + ], + "provides": [ + "cmd:usb_modeswitch_dispatcher" + ] + }, + "libraw1394-tools": { + "versions": { + "2.1.2-r0": 1509470074 + }, + "origin": "libraw1394", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libraw1394.so.11" + ], + "provides": [ + "cmd:dumpiso", + "cmd:sendiso", + "cmd:testlibraw" + ] + }, + "cpufrequtils-doc": { + "versions": { + "008-r4": 1509486336 + }, + "origin": "cpufrequtils" + }, + "perl-role-tiny-doc": { + "versions": { + "2.000005-r0": 1509481549 + }, + "origin": "perl-role-tiny" + }, + "libdaemon-doc": { + "versions": { + "0.14-r2": 1509465393 + }, + "origin": "libdaemon" + }, + "gross": { + "versions": { + "1.0.2-r11": 1509482387 + }, + "origin": "gross", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2" + ], + "provides": [ + "so:grosscheck.so=0", + "cmd:gclient", + "cmd:grossd" + ] + }, + "gdk-pixbuf": { + "versions": { + "2.36.10-r0": 1509465514 + }, + "origin": "gdk-pixbuf", + "dependencies": [ + "shared-mime-info", + "/bin/sh", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:libpng16.so.16" + ], + "provides": [ + "so:libgdk_pixbuf-2.0.so.0=0.3610.0", + "so:libgdk_pixbuf_xlib-2.0.so.0=0.3610.0", + "cmd:gdk-pixbuf-csource", + "cmd:gdk-pixbuf-pixdata", + "cmd:gdk-pixbuf-query-loaders", + "cmd:gdk-pixbuf-thumbnailer" + ] + }, + "rpcgen": { + "versions": { + "2.1.1-r4": 1509492107 + }, + "origin": "nfs-utils", + "dependencies": [ + "rpcbind", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rpcgen" + ] + }, + "py-babel": { + "versions": { + "2.5.1-r0": 1509490097 + }, + "origin": "py-babel", + "dependencies": [ + "py-tz" + ] + }, + "liblastfm": { + "versions": { + "1.0.9-r1": 1510074389 + }, + "origin": "liblastfm", + "dependencies": [ + "so:libQtCore.so.4", + "so:libQtDBus.so.4", + "so:libQtNetwork.so.4", + "so:libQtSql.so.4", + "so:libQtXml.so.4", + "so:libc.musl-x86_64.so.1", + "so:libfftw3f.so.3", + "so:libgcc_s.so.1", + "so:libsamplerate.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:liblastfm.so.1=1.0.9", + "so:liblastfm_fingerprint.so.1=1.0.9" + ] + }, + "gtk-murrine-engine": { + "versions": { + "0.98.2-r2": 1510075986 + }, + "origin": "gtk-murrine-engine", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpixman-1.so.0" + ] + }, + "udisks2-libs": { + "versions": { + "2.6.5-r0": 1510075152 + }, + "origin": "udisks2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libudisks2.so.0=0.0.0" + ] + }, + "spl-vanilla": { + "versions": { + "4.9.161-r0": 1551779820, + "4.9.182-r0": 1560866250 + }, + "origin": "spl-vanilla", + "dependencies": [ + "linux-vanilla=4.9.182-r0" + ] + }, + "axel": { + "versions": { + "2.4-r1": 1509494658 + }, + "origin": "axel", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:axel" + ] + }, + "lua-filesystem": { + "versions": { + "1.7.0.2-r0": 1511049451 + }, + "origin": "lua-filesystem" + }, + "libcanberra-dev": { + "versions": { + "0.30-r1": 1510071974 + }, + "origin": "libcanberra", + "dependencies": [ + "gtk+-dev", + "libogg-dev", + "libvorbis-dev", + "alsa-lib-dev", + "libtool", + "gtk+3.0-dev", + "libcanberra-gtk2=0.30-r1", + "libcanberra-gtk3=0.30-r1", + "libcanberra=0.30-r1", + "pc:gdk-2.0", + "pc:gdk-3.0", + "pc:gtk+-2.0", + "pc:gtk+-3.0", + "pkgconfig" + ], + "provides": [ + "pc:libcanberra-gtk3=0.30", + "pc:libcanberra-gtk=0.30", + "pc:libcanberra=0.30" + ] + }, + "sox": { + "versions": { + "14.4.2-r0": 1510074688 + }, + "origin": "sox", + "dependencies": [ + "so:libFLAC.so.8", + "so:libao.so.4", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libgomp.so.1", + "so:libgsm.so.1", + "so:libltdl.so.7", + "so:libmad.so.0", + "so:libmagic.so.1", + "so:libmp3lame.so.0", + "so:libogg.so.0", + "so:libopusfile.so.0", + "so:libsndfile.so.1", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2", + "so:libvorbisfile.so.3" + ], + "provides": [ + "so:libsox.so.3=3.0.0", + "cmd:play", + "cmd:rec", + "cmd:sox", + "cmd:soxi" + ] + }, + "lddtree": { + "versions": { + "1.26-r1": 1509462282 + }, + "origin": "lddtree", + "dependencies": [ + "scanelf" + ], + "provides": [ + "cmd:lddtree" + ] + }, + "nagios-plugins-file_age": { + "versions": { + "2.2.1-r3": 1510288495 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins" + ] + }, + "glib": { + "versions": { + "2.54.2-r0": 1509911133, + "2.54.2-r1": 1560764733 + }, + "origin": "glib", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libintl.so.8", + "so:libmount.so.1", + "so:libpcre.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libgio-2.0.so.0=0.5400.2", + "so:libglib-2.0.so.0=0.5400.2", + "so:libgmodule-2.0.so.0=0.5400.2", + "so:libgobject-2.0.so.0=0.5400.2", + "so:libgthread-2.0.so.0=0.5400.2", + "cmd:gapplication", + "cmd:gdbus", + "cmd:gio", + "cmd:gio-querymodules", + "cmd:glib-compile-schemas", + "cmd:gsettings" + ] + }, + "freeradius-lib": { + "versions": { + "3.0.15-r4": 1556202796, + "3.0.15-r5": 1566310603 + }, + "origin": "freeradius", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpcap.so.1", + "so:libssl.so.44", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libfreeradius-dhcp.so=0", + "so:libfreeradius-eap.so=0", + "so:libfreeradius-radius.so=0", + "so:libfreeradius-server.so=0" + ] + }, + "qt-config": { + "versions": { + "4.8.7-r8": 1510287208 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtGui.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:qtconfig" + ] + }, + "lua5.3-lzmq": { + "versions": { + "0.4.4-r0": 1509475920 + }, + "origin": "lua-lzmq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5", + "so:libzmq.so.5" + ] + }, + "exiv2-doc": { + "versions": { + "0.25-r0": 1509475473 + }, + "origin": "exiv2" + }, + "perl-params-classify-doc": { + "versions": { + "0.015-r0": 1509470454 + }, + "origin": "perl-params-classify" + }, + "xf86-video-ark": { + "versions": { + "0.7.5-r7": 1510074927 + }, + "origin": "xf86-video-ark", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-tree-dag_node": { + "versions": { + "1.29-r0": 1509494497 + }, + "origin": "perl-tree-dag_node", + "dependencies": [ + "perl-file-slurp-tiny" + ] + }, + "xkbcomp-dev": { + "versions": { + "1.4.0-r2": 1509473696 + }, + "origin": "xkbcomp", + "dependencies": [ + "pc:x11", + "pc:xkbfile", + "pc:xproto>=7.0.17", + "pkgconfig" + ], + "provides": [ + "pc:xkbcomp=1.4.0" + ] + }, + "pkgconf-dev": { + "versions": { + "1.3.10-r0": 1509459814 + }, + "origin": "pkgconf", + "dependencies": [ + "pkgconf=1.3.10-r0", + "pkgconfig" + ], + "provides": [ + "pc:libpkgconf=1.3.10" + ] + }, + "gdk-pixbuf-doc": { + "versions": { + "2.36.10-r0": 1509465514 + }, + "origin": "gdk-pixbuf" + }, + "aconf-mod-openssh": { + "versions": { + "0.6.5-r0": 1510073710 + }, + "origin": "aconf", + "dependencies": [ + "aconf", + "openssh" + ] + }, + "aria2": { + "versions": { + "1.33.1-r1": 1548941587 + }, + "origin": "aria2", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libgmp.so.10", + "so:libgnutls.so.30", + "so:libnettle.so.6", + "so:libsqlite3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:aria2c" + ] + }, + "pekwm": { + "versions": { + "0.1.17-r2": 1509494548 + }, + "origin": "pekwm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXpm.so.4", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpng16.so.16", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:pekwm" + ] + }, + "nginx-mod-http-echo": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "iftop": { + "versions": { + "0.17-r7": 1509489489 + }, + "origin": "iftop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:iftop" + ] + }, + "dhcpcd-ui-doc": { + "versions": { + "0.7.5-r1": 1510073427 + }, + "origin": "dhcpcd-ui" + }, + "msmtp-vim": { + "versions": { + "1.6.6-r2": 1510261496 + }, + "origin": "msmtp" + }, + "libglade-dev": { + "versions": { + "2.6.4-r14": 1510071790 + }, + "origin": "libglade", + "dependencies": [ + "gtk+2.0-dev", + "libxml2-dev", + "libglade=2.6.4-r14", + "pc:gtk+-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libglade-2.0=2.6.4" + ] + }, + "dev86": { + "versions": { + "0.16.21-r0": 1509479310 + }, + "origin": "dev86", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ar86", + "cmd:as86", + "cmd:bcc", + "cmd:ld86", + "cmd:nm86", + "cmd:objdump86", + "cmd:size86" + ] + }, + "cifs-utils-doc": { + "versions": { + "6.7-r1": 1509475350 + }, + "origin": "cifs-utils" + }, + "xrandr-doc": { + "versions": { + "1.5.0-r1": 1509491916 + }, + "origin": "xrandr" + }, + "postfix-pgsql": { + "versions": { + "3.2.4-r1": 1510285340 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:postfix-pgsql.so=0" + ] + }, + "xcb-util-cursor": { + "versions": { + "0.1.3-r1": 1509491137 + }, + "origin": "xcb-util-cursor", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-image.so.0", + "so:libxcb-render-util.so.0", + "so:libxcb-render.so.0", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-cursor.so.0=0.0.0" + ] + }, + "ragel-doc": { + "versions": { + "6.10-r0": 1509490668 + }, + "origin": "ragel" + }, + "perl-ipc-system-simple": { + "versions": { + "1.25-r0": 1509495320 + }, + "origin": "perl-ipc-system-simple" + }, + "perl-net-ssleay-doc": { + "versions": { + "1.82-r0": 1510260115 + }, + "origin": "perl-net-ssleay" + }, + "lua5.3-sql-sqlite3": { + "versions": { + "2.3.5-r1": 1509488833 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "phodav-doc": { + "versions": { + "2.2-r0": 1509492230 + }, + "origin": "phodav" + }, + "duplicity-lang": { + "versions": { + "0.7.13.1-r0": 1510588266 + }, + "origin": "duplicity", + "dependencies": [ + "python2", + "py-boto", + "gnupg", + "ncftp", + "py-lockfile" + ] + }, + "binutils-dev": { + "versions": { + "2.30-r1": 1527754980, + "2.30-r2": 1565788943 + }, + "origin": "binutils", + "dependencies": [ + "binutils-libs=2.30-r2" + ] + }, + "lcms-doc": { + "versions": { + "1.19-r6": 1509477354 + }, + "origin": "lcms" + }, + "procps-dev": { + "versions": { + "3.3.12-r3": 1509492054 + }, + "origin": "procps", + "dependencies": [ + "libproc=3.3.12-r3", + "pkgconfig" + ] + }, + "po4a-lang": { + "versions": { + "0.51-r1": 1509459568 + }, + "origin": "po4a", + "dependencies": [ + "perl", + "gettext" + ] + }, + "libnetfilter_conntrack": { + "versions": { + "1.0.6-r0": 1509469226 + }, + "origin": "libnetfilter_conntrack", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnfnetlink.so.0" + ], + "provides": [ + "so:libnetfilter_conntrack.so.3=3.6.0" + ] + }, + "py3-gobject3": { + "versions": { + "3.24.1-r2": 1509481893 + }, + "origin": "py-gobject3", + "dependencies": [ + "py3-cairo", + "so:libc.musl-x86_64.so.1", + "so:libcairo-gobject.so.2", + "so:libcairo.so.2", + "so:libffi.so.6", + "so:libgirepository-1.0.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ] + }, + "apache2-proxy": { + "versions": { + "2.4.39-r0": 1554306882, + "2.4.41-r0": 1566292328 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-pbr": { + "versions": { + "3.1.1-r0": 1509480861 + }, + "origin": "py-pbr" + }, + "xcb-proto": { + "versions": { + "1.12-r1": 1509461815 + }, + "origin": "xcb-proto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xcb-proto=1.12" + ] + }, + "irssi-perl": { + "versions": { + "1.0.6-r0": 1519052408, + "1.0.8-r0": 1562236917 + }, + "origin": "irssi", + "dependencies": [ + "irssi", + "perl", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "perl-encode-hanextra": { + "versions": { + "0.23-r2": 1509489061 + }, + "origin": "perl-encode-hanextra", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "meson-doc": { + "versions": { + "0.43.0-r0": 1509475829 + }, + "origin": "meson" + }, + "libxkbcommon": { + "versions": { + "0.7.1-r1": 1509482486 + }, + "origin": "libxkbcommon", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-xkb.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxkbcommon-x11.so.0=0.0.0", + "so:libxkbcommon.so.0=0.0.0" + ] + }, + "abiword-doc": { + "versions": { + "3.0.2-r1": 1510073355 + }, + "origin": "abiword" + }, + "gmp": { + "versions": { + "6.1.2-r1": 1509456917 + }, + "origin": "gmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgmp.so.10=10.3.2" + ] + }, + "yeahconsole": { + "versions": { + "0.3.4-r0": 1509490577 + }, + "origin": "yeahconsole", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:yeahconsole" + ] + }, + "libinput-doc": { + "versions": { + "1.9.2-r0": 1511276852 + }, + "origin": "libinput" + }, + "py2-icu": { + "versions": { + "1.9.6-r3": 1510832509 + }, + "origin": "py-icu", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libicui18n.so.59", + "so:libicuuc.so.59", + "so:libpython2.7.so.1.0", + "so:libstdc++.so.6" + ] + }, + "abiword-plugin-google": { + "versions": { + "3.0.2-r1": 1510073361 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "xf86-input-vmmouse": { + "versions": { + "13.1.0-r3": 1510074938 + }, + "origin": "xf86-input-vmmouse", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ], + "provides": [ + "cmd:vmmouse_detect" + ] + }, + "gtk-engines": { + "versions": { + "2.21.0-r2": 1510071725 + }, + "origin": "gtk-engines", + "dependencies": [ + "gtk-engines-clearlooks", + "gtk-engines-crux", + "gtk-engines-industrial", + "gtk-engines-mist", + "gtk-engines-redmond", + "gtk-engines-thinice", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "btrfs-progs-extra": { + "versions": { + "4.13.2-r0": 1509481819 + }, + "origin": "btrfs-progs", + "dependencies": [ + "btrfs-progs", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libext2fs.so.2", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:btrfs-convert", + "cmd:btrfs-debug-tree", + "cmd:btrfs-find-root", + "cmd:btrfs-image", + "cmd:btrfs-map-logical", + "cmd:btrfs-select-super", + "cmd:btrfs-zero-log", + "cmd:btrfstune" + ] + }, + "cgdb-doc": { + "versions": { + "0.7.0-r1": 1509495074 + }, + "origin": "cgdb" + }, + "perl-email-address-list": { + "versions": { + "0.05-r2": 1510564503 + }, + "origin": "perl-email-address-list", + "dependencies": [ + "perl", + "perl-email-address" + ] + }, + "cups-lang": { + "versions": { + "2.2.10-r0": 1549287809, + "2.2.12-r0": 1566207597 + }, + "origin": "cups", + "dependencies": [ + "cups-client", + "poppler-utils", + "libressl", + "dbus" + ] + }, + "collectd-ascent": { + "versions": { + "5.7.2-r0": 1510069650 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libxml2.so.2" + ] + }, + "smokeping-doc": { + "versions": { + "2.6.11-r3": 1510564558 + }, + "origin": "smokeping" + }, + "py3-oauth2client": { + "versions": { + "4.1.2-r1": 1509551799 + }, + "origin": "py-oauth2client", + "dependencies": [ + "py3asn1", + "py3httplib2", + "py3asn1-modules", + "py3rsa", + "py3six", + "python3" + ] + }, + "apk-tools": { + "versions": { + "2.10.1-r0": 1536582152 + }, + "origin": "apk-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:apk" + ] + }, + "py3-asn1crypto": { + "versions": { + "0.23.0-r0": 1509476390 + }, + "origin": "py-asn1crypto", + "dependencies": [ + "python3" + ] + }, + "openldap-doc": { + "versions": { + "2.4.45-r3": 1510258131, + "2.4.46-r0": 1565073937, + "2.4.48-r0": 1566900514 + }, + "origin": "openldap" + }, + "less-doc": { + "versions": { + "520-r0": 1509496059 + }, + "origin": "less" + }, + "lame": { + "versions": { + "3.100-r0": 1510088509 + }, + "origin": "lame", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "so:libmp3lame.so.0=0.0.0", + "cmd:lame" + ] + }, + "libgcab-lang": { + "versions": { + "0.7-r1": 1510067822 + }, + "origin": "libgcab" + }, + "font-sony-misc": { + "versions": { + "1.0.3-r0": 1509475370 + }, + "origin": "font-sony-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "jasper-doc": { + "versions": { + "2.0.14-r0": 1509475517 + }, + "origin": "jasper" + }, + "confuse": { + "versions": { + "3.2.1-r0": 1509471872 + }, + "origin": "confuse", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libconfuse.so.2=2.0.0" + ] + }, + "lua5.2-sql-postgres": { + "versions": { + "2.3.5-r1": 1509488830 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "gnutls-dev": { + "versions": { + "3.6.1-r0": 1509465348 + }, + "origin": "gnutls", + "dependencies": [ + "gnutls-c++=3.6.1-r0", + "gnutls=3.6.1-r0", + "pc:hogweed", + "pc:libtasn1", + "pc:nettle", + "pc:p11-kit-1", + "pkgconfig" + ], + "provides": [ + "pc:gnutls=3.6.1" + ] + }, + "transmission-doc": { + "versions": { + "2.92-r8": 1510932986 + }, + "origin": "transmission" + }, + "swfdec-doc": { + "versions": { + "0.9.2-r0": 1510073744 + }, + "origin": "swfdec" + }, + "perl-test-inter": { + "versions": { + "1.06-r0": 1509485924 + }, + "origin": "perl-test-inter" + }, + "libpng-dev": { + "versions": { + "1.6.37-r0": 1557132258 + }, + "origin": "libpng", + "dependencies": [ + "libpng=1.6.37-r0", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:libpng16=1.6.37", + "pc:libpng=1.6.37", + "cmd:libpng-config", + "cmd:libpng16-config" + ] + }, + "monkeysphere": { + "versions": { + "0.41-r0": 1510588391 + }, + "origin": "monkeysphere", + "dependencies": [ + "libressl", + "gnupg", + "so:libassuan.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libgpg-error.so.0" + ], + "provides": [ + "cmd:agent-transfer", + "cmd:monkeysphere", + "cmd:monkeysphere-authentication", + "cmd:monkeysphere-host", + "cmd:openpgp2pem", + "cmd:openpgp2spki", + "cmd:openpgp2ssh", + "cmd:pem2openpgp" + ] + }, + "lua-cqueues": { + "versions": { + "20171014-r0": 1510619111 + }, + "origin": "lua-cqueues" + }, + "swfdec-dev": { + "versions": { + "0.9.2-r0": 1510073741 + }, + "origin": "swfdec", + "dependencies": [ + "glib-dev", + "liboil-dev", + "cairo-dev", + "pango-dev", + "gtk+2.0-dev", + "libsoup-dev", + "gstreamer0.10-dev", + "gst-plugins-base0.10-dev", + "alsa-lib-dev", + "pc:cairo", + "pc:glib-2.0>=2.16", + "pc:gobject-2.0>=2.16", + "pc:gtk+-2.0>=2.8.0", + "pc:liboil-0.3", + "pc:pangocairo", + "pkgconfig", + "swfdec=0.9.2-r0" + ], + "provides": [ + "pc:swfdec-0.9=0.9.2", + "pc:swfdec-gtk-0.9=0.9.2" + ] + }, + "py2-asn1crypto": { + "versions": { + "0.23.0-r0": 1509476391 + }, + "origin": "py-asn1crypto", + "dependencies": [ + "python2" + ] + }, + "glm": { + "versions": { + "0.9.8.5-r0": 1509494919 + }, + "origin": "glm" + }, + "libcrystalhd": { + "versions": { + "20130708-r2": 1509493205 + }, + "origin": "libcrystalhd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcrystalhd.so.3=3.6" + ] + }, + "audit-doc": { + "versions": { + "2.7.7-r1": 1509491479 + }, + "origin": "audit" + }, + "librsvg-doc": { + "versions": { + "2.40.19-r0": 1510066818 + }, + "origin": "librsvg" + }, + "perl-sub-name-doc": { + "versions": { + "0.21-r1": 1509474063 + }, + "origin": "perl-sub-name" + }, + "perl-file-remove": { + "versions": { + "1.57-r0": 1510352929 + }, + "origin": "perl-file-remove" + }, + "py-urlgrabber": { + "versions": { + "3.10.1-r1": 1509468416 + }, + "origin": "py-urlgrabber", + "dependencies": [ + "python2", + "py-curl" + ], + "provides": [ + "cmd:urlgrabber" + ] + }, + "gtk+3.0-dev": { + "versions": { + "3.22.21-r0": 1510067778 + }, + "origin": "gtk+3.0", + "dependencies": [ + "at-spi2-atk-dev", + "atk-dev", + "cairo-dev", + "fontconfig-dev", + "gdk-pixbuf-dev", + "glib-dev", + "intltool", + "libepoxy-dev", + "libx11-dev", + "libxcomposite-dev", + "libxcursor-dev", + "libxdamage-dev", + "libxext-dev", + "libxfixes-dev", + "libxi-dev", + "libxinerama-dev", + "libxrandr-dev", + "pango-dev", + "gtk+3.0=3.22.21-r0", + "pc:atk", + "pc:atk-bridge-2.0", + "pc:atk>=2.15.1", + "pc:cairo-gobject>=1.14.0", + "pc:cairo-xlib", + "pc:cairo>=1.14.0", + "pc:epoxy>=1.0", + "pc:fontconfig", + "pc:gdk-pixbuf-2.0>=2.30.0", + "pc:gio-2.0>=2.49.4", + "pc:gio-unix-2.0>=2.49.4", + "pc:pango", + "pc:pangocairo", + "pc:pangoft2", + "pc:x11", + "pc:xcomposite", + "pc:xcursor", + "pc:xdamage", + "pc:xext", + "pc:xfixes", + "pc:xi", + "pc:xinerama", + "pc:xrandr", + "pkgconfig" + ], + "provides": [ + "pc:gail-3.0=3.22.21", + "pc:gdk-3.0=3.22.21", + "pc:gdk-x11-3.0=3.22.21", + "pc:gtk+-3.0=3.22.21", + "pc:gtk+-unix-print-3.0=3.22.21", + "pc:gtk+-x11-3.0=3.22.21" + ] + }, + "newt-dev": { + "versions": { + "0.52.20-r0": 1509476145 + }, + "origin": "newt", + "dependencies": [ + "newt=0.52.20-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnewt=0.52.20" + ] + }, + "nmap-nselibs": { + "versions": { + "7.60-r2": 1510261467, + "7.60-r3": 1572297245 + }, + "origin": "nmap" + }, + "ipsec-tools-dev": { + "versions": { + "0.8.2-r6": 1510261510 + }, + "origin": "ipsec-tools" + }, + "liblogging-doc": { + "versions": { + "1.0.6-r0": 1509517791 + }, + "origin": "liblogging" + }, + "ppp-doc": { + "versions": { + "2.4.7-r5": 1509480123 + }, + "origin": "ppp" + }, + "xf86-video-ati-doc": { + "versions": { + "7.10.0-r1": 1511981941 + }, + "origin": "xf86-video-ati" + }, + "qemu-system-moxie": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-moxie" + ] + }, + "netcf-libs": { + "versions": { + "0.2.8-r4": 1509491201 + }, + "origin": "netcf", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libnl-3.so.200", + "so:libnl-route-3.so.200", + "so:libxml2.so.2", + "so:libxslt.so.1" + ], + "provides": [ + "so:libnetcf.so.1=1.4.0" + ] + }, + "mdocml-doc": { + "versions": { + "1.14.3-r0": 1509459635 + }, + "origin": "mdocml" + }, + "jpeg-dev": { + "versions": { + "8-r6": 1509469915 + }, + "origin": "jpeg", + "dependencies": [ + "libjpeg-turbo-dev" + ] + }, + "pcre2": { + "versions": { + "10.30-r0": 1509461775 + }, + "origin": "pcre2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre2-8.so.0=0.6.0", + "so:libpcre2-posix.so.2=2.0.0" + ] + }, + "lua5.1-lzlib": { + "versions": { + "0.4.3-r0": 1509494164 + }, + "origin": "lua-lzlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ] + }, + "bwm-ng": { + "versions": { + "0.6.1-r3": 1509475295 + }, + "origin": "bwm-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:bwm-ng" + ] + }, + "perl-yaml-libyaml-doc": { + "versions": { + "0.65-r2": 1509494320 + }, + "origin": "perl-yaml-libyaml" + }, + "xfsprogs-extra": { + "versions": { + "4.14.0-r1": 1528279973 + }, + "origin": "xfsprogs", + "dependencies": [ + "xfsprogs", + "so:libc.musl-x86_64.so.1", + "so:libhandle.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:xfs_admin", + "cmd:xfs_bmap", + "cmd:xfs_copy", + "cmd:xfs_db", + "cmd:xfs_estimate", + "cmd:xfs_freeze", + "cmd:xfs_fsr", + "cmd:xfs_growfs", + "cmd:xfs_info", + "cmd:xfs_io", + "cmd:xfs_logprint", + "cmd:xfs_mdrestore", + "cmd:xfs_metadump", + "cmd:xfs_mkfile", + "cmd:xfs_ncheck", + "cmd:xfs_quota", + "cmd:xfs_rtcp", + "cmd:xfs_spaceman" + ] + }, + "py-meld3": { + "versions": { + "1.0.2-r1": 1509552703 + }, + "origin": "py-meld3", + "dependencies": [ + "python2" + ] + }, + "perl-devel-globaldestruction-doc": { + "versions": { + "0.14-r0": 1509475924 + }, + "origin": "perl-devel-globaldestruction" + }, + "pidgin-otr-lang": { + "versions": { + "4.0.2-r0": 1510072469 + }, + "origin": "pidgin-otr", + "dependencies": [ + "pidgin" + ] + }, + "debootstrap": { + "versions": { + "1.0.92-r0": 1510588271 + }, + "origin": "debootstrap", + "dependencies": [ + "debian-archive-keyring", + "dpkg", + "tar" + ], + "provides": [ + "cmd:debootstrap" + ] + }, + "snort": { + "versions": { + "2.9.11-r0": 1510848485 + }, + "origin": "snort", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libdnet.so.1", + "so:libpcap.so.1", + "so:libpcre.so.1", + "so:libsfbpf.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:snort", + "cmd:u2boat", + "cmd:u2spewfoo" + ] + }, + "charybdis": { + "versions": { + "3.5.5-r3": 1510260652 + }, + "origin": "charybdis", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:libratbox.so=0", + "cmd:charybdis-bantool", + "cmd:charybdis-genssl", + "cmd:charybdis-ircd", + "cmd:charybdis-mkpasswd", + "cmd:charybdis-viconf", + "cmd:charybdis-vimotd" + ] + }, + "netcf-doc": { + "versions": { + "0.2.8-r4": 1509491201 + }, + "origin": "netcf" + }, + "perl-list-someutils-xs": { + "versions": { + "0.55-r0": 1510564670 + }, + "origin": "perl-list-someutils-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "aaudit-server": { + "versions": { + "0.7.2-r1": 1510074392 + }, + "origin": "aaudit", + "dependencies": [ + "aaudit", + "git", + "lua5.2", + "lua5.2-posix", + "lua5.2-cjson", + "lua5.2-lzlib" + ], + "provides": [ + "cmd:aaudit-repo", + "cmd:aaudit-update-keys" + ] + }, + "py-gflags": { + "versions": { + "3.1.1-r1": 1509551806 + }, + "origin": "py-gflags" + }, + "fsarchiver-doc": { + "versions": { + "0.8.2-r0": 1509485944 + }, + "origin": "fsarchiver" + }, + "ffmpeg-dev": { + "versions": { + "3.4-r1": 1510072146 + }, + "origin": "ffmpeg", + "dependencies": [ + "ffmpeg-libs=3.4-r1", + "pkgconfig" + ], + "provides": [ + "pc:libavcodec=57.107.100", + "pc:libavdevice=57.10.100", + "pc:libavfilter=6.107.100", + "pc:libavformat=57.83.100", + "pc:libavresample=3.7.0", + "pc:libavutil=55.78.100", + "pc:libpostproc=54.7.100", + "pc:libswresample=2.9.100", + "pc:libswscale=4.8.100" + ] + }, + "py-pillow-doc": { + "versions": { + "4.3.0-r0": 1509489959 + }, + "origin": "py-pillow", + "dependencies": [ + "py-olefile" + ] + }, + "fastjar": { + "versions": { + "0.98-r2": 1509490598 + }, + "origin": "fastjar", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:fastjar", + "cmd:grepjar" + ] + }, + "slang-doc": { + "versions": { + "2.3.1a-r0": 1509476136 + }, + "origin": "slang" + }, + "uriparser-dev": { + "versions": { + "0.8.4-r0": 1509494962 + }, + "origin": "uriparser", + "dependencies": [ + "pkgconfig", + "uriparser=0.8.4-r0" + ], + "provides": [ + "pc:liburiparser=0.8.4" + ] + }, + "rpcbind": { + "versions": { + "0.2.4-r0": 1509488291 + }, + "origin": "rpcbind", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libtirpc.so.3" + ], + "provides": [ + "cmd:rpcbind", + "cmd:rpcinfo" + ] + }, + "jack-doc": { + "versions": { + "1.9.10-r5": 1509473318 + }, + "origin": "jack" + }, + "dhcp-doc": { + "versions": { + "4.3.5-r0": 1509496512 + }, + "origin": "dhcp" + }, + "cmph-doc": { + "versions": { + "2.0-r1": 1509482578 + }, + "origin": "cmph" + }, + "xf86-input-evdev-dev": { + "versions": { + "2.10.5-r0": 1510074956 + }, + "origin": "xf86-input-evdev", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xorg-evdev=2.10.5" + ] + }, + "perl-email-date-format-doc": { + "versions": { + "1.005-r0": 1509489452 + }, + "origin": "perl-email-date-format" + }, + "gst-plugins-base-dev": { + "versions": { + "1.12.3-r0": 1510070597 + }, + "origin": "gst-plugins-base", + "dependencies": [ + "gst-plugins-base=1.12.3-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gstreamer-1.0", + "pc:gstreamer-base-1.0", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-allocators-1.0=1.12.3", + "pc:gstreamer-app-1.0=1.12.3", + "pc:gstreamer-audio-1.0=1.12.3", + "pc:gstreamer-fft-1.0=1.12.3", + "pc:gstreamer-pbutils-1.0=1.12.3", + "pc:gstreamer-plugins-base-1.0=1.12.3", + "pc:gstreamer-riff-1.0=1.12.3", + "pc:gstreamer-rtp-1.0=1.12.3", + "pc:gstreamer-rtsp-1.0=1.12.3", + "pc:gstreamer-sdp-1.0=1.12.3", + "pc:gstreamer-tag-1.0=1.12.3", + "pc:gstreamer-video-1.0=1.12.3" + ] + }, + "perl-dbix-dbschema": { + "versions": { + "0.39-r0": 1509477489 + }, + "origin": "perl-dbix-dbschema", + "dependencies": [ + "perl", + "perl-dbi" + ] + }, + "perl-html-tree-doc": { + "versions": { + "5.07-r0": 1509481555 + }, + "origin": "perl-html-tree" + }, + "font-bitstream-100dpi": { + "versions": { + "1.0.3-r0": 1509494062 + }, + "origin": "font-bitstream-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "iproute2-doc": { + "versions": { + "4.13.0-r0": 1509481070 + }, + "origin": "iproute2" + }, + "alsa-utils": { + "versions": { + "1.1.4-r0": 1509468852 + }, + "origin": "alsa-utils", + "dependencies": [ + "dialog", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libfftw3f.so.3", + "so:libformw.so.6", + "so:libmenuw.so.6", + "so:libncursesw.so.6", + "so:libpanelw.so.6" + ], + "provides": [ + "cmd:aconnect", + "cmd:alsa-info.sh", + "cmd:alsabat", + "cmd:alsabat-test.sh", + "cmd:alsactl", + "cmd:alsaloop", + "cmd:alsamixer", + "cmd:alsatplg", + "cmd:alsaucm", + "cmd:amidi", + "cmd:amixer", + "cmd:aplay", + "cmd:aplaymidi", + "cmd:arecord", + "cmd:arecordmidi", + "cmd:aseqdump", + "cmd:aseqnet", + "cmd:iecset", + "cmd:speaker-test" + ] + }, + "soxr-dev": { + "versions": { + "0.1.2-r0": 1509492269 + }, + "origin": "soxr", + "dependencies": [ + "cmake", + "pkgconfig", + "soxr=0.1.2-r0" + ], + "provides": [ + "pc:soxr-lsr=0.1.2", + "pc:soxr=0.1.2" + ] + }, + "xorg-server-doc": { + "versions": { + "1.19.5-r1": 1540838460 + }, + "origin": "xorg-server" + }, + "lua5.1": { + "versions": { + "5.1.5-r3": 1509462459 + }, + "origin": "lua5.1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ], + "provides": [ + "cmd:lua5.1", + "cmd:luac5.1" + ] + }, + "mariadb-libs": { + "versions": { + "10.1.38-r1": 1551187990, + "10.1.40-r0": 1560354887, + "10.1.41-r0": 1565163111 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libmysqld.so.18=18" + ] + }, + "perl-convert-asn1-doc": { + "versions": { + "0.27-r0": 1509477727 + }, + "origin": "perl-convert-asn1" + }, + "gphoto2": { + "versions": { + "2.5.14-r0": 1509493217 + }, + "origin": "gphoto2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexif.so.12", + "so:libgphoto2.so.6", + "so:libgphoto2_port.so.12", + "so:libintl.so.8", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:gphoto2" + ] + }, + "acf-ipsec-tools": { + "versions": { + "0.12.0-r2": 1510072901 + }, + "origin": "acf-ipsec-tools", + "dependencies": [ + "acf-core", + "ipsec-tools" + ] + }, + "py2-markupsafe": { + "versions": { + "1.0-r0": 1509476744 + }, + "origin": "py-markupsafe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "tdb-dev": { + "versions": { + "1.3.15-r0": 1509486503 + }, + "origin": "tdb", + "dependencies": [ + "python2", + "pkgconfig", + "tdb-libs=1.3.15-r0" + ], + "provides": [ + "pc:tdb=1.3.15" + ] + }, + "avahi-ui": { + "versions": { + "0.6.31-r7": 1510076324 + }, + "origin": "avahi-ui", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-glib.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libavahi-ui.so.0=0.1.4" + ] + }, + "libpciaccess-doc": { + "versions": { + "0.13.5-r1": 1509466088 + }, + "origin": "libpciaccess" + }, + "garcon-dev": { + "versions": { + "0.6.1-r1": 1510068107 + }, + "origin": "garcon", + "dependencies": [ + "garcon=0.6.1-r1", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pc:gtk+-2.0", + "pc:gtk+-3.0", + "pc:libxfce4ui-1", + "pc:libxfce4ui-2", + "pkgconfig" + ], + "provides": [ + "pc:garcon-1=0.6.1", + "pc:garcon-gtk2-1=0.6.1", + "pc:garcon-gtk3-1=0.6.1" + ] + }, + "nginx-mod-stream-geoip": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-math-round-doc": { + "versions": { + "0.07-r0": 1509483897 + }, + "origin": "perl-math-round" + }, + "libgphoto2-doc": { + "versions": { + "2.5.14-r0": 1509481938 + }, + "origin": "libgphoto2" + }, + "ulogd-pgsql": { + "versions": { + "2.0.5-r4": 1509480396 + }, + "origin": "ulogd", + "dependencies": [ + "ulogd", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "ghi": { + "versions": { + "1.2.0-r1": 1509552714 + }, + "origin": "ghi", + "dependencies": [ + "ruby", + "ncurses" + ], + "provides": [ + "cmd:ghi" + ] + }, + "abiword-plugin-garble": { + "versions": { + "3.0.2-r1": 1510073359 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libxml2.so.2" + ] + }, + "vte-doc": { + "versions": { + "0.28.2-r13": 1510071902 + }, + "origin": "vte" + }, + "rtpproxy-tools": { + "versions": { + "2.0.0-r4": 1509492568 + }, + "origin": "rtpproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsndfile.so.1" + ], + "provides": [ + "cmd:extractaudio", + "cmd:makeann" + ] + }, + "mp3splt-gtk": { + "versions": { + "0.9.2-r2": 1510070623 + }, + "origin": "mp3splt-gtk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libmp3splt.so.0" + ], + "provides": [ + "cmd:mp3splt-gtk" + ] + }, + "openvpn-ad-check": { + "versions": { + "1.1-r2": 1509491056 + }, + "origin": "openvpn-ad-check", + "dependencies": [ + "openvpn", + "lua-ldap" + ] + }, + "vanessa_adt": { + "versions": { + "0.0.9-r0": 1509481362 + }, + "origin": "vanessa_adt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libvanessa_logger.so.0" + ], + "provides": [ + "so:libvanessa_adt.so.1=1.0.0" + ] + }, + "abiword-plugin-gimp": { + "versions": { + "3.0.2-r1": 1510073360 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "strongswan-doc": { + "versions": { + "5.6.3-r2": 1539005308 + }, + "origin": "strongswan" + }, + "py-roman": { + "versions": { + "2.0.0-r2": 1509493357 + }, + "origin": "py-roman" + }, + "dansguardian-doc": { + "versions": { + "2.12.0.3-r4": 1509494014 + }, + "origin": "dansguardian" + }, + "apr-util-dbm_db": { + "versions": { + "1.6.1-r1": 1510285059 + }, + "origin": "apr-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ] + }, + "jbig2dec-dev": { + "versions": { + "0.14-r0": 1509469939 + }, + "origin": "jbig2dec", + "dependencies": [ + "jbig2dec=0.14-r0" + ] + }, + "rlog-doc": { + "versions": { + "1.4-r4": 1509475977 + }, + "origin": "rlog" + }, + "acf-quagga": { + "versions": { + "0.10.1-r1": 1510075434 + }, + "origin": "acf-quagga", + "dependencies": [ + "acf-core", + "lua-socket", + "quagga" + ] + }, + "mysql": { + "versions": { + "10.1.38-r1": 1551187995, + "10.1.40-r0": 1560354890, + "10.1.41-r0": 1565163115 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb" + ] + }, + "py3-cffi": { + "versions": { + "1.10.0-r0": 1509476372 + }, + "origin": "py-cffi", + "dependencies": [ + "py3-cparser", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libpython3.6m.so.1.0" + ] + }, + "libdvdnav-dev": { + "versions": { + "5.0.3-r0": 1509481832 + }, + "origin": "libdvdnav", + "dependencies": [ + "libdvdread-dev>=5.0.3", + "libdvdnav=5.0.3-r0", + "pc:dvdread>=4.1.2", + "pkgconfig" + ], + "provides": [ + "pc:dvdnav=5.0.3" + ] + }, + "duplicity": { + "versions": { + "0.7.13.1-r0": 1510588267 + }, + "origin": "duplicity", + "dependencies": [ + "python2", + "py-boto", + "gnupg", + "ncftp", + "py-lockfile", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:librsync.so.2" + ], + "provides": [ + "cmd:duplicity", + "cmd:rdiffdir" + ] + }, + "perl-net-dns-resolver-mock": { + "versions": { + "1.20170814-r0": 1509469054 + }, + "origin": "perl-net-dns-resolver-mock", + "dependencies": [ + "perl-net-dns" + ] + }, + "sudo-doc": { + "versions": { + "1.8.21_p2-r1": 1509459675 + }, + "origin": "sudo" + }, + "perl-encode-doc": { + "versions": { + "2.93-r0": 1511889457 + }, + "origin": "perl-encode" + }, + "libcap-ng-doc": { + "versions": { + "0.7.8-r1": 1509461310 + }, + "origin": "libcap-ng" + }, + "bbsuid": { + "versions": { + "0.6-r0": 1509494150 + }, + "origin": "bbsuid", + "dependencies": [ + "busybox" + ] + }, + "pacman-dev": { + "versions": { + "5.0.2-r1": 1510588357 + }, + "origin": "pacman", + "dependencies": [ + "libarchive-dev", + "curl-dev", + "libressl-dev", + "gpgme-dev", + "gettext-dev", + "pacman=5.0.2-r1", + "pkgconfig" + ], + "provides": [ + "pc:libalpm=10.0.2" + ] + }, + "libevent": { + "versions": { + "2.1.8-r2": 1510258602 + }, + "origin": "libevent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "so:libevent-2.1.so.6=6.0.2", + "so:libevent_core-2.1.so.6=6.0.2", + "so:libevent_extra-2.1.so.6=6.0.2", + "so:libevent_openssl-2.1.so.6=6.0.2", + "so:libevent_pthreads-2.1.so.6=6.0.2" + ] + }, + "py-eyed3": { + "versions": { + "0.7.8-r1": 1509551898 + }, + "origin": "py-eyed3", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:eyeD3" + ] + }, + "lua-inspect-doc": { + "versions": { + "3.1.0-r1": 1509491025 + }, + "origin": "lua-inspect" + }, + "perl-file-listing-doc": { + "versions": { + "6.04-r1": 1509464400 + }, + "origin": "perl-file-listing" + }, + "conntrack-tools": { + "versions": { + "1.4.4-r0": 1509469777 + }, + "origin": "conntrack-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnetfilter_conntrack.so.3", + "so:libnetfilter_cthelper.so.0", + "so:libnetfilter_cttimeout.so.1", + "so:libnetfilter_queue.so.1", + "so:libnfnetlink.so.0" + ], + "provides": [ + "cmd:conntrack", + "cmd:conntrackd", + "cmd:nfct" + ] + }, + "thunar-volman": { + "versions": { + "0.8.1-r2": 1510075205 + }, + "origin": "thunar-volman", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexo-1.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libnotify.so.4", + "so:libpango-1.0.so.0", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:thunar-volman", + "cmd:thunar-volman-settings" + ] + }, + "perl-class-inspector-doc": { + "versions": { + "1.28-r1": 1510564482 + }, + "origin": "perl-class-inspector" + }, + "perl-file-listing": { + "versions": { + "6.04-r1": 1509464400 + }, + "origin": "perl-file-listing", + "dependencies": [ + "perl", + "perl-http-date" + ] + }, + "libgee-dev": { + "versions": { + "0.20.0-r0": 1509468648 + }, + "origin": "libgee", + "dependencies": [ + "libgee=0.20.0-r0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gee-0.8=0.20.0" + ] + }, + "apache2-http2": { + "versions": { + "2.4.39-r0": 1554306881, + "2.4.41-r0": 1566292327 + }, + "origin": "apache2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libnghttp2.so.14" + ] + }, + "libdvdread-dev": { + "versions": { + "5.0.3-r1": 1509480295 + }, + "origin": "libdvdread", + "dependencies": [ + "libdvdread=5.0.3-r1", + "pc:libdvdcss>=1.2", + "pkgconfig" + ], + "provides": [ + "pc:dvdread=5.0.3" + ] + }, + "libgpg-error-lisp": { + "versions": { + "1.27-r1": 1509459212 + }, + "origin": "libgpg-error" + }, + "mutt-lang": { + "versions": { + "1.10.1-r0": 1532446898 + }, + "origin": "mutt" + }, + "libraw1394": { + "versions": { + "2.1.2-r0": 1509470075 + }, + "origin": "libraw1394", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libraw1394.so.11=11.1.0" + ] + }, + "perl-mail-dkim": { + "versions": { + "0.44-r0": 1509516996 + }, + "origin": "perl-mail-dkim", + "dependencies": [ + "perl", + "perl-net-dns", + "perl-net-ip", + "perl-mailtools", + "perl-crypt-openssl-rsa", + "perl-yaml-xs", + "perl-net-dns-resolver-mock" + ] + }, + "lz4-doc": { + "versions": { + "1.8.0-r1": 1509461404 + }, + "origin": "lz4" + }, + "apache2-dev": { + "versions": { + "2.4.39-r0": 1554306881, + "2.4.41-r0": 1566292327 + }, + "origin": "apache2", + "dependencies": [ + "perl", + "apr-util-dev" + ], + "provides": [ + "cmd:apxs" + ] + }, + "dejagnu": { + "versions": { + "1.6-r1": 1509491945 + }, + "origin": "dejagnu", + "dependencies": [ + "expect" + ], + "provides": [ + "cmd:runtest" + ] + }, + "uwsgi-nagios": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-xml-simple-doc": { + "versions": { + "2.24-r0": 1509477314 + }, + "origin": "perl-xml-simple" + }, + "bridge-utils-doc": { + "versions": { + "1.6-r0": 1509491170 + }, + "origin": "bridge-utils" + }, + "uwsgi-router_hash": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "qemu-aarch64": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-aarch64" + ] + }, + "qemu-openrc": { + "versions": { + "0.6.0-r0": 1510070486 + }, + "origin": "qemu-openrc", + "dependencies": [ + "qemu", + "socat", + "/bin/sh" + ], + "provides": [ + "cmd:qemush" + ] + }, + "qt-dev": { + "versions": { + "4.8.7-r8": 1510287207 + }, + "origin": "qt", + "dependencies": [ + "mesa-dev", + "libice-dev", + "libsm-dev", + "libx11-dev", + "libxext-dev", + "libxrender-dev", + "alsa-lib-dev", + "libressl-dev", + "fontconfig-dev", + "freetype-dev", + "glib-dev", + "libpng-dev", + "zlib-dev", + "sqlite-dev", + "dbus-dev", + "pkgconfig", + "qt-webkit=4.8.7-r8", + "qt-x11=4.8.7-r8", + "qt=4.8.7-r8", + "so:libQt3Support.so.4", + "so:libQtCore.so.4", + "so:libQtDBus.so.4", + "so:libQtDeclarative.so.4", + "so:libQtDesigner.so.4", + "so:libQtDesignerComponents.so.4", + "so:libQtGui.so.4", + "so:libQtHelp.so.4", + "so:libQtNetwork.so.4", + "so:libQtWebKit.so.4", + "so:libQtXml.so.4", + "so:libQtXmlPatterns.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "pc:Qt3Support=4.8.7", + "pc:QtCLucene=4.8.7", + "pc:QtCore=4.8.7", + "pc:QtDBus=4.8.7", + "pc:QtDeclarative=4.8.7", + "pc:QtDesigner=4.8.7", + "pc:QtDesignerComponents=4.8.7", + "pc:QtGui=4.8.7", + "pc:QtHelp=4.8.7", + "pc:QtMultimedia=4.8.7", + "pc:QtNetwork=4.8.7", + "pc:QtOpenGL=4.8.7", + "pc:QtScript=4.8.7", + "pc:QtScriptTools=4.8.7", + "pc:QtSql=4.8.7", + "pc:QtSvg=4.8.7", + "pc:QtTest=4.8.7", + "pc:QtUiTools=4.8.7", + "pc:QtWebKit=4.9.4", + "pc:QtXml=4.8.7", + "pc:QtXmlPatterns=4.8.7", + "cmd:designer", + "cmd:lconvert", + "cmd:linguist", + "cmd:lrelease", + "cmd:lupdate", + "cmd:moc", + "cmd:pixeltool", + "cmd:qcollectiongenerator", + "cmd:qdbuscpp2xml", + "cmd:qdbusxml2cpp", + "cmd:qdoc3", + "cmd:qhelpconverter", + "cmd:qhelpgenerator", + "cmd:qmake", + "cmd:qt3to4", + "cmd:qttracereplay", + "cmd:rcc", + "cmd:uic", + "cmd:uic3", + "cmd:xmlpatterns", + "cmd:xmlpatternsvalidator" + ] + }, + "lua-maxminddb": { + "versions": { + "0.1-r1": 1509496577 + }, + "origin": "lua-maxminddb", + "dependencies": [ + "lua5.1-maxminddb", + "lua5.2-maxminddb", + "lua5.3-maxminddb" + ] + }, + "ruby-rake": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ], + "provides": [ + "cmd:rake" + ] + }, + "fuse-doc": { + "versions": { + "2.9.8-r0": 1532967982 + }, + "origin": "fuse" + }, + "glu": { + "versions": { + "9.0.0-r3": 1510067879 + }, + "origin": "glu", + "dependencies": [ + "so:libGL.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libGLU.so.1=1.3.1" + ] + }, + "libxft": { + "versions": { + "2.3.2-r2": 1509462101 + }, + "origin": "libxft", + "dependencies": [ + "so:libX11.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6" + ], + "provides": [ + "so:libXft.so.2=2.3.2" + ] + }, + "aconf-mod-dnsmasq": { + "versions": { + "0.6.5-r0": 1510073707 + }, + "origin": "aconf", + "dependencies": [ + "aconf", + "dnsmasq" + ] + }, + "libjpeg-turbo": { + "versions": { + "1.5.3-r2": 1537879743, + "1.5.3-r3": 1563792634 + }, + "origin": "libjpeg-turbo", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libjpeg.so.8=8.1.2", + "so:libturbojpeg.so.0=0.1.0" + ] + }, + "uwsgi-corerouter": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "qemu-tilegx": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-tilegx" + ] + }, + "mtx": { + "versions": { + "1.3.12-r2": 1509482374 + }, + "origin": "mtx", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:loaderinfo", + "cmd:mtx", + "cmd:scsieject", + "cmd:scsitape", + "cmd:tapeinfo" + ] + }, + "gst-plugins-good0.10": { + "versions": { + "0.10.31-r0": 1510068454 + }, + "origin": "gst-plugins-good0.10", + "dependencies": [ + "so:libFLAC.so.8", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libavc1394.so.0", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdv.so.4", + "so:libgcc_s.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-0.10.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstcontroller-0.10.so.0", + "so:libgstfft-0.10.so.0", + "so:libgstinterfaces-0.10.so.0", + "so:libgstnetbuffer-0.10.so.0", + "so:libgstpbutils-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgstriff-0.10.so.0", + "so:libgstrtp-0.10.so.0", + "so:libgstrtsp-0.10.so.0", + "so:libgstsdp-0.10.so.0", + "so:libgsttag-0.10.so.0", + "so:libgstvideo-0.10.so.0", + "so:libiec61883.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:liborc-0.4.so.0", + "so:libraw1394.so.11", + "so:librom1394.so.0", + "so:libshout.so.3", + "so:libsoup-2.4.so.1", + "so:libsoup-gnome-2.4.so.1", + "so:libspeex.so.1", + "so:libstdc++.so.6", + "so:libtag.so.1", + "so:libwavpack.so.1", + "so:libxml2.so.2", + "so:libz.so.1" + ] + }, + "pidgin-otr": { + "versions": { + "4.0.2-r0": 1510072472 + }, + "origin": "pidgin-otr", + "dependencies": [ + "pidgin", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libotr.so.5" + ] + }, + "cups-filters-dev": { + "versions": { + "1.17.9-r0": 1510075883 + }, + "origin": "cups-filters", + "dependencies": [ + "cups-filters-libs=1.17.9-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcupsfilters=1.17.9", + "pc:libfontembed=1.17.9" + ] + }, + "jasper-libs": { + "versions": { + "2.0.14-r0": 1509475517 + }, + "origin": "jasper", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8" + ], + "provides": [ + "so:libjasper.so.4=4.0.0" + ] + }, + "py-twitter": { + "versions": { + "3.1-r2": 1509552772 + }, + "origin": "py-twitter", + "dependencies": [ + "py-future", + "py-requests", + "py-requests-oauthlib" + ] + }, + "openipmi-libs": { + "versions": { + "2.0.22-r1": 1510260044 + }, + "origin": "openipmi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libglib-2.0.so.0" + ], + "provides": [ + "so:libOpenIPMI.so.0=0.0.5", + "so:libOpenIPMIcmdlang.so.0=0.0.5", + "so:libOpenIPMIglib.so.0=0.0.1", + "so:libOpenIPMIposix.so.0=0.0.1", + "so:libOpenIPMIpthread.so.0=0.0.1", + "so:libOpenIPMIui.so.1=1.0.1", + "so:libOpenIPMIutils.so.0=0.0.1" + ] + }, + "bc-doc": { + "versions": { + "1.07.1-r0": 1509465953 + }, + "origin": "bc" + }, + "qemu-mipsn32": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mipsn32" + ] + }, + "directfb-dev": { + "versions": { + "1.7.7-r1": 1509488976 + }, + "origin": "directfb", + "dependencies": [ + "directfb=1.7.7-r1", + "pkgconfig" + ], + "provides": [ + "pc:++dfb=1.7.7", + "pc:direct=1.7.7", + "pc:directfb-internal=1.7.7", + "pc:directfb=1.7.7", + "pc:fusion=1.7.7", + "cmd:directfb-config" + ] + }, + "py-libxslt": { + "versions": { + "1.1.31-r1": 1555487793, + "1.1.31-r2": 1572541164 + }, + "origin": "libxslt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libpython2.7.so.1.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ] + }, + "libseccomp-doc": { + "versions": { + "2.3.2-r1": 1519818661 + }, + "origin": "libseccomp" + }, + "xfce4-notes-plugin": { + "versions": { + "1.8.1-r0": 1510074769 + }, + "origin": "xfce4-notes-plugin", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libunique-1.0.so.0", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:xfce4-notes", + "cmd:xfce4-notes-settings", + "cmd:xfce4-popup-notes" + ] + }, + "perl-net-http-doc": { + "versions": { + "6.09-r1": 1509464377 + }, + "origin": "perl-net-http" + }, + "procps": { + "versions": { + "3.3.12-r3": 1509492056 + }, + "origin": "procps", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libprocps.so.6" + ], + "provides": [ + "cmd:free", + "cmd:pgrep", + "cmd:pidof", + "cmd:pkill", + "cmd:pmap", + "cmd:ps", + "cmd:pwdx", + "cmd:slabtop", + "cmd:sysctl", + "cmd:tload", + "cmd:top", + "cmd:uptime", + "cmd:vmstat", + "cmd:w", + "cmd:watch" + ] + }, + "perdition": { + "versions": { + "1.18-r10": 1510260023 + }, + "origin": "perdition", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgdbm.so.4", + "so:libpopt.so.0", + "so:libssl.so.44", + "so:libvanessa_adt.so.1", + "so:libvanessa_logger.so.0", + "so:libvanessa_socket.so.2" + ], + "provides": [ + "so:libperditiondb_gdbm.so.0=0.0.0", + "cmd:makegdbm", + "cmd:perdition", + "cmd:perdition.imap4", + "cmd:perdition.imap4s", + "cmd:perdition.imaps", + "cmd:perdition.pop3", + "cmd:perdition.pop3s" + ] + }, + "sems-registrar": { + "versions": { + "1.6.0-r6": 1510260897 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "s6-dns": { + "versions": { + "2.2.0.1-r0": 1509480282 + }, + "origin": "s6-dns", + "dependencies": [ + "skalibs", + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.6" + ], + "provides": [ + "so:libs6dns.so.2.2=2.2.0.1", + "so:libskadns.so.2.2=2.2.0.1", + "cmd:s6-dnsip4", + "cmd:s6-dnsip4-filter", + "cmd:s6-dnsip6", + "cmd:s6-dnsip6-filter", + "cmd:s6-dnsmx", + "cmd:s6-dnsname", + "cmd:s6-dnsname-filter", + "cmd:s6-dnsns", + "cmd:s6-dnsq", + "cmd:s6-dnsqr", + "cmd:s6-dnsqualify", + "cmd:s6-dnssoa", + "cmd:s6-dnssrv", + "cmd:s6-dnstxt", + "cmd:s6-randomip", + "cmd:skadnsd" + ] + }, + "libxmu-doc": { + "versions": { + "1.1.2-r1": 1509473711 + }, + "origin": "libxmu" + }, + "py-musicbrainzngs": { + "versions": { + "0.6-r0": 1509483362 + }, + "origin": "py-musicbrainzngs", + "dependencies": [ + "python2" + ] + }, + "fcgiwrap": { + "versions": { + "1.1.0-r2": 1510933068 + }, + "origin": "fcgiwrap", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libfcgi.so.0" + ], + "provides": [ + "cmd:fcgiwrap" + ] + }, + "cmake": { + "versions": { + "3.9.5-r0": 1509907173 + }, + "origin": "cmake", + "dependencies": [ + "so:libarchive.so.13", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libexpat.so.1", + "so:libformw.so.6", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:librhash.so.0", + "so:libstdc++.so.6", + "so:libuv.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:ccmake", + "cmd:cmake", + "cmd:cpack", + "cmd:ctest" + ] + }, + "font-bh-lucidatypewriter-100dpi": { + "versions": { + "1.0.3-r0": 1509492731 + }, + "origin": "font-bh-lucidatypewriter-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "gnupg1": { + "versions": { + "1.4.23-r0": 1534431039 + }, + "origin": "gnupg1", + "dependencies": [ + "pinentry", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libz.so.1" + ], + "provides": [ + "gnupg=1.4.23-r0", + "cmd:gpg", + "cmd:gpg-zip", + "cmd:gpgsplit", + "cmd:gpgv" + ] + }, + "py-funcsigs": { + "versions": { + "1.0.2-r1": 1509485908 + }, + "origin": "py-funcsigs" + }, + "perl-datetime-timezone-doc": { + "versions": { + "2.15-r0": 1511251871 + }, + "origin": "perl-datetime-timezone" + }, + "lua5.3-inspect": { + "versions": { + "3.1.0-r1": 1509491026 + }, + "origin": "lua-inspect", + "dependencies": [ + "lua5.3" + ] + }, + "libao": { + "versions": { + "1.2.0-r2": 1543926072 + }, + "origin": "libao", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libao.so.4=4.1.0" + ] + }, + "perl-mail-imapclient-doc": { + "versions": { + "3.39-r0": 1509494892 + }, + "origin": "perl-mail-imapclient" + }, + "py-talloc": { + "versions": { + "2.1.10-r0": 1509466374 + }, + "origin": "talloc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libpytalloc-util.so.2=2.1.10" + ] + }, + "sessreg-doc": { + "versions": { + "1.1.1-r0": 1509480573 + }, + "origin": "sessreg" + }, + "gawk-doc": { + "versions": { + "4.2.0-r0": 1509457020 + }, + "origin": "gawk" + }, + "libassuan": { + "versions": { + "2.4.4-r0": 1512029943 + }, + "origin": "libassuan", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgpg-error.so.0" + ], + "provides": [ + "so:libassuan.so.0=0.7.4" + ] + }, + "libxdmcp-doc": { + "versions": { + "1.1.2-r4": 1509461845 + }, + "origin": "libxdmcp" + }, + "git-bash-completion": { + "versions": { + "2.15.3-r0": 1540401292, + "2.15.4-r0": 1576015189 + }, + "origin": "git" + }, + "snownews": { + "versions": { + "1.5.12-r6": 1510260846 + }, + "origin": "snownews", + "dependencies": [ + "ncurses", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:opml2snow", + "cmd:snow2opml", + "cmd:snownews" + ] + }, + "rrdtool-utils": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "cmd:rrdcreate", + "cmd:rrdinfo", + "cmd:rrdupdate" + ] + }, + "py-irc-scripts": { + "versions": { + "8.5.1-r0": 1509491081 + }, + "origin": "py-irc" + }, + "linux-vanilla": { + "versions": { + "4.9.161-r0": 1551779592, + "4.9.182-r0": 1560866043 + }, + "origin": "linux-vanilla", + "dependencies": [ + "mkinitfs", + "linux-firmware" + ] + }, + "py-imaging": { + "versions": { + "1.1.7-r4": 1509493735 + }, + "origin": "py-imaging", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:pilconvert.py", + "cmd:pildriver.py", + "cmd:pilfile.py", + "cmd:pilfont.py", + "cmd:pilprint.py" + ] + }, + "drbd-utils": { + "versions": { + "9.1.1-r0": 1511795634 + }, + "origin": "drbd-utils", + "dependencies": [ + "bash", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "drbd", + "cmd:drbd-overview", + "cmd:drbdadm", + "cmd:drbdmeta", + "cmd:drbdmon", + "cmd:drbdsetup" + ] + }, + "tdb-doc": { + "versions": { + "1.3.15-r0": 1509486505 + }, + "origin": "tdb" + }, + "wxgtk2.8": { + "versions": { + "2.8.12.1-r4": 1510928427 + }, + "origin": "wxgtk2.8", + "dependencies": [ + "so:libSDL-1.2.so.0", + "so:libSM.so.6", + "so:libXinerama.so.1", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libjpeg.so.8", + "so:libpango-1.0.so.0", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libtiff.so.5", + "so:libwx_baseu-2.8.so.0", + "so:libwx_baseu_xml-2.8.so.0" + ], + "provides": [ + "so:libwx_gtk2u_adv-2.8.so.0=0.8.0", + "so:libwx_gtk2u_aui-2.8.so.0=0.8.0", + "so:libwx_gtk2u_core-2.8.so.0=0.8.0", + "so:libwx_gtk2u_html-2.8.so.0=0.8.0", + "so:libwx_gtk2u_qa-2.8.so.0=0.8.0", + "so:libwx_gtk2u_richtext-2.8.so.0=0.8.0", + "so:libwx_gtk2u_xrc-2.8.so.0=0.8.0" + ] + }, + "sylpheed-dev": { + "versions": { + "3.6.0-r1": 1510588302 + }, + "origin": "sylpheed", + "dependencies": [ + "sylpheed=3.6.0-r1" + ] + }, + "freetds": { + "versions": { + "1.00.44-r0": 1509480207 + }, + "origin": "freetds", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2", + "so:libodbcinst.so.2", + "so:libreadline.so.7" + ], + "provides": [ + "so:libct.so.4=4.0.0", + "so:libsybdb.so.5=5.1.0", + "so:libtdsodbc.so.0=0.0.0", + "cmd:bsqldb", + "cmd:bsqlodbc", + "cmd:datacopy", + "cmd:defncopy", + "cmd:fisql", + "cmd:freebcp", + "cmd:osql", + "cmd:tdspool", + "cmd:tsql" + ] + }, + "collectd-ipmi": { + "versions": { + "5.7.2-r0": 1510069654 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libOpenIPMI.so.0", + "so:libOpenIPMIpthread.so.0", + "so:libc.musl-x86_64.so.1" + ] + }, + "acf-lib-lua5.2": { + "versions": { + "0.10.1-r0": 1510068294 + }, + "origin": "acf-lib" + }, + "gucharmap-dev": { + "versions": { + "3.18.2-r0": 1510074174 + }, + "origin": "gucharmap", + "dependencies": [ + "gucharmap=3.18.2-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gtk+-3.0", + "pkgconfig" + ], + "provides": [ + "pc:gucharmap-2.90=3.18.2" + ] + }, + "byobu": { + "versions": { + "5.123-r1": 1509470003 + }, + "origin": "byobu", + "dependencies": [ + "python3", + "tmux" + ], + "provides": [ + "cmd:byobu", + "cmd:byobu-config", + "cmd:byobu-ctrl-a", + "cmd:byobu-disable", + "cmd:byobu-disable-prompt", + "cmd:byobu-enable", + "cmd:byobu-enable-prompt", + "cmd:byobu-export", + "cmd:byobu-janitor", + "cmd:byobu-keybindings", + "cmd:byobu-launch", + "cmd:byobu-launcher", + "cmd:byobu-launcher-install", + "cmd:byobu-launcher-uninstall", + "cmd:byobu-layout", + "cmd:byobu-prompt", + "cmd:byobu-quiet", + "cmd:byobu-reconnect-sockets", + "cmd:byobu-screen", + "cmd:byobu-select-backend", + "cmd:byobu-select-profile", + "cmd:byobu-select-session", + "cmd:byobu-shell", + "cmd:byobu-silent", + "cmd:byobu-status", + "cmd:byobu-status-detail", + "cmd:byobu-tmux", + "cmd:byobu-ugraph", + "cmd:byobu-ulevel", + "cmd:col1", + "cmd:ctail", + "cmd:manifest", + "cmd:purge-old-kernels", + "cmd:vigpg", + "cmd:wifi-status" + ] + }, + "farstream0.1-doc": { + "versions": { + "0.1.2-r2": 1510933112 + }, + "origin": "farstream0.1" + }, + "presentproto-doc": { + "versions": { + "1.1-r1": 1509466248 + }, + "origin": "presentproto" + }, + "squid-lang-lt": { + "versions": { + "3.5.27-r0": 1519824032, + "3.5.27-r1": 1562865668 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libgnome-dev": { + "versions": { + "2.32.1-r7": 1510931516 + }, + "origin": "libgnome", + "dependencies": [ + "gtk+3.0-dev", + "gconf-dev", + "libcanberra-dev", + "gnome-vfs-dev", + "libbonobo-dev", + "libgnome=2.32.1-r7", + "pc:ORBit-2.0", + "pc:gconf-2.0", + "pc:glib-2.0", + "pc:gnome-vfs-2.0", + "pc:libbonobo-2.0", + "pc:libcanberra>=0", + "pkgconfig" + ], + "provides": [ + "pc:libgnome-2.0=2.32.1" + ] + }, + "libjpeg-turbo-utils": { + "versions": { + "1.5.3-r2": 1537879742, + "1.5.3-r3": 1563792633 + }, + "origin": "libjpeg-turbo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:libturbojpeg.so.0" + ], + "provides": [ + "cmd:cjpeg", + "cmd:djpeg", + "cmd:jpegtran", + "cmd:rdjpgcom", + "cmd:tjbench", + "cmd:wrjpgcom" + ] + }, + "lcms": { + "versions": { + "1.19-r6": 1509477358 + }, + "origin": "lcms", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:liblcms.so.1", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:icc2ps", + "cmd:icclink", + "cmd:icctrans", + "cmd:jpegicc", + "cmd:tiffdiff", + "cmd:tifficc", + "cmd:wtpt" + ] + }, + "squid-lang-zh": { + "versions": { + "3.5.27-r0": 1519824033, + "3.5.27-r1": 1562865670 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "flac-dev": { + "versions": { + "1.3.2-r0": 1509473287 + }, + "origin": "flac", + "dependencies": [ + "flac=1.3.2-r0", + "pc:ogg", + "pkgconfig" + ], + "provides": [ + "pc:flac++=1.3.2", + "pc:flac=1.3.2" + ] + }, + "po4a-doc": { + "versions": { + "0.51-r1": 1509459568 + }, + "origin": "po4a" + }, + "libwnck-dev": { + "versions": { + "2.31.0-r5": 1510068128 + }, + "origin": "libwnck", + "dependencies": [ + "glib-dev", + "gtk+2.0-dev", + "libx11-dev", + "pango-dev", + "cairo-dev", + "startup-notification-dev", + "libxres-dev", + "libwnck=2.31.0-r5", + "pc:cairo", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gtk+-2.0", + "pc:libstartup-notification-1.0", + "pc:pango", + "pc:x11", + "pc:xres", + "pkgconfig" + ], + "provides": [ + "pc:libwnck-1.0=2.31.0" + ] + }, + "wavpack": { + "versions": { + "5.1.0-r3": 1548940101, + "5.1.0-r4": 1566809779 + }, + "origin": "wavpack", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libwavpack.so.1=1.2.0", + "cmd:wavpack", + "cmd:wvgain", + "cmd:wvtag", + "cmd:wvunpack" + ] + }, + "perl-net-cidr": { + "versions": { + "0.17-r0": 1509486330 + }, + "origin": "perl-net-cidr", + "dependencies": [ + "perl" + ] + }, + "encfs-doc": { + "versions": { + "1.8.1-r7": 1510258715 + }, + "origin": "encfs" + }, + "ksymoops": { + "versions": { + "2.4.11-r7": 1509489730 + }, + "origin": "ksymoops", + "dependencies": [ + "so:libbfd-2.28.so", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ksymoops" + ] + }, + "v4l-utils": { + "versions": { + "1.12.5-r1": 1510072048 + }, + "origin": "v4l-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libstdc++.so.6", + "so:libudev.so.1", + "so:libv4l2.so.0", + "so:libv4l2rds.so.0" + ], + "provides": [ + "cmd:cec-compliance", + "cmd:cec-ctl", + "cmd:cec-follower", + "cmd:cx18-ctl", + "cmd:decode_tm6000", + "cmd:ir-ctl", + "cmd:ivtv-ctl", + "cmd:media-ctl", + "cmd:rds-ctl", + "cmd:v4l2-compliance", + "cmd:v4l2-ctl", + "cmd:v4l2-dbg", + "cmd:v4l2-sysfs-path" + ] + }, + "umurmur": { + "versions": { + "0.2.17-r1": 1510846249 + }, + "origin": "umurmur", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libconfig.so.9", + "so:libcrypto.so.42", + "so:libprotobuf-c.so.1", + "so:libssl.so.44" + ], + "provides": [ + "cmd:umurmurd" + ] + }, + "fontsproto-doc": { + "versions": { + "2.1.3-r2": 1509470392 + }, + "origin": "fontsproto" + }, + "mariadb-bench": { + "versions": { + "10.1.38-r1": 1551187991, + "10.1.40-r0": 1560354887, + "10.1.41-r0": 1565163111 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common" + ] + }, + "nftables": { + "versions": { + "0.8-r0": 1509496566 + }, + "origin": "nftables", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libmnl.so.0", + "so:libnftnl.so.7", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:nft" + ] + }, + "libxrandr-doc": { + "versions": { + "1.5.1-r1": 1509466001 + }, + "origin": "libxrandr" + }, + "avahi": { + "versions": { + "0.6.32-r4": 1509465450, + "0.6.32-r5": 1563345620 + }, + "origin": "avahi", + "dependencies": [ + "/bin/sh", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libdaemon.so.0", + "so:libdbus-1.so.3", + "so:libexpat.so.1" + ], + "provides": [ + "so:libavahi-core.so.7=7.0.2", + "cmd:avahi-daemon", + "cmd:avahi-dnsconfd" + ] + }, + "at-spi2-core-doc": { + "versions": { + "2.26.2-r0": 1509466052 + }, + "origin": "at-spi2-core" + }, + "gcc-doc": { + "versions": { + "6.4.0-r5": 1509458070 + }, + "origin": "gcc" + }, + "xkeyboard-config": { + "versions": { + "2.22-r0": 1509473703 + }, + "origin": "xkeyboard-config" + }, + "lua5.2-alt-getopt": { + "versions": { + "0.7-r8": 1509480076 + }, + "origin": "lua-alt-getopt" + }, + "zeromq-doc": { + "versions": { + "4.2.5-r0": 1549279390, + "4.2.5-r1": 1563908217 + }, + "origin": "zeromq" + }, + "libev-dev": { + "versions": { + "4.24-r0": 1509469568 + }, + "origin": "libev", + "dependencies": [ + "libev=4.24-r0", + "pkgconfig" + ], + "provides": [ + "pc:libev=4.24" + ] + }, + "py3-roman": { + "versions": { + "2.0.0-r2": 1509493355 + }, + "origin": "py-roman", + "dependencies": [ + "python3" + ] + }, + "kamailio-jansson": { + "versions": { + "5.0.7-r0": 1532960877 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libjansson.so.4" + ] + }, + "librevenge-doc": { + "versions": { + "0.0.4-r0": 1509495167 + }, + "origin": "librevenge" + }, + "libgpg-error": { + "versions": { + "1.27-r1": 1509459212 + }, + "origin": "libgpg-error", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgpg-error.so.0=0.22.0", + "cmd:gpg-error" + ] + }, + "gengetopt-dev": { + "versions": { + "2.22.6-r2": 1509488506 + }, + "origin": "gengetopt", + "dependencies": [ + "gengetopt" + ] + }, + "binutils-gold": { + "versions": { + "2.30-r1": 1527754981, + "2.30-r2": 1565788943 + }, + "origin": "binutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:ld.gold" + ] + }, + "gstreamer-dev": { + "versions": { + "1.12.3-r0": 1509470920 + }, + "origin": "gstreamer", + "dependencies": [ + "libxml2-dev", + "gstreamer=1.12.3-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-1.0=1.12.3", + "pc:gstreamer-base-1.0=1.12.3", + "pc:gstreamer-check-1.0=1.12.3", + "pc:gstreamer-controller-1.0=1.12.3", + "pc:gstreamer-net-1.0=1.12.3" + ] + }, + "renderproto": { + "versions": { + "0.11.1-r3": 1509462089 + }, + "origin": "renderproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:renderproto=0.11.1" + ] + }, + "mosquitto-libs": { + "versions": { + "1.4.15-r0": 1520176488, + "1.4.15-r2": 1563346324 + }, + "origin": "mosquitto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "so:libmosquitto.so.1=1" + ] + }, + "libc-utils": { + "versions": { + "0.7.1-r0": 1509458194 + }, + "origin": "libc-dev", + "dependencies": [ + "musl-utils" + ] + }, + "lvm2-dmeventd": { + "versions": { + "2.02.175-r0": 1509462402 + }, + "origin": "lvm2", + "dependencies": [ + "lvm2-libs=2.02.175-r0", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper-event.so.1.02", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "cmd:dmeventd" + ] + }, + "xfwm4": { + "versions": { + "4.12.4-r1": 1510074449 + }, + "origin": "xfwm4", + "dependencies": [ + "hicolor-icon-theme", + "so:libX11.so.6", + "so:libXcomposite.so.1", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXrandr.so.2", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdbus-glib-1.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libstartup-notification-1.so.0", + "so:libwnck-1.so.22", + "so:libxfce4kbd-private-2.so.0", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:xfwm4", + "cmd:xfwm4-settings", + "cmd:xfwm4-tweaks-settings", + "cmd:xfwm4-workspace-settings" + ] + }, + "libsigc++-doc": { + "versions": { + "2.10.0-r1": 1509472940 + }, + "origin": "libsigc++" + }, + "nginx-mod-rtmp": { + "versions": { + "1.12.2-r4": 1542814448 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "nginx-rtmp" + ] + }, + "perl-dev": { + "versions": { + "5.26.3-r0": 1543940997 + }, + "origin": "perl", + "dependencies": [ + "perl-utils" + ], + "provides": [ + "cmd:enc2xs", + "cmd:h2xs", + "cmd:perlivp", + "cmd:xsubpp" + ] + }, + "libksba": { + "versions": { + "1.3.5-r0": 1509472891 + }, + "origin": "libksba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgpg-error.so.0" + ], + "provides": [ + "so:libksba.so.8=8.11.6" + ] + }, + "font-adobe-75dpi": { + "versions": { + "1.0.3-r0": 1509495298 + }, + "origin": "font-adobe-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "portaudio": { + "versions": { + "19-r1": 1509473346 + }, + "origin": "portaudio", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libjack.so.0" + ], + "provides": [ + "so:libportaudio.so.2=2.0.0" + ] + }, + "libuv": { + "versions": { + "1.17.0-r0": 1511652082 + }, + "origin": "libuv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libuv.so.1=1.0.0" + ] + }, + "zfs-hardened-dev": { + "versions": { + "4.9.65-r1": 1511799189 + }, + "origin": "zfs-hardened", + "dependencies": [ + "glib-dev", + "e2fsprogs-dev", + "util-linux-dev", + "libtirpc-dev", + "linux-hardened-dev=4.9.65-r1", + "spl-hardened-dev" + ] + }, + "libotr-dev": { + "versions": { + "4.1.1-r1": 1509473948 + }, + "origin": "libotr", + "dependencies": [ + "libgcrypt-dev", + "libotr=4.1.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:libotr=4.1.1" + ] + }, + "gtk-doc": { + "versions": { + "1.26-r2": 1509628913 + }, + "origin": "gtk-doc", + "dependencies": [ + "docbook-xsl", + "gnome-doc-utils", + "perl", + "python2", + "py2-six", + "pkgconf", + "glib-dev", + "highlight" + ], + "provides": [ + "cmd:gtkdoc-check", + "cmd:gtkdoc-depscan", + "cmd:gtkdoc-fixxref", + "cmd:gtkdoc-mkdb", + "cmd:gtkdoc-mkhtml", + "cmd:gtkdoc-mkman", + "cmd:gtkdoc-mkpdf", + "cmd:gtkdoc-rebase", + "cmd:gtkdoc-scan", + "cmd:gtkdoc-scangobj", + "cmd:gtkdocize" + ] + }, + "openbox-doc": { + "versions": { + "3.6.1-r1": 1510073884 + }, + "origin": "openbox" + }, + "perl-module-implementation-doc": { + "versions": { + "0.09-r0": 1509470579 + }, + "origin": "perl-module-implementation" + }, + "qt-mysql": { + "versions": { + "4.8.7-r8": 1510287209 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libmysqlclient.so.18", + "so:libstdc++.so.6" + ] + }, + "gtkmm-doc": { + "versions": { + "2.24.5-r0": 1510073814 + }, + "origin": "gtkmm" + }, + "openvswitch-dbg": { + "versions": { + "2.8.1-r2": 1512030506 + }, + "origin": "openvswitch" + }, + "wavpack-dev": { + "versions": { + "5.1.0-r3": 1548940101, + "5.1.0-r4": 1566809779 + }, + "origin": "wavpack", + "dependencies": [ + "pkgconfig", + "wavpack=5.1.0-r4" + ], + "provides": [ + "pc:wavpack=5.1.0" + ] + }, + "at-spi2-core": { + "versions": { + "2.26.2-r0": 1509466052 + }, + "origin": "at-spi2-core", + "dependencies": [ + "so:libX11.so.6", + "so:libXtst.so.6", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libatspi.so.0=0.0.1" + ] + }, + "perl-crypt-openssl-random-doc": { + "versions": { + "0.11-r4": 1510260064 + }, + "origin": "perl-crypt-openssl-random" + }, + "lxpolkit": { + "versions": { + "0.1.0-r1": 1510076430 + }, + "origin": "lxpolkit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpolkit-agent-1.so.0", + "so:libpolkit-gobject-1.so.0" + ] + }, + "bitchx-doc": { + "versions": { + "1.2.1-r7": 1510260133 + }, + "origin": "bitchx" + }, + "libxcursor-doc": { + "versions": { + "1.1.15-r0": 1511975691 + }, + "origin": "libxcursor" + }, + "py-exifread": { + "versions": { + "2.1.2-r1": 1509552778 + }, + "origin": "py-exifread", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:EXIF.py" + ] + }, + "spamassassin-doc": { + "versions": { + "3.4.1-r8": 1509517411 + }, + "origin": "spamassassin" + }, + "py-vobject": { + "versions": { + "0.9.5-r2": 1510832515 + }, + "origin": "py-vobject", + "provides": [ + "cmd:change_tz", + "cmd:ics_diff" + ] + }, + "ipfw": { + "versions": { + "3.0_git20130607-r1": 1509495344 + }, + "origin": "ipfw", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ipfw" + ] + }, + "lua5.1-optarg": { + "versions": { + "0.2-r1": 1509462489 + }, + "origin": "lua-optarg", + "dependencies": [ + "lua5.1" + ] + }, + "gparted-lang": { + "versions": { + "0.30.0-r0": 1510073982 + }, + "origin": "gparted", + "dependencies": [ + "e2fsprogs", + "ntfs-3g-progs" + ] + }, + "squark": { + "versions": { + "0.6.1-r1": 1509492126 + }, + "origin": "squark", + "dependencies": [ + "haserl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcmph.so.0", + "so:liblua.so.5", + "so:libnetsnmp.so.30" + ], + "provides": [ + "cmd:sqdb-build.lua", + "cmd:squark-auth-ip", + "cmd:squark-auth-snmp", + "cmd:squark-filter" + ] + }, + "perl-async-mergepoint-doc": { + "versions": { + "0.04-r0": 1509494969 + }, + "origin": "perl-async-mergepoint" + }, + "libtirpc-dbg": { + "versions": { + "1.0.1-r2": 1509469768 + }, + "origin": "libtirpc" + }, + "rp-pppoe-doc": { + "versions": { + "3.12-r1": 1541067520 + }, + "origin": "rp-pppoe" + }, + "libee-dev": { + "versions": { + "0.4.1-r0": 1509486165 + }, + "origin": "libee", + "dependencies": [ + "libee=0.4.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libee=0.4.1" + ] + }, + "hvtools": { + "versions": { + "4.11.9-r0": 1509489575 + }, + "origin": "hvtools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:hv_fcopy_daemon", + "cmd:hv_kvp_daemon", + "cmd:hv_vss_daemon" + ] + }, + "py2-sphinxcontrib-websupport": { + "versions": { + "1.0.1-r3": 1509493319 + }, + "origin": "py-sphinxcontrib-websupport", + "dependencies": [ + "python2" + ] + }, + "qemu-s390x": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-s390x" + ] + }, + "lua5.1-curl": { + "versions": { + "0.3.7-r1": 1509493017 + }, + "origin": "lua-curl", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "py2-cryptography": { + "versions": { + "2.0.3-r1": 1510288192, + "2.1.4-r0": 1559543210 + }, + "origin": "py-cryptography", + "dependencies": [ + "py2-cffi", + "py2-idna", + "py2-asn1crypto", + "py2-six", + "py2-ipaddress", + "py-enum34", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpython2.7.so.1.0", + "so:libssl.so.44" + ] + }, + "xfce4-cpugraph-plugin-lang": { + "versions": { + "1.0.5-r1": 1510074086 + }, + "origin": "xfce4-cpugraph-plugin" + }, + "frotz-doc": { + "versions": { + "2.44-r0": 1509491523 + }, + "origin": "frotz" + }, + "ckbcomp": { + "versions": { + "1.170-r0": 1510922360 + }, + "origin": "ckbcomp", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:ckbcomp" + ] + }, + "xf86-video-modesetting": { + "versions": { + "0.9.0-r6": 1510074885 + }, + "origin": "xf86-video-modesetting", + "dependencies": [ + "xorg-server" + ] + }, + "perl-probe-perl": { + "versions": { + "0.03-r0": 1509482467 + }, + "origin": "perl-probe-perl" + }, + "strace-doc": { + "versions": { + "4.19-r0": 1509477631 + }, + "origin": "strace" + }, + "galculator-lang": { + "versions": { + "2.1.4-r0": 1510075353 + }, + "origin": "galculator" + }, + "py-unbound": { + "versions": { + "1.6.7-r1": 1510588258 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root", + "so:libc.musl-x86_64.so.1", + "so:libunbound.so.2" + ] + }, + "procmail-doc": { + "versions": { + "3.22-r3": 1513159329 + }, + "origin": "procmail" + }, + "gvfs-doc": { + "versions": { + "1.34.1-r0": 1511430257, + "1.34.1-r1": 1563787223 + }, + "origin": "gvfs" + }, + "gdbm-dev": { + "versions": { + "1.13-r1": 1509458649 + }, + "origin": "gdbm", + "dependencies": [ + "gdbm=1.13-r1" + ] + }, + "qemu-or1k": { + "versions": { + "2.10.1-r3": 1519746240 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-or1k" + ] + }, + "qt-sqlite": { + "versions": { + "4.8.7-r8": 1510287208 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libsqlite3.so.0", + "so:libstdc++.so.6" + ] + }, + "libmemcached": { + "versions": { + "1.0.18-r2": 1520366185 + }, + "origin": "libmemcached", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libmemcached.so.11", + "so:libmemcachedutil.so.2", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:memcapable", + "cmd:memcat", + "cmd:memcp", + "cmd:memdump", + "cmd:memerror", + "cmd:memexist", + "cmd:memflush", + "cmd:memparse", + "cmd:memping", + "cmd:memrm", + "cmd:memslap", + "cmd:memstat", + "cmd:memtouch" + ] + }, + "the_silver_searcher": { + "versions": { + "2.1.0-r2": 1510831171 + }, + "origin": "the_silver_searcher", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblzma.so.5", + "so:libpcre.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:ag" + ] + }, + "freetype-doc": { + "versions": { + "2.8.1-r4": 1549555370 + }, + "origin": "freetype" + }, + "polkit-doc": { + "versions": { + "0.105-r8": 1551780676, + "0.105-r9": 1563792450, + "0.105-r10": 1567523133 + }, + "origin": "polkit" + }, + "xcb-util-cursor-dev": { + "versions": { + "0.1.3-r1": 1509491136 + }, + "origin": "xcb-util-cursor", + "dependencies": [ + "xcb-util-dev", + "pc:xcb-image", + "pc:xcb-render", + "pc:xcb-renderutil", + "pkgconfig", + "xcb-util-cursor=0.1.3-r1" + ], + "provides": [ + "pc:xcb-cursor=0.1.3" + ] + }, + "libnl-dev": { + "versions": { + "1.1.4-r0": 1509473007 + }, + "origin": "libnl", + "dependencies": [ + "libnl=1.1.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnl-1=1.1.4" + ] + }, + "skalibs-doc": { + "versions": { + "2.6.1.0-r0": 1510364877 + }, + "origin": "skalibs" + }, + "libverto": { + "versions": { + "0.3.0-r0": 1509469587 + }, + "origin": "libverto", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libverto.so.1=1.0.0" + ] + }, + "spice-protocol": { + "versions": { + "0.12.14-r0": 1541599121 + }, + "origin": "spice-protocol", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:spice-protocol=0.12.14" + ] + }, + "py2-pip": { + "versions": { + "9.0.1-r1": 1509493196 + }, + "origin": "py2-pip", + "dependencies": [ + "python2", + "py-setuptools" + ], + "provides": [ + "py-pip=9.0.1-r1", + "cmd:pip", + "cmd:pip2", + "cmd:pip2.7" + ] + }, + "motif-doc": { + "versions": { + "2.3.4-r2": 1509495442 + }, + "origin": "motif" + }, + "lua5.1-socket": { + "versions": { + "3.0_rc1_git20160306-r2": 1509480759 + }, + "origin": "lua-socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-text-vfile-asdata": { + "versions": { + "0.08-r0": 1509474110 + }, + "origin": "perl-text-vfile-asdata", + "dependencies": [ + "perl-class-accessor-chained" + ] + }, + "giblib-dev": { + "versions": { + "1.2.4-r8": 1509470529 + }, + "origin": "giblib", + "dependencies": [ + "imlib2-dev", + "freetype-dev", + "zlib-dev", + "libx11-dev", + "libxext-dev", + "giblib=1.2.4-r8", + "pkgconfig" + ], + "provides": [ + "pc:giblib=1.2.4", + "cmd:giblib-config" + ] + }, + "font-bitstream-speedo": { + "versions": { + "1.0.2-r0": 1509475953 + }, + "origin": "font-bitstream-speedo", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-font-afm": { + "versions": { + "1.20-r0": 1509481558 + }, + "origin": "perl-font-afm" + }, + "libnfsidmap-doc": { + "versions": { + "0.25-r1": 1509492064 + }, + "origin": "libnfsidmap" + }, + "coreutils-doc": { + "versions": { + "8.28-r0": 1509462378 + }, + "origin": "coreutils" + }, + "perl-file-next-doc": { + "versions": { + "1.16-r0": 1509491909 + }, + "origin": "perl-file-next" + }, + "libotr-tools": { + "versions": { + "4.1.1-r1": 1509473951 + }, + "origin": "libotr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libotr.so.5" + ], + "provides": [ + "cmd:otr_mackey", + "cmd:otr_modify", + "cmd:otr_parse", + "cmd:otr_readforge", + "cmd:otr_remac", + "cmd:otr_sesskeys" + ] + }, + "py-cryptography": { + "versions": { + "2.0.3-r1": 1510288193, + "2.1.4-r0": 1559543210 + }, + "origin": "py-cryptography", + "dependencies": [ + "py-cffi", + "py-idna", + "py-asn1crypto", + "py-six" + ] + }, + "charybdis-dev": { + "versions": { + "3.5.5-r3": 1510260651 + }, + "origin": "charybdis", + "dependencies": [ + "pkgconfig" + ] + }, + "gtk+3.0-doc": { + "versions": { + "3.22.21-r0": 1510067779 + }, + "origin": "gtk+3.0" + }, + "libxxf86vm": { + "versions": { + "1.1.4-r1": 1509466237 + }, + "origin": "libxxf86vm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXxf86vm.so.1=1.0.0" + ] + }, + "perl-params-validate": { + "versions": { + "1.28-r0": 1509470588 + }, + "origin": "perl-params-validate", + "dependencies": [ + "perl-module-implementation", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-html-scrubber": { + "versions": { + "0.11-r0": 1509481325 + }, + "origin": "perl-html-scrubber", + "dependencies": [ + "perl-html-parser" + ] + }, + "net-tools-doc": { + "versions": { + "1.60_git20140218-r1": 1509492734 + }, + "origin": "net-tools" + }, + "py-certifi": { + "versions": { + "2017.7.27.1-r1": 1509551896 + }, + "origin": "py-certifi" + }, + "perl-variable-magic": { + "versions": { + "0.62-r0": 1510087954 + }, + "origin": "perl-variable-magic", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.2-microlight": { + "versions": { + "1.1.1-r2": 1509480784 + }, + "origin": "lua-microlight" + }, + "ipsec-tools": { + "versions": { + "0.8.2-r6": 1510261510 + }, + "origin": "ipsec-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ], + "provides": [ + "cmd:plainrsa-gen", + "cmd:racoon", + "cmd:racoonctl", + "cmd:setkey" + ] + }, + "libgit2": { + "versions": { + "0.25.1-r4": 1510288231 + }, + "origin": "libgit2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libssh2.so.1", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "libgit2-libs", + "so:libgit2.so.25=0.25.1" + ] + }, + "freeswitch-sounds-en-us-callie-32000": { + "versions": { + "1.0.16-r1": 1509482236 + }, + "origin": "freeswitch-sounds-en-us-callie-32000" + }, + "lua5.2-expat": { + "versions": { + "1.3.0-r2": 1509483367 + }, + "origin": "lua-expat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "talloc-dev": { + "versions": { + "2.1.10-r0": 1509466374 + }, + "origin": "talloc", + "dependencies": [ + "pkgconfig", + "py-talloc=2.1.10-r0", + "talloc=2.1.10-r0" + ], + "provides": [ + "pc:pytalloc-util=2.1.10", + "pc:talloc=2.1.10" + ] + }, + "libxdmcp-dev": { + "versions": { + "1.1.2-r4": 1509461845 + }, + "origin": "libxdmcp", + "dependencies": [ + "libxdmcp=1.1.2-r4", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xdmcp=1.1.2" + ] + }, + "open-vm-tools-dbg": { + "versions": { + "10.1.15-r0": 1510922342 + }, + "origin": "open-vm-tools" + }, + "lua": { + "versions": { + "5.1.5-r4": 1509471882 + }, + "origin": "lua", + "dependencies": [ + "lua5.1" + ] + }, + "squid-lang-vi": { + "versions": { + "3.5.27-r0": 1519824033, + "3.5.27-r1": 1562865670 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "py2-cairo-dev": { + "versions": { + "1.10.0-r0": 1509471033 + }, + "origin": "py2-cairo", + "dependencies": [ + "pc:cairo", + "pkgconfig" + ], + "provides": [ + "pc:pycairo=1.10.0" + ] + }, + "geoip-doc": { + "versions": { + "1.6.11-r0": 1509474613 + }, + "origin": "geoip" + }, + "libmemcached-dev": { + "versions": { + "1.0.18-r2": 1520366184 + }, + "origin": "libmemcached", + "dependencies": [ + "cyrus-sasl-dev", + "libmemcached-libs=1.0.18-r2", + "pkgconfig" + ], + "provides": [ + "pc:libmemcached=1.0.18" + ] + }, + "perl-stream-buffered": { + "versions": { + "0.02-r1": 1510564489 + }, + "origin": "perl-stream-buffered" + }, + "lua5.1-pc": { + "versions": { + "1.0.0-r9": 1509480085 + }, + "origin": "lua-pc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "subversion-doc": { + "versions": { + "1.9.7-r0": 1510314500, + "1.9.12-r0": 1565086191 + }, + "origin": "subversion" + }, + "mkfontdir": { + "versions": { + "1.0.7-r1": 1509469874 + }, + "origin": "mkfontdir", + "dependencies": [ + "mkfontscale", + "/bin/sh" + ], + "provides": [ + "cmd:mkfontdir" + ] + }, + "keyutils-dev": { + "versions": { + "1.5.10-r0": 1509469693 + }, + "origin": "keyutils", + "dependencies": [ + "keyutils-libs=1.5.10-r0" + ] + }, + "lua5.2-mqtt-publish": { + "versions": { + "0.1-r0": 1509462502 + }, + "origin": "lua-mqtt-publish", + "dependencies": [ + "lua5.2-mosquitto" + ] + }, + "dhcpcd-dbus": { + "versions": { + "0.6.1-r3": 1509486340 + }, + "origin": "dhcpcd-dbus", + "dependencies": [ + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ] + }, + "perl-libwww": { + "versions": { + "6.29-r0": 1510038531 + }, + "origin": "perl-libwww", + "dependencies": [ + "perl-http-date", + "perl-http-cookies", + "perl-net-http", + "perl-http-daemon", + "perl-html-parser", + "perl-file-listing", + "perl-www-robotrules", + "perl-http-negotiate", + "perl-uri", + "perl-http-message", + "perl-lwp-mediatypes", + "perl-encode-locale", + "perl-try-tiny" + ], + "provides": [ + "cmd:lwp-download", + "cmd:lwp-dump", + "cmd:lwp-mirror", + "cmd:lwp-request" + ] + }, + "collectd-write_redis": { + "versions": { + "5.7.2-r0": 1510069651 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.13" + ] + }, + "zsh-zftp": { + "versions": { + "5.4.2-r1": 1522503663 + }, + "origin": "zsh", + "dependencies": [ + "zsh", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-mime-lite": { + "versions": { + "3.030-r1": 1509495013 + }, + "origin": "perl-mime-lite", + "dependencies": [ + "perl", + "perl-mime-types", + "perl-email-date-format", + "perl-mailtools", + "perl-test-pod-coverage" + ] + }, + "arpwatch": { + "versions": { + "2.1a15-r16": 1510075585 + }, + "origin": "arpwatch", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:arp2ethers", + "cmd:arpfetch", + "cmd:arpsnmp", + "cmd:arpwatch", + "cmd:bihourly.sh", + "cmd:massagevendor" + ] + }, + "open-vm-tools": { + "versions": { + "10.1.15-r0": 1510922342 + }, + "origin": "open-vm-tools", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libfuse.so.2", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libicui18n.so.59", + "so:libicuuc.so.59", + "so:libintl.so.8", + "so:libmspack.so.0", + "so:libssl.so.44", + "so:libtirpc.so.3" + ], + "provides": [ + "so:libDeployPkg.so.0=0.0.0", + "so:libguestlib.so.0=0.0.0", + "so:libhgfs.so.0=0.0.0", + "so:libvmtools.so.0=0.0.0", + "cmd:mount.vmhgfs", + "cmd:vm-support", + "cmd:vmhgfs-fuse", + "cmd:vmtoolsd", + "cmd:vmware-checkvm", + "cmd:vmware-guestproxycerttool", + "cmd:vmware-hgfsclient", + "cmd:vmware-namespace-cmd", + "cmd:vmware-rpctool", + "cmd:vmware-toolbox-cmd", + "cmd:vmware-xferlogs" + ] + }, + "libpng-utils": { + "versions": { + "1.6.37-r0": 1557132258 + }, + "origin": "libpng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpng16.so.16", + "so:libz.so.1" + ], + "provides": [ + "cmd:png-fix-itxt", + "cmd:pngfix" + ] + }, + "startup-notification-dev": { + "versions": { + "0.12-r3": 1509468553 + }, + "origin": "startup-notification", + "dependencies": [ + "libsm-dev", + "xcb-util-dev", + "pkgconfig", + "startup-notification=0.12-r3" + ], + "provides": [ + "pc:libstartup-notification-1.0=0.12" + ] + }, + "quagga-doc": { + "versions": { + "1.2.4-r0": 1519133992 + }, + "origin": "quagga" + }, + "sg3_utils": { + "versions": { + "1.42-r0": 1509482219 + }, + "origin": "sg3_utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsgutils2.so.2=2.0.0", + "cmd:scsi_logging_level", + "cmd:scsi_mandat", + "cmd:scsi_readcap", + "cmd:scsi_ready", + "cmd:scsi_satl", + "cmd:scsi_start", + "cmd:scsi_stop", + "cmd:scsi_temperature", + "cmd:sg_compare_and_write", + "cmd:sg_copy_results", + "cmd:sg_dd", + "cmd:sg_decode_sense", + "cmd:sg_emc_trespass", + "cmd:sg_format", + "cmd:sg_get_config", + "cmd:sg_get_lba_status", + "cmd:sg_ident", + "cmd:sg_inq", + "cmd:sg_logs", + "cmd:sg_luns", + "cmd:sg_map", + "cmd:sg_map26", + "cmd:sg_modes", + "cmd:sg_opcodes", + "cmd:sg_persist", + "cmd:sg_prevent", + "cmd:sg_raw", + "cmd:sg_rbuf", + "cmd:sg_rdac", + "cmd:sg_read", + "cmd:sg_read_attr", + "cmd:sg_read_block_limits", + "cmd:sg_read_buffer", + "cmd:sg_read_long", + "cmd:sg_readcap", + "cmd:sg_reassign", + "cmd:sg_referrals", + "cmd:sg_rep_zones", + "cmd:sg_requests", + "cmd:sg_reset", + "cmd:sg_reset_wp", + "cmd:sg_rmsn", + "cmd:sg_rtpg", + "cmd:sg_safte", + "cmd:sg_sanitize", + "cmd:sg_sat_identify", + "cmd:sg_sat_phy_event", + "cmd:sg_sat_read_gplog", + "cmd:sg_sat_set_features", + "cmd:sg_scan", + "cmd:sg_senddiag", + "cmd:sg_ses", + "cmd:sg_ses_microcode", + "cmd:sg_start", + "cmd:sg_stpg", + "cmd:sg_sync", + "cmd:sg_test_rwbuf", + "cmd:sg_timestamp", + "cmd:sg_turs", + "cmd:sg_unmap", + "cmd:sg_verify", + "cmd:sg_vpd", + "cmd:sg_wr_mode", + "cmd:sg_write_buffer", + "cmd:sg_write_long", + "cmd:sg_write_same", + "cmd:sg_write_verify", + "cmd:sg_xcopy", + "cmd:sg_zone", + "cmd:sginfo", + "cmd:sgm_dd", + "cmd:sgp_dd" + ] + }, + "perl-html-quoted-doc": { + "versions": { + "0.04-r0": 1511989791 + }, + "origin": "perl-html-quoted" + }, + "perl-file-temp": { + "versions": { + "0.2304-r0": 1509495373 + }, + "origin": "perl-file-temp" + }, + "perl-css-minifier-xs": { + "versions": { + "0.09-r3": 1509489144 + }, + "origin": "perl-css-minifier-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "aaudit": { + "versions": { + "0.7.2-r1": 1510074394 + }, + "origin": "aaudit", + "dependencies": [ + "lua5.2", + "lua5.2-posix", + "lua5.2-cjson", + "lua5.2-pc", + "lua5.2-socket" + ], + "provides": [ + "cmd:aaudit" + ] + }, + "qjson": { + "versions": { + "0.9.0-r0": 1510075393 + }, + "origin": "qjson", + "dependencies": [ + "so:libQtCore.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libqjson.so.0=0.9.0" + ] + }, + "libfontenc-dev": { + "versions": { + "1.1.3-r2": 1509469860 + }, + "origin": "libfontenc", + "dependencies": [ + "libfontenc=1.1.3-r2", + "pkgconfig" + ], + "provides": [ + "pc:fontenc=1.1.3" + ] + }, + "cmocka": { + "versions": { + "1.1.1-r1": 1509476798 + }, + "origin": "cmocka", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcmocka.so.0=0.4.1" + ] + }, + "lsof": { + "versions": { + "4.89-r1": 1510330781 + }, + "origin": "lsof", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lsof" + ] + }, + "openldap-backend-all": { + "versions": { + "2.4.45-r3": 1510258132, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "openldap-back-bdb", + "openldap-back-dnssrv", + "openldap-back-hdb", + "openldap-back-ldap", + "openldap-back-mdb", + "openldap-back-meta", + "openldap-back-monitor", + "openldap-back-null", + "openldap-back-passwd", + "openldap-back-relay", + "openldap-back-shell", + "openldap-back-sql", + "openldap-back-sock" + ] + }, + "desktop-file-utils-doc": { + "versions": { + "0.23-r0": 1509470840 + }, + "origin": "desktop-file-utils" + }, + "perl-crypt-x509": { + "versions": { + "0.51-r0": 1509480699 + }, + "origin": "perl-crypt-x509", + "dependencies": [ + "perl-convert-asn1" + ] + }, + "links-doc": { + "versions": { + "2.14-r2": 1510259134 + }, + "origin": "links" + }, + "acct-doc": { + "versions": { + "6.6.4-r0": 1509462238 + }, + "origin": "acct" + }, + "abiword-plugin-freetranslation": { + "versions": { + "3.0.2-r1": 1510073359 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "xfce4-clipman-plugin": { + "versions": { + "1.4.2-r0": 1510073453 + }, + "origin": "xfce4-clipman-plugin", + "dependencies": [ + "so:libX11.so.6", + "so:libXtst.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libxfce4panel-2.0.so.4", + "so:libxfce4ui-2.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "cmd:xfce4-clipman", + "cmd:xfce4-clipman-settings", + "cmd:xfce4-popup-clipman", + "cmd:xfce4-popup-clipman-actions" + ] + }, + "gtk-engines-redmond": { + "versions": { + "2.21.0-r2": 1510071724 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "pidgin-sipe": { + "versions": { + "1.22.1-r0": 1510931372 + }, + "origin": "pidgin-sipe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmime-2.6.so.0", + "so:libgobject-2.0.so.0", + "so:libgssapi.so.3", + "so:libintl.so.8", + "so:libnspr4.so", + "so:libnss3.so", + "so:libpurple.so.0", + "so:libsmime3.so", + "so:libxml2.so.2" + ] + }, + "mt-st": { + "versions": { + "1.1-r4": 1509496051 + }, + "origin": "mt-st", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mt", + "cmd:stinit" + ] + }, + "ppp": { + "versions": { + "2.4.7-r5": 1509480143 + }, + "origin": "ppp", + "dependencies": [ + "ppp-chat", + "ppp-radius", + "ppp-atm", + "ppp-pppoe", + "ppp-l2tp", + "ppp-winbind", + "ppp-passprompt", + "ppp-passwordfd", + "ppp-minconn", + "ppp-daemon" + ] + }, + "nagios-plugins-mailq": { + "versions": { + "2.2.1-r3": 1510288496 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins" + ] + }, + "clutter-doc": { + "versions": { + "1.26.2-r1": 1510073008 + }, + "origin": "clutter" + }, + "py2-backports_abc": { + "versions": { + "0.4-r3": 1509552781 + }, + "origin": "py-backports_abc", + "dependencies": [ + "python2" + ] + }, + "py-subversion": { + "versions": { + "1.9.7-r0": 1510314503, + "1.9.12-r0": 1565086191 + }, + "origin": "subversion", + "dependencies": [ + "so:libapr-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libsvn_client-1.so.0", + "so:libsvn_delta-1.so.0", + "so:libsvn_diff-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_ra-1.so.0", + "so:libsvn_repos-1.so.0", + "so:libsvn_subr-1.so.0", + "so:libsvn_wc-1.so.0" + ], + "provides": [ + "so:libsvn_swig_py-1.so.0=0.0.0" + ] + }, + "libxfont-dev": { + "versions": { + "1.5.4-r0": 1511976122 + }, + "origin": "libxfont", + "dependencies": [ + "libxfont=1.5.4-r0", + "pc:fontenc", + "pc:fontsproto", + "pc:freetype2", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xfont=1.5.4" + ] + }, + "qemu-system-aarch64": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libstdc++.so.6", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-aarch64" + ] + }, + "py3-click": { + "versions": { + "6.7-r2": 1509476499 + }, + "origin": "py-click", + "dependencies": [ + "python3" + ] + }, + "py-docutils-doc": { + "versions": { + "0.13.1-r0": 1509486247 + }, + "origin": "py-docutils", + "dependencies": [ + "py3-docutils" + ] + }, + "unifont": { + "versions": { + "9.0.06-r0": 1509491375 + }, + "origin": "unifont", + "dependencies": [ + "bdftopcf", + "perl-gd", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bdfimplode", + "cmd:hex2bdf", + "cmd:hex2sfd", + "cmd:hexbraille", + "cmd:hexdraw", + "cmd:hexkinya", + "cmd:hexmerge", + "cmd:johab2ucs2", + "cmd:unibdf2hex", + "cmd:unibmp2hex", + "cmd:unicoverage", + "cmd:unidup", + "cmd:unifont-viewer", + "cmd:unifontchojung", + "cmd:unifontksx", + "cmd:unifontpic", + "cmd:unigencircles", + "cmd:unigenwidth", + "cmd:unihex2bmp", + "cmd:unihex2png", + "cmd:unihexfill", + "cmd:unihexgen", + "cmd:unipagecount", + "cmd:unipng2hex" + ] + }, + "dnsmasq": { + "versions": { + "2.78-r3": 1540537793 + }, + "origin": "dnsmasq", + "dependencies": [ + "!dnsmasq-dnssec", + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dnsmasq" + ] + }, + "lynx-doc": { + "versions": { + "2.8.8_p2-r6": 1510842477 + }, + "origin": "lynx" + }, + "py-gtk-doc": { + "versions": { + "2.24.0-r14": 1510071866 + }, + "origin": "py-gtk" + }, + "augeas": { + "versions": { + "1.9.0-r3": 1511899746 + }, + "origin": "augeas", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1", + "so:libfa.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:augparse", + "cmd:augtool", + "cmd:fadot" + ] + }, + "python3-tests": { + "versions": { + "3.6.8-r0": 1555928808, + "3.6.9-r0": 1571233994, + "3.6.9-r1": 1571314638 + }, + "origin": "python3" + }, + "qemu-system-mips64el": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-mips64el" + ] + }, + "jsoncpp-dev": { + "versions": { + "1.8.1-r1": 1510311447 + }, + "origin": "jsoncpp", + "dependencies": [ + "jsoncpp=1.8.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:jsoncpp=1.8.1" + ] + }, + "perl-devel-stacktrace": { + "versions": { + "2.02-r0": 1509468385 + }, + "origin": "perl-devel-stacktrace" + }, + "py3-pylast": { + "versions": { + "1.9.0-r0": 1509491030 + }, + "origin": "py-pylast", + "dependencies": [ + "python3" + ] + }, + "pango": { + "versions": { + "1.40.14-r1": 1541519450 + }, + "origin": "pango", + "dependencies": [ + "/bin/sh", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libharfbuzz.so.0" + ], + "provides": [ + "so:libpango-1.0.so.0=0.4000.14", + "so:libpangocairo-1.0.so.0=0.4000.14", + "so:libpangoft2-1.0.so.0=0.4000.14", + "so:libpangoxft-1.0.so.0=0.4000.14", + "cmd:pango-view" + ] + }, + "openrc": { + "versions": { + "0.24.1-r4": 1511887835 + }, + "origin": "openrc", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libeinfo.so.1=1", + "so:librc.so.1=1", + "cmd:openrc", + "cmd:openrc-run", + "cmd:rc", + "cmd:rc-service", + "cmd:rc-sstat", + "cmd:rc-status", + "cmd:rc-update", + "cmd:runscript", + "cmd:service", + "cmd:start-stop-daemon", + "cmd:supervise-daemon" + ] + }, + "apache2-ctl": { + "versions": { + "2.4.39-r0": 1554306880, + "2.4.41-r0": 1566292326 + }, + "origin": "apache2", + "dependencies": [ + "lynx" + ], + "provides": [ + "cmd:apachectl" + ] + }, + "perl-dbix-searchbuilder-doc": { + "versions": { + "1.67-r1": 1510588867 + }, + "origin": "perl-dbix-searchbuilder" + }, + "perl-error": { + "versions": { + "0.17025-r0": 1509461799 + }, + "origin": "perl-error", + "dependencies": [ + "perl" + ] + }, + "clang": { + "versions": { + "5.0.0-r0": 1510683247 + }, + "origin": "clang", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libclang.so.5.0", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:c-index-test", + "cmd:clang", + "cmd:clang++", + "cmd:clang-5.0", + "cmd:clang-check", + "cmd:clang-cl", + "cmd:clang-cpp", + "cmd:clang-format", + "cmd:clang-import-test", + "cmd:clang-offload-bundler", + "cmd:clang-rename", + "cmd:git-clang-format" + ] + }, + "wayland-protocols": { + "versions": { + "1.11-r0": 1510088213 + }, + "origin": "wayland-protocols", + "provides": [ + "wayland-protocols-dev" + ] + }, + "perl-future-doc": { + "versions": { + "0.37-r0": 1511989972 + }, + "origin": "perl-future" + }, + "cyrus-sasl-crammd5": { + "versions": { + "2.1.26-r11": 1510258045 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libtxc_dxtn-dev": { + "versions": { + "1.0.1-r5": 1510075567 + }, + "origin": "libtxc_dxtn", + "dependencies": [ + "mesa-dev" + ] + }, + "py-country": { + "versions": { + "17.9.23-r0": 1509493919 + }, + "origin": "py-country" + }, + "kamailio-extras": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libhiredis.so.0.13", + "so:libsrdb1.so.1", + "so:libsrutils.so.1" + ] + }, + "mercurial": { + "versions": { + "4.5.2-r0": 1532937176, + "4.5.2-r1": 1563792023 + }, + "origin": "mercurial", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:hg", + "cmd:hgeditor", + "cmd:hgk" + ] + }, + "libpcap-doc": { + "versions": { + "1.8.1-r1": 1509475964 + }, + "origin": "libpcap" + }, + "testdisk": { + "versions": { + "7.0-r4": 1509491594 + }, + "origin": "testdisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libext2fs.so.2", + "so:libjpeg.so.8", + "so:libncursesw.so.6", + "so:libntfs-3g.so.88", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:fidentify", + "cmd:photorec", + "cmd:testdisk" + ] + }, + "gdl": { + "versions": { + "3.22.0-r0": 1510074988 + }, + "origin": "gdl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgdl-3.so.5=5.0.9" + ] + }, + "syslog-ng-json": { + "versions": { + "3.9.1-r3": 1510259404 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjson-c.so.2", + "so:libsyslog-ng-3.9.so.0" + ] + }, + "privoxy-doc": { + "versions": { + "3.0.26-r0": 1509495184 + }, + "origin": "privoxy" + }, + "uwsgi-stats_pusher_statsd": { + "versions": { + "2.0.17-r0": 1522154659 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "gmime-dev": { + "versions": { + "2.6.20-r0": 1510588179 + }, + "origin": "gmime", + "dependencies": [ + "glib-dev", + "gpgme-dev", + "gmime=2.6.20-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gmime-2.6=2.6.20" + ] + }, + "ppp-winbind": { + "versions": { + "2.4.7-r5": 1509480134 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-yaml-syck": { + "versions": { + "1.30-r1": 1509461788 + }, + "origin": "perl-yaml-syck", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "net-snmp-agent-libs": { + "versions": { + "5.7.3-r10": 1510259616 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.30" + ], + "provides": [ + "so:libnetsnmpagent.so.30=30.0.3", + "so:libnetsnmphelpers.so.30=30.0.3", + "so:libnetsnmpmibs.so.30=30.0.3", + "so:libnetsnmptrapd.so.30=30.0.3" + ] + }, + "xmlrpc-c": { + "versions": { + "1.39.11-r0": 1509482043 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "so:libxmlrpc.so.3=3.39", + "so:libxmlrpc_server.so.3=3.39", + "so:libxmlrpc_util.so.3=3.39" + ] + }, + "py-ldb": { + "versions": { + "1.3.0-r1": 1534939012 + }, + "origin": "ldb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldb.so.1", + "so:libpython2.7.so.1.0", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libpyldb-util.so.1=1.3.0" + ] + }, + "udns": { + "versions": { + "0.4-r0": 1509491398 + }, + "origin": "udns", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libudns.so.0=0", + "cmd:dnsget", + "cmd:ex-rdns", + "cmd:rblcheck" + ] + }, + "librsync-doc": { + "versions": { + "2.0.0-r1": 1509472924 + }, + "origin": "librsync" + }, + "f2fs-tools": { + "versions": { + "1.6.1-r0": 1509483525 + }, + "origin": "f2fs-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libf2fs.so.0", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:defrag.f2fs", + "cmd:dump.f2fs", + "cmd:f2fstat", + "cmd:fibmap.f2fs", + "cmd:fsck.f2fs", + "cmd:mkfs.f2fs", + "cmd:parse.f2fs" + ] + }, + "libmms": { + "versions": { + "0.6.4-r0": 1509480980 + }, + "origin": "libmms", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmms.so.0=0.0.2" + ] + }, + "libxslt": { + "versions": { + "1.1.31-r1": 1555487793, + "1.1.31-r2": 1572541164 + }, + "origin": "libxslt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libxml2.so.2" + ], + "provides": [ + "so:libexslt.so.0=0.8.19", + "so:libxslt.so.1=1.1.31", + "cmd:xsltproc" + ] + }, + "libxaw-dev": { + "versions": { + "1.0.13-r0": 1509480583 + }, + "origin": "libxaw", + "dependencies": [ + "xproto", + "libxmu-dev", + "libxpm-dev", + "libxext-dev", + "libxaw=1.0.13-r0", + "pc:x11", + "pc:xext", + "pc:xmu", + "pc:xpm", + "pc:xproto", + "pc:xt", + "pkgconfig" + ], + "provides": [ + "pc:xaw7=1.0.13" + ] + }, + "sngtc_client-dev": { + "versions": { + "1.3.7-r1": 1509481973 + }, + "origin": "sngtc_client", + "dependencies": [ + "sngtc_client" + ] + }, + "ansible": { + "versions": { + "2.4.6.0-r0": 1538034318, + "2.4.6.0-r1": 1568287983 + }, + "origin": "ansible", + "dependencies": [ + "python2", + "py2-yaml", + "py2-paramiko", + "py2-jinja2", + "py2-markupsafe", + "py2-crypto" + ], + "provides": [ + "cmd:ansible", + "cmd:ansible-config", + "cmd:ansible-connection", + "cmd:ansible-console", + "cmd:ansible-doc", + "cmd:ansible-galaxy", + "cmd:ansible-inventory", + "cmd:ansible-playbook", + "cmd:ansible-pull", + "cmd:ansible-vault" + ] + }, + "py-flask-assets": { + "versions": { + "0.10-r1": 1509551875 + }, + "origin": "py-flask-assets", + "dependencies": [ + "python2", + "py-flask", + "py-webassets" + ] + }, + "apache2-icons": { + "versions": { + "2.4.39-r0": 1554306881, + "2.4.41-r0": 1566292327 + }, + "origin": "apache2" + }, + "hylafax-doc": { + "versions": { + "6.0.6-r4": 1539067243 + }, + "origin": "hylafax" + }, + "sparsehash": { + "versions": { + "2.0.3-r0": 1509489128 + }, + "origin": "sparsehash", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:libsparsehash=2.0.2" + ] + }, + "opensp": { + "versions": { + "1.5.2-r0": 1509488687 + }, + "origin": "opensp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libosp.so.5=5.0.0", + "cmd:onsgmls", + "cmd:osgmlnorm", + "cmd:ospam", + "cmd:ospcat", + "cmd:ospent", + "cmd:osx" + ] + }, + "asterisk": { + "versions": { + "15.6.1-r0": 1537795343, + "15.6.2-r0": 1568705005 + }, + "origin": "asterisk", + "dependencies": [ + "/bin/sh", + "so:libc-client.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libedit.so.0", + "so:libjansson.so.4", + "so:liblua.so.5", + "so:libpj.so.2", + "so:libpjlib-util.so.2", + "so:libpjmedia.so.2", + "so:libpjnath.so.2", + "so:libpjsip-simple.so.2", + "so:libpjsip-ua.so.2", + "so:libpjsip.so.2", + "so:libportaudio.so.2", + "so:libsqlite3.so.0", + "so:libssl.so.44", + "so:libuuid.so.1", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libasteriskssl.so.1=1", + "cmd:astcanary", + "cmd:astdb2bdb", + "cmd:astdb2sqlite3", + "cmd:asterisk", + "cmd:astgenkey", + "cmd:astversion", + "cmd:autosupport", + "cmd:rasterisk", + "cmd:safe_asterisk" + ] + }, + "libgsf-doc": { + "versions": { + "1.14.41-r0": 1509467200 + }, + "origin": "libgsf" + }, + "apache2-doc": { + "versions": { + "2.4.39-r0": 1554306881, + "2.4.41-r0": 1566292327 + }, + "origin": "apache2" + }, + "py-pygments": { + "versions": { + "2.2.0-r0": 1509493373 + }, + "origin": "py-pygments", + "dependencies": [ + "py3-pygments", + "py3-pygments=2.2.0-r0" + ] + }, + "bacula-sqlite": { + "versions": { + "9.0.5-r3": 1512992127 + }, + "origin": "bacula", + "dependencies": [ + "bacula", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libbaccats-sqlite3-9.0.5.so=0" + ] + }, + "ldb-dev": { + "versions": { + "1.3.0-r1": 1534939012 + }, + "origin": "ldb", + "dependencies": [ + "ldb=1.3.0-r1", + "pc:talloc", + "pc:tdb", + "pkgconfig", + "py-ldb=1.3.0-r1" + ], + "provides": [ + "pc:ldb=1.3.0", + "pc:pyldb-util=1.3.0" + ] + }, + "monit-doc": { + "versions": { + "5.24.0-r1": 1510261491, + "5.24.0-r2": 1559744492 + }, + "origin": "monit" + }, + "abiword-plugin-docbook": { + "versions": { + "3.0.2-r1": 1510073358 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "py3-olefile": { + "versions": { + "0.43-r2": 1509489936 + }, + "origin": "py-olefile" + }, + "perl-path-tiny": { + "versions": { + "0.104-r1": 1510855685 + }, + "origin": "perl-path-tiny" + }, + "lua5.3-sqlite": { + "versions": { + "0.9.4-r2": 1509482116 + }, + "origin": "lua-sqlite", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "lua5.3-sql-mysql": { + "versions": { + "2.3.5-r1": 1509488832 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libmysqlclient.so.18" + ] + }, + "xset-doc": { + "versions": { + "1.2.3-r1": 1509481585 + }, + "origin": "xset" + }, + "i3lock": { + "versions": { + "2.9.1-r0": 1509491492 + }, + "origin": "i3lock", + "dependencies": [ + "xkeyboard-config", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libev.so.4", + "so:libpam.so.0", + "so:libxcb-image.so.0", + "so:libxcb-util.so.1", + "so:libxcb-xinerama.so.0", + "so:libxcb-xkb.so.1", + "so:libxcb.so.1", + "so:libxkbcommon-x11.so.0", + "so:libxkbcommon.so.0" + ], + "provides": [ + "cmd:i3lock" + ] + }, + "openssl-dev": { + "versions": { + "1.0.2r-r0": 1552814650, + "1.0.2t-r0": 1568300394 + }, + "origin": "openssl", + "dependencies": [ + "zlib-dev", + "libcrypto1.0=1.0.2t-r0", + "libssl1.0=1.0.2t-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcrypto=1.0.2t", + "pc:libssl=1.0.2t", + "pc:openssl=1.0.2t" + ] + }, + "iptraf-ng-doc": { + "versions": { + "1.1.4-r4": 1509495003 + }, + "origin": "iptraf-ng" + }, + "cairo-dbg": { + "versions": { + "1.14.10-r0": 1509464621 + }, + "origin": "cairo" + }, + "freeradius-dev": { + "versions": { + "3.0.15-r4": 1556202795, + "3.0.15-r5": 1566310603 + }, + "origin": "freeradius" + }, + "avahi-lang": { + "versions": { + "0.6.32-r4": 1509465449, + "0.6.32-r5": 1563345619 + }, + "origin": "avahi" + }, + "sqlite-doc": { + "versions": { + "3.25.3-r0": 1548491335, + "3.25.3-r1": 1563951530, + "3.25.3-r2": 1571590443 + }, + "origin": "sqlite" + }, + "rtorrent": { + "versions": { + "0.9.6-r2": 1509492669 + }, + "origin": "rtorrent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libstdc++.so.6", + "so:libtorrent.so.19", + "so:libxmlrpc.so.3", + "so:libxmlrpc_server.so.3", + "so:libxmlrpc_util.so.3" + ], + "provides": [ + "cmd:rtorrent" + ] + }, + "libdc1394-dev": { + "versions": { + "2.2.5-r1": 1509480969 + }, + "origin": "libdc1394", + "dependencies": [ + "libusb-dev", + "libraw1394-dev", + "libdc1394=2.2.5-r1", + "pkgconfig" + ], + "provides": [ + "pc:libdc1394-2=2.2.5" + ] + }, + "perl-term-table-doc": { + "versions": { + "0.012-r0": 1509631261 + }, + "origin": "perl-term-table" + }, + "xf86-input-synaptics-dev": { + "versions": { + "1.9.0-r1": 1510073650 + }, + "origin": "xf86-input-synaptics", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xorg-synaptics=1.9.0" + ] + }, + "audacious-plugins": { + "versions": { + "3.9-r0": 1510288542 + }, + "origin": "audacious-plugins", + "dependencies": [ + "audacious", + "so:libFLAC.so.8", + "so:libSDL-1.2.so.0", + "so:libX11.so.6", + "so:libXcomposite.so.1", + "so:libXrender.so.1", + "so:libasound.so.2", + "so:libaudcore.so.5", + "so:libaudgui.so.5", + "so:libaudtag.so.3", + "so:libavcodec.so.57", + "so:libavformat.so.57", + "so:libavutil.so.55", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcddb.so.2", + "so:libcdio.so.16", + "so:libcdio_cdda.so.2", + "so:libcue.so.2", + "so:libcurl.so.4", + "so:libdbus-glib-1.so.2", + "so:libfaad.so.2", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libmms.so.0", + "so:libmp3lame.so.0", + "so:libmpg123.so.0", + "so:libneon.so.27", + "so:libnotify.so.4", + "so:libogg.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libsamplerate.so.0", + "so:libsndfile.so.1", + "so:libstdc++.so.6", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2", + "so:libvorbisfile.so.3", + "so:libwavpack.so.1", + "so:libxml2.so.2", + "so:libz.so.1" + ] + }, + "qemu-system-lm32": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libX11.so.6", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-lm32" + ] + }, + "s6-dev": { + "versions": { + "2.6.1.1-r0": 1510364940 + }, + "origin": "s6", + "dependencies": [ + "s6=2.6.1.1-r0" + ] + }, + "perl-mail-domainkeys": { + "versions": { + "1.0-r2": 1509494036 + }, + "origin": "perl-mail-domainkeys", + "dependencies": [ + "perl" + ] + }, + "putty-doc": { + "versions": { + "0.71-r1": 1554726628 + }, + "origin": "putty" + }, + "awall": { + "versions": { + "1.5.0-r0": 1509742667 + }, + "origin": "awall", + "dependencies": [ + "bind-tools", + "ip6tables", + "ipset", + "iptables", + "lua5.2", + "lua5.2-alt-getopt", + "lua5.2-cjson", + "lua5.2-pc", + "lua5.2-posix", + "lua5.2-stringy", + "xtables-addons", + "/bin/sh" + ], + "provides": [ + "cmd:awall" + ] + }, + "uwsgi-webdav": { + "versions": { + "2.0.17-r0": 1522154660 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ] + }, + "util-linux-bash-completion": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux" + }, + "lzo": { + "versions": { + "2.10-r2": 1509462188 + }, + "origin": "lzo", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblzo2.so.2=2.0.0" + ] + }, + "glade-dev": { + "versions": { + "3.20.1-r0": 1510073637 + }, + "origin": "glade", + "dependencies": [ + "glade=3.20.1-r0", + "pc:gtk+-3.0>=2.91.2", + "pc:libxml-2.0>=2.4.0", + "pkgconfig" + ], + "provides": [ + "pc:gladeui-2.0=3.20.1" + ] + }, + "libxfont2-dev": { + "versions": { + "2.0.3-r0": 1511975683 + }, + "origin": "libxfont2", + "dependencies": [ + "libxfont2=2.0.3-r0", + "pc:fontenc", + "pc:fontsproto", + "pc:freetype2", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xfont2=2.0.3" + ] + }, + "openvpn-dev": { + "versions": { + "2.4.4-r1": 1510259339 + }, + "origin": "openvpn" + }, + "alsa-utils-doc": { + "versions": { + "1.1.4-r0": 1509468851 + }, + "origin": "alsa-utils" + }, + "ruby-bundler": { + "versions": { + "1.16.1-r0": 1521049537 + }, + "origin": "ruby-bundler", + "dependencies": [ + "ruby", + "ruby-io-console" + ] + }, + "gcc-java": { + "versions": { + "6.4.0-r5": 1509458077 + }, + "origin": "gcc", + "dependencies": [ + "zlib-dev", + "gcc=6.4.0-r5", + "libgcj=6.4.0-r5", + "libgcj=6.4.0-r5", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgcj-tools.so.17", + "so:libgcj.so.17", + "so:libgcj_bc.so.1", + "so:libgmp.so.10", + "so:libisl.so.15", + "so:libmpc.so.3", + "so:libmpfr.so.4", + "so:libz.so.1" + ], + "provides": [ + "cmd:gcj", + "cmd:gcj-dbtool", + "cmd:gcjh", + "cmd:gjavah", + "cmd:jcf-dump" + ] + }, + "perl-params-validationcompiler-doc": { + "versions": { + "0.24-r1": 1510588892 + }, + "origin": "perl-params-validationcompiler" + }, + "pax-utils-doc": { + "versions": { + "1.2.2-r1": 1509459679 + }, + "origin": "pax-utils" + }, + "perl-net-ip": { + "versions": { + "1.26-r0": 1509469034 + }, + "origin": "perl-net-ip", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:ipcount", + "cmd:iptab" + ] + }, + "subversion-dev": { + "versions": { + "1.9.7-r0": 1510314499, + "1.9.12-r0": 1565086191 + }, + "origin": "subversion", + "dependencies": [ + "perl-subversion=1.9.12-r0", + "py-subversion=1.9.12-r0", + "subversion-libs=1.9.12-r0" + ] + }, + "lua5.1-stdlib": { + "versions": { + "41.2.0-r0": 1509495242 + }, + "origin": "lua-stdlib" + }, + "acf-provisioning": { + "versions": { + "0.10.1-r0": 1510075372 + }, + "origin": "acf-provisioning", + "dependencies": [ + "acf-core", + "lua-sql-postgres", + "postgresql-client", + "lua-posixtz", + "lua-xml" + ] + }, + "acf-jquery": { + "versions": { + "0.4.2-r1": 1510068292 + }, + "origin": "acf-jquery" + }, + "xmlindent": { + "versions": { + "0.2.17-r0": 1509490834 + }, + "origin": "xmlindent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfl.so.2" + ], + "provides": [ + "cmd:xmlindent" + ] + }, + "perl-inline-doc": { + "versions": { + "0.80-r0": 1509489227 + }, + "origin": "perl-inline" + }, + "libxml2-doc": { + "versions": { + "2.9.8-r1": 1540398579 + }, + "origin": "libxml2" + }, + "libxres-dev": { + "versions": { + "1.2.0-r0": 1509468563 + }, + "origin": "libxres", + "dependencies": [ + "resourceproto", + "libxres=1.2.0-r0", + "pc:x11", + "pc:xext", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xres=1.2.0" + ] + }, + "jack-dev": { + "versions": { + "1.9.10-r5": 1509473318 + }, + "origin": "jack", + "dependencies": [ + "jack=1.9.10-r5", + "pkgconfig" + ], + "provides": [ + "pc:jack=1.9.10" + ] + }, + "libsndfile-doc": { + "versions": { + "1.0.28-r4": 1548491633, + "1.0.28-r5": 1563345857 + }, + "origin": "libsndfile" + }, + "gcr-dev": { + "versions": { + "3.20.0-r1": 1510073692 + }, + "origin": "gcr", + "dependencies": [ + "glib-dev", + "gtk+3.0-dev", + "libgcrypt-dev", + "p11-kit-dev", + "gcr=3.20.0-r1", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gtk+-3.0", + "pc:p11-kit-1", + "pkgconfig" + ], + "provides": [ + "pc:gck-1=3.20.0", + "pc:gcr-3=3.20.0", + "pc:gcr-base-3=3.20.0", + "pc:gcr-ui-3=3.20.0" + ] + }, + "lua5.2-stringy": { + "versions": { + "0.4.0-r5": 1509480101 + }, + "origin": "lua-stringy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libwebsockets-dev": { + "versions": { + "2.4.0-r1": 1510258054 + }, + "origin": "libwebsockets", + "dependencies": [ + "libwebsockets=2.4.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:libwebsockets=2.4.0", + "pc:libwebsockets_static=2.4.0" + ] + }, + "qt-private-dev": { + "versions": { + "4.8.7-r8": 1510287210 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates" + ] + }, + "py2-gunicorn": { + "versions": { + "19.7.1-r1": 1509493756 + }, + "origin": "py-gunicorn", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:gunicorn", + "cmd:gunicorn_paster" + ] + }, + "libunwind-dev": { + "versions": { + "1.2.1-r1": 1509493800 + }, + "origin": "libunwind", + "dependencies": [ + "libexecinfo-dev", + "libunwind=1.2.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:libunwind-coredump=1.21", + "pc:libunwind-generic=1.21", + "pc:libunwind-ptrace=1.21", + "pc:libunwind-setjmp=1.21", + "pc:libunwind=1.21" + ] + }, + "py-gnome": { + "versions": { + "2.28.1-r5": 1510933057 + }, + "origin": "py-gnome", + "dependencies": [ + "py-gnome-bonobo", + "py-gnome-gconf", + "py-gnome-libgnome", + "py-gnome-gnomevfs" + ] + }, + "hdparm": { + "versions": { + "9.52-r0": 1509479800 + }, + "origin": "hdparm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:hdparm", + "cmd:idectl", + "cmd:ultrabayd" + ] + }, + "kmod-dev": { + "versions": { + "24-r0": 1509462316 + }, + "origin": "kmod", + "dependencies": [ + "kmod=24-r0", + "pkgconfig" + ], + "provides": [ + "pc:libkmod=24" + ] + }, + "x264-dev": { + "versions": { + "20170930-r0": 1509473602 + }, + "origin": "x264", + "dependencies": [ + "pkgconfig", + "x264-libs=20170930-r0" + ], + "provides": [ + "pc:x264=0.148.x" + ] + }, + "perl-tree-simple": { + "versions": { + "1.18-r1": 1509494482 + }, + "origin": "perl-tree-simple", + "dependencies": [ + "perl", + "perl-test-exception" + ] + }, + "taglib": { + "versions": { + "1.11.1-r0": 1509489613 + }, + "origin": "taglib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libtag.so.1=1.17.0", + "so:libtag_c.so.0=0.0.0" + ] + }, + "kamailio-uuid": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ] + }, + "libgee": { + "versions": { + "0.20.0-r0": 1509468648 + }, + "origin": "libgee", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libgee-0.8.so.2=2.6.0" + ] + }, + "py2-future": { + "versions": { + "0.15.2-r4": 1509491797 + }, + "origin": "py-future", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:futurize", + "cmd:pasteurize" + ] + }, + "daq-dev": { + "versions": { + "2.0.6-r2": 1510839244 + }, + "origin": "daq", + "dependencies": [ + "daq=2.0.6-r2" + ], + "provides": [ + "cmd:daq-modules-config" + ] + }, + "appstream-glib-doc": { + "versions": { + "0.6.3-r0": 1510067861 + }, + "origin": "appstream-glib" + }, + "py2-urllib3": { + "versions": { + "1.22-r0": 1509483031 + }, + "origin": "py-urllib3", + "dependencies": [ + "python2" + ] + }, + "perl-ipc-system-simple-doc": { + "versions": { + "1.25-r0": 1509495315 + }, + "origin": "perl-ipc-system-simple" + }, + "samba-winbind-krb5-locator": { + "versions": { + "4.7.6-r3": 1555491787 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libwbclient.so.0" + ], + "provides": [ + "so:winbind_krb5_locator.so=0" + ] + }, + "glade": { + "versions": { + "3.20.1-r0": 1510073638 + }, + "origin": "glade", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgladeui-2.so.6=6.3.1", + "cmd:glade", + "cmd:glade-previewer" + ] + }, + "perl-data-page-doc": { + "versions": { + "2.02-r1": 1509493849 + }, + "origin": "perl-data-page" + }, + "syslinux-doc": { + "versions": { + "6.04_pre1-r1": 1509482641 + }, + "origin": "syslinux" + }, + "flashrom": { + "versions": { + "0.9.9-r2": 1509482145 + }, + "origin": "flashrom", + "dependencies": [ + "dmidecode", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libpci.so.3", + "so:libusb-0.1.so.4", + "so:libusb-1.0.so.0" + ], + "provides": [ + "cmd:flashrom" + ] + }, + "arpon": { + "versions": { + "3.0-r0": 1509495305 + }, + "origin": "arpon", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdnet.so.1", + "so:libnet.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:arpon" + ] + }, + "xorg-server-xwayland": { + "versions": { + "1.19.5-r1": 1540838460 + }, + "origin": "xorg-server", + "dependencies": [ + "font-misc-misc", + "font-cursor-misc", + "xkeyboard-config", + "xkbcomp", + "xinit", + "so:libGL.so.1", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.0.0", + "so:libdrm.so.2", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libpixman-1.so.0", + "so:libwayland-client.so.0", + "so:libxshmfence.so.1" + ], + "provides": [ + "cmd:Xwayland" + ] + }, + "py-simplejson": { + "versions": { + "3.8.2-r2": 1509494754 + }, + "origin": "py-simplejson" + }, + "apache2-error": { + "versions": { + "2.4.39-r0": 1554306881, + "2.4.41-r0": 1566292327 + }, + "origin": "apache2" + }, + "jwhois": { + "versions": { + "4.0-r4": 1509493815 + }, + "origin": "jwhois", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4" + ], + "provides": [ + "cmd:jwhois" + ] + }, + "liblogging": { + "versions": { + "1.0.6-r0": 1509517792 + }, + "origin": "liblogging", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblogging-stdlog.so.0=0.1.0", + "cmd:stdlogctl" + ] + }, + "skalibs-dev": { + "versions": { + "2.6.1.0-r0": 1510364877 + }, + "origin": "skalibs", + "dependencies": [ + "skalibs=2.6.1.0-r0" + ] + }, + "faac-dev": { + "versions": { + "1.28-r11": 1509470017 + }, + "origin": "faac", + "dependencies": [ + "faac=1.28-r11" + ] + }, + "ruby-xmlrpc": { + "versions": { + "2.4.6-r0": 1557166823 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ] + }, + "sendpage-doc": { + "versions": { + "1.0.3-r5": 1509489416 + }, + "origin": "sendpage" + }, + "py3-lxml": { + "versions": { + "4.1.1-r0": 1510088209 + }, + "origin": "py-lxml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libpython3.6m.so.1.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ] + }, + "font-sun-misc": { + "versions": { + "1.0.3-r0": 1509494263 + }, + "origin": "font-sun-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "qemu-alpha": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-alpha" + ] + }, + "gst-plugins-bad0.10-dev": { + "versions": { + "0.10.23-r7": 1510287373 + }, + "origin": "gst-plugins-bad0.10", + "dependencies": [ + "gst-plugins-bad0.10=0.10.23-r7", + "pc:gstreamer-0.10", + "pc:gstreamer-base-0.10", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-basevideo-0.10=0.10.23", + "pc:gstreamer-codecparsers-0.10=0.10.23", + "pc:gstreamer-plugins-bad-0.10=0.10.23" + ] + }, + "p11-kit-dev": { + "versions": { + "0.23.2-r2": 1509465178 + }, + "origin": "p11-kit", + "dependencies": [ + "p11-kit=0.23.2-r2", + "pkgconfig" + ], + "provides": [ + "pc:p11-kit-1=0.23.2" + ] + }, + "cdparanoia": { + "versions": { + "10.2-r7": 1509471030 + }, + "origin": "cdparanoia", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcdda_interface.so.0", + "so:libcdda_paranoia.so.0" + ], + "provides": [ + "cmd:cdparanoia" + ] + }, + "java-gcj-compat": { + "versions": { + "6.4.0-r8": 1509490656 + }, + "origin": "java-gcj-compat", + "dependencies": [ + "fastjar", + "gcc-java=6.4.0-r5", + "java-common", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgcj.so.17" + ] + }, + "perl-file-tail": { + "versions": { + "1.3-r1": 1511889532 + }, + "origin": "perl-file-tail" + }, + "ruby-did_you_mean": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ] + }, + "openldap-back-mdb": { + "versions": { + "2.4.45-r3": 1510258133, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "slim-themes": { + "versions": { + "1.2.3-r2": 1510067550 + }, + "origin": "slim-themes", + "dependencies": [ + "slim" + ] + }, + "bitlbee-doc": { + "versions": { + "3.5.1-r1": 1510259355 + }, + "origin": "bitlbee" + }, + "openldap-back-hdb": { + "versions": { + "2.4.45-r3": 1510258132, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "speex-doc": { + "versions": { + "1.2.0-r0": 1509473246 + }, + "origin": "speex" + }, + "perl-test-output": { + "versions": { + "1.031-r0": 1509485674 + }, + "origin": "perl-test-output", + "dependencies": [ + "perl-capture-tiny", + "perl-sub-exporter", + "perl-test-simple" + ] + }, + "py-gnome-dev": { + "versions": { + "2.28.1-r5": 1510933054 + }, + "origin": "py-gnome", + "dependencies": [ + "gtk+2.0-dev", + "libgnome-dev", + "py-gobject-dev", + "py-gtk-dev", + "python2-dev", + "pkgconfig" + ], + "provides": [ + "pc:gnome-python-2.0=2.28.1" + ] + }, + "py3-uritemplate.py": { + "versions": { + "3.0.0-r0": 1509486306 + }, + "origin": "py-uritemplate", + "dependencies": [ + "python3" + ] + }, + "ircii": { + "versions": { + "20111115-r4": 1509492742 + }, + "origin": "ircii", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:irc", + "cmd:irc-20111115", + "cmd:ircbug", + "cmd:ircflush" + ] + }, + "py-ecdsa": { + "versions": { + "0.13-r5": 1509494957 + }, + "origin": "py-ecdsa" + }, + "iso-codes-lang": { + "versions": { + "3.75-r0": 1509466020 + }, + "origin": "iso-codes" + }, + "perl-css-squish": { + "versions": { + "0.10-r0": 1509470430 + }, + "origin": "perl-css-squish", + "dependencies": [ + "perl", + "perl-uri", + "perl-test-longstring" + ] + }, + "hunspell": { + "versions": { + "1.6.2-r1": 1509477668 + }, + "origin": "hunspell", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libreadline.so.7", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libhunspell-1.6.so.0=0.0.1", + "cmd:affixcompress", + "cmd:analyze", + "cmd:chmorph", + "cmd:hunspell", + "cmd:hunzip", + "cmd:hzip", + "cmd:ispellaff2myspell", + "cmd:makealias", + "cmd:munch", + "cmd:unmunch", + "cmd:wordforms", + "cmd:wordlist2hunspell" + ] + }, + "libxxf86dga-dev": { + "versions": { + "1.1.4-r1": 1509473787 + }, + "origin": "libxxf86dga", + "dependencies": [ + "libxxf86dga=1.1.4-r1", + "pc:x11", + "pc:xext", + "pc:xf86dgaproto", + "pkgconfig" + ], + "provides": [ + "pc:xxf86dga=1.1.4" + ] + }, + "py-requests-oauthlib": { + "versions": { + "0.8.0-r1": 1509552764 + }, + "origin": "py-requests-oauthlib", + "dependencies": [ + "py-oauthlib", + "py-requests" + ] + }, + "ciwiki": { + "versions": { + "2.0.5-r1": 1509493443 + }, + "origin": "ciwiki", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ciwiki" + ] + }, + "libpng-doc": { + "versions": { + "1.6.37-r0": 1557132257 + }, + "origin": "libpng" + }, + "clamav-milter": { + "versions": { + "0.100.3-r0": 1555508238 + }, + "origin": "clamav", + "dependencies": [ + "clamav-scanner", + "clamav-daemon", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libmilter.so.1.0.2", + "so:libssl.so.44" + ], + "provides": [ + "cmd:clamav-milter" + ] + }, + "acl-doc": { + "versions": { + "2.2.52-r3": 1509459587 + }, + "origin": "acl" + }, + "perl-test-requires-doc": { + "versions": { + "0.10-r0": 1509470570 + }, + "origin": "perl-test-requires" + }, + "squid-lang-af": { + "versions": { + "3.5.27-r0": 1519824030, + "3.5.27-r1": 1562865666 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-class-container": { + "versions": { + "0.12-r1": 1509470591 + }, + "origin": "perl-class-container", + "dependencies": [ + "perl-params-validate" + ] + }, + "libogg": { + "versions": { + "1.3.3-r1": 1511046403 + }, + "origin": "libogg", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libogg.so.0=0.8.3" + ] + }, + "libmad": { + "versions": { + "0.15.1b-r7": 1509473573, + "0.15.1b-r8": 1565784570 + }, + "origin": "libmad", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmad.so.0=0.2.1" + ] + }, + "py2-unidecode": { + "versions": { + "0.04.21-r0": 1509493255 + }, + "origin": "py-unidecode", + "dependencies": [ + "py3-unidecode", + "python2" + ], + "provides": [ + "cmd:unidecode-2" + ] + }, + "nagios-plugins-cluster": { + "versions": { + "2.2.1-r3": 1510288494 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "ristretto-lang": { + "versions": { + "0.8.2-r0": 1510072649 + }, + "origin": "ristretto", + "dependencies": [ + "desktop-file-utils", + "hicolor-icon-theme" + ] + }, + "perl-test-mockobject-doc": { + "versions": { + "1.20161202-r0": 1509489410 + }, + "origin": "perl-test-mockobject" + }, + "uwsgi-http": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-package-stash": { + "versions": { + "0.37-r0": 1509474047 + }, + "origin": "perl-package-stash", + "dependencies": [ + "perl-dist-checkconflicts", + "perl-package-stash-xs", + "perl-module-implementation" + ], + "provides": [ + "cmd:package-stash-conflicts" + ] + }, + "uwsgi-router_radius": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libidl": { + "versions": { + "0.8.14-r2": 1510928272 + }, + "origin": "libidl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "so:libIDL-2.so.0=0.0.0", + "cmd:libIDL-config-2" + ] + }, + "abiword-plugin-wikipedia": { + "versions": { + "3.0.2-r1": 1510073369 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "openvswitch-bash-completion": { + "versions": { + "2.8.1-r2": 1512030506 + }, + "origin": "openvswitch" + }, + "gvfs-dev": { + "versions": { + "1.34.1-r0": 1511430257, + "1.34.1-r1": 1563787223 + }, + "origin": "gvfs" + }, + "libxfixes-dev": { + "versions": { + "5.0.3-r1": 1509464684 + }, + "origin": "libxfixes", + "dependencies": [ + "libxfixes=5.0.3-r1", + "pc:fixesproto>=5.0", + "pc:x11", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xfixes=5.0.3" + ] + }, + "libressl-dbg": { + "versions": { + "2.6.5-r0": 1529043883 + }, + "origin": "libressl" + }, + "libnl3": { + "versions": { + "3.2.28-r1": 1509475887 + }, + "origin": "libnl3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnl-3.so.200=200.23.0", + "so:libnl-genl-3.so.200=200.23.0", + "so:libnl-idiag-3.so.200=200.23.0", + "so:libnl-nf-3.so.200=200.23.0", + "so:libnl-route-3.so.200=200.23.0", + "so:libnl-xfrm-3.so.200=200.23.0" + ] + }, + "libsmbclient": { + "versions": { + "4.7.6-r3": 1555491786 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcli-smb-common-samba4.so", + "so:libdcerpc-samba-samba4.so", + "so:libgse-samba4.so", + "so:liblibcli-lsa3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-security-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsecrets3-samba4.so", + "so:libsmbconf.so.0", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-cmdline-samba4.so" + ], + "provides": [ + "so:libsmbclient.so.0=0.2.3" + ] + }, + "gtest-dev": { + "versions": { + "1.8.0-r1": 1509475785 + }, + "origin": "gtest", + "dependencies": [ + "python2", + "cmake" + ] + }, + "fish-doc": { + "versions": { + "2.6.0-r2": 1511486201 + }, + "origin": "fish" + }, + "libxtst": { + "versions": { + "1.2.3-r1": 1509466039 + }, + "origin": "libxtst", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXtst.so.6=6.1.0" + ] + }, + "uwsgi-echo": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "lucene++": { + "versions": { + "3.0.7-r5": 1509496354 + }, + "origin": "lucene++", + "dependencies": [ + "so:libboost_filesystem-mt.so.1.62.0", + "so:libboost_iostreams-mt.so.1.62.0", + "so:libboost_regex-mt.so.1.62.0", + "so:libboost_system-mt.so.1.62.0", + "so:libboost_thread-mt.so.1.62.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:liblucene++-contrib.so.0=3.0.7", + "so:liblucene++.so.0=3.0.7" + ] + }, + "gvpe": { + "versions": { + "2.25-r4": 1510259350 + }, + "origin": "gvpe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libgmp.so.10", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:gvpe", + "cmd:gvpectrl" + ] + }, + "augeas-doc": { + "versions": { + "1.9.0-r3": 1511899746 + }, + "origin": "augeas" + }, + "lua5.3-hashids": { + "versions": { + "1.0.6-r1": 1509480766 + }, + "origin": "lua-hashids", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-text-autoformat": { + "versions": { + "1.669004-r0": 1509475845 + }, + "origin": "perl-text-autoformat", + "dependencies": [ + "perl", + "perl-text-reform" + ] + }, + "lua-stdlib": { + "versions": { + "41.2.0-r0": 1509495249 + }, + "origin": "lua-stdlib" + }, + "postgresql-contrib": { + "versions": { + "10.7-r0": 1554274195, + "10.8-r0": 1557668011, + "10.9-r0": 1562225611, + "10.10-r0": 1565683434 + }, + "origin": "postgresql", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpq.so.5", + "so:libssl.so.44", + "so:libuuid.so.1", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:oid2name", + "cmd:pg_standby", + "cmd:vacuumlo" + ] + }, + "ldns": { + "versions": { + "1.6.17-r6": 1510258937 + }, + "origin": "ldns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ], + "provides": [ + "so:libldns.so.1=1.6.17" + ] + }, + "nginx-mod-http-perl": { + "versions": { + "1.12.2-r4": 1542814447 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "privoxy": { + "versions": { + "3.0.26-r0": 1509495185 + }, + "origin": "privoxy", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1", + "so:libpcreposix.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:privoxy" + ] + }, + "openssl": { + "versions": { + "1.0.2r-r0": 1552814669, + "1.0.2t-r0": 1568300415 + }, + "origin": "openssl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.0.0", + "so:libssl.so.1.0.0" + ], + "provides": [ + "cmd:openssl" + ] + }, + "perl-date-calc": { + "versions": { + "6.4-r1": 1509485626 + }, + "origin": "perl-date-calc", + "dependencies": [ + "perl" + ] + }, + "perl-control-x10": { + "versions": { + "2.09-r1": 1509491054 + }, + "origin": "perl-control-x10", + "dependencies": [ + "perl" + ] + }, + "ortp": { + "versions": { + "0.25.0-r0": 1509479730 + }, + "origin": "ortp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libortp.so.10=10.0.0" + ] + }, + "gst-plugins-bad": { + "versions": { + "1.12.3-r2": 1512039894 + }, + "origin": "gst-plugins-bad", + "dependencies": [ + "so:libEGL.so.1", + "so:libGL.so.1", + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcrypto.so.42", + "so:libcurl.so.4", + "so:libdc1394.so.22", + "so:libdirectfb-1.7.so.7", + "so:libdrm.so.2", + "so:libfaac.so.0", + "so:libfaad.so.2", + "so:libflite.so.1", + "so:libflite_cmu_us_kal.so.1", + "so:libflite_cmulex.so.1", + "so:libflite_usenglish.so.1", + "so:libgcc_s.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsm.so.1", + "so:libgstallocators-1.0.so.0", + "so:libgstapp-1.0.so.0", + "so:libgstaudio-1.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstcontroller-1.0.so.0", + "so:libgstfft-1.0.so.0", + "so:libgstnet-1.0.so.0", + "so:libgstpbutils-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgstrtp-1.0.so.0", + "so:libgstsdp-1.0.so.0", + "so:libgsttag-1.0.so.0", + "so:libgstvideo-1.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:libmms.so.0", + "so:libmodplug.so.1", + "so:libneon.so.27", + "so:liborc-0.4.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpng16.so.16", + "so:librsvg-2.so.2", + "so:libspandsp.so.2", + "so:libssl.so.44", + "so:libstdc++.so.6", + "so:libvdpau.so.1", + "so:libwayland-client.so.0", + "so:libwayland-egl.so.1", + "so:libwebp.so.7", + "so:libx265.so.130", + "so:libxcb.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgstadaptivedemux-1.0.so.0=0.1203.0", + "so:libgstbadallocators-1.0.so.0=0.1203.0", + "so:libgstbadaudio-1.0.so.0=0.1203.0", + "so:libgstbadbase-1.0.so.0=0.1203.0", + "so:libgstbadvideo-1.0.so.0=0.1203.0", + "so:libgstbasecamerabinsrc-1.0.so.0=0.1203.0", + "so:libgstcodecparsers-1.0.so.0=0.1203.0", + "so:libgstgl-1.0.so.0=0.1203.0", + "so:libgstinsertbin-1.0.so.0=0.1203.0", + "so:libgstmpegts-1.0.so.0=0.1203.0", + "so:libgstphotography-1.0.so.0=0.1203.0", + "so:libgstplayer-1.0.so.0=0.1203.0", + "so:libgsturidownloader-1.0.so.0=0.1203.0" + ] + }, + "xen": { + "versions": { + "4.9.4-r0": 1551280496, + "4.9.4-r1": 1558103152 + }, + "origin": "xen", + "dependencies": [ + "bash", + "iproute2", + "logrotate", + "syslinux", + "so:libaio.so.1", + "so:libblktapctl.so.1.0", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libfsimage.so.1.0", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libpci.so.3", + "so:libpixman-1.so.0", + "so:libpython2.7.so.1.0", + "so:libspice-server.so.1", + "so:libvhd.so.1.0", + "so:libxenctrl.so.4.9", + "so:libxenevtchn.so.1", + "so:libxenforeignmemory.so.1", + "so:libxengnttab.so.1", + "so:libxenguest.so.4.9", + "so:libxenlight.so.4.9", + "so:libxenstat.so.0", + "so:libxenstore.so.3.0", + "so:libxentoollog.so.1", + "so:libxlutil.so.4.9", + "so:libyajl.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:flask-get-bool", + "cmd:flask-getenforce", + "cmd:flask-label-pci", + "cmd:flask-loadpolicy", + "cmd:flask-set-bool", + "cmd:flask-setenforce", + "cmd:gdbsx", + "cmd:img2qcow", + "cmd:kdd", + "cmd:lock-util", + "cmd:qcow-create", + "cmd:qcow2raw", + "cmd:qemu-img-xen", + "cmd:qemu-nbd-xen", + "cmd:tap-ctl", + "cmd:tapdisk-client", + "cmd:tapdisk-diff", + "cmd:tapdisk-stream", + "cmd:tapdisk2", + "cmd:td-util", + "cmd:vhd-update", + "cmd:vhd-util", + "cmd:xen-bugtool", + "cmd:xen-cpuid", + "cmd:xen-detect", + "cmd:xen-hptool", + "cmd:xen-hvmcrash", + "cmd:xen-hvmctx", + "cmd:xen-livepatch", + "cmd:xen-lowmemd", + "cmd:xen-mfndump", + "cmd:xen-ringwatch", + "cmd:xen-tmem-list-parse", + "cmd:xenalyze", + "cmd:xenbaked", + "cmd:xencons", + "cmd:xenconsoled", + "cmd:xencov", + "cmd:xencov_split", + "cmd:xenlockprof", + "cmd:xenmon.py", + "cmd:xenperf", + "cmd:xenpm", + "cmd:xenpmd", + "cmd:xenstore", + "cmd:xenstore-chmod", + "cmd:xenstore-control", + "cmd:xenstore-exists", + "cmd:xenstore-list", + "cmd:xenstore-ls", + "cmd:xenstore-read", + "cmd:xenstore-rm", + "cmd:xenstore-watch", + "cmd:xenstore-write", + "cmd:xenstored", + "cmd:xentop", + "cmd:xentrace", + "cmd:xentrace_format", + "cmd:xentrace_setmask", + "cmd:xentrace_setsize", + "cmd:xenwatchdogd", + "cmd:xl" + ] + }, + "fcgi-dev": { + "versions": { + "2.4.0-r8": 1510330997 + }, + "origin": "fcgi", + "dependencies": [ + "fcgi++=2.4.0-r8", + "fcgi=2.4.0-r8" + ] + }, + "libotr3-doc": { + "versions": { + "3.2.1-r4": 1509481188 + }, + "origin": "libotr3", + "dependencies": [ + "libotr3" + ] + }, + "abuild-rootbld": { + "versions": { + "3.1.0-r4": 1527832619, + "3.1.0-r5": 1572356050 + }, + "origin": "abuild", + "dependencies": [ + "abuild", + "bubblewrap", + "gettext", + "git" + ] + }, + "librsync": { + "versions": { + "2.0.0-r1": 1509472925 + }, + "origin": "librsync", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "so:librsync.so.2=2.0.0", + "cmd:rdiff" + ] + }, + "perl-ldap-doc": { + "versions": { + "0.65-r1": 1510564476 + }, + "origin": "perl-ldap" + }, + "perl-dbix-searchbuilder": { + "versions": { + "1.67-r1": 1510588867 + }, + "origin": "perl-dbix-searchbuilder", + "dependencies": [ + "perl-class-returnvalue", + "perl-dbi", + "perl-cache-simple-timedexpiry", + "perl-class-accessor", + "perl-clone", + "perl-want", + "perl-dbix-dbschema" + ] + }, + "udisks2-lang": { + "versions": { + "2.6.5-r0": 1510075151 + }, + "origin": "udisks2" + }, + "adwaita-icon-theme-dev": { + "versions": { + "3.26.0-r0": 1510076148 + }, + "origin": "adwaita-icon-theme", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:adwaita-icon-theme=3.26.0" + ] + }, + "minicom-doc": { + "versions": { + "2.7.1-r0": 1509485710 + }, + "origin": "minicom" + }, + "jbig2dec-doc": { + "versions": { + "0.14-r0": 1509469939 + }, + "origin": "jbig2dec" + }, + "perl-net-dns-doc": { + "versions": { + "1.13-r0": 1509469031 + }, + "origin": "perl-net-dns" + }, + "xrefresh": { + "versions": { + "1.0.5-r0": 1509494699 + }, + "origin": "xrefresh", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xrefresh" + ] + }, + "libxfce4ui": { + "versions": { + "4.12.1-r3": 1510067994 + }, + "origin": "libxfce4ui", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libstartup-notification-1.so.0", + "so:libxfce4util.so.7", + "so:libxfconf-0.so.2" + ], + "provides": [ + "so:libxfce4kbd-private-2.so.0=0.0.0", + "so:libxfce4ui-1.so.0=0.0.0", + "cmd:xfce4-about" + ] + }, + "mutt": { + "versions": { + "1.10.1-r0": 1532446898 + }, + "origin": "mutt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgdbm.so.4", + "so:libgpgme.so.11", + "so:libidn.so.11", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libsasl2.so.3", + "so:libssl.so.44" + ], + "provides": [ + "cmd:mutt", + "cmd:pgpewrap", + "cmd:pgpring", + "cmd:smime_keys" + ] + }, + "zsh-vcs": { + "versions": { + "5.4.2-r1": 1522503663 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "xrdp": { + "versions": { + "0.9.2-r3": 1510260212 + }, + "origin": "xrdp", + "dependencies": [ + "so:libX11.so.6", + "so:libXfixes.so.3", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libfuse.so.2", + "so:libssl.so.44", + "so:libturbojpeg.so.0" + ], + "provides": [ + "so:libcommon.so.0=0.0.0", + "so:libmc.so=0", + "so:libpainter.so.0=0.0.0", + "so:librfxencode.so.0=0.0.0", + "so:libscp.so.0=0.0.0", + "so:libvnc.so=0", + "so:libxrdp.so.0=0.0.0", + "so:libxrdpapi.so.0=0.0.0", + "so:libxup.so=0", + "cmd:xrdp", + "cmd:xrdp-chansrv", + "cmd:xrdp-dis", + "cmd:xrdp-genkeymap", + "cmd:xrdp-keygen", + "cmd:xrdp-sesadmin", + "cmd:xrdp-sesman", + "cmd:xrdp-sesrun" + ] + }, + "libgssglue-dev": { + "versions": { + "0.4-r0": 1509479796 + }, + "origin": "libgssglue", + "dependencies": [ + "libgssglue=0.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:libgssglue=0.4" + ] + }, + "sircbot": { + "versions": { + "0.4-r1": 1509492362 + }, + "origin": "sircbot", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sircbot", + "cmd:sircbot-send" + ] + }, + "db": { + "versions": { + "5.3.28-r0": 1509469316 + }, + "origin": "db", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdb-5.3.so=0" + ] + }, + "gzip-doc": { + "versions": { + "1.8-r0": 1509456993 + }, + "origin": "gzip" + }, + "imagemagick-libs": { + "versions": { + "7.0.7.11-r1": 1510748307 + }, + "origin": "imagemagick", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libgcc_s.so.1", + "so:libgomp.so.1", + "so:liblcms2.so.2", + "so:libltdl.so.7", + "so:libz.so.1" + ], + "provides": [ + "so:libMagickCore-7.Q16HDRI.so.4=4.0.0", + "so:libMagickWand-7.Q16HDRI.so.4=4.0.0" + ] + }, + "py-setuptools": { + "versions": { + "33.1.1-r1": 1509464242 + }, + "origin": "py-setuptools", + "dependencies": [ + "python2" + ], + "provides": [ + "py2-setuptools=33.1.1-r1", + "cmd:easy_install-2.7" + ] + }, + "dropbear-dbclient": { + "versions": { + "2018.76-r2": 1537439117 + }, + "origin": "dropbear", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:dbclient" + ] + }, + "gtkman-lang": { + "versions": { + "1.0.1-r0": 1510073898 + }, + "origin": "gtkman", + "dependencies": [ + "py-gtk" + ] + }, + "unifont-dev": { + "versions": { + "9.0.06-r0": 1509491372 + }, + "origin": "unifont" + }, + "pcre2-tools": { + "versions": { + "10.30-r0": 1509461774 + }, + "origin": "pcre2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libedit.so.0", + "so:libpcre2-16.so.0", + "so:libpcre2-32.so.0", + "so:libpcre2-8.so.0", + "so:libpcre2-posix.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:pcre2grep", + "cmd:pcre2test" + ] + }, + "xorg-server": { + "versions": { + "1.19.5-r1": 1540838460 + }, + "origin": "xorg-server", + "dependencies": [ + "font-misc-misc", + "font-cursor-misc", + "xkeyboard-config", + "xkbcomp", + "xinit", + "so:libGL.so.1", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.0.0", + "so:libdrm.so.2", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libpciaccess.so.0", + "so:libpixman-1.so.0", + "so:libudev.so.1", + "so:libxshmfence.so.1" + ], + "provides": [ + "cmd:X", + "cmd:Xorg", + "cmd:cvt", + "cmd:gtf" + ] + }, + "tumbler": { + "versions": { + "0.2.0-r0": 1510075181 + }, + "origin": "tumbler", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcurl.so.4", + "so:libfreetype.so.6", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libpoppler-glib.so.8" + ], + "provides": [ + "so:libtumbler-1.so.0=0.0.0" + ] + }, + "perl-file-which-doc": { + "versions": { + "1.22-r0": 1509495219 + }, + "origin": "perl-file-which" + }, + "perl-class-singleton-doc": { + "versions": { + "1.5-r0": 1509485594 + }, + "origin": "perl-class-singleton" + }, + "py-jinja2": { + "versions": { + "2.9.6-r0": 1509476516 + }, + "origin": "py-jinja2" + }, + "perl-image-exiftool-doc": { + "versions": { + "10.55-r0": 1509495133 + }, + "origin": "perl-image-exiftool" + }, + "perl-test-number-delta-doc": { + "versions": { + "1.06-r0": 1509481691 + }, + "origin": "perl-test-number-delta" + }, + "gtkmm": { + "versions": { + "2.24.5-r0": 1510073814 + }, + "origin": "gtkmm", + "dependencies": [ + "so:libatkmm-1.6.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairomm-1.0.so.1", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgiomm-2.4.so.1", + "so:libglib-2.0.so.0", + "so:libglibmm-2.4.so.1", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpangomm-1.4.so.1", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgdkmm-2.4.so.1=1.1.0", + "so:libgtkmm-2.4.so.1=1.1.0" + ] + }, + "pjproject-dev": { + "versions": { + "2.5.5-r3": 1510258880 + }, + "origin": "pjproject", + "dependencies": [ + "libressl-dev", + "alsa-lib-dev", + "gsm-dev", + "speex-dev", + "speexdsp-dev", + "portaudio-dev", + "libsrtp-dev", + "libsamplerate-dev", + "pjproject=2.5.5-r3", + "pkgconfig" + ], + "provides": [ + "pc:libpjproject=2.5.5" + ] + }, + "git-subtree": { + "versions": { + "2.15.3-r0": 1540401294, + "2.15.4-r0": 1576015194 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0" + ] + }, + "taskd": { + "versions": { + "1.1.0-r4": 1509491729 + }, + "origin": "taskd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:taskd", + "cmd:taskdctl" + ] + }, + "mosh-client": { + "versions": { + "1.3.2-r3": 1510846214 + }, + "origin": "mosh", + "dependencies": [ + "openssh-client", + "perl-io-tty", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libprotobuf.so.14", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:mosh-client" + ] + }, + "sems-conf_auth": { + "versions": { + "1.6.0-r6": 1510260895 + }, + "origin": "sems", + "dependencies": [ + "sems", + "sems-ivr" + ] + }, + "gnumeric-doc": { + "versions": { + "1.12.36-r0": 1511455615 + }, + "origin": "gnumeric" + }, + "cpufreqd": { + "versions": { + "2.4.2-r4": 1509492587 + }, + "origin": "cpufreqd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcpufreq.so.0", + "so:libsysfs.so.2" + ], + "provides": [ + "so:cpufreqd_acpi.so=0", + "so:cpufreqd_apm.so=0", + "so:cpufreqd_cpu.so=0", + "so:cpufreqd_exec.so=0", + "so:cpufreqd_governor_parameters.so=0", + "so:cpufreqd_nforce2.so=0", + "so:cpufreqd_pmu.so=0", + "so:cpufreqd_programs.so=0", + "so:cpufreqd_tau.so=0", + "cmd:cpufreqd", + "cmd:cpufreqd-get", + "cmd:cpufreqd-set" + ] + }, + "freeswitch-sounds-pt-BR-karina-8000": { + "versions": { + "1.0.51-r1": 1509495978 + }, + "origin": "freeswitch-sounds-pt-BR-karina-8000" + }, + "mkfontscale": { + "versions": { + "1.1.2-r1": 1509469866 + }, + "origin": "mkfontscale", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libfontenc.so.1", + "so:libfreetype.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:mkfontscale" + ] + }, + "libunwind": { + "versions": { + "1.2.1-r1": 1509493801 + }, + "origin": "libunwind", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libunwind-coredump.so.0=0.0.0", + "so:libunwind-ptrace.so.0=0.0.0", + "so:libunwind-setjmp.so.0=0.0.0", + "so:libunwind-x86_64.so.8=8.0.1", + "so:libunwind.so.8=8.0.1" + ] + }, + "perl-protocol-websocket": { + "versions": { + "0.20-r0": 1509474791 + }, + "origin": "perl-protocol-websocket", + "dependencies": [ + "perl" + ] + }, + "gst-plugins-base-doc": { + "versions": { + "1.12.3-r0": 1510070597 + }, + "origin": "gst-plugins-base" + }, + "xmlrpc-c-abyss": { + "versions": { + "1.39.11-r0": 1509482041 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxmlrpc.so.3", + "so:libxmlrpc_server.so.3", + "so:libxmlrpc_util.so.3" + ], + "provides": [ + "so:libxmlrpc_abyss.so.3=3.39", + "so:libxmlrpc_server_abyss.so.3=3.39" + ] + }, + "gsm-doc": { + "versions": { + "1.0.16-r0": 1509473211 + }, + "origin": "gsm" + }, + "intltool-doc": { + "versions": { + "0.51.0-r4": 1509464451 + }, + "origin": "intltool" + }, + "kamailio-http_async": { + "versions": { + "5.0.7-r0": 1532960877 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libevent-2.1.so.6" + ] + }, + "shorewall6": { + "versions": { + "5.1.8-r0": 1509489476 + }, + "origin": "shorewall6", + "dependencies": [ + "shorewall-core", + "perl", + "ip6tables", + "iproute2" + ] + }, + "popt-dev": { + "versions": { + "1.16-r7": 1509462411 + }, + "origin": "popt", + "dependencies": [ + "pkgconfig", + "popt=1.16-r7" + ], + "provides": [ + "pc:popt=1.16" + ] + }, + "perl-class-tiny-doc": { + "versions": { + "1.006-r0": 1510845710 + }, + "origin": "perl-class-tiny" + }, + "farstream0.1-dev": { + "versions": { + "0.1.2-r2": 1510933111 + }, + "origin": "farstream0.1", + "dependencies": [ + "libnice-dev", + "gstreamer0.10-dev", + "gst-plugins-base0.10-dev", + "farstream0.1=0.1.2-r2", + "pc:gstreamer-0.10", + "pc:gstreamer-base-0.10", + "pkgconfig" + ], + "provides": [ + "pc:farstream-0.1=0.1.2" + ] + }, + "perl-date-format-doc": { + "versions": { + "2.30-r0": 1510564547 + }, + "origin": "perl-date-format" + }, + "perl-exception-class": { + "versions": { + "1.43-r0": 1509470598 + }, + "origin": "perl-exception-class", + "dependencies": [ + "perl-devel-stacktrace", + "perl-class-data-inheritable" + ] + }, + "git-perl": { + "versions": { + "2.15.3-r0": 1540401295, + "2.15.4-r0": 1576015196 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0", + "perl-git=2.15.4-r0", + "perl" + ] + }, + "busybox-static": { + "versions": { + "1.27.2-r11": 1528276162 + }, + "origin": "busybox", + "provides": [ + "cmd:busybox.static" + ] + }, + "lua5.3-dev": { + "versions": { + "5.3.5-r2": 1557163184 + }, + "origin": "lua5.3", + "dependencies": [ + "lua5.3", + "lua5.3-libs=5.3.5-r2", + "pkgconfig" + ], + "provides": [ + "pc:lua5.3=5.3.5" + ] + }, + "libzmq": { + "versions": { + "4.2.5-r0": 1549279390, + "4.2.5-r1": 1563908217 + }, + "origin": "zeromq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libsodium.so.23", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libzmq.so.5=5.1.5" + ] + }, + "perl-switch": { + "versions": { + "2.17-r0": 1509475524 + }, + "origin": "perl-switch", + "dependencies": [ + "perl" + ] + }, + "iwlwifi-5150-ucode-doc": { + "versions": { + "8.24.2.2-r1": 1509475697 + }, + "origin": "iwlwifi-5150-ucode" + }, + "sems-ivr": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "python2", + "so:libc.musl-x86_64.so.1", + "so:libflite.so.1", + "so:libflite_cmu_us_kal.so.1", + "so:libflite_cmulex.so.1", + "so:libflite_usenglish.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "xfce4-panel-doc": { + "versions": { + "4.12.1-r1": 1510068172 + }, + "origin": "xfce4-panel" + }, + "lxc-download": { + "versions": { + "2.1.1-r3": 1533578833 + }, + "origin": "lxc", + "dependencies": [ + "lxc", + "gnupg", + "tar", + "xz", + "wget" + ] + }, + "xf86-video-chips-doc": { + "versions": { + "1.2.7-r0": 1510074063 + }, + "origin": "xf86-video-chips" + }, + "loudmouth-doc": { + "versions": { + "1.5.3-r0": 1509490879 + }, + "origin": "loudmouth" + }, + "libcap-ng": { + "versions": { + "0.7.8-r1": 1509461312 + }, + "origin": "libcap-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcap-ng.so.0=0.0.0" + ] + }, + "lua-dev": { + "versions": { + "5.1.5-r4": 1509471873 + }, + "origin": "lua", + "dependencies": [ + "lua5.1-dev", + "pkgconfig" + ] + }, + "libqrencode-doc": { + "versions": { + "4.0.0-r0": 1509482400 + }, + "origin": "libqrencode" + }, + "udisks-dev": { + "versions": { + "1.0.5-r3": 1510073401 + }, + "origin": "udisks", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:udisks=1.0.5" + ] + }, + "aspell": { + "versions": { + "0.60.6.1-r12": 1509472868, + "0.60.6.1-r13": 1572541141 + }, + "origin": "aspell", + "dependencies": [ + "so:libaspell.so.15", + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:aspell" + ] + }, + "kamailio-authephemeral": { + "versions": { + "5.0.7-r0": 1532960876 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "libsoup-dev": { + "versions": { + "2.60.2-r0": 1509480669 + }, + "origin": "libsoup", + "dependencies": [ + "gnutls-dev", + "sqlite-dev", + "libsoup=2.60.2-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libsoup-2.4=2.60.2", + "pc:libsoup-gnome-2.4=2.60.2" + ] + }, + "lua5.1-iconv": { + "versions": { + "7-r1": 1509493322 + }, + "origin": "lua-iconv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gvpe-doc": { + "versions": { + "2.25-r4": 1510259350 + }, + "origin": "gvpe" + }, + "xrdp-doc": { + "versions": { + "0.9.2-r3": 1510260212 + }, + "origin": "xrdp" + }, + "rxvt-unicode": { + "versions": { + "9.22-r2": 1509476790 + }, + "origin": "rxvt-unicode", + "dependencies": [ + "rxvt-unicode-terminfo", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libgcc_s.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libperl.so", + "so:libstartup-notification-1.so.0" + ], + "provides": [ + "cmd:urxvt", + "cmd:urxvtc", + "cmd:urxvtd" + ] + }, + "py2-vte": { + "versions": { + "0.28.2-r13": 1510071903 + }, + "origin": "vte", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libvte.so.9" + ] + }, + "perl-list-allutils": { + "versions": { + "0.14-r0": 1509493248 + }, + "origin": "perl-list-allutils", + "dependencies": [ + "perl-list-utilsby", + "perl-list-someutils", + "perl-scalar-list-utils" + ] + }, + "libfastjson-dbg": { + "versions": { + "0.99.7-r0": 1509486267 + }, + "origin": "libfastjson" + }, + "graphite2": { + "versions": { + "1.3.10-r0": 1509464858 + }, + "origin": "graphite2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgraphite2.so.3=3.0.1" + ] + }, + "xz": { + "versions": { + "5.2.3-r1": 1509459906 + }, + "origin": "xz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblzma.so.5" + ], + "provides": [ + "cmd:lzcat", + "cmd:lzcmp", + "cmd:lzdiff", + "cmd:lzegrep", + "cmd:lzfgrep", + "cmd:lzgrep", + "cmd:lzless", + "cmd:lzma", + "cmd:lzmadec", + "cmd:lzmainfo", + "cmd:lzmore", + "cmd:unlzma", + "cmd:unxz", + "cmd:xz", + "cmd:xzcat", + "cmd:xzcmp", + "cmd:xzdec", + "cmd:xzdiff", + "cmd:xzegrep", + "cmd:xzfgrep", + "cmd:xzgrep", + "cmd:xzless", + "cmd:xzmore" + ] + }, + "py2-alabaster": { + "versions": { + "0.7.10-r0": 1509476478 + }, + "origin": "py-alabaster", + "dependencies": [ + "python2" + ] + }, + "perl-test-deep": { + "versions": { + "1.126-r0": 1509468481 + }, + "origin": "perl-test-deep", + "dependencies": [ + "perl", + "perl-test-tester", + "perl-test-nowarnings" + ] + }, + "gpicview-lang": { + "versions": { + "0.2.5-r0": 1510074706 + }, + "origin": "gpicview" + }, + "zd1211-firmware": { + "versions": { + "1.5-r0": 1509496083 + }, + "origin": "zd1211-firmware" + }, + "xfce4-panel-dev": { + "versions": { + "4.12.1-r1": 1510068171 + }, + "origin": "xfce4-panel", + "dependencies": [ + "libxfce4util-dev", + "gtk+2.0-dev", + "gtk+3.0-dev", + "pc:glib-2.0", + "pc:gmodule-2.0", + "pc:gtk+-2.0", + "pc:gtk+-3.0", + "pc:libxfce4util-1.0", + "pkgconfig", + "xfce4-panel=4.12.1-r1" + ], + "provides": [ + "pc:libxfce4panel-1.0=4.12.1", + "pc:libxfce4panel-2.0=4.12.1" + ] + }, + "uwsgi-router_http": { + "versions": { + "2.0.17-r0": 1522154657 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "nagios-plugins-ups": { + "versions": { + "2.2.1-r3": 1510288498 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua-stringy": { + "versions": { + "0.4.0-r5": 1509480105 + }, + "origin": "lua-stringy" + }, + "cksfv": { + "versions": { + "1.3.14-r4": 1509486269 + }, + "origin": "cksfv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cksfv" + ] + }, + "acf-awall": { + "versions": { + "0.4.1-r2": 1510072215 + }, + "origin": "acf-awall", + "dependencies": [ + "acf-core", + "awall" + ] + }, + "nagios-plugins-wave": { + "versions": { + "2.2.1-r3": 1510288499 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins" + ] + }, + "http-parser": { + "versions": { + "2.7.1-r1": 1509483708 + }, + "origin": "http-parser", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhttp_parser.so.2.7.1=2.7.1" + ] + }, + "py2-oauthlib": { + "versions": { + "2.0.6-r1": 1509552759 + }, + "origin": "py-oauthlib", + "dependencies": [ + "py2-crypto", + "py2-jwt", + "python2" + ] + }, + "cpulimit": { + "versions": { + "0.2-r0": 1509489640 + }, + "origin": "cpulimit", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cpulimit" + ] + }, + "vde2": { + "versions": { + "2.3.2-r8": 1510260476 + }, + "origin": "vde2", + "dependencies": [ + "libressl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libpcap.so.1", + "so:libvdehist.so.0", + "so:libvdemgmt.so.0", + "so:libvdeplug.so.3" + ], + "provides": [ + "cmd:dpipe", + "cmd:slirpvde", + "cmd:unixcmd", + "cmd:unixterm", + "cmd:vde_autolink", + "cmd:vde_cryptcab", + "cmd:vde_l3", + "cmd:vde_over_ns", + "cmd:vde_pcapplug", + "cmd:vde_plug", + "cmd:vde_plug2tap", + "cmd:vde_switch", + "cmd:vde_tunctl", + "cmd:vdecmd", + "cmd:vdekvm", + "cmd:vdeq", + "cmd:vdeqemu", + "cmd:vdeterm", + "cmd:wirefilter" + ] + }, + "libvpx": { + "versions": { + "1.6.1-r0": 1509480734 + }, + "origin": "libvpx", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libvpx.so.4=4.1.0" + ] + }, + "sems-annrecorder": { + "versions": { + "1.6.0-r6": 1510260897 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "perl-devel-symdump-doc": { + "versions": { + "2.18-r0": 1509470432 + }, + "origin": "perl-devel-symdump" + }, + "ruby-power_assert": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ] + }, + "aspell-lang": { + "versions": { + "0.60.6.1-r12": 1509472868, + "0.60.6.1-r13": 1572541141 + }, + "origin": "aspell" + }, + "libpri": { + "versions": { + "1.6.0-r0": 1509476161 + }, + "origin": "libpri", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpri.so.1.4=1.4" + ] + }, + "fltk-dev": { + "versions": { + "1.3.4-r0": 1510072237 + }, + "origin": "fltk", + "dependencies": [ + "libx11-dev", + "libxext-dev", + "libxft-dev", + "mesa-dev", + "fltk=1.3.4-r0" + ], + "provides": [ + "cmd:fltk-config" + ] + }, + "uwsgi-logsocket": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libxxf86vm-dev": { + "versions": { + "1.1.4-r1": 1509466237 + }, + "origin": "libxxf86vm", + "dependencies": [ + "libxxf86vm=1.1.4-r1", + "pc:x11", + "pc:xext", + "pc:xf86vidmodeproto", + "pkgconfig" + ], + "provides": [ + "pc:xxf86vm=1.1.4" + ] + }, + "bzip2-dev": { + "versions": { + "1.0.6-r6": 1509456387, + "1.0.6-r7": 1562268494 + }, + "origin": "bzip2", + "dependencies": [ + "libbz2=1.0.6-r7" + ] + }, + "rpcbind-dbg": { + "versions": { + "0.2.4-r0": 1509488290 + }, + "origin": "rpcbind" + }, + "py3-certifi": { + "versions": { + "2017.7.27.1-r1": 1509551896 + }, + "origin": "py-certifi", + "dependencies": [ + "python3" + ] + }, + "libbsd-dev": { + "versions": { + "0.8.6-r1": 1509461837 + }, + "origin": "libbsd", + "dependencies": [ + "bsd-compat-headers", + "linux-headers", + "libbsd=0.8.6-r1", + "pkgconfig" + ], + "provides": [ + "pc:libbsd-overlay=0.8.6", + "pc:libbsd=0.8.6" + ] + }, + "patchwork-uwsgi-apache2": { + "versions": { + "1.1.3-r0": 1509474237, + "1.1.3-r1": 1562223418 + }, + "origin": "patchwork", + "dependencies": [ + "apache2", + "apache2-mod-wsgi" + ] + }, + "farstream": { + "versions": { + "0.2.8-r2": 1510931395 + }, + "origin": "farstream", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstnet-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgstrtp-1.0.so.0", + "so:libnice.so.10" + ], + "provides": [ + "so:libfarstream-0.2.so.5=5.1.0" + ] + }, + "lua-pingu": { + "versions": { + "1.5-r1": 1509474486 + }, + "origin": "pingu", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libarchive-tools": { + "versions": { + "3.3.2-r2": 1510258000, + "3.3.3-r0": 1566312165, + "3.3.3-r1": 1572677952 + }, + "origin": "libarchive", + "dependencies": [ + "so:libacl.so.1", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:liblz4.so.1", + "so:liblzma.so.5", + "so:libz.so.1" + ], + "provides": [ + "cmd:bsdcat", + "cmd:bsdcpio", + "cmd:bsdtar" + ] + }, + "py-gtk": { + "versions": { + "2.24.0-r14": 1510071867 + }, + "origin": "py-gtk", + "dependencies": [ + "py2-cairo", + "py-gobject", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglade-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0" + ], + "provides": [ + "cmd:pygtk-codegen-2.0" + ] + }, + "nodejs-dev": { + "versions": { + "8.9.3-r1": 1522616958 + }, + "origin": "nodejs", + "dependencies": [ + "libuv" + ], + "provides": [ + "nodejs-lts-dev=8.9.3" + ] + }, + "gst-plugins-ugly-lang": { + "versions": { + "1.12.3-r0": 1510075819 + }, + "origin": "gst-plugins-ugly" + }, + "atkmm-dev": { + "versions": { + "2.24.2-r0": 1509483360 + }, + "origin": "atkmm", + "dependencies": [ + "atkmm=2.24.2-r0", + "pc:atk>=1.18", + "pc:glibmm-2.4>=2.46.2", + "pkgconfig" + ], + "provides": [ + "pc:atkmm-1.6=2.24.2" + ] + }, + "radvd": { + "versions": { + "2.16-r0": 1509493026 + }, + "origin": "radvd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:radvd", + "cmd:radvdump" + ] + }, + "oniguruma": { + "versions": { + "6.6.1-r0": 1509470649 + }, + "origin": "oniguruma", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libonig.so.4=4.0.0" + ] + }, + "darkstat": { + "versions": { + "3.0.719-r0": 1509486286 + }, + "origin": "darkstat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:darkstat" + ] + }, + "gstreamer0.10-doc": { + "versions": { + "0.10.36-r2": 1509470981 + }, + "origin": "gstreamer0.10" + }, + "openjade": { + "versions": { + "1.3.2-r5": 1509488730 + }, + "origin": "openjade", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libogrove.so.0", + "so:libosp.so.5", + "so:libospgrove.so.0", + "so:libostyle.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:openjade" + ] + }, + "blkid": { + "versions": { + "2.31.1-r0": 1541506295 + }, + "origin": "util-linux", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:blkid" + ] + }, + "libxaw": { + "versions": { + "1.0.13-r0": 1509480584 + }, + "origin": "libxaw", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXmu.so.6", + "so:libXpm.so.4", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXaw.so.7=7.0.0" + ] + }, + "py-gobject3-dev": { + "versions": { + "3.24.1-r2": 1509481893 + }, + "origin": "py-gobject3", + "dependencies": [ + "pc:gobject-2.0", + "pc:libffi", + "pkgconfig" + ], + "provides": [ + "pc:pygobject-3.0=3.24.1" + ] + }, + "perl-sub-exporter-progressive-doc": { + "versions": { + "0.001013-r0": 1509473994 + }, + "origin": "perl-sub-exporter-progressive" + }, + "polkit": { + "versions": { + "0.105-r8": 1551780676, + "0.105-r9": 1563792450, + "0.105-r10": 1567523134 + }, + "origin": "polkit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libpolkit-agent-1.so.0=0.0.0", + "so:libpolkit-backend-1.so.0=0.0.0", + "so:libpolkit-gobject-1.so.0=0.0.0", + "cmd:pk-example-frobnicate", + "cmd:pkaction", + "cmd:pkcheck", + "cmd:pkexec", + "cmd:pkttyagent" + ] + }, + "tsocks-doc": { + "versions": { + "1.8_beta5-r0": 1509496537 + }, + "origin": "tsocks" + }, + "mp3splt-doc": { + "versions": { + "2.6.2-r0": 1509488737 + }, + "origin": "mp3splt" + }, + "perl-xml-sax-doc": { + "versions": { + "0.99-r2": 1509475904 + }, + "origin": "perl-xml-sax" + }, + "xen-hypervisor": { + "versions": { + "4.9.4-r0": 1551280495, + "4.9.4-r1": 1558103152 + }, + "origin": "xen" + }, + "nghttp2-doc": { + "versions": { + "1.28.0-r0": 1511922922, + "1.39.2-r0": 1568186892 + }, + "origin": "nghttp2" + }, + "debian-archive-keyring": { + "versions": { + "2017.6-r0": 1510588269 + }, + "origin": "debian-archive-keyring", + "dependencies": [ + "gnupg" + ] + }, + "libxpm-doc": { + "versions": { + "3.5.12-r0": 1509469832 + }, + "origin": "libxpm" + }, + "haserl-lua5.3": { + "versions": { + "0.9.35-r1": 1509468294 + }, + "origin": "haserl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua-5.3.so.0" + ], + "provides": [ + "cmd:haserl-lua5.3" + ] + }, + "cppunit-doc": { + "versions": { + "1.14.0-r0": 1509481255 + }, + "origin": "cppunit" + }, + "lua5.3-openrc": { + "versions": { + "0.2-r2": 1509492202 + }, + "origin": "lua-openrc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librc.so.1" + ] + }, + "postgresql-doc": { + "versions": { + "10.7-r0": 1554274198, + "10.8-r0": 1557668014, + "10.9-r0": 1562225613, + "10.10-r0": 1565683438 + }, + "origin": "postgresql" + }, + "mpg123-dev": { + "versions": { + "1.25.7-r0": 1509707456 + }, + "origin": "mpg123", + "dependencies": [ + "mpg123=1.25.7-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmpg123=1.25.7", + "pc:libout123=1.25.7" + ] + }, + "perl-string-shellquote-doc": { + "versions": { + "1.04-r0": 1509494702 + }, + "origin": "perl-string-shellquote" + }, + "nagios-plugins-ide_smart": { + "versions": { + "2.2.1-r3": 1510288495 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "mp3splt-gtk-lang": { + "versions": { + "0.9.2-r2": 1510070623 + }, + "origin": "mp3splt-gtk" + }, + "libexif-doc": { + "versions": { + "0.6.21-r2": 1539006611 + }, + "origin": "libexif" + }, + "libelf-dev": { + "versions": { + "0.8.13-r3": 1509468228 + }, + "origin": "libelf", + "dependencies": [ + "libelf=0.8.13-r3", + "pkgconfig" + ], + "provides": [ + "pc:libelf=0.8.13" + ] + }, + "nagios-plugins-sensors": { + "versions": { + "2.2.1-r3": 1510288498 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "grep", + "lm_sensors" + ] + }, + "hunspell-en": { + "versions": { + "2017.01.22-r0": 1511186496 + }, + "origin": "hunspell-en" + }, + "acf-lib": { + "versions": { + "0.10.1-r0": 1510068294 + }, + "origin": "acf-lib", + "dependencies": [ + "lua-subprocess" + ] + }, + "xf86-input-libinput-dev": { + "versions": { + "0.26.0-r0": 1510303211 + }, + "origin": "xf86-input-libinput", + "dependencies": [ + "libinput-dev", + "xorg-server-dev", + "pkgconfig" + ], + "provides": [ + "pc:xorg-libinput=0.26.0" + ] + }, + "libidn-dev": { + "versions": { + "1.33-r1": 1509468968 + }, + "origin": "libidn", + "dependencies": [ + "libidn=1.33-r1", + "pkgconfig" + ], + "provides": [ + "pc:libidn=1.33" + ] + }, + "dejagnu-dev": { + "versions": { + "1.6-r1": 1509491945 + }, + "origin": "dejagnu" + }, + "bzip2": { + "versions": { + "1.0.6-r6": 1509456387, + "1.0.6-r7": 1562268494 + }, + "origin": "bzip2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bunzip2", + "cmd:bzcat", + "cmd:bzcmp", + "cmd:bzdiff", + "cmd:bzegrep", + "cmd:bzfgrep", + "cmd:bzgrep", + "cmd:bzip2", + "cmd:bzip2recover", + "cmd:bzless", + "cmd:bzmore" + ] + }, + "perl-scope-guard": { + "versions": { + "0.21-r0": 1509482412 + }, + "origin": "perl-scope-guard", + "dependencies": [ + "perl" + ] + }, + "mesa-dri-virtio": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "stalonetray-doc": { + "versions": { + "0.8.3-r0": 1509489782 + }, + "origin": "stalonetray" + }, + "cpufrequtils": { + "versions": { + "008-r4": 1509486337 + }, + "origin": "cpufrequtils", + "dependencies": [ + "sysfsutils", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcpufreq.so.0=0.0.0", + "cmd:cpufreq-aperf", + "cmd:cpufreq-info", + "cmd:cpufreq-set" + ] + }, + "perl-specio-doc": { + "versions": { + "0.36-r0": 1509481612 + }, + "origin": "perl-specio" + }, + "perl-http-negotiate": { + "versions": { + "6.01-r1": 1509464410 + }, + "origin": "perl-http-negotiate", + "dependencies": [ + "perl", + "perl-http-message" + ] + }, + "gnutls": { + "versions": { + "3.6.1-r0": 1509465349 + }, + "origin": "gnutls", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libhogweed.so.4", + "so:libnettle.so.6", + "so:libp11-kit.so.0", + "so:libtasn1.so.6", + "so:libunistring.so.2" + ], + "provides": [ + "so:libgnutls.so.30=30.20.1" + ] + }, + "xvinfo-doc": { + "versions": { + "1.1.3-r0": 1509492434 + }, + "origin": "xvinfo" + }, + "nagios-plugins-tcp": { + "versions": { + "2.2.1-r3": 1510288499 + }, + "origin": "nagios-plugins", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "nagios-plugins-clamdnagios-plugins-ftpnagios-plugins-imapnagios-plugins-jabbernagios-plugins-nntpnagios-plugins-nntpsnagios-plugins-popnagios-plugins-simapnagios-plugins-spopnagios-plugins-ssmtpnagios-plugins-udp" + ] + }, + "cairo-gobject": { + "versions": { + "1.14.10-r0": 1509464620 + }, + "origin": "cairo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libcairo-gobject.so.2=2.11400.10" + ] + }, + "uwsgi-gevent": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libwebp-dev": { + "versions": { + "0.6.0-r1": 1509473038 + }, + "origin": "libwebp", + "dependencies": [ + "libwebp=0.6.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:libwebp=0.6.0", + "pc:libwebpdecoder=0.6.0", + "pc:libwebpdemux=0.6.0", + "pc:libwebpmux=0.6.0" + ] + }, + "asterisk-tds": { + "versions": { + "15.6.1-r0": 1537795340, + "15.6.2-r0": 1568705002 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsybdb.so.5" + ] + }, + "xf86-video-intel-doc": { + "versions": { + "2.99.917_git20170325-r0": 1510076034 + }, + "origin": "xf86-video-intel" + }, + "acf-amavisd-new": { + "versions": { + "0.4.0-r2": 1510076339 + }, + "origin": "acf-amavisd-new", + "dependencies": [ + "acf-core", + "amavisd-new" + ] + }, + "http-parser-dev": { + "versions": { + "2.7.1-r1": 1509483707 + }, + "origin": "http-parser", + "dependencies": [ + "http-parser=2.7.1-r1" + ] + }, + "sqlite-libs": { + "versions": { + "3.25.3-r0": 1548491335, + "3.25.3-r1": 1563951530, + "3.25.3-r2": 1571590443 + }, + "origin": "sqlite", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsqlite3.so.0=0.8.6" + ] + }, + "cups-client": { + "versions": { + "2.2.10-r0": 1549287809, + "2.2.12-r0": 1566207596 + }, + "origin": "cups", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libcupsimage.so.2", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:accept", + "cmd:cancel", + "cmd:cupsaccept", + "cmd:cupsaddsmb", + "cmd:cupsctl", + "cmd:cupsdisable", + "cmd:cupsenable", + "cmd:cupstestdsc", + "cmd:cupstestppd", + "cmd:ippfind", + "cmd:lp", + "cmd:lpadmin", + "cmd:lpc", + "cmd:lpinfo", + "cmd:lpmove", + "cmd:lpoptions", + "cmd:lpq", + "cmd:lpr", + "cmd:lprm", + "cmd:lpstat", + "cmd:ppdc", + "cmd:ppdhtml", + "cmd:ppdi", + "cmd:ppdmerge", + "cmd:ppdpo", + "cmd:reject" + ] + }, + "strongswan-dbg": { + "versions": { + "5.6.3-r2": 1539005310 + }, + "origin": "strongswan", + "dependencies": [ + "iproute2" + ] + }, + "recordproto": { + "versions": { + "1.14.2-r2": 1509466033 + }, + "origin": "recordproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:recordproto=1.14.2" + ] + }, + "perl-module-implementation": { + "versions": { + "0.09-r0": 1509470580 + }, + "origin": "perl-module-implementation", + "dependencies": [ + "perl-try-tiny", + "perl-module-runtime" + ] + }, + "highlight-doc": { + "versions": { + "3.41-r0": 1512029997 + }, + "origin": "highlight" + }, + "py-libvirt": { + "versions": { + "3.9.0-r0": 1510088408 + }, + "origin": "py-libvirt" + }, + "cups-doc": { + "versions": { + "2.2.10-r0": 1549287809, + "2.2.12-r0": 1566207590 + }, + "origin": "cups" + }, + "ruby-minitest": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ] + }, + "rhash": { + "versions": { + "1.3.5-r1": 1509461596 + }, + "origin": "rhash", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librhash.so.0" + ], + "provides": [ + "cmd:ed2k-link", + "cmd:gost-hash", + "cmd:has160-hash", + "cmd:magnet-link", + "cmd:rhash", + "cmd:sfv-hash", + "cmd:tiger-hash", + "cmd:tth-hash", + "cmd:whirlpool-hash" + ] + }, + "qemu-arm": { + "versions": { + "2.10.1-r3": 1519746239 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-arm" + ] + }, + "kbd-vlock": { + "versions": { + "2.0.4-r2": 1510922532 + }, + "origin": "kbd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0", + "so:libpam_misc.so.0" + ], + "provides": [ + "cmd:vlock" + ] + }, + "dbus-glib-doc": { + "versions": { + "0.108-r1": 1509465372 + }, + "origin": "dbus-glib" + }, + "xfce4-mixer-lang": { + "versions": { + "4.11.0-r2": 1510069965 + }, + "origin": "xfce4-mixer", + "dependencies": [ + "hicolor-icon-theme" + ] + }, + "mtdev": { + "versions": { + "1.1.5-r1": 1509475744 + }, + "origin": "mtdev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmtdev.so.1=1.0.0" + ] + }, + "linux-virthardened-dev": { + "versions": { + "4.9.65-r1": 1511798478 + }, + "origin": "linux-hardened", + "dependencies": [ + "gmp-dev", + "bash", + "so:libc.musl-x86_64.so.1", + "so:libelf.so.1", + "so:libstdc++.so.6" + ] + }, + "live-media-dev": { + "versions": { + "2017.10.28-r0": 1510046891 + }, + "origin": "live-media", + "dependencies": [ + "live-media=2017.10.28-r0" + ] + }, + "feh": { + "versions": { + "2.22-r0": 1509878783 + }, + "origin": "feh", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libpng16.so.16" + ], + "provides": [ + "cmd:feh" + ] + }, + "uwsgi-rsyslog": { + "versions": { + "2.0.17-r0": 1522154659 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "xextproto": { + "versions": { + "7.3.0-r2": 1509461947 + }, + "origin": "xextproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xextproto=7.3.0" + ] + }, + "thunar-archive-plugin-lang": { + "versions": { + "0.3.1-r1": 1510075227 + }, + "origin": "thunar-archive-plugin" + }, + "libgsasl": { + "versions": { + "1.8.0-r2": 1509493437 + }, + "origin": "libgsasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libidn.so.11" + ], + "provides": [ + "so:libgsasl.so.7=7.9.6" + ] + }, + "freeradius-krb5": { + "versions": { + "3.0.15-r4": 1556202797, + "3.0.15-r5": 1566310605 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.15-r5", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libkrb5.so.3" + ], + "provides": [ + "freeradius3-krb5=3.0.15-r5", + "so:rlm_krb5.so=0" + ] + }, + "perl-http-negotiate-doc": { + "versions": { + "6.01-r1": 1509464410 + }, + "origin": "perl-http-negotiate" + }, + "hunspell-lang": { + "versions": { + "1.6.2-r1": 1509477668 + }, + "origin": "hunspell" + }, + "uwsgi-router_uwsgi": { + "versions": { + "2.0.17-r0": 1522154658 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "squid": { + "versions": { + "3.5.27-r0": 1519824033, + "3.5.27-r1": 1562865670 + }, + "origin": "squid", + "dependencies": [ + "logrotate", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.42", + "so:libdb-5.3.so", + "so:libgcc_s.so.1", + "so:libltdl.so.7", + "so:libssl.so.44", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:purge", + "cmd:squid", + "cmd:squidclient" + ] + }, + "bash": { + "versions": { + "4.4.19-r1": 1518031135 + }, + "origin": "bash", + "dependencies": [ + "/bin/sh", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:bash", + "cmd:bashbug" + ] + }, + "lua5.1-dbi-sqlite3": { + "versions": { + "0.6-r1": 1511483402 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "postgresql-plperl": { + "versions": { + "10.7-r0": 1554274199, + "10.8-r0": 1557668015, + "10.9-r0": 1562225614, + "10.10-r0": 1565683438 + }, + "origin": "postgresql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "libestr-dev": { + "versions": { + "0.1.10-r0": 1509486151 + }, + "origin": "libestr", + "dependencies": [ + "libestr=0.1.10-r0", + "pkgconfig" + ], + "provides": [ + "pc:libestr=0.1.10" + ] + }, + "chrpath-doc": { + "versions": { + "0.16-r1": 1509466380 + }, + "origin": "chrpath" + }, + "cairo-doc": { + "versions": { + "1.14.10-r0": 1509464620 + }, + "origin": "cairo" + }, + "perl-test-identity": { + "versions": { + "0.01-r0": 1509477788 + }, + "origin": "perl-test-identity" + }, + "libart-lgpl-dev": { + "versions": { + "2.3.21-r5": 1509475376 + }, + "origin": "libart-lgpl", + "dependencies": [ + "libart-lgpl=2.3.21-r5", + "pkgconfig" + ], + "provides": [ + "pc:libart-2.0=2.3.21", + "cmd:libart2-config" + ] + }, + "lua-dns": { + "versions": { + "20080404-r2": 1509488749 + }, + "origin": "lua-dns", + "dependencies": [ + "lua", + "lua-socket" + ] + }, + "usbredir": { + "versions": { + "0.7.1-r0": 1509707469 + }, + "origin": "usbredir", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libusbredirhost.so.1=1.0.0", + "so:libusbredirparser.so.1=1.0.0" + ] + }, + "boost-dev": { + "versions": { + "1.62.0-r5": 1509465870 + }, + "origin": "boost", + "dependencies": [ + "linux-headers", + "boost-date_time=1.62.0-r5", + "boost-filesystem=1.62.0-r5", + "boost-graph=1.62.0-r5", + "boost-iostreams=1.62.0-r5", + "boost-math=1.62.0-r5", + "boost-prg_exec_monitor=1.62.0-r5", + "boost-program_options=1.62.0-r5", + "boost-python3=1.62.0-r5", + "boost-python=1.62.0-r5", + "boost-random=1.62.0-r5", + "boost-regex=1.62.0-r5", + "boost-serialization=1.62.0-r5", + "boost-signals=1.62.0-r5", + "boost-system=1.62.0-r5", + "boost-thread=1.62.0-r5", + "boost-unit_test_framework=1.62.0-r5", + "boost-wave=1.62.0-r5", + "boost-wserialization=1.62.0-r5", + "boost=1.62.0-r5" + ] + }, + "py3-flask-wtf": { + "versions": { + "0.14.2-r0": 1509476546 + }, + "origin": "py-flask-wtf", + "dependencies": [ + "py3-flask", + "py3-wtforms", + "python3" + ] + }, + "texinfo-doc": { + "versions": { + "6.5-r0": 1509456625 + }, + "origin": "texinfo" + }, + "xfce4-cpufreq-plugin": { + "versions": { + "1.1.3-r2": 1510074007 + }, + "origin": "xfce4-cpufreq-plugin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxfce4panel-1.0.so.4", + "so:libxfce4ui-1.so.0", + "so:libxfce4util.so.7" + ] + }, + "irssi-doc": { + "versions": { + "1.0.6-r0": 1519052408, + "1.0.8-r0": 1562236916 + }, + "origin": "irssi" + }, + "g++": { + "versions": { + "6.4.0-r5": 1509458073 + }, + "origin": "gcc", + "dependencies": [ + "libstdc++=6.4.0-r5", + "gcc=6.4.0-r5", + "libc-dev", + "libstdc++=6.4.0-r5", + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libisl.so.15", + "so:libmpc.so.3", + "so:libmpfr.so.4", + "so:libz.so.1" + ], + "provides": [ + "cmd:c++", + "cmd:g++", + "cmd:x86_64-alpine-linux-musl-c++", + "cmd:x86_64-alpine-linux-musl-g++" + ] + }, + "tcpflow-doc": { + "versions": { + "1.4.5-r3": 1510260713, + "1.5.0-r1": 1562596229 + }, + "origin": "tcpflow" + }, + "execline-dev": { + "versions": { + "2.3.0.3-r0": 1509488629 + }, + "origin": "execline", + "dependencies": [ + "execline=2.3.0.3-r0" + ] + }, + "apache2-webdav": { + "versions": { + "2.4.39-r0": 1554306882, + "2.4.41-r0": 1566292328 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1" + ] + }, + "fossil": { + "versions": { + "2.3-r1": 1510260449 + }, + "origin": "fossil", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libsqlite3.so.0", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "cmd:fossil" + ] + }, + "lxc-libs": { + "versions": { + "2.1.1-r3": 1533578833 + }, + "origin": "lxc", + "dependencies": [ + "gzip", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libseccomp.so.2" + ], + "provides": [ + "so:liblxc.so.1=1.3.0" + ] + }, + "pssh": { + "versions": { + "2.3.1-r1": 1509494678 + }, + "origin": "pssh", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:pnuke", + "cmd:prsync", + "cmd:pscp", + "cmd:pslurp", + "cmd:pssh" + ] + }, + "py3-cairo": { + "versions": { + "1.10.0-r0": 1509481861 + }, + "origin": "py3-cairo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libpython3.6m.so.1.0" + ] + }, + "abiword-plugin-eml": { + "versions": { + "3.0.2-r1": 1510073358 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "py2-imagesize": { + "versions": { + "0.7.1-r2": 1509474090 + }, + "origin": "py-imagesize", + "dependencies": [ + "python2" + ] + }, + "uwsgi-router_static": { + "versions": { + "2.0.17-r0": 1522154658 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "xtrans": { + "versions": { + "1.3.5-r1": 1509461954 + }, + "origin": "xtrans" + }, + "font-adobe-utopia-75dpi": { + "versions": { + "1.0.4-r0": 1509489768 + }, + "origin": "font-adobe-utopia-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "oidentd": { + "versions": { + "2.0.8-r5": 1509483895 + }, + "origin": "oidentd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:oidentd" + ] + }, + "mpc1-doc": { + "versions": { + "1.0.3-r1": 1509457077 + }, + "origin": "mpc1" + }, + "libxcb-doc": { + "versions": { + "1.12-r1": 1509461940 + }, + "origin": "libxcb" + }, + "usb-modeswitch-doc": { + "versions": { + "2.5.1-r0": 1509480147 + }, + "origin": "usb-modeswitch" + }, + "py3-pillow": { + "versions": { + "4.3.0-r0": 1509489959 + }, + "origin": "py-pillow", + "dependencies": [ + "py3-olefile", + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libopenjp2.so.7", + "so:libpython3.6m.so.1.0", + "so:libtiff.so.5", + "so:libwebp.so.7", + "so:libwebpmux.so.3", + "so:libz.so.1" + ] + }, + "renderproto-doc": { + "versions": { + "0.11.1-r3": 1509462088 + }, + "origin": "renderproto" + }, + "py-egenix-mx-base": { + "versions": { + "3.2.9-r0": 1509474197 + }, + "origin": "py-egenix-mx-base", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "squid-lang-es": { + "versions": { + "3.5.27-r0": 1519824031, + "3.5.27-r1": 1562865667 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libmemcached-doc": { + "versions": { + "1.0.18-r2": 1520366184 + }, + "origin": "libmemcached" + }, + "libunistring": { + "versions": { + "0.9.7-r0": 1509459000 + }, + "origin": "libunistring", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libunistring.so.2=2.0.0" + ] + }, + "valgrind-doc": { + "versions": { + "3.12.0-r1": 1509494840 + }, + "origin": "valgrind" + }, + "nsd-doc": { + "versions": { + "4.1.16-r1": 1510260999 + }, + "origin": "nsd" + }, + "zfs-dev": { + "versions": { + "0.7.3-r0": 1509490073 + }, + "origin": "zfs", + "dependencies": [ + "glib-dev", + "e2fsprogs-dev", + "util-linux-dev", + "libtirpc-dev", + "attr-dev", + "pkgconfig", + "zfs-libs=0.7.3-r0" + ], + "provides": [ + "pc:libzfs=0.7.3", + "pc:libzfs_core=0.7.3" + ] + }, + "nginx-mod-http-shibboleth": { + "versions": { + "1.12.2-r4": 1542814448 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.3-lub": { + "versions": { + "1.1.0-r1": 1509475704 + }, + "origin": "lua-lub", + "dependencies": [ + "lua5.3", + "lua5.3-filesystem" + ] + }, + "vanessa_adt-dev": { + "versions": { + "0.0.9-r0": 1509481358 + }, + "origin": "vanessa_adt", + "dependencies": [ + "vanessa_logger-dev", + "vanessa_adt=0.0.9-r0" + ] + }, + "libxdamage-dev": { + "versions": { + "1.1.4-r1": 1509464691 + }, + "origin": "libxdamage", + "dependencies": [ + "damageproto", + "fixesproto", + "libxdamage=1.1.4-r1", + "pc:damageproto>=1.1", + "pc:x11", + "pc:xfixes", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xdamage=1.1.4" + ] + }, + "ciwiki-doc": { + "versions": { + "2.0.5-r1": 1509493442 + }, + "origin": "ciwiki" + }, + "spl-hardened": { + "versions": { + "4.9.65-r1": 1511798817 + }, + "origin": "spl-hardened", + "dependencies": [ + "linux-hardened=4.9.65-r1" + ], + "provides": [ + "spl-grsec=4.9.65-r1" + ] + }, + "perl-heap": { + "versions": { + "0.80-r1": 1511989874 + }, + "origin": "perl-heap" + }, + "apr-util-dev": { + "versions": { + "1.6.1-r1": 1510285059 + }, + "origin": "apr-util", + "dependencies": [ + "expat-dev", + "apr-dev", + "openldap-dev", + "sqlite-dev", + "postgresql-dev", + "db-dev", + "libressl-dev", + "apr-util=1.6.1-r1", + "pc:apr-1", + "pkgconfig" + ], + "provides": [ + "pc:apr-util-1=1.6.1", + "cmd:apu-1-config" + ] + }, + "open-lldp": { + "versions": { + "0.9.46-r3": 1510075777 + }, + "origin": "open-lldp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libconfig.so.9", + "so:libnl.so.1" + ], + "provides": [ + "so:liblldp_clif.so.1=1.0.0", + "cmd:dcbtool", + "cmd:lldpad", + "cmd:lldptool" + ] + }, + "perl-regexp-ipv6": { + "versions": { + "0.03-r0": 1509470472 + }, + "origin": "perl-regexp-ipv6", + "dependencies": [ + "perl" + ] + }, + "libnetfilter_cttimeout-dev": { + "versions": { + "1.0.0-r0": 1509469232 + }, + "origin": "libnetfilter_cttimeout", + "dependencies": [ + "libmnl-dev", + "libnetfilter_cttimeout=1.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_cttimeout=1.0.0" + ] + }, + "fftw": { + "versions": { + "3.3.6p2-r0": 1509468831 + }, + "origin": "fftw", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfftw3.so.3", + "so:libfftw3_threads.so.3", + "so:libfftw3f.so.3", + "so:libfftw3f_threads.so.3", + "so:libfftw3l.so.3", + "so:libfftw3l_threads.so.3" + ], + "provides": [ + "cmd:fftw-wisdom", + "cmd:fftw-wisdom-to-conf", + "cmd:fftwf-wisdom", + "cmd:fftwl-wisdom" + ] + }, + "qemu-img": { + "versions": { + "2.10.1-r3": 1519746244 + }, + "origin": "qemu", + "dependencies": [ + "so:libaio.so.1", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libssh2.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-img", + "cmd:qemu-io", + "cmd:qemu-nbd" + ] + }, + "jack-example-clients": { + "versions": { + "1.9.10-r5": 1509473320 + }, + "origin": "jack", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libjack.so.0", + "so:libjacknet.so.0", + "so:libjackserver.so.0", + "so:libreadline.so.7", + "so:libsamplerate.so.0", + "so:libsndfile.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:alsa_in", + "cmd:alsa_out", + "cmd:jack_alias", + "cmd:jack_bufsize", + "cmd:jack_connect", + "cmd:jack_control", + "cmd:jack_cpu", + "cmd:jack_cpu_load", + "cmd:jack_disconnect", + "cmd:jack_evmon", + "cmd:jack_freewheel", + "cmd:jack_iodelay", + "cmd:jack_latent_client", + "cmd:jack_load", + "cmd:jack_lsp", + "cmd:jack_metro", + "cmd:jack_midi_dump", + "cmd:jack_midi_latency_test", + "cmd:jack_midiseq", + "cmd:jack_midisine", + "cmd:jack_monitor_client", + "cmd:jack_multiple_metro", + "cmd:jack_net_master", + "cmd:jack_net_slave", + "cmd:jack_netsource", + "cmd:jack_rec", + "cmd:jack_samplerate", + "cmd:jack_server_control", + "cmd:jack_session_notify", + "cmd:jack_showtime", + "cmd:jack_simple_client", + "cmd:jack_simple_session_client", + "cmd:jack_test", + "cmd:jack_thru", + "cmd:jack_transport", + "cmd:jack_unload", + "cmd:jack_wait", + "cmd:jack_zombie" + ] + }, + "libtirpc": { + "versions": { + "1.0.1-r2": 1509469768 + }, + "origin": "libtirpc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgssapi_krb5.so.2" + ], + "provides": [ + "so:libtirpc.so.3=3.0.0" + ] + }, + "bash-completion": { + "versions": { + "2.7-r3": 1510573048 + }, + "origin": "bash-completion", + "dependencies": [ + "bash" + ] + }, + "perl-cpan-meta-check": { + "versions": { + "0.013-r0": 1509468486 + }, + "origin": "perl-cpan-meta-check", + "dependencies": [ + "perl-module-metadata>=1.000023" + ] + }, + "freeswitch-sounds-en-us-callie-8000": { + "versions": { + "1.0.51-r0": 1509476708 + }, + "origin": "freeswitch-sounds-en-us-callie-8000" + }, + "menu-cache": { + "versions": { + "0.5.1-r1": 1509469822 + }, + "origin": "menu-cache", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libmenu-cache.so.3=3.0.1" + ] + }, + "mesa-dev": { + "versions": { + "17.2.4-r1": 1510741944 + }, + "origin": "mesa", + "dependencies": [ + "libdrm-dev", + "dri2proto", + "libxext-dev", + "libxdamage-dev", + "libxcb-dev", + "glproto", + "dri3proto", + "presentproto", + "libxshmfence-dev", + "mesa-egl=17.2.4-r1", + "mesa-gbm=17.2.4-r1", + "mesa-gl=17.2.4-r1", + "mesa-glapi=17.2.4-r1", + "mesa-gles=17.2.4-r1", + "mesa-libwayland-egl=17.2.4-r1", + "mesa-osmesa=17.2.4-r1", + "mesa-xatracker=17.2.4-r1", + "pc:libdrm>=2.4.75", + "pc:wayland-client", + "pc:x11", + "pc:x11-xcb", + "pc:xcb", + "pc:xcb-dri2>=1.8", + "pc:xcb-glx>=1.8.1", + "pc:xdamage>=1.1", + "pc:xext", + "pc:xfixes", + "pc:xxf86vm", + "pkgconfig" + ], + "provides": [ + "pc:dri=17.2.4", + "pc:egl=17.2.4", + "pc:gbm=17.2.4", + "pc:gl=17.2.4", + "pc:glesv1_cm=17.2.4", + "pc:glesv2=17.2.4", + "pc:osmesa=8", + "pc:wayland-egl=17.2.4", + "pc:xatracker=2.3.0" + ] + }, + "jansson-dev": { + "versions": { + "2.10-r0": 1509472128 + }, + "origin": "jansson", + "dependencies": [ + "jansson=2.10-r0", + "pkgconfig" + ], + "provides": [ + "pc:jansson=2.10" + ] + }, + "hexchat": { + "versions": { + "2.12.4-r1": 1510260816 + }, + "origin": "hexchat", + "dependencies": [ + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libdbus-glib-1.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:liblua-5.3.so.0", + "so:libnotify.so.4", + "so:libpango-1.0.so.0", + "so:libproxy.so.1", + "so:libssl.so.44" + ], + "provides": [ + "pc:hexchat-plugin=2.12.4", + "cmd:hexchat" + ] + }, + "fftw-long-double-libs": { + "versions": { + "3.3.6p2-r0": 1509468830 + }, + "origin": "fftw", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfftw3l.so.3=3.5.6", + "so:libfftw3l_threads.so.3=3.5.6" + ] + }, + "terminus-font": { + "versions": { + "4.46-r0": 1509491617 + }, + "origin": "terminus-font" + }, + "py-click": { + "versions": { + "6.7-r2": 1509476499 + }, + "origin": "py-click" + }, + "perl-cgi-psgi-doc": { + "versions": { + "0.15-r1": 1510564479 + }, + "origin": "perl-cgi-psgi" + }, + "freeradius-checkrad": { + "versions": { + "3.0.15-r4": 1556202797, + "3.0.15-r5": 1566310605 + }, + "origin": "freeradius", + "dependencies": [ + "perl", + "perl-net-telnet", + "perl-snmp-session", + "net-snmp-tools" + ], + "provides": [ + "cmd:checkrad" + ] + }, + "ir_keytable": { + "versions": { + "1.12.5-r1": 1510072047 + }, + "origin": "v4l-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8" + ], + "provides": [ + "cmd:ir-keytable" + ] + }, + "rrdtool-cached": { + "versions": { + "1.5.6-r3": 1512296349 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:librrd_th.so.4" + ], + "provides": [ + "cmd:rrdcached" + ] + }, + "gstreamer": { + "versions": { + "1.12.3-r0": 1509470920 + }, + "origin": "gstreamer", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libgstbase-1.0.so.0=0.1203.0", + "so:libgstcheck-1.0.so.0=0.1203.0", + "so:libgstcontroller-1.0.so.0=0.1203.0", + "so:libgstnet-1.0.so.0=0.1203.0", + "so:libgstreamer-1.0.so.0=0.1203.0" + ] + }, + "perl-html-mason-doc": { + "versions": { + "1.58-r0": 1509470628 + }, + "origin": "perl-html-mason" + }, + "libconfig": { + "versions": { + "1.5-r3": 1509706433 + }, + "origin": "libconfig", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libconfig.so.9=9.2.0" + ] + }, + "libnfnetlink-dev": { + "versions": { + "1.0.1-r1": 1509469209 + }, + "origin": "libnfnetlink", + "dependencies": [ + "libnfnetlink=1.0.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:libnfnetlink=1.0.1" + ] + }, + "libassuan-dev": { + "versions": { + "2.4.4-r0": 1512029943 + }, + "origin": "libassuan", + "dependencies": [ + "libassuan=2.4.4-r0" + ], + "provides": [ + "cmd:libassuan-config" + ] + }, + "py-jwt": { + "versions": { + "1.5.0-r1": 1509552755 + }, + "origin": "py-jwt" + }, + "lua-md5": { + "versions": { + "1.2-r3": 1509468361 + }, + "origin": "lua-md5" + }, + "lsyncd": { + "versions": { + "2.2.2-r0": 1509493062 + }, + "origin": "lsyncd", + "dependencies": [ + "rsync", + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ], + "provides": [ + "cmd:lsyncd" + ] + }, + "fakeroot-doc": { + "versions": { + "1.21-r1": 1509459622 + }, + "origin": "fakeroot" + }, + "readline": { + "versions": { + "7.0.003-r0": 1509456790 + }, + "origin": "readline", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "so:libreadline.so.7=7.0" + ] + }, + "libquadmath": { + "versions": { + "6.4.0-r5": 1509458072 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libquadmath.so.0=0.0.0" + ] + }, + "perl-set-intspan-doc": { + "versions": { + "1.19-r0": 1509480155 + }, + "origin": "perl-set-intspan" + }, + "gst-plugins-base-lang": { + "versions": { + "1.12.3-r0": 1510070597 + }, + "origin": "gst-plugins-base" + }, + "perl-getopt-long": { + "versions": { + "2.50-r0": 1509491105 + }, + "origin": "perl-getopt-long" + }, + "spice-gtk-tools": { + "versions": { + "0.34-r1": 1510260978 + }, + "origin": "spice-gtk", + "dependencies": [ + "so:libacl.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgtk-3.so.0", + "so:libpolkit-gobject-1.so.0", + "so:libspice-client-glib-2.0.so.8", + "so:libspice-client-gtk-3.0.so.5" + ], + "provides": [ + "cmd:spice-client-glib-usb-acl-helper", + "cmd:spicy", + "cmd:spicy-screenshot", + "cmd:spicy-stats" + ] + }, + "patchutils": { + "versions": { + "0.3.4-r0": 1509495754 + }, + "origin": "patchutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:combinediff", + "cmd:dehtmldiff", + "cmd:editdiff", + "cmd:espdiff", + "cmd:filterdiff", + "cmd:fixcvsdiff", + "cmd:flipdiff", + "cmd:grepdiff", + "cmd:interdiff", + "cmd:lsdiff", + "cmd:recountdiff", + "cmd:rediff", + "cmd:splitdiff", + "cmd:unwrapdiff" + ] + }, + "py-gtk-dev": { + "versions": { + "2.24.0-r14": 1510071866 + }, + "origin": "py-gtk", + "dependencies": [ + "py-gobject-dev", + "py-gtk", + "pc:gtk+-2.0", + "pc:pygobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:pygtk-2.0=2.24.0" + ] + }, + "nagios-plugins-time": { + "versions": { + "2.2.1-r3": 1510288498 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "poppler": { + "versions": { + "0.56.0-r0": 1509465068, + "0.56.0-r1": 1569334548 + }, + "origin": "poppler", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libopenjp2.so.7", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libtiff.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libpoppler-cpp.so.0=0.3.0", + "so:libpoppler.so.67=67.0.0" + ] + }, + "perl-scope-guard-doc": { + "versions": { + "0.21-r0": 1509482409 + }, + "origin": "perl-scope-guard" + }, + "obex-data-server-doc": { + "versions": { + "0.4.6-r4": 1510071752 + }, + "origin": "obex-data-server" + }, + "clang-static": { + "versions": { + "5.0.0-r0": 1510683246 + }, + "origin": "clang" + }, + "awall-masquerade": { + "versions": { + "1.5.0-r0": 1509742667 + }, + "origin": "awall", + "dependencies": [ + "awall" + ] + }, + "aoetools-doc": { + "versions": { + "37-r0": 1509477685 + }, + "origin": "aoetools" + }, + "libconfig-dev": { + "versions": { + "1.5-r3": 1509706433 + }, + "origin": "libconfig", + "dependencies": [ + "libconfig++=1.5-r3", + "libconfig=1.5-r3", + "pkgconfig" + ], + "provides": [ + "pc:libconfig++=1.5", + "pc:libconfig=1.5" + ] + }, + "qemu-system-ppcemb": { + "versions": { + "2.10.1-r3": 1519746243 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-ppcemb" + ] + }, + "lua5.2-ldbus": { + "versions": { + "20150430-r2": 1509492576 + }, + "origin": "lua-ldbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ] + }, + "nagios-web": { + "versions": { + "3.5.1-r4": 1509494203 + }, + "origin": "nagios", + "dependencies": [ + "perl" + ] + }, + "expat": { + "versions": { + "2.2.5-r0": 1509908122, + "2.2.7-r0": 1561897528, + "2.2.7-r1": 1568353727, + "2.2.8-r0": 1568974114 + }, + "origin": "expat", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libexpat.so.1=1.6.10", + "cmd:xmlwf" + ] + }, + "bluez-doc": { + "versions": { + "5.47-r3": 1510069786 + }, + "origin": "bluez" + }, + "xdpyinfo-doc": { + "versions": { + "1.3.2-r0": 1509491067 + }, + "origin": "xdpyinfo" + }, + "perl-test-manifest": { + "versions": { + "2.02-r0": 1509471896 + }, + "origin": "perl-test-manifest" + }, + "perl-io-stringy-doc": { + "versions": { + "2.111-r0": 1509469879 + }, + "origin": "perl-io-stringy" + }, + "libpaper-dev": { + "versions": { + "1.1.24-r3": 1509465358 + }, + "origin": "libpaper", + "dependencies": [ + "libpaper=1.1.24-r3" + ] + }, + "iperf3-doc": { + "versions": { + "3.2-r0": 1509475695 + }, + "origin": "iperf3" + }, + "perl-class-data-inheritable": { + "versions": { + "0.08-r0": 1509470594 + }, + "origin": "perl-class-data-inheritable" + }, + "lua5.3-pty": { + "versions": { + "1.2.1-r1": 1509496424 + }, + "origin": "lua-pty", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-idna": { + "versions": { + "2.6-r0": 1509476381 + }, + "origin": "py-idna", + "dependencies": [ + "python2" + ] + }, + "jpeg": { + "versions": { + "8-r6": 1509469923 + }, + "origin": "jpeg", + "dependencies": [ + "libjpeg-turbo-utils" + ] + }, + "perl-css-squish-doc": { + "versions": { + "0.10-r0": 1509470429 + }, + "origin": "perl-css-squish" + }, + "libressl2.6-libcrypto": { + "versions": { + "2.6.5-r0": 1529043883 + }, + "origin": "libressl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcrypto.so.42=42.0.0" + ] + }, + "gcc-gnat": { + "versions": { + "6.4.0-r5": 1509458081 + }, + "origin": "gcc", + "dependencies": [ + "gcc=6.4.0-r5", + "libgnat=6.4.0-r5", + "libgnat=6.4.0-r5", + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libisl.so.15", + "so:libmpc.so.3", + "so:libmpfr.so.4", + "so:libz.so.1" + ], + "provides": [ + "cmd:gnat", + "cmd:gnatbind", + "cmd:gnatchop", + "cmd:gnatclean", + "cmd:gnatfind", + "cmd:gnatkr", + "cmd:gnatlink", + "cmd:gnatls", + "cmd:gnatmake", + "cmd:gnatname", + "cmd:gnatprep", + "cmd:gnatxref" + ] + }, + "cgdb": { + "versions": { + "0.7.0-r1": 1509495074 + }, + "origin": "cgdb", + "dependencies": [ + "gdb", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libreadline.so.7", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:cgdb" + ] + }, + "gnome-doc-utils-doc": { + "versions": { + "0.20.10-r2": 1509627733 + }, + "origin": "gnome-doc-utils" + }, + "perl-test-leaktrace-doc": { + "versions": { + "0.15-r3": 1509474016 + }, + "origin": "perl-test-leaktrace" + }, + "conky": { + "versions": { + "1.10.6-r0": 1512047603 + }, + "origin": "conky", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libgcc_s.so.1", + "so:liblua-5.2.so.0", + "so:libncursesw.so.6", + "so:libstdc++.so.6", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:conky" + ] + }, + "gpsd-dev": { + "versions": { + "3.16-r3": 1510311401 + }, + "origin": "gpsd", + "dependencies": [ + "gpsd=3.16-r3" + ] + }, + "perl-test-mockrandom-doc": { + "versions": { + "1.01-r1": 1510855674 + }, + "origin": "perl-test-mockrandom" + }, + "llvm5-libs": { + "versions": { + "5.0.0-r0": 1510682601 + }, + "origin": "llvm5", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "llvm-libs=5.0.0-r0", + "so:libLLVM-5.0.so=0" + ] + }, + "vde2-doc": { + "versions": { + "2.3.2-r8": 1510260476 + }, + "origin": "vde2" + }, + "libxscrnsaver-doc": { + "versions": { + "1.2.2-r1": 1509480774 + }, + "origin": "libxscrnsaver" + }, + "speexdsp": { + "versions": { + "1.2_rc3-r4": 1509473234 + }, + "origin": "speexdsp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libspeexdsp.so.1=1.5.0" + ] + }, + "lua5.2-crypto": { + "versions": { + "0.3.2-r5": 1510261400 + }, + "origin": "lua-crypto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "libxxf86dga-doc": { + "versions": { + "1.1.4-r1": 1509473787 + }, + "origin": "libxxf86dga" + }, + "py-django-sorl-thumbnail": { + "versions": { + "12.4.1-r0": 1517313676 + }, + "origin": "py-django-sorl-thumbnail", + "dependencies": [ + "py-django", + "py-pillow" + ] + }, + "nagios-plugins-dbi": { + "versions": { + "2.2.1-r3": 1510288494 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libdbi.so.1" + ] + }, + "openldap-overlay-collect": { + "versions": { + "2.4.45-r3": 1510258134, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "dri2proto": { + "versions": { + "2.8-r2": 1509466228 + }, + "origin": "dri2proto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:dri2proto=2.8" + ] + }, + "byacc-doc": { + "versions": { + "20170709-r0": 1510310594 + }, + "origin": "byacc" + }, + "libva-intel-driver": { + "versions": { + "1.7.3-r0": 1510073939 + }, + "origin": "libva-intel-driver", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_intel.so.1" + ] + }, + "perl-crypt-des-doc": { + "versions": { + "2.07-r4": 1509471888 + }, + "origin": "perl-crypt-des" + }, + "udns-dev": { + "versions": { + "0.4-r0": 1509491388 + }, + "origin": "udns", + "dependencies": [ + "udns=0.4-r0" + ] + }, + "json-c-static": { + "versions": { + "0.12.1-r1": 1509466320 + }, + "origin": "json-c" + }, + "libxft-doc": { + "versions": { + "2.3.2-r2": 1509462101 + }, + "origin": "libxft" + }, + "devicemaster-linux-doc": { + "versions": { + "7.15-r0": 1509481942 + }, + "origin": "devicemaster-linux" + }, + "libgssglue-doc": { + "versions": { + "0.4-r0": 1509479796 + }, + "origin": "libgssglue" + }, + "perl-net-libidn": { + "versions": { + "0.12-r3": 1509468972 + }, + "origin": "perl-net-libidn", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libidn.so.11" + ] + }, + "ruby-io-console": { + "versions": { + "2.4.6-r0": 1557166822 + }, + "origin": "ruby", + "dependencies": [ + "ruby", + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.4" + ] + }, + "bacula-client": { + "versions": { + "9.0.5-r3": 1512992127 + }, + "origin": "bacula", + "dependencies": [ + "so:libacl.so.1", + "so:libbac-9.0.5.so", + "so:libbaccfg-9.0.5.so", + "so:libbacfind-9.0.5.so", + "so:libc.musl-x86_64.so.1", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:bacula", + "cmd:bacula-fd", + "cmd:bconsole", + "cmd:btraceback" + ] + }, + "mupdf-dev": { + "versions": { + "1.13.0-r0": 1533746007 + }, + "origin": "mupdf", + "dependencies": [ + "mupdf=1.13.0-r0" + ] + }, + "thunar-vcs-plugin": { + "versions": { + "0.1.4-r6": 1510314594 + }, + "origin": "thunar-vcs-plugin", + "dependencies": [ + "so:libapr-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libsvn_client-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_subr-1.so.0", + "so:libsvn_wc-1.so.0" + ] + }, + "fribidi": { + "versions": { + "0.19.7-r0": 1509480347 + }, + "origin": "fribidi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfribidi.so.0=0.3.6", + "cmd:fribidi" + ] + }, + "newt-lang": { + "versions": { + "0.52.20-r0": 1509476146 + }, + "origin": "newt" + }, + "txt2man-doc": { + "versions": { + "1.6.0-r0": 1509475653 + }, + "origin": "txt2man" + }, + "nmap": { + "versions": { + "7.60-r2": 1510261468, + "7.60-r3": 1572297245 + }, + "origin": "nmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libgcc_s.so.1", + "so:libpcap.so.1", + "so:libssl.so.44", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:nmap" + ] + }, + "memcached-dev": { + "versions": { + "1.5.6-r0": 1520365888 + }, + "origin": "memcached", + "dependencies": [ + "memcached=1.5.6-r0" + ] + }, + "perl-http-cookies": { + "versions": { + "6.04-r0": 1511871378 + }, + "origin": "perl-http-cookies", + "dependencies": [ + "perl", + "perl-http-date", + "perl-http-message" + ] + }, + "perl-params-validationcompiler": { + "versions": { + "0.24-r1": 1510588893 + }, + "origin": "perl-params-validationcompiler", + "dependencies": [ + "perl-specio", + "perl-test2-suite", + "perl-test-simple", + "perl-test-without-module", + "perl-eval-closure", + "perl-exception-class", + "perl-test2-plugin-nowarnings", + "perl-role-tiny", + "perl-mro-compat" + ] + }, + "mesa-xatracker": { + "versions": { + "17.2.4-r1": 1510741947 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libxatracker.so.2=2.3.0" + ] + }, + "perl-devel-stacktrace-doc": { + "versions": { + "2.02-r0": 1509468384 + }, + "origin": "perl-devel-stacktrace" + }, + "py3-asn1": { + "versions": { + "0.4.2-r0": 1511889291 + }, + "origin": "py-asn1", + "dependencies": [ + "python3" + ] + }, + "libcrypto1.0": { + "versions": { + "1.0.2r-r0": 1552814668, + "1.0.2t-r0": 1568300415 + }, + "origin": "openssl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libcrypto.so.1.0.0=1.0.0" + ] + }, + "icecast": { + "versions": { + "2.4.4-r0": 1541491284 + }, + "origin": "icecast", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libogg.so.0", + "so:libssl.so.44", + "so:libtheora.so.0", + "so:libvorbis.so.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ], + "provides": [ + "cmd:icecast" + ] + }, + "lvm2-libs": { + "versions": { + "2.02.175-r0": 1509462402 + }, + "origin": "lvm2", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper-event.so.1.02", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "so:libdevmapper-event-lvm2.so.2.02=2.02", + "so:liblvm2app.so.2.2=2.2", + "so:liblvm2cmd.so.2.02=2.02" + ] + }, + "xf86-video-i128": { + "versions": { + "1.3.6-r8": 1510074908 + }, + "origin": "xf86-video-i128", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gnuchess": { + "versions": { + "6.2.5-r0": 1509496082 + }, + "origin": "gnuchess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:gnuchess", + "cmd:gnuchessu", + "cmd:gnuchessx" + ] + }, + "fixesproto": { + "versions": { + "5.0-r2": 1509464679 + }, + "origin": "fixesproto", + "dependencies": [ + "xextproto", + "pc:xextproto>=7.0.99.1", + "pkgconfig" + ], + "provides": [ + "pc:fixesproto=5.0" + ] + }, + "darkhttpd": { + "versions": { + "1.12-r2": 1526305370 + }, + "origin": "darkhttpd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:darkhttpd" + ] + }, + "rsyslog-tls": { + "versions": { + "8.31.0-r0": 1512031980, + "8.31.0-r1": 1571767017 + }, + "origin": "rsyslog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30" + ] + }, + "py3-django": { + "versions": { + "1.11.20-r0": 1551365037, + "1.11.21-r0": 1561497063, + "1.11.22-r0": 1563783118, + "1.11.23-r0": 1565087799 + }, + "origin": "py-django", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:django-admin-3" + ] + }, + "dropbear-doc": { + "versions": { + "2018.76-r2": 1537439117 + }, + "origin": "dropbear" + }, + "samba-server": { + "versions": { + "4.7.6-r3": 1555491788 + }, + "origin": "samba", + "dependencies": [ + "samba-common=4.7.6-r3", + "samba-initscript=4.7.6-r3", + "so:libCHARSET3-samba4.so", + "so:libauth-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libdbwrap-samba4.so", + "so:libgenrand-samba4.so", + "so:libgse-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmessages-dgm-samba4.so", + "so:libmsghdr-samba4.so", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libreplace-samba4.so", + "so:libsamba-cluster-support-samba4.so", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbd-shim-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-reg-samba4.so", + "so:libutil-tdb-samba4.so" + ], + "provides": [ + "so:libxattr-tdb-samba4.so=0", + "cmd:eventlogadm", + "cmd:nmbd", + "cmd:smbd", + "cmd:smbstatus" + ] + }, + "pcre": { + "versions": { + "8.41-r1": 1509464278 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre.so.1=1.2.9", + "so:libpcreposix.so.0=0.0.5" + ] + }, + "ntfs-3g-dev": { + "versions": { + "2017.3.23-r1": 1509489437 + }, + "origin": "ntfs-3g", + "dependencies": [ + "ntfs-3g-libs=2017.3.23-r1", + "pkgconfig" + ], + "provides": [ + "pc:libntfs-3g=2017.3.23" + ] + }, + "ssl_client": { + "versions": { + "1.27.2-r11": 1528276162 + }, + "origin": "busybox", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtls.so.16" + ], + "provides": [ + "cmd:ssl_client" + ] + }, + "s6-networking-doc": { + "versions": { + "2.3.0.2-r1": 1510260983 + }, + "origin": "s6-networking" + }, + "git": { + "versions": { + "2.15.3-r0": 1540401303, + "2.15.4-r0": 1576015212 + }, + "origin": "git", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libexpat.so.1", + "so:libpcre2-8.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:git", + "cmd:git-receive-pack", + "cmd:git-shell", + "cmd:git-upload-archive", + "cmd:git-upload-pack" + ] + }, + "perl-io-captureoutput": { + "versions": { + "1.1104-r0": 1509489308 + }, + "origin": "perl-io-captureoutput" + }, + "cryptsetup": { + "versions": { + "1.7.5-r1": 1510261295 + }, + "origin": "cryptsetup", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcryptsetup.so.4", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:cryptsetup", + "cmd:veritysetup" + ] + }, + "collectd-disk": { + "versions": { + "5.7.2-r0": 1510069652 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ] + }, + "intltool": { + "versions": { + "0.51.0-r4": 1509464452 + }, + "origin": "intltool", + "dependencies": [ + "perl", + "perl-xml-parser", + "gettext", + "file" + ], + "provides": [ + "cmd:intltool-extract", + "cmd:intltool-merge", + "cmd:intltool-prepare", + "cmd:intltool-update", + "cmd:intltoolize" + ] + }, + "libxfce4ui-dev": { + "versions": { + "4.12.1-r3": 1510067993 + }, + "origin": "libxfce4ui", + "dependencies": [ + "startup-notification-dev", + "libxfce4ui-gtk3=4.12.1-r3", + "libxfce4ui=4.12.1-r3", + "pc:gdk-2.0", + "pc:gdk-3.0", + "pc:gtk+-2.0", + "pc:gtk+-3.0", + "pc:libxfce4util-1.0", + "pc:libxfconf-0", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libgladeui-1.so.11", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libxfce4ui-1.so.0" + ], + "provides": [ + "pc:libxfce4kbd-private-2=4.12.1", + "pc:libxfce4kbd-private-3=4.12.1", + "pc:libxfce4ui-1=4.12.1", + "pc:libxfce4ui-2=4.12.1" + ] + }, + "asterisk-speex": { + "versions": { + "15.6.1-r0": 1537795343, + "15.6.2-r0": 1568705005 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libspeex.so.1", + "so:libspeexdsp.so.1" + ] + }, + "udns-doc": { + "versions": { + "0.4-r0": 1509491393 + }, + "origin": "udns" + }, + "perl-test2-suite": { + "versions": { + "0.000083-r0": 1510564541 + }, + "origin": "perl-test2-suite", + "dependencies": [ + "perl-test-simple", + "perl-importer", + "perl-term-table", + "perl-sub-info" + ] + }, + "gst-plugins-good-doc": { + "versions": { + "1.12.3-r0": 1510075045 + }, + "origin": "gst-plugins-good" + }, + "boost-program_options": { + "versions": { + "1.62.0-r5": 1509465877 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_program_options-mt.so.1.62.0=1.62.0", + "so:libboost_program_options.so.1.62.0=1.62.0" + ] + }, + "dmidecode": { + "versions": { + "3.1-r0": 1509482141 + }, + "origin": "dmidecode", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:biosdecode", + "cmd:dmidecode", + "cmd:ownership", + "cmd:vpddecode" + ] + }, + "alpine": { + "versions": { + "2.21-r1": 1510259111 + }, + "origin": "alpine", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libncursesw.so.6", + "so:libssl.so.44" + ], + "provides": [ + "cmd:alpine", + "cmd:pico", + "cmd:pilot", + "cmd:rpdump", + "cmd:rpload" + ] + }, + "pcre-tools": { + "versions": { + "8.41-r1": 1509464277 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1", + "so:libpcre16.so.0", + "so:libpcre32.so.0", + "so:libpcreposix.so.0" + ], + "provides": [ + "cmd:pcregrep", + "cmd:pcretest" + ] + }, + "videoproto": { + "versions": { + "2.3.3-r1": 1509467165 + }, + "origin": "videoproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:videoproto=2.3.3" + ] + }, + "gtk-engines-mist": { + "versions": { + "2.21.0-r2": 1510071723 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "libmng-doc": { + "versions": { + "2.0.3-r1": 1509480914 + }, + "origin": "libmng" + }, + "dconf": { + "versions": { + "0.26.0-r1": 1509492217 + }, + "origin": "dconf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libdconf.so.1=1.0.0", + "cmd:dconf" + ] + }, + "zfs-udev": { + "versions": { + "0.7.3-r0": 1509490074 + }, + "origin": "zfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "acf-openntpd": { + "versions": { + "0.9.0-r2": 1510073454 + }, + "origin": "acf-openntpd", + "dependencies": [ + "acf-core", + "openntpd" + ] + }, + "libestr": { + "versions": { + "0.1.10-r0": 1509486154 + }, + "origin": "libestr", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libestr.so.0=0.0.0" + ] + }, + "subversion-libs": { + "versions": { + "1.9.7-r0": 1510314504, + "1.9.12-r0": 1565086191 + }, + "origin": "subversion", + "dependencies": [ + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:libexpat.so.1", + "so:libsasl2.so.3", + "so:libserf-1.so.1", + "so:libsqlite3.so.0", + "so:libz.so.1" + ], + "provides": [ + "so:libsvn_client-1.so.0=0.0.0", + "so:libsvn_delta-1.so.0=0.0.0", + "so:libsvn_diff-1.so.0=0.0.0", + "so:libsvn_fs-1.so.0=0.0.0", + "so:libsvn_fs_base-1.so.0=0.0.0", + "so:libsvn_fs_fs-1.so.0=0.0.0", + "so:libsvn_fs_util-1.so.0=0.0.0", + "so:libsvn_fs_x-1.so.0=0.0.0", + "so:libsvn_ra-1.so.0=0.0.0", + "so:libsvn_ra_local-1.so.0=0.0.0", + "so:libsvn_ra_serf-1.so.0=0.0.0", + "so:libsvn_ra_svn-1.so.0=0.0.0", + "so:libsvn_repos-1.so.0=0.0.0", + "so:libsvn_subr-1.so.0=0.0.0", + "so:libsvn_wc-1.so.0=0.0.0" + ] + }, + "cdparanoia-dev": { + "versions": { + "10.2-r7": 1509471028 + }, + "origin": "cdparanoia", + "dependencies": [ + "cdparanoia-libs=10.2-r7" + ] + }, + "tumbler-lang": { + "versions": { + "0.2.0-r0": 1510075180 + }, + "origin": "tumbler" + }, + "py-tz": { + "versions": { + "2017.3-r0": 1510732132 + }, + "origin": "py-tz" + }, + "ldns-tools": { + "versions": { + "1.6.17-r6": 1510258937 + }, + "origin": "ldns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libldns.so.1", + "so:libssl.so.44" + ], + "provides": [ + "cmd:ldns-chaos", + "cmd:ldns-compare-zones", + "cmd:ldns-dane", + "cmd:ldns-dpa", + "cmd:ldns-gen-zone", + "cmd:ldns-key2ds", + "cmd:ldns-keyfetcher", + "cmd:ldns-keygen", + "cmd:ldns-mx", + "cmd:ldns-notify", + "cmd:ldns-nsec3-hash", + "cmd:ldns-read-zone", + "cmd:ldns-resolver", + "cmd:ldns-revoke", + "cmd:ldns-rrsig", + "cmd:ldns-signzone", + "cmd:ldns-test-edns", + "cmd:ldns-testns", + "cmd:ldns-update", + "cmd:ldns-verify-zone", + "cmd:ldns-version", + "cmd:ldns-walk", + "cmd:ldns-zcat", + "cmd:ldns-zsplit", + "cmd:ldnsd" + ] + }, + "upower-dev": { + "versions": { + "0.99.6-r0": 1510068213 + }, + "origin": "upower", + "dependencies": [ + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pkgconfig", + "upower=0.99.6-r0" + ], + "provides": [ + "pc:upower-glib=0.99.6" + ] + }, + "nagios-plugins-swap": { + "versions": { + "2.2.1-r3": 1510288498 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "dconf-doc": { + "versions": { + "0.26.0-r1": 1509492217 + }, + "origin": "dconf" + }, + "pinentry": { + "versions": { + "1.0.0-r0": 1510587945 + }, + "origin": "pinentry", + "dependencies": [ + "/bin/sh", + "so:libassuan.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libgpg-error.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:pinentry-curses" + ] + }, + "drbd9-hardened": { + "versions": { + "4.9.65-r1": 1511799284 + }, + "origin": "drbd9-hardened", + "dependencies": [ + "linux-hardened=4.9.65-r1" + ], + "provides": [ + "drbd9-grsec=4.9.65-r1" + ] + }, + "libev": { + "versions": { + "4.24-r0": 1509469569 + }, + "origin": "libev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libev.so.4=4.0.0" + ] + }, + "libpurple": { + "versions": { + "2.12.0-r2": 1510069758 + }, + "origin": "pidgin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libidn.so.11", + "so:libintl.so.8", + "so:libnspr4.so", + "so:libnss3.so", + "so:libsasl2.so.3", + "so:libsmime3.so", + "so:libssl3.so", + "so:libxml2.so.2" + ], + "provides": [ + "so:autoaccept.so=0", + "so:buddynote.so=0", + "so:idle.so=0", + "so:joinpart.so=0", + "so:libgg.so=0", + "so:libirc.so=0", + "so:libnovell.so=0", + "so:libpurple.so.0=0.12.0", + "so:libsimple.so=0", + "so:libzephyr.so=0", + "so:log_reader.so=0", + "so:newline.so=0", + "so:nss-prefs.so=0", + "so:offlinemsg.so=0", + "so:psychic.so=0", + "so:ssl-nss.so=0", + "so:ssl.so=0", + "so:statenotify.so=0" + ] + }, + "lua5.3-socket": { + "versions": { + "3.0_rc1_git20160306-r2": 1509480762 + }, + "origin": "lua-socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "sems-xmlrpc2di": { + "versions": { + "1.6.0-r6": 1510260896 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libssl.so.44", + "so:libstdc++.so.6" + ] + }, + "atkmm": { + "versions": { + "2.24.2-r0": 1509483360 + }, + "origin": "atkmm", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libglibmm-2.4.so.1", + "so:libgobject-2.0.so.0", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libatkmm-1.6.so.1=1.1.0" + ] + }, + "unionfs-fuse-doc": { + "versions": { + "1.0-r0": 1509489287 + }, + "origin": "unionfs-fuse" + }, + "ser2net": { + "versions": { + "3.4-r1": 1509492428 + }, + "origin": "ser2net", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ser2net" + ] + }, + "perl-http-daemon-doc": { + "versions": { + "6.01-r1": 1509464384 + }, + "origin": "perl-http-daemon" + }, + "fribidi-doc": { + "versions": { + "0.19.7-r0": 1509480345 + }, + "origin": "fribidi" + }, + "dnsmasq-doc": { + "versions": { + "2.78-r3": 1540537793 + }, + "origin": "dnsmasq" + }, + "perl-scope-upper": { + "versions": { + "0.30-r0": 1509952235 + }, + "origin": "perl-scope-upper", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-json-maybexs-doc": { + "versions": { + "1.003009-r0": 1510352918 + }, + "origin": "perl-json-maybexs" + }, + "lua-lyaml": { + "versions": { + "6.1.3-r1": 1509473486 + }, + "origin": "lua-lyaml" + }, + "dbus-glib": { + "versions": { + "0.108-r1": 1509465372 + }, + "origin": "dbus-glib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libexpat.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libdbus-glib-1.so.2=2.3.3", + "cmd:dbus-binding-tool" + ] + }, + "garcon-doc": { + "versions": { + "0.6.1-r1": 1510068108 + }, + "origin": "garcon" + }, + "help2man": { + "versions": { + "1.47.5-r0": 1509456690 + }, + "origin": "help2man", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:help2man" + ] + }, + "perl-config-inifiles": { + "versions": { + "2.94-r0": 1509489532 + }, + "origin": "perl-config-inifiles", + "dependencies": [ + "perl-list-moreutils" + ] + }, + "py3-docutils": { + "versions": { + "0.13.1-r0": 1509486245 + }, + "origin": "py-docutils", + "dependencies": [ + "py3-pillow", + "py3-roman", + "python3" + ], + "provides": [ + "cmd:rst2html-3", + "cmd:rst2html5-3", + "cmd:rst2latex-3", + "cmd:rst2man-3", + "cmd:rst2odt-3", + "cmd:rst2odt_prepstyles-3", + "cmd:rst2pseudoxml-3", + "cmd:rst2s5-3", + "cmd:rst2xetex-3", + "cmd:rst2xml-3", + "cmd:rstpep2html-3" + ] + }, + "ntfs-3g": { + "versions": { + "2017.3.23-r1": 1509489439 + }, + "origin": "ntfs-3g", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libntfs-3g.so.88" + ], + "provides": [ + "cmd:lowntfs-3g", + "cmd:ntfs-3g" + ] + }, + "vte-dev": { + "versions": { + "0.28.2-r13": 1510071902 + }, + "origin": "vte", + "dependencies": [ + "pango-dev", + "gtk+2.0-dev", + "pc:cairo-xlib", + "pc:gio-2.0", + "pc:gio-unix-2.0", + "pc:glib-2.0>=2.26.0", + "pc:gobject-2.0", + "pc:gtk+-2.0>=2.20.0", + "pc:pango>=1.22.0", + "pc:x11", + "pkgconfig", + "vte=0.28.2-r13" + ], + "provides": [ + "pc:pyvte=0.28.2", + "pc:vte=0.28.2" + ] + }, + "uwsgi-zergpool": { + "versions": { + "2.0.17-r0": 1522154661 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "hostapd": { + "versions": { + "2.6-r3": 1510261213, + "2.6-r4": 1559723195, + "2.6-r5": 1559724985, + "2.6-r6": 1568724303 + }, + "origin": "hostapd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libnl-3.so.200", + "so:libnl-genl-3.so.200", + "so:libssl.so.44" + ], + "provides": [ + "cmd:hostapd", + "cmd:hostapd_cli", + "cmd:nt_password_hash" + ] + }, + "giflib-utils": { + "versions": { + "5.1.4-r1": 1509470481 + }, + "origin": "giflib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgif.so.7" + ], + "provides": [ + "cmd:gif2rgb", + "cmd:gifbuild", + "cmd:gifclrmp", + "cmd:gifecho", + "cmd:giffix", + "cmd:gifinto", + "cmd:giftext", + "cmd:giftool" + ] + }, + "libasr-doc": { + "versions": { + "1.0.2-r6": 1510258974 + }, + "origin": "libasr" + }, + "xf86-video-apm": { + "versions": { + "1.2.5-r8": 1510070495 + }, + "origin": "xf86-video-apm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "msmtp": { + "versions": { + "1.6.6-r2": 1510261496 + }, + "origin": "msmtp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:msmtp" + ] + }, + "apache-mod-fcgid": { + "versions": { + "2.3.9-r1": 1509492409 + }, + "origin": "apache-mod-fcgid", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1" + ] + }, + "scrnsaverproto-doc": { + "versions": { + "1.2.2-r2": 1509473902 + }, + "origin": "scrnsaverproto" + }, + "itstool-doc": { + "versions": { + "2.0.4-r3": 1511265097 + }, + "origin": "itstool" + }, + "perl-devel-checkbin-doc": { + "versions": { + "0.02-r0": 1509474054 + }, + "origin": "perl-devel-checkbin" + }, + "dovecot-gssapi": { + "versions": { + "2.2.36.3-r0": 1554102390, + "2.2.36.4-r0": 1567075099 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot", + "so:libc.musl-x86_64.so.1", + "so:libgssapi.so.3" + ] + }, + "perl-test-requires": { + "versions": { + "0.10-r0": 1509470573 + }, + "origin": "perl-test-requires" + }, + "unixodbc-doc": { + "versions": { + "2.3.4-r2": 1509469609 + }, + "origin": "unixodbc" + }, + "py2-requests": { + "versions": { + "2.18.4-r0": 1509483042 + }, + "origin": "py-requests", + "dependencies": [ + "py2-chardet", + "py2-idna", + "py2-certifi", + "py2-urllib3", + "python2" + ] + }, + "cjdns": { + "versions": { + "20-r1": 1510601040 + }, + "origin": "cjdns", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cjdroute" + ] + }, + "inotify-tools-doc": { + "versions": { + "3.14-r2": 1509475674 + }, + "origin": "inotify-tools" + }, + "duplicity-doc": { + "versions": { + "0.7.13.1-r0": 1510588266 + }, + "origin": "duplicity" + }, + "perl-log-dispatch": { + "versions": { + "2.67-r0": 1510953098 + }, + "origin": "perl-log-dispatch", + "dependencies": [ + "perl-module-runtime", + "perl-params-validate", + "perl-dist-checkconflicts", + "perl-devel-globaldestruction" + ] + }, + "feh-doc": { + "versions": { + "2.22-r0": 1509878783 + }, + "origin": "feh" + }, + "openldap-overlay-dynlist": { + "versions": { + "2.4.45-r3": 1510258134, + "2.4.46-r0": 1565073940, + "2.4.48-r0": 1566900517 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "collectd-rrdtool": { + "versions": { + "5.7.2-r0": 1510069647 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:librrd_th.so.4" + ] + }, + "perl-date-manip": { + "versions": { + "6.60-r0": 1509492186 + }, + "origin": "perl-date-manip", + "dependencies": [ + "perl-yaml-syck" + ], + "provides": [ + "cmd:dm_date", + "cmd:dm_zdump" + ] + }, + "py-dateutil": { + "versions": { + "2.6.1-r0": 1509475855 + }, + "origin": "py-dateutil", + "dependencies": [ + "py-six" + ] + }, + "perl-timedate": { + "versions": { + "2.30-r1": 1509469038 + }, + "origin": "perl-timedate", + "provides": [ + "perl-time-date=2.30" + ] + }, + "nginx-mod-http-upload-progress": { + "versions": { + "1.12.2-r4": 1542814448 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "znc-dev": { + "versions": { + "1.7.1-r0": 1531900839, + "1.7.1-r1": 1565877330 + }, + "origin": "znc", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:znc=1.7.1", + "cmd:znc-buildmod" + ] + }, + "xf86-video-vmware-doc": { + "versions": { + "13.2.1-r0": 1510072911 + }, + "origin": "xf86-video-vmware" + }, + "tig-doc": { + "versions": { + "2.3.0-r0": 1511889467 + }, + "origin": "tig" + }, + "lua-sql-sqlite3": { + "versions": { + "2.3.5-r1": 1509488834 + }, + "origin": "lua-sql" + }, + "apache-mod-fcgid-doc": { + "versions": { + "2.3.9-r1": 1509492408 + }, + "origin": "apache-mod-fcgid" + }, + "lm_sensors-detect": { + "versions": { + "3.4.0-r4": 1509475443 + }, + "origin": "lm_sensors", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:sensors-conf-convert", + "cmd:sensors-detect" + ] + }, + "perl-test-file-sharedir-doc": { + "versions": { + "1.001002-r0": 1510855689 + }, + "origin": "perl-test-file-sharedir" + }, + "darkice-doc": { + "versions": { + "1.3-r0": 1509470067 + }, + "origin": "darkice" + }, + "py2-roman": { + "versions": { + "2.0.0-r2": 1509493353 + }, + "origin": "py-roman", + "dependencies": [ + "python2" + ] + }, + "py-uritemplate": { + "versions": { + "3.0.0-r0": 1509486308 + }, + "origin": "py-uritemplate" + }, + "perl-uri-doc": { + "versions": { + "1.71-r1": 1509464365 + }, + "origin": "perl-uri" + }, + "qemu-system-cris": { + "versions": { + "2.10.1-r3": 1519746241 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-cris" + ] + }, + "libnfs-doc": { + "versions": { + "2.0.0-r0": 1509493089 + }, + "origin": "libnfs" + }, + "py2-snowballstemmer": { + "versions": { + "1.2.1-r1": 1509489493 + }, + "origin": "py-snowballstemmer", + "dependencies": [ + "python2" + ] + }, + "lua5.2-dev": { + "versions": { + "5.2.4-r4": 1509459694 + }, + "origin": "lua5.2", + "dependencies": [ + "lua5.2", + "lua5.2-libs=5.2.4-r4", + "pkgconfig" + ], + "provides": [ + "pc:lua5.2=5.2.4" + ] + }, + "kyua-doc": { + "versions": { + "0.13-r1": 1509459786 + }, + "origin": "kyua" + }, + "atk": { + "versions": { + "2.26.1-r1": 1509464672 + }, + "origin": "atk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libatk-1.0.so.0=0.22610.1" + ] + }, + "py3-bind": { + "versions": { + "9.11.6_p1-r1": 1556872768, + "9.11.8-r0": 1561323229 + }, + "origin": "bind", + "dependencies": [ + "python3", + "py3-ply" + ] + }, + "xfconf-lang": { + "versions": { + "4.12.1-r2": 1510067933 + }, + "origin": "xfconf" + }, + "directfb": { + "versions": { + "1.7.7-r1": 1509488976 + }, + "origin": "directfb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libfreetype.so.6", + "so:libgcc_s.so.1", + "so:libkms.so.1", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libts.so.0" + ], + "provides": [ + "so:lib++dfb-1.7.so.7=7.0.0", + "so:libdirect-1.7.so.7=7.0.0", + "so:libdirectfb-1.7.so.7=7.0.0", + "so:libfusion-1.7.so.7=7.0.0", + "cmd:dfbdump", + "cmd:dfbdumpinput", + "cmd:dfbfx", + "cmd:dfbg", + "cmd:dfbinfo", + "cmd:dfbinput", + "cmd:dfbinspector", + "cmd:dfblayer", + "cmd:dfbmaster", + "cmd:dfbpenmount", + "cmd:dfbplay", + "cmd:dfbscreen", + "cmd:dfbshow", + "cmd:dfbswitch", + "cmd:directfb-csource", + "cmd:mkdfiff", + "cmd:mkdgiff", + "cmd:mkdgifft" + ] + }, + "libechonest": { + "versions": { + "2.3.1-r0": 1510075926 + }, + "origin": "libechonest", + "dependencies": [ + "so:libQtCore.so.4", + "so:libQtNetwork.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libqjson.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libechonest.so.2.3=2.3.1" + ] + }, + "a2ps-doc": { + "versions": { + "4.14-r7": 1510365670 + }, + "origin": "a2ps" + }, + "openldap-back-ldap": { + "versions": { + "2.4.45-r3": 1510258132, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "s6-portable-utils": { + "versions": { + "2.2.1.1-r0": 1509488755 + }, + "origin": "s6-portable-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.6" + ], + "provides": [ + "cmd:s6-basename", + "cmd:s6-cat", + "cmd:s6-chmod", + "cmd:s6-chown", + "cmd:s6-clock", + "cmd:s6-cut", + "cmd:s6-dirname", + "cmd:s6-dumpenv", + "cmd:s6-echo", + "cmd:s6-env", + "cmd:s6-expr", + "cmd:s6-false", + "cmd:s6-format-filter", + "cmd:s6-grep", + "cmd:s6-head", + "cmd:s6-hiercopy", + "cmd:s6-linkname", + "cmd:s6-ln", + "cmd:s6-ls", + "cmd:s6-maximumtime", + "cmd:s6-mkdir", + "cmd:s6-mkfifo", + "cmd:s6-nice", + "cmd:s6-nuke", + "cmd:s6-pause", + "cmd:s6-printenv", + "cmd:s6-quote", + "cmd:s6-quote-filter", + "cmd:s6-rename", + "cmd:s6-rmrf", + "cmd:s6-seq", + "cmd:s6-sleep", + "cmd:s6-sort", + "cmd:s6-sync", + "cmd:s6-tail", + "cmd:s6-test", + "cmd:s6-touch", + "cmd:s6-true", + "cmd:s6-uniquename", + "cmd:s6-unquote", + "cmd:s6-unquote-filter", + "cmd:s6-update-symlinks", + "cmd:seekablepipe" + ] + }, + "indent-doc": { + "versions": { + "2.2.11-r1": 1509496389 + }, + "origin": "indent" + }, + "abiword-plugin-t602": { + "versions": { + "3.0.2-r1": 1510073369 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libstdc++.so.6" + ] + }, + "poppler-qt4": { + "versions": { + "0.56.0-r0": 1510071693 + }, + "origin": "poppler-qt4", + "dependencies": [ + "so:libQtCore.so.4", + "so:libQtGui.so.4", + "so:libQtXml.so.4", + "so:libc.musl-x86_64.so.1", + "so:libpoppler.so.67", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libpoppler-qt4.so.4=4.11.0" + ] + }, + "boost-regex": { + "versions": { + "1.62.0-r5": 1509465879 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_regex-mt.so.1.62.0=1.62.0", + "so:libboost_regex.so.1.62.0=1.62.0" + ] + }, + "xz-dev": { + "versions": { + "5.2.3-r1": 1509459904 + }, + "origin": "xz", + "dependencies": [ + "pkgconfig", + "xz-libs=5.2.3-r1" + ], + "provides": [ + "pc:liblzma=5.2.3" + ] + }, + "py-psycopg2": { + "versions": { + "2.7.3.2-r0": 1510619592 + }, + "origin": "py-psycopg2" + }, + "perl-struct-dumb": { + "versions": { + "0.07-r0": 1509480429 + }, + "origin": "perl-struct-dumb" + }, + "samba-libs-py": { + "versions": { + "4.7.6-r3": 1555491789 + }, + "origin": "samba", + "dependencies": [ + "so:libMESSAGING-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcliauth-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libdcerpc.so.0", + "so:libevents-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libpytalloc-util.so.2", + "so:libpython2.7.so.1.0", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libserver-role-samba4.so", + "so:libsmbpasswdparser-samba4.so", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0" + ], + "provides": [ + "so:libsamba-net-samba4.so=0", + "so:libsamba-python-samba4.so=0" + ] + }, + "nagios-plugins-ifstatus": { + "versions": { + "2.2.1-r3": 1510288495 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins" + ] + }, + "glade3-lang": { + "versions": { + "3.8.5-r4": 1510067964 + }, + "origin": "glade3" + }, + "perl-time-modules": { + "versions": { + "2013.0912-r0": 1509493229 + }, + "origin": "perl-time-modules", + "dependencies": [ + "perl" + ] + }, + "lua-struct": { + "versions": { + "0.2-r2": 1509494934 + }, + "origin": "lua-struct" + }, + "eventlog-dev": { + "versions": { + "0.2.12-r2": 1509466297 + }, + "origin": "eventlog", + "dependencies": [ + "eventlog=0.2.12-r2", + "pkgconfig" + ], + "provides": [ + "pc:eventlog=0.2.12" + ] + }, + "pciutils": { + "versions": { + "3.5.6-r0": 1511165900 + }, + "origin": "pciutils", + "dependencies": [ + "hwdata-pci", + "so:libc.musl-x86_64.so.1", + "so:libpci.so.3" + ], + "provides": [ + "cmd:lspci", + "cmd:setpci", + "cmd:update-pciids" + ] + }, + "libgss-dev": { + "versions": { + "0.1.5-r1": 1509493414 + }, + "origin": "libgss", + "dependencies": [ + "pkgconfig", + "libgss=0.1.5-r1" + ], + "provides": [ + "pc:gss=0.1.5" + ] + }, + "lua5.2-dbi-postgresql": { + "versions": { + "0.6-r1": 1511483402 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "xdelta3-doc": { + "versions": { + "3.0.11-r0": 1509491097 + }, + "origin": "xdelta3" + }, + "soxr": { + "versions": { + "0.1.2-r0": 1509492275 + }, + "origin": "soxr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgomp.so.1" + ], + "provides": [ + "so:libsoxr-lsr.so.0=0.1.9", + "so:libsoxr.so.0=0.1.1" + ] + }, + "perl-net-smtp-tls-butmaintained-doc": { + "versions": { + "0.24-r0": 1509494937 + }, + "origin": "perl-net-smtp-tls-butmaintained" + }, + "ncurses-static": { + "versions": { + "6.0_p20171125-r1": 1534862974 + }, + "origin": "ncurses" + }, + "libcroco-dev": { + "versions": { + "0.6.12-r0": 1509467188, + "0.6.12-r1": 1563788970 + }, + "origin": "libcroco", + "dependencies": [ + "glib-dev", + "libxml2-dev", + "pkgconfig", + "libcroco=0.6.12-r1", + "pc:glib-2.0", + "pc:libxml-2.0" + ], + "provides": [ + "pc:libcroco-0.6=0.6.12", + "cmd:croco-0.6-config" + ] + }, + "acf-opennhrp": { + "versions": { + "0.10.0-r2": 1510076038 + }, + "origin": "acf-opennhrp", + "dependencies": [ + "acf-core", + "lua-posix", + "opennhrp" + ] + }, + "asterisk-fax": { + "versions": { + "15.6.1-r0": 1537795340, + "15.6.2-r0": 1568705002 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libspandsp.so.2" + ] + }, + "gvfs": { + "versions": { + "1.34.1-r0": 1511430258, + "1.34.1-r1": 1563787224 + }, + "origin": "gvfs", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgcr-base-3.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libintl.so.8", + "so:libpolkit-gobject-1.so.0", + "so:libsoup-2.4.so.1", + "so:libudev.so.1", + "so:libudisks2.so.0" + ], + "provides": [ + "so:libgvfscommon.so=0", + "so:libgvfsdaemon.so=0", + "cmd:gvfs-cat", + "cmd:gvfs-copy", + "cmd:gvfs-info", + "cmd:gvfs-less", + "cmd:gvfs-ls", + "cmd:gvfs-mime", + "cmd:gvfs-mkdir", + "cmd:gvfs-monitor-dir", + "cmd:gvfs-monitor-file", + "cmd:gvfs-mount", + "cmd:gvfs-move", + "cmd:gvfs-open", + "cmd:gvfs-rename", + "cmd:gvfs-rm", + "cmd:gvfs-save", + "cmd:gvfs-set-attribute", + "cmd:gvfs-trash", + "cmd:gvfs-tree" + ] + }, + "gtksourceview2": { + "versions": { + "2.10.5-r0": 1510067576 + }, + "origin": "gtksourceview2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgtksourceview-2.0.so.0=0.0.0" + ] + }, + "libconfig-doc": { + "versions": { + "1.5-r3": 1509706433 + }, + "origin": "libconfig" + }, + "openldap-back-dnssrv": { + "versions": { + "2.4.45-r3": 1510258132, + "2.4.46-r0": 1565073938, + "2.4.48-r0": 1566900515 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "kamailio-kazoo": { + "versions": { + "5.0.7-r0": 1532960877 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libjson-c.so.2", + "so:librabbitmq.so.4", + "so:libsrdb1.so.1", + "so:libuuid.so.1" + ] + }, + "pixman": { + "versions": { + "0.34.0-r3": 1509545247 + }, + "origin": "pixman", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpixman-1.so.0=0.34.0" + ] + }, + "gdbm": { + "versions": { + "1.13-r1": 1509458650 + }, + "origin": "gdbm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgdbm.so.4=4.0.0", + "so:libgdbm_compat.so.4=4.0.0", + "cmd:gdbm_dump", + "cmd:gdbm_load", + "cmd:gdbmtool" + ] + }, + "font-ibm-type1": { + "versions": { + "1.0.3-r0": 1509493385 + }, + "origin": "font-ibm-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "abiword-plugin-mif": { + "versions": { + "3.0.2-r1": 1510073364 + }, + "origin": "abiword", + "dependencies": [ + "so:libabiword-3.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ] + }, + "freeswitch-sounds-music-32000": { + "versions": { + "1.0.8-r1": 1509495910 + }, + "origin": "freeswitch-sounds-music-32000" + }, + "cyrus-sasl-scram": { + "versions": { + "2.1.26-r11": 1510258044 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42" + ] + }, + "libcdio-dev": { + "versions": { + "0.94-r0": 1509473626 + }, + "origin": "libcdio", + "dependencies": [ + "libcdio++=0.94-r0", + "libcdio=0.94-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcdio++=0.94", + "pc:libcdio=0.94", + "pc:libiso9660++=0.94", + "pc:libiso9660=0.94", + "pc:libudf=0.94" + ] + }, + "uwsgi-python": { + "versions": { + "2.0.17-r0": 1522154656 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "openssl-dbg": { + "versions": { + "1.0.2r-r0": 1552814650, + "1.0.2t-r0": 1568300394 + }, + "origin": "openssl" + }, + "lua5.3-xml": { + "versions": { + "130610-r5": 1509482648 + }, + "origin": "lua-xml", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "libdvbcsa-dev": { + "versions": { + "1.1.0-r0": 1509488846 + }, + "origin": "libdvbcsa", + "dependencies": [ + "libdvbcsa=1.1.0-r0" + ] + }, + "font-alias": { + "versions": { + "1.0.3-r1": 1509470382 + }, + "origin": "font-alias" + }, + "enca-dev": { + "versions": { + "1.19-r1": 1509480334 + }, + "origin": "enca", + "dependencies": [ + "enca=1.19-r1", + "pkgconfig" + ], + "provides": [ + "pc:enca=1.19" + ] + }, + "cogl-lang": { + "versions": { + "1.22.2-r0": 1510072960 + }, + "origin": "cogl" + }, + "gpgme": { + "versions": { + "1.9.0-r1": 1510588164 + }, + "origin": "gpgme", + "dependencies": [ + "gnupg", + "so:libassuan.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgpg-error.so.0" + ], + "provides": [ + "so:libgpgme.so.11=11.18.0", + "cmd:gpgme-tool" + ] + }, + "ctags": { + "versions": { + "5.8-r5": 1509491649 + }, + "origin": "ctags", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ctags" + ] + }, + "perl-javascript-minifier-xs": { + "versions": { + "0.11-r3": 1509495050 + }, + "origin": "perl-javascript-minifier-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "eventlog": { + "versions": { + "0.2.12-r2": 1509466297 + }, + "origin": "eventlog", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libevtlog.so.0=0.0.0" + ] + }, + "pidgin": { + "versions": { + "2.12.0-r2": 1510069759 + }, + "origin": "pidgin", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libgtkspell.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpurple.so.0" + ], + "provides": [ + "cmd:pidgin" + ] + }, + "seabios": { + "versions": { + "1.10.3-r0": 1510068884 + }, + "origin": "seabios", + "dependencies": [ + "seabios-bin=1.10.3-r0", + "seavgabios-bin=1.10.3-r0" + ] + }, + "shorewall-core-doc": { + "versions": { + "5.1.8-r0": 1509477808 + }, + "origin": "shorewall-core" + }, + "hiredis-dev": { + "versions": { + "0.13.3-r1": 1509551786 + }, + "origin": "hiredis", + "dependencies": [ + "hiredis=0.13.3-r1", + "pkgconfig" + ], + "provides": [ + "pc:hiredis=0.13.3" + ] + }, + "iwlwifi-6000-ucode": { + "versions": { + "9.221.4.1-r0": 1509495920 + }, + "origin": "iwlwifi-6000-ucode" + }, + "cpufrequtils-dev": { + "versions": { + "008-r4": 1509486334 + }, + "origin": "cpufrequtils", + "dependencies": [ + "cpufrequtils=008-r4" + ] + }, + "ircservices-doc": { + "versions": { + "5.1.24-r4": 1509481222 + }, + "origin": "ircservices" + }, + "git-gitk": { + "versions": { + "2.15.3-r0": 1540401295, + "2.15.4-r0": 1576015196 + }, + "origin": "git", + "dependencies": [ + "git=2.15.4-r0", + "tcl", + "tk" + ], + "provides": [ + "cmd:gitk" + ] + }, + "xautolock": { + "versions": { + "2.2-r3": 1510072542 + }, + "origin": "xautolock", + "dependencies": [ + "so:libX11.so.6", + "so:libXss.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xautolock" + ] + }, + "hiredis": { + "versions": { + "0.13.3-r1": 1509551786 + }, + "origin": "hiredis", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhiredis.so.0.13=0.13" + ] + }, + "email": { + "versions": { + "3.1.4-r4": 1510260519 + }, + "origin": "email", + "dependencies": [ + "libressl", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:email" + ] + }, + "alpine-keys": { + "versions": { + "2.1-r1": 1510072219 + }, + "origin": "alpine-keys" + }, + "iptables-doc": { + "versions": { + "1.6.1-r1": 1509469980 + }, + "origin": "iptables" + }, + "zsh-doc": { + "versions": { + "5.4.2-r1": 1522503663 + }, + "origin": "zsh" + }, + "lsof-doc": { + "versions": { + "4.89-r1": 1510330780 + }, + "origin": "lsof" + }, + "uwsgi-logzmq": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libzmq.so.5" + ] + }, + "mupdf-gl": { + "versions": { + "1.13.0-r0": 1533746007 + }, + "origin": "mupdf", + "dependencies": [ + "so:libGL.so.1", + "so:libc.musl-x86_64.so.1", + "so:libglut.so.3", + "so:libmupdf.so.0", + "so:libmupdfthird.so.0" + ], + "provides": [ + "cmd:mupdf-gl" + ] + }, + "jq": { + "versions": { + "1.5-r5": 1525102934 + }, + "origin": "jq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libonig.so.4" + ], + "provides": [ + "so:libjq.so.1=1.0.4", + "cmd:jq" + ] + }, + "parted": { + "versions": { + "3.2-r6": 1509482206 + }, + "origin": "parted", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02", + "so:libncursesw.so.6", + "so:libreadline.so.7", + "so:libuuid.so.1" + ], + "provides": [ + "so:libparted-fs-resize.so.0=0.0.1", + "so:libparted.so.2=2.0.1", + "cmd:parted", + "cmd:partprobe" + ] + }, + "dhcp-dbg": { + "versions": { + "4.3.5-r0": 1509496513 + }, + "origin": "dhcp" + }, + "perl-devel-checkbin": { + "versions": { + "0.02-r0": 1509474057 + }, + "origin": "perl-devel-checkbin" + }, + "crconf": { + "versions": { + "0_pre2-r0": 1509483276 + }, + "origin": "crconf", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:crconf" + ] + }, + "pdnsd-doc": { + "versions": { + "1.2.9a-r5": 1509492601 + }, + "origin": "pdnsd" + }, + "ldapvi-doc": { + "versions": { + "1.7-r8": 1510261318 + }, + "origin": "ldapvi" + }, + "dbus-glib-dev": { + "versions": { + "0.108-r1": 1509465372 + }, + "origin": "dbus-glib", + "dependencies": [ + "dbus-glib=0.108-r1", + "pc:dbus-1", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:dbus-glib-1=0.108" + ] + }, + "xf86-video-r128-doc": { + "versions": { + "6.10.2-r0": 1510074868 + }, + "origin": "xf86-video-r128" + }, + "lua5.3-yaml": { + "versions": { + "1.1.2-r1": 1509488585 + }, + "origin": "lua-yaml", + "dependencies": [ + "lua5.3", + "lua5.3-lub", + "so:libc.musl-x86_64.so.1" + ] + }, + "gst-plugins-good-lang": { + "versions": { + "1.12.3-r0": 1510075044 + }, + "origin": "gst-plugins-good" + }, + "lua-sircbot": { + "versions": { + "0.4-r1": 1509492350 + }, + "origin": "sircbot" + }, + "elfutils": { + "versions": { + "0.168-r1": 1509477712 + }, + "origin": "elfutils" + }, + "termrec-doc": { + "versions": { + "0.17-r1": 1509491605 + }, + "origin": "termrec" + }, + "perl-php-serialization-doc": { + "versions": { + "0.34-r1": 1509477756 + }, + "origin": "perl-php-serialization" + }, + "perl-text-wikiformat": { + "versions": { + "0.81-r0": 1509482363 + }, + "origin": "perl-text-wikiformat", + "dependencies": [ + "perl-uri" + ] + }, + "cyrus-sasl-dev": { + "versions": { + "2.1.26-r11": 1510258044 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "libsasl=2.1.26-r11", + "pkgconfig" + ], + "provides": [ + "pc:libsasl2=2.1.26" + ] + }, + "libzdb-dev": { + "versions": { + "3.1-r1": 1509491012 + }, + "origin": "libzdb", + "dependencies": [ + "flex-dev", + "sqlite-dev", + "mariadb-dev", + "postgresql-dev", + "libzdb=3.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:zdb=3.1" + ] + }, + "pixman-dbg": { + "versions": { + "0.34.0-r3": 1509545247 + }, + "origin": "pixman" + }, + "openldap-back-passwd": { + "versions": { + "2.4.45-r3": 1510258133, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "freetds-dev": { + "versions": { + "1.00.44-r0": 1509480207 + }, + "origin": "freetds", + "dependencies": [ + "freetds=1.00.44-r0" + ] + }, + "mdocml-apropos": { + "versions": { + "1.14.3-r0": 1509459638 + }, + "origin": "mdocml", + "dependencies": [ + "/bin/sh", + "mdocml=1.14.3-r0" + ] + }, + "makedepend": { + "versions": { + "1.0.5-r1": 1509466284 + }, + "origin": "makedepend", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:makedepend" + ] + }, + "keyutils": { + "versions": { + "1.5.10-r0": 1509469695 + }, + "origin": "keyutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libkeyutils.so.1" + ], + "provides": [ + "cmd:key.dns_resolver", + "cmd:keyctl", + "cmd:request-key" + ] + }, + "inotify-tools": { + "versions": { + "3.14-r2": 1509475675 + }, + "origin": "inotify-tools", + "dependencies": [ + "!inotify-tools-inc", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libinotifytools.so.0=0.4.1", + "cmd:inotifywait", + "cmd:inotifywatch" + ] + }, + "vim": { + "versions": { + "8.0.1359-r0": 1512029968, + "8.0.1359-r1": 1559759711, + "8.0.1359-r2": 1561188678 + }, + "origin": "vim", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua-5.2.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:ex", + "cmd:rview", + "cmd:rvim", + "cmd:view", + "cmd:vim", + "cmd:vimtutor", + "cmd:xxd" + ] + }, + "perl-net-rblclient": { + "versions": { + "0.5-r2": 1509479745 + }, + "origin": "perl-net-rblclient", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:spamalyze" + ] + }, + "coova-chilli-dev": { + "versions": { + "1.3.2-r2": 1510287299 + }, + "origin": "coova-chilli", + "dependencies": [ + "coova-chilli=1.3.2-r2" + ] + }, + "lua-crypto-doc": { + "versions": { + "0.3.2-r5": 1510261400 + }, + "origin": "lua-crypto" + }, + "openldap-back-monitor": { + "versions": { + "2.4.45-r3": 1510258133, + "2.4.46-r0": 1565073939, + "2.4.48-r0": 1566900516 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "xfbdev": { + "versions": { + "1.19.5-r1": 1540838460 + }, + "origin": "xorg-server", + "dependencies": [ + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.0.0", + "so:libpixman-1.so.0", + "so:libudev.so.1" + ], + "provides": [ + "cmd:Xfbdev" + ] + }, + "collectd-doc": { + "versions": { + "5.7.2-r0": 1510069642 + }, + "origin": "collectd" + }, + "mkinitfs": { + "versions": { + "3.2.0-r4": 1525036846 + }, + "origin": "mkinitfs", + "dependencies": [ + "busybox", + "apk-tools>=2.8.2", + "lddtree>=1.25", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcryptsetup.so.4", + "so:libkmod.so.2" + ], + "provides": [ + "cmd:bootchartd", + "cmd:mkinitfs", + "cmd:nlplug-findfs" + ] + }, + "perl-net-smtp-ssl": { + "versions": { + "1.04-r0": 1509494478 + }, + "origin": "perl-net-smtp-ssl", + "dependencies": [ + "perl-io-socket-ssl", + "perl-net-ssleay" + ] + }, + "iperf": { + "versions": { + "2.0.9-r1": 1509482124 + }, + "origin": "iperf", + "dependencies": [ + "!iperf3", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:iperf" + ] + }, + "net-snmp-dev": { + "versions": { + "5.7.3-r10": 1510259616 + }, + "origin": "net-snmp", + "dependencies": [ + "libressl-dev", + "net-snmp-agent-libs=5.7.3-r10", + "net-snmp-libs=5.7.3-r10" + ], + "provides": [ + "cmd:net-snmp-config" + ] + }, + "kbd-bkeymaps": { + "versions": { + "2.0.4-r2": 1510922528 + }, + "origin": "kbd", + "provides": [ + "bkeymaps" + ] + }, + "freeswitch-sounds-fr-ca-june-8000": { + "versions": { + "1.0.51-r0": 1509483411 + }, + "origin": "freeswitch-sounds-fr-ca-june-8000" + }, + "libmaxminddb-doc": { + "versions": { + "1.3.1-r0": 1511893707 + }, + "origin": "libmaxminddb" + }, + "openvswitch": { + "versions": { + "2.8.1-r2": 1512030507 + }, + "origin": "openvswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libssl.so.44" + ], + "provides": [ + "cmd:ovn-controller", + "cmd:ovn-controller-vtep", + "cmd:ovn-detrace", + "cmd:ovn-docker-overlay-driver", + "cmd:ovn-docker-underlay-driver", + "cmd:ovn-nbctl", + "cmd:ovn-northd", + "cmd:ovn-sbctl", + "cmd:ovn-trace", + "cmd:ovs-appctl", + "cmd:ovs-bugtool", + "cmd:ovs-docker", + "cmd:ovs-dpctl", + "cmd:ovs-dpctl-top", + "cmd:ovs-l3ping", + "cmd:ovs-ofctl", + "cmd:ovs-parse-backtrace", + "cmd:ovs-pcap", + "cmd:ovs-pki", + "cmd:ovs-tcpdump", + "cmd:ovs-tcpundump", + "cmd:ovs-test", + "cmd:ovs-testcontroller", + "cmd:ovs-vlan-bug-workaround", + "cmd:ovs-vlan-test", + "cmd:ovs-vsctl", + "cmd:ovs-vswitchd", + "cmd:ovsdb-client", + "cmd:ovsdb-server", + "cmd:ovsdb-tool", + "cmd:vtep-ctl" + ] + }, + "lua5.3-mosquitto": { + "versions": { + "0.2-r1": 1509496427 + }, + "origin": "lua-mosquitto", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ] + }, + "rsyslog-hiredis": { + "versions": { + "8.31.0-r0": 1512031981, + "8.31.0-r1": 1571767017 + }, + "origin": "rsyslog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.13" + ] + }, + "jbig2dec": { + "versions": { + "0.14-r0": 1509469939 + }, + "origin": "jbig2dec", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libjbig2dec.so.0=0.0.0", + "cmd:jbig2dec" + ] + }, + "s6-networking-dev": { + "versions": { + "2.3.0.2-r1": 1510260983 + }, + "origin": "s6-networking", + "dependencies": [ + "s6-networking=2.3.0.2-r1" + ] + }, + "perl-dbd-pg-doc": { + "versions": { + "3.7.0-r0": 1509491418 + }, + "origin": "perl-dbd-pg" + }, + "gettext-doc": { + "versions": { + "0.19.8.1-r1": 1509459195 + }, + "origin": "gettext" + }, + "libmtp-examples": { + "versions": { + "1.1.14-r0": 1509481648 + }, + "origin": "libmtp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmtp.so.9" + ], + "provides": [ + "cmd:mtp-albumart", + "cmd:mtp-albums", + "cmd:mtp-connect", + "cmd:mtp-delfile", + "cmd:mtp-detect", + "cmd:mtp-emptyfolders", + "cmd:mtp-files", + "cmd:mtp-filetree", + "cmd:mtp-folders", + "cmd:mtp-format", + "cmd:mtp-getfile", + "cmd:mtp-getplaylist", + "cmd:mtp-hotplug", + "cmd:mtp-newfolder", + "cmd:mtp-newplaylist", + "cmd:mtp-playlists", + "cmd:mtp-reset", + "cmd:mtp-sendfile", + "cmd:mtp-sendtr", + "cmd:mtp-thumb", + "cmd:mtp-tracks", + "cmd:mtp-trexist" + ] + }, + "perl-db_file": { + "versions": { + "1.840-r1": 1509491810 + }, + "origin": "perl-db_file", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ] + }, + "sg3_utils-doc": { + "versions": { + "1.42-r0": 1509482218 + }, + "origin": "sg3_utils" + }, + "perl-devel-checklib-doc": { + "versions": { + "1.11-r1": 1509489314 + }, + "origin": "perl-devel-checklib" + }, + "pciutils-doc": { + "versions": { + "3.5.6-r0": 1511165899 + }, + "origin": "pciutils" + }, + "gnome-mime-data": { + "versions": { + "2.18.0-r1": 1509481656 + }, + "origin": "gnome-mime-data" + }, + "nagios-plugins-ircd": { + "versions": { + "2.2.1-r3": 1510288496 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins" + ] + }, + "qemu-system-mips64": { + "versions": { + "2.10.1-r3": 1519746242 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libasound.so.2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libnfs.so.11", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-mips64" + ] + }, + "perl-file-sharedir-doc": { + "versions": { + "1.104-r0": 1510564485 + }, + "origin": "perl-file-sharedir" + }, + "libxrandr-dev": { + "versions": { + "1.5.1-r1": 1509466001 + }, + "origin": "libxrandr", + "dependencies": [ + "randrproto", + "libxext-dev", + "libxrandr=1.5.1-r1", + "pc:randrproto>=1.5", + "pc:x11", + "pc:xext", + "pc:xproto", + "pc:xrender", + "pkgconfig" + ], + "provides": [ + "pc:xrandr=1.5.1" + ] + }, + "bison-doc": { + "versions": { + "3.0.4-r0": 1509456687 + }, + "origin": "bison" + }, + "perl-mime-base64-doc": { + "versions": { + "3.15-r4": 1509491804 + }, + "origin": "perl-mime-base64" + }, + "gtkman": { + "versions": { + "1.0.1-r0": 1510073899 + }, + "origin": "gtkman", + "dependencies": [ + "py-gtk" + ], + "provides": [ + "cmd:gtkman" + ] + }, + "lua-rex-posix": { + "versions": { + "2.9.0-r0": 1510619366 + }, + "origin": "lua-rex" + }, + "memcached-doc": { + "versions": { + "1.5.6-r0": 1520365888 + }, + "origin": "memcached" + }, + "logcheck-doc": { + "versions": { + "1.3.18-r0": 1509468630 + }, + "origin": "logcheck" + }, + "pcmciautils-doc": { + "versions": { + "018-r1": 1509482231 + }, + "origin": "pcmciautils" + }, + "icu-doc": { + "versions": { + "59.1-r1": 1509464843 + }, + "origin": "icu" + }, + "perl-exporter": { + "versions": { + "5.72-r1": 1509483328 + }, + "origin": "perl-exporter" + }, + "perl-convert-uulib": { + "versions": { + "1.5-r2": 1509483418 + }, + "origin": "perl-convert-uulib", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "flashrom-doc": { + "versions": { + "0.9.9-r2": 1509482144 + }, + "origin": "flashrom" + }, + "dahdi-tools": { + "versions": { + "2.11.1-r0": 1509476156 + }, + "origin": "dahdi-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnewt.so.0.52" + ], + "provides": [ + "so:libtonezone.so.2=2.0.0", + "cmd:dahdi_cfg", + "cmd:dahdi_genconf", + "cmd:dahdi_hardware", + "cmd:dahdi_maint", + "cmd:dahdi_monitor", + "cmd:dahdi_registration", + "cmd:dahdi_scan", + "cmd:dahdi_span_assignments", + "cmd:dahdi_span_types", + "cmd:dahdi_speed", + "cmd:dahdi_test", + "cmd:dahdi_tool", + "cmd:dahdi_waitfor_span_assignments", + "cmd:fxotune", + "cmd:lsdahdi", + "cmd:sethdlc", + "cmd:twinstar", + "cmd:xpp_blink", + "cmd:xpp_sync" + ] + }, + "libmspack": { + "versions": { + "0.8_alpha-r0": 1543321986 + }, + "origin": "libmspack", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmspack.so.0=0.1.0", + "cmd:cabrip", + "cmd:chmextract", + "cmd:msexpand", + "cmd:oabextract" + ] + }, + "multisort": { + "versions": { + "1.1-r0": 1509482417 + }, + "origin": "multisort", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:multisort" + ] + }, + "gettext-lang": { + "versions": { + "0.19.8.1-r1": 1509459196 + }, + "origin": "gettext" + }, + "dzen": { + "versions": { + "0.9.5-r3": 1509787635 + }, + "origin": "dzen", + "dependencies": [ + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libXpm.so.4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dzen2" + ] + }, + "py3-cparser": { + "versions": { + "2.18-r0": 1509483347 + }, + "origin": "py-cparser", + "dependencies": [ + "python3" + ] + }, + "postgresql-client": { + "versions": { + "10.7-r0": 1554274199, + "10.8-r0": 1557668015, + "10.9-r0": 1562225614, + "10.10-r0": 1565683438 + }, + "origin": "postgresql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libedit.so.0", + "so:libpq.so.5", + "so:libz.so.1" + ], + "provides": [ + "cmd:clusterdb", + "cmd:createdb", + "cmd:createuser", + "cmd:dropdb", + "cmd:dropuser", + "cmd:pg_basebackup", + "cmd:pg_dump", + "cmd:pg_dumpall", + "cmd:pg_isready", + "cmd:pg_receivewal", + "cmd:pg_recvlogical", + "cmd:pg_restore", + "cmd:psql", + "cmd:reindexdb", + "cmd:vacuumdb" + ] + }, + "sprunge": { + "versions": { + "0.6-r0": 1509477746 + }, + "origin": "sprunge", + "dependencies": [ + "curl" + ], + "provides": [ + "cmd:sprunge" + ] + }, + "grub-xenhost": { + "versions": { + "2.02-r3": 1509495739 + }, + "origin": "grub" + }, + "lua5.2-lzlib": { + "versions": { + "0.4.3-r0": 1509494167 + }, + "origin": "lua-lzlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ] + }, + "libnice-dbg": { + "versions": { + "0.1.14-r2": 1510931344 + }, + "origin": "libnice" + }, + "drbd-utils-pacemaker": { + "versions": { + "9.1.1-r0": 1511795634 + }, + "origin": "drbd-utils", + "dependencies": [ + "drbd" + ] + }, + "fortify-headers": { + "versions": { + "0.9-r0": 1509458197 + }, + "origin": "fortify-headers" + }, + "haveged-doc": { + "versions": { + "1.9.2-r0": 1511893694 + }, + "origin": "haveged" + }, + "uwsgi-legion_cache_fetch": { + "versions": { + "2.0.17-r0": 1522154655 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "varnish-dev": { + "versions": { + "5.2.1-r0": 1511265090 + }, + "origin": "varnish", + "dependencies": [ + "pkgconfig", + "varnish-libs=5.2.1-r0" + ], + "provides": [ + "pc:varnishapi=5.2.1" + ] + }, + "ldapvi": { + "versions": { + "1.7-r8": 1510261318 + }, + "origin": "ldapvi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.42", + "so:libglib-2.0.so.0", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libncursesw.so.6", + "so:libpopt.so.0", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:ldapvi" + ] + }, + "perl-snmp-session": { + "versions": { + "1.13-r2": 1509483931 + }, + "origin": "perl-snmp-session", + "dependencies": [ + "perl" + ] + }, + "samba": { + "versions": { + "4.7.6-r3": 1555491790 + }, + "origin": "samba", + "dependencies": [ + "samba-server=4.7.6-r3", + "samba-client=4.7.6-r3", + "samba-common-tools=4.7.6-r3" + ] + }, + "mpc1": { + "versions": { + "1.0.3-r1": 1509457078 + }, + "origin": "mpc1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libmpfr.so.4" + ], + "provides": [ + "so:libmpc.so.3=3.0.0" + ] + }, + "faad2": { + "versions": { + "2.7-r7": 1509480955, + "2.9.0-r0": 1571918410 + }, + "origin": "faad2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfaad.so.2=2.0.0", + "so:libfaad_drm.so.2=2.0.0", + "cmd:faad" + ] + }, + "iwlwifi-5000-ucode-doc": { + "versions": { + "8.83.5.1-r0": 1509494264 + }, + "origin": "iwlwifi-5000-ucode" + }, + "cfdisk": { + "versions": { + "2.31.1-r0": 1541506294 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfdisk.so.1", + "so:libmount.so.1", + "so:libncursesw.so.6", + "so:libsmartcols.so.1" + ], + "provides": [ + "cmd:cfdisk" + ] + }, + "qpdf": { + "versions": { + "7.0.0-r0": 1509482356 + }, + "origin": "qpdf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libqpdf.so.18", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:qpdf", + "cmd:zlib-flate" + ] + }, + "rarian": { + "versions": { + "0.8.1-r6": 1509465523 + }, + "origin": "rarian", + "dependencies": [ + "bash", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:librarian.so.0=0.0.0", + "cmd:rarian-example", + "cmd:rarian-sk-extract", + "cmd:rarian-sk-gen-uuid", + "cmd:rarian-sk-get-cl", + "cmd:rarian-sk-get-content-list", + "cmd:rarian-sk-get-extended-content-list", + "cmd:rarian-sk-get-scripts", + "cmd:rarian-sk-install", + "cmd:rarian-sk-migrate", + "cmd:rarian-sk-preinstall", + "cmd:rarian-sk-rebuild", + "cmd:rarian-sk-update", + "cmd:scrollkeeper-extract", + "cmd:scrollkeeper-gen-seriesid", + "cmd:scrollkeeper-get-cl", + "cmd:scrollkeeper-get-content-list", + "cmd:scrollkeeper-get-extended-content-list", + "cmd:scrollkeeper-get-index-from-docpath", + "cmd:scrollkeeper-get-toc-from-docpath", + "cmd:scrollkeeper-get-toc-from-id", + "cmd:scrollkeeper-install", + "cmd:scrollkeeper-preinstall", + "cmd:scrollkeeper-rebuilddb", + "cmd:scrollkeeper-uninstall", + "cmd:scrollkeeper-update" + ] + }, + "mksh-doc": { + "versions": { + "56b-r0": 1509491850 + }, + "origin": "mksh" + }, + "openjade-dev": { + "versions": { + "1.3.2-r5": 1509488724 + }, + "origin": "openjade", + "dependencies": [ + "openjade-libs=1.3.2-r5" + ] + }, + "dhcrelay": { + "versions": { + "4.3.5-r0": 1509496514 + }, + "origin": "dhcp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "cmd:dhcrelay" + ] + }, + "avfs": { + "versions": { + "1.0.5-r0": 1509496598 + }, + "origin": "avfs", + "dependencies": [ + "bash", + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2" + ], + "provides": [ + "so:libavfs.so.0=0.0.2", + "cmd:avfsd", + "cmd:davpass", + "cmd:ftppass", + "cmd:mountavfs", + "cmd:umountavfs" + ] + }, + "libisofs-dev": { + "versions": { + "1.4.8-r0": 1509462207 + }, + "origin": "libisofs", + "dependencies": [ + "libisofs=1.4.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:libisofs-1=1.4.8" + ] + }, + "libsasl": { + "versions": { + "2.1.26-r11": 1510258045 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ], + "provides": [ + "so:libsasl2.so.3=3.0.0" + ] + }, + "libjpeg-turbo-doc": { + "versions": { + "1.5.3-r2": 1537879742, + "1.5.3-r3": 1563792633 + }, + "origin": "libjpeg-turbo" + }, + "lua5.3-iconv": { + "versions": { + "7-r1": 1509493324 + }, + "origin": "lua-iconv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libintl": { + "versions": { + "0.19.8.1-r1": 1509459196 + }, + "origin": "gettext", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libintl.so.8=8.1.5" + ] + }, + "cadaver-doc": { + "versions": { + "0.23.3-r3": 1510310613 + }, + "origin": "cadaver" + }, + "sems-voicemail": { + "versions": { + "1.6.0-r6": 1510260897 + }, + "origin": "sems", + "dependencies": [ + "sems", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "dovecot": { + "versions": { + "2.2.36.3-r0": 1554102392, + "2.2.36.4-r0": 1567075099 + }, + "origin": "dovecot", + "dependencies": [ + "libressl", + "/bin/sh", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.42", + "so:libssl.so.44", + "so:libz.so.1" + ], + "provides": [ + "so:lib01_acl_plugin.so=0", + "so:lib02_imap_acl_plugin.so=0", + "so:lib02_lazy_expunge_plugin.so=0", + "so:lib05_mail_crypt_acl_plugin.so=0", + "so:lib05_pop3_migration_plugin.so=0", + "so:lib05_snarf_plugin.so=0", + "so:lib10_last_login_plugin.so=0", + "so:lib10_mail_crypt_plugin.so=0", + "so:lib10_mail_filter_plugin.so=0", + "so:lib10_quota_plugin.so=0", + "so:lib11_imap_quota_plugin.so=0", + "so:lib11_trash_plugin.so=0", + "so:lib15_notify_plugin.so=0", + "so:lib20_autocreate_plugin.so=0", + "so:lib20_charset_alias_plugin.so=0", + "so:lib20_expire_plugin.so=0", + "so:lib20_fts_plugin.so=0", + "so:lib20_listescape_plugin.so=0", + "so:lib20_mail_log_plugin.so=0", + "so:lib20_mailbox_alias_plugin.so=0", + "so:lib20_notify_status_plugin.so=0", + "so:lib20_push_notification_plugin.so=0", + "so:lib20_quota_clone_plugin.so=0", + "so:lib20_replication_plugin.so=0", + "so:lib20_var_expand_crypt.so=0", + "so:lib20_virtual_plugin.so=0", + "so:lib20_zlib_plugin.so=0", + "so:lib21_fts_squat_plugin.so=0", + "so:lib30_imap_zlib_plugin.so=0", + "so:lib90_stats_plugin.so=0", + "so:lib95_imap_stats_plugin.so=0", + "so:lib99_welcome_plugin.so=0", + "so:libdcrypt_openssl.so=0", + "so:libdovecot-compression.so.0=0.0.0", + "so:libdovecot-dsync.so.0=0.0.0", + "so:libdovecot-fts.so.0=0.0.0", + "so:libdovecot-lda.so.0=0.0.0", + "so:libdovecot-login.so.0=0.0.0", + "so:libdovecot-storage.so.0=0.0.0", + "so:libdovecot.so.0=0.0.0", + "so:libfs_compress.so=0", + "so:libfs_crypt.so=0", + "so:libfs_mail_crypt.so=0", + "so:libssl_iostream_openssl.so=0", + "so:libstats_auth.so=0", + "so:libstats_mail.so=0", + "cmd:doveadm", + "cmd:doveconf", + "cmd:dovecot", + "cmd:dsync" + ] + }, + "syslinux": { + "versions": { + "6.04_pre1-r1": 1509482644 + }, + "origin": "syslinux", + "dependencies": [ + "mtools", + "blkid", + "mkinitfs", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:cat.c32=0", + "so:chain.c32=0", + "so:cmd.c32=0", + "so:cmenu.c32=0", + "so:config.c32=0", + "so:cptime.c32=0", + "so:cpu.c32=0", + "so:cpuid.c32=0", + "so:cpuidtest.c32=0", + "so:debug.c32=0", + "so:dhcp.c32=0", + "so:dir.c32=0", + "so:disk.c32=0", + "so:dmi.c32=0", + "so:dmitest.c32=0", + "so:elf.c32=0", + "so:ethersel.c32=0", + "so:gfxboot.c32=0", + "so:gpxecmd.c32=0", + "so:hdt.c32=0", + "so:hexdump.c32=0", + "so:host.c32=0", + "so:ifcpu.c32=0", + "so:ifcpu64.c32=0", + "so:ifmemdsk.c32=0", + "so:ifplop.c32=0", + "so:kbdmap.c32=0", + "so:kontron_wdt.c32=0", + "so:ldlinux.c32=0", + "so:lfs.c32=0", + "so:libcom32.c32=0", + "so:libgpl.c32=0", + "so:liblua.c32=0", + "so:libmenu.c32=0", + "so:libutil.c32=0", + "so:linux.c32=0", + "so:ls.c32=0", + "so:lua.c32=0", + "so:mboot.c32=0", + "so:meminfo.c32=0", + "so:menu.c32=0", + "so:pci.c32=0", + "so:pcitest.c32=0", + "so:pmload.c32=0", + "so:poweroff.c32=0", + "so:prdhcp.c32=0", + "so:pwd.c32=0", + "so:pxechn.c32=0", + "so:reboot.c32=0", + "so:rosh.c32=0", + "so:sanboot.c32=0", + "so:sdi.c32=0", + "so:sysdump.c32=0", + "so:syslinux.c32=0", + "so:vesa.c32=0", + "so:vesainfo.c32=0", + "so:vesamenu.c32=0", + "so:vpdtest.c32=0", + "so:whichsys.c32=0", + "so:zzjson.c32=0", + "cmd:extlinux", + "cmd:gethostip", + "cmd:isohybrid", + "cmd:isohybrid.pl", + "cmd:keytab-lilo", + "cmd:lss16toppm", + "cmd:md5pass", + "cmd:memdiskfind", + "cmd:mkdiskimage", + "cmd:ppmtolss16", + "cmd:pxelinux-options", + "cmd:sha1pass", + "cmd:syslinux", + "cmd:syslinux2ansi", + "cmd:update-extlinux" + ] + }, + "ghostscript-fonts": { + "versions": { + "8.11-r1": 1509489326 + }, + "origin": "ghostscript-fonts" + }, + "libquvi-scripts-doc": { + "versions": { + "0.9.20131130-r0": 1509491409 + }, + "origin": "libquvi-scripts" + }, + "freeswitch-perlesl": { + "versions": { + "1.6.19-r0": 1510585358 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libperl.so", + "so:libstdc++.so.6" + ] + }, + "py-urlgrabber-doc": { + "versions": { + "3.10.1-r1": 1509468415 + }, + "origin": "py-urlgrabber" + }, + "lua5.1-bitlib": { + "versions": { + "5.3.0-r0": 1509468304 + }, + "origin": "lua-bitlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py-imagesize": { + "versions": { + "0.7.1-r2": 1509474094 + }, + "origin": "py-imagesize" + }, + "py-egenix-mx-base-doc": { + "versions": { + "3.2.9-r0": 1509474193 + }, + "origin": "py-egenix-mx-base" + }, + "perl-apache-logformat-compiler-doc": { + "versions": { + "0.32-r0": 1509481806 + }, + "origin": "perl-apache-logformat-compiler" + }, + "lcms2-utils": { + "versions": { + "2.8-r1": 1509464952 + }, + "origin": "lcms2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:jpgicc", + "cmd:linkicc", + "cmd:psicc", + "cmd:tificc", + "cmd:transicc" + ] + }, + "mcpp-dev": { + "versions": { + "2.7.2-r1": 1509473730 + }, + "origin": "mcpp", + "dependencies": [ + "mcpp-libs=2.7.2-r1" + ] + }, + "perl-test-mocktime-doc": { + "versions": { + "0.15-r0": 1509475510 + }, + "origin": "perl-test-mocktime" + }, + "perl-archive-zip-doc": { + "versions": { + "1.59-r0": 1509483547 + }, + "origin": "perl-archive-zip" + }, + "py-flask": { + "versions": { + "0.12.2-r1": 1509551869 + }, + "origin": "py-flask", + "dependencies": [ + "py-click", + "py-itsdangerous", + "py-jinja2", + "py-werkzeug" + ] + }, + "perl-devel-stacktrace-ashtml": { + "versions": { + "0.15-r0": 1510564492 + }, + "origin": "perl-devel-stacktrace-ashtml", + "dependencies": [ + "perl", + "perl-devel-stacktrace" + ] + }, + "py-simpleparse": { + "versions": { + "2.2.0-r0": 1509495041 + }, + "origin": "py-simpleparse", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "perl-module-build-tiny-doc": { + "versions": { + "0.039-r0": 1509474778 + }, + "origin": "perl-module-build-tiny" + }, + "perl-locale-maketext-lexicon": { + "versions": { + "1.00-r0": 1509494308 + }, + "origin": "perl-locale-maketext-lexicon", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:xgettext.pl" + ] + }, + "gtk-engines-industrial": { + "versions": { + "2.21.0-r2": 1510071723 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "libinput-dev": { + "versions": { + "1.9.2-r0": 1511276852 + }, + "origin": "libinput", + "dependencies": [ + "libinput-libs=1.9.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libinput=1.9.2" + ] + }, + "uwsgi-dumbloop": { + "versions": { + "2.0.17-r0": 1522154654 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-mozilla-ca": { + "versions": { + "20160104-r0": 1509494409 + }, + "origin": "perl-mozilla-ca" + }, + "gnome-mime-data-lang": { + "versions": { + "2.18.0-r1": 1509481655 + }, + "origin": "gnome-mime-data" + }, + "gfortran": { + "versions": { + "6.4.0-r5": 1509458079 + }, + "origin": "gcc", + "dependencies": [ + "gcc=6.4.0-r5", + "libgfortran=6.4.0-r5", + "libquadmath=6.4.0-r5", + "libgfortran=6.4.0-r5", + "libquadmath=6.4.0-r5", + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libisl.so.15", + "so:libmpc.so.3", + "so:libmpfr.so.4", + "so:libz.so.1" + ], + "provides": [ + "cmd:gfortran", + "cmd:x86_64-alpine-linux-musl-gfortran" + ] + }, + "lua5.1-dev": { + "versions": { + "5.1.5-r3": 1509462457 + }, + "origin": "lua5.1", + "dependencies": [ + "lua5.1", + "lua5.1-libs=5.1.5-r3", + "pkgconfig" + ], + "provides": [ + "pc:lua5.1=5.1.5" + ] + }, + "fetchmail-doc": { + "versions": { + "6.3.26-r12": 1510261418 + }, + "origin": "fetchmail" + }, + "aspell-libs": { + "versions": { + "0.60.6.1-r12": 1509472868, + "0.60.6.1-r13": 1572541141 + }, + "origin": "aspell", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libaspell.so.15=15.1.5", + "so:libpspell.so.15=15.1.5" + ] + }, + "perl-net-cidr-lite-doc": { + "versions": { + "0.21-r2": 1509496071 + }, + "origin": "perl-net-cidr-lite" + }, + "rsyslog-pgsql": { + "versions": { + "8.31.0-r0": 1512031980, + "8.31.0-r1": 1571767017 + }, + "origin": "rsyslog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "perl-text-reform": { + "versions": { + "1.20-r0": 1509475837 + }, + "origin": "perl-text-reform", + "dependencies": [ + "perl" + ] + }, + "py-avahi": { + "versions": { + "0.6.32-r4": 1509465450, + "0.6.32-r5": 1563345619 + }, + "origin": "avahi" + }, + "libvpx-dev": { + "versions": { + "1.6.1-r0": 1509480733 + }, + "origin": "libvpx", + "dependencies": [ + "libvpx=1.6.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:vpx=1.6.1" + ] + }, + "libid3tag": { + "versions": { + "0.15.1b-r6": 1509470497 + }, + "origin": "libid3tag", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libid3tag.so.0=0.3.0" + ] + }, + "gst-plugins-base0.10-doc": { + "versions": { + "0.10.36-r3": 1510068367 + }, + "origin": "gst-plugins-base0.10" + }, + "cyrus-sasl": { + "versions": { + "2.1.26-r11": 1510258045 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:libkrb5.so.26", + "so:libroken.so.18", + "so:libsasl2.so.3" + ], + "provides": [ + "cmd:pluginviewer", + "cmd:saslauthd", + "cmd:sasldblistusers2", + "cmd:saslpasswd2", + "cmd:testsaslauthd" + ] + }, + "xev": { + "versions": { + "1.2.2-r0": 1509491061 + }, + "origin": "xev", + "dependencies": [ + "so:libX11.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xev" + ] + }, + "v4l-utils-dev": { + "versions": { + "1.12.5-r1": 1510072040 + }, + "origin": "v4l-utils", + "dependencies": [ + "pkgconfig", + "v4l-utils-dvbv5=1.12.5-r1", + "v4l-utils-libs=1.12.5-r1" + ], + "provides": [ + "pc:libdvbv5=1.12.5", + "pc:libv4l1=1.12.5", + "pc:libv4l2=1.12.5", + "pc:libv4l2rds=1.12.5", + "pc:libv4lconvert=1.12.5" + ] + }, + "py3-sphinxcontrib-websupport": { + "versions": { + "1.0.1-r3": 1509493320 + }, + "origin": "py-sphinxcontrib-websupport", + "dependencies": [ + "python3" + ] + } + }, + "provide": { + "so": { + "libflite.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_grapheme_lang.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_grapheme_lex.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_indic_lang.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_indic_lex.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_time_awb.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_us_awb.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_us_kal.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_us_kal16.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_us_rms.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmu_us_slt.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_cmulex.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libflite_usenglish.so.1": { + "package": "flite", + "version": "2.0.0" + }, + "libcryptsetup.so.4": { + "package": "cryptsetup-libs", + "version": "4.7.0" + }, + "liblber-2.4.so.2": { + "package": "libldap", + "version": "2.10.11" + }, + "libldap-2.4.so.2": { + "package": "libldap", + "version": "2.10.11" + }, + "libldap_r-2.4.so.2": { + "package": "libldap", + "version": "2.10.11" + }, + "libsmbpasswdparser-samba4.so": { + "package": "samba-dc", + "version": "0" + }, + "libupower-glib.so.3": { + "package": "upower", + "version": "3.0.1" + }, + "libxcb-keysyms.so.1": { + "package": "xcb-util-keysyms", + "version": "1.0.0" + }, + "libgdkglext-x11-1.0.so.0": { + "package": "gtkglext", + "version": "0.0.0" + }, + "libgtkglext-x11-1.0.so.0": { + "package": "gtkglext", + "version": "0.0.0" + }, + "libass.so.9": { + "package": "libass", + "version": "9.0.1" + }, + "libclutter-1.0.so.0": { + "package": "clutter", + "version": "0.2600.2" + }, + "libalpm.so.10": { + "package": "pacman", + "version": "10.0.2" + }, + "libxfce4util.so.7": { + "package": "libxfce4util", + "version": "7.0.0" + }, + "lib21_fts_lucene_plugin.so": { + "package": "dovecot-fts-lucene", + "version": "0" + }, + "libexecinfo.so.1": { + "package": "libexecinfo", + "version": "1" + }, + "libboost_filesystem-mt.so.1.62.0": { + "package": "boost-filesystem", + "version": "1.62.0" + }, + "libboost_filesystem.so.1.62.0": { + "package": "boost-filesystem", + "version": "1.62.0" + }, + "liblua.so.5": { + "package": "lua5.1-libs", + "version": "5.1.4" + }, + "libfarstream-0.1.so.0": { + "package": "farstream0.1", + "version": "0.0.1" + }, + "libasn1-samba4.so.8": { + "package": "samba-heimdal-libs", + "version": "8.0.0" + }, + "libgssapi-samba4.so.2": { + "package": "samba-heimdal-libs", + "version": "2.0.0" + }, + "libhcrypto-samba4.so.5": { + "package": "samba-heimdal-libs", + "version": "5.0.1" + }, + "libheimbase-samba4.so.1": { + "package": "samba-heimdal-libs", + "version": "1.0.0" + }, + "libheimntlm-samba4.so.1": { + "package": "samba-heimdal-libs", + "version": "1.0.1" + }, + "libhx509-samba4.so.5": { + "package": "samba-heimdal-libs", + "version": "5.0.0" + }, + "libkrb5-samba4.so.26": { + "package": "samba-heimdal-libs", + "version": "26.0.0" + }, + "libroken-samba4.so.19": { + "package": "samba-heimdal-libs", + "version": "19.0.1" + }, + "libwind-samba4.so.0": { + "package": "samba-heimdal-libs", + "version": "0.0.0" + }, + "libmpeg2.so.0": { + "package": "libmpeg2", + "version": "0.1.0" + }, + "libmpeg2convert.so.0": { + "package": "libmpeg2", + "version": "0.0.0" + }, + "libcroco-0.6.so.3": { + "package": "libcroco", + "version": "3.0.1" + }, + "libguess.so.1": { + "package": "libguess", + "version": "1.0.0" + }, + "libpyglib-2.0-python.so.0": { + "package": "py-gobject", + "version": "0.0.0" + }, + "libsyslog-ng-3.9.so.0": { + "package": "syslog-ng", + "version": "0.0.0" + }, + "libboost_serialization-mt.so.1.62.0": { + "package": "boost-serialization", + "version": "1.62.0" + }, + "libboost_serialization.so.1.62.0": { + "package": "boost-serialization", + "version": "1.62.0" + }, + "libgbm.so.1": { + "package": "mesa-gbm", + "version": "1.0.0" + }, + "libz.so.1": { + "package": "zlib", + "version": "1.2.11" + }, + "libdbus-1.so.3": { + "package": "dbus-libs", + "version": "3.14.16" + }, + "libabiword-3.0.so": { + "package": "abiword", + "version": "0" + }, + "libtiff.so.5": { + "package": "tiff", + "version": "5.4.0" + }, + "libtinyxml.so.0": { + "package": "tinyxml", + "version": "0.2.6.2" + }, + "libsamplerate.so.0": { + "package": "libsamplerate", + "version": "0.1.8" + }, + "libx264.so.148": { + "package": "x264-libs", + "version": "148" + }, + "librhash.so.0": { + "package": "rhash-libs", + "version": "0" + }, + "libmowgli-2.so.0": { + "package": "libmowgli", + "version": "0.0.0" + }, + "libsodium.so.23": { + "package": "libsodium", + "version": "23.0.0" + }, + "libboost_python-mt.so.1.62.0": { + "package": "boost-python", + "version": "1.62.0" + }, + "libboost_python.so.1.62.0": { + "package": "boost-python", + "version": "1.62.0" + }, + "libsigc-2.0.so.0": { + "package": "libsigc++", + "version": "0.0.0" + }, + "libXpm.so.4": { + "package": "libxpm", + "version": "4.11.0" + }, + "libconfig++.so.9": { + "package": "libconfig++", + "version": "9.2.0" + }, + "libldb.so.1": { + "package": "ldb", + "version": "1.3.0" + }, + "libxmlrpc_client++.so.8": { + "package": "xmlrpc-c-client++", + "version": "8.39" + }, + "libproxy.so.1": { + "package": "libproxy", + "version": "1.0.0" + }, + "librrd_th.so.4": { + "package": "librrd-th", + "version": "4.3.5" + }, + "libXv.so.1": { + "package": "libxv", + "version": "1.0.0" + }, + "libxshmfence.so.1": { + "package": "libxshmfence", + "version": "1.0.0" + }, + "libunbound.so.2": { + "package": "unbound-libs", + "version": "2.5.6" + }, + "libnfs.so.11": { + "package": "libnfs", + "version": "11.0.0" + }, + "libdevmapper.so.1.02": { + "package": "device-mapper-libs", + "version": "1.02" + }, + "libspice-client-glib-2.0.so.8": { + "package": "spice-glib", + "version": "8.6.0" + }, + "libspice-controller.so.0": { + "package": "spice-glib", + "version": "0.0.0" + }, + "libnpth.so.0": { + "package": "npth", + "version": "0.1.1" + }, + "libfreeradius-client.so.2": { + "package": "freeradius-client", + "version": "2.0.0" + }, + "libboost_atomic-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_chrono-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_chrono.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_container-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_container.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_context-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_coroutine-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_coroutine.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_locale-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_log-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_log.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_log_setup-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_log_setup.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_timer-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_timer.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_type_erasure-mt.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libboost_type_erasure.so.1.62.0": { + "package": "boost", + "version": "1.62.0" + }, + "libjemalloc.so.2": { + "package": "jemalloc", + "version": "2" + }, + "libtdb.so.1": { + "package": "tdb-libs", + "version": "1.3.15" + }, + "libbonobo-2.so.0": { + "package": "libbonobo", + "version": "0.0.0" + }, + "libbonobo-activation.so.4": { + "package": "libbonobo", + "version": "4.0.0" + }, + "libelf.so.1": { + "package": "elfutils-libelf", + "version": "0" + }, + "libiec61883.so.0": { + "package": "libiec61883", + "version": "0.1.1" + }, + "libMagick++-7.Q16HDRI.so.3": { + "package": "imagemagick-c++", + "version": "3.0.0" + }, + "libraw.so.16": { + "package": "libraw", + "version": "16.0.0" + }, + "libraw_r.so.16": { + "package": "libraw", + "version": "16.0.0" + }, + "libpq.so.5": { + "package": "libpq", + "version": "5.10" + }, + "libpng16.so.16": { + "package": "libpng", + "version": "16.37.0" + }, + "libburn.so.4": { + "package": "libburn", + "version": "4.101.0" + }, + "libclamav.so.7": { + "package": "clamav-lib", + "version": "7.1.2" + }, + "libgck-1.so.0": { + "package": "gcr", + "version": "0.0.0" + }, + "libgcr-base-3.so.1": { + "package": "gcr", + "version": "1.0.0" + }, + "libgcr-ui-3.so.1": { + "package": "gcr", + "version": "1.0.0" + }, + "liblz4.so.1": { + "package": "lz4-libs", + "version": "1.8.0" + }, + "libclamunrar.so.7": { + "package": "clamav-libunrar", + "version": "7.1.2" + }, + "libclamunrar_iface.so.7": { + "package": "clamav-libunrar", + "version": "7.1.2" + }, + "libtcl8.6.so": { + "package": "tcl", + "version": "0" + }, + "libtxc_dxtn.so": { + "package": "libtxc_dxtn", + "version": "0" + }, + "libbfd-2.30.so": { + "package": "binutils-libs", + "version": "0" + }, + "libopcodes-2.30.so": { + "package": "binutils-libs", + "version": "0" + }, + "libshout.so.3": { + "package": "libshout", + "version": "3.2.0" + }, + "libstdc++.so.6": { + "package": "libstdc++", + "version": "6.0.22" + }, + "libfetch.so": { + "package": "libfetch", + "version": "0" + }, + "libssl.so.44": { + "package": "libressl2.6-libssl", + "version": "44.0.1" + }, + "libfdisk.so.1": { + "package": "libfdisk", + "version": "1.1.0" + }, + "libusb-0.1.so.4": { + "package": "libusb-compat", + "version": "4.4.4" + }, + "libssh2.so.1": { + "package": "libssh2", + "version": "1.0.1" + }, + "libhavege.so.1": { + "package": "haveged", + "version": "1.1.0" + }, + "libkeyutils.so.1": { + "package": "keyutils-libs", + "version": "1.6" + }, + "libdlz-bind9-for-torture-samba4.so": { + "package": "samba-test", + "version": "0" + }, + "libtorture-samba4.so": { + "package": "samba-test", + "version": "0" + }, + "libasound.so.2": { + "package": "alsa-lib", + "version": "2.0.0" + }, + "libgconf-2.so.4": { + "package": "gconf", + "version": "4.1.5" + }, + "group_file.so": { + "package": "sudo", + "version": "0" + }, + "libsudo_noexec.so": { + "package": "sudo", + "version": "0" + }, + "libsudo_util.so.0": { + "package": "sudo", + "version": "0.0.0" + }, + "sudoers.so": { + "package": "sudo", + "version": "0" + }, + "system_group.so": { + "package": "sudo", + "version": "0" + }, + "libavahi-ui-gtk3.so.0": { + "package": "avahi-ui-gtk3", + "version": "0.1.4" + }, + "libvanessa_logger.so.0": { + "package": "vanessa_logger", + "version": "0.0.5" + }, + "libwebsockets.so.12": { + "package": "libwebsockets", + "version": "12" + }, + "libhashkit.so.2": { + "package": "libmemcached-libs", + "version": "2.0.0" + }, + "libmemcached.so.11": { + "package": "libmemcached-libs", + "version": "11.0.0" + }, + "libmemcachedprotocol.so.0": { + "package": "libmemcached-libs", + "version": "0.0.0" + }, + "libmemcachedutil.so.2": { + "package": "libmemcached-libs", + "version": "2.0.0" + }, + "libminizip.so.1": { + "package": "minizip", + "version": "1.0.0" + }, + "libebt_802_3.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_among.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_arp.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_arpreply.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_ip.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_ip6.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_limit.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_log.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_mark.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_mark_m.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_nat.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_nflog.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_pkttype.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_redirect.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_standard.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_stp.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_ulog.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_vlan.so": { + "package": "ebtables", + "version": "0" + }, + "libebtable_broute.so": { + "package": "ebtables", + "version": "0" + }, + "libebtable_filter.so": { + "package": "ebtables", + "version": "0" + }, + "libebtable_nat.so": { + "package": "ebtables", + "version": "0" + }, + "libebtc.so": { + "package": "ebtables", + "version": "0" + }, + "libldb-cmdline.so": { + "package": "ldb-tools", + "version": "0" + }, + "libcap.so.2": { + "package": "libcap", + "version": "2.25" + }, + "libBasicUsageEnvironment.so.1": { + "package": "live-media", + "version": "1.0.0" + }, + "libUsageEnvironment.so.3": { + "package": "live-media", + "version": "3.1.0" + }, + "libgroupsock.so.8": { + "package": "live-media", + "version": "8.1.1" + }, + "libliveMedia.so.61": { + "package": "live-media", + "version": "61.0.0" + }, + "libgarcon-1.so.0": { + "package": "garcon", + "version": "0.0.0" + }, + "libgarcon-gtk2-1.so.0": { + "package": "garcon", + "version": "0.0.0" + }, + "libgarcon-gtk3-1.so.0": { + "package": "garcon", + "version": "0.0.0" + }, + "libwayland-client.so.0": { + "package": "wayland-libs-client", + "version": "0.3.0" + }, + "libxcb-render-util.so.0": { + "package": "xcb-util-renderutil", + "version": "0.0.0" + }, + "libverto-libev.so.1": { + "package": "libverto-libev", + "version": "1.0.0" + }, + "libhogweed.so.4": { + "package": "nettle", + "version": "4.3" + }, + "libnettle.so.6": { + "package": "nettle", + "version": "6.3" + }, + "libgssapi_krb5.so.2": { + "package": "krb5-libs", + "version": "2.2" + }, + "libgssrpc.so.4": { + "package": "krb5-libs", + "version": "4.2" + }, + "libk5crypto.so.3": { + "package": "krb5-libs", + "version": "3.1" + }, + "libkadm5clnt_mit.so.11": { + "package": "krb5-libs", + "version": "11.0" + }, + "libkadm5srv_mit.so.11": { + "package": "krb5-libs", + "version": "11.0" + }, + "libkdb5.so.8": { + "package": "krb5-libs", + "version": "8.0" + }, + "libkrad.so.0": { + "package": "krb5-libs", + "version": "0.0" + }, + "libkrb5.so.3": { + "package": "krb5-libs", + "version": "3.3" + }, + "libkrb5support.so.0": { + "package": "krb5-libs", + "version": "0.1" + }, + "libboost_wave-mt.so.1.62.0": { + "package": "boost-wave", + "version": "1.62.0" + }, + "libogrove.so.0": { + "package": "openjade-libs", + "version": "0.0.1" + }, + "libospgrove.so.0": { + "package": "openjade-libs", + "version": "0.0.1" + }, + "libostyle.so.0": { + "package": "openjade-libs", + "version": "0.0.1" + }, + "libgtest1.so.34": { + "package": "nss-dev", + "version": "34" + }, + "libnsssysinit.so": { + "package": "nss-dev", + "version": "34" + }, + "libSDL_mixer-1.2.so.0": { + "package": "sdl_mixer", + "version": "0.12.0" + }, + "libX11-xcb.so.1": { + "package": "libx11", + "version": "1.0.0" + }, + "libX11.so.6": { + "package": "libx11", + "version": "6.3.0" + }, + "libmatroska.so.6": { + "package": "libmatroska", + "version": "6.0.0" + }, + "libsysfs.so.2": { + "package": "sysfsutils", + "version": "2.0.1" + }, + "rlm_rest.so": { + "package": "freeradius-rest", + "version": "0" + }, + "libmicrohttpd.so.12": { + "package": "libmicrohttpd", + "version": "12.45.0" + }, + "libboost_random-mt.so.1.62.0": { + "package": "boost-random", + "version": "1.62.0" + }, + "libboost_random.so.1.62.0": { + "package": "boost-random", + "version": "1.62.0" + }, + "libgoffice-0.10.so.10": { + "package": "goffice", + "version": "10.0.36" + }, + "libdevmapper-event.so.1.02": { + "package": "device-mapper-event-libs", + "version": "1.02" + }, + "libXxf86misc.so.1": { + "package": "libxxf86misc", + "version": "1.1.0" + }, + "libQt3Support.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtCLucene.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtDeclarative.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtDesigner.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtDesignerComponents.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtGui.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtHelp.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtMultimedia.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtOpenGL.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtScriptTools.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtSvg.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libenca.so.0": { + "package": "enca", + "version": "0.5.1" + }, + "openvpn-auth-ldap.so": { + "package": "openvpn-auth-ldap", + "version": "0" + }, + "libcddb.so.2": { + "package": "libcddb", + "version": "2.2.3" + }, + "libGLESv1_CM.so.1": { + "package": "mesa-gles", + "version": "1.1.0" + }, + "libGLESv2.so.2": { + "package": "mesa-gles", + "version": "2.0.0" + }, + "libasn1.so.8": { + "package": "heimdal-libs", + "version": "8.0.0" + }, + "libgssapi.so.3": { + "package": "heimdal-libs", + "version": "3.0.0" + }, + "libhcrypto.so.4": { + "package": "heimdal-libs", + "version": "4.1.0" + }, + "libhdb.so.9": { + "package": "heimdal-libs", + "version": "9.2.0" + }, + "libheimbase.so.1": { + "package": "heimdal-libs", + "version": "1.0.0" + }, + "libheimntlm.so.0": { + "package": "heimdal-libs", + "version": "0.1.0" + }, + "libhx509.so.5": { + "package": "heimdal-libs", + "version": "5.0.0" + }, + "libkadm5clnt.so.7": { + "package": "heimdal-libs", + "version": "7.0.1" + }, + "libkadm5srv.so.8": { + "package": "heimdal-libs", + "version": "8.0.1" + }, + "libkafs.so.0": { + "package": "heimdal-libs", + "version": "0.5.1" + }, + "libkdc.so.2": { + "package": "heimdal-libs", + "version": "2.0.0" + }, + "libkrb5.so.26": { + "package": "heimdal-libs", + "version": "26.0.0" + }, + "libotp.so.0": { + "package": "heimdal-libs", + "version": "0.1.5" + }, + "libroken.so.18": { + "package": "heimdal-libs", + "version": "18.1.0" + }, + "libsl.so.0": { + "package": "heimdal-libs", + "version": "0.2.1" + }, + "libwind.so.0": { + "package": "heimdal-libs", + "version": "0.0.0" + }, + "windc.so.0": { + "package": "heimdal-libs", + "version": "0.0.0" + }, + "libcli-ldap-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libcmdline-credentials-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libdcerpc.so.0": { + "package": "samba-client-libs", + "version": "0.0.1" + }, + "libdsdb-garbage-collect-tombstones-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libevents-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libhttp-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libnetif-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libregistry-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libsmbclient-raw-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libecpg.so.6": { + "package": "postgresql-libs", + "version": "6.10" + }, + "libecpg_compat.so.3": { + "package": "postgresql-libs", + "version": "3.10" + }, + "libpgtypes.so.3": { + "package": "postgresql-libs", + "version": "3.10" + }, + "libnetsnmp.so.30": { + "package": "net-snmp-libs", + "version": "30.0.3" + }, + "libxmlrpc_client.so.3": { + "package": "xmlrpc-c-client", + "version": "3.39" + }, + "libluajit-5.1.so.2": { + "package": "luajit", + "version": "2.1.0" + }, + "libIPMIlanserv.so.0": { + "package": "openipmi-lanserv", + "version": "0.0.1" + }, + "libXt.so.6": { + "package": "libxt", + "version": "6.0.0" + }, + "libgstbasecamerabinsrc-0.10.so.23": { + "package": "gst-plugins-bad0.10", + "version": "23.0.0" + }, + "libgstbasevideo-0.10.so.23": { + "package": "gst-plugins-bad0.10", + "version": "23.0.0" + }, + "libgstcodecparsers-0.10.so.23": { + "package": "gst-plugins-bad0.10", + "version": "23.0.0" + }, + "libgstphotography-0.10.so.23": { + "package": "gst-plugins-bad0.10", + "version": "23.0.0" + }, + "libgstsignalprocessor-0.10.so.23": { + "package": "gst-plugins-bad0.10", + "version": "23.0.0" + }, + "libdvbcsa.so.1": { + "package": "libdvbcsa", + "version": "1.0.1" + }, + "libjson-glib-1.0.so.0": { + "package": "json-glib", + "version": "0.200.8" + }, + "libfl.so.2": { + "package": "flex-libs", + "version": "2.0.0" + }, + "libqextserialport.so.1": { + "package": "qextserialport", + "version": "1.2.0" + }, + "libboost_unit_test_framework-mt.so.1.62.0": { + "package": "boost-unit_test_framework", + "version": "1.62.0" + }, + "libboost_unit_test_framework.so.1.62.0": { + "package": "boost-unit_test_framework", + "version": "1.62.0" + }, + "libXfont2.so.2": { + "package": "libxfont2", + "version": "2.0.0" + }, + "libMrm.so.4": { + "package": "motif", + "version": "4.0.4" + }, + "libUil.so.4": { + "package": "motif", + "version": "4.0.4" + }, + "libXm.so.4": { + "package": "motif", + "version": "4.0.4" + }, + "libcanberra-gtk3.so.0": { + "package": "libcanberra-gtk3", + "version": "0.1.9" + }, + "bpipe-fd.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbac-9.0.5.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbaccfg-9.0.5.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbacfind-9.0.5.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbacsd-9.0.5.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbacsql-9.0.5.so": { + "package": "bacula-libs", + "version": "0" + }, + "libavahi-client.so.3": { + "package": "avahi-libs", + "version": "3.2.9" + }, + "libavahi-common.so.3": { + "package": "avahi-libs", + "version": "3.5.3" + }, + "libiptcdata.so.0": { + "package": "libiptcdata", + "version": "0.3.3" + }, + "libGLEW.so.2.1": { + "package": "glew", + "version": "2.1.0" + }, + "libcollectdclient.so.1": { + "package": "collectd-libs", + "version": "1.0.0" + }, + "libwv-1.2.so.4": { + "package": "wv", + "version": "4.0.5" + }, + "libbaccats-postgresql-9.0.5.so": { + "package": "bacula-pgsql", + "version": "0" + }, + "libxdg-basedir.so.1": { + "package": "libxdg-basedir", + "version": "1.2.0" + }, + "libjson-c.so.2": { + "package": "json-c", + "version": "2.0.2" + }, + "libnetflowPlugin-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "libntop-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "libntopreport-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "librrdPlugin-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "libsflowPlugin-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "libfreebl3.so": { + "package": "nss", + "version": "34" + }, + "libgtestutil.so.34": { + "package": "nss", + "version": "34" + }, + "libnss3.so": { + "package": "nss", + "version": "34" + }, + "libnssckbi.so": { + "package": "nss", + "version": "34" + }, + "libnssdbm3.so": { + "package": "nss", + "version": "34" + }, + "libnssutil3.so": { + "package": "nss", + "version": "34" + }, + "libsmime3.so": { + "package": "nss", + "version": "34" + }, + "libsoftokn3.so": { + "package": "nss", + "version": "34" + }, + "libssl3.so": { + "package": "nss", + "version": "34" + }, + "libwx_baseu-2.8.so.0": { + "package": "wxgtk2.8-base", + "version": "0.8.0" + }, + "libwx_baseu_net-2.8.so.0": { + "package": "wxgtk2.8-base", + "version": "0.8.0" + }, + "libwx_baseu_xml-2.8.so.0": { + "package": "wxgtk2.8-base", + "version": "0.8.0" + }, + "libatomic.so.1": { + "package": "libatomic", + "version": "1.2.0" + }, + "libxcb-ewmh.so.2": { + "package": "xcb-util-wm", + "version": "2.0.0" + }, + "libxcb-icccm.so.4": { + "package": "xcb-util-wm", + "version": "4.0.0" + }, + "rlm_sql_unixodbc.so": { + "package": "freeradius-unixodbc", + "version": "0" + }, + "libfpm_pb.so.0": { + "package": "quagga", + "version": "0.0.0" + }, + "libospf.so.0": { + "package": "quagga", + "version": "0.0.0" + }, + "libospfapiclient.so.0": { + "package": "quagga", + "version": "0.0.0" + }, + "libquagga_pb.so.0": { + "package": "quagga", + "version": "0.0.0" + }, + "libzebra.so.1": { + "package": "quagga", + "version": "1.0.0" + }, + "libXp.so.6": { + "package": "libxp", + "version": "6.2.0" + }, + "libgstallocators-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgstapp-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgstaudio-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgstfft-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgstpbutils-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgstriff-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgstrtp-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgstrtsp-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgstsdp-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgsttag-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libgstvideo-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1203.0" + }, + "libkdb_ldap.so.1": { + "package": "krb5-server-ldap", + "version": "1.0" + }, + "libfltk.so.1.3": { + "package": "fltk", + "version": "1.3" + }, + "libfltk_forms.so.1.3": { + "package": "fltk", + "version": "1.3" + }, + "libfltk_gl.so.1.3": { + "package": "fltk", + "version": "1.3" + }, + "libfltk_images.so.1.3": { + "package": "fltk", + "version": "1.3" + }, + "libnfdump-1.6.15.so": { + "package": "nfdump", + "version": "0" + }, + "libspice-client-gtk-3.0.so.5": { + "package": "spice-gtk", + "version": "5.0.0" + }, + "libfprint.so.0": { + "package": "libfprint", + "version": "0.0.0" + }, + "libxfce4panel-1.0.so.4": { + "package": "xfce4-panel", + "version": "4.0.0" + }, + "libxfce4panel-2.0.so.4": { + "package": "xfce4-panel", + "version": "4.0.0" + }, + "postfix-sqlite.so": { + "package": "postfix-sqlite", + "version": "0" + }, + "postfix-mysql.so": { + "package": "postfix-mysql", + "version": "0" + }, + "liborc-0.4.so.0": { + "package": "orc", + "version": "0.27.0" + }, + "liborc-test-0.4.so.0": { + "package": "orc", + "version": "0.27.0" + }, + "libnetfilter_log.so.1": { + "package": "libnetfilter_log", + "version": "1.1.0" + }, + "libnetfilter_log_libipulog.so.1": { + "package": "libnetfilter_log", + "version": "1.0.0" + }, + "libnice.so.10": { + "package": "libnice", + "version": "10.7.0" + }, + "libcares.so.2": { + "package": "c-ares", + "version": "2.2.0" + }, + "libspeex.so.1": { + "package": "speex", + "version": "1.5.1" + }, + "rlm_eap.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_fast.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_gtc.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_leap.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_md5.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_mschapv2.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_peap.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_pwd.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_sim.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_tls.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_ttls.so": { + "package": "freeradius-eap", + "version": "0" + }, + "libpci.so.3": { + "package": "pciutils-libs", + "version": "3.5.6" + }, + "libXcomposite.so.1": { + "package": "libxcomposite", + "version": "1.0.0" + }, + "libgccpp.so.1": { + "package": "libgc++", + "version": "1.0.3" + }, + "libhowl.so.0": { + "package": "avahi-compat-howl", + "version": "0.0.0" + }, + "libgnarl-6.so": { + "package": "libgnat", + "version": "0" + }, + "libgnat-6.so": { + "package": "libgnat", + "version": "0" + }, + "libxfconf-0.so.2": { + "package": "xfconf", + "version": "2.0.0" + }, + "libgpo-samba4.so": { + "package": "samba-common-tools", + "version": "0" + }, + "libnet-keytab-samba4.so": { + "package": "samba-common-tools", + "version": "0" + }, + "libfcgi++.so.0": { + "package": "fcgi++", + "version": "0.0.0" + }, + "libboost_wserialization-mt.so.1.62.0": { + "package": "boost-wserialization", + "version": "1.62.0" + }, + "libboost_wserialization.so.1.62.0": { + "package": "boost-wserialization", + "version": "1.62.0" + }, + "libnetfilter_cthelper.so.0": { + "package": "libnetfilter_cthelper", + "version": "0.0.0" + }, + "libharfbuzz.so.0": { + "package": "harfbuzz", + "version": "0.10600.3" + }, + "libcairo-script-interpreter.so.2": { + "package": "cairo", + "version": "2.11400.10" + }, + "libcairo.so.2": { + "package": "cairo", + "version": "2.11400.10" + }, + "libgnt.so.0": { + "package": "finch", + "version": "0.8.10" + }, + "libboost_prg_exec_monitor-mt.so.1.62.0": { + "package": "boost-prg_exec_monitor", + "version": "1.62.0" + }, + "libboost_prg_exec_monitor.so.1.62.0": { + "package": "boost-prg_exec_monitor", + "version": "1.62.0" + }, + "libtalloc.so.2": { + "package": "talloc", + "version": "2.1.10" + }, + "libwayland-cursor.so.0": { + "package": "wayland-libs-cursor", + "version": "0.0.0" + }, + "libopusfile.so.0": { + "package": "opusfile", + "version": "0.4.3" + }, + "libopusurl.so.0": { + "package": "opusfile", + "version": "0.4.3" + }, + "libcupsfilters.so.1": { + "package": "cups-filters-libs", + "version": "1.0.0" + }, + "libfontembed.so.1": { + "package": "cups-filters-libs", + "version": "1.0.0" + }, + "libltdl.so.7": { + "package": "libltdl", + "version": "7.3.1" + }, + "libmaa.so.3": { + "package": "libmaa", + "version": "3.0.0" + }, + "libXxf86dga.so.1": { + "package": "libxxf86dga", + "version": "1.0.0" + }, + "libevdev.so.2": { + "package": "libevdev", + "version": "2.1.19" + }, + "liburiparser.so.1": { + "package": "uriparser", + "version": "1.0.20" + }, + "libdvbpsi.so.10": { + "package": "libdvbpsi", + "version": "10.0.0" + }, + "libpangomm-1.4.so.1": { + "package": "pangomm", + "version": "1.0.30" + }, + "libmaxminddb.so.0": { + "package": "libmaxminddb", + "version": "0.0.7" + }, + "libmp3splt.so.0": { + "package": "libmp3splt", + "version": "0.0.9" + }, + "libhistory.so.7": { + "package": "libhistory", + "version": "7.0" + }, + "libpaper.so.1": { + "package": "libpaper", + "version": "1.1.2" + }, + "libformw.so.6": { + "package": "ncurses-libs", + "version": "6.0" + }, + "libmenuw.so.6": { + "package": "ncurses-libs", + "version": "6.0" + }, + "libncursesw.so.6": { + "package": "ncurses-libs", + "version": "6.0" + }, + "libpanelw.so.6": { + "package": "ncurses-libs", + "version": "6.0" + }, + "libcurl.so.4": { + "package": "libcurl", + "version": "4.5.0" + }, + "libmosquittopp.so.1": { + "package": "mosquitto-libs++", + "version": "1" + }, + "liboping.so.0": { + "package": "liboping", + "version": "0.3.0" + }, + "libICE.so.6": { + "package": "libice", + "version": "6.3.0" + }, + "libdmmp.so.0.1.0": { + "package": "multipath-tools", + "version": "0.1.0" + }, + "libmpathcmd.so.0": { + "package": "multipath-tools", + "version": "0" + }, + "libmpathpersist.so.0": { + "package": "multipath-tools", + "version": "0" + }, + "libmultipath.so.0": { + "package": "multipath-tools", + "version": "0" + }, + "libXfixes.so.3": { + "package": "libxfixes", + "version": "3.1.0" + }, + "libelf.so.0": { + "package": "libelf", + "version": "0" + }, + "libcom_err.so.2": { + "package": "libcom_err", + "version": "2.1" + }, + "libXmu.so.6": { + "package": "libxmu", + "version": "6.2.0" + }, + "libXmuu.so.1": { + "package": "libxmu", + "version": "1.0.0" + }, + "libgnomekbd.so.8": { + "package": "libgnomekbd", + "version": "8.0.0" + }, + "libgnomekbdui.so.8": { + "package": "libgnomekbd", + "version": "8.0.0" + }, + "libnss_winbind.so.2": { + "package": "samba-libnss-winbind", + "version": "2" + }, + "libnss_wins.so.2": { + "package": "samba-libnss-winbind", + "version": "2" + }, + "libXss.so.1": { + "package": "libxscrnsaver", + "version": "1.0.0" + }, + "libpgm-5.2.so.0": { + "package": "openpgm", + "version": "0.0.122" + }, + "libmysqlclient.so.18": { + "package": "mariadb-client-libs", + "version": "18.0.0" + }, + "libloudmouth-1.so.0": { + "package": "loudmouth", + "version": "0.1.0" + }, + "libfontenc.so.1": { + "package": "libfontenc", + "version": "1.0.0" + }, + "libs6.so.2.6": { + "package": "s6", + "version": "2.6.1.1" + }, + "libMESSAGING-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libdcerpc-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libidmap-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libnon-posix-acls-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libnss-info-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libdriver_sqlite.so": { + "package": "dovecot-sqlite", + "version": "0" + }, + "libfaac.so.0": { + "package": "faac", + "version": "0.0.0" + }, + "libclammspack.so.0": { + "package": "clamav", + "version": "0.1.0" + }, + "libxt_ACCOUNT_cl.so.0": { + "package": "xtables-addons", + "version": "0.0.0" + }, + "libntfs-3g.so.88": { + "package": "ntfs-3g-libs", + "version": "88.0.0" + }, + "libI810XvMC.so.1": { + "package": "xf86-video-intel", + "version": "1.0.0" + }, + "libIntelXvMC.so.1": { + "package": "xf86-video-intel", + "version": "1.0.0" + }, + "libXRes.so.1": { + "package": "libxres", + "version": "1.0.0" + }, + "libperl.so": { + "package": "perl", + "version": "0" + }, + "libva-drm.so.1": { + "package": "libva", + "version": "1.4000.0" + }, + "libva-egl.so.1": { + "package": "libva", + "version": "1.4000.0" + }, + "libva-glx.so.1": { + "package": "libva", + "version": "1.4000.0" + }, + "libva-tpi.so.1": { + "package": "libva", + "version": "1.4000.0" + }, + "libva-x11.so.1": { + "package": "libva", + "version": "1.4000.0" + }, + "libva.so.1": { + "package": "libva", + "version": "1.4000.0" + }, + "libbluray.so.2": { + "package": "libbluray", + "version": "2.0.0" + }, + "libspandsp.so.2": { + "package": "spandsp", + "version": "2.0.0" + }, + "libfreetype.so.6": { + "package": "freetype", + "version": "6.15.0" + }, + "libquazip.so.1": { + "package": "quazip", + "version": "1.0.0" + }, + "libsctp.so.1": { + "package": "lksctp-tools", + "version": "1.0.17" + }, + "libcheck.so.0": { + "package": "check", + "version": "0.0.0" + }, + "libisoburn.so.1": { + "package": "libisoburn", + "version": "1.105.0" + }, + "libcdio_cdda.so.2": { + "package": "libcdio-paranoia", + "version": "2.0.0" + }, + "libcdio_paranoia.so.2": { + "package": "libcdio-paranoia", + "version": "2.0.0" + }, + "rlm_sql_mysql.so": { + "package": "freeradius-mysql", + "version": "0" + }, + "libdbi.so.1": { + "package": "libdbi", + "version": "1.1.0" + }, + "lib90_sieve_plugin.so": { + "package": "dovecot-pigeonhole-plugin", + "version": "0" + }, + "lib95_imap_sieve_plugin.so": { + "package": "dovecot-pigeonhole-plugin", + "version": "0" + }, + "libdovecot-sieve.so.0": { + "package": "dovecot-pigeonhole-plugin", + "version": "0.0.0" + }, + "libvala-0.36.so.0": { + "package": "vala", + "version": "0.0.0" + }, + "libchromeXvMC.so.1": { + "package": "xf86-video-openchrome", + "version": "1.0.0" + }, + "libchromeXvMCPro.so.1": { + "package": "xf86-video-openchrome", + "version": "1.0.0" + }, + "libXdmcp.so.6": { + "package": "libxdmcp", + "version": "6.0.0" + }, + "libdovecot-sql.so.0": { + "package": "dovecot-sql", + "version": "0.0.0" + }, + "libsmartcols.so.1": { + "package": "libsmartcols", + "version": "1.1.0" + }, + "libbaccats-mysql-9.0.5.so": { + "package": "bacula-mysql", + "version": "0" + }, + "libnl-cli-3.so.200": { + "package": "libnl3-cli", + "version": "200.23.0" + }, + "libXi.so.6": { + "package": "libxi", + "version": "6.1.0" + }, + "libopenobex.so.2": { + "package": "openobex", + "version": "1.7.2" + }, + "libxcb-util.so.1": { + "package": "xcb-util", + "version": "1.0.0" + }, + "libwnck-1.so.22": { + "package": "libwnck", + "version": "22.4.0" + }, + "libxcb-composite.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-damage.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-dpms.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-dri2.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-dri3.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-glx.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-present.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-randr.so.0": { + "package": "libxcb", + "version": "0.1.0" + }, + "libxcb-record.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-render.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-res.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-screensaver.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-shape.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-shm.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-sync.so.1": { + "package": "libxcb", + "version": "1.0.0" + }, + "libxcb-xf86dri.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xfixes.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xinerama.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xinput.so.0": { + "package": "libxcb", + "version": "0.1.0" + }, + "libxcb-xkb.so.1": { + "package": "libxcb", + "version": "1.0.0" + }, + "libxcb-xtest.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xv.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xvmc.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb.so.1": { + "package": "libxcb", + "version": "1.1.0" + }, + "libgstapp-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstaudio-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstcdda-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstfft-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstinterfaces-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstnetbuffer-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstpbutils-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstriff-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstrtp-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstrtsp-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstsdp-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgsttag-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstvideo-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libaprutil-1.so.0": { + "package": "apr-util", + "version": "0.6.1" + }, + "libXinerama.so.1": { + "package": "libxinerama", + "version": "1.0.0" + }, + "postfix-pcre.so": { + "package": "postfix-pcre", + "version": "0" + }, + "libacl.so.1": { + "package": "libacl", + "version": "1.1.0" + }, + "libgsl.so.23": { + "package": "gsl", + "version": "23.0.0" + }, + "libgslcblas.so.0": { + "package": "gsl", + "version": "0.0.0" + }, + "libglfw.so.3": { + "package": "glfw", + "version": "3.2" + }, + "libipset.so.3": { + "package": "ipset", + "version": "3.7.0" + }, + "libmagic.so.1": { + "package": "libmagic", + "version": "1.0.0" + }, + "libdb_cxx-5.3.so": { + "package": "db-c++", + "version": "0" + }, + "libXau.so.6": { + "package": "libxau", + "version": "6.0.0" + }, + "libilbc.so.0": { + "package": "ilbc", + "version": "0.0.2" + }, + "librecode.so.0": { + "package": "recode", + "version": "0.0.0" + }, + "postgresql-bdr:libecpg.so.6": { + "package": "postgresql-bdr", + "version": "6.6" + }, + "postgresql-bdr:libecpg_compat.so.3": { + "package": "postgresql-bdr", + "version": "3.6" + }, + "postgresql-bdr:libpgtypes.so.3": { + "package": "postgresql-bdr", + "version": "3.5" + }, + "libboost_iostreams-mt.so.1.62.0": { + "package": "boost-iostreams", + "version": "1.62.0" + }, + "libboost_iostreams.so.1.62.0": { + "package": "boost-iostreams", + "version": "1.62.0" + }, + "libgudev-1.0.so.0": { + "package": "libgudev", + "version": "0.2.0" + }, + "libpython2.7.so.1.0": { + "package": "python2", + "version": "1.0" + }, + "libattr.so.1": { + "package": "libattr", + "version": "1.1.0" + }, + "libuuid.so.1": { + "package": "libuuid", + "version": "1.3.0" + }, + "librest-0.7.so.0": { + "package": "rest", + "version": "0.0.0" + }, + "librest-extras-0.7.so.0": { + "package": "rest", + "version": "0.0.0" + }, + "libclang.so.5.0": { + "package": "clang-libs", + "version": "5.0" + }, + "libdvdcss.so.2": { + "package": "libdvdcss", + "version": "2.2.0" + }, + "libdaemon.so.0": { + "package": "libdaemon", + "version": "0.5.0" + }, + "libgnome-2.so.0": { + "package": "libgnome", + "version": "0.3200.1" + }, + "libjsoncpp.so.0": { + "package": "jsoncpp", + "version": "0.0.0" + }, + "libarchive.so.13": { + "package": "libarchive", + "version": "13.3.3" + }, + "libpcre2-16.so.0": { + "package": "libpcre2-16", + "version": "0.6.0" + }, + "libffi.so.6": { + "package": "libffi", + "version": "6.0.4" + }, + "libcdt.so.5": { + "package": "graphviz", + "version": "5.0.0" + }, + "libcgraph.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvc.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_core.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_dot_layout.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_neato_layout.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_pango.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_xlib.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvpr.so.2": { + "package": "graphviz", + "version": "2.0.0" + }, + "liblab_gamut.so.1": { + "package": "graphviz", + "version": "1.0.0" + }, + "libpathplan.so.4": { + "package": "graphviz", + "version": "4.0.0" + }, + "libxdot.so.4": { + "package": "graphviz", + "version": "4.0.0" + }, + "libvncclient.so.1": { + "package": "libvncserver", + "version": "1.0.0" + }, + "libvncserver.so.1": { + "package": "libvncserver", + "version": "1.0.0" + }, + "libXvMC.so.1": { + "package": "libxvmc", + "version": "1.0.0" + }, + "libXvMCW.so.1": { + "package": "libxvmc", + "version": "1.0.0" + }, + "libedit.so.0": { + "package": "libedit", + "version": "0.0.56" + }, + "libstartup-notification-1.so.0": { + "package": "startup-notification", + "version": "0.0.0" + }, + "libhylafax-6.0.so.6": { + "package": "hylafax", + "version": "6" + }, + "libFLAC++.so.6": { + "package": "flac", + "version": "6.3.0" + }, + "libFLAC.so.8": { + "package": "flac", + "version": "8.3.0" + }, + "libpcap.so.1": { + "package": "libpcap", + "version": "1.8.1" + }, + "libSDL2-2.0.so.0": { + "package": "sdl2", + "version": "0.10.0" + }, + "libprotobuf-c.so.1": { + "package": "protobuf-c", + "version": "1.0.0" + }, + "cmd-mirror.so": { + "package": "lftp", + "version": "0" + }, + "cmd-sleep.so": { + "package": "lftp", + "version": "0" + }, + "cmd-torrent.so": { + "package": "lftp", + "version": "0" + }, + "liblftp-jobs.so.0": { + "package": "lftp", + "version": "0.0.0" + }, + "liblftp-network.so": { + "package": "lftp", + "version": "0" + }, + "liblftp-pty.so": { + "package": "lftp", + "version": "0" + }, + "liblftp-tasks.so.0": { + "package": "lftp", + "version": "0.0.0" + }, + "proto-file.so": { + "package": "lftp", + "version": "0" + }, + "proto-fish.so": { + "package": "lftp", + "version": "0" + }, + "proto-ftp.so": { + "package": "lftp", + "version": "0" + }, + "proto-http.so": { + "package": "lftp", + "version": "0" + }, + "proto-sftp.so": { + "package": "lftp", + "version": "0" + }, + "libcanberra-gtk.so.0": { + "package": "libcanberra-gtk2", + "version": "0.1.9" + }, + "libnvpair.so.1": { + "package": "zfs-libs", + "version": "1.0.1" + }, + "libuutil.so.1": { + "package": "zfs-libs", + "version": "1.0.1" + }, + "libzfs.so.2": { + "package": "zfs-libs", + "version": "2.0.0" + }, + "libzfs_core.so.1": { + "package": "zfs-libs", + "version": "1.0.0" + }, + "libzpool.so.2": { + "package": "zfs-libs", + "version": "2.0.0" + }, + "libformw.so.5": { + "package": "ncurses5-widec-libs", + "version": "5.9" + }, + "libmenuw.so.5": { + "package": "ncurses5-widec-libs", + "version": "5.9" + }, + "libncursesw.so.5": { + "package": "ncurses5-widec-libs", + "version": "5.9" + }, + "libpanelw.so.5": { + "package": "ncurses5-widec-libs", + "version": "5.9" + }, + "libmikmod.so.3": { + "package": "libmikmod", + "version": "3.3.0" + }, + "libwayland-egl.so.1": { + "package": "mesa-libwayland-egl", + "version": "1.0.0" + }, + "libbz2.so.1": { + "package": "libbz2", + "version": "1.0.6" + }, + "libfftw3f.so.3": { + "package": "fftw-single-libs", + "version": "3.5.6" + }, + "libfftw3f_threads.so.3": { + "package": "fftw-single-libs", + "version": "3.5.6" + }, + "libencfs.so.6": { + "package": "encfs", + "version": "6.0.2" + }, + "libgpgmepp.so.6": { + "package": "gpgmepp", + "version": "6.4.0" + }, + "libcogl-gles2.so.20": { + "package": "cogl", + "version": "20.4.2" + }, + "libcogl-pango.so.20": { + "package": "cogl", + "version": "20.4.2" + }, + "libcogl-path.so.20": { + "package": "cogl", + "version": "20.4.2" + }, + "libcogl.so.20": { + "package": "cogl", + "version": "20.4.2" + }, + "libhandle.so.1": { + "package": "xfsprogs-libs", + "version": "1.0.3" + }, + "libxkbui.so.1": { + "package": "libxkbui", + "version": "1.0.0" + }, + "libvanessa_socket.so.2": { + "package": "vanessa_socket", + "version": "2.1.0" + }, + "libdns_sd.so.1": { + "package": "avahi-compat-libdns_sd", + "version": "1.0.0" + }, + "libads-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libauth-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libdfs-server-ad-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libnetapi.so.0": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libnpa-tstream-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libprinting-migrate-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libsmbd-base-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libsmbd-conn-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libsmbldap.so.2": { + "package": "samba-common-server-libs", + "version": "2" + }, + "libsmbldaphelper-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libQtWebKit.so.4": { + "package": "qt-webkit", + "version": "4.9.4" + }, + "libcloog-isl.so.4": { + "package": "cloog", + "version": "4.0.0" + }, + "libs6rc.so.0.2": { + "package": "s6-rc", + "version": "0.2.1.2" + }, + "libgiblib.so.1": { + "package": "giblib", + "version": "1.0.6" + }, + "libmpg123.so.0": { + "package": "mpg123", + "version": "0.44.6" + }, + "libout123.so.0": { + "package": "mpg123", + "version": "0.2.1" + }, + "libodbc.so.2": { + "package": "unixodbc", + "version": "2.0.0" + }, + "libodbccr.so.2": { + "package": "unixodbc", + "version": "2.0.0" + }, + "libodbcinst.so.2": { + "package": "unixodbc", + "version": "2.0.0" + }, + "libXdamage.so.1": { + "package": "libxdamage", + "version": "1.1.0" + }, + "libexiv2.so.14": { + "package": "exiv2", + "version": "14.0.0" + }, + "libxml2.so.2": { + "package": "libxml2", + "version": "2.9.8" + }, + "libmtp.so.9": { + "package": "libmtp", + "version": "9.3.0" + }, + "libudev.so.1": { + "package": "eudev-libs", + "version": "1.6.3" + }, + "libGL.so.1": { + "package": "mesa-gl", + "version": "1.2.0" + }, + "libusb-1.0.so.0": { + "package": "libusb", + "version": "0.1.0" + }, + "libeggdbus-1.so.0": { + "package": "eggdbus", + "version": "0.0.0" + }, + "libwx_gtk2u_media-2.8.so.0": { + "package": "wxgtk2.8-media", + "version": "0.8.0" + }, + "libgtest.so": { + "package": "gtest", + "version": "0" + }, + "libgtest_main.so": { + "package": "gtest", + "version": "0" + }, + "libaim.so": { + "package": "libpurple-oscar", + "version": "0" + }, + "libicq.so": { + "package": "libpurple-oscar", + "version": "0" + }, + "liboscar.so.0": { + "package": "libpurple-oscar", + "version": "0.0.0" + }, + "libOSMesa.so.8": { + "package": "mesa-osmesa", + "version": "8.0.0" + }, + "libnet.so.1": { + "package": "libnet", + "version": "1.7.0" + }, + "libboost_thread-mt.so.1.62.0": { + "package": "boost-thread", + "version": "1.62.0" + }, + "libpcsclite.so.1": { + "package": "pcsc-lite-libs", + "version": "1.0.0" + }, + "libpcscspy.so.0": { + "package": "pcsc-lite-libs", + "version": "0.0.0" + }, + "libphodav-2.0.so.0": { + "package": "phodav", + "version": "0.0.0" + }, + "libepoxy.so.0": { + "package": "libepoxy", + "version": "0.0.0" + }, + "libORBit-2.so.0": { + "package": "orbit2", + "version": "0.1.0" + }, + "libORBit-imodule-2.so.0": { + "package": "orbit2", + "version": "0.0.0" + }, + "libORBitCosNaming-2.so.0": { + "package": "orbit2", + "version": "0.1.0" + }, + "libnspr4.so": { + "package": "nspr", + "version": "0" + }, + "libplc4.so": { + "package": "nspr", + "version": "0" + }, + "libplds4.so": { + "package": "nspr", + "version": "0" + }, + "libtsocks.so.1.8": { + "package": "tsocks", + "version": "1.8" + }, + "libmilter.so.1.0.2": { + "package": "libmilter", + "version": "1.0.2" + }, + "libCHARSET3-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libaddns-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libasn1util-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libauthkrb5-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcli-cldap-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcli-ldap-common-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcli-nbt-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcli-smb-common-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcliauth-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcmocka-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcommon-auth-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libdbwrap-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libflag-mapping-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libgenrand-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libgensec-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libgse-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libinterfaces-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libiov-buf-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libkrb5samba-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libldbsamba-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libmessages-dgm-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libmessages-util-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libmsghdr-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libndr-krb5pac.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libndr-nbt.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libndr-samba-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libndr-standard.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libndr.so.0": { + "package": "samba-libs", + "version": "0.1.0" + }, + "libpopt-samba3-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-cluster-support-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-credentials.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libsamba-debug-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-errors.so.1": { + "package": "samba-libs", + "version": "1" + }, + "libsamba-hostconfig.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libsamba-modules-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-security-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-sockets-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-util.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libsamba3-util-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamdb-common-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamdb.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libsecrets3-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libserver-id-db-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libserver-role-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsmb-transport-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsmbconf.so.0": { + "package": "samba-libs", + "version": "0" + }, + "libsmbd-shim-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsocket-blocking-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsys-rw-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libtalloc-report-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libtdb-wrap-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libtevent-util.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libtime-basic-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libutil-cmdline-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libutil-reg-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libutil-setid-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libutil-tdb-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "liblua-5.2.so.0": { + "package": "lua5.2-libs", + "version": "0.0.0" + }, + "libnetfilter_queue.so.1": { + "package": "libnetfilter_queue", + "version": "1.3.0" + }, + "libnetfilter_acct.so.1": { + "package": "libnetfilter_acct", + "version": "1.0.0" + }, + "libXext.so.6": { + "package": "libxext", + "version": "6.4.0" + }, + "libmcrypt.so.4": { + "package": "libmcrypt", + "version": "4.4.8" + }, + "libunique-1.0.so.0": { + "package": "libunique", + "version": "0.100.6" + }, + "libskarnet.so.2.6": { + "package": "skalibs", + "version": "2.6.1.0" + }, + "libnih-dbus.so.1": { + "package": "libnih", + "version": "1.0.0" + }, + "libnih.so.1": { + "package": "libnih", + "version": "1.0.0" + }, + "libboost_system-mt.so.1.62.0": { + "package": "boost-system", + "version": "1.62.0" + }, + "libboost_system.so.1.62.0": { + "package": "boost-system", + "version": "1.62.0" + }, + "libisns.so.0": { + "package": "open-isns-lib", + "version": "0" + }, + "libgs.so.9": { + "package": "ghostscript", + "version": "9.26" + }, + "libijs-0.35.so": { + "package": "ghostscript", + "version": "0" + }, + "libImlib2.so.1": { + "package": "imlib2", + "version": "1.4.10" + }, + "libiperf.so.0": { + "package": "iperf3", + "version": "0.0.0" + }, + "rlm_redis.so": { + "package": "freeradius-redis", + "version": "0" + }, + "rlm_rediswho.so": { + "package": "freeradius-redis", + "version": "0" + }, + "libtorrent.so.19": { + "package": "libtorrent", + "version": "19.0.0" + }, + "libmodplug.so.1": { + "package": "libmodplug", + "version": "1.0.0" + }, + "libdvbv5.so.0": { + "package": "v4l-utils-dvbv5", + "version": "0.0.0" + }, + "libcue.so.2": { + "package": "libcue", + "version": "2.1.0" + }, + "lib21_fts_solr_plugin.so": { + "package": "dovecot-fts-solr", + "version": "0" + }, + "libfakeroot-0.so": { + "package": "fakeroot", + "version": "0" + }, + "libcrack.so.2": { + "package": "cracklib", + "version": "2.9.0" + }, + "libopus.so.0": { + "package": "opus", + "version": "0.6.1" + }, + "libbind9.so.161": { + "package": "bind-libs", + "version": "161.0.2" + }, + "libdns.so.1105": { + "package": "bind-libs", + "version": "1105.0.0" + }, + "libirs.so.161": { + "package": "bind-libs", + "version": "161.0.0" + }, + "libisc.so.1100": { + "package": "bind-libs", + "version": "1100.2.0" + }, + "libisccc.so.161": { + "package": "bind-libs", + "version": "161.0.1" + }, + "libisccfg.so.163": { + "package": "bind-libs", + "version": "163.0.1" + }, + "liblwres.so.161": { + "package": "bind-libs", + "version": "161.0.1" + }, + "rlm_perl.so": { + "package": "freeradius-perl", + "version": "0" + }, + "libqpdf.so.18": { + "package": "qpdf-libs", + "version": "18.1.0" + }, + "libspf2.so.2": { + "package": "libspf2", + "version": "2.1.0" + }, + "libSDL2_mixer-2.0.so.0": { + "package": "sdl2_mixer", + "version": "0.2.0" + }, + "libglut.so.3": { + "package": "freeglut", + "version": "3.10.0" + }, + "libEGL.so.1": { + "package": "mesa-egl", + "version": "1.0.0" + }, + "libexo-1.so.0": { + "package": "exo", + "version": "0.1.0" + }, + "libexo-2.so.0": { + "package": "exo", + "version": "0.1.0" + }, + "libboost_date_time-mt.so.1.62.0": { + "package": "boost-date_time", + "version": "1.62.0" + }, + "libboost_date_time.so.1.62.0": { + "package": "boost-date_time", + "version": "1.62.0" + }, + "libnl.so.1": { + "package": "libnl", + "version": "1.1.4" + }, + "libasyncns.so.0": { + "package": "libasyncns", + "version": "0.3.1" + }, + "libserf-1.so.1": { + "package": "serf", + "version": "1.3.0" + }, + "libaugeas.so.0": { + "package": "augeas-libs", + "version": "0.23.0" + }, + "libfa.so.1": { + "package": "augeas-libs", + "version": "1.4.5" + }, + "libaudit.so.1": { + "package": "audit-libs", + "version": "1.0.0" + }, + "libauparse.so.0": { + "package": "audit-libs", + "version": "0.0.0" + }, + "libgcrypt.so.20": { + "package": "libgcrypt", + "version": "20.2.3" + }, + "libSDL2_image-2.0.so.0": { + "package": "sdl2_image", + "version": "0.2.3" + }, + "libsecret-1.so.0": { + "package": "libsecret", + "version": "0.0.0" + }, + "libcanberra.so.0": { + "package": "libcanberra", + "version": "0.2.5" + }, + "libssh.so.4": { + "package": "libssh", + "version": "4.4.3" + }, + "libssh_threads.so.4": { + "package": "libssh", + "version": "4.4.3" + }, + "libcelt051.so.0": { + "package": "celt051", + "version": "0.0.0" + }, + "libgd.so.3": { + "package": "libgd", + "version": "3.0.5" + }, + "libobjc.so.4": { + "package": "libobjc", + "version": "4.0.0" + }, + "libappstream-builder.so.8": { + "package": "appstream-glib-builder", + "version": "8.0.10" + }, + "libetpan.so.20": { + "package": "libetpan", + "version": "20.1.0" + }, + "libmupdf.so.0": { + "package": "mupdf", + "version": "0" + }, + "libmupdfthird.so.0": { + "package": "mupdf", + "version": "0" + }, + "libmpfr.so.4": { + "package": "mpfr3", + "version": "4.1.5" + }, + "psqlodbca.so": { + "package": "psqlodbc", + "version": "0" + }, + "psqlodbcw.so": { + "package": "psqlodbc", + "version": "0" + }, + "libisl.so.15": { + "package": "isl", + "version": "15.3.0" + }, + "libtty.so.0": { + "package": "termrec", + "version": "0.0.0" + }, + "libguile-2.0.so.22": { + "package": "guile-libs", + "version": "22.8.1" + }, + "libee.so.0": { + "package": "libee", + "version": "0.0.0" + }, + "libasr.so.0": { + "package": "libasr", + "version": "0.0.2" + }, + "liblutok.so.3": { + "package": "lutok", + "version": "3.0.0" + }, + "libncftp.so.3": { + "package": "ncftp", + "version": "3" + }, + "libgettextlib-0.19.8.1.so": { + "package": "gettext", + "version": "0" + }, + "libgettextsrc-0.19.8.1.so": { + "package": "gettext", + "version": "0" + }, + "libp11-kit.so.0": { + "package": "p11-kit", + "version": "0.1.0" + }, + "libc-client.so.1": { + "package": "c-client", + "version": "1.0.0" + }, + "libjack.so.0": { + "package": "jack", + "version": "0.1.0" + }, + "libjacknet.so.0": { + "package": "jack", + "version": "0.1.0" + }, + "libjackserver.so.0": { + "package": "jack", + "version": "0.1.0" + }, + "libe2p.so.2": { + "package": "e2fsprogs-libs", + "version": "2.3" + }, + "libext2fs.so.2": { + "package": "e2fsprogs-libs", + "version": "2.4" + }, + "libss.so.2": { + "package": "e2fsprogs-libs", + "version": "2.0" + }, + "libglamor.so.0": { + "package": "glamor-egl", + "version": "0.0.0" + }, + "libdnet.so.1": { + "package": "libdnet", + "version": "1.0.1" + }, + "libidn.so.11": { + "package": "libidn", + "version": "11.6.16" + }, + "libblkid.so.1": { + "package": "libblkid", + "version": "1.1.0" + }, + "libkamailio_ims.so.0": { + "package": "kamailio", + "version": "0.1" + }, + "libprint.so.1": { + "package": "kamailio", + "version": "1.2" + }, + "libsrdb1.so.1": { + "package": "kamailio", + "version": "1.0" + }, + "libsrdb2.so.1": { + "package": "kamailio", + "version": "1.0" + }, + "libsrutils.so.1": { + "package": "kamailio", + "version": "1.0" + }, + "libtrie.so.1": { + "package": "kamailio", + "version": "1.0" + }, + "libgcab-1.0.so.0": { + "package": "libgcab", + "version": "0.0.0" + }, + "liblinenoise.so.0": { + "package": "linenoise", + "version": "0.0.0" + }, + "libgsf-1.so.114": { + "package": "libgsf", + "version": "114.0.41" + }, + "libXrandr.so.2": { + "package": "libxrandr", + "version": "2.2.0" + }, + "libboost_math_c99-mt.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_c99.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_c99f-mt.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_c99f.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_c99l-mt.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_c99l.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_tr1-mt.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_tr1.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_tr1f-mt.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_tr1f.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_tr1l-mt.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libboost_math_tr1l.so.1.62.0": { + "package": "boost-math", + "version": "1.62.0" + }, + "libgirepository-1.0.so.1": { + "package": "gobject-introspection", + "version": "1.0.0" + }, + "libexpect5.45.so": { + "package": "expect", + "version": "0" + }, + "libcunit.so.1": { + "package": "cunit", + "version": "1.0.1" + }, + "liblttng-ust-ctl.so.4": { + "package": "lttng-ust", + "version": "4.0.0" + }, + "liblttng-ust-cyg-profile-fast.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-cyg-profile.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-dl.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-fd.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-fork.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-libc-wrapper.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-pthread-wrapper.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-tracepoint.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "libiniparser.so.0": { + "package": "iniparser", + "version": "0" + }, + "libclucene-contribs-lib.so.1": { + "package": "clucene-contribs", + "version": "2.3.3.4" + }, + "libsexy.so.2": { + "package": "libsexy", + "version": "2.0.4" + }, + "libatasmart.so.4": { + "package": "libatasmart", + "version": "4.0.5" + }, + "libfcgi.so.0": { + "package": "fcgi", + "version": "0.0.0" + }, + "libotr.so.2": { + "package": "libotr3", + "version": "2.2.1" + }, + "libxvidcore.so.4": { + "package": "xvidcore", + "version": "4.3" + }, + "libfam.so.0": { + "package": "gamin", + "version": "0.0.0" + }, + "libgamin-1.so.0": { + "package": "gamin", + "version": "0.1.10" + }, + "libip4tc.so.0": { + "package": "iptables", + "version": "0.1.0" + }, + "libip6tc.so.0": { + "package": "iptables", + "version": "0.1.0" + }, + "libipq.so.0": { + "package": "iptables", + "version": "0.0.0" + }, + "libiptc.so.0": { + "package": "iptables", + "version": "0.0.0" + }, + "libxtables.so.12": { + "package": "iptables", + "version": "12.0.0" + }, + "libdriver_pgsql.so": { + "package": "dovecot-pgsql", + "version": "0" + }, + "libneon.so.27": { + "package": "neon", + "version": "27.3.2" + }, + "libatk-bridge-2.0.so.0": { + "package": "at-spi2-atk", + "version": "0.0.0" + }, + "libglapi.so.0": { + "package": "mesa-glapi", + "version": "0.0.0" + }, + "libstfl.so.0": { + "package": "stfl", + "version": "0.24" + }, + "libtasn1.so.6": { + "package": "libtasn1", + "version": "6.5.4" + }, + "libgomp.so.1": { + "package": "libgomp", + "version": "1.0.0" + }, + "libdv.so.4": { + "package": "libdv", + "version": "4.0.3" + }, + "libclucene-core.so.1": { + "package": "clucene", + "version": "2.3.3.4" + }, + "libclucene-shared.so.1": { + "package": "clucene", + "version": "2.3.3.4" + }, + "libvirt-admin.so.0": { + "package": "libvirt-client", + "version": "0.5005.0" + }, + "libvirt-lxc.so.0": { + "package": "libvirt-client", + "version": "0.5005.0" + }, + "libvirt-qemu.so.0": { + "package": "libvirt-client", + "version": "0.5005.0" + }, + "libvirt.so.0": { + "package": "libvirt-client", + "version": "0.5005.0" + }, + "libform.so.5": { + "package": "ncurses5-libs", + "version": "5.9" + }, + "libmenu.so.5": { + "package": "ncurses5-libs", + "version": "5.9" + }, + "libncurses.so.5": { + "package": "ncurses5-libs", + "version": "5.9" + }, + "libpanel.so.5": { + "package": "ncurses5-libs", + "version": "5.9" + }, + "libgstbase-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstcheck-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstcontroller-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstdataprotocol-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstnet-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstreamer-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libverto-glib.so.1": { + "package": "libverto-glib", + "version": "1.0.0" + }, + "libspreadsheet-1.12.36.so": { + "package": "gnumeric", + "version": "0" + }, + "libvdehist.so.0": { + "package": "vde2-libs", + "version": "0.0.1" + }, + "libvdemgmt.so.0": { + "package": "vde2-libs", + "version": "0.0.1" + }, + "libvdeplug.so.3": { + "package": "vde2-libs", + "version": "3.0.1" + }, + "libvdesnmp.so.0": { + "package": "vde2-libs", + "version": "0.0.1" + }, + "libbonjour.so": { + "package": "libpurple-bonjour", + "version": "0" + }, + "libwayland-server.so.0": { + "package": "wayland-libs-server", + "version": "0.1.0" + }, + "libtk8.6.so": { + "package": "tk", + "version": "0" + }, + "libpcp.so.1": { + "package": "pgpool", + "version": "1.0.0" + }, + "proto_dhcp.so": { + "package": "freeradius", + "version": "0" + }, + "proto_vmps.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_always.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_attr_filter.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_cache.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_cache_rbtree.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_chap.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_counter.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_cram.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_date.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_detail.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_dhcp.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_digest.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_dynamic_clients.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_exec.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_expiration.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_expr.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_files.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_ippool.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_linelog.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_logintime.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_mschap.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_otp.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_pap.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_passwd.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_preprocess.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_radutmp.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_realm.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_replicate.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_soh.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_sometimes.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_test.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_unix.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_unpack.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_utf8.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_wimax.so": { + "package": "freeradius", + "version": "0" + }, + "libgps.so.22": { + "package": "gpsd", + "version": "22.0.0" + }, + "libglade-2.0.so.0": { + "package": "libglade", + "version": "0.0.7" + }, + "libswish-e.so.2": { + "package": "swish-e", + "version": "2.0.0" + }, + "rlm_sql_postgresql.so": { + "package": "freeradius-postgresql", + "version": "0" + }, + "libnetfilter_cttimeout.so.1": { + "package": "libnetfilter_cttimeout", + "version": "1.0.0" + }, + "libgucharmap_2_90.so.7": { + "package": "gucharmap", + "version": "7.0.0" + }, + "libgailutil-3.so.0": { + "package": "gtk+3.0", + "version": "0.0.0" + }, + "libgdk-3.so.0": { + "package": "gtk+3.0", + "version": "0.2200.21" + }, + "libgtk-3.so.0": { + "package": "gtk+3.0", + "version": "0.2200.21" + }, + "libgvplugin_gdk.so.6": { + "package": "graphviz-gtk", + "version": "6.0.0" + }, + "libgvplugin_gtk.so.6": { + "package": "graphviz-gtk", + "version": "6.0.0" + }, + "libgvplugin_rsvg.so.6": { + "package": "graphviz-gtk", + "version": "6.0.0" + }, + "libmnl.so.0": { + "package": "libmnl", + "version": "0.2.0" + }, + "libproxychains4.so": { + "package": "proxychains-ng", + "version": "0" + }, + "libvdpau.so.1": { + "package": "libvdpau", + "version": "1.0.0" + }, + "libmng.so.2": { + "package": "libmng", + "version": "2.0.2" + }, + "libnfnetlink.so.0": { + "package": "libnfnetlink", + "version": "0.2.0" + }, + "libicudata.so.59": { + "package": "icu-libs", + "version": "59.1" + }, + "libicui18n.so.59": { + "package": "icu-libs", + "version": "59.1" + }, + "libicuio.so.59": { + "package": "icu-libs", + "version": "59.1" + }, + "libicutest.so.59": { + "package": "icu-libs", + "version": "59.1" + }, + "libicutu.so.59": { + "package": "icu-libs", + "version": "59.1" + }, + "libicuuc.so.59": { + "package": "icu-libs", + "version": "59.1" + }, + "libc.musl-x86_64.so.1": { + "package": "musl", + "version": "1" + }, + "librtmp.so.1": { + "package": "librtmp", + "version": "1" + }, + "libyaml-0.so.2": { + "package": "yaml", + "version": "2.0.5" + }, + "libCEGUIBase-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUIExpatParser-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUIFalagardWRBase-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUILuaScriptModule-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUIOpenGLRenderer-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUITGAImageCodec-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUITinyXMLParser-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUItoluapp-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libkeybinder.so.0": { + "package": "keybinder", + "version": "0.1.0" + }, + "libcups.so.2": { + "package": "cups-libs", + "version": "2" + }, + "libcupsimage.so.2": { + "package": "cups-libs", + "version": "2" + }, + "libpcre2-32.so.0": { + "package": "libpcre2-32", + "version": "0.6.0" + }, + "libasprintf.so.0": { + "package": "gettext-asprintf", + "version": "0.0.0" + }, + "libcc1.so.0": { + "package": "gcc", + "version": "0.0.0" + }, + "libcilkrts.so.5": { + "package": "gcc", + "version": "5.0.0" + }, + "libitm.so.1": { + "package": "gcc", + "version": "1.0.0" + }, + "rlm_sql_sqlite.so": { + "package": "freeradius-sqlite", + "version": "0" + }, + "libavahi-glib.so.1": { + "package": "avahi-glib", + "version": "1.0.2" + }, + "libavahi-gobject.so.0": { + "package": "avahi-glib", + "version": "0.0.4" + }, + "libdriver_mysql.so": { + "package": "dovecot-mysql", + "version": "0" + }, + "libboost_signals-mt.so.1.62.0": { + "package": "boost-signals", + "version": "1.62.0" + }, + "libboost_signals.so.1.62.0": { + "package": "boost-signals", + "version": "1.62.0" + }, + "libXfont.so.1": { + "package": "libxfont", + "version": "1.4.1" + }, + "libatomic_ops.so.1": { + "package": "libatomic_ops", + "version": "1.0.4" + }, + "libatomic_ops_gpl.so.1": { + "package": "libatomic_ops", + "version": "1.0.4" + }, + "libgss.so.1": { + "package": "libgss", + "version": "1.1.0" + }, + "libxfce4kbd-private-3.so.0": { + "package": "libxfce4ui-gtk3", + "version": "0.0.0" + }, + "libxfce4ui-2.so.0": { + "package": "libxfce4ui-gtk3", + "version": "0.0.0" + }, + "liblcms.so.1": { + "package": "liblcms", + "version": "1.0.19" + }, + "libslang.so.2": { + "package": "slang", + "version": "2.3.1" + }, + "rlm_ldap.so": { + "package": "freeradius-ldap", + "version": "0" + }, + "libotr.so.5": { + "package": "libotr", + "version": "5.1.1" + }, + "libgettextpo.so.0": { + "package": "gettext-libs", + "version": "0.5.4" + }, + "libharfbuzz-icu.so.0": { + "package": "harfbuzz-icu", + "version": "0.10600.3" + }, + "libxmlrpc++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_abyss++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_cpp.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_packetsocket.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_server++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_server_abyss++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_server_cgi++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_server_pstream++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_util++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxklavier.so.16": { + "package": "libxklavier", + "version": "16.4.0" + }, + "rlm_python.so": { + "package": "freeradius-python", + "version": "0" + }, + "libfuse.so.2": { + "package": "fuse", + "version": "2.9.8" + }, + "libulockmgr.so.1": { + "package": "fuse", + "version": "1.0.1" + }, + "liblzma.so.5": { + "package": "xz-libs", + "version": "5.2.3" + }, + "libpcre32.so.0": { + "package": "libpcre32", + "version": "0.0.9" + }, + "libfts.so.0": { + "package": "fts", + "version": "0.0.0" + }, + "libslim.so.1.3.6": { + "package": "slim", + "version": "1.3.6" + }, + "libtls.so.16": { + "package": "libressl2.6-libtls", + "version": "16.0.1" + }, + "libgsm.so.1": { + "package": "gsm", + "version": "1.0.12" + }, + "libGeoIP.so.1": { + "package": "geoip", + "version": "1.6.11" + }, + "libisofs.so.6": { + "package": "libisofs", + "version": "6.84.0" + }, + "libsndfile.so.1": { + "package": "libsndfile", + "version": "1.0.28" + }, + "libjabber.so.0": { + "package": "libpurple-xmpp", + "version": "0.0.0" + }, + "libxmpp.so": { + "package": "libpurple-xmpp", + "version": "0" + }, + "liblua-5.3.so.0": { + "package": "lua5.3-libs", + "version": "0.0.0" + }, + "libpam.so.0": { + "package": "linux-pam", + "version": "0.84.2" + }, + "libpam_misc.so.0": { + "package": "linux-pam", + "version": "0.82.1" + }, + "libpamc.so.0": { + "package": "linux-pam", + "version": "0.82.1" + }, + "libmcpp.so.0": { + "package": "mcpp-libs", + "version": "0.3.0" + }, + "libSDL_image-1.2.so.0": { + "package": "sdl_image", + "version": "0.8.4" + }, + "libgladeui-1.so.11": { + "package": "glade3", + "version": "11.2.0" + }, + "libnftnl.so.7": { + "package": "libnftnl-libs", + "version": "7.0.0" + }, + "libcdio++.so.0": { + "package": "libcdio++", + "version": "0.0.2" + }, + "libiso9660++.so.0": { + "package": "libcdio++", + "version": "0.0.0" + }, + "libg7221codec.so.2": { + "package": "pjproject", + "version": "2" + }, + "libilbccodec.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpj.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjlib-util.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjmedia-audiodev.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjmedia-codec.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjmedia-videodev.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjmedia.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjnath.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsip-simple.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsip-ua.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsip.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsua.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsua2.so.2": { + "package": "pjproject", + "version": "2" + }, + "libyuv.so.2": { + "package": "pjproject", + "version": "2" + }, + "libvte.so.9": { + "package": "vte", + "version": "9.2800.2" + }, + "libcharon.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libradius.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libsimaka.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libstrongswan.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libtls.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libvici.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libavc1394.so.0": { + "package": "libavc1394", + "version": "0.3.0" + }, + "librom1394.so.0": { + "package": "libavc1394", + "version": "0.3.0" + }, + "libXrender.so.1": { + "package": "libxrender", + "version": "1.3.0" + }, + "libpcre16.so.0": { + "package": "libpcre16", + "version": "0.2.9" + }, + "libfastjson.so.4": { + "package": "libfastjson", + "version": "4.2.0" + }, + "rlm_sql.so": { + "package": "freeradius-sql", + "version": "0" + }, + "rlm_sql_null.so": { + "package": "freeradius-sql", + "version": "0" + }, + "rlm_sqlcounter.so": { + "package": "freeradius-sql", + "version": "0" + }, + "rlm_sqlippool.so": { + "package": "freeradius-sql", + "version": "0" + }, + "libpoppler-glib.so.8": { + "package": "poppler-glib", + "version": "8.9.0" + }, + "libwebp.so.7": { + "package": "libwebp", + "version": "7.0.0" + }, + "libwebpdecoder.so.3": { + "package": "libwebp", + "version": "3.0.0" + }, + "libwebpdemux.so.2": { + "package": "libwebp", + "version": "2.0.2" + }, + "libwebpmux.so.3": { + "package": "libwebp", + "version": "3.0.0" + }, + "libSDL-1.2.so.0": { + "package": "sdl", + "version": "0.11.4" + }, + "libnewt.so.0.52": { + "package": "newt", + "version": "0.52.20" + }, + "libespeak.so.1": { + "package": "espeak", + "version": "1.1.48" + }, + "libpython3.6m.so.1.0": { + "package": "python3", + "version": "1.0" + }, + "libpython3.so": { + "package": "python3", + "version": "0" + }, + "libgmime-2.6.so.0": { + "package": "gmime", + "version": "0.620.0" + }, + "libxkbfile.so.1": { + "package": "libxkbfile", + "version": "1.0.2" + }, + "libtheora.so.0": { + "package": "libtheora", + "version": "0.3.10" + }, + "libtheoradec.so.1": { + "package": "libtheora", + "version": "1.1.4" + }, + "libtheoraenc.so.1": { + "package": "libtheora", + "version": "1.1.2" + }, + "libunique-3.0.so.0": { + "package": "libunique3", + "version": "0.0.2" + }, + "libpopt.so.0": { + "package": "popt", + "version": "0.0.0" + }, + "libgiomm-2.4.so.1": { + "package": "glibmm", + "version": "1.3.0" + }, + "libglibmm-2.4.so.1": { + "package": "glibmm", + "version": "1.3.0" + }, + "libglibmm_generate_extra_defs-2.4.so.1": { + "package": "glibmm", + "version": "1.3.0" + }, + "libSM.so.6": { + "package": "libsm", + "version": "6.0.1" + }, + "libquvi-0.9-0.9.4.so": { + "package": "libquvi", + "version": "0" + }, + "libdc1394.so.22": { + "package": "libdc1394", + "version": "22.2.1" + }, + "libvorbis.so.0": { + "package": "libvorbis", + "version": "0.4.8" + }, + "libvorbisenc.so.2": { + "package": "libvorbis", + "version": "2.0.11" + }, + "libvorbisfile.so.3": { + "package": "libvorbis", + "version": "3.3.7" + }, + "libgtkspell.so.0": { + "package": "gtkspell", + "version": "0.0.0" + }, + "libpostfix-dns.so": { + "package": "postfix", + "version": "0" + }, + "libpostfix-global.so": { + "package": "postfix", + "version": "0" + }, + "libpostfix-master.so": { + "package": "postfix", + "version": "0" + }, + "libpostfix-tls.so": { + "package": "postfix", + "version": "0" + }, + "libpostfix-util.so": { + "package": "postfix", + "version": "0" + }, + "librrd.so.4": { + "package": "librrd", + "version": "4.3.5" + }, + "libck-connector.so.0": { + "package": "consolekit2", + "version": "0.0.0" + }, + "libconsolekit.so.1": { + "package": "consolekit2", + "version": "1.0.0" + }, + "libdvdnav.so.4": { + "package": "libdvdnav", + "version": "4.2.0" + }, + "libdaq.so.2": { + "package": "daq", + "version": "2.0.4" + }, + "libsfbpf.so.0": { + "package": "daq", + "version": "0.0.1" + }, + "libsvn_swig_perl-1.so.0": { + "package": "perl-subversion", + "version": "0.0.0" + }, + "libexecline.so.2.3": { + "package": "execline", + "version": "2.3.0.3" + }, + "libHDB-SAMBA4-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libLIBWBCLIENT-OLD-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libauth-unix-token-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libauth4-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libcluster-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libdb-glue-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libdcerpc-samr.so.0": { + "package": "samba-dc-libs", + "version": "0.0.1" + }, + "libdcerpc-server.so.0": { + "package": "samba-dc-libs", + "version": "0.0.1" + }, + "libdnsserver-common-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libdsdb-module-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libhdb-samba4.so.11": { + "package": "samba-dc-libs", + "version": "11.0.2" + }, + "libkdc-samba4.so.2": { + "package": "samba-dc-libs", + "version": "2.0.0" + }, + "libpac-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libposix-eadb-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libprocess-model-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libsamba-policy.so.0": { + "package": "samba-dc-libs", + "version": "0.0.1" + }, + "libservice-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libshares-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libkmod.so.2": { + "package": "kmod", + "version": "2.3.2" + }, + "libv4l1.so.0": { + "package": "v4l-utils-libs", + "version": "0.0.0" + }, + "libv4l2.so.0": { + "package": "v4l-utils-libs", + "version": "0.0.0" + }, + "libv4l2rds.so.0": { + "package": "v4l-utils-libs", + "version": "0.0.0" + }, + "libv4lconvert.so.0": { + "package": "v4l-utils-libs", + "version": "0.0.0" + }, + "libfreeswitch.so.1": { + "package": "freeswitch", + "version": "1.0.0" + }, + "libvarnishapi.so.1": { + "package": "varnish-libs", + "version": "1.0.6" + }, + "libfdt.so.1": { + "package": "libfdt", + "version": "0" + }, + "libcppunit_subunit.so.0": { + "package": "subunit-libs", + "version": "0.0.0" + }, + "libsubunit.so.0": { + "package": "subunit-libs", + "version": "0.0.0" + }, + "libblktapctl.so.1.0": { + "package": "xen-libs", + "version": "1.0.0" + }, + "libfsimage.so.1.0": { + "package": "xen-libs", + "version": "1.0.0" + }, + "libvhd.so.1.0": { + "package": "xen-libs", + "version": "1.0.0" + }, + "libxencall.so.1": { + "package": "xen-libs", + "version": "1.0" + }, + "libxenctrl.so.4.9": { + "package": "xen-libs", + "version": "4.9.0" + }, + "libxendevicemodel.so.1": { + "package": "xen-libs", + "version": "1.0" + }, + "libxenevtchn.so.1": { + "package": "xen-libs", + "version": "1.0" + }, + "libxenforeignmemory.so.1": { + "package": "xen-libs", + "version": "1.1" + }, + "libxengnttab.so.1": { + "package": "xen-libs", + "version": "1.1" + }, + "libxenguest.so.4.9": { + "package": "xen-libs", + "version": "4.9.0" + }, + "libxenlight.so.4.9": { + "package": "xen-libs", + "version": "4.9.0" + }, + "libxenstat.so.0": { + "package": "xen-libs", + "version": "0.0" + }, + "libxenstore.so.3.0": { + "package": "xen-libs", + "version": "3.0.3" + }, + "libxentoollog.so.1": { + "package": "xen-libs", + "version": "1.0" + }, + "libxenvchan.so.4.9": { + "package": "xen-libs", + "version": "4.9.0" + }, + "libxlutil.so.4.9": { + "package": "xen-libs", + "version": "4.9.0" + }, + "libpkgconf.so.2": { + "package": "pkgconf", + "version": "2.0.0" + }, + "libnjb.so.5": { + "package": "libnjb", + "version": "5.1.1" + }, + "libgcj-tools.so.17": { + "package": "libgcj", + "version": "17.0.0" + }, + "libgcj.so.17": { + "package": "libgcj", + "version": "17.0.0" + }, + "libgcj_bc.so.1": { + "package": "libgcj", + "version": "1.0.0" + }, + "libgij.so.17": { + "package": "libgcj", + "version": "17.0.0" + }, + "libjavamath.so": { + "package": "libgcj", + "version": "0" + }, + "libjvm.so": { + "package": "libgcj", + "version": "0" + }, + "libboost_python3-mt.so.1.62.0": { + "package": "boost-python3", + "version": "1.62.0" + }, + "libboost_python3.so.1.62.0": { + "package": "boost-python3", + "version": "1.62.0" + }, + "libaudcore.so.5": { + "package": "audacious", + "version": "5.0.0" + }, + "libaudgui.so.5": { + "package": "audacious", + "version": "5.0.0" + }, + "libaudtag.so.3": { + "package": "audacious", + "version": "3.0.0" + }, + "libcmph.so.0": { + "package": "libcmph", + "version": "0.0.0" + }, + "librlog.so.5": { + "package": "rlog", + "version": "5.0.0" + }, + "libMESSAGING-SEND-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libcli-spoolss-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libdcerpc-binding.so.0": { + "package": "samba-common-libs", + "version": "0.0.1" + }, + "libdcerpc-samba-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "liblibcli-lsa3-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "liblibcli-netlogon3-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "liblibsmb-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libmsrpc3-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libndr-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libsamba-passdb.so.0": { + "package": "samba-common-libs", + "version": "0.27.0" + }, + "libtrusts-util-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libs6net.so.2.3": { + "package": "s6-networking", + "version": "2.3.0.2" + }, + "libstls.so.2.3": { + "package": "s6-networking", + "version": "2.3.0.2" + }, + "libappstream-glib.so.8": { + "package": "appstream-glib", + "version": "8.0.10" + }, + "libx265.so.130": { + "package": "x265", + "version": "130" + }, + "libxfsm-4.6.so.0": { + "package": "xfce4-session", + "version": "0.0.0" + }, + "libbluetooth.so.3": { + "package": "bluez-libs", + "version": "3.18.16" + }, + "libspice-server.so.1": { + "package": "spice-server", + "version": "1.12.5" + }, + "libgphoto2.so.6": { + "package": "libgphoto2", + "version": "6.0.0" + }, + "libgphoto2_port.so.12": { + "package": "libgphoto2", + "version": "12.0.0" + }, + "libgnomevfs-2.so.0": { + "package": "gnome-vfs", + "version": "0.2400.4" + }, + "libaio.so.1": { + "package": "libaio", + "version": "1.0.1" + }, + "libyajl.so.2": { + "package": "yajl", + "version": "2.1.0" + }, + "libsngtc_node.so": { + "package": "sngtc_client", + "version": "0" + }, + "liboil-0.3.so.0": { + "package": "liboil", + "version": "0.3.0" + }, + "libsnappy.so.1": { + "package": "snappy", + "version": "1.3.1" + }, + "libzdb.so.11": { + "package": "libzdb", + "version": "11.0.0" + }, + "libart_lgpl_2.so.2": { + "package": "libart-lgpl", + "version": "2.3.21" + }, + "libenchant.so.1": { + "package": "enchant", + "version": "1.6.0" + }, + "libgfortran.so.3": { + "package": "libgfortran", + "version": "3.0.0" + }, + "libgnutlsxx.so.28": { + "package": "gnutls-c++", + "version": "28.1.0" + }, + "libsylph-0.so.1": { + "package": "sylpheed", + "version": "1.2.1" + }, + "libsylpheed-plugin-0.so.1": { + "package": "sylpheed", + "version": "1.2.1" + }, + "libqrencode.so.4": { + "package": "libqrencode", + "version": "4.0.0" + }, + "librevenge-0.0.so.0": { + "package": "librevenge", + "version": "0.0.4" + }, + "librevenge-generators-0.0.so.0": { + "package": "librevenge", + "version": "0.0.4" + }, + "librevenge-stream-0.0.so.0": { + "package": "librevenge", + "version": "0.0.4" + }, + "libdvdread.so.4": { + "package": "libdvdread", + "version": "4.2.0" + }, + "libbstring.so.0": { + "package": "coova-chilli", + "version": "0.0.0" + }, + "libchilli.so.0": { + "package": "coova-chilli", + "version": "0.0.0" + }, + "libverto-libevent.so.1": { + "package": "libverto-libevent", + "version": "1.0.0" + }, + "libsoup-2.4.so.1": { + "package": "libsoup", + "version": "1.8.0" + }, + "libsoup-gnome-2.4.so.1": { + "package": "libsoup", + "version": "1.8.0" + }, + "libqca.so.2": { + "package": "qca", + "version": "2.1.3" + }, + "libf2fs.so.0": { + "package": "f2fs-tools-libs", + "version": "0.0.1" + }, + "libf2fs_format.so.0": { + "package": "f2fs-tools-libs", + "version": "0.0.1" + }, + "libebml.so.4": { + "package": "libebml", + "version": "4.0.0" + }, + "libswfdec-0.9.so.2": { + "package": "swfdec", + "version": "2.0.0" + }, + "libswfdec-gtk-0.9.so.2": { + "package": "swfdec", + "version": "2.0.0" + }, + "postfix-ldap.so": { + "package": "postfix-ldap", + "version": "0" + }, + "libcdda_interface.so.0": { + "package": "cdparanoia-libs", + "version": "0.10.2" + }, + "libcdda_paranoia.so.0": { + "package": "cdparanoia-libs", + "version": "0.10.2" + }, + "libsensors.so.4": { + "package": "lm_sensors", + "version": "4.4.0" + }, + "libpciaccess.so.0": { + "package": "libpciaccess", + "version": "0.11.1" + }, + "libgailutil.so.18": { + "package": "gtk+2.0", + "version": "18.0.1" + }, + "libgdk-x11-2.0.so.0": { + "package": "gtk+2.0", + "version": "0.2400.31" + }, + "libgtk-x11-2.0.so.0": { + "package": "gtk+2.0", + "version": "0.2400.31" + }, + "libjansson.so.4": { + "package": "jansson", + "version": "4.10.0" + }, + "libgif.so.7": { + "package": "giflib", + "version": "7.0.0" + }, + "libprocps.so.6": { + "package": "libproc", + "version": "6.0.0" + }, + "libgnokii.so.7": { + "package": "gnokii-libs", + "version": "7.0.0" + }, + "libcppunit-1.14.so.0": { + "package": "cppunit", + "version": "0.0.0" + }, + "libobrender.so.32": { + "package": "openbox-libs", + "version": "32.0.0" + }, + "libobt.so.2": { + "package": "openbox-libs", + "version": "2.0.2" + }, + "libcdio.so.16": { + "package": "libcdio", + "version": "16.0.0" + }, + "libiso9660.so.10": { + "package": "libcdio", + "version": "10.0.0" + }, + "libudf.so.0": { + "package": "libcdio", + "version": "0.0.0" + }, + "libtevent.so.0": { + "package": "tevent", + "version": "0.9.34" + }, + "libQtCore.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtDBus.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtNetwork.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtScript.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtSql.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtTest.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtXml.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtXmlPatterns.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "rlm_pam.so": { + "package": "freeradius-pam", + "version": "0" + }, + "libbsd.so.0": { + "package": "libbsd", + "version": "0.8.6" + }, + "libxcb-image.so.0": { + "package": "xcb-util-image", + "version": "0.0.0" + }, + "libgcc_s.so.1": { + "package": "libgcc", + "version": "1" + }, + "libdrm.so.2": { + "package": "libdrm", + "version": "2.4.0" + }, + "libdrm_amdgpu.so.1": { + "package": "libdrm", + "version": "1.0.0" + }, + "libdrm_freedreno.so.1": { + "package": "libdrm", + "version": "1.0.0" + }, + "libdrm_intel.so.1": { + "package": "libdrm", + "version": "1.0.0" + }, + "libdrm_nouveau.so.2": { + "package": "libdrm", + "version": "2.0.0" + }, + "libdrm_radeon.so.1": { + "package": "libdrm", + "version": "1.0.1" + }, + "libkms.so.1": { + "package": "libdrm", + "version": "1.0.0" + }, + "libopenjp2.so.7": { + "package": "openjpeg", + "version": "2.3.0" + }, + "libmandoc.so": { + "package": "mdocml", + "version": "0" + }, + "libfftw3.so.3": { + "package": "fftw-double-libs", + "version": "3.5.6" + }, + "libfftw3_threads.so.3": { + "package": "fftw-double-libs", + "version": "3.5.6" + }, + "librabbitmq.so.4": { + "package": "rabbitmq-c", + "version": "4.2.0" + }, + "liba52.so.0": { + "package": "a52dec", + "version": "0.0.0" + }, + "libboost_graph-mt.so.1.62.0": { + "package": "boost-graph", + "version": "1.62.0" + }, + "libboost_graph.so.1.62.0": { + "package": "boost-graph", + "version": "1.62.0" + }, + "libthunarx-2.so.0": { + "package": "thunar", + "version": "0.0.0" + }, + "libavcodec.so.57": { + "package": "ffmpeg-libs", + "version": "57.107.100" + }, + "libavdevice.so.57": { + "package": "ffmpeg-libs", + "version": "57.10.100" + }, + "libavfilter.so.6": { + "package": "ffmpeg-libs", + "version": "6.107.100" + }, + "libavformat.so.57": { + "package": "ffmpeg-libs", + "version": "57.83.100" + }, + "libavresample.so.3": { + "package": "ffmpeg-libs", + "version": "3.7.0" + }, + "libavutil.so.55": { + "package": "ffmpeg-libs", + "version": "55.78.100" + }, + "libpostproc.so.54": { + "package": "ffmpeg-libs", + "version": "54.7.100" + }, + "libswresample.so.2": { + "package": "ffmpeg-libs", + "version": "2.9.100" + }, + "libswscale.so.4": { + "package": "ffmpeg-libs", + "version": "4.8.100" + }, + "librsvg-2.so.2": { + "package": "librsvg", + "version": "2.40.19" + }, + "libseccomp.so.2": { + "package": "libseccomp", + "version": "2.3.2" + }, + "libcord.so.1": { + "package": "gc", + "version": "1.0.3" + }, + "libgc.so.1": { + "package": "gc", + "version": "1.0.3" + }, + "libts.so.0": { + "package": "tslib", + "version": "0.8.1" + }, + "libdovecot-ldap.so.0": { + "package": "dovecot-ldap", + "version": "0.0.0" + }, + "libSDL2_ttf-2.0.so.0": { + "package": "sdl2_ttf", + "version": "0.14.0" + }, + "libnotify.so.4": { + "package": "libnotify", + "version": "4.0.0" + }, + "libatf-c++.so.2": { + "package": "atf", + "version": "2.0.0" + }, + "libatf-c.so.1": { + "package": "atf", + "version": "1.0.0" + }, + "libreplace-samba4.so": { + "package": "libwbclient", + "version": "0" + }, + "libwbclient.so.0": { + "package": "libwbclient", + "version": "0.14" + }, + "libwinbind-client-samba4.so": { + "package": "libwbclient", + "version": "0" + }, + "libgmpxx.so.4": { + "package": "libgmpxx", + "version": "4.5.2" + }, + "libgssglue.so.1": { + "package": "libgssglue", + "version": "1.0.0" + }, + "libinput.so.10": { + "package": "libinput-libs", + "version": "10.13.0" + }, + "libruby.so.2.4": { + "package": "ruby-libs", + "version": "2.4.6" + }, + "libpcrecpp.so.0": { + "package": "libpcrecpp", + "version": "0.0.1" + }, + "libssl.so.1.0.0": { + "package": "libssl1.0", + "version": "1.0.0" + }, + "libnfsidmap.so.0": { + "package": "libnfsidmap", + "version": "0.3.0" + }, + "libapr-1.so.0": { + "package": "apr", + "version": "0.6.3" + }, + "libXcursor.so.1": { + "package": "libxcursor", + "version": "1.0.2" + }, + "libmount.so.1": { + "package": "libmount", + "version": "1.1.0" + }, + "libfontconfig.so.1": { + "package": "fontconfig", + "version": "1.10.1" + }, + "libical.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "libical_cxx.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "libicalss.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "libicalss_cxx.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "libicalvcal.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "liburcu-bp.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-cds.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-common.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-mb.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-qsbr.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-signal.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "libpangox-1.0.so.0": { + "package": "pangox-compat", + "version": "0.0.0" + }, + "libprotobuf-lite.so.14": { + "package": "protobuf", + "version": "14.0.0" + }, + "libprotobuf.so.14": { + "package": "protobuf", + "version": "14.0.0" + }, + "libprotoc.so.14": { + "package": "protobuf", + "version": "14.0.0" + }, + "libbtrfs.so.0": { + "package": "btrfs-progs-libs", + "version": "0.1" + }, + "libcairomm-1.0.so.1": { + "package": "cairomm", + "version": "1.4.0" + }, + "libnghttp2.so.14": { + "package": "nghttp2-libs", + "version": "14.18.0" + }, + "liblcms2.so.2": { + "package": "lcms2", + "version": "2.0.8" + }, + "libexif.so.12": { + "package": "libexif", + "version": "12.3.3" + }, + "libfreetdm.so.1": { + "package": "freeswitch-freetdm", + "version": "1.0.0" + }, + "grosscheck.so": { + "package": "gross", + "version": "0" + }, + "libgdk_pixbuf-2.0.so.0": { + "package": "gdk-pixbuf", + "version": "0.3610.0" + }, + "libgdk_pixbuf_xlib-2.0.so.0": { + "package": "gdk-pixbuf", + "version": "0.3610.0" + }, + "liblastfm.so.1": { + "package": "liblastfm", + "version": "1.0.9" + }, + "liblastfm_fingerprint.so.1": { + "package": "liblastfm", + "version": "1.0.9" + }, + "libudisks2.so.0": { + "package": "udisks2-libs", + "version": "0.0.0" + }, + "libsox.so.3": { + "package": "sox", + "version": "3.0.0" + }, + "libgio-2.0.so.0": { + "package": "glib", + "version": "0.5400.2" + }, + "libglib-2.0.so.0": { + "package": "glib", + "version": "0.5400.2" + }, + "libgmodule-2.0.so.0": { + "package": "glib", + "version": "0.5400.2" + }, + "libgobject-2.0.so.0": { + "package": "glib", + "version": "0.5400.2" + }, + "libgthread-2.0.so.0": { + "package": "glib", + "version": "0.5400.2" + }, + "libfreeradius-dhcp.so": { + "package": "freeradius-lib", + "version": "0" + }, + "libfreeradius-eap.so": { + "package": "freeradius-lib", + "version": "0" + }, + "libfreeradius-radius.so": { + "package": "freeradius-lib", + "version": "0" + }, + "libfreeradius-server.so": { + "package": "freeradius-lib", + "version": "0" + }, + "postfix-pgsql.so": { + "package": "postfix-pgsql", + "version": "0" + }, + "libxcb-cursor.so.0": { + "package": "xcb-util-cursor", + "version": "0.0.0" + }, + "libnetfilter_conntrack.so.3": { + "package": "libnetfilter_conntrack", + "version": "3.6.0" + }, + "libxkbcommon-x11.so.0": { + "package": "libxkbcommon", + "version": "0.0.0" + }, + "libxkbcommon.so.0": { + "package": "libxkbcommon", + "version": "0.0.0" + }, + "libgmp.so.10": { + "package": "gmp", + "version": "10.3.2" + }, + "libmp3lame.so.0": { + "package": "lame", + "version": "0.0.0" + }, + "libconfuse.so.2": { + "package": "confuse", + "version": "2.0.0" + }, + "libcrystalhd.so.3": { + "package": "libcrystalhd", + "version": "3.6" + }, + "libnetcf.so.1": { + "package": "netcf-libs", + "version": "1.4.0" + }, + "libpcre2-8.so.0": { + "package": "pcre2", + "version": "0.6.0" + }, + "libpcre2-posix.so.2": { + "package": "pcre2", + "version": "2.0.0" + }, + "libratbox.so": { + "package": "charybdis", + "version": "0" + }, + "libmysqld.so.18": { + "package": "mariadb-libs", + "version": "18" + }, + "libavahi-ui.so.0": { + "package": "avahi-ui", + "version": "0.1.4" + }, + "libvanessa_adt.so.1": { + "package": "vanessa_adt", + "version": "1.0.0" + }, + "libevent-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libevent_core-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libevent_extra-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libevent_openssl-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libevent_pthreads-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libraw1394.so.11": { + "package": "libraw1394", + "version": "11.1.0" + }, + "libGLU.so.1": { + "package": "glu", + "version": "1.3.1" + }, + "libXft.so.2": { + "package": "libxft", + "version": "2.3.2" + }, + "libjpeg.so.8": { + "package": "libjpeg-turbo", + "version": "8.1.2" + }, + "libturbojpeg.so.0": { + "package": "libjpeg-turbo", + "version": "0.1.0" + }, + "libjasper.so.4": { + "package": "jasper-libs", + "version": "4.0.0" + }, + "libOpenIPMI.so.0": { + "package": "openipmi-libs", + "version": "0.0.5" + }, + "libOpenIPMIcmdlang.so.0": { + "package": "openipmi-libs", + "version": "0.0.5" + }, + "libOpenIPMIglib.so.0": { + "package": "openipmi-libs", + "version": "0.0.1" + }, + "libOpenIPMIposix.so.0": { + "package": "openipmi-libs", + "version": "0.0.1" + }, + "libOpenIPMIpthread.so.0": { + "package": "openipmi-libs", + "version": "0.0.1" + }, + "libOpenIPMIui.so.1": { + "package": "openipmi-libs", + "version": "1.0.1" + }, + "libOpenIPMIutils.so.0": { + "package": "openipmi-libs", + "version": "0.0.1" + }, + "libperditiondb_gdbm.so.0": { + "package": "perdition", + "version": "0.0.0" + }, + "libs6dns.so.2.2": { + "package": "s6-dns", + "version": "2.2.0.1" + }, + "libskadns.so.2.2": { + "package": "s6-dns", + "version": "2.2.0.1" + }, + "libao.so.4": { + "package": "libao", + "version": "4.1.0" + }, + "libpytalloc-util.so.2": { + "package": "py-talloc", + "version": "2.1.10" + }, + "libassuan.so.0": { + "package": "libassuan", + "version": "0.7.4" + }, + "libwx_gtk2u_adv-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_aui-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_core-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_html-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_qa-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_richtext-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_xrc-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libct.so.4": { + "package": "freetds", + "version": "4.0.0" + }, + "libsybdb.so.5": { + "package": "freetds", + "version": "5.1.0" + }, + "libtdsodbc.so.0": { + "package": "freetds", + "version": "0.0.0" + }, + "libwavpack.so.1": { + "package": "wavpack", + "version": "1.2.0" + }, + "libavahi-core.so.7": { + "package": "avahi", + "version": "7.0.2" + }, + "libgpg-error.so.0": { + "package": "libgpg-error", + "version": "0.22.0" + }, + "libmosquitto.so.1": { + "package": "mosquitto-libs", + "version": "1" + }, + "libksba.so.8": { + "package": "libksba", + "version": "8.11.6" + }, + "libportaudio.so.2": { + "package": "portaudio", + "version": "2.0.0" + }, + "libuv.so.1": { + "package": "libuv", + "version": "1.0.0" + }, + "libatspi.so.0": { + "package": "at-spi2-core", + "version": "0.0.1" + }, + "libverto.so.1": { + "package": "libverto", + "version": "1.0.0" + }, + "libXxf86vm.so.1": { + "package": "libxxf86vm", + "version": "1.0.0" + }, + "libgit2.so.25": { + "package": "libgit2", + "version": "0.25.1" + }, + "libDeployPkg.so.0": { + "package": "open-vm-tools", + "version": "0.0.0" + }, + "libguestlib.so.0": { + "package": "open-vm-tools", + "version": "0.0.0" + }, + "libhgfs.so.0": { + "package": "open-vm-tools", + "version": "0.0.0" + }, + "libvmtools.so.0": { + "package": "open-vm-tools", + "version": "0.0.0" + }, + "libsgutils2.so.2": { + "package": "sg3_utils", + "version": "2.0.0" + }, + "libqjson.so.0": { + "package": "qjson", + "version": "0.9.0" + }, + "libcmocka.so.0": { + "package": "cmocka", + "version": "0.4.1" + }, + "libsvn_swig_py-1.so.0": { + "package": "py-subversion", + "version": "0.0.0" + }, + "libpango-1.0.so.0": { + "package": "pango", + "version": "0.4000.14" + }, + "libpangocairo-1.0.so.0": { + "package": "pango", + "version": "0.4000.14" + }, + "libpangoft2-1.0.so.0": { + "package": "pango", + "version": "0.4000.14" + }, + "libpangoxft-1.0.so.0": { + "package": "pango", + "version": "0.4000.14" + }, + "libeinfo.so.1": { + "package": "openrc", + "version": "1" + }, + "librc.so.1": { + "package": "openrc", + "version": "1" + }, + "libgdl-3.so.5": { + "package": "gdl", + "version": "5.0.9" + }, + "libnetsnmpagent.so.30": { + "package": "net-snmp-agent-libs", + "version": "30.0.3" + }, + "libnetsnmphelpers.so.30": { + "package": "net-snmp-agent-libs", + "version": "30.0.3" + }, + "libnetsnmpmibs.so.30": { + "package": "net-snmp-agent-libs", + "version": "30.0.3" + }, + "libnetsnmptrapd.so.30": { + "package": "net-snmp-agent-libs", + "version": "30.0.3" + }, + "libxmlrpc.so.3": { + "package": "xmlrpc-c", + "version": "3.39" + }, + "libxmlrpc_server.so.3": { + "package": "xmlrpc-c", + "version": "3.39" + }, + "libxmlrpc_util.so.3": { + "package": "xmlrpc-c", + "version": "3.39" + }, + "libpyldb-util.so.1": { + "package": "py-ldb", + "version": "1.3.0" + }, + "libudns.so.0": { + "package": "udns", + "version": "0" + }, + "libmms.so.0": { + "package": "libmms", + "version": "0.0.2" + }, + "libexslt.so.0": { + "package": "libxslt", + "version": "0.8.19" + }, + "libxslt.so.1": { + "package": "libxslt", + "version": "1.1.31" + }, + "libosp.so.5": { + "package": "opensp", + "version": "5.0.0" + }, + "libasteriskssl.so.1": { + "package": "asterisk", + "version": "1" + }, + "libbaccats-sqlite3-9.0.5.so": { + "package": "bacula-sqlite", + "version": "0" + }, + "liblzo2.so.2": { + "package": "lzo", + "version": "2.0.0" + }, + "libtag.so.1": { + "package": "taglib", + "version": "1.17.0" + }, + "libtag_c.so.0": { + "package": "taglib", + "version": "0.0.0" + }, + "libgee-0.8.so.2": { + "package": "libgee", + "version": "2.6.0" + }, + "winbind_krb5_locator.so": { + "package": "samba-winbind-krb5-locator", + "version": "0" + }, + "libgladeui-2.so.6": { + "package": "glade", + "version": "6.3.1" + }, + "liblogging-stdlog.so.0": { + "package": "liblogging", + "version": "0.1.0" + }, + "libhunspell-1.6.so.0": { + "package": "hunspell", + "version": "0.0.1" + }, + "libogg.so.0": { + "package": "libogg", + "version": "0.8.3" + }, + "libmad.so.0": { + "package": "libmad", + "version": "0.2.1" + }, + "libIDL-2.so.0": { + "package": "libidl", + "version": "0.0.0" + }, + "libnl-3.so.200": { + "package": "libnl3", + "version": "200.23.0" + }, + "libnl-genl-3.so.200": { + "package": "libnl3", + "version": "200.23.0" + }, + "libnl-idiag-3.so.200": { + "package": "libnl3", + "version": "200.23.0" + }, + "libnl-nf-3.so.200": { + "package": "libnl3", + "version": "200.23.0" + }, + "libnl-route-3.so.200": { + "package": "libnl3", + "version": "200.23.0" + }, + "libnl-xfrm-3.so.200": { + "package": "libnl3", + "version": "200.23.0" + }, + "libsmbclient.so.0": { + "package": "libsmbclient", + "version": "0.2.3" + }, + "libXtst.so.6": { + "package": "libxtst", + "version": "6.1.0" + }, + "liblucene++-contrib.so.0": { + "package": "lucene++", + "version": "3.0.7" + }, + "liblucene++.so.0": { + "package": "lucene++", + "version": "3.0.7" + }, + "libldns.so.1": { + "package": "ldns", + "version": "1.6.17" + }, + "libortp.so.10": { + "package": "ortp", + "version": "10.0.0" + }, + "libgstadaptivedemux-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstbadallocators-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstbadaudio-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstbadbase-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstbadvideo-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstbasecamerabinsrc-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstcodecparsers-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstgl-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstinsertbin-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstmpegts-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstphotography-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgstplayer-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "libgsturidownloader-1.0.so.0": { + "package": "gst-plugins-bad", + "version": "0.1203.0" + }, + "librsync.so.2": { + "package": "librsync", + "version": "2.0.0" + }, + "libxfce4kbd-private-2.so.0": { + "package": "libxfce4ui", + "version": "0.0.0" + }, + "libxfce4ui-1.so.0": { + "package": "libxfce4ui", + "version": "0.0.0" + }, + "libcommon.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libmc.so": { + "package": "xrdp", + "version": "0" + }, + "libpainter.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "librfxencode.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libscp.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libvnc.so": { + "package": "xrdp", + "version": "0" + }, + "libxrdp.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libxrdpapi.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libxup.so": { + "package": "xrdp", + "version": "0" + }, + "libdb-5.3.so": { + "package": "db", + "version": "0" + }, + "libMagickCore-7.Q16HDRI.so.4": { + "package": "imagemagick-libs", + "version": "4.0.0" + }, + "libMagickWand-7.Q16HDRI.so.4": { + "package": "imagemagick-libs", + "version": "4.0.0" + }, + "libtumbler-1.so.0": { + "package": "tumbler", + "version": "0.0.0" + }, + "libgdkmm-2.4.so.1": { + "package": "gtkmm", + "version": "1.1.0" + }, + "libgtkmm-2.4.so.1": { + "package": "gtkmm", + "version": "1.1.0" + }, + "cpufreqd_acpi.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_apm.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_cpu.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_exec.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_governor_parameters.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_nforce2.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_pmu.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_programs.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_tau.so": { + "package": "cpufreqd", + "version": "0" + }, + "libunwind-coredump.so.0": { + "package": "libunwind", + "version": "0.0.0" + }, + "libunwind-ptrace.so.0": { + "package": "libunwind", + "version": "0.0.0" + }, + "libunwind-setjmp.so.0": { + "package": "libunwind", + "version": "0.0.0" + }, + "libunwind-x86_64.so.8": { + "package": "libunwind", + "version": "8.0.1" + }, + "libunwind.so.8": { + "package": "libunwind", + "version": "8.0.1" + }, + "libxmlrpc_abyss.so.3": { + "package": "xmlrpc-c-abyss", + "version": "3.39" + }, + "libxmlrpc_server_abyss.so.3": { + "package": "xmlrpc-c-abyss", + "version": "3.39" + }, + "libzmq.so.5": { + "package": "libzmq", + "version": "5.1.5" + }, + "libcap-ng.so.0": { + "package": "libcap-ng", + "version": "0.0.0" + }, + "libgraphite2.so.3": { + "package": "graphite2", + "version": "3.0.1" + }, + "libhttp_parser.so.2.7.1": { + "package": "http-parser", + "version": "2.7.1" + }, + "libvpx.so.4": { + "package": "libvpx", + "version": "4.1.0" + }, + "libpri.so.1.4": { + "package": "libpri", + "version": "1.4" + }, + "libfarstream-0.2.so.5": { + "package": "farstream", + "version": "5.1.0" + }, + "libonig.so.4": { + "package": "oniguruma", + "version": "4.0.0" + }, + "libXaw.so.7": { + "package": "libxaw", + "version": "7.0.0" + }, + "libpolkit-agent-1.so.0": { + "package": "polkit", + "version": "0.0.0" + }, + "libpolkit-backend-1.so.0": { + "package": "polkit", + "version": "0.0.0" + }, + "libpolkit-gobject-1.so.0": { + "package": "polkit", + "version": "0.0.0" + }, + "libcpufreq.so.0": { + "package": "cpufrequtils", + "version": "0.0.0" + }, + "libgnutls.so.30": { + "package": "gnutls", + "version": "30.20.1" + }, + "libcairo-gobject.so.2": { + "package": "cairo-gobject", + "version": "2.11400.10" + }, + "libsqlite3.so.0": { + "package": "sqlite-libs", + "version": "0.8.6" + }, + "libmtdev.so.1": { + "package": "mtdev", + "version": "1.0.0" + }, + "libgsasl.so.7": { + "package": "libgsasl", + "version": "7.9.6" + }, + "rlm_krb5.so": { + "package": "freeradius-krb5", + "version": "0" + }, + "libusbredirhost.so.1": { + "package": "usbredir", + "version": "1.0.0" + }, + "libusbredirparser.so.1": { + "package": "usbredir", + "version": "1.0.0" + }, + "liblxc.so.1": { + "package": "lxc-libs", + "version": "1.3.0" + }, + "libunistring.so.2": { + "package": "libunistring", + "version": "2.0.0" + }, + "liblldp_clif.so.1": { + "package": "open-lldp", + "version": "1.0.0" + }, + "libtirpc.so.3": { + "package": "libtirpc", + "version": "3.0.0" + }, + "libmenu-cache.so.3": { + "package": "menu-cache", + "version": "3.0.1" + }, + "libfftw3l.so.3": { + "package": "fftw-long-double-libs", + "version": "3.5.6" + }, + "libfftw3l_threads.so.3": { + "package": "fftw-long-double-libs", + "version": "3.5.6" + }, + "libgstbase-1.0.so.0": { + "package": "gstreamer", + "version": "0.1203.0" + }, + "libgstcheck-1.0.so.0": { + "package": "gstreamer", + "version": "0.1203.0" + }, + "libgstcontroller-1.0.so.0": { + "package": "gstreamer", + "version": "0.1203.0" + }, + "libgstnet-1.0.so.0": { + "package": "gstreamer", + "version": "0.1203.0" + }, + "libgstreamer-1.0.so.0": { + "package": "gstreamer", + "version": "0.1203.0" + }, + "libconfig.so.9": { + "package": "libconfig", + "version": "9.2.0" + }, + "libreadline.so.7": { + "package": "readline", + "version": "7.0" + }, + "libquadmath.so.0": { + "package": "libquadmath", + "version": "0.0.0" + }, + "libpoppler-cpp.so.0": { + "package": "poppler", + "version": "0.3.0" + }, + "libpoppler.so.67": { + "package": "poppler", + "version": "67.0.0" + }, + "libexpat.so.1": { + "package": "expat", + "version": "1.6.10" + }, + "libcrypto.so.42": { + "package": "libressl2.6-libcrypto", + "version": "42.0.0" + }, + "libLLVM-5.0.so": { + "package": "llvm5-libs", + "version": "0" + }, + "libspeexdsp.so.1": { + "package": "speexdsp", + "version": "1.5.0" + }, + "libfribidi.so.0": { + "package": "fribidi", + "version": "0.3.6" + }, + "libxatracker.so.2": { + "package": "mesa-xatracker", + "version": "2.3.0" + }, + "libcrypto.so.1.0.0": { + "package": "libcrypto1.0", + "version": "1.0.0" + }, + "libdevmapper-event-lvm2.so.2.02": { + "package": "lvm2-libs", + "version": "2.02" + }, + "liblvm2app.so.2.2": { + "package": "lvm2-libs", + "version": "2.2" + }, + "liblvm2cmd.so.2.02": { + "package": "lvm2-libs", + "version": "2.02" + }, + "libxattr-tdb-samba4.so": { + "package": "samba-server", + "version": "0" + }, + "libpcre.so.1": { + "package": "pcre", + "version": "1.2.9" + }, + "libpcreposix.so.0": { + "package": "pcre", + "version": "0.0.5" + }, + "libboost_program_options-mt.so.1.62.0": { + "package": "boost-program_options", + "version": "1.62.0" + }, + "libboost_program_options.so.1.62.0": { + "package": "boost-program_options", + "version": "1.62.0" + }, + "libdconf.so.1": { + "package": "dconf", + "version": "1.0.0" + }, + "libestr.so.0": { + "package": "libestr", + "version": "0.0.0" + }, + "libsvn_client-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_delta-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_diff-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs_base-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs_fs-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs_util-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs_x-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_ra-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_ra_local-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_ra_serf-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_ra_svn-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_repos-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_subr-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_wc-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libev.so.4": { + "package": "libev", + "version": "4.0.0" + }, + "autoaccept.so": { + "package": "libpurple", + "version": "0" + }, + "buddynote.so": { + "package": "libpurple", + "version": "0" + }, + "idle.so": { + "package": "libpurple", + "version": "0" + }, + "joinpart.so": { + "package": "libpurple", + "version": "0" + }, + "libgg.so": { + "package": "libpurple", + "version": "0" + }, + "libirc.so": { + "package": "libpurple", + "version": "0" + }, + "libnovell.so": { + "package": "libpurple", + "version": "0" + }, + "libpurple.so.0": { + "package": "libpurple", + "version": "0.12.0" + }, + "libsimple.so": { + "package": "libpurple", + "version": "0" + }, + "libzephyr.so": { + "package": "libpurple", + "version": "0" + }, + "log_reader.so": { + "package": "libpurple", + "version": "0" + }, + "newline.so": { + "package": "libpurple", + "version": "0" + }, + "nss-prefs.so": { + "package": "libpurple", + "version": "0" + }, + "offlinemsg.so": { + "package": "libpurple", + "version": "0" + }, + "psychic.so": { + "package": "libpurple", + "version": "0" + }, + "ssl-nss.so": { + "package": "libpurple", + "version": "0" + }, + "ssl.so": { + "package": "libpurple", + "version": "0" + }, + "statenotify.so": { + "package": "libpurple", + "version": "0" + }, + "libatkmm-1.6.so.1": { + "package": "atkmm", + "version": "1.1.0" + }, + "libdbus-glib-1.so.2": { + "package": "dbus-glib", + "version": "2.3.3" + }, + "libatk-1.0.so.0": { + "package": "atk", + "version": "0.22610.1" + }, + "lib++dfb-1.7.so.7": { + "package": "directfb", + "version": "7.0.0" + }, + "libdirect-1.7.so.7": { + "package": "directfb", + "version": "7.0.0" + }, + "libdirectfb-1.7.so.7": { + "package": "directfb", + "version": "7.0.0" + }, + "libfusion-1.7.so.7": { + "package": "directfb", + "version": "7.0.0" + }, + "libechonest.so.2.3": { + "package": "libechonest", + "version": "2.3.1" + }, + "libpoppler-qt4.so.4": { + "package": "poppler-qt4", + "version": "4.11.0" + }, + "libboost_regex-mt.so.1.62.0": { + "package": "boost-regex", + "version": "1.62.0" + }, + "libboost_regex.so.1.62.0": { + "package": "boost-regex", + "version": "1.62.0" + }, + "libsamba-net-samba4.so": { + "package": "samba-libs-py", + "version": "0" + }, + "libsamba-python-samba4.so": { + "package": "samba-libs-py", + "version": "0" + }, + "libsoxr-lsr.so.0": { + "package": "soxr", + "version": "0.1.9" + }, + "libsoxr.so.0": { + "package": "soxr", + "version": "0.1.1" + }, + "libgvfscommon.so": { + "package": "gvfs", + "version": "0" + }, + "libgvfsdaemon.so": { + "package": "gvfs", + "version": "0" + }, + "libgtksourceview-2.0.so.0": { + "package": "gtksourceview2", + "version": "0.0.0" + }, + "libpixman-1.so.0": { + "package": "pixman", + "version": "0.34.0" + }, + "libgdbm.so.4": { + "package": "gdbm", + "version": "4.0.0" + }, + "libgdbm_compat.so.4": { + "package": "gdbm", + "version": "4.0.0" + }, + "libgpgme.so.11": { + "package": "gpgme", + "version": "11.18.0" + }, + "libevtlog.so.0": { + "package": "eventlog", + "version": "0.0.0" + }, + "libhiredis.so.0.13": { + "package": "hiredis", + "version": "0.13" + }, + "libjq.so.1": { + "package": "jq", + "version": "1.0.4" + }, + "libparted-fs-resize.so.0": { + "package": "parted", + "version": "0.0.1" + }, + "libparted.so.2": { + "package": "parted", + "version": "2.0.1" + }, + "libinotifytools.so.0": { + "package": "inotify-tools", + "version": "0.4.1" + }, + "libjbig2dec.so.0": { + "package": "jbig2dec", + "version": "0.0.0" + }, + "libtonezone.so.2": { + "package": "dahdi-tools", + "version": "2.0.0" + }, + "libmspack.so.0": { + "package": "libmspack", + "version": "0.1.0" + }, + "libmpc.so.3": { + "package": "mpc1", + "version": "3.0.0" + }, + "libfaad.so.2": { + "package": "faad2", + "version": "2.0.0" + }, + "librarian.so.0": { + "package": "rarian", + "version": "0.0.0" + }, + "libavfs.so.0": { + "package": "avfs", + "version": "0.0.2" + }, + "libsasl2.so.3": { + "package": "libsasl", + "version": "3.0.0" + }, + "libintl.so.8": { + "package": "libintl", + "version": "8.1.5" + }, + "lib01_acl_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib02_imap_acl_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib02_lazy_expunge_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib05_mail_crypt_acl_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib05_pop3_migration_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib05_snarf_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib10_last_login_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib10_mail_crypt_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib10_mail_filter_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib10_quota_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib11_imap_quota_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib11_trash_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib15_notify_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_autocreate_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_charset_alias_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_expire_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_fts_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_listescape_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_mail_log_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_mailbox_alias_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_notify_status_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_push_notification_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_quota_clone_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_replication_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_var_expand_crypt.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_virtual_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_zlib_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib21_fts_squat_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib30_imap_zlib_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib90_stats_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib95_imap_stats_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib99_welcome_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "libdcrypt_openssl.so": { + "package": "dovecot", + "version": "0" + }, + "libdovecot-compression.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-dsync.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-fts.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-lda.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-login.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-storage.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libfs_compress.so": { + "package": "dovecot", + "version": "0" + }, + "libfs_crypt.so": { + "package": "dovecot", + "version": "0" + }, + "libfs_mail_crypt.so": { + "package": "dovecot", + "version": "0" + }, + "libssl_iostream_openssl.so": { + "package": "dovecot", + "version": "0" + }, + "libstats_auth.so": { + "package": "dovecot", + "version": "0" + }, + "libstats_mail.so": { + "package": "dovecot", + "version": "0" + }, + "cat.c32": { + "package": "syslinux", + "version": "0" + }, + "chain.c32": { + "package": "syslinux", + "version": "0" + }, + "cmd.c32": { + "package": "syslinux", + "version": "0" + }, + "cmenu.c32": { + "package": "syslinux", + "version": "0" + }, + "config.c32": { + "package": "syslinux", + "version": "0" + }, + "cptime.c32": { + "package": "syslinux", + "version": "0" + }, + "cpu.c32": { + "package": "syslinux", + "version": "0" + }, + "cpuid.c32": { + "package": "syslinux", + "version": "0" + }, + "cpuidtest.c32": { + "package": "syslinux", + "version": "0" + }, + "debug.c32": { + "package": "syslinux", + "version": "0" + }, + "dhcp.c32": { + "package": "syslinux", + "version": "0" + }, + "dir.c32": { + "package": "syslinux", + "version": "0" + }, + "disk.c32": { + "package": "syslinux", + "version": "0" + }, + "dmi.c32": { + "package": "syslinux", + "version": "0" + }, + "dmitest.c32": { + "package": "syslinux", + "version": "0" + }, + "elf.c32": { + "package": "syslinux", + "version": "0" + }, + "ethersel.c32": { + "package": "syslinux", + "version": "0" + }, + "gfxboot.c32": { + "package": "syslinux", + "version": "0" + }, + "gpxecmd.c32": { + "package": "syslinux", + "version": "0" + }, + "hdt.c32": { + "package": "syslinux", + "version": "0" + }, + "hexdump.c32": { + "package": "syslinux", + "version": "0" + }, + "host.c32": { + "package": "syslinux", + "version": "0" + }, + "ifcpu.c32": { + "package": "syslinux", + "version": "0" + }, + "ifcpu64.c32": { + "package": "syslinux", + "version": "0" + }, + "ifmemdsk.c32": { + "package": "syslinux", + "version": "0" + }, + "ifplop.c32": { + "package": "syslinux", + "version": "0" + }, + "kbdmap.c32": { + "package": "syslinux", + "version": "0" + }, + "kontron_wdt.c32": { + "package": "syslinux", + "version": "0" + }, + "ldlinux.c32": { + "package": "syslinux", + "version": "0" + }, + "lfs.c32": { + "package": "syslinux", + "version": "0" + }, + "libcom32.c32": { + "package": "syslinux", + "version": "0" + }, + "libgpl.c32": { + "package": "syslinux", + "version": "0" + }, + "liblua.c32": { + "package": "syslinux", + "version": "0" + }, + "libmenu.c32": { + "package": "syslinux", + "version": "0" + }, + "libutil.c32": { + "package": "syslinux", + "version": "0" + }, + "linux.c32": { + "package": "syslinux", + "version": "0" + }, + "ls.c32": { + "package": "syslinux", + "version": "0" + }, + "lua.c32": { + "package": "syslinux", + "version": "0" + }, + "mboot.c32": { + "package": "syslinux", + "version": "0" + }, + "meminfo.c32": { + "package": "syslinux", + "version": "0" + }, + "menu.c32": { + "package": "syslinux", + "version": "0" + }, + "pci.c32": { + "package": "syslinux", + "version": "0" + }, + "pcitest.c32": { + "package": "syslinux", + "version": "0" + }, + "pmload.c32": { + "package": "syslinux", + "version": "0" + }, + "poweroff.c32": { + "package": "syslinux", + "version": "0" + }, + "prdhcp.c32": { + "package": "syslinux", + "version": "0" + }, + "pwd.c32": { + "package": "syslinux", + "version": "0" + }, + "pxechn.c32": { + "package": "syslinux", + "version": "0" + }, + "reboot.c32": { + "package": "syslinux", + "version": "0" + }, + "rosh.c32": { + "package": "syslinux", + "version": "0" + }, + "sanboot.c32": { + "package": "syslinux", + "version": "0" + }, + "sdi.c32": { + "package": "syslinux", + "version": "0" + }, + "sysdump.c32": { + "package": "syslinux", + "version": "0" + }, + "syslinux.c32": { + "package": "syslinux", + "version": "0" + }, + "vesa.c32": { + "package": "syslinux", + "version": "0" + }, + "vesainfo.c32": { + "package": "syslinux", + "version": "0" + }, + "vesamenu.c32": { + "package": "syslinux", + "version": "0" + }, + "vpdtest.c32": { + "package": "syslinux", + "version": "0" + }, + "whichsys.c32": { + "package": "syslinux", + "version": "0" + }, + "zzjson.c32": { + "package": "syslinux", + "version": "0" + }, + "libaspell.so.15": { + "package": "aspell-libs", + "version": "15.1.5" + }, + "libpspell.so.15": { + "package": "aspell-libs", + "version": "15.1.5" + }, + "libid3tag.so.0": { + "package": "libid3tag", + "version": "0.3.0" + }, + "libdns.so.1106": { + "package": "bind-libs", + "version": "1106.0.1" + }, + "libfaad_drm.so.2": { + "package": "faad2", + "version": "2.0.0" + } + }, + "package": { + "python-tests": { + "package": "python2-tests", + "version": "2.7.15-r2" + }, + "python-dev": { + "package": "python2-dev", + "version": "2.7.15-r2" + }, + "mysql-dev": { + "package": "mariadb-dev", + "version": "10.1.41-r0" + }, + "py-singledispatch": { + "package": "py2-singledispatch", + "version": 0 + }, + "python-gdbm": { + "package": "py-gdbm", + "version": "2.7.15-r2" + }, + "freeradius3-unixodbc": { + "package": "freeradius-unixodbc", + "version": "3.0.15-r5" + }, + "quagga-nhrp": { + "package": "quagga", + "version": "1.2.4" + }, + "freeradius3-eap": { + "package": "freeradius-eap", + "version": "3.0.15-r5" + }, + "ncurses-widec-libs": { + "package": "ncurses-libs", + "version": "6.0_p20171125-r1" + }, + "perl-yaml-xs": { + "package": "perl-yaml-libyaml", + "version": "0.65-r2" + }, + "freeradius3-mysql": { + "package": "freeradius-mysql", + "version": "3.0.15-r5" + }, + "nodejs-lts": { + "package": "nodejs", + "version": "8.9.3" + }, + "nodejs-current-npm": { + "package": "nodejs-npm", + "version": 0 + }, + "freeradius3-radclient": { + "package": "freeradius-radclient", + "version": "3.0.15-r5" + }, + "postgresql": { + "package": "postgresql-bdr", + "version": 0 + }, + "python": { + "package": "python2", + "version": "2.7.15-r2" + }, + "udev": { + "package": "eudev", + "version": "176" + }, + "perl-encode-piconv": { + "package": "perl-encode-utils", + "version": 0 + }, + "perl-test-tester": { + "package": "perl-test-simple", + "version": 0 + }, + "perl-net-ldap": { + "package": "perl-ldap", + "version": 0 + }, + "collectd-libvirt": { + "package": "collectd-virt", + "version": 0 + }, + "freeradius3-perl": { + "package": "freeradius-perl", + "version": "3.0.15-r5" + }, + "linux-virtgrsec": { + "package": "linux-virthardened", + "version": "4.9.65-r1" + }, + "py-backports.ssl_match_hostname": { + "package": "py2-backports.ssl_match_hostname", + "version": 0 + }, + "perl-mail-tools": { + "package": "perl-mailtools", + "version": "2.19" + }, + "/bin/sh": { + "package": "busybox", + "version": 0 + }, + "llvm-static": { + "package": "llvm5-static", + "version": "5.0.0-r0" + }, + "freeradius3": { + "package": "freeradius", + "version": "3.0.15-r5" + }, + "freeradius3-postgresql": { + "package": "freeradius-postgresql", + "version": "3.0.15-r5" + }, + "llvm-test-utils": { + "package": "llvm5-test-utils", + "version": "5.0.0-r0" + }, + "lit": { + "package": "llvm5-test-utils", + "version": "0.6.0-r0" + }, + "freeradius3-mssql": { + "package": "freeradius-mssql", + "version": "3.0.15-r5" + }, + "freeradius3-sqlite": { + "package": "freeradius-sqlite", + "version": "3.0.15-r5" + }, + "freeradius3-ldap": { + "package": "freeradius-ldap", + "version": "3.0.15-r5" + }, + "freeradius3-python": { + "package": "freeradius-python", + "version": "3.0.15-r5" + }, + "freeradius3-sql": { + "package": "freeradius-sql", + "version": "3.0.15-r5" + }, + "py3-pip": { + "package": "python3", + "version": 0 + }, + "llvm": { + "package": "llvm5", + "version": "5.0.0-r0" + }, + "llvm-dev": { + "package": "llvm5-dev", + "version": "5.0.0-r0" + }, + "consolekit": { + "package": "consolekit2", + "version": "1.2.0" + }, + "gnupg-doc": { + "package": "gnupg1-doc", + "version": "1.4.23-r0" + }, + "pkgconfig": { + "package": "pkgconf", + "version": "1" + }, + "linux-grsec": { + "package": "linux-hardened", + "version": "4.9.65-r1" + }, + "xtables-addons-grsec": { + "package": "xtables-addons-hardened", + "version": "4.9.65-r1" + }, + "freeradius3-pam": { + "package": "freeradius-pam", + "version": "3.0.15-r5" + }, + "zfs-grsec": { + "package": "zfs-hardened", + "version": "4.9.65-r1" + }, + "dahdi-linux-grsec": { + "package": "dahdi-linux-hardened", + "version": "4.9.65-r1" + }, + "devicemaster-linux-grsec": { + "package": "devicemaster-linux-hardened", + "version": "4.9.65-r1" + }, + "nginx-lua": { + "package": "nginx-mod-http-lua", + "version": 0 + }, + "gnupg": { + "package": "gnupg1", + "version": "1.4.23-r0" + }, + "drbd": { + "package": "drbd-utils", + "version": 0 + }, + "nginx-rtmp": { + "package": "nginx-mod-rtmp", + "version": 0 + }, + "py-pip": { + "package": "py2-pip", + "version": "9.0.1-r1" + }, + "libgit2-libs": { + "package": "libgit2", + "version": 0 + }, + "wayland-protocols-dev": { + "package": "wayland-protocols", + "version": 0 + }, + "py2-setuptools": { + "package": "py-setuptools", + "version": "33.1.1-r1" + }, + "nodejs-lts-dev": { + "package": "nodejs-dev", + "version": "8.9.3" + }, + "nagios-plugins-clamdnagios-plugins-ftpnagios-plugins-imapnagios-plugins-jabbernagios-plugins-nntpnagios-plugins-nntpsnagios-plugins-popnagios-plugins-simapnagios-plugins-spopnagios-plugins-ssmtpnagios-plugins-udp": { + "package": "nagios-plugins-tcp", + "version": 0 + }, + "freeradius3-krb5": { + "package": "freeradius-krb5", + "version": "3.0.15-r5" + }, + "spl-grsec": { + "package": "spl-hardened", + "version": "4.9.65-r1" + }, + "llvm-libs": { + "package": "llvm5-libs", + "version": "5.0.0-r0" + }, + "drbd9-grsec": { + "package": "drbd9-hardened", + "version": "4.9.65-r1" + }, + "perl-time-date": { + "package": "perl-timedate", + "version": "2.30" + }, + "bkeymaps": { + "package": "kbd-bkeymaps", + "version": 0 + } + } + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/imgconf/apk/testdata/history_v3.9.json b/pkg/fanal/analyzer/imgconf/apk/testdata/history_v3.9.json new file mode 100644 index 000000000000..39d9ec0b363f --- /dev/null +++ b/pkg/fanal/analyzer/imgconf/apk/testdata/history_v3.9.json @@ -0,0 +1,75866 @@ +{ + "package": { + "perl-cgi-emulate-psgi": { + "versions": { + "0.23-r0": 1545229757 + }, + "origin": "perl-cgi-emulate-psgi", + "dependencies": [ + "perl-http-message", + "perl-cgi" + ] + }, + "samba-common": { + "versions": { + "4.8.11-r1": 1555334972 + }, + "origin": "samba" + }, + "shared-mime-info": { + "versions": { + "1.10-r0": 1542821941 + }, + "origin": "shared-mime-info", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:update-mime-database" + ] + }, + "iputils": { + "versions": { + "20180629-r1": 1545209661 + }, + "origin": "iputils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.1.1" + ], + "provides": [ + "cmd:arping", + "cmd:clockdiff", + "cmd:ipg", + "cmd:ninfod", + "cmd:ping", + "cmd:ping6", + "cmd:rarpd", + "cmd:rdisc", + "cmd:tftpd", + "cmd:tracepath", + "cmd:tracepath6", + "cmd:traceroute6" + ] + }, + "py3-backports_abc": { + "versions": { + "0.5-r0": 1545293194 + }, + "origin": "py-backports_abc", + "dependencies": [ + "python3" + ] + }, + "eggdbus-dev": { + "versions": { + "0.6-r5": 1543935571 + }, + "origin": "eggdbus", + "dependencies": [ + "eggdbus=0.6-r5", + "pc:gio-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:eggdbus-1=0.6" + ] + }, + "qemu-audio-sdl": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libc.musl-x86_64.so.1" + ] + }, + "freeglut": { + "versions": { + "3.0.0-r0": 1545291114 + }, + "origin": "freeglut", + "dependencies": [ + "so:libGL.so.1", + "so:libX11.so.6", + "so:libXi.so.6", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libglut.so.3=3.10.0" + ] + }, + "rabbitmq-c": { + "versions": { + "0.8.0-r5": 1543923901 + }, + "origin": "rabbitmq-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "so:librabbitmq.so.4=4.2.0" + ] + }, + "font-bh-100dpi": { + "versions": { + "1.0.3-r0": 1545229751 + }, + "origin": "font-bh-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "udisks": { + "versions": { + "1.0.5-r3": 1545069135 + }, + "origin": "udisks", + "dependencies": [ + "so:libatasmart.so.4", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libdevmapper.so.1.02", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libparted.so.2", + "so:libpolkit-gobject-1.so.0", + "so:libsgutils2.so.2", + "so:libudev.so.1" + ], + "provides": [ + "cmd:udisks", + "cmd:udisks-tcp-bridge", + "cmd:umount.udisks" + ] + }, + "git-fast-import": { + "versions": { + "2.20.1-r0": 1545214194 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0", + "so:libc.musl-x86_64.so.1", + "so:libpcre2-8.so.0", + "so:libz.so.1" + ] + }, + "perl-parse-recdescent-doc": { + "versions": { + "1.967015-r1": 1545067081 + }, + "origin": "perl-parse-recdescent" + }, + "py-cliapp": { + "versions": { + "1.20170823-r0": 1544798597 + }, + "origin": "py-cliapp", + "dependencies": [ + "python2", + "python3" + ] + }, + "perl-encode": { + "versions": { + "2.98-r0": 1543935165 + }, + "origin": "perl-encode", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "cyrus-sasl-gs2": { + "versions": { + "2.1.27-r1": 1550353527 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgssapi_krb5.so.2" + ] + }, + "lua5.2-posix": { + "versions": { + "33.4.0-r1": 1546010825 + }, + "origin": "lua-posix", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "dconf-dev": { + "versions": { + "0.26.0-r1": 1545302104 + }, + "origin": "dconf", + "dependencies": [ + "dconf=0.26.0-r1", + "pc:gio-2.0>=2.25.7", + "pkgconfig" + ], + "provides": [ + "pc:dconf=0.26.0" + ] + }, + "gtksourceview2": { + "versions": { + "2.10.5-r0": 1545292667 + }, + "origin": "gtksourceview2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgtksourceview-2.0.so.0=0.0.0" + ] + }, + "a52dec": { + "versions": { + "0.7.4-r7": 1545214243 + }, + "origin": "a52dec", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liba52.so.0=0.0.0", + "cmd:a52dec", + "cmd:extract_a52" + ] + }, + "xen-dev": { + "versions": { + "4.11.1-r1": 1545075896 + }, + "origin": "xen", + "dependencies": [ + "openssl-dev", + "python2-dev", + "e2fsprogs-dev", + "gettext", + "zlib-dev", + "ncurses-dev", + "dev86", + "texinfo", + "perl", + "pciutils-dev", + "glib-dev", + "yajl-dev", + "libnl3-dev", + "spice-dev", + "gnutls-dev", + "curl-dev", + "libaio-dev", + "lzo-dev", + "xz-dev", + "util-linux-dev", + "e2fsprogs-dev", + "linux-headers", + "argp-standalone", + "perl-dev", + "flex", + "bison", + "xen-libs=4.11.1-r1" + ] + }, + "linux-firmware-korg": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "rrdtool": { + "versions": { + "1.7.0-r0": 1542924810 + }, + "origin": "rrdtool", + "dependencies": [ + "font-sony-misc", + "so:libc.musl-x86_64.so.1", + "so:librrd.so.8" + ], + "provides": [ + "cmd:rrdtool" + ] + }, + "gnats": { + "versions": { + "4.2.0-r5": 1545209105 + }, + "origin": "gnats", + "dependencies": [ + "postfix", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:edit-pr", + "cmd:getclose", + "cmd:install-sid", + "cmd:query-pr", + "cmd:send-pr" + ] + }, + "qemu-tilegx": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-tilegx" + ] + }, + "syslinux": { + "versions": { + "6.04_pre1-r2": 1543935716 + }, + "origin": "syslinux", + "dependencies": [ + "mtools", + "blkid", + "mkinitfs", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:cat.c32=0", + "so:chain.c32=0", + "so:cmd.c32=0", + "so:cmenu.c32=0", + "so:config.c32=0", + "so:cptime.c32=0", + "so:cpu.c32=0", + "so:cpuid.c32=0", + "so:cpuidtest.c32=0", + "so:debug.c32=0", + "so:dhcp.c32=0", + "so:dir.c32=0", + "so:disk.c32=0", + "so:dmi.c32=0", + "so:dmitest.c32=0", + "so:elf.c32=0", + "so:ethersel.c32=0", + "so:gfxboot.c32=0", + "so:gpxecmd.c32=0", + "so:hdt.c32=0", + "so:hexdump.c32=0", + "so:host.c32=0", + "so:ifcpu.c32=0", + "so:ifcpu64.c32=0", + "so:ifmemdsk.c32=0", + "so:ifplop.c32=0", + "so:kbdmap.c32=0", + "so:kontron_wdt.c32=0", + "so:ldlinux.c32=0", + "so:lfs.c32=0", + "so:libcom32.c32=0", + "so:libgpl.c32=0", + "so:liblua.c32=0", + "so:libmenu.c32=0", + "so:libutil.c32=0", + "so:linux.c32=0", + "so:ls.c32=0", + "so:lua.c32=0", + "so:mboot.c32=0", + "so:meminfo.c32=0", + "so:menu.c32=0", + "so:pci.c32=0", + "so:pcitest.c32=0", + "so:pmload.c32=0", + "so:poweroff.c32=0", + "so:prdhcp.c32=0", + "so:pwd.c32=0", + "so:pxechn.c32=0", + "so:reboot.c32=0", + "so:rosh.c32=0", + "so:sanboot.c32=0", + "so:sdi.c32=0", + "so:sysdump.c32=0", + "so:syslinux.c32=0", + "so:vesa.c32=0", + "so:vesainfo.c32=0", + "so:vesamenu.c32=0", + "so:vpdtest.c32=0", + "so:whichsys.c32=0", + "so:zzjson.c32=0", + "cmd:extlinux", + "cmd:gethostip", + "cmd:isohybrid", + "cmd:isohybrid.pl", + "cmd:keytab-lilo", + "cmd:lss16toppm", + "cmd:md5pass", + "cmd:memdiskfind", + "cmd:mkdiskimage", + "cmd:ppmtolss16", + "cmd:pxelinux-options", + "cmd:sha1pass", + "cmd:syslinux", + "cmd:syslinux2ansi", + "cmd:update-extlinux" + ] + }, + "parallel": { + "versions": { + "20180622-r0": 1545154588 + }, + "origin": "parallel", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:env_parallel", + "cmd:env_parallel.ash", + "cmd:env_parallel.bash", + "cmd:env_parallel.csh", + "cmd:env_parallel.dash", + "cmd:env_parallel.fish", + "cmd:env_parallel.ksh", + "cmd:env_parallel.pdksh", + "cmd:env_parallel.sh", + "cmd:env_parallel.tcsh", + "cmd:env_parallel.zsh", + "cmd:niceload", + "cmd:parallel", + "cmd:parcat", + "cmd:parset", + "cmd:sem", + "cmd:sql" + ] + }, + "keybinder-doc": { + "versions": { + "0.3.0-r1": 1545076730 + }, + "origin": "keybinder" + }, + "syslog-ng-examples": { + "versions": { + "3.19.1-r0": 1548543150 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libevtlog-3.19.so.0", + "so:libglib-2.0.so.0", + "so:libivykis.so.0", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "py-more-itertools": { + "versions": { + "4.1.0-r0": 1542824887 + }, + "origin": "py-more-itertools" + }, + "libidn-dev": { + "versions": { + "1.35-r0": 1542924920 + }, + "origin": "libidn", + "dependencies": [ + "libidn=1.35-r0", + "pkgconfig" + ], + "provides": [ + "pc:libidn=1.35" + ] + }, + "glade3-dev": { + "versions": { + "3.8.5-r5": 1543998724 + }, + "origin": "glade3", + "dependencies": [ + "glade3=3.8.5-r5", + "pc:gtk+-2.0>=2.14.0", + "pc:libxml-2.0>=2.4.0", + "pkgconfig" + ], + "provides": [ + "pc:gladeui-1.0=3.8.5" + ] + }, + "haveged-doc": { + "versions": { + "1.9.4-r4": 1557129010 + }, + "origin": "haveged" + }, + "atk-dev": { + "versions": { + "2.30.0-r0": 1542823743 + }, + "origin": "atk", + "dependencies": [ + "atk=2.30.0-r0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:atk=2.30.0" + ] + }, + "perl-test-fatal": { + "versions": { + "0.014-r1": 1542821869 + }, + "origin": "perl-test-fatal", + "dependencies": [ + "perl-try-tiny" + ] + }, + "lua5.1-curl": { + "versions": { + "0.3.8-r1": 1545224117 + }, + "origin": "lua-curl", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "perl-class-container-doc": { + "versions": { + "0.13-r0": 1543254089 + }, + "origin": "perl-class-container" + }, + "fribidi": { + "versions": { + "1.0.5-r0": 1542824048 + }, + "origin": "fribidi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfribidi.so.0=0.4.0", + "cmd:fribidi" + ] + }, + "paxctl-doc": { + "versions": { + "0.9-r0": 1545223087 + }, + "origin": "paxctl" + }, + "nfdump-doc": { + "versions": { + "1.6.17-r0": 1545223022 + }, + "origin": "nfdump" + }, + "py3-packaging": { + "versions": { + "17.1-r0": 1542825057 + }, + "origin": "py-packaging", + "dependencies": [ + "py3-parsing", + "py3-six", + "python3" + ] + }, + "libxt-dev": { + "versions": { + "1.1.5-r2": 1542883332 + }, + "origin": "libxt", + "dependencies": [ + "libsm-dev", + "libxt=1.1.5-r2", + "pc:ice", + "pc:sm", + "pc:x11", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xt=1.1.5" + ] + }, + "syslog-ng-graphite": { + "versions": { + "3.19.1-r0": 1548543150 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "py3-cryptography": { + "versions": { + "2.4.2-r2": 1545067164 + }, + "origin": "py-cryptography", + "dependencies": [ + "py3-cffi", + "py3-idna", + "py3-asn1crypto", + "py3-six", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpython3.6m.so.1.0", + "so:libssl.so.1.1" + ] + }, + "py-sphinx": { + "versions": { + "1.8.3-r0": 1548111932 + }, + "origin": "py-sphinx", + "dependencies": [ + "py3-sphinx", + "py3-sphinx=1.8.3-r0" + ] + }, + "strace-doc": { + "versions": { + "4.24-r0": 1543928279 + }, + "origin": "strace" + }, + "qemu-system-riscv32": { + "versions": { + "3.1.0-r3": 1551107305 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-riscv32" + ] + }, + "bluez-libs": { + "versions": { + "5.50-r0": 1545822205 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libbluetooth.so.3=3.18.16" + ] + }, + "subversion-doc": { + "versions": { + "1.11.1-r0": 1548692375 + }, + "origin": "subversion" + }, + "libxdamage": { + "versions": { + "1.1.4-r2": 1542823760 + }, + "origin": "libxdamage", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXdamage.so.1=1.1.0" + ] + }, + "xf86-video-vmware-doc": { + "versions": { + "13.3.0-r0": 1545076716 + }, + "origin": "xf86-video-vmware" + }, + "xf86-video-glint-doc": { + "versions": { + "1.2.9-r3": 1545300613 + }, + "origin": "xf86-video-glint" + }, + "ipvsadm-doc": { + "versions": { + "1.29-r0": 1545118132 + }, + "origin": "ipvsadm" + }, + "jansson-dev": { + "versions": { + "2.11-r0": 1542893194 + }, + "origin": "jansson", + "dependencies": [ + "jansson=2.11-r0", + "pkgconfig" + ], + "provides": [ + "pc:jansson=2.11" + ] + }, + "obex-data-server-doc": { + "versions": { + "0.4.6-r4": 1545069103 + }, + "origin": "obex-data-server" + }, + "libgmpxx": { + "versions": { + "6.1.2-r1": 1542301441 + }, + "origin": "gmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgmp.so.10", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgmpxx.so.4=4.5.2" + ] + }, + "util-linux": { + "versions": { + "2.33-r0": 1545307438 + }, + "origin": "util-linux", + "dependencies": [ + "findmnt", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfdisk.so.1", + "so:libmount.so.1", + "so:libncursesw.so.6", + "so:libsmartcols.so.1", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:addpart", + "cmd:agetty", + "cmd:blkdiscard", + "cmd:blkzone", + "cmd:blockdev", + "cmd:cal", + "cmd:chcpu", + "cmd:chmem", + "cmd:choom", + "cmd:chrt", + "cmd:col", + "cmd:colcrt", + "cmd:colrm", + "cmd:column", + "cmd:ctrlaltdel", + "cmd:delpart", + "cmd:dmesg", + "cmd:eject", + "cmd:fallocate", + "cmd:fdformat", + "cmd:fdisk", + "cmd:fincore", + "cmd:findfs", + "cmd:flock", + "cmd:fsck", + "cmd:fsck.cramfs", + "cmd:fsck.minix", + "cmd:fsfreeze", + "cmd:fstrim", + "cmd:getopt", + "cmd:hexdump", + "cmd:hwclock", + "cmd:i386", + "cmd:ionice", + "cmd:ipcmk", + "cmd:ipcrm", + "cmd:ipcs", + "cmd:isosize", + "cmd:ldattach", + "cmd:linux32", + "cmd:linux64", + "cmd:logger", + "cmd:look", + "cmd:losetup", + "cmd:lsblk", + "cmd:lscpu", + "cmd:lsipc", + "cmd:lslocks", + "cmd:lslogins", + "cmd:lsmem", + "cmd:lsns", + "cmd:mesg", + "cmd:mkfs", + "cmd:mkfs.bfs", + "cmd:mkfs.cramfs", + "cmd:mkfs.minix", + "cmd:mkswap", + "cmd:more", + "cmd:mount", + "cmd:mountpoint", + "cmd:namei", + "cmd:nologin", + "cmd:nsenter", + "cmd:partx", + "cmd:pivot_root", + "cmd:prlimit", + "cmd:raw", + "cmd:readprofile", + "cmd:rename", + "cmd:renice", + "cmd:resizepart", + "cmd:rev", + "cmd:rfkill", + "cmd:rtcwake", + "cmd:script", + "cmd:scriptreplay", + "cmd:setarch", + "cmd:setsid", + "cmd:setterm", + "cmd:swaplabel", + "cmd:swapoff", + "cmd:swapon", + "cmd:switch_root", + "cmd:taskset", + "cmd:ul", + "cmd:umount", + "cmd:uname26", + "cmd:unshare", + "cmd:utmpdump", + "cmd:uuidgen", + "cmd:uuidparse", + "cmd:wall", + "cmd:wdctl", + "cmd:whereis", + "cmd:wipefs", + "cmd:x86_64", + "cmd:zramctl" + ] + }, + "zfs-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308087 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "gzip-doc": { + "versions": { + "1.10-r0": 1546345202 + }, + "origin": "gzip" + }, + "gptfdisk-doc": { + "versions": { + "1.0.4-r0": 1543926505 + }, + "origin": "gptfdisk" + }, + "libpciaccess-doc": { + "versions": { + "0.14-r0": 1545838880 + }, + "origin": "libpciaccess" + }, + "perl-gdtextutil-doc": { + "versions": { + "0.86-r0": 1543248468 + }, + "origin": "perl-gdtextutil" + }, + "ngrep": { + "versions": { + "1.45-r4": 1543242183 + }, + "origin": "ngrep", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:ngrep" + ] + }, + "charybdis-doc": { + "versions": { + "3.5.6-r1": 1542883316 + }, + "origin": "charybdis" + }, + "b43-fwcutter": { + "versions": { + "019-r0": 1545163908 + }, + "origin": "b43-fwcutter", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:b43-fwcutter" + ] + }, + "gnupg-doc": { + "versions": { + "2.2.12-r0": 1545746222 + }, + "origin": "gnupg" + }, + "gtk+2.0": { + "versions": { + "2.24.32-r1": 1543925514 + }, + "origin": "gtk+2.0", + "dependencies": [ + "shared-mime-info", + "gtk-update-icon-cache", + "/bin/sh", + "so:libX11.so.6", + "so:libXcomposite.so.1", + "so:libXcursor.so.1", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXi.so.6", + "so:libXrandr.so.2", + "so:libXrender.so.1", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcups.so.2", + "so:libfontconfig.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0" + ], + "provides": [ + "so:libgailutil.so.18=18.0.1", + "so:libgdk-x11-2.0.so.0=0.2400.32", + "so:libgtk-x11-2.0.so.0=0.2400.32", + "cmd:gtk-query-immodules-2.0" + ] + }, + "libxml2-dev": { + "versions": { + "2.9.9-r1": 1551105514 + }, + "origin": "libxml2", + "dependencies": [ + "zlib-dev", + "libxml2=2.9.9-r1", + "pkgconfig" + ], + "provides": [ + "pc:libxml-2.0=2.9.9", + "cmd:xml2-config" + ] + }, + "musl-dev": { + "versions": { + "1.1.20-r4": 1552989415 + }, + "origin": "musl", + "dependencies": [ + "musl=1.1.20-r4" + ] + }, + "pptpd": { + "versions": { + "1.4.0-r1": 1544000994 + }, + "origin": "pptpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bcrelay", + "cmd:pptpctrl", + "cmd:pptpd" + ] + }, + "smartmontools-doc": { + "versions": { + "6.6-r1": 1545163011 + }, + "origin": "smartmontools" + }, + "postfix-policyd-spf-perl": { + "versions": { + "2.007-r2": 1545069435 + }, + "origin": "postfix-policyd-spf-perl", + "dependencies": [ + "perl", + "perl-mail-spf", + "perl-netaddr-ip" + ], + "provides": [ + "cmd:postfix-policyd-spf-perl" + ] + }, + "clutter-lang": { + "versions": { + "1.26.2-r2": 1545293148 + }, + "origin": "clutter" + }, + "hwdata-usb": { + "versions": { + "0.318-r0": 1545746062 + }, + "origin": "hwdata" + }, + "ruby-augeas": { + "versions": { + "0.5.0-r4": 1545301045 + }, + "origin": "ruby-augeas", + "dependencies": [ + "ruby", + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.5" + ] + }, + "libtiffxx": { + "versions": { + "4.0.10-r0": 1543921906 + }, + "origin": "tiff", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6", + "so:libtiff.so.5" + ], + "provides": [ + "so:libtiffxx.so.5=5.4.0" + ] + }, + "gcc-gnat": { + "versions": { + "8.3.0-r0": 1554745499 + }, + "origin": "gcc", + "dependencies": [ + "gcc=8.3.0-r0", + "libgnat=8.3.0-r0", + "libgnat=8.3.0-r0", + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libisl.so.15", + "so:libmpc.so.3", + "so:libmpfr.so.4", + "so:libz.so.1" + ], + "provides": [ + "cmd:gnat", + "cmd:gnatbind", + "cmd:gnatchop", + "cmd:gnatclean", + "cmd:gnatfind", + "cmd:gnatkr", + "cmd:gnatlink", + "cmd:gnatls", + "cmd:gnatmake", + "cmd:gnatname", + "cmd:gnatprep", + "cmd:gnatxref" + ] + }, + "nmap-nping": { + "versions": { + "7.70-r3": 1545163173 + }, + "origin": "nmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libpcap.so.1", + "so:libssl.so.1.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:nping" + ] + }, + "py-mock": { + "versions": { + "2.0.0-r3": 1543925747 + }, + "origin": "py-mock", + "dependencies": [ + "py-pbr", + "py-six" + ] + }, + "lighttpd-mod_webdav": { + "versions": { + "1.4.52-r0": 1545207630 + }, + "origin": "lighttpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0", + "so:libuuid.so.1", + "so:libxml2.so.2" + ] + }, + "qemu-mipsn32el": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mipsn32el" + ] + }, + "xcb-util-cursor": { + "versions": { + "0.1.3-r1": 1545163435 + }, + "origin": "xcb-util-cursor", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-image.so.0", + "so:libxcb-render-util.so.0", + "so:libxcb-render.so.0", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-cursor.so.0=0.0.0" + ] + }, + "harfbuzz-static": { + "versions": { + "2.2.0-r0": 1545249742 + }, + "origin": "harfbuzz" + }, + "libtls-standalone-dbg": { + "versions": { + "2.7.4-r6": 1546784623 + }, + "origin": "libtls-standalone", + "dependencies": [ + "ca-certificates-cacert" + ] + }, + "apache2-ldap": { + "versions": { + "2.4.39-r0": 1554306835 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "apr-util-ldap", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "qemu-microblaze": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-microblaze" + ] + }, + "iwlwifi-5000-ucode-doc": { + "versions": { + "8.83.5.1-r0": 1542845811 + }, + "origin": "iwlwifi-5000-ucode" + }, + "lksctp-tools-dev": { + "versions": { + "1.0.17-r0": 1543241223 + }, + "origin": "lksctp-tools", + "dependencies": [ + "lksctp-tools=1.0.17-r0", + "pkgconfig" + ], + "provides": [ + "pc:libsctp=1.0.17" + ] + }, + "py2-tox": { + "versions": { + "3.2.1-r1": 1545060599 + }, + "origin": "py-tox", + "dependencies": [ + "py2-py", + "py2-pluggy", + "py2-argparse", + "py-virtualenv", + "python2" + ], + "provides": [ + "cmd:tox", + "cmd:tox-quickstart" + ] + }, + "qemu-system-riscv64": { + "versions": { + "3.1.0-r3": 1551107305 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-riscv64" + ] + }, + "qemu-ui-sdl": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ] + }, + "gettext-dbg": { + "versions": { + "0.19.8.1-r4": 1542304411 + }, + "origin": "gettext" + }, + "py2-avahi": { + "versions": { + "0.7-r1": 1543925312 + }, + "origin": "avahi", + "dependencies": [ + "avahi=0.7-r1", + "python2" + ], + "provides": [ + "py-avahi=0.7-r1" + ] + }, + "libetpan": { + "versions": { + "1.9.2-r0": 1548063823 + }, + "origin": "libetpan", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:libgnutls.so.30", + "so:libsasl2.so.3", + "so:libz.so.1" + ], + "provides": [ + "so:libetpan.so.20=20.3.0" + ] + }, + "py-icu": { + "versions": { + "1.9.8-r2": 1545165267 + }, + "origin": "py-icu" + }, + "perl-scope-guard-doc": { + "versions": { + "0.21-r0": 1542924584 + }, + "origin": "perl-scope-guard" + }, + "uwsgi-webdav": { + "versions": { + "2.0.17.1-r0": 1545062214 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ] + }, + "linux-firmware-microchip": { + "versions": { + "20190322-r0": 1554980650 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "tzdata": { + "versions": { + "2019a-r0": 1555329220 + }, + "origin": "tzdata", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:posixtz", + "cmd:zdump", + "cmd:zic" + ] + }, + "nghttp2-libs": { + "versions": { + "1.35.1-r0": 1545858450 + }, + "origin": "nghttp2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnghttp2.so.14=14.17.1" + ] + }, + "sprunge": { + "versions": { + "0.6-r0": 1545076751 + }, + "origin": "sprunge", + "dependencies": [ + "curl" + ], + "provides": [ + "cmd:sprunge" + ] + }, + "rsyslog-http": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ], + "provides": [ + "rsyslog-omhttp=8.40.0-r3", + "rsyslog-fmhttp=8.40.0-r3" + ] + }, + "uwsgi-transformation_offload": { + "versions": { + "2.0.17.1-r0": 1545062212 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "udisks-dev": { + "versions": { + "1.0.5-r3": 1545069134 + }, + "origin": "udisks", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:udisks=1.0.5" + ] + }, + "rrdtool-utils": { + "versions": { + "1.7.0-r0": 1542924807 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "cmd:rrdcreate", + "cmd:rrdinfo", + "cmd:rrdupdate" + ] + }, + "su-exec": { + "versions": { + "0.2-r0": 1543999480 + }, + "origin": "su-exec", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:su-exec" + ] + }, + "qemu-sh4eb": { + "versions": { + "3.1.0-r3": 1551107302 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sh4eb" + ] + }, + "drill": { + "versions": { + "1.7.0-r2": 1545117117 + }, + "origin": "ldns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldns.so.2" + ], + "provides": [ + "cmd:drill" + ] + }, + "vde2": { + "versions": { + "2.3.2-r10": 1545074025 + }, + "origin": "vde2", + "dependencies": [ + "openssl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpcap.so.1", + "so:libpython2.7.so.1.0", + "so:libvdehist.so.0", + "so:libvdemgmt.so.0", + "so:libvdeplug.so.3" + ], + "provides": [ + "cmd:dpipe", + "cmd:slirpvde", + "cmd:unixcmd", + "cmd:unixterm", + "cmd:vde_autolink", + "cmd:vde_cryptcab", + "cmd:vde_l3", + "cmd:vde_over_ns", + "cmd:vde_pcapplug", + "cmd:vde_plug", + "cmd:vde_plug2tap", + "cmd:vde_switch", + "cmd:vde_tunctl", + "cmd:vdecmd", + "cmd:vdekvm", + "cmd:vdeq", + "cmd:vdeqemu", + "cmd:vdeterm", + "cmd:wirefilter" + ] + }, + "py3-lxml": { + "versions": { + "4.2.5-r0": 1545065048 + }, + "origin": "py-lxml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libpython3.6m.so.1.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ] + }, + "gnutls-utils": { + "versions": { + "3.6.7-r0": 1555049964 + }, + "origin": "gnutls", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30", + "so:libtasn1.so.6" + ], + "provides": [ + "cmd:certtool", + "cmd:gnutls-cli", + "cmd:gnutls-cli-debug", + "cmd:gnutls-serv", + "cmd:ocsptool", + "cmd:p11tool", + "cmd:psktool", + "cmd:srptool" + ] + }, + "perl-mail-imapclient-doc": { + "versions": { + "3.39-r0": 1545075908 + }, + "origin": "perl-mail-imapclient" + }, + "mariadb-connector-c": { + "versions": { + "3.0.8-r0": 1547470538 + }, + "origin": "mariadb-connector-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libmariadb.so.3=3" + ] + }, + "xf86-input-mouse-doc": { + "versions": { + "1.9.3-r1": 1545300644 + }, + "origin": "xf86-input-mouse" + }, + "xdelta3-doc": { + "versions": { + "3.0.11-r0": 1545207370 + }, + "origin": "xdelta3" + }, + "avahi-dev": { + "versions": { + "0.7-r1": 1543925309 + }, + "origin": "avahi", + "dependencies": [ + "gdbm-dev", + "avahi-compat-howl=0.7-r1", + "avahi-compat-libdns_sd=0.7-r1", + "avahi-glib=0.7-r1", + "avahi-libs=0.7-r1", + "avahi=0.7-r1", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:avahi-client=0.7", + "pc:avahi-compat-howl=0.9.8", + "pc:avahi-compat-libdns_sd=0.7", + "pc:avahi-core=0.7", + "pc:avahi-glib=0.7", + "pc:avahi-gobject=0.7", + "pc:howl=0.9.8", + "pc:libdns_sd=0.7" + ] + }, + "uwsgi-carbon": { + "versions": { + "2.0.17.1-r0": 1545062196 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "dahdi-tools-dev": { + "versions": { + "2.11.1-r0": 1543932615 + }, + "origin": "dahdi-tools", + "dependencies": [ + "bsd-compat-headers", + "linux-headers", + "dahdi-linux-dev", + "newt-dev", + "dahdi-tools=2.11.1-r0" + ] + }, + "wxgtk2.8-lang": { + "versions": { + "2.8.12.1-r4": 1545075286 + }, + "origin": "wxgtk2.8" + }, + "ragel": { + "versions": { + "6.10-r0": 1545208963 + }, + "origin": "ragel", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:ragel" + ] + }, + "pacman": { + "versions": { + "5.0.2-r4": 1543932340 + }, + "origin": "pacman", + "dependencies": [ + "libarchive-tools", + "so:libarchive.so.13", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libgpgme.so.11", + "so:libintl.so.8" + ], + "provides": [ + "so:libalpm.so.10=10.0.2", + "cmd:cleanupdelta", + "cmd:makepkg", + "cmd:makepkg-template", + "cmd:pacman", + "cmd:pacman-db-upgrade", + "cmd:pacman-key", + "cmd:pacman-optimize", + "cmd:pacsort", + "cmd:pactree", + "cmd:pkgdelta", + "cmd:repo-add", + "cmd:repo-elephant", + "cmd:repo-remove", + "cmd:testpkg", + "cmd:vercmp" + ] + }, + "libjpeg-turbo-utils": { + "versions": { + "1.5.3-r4": 1547027583 + }, + "origin": "libjpeg-turbo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:libturbojpeg.so.0" + ], + "provides": [ + "cmd:cjpeg", + "cmd:djpeg", + "cmd:jpegtran", + "cmd:rdjpgcom", + "cmd:tjbench", + "cmd:wrjpgcom" + ] + }, + "irssi-proxy": { + "versions": { + "1.1.2-r0": 1547738041 + }, + "origin": "irssi", + "dependencies": [ + "irssi", + "so:libc.musl-x86_64.so.1" + ] + }, + "rp-pppoe-openrc": { + "versions": { + "3.13-r0": 1545223084 + }, + "origin": "rp-pppoe" + }, + "xmlrpc-c": { + "versions": { + "1.39.13-r0": 1545665221 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "so:libxmlrpc.so.3=3.39", + "so:libxmlrpc_server.so.3=3.39", + "so:libxmlrpc_util.so.3=3.39" + ] + }, + "joe": { + "versions": { + "4.6-r0": 1543221866 + }, + "origin": "joe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:jmacs", + "cmd:joe", + "cmd:jpico", + "cmd:jstar", + "cmd:rjoe" + ] + }, + "gcc-objc": { + "versions": { + "8.3.0-r0": 1554745498 + }, + "origin": "gcc", + "dependencies": [ + "libc-dev", + "gcc=8.3.0-r0", + "libobjc=8.3.0-r0", + "libobjc=8.3.0-r0" + ] + }, + "squark-dbg": { + "versions": { + "0.6.1-r1": 1545060928 + }, + "origin": "squark", + "dependencies": [ + "haserl" + ] + }, + "spawn-fcgi-doc": { + "versions": { + "1.6.4-r3": 1542821586 + }, + "origin": "spawn-fcgi" + }, + "byacc-doc": { + "versions": { + "20180609-r0": 1545062012 + }, + "origin": "byacc" + }, + "uwsgi-logpipe": { + "versions": { + "2.0.17.1-r0": 1545062202 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-mozilla-ca-doc": { + "versions": { + "20160104-r0": 1545223045 + }, + "origin": "perl-mozilla-ca" + }, + "syslog-ng-amqp": { + "versions": { + "3.19.1-r0": 1548543150 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librabbitmq.so.4", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "linux-firmware-sun": { + "versions": { + "20190322-r0": 1554980647 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "lua-filesystem": { + "versions": { + "1.7.0.2-r0": 1542820936 + }, + "origin": "lua-filesystem" + }, + "krb5": { + "versions": { + "1.15.5-r0": 1548094458 + }, + "origin": "krb5", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libgssapi_krb5.so.2", + "so:libk5crypto.so.3", + "so:libkadm5clnt_mit.so.11", + "so:libkadm5srv_mit.so.11", + "so:libkdb5.so.8", + "so:libkrb5.so.3", + "so:libkrb5support.so.0", + "so:libss.so.2" + ], + "provides": [ + "cmd:gss-client", + "cmd:k5srvutil", + "cmd:kadmin", + "cmd:kdestroy", + "cmd:kinit", + "cmd:klist", + "cmd:kpasswd", + "cmd:ksu", + "cmd:kswitch", + "cmd:ktutil", + "cmd:kvno", + "cmd:sim_client", + "cmd:uuclient" + ] + }, + "libxkbcommon": { + "versions": { + "0.8.2-r0": 1545857327 + }, + "origin": "libxkbcommon", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-xkb.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxkbcommon-x11.so.0=0.0.0", + "so:libxkbcommon.so.0=0.0.0" + ] + }, + "py3-future": { + "versions": { + "0.17.0-r0": 1545213720 + }, + "origin": "py-future", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:futurize", + "cmd:pasteurize" + ] + }, + "py-gnome-libgnome": { + "versions": { + "2.28.1-r5": 1545301292 + }, + "origin": "py-gnome", + "dependencies": [ + "py-gtk", + "py-gnome-bonobo", + "py-gnome-gnomecanvas", + "py-gnome-gnomevfs", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgnome-2.so.0", + "so:libgobject-2.0.so.0", + "so:libpopt.so.0", + "so:libpython2.7.so.1.0" + ] + }, + "samba-client-libs": { + "versions": { + "4.8.11-r1": 1555334973 + }, + "origin": "samba", + "dependencies": [ + "so:libaddns-samba4.so", + "so:libasn1util-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libinterfaces-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libpopt.so.0", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsmbconf.so.0", + "so:libsocket-blocking-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-cmdline-samba4.so", + "so:libutil-reg-samba4.so" + ], + "provides": [ + "so:libcli-ldap-samba4.so=0", + "so:libcmdline-contexts-samba4.so=0", + "so:libcmdline-credentials-samba4.so=0", + "so:libdcerpc.so.0=0.0.1", + "so:libdsdb-garbage-collect-tombstones-samba4.so=0", + "so:libevents-samba4.so=0", + "so:libhttp-samba4.so=0", + "so:libnetif-samba4.so=0", + "so:libpopt-samba3-cmdline-samba4.so=0", + "so:libregistry-samba4.so=0", + "so:libsmbclient-raw-samba4.so=0" + ] + }, + "pixman-static": { + "versions": { + "0.34.0-r6": 1542822838 + }, + "origin": "pixman" + }, + "libtool-doc": { + "versions": { + "2.4.6-r5": 1542301346 + }, + "origin": "libtool" + }, + "iwlwifi-1000-ucode-doc": { + "versions": { + "39.31.5.1-r0": 1545076625 + }, + "origin": "iwlwifi-1000-ucode" + }, + "pciutils-libs": { + "versions": { + "3.6.2-r0": 1543921866 + }, + "origin": "pciutils", + "dependencies": [ + "hwdata-pci", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpci.so.3=3.6.2" + ] + }, + "perl-html-mason-psgihandler-doc": { + "versions": { + "0.53-r0": 1545299872 + }, + "origin": "perl-html-mason-psgihandler" + }, + "gtk-engines-thinice": { + "versions": { + "2.21.0-r2": 1545289338 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "s6-networking-doc": { + "versions": { + "2.3.0.3-r1": 1546957736 + }, + "origin": "s6-networking" + }, + "c-ares-doc": { + "versions": { + "1.15.0-r0": 1544791695 + }, + "origin": "c-ares" + }, + "freeswitch-sounds-en-us-callie-8000": { + "versions": { + "1.0.51-r0": 1545116746 + }, + "origin": "freeswitch-sounds-en-us-callie-8000" + }, + "ninja-doc": { + "versions": { + "1.8.2-r1": 1542822428 + }, + "origin": "ninja" + }, + "imagemagick-c++": { + "versions": { + "7.0.8.44-r0": 1557126217 + }, + "origin": "imagemagick", + "dependencies": [ + "so:libMagickCore-7.Q16HDRI.so.6", + "so:libMagickWand-7.Q16HDRI.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libMagick++-7.Q16HDRI.so.4=4.0.0" + ] + }, + "xsetmode": { + "versions": { + "1.0.0-r4": 1545075298 + }, + "origin": "xsetmode", + "dependencies": [ + "so:libX11.so.6", + "so:libXi.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xsetmode" + ] + }, + "collectd-write_redis": { + "versions": { + "5.8.0-r3": 1546422677 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.14" + ] + }, + "gtkglext": { + "versions": { + "1.2.0-r12": 1544001308 + }, + "origin": "gtkglext", + "dependencies": [ + "so:libGL.so.1", + "so:libGLU.so.1", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangox-1.0.so.0" + ], + "provides": [ + "so:libgdkglext-x11-1.0.so.0=0.0.0", + "so:libgtkglext-x11-1.0.so.0=0.0.0" + ] + }, + "qemu-system-mips": { + "versions": { + "3.1.0-r3": 1551107303 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libstdc++.so.6", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-mips" + ] + }, + "indent-doc": { + "versions": { + "2.2.12-r0": 1545299643 + }, + "origin": "indent" + }, + "skalibs-doc": { + "versions": { + "2.7.0.0-r0": 1543221683 + }, + "origin": "skalibs" + }, + "dovecot": { + "versions": { + "2.3.6-r0": 1557134099 + }, + "origin": "dovecot", + "dependencies": [ + "openssl", + "/bin/sh", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:lib01_acl_plugin.so=0", + "so:lib02_imap_acl_plugin.so=0", + "so:lib02_lazy_expunge_plugin.so=0", + "so:lib05_mail_crypt_acl_plugin.so=0", + "so:lib05_pop3_migration_plugin.so=0", + "so:lib05_snarf_plugin.so=0", + "so:lib10_last_login_plugin.so=0", + "so:lib10_mail_crypt_plugin.so=0", + "so:lib10_mail_filter_plugin.so=0", + "so:lib10_quota_plugin.so=0", + "so:lib11_imap_quota_plugin.so=0", + "so:lib11_trash_plugin.so=0", + "so:lib15_notify_plugin.so=0", + "so:lib20_autocreate_plugin.so=0", + "so:lib20_charset_alias_plugin.so=0", + "so:lib20_expire_plugin.so=0", + "so:lib20_fts_plugin.so=0", + "so:lib20_listescape_plugin.so=0", + "so:lib20_mail_log_plugin.so=0", + "so:lib20_mailbox_alias_plugin.so=0", + "so:lib20_notify_status_plugin.so=0", + "so:lib20_push_notification_plugin.so=0", + "so:lib20_quota_clone_plugin.so=0", + "so:lib20_replication_plugin.so=0", + "so:lib20_var_expand_crypt.so=0", + "so:lib20_virtual_plugin.so=0", + "so:lib20_zlib_plugin.so=0", + "so:lib21_fts_squat_plugin.so=0", + "so:lib30_imap_zlib_plugin.so=0", + "so:lib90_old_stats_plugin.so=0", + "so:lib95_imap_old_stats_plugin.so=0", + "so:lib99_welcome_plugin.so=0", + "so:libdcrypt_openssl.so=0", + "so:libdovecot-compression.so.0=0.0.0", + "so:libdovecot-dsync.so.0=0.0.0", + "so:libdovecot-fts.so.0=0.0.0", + "so:libdovecot-lda.so.0=0.0.0", + "so:libdovecot-login.so.0=0.0.0", + "so:libdovecot-storage.so.0=0.0.0", + "so:libdovecot.so.0=0.0.0", + "so:libfs_compress.so=0", + "so:libfs_crypt.so=0", + "so:libfs_mail_crypt.so=0", + "so:libold_stats_mail.so=0", + "so:libssl_iostream_openssl.so=0", + "so:libstats_auth.so=0", + "cmd:doveadm", + "cmd:doveconf", + "cmd:dovecot", + "cmd:dsync" + ] + }, + "gnokii-doc": { + "versions": { + "0.6.31-r8": 1545300479 + }, + "origin": "gnokii" + }, + "libxfixes-doc": { + "versions": { + "5.0.3-r2": 1542823751 + }, + "origin": "libxfixes" + }, + "perl-filesys-notify-simple-doc": { + "versions": { + "0.13-r0": 1543927835 + }, + "origin": "perl-filesys-notify-simple" + }, + "libfdisk": { + "versions": { + "2.33-r0": 1545307436 + }, + "origin": "util-linux", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libfdisk.so.1=1.1.0" + ] + }, + "perl-control-x10-doc": { + "versions": { + "2.09-r1": 1545253944 + }, + "origin": "perl-control-x10" + }, + "atop-doc": { + "versions": { + "2.3.0-r1": 1543226695 + }, + "origin": "atop" + }, + "uwsgi-router_redis": { + "versions": { + "2.0.17.1-r0": 1545062208 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-pgen": { + "versions": { + "2.7.10-r0": 1545300069 + }, + "origin": "py-pgen", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pgen" + ] + }, + "clamav-milter": { + "versions": { + "0.100.3-r0": 1555507336 + }, + "origin": "clamav", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libmilter.so.1.0.2", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:clamav-milter" + ] + }, + "aria2": { + "versions": { + "1.34.0-r1": 1543998971 + }, + "origin": "aria2", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libgmp.so.10", + "so:libgnutls.so.30", + "so:libnettle.so.6", + "so:libsqlite3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:aria2c" + ] + }, + "perl-convert-asn1": { + "versions": { + "0.27-r0": 1543081688 + }, + "origin": "perl-convert-asn1" + }, + "unrar": { + "versions": { + "5.6.8-r0": 1545069402 + }, + "origin": "unrar", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:unrar" + ] + }, + "speex-dev": { + "versions": { + "1.2.0-r0": 1544799247 + }, + "origin": "speex", + "dependencies": [ + "pkgconfig", + "speex=1.2.0-r0" + ], + "provides": [ + "pc:speex=1.2.0" + ] + }, + "perl-business-hours": { + "versions": { + "0.13-r0": 1547709105 + }, + "origin": "perl-business-hours", + "dependencies": [ + "perl-set-intspan" + ] + }, + "xorg-server-xwayland": { + "versions": { + "1.20.3-r1": 1543928069 + }, + "origin": "xorg-server", + "dependencies": [ + "font-misc-misc", + "font-cursor-misc", + "xkeyboard-config", + "xkbcomp", + "xinit", + "so:libGL.so.1", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libdrm.so.2", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libpixman-1.so.0", + "so:libwayland-client.so.0", + "so:libxshmfence.so.1" + ], + "provides": [ + "cmd:Xwayland" + ] + }, + "apcupsd-webif": { + "versions": { + "3.14.14-r0": 1545293177 + }, + "origin": "apcupsd", + "dependencies": [ + "util-linux", + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ] + }, + "kamailio-sqlite": { + "versions": { + "5.2.0-r1": 1546423169 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "kamailio-db", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0", + "so:libsrdb1.so.1" + ] + }, + "libtasn1-dev": { + "versions": { + "4.13-r0": 1542824444 + }, + "origin": "libtasn1", + "dependencies": [ + "libtasn1=4.13-r0", + "pkgconfig" + ], + "provides": [ + "pc:libtasn1=4.13" + ] + }, + "gdk-pixbuf-lang": { + "versions": { + "2.36.11-r2": 1543253990 + }, + "origin": "gdk-pixbuf", + "dependencies": [ + "shared-mime-info" + ] + }, + "libxfont-dev": { + "versions": { + "1.5.4-r1": 1542924728 + }, + "origin": "libxfont", + "dependencies": [ + "libxfont=1.5.4-r1", + "pc:fontenc", + "pc:fontsproto", + "pc:freetype2", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xfont=1.5.4" + ] + }, + "dovecot-fts-solr": { + "versions": { + "2.3.6-r0": 1557134099 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot=2.3.6-r0", + "so:lib20_fts_plugin.so", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ], + "provides": [ + "so:lib21_fts_solr_plugin.so=0" + ] + }, + "gpgme": { + "versions": { + "1.12.0-r3": 1543932322 + }, + "origin": "gpgme", + "dependencies": [ + "gnupg", + "so:libassuan.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgpg-error.so.0" + ], + "provides": [ + "so:libgpgme.so.11=11.21.0", + "cmd:gpgme-json", + "cmd:gpgme-tool" + ] + }, + "mlmmj": { + "versions": { + "1.3.0-r0": 1542883220 + }, + "origin": "mlmmj", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mlmmj-bounce", + "cmd:mlmmj-list", + "cmd:mlmmj-maintd", + "cmd:mlmmj-make-ml", + "cmd:mlmmj-make-ml.sh", + "cmd:mlmmj-process", + "cmd:mlmmj-receive", + "cmd:mlmmj-recieve", + "cmd:mlmmj-send", + "cmd:mlmmj-sub", + "cmd:mlmmj-unsub" + ] + }, + "bridge-utils-doc": { + "versions": { + "1.6-r0": 1543935359 + }, + "origin": "bridge-utils" + }, + "s6-networking-dev": { + "versions": { + "2.3.0.3-r1": 1546957736 + }, + "origin": "s6-networking", + "dependencies": [ + "s6-networking=2.3.0.3-r1" + ] + }, + "kamailio-doc": { + "versions": { + "5.2.0-r1": 1546423162 + }, + "origin": "kamailio" + }, + "ctags": { + "versions": { + "5.8-r5": 1545301883 + }, + "origin": "ctags", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ctags" + ] + }, + "gnokii-smsd": { + "versions": { + "0.6.31-r8": 1545300479 + }, + "origin": "gnokii", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnokii.so.7", + "so:libintl.so.8" + ], + "provides": [ + "cmd:smsd" + ] + }, + "dhcp-doc": { + "versions": { + "4.4.1-r1": 1543928451 + }, + "origin": "dhcp" + }, + "qemu-modules": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "qemu-audio-alsa", + "qemu-audio-oss", + "qemu-audio-sdl", + "qemu-block-curl", + "qemu-block-dmg-bz2", + "qemu-block-nfs", + "qemu-block-ssh", + "qemu-ui-curses", + "qemu-ui-gtk", + "qemu-ui-sdl" + ] + }, + "ruby-test-unit": { + "versions": { + "2.5.5-r0": 1557164837 + }, + "origin": "ruby", + "dependencies": [ + "ruby", + "ruby-power_assert" + ] + }, + "stfl": { + "versions": { + "0.24-r2": 1545075303 + }, + "origin": "stfl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libstfl.so.0=0.24" + ] + }, + "linux-firmware-mwlwifi": { + "versions": { + "20190322-r0": 1554980650 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "py3-asn1crypto": { + "versions": { + "0.24.0-r0": 1543246738 + }, + "origin": "py-asn1crypto", + "dependencies": [ + "python3" + ] + }, + "acl-doc": { + "versions": { + "2.2.52-r5": 1542304042 + }, + "origin": "acl" + }, + "lua5.2-rex-posix": { + "versions": { + "2.9.0-r0": 1545209204 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libunique-dev": { + "versions": { + "1.1.6-r6": 1545299840 + }, + "origin": "libunique", + "dependencies": [ + "libunique=1.1.6-r6", + "pc:gtk+-2.0", + "pkgconfig" + ], + "provides": [ + "pc:unique-1.0=1.1.6" + ] + }, + "openntpd": { + "versions": { + "6.2_p3-r2": 1543230955 + }, + "origin": "openntpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtls-standalone.so.1" + ], + "provides": [ + "cmd:ntpctl", + "cmd:ntpd" + ] + }, + "libmikmod-dev": { + "versions": { + "3.3.11.1-r0": 1545162983 + }, + "origin": "libmikmod", + "dependencies": [ + "libmikmod=3.3.11.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmikmod=3.3.11", + "cmd:libmikmod-config" + ] + }, + "faad2-doc": { + "versions": { + "2.7-r8": 1543998810 + }, + "origin": "faad2" + }, + "drbd-utils": { + "versions": { + "9.7.1-r0": 1545746125 + }, + "origin": "drbd-utils", + "dependencies": [ + "bash", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "drbd=9.7.1-r0", + "cmd:drbdadm", + "cmd:drbdmeta", + "cmd:drbdmon", + "cmd:drbdsetup" + ] + }, + "xfsprogs": { + "versions": { + "4.19.0-r1": 1546597448 + }, + "origin": "xfsprogs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:fsck.xfs", + "cmd:mkfs.xfs", + "cmd:xfs_repair" + ] + }, + "perl-extutils-pkgconfig": { + "versions": { + "1.16-r1": 1543077189 + }, + "origin": "perl-extutils-pkgconfig" + }, + "opennhrp": { + "versions": { + "0.14.1-r6": 1545069405 + }, + "origin": "opennhrp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2" + ], + "provides": [ + "cmd:opennhrp", + "cmd:opennhrpctl" + ] + }, + "perl-params-util-doc": { + "versions": { + "1.07-r5": 1542845639 + }, + "origin": "perl-params-util" + }, + "perl-config-grammar-doc": { + "versions": { + "1.12-r0": 1543999449 + }, + "origin": "perl-config-grammar" + }, + "perl-convert-binhex-doc": { + "versions": { + "1.125-r1": 1544001491 + }, + "origin": "perl-convert-binhex" + }, + "cups-lang": { + "versions": { + "2.2.10-r0": 1545910846 + }, + "origin": "cups", + "dependencies": [ + "cups-client", + "poppler-utils", + "openssl", + "dbus" + ] + }, + "libquadmath": { + "versions": { + "8.3.0-r0": 1554745497 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libquadmath.so.0=0.0.0" + ] + }, + "freeswitch-sounds-music-32000": { + "versions": { + "1.0.8-r1": 1545075920 + }, + "origin": "freeswitch-sounds-music-32000" + }, + "perl-ipc-system-simple-doc": { + "versions": { + "1.25-r0": 1542924660 + }, + "origin": "perl-ipc-system-simple" + }, + "zeromq-dev": { + "versions": { + "4.3.1-r0": 1548271358 + }, + "origin": "zeromq", + "dependencies": [ + "libzmq=4.3.1-r0", + "pc:libsodium", + "pkgconfig" + ], + "provides": [ + "pc:libzmq=4.3.1" + ] + }, + "duplicity-doc": { + "versions": { + "0.7.18.2-r0": 1544000473 + }, + "origin": "duplicity" + }, + "acf-openntpd": { + "versions": { + "0.9.0-r2": 1544799255 + }, + "origin": "acf-openntpd", + "dependencies": [ + "acf-core", + "openntpd" + ] + }, + "py3-mako": { + "versions": { + "1.0.7-r0": 1543220555 + }, + "origin": "py-mako", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:mako-render" + ] + }, + "squid-lang-el": { + "versions": { + "4.4-r1": 1545216324 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "acf-squid": { + "versions": { + "0.11.0-r2": 1545216338 + }, + "origin": "acf-squid", + "dependencies": [ + "acf-core", + "squid" + ] + }, + "vim": { + "versions": { + "8.1.0630-r0": 1545664995 + }, + "origin": "vim", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua-5.3.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:ex", + "cmd:rview", + "cmd:rvim", + "cmd:view", + "cmd:vim", + "cmd:vimtutor", + "cmd:xxd" + ] + }, + "libice-dev": { + "versions": { + "1.0.9-r3": 1542824332 + }, + "origin": "libice", + "dependencies": [ + "libice=1.0.9-r3", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:ice=1.0.9" + ] + }, + "gstreamer-doc": { + "versions": { + "1.14.4-r0": 1543254159 + }, + "origin": "gstreamer" + }, + "font-winitzki-cyrillic": { + "versions": { + "1.0.3-r0": 1545224037 + }, + "origin": "font-winitzki-cyrillic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "font-adobe-utopia-75dpi": { + "versions": { + "1.0.4-r0": 1545062788 + }, + "origin": "font-adobe-utopia-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "syslog-ng-geoip": { + "versions": { + "3.19.1-r0": 1548543150 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "gnats-doc": { + "versions": { + "4.2.0-r5": 1545209105 + }, + "origin": "gnats" + }, + "cfdisk": { + "versions": { + "2.33-r0": 1545307437 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfdisk.so.1", + "so:libmount.so.1", + "so:libncursesw.so.6", + "so:libsmartcols.so.1" + ], + "provides": [ + "cmd:cfdisk" + ] + }, + "haserl-lua5.3": { + "versions": { + "0.9.35-r1": 1542883797 + }, + "origin": "haserl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua-5.3.so.0" + ], + "provides": [ + "cmd:haserl-lua5.3" + ] + }, + "unifont": { + "versions": { + "11.0.01-r1": 1543077262 + }, + "origin": "unifont", + "dependencies": [ + "bdftopcf", + "perl-gd", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bdfimplode", + "cmd:hex2bdf", + "cmd:hex2sfd", + "cmd:hexbraille", + "cmd:hexdraw", + "cmd:hexkinya", + "cmd:hexmerge", + "cmd:johab2ucs2", + "cmd:unibdf2hex", + "cmd:unibmp2hex", + "cmd:unicoverage", + "cmd:unidup", + "cmd:unifont-viewer", + "cmd:unifont1per", + "cmd:unifontchojung", + "cmd:unifontksx", + "cmd:unifontpic", + "cmd:unigencircles", + "cmd:unigenwidth", + "cmd:unihex2bmp", + "cmd:unihex2png", + "cmd:unihexfill", + "cmd:unihexgen", + "cmd:unipagecount", + "cmd:unipng2hex" + ] + }, + "bzip2": { + "versions": { + "1.0.6-r6": 1542300119 + }, + "origin": "bzip2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bunzip2", + "cmd:bzcat", + "cmd:bzcmp", + "cmd:bzdiff", + "cmd:bzegrep", + "cmd:bzfgrep", + "cmd:bzgrep", + "cmd:bzip2", + "cmd:bzip2recover", + "cmd:bzless", + "cmd:bzmore" + ] + }, + "dhcpcd": { + "versions": { + "7.0.8-r0": 1545207907 + }, + "origin": "dhcpcd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dhcpcd" + ] + }, + "font-bh-type1": { + "versions": { + "1.0.3-r0": 1543934557 + }, + "origin": "font-bh-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-package-deprecationmanager-doc": { + "versions": { + "0.17-r0": 1542845764 + }, + "origin": "perl-package-deprecationmanager" + }, + "squark-doc": { + "versions": { + "0.6.1-r1": 1545060928 + }, + "origin": "squark" + }, + "swatch": { + "versions": { + "3.2.4-r4": 1545116516 + }, + "origin": "swatch", + "dependencies": [ + "perl", + "perl-date-calc", + "perl-date-format", + "perl-date-manip", + "perl-file-tail", + "perl-carp-clan" + ], + "provides": [ + "cmd:swatchdog" + ] + }, + "zsh-doc": { + "versions": { + "5.6.2-r0": 1545308085 + }, + "origin": "zsh" + }, + "perl-file-slurper-doc": { + "versions": { + "0.010-r0": 1545075942 + }, + "origin": "perl-file-slurper" + }, + "rhash": { + "versions": { + "1.3.6-r2": 1542820449 + }, + "origin": "rhash", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librhash.so.0" + ], + "provides": [ + "cmd:ed2k-link", + "cmd:edonr256-hash", + "cmd:edonr512-hash", + "cmd:gost-hash", + "cmd:has160-hash", + "cmd:magnet-link", + "cmd:rhash", + "cmd:sfv-hash", + "cmd:tiger-hash", + "cmd:tth-hash", + "cmd:whirlpool-hash" + ] + }, + "qemu-system-mips64": { + "versions": { + "3.1.0-r3": 1551107304 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libstdc++.so.6", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-mips64" + ] + }, + "enca": { + "versions": { + "1.19-r1": 1545076573 + }, + "origin": "enca", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libenca.so.0=0.5.1", + "cmd:enca", + "cmd:enconv" + ] + }, + "file-dev": { + "versions": { + "5.36-r0": 1557160812 + }, + "origin": "file", + "dependencies": [ + "libmagic=5.36-r0" + ] + }, + "mailx": { + "versions": { + "8.1.1-r1": 1545075412 + }, + "origin": "mailx", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mail" + ] + }, + "py-rrd": { + "versions": { + "1.7.0-r0": 1542924804 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:librrd.so.8" + ] + }, + "eudev-libs": { + "versions": { + "3.2.7-r0": 1542845384 + }, + "origin": "eudev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libudev.so.1=1.6.3" + ] + }, + "nghttp2-dev": { + "versions": { + "1.35.1-r0": 1545858449 + }, + "origin": "nghttp2", + "dependencies": [ + "nghttp2-libs=1.35.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnghttp2=1.35.1" + ] + }, + "e2fsprogs-dev": { + "versions": { + "1.44.5-r0": 1545745857 + }, + "origin": "e2fsprogs", + "dependencies": [ + "util-linux-dev", + "e2fsprogs-libs=1.44.5-r0", + "libcom_err=1.44.5-r0", + "pkgconfig" + ], + "provides": [ + "pc:com_err=1.44.5", + "pc:e2p=1.44.5", + "pc:ext2fs=1.44.5", + "pc:ss=1.44.5", + "cmd:compile_et", + "cmd:mk_cmds" + ] + }, + "snort-openrc": { + "versions": { + "2.9.12-r0": 1545301013 + }, + "origin": "snort" + }, + "jack-doc": { + "versions": { + "1.9.12-r0": 1545073439 + }, + "origin": "jack" + }, + "libtxc_dxtn": { + "versions": { + "1.0.1-r5": 1545289388 + }, + "origin": "libtxc_dxtn", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtxc_dxtn.so=0" + ] + }, + "kamailio-xml": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libsrdb1.so.1", + "so:libxml2.so.2" + ] + }, + "iperf3-openrc": { + "versions": { + "3.6-r0": 1543932136 + }, + "origin": "iperf3" + }, + "py-packaging": { + "versions": { + "17.1-r0": 1542825057 + }, + "origin": "py-packaging", + "dependencies": [ + "py-parsing", + "py-six" + ] + }, + "smartmontools": { + "versions": { + "6.6-r1": 1545163012 + }, + "origin": "smartmontools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:smartctl", + "cmd:smartd", + "cmd:update-smart-drivedb" + ] + }, + "sngtc_client": { + "versions": { + "1.3.7-r1": 1545117161 + }, + "origin": "sngtc_client", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libortp.so.10" + ], + "provides": [ + "so:libsngtc_node.so=0", + "cmd:sngtc_client" + ] + }, + "perl-html-parser-doc": { + "versions": { + "3.72-r2": 1542821838 + }, + "origin": "perl-html-parser" + }, + "lua-ldbus": { + "versions": { + "20150430-r2": 1545300027 + }, + "origin": "lua-ldbus" + }, + "cmph": { + "versions": { + "2.0-r1": 1544000442 + }, + "origin": "cmph", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcmph.so.0" + ], + "provides": [ + "cmd:cmph" + ] + }, + "lxc-templates-legacy": { + "versions": { + "3.0.3-r0": 1546416504 + }, + "origin": "lxc-templates-legacy", + "dependencies": [ + "bash", + "tar" + ] + }, + "uwsgi-router_expires": { + "versions": { + "2.0.17.1-r0": 1545062206 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libmspack-dev": { + "versions": { + "0.8_alpha-r0": 1543925821 + }, + "origin": "libmspack", + "dependencies": [ + "libmspack=0.8_alpha-r0", + "pkgconfig" + ] + }, + "perl-io-captureoutput-doc": { + "versions": { + "1.1104-r0": 1543259612 + }, + "origin": "perl-io-captureoutput" + }, + "patchutils-doc": { + "versions": { + "0.3.4-r1": 1548427864 + }, + "origin": "patchutils" + }, + "mg-doc": { + "versions": { + "20180824-r0": 1545208623 + }, + "origin": "mg" + }, + "lzo-dev": { + "versions": { + "2.10-r2": 1542985321 + }, + "origin": "lzo", + "dependencies": [ + "lzo=2.10-r2", + "pkgconfig" + ], + "provides": [ + "pc:lzo2=2.10" + ] + }, + "acpi": { + "versions": { + "1.7-r2": 1545300419 + }, + "origin": "acpi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:acpi" + ] + }, + "libxpm": { + "versions": { + "3.5.12-r0": 1542894077 + }, + "origin": "libxpm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXpm.so.4=4.11.0", + "cmd:cxpm", + "cmd:sxpm" + ] + }, + "gmock": { + "versions": { + "1.8.1-r0": 1542822390 + }, + "origin": "gtest", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgtest.so", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgmock.so=0", + "so:libgmock_main.so=0" + ] + }, + "py-django-simple-captcha": { + "versions": { + "0.4.3-r1": 1543934502 + }, + "origin": "py-django-simple-captcha", + "dependencies": [ + "python2" + ] + }, + "libxv-dev": { + "versions": { + "1.0.11-r2": 1542900290 + }, + "origin": "libxv", + "dependencies": [ + "libxv=1.0.11-r2", + "pc:videoproto", + "pc:x11", + "pc:xext", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xv=1.0.11" + ] + }, + "xdelta3": { + "versions": { + "3.0.11-r0": 1545207373 + }, + "origin": "xdelta3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xdelta3" + ] + }, + "vim-doc": { + "versions": { + "8.1.0630-r0": 1545664991 + }, + "origin": "vim" + }, + "iaxmodem": { + "versions": { + "1.3.0-r0": 1545214093 + }, + "origin": "iaxmodem", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:iaxmodem" + ] + }, + "alsa-lib-dev": { + "versions": { + "1.1.8-r0": 1547112801 + }, + "origin": "alsa-lib", + "dependencies": [ + "alsa-lib=1.1.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:alsa=1.1.8" + ] + }, + "task-doc": { + "versions": { + "2.5.1-r0": 1545254342 + }, + "origin": "task" + }, + "nagios-plugins-uptime": { + "versions": { + "2.2.1-r6": 1543933913 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "rtmpdump-doc": { + "versions": { + "2.4_git20160909-r6": 1545215000 + }, + "origin": "rtmpdump" + }, + "hunspell": { + "versions": { + "1.6.2-r1": 1542883765 + }, + "origin": "hunspell", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libreadline.so.7", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libhunspell-1.6.so.0=0.0.1", + "cmd:affixcompress", + "cmd:analyze", + "cmd:chmorph", + "cmd:hunspell", + "cmd:hunzip", + "cmd:hzip", + "cmd:ispellaff2myspell", + "cmd:makealias", + "cmd:munch", + "cmd:unmunch", + "cmd:wordforms", + "cmd:wordlist2hunspell" + ] + }, + "execline-doc": { + "versions": { + "2.5.0.1-r0": 1543221686 + }, + "origin": "execline" + }, + "mariadb-openrc": { + "versions": { + "10.3.13-r1": 1557431734 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common" + ] + }, + "linenoise-dev": { + "versions": { + "1.0-r1": 1542304877 + }, + "origin": "linenoise", + "dependencies": [ + "linenoise=1.0-r1" + ] + }, + "perl-probe-perl-doc": { + "versions": { + "0.03-r0": 1545061087 + }, + "origin": "perl-probe-perl" + }, + "libee": { + "versions": { + "0.4.1-r0": 1545292595 + }, + "origin": "libee", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libestr.so.0" + ], + "provides": [ + "so:libee.so.0=0.0.0", + "cmd:libee-convert" + ] + }, + "xorgproto": { + "versions": { + "2018.4-r0": 1542822442 + }, + "origin": "xorgproto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "bigreqsproto=2018.4-r0", + "compositeproto=2018.4-r0", + "damageproto=2018.4-r0", + "dri2proto=2018.4-r0", + "dri3proto=2018.4-r0", + "fixesproto=2018.4-r0", + "fontsproto=2018.4-r0", + "glproto=2018.4-r0", + "inputproto=2018.4-r0", + "kbproto=2018.4-r0", + "presentproto=2018.4-r0", + "printproto=2018.4-r0", + "randrproto=2018.4-r0", + "recordproto=2018.4-r0", + "renderproto=2018.4-r0", + "resourceproto=2018.4-r0", + "scrnsaverproto=2018.4-r0", + "videoproto=2018.4-r0", + "xcmiscproto=2018.4-r0", + "xextproto=2018.4-r0", + "xf86bigfontproto=2018.4-r0", + "xf86dgaproto=2018.4-r0", + "xf86driproto=2018.4-r0", + "xf86miscproto=2018.4-r0", + "xf86vidmodeproto=2018.4-r0", + "xineramaproto=2018.4-r0", + "xproto=2018.4-r0", + "pc:bigreqsproto=1.1.2", + "pc:compositeproto=0.4.2", + "pc:damageproto=1.2.1", + "pc:dmxproto=2.3.1", + "pc:dri2proto=2.8", + "pc:dri3proto=1.2", + "pc:evieproto=1.1.1", + "pc:fixesproto=5.0", + "pc:fontcacheproto=0.1.3", + "pc:fontsproto=2.1.3", + "pc:glproto=1.4.17", + "pc:inputproto=2.3.2", + "pc:kbproto=1.0.7", + "pc:lg3dproto=5.0", + "pc:presentproto=1.2", + "pc:printproto=1.0.5", + "pc:randrproto=1.6.0", + "pc:recordproto=1.14.2", + "pc:renderproto=0.11.1", + "pc:resourceproto=1.2.0", + "pc:scrnsaverproto=1.2.2", + "pc:trapproto=3.4.3", + "pc:videoproto=2.3.3", + "pc:xcalibrateproto=0.1.0", + "pc:xcmiscproto=1.2.2", + "pc:xextproto=7.3.0", + "pc:xf86bigfontproto=1.2.0", + "pc:xf86dgaproto=2.1", + "pc:xf86driproto=2.1.1", + "pc:xf86miscproto=0.9.3", + "pc:xf86rushproto=1.2.2", + "pc:xf86vidmodeproto=2.3.1", + "pc:xineramaproto=1.2.1", + "pc:xproto=7.0.32", + "pc:xproxymngproto=1.0.3" + ] + }, + "wget": { + "versions": { + "1.20.3-r0": 1554718236 + }, + "origin": "wget", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:wget" + ] + }, + "freeswitch-freetdm": { + "versions": { + "1.8.2-r1": 1545117892 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexecinfo.so.1", + "so:libfreeswitch.so.1", + "so:libpri.so.1.4" + ], + "provides": [ + "so:libfreetdm.so.1=1.0.0" + ] + }, + "py-asn1crypto": { + "versions": { + "0.24.0-r0": 1543246741 + }, + "origin": "py-asn1crypto" + }, + "xf86-video-openchrome": { + "versions": { + "0.6.0-r4": 1545249767 + }, + "origin": "xf86-video-openchrome", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libXvMC.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2" + ], + "provides": [ + "so:libchromeXvMC.so.1=1.0.0", + "so:libchromeXvMCPro.so.1=1.0.0" + ] + }, + "halberd-doc": { + "versions": { + "0.2.4-r1": 1545299950 + }, + "origin": "halberd" + }, + "perl-http-body": { + "versions": { + "1.17-r0": 1544001351 + }, + "origin": "perl-http-body", + "dependencies": [ + "perl", + "perl-http-message", + "perl-uri" + ] + }, + "perl-digest-hmac-doc": { + "versions": { + "1.03-r0": 1542924831 + }, + "origin": "perl-digest-hmac" + }, + "imagemagick-libs": { + "versions": { + "7.0.8.44-r0": 1557126217 + }, + "origin": "imagemagick", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:liblcms2.so.2", + "so:libltdl.so.7", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libMagickCore-7.Q16HDRI.so.6=6.0.0", + "so:libMagickWand-7.Q16HDRI.so.6=6.0.0" + ] + }, + "gparted-doc": { + "versions": { + "0.33.0-r0": 1545746178 + }, + "origin": "gparted" + }, + "ksymoops": { + "versions": { + "2.4.11-r7": 1545060803 + }, + "origin": "ksymoops", + "dependencies": [ + "so:libbfd-2.31.1.so", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ksymoops" + ] + }, + "djbdns-common": { + "versions": { + "1.05-r47": 1545208950 + }, + "origin": "djbdns", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dnsip", + "cmd:dnsqr" + ] + }, + "imagemagick": { + "versions": { + "7.0.8.44-r0": 1557126218 + }, + "origin": "imagemagick", + "dependencies": [ + "so:libMagickCore-7.Q16HDRI.so.6", + "so:libMagickWand-7.Q16HDRI.so.6", + "so:libX11.so.6", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgs.so.9", + "so:libjpeg.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpng16.so.16", + "so:librsvg-2.so.2", + "so:libtiff.so.5", + "so:libwebp.so.7", + "so:libwebpmux.so.3", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:animate", + "cmd:compare", + "cmd:composite", + "cmd:conjure", + "cmd:convert", + "cmd:display", + "cmd:identify", + "cmd:import", + "cmd:magick", + "cmd:magick-script", + "cmd:mogrify", + "cmd:montage", + "cmd:stream" + ] + }, + "qemu-ui-gtk": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libepoxy.so.0", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libvte-2.91.so.0" + ] + }, + "perl-device-serialport": { + "versions": { + "1.04-r10": 1545062312 + }, + "origin": "perl-device-serialport", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:modemtest" + ] + }, + "gsm-tools": { + "versions": { + "1.0.18-r0": 1543927819 + }, + "origin": "gsm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgsm.so.1" + ], + "provides": [ + "cmd:tcat", + "cmd:toast", + "cmd:untoast" + ] + }, + "perl-crypt-eksblowfish": { + "versions": { + "0.009-r4": 1545213647 + }, + "origin": "perl-crypt-eksblowfish", + "dependencies": [ + "perl-class-mix", + "so:libc.musl-x86_64.so.1" + ] + }, + "libnfsidmap-ldap": { + "versions": { + "2.3.2-r1": 1543933423 + }, + "origin": "nfs-utils", + "dependencies": [ + "rpcbind", + "so:libc.musl-x86_64.so.1", + "so:libldap-2.4.so.2" + ] + }, + "partimage": { + "versions": { + "0.6.9-r6": 1543924736 + }, + "origin": "partimage", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libnewt.so.0.52", + "so:libslang.so.2", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:partimage", + "cmd:partimaged" + ] + }, + "freeswitch-dbg": { + "versions": { + "1.8.2-r1": 1545117891 + }, + "origin": "freeswitch" + }, + "libdnet-dev": { + "versions": { + "1.12-r7": 1545076672 + }, + "origin": "libdnet", + "dependencies": [ + "libdnet=1.12-r7" + ], + "provides": [ + "cmd:dnet-config" + ] + }, + "ngrep-dbg": { + "versions": { + "1.45-r4": 1543242182 + }, + "origin": "ngrep" + }, + "hunspell-en": { + "versions": { + "2018.04.16-r0": 1543928313 + }, + "origin": "hunspell-en" + }, + "lxc-doc": { + "versions": { + "3.1.0-r1": 1549966161 + }, + "origin": "lxc" + }, + "xvinfo-doc": { + "versions": { + "1.1.3-r0": 1545301889 + }, + "origin": "xvinfo" + }, + "libhistory": { + "versions": { + "7.0.003-r1": 1542301065 + }, + "origin": "readline", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhistory.so.7=7.0" + ] + }, + "libdvdread": { + "versions": { + "6.0.0-r0": 1545838475 + }, + "origin": "libdvdread", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdvdcss.so.2" + ], + "provides": [ + "so:libdvdread.so.4=4.2.0" + ] + }, + "http-parser-dev": { + "versions": { + "2.8.1-r0": 1543934665 + }, + "origin": "http-parser", + "dependencies": [ + "http-parser=2.8.1-r0" + ] + }, + "debootstrap-doc": { + "versions": { + "1.0.111-r0": 1545257151 + }, + "origin": "debootstrap" + }, + "perl-class-tiny-doc": { + "versions": { + "1.006-r0": 1543076768 + }, + "origin": "perl-class-tiny" + }, + "m4-doc": { + "versions": { + "1.4.18-r1": 1542300481 + }, + "origin": "m4" + }, + "icu-dev": { + "versions": { + "62.1-r0": 1542823973 + }, + "origin": "icu", + "dependencies": [ + "icu=62.1-r0", + "icu-libs=62.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:icu-i18n=62.1", + "pc:icu-io=62.1", + "pc:icu-uc=62.1", + "cmd:icu-config" + ] + }, + "monit": { + "versions": { + "5.25.2-r0": 1545858142 + }, + "origin": "monit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:monit" + ] + }, + "device-mapper-libs": { + "versions": { + "2.02.182-r0": 1543928934 + }, + "origin": "lvm2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdevmapper.so.1.02=1.02" + ] + }, + "squid-lang-zh": { + "versions": { + "4.4-r1": 1545216331 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "autoconf": { + "versions": { + "2.69-r2": 1542301300 + }, + "origin": "autoconf", + "dependencies": [ + "m4", + "perl" + ], + "provides": [ + "cmd:autoconf", + "cmd:autoheader", + "cmd:autom4te", + "cmd:autoreconf", + "cmd:autoscan", + "cmd:autoupdate", + "cmd:ifnames" + ] + }, + "perl-data-page": { + "versions": { + "2.02-r1": 1544000370 + }, + "origin": "perl-data-page", + "dependencies": [ + "perl-class-accessor-chained", + "perl-test-exception" + ] + }, + "ckbcomp-doc": { + "versions": { + "1.187-r0": 1545292994 + }, + "origin": "ckbcomp" + }, + "myrepos-doc": { + "versions": { + "1.20180726-r0": 1542893508 + }, + "origin": "myrepos" + }, + "zd1211-firmware": { + "versions": { + "1.5-r0": 1544799019 + }, + "origin": "zd1211-firmware" + }, + "ferm": { + "versions": { + "2.4.1-r2": 1545292992 + }, + "origin": "ferm", + "dependencies": [ + "perl", + "iptables" + ], + "provides": [ + "cmd:ferm", + "cmd:import-ferm" + ] + }, + "drbd-utils-pacemaker": { + "versions": { + "9.7.1-r0": 1545746125 + }, + "origin": "drbd-utils", + "dependencies": [ + "drbd-utils=9.7.1-r0" + ] + }, + "py3-idna": { + "versions": { + "2.7-r0": 1542825008 + }, + "origin": "py-idna", + "dependencies": [ + "python3" + ] + }, + "chrony-doc": { + "versions": { + "3.4-r1": 1545841267 + }, + "origin": "chrony" + }, + "py-paramiko-demos": { + "versions": { + "2.4.2-r0": 1545216486 + }, + "origin": "py-paramiko", + "dependencies": [ + "py-asn1", + "py-cryptography", + "py-bcrypt", + "py-pynacl" + ] + }, + "perl-devel-stacktrace-ashtml": { + "versions": { + "0.15-r0": 1544792342 + }, + "origin": "perl-devel-stacktrace-ashtml", + "dependencies": [ + "perl", + "perl-devel-stacktrace" + ] + }, + "perl-fcgi-procmanager-doc": { + "versions": { + "0.28-r0": 1543924483 + }, + "origin": "perl-fcgi-procmanager" + }, + "uwsgi-transformation_tofile": { + "versions": { + "2.0.17.1-r0": 1545062213 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "py3-lxc": { + "versions": { + "3.0.3-r0": 1546416509 + }, + "origin": "py3-lxc", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:liblxc.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "avahi-compat-howl": { + "versions": { + "0.7-r1": 1543925311 + }, + "origin": "avahi", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhowl.so.0=0.0.0" + ] + }, + "gnupg": { + "versions": { + "2.2.12-r0": 1545746222 + }, + "origin": "gnupg", + "dependencies": [ + "pinentry", + "so:libassuan.so.0", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libgnutls.so.30", + "so:libgpg-error.so.0", + "so:libksba.so.8", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libnpth.so.0", + "so:libsqlite3.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:addgnupghome", + "cmd:applygnupgdefaults", + "cmd:dirmngr", + "cmd:dirmngr-client", + "cmd:gpg", + "cmd:gpg-agent", + "cmd:gpg-connect-agent", + "cmd:gpg-wks-server", + "cmd:gpg-zip", + "cmd:gpg2", + "cmd:gpgconf", + "cmd:gpgparsemail", + "cmd:gpgscm", + "cmd:gpgsm", + "cmd:gpgtar", + "cmd:gpgv", + "cmd:gpgv2", + "cmd:kbxutil", + "cmd:watchgnupg" + ] + }, + "unixodbc-dev": { + "versions": { + "2.3.7-r0": 1542845421 + }, + "origin": "unixodbc", + "dependencies": [ + "pkgconfig", + "unixodbc=2.3.7-r0" + ], + "provides": [ + "pc:odbc=2.3.7", + "pc:odbccr=2.3.7", + "pc:odbcinst=2.3.7" + ] + }, + "cegui06-dev": { + "versions": { + "0.6.2b-r15": 1543935898 + }, + "origin": "cegui06", + "dependencies": [ + "cegui06=0.6.2b-r15", + "pkgconfig" + ], + "provides": [ + "pc:CEGUI-OPENGL=0.6.2", + "pc:CEGUI=0.6.2" + ] + }, + "perl-convert-color-doc": { + "versions": { + "0.11-r0": 1545062507 + }, + "origin": "perl-convert-color" + }, + "xcb-util-keysyms-dev": { + "versions": { + "0.4.0-r1": 1543927910 + }, + "origin": "xcb-util-keysyms", + "dependencies": [ + "xcb-util-dev", + "pkgconfig", + "xcb-util-keysyms=0.4.0-r1" + ], + "provides": [ + "pc:xcb-keysyms=0.4.0" + ] + }, + "ferm-doc": { + "versions": { + "2.4.1-r2": 1545292991 + }, + "origin": "ferm" + }, + "qemu-block-curl": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libglib-2.0.so.0" + ] + }, + "libestr-dev": { + "versions": { + "0.1.10-r0": 1545060618 + }, + "origin": "libestr", + "dependencies": [ + "libestr=0.1.10-r0", + "pkgconfig" + ], + "provides": [ + "pc:libestr=0.1.10" + ] + }, + "samba-client": { + "versions": { + "4.8.11-r1": 1555334973 + }, + "origin": "samba", + "dependencies": [ + "samba-common=4.8.11-r1", + "so:libCHARSET3-samba4.so", + "so:libaddns-samba4.so", + "so:libarchive.so.13", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcli-spoolss-samba4.so", + "so:libcliauth-samba4.so", + "so:libcmdline-contexts-samba4.so", + "so:libcmdline-credentials-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libevents-samba4.so", + "so:libformw.so.6", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libkrb5-samba4.so.26", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:liblibcli-lsa3-samba4.so", + "so:liblibcli-netlogon3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libncursesw.so.6", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libpanelw.so.6", + "so:libpopt-samba3-cmdline-samba4.so", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libregistry-samba4.so", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb-common-samba4.so", + "so:libsecrets3-samba4.so", + "so:libserver-role-samba4.so", + "so:libsmbclient-raw-samba4.so", + "so:libsmbclient.so.0", + "so:libsmbconf.so.0", + "so:libsocket-blocking-samba4.so", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtrusts-util-samba4.so", + "so:libutil-cmdline-samba4.so", + "so:libutil-reg-samba4.so", + "so:libutil-tdb-samba4.so" + ], + "provides": [ + "cmd:cifsdd", + "cmd:dbwrap_tool", + "cmd:findsmb", + "cmd:mvxattr", + "cmd:nmblookup", + "cmd:oLschema2ldif", + "cmd:regdiff", + "cmd:regpatch", + "cmd:regshell", + "cmd:regtree", + "cmd:rpcclient", + "cmd:samba-regedit", + "cmd:sharesec", + "cmd:smbcacls", + "cmd:smbclient", + "cmd:smbcquotas", + "cmd:smbget", + "cmd:smbprint", + "cmd:smbspool", + "cmd:smbtar", + "cmd:smbtree" + ] + }, + "spamassassin": { + "versions": { + "3.4.2-r0": 1545061983 + }, + "origin": "spamassassin", + "dependencies": [ + "perl-mail-spamassassin", + "curl" + ], + "provides": [ + "cmd:sa-awl", + "cmd:sa-check_spamd", + "cmd:sa-learn", + "cmd:sa-update", + "cmd:spamassassin", + "cmd:spamd" + ] + }, + "findutils-doc": { + "versions": { + "4.6.0-r1": 1545073419 + }, + "origin": "findutils" + }, + "awall": { + "versions": { + "1.6.9-r0": 1548501700 + }, + "origin": "awall", + "dependencies": [ + "drill", + "ip6tables", + "ipset", + "iptables", + "lua5.2", + "lua5.2-alt-getopt", + "lua5.2-cjson", + "lua5.2-pc", + "lua5.2-posix", + "lua5.2-stringy", + "xtables-addons", + "/bin/sh" + ], + "provides": [ + "cmd:awall" + ] + }, + "ltrace-doc": { + "versions": { + "0.7.3-r1": 1544798547 + }, + "origin": "ltrace" + }, + "upower-lang": { + "versions": { + "0.99.7-r0": 1545069386 + }, + "origin": "upower" + }, + "perl-module-refresh-doc": { + "versions": { + "0.17-r1": 1545292777 + }, + "origin": "perl-module-refresh" + }, + "wayland-libs-cursor": { + "versions": { + "1.16.0-r0": 1544001083 + }, + "origin": "wayland", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libwayland-client.so.0" + ], + "provides": [ + "so:libwayland-cursor.so.0=0.0.0" + ] + }, + "polkit-doc": { + "versions": { + "0.105-r9": 1547130960 + }, + "origin": "polkit" + }, + "libnice-dbg": { + "versions": { + "0.1.14-r2": 1543928834 + }, + "origin": "libnice" + }, + "uwsgi-router_uwsgi": { + "versions": { + "2.0.17.1-r0": 1545062209 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "xhost-doc": { + "versions": { + "1.0.7-r1": 1545300591 + }, + "origin": "xhost" + }, + "libgsf-lang": { + "versions": { + "1.14.44-r0": 1543926574 + }, + "origin": "libgsf" + }, + "py3-ecdsa": { + "versions": { + "0.13-r5": 1545301307 + }, + "origin": "py-ecdsa", + "dependencies": [ + "py3-crypto" + ] + }, + "perl-javascript-minifier-xs": { + "versions": { + "0.11-r3": 1543998978 + }, + "origin": "perl-javascript-minifier-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "lksctp-tools": { + "versions": { + "1.0.17-r0": 1543241224 + }, + "origin": "lksctp-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsctp.so.1=1.0.17", + "cmd:checksctp", + "cmd:sctp_darn", + "cmd:sctp_status", + "cmd:sctp_test", + "cmd:withsctp" + ] + }, + "git-subtree": { + "versions": { + "2.20.1-r0": 1545214195 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0" + ] + }, + "aoetools-doc": { + "versions": { + "37-r0": 1545073928 + }, + "origin": "aoetools" + }, + "perl-heap-doc": { + "versions": { + "0.80-r1": 1545213984 + }, + "origin": "perl-heap" + }, + "linux-firmware-matrox": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "dillo": { + "versions": { + "3.0.5-r6": 1545302349 + }, + "origin": "dillo", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libfltk.so.1.3", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:dillo", + "cmd:dillo-install-hyphenation", + "cmd:dpid", + "cmd:dpidc" + ] + }, + "jwhois": { + "versions": { + "4.0-r4": 1545300910 + }, + "origin": "jwhois", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4" + ], + "provides": [ + "cmd:jwhois" + ] + }, + "glib-dbg": { + "versions": { + "2.58.1-r2": 1545290824 + }, + "origin": "glib" + }, + "protobuf-dev": { + "versions": { + "3.6.1-r1": 1543931874 + }, + "origin": "protobuf", + "dependencies": [ + "zlib-dev", + "pkgconfig", + "protobuf=3.6.1-r1" + ], + "provides": [ + "pc:protobuf-lite=3.6.1", + "pc:protobuf=3.6.1" + ] + }, + "py3-cparser": { + "versions": { + "2.19-r0": 1546791396 + }, + "origin": "py-cparser", + "dependencies": [ + "python3" + ] + }, + "py3-simplejson": { + "versions": { + "3.15.0-r0": 1543998766 + }, + "origin": "py-simplejson", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "uwsgi-router_rewrite": { + "versions": { + "2.0.17.1-r0": 1545062208 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "mesa-dri-virtio": { + "versions": { + "18.1.7-r2": 1554455971 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "freetds-dev": { + "versions": { + "1.00.104-r0": 1543934550 + }, + "origin": "freetds", + "dependencies": [ + "freetds=1.00.104-r0" + ] + }, + "polkit-gnome-lang": { + "versions": { + "0.105-r1": 1544798743 + }, + "origin": "polkit-gnome" + }, + "lua5.1-rex-pcre": { + "versions": { + "2.9.0-r0": 1545209199 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ] + }, + "mupdf-doc": { + "versions": { + "1.13.0-r2": 1544000575 + }, + "origin": "mupdf" + }, + "parallel-doc": { + "versions": { + "20180622-r0": 1545154587 + }, + "origin": "parallel" + }, + "ghostscript-dev": { + "versions": { + "9.26-r2": 1554362638 + }, + "origin": "ghostscript", + "dependencies": [ + "ghostscript=9.26-r2", + "pkgconfig" + ], + "provides": [ + "pc:ijs=0.35" + ] + }, + "libdrm-doc": { + "versions": { + "2.4.96-r0": 1544791765 + }, + "origin": "libdrm" + }, + "hunspell-doc": { + "versions": { + "1.6.2-r1": 1542883764 + }, + "origin": "hunspell" + }, + "perl-path-tiny": { + "versions": { + "0.108-r0": 1543238927 + }, + "origin": "perl-path-tiny" + }, + "libdc1394-doc": { + "versions": { + "2.2.5-r1": 1545249933 + }, + "origin": "libdc1394" + }, + "lua-socket": { + "versions": { + "3.0_rc1_git20160306-r2": 1543934477 + }, + "origin": "lua-socket" + }, + "mariadb-embedded": { + "versions": { + "10.3.13-r1": 1557431735 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common=10.3.13-r1", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:liblzma.so.5", + "so:libpcre.so.1", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libmariadbd.so.19=19" + ] + }, + "elinks-doc": { + "versions": { + "0.13-r6": 1543934619 + }, + "origin": "elinks" + }, + "speex-tools": { + "versions": { + "1.2.0-r0": 1544799250 + }, + "origin": "speex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libspeex.so.1", + "so:libspeexdsp.so.1" + ], + "provides": [ + "cmd:speexdec", + "cmd:speexenc" + ] + }, + "qemu-ppc64le": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-ppc64le" + ] + }, + "perl-locale-maketext-lexicon": { + "versions": { + "1.00-r0": 1543249752 + }, + "origin": "perl-locale-maketext-lexicon", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:xgettext.pl" + ] + }, + "perl-datetime-format-w3cdtf-doc": { + "versions": { + "0.07-r0": 1543239033 + }, + "origin": "perl-datetime-format-w3cdtf" + }, + "gc": { + "versions": { + "7.6.4-r2": 1543226893 + }, + "origin": "gc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcord.so.1=1.3.0", + "so:libgc.so.1=1.3.2" + ] + }, + "linux-firmware-dsp56k": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "xsetroot-doc": { + "versions": { + "1.1.2-r0": 1545069360 + }, + "origin": "xsetroot" + }, + "mdocml-soelim": { + "versions": { + "1.14.3-r0": 1542304783 + }, + "origin": "mdocml", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:soelim" + ] + }, + "perl-cps-doc": { + "versions": { + "0.18-r0": 1545289360 + }, + "origin": "perl-cps" + }, + "perl-ipc-sharelite-doc": { + "versions": { + "0.17-r2": 1543222926 + }, + "origin": "perl-ipc-sharelite" + }, + "librevenge-doc": { + "versions": { + "0.0.4-r2": 1543932070 + }, + "origin": "librevenge" + }, + "perl-test-exception-doc": { + "versions": { + "0.43-r0": 1542883362 + }, + "origin": "perl-test-exception" + }, + "libcdio-paranoia-doc": { + "versions": { + "0.94_p1-r1": 1543248403 + }, + "origin": "libcdio-paranoia" + }, + "uriparser-dev": { + "versions": { + "0.9.1-r0": 1548111967 + }, + "origin": "uriparser", + "dependencies": [ + "pkgconfig", + "uriparser=0.9.1-r0" + ], + "provides": [ + "pc:liburiparser=0.9.1" + ] + }, + "lua5.3-xml": { + "versions": { + "130610-r5": 1543998788 + }, + "origin": "lua-xml", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "mesa-dri-freedreno": { + "versions": { + "18.1.7-r2": 1554455970 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "jack": { + "versions": { + "1.9.12-r0": 1545073440 + }, + "origin": "jack", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libgcc_s.so.1", + "so:libsamplerate.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libjack.so.0=0.1.0", + "so:libjacknet.so.0=0.1.0", + "so:libjackserver.so.0=0.1.0", + "cmd:jackd" + ] + }, + "perl-yaml-tiny-doc": { + "versions": { + "1.73-r0": 1542893321 + }, + "origin": "perl-yaml-tiny" + }, + "python3": { + "versions": { + "3.6.8-r2": 1554747639 + }, + "origin": "python3", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libexpat.so.1", + "so:libffi.so.6", + "so:libgdbm.so.4", + "so:libgdbm_compat.so.4", + "so:liblzma.so.5", + "so:libncursesw.so.6", + "so:libpanelw.so.6", + "so:libreadline.so.7", + "so:libsqlite3.so.0", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "py3-pip", + "so:libpython3.6m.so.1.0=1.0", + "so:libpython3.so=0", + "cmd:2to3", + "cmd:2to3-3.6", + "cmd:easy_install-3.6", + "cmd:pip3", + "cmd:pip3.6", + "cmd:pydoc3", + "cmd:pydoc3.6", + "cmd:python3", + "cmd:python3.6", + "cmd:python3.6m", + "cmd:pyvenv", + "cmd:pyvenv-3.6" + ] + }, + "gengetopt-dev": { + "versions": { + "2.22.6-r2": 1545062092 + }, + "origin": "gengetopt", + "dependencies": [ + "gengetopt" + ] + }, + "xbitmaps": { + "versions": { + "1.1.2-r0": 1545062458 + }, + "origin": "xbitmaps", + "dependencies": [ + "util-macros" + ] + }, + "ldb": { + "versions": { + "1.3.8-r0": 1555334661 + }, + "origin": "ldb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent.so.0" + ], + "provides": [ + "so:libldb.so.1=1.3.8" + ] + }, + "libraw1394-tools": { + "versions": { + "2.1.2-r1": 1543932098 + }, + "origin": "libraw1394", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libraw1394.so.11" + ], + "provides": [ + "cmd:dumpiso", + "cmd:sendiso", + "cmd:testlibraw" + ] + }, + "netcf-libs": { + "versions": { + "0.2.8-r5": 1543935400 + }, + "origin": "netcf", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libnl-3.so.200", + "so:libnl-route-3.so.200", + "so:libxml2.so.2", + "so:libxslt.so.1" + ], + "provides": [ + "so:libnetcf.so.1=1.4.0" + ] + }, + "lua5.2-pty": { + "versions": { + "1.2.1-r1": 1545216359 + }, + "origin": "lua-pty", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "rsyslog-gssapi": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libgssapi_krb5.so.2" + ], + "provides": [ + "rsyslog-lmgssutil=8.40.0-r3", + "rsyslog-imgssapi=8.40.0-r3", + "rsyslog-omgssapi=8.40.0-r3" + ] + }, + "perl-net-smtp-ssl-doc": { + "versions": { + "1.04-r0": 1545209112 + }, + "origin": "perl-net-smtp-ssl" + }, + "dropbear-convert": { + "versions": { + "2018.76-r2": 1545208976 + }, + "origin": "dropbear", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dropbearconvert" + ] + }, + "nfs-utils-doc": { + "versions": { + "2.3.2-r1": 1543933421 + }, + "origin": "nfs-utils" + }, + "dwm": { + "versions": { + "6.1-r2": 1545292628 + }, + "origin": "dwm", + "dependencies": [ + "dmenu", + "st", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1" + ], + "provides": [ + "cmd:dwm" + ] + }, + "lua5.2-sql-mysql": { + "versions": { + "2.3.5-r2": 1543924400 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "syslog-ng-sql": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libdbi.so.1", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "motif-dev": { + "versions": { + "2.3.4-r2": 1545223193 + }, + "origin": "motif", + "dependencies": [ + "libx11-dev", + "libxft-dev", + "libxt-dev", + "libxpm-dev", + "libxext-dev", + "xbitmaps", + "motif=2.3.4-r2" + ] + }, + "rrdtool-cached": { + "versions": { + "1.7.0-r0": 1542924805 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:librrd.so.8" + ], + "provides": [ + "cmd:rrdcached" + ] + }, + "perl-io-multiplex-doc": { + "versions": { + "1.16-r1": 1545163452 + }, + "origin": "perl-io-multiplex" + }, + "py3-argparse": { + "versions": { + "1.4.0-r2": 1545060593 + }, + "origin": "py-argparse", + "dependencies": [ + "python3" + ] + }, + "xfsprogs-extra": { + "versions": { + "4.19.0-r1": 1546597448 + }, + "origin": "xfsprogs", + "dependencies": [ + "xfsprogs", + "python3", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:xfs_admin", + "cmd:xfs_bmap", + "cmd:xfs_copy", + "cmd:xfs_db", + "cmd:xfs_estimate", + "cmd:xfs_freeze", + "cmd:xfs_fsr", + "cmd:xfs_growfs", + "cmd:xfs_info", + "cmd:xfs_io", + "cmd:xfs_logprint", + "cmd:xfs_mdrestore", + "cmd:xfs_metadump", + "cmd:xfs_mkfile", + "cmd:xfs_ncheck", + "cmd:xfs_quota", + "cmd:xfs_rtcp", + "cmd:xfs_scrub", + "cmd:xfs_scrub_all", + "cmd:xfs_spaceman" + ] + }, + "make-doc": { + "versions": { + "4.2.1-r2": 1542302737 + }, + "origin": "make" + }, + "perl-term-table-doc": { + "versions": { + "0.012-r0": 1542972988 + }, + "origin": "perl-term-table" + }, + "help2man-doc": { + "versions": { + "1.47.8-r0": 1545745816 + }, + "origin": "help2man" + }, + "perl-locale-maketext-fuzzy-doc": { + "versions": { + "0.11-r1": 1544000295 + }, + "origin": "perl-locale-maketext-fuzzy" + }, + "py-dbus": { + "versions": { + "1.2.8-r1": 1543925185 + }, + "origin": "py-dbus", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0" + ] + }, + "expat-doc": { + "versions": { + "2.2.6-r0": 1542302776 + }, + "origin": "expat" + }, + "perl-plack": { + "versions": { + "1.0033-r0": 1544792456 + }, + "origin": "perl-plack", + "dependencies": [ + "perl-file-sharedir", + "perl-filesys-notify-simple", + "perl-devel-stacktrace", + "perl-stream-buffered", + "perl-uri", + "perl-devel-stacktrace-ashtml", + "perl-http-body", + "perl-test-tcp", + "perl-try-tiny", + "perl-apache-logformat-compiler", + "perl-hash-multivalue", + "perl-http-message" + ], + "provides": [ + "cmd:plackup" + ] + }, + "gstreamer-dev": { + "versions": { + "1.14.4-r0": 1543254159 + }, + "origin": "gstreamer", + "dependencies": [ + "libxml2-dev", + "gstreamer=1.14.4-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-1.0=1.14.4", + "pc:gstreamer-base-1.0=1.14.4", + "pc:gstreamer-check-1.0=1.14.4", + "pc:gstreamer-controller-1.0=1.14.4", + "pc:gstreamer-net-1.0=1.14.4" + ] + }, + "lua5.3-lzmq": { + "versions": { + "0.4.4-r0": 1545076585 + }, + "origin": "lua-lzmq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5", + "so:libzmq.so.5" + ] + }, + "lua5.3-hashids": { + "versions": { + "1.0.6-r1": 1545209084 + }, + "origin": "lua-hashids", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-capture-tiny": { + "versions": { + "0.48-r0": 1542893272 + }, + "origin": "perl-capture-tiny" + }, + "fftw": { + "versions": { + "3.3.8-r0": 1543926479 + }, + "origin": "fftw", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfftw3.so.3", + "so:libfftw3_threads.so.3", + "so:libfftw3f.so.3", + "so:libfftw3f_threads.so.3", + "so:libfftw3l.so.3", + "so:libfftw3l_threads.so.3" + ], + "provides": [ + "cmd:fftw-wisdom", + "cmd:fftw-wisdom-to-conf", + "cmd:fftwf-wisdom", + "cmd:fftwl-wisdom" + ] + }, + "flac-dev": { + "versions": { + "1.3.2-r2": 1545068440 + }, + "origin": "flac", + "dependencies": [ + "flac=1.3.2-r2", + "pc:ogg", + "pkgconfig" + ], + "provides": [ + "pc:flac++=1.3.2", + "pc:flac=1.3.2" + ] + }, + "acf-ppp": { + "versions": { + "0.5.0-r2": 1543999028 + }, + "origin": "acf-ppp", + "dependencies": [ + "acf-core", + "ppp" + ] + }, + "py-bluez": { + "versions": { + "0.22-r1": 1545062452 + }, + "origin": "py-bluez" + }, + "perl-dbi-doc": { + "versions": { + "1.642-r0": 1544792614 + }, + "origin": "perl-dbi" + }, + "lua5.1-filesystem": { + "versions": { + "1.7.0.2-r0": 1542820932 + }, + "origin": "lua-filesystem", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "ruby-rake": { + "versions": { + "2.5.5-r0": 1557164837 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ], + "provides": [ + "cmd:rake" + ] + }, + "py2-scandir": { + "versions": { + "1.9.0-r1": 1542824906 + }, + "origin": "py-scandir", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "libdvdnav": { + "versions": { + "5.0.3-r1": 1543999001 + }, + "origin": "libdvdnav", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdvdread.so.4" + ], + "provides": [ + "so:libdvdnav.so.4=4.2.0" + ] + }, + "perl-stream-buffered": { + "versions": { + "0.03-r0": 1544792337 + }, + "origin": "perl-stream-buffered" + }, + "lua5.2-mqtt-publish": { + "versions": { + "0.3-r0": 1542820948 + }, + "origin": "lua-mqtt-publish", + "dependencies": [ + "lua5.2-mosquitto" + ] + }, + "newt": { + "versions": { + "0.52.20-r0": 1543924705 + }, + "origin": "newt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0", + "so:libslang.so.2" + ], + "provides": [ + "so:libnewt.so.0.52=0.52.20", + "cmd:whiptail" + ] + }, + "menu-cache": { + "versions": { + "0.5.1-r2": 1543999314 + }, + "origin": "menu-cache", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libmenu-cache.so.3=3.0.1" + ] + }, + "tftp-hpa-doc": { + "versions": { + "5.2-r2": 1545207835 + }, + "origin": "tftp-hpa" + }, + "libnotify-dev": { + "versions": { + "0.7.7-r2": 1544001119 + }, + "origin": "libnotify", + "dependencies": [ + "gdk-pixbuf-dev", + "glib-dev", + "dbus-dev", + "libnotify=0.7.7-r2", + "pc:gdk-pixbuf-2.0", + "pc:gio-2.0>=2.26.0", + "pc:glib-2.0>=2.26.0", + "pkgconfig" + ], + "provides": [ + "pc:libnotify=0.7.7" + ] + }, + "py-vobject": { + "versions": { + "0.9.6.1-r0": 1545165276 + }, + "origin": "py-vobject", + "provides": [ + "cmd:change_tz", + "cmd:ics_diff" + ] + }, + "tslib": { + "versions": { + "1.18-r0": 1545859355 + }, + "origin": "tslib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libts.so.0=0.10.0", + "cmd:ts_calibrate", + "cmd:ts_conf", + "cmd:ts_finddev", + "cmd:ts_harvest", + "cmd:ts_print", + "cmd:ts_print_mt", + "cmd:ts_print_raw", + "cmd:ts_test", + "cmd:ts_test_mt", + "cmd:ts_uinput", + "cmd:ts_verify" + ] + }, + "kamailio-redis": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.14", + "so:libsrdb1.so.1" + ] + }, + "perl-mime-lite": { + "versions": { + "3.030-r1": 1543226640 + }, + "origin": "perl-mime-lite", + "dependencies": [ + "perl", + "perl-mime-types", + "perl-email-date-format", + "perl-mailtools", + "perl-test-pod-coverage" + ] + }, + "udisks2-libs": { + "versions": { + "2.6.5-r1": 1545293059 + }, + "origin": "udisks2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libudisks2.so.0=0.0.0" + ] + }, + "nagios-plugins-file_age": { + "versions": { + "2.2.1-r6": 1543933904 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "perl" + ] + }, + "putty": { + "versions": { + "0.71-r1": 1554726207 + }, + "origin": "putty", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:plink", + "cmd:pscp", + "cmd:psftp", + "cmd:puttygen" + ] + }, + "graphicsmagick-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "xauth": { + "versions": { + "1.0.10-r1": 1542973224 + }, + "origin": "xauth", + "dependencies": [ + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXext.so.6", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xauth" + ] + }, + "py2-gnutls": { + "versions": { + "3.1.2-r0": 1545062790 + }, + "origin": "py-gnutls" + }, + "confuse-dev": { + "versions": { + "3.2.2-r0": 1545922117 + }, + "origin": "confuse", + "dependencies": [ + "confuse=3.2.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libconfuse=3.2.2" + ] + }, + "weechat-perl": { + "versions": { + "2.3-r0": 1545299933 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "nginx-mod-stream": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "mesa-glapi": { + "versions": { + "18.1.7-r2": 1554455971 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libglapi.so.0=0.0.0" + ] + }, + "freeglut-doc": { + "versions": { + "3.0.0-r0": 1545291114 + }, + "origin": "freeglut" + }, + "btrfs-progs-bash-completion": { + "versions": { + "4.19.1-r0": 1545665047 + }, + "origin": "btrfs-progs" + }, + "oniguruma-dev": { + "versions": { + "6.9.1-r0": 1545858473 + }, + "origin": "oniguruma", + "dependencies": [ + "oniguruma=6.9.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:oniguruma=6.9.1", + "cmd:onig-config" + ] + }, + "zeromq-doc": { + "versions": { + "4.3.1-r0": 1548271359 + }, + "origin": "zeromq" + }, + "unionfs-fuse": { + "versions": { + "1.0-r0": 1545163920 + }, + "origin": "unionfs-fuse", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2" + ], + "provides": [ + "cmd:mount.unionfs", + "cmd:unionfs", + "cmd:unionfsctl" + ] + }, + "enchant-doc": { + "versions": { + "1.6.0-r13": 1542965845 + }, + "origin": "enchant" + }, + "harfbuzz-icu": { + "versions": { + "2.2.0-r0": 1545249742 + }, + "origin": "harfbuzz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libharfbuzz.so.0", + "so:libicuuc.so.62" + ], + "provides": [ + "so:libharfbuzz-icu.so.0=0.20200.0" + ] + }, + "ppp-pppoe": { + "versions": { + "2.4.7-r6": 1543999021 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pppoe-discovery" + ] + }, + "perl-http-body-doc": { + "versions": { + "1.17-r0": 1544001350 + }, + "origin": "perl-http-body" + }, + "bitlbee-openrc": { + "versions": { + "3.5.1-r4": 1543248420 + }, + "origin": "bitlbee" + }, + "orc-compiler": { + "versions": { + "0.4.28-r0": 1542883449 + }, + "origin": "orc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liborc-0.4.so.0" + ], + "provides": [ + "cmd:orcc" + ] + }, + "serf": { + "versions": { + "1.3.9-r5": 1545068687 + }, + "origin": "serf", + "dependencies": [ + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libserf-1.so.1=1.3.0" + ] + }, + "lua-stringy": { + "versions": { + "0.5.0-r1": 1545116590 + }, + "origin": "lua-stringy" + }, + "lua5.2-lyaml": { + "versions": { + "6.2-r3": 1545076421 + }, + "origin": "lua-lyaml", + "dependencies": [ + "lua5.2", + "lua5.2-stdlib-normalize", + "so:libc.musl-x86_64.so.1", + "so:libyaml-0.so.2" + ] + }, + "py-dbus-doc": { + "versions": { + "1.2.8-r1": 1543925185 + }, + "origin": "py-dbus" + }, + "libxxf86vm": { + "versions": { + "1.1.4-r2": 1542900278 + }, + "origin": "libxxf86vm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXxf86vm.so.1=1.0.0" + ] + }, + "qemu-s390x": { + "versions": { + "3.1.0-r3": 1551107302 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-s390x" + ] + }, + "libressl-dev": { + "versions": { + "2.7.5-r0": 1551116832 + }, + "origin": "libressl", + "dependencies": [ + "libressl2.7-libcrypto=2.7.5-r0", + "libressl2.7-libssl=2.7.5-r0", + "libressl2.7-libtls=2.7.5-r0", + "pkgconfig" + ], + "provides": [ + "pc:libressl:libcrypto=2.7.5", + "pc:libressl:libssl=2.7.5", + "pc:libressl:libtls=2.7.5", + "pc:libressl:openssl=2.7.5" + ] + }, + "ssh-getkey-ldap": { + "versions": { + "0.1.2-r0": 1544792481 + }, + "origin": "ssh-getkey-ldap", + "dependencies": [ + "lua", + "lua-ldap", + "/bin/sh" + ], + "provides": [ + "cmd:ssh-getkey-ldap" + ] + }, + "py-pep8": { + "versions": { + "1.7.1-r0": 1542902308 + }, + "origin": "py-pep8", + "dependencies": [ + "py3-pep8=1.7.1-r0" + ] + }, + "re2c-doc": { + "versions": { + "1.1.1-r0": 1542822316 + }, + "origin": "re2c" + }, + "libnfnetlink-dev": { + "versions": { + "1.0.1-r1": 1543924449 + }, + "origin": "libnfnetlink", + "dependencies": [ + "libnfnetlink=1.0.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:libnfnetlink=1.0.1" + ] + }, + "confuse": { + "versions": { + "3.2.2-r0": 1545922117 + }, + "origin": "confuse", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libconfuse.so.2=2.0.0" + ] + }, + "qemu-alpha": { + "versions": { + "3.1.0-r3": 1551107299 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-alpha" + ] + }, + "gparted": { + "versions": { + "0.33.0-r0": 1545746178 + }, + "origin": "gparted", + "dependencies": [ + "e2fsprogs", + "ntfs-3g-progs", + "so:libatkmm-1.6.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairomm-1.0.so.1", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdkmm-2.4.so.1", + "so:libglib-2.0.so.0", + "so:libglibmm-2.4.so.1", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libgtkmm-2.4.so.1", + "so:libintl.so.8", + "so:libpangomm-1.4.so.1", + "so:libparted-fs-resize.so.0", + "so:libparted.so.2", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:gparted", + "cmd:gpartedbin" + ] + }, + "perl-crypt-ssleay-doc": { + "versions": { + "0.72-r9": 1545062782 + }, + "origin": "perl-crypt-ssleay" + }, + "perl-devel-globaldestruction-doc": { + "versions": { + "0.14-r0": 1545162995 + }, + "origin": "perl-devel-globaldestruction" + }, + "nagios-plugins-load": { + "versions": { + "2.2.1-r6": 1543933907 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "snmptt-openrc": { + "versions": { + "1.4-r0": 1542924637 + }, + "origin": "snmptt", + "dependencies": [ + "perl", + "perl-config-inifiles", + "perl-list-moreutils" + ] + }, + "apr-util-dbd_sqlite3": { + "versions": { + "1.6.1-r5": 1545061010 + }, + "origin": "apr-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "perl-test-simple": { + "versions": { + "1.302141-r0": 1544792380 + }, + "origin": "perl-test-simple", + "provides": [ + "perl-test-tester" + ] + }, + "perl-digest-md5": { + "versions": { + "2.55-r2": 1544001430 + }, + "origin": "perl-digest-md5", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "orc-dev": { + "versions": { + "0.4.28-r0": 1542883449 + }, + "origin": "orc", + "dependencies": [ + "orc=0.4.28-r0", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:liborc-0.4.so.0", + "so:liborc-test-0.4.so.0" + ], + "provides": [ + "pc:orc-0.4=0.4.28", + "cmd:orc-bugreport" + ] + }, + "libattr": { + "versions": { + "2.4.47-r7": 1542301506 + }, + "origin": "attr", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libattr.so.1=1.1.0" + ] + }, + "libvdpau-dev": { + "versions": { + "1.1.1-r2": 1542900270 + }, + "origin": "libvdpau", + "dependencies": [ + "libvdpau=1.1.1-r2", + "pc:x11", + "pkgconfig" + ], + "provides": [ + "pc:vdpau=1.1.1" + ] + }, + "gnokii-lang": { + "versions": { + "0.6.31-r8": 1545300480 + }, + "origin": "gnokii" + }, + "ghostscript-doc": { + "versions": { + "9.26-r2": 1554362637 + }, + "origin": "ghostscript" + }, + "openldap-overlay-dynlist": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "net-snmp-dev": { + "versions": { + "5.8-r0": 1543923349 + }, + "origin": "net-snmp", + "dependencies": [ + "openssl-dev", + "net-snmp-agent-libs=5.8-r0", + "net-snmp-libs=5.8-r0" + ], + "provides": [ + "cmd:net-snmp-config" + ] + }, + "mksh-doc": { + "versions": { + "56c-r0": 1543925653 + }, + "origin": "mksh" + }, + "dahdi-linux-vanilla-dev": { + "versions": { + "4.19.41-r0": 1557400160 + }, + "origin": "dahdi-linux-vanilla", + "dependencies": [ + "dahdi-linux-dev" + ] + }, + "intltool": { + "versions": { + "0.51.0-r4": 1542821919 + }, + "origin": "intltool", + "dependencies": [ + "perl", + "perl-xml-parser", + "gettext", + "file" + ], + "provides": [ + "cmd:intltool-extract", + "cmd:intltool-merge", + "cmd:intltool-prepare", + "cmd:intltool-update", + "cmd:intltoolize" + ] + }, + "ircii-doc": { + "versions": { + "20111115-r4": 1545213657 + }, + "origin": "ircii" + }, + "py-jwt": { + "versions": { + "1.6.4-r0": 1543249954 + }, + "origin": "py-jwt" + }, + "lua-xctrl": { + "versions": { + "2015.04.10-r2": 1545208665 + }, + "origin": "lua-xctrl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-sys-hostname-long": { + "versions": { + "1.5-r0": 1545116817 + }, + "origin": "perl-sys-hostname-long", + "dependencies": [ + "perl" + ] + }, + "bind": { + "versions": { + "9.12.4_p1-r1": 1556801608 + }, + "origin": "bind", + "dependencies": [ + "/bin/sh", + "so:libbind9.so.1201", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.1.1", + "so:libdns.so.1208", + "so:libisc.so.1204", + "so:libisccc.so.1201", + "so:libisccfg.so.1203", + "so:libjson-c.so.4", + "so:libldap-2.4.so.2", + "so:libns.so.1207", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:arpaname", + "cmd:ddns-confgen", + "cmd:genrandom", + "cmd:isc-config.sh", + "cmd:mdig", + "cmd:named", + "cmd:named-checkconf", + "cmd:named-checkzone", + "cmd:named-compilezone", + "cmd:named-journalprint", + "cmd:named-rrchecker", + "cmd:nsec3hash", + "cmd:rndc", + "cmd:rndc-confgen", + "cmd:tsig-keygen" + ] + }, + "lxc-pam": { + "versions": { + "3.1.0-r1": 1549966161 + }, + "origin": "lxc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0" + ] + }, + "dmidecode-doc": { + "versions": { + "3.2-r0": 1543921858 + }, + "origin": "dmidecode" + }, + "qextserialport": { + "versions": { + "1.2_rc1-r0": 1545300097 + }, + "origin": "qextserialport", + "dependencies": [ + "so:libQtCore.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libqextserialport.so.1=1.2.0" + ] + }, + "openldap-overlay-constraint": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "py-gtk-dev": { + "versions": { + "2.24.0-r15": 1543927627 + }, + "origin": "py-gtk", + "dependencies": [ + "py-gobject-dev", + "py-gtk", + "pc:gtk+-2.0", + "pc:pygobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:pygtk-2.0=2.24.0" + ] + }, + "perl-net-async-http-doc": { + "versions": { + "0.42-r0": 1545209618 + }, + "origin": "perl-net-async-http" + }, + "mii-tool": { + "versions": { + "1.60_git20140218-r2": 1545216352 + }, + "origin": "net-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mii-tool" + ] + }, + "flex-doc": { + "versions": { + "2.6.4-r1": 1542300944 + }, + "origin": "flex" + }, + "jbig2dec-doc": { + "versions": { + "0.15-r0": 1545207929 + }, + "origin": "jbig2dec" + }, + "pngcrush": { + "versions": { + "1.8.13-r0": 1545209638 + }, + "origin": "pngcrush", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpng16.so.16", + "so:libz.so.1" + ], + "provides": [ + "cmd:pngcrush" + ] + }, + "cryptsetup1": { + "versions": { + "1.7.5-r4": 1545292766 + }, + "origin": "cryptsetup1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcryptsetup.so.4", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:cryptsetup", + "cmd:veritysetup" + ] + }, + "dnsmasq-dnssec": { + "versions": { + "2.80-r3": 1545223253 + }, + "origin": "dnsmasq", + "dependencies": [ + "!dnsmasq", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libhogweed.so.4", + "so:libnettle.so.6" + ], + "provides": [ + "cmd:dnsmasq" + ] + }, + "openbox-doc": { + "versions": { + "3.6.1-r2": 1545207327 + }, + "origin": "openbox" + }, + "openjade-dev": { + "versions": { + "1.3.2-r5": 1542893150 + }, + "origin": "openjade", + "dependencies": [ + "openjade-libs=1.3.2-r5" + ] + }, + "gawk": { + "versions": { + "4.2.1-r0": 1542301294 + }, + "origin": "gawk", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:awk", + "cmd:gawk", + "cmd:gawk-4.2.1" + ] + }, + "ntfs-3g-dev": { + "versions": { + "2017.3.23-r1": 1542973170 + }, + "origin": "ntfs-3g", + "dependencies": [ + "ntfs-3g-libs=2017.3.23-r1", + "pkgconfig" + ], + "provides": [ + "pc:libntfs-3g=2017.3.23" + ] + }, + "sfic": { + "versions": { + "0.1.7-r6": 1545208619 + }, + "origin": "sfic", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtdb.so.1" + ], + "provides": [ + "cmd:sfic" + ] + }, + "awstats-doc": { + "versions": { + "7.7-r0": 1545069107 + }, + "origin": "awstats" + }, + "hiredis": { + "versions": { + "0.14.0-r0": 1545837197 + }, + "origin": "hiredis", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhiredis.so.0.14=0.14" + ] + }, + "syslinux-dev": { + "versions": { + "6.04_pre1-r2": 1543935715 + }, + "origin": "syslinux" + }, + "dahdi-linux-vanilla": { + "versions": { + "4.19.41-r0": 1557400160 + }, + "origin": "dahdi-linux-vanilla", + "dependencies": [ + "dahdi-linux", + "linux-vanilla=4.19.41-r0" + ] + }, + "qemu-ui-curses": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libncursesw.so.6" + ] + }, + "libgee-dev": { + "versions": { + "0.20.1-r0": 1545060547 + }, + "origin": "libgee", + "dependencies": [ + "libgee=0.20.1-r0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gee-0.8=0.20.1" + ] + }, + "tumbler-lang": { + "versions": { + "0.2.3-r0": 1545859329 + }, + "origin": "tumbler" + }, + "lua5.3-posixtz": { + "versions": { + "0.5-r1": 1543928315 + }, + "origin": "lua-posixtz", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "pangox-compat-dev": { + "versions": { + "0.0.2-r0": 1544001258 + }, + "origin": "pangox-compat", + "dependencies": [ + "pango-dev", + "libice-dev", + "pangox-compat=0.0.2-r0", + "pc:pango", + "pkgconfig" + ], + "provides": [ + "pc:pangox=1.30" + ] + }, + "libmicrohttpd-doc": { + "versions": { + "0.9.62-r0": 1545300153 + }, + "origin": "libmicrohttpd" + }, + "perl-mail-imapclient": { + "versions": { + "3.39-r0": 1545075911 + }, + "origin": "perl-mail-imapclient", + "dependencies": [ + "perl-parse-recdescent" + ] + }, + "ipsec-tools-dev": { + "versions": { + "0.8.2-r8": 1543246797 + }, + "origin": "ipsec-tools" + }, + "kamailio-dbtext": { + "versions": { + "5.2.0-r1": 1546423169 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "kamailio-db", + "so:libc.musl-x86_64.so.1", + "so:libsrdb1.so.1" + ] + }, + "libpcre2-32": { + "versions": { + "10.32-r1": 1545069083 + }, + "origin": "pcre2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre2-32.so.0=0.7.1" + ] + }, + "bluez-firmware": { + "versions": { + "1.2-r0": 1543998829 + }, + "origin": "bluez-firmware" + }, + "acf-postfix": { + "versions": { + "0.10.0-r2": 1544798745 + }, + "origin": "acf-postfix", + "dependencies": [ + "acf-core", + "postfix" + ] + }, + "uwsgi-alarm_curl": { + "versions": { + "2.0.17.1-r0": 1545062196 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "acpi-doc": { + "versions": { + "1.7-r2": 1545300419 + }, + "origin": "acpi" + }, + "libtasn1": { + "versions": { + "4.13-r0": 1542824446 + }, + "origin": "libtasn1", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtasn1.so.6=6.5.5", + "cmd:asn1Coding", + "cmd:asn1Decoding", + "cmd:asn1Parser" + ] + }, + "ansible-doc": { + "versions": { + "2.7.0-r1": 1545223230 + }, + "origin": "ansible" + }, + "freeswitch-sangoma": { + "versions": { + "1.8.2-r1": 1545117892 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeswitch.so.1", + "so:libsngtc_node.so" + ] + }, + "acf-kamailio": { + "versions": { + "0.10.0-r2": 1543924389 + }, + "origin": "acf-kamailio", + "dependencies": [ + "acf-core", + "kamailio", + "acf-db-lib" + ] + }, + "xl2tpd": { + "versions": { + "1.3.10.1-r0": 1545300583 + }, + "origin": "xl2tpd", + "dependencies": [ + "ppp-l2tp", + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:pfc", + "cmd:xl2tpd", + "cmd:xl2tpd-control" + ] + }, + "lua5.3-graphviz": { + "versions": { + "2.40.1-r1": 1543926741 + }, + "origin": "graphviz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcgraph.so.6", + "so:libgvc.so.6", + "so:liblua-5.3.so.0" + ], + "provides": [ + "lua-graphviz=2.40.1-r1" + ] + }, + "openldap-overlay-seqmod": { + "versions": { + "2.4.47-r2": 1546016480 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldap_r-2.4.so.2" + ] + }, + "perl-config-grammar": { + "versions": { + "1.12-r0": 1543999452 + }, + "origin": "perl-config-grammar" + }, + "tumbler-dev": { + "versions": { + "0.2.3-r0": 1545859329 + }, + "origin": "tumbler", + "dependencies": [ + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pkgconfig", + "tumbler=0.2.3-r0" + ], + "provides": [ + "pc:tumbler-1=0.2.3" + ] + }, + "perl-locale-maketext-lexicon-doc": { + "versions": { + "1.00-r0": 1543249751 + }, + "origin": "perl-locale-maketext-lexicon" + }, + "xscreensaver": { + "versions": { + "5.40-r0": 1545224089 + }, + "origin": "xscreensaver", + "dependencies": [ + "bc", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXinerama.so.1", + "so:libXmu.so.6", + "so:libXrandr.so.2", + "so:libXt.so.6", + "so:libXxf86vm.so.1", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgdk_pixbuf_xlib-2.0.so.0", + "so:libglade-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:xscreensaver", + "cmd:xscreensaver-command", + "cmd:xscreensaver-demo", + "cmd:xscreensaver-getimage", + "cmd:xscreensaver-getimage-file", + "cmd:xscreensaver-getimage-video", + "cmd:xscreensaver-text" + ] + }, + "pcsc-lite-dev": { + "versions": { + "1.8.24-r1": 1545060761 + }, + "origin": "pcsc-lite", + "dependencies": [ + "eudev-dev", + "pcsc-lite-libs=1.8.24-r1", + "pkgconfig" + ], + "provides": [ + "pc:libpcsclite=1.8.24" + ] + }, + "libsndfile-dev": { + "versions": { + "1.0.28-r8": 1555066604 + }, + "origin": "libsndfile", + "dependencies": [ + "flac-dev", + "libvorbis-dev", + "libogg-dev", + "libsndfile=1.0.28-r8", + "pkgconfig" + ], + "provides": [ + "pc:sndfile=1.0.28" + ] + }, + "librsvg-dev": { + "versions": { + "2.40.20-r0": 1543926594 + }, + "origin": "librsvg", + "dependencies": [ + "gtk+2.0-dev", + "libcroco-dev", + "libgsf-dev", + "librsvg=2.40.20-r0", + "pc:cairo", + "pc:gdk-pixbuf-2.0", + "pc:gio-2.0", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:librsvg-2.0=2.40.20" + ] + }, + "acf-lvm2": { + "versions": { + "0.7.0-r2": 1543999366 + }, + "origin": "acf-lvm2", + "dependencies": [ + "acf-core", + "lvm2" + ] + }, + "vlan": { + "versions": { + "2.2-r0": 1545062235 + }, + "origin": "vlan" + }, + "squid-lang-pl": { + "versions": { + "4.4-r1": 1545216328 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "gnome-vfs-doc": { + "versions": { + "2.24.4-r6": 1545299690 + }, + "origin": "gnome-vfs" + }, + "perl-html-mason": { + "versions": { + "1.58-r0": 1545229451 + }, + "origin": "perl-html-mason", + "dependencies": [ + "perl-cgi", + "perl-cache-cache", + "perl-log-any", + "perl-html-parser", + "perl-class-container", + "perl-params-validate", + "perl-exception-class" + ], + "provides": [ + "cmd:convert0.6.README", + "cmd:convert0.6.pl", + "cmd:convert0.8.README", + "cmd:convert0.8.pl", + "cmd:mason.pl" + ] + }, + "alpine-sdk": { + "versions": { + "1.0-r0": 1545214216 + }, + "origin": "alpine-sdk", + "dependencies": [ + "abuild", + "build-base", + "git" + ] + }, + "perl-html-formattext-withlinks": { + "versions": { + "0.15-r0": 1545075967 + }, + "origin": "perl-html-formattext-withlinks", + "dependencies": [ + "perl-html-format", + "perl-uri", + "perl-html-tree" + ] + }, + "py-sphinxcontrib-websupport": { + "versions": { + "1.0.1-r3": 1542825040 + }, + "origin": "py-sphinxcontrib-websupport" + }, + "xf86-input-evdev": { + "versions": { + "2.10.6-r0": 1545229729 + }, + "origin": "xf86-input-evdev", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libevdev.so.2", + "so:libmtdev.so.1", + "so:libudev.so.1" + ] + }, + "libcdio-paranoia": { + "versions": { + "0.94_p1-r1": 1543248404 + }, + "origin": "libcdio-paranoia", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcdio.so.16" + ], + "provides": [ + "so:libcdio_cdda.so.2=2.0.0", + "so:libcdio_paranoia.so.2=2.0.0", + "cmd:cd-paranoia" + ] + }, + "xen": { + "versions": { + "4.11.1-r1": 1545075897 + }, + "origin": "xen", + "dependencies": [ + "bash", + "iproute2", + "logrotate", + "syslinux", + "so:libaio.so.1", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libfsimage.so.1.0", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libpci.so.3", + "so:libpixman-1.so.0", + "so:libpython2.7.so.1.0", + "so:libspice-server.so.1", + "so:libssh2.so.1", + "so:libxenctrl.so.4.11", + "so:libxendevicemodel.so.1", + "so:libxenevtchn.so.1", + "so:libxenforeignmemory.so.1", + "so:libxengnttab.so.1", + "so:libxenguest.so.4.11", + "so:libxenlight.so.4.11", + "so:libxenstat.so.0", + "so:libxenstore.so.3.0", + "so:libxentoollog.so.1", + "so:libxlutil.so.4.11", + "so:libyajl.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:flask-get-bool", + "cmd:flask-getenforce", + "cmd:flask-label-pci", + "cmd:flask-loadpolicy", + "cmd:flask-set-bool", + "cmd:flask-setenforce", + "cmd:gdbsx", + "cmd:kdd", + "cmd:qemu-img-xen", + "cmd:qemu-nbd-xen", + "cmd:xen-bugtool", + "cmd:xen-cpuid", + "cmd:xen-detect", + "cmd:xen-diag", + "cmd:xen-hptool", + "cmd:xen-hvmcrash", + "cmd:xen-hvmctx", + "cmd:xen-livepatch", + "cmd:xen-lowmemd", + "cmd:xen-mfndump", + "cmd:xen-ringwatch", + "cmd:xen-tmem-list-parse", + "cmd:xenalyze", + "cmd:xenbaked", + "cmd:xencons", + "cmd:xenconsoled", + "cmd:xencov", + "cmd:xencov_split", + "cmd:xenlockprof", + "cmd:xenmon.py", + "cmd:xenperf", + "cmd:xenpm", + "cmd:xenpmd", + "cmd:xenstore", + "cmd:xenstore-chmod", + "cmd:xenstore-control", + "cmd:xenstore-exists", + "cmd:xenstore-list", + "cmd:xenstore-ls", + "cmd:xenstore-read", + "cmd:xenstore-rm", + "cmd:xenstore-watch", + "cmd:xenstore-write", + "cmd:xenstored", + "cmd:xentop", + "cmd:xentrace", + "cmd:xentrace_format", + "cmd:xentrace_setmask", + "cmd:xentrace_setsize", + "cmd:xenwatchdogd", + "cmd:xl" + ] + }, + "py-gobject3": { + "versions": { + "3.28.2-r0": 1543925246 + }, + "origin": "py-gobject3" + }, + "serf-dev": { + "versions": { + "1.3.9-r5": 1545068687 + }, + "origin": "serf", + "dependencies": [ + "pc:libcrypto", + "pc:libssl", + "pkgconfig", + "serf=1.3.9-r5" + ], + "provides": [ + "pc:serf-1=1.3.9" + ] + }, + "py-attrs": { + "versions": { + "18.2.0-r0": 1542824881 + }, + "origin": "py-attrs" + }, + "py-rsa": { + "versions": { + "3.4.2-r2": 1545116598 + }, + "origin": "py-rsa", + "dependencies": [ + "py-asn1" + ] + }, + "twm": { + "versions": { + "1.0.10-r0": 1545209633 + }, + "origin": "twm", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXmu.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:twm" + ] + }, + "lua-penlight": { + "versions": { + "1.5.4-r2": 1543223291 + }, + "origin": "lua-penlight", + "dependencies": [ + "lua", + "lua-filesystem" + ], + "provides": [ + "lua-penlight-shared=1.5.4-r2", + "lua5.1-penlight=1.5.4-r2", + "lua5.2-penlight=1.5.4-r2", + "lua5.3-penlight=1.5.4-r2" + ] + }, + "libechonest-dev": { + "versions": { + "2.3.1-r0": 1545209019 + }, + "origin": "libechonest", + "dependencies": [ + "qjson-dev", + "qt-dev", + "libechonest=2.3.1-r0", + "pc:QJson", + "pc:QtCore", + "pc:QtNetwork", + "pkgconfig" + ], + "provides": [ + "pc:libechonest=2.3.1" + ] + }, + "postgresql-plperl-contrib": { + "versions": { + "11.2-r0": 1554274176 + }, + "origin": "postgresql", + "dependencies": [ + "postgresql-plperl", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "acf-openssl": { + "versions": { + "0.10.1-r3": 1543254043 + }, + "origin": "acf-openssl", + "dependencies": [ + "acf-core", + "libressl" + ] + }, + "iptables": { + "versions": { + "1.6.2-r1": 1545062497 + }, + "origin": "iptables", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnftnl.so.7" + ], + "provides": [ + "so:libip4tc.so.0=0.1.0", + "so:libip6tc.so.0=0.1.0", + "so:libipq.so.0=0.0.0", + "so:libiptc.so.0=0.0.0", + "so:libxtables.so.12=12.0.0", + "cmd:arptables-compat", + "cmd:ebtables-compat", + "cmd:iptables", + "cmd:iptables-compat", + "cmd:iptables-compat-restore", + "cmd:iptables-compat-save", + "cmd:iptables-restore", + "cmd:iptables-restore-translate", + "cmd:iptables-save", + "cmd:iptables-translate", + "cmd:xtables-compat-multi", + "cmd:xtables-multi" + ] + }, + "libxv": { + "versions": { + "1.0.11-r2": 1542900291 + }, + "origin": "libxv", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXv.so.1=1.0.0" + ] + }, + "dhcpcd-doc": { + "versions": { + "7.0.8-r0": 1545207907 + }, + "origin": "dhcpcd" + }, + "sdl-doc": { + "versions": { + "1.2.15-r9": 1545292836 + }, + "origin": "sdl" + }, + "libressl2.7-libcrypto": { + "versions": { + "2.7.5-r0": 1551116832 + }, + "origin": "libressl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcrypto.so.43=43.0.1" + ] + }, + "perl-role-basic": { + "versions": { + "0.13-r0": 1543925830 + }, + "origin": "perl-role-basic" + }, + "freeradius-krb5": { + "versions": { + "3.0.17-r5": 1556201989 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libkrb5.so.3" + ], + "provides": [ + "freeradius3-krb5=3.0.17-r5", + "so:rlm_krb5.so=0" + ] + }, + "acf-jquery": { + "versions": { + "0.4.2-r1": 1542883772 + }, + "origin": "acf-jquery" + }, + "encodings": { + "versions": { + "1.0.4-r1": 1542924695 + }, + "origin": "encodings" + }, + "py-flask-wtf": { + "versions": { + "0.14.2-r0": 1545213683 + }, + "origin": "py-flask-wtf", + "dependencies": [ + "py-flask", + "py-wtforms" + ] + }, + "sqsh": { + "versions": { + "2.5.16.1-r2": 1545300948 + }, + "origin": "sqsh", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libct.so.4", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:sqsh" + ] + }, + "perl-path-tiny-doc": { + "versions": { + "0.108-r0": 1543238926 + }, + "origin": "perl-path-tiny" + }, + "py3-nose": { + "versions": { + "1.3.7-r2": 1544797226 + }, + "origin": "py-nose", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:nosetests", + "cmd:nosetests-3.6" + ] + }, + "gstreamer": { + "versions": { + "1.14.4-r0": 1543254160 + }, + "origin": "gstreamer", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libgstbase-1.0.so.0=0.1404.0", + "so:libgstcheck-1.0.so.0=0.1404.0", + "so:libgstcontroller-1.0.so.0=0.1404.0", + "so:libgstnet-1.0.so.0=0.1404.0", + "so:libgstreamer-1.0.so.0=0.1404.0" + ] + }, + "bzip2-doc": { + "versions": { + "1.0.6-r6": 1542300119 + }, + "origin": "bzip2" + }, + "git-doc": { + "versions": { + "2.20.1-r0": 1545214194 + }, + "origin": "git" + }, + "gdl": { + "versions": { + "3.28.0-r0": 1545215938 + }, + "origin": "gdl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgdl-3.so.5=5.0.9" + ] + }, + "loudmouth-doc": { + "versions": { + "1.5.3-r1": 1543226581 + }, + "origin": "loudmouth" + }, + "apache2-webdav": { + "versions": { + "2.4.39-r0": 1554306836 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-firmware-3com": { + "versions": { + "20190322-r0": 1554980655 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "openldap-overlay-all": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "openldap-overlay-accesslog", + "openldap-overlay-auditlog", + "openldap-overlay-collect", + "openldap-overlay-constraint", + "openldap-overlay-dds", + "openldap-overlay-deref", + "openldap-overlay-dyngroup", + "openldap-overlay-dynlist", + "openldap-overlay-memberof", + "openldap-overlay-ppolicy", + "openldap-overlay-proxycache", + "openldap-overlay-refint", + "openldap-overlay-retcode", + "openldap-overlay-rwm", + "openldap-overlay-seqmod", + "openldap-overlay-sssvlv", + "openldap-overlay-syncprov", + "openldap-overlay-translucent", + "openldap-overlay-unique", + "openldap-overlay-valsort" + ] + }, + "libtls-standalone": { + "versions": { + "2.7.4-r6": 1546784623 + }, + "origin": "libtls-standalone", + "dependencies": [ + "ca-certificates-cacert", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libtls-standalone.so.1=1.0.0" + ] + }, + "py-roman": { + "versions": { + "2.0.0-r3": 1543998690 + }, + "origin": "py-roman" + }, + "py-pathlib2": { + "versions": { + "2.3.2-r0": 1542824912 + }, + "origin": "py-pathlib2", + "dependencies": [ + "py-six", + "py-scandir" + ] + }, + "gtkspell-dev": { + "versions": { + "2.0.16-r7": 1545215214 + }, + "origin": "gtkspell", + "dependencies": [ + "gtkspell=2.0.16-r7", + "pc:gtk+-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gtkspell-2.0=2.0.16" + ] + }, + "mt-st-doc": { + "versions": { + "1.1-r4": 1543223638 + }, + "origin": "mt-st" + }, + "nagios-plugins-mrtgtraf": { + "versions": { + "2.2.1-r6": 1543933908 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "tpaste": { + "versions": { + "0.6-r0": 1545076745 + }, + "origin": "sprunge", + "dependencies": [ + "curl" + ], + "provides": [ + "cmd:tpaste" + ] + }, + "perl-date-calc-doc": { + "versions": { + "6.4-r1": 1545060705 + }, + "origin": "perl-date-calc" + }, + "linux-firmware-mwl8k": { + "versions": { + "20190322-r0": 1554980650 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "perl-cache-simple-timedexpiry": { + "versions": { + "0.27-r1": 1544798626 + }, + "origin": "perl-cache-simple-timedexpiry", + "dependencies": [ + "perl" + ] + }, + "blkid": { + "versions": { + "2.33-r0": 1545307437 + }, + "origin": "util-linux", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:blkid" + ] + }, + "htop": { + "versions": { + "2.2.0-r0": 1545207288 + }, + "origin": "htop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:htop" + ] + }, + "docbook2x": { + "versions": { + "0.8.8-r6": 1542893185 + }, + "origin": "docbook2x", + "dependencies": [ + "texinfo", + "openjade", + "docbook-xml", + "docbook-xsl", + "perl-xml-sax", + "libxslt", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:db2x_manxml", + "cmd:db2x_texixml", + "cmd:db2x_xsltproc", + "cmd:docbook2x-man", + "cmd:docbook2x-texi", + "cmd:sgml2xml-isoent", + "cmd:utf8trans" + ] + }, + "argon2-dev": { + "versions": { + "20171227-r1": 1543928961 + }, + "origin": "argon2", + "dependencies": [ + "argon2-libs=20171227-r1" + ] + }, + "qemu-sh4": { + "versions": { + "3.1.0-r3": 1551107302 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sh4" + ] + }, + "lm_sensors-doc": { + "versions": { + "3.4.0-r6": 1542924817 + }, + "origin": "lm_sensors" + }, + "py2-libxml2": { + "versions": { + "2.9.9-r1": 1551105514 + }, + "origin": "libxml2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libxml2.so.2" + ] + }, + "perl-net-libidn": { + "versions": { + "0.12-r5": 1542924924 + }, + "origin": "perl-net-libidn", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libidn.so.12" + ] + }, + "figlet-doc": { + "versions": { + "2.2.5-r0": 1545216340 + }, + "origin": "figlet" + }, + "device-mapper-event-libs": { + "versions": { + "2.02.182-r0": 1543928934 + }, + "origin": "lvm2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "so:libdevmapper-event.so.1.02=1.02" + ] + }, + "acf-heimdal": { + "versions": { + "0.6.0-r2": 1543934553 + }, + "origin": "acf-heimdal", + "dependencies": [ + "acf-core", + "heimdal" + ] + }, + "squid-lang-ja": { + "versions": { + "4.4-r1": 1545216327 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-type-tiny": { + "versions": { + "1.000006-r0": 1542973083 + }, + "origin": "perl-type-tiny", + "dependencies": [ + "perl-exporter-tiny" + ] + }, + "lua5.1-ossl": { + "versions": { + "20180708-r2": 1545076447 + }, + "origin": "lua-ossl", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "perl-class-tiny": { + "versions": { + "1.006-r0": 1543076769 + }, + "origin": "perl-class-tiny" + }, + "pango-doc": { + "versions": { + "1.42.4-r0": 1542824069 + }, + "origin": "pango" + }, + "perl-control-x10": { + "versions": { + "2.09-r1": 1545253948 + }, + "origin": "perl-control-x10", + "dependencies": [ + "perl" + ] + }, + "perl-compress-raw-bzip2": { + "versions": { + "2.084-r0": 1546944093 + }, + "origin": "perl-compress-raw-bzip2", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "freeswitch-perl": { + "versions": { + "1.8.2-r1": 1545117893 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeswitch.so.1", + "so:libgcc_s.so.1", + "so:libperl.so", + "so:libstdc++.so.6" + ] + }, + "perl-file-sharedir": { + "versions": { + "1.116-r0": 1543238884 + }, + "origin": "perl-file-sharedir", + "dependencies": [ + "perl", + "perl-class-inspector" + ] + }, + "libnetfilter_log-dev": { + "versions": { + "1.0.1-r2": 1543924455 + }, + "origin": "libnetfilter_log", + "dependencies": [ + "linux-headers", + "libnetfilter_log=1.0.1-r2", + "pc:libnfnetlink", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_log=1.0.1" + ] + }, + "lua5.2-dbi-mysql": { + "versions": { + "0.6-r3": 1545214221 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "startup-notification-dev": { + "versions": { + "0.12-r3": 1543253999 + }, + "origin": "startup-notification", + "dependencies": [ + "libsm-dev", + "xcb-util-dev", + "pkgconfig", + "startup-notification=0.12-r3" + ], + "provides": [ + "pc:libstartup-notification-1.0=0.12" + ] + }, + "freeradius-lib": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpcap.so.1", + "so:libssl.so.1.1", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libfreeradius-dhcp.so=0", + "so:libfreeradius-eap.so=0", + "so:libfreeradius-radius.so=0", + "so:libfreeradius-server.so=0" + ] + }, + "uwsgi-router_cache": { + "versions": { + "2.0.17.1-r0": 1545062206 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "msmtp-lang": { + "versions": { + "1.8.1-r1": 1545062528 + }, + "origin": "msmtp" + }, + "termrec-doc": { + "versions": { + "0.17-r1": 1545249955 + }, + "origin": "termrec" + }, + "linux-firmware-vxge": { + "versions": { + "20190322-r0": 1554980646 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "ipfw": { + "versions": { + "3.0_git20130607-r1": 1545075363 + }, + "origin": "ipfw", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ipfw" + ] + }, + "mariadb-static": { + "versions": { + "10.3.13-r1": 1557431725 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common" + ] + }, + "gross": { + "versions": { + "1.0.2-r11": 1545062673 + }, + "origin": "gross", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2" + ], + "provides": [ + "so:grosscheck.so=0", + "cmd:gclient", + "cmd:grossd" + ] + }, + "freeswitch-perlesl": { + "versions": { + "1.8.2-r1": 1545117893 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libperl.so", + "so:libstdc++.so.6" + ] + }, + "perl-data-hexdump": { + "versions": { + "0.02-r1": 1545664906 + }, + "origin": "perl-data-hexdump", + "provides": [ + "cmd:hexdump" + ] + }, + "gconf": { + "versions": { + "3.2.6-r4": 1545075211 + }, + "origin": "gconf", + "dependencies": [ + "so:libORBit-2.so.0", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libintl.so.8", + "so:libpolkit-gobject-1.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgconf-2.so.4=4.1.5", + "cmd:gconf-merge-tree", + "cmd:gconftool-2", + "cmd:gsettings-data-convert", + "cmd:gsettings-schema-convert" + ] + }, + "lua-json4": { + "versions": { + "1.0.0-r3": 1543246771 + }, + "origin": "lua-json4", + "dependencies": [ + "lua" + ], + "provides": [ + "lua5.1-json4=1.0.0-r3", + "lua5.2-json4=1.0.0-r3", + "lua5.3-json4=1.0.0-r3" + ] + }, + "libxshmfence": { + "versions": { + "1.3-r0": 1542900235 + }, + "origin": "libxshmfence", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxshmfence.so.1=1.0.0" + ] + }, + "freeradius-postgresql": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius-sql=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "freeradius3-postgresql=3.0.17-r5", + "so:rlm_sql_postgresql.so=0" + ] + }, + "gummiboot-doc": { + "versions": { + "48.1-r0": 1545073320 + }, + "origin": "gummiboot" + }, + "loudmouth": { + "versions": { + "1.5.3-r1": 1543226581 + }, + "origin": "loudmouth", + "dependencies": [ + "so:libasyncns.so.0", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libgobject-2.0.so.0", + "so:libidn.so.12", + "so:libintl.so.8" + ], + "provides": [ + "so:libloudmouth-1.so.0=0.1.0" + ] + }, + "makedepend": { + "versions": { + "1.0.5-r2": 1543220550 + }, + "origin": "makedepend", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:makedepend" + ] + }, + "lua5.3-bit32": { + "versions": { + "5.3.0-r2": 1546011988 + }, + "origin": "lua-bit32", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "freeswitch-sounds-en-us-callie-32000": { + "versions": { + "1.0.16-r1": 1545235371 + }, + "origin": "freeswitch-sounds-en-us-callie-32000" + }, + "bmd-tools": { + "versions": { + "1.0.2-r0": 1545062473 + }, + "origin": "bmd-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "cmd:bmd-extractfw", + "cmd:bmd-streamer" + ] + }, + "kamailio-memcached": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libmemcached.so.11" + ] + }, + "jwhois-doc": { + "versions": { + "4.0-r4": 1545300909 + }, + "origin": "jwhois" + }, + "libsasl": { + "versions": { + "2.1.27-r1": 1550353527 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ], + "provides": [ + "so:libsasl2.so.3=3.0.0" + ] + }, + "py3-atomicwrites": { + "versions": { + "1.1.5-r0": 1542824873 + }, + "origin": "py-atomicwrites", + "dependencies": [ + "python3" + ] + }, + "lockfile-progs": { + "versions": { + "0.1.18-r0": 1546267694 + }, + "origin": "lockfile-progs", + "dependencies": [ + "liblockfile", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lockfile-check", + "cmd:lockfile-create", + "cmd:lockfile-remove", + "cmd:lockfile-touch", + "cmd:mail-lock", + "cmd:mail-touchlock", + "cmd:mail-unlock" + ] + }, + "vanessa_socket-doc": { + "versions": { + "0.0.13-r0": 1545116979 + }, + "origin": "vanessa_socket" + }, + "perl-crypt-ssleay": { + "versions": { + "0.72-r9": 1545062783 + }, + "origin": "perl-crypt-ssleay", + "dependencies": [ + "perl-path-class", + "perl-try-tiny", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "libfastjson": { + "versions": { + "0.99.8-r1": 1545060652 + }, + "origin": "libfastjson", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfastjson.so.4=4.2.0" + ] + }, + "py3-virtualenv": { + "versions": { + "15.1.0-r0": 1545066962 + }, + "origin": "py-virtualenv", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:virtualenv" + ] + }, + "udisks2-dev": { + "versions": { + "2.6.5-r1": 1545293053 + }, + "origin": "udisks2", + "dependencies": [ + "gobject-introspection-dev", + "polkit-dev", + "libatasmart-dev", + "libgudev-dev", + "acl-dev", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig", + "udisks2-libs=2.6.5-r1" + ], + "provides": [ + "pc:udisks2=2.6.5" + ] + }, + "py-urlnorm": { + "versions": { + "1.1.2-r1": 1542924622 + }, + "origin": "py-urlnorm", + "dependencies": [ + "python2" + ] + }, + "ipsec-tools": { + "versions": { + "0.8.2-r8": 1543246797 + }, + "origin": "ipsec-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ], + "provides": [ + "cmd:plainrsa-gen", + "cmd:racoon", + "cmd:racoonctl", + "cmd:setkey" + ] + }, + "perl-test-file-sharedir-doc": { + "versions": { + "1.001002-r0": 1543238931 + }, + "origin": "perl-test-file-sharedir" + }, + "gvpe-doc": { + "versions": { + "3.1-r0": 1545214040 + }, + "origin": "gvpe" + }, + "rsyslog-mmcount": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-django-oscar": { + "versions": { + "1.6.4-r0": 1543925765 + }, + "origin": "py-django-oscar", + "dependencies": [ + "py-babel", + "py-django", + "py-django-extra-views", + "py-django-haystack", + "py-django-phonenumber-field", + "py-django-sorl-thumbnail", + "py-django-tables2", + "py-django-treebeard", + "py-django-widget-tweaks", + "py-factory-boy", + "py-mock", + "py-pillow", + "py-purl", + "py-unidecode" + ] + }, + "perl-test-exception": { + "versions": { + "0.43-r0": 1542883368 + }, + "origin": "perl-test-exception", + "dependencies": [ + "perl-sub-uplevel" + ] + }, + "syslog-ng-json": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjson-c.so.4", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "nagios-plugins-rpc": { + "versions": { + "2.2.1-r6": 1543933911 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "perl" + ] + }, + "btrfs-progs": { + "versions": { + "4.19.1-r0": 1545665054 + }, + "origin": "btrfs-progs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:liblzo2.so.2", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:btrfs", + "cmd:btrfsck", + "cmd:fsck.btrfs", + "cmd:mkfs.btrfs" + ] + }, + "rhash-libs": { + "versions": { + "1.3.6-r2": 1542820449 + }, + "origin": "rhash", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ], + "provides": [ + "so:librhash.so.0=0" + ] + }, + "py-paramiko-doc": { + "versions": { + "2.4.2-r0": 1545216485 + }, + "origin": "py-paramiko", + "dependencies": [ + "py-asn1", + "py-cryptography", + "py-bcrypt", + "py-pynacl" + ] + }, + "p11-kit-dev": { + "versions": { + "0.23.14-r0": 1544791899 + }, + "origin": "p11-kit", + "dependencies": [ + "p11-kit=0.23.14-r0", + "pkgconfig" + ], + "provides": [ + "pc:p11-kit-1=0.23.14" + ] + }, + "lua-lustache": { + "versions": { + "1.3.1-r2": 1545076397 + }, + "origin": "lua-lustache", + "dependencies": [ + "lua" + ], + "provides": [ + "lua-lustache-common=1.3.1-r2", + "lua5.1-lustache=1.3.1-r2", + "lua5.2-lustache=1.3.1-r2", + "lua5.3-lustache=1.3.1-r2" + ] + }, + "libqrencode": { + "versions": { + "4.0.2-r0": 1543226703 + }, + "origin": "libqrencode", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpng16.so.16" + ], + "provides": [ + "so:libqrencode.so.4=4.0.2", + "cmd:qrencode" + ] + }, + "libisofs": { + "versions": { + "1.4.8-r0": 1544797288 + }, + "origin": "libisofs", + "dependencies": [ + "so:libacl.so.1", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libisofs.so.6=6.84.0" + ] + }, + "perl-dbd-pg": { + "versions": { + "3.7.4-r0": 1542972915 + }, + "origin": "perl-dbd-pg", + "dependencies": [ + "perl-dbi", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "font-alias": { + "versions": { + "1.0.3-r1": 1542924698 + }, + "origin": "font-alias" + }, + "gsl-dev": { + "versions": { + "2.5-r0": 1545117967 + }, + "origin": "gsl", + "dependencies": [ + "gsl=2.5-r0", + "pkgconfig" + ], + "provides": [ + "pc:gsl=2.5", + "cmd:gsl-config" + ] + }, + "compat-pvgrub": { + "versions": { + "1-r0": 1545165192 + }, + "origin": "compat-pvgrub", + "dependencies": [ + "/bin/sh" + ], + "provides": [ + "cmd:update-pvgrub" + ] + }, + "perl-role-tiny-doc": { + "versions": { + "2.000006-r0": 1542972958 + }, + "origin": "perl-role-tiny" + }, + "acf-ipsec-tools": { + "versions": { + "0.12.0-r2": 1543246798 + }, + "origin": "acf-ipsec-tools", + "dependencies": [ + "acf-core", + "ipsec-tools" + ] + }, + "openobex-apps": { + "versions": { + "1.7.2-r1": 1545069091 + }, + "origin": "openobex", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libopenobex.so.2" + ], + "provides": [ + "cmd:ircp", + "cmd:irobex_palm3", + "cmd:irxfer", + "cmd:obex_find", + "cmd:obex_tcp", + "cmd:obex_test" + ] + }, + "postgresql-plpython2-contrib": { + "versions": { + "11.2-r0": 1554274176 + }, + "origin": "postgresql", + "dependencies": [ + "postgresql-plpython2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "gtk+2.0-doc": { + "versions": { + "2.24.32-r1": 1543925513 + }, + "origin": "gtk+2.0" + }, + "linux-firmware-ti-keystone": { + "versions": { + "20190322-r0": 1554980647 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "apkbuild-gem-resolver": { + "versions": { + "3.3.1-r0": 1551786762 + }, + "origin": "abuild", + "dependencies": [ + "ruby", + "ruby-augeas" + ], + "provides": [ + "cmd:apkbuild-gem-resolver" + ] + }, + "openldap-back-mdb": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "libarchive": { + "versions": { + "3.3.2-r4": 1542820311 + }, + "origin": "libarchive", + "dependencies": [ + "so:libacl.so.1", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libexpat.so.1", + "so:liblz4.so.1", + "so:liblzma.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libarchive.so.13=13.3.2" + ] + }, + "minicom-doc": { + "versions": { + "2.7.1-r0": 1544799265 + }, + "origin": "minicom" + }, + "py-tox": { + "versions": { + "3.2.1-r1": 1545060603 + }, + "origin": "py-tox", + "dependencies": [ + "py-py", + "py-pluggy", + "py-argparse" + ] + }, + "socat": { + "versions": { + "1.7.3.2-r5": 1543259609 + }, + "origin": "socat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libreadline.so.7", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:filan", + "cmd:procan", + "cmd:socat" + ] + }, + "lua-penlight-doc": { + "versions": { + "1.5.4-r2": 1543223290 + }, + "origin": "lua-penlight" + }, + "perl-params-classify": { + "versions": { + "0.015-r0": 1542845689 + }, + "origin": "perl-params-classify", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "rpcbind": { + "versions": { + "0.2.4-r1": 1543933379 + }, + "origin": "rpcbind", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libtirpc.so.3" + ], + "provides": [ + "cmd:rpcbind", + "cmd:rpcinfo" + ] + }, + "libxscrnsaver-dev": { + "versions": { + "1.2.2-r3": 1543932085 + }, + "origin": "libxscrnsaver", + "dependencies": [ + "libxext-dev", + "libxscrnsaver=1.2.2-r3", + "pc:scrnsaverproto", + "pc:x11", + "pc:xext", + "pkgconfig" + ], + "provides": [ + "pc:xscrnsaver=1.2.2" + ] + }, + "python2-tests": { + "versions": { + "2.7.16-r1": 1557171398 + }, + "origin": "python2", + "provides": [ + "python-tests=2.7.16-r1" + ] + }, + "s6-dns-dev": { + "versions": { + "2.3.0.1-r0": 1545062684 + }, + "origin": "s6-dns", + "dependencies": [ + "s6-dns=2.3.0.1-r0" + ] + }, + "libxcb-doc": { + "versions": { + "1.13-r2": 1542822658 + }, + "origin": "libxcb" + }, + "eudev-doc": { + "versions": { + "3.2.7-r0": 1542845384 + }, + "origin": "eudev" + }, + "lz4": { + "versions": { + "1.8.3-r2": 1545154434 + }, + "origin": "lz4", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lz4", + "cmd:lz4c", + "cmd:lz4cat", + "cmd:unlz4" + ] + }, + "cpufrequtils": { + "versions": { + "008-r4": 1543927815 + }, + "origin": "cpufrequtils", + "dependencies": [ + "sysfsutils", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcpufreq.so.0=0.0.0", + "cmd:cpufreq-aperf", + "cmd:cpufreq-info", + "cmd:cpufreq-set" + ] + }, + "rsyslog-dbg": { + "versions": { + "8.40.0-r3": 1548686789 + }, + "origin": "rsyslog" + }, + "libunistring": { + "versions": { + "0.9.10-r0": 1542304036 + }, + "origin": "libunistring", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libunistring.so.2=2.1.0" + ] + }, + "perl-mime-base64-doc": { + "versions": { + "3.15-r4": 1545061195 + }, + "origin": "perl-mime-base64" + }, + "libunique3-dev": { + "versions": { + "3.0.2-r0": 1543927461 + }, + "origin": "libunique3", + "dependencies": [ + "libunique3=3.0.2-r0", + "pc:gtk+-3.0", + "pkgconfig" + ], + "provides": [ + "pc:unique-3.0=3.0.2" + ] + }, + "uwsgi-syslog": { + "versions": { + "2.0.17.1-r0": 1545062212 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "xf86-video-qxl-doc": { + "versions": { + "0.1.5-r5": 1545292750 + }, + "origin": "xf86-video-qxl" + }, + "nagios-plugins-users": { + "versions": { + "2.2.1-r6": 1543933913 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "rxvt-unicode-doc": { + "versions": { + "9.22-r4": 1543254025 + }, + "origin": "rxvt-unicode" + }, + "py3-gunicorn": { + "versions": { + "19.7.1-r1": 1545214070 + }, + "origin": "py-gunicorn", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:gunicorn", + "cmd:gunicorn_paster" + ] + }, + "consolekit2-lang": { + "versions": { + "1.2.1-r1": 1545062380 + }, + "origin": "consolekit2", + "dependencies": [ + "polkit", + "eudev" + ] + }, + "lua5.1-doc": { + "versions": { + "5.1.5-r7": 1542820876 + }, + "origin": "lua5.1", + "provides": [ + "lua-doc" + ] + }, + "tarsnap": { + "versions": { + "1.0.39-r4": 1544000651 + }, + "origin": "tarsnap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:tarsnap", + "cmd:tarsnap-keygen", + "cmd:tarsnap-keymgmt", + "cmd:tarsnap-keyregen", + "cmd:tarsnap-recrypt" + ] + }, + "fltk-doc": { + "versions": { + "1.3.4-r2": 1545076700 + }, + "origin": "fltk" + }, + "check-doc": { + "versions": { + "0.12.0-r1": 1542822523 + }, + "origin": "check" + }, + "jasper-doc": { + "versions": { + "2.0.14-r0": 1543932347 + }, + "origin": "jasper" + }, + "sdl2_mixer-dev": { + "versions": { + "2.0.4-r0": 1545208849 + }, + "origin": "sdl2_mixer", + "dependencies": [ + "pc:sdl2>=2.0.7", + "pkgconfig", + "sdl2_mixer=2.0.4-r0" + ], + "provides": [ + "pc:SDL2_mixer=2.0.4" + ] + }, + "ed-doc": { + "versions": { + "1.14.2-r2": 1542304951 + }, + "origin": "ed" + }, + "nginx-mod-http-echo": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-io-doc": { + "versions": { + "1.25-r4": 1544001435 + }, + "origin": "perl-io" + }, + "uwsgi-lua": { + "versions": { + "2.0.17.1-r0": 1545062203 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:liblua-5.3.so.0" + ] + }, + "g++": { + "versions": { + "8.3.0-r0": 1554745498 + }, + "origin": "gcc", + "dependencies": [ + "libstdc++=8.3.0-r0", + "gcc=8.3.0-r0", + "libc-dev", + "libstdc++=8.3.0-r0", + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libisl.so.15", + "so:libmpc.so.3", + "so:libmpfr.so.4", + "so:libz.so.1" + ], + "provides": [ + "cmd:c++", + "cmd:g++", + "cmd:x86_64-alpine-linux-musl-c++", + "cmd:x86_64-alpine-linux-musl-g++" + ] + }, + "dvgrab": { + "versions": { + "3.5-r3": 1543999358 + }, + "origin": "dvgrab", + "dependencies": [ + "so:libavc1394.so.0", + "so:libc.musl-x86_64.so.1", + "so:libdv.so.4", + "so:libgcc_s.so.1", + "so:libiec61883.so.0", + "so:libjpeg.so.8", + "so:libraw1394.so.11", + "so:librom1394.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:dvgrab" + ] + }, + "xorg-server-dbg": { + "versions": { + "1.20.3-r1": 1543928067 + }, + "origin": "xorg-server", + "dependencies": [ + "font-misc-misc", + "font-cursor-misc", + "xkeyboard-config", + "xkbcomp", + "xinit" + ] + }, + "libxklavier-doc": { + "versions": { + "5.4-r4": 1546458613 + }, + "origin": "libxklavier" + }, + "dtach-doc": { + "versions": { + "0.9-r1": 1543254079 + }, + "origin": "dtach" + }, + "py-django-sorl-thumbnail": { + "versions": { + "12.4.1-r0": 1543925696 + }, + "origin": "py-django-sorl-thumbnail", + "dependencies": [ + "py-django", + "py-pillow" + ] + }, + "uwsgi-sslrouter": { + "versions": { + "2.0.17.1-r0": 1545062210 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-html-mason-doc": { + "versions": { + "1.58-r0": 1545229449 + }, + "origin": "perl-html-mason" + }, + "bind-libs": { + "versions": { + "9.12.4_p1-r1": 1556801608 + }, + "origin": "bind", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libgssapi_krb5.so.2", + "so:libjson-c.so.4", + "so:libkrb5.so.3", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libbind9.so.1201=1201.0.1", + "so:libdns.so.1208=1208.0.0", + "so:libirs.so.1201=1201.0.1", + "so:libisc.so.1204=1204.1.0", + "so:libisccc.so.1201=1201.0.1", + "so:libisccfg.so.1203=1203.0.1", + "so:libns.so.1207=1207.0.0" + ] + }, + "gettext-static": { + "versions": { + "0.19.8.1-r4": 1542304412 + }, + "origin": "gettext" + }, + "postgresql-bdr-extension-doc": { + "versions": { + "1.0.2-r4": 1543223174 + }, + "origin": "postgresql-bdr-extension" + }, + "speexdsp": { + "versions": { + "1.2_rc3-r5": 1544799236 + }, + "origin": "speexdsp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libspeexdsp.so.1=1.5.0" + ] + }, + "gnutls-c++": { + "versions": { + "3.6.7-r0": 1555049965 + }, + "origin": "gnutls", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgnutlsxx.so.28=28.1.0" + ] + }, + "xinput": { + "versions": { + "1.6.2-r0": 1545235264 + }, + "origin": "xinput", + "dependencies": [ + "so:libX11.so.6", + "so:libXi.so.6", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xinput" + ] + }, + "libgnat": { + "versions": { + "8.3.0-r0": 1554745498 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libgnarl-8.so=0", + "so:libgnat-8.so=0" + ] + }, + "duplicity": { + "versions": { + "0.7.18.2-r0": 1544000473 + }, + "origin": "duplicity", + "dependencies": [ + "python2", + "py-boto", + "gnupg", + "ncftp", + "py2-fasteners", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:librsync.so.2" + ], + "provides": [ + "cmd:duplicity", + "cmd:rdiffdir" + ] + }, + "faac-dev": { + "versions": { + "1.28-r12": 1545165108 + }, + "origin": "faac", + "dependencies": [ + "faac=1.28-r12" + ] + }, + "perl-test-script": { + "versions": { + "1.25-r0": 1545061105 + }, + "origin": "perl-test-script", + "dependencies": [ + "perl-ipc-run3", + "perl-probe-perl", + "perl-test-simple" + ] + }, + "mtr": { + "versions": { + "0.92-r0": 1545163038 + }, + "origin": "mtr", + "dependencies": [ + "ncurses", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:mtr", + "cmd:mtr-packet" + ] + }, + "nginx-mod-http-geoip": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-pathtools-doc": { + "versions": { + "3.75-r0": 1545163545 + }, + "origin": "perl-pathtools" + }, + "asterisk-sounds-en": { + "versions": { + "15.7.1-r0": 1546247585 + }, + "origin": "asterisk" + }, + "perl-uri": { + "versions": { + "1.74-r0": 1542821787 + }, + "origin": "perl-uri", + "dependencies": [ + "perl" + ] + }, + "a2ps-doc": { + "versions": { + "4.14-r7": 1545209605 + }, + "origin": "a2ps" + }, + "xf86-input-vmmouse-doc": { + "versions": { + "13.1.0-r4": 1545207341 + }, + "origin": "xf86-input-vmmouse" + }, + "snappy-doc": { + "versions": { + "1.1.7-r1": 1545208921 + }, + "origin": "snappy" + }, + "gpsd-doc": { + "versions": { + "3.18.1-r1": 1549881587 + }, + "origin": "gpsd" + }, + "net-tools": { + "versions": { + "1.60_git20140218-r2": 1545216354 + }, + "origin": "net-tools", + "dependencies": [ + "mii-tool", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:arp", + "cmd:dnsdomainname", + "cmd:domainname", + "cmd:hostname", + "cmd:ifconfig", + "cmd:ipmaddr", + "cmd:iptunnel", + "cmd:nameif", + "cmd:netstat", + "cmd:nisdomainname", + "cmd:plipconfig", + "cmd:rarp", + "cmd:route", + "cmd:slattach", + "cmd:ypdomainname" + ] + }, + "scstadmin": { + "versions": { + "2.2.0-r2": 1545223236 + }, + "origin": "scstadmin", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:scstadmin" + ] + }, + "git-email": { + "versions": { + "2.20.1-r0": 1545214194 + }, + "origin": "git", + "dependencies": [ + "perl", + "perl-git=2.20.1-r0", + "perl-net-smtp-ssl", + "perl-authen-sasl", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libpcre2-8.so.0", + "so:libssl.so.1.1", + "so:libz.so.1" + ] + }, + "samba-heimdal-libs": { + "versions": { + "4.8.11-r1": 1555334974 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2" + ], + "provides": [ + "so:libasn1-samba4.so.8=8.0.0", + "so:libgssapi-samba4.so.2=2.0.0", + "so:libhcrypto-samba4.so.5=5.0.1", + "so:libheimbase-samba4.so.1=1.0.0", + "so:libheimntlm-samba4.so.1=1.0.1", + "so:libhx509-samba4.so.5=5.0.0", + "so:libkrb5-samba4.so.26=26.0.0", + "so:libroken-samba4.so.19=19.0.1", + "so:libwind-samba4.so.0=0.0.0" + ] + }, + "libexecinfo-dev": { + "versions": { + "1.1-r0": 1545117122 + }, + "origin": "libexecinfo", + "dependencies": [ + "libexecinfo=1.1-r0" + ] + }, + "perl-regexp-common": { + "versions": { + "2017060201-r0": 1543924764 + }, + "origin": "perl-regexp-common" + }, + "libxrender-dev": { + "versions": { + "0.9.10-r3": 1542822735 + }, + "origin": "libxrender", + "dependencies": [ + "libxrender=0.9.10-r3", + "pc:renderproto>=0.9", + "pc:x11", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xrender=0.9.10" + ] + }, + "lxterminal-doc": { + "versions": { + "0.3.2-r0": 1543927689 + }, + "origin": "lxterminal" + }, + "gnokii": { + "versions": { + "0.6.31-r8": 1545300480 + }, + "origin": "gnokii", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgnokii.so.7", + "so:libintl.so.8" + ], + "provides": [ + "cmd:gnokii", + "cmd:gnokiid", + "cmd:mgnokiidev" + ] + }, + "libvirt-bash-completion": { + "versions": { + "4.10.0-r1": 1547051658 + }, + "origin": "libvirt" + }, + "augeas-doc": { + "versions": { + "1.11.0-r0": 1542924581 + }, + "origin": "augeas" + }, + "kamailio-postgres": { + "versions": { + "5.2.0-r1": 1546423169 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "kamailio-db", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5", + "so:libsrdb1.so.1", + "so:libsrdb2.so.1" + ] + }, + "dovecot-sql": { + "versions": { + "2.3.6-r0": 1557134098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libdovecot.so.0" + ], + "provides": [ + "so:libdovecot-sql.so.0=0.0.0" + ] + }, + "libatomic_ops-dev": { + "versions": { + "7.6.4-r0": 1543226875 + }, + "origin": "libatomic_ops", + "dependencies": [ + "libatomic_ops=7.6.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:atomic_ops=7.6.4" + ] + }, + "xcb-util-image-dev": { + "versions": { + "0.4.0-r1": 1543927899 + }, + "origin": "xcb-util-image", + "dependencies": [ + "xcb-util-dev", + "pc:xcb-shm", + "pkgconfig", + "xcb-util-image=0.4.0-r1" + ], + "provides": [ + "pc:xcb-image=0.4.0" + ] + }, + "lua5.2-md5": { + "versions": { + "1.2-r3": 1542883861 + }, + "origin": "lua-md5", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libx11": { + "versions": { + "1.6.7-r0": 1542822727 + }, + "origin": "libx11", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libX11-xcb.so.1=1.0.0", + "so:libX11.so.6=6.3.0" + ] + }, + "ngircd": { + "versions": { + "24-r4": 1544001364 + }, + "origin": "ngircd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpam.so.0", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:ngircd" + ] + }, + "protobuf-vim": { + "versions": { + "3.6.1-r1": 1543931874 + }, + "origin": "protobuf" + }, + "gc-dev": { + "versions": { + "7.6.4-r2": 1543226891 + }, + "origin": "gc", + "dependencies": [ + "gc=7.6.4-r2", + "libgc++=7.6.4-r2", + "pkgconfig" + ], + "provides": [ + "pc:bdw-gc=7.6.4" + ] + }, + "lua5.2-stdlib": { + "versions": { + "41.2.1-r0": 1545292924 + }, + "origin": "lua-stdlib" + }, + "perl-list-someutils-doc": { + "versions": { + "0.55-r0": 1543934260 + }, + "origin": "perl-list-someutils" + }, + "libucontext-dev": { + "versions": { + "0.1.3-r0": 1545207700 + }, + "origin": "libucontext", + "dependencies": [ + "libucontext=0.1.3-r0" + ] + }, + "lua5.2-evdev": { + "versions": { + "2.2.1-r1": 1545076553 + }, + "origin": "lua-evdev", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "loudmouth-dev": { + "versions": { + "1.5.3-r1": 1543226580 + }, + "origin": "loudmouth", + "dependencies": [ + "pkgconfig", + "gnutls-dev", + "libidn-dev", + "libasyncns-dev", + "check-dev", + "autoconf", + "loudmouth=1.5.3-r1", + "pc:glib-2.0" + ], + "provides": [ + "pc:loudmouth-1.0=1.5.3" + ] + }, + "apache2-dev": { + "versions": { + "2.4.39-r0": 1554306834 + }, + "origin": "apache2", + "dependencies": [ + "perl", + "apr-util-dev" + ], + "provides": [ + "cmd:apxs" + ] + }, + "bind-doc": { + "versions": { + "9.12.4_p1-r1": 1556801607 + }, + "origin": "bind" + }, + "apk-cron": { + "versions": { + "1.0-r1": 1544798533 + }, + "origin": "apk-cron", + "dependencies": [ + "apk-tools" + ] + }, + "libinput-libs": { + "versions": { + "1.12.4-r0": 1545734494 + }, + "origin": "libinput", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libevdev.so.2", + "so:libmtdev.so.1", + "so:libudev.so.1" + ], + "provides": [ + "so:libinput.so.10=10.13.0" + ] + }, + "lua5.1-alt-getopt": { + "versions": { + "0.7-r8": 1544799002 + }, + "origin": "lua-alt-getopt" + }, + "apr-util-ldap": { + "versions": { + "1.6.1-r5": 1545061010 + }, + "origin": "apr-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldap-2.4.so.2" + ] + }, + "bwm-ng": { + "versions": { + "0.6.1-r4": 1545073954 + }, + "origin": "bwm-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:bwm-ng" + ] + }, + "fribidi-doc": { + "versions": { + "1.0.5-r0": 1542824048 + }, + "origin": "fribidi" + }, + "orbit2-doc": { + "versions": { + "2.14.19-r4": 1543223378 + }, + "origin": "orbit2" + }, + "uwsgi-corerouter": { + "versions": { + "2.0.17.1-r0": 1545062197 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-firmware-yam": { + "versions": { + "20190322-r0": 1554980646 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "perl-file-sharedir-doc": { + "versions": { + "1.116-r0": 1543238884 + }, + "origin": "perl-file-sharedir" + }, + "icu": { + "versions": { + "62.1-r0": 1542823974 + }, + "origin": "icu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libicui18n.so.62", + "so:libicuio.so.62", + "so:libicutu.so.62", + "so:libicuuc.so.62", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:derb", + "cmd:escapesrc", + "cmd:genbrk", + "cmd:genccode", + "cmd:gencfu", + "cmd:gencmn", + "cmd:gencnval", + "cmd:gendict", + "cmd:gennorm2", + "cmd:genrb", + "cmd:gensprep", + "cmd:icuinfo", + "cmd:icupkg", + "cmd:makeconv", + "cmd:pkgdata", + "cmd:uconv" + ] + }, + "kamailio-rabbitmq": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:librabbitmq.so.4", + "so:libuuid.so.1" + ] + }, + "libev-doc": { + "versions": { + "4.25-r0": 1545838253 + }, + "origin": "libev" + }, + "libgcab-dev": { + "versions": { + "0.7-r2": 1543249782 + }, + "origin": "libgcab", + "dependencies": [ + "gettext-dev", + "libgcab=0.7-r2", + "pc:gio-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libgcab-1.0=0.7" + ] + }, + "collectd-virt": { + "versions": { + "5.8.0-r3": 1546422677 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libvirt.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "collectd-libvirt" + ] + }, + "py3-bind": { + "versions": { + "9.12.4_p1-r1": 1556801608 + }, + "origin": "bind", + "dependencies": [ + "python3", + "py3-ply" + ] + }, + "cdparanoia-doc": { + "versions": { + "10.2-r7": 1543254198 + }, + "origin": "cdparanoia" + }, + "xf86-video-siliconmotion": { + "versions": { + "1.7.9-r3": 1545235258 + }, + "origin": "xf86-video-siliconmotion", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "arpon-doc": { + "versions": { + "3.0-r2": 1545224126 + }, + "origin": "arpon" + }, + "perl-extutils-pkgconfig-doc": { + "versions": { + "1.16-r1": 1543077187 + }, + "origin": "perl-extutils-pkgconfig" + }, + "dev86-doc": { + "versions": { + "0.16.21-r0": 1543935746 + }, + "origin": "dev86" + }, + "freeswitch-sample-config": { + "versions": { + "1.8.2-r1": 1545117892 + }, + "origin": "freeswitch", + "dependencies": [ + "freeswitch-timezones" + ] + }, + "libxxf86vm-doc": { + "versions": { + "1.1.4-r2": 1542900278 + }, + "origin": "libxxf86vm" + }, + "dialog-doc": { + "versions": { + "1.3.20180621-r0": 1543925865 + }, + "origin": "dialog" + }, + "nagios-plugins-snmp": { + "versions": { + "2.2.1-r6": 1543933912 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "net-snmp-tools", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.3-sql-postgres": { + "versions": { + "2.3.5-r2": 1543924404 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "xen-libs": { + "versions": { + "4.11.1-r1": 1545075896 + }, + "origin": "xen", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libext2fs.so.2", + "so:liblzma.so.5", + "so:liblzo2.so.2", + "so:libnl-3.so.200", + "so:libnl-route-3.so.200", + "so:libuuid.so.1", + "so:libyajl.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libfsimage.so.1.0=1.0.0", + "so:libxencall.so.1=1.1", + "so:libxenctrl.so.4.11=4.11.0", + "so:libxendevicemodel.so.1=1.2", + "so:libxenevtchn.so.1=1.1", + "so:libxenforeignmemory.so.1=1.3", + "so:libxengnttab.so.1=1.1", + "so:libxenguest.so.4.11=4.11.0", + "so:libxenlight.so.4.11=4.11.0", + "so:libxenstat.so.0=0.0", + "so:libxenstore.so.3.0=3.0.3", + "so:libxentoolcore.so.1=1.0", + "so:libxentoollog.so.1=1.0", + "so:libxenvchan.so.4.11=4.11.0", + "so:libxlutil.so.4.11=4.11.0" + ] + }, + "goaccess-doc": { + "versions": { + "1.2-r1": 1545116583 + }, + "origin": "goaccess" + }, + "libnetfilter_acct-dev": { + "versions": { + "1.0.3-r0": 1545209157 + }, + "origin": "libnetfilter_acct", + "dependencies": [ + "libnetfilter_acct=1.0.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_acct=1.0.3" + ] + }, + "libnjb-dev": { + "versions": { + "2.2.7-r4": 1545300189 + }, + "origin": "libnjb", + "dependencies": [ + "libusb-compat-dev", + "libnjb=2.2.7-r4", + "pkgconfig" + ], + "provides": [ + "pc:libnjb=2.2.7" + ] + }, + "pgpool": { + "versions": { + "3.7.5-r0": 1545073842 + }, + "origin": "pgpool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:libpcp.so.1=1.0.0", + "cmd:pcp_attach_node", + "cmd:pcp_detach_node", + "cmd:pcp_node_count", + "cmd:pcp_node_info", + "cmd:pcp_pool_status", + "cmd:pcp_proc_count", + "cmd:pcp_proc_info", + "cmd:pcp_promote_node", + "cmd:pcp_recovery_node", + "cmd:pcp_stop_pgpool", + "cmd:pcp_watchdog_info", + "cmd:pg_md5", + "cmd:pgpool", + "cmd:pgpool_setup", + "cmd:watchdog_setup" + ] + }, + "perl-list-allutils": { + "versions": { + "0.14-r0": 1545214301 + }, + "origin": "perl-list-allutils", + "dependencies": [ + "perl-list-utilsby", + "perl-list-someutils", + "perl-scalar-list-utils" + ] + }, + "fltk": { + "versions": { + "1.3.4-r2": 1545076702 + }, + "origin": "fltk", + "dependencies": [ + "so:libGL.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libfltk.so.1.3=1.3", + "so:libfltk_forms.so.1.3=1.3", + "so:libfltk_gl.so.1.3=1.3", + "so:libfltk_images.so.1.3=1.3" + ] + }, + "awstats": { + "versions": { + "7.7-r0": 1545069108 + }, + "origin": "awstats", + "dependencies": [ + "perl", + "perl-uri" + ], + "provides": [ + "cmd:awstats_buildstaticpages.pl", + "cmd:awstats_configure.pl", + "cmd:awstats_exportlib.pl", + "cmd:awstats_updateall.pl", + "cmd:geoip_generator.pl", + "cmd:logresolvemerge.pl", + "cmd:maillogconvert.pl", + "cmd:urlaliasbuilder.pl" + ] + }, + "python2-dev": { + "versions": { + "2.7.16-r1": 1557171398 + }, + "origin": "python2", + "dependencies": [ + "pkgconfig", + "python2=2.7.16-r1" + ], + "provides": [ + "python-dev=2.7.16-r1", + "pc:python-2.7=2.7", + "pc:python2=2.7", + "pc:python=2.7", + "cmd:python-config", + "cmd:python2-config", + "cmd:python2.7-config" + ] + }, + "linux-pam": { + "versions": { + "1.3.0-r0": 1542820663 + }, + "origin": "linux-pam", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpam.so.0=0.84.2", + "so:libpam_misc.so.0=0.82.1", + "so:libpamc.so.0=0.82.1", + "cmd:mkhomedir_helper", + "cmd:pam_tally", + "cmd:pam_tally2", + "cmd:pam_timestamp_check", + "cmd:unix_chkpwd", + "cmd:unix_update" + ] + }, + "openvswitch-ovn": { + "versions": { + "2.10.1-r0": 1544000345 + }, + "origin": "openvswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:ovn-controller", + "cmd:ovn-controller-vtep", + "cmd:ovn-detrace", + "cmd:ovn-docker-overlay-driver", + "cmd:ovn-docker-underlay-driver", + "cmd:ovn-nbctl", + "cmd:ovn-northd", + "cmd:ovn-sbctl", + "cmd:ovn-trace" + ] + }, + "bbsuid": { + "versions": { + "0.6-r0": 1543934485 + }, + "origin": "bbsuid", + "dependencies": [ + "busybox" + ] + }, + "py2-sphinx_rtd_theme": { + "versions": { + "0.4.2-r0": 1544791909 + }, + "origin": "py-sphinx_rtd_theme", + "dependencies": [ + "python2" + ] + }, + "py2-fasteners": { + "versions": { + "0.14.1-r0": 1544000469 + }, + "origin": "py2-fasteners", + "dependencies": [ + "python2", + "py2-monotonic", + "py2-six" + ] + }, + "mini_httpd-doc": { + "versions": { + "1.29-r2": 1544000654 + }, + "origin": "mini_httpd" + }, + "libgsasl-dev": { + "versions": { + "1.8.0-r3": 1545215966 + }, + "origin": "libgsasl", + "dependencies": [ + "pkgconfig", + "libgsasl=1.8.0-r3" + ], + "provides": [ + "pc:libgsasl=1.8.0" + ] + }, + "perl-pod-coverage": { + "versions": { + "0.23-r0": 1542845663 + }, + "origin": "perl-pod-coverage", + "dependencies": [ + "perl", + "perl-devel-symdump", + "perl-test-pod" + ], + "provides": [ + "cmd:pod_cover" + ] + }, + "py2-nose": { + "versions": { + "1.3.7-r2": 1544797223 + }, + "origin": "py-nose", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:nosetests-2.7" + ] + }, + "perl-test-tcp-doc": { + "versions": { + "2.19-r1": 1543249856 + }, + "origin": "perl-test-tcp" + }, + "socat-doc": { + "versions": { + "1.7.3.2-r5": 1543259608 + }, + "origin": "socat" + }, + "xvidcore-dev": { + "versions": { + "1.3.4-r1": 1545302181 + }, + "origin": "xvidcore", + "dependencies": [ + "xvidcore=1.3.4-r1" + ] + }, + "feh-doc": { + "versions": { + "3.1.1-r0": 1545060588 + }, + "origin": "feh" + }, + "zonenotify": { + "versions": { + "0.1-r3": 1545060560 + }, + "origin": "zonenotify", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:zonenotify" + ] + }, + "gnome-doc-utils-doc": { + "versions": { + "0.20.10-r2": 1543254073 + }, + "origin": "gnome-doc-utils" + }, + "libgssglue-doc": { + "versions": { + "0.4-r1": 1543248485 + }, + "origin": "libgssglue" + }, + "ghostscript-gtk": { + "versions": { + "9.26-r2": 1554362638 + }, + "origin": "ghostscript", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgs.so.9", + "so:libgtk-3.so.0" + ], + "provides": [ + "cmd:gsx" + ] + }, + "squid-doc": { + "versions": { + "4.4-r1": 1545216322 + }, + "origin": "squid" + }, + "qemu-system-mips64el": { + "versions": { + "3.1.0-r3": 1551107304 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libstdc++.so.6", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-mips64el" + ] + }, + "avfs-dev": { + "versions": { + "1.0.6-r0": 1545665869 + }, + "origin": "avfs", + "dependencies": [ + "avfs=1.0.6-r0" + ], + "provides": [ + "cmd:avfs-config" + ] + }, + "cracklib": { + "versions": { + "2.9.6-r0": 1545060800 + }, + "origin": "cracklib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcrack.so.2=2.9.0", + "cmd:cracklib-check", + "cmd:cracklib-format", + "cmd:cracklib-packer", + "cmd:cracklib-unpacker", + "cmd:create-cracklib-dict" + ] + }, + "freeradius-python": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "python2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "freeradius3-python=3.0.17-r5", + "so:rlm_python.so=0" + ] + }, + "py2-munkres": { + "versions": { + "1.0.12-r0": 1545292843 + }, + "origin": "py-munkres", + "dependencies": [ + "python2" + ] + }, + "git-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "dpkg-dev": { + "versions": { + "1.19.2-r0": 1545820223 + }, + "origin": "dpkg", + "dependencies": [ + "perl", + "pkgconfig" + ], + "provides": [ + "pc:libdpkg=1.19.2", + "cmd:dpkg-architecture", + "cmd:dpkg-buildpackage", + "cmd:dpkg-checkbuilddeps", + "cmd:dpkg-distaddfile", + "cmd:dpkg-genchanges", + "cmd:dpkg-gencontrol", + "cmd:dpkg-gensymbols", + "cmd:dpkg-name", + "cmd:dpkg-scanpackages", + "cmd:dpkg-scansources", + "cmd:dpkg-shlibdeps", + "cmd:dpkg-source", + "cmd:dpkg-vendor" + ] + }, + "libgpg-error-doc": { + "versions": { + "1.33-r0": 1545838267 + }, + "origin": "libgpg-error" + }, + "freeswitch-sounds-pt-BR-karina-8000": { + "versions": { + "1.0.51-r1": 1544799193 + }, + "origin": "freeswitch-sounds-pt-BR-karina-8000" + }, + "py2-talloc": { + "versions": { + "2.1.14-r0": 1543220636 + }, + "origin": "talloc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libtalloc.so.2" + ], + "provides": [ + "py-talloc=2.1.14-r0", + "so:libpytalloc-util.so.2=2.1.14" + ] + }, + "a52dec-dev": { + "versions": { + "0.7.4-r7": 1545214242 + }, + "origin": "a52dec", + "dependencies": [ + "a52dec=0.7.4-r7" + ] + }, + "lua5.3-dbi-sqlite3": { + "versions": { + "0.6-r3": 1545214226 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "lua5.3-pty": { + "versions": { + "1.2.1-r1": 1545216361 + }, + "origin": "lua-pty", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "usb-modeswitch-doc": { + "versions": { + "2.5.2-r0": 1543932709 + }, + "origin": "usb-modeswitch" + }, + "perl-extutils-helpers": { + "versions": { + "0.026-r0": 1543238834 + }, + "origin": "perl-extutils-helpers" + }, + "cryptsetup": { + "versions": { + "2.0.6-r0": 1545746292 + }, + "origin": "cryptsetup", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcryptsetup.so.12", + "so:libpopt.so.0", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:cryptsetup", + "cmd:cryptsetup-reencrypt", + "cmd:integritysetup", + "cmd:veritysetup" + ] + }, + "libpq": { + "versions": { + "11.2-r0": 1554274176 + }, + "origin": "postgresql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libldap_r-2.4.so.2", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libpq.so.5=5.11" + ] + }, + "krb5-server-ldap": { + "versions": { + "1.15.5-r0": 1548094457 + }, + "origin": "krb5", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libgssrpc.so.4", + "so:libkadm5srv_mit.so.11", + "so:libkdb5.so.8", + "so:libkrb5.so.3", + "so:libkrb5support.so.0", + "so:libldap-2.4.so.2" + ], + "provides": [ + "so:libkdb_ldap.so.1=1.0" + ] + }, + "perl-want-doc": { + "versions": { + "0.29-r1": 1543932632 + }, + "origin": "perl-want" + }, + "rtmpdump": { + "versions": { + "2.4_git20160909-r6": 1545215000 + }, + "origin": "rtmpdump", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librtmp.so.1" + ], + "provides": [ + "cmd:rtmpdump", + "cmd:rtmpgw", + "cmd:rtmpsrv", + "cmd:rtmpsuck" + ] + }, + "xkbcomp": { + "versions": { + "1.4.2-r0": 1542973209 + }, + "origin": "xkbcomp", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libxkbfile.so.1" + ], + "provides": [ + "cmd:xkbcomp" + ] + }, + "minizip": { + "versions": { + "1.2.11-r0": 1545075940 + }, + "origin": "minizip", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libminizip.so.1=1.0.0" + ] + }, + "abuild-doc": { + "versions": { + "3.3.1-r0": 1551786762 + }, + "origin": "abuild" + }, + "tcpflow": { + "versions": { + "1.5.0-r0": 1545207692 + }, + "origin": "tcpflow", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libpcap.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:tcpflow" + ] + }, + "perl-net-http": { + "versions": { + "6.09-r1": 1542821815 + }, + "origin": "perl-net-http", + "dependencies": [ + "perl", + "perl-uri" + ] + }, + "uwsgi-graylog2": { + "versions": { + "2.0.17.1-r0": 1545062201 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ] + }, + "libxpm-dev": { + "versions": { + "3.5.12-r0": 1542894077 + }, + "origin": "libxpm", + "dependencies": [ + "libxpm=3.5.12-r0", + "pc:x11", + "pkgconfig" + ], + "provides": [ + "pc:xpm=3.5.12" + ] + }, + "xhost": { + "versions": { + "1.0.7-r1": 1545300592 + }, + "origin": "xhost", + "dependencies": [ + "so:libX11.so.6", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xhost" + ] + }, + "perl-dbd-sqlite": { + "versions": { + "1.62-r0": 1546944026 + }, + "origin": "perl-dbd-sqlite", + "dependencies": [ + "perl-dbi", + "so:libc.musl-x86_64.so.1" + ] + }, + "pwgen-doc": { + "versions": { + "2.08-r0": 1545290834 + }, + "origin": "pwgen" + }, + "elinks-lang": { + "versions": { + "0.13-r6": 1543934620 + }, + "origin": "elinks" + }, + "bluez-hid2hci": { + "versions": { + "5.50-r0": 1545822205 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ] + }, + "gcc-doc": { + "versions": { + "8.3.0-r0": 1554745497 + }, + "origin": "gcc" + }, + "lua5.1-lub": { + "versions": { + "1.1.0-r1": 1544000680 + }, + "origin": "lua-lub", + "dependencies": [ + "lua5.1", + "lua5.1-filesystem" + ] + }, + "avahi-glib": { + "versions": { + "0.7-r1": 1543925310 + }, + "origin": "avahi", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libavahi-glib.so.1=1.0.2", + "so:libavahi-gobject.so.0=0.0.5" + ] + }, + "libxau-doc": { + "versions": { + "1.0.8-r3": 1542822450 + }, + "origin": "libxau" + }, + "libmikmod-doc": { + "versions": { + "3.3.11.1-r0": 1545162983 + }, + "origin": "libmikmod" + }, + "libiptcdata-doc": { + "versions": { + "1.0.4-r2": 1543932145 + }, + "origin": "libiptcdata" + }, + "py2-pygments": { + "versions": { + "2.2.0-r0": 1542824941 + }, + "origin": "py-pygments", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:pygmentize-2" + ] + }, + "perl-email-address-list": { + "versions": { + "0.05-r2": 1542893363 + }, + "origin": "perl-email-address-list", + "dependencies": [ + "perl", + "perl-email-address" + ] + }, + "kbd-bkeymaps": { + "versions": { + "2.0.4-r3": 1545215186 + }, + "origin": "kbd", + "provides": [ + "bkeymaps" + ] + }, + "mini_httpd-openrc": { + "versions": { + "1.29-r2": 1544000654 + }, + "origin": "mini_httpd" + }, + "libvirt-xen": { + "versions": { + "4.10.0-r1": 1547051658 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=4.10.0-r1", + "libvirt-common-drivers=4.10.0-r1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libvirt.so.0", + "so:libxenlight.so.4.11", + "so:libxenstore.so.3.0", + "so:libxentoollog.so.1", + "so:libxlutil.so.4.11", + "so:libxml2.so.2" + ] + }, + "xdm-doc": { + "versions": { + "1.1.11-r5": 1542985407 + }, + "origin": "xdm" + }, + "s6-rc-doc": { + "versions": { + "0.4.1.0-r0": 1545076680 + }, + "origin": "s6-rc" + }, + "dovecot-ldap": { + "versions": { + "2.3.6-r0": 1557134098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libdovecot.so.0", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ], + "provides": [ + "so:libdovecot-ldap.so.0=0.0.0" + ] + }, + "xf86-input-mouse": { + "versions": { + "1.9.3-r1": 1545300645 + }, + "origin": "xf86-input-mouse", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "coova-chilli": { + "versions": { + "1.4-r2": 1545163684 + }, + "origin": "coova-chilli", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libbstring.so.0=0.0.0", + "so:libchilli.so.0=0.0.0", + "cmd:chilli", + "cmd:chilli_opt", + "cmd:chilli_query", + "cmd:chilli_radconfig", + "cmd:chilli_redir", + "cmd:chilli_response", + "cmd:chilli_script" + ] + }, + "pigz": { + "versions": { + "2.4-r0": 1542820956 + }, + "origin": "pigz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:pigz", + "cmd:unpigz" + ] + }, + "lua-sircbot": { + "versions": { + "0.4-r2": 1543924832 + }, + "origin": "sircbot" + }, + "lxdm-lang": { + "versions": { + "0.5.3-r1": 1545215913 + }, + "origin": "lxdm", + "dependencies": [ + "bash" + ] + }, + "gst-plugins-base-doc": { + "versions": { + "1.14.4-r0": 1543928664 + }, + "origin": "gst-plugins-base" + }, + "uwsgi-emperor_zeromq": { + "versions": { + "2.0.17.1-r0": 1545062199 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libzmq.so.5" + ] + }, + "perl-html-rewriteattributes": { + "versions": { + "0.05-r1": 1545289346 + }, + "origin": "perl-html-rewriteattributes", + "dependencies": [ + "perl-html-tagset", + "perl-uri", + "perl-html-parser" + ] + }, + "postfix-stone": { + "versions": { + "3.3.2-r0": 1547130363 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpostfix-global.so", + "so:libpostfix-util.so" + ], + "provides": [ + "cmd:fsstone", + "cmd:qmqp-sink", + "cmd:qmqp-source", + "cmd:smtp-sink", + "cmd:smtp-source" + ] + }, + "lame-doc": { + "versions": { + "3.100-r0": 1545117086 + }, + "origin": "lame" + }, + "sqlite": { + "versions": { + "3.26.0-r3": 1546255354 + }, + "origin": "sqlite", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:sqlite3" + ] + }, + "cogl-lang": { + "versions": { + "1.22.2-r0": 1544000912 + }, + "origin": "cogl" + }, + "perl-cgi-emulate-psgi-doc": { + "versions": { + "0.23-r0": 1545229754 + }, + "origin": "perl-cgi-emulate-psgi" + }, + "collectd-mqtt": { + "versions": { + "5.8.0-r3": 1546422678 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ] + }, + "postgresql-plpython3": { + "versions": { + "11.2-r0": 1554274177 + }, + "origin": "postgresql", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "xfsprogs-libs": { + "versions": { + "4.19.0-r1": 1546597448 + }, + "origin": "xfsprogs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhandle.so.1=1.0.3" + ] + }, + "libaio-dev": { + "versions": { + "0.3.111-r0": 1542845871 + }, + "origin": "libaio", + "dependencies": [ + "libaio=0.3.111-r0" + ] + }, + "perl-db-doc": { + "versions": { + "0.55-r1": 1542985267 + }, + "origin": "perl-db" + }, + "xcb-util": { + "versions": { + "0.4.0-r1": 1542822846 + }, + "origin": "xcb-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-util.so.1=1.0.0" + ] + }, + "perl-server-starter": { + "versions": { + "0.15-r1": 1543249942 + }, + "origin": "perl-server-starter", + "dependencies": [ + "perl-proc-wait3", + "perl-list-moreutils", + "perl-scope-guard" + ], + "provides": [ + "cmd:start_server" + ] + }, + "libotr3-doc": { + "versions": { + "3.2.1-r4": 1543230934 + }, + "origin": "libotr3", + "dependencies": [ + "libotr3" + ] + }, + "libraw1394-dev": { + "versions": { + "2.1.2-r1": 1543932096 + }, + "origin": "libraw1394", + "dependencies": [ + "libraw1394=2.1.2-r1", + "pkgconfig" + ], + "provides": [ + "pc:libraw1394=2.1.2" + ] + }, + "ruby-bundler": { + "versions": { + "1.17.1-r0": 1545301042 + }, + "origin": "ruby-bundler", + "dependencies": [ + "ruby", + "ruby-etc", + "ruby-io-console" + ], + "provides": [ + "cmd:bundle", + "cmd:bundler" + ] + }, + "lua5.2-lzlib": { + "versions": { + "0.4.3-r0": 1542845781 + }, + "origin": "lua-lzlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ] + }, + "icu-static": { + "versions": { + "62.1-r0": 1542823972 + }, + "origin": "icu" + }, + "qemu-microblazeel": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-microblazeel" + ] + }, + "glade3": { + "versions": { + "3.8.5-r5": 1543998728 + }, + "origin": "glade3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgladeui-1.so.11=11.2.0", + "cmd:glade-3" + ] + }, + "cd-discid": { + "versions": { + "1.4-r2": 1545117998 + }, + "origin": "cd-discid", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cd-discid" + ] + }, + "quagga-dev": { + "versions": { + "1.2.4-r1": 1545068939 + }, + "origin": "quagga", + "dependencies": [ + "quagga=1.2.4-r1" + ] + }, + "libdv-tools": { + "versions": { + "1.0.0-r3": 1543999341 + }, + "origin": "libdv", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdv.so.4", + "so:libgdk-x11-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ], + "provides": [ + "cmd:dubdv", + "cmd:dvconnect", + "cmd:encodedv", + "cmd:playdv" + ] + }, + "py-django-treebeard": { + "versions": { + "4.3-r0": 1543925708 + }, + "origin": "py-django-treebeard", + "dependencies": [ + "py-django" + ] + }, + "which": { + "versions": { + "2.21-r1": 1543929035 + }, + "origin": "which", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:which" + ] + }, + "ppp": { + "versions": { + "2.4.7-r6": 1543999024 + }, + "origin": "ppp", + "dependencies": [ + "ppp-chat", + "ppp-radius", + "ppp-atm", + "ppp-pppoe", + "ppp-l2tp", + "ppp-winbind", + "ppp-passprompt", + "ppp-passwordfd", + "ppp-minconn", + "ppp-daemon" + ] + }, + "gobject-introspection-doc": { + "versions": { + "1.56.1-r0": 1542823015 + }, + "origin": "gobject-introspection" + }, + "hylafax-doc": { + "versions": { + "6.0.7-r0": 1545746106 + }, + "origin": "hylafax" + }, + "lua5.1-openrc": { + "versions": { + "0.2-r3": 1545066956 + }, + "origin": "lua-openrc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librc.so.1" + ] + }, + "lua5.1-dbi-mysql": { + "versions": { + "0.6-r3": 1545214218 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "asterisk-mobile": { + "versions": { + "15.7.1-r0": 1546247585 + }, + "origin": "asterisk", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "gnome-common": { + "versions": { + "3.18.0-r1": 1543925206 + }, + "origin": "gnome-common", + "provides": [ + "cmd:gnome-autogen.sh" + ] + }, + "distcc": { + "versions": { + "3.3.2-r0": 1544799001 + }, + "origin": "distcc", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:distcc", + "cmd:distccd", + "cmd:distccmon-text", + "cmd:lsdistcc", + "cmd:update-distcc-symlinks" + ] + }, + "gtkglext-doc": { + "versions": { + "1.2.0-r12": 1544001308 + }, + "origin": "gtkglext" + }, + "linux-firmware-ttusb-budget": { + "versions": { + "20190322-r0": 1554980647 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "dnstop": { + "versions": { + "20140915-r3": 1545165121 + }, + "origin": "dnstop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:dnstop" + ] + }, + "perl-data-guid-doc": { + "versions": { + "0.049-r0": 1545163358 + }, + "origin": "perl-data-guid" + }, + "vde2-doc": { + "versions": { + "2.3.2-r10": 1545074024 + }, + "origin": "vde2" + }, + "avahi-tools": { + "versions": { + "0.7-r1": 1543925310 + }, + "origin": "avahi", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libintl.so.8" + ], + "provides": [ + "cmd:avahi-browse", + "cmd:avahi-browse-domains", + "cmd:avahi-publish", + "cmd:avahi-publish-address", + "cmd:avahi-publish-service", + "cmd:avahi-resolve", + "cmd:avahi-resolve-address", + "cmd:avahi-resolve-host-name", + "cmd:avahi-set-host-name" + ] + }, + "freeswitch-flite": { + "versions": { + "1.8.2-r1": 1545117891 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libflite.so.1", + "so:libflite_cmu_us_awb.so.1", + "so:libflite_cmu_us_kal.so.1", + "so:libflite_cmu_us_kal16.so.1", + "so:libflite_cmu_us_rms.so.1", + "so:libflite_cmu_us_slt.so.1", + "so:libflite_usenglish.so.1", + "so:libfreeswitch.so.1" + ] + }, + "lua-dmvpn": { + "versions": { + "1.0.2-r0": 1553425622 + }, + "origin": "dmvpn", + "dependencies": [ + "lua-asn1" + ] + }, + "libgc++": { + "versions": { + "7.6.4-r2": 1543226893 + }, + "origin": "gc", + "dependencies": [ + "so:libgc.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgccpp.so.1=1.3.1" + ] + }, + "asterisk-cdr-mysql": { + "versions": { + "15.7.1-r0": 1546247586 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "libdaemon": { + "versions": { + "0.14-r2": 1543925257 + }, + "origin": "libdaemon", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdaemon.so.0=0.5.0" + ] + }, + "gradm": { + "versions": { + "3.1.201607172312-r0": 1545299891 + }, + "origin": "gradm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:gradm", + "cmd:grlearn" + ] + }, + "sdl_image-dev": { + "versions": { + "1.2.12-r4": 1545302048 + }, + "origin": "sdl_image", + "dependencies": [ + "pc:sdl>=1.2.10", + "pkgconfig", + "sdl_image=1.2.12-r4" + ], + "provides": [ + "pc:SDL_image=1.2.12" + ] + }, + "orc": { + "versions": { + "0.4.28-r0": 1542883449 + }, + "origin": "orc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liborc-0.4.so.0=0.28.0", + "so:liborc-test-0.4.so.0=0.28.0" + ] + }, + "kamailio-cpl": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libsrdb1.so.1", + "so:libxml2.so.2" + ] + }, + "taskd-pki": { + "versions": { + "1.1.0-r4": 1545163049 + }, + "origin": "taskd", + "dependencies": [ + "taskd", + "gnutls-utils" + ] + }, + "lua-subprocess": { + "versions": { + "0.0.20141229-r2": 1542883782 + }, + "origin": "lua-subprocess" + }, + "libxkbcommon-dev": { + "versions": { + "0.8.2-r0": 1545857326 + }, + "origin": "libxkbcommon", + "dependencies": [ + "libxkbcommon=0.8.2-r0", + "pc:xcb", + "pc:xcb-xkb", + "pkgconfig" + ], + "provides": [ + "pc:xkbcommon-x11=0.8.2", + "pc:xkbcommon=0.8.2" + ] + }, + "lua-rex-posix": { + "versions": { + "2.9.0-r0": 1545209198 + }, + "origin": "lua-rex" + }, + "librevenge-dev": { + "versions": { + "0.0.4-r2": 1543932070 + }, + "origin": "librevenge", + "dependencies": [ + "boost-dev", + "zlib-dev", + "cppunit-dev", + "librevenge=0.0.4-r2", + "pkgconfig" + ], + "provides": [ + "pc:librevenge-0.0=0.0.4", + "pc:librevenge-generators-0.0=0.0.4", + "pc:librevenge-stream-0.0=0.0.4" + ] + }, + "libpciaccess-dev": { + "versions": { + "0.14-r0": 1545838880 + }, + "origin": "libpciaccess", + "dependencies": [ + "libpciaccess=0.14-r0", + "pkgconfig" + ], + "provides": [ + "pc:pciaccess=0.14" + ] + }, + "qemu-block-dmg-bz2": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "axel-doc": { + "versions": { + "2.16.1-r2": 1545229308 + }, + "origin": "axel" + }, + "quazip-dev": { + "versions": { + "0.7.3-r0": 1545118030 + }, + "origin": "quazip", + "dependencies": [ + "quazip=0.7.3-r0" + ] + }, + "libcanberra-dev": { + "versions": { + "0.30-r2": 1545118058 + }, + "origin": "libcanberra", + "dependencies": [ + "gtk+-dev", + "libogg-dev", + "libvorbis-dev", + "alsa-lib-dev", + "libtool", + "gtk+3.0-dev", + "libcanberra-gtk2=0.30-r2", + "libcanberra-gtk3=0.30-r2", + "libcanberra=0.30-r2", + "pc:gdk-2.0", + "pc:gdk-3.0", + "pc:gtk+-2.0", + "pc:gtk+-3.0", + "pkgconfig" + ], + "provides": [ + "pc:libcanberra-gtk3=0.30", + "pc:libcanberra-gtk=0.30", + "pc:libcanberra=0.30" + ] + }, + "virglrenderer-doc": { + "versions": { + "0.7.0-r1": 1547472270 + }, + "origin": "virglrenderer" + }, + "pciutils-dev": { + "versions": { + "3.6.2-r0": 1543921866 + }, + "origin": "pciutils", + "dependencies": [ + "pciutils-libs=3.6.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libpci=3.6.2" + ] + }, + "xf86-video-qxl": { + "versions": { + "0.1.5-r5": 1545292752 + }, + "origin": "xf86-video-qxl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "qt-webkit": { + "versions": { + "4.8.7-r11": 1545075089 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtDeclarative.so.4", + "so:libQtGui.so.4", + "so:libQtNetwork.so.4", + "so:libX11.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libsqlite3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libQtWebKit.so.4=4.9.4" + ] + }, + "kamailio-ev": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libev.so.4" + ] + }, + "liblogging-dev": { + "versions": { + "1.0.6-r0": 1545060626 + }, + "origin": "liblogging", + "dependencies": [ + "liblogging=1.0.6-r0", + "pkgconfig" + ], + "provides": [ + "pc:liblogging-stdlog=1.0.6" + ] + }, + "readline-dev": { + "versions": { + "7.0.003-r1": 1542301064 + }, + "origin": "readline", + "dependencies": [ + "libhistory=7.0.003-r1", + "readline=7.0.003-r1" + ] + }, + "libssh": { + "versions": { + "0.7.6-r1": 1545300250 + }, + "origin": "libssh", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libssh.so.4=4.4.3", + "so:libssh_threads.so.4=4.4.3" + ] + }, + "checkbashisms-doc": { + "versions": { + "2.18.11-r0": 1545292631 + }, + "origin": "checkbashisms" + }, + "perl-yaml-tiny": { + "versions": { + "1.73-r0": 1542893323 + }, + "origin": "perl-yaml-tiny" + }, + "gettext-asprintf": { + "versions": { + "0.19.8.1-r4": 1542304413 + }, + "origin": "gettext", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libasprintf.so.0=0.0.0" + ] + }, + "samba-common-server-libs": { + "versions": { + "4.8.11-r1": 1555334973 + }, + "origin": "samba", + "dependencies": [ + "so:libCHARSET3-samba4.so", + "so:libacl.so.1", + "so:libaddns-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcli-spoolss-samba4.so", + "so:libcliauth-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libcups.so.2", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libevents-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libgssapi-samba4.so.2", + "so:libiov-buf-samba4.so", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libldb.so.1", + "so:liblibcli-lsa3-samba4.so", + "so:liblibcli-netlogon3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmessages-dgm-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libpam.so.0", + "so:libreplace-samba4.so", + "so:libsamba-cluster-support-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-shim-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtdb-wrap-samba4.so", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so", + "so:libtrusts-util-samba4.so", + "so:libutil-cmdline-samba4.so", + "so:libutil-reg-samba4.so", + "so:libutil-setid-samba4.so", + "so:libutil-tdb-samba4.so", + "so:libwbclient.so.0" + ], + "provides": [ + "so:libads-samba4.so=0", + "so:libauth-samba4.so=0", + "so:libdfs-server-ad-samba4.so=0", + "so:libnetapi.so.0=0", + "so:libnpa-tstream-samba4.so=0", + "so:libprinting-migrate-samba4.so=0", + "so:libsmbd-base-samba4.so=0", + "so:libsmbd-conn-samba4.so=0", + "so:libsmbldap.so.2=2", + "so:libsmbldaphelper-samba4.so=0" + ] + }, + "py-pluggy": { + "versions": { + "0.7.1-r0": 1542824895 + }, + "origin": "py-pluggy" + }, + "qemu": { + "versions": { + "3.1.0-r3": 1551107308 + }, + "origin": "qemu", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libcap.so.2", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libnettle.so.6", + "so:libxkbcommon.so.0" + ], + "provides": [ + "cmd:qemu-edid", + "cmd:qemu-keymap", + "cmd:qemu-pr-helper", + "cmd:virtfs-proxy-helper" + ] + }, + "lxsession-lang": { + "versions": { + "0.5.3-r0": 1545299866 + }, + "origin": "lxsession" + }, + "perl-io": { + "versions": { + "1.25-r4": 1544001438 + }, + "origin": "perl-io", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-file-remove-doc": { + "versions": { + "1.58-r0": 1544792322 + }, + "origin": "perl-file-remove" + }, + "gd": { + "versions": { + "2.2.5-r3": 1554727880 + }, + "origin": "gd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ], + "provides": [ + "cmd:annotate", + "cmd:gd2copypal", + "cmd:gd2togif", + "cmd:gd2topng", + "cmd:gdcmpgif", + "cmd:gdparttopng", + "cmd:gdtopng", + "cmd:giftogd2", + "cmd:pngtogd", + "cmd:pngtogd2", + "cmd:webpng" + ] + }, + "recode-dev": { + "versions": { + "3.6-r2": 1543245780 + }, + "origin": "recode", + "dependencies": [ + "recode=3.6-r2" + ] + }, + "openbox-gnome": { + "versions": { + "3.6.1-r2": 1545207328 + }, + "origin": "openbox", + "dependencies": [ + "so:libX11.so.6", + "so:libXau.so.6", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "cmd:gdm-control", + "cmd:gnome-panel-control", + "cmd:openbox-gnome-session" + ] + }, + "bsd-compat-headers": { + "versions": { + "0.7.1-r0": 1542302754 + }, + "origin": "libc-dev" + }, + "perl-list-utilsby": { + "versions": { + "0.11-r0": 1545062502 + }, + "origin": "perl-list-utilsby" + }, + "speexdsp-dev": { + "versions": { + "1.2_rc3-r5": 1544799233 + }, + "origin": "speexdsp", + "dependencies": [ + "pkgconfig", + "speexdsp=1.2_rc3-r5" + ] + }, + "pjproject": { + "versions": { + "2.7.2-r4": 1545073633 + }, + "origin": "pjproject", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libgsm.so.1", + "so:libsamplerate.so.0", + "so:libspeex.so.1", + "so:libspeexdsp.so.1", + "so:libssl.so.1.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libg7221codec.so.2=2", + "so:libilbccodec.so.2=2", + "so:libpj.so.2=2", + "so:libpjlib-util.so.2=2", + "so:libpjmedia-audiodev.so.2=2", + "so:libpjmedia-codec.so.2=2", + "so:libpjmedia-videodev.so.2=2", + "so:libpjmedia.so.2=2", + "so:libpjnath.so.2=2", + "so:libpjsip-simple.so.2=2", + "so:libpjsip-ua.so.2=2", + "so:libpjsip.so.2=2", + "so:libpjsua.so.2=2", + "so:libpjsua2.so.2=2", + "so:libyuv.so.2=2" + ] + }, + "postgresql-bdr-dev": { + "versions": { + "9.4.14_p1-r1": 1543223160 + }, + "origin": "postgresql-bdr", + "dependencies": [ + "readline-dev", + "openssl-dev", + "zlib-dev", + "libxml2-dev", + "pkgconfig", + "postgresql-bdr=9.4.14_p1-r1" + ], + "provides": [ + "pc:libecpg=9.4.14", + "pc:libecpg_compat=9.4.14", + "pc:libpgtypes=9.4.14", + "pc:libpq=9.4.14" + ] + }, + "geoip": { + "versions": { + "1.6.12-r1": 1544799416 + }, + "origin": "geoip", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libGeoIP.so.1=1.6.12", + "cmd:geoiplookup", + "cmd:geoiplookup6" + ] + }, + "libxext-doc": { + "versions": { + "1.3.3-r3": 1542822855 + }, + "origin": "libxext" + }, + "libldap": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libsasl2.so.3", + "so:libssl.so.1.1" + ], + "provides": [ + "so:liblber-2.4.so.2=2.10.10", + "so:libldap-2.4.so.2=2.10.10", + "so:libldap_r-2.4.so.2=2.10.10" + ] + }, + "lua-b64": { + "versions": { + "0.1-r2": 1545293188 + }, + "origin": "lua-b64", + "dependencies": [ + "lua" + ] + }, + "lua5.3-rex-posix": { + "versions": { + "2.9.0-r0": 1545209207 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "fontconfig-dev": { + "versions": { + "2.13.1-r0": 1545745987 + }, + "origin": "fontconfig", + "dependencies": [ + "fontconfig=2.13.1-r0", + "pc:expat", + "pc:freetype2>=21.0.15", + "pc:uuid", + "pkgconfig" + ], + "provides": [ + "pc:fontconfig=2.13.1" + ] + }, + "libnetfilter_cthelper-dev": { + "versions": { + "1.0.0-r0": 1545073849 + }, + "origin": "libnetfilter_cthelper", + "dependencies": [ + "libnetfilter_cthelper=1.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_cthelper=1.0.0" + ] + }, + "feh": { + "versions": { + "3.1.1-r0": 1545060588 + }, + "origin": "feh", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libpng16.so.16" + ], + "provides": [ + "cmd:feh" + ] + }, + "ncurses-terminfo-base": { + "versions": { + "6.1_p20190105-r0": 1546948259 + }, + "origin": "ncurses" + }, + "perl-test-nowarnings": { + "versions": { + "1.04-r1": 1542845590 + }, + "origin": "perl-test-nowarnings", + "dependencies": [ + "perl", + "perl-test-simple" + ] + }, + "partimage-doc": { + "versions": { + "0.6.9-r6": 1543924736 + }, + "origin": "partimage" + }, + "dropbear-doc": { + "versions": { + "2018.76-r2": 1545208975 + }, + "origin": "dropbear" + }, + "lua5.3-lxc": { + "versions": { + "3.0.2-r0": 1546416498 + }, + "origin": "lua-lxc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblxc.so.1" + ] + }, + "iceauth": { + "versions": { + "1.0.8-r0": 1545214113 + }, + "origin": "iceauth", + "dependencies": [ + "so:libICE.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:iceauth" + ] + }, + "dovecot-pgsql": { + "versions": { + "2.3.6-r0": 1557134098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot-sql=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:libdriver_pgsql.so=0" + ] + }, + "perl-mail-spf": { + "versions": { + "2.9.0-r2": 1542925020 + }, + "origin": "perl-mail-spf", + "dependencies": [ + "perl", + "perl-error", + "perl-net-dns", + "perl-uri", + "perl-netaddr-ip", + "perl-net-dns-resolver-programmable" + ] + }, + "libarchive-dev": { + "versions": { + "3.3.2-r4": 1542820310 + }, + "origin": "libarchive", + "dependencies": [ + "libarchive=3.3.2-r4", + "pkgconfig" + ], + "provides": [ + "pc:libarchive=3.3.2" + ] + }, + "rrdtool-doc": { + "versions": { + "1.7.0-r0": 1542924800 + }, + "origin": "rrdtool" + }, + "libxau": { + "versions": { + "1.0.8-r3": 1542822450 + }, + "origin": "libxau", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXau.so.6=6.0.0" + ] + }, + "libmaa-dev": { + "versions": { + "1.3.2-r1": 1545209186 + }, + "origin": "libmaa", + "dependencies": [ + "libmaa=1.3.2-r1" + ] + }, + "open-isns-doc": { + "versions": { + "0.97-r4": 1542883211 + }, + "origin": "open-isns" + }, + "xdpyinfo-doc": { + "versions": { + "1.3.2-r0": 1545069116 + }, + "origin": "xdpyinfo" + }, + "f2fs-tools": { + "versions": { + "1.12.0-r0": 1546344802 + }, + "origin": "f2fs-tools", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libf2fs.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:defrag.f2fs", + "cmd:dump.f2fs", + "cmd:f2fscrypt", + "cmd:f2fstat", + "cmd:fibmap.f2fs", + "cmd:fsck.f2fs", + "cmd:mkfs.f2fs", + "cmd:parse.f2fs", + "cmd:resize.f2fs", + "cmd:sg_write_buffer", + "cmd:sload.f2fs" + ] + }, + "font-ibm-type1": { + "versions": { + "1.0.3-r0": 1545116965 + }, + "origin": "font-ibm-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "libva-glx": { + "versions": { + "2.2.0-r0": 1545300086 + }, + "origin": "libva-glx", + "dependencies": [ + "so:libGL.so.1", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libva-x11.so.2", + "so:libva.so.2" + ], + "provides": [ + "so:libva-glx.so.2=2.200.0" + ] + }, + "slim-themes": { + "versions": { + "1.2.3-r3": 1545293196 + }, + "origin": "slim-themes", + "dependencies": [ + "slim" + ] + }, + "lua5.1-discount": { + "versions": { + "1.2.10.1-r4": 1545209119 + }, + "origin": "lua-discount", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-exporter": { + "versions": { + "5.73-r0": 1544001442 + }, + "origin": "perl-exporter" + }, + "libnice-gstreamer": { + "versions": { + "0.1.14-r2": 1543928835 + }, + "origin": "libnice", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libnice.so.10" + ] + }, + "xf86-video-sis": { + "versions": { + "0.10.9-r3": 1545253991 + }, + "origin": "xf86-video-sis", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libguess-dev": { + "versions": { + "1.2-r0": 1543998752 + }, + "origin": "libguess", + "dependencies": [ + "libguess=1.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libguess=1.2" + ] + }, + "perl-file-copy-recursive-doc": { + "versions": { + "0.44-r0": 1543238906 + }, + "origin": "perl-file-copy-recursive" + }, + "uwsgi-symcall": { + "versions": { + "2.0.17.1-r0": 1545062211 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-file-next": { + "versions": { + "1.16-r0": 1544000675 + }, + "origin": "perl-file-next" + }, + "xmlrpc-c-dev": { + "versions": { + "1.39.13-r0": 1545665208 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "libxml2-dev", + "xmlrpc-c++=1.39.13-r0", + "xmlrpc-c-abyss=1.39.13-r0", + "xmlrpc-c-client++=1.39.13-r0", + "xmlrpc-c-client=1.39.13-r0", + "xmlrpc-c=1.39.13-r0" + ], + "provides": [ + "cmd:xmlrpc-c-config" + ] + }, + "ttf-linux-libertine": { + "versions": { + "5.3.0-r0": 1544000077 + }, + "origin": "ttf-linux-libertine", + "dependencies": [ + "fontconfig", + "encodings", + "mkfontdir", + "mkfontscale" + ] + }, + "apache2": { + "versions": { + "2.4.39-r0": 1554306836 + }, + "origin": "apache2", + "dependencies": [ + "/bin/sh", + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:fcgistarter", + "cmd:httpd", + "cmd:suexec" + ] + }, + "aaudit": { + "versions": { + "0.7.2-r1": 1543934483 + }, + "origin": "aaudit", + "dependencies": [ + "lua5.2", + "lua5.2-posix", + "lua5.2-cjson", + "lua5.2-pc", + "lua5.2-socket" + ], + "provides": [ + "cmd:aaudit" + ] + }, + "mtr-gtk": { + "versions": { + "0.92-r0": 1545163036 + }, + "origin": "mtr", + "dependencies": [ + "ncurses", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:mtr-gtk", + "cmd:mtr-packet-gtk" + ] + }, + "cmph-doc": { + "versions": { + "2.0-r1": 1544000441 + }, + "origin": "cmph" + }, + "py-gtk-doc": { + "versions": { + "2.24.0-r15": 1543927628 + }, + "origin": "py-gtk" + }, + "acf-unbound": { + "versions": { + "0.1.0-r2": 1543932151 + }, + "origin": "acf-unbound", + "dependencies": [ + "acf-core", + "unbound" + ] + }, + "json-c0.12": { + "versions": { + "0.12.1-r2": 1545301158 + }, + "origin": "json-c0.12", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libjson-c.so.2=2.0.2" + ] + }, + "py3-paramiko": { + "versions": { + "2.4.2-r0": 1545216486 + }, + "origin": "py-paramiko", + "dependencies": [ + "py3-asn1", + "py3-cryptography", + "py3-bcrypt", + "py3-pynacl", + "python3" + ] + }, + "py3-gobject3": { + "versions": { + "3.28.2-r0": 1543925244 + }, + "origin": "py-gobject3", + "dependencies": [ + "py3-cairo", + "so:libc.musl-x86_64.so.1", + "so:libcairo-gobject.so.2", + "so:libcairo.so.2", + "so:libffi.so.6", + "so:libgirepository-1.0.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ] + }, + "rsyslog-doc": { + "versions": { + "8.40.0-r3": 1548686787 + }, + "origin": "rsyslog" + }, + "libxrender-doc": { + "versions": { + "0.9.10-r3": 1542822736 + }, + "origin": "libxrender" + }, + "libgcab-doc": { + "versions": { + "0.7-r2": 1543249783 + }, + "origin": "libgcab" + }, + "aconf-mod-network": { + "versions": { + "0.7.1-r0": 1553432497 + }, + "origin": "aconf", + "dependencies": [ + "aconf" + ] + }, + "nginx-mod-http-perl": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "conntrack-tools-doc": { + "versions": { + "1.4.4-r0": 1545301898 + }, + "origin": "conntrack-tools" + }, + "perl-time-modules": { + "versions": { + "2013.0912-r0": 1545061993 + }, + "origin": "perl-time-modules", + "dependencies": [ + "perl" + ] + }, + "perl-unix-syslog": { + "versions": { + "1.1-r11": 1545067185 + }, + "origin": "perl-unix-syslog", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "qt": { + "versions": { + "4.8.7-r11": 1545075091 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libQtCore.so.4=4.8.7", + "so:libQtDBus.so.4=4.8.7", + "so:libQtNetwork.so.4=4.8.7", + "so:libQtScript.so.4=4.8.7", + "so:libQtSql.so.4=4.8.7", + "so:libQtTest.so.4=4.8.7", + "so:libQtXml.so.4=4.8.7", + "so:libQtXmlPatterns.so.4=4.8.7", + "cmd:qdbus" + ] + }, + "libnl3-cli": { + "versions": { + "3.4.0-r0": 1542965892 + }, + "origin": "libnl3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnl-3.so.200", + "so:libnl-genl-3.so.200", + "so:libnl-idiag-3.so.200", + "so:libnl-nf-3.so.200", + "so:libnl-route-3.so.200" + ], + "provides": [ + "so:libnl-cli-3.so.200=200.26.0", + "cmd:genl-ctrl-list", + "cmd:idiag-socket-details", + "cmd:nf-ct-add", + "cmd:nf-ct-list", + "cmd:nf-exp-add", + "cmd:nf-exp-delete", + "cmd:nf-exp-list", + "cmd:nf-log", + "cmd:nf-monitor", + "cmd:nf-queue", + "cmd:nl-addr-add", + "cmd:nl-addr-delete", + "cmd:nl-addr-list", + "cmd:nl-class-add", + "cmd:nl-class-delete", + "cmd:nl-class-list", + "cmd:nl-classid-lookup", + "cmd:nl-cls-add", + "cmd:nl-cls-delete", + "cmd:nl-cls-list", + "cmd:nl-fib-lookup", + "cmd:nl-link-enslave", + "cmd:nl-link-ifindex2name", + "cmd:nl-link-list", + "cmd:nl-link-name2ifindex", + "cmd:nl-link-release", + "cmd:nl-link-set", + "cmd:nl-link-stats", + "cmd:nl-list-caches", + "cmd:nl-list-sockets", + "cmd:nl-monitor", + "cmd:nl-neigh-add", + "cmd:nl-neigh-delete", + "cmd:nl-neigh-list", + "cmd:nl-neightbl-list", + "cmd:nl-pktloc-lookup", + "cmd:nl-qdisc-add", + "cmd:nl-qdisc-delete", + "cmd:nl-qdisc-list", + "cmd:nl-route-add", + "cmd:nl-route-delete", + "cmd:nl-route-get", + "cmd:nl-route-list", + "cmd:nl-rule-list", + "cmd:nl-tctree-list", + "cmd:nl-util-addr" + ] + }, + "nrpe": { + "versions": { + "3.2.1-r0": 1545249893 + }, + "origin": "nrpe", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:nrpe", + "cmd:nrpe-uninstall" + ] + }, + "openldap-overlay-dyngroup": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "nginx-mod-stream-js": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "nginx-mod-stream", + "so:libc.musl-x86_64.so.1" + ] + }, + "acf-freeradius3": { + "versions": { + "0.3.1-r0": 1545062651 + }, + "origin": "acf-freeradius3", + "dependencies": [ + "acf-core", + "freeradius>3" + ] + }, + "acf-quagga": { + "versions": { + "0.10.1-r1": 1545069552 + }, + "origin": "acf-quagga", + "dependencies": [ + "acf-core", + "lua-socket", + "quagga" + ] + }, + "libpaper-doc": { + "versions": { + "1.1.24-r4": 1542824769 + }, + "origin": "libpaper" + }, + "gstreamer0.10": { + "versions": { + "0.10.36-r3": 1544000752 + }, + "origin": "gstreamer0.10", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgstbase-0.10.so.0=0.30.0", + "so:libgstcheck-0.10.so.0=0.30.0", + "so:libgstcontroller-0.10.so.0=0.30.0", + "so:libgstdataprotocol-0.10.so.0=0.30.0", + "so:libgstnet-0.10.so.0=0.30.0", + "so:libgstreamer-0.10.so.0=0.30.0" + ] + }, + "boost-static": { + "versions": { + "1.67.0-r2": 1542823625 + }, + "origin": "boost" + }, + "glfw": { + "versions": { + "3.2.1-r2": 1545299815 + }, + "origin": "glfw", + "dependencies": [ + "so:libX11.so.6", + "so:libXcursor.so.1", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libglfw.so.3=3.2" + ] + }, + "subversion-libs": { + "versions": { + "1.11.1-r0": 1548692375 + }, + "origin": "subversion", + "dependencies": [ + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:libexpat.so.1", + "so:liblz4.so.1", + "so:libsasl2.so.3", + "so:libserf-1.so.1", + "so:libsqlite3.so.0", + "so:libz.so.1" + ], + "provides": [ + "so:libsvn_client-1.so.0=0.0.0", + "so:libsvn_delta-1.so.0=0.0.0", + "so:libsvn_diff-1.so.0=0.0.0", + "so:libsvn_fs-1.so.0=0.0.0", + "so:libsvn_fs_base-1.so.0=0.0.0", + "so:libsvn_fs_fs-1.so.0=0.0.0", + "so:libsvn_fs_util-1.so.0=0.0.0", + "so:libsvn_fs_x-1.so.0=0.0.0", + "so:libsvn_ra-1.so.0=0.0.0", + "so:libsvn_ra_local-1.so.0=0.0.0", + "so:libsvn_ra_serf-1.so.0=0.0.0", + "so:libsvn_ra_svn-1.so.0=0.0.0", + "so:libsvn_repos-1.so.0=0.0.0", + "so:libsvn_subr-1.so.0=0.0.0", + "so:libsvn_wc-1.so.0=0.0.0" + ] + }, + "awall-policies": { + "versions": { + "1.6.9-r0": 1548501700 + }, + "origin": "awall", + "dependencies": [ + "awall" + ], + "provides": [ + "cmd:setup-firewall" + ] + }, + "gtk+3.0": { + "versions": { + "3.24.1-r0": 1543927436 + }, + "origin": "gtk+3.0", + "dependencies": [ + "shared-mime-info", + "gtk-update-icon-cache", + "/bin/sh", + "so:libX11.so.6", + "so:libXcomposite.so.1", + "so:libXcursor.so.1", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXi.so.6", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libatk-1.0.so.0", + "so:libatk-bridge-2.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo-gobject.so.2", + "so:libcairo.so.2", + "so:libcups.so.2", + "so:libepoxy.so.0", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libharfbuzz.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0", + "so:libwayland-client.so.0", + "so:libwayland-cursor.so.0", + "so:libwayland-egl.so.1", + "so:libxkbcommon.so.0" + ], + "provides": [ + "so:libgailutil-3.so.0=0.0.0", + "so:libgdk-3.so.0=0.2400.1", + "so:libgtk-3.so.0=0.2400.1", + "cmd:gtk-builder-tool", + "cmd:gtk-encode-symbolic-svg", + "cmd:gtk-launch", + "cmd:gtk-query-immodules-3.0", + "cmd:gtk-query-settings", + "cmd:gtk3-demo", + "cmd:gtk3-demo-application", + "cmd:gtk3-icon-browser", + "cmd:gtk3-widget-factory" + ] + }, + "freeswitch-sounds-es-mx-maria-44100": { + "versions": { + "0-r2": 1542893557 + }, + "origin": "freeswitch-sounds-es-mx-maria-44100" + }, + "perl-module-implementation": { + "versions": { + "0.09-r0": 1542845724 + }, + "origin": "perl-module-implementation", + "dependencies": [ + "perl-try-tiny", + "perl-module-runtime" + ] + }, + "mesa-dev": { + "versions": { + "18.1.7-r2": 1554455968 + }, + "origin": "mesa", + "dependencies": [ + "libdrm-dev", + "libxext-dev", + "libxdamage-dev", + "libxcb-dev", + "libxshmfence-dev", + "mesa-egl=18.1.7-r2", + "mesa-gbm=18.1.7-r2", + "mesa-gl=18.1.7-r2", + "mesa-glapi=18.1.7-r2", + "mesa-gles=18.1.7-r2", + "mesa-osmesa=18.1.7-r2", + "mesa-xatracker=18.1.7-r2", + "pc:libdrm>=2.4.75", + "pc:x11", + "pc:x11-xcb", + "pc:xcb", + "pc:xcb-dri2>=1.8", + "pc:xcb-glx>=1.8.1", + "pc:xdamage>=1.1", + "pc:xext", + "pc:xfixes", + "pc:xxf86vm", + "pkgconfig" + ], + "provides": [ + "pc:dri=18.1.7", + "pc:egl=18.1.7", + "pc:gbm=18.1.7", + "pc:gl=18.1.7", + "pc:glesv1_cm=18.1.7", + "pc:glesv2=18.1.7", + "pc:osmesa=8", + "pc:xatracker=2.3.0" + ] + }, + "iptables-doc": { + "versions": { + "1.6.2-r1": 1545062496 + }, + "origin": "iptables" + }, + "gtkmm-doc": { + "versions": { + "2.24.5-r0": 1543925601 + }, + "origin": "gtkmm" + }, + "perl-encode-doc": { + "versions": { + "2.98-r0": 1543935163 + }, + "origin": "perl-encode" + }, + "mesa-egl": { + "versions": { + "18.1.7-r2": 1554455971 + }, + "origin": "mesa", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libexpat.so.1", + "so:libgbm.so.1", + "so:libglapi.so.0", + "so:libwayland-client.so.0", + "so:libwayland-server.so.0", + "so:libxcb-dri2.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb-xfixes.so.0", + "so:libxcb.so.1", + "so:libxshmfence.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libEGL.so.1=1.0.0" + ] + }, + "libxklavier": { + "versions": { + "5.4-r4": 1546458613 + }, + "origin": "libxklavier", + "dependencies": [ + "xkeyboard-config", + "iso-codes", + "so:libX11.so.6", + "so:libXi.so.6", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libxkbfile.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "so:libxklavier.so.16=16.4.0" + ] + }, + "swish-e": { + "versions": { + "2.4.7-r8": 1545163776 + }, + "origin": "swish-e", + "dependencies": [ + "perl-html-parser", + "perl-html-tagset", + "perl-libwww", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libswish-e.so.2=2.0.0", + "cmd:swish-e", + "cmd:swish-filter-test" + ] + }, + "e2fsprogs-doc": { + "versions": { + "1.44.5-r0": 1545745858 + }, + "origin": "e2fsprogs" + }, + "rpcbind-doc": { + "versions": { + "0.2.4-r1": 1543933378 + }, + "origin": "rpcbind" + }, + "py3-dateutil": { + "versions": { + "2.7.3-r0": 1543248498 + }, + "origin": "py-dateutil", + "dependencies": [ + "py3-six", + "python3" + ] + }, + "perl-parse-recdescent": { + "versions": { + "1.967015-r1": 1545067083 + }, + "origin": "perl-parse-recdescent", + "dependencies": [ + "perl" + ] + }, + "py2-urwid": { + "versions": { + "1.3.1-r2": 1542981236 + }, + "origin": "py-urwid", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "xscreensaver-extras": { + "versions": { + "5.40-r0": 1545224088 + }, + "origin": "xscreensaver", + "dependencies": [ + "bc", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXmu.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgdk_pixbuf_xlib-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libjpeg.so.8" + ] + }, + "uwsgi-logfile": { + "versions": { + "2.0.17.1-r0": 1545062202 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "rp-pppoe": { + "versions": { + "3.13-r0": 1545223084 + }, + "origin": "rp-pppoe", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pppoe", + "cmd:pppoe-connect", + "cmd:pppoe-relay", + "cmd:pppoe-server", + "cmd:pppoe-setup", + "cmd:pppoe-sniff", + "cmd:pppoe-start", + "cmd:pppoe-status", + "cmd:pppoe-stop" + ] + }, + "diffutils": { + "versions": { + "3.7-r0": 1546846563 + }, + "origin": "diffutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cmp", + "cmd:diff", + "cmd:diff3", + "cmd:sdiff" + ] + }, + "apache2-error": { + "versions": { + "2.4.39-r0": 1554306835 + }, + "origin": "apache2" + }, + "xtrans": { + "versions": { + "1.3.5-r1": 1542822666 + }, + "origin": "xtrans" + }, + "xfontsel-doc": { + "versions": { + "1.0.6-r0": 1545215013 + }, + "origin": "xfontsel" + }, + "nfdump-openrc": { + "versions": { + "1.6.17-r0": 1545223024 + }, + "origin": "nfdump" + }, + "fetchmailconf": { + "versions": { + "6.3.26-r14": 1544001391 + }, + "origin": "fetchmail", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:fetchmailconf" + ] + }, + "mesa-dri-swrast": { + "versions": { + "18.1.7-r2": 1554455970 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "pssh": { + "versions": { + "2.3.1-r1": 1545302366 + }, + "origin": "pssh", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:pnuke", + "cmd:prsync", + "cmd:pscp", + "cmd:pslurp", + "cmd:pssh" + ] + }, + "transmission-daemon": { + "versions": { + "2.94-r1": 1545208748 + }, + "origin": "transmission", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libevent-2.1.so.6", + "so:libintl.so.8", + "so:libz.so.1" + ], + "provides": [ + "cmd:transmission-daemon" + ] + }, + "dbus": { + "versions": { + "1.10.24-r1": 1542824378 + }, + "origin": "dbus", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libexpat.so.1" + ], + "provides": [ + "cmd:dbus-cleanup-sockets", + "cmd:dbus-daemon", + "cmd:dbus-monitor", + "cmd:dbus-run-session", + "cmd:dbus-send", + "cmd:dbus-test-tool", + "cmd:dbus-update-activation-environment", + "cmd:dbus-uuidgen" + ] + }, + "cairo-doc": { + "versions": { + "1.16.0-r1": 1546948314 + }, + "origin": "cairo" + }, + "linux-vanilla": { + "versions": { + "4.19.41-r0": 1557400012 + }, + "origin": "linux-vanilla", + "dependencies": [ + "mkinitfs", + "linux-firmware-any" + ] + }, + "squid-lang-sl": { + "versions": { + "4.4-r1": 1545216329 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "conky-doc": { + "versions": { + "1.11.1-r0": 1545746033 + }, + "origin": "conky" + }, + "json-glib-tests": { + "versions": { + "1.4.4-r0": 1545837175 + }, + "origin": "json-glib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libjson-glib-1.0.so.0" + ] + }, + "libevdev": { + "versions": { + "1.6.0-r0": 1545838440 + }, + "origin": "libevdev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libevdev.so.2=2.2.0", + "cmd:libevdev-tweak-device", + "cmd:mouse-dpi-tool", + "cmd:touchpad-edge-detector" + ] + }, + "slim": { + "versions": { + "1.3.6-r8": 1545229417 + }, + "origin": "slim", + "dependencies": [ + "dbus", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXmu.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libck-connector.so.0", + "so:libdbus-1.so.3", + "so:libgcc_s.so.1", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libslim.so.1.3.6=1.3.6", + "cmd:slim" + ] + }, + "sshguard": { + "versions": { + "2.2.0-r0": 1545300225 + }, + "origin": "sshguard", + "dependencies": [ + "iptables", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sshguard" + ] + }, + "freeswitch-dev": { + "versions": { + "1.8.2-r1": 1545117891 + }, + "origin": "freeswitch", + "dependencies": [ + "freeswitch-freetdm=1.8.2-r1", + "freeswitch=1.8.2-r1", + "pkgconfig" + ], + "provides": [ + "pc:freeswitch=1.8.2", + "pc:freetdm=0.1" + ] + }, + "wireless-tools-dev": { + "versions": { + "30_pre9-r0": 1545062248 + }, + "origin": "wireless-tools" + }, + "acf-alpine-conf": { + "versions": { + "0.9.0-r3": 1545116967 + }, + "origin": "acf-alpine-conf", + "dependencies": [ + "acf-core", + "lua-posix", + "libressl" + ] + }, + "libxpm-doc": { + "versions": { + "3.5.12-r0": 1542894077 + }, + "origin": "libxpm" + }, + "atkmm": { + "versions": { + "2.24.2-r1": 1544799364 + }, + "origin": "atkmm", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libglibmm-2.4.so.1", + "so:libgobject-2.0.so.0", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libatkmm-1.6.so.1=1.1.0" + ] + }, + "libunwind-dev": { + "versions": { + "1.2.1-r3": 1545915765 + }, + "origin": "libunwind", + "dependencies": [ + "libexecinfo-dev", + "libunwind=1.2.1-r3", + "pkgconfig" + ], + "provides": [ + "pc:libunwind-coredump=1.21", + "pc:libunwind-generic=1.21", + "pc:libunwind-ptrace=1.21", + "pc:libunwind-setjmp=1.21", + "pc:libunwind=1.21" + ] + }, + "icu-doc": { + "versions": { + "62.1-r0": 1542823973 + }, + "origin": "icu" + }, + "bitlbee-otr": { + "versions": { + "3.5.1-r4": 1543248420 + }, + "origin": "bitlbee", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libotr.so.5" + ] + }, + "st-doc": { + "versions": { + "0.8.1-r0": 1545292621 + }, + "origin": "st" + }, + "ncftp-doc": { + "versions": { + "3.2.6-r1": 1544000463 + }, + "origin": "ncftp" + }, + "lzo": { + "versions": { + "2.10-r2": 1542985324 + }, + "origin": "lzo", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblzo2.so.2=2.0.0" + ] + }, + "userspace-rcu-dev": { + "versions": { + "0.10.1-r0": 1543249589 + }, + "origin": "userspace-rcu", + "dependencies": [ + "pkgconfig", + "userspace-rcu=0.10.1-r0" + ], + "provides": [ + "pc:liburcu-bp=0.10.1", + "pc:liburcu-cds=0.10.1", + "pc:liburcu-mb=0.10.1", + "pc:liburcu-qsbr=0.10.1", + "pc:liburcu-signal=0.10.1", + "pc:liburcu=0.10.1" + ] + }, + "llvm5-static": { + "versions": { + "5.0.2-r0": 1546874979 + }, + "origin": "llvm5", + "provides": [ + "llvm-static=5.0.2-r0" + ] + }, + "rdiff-backup-doc": { + "versions": { + "1.2.8-r2": 1545213660 + }, + "origin": "rdiff-backup" + }, + "postfix-sqlite": { + "versions": { + "3.3.2-r0": 1547130363 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:postfix-sqlite.so=0" + ] + }, + "gtk+2.0-lang": { + "versions": { + "2.24.32-r1": 1543925513 + }, + "origin": "gtk+2.0", + "dependencies": [ + "shared-mime-info", + "gtk-update-icon-cache" + ] + }, + "perl-test-output-doc": { + "versions": { + "1.031-r0": 1545300171 + }, + "origin": "perl-test-output" + }, + "libasyncns-doc": { + "versions": { + "0.8-r1": 1543226569 + }, + "origin": "libasyncns" + }, + "perl-variable-magic": { + "versions": { + "0.62-r0": 1542973102 + }, + "origin": "perl-variable-magic", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gtk+2.0-dev": { + "versions": { + "2.24.32-r1": 1543925512 + }, + "origin": "gtk+2.0", + "dependencies": [ + "atk-dev", + "cairo-dev", + "intltool", + "libxdamage-dev", + "pango-dev", + "gtk+2.0=2.24.32-r1", + "pc:atk", + "pc:cairo", + "pc:gdk-pixbuf-2.0", + "pc:gio-2.0", + "pc:pango", + "pc:pangocairo", + "pc:pangoft2", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0" + ], + "provides": [ + "pc:gail=2.24.32", + "pc:gdk-2.0=2.24.32", + "pc:gdk-x11-2.0=2.24.32", + "pc:gtk+-2.0=2.24.32", + "pc:gtk+-unix-print-2.0=2.24.32", + "pc:gtk+-x11-2.0=2.24.32", + "cmd:gtk-builder-convert", + "cmd:gtk-demo" + ] + }, + "gnutls-dev": { + "versions": { + "3.6.7-r0": 1555049964 + }, + "origin": "gnutls", + "dependencies": [ + "gnutls-c++=3.6.7-r0", + "gnutls=3.6.7-r0", + "pc:hogweed", + "pc:libtasn1", + "pc:nettle", + "pc:p11-kit-1", + "pkgconfig" + ], + "provides": [ + "pc:gnutls=3.6.7" + ] + }, + "libell": { + "versions": { + "0.16-r0": 1545822164 + }, + "origin": "libell", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libell.so.0=0.0.2" + ] + }, + "nginx-mod-mail": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "alpine-ipxe-ipxe_pxe": { + "versions": { + "1.0_git20180825-r1": 1545986078 + }, + "origin": "alpine-ipxe" + }, + "argon2": { + "versions": { + "20171227-r1": 1543928962 + }, + "origin": "argon2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:argon2" + ] + }, + "linux-firmware-none": { + "versions": { + "20190322-r0": 1554980655 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "sharutils-lang": { + "versions": { + "4.15.2-r0": 1543929025 + }, + "origin": "sharutils", + "dependencies": [ + "bzip2" + ] + }, + "libmnl": { + "versions": { + "1.0.4-r0": 1542883675 + }, + "origin": "libmnl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmnl.so.0=0.2.0" + ] + }, + "liblognorm": { + "versions": { + "2.0.6-r1": 1548783946 + }, + "origin": "liblognorm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libestr.so.0", + "so:libfastjson.so.4" + ], + "provides": [ + "so:liblognorm.so.5=5.1.0", + "cmd:lognormalizer" + ] + }, + "myrepos": { + "versions": { + "1.20180726-r0": 1542893509 + }, + "origin": "myrepos", + "dependencies": [ + "perl", + "git" + ], + "provides": [ + "cmd:mr" + ] + }, + "libmaxminddb-dev": { + "versions": { + "1.3.2-r0": 1543226665 + }, + "origin": "libmaxminddb", + "dependencies": [ + "libmaxminddb=1.3.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmaxminddb=1.3.2" + ] + }, + "darkhttpd": { + "versions": { + "1.12-r2": 1543223261 + }, + "origin": "darkhttpd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:darkhttpd" + ] + }, + "git-p4": { + "versions": { + "2.20.1-r0": 1545214195 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0", + "git-fast-import=2.20.1-r0" + ] + }, + "xtables-addons": { + "versions": { + "3.2-r0": 1545154536 + }, + "origin": "xtables-addons", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxtables.so.12" + ], + "provides": [ + "so:libxt_ACCOUNT_cl.so.0=0.0.0", + "cmd:iptaccount" + ] + }, + "ddate": { + "versions": { + "0.2.2-r0": 1545207008 + }, + "origin": "ddate", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ddate" + ] + }, + "audit": { + "versions": { + "2.8.4-r0": 1543245856 + }, + "origin": "audit", + "dependencies": [ + "so:libaudit.so.1", + "so:libauparse.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0" + ], + "provides": [ + "cmd:audisp-remote", + "cmd:audispd", + "cmd:auditctl", + "cmd:auditd", + "cmd:augenrules", + "cmd:aulast", + "cmd:aulastlog", + "cmd:aureport", + "cmd:ausearch", + "cmd:ausyscall", + "cmd:autrace", + "cmd:auvirt" + ] + }, + "spl-vanilla": { + "versions": { + "4.19.41-r0": 1557400331 + }, + "origin": "spl-vanilla", + "dependencies": [ + "linux-vanilla=4.19.41-r0" + ] + }, + "rtpproxy-doc": { + "versions": { + "2.0.0-r4": 1545301076 + }, + "origin": "rtpproxy" + }, + "drbd-utils-doc": { + "versions": { + "9.7.1-r0": 1545746125 + }, + "origin": "drbd-utils" + }, + "cmake-bash-completion": { + "versions": { + "3.13.0-r0": 1542820577 + }, + "origin": "cmake" + }, + "udisks2-doc": { + "versions": { + "2.6.5-r1": 1545293055 + }, + "origin": "udisks2" + }, + "perl-test-eol": { + "versions": { + "2.00-r0": 1545075952 + }, + "origin": "perl-test-eol" + }, + "setxkbmap": { + "versions": { + "1.3.1-r1": 1545209626 + }, + "origin": "setxkbmap", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libxkbfile.so.1" + ], + "provides": [ + "cmd:setxkbmap" + ] + }, + "libunique3-doc": { + "versions": { + "3.0.2-r0": 1543927462 + }, + "origin": "libunique3" + }, + "lua-lxc": { + "versions": { + "3.0.2-r0": 1546416498 + }, + "origin": "lua-lxc" + }, + "boost-fiber": { + "versions": { + "1.67.0-r2": 1542823631 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_context-mt.so.1.67.0", + "so:libboost_filesystem-mt.so.1.67.0", + "so:libboost_system-mt.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_fiber-mt.so.1.67.0=1.67.0" + ] + }, + "polkit": { + "versions": { + "0.105-r9": 1547130960 + }, + "origin": "polkit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libpolkit-agent-1.so.0=0.0.0", + "so:libpolkit-backend-1.so.0=0.0.0", + "so:libpolkit-gobject-1.so.0=0.0.0", + "cmd:pk-example-frobnicate", + "cmd:pkaction", + "cmd:pkcheck", + "cmd:pkexec", + "cmd:pkttyagent" + ] + }, + "upower": { + "versions": { + "0.99.7-r0": 1545069387 + }, + "origin": "upower", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libupower-glib.so.3=3.0.1", + "cmd:upower" + ] + }, + "perl-css-squish": { + "versions": { + "0.10-r0": 1544000384 + }, + "origin": "perl-css-squish", + "dependencies": [ + "perl", + "perl-uri", + "perl-test-longstring" + ] + }, + "aconf-mod-openssh": { + "versions": { + "0.7.1-r0": 1553432498 + }, + "origin": "aconf", + "dependencies": [ + "aconf", + "openssh" + ] + }, + "xf86-video-i740-doc": { + "versions": { + "1.3.6-r3": 1545299626 + }, + "origin": "xf86-video-i740" + }, + "lua-pty": { + "versions": { + "1.2.1-r1": 1545216362 + }, + "origin": "lua-pty" + }, + "boost-filesystem": { + "versions": { + "1.67.0-r2": 1542823631 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.67.0", + "so:libboost_system.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_filesystem-mt.so.1.67.0=1.67.0", + "so:libboost_filesystem.so.1.67.0=1.67.0" + ] + }, + "freeswitch-snmp": { + "versions": { + "1.8.2-r1": 1545117893 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeswitch.so.1", + "so:libnetsnmp.so.35", + "so:libnetsnmpagent.so.35" + ] + }, + "tdb-libs": { + "versions": { + "1.3.16-r0": 1543933255 + }, + "origin": "tdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtdb.so.1=1.3.16" + ] + }, + "rsyslog-pmlastmsg": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "opensp-doc": { + "versions": { + "1.5.2-r0": 1542893110 + }, + "origin": "opensp" + }, + "byobu": { + "versions": { + "5.127-r0": 1545062321 + }, + "origin": "byobu", + "dependencies": [ + "python3", + "tmux" + ], + "provides": [ + "cmd:byobu", + "cmd:byobu-config", + "cmd:byobu-ctrl-a", + "cmd:byobu-disable", + "cmd:byobu-disable-prompt", + "cmd:byobu-enable", + "cmd:byobu-enable-prompt", + "cmd:byobu-export", + "cmd:byobu-janitor", + "cmd:byobu-keybindings", + "cmd:byobu-launch", + "cmd:byobu-launcher", + "cmd:byobu-launcher-install", + "cmd:byobu-launcher-uninstall", + "cmd:byobu-layout", + "cmd:byobu-prompt", + "cmd:byobu-quiet", + "cmd:byobu-reconnect-sockets", + "cmd:byobu-screen", + "cmd:byobu-select-backend", + "cmd:byobu-select-profile", + "cmd:byobu-select-session", + "cmd:byobu-shell", + "cmd:byobu-silent", + "cmd:byobu-status", + "cmd:byobu-status-detail", + "cmd:byobu-tmux", + "cmd:byobu-ugraph", + "cmd:byobu-ulevel", + "cmd:col1", + "cmd:ctail", + "cmd:manifest", + "cmd:purge-old-kernels", + "cmd:vigpg", + "cmd:wifi-status" + ] + }, + "eggdbus": { + "versions": { + "0.6-r5": 1543935571 + }, + "origin": "eggdbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libeggdbus-1.so.0=0.0.0", + "cmd:eggdbus-binding-tool", + "cmd:eggdbus-glib-genmarshal" + ] + }, + "mkfontdir": { + "versions": { + "1.0.7-r1": 1542924704 + }, + "origin": "mkfontdir", + "dependencies": [ + "mkfontscale", + "/bin/sh" + ], + "provides": [ + "cmd:mkfontdir" + ] + }, + "libnice": { + "versions": { + "0.1.14-r2": 1543928835 + }, + "origin": "libnice", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libnice.so.10=10.7.0", + "cmd:sdp-example", + "cmd:simple-example", + "cmd:stunbdc", + "cmd:stund", + "cmd:threaded-example" + ] + }, + "libzdb-dev": { + "versions": { + "3.1-r2": 1543221886 + }, + "origin": "libzdb", + "dependencies": [ + "flex-dev", + "sqlite-dev", + "mariadb-connector-c-dev", + "postgresql-dev", + "libzdb=3.1-r2", + "pkgconfig" + ], + "provides": [ + "pc:zdb=3.1" + ] + }, + "ack": { + "versions": { + "2.24-r0": 1545224032 + }, + "origin": "ack", + "dependencies": [ + "perl-file-next", + "perl" + ], + "provides": [ + "cmd:ack" + ] + }, + "makedepend-doc": { + "versions": { + "1.0.5-r2": 1543220550 + }, + "origin": "makedepend" + }, + "uwsgi-cache": { + "versions": { + "2.0.17.1-r0": 1545062196 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "collectd-perl": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "alsa-utils-openrc": { + "versions": { + "1.1.8-r0": 1547112822 + }, + "origin": "alsa-utils", + "dependencies": [ + "dialog" + ] + }, + "linux-firmware-atmel": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "font-bitstream-100dpi": { + "versions": { + "1.0.3-r0": 1543932149 + }, + "origin": "font-bitstream-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "font-misc-misc": { + "versions": { + "1.1.2-r1": 1542973188 + }, + "origin": "font-misc-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig", + "util-macros" + ] + }, + "openjpeg": { + "versions": { + "2.3.0-r3": 1552584664 + }, + "origin": "openjpeg", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libopenjp2.so.7=2.3.0" + ] + }, + "yasm-dev": { + "versions": { + "1.3.0-r1": 1545117177 + }, + "origin": "yasm" + }, + "linux-firmware-kaweth": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "perl-mail-clamav-doc": { + "versions": { + "0.29-r14": 1545067091 + }, + "origin": "perl-mail-clamav" + }, + "fuse-openrc": { + "versions": { + "3.2.6-r1": 1548943636 + }, + "origin": "fuse3", + "dependencies": [ + "fuse-common" + ] + }, + "linux-firmware-imx": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "sqlite-doc": { + "versions": { + "3.26.0-r3": 1546255353 + }, + "origin": "sqlite" + }, + "fltk-fluid": { + "versions": { + "1.3.4-r2": 1545076701 + }, + "origin": "fltk", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libfltk.so.1.3", + "so:libfltk_forms.so.1.3", + "so:libfltk_images.so.1.3", + "so:libpng16.so.16", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:fluid" + ] + }, + "acpica": { + "versions": { + "20181213-r0": 1545995082 + }, + "origin": "acpica", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:acpibin", + "cmd:acpidump", + "cmd:acpiexamples", + "cmd:acpiexec", + "cmd:acpihelp", + "cmd:acpinames", + "cmd:acpisrc", + "cmd:acpixtract" + ] + }, + "gpsd-dev": { + "versions": { + "3.18.1-r1": 1549881587 + }, + "origin": "gpsd", + "dependencies": [ + "gpsd=3.18.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:libgps=3.18.1" + ] + }, + "grep": { + "versions": { + "3.1-r2": 1543926780 + }, + "origin": "grep", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ], + "provides": [ + "cmd:egrep", + "cmd:fgrep", + "cmd:grep" + ] + }, + "libotr3": { + "versions": { + "3.2.1-r4": 1543230935 + }, + "origin": "libotr3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20" + ], + "provides": [ + "so:libotr.so.2=2.2.1" + ] + }, + "motif": { + "versions": { + "2.3.4-r2": 1545223206 + }, + "origin": "motif", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1" + ], + "provides": [ + "so:libMrm.so.4=4.0.4", + "so:libUil.so.4=4.0.4", + "so:libXm.so.4=4.0.4", + "cmd:uil" + ] + }, + "py2-zope-interface": { + "versions": { + "4.6.0-r0": 1548537849 + }, + "origin": "py-zope-interface", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "perl-test-harness-doc": { + "versions": { + "3.42-r0": 1543238856 + }, + "origin": "perl-test-harness" + }, + "py-irc-scripts": { + "versions": { + "8.5.1-r0": 1545060749 + }, + "origin": "py-irc" + }, + "py3-uritemplate": { + "versions": { + "3.0.0-r1": 1545207293 + }, + "origin": "py-uritemplate", + "dependencies": [ + "python3" + ], + "provides": [ + "py3-uritemplate.py" + ] + }, + "perl-file-slurper": { + "versions": { + "0.010-r0": 1545075943 + }, + "origin": "perl-file-slurper" + }, + "ethtool-doc": { + "versions": { + "4.19-r0": 1545820353 + }, + "origin": "ethtool" + }, + "ivykis-dev": { + "versions": { + "0.42.3-r0": 1548543104 + }, + "origin": "ivykis", + "dependencies": [ + "ivykis=0.42.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:ivykis=0.42.3" + ] + }, + "perl-text-wikiformat": { + "versions": { + "0.81-r0": 1545076739 + }, + "origin": "perl-text-wikiformat", + "dependencies": [ + "perl-uri" + ] + }, + "mqtt-exec-dbg": { + "versions": { + "0.4-r4": 1545162969 + }, + "origin": "mqtt-exec" + }, + "imake": { + "versions": { + "1.0.7-r2": 1543932076 + }, + "origin": "imake", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ccmakedep", + "cmd:cleanlinks", + "cmd:imake", + "cmd:makeg", + "cmd:mergelib", + "cmd:mkdirhier", + "cmd:mkhtmlindex", + "cmd:revpath", + "cmd:xmkmf" + ] + }, + "libpng-utils": { + "versions": { + "1.6.37-r0": 1557129095 + }, + "origin": "libpng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpng16.so.16", + "so:libz.so.1" + ], + "provides": [ + "cmd:png-fix-itxt", + "cmd:pngfix" + ] + }, + "xf86-video-vesa": { + "versions": { + "2.3.4-r5": 1545116527 + }, + "origin": "xf86-video-vesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "linenoise": { + "versions": { + "1.0-r1": 1542304878 + }, + "origin": "linenoise", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblinenoise.so.0=0.0.0" + ] + }, + "dropbear-ssh": { + "versions": { + "2018.76-r2": 1545208976 + }, + "origin": "dropbear", + "dependencies": [ + "dropbear-dbclient", + "!openssh-client", + "dropbear-dbclient=2018.76-r2" + ] + }, + "sqlite-static": { + "versions": { + "3.26.0-r3": 1546255353 + }, + "origin": "sqlite" + }, + "qemu-system-i386": { + "versions": { + "3.1.0-r3": 1551107303 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-i386" + ] + }, + "nagios-plugins-log": { + "versions": { + "2.2.1-r6": 1543933907 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "grep" + ] + }, + "dzen-gadgets": { + "versions": { + "0.9.5-r3": 1545062316 + }, + "origin": "dzen", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dbar", + "cmd:gcpubar", + "cmd:gdbar", + "cmd:textwidth" + ] + }, + "ldns-tools": { + "versions": { + "1.7.0-r2": 1545117118 + }, + "origin": "ldns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libldns.so.2", + "so:libpcap.so.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:ldns-chaos", + "cmd:ldns-compare-zones", + "cmd:ldns-dane", + "cmd:ldns-dpa", + "cmd:ldns-gen-zone", + "cmd:ldns-key2ds", + "cmd:ldns-keyfetcher", + "cmd:ldns-keygen", + "cmd:ldns-mx", + "cmd:ldns-notify", + "cmd:ldns-nsec3-hash", + "cmd:ldns-read-zone", + "cmd:ldns-resolver", + "cmd:ldns-revoke", + "cmd:ldns-rrsig", + "cmd:ldns-signzone", + "cmd:ldns-test-edns", + "cmd:ldns-testns", + "cmd:ldns-update", + "cmd:ldns-verify-zone", + "cmd:ldns-version", + "cmd:ldns-walk", + "cmd:ldns-zcat", + "cmd:ldns-zsplit", + "cmd:ldnsd" + ] + }, + "py-flask-assets": { + "versions": { + "0.12-r0": 1545214106 + }, + "origin": "py-flask-assets", + "dependencies": [ + "python2", + "py-flask", + "py-webassets" + ] + }, + "abuild": { + "versions": { + "3.3.1-r0": 1551786762 + }, + "origin": "abuild", + "dependencies": [ + "fakeroot", + "sudo", + "pax-utils", + "openssl", + "apk-tools>=2.0.7-r1", + "libc-utils", + "attr", + "tar", + "pkgconf", + "patch", + "lzip", + "curl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:abuild", + "cmd:abuild-addgroup", + "cmd:abuild-adduser", + "cmd:abuild-apk", + "cmd:abuild-fetch", + "cmd:abuild-gzsplit", + "cmd:abuild-keygen", + "cmd:abuild-rmtemp", + "cmd:abuild-sign", + "cmd:abuild-sudo", + "cmd:abuild-tar", + "cmd:abump", + "cmd:apkgrel", + "cmd:buildlab", + "cmd:checkapk", + "cmd:newapkbuild" + ] + }, + "xf86-input-libinput-dev": { + "versions": { + "0.28.1-r0": 1545207900 + }, + "origin": "xf86-input-libinput", + "dependencies": [ + "libinput-dev", + "xorg-server-dev", + "pkgconfig" + ], + "provides": [ + "pc:xorg-libinput=0.28.1" + ] + }, + "kyua-doc": { + "versions": { + "0.13-r3": 1542304932 + }, + "origin": "kyua" + }, + "openldap-overlay-collect": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "nsd-dbg": { + "versions": { + "4.1.26-r0": 1546948337 + }, + "origin": "nsd" + }, + "lua5.2-posixtz": { + "versions": { + "0.5-r1": 1543928315 + }, + "origin": "lua-posixtz", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "pekwm": { + "versions": { + "0.1.17-r3": 1545069433 + }, + "origin": "pekwm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXpm.so.4", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpng16.so.16", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:pekwm" + ] + }, + "jq": { + "versions": { + "1.6-r0": 1544000550 + }, + "origin": "jq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libonig.so.5" + ], + "provides": [ + "so:libjq.so.1=1.0.4", + "cmd:jq" + ] + }, + "perl-ipc-sharelite": { + "versions": { + "0.17-r2": 1543222928 + }, + "origin": "perl-ipc-sharelite", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "sudo-dev": { + "versions": { + "1.8.25_p1-r2": 1542304827 + }, + "origin": "sudo" + }, + "openssh-keysign": { + "versions": { + "7.9_p1-r5": 1556034593 + }, + "origin": "openssh", + "dependencies": [ + "openssh-client", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libz.so.1" + ] + }, + "perl-net-smtp-tls-butmaintained": { + "versions": { + "0.24-r0": 1545235364 + }, + "origin": "perl-net-smtp-tls-butmaintained", + "dependencies": [ + "perl-net-ssleay", + "perl-io-socket-ssl", + "perl-digest-hmac" + ] + }, + "xf86-video-nouveau": { + "versions": { + "1.0.15-r3": 1545299946 + }, + "origin": "xf86-video-nouveau", + "dependencies": [ + "mesa-dri-nouveau", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_nouveau.so.2", + "so:libudev.so.1" + ] + }, + "weechat-ruby": { + "versions": { + "2.3-r0": 1545299934 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.5" + ] + }, + "perl-html-formattext-withlinks-andtables-doc": { + "versions": { + "0.07-r0": 1545075973 + }, + "origin": "perl-html-formattext-withlinks-andtables" + }, + "oniguruma": { + "versions": { + "6.9.1-r0": 1545858473 + }, + "origin": "oniguruma", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libonig.so.5=5.0.0" + ] + }, + "glib-static": { + "versions": { + "2.58.1-r2": 1545290824 + }, + "origin": "glib", + "dependencies": [ + "gettext-static" + ] + }, + "py-cffi": { + "versions": { + "1.11.5-r0": 1543924872 + }, + "origin": "py-cffi" + }, + "linux-firmware-emi62": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "boost-date_time": { + "versions": { + "1.67.0-r2": 1542823631 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_date_time-mt.so.1.67.0=1.67.0", + "so:libboost_date_time.so.1.67.0=1.67.0" + ] + }, + "py2-phonenumbers": { + "versions": { + "8.10.4-r0": 1548490629 + }, + "origin": "py-phonenumbers", + "dependencies": [ + "python2" + ] + }, + "py3-more-itertools": { + "versions": { + "4.1.0-r0": 1542824886 + }, + "origin": "py-more-itertools", + "dependencies": [ + "python3" + ] + }, + "openssl": { + "versions": { + "1.1.1b-r1": 1552660176 + }, + "origin": "openssl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:openssl" + ] + }, + "perl-convert-tnef-doc": { + "versions": { + "0.18-r1": 1544001410 + }, + "origin": "perl-convert-tnef" + }, + "bitlbee": { + "versions": { + "3.5.1-r4": 1543248421 + }, + "origin": "bitlbee", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:bitlbee" + ] + }, + "docbook2x-doc": { + "versions": { + "0.8.8-r6": 1542893185 + }, + "origin": "docbook2x" + }, + "kmod-dev": { + "versions": { + "24-r1": 1542845353 + }, + "origin": "kmod", + "dependencies": [ + "kmod=24-r1", + "pkgconfig" + ], + "provides": [ + "pc:libkmod=24" + ] + }, + "py-hoedown": { + "versions": { + "0.2.3-r1": 1545116563 + }, + "origin": "py-hoedown", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:hoedownpy" + ] + }, + "xspice": { + "versions": { + "0.1.5-r5": 1545292751 + }, + "origin": "xf86-video-qxl", + "dependencies": [ + "python2", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libspice-server.so.1" + ], + "provides": [ + "cmd:Xspice" + ] + }, + "mcpp-dev": { + "versions": { + "2.7.2-r1": 1542883471 + }, + "origin": "mcpp", + "dependencies": [ + "mcpp-libs=2.7.2-r1" + ] + }, + "giflib-dev": { + "versions": { + "5.1.4-r2": 1543077198 + }, + "origin": "giflib", + "dependencies": [ + "giflib=5.1.4-r2" + ] + }, + "libcap-ng-utils": { + "versions": { + "0.7.9-r1": 1545918386 + }, + "origin": "libcap-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0" + ], + "provides": [ + "cmd:captest", + "cmd:filecap", + "cmd:netcap", + "cmd:pscap" + ] + }, + "perl-html-mason-psgihandler": { + "versions": { + "0.53-r0": 1545299878 + }, + "origin": "perl-html-mason-psgihandler", + "dependencies": [ + "perl-cgi-psgi", + "perl-html-mason" + ] + }, + "perl-cgi-fast": { + "versions": { + "2.13-r0": 1543242214 + }, + "origin": "perl-cgi-fast", + "dependencies": [ + "perl-cgi", + "perl-fcgi" + ] + }, + "testdisk-doc": { + "versions": { + "7.0-r4": 1544001241 + }, + "origin": "testdisk" + }, + "libshout-dev": { + "versions": { + "2.4.1-r5": 1545117135 + }, + "origin": "libshout", + "dependencies": [ + "libshout=2.4.1-r5", + "pc:ogg", + "pc:speex", + "pc:theora", + "pc:vorbis", + "pkgconfig" + ], + "provides": [ + "pc:shout=2.4.1" + ] + }, + "openldap-back-ldap": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "kbd-vlock": { + "versions": { + "2.0.4-r3": 1545215191 + }, + "origin": "kbd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0", + "so:libpam_misc.so.0" + ], + "provides": [ + "cmd:vlock" + ] + }, + "squid-lang-he": { + "versions": { + "4.4-r1": 1545216326 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "unifont-dev": { + "versions": { + "11.0.01-r1": 1543077262 + }, + "origin": "unifont" + }, + "lua-rrd": { + "versions": { + "1.7.0-r0": 1542924803 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5", + "so:librrd.so.8" + ] + }, + "lmdb-tools": { + "versions": { + "0.9.23-r0": 1547025175 + }, + "origin": "lmdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblmdb.so.0" + ], + "provides": [ + "cmd:mdb_copy", + "cmd:mdb_dump", + "cmd:mdb_load", + "cmd:mdb_stat" + ] + }, + "lua5.3-mqtt-publish": { + "versions": { + "0.3-r0": 1542820948 + }, + "origin": "lua-mqtt-publish", + "dependencies": [ + "lua5.3-mosquitto" + ] + }, + "py-gnome-gnomevfs": { + "versions": { + "2.28.1-r5": 1545301294 + }, + "origin": "py-gnome", + "dependencies": [ + "gnome-vfs", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgnomevfs-2.so.0", + "so:libgobject-2.0.so.0", + "so:libpython2.7.so.1.0" + ] + }, + "perl-sys-mmap": { + "versions": { + "0.19-r1": 1545075424 + }, + "origin": "perl-sys-mmap", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "libcmph": { + "versions": { + "2.0-r1": 1544000441 + }, + "origin": "cmph", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcmph.so.0=0.0.0" + ] + }, + "irqbalance": { + "versions": { + "1.5.0-r0": 1545837194 + }, + "origin": "irqbalance", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:irqbalance", + "cmd:irqbalance-ui" + ] + }, + "py3-setuptools": { + "versions": { + "40.6.3-r0": 1547652242 + }, + "origin": "py-setuptools", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:easy_install-3.6" + ] + }, + "rlog": { + "versions": { + "1.4-r4": 1545061037 + }, + "origin": "rlog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:librlog.so.5=5.0.0" + ] + }, + "lua5.1-lzlib": { + "versions": { + "0.4.3-r0": 1542845777 + }, + "origin": "lua-lzlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ] + }, + "nagios-plugins-nwstat": { + "versions": { + "2.2.1-r6": 1543933909 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "cutter": { + "versions": { + "1.04-r1": 1543927857 + }, + "origin": "cutter", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cutter" + ] + }, + "nginx-mod-http-redis2": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "cmake-doc": { + "versions": { + "3.13.0-r0": 1542820577 + }, + "origin": "cmake" + }, + "cvechecker-doc": { + "versions": { + "3.9-r1": 1543249725 + }, + "origin": "cvechecker" + }, + "py-gobject3-dev": { + "versions": { + "3.28.2-r0": 1543925245 + }, + "origin": "py-gobject3", + "dependencies": [ + "pc:gobject-2.0", + "pc:libffi", + "pkgconfig" + ], + "provides": [ + "pc:pygobject-3.0=3.28.2" + ] + }, + "busybox": { + "versions": { + "1.29.3-r10": 1548315956 + }, + "origin": "busybox", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "/bin/sh", + "cmd:busybox", + "cmd:sh" + ] + }, + "gnupg-scdaemon": { + "versions": { + "2.2.12-r0": 1545746222 + }, + "origin": "gnupg", + "dependencies": [ + "gnupg", + "/bin/sh", + "so:libassuan.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libgpg-error.so.0", + "so:libksba.so.8", + "so:libnpth.so.0", + "so:libusb-1.0.so.0" + ] + }, + "libgphoto2-doc": { + "versions": { + "2.5.16-r0": 1545116802 + }, + "origin": "libgphoto2" + }, + "perl": { + "versions": { + "5.26.3-r0": 1543998674 + }, + "origin": "perl", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libperl.so=0", + "cmd:perl", + "cmd:perl5.26.3", + "cmd:perldoc", + "cmd:pod2html", + "cmd:pod2man", + "cmd:pod2text", + "cmd:pod2usage", + "cmd:podchecker", + "cmd:podselect" + ] + }, + "perl-lwp-mediatypes": { + "versions": { + "6.02-r1": 1542821765 + }, + "origin": "perl-lwp-mediatypes", + "dependencies": [ + "perl" + ] + }, + "libasr-dev": { + "versions": { + "1.0.2-r9": 1543933802 + }, + "origin": "libasr", + "dependencies": [ + "libasr=1.0.2-r9" + ] + }, + "drbd-vanilla": { + "versions": { + "4.19.41-r0": 1557400098 + }, + "origin": "drbd-vanilla", + "dependencies": [ + "linux-vanilla=4.19.41-r0" + ] + }, + "minizip-dev": { + "versions": { + "1.2.11-r0": 1545075939 + }, + "origin": "minizip", + "dependencies": [ + "minizip=1.2.11-r0", + "pkgconfig" + ], + "provides": [ + "pc:minizip=1.2.11" + ] + }, + "boost-python3": { + "versions": { + "1.67.0-r2": 1542823632 + }, + "origin": "boost", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpython3.6m.so.1.0" + ], + "provides": [ + "so:libboost_python36-mt.so.1.67.0=1.67.0", + "so:libboost_python36.so.1.67.0=1.67.0" + ] + }, + "sircbot": { + "versions": { + "0.4-r2": 1543924833 + }, + "origin": "sircbot", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sircbot", + "cmd:sircbot-send" + ] + }, + "qemu-openrc": { + "versions": { + "0.6.0-r0": 1545215888 + }, + "origin": "qemu-openrc", + "dependencies": [ + "qemu", + "socat", + "/bin/sh" + ], + "provides": [ + "cmd:qemush" + ] + }, + "usb-modeswitch-udev": { + "versions": { + "2.5.2-r0": 1543932711 + }, + "origin": "usb-modeswitch", + "dependencies": [ + "tcl", + "eudev", + "usb-modeswitch=2.5.2-r0" + ], + "provides": [ + "cmd:usb_modeswitch_dispatcher" + ] + }, + "xen-hypervisor": { + "versions": { + "4.11.1-r1": 1545075897 + }, + "origin": "xen" + }, + "perl-hash-multivalue": { + "versions": { + "0.16-r0": 1543226611 + }, + "origin": "perl-hash-multivalue", + "dependencies": [ + "perl" + ] + }, + "perl-test-mockrandom": { + "versions": { + "1.01-r1": 1543238912 + }, + "origin": "perl-test-mockrandom" + }, + "mpt-status": { + "versions": { + "1.2.0-r0": 1545069140 + }, + "origin": "mpt-status", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mpt-status" + ] + }, + "lua5.2-xml": { + "versions": { + "130610-r5": 1543998786 + }, + "origin": "lua-xml", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-date-extract-doc": { + "versions": { + "0.06-r1": 1545163120 + }, + "origin": "perl-date-extract" + }, + "lua-ossl": { + "versions": { + "20180708-r2": 1545076448 + }, + "origin": "lua-ossl" + }, + "libgfortran": { + "versions": { + "8.3.0-r0": 1554745498 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libquadmath.so.0" + ], + "provides": [ + "so:libgfortran.so.5=5.0.0" + ] + }, + "perl-module-metadata": { + "versions": { + "1.000033-r0": 1542845546 + }, + "origin": "perl-module-metadata" + }, + "nginx-mod-http-xslt-filter": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ] + }, + "uwsgi-rrdtool": { + "versions": { + "2.0.17.1-r0": 1545062209 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "scstadmin-doc": { + "versions": { + "2.2.0-r2": 1545223236 + }, + "origin": "scstadmin" + }, + "perl-apache-logformat-compiler": { + "versions": { + "0.32-r0": 1544792423 + }, + "origin": "perl-apache-logformat-compiler", + "dependencies": [ + "perl-posix-strftime-compiler" + ] + }, + "perl-uri-doc": { + "versions": { + "1.74-r0": 1542821786 + }, + "origin": "perl-uri" + }, + "gstreamer0.10-tools": { + "versions": { + "0.10.36-r3": 1544000752 + }, + "origin": "gstreamer0.10", + "dependencies": [ + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-0.10.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:gst-feedback", + "cmd:gst-feedback-0.10", + "cmd:gst-inspect", + "cmd:gst-inspect-0.10", + "cmd:gst-launch", + "cmd:gst-launch-0.10", + "cmd:gst-typefind", + "cmd:gst-typefind-0.10", + "cmd:gst-xmlinspect", + "cmd:gst-xmlinspect-0.10", + "cmd:gst-xmllaunch", + "cmd:gst-xmllaunch-0.10" + ] + }, + "postgresql-pltcl": { + "versions": { + "11.2-r0": 1554274176 + }, + "origin": "postgresql", + "dependencies": [ + "pgtcl", + "so:libc.musl-x86_64.so.1", + "so:libtcl8.6.so" + ] + }, + "lua5.3-augeas": { + "versions": { + "0.1.2-r4": 1543246061 + }, + "origin": "lua-augeas", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1" + ] + }, + "alsaconf": { + "versions": { + "1.1.8-r0": 1547112822 + }, + "origin": "alsa-utils", + "dependencies": [ + "alsa-utils", + "bash" + ], + "provides": [ + "cmd:alsaconf" + ] + }, + "cramfs": { + "versions": { + "1.1-r2": 1545165278 + }, + "origin": "cramfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:cramfsck", + "cmd:mkcramfs" + ] + }, + "f2fs-tools-dev": { + "versions": { + "1.12.0-r0": 1546344802 + }, + "origin": "f2fs-tools", + "dependencies": [ + "f2fs-tools-libs=1.12.0-r0" + ] + }, + "hunspell-pt-br": { + "versions": { + "20131017-r0": 1545300017 + }, + "origin": "hunspell-pt-br" + }, + "ulogd": { + "versions": { + "2.0.7-r0": 1545300897 + }, + "origin": "ulogd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnetfilter_acct.so.1", + "so:libnetfilter_conntrack.so.3", + "so:libnetfilter_log.so.1", + "so:libnfnetlink.so.0" + ], + "provides": [ + "cmd:ulogd" + ] + }, + "lcms2": { + "versions": { + "2.9-r1": 1542824190 + }, + "origin": "lcms2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblcms2.so.2=2.0.8" + ] + }, + "linux-firmware-cis": { + "versions": { + "20190322-r0": 1554980653 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "collectd": { + "versions": { + "5.8.0-r3": 1546422679 + }, + "origin": "collectd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libnetsnmpagent.so.35", + "so:libyajl.so.2" + ], + "provides": [ + "cmd:collectd", + "cmd:collectdmon" + ] + }, + "xorriso": { + "versions": { + "1.4.8-r0": 1544797305 + }, + "origin": "libisoburn", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libisoburn.so.1" + ], + "provides": [ + "cmd:mkisofs", + "cmd:osirrox", + "cmd:xorrecord", + "cmd:xorriso", + "cmd:xorrisofs" + ] + }, + "lxc-libs": { + "versions": { + "3.1.0-r1": 1549966161 + }, + "origin": "lxc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libseccomp.so.2" + ], + "provides": [ + "so:liblxc.so.1=1.5.0" + ] + }, + "libnfs-dev": { + "versions": { + "3.0.0-r0": 1544799293 + }, + "origin": "libnfs", + "dependencies": [ + "libnfs=3.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnfs=3.0.0" + ] + }, + "mp3splt-gtk-doc": { + "versions": { + "0.9.2-r2": 1545069036 + }, + "origin": "mp3splt-gtk" + }, + "py3-six": { + "versions": { + "1.11.0-r0": 1542823019 + }, + "origin": "py-six", + "dependencies": [ + "python3" + ] + }, + "charybdis-dev": { + "versions": { + "3.5.6-r1": 1542883316 + }, + "origin": "charybdis", + "dependencies": [ + "pkgconfig" + ] + }, + "libgpg-error": { + "versions": { + "1.33-r0": 1545838267 + }, + "origin": "libgpg-error", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgpg-error.so.0=0.25.0", + "cmd:gpg-error", + "cmd:yat2m" + ] + }, + "perl-extutils-config": { + "versions": { + "0.008-r0": 1542924647 + }, + "origin": "perl-extutils-config" + }, + "py-pillow": { + "versions": { + "5.4.1-r0": 1548112615 + }, + "origin": "py-pillow", + "dependencies": [ + "py-olefile" + ] + }, + "gawk-doc": { + "versions": { + "4.2.1-r0": 1542301294 + }, + "origin": "gawk" + }, + "lua-yaml": { + "versions": { + "1.1.2-r1": 1544000073 + }, + "origin": "lua-yaml" + }, + "jbig2dec": { + "versions": { + "0.15-r0": 1545207929 + }, + "origin": "jbig2dec", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libjbig2dec.so.0=0.0.0", + "cmd:jbig2dec" + ] + }, + "tango-icon-theme": { + "versions": { + "0.8.90-r4": 1545235345 + }, + "origin": "tango-icon-theme" + }, + "libetpan-dev": { + "versions": { + "1.9.2-r0": 1548063822 + }, + "origin": "libetpan", + "dependencies": [ + "cyrus-sasl-dev", + "db-dev", + "libetpan=1.9.2-r0" + ], + "provides": [ + "cmd:libetpan-config" + ] + }, + "zstd-libs": { + "versions": { + "1.3.8-r0": 1546966444 + }, + "origin": "zstd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libzstd.so.1=1.3.8" + ] + }, + "collectd-log_logstash": { + "versions": { + "5.8.0-r3": 1546422678 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libyajl.so.2" + ] + }, + "perl-super": { + "versions": { + "1.20141117-r0": 1543932005 + }, + "origin": "perl-super", + "dependencies": [ + "perl" + ] + }, + "xf86-input-synaptics-doc": { + "versions": { + "1.9.1-r1": 1545154609 + }, + "origin": "xf86-input-synaptics" + }, + "xautolock-doc": { + "versions": { + "2.2-r4": 1543932088 + }, + "origin": "xautolock" + }, + "devicemaster-linux-doc": { + "versions": { + "7.15-r0": 1543999030 + }, + "origin": "devicemaster-linux" + }, + "perl-ipc-run3-doc": { + "versions": { + "0.048-r0": 1542973046 + }, + "origin": "perl-ipc-run3" + }, + "openldap-overlay-deref": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "py-yaml": { + "versions": { + "4.1-r0": 1544798590 + }, + "origin": "py-yaml" + }, + "squid-lang-uk": { + "versions": { + "4.4-r1": 1545216330 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "gross-doc": { + "versions": { + "1.0.2-r11": 1545062670 + }, + "origin": "gross" + }, + "cpufreqd-doc": { + "versions": { + "2.4.2-r4": 1543998859 + }, + "origin": "cpufreqd" + }, + "rsyslog-rabbitmq": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:librabbitmq.so.4" + ], + "provides": [ + "rsyslog-omrabbitmq=8.40.0-r3" + ] + }, + "perl-test-refcount": { + "versions": { + "0.08-r0": 1544799031 + }, + "origin": "perl-test-refcount" + }, + "lua5.1-mqtt-publish": { + "versions": { + "0.3-r0": 1542820947 + }, + "origin": "lua-mqtt-publish", + "dependencies": [ + "lua5.1-mosquitto" + ] + }, + "elinks": { + "versions": { + "0.13-r6": 1543934620 + }, + "origin": "elinks", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libexpat.so.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:elinks" + ] + }, + "uwsgi-cheaper_busyness": { + "versions": { + "2.0.17.1-r0": 1545062197 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "busybox-extras": { + "versions": { + "1.29.3-r10": 1548315956 + }, + "origin": "busybox", + "dependencies": [ + "busybox", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:busybox-extras" + ] + }, + "perl-devel-symdump-doc": { + "versions": { + "2.18-r0": 1542845656 + }, + "origin": "perl-devel-symdump" + }, + "py3-lockfile": { + "versions": { + "0.12.2-r0": 1544799212 + }, + "origin": "py-lockfile", + "dependencies": [ + "python3" + ] + }, + "msmtp-doc": { + "versions": { + "1.8.1-r1": 1545062528 + }, + "origin": "msmtp" + }, + "freeradius-rest": { + "versions": { + "3.0.17-r5": 1556201989 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libjson-c.so.4" + ], + "provides": [ + "so:rlm_rest.so=0" + ] + }, + "lxsession": { + "versions": { + "0.5.3-r0": 1545299866 + }, + "origin": "lxsession", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpolkit-agent-1.so.0", + "so:libpolkit-gobject-1.so.0", + "so:libunique-1.0.so.0" + ], + "provides": [ + "cmd:lxclipboard", + "cmd:lxlock", + "cmd:lxpolkit", + "cmd:lxsession", + "cmd:lxsession-db", + "cmd:lxsession-default", + "cmd:lxsession-default-apps", + "cmd:lxsession-default-terminal", + "cmd:lxsession-edit", + "cmd:lxsession-logout", + "cmd:lxsession-xdg-autostart", + "cmd:lxsettings-daemon" + ] + }, + "pingu-doc": { + "versions": { + "1.5-r2": 1544000080 + }, + "origin": "pingu" + }, + "audit-openrc": { + "versions": { + "2.8.4-r0": 1543245856 + }, + "origin": "audit" + }, + "kmod": { + "versions": { + "24-r1": 1542845356 + }, + "origin": "kmod", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:liblzma.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libkmod.so.2=2.3.2", + "cmd:depmod", + "cmd:insmod", + "cmd:kmod", + "cmd:lsmod", + "cmd:modinfo", + "cmd:modprobe", + "cmd:rmmod" + ] + }, + "linux-firmware-ess": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "tcl": { + "versions": { + "8.6.9-r0": 1545915062 + }, + "origin": "tcl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libtcl8.6.so=0", + "cmd:tclsh", + "cmd:tclsh8.6" + ] + }, + "nginx-mod-http-set-misc": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "nginx-mod-devel-kit", + "so:libc.musl-x86_64.so.1" + ] + }, + "freeradius-dev": { + "versions": { + "3.0.17-r5": 1556201987 + }, + "origin": "freeradius" + }, + "qt-dev": { + "versions": { + "4.8.7-r11": 1545075085 + }, + "origin": "qt", + "dependencies": [ + "mesa-dev", + "libice-dev", + "libsm-dev", + "libx11-dev", + "libxext-dev", + "libxrender-dev", + "alsa-lib-dev", + "openssl-dev", + "fontconfig-dev", + "freetype-dev", + "glib-dev", + "libpng-dev", + "zlib-dev", + "sqlite-dev", + "dbus-dev", + "pkgconfig", + "qt-webkit=4.8.7-r11", + "qt-x11=4.8.7-r11", + "qt=4.8.7-r11", + "so:libQt3Support.so.4", + "so:libQtCore.so.4", + "so:libQtDBus.so.4", + "so:libQtDeclarative.so.4", + "so:libQtDesigner.so.4", + "so:libQtDesignerComponents.so.4", + "so:libQtGui.so.4", + "so:libQtHelp.so.4", + "so:libQtNetwork.so.4", + "so:libQtWebKit.so.4", + "so:libQtXml.so.4", + "so:libQtXmlPatterns.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "pc:Qt3Support=4.8.7", + "pc:QtCLucene=4.8.7", + "pc:QtCore=4.8.7", + "pc:QtDBus=4.8.7", + "pc:QtDeclarative=4.8.7", + "pc:QtDesigner=4.8.7", + "pc:QtDesignerComponents=4.8.7", + "pc:QtGui=4.8.7", + "pc:QtHelp=4.8.7", + "pc:QtMultimedia=4.8.7", + "pc:QtNetwork=4.8.7", + "pc:QtOpenGL=4.8.7", + "pc:QtScript=4.8.7", + "pc:QtScriptTools=4.8.7", + "pc:QtSql=4.8.7", + "pc:QtSvg=4.8.7", + "pc:QtTest=4.8.7", + "pc:QtUiTools=4.8.7", + "pc:QtWebKit=4.9.4", + "pc:QtXml=4.8.7", + "pc:QtXmlPatterns=4.8.7", + "cmd:designer", + "cmd:lconvert", + "cmd:linguist", + "cmd:lrelease", + "cmd:lupdate", + "cmd:moc", + "cmd:pixeltool", + "cmd:qcollectiongenerator", + "cmd:qdbuscpp2xml", + "cmd:qdbusxml2cpp", + "cmd:qdoc3", + "cmd:qhelpconverter", + "cmd:qhelpgenerator", + "cmd:qmake", + "cmd:qt3to4", + "cmd:qttracereplay", + "cmd:rcc", + "cmd:uic", + "cmd:uic3", + "cmd:xmlpatterns", + "cmd:xmlpatternsvalidator" + ] + }, + "wxgtk2.8-base": { + "versions": { + "2.8.12.1-r4": 1545075287 + }, + "origin": "wxgtk2.8", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libwx_baseu-2.8.so.0=0.8.0", + "so:libwx_baseu_net-2.8.so.0=0.8.0", + "so:libwx_baseu_xml-2.8.so.0=0.8.0" + ] + }, + "libidl-doc": { + "versions": { + "0.8.14-r2": 1543223346 + }, + "origin": "libidl" + }, + "gnutls-doc": { + "versions": { + "3.6.7-r0": 1555049964 + }, + "origin": "gnutls" + }, + "less-doc": { + "versions": { + "530-r0": 1542301312 + }, + "origin": "less" + }, + "mosquitto-openrc": { + "versions": { + "1.5.6-r0": 1549970968 + }, + "origin": "mosquitto" + }, + "lvm2-extra": { + "versions": { + "2.02.182-r0": 1543928934 + }, + "origin": "lvm2", + "dependencies": [ + "bash", + "coreutils" + ], + "provides": [ + "cmd:blkdeactivate", + "cmd:fsadm", + "cmd:lvmconf", + "cmd:lvmdump" + ] + }, + "freeradius-redis": { + "versions": { + "3.0.17-r5": 1556201989 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.14" + ], + "provides": [ + "so:rlm_redis.so=0", + "so:rlm_rediswho.so=0" + ] + }, + "avahi-ui": { + "versions": { + "0.6.31-r7": 1545163747 + }, + "origin": "avahi-ui", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-glib.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libavahi-ui.so.0=0.1.4" + ] + }, + "py3-py": { + "versions": { + "1.5.3-r0": 1542824876 + }, + "origin": "py-py", + "dependencies": [ + "python3" + ] + }, + "acf-alpine-baselayout": { + "versions": { + "0.13.2-r0": 1545116952 + }, + "origin": "acf-alpine-baselayout", + "dependencies": [ + "acf-core", + "lua-json4", + "lua-posix" + ] + }, + "perl-module-runtime-doc": { + "versions": { + "0.016-r1": 1542845694 + }, + "origin": "perl-module-runtime" + }, + "gdk-pixbuf-dev": { + "versions": { + "2.36.11-r2": 1543253989 + }, + "origin": "gdk-pixbuf", + "dependencies": [ + "gdk-pixbuf=2.36.11-r2", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:libpng16", + "pkgconfig" + ], + "provides": [ + "pc:gdk-pixbuf-2.0=2.36.11", + "pc:gdk-pixbuf-xlib-2.0=2.36.11" + ] + }, + "speex-doc": { + "versions": { + "1.2.0-r0": 1544799248 + }, + "origin": "speex" + }, + "nagios-web": { + "versions": { + "3.5.1-r4": 1543924480 + }, + "origin": "nagios", + "dependencies": [ + "perl" + ] + }, + "uriparser": { + "versions": { + "0.9.1-r0": 1548111967 + }, + "origin": "uriparser", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liburiparser.so.1=1.0.24", + "cmd:uriparse" + ] + }, + "libunique3": { + "versions": { + "3.0.2-r0": 1543927464 + }, + "origin": "libunique3", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk-3.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0" + ], + "provides": [ + "so:libunique-3.0.so.0=0.0.2" + ] + }, + "collectd-ipmi": { + "versions": { + "5.8.0-r3": 1546422678 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libOpenIPMI.so.0", + "so:libOpenIPMIpthread.so.0", + "so:libc.musl-x86_64.so.1" + ] + }, + "subversion": { + "versions": { + "1.11.1-r0": 1548692376 + }, + "origin": "subversion", + "dependencies": [ + "/bin/sh", + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libsvn_client-1.so.0", + "so:libsvn_delta-1.so.0", + "so:libsvn_diff-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_fs_fs-1.so.0", + "so:libsvn_ra-1.so.0", + "so:libsvn_ra_svn-1.so.0", + "so:libsvn_repos-1.so.0", + "so:libsvn_subr-1.so.0", + "so:libsvn_wc-1.so.0" + ], + "provides": [ + "cmd:svn", + "cmd:svnadmin", + "cmd:svnbench", + "cmd:svndumpfilter", + "cmd:svnfsfs", + "cmd:svnlook", + "cmd:svnmucc", + "cmd:svnrdump", + "cmd:svnserve", + "cmd:svnsync", + "cmd:svnversion" + ] + }, + "net-tools-dbg": { + "versions": { + "1.60_git20140218-r2": 1545216351 + }, + "origin": "net-tools", + "dependencies": [ + "mii-tool" + ] + }, + "userspace-rcu": { + "versions": { + "0.10.1-r0": 1543249590 + }, + "origin": "userspace-rcu", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liburcu-bp.so.6=6.0.0", + "so:liburcu-cds.so.6=6.0.0", + "so:liburcu-common.so.6=6.0.0", + "so:liburcu-mb.so.6=6.0.0", + "so:liburcu-qsbr.so.6=6.0.0", + "so:liburcu-signal.so.6=6.0.0", + "so:liburcu.so.6=6.0.0" + ] + }, + "python2-wininst": { + "versions": { + "2.7.16-r1": 1557171399 + }, + "origin": "python2" + }, + "py-google-api-python-client": { + "versions": { + "1.7.7-r0": 1546936521 + }, + "origin": "py-google-api-python-client", + "dependencies": [ + "py-httplib2", + "py-oauth2client", + "py-uritemplate", + "py-six" + ] + }, + "perl-db_file": { + "versions": { + "1.843-r0": 1545061210 + }, + "origin": "perl-db_file", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ] + }, + "py-boto": { + "versions": { + "2.49.0-r0": 1544000446 + }, + "origin": "py-boto", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:asadmin", + "cmd:bundle_image", + "cmd:cfadmin", + "cmd:cq", + "cmd:cwutil", + "cmd:dynamodb_dump", + "cmd:dynamodb_load", + "cmd:elbadmin", + "cmd:fetch_file", + "cmd:glacier", + "cmd:instance_events", + "cmd:kill_instance", + "cmd:launch_instance", + "cmd:list_instances", + "cmd:lss3", + "cmd:mturk", + "cmd:pyami_sendmail", + "cmd:route53", + "cmd:s3put", + "cmd:sdbadmin", + "cmd:taskadmin" + ] + }, + "perl-file-which-doc": { + "versions": { + "1.22-r0": 1545214292 + }, + "origin": "perl-file-which" + }, + "procps-lang": { + "versions": { + "3.3.15-r0": 1543934062 + }, + "origin": "procps" + }, + "py3-icu": { + "versions": { + "1.9.8-r2": 1545165267 + }, + "origin": "py-icu", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libicui18n.so.62", + "so:libicuuc.so.62", + "so:libpython3.6m.so.1.0", + "so:libstdc++.so.6" + ] + }, + "py3-tox": { + "versions": { + "3.2.1-r1": 1545060602 + }, + "origin": "py-tox", + "dependencies": [ + "py3-py", + "py3-pluggy", + "py3-argparse", + "python3" + ], + "provides": [ + "cmd:tox", + "cmd:tox-quickstart" + ] + }, + "aspell-fr": { + "versions": { + "0.50_p3-r1": 1545116568 + }, + "origin": "aspell-fr" + }, + "json-glib": { + "versions": { + "1.4.4-r0": 1545837175 + }, + "origin": "json-glib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libjson-glib-1.0.so.0=0.400.4" + ] + }, + "graphite2-static": { + "versions": { + "1.3.12-r1": 1542823993 + }, + "origin": "graphite2" + }, + "cairo-dev": { + "versions": { + "1.16.0-r1": 1546948314 + }, + "origin": "cairo", + "dependencies": [ + "fontconfig-dev", + "freetype-dev", + "libxrender-dev", + "pixman-dev", + "xcb-util-dev", + "libxext-dev", + "cairo-tools", + "cairo-gobject=1.16.0-r1", + "cairo=1.16.0-r1", + "pc:fontconfig>=2.2.95", + "pc:freetype2>=9.7.3", + "pc:glib-2.0>=2.14", + "pc:gobject-2.0", + "pc:libpng", + "pc:pixman-1>=0.30.0", + "pc:x11", + "pc:xcb-render>=1.6", + "pc:xcb-shm", + "pc:xcb>=1.6", + "pc:xext", + "pc:xrender>=0.6", + "pkgconfig" + ], + "provides": [ + "pc:cairo-fc=1.16.0", + "pc:cairo-ft=1.16.0", + "pc:cairo-gobject=1.16.0", + "pc:cairo-pdf=1.16.0", + "pc:cairo-png=1.16.0", + "pc:cairo-ps=1.16.0", + "pc:cairo-script=1.16.0", + "pc:cairo-svg=1.16.0", + "pc:cairo-tee=1.16.0", + "pc:cairo-xcb-shm=1.16.0", + "pc:cairo-xcb=1.16.0", + "pc:cairo-xlib-xrender=1.16.0", + "pc:cairo-xlib=1.16.0", + "pc:cairo=1.16.0" + ] + }, + "perl-dbd-sqlite-doc": { + "versions": { + "1.62-r0": 1546944025 + }, + "origin": "perl-dbd-sqlite" + }, + "avahi-ui-tools": { + "versions": { + "0.6.31-r7": 1545163746 + }, + "origin": "avahi-ui", + "dependencies": [ + "py-gtk", + "py-dbus", + "py-gdbm", + "py2-avahi", + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-ui-gtk3.so.0", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:avahi-bookmarks", + "cmd:avahi-discover", + "cmd:bshell", + "cmd:bssh", + "cmd:bvnc" + ] + }, + "lua5.1-iconv": { + "versions": { + "7-r1": 1545062767 + }, + "origin": "lua-iconv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "xf86-input-evdev-doc": { + "versions": { + "2.10.6-r0": 1545229727 + }, + "origin": "xf86-input-evdev" + }, + "perl-css-minifier-xs": { + "versions": { + "0.09-r3": 1545062656 + }, + "origin": "perl-css-minifier-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "dialog": { + "versions": { + "1.3.20180621-r0": 1543925866 + }, + "origin": "dialog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:dialog" + ] + }, + "xrdp-dev": { + "versions": { + "0.9.9-r0": 1545859298 + }, + "origin": "xrdp", + "dependencies": [ + "pkgconfig", + "xrdp=0.9.9-r0" + ], + "provides": [ + "pc:libpainter=0.1.1", + "pc:rfxcodec=0.1.4", + "pc:xrdp=0.9.9" + ] + }, + "libcap-ng": { + "versions": { + "0.7.9-r1": 1545918386 + }, + "origin": "libcap-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcap-ng.so.0=0.0.0" + ] + }, + "hicolor-icon-theme": { + "versions": { + "0.17-r0": 1543925362 + }, + "origin": "hicolor-icon-theme" + }, + "acl": { + "versions": { + "2.2.52-r5": 1542304043 + }, + "origin": "acl", + "dependencies": [ + "so:libacl.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chacl", + "cmd:getfacl", + "cmd:setfacl" + ] + }, + "ed": { + "versions": { + "1.14.2-r2": 1542304952 + }, + "origin": "ed", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ed", + "cmd:red" + ] + }, + "perl-authen-sasl-doc": { + "versions": { + "2.16-r1": 1545215984 + }, + "origin": "perl-authen-sasl" + }, + "guile-libs": { + "versions": { + "2.0.14-r0": 1543227920 + }, + "origin": "guile", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgc.so.1", + "so:libgmp.so.10", + "so:libltdl.so.7", + "so:libunistring.so.2" + ], + "provides": [ + "so:libguile-2.0.so.22=22.8.1" + ] + }, + "sdl2_ttf-dev": { + "versions": { + "2.0.14-r1": 1543934466 + }, + "origin": "sdl2_ttf", + "dependencies": [ + "freetype-dev", + "sdl2-dev", + "pc:sdl2>=2.0.0", + "pkgconfig", + "sdl2_ttf=2.0.14-r1" + ], + "provides": [ + "pc:SDL2_ttf=2.0.14" + ] + }, + "nginx-mod-stream-geoip": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "mesa-dri-intel": { + "versions": { + "18.1.7-r2": 1554455972 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_intel.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "taskd": { + "versions": { + "1.1.0-r4": 1545163049 + }, + "origin": "taskd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:taskd", + "cmd:taskdctl" + ] + }, + "zlib": { + "versions": { + "1.2.11-r1": 1542300123 + }, + "origin": "zlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libz.so.1=1.2.11" + ] + }, + "heimdal-openrc": { + "versions": { + "7.5.0-r2": 1542819374 + }, + "origin": "heimdal", + "dependencies": [ + "krb5-conf" + ] + }, + "py3-django-tables2": { + "versions": { + "1.21.2-r0": 1543925703 + }, + "origin": "py-django-tables2", + "dependencies": [ + "py3-django", + "py3-six", + "python3" + ] + }, + "libgpg-error-dev": { + "versions": { + "1.33-r0": 1545838267 + }, + "origin": "libgpg-error", + "dependencies": [ + "libgpg-error=1.33-r0", + "pkgconfig" + ], + "provides": [ + "pc:gpg-error=1.33", + "cmd:gpg-error-config", + "cmd:gpgrt-config" + ] + }, + "perl-dbi": { + "versions": { + "1.642-r0": 1544792614 + }, + "origin": "perl-dbi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dbilogstrip", + "cmd:dbiprof", + "cmd:dbiproxy" + ] + }, + "gstreamer-tools": { + "versions": { + "1.14.4-r0": 1543254159 + }, + "origin": "gstreamer", + "dependencies": [ + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:gst-inspect-1.0", + "cmd:gst-launch-1.0", + "cmd:gst-stats-1.0", + "cmd:gst-typefind-1.0" + ] + }, + "xcmsdb-doc": { + "versions": { + "1.0.5-r0": 1545293076 + }, + "origin": "xcmsdb" + }, + "mosquitto-dev": { + "versions": { + "1.5.6-r0": 1549970968 + }, + "origin": "mosquitto", + "dependencies": [ + "mosquitto-libs++=1.5.6-r0", + "mosquitto-libs=1.5.6-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmosquitto=1.5.6", + "pc:libmosquittopp=1.5.6" + ] + }, + "asterisk-dahdi": { + "versions": { + "15.7.1-r0": 1546247586 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpri.so.1.4", + "so:libtonezone.so.2" + ] + }, + "lua5.2-stringy": { + "versions": { + "0.5.0-r1": 1545116589 + }, + "origin": "lua-stringy", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "lutok-dev": { + "versions": { + "0.4-r2": 1542304897 + }, + "origin": "lutok", + "dependencies": [ + "lua5.3-dev", + "lutok=0.4-r2", + "pkgconfig" + ], + "provides": [ + "pc:lutok=0.4" + ] + }, + "py3-requests-oauthlib": { + "versions": { + "0.8.0-r1": 1545062565 + }, + "origin": "py-requests-oauthlib", + "dependencies": [ + "py3-oauthlib", + "py3-requests", + "python3" + ] + }, + "libinput-dev": { + "versions": { + "1.12.4-r0": 1545734494 + }, + "origin": "libinput", + "dependencies": [ + "libinput-libs=1.12.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:libinput=1.12.4" + ] + }, + "wv-dev": { + "versions": { + "1.2.9-r3": 1545300661 + }, + "origin": "wv", + "dependencies": [ + "pc:libgsf-1>=1.13.0", + "pkgconfig", + "wv=1.2.9-r3" + ], + "provides": [ + "pc:wv-1.0=1.2.9" + ] + }, + "gperf-doc": { + "versions": { + "3.1-r2": 1542822039 + }, + "origin": "gperf" + }, + "slang-dev": { + "versions": { + "2.3.2-r0": 1543924688 + }, + "origin": "slang", + "dependencies": [ + "pkgconfig", + "slang=2.3.2-r0" + ], + "provides": [ + "pc:slang=2.3.2" + ] + }, + "swish-e-doc": { + "versions": { + "2.4.7-r8": 1545163773 + }, + "origin": "swish-e" + }, + "frotz-doc": { + "versions": { + "2.44-r0": 1545293066 + }, + "origin": "frotz" + }, + "haveged-openrc": { + "versions": { + "1.9.4-r4": 1557129010 + }, + "origin": "haveged" + }, + "lua-ldap": { + "versions": { + "1.2.3-r5": 1543223255 + }, + "origin": "lua-ldap" + }, + "samba-doc": { + "versions": { + "4.8.11-r1": 1555334972 + }, + "origin": "samba" + }, + "audit-static": { + "versions": { + "2.8.4-r0": 1543245855 + }, + "origin": "audit" + }, + "rsyslog-mmfields": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-gtk": { + "versions": { + "2.24.0-r15": 1543927630 + }, + "origin": "py-gtk", + "dependencies": [ + "py2-cairo", + "py-gobject", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglade-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0" + ], + "provides": [ + "cmd:pygtk-codegen-2.0" + ] + }, + "iwlwifi-5150-ucode": { + "versions": { + "8.24.2.2-r1": 1545208933 + }, + "origin": "iwlwifi-5150-ucode" + }, + "mercurial-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308087, + "4.9.1-r0": 1557160461 + }, + "origin": "mercurial", + "dependencies": [ + "zsh" + ] + }, + "acf-asterisk": { + "versions": { + "0.7.0-r2": 1545073807 + }, + "origin": "acf-asterisk", + "dependencies": [ + "acf-core", + "asterisk" + ] + }, + "perl-test-mocktime-doc": { + "versions": { + "0.17-r0": 1544792416 + }, + "origin": "perl-test-mocktime" + }, + "util-linux-dev": { + "versions": { + "2.33-r0": 1545307436 + }, + "origin": "util-linux", + "dependencies": [ + "libblkid=2.33-r0", + "libfdisk=2.33-r0", + "libmount=2.33-r0", + "libsmartcols=2.33-r0", + "libuuid=2.33-r0", + "pkgconfig" + ], + "provides": [ + "pc:blkid=2.33.0", + "pc:fdisk=2.33.0", + "pc:mount=2.33.0", + "pc:smartcols=2.33.0", + "pc:uuid=2.33.0" + ] + }, + "python3-dev": { + "versions": { + "3.6.8-r2": 1554747638 + }, + "origin": "python3", + "dependencies": [ + "pkgconfig", + "python3=3.6.8-r2" + ], + "provides": [ + "pc:python-3.6=3.6", + "pc:python-3.6m=3.6", + "pc:python3=3.6", + "cmd:python3-config", + "cmd:python3.6-config", + "cmd:python3.6m-config" + ] + }, + "libxft-dev": { + "versions": { + "2.3.2-r3": 1542824035 + }, + "origin": "libxft", + "dependencies": [ + "zlib-dev", + "libxft=2.3.2-r3", + "pc:fontconfig", + "pc:freetype2", + "pc:xproto", + "pc:xrender", + "pkgconfig" + ], + "provides": [ + "pc:xft=2.3.2" + ] + }, + "libuuid": { + "versions": { + "2.33-r0": 1545307436 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libuuid.so.1=1.3.0" + ] + }, + "sdl2_image": { + "versions": { + "2.0.4-r0": 1545062544 + }, + "origin": "sdl2_image", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL2_image-2.0.so.0=0.2.2" + ] + }, + "gst-plugins-base-dev": { + "versions": { + "1.14.4-r0": 1543928664 + }, + "origin": "gst-plugins-base", + "dependencies": [ + "gst-plugins-base=1.14.4-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gstreamer-1.0", + "pc:gstreamer-base-1.0", + "pc:orc-0.4", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-allocators-1.0=1.14.4", + "pc:gstreamer-app-1.0=1.14.4", + "pc:gstreamer-audio-1.0=1.14.4", + "pc:gstreamer-fft-1.0=1.14.4", + "pc:gstreamer-gl-1.0=1.14.4", + "pc:gstreamer-pbutils-1.0=1.14.4", + "pc:gstreamer-plugins-base-1.0=1.14.4", + "pc:gstreamer-riff-1.0=1.14.4", + "pc:gstreamer-rtp-1.0=1.14.4", + "pc:gstreamer-rtsp-1.0=1.14.4", + "pc:gstreamer-sdp-1.0=1.14.4", + "pc:gstreamer-tag-1.0=1.14.4", + "pc:gstreamer-video-1.0=1.14.4" + ] + }, + "libxext-dev": { + "versions": { + "1.3.3-r3": 1542822854 + }, + "origin": "libxext", + "dependencies": [ + "libxau-dev", + "libxext=1.3.3-r3", + "pc:x11", + "pc:xextproto", + "pkgconfig" + ], + "provides": [ + "pc:xext=1.3.3" + ] + }, + "lua5.3-cjson": { + "versions": { + "2.1.0-r8": 1542820884 + }, + "origin": "lua-cjson", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "recode": { + "versions": { + "3.6-r2": 1543245780 + }, + "origin": "recode", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:librecode.so.0=0.0.0", + "cmd:recode" + ] + }, + "groff-doc": { + "versions": { + "1.22.3-r2": 1542819579 + }, + "origin": "groff" + }, + "xf86-video-s3": { + "versions": { + "0.6.5-r11": 1544792315 + }, + "origin": "xf86-video-s3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libsodium-dev": { + "versions": { + "1.0.16-r0": 1544000835 + }, + "origin": "libsodium", + "dependencies": [ + "libsodium=1.0.16-r0", + "pkgconfig" + ], + "provides": [ + "pc:libsodium=1.0.16" + ] + }, + "qemu-mips": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mips" + ] + }, + "perl-mail-clamav": { + "versions": { + "0.29-r14": 1545067092 + }, + "origin": "perl-mail-clamav", + "dependencies": [ + "perl", + "clamav", + "so:libc.musl-x86_64.so.1", + "so:libclamav.so.7" + ] + }, + "xfsprogs-dev": { + "versions": { + "4.19.0-r1": 1546597447 + }, + "origin": "xfsprogs", + "dependencies": [ + "xfsprogs-libs=4.19.0-r1" + ] + }, + "perl-html-formattext-withlinks-andtables": { + "versions": { + "0.07-r0": 1545075975 + }, + "origin": "perl-html-formattext-withlinks-andtables", + "dependencies": [ + "perl-html-formattext-withlinks" + ] + }, + "aspell-dev": { + "versions": { + "0.60.6.1-r13": 1542965830 + }, + "origin": "aspell", + "dependencies": [ + "aspell-utils", + "aspell-libs=0.60.6.1-r13" + ], + "provides": [ + "cmd:pspell-config" + ] + }, + "apk-tools-static": { + "versions": { + "2.10.3-r1": 1547112494 + }, + "origin": "apk-tools", + "provides": [ + "cmd:apk.static" + ] + }, + "openldap-clients": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libsasl2.so.3" + ], + "provides": [ + "cmd:ldapadd", + "cmd:ldapcompare", + "cmd:ldapdelete", + "cmd:ldapexop", + "cmd:ldapmodify", + "cmd:ldapmodrdn", + "cmd:ldappasswd", + "cmd:ldapsearch", + "cmd:ldapurl", + "cmd:ldapwhoami" + ] + }, + "perl-inline-c": { + "versions": { + "0.78-r0": 1545067076 + }, + "origin": "perl-inline-c" + }, + "syslog-ng-xml": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "tcl-doc": { + "versions": { + "8.6.9-r0": 1545915060 + }, + "origin": "tcl" + }, + "lua5.3-ldbus": { + "versions": { + "20150430-r2": 1545300024 + }, + "origin": "lua-ldbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ] + }, + "pcre-doc": { + "versions": { + "8.42-r1": 1545067004 + }, + "origin": "pcre" + }, + "usbredir-doc": { + "versions": { + "0.7.1-r0": 1543223286 + }, + "origin": "usbredir" + }, + "py2-openssl": { + "versions": { + "18.0.0-r0": 1545223367 + }, + "origin": "py-openssl", + "dependencies": [ + "py2-cryptography", + "py2-six", + "python2" + ] + }, + "hwdata-pci": { + "versions": { + "0.318-r0": 1545746063 + }, + "origin": "hwdata" + }, + "tdb": { + "versions": { + "1.3.16-r0": 1543933256 + }, + "origin": "tdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtdb.so.1" + ], + "provides": [ + "cmd:tdbbackup", + "cmd:tdbdump", + "cmd:tdbrestore", + "cmd:tdbtool" + ] + }, + "lua5.3-pc": { + "versions": { + "1.0.0-r9": 1543928870 + }, + "origin": "lua-pc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "knock-doc": { + "versions": { + "0.7-r2": 1551279491 + }, + "origin": "knock" + }, + "coreutils-doc": { + "versions": { + "8.30-r0": 1542304119 + }, + "origin": "coreutils" + }, + "open-isns-dev": { + "versions": { + "0.97-r4": 1542883211 + }, + "origin": "open-isns", + "dependencies": [ + "open-isns-lib=0.97-r4" + ] + }, + "rsyslog-elasticsearch": { + "versions": { + "8.40.0-r3": 1548686789 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ], + "provides": [ + "rsyslog-omelasticsearch=8.40.0-r3" + ] + }, + "xen-doc": { + "versions": { + "4.11.1-r1": 1545075896 + }, + "origin": "xen" + }, + "perl-set-intspan": { + "versions": { + "1.19-r0": 1545213692 + }, + "origin": "perl-set-intspan" + }, + "mercurial-bash-completion": { + "versions": { + "4.9.1-r0": 1557160461 + }, + "origin": "mercurial" + }, + "dhclient": { + "versions": { + "4.4.1-r1": 1543928453 + }, + "origin": "dhcp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "cmd:dhclient", + "cmd:dhclient-script" + ] + }, + "py3-urllib3": { + "versions": { + "1.22-r0": 1542825022 + }, + "origin": "py-urllib3", + "dependencies": [ + "python3" + ] + }, + "linux-firmware-yamaha": { + "versions": { + "20190322-r0": 1554980646 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "rsyslog-libdbi": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libdbi.so.1" + ], + "provides": [ + "rsyslog-omlibdbi=8.40.0-r3" + ] + }, + "py3-pep8": { + "versions": { + "1.7.1-r0": 1542902307 + }, + "origin": "py-pep8", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:pep8-3" + ] + }, + "net-snmp-perl": { + "versions": { + "5.8-r0": 1543923350 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.35", + "so:libnetsnmpagent.so.35", + "so:libnetsnmptrapd.so.35" + ], + "provides": [ + "cmd:mib2c", + "cmd:mib2c-update", + "cmd:net-snmp-cert", + "cmd:snmp-bridge-mib", + "cmd:traptoemail" + ] + }, + "lttng-ust": { + "versions": { + "2.10.1-r0": 1543249619 + }, + "origin": "lttng-ust", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liburcu-bp.so.6", + "so:liburcu-cds.so.6" + ], + "provides": [ + "so:liblttng-ust-ctl.so.4=4.0.0", + "so:liblttng-ust-cyg-profile-fast.so.0=0.0.0", + "so:liblttng-ust-cyg-profile.so.0=0.0.0", + "so:liblttng-ust-dl.so.0=0.0.0", + "so:liblttng-ust-fd.so.0=0.0.0", + "so:liblttng-ust-fork.so.0=0.0.0", + "so:liblttng-ust-libc-wrapper.so.0=0.0.0", + "so:liblttng-ust-pthread-wrapper.so.0=0.0.0", + "so:liblttng-ust-tracepoint.so.0=0.0.0", + "so:liblttng-ust.so.0=0.0.0", + "cmd:lttng-gen-tp" + ] + }, + "atf-doc": { + "versions": { + "0.21-r1": 1542304874 + }, + "origin": "atf" + }, + "iptraf-ng-doc": { + "versions": { + "1.1.4-r4": 1545214012 + }, + "origin": "iptraf-ng" + }, + "popt-dev": { + "versions": { + "1.16-r7": 1542820792 + }, + "origin": "popt", + "dependencies": [ + "pkgconfig", + "popt=1.16-r7" + ], + "provides": [ + "pc:popt=1.16" + ] + }, + "glib": { + "versions": { + "2.58.1-r2": 1545290825 + }, + "origin": "glib", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libintl.so.8", + "so:libmount.so.1", + "so:libpcre.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libgio-2.0.so.0=0.5800.1", + "so:libglib-2.0.so.0=0.5800.1", + "so:libgmodule-2.0.so.0=0.5800.1", + "so:libgobject-2.0.so.0=0.5800.1", + "so:libgthread-2.0.so.0=0.5800.1", + "cmd:gapplication", + "cmd:gdbus", + "cmd:gio", + "cmd:gio-launch-desktop", + "cmd:gio-querymodules", + "cmd:glib-compile-schemas", + "cmd:gsettings" + ] + }, + "cdrkit-doc": { + "versions": { + "1.1.11-r2": 1545075404 + }, + "origin": "cdrkit" + }, + "perl-getopt-long": { + "versions": { + "2.50-r0": 1543922468 + }, + "origin": "perl-getopt-long" + }, + "perl-cgi-session": { + "versions": { + "4.48-r0": 1543999428 + }, + "origin": "perl-cgi-session", + "dependencies": [ + "perl" + ] + }, + "py3-google-api-python-client": { + "versions": { + "1.7.7-r0": 1546936520 + }, + "origin": "py-google-api-python-client", + "dependencies": [ + "py3-httplib2", + "py3-oauth2client", + "py3-uritemplate", + "py3-six", + "python3" + ] + }, + "alpine-ipxe-ipxe_iso": { + "versions": { + "1.0_git20180825-r1": 1545986078 + }, + "origin": "alpine-ipxe" + }, + "pjproject-dbg": { + "versions": { + "2.7.2-r4": 1545073631 + }, + "origin": "pjproject" + }, + "bison-doc": { + "versions": { + "3.0.5-r0": 1542300926 + }, + "origin": "bison" + }, + "ruby-io-console": { + "versions": { + "2.5.5-r0": 1557164836 + }, + "origin": "ruby", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.5" + ] + }, + "libbonobo-lang": { + "versions": { + "2.32.1-r6": 1545299731 + }, + "origin": "libbonobo" + }, + "py3-openssl": { + "versions": { + "18.0.0-r0": 1545223366 + }, + "origin": "py-openssl", + "dependencies": [ + "py3-cryptography", + "py3-six", + "python3" + ] + }, + "vblade": { + "versions": { + "24-r0": 1543222855 + }, + "origin": "vblade", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:vblade", + "cmd:vbladed" + ] + }, + "py-egenix-mx-base-doc": { + "versions": { + "3.2.9-r0": 1542821601 + }, + "origin": "py-egenix-mx-base" + }, + "postgresql-plpython3-contrib": { + "versions": { + "11.2-r0": 1554274177 + }, + "origin": "postgresql", + "dependencies": [ + "postgresql-plpython3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "gpgme-dev": { + "versions": { + "1.12.0-r3": 1543932321 + }, + "origin": "gpgme", + "dependencies": [ + "libgpg-error-dev", + "libassuan-dev", + "gpgme=1.12.0-r3", + "gpgmepp=1.12.0-r3" + ], + "provides": [ + "cmd:gpgme-config" + ] + }, + "p11-kit": { + "versions": { + "0.23.14-r0": 1544791899 + }, + "origin": "p11-kit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6" + ], + "provides": [ + "so:libp11-kit.so.0=0.3.0", + "cmd:p11-kit" + ] + }, + "nfs-utils-dbg": { + "versions": { + "2.3.2-r1": 1543933421 + }, + "origin": "nfs-utils", + "dependencies": [ + "rpcbind" + ] + }, + "musl": { + "versions": { + "1.1.20-r4": 1552989415 + }, + "origin": "musl", + "provides": [ + "so:libc.musl-x86_64.so.1=1" + ] + }, + "yeahconsole": { + "versions": { + "0.3.4-r0": 1543998770 + }, + "origin": "yeahconsole", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:yeahconsole" + ] + }, + "py3-curl": { + "versions": { + "7.43.0.2-r1": 1543935132 + }, + "origin": "py-curl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libpython3.6m.so.1.0", + "so:libssl.so.1.1" + ] + }, + "minicom-lang": { + "versions": { + "2.7.1-r0": 1544799266 + }, + "origin": "minicom" + }, + "rsyslog-testing": { + "versions": { + "8.40.0-r3": 1548686792 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "rsyslog-omtesting=8.40.0-r3" + ] + }, + "libksba-doc": { + "versions": { + "1.3.5-r0": 1543932190 + }, + "origin": "libksba" + }, + "augeas": { + "versions": { + "1.11.0-r0": 1542924581 + }, + "origin": "augeas", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1", + "so:libfa.so.1", + "so:libgcc_s.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:augmatch", + "cmd:augparse", + "cmd:augtool", + "cmd:fadot" + ] + }, + "perl-http-negotiate": { + "versions": { + "6.01-r1": 1542821863 + }, + "origin": "perl-http-negotiate", + "dependencies": [ + "perl", + "perl-http-message" + ] + }, + "nagios-plugins": { + "versions": { + "2.2.1-r6": 1543933915 + }, + "origin": "nagios-plugins", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ] + }, + "libnftnl-dev": { + "versions": { + "1.1.1-r0": 1542893202 + }, + "origin": "libnftnl", + "dependencies": [ + "libmnl-dev", + "libnftnl-libs=1.1.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnftnl=1.1.1" + ] + }, + "procps": { + "versions": { + "3.3.15-r0": 1543934064 + }, + "origin": "procps", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libprocps.so.7" + ], + "provides": [ + "cmd:free", + "cmd:pgrep", + "cmd:pidof", + "cmd:pkill", + "cmd:pmap", + "cmd:ps", + "cmd:pwdx", + "cmd:slabtop", + "cmd:sysctl", + "cmd:tload", + "cmd:top", + "cmd:uptime", + "cmd:vmstat", + "cmd:w", + "cmd:watch" + ] + }, + "varnish": { + "versions": { + "6.1.1-r0": 1545300752 + }, + "origin": "varnish", + "dependencies": [ + "gcc", + "libc-dev", + "libgcc", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libedit.so.0", + "so:libexecinfo.so.1", + "so:libncursesw.so.6", + "so:libpcre.so.1", + "so:libvarnishapi.so.2" + ], + "provides": [ + "cmd:varnishadm", + "cmd:varnishd", + "cmd:varnishhist", + "cmd:varnishlog", + "cmd:varnishncsa", + "cmd:varnishstat", + "cmd:varnishtest", + "cmd:varnishtop" + ] + }, + "nagios-plugins-by_ssh": { + "versions": { + "2.2.1-r6": 1543933902 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "openssh-client", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-email-address-list-doc": { + "versions": { + "0.05-r2": 1542893362 + }, + "origin": "perl-email-address-list" + }, + "lua5.1-dbi-sqlite3": { + "versions": { + "0.6-r3": 1545214220 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "yajl-tools": { + "versions": { + "2.1.0-r0": 1543935354 + }, + "origin": "yajl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:json_reformat", + "cmd:json_verify" + ] + }, + "lcms-doc": { + "versions": { + "1.19-r8": 1546249798 + }, + "origin": "lcms" + }, + "charybdis": { + "versions": { + "3.5.6-r1": 1542883317 + }, + "origin": "charybdis", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libratbox.so=0", + "cmd:charybdis-bantool", + "cmd:charybdis-genssl", + "cmd:charybdis-ircd", + "cmd:charybdis-mkpasswd", + "cmd:charybdis-viconf", + "cmd:charybdis-vimotd" + ] + }, + "perl-net-http-doc": { + "versions": { + "6.09-r1": 1542821813 + }, + "origin": "perl-net-http" + }, + "nagios-plugins-time": { + "versions": { + "2.2.1-r6": 1543933913 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "sqlite-dev": { + "versions": { + "3.26.0-r3": 1546255353 + }, + "origin": "sqlite", + "dependencies": [ + "pkgconfig", + "sqlite-libs=3.26.0-r3" + ], + "provides": [ + "pc:sqlite3=3.26.0" + ] + }, + "perl-class-returnvalue-doc": { + "versions": { + "0.55-r1": 1544798600 + }, + "origin": "perl-class-returnvalue" + }, + "kamailio-http_async": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libevent-2.1.so.6" + ] + }, + "unixodbc-doc": { + "versions": { + "2.3.7-r0": 1542845421 + }, + "origin": "unixodbc" + }, + "util-macros": { + "versions": { + "1.19.2-r0": 1542822437 + }, + "origin": "util-macros" + }, + "perl-net-snpp": { + "versions": { + "1.17-r2": 1544000615 + }, + "origin": "perl-net-snpp", + "dependencies": [ + "perl" + ] + }, + "uwsgi-msgpack": { + "versions": { + "2.0.17.1-r0": 1545062203 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "font-adobe-utopia-type1": { + "versions": { + "1.0.4-r0": 1545216492 + }, + "origin": "font-adobe-utopia-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-cgi-psgi-doc": { + "versions": { + "0.15-r1": 1545208755 + }, + "origin": "perl-cgi-psgi" + }, + "links": { + "versions": { + "2.15-r2": 1545207743 + }, + "origin": "links", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:links" + ] + }, + "squid-lang-pt": { + "versions": { + "4.4-r1": 1545216328 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "liblockfile-dev": { + "versions": { + "1.14-r0": 1545075166 + }, + "origin": "liblockfile" + }, + "lua-stdlib": { + "versions": { + "41.2.1-r0": 1545292926 + }, + "origin": "lua-stdlib" + }, + "py2-pyflakes": { + "versions": { + "1.6.0-r3": 1545073865 + }, + "origin": "py-pyflakes", + "dependencies": [ + "python2" + ], + "provides": [ + "pyflakes=1.6.0-r3", + "cmd:pyflakes-2" + ] + }, + "libavc1394": { + "versions": { + "0.5.4-r2": 1543932111 + }, + "origin": "libavc1394", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libraw1394.so.11" + ], + "provides": [ + "so:libavc1394.so.0=0.3.0", + "so:librom1394.so.0=0.3.0", + "cmd:dvcont", + "cmd:mkrfc2734", + "cmd:panelctl" + ] + }, + "py-cairo-dev": { + "versions": { + "1.16.3-r0": 1543925199 + }, + "origin": "py-cairo", + "dependencies": [ + "pc:cairo", + "pkgconfig" + ], + "provides": [ + "pc:py3cairo=1.16.3", + "pc:pycairo=1.16.3" + ] + }, + "nagios-plugins-swap": { + "versions": { + "2.2.1-r6": 1543933912 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.2-augeas": { + "versions": { + "0.1.2-r4": 1543246060 + }, + "origin": "lua-augeas", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-firmware-rtw88": { + "versions": { + "20190322-r0": 1554980648 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "lua5.2-expat": { + "versions": { + "1.3.0-r2": 1544000280 + }, + "origin": "lua-expat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "lua5.2-sql-postgres": { + "versions": { + "2.3.5-r2": 1543924401 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "xinput-doc": { + "versions": { + "1.6.2-r0": 1545235264 + }, + "origin": "xinput" + }, + "ruby-bigdecimal": { + "versions": { + "2.5.5-r0": 1557164836 + }, + "origin": "ruby", + "dependencies": [ + "ruby-libs", + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.5" + ] + }, + "nsd-doc": { + "versions": { + "4.1.26-r0": 1546948336 + }, + "origin": "nsd" + }, + "libwebp": { + "versions": { + "1.0.1-r0": 1545856969 + }, + "origin": "libwebp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libwebp.so.7=7.0.3", + "so:libwebpdecoder.so.3=3.0.3", + "so:libwebpdemux.so.2=2.0.5", + "so:libwebpmux.so.3=3.0.3" + ] + }, + "pcsc-lite": { + "versions": { + "1.8.24-r1": 1545060762 + }, + "origin": "pcsc-lite", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ], + "provides": [ + "cmd:pcsc-spy", + "cmd:pcscd" + ] + }, + "zeromq": { + "versions": { + "4.3.1-r0": 1548271359 + }, + "origin": "zeromq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libzmq.so.5" + ], + "provides": [ + "cmd:curve_keygen" + ] + }, + "paxmark": { + "versions": { + "0.11-r0": 1542301507 + }, + "origin": "paxmark", + "dependencies": [ + "attr" + ], + "provides": [ + "cmd:paxmark", + "cmd:paxmark.sh" + ] + }, + "libgss-doc": { + "versions": { + "0.1.5-r1": 1543998825 + }, + "origin": "libgss" + }, + "libgcab-lang": { + "versions": { + "0.7-r2": 1543249784 + }, + "origin": "libgcab" + }, + "qemu-audio-oss": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ] + }, + "xf86-video-chips-doc": { + "versions": { + "1.2.7-r3": 1545300414 + }, + "origin": "xf86-video-chips" + }, + "gtk-engines-redmond": { + "versions": { + "2.21.0-r2": 1545289337 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "tiff-doc": { + "versions": { + "4.0.10-r0": 1543921905 + }, + "origin": "tiff" + }, + "py-tornado": { + "versions": { + "4.5.2-r1": 1542845331 + }, + "origin": "py-tornado" + }, + "font-bitstream-type1": { + "versions": { + "1.0.3-r0": 1545300424 + }, + "origin": "font-bitstream-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "squid-lang-az": { + "versions": { + "4.4-r1": 1545216323 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "openldap-back-passwd": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "libxklavier-dev": { + "versions": { + "5.4-r4": 1546458613 + }, + "origin": "libxklavier", + "dependencies": [ + "gettext-dev", + "libxkbfile-dev", + "libxklavier=5.4-r4", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libxklavier=5.4" + ] + }, + "libdrm-dev": { + "versions": { + "2.4.96-r0": 1544791765 + }, + "origin": "libdrm", + "dependencies": [ + "linux-headers", + "libdrm=2.4.96-r0", + "pkgconfig" + ], + "provides": [ + "pc:libdrm=2.4.96", + "pc:libdrm_amdgpu=2.4.96", + "pc:libdrm_freedreno=2.4.96", + "pc:libdrm_intel=2.4.96", + "pc:libdrm_nouveau=2.4.96", + "pc:libdrm_radeon=2.4.96", + "pc:libkms=1.0.0" + ] + }, + "shorewall6-doc": { + "versions": { + "5.2.2-r0": 1548095417 + }, + "origin": "shorewall6" + }, + "gtest-dev": { + "versions": { + "1.8.1-r0": 1542822389 + }, + "origin": "gtest", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:gmock=1.8.1", + "pc:gmock_main=1.8.1", + "pc:gtest=1.8.1", + "pc:gtest_main=1.8.1" + ] + }, + "cogl-dev": { + "versions": { + "1.22.2-r0": 1544000911 + }, + "origin": "cogl", + "dependencies": [ + "glib-dev", + "mesa-dev", + "libdrm-dev", + "libxdamage-dev", + "libxcomposite-dev", + "libxrandr-dev", + "gdk-pixbuf-dev", + "pango-dev", + "cairo-dev", + "gobject-introspection-dev", + "libxext-dev", + "eudev-dev", + "cogl=1.22.2-r0", + "pc:cairo>=1.10", + "pc:egl", + "pc:gbm", + "pc:gdk-pixbuf-2.0>=2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:libdrm", + "pc:x11", + "pc:xcomposite>=0.4", + "pc:xdamage", + "pc:xext", + "pc:xfixes>=3", + "pc:xrandr>=1.2", + "pkgconfig" + ], + "provides": [ + "pc:cogl-1.0=1.22.2", + "pc:cogl-2.0-experimental=2.0.0", + "pc:cogl-gl-1.0=1.22.2", + "pc:cogl-gles2-1.0=1.22.2", + "pc:cogl-gles2-2.0-experimental=2.0.0", + "pc:cogl-pango-1.0=1.22.2", + "pc:cogl-pango-2.0-experimental=1.22.2", + "pc:cogl-path-1.0=1.22.2", + "pc:cogl-path-2.0-experimental=1.22.2" + ] + }, + "perl-file-slurp-tiny": { + "versions": { + "0.004-r0": 1545213668 + }, + "origin": "perl-file-slurp-tiny" + }, + "libresample": { + "versions": { + "0.1.3-r1": 1545073659 + }, + "origin": "libresample" + }, + "perl-crypt-rijndael-doc": { + "versions": { + "1.13-r0": 1543923070 + }, + "origin": "perl-crypt-rijndael" + }, + "py3-tdb": { + "versions": { + "1.3.16-r0": 1543933254 + }, + "origin": "tdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0", + "so:libtdb.so.1" + ] + }, + "wipe-doc": { + "versions": { + "2.3.1-r0": 1543924496 + }, + "origin": "wipe" + }, + "alpine-ipxe-undionly_kpxe": { + "versions": { + "1.0_git20180825-r1": 1545986078 + }, + "origin": "alpine-ipxe" + }, + "libtheora": { + "versions": { + "1.1.1-r13": 1543928533 + }, + "origin": "libtheora", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0" + ], + "provides": [ + "so:libtheora.so.0=0.3.10", + "so:libtheoradec.so.1=1.1.4", + "so:libtheoraenc.so.1=1.1.2" + ] + }, + "boost-system": { + "versions": { + "1.67.0-r2": 1542823633 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libboost_system-mt.so.1.67.0=1.67.0", + "so:libboost_system.so.1.67.0=1.67.0" + ] + }, + "nginx-mod-devel-kit": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "uwsgi-gevent3": { + "versions": { + "2.0.17.1-r0": 1545062215 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-firmware-moxa": { + "versions": { + "20190322-r0": 1554980650 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "distcc-pump": { + "versions": { + "3.3.2-r0": 1544799001 + }, + "origin": "distcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ], + "provides": [ + "cmd:pump" + ] + }, + "gtkspell-doc": { + "versions": { + "2.0.16-r7": 1545215215 + }, + "origin": "gtkspell" + }, + "linux-firmware-dabusb": { + "versions": { + "20190322-r0": 1554980653 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "privoxy": { + "versions": { + "3.0.26-r0": 1545154403 + }, + "origin": "privoxy", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1", + "so:libpcreposix.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:privoxy" + ] + }, + "lua5.1-lxc": { + "versions": { + "3.0.2-r0": 1546416498 + }, + "origin": "lua-lxc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblxc.so.1" + ] + }, + "expat": { + "versions": { + "2.2.6-r0": 1542302776 + }, + "origin": "expat", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libexpat.so.1=1.6.8", + "cmd:xmlwf" + ] + }, + "automake": { + "versions": { + "1.16.1-r0": 1542301305 + }, + "origin": "automake", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:aclocal", + "cmd:aclocal-1.16", + "cmd:automake", + "cmd:automake-1.16" + ] + }, + "perl-cache-cache": { + "versions": { + "1.08-r0": 1543223060 + }, + "origin": "perl-cache-cache", + "dependencies": [ + "perl-error", + "perl-digest-sha1" + ] + }, + "perl-encode-locale": { + "versions": { + "1.05-r1": 1542821771 + }, + "origin": "perl-encode-locale", + "dependencies": [ + "perl" + ] + }, + "fprobe-ulog": { + "versions": { + "1.2-r4": 1543924459 + }, + "origin": "fprobe-ulog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetfilter_log.so.1", + "so:libnfnetlink.so.0" + ], + "provides": [ + "cmd:fprobe-ulog" + ] + }, + "irssi": { + "versions": { + "1.1.2-r0": 1547738041 + }, + "origin": "irssi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libncursesw.so.6", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:irssi" + ] + }, + "glew": { + "versions": { + "2.1.0-r0": 1543935774 + }, + "origin": "glew", + "dependencies": [ + "so:libGL.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libGLEW.so.2.1=2.1.0" + ] + }, + "vte3-doc": { + "versions": { + "0.52.2-r0": 1545215298 + }, + "origin": "vte3" + }, + "font-bh-lucidatypewriter-75dpi": { + "versions": { + "1.0.3-r0": 1545116961 + }, + "origin": "font-bh-lucidatypewriter-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "pcre2-doc": { + "versions": { + "10.32-r1": 1545069083 + }, + "origin": "pcre2" + }, + "tar-doc": { + "versions": { + "1.32-r0": 1551091781 + }, + "origin": "tar" + }, + "netcf-dev": { + "versions": { + "0.2.8-r5": 1543935398 + }, + "origin": "netcf", + "dependencies": [ + "netcf-libs=0.2.8-r5", + "pkgconfig" + ], + "provides": [ + "pc:netcf=0.2.8" + ] + }, + "paxctl": { + "versions": { + "0.9-r0": 1545223088 + }, + "origin": "paxctl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:paxctl" + ] + }, + "fuse3-doc": { + "versions": { + "3.2.6-r1": 1548943636 + }, + "origin": "fuse3" + }, + "kyua": { + "versions": { + "0.13-r3": 1542304934 + }, + "origin": "kyua", + "dependencies": [ + "so:libatf-c++.so.2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:liblutok.so.3", + "so:libsqlite3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:kyua" + ] + }, + "dovecot-doc": { + "versions": { + "2.3.6-r0": 1557134097 + }, + "origin": "dovecot" + }, + "perl-regexp-ipv6": { + "versions": { + "0.03-r0": 1545289354 + }, + "origin": "perl-regexp-ipv6", + "dependencies": [ + "perl" + ] + }, + "libgssglue-dev": { + "versions": { + "0.4-r1": 1543248484 + }, + "origin": "libgssglue", + "dependencies": [ + "libgssglue=0.4-r1", + "pkgconfig" + ], + "provides": [ + "pc:libgssglue=0.4" + ] + }, + "protobuf": { + "versions": { + "3.6.1-r1": 1543931874 + }, + "origin": "protobuf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libprotobuf-lite.so.17=17.0.0", + "so:libprotobuf.so.17=17.0.0", + "so:libprotoc.so.17=17.0.0", + "cmd:protoc" + ] + }, + "libcroco-dev": { + "versions": { + "0.6.12-r1": 1543926545 + }, + "origin": "libcroco", + "dependencies": [ + "libcroco=0.6.12-r1", + "pc:glib-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libcroco-0.6=0.6.12", + "cmd:croco-0.6-config" + ] + }, + "perl-file-rsync-doc": { + "versions": { + "0.74-r1": 1545299965 + }, + "origin": "perl-file-rsync" + }, + "perl-javascript-minifier": { + "versions": { + "1.14-r0": 1545300399 + }, + "origin": "perl-javascript-minifier" + }, + "perl-email-date-format": { + "versions": { + "1.005-r0": 1543226621 + }, + "origin": "perl-email-date-format" + }, + "monit-openrc": { + "versions": { + "5.25.2-r0": 1545858142 + }, + "origin": "monit" + }, + "perl-symbol-global-name-doc": { + "versions": { + "0.05-r1": 1545223347 + }, + "origin": "perl-symbol-global-name" + }, + "lxterminal-lang": { + "versions": { + "0.3.2-r0": 1543927689 + }, + "origin": "lxterminal" + }, + "dzen": { + "versions": { + "0.9.5-r3": 1545062317 + }, + "origin": "dzen", + "dependencies": [ + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libXpm.so.4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dzen2" + ] + }, + "highlight": { + "versions": { + "3.46-r0": 1542823672 + }, + "origin": "highlight", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:liblua-5.3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:highlight" + ] + }, + "qt-odbc": { + "versions": { + "4.8.7-r11": 1545075087 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libodbc.so.2", + "so:libstdc++.so.6" + ] + }, + "py2-django": { + "versions": { + "1.11.20-r0": 1551267356 + }, + "origin": "py-django", + "dependencies": [ + "py2-tz", + "python2" + ], + "provides": [ + "cmd:django-admin-2" + ] + }, + "dahdi-linux": { + "versions": { + "3.0.0-r0": 1545911874 + }, + "origin": "dahdi-linux" + }, + "bubblewrap": { + "versions": { + "0.3.1-r0": 1546247687 + }, + "origin": "bubblewrap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2" + ], + "provides": [ + "cmd:bwrap" + ] + }, + "perl-xml-sax-doc": { + "versions": { + "0.99-r2": 1542893167 + }, + "origin": "perl-xml-sax" + }, + "guile-dev": { + "versions": { + "2.0.14-r0": 1543227919 + }, + "origin": "guile", + "dependencies": [ + "guile", + "gc-dev", + "guile-libs=2.0.14-r0", + "pkgconfig" + ], + "provides": [ + "pc:guile-2.0=2.0.14", + "cmd:guile-config" + ] + }, + "cracklib-words": { + "versions": { + "20080507-r2": 1542965863 + }, + "origin": "cracklib-words" + }, + "py2-snowballstemmer": { + "versions": { + "1.2.1-r1": 1542824983 + }, + "origin": "py-snowballstemmer", + "dependencies": [ + "python2" + ] + }, + "acf-freeswitch-vmail": { + "versions": { + "0.6.2-r1": 1545209644 + }, + "origin": "acf-freeswitch-vmail", + "dependencies": [ + "acf-core", + "lua-sql-sqlite3", + "lua-socket", + "freeswitch", + "/bin/sh" + ] + }, + "perl-business-hours-doc": { + "versions": { + "0.13-r0": 1547709105 + }, + "origin": "perl-business-hours" + }, + "libxres-dev": { + "versions": { + "1.2.0-r1": 1545302056 + }, + "origin": "libxres", + "dependencies": [ + "xorgproto", + "libxres=1.2.0-r1", + "pc:x11", + "pc:xext", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xres=1.2.0" + ] + }, + "gperf": { + "versions": { + "3.1-r2": 1542822039 + }, + "origin": "gperf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:gperf" + ] + }, + "miniperl": { + "versions": { + "5.26.3-r0": 1543998673 + }, + "origin": "perl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:miniperl" + ] + }, + "flite": { + "versions": { + "2.1-r0": 1545117049 + }, + "origin": "flite", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libflite.so.1=2.1", + "so:libflite_cmu_grapheme_lang.so.1=2.1", + "so:libflite_cmu_grapheme_lex.so.1=2.1", + "so:libflite_cmu_indic_lang.so.1=2.1", + "so:libflite_cmu_indic_lex.so.1=2.1", + "so:libflite_cmu_time_awb.so.1=2.1", + "so:libflite_cmu_us_awb.so.1=2.1", + "so:libflite_cmu_us_kal.so.1=2.1", + "so:libflite_cmu_us_kal16.so.1=2.1", + "so:libflite_cmu_us_rms.so.1=2.1", + "so:libflite_cmu_us_slt.so.1=2.1", + "so:libflite_cmulex.so.1=2.1", + "so:libflite_usenglish.so.1=2.1", + "cmd:flite", + "cmd:flite_cmu_time_awb", + "cmd:flite_cmu_us_awb", + "cmd:flite_cmu_us_kal", + "cmd:flite_cmu_us_kal16", + "cmd:flite_cmu_us_rms", + "cmd:flite_cmu_us_slt", + "cmd:flite_time" + ] + }, + "qemu-hppa": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-hppa" + ] + }, + "perl-capture-tiny-doc": { + "versions": { + "0.48-r0": 1542893272 + }, + "origin": "perl-capture-tiny" + }, + "uwsgi-nagios": { + "versions": { + "2.0.17.1-r0": 1545062203 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "nmap": { + "versions": { + "7.70-r3": 1545163173 + }, + "origin": "nmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:liblua-5.3.so.0", + "so:libpcap.so.1", + "so:libpcre.so.1", + "so:libssh2.so.1", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:nmap" + ] + }, + "py-gflags": { + "versions": { + "3.1.1-r1": 1545208600 + }, + "origin": "py-gflags" + }, + "xrdb-doc": { + "versions": { + "1.1.1-r0": 1542883478 + }, + "origin": "xrdb" + }, + "py3-certifi": { + "versions": { + "2018.4.16-r0": 1542825014 + }, + "origin": "py-certifi", + "dependencies": [ + "python3" + ] + }, + "qpdf-libs": { + "versions": { + "8.3.0-r0": 1547117323 + }, + "origin": "qpdf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libjpeg.so.8", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libqpdf.so.21=21.3.0" + ] + }, + "perl-file-remove": { + "versions": { + "1.58-r0": 1544792322 + }, + "origin": "perl-file-remove" + }, + "libev": { + "versions": { + "4.25-r0": 1545838253 + }, + "origin": "libev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libev.so.4=4.0.0" + ] + }, + "slim-doc": { + "versions": { + "1.3.6-r8": 1545229417 + }, + "origin": "slim" + }, + "openldap-overlay-sssvlv": { + "versions": { + "2.4.47-r2": 1546016480 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "perl-mime-types": { + "versions": { + "2.17-r0": 1543226616 + }, + "origin": "perl-mime-types" + }, + "collectd-python": { + "versions": { + "5.8.0-r3": 1546422677 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "perl-mail-spf-doc": { + "versions": { + "2.9.0-r2": 1542925015 + }, + "origin": "perl-mail-spf" + }, + "perl-data-hexdump-doc": { + "versions": { + "0.02-r1": 1545664905 + }, + "origin": "perl-data-hexdump" + }, + "perl-gdtextutil": { + "versions": { + "0.86-r0": 1543248473 + }, + "origin": "perl-gdtextutil", + "dependencies": [ + "perl-gd" + ] + }, + "linux-firmware-mrvl": { + "versions": { + "20190322-r0": 1554980650 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "py-flask-oauthlib": { + "versions": { + "0.9.5-r0": 1545164578 + }, + "origin": "py-flask-oauthlib", + "dependencies": [ + "py-flask", + "py-requests-oauthlib" + ] + }, + "tcpdump-doc": { + "versions": { + "4.9.2-r4": 1545300929 + }, + "origin": "tcpdump" + }, + "perl-netaddr-ip": { + "versions": { + "4.079-r1": 1542925008 + }, + "origin": "perl-netaddr-ip", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "varnish-doc": { + "versions": { + "6.1.1-r0": 1545300751 + }, + "origin": "varnish" + }, + "rabbitmq-c-dev": { + "versions": { + "0.8.0-r5": 1543923901 + }, + "origin": "rabbitmq-c", + "dependencies": [ + "openssl-dev", + "popt-dev", + "pkgconfig", + "rabbitmq-c=0.8.0-r5" + ], + "provides": [ + "pc:librabbitmq=0.8.0" + ] + }, + "po4a": { + "versions": { + "0.52-r1": 1542304731 + }, + "origin": "po4a", + "dependencies": [ + "perl", + "gettext" + ], + "provides": [ + "cmd:msguntypot", + "cmd:po4a", + "cmd:po4a-build", + "cmd:po4a-gettextize", + "cmd:po4a-normalize", + "cmd:po4a-translate", + "cmd:po4a-updatepo", + "cmd:po4aman-display-po", + "cmd:po4apod-display-po" + ] + }, + "grep-doc": { + "versions": { + "3.1-r2": 1543926780 + }, + "origin": "grep" + }, + "linux-pam-doc": { + "versions": { + "1.3.0-r0": 1542820661 + }, + "origin": "linux-pam" + }, + "clamav-db": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav", + "dependencies": [ + "freshclam" + ] + }, + "cciss_vol_status": { + "versions": { + "1.12-r0": 1545073935 + }, + "origin": "cciss_vol_status", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cciss_vol_status" + ] + }, + "v86d": { + "versions": { + "0.1.10-r1": 1545229398 + }, + "origin": "v86d", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:v86d" + ] + }, + "libexif-doc": { + "versions": { + "0.6.21-r3": 1545116758 + }, + "origin": "libexif" + }, + "py-gnome-dev": { + "versions": { + "2.28.1-r5": 1545301285 + }, + "origin": "py-gnome", + "dependencies": [ + "gtk+2.0-dev", + "libgnome-dev", + "py-gobject-dev", + "py-gtk-dev", + "python2-dev", + "pkgconfig" + ], + "provides": [ + "pc:gnome-python-2.0=2.28.1" + ] + }, + "libee-dev": { + "versions": { + "0.4.1-r0": 1545292594 + }, + "origin": "libee", + "dependencies": [ + "libee=0.4.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libee=0.4.1" + ] + }, + "libmspack": { + "versions": { + "0.8_alpha-r0": 1543925822 + }, + "origin": "libmspack", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmspack.so.0=0.1.0" + ] + }, + "dmenu": { + "versions": { + "4.8-r0": 1543222729 + }, + "origin": "dmenu", + "dependencies": [ + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1" + ], + "provides": [ + "cmd:dmenu", + "cmd:dmenu_path", + "cmd:dmenu_run", + "cmd:stest" + ] + }, + "sendpage": { + "versions": { + "1.0.3-r5": 1545116829 + }, + "origin": "sendpage", + "dependencies": [ + "perl-mailtools", + "perl-net-snpp", + "perl-device-serialport", + "perl-dbi", + "perl-sys-hostname-long", + "perl-test-mockobject" + ], + "provides": [ + "cmd:email2page", + "cmd:sendmail2snpp", + "cmd:sendpage", + "cmd:sendpage-db", + "cmd:snpp" + ] + }, + "jack-dbus": { + "versions": { + "1.9.12-r0": 1545073439 + }, + "origin": "jack", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libexpat.so.1", + "so:libjackserver.so.0" + ], + "provides": [ + "cmd:jackdbus" + ] + }, + "libxkbfile": { + "versions": { + "1.0.9-r2": 1542973201 + }, + "origin": "libxkbfile", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxkbfile.so.1=1.0.2" + ] + }, + "libburn-doc": { + "versions": { + "1.5.0-r0": 1545837389 + }, + "origin": "libburn" + }, + "findmnt": { + "versions": { + "2.33-r0": 1545307437 + }, + "origin": "util-linux", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libmount.so.1", + "so:libsmartcols.so.1" + ], + "provides": [ + "cmd:findmnt" + ] + }, + "perl-timedate": { + "versions": { + "2.30-r1": 1543226629 + }, + "origin": "perl-timedate", + "provides": [ + "perl-time-date=2.30" + ] + }, + "uwsgi-transformation_chunked": { + "versions": { + "2.0.17.1-r0": 1545062212 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "xmodmap-doc": { + "versions": { + "1.0.9-r2": 1542973230 + }, + "origin": "xmodmap" + }, + "qemu-sparc32plus": { + "versions": { + "3.1.0-r3": 1551107302 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sparc32plus" + ] + }, + "mt-st": { + "versions": { + "1.1-r4": 1543223640 + }, + "origin": "mt-st", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mt", + "cmd:stinit" + ] + }, + "py-twitter": { + "versions": { + "3.5-r0": 1546524754 + }, + "origin": "py-twitter", + "dependencies": [ + "py-future", + "py-requests", + "py-requests-oauthlib" + ] + }, + "qemu-cris": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-cris" + ] + }, + "clamav-libunrar": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav" + }, + "ccache": { + "versions": { + "3.5.1-r0": 1547294261 + }, + "origin": "ccache", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:ccache" + ] + }, + "perl-namespace-clean-doc": { + "versions": { + "0.27-r0": 1542973123 + }, + "origin": "perl-namespace-clean" + }, + "dropbear-openrc": { + "versions": { + "2018.76-r2": 1545208975 + }, + "origin": "dropbear" + }, + "py2-oauth2client": { + "versions": { + "4.1.2-r2": 1545229290 + }, + "origin": "py-oauth2client", + "dependencies": [ + "py2-asn1", + "py2-httplib2", + "py2-asn1-modules", + "py2-rsa", + "py2-six", + "python2" + ] + }, + "s6-rc-dev": { + "versions": { + "0.4.1.0-r0": 1545076679 + }, + "origin": "s6-rc", + "dependencies": [ + "s6-rc=0.4.1.0-r0" + ] + }, + "squid-lang-ro": { + "versions": { + "4.4-r1": 1545216329 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "e2fsprogs-libs": { + "versions": { + "1.44.5-r0": 1545745858 + }, + "origin": "e2fsprogs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2" + ], + "provides": [ + "so:libe2p.so.2=2.3", + "so:libext2fs.so.2=2.4", + "so:libss.so.2=2.0" + ] + }, + "perl-try-tiny": { + "versions": { + "0.22-r1": 1542821798 + }, + "origin": "perl-try-tiny" + }, + "dtach": { + "versions": { + "0.9-r1": 1543254080 + }, + "origin": "dtach", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dtach" + ] + }, + "xcmsdb": { + "versions": { + "1.0.5-r0": 1545293076 + }, + "origin": "xcmsdb", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xcmsdb" + ] + }, + "postfix-openrc": { + "versions": { + "3.3.2-r0": 1547130362 + }, + "origin": "postfix" + }, + "syslog-ng-map-value-pairs": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "patch-doc": { + "versions": { + "2.7.6-r4": 1548420016 + }, + "origin": "patch" + }, + "xf86-input-libinput-doc": { + "versions": { + "0.28.1-r0": 1545207901 + }, + "origin": "xf86-input-libinput" + }, + "xf86-input-synaptics-dev": { + "versions": { + "1.9.1-r1": 1545154611 + }, + "origin": "xf86-input-synaptics", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xorg-synaptics=1.9.1" + ] + }, + "asciidoc-doc": { + "versions": { + "8.6.10-r0": 1542883777 + }, + "origin": "asciidoc" + }, + "lua5.3-evdev": { + "versions": { + "2.2.1-r1": 1545076555 + }, + "origin": "lua-evdev", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "lm_sensors-detect": { + "versions": { + "3.4.0-r6": 1542924818 + }, + "origin": "lm_sensors", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:sensors-conf-convert", + "cmd:sensors-detect" + ] + }, + "unbound-migrate": { + "versions": { + "1.8.3-r1": 1555953584 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root" + ], + "provides": [ + "cmd:migrate-dnscache-to-unbound" + ] + }, + "ix": { + "versions": { + "0.6-r0": 1545076749 + }, + "origin": "sprunge", + "dependencies": [ + "curl" + ], + "provides": [ + "cmd:ix" + ] + }, + "cryptsetup-libs": { + "versions": { + "2.0.6-r0": 1545746292 + }, + "origin": "cryptsetup", + "dependencies": [ + "so:libargon2.so.1", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libdevmapper.so.1.02", + "so:libjson-c.so.4", + "so:libuuid.so.1" + ], + "provides": [ + "so:libcryptsetup.so.12=12.3.0" + ] + }, + "libxrandr-doc": { + "versions": { + "1.5.1-r2": 1542972233 + }, + "origin": "libxrandr" + }, + "clang-dev": { + "versions": { + "5.0.2-r0": 1546873920 + }, + "origin": "clang", + "dependencies": [ + "clang=5.0.2-r0", + "clang-libs=5.0.2-r0" + ] + }, + "acl-dev": { + "versions": { + "2.2.52-r5": 1542304042 + }, + "origin": "acl", + "dependencies": [ + "libacl=2.2.52-r5" + ] + }, + "py2-libvirt": { + "versions": { + "4.10.0-r0": 1545291084 + }, + "origin": "py-libvirt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libvirt-lxc.so.0", + "so:libvirt-qemu.so.0", + "so:libvirt.so.0" + ] + }, + "nginx-mod-http-lua-upstream": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "nginx-mod-http-lua", + "so:libc.musl-x86_64.so.1" + ] + }, + "qemu-system-microblaze": { + "versions": { + "3.1.0-r3": 1551107303 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-microblaze" + ] + }, + "perl-file-temp-doc": { + "versions": { + "0.2308-r0": 1543226522 + }, + "origin": "perl-file-temp" + }, + "imap-dev": { + "versions": { + "2007f-r9": 1545062297 + }, + "origin": "imap", + "dependencies": [ + "c-client=2007f-r9" + ] + }, + "py-dateutil": { + "versions": { + "2.7.3-r0": 1543248498 + }, + "origin": "py-dateutil", + "dependencies": [ + "py-six" + ] + }, + "pcre-tools": { + "versions": { + "8.42-r1": 1545067004 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1", + "so:libpcre16.so.0", + "so:libpcre32.so.0", + "so:libpcreposix.so.0" + ], + "provides": [ + "cmd:pcregrep", + "cmd:pcretest" + ] + }, + "qt-x11": { + "versions": { + "4.8.7-r11": 1545075089 + }, + "origin": "qt", + "dependencies": [ + "hicolor-icon-theme", + "so:libGL.so.1", + "so:libICE.so.6", + "so:libQtCore.so.4", + "so:libQtDBus.so.4", + "so:libQtNetwork.so.4", + "so:libQtScript.so.4", + "so:libQtSql.so.4", + "so:libQtXml.so.4", + "so:libQtXmlPatterns.so.4", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libjpeg.so.8", + "so:libmng.so.2", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libtiff.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libQt3Support.so.4=4.8.7", + "so:libQtCLucene.so.4=4.8.7", + "so:libQtDeclarative.so.4=4.8.7", + "so:libQtDesigner.so.4=4.8.7", + "so:libQtDesignerComponents.so.4=4.8.7", + "so:libQtGui.so.4=4.8.7", + "so:libQtHelp.so.4=4.8.7", + "so:libQtMultimedia.so.4=4.8.7", + "so:libQtOpenGL.so.4=4.8.7", + "so:libQtScriptTools.so.4=4.8.7", + "so:libQtSvg.so.4=4.8.7", + "cmd:qdbusviewer", + "cmd:qmlplugindump", + "cmd:qmlviewer" + ] + }, + "linux-firmware-tehuti": { + "versions": { + "20190322-r0": 1554980647 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "py-gnutls": { + "versions": { + "3.1.2-r0": 1545062790 + }, + "origin": "py-gnutls" + }, + "perl-role-tiny": { + "versions": { + "2.000006-r0": 1542972959 + }, + "origin": "perl-role-tiny" + }, + "kamailio-dbg": { + "versions": { + "5.2.0-r1": 1546423169 + }, + "origin": "kamailio", + "dependencies": [ + "gawk" + ] + }, + "libdnet-doc": { + "versions": { + "1.12-r7": 1545076673 + }, + "origin": "libdnet" + }, + "hwdata-pnp": { + "versions": { + "0.318-r0": 1545746063 + }, + "origin": "hwdata" + }, + "perl-file-next-doc": { + "versions": { + "1.16-r0": 1544000673 + }, + "origin": "perl-file-next" + }, + "soxr-doc": { + "versions": { + "0.1.3-r0": 1545208985 + }, + "origin": "soxr" + }, + "setup-box": { + "versions": { + "1.0.1-r0": 1544000552 + }, + "origin": "setup-box", + "dependencies": [ + "jq" + ], + "provides": [ + "cmd:setup-box" + ] + }, + "byobu-doc": { + "versions": { + "5.127-r0": 1545062321 + }, + "origin": "byobu" + }, + "testdisk": { + "versions": { + "7.0-r4": 1544001241 + }, + "origin": "testdisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libext2fs.so.2", + "so:libjpeg.so.8", + "so:libncursesw.so.6", + "so:libntfs-3g.so.88", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:fidentify", + "cmd:photorec", + "cmd:testdisk" + ] + }, + "dbus-dev": { + "versions": { + "1.10.24-r1": 1542824377 + }, + "origin": "dbus", + "dependencies": [ + "util-linux-dev", + "dbus-libs=1.10.24-r1", + "pkgconfig" + ], + "provides": [ + "pc:dbus-1=1.10.24" + ] + }, + "psqlodbc": { + "versions": { + "09.06.0410-r1": 1544792479 + }, + "origin": "psqlodbc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libodbcinst.so.2", + "so:libpq.so.5" + ], + "provides": [ + "so:psqlodbca.so=0", + "so:psqlodbcw.so=0" + ] + }, + "darkice": { + "versions": { + "1.3-r0": 1545302359 + }, + "origin": "darkice", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libfaac.so.0", + "so:libgcc_s.so.1", + "so:libmp3lame.so.0", + "so:libogg.so.0", + "so:libstdc++.so.6", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2" + ], + "provides": [ + "cmd:darkice" + ] + }, + "xorg-server-dev": { + "versions": { + "1.20.3-r1": 1543928068 + }, + "origin": "xorg-server", + "dependencies": [ + "libepoxy-dev", + "libxfont2-dev", + "mesa-dev", + "pc:dri2proto>=2.8", + "pc:dri3proto>=1.2", + "pc:dri>=7.8.0", + "pc:fontsproto>=2.1.3", + "pc:glproto>=1.4.17", + "pc:inputproto>=2.3", + "pc:kbproto>=1.0.3", + "pc:pciaccess>=0.12.901", + "pc:pixman-1>=0.27.2", + "pc:presentproto>=1.1", + "pc:randrproto>=1.6.0", + "pc:renderproto>=0.11", + "pc:resourceproto>=1.2.0", + "pc:scrnsaverproto>=1.1", + "pc:videoproto", + "pc:xextproto>=7.2.99.901", + "pc:xf86driproto>=2.1.0", + "pc:xineramaproto", + "pc:xproto>=7.0.31", + "pkgconfig" + ], + "provides": [ + "pc:xorg-server=1.20.3" + ] + }, + "openldap-overlay-retcode": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "iperf-doc": { + "versions": { + "2.0.10-r1": 1543222921 + }, + "origin": "iperf" + }, + "inotify-tools": { + "versions": { + "3.20.1-r1": 1545302268 + }, + "origin": "inotify-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libinotifytools.so.0=0.4.1", + "cmd:inotifywait", + "cmd:inotifywatch" + ] + }, + "lua5.2-apk": { + "versions": { + "2.10.3-r1": 1547112494 + }, + "origin": "apk-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ] + }, + "py3-asn1-modules": { + "versions": { + "0.2.2-r0": 1543928876 + }, + "origin": "py-asn1-modules", + "dependencies": [ + "py3-openssl", + "py3-asn1" + ] + }, + "spamassassin-client": { + "versions": { + "3.4.2-r0": 1545061981 + }, + "origin": "spamassassin", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:spamc" + ] + }, + "aspell-uk": { + "versions": { + "1.4.0-r1": 1545235381 + }, + "origin": "aspell-uk" + }, + "gnokii-smsd-pgsql": { + "versions": { + "0.6.31-r8": 1545300480 + }, + "origin": "gnokii", + "dependencies": [ + "gnokii-smsd", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "libdc1394-dev": { + "versions": { + "2.2.5-r1": 1545249932 + }, + "origin": "libdc1394", + "dependencies": [ + "libusb-dev", + "libraw1394-dev", + "libdc1394=2.2.5-r1", + "pkgconfig" + ], + "provides": [ + "pc:libdc1394-2=2.2.5" + ] + }, + "jsoncpp-dev": { + "versions": { + "1.8.4-r0": 1545075439 + }, + "origin": "jsoncpp", + "dependencies": [ + "jsoncpp=1.8.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:jsoncpp=1.8.4" + ] + }, + "seabios": { + "versions": { + "1.11.0-r0": 1543936027 + }, + "origin": "seabios", + "dependencies": [ + "seabios-bin=1.11.0-r0", + "seavgabios-bin=1.11.0-r0" + ] + }, + "perl-try-tiny-doc": { + "versions": { + "0.22-r1": 1542821796 + }, + "origin": "perl-try-tiny" + }, + "lame-dev": { + "versions": { + "3.100-r0": 1545117086 + }, + "origin": "lame", + "dependencies": [ + "lame=3.100-r0" + ] + }, + "ipsec-tools-doc": { + "versions": { + "0.8.2-r8": 1543246797 + }, + "origin": "ipsec-tools" + }, + "perl-clone": { + "versions": { + "0.41-r1": 1546096794 + }, + "origin": "perl-clone", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "uwsgi-echo": { + "versions": { + "2.0.17.1-r0": 1545062198 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "shorewall-doc": { + "versions": { + "5.2.2-r0": 1548095422 + }, + "origin": "shorewall" + }, + "py-farstream0.1": { + "versions": { + "0.1.2-r2": 1545069353 + }, + "origin": "farstream0.1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfarstream-0.1.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-0.10.so.0" + ] + }, + "rp-pppoe-doc": { + "versions": { + "3.13-r0": 1545223084 + }, + "origin": "rp-pppoe" + }, + "perl-module-util-doc": { + "versions": { + "1.09-r0": 1545163058 + }, + "origin": "perl-module-util" + }, + "libmpeg2": { + "versions": { + "0.5.1-r8": 1544000976 + }, + "origin": "libmpeg2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmpeg2.so.0=0.1.0", + "so:libmpeg2convert.so.0=0.0.0", + "cmd:corrupt_mpeg2", + "cmd:extract_mpeg2" + ] + }, + "libmng": { + "versions": { + "2.0.3-r1": 1545062629 + }, + "origin": "libmng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libmng.so.2=2.0.2" + ] + }, + "lua5.3-sql-sqlite3": { + "versions": { + "2.3.5-r2": 1543924405 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "perl-exporter-tiny": { + "versions": { + "1.002001-r0": 1542845620 + }, + "origin": "perl-exporter-tiny" + }, + "libmms": { + "versions": { + "0.6.4-r0": 1544001046 + }, + "origin": "libmms", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmms.so.0=0.0.2" + ] + }, + "tinydns": { + "versions": { + "1.05-r47": 1545208950 + }, + "origin": "djbdns", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tinydns", + "cmd:tinydns-conf", + "cmd:tinydns-data", + "cmd:tinydns-edit", + "cmd:tinydns-get" + ] + }, + "perl-libwww": { + "versions": { + "6.36-r0": 1544791738 + }, + "origin": "perl-libwww", + "dependencies": [ + "perl-http-date", + "perl-http-cookies", + "perl-net-http", + "perl-http-daemon", + "perl-html-parser", + "perl-file-listing", + "perl-www-robotrules", + "perl-http-negotiate", + "perl-uri", + "perl-http-message", + "perl-lwp-mediatypes", + "perl-encode-locale", + "perl-try-tiny" + ], + "provides": [ + "cmd:lwp-download", + "cmd:lwp-dump", + "cmd:lwp-mirror", + "cmd:lwp-request" + ] + }, + "postgresql-client": { + "versions": { + "11.2-r0": 1554274176 + }, + "origin": "postgresql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libedit.so.0", + "so:libpq.so.5", + "so:libz.so.1" + ], + "provides": [ + "cmd:clusterdb", + "cmd:createdb", + "cmd:createuser", + "cmd:dropdb", + "cmd:dropuser", + "cmd:pg_basebackup", + "cmd:pg_dump", + "cmd:pg_dumpall", + "cmd:pg_isready", + "cmd:pg_receivewal", + "cmd:pg_recvlogical", + "cmd:pg_restore", + "cmd:psql", + "cmd:reindexdb", + "cmd:vacuumdb" + ] + }, + "faac": { + "versions": { + "1.28-r12": 1545165108 + }, + "origin": "faac", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libfaac.so.0=0.0.0", + "cmd:faac" + ] + }, + "py2-tornado": { + "versions": { + "4.5.2-r1": 1542845326 + }, + "origin": "py-tornado", + "dependencies": [ + "py2-backports_abc", + "py2-certifi", + "py2-singledispatch", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "ruby-doc": { + "versions": { + "2.5.5-r0": 1557164832 + }, + "origin": "ruby" + }, + "libxtst-doc": { + "versions": { + "1.2.3-r2": 1543241194 + }, + "origin": "libxtst" + }, + "py3-roman": { + "versions": { + "2.0.0-r3": 1543998690 + }, + "origin": "py-roman", + "dependencies": [ + "python3" + ] + }, + "libasr-doc": { + "versions": { + "1.0.2-r9": 1543933802 + }, + "origin": "libasr" + }, + "libedit": { + "versions": { + "20181209.3.1-r0": 1545839273 + }, + "origin": "libedit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "so:libedit.so.0=0.0.59" + ] + }, + "perl-apache-session-doc": { + "versions": { + "1.93-r0": 1543927695 + }, + "origin": "perl-apache-session" + }, + "c-ares": { + "versions": { + "1.15.0-r0": 1544791695 + }, + "origin": "c-ares", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcares.so.2=2.3.0" + ] + }, + "perl-inline": { + "versions": { + "0.80-r1": 1545067072 + }, + "origin": "perl-inline", + "dependencies": [ + "perl" + ] + }, + "zfs-dev": { + "versions": { + "0.7.12-r1": 1552933678 + }, + "origin": "zfs", + "dependencies": [ + "glib-dev", + "e2fsprogs-dev", + "util-linux-dev", + "libtirpc-dev", + "attr-dev", + "pkgconfig", + "zfs-libs=0.7.12-r1" + ], + "provides": [ + "pc:libzfs=0.7.12", + "pc:libzfs_core=0.7.12" + ] + }, + "perl-email-date-format-doc": { + "versions": { + "1.005-r0": 1543226619 + }, + "origin": "perl-email-date-format" + }, + "qemu-system-xtensaeb": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-xtensaeb" + ] + }, + "syslog-ng-stardate": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "tinyxml": { + "versions": { + "2.6.2-r1": 1545292911 + }, + "origin": "tinyxml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libtinyxml.so.0=0.2.6.2" + ] + }, + "guile": { + "versions": { + "2.0.14-r0": 1543227921 + }, + "origin": "guile", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libguile-2.0.so.22" + ], + "provides": [ + "cmd:guild", + "cmd:guile", + "cmd:guile-snarf", + "cmd:guile-tools" + ] + }, + "heimdal": { + "versions": { + "7.5.0-r2": 1542819374 + }, + "origin": "heimdal", + "dependencies": [ + "krb5-conf", + "so:libasn1.so.8", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libgssapi.so.3", + "so:libhcrypto.so.4", + "so:libhdb.so.9", + "so:libheimbase.so.1", + "so:libheimntlm.so.0", + "so:libhx509.so.5", + "so:libkadm5clnt.so.7", + "so:libkadm5srv.so.8", + "so:libkafs.so.0", + "so:libkdc.so.2", + "so:libkrb5.so.26", + "so:libotp.so.0", + "so:libreadline.so.7", + "so:libroken.so.18", + "so:libsl.so.0", + "so:libwind.so.0" + ], + "provides": [ + "cmd:afslog", + "cmd:bsearch", + "cmd:gsstool", + "cmd:heimdal", + "cmd:heimtools", + "cmd:hprop", + "cmd:hpropd", + "cmd:hxtool", + "cmd:idn-lookup", + "cmd:iprop-log", + "cmd:ipropd-master", + "cmd:ipropd-slave", + "cmd:kadmin", + "cmd:kadmind", + "cmd:kcm", + "cmd:kdc", + "cmd:kdestroy", + "cmd:kf", + "cmd:kfd", + "cmd:kgetcred", + "cmd:kimpersonate", + "cmd:kinit", + "cmd:klist", + "cmd:kpasswd", + "cmd:kpasswdd", + "cmd:kstash", + "cmd:kswitch", + "cmd:ktutil", + "cmd:otp", + "cmd:otpprint", + "cmd:pagsh", + "cmd:su" + ] + }, + "py-gtk-demo": { + "versions": { + "2.24.0-r15": 1543927629 + }, + "origin": "py-gtk", + "dependencies": [ + "py-gtk" + ], + "provides": [ + "cmd:pygtk-demo" + ] + }, + "libvirt-vbox": { + "versions": { + "4.10.0-r1": 1547051658 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=4.10.0-r1", + "libvirt-common-drivers=4.10.0-r1", + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libvirt.so.0", + "so:libxml2.so.2" + ] + }, + "py-itsdangerous": { + "versions": { + "0.24-r3": 1545164553 + }, + "origin": "py-itsdangerous" + }, + "perl-sub-install-doc": { + "versions": { + "0.928-r0": 1542845631 + }, + "origin": "perl-sub-install" + }, + "gdbm-dev": { + "versions": { + "1.13-r1": 1542303058 + }, + "origin": "gdbm", + "dependencies": [ + "gdbm=1.13-r1" + ] + }, + "kamailio-carrierroute": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libsrdb1.so.1", + "so:libtrie.so.1" + ] + }, + "xf86-input-synaptics": { + "versions": { + "1.9.1-r1": 1545154612 + }, + "origin": "xf86-input-synaptics", + "dependencies": [ + "so:libX11.so.6", + "so:libXi.so.6", + "so:libXtst.so.6", + "so:libc.musl-x86_64.so.1", + "so:libevdev.so.2" + ], + "provides": [ + "cmd:synclient", + "cmd:syndaemon" + ] + }, + "udns-doc": { + "versions": { + "0.4-r0": 1542924405 + }, + "origin": "udns" + }, + "mrxvt": { + "versions": { + "0.5.4-r7": 1545073299 + }, + "origin": "mrxvt", + "dependencies": [ + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXpm.so.4", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libjpeg.so.8" + ], + "provides": [ + "cmd:mrxvt" + ] + }, + "perl-parse-syslog-doc": { + "versions": { + "1.10-r3": 1545073946 + }, + "origin": "perl-parse-syslog" + }, + "cppunit-dev": { + "versions": { + "1.14.0-r0": 1543932038 + }, + "origin": "cppunit", + "dependencies": [ + "cppunit=1.14.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:cppunit=1.14.0" + ] + }, + "mosquitto-libs++": { + "versions": { + "1.5.6-r0": 1549970968 + }, + "origin": "mosquitto", + "dependencies": [ + "so:libmosquitto.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libmosquittopp.so.1=1" + ] + }, + "s6-rc": { + "versions": { + "0.4.1.0-r0": 1545076681 + }, + "origin": "s6-rc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexecline.so.2.5", + "so:libs6.so.2.7", + "so:libskarnet.so.2.7" + ], + "provides": [ + "so:libs6rc.so.0.4=0.4.1.0", + "cmd:s6-rc", + "cmd:s6-rc-bundle", + "cmd:s6-rc-compile", + "cmd:s6-rc-db", + "cmd:s6-rc-dryrun", + "cmd:s6-rc-format-upgrade", + "cmd:s6-rc-init", + "cmd:s6-rc-update" + ] + }, + "libdvdread-doc": { + "versions": { + "6.0.0-r0": 1545838475 + }, + "origin": "libdvdread" + }, + "perl-archive-zip": { + "versions": { + "1.64-r0": 1544001402 + }, + "origin": "perl-archive-zip", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:crc32" + ] + }, + "nfs-utils-dev": { + "versions": { + "2.3.2-r1": 1543933420 + }, + "origin": "nfs-utils", + "dependencies": [ + "libnfsidmap=2.3.2-r1", + "pkgconfig" + ], + "provides": [ + "pc:libnfsidmap=2.3.2" + ] + }, + "multipath-tools-doc": { + "versions": { + "0.7.9-r0": 1545224102 + }, + "origin": "multipath-tools" + }, + "subunit-dev": { + "versions": { + "1.2.0-r0": 1543933432 + }, + "origin": "subunit", + "dependencies": [ + "pkgconfig", + "subunit-libs=1.2.0-r0" + ], + "provides": [ + "pc:libcppunit_subunit=1.2.0", + "pc:libsubunit=1.2.0" + ] + }, + "upower-doc": { + "versions": { + "0.99.7-r0": 1545069385 + }, + "origin": "upower" + }, + "acf-amavisd-new": { + "versions": { + "0.4.0-r2": 1545299896 + }, + "origin": "acf-amavisd-new", + "dependencies": [ + "acf-core", + "amavisd-new" + ] + }, + "parted": { + "versions": { + "3.2-r7": 1543935531 + }, + "origin": "parted", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02", + "so:libncursesw.so.6", + "so:libreadline.so.7", + "so:libuuid.so.1" + ], + "provides": [ + "so:libparted-fs-resize.so.0=0.0.1", + "so:libparted.so.2=2.0.1", + "cmd:parted", + "cmd:partprobe" + ] + }, + "freeradius-unixodbc": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ], + "provides": [ + "freeradius3-unixodbc=3.0.17-r5", + "so:rlm_sql_unixodbc.so=0" + ] + }, + "xgamma-doc": { + "versions": { + "1.0.6-r0": 1545214307 + }, + "origin": "xgamma" + }, + "cracklib-dev": { + "versions": { + "2.9.6-r0": 1545060799 + }, + "origin": "cracklib", + "dependencies": [ + "cracklib=2.9.6-r0" + ] + }, + "bitlbee-doc": { + "versions": { + "3.5.1-r4": 1543248420 + }, + "origin": "bitlbee" + }, + "perl-io-async": { + "versions": { + "0.72-r0": 1544799158 + }, + "origin": "perl-io-async", + "dependencies": [ + "perl-struct-dumb", + "perl-future" + ] + }, + "libepoxy": { + "versions": { + "1.5.3-r0": 1545838421 + }, + "origin": "libepoxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libepoxy.so.0=0.0.0" + ] + }, + "ldoc": { + "versions": { + "1.4.6-r1": 1545300108 + }, + "origin": "ldoc", + "dependencies": [ + "lua5.3-penlight", + "lua5.3" + ], + "provides": [ + "cmd:ldoc" + ] + }, + "lua-mosquitto": { + "versions": { + "0.2-r1": 1543932703 + }, + "origin": "lua-mosquitto" + }, + "iceauth-doc": { + "versions": { + "1.0.8-r0": 1545214112 + }, + "origin": "iceauth" + }, + "py2-django-tables2": { + "versions": { + "1.21.2-r0": 1543925702 + }, + "origin": "py-django-tables2", + "dependencies": [ + "py2-django", + "py2-six", + "python2" + ], + "provides": [ + "py-django-tables2-r1.21.2" + ] + }, + "xf86-video-ati": { + "versions": { + "18.1.0-r0": 1545223268 + }, + "origin": "xf86-video-ati", + "dependencies": [ + "mesa-dri-ati", + "so:libc.musl-x86_64.so.1", + "so:libdrm_radeon.so.1", + "so:libgbm.so.1", + "so:libpciaccess.so.0", + "so:libudev.so.1" + ] + }, + "perl-data-uuid": { + "versions": { + "1.221-r1": 1545163354 + }, + "origin": "perl-data-uuid", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libnfnetlink": { + "versions": { + "1.0.1-r1": 1543924449 + }, + "origin": "libnfnetlink", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnfnetlink.so.0=0.2.0" + ] + }, + "lua5.2-dbi-postgresql": { + "versions": { + "0.6-r3": 1545214222 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "linux-firmware-bnx2": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "py-oauth2": { + "versions": { + "1.9.0-r0": 1543245788 + }, + "origin": "py-oauth2", + "dependencies": [ + "python2", + "py-httplib2" + ] + }, + "libnfs-doc": { + "versions": { + "3.0.0-r0": 1544799294 + }, + "origin": "libnfs" + }, + "squashfs-tools": { + "versions": { + "4.3-r5": 1543926510 + }, + "origin": "squashfs-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblz4.so.1", + "so:liblzma.so.5", + "so:liblzo2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:mksquashfs", + "cmd:unsquashfs" + ] + }, + "perl-class-returnvalue": { + "versions": { + "0.55-r1": 1544798602 + }, + "origin": "perl-class-returnvalue", + "dependencies": [ + "perl", + "perl-devel-stacktrace" + ] + }, + "libatomic": { + "versions": { + "8.3.0-r0": 1554745497 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libatomic.so.1=1.2.0" + ] + }, + "libintl": { + "versions": { + "0.19.8.1-r4": 1542304412 + }, + "origin": "gettext", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libintl.so.8=8.1.5" + ] + }, + "mrxvt-doc": { + "versions": { + "0.5.4-r7": 1545073299 + }, + "origin": "mrxvt" + }, + "libltdl": { + "versions": { + "2.4.6-r5": 1542301345 + }, + "origin": "libtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libltdl.so.7=7.3.1" + ] + }, + "apache2-doc": { + "versions": { + "2.4.39-r0": 1554306834 + }, + "origin": "apache2" + }, + "qemu-or1k": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-or1k" + ] + }, + "uwsgi-spooler": { + "versions": { + "2.0.17.1-r0": 1545062210 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "rtmpdump-dev": { + "versions": { + "2.4_git20160909-r6": 1545214999 + }, + "origin": "rtmpdump", + "dependencies": [ + "zlib-dev", + "librtmp=2.4_git20160909-r6", + "pc:gnutls", + "pc:hogweed", + "pc:nettle", + "pkgconfig" + ], + "provides": [ + "pc:librtmp=v2.4" + ] + }, + "samba-common-tools": { + "versions": { + "4.8.11-r1": 1555334972 + }, + "origin": "samba", + "dependencies": [ + "so:libCHARSET3-samba4.so", + "so:libaddns-samba4.so", + "so:libads-samba4.so", + "so:libauth-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-smb-common-samba4.so", + "so:libcli-spoolss-samba4.so", + "so:libcliauth-samba4.so", + "so:libcmdline-contexts-samba4.so", + "so:libcom_err.so.2", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libgssapi-samba4.so.2", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldap-2.4.so.2", + "so:liblibcli-lsa3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnetapi.so.0", + "so:libpopt-samba3-cmdline-samba4.so", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libprinting-migrate-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-modules-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libserver-role-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbd-shim-samba4.so", + "so:libsmbldap.so.2", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so", + "so:libtrusts-util-samba4.so", + "so:libutil-reg-samba4.so", + "so:libutil-tdb-samba4.so", + "so:libwbclient.so.0" + ], + "provides": [ + "so:libgpext-samba4.so=0", + "so:libnet-keytab-samba4.so=0", + "cmd:net", + "cmd:pdbedit", + "cmd:profiles", + "cmd:smbcontrol", + "cmd:smbpasswd", + "cmd:testparm" + ] + }, + "openldap-overlay-syncprov": { + "versions": { + "2.4.47-r2": 1546016480 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "openssl-dev": { + "versions": { + "1.1.1b-r1": 1552660099 + }, + "origin": "openssl", + "dependencies": [ + "libcrypto1.1=1.1.1b-r1", + "libssl1.1=1.1.1b-r1", + "pkgconfig" + ], + "provides": [ + "pc:libcrypto=1.1.1b", + "pc:libssl=1.1.1b", + "pc:openssl=1.1.1b" + ] + }, + "uwsgi-router_radius": { + "versions": { + "2.0.17.1-r0": 1545062207 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "nmap-scripts": { + "versions": { + "7.70-r3": 1545163172 + }, + "origin": "nmap", + "dependencies": [ + "nmap-nselibs" + ] + }, + "libmilter-doc": { + "versions": { + "1.0.2-r6": 1543927484 + }, + "origin": "libmilter" + }, + "ruby-sdbm": { + "versions": { + "2.5.5-r0": 1557164837 + }, + "origin": "ruby", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.5" + ] + }, + "py2-dateutil": { + "versions": { + "2.7.3-r0": 1543248495 + }, + "origin": "py-dateutil", + "dependencies": [ + "py2-six", + "python2" + ] + }, + "perl-datetime-timezone": { + "versions": { + "2.19-r0": 1542973145 + }, + "origin": "perl-datetime-timezone", + "dependencies": [ + "perl-class-singleton", + "perl-params-validationcompiler", + "perl-namespace-autoclean", + "perl-try-tiny", + "perl-module-runtime", + "perl-specio" + ] + }, + "compiler-rt": { + "versions": { + "5.0.2-r0": 1546873979 + }, + "origin": "compiler-rt" + }, + "postgresql-contrib": { + "versions": { + "11.2-r0": 1554274171 + }, + "origin": "postgresql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpq.so.5", + "so:libssl.so.1.1", + "so:libuuid.so.1", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:oid2name", + "cmd:pg_standby", + "cmd:vacuumlo" + ] + }, + "sfic-doc": { + "versions": { + "0.1.7-r6": 1545208618 + }, + "origin": "sfic" + }, + "xev": { + "versions": { + "1.2.2-r0": 1545301876 + }, + "origin": "xev", + "dependencies": [ + "so:libX11.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xev" + ] + }, + "libgudev-dev": { + "versions": { + "230-r2": 1542845392 + }, + "origin": "libgudev", + "dependencies": [ + "libgudev=230-r2", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gudev-1.0=230" + ] + }, + "xen-bridge": { + "versions": { + "4.11.1-r1": 1545075897 + }, + "origin": "xen", + "dependencies": [ + "dnsmasq" + ] + }, + "perl-text-autoformat": { + "versions": { + "1.74-r0": 1545118022 + }, + "origin": "perl-text-autoformat", + "dependencies": [ + "perl", + "perl-text-reform" + ] + }, + "freetds-doc": { + "versions": { + "1.00.104-r0": 1543934550 + }, + "origin": "freetds" + }, + "gnuchess-doc": { + "versions": { + "6.2.5-r1": 1545302331 + }, + "origin": "gnuchess" + }, + "vde2-libs": { + "versions": { + "2.3.2-r10": 1545074025 + }, + "origin": "vde2", + "dependencies": [ + "openssl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libvdehist.so.0=0.0.1", + "so:libvdemgmt.so.0=0.0.1", + "so:libvdeplug.so.3=3.0.1", + "so:libvdesnmp.so.0=0.0.1" + ] + }, + "py3-pytest": { + "versions": { + "4.1.0-r0": 1548112480 + }, + "origin": "pytest", + "dependencies": [ + "py3-atomicwrites", + "py3-py", + "py3-six", + "py3-attrs", + "py3-more-itertools", + "py3-pluggy", + "py3-setuptools", + "python3" + ], + "provides": [ + "cmd:py.test-3", + "cmd:pytest-3" + ] + }, + "dovecot-pigeonhole-plugin-ldap": { + "versions": { + "2.3.6-r0": 1557134098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot-pigeonhole-plugin=2.3.6-r0", + "dovecot-ldap=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "cjdns-openrc": { + "versions": { + "20.2-r1": 1546423250 + }, + "origin": "cjdns" + }, + "lua-evdev": { + "versions": { + "2.2.1-r1": 1545076556 + }, + "origin": "lua-evdev" + }, + "perl-dbd-odbc-doc": { + "versions": { + "1.60-r0": 1545061020 + }, + "origin": "perl-dbd-odbc" + }, + "fakeroot": { + "versions": { + "1.23-r0": 1542304762 + }, + "origin": "fakeroot", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfakeroot-0.so=0", + "cmd:faked", + "cmd:fakeroot" + ] + }, + "libseccomp": { + "versions": { + "2.3.3-r1": 1543923554 + }, + "origin": "libseccomp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libseccomp.so.2=2.3.3", + "cmd:scmp_sys_resolver" + ] + }, + "keyutils": { + "versions": { + "1.6-r0": 1545837151 + }, + "origin": "keyutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libkeyutils.so.1" + ], + "provides": [ + "cmd:key.dns_resolver", + "cmd:keyctl", + "cmd:request-key" + ] + }, + "libxcursor-dev": { + "versions": { + "1.1.15-r1": 1543925379 + }, + "origin": "libxcursor", + "dependencies": [ + "libxcursor=1.1.15-r1", + "pc:x11", + "pc:xfixes", + "pc:xproto", + "pc:xrender", + "pkgconfig" + ], + "provides": [ + "pc:xcursor=1.1.15" + ] + }, + "dhcpcd-openrc": { + "versions": { + "7.0.8-r0": 1545207907 + }, + "origin": "dhcpcd" + }, + "ldb-dev": { + "versions": { + "1.3.8-r0": 1555334661 + }, + "origin": "ldb", + "dependencies": [ + "ldb=1.3.8-r0", + "pc:talloc", + "pc:tdb", + "pkgconfig", + "py2-ldb=1.3.8-r0", + "py3-ldb=1.3.8-r0" + ], + "provides": [ + "pc:ldb=1.3.8", + "pc:pyldb-util.cpython-36m-x86_64-linux-gnu=1.3.8", + "pc:pyldb-util=1.3.8" + ] + }, + "perl-posix-strftime-compiler-doc": { + "versions": { + "0.42-r0": 1544792383 + }, + "origin": "perl-posix-strftime-compiler" + }, + "udns": { + "versions": { + "0.4-r0": 1542924414 + }, + "origin": "udns", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libudns.so.0=0", + "cmd:dnsget", + "cmd:ex-rdns", + "cmd:rblcheck" + ] + }, + "zstd": { + "versions": { + "1.3.8-r0": 1546966444 + }, + "origin": "zstd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:unzstd", + "cmd:zstd", + "cmd:zstdcat", + "cmd:zstdgrep", + "cmd:zstdless", + "cmd:zstdmt" + ] + }, + "perl-gd": { + "versions": { + "2.67-r0": 1543077253 + }, + "origin": "perl-gd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ], + "provides": [ + "cmd:bdf2gdfont.pl" + ] + }, + "lxc-templates": { + "versions": { + "3.1.0-r1": 1549966162 + }, + "origin": "lxc", + "dependencies": [ + "tar" + ] + }, + "py2-pynacl": { + "versions": { + "1.3.0-r1": 1546125263 + }, + "origin": "py-pynacl", + "dependencies": [ + "py2-cffi", + "py2-six", + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "py2-mccabe": { + "versions": { + "0.6.1-r1": 1545060941 + }, + "origin": "py-mccabe", + "dependencies": [ + "python2" + ] + }, + "libcap-doc": { + "versions": { + "2.26-r0": 1546585711 + }, + "origin": "libcap" + }, + "audit-doc": { + "versions": { + "2.8.4-r0": 1543245856 + }, + "origin": "audit" + }, + "jasper-libs": { + "versions": { + "2.0.14-r0": 1543932348 + }, + "origin": "jasper", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8" + ], + "provides": [ + "so:libjasper.so.4=4.0.0" + ] + }, + "postgresql-pllua": { + "versions": { + "2.0-r0": 1544798727 + }, + "origin": "postgresql-pllua", + "dependencies": [ + "postgresql", + "so:libc.musl-x86_64.so.1", + "so:libluajit-5.1.so.2" + ], + "provides": [ + "pllua=2.0-r0" + ] + }, + "lua5.3-file-magic": { + "versions": { + "0.2-r1": 1545116948 + }, + "origin": "lua-file-magic", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libmagic.so.1" + ] + }, + "libxrandr": { + "versions": { + "1.5.1-r2": 1542972233 + }, + "origin": "libxrandr", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXrandr.so.2=2.2.0" + ] + }, + "lutok": { + "versions": { + "0.4-r2": 1542304899 + }, + "origin": "lutok", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:liblua-5.3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:liblutok.so.3=3.0.0" + ] + }, + "fribidi-dev": { + "versions": { + "1.0.5-r0": 1542824048 + }, + "origin": "fribidi", + "dependencies": [ + "fribidi=1.0.5-r0", + "pkgconfig" + ], + "provides": [ + "pc:fribidi=1.0.5" + ] + }, + "distcc-doc": { + "versions": { + "3.3.2-r0": 1544799001 + }, + "origin": "distcc" + }, + "py3-cliapp": { + "versions": { + "1.20170823-r0": 1544798595 + }, + "origin": "py-cliapp", + "dependencies": [ + "python2", + "python3", + "python3" + ] + }, + "wpa_supplicant-openrc": { + "versions": { + "2.7-r2": 1555501504 + }, + "origin": "wpa_supplicant" + }, + "perl-test2-suite-doc": { + "versions": { + "0.000114-r0": 1542973041 + }, + "origin": "perl-test2-suite" + }, + "one-context": { + "versions": { + "0.5.5-r0": 1544798553 + }, + "origin": "one-context", + "dependencies": [ + "blkid" + ] + }, + "libxdmcp-dev": { + "versions": { + "1.1.2-r5": 1542822510 + }, + "origin": "libxdmcp", + "dependencies": [ + "libxdmcp=1.1.2-r5", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xdmcp=1.1.2" + ] + }, + "libtool": { + "versions": { + "2.4.6-r5": 1542301347 + }, + "origin": "libtool", + "dependencies": [ + "bash", + "libltdl" + ], + "provides": [ + "cmd:libtool", + "cmd:libtoolize" + ] + }, + "perl-net-rblclient": { + "versions": { + "0.5-r3": 1545214329 + }, + "origin": "perl-net-rblclient", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:spamalyze" + ] + }, + "file": { + "versions": { + "5.36-r0": 1557160813 + }, + "origin": "file", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmagic.so.1" + ], + "provides": [ + "cmd:file" + ] + }, + "umurmur-doc": { + "versions": { + "0.2.17-r3": 1545207064 + }, + "origin": "umurmur" + }, + "perl-devel-checkbin": { + "versions": { + "0.02-r0": 1542845750 + }, + "origin": "perl-devel-checkbin" + }, + "kamailio-jansson": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libjansson.so.4" + ] + }, + "jack-dev": { + "versions": { + "1.9.12-r0": 1545073438 + }, + "origin": "jack", + "dependencies": [ + "jack=1.9.12-r0", + "pkgconfig" + ], + "provides": [ + "pc:jack=1.9.12" + ] + }, + "freetype-doc": { + "versions": { + "2.9.1-r2": 1542822031 + }, + "origin": "freetype" + }, + "libmodplug-doc": { + "versions": { + "0.8.9.0-r0": 1545208837 + }, + "origin": "libmodplug" + }, + "cyrus-sasl-doc": { + "versions": { + "2.1.27-r1": 1550353527 + }, + "origin": "cyrus-sasl" + }, + "libgit2-tests": { + "versions": { + "0.27.7-r0": 1545838464 + }, + "origin": "libgit2" + }, + "acf-openssh": { + "versions": { + "0.11.2-r0": 1545292772 + }, + "origin": "acf-openssh", + "dependencies": [ + "acf-core", + "openssh" + ] + }, + "qemu-system-unicore32": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-unicore32" + ] + }, + "libtirpc": { + "versions": { + "1.0.3-r0": 1543223618 + }, + "origin": "libtirpc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgssapi_krb5.so.2" + ], + "provides": [ + "so:libtirpc.so.3=3.0.0" + ] + }, + "lua5.2-socket": { + "versions": { + "3.0_rc1_git20160306-r2": 1543934472 + }, + "origin": "lua-socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "mrtg-doc": { + "versions": { + "2.17.7-r0": 1545858156 + }, + "origin": "mrtg" + }, + "libass": { + "versions": { + "0.14.0-r0": 1545300165 + }, + "origin": "libass", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libfribidi.so.0" + ], + "provides": [ + "so:libass.so.9=9.0.2" + ] + }, + "openldap-overlay-refint": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "pkgconf-doc": { + "versions": { + "1.6.0-r0": 1547496958 + }, + "origin": "pkgconf" + }, + "keyutils-doc": { + "versions": { + "1.6-r0": 1545837151 + }, + "origin": "keyutils" + }, + "libmodplug": { + "versions": { + "0.8.9.0-r0": 1545208838 + }, + "origin": "libmodplug", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libmodplug.so.1=1.0.0" + ] + }, + "nagios-plugins-fping": { + "versions": { + "2.2.1-r6": 1543933905 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "fping", + "so:libc.musl-x86_64.so.1" + ] + }, + "ilbc-dev": { + "versions": { + "0.0.1-r0": 1545117064 + }, + "origin": "ilbc", + "dependencies": [ + "ilbc=0.0.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:ilbc=0.0.1" + ] + }, + "rdesktop-doc": { + "versions": { + "1.8.3-r5": 1545068564 + }, + "origin": "rdesktop" + }, + "xmlto-doc": { + "versions": { + "0.0.28-r2": 1542822498 + }, + "origin": "xmlto" + }, + "rarian-dev": { + "versions": { + "0.8.1-r7": 1542823681 + }, + "origin": "rarian", + "dependencies": [ + "pkgconfig", + "rarian=0.8.1-r7" + ], + "provides": [ + "pc:rarian=0.8.1", + "cmd:rarian-sk-config", + "cmd:scrollkeeper-config" + ] + }, + "gobject-introspection-dev": { + "versions": { + "1.56.1-r0": 1542823016 + }, + "origin": "gobject-introspection", + "dependencies": [ + "python3", + "cairo-dev", + "libtool", + "gobject-introspection=1.56.1-r0", + "pc:glib-2.0", + "pc:gmodule-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:libffi", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgio-2.0.so.0", + "so:libgirepository-1.0.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "pc:gobject-introspection-1.0=1.56.1", + "pc:gobject-introspection-no-export-1.0=1.56.1", + "cmd:g-ir-annotation-tool", + "cmd:g-ir-compiler", + "cmd:g-ir-generate", + "cmd:g-ir-inspect", + "cmd:g-ir-scanner" + ] + }, + "udisks2-lang": { + "versions": { + "2.6.5-r1": 1545293057 + }, + "origin": "udisks2" + }, + "qemu-sparc": { + "versions": { + "3.1.0-r3": 1551107302 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sparc" + ] + }, + "font-sun-misc": { + "versions": { + "1.0.3-r0": 1545165113 + }, + "origin": "font-sun-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "libcroco": { + "versions": { + "0.6.12-r1": 1543926546 + }, + "origin": "libcroco", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libcroco-0.6.so.3=3.0.1", + "cmd:csslint-0.6" + ] + }, + "perl-time-hires": { + "versions": { + "1.9758-r0": 1545061218 + }, + "origin": "perl-time-hires", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "mariadb-common": { + "versions": { + "10.3.13-r1": 1557431734 + }, + "origin": "mariadb", + "dependencies": [ + "/bin/sh" + ] + }, + "libxext": { + "versions": { + "1.3.3-r3": 1542822855 + }, + "origin": "libxext", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXext.so.6=6.4.0" + ] + }, + "py-bcrypt": { + "versions": { + "3.1.4-r0": 1545216390 + }, + "origin": "py-bcrypt" + }, + "perl-log-any": { + "versions": { + "1.707-r0": 1545208928 + }, + "origin": "perl-log-any" + }, + "perl-scope-upper": { + "versions": { + "0.31-r0": 1545164596 + }, + "origin": "perl-scope-upper", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-imagesize": { + "versions": { + "1.1.0-r0": 1542824996 + }, + "origin": "py-imagesize", + "dependencies": [ + "python2" + ] + }, + "tiff": { + "versions": { + "4.0.10-r0": 1543921906 + }, + "origin": "tiff", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:libz.so.1" + ], + "provides": [ + "so:libtiff.so.5=5.4.0" + ] + }, + "lxc-lvm": { + "versions": { + "3.1.0-r1": 1549966161 + }, + "origin": "lxc", + "dependencies": [ + "lxc=3.1.0-r1", + "lvm2", + "util-linux" + ] + }, + "py-pynacl": { + "versions": { + "1.3.0-r1": 1546125263 + }, + "origin": "py-pynacl", + "dependencies": [ + "py-cffi", + "py-six" + ] + }, + "py2-jinja2": { + "versions": { + "2.10-r2": 1542824936 + }, + "origin": "py-jinja2", + "dependencies": [ + "py2-markupsafe", + "python2" + ] + }, + "py2-country": { + "versions": { + "18.12.8-r0": 1548109640 + }, + "origin": "py-country", + "dependencies": [ + "python2" + ] + }, + "liboping-doc": { + "versions": { + "1.10.0-r0": 1545214322 + }, + "origin": "liboping" + }, + "syslog-ng-http": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "perl-exporter-tiny-doc": { + "versions": { + "1.002001-r0": 1542845619 + }, + "origin": "perl-exporter-tiny" + }, + "xclip": { + "versions": { + "0.13-r0": 1542883354 + }, + "origin": "xclip", + "dependencies": [ + "so:libX11.so.6", + "so:libXmu.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xclip", + "cmd:xclip-copyfile", + "cmd:xclip-cutfile", + "cmd:xclip-pastefile" + ] + }, + "directfb-doc": { + "versions": { + "1.7.7-r1": 1543934410 + }, + "origin": "directfb" + }, + "sipcalc": { + "versions": { + "1.1.6-r0": 1545164539 + }, + "origin": "sipcalc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sipcalc" + ] + }, + "py-purl": { + "versions": { + "1.4-r0": 1543925756 + }, + "origin": "py-purl", + "dependencies": [ + "py-six" + ] + }, + "mosh": { + "versions": { + "1.3.2-r7": 1543932001 + }, + "origin": "mosh", + "dependencies": [ + "mosh-client", + "mosh-server" + ], + "provides": [ + "cmd:mosh" + ] + }, + "fftw-long-double-libs": { + "versions": { + "3.3.8-r0": 1543926479 + }, + "origin": "fftw", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfftw3l.so.3=3.5.8", + "so:libfftw3l_threads.so.3=3.5.8" + ] + }, + "xf86-video-siliconmotion-doc": { + "versions": { + "1.7.9-r3": 1545235257 + }, + "origin": "xf86-video-siliconmotion" + }, + "hostapd-doc": { + "versions": { + "2.7-r0": 1547822982 + }, + "origin": "hostapd" + }, + "aconf-mod-dnsmasq": { + "versions": { + "0.7.1-r0": 1553432497 + }, + "origin": "aconf", + "dependencies": [ + "aconf", + "dnsmasq" + ] + }, + "libgsasl": { + "versions": { + "1.8.0-r3": 1545215966 + }, + "origin": "libgsasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libidn.so.12" + ], + "provides": [ + "so:libgsasl.so.7=7.9.6" + ] + }, + "rsyslog-uxsock": { + "versions": { + "8.40.0-r3": 1548686792 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "rsyslog-omuxsock=8.40.0-r3" + ] + }, + "nagios-plugins-ntp": { + "versions": { + "2.2.1-r6": 1543933909 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "acf-samba": { + "versions": { + "0.10.0-r2": 1545290860 + }, + "origin": "acf-samba", + "dependencies": [ + "acf-core", + "samba" + ] + }, + "gnutls-dbg": { + "versions": { + "3.6.7-r0": 1555049934 + }, + "origin": "gnutls" + }, + "nagios-plugins-nt": { + "versions": { + "2.2.1-r6": 1543933909 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-mail-dkim-doc": { + "versions": { + "0.54-r0": 1545061192 + }, + "origin": "perl-mail-dkim" + }, + "perl-text-password-pronounceable-doc": { + "versions": { + "0.30-r1": 1543928463 + }, + "origin": "perl-text-password-pronounceable" + }, + "gtkspell": { + "versions": { + "2.0.16-r7": 1545215217 + }, + "origin": "gtkspell", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libenchant.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libgtkspell.so.0=0.0.0" + ] + }, + "font-mutt-misc": { + "versions": { + "1.0.3-r0": 1545216347 + }, + "origin": "font-mutt-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "tinyproxy-doc": { + "versions": { + "1.10.0-r1": 1551109287 + }, + "origin": "tinyproxy" + }, + "libtasn1-doc": { + "versions": { + "4.13-r0": 1542824445 + }, + "origin": "libtasn1" + }, + "sic": { + "versions": { + "1.2-r1": 1543248489 + }, + "origin": "sic", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sic" + ] + }, + "bzip2-dev": { + "versions": { + "1.0.6-r6": 1542300119 + }, + "origin": "bzip2", + "dependencies": [ + "libbz2=1.0.6-r6" + ] + }, + "lua-posix-doc": { + "versions": { + "33.4.0-r1": 1546010825 + }, + "origin": "lua-posix" + }, + "libxmu-doc": { + "versions": { + "1.1.2-r1": 1542883345 + }, + "origin": "libxmu" + }, + "acf-dansguardian": { + "versions": { + "0.8.0-r2": 1545207100 + }, + "origin": "acf-dansguardian", + "dependencies": [ + "acf-core", + "dansguardian" + ] + }, + "font-adobe-utopia-100dpi": { + "versions": { + "1.0.4-r0": 1543924839 + }, + "origin": "font-adobe-utopia-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "py-django-phonenumber-field": { + "versions": { + "2.0.0-r0": 1543925692 + }, + "origin": "py-django-phonenumber-field", + "dependencies": [ + "py-babel", + "py-django", + "py-phonenumbers" + ] + }, + "py2-backports_abc": { + "versions": { + "0.5-r0": 1545293193 + }, + "origin": "py-backports_abc", + "dependencies": [ + "python2" + ] + }, + "qemu-system-sh4": { + "versions": { + "3.1.0-r3": 1551107305 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-sh4" + ] + }, + "libpaper-dev": { + "versions": { + "1.1.24-r4": 1542824768 + }, + "origin": "libpaper", + "dependencies": [ + "libpaper=1.1.24-r4" + ] + }, + "perl-io-html-doc": { + "versions": { + "1.001-r1": 1542821789 + }, + "origin": "perl-io-html" + }, + "openssh-doc": { + "versions": { + "7.9_p1-r5": 1556034587 + }, + "origin": "openssh" + }, + "chrpath-doc": { + "versions": { + "0.16-r1": 1542900311 + }, + "origin": "chrpath" + }, + "perl-namespace-autoclean-doc": { + "versions": { + "0.28-r0": 1542973134 + }, + "origin": "perl-namespace-autoclean" + }, + "tinc-doc": { + "versions": { + "1.0.35-r1": 1545299617 + }, + "origin": "tinc" + }, + "ncurses-libs": { + "versions": { + "6.1_p20190105-r0": 1546948259 + }, + "origin": "ncurses", + "dependencies": [ + "ncurses-terminfo-base", + "ncurses-terminfo=6.1_p20190105-r0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "ncurses-widec-libs=6.1_p20190105-r0", + "so:libformw.so.6=6.1", + "so:libmenuw.so.6=6.1", + "so:libncursesw.so.6=6.1", + "so:libpanelw.so.6=6.1" + ] + }, + "perl-specio-doc": { + "versions": { + "0.42-r0": 1542972980 + }, + "origin": "perl-specio" + }, + "libinput-doc": { + "versions": { + "1.12.4-r0": 1545734494 + }, + "origin": "libinput" + }, + "perl-test-warnings-doc": { + "versions": { + "0.026-r0": 1542845607 + }, + "origin": "perl-test-warnings" + }, + "kamailio-jsdt": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1" + ] + }, + "snappy-dev": { + "versions": { + "1.1.7-r1": 1545208921 + }, + "origin": "snappy", + "dependencies": [ + "snappy=1.1.7-r1" + ] + }, + "mod_dav_svn": { + "versions": { + "1.11.1-r0": 1548692375 + }, + "origin": "subversion", + "dependencies": [ + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libsvn_delta-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_repos-1.so.0", + "so:libsvn_subr-1.so.0" + ] + }, + "twm-doc": { + "versions": { + "1.0.10-r0": 1545209633 + }, + "origin": "twm" + }, + "py2-pbr": { + "versions": { + "5.1.1-r0": 1545235274 + }, + "origin": "py-pbr", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:pbr" + ] + }, + "aaudit-server": { + "versions": { + "0.7.2-r1": 1543934481 + }, + "origin": "aaudit", + "dependencies": [ + "aaudit", + "git", + "lua5.2", + "lua5.2-posix", + "lua5.2-cjson", + "lua5.2-lzlib" + ], + "provides": [ + "cmd:aaudit-repo", + "cmd:aaudit-update-keys" + ] + }, + "libxinerama-dev": { + "versions": { + "1.1.4-r1": 1543077317 + }, + "origin": "libxinerama", + "dependencies": [ + "libxinerama=1.1.4-r1", + "pc:x11", + "pc:xext", + "pc:xineramaproto", + "pkgconfig" + ], + "provides": [ + "pc:xinerama=1.1.4" + ] + }, + "yasm": { + "versions": { + "1.3.0-r1": 1545117180 + }, + "origin": "yasm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:vsyasm", + "cmd:yasm", + "cmd:ytasm" + ] + }, + "pspg": { + "versions": { + "1.6.3-r0": 1545292943 + }, + "origin": "pspg", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libpanelw.so.6", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:pspg" + ] + }, + "linux-firmware-i915": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "aspell-utils": { + "versions": { + "0.60.6.1-r13": 1542965830 + }, + "origin": "aspell", + "dependencies": [ + "aspell", + "perl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:aspell-import", + "cmd:precat", + "cmd:preunzip", + "cmd:prezip", + "cmd:prezip-bin", + "cmd:run-with-aspell", + "cmd:word-list-compress" + ] + }, + "libgomp": { + "versions": { + "8.3.0-r0": 1554745497 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgomp.so.1=1.0.0" + ] + }, + "perl-convert-binhex": { + "versions": { + "1.125-r1": 1544001492 + }, + "origin": "perl-convert-binhex", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:binhex.pl", + "cmd:debinhex.pl" + ] + }, + "net-snmp-agent-libs": { + "versions": { + "5.8-r0": 1543923350 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.35" + ], + "provides": [ + "so:libnetsnmpagent.so.35=35.0.0", + "so:libnetsnmphelpers.so.35=35.0.0", + "so:libnetsnmpmibs.so.35=35.0.0", + "so:libnetsnmptrapd.so.35=35.0.0" + ] + }, + "ppp-minconn": { + "versions": { + "2.4.7-r6": 1543999023 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "yajl-dev": { + "versions": { + "2.1.0-r0": 1543935354 + }, + "origin": "yajl", + "dependencies": [ + "pkgconfig", + "yajl=2.1.0-r0" + ], + "provides": [ + "pc:yajl=2.1.0" + ] + }, + "libmicrohttpd": { + "versions": { + "0.9.62-r0": 1545300154 + }, + "origin": "libmicrohttpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30" + ], + "provides": [ + "so:libmicrohttpd.so.12=12.49.0" + ] + }, + "libffi": { + "versions": { + "3.2.1-r6": 1545154420 + }, + "origin": "libffi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libffi.so.6=6.0.4" + ] + }, + "kamailio-radius": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libfreeradius-client.so.2" + ] + }, + "zmap-doc": { + "versions": { + "2.1.1-r3": 1545062097 + }, + "origin": "zmap" + }, + "py-pygments-doc": { + "versions": { + "2.2.0-r0": 1542824950 + }, + "origin": "py-pygments" + }, + "libnih": { + "versions": { + "1.0.3-r5": 1545062345 + }, + "origin": "libnih", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libexpat.so.1" + ], + "provides": [ + "so:libnih-dbus.so.1=1.0.0", + "so:libnih.so.1=1.0.0", + "cmd:nih-dbus-tool" + ] + }, + "poppler-qt4-dev": { + "versions": { + "0.56.0-r0": 1545254179 + }, + "origin": "poppler-qt4", + "dependencies": [ + "jpeg-dev", + "cairo-dev", + "libxml2-dev", + "fontconfig-dev", + "qt-dev", + "poppler-dev>=0.56.0", + "lcms2-dev", + "openjpeg-dev", + "pkgconfig", + "poppler-qt4=0.56.0-r0" + ], + "provides": [ + "pc:poppler-qt4=0.56.0" + ] + }, + "mailcap": { + "versions": { + "2.1.48-r0": 1545062136 + }, + "origin": "mailcap" + }, + "atf-dev": { + "versions": { + "0.21-r1": 1542304873 + }, + "origin": "atf", + "dependencies": [ + "atf=0.21-r1", + "pkgconfig" + ], + "provides": [ + "pc:atf-c++=0.21", + "pc:atf-c=0.21", + "pc:atf-sh=0.21" + ] + }, + "perl-date-format-doc": { + "versions": { + "2.30-r0": 1543924841 + }, + "origin": "perl-date-format" + }, + "lua-bit32": { + "versions": { + "5.3.0-r2": 1546011988 + }, + "origin": "lua-bit32" + }, + "libx11-doc": { + "versions": { + "1.6.7-r0": 1542822727 + }, + "origin": "libx11" + }, + "jq-dev": { + "versions": { + "1.6-r0": 1544000550 + }, + "origin": "jq", + "dependencies": [ + "jq=1.6-r0" + ] + }, + "libx11-dev": { + "versions": { + "1.6.7-r0": 1542822710 + }, + "origin": "libx11", + "dependencies": [ + "libxcb-dev", + "xtrans", + "libx11=1.6.7-r0", + "pc:kbproto", + "pc:xcb", + "pc:xcb>=1.11.1", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:x11-xcb=1.6.7", + "pc:x11=1.6.7" + ] + }, + "rrsync": { + "versions": { + "3.1.3-r1": 1542820865 + }, + "origin": "rsync", + "dependencies": [ + "rsync", + "perl" + ], + "provides": [ + "cmd:rrsync" + ] + }, + "lua5.2-lxc": { + "versions": { + "3.0.2-r0": 1546416498 + }, + "origin": "lua-lxc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblxc.so.1" + ] + }, + "libcdio-tools": { + "versions": { + "0.94-r2": 1543248376 + }, + "origin": "libcdio", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcddb.so.2", + "so:libcdio.so.16", + "so:libiso9660.so.10", + "so:libncursesw.so.6", + "so:libudf.so.0" + ], + "provides": [ + "cmd:cd-drive", + "cmd:cd-info", + "cmd:cd-read", + "cmd:cdda-player", + "cmd:iso-info", + "cmd:iso-read", + "cmd:mmc-tool" + ] + }, + "openipmi-dev": { + "versions": { + "2.0.25-r1": 1545214269 + }, + "origin": "openipmi", + "dependencies": [ + "openipmi-lanserv=2.0.25-r1", + "openipmi-libs=2.0.25-r1", + "pc:ncurses", + "pkgconfig" + ], + "provides": [ + "pc:OpenIPMI=2.0.25", + "pc:OpenIPMIcmdlang=2.0.25", + "pc:OpenIPMIglib=2.0.25", + "pc:OpenIPMIposix=2.0.25", + "pc:OpenIPMIpthread=2.0.25", + "pc:OpenIPMIui=2.0.25", + "pc:OpenIPMIutils=2.0.25" + ] + }, + "collectd-nginx": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "spandsp": { + "versions": { + "0.0.6-r1": 1545815280 + }, + "origin": "spandsp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtiff.so.5" + ], + "provides": [ + "so:libspandsp.so.2=2.0.0" + ] + }, + "perl-module-versions-report": { + "versions": { + "1.06-r0": 1545067097 + }, + "origin": "perl-module-versions-report", + "dependencies": [ + "perl" + ] + }, + "privoxy-doc": { + "versions": { + "3.0.26-r0": 1545154403 + }, + "origin": "privoxy" + }, + "asterisk-curl": { + "versions": { + "15.7.1-r0": 1546247586 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "poppler-dev": { + "versions": { + "0.56.0-r1": 1542824315 + }, + "origin": "poppler", + "dependencies": [ + "cairo-dev", + "glib-dev", + "pc:cairo>=1.10.0", + "pc:gio-2.0>=2.41", + "pc:glib-2.0>=2.41", + "pc:gobject-2.0>=2.41", + "pkgconfig", + "poppler-glib=0.56.0-r1", + "poppler=0.56.0-r1" + ], + "provides": [ + "pc:poppler-cairo=0.56.0", + "pc:poppler-cpp=0.56.0", + "pc:poppler-glib=0.56.0", + "pc:poppler-splash=0.56.0", + "pc:poppler=0.56.0" + ] + }, + "perl-net-snmp-doc": { + "versions": { + "6.0.1-r2": 1543923074 + }, + "origin": "perl-net-snmp" + }, + "rarian": { + "versions": { + "0.8.1-r7": 1542823683 + }, + "origin": "rarian", + "dependencies": [ + "bash", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:librarian.so.0=0.0.0", + "cmd:rarian-example", + "cmd:rarian-sk-extract", + "cmd:rarian-sk-gen-uuid", + "cmd:rarian-sk-get-cl", + "cmd:rarian-sk-get-content-list", + "cmd:rarian-sk-get-extended-content-list", + "cmd:rarian-sk-get-scripts", + "cmd:rarian-sk-install", + "cmd:rarian-sk-migrate", + "cmd:rarian-sk-preinstall", + "cmd:rarian-sk-rebuild", + "cmd:rarian-sk-update", + "cmd:scrollkeeper-extract", + "cmd:scrollkeeper-gen-seriesid", + "cmd:scrollkeeper-get-cl", + "cmd:scrollkeeper-get-content-list", + "cmd:scrollkeeper-get-extended-content-list", + "cmd:scrollkeeper-get-index-from-docpath", + "cmd:scrollkeeper-get-toc-from-docpath", + "cmd:scrollkeeper-get-toc-from-id", + "cmd:scrollkeeper-install", + "cmd:scrollkeeper-preinstall", + "cmd:scrollkeeper-rebuilddb", + "cmd:scrollkeeper-uninstall", + "cmd:scrollkeeper-update" + ] + }, + "zip": { + "versions": { + "3.0-r7": 1542301519 + }, + "origin": "zip", + "dependencies": [ + "unzip", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:zip", + "cmd:zipcloak", + "cmd:zipnote", + "cmd:zipsplit" + ] + }, + "tlsdate": { + "versions": { + "0.0.13-r7": 1545214008 + }, + "origin": "tlsdate", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.43", + "so:libevent-2.1.so.6", + "so:libssl.so.45" + ], + "provides": [ + "cmd:tlsdate", + "cmd:tlsdate-helper", + "cmd:tlsdated" + ] + }, + "check-dev": { + "versions": { + "0.12.0-r1": 1542822524 + }, + "origin": "check", + "dependencies": [ + "check=0.12.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:check=0.12.0" + ] + }, + "perl-class-mix-doc": { + "versions": { + "0.006-r0": 1545163343 + }, + "origin": "perl-class-mix" + }, + "rsyslog": { + "versions": { + "8.40.0-r3": 1548686792 + }, + "origin": "rsyslog", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libestr.so.0", + "so:libfastjson.so.4", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:rsyslogd" + ] + }, + "spice": { + "versions": { + "0.14.1-r6": 1548919556 + }, + "origin": "spice" + }, + "cairo-tools": { + "versions": { + "1.16.0-r1": 1546948315 + }, + "origin": "cairo", + "dependencies": [ + "so:libX11.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo-script-interpreter.so.2", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libpixman-1.so.0", + "so:libxcb-render.so.0", + "so:libxcb.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:cairo-sphinx", + "cmd:cairo-trace" + ] + }, + "perl-carp-doc": { + "versions": { + "1.38-r0": 1544001404 + }, + "origin": "perl-carp" + }, + "asciidoc-vim": { + "versions": { + "8.6.10-r0": 1542883778 + }, + "origin": "asciidoc" + }, + "alpine-mirrors": { + "versions": { + "3.5.10-r0": 1547140761 + }, + "origin": "alpine-mirrors" + }, + "perl-mailtools": { + "versions": { + "2.19-r0": 1543226635 + }, + "origin": "perl-mailtools", + "dependencies": [ + "perl-timedate" + ], + "provides": [ + "perl-mail-tools=2.19" + ] + }, + "py2-ecdsa": { + "versions": { + "0.13-r5": 1545301309 + }, + "origin": "py-ecdsa", + "dependencies": [ + "py2-crypto" + ] + }, + "ttf-liberation": { + "versions": { + "2.00.1-r1": 1545069526 + }, + "origin": "ttf-liberation", + "dependencies": [ + "fontconfig" + ] + }, + "perl-sub-exporter-progressive-doc": { + "versions": { + "0.001013-r0": 1542883263 + }, + "origin": "perl-sub-exporter-progressive" + }, + "boost-chrono": { + "versions": { + "1.67.0-r2": 1542823630 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.67.0", + "so:libboost_system.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_chrono-mt.so.1.67.0=1.67.0", + "so:libboost_chrono.so.1.67.0=1.67.0" + ] + }, + "kamailio-ldap": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libldap-2.4.so.2", + "so:libsrdb2.so.1" + ] + }, + "libvirt-dev": { + "versions": { + "4.10.0-r1": 1547051657 + }, + "origin": "libvirt", + "dependencies": [ + "libtirpc-dev", + "libvirt-libs=4.10.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:libvirt-admin=4.10.0", + "pc:libvirt-lxc=4.10.0", + "pc:libvirt-qemu=4.10.0", + "pc:libvirt=4.10.0" + ] + }, + "vanessa_adt": { + "versions": { + "0.0.9-r0": 1545223299 + }, + "origin": "vanessa_adt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libvanessa_logger.so.0" + ], + "provides": [ + "so:libvanessa_adt.so.1=1.0.0" + ] + }, + "clucene": { + "versions": { + "2.3.3.4-r5": 1543999112 + }, + "origin": "clucene", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libclucene-core.so.1=2.3.3.4", + "so:libclucene-shared.so.1=2.3.3.4" + ] + }, + "lua-aports": { + "versions": { + "1.0.0-r0": 1547042898 + }, + "origin": "lua-aports", + "dependencies": [ + "lua5.2-cjson", + "lua5.2-filesystem", + "lua5.2-optarg", + "lua5.2" + ], + "provides": [ + "cmd:ap", + "cmd:buildrepo" + ] + }, + "xrefresh": { + "versions": { + "1.0.6-r0": 1545290858 + }, + "origin": "xrefresh", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xrefresh" + ] + }, + "mpg123-doc": { + "versions": { + "1.25.10-r0": 1545117149 + }, + "origin": "mpg123" + }, + "gst-plugins-base-lang": { + "versions": { + "1.14.4-r0": 1543928665 + }, + "origin": "gst-plugins-base" + }, + "acf-lighttpd": { + "versions": { + "0.6.0-r2": 1545207632 + }, + "origin": "acf-lighttpd", + "dependencies": [ + "acf-core", + "lighttpd" + ] + }, + "libxxf86misc-dev": { + "versions": { + "1.0.3-r2": 1543927841 + }, + "origin": "libxxf86misc", + "dependencies": [ + "libxxf86misc=1.0.3-r2", + "pc:x11", + "pc:xext", + "pc:xf86miscproto", + "pkgconfig" + ], + "provides": [ + "pc:xxf86misc=1.0.3" + ] + }, + "lua-xctrl-doc": { + "versions": { + "2015.04.10-r2": 1545208663 + }, + "origin": "lua-xctrl" + }, + "mpfr3-doc": { + "versions": { + "3.1.5-r1": 1542301546 + }, + "origin": "mpfr3" + }, + "libwebp-dev": { + "versions": { + "1.0.1-r0": 1545856968 + }, + "origin": "libwebp", + "dependencies": [ + "libwebp=1.0.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libwebp=1.0.1", + "pc:libwebpdecoder=1.0.1", + "pc:libwebpdemux=1.0.1", + "pc:libwebpmux=1.0.1" + ] + }, + "py-lxml": { + "versions": { + "4.2.5-r0": 1545065048 + }, + "origin": "py-lxml" + }, + "samba-libnss-winbind": { + "versions": { + "4.8.11-r1": 1555334974 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libwbclient.so.0", + "so:libwinbind-client-samba4.so" + ], + "provides": [ + "so:libnss_winbind.so.2=2", + "so:libnss_wins.so.2=2" + ] + }, + "gtk-engines-industrial": { + "versions": { + "2.21.0-r2": 1545289337 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "libmaxminddb-doc": { + "versions": { + "1.3.2-r0": 1543226666 + }, + "origin": "libmaxminddb" + }, + "py3-jwt": { + "versions": { + "1.6.4-r0": 1543249954 + }, + "origin": "py-jwt", + "dependencies": [ + "python3" + ] + }, + "py3-funcsigs": { + "versions": { + "1.0.2-r1": 1542824899 + }, + "origin": "py-funcsigs", + "dependencies": [ + "python3" + ] + }, + "perl-digest-md5-doc": { + "versions": { + "2.55-r2": 1544001428 + }, + "origin": "perl-digest-md5" + }, + "xprop": { + "versions": { + "1.2.3-r0": 1543254086 + }, + "origin": "xprop", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xprop" + ] + }, + "py-cryptography": { + "versions": { + "2.4.2-r2": 1545067167 + }, + "origin": "py-cryptography", + "dependencies": [ + "py-cffi", + "py-idna", + "py-asn1crypto", + "py-six" + ] + }, + "font-util": { + "versions": { + "1.3.1-r2": 1542924710 + }, + "origin": "font-util", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bdftruncate", + "cmd:ucs2any" + ] + }, + "squid-lang-af": { + "versions": { + "4.4-r1": 1545216322 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "fftw-dev": { + "versions": { + "3.3.8-r0": 1543926477 + }, + "origin": "fftw", + "dependencies": [ + "fftw-double-libs=3.3.8-r0", + "fftw-long-double-libs=3.3.8-r0", + "fftw-single-libs=3.3.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:fftw3=3.3.8", + "pc:fftw3f=3.3.8", + "pc:fftw3l=3.3.8" + ] + }, + "supervisor": { + "versions": { + "3.3.4-r1": 1545062792 + }, + "origin": "supervisor", + "dependencies": [ + "python2", + "py-meld3", + "py-setuptools" + ], + "provides": [ + "cmd:echo_supervisord_conf", + "cmd:pidproxy", + "cmd:supervisorctl", + "cmd:supervisord" + ] + }, + "font-cursor-misc": { + "versions": { + "1.0.3-r1": 1542973193 + }, + "origin": "font-cursor-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig", + "util-macros" + ] + }, + "cgdb-doc": { + "versions": { + "0.7.0-r2": 1545214140 + }, + "origin": "cgdb" + }, + "libxscrnsaver-doc": { + "versions": { + "1.2.2-r3": 1543932085 + }, + "origin": "libxscrnsaver" + }, + "lzip": { + "versions": { + "1.20-r0": 1542304994 + }, + "origin": "lzip", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:lzip" + ] + }, + "jpeg-dev": { + "versions": { + "8-r6": 1542893529 + }, + "origin": "jpeg", + "dependencies": [ + "libjpeg-turbo-dev" + ] + }, + "qemu-arm": { + "versions": { + "3.1.0-r3": 1551107299 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-arm" + ] + }, + "py-py": { + "versions": { + "1.5.3-r0": 1542824877 + }, + "origin": "py-py" + }, + "nmap-doc": { + "versions": { + "7.70-r3": 1545163172 + }, + "origin": "nmap" + }, + "libgit2": { + "versions": { + "0.27.7-r0": 1545838466 + }, + "origin": "libgit2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libssh2.so.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "libgit2-libs", + "so:libgit2.so.27=0.27.7" + ] + }, + "squid-lang-hy": { + "versions": { + "4.4-r1": 1545216326 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-extutils-installpaths": { + "versions": { + "0.012-r0": 1542924655 + }, + "origin": "perl-extutils-installpaths", + "dependencies": [ + "perl-extutils-config" + ] + }, + "valgrind": { + "versions": { + "3.14.0-r0": 1545300865 + }, + "origin": "valgrind", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:callgrind_annotate", + "cmd:callgrind_control", + "cmd:cg_annotate", + "cmd:cg_diff", + "cmd:cg_merge", + "cmd:ms_print", + "cmd:valgrind", + "cmd:valgrind-di-server", + "cmd:valgrind-listener", + "cmd:vgdb" + ] + }, + "py3-django": { + "versions": { + "1.11.20-r0": 1551267363 + }, + "origin": "py-django", + "dependencies": [ + "py3-tz", + "python3" + ], + "provides": [ + "cmd:django-admin-3" + ] + }, + "m4": { + "versions": { + "1.4.18-r1": 1542300482 + }, + "origin": "m4", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:m4" + ] + }, + "mdocml-doc": { + "versions": { + "1.14.3-r0": 1542304776 + }, + "origin": "mdocml" + }, + "perl-text-reform": { + "versions": { + "1.20-r0": 1545118017 + }, + "origin": "perl-text-reform", + "dependencies": [ + "perl" + ] + }, + "linux-firmware-av7110": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "libvirt-libs": { + "versions": { + "4.10.0-r1": 1547051657 + }, + "origin": "libvirt", + "dependencies": [ + "lvm2", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libcurl.so.4", + "so:libdbus-1.so.3", + "so:libdevmapper.so.1.02", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libintl.so.8", + "so:libnl-3.so.200", + "so:libsasl2.so.3", + "so:libssh2.so.1", + "so:libtirpc.so.3", + "so:libxml2.so.2", + "so:libyajl.so.2" + ], + "provides": [ + "so:libvirt-admin.so.0=0.4010.0", + "so:libvirt-lxc.so.0=0.4010.0", + "so:libvirt-qemu.so.0=0.4010.0", + "so:libvirt.so.0=0.4010.0" + ] + }, + "vala": { + "versions": { + "0.42.4-r0": 1545859420 + }, + "origin": "vala", + "dependencies": [ + "glib-dev", + "pc:glib-2.0", + "pc:gmodule-2.0", + "pc:gobject-2.0", + "pc:libgvc", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libcgraph.so.6", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgvc.so.6" + ], + "provides": [ + "so:libvala-0.42.so.0=0.0.0", + "so:libvalaccodegen.so=0", + "so:libvaladoc-0.42.so.0=0.0.0", + "pc:libvala-0.42=0.42.4", + "pc:valadoc-0.42=0.42.4", + "pc:vapigen-0.42=0.42.4", + "pc:vapigen=0.42.4", + "cmd:vala", + "cmd:vala-0.42", + "cmd:vala-gen-introspect", + "cmd:vala-gen-introspect-0.42", + "cmd:valac", + "cmd:valac-0.42", + "cmd:valadoc", + "cmd:valadoc-0.42", + "cmd:vapigen", + "cmd:vapigen-0.42" + ] + }, + "gengetopt": { + "versions": { + "2.22.6-r2": 1545062093 + }, + "origin": "gengetopt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:gengetopt" + ] + }, + "subversion-dev": { + "versions": { + "1.11.1-r0": 1548692374 + }, + "origin": "subversion", + "dependencies": [ + "perl-subversion=1.11.1-r0", + "py-subversion=1.11.1-r0", + "subversion-libs=1.11.1-r0" + ] + }, + "apg-doc": { + "versions": { + "2.2.3-r4": 1545207841 + }, + "origin": "apg" + }, + "libgnome-dev": { + "versions": { + "2.32.1-r7": 1545299764 + }, + "origin": "libgnome", + "dependencies": [ + "gtk+3.0-dev", + "gconf-dev", + "libcanberra-dev", + "gnome-vfs-dev", + "libbonobo-dev", + "libgnome=2.32.1-r7", + "pc:ORBit-2.0", + "pc:gconf-2.0", + "pc:glib-2.0", + "pc:gnome-vfs-2.0", + "pc:libbonobo-2.0", + "pc:libcanberra>=0", + "pkgconfig" + ], + "provides": [ + "pc:libgnome-2.0=2.32.1" + ] + }, + "lua-optarg": { + "versions": { + "0.2-r1": 1542820943 + }, + "origin": "lua-optarg" + }, + "libatasmart-doc": { + "versions": { + "0.19-r1": 1545062469 + }, + "origin": "libatasmart" + }, + "sshpass-doc": { + "versions": { + "1.06-r0": 1542985469 + }, + "origin": "sshpass" + }, + "jack-example-clients": { + "versions": { + "1.9.12-r0": 1545073440 + }, + "origin": "jack", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libjack.so.0", + "so:libjacknet.so.0", + "so:libjackserver.so.0", + "so:libreadline.so.7", + "so:libsamplerate.so.0", + "so:libsndfile.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:alsa_in", + "cmd:alsa_out", + "cmd:jack_alias", + "cmd:jack_bufsize", + "cmd:jack_connect", + "cmd:jack_control", + "cmd:jack_cpu", + "cmd:jack_cpu_load", + "cmd:jack_disconnect", + "cmd:jack_evmon", + "cmd:jack_freewheel", + "cmd:jack_iodelay", + "cmd:jack_latent_client", + "cmd:jack_load", + "cmd:jack_lsp", + "cmd:jack_metro", + "cmd:jack_midi_dump", + "cmd:jack_midi_latency_test", + "cmd:jack_midiseq", + "cmd:jack_midisine", + "cmd:jack_monitor_client", + "cmd:jack_multiple_metro", + "cmd:jack_net_master", + "cmd:jack_net_slave", + "cmd:jack_netsource", + "cmd:jack_rec", + "cmd:jack_samplerate", + "cmd:jack_server_control", + "cmd:jack_session_notify", + "cmd:jack_showtime", + "cmd:jack_simdtests", + "cmd:jack_simple_client", + "cmd:jack_simple_session_client", + "cmd:jack_test", + "cmd:jack_thru", + "cmd:jack_transport", + "cmd:jack_unload", + "cmd:jack_wait", + "cmd:jack_zombie" + ] + }, + "linux-firmware-slicoss": { + "versions": { + "20190322-r0": 1554980648 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "perl-test-fatal-doc": { + "versions": { + "0.014-r1": 1542821867 + }, + "origin": "perl-test-fatal" + }, + "perl-net-async-http": { + "versions": { + "0.42-r0": 1545209620 + }, + "origin": "perl-net-async-http", + "dependencies": [ + "perl-io-async", + "perl-future", + "perl-uri", + "perl-http-message", + "perl-struct-dumb" + ] + }, + "libxt-doc": { + "versions": { + "1.1.5-r2": 1542883334 + }, + "origin": "libxt" + }, + "linux-firmware-rtl_bt": { + "versions": { + "20190322-r0": 1554980648 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "fish": { + "versions": { + "2.7.1-r0": 1545302080 + }, + "origin": "fish", + "dependencies": [ + "bc", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libpcre2-32.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:fish", + "cmd:fish_indent", + "cmd:fish_key_reader" + ] + }, + "libmatroska-dev": { + "versions": { + "1.4.9-r0": 1543928899 + }, + "origin": "libmatroska", + "dependencies": [ + "libmatroska=1.4.9-r0", + "pc:libebml", + "pkgconfig" + ], + "provides": [ + "pc:libmatroska=1.4.9" + ] + }, + "libgphoto2-dev": { + "versions": { + "2.5.16-r0": 1545116801 + }, + "origin": "libgphoto2", + "dependencies": [ + "libexif-dev", + "libusb-dev", + "libgphoto2=2.5.16-r0", + "pc:libexif>=0.6.13", + "pkgconfig" + ], + "provides": [ + "pc:libgphoto2=2.5.16", + "pc:libgphoto2_port=0.12.0", + "cmd:gphoto2-config", + "cmd:gphoto2-port-config" + ] + }, + "alpine-ipxe-ipxe_efi": { + "versions": { + "1.0_git20180825-r1": 1545986078 + }, + "origin": "alpine-ipxe" + }, + "perl-namespace-autoclean": { + "versions": { + "0.28-r0": 1542973137 + }, + "origin": "perl-namespace-autoclean", + "dependencies": [ + "perl-b-hooks-endofscope", + "perl-namespace-clean" + ] + }, + "py2-pillow": { + "versions": { + "5.4.1-r0": 1548112614 + }, + "origin": "py-pillow", + "dependencies": [ + "py2-olefile", + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libopenjp2.so.7", + "so:libpython2.7.so.1.0", + "so:libtiff.so.5", + "so:libwebp.so.7", + "so:libwebpdemux.so.2", + "so:libwebpmux.so.3", + "so:libz.so.1" + ] + }, + "libverto-libev": { + "versions": { + "0.3.0-r1": 1543223525 + }, + "origin": "libverto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libev.so.4", + "so:libverto.so.1" + ], + "provides": [ + "so:libverto-libev.so.1=1.0.0" + ] + }, + "soxr": { + "versions": { + "0.1.3-r0": 1545208988 + }, + "origin": "soxr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgomp.so.1" + ], + "provides": [ + "so:libsoxr-lsr.so.0=0.1.9", + "so:libsoxr.so.0=0.1.2" + ] + }, + "libksba-dev": { + "versions": { + "1.3.5-r0": 1543932189 + }, + "origin": "libksba", + "dependencies": [ + "libksba=1.3.5-r0" + ], + "provides": [ + "cmd:ksba-config" + ] + }, + "jfsutils": { + "versions": { + "1.1.15-r1": 1542883492 + }, + "origin": "jfsutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:fsck.jfs", + "cmd:jfs_debugfs", + "cmd:jfs_fsck", + "cmd:jfs_fscklog", + "cmd:jfs_logdump", + "cmd:jfs_mkfs", + "cmd:jfs_tune", + "cmd:mkfs.jfs" + ] + }, + "bc-doc": { + "versions": { + "1.07.1-r0": 1542823693 + }, + "origin": "bc" + }, + "perl-datetime-format-natural-doc": { + "versions": { + "1.06-r0": 1545163113 + }, + "origin": "perl-datetime-format-natural" + }, + "openbox-libs": { + "versions": { + "3.6.1-r2": 1545207327 + }, + "origin": "openbox", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangoxft-1.0.so.0", + "so:librsvg-2.so.2", + "so:libxml2.so.2" + ], + "provides": [ + "so:libobrender.so.32=32.0.0", + "so:libobt.so.2=2.0.2" + ] + }, + "mtx": { + "versions": { + "1.3.12-r2": 1545207723 + }, + "origin": "mtx", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:loaderinfo", + "cmd:mtx", + "cmd:scsieject", + "cmd:scsitape", + "cmd:tapeinfo" + ] + }, + "snort": { + "versions": { + "2.9.12-r0": 1545301013 + }, + "origin": "snort", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libdnet.so.1", + "so:libpcap.so.1", + "so:libpcre.so.1", + "so:libsfbpf.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:snort", + "cmd:u2boat", + "cmd:u2spewfoo" + ] + }, + "perl-list-moreutils-doc": { + "versions": { + "0.419-r1": 1542845623 + }, + "origin": "perl-list-moreutils" + }, + "squid-lang-it": { + "versions": { + "4.4-r1": 1545216326 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-digest-sha1-doc": { + "versions": { + "2.13-r9": 1542924825 + }, + "origin": "perl-digest-sha1" + }, + "fortify-headers": { + "versions": { + "1.0-r0": 1542302757 + }, + "origin": "fortify-headers" + }, + "dahdi-tools": { + "versions": { + "2.11.1-r0": 1543932616 + }, + "origin": "dahdi-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnewt.so.0.52" + ], + "provides": [ + "so:libtonezone.so.2=2.0.0", + "cmd:dahdi_cfg", + "cmd:dahdi_genconf", + "cmd:dahdi_hardware", + "cmd:dahdi_maint", + "cmd:dahdi_monitor", + "cmd:dahdi_registration", + "cmd:dahdi_scan", + "cmd:dahdi_span_assignments", + "cmd:dahdi_span_types", + "cmd:dahdi_speed", + "cmd:dahdi_test", + "cmd:dahdi_tool", + "cmd:dahdi_waitfor_span_assignments", + "cmd:fxotune", + "cmd:lsdahdi", + "cmd:sethdlc", + "cmd:twinstar", + "cmd:xpp_blink", + "cmd:xpp_sync" + ] + }, + "lua5.2-lub": { + "versions": { + "1.1.0-r1": 1544000682 + }, + "origin": "lua-lub", + "dependencies": [ + "lua5.2", + "lua5.2-filesystem" + ] + }, + "xfsprogs-doc": { + "versions": { + "4.19.0-r1": 1546597448 + }, + "origin": "xfsprogs" + }, + "mkinitfs-doc": { + "versions": { + "3.4.1-r0": 1551202041 + }, + "origin": "mkinitfs" + }, + "squid-lang-id": { + "versions": { + "4.4-r1": 1545216326 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "udisks2": { + "versions": { + "2.6.5-r1": 1545293060 + }, + "origin": "udisks2", + "dependencies": [ + "so:libacl.so.1", + "so:libatasmart.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgudev-1.0.so.0", + "so:libpolkit-agent-1.so.0", + "so:libpolkit-gobject-1.so.0", + "so:libudisks2.so.0" + ], + "provides": [ + "cmd:udisksctl", + "cmd:umount.udisks2" + ] + }, + "cjdns": { + "versions": { + "20.2-r1": 1546423250 + }, + "origin": "cjdns", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cjdroute" + ] + }, + "xf86-video-s3virge-doc": { + "versions": { + "1.10.7-r4": 1545069464 + }, + "origin": "xf86-video-s3virge" + }, + "squid-lang-fa": { + "versions": { + "4.4-r1": 1545216325 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "bluez-dev": { + "versions": { + "5.50-r0": 1545822205 + }, + "origin": "bluez", + "dependencies": [ + "bluez-libs=5.50-r0", + "pkgconfig" + ], + "provides": [ + "pc:bluez=5.50" + ] + }, + "linux-firmware-amd": { + "versions": { + "20190322-r0": 1554980655 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "xwininfo-doc": { + "versions": { + "1.1.4-r0": 1545249942 + }, + "origin": "xwininfo" + }, + "libnjb-examples": { + "versions": { + "2.2.7-r4": 1545300191 + }, + "origin": "libnjb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libnjb.so.5", + "so:libz.so.1" + ], + "provides": [ + "cmd:njb-cursesplay", + "cmd:njb-delfile", + "cmd:njb-deltr", + "cmd:njb-dumpeax", + "cmd:njb-dumptime", + "cmd:njb-files", + "cmd:njb-fwupgrade", + "cmd:njb-getfile", + "cmd:njb-getowner", + "cmd:njb-gettr", + "cmd:njb-getusage", + "cmd:njb-handshake", + "cmd:njb-pl", + "cmd:njb-play", + "cmd:njb-playlists", + "cmd:njb-sendfile", + "cmd:njb-sendtr", + "cmd:njb-setowner", + "cmd:njb-setpbm", + "cmd:njb-settime", + "cmd:njb-tagtr", + "cmd:njb-tracks" + ] + }, + "libressl": { + "versions": { + "2.7.5-r0": 1551116871 + }, + "origin": "libressl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.43", + "so:libssl.so.45", + "so:libtls.so.17" + ], + "provides": [ + "cmd:libressl", + "cmd:ocspcheck", + "cmd:openssl" + ] + }, + "nss-dev": { + "versions": { + "3.41-r0": 1545307910 + }, + "origin": "nss", + "dependencies": [ + "nss", + "nspr-dev", + "pc:nspr>=3.41", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libnspr4.so", + "so:libnssutil3.so", + "so:libplc4.so", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgtest1.so.41=41", + "so:libnsssysinit.so=41", + "pc:mozilla-nss=3.41", + "pc:nss-softokn=3.41", + "pc:nss-util=3.41", + "pc:nss=3.41", + "cmd:nss-config" + ] + }, + "libcrystalhd-dev": { + "versions": { + "20130708-r2": 1543924960 + }, + "origin": "libcrystalhd", + "dependencies": [ + "libcrystalhd=20130708-r2" + ] + }, + "linux-firmware-qlogic": { + "versions": { + "20190322-r0": 1554980649 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "nsd": { + "versions": { + "4.1.26-r0": 1546948337 + }, + "origin": "nsd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libevent-2.1.so.6", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:nsd", + "cmd:nsd-checkconf", + "cmd:nsd-checkzone", + "cmd:nsd-control", + "cmd:nsd-control-setup" + ] + }, + "rsyslog-mmaudit": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "glade3-doc": { + "versions": { + "3.8.5-r5": 1543998725 + }, + "origin": "glade3" + }, + "zsh-zftp": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh", + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-firmware-ath6k": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "collectd-sensors": { + "versions": { + "5.8.0-r3": 1546422677 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libsensors.so.4" + ] + }, + "jq-doc": { + "versions": { + "1.6-r0": 1544000549 + }, + "origin": "jq" + }, + "libvirt-static": { + "versions": { + "4.10.0-r1": 1547051657 + }, + "origin": "libvirt", + "dependencies": [ + "lvm2" + ] + }, + "libxxf86dga": { + "versions": { + "1.1.4-r2": 1542845775 + }, + "origin": "libxxf86dga", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXxf86dga.so.1=1.0.0" + ] + }, + "musl-dbg": { + "versions": { + "1.1.20-r4": 1552989415 + }, + "origin": "musl" + }, + "iproute2-bash-completion": { + "versions": { + "4.19.0-r0": 1543223212 + }, + "origin": "iproute2" + }, + "tsocks": { + "versions": { + "1.8_beta5-r0": 1545207039 + }, + "origin": "tsocks", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtsocks.so.1.8=1.8", + "cmd:tsocks" + ] + }, + "cvs-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "mesa-dri-nouveau": { + "versions": { + "18.1.7-r2": 1554455970 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_intel.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libxcb-dri2.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb-xfixes.so.0", + "so:libxcb.so.1", + "so:libxshmfence.so.1", + "so:libz.so.1" + ] + }, + "perl-xml-sax": { + "versions": { + "0.99-r2": 1542893171 + }, + "origin": "perl-xml-sax", + "dependencies": [ + "perl-xml-sax-base", + "perl-xml-namespacesupport", + "/bin/sh" + ] + }, + "json-glib-doc": { + "versions": { + "1.4.4-r0": 1545837175 + }, + "origin": "json-glib" + }, + "lua-sql-postgres": { + "versions": { + "2.3.5-r2": 1543924408 + }, + "origin": "lua-sql" + }, + "py-subversion": { + "versions": { + "1.11.1-r0": 1548692375 + }, + "origin": "subversion", + "dependencies": [ + "so:libapr-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libsvn_client-1.so.0", + "so:libsvn_delta-1.so.0", + "so:libsvn_diff-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_ra-1.so.0", + "so:libsvn_repos-1.so.0", + "so:libsvn_subr-1.so.0", + "so:libsvn_wc-1.so.0" + ], + "provides": [ + "so:libsvn_swig_py-1.so.0=0.0.0" + ] + }, + "py-future": { + "versions": { + "0.17.0-r0": 1545213721 + }, + "origin": "py-future" + }, + "py-django-haystack": { + "versions": { + "2.8.1-r1": 1543925662 + }, + "origin": "py-django-haystack", + "dependencies": [ + "py-django", + "py-setuptools" + ] + }, + "ruby-did_you_mean": { + "versions": { + "2.5.5-r0": 1557164836 + }, + "origin": "ruby", + "dependencies": [ + "ruby-libs" + ] + }, + "perl-net-dns-resolver-programmable": { + "versions": { + "0.009-r1": 1542925012 + }, + "origin": "perl-net-dns-resolver-programmable", + "dependencies": [ + "perl", + "perl-net-dns" + ] + }, + "asterisk-doc": { + "versions": { + "15.7.1-r0": 1546247583 + }, + "origin": "asterisk" + }, + "libsamplerate-doc": { + "versions": { + "0.1.9-r1": 1545068553 + }, + "origin": "libsamplerate" + }, + "py-flake8": { + "versions": { + "3.4.1-r2": 1545223360 + }, + "origin": "py-flake8", + "dependencies": [ + "py-mccabe", + "py-pep8", + "py-pyflakes", + "py3-flake8=3.4.1-r2" + ] + }, + "perl-path-class-doc": { + "versions": { + "0.37-r0": 1543246837 + }, + "origin": "perl-path-class" + }, + "transmission-lang": { + "versions": { + "2.94-r1": 1545208750 + }, + "origin": "transmission" + }, + "libssl1.1": { + "versions": { + "1.1.1b-r1": 1552660176 + }, + "origin": "openssl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ], + "provides": [ + "so:libssl.so.1.1=1.1" + ] + }, + "linux-firmware-nvidia": { + "versions": { + "20190322-r0": 1554980650 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "py2-icu": { + "versions": { + "1.9.8-r2": 1545165266 + }, + "origin": "py-icu", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libicui18n.so.62", + "so:libicuuc.so.62", + "so:libpython2.7.so.1.0", + "so:libstdc++.so.6" + ] + }, + "libxdamage-dev": { + "versions": { + "1.1.4-r2": 1542823760 + }, + "origin": "libxdamage", + "dependencies": [ + "libxdamage=1.1.4-r2", + "pc:damageproto>=1.1", + "pc:x11", + "pc:xfixes", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xdamage=1.1.4" + ] + }, + "libotr-tools": { + "versions": { + "4.1.1-r1": 1543248414 + }, + "origin": "libotr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libotr.so.5" + ], + "provides": [ + "cmd:otr_mackey", + "cmd:otr_modify", + "cmd:otr_parse", + "cmd:otr_readforge", + "cmd:otr_remac", + "cmd:otr_sesskeys" + ] + }, + "strongswan-doc": { + "versions": { + "5.7.2-r1": 1557161392 + }, + "origin": "strongswan" + }, + "lua-inspect-doc": { + "versions": { + "3.1.1-r1": 1545075417 + }, + "origin": "lua-inspect" + }, + "wayland-libs-server": { + "versions": { + "1.16.0-r0": 1544001083 + }, + "origin": "wayland", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6" + ], + "provides": [ + "so:libwayland-server.so.0=0.1.0" + ] + }, + "libqrencode-dev": { + "versions": { + "4.0.2-r0": 1543226702 + }, + "origin": "libqrencode", + "dependencies": [ + "libqrencode=4.0.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libqrencode=4.0.2" + ] + }, + "open-isns-lib": { + "versions": { + "0.97-r4": 1542883211 + }, + "origin": "open-isns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ], + "provides": [ + "so:libisns.so.0=0" + ] + }, + "cdparanoia": { + "versions": { + "10.2-r7": 1543254200 + }, + "origin": "cdparanoia", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcdda_interface.so.0", + "so:libcdda_paranoia.so.0" + ], + "provides": [ + "cmd:cdparanoia" + ] + }, + "nagios-plugins-dbi": { + "versions": { + "2.2.1-r6": 1543933903 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libdbi.so.1" + ] + }, + "lzip-doc": { + "versions": { + "1.20-r0": 1542304994 + }, + "origin": "lzip" + }, + "samba-libs-py": { + "versions": { + "4.8.11-r1": 1555334976 + }, + "origin": "samba", + "dependencies": [ + "so:libMESSAGING-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcliauth-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libdcerpc.so.0", + "so:libevents-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libpytalloc-util.so.2", + "so:libpython2.7.so.1.0", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libserver-role-samba4.so", + "so:libsmbpasswdparser-samba4.so", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0" + ], + "provides": [ + "so:libsamba-net-samba4.so=0", + "so:libsamba-python-samba4.so=0" + ] + }, + "perl-role-basic-doc": { + "versions": { + "0.13-r0": 1543925825 + }, + "origin": "perl-role-basic" + }, + "at-spi2-atk-dev": { + "versions": { + "2.26.2-r0": 1543926534 + }, + "origin": "at-spi2-atk", + "dependencies": [ + "dbus-dev", + "atk-dev", + "at-spi2-atk=2.26.2-r0", + "pc:atspi-2", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:atk-bridge-2.0=2.26.2" + ] + }, + "nagios-plugins-cluster": { + "versions": { + "2.2.1-r6": 1543933902 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "inotify-tools-dev": { + "versions": { + "3.20.1-r1": 1545302266 + }, + "origin": "inotify-tools", + "dependencies": [ + "inotify-tools=3.20.1-r1" + ] + }, + "linux-firmware-brcm": { + "versions": { + "20190322-r0": 1554980653 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "perl-cpanel-json-xs-doc": { + "versions": { + "4.08-r0": 1544792332 + }, + "origin": "perl-cpanel-json-xs" + }, + "linux-firmware-cxgb3": { + "versions": { + "20190322-r0": 1554980653 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "lua5.2-libs": { + "versions": { + "5.2.4-r7": 1542304843 + }, + "origin": "lua5.2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblua-5.2.so.0=0.0.0" + ] + }, + "xf86-video-amdgpu-doc": { + "versions": { + "18.0.1-r2": 1545215978 + }, + "origin": "xf86-video-amdgpu" + }, + "screen-doc": { + "versions": { + "4.6.2-r0": 1545290724 + }, + "origin": "screen" + }, + "xmlrpc-c++": { + "versions": { + "1.39.13-r0": 1545665217 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libxmlrpc.so.3", + "so:libxmlrpc_abyss.so.3", + "so:libxmlrpc_server.so.3", + "so:libxmlrpc_server_abyss.so.3", + "so:libxmlrpc_util.so.3" + ], + "provides": [ + "so:libxmlrpc++.so.8=8.39", + "so:libxmlrpc_abyss++.so.8=8.39", + "so:libxmlrpc_cpp.so.8=8.39", + "so:libxmlrpc_packetsocket.so.8=8.39", + "so:libxmlrpc_server++.so.8=8.39", + "so:libxmlrpc_server_abyss++.so.8=8.39", + "so:libxmlrpc_server_cgi++.so.8=8.39", + "so:libxmlrpc_server_pstream++.so.8=8.39", + "so:libxmlrpc_util++.so.8=8.39" + ] + }, + "xf86-video-dummy": { + "versions": { + "0.3.8-r3": 1545300623 + }, + "origin": "xf86-video-dummy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "rsyslog-mmutf8fix": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "newt-doc": { + "versions": { + "0.52.20-r0": 1543924700 + }, + "origin": "newt" + }, + "motif-doc": { + "versions": { + "2.3.4-r2": 1545223206 + }, + "origin": "motif" + }, + "python2-doc": { + "versions": { + "2.7.16-r1": 1557171398 + }, + "origin": "python2" + }, + "libjpeg-turbo-doc": { + "versions": { + "1.5.3-r4": 1547027583 + }, + "origin": "libjpeg-turbo" + }, + "mesa-dri-vmwgfx": { + "versions": { + "18.1.7-r2": 1554455972 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "fprobe-ulog-doc": { + "versions": { + "1.2-r4": 1543924459 + }, + "origin": "fprobe-ulog" + }, + "libcanberra-doc": { + "versions": { + "0.30-r2": 1545118058 + }, + "origin": "libcanberra" + }, + "quagga-doc": { + "versions": { + "1.2.4-r1": 1545068940 + }, + "origin": "quagga" + }, + "popt-doc": { + "versions": { + "1.16-r7": 1542820790 + }, + "origin": "popt" + }, + "perl-cgi-session-doc": { + "versions": { + "4.48-r0": 1543999426 + }, + "origin": "perl-cgi-session" + }, + "perl-date-manip": { + "versions": { + "6.75-r1": 1546098110 + }, + "origin": "perl-date-manip", + "dependencies": [ + "perl-yaml-syck" + ], + "provides": [ + "cmd:dm_date", + "cmd:dm_zdump" + ] + }, + "libgss": { + "versions": { + "0.1.5-r1": 1543998826 + }, + "origin": "libgss", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgss.so.1=1.1.0", + "cmd:gss" + ] + }, + "perl-test-pod": { + "versions": { + "1.52-r0": 1542822494 + }, + "origin": "perl-test-pod" + }, + "libspf2-tools": { + "versions": { + "1.2.10-r3": 1546267711 + }, + "origin": "libspf2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libspf2.so.2" + ], + "provides": [ + "cmd:spfd", + "cmd:spfquery" + ] + }, + "perl-fcgi": { + "versions": { + "0.78-r2": 1543242209 + }, + "origin": "perl-fcgi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "openldap-back-shell": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "libressl-doc": { + "versions": { + "2.7.5-r0": 1551116864 + }, + "origin": "libressl" + }, + "glib-lang": { + "versions": { + "2.58.1-r2": 1545290825 + }, + "origin": "glib" + }, + "cunit": { + "versions": { + "2.1.3-r1": 1542900198 + }, + "origin": "cunit", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcunit.so.1=1.0.1" + ] + }, + "freetds": { + "versions": { + "1.00.104-r0": 1543934551 + }, + "origin": "freetds", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libodbc.so.2", + "so:libodbcinst.so.2", + "so:libreadline.so.7", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libct.so.4=4.0.0", + "so:libsybdb.so.5=5.1.0", + "so:libtdsodbc.so.0=0.0.0", + "cmd:bsqldb", + "cmd:bsqlodbc", + "cmd:datacopy", + "cmd:defncopy", + "cmd:fisql", + "cmd:freebcp", + "cmd:osql", + "cmd:tdspool", + "cmd:tsql" + ] + }, + "cryptsetup-openrc": { + "versions": { + "2.0.6-r0": 1545746292 + }, + "origin": "cryptsetup" + }, + "xf86-video-vesa-doc": { + "versions": { + "2.3.4-r5": 1545116526 + }, + "origin": "xf86-video-vesa" + }, + "aria2-bash-completion": { + "versions": { + "1.34.0-r1": 1543998971 + }, + "origin": "aria2", + "dependencies": [ + "ca-certificates" + ] + }, + "py-templayer": { + "versions": { + "1.5.1-r3": 1543935169 + }, + "origin": "py-templayer", + "dependencies": [ + "python2" + ] + }, + "perl-file-tail-doc": { + "versions": { + "1.3-r1": 1545116501 + }, + "origin": "perl-file-tail" + }, + "sylpheed-dev": { + "versions": { + "3.7.0-r2": 1544799189 + }, + "origin": "sylpheed", + "dependencies": [ + "sylpheed=3.7.0-r2" + ] + }, + "perl-protocol-websocket-doc": { + "versions": { + "0.20-r0": 1545164019 + }, + "origin": "perl-protocol-websocket" + }, + "razor": { + "versions": { + "2.85-r7": 1545292686 + }, + "origin": "razor", + "dependencies": [ + "perl", + "perl-digest-sha1", + "perl-getopt-long", + "perl-uri", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:razor-admin", + "cmd:razor-check", + "cmd:razor-client", + "cmd:razor-report", + "cmd:razor-revoke" + ] + }, + "lua5.3-sql-mysql": { + "versions": { + "2.3.5-r2": 1543924403 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "acf-apk-tools": { + "versions": { + "0.11.0-r1": 1545249838 + }, + "origin": "acf-apk-tools", + "dependencies": [ + "acf-core", + "lua-posix", + "apk-tools" + ] + }, + "sdl": { + "versions": { + "1.2.15-r9": 1545292836 + }, + "origin": "sdl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL-1.2.so.0=0.11.4" + ] + }, + "htop-doc": { + "versions": { + "2.2.0-r0": 1545207287 + }, + "origin": "htop" + }, + "perl-io-tty-doc": { + "versions": { + "1.12-r4": 1542972211 + }, + "origin": "perl-io-tty" + }, + "swig": { + "versions": { + "3.0.12-r4": 1543228065 + }, + "origin": "swig", + "dependencies": [ + "guile", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpcre.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:ccache-swig", + "cmd:swig" + ] + }, + "s6-portable-utils-doc": { + "versions": { + "2.2.1.2-r0": 1545069441 + }, + "origin": "s6-portable-utils" + }, + "collectd-snmp": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.35" + ] + }, + "libbonobo-doc": { + "versions": { + "2.32.1-r6": 1545299730 + }, + "origin": "libbonobo" + }, + "asciidoc": { + "versions": { + "8.6.10-r0": 1542883778 + }, + "origin": "asciidoc", + "dependencies": [ + "python2", + "libxml2-utils", + "docbook-xsl" + ], + "provides": [ + "cmd:a2x", + "cmd:a2x.py", + "cmd:asciidoc", + "cmd:asciidoc.py" + ] + }, + "lm_sensors-dev": { + "versions": { + "3.4.0-r6": 1542924817 + }, + "origin": "lm_sensors", + "dependencies": [ + "lm_sensors=3.4.0-r6" + ] + }, + "perl-yaml-libyaml-doc": { + "versions": { + "0.72-r0": 1545061179 + }, + "origin": "perl-yaml-libyaml" + }, + "perl-mail-domainkeys-doc": { + "versions": { + "1.0-r2": 1545060514 + }, + "origin": "perl-mail-domainkeys" + }, + "lucene++": { + "versions": { + "3.0.7-r8": 1545256828 + }, + "origin": "lucene++", + "dependencies": [ + "so:libboost_filesystem-mt.so.1.67.0", + "so:libboost_iostreams-mt.so.1.67.0", + "so:libboost_regex-mt.so.1.67.0", + "so:libboost_system-mt.so.1.67.0", + "so:libboost_thread-mt.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:liblucene++-contrib.so.0=3.0.7", + "so:liblucene++.so.0=3.0.7" + ] + }, + "boost-iostreams": { + "versions": { + "1.67.0-r2": 1542823632 + }, + "origin": "boost", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libboost_iostreams-mt.so.1.67.0=1.67.0", + "so:libboost_iostreams.so.1.67.0=1.67.0" + ] + }, + "libtheora-examples": { + "versions": { + "1.1.1-r13": 1543928531 + }, + "origin": "libtheora", + "dependencies": [ + "so:libSDL-1.2.so.0", + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libtheoradec.so.1", + "so:libtheoraenc.so.1", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2" + ], + "provides": [ + "cmd:dump_psnr", + "cmd:dump_video", + "cmd:encoder_example", + "cmd:player_example" + ] + }, + "lua-maxminddb": { + "versions": { + "0.1-r1": 1543226683 + }, + "origin": "lua-maxminddb", + "dependencies": [ + "lua5.1-maxminddb", + "lua5.2-maxminddb", + "lua5.3-maxminddb" + ] + }, + "libseccomp-dev": { + "versions": { + "2.3.3-r1": 1543923552 + }, + "origin": "libseccomp", + "dependencies": [ + "linux-headers", + "libseccomp=2.3.3-r1", + "pkgconfig" + ], + "provides": [ + "pc:libseccomp=2.3.3" + ] + }, + "lvm2": { + "versions": { + "2.02.182-r0": 1543928934 + }, + "origin": "lvm2", + "dependencies": [ + "lvm2-libs=2.02.182-r0", + "so:libaio.so.1", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper-event.so.1.02", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "cmd:lvchange", + "cmd:lvconvert", + "cmd:lvcreate", + "cmd:lvdisplay", + "cmd:lvextend", + "cmd:lvm", + "cmd:lvmconfig", + "cmd:lvmdiskscan", + "cmd:lvmsadc", + "cmd:lvmsar", + "cmd:lvreduce", + "cmd:lvremove", + "cmd:lvrename", + "cmd:lvresize", + "cmd:lvs", + "cmd:lvscan", + "cmd:pvchange", + "cmd:pvck", + "cmd:pvcreate", + "cmd:pvdisplay", + "cmd:pvmove", + "cmd:pvremove", + "cmd:pvresize", + "cmd:pvs", + "cmd:pvscan", + "cmd:vgcfgbackup", + "cmd:vgcfgrestore", + "cmd:vgchange", + "cmd:vgck", + "cmd:vgconvert", + "cmd:vgcreate", + "cmd:vgdisplay", + "cmd:vgexport", + "cmd:vgextend", + "cmd:vgimport", + "cmd:vgimportclone", + "cmd:vgmerge", + "cmd:vgmknodes", + "cmd:vgreduce", + "cmd:vgremove", + "cmd:vgrename", + "cmd:vgs", + "cmd:vgscan", + "cmd:vgsplit" + ] + }, + "perl-lwp-useragent-determined": { + "versions": { + "1.07-r0": 1545062801 + }, + "origin": "perl-lwp-useragent-determined", + "dependencies": [ + "perl", + "perl-libwww" + ] + }, + "lua-cjson": { + "versions": { + "2.1.0-r8": 1542820885 + }, + "origin": "lua-cjson" + }, + "perl-test-inter": { + "versions": { + "1.07-r0": 1543077266 + }, + "origin": "perl-test-inter" + }, + "libgss-dev": { + "versions": { + "0.1.5-r1": 1543998824 + }, + "origin": "libgss", + "dependencies": [ + "pkgconfig", + "libgss=0.1.5-r1" + ], + "provides": [ + "pc:gss=0.1.5" + ] + }, + "sessreg": { + "versions": { + "1.1.1-r1": 1542985395 + }, + "origin": "sessreg", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sessreg" + ] + }, + "perl-test-simple-doc": { + "versions": { + "1.302141-r0": 1544792380 + }, + "origin": "perl-test-simple" + }, + "unfs3-doc": { + "versions": { + "0.9.22-r4": 1545222983 + }, + "origin": "unfs3" + }, + "perl-module-metadata-doc": { + "versions": { + "1.000033-r0": 1542845543 + }, + "origin": "perl-module-metadata" + }, + "hvtools": { + "versions": { + "4.11.9-r0": 1543249741 + }, + "origin": "hvtools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:hv_fcopy_daemon", + "cmd:hv_kvp_daemon", + "cmd:hv_vss_daemon" + ] + }, + "elfutils-libelf": { + "versions": { + "0.168-r2": 1546849158 + }, + "origin": "elfutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libelf.so.1=0" + ] + }, + "boost-graph": { + "versions": { + "1.67.0-r2": 1542823632 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_regex-mt.so.1.67.0", + "so:libboost_regex.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_graph-mt.so.1.67.0=1.67.0", + "so:libboost_graph.so.1.67.0=1.67.0" + ] + }, + "frotz": { + "versions": { + "2.44-r0": 1545293068 + }, + "origin": "frotz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:frotz" + ] + }, + "libpcre16": { + "versions": { + "8.42-r1": 1545067005 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre16.so.0=0.2.10" + ] + }, + "lua-pc": { + "versions": { + "1.0.0-r9": 1543928870 + }, + "origin": "lua-pc", + "dependencies": [ + "lua5.1-pc" + ] + }, + "gsl": { + "versions": { + "2.5-r0": 1545117968 + }, + "origin": "gsl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgsl.so.23=23.1.0", + "so:libgslcblas.so.0=0.0.0", + "cmd:gsl-histogram", + "cmd:gsl-randist" + ] + }, + "f2fs-tools-libs": { + "versions": { + "1.12.0-r0": 1546344802 + }, + "origin": "f2fs-tools", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libf2fs.so.6=6.0.0", + "so:libf2fs_format.so.5=5.0.0" + ] + }, + "collectd-dev": { + "versions": { + "5.8.0-r3": 1546422675 + }, + "origin": "collectd", + "dependencies": [ + "collectd-libs=5.8.0-r3", + "pkgconfig" + ], + "provides": [ + "pc:libcollectdclient=5.8.0" + ] + }, + "dmvpn-ca": { + "versions": { + "1.0.2-r0": 1553425621 + }, + "origin": "dmvpn", + "dependencies": [ + "lua5.2", + "lua5.2-lyaml", + "lua5.2-ossl", + "lua5.2-posix", + "lua5.2-sql-sqlite3", + "lua5.2-stringy", + "lua-asn1", + "lua-dmvpn" + ], + "provides": [ + "cmd:dmvpn-ca" + ] + }, + "perl-regexp-ipv6-doc": { + "versions": { + "0.03-r0": 1545289352 + }, + "origin": "perl-regexp-ipv6" + }, + "c-client": { + "versions": { + "2007f-r9": 1545062297 + }, + "origin": "imap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libc-client.so.1=1.0.0" + ] + }, + "neon-dev": { + "versions": { + "0.30.2-r5": 1542883669 + }, + "origin": "neon", + "dependencies": [ + "expat-dev", + "openssl-dev", + "zlib-dev", + "neon=0.30.2-r5", + "pkgconfig" + ], + "provides": [ + "pc:neon=0.30.2", + "cmd:neon-config" + ] + }, + "mcabber-dev": { + "versions": { + "1.1.0-r2": 1545214061 + }, + "origin": "mcabber", + "dependencies": [ + "pc:glib-2.0", + "pc:gmodule-2.0", + "pc:loudmouth-1.0", + "pkgconfig" + ], + "provides": [ + "pc:mcabber=1.1.0" + ] + }, + "perl-sub-identify-doc": { + "versions": { + "0.14-r1": 1542973112 + }, + "origin": "perl-sub-identify" + }, + "expat-dev": { + "versions": { + "2.2.6-r0": 1542302776 + }, + "origin": "expat", + "dependencies": [ + "expat=2.2.6-r0", + "pkgconfig" + ], + "provides": [ + "pc:expat=2.2.6" + ] + }, + "pound": { + "versions": { + "2.8-r0": 1545207004 + }, + "origin": "pound", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpcreposix.so.0", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:pound", + "cmd:poundctl" + ] + }, + "libssh2": { + "versions": { + "1.8.2-r0": 1553681860 + }, + "origin": "libssh2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libssh2.so.1=1.0.1" + ] + }, + "speexdsp-doc": { + "versions": { + "1.2_rc3-r5": 1544799235 + }, + "origin": "speexdsp" + }, + "fish-dev": { + "versions": { + "2.7.1-r0": 1545302078 + }, + "origin": "fish", + "dependencies": [ + "fish-tools" + ] + }, + "gcr-lang": { + "versions": { + "3.28.0-r0": 1545300012 + }, + "origin": "gcr" + }, + "libsecret-dev": { + "versions": { + "0.18.6-r0": 1545291102 + }, + "origin": "libsecret", + "dependencies": [ + "glib-dev", + "libsecret=0.18.6-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libsecret-1=0.18.6", + "pc:libsecret-unstable=0.18.6" + ] + }, + "perl-text-csv-doc": { + "versions": { + "1.97-r0": 1545163609 + }, + "origin": "perl-text-csv" + }, + "perl-http-negotiate-doc": { + "versions": { + "6.01-r1": 1542821862 + }, + "origin": "perl-http-negotiate" + }, + "perl-devel-checklib-doc": { + "versions": { + "1.13-r0": 1543922455 + }, + "origin": "perl-devel-checklib" + }, + "xf86-video-sunleo": { + "versions": { + "1.2.2-r3": 1545293002 + }, + "origin": "xf86-video-sunleo", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "xcb-util-wm": { + "versions": { + "0.4.1-r1": 1543927935 + }, + "origin": "xcb-util-wm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-ewmh.so.2=2.0.0", + "so:libxcb-icccm.so.4=4.0.0" + ] + }, + "libsigc++": { + "versions": { + "2.10.0-r1": 1543077171 + }, + "origin": "libsigc++", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libsigc-2.0.so.0=0.0.0" + ] + }, + "boost-wserialization": { + "versions": { + "1.67.0-r2": 1542823634 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_serialization-mt.so.1.67.0", + "so:libboost_serialization.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_wserialization-mt.so.1.67.0=1.67.0", + "so:libboost_wserialization.so.1.67.0=1.67.0" + ] + }, + "py-genshi": { + "versions": { + "0.7-r0": 1543924847 + }, + "origin": "py-genshi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "celt051": { + "versions": { + "0.5.1.3-r0": 1543935762 + }, + "origin": "celt051", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0" + ], + "provides": [ + "so:libcelt051.so.0=0.0.0", + "cmd:celtdec051", + "cmd:celtenc051" + ] + }, + "libxres-doc": { + "versions": { + "1.2.0-r1": 1545302056 + }, + "origin": "libxres" + }, + "flex-libs": { + "versions": { + "2.6.4-r1": 1542300944 + }, + "origin": "flex", + "dependencies": [ + "m4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfl.so.2=2.0.0" + ] + }, + "perl-data-page-doc": { + "versions": { + "2.02-r1": 1544000368 + }, + "origin": "perl-data-page" + }, + "libmtp": { + "versions": { + "1.1.15-r0": 1543924442 + }, + "origin": "libmtp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libmtp.so.9=9.4.0" + ] + }, + "libblkid": { + "versions": { + "2.33-r0": 1545307436 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libblkid.so.1=1.1.0" + ] + }, + "alsa-utils": { + "versions": { + "1.1.8-r0": 1547112822 + }, + "origin": "alsa-utils", + "dependencies": [ + "dialog", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libfftw3f.so.3", + "so:libformw.so.6", + "so:libmenuw.so.6", + "so:libncursesw.so.6", + "so:libpanelw.so.6" + ], + "provides": [ + "cmd:aconnect", + "cmd:alsa-info.sh", + "cmd:alsabat", + "cmd:alsabat-test.sh", + "cmd:alsactl", + "cmd:alsaloop", + "cmd:alsamixer", + "cmd:alsatplg", + "cmd:alsaucm", + "cmd:amidi", + "cmd:amixer", + "cmd:aplay", + "cmd:aplaymidi", + "cmd:arecord", + "cmd:arecordmidi", + "cmd:aseqdump", + "cmd:aseqnet", + "cmd:axfer", + "cmd:iecset", + "cmd:speaker-test" + ] + }, + "lua5.2-curl": { + "versions": { + "0.3.8-r1": 1545224117 + }, + "origin": "lua-curl", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "py2-httplib2": { + "versions": { + "0.12.0-r0": 1548112518 + }, + "origin": "py-httplib2", + "dependencies": [ + "python2" + ] + }, + "nginx-vim": { + "versions": { + "1.14.2-r1": 1557179819 + }, + "origin": "nginx" + }, + "mesa-vulkan-intel": { + "versions": { + "18.1.7-r2": 1554455972 + }, + "origin": "mesa", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libwayland-client.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb.so.1", + "so:libxshmfence.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libvulkan_intel.so=0" + ] + }, + "nginx-mod-http-nchan": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-alabaster": { + "versions": { + "0.7.12-r0": 1542824966 + }, + "origin": "py-alabaster", + "dependencies": [ + "python2" + ] + }, + "nano": { + "versions": { + "3.2-r0": 1545292880 + }, + "origin": "nano", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmagic.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:nano", + "cmd:rnano" + ] + }, + "libcanberra": { + "versions": { + "0.30-r2": 1545118059 + }, + "origin": "libcanberra", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libltdl.so.7", + "so:libvorbisfile.so.3" + ], + "provides": [ + "so:libcanberra.so.0=0.2.5" + ] + }, + "libxft-doc": { + "versions": { + "2.3.2-r3": 1542824036 + }, + "origin": "libxft" + }, + "lua5.1-microlight": { + "versions": { + "1.1.1-r2": 1543249965 + }, + "origin": "lua-microlight" + }, + "libpciaccess": { + "versions": { + "0.14-r0": 1545838880 + }, + "origin": "libpciaccess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpciaccess.so.0=0.11.1" + ] + }, + "libbonobo-dev": { + "versions": { + "2.32.1-r6": 1545299729 + }, + "origin": "libbonobo", + "dependencies": [ + "gtk+2.0-dev", + "libidl-dev", + "orbit2-dev", + "popt-dev", + "libxml2-dev", + "libbonobo=2.32.1-r6", + "pc:ORBit-2.0", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pkgconfig" + ], + "provides": [ + "pc:bonobo-activation-2.0=2.32.1", + "pc:libbonobo-2.0=2.32.1" + ] + }, + "perl-class-inspector-doc": { + "versions": { + "1.32-r0": 1543238829 + }, + "origin": "perl-class-inspector" + }, + "lz4-tests": { + "versions": { + "1.8.3-r2": 1545154434 + }, + "origin": "lz4", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libpng-static": { + "versions": { + "1.6.37-r0": 1557129094 + }, + "origin": "libpng" + }, + "py-uritemplate": { + "versions": { + "3.0.0-r1": 1545207296 + }, + "origin": "py-uritemplate" + }, + "xdm": { + "versions": { + "1.1.11-r5": 1542985410 + }, + "origin": "xdm", + "dependencies": [ + "sessreg", + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXaw.so.7", + "so:libXdmcp.so.6", + "so:libXext.so.6", + "so:libXmu.so.6", + "so:libXpm.so.4", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0" + ], + "provides": [ + "cmd:xdm" + ] + }, + "perl-server-starter-doc": { + "versions": { + "0.15-r1": 1543249940 + }, + "origin": "perl-server-starter" + }, + "lua5.2-subprocess": { + "versions": { + "0.0.20141229-r2": 1542883782 + }, + "origin": "lua-subprocess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "ntfs-3g": { + "versions": { + "2017.3.23-r1": 1542973176 + }, + "origin": "ntfs-3g", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libntfs-3g.so.88" + ], + "provides": [ + "cmd:lowntfs-3g", + "cmd:ntfs-3g" + ] + }, + "openvswitch-bash-completion": { + "versions": { + "2.10.1-r0": 1544000344 + }, + "origin": "openvswitch" + }, + "cpulimit": { + "versions": { + "0.2-r0": 1545062299 + }, + "origin": "cpulimit", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cpulimit" + ] + }, + "libxcursor": { + "versions": { + "1.1.15-r1": 1543925380 + }, + "origin": "libxcursor", + "dependencies": [ + "so:libX11.so.6", + "so:libXfixes.so.3", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXcursor.so.1=1.0.2" + ] + }, + "nagios-plugins-ldap": { + "versions": { + "2.2.1-r6": 1543933907 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libldap-2.4.so.2" + ] + }, + "tk-doc": { + "versions": { + "8.6.9-r0": 1545915088 + }, + "origin": "tk" + }, + "py2-bcrypt": { + "versions": { + "3.1.4-r0": 1545216388 + }, + "origin": "py-bcrypt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "eudev": { + "versions": { + "3.2.7-r0": 1542845385 + }, + "origin": "eudev", + "dependencies": [ + "udev-init-scripts", + "eudev-libs=3.2.7-r0", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libkmod.so.2" + ], + "provides": [ + "udev=176", + "cmd:setup-udev", + "cmd:udevadm", + "cmd:udevd" + ] + }, + "xcb-util-wm-dev": { + "versions": { + "0.4.1-r1": 1543927933 + }, + "origin": "xcb-util-wm", + "dependencies": [ + "xcb-util-dev", + "pkgconfig", + "xcb-util-wm=0.4.1-r1" + ], + "provides": [ + "pc:xcb-ewmh=0.4.1", + "pc:xcb-icccm=0.4.1" + ] + }, + "xf86-video-ark-doc": { + "versions": { + "0.7.5-r10": 1543928085 + }, + "origin": "xf86-video-ark" + }, + "libisoburn": { + "versions": { + "1.4.8-r0": 1544797306 + }, + "origin": "libisoburn", + "dependencies": [ + "so:libburn.so.4", + "so:libc.musl-x86_64.so.1", + "so:libedit.so.0", + "so:libisofs.so.6" + ], + "provides": [ + "so:libisoburn.so.1=1.105.0" + ] + }, + "freeswitch": { + "versions": { + "1.8.2-r1": 1545117893 + }, + "origin": "freeswitch", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libedit.so.0", + "so:libgcc_s.so.1", + "so:libilbc.so.0", + "so:libjpeg.so.8", + "so:libldns.so.2", + "so:liblua-5.3.so.0", + "so:libmp3lame.so.0", + "so:libmpg123.so.0", + "so:libncursesw.so.6", + "so:libodbc.so.2", + "so:libopus.so.0", + "so:libpcre.so.1", + "so:libportaudio.so.2", + "so:libpq.so.5", + "so:libshout.so.3", + "so:libsndfile.so.1", + "so:libspeex.so.1", + "so:libspeexdsp.so.1", + "so:libsqlite3.so.0", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libtiff.so.5", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libfreeswitch.so.1=1.0.0", + "cmd:freeswitch", + "cmd:fs_cli", + "cmd:fs_encode", + "cmd:fs_ivrd", + "cmd:fsxs", + "cmd:gentls_cert", + "cmd:tone2wav" + ] + }, + "pcsc-lite-doc": { + "versions": { + "1.8.24-r1": 1545060761 + }, + "origin": "pcsc-lite" + }, + "nagios-plugins-ups": { + "versions": { + "2.2.1-r6": 1543933913 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-ipaddress": { + "versions": { + "1.0.22-r0": 1543926515 + }, + "origin": "py-ipaddress" + }, + "re2c": { + "versions": { + "1.1.1-r0": 1542822316 + }, + "origin": "re2c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:re2c" + ] + }, + "sendpage-doc": { + "versions": { + "1.0.3-r5": 1545116828 + }, + "origin": "sendpage" + }, + "xorg-server": { + "versions": { + "1.20.3-r1": 1543928069 + }, + "origin": "xorg-server", + "dependencies": [ + "font-misc-misc", + "font-cursor-misc", + "xkeyboard-config", + "xkbcomp", + "xinit", + "so:libGL.so.1", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libdrm.so.2", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libpciaccess.so.0", + "so:libpixman-1.so.0", + "so:libudev.so.1", + "so:libxshmfence.so.1" + ], + "provides": [ + "cmd:X", + "cmd:Xorg", + "cmd:cvt", + "cmd:gtf" + ] + }, + "orbit2-dev": { + "versions": { + "2.14.19-r4": 1543223377 + }, + "origin": "orbit2", + "dependencies": [ + "orbit2=2.14.19-r4", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pc:libIDL-2.0", + "pkgconfig" + ], + "provides": [ + "pc:ORBit-2.0=2.14.19", + "pc:ORBit-CosNaming-2.0=2.14.19", + "pc:ORBit-idl-2.0=2.14.19", + "pc:ORBit-imodule-2.0=2.14.19", + "cmd:orbit2-config" + ] + }, + "glibmm": { + "versions": { + "2.56.0-r1": 1544799352 + }, + "origin": "glibmm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgiomm-2.4.so.1=1.3.0", + "so:libglibmm-2.4.so.1=1.3.0", + "so:libglibmm_generate_extra_defs-2.4.so.1=1.3.0" + ] + }, + "py3-pylast": { + "versions": { + "2.4.0-r0": 1545301100 + }, + "origin": "py-pylast", + "dependencies": [ + "python3" + ] + }, + "rsyslog-snmp": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.35" + ], + "provides": [ + "rsyslog-omsnmp=8.40.0-r3" + ] + }, + "xf86-input-keyboard": { + "versions": { + "1.9.0-r1": 1545073858 + }, + "origin": "xf86-input-keyboard", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "minicom": { + "versions": { + "2.7.1-r0": 1544799266 + }, + "origin": "minicom", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:ascii-xfr", + "cmd:minicom", + "cmd:runscript", + "cmd:xminicom" + ] + }, + "mesa-osmesa": { + "versions": { + "18.1.7-r2": 1554455971 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libOSMesa.so.8=8.0.0" + ] + }, + "libxcb": { + "versions": { + "1.13-r2": 1542822659 + }, + "origin": "libxcb", + "dependencies": [ + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxcb-composite.so.0=0.0.0", + "so:libxcb-damage.so.0=0.0.0", + "so:libxcb-dpms.so.0=0.0.0", + "so:libxcb-dri2.so.0=0.0.0", + "so:libxcb-dri3.so.0=0.0.0", + "so:libxcb-glx.so.0=0.0.0", + "so:libxcb-present.so.0=0.0.0", + "so:libxcb-randr.so.0=0.1.0", + "so:libxcb-record.so.0=0.0.0", + "so:libxcb-render.so.0=0.0.0", + "so:libxcb-res.so.0=0.0.0", + "so:libxcb-screensaver.so.0=0.0.0", + "so:libxcb-shape.so.0=0.0.0", + "so:libxcb-shm.so.0=0.0.0", + "so:libxcb-sync.so.1=1.0.0", + "so:libxcb-xf86dri.so.0=0.0.0", + "so:libxcb-xfixes.so.0=0.0.0", + "so:libxcb-xinerama.so.0=0.0.0", + "so:libxcb-xinput.so.0=0.1.0", + "so:libxcb-xkb.so.1=1.0.0", + "so:libxcb-xtest.so.0=0.0.0", + "so:libxcb-xv.so.0=0.0.0", + "so:libxcb-xvmc.so.0=0.0.0", + "so:libxcb.so.1=1.1.0" + ] + }, + "jwm-doc": { + "versions": { + "2.3.7-r0": 1545292614 + }, + "origin": "jwm" + }, + "opensp-lang": { + "versions": { + "1.5.2-r0": 1542893110 + }, + "origin": "opensp" + }, + "py-larch": { + "versions": { + "1.20131130-r1": 1545209178 + }, + "origin": "py-larch", + "dependencies": [ + "python2", + "py-tracing", + "py-cliapp", + "py-ttystatus" + ], + "provides": [ + "cmd:fsck-larch" + ] + }, + "py-click": { + "versions": { + "6.7-r2": 1545116992 + }, + "origin": "py-click" + }, + "luajit-dev": { + "versions": { + "2.1.0_beta3-r4": 1542820926 + }, + "origin": "luajit", + "dependencies": [ + "luajit=2.1.0_beta3-r4", + "pkgconfig" + ] + }, + "libshout-doc": { + "versions": { + "2.4.1-r5": 1545117135 + }, + "origin": "libshout" + }, + "lua-lyaml": { + "versions": { + "6.2-r3": 1545076423 + }, + "origin": "lua-lyaml" + }, + "libxt": { + "versions": { + "1.1.5-r2": 1542883335 + }, + "origin": "libxt", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXt.so.6=6.0.0" + ] + }, + "lua5.2-filesystem": { + "versions": { + "1.7.0.2-r0": 1542820934 + }, + "origin": "lua-filesystem", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "desktop-file-utils": { + "versions": { + "0.23-r1": 1545116737 + }, + "origin": "desktop-file-utils", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:desktop-file-edit", + "cmd:desktop-file-install", + "cmd:desktop-file-validate", + "cmd:update-desktop-database" + ] + }, + "py3-attrs": { + "versions": { + "18.2.0-r0": 1542824881 + }, + "origin": "py-attrs", + "dependencies": [ + "python3" + ], + "provides": [ + "py-attrs-tools" + ] + }, + "dovecot-openrc": { + "versions": { + "2.3.6-r0": 1557134097 + }, + "origin": "dovecot", + "dependencies": [ + "openssl" + ] + }, + "ruby-minitest": { + "versions": { + "2.5.5-r0": 1557164837 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ] + }, + "ebtables": { + "versions": { + "2.0.10.4-r2": 1543935617 + }, + "origin": "ebtables", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libebt_802_3.so=0", + "so:libebt_among.so=0", + "so:libebt_arp.so=0", + "so:libebt_arpreply.so=0", + "so:libebt_ip.so=0", + "so:libebt_ip6.so=0", + "so:libebt_limit.so=0", + "so:libebt_log.so=0", + "so:libebt_mark.so=0", + "so:libebt_mark_m.so=0", + "so:libebt_nat.so=0", + "so:libebt_nflog.so=0", + "so:libebt_pkttype.so=0", + "so:libebt_redirect.so=0", + "so:libebt_standard.so=0", + "so:libebt_stp.so=0", + "so:libebt_ulog.so=0", + "so:libebt_vlan.so=0", + "so:libebtable_broute.so=0", + "so:libebtable_filter.so=0", + "so:libebtable_nat.so=0", + "so:libebtc.so=0", + "cmd:ebtables", + "cmd:ebtables-restore", + "cmd:ebtables-save" + ] + }, + "asterisk-pgsql": { + "versions": { + "15.7.1-r0": 1546247583 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "sipsak-dbg": { + "versions": { + "0.9.6-r11": 1543077292 + }, + "origin": "sipsak" + }, + "cvs": { + "versions": { + "1.11.23-r0": 1542893528 + }, + "origin": "cvs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cvs", + "cmd:cvsbug", + "cmd:rcs2log" + ] + }, + "py-oauth2client": { + "versions": { + "4.1.2-r2": 1545229294 + }, + "origin": "py-oauth2client", + "dependencies": [ + "py-asn1", + "py-httplib2", + "py-asn1-modules", + "py-rsa", + "py-six" + ] + }, + "mdocml": { + "versions": { + "1.14.3-r0": 1542304784 + }, + "origin": "mdocml", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libmandoc.so=0", + "cmd:demandoc", + "cmd:mandoc" + ] + }, + "perl-socket": { + "versions": { + "2.027-r0": 1543922479 + }, + "origin": "perl-socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-io-tty": { + "versions": { + "1.12-r4": 1542972213 + }, + "origin": "perl-io-tty", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "dwm-doc": { + "versions": { + "6.1-r2": 1545292628 + }, + "origin": "dwm" + }, + "perl-carp-clan-doc": { + "versions": { + "6.06-r1": 1545116506 + }, + "origin": "perl-carp-clan" + }, + "mariadb-bench": { + "versions": { + "10.3.13-r1": 1557431735 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common" + ] + }, + "sg3_utils": { + "versions": { + "1.42-r1": 1544000669 + }, + "origin": "sg3_utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsgutils2.so.2=2.0.0", + "cmd:scsi_logging_level", + "cmd:scsi_mandat", + "cmd:scsi_readcap", + "cmd:scsi_ready", + "cmd:scsi_satl", + "cmd:scsi_start", + "cmd:scsi_stop", + "cmd:scsi_temperature", + "cmd:sg_compare_and_write", + "cmd:sg_copy_results", + "cmd:sg_dd", + "cmd:sg_decode_sense", + "cmd:sg_emc_trespass", + "cmd:sg_format", + "cmd:sg_get_config", + "cmd:sg_get_lba_status", + "cmd:sg_ident", + "cmd:sg_inq", + "cmd:sg_logs", + "cmd:sg_luns", + "cmd:sg_map", + "cmd:sg_map26", + "cmd:sg_modes", + "cmd:sg_opcodes", + "cmd:sg_persist", + "cmd:sg_prevent", + "cmd:sg_raw", + "cmd:sg_rbuf", + "cmd:sg_rdac", + "cmd:sg_read", + "cmd:sg_read_attr", + "cmd:sg_read_block_limits", + "cmd:sg_read_buffer", + "cmd:sg_read_long", + "cmd:sg_readcap", + "cmd:sg_reassign", + "cmd:sg_referrals", + "cmd:sg_rep_zones", + "cmd:sg_requests", + "cmd:sg_reset", + "cmd:sg_reset_wp", + "cmd:sg_rmsn", + "cmd:sg_rtpg", + "cmd:sg_safte", + "cmd:sg_sanitize", + "cmd:sg_sat_identify", + "cmd:sg_sat_phy_event", + "cmd:sg_sat_read_gplog", + "cmd:sg_sat_set_features", + "cmd:sg_scan", + "cmd:sg_senddiag", + "cmd:sg_ses", + "cmd:sg_ses_microcode", + "cmd:sg_start", + "cmd:sg_stpg", + "cmd:sg_sync", + "cmd:sg_test_rwbuf", + "cmd:sg_timestamp", + "cmd:sg_turs", + "cmd:sg_unmap", + "cmd:sg_verify", + "cmd:sg_vpd", + "cmd:sg_wr_mode", + "cmd:sg_write_buffer", + "cmd:sg_write_long", + "cmd:sg_write_same", + "cmd:sg_write_verify", + "cmd:sg_xcopy", + "cmd:sg_zone", + "cmd:sginfo", + "cmd:sgm_dd", + "cmd:sgp_dd" + ] + }, + "openvpn-dev": { + "versions": { + "2.4.6-r4": 1543223245 + }, + "origin": "openvpn" + }, + "libwebsockets-doc": { + "versions": { + "3.1.0-r0": 1545856933 + }, + "origin": "libwebsockets" + }, + "gtkglext-dev": { + "versions": { + "1.2.0-r12": 1544001307 + }, + "origin": "gtkglext", + "dependencies": [ + "gtk+2.0-dev", + "mesa-dev", + "libice-dev", + "libxxf86vm-dev", + "libxi-dev", + "libx11-dev", + "libxt-dev", + "glu-dev", + "pangox-compat-dev", + "gtkglext=1.2.0-r12", + "pc:gdk-2.0", + "pc:gmodule-2.0", + "pc:gtk+-2.0", + "pc:pango", + "pc:pangox", + "pkgconfig" + ], + "provides": [ + "pc:gdkglext-1.0=1.2.0", + "pc:gdkglext-x11-1.0=1.2.0", + "pc:gtkglext-1.0=1.2.0", + "pc:gtkglext-x11-1.0=1.2.0" + ] + }, + "glibmm-dev": { + "versions": { + "2.56.0-r1": 1544799351 + }, + "origin": "glibmm", + "dependencies": [ + "libsigc++-dev", + "glibmm=2.56.0-r1", + "pc:gio-2.0", + "pc:gobject-2.0", + "pc:sigc++-2.0", + "pkgconfig" + ], + "provides": [ + "pc:giomm-2.4=2.56.0", + "pc:glibmm-2.4=2.56.0" + ] + }, + "mupdf": { + "versions": { + "1.13.0-r2": 1544000576 + }, + "origin": "mupdf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libharfbuzz.so.0", + "so:libjbig2dec.so.0", + "so:libjpeg.so.8", + "so:libopenjp2.so.7", + "so:libz.so.1" + ], + "provides": [ + "so:libmupdf.so.0=0", + "so:libmupdfthird.so.0=0" + ] + }, + "linux-firmware-cxgb4": { + "versions": { + "20190322-r0": 1554980653 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "dropbear": { + "versions": { + "2018.76-r2": 1545208977 + }, + "origin": "dropbear", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:dropbear", + "cmd:dropbearkey" + ] + }, + "acct-doc": { + "versions": { + "6.6.4-r0": 1545209152 + }, + "origin": "acct" + }, + "lua-cqueues": { + "versions": { + "20171014-r3": 1546520956 + }, + "origin": "lua-cqueues" + }, + "libmms-dev": { + "versions": { + "0.6.4-r0": 1544001045 + }, + "origin": "libmms", + "dependencies": [ + "libmms=0.6.4-r0", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libmms=0.6.4" + ] + }, + "cyrus-sasl-crammd5": { + "versions": { + "2.1.27-r1": 1550353528 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "consolekit2-dev": { + "versions": { + "1.2.1-r1": 1545062380 + }, + "origin": "consolekit2", + "dependencies": [ + "consolekit2=1.2.1-r1", + "pc:dbus-1", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:ck-connector=1.2.1", + "pc:libconsolekit=1.2.1" + ] + }, + "liblogging-doc": { + "versions": { + "1.0.6-r0": 1545060628 + }, + "origin": "liblogging" + }, + "lua-microlight": { + "versions": { + "1.1.1-r2": 1543249978 + }, + "origin": "lua-microlight" + }, + "libxcomposite-dev": { + "versions": { + "0.4.4-r2": 1543925370 + }, + "origin": "libxcomposite", + "dependencies": [ + "libxext-dev", + "libxcomposite=0.4.4-r2", + "pc:compositeproto>=0.4", + "pc:x11", + "pc:xfixes", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xcomposite=0.4.4" + ] + }, + "epris": { + "versions": { + "0.2-r4": 1545300236 + }, + "origin": "epris", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgthread-2.0.so.0" + ], + "provides": [ + "cmd:epr" + ] + }, + "py3-ipaddress": { + "versions": { + "1.0.22-r0": 1543926512 + }, + "origin": "py-ipaddress", + "dependencies": [ + "python3" + ] + }, + "py-boto-doc": { + "versions": { + "2.49.0-r0": 1544000446 + }, + "origin": "py-boto", + "dependencies": [ + "python2" + ] + }, + "uvncrepeater": { + "versions": { + "014-r7": 1545224119 + }, + "origin": "uvncrepeater", + "dependencies": [ + "openrc>=0.6", + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:repeater" + ] + }, + "py3-pyflakes": { + "versions": { + "1.6.0-r3": 1545073863 + }, + "origin": "py-pyflakes", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:pyflakes-3" + ] + }, + "py-jinja2-doc": { + "versions": { + "2.10-r2": 1542824937 + }, + "origin": "py-jinja2" + }, + "librrd": { + "versions": { + "1.7.0-r0": 1542924809 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpng16.so.16", + "so:libxml2.so.2" + ], + "provides": [ + "so:librrd.so.8=8.1.0" + ] + }, + "abi-compliance-checker": { + "versions": { + "2.3-r0": 1545745819 + }, + "origin": "abi-compliance-checker", + "dependencies": [ + "perl", + "build-base" + ], + "provides": [ + "cmd:abi-compliance-checker" + ] + }, + "chrpath": { + "versions": { + "0.16-r1": 1542900311 + }, + "origin": "chrpath", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chrpath" + ] + }, + "perl-fcgi-doc": { + "versions": { + "0.78-r2": 1543242207 + }, + "origin": "perl-fcgi" + }, + "lua5.3-maxminddb": { + "versions": { + "0.1-r1": 1543226678 + }, + "origin": "lua-maxminddb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmaxminddb.so.0" + ] + }, + "conky": { + "versions": { + "1.11.1-r0": 1545746033 + }, + "origin": "conky", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libgcc_s.so.1", + "so:liblua-5.3.so.0", + "so:libncursesw.so.6", + "so:libstdc++.so.6", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:conky" + ] + }, + "perl-io-async-doc": { + "versions": { + "0.72-r0": 1544799157 + }, + "origin": "perl-io-async" + }, + "perl-datetime": { + "versions": { + "1.44-r0": 1543239028 + }, + "origin": "perl-datetime", + "dependencies": [ + "perl-datetime-locale", + "perl-try-tiny", + "perl-dist-checkconflicts", + "perl-params-validationcompiler", + "perl-datetime-timezone", + "perl-namespace-autoclean", + "perl-specio", + "so:libc.musl-x86_64.so.1" + ] + }, + "nss-pam-ldapd": { + "versions": { + "0.9.8-r0": 1542845539 + }, + "origin": "nss-pam-ldapd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2", + "so:libpam.so.0" + ], + "provides": [ + "cmd:nslcd" + ] + }, + "dnsmasq-doc": { + "versions": { + "2.80-r3": 1545223253 + }, + "origin": "dnsmasq" + }, + "nagios-plugins-all": { + "versions": { + "2.2.1-r6": 1543933914 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins-breeze", + "nagios-plugins-by_ssh", + "nagios-plugins-cluster", + "nagios-plugins-dbi", + "nagios-plugins-dhcp", + "nagios-plugins-dig", + "nagios-plugins-disk_smb", + "nagios-plugins-disk", + "nagios-plugins-dns", + "nagios-plugins-dummy", + "nagios-plugins-file_age", + "nagios-plugins-fping", + "nagios-plugins-hpjd", + "nagios-plugins-http", + "nagios-plugins-icmp", + "nagios-plugins-ide_smart", + "nagios-plugins-ifoperstatus", + "nagios-plugins-ifstatus", + "nagios-plugins-ircd", + "nagios-plugins-ldap", + "nagios-plugins-load", + "nagios-plugins-log", + "nagios-plugins-mailq", + "nagios-plugins-mrtgtraf", + "nagios-plugins-mrtg", + "nagios-plugins-mysql", + "nagios-plugins-nagios", + "nagios-plugins-ntp", + "nagios-plugins-nt", + "nagios-plugins-nwstat", + "nagios-plugins-overcr", + "nagios-plugins-pgsql", + "nagios-plugins-ping", + "nagios-plugins-procs", + "nagios-plugins-radius", + "nagios-plugins-real", + "nagios-plugins-rpc", + "nagios-plugins-sensors", + "nagios-plugins-smtp", + "nagios-plugins-snmp", + "nagios-plugins-ssh", + "nagios-plugins-swap", + "nagios-plugins-time", + "nagios-plugins-ups", + "nagios-plugins-uptime", + "nagios-plugins-users", + "nagios-plugins-wave", + "nagios-plugins-openrc", + "nagios-plugins-tcp" + ] + }, + "lua5.1-cjson": { + "versions": { + "2.1.0-r8": 1542820882 + }, + "origin": "lua-cjson", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-virt-dev": { + "versions": { + "4.19.41-r0": 1557400007 + }, + "origin": "linux-vanilla", + "dependencies": [ + "perl", + "gmp-dev", + "elfutils-dev", + "bash", + "flex", + "bison", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libelf.so.1" + ] + }, + "freeradius-doc": { + "versions": { + "3.0.17-r5": 1556201987 + }, + "origin": "freeradius" + }, + "dnssec-root": { + "versions": { + "20190225-r0": 1551205319 + }, + "origin": "dnssec-root" + }, + "py2-lxml": { + "versions": { + "4.2.5-r0": 1545065047 + }, + "origin": "py-lxml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libpython2.7.so.1.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ] + }, + "avahi-compat-libdns_sd": { + "versions": { + "0.7-r1": 1543925311 + }, + "origin": "avahi", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdns_sd.so.1=1.0.0" + ] + }, + "qemu-lang": { + "versions": { + "3.1.0-r3": 1551107299 + }, + "origin": "qemu" + }, + "gnu-efi": { + "versions": { + "3.0.4-r1": 1543935645 + }, + "origin": "gnu-efi" + }, + "cryptsetup-doc": { + "versions": { + "2.0.6-r0": 1545746292 + }, + "origin": "cryptsetup" + }, + "py-six": { + "versions": { + "1.11.0-r0": 1542823023 + }, + "origin": "py-six" + }, + "uwsgi-logzmq": { + "versions": { + "2.0.17.1-r0": 1545062202 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libzmq.so.5" + ] + }, + "libxxf86misc-doc": { + "versions": { + "1.0.3-r2": 1543927842 + }, + "origin": "libxxf86misc" + }, + "slibtool": { + "versions": { + "0.5.26-r0": 1545213733 + }, + "origin": "slibtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:clibtool", + "cmd:clibtool-shared", + "cmd:clibtool-static", + "cmd:dlibtool", + "cmd:dlibtool-shared", + "cmd:dlibtool-static", + "cmd:rclibtool", + "cmd:rdclibtool", + "cmd:rdlibtool", + "cmd:rlibtool", + "cmd:slibtool", + "cmd:slibtool-shared", + "cmd:slibtool-static" + ] + }, + "eggdbus-doc": { + "versions": { + "0.6-r5": 1543935571 + }, + "origin": "eggdbus" + }, + "lua5.3-discount": { + "versions": { + "1.2.10.1-r4": 1545209123 + }, + "origin": "lua-discount", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libverto-libevent": { + "versions": { + "0.3.0-r1": 1543223526 + }, + "origin": "libverto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libverto.so.1" + ], + "provides": [ + "so:libverto-libevent.so.1=1.0.0" + ] + }, + "mtr-doc": { + "versions": { + "0.92-r0": 1545163034 + }, + "origin": "mtr" + }, + "libpng-dev": { + "versions": { + "1.6.37-r0": 1557129094 + }, + "origin": "libpng", + "dependencies": [ + "libpng=1.6.37-r0", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:libpng16=1.6.37", + "pc:libpng=1.6.37", + "cmd:libpng-config", + "cmd:libpng16-config" + ] + }, + "hunspell-pt": { + "versions": { + "20170814-r0": 1545223270 + }, + "origin": "hunspell-pt" + }, + "build-base": { + "versions": { + "0.5-r1": 1542302758 + }, + "origin": "build-base", + "dependencies": [ + "binutils", + "file", + "gcc", + "g++", + "make", + "libc-dev", + "fortify-headers" + ] + }, + "py-meld3": { + "versions": { + "1.0.2-r1": 1542972217 + }, + "origin": "py-meld3", + "dependencies": [ + "python2" + ] + }, + "py3-country": { + "versions": { + "18.12.8-r0": 1548109641 + }, + "origin": "py-country", + "dependencies": [ + "python3" + ] + }, + "sshfs": { + "versions": { + "3.5.1-r0": 1548094627 + }, + "origin": "sshfs", + "dependencies": [ + "openssh-client", + "so:libc.musl-x86_64.so.1", + "so:libfuse3.so.3", + "so:libglib-2.0.so.0" + ], + "provides": [ + "cmd:mount.fuse.sshfs", + "cmd:mount.sshfs", + "cmd:sshfs" + ] + }, + "py-httplib2": { + "versions": { + "0.12.0-r0": 1548112519 + }, + "origin": "py-httplib2" + }, + "samba-winbind-clients": { + "versions": { + "4.8.11-r1": 1555334974 + }, + "origin": "samba", + "dependencies": [ + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcliauth-samba4.so", + "so:libcmdline-contexts-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsmbconf.so.0", + "so:libtalloc.so.2", + "so:libwbclient.so.0", + "so:libwinbind-client-samba4.so" + ], + "provides": [ + "cmd:ntlm_auth", + "cmd:wbinfo" + ] + }, + "links-doc": { + "versions": { + "2.15-r2": 1545207743 + }, + "origin": "links" + }, + "linux-firmware-e100": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "uwsgi-router_basicauth": { + "versions": { + "2.0.17.1-r0": 1545062205 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-set-intspan-doc": { + "versions": { + "1.19-r0": 1545213690 + }, + "origin": "perl-set-intspan" + }, + "nfs-utils": { + "versions": { + "2.3.2-r1": 1543933423 + }, + "origin": "nfs-utils", + "dependencies": [ + "rpcbind", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcom_err.so.2", + "so:libdevmapper.so.1.02", + "so:libevent-2.1.so.6", + "so:libgssapi_krb5.so.2", + "so:libkeyutils.so.1", + "so:libkrb5.so.3", + "so:libmount.so.1", + "so:libnfsidmap.so.1", + "so:libsqlite3.so.0", + "so:libtirpc.so.3" + ], + "provides": [ + "cmd:blkmapd", + "cmd:exportfs", + "cmd:mount.nfs", + "cmd:mount.nfs4", + "cmd:mountstats", + "cmd:nfsconf", + "cmd:nfsdcltrack", + "cmd:nfsidmap", + "cmd:nfsiostat", + "cmd:nfsstat", + "cmd:osd_login", + "cmd:rpc.gssd", + "cmd:rpc.idmapd", + "cmd:rpc.mountd", + "cmd:rpc.nfsd", + "cmd:rpc.statd", + "cmd:rpc.svcgssd", + "cmd:rpcdebug", + "cmd:showmount", + "cmd:sm-notify", + "cmd:start-statd", + "cmd:umount.nfs", + "cmd:umount.nfs4" + ] + }, + "perl-html-parser": { + "versions": { + "3.72-r2": 1542821840 + }, + "origin": "perl-html-parser", + "dependencies": [ + "perl-html-tagset", + "so:libc.musl-x86_64.so.1" + ] + }, + "kamailio-websocket": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libunistring.so.2" + ] + }, + "libwebsockets-test": { + "versions": { + "3.1.0-r0": 1545856933 + }, + "origin": "libwebsockets", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:libwebsockets-test-client", + "cmd:libwebsockets-test-lejp", + "cmd:libwebsockets-test-server", + "cmd:libwebsockets-test-server-extpoll" + ] + }, + "perl-gdgraph-doc": { + "versions": { + "1.54-r0": 1545060786 + }, + "origin": "perl-gdgraph" + }, + "openbox": { + "versions": { + "3.6.1-r2": 1545207329 + }, + "origin": "openbox", + "dependencies": [ + "so:libICE.so.6", + "so:libSM.so.6", + "so:libX11.so.6", + "so:libXcursor.so.1", + "so:libXext.so.6", + "so:libXinerama.so.1", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libintl.so.8", + "so:libobrender.so.32", + "so:libobt.so.2", + "so:libstartup-notification-1.so.0" + ], + "provides": [ + "cmd:obxprop", + "cmd:openbox", + "cmd:openbox-session", + "cmd:setlayout" + ] + }, + "acf-postgresql": { + "versions": { + "0.11.0-r2": 1543924416 + }, + "origin": "acf-postgresql", + "dependencies": [ + "acf-core", + "postgresql", + "acf-db-lib", + "lua-sql-postgres" + ] + }, + "libxi": { + "versions": { + "1.7.9-r2": 1543228126 + }, + "origin": "libxi", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXi.so.6=6.1.0" + ] + }, + "libcap": { + "versions": { + "2.26-r0": 1546585711 + }, + "origin": "libcap", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcap.so.2=2.26", + "cmd:capsh", + "cmd:getcap", + "cmd:getpcaps", + "cmd:setcap" + ] + }, + "libfontenc-dev": { + "versions": { + "1.1.3-r3": 1542924592 + }, + "origin": "libfontenc", + "dependencies": [ + "libfontenc=1.1.3-r3", + "pkgconfig" + ], + "provides": [ + "pc:fontenc=1.1.3" + ] + }, + "lua5.3-stringy": { + "versions": { + "0.5.0-r1": 1545116589 + }, + "origin": "lua-stringy", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "vanessa_logger-dev": { + "versions": { + "0.0.10-r0": 1542893597 + }, + "origin": "vanessa_logger", + "dependencies": [ + "pkgconfig", + "vanessa_logger=0.0.10-r0" + ], + "provides": [ + "pc:vanessa-logger=0.0.10" + ] + }, + "libfastjson-dev": { + "versions": { + "0.99.8-r1": 1545060650 + }, + "origin": "libfastjson", + "dependencies": [ + "libfastjson=0.99.8-r1", + "pkgconfig" + ], + "provides": [ + "pc:libfastjson=0.99.8" + ] + }, + "mosh-doc": { + "versions": { + "1.3.2-r7": 1543932000 + }, + "origin": "mosh" + }, + "rpcbind-dbg": { + "versions": { + "0.2.4-r1": 1543933378 + }, + "origin": "rpcbind" + }, + "linux-firmware-other": { + "versions": { + "20190322-r0": 1554980655 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "perl-socket-getaddrinfo": { + "versions": { + "0.22-r0": 1542883395 + }, + "origin": "perl-socket-getaddrinfo", + "dependencies": [ + "perl-extutils-cchecker" + ], + "provides": [ + "cmd:getaddrinfo", + "cmd:getnameinfo" + ] + }, + "xf86-video-r128": { + "versions": { + "6.11.0-r0": 1545060960 + }, + "origin": "xf86-video-r128", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "fftw-doc": { + "versions": { + "3.3.8-r0": 1543926478 + }, + "origin": "fftw" + }, + "lua5.1-socket": { + "versions": { + "3.0_rc1_git20160306-r2": 1543934470 + }, + "origin": "lua-socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gmp-doc": { + "versions": { + "6.1.2-r1": 1542301439 + }, + "origin": "gmp" + }, + "giflib-utils": { + "versions": { + "5.1.4-r2": 1543077199 + }, + "origin": "giflib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgif.so.7" + ], + "provides": [ + "cmd:gif2rgb", + "cmd:gifbuild", + "cmd:gifclrmp", + "cmd:gifecho", + "cmd:giffix", + "cmd:gifinto", + "cmd:giftext", + "cmd:giftool" + ] + }, + "libbsd-dev": { + "versions": { + "0.8.6-r2": 1542822478 + }, + "origin": "libbsd", + "dependencies": [ + "bsd-compat-headers", + "linux-headers", + "libbsd=0.8.6-r2", + "pkgconfig" + ], + "provides": [ + "pc:libbsd-overlay=0.8.6", + "pc:libbsd=0.8.6" + ] + }, + "boost-signals": { + "versions": { + "1.67.0-r2": 1542823633 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_signals-mt.so.1.67.0=1.67.0", + "so:libboost_signals.so.1.67.0=1.67.0" + ] + }, + "gfortran": { + "versions": { + "8.3.0-r0": 1554745498 + }, + "origin": "gcc", + "dependencies": [ + "gcc=8.3.0-r0", + "libgfortran=8.3.0-r0", + "libquadmath=8.3.0-r0", + "libgfortran=8.3.0-r0", + "libquadmath=8.3.0-r0", + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libisl.so.15", + "so:libmpc.so.3", + "so:libmpfr.so.4", + "so:libz.so.1" + ], + "provides": [ + "cmd:gfortran", + "cmd:x86_64-alpine-linux-musl-gfortran" + ] + }, + "drbd-utils-bash-completion": { + "versions": { + "9.7.1-r0": 1545746125 + }, + "origin": "drbd-utils" + }, + "giflib-doc": { + "versions": { + "5.1.4-r2": 1543077198 + }, + "origin": "giflib" + }, + "geeqie-lang": { + "versions": { + "1.4-r0": 1545292985 + }, + "origin": "geeqie" + }, + "perl-xml-sax-base": { + "versions": { + "1.09-r0": 1542893156 + }, + "origin": "perl-xml-sax-base", + "dependencies": [ + "perl" + ] + }, + "acf-openvpn": { + "versions": { + "0.11.1-r2": 1543932518 + }, + "origin": "acf-openvpn", + "dependencies": [ + "acf-core", + "openvpn" + ] + }, + "haveged": { + "versions": { + "1.9.4-r4": 1557129010 + }, + "origin": "haveged", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhavege.so.1=1.1.0", + "cmd:haveged" + ] + }, + "mesa-gl": { + "versions": { + "18.1.7-r2": 1554455971 + }, + "origin": "mesa", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libexpat.so.1", + "so:libglapi.so.0", + "so:libxcb-dri2.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-glx.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb.so.1", + "so:libxshmfence.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libGL.so.1=1.2.0" + ] + }, + "herbstluftwm-zsh-completion": { + "versions": { + "0.7.1-r0": 1545300036 + }, + "origin": "herbstluftwm" + }, + "py-nose-doc": { + "versions": { + "1.3.7-r2": 1544797221 + }, + "origin": "py-nose" + }, + "perl-test-requires": { + "versions": { + "0.10-r0": 1542845649 + }, + "origin": "perl-test-requires" + }, + "gdk-pixbuf": { + "versions": { + "2.36.11-r2": 1543253991 + }, + "origin": "gdk-pixbuf", + "dependencies": [ + "shared-mime-info", + "/bin/sh", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libtiff.so.5" + ], + "provides": [ + "so:libgdk_pixbuf-2.0.so.0=0.3611.0", + "so:libgdk_pixbuf_xlib-2.0.so.0=0.3611.0", + "cmd:gdk-pixbuf-csource", + "cmd:gdk-pixbuf-pixdata", + "cmd:gdk-pixbuf-query-loaders", + "cmd:gdk-pixbuf-thumbnailer" + ] + }, + "libksba": { + "versions": { + "1.3.5-r0": 1543932192 + }, + "origin": "libksba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgpg-error.so.0" + ], + "provides": [ + "so:libksba.so.8=8.11.6" + ] + }, + "espeak": { + "versions": { + "1.48.04-r1": 1542973182 + }, + "origin": "espeak", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libespeak.so.1=1.1.48", + "cmd:espeak" + ] + }, + "boost-unit_test_framework": { + "versions": { + "1.67.0-r2": 1542823634 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_unit_test_framework-mt.so.1.67.0=1.67.0", + "so:libboost_unit_test_framework.so.1.67.0=1.67.0" + ] + }, + "make": { + "versions": { + "4.2.1-r2": 1542302738 + }, + "origin": "make", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:make" + ] + }, + "perl-datetime-locale": { + "versions": { + "1.22-r0": 1543238993 + }, + "origin": "perl-datetime-locale", + "dependencies": [ + "perl", + "perl-cpan-meta-check", + "perl-dist-checkconflicts", + "perl-file-sharedir", + "perl-file-sharedir-install", + "perl-list-moreutils", + "perl-namespace-autoclean", + "perl-params-validate", + "perl-params-validationcompiler", + "perl-scalar-list-utils", + "perl-test-fatal", + "perl-test-requires", + "perl-test-warnings" + ] + }, + "nginx-mod-http-shibboleth": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "ssl_client": { + "versions": { + "1.29.3-r10": 1548315956 + }, + "origin": "busybox", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtls-standalone.so.1" + ], + "provides": [ + "cmd:ssl_client" + ] + }, + "openjade": { + "versions": { + "1.3.2-r5": 1542893151 + }, + "origin": "openjade", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libogrove.so.0", + "so:libosp.so.5", + "so:libospgrove.so.0", + "so:libostyle.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:openjade" + ] + }, + "libgssglue": { + "versions": { + "0.4-r1": 1543248486 + }, + "origin": "libgssglue", + "dependencies": [ + "heimdal-dev", + "heimdal-libs", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgssglue.so.1=1.0.0" + ] + }, + "lua5.1-augeas": { + "versions": { + "0.1.2-r4": 1543246060 + }, + "origin": "lua-augeas", + "dependencies": [ + "so:libaugeas.so.0", + "so:libc.musl-x86_64.so.1" + ] + }, + "font-schumacher-misc": { + "versions": { + "1.1.2-r0": 1545075918 + }, + "origin": "font-schumacher-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "libtxc_dxtn-dev": { + "versions": { + "1.0.1-r5": 1545289386 + }, + "origin": "libtxc_dxtn", + "dependencies": [ + "mesa-dev" + ] + }, + "qemu-system-aarch64": { + "versions": { + "3.1.0-r3": 1551107302 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libstdc++.so.6", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-aarch64" + ] + }, + "xcb-util-renderutil-dev": { + "versions": { + "0.3.9-r1": 1543927920 + }, + "origin": "xcb-util-renderutil", + "dependencies": [ + "xcb-util-dev", + "pc:xcb-render", + "pkgconfig", + "xcb-util-renderutil=0.3.9-r1" + ], + "provides": [ + "pc:xcb-renderutil=0.3.9" + ] + }, + "perl-crypt-des-doc": { + "versions": { + "2.07-r4": 1543923063 + }, + "origin": "perl-crypt-des" + }, + "perl-filesys-notify-simple": { + "versions": { + "0.13-r0": 1543927836 + }, + "origin": "perl-filesys-notify-simple", + "dependencies": [ + "perl" + ] + }, + "libnih-dev": { + "versions": { + "1.0.3-r5": 1545062343 + }, + "origin": "libnih", + "dependencies": [ + "dbus-dev", + "expat-dev", + "libnih=1.0.3-r5", + "pkgconfig" + ], + "provides": [ + "pc:libnih-dbus=1.0.3", + "pc:libnih=1.0.3" + ] + }, + "swish-e-dev": { + "versions": { + "2.4.7-r8": 1545163770 + }, + "origin": "swish-e", + "dependencies": [ + "pkgconfig", + "swish-e=2.4.7-r8" + ], + "provides": [ + "pc:swish-e=2.4.7", + "cmd:swish-config" + ] + }, + "lua5.3-sec": { + "versions": { + "0.7-r1": 1545075388 + }, + "origin": "lua-sec", + "dependencies": [ + "lua5.3-socket", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "xautolock": { + "versions": { + "2.2-r4": 1543932089 + }, + "origin": "xautolock", + "dependencies": [ + "so:libX11.so.6", + "so:libXss.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xautolock" + ] + }, + "elfutils": { + "versions": { + "0.168-r2": 1546849158 + }, + "origin": "elfutils" + }, + "sysklogd": { + "versions": { + "1.5.1-r1": 1545213704 + }, + "origin": "sysklogd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:klogd", + "cmd:syslogd" + ] + }, + "clang": { + "versions": { + "5.0.2-r0": 1546873921 + }, + "origin": "clang", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libclang.so.5.0", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:c-index-test", + "cmd:clang", + "cmd:clang++", + "cmd:clang-5.0", + "cmd:clang-check", + "cmd:clang-cl", + "cmd:clang-cpp", + "cmd:clang-format", + "cmd:clang-import-test", + "cmd:clang-offload-bundler", + "cmd:clang-rename", + "cmd:git-clang-format" + ] + }, + "grub-efi": { + "versions": { + "2.02-r14": 1548432370 + }, + "origin": "grub", + "dependencies": [ + "grub" + ] + }, + "recordmydesktop": { + "versions": { + "0.3.8.1-r2": 1545302039 + }, + "origin": "recordmydesktop", + "dependencies": [ + "so:libX11.so.6", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libtheora.so.0", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:recordmydesktop" + ] + }, + "openldap-overlay-translucent": { + "versions": { + "2.4.47-r2": 1546016480 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "openldap-back-meta": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "py2-simplejson": { + "versions": { + "3.15.0-r0": 1543998765 + }, + "origin": "py-simplejson", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "heimdal-dev": { + "versions": { + "7.5.0-r2": 1542819373 + }, + "origin": "heimdal", + "dependencies": [ + "openssl-dev", + "e2fsprogs-dev", + "db-dev", + "heimdal-libs=7.5.0-r2", + "pkgconfig" + ], + "provides": [ + "pc:heimdal-gssapi=7.5.0", + "pc:heimdal-kadm-client=7.5.0", + "pc:heimdal-kadm-server=7.5.0", + "pc:heimdal-krb5=7.5.0", + "pc:kadm-client=7.5.0", + "pc:kadm-server=7.5.0", + "pc:kafs=7.5.0", + "pc:krb5-gssapi=7.5.0", + "pc:krb5=7.5.0", + "cmd:krb5-config" + ] + }, + "cryptsetup-dev": { + "versions": { + "2.0.6-r0": 1545746292 + }, + "origin": "cryptsetup", + "dependencies": [ + "cryptsetup-libs=2.0.6-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcryptsetup=2.0.6" + ] + }, + "rtpproxy-tools": { + "versions": { + "2.0.0-r4": 1545301082 + }, + "origin": "rtpproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsndfile.so.1" + ], + "provides": [ + "cmd:extractaudio", + "cmd:makeann" + ] + }, + "pekwm-doc": { + "versions": { + "0.1.17-r3": 1545069432 + }, + "origin": "pekwm" + }, + "py-newt": { + "versions": { + "0.52.20-r0": 1543924701 + }, + "origin": "newt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnewt.so.0.52" + ] + }, + "libxp-doc": { + "versions": { + "1.0.3-r2": 1545224134 + }, + "origin": "libxp" + }, + "apr-util-dev": { + "versions": { + "1.6.1-r5": 1545061009 + }, + "origin": "apr-util", + "dependencies": [ + "expat-dev", + "apr-dev", + "openldap-dev", + "sqlite-dev", + "postgresql-dev", + "db-dev", + "openssl-dev", + "mariadb-dev", + "apr-util=1.6.1-r5", + "pc:apr-1", + "pkgconfig" + ], + "provides": [ + "pc:apr-util-1=1.6.1", + "cmd:apu-1-config" + ] + }, + "nagios-plugins-wave": { + "versions": { + "2.2.1-r6": 1543933914 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "perl" + ] + }, + "perl-test-inter-doc": { + "versions": { + "1.07-r0": 1543077266 + }, + "origin": "perl-test-inter" + }, + "py-snowballstemmer": { + "versions": { + "1.2.1-r1": 1542824990 + }, + "origin": "py-snowballstemmer" + }, + "libestr": { + "versions": { + "0.1.10-r0": 1545060619 + }, + "origin": "libestr", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libestr.so.0=0.0.0" + ] + }, + "sipsak": { + "versions": { + "0.9.6-r11": 1543077292 + }, + "origin": "sipsak", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libcrypto.so.1.1" + ], + "provides": [ + "cmd:sipsak" + ] + }, + "py3-chardet": { + "versions": { + "3.0.4-r0": 1542825003 + }, + "origin": "py-chardet", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:chardetect" + ] + }, + "libssh2-dev": { + "versions": { + "1.8.2-r0": 1553681858 + }, + "origin": "libssh2", + "dependencies": [ + "libssh2=1.8.2-r0", + "pc:libcrypto", + "pc:libssl", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:libssh2=1.8.2" + ] + }, + "ruby-irb": { + "versions": { + "2.5.5-r0": 1557164837 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ], + "provides": [ + "cmd:irb" + ] + }, + "py2-lockfile": { + "versions": { + "0.12.2-r0": 1544799204 + }, + "origin": "py-lockfile", + "dependencies": [ + "python2" + ] + }, + "argon2-libs": { + "versions": { + "20171227-r1": 1543928961 + }, + "origin": "argon2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libargon2.so.1=1" + ] + }, + "shared-mime-info-doc": { + "versions": { + "1.10-r0": 1542821940 + }, + "origin": "shared-mime-info" + }, + "ttf-linux-libertine-doc": { + "versions": { + "5.3.0-r0": 1544000076 + }, + "origin": "ttf-linux-libertine" + }, + "py3-markupsafe": { + "versions": { + "1.1.0-r0": 1544791904 + }, + "origin": "py-markupsafe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "gtk+": { + "versions": { + "2.24.31-r0": 1543928745 + }, + "origin": "gtk+", + "dependencies": [ + "gtk+2.0>=2.24.31" + ] + }, + "vte3": { + "versions": { + "0.52.2-r0": 1545215300 + }, + "origin": "vte3", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo-gobject.so.2", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgnutls.so.30", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpcre2-8.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libvte-2.91.so.0=0.5200.2", + "cmd:vte-2.91" + ] + }, + "swatch-doc": { + "versions": { + "3.2.4-r4": 1545116516 + }, + "origin": "swatch" + }, + "perl-lwp-mediatypes-doc": { + "versions": { + "6.02-r1": 1542821763 + }, + "origin": "perl-lwp-mediatypes" + }, + "kamailio-outbound": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "exiftool": { + "versions": { + "11.23-r0": 1545835015 + }, + "origin": "perl-image-exiftool", + "dependencies": [ + "perl-image-exiftool" + ], + "provides": [ + "cmd:exiftool" + ] + }, + "multisort": { + "versions": { + "1.1-r0": 1545163700 + }, + "origin": "multisort", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:multisort" + ] + }, + "fcgi": { + "versions": { + "2.4.0-r8": 1543238826 + }, + "origin": "fcgi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfcgi.so.0=0.0.0", + "cmd:cgi-fcgi" + ] + }, + "usbredir-dev": { + "versions": { + "0.7.1-r0": 1543223285 + }, + "origin": "usbredir", + "dependencies": [ + "libusb-dev", + "pc:libusb-1.0", + "pkgconfig", + "usbredir=0.7.1-r0" + ], + "provides": [ + "pc:libusbredirhost=0.7.1", + "pc:libusbredirparser-0.5=0.7.1" + ] + }, + "mc": { + "versions": { + "4.8.22-r0": 1547126443 + }, + "origin": "mc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libssh2.so.1" + ], + "provides": [ + "cmd:mc", + "cmd:mcdiff", + "cmd:mcedit", + "cmd:mcview" + ] + }, + "libspf2": { + "versions": { + "1.2.10-r3": 1546267712 + }, + "origin": "libspf2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libspf2.so.2=2.1.0" + ] + }, + "mysql-client": { + "versions": { + "10.3.13-r1": 1557431735 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-client" + ] + }, + "perl-net-server-doc": { + "versions": { + "2.009-r1": 1545213952 + }, + "origin": "perl-net-server" + }, + "perl-scope-upper-doc": { + "versions": { + "0.31-r0": 1545164596 + }, + "origin": "perl-scope-upper" + }, + "lua5.1-evdev": { + "versions": { + "2.2.1-r1": 1545076551 + }, + "origin": "lua-evdev", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-list-utilsby-doc": { + "versions": { + "0.11-r0": 1545062502 + }, + "origin": "perl-list-utilsby" + }, + "man": { + "versions": { + "1.14.3-r0": 1542304779 + }, + "origin": "mdocml", + "dependencies": [ + "mdocml" + ] + }, + "lsyncd-doc": { + "versions": { + "2.2.3-r1": 1545253927 + }, + "origin": "lsyncd" + }, + "spandsp-dev": { + "versions": { + "0.0.6-r1": 1545815280 + }, + "origin": "spandsp", + "dependencies": [ + "pkgconfig", + "spandsp=0.0.6-r1" + ], + "provides": [ + "pc:spandsp=0.0.6" + ] + }, + "scrot-doc": { + "versions": { + "0.8.18-r0": 1545299549 + }, + "origin": "scrot" + }, + "gconf-doc": { + "versions": { + "3.2.6-r4": 1545075211 + }, + "origin": "gconf" + }, + "perl-list-someutils-xs": { + "versions": { + "0.55-r0": 1543934254 + }, + "origin": "perl-list-someutils-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-eval-closure-doc": { + "versions": { + "0.14-r0": 1542972940 + }, + "origin": "perl-eval-closure" + }, + "ruby-google-protobuf": { + "versions": { + "3.6.1-r1": 1543931874 + }, + "origin": "protobuf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.5" + ] + }, + "libxkbui-dev": { + "versions": { + "1.0.2-r9": 1543927889 + }, + "origin": "libxkbui", + "dependencies": [ + "xorgproto", + "libxkbui=1.0.2-r9", + "pc:kbproto", + "pc:x11", + "pc:xt", + "pkgconfig" + ], + "provides": [ + "pc:xkbui=1.0.2" + ] + }, + "py-hgtools": { + "versions": { + "6.5.2-r0": 1545060744 + }, + "origin": "py-hgtools" + }, + "audit-dev": { + "versions": { + "2.8.4-r0": 1543245855 + }, + "origin": "audit", + "dependencies": [ + "linux-headers", + "audit-libs=2.8.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:audit=2.8.4", + "pc:auparse=2.8.4" + ] + }, + "tsocks-doc": { + "versions": { + "1.8_beta5-r0": 1545207038 + }, + "origin": "tsocks" + }, + "kamailio-sctp": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libsctp.so.1" + ] + }, + "iperf-openrc": { + "versions": { + "2.0.10-r1": 1543222922 + }, + "origin": "iperf", + "dependencies": [ + "!iperf3" + ] + }, + "libwbclient": { + "versions": { + "4.8.11-r1": 1555334974 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libreplace-samba4.so=0", + "so:libwbclient.so.0=0.14", + "so:libwinbind-client-samba4.so=0" + ] + }, + "bdftopcf": { + "versions": { + "1.1-r0": 1542924735 + }, + "origin": "bdftopcf", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bdftopcf" + ] + }, + "libmemcached-dev": { + "versions": { + "1.0.18-r3": 1543923891 + }, + "origin": "libmemcached", + "dependencies": [ + "cyrus-sasl-dev", + "libmemcached-libs=1.0.18-r3", + "pkgconfig" + ], + "provides": [ + "pc:libmemcached=1.0.18" + ] + }, + "perl-module-scandeps-doc": { + "versions": { + "1.25-r0": 1542893295 + }, + "origin": "perl-module-scandeps" + }, + "freetype-dev": { + "versions": { + "2.9.1-r2": 1542822031 + }, + "origin": "freetype", + "dependencies": [ + "freetype=2.9.1-r2", + "pc:libpng", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:freetype2=22.1.16", + "cmd:freetype-config" + ] + }, + "perl-devel-symdump": { + "versions": { + "2.18-r0": 1542845658 + }, + "origin": "perl-devel-symdump" + }, + "libpcap-dev": { + "versions": { + "1.9.0-r1": 1545905178 + }, + "origin": "libpcap", + "dependencies": [ + "libpcap=1.9.0-r1", + "pkgconfig" + ], + "provides": [ + "cmd:pcap-config" + ] + }, + "acf-dnscache": { + "versions": { + "0.6.0-r2": 1545208952 + }, + "origin": "acf-dnscache", + "dependencies": [ + "acf-core", + "lua-posix", + "dnscache" + ] + }, + "py2-sphinx": { + "versions": { + "1.8.3-r0": 1548111929 + }, + "origin": "py-sphinx", + "dependencies": [ + "make", + "py2-docutils", + "py2-jinja2", + "py2-pygments", + "py2-six", + "py2-sphinx_rtd_theme", + "py2-alabaster<0.8", + "py2-babel", + "py2-snowballstemmer", + "py2-imagesize", + "py2-requests", + "py2-sphinxcontrib-websupport", + "py2-setuptools", + "py2-packaging", + "py2-typing", + "python2" + ], + "provides": [ + "cmd:sphinx-apidoc-2", + "cmd:sphinx-autogen-2", + "cmd:sphinx-build-2", + "cmd:sphinx-quickstart-2" + ] + }, + "opusfile": { + "versions": { + "0.11-r1": 1545209752 + }, + "origin": "opusfile", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libogg.so.0", + "so:libopus.so.0", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libopusfile.so.0=0.4.4", + "so:libopusurl.so.0=0.4.4" + ] + }, + "nss": { + "versions": { + "3.41-r0": 1545307911 + }, + "origin": "nss", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libnspr4.so", + "so:libplc4.so", + "so:libplds4.so", + "so:libsqlite3.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libfreebl3.so=41", + "so:libgtestutil.so.41=41", + "so:libnss3.so=41", + "so:libnssckbi.so=41", + "so:libnssdbm3.so=41", + "so:libnssutil3.so=41", + "so:libsmime3.so=41", + "so:libsoftokn3.so=41", + "so:libssl3.so=41" + ] + }, + "gdbm-doc": { + "versions": { + "1.13-r1": 1542303059 + }, + "origin": "gdbm" + }, + "libcdio-dev": { + "versions": { + "0.94-r2": 1543248376 + }, + "origin": "libcdio", + "dependencies": [ + "libcdio++=0.94-r2", + "libcdio=0.94-r2", + "pkgconfig" + ], + "provides": [ + "pc:libcdio++=0.94", + "pc:libcdio=0.94", + "pc:libiso9660++=0.94", + "pc:libiso9660=0.94", + "pc:libudf=0.94" + ] + }, + "libglade-dev": { + "versions": { + "2.6.4-r14": 1543927548 + }, + "origin": "libglade", + "dependencies": [ + "gtk+2.0-dev", + "libxml2-dev", + "libglade=2.6.4-r14", + "pc:gtk+-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libglade-2.0=2.6.4" + ] + }, + "iproute2-qos": { + "versions": { + "0.5-r1": 1544000678 + }, + "origin": "iproute2-qos", + "dependencies": [ + "iproute2" + ], + "provides": [ + "cmd:setup-qos" + ] + }, + "pinentry": { + "versions": { + "1.1.0-r0": 1543932178 + }, + "origin": "pinentry", + "dependencies": [ + "/bin/sh", + "so:libassuan.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libgpg-error.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:pinentry-curses" + ] + }, + "lua5.1-mosquitto": { + "versions": { + "0.2-r1": 1543932698 + }, + "origin": "lua-mosquitto", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ] + }, + "lighttpd-doc": { + "versions": { + "1.4.52-r0": 1545207628 + }, + "origin": "lighttpd" + }, + "lz4-dev": { + "versions": { + "1.8.3-r2": 1545154434 + }, + "origin": "lz4", + "dependencies": [ + "lz4-libs=1.8.3-r2", + "pkgconfig" + ], + "provides": [ + "pc:liblz4=1.8.3" + ] + }, + "rdesktop": { + "versions": { + "1.8.3-r5": 1545068564 + }, + "origin": "rdesktop", + "dependencies": [ + "so:libX11.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgssglue.so.1", + "so:libsamplerate.so.0", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:rdesktop" + ] + }, + "perl-package-stash": { + "versions": { + "0.37-r0": 1542845736 + }, + "origin": "perl-package-stash", + "dependencies": [ + "perl-dist-checkconflicts", + "perl-package-stash-xs", + "perl-module-implementation" + ], + "provides": [ + "cmd:package-stash-conflicts" + ] + }, + "mcpp": { + "versions": { + "2.7.2-r1": 1542883471 + }, + "origin": "mcpp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmcpp.so.0" + ], + "provides": [ + "cmd:mcpp" + ] + }, + "ulogd-doc": { + "versions": { + "2.0.7-r0": 1545300897 + }, + "origin": "ulogd" + }, + "py-crypto": { + "versions": { + "2.6.1-r2": 1543223473 + }, + "origin": "py-crypto" + }, + "py-django-pipeline": { + "versions": { + "1.3.25-r0": 1545207042 + }, + "origin": "py-django-pipeline", + "dependencies": [ + "python2" + ] + }, + "cksfv": { + "versions": { + "1.3.14-r4": 1545069445 + }, + "origin": "cksfv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cksfv" + ] + }, + "putty-doc": { + "versions": { + "0.71-r1": 1554726207 + }, + "origin": "putty" + }, + "json-glib-dev": { + "versions": { + "1.4.4-r0": 1545837175 + }, + "origin": "json-glib", + "dependencies": [ + "json-glib=1.4.4-r0", + "pc:gio-2.0>=2.44.0", + "pc:gobject-2.0>=2.44.0", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjson-glib-1.0.so.0" + ], + "provides": [ + "pc:json-glib-1.0=1.4.4", + "cmd:json-glib-format", + "cmd:json-glib-validate" + ] + }, + "atk": { + "versions": { + "2.30.0-r0": 1542823743 + }, + "origin": "atk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libatk-1.0.so.0=0.23009.1" + ] + }, + "perl-xml-namespacesupport": { + "versions": { + "1.12-r0": 1542893163 + }, + "origin": "perl-xml-namespacesupport", + "dependencies": [ + "perl" + ] + }, + "radvd-doc": { + "versions": { + "2.17-r2": 1545254216 + }, + "origin": "radvd" + }, + "zmap": { + "versions": { + "2.1.1-r3": 1545062099 + }, + "origin": "zmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libjson-c.so.4", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:zblacklist", + "cmd:zmap", + "cmd:ztee" + ] + }, + "perl-clone-doc": { + "versions": { + "0.41-r1": 1546096794 + }, + "origin": "perl-clone" + }, + "poppler-doc": { + "versions": { + "0.56.0-r1": 1542824316 + }, + "origin": "poppler" + }, + "linux-firmware-myricom": { + "versions": { + "20190322-r0": 1554980650 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "libvirt-lxc": { + "versions": { + "4.10.0-r1": 1547051658 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=4.10.0-r1", + "libvirt-common-drivers=4.10.0-r1", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libfuse.so.2", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libtirpc.so.3", + "so:libvirt.so.0", + "so:libxml2.so.2" + ] + }, + "xf86-video-nv": { + "versions": { + "2.1.21-r3": 1545300604 + }, + "origin": "xf86-video-nv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "nagios-plugins-smtp": { + "versions": { + "2.2.1-r6": 1543933912 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "xtables-addons-vanilla": { + "versions": { + "4.19.41-r0": 1557400893 + }, + "origin": "xtables-addons-vanilla", + "dependencies": [ + "linux-vanilla=4.19.41-r0" + ] + }, + "gnuchess": { + "versions": { + "6.2.5-r1": 1545302332 + }, + "origin": "gnuchess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:gnuchess", + "cmd:gnuchessu", + "cmd:gnuchessx" + ] + }, + "freeswitch-sounds-ru-RU-elena-8000": { + "versions": { + "1.0.12-r1": 1545249945 + }, + "origin": "freeswitch-sounds-ru-RU-elena-8000" + }, + "perl-params-classify-doc": { + "versions": { + "0.015-r0": 1542845687 + }, + "origin": "perl-params-classify" + }, + "weechat-aspell": { + "versions": { + "2.3-r0": 1545299932 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libaspell.so.15", + "so:libc.musl-x86_64.so.1" + ] + }, + "libnetfilter_queue": { + "versions": { + "1.0.3-r0": 1546267693 + }, + "origin": "libnetfilter_queue", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnfnetlink.so.0" + ], + "provides": [ + "so:libnetfilter_queue.so.1=1.4.0" + ] + }, + "dovecot-pigeonhole-plugin": { + "versions": { + "2.3.6-r0": 1557134098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libdovecot-login.so.0", + "so:libdovecot-storage.so.0", + "so:libdovecot.so.0" + ], + "provides": [ + "so:lib90_sieve_plugin.so=0", + "so:lib95_imap_filter_sieve_plugin.so=0", + "so:lib95_imap_sieve_plugin.so=0", + "so:libdovecot-sieve.so.0=0.0.0", + "cmd:sieve-dump", + "cmd:sieve-filter", + "cmd:sieve-test", + "cmd:sievec" + ] + }, + "perl-json": { + "versions": { + "2.97000-r0": 1542819174 + }, + "origin": "perl-json" + }, + "lua-asn1": { + "versions": { + "2.2.0-r0": 1545116591 + }, + "origin": "lua-asn1", + "dependencies": [ + "lua-stringy" + ] + }, + "libzdb": { + "versions": { + "3.1-r2": 1543221887 + }, + "origin": "libzdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3", + "so:libpq.so.5", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libzdb.so.11=11.0.0" + ] + }, + "lua-stdlib-doc": { + "versions": { + "41.2.1-r0": 1545292922 + }, + "origin": "lua-stdlib" + }, + "netcf": { + "versions": { + "0.2.8-r5": 1543935401 + }, + "origin": "netcf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetcf.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:ncftool" + ] + }, + "openssh-keygen": { + "versions": { + "7.9_p1-r5": 1556034589 + }, + "origin": "openssh", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ], + "provides": [ + "cmd:ssh-keygen" + ] + }, + "libsamplerate": { + "versions": { + "0.1.9-r1": 1545068554 + }, + "origin": "libsamplerate", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsndfile.so.1" + ], + "provides": [ + "so:libsamplerate.so.0=0.1.8", + "cmd:sndfile-resample" + ] + }, + "perl-sub-quote": { + "versions": { + "2.004000-r0": 1542972964 + }, + "origin": "perl-sub-quote" + }, + "nload-doc": { + "versions": { + "0.7.4-r3": 1545214121 + }, + "origin": "nload" + }, + "binutils-gold": { + "versions": { + "2.31.1-r2": 1546441178 + }, + "origin": "binutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:ld.gold" + ] + }, + "perl-dbd-mysql": { + "versions": { + "4.050-r0": 1547124434 + }, + "origin": "perl-dbd-mysql", + "dependencies": [ + "perl", + "perl-dbi", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "libnl3": { + "versions": { + "3.4.0-r0": 1542965893 + }, + "origin": "libnl3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnl-3.so.200=200.26.0", + "so:libnl-genl-3.so.200=200.26.0", + "so:libnl-idiag-3.so.200=200.26.0", + "so:libnl-nf-3.so.200=200.26.0", + "so:libnl-route-3.so.200=200.26.0", + "so:libnl-xfrm-3.so.200=200.26.0" + ] + }, + "dkimproxy-doc": { + "versions": { + "1.4.1-r5": 1545213959 + }, + "origin": "dkimproxy" + }, + "bacula-doc": { + "versions": { + "9.4.1-r1": 1546944087 + }, + "origin": "bacula" + }, + "libidn-doc": { + "versions": { + "1.35-r0": 1542924920 + }, + "origin": "libidn" + }, + "libatasmart-dev": { + "versions": { + "0.19-r1": 1545062468 + }, + "origin": "libatasmart", + "dependencies": [ + "eudev-dev", + "libatasmart=0.19-r1", + "pkgconfig" + ], + "provides": [ + "pc:libatasmart=0.19" + ] + }, + "gc-doc": { + "versions": { + "7.6.4-r2": 1543226892 + }, + "origin": "gc" + }, + "gsm-dev": { + "versions": { + "1.0.18-r0": 1543927819 + }, + "origin": "gsm", + "dependencies": [ + "gsm=1.0.18-r0" + ] + }, + "qt-doc": { + "versions": { + "4.8.7-r11": 1545075086 + }, + "origin": "qt" + }, + "cyrus-sasl-dev": { + "versions": { + "2.1.27-r1": 1550353527 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "libsasl=2.1.27-r1", + "pkgconfig" + ], + "provides": [ + "pc:libsasl2=2.1.27" + ] + }, + "perl-xml-rss": { + "versions": { + "1.60-r0": 1545301036 + }, + "origin": "perl-xml-rss", + "dependencies": [ + "perl-xml-parser", + "perl-html-parser", + "perl-datetime-format-mail", + "perl-datetime-format-w3cdtf", + "perl-datetime" + ] + }, + "lua5.3-md5": { + "versions": { + "1.2-r3": 1542883866 + }, + "origin": "lua-md5", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-pip": { + "versions": { + "18.1-r0": 1545062533 + }, + "origin": "py2-pip", + "dependencies": [ + "python2", + "py-setuptools" + ], + "provides": [ + "py-pip=18.1-r0", + "cmd:pip", + "cmd:pip2", + "cmd:pip2.7" + ] + }, + "py3-requests": { + "versions": { + "2.19.1-r0": 1542825029 + }, + "origin": "py-requests", + "dependencies": [ + "py3-chardet", + "py3-idna", + "py3-certifi", + "py3-urllib3", + "python3" + ] + }, + "openrc": { + "versions": { + "0.39.2-r3": 1548592318 + }, + "origin": "openrc", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libeinfo.so.1=1", + "so:librc.so.1=1", + "cmd:openrc", + "cmd:openrc-init", + "cmd:openrc-run", + "cmd:openrc-shutdown", + "cmd:rc", + "cmd:rc-service", + "cmd:rc-sstat", + "cmd:rc-status", + "cmd:rc-update", + "cmd:runscript", + "cmd:service", + "cmd:start-stop-daemon", + "cmd:supervise-daemon" + ] + }, + "rsyslog-mmpstrucdata": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "nettle-dev": { + "versions": { + "3.4.1-r0": 1544791824 + }, + "origin": "nettle", + "dependencies": [ + "gmp-dev", + "nettle=3.4.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:hogweed=3.4.1", + "pc:nettle=3.4.1" + ] + }, + "ncurses5-widec-libs": { + "versions": { + "5.9-r1": 1545292734 + }, + "origin": "ncurses5", + "dependencies": [ + "ncurses-terminfo-base", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libformw.so.5=5.9", + "so:libmenuw.so.5=5.9", + "so:libncursesw.so.5=5.9", + "so:libpanelw.so.5=5.9" + ] + }, + "kamailio-mysql": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "kamailio-db", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3", + "so:libsrdb1.so.1", + "so:libsrdb2.so.1" + ] + }, + "gdb-doc": { + "versions": { + "8.2-r1": 1544798982 + }, + "origin": "gdb" + }, + "avahi-libs": { + "versions": { + "0.7-r1": 1543925311 + }, + "origin": "avahi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libintl.so.8" + ], + "provides": [ + "so:libavahi-client.so.3=3.2.9", + "so:libavahi-common.so.3=3.5.3" + ] + }, + "seavgabios-bin": { + "versions": { + "1.11.0-r0": 1543936026 + }, + "origin": "seabios" + }, + "libfdt": { + "versions": { + "1.4.7-r0": 1545300716 + }, + "origin": "dtc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfdt.so.1=0" + ] + }, + "at-spi2-core-lang": { + "versions": { + "2.28.0-r0": 1543241208 + }, + "origin": "at-spi2-core" + }, + "perl-sub-name": { + "versions": { + "0.21-r1": 1542845760 + }, + "origin": "perl-sub-name", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "fuse3": { + "versions": { + "3.2.6-r1": 1548943636 + }, + "origin": "fuse3", + "dependencies": [ + "fuse-common", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfuse3.so.3=3.2.6", + "cmd:fusermount3", + "cmd:mount.fuse3" + ] + }, + "lua5.1-lzmq": { + "versions": { + "0.4.4-r0": 1545076581 + }, + "origin": "lua-lzmq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5", + "so:libzmq.so.5" + ] + }, + "libjpeg-turbo": { + "versions": { + "1.5.3-r4": 1547027583 + }, + "origin": "libjpeg-turbo", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libjpeg.so.8=8.1.2", + "so:libturbojpeg.so.0=0.1.0" + ] + }, + "exiv2": { + "versions": { + "0.26-r0": 1543246769 + }, + "origin": "exiv2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libexiv2.so.26=26.0.0", + "cmd:exiv2" + ] + }, + "perl-universal-require-doc": { + "versions": { + "0.18-r0": 1545995649 + }, + "origin": "perl-universal-require" + }, + "linux-firmware-vicam": { + "versions": { + "20190322-r0": 1554980646 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "libxml2": { + "versions": { + "2.9.9-r1": 1551105514 + }, + "origin": "libxml2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libxml2.so.2=2.9.9" + ] + }, + "perl-test-requires-doc": { + "versions": { + "0.10-r0": 1542845644 + }, + "origin": "perl-test-requires" + }, + "py-urlgrabber": { + "versions": { + "3.10.1-r1": 1543935136 + }, + "origin": "py-urlgrabber", + "dependencies": [ + "python2", + "py-curl" + ], + "provides": [ + "cmd:urlgrabber" + ] + }, + "libogg": { + "versions": { + "1.3.3-r2": 1543925777 + }, + "origin": "libogg", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libogg.so.0=0.8.3" + ] + }, + "perl-dbd-sqlite-dev": { + "versions": { + "1.62-r0": 1546944025 + }, + "origin": "perl-dbd-sqlite" + }, + "zstd-static": { + "versions": { + "1.3.8-r0": 1546966443 + }, + "origin": "zstd" + }, + "openvswitch-dbg": { + "versions": { + "2.10.1-r0": 1544000344 + }, + "origin": "openvswitch" + }, + "perl-file-sharedir-install-doc": { + "versions": { + "0.13-r0": 1543238879 + }, + "origin": "perl-file-sharedir-install" + }, + "perl-test-failwarnings-doc": { + "versions": { + "0.008-r1": 1543238916 + }, + "origin": "perl-test-failwarnings" + }, + "darkstat-doc": { + "versions": { + "3.0.719-r1": 1543223079 + }, + "origin": "darkstat" + }, + "linux-firmware-ene-ub6250": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "py-atomicwrites": { + "versions": { + "1.1.5-r0": 1542824873 + }, + "origin": "py-atomicwrites" + }, + "perl-scalar-list-utils": { + "versions": { + "1.50-r0": 1543238903 + }, + "origin": "perl-scalar-list-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-sub-uplevel": { + "versions": { + "0.2800-r0": 1542883359 + }, + "origin": "perl-sub-uplevel", + "dependencies": [ + "perl" + ] + }, + "perl-x10-doc": { + "versions": { + "0.04-r1": 1545163612 + }, + "origin": "perl-x10" + }, + "cups-filters-libs": { + "versions": { + "1.21.6-r0": 1545820385 + }, + "origin": "cups-filters", + "dependencies": [ + "poppler-utils", + "bc", + "ttf-freefont", + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libdbus-1.so.3", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libtiff.so.5" + ], + "provides": [ + "so:libcupsfilters.so.1=1.0.0", + "so:libfontembed.so.1=1.0.0" + ] + }, + "wv": { + "versions": { + "1.2.9-r3": 1545300665 + }, + "origin": "wv", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgsf-1.so.114", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libwv-1.2.so.4=4.0.5", + "cmd:wvAbw", + "cmd:wvCleanLatex", + "cmd:wvConvert", + "cmd:wvDVI", + "cmd:wvDocBook", + "cmd:wvHtml", + "cmd:wvLatex", + "cmd:wvMime", + "cmd:wvPDF", + "cmd:wvPS", + "cmd:wvRTF", + "cmd:wvSummary", + "cmd:wvText", + "cmd:wvVersion", + "cmd:wvWare", + "cmd:wvWml" + ] + }, + "hunspell-lang": { + "versions": { + "1.6.2-r1": 1542883764 + }, + "origin": "hunspell" + }, + "rlog-dev": { + "versions": { + "1.4-r4": 1545061033 + }, + "origin": "rlog", + "dependencies": [ + "pkgconfig", + "rlog=1.4-r4" + ], + "provides": [ + "pc:librlog=1.4" + ] + }, + "snownews": { + "versions": { + "1.5.12-r8": 1543999424 + }, + "origin": "snownews", + "dependencies": [ + "ncurses", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:opml2snow", + "cmd:snow2opml", + "cmd:snownews" + ] + }, + "git-daemon": { + "versions": { + "2.20.1-r0": 1545214195 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0", + "so:libc.musl-x86_64.so.1", + "so:libpcre2-8.so.0", + "so:libz.so.1" + ] + }, + "exiv2-doc": { + "versions": { + "0.26-r0": 1543246769 + }, + "origin": "exiv2" + }, + "perl-xml-sax-base-doc": { + "versions": { + "1.09-r0": 1542893154 + }, + "origin": "perl-xml-sax-base" + }, + "perl-db": { + "versions": { + "0.55-r1": 1542985269 + }, + "origin": "perl-db", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ] + }, + "lftp-doc": { + "versions": { + "4.8.4-r1": 1545073926 + }, + "origin": "lftp" + }, + "apr": { + "versions": { + "1.6.5-r0": 1544000273 + }, + "origin": "apr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libapr-1.so.0=0.6.5" + ] + }, + "postgresql-bdr-openrc": { + "versions": { + "9.4.14_p1-r1": 1543223165 + }, + "origin": "postgresql-bdr", + "dependencies": [ + "bash", + "libpq" + ] + }, + "gnu-efi-dev": { + "versions": { + "3.0.4-r1": 1543935645 + }, + "origin": "gnu-efi", + "dependencies": [ + "gnu-efi" + ] + }, + "s6-linux-init-doc": { + "versions": { + "0.4.0.0-r0": 1545292930 + }, + "origin": "s6-linux-init" + }, + "iperf3-doc": { + "versions": { + "3.6-r0": 1543932135 + }, + "origin": "iperf3" + }, + "libssh2-dbg": { + "versions": { + "1.8.2-r0": 1553681858 + }, + "origin": "libssh2" + }, + "giblib-dev": { + "versions": { + "1.2.4-r10": 1543226558 + }, + "origin": "giblib", + "dependencies": [ + "imlib2-dev", + "freetype-dev", + "zlib-dev", + "libx11-dev", + "libxext-dev", + "giblib=1.2.4-r10", + "pkgconfig" + ], + "provides": [ + "pc:giblib=1.2.4", + "cmd:giblib-config" + ] + }, + "libglade-doc": { + "versions": { + "2.6.4-r14": 1543927550 + }, + "origin": "libglade" + }, + "desktop-file-utils-doc": { + "versions": { + "0.23-r1": 1545116736 + }, + "origin": "desktop-file-utils" + }, + "flex": { + "versions": { + "2.6.4-r1": 1542300946 + }, + "origin": "flex", + "dependencies": [ + "m4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:flex", + "cmd:flex++", + "cmd:lex" + ] + }, + "unbound-dbg": { + "versions": { + "1.8.3-r1": 1555953584 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root" + ] + }, + "perl-css-minifier-xs-doc": { + "versions": { + "0.09-r3": 1545062654 + }, + "origin": "perl-css-minifier-xs" + }, + "libcap-dev": { + "versions": { + "2.26-r0": 1546585711 + }, + "origin": "libcap", + "dependencies": [ + "linux-headers", + "libcap=2.26-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcap=2.26" + ] + }, + "libmikmod": { + "versions": { + "3.3.11.1-r0": 1545162983 + }, + "origin": "libmikmod", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmikmod.so.3=3.3.0" + ] + }, + "nodejs": { + "versions": { + "10.14.2-r0": 1545345078 + }, + "origin": "nodejs", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libhttp_parser.so.2.8", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libuv.so.1", + "so:libz.so.1" + ], + "provides": [ + "nodejs-lts=10.14.2", + "cmd:node" + ] + }, + "gnome-doc-utils-lang": { + "versions": { + "0.20.10-r2": 1543254072 + }, + "origin": "gnome-doc-utils", + "dependencies": [ + "python2", + "docbook-xml", + "rarian", + "py2-libxml2", + "libxslt" + ] + }, + "mosquitto": { + "versions": { + "1.5.6-r0": 1549970968 + }, + "origin": "mosquitto", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libuuid.so.1", + "so:libwebsockets.so.14" + ], + "provides": [ + "cmd:mosquitto", + "cmd:mosquitto_passwd" + ] + }, + "mdocml-apropos": { + "versions": { + "1.14.3-r0": 1542304781 + }, + "origin": "mdocml", + "dependencies": [ + "/bin/sh", + "mdocml=1.14.3-r0" + ] + }, + "py-simplejson": { + "versions": { + "3.15.0-r0": 1543998767 + }, + "origin": "py-simplejson" + }, + "pacman-doc": { + "versions": { + "5.0.2-r4": 1543932339 + }, + "origin": "pacman" + }, + "perl-html-tree-doc": { + "versions": { + "5.07-r0": 1542893575 + }, + "origin": "perl-html-tree" + }, + "net-snmp-doc": { + "versions": { + "5.8-r0": 1543923349 + }, + "origin": "net-snmp" + }, + "perl-crypt-openssl-guess": { + "versions": { + "0.11-r0": 1543925838 + }, + "origin": "perl-crypt-openssl-guess" + }, + "scanelf": { + "versions": { + "1.2.3-r0": 1542304832 + }, + "origin": "pax-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:scanelf" + ] + }, + "cups-dev": { + "versions": { + "2.2.10-r0": 1545910846 + }, + "origin": "cups", + "dependencies": [ + "libgcrypt-dev", + "gnutls-dev", + "zlib-dev", + "cups-libs=2.2.10-r0" + ], + "provides": [ + "cmd:cups-config" + ] + }, + "zfs-scripts": { + "versions": { + "0.7.12-r1": 1552933679 + }, + "origin": "zfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libzfs.so.2" + ] + }, + "mcpp-doc": { + "versions": { + "2.7.2-r1": 1542883471 + }, + "origin": "mcpp" + }, + "gnokii-smsd-sqlite": { + "versions": { + "0.6.31-r8": 1545300480 + }, + "origin": "gnokii", + "dependencies": [ + "gnokii-smsd", + "so:libc.musl-x86_64.so.1" + ] + }, + "gross-dev": { + "versions": { + "1.0.2-r11": 1545062672 + }, + "origin": "gross" + }, + "libnotify-doc": { + "versions": { + "0.7.7-r2": 1544001120 + }, + "origin": "libnotify" + }, + "djbdns": { + "versions": { + "1.05-r47": 1545208951 + }, + "origin": "djbdns", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:axfr-get", + "cmd:axfrdns", + "cmd:axfrdns-conf", + "cmd:dnsfilter", + "cmd:dnsipq", + "cmd:dnsmx", + "cmd:dnsname", + "cmd:dnsq", + "cmd:dnstrace", + "cmd:dnstracesort", + "cmd:dnstxt", + "cmd:pickdns", + "cmd:pickdns-conf", + "cmd:pickdns-data", + "cmd:random-ip", + "cmd:rbldns", + "cmd:rbldns-conf", + "cmd:rbldns-data", + "cmd:walldns", + "cmd:walldns-conf" + ] + }, + "perl-class-accessor": { + "versions": { + "0.34-r0": 1543934265 + }, + "origin": "perl-class-accessor", + "dependencies": [ + "perl" + ] + }, + "libc-dev": { + "versions": { + "0.7.1-r0": 1542302755 + }, + "origin": "libc-dev", + "dependencies": [ + "musl-dev" + ] + }, + "perl-file-listing-doc": { + "versions": { + "6.04-r1": 1542821848 + }, + "origin": "perl-file-listing" + }, + "flashrom": { + "versions": { + "1.0-r3": 1543921877 + }, + "origin": "flashrom", + "dependencies": [ + "dmidecode", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libpci.so.3", + "so:libusb-0.1.so.4", + "so:libusb-1.0.so.0" + ], + "provides": [ + "cmd:flashrom" + ] + }, + "freeradius-sqlite": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius-sql=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ], + "provides": [ + "freeradius3-sqlite=3.0.17-r5", + "so:rlm_sql_sqlite.so=0" + ] + }, + "mariadb-client": { + "versions": { + "10.3.13-r1": 1557431734 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common=10.3.13-r1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libncursesw.so.6", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:myisam_ftdump", + "cmd:mysql", + "cmd:mysql_find_rows", + "cmd:mysql_fix_extensions", + "cmd:mysql_waitpid", + "cmd:mysqlaccess", + "cmd:mysqladmin", + "cmd:mysqlcheck", + "cmd:mysqldump", + "cmd:mysqldumpslow", + "cmd:mysqlimport", + "cmd:mysqlshow" + ] + }, + "git": { + "versions": { + "2.20.1-r0": 1545214212 + }, + "origin": "git", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libexpat.so.1", + "so:libpcre2-8.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:git", + "cmd:git-receive-pack", + "cmd:git-shell", + "cmd:git-upload-archive", + "cmd:git-upload-pack" + ] + }, + "openntpd-doc": { + "versions": { + "6.2_p3-r2": 1543230955 + }, + "origin": "openntpd" + }, + "cairo-dbg": { + "versions": { + "1.16.0-r1": 1546948315 + }, + "origin": "cairo" + }, + "xev-doc": { + "versions": { + "1.2.2-r0": 1545301876 + }, + "origin": "xev" + }, + "less": { + "versions": { + "530-r0": 1542301312 + }, + "origin": "less", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:less", + "cmd:lessecho", + "cmd:lesskey" + ] + }, + "rsync": { + "versions": { + "3.1.3-r1": 1542820866 + }, + "origin": "rsync", + "dependencies": [ + "so:libacl.so.1", + "so:libattr.so.1", + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:rsync" + ] + }, + "nettle-utils": { + "versions": { + "3.4.1-r0": 1544791824 + }, + "origin": "nettle", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libhogweed.so.4", + "so:libnettle.so.6" + ], + "provides": [ + "cmd:nettle-hash", + "cmd:nettle-lfib-stream", + "cmd:nettle-pbkdf2", + "cmd:pkcs1-conv", + "cmd:sexp-conv" + ] + }, + "mc-doc": { + "versions": { + "4.8.22-r0": 1547126443 + }, + "origin": "mc" + }, + "poppler": { + "versions": { + "0.56.0-r1": 1542824320 + }, + "origin": "poppler", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libopenjp2.so.7", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libtiff.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libpoppler-cpp.so.0=0.3.0", + "so:libpoppler.so.67=67.0.0" + ] + }, + "redis": { + "versions": { + "4.0.12-r0": 1546005788 + }, + "origin": "redis", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:redis-benchmark", + "cmd:redis-check-aof", + "cmd:redis-check-rdb", + "cmd:redis-cli", + "cmd:redis-sentinel", + "cmd:redis-server" + ] + }, + "perl-test-requiresinternet-doc": { + "versions": { + "0.05-r1": 1542821874 + }, + "origin": "perl-test-requiresinternet" + }, + "postgresql-libs": { + "versions": { + "11.2-r0": 1554274176 + }, + "origin": "postgresql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:libecpg.so.6=6.11", + "so:libecpg_compat.so.3=3.11", + "so:libpgtypes.so.3=3.11" + ] + }, + "py-munkres": { + "versions": { + "1.0.12-r0": 1545292848 + }, + "origin": "py-munkres" + }, + "kamailio-unixodbc": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2", + "so:libsrdb1.so.1" + ] + }, + "font-vollkorn": { + "versions": { + "4.105-r0": 1545062649 + }, + "origin": "font-vollkorn" + }, + "py2-argparse": { + "versions": { + "1.4.0-r2": 1545060591 + }, + "origin": "py-argparse", + "dependencies": [ + "python2" + ] + }, + "boost-context": { + "versions": { + "1.67.0-r2": 1542823631 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libboost_context-mt.so.1.67.0=1.67.0" + ] + }, + "ansible": { + "versions": { + "2.7.0-r1": 1545223231 + }, + "origin": "ansible", + "dependencies": [ + "python3", + "py3-yaml", + "py3-paramiko", + "py3-jinja2", + "py3-markupsafe", + "py3-crypto" + ], + "provides": [ + "cmd:ansible", + "cmd:ansible-config", + "cmd:ansible-connection", + "cmd:ansible-console", + "cmd:ansible-doc", + "cmd:ansible-galaxy", + "cmd:ansible-inventory", + "cmd:ansible-playbook", + "cmd:ansible-pull", + "cmd:ansible-vault" + ] + }, + "libnice-doc": { + "versions": { + "0.1.14-r2": 1543928834 + }, + "origin": "libnice" + }, + "lua5.3-struct": { + "versions": { + "0.2-r2": 1545076455 + }, + "origin": "lua-struct", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libnetfilter_cttimeout": { + "versions": { + "1.0.0-r0": 1542883681 + }, + "origin": "libnetfilter_cttimeout", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0" + ], + "provides": [ + "so:libnetfilter_cttimeout.so.1=1.0.0" + ] + }, + "bluez": { + "versions": { + "5.50-r0": 1545822206 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:bluemoon", + "cmd:bluetoothctl", + "cmd:bluez-simple-agent", + "cmd:btattach", + "cmd:hex2hcd", + "cmd:l2ping", + "cmd:l2test", + "cmd:mpris-proxy", + "cmd:rctest" + ] + }, + "perl-probe-perl": { + "versions": { + "0.03-r0": 1545061093 + }, + "origin": "perl-probe-perl" + }, + "py-libxml2": { + "versions": { + "2.9.9-r1": 1551105514 + }, + "origin": "libxml2" + }, + "acf-tinydns": { + "versions": { + "0.11.0-r2": 1545214114 + }, + "origin": "acf-tinydns", + "dependencies": [ + "acf-core", + "tinydns" + ] + }, + "spamassassin-compiler": { + "versions": { + "3.4.2-r0": 1545061982 + }, + "origin": "spamassassin", + "dependencies": [ + "re2c", + "gcc", + "perl-dev", + "perl-mail-spamassassin" + ], + "provides": [ + "cmd:sa-compile" + ] + }, + "py3-pluggy": { + "versions": { + "0.7.1-r0": 1542824894 + }, + "origin": "py-pluggy", + "dependencies": [ + "python3" + ] + }, + "kmod-bash-completion": { + "versions": { + "24-r1": 1542845355 + }, + "origin": "kmod" + }, + "lua-xml": { + "versions": { + "130610-r5": 1543998790 + }, + "origin": "lua-xml" + }, + "a2ps-dev": { + "versions": { + "4.14-r7": 1545209604 + }, + "origin": "a2ps" + }, + "sparsehash": { + "versions": { + "2.0.3-r0": 1545249832 + }, + "origin": "sparsehash", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:libsparsehash=2.0.2" + ] + }, + "font-micro-misc": { + "versions": { + "1.0.3-r0": 1544000376 + }, + "origin": "font-micro-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "boost-python": { + "versions": { + "1.67.0-r2": 1542823633 + }, + "origin": "boost", + "dependencies": [ + "python", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "so:libboost_python27-mt.so.1.67.0=1.67.0", + "so:libboost_python27.so.1.67.0=1.67.0" + ] + }, + "jasper-dev": { + "versions": { + "2.0.14-r0": 1543932347 + }, + "origin": "jasper", + "dependencies": [ + "jasper-libs=2.0.14-r0", + "pkgconfig" + ], + "provides": [ + "pc:jasper=2.0.14" + ] + }, + "uwsgi-ping": { + "versions": { + "2.0.17.1-r0": 1545062204 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "sdl2_mixer": { + "versions": { + "2.0.4-r0": 1545208849 + }, + "origin": "sdl2_mixer", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL2_mixer-2.0.so.0=0.2.2" + ] + }, + "openldap-back-sql": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2", + "so:libodbc.so.2" + ] + }, + "daq-dev": { + "versions": { + "2.0.6-r2": 1545163826 + }, + "origin": "daq", + "dependencies": [ + "daq=2.0.6-r2" + ], + "provides": [ + "cmd:daq-modules-config" + ] + }, + "lighttpd": { + "versions": { + "1.4.52-r0": 1545207630 + }, + "origin": "lighttpd", + "dependencies": [ + "/bin/sh", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libev.so.4", + "so:libfam.so.0", + "so:libldap-2.4.so.2", + "so:liblua-5.3.so.0", + "so:libpcre.so.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:lighttpd", + "cmd:lighttpd-angel" + ] + }, + "faad2": { + "versions": { + "2.7-r8": 1543998811 + }, + "origin": "faad2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfaad.so.2=2.0.0", + "cmd:faad" + ] + }, + "cryptsetup1-doc": { + "versions": { + "1.7.5-r4": 1545292765 + }, + "origin": "cryptsetup1" + }, + "libnih-doc": { + "versions": { + "1.0.3-r5": 1545062344 + }, + "origin": "libnih" + }, + "py-gnome": { + "versions": { + "2.28.1-r5": 1545301296 + }, + "origin": "py-gnome", + "dependencies": [ + "py-gnome-bonobo", + "py-gnome-gconf", + "py-gnome-libgnome", + "py-gnome-gnomevfs" + ] + }, + "collectd-apache": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "wayland-protocols": { + "versions": { + "1.17-r0": 1544791918 + }, + "origin": "wayland-protocols", + "provides": [ + "wayland-protocols-dev" + ] + }, + "acf-mdadm": { + "versions": { + "0.5.0-r2": 1545209219 + }, + "origin": "acf-mdadm", + "dependencies": [ + "acf-core", + "mdadm" + ] + }, + "lxc": { + "versions": { + "3.1.0-r1": 1549966162 + }, + "origin": "lxc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblxc.so.1" + ], + "provides": [ + "cmd:init.lxc", + "cmd:init.lxc.static", + "cmd:lxc-attach", + "cmd:lxc-autostart", + "cmd:lxc-cgroup", + "cmd:lxc-checkconfig", + "cmd:lxc-checkpoint", + "cmd:lxc-config", + "cmd:lxc-console", + "cmd:lxc-copy", + "cmd:lxc-create", + "cmd:lxc-destroy", + "cmd:lxc-device", + "cmd:lxc-execute", + "cmd:lxc-freeze", + "cmd:lxc-info", + "cmd:lxc-ls", + "cmd:lxc-monitor", + "cmd:lxc-snapshot", + "cmd:lxc-start", + "cmd:lxc-stop", + "cmd:lxc-top", + "cmd:lxc-unfreeze", + "cmd:lxc-unshare", + "cmd:lxc-update-config", + "cmd:lxc-usernsexec", + "cmd:lxc-wait" + ] + }, + "dovecot-dev": { + "versions": { + "2.3.6-r0": 1557134097 + }, + "origin": "dovecot" + }, + "hylafax": { + "versions": { + "6.0.7-r0": 1545746107 + }, + "origin": "hylafax", + "dependencies": [ + "ghostscript", + "bash", + "tiff-tools", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libstdc++.so.6", + "so:libtiff.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libhylafax-6.0.so.7=7", + "cmd:choptest", + "cmd:cqtest", + "cmd:dialtest", + "cmd:edit-faxcover", + "cmd:faxabort", + "cmd:faxaddmodem", + "cmd:faxadduser", + "cmd:faxalter", + "cmd:faxanswer", + "cmd:faxconfig", + "cmd:faxcover", + "cmd:faxcron", + "cmd:faxdeluser", + "cmd:faxgetty", + "cmd:faxinfo", + "cmd:faxlock", + "cmd:faxmail", + "cmd:faxmodem", + "cmd:faxmsg", + "cmd:faxq", + "cmd:faxqclean", + "cmd:faxquit", + "cmd:faxrm", + "cmd:faxsend", + "cmd:faxsetup", + "cmd:faxsetup.bsdi", + "cmd:faxsetup.irix", + "cmd:faxsetup.linux", + "cmd:faxstat", + "cmd:faxstate", + "cmd:faxwatch", + "cmd:hfaxd", + "cmd:hylafax", + "cmd:lockname", + "cmd:ondelay", + "cmd:pagesend", + "cmd:probemodem", + "cmd:recvstats", + "cmd:sendfax", + "cmd:sendpage", + "cmd:tagtest", + "cmd:textfmt", + "cmd:tiffcheck", + "cmd:tsitest", + "cmd:typetest", + "cmd:xferfaxstats" + ] + }, + "kmod-doc": { + "versions": { + "24-r1": 1542845353 + }, + "origin": "kmod" + }, + "perl-test-longstring": { + "versions": { + "0.17-r0": 1544000379 + }, + "origin": "perl-test-longstring", + "dependencies": [ + "perl" + ] + }, + "curl-dev": { + "versions": { + "7.64.0-r1": 1551205311 + }, + "origin": "curl", + "dependencies": [ + "openssl-dev", + "libssh2-dev", + "nghttp2-dev", + "zlib-dev", + "libcurl=7.64.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:libcurl=7.64.0", + "cmd:curl-config" + ] + }, + "xvfb": { + "versions": { + "1.20.3-r1": 1543928068 + }, + "origin": "xorg-server", + "dependencies": [ + "font-misc-misc", + "font-cursor-misc", + "xkeyboard-config", + "xkbcomp", + "xinit", + "so:libGL.so.1", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpixman-1.so.0" + ], + "provides": [ + "cmd:Xvfb" + ] + }, + "py-tracing": { + "versions": { + "0.8-r1": 1545209173 + }, + "origin": "py-tracing", + "dependencies": [ + "python2" + ] + }, + "lua5.1-stdlib": { + "versions": { + "41.2.1-r0": 1545292923 + }, + "origin": "lua-stdlib" + }, + "perl-datetime-format-natural": { + "versions": { + "1.06-r0": 1545163113 + }, + "origin": "perl-datetime-format-natural", + "dependencies": [ + "perl-datetime-timezone", + "perl-clone", + "perl-params-validate", + "perl-list-moreutils", + "perl-datetime", + "perl-boolean" + ], + "provides": [ + "cmd:dateparse" + ] + }, + "sg3_utils-doc": { + "versions": { + "1.42-r1": 1544000667 + }, + "origin": "sg3_utils" + }, + "s6-linux-init": { + "versions": { + "0.4.0.0-r0": 1545292931 + }, + "origin": "s6-linux-init", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.7" + ], + "provides": [ + "cmd:s6-halt", + "cmd:s6-linux-init-maker", + "cmd:s6-poweroff", + "cmd:s6-reboot" + ] + }, + "perl-fcgi-procmanager": { + "versions": { + "0.28-r0": 1543924484 + }, + "origin": "perl-fcgi-procmanager", + "dependencies": [ + "perl" + ] + }, + "ser2net": { + "versions": { + "3.4-r1": 1543999374 + }, + "origin": "ser2net", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ser2net" + ] + }, + "cogl": { + "versions": { + "1.22.2-r0": 1544000913 + }, + "origin": "cogl", + "dependencies": [ + "so:libEGL.so.1", + "so:libX11.so.6", + "so:libXdamage.so.1", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libdrm.so.2", + "so:libgbm.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0" + ], + "provides": [ + "so:libcogl-gles2.so.20=20.4.2", + "so:libcogl-pango.so.20=20.4.2", + "so:libcogl-path.so.20=20.4.2", + "so:libcogl.so.20=20.4.2" + ] + }, + "pigz-doc": { + "versions": { + "2.4-r0": 1542820956 + }, + "origin": "pigz" + }, + "rtapd": { + "versions": { + "1.7-r6": 1545301051 + }, + "origin": "rtapd", + "dependencies": [ + "rtnppd", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "cmd:rtapd", + "cmd:vsnppd" + ] + }, + "yajl": { + "versions": { + "2.1.0-r0": 1543935354 + }, + "origin": "yajl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libyajl.so.2=2.1.0" + ] + }, + "libxml2-dbg": { + "versions": { + "2.9.9-r1": 1551105514 + }, + "origin": "libxml2" + }, + "font-bh-lucidatypewriter-100dpi": { + "versions": { + "1.0.3-r0": 1545076403 + }, + "origin": "font-bh-lucidatypewriter-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-socket-doc": { + "versions": { + "2.027-r0": 1543922478 + }, + "origin": "perl-socket" + }, + "acf-awall": { + "versions": { + "0.4.1-r2": 1545300390 + }, + "origin": "acf-awall", + "dependencies": [ + "acf-core", + "awall" + ] + }, + "task": { + "versions": { + "2.5.1-r0": 1545254344 + }, + "origin": "task", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:task" + ] + }, + "py3-babel": { + "versions": { + "2.6.0-r0": 1542824979 + }, + "origin": "py-babel", + "dependencies": [ + "py3-tz", + "python3" + ], + "provides": [ + "cmd:pybabel" + ] + }, + "xrdp": { + "versions": { + "0.9.9-r0": 1545859298 + }, + "origin": "xrdp", + "dependencies": [ + "so:libX11.so.6", + "so:libXfixes.so.3", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libfuse.so.2", + "so:libssl.so.1.1", + "so:libturbojpeg.so.0" + ], + "provides": [ + "so:libcommon.so.0=0.0.0", + "so:libmc.so=0", + "so:libpainter.so.0=0.0.0", + "so:librfxencode.so.0=0.0.0", + "so:libscp.so.0=0.0.0", + "so:libvnc.so=0", + "so:libxrdp.so.0=0.0.0", + "so:libxrdpapi.so.0=0.0.0", + "so:libxup.so=0", + "cmd:xrdp", + "cmd:xrdp-chansrv", + "cmd:xrdp-dis", + "cmd:xrdp-genkeymap", + "cmd:xrdp-keygen", + "cmd:xrdp-sesadmin", + "cmd:xrdp-sesman", + "cmd:xrdp-sesrun" + ] + }, + "pdnsd-doc": { + "versions": { + "1.2.9a-r5": 1543077303 + }, + "origin": "pdnsd" + }, + "lua5.3-lzlib": { + "versions": { + "0.4.3-r0": 1542845786 + }, + "origin": "lua-lzlib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ] + }, + "qemu-xtensaeb": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-xtensaeb" + ] + }, + "udisks-doc": { + "versions": { + "1.0.5-r3": 1545069134 + }, + "origin": "udisks" + }, + "wavpack": { + "versions": { + "5.1.0-r7": 1548939905 + }, + "origin": "wavpack", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libwavpack.so.1=1.2.0", + "cmd:wavpack", + "cmd:wvgain", + "cmd:wvtag", + "cmd:wvunpack" + ] + }, + "perl-extutils-cchecker-doc": { + "versions": { + "0.10-r0": 1542883381 + }, + "origin": "perl-extutils-cchecker" + }, + "perl-test-deep": { + "versions": { + "1.128-r0": 1542845599 + }, + "origin": "perl-test-deep", + "dependencies": [ + "perl", + "perl-test-tester", + "perl-test-nowarnings" + ] + }, + "imap": { + "versions": { + "2007f-r9": 1545062297 + }, + "origin": "imap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:imapd", + "cmd:ipop2d", + "cmd:ipop3d" + ] + }, + "xtrans-doc": { + "versions": { + "1.3.5-r1": 1542822665 + }, + "origin": "xtrans" + }, + "cython": { + "versions": { + "0.29.2-r0": 1545918551 + }, + "origin": "cython", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:cygdb", + "cmd:cython", + "cmd:cythonize" + ] + }, + "libmspack-utils": { + "versions": { + "0.8_alpha-r0": 1543925821 + }, + "origin": "libmspack", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmspack.so.0" + ], + "provides": [ + "cmd:cabrip", + "cmd:chmextract", + "cmd:msexpand", + "cmd:oabextract" + ] + }, + "libtirpc-doc": { + "versions": { + "1.0.3-r0": 1543223616 + }, + "origin": "libtirpc" + }, + "ruby-net-telnet": { + "versions": { + "2.5.5-r0": 1557164837 + }, + "origin": "ruby", + "dependencies": [ + "ruby-libs" + ] + }, + "nload": { + "versions": { + "0.7.4-r3": 1545214123 + }, + "origin": "nload", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libformw.so.6", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:nload" + ] + }, + "libdvdcss-dev": { + "versions": { + "1.4.2-r0": 1545838428 + }, + "origin": "libdvdcss", + "dependencies": [ + "libdvdcss=1.4.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libdvdcss=1.4.2" + ] + }, + "libmilter": { + "versions": { + "1.0.2-r6": 1543927485 + }, + "origin": "libmilter", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmilter.so.1.0.2=1.0.2" + ] + }, + "ruby-webrick": { + "versions": { + "2.5.5-r0": 1557164838 + }, + "origin": "ruby", + "dependencies": [ + "ruby" + ] + }, + "bash-completion": { + "versions": { + "2.8-r0": 1543935613 + }, + "origin": "bash-completion", + "dependencies": [ + "bash" + ] + }, + "boost-serialization": { + "versions": { + "1.67.0-r2": 1542823633 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_serialization-mt.so.1.67.0=1.67.0", + "so:libboost_serialization.so.1.67.0=1.67.0" + ] + }, + "xsetroot": { + "versions": { + "1.1.2-r0": 1545069361 + }, + "origin": "xsetroot", + "dependencies": [ + "so:libX11.so.6", + "so:libXcursor.so.1", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xsetroot" + ] + }, + "lua5.1-posixtz": { + "versions": { + "0.5-r1": 1543928315 + }, + "origin": "lua-posixtz", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "pax-utils-doc": { + "versions": { + "1.2.3-r0": 1542304831 + }, + "origin": "pax-utils" + }, + "cdw-doc": { + "versions": { + "0.8.1-r0": 1545302173 + }, + "origin": "cdw" + }, + "py3-httplib2": { + "versions": { + "0.12.0-r0": 1548112519 + }, + "origin": "py-httplib2", + "dependencies": [ + "python3" + ] + }, + "lua5.2-optarg": { + "versions": { + "0.2-r1": 1542820940 + }, + "origin": "lua-optarg", + "dependencies": [ + "lua5.2" + ] + }, + "libgsf": { + "versions": { + "1.14.44-r0": 1543926575 + }, + "origin": "libgsf", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libgsf-1.so.114=114.0.44", + "cmd:gsf", + "cmd:gsf-vba-dump" + ] + }, + "lua5.1-ldbus": { + "versions": { + "20150430-r2": 1545300019 + }, + "origin": "lua-ldbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ] + }, + "luajit": { + "versions": { + "2.1.0_beta3-r4": 1542820929 + }, + "origin": "luajit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "lua", + "so:libluajit-5.1.so.2=2.1.0", + "cmd:luajit", + "cmd:luajit-2.1.0-beta3" + ] + }, + "xf86-video-s3virge": { + "versions": { + "1.10.7-r4": 1545069465 + }, + "origin": "xf86-video-s3virge", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "installkernel": { + "versions": { + "3.5-r0": 1543929099 + }, + "origin": "installkernel", + "provides": [ + "cmd:installkernel" + ] + }, + "gnupg1-doc": { + "versions": { + "1.4.23-r0": 1545302139 + }, + "origin": "gnupg1", + "provides": [ + "gnupg-doc=1.4.23-r0" + ] + }, + "lua5.2-cqueues": { + "versions": { + "20171014-r3": 1546520955 + }, + "origin": "lua-cqueues", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "bind-openrc": { + "versions": { + "9.12.4_p1-r1": 1556801608 + }, + "origin": "bind" + }, + "perl-net-telnet": { + "versions": { + "3.04-r0": 1543999441 + }, + "origin": "perl-net-telnet" + }, + "gdl-lang": { + "versions": { + "3.28.0-r0": 1545215938 + }, + "origin": "gdl" + }, + "lua5.3-expat": { + "versions": { + "1.3.0-r2": 1544000284 + }, + "origin": "lua-expat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "perl-future-doc": { + "versions": { + "0.39-r0": 1544799049 + }, + "origin": "perl-future" + }, + "mercurial": { + "versions": { + "4.9.1-r0": 1557160461 + }, + "origin": "mercurial", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:hg", + "cmd:hgeditor", + "cmd:hgk" + ] + }, + "ssmtp-doc": { + "versions": { + "2.64-r14": 1545215006 + }, + "origin": "ssmtp" + }, + "py2-cryptography": { + "versions": { + "2.4.2-r2": 1545067167 + }, + "origin": "py-cryptography", + "dependencies": [ + "py2-cffi", + "py2-idna", + "py2-asn1crypto", + "py2-six", + "py2-ipaddress", + "py-enum34", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpython2.7.so.1.0", + "so:libssl.so.1.1" + ] + }, + "gobject-introspection": { + "versions": { + "1.56.1-r0": 1542823016 + }, + "origin": "gobject-introspection", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libgirepository-1.0.so.1=1.0.0" + ] + }, + "netcat-openbsd": { + "versions": { + "1.130-r1": 1543935626 + }, + "origin": "netcat-openbsd", + "dependencies": [ + "so:libbsd.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:nc" + ] + }, + "libxaw": { + "versions": { + "1.0.13-r2": 1542894088 + }, + "origin": "libxaw", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXmu.so.6", + "so:libXpm.so.4", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXaw.so.7=7.0.0" + ] + }, + "perl-io-captureoutput": { + "versions": { + "1.1104-r0": 1543259614 + }, + "origin": "perl-io-captureoutput" + }, + "libotr3-tools": { + "versions": { + "3.2.1-r4": 1543230935 + }, + "origin": "libotr3", + "dependencies": [ + "libotr3", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libotr.so.2" + ], + "provides": [ + "cmd:otr_mackey", + "cmd:otr_modify", + "cmd:otr_parse", + "cmd:otr_readforge", + "cmd:otr_remac", + "cmd:otr_sesskeys" + ] + }, + "xorg-cf-files": { + "versions": { + "1.0.6-r0": 1543927852 + }, + "origin": "xorg-cf-files" + }, + "wayland-libs-client": { + "versions": { + "1.16.0-r0": 1544001083 + }, + "origin": "wayland", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6" + ], + "provides": [ + "so:libwayland-client.so.0=0.3.0" + ] + }, + "icecast-openrc": { + "versions": { + "2.4.4-r1": 1543934499 + }, + "origin": "icecast" + }, + "freeradius": { + "versions": { + "3.0.17-r5": 1556201989 + }, + "origin": "freeradius", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libfreeradius-dhcp.so", + "so:libfreeradius-radius.so", + "so:libfreeradius-server.so", + "so:libgdbm.so.4", + "so:libpcap.so.1", + "so:libreadline.so.7", + "so:libssl.so.1.1", + "so:libtalloc.so.2" + ], + "provides": [ + "freeradius3=3.0.17-r5", + "so:proto_dhcp.so=0", + "so:proto_vmps.so=0", + "so:rlm_always.so=0", + "so:rlm_attr_filter.so=0", + "so:rlm_cache.so=0", + "so:rlm_cache_rbtree.so=0", + "so:rlm_chap.so=0", + "so:rlm_counter.so=0", + "so:rlm_cram.so=0", + "so:rlm_date.so=0", + "so:rlm_detail.so=0", + "so:rlm_dhcp.so=0", + "so:rlm_digest.so=0", + "so:rlm_dynamic_clients.so=0", + "so:rlm_exec.so=0", + "so:rlm_expiration.so=0", + "so:rlm_expr.so=0", + "so:rlm_files.so=0", + "so:rlm_ippool.so=0", + "so:rlm_linelog.so=0", + "so:rlm_logintime.so=0", + "so:rlm_mschap.so=0", + "so:rlm_otp.so=0", + "so:rlm_pap.so=0", + "so:rlm_passwd.so=0", + "so:rlm_preprocess.so=0", + "so:rlm_radutmp.so=0", + "so:rlm_realm.so=0", + "so:rlm_replicate.so=0", + "so:rlm_soh.so=0", + "so:rlm_sometimes.so=0", + "so:rlm_test.so=0", + "so:rlm_unix.so=0", + "so:rlm_unpack.so=0", + "so:rlm_utf8.so=0", + "so:rlm_wimax.so=0", + "cmd:dhcpclient", + "cmd:map_unit", + "cmd:rad_counter", + "cmd:radattr", + "cmd:radcrypt", + "cmd:raddebug", + "cmd:radiusd", + "cmd:radlast", + "cmd:radmin", + "cmd:radsniff", + "cmd:radsqlrelay", + "cmd:radtest", + "cmd:radwho", + "cmd:radzap", + "cmd:rlm_ippool_tool", + "cmd:smbencrypt" + ] + }, + "xl2tpd-doc": { + "versions": { + "1.3.10.1-r0": 1545300581 + }, + "origin": "xl2tpd" + }, + "perl-struct-dumb-doc": { + "versions": { + "0.09-r0": 1544799022 + }, + "origin": "perl-struct-dumb" + }, + "xmlto": { + "versions": { + "0.0.28-r2": 1542822500 + }, + "origin": "xmlto", + "dependencies": [ + "libxslt", + "perl-yaml-syck", + "perl-test-pod", + "bash", + "docbook-xsl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xmlif", + "cmd:xmlto" + ] + }, + "qpdf-dev": { + "versions": { + "8.3.0-r0": 1547117323 + }, + "origin": "qpdf", + "dependencies": [ + "pc:libjpeg", + "pc:zlib", + "pkgconfig", + "qpdf-libs=8.3.0-r0" + ], + "provides": [ + "pc:libqpdf=8.3.0" + ] + }, + "py-nose": { + "versions": { + "1.3.7-r2": 1544797228 + }, + "origin": "py-nose" + }, + "gtk+3.0-dev": { + "versions": { + "3.24.1-r0": 1543927434 + }, + "origin": "gtk+3.0", + "dependencies": [ + "at-spi2-atk-dev", + "atk-dev", + "cairo-dev", + "fontconfig-dev", + "gdk-pixbuf-dev", + "glib-dev", + "libepoxy-dev", + "libx11-dev", + "libxcomposite-dev", + "libxcursor-dev", + "libxdamage-dev", + "libxext-dev", + "libxfixes-dev", + "libxi-dev", + "libxinerama-dev", + "libxrandr-dev", + "pango-dev", + "wayland-protocols", + "wayland-libs-client", + "wayland-libs-cursor", + "wayland-dev", + "libxkbcommon-dev", + "gtk+3.0=3.24.1-r0", + "pc:atk", + "pc:atk-bridge-2.0", + "pc:atk>=2.15.1", + "pc:cairo", + "pc:cairo-gobject>=1.14.0", + "pc:cairo-xlib", + "pc:cairo>=1.14.0", + "pc:epoxy>=1.4", + "pc:fontconfig", + "pc:gdk-pixbuf-2.0>=2.30.0", + "pc:gio-2.0>=2.53.4", + "pc:gio-unix-2.0>=2.53.4", + "pc:pango", + "pc:pangocairo", + "pc:pangoft2", + "pc:wayland-client>=1.9.91", + "pc:wayland-cursor>=1.9.91", + "pc:wayland-egl", + "pc:x11", + "pc:xcomposite", + "pc:xcursor", + "pc:xdamage", + "pc:xext", + "pc:xfixes", + "pc:xi", + "pc:xinerama", + "pc:xkbcommon>=0.2.0", + "pc:xrandr", + "pkgconfig" + ], + "provides": [ + "pc:gail-3.0=3.24.1", + "pc:gdk-3.0=3.24.1", + "pc:gdk-wayland-3.0=3.24.1", + "pc:gdk-x11-3.0=3.24.1", + "pc:gtk+-3.0=3.24.1", + "pc:gtk+-unix-print-3.0=3.24.1", + "pc:gtk+-wayland-3.0=3.24.1", + "pc:gtk+-x11-3.0=3.24.1" + ] + }, + "perl-cache-simple-timedexpiry-doc": { + "versions": { + "0.27-r1": 1544798624 + }, + "origin": "perl-cache-simple-timedexpiry" + }, + "at-spi2-atk": { + "versions": { + "2.26.2-r0": 1543926534 + }, + "origin": "at-spi2-atk", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libatspi.so.0", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libatk-bridge-2.0.so.0=0.0.0" + ] + }, + "tree-doc": { + "versions": { + "1.8.0-r0": 1544791740 + }, + "origin": "tree" + }, + "perl-test-number-delta-doc": { + "versions": { + "1.06-r0": 1545062324 + }, + "origin": "perl-test-number-delta" + }, + "highlight-doc": { + "versions": { + "3.46-r0": 1542823672 + }, + "origin": "highlight" + }, + "py-phonenumbers": { + "versions": { + "8.10.4-r0": 1548490639 + }, + "origin": "py-phonenumbers" + }, + "gradm-doc": { + "versions": { + "3.1.201607172312-r0": 1545299889 + }, + "origin": "gradm" + }, + "perl-xml-parser": { + "versions": { + "2.44-r4": 1542821912 + }, + "origin": "perl-xml-parser", + "dependencies": [ + "perl-libwww", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "postfix-ldap": { + "versions": { + "3.3.2-r0": 1547130362 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ], + "provides": [ + "so:postfix-ldap.so=0" + ] + }, + "py-ediarpc": { + "versions": { + "0.3-r0": 1545214310 + }, + "origin": "py-ediarpc", + "dependencies": [ + "python2" + ] + }, + "libwebp-doc": { + "versions": { + "1.0.1-r0": 1545856968 + }, + "origin": "libwebp" + }, + "adwaita-icon-theme-dev": { + "versions": { + "3.31.1-r0": 1545820342 + }, + "origin": "adwaita-icon-theme", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:adwaita-icon-theme=3.31.1" + ] + }, + "java-common": { + "versions": { + "0.1-r0": 1545076633 + }, + "origin": "java-common", + "dependencies": [ + "/bin/sh" + ] + }, + "p7zip-doc": { + "versions": { + "16.02-r3": 1545118129 + }, + "origin": "p7zip" + }, + "sylpheed": { + "versions": { + "3.7.0-r2": 1544799190 + }, + "origin": "sylpheed", + "dependencies": [ + "pinentry-gtk", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcrypto.so.1.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgpgme.so.11", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libsylph-0.so.1=1.3.0", + "so:libsylpheed-plugin-0.so.1=1.3.0", + "cmd:sylpheed" + ] + }, + "newt-dev": { + "versions": { + "0.52.20-r0": 1543924699 + }, + "origin": "newt", + "dependencies": [ + "newt=0.52.20-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnewt=0.52.20" + ] + }, + "perl-mime-lite-doc": { + "versions": { + "3.030-r1": 1543226638 + }, + "origin": "perl-mime-lite" + }, + "vala-doc": { + "versions": { + "0.42.4-r0": 1545859420 + }, + "origin": "vala" + }, + "mariadb": { + "versions": { + "10.3.13-r1": 1557431745 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common", + "/bin/sh", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:liblzma.so.5", + "so:libncursesw.so.6", + "so:libpcre.so.1", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:aria_chk", + "cmd:aria_dump_log", + "cmd:aria_ftdump", + "cmd:aria_pack", + "cmd:aria_read_log", + "cmd:innochecksum", + "cmd:msql2mysql", + "cmd:my_print_defaults", + "cmd:myisamchk", + "cmd:myisamlog", + "cmd:myisampack", + "cmd:myrocks_hotbackup", + "cmd:mysql_client_test_embedded", + "cmd:mysql_embedded", + "cmd:mysql_install_db", + "cmd:mysql_ldb", + "cmd:mysql_plugin", + "cmd:mysql_secure_installation", + "cmd:mysql_tzinfo_to_sql", + "cmd:mysqlbinlog", + "cmd:mysqld", + "cmd:mysqld_safe", + "cmd:mysqlslap", + "cmd:replace", + "cmd:resolve_stack_dump", + "cmd:resolveip", + "cmd:sst_dump", + "cmd:test-connect-t", + "cmd:wsrep_sst_mariabackup", + "cmd:wsrep_sst_mysqldump", + "cmd:wsrep_sst_rsync", + "cmd:wsrep_sst_rsync_wan" + ] + }, + "rxvt-unicode-terminfo": { + "versions": { + "9.22-r4": 1543254026 + }, + "origin": "rxvt-unicode" + }, + "mdadm": { + "versions": { + "4.1-r0": 1545858163 + }, + "origin": "mdadm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mdadm", + "cmd:mdmon" + ] + }, + "bind-dev": { + "versions": { + "9.12.4_p1-r1": 1556801608 + }, + "origin": "bind", + "dependencies": [ + "bind-libs=9.12.4_p1-r1" + ], + "provides": [ + "cmd:bind9-config" + ] + }, + "libressl2.7-libssl": { + "versions": { + "2.7.5-r0": 1551116832 + }, + "origin": "libressl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.43" + ], + "provides": [ + "so:libssl.so.45=45.0.1" + ] + }, + "cups-client": { + "versions": { + "2.2.10-r0": 1545910846 + }, + "origin": "cups", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libcupsimage.so.2", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:accept", + "cmd:cancel", + "cmd:cupsaccept", + "cmd:cupsaddsmb", + "cmd:cupsctl", + "cmd:cupsdisable", + "cmd:cupsenable", + "cmd:cupstestdsc", + "cmd:cupstestppd", + "cmd:ippfind", + "cmd:lp", + "cmd:lpadmin", + "cmd:lpc", + "cmd:lpinfo", + "cmd:lpmove", + "cmd:lpoptions", + "cmd:lpq", + "cmd:lpr", + "cmd:lprm", + "cmd:lpstat", + "cmd:ppdc", + "cmd:ppdhtml", + "cmd:ppdi", + "cmd:ppdmerge", + "cmd:ppdpo", + "cmd:reject" + ] + }, + "qemu-armeb": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-armeb" + ] + }, + "cryptsetup1-libs": { + "versions": { + "1.7.5-r4": 1545292766 + }, + "origin": "cryptsetup1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libdevmapper.so.1.02", + "so:libuuid.so.1" + ], + "provides": [ + "so:libcryptsetup.so.4=4.7.0" + ] + }, + "libgphoto2": { + "versions": { + "2.5.16-r0": 1545116802 + }, + "origin": "libgphoto2", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libexif.so.12", + "so:libjpeg.so.8", + "so:libltdl.so.7", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libgphoto2.so.6=6.0.0", + "so:libgphoto2_port.so.12=12.0.0" + ] + }, + "freeradius-dbg": { + "versions": { + "3.0.17-r5": 1556201987 + }, + "origin": "freeradius" + }, + "libdvbcsa-dev": { + "versions": { + "1.1.0-r1": 1545293164 + }, + "origin": "libdvbcsa", + "dependencies": [ + "libdvbcsa=1.1.0-r1" + ] + }, + "lua5.3-dbi-mysql": { + "versions": { + "0.6-r3": 1545214224 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "libusb-dev": { + "versions": { + "1.0.22-r0": 1543223278 + }, + "origin": "libusb", + "dependencies": [ + "libusb=1.0.22-r0", + "pkgconfig" + ], + "provides": [ + "pc:libusb-1.0=1.0.22" + ] + }, + "poppler-qt4": { + "versions": { + "0.56.0-r0": 1545254184 + }, + "origin": "poppler-qt4", + "dependencies": [ + "so:libQtCore.so.4", + "so:libQtGui.so.4", + "so:libQtXml.so.4", + "so:libc.musl-x86_64.so.1", + "so:libpoppler.so.67", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libpoppler-qt4.so.4=4.11.0" + ] + }, + "perl-crypt-openssl-rsa-doc": { + "versions": { + "0.31-r1": 1545061168 + }, + "origin": "perl-crypt-openssl-rsa" + }, + "execline": { + "versions": { + "2.5.0.1-r0": 1543221686 + }, + "origin": "execline", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.7" + ], + "provides": [ + "so:libexecline.so.2.5=2.5.0.1", + "cmd:background", + "cmd:backtick", + "cmd:cd", + "cmd:define", + "cmd:dollarat", + "cmd:elgetopt", + "cmd:elgetpositionals", + "cmd:elglob", + "cmd:emptyenv", + "cmd:exec", + "cmd:execlineb", + "cmd:exit", + "cmd:export", + "cmd:fdblock", + "cmd:fdclose", + "cmd:fdmove", + "cmd:fdreserve", + "cmd:fdswap", + "cmd:forbacktickx", + "cmd:foreground", + "cmd:forstdin", + "cmd:forx", + "cmd:getcwd", + "cmd:getpid", + "cmd:heredoc", + "cmd:homeof", + "cmd:if", + "cmd:ifelse", + "cmd:ifte", + "cmd:ifthenelse", + "cmd:importas", + "cmd:loopwhilex", + "cmd:multidefine", + "cmd:multisubstitute", + "cmd:pipeline", + "cmd:piperw", + "cmd:redirfd", + "cmd:runblock", + "cmd:shift", + "cmd:trap", + "cmd:tryexec", + "cmd:umask", + "cmd:unexport", + "cmd:wait", + "cmd:withstdinas" + ] + }, + "libotr3-dev": { + "versions": { + "3.2.1-r4": 1543230934 + }, + "origin": "libotr3", + "dependencies": [ + "libgcrypt-dev", + "libotr3=3.2.1-r4", + "pkgconfig" + ], + "provides": [ + "pc:libotr=3.1.0" + ] + }, + "x264-dev": { + "versions": { + "20180304-r1": 1545299801 + }, + "origin": "x264", + "dependencies": [ + "pkgconfig", + "x264-libs=20180304-r1" + ], + "provides": [ + "pc:x264=0.152.x" + ] + }, + "xf86-video-glint": { + "versions": { + "1.2.9-r3": 1545300614 + }, + "origin": "xf86-video-glint", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-doc": { + "versions": { + "5.26.3-r0": 1543998672 + }, + "origin": "perl" + }, + "antiword": { + "versions": { + "0.37-r3": 1545163687 + }, + "origin": "antiword", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:antiword" + ] + }, + "gtksourceview2-dev": { + "versions": { + "2.10.5-r0": 1545292663 + }, + "origin": "gtksourceview2", + "dependencies": [ + "gtk+2.0-dev", + "libxml2-dev", + "gtksourceview2=2.10.5-r0", + "pc:gtk+-2.0>=2.12.0", + "pc:libxml-2.0>=2.5.0", + "pkgconfig" + ], + "provides": [ + "pc:gtksourceview-2.0=2.10.5" + ] + }, + "skalibs": { + "versions": { + "2.7.0.0-r0": 1543221683 + }, + "origin": "skalibs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libskarnet.so.2.7=2.7.0.0" + ] + }, + "zsh": { + "versions": { + "5.6.2-r0": 1545308088 + }, + "origin": "zsh", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:zsh", + "cmd:zsh-5.6.2" + ] + }, + "perl-html-quoted": { + "versions": { + "0.04-r0": 1545214098 + }, + "origin": "perl-html-quoted", + "dependencies": [ + "perl", + "perl-html-parser" + ] + }, + "kamailio-authephemeral": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "openldap-overlay-rwm": { + "versions": { + "2.4.47-r2": 1546016480 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "freeradius-sql": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "freeradius3-sql=3.0.17-r5", + "so:rlm_sql.so=0", + "so:rlm_sql_null.so=0", + "so:rlm_sqlcounter.so=0", + "so:rlm_sqlippool.so=0" + ] + }, + "perl-test-without-doc": { + "versions": { + "0.10-r0": 1545223239 + }, + "origin": "perl-test-without" + }, + "libcom_err": { + "versions": { + "1.44.5-r0": 1545745858 + }, + "origin": "e2fsprogs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcom_err.so.2=2.1" + ] + }, + "py2-pathlib2": { + "versions": { + "2.3.2-r0": 1542824911 + }, + "origin": "py-pathlib2", + "dependencies": [ + "py2-six", + "py2-scandir", + "python2" + ] + }, + "py-babel": { + "versions": { + "2.6.0-r0": 1542824980 + }, + "origin": "py-babel", + "dependencies": [ + "py-tz" + ] + }, + "bacula-client": { + "versions": { + "9.4.1-r1": 1546944087 + }, + "origin": "bacula", + "dependencies": [ + "so:libacl.so.1", + "so:libbac-9.4.1.so", + "so:libbaccfg-9.4.1.so", + "so:libbacfind-9.4.1.so", + "so:libc.musl-x86_64.so.1", + "so:liblzo2.so.2", + "so:libncursesw.so.6", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:bacula", + "cmd:bacula-fd", + "cmd:bconsole", + "cmd:btraceback" + ] + }, + "s6-linux-utils": { + "versions": { + "2.5.0.0-r0": 1545299630 + }, + "origin": "s6-linux-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.7" + ], + "provides": [ + "cmd:s6-chroot", + "cmd:s6-fillurandompool", + "cmd:s6-freeramdisk", + "cmd:s6-hostname", + "cmd:s6-logwatch", + "cmd:s6-mount", + "cmd:s6-pivotchroot", + "cmd:s6-ps", + "cmd:s6-swapoff", + "cmd:s6-swapon", + "cmd:s6-umount" + ] + }, + "libobjc": { + "versions": { + "8.3.0-r0": 1554745498 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libobjc.so.4=4.0.0" + ] + }, + "dnscache": { + "versions": { + "1.05-r47": 1545208950 + }, + "origin": "djbdns", + "dependencies": [ + "djbdns-common", + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dnscache", + "cmd:dnscache-conf" + ] + }, + "shared-mime-info-lang": { + "versions": { + "1.10-r0": 1542821941 + }, + "origin": "shared-mime-info" + }, + "fuse": { + "versions": { + "2.9.8-r2": 1542972242 + }, + "origin": "fuse", + "dependencies": [ + "fuse-common", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfuse.so.2=2.9.8", + "so:libulockmgr.so.1=1.0.1", + "cmd:fusermount", + "cmd:mount.fuse", + "cmd:ulockmgr_server" + ] + }, + "nginx-doc": { + "versions": { + "1.14.2-r1": 1557179819 + }, + "origin": "nginx" + }, + "perl-net-smtp-ssl": { + "versions": { + "1.04-r0": 1545209114 + }, + "origin": "perl-net-smtp-ssl", + "dependencies": [ + "perl-io-socket-ssl", + "perl-net-ssleay" + ] + }, + "py3-cairo": { + "versions": { + "1.16.3-r0": 1543925201 + }, + "origin": "py-cairo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libpython3.6m.so.1.0" + ] + }, + "vte3-dev": { + "versions": { + "0.52.2-r0": 1545215297 + }, + "origin": "vte3", + "dependencies": [ + "pc:gio-2.0", + "pc:gio-unix-2.0", + "pc:glib-2.0>=2.40.0", + "pc:gnutls>=3.2.7", + "pc:gobject-2.0", + "pc:gtk+-3.0>=3.8.0", + "pc:libpcre2-8>=10.21", + "pc:pango>=1.22.0", + "pc:zlib", + "pkgconfig", + "vte3=0.52.2-r0" + ], + "provides": [ + "pc:vte-2.91=0.52.2" + ] + }, + "pam-pgsql-doc": { + "versions": { + "0.7.3.2-r0": 1545068588 + }, + "origin": "pam-pgsql" + }, + "gapk": { + "versions": { + "0.1-r1": 1545223041 + }, + "origin": "gapk", + "dependencies": [ + "apk-tools", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0" + ], + "provides": [ + "cmd:gapk" + ] + }, + "perl-cpanel-json-xs": { + "versions": { + "4.08-r0": 1544792332 + }, + "origin": "perl-cpanel-json-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cpanel_json_xs" + ] + }, + "libsndfile": { + "versions": { + "1.0.28-r8": 1555066604 + }, + "origin": "libsndfile", + "dependencies": [ + "so:libFLAC.so.8", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2" + ], + "provides": [ + "so:libsndfile.so.1=1.0.28", + "cmd:sndfile-cmp", + "cmd:sndfile-concat", + "cmd:sndfile-convert", + "cmd:sndfile-deinterleave", + "cmd:sndfile-info", + "cmd:sndfile-interleave", + "cmd:sndfile-metadata-get", + "cmd:sndfile-metadata-set", + "cmd:sndfile-play", + "cmd:sndfile-salvage" + ] + }, + "py2-wtforms": { + "versions": { + "2.1-r0": 1544000620 + }, + "origin": "py-wtforms", + "dependencies": [ + "python2" + ] + }, + "perl-data-uuid-doc": { + "versions": { + "1.221-r1": 1545163353 + }, + "origin": "perl-data-uuid" + }, + "perl-ldap-doc": { + "versions": { + "0.65-r1": 1543081710 + }, + "origin": "perl-ldap" + }, + "perl-class-data-inheritable": { + "versions": { + "0.08-r0": 1542845803 + }, + "origin": "perl-class-data-inheritable" + }, + "perl-socket-getaddrinfo-doc": { + "versions": { + "0.22-r0": 1542883394 + }, + "origin": "perl-socket-getaddrinfo" + }, + "uwsgi-emperor_pg": { + "versions": { + "2.0.17.1-r0": 1545062199 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "perl-class-load": { + "versions": { + "0.25-r0": 1543226708 + }, + "origin": "perl-class-load", + "dependencies": [ + "perl-data-optlist", + "perl-module-runtime", + "perl-module-implementation", + "perl-try-tiny", + "perl-namespace-clean", + "perl-package-stash" + ] + }, + "collectd-utils": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcollectdclient.so.1" + ], + "provides": [ + "cmd:collectd-nagios", + "cmd:collectd-tg", + "cmd:collectdctl" + ] + }, + "apache-mod-auth-kerb": { + "versions": { + "5.4-r5": 1545076830 + }, + "origin": "apache-mod-auth-kerb", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libgssapi_krb5.so.2", + "so:libkrb5.so.3" + ] + }, + "iwlwifi-5000-ucode": { + "versions": { + "8.83.5.1-r0": 1542845814 + }, + "origin": "iwlwifi-5000-ucode" + }, + "ragel-doc": { + "versions": { + "6.10-r0": 1545208962 + }, + "origin": "ragel" + }, + "sg3_utils-dev": { + "versions": { + "1.42-r1": 1544000665 + }, + "origin": "sg3_utils", + "dependencies": [ + "sg3_utils=1.42-r1" + ] + }, + "cciss_vol_status-doc": { + "versions": { + "1.12-r0": 1545073934 + }, + "origin": "cciss_vol_status" + }, + "audit-libs": { + "versions": { + "2.8.4-r0": 1543245856 + }, + "origin": "audit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0" + ], + "provides": [ + "so:libaudit.so.1=1.0.0", + "so:libauparse.so.0=0.0.0" + ] + }, + "git-svn": { + "versions": { + "2.20.1-r0": 1545214194 + }, + "origin": "git", + "dependencies": [ + "perl", + "perl-git-svn=2.20.1-r0", + "perl-subversion", + "perl-term-readkey", + "so:libc.musl-x86_64.so.1", + "so:libpcre2-8.so.0", + "so:libz.so.1" + ] + }, + "lua5.3-openrc": { + "versions": { + "0.2-r3": 1545066957 + }, + "origin": "lua-openrc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librc.so.1" + ] + }, + "irssi-dev": { + "versions": { + "1.1.2-r0": 1547738040 + }, + "origin": "irssi" + }, + "transmission-doc": { + "versions": { + "2.94-r1": 1545208749 + }, + "origin": "transmission" + }, + "kbd": { + "versions": { + "2.0.4-r3": 1545215192 + }, + "origin": "kbd", + "dependencies": [ + "kbd-misc", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chvt", + "cmd:deallocvt", + "cmd:dumpkeys", + "cmd:fgconsole", + "cmd:getkeycodes", + "cmd:kbd_mode", + "cmd:kbdinfo", + "cmd:kbdrate", + "cmd:loadkeys", + "cmd:loadunimap", + "cmd:mapscrn", + "cmd:open", + "cmd:openvt", + "cmd:psfaddtable", + "cmd:psfgettable", + "cmd:psfstriptable", + "cmd:psfxtable", + "cmd:resizecons", + "cmd:setfont", + "cmd:setkeycodes", + "cmd:setleds", + "cmd:setmetamode", + "cmd:setvtrgb", + "cmd:showconsolefont", + "cmd:showkey", + "cmd:unicode_start", + "cmd:unicode_stop" + ] + }, + "libxrandr-dev": { + "versions": { + "1.5.1-r2": 1542972233 + }, + "origin": "libxrandr", + "dependencies": [ + "libxext-dev", + "libxrandr=1.5.1-r2", + "pc:randrproto>=1.5", + "pc:x11", + "pc:xext", + "pc:xproto", + "pc:xrender", + "pkgconfig" + ], + "provides": [ + "pc:xrandr=1.5.1" + ] + }, + "perl-class-method-modifiers": { + "versions": { + "2.12-r0": 1545073307 + }, + "origin": "perl-class-method-modifiers" + }, + "libavc1394-dev": { + "versions": { + "0.5.4-r2": 1543932110 + }, + "origin": "libavc1394", + "dependencies": [ + "libavc1394=0.5.4-r2", + "pc:libraw1394", + "pkgconfig" + ], + "provides": [ + "pc:libavc1394=0.5.4" + ] + }, + "libtls-standalone-doc": { + "versions": { + "2.7.4-r6": 1546784623 + }, + "origin": "libtls-standalone" + }, + "xbanish-doc": { + "versions": { + "1.6-r0": 1545290830 + }, + "origin": "xbanish" + }, + "perl-test-failwarnings": { + "versions": { + "0.008-r1": 1543238918 + }, + "origin": "perl-test-failwarnings" + }, + "perl-module-implementation-doc": { + "versions": { + "0.09-r0": 1542845723 + }, + "origin": "perl-module-implementation" + }, + "libsmbclient": { + "versions": { + "4.8.11-r1": 1555334973 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcli-smb-common-samba4.so", + "so:libdcerpc-samba-samba4.so", + "so:libgse-samba4.so", + "so:liblibcli-lsa3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-security-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsecrets3-samba4.so", + "so:libsmbconf.so.0", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-cmdline-samba4.so" + ], + "provides": [ + "so:libsmbclient.so.0=0.3.1" + ] + }, + "asterisk-srtp": { + "versions": { + "15.7.1-r0": 1546247586 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "ntop-dev": { + "versions": { + "5.0.1-r14": 1545209735 + }, + "origin": "ntop", + "dependencies": [ + "ntop=5.0.1-r14" + ] + }, + "pixman-dbg": { + "versions": { + "0.34.0-r6": 1542822838 + }, + "origin": "pixman" + }, + "asterisk-speex": { + "versions": { + "15.7.1-r0": 1546247586 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0", + "so:libspeex.so.1", + "so:libspeexdsp.so.1" + ] + }, + "perl-scalar-list-utils-doc": { + "versions": { + "1.50-r0": 1543238901 + }, + "origin": "perl-scalar-list-utils" + }, + "qextserialport-dev": { + "versions": { + "1.2_rc1-r0": 1545300093 + }, + "origin": "qextserialport", + "dependencies": [ + "qt-dev", + "qextserialport=1.2_rc1-r0" + ] + }, + "qemu-mipsel": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mipsel" + ] + }, + "gphoto2-doc": { + "versions": { + "2.5.15-r0": 1545300679 + }, + "origin": "gphoto2" + }, + "libconfig-doc": { + "versions": { + "1.5-r3": 1543249698 + }, + "origin": "libconfig" + }, + "libxslt-dev": { + "versions": { + "1.1.33-r1": 1555485887 + }, + "origin": "libxslt", + "dependencies": [ + "libxslt=1.1.33-r1", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libexslt=0.8.20", + "pc:libxslt=1.1.33", + "cmd:xslt-config" + ] + }, + "collectd-lua": { + "versions": { + "5.8.0-r3": 1546422678 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ] + }, + "squid-lang-et": { + "versions": { + "4.4-r1": 1545216325 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-crypt-des": { + "versions": { + "2.07-r4": 1543923065 + }, + "origin": "perl-crypt-des", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.1-sql-mysql": { + "versions": { + "2.3.5-r2": 1543924396 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "iwlwifi-6000-ucode-doc": { + "versions": { + "9.221.4.1-r0": 1545256915 + }, + "origin": "iwlwifi-6000-ucode" + }, + "ca-certificates-doc": { + "versions": { + "20190108-r0": 1548779239 + }, + "origin": "ca-certificates" + }, + "collectd-ascent": { + "versions": { + "5.8.0-r3": 1546422677 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libxml2.so.2" + ] + }, + "spice-server": { + "versions": { + "0.14.1-r6": 1548919556 + }, + "origin": "spice", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcelt051.so.0", + "so:libcrypto.so.1.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libjpeg.so.8", + "so:libopus.so.0", + "so:libpixman-1.so.0", + "so:libsasl2.so.3", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libspice-server.so.1=1.12.5" + ] + }, + "tcpdump": { + "versions": { + "4.9.2-r4": 1545300929 + }, + "origin": "tcpdump", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:tcpdump" + ] + }, + "encfs": { + "versions": { + "1.9.5-r3": 1545061083 + }, + "origin": "encfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libfuse.so.2", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libtinyxml2.so.7" + ], + "provides": [ + "so:libencfs.so.1.9=1.9.5", + "cmd:encfs", + "cmd:encfsctl", + "cmd:encfssh" + ] + }, + "xrdp-openrc": { + "versions": { + "0.9.9-r0": 1545859298 + }, + "origin": "xrdp" + }, + "multipath-tools": { + "versions": { + "0.7.9-r0": 1545224103 + }, + "origin": "multipath-tools", + "dependencies": [ + "eudev", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02", + "so:libjson-c.so.4", + "so:libreadline.so.7", + "so:libudev.so.1", + "so:liburcu.so.6" + ], + "provides": [ + "so:libdmmp.so.0.2.0=0.2.0", + "so:libmpathcmd.so.0=0", + "so:libmpathpersist.so.0=0", + "so:libmultipath.so.0=0", + "cmd:kpartx", + "cmd:mpathpersist", + "cmd:multipath", + "cmd:multipathd" + ] + }, + "libvorbis-dev": { + "versions": { + "1.3.6-r2": 1549615841 + }, + "origin": "libvorbis", + "dependencies": [ + "libvorbis=1.3.6-r2", + "pc:ogg", + "pkgconfig" + ], + "provides": [ + "pc:vorbis=1.3.6", + "pc:vorbisenc=1.3.6", + "pc:vorbisfile=1.3.6" + ] + }, + "libconfig-dev": { + "versions": { + "1.5-r3": 1543249699 + }, + "origin": "libconfig", + "dependencies": [ + "libconfig++=1.5-r3", + "libconfig=1.5-r3", + "pkgconfig" + ], + "provides": [ + "pc:libconfig++=1.5", + "pc:libconfig=1.5" + ] + }, + "curl-doc": { + "versions": { + "7.64.0-r1": 1551205311 + }, + "origin": "curl" + }, + "perl-xml-parser-doc": { + "versions": { + "2.44-r4": 1542821909 + }, + "origin": "perl-xml-parser" + }, + "dejagnu-dev": { + "versions": { + "1.6.2-r0": 1545820393 + }, + "origin": "dejagnu" + }, + "libsrtp-dev": { + "versions": { + "1.5.4-r1": 1544000953 + }, + "origin": "libsrtp", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:libsrtp=1.5.4" + ] + }, + "giblib-doc": { + "versions": { + "1.2.4-r10": 1543226558 + }, + "origin": "giblib" + }, + "graphite2-dev": { + "versions": { + "1.3.12-r1": 1542823993 + }, + "origin": "graphite2", + "dependencies": [ + "freetype-dev", + "graphite2=1.3.12-r1", + "pkgconfig" + ], + "provides": [ + "pc:graphite2=3.0.1" + ] + }, + "libid3tag-dev": { + "versions": { + "0.15.1b-r7": 1542972923 + }, + "origin": "libid3tag", + "dependencies": [ + "libid3tag=0.15.1b-r7", + "pkgconfig" + ], + "provides": [ + "pc:id3tag=0.15.1b" + ] + }, + "mcookie": { + "versions": { + "2.33-r0": 1545307437 + }, + "origin": "util-linux", + "dependencies": [ + "findmnt", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mcookie" + ] + }, + "gpsd": { + "versions": { + "3.18.1-r1": 1549881588 + }, + "origin": "gpsd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgps.so.24=24.0.0", + "cmd:gpsd", + "cmd:gpsdctl" + ] + }, + "llvm5-dev": { + "versions": { + "5.0.2-r0": 1546874980 + }, + "origin": "llvm5", + "dependencies": [ + "llvm5=5.0.2-r0", + "llvm5-libs=5.0.2-r0", + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "llvm-dev=5.0.2-r0", + "cmd:llvm-config" + ] + }, + "linux-firmware-amdgpu": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "curl": { + "versions": { + "7.64.0-r1": 1551205311 + }, + "origin": "curl", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libz.so.1" + ], + "provides": [ + "cmd:curl" + ] + }, + "libdbi": { + "versions": { + "0.9.0-r0": 1543932516 + }, + "origin": "libdbi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdbi.so.1=1.1.0" + ] + }, + "wv-doc": { + "versions": { + "1.2.9-r3": 1545300663 + }, + "origin": "wv" + }, + "iniparser": { + "versions": { + "4.1-r0": 1543928460 + }, + "origin": "iniparser", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libiniparser.so.1=1" + ] + }, + "ttf-droid-nonlatin": { + "versions": { + "20121017-r0": 1545300914 + }, + "origin": "ttf-droid", + "dependencies": [ + "fontconfig" + ] + }, + "xf86-video-i740": { + "versions": { + "1.3.6-r3": 1545299627 + }, + "origin": "xf86-video-i740", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gtksourceview2-doc": { + "versions": { + "2.10.5-r0": 1545292664 + }, + "origin": "gtksourceview2" + }, + "keyutils-dev": { + "versions": { + "1.6-r0": 1545837151 + }, + "origin": "keyutils", + "dependencies": [ + "keyutils-libs=1.6-r0" + ] + }, + "git-cvs": { + "versions": { + "2.20.1-r0": 1545214195 + }, + "origin": "git", + "dependencies": [ + "perl", + "perl-git=2.20.1-r0", + "cvs", + "perl-dbd-sqlite" + ], + "provides": [ + "cmd:git-cvsserver" + ] + }, + "daq": { + "versions": { + "2.0.6-r2": 1545163827 + }, + "origin": "daq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "so:libdaq.so.2=2.0.4", + "so:libsfbpf.so.0=0.0.1" + ] + }, + "librsvg": { + "versions": { + "2.40.20-r0": 1543926596 + }, + "origin": "librsvg", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcroco-0.6.so.3", + "so:libfontconfig.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:librsvg-2.so.2=2.40.20", + "cmd:rsvg-convert" + ] + }, + "font-isas-misc": { + "versions": { + "1.0.3-r0": 1545163915 + }, + "origin": "font-isas-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "libraw1394": { + "versions": { + "2.1.2-r1": 1543932099 + }, + "origin": "libraw1394", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libraw1394.so.11=11.1.0" + ] + }, + "taglib": { + "versions": { + "1.11.1-r1": 1545062765 + }, + "origin": "taglib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libtag.so.1=1.17.0", + "so:libtag_c.so.0=0.0.0" + ] + }, + "py3-phonenumbers": { + "versions": { + "8.10.4-r0": 1548490639 + }, + "origin": "py-phonenumbers", + "dependencies": [ + "python3" + ] + }, + "xkbcomp-doc": { + "versions": { + "1.4.2-r0": 1542973208 + }, + "origin": "xkbcomp" + }, + "lua-dns": { + "versions": { + "20080404-r2": 1545292670 + }, + "origin": "lua-dns", + "dependencies": [ + "lua", + "lua-socket" + ] + }, + "xf86-video-sunleo-doc": { + "versions": { + "1.2.2-r3": 1545293001 + }, + "origin": "xf86-video-sunleo" + }, + "wireless-regdb": { + "versions": { + "2018.05.31-r1": 1550837521 + }, + "origin": "wireless-regdb" + }, + "grub-doc": { + "versions": { + "2.02-r14": 1548432370 + }, + "origin": "grub" + }, + "boost-wave": { + "versions": { + "1.67.0-r2": 1542823634 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.67.0", + "so:libboost_thread-mt.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_wave-mt.so.1.67.0=1.67.0" + ] + }, + "busybox-static": { + "versions": { + "1.29.3-r10": 1548315956 + }, + "origin": "busybox", + "provides": [ + "cmd:busybox.static" + ] + }, + "sqlite-libs": { + "versions": { + "3.26.0-r3": 1546255353 + }, + "origin": "sqlite", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsqlite3.so.0=0.8.6" + ] + }, + "py2-flask": { + "versions": { + "1.0.2-r1": 1546978284 + }, + "origin": "py-flask", + "dependencies": [ + "py2-click", + "py2-itsdangerous", + "py2-jinja2", + "py2-werkzeug", + "python2" + ], + "provides": [ + "cmd:flask" + ] + }, + "qemu-nios2": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-nios2" + ] + }, + "ruby-fiddle": { + "versions": { + "2.5.5-r0": 1557164836 + }, + "origin": "ruby", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libruby.so.2.5" + ] + }, + "libgee": { + "versions": { + "0.20.1-r0": 1545060547 + }, + "origin": "libgee", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libgee-0.8.so.2=2.6.1" + ] + }, + "py-parsing": { + "versions": { + "2.2.0-r0": 1542825051 + }, + "origin": "py-parsing" + }, + "spawn-fcgi": { + "versions": { + "1.6.4-r3": 1542821586 + }, + "origin": "spawn-fcgi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:spawn-fcgi" + ] + }, + "perl-test-without": { + "versions": { + "0.10-r0": 1545223243 + }, + "origin": "perl-test-without" + }, + "libotr-doc": { + "versions": { + "4.1.1-r1": 1543248414 + }, + "origin": "libotr" + }, + "rhash-doc": { + "versions": { + "1.3.6-r2": 1542820448 + }, + "origin": "rhash" + }, + "unbound-dev": { + "versions": { + "1.8.3-r1": 1555953583 + }, + "origin": "unbound", + "dependencies": [ + "expat-dev", + "pc:libcrypto", + "pc:libevent", + "pc:libssl", + "pc:python2", + "pkgconfig", + "unbound-libs=1.8.3-r1" + ], + "provides": [ + "pc:libunbound=1.8.3" + ] + }, + "brlaser-doc": { + "versions": { + "3-r0": 1545214281 + }, + "origin": "brlaser" + }, + "bcache-tools": { + "versions": { + "1.0.8-r1": 1545060583 + }, + "origin": "bcache-tools", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:bcache-super-show", + "cmd:make-bcache" + ] + }, + "dhcpcd-dbus": { + "versions": { + "0.6.1-r3": 1543223071 + }, + "origin": "dhcpcd-dbus", + "dependencies": [ + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ] + }, + "libxinerama-doc": { + "versions": { + "1.1.4-r1": 1543077318 + }, + "origin": "libxinerama" + }, + "which-doc": { + "versions": { + "2.21-r1": 1543929034 + }, + "origin": "which" + }, + "keybinder": { + "versions": { + "0.3.0-r1": 1545076730 + }, + "origin": "keybinder", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ], + "provides": [ + "so:libkeybinder.so.0=0.1.0" + ] + }, + "perl-file-copy-recursive": { + "versions": { + "0.44-r0": 1543238907 + }, + "origin": "perl-file-copy-recursive" + }, + "libvpx": { + "versions": { + "1.6.1-r1": 1545163814 + }, + "origin": "libvpx", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libvpx.so.4=4.1.0" + ] + }, + "hiredis-dev": { + "versions": { + "0.14.0-r0": 1545837197 + }, + "origin": "hiredis", + "dependencies": [ + "hiredis=0.14.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:hiredis=0.14.0" + ] + }, + "open-lldp-dev": { + "versions": { + "0.9.46-r3": 1545062127 + }, + "origin": "open-lldp", + "dependencies": [ + "open-lldp=0.9.46-r3", + "pkgconfig" + ], + "provides": [ + "pc:liblldp_clif=1.0.0", + "pc:lldpad=0.9.46" + ] + }, + "xf86-video-nouveau-doc": { + "versions": { + "1.0.15-r3": 1545299946 + }, + "origin": "xf86-video-nouveau" + }, + "valgrind-dev": { + "versions": { + "3.14.0-r0": 1545300865 + }, + "origin": "valgrind", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:valgrind=3.14.0" + ] + }, + "acf-opennhrp": { + "versions": { + "0.10.0-r2": 1545069407 + }, + "origin": "acf-opennhrp", + "dependencies": [ + "acf-core", + "lua-posix", + "opennhrp" + ] + }, + "lua5.3-posix": { + "versions": { + "33.4.0-r1": 1546010825 + }, + "origin": "lua-posix", + "dependencies": [ + "lua5.3-bit32<26", + "so:libc.musl-x86_64.so.1" + ] + }, + "py3-pbr": { + "versions": { + "5.1.1-r0": 1545235280 + }, + "origin": "py-pbr", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:pbr" + ] + }, + "subversion-bash-completion": { + "versions": { + "1.11.1-r0": 1548692375 + }, + "origin": "subversion" + }, + "bluez-deprecated": { + "versions": { + "5.50-r0": 1545822205 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libreadline.so.7", + "so:libudev.so.1" + ], + "provides": [ + "cmd:ciptool", + "cmd:gatttool", + "cmd:hciattach", + "cmd:hciconfig", + "cmd:hcidump", + "cmd:hcitool", + "cmd:rfcomm", + "cmd:sdptool" + ] + }, + "perl-protocol-websocket": { + "versions": { + "0.20-r0": 1545164022 + }, + "origin": "perl-protocol-websocket", + "dependencies": [ + "perl" + ] + }, + "lua5.2-file-magic": { + "versions": { + "0.2-r1": 1545116947 + }, + "origin": "lua-file-magic", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libmagic.so.1" + ] + }, + "jansson": { + "versions": { + "2.11-r0": 1542893195 + }, + "origin": "jansson", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libjansson.so.4=4.11.0" + ] + }, + "libunwind-dbg": { + "versions": { + "1.2.1-r3": 1545915765 + }, + "origin": "libunwind" + }, + "postgresql-dev": { + "versions": { + "11.2-r0": 1554274172 + }, + "origin": "postgresql", + "dependencies": [ + "openssl-dev", + "libpq=11.2-r0", + "pkgconfig", + "postgresql-libs=11.2-r0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "pc:libecpg=11.2", + "pc:libecpg_compat=11.2", + "pc:libpgtypes=11.2", + "pc:libpq=11.2", + "cmd:ecpg", + "cmd:pg_config" + ] + }, + "procmail-doc": { + "versions": { + "3.22-r4": 1545299910 + }, + "origin": "procmail" + }, + "py-cparser": { + "versions": { + "2.19-r0": 1546791397 + }, + "origin": "py-cparser" + }, + "openldap-dev": { + "versions": { + "2.4.47-r2": 1546016475 + }, + "origin": "openldap", + "dependencies": [ + "openssl-dev", + "cyrus-sasl-dev", + "util-linux-dev", + "libldap=2.4.47-r2" + ] + }, + "bc": { + "versions": { + "1.07.1-r0": 1542823693 + }, + "origin": "bc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:bc", + "cmd:dc" + ] + }, + "uwsgi-dummy": { + "versions": { + "2.0.17.1-r0": 1545062198 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "zlib-doc": { + "versions": { + "1.2.11-r1": 1542300122 + }, + "origin": "zlib" + }, + "libsm": { + "versions": { + "1.2.2-r2": 1542824342 + }, + "origin": "libsm", + "dependencies": [ + "so:libICE.so.6", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libSM.so.6=6.0.1" + ] + }, + "perl-mime-tools": { + "versions": { + "5.509-r1": 1544001507 + }, + "origin": "perl-mime-tools", + "dependencies": [ + "perl", + "perl-io-stringy", + "perl-mailtools", + "perl-convert-binhex" + ] + }, + "qt-assistant": { + "versions": { + "4.8.7-r11": 1545075086 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtGui.so.4", + "so:libQtHelp.so.4", + "so:libQtNetwork.so.4", + "so:libQtSql.so.4", + "so:libQtWebKit.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:assistant" + ] + }, + "ivykis-doc": { + "versions": { + "0.42.3-r0": 1548543106 + }, + "origin": "ivykis" + }, + "gpicview": { + "versions": { + "0.2.5-r0": 1543932505 + }, + "origin": "gpicview", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8" + ], + "provides": [ + "cmd:gpicview" + ] + }, + "memcached-doc": { + "versions": { + "1.5.12-r0": 1543923566 + }, + "origin": "memcached" + }, + "py-wtforms": { + "versions": { + "2.1-r0": 1544000626 + }, + "origin": "py-wtforms" + }, + "util-linux-bash-completion": { + "versions": { + "2.33-r0": 1545307436 + }, + "origin": "util-linux" + }, + "tolua++": { + "versions": { + "1.0.93-r2": 1545209663 + }, + "origin": "tolua++", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ], + "provides": [ + "cmd:tolua++" + ] + }, + "py-django-widget-tweaks": { + "versions": { + "1.4.3-r0": 1545163699 + }, + "origin": "py-django-widget-tweaks", + "dependencies": [ + "py-django" + ] + }, + "tinyproxy": { + "versions": { + "1.10.0-r1": 1551109287 + }, + "origin": "tinyproxy", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tinyproxy" + ] + }, + "aconf": { + "versions": { + "0.7.1-r0": 1553432498 + }, + "origin": "aconf", + "dependencies": [ + "lua5.3-augeas", + "lua5.3-b64", + "lua5.3-cjson", + "lua5.3-file-magic", + "lua5.3-openrc", + "lua5.3-ossl", + "lua5.3-posix", + "lua5.3-stringy", + "uwsgi", + "uwsgi-lua" + ], + "provides": [ + "cmd:aconfd" + ] + }, + "tcl-dev": { + "versions": { + "8.6.9-r0": 1545915062 + }, + "origin": "tcl", + "dependencies": [ + "tcl", + "pc:zlib>=1.2.3", + "pkgconfig" + ], + "provides": [ + "pc:tcl=8.6.9" + ] + }, + "collectd-network": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20" + ] + }, + "alsa-lib": { + "versions": { + "1.1.8-r0": 1547112801 + }, + "origin": "alsa-lib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libasound.so.2=2.0.0", + "cmd:aserver" + ] + }, + "ruby-full": { + "versions": { + "2.5.5-r0": 1557164838 + }, + "origin": "ruby", + "dependencies": [ + "ruby", + "ruby-bigdecimal", + "ruby-dbm", + "ruby-did_you_mean", + "ruby-etc", + "ruby-fiddle", + "ruby-gdbm", + "ruby-io-console", + "ruby-irb", + "ruby-json", + "ruby-minitest", + "ruby-net-telnet", + "ruby-power_assert", + "ruby-rake", + "ruby-rdoc", + "ruby-sdbm", + "ruby-test-unit", + "ruby-webrick", + "ruby-xmlrpc" + ] + }, + "sipsak-doc": { + "versions": { + "0.9.6-r11": 1543077292 + }, + "origin": "sipsak" + }, + "linux-firmware-acenic": { + "versions": { + "20190322-r0": 1554980655 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "ngrep-doc": { + "versions": { + "1.45-r4": 1543242183 + }, + "origin": "ngrep" + }, + "perl-dbix-dbschema-doc": { + "versions": { + "0.39-r0": 1543998794 + }, + "origin": "perl-dbix-dbschema" + }, + "sessreg-doc": { + "versions": { + "1.1.1-r1": 1542985395 + }, + "origin": "sessreg" + }, + "qemu-ppc64": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-ppc64" + ] + }, + "net-snmp": { + "versions": { + "5.8-r0": 1543923351 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.35", + "so:libnetsnmpagent.so.35", + "so:libnetsnmpmibs.so.35", + "so:libnetsnmptrapd.so.35" + ], + "provides": [ + "cmd:snmpd", + "cmd:snmptrapd" + ] + }, + "hwdata": { + "versions": { + "0.318-r0": 1545746063 + }, + "origin": "hwdata", + "dependencies": [ + "hwdata-usb", + "hwdata-pci", + "hwdata-pnp", + "hwdata-oui" + ] + }, + "ntop-doc": { + "versions": { + "5.0.1-r14": 1545209735 + }, + "origin": "ntop" + }, + "nodejs-dev": { + "versions": { + "10.14.2-r0": 1545345076 + }, + "origin": "nodejs", + "dependencies": [ + "libuv" + ], + "provides": [ + "nodejs-lts-dev=10.14.2" + ] + }, + "perl-namespace-clean": { + "versions": { + "0.27-r0": 1542973127 + }, + "origin": "perl-namespace-clean", + "dependencies": [ + "perl-package-stash", + "perl-sub-name", + "perl-sub-identify", + "perl-b-hooks-endofscope" + ] + }, + "py3-sphinx": { + "versions": { + "1.8.3-r0": 1548111931 + }, + "origin": "py-sphinx", + "dependencies": [ + "make", + "py3-docutils", + "py3-jinja2", + "py3-pygments", + "py3-six", + "py3-sphinx_rtd_theme", + "py3-alabaster<0.8", + "py3-babel", + "py3-snowballstemmer", + "py3-imagesize", + "py3-requests", + "py3-sphinxcontrib-websupport", + "py3-setuptools", + "py3-packaging", + "python3" + ], + "provides": [ + "cmd:sphinx-apidoc-3", + "cmd:sphinx-autogen-3", + "cmd:sphinx-build-3", + "cmd:sphinx-quickstart-3" + ] + }, + "slang-doc": { + "versions": { + "2.3.2-r0": 1543924689 + }, + "origin": "slang" + }, + "lua-hashids": { + "versions": { + "1.0.6-r1": 1545209085 + }, + "origin": "lua-hashids" + }, + "gnome-mime-data": { + "versions": { + "2.18.0-r1": 1543934222 + }, + "origin": "gnome-mime-data" + }, + "perl-css-squish-doc": { + "versions": { + "0.10-r0": 1544000383 + }, + "origin": "perl-css-squish" + }, + "cups-filters": { + "versions": { + "1.21.6-r0": 1545820385 + }, + "origin": "cups-filters", + "dependencies": [ + "poppler-utils", + "bc", + "ttf-freefont", + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-glib.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libcupsfilters.so.1", + "so:libcupsimage.so.2", + "so:libfontconfig.so.1", + "so:libfontembed.so.1", + "so:libgcc_s.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:liblcms2.so.2", + "so:libpoppler.so.67", + "so:libqpdf.so.21", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:cups-browsed", + "cmd:driverless", + "cmd:foomatic-rip", + "cmd:ttfread" + ] + }, + "alsa-utils-dbg": { + "versions": { + "1.1.8-r0": 1547112822 + }, + "origin": "alsa-utils", + "dependencies": [ + "dialog" + ] + }, + "perl-config-inifiles-doc": { + "versions": { + "3.000001-r0": 1547708571 + }, + "origin": "perl-config-inifiles" + }, + "libdv-dev": { + "versions": { + "1.0.0-r3": 1543999340 + }, + "origin": "libdv", + "dependencies": [ + "libdv=1.0.0-r3", + "pkgconfig" + ], + "provides": [ + "pc:libdv=1.0.0" + ] + }, + "git-diff-highlight": { + "versions": { + "2.20.1-r0": 1545214200 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0", + "perl" + ], + "provides": [ + "cmd:diff-highlight" + ] + }, + "perl-datetime-timezone-doc": { + "versions": { + "2.19-r0": 1542973145 + }, + "origin": "perl-datetime-timezone" + }, + "tdb-dev": { + "versions": { + "1.3.16-r0": 1543933253 + }, + "origin": "tdb", + "dependencies": [ + "python2", + "pkgconfig", + "tdb-libs=1.3.16-r0" + ], + "provides": [ + "pc:tdb=1.3.16" + ] + }, + "db-c++": { + "versions": { + "5.3.28-r1": 1542819053 + }, + "origin": "db", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libdb_cxx-5.3.so=0" + ] + }, + "libexif": { + "versions": { + "0.6.21-r3": 1545116759 + }, + "origin": "libexif", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libexif.so.12=12.3.3" + ] + }, + "libpcre2-16": { + "versions": { + "10.32-r1": 1545069083 + }, + "origin": "pcre2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre2-16.so.0=0.7.1" + ] + }, + "xf86-video-rendition-doc": { + "versions": { + "4.2.7-r0": 1544000925 + }, + "origin": "xf86-video-rendition" + }, + "libnetfilter_conntrack-dev": { + "versions": { + "1.0.6-r0": 1545165062 + }, + "origin": "libnetfilter_conntrack", + "dependencies": [ + "libnetfilter_conntrack=1.0.6-r0", + "pc:libnfnetlink", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_conntrack=1.0.6" + ] + }, + "perl-canary-stability-doc": { + "versions": { + "2012-r0": 1543223266 + }, + "origin": "perl-canary-stability" + }, + "squid-lang-es": { + "versions": { + "4.4-r1": 1545216324 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libcdio++": { + "versions": { + "0.94-r2": 1543248377 + }, + "origin": "libcdio", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcdio.so.16", + "so:libiso9660.so.10", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcdio++.so.0=0.0.2", + "so:libiso9660++.so.0=0.0.0" + ] + }, + "libao-dev": { + "versions": { + "1.2.0-r3": 1545067193 + }, + "origin": "libao", + "dependencies": [ + "libao=1.2.0-r3", + "pkgconfig" + ], + "provides": [ + "pc:ao=1.2.0" + ] + }, + "py3-redis": { + "versions": { + "2.10.6-r0": 1545301345 + }, + "origin": "py-redis", + "dependencies": [ + "python3" + ] + }, + "openbox-lang": { + "versions": { + "3.6.1-r2": 1545207329 + }, + "origin": "openbox" + }, + "lua-stdlib-debug": { + "versions": { + "1.0.1-r0": 1545223086 + }, + "origin": "lua-stdlib-debug", + "dependencies": [ + "lua" + ], + "provides": [ + "lua5.1-stdlib-debug=1.0.1-r0", + "lua5.2-stdlib-debug=1.0.1-r0", + "lua5.3-stdlib-debug=1.0.1-r0" + ] + }, + "gdl-doc": { + "versions": { + "3.28.0-r0": 1545215937 + }, + "origin": "gdl" + }, + "py-cairo": { + "versions": { + "1.16.3-r0": 1543925202 + }, + "origin": "py-cairo" + }, + "db": { + "versions": { + "5.3.28-r1": 1542819053 + }, + "origin": "db", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdb-5.3.so=0" + ] + }, + "mercurial-doc": { + "versions": { + "4.9.1-r0": 1557160461 + }, + "origin": "mercurial" + }, + "py2-vobject": { + "versions": { + "0.9.6.1-r0": 1545165275 + }, + "origin": "py-vobject", + "dependencies": [ + "python2", + "py2-icu", + "py2-dateutil" + ] + }, + "lua5.3-ldap": { + "versions": { + "1.2.3-r5": 1543223253 + }, + "origin": "lua-ldap", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "lua5.3-curl": { + "versions": { + "0.3.8-r1": 1545224117 + }, + "origin": "lua-curl", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "uwsgi-rpc": { + "versions": { + "2.0.17.1-r0": 1545062209 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "s6-portable-utils": { + "versions": { + "2.2.1.2-r0": 1545069442 + }, + "origin": "s6-portable-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.7" + ], + "provides": [ + "cmd:s6-basename", + "cmd:s6-cat", + "cmd:s6-chmod", + "cmd:s6-chown", + "cmd:s6-clock", + "cmd:s6-cut", + "cmd:s6-dirname", + "cmd:s6-dumpenv", + "cmd:s6-echo", + "cmd:s6-env", + "cmd:s6-expr", + "cmd:s6-false", + "cmd:s6-format-filter", + "cmd:s6-grep", + "cmd:s6-head", + "cmd:s6-hiercopy", + "cmd:s6-linkname", + "cmd:s6-ln", + "cmd:s6-ls", + "cmd:s6-maximumtime", + "cmd:s6-mkdir", + "cmd:s6-mkfifo", + "cmd:s6-nice", + "cmd:s6-nuke", + "cmd:s6-pause", + "cmd:s6-printenv", + "cmd:s6-quote", + "cmd:s6-quote-filter", + "cmd:s6-rename", + "cmd:s6-rmrf", + "cmd:s6-seq", + "cmd:s6-sleep", + "cmd:s6-sort", + "cmd:s6-sync", + "cmd:s6-tail", + "cmd:s6-test", + "cmd:s6-touch", + "cmd:s6-true", + "cmd:s6-uniquename", + "cmd:s6-unquote", + "cmd:s6-unquote-filter", + "cmd:s6-update-symlinks", + "cmd:seekablepipe" + ] + }, + "perl-test-without-module": { + "versions": { + "0.20-r0": 1542893310 + }, + "origin": "perl-test-without-module" + }, + "lua5.2-ldbus": { + "versions": { + "20150430-r2": 1545300022 + }, + "origin": "lua-ldbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ] + }, + "lua-sec": { + "versions": { + "0.7-r1": 1545075389 + }, + "origin": "lua-sec" + }, + "perl-data-optlist": { + "versions": { + "0.110-r0": 1542883240 + }, + "origin": "perl-data-optlist", + "dependencies": [ + "perl", + "perl-params-util", + "perl-sub-install" + ] + }, + "thin-provisioning-tools": { + "versions": { + "0.7.1-r1": 1545208657 + }, + "origin": "thin-provisioning-tools", + "dependencies": [ + "expat", + "boost", + "libaio", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:cache_check", + "cmd:cache_dump", + "cmd:cache_metadata_size", + "cmd:cache_repair", + "cmd:cache_restore", + "cmd:cache_writeback", + "cmd:era_check", + "cmd:era_dump", + "cmd:era_invalidate", + "cmd:era_restore", + "cmd:pdata_tools", + "cmd:thin_check", + "cmd:thin_delta", + "cmd:thin_dump", + "cmd:thin_ls", + "cmd:thin_metadata_size", + "cmd:thin_repair", + "cmd:thin_restore", + "cmd:thin_rmap" + ] + }, + "ppp-passprompt": { + "versions": { + "2.4.7-r6": 1543999022 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-io-html": { + "versions": { + "1.001-r1": 1542821791 + }, + "origin": "perl-io-html" + }, + "libcddb": { + "versions": { + "1.3.2-r3": 1543248357 + }, + "origin": "libcddb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcddb.so.2=2.2.3", + "cmd:cddb_query" + ] + }, + "libnice-gstreamer0.10": { + "versions": { + "0.1.14-r2": 1543928835 + }, + "origin": "libnice", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libnice.so.10" + ] + }, + "py-psycopg2": { + "versions": { + "2.7.5-r0": 1544000431 + }, + "origin": "py-psycopg2" + }, + "linux-firmware-bnx2x": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "py-flask-script": { + "versions": { + "2.0.6-r0": 1545207605 + }, + "origin": "py-flask-script", + "dependencies": [ + "python2", + "py-flask" + ] + }, + "dpkg": { + "versions": { + "1.19.2-r0": 1545820223 + }, + "origin": "dpkg", + "dependencies": [ + "xz", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:dpkg", + "cmd:dpkg-buildflags", + "cmd:dpkg-deb", + "cmd:dpkg-divert", + "cmd:dpkg-genbuildinfo", + "cmd:dpkg-maintscript-helper", + "cmd:dpkg-mergechangelogs", + "cmd:dpkg-parsechangelog", + "cmd:dpkg-query", + "cmd:dpkg-split", + "cmd:dpkg-statoverride", + "cmd:dpkg-trigger", + "cmd:update-alternatives" + ] + }, + "openresolv": { + "versions": { + "3.9.0-r0": 1545116956 + }, + "origin": "openresolv", + "provides": [ + "cmd:resolvconf" + ] + }, + "rsyslog-crypto": { + "versions": { + "8.40.0-r3": 1548686789 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20" + ], + "provides": [ + "rsyslog-lmcry_gcry=8.40.0-r3" + ] + }, + "djbdns-doc": { + "versions": { + "1.05-r47": 1545208951 + }, + "origin": "djbdns" + }, + "haserl-lua5.2": { + "versions": { + "0.9.35-r1": 1542883797 + }, + "origin": "haserl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua-5.2.so.0" + ], + "provides": [ + "cmd:haserl-lua5.2" + ] + }, + "pkgconf-dev": { + "versions": { + "1.6.0-r0": 1547496958 + }, + "origin": "pkgconf", + "dependencies": [ + "pkgconf=1.6.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libpkgconf=1.6.0" + ] + }, + "grub-xenhost": { + "versions": { + "2.02-r14": 1548432370 + }, + "origin": "grub", + "dependencies": [ + "mkinitfs" + ] + }, + "perl-data-page-pageset": { + "versions": { + "1.02-r1": 1545213964 + }, + "origin": "perl-data-page-pageset", + "dependencies": [ + "perl-data-page", + "perl-test-exception", + "perl-class-accessor" + ] + }, + "libxslt-doc": { + "versions": { + "1.1.33-r1": 1555485888 + }, + "origin": "libxslt" + }, + "farstream": { + "versions": { + "0.2.8-r2": 1543928859 + }, + "origin": "farstream", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstnet-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgstrtp-1.0.so.0", + "so:libnice.so.10" + ], + "provides": [ + "so:libfarstream-0.2.so.5=5.1.0" + ] + }, + "freeswitch-timezones": { + "versions": { + "1.8.2-r1": 1545117892 + }, + "origin": "freeswitch" + }, + "subunit-libs": { + "versions": { + "1.2.0-r0": 1543933435 + }, + "origin": "subunit", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcppunit_subunit.so.0=0.0.0", + "so:libsubunit.so.0=0.0.0" + ] + }, + "lua5.1-subprocess": { + "versions": { + "0.0.20141229-r2": 1542883781 + }, + "origin": "lua-subprocess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-authen-sasl": { + "versions": { + "2.16-r1": 1545215986 + }, + "origin": "perl-authen-sasl", + "dependencies": [ + "perl", + "perl-digest-hmac" + ] + }, + "perl-class-load-doc": { + "versions": { + "0.25-r0": 1543226708 + }, + "origin": "perl-class-load" + }, + "samba-winbind": { + "versions": { + "4.8.11-r1": 1555334973 + }, + "origin": "samba", + "dependencies": [ + "samba-common=4.8.11-r1", + "so:libMESSAGING-samba4.so", + "so:libads-samba4.so", + "so:libasn1util-samba4.so", + "so:libauth-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-ldap-common-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libdcerpc-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libidmap-samba4.so", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldap-2.4.so.2", + "so:liblibcli-lsa3-samba4.so", + "so:liblibcli-netlogon3-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmsrpc3-samba4.so", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnpa-tstream-samba4.so", + "so:libnss-info-samba4.so", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb-common-samba4.so", + "so:libsecrets3-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-shim-samba4.so", + "so:libsmbldap.so.2", + "so:libsocket-blocking-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so", + "so:libtrusts-util-samba4.so", + "so:libutil-tdb-samba4.so" + ], + "provides": [ + "cmd:winbindd" + ] + }, + "sysfsutils-doc": { + "versions": { + "2.1.0-r8": 1542924684 + }, + "origin": "sysfsutils" + }, + "gmp-dev": { + "versions": { + "6.1.2-r1": 1542301440 + }, + "origin": "gmp", + "dependencies": [ + "gmp=6.1.2-r1", + "libgmpxx=6.1.2-r1" + ] + }, + "sdl2_image-dev": { + "versions": { + "2.0.4-r0": 1545062544 + }, + "origin": "sdl2_image", + "dependencies": [ + "pc:sdl2>=2.0.8", + "pkgconfig", + "sdl2_image=2.0.4-r0" + ], + "provides": [ + "pc:SDL2_image=2.0.4" + ] + }, + "xf86-video-ast-doc": { + "versions": { + "1.1.5-r4": 1545300633 + }, + "origin": "xf86-video-ast" + }, + "uwsgi-gevent": { + "versions": { + "2.0.17.1-r0": 1545062200 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libnetfilter_cthelper": { + "versions": { + "1.0.0-r0": 1545073849 + }, + "origin": "libnetfilter_cthelper", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0" + ], + "provides": [ + "so:libnetfilter_cthelper.so.0=0.0.0" + ] + }, + "qemu-system-sparc64": { + "versions": { + "3.1.0-r3": 1551107305 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-sparc64" + ] + }, + "perl-net-dns-resolver-mock": { + "versions": { + "1.20170814-r0": 1545061184 + }, + "origin": "perl-net-dns-resolver-mock", + "dependencies": [ + "perl-net-dns" + ] + }, + "py2-asn1crypto": { + "versions": { + "0.24.0-r0": 1543246739 + }, + "origin": "py-asn1crypto", + "dependencies": [ + "python2" + ] + }, + "itstool": { + "versions": { + "2.0.4-r3": 1545062596 + }, + "origin": "itstool", + "dependencies": [ + "py3-libxml2", + "python3" + ], + "provides": [ + "cmd:itstool" + ] + }, + "ddate-doc": { + "versions": { + "0.2.2-r0": 1545207007 + }, + "origin": "ddate" + }, + "apache2-icons": { + "versions": { + "2.4.39-r0": 1554306835 + }, + "origin": "apache2" + }, + "perl-io-compress": { + "versions": { + "2.084-r0": 1546944136 + }, + "origin": "perl-io-compress", + "dependencies": [ + "perl", + "perl-compress-raw-bzip2", + "perl-compress-raw-zlib" + ] + }, + "py3-itsdangerous": { + "versions": { + "0.24-r3": 1545164551 + }, + "origin": "py-itsdangerous", + "dependencies": [ + "python3" + ] + }, + "dmidecode": { + "versions": { + "3.2-r0": 1543921858 + }, + "origin": "dmidecode", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:biosdecode", + "cmd:dmidecode", + "cmd:ownership", + "cmd:vpddecode" + ] + }, + "clang-static": { + "versions": { + "5.0.2-r0": 1546873919 + }, + "origin": "clang" + }, + "heimdal-doc": { + "versions": { + "7.5.0-r2": 1542819372 + }, + "origin": "heimdal" + }, + "lame": { + "versions": { + "3.100-r0": 1545117087 + }, + "origin": "lame", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "so:libmp3lame.so.0=0.0.0", + "cmd:lame" + ] + }, + "alsa-lib-dbg": { + "versions": { + "1.1.8-r0": 1547112801 + }, + "origin": "alsa-lib" + }, + "syslog-ng-stomp": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "py2-funcsigs": { + "versions": { + "1.0.2-r1": 1542824897 + }, + "origin": "py-funcsigs", + "dependencies": [ + "python2" + ] + }, + "lua5.2": { + "versions": { + "5.2.4-r7": 1542304845 + }, + "origin": "lua5.2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua-5.2.so.0" + ], + "provides": [ + "lua", + "cmd:lua5.2", + "cmd:luac5.2" + ] + }, + "xrandr-doc": { + "versions": { + "1.5.0-r1": 1545207350 + }, + "origin": "xrandr" + }, + "aports-build": { + "versions": { + "1.5-r0": 1547042899 + }, + "origin": "aports-build", + "dependencies": [ + "abuild>2.20.0", + "mosquitto-clients", + "openssh-client", + "mqtt-exec", + "rsync", + "lua-aports>1.0.0", + "lua5.2-cjson", + "lua5.2-mqtt-publish", + "pigz", + "/bin/sh" + ], + "provides": [ + "cmd:aports-build" + ] + }, + "fluxbox": { + "versions": { + "1.3.7-r3": 1545165091 + }, + "origin": "fluxbox", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libXpm.so.4", + "so:libXrandr.so.2", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:fbrun", + "cmd:fbsetbg", + "cmd:fbsetroot", + "cmd:fluxbox", + "cmd:fluxbox-generate_menu", + "cmd:fluxbox-remote", + "cmd:fluxbox-update_configs", + "cmd:startfluxbox" + ] + }, + "py-paramiko": { + "versions": { + "2.4.2-r0": 1545216487 + }, + "origin": "py-paramiko", + "dependencies": [ + "py-asn1", + "py-cryptography", + "py-bcrypt", + "py-pynacl" + ] + }, + "linux-firmware-amd-ucode": { + "versions": { + "20190322-r0": 1554980655 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "unixodbc": { + "versions": { + "2.3.7-r0": 1542845421 + }, + "origin": "unixodbc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "so:libodbc.so.2=2.0.0", + "so:libodbccr.so.2=2.0.0", + "so:libodbcinst.so.2=2.0.0", + "cmd:dltest", + "cmd:isql", + "cmd:iusql", + "cmd:odbc_config", + "cmd:odbcinst", + "cmd:slencheck" + ] + }, + "coova-chilli-doc": { + "versions": { + "1.4-r2": 1545163683 + }, + "origin": "coova-chilli" + }, + "libavc1394-doc": { + "versions": { + "0.5.4-r2": 1543932110 + }, + "origin": "libavc1394" + }, + "lua-alt-getopt": { + "versions": { + "0.7-r8": 1544799014 + }, + "origin": "lua-alt-getopt" + }, + "libnetfilter_cttimeout-dev": { + "versions": { + "1.0.0-r0": 1542883681 + }, + "origin": "libnetfilter_cttimeout", + "dependencies": [ + "libmnl-dev", + "libnetfilter_cttimeout=1.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_cttimeout=1.0.0" + ] + }, + "avahi-doc": { + "versions": { + "0.7-r1": 1543925309 + }, + "origin": "avahi" + }, + "py2-rsa": { + "versions": { + "3.4.2-r2": 1545116596 + }, + "origin": "py-rsa", + "dependencies": [ + "py2-asn1", + "python2" + ], + "provides": [ + "cmd:pyrsa-decrypt", + "cmd:pyrsa-decrypt-bigfile", + "cmd:pyrsa-encrypt", + "cmd:pyrsa-encrypt-bigfile", + "cmd:pyrsa-keygen", + "cmd:pyrsa-priv2pub", + "cmd:pyrsa-sign", + "cmd:pyrsa-verify" + ] + }, + "rtnppd-dbg": { + "versions": { + "1.7b-r8": 1543935345 + }, + "origin": "rtnppd" + }, + "font-bitstream-speedo": { + "versions": { + "1.0.2-r0": 1543932490 + }, + "origin": "font-bitstream-speedo", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-php-serialization-doc": { + "versions": { + "0.34-r1": 1545214145 + }, + "origin": "perl-php-serialization" + }, + "perl-gdgraph": { + "versions": { + "1.54-r0": 1545060789 + }, + "origin": "perl-gdgraph", + "dependencies": [ + "perl-gdtextutil", + "perl-gd" + ] + }, + "nfs-utils-openrc": { + "versions": { + "2.3.2-r1": 1543933422 + }, + "origin": "nfs-utils", + "dependencies": [ + "rpcbind" + ] + }, + "gdl-dev": { + "versions": { + "3.28.0-r0": 1545215936 + }, + "origin": "gdl", + "dependencies": [ + "gdl=3.28.0-r0", + "pc:gtk+-3.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gdl-3.0=3.28.0" + ] + }, + "opensmtpd": { + "versions": { + "6.0.3p1-r3": 1554301264 + }, + "origin": "opensmtpd", + "dependencies": [ + "!postfix", + "/bin/sh", + "so:libasr.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libdb-5.3.so", + "so:libevent-2.1.so.6", + "so:libfts.so.0", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:smtpctl", + "cmd:smtpd" + ] + }, + "libart-lgpl-dev": { + "versions": { + "2.3.21-r5": 1542924746 + }, + "origin": "libart-lgpl", + "dependencies": [ + "libart-lgpl=2.3.21-r5", + "pkgconfig" + ], + "provides": [ + "pc:libart-2.0=2.3.21", + "cmd:libart2-config" + ] + }, + "uwsgi-logcrypto": { + "versions": { + "2.0.17.1-r0": 1545062201 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "jasper": { + "versions": { + "2.0.14-r0": 1543932348 + }, + "origin": "jasper", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjasper.so.4" + ], + "provides": [ + "cmd:imgcmp", + "cmd:imginfo", + "cmd:jasper" + ] + }, + "qemu-system-alpha": { + "versions": { + "3.1.0-r3": 1551107302 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-alpha" + ] + }, + "freeradius-ldap": { + "versions": { + "3.0.17-r5": 1556201987 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libldap-2.4.so.2" + ], + "provides": [ + "freeradius3-ldap=3.0.17-r5", + "so:rlm_ldap.so=0" + ] + }, + "libxml2-doc": { + "versions": { + "2.9.9-r1": 1551105514 + }, + "origin": "libxml2" + }, + "tlsdate-doc": { + "versions": { + "0.0.13-r7": 1545214008 + }, + "origin": "tlsdate" + }, + "perl-datetime-format-mail-doc": { + "versions": { + "0.403-r0": 1543927500 + }, + "origin": "perl-datetime-format-mail" + }, + "xdpyinfo": { + "versions": { + "1.3.2-r0": 1545069117 + }, + "origin": "xdpyinfo", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXi.so.6", + "so:libXtst.so.6", + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "cmd:xdpyinfo" + ] + }, + "xrefresh-doc": { + "versions": { + "1.0.6-r0": 1545290858 + }, + "origin": "xrefresh" + }, + "samba-dc": { + "versions": { + "4.8.11-r1": 1555334974 + }, + "origin": "samba", + "dependencies": [ + "samba-common=4.8.11-r1", + "samba-server=4.8.11-r1", + "samba-winbind=4.8.11-r1", + "py-samba=4.8.11-r1", + "tdb", + "so:libMESSAGING-samba4.so", + "so:libasn1util-samba4.so", + "so:libauth-samba4.so", + "so:libauth4-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcliauth-samba4.so", + "so:libcluster-samba4.so", + "so:libcmdline-credentials-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdsdb-module-samba4.so", + "so:libevents-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgnutls.so.30", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr.so.0", + "so:libnetif-samba4.so", + "so:libpopt.so.0", + "so:libposix-eadb-samba4.so", + "so:libprocess-model-samba4.so", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libservice-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbd-shim-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libtalloc.so.2", + "so:libtdb-wrap-samba4.so", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so" + ], + "provides": [ + "so:libsmbpasswdparser-samba4.so=0", + "cmd:samba", + "cmd:samba-tool", + "cmd:samba_dnsupdate", + "cmd:samba_gpoupdate", + "cmd:samba_kcc", + "cmd:samba_spnupdate", + "cmd:samba_upgradedns" + ] + }, + "libmatroska": { + "versions": { + "1.4.9-r0": 1543928900 + }, + "origin": "libmatroska", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libebml.so.4", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libmatroska.so.6=6.0.0" + ] + }, + "libmad-dev": { + "versions": { + "0.15.1b-r8": 1543925792 + }, + "origin": "libmad", + "dependencies": [ + "libmad=0.15.1b-r8", + "pkgconfig" + ], + "provides": [ + "pc:mad=0.15.0b" + ] + }, + "netcat-openbsd-doc": { + "versions": { + "1.130-r1": 1543935624 + }, + "origin": "netcat-openbsd" + }, + "s6-dns-doc": { + "versions": { + "2.3.0.1-r0": 1545062684 + }, + "origin": "s6-dns" + }, + "linux-firmware": { + "versions": { + "20190322-r0": 1554980655 + }, + "origin": "linux-firmware", + "dependencies": [ + "linux-firmware-yamaha=20190322-r0", + "linux-firmware-yam=20190322-r0", + "linux-firmware-vxge=20190322-r0", + "linux-firmware-vicam=20190322-r0", + "linux-firmware-ueagle-atm=20190322-r0", + "linux-firmware-ttusb-budget=20190322-r0", + "linux-firmware-tigon=20190322-r0", + "linux-firmware-ti-keystone=20190322-r0", + "linux-firmware-ti-connectivity=20190322-r0", + "linux-firmware-tehuti=20190322-r0", + "linux-firmware-sxg=20190322-r0", + "linux-firmware-sun=20190322-r0", + "linux-firmware-slicoss=20190322-r0", + "linux-firmware-sb16=20190322-r0", + "linux-firmware-rtw88=20190322-r0", + "linux-firmware-rtlwifi=20190322-r0", + "linux-firmware-rtl_nic=20190322-r0", + "linux-firmware-rtl_bt=20190322-r0", + "linux-firmware-rtl8192e=20190322-r0", + "linux-firmware-rsi=20190322-r0", + "linux-firmware-rockchip=20190322-r0", + "linux-firmware-radeon=20190322-r0", + "linux-firmware-r128=20190322-r0", + "linux-firmware-qlogic=20190322-r0", + "linux-firmware-qed=20190322-r0", + "linux-firmware-qcom=20190322-r0", + "linux-firmware-qca=20190322-r0", + "linux-firmware-ositech=20190322-r0", + "linux-firmware-nvidia=20190322-r0", + "linux-firmware-netronome=20190322-r0", + "linux-firmware-myricom=20190322-r0", + "linux-firmware-mwlwifi=20190322-r0", + "linux-firmware-mwl8k=20190322-r0", + "linux-firmware-mrvl=20190322-r0", + "linux-firmware-moxa=20190322-r0", + "linux-firmware-microchip=20190322-r0", + "linux-firmware-mellanox=20190322-r0", + "linux-firmware-mediatek=20190322-r0", + "linux-firmware-matrox=20190322-r0", + "linux-firmware-liquidio=20190322-r0", + "linux-firmware-libertas=20190322-r0", + "linux-firmware-korg=20190322-r0", + "linux-firmware-keyspan_pda=20190322-r0", + "linux-firmware-keyspan=20190322-r0", + "linux-firmware-kaweth=20190322-r0", + "linux-firmware-isci=20190322-r0", + "linux-firmware-intel=20190322-r0", + "linux-firmware-imx=20190322-r0", + "linux-firmware-i915=20190322-r0", + "linux-firmware-go7007=20190322-r0", + "linux-firmware-ess=20190322-r0", + "linux-firmware-ene-ub6250=20190322-r0", + "linux-firmware-emi62=20190322-r0", + "linux-firmware-emi26=20190322-r0", + "linux-firmware-edgeport=20190322-r0", + "linux-firmware-e100=20190322-r0", + "linux-firmware-dsp56k=20190322-r0", + "linux-firmware-dpaa2=20190322-r0", + "linux-firmware-dabusb=20190322-r0", + "linux-firmware-cxgb4=20190322-r0", + "linux-firmware-cxgb3=20190322-r0", + "linux-firmware-cpia2=20190322-r0", + "linux-firmware-cis=20190322-r0", + "linux-firmware-cavium=20190322-r0", + "linux-firmware-cadence=20190322-r0", + "linux-firmware-brcm=20190322-r0", + "linux-firmware-bnx2x=20190322-r0", + "linux-firmware-bnx2=20190322-r0", + "linux-firmware-av7110=20190322-r0", + "linux-firmware-atusb=20190322-r0", + "linux-firmware-atmel=20190322-r0", + "linux-firmware-ath9k_htc=20190322-r0", + "linux-firmware-ath6k=20190322-r0", + "linux-firmware-ath10k=20190322-r0", + "linux-firmware-ar3k=20190322-r0", + "linux-firmware-amdgpu=20190322-r0", + "linux-firmware-amd-ucode=20190322-r0", + "linux-firmware-amd=20190322-r0", + "linux-firmware-advansys=20190322-r0", + "linux-firmware-adaptec=20190322-r0", + "linux-firmware-acenic=20190322-r0", + "linux-firmware-3com=20190322-r0", + "linux-firmware-other=20190322-r0" + ], + "provides": [ + "linux-firmware-any" + ] + }, + "libconfig": { + "versions": { + "1.5-r3": 1543249701 + }, + "origin": "libconfig", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libconfig.so.9=9.2.0" + ] + }, + "apache-mod-auth-ntlm-winbind": { + "versions": { + "0.1-r6": 1545258464 + }, + "origin": "apache-mod-auth-ntlm-winbind", + "dependencies": [ + "apache2", + "samba", + "samba-winbind", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-file-temp": { + "versions": { + "0.2308-r0": 1543226523 + }, + "origin": "perl-file-temp" + }, + "font-misc-ethiopic": { + "versions": { + "1.0.3-r0": 1545222989 + }, + "origin": "font-misc-ethiopic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "sdl_mixer-dev": { + "versions": { + "1.2.12-r1": 1545162992 + }, + "origin": "sdl_mixer", + "dependencies": [ + "pc:sdl>=1.2.10", + "pkgconfig", + "sdl_mixer=1.2.12-r1" + ], + "provides": [ + "pc:SDL_mixer=1.2.12" + ] + }, + "llvm5-libs": { + "versions": { + "5.0.2-r0": 1546874980 + }, + "origin": "llvm5", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "llvm-libs=5.0.2-r0", + "so:libLLVM-5.0.so=0" + ] + }, + "libsecret": { + "versions": { + "0.18.6-r0": 1545291103 + }, + "origin": "libsecret", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libsecret-1.so.0=0.0.0", + "cmd:secret-tool" + ] + }, + "postgresql": { + "versions": { + "11.2-r0": 1554274177 + }, + "origin": "postgresql", + "dependencies": [ + "postgresql-client", + "tzdata", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libldap-2.4.so.2", + "so:libpq.so.5", + "so:libssl.so.1.1", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:initdb", + "cmd:pg_archivecleanup", + "cmd:pg_controldata", + "cmd:pg_ctl", + "cmd:pg_resetwal", + "cmd:pg_rewind", + "cmd:pg_test_fsync", + "cmd:pg_test_timing", + "cmd:pg_upgrade", + "cmd:pg_verify_checksums", + "cmd:pg_waldump", + "cmd:pgbench", + "cmd:postgres", + "cmd:postmaster" + ] + }, + "b43-fwcutter-doc": { + "versions": { + "019-r0": 1545163906 + }, + "origin": "b43-fwcutter" + }, + "perl-dbd-pg-doc": { + "versions": { + "3.7.4-r0": 1542972912 + }, + "origin": "perl-dbd-pg" + }, + "perl-test-manifest": { + "versions": { + "2.021-r0": 1543932508 + }, + "origin": "perl-test-manifest" + }, + "libmemcached-doc": { + "versions": { + "1.0.18-r3": 1543923892 + }, + "origin": "libmemcached" + }, + "npth": { + "versions": { + "1.6-r0": 1543932201 + }, + "origin": "npth", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnpth.so.0=0.1.2" + ] + }, + "qemu-block-ssh": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libssh2.so.1" + ] + }, + "libsecret-lang": { + "versions": { + "0.18.6-r0": 1545291103 + }, + "origin": "libsecret" + }, + "uwsgi-rsyslog": { + "versions": { + "2.0.17.1-r0": 1545062209 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "pcre2-dev": { + "versions": { + "10.32-r1": 1545069082 + }, + "origin": "pcre2", + "dependencies": [ + "libedit-dev", + "zlib-dev", + "libpcre2-16=10.32-r1", + "libpcre2-32=10.32-r1", + "pcre2=10.32-r1", + "pkgconfig" + ], + "provides": [ + "pc:libpcre2-16=10.32", + "pc:libpcre2-32=10.32", + "pc:libpcre2-8=10.32", + "pc:libpcre2-posix=10.32", + "cmd:pcre2-config" + ] + }, + "icecast": { + "versions": { + "2.4.4-r1": 1543934500 + }, + "origin": "icecast", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libogg.so.0", + "so:libssl.so.1.1", + "so:libtheora.so.0", + "so:libvorbis.so.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ], + "provides": [ + "cmd:icecast" + ] + }, + "libasyncns-dev": { + "versions": { + "0.8-r1": 1543226569 + }, + "origin": "libasyncns", + "dependencies": [ + "libasyncns=0.8-r1", + "pkgconfig" + ], + "provides": [ + "pc:libasyncns=0.8" + ] + }, + "mesa-gbm": { + "versions": { + "18.1.7-r2": 1554455971 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libexpat.so.1", + "so:libwayland-server.so.0", + "so:libz.so.1" + ], + "provides": [ + "so:libgbm.so.1=1.0.0" + ] + }, + "libmodplug-dev": { + "versions": { + "0.8.9.0-r0": 1545208837 + }, + "origin": "libmodplug", + "dependencies": [ + "libmodplug=0.8.9.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmodplug=0.8.9.0" + ] + }, + "quagga-dbg": { + "versions": { + "1.2.4-r1": 1545068941 + }, + "origin": "quagga", + "dependencies": [ + "iproute2" + ] + }, + "pm-utils-doc": { + "versions": { + "1.4.1-r1": 1543935621 + }, + "origin": "pm-utils" + }, + "mariadb-backup": { + "versions": { + "10.3.13-r1": 1557431735 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:liblzma.so.5", + "so:libpcre.so.1", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:mariabackup", + "cmd:mbstream" + ] + }, + "dnsmasq": { + "versions": { + "2.80-r3": 1545223253 + }, + "origin": "dnsmasq", + "dependencies": [ + "!dnsmasq-dnssec", + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dnsmasq" + ] + }, + "openssl-doc": { + "versions": { + "1.1.1b-r1": 1552660155 + }, + "origin": "openssl" + }, + "enchant": { + "versions": { + "1.6.0-r13": 1542965846 + }, + "origin": "enchant", + "dependencies": [ + "so:libaspell.so.15", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libhunspell-1.6.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libenchant.so.1=1.6.0", + "cmd:enchant", + "cmd:enchant-lsmod" + ] + }, + "lcms": { + "versions": { + "1.19-r8": 1546249798 + }, + "origin": "lcms", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:liblcms.so.1", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:icc2ps", + "cmd:icclink", + "cmd:icctrans", + "cmd:jpegicc", + "cmd:tiffdiff", + "cmd:tifficc", + "cmd:wtpt" + ] + }, + "qt-sqlite": { + "versions": { + "4.8.7-r11": 1545075087 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libsqlite3.so.0", + "so:libstdc++.so.6" + ] + }, + "aoetools": { + "versions": { + "37-r0": 1545073929 + }, + "origin": "aoetools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:aoe-discover", + "cmd:aoe-flush", + "cmd:aoe-interfaces", + "cmd:aoe-mkdevs", + "cmd:aoe-mkshelf", + "cmd:aoe-revalidate", + "cmd:aoe-sancheck", + "cmd:aoe-stat", + "cmd:aoe-version", + "cmd:aoecfg", + "cmd:aoeping", + "cmd:coraid-update" + ] + }, + "libvpx-dev": { + "versions": { + "1.6.1-r1": 1545163814 + }, + "origin": "libvpx", + "dependencies": [ + "libvpx=1.6.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:vpx=1.6.1" + ] + }, + "cunit-dev": { + "versions": { + "2.1.3-r1": 1542900197 + }, + "origin": "cunit", + "dependencies": [ + "cunit=2.1.3-r1", + "pkgconfig" + ] + }, + "perl-boolean": { + "versions": { + "0.46-r0": 1543928865 + }, + "origin": "perl-boolean" + }, + "collectd-lvm": { + "versions": { + "5.8.0-r3": 1546422678 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:liblvm2app.so.2.2" + ] + }, + "py3-zope-interface": { + "versions": { + "4.6.0-r0": 1548537850 + }, + "origin": "py-zope-interface", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "ppp-winbind": { + "versions": { + "2.4.7-r6": 1543999022 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "nftables-doc": { + "versions": { + "0.9.0-r0": 1542893220 + }, + "origin": "nftables" + }, + "clamav-daemon": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav", + "dependencies": [ + "freshclam", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libclamav.so.7", + "so:libcrypto.so.1.1", + "so:libfts.so.0", + "so:libncursesw.so.6", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:clamconf", + "cmd:clamd", + "cmd:clamdscan", + "cmd:clamdtop" + ] + }, + "mosh-client": { + "versions": { + "1.3.2-r7": 1543932000 + }, + "origin": "mosh", + "dependencies": [ + "openssh-client", + "perl-io-tty", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libprotobuf.so.17", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:mosh-client" + ] + }, + "screen": { + "versions": { + "4.6.2-r0": 1545290725 + }, + "origin": "screen", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:screen", + "cmd:screen-4.6.2" + ] + }, + "perl-text-wrapper": { + "versions": { + "1.05-r1": 1543927508 + }, + "origin": "perl-text-wrapper" + }, + "opensmtpd-doc": { + "versions": { + "6.0.3p1-r3": 1554301264 + }, + "origin": "opensmtpd" + }, + "py-openssl": { + "versions": { + "18.0.0-r0": 1545223367 + }, + "origin": "py-openssl", + "dependencies": [ + "py-cryptography", + "py-six" + ] + }, + "wpa_supplicant-doc": { + "versions": { + "2.7-r2": 1555501504 + }, + "origin": "wpa_supplicant" + }, + "nagios-plugins-disk_smb": { + "versions": { + "2.2.1-r6": 1543933903 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "samba-client", + "perl" + ] + }, + "py3-twitter": { + "versions": { + "3.5-r0": 1546524754 + }, + "origin": "py-twitter", + "dependencies": [ + "py3-future", + "py3-requests", + "py3-requests-oauthlib", + "python3" + ] + }, + "xf86-input-vmmouse": { + "versions": { + "13.1.0-r4": 1545207342 + }, + "origin": "xf86-input-vmmouse", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ], + "provides": [ + "cmd:vmmouse_detect" + ] + }, + "cairo": { + "versions": { + "1.16.0-r1": 1546948315 + }, + "origin": "cairo", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libxcb-render.so.0", + "so:libxcb-shm.so.0", + "so:libxcb.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libcairo-script-interpreter.so.2=2.11600.0", + "so:libcairo.so.2=2.11600.0" + ] + }, + "nginx-mod-http-upload-progress": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "libva-glx-dev": { + "versions": { + "2.2.0-r0": 1545300086 + }, + "origin": "libva-glx", + "dependencies": [ + "libva-dev", + "mesa-dev", + "libva-glx=2.2.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libva-glx=1.2.0" + ] + }, + "libsm-doc": { + "versions": { + "1.2.2-r2": 1542824341 + }, + "origin": "libsm" + }, + "xbacklight-doc": { + "versions": { + "1.2.2-r0": 1544792468 + }, + "origin": "xbacklight" + }, + "weechat-dev": { + "versions": { + "2.3-r0": 1545299932 + }, + "origin": "weechat", + "dependencies": [ + "cmake", + "libintl", + "ncurses-dev", + "gnutls-dev", + "libgcrypt-dev", + "curl-dev", + "aspell-dev", + "lua-dev", + "perl-dev", + "python2-dev", + "ruby-dev", + "zlib-dev", + "pkgconfig" + ], + "provides": [ + "pc:weechat=2.3" + ] + }, + "lua5.2-pc": { + "versions": { + "1.0.0-r9": 1543928870 + }, + "origin": "lua-pc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-list-allutils-doc": { + "versions": { + "0.14-r0": 1545214299 + }, + "origin": "perl-list-allutils" + }, + "augeas-tests": { + "versions": { + "1.11.0-r0": 1542924581 + }, + "origin": "augeas" + }, + "xz": { + "versions": { + "5.2.4-r0": 1542303286 + }, + "origin": "xz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblzma.so.5" + ], + "provides": [ + "cmd:lzcat", + "cmd:lzcmp", + "cmd:lzdiff", + "cmd:lzegrep", + "cmd:lzfgrep", + "cmd:lzgrep", + "cmd:lzless", + "cmd:lzma", + "cmd:lzmadec", + "cmd:lzmainfo", + "cmd:lzmore", + "cmd:unlzma", + "cmd:unxz", + "cmd:xz", + "cmd:xzcat", + "cmd:xzcmp", + "cmd:xzdec", + "cmd:xzdiff", + "cmd:xzegrep", + "cmd:xzfgrep", + "cmd:xzgrep", + "cmd:xzless", + "cmd:xzmore" + ] + }, + "linux-firmware-libertas": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "libgnome-doc": { + "versions": { + "2.32.1-r7": 1545299765 + }, + "origin": "libgnome" + }, + "perl-text-autoformat-doc": { + "versions": { + "1.74-r0": 1545118021 + }, + "origin": "perl-text-autoformat" + }, + "lua-lzmq": { + "versions": { + "0.4.4-r0": 1545076587 + }, + "origin": "lua-lzmq" + }, + "apcupsd": { + "versions": { + "3.14.14-r0": 1545293177 + }, + "origin": "apcupsd", + "dependencies": [ + "util-linux", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "cmd:apcaccess", + "cmd:apctest", + "cmd:apcupsd", + "cmd:smtp" + ] + }, + "vte": { + "versions": { + "0.28.2-r13": 1543927671 + }, + "origin": "vte", + "dependencies": [ + "so:libX11.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0" + ], + "provides": [ + "so:libvte.so.9=9.2800.2", + "cmd:vte" + ] + }, + "py2-olefile": { + "versions": { + "0.43-r2": 1542883723 + }, + "origin": "py-olefile" + }, + "clamav-daemon-openrc": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav", + "dependencies": [ + "freshclam-openrc" + ] + }, + "py-cliapp-doc": { + "versions": { + "1.20170823-r0": 1544798592 + }, + "origin": "py-cliapp" + }, + "uwsgi-notfound": { + "versions": { + "2.0.17.1-r0": 1545062204 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "vsftpd": { + "versions": { + "3.0.3-r6": 1544000085 + }, + "origin": "vsftpd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.1.1", + "so:libpam.so.0", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:vsftpd" + ] + }, + "qt-tds": { + "versions": { + "4.8.7-r11": 1545075088 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libsybdb.so.5" + ] + }, + "linux-firmware-qca": { + "versions": { + "20190322-r0": 1554980649 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "tig": { + "versions": { + "2.4.1-r0": 1545216501 + }, + "origin": "tig", + "dependencies": [ + "git", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:tig" + ] + }, + "perl-test-refcount-doc": { + "versions": { + "0.08-r0": 1544799026 + }, + "origin": "perl-test-refcount" + }, + "py-virtualenv": { + "versions": { + "15.1.0-r0": 1545066964 + }, + "origin": "py-virtualenv" + }, + "alpine-git-mirror-syncd": { + "versions": { + "0.3.0-r0": 1543932706 + }, + "origin": "alpine-git-mirror-syncd", + "dependencies": [ + "ca-certificates", + "git", + "lua5.1", + "lua5.1-cjson", + "lua5.1-mosquitto" + ], + "provides": [ + "cmd:git-mirror-syncd" + ] + }, + "linux-firmware-rtl_nic": { + "versions": { + "20190322-r0": 1554980648 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "py2-werkzeug": { + "versions": { + "0.14.1-r0": 1549977320 + }, + "origin": "py-werkzeug", + "dependencies": [ + "python2" + ] + }, + "ckbcomp": { + "versions": { + "1.187-r0": 1545292994 + }, + "origin": "ckbcomp", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:ckbcomp" + ] + }, + "jsoncpp": { + "versions": { + "1.8.4-r0": 1545075440 + }, + "origin": "jsoncpp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libjsoncpp.so.20=20" + ] + }, + "uwsgi-ugreen": { + "versions": { + "2.0.17.1-r0": 1545062213 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "py3-bluez": { + "versions": { + "0.22-r1": 1545062450 + }, + "origin": "py-bluez", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "py3-purl": { + "versions": { + "1.4-r0": 1543925754 + }, + "origin": "py-purl", + "dependencies": [ + "py3-six", + "python3" + ] + }, + "openldap-backend-all": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "openldap-back-bdb", + "openldap-back-dnssrv", + "openldap-back-hdb", + "openldap-back-ldap", + "openldap-back-mdb", + "openldap-back-meta", + "openldap-back-monitor", + "openldap-back-null", + "openldap-back-passwd", + "openldap-back-relay", + "openldap-back-shell", + "openldap-back-sql", + "openldap-back-sock" + ] + }, + "openpgm-dev": { + "versions": { + "5.2.122-r1": 1543222717 + }, + "origin": "openpgm", + "dependencies": [ + "openpgm=5.2.122-r1", + "pkgconfig" + ], + "provides": [ + "pc:openpgm-5.2=5.2.122" + ] + }, + "py2-singledispatch": { + "versions": { + "3.4.0.3-r1": 1545300040 + }, + "origin": "py2-singledispatch", + "dependencies": [ + "python2" + ], + "provides": [ + "py-singledispatch" + ] + }, + "libvncserver": { + "versions": { + "0.9.11-r2": 1543248465 + }, + "origin": "libvncserver", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libz.so.1" + ], + "provides": [ + "so:libvncclient.so.1=1.0.0", + "so:libvncserver.so.1=1.0.0" + ] + }, + "libnetfilter_queue-dev": { + "versions": { + "1.0.3-r0": 1546267692 + }, + "origin": "libnetfilter_queue", + "dependencies": [ + "libnetfilter_queue=1.0.3-r0", + "pc:libnfnetlink", + "pkgconfig" + ], + "provides": [ + "pc:libnetfilter_queue=1.0.3" + ] + }, + "ruby-etc": { + "versions": { + "2.5.5-r0": 1557164836 + }, + "origin": "ruby", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.5" + ] + }, + "libmng-dev": { + "versions": { + "2.0.3-r1": 1545062629 + }, + "origin": "libmng", + "dependencies": [ + "libjpeg-turbo-dev", + "libmng=2.0.3-r1", + "pkgconfig" + ], + "provides": [ + "pc:libmng=2.0.2" + ] + }, + "linux-firmware-tigon": { + "versions": { + "20190322-r0": 1554980647 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "polkit-gnome": { + "versions": { + "0.105-r1": 1544798744 + }, + "origin": "polkit-gnome", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpolkit-agent-1.so.0", + "so:libpolkit-gobject-1.so.0" + ] + }, + "perl-io-socket-ssl-doc": { + "versions": { + "2.060-r0": 1545164013 + }, + "origin": "perl-io-socket-ssl" + }, + "iso-codes-dev": { + "versions": { + "4.1-r0": 1545257105 + }, + "origin": "iso-codes", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:iso-codes=4.1" + ] + }, + "freeradius-perl": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "perl", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ], + "provides": [ + "freeradius3-perl=3.0.17-r5", + "so:rlm_perl.so=0" + ] + }, + "libdvdnav-doc": { + "versions": { + "5.0.3-r1": 1543999001 + }, + "origin": "libdvdnav" + }, + "dejagnu": { + "versions": { + "1.6.2-r0": 1545820393 + }, + "origin": "dejagnu", + "dependencies": [ + "expect" + ], + "provides": [ + "cmd:runtest" + ] + }, + "dovecot-sqlite": { + "versions": { + "2.3.6-r0": 1557134098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot-sql=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libdriver_sqlite.so=0" + ] + }, + "spl-vanilla-dev": { + "versions": { + "4.19.41-r0": 1557400331 + }, + "origin": "spl-vanilla", + "dependencies": [ + "linux-vanilla-dev=4.19.41-r0" + ] + }, + "lua5.2-rex-pcre": { + "versions": { + "2.9.0-r0": 1545209202 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ] + }, + "libxdg-basedir": { + "versions": { + "1.2.0-r0": 1545116744 + }, + "origin": "libxdg-basedir", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxdg-basedir.so.1=1.2.0" + ] + }, + "fftw-single-libs": { + "versions": { + "3.3.8-r0": 1543926478 + }, + "origin": "fftw", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfftw3f.so.3=3.5.8", + "so:libfftw3f_threads.so.3=3.5.8" + ] + }, + "sshguard-doc": { + "versions": { + "2.2.0-r0": 1545300225 + }, + "origin": "sshguard" + }, + "skytraq-datalogger": { + "versions": { + "0.5.1-r1": 1543927504 + }, + "origin": "skytraq-datalogger", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ], + "provides": [ + "cmd:skytraq-datalogger" + ] + }, + "xcb-util-renderutil": { + "versions": { + "0.3.9-r1": 1543927922 + }, + "origin": "xcb-util-renderutil", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-render.so.0", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-render-util.so.0=0.0.0" + ] + }, + "py3-btrfs-progs": { + "versions": { + "4.19.1-r0": 1545665049 + }, + "origin": "btrfs-progs", + "dependencies": [ + "so:libbtrfsutil.so.1", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ], + "provides": [ + "py-btrfs-progs=4.19.1-r0" + ] + }, + "perl-http-date-doc": { + "versions": { + "6.02-r1": 1542821757 + }, + "origin": "perl-http-date" + }, + "py3-gpsd": { + "versions": { + "3.18.1-r1": 1549881587 + }, + "origin": "gpsd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:gpscat", + "cmd:gpsfake", + "cmd:gpsprof" + ] + }, + "tunnel": { + "versions": { + "1.0-r0": 1545076546 + }, + "origin": "tunnel" + }, + "libverto-glib": { + "versions": { + "0.3.0-r1": 1543223527 + }, + "origin": "libverto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libverto.so.1" + ], + "provides": [ + "so:libverto-glib.so.1=1.0.0" + ] + }, + "makekit": { + "versions": { + "0.2-r0": 1545223342 + }, + "origin": "makekit", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:makekit", + "cmd:mkdash" + ] + }, + "libpcre32": { + "versions": { + "8.42-r1": 1545067005 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre32.so.0=0.0.10" + ] + }, + "libgpg-error-lisp": { + "versions": { + "1.33-r0": 1545838267 + }, + "origin": "libgpg-error" + }, + "libxft": { + "versions": { + "2.3.2-r3": 1542824036 + }, + "origin": "libxft", + "dependencies": [ + "so:libX11.so.6", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libfreetype.so.6" + ], + "provides": [ + "so:libXft.so.2=2.3.2" + ] + }, + "nginx-mod-rtmp": { + "versions": { + "1.14.2-r1": 1557179822 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "nginx-rtmp" + ] + }, + "libiptcdata": { + "versions": { + "1.0.4-r2": 1543932145 + }, + "origin": "libiptcdata", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libiptcdata.so.0=0.3.3", + "cmd:iptc" + ] + }, + "mesa-vulkan-ati": { + "versions": { + "18.1.7-r2": 1554455971 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libelf.so.0", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libwayland-client.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb.so.1", + "so:libxshmfence.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libvulkan_radeon.so=0" + ] + }, + "glu": { + "versions": { + "9.0.0-r4": 1543927738 + }, + "origin": "glu", + "dependencies": [ + "so:libGL.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libGLU.so.1=1.3.1" + ] + }, + "xf86-video-ast": { + "versions": { + "1.1.5-r4": 1545300634 + }, + "origin": "xf86-video-ast", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libevent": { + "versions": { + "2.1.8-r6": 1543223505 + }, + "origin": "libevent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libevent-2.1.so.6=6.0.2", + "so:libevent_core-2.1.so.6=6.0.2", + "so:libevent_extra-2.1.so.6=6.0.2", + "so:libevent_openssl-2.1.so.6=6.0.2", + "so:libevent_pthreads-2.1.so.6=6.0.2" + ] + }, + "perl-symbol-global-name": { + "versions": { + "0.05-r1": 1545223348 + }, + "origin": "perl-symbol-global-name" + }, + "lua5.3-socket": { + "versions": { + "3.0_rc1_git20160306-r2": 1543934475 + }, + "origin": "lua-socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-test-leaktrace-doc": { + "versions": { + "0.16-r0": 1542821880 + }, + "origin": "perl-test-leaktrace" + }, + "diffutils-doc": { + "versions": { + "3.7-r0": 1546846562 + }, + "origin": "diffutils" + }, + "dovecot-submissiond": { + "versions": { + "2.3.6-r0": 1557134098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libdovecot-login.so.0", + "so:libdovecot-storage.so.0", + "so:libdovecot.so.0" + ] + }, + "cups-doc": { + "versions": { + "2.2.10-r0": 1545910846 + }, + "origin": "cups" + }, + "libunique-doc": { + "versions": { + "1.1.6-r6": 1545299841 + }, + "origin": "libunique" + }, + "gst-plugins-base": { + "versions": { + "1.14.4-r0": 1543928665 + }, + "origin": "gst-plugins-base", + "dependencies": [ + "so:libEGL.so.1", + "so:libGL.so.1", + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcdda_interface.so.0", + "so:libcdda_paranoia.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-1.0.so.0", + "so:libgstcontroller-1.0.so.0", + "so:libgstnet-1.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libintl.so.8", + "so:libogg.so.0", + "so:libopus.so.0", + "so:liborc-0.4.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpng16.so.16", + "so:libtheoradec.so.1", + "so:libtheoraenc.so.1", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2", + "so:libwayland-client.so.0", + "so:libwayland-egl.so.1", + "so:libxcb.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libgstadder.so=0", + "so:libgstallocators-1.0.so.0=0.1404.0", + "so:libgstalsa.so=0", + "so:libgstapp-1.0.so.0=0.1404.0", + "so:libgstapp.so=0", + "so:libgstaudio-1.0.so.0=0.1404.0", + "so:libgstaudioconvert.so=0", + "so:libgstaudiomixer.so=0", + "so:libgstaudiorate.so=0", + "so:libgstaudioresample.so=0", + "so:libgstaudiotestsrc.so=0", + "so:libgstcdparanoia.so=0", + "so:libgstencoding.so=0", + "so:libgstfft-1.0.so.0=0.1404.0", + "so:libgstgio.so=0", + "so:libgstgl-1.0.so.0=0.1404.0", + "so:libgstogg.so=0", + "so:libgstopengl.so=0", + "so:libgstopus.so=0", + "so:libgstpango.so=0", + "so:libgstpbtypes.so=0", + "so:libgstpbutils-1.0.so.0=0.1404.0", + "so:libgstplayback.so=0", + "so:libgstrawparse.so=0", + "so:libgstriff-1.0.so.0=0.1404.0", + "so:libgstrtp-1.0.so.0=0.1404.0", + "so:libgstrtsp-1.0.so.0=0.1404.0", + "so:libgstsdp-1.0.so.0=0.1404.0", + "so:libgstsubparse.so=0", + "so:libgsttag-1.0.so.0=0.1404.0", + "so:libgsttcp.so=0", + "so:libgsttheora.so=0", + "so:libgsttypefindfunctions.so=0", + "so:libgstvideo-1.0.so.0=0.1404.0", + "so:libgstvideoconvert.so=0", + "so:libgstvideorate.so=0", + "so:libgstvideoscale.so=0", + "so:libgstvideotestsrc.so=0", + "so:libgstvolume.so=0", + "so:libgstvorbis.so=0", + "so:libgstximagesink.so=0", + "so:libgstxvimagesink.so=0", + "cmd:gst-device-monitor-1.0", + "cmd:gst-discoverer-1.0", + "cmd:gst-play-1.0" + ] + }, + "py2-unidecode": { + "versions": { + "1.0.23-r0": 1545299542 + }, + "origin": "py-unidecode", + "dependencies": [ + "py3-unidecode", + "python2" + ], + "provides": [ + "cmd:unidecode-2" + ] + }, + "help2man": { + "versions": { + "1.47.8-r0": 1545745816 + }, + "origin": "help2man", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:help2man" + ] + }, + "perl-tree-dag_node": { + "versions": { + "1.31-r0": 1545213971 + }, + "origin": "perl-tree-dag_node", + "dependencies": [ + "perl-file-slurp-tiny" + ] + }, + "perl-php-serialization": { + "versions": { + "0.34-r1": 1545214147 + }, + "origin": "perl-php-serialization", + "dependencies": [ + "perl" + ] + }, + "py-django-registration": { + "versions": { + "1.0-r1": 1545207353 + }, + "origin": "py-django-registration", + "dependencies": [ + "python2" + ] + }, + "lua5.2-sec": { + "versions": { + "0.7-r1": 1545075388 + }, + "origin": "lua-sec", + "dependencies": [ + "lua5.2-socket", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "py3-ply": { + "versions": { + "3.11-r0": 1556798213 + }, + "origin": "py3-ply", + "dependencies": [ + "python3" + ] + }, + "perl-db_file-doc": { + "versions": { + "1.843-r0": 1545061210 + }, + "origin": "perl-db_file" + }, + "mupdf-dev": { + "versions": { + "1.13.0-r2": 1544000575 + }, + "origin": "mupdf", + "dependencies": [ + "mupdf=1.13.0-r2" + ] + }, + "opensp": { + "versions": { + "1.5.2-r0": 1542893110 + }, + "origin": "opensp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libosp.so.5=5.0.0", + "cmd:onsgmls", + "cmd:osgmlnorm", + "cmd:ospam", + "cmd:ospcat", + "cmd:ospent", + "cmd:osx" + ] + }, + "lcms2-doc": { + "versions": { + "2.9-r1": 1542824189 + }, + "origin": "lcms2" + }, + "gettext-libs": { + "versions": { + "0.19.8.1-r4": 1542304413 + }, + "origin": "gettext", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libunistring.so.2" + ], + "provides": [ + "so:libgettextpo.so.0=0.5.4" + ] + }, + "lua5.1-pty": { + "versions": { + "1.2.1-r1": 1545216358 + }, + "origin": "lua-pty", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua-uuid": { + "versions": { + "2012.05-r1": 1542883709 + }, + "origin": "lua-uuid", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ] + }, + "eudev-dev": { + "versions": { + "3.2.7-r0": 1542845384 + }, + "origin": "eudev", + "dependencies": [ + "eudev=3.2.7-r0", + "pkgconfig" + ], + "provides": [ + "pc:libudev=220", + "pc:udev=220" + ] + }, + "st": { + "versions": { + "0.8.1-r0": 1545292624 + }, + "origin": "st", + "dependencies": [ + "ncurses-terminfo", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1" + ], + "provides": [ + "cmd:st" + ] + }, + "mp3splt-gtk-lang": { + "versions": { + "0.9.2-r2": 1545069036 + }, + "origin": "mp3splt-gtk" + }, + "py-tz": { + "versions": { + "2018.9-r0": 1548109649 + }, + "origin": "py-tz" + }, + "libcap-ng-dev": { + "versions": { + "0.7.9-r1": 1545918386 + }, + "origin": "libcap-ng", + "dependencies": [ + "linux-headers", + "libcap-ng=0.7.9-r1", + "pkgconfig" + ], + "provides": [ + "pc:libcap-ng=0.7.9" + ] + }, + "gamin-dev": { + "versions": { + "0.1.10-r10": 1543928476 + }, + "origin": "gamin", + "dependencies": [ + "gamin=0.1.10-r10", + "pkgconfig" + ], + "provides": [ + "pc:gamin=0.1.10" + ] + }, + "msmtp": { + "versions": { + "1.8.1-r1": 1545062528 + }, + "origin": "msmtp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30", + "so:libintl.so.8" + ], + "provides": [ + "cmd:msmtp", + "cmd:msmtpd" + ] + }, + "zsh-calendar": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "py2-idna": { + "versions": { + "2.7-r0": 1542825009 + }, + "origin": "py-idna", + "dependencies": [ + "python2" + ] + }, + "postfix": { + "versions": { + "3.3.2-r0": 1547130363 + }, + "origin": "postfix", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libdb-5.3.so", + "so:libsasl2.so.3", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libpostfix-dns.so=0", + "so:libpostfix-global.so=0", + "so:libpostfix-master.so=0", + "so:libpostfix-tls.so=0", + "so:libpostfix-util.so=0", + "cmd:mailq", + "cmd:newaliases", + "cmd:postalias", + "cmd:postcat", + "cmd:postconf", + "cmd:postdrop", + "cmd:postfix", + "cmd:postkick", + "cmd:postlock", + "cmd:postlog", + "cmd:postmap", + "cmd:postmulti", + "cmd:postqueue", + "cmd:postsuper", + "cmd:sendmail" + ] + }, + "ebtables-doc": { + "versions": { + "2.0.10.4-r2": 1543935616 + }, + "origin": "ebtables" + }, + "oidentd-doc": { + "versions": { + "2.2.3-r0": 1545163555 + }, + "origin": "oidentd" + }, + "opensp-dev": { + "versions": { + "1.5.2-r0": 1542893109 + }, + "origin": "opensp", + "dependencies": [ + "opensp=1.5.2-r0" + ] + }, + "py3-werkzeug": { + "versions": { + "0.14.1-r0": 1549977321 + }, + "origin": "py-werkzeug", + "dependencies": [ + "python3" + ] + }, + "setpriv": { + "versions": { + "2.33-r0": 1545307437 + }, + "origin": "util-linux", + "dependencies": [ + "findmnt", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0" + ], + "provides": [ + "cmd:setpriv" + ] + }, + "hexchat": { + "versions": { + "2.14.2-r0": 1545746059 + }, + "origin": "hexchat", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libdbus-glib-1.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:liblua-5.3.so.0", + "so:libnotify.so.4", + "so:libpango-1.0.so.0", + "so:libproxy.so.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:hexchat" + ] + }, + "perl-module-scandeps": { + "versions": { + "1.25-r0": 1542893295 + }, + "origin": "perl-module-scandeps", + "provides": [ + "cmd:scandeps.pl" + ] + }, + "alpine-base": { + "versions": { + "3.9.4-r0": 1557434973 + }, + "origin": "alpine-base", + "dependencies": [ + "alpine-baselayout", + "alpine-conf", + "apk-tools", + "busybox", + "busybox-suid", + "busybox-initscripts", + "openrc", + "libc-utils", + "alpine-keys" + ] + }, + "pcmciautils": { + "versions": { + "018-r1": 1543077272 + }, + "origin": "pcmciautils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lspcmcia", + "cmd:pccardctl" + ] + }, + "boost-random": { + "versions": { + "1.67.0-r2": 1542823633 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.67.0", + "so:libboost_system.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_random-mt.so.1.67.0=1.67.0", + "so:libboost_random.so.1.67.0=1.67.0" + ] + }, + "lm_sensors-sensord-openrc": { + "versions": { + "3.4.0-r6": 1542924820 + }, + "origin": "lm_sensors", + "dependencies": [ + "bash", + "sysfsutils" + ] + }, + "perl-test2-plugin-nowarnings": { + "versions": { + "0.06-r0": 1542973053 + }, + "origin": "perl-test2-plugin-nowarnings", + "dependencies": [ + "perl-ipc-run3", + "perl-test2-suite", + "perl-test-simple" + ] + }, + "igmpproxy": { + "versions": { + "0.2.1-r0": 1545746723 + }, + "origin": "igmpproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:igmpproxy" + ] + }, + "nagios-plugins-overcr": { + "versions": { + "2.2.1-r6": 1543933910 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-firmware-keyspan": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "duply": { + "versions": { + "2.1-r0": 1545300230 + }, + "origin": "duply", + "dependencies": [ + "duplicity", + "bash" + ], + "provides": [ + "cmd:duply" + ] + }, + "squid-lang-sr": { + "versions": { + "4.4-r1": 1545216329 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-error-doc": { + "versions": { + "0.17027-r0": 1545062330 + }, + "origin": "perl-error" + }, + "xscreensaver-doc": { + "versions": { + "5.40-r0": 1545224086 + }, + "origin": "xscreensaver" + }, + "perl-sub-exporter-progressive": { + "versions": { + "0.001013-r0": 1542883267 + }, + "origin": "perl-sub-exporter-progressive" + }, + "perl-devel-globaldestruction": { + "versions": { + "0.14-r0": 1545162998 + }, + "origin": "perl-devel-globaldestruction", + "dependencies": [ + "perl-sub-exporter-progressive" + ] + }, + "kamailio-ims": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libkamailio_ims.so.0", + "so:libsrdb1.so.1", + "so:libsrutils.so.1", + "so:libxml2.so.2" + ] + }, + "axel": { + "versions": { + "2.16.1-r2": 1545229308 + }, + "origin": "axel", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:axel" + ] + }, + "perl-libwww-doc": { + "versions": { + "6.36-r0": 1544791738 + }, + "origin": "perl-libwww" + }, + "libxcomposite-doc": { + "versions": { + "0.4.4-r2": 1543925370 + }, + "origin": "libxcomposite" + }, + "trac": { + "versions": { + "1.2.2-r0": 1543924853 + }, + "origin": "trac", + "dependencies": [ + "python2", + "py-setuptools", + "py-genshi", + "/bin/sh" + ], + "provides": [ + "cmd:trac-admin", + "cmd:tracd" + ] + }, + "openldap-back-bdb": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "py2-urllib3": { + "versions": { + "1.22-r0": 1542825019 + }, + "origin": "py-urllib3", + "dependencies": [ + "python2" + ] + }, + "unbound-libs": { + "versions": { + "1.8.3-r1": 1555953584 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libevent-2.1.so.6", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libunbound.so.8=8.0.3" + ] + }, + "xauth-doc": { + "versions": { + "1.0.10-r1": 1542973224 + }, + "origin": "xauth" + }, + "perl-mro-compat": { + "versions": { + "0.13-r0": 1542972950 + }, + "origin": "perl-mro-compat" + }, + "augeas-dev": { + "versions": { + "1.11.0-r0": 1542924580 + }, + "origin": "augeas", + "dependencies": [ + "augeas-libs=1.11.0-r0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:augeas=1.11.0" + ] + }, + "sed": { + "versions": { + "4.5-r0": 1542822960 + }, + "origin": "sed", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sed" + ] + }, + "libassuan-dev": { + "versions": { + "2.5.1-r0": 1543932159 + }, + "origin": "libassuan", + "dependencies": [ + "libassuan=2.5.1-r0" + ], + "provides": [ + "cmd:libassuan-config" + ] + }, + "bluez-meshctl": { + "versions": { + "5.50-r0": 1545822205 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0", + "so:libjson-c.so.4", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:meshctl" + ] + }, + "perl-package-stash-doc": { + "versions": { + "0.37-r0": 1542845730 + }, + "origin": "perl-package-stash" + }, + "postfix-mysql": { + "versions": { + "3.3.2-r0": 1547130363 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ], + "provides": [ + "so:postfix-mysql.so=0" + ] + }, + "wxgtk2.8-media": { + "versions": { + "2.8.12.1-r4": 1545075287 + }, + "origin": "wxgtk2.8", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstinterfaces-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libstdc++.so.6", + "so:libwx_baseu-2.8.so.0", + "so:libwx_gtk2u_core-2.8.so.0" + ], + "provides": [ + "so:libwx_gtk2u_media-2.8.so.0=0.8.0" + ] + }, + "cpufrequtils-dev": { + "versions": { + "008-r4": 1543927814 + }, + "origin": "cpufrequtils", + "dependencies": [ + "cpufrequtils=008-r4" + ] + }, + "cpufreqd": { + "versions": { + "2.4.2-r4": 1543998859 + }, + "origin": "cpufreqd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcpufreq.so.0", + "so:libsysfs.so.2" + ], + "provides": [ + "so:cpufreqd_acpi.so=0", + "so:cpufreqd_apm.so=0", + "so:cpufreqd_cpu.so=0", + "so:cpufreqd_exec.so=0", + "so:cpufreqd_governor_parameters.so=0", + "so:cpufreqd_nforce2.so=0", + "so:cpufreqd_pmu.so=0", + "so:cpufreqd_programs.so=0", + "so:cpufreqd_tau.so=0", + "cmd:cpufreqd", + "cmd:cpufreqd-get", + "cmd:cpufreqd-set" + ] + }, + "shorewall": { + "versions": { + "5.2.2-r0": 1548095422 + }, + "origin": "shorewall", + "dependencies": [ + "shorewall-core", + "perl", + "iptables", + "iproute2" + ] + }, + "network-extras": { + "versions": { + "1.2-r0": 1545062256 + }, + "origin": "network-extras", + "dependencies": [ + "bridge", + "bonding", + "vlan", + "wpa_supplicant", + "wireless-tools", + "ppp-atm", + "ppp-chat", + "ppp-daemon", + "ppp-l2tp", + "ppp-minconn", + "ppp-passprompt", + "ppp-passwordfd", + "ppp-pppoe", + "ppp-radius", + "ppp-winbind", + "usb-modeswitch" + ] + }, + "lxterminal": { + "versions": { + "0.3.2-r0": 1543927689 + }, + "origin": "lxterminal", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libvte.so.9" + ], + "provides": [ + "cmd:lxterminal" + ] + }, + "py-werkzeug": { + "versions": { + "0.14.1-r0": 1549977321 + }, + "origin": "py-werkzeug" + }, + "lvm2-doc": { + "versions": { + "2.02.182-r0": 1543928933 + }, + "origin": "lvm2" + }, + "py2-crypto": { + "versions": { + "2.6.1-r2": 1543223470 + }, + "origin": "py-crypto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libpython2.7.so.1.0" + ] + }, + "perl-sub-identify": { + "versions": { + "0.14-r1": 1542973115 + }, + "origin": "perl-sub-identify", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "apache-mod-fcgid": { + "versions": { + "2.3.9-r2": 1545293007 + }, + "origin": "apache-mod-fcgid", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1" + ] + }, + "libxslt": { + "versions": { + "1.1.33-r1": 1555485888 + }, + "origin": "libxslt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libxml2.so.2" + ], + "provides": [ + "so:libexslt.so.0=0.8.20", + "so:libxslt.so.1=1.1.33", + "cmd:xsltproc" + ] + }, + "libusb-compat-dev": { + "versions": { + "0.1.5-r4": 1543921872 + }, + "origin": "libusb-compat", + "dependencies": [ + "libusb-compat=0.1.5-r4", + "pc:libusb-1.0", + "pkgconfig" + ], + "provides": [ + "pc:libusb=0.1.12", + "cmd:libusb-config" + ] + }, + "openldap-back-sock": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "perl-sub-exporter-doc": { + "versions": { + "0.987-r0": 1542883246 + }, + "origin": "perl-sub-exporter" + }, + "bash": { + "versions": { + "4.4.19-r1": 1542301270 + }, + "origin": "bash", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:bash" + ] + }, + "gphoto2-lang": { + "versions": { + "2.5.15-r0": 1545300678 + }, + "origin": "gphoto2" + }, + "perl-device-serialport-doc": { + "versions": { + "1.04-r10": 1545062309 + }, + "origin": "perl-device-serialport" + }, + "byacc": { + "versions": { + "20180609-r0": 1545062012 + }, + "origin": "byacc", + "dependencies": [ + "!bison", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:yacc" + ] + }, + "perl-data-guid": { + "versions": { + "0.049-r0": 1545163361 + }, + "origin": "perl-data-guid", + "dependencies": [ + "perl-sub-install", + "perl-sub-exporter", + "perl-data-uuid" + ] + }, + "py-argparse": { + "versions": { + "1.4.0-r2": 1545060593 + }, + "origin": "py-argparse" + }, + "ortp": { + "versions": { + "0.25.0-r0": 1543077282 + }, + "origin": "ortp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libortp.so.10=10.0.0" + ] + }, + "qemu-x86_64": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-x86_64" + ] + }, + "gnokii-dev": { + "versions": { + "0.6.31-r8": 1545300478 + }, + "origin": "gnokii", + "dependencies": [ + "gnokii-libs=0.6.31-r8", + "pkgconfig" + ], + "provides": [ + "pc:gnokii=0.6.31", + "pc:xgnokii=1.0" + ] + }, + "libmng-doc": { + "versions": { + "2.0.3-r1": 1545062629 + }, + "origin": "libmng" + }, + "nagios-plugins-tcp": { + "versions": { + "2.2.1-r6": 1543933914 + }, + "origin": "nagios-plugins", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "nagios-plugins-clamdnagios-plugins-ftpnagios-plugins-imapnagios-plugins-jabbernagios-plugins-nntpnagios-plugins-nntpsnagios-plugins-popnagios-plugins-simapnagios-plugins-spopnagios-plugins-ssmtpnagios-plugins-udp" + ] + }, + "squid": { + "versions": { + "4.4-r1": 1545216331 + }, + "origin": "squid", + "dependencies": [ + "logrotate", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.1.1", + "so:libdb-5.3.so", + "so:libgcc_s.so.1", + "so:libltdl.so.7", + "so:libssl.so.1.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:purge", + "cmd:squid", + "cmd:squidclient" + ] + }, + "cvechecker": { + "versions": { + "3.9-r1": 1543249725 + }, + "origin": "cvechecker", + "dependencies": [ + "gawk", + "wget", + "libxslt", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libconfig.so.9", + "so:libmariadb.so.3", + "so:libsqlite3.so.0" + ], + "provides": [ + "cmd:cvechecker", + "cmd:cvegenversdat", + "cmd:cvereport", + "cmd:cverules", + "cmd:pullcves" + ] + }, + "py2-markupsafe": { + "versions": { + "1.1.0-r0": 1544791904 + }, + "origin": "py-markupsafe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "lftp": { + "versions": { + "4.8.4-r1": 1545073926 + }, + "origin": "lftp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libncursesw.so.6", + "so:libreadline.so.7", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:cmd-mirror.so=0", + "so:cmd-sleep.so=0", + "so:cmd-torrent.so=0", + "so:liblftp-jobs.so.0=0.0.0", + "so:liblftp-network.so=0", + "so:liblftp-pty.so=0", + "so:liblftp-tasks.so.0=0.0.0", + "so:proto-file.so=0", + "so:proto-fish.so=0", + "so:proto-ftp.so=0", + "so:proto-http.so=0", + "so:proto-sftp.so=0", + "cmd:lftp", + "cmd:lftpget" + ] + }, + "mariadb-dev": { + "versions": { + "10.3.13-r1": 1557431734 + }, + "origin": "mariadb", + "dependencies": [ + "openssl-dev", + "zlib-dev", + "mariadb-connector-c-dev", + "mariadb-embedded=10.3.13-r1", + "pkgconfig" + ], + "provides": [ + "mysql-dev=10.3.13-r1" + ] + }, + "collectd-mysql": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "ntfs-3g-progs": { + "versions": { + "2017.3.23-r1": 1542973174 + }, + "origin": "ntfs-3g", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libntfs-3g.so.88", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:mkntfs", + "cmd:ntfs-3g.probe", + "cmd:ntfscat", + "cmd:ntfsclone", + "cmd:ntfscluster", + "cmd:ntfscmp", + "cmd:ntfscp", + "cmd:ntfsfix", + "cmd:ntfsinfo", + "cmd:ntfslabel", + "cmd:ntfsls", + "cmd:ntfsresize", + "cmd:ntfsundelete" + ] + }, + "py-gdbm": { + "versions": { + "2.7.16-r1": 1557171399 + }, + "origin": "python2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "python-gdbm=2.7.16-r1" + ] + }, + "clucene-contribs": { + "versions": { + "2.3.3.4-r5": 1543999111 + }, + "origin": "clucene", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libclucene-core.so.1", + "so:libclucene-shared.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libclucene-contribs-lib.so.1=2.3.3.4" + ] + }, + "sylpheed-lang": { + "versions": { + "3.7.0-r2": 1544799190 + }, + "origin": "sylpheed", + "dependencies": [ + "pinentry-gtk" + ] + }, + "cups-libs": { + "versions": { + "2.2.10-r0": 1545910846 + }, + "origin": "cups", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30", + "so:libz.so.1" + ], + "provides": [ + "so:libcups.so.2=2", + "so:libcupsimage.so.2=2" + ] + }, + "run-parts": { + "versions": { + "4.8.6-r0": 1543924425 + }, + "origin": "run-parts", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:run-parts" + ] + }, + "aspell-ru": { + "versions": { + "0.99f7-r0": 1543223264 + }, + "origin": "aspell-ru" + }, + "libxaw-doc": { + "versions": { + "1.0.13-r2": 1542894088 + }, + "origin": "libxaw" + }, + "vanessa_logger": { + "versions": { + "0.0.10-r0": 1542893600 + }, + "origin": "vanessa_logger", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libvanessa_logger.so.0=0.0.5", + "cmd:vanessa_logger_sample" + ] + }, + "perl-net-smtp-tls-butmaintained-doc": { + "versions": { + "0.24-r0": 1545235359 + }, + "origin": "perl-net-smtp-tls-butmaintained" + }, + "charybdis-openrc": { + "versions": { + "3.5.6-r1": 1542883316 + }, + "origin": "charybdis" + }, + "perl-tree-dag_node-doc": { + "versions": { + "1.31-r0": 1545213970 + }, + "origin": "perl-tree-dag_node" + }, + "altermime": { + "versions": { + "0.3.11-r0": 1543222722 + }, + "origin": "altermime", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:altermime" + ] + }, + "jpeg": { + "versions": { + "8-r6": 1542893530 + }, + "origin": "jpeg", + "dependencies": [ + "libjpeg-turbo-utils" + ] + }, + "qemu-system-hppa": { + "versions": { + "3.1.0-r3": 1551107303 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-hppa" + ] + }, + "font-misc-cyrillic": { + "versions": { + "1.0.3-r0": 1545207028 + }, + "origin": "font-misc-cyrillic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "boost-coroutine": { + "versions": { + "1.67.0-r2": 1542823631 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_context-mt.so.1.67.0", + "so:libboost_system-mt.so.1.67.0", + "so:libboost_system.so.1.67.0", + "so:libboost_thread-mt.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_coroutine-mt.so.1.67.0=1.67.0", + "so:libboost_coroutine.so.1.67.0=1.67.0" + ] + }, + "gvpe": { + "versions": { + "3.1-r0": 1545214041 + }, + "origin": "gvpe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libgmp.so.10", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:gvpe", + "cmd:gvpectrl" + ] + }, + "linux-firmware-rtlwifi": { + "versions": { + "20190322-r0": 1554980648 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "xmlrpc-c-client++": { + "versions": { + "1.39.13-r0": 1545665215 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libxmlrpc++.so.8", + "so:libxmlrpc.so.3", + "so:libxmlrpc_client.so.3", + "so:libxmlrpc_packetsocket.so.8", + "so:libxmlrpc_util++.so.8", + "so:libxmlrpc_util.so.3" + ], + "provides": [ + "so:libxmlrpc_client++.so.8=8.39" + ] + }, + "nagios-plugins-dhcp": { + "versions": { + "2.2.1-r6": 1543933903 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-imagesize": { + "versions": { + "1.1.0-r0": 1542824997 + }, + "origin": "py-imagesize" + }, + "expect-dev": { + "versions": { + "5.45.4-r0": 1543246822 + }, + "origin": "expect" + }, + "consolekit2": { + "versions": { + "1.2.1-r1": 1545062381 + }, + "origin": "consolekit2", + "dependencies": [ + "polkit", + "eudev", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libpolkit-gobject-1.so.0", + "so:libudev.so.1", + "so:libz.so.1" + ], + "provides": [ + "consolekit=1.2.1", + "so:libck-connector.so.0=0.0.0", + "so:libconsolekit.so.1=1.0.0", + "cmd:ck-history", + "cmd:ck-launch-session", + "cmd:ck-list-sessions", + "cmd:ck-log-system-restart", + "cmd:ck-log-system-start", + "cmd:ck-log-system-stop", + "cmd:console-kit-daemon" + ] + }, + "bridge": { + "versions": { + "1.5-r3": 1545062228 + }, + "origin": "bridge" + }, + "linux-firmware-cpia2": { + "versions": { + "20190322-r0": 1554980653 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "figlet": { + "versions": { + "2.2.5-r0": 1545216341 + }, + "origin": "figlet", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chkfont", + "cmd:figlet", + "cmd:figlist", + "cmd:showfigfonts" + ] + }, + "apache2-mod-wsgi-doc": { + "versions": { + "4.5.24-r0": 1545213946 + }, + "origin": "apache2-mod-wsgi" + }, + "xtables-addons-doc": { + "versions": { + "3.2-r0": 1545154536 + }, + "origin": "xtables-addons" + }, + "perl-net-libidn-doc": { + "versions": { + "0.12-r5": 1542924924 + }, + "origin": "perl-net-libidn" + }, + "libelf-dev": { + "versions": { + "0.8.13-r3": 1542900241 + }, + "origin": "libelf", + "dependencies": [ + "libelf=0.8.13-r3", + "pkgconfig" + ], + "provides": [ + "pc:libelf=0.8.13" + ] + }, + "uwsgi-stats_pusher_file": { + "versions": { + "2.0.17.1-r0": 1545062211 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-twitter": { + "versions": { + "3.5-r0": 1546524754 + }, + "origin": "py-twitter", + "dependencies": [ + "py2-future", + "py2-requests", + "py2-requests-oauthlib", + "python2" + ] + }, + "perl-text-csv": { + "versions": { + "1.97-r0": 1545163610 + }, + "origin": "perl-text-csv", + "dependencies": [ + "perl" + ] + }, + "http-parser": { + "versions": { + "2.8.1-r0": 1543934666 + }, + "origin": "http-parser", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libhttp_parser.so.2.8=2.8.1" + ] + }, + "syslog-ng-scl": { + "versions": { + "3.19.1-r0": 1548543149 + }, + "origin": "syslog-ng", + "dependencies": [ + "syslog-ng=3.19.1-r0" + ] + }, + "libotr": { + "versions": { + "4.1.1-r1": 1543248415 + }, + "origin": "libotr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20" + ], + "provides": [ + "so:libotr.so.5=5.1.1" + ] + }, + "openjade-doc": { + "versions": { + "1.3.2-r5": 1542893151 + }, + "origin": "openjade" + }, + "newsboat-doc": { + "versions": { + "2.13-r0": 1545075359 + }, + "origin": "newsboat" + }, + "rsyslog-mmrm1stspace": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "kbd-openrc": { + "versions": { + "2.0.4-r3": 1545215190 + }, + "origin": "kbd", + "dependencies": [ + "kbd-misc" + ] + }, + "lcms-dev": { + "versions": { + "1.19-r8": 1546249797 + }, + "origin": "lcms", + "dependencies": [ + "liblcms=1.19-r8", + "pkgconfig" + ], + "provides": [ + "pc:lcms=1.19" + ] + }, + "fcgiwrap-openrc": { + "versions": { + "1.1.0-r3": 1545665781 + }, + "origin": "fcgiwrap" + }, + "py2-sphinxcontrib-websupport": { + "versions": { + "1.0.1-r3": 1542825034 + }, + "origin": "py-sphinxcontrib-websupport", + "dependencies": [ + "python2" + ] + }, + "perl-test-output": { + "versions": { + "1.031-r0": 1545300174 + }, + "origin": "perl-test-output", + "dependencies": [ + "perl-capture-tiny", + "perl-sub-exporter", + "perl-test-simple" + ] + }, + "libmtp-examples": { + "versions": { + "1.1.15-r0": 1543924441 + }, + "origin": "libmtp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmtp.so.9" + ], + "provides": [ + "cmd:mtp-albumart", + "cmd:mtp-albums", + "cmd:mtp-connect", + "cmd:mtp-delfile", + "cmd:mtp-detect", + "cmd:mtp-emptyfolders", + "cmd:mtp-files", + "cmd:mtp-filetree", + "cmd:mtp-folders", + "cmd:mtp-format", + "cmd:mtp-getfile", + "cmd:mtp-getplaylist", + "cmd:mtp-hotplug", + "cmd:mtp-newfolder", + "cmd:mtp-newplaylist", + "cmd:mtp-playlists", + "cmd:mtp-reset", + "cmd:mtp-sendfile", + "cmd:mtp-sendtr", + "cmd:mtp-thumb", + "cmd:mtp-tracks", + "cmd:mtp-trexist" + ] + }, + "py2-requests-oauthlib": { + "versions": { + "0.8.0-r1": 1545062562 + }, + "origin": "py-requests-oauthlib", + "dependencies": [ + "py2-oauthlib", + "py2-requests", + "python2" + ] + }, + "perl-crypt-openssl-random-doc": { + "versions": { + "0.15-r1": 1543925841 + }, + "origin": "perl-crypt-openssl-random" + }, + "openldap-overlay-ppolicy": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2", + "so:libltdl.so.7" + ] + }, + "acf-skins": { + "versions": { + "0.6.0-r1": 1542883784 + }, + "origin": "acf-skins" + }, + "py2-incremental": { + "versions": { + "17.5.0-r0": 1546513617 + }, + "origin": "py-incremental", + "dependencies": [ + "python2" + ] + }, + "py-webassets": { + "versions": { + "0.12.1-r1": 1545214102 + }, + "origin": "py-webassets", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:webassets" + ] + }, + "wayland": { + "versions": { + "1.16.0-r0": 1544001084 + }, + "origin": "wayland", + "dependencies": [ + "wayland-libs-client", + "wayland-libs-cursor", + "wayland-libs-egl", + "wayland-libs-server" + ] + }, + "lua-posix": { + "versions": { + "33.4.0-r1": 1546010825 + }, + "origin": "lua-posix" + }, + "libmcrypt": { + "versions": { + "2.5.8-r7": 1545066949 + }, + "origin": "libmcrypt", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmcrypt.so.4=4.4.8" + ] + }, + "s6-doc": { + "versions": { + "2.7.2.0-r0": 1545062679 + }, + "origin": "s6" + }, + "syslinux-doc": { + "versions": { + "6.04_pre1-r2": 1543935715 + }, + "origin": "syslinux" + }, + "liblogging": { + "versions": { + "1.0.6-r0": 1545060630 + }, + "origin": "liblogging", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblogging-stdlog.so.0=0.1.0", + "cmd:stdlogctl" + ] + }, + "perl-www-robotrules": { + "versions": { + "6.02-r1": 1542821859 + }, + "origin": "perl-www-robotrules", + "dependencies": [ + "perl", + "perl-uri" + ] + }, + "flite-dev": { + "versions": { + "2.1-r0": 1545117048 + }, + "origin": "flite", + "dependencies": [ + "flite=2.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:flite=2.1" + ] + }, + "squid-lang-vi": { + "versions": { + "4.4-r1": 1545216331 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "open-iscsi-doc": { + "versions": { + "2.0.874-r2": 1547188207 + }, + "origin": "open-iscsi" + }, + "hdparm-doc": { + "versions": { + "9.58-r0": 1545745823 + }, + "origin": "hdparm" + }, + "libvdpau-doc": { + "versions": { + "1.1.1-r2": 1542900270 + }, + "origin": "libvdpau" + }, + "tcpflow-doc": { + "versions": { + "1.5.0-r0": 1545207691 + }, + "origin": "tcpflow" + }, + "libraw1394-doc": { + "versions": { + "2.1.2-r1": 1543932097 + }, + "origin": "libraw1394" + }, + "libconfig++": { + "versions": { + "1.5-r3": 1543249700 + }, + "origin": "libconfig", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libconfig++.so.9=9.2.0" + ] + }, + "ruby-dbm": { + "versions": { + "2.5.5-r0": 1557164836 + }, + "origin": "ruby", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libgdbm_compat.so.4", + "so:libruby.so.2.5" + ] + }, + "libvirt-lang": { + "versions": { + "4.10.0-r1": 1547051657 + }, + "origin": "libvirt", + "dependencies": [ + "lvm2" + ] + }, + "mysql": { + "versions": { + "10.3.13-r1": 1557431735 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb" + ] + }, + "collectd-redis": { + "versions": { + "5.8.0-r3": 1546422677 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.14" + ] + }, + "qemu-mips64": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mips64" + ] + }, + "uwsgi-stats_pusher_socket": { + "versions": { + "2.0.17.1-r0": 1545062211 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libsamplerate-dev": { + "versions": { + "0.1.9-r1": 1545068552 + }, + "origin": "libsamplerate", + "dependencies": [ + "libsamplerate=0.1.9-r1", + "pkgconfig" + ], + "provides": [ + "pc:samplerate=0.1.9" + ] + }, + "nano-syntax": { + "versions": { + "3.2-r0": 1545292880 + }, + "origin": "nano" + }, + "gsl-doc": { + "versions": { + "2.5-r0": 1545117968 + }, + "origin": "gsl" + }, + "perl-convert-tnef": { + "versions": { + "0.18-r1": 1544001412 + }, + "origin": "perl-convert-tnef", + "dependencies": [ + "perl" + ] + }, + "s6-linux-utils-doc": { + "versions": { + "2.5.0.0-r0": 1545299629 + }, + "origin": "s6-linux-utils" + }, + "tmux-doc": { + "versions": { + "2.8-r0": 1546590688 + }, + "origin": "tmux" + }, + "perl-convert-uulib-doc": { + "versions": { + "1.5-r2": 1544001423 + }, + "origin": "perl-convert-uulib" + }, + "lsyncd-openrc": { + "versions": { + "2.2.3-r1": 1545253929 + }, + "origin": "lsyncd", + "dependencies": [ + "rsync" + ] + }, + "linux-firmware-ueagle-atm": { + "versions": { + "20190322-r0": 1554980647 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "qemu-aarch64_be": { + "versions": { + "3.1.0-r3": 1551107299 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-aarch64_be" + ] + }, + "libdbi-doc": { + "versions": { + "0.9.0-r0": 1543932516 + }, + "origin": "libdbi" + }, + "mdadm-doc": { + "versions": { + "4.1-r0": 1545858162 + }, + "origin": "mdadm" + }, + "linux-firmware-keyspan_pda": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "openldap-back-hdb": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "perl-net-dns-resolver-programmable-doc": { + "versions": { + "0.009-r1": 1542925012 + }, + "origin": "perl-net-dns-resolver-programmable" + }, + "libnice-dev": { + "versions": { + "0.1.14-r2": 1543928834 + }, + "origin": "libnice", + "dependencies": [ + "libnice=0.1.14-r2", + "pc:gio-2.0>=2.44", + "pc:glib-2.0>=2.44", + "pc:gnutls>=2.12.0", + "pc:gobject-2.0>=2.44", + "pc:gthread-2.0", + "pkgconfig" + ], + "provides": [ + "pc:nice=0.1.14" + ] + }, + "samba-pidl": { + "versions": { + "4.8.11-r1": 1555334975 + }, + "origin": "samba", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:pidl" + ] + }, + "ssmtp": { + "versions": { + "2.64-r14": 1545215006 + }, + "origin": "ssmtp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:sendmail", + "cmd:ssmtp" + ] + }, + "xf86-video-savage-doc": { + "versions": { + "2.3.9-r3": 1545163475 + }, + "origin": "xf86-video-savage" + }, + "xscreensaver-gl-extras": { + "versions": { + "5.40-r0": 1545224087 + }, + "origin": "xscreensaver", + "dependencies": [ + "bc", + "so:libGL.so.1", + "so:libGLU.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXmu.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgdk_pixbuf_xlib-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "cmd:xscreensaver-gl-helper" + ] + }, + "nss-pam-ldapd-doc": { + "versions": { + "0.9.8-r0": 1542845538 + }, + "origin": "nss-pam-ldapd" + }, + "font-dec-misc": { + "versions": { + "1.0.3-r0": 1545302110 + }, + "origin": "font-dec-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "openvpn": { + "versions": { + "2.4.6-r4": 1543223245 + }, + "origin": "openvpn", + "dependencies": [ + "iproute2", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:liblzo2.so.2", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:openvpn" + ] + }, + "db-dev": { + "versions": { + "5.3.28-r1": 1542819052 + }, + "origin": "db", + "dependencies": [ + "db-c++=5.3.28-r1", + "db=5.3.28-r1" + ] + }, + "lua5.3-lub": { + "versions": { + "1.1.0-r1": 1544000684 + }, + "origin": "lua-lub", + "dependencies": [ + "lua5.3", + "lua5.3-filesystem" + ] + }, + "perl-crypt-rijndael": { + "versions": { + "1.13-r0": 1543923071 + }, + "origin": "perl-crypt-rijndael", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "aria2-doc": { + "versions": { + "1.34.0-r1": 1543998971 + }, + "origin": "aria2" + }, + "spl-doc": { + "versions": { + "0.7.8-r0": 1545117988 + }, + "origin": "spl" + }, + "libice-doc": { + "versions": { + "1.0.9-r3": 1542824333 + }, + "origin": "libice" + }, + "mailx-doc": { + "versions": { + "8.1.1-r1": 1545075408 + }, + "origin": "mailx" + }, + "fts": { + "versions": { + "1.2.7-r1": 1543933813 + }, + "origin": "fts", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfts.so.0=0.0.0" + ] + }, + "perl-date-extract": { + "versions": { + "0.06-r1": 1545163121 + }, + "origin": "perl-date-extract", + "dependencies": [ + "perl-datetime-format-natural", + "perl-class-data-inheritable", + "perl-test-mocktime" + ] + }, + "apache2-ssl": { + "versions": { + "2.4.39-r0": 1554306836 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "openssl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "eggdrop-logs2html": { + "versions": { + "1.6.21-r2": 1545206947 + }, + "origin": "eggdrop", + "dependencies": [ + "tcl", + "so:libc.musl-x86_64.so.1" + ] + }, + "gptfdisk": { + "versions": { + "1.0.4-r0": 1543926506 + }, + "origin": "gptfdisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:cgdisk", + "cmd:fixparts", + "cmd:gdisk" + ] + }, + "speedtest-cli": { + "versions": { + "2.0.2-r0": 1545209109 + }, + "origin": "speedtest-cli", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:speedtest", + "cmd:speedtest-cli" + ] + }, + "ksymoops-doc": { + "versions": { + "2.4.11-r7": 1545060802 + }, + "origin": "ksymoops" + }, + "pjproject-dev": { + "versions": { + "2.7.2-r4": 1545073632 + }, + "origin": "pjproject", + "dependencies": [ + "openssl-dev", + "alsa-lib-dev", + "gsm-dev", + "speex-dev", + "speexdsp-dev", + "portaudio-dev", + "libsrtp-dev", + "libsamplerate-dev", + "pjproject=2.7.2-r4", + "pkgconfig" + ], + "provides": [ + "pc:libpjproject=2.7.2" + ] + }, + "e2fsprogs": { + "versions": { + "1.44.5-r0": 1545745858 + }, + "origin": "e2fsprogs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libe2p.so.2", + "so:libext2fs.so.2", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:e2fsck", + "cmd:fsck.ext2", + "cmd:fsck.ext3", + "cmd:fsck.ext4", + "cmd:mke2fs", + "cmd:mkfs.ext2", + "cmd:mkfs.ext3", + "cmd:mkfs.ext4" + ] + }, + "virglrenderer": { + "versions": { + "0.7.0-r1": 1547472270 + }, + "origin": "virglrenderer", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1" + ], + "provides": [ + "so:libvirglrenderer.so.0=0.2.0", + "cmd:virgl_test_server" + ] + }, + "lua5.3-yaml": { + "versions": { + "1.1.2-r1": 1544000071 + }, + "origin": "lua-yaml", + "dependencies": [ + "lua5.3", + "lua5.3-lub", + "so:libc.musl-x86_64.so.1" + ] + }, + "tslib-dev": { + "versions": { + "1.18-r0": 1545859355 + }, + "origin": "tslib", + "dependencies": [ + "pkgconfig", + "tslib=1.18-r0" + ], + "provides": [ + "pc:tslib=1.18" + ] + }, + "lua5.1-sec": { + "versions": { + "0.7-r1": 1545075388 + }, + "origin": "lua-sec", + "dependencies": [ + "lua5.1-socket", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "gst-plugins-base0.10-doc": { + "versions": { + "0.10.36-r4": 1543928810 + }, + "origin": "gst-plugins-base0.10" + }, + "py-jwt-cli": { + "versions": { + "1.6.4-r0": 1543249952 + }, + "origin": "py-jwt", + "dependencies": [ + "py3-jwt=1.6.4-r0" + ], + "provides": [ + "cmd:pyjwt" + ] + }, + "perl-test-mockobject-doc": { + "versions": { + "1.20180705-r0": 1545116825 + }, + "origin": "perl-test-mockobject" + }, + "libao-doc": { + "versions": { + "1.2.0-r3": 1545067193 + }, + "origin": "libao" + }, + "vte-dev": { + "versions": { + "0.28.2-r13": 1543927664 + }, + "origin": "vte", + "dependencies": [ + "pango-dev", + "gtk+2.0-dev", + "pc:cairo-xlib", + "pc:gio-2.0", + "pc:gio-unix-2.0", + "pc:glib-2.0>=2.26.0", + "pc:gobject-2.0", + "pc:gtk+-2.0>=2.20.0", + "pc:pango>=1.22.0", + "pc:x11", + "pkgconfig", + "vte=0.28.2-r13" + ], + "provides": [ + "pc:pyvte=0.28.2", + "pc:vte=0.28.2" + ] + }, + "perl-net-snmp": { + "versions": { + "6.0.1-r2": 1543923076 + }, + "origin": "perl-net-snmp", + "dependencies": [ + "perl-crypt-des", + "perl-crypt-rijndael", + "perl-digest-sha1" + ], + "provides": [ + "cmd:snmpkey" + ] + }, + "mariadb-connector-c-dev": { + "versions": { + "3.0.8-r0": 1547470538 + }, + "origin": "mariadb-connector-c", + "dependencies": [ + "openssl-dev", + "zlib-dev", + "mariadb-connector-c=3.0.8-r0", + "pkgconfig", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "pc:libmariadb=3.0.8", + "cmd:mariadb_config", + "cmd:mysql_config" + ] + }, + "btrfs-progs-doc": { + "versions": { + "4.19.1-r0": 1545665043 + }, + "origin": "btrfs-progs" + }, + "font-sony-misc": { + "versions": { + "1.0.3-r1": 1542924740 + }, + "origin": "font-sony-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "vte-lang": { + "versions": { + "0.28.2-r13": 1543927668 + }, + "origin": "vte" + }, + "perl-io-socket-inet6": { + "versions": { + "2.72-r0": 1543932733 + }, + "origin": "perl-io-socket-inet6", + "dependencies": [ + "perl-socket6" + ] + }, + "perl-list-someutils-xs-doc": { + "versions": { + "0.55-r0": 1543934252 + }, + "origin": "perl-list-someutils-xs" + }, + "bubblewrap-bash-completion": { + "versions": { + "0.3.1-r0": 1546247687 + }, + "origin": "bubblewrap" + }, + "cyrus-sasl": { + "versions": { + "2.1.27-r1": 1550353528 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:libkrb5.so.3", + "so:libsasl2.so.3" + ], + "provides": [ + "cmd:pluginviewer", + "cmd:saslauthd", + "cmd:sasldblistusers2", + "cmd:saslpasswd2", + "cmd:testsaslauthd" + ] + }, + "py3-psycopg2": { + "versions": { + "2.7.5-r0": 1544000430 + }, + "origin": "py-psycopg2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5", + "so:libpython3.6m.so.1.0" + ] + }, + "grub-bios": { + "versions": { + "2.02-r14": 1548432370 + }, + "origin": "grub", + "dependencies": [ + "grub" + ] + }, + "rabbitmq-c-utils": { + "versions": { + "0.8.0-r5": 1543923901 + }, + "origin": "rabbitmq-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0", + "so:librabbitmq.so.4" + ], + "provides": [ + "cmd:amqp-consume", + "cmd:amqp-declare-queue", + "cmd:amqp-delete-queue", + "cmd:amqp-get", + "cmd:amqp-publish" + ] + }, + "py-setuptools": { + "versions": { + "40.6.3-r0": 1547652242 + }, + "origin": "py-setuptools", + "dependencies": [ + "python2" + ], + "provides": [ + "py2-setuptools=40.6.3-r0", + "cmd:easy_install-2.7" + ] + }, + "uwsgi-router_static": { + "versions": { + "2.0.17.1-r0": 1545062208 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "bearssl-dev": { + "versions": { + "0.6-r0": 1546957733 + }, + "origin": "bearssl", + "dependencies": [ + "bearssl=0.6-r0" + ] + }, + "perl-time-modules-doc": { + "versions": { + "2013.0912-r0": 1545061985 + }, + "origin": "perl-time-modules" + }, + "libisoburn-dev": { + "versions": { + "1.4.8-r0": 1544797303 + }, + "origin": "libisoburn", + "dependencies": [ + "libisoburn=1.4.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:libisoburn-1=1.4.8" + ] + }, + "enca-doc": { + "versions": { + "1.19-r1": 1545076572 + }, + "origin": "enca" + }, + "msmtp-vim": { + "versions": { + "1.8.1-r1": 1545062528 + }, + "origin": "msmtp" + }, + "acf-snort": { + "versions": { + "0.8.0-r2": 1545163905 + }, + "origin": "acf-snort", + "dependencies": [ + "acf-core", + "snort" + ] + }, + "nagios-plugins-hpjd": { + "versions": { + "2.2.1-r6": 1543933905 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "libtirpc-dbg": { + "versions": { + "1.0.3-r0": 1543223617 + }, + "origin": "libtirpc" + }, + "devicemaster-linux-vanilla": { + "versions": { + "4.19.41-r0": 1557400832 + }, + "origin": "devicemaster-linux-vanilla", + "dependencies": [ + "linux-vanilla=4.19.41-r0" + ] + }, + "bacula": { + "versions": { + "9.4.1-r1": 1546944088 + }, + "origin": "bacula", + "dependencies": [ + "/bin/sh", + "so:libbac-9.4.1.so", + "so:libbaccfg-9.4.1.so", + "so:libbacfind-9.4.1.so", + "so:libbacsd-9.4.1.so", + "so:libbacsql-9.4.1.so", + "so:libc.musl-x86_64.so.1", + "so:liblzo2.so.2", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:bacula-dir", + "cmd:bacula-sd", + "cmd:bbconsjson", + "cmd:bcopy", + "cmd:bdirjson", + "cmd:bextract", + "cmd:bfdjson", + "cmd:bls", + "cmd:bregex", + "cmd:bscan", + "cmd:bsdjson", + "cmd:bsmtp", + "cmd:btape", + "cmd:bwild", + "cmd:dbcheck" + ] + }, + "dhcp-dev": { + "versions": { + "4.4.1-r1": 1543928451 + }, + "origin": "dhcp" + }, + "perl-sub-info-doc": { + "versions": { + "0.002-r0": 1542972994 + }, + "origin": "perl-sub-info" + }, + "linux-firmware-go7007": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "perl-net-cidr": { + "versions": { + "0.19-r0": 1544000981 + }, + "origin": "perl-net-cidr", + "dependencies": [ + "perl" + ] + }, + "dosfstools": { + "versions": { + "4.1-r1": 1545163465 + }, + "origin": "dosfstools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dosfsck", + "cmd:dosfslabel", + "cmd:fatlabel", + "cmd:fsck.fat", + "cmd:fsck.msdos", + "cmd:fsck.vfat", + "cmd:mkdosfs", + "cmd:mkfs.fat", + "cmd:mkfs.msdos", + "cmd:mkfs.vfat" + ] + }, + "perl-data-page-pageset-doc": { + "versions": { + "1.02-r1": 1545213962 + }, + "origin": "perl-data-page-pageset" + }, + "git-gitweb": { + "versions": { + "2.20.1-r0": 1545214195 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0", + "perl" + ] + }, + "perl-devel-stacktrace": { + "versions": { + "2.03-r0": 1542845798 + }, + "origin": "perl-devel-stacktrace" + }, + "userspace-rcu-doc": { + "versions": { + "0.10.1-r0": 1543249589 + }, + "origin": "userspace-rcu" + }, + "libmicrohttpd-dev": { + "versions": { + "0.9.62-r0": 1545300153 + }, + "origin": "libmicrohttpd", + "dependencies": [ + "libmicrohttpd=0.9.62-r0", + "pc:gnutls", + "pkgconfig" + ], + "provides": [ + "pc:libmicrohttpd=0.9.62" + ] + }, + "font-util-dev": { + "versions": { + "1.3.1-r2": 1542924710 + }, + "origin": "font-util", + "dependencies": [ + "font-util", + "pkgconfig" + ], + "provides": [ + "pc:fontutil=1.3.1" + ] + }, + "perl-date-format": { + "versions": { + "2.30-r0": 1543924842 + }, + "origin": "perl-date-format", + "dependencies": [ + "perl" + ] + }, + "perl-devel-stacktrace-doc": { + "versions": { + "2.03-r0": 1542845798 + }, + "origin": "perl-devel-stacktrace" + }, + "pcsc-lite-libs": { + "versions": { + "1.8.24-r1": 1545060761 + }, + "origin": "pcsc-lite", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcsclite.so.1=1.0.0", + "so:libpcscspy.so.0=0.0.0" + ] + }, + "libvdpau": { + "versions": { + "1.1.1-r2": 1542900270 + }, + "origin": "libvdpau", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libvdpau.so.1=1.0.0" + ] + }, + "py-gobject-doc": { + "versions": { + "2.28.7-r0": 1543927527 + }, + "origin": "py-gobject" + }, + "aspell-en": { + "versions": { + "2018.04.16-r0": 1545069363 + }, + "origin": "aspell-en" + }, + "ldapvi": { + "versions": { + "1.7-r10": 1545117995 + }, + "origin": "ldapvi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libglib-2.0.so.0", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libncursesw.so.6", + "so:libpopt.so.0", + "so:libreadline.so.7" + ], + "provides": [ + "cmd:ldapvi" + ] + }, + "libvirt-client": { + "versions": { + "4.10.0-r1": 1547051657 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt", + "pm-utils", + "gnutls-utils", + "netcat-openbsd", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libreadline.so.7", + "so:libtirpc.so.3", + "so:libvirt-admin.so.0", + "so:libvirt-lxc.so.0", + "so:libvirt-qemu.so.0", + "so:libvirt.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:virsh", + "cmd:virt-admin", + "cmd:virt-host-validate", + "cmd:virt-login-shell", + "cmd:virt-pki-validate", + "cmd:virt-xml-validate" + ] + }, + "libnet-dev": { + "versions": { + "1.1.6-r2": 1545060612 + }, + "origin": "libnet", + "dependencies": [ + "libnet=1.1.6-r2" + ], + "provides": [ + "cmd:libnet-config" + ] + }, + "py3-libvirt": { + "versions": { + "4.10.0-r0": 1545291074 + }, + "origin": "py-libvirt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0", + "so:libvirt-lxc.so.0", + "so:libvirt-qemu.so.0", + "so:libvirt.so.0" + ] + }, + "ruby-gdbm": { + "versions": { + "2.5.5-r0": 1557164836 + }, + "origin": "ruby", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libruby.so.2.5" + ] + }, + "py-idna": { + "versions": { + "2.7-r0": 1542825010 + }, + "origin": "py-idna" + }, + "openjpeg-tools": { + "versions": { + "2.3.0-r3": 1552584664 + }, + "origin": "openjpeg", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblcms2.so.2", + "so:libopenjp2.so.7", + "so:libpng16.so.16", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:opj_compress", + "cmd:opj_decompress", + "cmd:opj_dump" + ] + }, + "qemu-system-arm": { + "versions": { + "3.1.0-r3": 1551107303 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libstdc++.so.6", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-arm" + ] + }, + "py3-tevent": { + "versions": { + "0.9.37-r1": 1543933087 + }, + "origin": "tevent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0", + "so:libtalloc.so.2", + "so:libtevent.so.0" + ] + }, + "py3-sphinxcontrib-websupport": { + "versions": { + "1.0.1-r3": 1542825037 + }, + "origin": "py-sphinxcontrib-websupport", + "dependencies": [ + "python3" + ] + }, + "nginx": { + "versions": { + "1.14.2-r1": 1557179822 + }, + "origin": "nginx", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpcre.so.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:nginx" + ] + }, + "collectd-postgresql": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "altermime-doc": { + "versions": { + "0.3.11-r0": 1543222722 + }, + "origin": "altermime" + }, + "openldap-mqtt": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "openldap", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libmosquitto.so.1" + ] + }, + "acf-weblog": { + "versions": { + "0.11.1-r1": 1545301038 + }, + "origin": "acf-weblog", + "dependencies": [ + "acf-core", + "lua-sql-postgres", + "wget", + "postgresql-client", + "lua-subprocess", + "/bin/sh" + ], + "provides": [ + "cmd:acf-weblog-update-schema" + ] + }, + "perl-carp-clan": { + "versions": { + "6.06-r1": 1545116510 + }, + "origin": "perl-carp-clan", + "dependencies": [ + "perl", + "perl-test-exception" + ] + }, + "libvirt-daemon": { + "versions": { + "4.10.0-r1": 1547051657 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-client", + "bridge-utils", + "dmidecode", + "dnsmasq", + "ebtables", + "ip6tables", + "iptables", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libintl.so.8", + "so:libtirpc.so.3", + "so:libvirt-lxc.so.0", + "so:libvirt-qemu.so.0", + "so:libvirt.so.0" + ], + "provides": [ + "cmd:libvirtd" + ] + }, + "logcheck-doc": { + "versions": { + "1.3.19-r0": 1545857328 + }, + "origin": "logcheck" + }, + "umix-doc": { + "versions": { + "1.0.2-r7": 1545235248 + }, + "origin": "umix" + }, + "acf-dnsmasq": { + "versions": { + "0.7.1-r0": 1545299894 + }, + "origin": "acf-dnsmasq", + "dependencies": [ + "acf-core", + "dnsmasq" + ] + }, + "perl-time-hires-doc": { + "versions": { + "1.9758-r0": 1545061217 + }, + "origin": "perl-time-hires" + }, + "sudo-doc": { + "versions": { + "1.8.25_p1-r2": 1542304826 + }, + "origin": "sudo" + }, + "uwsgi-logsocket": { + "versions": { + "2.0.17.1-r0": 1545062202 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "czmq": { + "versions": { + "4.1.1-r0": 1548683467 + }, + "origin": "czmq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1", + "so:libzmq.so.5" + ], + "provides": [ + "so:libczmq.so.4=4.1.1", + "cmd:zmakecert" + ] + }, + "strongswan-dbg": { + "versions": { + "5.7.2-r1": 1557161394 + }, + "origin": "strongswan", + "dependencies": [ + "iproute2" + ] + }, + "nginx-mod-http-fancyindex": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "ghi": { + "versions": { + "1.2.0-r3": 1544000928 + }, + "origin": "ghi", + "dependencies": [ + "ruby", + "ruby-json", + "ncurses", + "less" + ], + "provides": [ + "cmd:ghi" + ] + }, + "libqrencode-doc": { + "versions": { + "4.0.2-r0": 1543226702 + }, + "origin": "libqrencode" + }, + "openipmi-doc": { + "versions": { + "2.0.25-r1": 1545214270 + }, + "origin": "openipmi" + }, + "perl-test-nowarnings-doc": { + "versions": { + "1.04-r1": 1542845589 + }, + "origin": "perl-test-nowarnings" + }, + "uwsgi-zergpool": { + "versions": { + "2.0.17.1-r0": 1545062215 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libxres": { + "versions": { + "1.2.0-r1": 1545302056 + }, + "origin": "libxres", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXRes.so.1=1.0.0" + ] + }, + "lang": { + "versions": { + "0.1-r0": 1545293179 + }, + "origin": "lang" + }, + "qemu-system-lm32": { + "versions": { + "3.1.0-r3": 1551107303 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libX11.so.6", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-lm32" + ] + }, + "fuse-dev": { + "versions": { + "2.9.8-r2": 1542972242 + }, + "origin": "fuse", + "dependencies": [ + "fuse=2.9.8-r2", + "pkgconfig" + ], + "provides": [ + "pc:fuse=2.9.8" + ] + }, + "tftp-hpa": { + "versions": { + "5.2-r2": 1545207837 + }, + "origin": "tftp-hpa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:in.tftpd", + "cmd:tftp" + ] + }, + "xorg-server-xnest": { + "versions": { + "1.20.3-r1": 1543928069 + }, + "origin": "xorg-server", + "dependencies": [ + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXext.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpixman-1.so.0" + ], + "provides": [ + "cmd:Xnest" + ] + }, + "libxfixes": { + "versions": { + "5.0.3-r2": 1542823751 + }, + "origin": "libxfixes", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXfixes.so.3=3.1.0" + ] + }, + "mdocml-dev": { + "versions": { + "1.14.3-r0": 1542304778 + }, + "origin": "mdocml" + }, + "samba-initscript": { + "versions": { + "4.8.11-r1": 1555334972 + }, + "origin": "samba" + }, + "bacula-pgsql": { + "versions": { + "9.4.1-r1": 1546944087 + }, + "origin": "bacula", + "dependencies": [ + "bacula", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:libbaccats-postgresql-9.4.1.so=0" + ] + }, + "xf86-video-s3-doc": { + "versions": { + "0.6.5-r11": 1544792314 + }, + "origin": "xf86-video-s3" + }, + "font-cronyx-cyrillic": { + "versions": { + "1.0.3-r0": 1545165157 + }, + "origin": "font-cronyx-cyrillic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "lua-file-magic": { + "versions": { + "0.2-r1": 1545116950 + }, + "origin": "lua-file-magic", + "dependencies": [ + "lua" + ] + }, + "perl-test-manifest-doc": { + "versions": { + "2.021-r0": 1543932508 + }, + "origin": "perl-test-manifest" + }, + "libdv-doc": { + "versions": { + "1.0.0-r3": 1543999340 + }, + "origin": "libdv" + }, + "sysklogd-doc": { + "versions": { + "1.5.1-r1": 1545213702 + }, + "origin": "sysklogd" + }, + "py3-openvswitch": { + "versions": { + "2.10.1-r0": 1544000345 + }, + "origin": "openvswitch", + "dependencies": [ + "py3-six" + ] + }, + "nmap-ncat": { + "versions": { + "7.70-r3": 1545163173 + }, + "origin": "nmap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:liblua-5.3.so.0", + "so:libpcap.so.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:ncat" + ] + }, + "perl-pathtools": { + "versions": { + "3.75-r0": 1545163545 + }, + "origin": "perl-pathtools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "lxc-dev": { + "versions": { + "3.1.0-r1": 1549966160 + }, + "origin": "lxc", + "dependencies": [ + "lxc-libs=3.1.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:lxc=3.1.0" + ] + }, + "lksctp-tools-doc": { + "versions": { + "1.0.17-r0": 1543241223 + }, + "origin": "lksctp-tools" + }, + "ghi-doc": { + "versions": { + "1.2.0-r3": 1544000928 + }, + "origin": "ghi" + }, + "perl-date-manip-doc": { + "versions": { + "6.75-r1": 1546098110 + }, + "origin": "perl-date-manip" + }, + "txt2man": { + "versions": { + "1.6.0-r0": 1545076644 + }, + "origin": "txt2man", + "dependencies": [ + "gawk" + ], + "provides": [ + "cmd:bookman", + "cmd:src2man", + "cmd:txt2man" + ] + }, + "qpdf-doc": { + "versions": { + "8.3.0-r0": 1547117323 + }, + "origin": "qpdf" + }, + "lvm2-dmeventd": { + "versions": { + "2.02.182-r0": 1543928933 + }, + "origin": "lvm2", + "dependencies": [ + "lvm2-libs=2.02.182-r0", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper-event.so.1.02", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "cmd:dmeventd" + ] + }, + "perl-extutils-config-doc": { + "versions": { + "0.008-r0": 1542924640 + }, + "origin": "perl-extutils-config" + }, + "fetchmail-doc": { + "versions": { + "6.3.26-r14": 1544001391 + }, + "origin": "fetchmail" + }, + "perl-cgi-psgi": { + "versions": { + "0.15-r1": 1545208756 + }, + "origin": "perl-cgi-psgi", + "dependencies": [ + "perl-cgi" + ] + }, + "pciutils": { + "versions": { + "3.6.2-r0": 1543921867 + }, + "origin": "pciutils", + "dependencies": [ + "hwdata-pci", + "so:libc.musl-x86_64.so.1", + "so:libpci.so.3" + ], + "provides": [ + "cmd:lspci", + "cmd:setpci", + "cmd:update-pciids" + ] + }, + "celt051-dev": { + "versions": { + "0.5.1.3-r0": 1543935760 + }, + "origin": "celt051", + "dependencies": [ + "celt051=0.5.1.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:celt051=0.5.1.3" + ] + }, + "gstreamer0.10-dev": { + "versions": { + "0.10.36-r3": 1544000752 + }, + "origin": "gstreamer0.10", + "dependencies": [ + "glib-dev", + "libxml2-dev", + "gstreamer0.10=0.10.36-r3", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-0.10=0.10.36", + "pc:gstreamer-base-0.10=0.10.36", + "pc:gstreamer-check-0.10=0.10.36", + "pc:gstreamer-controller-0.10=0.10.36", + "pc:gstreamer-dataprotocol-0.10=0.10.36", + "pc:gstreamer-net-0.10=0.10.36" + ] + }, + "fsarchiver-doc": { + "versions": { + "0.8.5-r0": 1543226651 + }, + "origin": "fsarchiver" + }, + "dillo-doc": { + "versions": { + "3.0.5-r6": 1545302349 + }, + "origin": "dillo" + }, + "py3-sphinx_rtd_theme": { + "versions": { + "0.4.2-r0": 1544791909 + }, + "origin": "py-sphinx_rtd_theme", + "dependencies": [ + "python3" + ] + }, + "graphviz-graphs": { + "versions": { + "2.40.1-r1": 1543926741 + }, + "origin": "graphviz" + }, + "at-spi2-core-dbg": { + "versions": { + "2.28.0-r0": 1543241207 + }, + "origin": "at-spi2-core" + }, + "eventlog-dev": { + "versions": { + "0.2.12-r4": 1545118013 + }, + "origin": "eventlog", + "dependencies": [ + "eventlog=0.2.12-r4", + "pkgconfig" + ], + "provides": [ + "pc:eventlog=0.2.12" + ] + }, + "squid-lang-lt": { + "versions": { + "4.4-r1": 1545216327 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "iw": { + "versions": { + "4.14-r0": 1543249946 + }, + "origin": "iw", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnl-3.so.200", + "so:libnl-genl-3.so.200" + ], + "provides": [ + "cmd:iw" + ] + }, + "uwsgi-emperor_amqp": { + "versions": { + "2.0.17.1-r0": 1545062199 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.2-sql-odbc": { + "versions": { + "2.3.5-r2": 1543924402 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "mariadb-doc": { + "versions": { + "10.3.13-r1": 1557431733 + }, + "origin": "mariadb" + }, + "dbus-glib": { + "versions": { + "0.108-r1": 1542824780 + }, + "origin": "dbus-glib", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libexpat.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libdbus-glib-1.so.2=2.3.3", + "cmd:dbus-binding-tool" + ] + }, + "sparsehash-doc": { + "versions": { + "2.0.3-r0": 1545249827 + }, + "origin": "sparsehash" + }, + "kamailio-tls": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libssl.so.1.1" + ] + }, + "krb5-dev": { + "versions": { + "1.15.5-r0": 1548094457 + }, + "origin": "krb5", + "dependencies": [ + "e2fsprogs-dev", + "krb5-libs=1.15.5-r0", + "krb5-server-ldap=1.15.5-r0", + "pkgconfig" + ], + "provides": [ + "pc:gssrpc=1.15.5", + "pc:kadm-client=1.15.5", + "pc:kadm-server=1.15.5", + "pc:kdb=1.15.5", + "pc:krb5-gssapi=1.15.5", + "pc:krb5=1.15.5", + "pc:mit-krb5-gssapi=1.15.5", + "pc:mit-krb5=1.15.5", + "cmd:krb5-config" + ] + }, + "fprobe-doc": { + "versions": { + "1.1-r8": 1545213730 + }, + "origin": "fprobe" + }, + "gtk+3.0-dbg": { + "versions": { + "3.24.1-r0": 1543927436 + }, + "origin": "gtk+3.0", + "dependencies": [ + "shared-mime-info", + "gtk-update-icon-cache" + ] + }, + "freeswitch-pgsql": { + "versions": { + "1.8.2-r1": 1545117893 + }, + "origin": "freeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeswitch.so.1", + "so:libpq.so.5" + ] + }, + "libxfont": { + "versions": { + "1.5.4-r1": 1542924729 + }, + "origin": "libxfont", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfontenc.so.1", + "so:libfreetype.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libXfont.so.1=1.4.1" + ] + }, + "py2-typing": { + "versions": { + "3.6.6-r0": 1543921909 + }, + "origin": "py-typing", + "dependencies": [ + "python2" + ] + }, + "wayland-libs-egl": { + "versions": { + "1.16.0-r0": 1544001083 + }, + "origin": "wayland", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libwayland-egl.so.1=1.0.0" + ] + }, + "lua5.1-stringy": { + "versions": { + "0.5.0-r1": 1545116589 + }, + "origin": "lua-stringy", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "pinentry-gtk": { + "versions": { + "1.1.0-r0": 1543932176 + }, + "origin": "pinentry", + "dependencies": [ + "/bin/sh", + "so:libassuan.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgpg-error.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:pinentry-gtk-2" + ] + }, + "rsync-doc": { + "versions": { + "3.1.3-r1": 1542820862 + }, + "origin": "rsync" + }, + "py2-curl": { + "versions": { + "7.43.0.2-r1": 1543935132 + }, + "origin": "py-curl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libpython2.7.so.1.0", + "so:libssl.so.1.1" + ] + }, + "uwsgi-pty": { + "versions": { + "2.0.17.1-r0": 1545062204 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-firmware-advansys": { + "versions": { + "20190322-r0": 1554980655 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "acf-provisioning": { + "versions": { + "0.10.1-r0": 1545163176 + }, + "origin": "acf-provisioning", + "dependencies": [ + "acf-core", + "lua-sql-postgres", + "postgresql-client", + "lua-posixtz", + "lua-xml" + ] + }, + "perl-module-refresh": { + "versions": { + "0.17-r1": 1545292779 + }, + "origin": "perl-module-refresh" + }, + "cvs-doc": { + "versions": { + "1.11.23-r0": 1542893528 + }, + "origin": "cvs" + }, + "at-spi2-core-dev": { + "versions": { + "2.28.0-r0": 1543241208 + }, + "origin": "at-spi2-core", + "dependencies": [ + "libxtst-dev", + "at-spi2-core=2.28.0-r0", + "pc:dbus-1", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:atspi-2=2.28.0" + ] + }, + "perl-log-dispatch-doc": { + "versions": { + "2.68-r0": 1545209654 + }, + "origin": "perl-log-dispatch" + }, + "perl-test-mockrandom-doc": { + "versions": { + "1.01-r1": 1543238910 + }, + "origin": "perl-test-mockrandom" + }, + "perl-module-runtime": { + "versions": { + "0.016-r1": 1542845695 + }, + "origin": "perl-module-runtime", + "dependencies": [ + "perl-params-classify", + "perl-module-build" + ] + }, + "libunistring-doc": { + "versions": { + "0.9.10-r0": 1542304035 + }, + "origin": "libunistring" + }, + "brlaser": { + "versions": { + "3-r0": 1545214282 + }, + "origin": "brlaser", + "dependencies": [ + "cups-filters", + "so:libc.musl-x86_64.so.1", + "so:libcupsimage.so.2", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ] + }, + "bash-dbg": { + "versions": { + "4.4.19-r1": 1542301270 + }, + "origin": "bash" + }, + "the_silver_searcher-bash-completion": { + "versions": { + "2.1.0-r2": 1543998780 + }, + "origin": "the_silver_searcher" + }, + "directfb-dev": { + "versions": { + "1.7.7-r1": 1543934412 + }, + "origin": "directfb", + "dependencies": [ + "directfb=1.7.7-r1", + "pkgconfig" + ], + "provides": [ + "pc:++dfb=1.7.7", + "pc:direct=1.7.7", + "pc:directfb-internal=1.7.7", + "pc:directfb=1.7.7", + "pc:fusion=1.7.7", + "cmd:directfb-config" + ] + }, + "perl-params-util": { + "versions": { + "1.07-r5": 1542845640 + }, + "origin": "perl-params-util", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-jinja2": { + "versions": { + "2.10-r2": 1542824938 + }, + "origin": "py-jinja2", + "dependencies": [ + "py-markupsafe" + ] + }, + "apg": { + "versions": { + "2.2.3-r4": 1545207842 + }, + "origin": "apg", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:apg", + "cmd:apgbfm" + ] + }, + "libnjb": { + "versions": { + "2.2.7-r4": 1545300193 + }, + "origin": "libnjb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-0.1.so.4" + ], + "provides": [ + "so:libnjb.so.5=5.1.1" + ] + }, + "nmap-nselibs": { + "versions": { + "7.70-r3": 1545163173 + }, + "origin": "nmap" + }, + "perl-test-mockmodule-doc": { + "versions": { + "0.15-r0": 1543932008 + }, + "origin": "perl-test-mockmodule" + }, + "perl-test-warnings": { + "versions": { + "0.026-r0": 1542845612 + }, + "origin": "perl-test-warnings" + }, + "apr-dev": { + "versions": { + "1.6.5-r0": 1544000273 + }, + "origin": "apr", + "dependencies": [ + "apr", + "util-linux-dev", + "bash", + "pkgconfig" + ], + "provides": [ + "pc:apr-1=1.6.5", + "cmd:apr-1-config" + ] + }, + "libmilter-dev": { + "versions": { + "1.0.2-r6": 1543927483 + }, + "origin": "libmilter", + "dependencies": [ + "libmilter=1.0.2-r6" + ] + }, + "py2-roman": { + "versions": { + "2.0.0-r3": 1543998690 + }, + "origin": "py-roman", + "dependencies": [ + "python2" + ] + }, + "lua5.3-b64": { + "versions": { + "0.1-r2": 1545293186 + }, + "origin": "lua-b64", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "nghttp2-doc": { + "versions": { + "1.35.1-r0": 1545858450 + }, + "origin": "nghttp2" + }, + "libglade": { + "versions": { + "2.6.4-r14": 1543927551 + }, + "origin": "libglade", + "dependencies": [ + "libxml2-utils", + "/bin/sh", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libglade-2.0.so.0=0.0.7", + "cmd:libglade-convert" + ] + }, + "perl-term-readkey-doc": { + "versions": { + "2.37-r1": 1543922483 + }, + "origin": "perl-term-readkey" + }, + "perl-super-doc": { + "versions": { + "1.20141117-r0": 1543932004 + }, + "origin": "perl-super" + }, + "gtk-engines-dev": { + "versions": { + "2.21.0-r2": 1545289336 + }, + "origin": "gtk-engines", + "dependencies": [ + "pc:gtk+-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gtk-engines-2=2.21.0" + ] + }, + "perl-test-deep-doc": { + "versions": { + "1.128-r0": 1542845599 + }, + "origin": "perl-test-deep" + }, + "perl-string-shellquote-doc": { + "versions": { + "1.04-r0": 1545302145 + }, + "origin": "perl-string-shellquote" + }, + "portaudio": { + "versions": { + "190600.20161030-r0": 1545073464 + }, + "origin": "portaudio", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libjack.so.0" + ], + "provides": [ + "so:libportaudio.so.2=2.0.0" + ] + }, + "wavpack-doc": { + "versions": { + "5.1.0-r7": 1548939905 + }, + "origin": "wavpack" + }, + "dejagnu-doc": { + "versions": { + "1.6.2-r0": 1545820393 + }, + "origin": "dejagnu" + }, + "libxxf86vm-dev": { + "versions": { + "1.1.4-r2": 1542900277 + }, + "origin": "libxxf86vm", + "dependencies": [ + "libxxf86vm=1.1.4-r2", + "pc:x11", + "pc:xext", + "pc:xf86vidmodeproto", + "pkgconfig" + ], + "provides": [ + "pc:xxf86vm=1.1.4" + ] + }, + "perl-file-tail": { + "versions": { + "1.3-r1": 1545116502 + }, + "origin": "perl-file-tail" + }, + "iscsi-scst": { + "versions": { + "2.2.1-r2": 1545302142 + }, + "origin": "iscsi-scst", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:iscsi-scst-adm", + "cmd:iscsi-scstd" + ] + }, + "zfs-vanilla-dev": { + "versions": { + "4.19.41-r0": 1557400769 + }, + "origin": "zfs-vanilla", + "dependencies": [ + "glib-dev", + "e2fsprogs-dev", + "util-linux-dev", + "libtirpc-dev", + "linux-vanilla-dev=4.19.41-r0", + "spl-vanilla-dev" + ] + }, + "libpaper": { + "versions": { + "1.1.24-r4": 1542824770 + }, + "origin": "libpaper", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpaper.so.1=1.1.2", + "cmd:paperconf", + "cmd:paperconfig" + ] + }, + "librsync": { + "versions": { + "2.0.2-r1": 1543932724 + }, + "origin": "librsync", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "so:librsync.so.2=2.0.2", + "cmd:rdiff" + ] + }, + "pango-dev": { + "versions": { + "1.42.4-r0": 1542824069 + }, + "origin": "pango", + "dependencies": [ + "pango=1.42.4-r0", + "pc:cairo", + "pc:fontconfig", + "pc:freetype2", + "pc:fribidi", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:harfbuzz", + "pc:xft", + "pkgconfig" + ], + "provides": [ + "pc:pango=1.42.4", + "pc:pangocairo=1.42.4", + "pc:pangoft2=1.42.4", + "pc:pangoxft=1.42.4" + ] + }, + "perl-dist-checkconflicts-doc": { + "versions": { + "0.11-r0": 1542845698 + }, + "origin": "perl-dist-checkconflicts" + }, + "py3-cffi": { + "versions": { + "1.11.5-r0": 1543924871 + }, + "origin": "py-cffi", + "dependencies": [ + "py3-cparser", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libpython3.6m.so.1.0" + ] + }, + "py3-constantly": { + "versions": { + "15.1.0-r0": 1546513613 + }, + "origin": "py-constantly", + "dependencies": [ + "python3" + ] + }, + "atk-lang": { + "versions": { + "2.30.0-r0": 1542823743 + }, + "origin": "atk" + }, + "libmemcached": { + "versions": { + "1.0.18-r3": 1543923893 + }, + "origin": "libmemcached", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libmemcached.so.11", + "so:libmemcachedutil.so.2", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:memcapable", + "cmd:memcat", + "cmd:memcp", + "cmd:memdump", + "cmd:memerror", + "cmd:memexist", + "cmd:memflush", + "cmd:memparse", + "cmd:memping", + "cmd:memrm", + "cmd:memslap", + "cmd:memstat", + "cmd:memtouch" + ] + }, + "mrtg": { + "versions": { + "2.17.7-r0": 1545858156 + }, + "origin": "mrtg", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ], + "provides": [ + "cmd:cfgmaker", + "cmd:indexmaker", + "cmd:mrtg", + "cmd:mrtg-traffic-sum", + "cmd:rateup" + ] + }, + "libnl3-doc": { + "versions": { + "3.4.0-r0": 1542965891 + }, + "origin": "libnl3" + }, + "libass-dev": { + "versions": { + "0.14.0-r0": 1545300164 + }, + "origin": "libass", + "dependencies": [ + "enca-dev", + "fontconfig-dev", + "fribidi-dev", + "freetype-dev", + "libass=0.14.0-r0", + "pc:fontconfig>=2.10.92", + "pc:freetype2>=9.10.3", + "pc:fribidi>=0.19.0", + "pkgconfig" + ], + "provides": [ + "pc:libass=0.14.0" + ] + }, + "libxfont2-dev": { + "versions": { + "2.0.3-r1": 1542924620 + }, + "origin": "libxfont2", + "dependencies": [ + "libxfont2=2.0.3-r1", + "pc:fontenc", + "pc:fontsproto", + "pc:freetype2", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xfont2=2.0.3" + ] + }, + "perl-email-address": { + "versions": { + "1.909-r0": 1542893278 + }, + "origin": "perl-email-address", + "dependencies": [ + "perl", + "perl-capture-tiny" + ] + }, + "dhcpcd-ui-doc": { + "versions": { + "0.7.5-r2": 1545060722 + }, + "origin": "dhcpcd-ui" + }, + "lua5.2-b64": { + "versions": { + "0.1-r2": 1545293185 + }, + "origin": "lua-b64", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "eggdrop": { + "versions": { + "1.6.21-r2": 1545206952 + }, + "origin": "eggdrop", + "dependencies": [ + "tcl", + "so:libc.musl-x86_64.so.1", + "so:libtcl8.6.so", + "so:libz.so.1" + ], + "provides": [ + "cmd:eggdrop" + ] + }, + "perl-crypt-x509": { + "versions": { + "0.51-r0": 1545207018 + }, + "origin": "perl-crypt-x509", + "dependencies": [ + "perl-convert-asn1" + ] + }, + "perl-params-validationcompiler-doc": { + "versions": { + "0.27-r0": 1542973092 + }, + "origin": "perl-params-validationcompiler" + }, + "linux-firmware-r128": { + "versions": { + "20190322-r0": 1554980649 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "font-adobe-75dpi": { + "versions": { + "1.0.3-r0": 1543241232 + }, + "origin": "font-adobe-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "varnish-libs": { + "versions": { + "6.1.1-r0": 1545300751 + }, + "origin": "varnish", + "dependencies": [ + "gcc", + "libc-dev", + "libgcc", + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ], + "provides": [ + "so:libvarnishapi.so.2=2.0.0" + ] + }, + "samba-dc-libs": { + "versions": { + "4.8.11-r1": 1555334975 + }, + "origin": "samba", + "dependencies": [ + "samba-server=4.8.11-r1", + "samba-client=4.8.11-r1", + "samba-common-tools=4.8.11-r1", + "so:libMESSAGING-SEND-samba4.so", + "so:libMESSAGING-samba4.so", + "so:libasn1-samba4.so.8", + "so:libasn1util-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcliauth-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libdcerpc-samba4.so", + "so:libdcerpc.so.0", + "so:libdsdb-garbage-collect-tombstones-samba4.so", + "so:libevents-samba4.so", + "so:libflag-mapping-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgnutls.so.30", + "so:libhcrypto-samba4.so.5", + "so:libhx509-samba4.so.5", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libmessages-dgm-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnetif-samba4.so", + "so:libnpa-tstream-samba4.so", + "so:libpam.so.0", + "so:libpopt.so.0", + "so:libpython2.7.so.1.0", + "so:libreplace-samba4.so", + "so:libroken-samba4.so.19", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-net-samba4.so", + "so:libsamba-python-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsmb-transport-samba4.so", + "so:libsmbclient-raw-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libwbclient.so.0" + ], + "provides": [ + "so:libHDB-SAMBA4-samba4.so=0", + "so:libLIBWBCLIENT-OLD-samba4.so=0", + "so:libauth-unix-token-samba4.so=0", + "so:libauth4-samba4.so=0", + "so:libcluster-samba4.so=0", + "so:libdb-glue-samba4.so=0", + "so:libdcerpc-samr.so.0=0.0.1", + "so:libdcerpc-server.so.0=0.0.1", + "so:libdnsserver-common-samba4.so=0", + "so:libdsdb-module-samba4.so=0", + "so:libhdb-samba4.so.11=11.0.2", + "so:libkdc-samba4.so.2=2.0.0", + "so:libpac-samba4.so=0", + "so:libposix-eadb-samba4.so=0", + "so:libprocess-model-samba4.so=0", + "so:libsamba-policy.so.0=0.0.1", + "so:libservice-samba4.so=0", + "so:libshares-samba4.so=0" + ] + }, + "knock": { + "versions": { + "0.7-r2": 1551279491 + }, + "origin": "knock", + "dependencies": [ + "iptables", + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:knock", + "cmd:knockd" + ] + }, + "asterisk-dbg": { + "versions": { + "15.7.1-r0": 1546247583 + }, + "origin": "asterisk" + }, + "nagios": { + "versions": { + "3.5.1-r4": 1543924481 + }, + "origin": "nagios", + "dependencies": [ + "perl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3", + "so:libperl.so" + ], + "provides": [ + "cmd:nagios", + "cmd:nagiostats" + ] + }, + "lua-imlib2": { + "versions": { + "0.1-r2": 1543221707 + }, + "origin": "lua-imlib2", + "dependencies": [ + "so:libImlib2.so.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "mkfontscale-doc": { + "versions": { + "1.1.3-r1": 1542924691 + }, + "origin": "mkfontscale" + }, + "openldap-overlay-memberof": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "uwsgi-geoip": { + "versions": { + "2.0.17.1-r0": 1545062200 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "freeradius-radclient": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreeradius-radius.so", + "so:libtalloc.so.2" + ], + "provides": [ + "freeradius3-radclient=3.0.17-r5", + "cmd:radclient" + ] + }, + "libvncserver-dev": { + "versions": { + "0.9.11-r2": 1543248464 + }, + "origin": "libvncserver", + "dependencies": [ + "libgcrypt-dev", + "libjpeg-turbo-dev", + "gnutls-dev", + "libpng-dev", + "libice-dev", + "libx11-dev", + "libxdamage-dev", + "libxext-dev", + "libxfixes-dev", + "libxi-dev", + "libxinerama-dev", + "libxrandr-dev", + "libxtst-dev", + "libvncserver=0.9.11-r2", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:libvncclient=0.9.11", + "pc:libvncserver=0.9.11", + "cmd:libvncserver-config" + ] + }, + "cunit-doc": { + "versions": { + "2.1.3-r1": 1542900198 + }, + "origin": "cunit" + }, + "openobex-doc": { + "versions": { + "1.7.2-r1": 1545069090 + }, + "origin": "openobex" + }, + "uwsgi-curl_cron": { + "versions": { + "2.0.17.1-r0": 1545062198 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4" + ] + }, + "libtheora-dev": { + "versions": { + "1.1.1-r13": 1543928528 + }, + "origin": "libtheora", + "dependencies": [ + "libogg-dev", + "libtheora=1.1.1-r13", + "pc:ogg>=1.1", + "pkgconfig" + ], + "provides": [ + "pc:theora=1.1.1", + "pc:theoradec=1.1.1", + "pc:theoraenc=1.1.1" + ] + }, + "gsm": { + "versions": { + "1.0.18-r0": 1543927819 + }, + "origin": "gsm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgsm.so.1=1.0.12" + ] + }, + "libatomic_ops-doc": { + "versions": { + "7.6.4-r0": 1543226876 + }, + "origin": "libatomic_ops" + }, + "python2": { + "versions": { + "2.7.16-r1": 1557171399 + }, + "origin": "python2", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libexpat.so.1", + "so:libffi.so.6", + "so:libgdbm_compat.so.4", + "so:libncursesw.so.6", + "so:libpanelw.so.6", + "so:libreadline.so.7", + "so:libsqlite3.so.0", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "python=2.7.16-r1", + "so:libpython2.7.so.1.0=1.0", + "cmd:idle", + "cmd:pydoc", + "cmd:python", + "cmd:python2", + "cmd:python2.7", + "cmd:smtpd.py" + ] + }, + "mariadb-embedded-dev": { + "versions": { + "10.3.13-r1": 1557431732 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common", + "mariadb-embedded=10.3.13-r1" + ] + }, + "tk-dev": { + "versions": { + "8.6.9-r0": 1545915089 + }, + "origin": "tk", + "dependencies": [ + "tcl-dev", + "libx11-dev", + "libxft-dev", + "fontconfig-dev", + "pc:tcl>=8.6", + "pkgconfig" + ], + "provides": [ + "pc:tk=8.6.9" + ] + }, + "gphoto2": { + "versions": { + "2.5.15-r0": 1545300680 + }, + "origin": "gphoto2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexif.so.12", + "so:libgphoto2.so.6", + "so:libgphoto2_port.so.12", + "so:libintl.so.8", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:gphoto2" + ] + }, + "geoip-doc": { + "versions": { + "1.6.12-r1": 1544799415 + }, + "origin": "geoip" + }, + "haserl": { + "versions": { + "0.9.35-r1": 1542883797 + }, + "origin": "haserl", + "dependencies": [ + "haserl-lua5.3", + "haserl-lua5.2", + "haserl-lua5.1", + "haserl-lua5.3=0.9.35-r1" + ] + }, + "tslib-doc": { + "versions": { + "1.18-r0": 1545859355 + }, + "origin": "tslib" + }, + "postfix-lmdb": { + "versions": { + "3.3.2-r0": 1547130363 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblmdb.so.0" + ], + "provides": [ + "so:postfix-lmdb.so=0" + ] + }, + "texinfo": { + "versions": { + "6.5-r1": 1542300392 + }, + "origin": "texinfo", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:info", + "cmd:install-info", + "cmd:makeinfo", + "cmd:pdftexi2dvi", + "cmd:pod2texi", + "cmd:texi2any", + "cmd:texi2dvi", + "cmd:texi2pdf", + "cmd:texindex" + ] + }, + "perl-switch-doc": { + "versions": { + "2.17-r0": 1543998982 + }, + "origin": "perl-switch" + }, + "libxdmcp": { + "versions": { + "1.1.2-r5": 1542822510 + }, + "origin": "libxdmcp", + "dependencies": [ + "so:libbsd.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXdmcp.so.6=6.0.0" + ] + }, + "openobex": { + "versions": { + "1.7.2-r1": 1545069091 + }, + "origin": "openobex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libopenobex.so.2=1.7.2", + "cmd:obex-check-device" + ] + }, + "hexchat-doc": { + "versions": { + "2.14.2-r0": 1545746058 + }, + "origin": "hexchat" + }, + "cyrus-sasl-plain": { + "versions": { + "2.1.27-r1": 1550353528 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "apkbuild-cpan": { + "versions": { + "3.3.1-r0": 1551786761 + }, + "origin": "abuild", + "dependencies": [ + "perl", + "perl-libwww", + "perl-json", + "perl-module-build-tiny" + ], + "provides": [ + "cmd:apkbuild-cpan" + ] + }, + "perl-test-longstring-doc": { + "versions": { + "0.17-r0": 1544000379 + }, + "origin": "perl-test-longstring" + }, + "squid-lang-ko": { + "versions": { + "4.4-r1": 1545216327 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-mail-spamassassin-doc": { + "versions": { + "3.4.1-r2": 1545061231 + }, + "origin": "perl-mail-spamassassin" + }, + "py-alabaster": { + "versions": { + "0.7.12-r0": 1542824967 + }, + "origin": "py-alabaster" + }, + "perl-cgi-fast-doc": { + "versions": { + "2.13-r0": 1543242213 + }, + "origin": "perl-cgi-fast" + }, + "py3-asn1": { + "versions": { + "0.4.2-r0": 1542821563 + }, + "origin": "py-asn1", + "dependencies": [ + "python3" + ] + }, + "nginx-mod-http-js": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "ucarp": { + "versions": { + "1.5.2-r7": 1545076603 + }, + "origin": "ucarp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:ucarp" + ] + }, + "py-oauthlib": { + "versions": { + "2.0.6-r1": 1543249962 + }, + "origin": "py-oauthlib", + "dependencies": [ + "py-crypto", + "py-jwt" + ] + }, + "openldap-overlay-proxycache": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "ca-certificates-cacert": { + "versions": { + "20190108-r0": 1548779239 + }, + "origin": "ca-certificates" + }, + "unfs3": { + "versions": { + "0.9.22-r4": 1545222984 + }, + "origin": "unfs3", + "dependencies": [ + "rpcbind", + "so:libc.musl-x86_64.so.1", + "so:libfl.so.2", + "so:libtirpc.so.3" + ], + "provides": [ + "cmd:unfsd" + ] + }, + "perl-hash-multivalue-doc": { + "versions": { + "0.16-r0": 1543226610 + }, + "origin": "perl-hash-multivalue" + }, + "kamailio-lua": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ] + }, + "py2-tevent": { + "versions": { + "0.9.37-r1": 1543933086 + }, + "origin": "tevent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libtalloc.so.2", + "so:libtevent.so.0" + ], + "provides": [ + "py-tevent=0.9.37-r1" + ] + }, + "py3-snowballstemmer": { + "versions": { + "1.2.1-r1": 1542824987 + }, + "origin": "py-snowballstemmer", + "dependencies": [ + "python3" + ] + }, + "gettext": { + "versions": { + "0.19.8.1-r4": 1542304413 + }, + "origin": "gettext", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgomp.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libunistring.so.2", + "so:libxml2.so.2" + ], + "provides": [ + "so:libgettextlib-0.19.8.1.so=0", + "so:libgettextsrc-0.19.8.1.so=0", + "cmd:autopoint", + "cmd:envsubst", + "cmd:gettext", + "cmd:gettext.sh", + "cmd:gettextize", + "cmd:msgattrib", + "cmd:msgcat", + "cmd:msgcmp", + "cmd:msgcomm", + "cmd:msgconv", + "cmd:msgen", + "cmd:msgexec", + "cmd:msgfilter", + "cmd:msgfmt", + "cmd:msggrep", + "cmd:msginit", + "cmd:msgmerge", + "cmd:msgunfmt", + "cmd:msguniq", + "cmd:ngettext", + "cmd:recode-sr-latin", + "cmd:xgettext" + ] + }, + "py3-flake8": { + "versions": { + "3.4.1-r2": 1545223358 + }, + "origin": "py-flake8", + "dependencies": [ + "py3-mccabe", + "py3-pep8", + "py3-pyflakes", + "python3" + ], + "provides": [ + "cmd:flake8-3" + ] + }, + "perl-mail-domainkeys": { + "versions": { + "1.0-r2": 1545060517 + }, + "origin": "perl-mail-domainkeys", + "dependencies": [ + "perl" + ] + }, + "perl-exception-class": { + "versions": { + "1.44-r0": 1542845809 + }, + "origin": "perl-exception-class", + "dependencies": [ + "perl-devel-stacktrace", + "perl-class-data-inheritable" + ] + }, + "perl-scope-guard": { + "versions": { + "0.21-r0": 1542924586 + }, + "origin": "perl-scope-guard", + "dependencies": [ + "perl" + ] + }, + "fltk-dev": { + "versions": { + "1.3.4-r2": 1545076701 + }, + "origin": "fltk", + "dependencies": [ + "libx11-dev", + "libxext-dev", + "libxft-dev", + "mesa-dev", + "libxinerama-dev", + "fltk=1.3.4-r2" + ], + "provides": [ + "cmd:fltk-config" + ] + }, + "xset": { + "versions": { + "1.2.4-r0": 1545076640 + }, + "origin": "xset", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xset" + ] + }, + "perl-sub-quote-doc": { + "versions": { + "2.004000-r0": 1542972963 + }, + "origin": "perl-sub-quote" + }, + "expect-doc": { + "versions": { + "5.45.4-r0": 1543246822 + }, + "origin": "expect" + }, + "ldns-dev": { + "versions": { + "1.7.0-r2": 1545117110 + }, + "origin": "ldns", + "dependencies": [ + "openssl-dev", + "ldns=1.7.0-r2", + "pc:libcrypto", + "pkgconfig" + ], + "provides": [ + "pc:libldns=1.7.0", + "cmd:ldns-config" + ] + }, + "qemu-img": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libnettle.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-img", + "cmd:qemu-io", + "cmd:qemu-nbd" + ] + }, + "py3-wtforms": { + "versions": { + "2.1-r0": 1544000623 + }, + "origin": "py-wtforms", + "dependencies": [ + "python3" + ] + }, + "linux-firmware-sb16": { + "versions": { + "20190322-r0": 1554980648 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "check": { + "versions": { + "0.12.0-r1": 1542822525 + }, + "origin": "check", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcheck.so.0=0.0.0", + "cmd:checkmk" + ] + }, + "geeqie-doc": { + "versions": { + "1.4-r0": 1545292984 + }, + "origin": "geeqie" + }, + "p11-kit-trust": { + "versions": { + "0.23.14-r0": 1544791899 + }, + "origin": "p11-kit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libp11-kit.so.0", + "so:libtasn1.so.6" + ], + "provides": [ + "cmd:trust" + ] + }, + "apcupsd-doc": { + "versions": { + "3.14.14-r0": 1545293176 + }, + "origin": "apcupsd" + }, + "py3-flask-wtf": { + "versions": { + "0.14.2-r0": 1545213681 + }, + "origin": "py-flask-wtf", + "dependencies": [ + "py3-flask", + "py3-wtforms", + "python3" + ] + }, + "linux-firmware-liquidio": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "acf-dovecot": { + "versions": { + "0.6.0-r2": 1543999303 + }, + "origin": "acf-dovecot", + "dependencies": [ + "acf-core", + "dovecot" + ] + }, + "lm_sensors-sensord": { + "versions": { + "3.4.0-r6": 1542924819 + }, + "origin": "lm_sensors", + "dependencies": [ + "bash", + "sysfsutils", + "so:libc.musl-x86_64.so.1", + "so:librrd.so.8", + "so:libsensors.so.4" + ], + "provides": [ + "cmd:sensord" + ] + }, + "at-spi2-core": { + "versions": { + "2.28.0-r0": 1543241208 + }, + "origin": "at-spi2-core", + "dependencies": [ + "so:libX11.so.6", + "so:libXtst.so.6", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libatspi.so.0=0.0.1" + ] + }, + "cpufrequtils-doc": { + "versions": { + "008-r4": 1543927815 + }, + "origin": "cpufrequtils" + }, + "lua-feedparser": { + "versions": { + "0.71-r1": 1545299555 + }, + "origin": "lua-feedparser", + "dependencies": [ + "lua", + "lua-expat" + ], + "provides": [ + "lua-feedparser-common=0.71-r1", + "lua5.1-feedparser=0.71-r1", + "lua5.2-feedparser=0.71-r1", + "lua5.3-feedparser=0.71-r1" + ] + }, + "aspell-compat": { + "versions": { + "0.60.6.1-r13": 1542965829 + }, + "origin": "aspell", + "dependencies": [ + "aspell" + ], + "provides": [ + "cmd:ispell", + "cmd:spell" + ] + }, + "py-egenix-mx-base": { + "versions": { + "3.2.9-r0": 1542821605 + }, + "origin": "py-egenix-mx-base", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "py-django-extra-views": { + "versions": { + "0.11.0-r0": 1543249813 + }, + "origin": "py-django-extra-views", + "dependencies": [ + "py-django", + "py-six" + ] + }, + "libdrm": { + "versions": { + "2.4.96-r0": 1544791765 + }, + "origin": "libdrm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpciaccess.so.0" + ], + "provides": [ + "so:libdrm.so.2=2.4.0", + "so:libdrm_amdgpu.so.1=1.0.0", + "so:libdrm_freedreno.so.1=1.0.0", + "so:libdrm_intel.so.1=1.0.0", + "so:libdrm_nouveau.so.2=2.0.0", + "so:libdrm_radeon.so.1=1.0.1", + "so:libkms.so.1=1.0.0" + ] + }, + "py-gnome-bonobo": { + "versions": { + "2.28.1-r5": 1545301287 + }, + "origin": "py-gnome", + "dependencies": [ + "py-gtk", + "py-gnome-gnomecanvas" + ] + }, + "acf-lib": { + "versions": { + "0.10.1-r0": 1542883783 + }, + "origin": "acf-lib", + "dependencies": [ + "lua-subprocess" + ] + }, + "xf86-video-chips": { + "versions": { + "1.2.7-r3": 1545300415 + }, + "origin": "xf86-video-chips", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gstreamer0.10-lang": { + "versions": { + "0.10.36-r3": 1544000752 + }, + "origin": "gstreamer0.10" + }, + "lua-sec-doc": { + "versions": { + "0.7-r1": 1545075387 + }, + "origin": "lua-sec" + }, + "libdvbcsa": { + "versions": { + "1.1.0-r1": 1545293164 + }, + "origin": "libdvbcsa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdvbcsa.so.1=1.0.1" + ] + }, + "opusfile-dev": { + "versions": { + "0.11-r1": 1545209752 + }, + "origin": "opusfile", + "dependencies": [ + "openssl-dev", + "opusfile=0.11-r1", + "pc:ogg>=1.3", + "pc:opus>=1.0.1", + "pkgconfig" + ], + "provides": [ + "pc:opusfile=0.11", + "pc:opusurl=0.11" + ] + }, + "xf86-video-i128-doc": { + "versions": { + "1.3.6-r11": 1545069417 + }, + "origin": "xf86-video-i128" + }, + "beep": { + "versions": { + "1.3-r2": 1545214044 + }, + "origin": "beep", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:beep" + ] + }, + "rgb": { + "versions": { + "1.0.6-r2": 1545075162 + }, + "origin": "rgb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:showrgb" + ] + }, + "perl-font-afm": { + "versions": { + "1.20-r0": 1542983922 + }, + "origin": "perl-font-afm" + }, + "logcheck": { + "versions": { + "1.3.19-r0": 1545857329 + }, + "origin": "logcheck", + "dependencies": [ + "lockfile-progs", + "/bin/sh" + ], + "provides": [ + "cmd:logcheck", + "cmd:logcheck-test", + "cmd:logtail", + "cmd:logtail2" + ] + }, + "perl-xml-simple-doc": { + "versions": { + "2.25-r0": 1545062590 + }, + "origin": "perl-xml-simple" + }, + "linux-firmware-rsi": { + "versions": { + "20190322-r0": 1554980648 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "autoconf-doc": { + "versions": { + "2.69-r2": 1542301299 + }, + "origin": "autoconf" + }, + "nagios-plugins-http": { + "versions": { + "2.2.1-r6": 1543933905 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "ngircd-doc": { + "versions": { + "24-r4": 1544001364 + }, + "origin": "ngircd" + }, + "perl-module-install-doc": { + "versions": { + "1.19-r0": 1542893355 + }, + "origin": "perl-module-install" + }, + "geoip-dev": { + "versions": { + "1.6.12-r1": 1544799414 + }, + "origin": "geoip", + "dependencies": [ + "geoip=1.6.12-r1", + "pkgconfig" + ], + "provides": [ + "pc:geoip=1.6.12" + ] + }, + "openvpn-doc": { + "versions": { + "2.4.6-r4": 1543223245 + }, + "origin": "openvpn" + }, + "linux-firmware-cadence": { + "versions": { + "20190322-r0": 1554980653 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "jwm": { + "versions": { + "2.3.7-r0": 1545292616 + }, + "origin": "jwm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXft.so.2", + "so:libXinerama.so.1", + "so:libXmu.so.6", + "so:libXpm.so.4", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfribidi.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:librsvg-2.so.2" + ], + "provides": [ + "cmd:jwm" + ] + }, + "tiff-tools": { + "versions": { + "4.0.10-r0": 1543921906 + }, + "origin": "tiff", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:fax2ps", + "cmd:fax2tiff", + "cmd:pal2rgb", + "cmd:ppm2tiff", + "cmd:raw2tiff", + "cmd:tiff2bw", + "cmd:tiff2pdf", + "cmd:tiff2ps", + "cmd:tiff2rgba", + "cmd:tiffcmp", + "cmd:tiffcp", + "cmd:tiffcrop", + "cmd:tiffdither", + "cmd:tiffdump", + "cmd:tiffinfo", + "cmd:tiffmedian", + "cmd:tiffset", + "cmd:tiffsplit" + ] + }, + "coova-chilli-dev": { + "versions": { + "1.4-r2": 1545163684 + }, + "origin": "coova-chilli", + "dependencies": [ + "coova-chilli=1.4-r2" + ] + }, + "lua-sql-odbc": { + "versions": { + "2.3.5-r2": 1543924410 + }, + "origin": "lua-sql" + }, + "perl-devel-checkbin-doc": { + "versions": { + "0.02-r0": 1542845746 + }, + "origin": "perl-devel-checkbin" + }, + "perl-astro-suntime": { + "versions": { + "0.06-r0": 1545062002 + }, + "origin": "perl-astro-suntime", + "dependencies": [ + "perl" + ] + }, + "lua5.1-libs": { + "versions": { + "5.1.5-r7": 1542820877 + }, + "origin": "lua5.1", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "lua-libs", + "so:liblua.so.5=5.1.4" + ] + }, + "libmad": { + "versions": { + "0.15.1b-r8": 1543925793 + }, + "origin": "libmad", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmad.so.0=0.2.1" + ] + }, + "aria2-daemon": { + "versions": { + "1.34.0-r1": 1543998971 + }, + "origin": "aria2", + "dependencies": [ + "aria2=1.34.0-r1", + "openrc", + "/bin/sh" + ] + }, + "hypermail": { + "versions": { + "2.3.0-r4": 1545229738 + }, + "origin": "hypermail", + "dependencies": [ + "lua", + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libpcre.so.1" + ], + "provides": [ + "cmd:hypermail", + "cmd:mdir2mbox.lua", + "cmd:msgarchive", + "cmd:rdmsg" + ] + }, + "libxtst-dev": { + "versions": { + "1.2.3-r2": 1543241194 + }, + "origin": "libxtst", + "dependencies": [ + "xorgproto", + "libxtst=1.2.3-r2", + "pc:recordproto", + "pc:x11", + "pc:xext", + "pc:xextproto", + "pc:xi", + "pkgconfig" + ], + "provides": [ + "pc:xtst=1.2.3" + ] + }, + "freeswitch-sounds-fr-ca-june-8000": { + "versions": { + "1.0.51-r0": 1545075153 + }, + "origin": "freeswitch-sounds-fr-ca-june-8000" + }, + "parted-doc": { + "versions": { + "3.2-r7": 1543935530 + }, + "origin": "parted" + }, + "freeswitch-sounds-ru-RU-elena-32000": { + "versions": { + "1.0.12-r1": 1545254249 + }, + "origin": "freeswitch-sounds-ru-RU-elena-32000" + }, + "rrdcollect-doc": { + "versions": { + "0.2.10-r1": 1545207035 + }, + "origin": "rrdcollect" + }, + "sntpc": { + "versions": { + "0.9-r6": 1545215980 + }, + "origin": "sntpc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sntpc" + ] + }, + "perl-cpan-meta-check-doc": { + "versions": { + "0.014-r0": 1542845602 + }, + "origin": "perl-cpan-meta-check" + }, + "herbstluftwm-fish-completion": { + "versions": { + "0.7.1-r0": 1545300036 + }, + "origin": "herbstluftwm" + }, + "poppler-utils": { + "versions": { + "0.56.0-r1": 1542824318 + }, + "origin": "poppler", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfreetype.so.6", + "so:liblcms2.so.2", + "so:libpoppler.so.67", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:pdfdetach", + "cmd:pdffonts", + "cmd:pdfimages", + "cmd:pdfinfo", + "cmd:pdfseparate", + "cmd:pdftocairo", + "cmd:pdftohtml", + "cmd:pdftoppm", + "cmd:pdftops", + "cmd:pdftotext", + "cmd:pdfunite" + ] + }, + "freeradius-mssql": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius-sql=3.0.17-r5" + ], + "provides": [ + "freeradius3-mssql=3.0.17-r5" + ] + }, + "st-dbg": { + "versions": { + "0.8.1-r0": 1545292622 + }, + "origin": "st", + "dependencies": [ + "ncurses-terminfo" + ] + }, + "py-enum34": { + "versions": { + "1.1.6-r2": 1545116813 + }, + "origin": "py-enum34" + }, + "pcre": { + "versions": { + "8.42-r1": 1545067005 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre.so.1=1.2.10", + "so:libpcreposix.so.0=0.0.6" + ] + }, + "font-util-doc": { + "versions": { + "1.3.1-r2": 1542924710 + }, + "origin": "font-util" + }, + "syslog-ng": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libivykis.so.0", + "so:libpcre.so.1", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libevtlog-3.19.so.0=0.0.0", + "so:libloggen_helper-3.19.so.0=0.0.0", + "so:libloggen_plugin-3.19.so.0=0.0.0", + "so:libsecret-storage.so.0=0.0.0", + "so:libsyslog-ng-3.19.so.0=0.0.0", + "cmd:dqtool", + "cmd:loggen", + "cmd:pdbtool", + "cmd:syslog-ng", + "cmd:syslog-ng-ctl", + "cmd:syslog-ng-debun", + "cmd:update-patterndb" + ] + }, + "acf-freeswitch": { + "versions": { + "0.8.0-r2": 1545292839 + }, + "origin": "acf-freeswitch", + "dependencies": [ + "acf-core", + "freeswitch", + "lua-xml" + ] + }, + "perl-net-ip-doc": { + "versions": { + "1.26-r1": 1543924417 + }, + "origin": "perl-net-ip" + }, + "cifs-utils": { + "versions": { + "6.8-r0": 1545290852 + }, + "origin": "cifs-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libkeyutils.so.1", + "so:libkrb5.so.3", + "so:libtalloc.so.2", + "so:libwbclient.so.0" + ], + "provides": [ + "cmd:cifs.idmap", + "cmd:cifs.upcall", + "cmd:mount.cifs" + ] + }, + "pcre2-tools": { + "versions": { + "10.32-r1": 1545069083 + }, + "origin": "pcre2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libedit.so.0", + "so:libpcre2-16.so.0", + "so:libpcre2-32.so.0", + "so:libpcre2-8.so.0", + "so:libpcre2-posix.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:pcre2grep", + "cmd:pcre2test" + ] + }, + "p7zip": { + "versions": { + "16.02-r3": 1545118129 + }, + "origin": "p7zip", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:7z", + "cmd:7za", + "cmd:7zr", + "cmd:p7zip" + ] + }, + "libevdev-doc": { + "versions": { + "1.6.0-r0": 1545838440 + }, + "origin": "libevdev" + }, + "ircservices": { + "versions": { + "5.1.24-r4": 1545069549 + }, + "origin": "ircservices", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ircservices", + "cmd:ircservices-chk", + "cmd:ircservices-convert-db" + ] + }, + "ppp-l2tp": { + "versions": { + "2.4.7-r6": 1543999022 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "igmpproxy-doc": { + "versions": { + "0.2.1-r0": 1545746723 + }, + "origin": "igmpproxy" + }, + "vanessa_socket": { + "versions": { + "0.0.13-r0": 1545116981 + }, + "origin": "vanessa_socket", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0", + "so:libvanessa_logger.so.0" + ], + "provides": [ + "so:libvanessa_socket.so.2=2.1.0", + "cmd:vanessa_socket_pipe" + ] + }, + "lzo-doc": { + "versions": { + "2.10-r2": 1542985323 + }, + "origin": "lzo" + }, + "shorewall6": { + "versions": { + "5.2.2-r0": 1548095417 + }, + "origin": "shorewall6", + "dependencies": [ + "shorewall-core", + "perl", + "ip6tables", + "iproute2" + ] + }, + "perl-struct-dumb": { + "versions": { + "0.09-r0": 1544799022 + }, + "origin": "perl-struct-dumb" + }, + "lua-iconv": { + "versions": { + "7-r1": 1545062773 + }, + "origin": "lua-iconv" + }, + "attr": { + "versions": { + "2.4.47-r7": 1542301506 + }, + "origin": "attr", + "dependencies": [ + "so:libattr.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:attr", + "cmd:getfattr", + "cmd:setfattr" + ] + }, + "perl-dist-checkconflicts": { + "versions": { + "0.11-r0": 1542845704 + }, + "origin": "perl-dist-checkconflicts", + "dependencies": [ + "perl-list-moreutils", + "perl-module-runtime" + ] + }, + "openrc-doc": { + "versions": { + "0.39.2-r3": 1548592317 + }, + "origin": "openrc" + }, + "py2-cairo": { + "versions": { + "1.16.3-r0": 1543925200 + }, + "origin": "py-cairo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libpython2.7.so.1.0" + ] + }, + "libdnet": { + "versions": { + "1.12-r7": 1545076675 + }, + "origin": "libdnet", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdnet.so.1=1.0.1", + "cmd:dnet" + ] + }, + "py-samba": { + "versions": { + "4.8.11-r1": 1555334976 + }, + "origin": "samba", + "dependencies": [ + "py2-tdb", + "so:libMESSAGING-SEND-samba4.so", + "so:libMESSAGING-samba4.so", + "so:libads-samba4.so", + "so:libauth4-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-ldap-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcliauth-samba4.so", + "so:libcluster-samba4.so", + "so:libcmdline-credentials-samba4.so", + "so:libdb-glue-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libdcerpc-samba4.so", + "so:libdcerpc.so.0", + "so:libdnsserver-common-samba4.so", + "so:libdsdb-garbage-collect-tombstones-samba4.so", + "so:libevents-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgpext-samba4.so", + "so:libgse-samba4.so", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:liblibsmb-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnetif-samba4.so", + "so:libposix-eadb-samba4.so", + "so:libpyldb-util.so.1", + "so:libpytalloc-util.so.2", + "so:libpython2.7.so.1.0", + "so:libregistry-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-net-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-policy.so.0", + "so:libsamba-python-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libserver-role-samba4.so", + "so:libsmbclient-raw-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbd-conn-samba4.so", + "so:libtalloc.so.2", + "so:libtdb-wrap-samba4.so", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-reg-samba4.so", + "so:libxattr-tdb-samba4.so" + ] + }, + "perl-datetime-format-mail": { + "versions": { + "0.403-r0": 1543927501 + }, + "origin": "perl-datetime-format-mail", + "dependencies": [ + "perl", + "perl-datetime", + "perl-params-validate" + ] + }, + "perl-eval-closure": { + "versions": { + "0.14-r0": 1542972943 + }, + "origin": "perl-eval-closure", + "dependencies": [ + "perl-try-tiny" + ] + }, + "ttf-droid": { + "versions": { + "20121017-r0": 1545300915 + }, + "origin": "ttf-droid", + "dependencies": [ + "fontconfig" + ] + }, + "py-jinja2-vim": { + "versions": { + "2.10-r2": 1542824938 + }, + "origin": "py-jinja2", + "dependencies": [ + "vim" + ] + }, + "lua5.2-ldap": { + "versions": { + "1.2.3-r5": 1543223251 + }, + "origin": "lua-ldap", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "parted-dev": { + "versions": { + "3.2-r7": 1543935530 + }, + "origin": "parted", + "dependencies": [ + "parted=3.2-r7", + "pkgconfig" + ], + "provides": [ + "pc:libparted=3.2" + ] + }, + "gst-plugins-base0.10-dev": { + "versions": { + "0.10.36-r4": 1543928809 + }, + "origin": "gst-plugins-base0.10", + "dependencies": [ + "gstreamer0.10-dev", + "gst-plugins-base0.10=0.10.36-r4", + "pc:glib-2.0", + "pc:gstreamer-0.10", + "pc:gstreamer-base-0.10", + "pkgconfig" + ], + "provides": [ + "pc:gstreamer-app-0.10=0.10.36", + "pc:gstreamer-audio-0.10=0.10.36", + "pc:gstreamer-cdda-0.10=0.10.36", + "pc:gstreamer-fft-0.10=0.10.36", + "pc:gstreamer-floatcast-0.10=0.10.36", + "pc:gstreamer-interfaces-0.10=0.10.36", + "pc:gstreamer-netbuffer-0.10=0.10.36", + "pc:gstreamer-pbutils-0.10=0.10.36", + "pc:gstreamer-plugins-base-0.10=0.10.36", + "pc:gstreamer-riff-0.10=0.10.36", + "pc:gstreamer-rtp-0.10=0.10.36", + "pc:gstreamer-rtsp-0.10=0.10.36", + "pc:gstreamer-sdp-0.10=0.10.36", + "pc:gstreamer-tag-0.10=0.10.36", + "pc:gstreamer-video-0.10=0.10.36" + ] + }, + "xrdp-doc": { + "versions": { + "0.9.9-r0": 1545859298 + }, + "origin": "xrdp" + }, + "mailcap-doc": { + "versions": { + "2.1.48-r0": 1545062135 + }, + "origin": "mailcap" + }, + "postgresql-bdr": { + "versions": { + "9.4.14_p1-r1": 1543223165 + }, + "origin": "postgresql-bdr", + "dependencies": [ + "bash", + "libpq", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpq.so.5", + "so:libssl.so.1.1" + ], + "provides": [ + "postgresql", + "so:postgresql-bdr:libecpg.so.6=6.6", + "so:postgresql-bdr:libecpg_compat.so.3=3.6", + "so:postgresql-bdr:libpgtypes.so.3=3.5", + "cmd:createlang", + "cmd:droplang", + "cmd:ecpg", + "cmd:initdb", + "cmd:pg_config", + "cmd:pg_controldata", + "cmd:pg_ctl", + "cmd:pg_receivexlog", + "cmd:pg_resetxlog", + "cmd:postgres", + "cmd:postmaster" + ] + }, + "qt-lang": { + "versions": { + "4.8.7-r11": 1545075090 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates" + ] + }, + "hostapd": { + "versions": { + "2.7-r0": 1547822982 + }, + "origin": "hostapd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libnl-3.so.200", + "so:libnl-genl-3.so.200", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:hostapd", + "cmd:hostapd_cli", + "cmd:nt_password_hash" + ] + }, + "net-tools-doc": { + "versions": { + "1.60_git20140218-r2": 1545216350 + }, + "origin": "net-tools" + }, + "xcb-util-keysyms": { + "versions": { + "0.4.0-r1": 1543927912 + }, + "origin": "xcb-util-keysyms", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-keysyms.so.1=1.0.0" + ] + }, + "btrfs-progs-dev": { + "versions": { + "4.19.1-r0": 1545665045 + }, + "origin": "btrfs-progs", + "dependencies": [ + "linux-headers", + "btrfs-progs-libs=4.19.1-r0" + ] + }, + "perl-yaml-syck-doc": { + "versions": { + "1.31-r0": 1545060526 + }, + "origin": "perl-yaml-syck" + }, + "procps-dev": { + "versions": { + "3.3.15-r0": 1543934060 + }, + "origin": "procps", + "dependencies": [ + "libproc=3.3.15-r0", + "pkgconfig" + ] + }, + "snappy-dbg": { + "versions": { + "1.1.7-r1": 1545208921 + }, + "origin": "snappy" + }, + "meson": { + "versions": { + "0.48.2-r0": 1544791717 + }, + "origin": "meson", + "dependencies": [ + "ninja", + "python3" + ], + "provides": [ + "cmd:meson" + ] + }, + "perl-subversion": { + "versions": { + "1.11.1-r0": 1548692375 + }, + "origin": "subversion", + "dependencies": [ + "so:libapr-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libsvn_client-1.so.0", + "so:libsvn_delta-1.so.0", + "so:libsvn_diff-1.so.0", + "so:libsvn_fs-1.so.0", + "so:libsvn_ra-1.so.0", + "so:libsvn_repos-1.so.0", + "so:libsvn_subr-1.so.0", + "so:libsvn_wc-1.so.0" + ], + "provides": [ + "so:libsvn_swig_perl-1.so.0=0.0.0" + ] + }, + "openldap-overlay-valsort": { + "versions": { + "2.4.47-r2": 1546016480 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "lmdb": { + "versions": { + "0.9.23-r0": 1547025175 + }, + "origin": "lmdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblmdb.so.0=0.0.0" + ] + }, + "umurmur": { + "versions": { + "0.2.17-r3": 1545207064 + }, + "origin": "umurmur", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libconfig.so.9", + "so:libcrypto.so.1.1", + "so:libprotobuf-c.so.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:umurmurd" + ] + }, + "squid-lang-th": { + "versions": { + "4.4-r1": 1545216330 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "net-snmp-tools": { + "versions": { + "5.8-r0": 1543923351 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libnetsnmp.so.35", + "so:libnetsnmpagent.so.35" + ], + "provides": [ + "cmd:agentxtrap", + "cmd:checkbandwidth", + "cmd:encode_keychange", + "cmd:net-snmp-create-v3-user", + "cmd:snmpbulkget", + "cmd:snmpbulkwalk", + "cmd:snmpconf", + "cmd:snmpdelta", + "cmd:snmpdf", + "cmd:snmpget", + "cmd:snmpgetnext", + "cmd:snmpinform", + "cmd:snmpnetstat", + "cmd:snmpping", + "cmd:snmpps", + "cmd:snmpset", + "cmd:snmpstatus", + "cmd:snmptable", + "cmd:snmptest", + "cmd:snmptop", + "cmd:snmptranslate", + "cmd:snmptrap", + "cmd:snmpusm", + "cmd:snmpvacm", + "cmd:snmpwalk" + ] + }, + "lua5.1-sircbot": { + "versions": { + "0.4-r2": 1543924832 + }, + "origin": "sircbot", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-json-doc": { + "versions": { + "2.97000-r0": 1542819173 + }, + "origin": "perl-json" + }, + "perl-specio": { + "versions": { + "0.42-r0": 1542972981 + }, + "origin": "perl-specio", + "dependencies": [ + "perl-test-needs", + "perl-devel-stacktrace", + "perl-eval-closure", + "perl-mro-compat", + "perl-role-tiny", + "perl-test-fatal", + "perl-module-runtime", + "perl-sub-quote" + ] + }, + "tree": { + "versions": { + "1.8.0-r0": 1544791740 + }, + "origin": "tree", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tree" + ] + }, + "collectd-rrdtool": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:librrd.so.8" + ] + }, + "snownews-lang": { + "versions": { + "1.5.12-r8": 1543999423 + }, + "origin": "snownews", + "dependencies": [ + "ncurses" + ] + }, + "openipmi-lanserv": { + "versions": { + "2.0.25-r1": 1545214270 + }, + "origin": "openipmi", + "dependencies": [ + "so:libOpenIPMIposix.so.0", + "so:libOpenIPMIutils.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpopt.so.0" + ], + "provides": [ + "so:libIPMIlanserv.so.0=0.0.1", + "cmd:ipmi_sim", + "cmd:ipmilan", + "cmd:sdrcomp" + ] + }, + "squid-lang-hu": { + "versions": { + "4.4-r1": 1545216326 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "postgresql-bdr-contrib": { + "versions": { + "9.4.14_p1-r1": 1543223164 + }, + "origin": "postgresql-bdr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpq.so.5", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:oid2name", + "cmd:pg_archivecleanup", + "cmd:pg_standby", + "cmd:pg_test_fsync", + "cmd:pg_test_timing", + "cmd:pg_upgrade", + "cmd:pg_xlogdump", + "cmd:pgbench", + "cmd:vacuumlo" + ] + }, + "libdbi-dev": { + "versions": { + "0.9.0-r0": 1543932516 + }, + "origin": "libdbi", + "dependencies": [ + "libdbi=0.9.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:dbi=0.9.0" + ] + }, + "openldap-overlay-dds": { + "versions": { + "2.4.47-r2": 1546016479 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "libuv-dev": { + "versions": { + "1.23.2-r0": 1542820441 + }, + "origin": "libuv", + "dependencies": [ + "libuv=1.23.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libuv=1.23.2" + ] + }, + "ppp-openrc": { + "versions": { + "2.4.7-r6": 1543999024 + }, + "origin": "ppp", + "dependencies": [ + "ppp-chat", + "ppp-radius", + "ppp-atm", + "ppp-pppoe", + "ppp-l2tp", + "ppp-winbind", + "ppp-passprompt", + "ppp-passwordfd", + "ppp-minconn", + "ppp-daemon" + ] + }, + "font-adobe-100dpi": { + "versions": { + "1.0.3-r0": 1545062647 + }, + "origin": "font-adobe-100dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-test2-suite": { + "versions": { + "0.000114-r0": 1542973042 + }, + "origin": "perl-test2-suite", + "dependencies": [ + "perl-test-simple", + "perl-importer", + "perl-term-table", + "perl-sub-info", + "perl-scope-guard", + "perl-module-pluggable" + ] + }, + "squid-lang-bg": { + "versions": { + "4.4-r1": 1545216323 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "xf86-video-fbdev": { + "versions": { + "0.5.0-r2": 1545067180 + }, + "origin": "xf86-video-fbdev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "kamailio-extras": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libhiredis.so.0.14", + "so:libsrdb1.so.1", + "so:libsrutils.so.1" + ] + }, + "libpng-doc": { + "versions": { + "1.6.37-r0": 1557129094 + }, + "origin": "libpng" + }, + "pcre2": { + "versions": { + "10.32-r1": 1545069083 + }, + "origin": "pcre2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcre2-8.so.0=0.7.1", + "so:libpcre2-posix.so.2=2.0.1" + ] + }, + "eventlog": { + "versions": { + "0.2.12-r4": 1545118013 + }, + "origin": "eventlog", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libevtlog.so.0=0.0.0" + ] + }, + "libjpeg": { + "versions": { + "8-r6": 1542893530 + }, + "origin": "jpeg", + "dependencies": [ + "libjpeg-turbo" + ] + }, + "sdl2_ttf": { + "versions": { + "2.0.14-r1": 1543934467 + }, + "origin": "sdl2_ttf", + "dependencies": [ + "so:libSDL2-2.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6" + ], + "provides": [ + "so:libSDL2_ttf-2.0.so.0=0.14.0" + ] + }, + "cifs-utils-dev": { + "versions": { + "6.8-r0": 1545290851 + }, + "origin": "cifs-utils" + }, + "gtk+3.0-lang": { + "versions": { + "3.24.1-r0": 1543927435 + }, + "origin": "gtk+3.0", + "dependencies": [ + "shared-mime-info", + "gtk-update-icon-cache" + ] + }, + "uwsgi-signal": { + "versions": { + "2.0.17.1-r0": 1545062210 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libbz2": { + "versions": { + "1.0.6-r6": 1542300119 + }, + "origin": "bzip2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libbz2.so.1=1.0.6" + ] + }, + "boost-program_options": { + "versions": { + "1.67.0-r2": 1542823632 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_program_options-mt.so.1.67.0=1.67.0", + "so:libboost_program_options.so.1.67.0=1.67.0" + ] + }, + "perl-mailtools-doc": { + "versions": { + "2.19-r0": 1543226634 + }, + "origin": "perl-mailtools" + }, + "cyrus-sasl-gssapiv2": { + "versions": { + "2.1.27-r1": 1550353527 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgssapi_krb5.so.2" + ] + }, + "squid-lang-da": { + "versions": { + "4.4-r1": 1545216324 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-class-accessor-chained-doc": { + "versions": { + "0.01-r0": 1544000361 + }, + "origin": "perl-class-accessor-chained" + }, + "linux-firmware-ar3k": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "qemu-mips64el": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mips64el" + ] + }, + "samba-common-libs": { + "versions": { + "4.8.11-r1": 1555334973 + }, + "origin": "samba", + "dependencies": [ + "so:libCHARSET3-samba4.so", + "so:libasn1util-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libcom_err.so.2", + "so:libdbwrap-samba4.so", + "so:libevents-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgse-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:libmessages-dgm-samba4.so", + "so:libmessages-util-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-shim-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libtalloc.so.2", + "so:libtdb-wrap-samba4.so", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-cmdline-samba4.so", + "so:libutil-setid-samba4.so", + "so:libutil-tdb-samba4.so", + "so:libwbclient.so.0" + ], + "provides": [ + "so:libMESSAGING-SEND-samba4.so=0", + "so:libcli-spoolss-samba4.so=0", + "so:libdcerpc-binding.so.0=0.0.1", + "so:libdcerpc-samba-samba4.so=0", + "so:liblibcli-lsa3-samba4.so=0", + "so:liblibcli-netlogon3-samba4.so=0", + "so:liblibsmb-samba4.so=0", + "so:libmsrpc3-samba4.so=0", + "so:libndr-samba4.so=0", + "so:libsamba-passdb.so.0=0.27.2", + "so:libtrusts-util-samba4.so=0" + ] + }, + "libva-intel-driver": { + "versions": { + "2.2.0-r0": 1543245814 + }, + "origin": "libva-intel-driver", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_intel.so.1" + ] + }, + "yaml": { + "versions": { + "0.2.1-r0": 1542822092 + }, + "origin": "yaml", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libyaml-0.so.2=2.0.5" + ] + }, + "lua5.2-lzmq": { + "versions": { + "0.4.4-r0": 1545076583 + }, + "origin": "lua-lzmq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5", + "so:libzmq.so.5" + ] + }, + "debootstrap": { + "versions": { + "1.0.111-r0": 1545257152 + }, + "origin": "debootstrap", + "dependencies": [ + "debian-archive-keyring", + "dpkg", + "tar" + ], + "provides": [ + "cmd:debootstrap" + ] + }, + "weechat-lua": { + "versions": { + "2.3-r0": 1545299933 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ] + }, + "uwsgi-transformation_gzip": { + "versions": { + "2.0.17.1-r0": 1545062212 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.1": { + "versions": { + "5.1.5-r7": 1542820878 + }, + "origin": "lua5.1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ], + "provides": [ + "lua", + "cmd:lua", + "cmd:lua5.1", + "cmd:luac", + "cmd:luac5.1" + ] + }, + "libatasmart": { + "versions": { + "0.19-r1": 1545062470 + }, + "origin": "libatasmart", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ], + "provides": [ + "so:libatasmart.so.4=4.0.5", + "cmd:skdump", + "cmd:sktest" + ] + }, + "perl-test-mocktime": { + "versions": { + "0.17-r0": 1544792417 + }, + "origin": "perl-test-mocktime" + }, + "xz-doc": { + "versions": { + "5.2.4-r0": 1542303284 + }, + "origin": "xz" + }, + "nagios-plugins-icmp": { + "versions": { + "2.2.1-r6": 1543933906 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-getopt-long-doc": { + "versions": { + "2.50-r0": 1543922466 + }, + "origin": "perl-getopt-long" + }, + "ruby-power_assert": { + "versions": { + "2.5.5-r0": 1557164837 + }, + "origin": "ruby", + "dependencies": [ + "ruby-libs" + ] + }, + "giflib": { + "versions": { + "5.1.4-r2": 1543077199 + }, + "origin": "giflib", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgif.so.7=7.0.0" + ] + }, + "mpc1": { + "versions": { + "1.0.3-r1": 1542301580 + }, + "origin": "mpc1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libmpfr.so.4" + ], + "provides": [ + "so:libmpc.so.3=3.0.0" + ] + }, + "kbd-misc": { + "versions": { + "2.0.4-r3": 1545215188 + }, + "origin": "kbd" + }, + "openvswitch": { + "versions": { + "2.10.1-r0": 1544000345 + }, + "origin": "openvswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:ovs-appctl", + "cmd:ovs-bugtool", + "cmd:ovs-docker", + "cmd:ovs-dpctl", + "cmd:ovs-dpctl-top", + "cmd:ovs-ofctl", + "cmd:ovs-parse-backtrace", + "cmd:ovs-pki", + "cmd:ovs-vlan-bug-workaround", + "cmd:ovs-vsctl", + "cmd:ovs-vswitchd", + "cmd:ovsdb-client", + "cmd:ovsdb-server", + "cmd:ovsdb-tool", + "cmd:vtep-ctl" + ] + }, + "tumbler-doc": { + "versions": { + "0.2.3-r0": 1545859329 + }, + "origin": "tumbler" + }, + "py-django-tables2": { + "versions": { + "1.21.2-r0": 1543925704 + }, + "origin": "py-django-tables2", + "dependencies": [ + "py-django", + "py-six" + ] + }, + "bacula-libs": { + "versions": { + "9.4.1-r1": 1546944087 + }, + "origin": "bacula", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:bpipe-fd.so=0", + "so:libbac-9.4.1.so=0", + "so:libbaccfg-9.4.1.so=0", + "so:libbacfind-9.4.1.so=0", + "so:libbacsd-9.4.1.so=0", + "so:libbacsql-9.4.1.so=0" + ] + }, + "xf86-video-openchrome-doc": { + "versions": { + "0.6.0-r4": 1545249765 + }, + "origin": "xf86-video-openchrome" + }, + "py2-click": { + "versions": { + "6.7-r2": 1545116987 + }, + "origin": "py-click", + "dependencies": [ + "python2" + ] + }, + "py2-bluez": { + "versions": { + "0.22-r1": 1545062448 + }, + "origin": "py-bluez", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "libnl-dev": { + "versions": { + "1.1.4-r0": 1543924490 + }, + "origin": "libnl", + "dependencies": [ + "libnl=1.1.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnl-1=1.1.4" + ] + }, + "py2-docutils": { + "versions": { + "0.14-r1": 1543925151 + }, + "origin": "py-docutils", + "dependencies": [ + "py2-pillow", + "py2-roman", + "python2" + ], + "provides": [ + "cmd:rst2html-2", + "cmd:rst2html4-2", + "cmd:rst2html5-2", + "cmd:rst2latex-2", + "cmd:rst2man-2", + "cmd:rst2odt-2", + "cmd:rst2odt_prepstyles-2", + "cmd:rst2pseudoxml-2", + "cmd:rst2s5-2", + "cmd:rst2xetex-2", + "cmd:rst2xml-2", + "cmd:rstpep2html-2" + ] + }, + "encfs-doc": { + "versions": { + "1.9.5-r3": 1545061083 + }, + "origin": "encfs" + }, + "clang-libs": { + "versions": { + "5.0.2-r0": 1546873920 + }, + "origin": "clang", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libclang.so.5.0=5.0" + ] + }, + "openldap-overlay-unique": { + "versions": { + "2.4.47-r2": 1546016480 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "lvm2-openrc": { + "versions": { + "2.02.182-r0": 1543928933 + }, + "origin": "lvm2", + "dependencies": [ + "lvm2-libs=2.02.182-r0" + ] + }, + "sudo": { + "versions": { + "1.8.25_p1-r2": 1542304827 + }, + "origin": "sudo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:group_file.so=0", + "so:libsudo_noexec.so=0", + "so:libsudo_util.so.0=0.0.0", + "so:sudoers.so=0", + "so:system_group.so=0", + "cmd:cvtsudoers", + "cmd:sudo", + "cmd:sudoedit", + "cmd:sudoreplay", + "cmd:visudo" + ] + }, + "py2-google-api-python-client": { + "versions": { + "1.7.7-r0": 1546936521 + }, + "origin": "py-google-api-python-client", + "dependencies": [ + "py2-httplib2", + "py2-oauth2client", + "py2-uritemplate", + "py2-six", + "python2" + ] + }, + "lockfile-progs-doc": { + "versions": { + "0.1.18-r0": 1546267694 + }, + "origin": "lockfile-progs" + }, + "perl-heap": { + "versions": { + "0.80-r1": 1545213985 + }, + "origin": "perl-heap" + }, + "busybox-initscripts": { + "versions": { + "3.1-r6": 1545665791 + }, + "origin": "busybox-initscripts", + "dependencies": [ + "busybox", + "openrc>=0.24.1-r6" + ] + }, + "s6-dns": { + "versions": { + "2.3.0.1-r0": 1545062685 + }, + "origin": "s6-dns", + "dependencies": [ + "skalibs", + "so:libc.musl-x86_64.so.1", + "so:libskarnet.so.2.7" + ], + "provides": [ + "so:libs6dns.so.2.3=2.3.0.1", + "so:libskadns.so.2.3=2.3.0.1", + "cmd:s6-dnsip4", + "cmd:s6-dnsip4-filter", + "cmd:s6-dnsip6", + "cmd:s6-dnsip6-filter", + "cmd:s6-dnsmx", + "cmd:s6-dnsname", + "cmd:s6-dnsname-filter", + "cmd:s6-dnsns", + "cmd:s6-dnsq", + "cmd:s6-dnsqr", + "cmd:s6-dnsqualify", + "cmd:s6-dnssoa", + "cmd:s6-dnssrv", + "cmd:s6-dnstxt", + "cmd:s6-randomip", + "cmd:skadnsd" + ] + }, + "libassuan-doc": { + "versions": { + "2.5.1-r0": 1543932160 + }, + "origin": "libassuan" + }, + "xf86-video-dummy-doc": { + "versions": { + "0.3.8-r3": 1545300622 + }, + "origin": "xf86-video-dummy" + }, + "argp-standalone": { + "versions": { + "1.3-r3": 1543249712 + }, + "origin": "argp-standalone" + }, + "gitolite": { + "versions": { + "3.6.11-r0": 1548271316 + }, + "origin": "gitolite", + "dependencies": [ + "git", + "perl", + "/bin/sh" + ] + }, + "xinit": { + "versions": { + "1.4.0-r1": 1542973236 + }, + "origin": "xinit", + "dependencies": [ + "xauth", + "mcookie", + "xmodmap", + "xrdb", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:startx", + "cmd:xinit" + ] + }, + "recode-doc": { + "versions": { + "3.6-r2": 1543245780 + }, + "origin": "recode" + }, + "xf86-video-intel": { + "versions": { + "2.99.917_git20170325-r4": 1545665365 + }, + "origin": "xf86-video-intel", + "dependencies": [ + "mesa-dri-intel", + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXv.so.1", + "so:libXvMC.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_intel.so.1", + "so:libpciaccess.so.0", + "so:libpixman-1.so.0", + "so:libudev.so.1", + "so:libxcb-dri2.so.0", + "so:libxcb-util.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libI810XvMC.so.1=1.0.0", + "so:libIntelXvMC.so.1=1.0.0" + ] + }, + "alpine-conf": { + "versions": { + "3.8.1-r5": 1557161476 + }, + "origin": "alpine-conf", + "dependencies": [ + "openrc>=0.24.1-r6", + "busybox>=1.26.1-r3", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lbu", + "cmd:lbu_commit", + "cmd:lbu_exclude", + "cmd:lbu_include", + "cmd:lbu_status", + "cmd:lbu_update", + "cmd:setup-acf", + "cmd:setup-alpine", + "cmd:setup-apkcache", + "cmd:setup-apkrepos", + "cmd:setup-bootable", + "cmd:setup-disk", + "cmd:setup-dns", + "cmd:setup-gparted-desktop", + "cmd:setup-hostname", + "cmd:setup-interfaces", + "cmd:setup-keymap", + "cmd:setup-lbu", + "cmd:setup-mta", + "cmd:setup-ntp", + "cmd:setup-proxy", + "cmd:setup-sshd", + "cmd:setup-timezone", + "cmd:setup-xen-dom0", + "cmd:setup-xorg-base", + "cmd:uniso", + "cmd:update-conf", + "cmd:update-kernel" + ] + }, + "postfix-doc": { + "versions": { + "3.3.2-r0": 1547130362 + }, + "origin": "postfix" + }, + "qemu-m68k": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-m68k" + ] + }, + "enca-dev": { + "versions": { + "1.19-r1": 1545076571 + }, + "origin": "enca", + "dependencies": [ + "enca=1.19-r1", + "pkgconfig" + ], + "provides": [ + "pc:enca=1.19" + ] + }, + "heimdal-libs": { + "versions": { + "7.5.0-r2": 1542819373 + }, + "origin": "heimdal", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libdb-5.3.so", + "so:libreadline.so.7", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libasn1.so.8=8.0.0", + "so:libgssapi.so.3=3.0.0", + "so:libhcrypto.so.4=4.1.0", + "so:libhdb.so.9=9.2.0", + "so:libheimbase.so.1=1.0.0", + "so:libheimntlm.so.0=0.1.0", + "so:libhx509.so.5=5.0.0", + "so:libkadm5clnt.so.7=7.0.1", + "so:libkadm5srv.so.8=8.0.1", + "so:libkafs.so.0=0.5.1", + "so:libkdc.so.2=2.0.0", + "so:libkrb5.so.26=26.0.0", + "so:libotp.so.0=0.1.5", + "so:libroken.so.18=18.1.0", + "so:libsl.so.0=0.2.1", + "so:libwind.so.0=0.0.0", + "so:windc.so.0=0.0.0", + "cmd:digest-service", + "cmd:kdigest", + "cmd:string2key", + "cmd:verify_krb5_conf" + ] + }, + "strongswan": { + "versions": { + "5.7.2-r1": 1557161394 + }, + "origin": "strongswan", + "dependencies": [ + "iproute2", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libgmp.so.10", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libcharon.so.0=0.0.0", + "so:libradius.so.0=0.0.0", + "so:libsimaka.so.0=0.0.0", + "so:libstrongswan.so.0=0.0.0", + "so:libtls.so.0=0.0.0", + "so:libvici.so.0=0.0.0", + "cmd:charon-cmd", + "cmd:ipsec", + "cmd:pki", + "cmd:swanctl" + ] + }, + "libical-dev": { + "versions": { + "2.0.0-r3": 1545062403 + }, + "origin": "libical", + "dependencies": [ + "libical=2.0.0-r3", + "pkgconfig" + ], + "provides": [ + "pc:libical=2.0" + ] + }, + "zstd-doc": { + "versions": { + "1.3.8-r0": 1546966444 + }, + "origin": "zstd" + }, + "hylafax-lang": { + "versions": { + "6.0.7-r0": 1545746106 + }, + "origin": "hylafax", + "dependencies": [ + "ghostscript", + "bash", + "tiff-tools" + ] + }, + "sshfs-doc": { + "versions": { + "3.5.1-r0": 1548094627 + }, + "origin": "sshfs" + }, + "farstream-doc": { + "versions": { + "0.2.8-r2": 1543928858 + }, + "origin": "farstream" + }, + "uwsgi-xslt": { + "versions": { + "2.0.17.1-r0": 1545062214 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2", + "so:libxslt.so.1" + ] + }, + "attr-dev": { + "versions": { + "2.4.47-r7": 1542301505 + }, + "origin": "attr", + "dependencies": [ + "libattr=2.4.47-r7" + ] + }, + "apache2-proxy": { + "versions": { + "2.4.39-r0": 1554306836 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1" + ] + }, + "fish-doc": { + "versions": { + "2.7.1-r0": 1545302079 + }, + "origin": "fish" + }, + "alpine-ipxe-ipxe_usb": { + "versions": { + "1.0_git20180825-r1": 1545986078 + }, + "origin": "alpine-ipxe" + }, + "libxvmc": { + "versions": { + "1.0.10-r2": 1542900306 + }, + "origin": "libxvmc", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXvMC.so.1=1.0.0", + "so:libXvMCW.so.1=1.0.0" + ] + }, + "kamailio-sqlang": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ] + }, + "orbit2": { + "versions": { + "2.14.19-r4": 1543223380 + }, + "origin": "orbit2", + "dependencies": [ + "so:libIDL-2.so.0", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libORBit-2.so.0=0.1.0", + "so:libORBit-imodule-2.so.0=0.0.0", + "so:libORBitCosNaming-2.so.0=0.1.0", + "cmd:ior-decode-2", + "cmd:linc-cleanup-sockets", + "cmd:orbit-idl-2", + "cmd:typelib-dump" + ] + }, + "libmp3splt-dev": { + "versions": { + "0.9.2-r0": 1545069016 + }, + "origin": "libmp3splt", + "dependencies": [ + "pcre-dev", + "libogg-dev", + "libmad-dev", + "libvorbis-dev", + "libid3tag-dev", + "libtool", + "libmp3splt=0.9.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmp3splt=0.9.2" + ] + }, + "hunspell-dev": { + "versions": { + "1.6.2-r1": 1542883763 + }, + "origin": "hunspell", + "dependencies": [ + "hunspell=1.6.2-r1", + "pkgconfig" + ], + "provides": [ + "pc:hunspell=1.6.2" + ] + }, + "perl-test-requiresinternet": { + "versions": { + "0.05-r1": 1542821875 + }, + "origin": "perl-test-requiresinternet" + }, + "sic-doc": { + "versions": { + "1.2-r1": 1543248488 + }, + "origin": "sic" + }, + "fts-dev": { + "versions": { + "1.2.7-r1": 1543933812 + }, + "origin": "fts", + "dependencies": [ + "fts=1.2.7-r1", + "pkgconfig" + ], + "provides": [ + "pc:libfts=1.2.7", + "pc:musl-fts=1.2.7" + ] + }, + "squid-lang-sv": { + "versions": { + "4.4-r1": 1545216330 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-sub-install": { + "versions": { + "0.928-r0": 1542845633 + }, + "origin": "perl-sub-install", + "dependencies": [ + "perl" + ] + }, + "py-urwid": { + "versions": { + "1.3.1-r2": 1542981240 + }, + "origin": "py-urwid" + }, + "quazip": { + "versions": { + "0.7.3-r0": 1545118030 + }, + "origin": "quazip", + "dependencies": [ + "so:libQtCore.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libquazip.so.1=1.0.0" + ] + }, + "openldap-back-null": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "freeglut-dev": { + "versions": { + "3.0.0-r0": 1545291113 + }, + "origin": "freeglut", + "dependencies": [ + "mesa-dev", + "libx11-dev", + "libice-dev", + "libxxf86vm-dev", + "libxi-dev", + "glu-dev", + "freeglut=3.0.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:freeglut=3.0.0" + ] + }, + "pam-winbind": { + "versions": { + "4.8.11-r1": 1555334974 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0", + "so:libtalloc.so.2", + "so:libwbclient.so.0" + ] + }, + "irssi-doc": { + "versions": { + "1.1.2-r0": 1547738040 + }, + "origin": "irssi" + }, + "lutok-doc": { + "versions": { + "0.4-r2": 1542304898 + }, + "origin": "lutok" + }, + "logrotate-doc": { + "versions": { + "3.15.0-r0": 1545857314 + }, + "origin": "logrotate" + }, + "open-lldp": { + "versions": { + "0.9.46-r3": 1545062132 + }, + "origin": "open-lldp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libconfig.so.9", + "so:libnl.so.1" + ], + "provides": [ + "so:liblldp_clif.so.1=1.0.0", + "cmd:dcbtool", + "cmd:lldpad", + "cmd:lldptool" + ] + }, + "wireless-tools": { + "versions": { + "30_pre9-r0": 1545062252 + }, + "origin": "wireless-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ifrename", + "cmd:iwconfig", + "cmd:iwevent", + "cmd:iwgetid", + "cmd:iwlist", + "cmd:iwpriv", + "cmd:iwspy" + ] + }, + "linux-firmware-adaptec": { + "versions": { + "20190322-r0": 1554980655 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "linux-firmware-mediatek": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "perl-devel-checklib": { + "versions": { + "1.13-r0": 1543922456 + }, + "origin": "perl-devel-checklib", + "provides": [ + "cmd:use-devel-checklib" + ] + }, + "qemu-riscv32": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-riscv32" + ] + }, + "lsyncd": { + "versions": { + "2.2.3-r1": 1545253932 + }, + "origin": "lsyncd", + "dependencies": [ + "rsync", + "so:libc.musl-x86_64.so.1", + "so:liblua-5.3.so.0" + ], + "provides": [ + "cmd:lsyncd" + ] + }, + "ipvsadm": { + "versions": { + "1.29-r0": 1545118133 + }, + "origin": "ipvsadm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnl.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:ipvsadm", + "cmd:ipvsadm-restore", + "cmd:ipvsadm-save" + ] + }, + "lcms2-dev": { + "versions": { + "2.9-r1": 1542824189 + }, + "origin": "lcms2", + "dependencies": [ + "libjpeg-turbo-dev", + "tiff-dev", + "zlib-dev", + "lcms2=2.9-r1", + "pkgconfig" + ], + "provides": [ + "pc:lcms2=2.9" + ] + }, + "perl-test-tcp": { + "versions": { + "2.19-r1": 1543249857 + }, + "origin": "perl-test-tcp", + "dependencies": [ + "perl-test-sharedfork" + ] + }, + "py3-alabaster": { + "versions": { + "0.7.12-r0": 1542824966 + }, + "origin": "py-alabaster", + "dependencies": [ + "python3" + ] + }, + "conntrack-tools": { + "versions": { + "1.4.4-r0": 1545301899 + }, + "origin": "conntrack-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnetfilter_conntrack.so.3", + "so:libnetfilter_cthelper.so.0", + "so:libnetfilter_cttimeout.so.1", + "so:libnetfilter_queue.so.1", + "so:libnfnetlink.so.0" + ], + "provides": [ + "cmd:conntrack", + "cmd:conntrackd", + "cmd:nfct" + ] + }, + "lua-sql": { + "versions": { + "2.3.5-r2": 1543924411 + }, + "origin": "lua-sql" + }, + "xmlindent-doc": { + "versions": { + "0.2.17-r0": 1543223065 + }, + "origin": "xmlindent" + }, + "portaudio-dev": { + "versions": { + "190600.20161030-r0": 1545073462 + }, + "origin": "portaudio", + "dependencies": [ + "pc:alsa", + "pkgconfig", + "portaudio=190600.20161030-r0" + ], + "provides": [ + "pc:portaudio-2.0=19" + ] + }, + "lttng-ust-doc": { + "versions": { + "2.10.1-r0": 1543249617 + }, + "origin": "lttng-ust" + }, + "lua5.2-mosquitto": { + "versions": { + "0.2-r1": 1543932700 + }, + "origin": "lua-mosquitto", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ] + }, + "libva-dev": { + "versions": { + "2.2.0-r0": 1542900261 + }, + "origin": "libva", + "dependencies": [ + "libx11-dev", + "libxext-dev", + "libxfixes-dev", + "libdrm-dev", + "libva=2.2.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libva-drm=1.2.0", + "pc:libva-x11=1.2.0", + "pc:libva=1.2.0" + ] + }, + "ifupdown": { + "versions": { + "0.7.53.1-r1": 1545066951 + }, + "origin": "ifupdown", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ifdown", + "cmd:ifquery", + "cmd:ifup" + ] + }, + "lua-dbi": { + "versions": { + "0.6-r3": 1545214227 + }, + "origin": "lua-dbi" + }, + "chrony": { + "versions": { + "3.4-r1": 1545841267 + }, + "origin": "chrony", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2" + ], + "provides": [ + "cmd:chronyc", + "cmd:chronyd" + ] + }, + "sdl_mixer": { + "versions": { + "1.2.12-r1": 1545162992 + }, + "origin": "sdl_mixer", + "dependencies": [ + "so:libSDL-1.2.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL_mixer-1.2.so.0=0.12.0" + ] + }, + "squid-lang-ar": { + "versions": { + "4.4-r1": 1545216323 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "inotify-tools-doc": { + "versions": { + "3.20.1-r1": 1545302267 + }, + "origin": "inotify-tools" + }, + "openssh-sftp-server": { + "versions": { + "7.9_p1-r5": 1556034594 + }, + "origin": "openssh", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "consolekit2-doc": { + "versions": { + "1.2.1-r1": 1545062380 + }, + "origin": "consolekit2" + }, + "tar": { + "versions": { + "1.32-r0": 1551091781 + }, + "origin": "tar", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tar" + ] + }, + "perl-test-pod-doc": { + "versions": { + "1.52-r0": 1542822493 + }, + "origin": "perl-test-pod" + }, + "perl-compress-raw-zlib": { + "versions": { + "2.084-r0": 1546943961 + }, + "origin": "perl-compress-raw-zlib", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "squid-lang-lv": { + "versions": { + "4.4-r1": 1545216327 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "rsyslog-pgsql": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "rsyslog-ompgsql=8.40.0-r3" + ] + }, + "kbd-legacy": { + "versions": { + "2.0.4-r3": 1545215187 + }, + "origin": "kbd" + }, + "perl-html-quoted-doc": { + "versions": { + "0.04-r0": 1545214096 + }, + "origin": "perl-html-quoted" + }, + "tarsnap-doc": { + "versions": { + "1.0.39-r4": 1544000650 + }, + "origin": "tarsnap" + }, + "perl-cgi-doc": { + "versions": { + "4.40-r0": 1543242201 + }, + "origin": "perl-cgi" + }, + "uwsgi-cgi": { + "versions": { + "2.0.17.1-r0": 1545062197 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-gunicorn": { + "versions": { + "19.7.1-r1": 1545214072 + }, + "origin": "py-gunicorn" + }, + "rsyslog-tls": { + "versions": { + "8.40.0-r3": 1548686792 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30" + ], + "provides": [ + "rsyslog-lmnsd_gtls=8.40.0-r3" + ] + }, + "iaxmodem-doc": { + "versions": { + "1.3.0-r0": 1545214093 + }, + "origin": "iaxmodem" + }, + "nano-doc": { + "versions": { + "3.2-r0": 1545292879 + }, + "origin": "nano" + }, + "gcr-doc": { + "versions": { + "3.28.0-r0": 1545300011 + }, + "origin": "gcr" + }, + "apache2-lua": { + "versions": { + "2.4.39-r0": 1554306835 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ] + }, + "py3-incremental": { + "versions": { + "17.5.0-r0": 1546513617 + }, + "origin": "py-incremental", + "dependencies": [ + "python3" + ] + }, + "imagemagick-dev": { + "versions": { + "7.0.8.44-r0": 1557126217 + }, + "origin": "imagemagick", + "dependencies": [ + "imagemagick-c++=7.0.8.44-r0", + "imagemagick-libs=7.0.8.44-r0", + "pkgconfig" + ], + "provides": [ + "pc:ImageMagick-7.Q16HDRI=7.0.8", + "pc:ImageMagick=7.0.8", + "pc:Magick++-7.Q16HDRI=7.0.8", + "pc:Magick++=7.0.8", + "pc:MagickCore-7.Q16HDRI=7.0.8", + "pc:MagickCore=7.0.8", + "pc:MagickWand-7.Q16HDRI=7.0.8", + "pc:MagickWand=7.0.8", + "cmd:Magick++-config", + "cmd:MagickCore-config", + "cmd:MagickWand-config" + ] + }, + "perl-test-harness": { + "versions": { + "3.42-r0": 1543238858 + }, + "origin": "perl-test-harness" + }, + "rsyslog-mmdblookup": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libmaxminddb.so.0" + ] + }, + "policyd-spf-fs": { + "versions": { + "23-r3": 1543226491 + }, + "origin": "policyd-spf-fs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libspf2.so.2" + ], + "provides": [ + "cmd:policyd-spf-fs" + ] + }, + "protobuf-c-dev": { + "versions": { + "1.3.0-r6": 1545165151 + }, + "origin": "protobuf-c", + "dependencies": [ + "pkgconfig", + "protobuf-c=1.3.0-r6" + ], + "provides": [ + "pc:libprotobuf-c=1.3.0" + ] + }, + "gnokii-smsd-mysql": { + "versions": { + "0.6.31-r8": 1545300480 + }, + "origin": "gnokii", + "dependencies": [ + "gnokii-smsd", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "fftw-double-libs": { + "versions": { + "3.3.8-r0": 1543926479 + }, + "origin": "fftw", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libfftw3.so.3=3.5.8", + "so:libfftw3_threads.so.3=3.5.8" + ] + }, + "libuv-dbg": { + "versions": { + "1.23.2-r0": 1542820441 + }, + "origin": "libuv" + }, + "libgcrypt": { + "versions": { + "1.8.4-r0": 1545838380 + }, + "origin": "libgcrypt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgpg-error.so.0" + ], + "provides": [ + "so:libgcrypt.so.20=20.2.4", + "cmd:dumpsexp", + "cmd:hmac256", + "cmd:mpicalc" + ] + }, + "lua-soap": { + "versions": { + "3.0-r0": 1545207745 + }, + "origin": "lua-soap", + "dependencies": [ + "lua-expat", + "lua-socket" + ] + }, + "docs": { + "versions": { + "0.2-r0": 1545229743 + }, + "origin": "docs", + "dependencies": [ + "man" + ] + }, + "krb5-doc": { + "versions": { + "1.15.5-r0": 1548094457 + }, + "origin": "krb5" + }, + "bison": { + "versions": { + "3.0.5-r0": 1542300926 + }, + "origin": "bison", + "dependencies": [ + "m4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bison", + "cmd:yacc" + ] + }, + "grub-dev": { + "versions": { + "2.02-r14": 1548432369 + }, + "origin": "grub" + }, + "indent-lang": { + "versions": { + "2.2.12-r0": 1545299644 + }, + "origin": "indent" + }, + "perl-file-slurp": { + "versions": { + "9999.25-r0": 1546005797 + }, + "origin": "perl-file-slurp" + }, + "samba": { + "versions": { + "4.8.11-r1": 1555334977 + }, + "origin": "samba", + "dependencies": [ + "samba-server=4.8.11-r1", + "samba-client=4.8.11-r1", + "samba-common-tools=4.8.11-r1" + ] + }, + "libmpeg2-doc": { + "versions": { + "0.5.1-r8": 1544000975 + }, + "origin": "libmpeg2" + }, + "ncdu-doc": { + "versions": { + "1.13-r0": 1545154408 + }, + "origin": "ncdu" + }, + "libvpx-utils": { + "versions": { + "1.6.1-r1": 1545163814 + }, + "origin": "libvpx", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libvpx.so.4" + ], + "provides": [ + "cmd:vpxdec", + "cmd:vpxenc" + ] + }, + "libverto-dev": { + "versions": { + "0.3.0-r1": 1543223524 + }, + "origin": "libverto", + "dependencies": [ + "libverto-glib=0.3.0-r1", + "libverto-libev=0.3.0-r1", + "libverto-libevent=0.3.0-r1", + "libverto=0.3.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:libverto-glib=0.3.0", + "pc:libverto-libev=0.3.0", + "pc:libverto-libevent=0.3.0", + "pc:libverto=0.3.0" + ] + }, + "gcr-dev": { + "versions": { + "3.28.0-r0": 1545300010 + }, + "origin": "gcr", + "dependencies": [ + "glib-dev", + "gtk+3.0-dev", + "libgcrypt-dev", + "p11-kit-dev", + "gcr=3.28.0-r0", + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gtk+-3.0", + "pc:p11-kit-1", + "pkgconfig" + ], + "provides": [ + "pc:gck-1=3.28.0", + "pc:gcr-3=3.28.0", + "pc:gcr-base-3=3.28.0", + "pc:gcr-ui-3=3.28.0" + ] + }, + "cdrkit": { + "versions": { + "1.1.11-r2": 1545075404 + }, + "origin": "cdrkit", + "dependencies": [ + "file", + "bzip2", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:cdda2mp3", + "cmd:cdda2ogg", + "cmd:cdda2wav", + "cmd:cdrecord", + "cmd:devdump", + "cmd:dirsplit", + "cmd:genisoimage", + "cmd:icedax", + "cmd:isodebug", + "cmd:isodump", + "cmd:isoinfo", + "cmd:isovfy", + "cmd:mkhybrid", + "cmd:mkisofs", + "cmd:netscsid", + "cmd:pitchplay", + "cmd:readcd", + "cmd:readmult", + "cmd:readom", + "cmd:wodim" + ] + }, + "opusfile-doc": { + "versions": { + "0.11-r1": 1545209752 + }, + "origin": "opusfile" + }, + "soxr-dev": { + "versions": { + "0.1.3-r0": 1545208987 + }, + "origin": "soxr", + "dependencies": [ + "cmake", + "pkgconfig", + "soxr=0.1.3-r0" + ], + "provides": [ + "pc:soxr-lsr=0.1.3", + "pc:soxr=0.1.3" + ] + }, + "irqbalance-openrc": { + "versions": { + "1.5.0-r0": 1545837194 + }, + "origin": "irqbalance" + }, + "xkeyboard-config": { + "versions": { + "2.22-r0": 1542973216 + }, + "origin": "xkeyboard-config" + }, + "perl-rrd": { + "versions": { + "1.7.0-r0": 1542924801 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librrd.so.8" + ] + }, + "postgresql-pllua-dev": { + "versions": { + "2.0-r0": 1544798727 + }, + "origin": "postgresql-pllua" + }, + "py2-ldb": { + "versions": { + "1.3.8-r0": 1555334661 + }, + "origin": "ldb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldb.so.1", + "so:libpython2.7.so.1.0", + "so:libtalloc.so.2" + ], + "provides": [ + "py-ldb=1.3.8-r0", + "so:libpyldb-util.so.1=1.3.8" + ] + }, + "iftop": { + "versions": { + "0.17-r7": 1542981213 + }, + "origin": "iftop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:iftop" + ] + }, + "glu-dev": { + "versions": { + "9.0.0-r4": 1543927738 + }, + "origin": "glu", + "dependencies": [ + "mesa-dev", + "glu=9.0.0-r4", + "pc:gl", + "pkgconfig" + ], + "provides": [ + "pc:glu=9.0.0" + ] + }, + "glib-dev": { + "versions": { + "2.58.1-r2": 1545290825 + }, + "origin": "glib", + "dependencies": [ + "python3", + "gettext-dev", + "zlib-dev", + "bzip2-dev", + "libffi-dev", + "util-linux-dev", + "libxml2-utils", + "libxslt", + "docbook-xml", + "docbook-xsl", + "glib=2.58.1-r2", + "pc:libpcre", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "pc:gio-2.0=2.58.1", + "pc:gio-unix-2.0=2.58.1", + "pc:glib-2.0=2.58.1", + "pc:gmodule-2.0=2.58.1", + "pc:gmodule-export-2.0=2.58.1", + "pc:gmodule-no-export-2.0=2.58.1", + "pc:gobject-2.0=2.58.1", + "pc:gthread-2.0=2.58.1", + "cmd:gdbus-codegen", + "cmd:glib-compile-resources", + "cmd:glib-genmarshal", + "cmd:glib-gettextize", + "cmd:glib-mkenums", + "cmd:gobject-query", + "cmd:gresource", + "cmd:gtester", + "cmd:gtester-report" + ] + }, + "file-doc": { + "versions": { + "5.36-r0": 1557160812 + }, + "origin": "file" + }, + "libshout": { + "versions": { + "2.4.1-r5": 1545117136 + }, + "origin": "libshout", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libogg.so.0", + "so:libspeex.so.1", + "so:libssl.so.1.1", + "so:libtheora.so.0", + "so:libvorbis.so.0" + ], + "provides": [ + "so:libshout.so.3=3.2.0" + ] + }, + "bind-dnssec-tools": { + "versions": { + "9.12.4_p1-r1": 1556801608 + }, + "origin": "bind", + "dependencies": [ + "py3-bind=9.12.4_p1-r1" + ], + "provides": [ + "cmd:dnssec-checkds", + "cmd:dnssec-coverage", + "cmd:dnssec-keymgr" + ] + }, + "libidn": { + "versions": { + "1.35-r0": 1542924921 + }, + "origin": "libidn", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libidn.so.12=12.6.0", + "cmd:idn" + ] + }, + "acf-iproute2-qos": { + "versions": { + "0.4.0-r2": 1545116953 + }, + "origin": "acf-iproute2-qos", + "dependencies": [ + "acf-core", + "iproute2-qos", + "acf-alpine-baselayout>=0.5.7" + ] + }, + "asterisk-odbc": { + "versions": { + "15.7.1-r0": 1546247584 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "py-pygments": { + "versions": { + "2.2.0-r0": 1542824953 + }, + "origin": "py-pygments", + "dependencies": [ + "py3-pygments", + "py3-pygments=2.2.0-r0" + ] + }, + "perl-convert-asn1-doc": { + "versions": { + "0.27-r0": 1543081682 + }, + "origin": "perl-convert-asn1" + }, + "perl-http-message": { + "versions": { + "6.18-r0": 1542821805 + }, + "origin": "perl-http-message", + "dependencies": [ + "perl-lwp-mediatypes", + "perl-encode-locale", + "perl-http-date", + "perl-uri", + "perl-io-html" + ] + }, + "perl-boolean-doc": { + "versions": { + "0.46-r0": 1543928862 + }, + "origin": "perl-boolean" + }, + "confuse-doc": { + "versions": { + "3.2.2-r0": 1545922117 + }, + "origin": "confuse" + }, + "halberd": { + "versions": { + "0.2.4-r1": 1545299951 + }, + "origin": "halberd", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:halberd" + ] + }, + "freeradius-client-dev": { + "versions": { + "1.1.7-r2": 1543922358 + }, + "origin": "freeradius-client", + "dependencies": [ + "freeradius-client=1.1.7-r2" + ] + }, + "binutils-dev": { + "versions": { + "2.31.1-r2": 1546441178 + }, + "origin": "binutils", + "dependencies": [ + "binutils=2.31.1-r2" + ] + }, + "vte3-lang": { + "versions": { + "0.52.2-r0": 1545215299 + }, + "origin": "vte3" + }, + "attr-doc": { + "versions": { + "2.4.47-r7": 1542301505 + }, + "origin": "attr" + }, + "qemu-system-tricore": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-tricore" + ] + }, + "snownews-doc": { + "versions": { + "1.5.12-r8": 1543999423 + }, + "origin": "snownews" + }, + "lxc-templates-legacy-alpine": { + "versions": { + "3.0.3-r0": 1546416504 + }, + "origin": "lxc-templates-legacy" + }, + "kamailio-perl": { + "versions": { + "5.2.0-r1": 1546423173 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "nginx-mod-http-headers-more": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "libmp3splt": { + "versions": { + "0.9.2-r0": 1545069016 + }, + "origin": "libmp3splt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libid3tag.so.0", + "so:libltdl.so.7", + "so:libmad.so.0", + "so:libogg.so.0", + "so:libpcre.so.1", + "so:libvorbis.so.0", + "so:libvorbisfile.so.3" + ], + "provides": [ + "so:libmp3splt.so.0=0.0.9" + ] + }, + "py-asn1": { + "versions": { + "0.4.2-r0": 1542821567 + }, + "origin": "py-asn1" + }, + "rsyslog-relp": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:librelp.so.0" + ], + "provides": [ + "rsyslog-imrelp=8.40.0-r3", + "rsyslog-omrelp=8.40.0-r3" + ] + }, + "pangomm": { + "versions": { + "2.40.2-r0": 1543925531 + }, + "origin": "pangomm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairomm-1.0.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libglibmm-2.4.so.1", + "so:libgobject-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libpangomm-1.4.so.1=1.0.30" + ] + }, + "cgit": { + "versions": { + "1.2.1-r1": 1544000809 + }, + "origin": "cgit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua-5.3.so.0", + "so:libz.so.1" + ] + }, + "transmission": { + "versions": { + "2.94-r1": 1545208750 + }, + "origin": "transmission", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libevent-2.1.so.6", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:transmission-gtk" + ] + }, + "libxp": { + "versions": { + "1.0.3-r2": 1545224135 + }, + "origin": "libxp", + "dependencies": [ + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXp.so.6=6.2.0" + ] + }, + "sipp": { + "versions": { + "3.5.2-r0": 1545300214 + }, + "origin": "sipp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libpcap.so.1", + "so:libsctp.so.1", + "so:libssl.so.1.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:sipp" + ] + }, + "py3-pynacl": { + "versions": { + "1.3.0-r1": 1546125247 + }, + "origin": "py-pynacl", + "dependencies": [ + "py3-cffi", + "py3-six", + "python3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "perl-x10": { + "versions": { + "0.04-r1": 1545163614 + }, + "origin": "perl-x10", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:x10client", + "cmd:x10server" + ] + }, + "duplicity-lang": { + "versions": { + "0.7.18.2-r0": 1544000473 + }, + "origin": "duplicity", + "dependencies": [ + "python2", + "py-boto", + "gnupg", + "ncftp", + "py2-fasteners" + ] + }, + "wget-doc": { + "versions": { + "1.20.3-r0": 1554718236 + }, + "origin": "wget" + }, + "lua5.2-microlight": { + "versions": { + "1.1.1-r2": 1543249969 + }, + "origin": "lua-microlight" + }, + "logrotate": { + "versions": { + "3.15.0-r0": 1545857314 + }, + "origin": "logrotate", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpopt.so.0" + ], + "provides": [ + "cmd:logrotate" + ] + }, + "libmcrypt-doc": { + "versions": { + "2.5.8-r7": 1545066949 + }, + "origin": "libmcrypt" + }, + "py2-parsing": { + "versions": { + "2.2.0-r0": 1542825044 + }, + "origin": "py-parsing", + "dependencies": [ + "python2" + ] + }, + "perl-test-pod-coverage": { + "versions": { + "1.10-r0": 1542845675 + }, + "origin": "perl-test-pod-coverage", + "dependencies": [ + "perl", + "perl-pod-coverage", + "perl-test-pod", + "perl-devel-symdump" + ] + }, + "libnetfilter_acct": { + "versions": { + "1.0.3-r0": 1545209158 + }, + "origin": "libnetfilter_acct", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0" + ], + "provides": [ + "so:libnetfilter_acct.so.1=1.0.0" + ] + }, + "gnome-vfs-lang": { + "versions": { + "2.24.4-r6": 1545299691 + }, + "origin": "gnome-vfs", + "dependencies": [ + "gnome-mime-data" + ] + }, + "perl-test-mockmodule": { + "versions": { + "0.15-r0": 1543932008 + }, + "origin": "perl-test-mockmodule", + "dependencies": [ + "perl" + ] + }, + "mosquitto-clients": { + "versions": { + "1.5.6-r0": 1549970968 + }, + "origin": "mosquitto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ], + "provides": [ + "cmd:mosquitto_pub", + "cmd:mosquitto_sub" + ] + }, + "libvirt-doc": { + "versions": { + "4.10.0-r1": 1547051657 + }, + "origin": "libvirt" + }, + "nagios-plugins-openrc": { + "versions": { + "2.2.1-r6": 1543933914 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "openrc", + "sudo" + ] + }, + "pjsua": { + "versions": { + "2.7.2-r4": 1545073632 + }, + "origin": "pjproject", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpj.so.2", + "so:libpjlib-util.so.2", + "so:libpjmedia.so.2", + "so:libpjsip-simple.so.2", + "so:libpjsip.so.2", + "so:libpjsua.so.2" + ], + "provides": [ + "cmd:pjsua" + ] + }, + "cyrus-sasl-scram": { + "versions": { + "2.1.27-r1": 1550353528 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "fuse3-dev": { + "versions": { + "3.2.6-r1": 1548943636 + }, + "origin": "fuse3", + "dependencies": [ + "fuse3=3.2.6-r1", + "pkgconfig" + ], + "provides": [ + "pc:fuse3=3.2.6" + ] + }, + "oidentd": { + "versions": { + "2.2.3-r0": 1545163555 + }, + "origin": "oidentd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:oidentd" + ] + }, + "imlib2": { + "versions": { + "1.5.1-r0": 1543221705 + }, + "origin": "imlib2", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXext.so.6", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libgif.so.7", + "so:libid3tag.so.0", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libtiff.so.5", + "so:libxcb-shm.so.0", + "so:libxcb.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libImlib2.so.1=1.5.1", + "cmd:imlib2_bumpmap", + "cmd:imlib2_colorspace", + "cmd:imlib2_conv", + "cmd:imlib2_grab", + "cmd:imlib2_poly", + "cmd:imlib2_show", + "cmd:imlib2_test", + "cmd:imlib2_view" + ] + }, + "perl-test-identity": { + "versions": { + "0.01-r0": 1544799040 + }, + "origin": "perl-test-identity" + }, + "p11-kit-doc": { + "versions": { + "0.23.14-r0": 1544791899 + }, + "origin": "p11-kit" + }, + "lua-lzlib": { + "versions": { + "0.4.3-r0": 1542845790 + }, + "origin": "lua-lzlib" + }, + "py3-parsing": { + "versions": { + "2.2.0-r0": 1542825048 + }, + "origin": "py-parsing", + "dependencies": [ + "python3" + ] + }, + "freeradius-checkrad": { + "versions": { + "3.0.17-r5": 1556201989 + }, + "origin": "freeradius", + "dependencies": [ + "perl", + "perl-net-telnet", + "perl-snmp-session", + "net-snmp-tools" + ], + "provides": [ + "cmd:checkrad" + ] + }, + "unbound-doc": { + "versions": { + "1.8.3-r1": 1555953584 + }, + "origin": "unbound" + }, + "xclip-doc": { + "versions": { + "0.13-r0": 1542883352 + }, + "origin": "xclip" + }, + "mpg123-dev": { + "versions": { + "1.25.10-r0": 1545117148 + }, + "origin": "mpg123", + "dependencies": [ + "mpg123=1.25.10-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmpg123=1.25.10", + "pc:libout123=1.25.10" + ] + }, + "mkfontdir-doc": { + "versions": { + "1.0.7-r1": 1542924704 + }, + "origin": "mkfontdir" + }, + "libgsf-dev": { + "versions": { + "1.14.44-r0": 1543926573 + }, + "origin": "libgsf", + "dependencies": [ + "bzip2-dev", + "libgsf=1.14.44-r0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:libxml-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libgsf-1=1.14.44" + ] + }, + "linux-virt": { + "versions": { + "4.19.41-r0": 1557399975 + }, + "origin": "linux-vanilla", + "dependencies": [ + "mkinitfs" + ] + }, + "perl-sub-info": { + "versions": { + "0.002-r0": 1542972997 + }, + "origin": "perl-sub-info", + "dependencies": [ + "perl-importer", + "perl-test-simple" + ] + }, + "amavisd-new": { + "versions": { + "2.11.1-r0": 1545229742 + }, + "origin": "amavisd-new", + "dependencies": [ + "sed", + "file", + "perl", + "perl-archive-zip", + "perl-carp", + "perl-convert-tnef", + "perl-compress-raw-zlib", + "perl-convert-uulib", + "perl-digest-md5", + "perl-io", + "perl-exporter", + "perl-io-compress", + "perl-io-stringy", + "perl-mime-tools", + "perl-mailtools", + "perl-socket", + "perl-net-libidn", + "perl-net-server", + "perl-time-hires", + "perl-unix-syslog", + "perl-db", + "perl-mail-dkim", + "perl-io-socket-inet6", + "/bin/sh" + ], + "provides": [ + "cmd:amavisd", + "cmd:amavisd-nanny", + "cmd:amavisd-release" + ] + }, + "perl-encode-utils": { + "versions": { + "2.98-r0": 1543935164 + }, + "origin": "perl-encode", + "dependencies": [ + "perl-encode", + "perl" + ], + "provides": [ + "perl-encode-piconv", + "cmd:encguess", + "cmd:piconv" + ] + }, + "lua5.2-sqlite": { + "versions": { + "0.9.5-r2": 1545214288 + }, + "origin": "lua-sqlite", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "bearssl": { + "versions": { + "0.6-r0": 1546957733 + }, + "origin": "bearssl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libbearssl.so.0=0.6", + "cmd:brssl" + ] + }, + "py-configshell": { + "versions": { + "1.1_p19-r3": 1542981244 + }, + "origin": "py-configshell", + "dependencies": [ + "python2", + "py-six", + "py-urwid", + "py-parsing" + ] + }, + "icon-naming-utils": { + "versions": { + "0.8.90-r2": 1545075429 + }, + "origin": "icon-naming-utils", + "dependencies": [ + "perl-xml-simple", + "pkgconfig" + ], + "provides": [ + "pc:icon-naming-utils=0.8.90" + ] + }, + "lua5.2-bit32": { + "versions": { + "5.3.0-r2": 1546011988 + }, + "origin": "lua-bit32", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "vimdiff": { + "versions": { + "8.1.0630-r0": 1545664993 + }, + "origin": "vim", + "dependencies": [ + "diffutils", + "vim=8.1.0630-r0" + ] + }, + "perl-mime-tools-doc": { + "versions": { + "5.509-r1": 1544001504 + }, + "origin": "perl-mime-tools" + }, + "mysql-bench": { + "versions": { + "10.3.13-r1": 1557431736 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-client" + ] + }, + "aspell-libs": { + "versions": { + "0.60.6.1-r13": 1542965830 + }, + "origin": "aspell", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libaspell.so.15=15.1.5", + "so:libpspell.so.15=15.1.5" + ] + }, + "nagios-plugins-ifoperstatus": { + "versions": { + "2.2.1-r6": 1543933906 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "perl" + ] + }, + "sdl2": { + "versions": { + "2.0.9-r0": 1543934457 + }, + "origin": "sdl2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL2-2.0.so.0=0.9.0" + ] + }, + "collectd-bind": { + "versions": { + "5.8.0-r3": 1546422677 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libxml2.so.2" + ] + }, + "gcalcli": { + "versions": { + "3.4.0-r1": 1545292692 + }, + "origin": "gcalcli", + "dependencies": [ + "python3", + "py-google-api-python-client", + "py-dateutil", + "py-gflags", + "py-vobject" + ], + "provides": [ + "cmd:gcalcli" + ] + }, + "mupdf-gl": { + "versions": { + "1.13.0-r2": 1544000576 + }, + "origin": "mupdf", + "dependencies": [ + "so:libGL.so.1", + "so:libX11.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1", + "so:libmupdf.so.0", + "so:libmupdfthird.so.0" + ], + "provides": [ + "cmd:mupdf-gl" + ] + }, + "lua5.3-optarg": { + "versions": { + "0.2-r1": 1542820941 + }, + "origin": "lua-optarg", + "dependencies": [ + "lua5.3" + ] + }, + "libburn-dev": { + "versions": { + "1.5.0-r0": 1545837389 + }, + "origin": "libburn", + "dependencies": [ + "libburn=1.5.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libburn-1=1.5.0" + ] + }, + "py2-cliapp": { + "versions": { + "1.20170823-r0": 1544798594 + }, + "origin": "py-cliapp", + "dependencies": [ + "python2", + "python3", + "python2" + ] + }, + "font-arabic-misc": { + "versions": { + "1.0.3-r0": 1545292771 + }, + "origin": "font-arabic-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "libproxy": { + "versions": { + "0.4.15-r1": 1544001160 + }, + "origin": "libproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libproxy.so.1=1.0.0" + ] + }, + "glamor-egl-dev": { + "versions": { + "0.6.0-r4": 1545060558 + }, + "origin": "glamor-egl", + "dependencies": [ + "mesa-dev", + "glamor-egl=0.6.0-r4", + "pkgconfig" + ], + "provides": [ + "pc:glamor-egl=0.6.0", + "pc:glamor=0.6.0" + ] + }, + "perl-html-rewriteattributes-doc": { + "versions": { + "0.05-r1": 1545289344 + }, + "origin": "perl-html-rewriteattributes" + }, + "mupdf-x11": { + "versions": { + "1.13.0-r2": 1544000575 + }, + "origin": "mupdf", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1", + "so:libmupdf.so.0", + "so:libmupdfthird.so.0" + ], + "provides": [ + "cmd:mupdf-x11" + ] + }, + "libproxy-dev": { + "versions": { + "0.4.15-r1": 1544001154 + }, + "origin": "libproxy", + "dependencies": [ + "zlib-dev", + "libproxy=0.4.15-r1", + "pkgconfig" + ], + "provides": [ + "pc:libproxy-1.0=0.4.15" + ] + }, + "py3-urwid": { + "versions": { + "1.3.1-r2": 1542981238 + }, + "origin": "py-urwid", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "libinput": { + "versions": { + "1.12.4-r0": 1545734494 + }, + "origin": "libinput", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libevdev.so.2", + "so:libinput.so.10", + "so:libudev.so.1" + ], + "provides": [ + "cmd:libinput" + ] + }, + "xwininfo": { + "versions": { + "1.1.4-r0": 1545249943 + }, + "origin": "xwininfo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-shape.so.0", + "so:libxcb.so.1" + ], + "provides": [ + "cmd:xwininfo" + ] + }, + "gstreamer0.10-doc": { + "versions": { + "0.10.36-r3": 1544000752 + }, + "origin": "gstreamer0.10" + }, + "perl-extutils-helpers-doc": { + "versions": { + "0.026-r0": 1543238832 + }, + "origin": "perl-extutils-helpers" + }, + "perl-locale-maketext-fuzzy": { + "versions": { + "0.11-r1": 1544000297 + }, + "origin": "perl-locale-maketext-fuzzy", + "dependencies": [ + "perl" + ] + }, + "linux-firmware-ath9k_htc": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "libical": { + "versions": { + "2.0.0-r3": 1545062404 + }, + "origin": "libical", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libical.so.2=2.0.0", + "so:libical_cxx.so.2=2.0.0", + "so:libicalss.so.2=2.0.0", + "so:libicalss_cxx.so.2=2.0.0", + "so:libicalvcal.so.2=2.0.0" + ] + }, + "upower-dev": { + "versions": { + "0.99.7-r0": 1545069384 + }, + "origin": "upower", + "dependencies": [ + "pc:gio-2.0", + "pc:glib-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pkgconfig", + "upower=0.99.7-r0" + ], + "provides": [ + "pc:upower-glib=0.99.7" + ] + }, + "perl-http-daemon-doc": { + "versions": { + "6.01-r1": 1542821822 + }, + "origin": "perl-http-daemon" + }, + "acf-gross": { + "versions": { + "0.6.0-r2": 1545062675 + }, + "origin": "acf-gross", + "dependencies": [ + "acf-core", + "gross" + ] + }, + "ncurses": { + "versions": { + "6.1_p20190105-r0": 1546948260 + }, + "origin": "ncurses", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:captoinfo", + "cmd:clear", + "cmd:infocmp", + "cmd:infotocap", + "cmd:reset", + "cmd:tabs", + "cmd:tic", + "cmd:toe", + "cmd:tput", + "cmd:tset" + ] + }, + "gdk-pixbuf-dbg": { + "versions": { + "2.36.11-r2": 1543253991 + }, + "origin": "gdk-pixbuf", + "dependencies": [ + "shared-mime-info" + ] + }, + "perl-text-reform-doc": { + "versions": { + "1.20-r0": 1545118015 + }, + "origin": "perl-text-reform" + }, + "py2-flake8": { + "versions": { + "3.4.1-r2": 1545223355 + }, + "origin": "py-flake8", + "dependencies": [ + "py2-mccabe", + "py2-pep8", + "py2-pyflakes", + "python2" + ], + "provides": [ + "flake8=3.4.1-r2", + "cmd:flake8-2" + ] + }, + "py2-pep8": { + "versions": { + "1.7.1-r0": 1542902304 + }, + "origin": "py-pep8", + "dependencies": [ + "python2" + ], + "provides": [ + "py-pep8=1.7.1-r0", + "cmd:pep8-2" + ] + }, + "nspr-dev": { + "versions": { + "4.20-r0": 1545207861 + }, + "origin": "nspr", + "dependencies": [ + "nspr", + "linux-headers", + "pkgconfig" + ], + "provides": [ + "pc:nspr=4.20.0", + "cmd:nspr-config" + ] + }, + "apache2-http2": { + "versions": { + "2.4.39-r0": 1554306835 + }, + "origin": "apache2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libnghttp2.so.14" + ] + }, + "graphviz": { + "versions": { + "2.40.1-r1": 1543926741 + }, + "origin": "graphviz", + "dependencies": [ + "/bin/sh", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libltdl.so.7", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libcdt.so.5=5.0.0", + "so:libcgraph.so.6=6.0.0", + "so:libgvc.so.6=6.0.0", + "so:libgvplugin_core.so.6=6.0.0", + "so:libgvplugin_dot_layout.so.6=6.0.0", + "so:libgvplugin_neato_layout.so.6=6.0.0", + "so:libgvplugin_pango.so.6=6.0.0", + "so:libgvplugin_xlib.so.6=6.0.0", + "so:libgvpr.so.2=2.0.0", + "so:liblab_gamut.so.1=1.0.0", + "so:libpathplan.so.4=4.0.0", + "so:libxdot.so.4=4.0.0", + "cmd:acyclic", + "cmd:bcomps", + "cmd:ccomps", + "cmd:circo", + "cmd:cluster", + "cmd:dijkstra", + "cmd:dot", + "cmd:dot2gxl", + "cmd:dot_builtins", + "cmd:dotty", + "cmd:edgepaint", + "cmd:fdp", + "cmd:gc", + "cmd:gml2gv", + "cmd:graphml2gv", + "cmd:gv2gml", + "cmd:gv2gxl", + "cmd:gvcolor", + "cmd:gvgen", + "cmd:gvmap", + "cmd:gvmap.sh", + "cmd:gvpack", + "cmd:gvpr", + "cmd:gxl2dot", + "cmd:gxl2gv", + "cmd:lneato", + "cmd:mm2gv", + "cmd:neato", + "cmd:nop", + "cmd:osage", + "cmd:patchwork", + "cmd:prune", + "cmd:sccmap", + "cmd:sfdp", + "cmd:tred", + "cmd:twopi", + "cmd:unflatten", + "cmd:vimdot" + ] + }, + "libxxf86dga-doc": { + "versions": { + "1.1.4-r2": 1542845774 + }, + "origin": "libxxf86dga" + }, + "liblockfile-doc": { + "versions": { + "1.14-r0": 1545075167 + }, + "origin": "liblockfile" + }, + "cloog": { + "versions": { + "0.18.4-r2": 1543226589 + }, + "origin": "cloog", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libisl.so.15" + ], + "provides": [ + "so:libcloog-isl.so.4=4.0.0", + "cmd:cloog" + ] + }, + "sysfsutils": { + "versions": { + "2.1.0-r8": 1542924685 + }, + "origin": "sysfsutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsysfs.so.2=2.0.1", + "cmd:dlist_test", + "cmd:get_device", + "cmd:get_driver", + "cmd:get_module", + "cmd:systool" + ] + }, + "acf-openldap": { + "versions": { + "1.0.1-r4": 1545154436 + }, + "origin": "acf-openldap", + "dependencies": [ + "acf-core", + "openldap", + "openldap-back-bdb" + ] + }, + "qemu-system-sparc": { + "versions": { + "3.1.0-r3": 1551107305 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-sparc" + ] + }, + "lua5.1-maxminddb": { + "versions": { + "0.1-r1": 1543226669 + }, + "origin": "lua-maxminddb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmaxminddb.so.0" + ] + }, + "linux-firmware-dpaa2": { + "versions": { + "20190322-r0": 1554980653 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "xorg-server-doc": { + "versions": { + "1.20.3-r1": 1543928068 + }, + "origin": "xorg-server" + }, + "libnftnl-libs": { + "versions": { + "1.1.1-r0": 1542893203 + }, + "origin": "libnftnl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjansson.so.4", + "so:libmnl.so.0" + ], + "provides": [ + "so:libnftnl.so.7=7.3.0" + ] + }, + "ncurses-terminfo": { + "versions": { + "6.1_p20190105-r0": 1546948260 + }, + "origin": "ncurses", + "dependencies": [ + "ncurses-terminfo-base", + "ncurses-terminfo-base=6.1_p20190105-r0" + ] + }, + "ncurses5": { + "versions": { + "5.9-r1": 1545292735 + }, + "origin": "ncurses5" + }, + "libisoburn-doc": { + "versions": { + "1.4.8-r0": 1544797304 + }, + "origin": "libisoburn" + }, + "speex": { + "versions": { + "1.2.0-r0": 1544799252 + }, + "origin": "speex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libspeex.so.1=1.5.1" + ] + }, + "varnish-dev": { + "versions": { + "6.1.1-r0": 1545300751 + }, + "origin": "varnish", + "dependencies": [ + "pkgconfig", + "varnish-libs=6.1.1-r0" + ], + "provides": [ + "pc:varnishapi=6.1.1" + ] + }, + "linux-firmware-ath10k": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "xf86-input-libinput": { + "versions": { + "0.28.1-r0": 1545207901 + }, + "origin": "xf86-input-libinput", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libinput.so.10" + ] + }, + "dbus-glib-doc": { + "versions": { + "0.108-r1": 1542824780 + }, + "origin": "dbus-glib" + }, + "openldap-back-monitor": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "bash-doc": { + "versions": { + "4.4.19-r1": 1542301270 + }, + "origin": "bash" + }, + "libmowgli": { + "versions": { + "2.1.3-r3": 1543998747 + }, + "origin": "libmowgli", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libmowgli-2.so.0=0.0.0" + ] + }, + "libvirt": { + "versions": { + "4.10.0-r1": 1547051658 + }, + "origin": "libvirt", + "dependencies": [ + "lvm2", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libdbus-1.so.3", + "so:libdevmapper.so.1.02", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libintl.so.8", + "so:libparted.so.2", + "so:libsasl2.so.3", + "so:libssh2.so.1", + "so:libtirpc.so.3", + "so:libvirt.so.0", + "so:libxml2.so.2", + "so:libyajl.so.2" + ], + "provides": [ + "cmd:virtlockd", + "cmd:virtlogd" + ] + }, + "eggdrop-gseen": { + "versions": { + "1.6.21-r2": 1545206950 + }, + "origin": "eggdrop", + "dependencies": [ + "tcl", + "so:libc.musl-x86_64.so.1", + "so:libtcl8.6.so" + ] + }, + "py-requests-oauthlib": { + "versions": { + "0.8.0-r1": 1545062567 + }, + "origin": "py-requests-oauthlib", + "dependencies": [ + "py-oauthlib", + "py-requests" + ] + }, + "rlog-doc": { + "versions": { + "1.4-r4": 1545061029 + }, + "origin": "rlog" + }, + "py3-mock": { + "versions": { + "2.0.0-r3": 1543925744 + }, + "origin": "py-mock", + "dependencies": [ + "py3-pbr", + "py3-six", + "python3" + ] + }, + "fetchmail": { + "versions": { + "6.3.26-r14": 1544001392 + }, + "origin": "fetchmail", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:fetchmail" + ] + }, + "perl-test-leaktrace": { + "versions": { + "0.16-r0": 1542821881 + }, + "origin": "perl-test-leaktrace", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "gtkman-lang": { + "versions": { + "1.0.1-r0": 1545075132 + }, + "origin": "gtkman", + "dependencies": [ + "py-gtk" + ] + }, + "font-misc-meltho": { + "versions": { + "1.0.3-r0": 1542981218 + }, + "origin": "font-misc-meltho", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "py3-newt": { + "versions": { + "0.52.20-r0": 1543924702 + }, + "origin": "newt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnewt.so.0.52" + ] + }, + "acf-nsd": { + "versions": { + "0.0.1-r0": 1545207717 + }, + "origin": "acf-nsd", + "dependencies": [ + "acf-core", + "nsd" + ] + }, + "quagga": { + "versions": { + "1.2.4-r1": 1545068941 + }, + "origin": "quagga", + "dependencies": [ + "iproute2", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libnetsnmp.so.35", + "so:libnetsnmpagent.so.35", + "so:libreadline.so.7" + ], + "provides": [ + "quagga-nhrp=1.2.4", + "so:libfpm_pb.so.0=0.0.0", + "so:libospf.so.0=0.0.0", + "so:libospfapiclient.so.0=0.0.0", + "so:libquagga_pb.so.0=0.0.0", + "so:libzebra.so.1=1.0.0", + "cmd:bgp_btoa", + "cmd:bgpd", + "cmd:isisd", + "cmd:nhrpd", + "cmd:ospf6d", + "cmd:ospfclient", + "cmd:ospfd", + "cmd:pimd", + "cmd:ripd", + "cmd:ripngd", + "cmd:test_igmpv3_join", + "cmd:vtysh", + "cmd:watchquagga", + "cmd:zebra" + ] + }, + "perl-net-openssh": { + "versions": { + "0.78-r0": 1545995475 + }, + "origin": "perl-net-openssh" + }, + "ircii": { + "versions": { + "20111115-r4": 1545213657 + }, + "origin": "ircii", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:irc", + "cmd:irc-20111115", + "cmd:ircbug", + "cmd:ircflush" + ] + }, + "apache2-utils": { + "versions": { + "2.4.39-r0": 1554306836 + }, + "origin": "apache2", + "dependencies": [ + "so:libapr-1.so.0", + "so:libaprutil-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:ab", + "cmd:checkgid", + "cmd:dbmmanage", + "cmd:htcacheclean", + "cmd:htdbm", + "cmd:htdigest", + "cmd:htpasswd", + "cmd:httxt2dbm", + "cmd:logresolve", + "cmd:rotatelogs" + ] + }, + "libwebp-tools": { + "versions": { + "1.0.1-r0": 1545856969 + }, + "origin": "libwebp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgif.so.7", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libtiff.so.5", + "so:libwebp.so.7", + "so:libwebpdemux.so.2", + "so:libwebpmux.so.3" + ], + "provides": [ + "cmd:cwebp", + "cmd:dwebp", + "cmd:gif2webp", + "cmd:img2webp", + "cmd:webpinfo", + "cmd:webpmux" + ] + }, + "db-doc": { + "versions": { + "5.3.28-r1": 1542819052 + }, + "origin": "db" + }, + "docbook-xsl": { + "versions": { + "1.79.1-r1": 1542304593 + }, + "origin": "docbook-xsl", + "dependencies": [ + "libxml2-utils", + "libxslt", + "docbook-xml", + "/bin/sh" + ] + }, + "zlib-dev": { + "versions": { + "1.2.11-r1": 1542300122 + }, + "origin": "zlib", + "dependencies": [ + "pkgconfig", + "zlib=1.2.11-r1" + ], + "provides": [ + "pc:zlib=1.2.11" + ] + }, + "perl-math-round-doc": { + "versions": { + "0.07-r0": 1545163052 + }, + "origin": "perl-math-round" + }, + "shorewall-core": { + "versions": { + "5.2.2-r0": 1548095414 + }, + "origin": "shorewall-core", + "provides": [ + "cmd:shorewall" + ] + }, + "lvm2-libs": { + "versions": { + "2.02.182-r0": 1543928934 + }, + "origin": "lvm2", + "dependencies": [ + "so:libaio.so.1", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper-event.so.1.02", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "so:libdevmapper-event-lvm2.so.2.02=2.02", + "so:liblvm2app.so.2.2=2.2", + "so:liblvm2cmd.so.2.02=2.02" + ] + }, + "perl-mozilla-ca": { + "versions": { + "20160104-r0": 1545223048 + }, + "origin": "perl-mozilla-ca" + }, + "perl-b-hooks-endofscope-doc": { + "versions": { + "0.24-r0": 1542973108 + }, + "origin": "perl-b-hooks-endofscope" + }, + "iw-doc": { + "versions": { + "4.14-r0": 1543249945 + }, + "origin": "iw" + }, + "lua5.1-sql-postgres": { + "versions": { + "2.3.5-r2": 1543924397 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "razor-doc": { + "versions": { + "2.85-r7": 1545292685 + }, + "origin": "razor" + }, + "gtest": { + "versions": { + "1.8.1-r0": 1542822390 + }, + "origin": "gtest", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgtest.so=0", + "so:libgtest_main.so=0" + ] + }, + "vanessa_adt-dev": { + "versions": { + "0.0.9-r0": 1545223297 + }, + "origin": "vanessa_adt", + "dependencies": [ + "vanessa_logger-dev", + "vanessa_adt=0.0.9-r0" + ] + }, + "mariadb-test": { + "versions": { + "10.3.13-r1": 1557431727 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb-common", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:liblzma.so.5", + "so:libpcre.so.1", + "so:libpcreposix.so.0", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:my_safe_process", + "cmd:mysql_client_test", + "cmd:mysqltest", + "cmd:mysqltest_embedded" + ] + }, + "perl-html-formattext-withlinks-doc": { + "versions": { + "0.15-r0": 1545075963 + }, + "origin": "perl-html-formattext-withlinks" + }, + "perl-file-listing": { + "versions": { + "6.04-r1": 1542821849 + }, + "origin": "perl-file-listing", + "dependencies": [ + "perl", + "perl-http-date" + ] + }, + "libbonobo": { + "versions": { + "2.32.1-r6": 1545299732 + }, + "origin": "libbonobo", + "dependencies": [ + "so:libORBit-2.so.0", + "so:libORBitCosNaming-2.so.0", + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libintl.so.8", + "so:libxml2.so.2" + ], + "provides": [ + "so:libbonobo-2.so.0=0.0.0", + "so:libbonobo-activation.so.4=4.0.0", + "cmd:activation-client", + "cmd:bonobo-activation-sysconf", + "cmd:bonobo-slay" + ] + }, + "linux-firmware-mellanox": { + "versions": { + "20190322-r0": 1554980650 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "libxcomposite": { + "versions": { + "0.4.4-r2": 1543925371 + }, + "origin": "libxcomposite", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXcomposite.so.1=1.0.0" + ] + }, + "libsm-dev": { + "versions": { + "1.2.2-r2": 1542824341 + }, + "origin": "libsm", + "dependencies": [ + "libsm=1.2.2-r2", + "pc:ice", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:sm=1.2.2" + ] + }, + "lxc-download": { + "versions": { + "3.1.0-r1": 1549966161 + }, + "origin": "lxc", + "dependencies": [ + "lxc", + "gnupg1", + "tar", + "xz", + "wget" + ] + }, + "asterisk-alsa": { + "versions": { + "15.7.1-r0": 1546247586 + }, + "origin": "asterisk", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-gobject-dev": { + "versions": { + "2.28.7-r0": 1543927527 + }, + "origin": "py-gobject", + "dependencies": [ + "pc:gobject-2.0", + "pc:libffi", + "pkgconfig", + "py-gobject=2.28.7-r0" + ], + "provides": [ + "pc:pygobject-2.0=2.28.7" + ] + }, + "libxfixes-dev": { + "versions": { + "5.0.3-r2": 1542823751 + }, + "origin": "libxfixes", + "dependencies": [ + "libxfixes=5.0.3-r2", + "pc:fixesproto>=5.0", + "pc:x11", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xfixes=5.0.3" + ] + }, + "lua-cmsgpack": { + "versions": { + "0.4.0-r0": 1545163347 + }, + "origin": "lua-cmsgpack", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "acf-chrony": { + "versions": { + "0.8.0-r2": 1545299915 + }, + "origin": "acf-chrony", + "dependencies": [ + "acf-core", + "lua-posix", + "chrony" + ] + }, + "dmvpn": { + "versions": { + "1.0.2-r0": 1553425622 + }, + "origin": "dmvpn", + "dependencies": [ + "augeas", + "bind-tools", + "lua5.2", + "lua5.2-cqueues", + "lua5.2-lyaml", + "lua5.2-ossl", + "lua5.2-posix", + "lua5.2-struct", + "lua-dmvpn", + "quagga", + "strongswan", + "tunnel" + ], + "provides": [ + "cmd:nhrp-events", + "cmd:setup-dmvpn" + ] + }, + "cups": { + "versions": { + "2.2.10-r0": 1545910847 + }, + "origin": "cups", + "dependencies": [ + "cups-client", + "poppler-utils", + "openssl", + "dbus", + "/bin/sh", + "cups-client=2.2.10-r0", + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libcupsimage.so.2", + "so:libdbus-1.so.3", + "so:libgcc_s.so.1", + "so:libpaper.so.1", + "so:libstdc++.so.6", + "so:libusb-1.0.so.0" + ], + "provides": [ + "cmd:cupsd", + "cmd:cupsfilter" + ] + }, + "lua5.3-filesystem": { + "versions": { + "1.7.0.2-r0": 1542820935 + }, + "origin": "lua-filesystem", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1" + ] + }, + "protobuf-c": { + "versions": { + "1.3.0-r6": 1545165152 + }, + "origin": "protobuf-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libprotobuf.so.17", + "so:libprotoc.so.17", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libprotobuf-c.so.1=1.0.0", + "cmd:protoc-c", + "cmd:protoc-gen-c" + ] + }, + "libcurl": { + "versions": { + "7.64.0-r1": 1551205311 + }, + "origin": "curl", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libnghttp2.so.14", + "so:libssh2.so.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libcurl.so.4=4.5.0" + ] + }, + "py3-pygments": { + "versions": { + "2.2.0-r0": 1542824946 + }, + "origin": "py-pygments", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:pygmentize-3" + ] + }, + "guile-doc": { + "versions": { + "2.0.14-r0": 1543227920 + }, + "origin": "guile" + }, + "ntfs-3g-libs": { + "versions": { + "2017.3.23-r1": 1542973172 + }, + "origin": "ntfs-3g", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libntfs-3g.so.88=88.0.0" + ] + }, + "py2-constantly": { + "versions": { + "15.1.0-r0": 1546513612 + }, + "origin": "py-constantly", + "dependencies": [ + "python2" + ] + }, + "menu-cache-dev": { + "versions": { + "0.5.1-r2": 1543999313 + }, + "origin": "menu-cache", + "dependencies": [ + "menu-cache=0.5.1-r2", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libmenu-cache=0.5.1" + ] + }, + "perl-dbd-mysql-doc": { + "versions": { + "4.050-r0": 1547124434 + }, + "origin": "perl-dbd-mysql" + }, + "xorg-server-xephyr": { + "versions": { + "1.20.3-r1": 1543928069 + }, + "origin": "xorg-server", + "dependencies": [ + "so:libGL.so.1", + "so:libX11-xcb.so.1", + "so:libX11.so.6", + "so:libXau.so.6", + "so:libXdmcp.so.6", + "so:libXfont2.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libepoxy.so.0", + "so:libpixman-1.so.0", + "so:libudev.so.1", + "so:libxcb-icccm.so.4", + "so:libxcb-image.so.0", + "so:libxcb-keysyms.so.1", + "so:libxcb-randr.so.0", + "so:libxcb-render-util.so.0", + "so:libxcb-render.so.0", + "so:libxcb-shape.so.0", + "so:libxcb-shm.so.0", + "so:libxcb-util.so.1", + "so:libxcb-xkb.so.1", + "so:libxcb-xv.so.0", + "so:libxcb.so.1", + "so:libxshmfence.so.1" + ], + "provides": [ + "cmd:Xephyr" + ] + }, + "lttng-ust-dev": { + "versions": { + "2.10.1-r0": 1543249618 + }, + "origin": "lttng-ust", + "dependencies": [ + "lttng-ust=2.10.1-r0", + "pc:liburcu-bp", + "pkgconfig" + ], + "provides": [ + "pc:lttng-ust=2.10.1" + ] + }, + "rsyslog-mmsnmptrapd": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "gtkmm-dev": { + "versions": { + "2.24.5-r0": 1543925600 + }, + "origin": "gtkmm", + "dependencies": [ + "atkmm-dev", + "gtk+2.0-dev", + "glibmm-dev", + "pangomm-dev", + "gtkmm=2.24.5-r0", + "pc:atkmm-1.6>=2.22.2", + "pc:giomm-2.4>=2.27.93", + "pc:gtk+-2.0>=2.24.0", + "pc:gtk+-unix-print-2.0", + "pc:pangomm-1.4>=2.27.1", + "pkgconfig" + ], + "provides": [ + "pc:gdkmm-2.4=2.24.5", + "pc:gtkmm-2.4=2.24.5" + ] + }, + "libexif-dev": { + "versions": { + "0.6.21-r3": 1545116758 + }, + "origin": "libexif", + "dependencies": [ + "libexif=0.6.21-r3", + "pkgconfig" + ], + "provides": [ + "pc:libexif=0.6.21" + ] + }, + "cabextract": { + "versions": { + "1.9-r0": 1545209003 + }, + "origin": "cabextract", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmspack.so.0" + ], + "provides": [ + "cmd:cabextract" + ] + }, + "mtools-doc": { + "versions": { + "4.0.23-r0": 1545858117 + }, + "origin": "mtools" + }, + "itstool-doc": { + "versions": { + "2.0.4-r3": 1545062595 + }, + "origin": "itstool" + }, + "lua5.3-libs": { + "versions": { + "5.3.5-r2": 1557162501 + }, + "origin": "lua5.3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblua-5.3.so.0=0.0.0" + ] + }, + "linux-vanilla-dev": { + "versions": { + "4.19.41-r0": 1557399963 + }, + "origin": "linux-vanilla", + "dependencies": [ + "perl", + "gmp-dev", + "elfutils-dev", + "bash", + "flex", + "bison", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libelf.so.1" + ] + }, + "ez-ipupdate": { + "versions": { + "3.0.10-r9": 1545116434 + }, + "origin": "ez-ipupdate", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ez-ipupdate" + ] + }, + "perl-xml-namespacesupport-doc": { + "versions": { + "1.12-r0": 1542893160 + }, + "origin": "perl-xml-namespacesupport" + }, + "gtk-xfce-engine": { + "versions": { + "3.2.0-r2": 1545292907 + }, + "origin": "gtk-xfce-engine" + }, + "py3-typing": { + "versions": { + "3.6.6-r0": 1543921910 + }, + "origin": "py-typing", + "dependencies": [ + "python3" + ] + }, + "neon": { + "versions": { + "0.30.2-r5": 1542883670 + }, + "origin": "neon", + "dependencies": [ + "ca-certificates", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libexpat.so.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libneon.so.27=27.3.2" + ] + }, + "ppp-radius": { + "versions": { + "2.4.7-r6": 1543999020 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-asn1": { + "versions": { + "0.4.2-r0": 1542821565 + }, + "origin": "py-asn1", + "dependencies": [ + "python2" + ] + }, + "perl-ipc-run3": { + "versions": { + "0.048-r0": 1542973048 + }, + "origin": "perl-ipc-run3", + "dependencies": [ + "perl" + ] + }, + "dansguardian-doc": { + "versions": { + "2.12.0.3-r4": 1545207099 + }, + "origin": "dansguardian" + }, + "gpgmepp": { + "versions": { + "1.12.0-r3": 1543932321 + }, + "origin": "gpgme", + "dependencies": [ + "gnupg", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgpgme.so.11", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgpgmepp.so.6=6.8.0" + ] + }, + "rsnapshot-doc": { + "versions": { + "1.4.2-r0": 1544799298 + }, + "origin": "rsnapshot" + }, + "ruby": { + "versions": { + "2.5.5-r0": 1557164838 + }, + "origin": "ruby", + "dependencies": [ + "ca-certificates", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.5" + ], + "provides": [ + "cmd:erb", + "cmd:gem", + "cmd:ruby" + ] + }, + "perl-html-tree": { + "versions": { + "5.07-r0": 1542893578 + }, + "origin": "perl-html-tree", + "dependencies": [ + "perl-html-tagset", + "perl-html-parser" + ], + "provides": [ + "cmd:htmltree" + ] + }, + "orc-doc": { + "versions": { + "0.4.28-r0": 1542883449 + }, + "origin": "orc" + }, + "git-gui": { + "versions": { + "2.20.1-r0": 1545214200 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0", + "tcl", + "tk" + ] + }, + "the_silver_searcher-zsh-completion": { + "versions": { + "2.1.0-r2": 1543998779 + }, + "origin": "the_silver_searcher" + }, + "perl-file-rsync": { + "versions": { + "0.74-r1": 1545299967 + }, + "origin": "perl-file-rsync", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "xrdb": { + "versions": { + "1.1.1-r0": 1542883479 + }, + "origin": "xrdb", + "dependencies": [ + "mcpp", + "so:libX11.so.6", + "so:libXmuu.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xrdb" + ] + }, + "iasl": { + "versions": { + "20181213-r0": 1545995081 + }, + "origin": "acpica", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:iasl" + ] + }, + "lsof-doc": { + "versions": { + "4.91-r0": 1545235376 + }, + "origin": "lsof" + }, + "ruby-json": { + "versions": { + "2.5.5-r0": 1557164837 + }, + "origin": "ruby", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libruby.so.2.5" + ] + }, + "ruby-libs": { + "versions": { + "2.5.5-r0": 1557164838 + }, + "origin": "ruby", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgmp.so.10", + "so:libreadline.so.7", + "so:libssl.so.1.1", + "so:libyaml-0.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libruby.so.2.5=2.5.5" + ] + }, + "mariadb-server-utils": { + "versions": { + "10.3.13-r1": 1557431735 + }, + "origin": "mariadb", + "dependencies": [ + "perl", + "mariadb-common=10.3.13-r1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mysql_convert_table_format", + "cmd:mysql_setpermission", + "cmd:mysql_upgrade", + "cmd:mysqld_multi", + "cmd:mysqld_safe_helper", + "cmd:mysqlhotcopy", + "cmd:perror" + ] + }, + "dovecot-lmtpd": { + "versions": { + "2.3.6-r0": 1557134097 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libdovecot-lda.so.0", + "so:libdovecot-storage.so.0", + "so:libdovecot.so.0" + ] + }, + "apache-mod-auth-radius": { + "versions": { + "1.5.8-r3": 1545224139 + }, + "origin": "apache-mod-auth-radius", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1" + ] + }, + "linux-firmware-ti-connectivity": { + "versions": { + "20190322-r0": 1554980647 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "libatomic_ops": { + "versions": { + "7.6.4-r0": 1543226877 + }, + "origin": "libatomic_ops", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libatomic_ops.so.1=1.1.1", + "so:libatomic_ops_gpl.so.1=1.1.2" + ] + }, + "gstreamer-lang": { + "versions": { + "1.14.4-r0": 1543254159 + }, + "origin": "gstreamer" + }, + "xinit-doc": { + "versions": { + "1.4.0-r1": 1542973236 + }, + "origin": "xinit" + }, + "perl-mail-spamassassin": { + "versions": { + "3.4.1-r2": 1545061232, + "3.4.2-r0": 1545061982 + }, + "origin": "spamassassin", + "dependencies": [ + "perl", + "gnupg", + "perl-html-parser", + "perl-digest-sha1", + "perl-netaddr-ip", + "perl-net-dns", + "perl-mail-dkim", + "perl-mime-base64", + "perl-db_file", + "perl-time-hires", + "perl-libwww" + ], + "provides": [ + "cmd:sa-awl", + "cmd:sa-check_spamd", + "cmd:sa-compile", + "cmd:sa-learn", + "cmd:sa-update", + "cmd:spamassassin", + "cmd:spamc", + "cmd:spamd" + ] + }, + "ncftp": { + "versions": { + "3.2.6-r1": 1544000464 + }, + "origin": "ncftp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libncftp.so.3=3", + "cmd:ncftp", + "cmd:ncftpbatch", + "cmd:ncftpget", + "cmd:ncftpls", + "cmd:ncftpput", + "cmd:ncftpspooler" + ] + }, + "findutils": { + "versions": { + "4.6.0-r1": 1545073420 + }, + "origin": "findutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:find", + "cmd:locate", + "cmd:updatedb", + "cmd:xargs" + ] + }, + "nagios-plugins-ping": { + "versions": { + "2.2.1-r6": 1543933910 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "python3-wininst": { + "versions": { + "3.6.8-r2": 1554747639 + }, + "origin": "python3" + }, + "py-pylast": { + "versions": { + "2.4.0-r0": 1545301102 + }, + "origin": "py-pylast" + }, + "perl-canary-stability": { + "versions": { + "2012-r0": 1543223266 + }, + "origin": "perl-canary-stability" + }, + "nagios-plugins-sensors": { + "versions": { + "2.2.1-r6": 1543933911 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "grep", + "lm_sensors" + ] + }, + "gtk-engines-lang": { + "versions": { + "2.21.0-r2": 1545289335 + }, + "origin": "gtk-engines", + "dependencies": [ + "gtk-engines-clearlooks", + "gtk-engines-crux", + "gtk-engines-industrial", + "gtk-engines-mist", + "gtk-engines-redmond", + "gtk-engines-thinice" + ] + }, + "apache2-mod-wsgi": { + "versions": { + "4.5.24-r0": 1545213947 + }, + "origin": "apache2-mod-wsgi", + "dependencies": [ + "apache2", + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "apr-util-dbd_pgsql": { + "versions": { + "1.6.1-r5": 1545061009 + }, + "origin": "apr-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "font-bitstream-75dpi": { + "versions": { + "1.0.3-r0": 1545075927 + }, + "origin": "font-bitstream-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "zfs": { + "versions": { + "0.7.12-r1": 1552933680 + }, + "origin": "zfs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libnvpair.so.1", + "so:libuuid.so.1", + "so:libuutil.so.1", + "so:libzfs.so.2", + "so:libzfs_core.so.1", + "so:libzpool.so.2" + ], + "provides": [ + "cmd:fsck.zfs", + "cmd:mount.zfs", + "cmd:raidz_test", + "cmd:zdb", + "cmd:zed", + "cmd:zfs", + "cmd:zgenhostid", + "cmd:zhack", + "cmd:zinject", + "cmd:zpios", + "cmd:zpool", + "cmd:zstreamdump", + "cmd:ztest" + ] + }, + "py-incremental": { + "versions": { + "17.5.0-r0": 1546513618 + }, + "origin": "py-incremental" + }, + "perl-package-anon": { + "versions": { + "0.05-r4": 1545116809 + }, + "origin": "perl-package-anon", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "mdadm-misc": { + "versions": { + "4.1-r0": 1545858163 + }, + "origin": "mdadm", + "dependencies": [ + "mdadm", + "bash" + ], + "provides": [ + "cmd:handle-mdadm-events", + "cmd:mdcheck" + ] + }, + "lua5.1-expat": { + "versions": { + "1.3.0-r2": 1544000275 + }, + "origin": "lua-expat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "libusb-compat": { + "versions": { + "0.1.5-r4": 1543921873 + }, + "origin": "libusb-compat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libusb-0.1.so.4=4.4.4" + ] + }, + "libxdg-basedir-dev": { + "versions": { + "1.2.0-r0": 1545116744 + }, + "origin": "libxdg-basedir", + "dependencies": [ + "libxdg-basedir=1.2.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libxdg-basedir=1.2.0" + ] + }, + "opus-dev": { + "versions": { + "1.3-r0": 1545069004 + }, + "origin": "opus", + "dependencies": [ + "opus=1.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:opus=1.3" + ] + }, + "udisks-lang": { + "versions": { + "1.0.5-r3": 1545069134 + }, + "origin": "udisks" + }, + "clutter-doc": { + "versions": { + "1.26.2-r2": 1545293147 + }, + "origin": "clutter" + }, + "tcl-tls": { + "versions": { + "1.7.16-r0": 1545165056 + }, + "origin": "tcl-tls", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "ulogd-pgsql": { + "versions": { + "2.0.7-r0": 1545300896 + }, + "origin": "ulogd", + "dependencies": [ + "ulogd", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "dahdi-tools-doc": { + "versions": { + "2.11.1-r0": 1543932614 + }, + "origin": "dahdi-tools" + }, + "py-pyflakes": { + "versions": { + "1.6.0-r3": 1545073867 + }, + "origin": "py-pyflakes", + "dependencies": [ + "py3-pyflakes=1.6.0-r3" + ] + }, + "email": { + "versions": { + "3.1.4-r7": 1543925854 + }, + "origin": "email", + "dependencies": [ + "openssl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:email" + ] + }, + "libnetfilter_log": { + "versions": { + "1.0.1-r2": 1543924455 + }, + "origin": "libnetfilter_log", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnfnetlink.so.0" + ], + "provides": [ + "so:libnetfilter_log.so.1=1.1.0", + "so:libnetfilter_log_libipulog.so.1=1.0.0" + ] + }, + "perl-xml-rss-doc": { + "versions": { + "1.60-r0": 1545301035 + }, + "origin": "perl-xml-rss" + }, + "smokeping-doc": { + "versions": { + "2.7.3-r3": 1545734482 + }, + "origin": "smokeping" + }, + "hexchat-lang": { + "versions": { + "2.14.2-r0": 1545746058 + }, + "origin": "hexchat" + }, + "scrot": { + "versions": { + "0.8.18-r0": 1545299550 + }, + "origin": "scrot", + "dependencies": [ + "so:libImlib2.so.1", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libgiblib.so.1" + ], + "provides": [ + "cmd:scrot" + ] + }, + "alpine-baselayout": { + "versions": { + "3.1.0-r3": 1548276389 + }, + "origin": "alpine-baselayout", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mkmntdirs" + ] + }, + "py-markupsafe": { + "versions": { + "1.1.0-r0": 1544791904 + }, + "origin": "py-markupsafe" + }, + "perl-git": { + "versions": { + "2.20.1-r0": 1545214194 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0", + "perl-error" + ] + }, + "gtkspell-lang": { + "versions": { + "2.0.16-r7": 1545215216 + }, + "origin": "gtkspell" + }, + "freeradius-pam": { + "versions": { + "3.0.17-r5": 1556201989 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0" + ], + "provides": [ + "freeradius3-pam=3.0.17-r5", + "so:rlm_pam.so=0" + ] + }, + "python2-dbg": { + "versions": { + "2.7.16-r1": 1557171398 + }, + "origin": "python2" + }, + "gst-plugins-base0.10-lang": { + "versions": { + "0.10.36-r4": 1543928811 + }, + "origin": "gst-plugins-base0.10" + }, + "openrc-dev": { + "versions": { + "0.39.2-r3": 1548592317 + }, + "origin": "openrc", + "dependencies": [ + "openrc=0.39.2-r3", + "pkgconfig" + ], + "provides": [ + "pc:einfo=0.39.2", + "pc:openrc=0.39.2" + ] + }, + "apk-tools": { + "versions": { + "2.10.3-r1": 1547112494 + }, + "origin": "apk-tools", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:apk" + ] + }, + "flashcache-utils": { + "versions": { + "3.1.3-r0": 1545209189 + }, + "origin": "flashcache-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:flashcache_create", + "cmd:flashcache_destroy", + "cmd:flashcache_load", + "cmd:flashcache_setioctl", + "cmd:get_agsize" + ] + }, + "pinentry-doc": { + "versions": { + "1.1.0-r0": 1543932175 + }, + "origin": "pinentry" + }, + "iwlwifi-6000-ucode": { + "versions": { + "9.221.4.1-r0": 1545256922 + }, + "origin": "iwlwifi-6000-ucode" + }, + "kamailio-sipdump": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1" + ] + }, + "cegui06": { + "versions": { + "0.6.2b-r15": 1543935898 + }, + "origin": "cegui06", + "dependencies": [ + "so:libGL.so.1", + "so:libGLEW.so.2.1", + "so:libGLU.so.1", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libfreetype.so.6", + "so:libgcc_s.so.1", + "so:liblua.so.5", + "so:libpcre.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libCEGUIBase-0.6.2.so=0", + "so:libCEGUIExpatParser-0.6.2.so=0", + "so:libCEGUIFalagardWRBase-0.6.2.so=0", + "so:libCEGUILuaScriptModule-0.6.2.so=0", + "so:libCEGUIOpenGLRenderer-0.6.2.so=0", + "so:libCEGUITGAImageCodec-0.6.2.so=0", + "so:libCEGUITinyXMLParser-0.6.2.so=0", + "so:libCEGUItoluapp-0.6.2.so=0" + ] + }, + "usbredir-server": { + "versions": { + "0.7.1-r0": 1543223286 + }, + "origin": "usbredir", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirhost.so.1" + ], + "provides": [ + "cmd:usbredirserver" + ] + }, + "beep-doc": { + "versions": { + "1.3-r2": 1545214043 + }, + "origin": "beep" + }, + "postfix-pcre": { + "versions": { + "3.3.2-r0": 1547130363 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ], + "provides": [ + "so:postfix-pcre.so=0" + ] + }, + "xgnokii": { + "versions": { + "0.6.31-r8": 1545300479 + }, + "origin": "gnokii", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgnokii.so.7", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:xgnokii" + ] + }, + "tmux-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308087 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "snort-doc": { + "versions": { + "2.9.12-r0": 1545301013 + }, + "origin": "snort" + }, + "hdparm": { + "versions": { + "9.58-r0": 1545745823 + }, + "origin": "hdparm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:hdparm", + "cmd:idectl", + "cmd:ultrabayd" + ] + }, + "perl-net-cidr-lite-doc": { + "versions": { + "0.21-r4": 1543924461 + }, + "origin": "perl-net-cidr-lite" + }, + "unionfs-fuse-doc": { + "versions": { + "1.0-r0": 1545163917 + }, + "origin": "unionfs-fuse" + }, + "gnupg1": { + "versions": { + "1.4.23-r0": 1545302139 + }, + "origin": "gnupg1", + "dependencies": [ + "pinentry", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libz.so.1" + ], + "provides": [ + "gnupg=1.4.23-r0", + "cmd:gpg", + "cmd:gpg-zip", + "cmd:gpgsplit", + "cmd:gpgv" + ] + }, + "git-bash-completion": { + "versions": { + "2.20.1-r0": 1545214194 + }, + "origin": "git" + }, + "lz4-libs": { + "versions": { + "1.8.3-r2": 1545154434 + }, + "origin": "lz4", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblz4.so.1=1.8.3" + ] + }, + "perl-io-socket-inet6-doc": { + "versions": { + "2.72-r0": 1543932732 + }, + "origin": "perl-io-socket-inet6" + }, + "openjpeg-dev": { + "versions": { + "2.3.0-r3": 1552584664 + }, + "origin": "openjpeg", + "dependencies": [ + "openjpeg=2.3.0-r3", + "pkgconfig" + ], + "provides": [ + "pc:libopenjp2=2.3.0" + ] + }, + "uwsgi-redislog": { + "versions": { + "2.0.17.1-r0": 1545062205 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "thin-provisioning-tools-doc": { + "versions": { + "0.7.1-r1": 1545208656 + }, + "origin": "thin-provisioning-tools" + }, + "mcabber": { + "versions": { + "1.1.0-r2": 1545214061 + }, + "origin": "mcabber", + "dependencies": [ + "beep", + "so:libc.musl-x86_64.so.1", + "so:libenchant.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgpgme.so.11", + "so:libidn.so.12", + "so:libloudmouth-1.so.0", + "so:libncursesw.so.6", + "so:libotr.so.2", + "so:libpanelw.so.6" + ], + "provides": [ + "cmd:mcabber" + ] + }, + "nasm-doc": { + "versions": { + "2.13.03-r0": 1542824085 + }, + "origin": "nasm" + }, + "lxc-openrc": { + "versions": { + "3.1.0-r1": 1549966161 + }, + "origin": "lxc" + }, + "perl-html-format-doc": { + "versions": { + "2.12-r0": 1545075957 + }, + "origin": "perl-html-format" + }, + "perl-test-needs-doc": { + "versions": { + "0.002005-r1": 1542821778 + }, + "origin": "perl-test-needs" + }, + "gsm-doc": { + "versions": { + "1.0.18-r0": 1543927819 + }, + "origin": "gsm" + }, + "kmod-openrc": { + "versions": { + "24-r1": 1542845354 + }, + "origin": "kmod" + }, + "perl-http-cookies": { + "versions": { + "6.04-r0": 1542821810 + }, + "origin": "perl-http-cookies", + "dependencies": [ + "perl", + "perl-http-date", + "perl-http-message" + ] + }, + "perl-xml-simple": { + "versions": { + "2.25-r0": 1545062591 + }, + "origin": "perl-xml-simple", + "dependencies": [ + "perl-xml-parser", + "perl" + ] + }, + "wxgtk2.8-dev": { + "versions": { + "2.8.12.1-r4": 1545075285 + }, + "origin": "wxgtk2.8", + "dependencies": [ + "gtk+2.0-dev", + "zlib-dev", + "tiff-dev", + "libpng-dev", + "libjpeg-turbo-dev", + "expat-dev", + "libsm-dev", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libwx_baseu-2.8.so.0", + "so:libwx_baseu_xml-2.8.so.0", + "wxgtk2.8-base=2.8.12.1-r4", + "wxgtk2.8-media=2.8.12.1-r4", + "wxgtk2.8=2.8.12.1-r4" + ], + "provides": [ + "cmd:wxrc", + "cmd:wxrc-2.8" + ] + }, + "lua5.1-lyaml": { + "versions": { + "6.2-r3": 1545076419 + }, + "origin": "lua-lyaml", + "dependencies": [ + "lua5.1", + "lua5.1-stdlib-normalize", + "so:libc.musl-x86_64.so.1", + "so:libyaml-0.so.2" + ] + }, + "perl-module-pluggable-doc": { + "versions": { + "5.2-r0": 1542973005 + }, + "origin": "perl-module-pluggable" + }, + "perl-date-calc": { + "versions": { + "6.4-r1": 1545060709 + }, + "origin": "perl-date-calc", + "dependencies": [ + "perl" + ] + }, + "command-not-found": { + "versions": { + "0.3-r0": 1545062749 + }, + "origin": "command-not-found" + }, + "perl-term-table": { + "versions": { + "0.012-r0": 1542972990 + }, + "origin": "perl-term-table", + "dependencies": [ + "perl-importer" + ] + }, + "websocket++": { + "versions": { + "0.8.1-r0": 1543222726 + }, + "origin": "websocket++" + }, + "libasr": { + "versions": { + "1.0.2-r9": 1543933802 + }, + "origin": "libasr", + "dependencies": [ + "so:libbsd.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libasr.so.0=0.0.2" + ] + }, + "lua-struct": { + "versions": { + "0.2-r2": 1545076457 + }, + "origin": "lua-struct" + }, + "uwsgi": { + "versions": { + "2.0.17.1-r0": 1545062215 + }, + "origin": "uwsgi", + "dependencies": [ + "mailcap", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.1.1", + "so:libjansson.so.4", + "so:libpcre.so.1", + "so:libssl.so.1.1", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:uwsgi" + ] + }, + "unzip": { + "versions": { + "6.0-r4": 1542301513 + }, + "origin": "unzip", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:funzip", + "cmd:unzip", + "cmd:unzipsfx", + "cmd:zipgrep", + "cmd:zipinfo" + ] + }, + "talloc-doc": { + "versions": { + "2.1.14-r0": 1543220639 + }, + "origin": "talloc" + }, + "py-asn1-modules": { + "versions": { + "0.2.2-r0": 1543928878 + }, + "origin": "py-asn1-modules" + }, + "fish-tools": { + "versions": { + "2.7.1-r0": 1545302079 + }, + "origin": "fish", + "dependencies": [ + "fish", + "python" + ] + }, + "hostapd-openrc": { + "versions": { + "2.7-r0": 1547822982 + }, + "origin": "hostapd" + }, + "perl-cgi": { + "versions": { + "4.40-r0": 1543242201 + }, + "origin": "perl-cgi", + "dependencies": [ + "perl-html-parser" + ] + }, + "perl-net-dns-resolver-mock-doc": { + "versions": { + "1.20170814-r0": 1545061182 + }, + "origin": "perl-net-dns-resolver-mock" + }, + "py-typing": { + "versions": { + "3.6.6-r0": 1543921910 + }, + "origin": "py-typing" + }, + "polkit-dev": { + "versions": { + "0.105-r9": 1547130960 + }, + "origin": "polkit", + "dependencies": [ + "eggdbus-dev", + "dbus-glib-dev", + "pc:gio-2.0>=2.18", + "pc:glib-2.0>=2.18", + "pkgconfig", + "polkit=0.105-r9" + ], + "provides": [ + "pc:polkit-agent-1=0.105", + "pc:polkit-backend-1=0.105", + "pc:polkit-gobject-1=0.105" + ] + }, + "lvm2-dev": { + "versions": { + "2.02.182-r0": 1543928933 + }, + "origin": "lvm2", + "dependencies": [ + "linux-headers", + "device-mapper-event-libs=2.02.182-r0", + "device-mapper-libs=2.02.182-r0", + "lvm2-libs=2.02.182-r0", + "pc:blkid", + "pkgconfig" + ], + "provides": [ + "pc:devmapper-event=1.02.152", + "pc:devmapper=1.02.152", + "pc:lvm2app=2.2" + ] + }, + "postgresql-doc": { + "versions": { + "11.2-r0": 1554274175 + }, + "origin": "postgresql" + }, + "openvpn-ad-check": { + "versions": { + "1.1-r2": 1543223258 + }, + "origin": "openvpn-ad-check", + "dependencies": [ + "openvpn", + "lua-ldap" + ] + }, + "net-snmp-gui": { + "versions": { + "5.8-r0": 1543923351 + }, + "origin": "net-snmp", + "dependencies": [ + "perl-net-snmp" + ], + "provides": [ + "cmd:tkmib" + ] + }, + "cyrus-sasl-digestmd5": { + "versions": { + "2.1.27-r1": 1550353528 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "nss-static": { + "versions": { + "3.41-r0": 1545307910 + }, + "origin": "nss" + }, + "libsexy": { + "versions": { + "0.1.11-r8": 1544001180 + }, + "origin": "libsexy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "so:libsexy.so.2=2.0.4" + ] + }, + "lucene++-dev": { + "versions": { + "3.0.7-r8": 1545256825 + }, + "origin": "lucene++", + "dependencies": [ + "boost-dev", + "lucene++=3.0.7-r8", + "pkgconfig" + ], + "provides": [ + "pc:liblucene++-contrib=3.0.7", + "pc:liblucene++=3.0.7" + ] + }, + "libressl-dbg": { + "versions": { + "2.7.5-r0": 1551116832 + }, + "origin": "libressl" + }, + "py-egenix-mx-base-dev": { + "versions": { + "3.2.9-r0": 1542821603 + }, + "origin": "py-egenix-mx-base" + }, + "devicemaster-linux": { + "versions": { + "7.15-r0": 1543999030 + }, + "origin": "devicemaster-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:nslinkadmin", + "cmd:nslinkd", + "cmd:nslinkrelease", + "cmd:nslinktool" + ] + }, + "perl-extutils-installpaths-doc": { + "versions": { + "0.012-r0": 1542924654 + }, + "origin": "perl-extutils-installpaths" + }, + "perl-cpan-meta-check": { + "versions": { + "0.014-r0": 1542845603 + }, + "origin": "perl-cpan-meta-check", + "dependencies": [ + "perl-module-metadata>=1.000023" + ] + }, + "perl-carp": { + "versions": { + "1.38-r0": 1544001407 + }, + "origin": "perl-carp" + }, + "boost-prg_exec_monitor": { + "versions": { + "1.67.0-r2": 1542823632 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_prg_exec_monitor-mt.so.1.67.0=1.67.0", + "so:libboost_prg_exec_monitor.so.1.67.0=1.67.0" + ] + }, + "lua5.3-ossl": { + "versions": { + "20180708-r2": 1545076448 + }, + "origin": "lua-ossl", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "py-requests": { + "versions": { + "2.19.1-r0": 1542825031 + }, + "origin": "py-requests", + "dependencies": [ + "py-chardet", + "py-idna", + "py-certifi", + "py-urllib3" + ] + }, + "py3-jinja2": { + "versions": { + "2.10-r2": 1542824937 + }, + "origin": "py-jinja2", + "dependencies": [ + "py3-markupsafe", + "python3" + ] + }, + "snort-dev": { + "versions": { + "2.9.12-r0": 1545301013 + }, + "origin": "snort", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:snort=2.9.12", + "pc:snort_output=2.9.12", + "pc:snort_preproc=2.9.12" + ] + }, + "perl-test-warn": { + "versions": { + "0.36-r0": 1543242189 + }, + "origin": "perl-test-warn", + "dependencies": [ + "perl-sub-uplevel" + ] + }, + "apr-util-dbd_mysql": { + "versions": { + "1.6.1-r5": 1545061009 + }, + "origin": "apr-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "lua5.3-rex-pcre": { + "versions": { + "2.9.0-r0": 1545209205 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ] + }, + "perl-crypt-eksblowfish-doc": { + "versions": { + "0.009-r4": 1545213645 + }, + "origin": "perl-crypt-eksblowfish" + }, + "harfbuzz": { + "versions": { + "2.2.0-r0": 1545249743 + }, + "origin": "harfbuzz", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgraphite2.so.3" + ], + "provides": [ + "so:libharfbuzz-gobject.so.0=0.20200.0", + "so:libharfbuzz-subset.so.0=0.20200.0", + "so:libharfbuzz.so.0=0.20200.0" + ] + }, + "encfs-lang": { + "versions": { + "1.9.5-r3": 1545061083 + }, + "origin": "encfs" + }, + "pgtcl": { + "versions": { + "2.1.0-r0": 1545301848 + }, + "origin": "pgtcl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "libdvdnav-dev": { + "versions": { + "5.0.3-r1": 1543999001 + }, + "origin": "libdvdnav", + "dependencies": [ + "libdvdread-dev>=5.0.3", + "libdvdnav=5.0.3-r1", + "pc:dvdread>=4.1.2", + "pkgconfig" + ], + "provides": [ + "pc:dvdnav=5.0.3" + ] + }, + "libsexy-doc": { + "versions": { + "0.1.11-r8": 1544001179 + }, + "origin": "libsexy" + }, + "kamailio-xmpp": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1" + ] + }, + "swig-doc": { + "versions": { + "3.0.12-r4": 1543228065 + }, + "origin": "swig" + }, + "perl-io-multiplex": { + "versions": { + "1.16-r1": 1545163454 + }, + "origin": "perl-io-multiplex", + "dependencies": [ + "perl" + ] + }, + "perl-cache-cache-doc": { + "versions": { + "1.08-r0": 1543223056 + }, + "origin": "perl-cache-cache" + }, + "libidl-dev": { + "versions": { + "0.8.14-r2": 1543223345 + }, + "origin": "libidl", + "dependencies": [ + "libidl=0.8.14-r2", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:libIDL-2.0=0.8.14" + ] + }, + "rrdcollect": { + "versions": { + "0.2.10-r1": 1545207035 + }, + "origin": "rrdcollect", + "dependencies": [ + "rrdtool", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rrdcollect" + ] + }, + "perl-params-validate-doc": { + "versions": { + "1.29-r0": 1543238893 + }, + "origin": "perl-params-validate" + }, + "libdaemon-dev": { + "versions": { + "0.14-r2": 1543925256 + }, + "origin": "libdaemon", + "dependencies": [ + "libdaemon=0.14-r2", + "pkgconfig" + ], + "provides": [ + "pc:libdaemon=0.14" + ] + }, + "perl-module-install": { + "versions": { + "1.19-r0": 1542893357 + }, + "origin": "perl-module-install" + }, + "ctags-doc": { + "versions": { + "5.8-r5": 1545301883 + }, + "origin": "ctags" + }, + "btrfs-progs-extra": { + "versions": { + "4.19.1-r0": 1545665053 + }, + "origin": "btrfs-progs", + "dependencies": [ + "btrfs-progs", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libext2fs.so.2", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:btrfs-convert", + "cmd:btrfs-find-root", + "cmd:btrfs-image", + "cmd:btrfs-map-logical", + "cmd:btrfs-select-super", + "cmd:btrfstune" + ] + }, + "squid-lang-sk": { + "versions": { + "4.4-r1": 1545216329 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libnet-doc": { + "versions": { + "1.1.6-r2": 1545060612 + }, + "origin": "libnet" + }, + "mqtt-exec": { + "versions": { + "0.4-r4": 1545162970 + }, + "origin": "mqtt-exec", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ], + "provides": [ + "cmd:mqtt-exec" + ] + }, + "rsyslog-hiredis": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.14" + ], + "provides": [ + "rsyslog-omhiredis=8.40.0-r3" + ] + }, + "collectd-dns": { + "versions": { + "5.8.0-r3": 1546422678 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ] + }, + "perl-lwp-useragent-determined-doc": { + "versions": { + "1.07-r0": 1545062795 + }, + "origin": "perl-lwp-useragent-determined" + }, + "mosh-server": { + "versions": { + "1.3.2-r7": 1543932001 + }, + "origin": "mosh", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libprotobuf.so.17", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:mosh-server" + ] + }, + "lua-stdlib-normalize": { + "versions": { + "2.0.2-r0": 1542883720 + }, + "origin": "lua-stdlib-normalize", + "dependencies": [ + "lua", + "lua-stdlib-debug" + ], + "provides": [ + "lua5.1-stdlib-normalize=2.0.2-r0", + "lua5.2-stdlib-normalize=2.0.2-r0", + "lua5.3-stdlib-normalize=2.0.2-r0" + ] + }, + "spl": { + "versions": { + "0.7.8-r0": 1545117988 + }, + "origin": "spl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:splat", + "cmd:splslab.py" + ] + }, + "py-fuse": { + "versions": { + "0.2.1-r1": 1543935349 + }, + "origin": "py-fuse", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2", + "so:libpython2.7.so.1.0" + ] + }, + "json-c0.12-dev": { + "versions": { + "0.12.1-r2": 1545301158 + }, + "origin": "json-c0.12", + "dependencies": [ + "json-c0.12=0.12.1-r2", + "pkgconfig" + ], + "provides": [ + "pc:json-c=0.12.1" + ] + }, + "py3-mccabe": { + "versions": { + "0.6.1-r1": 1545060943 + }, + "origin": "py-mccabe", + "dependencies": [ + "python3" + ] + }, + "linux-firmware-sxg": { + "versions": { + "20190322-r0": 1554980647 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "postfix-pgsql": { + "versions": { + "3.3.2-r0": 1547130363 + }, + "origin": "postfix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ], + "provides": [ + "so:postfix-pgsql.so=0" + ] + }, + "perl-html-tagset": { + "versions": { + "3.20-r1": 1542821829 + }, + "origin": "perl-html-tagset" + }, + "py3-crypto": { + "versions": { + "2.6.1-r2": 1543223466 + }, + "origin": "py-crypto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libpython3.6m.so.1.0" + ] + }, + "debian-archive-keyring": { + "versions": { + "2018.1-r0": 1543999031 + }, + "origin": "debian-archive-keyring", + "dependencies": [ + "gnupg" + ] + }, + "galculator-lang": { + "versions": { + "2.1.4-r0": 1545076847 + }, + "origin": "galculator" + }, + "pmacct": { + "versions": { + "1.7.0-r0": 1545116555 + }, + "origin": "pmacct", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjansson.so.4", + "so:libmariadb.so.3", + "so:libpcap.so.1", + "so:libpq.so.5", + "so:libsqlite3.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:nfacctd", + "cmd:pmacct", + "cmd:pmacctd", + "cmd:pmbgpd", + "cmd:pmbmpd", + "cmd:pmtelemetryd", + "cmd:sfacctd" + ] + }, + "libnftnl": { + "versions": { + "1.1.1-r0": 1542893203 + }, + "origin": "libnftnl" + }, + "py2-musicbrainzngs": { + "versions": { + "0.6-r2": 1543240635 + }, + "origin": "py-musicbrainzngs", + "dependencies": [ + "python2" + ] + }, + "py2-gobject3": { + "versions": { + "3.28.2-r0": 1543925241 + }, + "origin": "py-gobject3", + "dependencies": [ + "py2-cairo", + "so:libc.musl-x86_64.so.1", + "so:libcairo-gobject.so.2", + "so:libcairo.so.2", + "so:libffi.so.6", + "so:libgirepository-1.0.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ] + }, + "libiec61883-utils": { + "versions": { + "1.2.0-r2": 1543999349 + }, + "origin": "libiec61883", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libiec61883.so.0", + "so:libraw1394.so.11" + ], + "provides": [ + "cmd:plugctl", + "cmd:plugreport" + ] + }, + "perl-datetime-doc": { + "versions": { + "1.44-r0": 1543239027 + }, + "origin": "perl-datetime" + }, + "wayland-dev": { + "versions": { + "1.16.0-r0": 1544001083 + }, + "origin": "wayland", + "dependencies": [ + "libffi-dev", + "expat-dev", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libxml2.so.2", + "wayland-libs-client=1.16.0-r0", + "wayland-libs-cursor=1.16.0-r0", + "wayland-libs-egl=1.16.0-r0", + "wayland-libs-server=1.16.0-r0" + ], + "provides": [ + "pc:wayland-client=1.16.0", + "pc:wayland-cursor=1.16.0", + "pc:wayland-egl-backend=3", + "pc:wayland-egl=18.1.0", + "pc:wayland-scanner=1.16.0", + "pc:wayland-server=1.16.0", + "cmd:wayland-scanner" + ] + }, + "acf-pingu": { + "versions": { + "0.4.0-r2": 1545163694 + }, + "origin": "acf-pingu", + "dependencies": [ + "acf-core", + "pingu" + ] + }, + "py2-vte": { + "versions": { + "0.28.2-r13": 1543927670 + }, + "origin": "vte", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpango-1.0.so.0", + "so:libvte.so.9" + ] + }, + "uwsgi-pam": { + "versions": { + "2.0.17.1-r0": 1545062204 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0" + ] + }, + "haserl-doc": { + "versions": { + "0.9.35-r1": 1542883797 + }, + "origin": "haserl" + }, + "clutter": { + "versions": { + "1.26.2-r2": 1545293148 + }, + "origin": "clutter", + "dependencies": [ + "so:libX11.so.6", + "so:libXcomposite.so.1", + "so:libXdamage.so.1", + "so:libXi.so.6", + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo-gobject.so.2", + "so:libcairo.so.2", + "so:libcogl-pango.so.20", + "so:libcogl-path.so.20", + "so:libcogl.so.20", + "so:libfontconfig.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjson-glib-1.0.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpangoft2-1.0.so.0" + ], + "provides": [ + "so:libclutter-1.0.so.0=0.2600.2" + ] + }, + "qemu-system-x86_64": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-x86_64" + ] + }, + "py2-mock": { + "versions": { + "2.0.0-r3": 1543925735 + }, + "origin": "py-mock", + "dependencies": [ + "py2-pbr", + "py2-six", + "py2-funcsigs", + "python2" + ] + }, + "freetype": { + "versions": { + "2.9.1-r2": 1542822032 + }, + "origin": "freetype", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libpng16.so.16", + "so:libz.so.1" + ], + "provides": [ + "so:libfreetype.so.6=6.16.1" + ] + }, + "android-tools-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "xf86-video-xgixp-doc": { + "versions": { + "1.8.1-r11": 1545075375 + }, + "origin": "xf86-video-xgixp" + }, + "perl-text-soundex": { + "versions": { + "3.05-r0": 1543081699 + }, + "origin": "perl-text-soundex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py-lockfile": { + "versions": { + "0.12.2-r0": 1544799214 + }, + "origin": "py-lockfile" + }, + "shorewall-core-doc": { + "versions": { + "5.2.2-r0": 1548095414 + }, + "origin": "shorewall-core" + }, + "pacman-dev": { + "versions": { + "5.0.2-r4": 1543932339 + }, + "origin": "pacman", + "dependencies": [ + "libarchive-dev", + "curl-dev", + "openssl-dev", + "gpgme-dev", + "gettext-dev", + "pacman=5.0.2-r4", + "pkgconfig" + ], + "provides": [ + "pc:libalpm=10.0.2" + ] + }, + "py3-scandir": { + "versions": { + "1.9.0-r1": 1542824907 + }, + "origin": "py-scandir", + "dependencies": [ + "python3", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "pm-utils-dev": { + "versions": { + "1.4.1-r1": 1543935620 + }, + "origin": "pm-utils", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:pm-utils=1.4.1" + ] + }, + "syslog-ng-dev": { + "versions": { + "3.19.1-r0": 1548543150 + }, + "origin": "syslog-ng", + "dependencies": [ + "pc:glib-2.0", + "pkgconfig", + "syslog-ng=3.19.1-r0" + ], + "provides": [ + "pc:syslog-ng-native-connector=0.1.0", + "pc:syslog-ng=3.19.1" + ] + }, + "sc-doc": { + "versions": { + "7.16-r5": 1545208853 + }, + "origin": "sc" + }, + "perl-config-inifiles": { + "versions": { + "3.000001-r0": 1547708572 + }, + "origin": "perl-config-inifiles", + "dependencies": [ + "perl-list-moreutils" + ] + }, + "perl-encode-dev": { + "versions": { + "2.98-r0": 1543935163 + }, + "origin": "perl-encode", + "dependencies": [ + "perl-encode-utils" + ], + "provides": [ + "cmd:enc2xs" + ] + }, + "cdparanoia-dev": { + "versions": { + "10.2-r7": 1543254198 + }, + "origin": "cdparanoia", + "dependencies": [ + "cdparanoia-libs=10.2-r7" + ] + }, + "rdiff-backup": { + "versions": { + "1.2.8-r2": 1545213661 + }, + "origin": "rdiff-backup", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:librsync.so.2" + ], + "provides": [ + "cmd:rdiff-backup", + "cmd:rdiff-backup-statistics" + ] + }, + "lua5.1-posix": { + "versions": { + "33.4.0-r1": 1546010825 + }, + "origin": "lua-posix", + "dependencies": [ + "lua5.1-bit32<26", + "so:libc.musl-x86_64.so.1" + ] + }, + "json-c-static": { + "versions": { + "0.13.1-r0": 1543923376 + }, + "origin": "json-c" + }, + "lua5.3-sql-odbc": { + "versions": { + "2.3.5-r2": 1543924406 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "libcanberra-gtk2": { + "versions": { + "0.30-r2": 1545118058 + }, + "origin": "libcanberra", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcanberra.so.0", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ], + "provides": [ + "so:libcanberra-gtk.so.0=0.1.9" + ] + }, + "perl-test-cpan-meta-doc": { + "versions": { + "0.25-r0": 1545075946 + }, + "origin": "perl-test-cpan-meta" + }, + "aspell-de": { + "versions": { + "20030222-r1": 1545256936 + }, + "origin": "aspell-de" + }, + "perl-mail-dkim": { + "versions": { + "0.54-r0": 1545061192 + }, + "origin": "perl-mail-dkim", + "dependencies": [ + "perl", + "perl-net-dns", + "perl-net-ip", + "perl-mailtools", + "perl-crypt-openssl-rsa", + "perl-yaml-libyaml", + "perl-net-dns-resolver-mock" + ] + }, + "perl-file-which": { + "versions": { + "1.22-r0": 1545214294 + }, + "origin": "perl-file-which" + }, + "py2-virtualenv": { + "versions": { + "15.1.0-r0": 1545066960 + }, + "origin": "py-virtualenv", + "dependencies": [ + "py2-pip", + "python2" + ], + "provides": [ + "cmd:virtualenv" + ] + }, + "py-hiredis": { + "versions": { + "0.1.4-r2": 1546423252 + }, + "origin": "py-hiredis", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "librevenge": { + "versions": { + "0.0.4-r2": 1543932070 + }, + "origin": "librevenge", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:librevenge-0.0.so.0=0.0.4", + "so:librevenge-generators-0.0.so.0=0.0.4", + "so:librevenge-stream-0.0.so.0=0.0.4" + ] + }, + "lxc-bridge": { + "versions": { + "3.1.0-r1": 1549966161 + }, + "origin": "lxc", + "dependencies": [ + "dnsmasq" + ] + }, + "uwsgi-legion_cache_fetch": { + "versions": { + "2.0.17.1-r0": 1545062201 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-datetime-format-w3cdtf": { + "versions": { + "0.07-r0": 1543239034 + }, + "origin": "perl-datetime-format-w3cdtf", + "dependencies": [ + "perl", + "perl-datetime" + ] + }, + "s6-dev": { + "versions": { + "2.7.2.0-r0": 1545062679 + }, + "origin": "s6", + "dependencies": [ + "s6=2.7.2.0-r0" + ] + }, + "ruby-bundler-doc": { + "versions": { + "1.17.1-r0": 1545301042 + }, + "origin": "ruby-bundler" + }, + "setup-box-doc": { + "versions": { + "1.0.1-r0": 1544000552 + }, + "origin": "setup-box", + "dependencies": [ + "jq" + ] + }, + "perl-http-daemon": { + "versions": { + "6.01-r1": 1542821824 + }, + "origin": "perl-http-daemon", + "dependencies": [ + "perl", + "perl-http-date", + "perl-http-message" + ] + }, + "lua5.2-dbi-sqlite3": { + "versions": { + "0.6-r3": 1545214223 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "kamailio-kazoo": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libjson-c.so.4", + "so:librabbitmq.so.4", + "so:libsrdb1.so.1", + "so:libuuid.so.1" + ] + }, + "geeqie": { + "versions": { + "1.4-r0": 1545292985 + }, + "origin": "geeqie", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libexiv2.so.26", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:liblua.so.5", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libstdc++.so.6", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:geeqie" + ] + }, + "openssh-server": { + "versions": { + "7.9_p1-r5": 1556034597 + }, + "origin": "openssh", + "dependencies": [ + "openssh-keygen", + "openssh-server-common", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:sshd" + ] + }, + "kamailio": { + "versions": { + "5.2.0-r1": 1546423173 + }, + "origin": "kamailio", + "dependencies": [ + "gawk", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:libpcre.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libkamailio_ims.so.0=0.1", + "so:libprint.so.1=1.2", + "so:libsrdb1.so.1=1.0", + "so:libsrdb2.so.1=1.0", + "so:libsrutils.so.1=1.0", + "so:libtrie.so.1=1.0", + "cmd:kamailio", + "cmd:kambdb_recover", + "cmd:kamcmd", + "cmd:kamctl", + "cmd:kamdbctl" + ] + }, + "liblastfm-dev": { + "versions": { + "1.0.9-r1": 1545207823 + }, + "origin": "liblastfm", + "dependencies": [ + "qt-dev", + "libsamplerate-dev", + "fftw-dev", + "liblastfm=1.0.9-r1" + ] + }, + "linux-firmware-atusb": { + "versions": { + "20190322-r0": 1554980654 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "nagios-plugins-ssh": { + "versions": { + "2.2.1-r6": 1543933912 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "qt-mysql": { + "versions": { + "4.8.7-r11": 1545075088 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libmariadb.so.3", + "so:libstdc++.so.6" + ] + }, + "mercurial-vim": { + "versions": { + "4.9.1-r0": 1557160461 + }, + "origin": "mercurial" + }, + "openldap-overlay-accesslog": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "zstd-dev": { + "versions": { + "1.3.8-r0": 1546966444 + }, + "origin": "zstd", + "dependencies": [ + "pkgconfig", + "zstd-libs=1.3.8-r0" + ], + "provides": [ + "pc:libzstd=1.3.8" + ] + }, + "aconf-doc": { + "versions": { + "0.7.1-r0": 1553432497 + }, + "origin": "aconf" + }, + "libxtst": { + "versions": { + "1.2.3-r2": 1543241194 + }, + "origin": "libxtst", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXtst.so.6=6.1.0" + ] + }, + "nghttp2": { + "versions": { + "1.35.1-r0": 1545858450 + }, + "origin": "nghttp2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libcrypto.so.1.1", + "so:libev.so.4", + "so:libgcc_s.so.1", + "so:libnghttp2.so.14", + "so:libssl.so.1.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:h2load", + "cmd:nghttp", + "cmd:nghttpd", + "cmd:nghttpx" + ] + }, + "smem": { + "versions": { + "1.4-r1": 1545060973 + }, + "origin": "smem", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:smem" + ] + }, + "xf86-video-rendition": { + "versions": { + "4.2.7-r0": 1544000926 + }, + "origin": "xf86-video-rendition", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "atkmm-dev": { + "versions": { + "2.24.2-r1": 1544799364 + }, + "origin": "atkmm", + "dependencies": [ + "atkmm=2.24.2-r1", + "pc:atk>=1.18", + "pc:glibmm-2.4>=2.46.2", + "pkgconfig" + ], + "provides": [ + "pc:atkmm-1.6=2.24.2" + ] + }, + "perl-universal-require": { + "versions": { + "0.18-r0": 1545995649 + }, + "origin": "perl-universal-require", + "dependencies": [ + "perl" + ] + }, + "gdb": { + "versions": { + "8.2-r1": 1544798982 + }, + "origin": "gdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libncursesw.so.6", + "so:libpython3.6m.so.1.0", + "so:libreadline.so.7", + "so:libz.so.1" + ], + "provides": [ + "cmd:gcore", + "cmd:gdb", + "cmd:gdb-add-index", + "cmd:gdbserver" + ] + }, + "libmagic": { + "versions": { + "5.36-r0": 1557160812 + }, + "origin": "file", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmagic.so.1=1.0.0" + ] + }, + "dovecot-mysql": { + "versions": { + "2.3.6-r0": 1557134098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot-sql=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ], + "provides": [ + "so:libdriver_mysql.so=0" + ] + }, + "uwsgi-fastrouter": { + "versions": { + "2.0.17.1-r0": 1545062200 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "zsh-vcs": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "librsync-dev": { + "versions": { + "2.0.2-r1": 1543932722 + }, + "origin": "librsync", + "dependencies": [ + "librsync=2.0.2-r1" + ] + }, + "lcms2-utils": { + "versions": { + "2.9-r1": 1542824189 + }, + "origin": "lcms2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libtiff.so.5" + ], + "provides": [ + "cmd:jpgicc", + "cmd:linkicc", + "cmd:psicc", + "cmd:tificc", + "cmd:transicc" + ] + }, + "perl-path-class": { + "versions": { + "0.37-r0": 1543246841 + }, + "origin": "perl-path-class" + }, + "faad2-dev": { + "versions": { + "2.7-r8": 1543998810 + }, + "origin": "faad2", + "dependencies": [ + "faad2=2.7-r8" + ] + }, + "zfs-udev": { + "versions": { + "0.7.12-r1": 1552933679 + }, + "origin": "zfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "mksh": { + "versions": { + "56c-r0": 1543925655 + }, + "origin": "mksh", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mksh" + ] + }, + "py-exifread": { + "versions": { + "2.1.2-r2": 1542821588 + }, + "origin": "py-exifread", + "dependencies": [ + "python3" + ], + "provides": [ + "cmd:EXIF.py" + ] + }, + "collectd-iptables": { + "versions": { + "5.8.0-r3": 1546422677 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libip4tc.so.0", + "so:libip6tc.so.0" + ] + }, + "wireless-tools-doc": { + "versions": { + "30_pre9-r0": 1545062250 + }, + "origin": "wireless-tools" + }, + "libdv": { + "versions": { + "1.0.0-r3": 1543999341 + }, + "origin": "libdv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdv.so.4=4.0.3" + ] + }, + "asterisk-sample-config": { + "versions": { + "15.7.1-r0": 1546247585 + }, + "origin": "asterisk" + }, + "perl-net-telnet-doc": { + "versions": { + "3.04-r0": 1543999436 + }, + "origin": "perl-net-telnet" + }, + "perl-crypt-openssl-random": { + "versions": { + "0.15-r1": 1543925841 + }, + "origin": "perl-crypt-openssl-random", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "lua5.2-cjson": { + "versions": { + "2.1.0-r8": 1542820883 + }, + "origin": "lua-cjson", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "procmail": { + "versions": { + "3.22-r4": 1545299912 + }, + "origin": "procmail", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:formail", + "cmd:lockfile", + "cmd:mailstat", + "cmd:procmail" + ] + }, + "perl-file-slurp-doc": { + "versions": { + "9999.25-r0": 1546005797 + }, + "origin": "perl-file-slurp" + }, + "gconf-lang": { + "versions": { + "3.2.6-r4": 1545075211 + }, + "origin": "gconf" + }, + "libnet": { + "versions": { + "1.1.6-r2": 1545060613 + }, + "origin": "libnet", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnet.so.1=1.7.0" + ] + }, + "readline-doc": { + "versions": { + "7.0.003-r1": 1542301065 + }, + "origin": "readline" + }, + "lua5.2-hashids": { + "versions": { + "1.0.6-r1": 1545209082 + }, + "origin": "lua-hashids", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1" + ] + }, + "bitlbee-dev": { + "versions": { + "3.5.1-r4": 1543248420 + }, + "origin": "bitlbee", + "dependencies": [ + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:bitlbee=3.5.1" + ] + }, + "py3-ldb": { + "versions": { + "1.3.8-r0": 1555334661 + }, + "origin": "ldb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldb.so.1", + "so:libpython3.6m.so.1.0", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libpyldb-util.cpython-36m-x86-64-linux-gnu.so.1=1.3.8" + ] + }, + "perl-list-someutils": { + "versions": { + "0.55-r0": 1543934260 + }, + "origin": "perl-list-someutils", + "dependencies": [ + "perl-module-implementation", + "perl-list-someutils-xs" + ] + }, + "uwsgi-router_metrics": { + "versions": { + "2.0.17.1-r0": 1545062207 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "poppler-glib": { + "versions": { + "0.56.0-r1": 1542824319 + }, + "origin": "poppler", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfreetype.so.6", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpoppler.so.67", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libpoppler-glib.so.8=8.9.0" + ] + }, + "xset-doc": { + "versions": { + "1.2.4-r0": 1545076640 + }, + "origin": "xset" + }, + "py-musicbrainzngs": { + "versions": { + "0.6-r2": 1543240637 + }, + "origin": "py-musicbrainzngs" + }, + "indent": { + "versions": { + "2.2.12-r0": 1545299644 + }, + "origin": "indent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8" + ], + "provides": [ + "cmd:indent" + ] + }, + "sqsh-doc": { + "versions": { + "2.5.16.1-r2": 1545300947 + }, + "origin": "sqsh" + }, + "fprobe": { + "versions": { + "1.1-r8": 1545213731 + }, + "origin": "fprobe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:fprobe" + ] + }, + "bdftopcf-doc": { + "versions": { + "1.1-r0": 1542924734 + }, + "origin": "bdftopcf" + }, + "polkit-lang": { + "versions": { + "0.105-r9": 1547130960 + }, + "origin": "polkit" + }, + "lua5.2-openrc": { + "versions": { + "0.2-r3": 1545066957 + }, + "origin": "lua-openrc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librc.so.1" + ] + }, + "perl-text-wrapper-doc": { + "versions": { + "1.05-r1": 1543927508 + }, + "origin": "perl-text-wrapper" + }, + "gtk-engines-mist": { + "versions": { + "2.21.0-r2": 1545289337 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "lua-sqlite": { + "versions": { + "0.9.5-r2": 1545214289 + }, + "origin": "lua-sqlite" + }, + "libnfs": { + "versions": { + "3.0.0-r0": 1544799294 + }, + "origin": "libnfs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnfs.so.12=12.0.0", + "cmd:nfs-cat", + "cmd:nfs-cp", + "cmd:nfs-ls" + ] + }, + "gnokii-libs": { + "versions": { + "0.6.31-r8": 1545300479 + }, + "origin": "gnokii", + "dependencies": [ + "so:libXpm.so.4", + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libical.so.2", + "so:libintl.so.8", + "so:libusb-0.1.so.4" + ], + "provides": [ + "so:libgnokii.so.7=7.0.0" + ] + }, + "freeradius-eap": { + "versions": { + "3.0.17-r5": 1556201989 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libfreeradius-eap.so", + "so:libfreeradius-radius.so", + "so:libfreeradius-server.so", + "so:libssl.so.1.1", + "so:libtalloc.so.2" + ], + "provides": [ + "freeradius3-eap=3.0.17-r5", + "so:rlm_eap.so=0", + "so:rlm_eap_fast.so=0", + "so:rlm_eap_gtc.so=0", + "so:rlm_eap_leap.so=0", + "so:rlm_eap_md5.so=0", + "so:rlm_eap_mschapv2.so=0", + "so:rlm_eap_peap.so=0", + "so:rlm_eap_pwd.so=0", + "so:rlm_eap_sim.so=0", + "so:rlm_eap_tls.so=0", + "so:rlm_eap_ttls.so=0", + "cmd:radeapclient" + ] + }, + "glew-doc": { + "versions": { + "2.1.0-r0": 1543935773 + }, + "origin": "glew" + }, + "perl-test-cpan-meta": { + "versions": { + "0.25-r0": 1545075947 + }, + "origin": "perl-test-cpan-meta" + }, + "py3-oauthlib": { + "versions": { + "2.0.6-r1": 1543249960 + }, + "origin": "py-oauthlib", + "dependencies": [ + "py3-crypto", + "py3-jwt", + "python3" + ] + }, + "atkmm-doc": { + "versions": { + "2.24.2-r1": 1544799364 + }, + "origin": "atkmm" + }, + "iperf": { + "versions": { + "2.0.10-r1": 1543222923 + }, + "origin": "iperf", + "dependencies": [ + "!iperf3", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:iperf" + ] + }, + "joe-doc": { + "versions": { + "4.6-r0": 1543221866 + }, + "origin": "joe" + }, + "lua-rex": { + "versions": { + "2.9.0-r0": 1545209208 + }, + "origin": "lua-rex", + "dependencies": [ + "lua-rex-pcre", + "lua-rex-posix" + ] + }, + "popt": { + "versions": { + "1.16-r7": 1542820794 + }, + "origin": "popt", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpopt.so.0=0.0.0" + ] + }, + "lua-md5": { + "versions": { + "1.2-r3": 1542883871 + }, + "origin": "lua-md5" + }, + "jfsutils-doc": { + "versions": { + "1.1.15-r1": 1542883489 + }, + "origin": "jfsutils" + }, + "gtk-engines-crux": { + "versions": { + "2.21.0-r2": 1545289336 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "fontconfig": { + "versions": { + "2.13.1-r0": 1545745987 + }, + "origin": "fontconfig", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libexpat.so.1", + "so:libfreetype.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "so:libfontconfig.so.1=1.12.0", + "cmd:fc-cache", + "cmd:fc-cat", + "cmd:fc-conflist", + "cmd:fc-list", + "cmd:fc-match", + "cmd:fc-pattern", + "cmd:fc-query", + "cmd:fc-scan", + "cmd:fc-validate" + ] + }, + "gzip": { + "versions": { + "1.10-r0": 1546345202 + }, + "origin": "gzip", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:gunzip", + "cmd:gzexe", + "cmd:gzip", + "cmd:uncompress", + "cmd:zcat", + "cmd:zcmp", + "cmd:zdiff", + "cmd:zegrep", + "cmd:zfgrep", + "cmd:zforce", + "cmd:zgrep", + "cmd:zless", + "cmd:zmore", + "cmd:znew" + ] + }, + "gtk+-dev": { + "versions": { + "2.24.31-r0": 1543928744 + }, + "origin": "gtk+", + "dependencies": [ + "gtk+2.0-dev" + ] + }, + "lm_sensors": { + "versions": { + "3.4.0-r6": 1542924821 + }, + "origin": "lm_sensors", + "dependencies": [ + "bash", + "sysfsutils", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsensors.so.4=4.4.0", + "cmd:fancontrol", + "cmd:isadump", + "cmd:isaset", + "cmd:pwmconfig", + "cmd:sensors" + ] + }, + "libxmu": { + "versions": { + "1.1.2-r1": 1542883345 + }, + "origin": "libxmu", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXmu.so.6=6.2.0", + "so:libXmuu.so.1=1.0.0" + ] + }, + "openldap-back-relay": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2" + ] + }, + "libxxf86misc": { + "versions": { + "1.0.3-r2": 1543927842 + }, + "origin": "libxxf86misc", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXxf86misc.so.1=1.1.0" + ] + }, + "openldap-overlay-auditlog": { + "versions": { + "2.4.47-r2": 1546016478 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "perl-async-mergepoint": { + "versions": { + "0.04-r0": 1545075381 + }, + "origin": "perl-async-mergepoint" + }, + "qemu-system-nios2": { + "versions": { + "3.1.0-r3": 1551107304 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-nios2" + ] + }, + "perl-html-scrubber": { + "versions": { + "0.17-r0": 1545292679 + }, + "origin": "perl-html-scrubber", + "dependencies": [ + "perl-html-parser" + ] + }, + "krb5-server": { + "versions": { + "1.15.5-r0": 1548094457 + }, + "origin": "krb5", + "dependencies": [ + "libverto-libev", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libgssapi_krb5.so.2", + "so:libgssrpc.so.4", + "so:libk5crypto.so.3", + "so:libkadm5clnt_mit.so.11", + "so:libkadm5srv_mit.so.11", + "so:libkdb5.so.8", + "so:libkdb_ldap.so.1", + "so:libkrb5.so.3", + "so:libkrb5support.so.0", + "so:libss.so.2", + "so:libverto.so.1" + ], + "provides": [ + "cmd:gss-server", + "cmd:kadmin.local", + "cmd:kadmind", + "cmd:kdb5_ldap_util", + "cmd:kdb5_util", + "cmd:kprop", + "cmd:kpropd", + "cmd:kproplog", + "cmd:krb5-send-pr", + "cmd:krb5kdc", + "cmd:sclient", + "cmd:sim_server", + "cmd:sserver", + "cmd:uuserver" + ] + }, + "xf86-video-r128-doc": { + "versions": { + "6.11.0-r0": 1545060959 + }, + "origin": "xf86-video-r128" + }, + "libxv-doc": { + "versions": { + "1.0.11-r2": 1542900291 + }, + "origin": "libxv" + }, + "icu-libs": { + "versions": { + "62.1-r0": 1542823973 + }, + "origin": "icu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libicudata.so.62=62.1", + "so:libicui18n.so.62=62.1", + "so:libicuio.so.62=62.1", + "so:libicutest.so.62=62.1", + "so:libicutu.so.62=62.1", + "so:libicuuc.so.62=62.1" + ] + }, + "libechonest": { + "versions": { + "2.3.1-r0": 1545209022 + }, + "origin": "libechonest", + "dependencies": [ + "so:libQtCore.so.4", + "so:libQtNetwork.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libqjson.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libechonest.so.2.3=2.3.1" + ] + }, + "py2-itsdangerous": { + "versions": { + "0.24-r3": 1545164549 + }, + "origin": "py-itsdangerous", + "dependencies": [ + "python2" + ] + }, + "avfs": { + "versions": { + "1.0.6-r0": 1545665871 + }, + "origin": "avfs", + "dependencies": [ + "bash", + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2" + ], + "provides": [ + "so:libavfs.so.0=0.0.2", + "cmd:avfsd", + "cmd:davpass", + "cmd:ftppass", + "cmd:mountavfs", + "cmd:umountavfs" + ] + }, + "dpaste": { + "versions": { + "0.6-r0": 1545076747 + }, + "origin": "sprunge", + "dependencies": [ + "curl" + ], + "provides": [ + "cmd:dpaste" + ] + }, + "dconf": { + "versions": { + "0.26.0-r1": 1545302105 + }, + "origin": "dconf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libdconf.so.1=1.0.0", + "cmd:dconf" + ] + }, + "powertop": { + "versions": { + "2.9-r1": 1543927725 + }, + "origin": "powertop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libnl.so.1", + "so:libpci.so.3", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:powertop" + ] + }, + "ipset-doc": { + "versions": { + "7.1-r0": 1545163450 + }, + "origin": "ipset" + }, + "gcc": { + "versions": { + "8.3.0-r0": 1554745500 + }, + "origin": "gcc", + "dependencies": [ + "binutils", + "isl", + "libgomp=8.3.0-r0", + "libatomic=8.3.0-r0", + "libgomp=8.3.0-r0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgmp.so.10", + "so:libisl.so.15", + "so:libmpc.so.3", + "so:libmpfr.so.4", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libcc1.so.0=0.0.0", + "so:libitm.so.1=1.0.0", + "cmd:c89", + "cmd:c99", + "cmd:cc", + "cmd:cpp", + "cmd:gcc", + "cmd:gcc-ar", + "cmd:gcc-nm", + "cmd:gcc-ranlib", + "cmd:gcov", + "cmd:gcov-dump", + "cmd:gcov-tool", + "cmd:x86_64-alpine-linux-musl-gcc", + "cmd:x86_64-alpine-linux-musl-gcc-8.3.0", + "cmd:x86_64-alpine-linux-musl-gcc-ar", + "cmd:x86_64-alpine-linux-musl-gcc-nm", + "cmd:x86_64-alpine-linux-musl-gcc-ranlib" + ] + }, + "xrandr": { + "versions": { + "1.5.0-r1": 1545207350 + }, + "origin": "xrandr", + "dependencies": [ + "so:libX11.so.6", + "so:libXrandr.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xrandr" + ] + }, + "uwsgi-router_memcached": { + "versions": { + "2.0.17.1-r0": 1545062207 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "spamassassin-doc": { + "versions": { + "3.4.2-r0": 1545061981 + }, + "origin": "spamassassin" + }, + "mtdev-dev": { + "versions": { + "1.1.5-r2": 1545154597 + }, + "origin": "mtdev", + "dependencies": [ + "linux-headers", + "mtdev=1.1.5-r2", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libmtdev.so.1" + ], + "provides": [ + "pc:mtdev=1.1.5", + "cmd:mtdev-test" + ] + }, + "linux-firmware-isci": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "openipmi": { + "versions": { + "2.0.25-r1": 1545214271 + }, + "origin": "openipmi", + "dependencies": [ + "so:libOpenIPMI.so.0", + "so:libOpenIPMIcmdlang.so.0", + "so:libOpenIPMIglib.so.0", + "so:libOpenIPMIposix.so.0", + "so:libOpenIPMIui.so.1", + "so:libOpenIPMIutils.so.0", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libncursesw.so.6", + "so:libnetsnmp.so.35" + ], + "provides": [ + "cmd:ipmi_ui", + "cmd:ipmicmd", + "cmd:ipmish", + "cmd:openipmi_eventd", + "cmd:openipmicmd", + "cmd:openipmish", + "cmd:rmcp_ping", + "cmd:solterm" + ] + }, + "flashrom-doc": { + "versions": { + "1.0-r3": 1543921877 + }, + "origin": "flashrom" + }, + "samba-test": { + "versions": { + "4.8.11-r1": 1555334976 + }, + "origin": "samba", + "dependencies": [ + "so:libMESSAGING-SEND-samba4.so", + "so:libMESSAGING-samba4.so", + "so:libaddns-samba4.so", + "so:libasn1-samba4.so.8", + "so:libasn1util-samba4.so", + "so:libauthkrb5-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-ldap-common-samba4.so", + "so:libcli-ldap-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libcluster-samba4.so", + "so:libcmdline-credentials-samba4.so", + "so:libcom_err.so.2", + "so:libcommon-auth-samba4.so", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libdcerpc-samba-samba4.so", + "so:libdcerpc.so.0", + "so:libdnsserver-common-samba4.so", + "so:libdsdb-module-samba4.so", + "so:libevents-samba4.so", + "so:libgenrand-samba4.so", + "so:libgensec-samba4.so", + "so:libgnutls.so.30", + "so:libkrb5-samba4.so.26", + "so:libkrb5samba-samba4.so", + "so:libldb.so.1", + "so:libldbsamba-samba4.so", + "so:liblibsmb-samba4.so", + "so:libndr-krb5pac.so.0", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libnetapi.so.0", + "so:libnetif-samba4.so", + "so:libpopt.so.0", + "so:libregistry-samba4.so", + "so:libreplace-samba4.so", + "so:libsamba-credentials.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-net-samba4.so", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamdb-common-samba4.so", + "so:libsamdb.so.0", + "so:libshares-samba4.so", + "so:libsmbclient-raw-samba4.so", + "so:libsmbclient.so.0", + "so:libsmbconf.so.0", + "so:libsmbpasswdparser-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtdb-wrap-samba4.so", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libtime-basic-samba4.so", + "so:libutil-reg-samba4.so", + "so:libutil-tdb-samba4.so", + "so:libwbclient.so.0", + "so:libwinbind-client-samba4.so" + ], + "provides": [ + "so:libdlz-bind9-for-torture-samba4.so=0", + "so:libtorture-samba4.so=0", + "cmd:gentest", + "cmd:locktest", + "cmd:masktest", + "cmd:ndrdump", + "cmd:smbtorture" + ] + }, + "lua5.3-alt-getopt": { + "versions": { + "0.7-r8": 1544799010 + }, + "origin": "lua-alt-getopt" + }, + "dkimproxy": { + "versions": { + "1.4.1-r5": 1545213959 + }, + "origin": "dkimproxy", + "dependencies": [ + "perl-mail-dkim", + "perl-net-server", + "perl-error", + "/bin/sh" + ], + "provides": [ + "cmd:dkim_responder", + "cmd:dkimproxy.in", + "cmd:dkimproxy.out" + ] + }, + "linux-firmware-qed": { + "versions": { + "20190322-r0": 1554980649 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "mtdev": { + "versions": { + "1.1.5-r2": 1545154598 + }, + "origin": "mtdev", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmtdev.so.1=1.0.0" + ] + }, + "xf86-video-amdgpu": { + "versions": { + "18.0.1-r2": 1545215978 + }, + "origin": "xf86-video-amdgpu", + "dependencies": [ + "mesa-dri-ati", + "so:libc.musl-x86_64.so.1", + "so:libdrm_amdgpu.so.1", + "so:libgbm.so.1", + "so:libudev.so.1" + ] + }, + "py2-six": { + "versions": { + "1.11.0-r0": 1542823022 + }, + "origin": "py-six", + "dependencies": [ + "python2" + ] + }, + "libell-dev": { + "versions": { + "0.16-r0": 1545822164 + }, + "origin": "libell", + "dependencies": [ + "libell=0.16-r0", + "pkgconfig" + ], + "provides": [ + "pc:ell=0.16" + ] + }, + "pcre-dev": { + "versions": { + "8.42-r1": 1545067003 + }, + "origin": "pcre", + "dependencies": [ + "libpcre16=8.42-r1", + "libpcre32=8.42-r1", + "libpcrecpp=8.42-r1", + "pcre=8.42-r1", + "pkgconfig" + ], + "provides": [ + "pc:libpcre16=8.42", + "pc:libpcre32=8.42", + "pc:libpcre=8.42", + "pc:libpcrecpp=8.42", + "pc:libpcreposix=8.42", + "cmd:pcre-config" + ] + }, + "gpsd-clients": { + "versions": { + "3.18.1-r1": 1549881587 + }, + "origin": "gpsd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:cgps", + "cmd:gegps", + "cmd:gps2udp", + "cmd:gpsctl", + "cmd:gpsdecode", + "cmd:gpsmon", + "cmd:gpspipe", + "cmd:gpxlogger", + "cmd:lcdgps", + "cmd:ntpshmmon", + "cmd:ppscheck" + ] + }, + "ncurses-static": { + "versions": { + "6.1_p20190105-r0": 1546948252 + }, + "origin": "ncurses" + }, + "binutils": { + "versions": { + "2.31.1-r2": 1546441179 + }, + "origin": "binutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libbfd-2.31.1.so=0", + "so:libopcodes-2.31.1.so=0", + "cmd:addr2line", + "cmd:ar", + "cmd:as", + "cmd:c++filt", + "cmd:dwp", + "cmd:elfedit", + "cmd:gprof", + "cmd:ld", + "cmd:ld.bfd", + "cmd:nm", + "cmd:objcopy", + "cmd:objdump", + "cmd:ranlib", + "cmd:readelf", + "cmd:size", + "cmd:strings", + "cmd:strip" + ] + }, + "asterisk-dev": { + "versions": { + "15.7.1-r0": 1546247583 + }, + "origin": "asterisk", + "dependencies": [ + "asterisk" + ] + }, + "asterisk-sounds-moh": { + "versions": { + "15.7.1-r0": 1546247585 + }, + "origin": "asterisk" + }, + "iftop-doc": { + "versions": { + "0.17-r7": 1542981212 + }, + "origin": "iftop" + }, + "asterisk-fax": { + "versions": { + "15.7.1-r0": 1546247584 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libspandsp.so.2" + ] + }, + "rpcgen": { + "versions": { + "2.3.2-r1": 1543933422 + }, + "origin": "nfs-utils", + "dependencies": [ + "rpcbind", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rpcgen" + ] + }, + "perl-devel-stacktrace-ashtml-doc": { + "versions": { + "0.15-r0": 1544792341 + }, + "origin": "perl-devel-stacktrace-ashtml" + }, + "lua-expat": { + "versions": { + "1.3.0-r2": 1544000289 + }, + "origin": "lua-expat" + }, + "duply-doc": { + "versions": { + "2.1-r0": 1545300230 + }, + "origin": "duply" + }, + "openldap": { + "versions": { + "2.4.47-r2": 1546016480 + }, + "origin": "openldap", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2", + "so:libltdl.so.7", + "so:libsasl2.so.3", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:slapacl", + "cmd:slapadd", + "cmd:slapauth", + "cmd:slapcat", + "cmd:slapd", + "cmd:slapdn", + "cmd:slapindex", + "cmd:slappasswd", + "cmd:slapschema", + "cmd:slaptest" + ] + }, + "libffi-dev": { + "versions": { + "3.2.1-r6": 1545154420 + }, + "origin": "libffi", + "dependencies": [ + "linux-headers", + "libffi=3.2.1-r6", + "pkgconfig" + ], + "provides": [ + "pc:libffi=3.2.1" + ] + }, + "smokeping": { + "versions": { + "2.7.3-r3": 1545734482 + }, + "origin": "smokeping", + "dependencies": [ + "fping", + "perl", + "perl-cgi", + "perl-cgi-fast", + "perl-cgi-session", + "perl-config-grammar", + "perl-data-hexdump", + "perl-digest-hmac", + "perl-fcgi", + "perl-io-socket-ssl", + "perl-io-tty", + "perl-ldap", + "perl-libwww", + "perl-mozilla-ca", + "perl-net-dns", + "perl-net-ip", + "perl-net-openssh", + "perl-net-snmp", + "perl-net-telnet", + "perl-text-soundex", + "perl-rrd", + "perl-snmp-session", + "perl-uri", + "rrdtool", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:smokeinfo", + "cmd:smokeping", + "cmd:smokeping_cgi", + "cmd:tSmoke" + ] + }, + "memcached-dev": { + "versions": { + "1.5.12-r0": 1543923566 + }, + "origin": "memcached", + "dependencies": [ + "memcached=1.5.12-r0" + ] + }, + "pcmciautils-doc": { + "versions": { + "018-r1": 1543077269 + }, + "origin": "pcmciautils" + }, + "umix": { + "versions": { + "1.0.2-r7": 1545235248 + }, + "origin": "umix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:umix" + ] + }, + "sc": { + "versions": { + "7.16-r5": 1545208854 + }, + "origin": "sc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:psc", + "cmd:sc", + "cmd:scqref" + ] + }, + "herbstluftwm": { + "versions": { + "0.7.1-r0": 1545300037 + }, + "origin": "herbstluftwm", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXinerama.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:dmenu_run_hlwm", + "cmd:herbstclient", + "cmd:herbstluftwm" + ] + }, + "cdparanoia-libs": { + "versions": { + "10.2-r7": 1543254199 + }, + "origin": "cdparanoia", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcdda_interface.so.0=0.10.2", + "so:libcdda_paranoia.so.0=0.10.2" + ] + }, + "lua5.2-iconv": { + "versions": { + "7-r1": 1545062769 + }, + "origin": "lua-iconv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libaio": { + "versions": { + "0.3.111-r0": 1542845872 + }, + "origin": "libaio", + "provides": [ + "so:libaio.so.1=1.0.1" + ] + }, + "linux-firmware-edgeport": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "collectd-curl": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libxml2.so.2", + "so:libyajl.so.2" + ] + }, + "run-parts-doc": { + "versions": { + "4.8.6-r0": 1543924424 + }, + "origin": "run-parts" + }, + "ntfs-3g-doc": { + "versions": { + "2017.3.23-r1": 1542973168 + }, + "origin": "ntfs-3g" + }, + "fontconfig-doc": { + "versions": { + "2.13.1-r0": 1545745987 + }, + "origin": "fontconfig" + }, + "qemu-ppc64abi32": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-ppc64abi32" + ] + }, + "lua5.1-hashids": { + "versions": { + "1.0.6-r1": 1545209081 + }, + "origin": "lua-hashids", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-packaging": { + "versions": { + "17.1-r0": 1542825055 + }, + "origin": "py-packaging", + "dependencies": [ + "py2-parsing", + "py2-six", + "python2" + ] + }, + "nagios-plugins-ircd": { + "versions": { + "2.2.1-r6": 1543933906 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "perl" + ] + }, + "libmaa": { + "versions": { + "1.3.2-r1": 1545209186 + }, + "origin": "libmaa", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmaa.so.3=3.0.0" + ] + }, + "ncurses-dev": { + "versions": { + "6.1_p20190105-r0": 1546948252 + }, + "origin": "ncurses", + "dependencies": [ + "ncurses-libs=6.1_p20190105-r0", + "pkgconfig" + ], + "provides": [ + "pc:form=6.1.20190105", + "pc:formw=6.1.20190105", + "pc:menu=6.1.20190105", + "pc:menuw=6.1.20190105", + "pc:ncurses=6.1.20190105", + "pc:ncursesw=6.1.20190105", + "pc:panel=6.1.20190105", + "pc:panelw=6.1.20190105", + "cmd:ncursesw6-config" + ] + }, + "enchant-dev": { + "versions": { + "1.6.0-r13": 1542965845 + }, + "origin": "enchant", + "dependencies": [ + "enchant=1.6.0-r13", + "pc:glib-2.0", + "pc:gmodule-no-export-2.0", + "pkgconfig" + ], + "provides": [ + "pc:enchant=1.6.0" + ] + }, + "qjson": { + "versions": { + "0.9.0-r0": 1545075113 + }, + "origin": "qjson", + "dependencies": [ + "so:libQtCore.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libqjson.so.0=0.9.0" + ] + }, + "perl-params-validationcompiler": { + "versions": { + "0.27-r0": 1542973093 + }, + "origin": "perl-params-validationcompiler", + "dependencies": [ + "perl-specio", + "perl-test2-suite", + "perl-test-simple", + "perl-test-without-module", + "perl-eval-closure", + "perl-exception-class", + "perl-test2-plugin-nowarnings", + "perl-role-tiny", + "perl-mro-compat" + ] + }, + "sharutils": { + "versions": { + "4.15.2-r0": 1543929028 + }, + "origin": "sharutils", + "dependencies": [ + "bzip2", + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8" + ], + "provides": [ + "cmd:shar", + "cmd:unshar", + "cmd:uudecode", + "cmd:uuencode" + ] + }, + "nagios-plugins-mrtg": { + "versions": { + "2.2.1-r6": 1543933908 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-django-djblets": { + "versions": { + "0.6.31-r0": 1545207048 + }, + "origin": "py-django-djblets", + "dependencies": [ + "python2", + "py-django-pipeline" + ] + }, + "lua-sql-mysql": { + "versions": { + "2.3.5-r2": 1543924407 + }, + "origin": "lua-sql" + }, + "syslog-ng-doc": { + "versions": { + "3.19.1-r0": 1548543150 + }, + "origin": "syslog-ng" + }, + "nginx-mod-http-image-filter": { + "versions": { + "1.14.2-r1": 1557179820 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1", + "so:libgd.so.3" + ] + }, + "libxscrnsaver": { + "versions": { + "1.2.2-r3": 1543932086 + }, + "origin": "libxscrnsaver", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXss.so.1=1.0.0" + ] + }, + "uwsgi-dumbloop": { + "versions": { + "2.0.17.1-r0": 1545062198 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "cabextract-doc": { + "versions": { + "1.9-r0": 1545209003 + }, + "origin": "cabextract" + }, + "py2-jwt": { + "versions": { + "1.6.4-r0": 1543249953 + }, + "origin": "py-jwt", + "dependencies": [ + "python2" + ] + }, + "collectd-ceph": { + "versions": { + "5.8.0-r3": 1546422678 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libyajl.so.2" + ] + }, + "perl-http-message-doc": { + "versions": { + "6.18-r0": 1542821805 + }, + "origin": "perl-http-message" + }, + "libmowgli-dev": { + "versions": { + "2.1.3-r3": 1543998747 + }, + "origin": "libmowgli", + "dependencies": [ + "libmowgli=2.1.3-r3", + "pkgconfig" + ], + "provides": [ + "pc:libmowgli-2=2.1.3" + ] + }, + "freshclam-openrc": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav" + }, + "krb5-conf": { + "versions": { + "1.0-r1": 1542819056 + }, + "origin": "krb5-conf" + }, + "lua5.1-dev": { + "versions": { + "5.1.5-r7": 1542820875 + }, + "origin": "lua5.1", + "dependencies": [ + "lua5.1", + "lua5.1-libs=5.1.5-r7", + "pkgconfig" + ], + "provides": [ + "lua-dev", + "pc:lua5.1=5.1.5", + "pc:lua=5.1.5" + ] + }, + "ncftp-bookmarks": { + "versions": { + "3.2.6-r1": 1544000464 + }, + "origin": "ncftp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncftp.so.3", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:ncftpbookmarks" + ] + }, + "py2-uritemplate": { + "versions": { + "3.0.0-r1": 1545207295 + }, + "origin": "py-uritemplate", + "dependencies": [ + "python2" + ], + "provides": [ + "py2-uritemplate.py" + ] + }, + "dropbear-dbclient": { + "versions": { + "2018.76-r2": 1545208976 + }, + "origin": "dropbear", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:dbclient" + ] + }, + "libedit-doc": { + "versions": { + "20181209.3.1-r0": 1545839273 + }, + "origin": "libedit" + }, + "sysstat": { + "versions": { + "11.6.0-r0": 1543922334 + }, + "origin": "sysstat", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:cifsiostat", + "cmd:iostat", + "cmd:mpstat", + "cmd:pidstat", + "cmd:sadf", + "cmd:sar", + "cmd:tapestat" + ] + }, + "graphviz-gtk": { + "versions": { + "2.40.1-r1": 1543926741 + }, + "origin": "graphviz", + "dependencies": [ + "so:libatk-1.0.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libgvc.so.6", + "so:librsvg-2.so.2" + ], + "provides": [ + "so:libgvplugin_gdk.so.6=6.0.0", + "so:libgvplugin_gtk.so.6=6.0.0", + "so:libgvplugin_rsvg.so.6=6.0.0" + ] + }, + "cryptsetup1-dev": { + "versions": { + "1.7.5-r4": 1545292765 + }, + "origin": "cryptsetup1", + "dependencies": [ + "cryptsetup1-libs=1.7.5-r4", + "pkgconfig" + ], + "provides": [ + "pc:libcryptsetup=1.7.5" + ] + }, + "pango": { + "versions": { + "1.42.4-r0": 1542824069 + }, + "origin": "pango", + "dependencies": [ + "/bin/sh", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libfribidi.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libharfbuzz.so.0" + ], + "provides": [ + "so:libpango-1.0.so.0=0.4200.4", + "so:libpangocairo-1.0.so.0=0.4200.4", + "so:libpangoft2-1.0.so.0=0.4200.4", + "so:libpangoxft-1.0.so.0=0.4200.4", + "cmd:pango-list", + "cmd:pango-view" + ] + }, + "cairomm": { + "versions": { + "1.12.2-r0": 1543077183 + }, + "origin": "cairomm", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgcc_s.so.1", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcairomm-1.0.so.1=1.4.0" + ] + }, + "lua-pingu": { + "versions": { + "1.5-r2": 1544000080 + }, + "origin": "pingu", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "nagios-plugins-dig": { + "versions": { + "2.2.1-r6": 1543933903 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "bind-tools", + "so:libc.musl-x86_64.so.1" + ] + }, + "rsnapshot": { + "versions": { + "1.4.2-r0": 1544799300 + }, + "origin": "rsnapshot", + "dependencies": [ + "perl", + "rsync", + "openssh-client" + ], + "provides": [ + "cmd:rsnapshot", + "cmd:rsnapshot-diff" + ] + }, + "acf-dhcp": { + "versions": { + "0.9.1-r0": 1545229760 + }, + "origin": "acf-dhcp", + "dependencies": [ + "acf-core", + "dhcp" + ] + }, + "expect": { + "versions": { + "5.45.4-r0": 1543246822 + }, + "origin": "expect", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtcl8.6.so" + ], + "provides": [ + "so:libexpect5.45.4.so=0", + "cmd:autoexpect", + "cmd:autopasswd", + "cmd:cryptdir", + "cmd:decryptdir", + "cmd:dislocate", + "cmd:expect", + "cmd:ftp-rfc", + "cmd:kibitz", + "cmd:lpunlock", + "cmd:mkpasswd", + "cmd:multixterm", + "cmd:passmass", + "cmd:rftp", + "cmd:rlogin-cwd", + "cmd:timed-read", + "cmd:timed-run", + "cmd:tknewsbiff", + "cmd:tkpasswd", + "cmd:unbuffer", + "cmd:weather", + "cmd:xkibitz", + "cmd:xpstat" + ] + }, + "perl-net-cidr-lite": { + "versions": { + "0.21-r4": 1543924461 + }, + "origin": "perl-net-cidr-lite", + "dependencies": [ + "perl" + ] + }, + "aumix": { + "versions": { + "2.9.1-r6": 1545069455 + }, + "origin": "aumix", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:aumix", + "cmd:mute", + "cmd:xaumix" + ] + }, + "libgcrypt-doc": { + "versions": { + "1.8.4-r0": 1545838380 + }, + "origin": "libgcrypt" + }, + "perl-encode-hanextra": { + "versions": { + "0.23-r2": 1545075149 + }, + "origin": "perl-encode-hanextra", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "pixman": { + "versions": { + "0.34.0-r6": 1542822838 + }, + "origin": "pixman", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpixman-1.so.0=0.34.0" + ] + }, + "cyrus-sasl-ntlm": { + "versions": { + "2.1.27-r1": 1550353528 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "py2-cffi": { + "versions": { + "1.11.5-r0": 1543924872 + }, + "origin": "py-cffi", + "dependencies": [ + "py2-cparser", + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libpython2.7.so.1.0" + ] + }, + "farstream0.1": { + "versions": { + "0.1.2-r2": 1545069353 + }, + "origin": "farstream0.1", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstnetbuffer-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgstrtp-0.10.so.0", + "so:libnice.so.10" + ], + "provides": [ + "so:libfarstream-0.1.so.0=0.0.1" + ] + }, + "gtkman": { + "versions": { + "1.0.1-r0": 1545075133 + }, + "origin": "gtkman", + "dependencies": [ + "py-gtk" + ], + "provides": [ + "cmd:gtkman" + ] + }, + "cairo-gobject": { + "versions": { + "1.16.0-r1": 1546948315 + }, + "origin": "cairo", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libcairo-gobject.so.2=2.11600.0" + ] + }, + "syslog-ng-add-contextual-data": { + "versions": { + "3.19.1-r0": 1548543150 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "gdk-pixbuf-doc": { + "versions": { + "2.36.11-r2": 1543253989 + }, + "origin": "gdk-pixbuf" + }, + "uwsgi-transformation_template": { + "versions": { + "2.0.17.1-r0": 1545062213 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "bacula-mysql": { + "versions": { + "9.4.1-r1": 1546944087 + }, + "origin": "bacula", + "dependencies": [ + "bacula", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ], + "provides": [ + "so:libbaccats-mysql-9.4.1.so=0" + ] + }, + "xf86-video-xgixp": { + "versions": { + "1.8.1-r11": 1545075376 + }, + "origin": "xf86-video-xgixp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "graphviz-doc": { + "versions": { + "2.40.1-r1": 1543926740 + }, + "origin": "graphviz" + }, + "py-docutils": { + "versions": { + "0.14-r1": 1543925153 + }, + "origin": "py-docutils", + "dependencies": [ + "py3-docutils", + "py3-docutils=0.14-r1" + ] + }, + "vblade-doc": { + "versions": { + "24-r0": 1543222854 + }, + "origin": "vblade" + }, + "redis-openrc": { + "versions": { + "4.0.12-r0": 1546005788 + }, + "origin": "redis" + }, + "acf-clamav": { + "versions": { + "0.8.0-r2": 1545209739 + }, + "origin": "acf-clamav", + "dependencies": [ + "acf-core", + "clamav" + ] + }, + "nagios-plugins-dummy": { + "versions": { + "2.2.1-r6": 1543933904 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "galculator": { + "versions": { + "2.1.4-r0": 1545076849 + }, + "origin": "galculator", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0" + ], + "provides": [ + "cmd:galculator" + ] + }, + "py-libvirt": { + "versions": { + "4.10.0-r0": 1545291084 + }, + "origin": "py-libvirt" + }, + "rhash-dev": { + "versions": { + "1.3.6-r2": 1542820449 + }, + "origin": "rhash", + "dependencies": [ + "rhash-libs=1.3.6-r2" + ] + }, + "openssh-server-pam": { + "versions": { + "7.9_p1-r5": 1556034599 + }, + "origin": "openssh", + "dependencies": [ + "openssh-keygen", + "openssh-server-common", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpam.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:sshd" + ] + }, + "git-gitk": { + "versions": { + "2.20.1-r0": 1545214200 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0", + "tcl", + "tk" + ], + "provides": [ + "cmd:gitk" + ] + }, + "xdriinfo": { + "versions": { + "1.0.5-r1": 1543927864 + }, + "origin": "xdriinfo", + "dependencies": [ + "so:libGL.so.1", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xdriinfo" + ] + }, + "nikto": { + "versions": { + "2.1.6-r0": 1543924830 + }, + "origin": "nikto", + "dependencies": [ + "perl", + "nmap", + "libressl" + ], + "provides": [ + "cmd:nikto.pl" + ] + }, + "pgpool-dev": { + "versions": { + "3.7.5-r0": 1545073842 + }, + "origin": "pgpool", + "dependencies": [ + "pgpool=3.7.5-r0" + ] + }, + "libxxf86dga-dev": { + "versions": { + "1.1.4-r2": 1542845774 + }, + "origin": "libxxf86dga", + "dependencies": [ + "libxxf86dga=1.1.4-r2", + "pc:x11", + "pc:xext", + "pc:xf86dgaproto", + "pkgconfig" + ], + "provides": [ + "pc:xxf86dga=1.1.4" + ] + }, + "weechat-python": { + "versions": { + "2.3-r0": 1545299933 + }, + "origin": "weechat", + "dependencies": [ + "weechat", + "so:libc.musl-x86_64.so.1" + ] + }, + "xz-libs": { + "versions": { + "5.2.4-r0": 1542303285 + }, + "origin": "xz", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblzma.so.5=5.2.4" + ] + }, + "gummiboot": { + "versions": { + "48.1-r0": 1545073320 + }, + "origin": "gummiboot", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:gummiboot" + ] + }, + "py3-talloc": { + "versions": { + "2.1.14-r0": 1543220637 + }, + "origin": "talloc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libpytalloc-util.cpython-36m-x86-64-linux-gnu.so.2=2.1.14" + ] + }, + "ortp-dev": { + "versions": { + "0.25.0-r0": 1543077281 + }, + "origin": "ortp", + "dependencies": [ + "ortp=0.25.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:ortp=0.25.0" + ] + }, + "uwsgi-forkptyrouter": { + "versions": { + "2.0.17.1-r0": 1545062200 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "lighttpd-mod_auth": { + "versions": { + "1.4.52-r0": 1545207630 + }, + "origin": "lighttpd", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libice": { + "versions": { + "1.0.9-r3": 1542824333 + }, + "origin": "libice", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libICE.so.6=6.3.0" + ] + }, + "perl-test2-plugin-nowarnings-doc": { + "versions": { + "0.06-r0": 1542973052 + }, + "origin": "perl-test2-plugin-nowarnings" + }, + "pixman-dev": { + "versions": { + "0.34.0-r6": 1542822838 + }, + "origin": "pixman", + "dependencies": [ + "pixman=0.34.0-r6", + "pkgconfig" + ], + "provides": [ + "pc:pixman-1=0.34.0" + ] + }, + "libiec61883": { + "versions": { + "1.2.0-r2": 1543999349 + }, + "origin": "libiec61883", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libraw1394.so.11" + ], + "provides": [ + "so:libiec61883.so.0=0.1.1" + ] + }, + "ip6tables": { + "versions": { + "1.6.2-r1": 1545062496 + }, + "origin": "iptables", + "dependencies": [ + "iptables", + "iptables=1.6.2-r1", + "so:libc.musl-x86_64.so.1", + "so:libxtables.so.12" + ] + }, + "tinyxml2-dev": { + "versions": { + "7.0.1-r0": 1545061051 + }, + "origin": "tinyxml2", + "dependencies": [ + "pkgconfig", + "tinyxml2=7.0.1-r0" + ], + "provides": [ + "pc:tinyxml2=7.0.1" + ] + }, + "py2-paramiko": { + "versions": { + "2.4.2-r0": 1545216487 + }, + "origin": "py-paramiko", + "dependencies": [ + "py2-asn1", + "py2-cryptography", + "py2-bcrypt", + "py2-pynacl", + "python2" + ] + }, + "neon-doc": { + "versions": { + "0.30.2-r5": 1542883669 + }, + "origin": "neon" + }, + "proxychains-ng": { + "versions": { + "4.13-r0": 1545116587 + }, + "origin": "proxychains-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libproxychains4.so=0", + "cmd:proxychains", + "cmd:proxychains4" + ] + }, + "asterisk-tds": { + "versions": { + "15.7.1-r0": 1546247584 + }, + "origin": "asterisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsybdb.so.5" + ] + }, + "libcrystalhd": { + "versions": { + "20130708-r2": 1543924960 + }, + "origin": "libcrystalhd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcrystalhd.so.3=3.6" + ] + }, + "libisofs-dev": { + "versions": { + "1.4.8-r0": 1544797287 + }, + "origin": "libisofs", + "dependencies": [ + "libisofs=1.4.8-r0", + "pkgconfig" + ], + "provides": [ + "pc:libisofs-1=1.4.8" + ] + }, + "augeas-libs": { + "versions": { + "1.11.0-r0": 1542924581 + }, + "origin": "augeas", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "so:libaugeas.so.0=0.24.1", + "so:libfa.so.1=1.5.2" + ] + }, + "libsigc++-doc": { + "versions": { + "2.10.0-r1": 1543077170 + }, + "origin": "libsigc++" + }, + "gtk2-xfce-engine": { + "versions": { + "3.2.0-r2": 1545292906 + }, + "origin": "gtk-xfce-engine", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "perl-encode-locale-doc": { + "versions": { + "1.05-r1": 1542821769 + }, + "origin": "perl-encode-locale" + }, + "qt-config": { + "versions": { + "4.8.7-r11": 1545075087 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtGui.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:qtconfig" + ] + }, + "lua5.2-alt-getopt": { + "versions": { + "0.7-r8": 1544799006 + }, + "origin": "lua-alt-getopt" + }, + "seabios-bin": { + "versions": { + "1.11.0-r0": 1543936026 + }, + "origin": "seabios" + }, + "perl-javascript-minifier-xs-doc": { + "versions": { + "0.11-r3": 1543998975 + }, + "origin": "perl-javascript-minifier-xs" + }, + "haproxy-doc": { + "versions": { + "1.8.12-r1": 1545208999 + }, + "origin": "haproxy" + }, + "libsexy-dev": { + "versions": { + "0.1.11-r8": 1544001178 + }, + "origin": "libsexy", + "dependencies": [ + "libsexy=0.1.11-r8", + "pkgconfig" + ], + "provides": [ + "pc:libsexy=0.1.11" + ] + }, + "lua5.2-discount": { + "versions": { + "1.2.10.1-r4": 1545209121 + }, + "origin": "lua-discount", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "sysfsutils-dev": { + "versions": { + "2.1.0-r8": 1542924684 + }, + "origin": "sysfsutils", + "dependencies": [ + "sysfsutils=2.1.0-r8" + ] + }, + "perl-image-exiftool": { + "versions": { + "11.23-r0": 1545835015 + }, + "origin": "perl-image-exiftool", + "dependencies": [ + "perl" + ] + }, + "py3-rsa": { + "versions": { + "3.4.2-r2": 1545116593 + }, + "origin": "py-rsa", + "dependencies": [ + "py3-asn1", + "python3" + ], + "provides": [ + "cmd:pyrsa-decrypt", + "cmd:pyrsa-decrypt-bigfile", + "cmd:pyrsa-encrypt", + "cmd:pyrsa-encrypt-bigfile", + "cmd:pyrsa-keygen", + "cmd:pyrsa-priv2pub", + "cmd:pyrsa-sign", + "cmd:pyrsa-verify" + ] + }, + "libacl": { + "versions": { + "2.2.52-r5": 1542304043 + }, + "origin": "acl", + "dependencies": [ + "so:libattr.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libacl.so.1=1.1.0" + ] + }, + "libxshmfence-dev": { + "versions": { + "1.3-r0": 1542900235 + }, + "origin": "libxshmfence", + "dependencies": [ + "linux-headers", + "libxshmfence=1.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:xshmfence=1.3" + ] + }, + "lua5.2-doc": { + "versions": { + "5.2.4-r7": 1542304842 + }, + "origin": "lua5.2" + }, + "perl-test-warn-doc": { + "versions": { + "0.36-r0": 1543242188 + }, + "origin": "perl-test-warn" + }, + "squid-lang-ca": { + "versions": { + "4.4-r1": 1545216323 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libvirt-qemu": { + "versions": { + "4.10.0-r1": 1547051658 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=4.10.0-r1", + "libvirt-common-drivers=4.10.0-r1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgnutls.so.30", + "so:libintl.so.8", + "so:libvirt.so.0", + "so:libxml2.so.2" + ] + }, + "ppp-atm": { + "versions": { + "2.4.7-r6": 1543999021 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "haproxy": { + "versions": { + "1.8.12-r1": 1545208999 + }, + "origin": "haproxy", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:liblua-5.3.so.0", + "so:libpcre.so.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:haproxy" + ] + }, + "libetpan-doc": { + "versions": { + "1.9.2-r0": 1548063823 + }, + "origin": "libetpan" + }, + "libsndfile-doc": { + "versions": { + "1.0.28-r8": 1555066604 + }, + "origin": "libsndfile" + }, + "darkice-doc": { + "versions": { + "1.3-r0": 1545302358 + }, + "origin": "darkice" + }, + "perl-class-data-inheritable-doc": { + "versions": { + "0.08-r0": 1542845801 + }, + "origin": "perl-class-data-inheritable" + }, + "py2-flask-oauthlib": { + "versions": { + "0.9.5-r0": 1545164577 + }, + "origin": "py-flask-oauthlib", + "dependencies": [ + "py2-flask", + "py2-requests-oauthlib", + "python2" + ] + }, + "py2-gflags": { + "versions": { + "3.1.1-r1": 1545208598 + }, + "origin": "py-gflags", + "dependencies": [ + "python2" + ] + }, + "perl-crypt-openssl-guess-doc": { + "versions": { + "0.11-r0": 1543925837 + }, + "origin": "perl-crypt-openssl-guess" + }, + "net-snmp-libs": { + "versions": { + "5.8-r0": 1543923350 + }, + "origin": "net-snmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ], + "provides": [ + "so:libnetsnmp.so.35=35.0.0" + ] + }, + "tk": { + "versions": { + "8.6.9-r0": 1545915089 + }, + "origin": "tk", + "dependencies": [ + "so:libX11.so.6", + "so:libXft.so.2", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libtcl8.6.so" + ], + "provides": [ + "so:libtk8.6.so=0", + "cmd:wish", + "cmd:wish8.6" + ] + }, + "py-certifi": { + "versions": { + "2018.4.16-r0": 1542825015 + }, + "origin": "py-certifi" + }, + "dosfstools-doc": { + "versions": { + "4.1-r1": 1545163464 + }, + "origin": "dosfstools" + }, + "graphite2": { + "versions": { + "1.3.12-r1": 1542823993 + }, + "origin": "graphite2", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgraphite2.so.3=3.2.1" + ] + }, + "nfdump-dbg": { + "versions": { + "1.6.17-r0": 1545223023 + }, + "origin": "nfdump" + }, + "libpri-dev": { + "versions": { + "1.6.0-r0": 1543932622 + }, + "origin": "libpri", + "dependencies": [ + "libpri=1.6.0-r0" + ] + }, + "libssh2-doc": { + "versions": { + "1.8.2-r0": 1553681860 + }, + "origin": "libssh2" + }, + "llvm5-test-utils": { + "versions": { + "5.0.2-r0": 1546874981 + }, + "origin": "llvm5", + "dependencies": [ + "python2", + "py-setuptools", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "llvm-test-utils=5.0.2-r0", + "lit=0.6.0-r0", + "cmd:lit" + ] + }, + "tig-doc": { + "versions": { + "2.4.1-r0": 1545216500 + }, + "origin": "tig" + }, + "iwlwifi-5150-ucode-doc": { + "versions": { + "8.24.2.2-r1": 1545208930 + }, + "origin": "iwlwifi-5150-ucode" + }, + "automake-doc": { + "versions": { + "1.16.1-r0": 1542301304 + }, + "origin": "automake" + }, + "libwebsockets-dev": { + "versions": { + "3.1.0-r0": 1545856933 + }, + "origin": "libwebsockets", + "dependencies": [ + "libwebsockets=3.1.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libwebsockets=3.1.0", + "pc:libwebsockets_static=3.1.0" + ] + }, + "py3-munkres": { + "versions": { + "1.0.12-r0": 1545292846 + }, + "origin": "py-munkres", + "dependencies": [ + "python3" + ] + }, + "mosquitto-doc": { + "versions": { + "1.5.6-r0": 1549970968 + }, + "origin": "mosquitto" + }, + "perl-dbd-odbc": { + "versions": { + "1.60-r0": 1545061020 + }, + "origin": "perl-dbd-odbc", + "dependencies": [ + "perl", + "perl-dbi", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "perl-net-cidr-doc": { + "versions": { + "0.19-r0": 1544000981 + }, + "origin": "perl-net-cidr" + }, + "openvpn-auth-ldap": { + "versions": { + "2.0.3-r6": 1545069395 + }, + "origin": "openvpn-auth-ldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libobjc.so.4" + ], + "provides": [ + "so:openvpn-auth-ldap.so=0" + ] + }, + "libsmartcols": { + "versions": { + "2.33-r0": 1545307436 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsmartcols.so.1=1.1.0" + ] + }, + "sgdisk": { + "versions": { + "1.0.4-r0": 1543926506 + }, + "origin": "gptfdisk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpopt.so.0", + "so:libstdc++.so.6", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:sgdisk" + ] + }, + "perl-net-ssleay": { + "versions": { + "1.85-r4": 1545163948 + }, + "origin": "perl-net-ssleay", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "libxp-dev": { + "versions": { + "1.0.3-r2": 1545224134 + }, + "origin": "libxp", + "dependencies": [ + "libxp=1.0.3-r2", + "pc:printproto", + "pc:x11", + "pc:xau", + "pc:xext", + "pkgconfig" + ], + "provides": [ + "pc:xp=1.0.3" + ] + }, + "dmenu-doc": { + "versions": { + "4.8-r0": 1543222729 + }, + "origin": "dmenu" + }, + "checkbashisms": { + "versions": { + "2.18.11-r0": 1545292631 + }, + "origin": "checkbashisms", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:checkbashisms" + ] + }, + "perl-data-optlist-doc": { + "versions": { + "0.110-r0": 1542883239 + }, + "origin": "perl-data-optlist" + }, + "sshpass": { + "versions": { + "1.06-r0": 1542985470 + }, + "origin": "sshpass", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:sshpass" + ] + }, + "luajit-doc": { + "versions": { + "2.1.0_beta3-r4": 1542820928 + }, + "origin": "luajit" + }, + "squid-lang-oc": { + "versions": { + "4.4-r1": 1545216328 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "dpkg-doc": { + "versions": { + "1.19.2-r0": 1545820223 + }, + "origin": "dpkg" + }, + "py2-ipaddress": { + "versions": { + "1.0.22-r0": 1543926514 + }, + "origin": "py-ipaddress", + "dependencies": [ + "python2" + ] + }, + "perl-timedate-doc": { + "versions": { + "2.30-r1": 1543226625 + }, + "origin": "perl-timedate" + }, + "lz4-doc": { + "versions": { + "1.8.3-r2": 1545154434 + }, + "origin": "lz4" + }, + "gmp": { + "versions": { + "6.1.2-r1": 1542301442 + }, + "origin": "gmp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgmp.so.10=10.3.2" + ] + }, + "py3-yaml": { + "versions": { + "4.1-r0": 1544798589 + }, + "origin": "py-yaml" + }, + "uwsgi-python3": { + "versions": { + "2.0.17.1-r0": 1545062215 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "udev-init-scripts-openrc": { + "versions": { + "32-r2": 1542845335 + }, + "origin": "udev-init-scripts" + }, + "perl-http-date": { + "versions": { + "6.02-r1": 1542821759 + }, + "origin": "perl-http-date", + "dependencies": [ + "perl" + ] + }, + "rtpproxy": { + "versions": { + "2.0.0-r4": 1545301085 + }, + "origin": "rtpproxy", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rtpproxy" + ] + }, + "sipcalc-doc": { + "versions": { + "1.1.6-r0": 1545164533 + }, + "origin": "sipcalc" + }, + "py2-mako": { + "versions": { + "1.0.7-r0": 1543220552 + }, + "origin": "py-mako", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:mako-render" + ] + }, + "py3-flask": { + "versions": { + "1.0.2-r1": 1546978285 + }, + "origin": "py-flask", + "dependencies": [ + "py3-click", + "py3-itsdangerous", + "py3-jinja2", + "py3-werkzeug", + "python3" + ], + "provides": [ + "cmd:flask" + ] + }, + "py-curl": { + "versions": { + "7.43.0.2-r1": 1543935133 + }, + "origin": "py-curl" + }, + "fping-doc": { + "versions": { + "4.1-r0": 1545746006 + }, + "origin": "fping" + }, + "postgrey": { + "versions": { + "1.37-r1": 1545214334 + }, + "origin": "postgrey", + "dependencies": [ + "perl", + "perl-db", + "perl-net-dns", + "perl-net-server", + "perl-io-multiplex", + "perl-net-rblclient", + "perl-parse-syslog", + "perl-netaddr-ip", + "/bin/sh" + ], + "provides": [ + "cmd:postgrey", + "cmd:postgreyreport" + ] + }, + "ttf-freefont": { + "versions": { + "20120503-r1": 1544000554 + }, + "origin": "ttf-freefont", + "dependencies": [ + "fontconfig", + "encodings", + "mkfontdir", + "mkfontscale" + ] + }, + "libebml": { + "versions": { + "1.3.6-r1": 1543226528 + }, + "origin": "libebml", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libebml.so.4=4.0.0" + ] + }, + "rsyslog-mmnormalize": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:liblognorm.so.5" + ] + }, + "py3-imagesize": { + "versions": { + "1.1.0-r0": 1542824997 + }, + "origin": "py-imagesize", + "dependencies": [ + "python3" + ] + }, + "lua5.1-sqlite": { + "versions": { + "0.9.5-r2": 1545214288 + }, + "origin": "lua-sqlite", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "iptraf-ng": { + "versions": { + "1.1.4-r4": 1545214013 + }, + "origin": "iptraf-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libpanelw.so.6" + ], + "provides": [ + "cmd:iptraf-ng", + "cmd:rvnamed-ng" + ] + }, + "perl-net-dns-doc": { + "versions": { + "1.19-r0": 1545061164 + }, + "origin": "perl-net-dns" + }, + "py2-oauthlib": { + "versions": { + "2.0.6-r1": 1543249957 + }, + "origin": "py-oauthlib", + "dependencies": [ + "py2-crypto", + "py2-jwt", + "python2" + ] + }, + "perl-dev": { + "versions": { + "5.26.3-r0": 1543998673 + }, + "origin": "perl", + "dependencies": [ + "perl-utils" + ], + "provides": [ + "cmd:enc2xs", + "cmd:h2xs", + "cmd:perlivp", + "cmd:xsubpp" + ] + }, + "perl-convert-color": { + "versions": { + "0.11-r0": 1545062511 + }, + "origin": "perl-convert-color", + "dependencies": [ + "perl-list-utilsby", + "perl-module-pluggable" + ] + }, + "subversion-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308087 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "cadaver-doc": { + "versions": { + "0.23.3-r4": 1545076658 + }, + "origin": "cadaver" + }, + "opennhrp-doc": { + "versions": { + "0.14.1-r6": 1545069405 + }, + "origin": "opennhrp" + }, + "gparted-lang": { + "versions": { + "0.33.0-r0": 1545746178 + }, + "origin": "gparted", + "dependencies": [ + "e2fsprogs", + "ntfs-3g-progs" + ] + }, + "nginx-mod-http-upstream-fair": { + "versions": { + "1.14.2-r1": 1557179822 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "bwm-ng-doc": { + "versions": { + "0.6.1-r4": 1545073954 + }, + "origin": "bwm-ng" + }, + "mupdf-tools": { + "versions": { + "1.13.0-r2": 1544000576 + }, + "origin": "mupdf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmupdf.so.0", + "so:libmupdfthird.so.0" + ], + "provides": [ + "cmd:mjsgen", + "cmd:mujstest", + "cmd:muraster", + "cmd:mutool" + ] + }, + "radvd-openrc": { + "versions": { + "2.17-r2": 1545254220 + }, + "origin": "radvd" + }, + "libiptcdata-dev": { + "versions": { + "1.0.4-r2": 1543932145 + }, + "origin": "libiptcdata", + "dependencies": [ + "libiptcdata=1.0.4-r2", + "pkgconfig" + ], + "provides": [ + "pc:libiptcdata=1.0.4" + ] + }, + "xz-dev": { + "versions": { + "5.2.4-r0": 1542303283 + }, + "origin": "xz", + "dependencies": [ + "pkgconfig", + "xz-libs=5.2.4-r0" + ], + "provides": [ + "pc:liblzma=5.2.4" + ] + }, + "lua5.1-sql-sqlite3": { + "versions": { + "2.3.5-r2": 1543924398 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "cups-filters-dev": { + "versions": { + "1.21.6-r0": 1545820385 + }, + "origin": "cups-filters", + "dependencies": [ + "cups-filters-libs=1.21.6-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcupsfilters=1.21.6", + "pc:libfontembed=1.21.6" + ] + }, + "xvinfo": { + "versions": { + "1.1.3-r0": 1545301889 + }, + "origin": "xvinfo", + "dependencies": [ + "so:libX11.so.6", + "so:libXv.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xvinfo" + ] + }, + "talloc-dev": { + "versions": { + "2.1.14-r0": 1543220634 + }, + "origin": "talloc", + "dependencies": [ + "pkgconfig", + "py2-talloc=2.1.14-r0", + "py3-talloc=2.1.14-r0", + "talloc=2.1.14-r0" + ], + "provides": [ + "pc:pytalloc-util.cpython-36m-x86_64-linux-gnu=2.1.14", + "pc:pytalloc-util=2.1.14", + "pc:talloc=2.1.14" + ] + }, + "xf86-video-savage": { + "versions": { + "2.3.9-r3": 1545163476 + }, + "origin": "xf86-video-savage", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "openssh-client": { + "versions": { + "7.9_p1-r5": 1556034591 + }, + "origin": "openssh", + "dependencies": [ + "openssh-keygen", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libedit.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:findssl.sh", + "cmd:scp", + "cmd:sftp", + "cmd:ssh", + "cmd:ssh-add", + "cmd:ssh-agent", + "cmd:ssh-copy-id", + "cmd:ssh-keyscan", + "cmd:ssh-pkcs11-helper" + ] + }, + "openbox-dev": { + "versions": { + "3.6.1-r2": 1545207327 + }, + "origin": "openbox", + "dependencies": [ + "libxcursor-dev", + "libxrandr-dev", + "libxinerama-dev", + "startup-notification-dev", + "openbox-libs=3.6.1-r2", + "pc:glib-2.0", + "pc:imlib2", + "pc:librsvg-2.0", + "pc:libxml-2.0", + "pc:pangoxft", + "pc:xft", + "pkgconfig" + ], + "provides": [ + "pc:obrender-3.5=3.6", + "pc:obt-3.5=3.6" + ] + }, + "fail2ban-openrc": { + "versions": { + "0.10.3.1-r2": 1545300388 + }, + "origin": "fail2ban", + "dependencies": [ + "python3", + "iptables", + "ip6tables", + "logrotate" + ] + }, + "libtirpc-dev": { + "versions": { + "1.0.3-r0": 1543223615 + }, + "origin": "libtirpc", + "dependencies": [ + "krb5-dev", + "bsd-compat-headers", + "libtirpc=1.0.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:libtirpc=1.0.3" + ] + }, + "lua5.3-mosquitto": { + "versions": { + "0.2-r1": 1543932701 + }, + "origin": "lua-mosquitto", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libmosquitto.so.1" + ] + }, + "uwsgi-rawrouter": { + "versions": { + "2.0.17.1-r0": 1545062205 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "dropbear-scp": { + "versions": { + "2018.76-r2": 1545208976 + }, + "origin": "dropbear", + "dependencies": [ + "!openssh-client", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:scp" + ] + }, + "mwm": { + "versions": { + "2.3.4-r2": 1545223192 + }, + "origin": "motif", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXm.so.4", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mwm", + "cmd:xmbind" + ] + }, + "pam-pgsql": { + "versions": { + "0.7.3.2-r0": 1545068588 + }, + "origin": "pam-pgsql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20", + "so:libpam.so.0", + "so:libpq.so.5" + ] + }, + "nss-tools": { + "versions": { + "3.41-r0": 1545307911 + }, + "origin": "nss", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnspr4.so", + "so:libnss3.so", + "so:libnssutil3.so", + "so:libplc4.so", + "so:libplds4.so", + "so:libsmime3.so", + "so:libssl3.so", + "so:libz.so.1" + ], + "provides": [ + "cmd:certutil", + "cmd:cmsutil", + "cmd:crlutil", + "cmd:modutil", + "cmd:pk12util", + "cmd:shlibsign", + "cmd:signtool", + "cmd:signver", + "cmd:ssltap" + ] + }, + "py2-attrs": { + "versions": { + "18.2.0-r0": 1542824881 + }, + "origin": "py-attrs", + "dependencies": [ + "python2" + ], + "provides": [ + "py-attrs-tools" + ] + }, + "linux-firmware-netronome": { + "versions": { + "20190322-r0": 1554980650 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "crconf": { + "versions": { + "0_pre2-r0": 1545069110 + }, + "origin": "crconf", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:crconf" + ] + }, + "linux-firmware-ositech": { + "versions": { + "20190322-r0": 1554980649 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "perl-io-stringy": { + "versions": { + "2.111-r1": 1542924627 + }, + "origin": "perl-io-stringy", + "dependencies": [ + "perl" + ] + }, + "flex-dev": { + "versions": { + "2.6.4-r1": 1542300945 + }, + "origin": "flex", + "dependencies": [ + "flex", + "flex-libs=2.6.4-r1" + ] + }, + "qemu-system-sh4eb": { + "versions": { + "3.1.0-r3": 1551107305 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-sh4eb" + ] + }, + "gtk-doc": { + "versions": { + "1.29-r0": 1542823716 + }, + "origin": "gtk-doc", + "dependencies": [ + "docbook-xsl", + "python3", + "py3-six", + "pkgconf", + "glib-dev", + "highlight" + ], + "provides": [ + "cmd:gtkdoc-check", + "cmd:gtkdoc-depscan", + "cmd:gtkdoc-fixxref", + "cmd:gtkdoc-mkdb", + "cmd:gtkdoc-mkhtml", + "cmd:gtkdoc-mkhtml2", + "cmd:gtkdoc-mkman", + "cmd:gtkdoc-mkpdf", + "cmd:gtkdoc-rebase", + "cmd:gtkdoc-scan", + "cmd:gtkdoc-scangobj", + "cmd:gtkdocize" + ] + }, + "xmlindent": { + "versions": { + "0.2.17-r0": 1543223068 + }, + "origin": "xmlindent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfl.so.2" + ], + "provides": [ + "cmd:xmlindent" + ] + }, + "mp3splt-gtk": { + "versions": { + "0.9.2-r2": 1545069037 + }, + "origin": "mp3splt-gtk", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstreamer-1.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libmp3splt.so.0" + ], + "provides": [ + "cmd:mp3splt-gtk" + ] + }, + "uwsgi-router_redirect": { + "versions": { + "2.0.17.1-r0": 1545062208 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "mkfontscale": { + "versions": { + "1.1.3-r1": 1542924691 + }, + "origin": "mkfontscale", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libfontenc.so.1", + "so:libfreetype.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:mkfontscale" + ] + }, + "stfl-dev": { + "versions": { + "0.24-r2": 1545075301 + }, + "origin": "stfl", + "dependencies": [ + "pkgconfig", + "stfl=0.24-r2" + ], + "provides": [ + "pc:stfl=0.24" + ] + }, + "gnome-mime-data-lang": { + "versions": { + "2.18.0-r1": 1543934221 + }, + "origin": "gnome-mime-data" + }, + "dbus-libs": { + "versions": { + "1.10.24-r1": 1542824378 + }, + "origin": "dbus", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdbus-1.so.3=3.14.14" + ] + }, + "flac-doc": { + "versions": { + "1.3.2-r2": 1545068441 + }, + "origin": "flac" + }, + "libvirt-uml": { + "versions": { + "4.10.0-r1": 1547051658 + }, + "origin": "libvirt", + "dependencies": [ + "libvirt-daemon=4.10.0-r1", + "libvirt-common-drivers=4.10.0-r1", + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libvirt.so.0" + ] + }, + "qemu-system-cris": { + "versions": { + "3.1.0-r3": 1551107303 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-cris" + ] + }, + "alsa-utils-doc": { + "versions": { + "1.1.8-r0": 1547112821 + }, + "origin": "alsa-utils" + }, + "perl-sys-mmap-doc": { + "versions": { + "0.19-r1": 1545075421 + }, + "origin": "perl-sys-mmap" + }, + "termrec-dev": { + "versions": { + "0.17-r1": 1545249955 + }, + "origin": "termrec", + "dependencies": [ + "termrec=0.17-r1" + ] + }, + "ldapvi-doc": { + "versions": { + "1.7-r10": 1545117995 + }, + "origin": "ldapvi" + }, + "perl-mime-types-doc": { + "versions": { + "2.17-r0": 1543226615 + }, + "origin": "perl-mime-types" + }, + "libasyncns": { + "versions": { + "0.8-r1": 1543226569 + }, + "origin": "libasyncns", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libasyncns.so.0=0.3.1" + ] + }, + "qemu-guest-agent": { + "versions": { + "3.1.0-r3": 1551107299 + }, + "origin": "qemu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0" + ], + "provides": [ + "cmd:qemu-ga" + ] + }, + "librelp-dev": { + "versions": { + "1.3.0-r0": 1548636902 + }, + "origin": "librelp", + "dependencies": [ + "librelp=1.3.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:relp=1.3.0" + ] + }, + "cloog-dev": { + "versions": { + "0.18.4-r2": 1543226589 + }, + "origin": "cloog", + "dependencies": [ + "gmp-dev", + "isl-dev", + "cloog=0.18.4-r2", + "pkgconfig" + ], + "provides": [ + "pc:cloog-isl=0.18.4" + ] + }, + "mpfr3": { + "versions": { + "3.1.5-r1": 1542301549 + }, + "origin": "mpfr3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10" + ], + "provides": [ + "so:libmpfr.so.4=4.1.5" + ] + }, + "glade3-lang": { + "versions": { + "3.8.5-r5": 1543998726 + }, + "origin": "glade3" + }, + "perdition-doc": { + "versions": { + "2.2-r0": 1545223335 + }, + "origin": "perdition" + }, + "lua5.3-microlight": { + "versions": { + "1.1.1-r2": 1543249973 + }, + "origin": "lua-microlight" + }, + "f2fs-tools-doc": { + "versions": { + "1.12.0-r0": 1546344802 + }, + "origin": "f2fs-tools" + }, + "libpng": { + "versions": { + "1.6.37-r0": 1557129095 + }, + "origin": "libpng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libpng16.so.16=16.37.0" + ] + }, + "exiv2-dev": { + "versions": { + "0.26-r0": 1543246768 + }, + "origin": "exiv2", + "dependencies": [ + "expat-dev", + "zlib-dev", + "exiv2=0.26-r0", + "pkgconfig" + ], + "provides": [ + "pc:exiv2=0.26" + ] + }, + "ilbc": { + "versions": { + "0.0.1-r0": 1545117065 + }, + "origin": "ilbc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libilbc.so.0=0.0.2" + ] + }, + "ivykis": { + "versions": { + "0.42.3-r0": 1548543106 + }, + "origin": "ivykis", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libivykis.so.0=0.5.5" + ] + }, + "linux-firmware-cavium": { + "versions": { + "20190322-r0": 1554980653 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "lua-gversion": { + "versions": { + "0.2.0-r2": 1545292687 + }, + "origin": "lua-gversion", + "dependencies": [ + "lua" + ], + "provides": [ + "lua5.1-gversion=0.2.0-r2", + "lua5.2-gversion=0.2.0-r2", + "lua5.3-gversion=0.2.0-r2" + ] + }, + "gpgme-doc": { + "versions": { + "1.12.0-r3": 1543932321 + }, + "origin": "gpgme" + }, + "libnetfilter_conntrack": { + "versions": { + "1.0.6-r0": 1545165063 + }, + "origin": "libnetfilter_conntrack", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0", + "so:libnfnetlink.so.0" + ], + "provides": [ + "so:libnetfilter_conntrack.so.3=3.6.0" + ] + }, + "ipset-dev": { + "versions": { + "7.1-r0": 1545163449 + }, + "origin": "ipset", + "dependencies": [ + "libmnl-dev", + "ipset=7.1-r0", + "pc:libmnl>=1", + "pkgconfig" + ], + "provides": [ + "pc:libipset=7.1" + ] + }, + "bluez-cups": { + "versions": { + "5.50-r0": 1545822205 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0" + ] + }, + "perl-json-maybexs-doc": { + "versions": { + "1.004000-r0": 1542893316 + }, + "origin": "perl-json-maybexs" + }, + "nagios-plugins-procs": { + "versions": { + "2.2.1-r6": 1543933910 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "dansguardian": { + "versions": { + "2.12.0.3-r4": 1545207099 + }, + "origin": "dansguardian", + "dependencies": [ + "logrotate", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpcreposix.so.0", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:dansguardian" + ] + }, + "py-ecdsa": { + "versions": { + "0.13-r5": 1545301311 + }, + "origin": "py-ecdsa" + }, + "perl-gd-doc": { + "versions": { + "2.67-r0": 1543077251 + }, + "origin": "perl-gd" + }, + "coreutils": { + "versions": { + "8.30-r0": 1542304119 + }, + "origin": "coreutils", + "dependencies": [ + "/bin/sh", + "so:libacl.so.1", + "so:libattr.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:[", + "cmd:b2sum", + "cmd:base32", + "cmd:base64", + "cmd:basename", + "cmd:cat", + "cmd:chcon", + "cmd:chgrp", + "cmd:chmod", + "cmd:chown", + "cmd:chroot", + "cmd:cksum", + "cmd:comm", + "cmd:coreutils", + "cmd:cp", + "cmd:csplit", + "cmd:cut", + "cmd:date", + "cmd:dd", + "cmd:df", + "cmd:dir", + "cmd:dircolors", + "cmd:dirname", + "cmd:du", + "cmd:echo", + "cmd:env", + "cmd:expand", + "cmd:expr", + "cmd:factor", + "cmd:false", + "cmd:fmt", + "cmd:fold", + "cmd:groups", + "cmd:head", + "cmd:hostid", + "cmd:id", + "cmd:install", + "cmd:join", + "cmd:link", + "cmd:ln", + "cmd:logname", + "cmd:ls", + "cmd:md5sum", + "cmd:mkdir", + "cmd:mkfifo", + "cmd:mknod", + "cmd:mktemp", + "cmd:mv", + "cmd:nice", + "cmd:nl", + "cmd:nohup", + "cmd:nproc", + "cmd:numfmt", + "cmd:od", + "cmd:paste", + "cmd:pathchk", + "cmd:pinky", + "cmd:pr", + "cmd:printenv", + "cmd:printf", + "cmd:ptx", + "cmd:pwd", + "cmd:readlink", + "cmd:realpath", + "cmd:rm", + "cmd:rmdir", + "cmd:runcon", + "cmd:seq", + "cmd:sha1sum", + "cmd:sha224sum", + "cmd:sha256sum", + "cmd:sha384sum", + "cmd:sha512sum", + "cmd:shred", + "cmd:shuf", + "cmd:sleep", + "cmd:sort", + "cmd:split", + "cmd:stat", + "cmd:stdbuf", + "cmd:stty", + "cmd:sum", + "cmd:sync", + "cmd:tac", + "cmd:tail", + "cmd:tee", + "cmd:test", + "cmd:timeout", + "cmd:touch", + "cmd:tr", + "cmd:true", + "cmd:truncate", + "cmd:tsort", + "cmd:tty", + "cmd:uname", + "cmd:unexpand", + "cmd:uniq", + "cmd:unlink", + "cmd:users", + "cmd:vdir", + "cmd:wc", + "cmd:who", + "cmd:whoami", + "cmd:yes" + ] + }, + "py2-redis": { + "versions": { + "2.10.6-r0": 1545301343 + }, + "origin": "py-redis", + "dependencies": [ + "python2" + ] + }, + "cyrus-sasl-login": { + "versions": { + "2.1.27-r1": 1550353528 + }, + "origin": "cyrus-sasl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "ttf-ubuntu-font-family": { + "versions": { + "0.83-r0": 1545300911 + }, + "origin": "ttf-ubuntu-font-family" + }, + "tevent-dev": { + "versions": { + "0.9.37-r1": 1543933085 + }, + "origin": "tevent", + "dependencies": [ + "pc:talloc", + "pkgconfig", + "tevent=0.9.37-r1" + ], + "provides": [ + "pc:tevent=0.9.37" + ] + }, + "pangox-compat": { + "versions": { + "0.0.2-r0": 1544001260 + }, + "origin": "pangox-compat", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpango-1.0.so.0" + ], + "provides": [ + "so:libpangox-1.0.so.0=0.0.0" + ] + }, + "py2-tdb": { + "versions": { + "1.3.16-r0": 1543933253 + }, + "origin": "tdb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libtdb.so.1" + ], + "provides": [ + "py-tdb=1.3.16-r0" + ] + }, + "perl-netaddr-ip-doc": { + "versions": { + "4.079-r1": 1542925007 + }, + "origin": "perl-netaddr-ip" + }, + "py-urllib3": { + "versions": { + "1.22-r0": 1542825025 + }, + "origin": "py-urllib3" + }, + "py2-future": { + "versions": { + "0.17.0-r0": 1545213721 + }, + "origin": "py-future", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:futurize", + "cmd:pasteurize" + ] + }, + "mariadb-plugin-rocksdb": { + "versions": { + "10.3.13-r1": 1557431736 + }, + "origin": "mariadb", + "dependencies": [ + "mariadb=10.3.13-r1", + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ] + }, + "tmux": { + "versions": { + "2.8-r0": 1546590689 + }, + "origin": "tmux", + "dependencies": [ + "ncurses-terminfo", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:tmux" + ] + }, + "grub": { + "versions": { + "2.02-r14": 1548432370 + }, + "origin": "grub", + "dependencies": [ + "mkinitfs", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02", + "so:libfreetype.so.6", + "so:liblzma.so.5" + ], + "provides": [ + "cmd:grub-bios-setup", + "cmd:grub-editenv", + "cmd:grub-file", + "cmd:grub-fstest", + "cmd:grub-glue-efi", + "cmd:grub-install", + "cmd:grub-kbdcomp", + "cmd:grub-macbless", + "cmd:grub-menulst2cfg", + "cmd:grub-mkconfig", + "cmd:grub-mkfont", + "cmd:grub-mkimage", + "cmd:grub-mklayout", + "cmd:grub-mknetdir", + "cmd:grub-mkpasswd-pbkdf2", + "cmd:grub-mkrelpath", + "cmd:grub-mkrescue", + "cmd:grub-mkstandalone", + "cmd:grub-ofpathname", + "cmd:grub-probe", + "cmd:grub-reboot", + "cmd:grub-render-label", + "cmd:grub-script-check", + "cmd:grub-set-default", + "cmd:grub-sparc64-setup", + "cmd:grub-syslinux2cfg" + ] + }, + "libxi-dev": { + "versions": { + "1.7.9-r2": 1543228125 + }, + "origin": "libxi", + "dependencies": [ + "libxi=1.7.9-r2", + "pc:inputproto", + "pc:x11", + "pc:xext", + "pc:xfixes", + "pkgconfig" + ], + "provides": [ + "pc:xi=1.7.9" + ] + }, + "py-factory-boy": { + "versions": { + "2.11.1-r0": 1543253882 + }, + "origin": "py-factory-boy" + }, + "libmount": { + "versions": { + "2.33-r0": 1545307436 + }, + "origin": "util-linux", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmount.so.1=1.1.0" + ] + }, + "perl-test-sharedfork-doc": { + "versions": { + "0.35-r0": 1543249822 + }, + "origin": "perl-test-sharedfork" + }, + "acf-core": { + "versions": { + "0.21.1-r1": 1543246773 + }, + "origin": "acf-core", + "dependencies": [ + "acf-jquery", + "acf-lib", + "acf-skins", + "haserl-lua5.2", + "lua5.2", + "lua5.2-posix", + "lua5.2-md5", + "lua-json4", + "lua5.2-subprocess", + "/bin/sh" + ], + "provides": [ + "cmd:acf-cli", + "cmd:acfpasswd" + ] + }, + "lua5.2-ossl": { + "versions": { + "20180708-r2": 1545076448 + }, + "origin": "lua-ossl", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "libcanberra-gtk3": { + "versions": { + "0.30-r2": 1545118059 + }, + "origin": "libcanberra", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcanberra.so.0", + "so:libgdk-3.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0" + ], + "provides": [ + "so:libcanberra-gtk3.so.0=0.1.9", + "cmd:canberra-gtk-play" + ] + }, + "avahi-ui-gtk3": { + "versions": { + "0.6.31-r7": 1545163747 + }, + "origin": "avahi-ui", + "dependencies": [ + "so:libavahi-client.so.3", + "so:libavahi-common.so.3", + "so:libavahi-glib.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libgdk-3.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8" + ], + "provides": [ + "so:libavahi-ui-gtk3.so.0=0.1.4" + ] + }, + "aspell-lang": { + "versions": { + "0.60.6.1-r13": 1542965830 + }, + "origin": "aspell" + }, + "hexchat-python": { + "versions": { + "2.14.2-r0": 1545746059 + }, + "origin": "hexchat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libpython3.6m.so.1.0" + ] + }, + "libva": { + "versions": { + "2.2.0-r0": 1542900261 + }, + "origin": "libva", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXfixes.so.3", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2" + ], + "provides": [ + "so:libva-drm.so.2=2.200.0", + "so:libva-x11.so.2=2.200.0", + "so:libva.so.2=2.200.0" + ] + }, + "lynx-doc": { + "versions": { + "2.8.8_p2-r8": 1545154517 + }, + "origin": "lynx" + }, + "boost-contract": { + "versions": { + "1.67.0-r2": 1542823631 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.67.0", + "so:libboost_system.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_contract-mt.so.1.67.0=1.67.0", + "so:libboost_contract.so.1.67.0=1.67.0" + ] + }, + "font-bh-ttf": { + "versions": { + "1.0.3-r0": 1545213976 + }, + "origin": "font-bh-ttf", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "uwsgi-router_hash": { + "versions": { + "2.0.17.1-r0": 1545062206 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "acct": { + "versions": { + "6.6.4-r0": 1545209152 + }, + "origin": "acct", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ac", + "cmd:accton", + "cmd:dump-acct", + "cmd:dump-utmp", + "cmd:last", + "cmd:lastcomm", + "cmd:sa" + ] + }, + "gamin": { + "versions": { + "0.1.10-r10": 1543928476 + }, + "origin": "gamin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "so:libfam.so.0=0.0.0", + "so:libgamin-1.so.0=0.1.10" + ] + }, + "ipptool": { + "versions": { + "2.2.10-r0": 1545910846 + }, + "origin": "cups", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2" + ], + "provides": [ + "cmd:ipptool" + ] + }, + "perl-encode-hanextra-doc": { + "versions": { + "0.23-r2": 1545075147 + }, + "origin": "perl-encode-hanextra" + }, + "snmptt": { + "versions": { + "1.4-r0": 1542924637 + }, + "origin": "snmptt", + "dependencies": [ + "perl", + "perl-config-inifiles", + "perl-list-moreutils", + "/bin/sh" + ], + "provides": [ + "cmd:snmptt", + "cmd:snmpttconvert", + "cmd:snmpttconvertmib", + "cmd:snmptthandler" + ] + }, + "mesa-xatracker": { + "versions": { + "18.1.7-r2": 1554455971 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_freedreno.so.1", + "so:libdrm_nouveau.so.2", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libxatracker.so.2=2.3.0" + ] + }, + "txt2man-doc": { + "versions": { + "1.6.0-r0": 1545076642 + }, + "origin": "txt2man" + }, + "nspr": { + "versions": { + "4.20-r0": 1545207862 + }, + "origin": "nspr", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnspr4.so=0", + "so:libplc4.so=0", + "so:libplds4.so=0" + ] + }, + "libpcap-doc": { + "versions": { + "1.9.0-r1": 1545905178 + }, + "origin": "libpcap" + }, + "iso-codes": { + "versions": { + "4.1-r0": 1545257107 + }, + "origin": "iso-codes" + }, + "ncurses5-libs": { + "versions": { + "5.9-r1": 1545292735 + }, + "origin": "ncurses5", + "dependencies": [ + "ncurses-terminfo-base", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libform.so.5=5.9", + "so:libmenu.so.5=5.9", + "so:libncurses.so.5=5.9", + "so:libpanel.so.5=5.9" + ] + }, + "icecast-doc": { + "versions": { + "2.4.4-r1": 1543934499 + }, + "origin": "icecast" + }, + "dovecot-pop3d": { + "versions": { + "2.3.6-r0": 1557134097 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libdovecot-login.so.0", + "so:libdovecot-storage.so.0", + "so:libdovecot.so.0" + ] + }, + "qemu-mipsn32": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-mipsn32" + ] + }, + "perl-importer": { + "versions": { + "0.025-r0": 1542972984 + }, + "origin": "perl-importer", + "dependencies": [ + "perl" + ] + }, + "ccache-doc": { + "versions": { + "3.5.1-r0": 1547294261 + }, + "origin": "ccache" + }, + "ncdu": { + "versions": { + "1.13-r0": 1545154410 + }, + "origin": "ncdu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:ncdu" + ] + }, + "libarchive-tools": { + "versions": { + "3.3.2-r4": 1542820311 + }, + "origin": "libarchive", + "dependencies": [ + "so:libacl.so.1", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libexpat.so.1", + "so:liblz4.so.1", + "so:liblzma.so.5", + "so:libz.so.1" + ], + "provides": [ + "cmd:bsdcat", + "cmd:bsdcpio", + "cmd:bsdtar" + ] + }, + "boost": { + "versions": { + "1.67.0-r2": 1542823634 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_chrono-mt.so.1.67.0", + "so:libboost_chrono.so.1.67.0", + "so:libboost_filesystem-mt.so.1.67.0", + "so:libboost_filesystem.so.1.67.0", + "so:libboost_regex-mt.so.1.67.0", + "so:libboost_regex.so.1.67.0", + "so:libboost_system-mt.so.1.67.0", + "so:libboost_system.so.1.67.0", + "so:libboost_thread-mt.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libboost_locale-mt.so.1.67.0=1.67.0", + "so:libboost_log-mt.so.1.67.0=1.67.0", + "so:libboost_log.so.1.67.0=1.67.0", + "so:libboost_log_setup-mt.so.1.67.0=1.67.0", + "so:libboost_log_setup.so.1.67.0=1.67.0", + "so:libboost_stacktrace_basic-mt.so.1.67.0=1.67.0", + "so:libboost_stacktrace_basic.so.1.67.0=1.67.0", + "so:libboost_stacktrace_noop-mt.so.1.67.0=1.67.0", + "so:libboost_stacktrace_noop.so.1.67.0=1.67.0", + "so:libboost_timer-mt.so.1.67.0=1.67.0", + "so:libboost_timer.so.1.67.0=1.67.0", + "so:libboost_type_erasure-mt.so.1.67.0=1.67.0", + "so:libboost_type_erasure.so.1.67.0=1.67.0", + "cmd:b2", + "cmd:bcp", + "cmd:bjam" + ] + }, + "x264": { + "versions": { + "20180304-r1": 1545299804 + }, + "origin": "x264", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:x264" + ] + }, + "py3-musicbrainzngs": { + "versions": { + "0.6-r2": 1543240636 + }, + "origin": "py-musicbrainzngs", + "dependencies": [ + "python3" + ] + }, + "alpine-ipxe": { + "versions": { + "1.0_git20180825-r1": 1545986078 + }, + "origin": "alpine-ipxe" + }, + "gtk-murrine-engine": { + "versions": { + "0.98.2-r2": 1543934246 + }, + "origin": "gtk-murrine-engine", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libpixman-1.so.0" + ] + }, + "krb5-pkinit": { + "versions": { + "1.15.5-r0": 1548094458 + }, + "origin": "krb5", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libcrypto.so.1.1", + "so:libk5crypto.so.3", + "so:libkrb5.so.3", + "so:libkrb5support.so.0" + ] + }, + "squid-lang-ru": { + "versions": { + "4.4-r1": 1545216329 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "boost-container": { + "versions": { + "1.67.0-r2": 1542823630 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_container-mt.so.1.67.0=1.67.0", + "so:libboost_container.so.1.67.0=1.67.0" + ] + }, + "ldb-tools": { + "versions": { + "1.3.8-r0": 1555334661 + }, + "origin": "ldb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libldb.so.1", + "so:libpopt.so.0", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libldb-cmdline.so=0", + "cmd:ldbadd", + "cmd:ldbdel", + "cmd:ldbedit", + "cmd:ldbmodify", + "cmd:ldbrename", + "cmd:ldbsearch" + ] + }, + "json-glib-lang": { + "versions": { + "1.4.4-r0": 1545837175 + }, + "origin": "json-glib" + }, + "harfbuzz-dev": { + "versions": { + "2.2.0-r0": 1545249742 + }, + "origin": "harfbuzz", + "dependencies": [ + "harfbuzz-icu=2.2.0-r0", + "harfbuzz=2.2.0-r0", + "pc:glib-2.0", + "pc:glib-2.0>=2.19.1", + "pc:gobject-2.0", + "pc:graphite2>=1.2.0", + "pc:icu-uc", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libfreetype.so.6", + "so:libglib-2.0.so.0", + "so:libharfbuzz-subset.so.0", + "so:libharfbuzz.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "pc:harfbuzz-gobject=2.2.0", + "pc:harfbuzz-icu=2.2.0", + "pc:harfbuzz-subset=2.2.0", + "pc:harfbuzz=2.2.0", + "cmd:hb-ot-shape-closure", + "cmd:hb-shape", + "cmd:hb-subset", + "cmd:hb-view" + ] + }, + "libgsf-doc": { + "versions": { + "1.14.44-r0": 1543926574 + }, + "origin": "libgsf" + }, + "herbstluftwm-doc": { + "versions": { + "0.7.1-r0": 1545300035 + }, + "origin": "herbstluftwm" + }, + "rsyslog-udpspoof": { + "versions": { + "8.40.0-r3": 1548686792 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libnet.so.1" + ], + "provides": [ + "rsyslog-omudpspoof=8.40.0-r3" + ] + }, + "alpine-ipxe-ipxe_lkrn": { + "versions": { + "1.0_git20180825-r1": 1545986078 + }, + "origin": "alpine-ipxe" + }, + "kamailio-presence": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libsrdb1.so.1", + "so:libsrutils.so.1", + "so:libxml2.so.2" + ] + }, + "logtail": { + "versions": { + "3.21-r0": 1543999486 + }, + "origin": "logtail", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:logtail" + ] + }, + "the_silver_searcher-doc": { + "versions": { + "2.1.0-r2": 1543998777 + }, + "origin": "the_silver_searcher" + }, + "acf-lib-lua5.1": { + "versions": { + "0.10.1-r0": 1542883783 + }, + "origin": "acf-lib" + }, + "squid-lang-ms": { + "versions": { + "4.4-r1": 1545216328 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "gtk-engines": { + "versions": { + "2.21.0-r2": 1545289338 + }, + "origin": "gtk-engines", + "dependencies": [ + "gtk-engines-clearlooks", + "gtk-engines-crux", + "gtk-engines-industrial", + "gtk-engines-mist", + "gtk-engines-redmond", + "gtk-engines-thinice", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "collectd-ping": { + "versions": { + "5.8.0-r3": 1546422678 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:liboping.so.0" + ] + }, + "email2trac": { + "versions": { + "2.5.0-r1": 1543924858 + }, + "origin": "email2trac", + "dependencies": [ + "trac", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:delete_spam", + "cmd:email2trac", + "cmd:run_email2trac" + ] + }, + "xf86-input-mouse-dev": { + "versions": { + "1.9.3-r1": 1545300644 + }, + "origin": "xf86-input-mouse", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xorg-mouse=1.9.3" + ] + }, + "qemu-system-microblazeel": { + "versions": { + "3.1.0-r3": 1551107303 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-microblazeel" + ] + }, + "aumix-doc": { + "versions": { + "2.9.1-r6": 1545069455 + }, + "origin": "aumix" + }, + "libpthread-stubs": { + "versions": { + "0.3-r5": 1542822458 + }, + "origin": "libpthread-stubs", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:pthread-stubs=0.3" + ] + }, + "linux-pam-dev": { + "versions": { + "1.3.0-r0": 1542820658 + }, + "origin": "linux-pam", + "dependencies": [ + "gettext-dev", + "linux-pam=1.3.0-r0" + ] + }, + "perl-list-moreutils": { + "versions": { + "0.419-r1": 1542845626 + }, + "origin": "perl-list-moreutils", + "dependencies": [ + "perl-exporter-tiny" + ] + }, + "zfs-vanilla": { + "versions": { + "4.19.41-r0": 1557400769 + }, + "origin": "zfs-vanilla", + "dependencies": [ + "spl-vanilla", + "linux-vanilla=4.19.41-r0" + ] + }, + "linux-firmware-rockchip": { + "versions": { + "20190322-r0": 1554980649 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "intltool-doc": { + "versions": { + "0.51.0-r4": 1542821919 + }, + "origin": "intltool" + }, + "groff": { + "versions": { + "1.22.3-r2": 1542819580 + }, + "origin": "groff", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:addftinfo", + "cmd:afmtodit", + "cmd:chem", + "cmd:eqn", + "cmd:eqn2graph", + "cmd:gdiffmk", + "cmd:glilypond", + "cmd:gperl", + "cmd:gpinyin", + "cmd:grap2graph", + "cmd:grn", + "cmd:grodvi", + "cmd:groff", + "cmd:groffer", + "cmd:grog", + "cmd:grolbp", + "cmd:grolj4", + "cmd:gropdf", + "cmd:grops", + "cmd:grotty", + "cmd:hpftodit", + "cmd:indxbib", + "cmd:lkbib", + "cmd:lookbib", + "cmd:mmroff", + "cmd:neqn", + "cmd:nroff", + "cmd:pdfmom", + "cmd:pdfroff", + "cmd:pfbtops", + "cmd:pic", + "cmd:pic2graph", + "cmd:post-grohtml", + "cmd:pre-grohtml", + "cmd:preconv", + "cmd:refer", + "cmd:roff2dvi", + "cmd:roff2html", + "cmd:roff2pdf", + "cmd:roff2ps", + "cmd:roff2text", + "cmd:roff2x", + "cmd:soelim", + "cmd:tbl", + "cmd:tfmtodit", + "cmd:troff" + ] + }, + "py3-flask-oauthlib": { + "versions": { + "0.9.5-r0": 1545164578 + }, + "origin": "py-flask-oauthlib", + "dependencies": [ + "py3-flask", + "py3-requests-oauthlib", + "python3" + ] + }, + "perl-archive-zip-doc": { + "versions": { + "1.64-r0": 1544001401 + }, + "origin": "perl-archive-zip" + }, + "ciwiki-doc": { + "versions": { + "2.0.5-r1": 1545292637 + }, + "origin": "ciwiki" + }, + "py3-tz": { + "versions": { + "2018.9-r0": 1548109649 + }, + "origin": "py-tz", + "dependencies": [ + "python3" + ] + }, + "py-gnome-gconf": { + "versions": { + "2.28.1-r5": 1545301290 + }, + "origin": "py-gnome", + "dependencies": [ + "gconf", + "py-gtk", + "so:libc.musl-x86_64.so.1", + "so:libgconf-2.so.4", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libpython2.7.so.1.0" + ] + }, + "unbound": { + "versions": { + "1.8.3-r1": 1555953584 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libevent-2.1.so.6", + "so:libexpat.so.1", + "so:libssl.so.1.1", + "so:libunbound.so.8" + ], + "provides": [ + "cmd:unbound", + "cmd:unbound-anchor", + "cmd:unbound-checkconf", + "cmd:unbound-control", + "cmd:unbound-control-setup", + "cmd:unbound-host" + ] + }, + "kamailio-db": { + "versions": { + "5.2.0-r1": 1546423169 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so", + "so:libsrdb1.so.1", + "so:libsrdb2.so.1", + "so:libsrutils.so.1", + "so:libtrie.so.1" + ] + }, + "gnome-vfs": { + "versions": { + "2.24.4-r6": 1545299692 + }, + "origin": "gnome-vfs", + "dependencies": [ + "gnome-mime-data", + "/bin/sh", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libdbus-glib-1.so.2", + "so:libfam.so.0", + "so:libgconf-2.so.4", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libgnomevfs-2.so.0=0.2400.4", + "cmd:gnomevfs-cat", + "cmd:gnomevfs-copy", + "cmd:gnomevfs-df", + "cmd:gnomevfs-info", + "cmd:gnomevfs-ls", + "cmd:gnomevfs-mkdir", + "cmd:gnomevfs-monitor", + "cmd:gnomevfs-mv", + "cmd:gnomevfs-rm" + ] + }, + "ruby-dev": { + "versions": { + "2.5.5-r0": 1557164836 + }, + "origin": "ruby", + "dependencies": [ + "ruby=2.5.5-r0", + "gmp-dev", + "pkgconfig", + "ruby-libs=2.5.5-r0" + ], + "provides": [ + "pc:ruby-2.5=2.5.0" + ] + }, + "perl-test-notabs-doc": { + "versions": { + "2.02-r0": 1545069438 + }, + "origin": "perl-test-notabs" + }, + "libxvmc-doc": { + "versions": { + "1.0.10-r2": 1542900306 + }, + "origin": "libxvmc" + }, + "xf86-video-ark": { + "versions": { + "0.7.5-r10": 1543928086 + }, + "origin": "xf86-video-ark", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "xmlrpc-c-client": { + "versions": { + "1.39.13-r0": 1545665213 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libxmlrpc.so.3", + "so:libxmlrpc_util.so.3" + ], + "provides": [ + "so:libxmlrpc_client.so.3=3.39" + ] + }, + "perl-net-ssleay-doc": { + "versions": { + "1.85-r4": 1545163948 + }, + "origin": "perl-net-ssleay" + }, + "libao": { + "versions": { + "1.2.0-r3": 1545067193 + }, + "origin": "libao", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libao.so.4=4.1.0" + ] + }, + "s6": { + "versions": { + "2.7.2.0-r0": 1545062681 + }, + "origin": "s6", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libexecline.so.2.5", + "so:libskarnet.so.2.7" + ], + "provides": [ + "so:libs6.so.2.7=2.7.2.0", + "cmd:s6-accessrules-cdb-from-fs", + "cmd:s6-accessrules-fs-from-cdb", + "cmd:s6-applyuidgid", + "cmd:s6-cleanfifodir", + "cmd:s6-connlimit", + "cmd:s6-envdir", + "cmd:s6-envuidgid", + "cmd:s6-fdholder-daemon", + "cmd:s6-fdholder-delete", + "cmd:s6-fdholder-deletec", + "cmd:s6-fdholder-getdump", + "cmd:s6-fdholder-getdumpc", + "cmd:s6-fdholder-list", + "cmd:s6-fdholder-listc", + "cmd:s6-fdholder-retrieve", + "cmd:s6-fdholder-retrievec", + "cmd:s6-fdholder-setdump", + "cmd:s6-fdholder-setdumpc", + "cmd:s6-fdholder-store", + "cmd:s6-fdholder-storec", + "cmd:s6-fdholder-transferdump", + "cmd:s6-fdholder-transferdumpc", + "cmd:s6-fdholderd", + "cmd:s6-fghack", + "cmd:s6-ftrig-listen", + "cmd:s6-ftrig-listen1", + "cmd:s6-ftrig-notify", + "cmd:s6-ftrig-wait", + "cmd:s6-ftrigrd", + "cmd:s6-ioconnect", + "cmd:s6-ipcclient", + "cmd:s6-ipcserver", + "cmd:s6-ipcserver-access", + "cmd:s6-ipcserver-socketbinder", + "cmd:s6-ipcserverd", + "cmd:s6-log", + "cmd:s6-mkfifodir", + "cmd:s6-notifyoncheck", + "cmd:s6-permafailon", + "cmd:s6-setlock", + "cmd:s6-setsid", + "cmd:s6-setuidgid", + "cmd:s6-softlimit", + "cmd:s6-sudo", + "cmd:s6-sudoc", + "cmd:s6-sudod", + "cmd:s6-supervise", + "cmd:s6-svc", + "cmd:s6-svdt", + "cmd:s6-svdt-clear", + "cmd:s6-svlisten", + "cmd:s6-svlisten1", + "cmd:s6-svok", + "cmd:s6-svscan", + "cmd:s6-svscanctl", + "cmd:s6-svstat", + "cmd:s6-svwait", + "cmd:s6-tai64n", + "cmd:s6-tai64nlocal", + "cmd:s6lockd", + "cmd:ucspilogd" + ] + }, + "directfb": { + "versions": { + "1.7.7-r1": 1543934413 + }, + "origin": "directfb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libfreetype.so.6", + "so:libgcc_s.so.1", + "so:libkms.so.1", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libts.so.0" + ], + "provides": [ + "so:lib++dfb-1.7.so.7=7.0.0", + "so:libdirect-1.7.so.7=7.0.0", + "so:libdirectfb-1.7.so.7=7.0.0", + "so:libfusion-1.7.so.7=7.0.0", + "cmd:dfbdump", + "cmd:dfbdumpinput", + "cmd:dfbfx", + "cmd:dfbg", + "cmd:dfbinfo", + "cmd:dfbinput", + "cmd:dfbinspector", + "cmd:dfblayer", + "cmd:dfbmaster", + "cmd:dfbpenmount", + "cmd:dfbplay", + "cmd:dfbscreen", + "cmd:dfbshow", + "cmd:dfbswitch", + "cmd:directfb-csource", + "cmd:mkdfiff", + "cmd:mkdgiff", + "cmd:mkdgifft" + ] + }, + "py3-unidecode": { + "versions": { + "1.0.23-r0": 1545299544 + }, + "origin": "py-unidecode", + "dependencies": [ + "py3-unidecode", + "python3" + ], + "provides": [ + "cmd:unidecode-3" + ] + }, + "zip-doc": { + "versions": { + "3.0-r7": 1542301519 + }, + "origin": "zip" + }, + "startup-notification": { + "versions": { + "0.12-r3": 1543254000 + }, + "origin": "startup-notification", + "dependencies": [ + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libxcb-util.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libstartup-notification-1.so.0=0.0.0" + ] + }, + "stalonetray": { + "versions": { + "0.8.3-r0": 1545207381 + }, + "origin": "stalonetray", + "dependencies": [ + "so:libX11.so.6", + "so:libXpm.so.4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:stalonetray" + ] + }, + "lua-lub": { + "versions": { + "1.1.0-r1": 1544000686 + }, + "origin": "lua-lub" + }, + "libuv": { + "versions": { + "1.23.2-r0": 1542820441 + }, + "origin": "libuv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libuv.so.1=1.0.0" + ] + }, + "pytest": { + "versions": { + "4.1.0-r0": 1548112483 + }, + "origin": "pytest", + "dependencies": [ + "py3-pytest", + "py3-pytest=4.1.0-r0" + ] + }, + "stalonetray-doc": { + "versions": { + "0.8.3-r0": 1545207381 + }, + "origin": "stalonetray" + }, + "rrdtool-dev": { + "versions": { + "1.7.0-r0": 1542924799 + }, + "origin": "rrdtool", + "dependencies": [ + "librrd=1.7.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:librrd=1.7.0" + ] + }, + "libogg-dev": { + "versions": { + "1.3.3-r2": 1543925776 + }, + "origin": "libogg", + "dependencies": [ + "libogg=1.3.3-r2", + "pkgconfig" + ], + "provides": [ + "pc:ogg=1.3.3" + ] + }, + "pptpd-doc": { + "versions": { + "1.4.0-r1": 1544000992 + }, + "origin": "pptpd" + }, + "arpwatch": { + "versions": { + "2.1a15-r16": 1544000304 + }, + "origin": "arpwatch", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:arp2ethers", + "cmd:arpfetch", + "cmd:arpsnmp", + "cmd:arpwatch", + "cmd:bihourly.sh", + "cmd:massagevendor" + ] + }, + "apache-mod-fcgid-doc": { + "versions": { + "2.3.9-r2": 1545293006 + }, + "origin": "apache-mod-fcgid" + }, + "libmnl-dev": { + "versions": { + "1.0.4-r0": 1542883675 + }, + "origin": "libmnl", + "dependencies": [ + "linux-headers", + "libmnl=1.0.4-r0", + "pkgconfig" + ], + "provides": [ + "pc:libmnl=1.0.4" + ] + }, + "perl-text-vfile-asdata": { + "versions": { + "0.08-r0": 1545254277 + }, + "origin": "perl-text-vfile-asdata", + "dependencies": [ + "perl-class-accessor-chained" + ] + }, + "cmake": { + "versions": { + "3.13.0-r0": 1542820578 + }, + "origin": "cmake", + "dependencies": [ + "so:libarchive.so.13", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libexpat.so.1", + "so:libformw.so.6", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:librhash.so.0", + "so:libstdc++.so.6", + "so:libuv.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:ccmake", + "cmd:cmake", + "cmd:cpack", + "cmd:ctest" + ] + }, + "lua5.2-maxminddb": { + "versions": { + "0.1-r1": 1543226674 + }, + "origin": "lua-maxminddb", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmaxminddb.so.0" + ] + }, + "qemu-system-m68k": { + "versions": { + "3.1.0-r3": 1551107303 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-m68k" + ] + }, + "perl-test-number-delta": { + "versions": { + "1.06-r0": 1545062326 + }, + "origin": "perl-test-number-delta" + }, + "py2-chardet": { + "versions": { + "3.0.4-r0": 1542825000 + }, + "origin": "py-chardet", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:chardetect" + ] + }, + "py-irc": { + "versions": { + "8.5.1-r0": 1545060751 + }, + "origin": "py-irc" + }, + "perl-inline-c-doc": { + "versions": { + "0.78-r0": 1545067075 + }, + "origin": "perl-inline-c" + }, + "libotr-dev": { + "versions": { + "4.1.1-r1": 1543248414 + }, + "origin": "libotr", + "dependencies": [ + "libgcrypt-dev", + "libotr=4.1.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:libotr=4.1.1" + ] + }, + "py-django-contact-form": { + "versions": { + "1.6-r0": 1545301319 + }, + "origin": "py-django-contact-form", + "dependencies": [ + "py-django" + ] + }, + "xmlrpc-c-doc": { + "versions": { + "1.39.13-r0": 1545665211 + }, + "origin": "xmlrpc-c" + }, + "libiec61883-doc": { + "versions": { + "1.2.0-r2": 1543999348 + }, + "origin": "libiec61883" + }, + "rsync-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308087 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "libcdio": { + "versions": { + "0.94-r2": 1543248377 + }, + "origin": "libcdio", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcdio.so.16=16.0.0", + "so:libiso9660.so.10=10.0.0", + "so:libudf.so.0=0.0.0" + ] + }, + "perl-importer-doc": { + "versions": { + "0.025-r0": 1542972983 + }, + "origin": "perl-importer" + }, + "imagemagick-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "nagios-plugins-mailq": { + "versions": { + "2.2.1-r6": 1543933908 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "perl" + ] + }, + "openpgm": { + "versions": { + "5.2.122-r1": 1543222718 + }, + "origin": "openpgm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpgm-5.2.so.0=0.0.122" + ] + }, + "librtmp": { + "versions": { + "2.4_git20160909-r6": 1545215000 + }, + "origin": "rtmpdump", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libgnutls.so.30", + "so:libhogweed.so.4", + "so:libnettle.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:librtmp.so.1=1" + ] + }, + "perl-font-afm-doc": { + "versions": { + "1.20-r0": 1542983919 + }, + "origin": "perl-font-afm" + }, + "btrfs-progs-libs": { + "versions": { + "4.19.1-r0": 1545665051 + }, + "origin": "btrfs-progs", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ], + "provides": [ + "so:libbtrfs.so.0=0.1", + "so:libbtrfsutil.so.1=1.1.0" + ] + }, + "libxdmcp-doc": { + "versions": { + "1.1.2-r5": 1542822510 + }, + "origin": "libxdmcp" + }, + "collectd-disk": { + "versions": { + "5.8.0-r3": 1546422678 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1" + ] + }, + "libid3tag": { + "versions": { + "0.15.1b-r7": 1542972924 + }, + "origin": "libid3tag", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libid3tag.so.0=0.3.0" + ] + }, + "clamav-scanner": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav", + "dependencies": [ + "freshclam", + "so:libc.musl-x86_64.so.1", + "so:libclamav.so.7", + "so:libcrypto.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:clambc", + "cmd:clamscan", + "cmd:sigtool" + ] + }, + "nagios-plugins-nagios": { + "versions": { + "2.2.1-r6": 1543933909 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-imaging": { + "versions": { + "1.1.7-r4": 1542893551 + }, + "origin": "py-imaging", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:pilconvert.py", + "cmd:pildriver.py", + "cmd:pilfile.py", + "cmd:pilfont.py", + "cmd:pilprint.py" + ] + }, + "ser2net-doc": { + "versions": { + "3.4-r1": 1543999372 + }, + "origin": "ser2net" + }, + "perl-log-any-doc": { + "versions": { + "1.707-r0": 1545208928 + }, + "origin": "perl-log-any" + }, + "lsof": { + "versions": { + "4.91-r0": 1545235377 + }, + "origin": "lsof", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:lsof" + ] + }, + "hwdata-oui": { + "versions": { + "0.318-r0": 1545746063 + }, + "origin": "hwdata" + }, + "fuse-doc": { + "versions": { + "2.9.8-r2": 1542972242 + }, + "origin": "fuse" + }, + "unrar-doc": { + "versions": { + "5.6.8-r0": 1545069402 + }, + "origin": "unrar" + }, + "cmph-dev": { + "versions": { + "2.0-r1": 1544000441 + }, + "origin": "cmph", + "dependencies": [ + "libcmph=2.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:cmph=2.0" + ] + }, + "perl-b-hooks-endofscope": { + "versions": { + "0.24-r0": 1542973108 + }, + "origin": "perl-b-hooks-endofscope", + "dependencies": [ + "perl-sub-exporter-progressive", + "perl-module-runtime", + "perl-module-implementation", + "perl-variable-magic" + ] + }, + "libbsd": { + "versions": { + "0.8.6-r2": 1542822480 + }, + "origin": "libbsd", + "dependencies": [ + "musl>=1.1.16-r22", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libbsd.so.0=0.8.6" + ] + }, + "py-simpleparse": { + "versions": { + "2.2.0-r0": 1545301094 + }, + "origin": "py-simpleparse", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "mc-lang": { + "versions": { + "4.8.22-r0": 1547126443 + }, + "origin": "mc" + }, + "libev-dev": { + "versions": { + "4.25-r0": 1545838252 + }, + "origin": "libev", + "dependencies": [ + "libev=4.25-r0", + "pkgconfig" + ], + "provides": [ + "pc:libev=4.25" + ] + }, + "font-bh-75dpi": { + "versions": { + "1.0.3-r0": 1545207070 + }, + "origin": "font-bh-75dpi", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-net-rblclient-doc": { + "versions": { + "0.5-r3": 1545214327 + }, + "origin": "perl-net-rblclient" + }, + "xcalib": { + "versions": { + "0.8-r0": 1543927704 + }, + "origin": "xcalib", + "dependencies": [ + "so:libX11.so.6", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xcalib" + ] + }, + "lmdb-doc": { + "versions": { + "0.9.23-r0": 1547025175 + }, + "origin": "lmdb" + }, + "py-templayer-doc": { + "versions": { + "1.5.1-r3": 1543935167 + }, + "origin": "py-templayer", + "dependencies": [ + "python2" + ] + }, + "libmemcached-libs": { + "versions": { + "1.0.18-r3": 1543923893 + }, + "origin": "libmemcached", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libsasl2.so.3", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libhashkit.so.2=2.0.0", + "so:libmemcached.so.11=11.0.0", + "so:libmemcachedprotocol.so.0=0.0.0", + "so:libmemcachedutil.so.2=2.0.0" + ] + }, + "openvswitch-dev": { + "versions": { + "2.10.1-r0": 1544000344 + }, + "origin": "openvswitch", + "dependencies": [ + "openssl-dev", + "pkgconfig" + ], + "provides": [ + "pc:libofproto=2.10.1", + "pc:libopenvswitch=2.10.1", + "pc:libovsdb=2.10.1", + "pc:libsflow=2.10.1" + ] + }, + "perl-class-singleton": { + "versions": { + "1.5-r0": 1542972932 + }, + "origin": "perl-class-singleton" + }, + "pax-utils": { + "versions": { + "1.2.3-r0": 1542304833 + }, + "origin": "pax-utils", + "dependencies": [ + "scanelf", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2" + ], + "provides": [ + "cmd:dumpelf", + "cmd:pspax", + "cmd:scanmacho", + "cmd:symtree" + ] + }, + "openresolv-doc": { + "versions": { + "3.9.0-r0": 1545116954 + }, + "origin": "openresolv" + }, + "libevent-dev": { + "versions": { + "2.1.8-r6": 1543223505 + }, + "origin": "libevent", + "dependencies": [ + "python2", + "libevent=2.1.8-r6", + "pkgconfig" + ], + "provides": [ + "pc:libevent=2.1.8-r6", + "pc:libevent_core=2.1.8-r6", + "pc:libevent_extra=2.1.8-r6", + "pc:libevent_openssl=2.1.8-r6", + "pc:libevent_pthreads=2.1.8-r6", + "cmd:event_rpcgen.py" + ] + }, + "xkbcomp-dev": { + "versions": { + "1.4.2-r0": 1542973207 + }, + "origin": "xkbcomp", + "dependencies": [ + "pc:x11", + "pc:xkbfile", + "pc:xproto>=7.0.17", + "pkgconfig" + ], + "provides": [ + "pc:xkbcomp=1.4.2" + ] + }, + "dbus-x11": { + "versions": { + "1.10.24-r1": 1542824378 + }, + "origin": "dbus", + "dependencies": [ + "dbus=1.10.24-r1", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3" + ], + "provides": [ + "cmd:dbus-launch" + ] + }, + "xgamma": { + "versions": { + "1.0.6-r0": 1545214308 + }, + "origin": "xgamma", + "dependencies": [ + "so:libX11.so.6", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xgamma" + ] + }, + "mosh-bash-completion": { + "versions": { + "1.3.2-r7": 1543932001 + }, + "origin": "mosh" + }, + "rsync-openrc": { + "versions": { + "3.1.3-r1": 1542820864 + }, + "origin": "rsync" + }, + "skalibs-dev": { + "versions": { + "2.7.0.0-r0": 1543221682 + }, + "origin": "skalibs", + "dependencies": [ + "skalibs=2.7.0.0-r0" + ] + }, + "libxcb-dev": { + "versions": { + "1.13-r2": 1542822547 + }, + "origin": "libxcb", + "dependencies": [ + "libxau-dev", + "xcb-proto", + "libxcb=1.13-r2", + "pc:pthread-stubs", + "pc:xau>=0.99.2", + "pc:xdmcp", + "pkgconfig" + ], + "provides": [ + "pc:xcb-composite=1.13", + "pc:xcb-damage=1.13", + "pc:xcb-dpms=1.13", + "pc:xcb-dri2=1.13", + "pc:xcb-dri3=1.13", + "pc:xcb-glx=1.13", + "pc:xcb-present=1.13", + "pc:xcb-randr=1.13", + "pc:xcb-record=1.13", + "pc:xcb-render=1.13", + "pc:xcb-res=1.13", + "pc:xcb-screensaver=1.13", + "pc:xcb-shape=1.13", + "pc:xcb-shm=1.13", + "pc:xcb-sync=1.13", + "pc:xcb-xf86dri=1.13", + "pc:xcb-xfixes=1.13", + "pc:xcb-xinerama=1.13", + "pc:xcb-xinput=1.13", + "pc:xcb-xkb=1.13", + "pc:xcb-xtest=1.13", + "pc:xcb-xv=1.13", + "pc:xcb-xvmc=1.13", + "pc:xcb=1.13" + ] + }, + "perl-www-robotrules-doc": { + "versions": { + "6.02-r1": 1542821857 + }, + "origin": "perl-www-robotrules" + }, + "avahi": { + "versions": { + "0.7-r1": 1543925313 + }, + "origin": "avahi", + "dependencies": [ + "/bin/sh", + "so:libavahi-common.so.3", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libdaemon.so.0", + "so:libdbus-1.so.3", + "so:libexpat.so.1" + ], + "provides": [ + "so:libavahi-core.so.7=7.0.2", + "cmd:avahi-daemon", + "cmd:avahi-discover", + "cmd:avahi-dnsconfd" + ] + }, + "uwsgi-zabbix": { + "versions": { + "2.0.17.1-r0": 1545062214 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "libtheora-doc": { + "versions": { + "1.1.1-r13": 1543928530 + }, + "origin": "libtheora" + }, + "dev86": { + "versions": { + "0.16.21-r0": 1543935746 + }, + "origin": "dev86", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ar86", + "cmd:as86", + "cmd:bcc", + "cmd:ld86", + "cmd:nm86", + "cmd:objdump86", + "cmd:size86" + ] + }, + "lua-posixtz": { + "versions": { + "0.5-r1": 1543928315 + }, + "origin": "lua-posixtz" + }, + "libgd": { + "versions": { + "2.2.5-r3": 1554727880 + }, + "origin": "gd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libwebp.so.7", + "so:libz.so.1" + ], + "provides": [ + "so:libgd.so.3=3.0.5" + ] + }, + "xscreensaver-lang": { + "versions": { + "5.40-r0": 1545224088 + }, + "origin": "xscreensaver", + "dependencies": [ + "bc" + ] + }, + "ghostscript-dbg": { + "versions": { + "9.26-r2": 1554362637 + }, + "origin": "ghostscript" + }, + "xf86-video-apm": { + "versions": { + "1.2.5-r11": 1545062640 + }, + "origin": "xf86-video-apm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py-urlgrabber-doc": { + "versions": { + "3.10.1-r1": 1543935134 + }, + "origin": "py-urlgrabber" + }, + "openvpn-auth-pam": { + "versions": { + "2.4.6-r4": 1543223245 + }, + "origin": "openvpn", + "dependencies": [ + "iproute2", + "so:libc.musl-x86_64.so.1", + "so:libpam.so.0" + ] + }, + "lua5.1-optarg": { + "versions": { + "0.2-r1": 1542820938 + }, + "origin": "lua-optarg", + "dependencies": [ + "lua5.1" + ] + }, + "libxvmc-dev": { + "versions": { + "1.0.10-r2": 1542900305 + }, + "origin": "libxvmc", + "dependencies": [ + "libxext-dev", + "libxvmc=1.0.10-r2", + "pc:videoproto", + "pc:x11", + "pc:xext", + "pc:xproto", + "pc:xv", + "pkgconfig" + ], + "provides": [ + "pc:xvmc=1.0.10" + ] + }, + "libunwind": { + "versions": { + "1.2.1-r3": 1545915765 + }, + "origin": "libunwind", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libunwind-coredump.so.0=0.0.0", + "so:libunwind-ptrace.so.0=0.0.0", + "so:libunwind-setjmp.so.0=0.0.0", + "so:libunwind-x86_64.so.8=8.0.1", + "so:libunwind.so.8=8.0.1" + ] + }, + "py-mccabe": { + "versions": { + "0.6.1-r1": 1545060945 + }, + "origin": "py-mccabe" + }, + "openssh-server-common": { + "versions": { + "7.9_p1-r5": 1556034596 + }, + "origin": "openssh" + }, + "truecrypt": { + "versions": { + "7.1a-r3": 1545300290 + }, + "origin": "truecrypt", + "dependencies": [ + "device-mapper", + "so:libc.musl-x86_64.so.1", + "so:libfuse.so.2", + "so:libgcc_s.so.1", + "so:libstdc++.so.6", + "so:libwx_baseu-2.8.so.0", + "so:libwx_gtk2u_adv-2.8.so.0", + "so:libwx_gtk2u_core-2.8.so.0" + ], + "provides": [ + "cmd:truecrypt" + ] + }, + "iptables-dev": { + "versions": { + "1.6.2-r1": 1545062496 + }, + "origin": "iptables", + "dependencies": [ + "linux-headers", + "iptables=1.6.2-r1", + "pkgconfig" + ], + "provides": [ + "pc:libip4tc=1.6.2", + "pc:libip6tc=1.6.2", + "pc:libipq=1.6.2", + "pc:libiptc=1.6.2", + "pc:xtables=1.6.2" + ] + }, + "dtc": { + "versions": { + "1.4.7-r0": 1545300716 + }, + "origin": "dtc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:convert-dtsv0", + "cmd:dtc", + "cmd:dtdiff", + "cmd:fdtdump", + "cmd:fdtget", + "cmd:fdtoverlay", + "cmd:fdtput" + ] + }, + "atk-doc": { + "versions": { + "2.30.0-r0": 1542823743 + }, + "origin": "atk" + }, + "xf86-video-nv-doc": { + "versions": { + "2.1.21-r3": 1545300603 + }, + "origin": "xf86-video-nv" + }, + "dconf-doc": { + "versions": { + "0.26.0-r1": 1545302104 + }, + "origin": "dconf" + }, + "xcb-util-dev": { + "versions": { + "0.4.0-r1": 1542822846 + }, + "origin": "xcb-util", + "dependencies": [ + "libxcb-dev", + "util-macros", + "pkgconfig", + "xcb-util=0.4.0-r1" + ], + "provides": [ + "pc:xcb-atom=0.4.0", + "pc:xcb-aux=0.4.0", + "pc:xcb-event=0.4.0", + "pc:xcb-util=0.4.0" + ] + }, + "lua-augeas": { + "versions": { + "0.1.2-r4": 1543246061 + }, + "origin": "lua-augeas" + }, + "rsyslog-mmanon": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "libepoxy-dev": { + "versions": { + "1.5.3-r0": 1545838421 + }, + "origin": "libepoxy", + "dependencies": [ + "libx11-dev", + "mesa-dev", + "libepoxy=1.5.3-r0", + "pc:egl", + "pc:gl", + "pkgconfig" + ], + "provides": [ + "pc:epoxy=1.5.3" + ] + }, + "nagios-plugins-radius": { + "versions": { + "2.2.1-r6": 1543933911 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libfreeradius-client.so.2" + ] + }, + "perl-file-slurp-tiny-doc": { + "versions": { + "0.004-r0": 1545213664 + }, + "origin": "perl-file-slurp-tiny" + }, + "py-gamin": { + "versions": { + "0.1.10-r10": 1543928476 + }, + "origin": "gamin", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgamin-1.so.0" + ] + }, + "postgresql-bdr-extension": { + "versions": { + "1.0.2-r4": 1543223174 + }, + "origin": "postgresql-bdr-extension", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:bdr_dump", + "cmd:bdr_init_copy", + "cmd:bdr_initial_load" + ] + }, + "x264-libs": { + "versions": { + "20180304-r1": 1545299803 + }, + "origin": "x264", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libx264.so.152=152" + ] + }, + "ferm-openrc": { + "versions": { + "2.4.1-r2": 1545292991 + }, + "origin": "ferm", + "dependencies": [ + "perl", + "iptables" + ] + }, + "gd-dev": { + "versions": { + "2.2.5-r3": 1554727880 + }, + "origin": "gd", + "dependencies": [ + "gd", + "perl", + "libgd=2.2.5-r3", + "pkgconfig" + ], + "provides": [ + "pc:gdlib=2.2.5", + "cmd:bdftogd", + "cmd:gdlib-config" + ] + }, + "apache2-ctl": { + "versions": { + "2.4.39-r0": 1554306834 + }, + "origin": "apache2", + "dependencies": [ + "lynx" + ], + "provides": [ + "cmd:apachectl" + ] + }, + "xf86-video-intel-doc": { + "versions": { + "2.99.917_git20170325-r4": 1545665362 + }, + "origin": "xf86-video-intel" + }, + "dbus-glib-dev": { + "versions": { + "0.108-r1": 1542824780 + }, + "origin": "dbus-glib", + "dependencies": [ + "dbus-glib=0.108-r1", + "pc:dbus-1", + "pc:glib-2.0", + "pc:gobject-2.0", + "pkgconfig" + ], + "provides": [ + "pc:dbus-glib-1=0.108" + ] + }, + "py2-more-itertools": { + "versions": { + "4.1.0-r0": 1542824885 + }, + "origin": "py-more-itertools", + "dependencies": [ + "python2" + ] + }, + "curl-dbg": { + "versions": { + "7.64.0-r1": 1551205305 + }, + "origin": "curl", + "dependencies": [ + "ca-certificates" + ] + }, + "python3-tests": { + "versions": { + "3.6.8-r2": 1554747638 + }, + "origin": "python3" + }, + "perl-regexp-common-doc": { + "versions": { + "2017060201-r0": 1543924763 + }, + "origin": "perl-regexp-common" + }, + "acf-lib-lua5.2": { + "versions": { + "0.10.1-r0": 1542883783 + }, + "origin": "acf-lib" + }, + "perl-html-scrubber-doc": { + "versions": { + "0.17-r0": 1545292678 + }, + "origin": "perl-html-scrubber" + }, + "gettext-dev": { + "versions": { + "0.19.8.1-r4": 1542304412 + }, + "origin": "gettext", + "dependencies": [ + "gettext-asprintf=0.19.8.1-r4", + "gettext-libs=0.19.8.1-r4", + "gettext=0.19.8.1-r4", + "libintl=0.19.8.1-r4" + ] + }, + "kamailio-snmpstats": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libnetsnmp.so.35", + "so:libnetsnmpagent.so.35" + ] + }, + "procps-doc": { + "versions": { + "3.3.15-r0": 1543934061 + }, + "origin": "procps" + }, + "gengetopt-doc": { + "versions": { + "2.22.6-r2": 1545062092 + }, + "origin": "gengetopt" + }, + "ttf-dejavu": { + "versions": { + "2.37-r1": 1545292697 + }, + "origin": "ttf-dejavu", + "dependencies": [ + "fontconfig", + "encodings", + "mkfontdir", + "mkfontscale" + ] + }, + "librelp": { + "versions": { + "1.3.0-r0": 1548636902 + }, + "origin": "librelp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgnutls.so.30" + ], + "provides": [ + "so:librelp.so.0=0.4.0" + ] + }, + "liblockfile": { + "versions": { + "1.14-r0": 1545075168 + }, + "origin": "liblockfile", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:dotlockfile" + ] + }, + "irqbalance-doc": { + "versions": { + "1.5.0-r0": 1545837194 + }, + "origin": "irqbalance" + }, + "libdc1394": { + "versions": { + "2.2.5-r1": 1545249933 + }, + "origin": "libdc1394", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libraw1394.so.11", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libdc1394.so.22=22.2.1", + "cmd:dc1394_reset_bus" + ] + }, + "libxaw-dev": { + "versions": { + "1.0.13-r2": 1542894088 + }, + "origin": "libxaw", + "dependencies": [ + "libxaw=1.0.13-r2", + "pc:x11", + "pc:xext", + "pc:xmu", + "pc:xpm", + "pc:xproto", + "pc:xt", + "pkgconfig" + ], + "provides": [ + "pc:xaw7=1.0.13" + ] + }, + "libart-lgpl": { + "versions": { + "2.3.21-r5": 1542924747 + }, + "origin": "libart-lgpl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libart_lgpl_2.so.2=2.3.21" + ] + }, + "pm-utils": { + "versions": { + "1.4.1-r1": 1543935621 + }, + "origin": "pm-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:on_ac_power", + "cmd:pm-is-supported", + "cmd:pm-powersave" + ] + }, + "py2-monotonic": { + "versions": { + "1.5-r0": 1544000466 + }, + "origin": "py2-monotonic", + "dependencies": [ + "python2" + ] + }, + "xf86-video-apm-doc": { + "versions": { + "1.2.5-r11": 1545062639 + }, + "origin": "xf86-video-apm" + }, + "libdvdcss-doc": { + "versions": { + "1.4.2-r0": 1545838429 + }, + "origin": "libdvdcss" + }, + "recordmydesktop-doc": { + "versions": { + "0.3.8.1-r2": 1545302038 + }, + "origin": "recordmydesktop" + }, + "ack-doc": { + "versions": { + "2.24-r0": 1545224031 + }, + "origin": "ack" + }, + "libxi-doc": { + "versions": { + "1.7.9-r2": 1543228126 + }, + "origin": "libxi" + }, + "py3-gpgme": { + "versions": { + "1.12.0-r3": 1543932322 + }, + "origin": "gpgme", + "dependencies": [ + "gnupg", + "gpgme", + "python3", + "so:libc.musl-x86_64.so.1", + "so:libgpgme.so.11", + "so:libpython3.6m.so.1.0" + ] + }, + "py-flask": { + "versions": { + "1.0.2-r1": 1546978285 + }, + "origin": "py-flask", + "dependencies": [ + "py-click", + "py-itsdangerous", + "py-jinja2", + "py-werkzeug" + ] + }, + "lua-openrc": { + "versions": { + "0.2-r3": 1545066957 + }, + "origin": "lua-openrc" + }, + "perl-module-build": { + "versions": { + "0.4224-r0": 1542304668 + }, + "origin": "perl-module-build", + "provides": [ + "cmd:config_data" + ] + }, + "fcgi-dev": { + "versions": { + "2.4.0-r8": 1543238825 + }, + "origin": "fcgi", + "dependencies": [ + "fcgi++=2.4.0-r8", + "fcgi=2.4.0-r8" + ] + }, + "unzip-doc": { + "versions": { + "6.0-r4": 1542301513 + }, + "origin": "unzip" + }, + "perl-text-vfile-asdata-doc": { + "versions": { + "0.08-r0": 1545254262 + }, + "origin": "perl-text-vfile-asdata" + }, + "perl-image-exiftool-doc": { + "versions": { + "11.23-r0": 1545835014 + }, + "origin": "perl-image-exiftool" + }, + "dhcp": { + "versions": { + "4.4.1-r1": 1543928454 + }, + "origin": "dhcp", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "cmd:dhcpd", + "cmd:omshell" + ] + }, + "xcb-util-image": { + "versions": { + "0.4.0-r1": 1543927901 + }, + "origin": "xcb-util-image", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-shm.so.0", + "so:libxcb-util.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "so:libxcb-image.so.0=0.0.0" + ] + }, + "cgdb": { + "versions": { + "0.7.0-r2": 1545214140 + }, + "origin": "cgdb", + "dependencies": [ + "gdb", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libncursesw.so.6", + "so:libreadline.so.7", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:cgdb" + ] + }, + "perl-test-without-module-doc": { + "versions": { + "0.20-r0": 1542893308 + }, + "origin": "perl-test-without-module" + }, + "perl-crypt-x509-doc": { + "versions": { + "0.51-r0": 1545207011 + }, + "origin": "perl-crypt-x509" + }, + "perl-net-openssh-doc": { + "versions": { + "0.78-r0": 1545995475 + }, + "origin": "perl-net-openssh" + }, + "libgudev": { + "versions": { + "230-r2": 1542845393 + }, + "origin": "libgudev", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libudev.so.1" + ], + "provides": [ + "so:libgudev-1.0.so.0=0.2.0" + ] + }, + "vsftpd-doc": { + "versions": { + "3.0.3-r6": 1544000084 + }, + "origin": "vsftpd" + }, + "imake-doc": { + "versions": { + "1.0.7-r2": 1543932076 + }, + "origin": "imake" + }, + "tzdata-doc": { + "versions": { + "2019a-r0": 1555329220 + }, + "origin": "tzdata" + }, + "vte-doc": { + "versions": { + "0.28.2-r13": 1543927666 + }, + "origin": "vte" + }, + "libfastjson-dbg": { + "versions": { + "0.99.8-r1": 1545060651 + }, + "origin": "libfastjson" + }, + "xf86-video-i128": { + "versions": { + "1.3.6-r11": 1545069417 + }, + "origin": "xf86-video-i128", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "arpwatch-ethercodes": { + "versions": { + "2.1a15-r16": 1544000303 + }, + "origin": "arpwatch" + }, + "wxgtk2.8": { + "versions": { + "2.8.12.1-r4": 1545075288 + }, + "origin": "wxgtk2.8", + "dependencies": [ + "so:libSDL-1.2.so.0", + "so:libSM.so.6", + "so:libXinerama.so.1", + "so:libXxf86vm.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libjpeg.so.8", + "so:libpango-1.0.so.0", + "so:libpng16.so.16", + "so:libstdc++.so.6", + "so:libtiff.so.5", + "so:libwx_baseu-2.8.so.0", + "so:libwx_baseu_xml-2.8.so.0" + ], + "provides": [ + "so:libwx_gtk2u_adv-2.8.so.0=0.8.0", + "so:libwx_gtk2u_aui-2.8.so.0=0.8.0", + "so:libwx_gtk2u_core-2.8.so.0=0.8.0", + "so:libwx_gtk2u_html-2.8.so.0=0.8.0", + "so:libwx_gtk2u_qa-2.8.so.0=0.8.0", + "so:libwx_gtk2u_richtext-2.8.so.0=0.8.0", + "so:libwx_gtk2u_xrc-2.8.so.0=0.8.0" + ] + }, + "clang-doc": { + "versions": { + "5.0.2-r0": 1546873920 + }, + "origin": "clang" + }, + "execline-dev": { + "versions": { + "2.5.0.1-r0": 1543221686 + }, + "origin": "execline", + "dependencies": [ + "execline=2.5.0.1-r0" + ] + }, + "freeradius-client": { + "versions": { + "1.1.7-r2": 1543922359 + }, + "origin": "freeradius-client", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnettle.so.6" + ], + "provides": [ + "so:libfreeradius-client.so.2=2.0.0", + "cmd:login.radius", + "cmd:radacct", + "cmd:radembedded", + "cmd:radexample", + "cmd:radiusclient", + "cmd:radlogin", + "cmd:radstatus" + ] + }, + "monit-doc": { + "versions": { + "5.25.2-r0": 1545858142 + }, + "origin": "monit" + }, + "libgcab": { + "versions": { + "0.7-r2": 1543249784 + }, + "origin": "libgcab", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libz.so.1" + ], + "provides": [ + "so:libgcab-1.0.so.0=0.0.0", + "cmd:gcab" + ] + }, + "acf-iptables": { + "versions": { + "0.7.1-r2": 1545222974 + }, + "origin": "acf-iptables", + "dependencies": [ + "acf-core", + "iptables" + ] + }, + "perl-http-cookies-doc": { + "versions": { + "6.04-r0": 1542821808 + }, + "origin": "perl-http-cookies" + }, + "usbutils": { + "versions": { + "010-r0": 1545075126 + }, + "origin": "usbutils", + "dependencies": [ + "hwdata-usb", + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "cmd:lsusb", + "cmd:lsusb.py", + "cmd:usb-devices", + "cmd:usbhid-dump" + ] + }, + "perl-dbix-searchbuilder": { + "versions": { + "1.67-r1": 1544798721 + }, + "origin": "perl-dbix-searchbuilder", + "dependencies": [ + "perl-class-returnvalue", + "perl-dbi", + "perl-cache-simple-timedexpiry", + "perl-class-accessor", + "perl-clone", + "perl-want", + "perl-dbix-dbschema" + ] + }, + "herbstluftwm-bash-completion": { + "versions": { + "0.7.1-r0": 1545300035 + }, + "origin": "herbstluftwm" + }, + "font-jis-misc": { + "versions": { + "1.0.3-r0": 1544000787 + }, + "origin": "font-jis-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "qemu-system-ppc64": { + "versions": { + "3.1.0-r3": 1551107305 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-ppc64" + ] + }, + "pkgconf": { + "versions": { + "1.6.0-r0": 1547496958 + }, + "origin": "pkgconf", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "pkgconfig=1", + "so:libpkgconf.so.3=3.0.0", + "cmd:pkg-config", + "cmd:pkgconf" + ] + }, + "perl-html-format": { + "versions": { + "2.12-r0": 1545075958 + }, + "origin": "perl-html-format", + "dependencies": [ + "perl-html-tree", + "perl-font-afm" + ] + }, + "rsyslog-mysql": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ], + "provides": [ + "rsyslog-ommysql=8.40.0-r3" + ] + }, + "openssh": { + "versions": { + "7.9_p1-r5": 1556034600 + }, + "origin": "openssh", + "dependencies": [ + "openssh-client", + "openssh-sftp-server", + "openssh-server", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "fossil": { + "versions": { + "2.7-r0": 1545746244 + }, + "origin": "fossil", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libsqlite3.so.0", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:fossil" + ] + }, + "spice-protocol": { + "versions": { + "0.12.14-r0": 1543935749 + }, + "origin": "spice-protocol", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:spice-protocol=0.12.14" + ] + }, + "libcrypto1.1": { + "versions": { + "1.1.1b-r1": 1552660176 + }, + "origin": "openssl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcrypto.so.1.1=1.1" + ] + }, + "open-isns": { + "versions": { + "0.97-r4": 1542883212 + }, + "origin": "open-isns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libisns.so.0" + ], + "provides": [ + "cmd:isnsadm", + "cmd:isnsd", + "cmd:isnsdd" + ] + }, + "ncurses-doc": { + "versions": { + "6.1_p20190105-r0": 1546948257 + }, + "origin": "ncurses" + }, + "rsyslog-zmq": { + "versions": { + "8.40.0-r3": 1548686792 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1", + "so:libczmq.so.4" + ], + "provides": [ + "rsyslog-imczmq=8.40.0-r3", + "rsyslog-omczmq=8.40.0-r3" + ] + }, + "bluez-obexd": { + "versions": { + "5.50-r0": 1545822206 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libglib-2.0.so.0", + "so:libicalvcal.so.2" + ] + }, + "tiff-dev": { + "versions": { + "4.0.10-r0": 1543921905 + }, + "origin": "tiff", + "dependencies": [ + "zlib-dev", + "libjpeg-turbo-dev", + "libtiffxx=4.0.10-r0", + "pkgconfig", + "tiff=4.0.10-r0" + ], + "provides": [ + "pc:libtiff-4=4.0.10" + ] + }, + "acf-fetchmail": { + "versions": { + "0.9.0-r2": 1545257160 + }, + "origin": "acf-fetchmail", + "dependencies": [ + "acf-core", + "fetchmail" + ] + }, + "lua5.3-doc": { + "versions": { + "5.3.5-r2": 1557162501 + }, + "origin": "lua5.3" + }, + "perl-datetime-locale-doc": { + "versions": { + "1.22-r0": 1543238992 + }, + "origin": "perl-datetime-locale" + }, + "libsodium": { + "versions": { + "1.0.16-r0": 1544000836 + }, + "origin": "libsodium", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libsodium.so.23=23.1.0" + ] + }, + "perl-class-singleton-doc": { + "versions": { + "1.5-r0": 1542972927 + }, + "origin": "perl-class-singleton" + }, + "varnish-dbg": { + "versions": { + "6.1.1-r0": 1545300750 + }, + "origin": "varnish", + "dependencies": [ + "gcc", + "libc-dev", + "libgcc" + ] + }, + "qjson-dev": { + "versions": { + "0.9.0-r0": 1545075111 + }, + "origin": "qjson", + "dependencies": [ + "pc:QtCore", + "pkgconfig", + "qjson=0.9.0-r0" + ], + "provides": [ + "pc:QJson=0.9.0" + ] + }, + "darkstat": { + "versions": { + "3.0.719-r1": 1543223080 + }, + "origin": "darkstat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcap.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:darkstat" + ] + }, + "lua5.2-yaml": { + "versions": { + "1.1.2-r1": 1544000069 + }, + "origin": "lua-yaml", + "dependencies": [ + "lua5.2", + "lua5.2-lub", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-module-build-doc": { + "versions": { + "0.4224-r0": 1542304665 + }, + "origin": "perl-module-build" + }, + "py2-asn1-modules": { + "versions": { + "0.2.2-r0": 1543928877 + }, + "origin": "py-asn1-modules", + "dependencies": [ + "py2-openssl", + "py2-asn1" + ] + }, + "sysstat-doc": { + "versions": { + "11.6.0-r0": 1543922333 + }, + "origin": "sysstat" + }, + "perl-package-deprecationmanager": { + "versions": { + "0.17-r0": 1542845766 + }, + "origin": "perl-package-deprecationmanager", + "dependencies": [ + "perl-list-moreutils", + "perl-sub-install", + "perl-params-util", + "perl-test-fatal", + "perl-test-requires", + "perl-package-stash", + "perl-test-warnings", + "perl-sub-name" + ] + }, + "perl-async-mergepoint-doc": { + "versions": { + "0.04-r0": 1545075379 + }, + "origin": "perl-async-mergepoint" + }, + "boost-dev": { + "versions": { + "1.67.0-r2": 1542823626 + }, + "origin": "boost", + "dependencies": [ + "linux-headers", + "boost-atomic=1.67.0-r2", + "boost-chrono=1.67.0-r2", + "boost-container=1.67.0-r2", + "boost-context=1.67.0-r2", + "boost-contract=1.67.0-r2", + "boost-coroutine=1.67.0-r2", + "boost-date_time=1.67.0-r2", + "boost-fiber=1.67.0-r2", + "boost-filesystem=1.67.0-r2", + "boost-graph=1.67.0-r2", + "boost-iostreams=1.67.0-r2", + "boost-math=1.67.0-r2", + "boost-prg_exec_monitor=1.67.0-r2", + "boost-program_options=1.67.0-r2", + "boost-python3=1.67.0-r2", + "boost-python=1.67.0-r2", + "boost-random=1.67.0-r2", + "boost-regex=1.67.0-r2", + "boost-serialization=1.67.0-r2", + "boost-signals=1.67.0-r2", + "boost-system=1.67.0-r2", + "boost-thread=1.67.0-r2", + "boost-unit_test_framework=1.67.0-r2", + "boost-wave=1.67.0-r2", + "boost-wserialization=1.67.0-r2", + "boost=1.67.0-r2" + ] + }, + "darkhttpd-openrc": { + "versions": { + "1.12-r2": 1543223261 + }, + "origin": "darkhttpd" + }, + "czmq-dev": { + "versions": { + "4.1.1-r0": 1548683467 + }, + "origin": "czmq", + "dependencies": [ + "util-linux-dev", + "zeromq-dev", + "czmq=4.1.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libczmq=4.1.1" + ] + }, + "qemu-system-xtensa": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-xtensa" + ] + }, + "nasm": { + "versions": { + "2.13.03-r0": 1542824085 + }, + "origin": "nasm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:nasm", + "cmd:ndisasm" + ] + }, + "rrdtool-cgi": { + "versions": { + "1.7.0-r0": 1542924806 + }, + "origin": "rrdtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:librrd.so.8" + ], + "provides": [ + "cmd:rrdcgi" + ] + }, + "openldap-doc": { + "versions": { + "2.4.47-r2": 1546016476 + }, + "origin": "openldap" + }, + "lxc-bash-completion": { + "versions": { + "3.1.0-r1": 1549966161 + }, + "origin": "lxc" + }, + "py-country": { + "versions": { + "18.12.8-r0": 1548109642 + }, + "origin": "py-country" + }, + "pmacct-doc": { + "versions": { + "1.7.0-r0": 1545116554 + }, + "origin": "pmacct" + }, + "adwaita-icon-theme": { + "versions": { + "3.31.1-r0": 1545820343 + }, + "origin": "adwaita-icon-theme" + }, + "dovecot-fts-lucene": { + "versions": { + "2.3.6-r0": 1557134099 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libclucene-core.so.1", + "so:libclucene-shared.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:lib21_fts_lucene_plugin.so=0" + ] + }, + "fuse-common": { + "versions": { + "3.2.6-r1": 1548943636 + }, + "origin": "fuse3" + }, + "bonding": { + "versions": { + "2.6-r3": 1545062232 + }, + "origin": "bonding" + }, + "py-gst0.10": { + "versions": { + "0.10.22-r0": 1544000781 + }, + "origin": "py-gst0.10", + "dependencies": [ + "py-gtk", + "py-gobject", + "pc:gstreamer-0.10", + "pc:pygobject-2.0", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstaudio-0.10.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstcontroller-0.10.so.0", + "so:libgstdataprotocol-0.10.so.0", + "so:libgstinterfaces-0.10.so.0", + "so:libgstnet-0.10.so.0", + "so:libgstpbutils-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libgsttag-0.10.so.0", + "so:libgstvideo-0.10.so.0" + ], + "provides": [ + "pc:gst-python-0.10=0.10.22" + ] + }, + "eudev-netifnames": { + "versions": { + "3.2.7-r0": 1542845385 + }, + "origin": "eudev", + "dependencies": [ + "udev-init-scripts" + ] + }, + "py2-babel": { + "versions": { + "2.6.0-r0": 1542824977 + }, + "origin": "py-babel", + "dependencies": [ + "py2-tz", + "python2" + ], + "provides": [ + "cmd:pybabel" + ] + }, + "xf86-video-modesetting": { + "versions": { + "0.9.0-r6": 1544001371 + }, + "origin": "xf86-video-modesetting", + "dependencies": [ + "xorg-server" + ] + }, + "bubblewrap-doc": { + "versions": { + "0.3.1-r0": 1546247686 + }, + "origin": "bubblewrap" + }, + "strace": { + "versions": { + "4.24-r0": 1543928279 + }, + "origin": "strace", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:strace", + "cmd:strace-graph", + "cmd:strace-log-merge" + ] + }, + "openvswitch-test": { + "versions": { + "2.10.1-r0": 1544000344 + }, + "origin": "openvswitch", + "dependencies": [ + "py2-openvswitch=2.10.1-r0", + "py2-twisted", + "so:libc.musl-x86_64.so.1", + "so:libcap-ng.so.0", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:ovs-l3ping", + "cmd:ovs-pcap", + "cmd:ovs-tcpdump", + "cmd:ovs-tcpundump", + "cmd:ovs-test", + "cmd:ovs-testcontroller", + "cmd:ovs-vlan-test" + ] + }, + "py2-gunicorn": { + "versions": { + "19.7.1-r1": 1545214067 + }, + "origin": "py-gunicorn", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:gunicorn", + "cmd:gunicorn_paster" + ] + }, + "libmaxminddb": { + "versions": { + "1.3.2-r0": 1543226667 + }, + "origin": "libmaxminddb", + "dependencies": [ + "curl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmaxminddb.so.0=0.0.7", + "cmd:mmdblookup" + ] + }, + "docbook-xml": { + "versions": { + "4.5-r6": 1542304586 + }, + "origin": "docbook-xml", + "dependencies": [ + "libxml2-utils", + "/bin/sh" + ] + }, + "perl-digest-sha1": { + "versions": { + "2.13-r9": 1542924827 + }, + "origin": "perl-digest-sha1", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-module-build-tiny-doc": { + "versions": { + "0.039-r0": 1543238863 + }, + "origin": "perl-module-build-tiny" + }, + "rsyslog-pmaixforwardedfrom": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "clutter-dev": { + "versions": { + "1.26.2-r2": 1545293146 + }, + "origin": "clutter", + "dependencies": [ + "gdk-pixbuf-dev", + "json-glib-dev", + "atk-dev", + "pango-dev", + "mesa-dev", + "libxcomposite-dev", + "libxi-dev", + "cairo-dev", + "cogl-dev", + "clutter=1.26.2-r2", + "pc:atk", + "pc:atk>=2.5.3", + "pc:cairo-gobject>=1.14.0", + "pc:cogl-1.0>=1.21.2", + "pc:cogl-pango-1.0", + "pc:cogl-path-1.0", + "pc:gio-2.0>=2.44.0", + "pc:json-glib-1.0>=0.12.0", + "pc:pangocairo>=1.30", + "pc:pangoft2", + "pc:x11", + "pc:xcomposite>=0.4", + "pc:xdamage", + "pc:xext", + "pc:xi", + "pkgconfig" + ], + "provides": [ + "pc:cally-1.0=1.26.2", + "pc:clutter-1.0=1.26.2", + "pc:clutter-cogl-1.0=1.26.2", + "pc:clutter-egl-1.0=1.26.2", + "pc:clutter-glx-1.0=1.26.2", + "pc:clutter-x11-1.0=1.26.2" + ] + }, + "linux-firmware-rtl8192e": { + "versions": { + "20190322-r0": 1554980648 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "qemu-i386": { + "versions": { + "3.1.0-r3": 1551107300 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-i386" + ] + }, + "lua5.1-md5": { + "versions": { + "1.2-r3": 1542883857 + }, + "origin": "lua-md5", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libgit2-dev": { + "versions": { + "0.27.7-r0": 1545838464 + }, + "origin": "libgit2", + "dependencies": [ + "curl-dev", + "libssh2-dev", + "libgit2=0.27.7-r0", + "pc:openssl", + "pc:zlib", + "pkgconfig" + ], + "provides": [ + "pc:libgit2=0.27.7" + ] + }, + "rxvt-unicode": { + "versions": { + "9.22-r4": 1543254027 + }, + "origin": "rxvt-unicode", + "dependencies": [ + "rxvt-unicode-terminfo", + "so:libX11.so.6", + "so:libXft.so.2", + "so:libXrender.so.1", + "so:libc.musl-x86_64.so.1", + "so:libfontconfig.so.1", + "so:libgcc_s.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libperl.so", + "so:libstartup-notification-1.so.0" + ], + "provides": [ + "cmd:urxvt", + "cmd:urxvtc", + "cmd:urxvtd" + ] + }, + "kamailio-geoip2": { + "versions": { + "5.2.0-r1": 1546423172 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libmaxminddb.so.0" + ] + }, + "py2-yaml": { + "versions": { + "4.1-r0": 1544798588 + }, + "origin": "py-yaml" + }, + "lua5.2-sircbot": { + "versions": { + "0.4-r2": 1543924833 + }, + "origin": "sircbot", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py3-pillow": { + "versions": { + "5.4.1-r0": 1548112615 + }, + "origin": "py-pillow", + "dependencies": [ + "py3-olefile", + "so:libc.musl-x86_64.so.1", + "so:libfreetype.so.6", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libopenjp2.so.7", + "so:libpython3.6m.so.1.0", + "so:libtiff.so.5", + "so:libwebp.so.7", + "so:libwebpdemux.so.2", + "so:libwebpmux.so.3", + "so:libz.so.1" + ] + }, + "libnl3-dev": { + "versions": { + "3.4.0-r0": 1542965891 + }, + "origin": "libnl3", + "dependencies": [ + "libnl3-cli=3.4.0-r0", + "libnl3=3.4.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libnl-3.0=3.4.0", + "pc:libnl-cli-3.0=3.4.0", + "pc:libnl-genl-3.0=3.4.0", + "pc:libnl-idiag-3.0=3.4.0", + "pc:libnl-nf-3.0=3.4.0", + "pc:libnl-route-3.0=3.4.0", + "pc:libnl-xfrm-3.0=3.4.0" + ] + }, + "perl-io-socket-ssl": { + "versions": { + "2.060-r0": 1545164014 + }, + "origin": "perl-io-socket-ssl", + "dependencies": [ + "ca-certificates", + "perl-net-libidn", + "perl-net-ssleay" + ] + }, + "perl-sub-name-doc": { + "versions": { + "0.21-r1": 1542845757 + }, + "origin": "perl-sub-name" + }, + "sfdisk": { + "versions": { + "2.33-r0": 1545307437 + }, + "origin": "util-linux", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfdisk.so.1", + "so:libncursesw.so.6", + "so:libsmartcols.so.1" + ], + "provides": [ + "cmd:sfdisk" + ] + }, + "mosquitto-libs": { + "versions": { + "1.5.6-r0": 1549970968 + }, + "origin": "mosquitto", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcares.so.2", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libmosquitto.so.1=1" + ] + }, + "libjpeg-turbo-dev": { + "versions": { + "1.5.3-r4": 1547027583 + }, + "origin": "libjpeg-turbo", + "dependencies": [ + "libjpeg-turbo=1.5.3-r4", + "pkgconfig" + ], + "provides": [ + "pc:libjpeg=1.5.3", + "pc:libturbojpeg=1.5.3" + ] + }, + "gdbm": { + "versions": { + "1.13-r1": 1542303060 + }, + "origin": "gdbm", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgdbm.so.4=4.0.0", + "so:libgdbm_compat.so.4=4.0.0", + "cmd:gdbm_dump", + "cmd:gdbm_load", + "cmd:gdbmtool" + ] + }, + "fakeroot-doc": { + "versions": { + "1.23-r0": 1542304762 + }, + "origin": "fakeroot" + }, + "lua5.3-cqueues": { + "versions": { + "20171014-r3": 1546520956 + }, + "origin": "lua-cqueues", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "cmocka-dev": { + "versions": { + "1.1.1-r1": 1543933265 + }, + "origin": "cmocka", + "dependencies": [ + "cmocka=1.1.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:cmocka=1.1.1" + ] + }, + "nginx-mod-http-cache-purge": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1" + ] + }, + "rgb-doc": { + "versions": { + "1.0.6-r2": 1545075161 + }, + "origin": "rgb" + }, + "qpdf": { + "versions": { + "8.3.0-r0": 1547117323 + }, + "origin": "qpdf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libqpdf.so.21", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:qpdf", + "cmd:zlib-flate" + ] + }, + "p11-kit-server": { + "versions": { + "0.23.14-r0": 1544791899 + }, + "origin": "p11-kit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libp11-kit.so.0" + ] + }, + "libproc": { + "versions": { + "3.3.15-r0": 1543934063 + }, + "origin": "procps", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libprocps.so.7=7.1.0" + ] + }, + "perl-math-round": { + "versions": { + "0.07-r0": 1545163054 + }, + "origin": "perl-math-round", + "dependencies": [ + "perl" + ] + }, + "binutils-doc": { + "versions": { + "2.31.1-r2": 1546441178 + }, + "origin": "binutils" + }, + "xbacklight": { + "versions": { + "1.2.2-r0": 1544792468 + }, + "origin": "xbacklight", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxcb-randr.so.0", + "so:libxcb-util.so.1", + "so:libxcb.so.1" + ], + "provides": [ + "cmd:xbacklight" + ] + }, + "gettext-lang": { + "versions": { + "0.19.8.1-r4": 1542304412 + }, + "origin": "gettext" + }, + "ldns": { + "versions": { + "1.7.0-r2": 1545117118 + }, + "origin": "ldns", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ], + "provides": [ + "so:libldns.so.2=2.0.0" + ] + }, + "lua5.3-dbi-postgresql": { + "versions": { + "0.6-r3": 1545214225 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "openldap-back-dnssrv": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap_r-2.4.so.2" + ] + }, + "clamav-libs": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav", + "dependencies": [ + "clamav-scanner", + "clamav-daemon", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libpcre.so.1", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libclamav.so.7=7.1.2", + "so:libclammspack.so.0=0.1.0", + "so:libclamunrar.so.7=7.1.2", + "so:libclamunrar_iface.so.7=7.1.2" + ] + }, + "freeradius-mysql": { + "versions": { + "3.0.17-r5": 1556201988 + }, + "origin": "freeradius", + "dependencies": [ + "freeradius-sql=3.0.17-r5", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ], + "provides": [ + "freeradius3-mysql=3.0.17-r5", + "so:rlm_sql_mysql.so=0" + ] + }, + "clang-analyzer": { + "versions": { + "5.0.2-r0": 1546873920 + }, + "origin": "clang", + "dependencies": [ + "clang=5.0.2-r0", + "perl", + "python2" + ], + "provides": [ + "cmd:scan-build", + "cmd:scan-view" + ] + }, + "libevdev-dev": { + "versions": { + "1.6.0-r0": 1545838440 + }, + "origin": "libevdev", + "dependencies": [ + "libevdev=1.6.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libevdev=1.6.0" + ] + }, + "perl-dbix-searchbuilder-doc": { + "versions": { + "1.67-r1": 1544798719 + }, + "origin": "perl-dbix-searchbuilder" + }, + "opus-doc": { + "versions": { + "1.3-r0": 1545069004 + }, + "origin": "opus" + }, + "gtkmm": { + "versions": { + "2.24.5-r0": 1543925602 + }, + "origin": "gtkmm", + "dependencies": [ + "so:libatkmm-1.6.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcairomm-1.0.so.1", + "so:libgcc_s.so.1", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgiomm-2.4.so.1", + "so:libglib-2.0.so.0", + "so:libglibmm-2.4.so.1", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libpangomm-1.4.so.1", + "so:libsigc-2.0.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libgdkmm-2.4.so.1=1.1.0", + "so:libgtkmm-2.4.so.1=1.1.0" + ] + }, + "perl-module-util": { + "versions": { + "1.09-r0": 1545163062 + }, + "origin": "perl-module-util", + "provides": [ + "cmd:pm_which" + ] + }, + "lua5.3-lyaml": { + "versions": { + "6.2-r3": 1545076422 + }, + "origin": "lua-lyaml", + "dependencies": [ + "lua5.3", + "lua5.3-stdlib-normalize", + "so:libc.musl-x86_64.so.1", + "so:libyaml-0.so.2" + ] + }, + "libxcursor-doc": { + "versions": { + "1.1.15-r1": 1543925380 + }, + "origin": "libxcursor" + }, + "vanessa_logger-doc": { + "versions": { + "0.0.10-r0": 1542893598 + }, + "origin": "vanessa_logger" + }, + "perl-convert-uulib": { + "versions": { + "1.5-r2": 1544001424 + }, + "origin": "perl-convert-uulib", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ] + }, + "openbox-kde": { + "versions": { + "3.6.1-r2": 1545207328 + }, + "origin": "openbox", + "provides": [ + "cmd:openbox-kde-session" + ] + }, + "dmvpn-crl-dp": { + "versions": { + "1.0.2-r0": 1553425622 + }, + "origin": "dmvpn", + "dependencies": [ + "lighttpd" + ], + "provides": [ + "cmd:dmvpn-crl-update" + ] + }, + "ppp-passwordfd": { + "versions": { + "2.4.7-r6": 1543999023 + }, + "origin": "ppp", + "dependencies": [ + "ppp-daemon", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-class-accessor-chained": { + "versions": { + "0.01-r0": 1544000364 + }, + "origin": "perl-class-accessor-chained", + "dependencies": [ + "perl-class-accessor" + ] + }, + "yaml-dev": { + "versions": { + "0.2.1-r0": 1542822091 + }, + "origin": "yaml", + "dependencies": [ + "pkgconfig", + "yaml=0.2.1-r0" + ], + "provides": [ + "pc:yaml-0.1=0.2.1" + ] + }, + "perdition": { + "versions": { + "2.2-r0": 1545223336 + }, + "origin": "perdition", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libgdbm.so.4", + "so:libpopt.so.0", + "so:libssl.so.1.1", + "so:libvanessa_adt.so.1", + "so:libvanessa_logger.so.0", + "so:libvanessa_socket.so.2" + ], + "provides": [ + "so:libperditiondb_gdbm.so.0=0.0.0", + "cmd:makegdbm", + "cmd:perdition", + "cmd:perdition.imap4", + "cmd:perdition.imap4s", + "cmd:perdition.imaps", + "cmd:perdition.managesieve", + "cmd:perdition.pop3", + "cmd:perdition.pop3s" + ] + }, + "libbsd-doc": { + "versions": { + "0.8.6-r2": 1542822480 + }, + "origin": "libbsd" + }, + "kamailio-utils": { + "versions": { + "5.2.0-r1": 1546423170 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libsrdb1.so.1", + "so:libxml2.so.2" + ] + }, + "py-pbr": { + "versions": { + "5.1.1-r0": 1545235280 + }, + "origin": "py-pbr" + }, + "termrec": { + "versions": { + "0.17-r1": 1545249956 + }, + "origin": "termrec", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:liblzma.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libtty.so.0=0.0.0", + "cmd:proxyrec", + "cmd:termcat", + "cmd:termplay", + "cmd:termrec", + "cmd:termtime" + ] + }, + "lxc-templates-oci": { + "versions": { + "3.1.0-r1": 1549966161 + }, + "origin": "lxc", + "dependencies": [ + "bash", + "jq" + ] + }, + "cmocka": { + "versions": { + "1.1.1-r1": 1543933266 + }, + "origin": "cmocka", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libcmocka.so.0=0.4.1" + ] + }, + "wipe": { + "versions": { + "2.3.1-r0": 1543924496 + }, + "origin": "wipe", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:wipe" + ] + }, + "cgit-doc": { + "versions": { + "1.2.1-r1": 1544000809 + }, + "origin": "cgit" + }, + "gcc-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "perl-proc-wait3": { + "versions": { + "0.05-r1": 1543249818 + }, + "origin": "perl-proc-wait3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "cython-dev": { + "versions": { + "0.29.2-r0": 1545918551 + }, + "origin": "cython", + "dependencies": [ + "python2-dev", + "py-pgen", + "cython" + ] + }, + "libguess": { + "versions": { + "1.2-r0": 1543998753 + }, + "origin": "libguess", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libguess.so.1=1.0.0" + ] + }, + "e2fsprogs-extra": { + "versions": { + "1.44.5-r0": 1545745858 + }, + "origin": "e2fsprogs", + "dependencies": [ + "e2fsprogs", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libe2p.so.2", + "so:libext2fs.so.2", + "so:libss.so.2", + "so:libuuid.so.1" + ], + "provides": [ + "cmd:badblocks", + "cmd:chattr", + "cmd:debugfs", + "cmd:dumpe2fs", + "cmd:e2freefrag", + "cmd:e2image", + "cmd:e2label", + "cmd:e2mmpstatus", + "cmd:e2undo", + "cmd:e4crypt", + "cmd:e4defrag", + "cmd:filefrag", + "cmd:logsave", + "cmd:lsattr", + "cmd:mklost+found", + "cmd:resize2fs", + "cmd:tune2fs" + ] + }, + "kamailio-uuid": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libuuid.so.1" + ] + }, + "qt-private-dev": { + "versions": { + "4.8.7-r11": 1545075090 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates" + ] + }, + "py-backports_abc": { + "versions": { + "0.5-r0": 1545293194 + }, + "origin": "py-backports_abc" + }, + "libmcrypt-dev": { + "versions": { + "2.5.8-r7": 1545066949 + }, + "origin": "libmcrypt", + "dependencies": [ + "libmcrypt=2.5.8-r7" + ], + "provides": [ + "cmd:libmcrypt-config" + ] + }, + "udev-init-scripts": { + "versions": { + "32-r2": 1542845335 + }, + "origin": "udev-init-scripts", + "dependencies": [ + "/bin/sh" + ] + }, + "faac-doc": { + "versions": { + "1.28-r12": 1545165108 + }, + "origin": "faac" + }, + "perl-ldap": { + "versions": { + "0.65-r1": 1543081711 + }, + "origin": "perl-ldap", + "dependencies": [ + "perl-convert-asn1" + ], + "provides": [ + "perl-net-ldap" + ] + }, + "libdvbpsi": { + "versions": { + "1.3.2-r0": 1544792492 + }, + "origin": "libdvbpsi", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdvbpsi.so.10=10.0.0" + ] + }, + "nginx-mod-http-lua": { + "versions": { + "1.14.2-r1": 1557179821 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "nginx-mod-devel-kit", + "so:libc.musl-x86_64.so.1", + "so:libluajit-5.1.so.2" + ], + "provides": [ + "nginx-lua" + ] + }, + "farstream-dev": { + "versions": { + "0.2.8-r2": 1543928858 + }, + "origin": "farstream", + "dependencies": [ + "libnice-dev", + "gstreamer-dev", + "gst-plugins-base-dev", + "farstream=0.2.8-r2", + "pc:gstreamer-1.0", + "pc:gstreamer-base-1.0", + "pkgconfig" + ], + "provides": [ + "pc:farstream-0.2=0.2.8" + ] + }, + "ulogd-openrc": { + "versions": { + "2.0.7-r0": 1545300897 + }, + "origin": "ulogd" + }, + "open-lldp-doc": { + "versions": { + "0.9.46-r3": 1545062129 + }, + "origin": "open-lldp" + }, + "static-routing": { + "versions": { + "1.0-r0": 1545213722 + }, + "origin": "static-routing" + }, + "uwsgi-http": { + "versions": { + "2.0.17.1-r0": 1545062201 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "uwsgi-corerouter", + "so:libc.musl-x86_64.so.1" + ] + }, + "eggdrop-doc": { + "versions": { + "1.6.21-r2": 1545206945 + }, + "origin": "eggdrop" + }, + "py-libproxy": { + "versions": { + "0.4.15-r1": 1544001158 + }, + "origin": "libproxy" + }, + "perl-posix-strftime-compiler": { + "versions": { + "0.42-r0": 1544792384 + }, + "origin": "perl-posix-strftime-compiler", + "dependencies": [ + "tzdata" + ] + }, + "busybox-suid": { + "versions": { + "1.29.3-r10": 1548315956 + }, + "origin": "busybox", + "dependencies": [ + "busybox", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bbsuid" + ] + }, + "lighttpd-dbg": { + "versions": { + "1.4.52-r0": 1545207629 + }, + "origin": "lighttpd" + }, + "json-c": { + "versions": { + "0.13.1-r0": 1543923377 + }, + "origin": "json-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libjson-c.so.4=4.0.0" + ] + }, + "gnome-doc-utils": { + "versions": { + "0.20.10-r2": 1543254074 + }, + "origin": "gnome-doc-utils", + "dependencies": [ + "python2", + "docbook-xml", + "rarian", + "py2-libxml2", + "libxslt" + ], + "provides": [ + "cmd:gnome-doc-prepare", + "cmd:gnome-doc-tool", + "cmd:xml2po" + ] + }, + "postgresql-plpython2": { + "versions": { + "11.2-r0": 1554274176 + }, + "origin": "postgresql", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "clucene-dev": { + "versions": { + "2.3.3.4-r5": 1543999111 + }, + "origin": "clucene", + "dependencies": [ + "zlib-dev", + "boost-dev", + "clucene-contribs=2.3.3.4-r5", + "clucene=2.3.3.4-r5", + "pkgconfig" + ], + "provides": [ + "pc:libclucene-core=2.3.3.4" + ] + }, + "sdl-dev": { + "versions": { + "1.2.15-r9": 1545292835 + }, + "origin": "sdl", + "dependencies": [ + "libx11-dev", + "pkgconfig", + "sdl=1.2.15-r9" + ], + "provides": [ + "pc:sdl=1.2.15", + "cmd:sdl-config" + ] + }, + "linux-firmware-emi26": { + "versions": { + "20190322-r0": 1554980652 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "npm": { + "versions": { + "10.14.2-r0": 1545345077 + }, + "origin": "nodejs", + "dependencies": [ + "nodejs" + ], + "provides": [ + "nodejs-npm=10.14.2-r0", + "nodejs-current-npm=10.14.2-r0", + "cmd:npm", + "cmd:npx" + ] + }, + "zfs-dracut": { + "versions": { + "0.7.12-r1": 1552933679 + }, + "origin": "zfs" + }, + "lua5.3-iconv": { + "versions": { + "7-r1": 1545062771 + }, + "origin": "lua-iconv", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "dtc-dev": { + "versions": { + "1.4.7-r0": 1545300716 + }, + "origin": "dtc", + "dependencies": [ + "libfdt=1.4.7-r0" + ] + }, + "lua5.1-struct": { + "versions": { + "0.2-r2": 1545076451 + }, + "origin": "lua-struct", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "glibmm-doc": { + "versions": { + "2.56.0-r1": 1544799351 + }, + "origin": "glibmm" + }, + "perl-class-accessor-doc": { + "versions": { + "0.34-r0": 1543934263 + }, + "origin": "perl-class-accessor" + }, + "perl-term-readkey": { + "versions": { + "2.37-r1": 1543922485 + }, + "origin": "perl-term-readkey", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-purl": { + "versions": { + "1.4-r0": 1543925752 + }, + "origin": "py-purl", + "dependencies": [ + "py2-six", + "python2" + ] + }, + "scons": { + "versions": { + "3.0.1-r2": 1543245859 + }, + "origin": "scons", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:scons", + "cmd:scons-3.0.1", + "cmd:scons-configure-cache", + "cmd:scons-configure-cache-3.0.1", + "cmd:scons-time", + "cmd:scons-time-3.0.1", + "cmd:sconsign", + "cmd:sconsign-3.0.1" + ] + }, + "taglib-dev": { + "versions": { + "1.11.1-r1": 1545062764 + }, + "origin": "taglib", + "dependencies": [ + "pkgconfig", + "taglib=1.11.1-r1" + ], + "provides": [ + "pc:taglib=1.11.1", + "pc:taglib_c=1.11.1", + "cmd:taglib-config" + ] + }, + "sharutils-doc": { + "versions": { + "4.15.2-r0": 1543929026 + }, + "origin": "sharutils" + }, + "ethtool": { + "versions": { + "4.19-r0": 1545820353 + }, + "origin": "ethtool", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ethtool" + ] + }, + "perl-mro-compat-doc": { + "versions": { + "0.13-r0": 1542972948 + }, + "origin": "perl-mro-compat" + }, + "tinyxml2": { + "versions": { + "7.0.1-r0": 1545061051 + }, + "origin": "tinyxml2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libtinyxml2.so.7=7.0.1" + ] + }, + "goaccess": { + "versions": { + "1.2-r1": 1545116584 + }, + "origin": "goaccess", + "dependencies": [ + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:goaccess" + ] + }, + "font-daewoo-misc": { + "versions": { + "1.0.3-r0": 1544798553 + }, + "origin": "font-daewoo-misc", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "perl-text-wikiformat-doc": { + "versions": { + "0.81-r0": 1545076735 + }, + "origin": "perl-text-wikiformat" + }, + "glib-bash-completion": { + "versions": { + "2.58.1-r2": 1545290825 + }, + "origin": "glib" + }, + "man-pages": { + "versions": { + "4.16-r0": 1545256949 + }, + "origin": "man-pages" + }, + "irssi-perl": { + "versions": { + "1.1.2-r0": 1547738041 + }, + "origin": "irssi", + "dependencies": [ + "irssi", + "perl", + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "dvgrab-doc": { + "versions": { + "3.5-r3": 1543999358 + }, + "origin": "dvgrab" + }, + "acf-db-lib": { + "versions": { + "0.2.1-r2": 1543924387 + }, + "origin": "acf-db", + "dependencies": [ + "acf-core", + "lua", + "acf-lib" + ] + }, + "libpcap": { + "versions": { + "1.9.0-r1": 1545905178 + }, + "origin": "libpcap", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpcap.so.1=1.9.0" + ] + }, + "acf-db": { + "versions": { + "0.2.1-r2": 1543924387 + }, + "origin": "acf-db", + "dependencies": [ + "acf-core", + "acf-db-lib", + "acf-db-lib=0.2.1-r2" + ] + }, + "pangomm-dev": { + "versions": { + "2.40.2-r0": 1543925531 + }, + "origin": "pangomm", + "dependencies": [ + "pango-dev", + "glibmm-dev", + "cairomm-dev", + "pangomm=2.40.2-r0", + "pc:cairomm-1.0>=1.2.2", + "pc:glibmm-2.4>=2.48.0", + "pc:pangocairo>=1.38.0", + "pkgconfig" + ], + "provides": [ + "pc:pangomm-1.4=2.40.2" + ] + }, + "xf86-video-tdfx": { + "versions": { + "1.4.7-r3": 1545073943 + }, + "origin": "xf86-video-tdfx", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-openvswitch": { + "versions": { + "2.10.1-r0": 1544000344 + }, + "origin": "openvswitch", + "dependencies": [ + "py2-six" + ] + }, + "cifs-utils-doc": { + "versions": { + "6.8-r0": 1545290851 + }, + "origin": "cifs-utils" + }, + "lua5.1-dbi-postgresql": { + "versions": { + "0.6-r3": 1545214219 + }, + "origin": "lua-dbi", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "perl-switch": { + "versions": { + "2.17-r0": 1543998989 + }, + "origin": "perl-switch", + "dependencies": [ + "perl" + ] + }, + "ntop": { + "versions": { + "5.0.1-r14": 1545209736 + }, + "origin": "ntop", + "dependencies": [ + "/bin/sh", + "so:libGeoIP.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgdbm.so.4", + "so:libpcap.so.1", + "so:libpython2.7.so.1.0", + "so:librrd.so.8", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "so:libnetflowPlugin-5.0.1.so=0", + "so:libntop-5.0.1.so=0", + "so:libntopreport-5.0.1.so=0", + "so:librrdPlugin-5.0.1.so=0", + "so:libsflowPlugin-5.0.1.so=0", + "cmd:ntop", + "cmd:ntop-update-geoip-db" + ] + }, + "openvswitch-doc": { + "versions": { + "2.10.1-r0": 1544000341 + }, + "origin": "openvswitch" + }, + "fcgiwrap": { + "versions": { + "1.1.0-r3": 1545665782 + }, + "origin": "fcgiwrap", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libfcgi.so.0" + ], + "provides": [ + "cmd:fcgiwrap" + ] + }, + "netcf-doc": { + "versions": { + "0.2.8-r5": 1543935399 + }, + "origin": "netcf" + }, + "dovecot-gssapi": { + "versions": { + "2.3.6-r0": 1557134098 + }, + "origin": "dovecot", + "dependencies": [ + "dovecot=2.3.6-r0", + "so:libc.musl-x86_64.so.1", + "so:libgssapi.so.3" + ] + }, + "gtksourceview2-lang": { + "versions": { + "2.10.5-r0": 1545292665 + }, + "origin": "gtksourceview2" + }, + "collectd-libs": { + "versions": { + "5.8.0-r3": 1546422675 + }, + "origin": "collectd", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcrypt.so.20" + ], + "provides": [ + "so:libcollectdclient.so.1=1.1.0" + ] + }, + "bash-dev": { + "versions": { + "4.4.19-r1": 1542301270 + }, + "origin": "bash", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "cmd:bashbug" + ] + }, + "ruby-rdoc": { + "versions": { + "2.5.5-r0": 1557164837 + }, + "origin": "ruby", + "dependencies": [ + "ruby", + "ruby-json", + "ruby-io-console" + ], + "provides": [ + "cmd:rdoc", + "cmd:ri" + ] + }, + "libpri": { + "versions": { + "1.6.0-r0": 1543932624 + }, + "origin": "libpri", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libpri.so.1.4=1.4" + ] + }, + "bind-tools": { + "versions": { + "9.12.4_p1-r1": 1556801608 + }, + "origin": "bind", + "dependencies": [ + "so:libbind9.so.1201", + "so:libc.musl-x86_64.so.1", + "so:libdns.so.1208", + "so:libirs.so.1201", + "so:libisc.so.1204", + "so:libisccfg.so.1203", + "so:libkrb5.so.3" + ], + "provides": [ + "cmd:delv", + "cmd:dig", + "cmd:dnssec-cds", + "cmd:dnssec-dsfromkey", + "cmd:dnssec-importkey", + "cmd:dnssec-keyfromlabel", + "cmd:dnssec-keygen", + "cmd:dnssec-revoke", + "cmd:dnssec-settime", + "cmd:dnssec-signzone", + "cmd:dnssec-verify", + "cmd:host", + "cmd:nslookup", + "cmd:nsupdate" + ] + }, + "xvidcore": { + "versions": { + "1.3.4-r1": 1545302183 + }, + "origin": "xvidcore", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libxvidcore.so.4=4.3" + ] + }, + "openjade-libs": { + "versions": { + "1.3.2-r5": 1542893151 + }, + "origin": "openjade", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libogrove.so.0=0.0.1", + "so:libospgrove.so.0=0.0.1", + "so:libostyle.so.0=0.0.1" + ] + }, + "xsetmode-doc": { + "versions": { + "1.0.0-r4": 1545075298 + }, + "origin": "xsetmode" + }, + "cppunit": { + "versions": { + "1.14.0-r0": 1543932038 + }, + "origin": "cppunit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libcppunit-1.14.so.0=0.0.0", + "cmd:DllPlugInTester" + ] + }, + "py3-docutils": { + "versions": { + "0.14-r1": 1543925152 + }, + "origin": "py-docutils", + "dependencies": [ + "py3-pillow", + "py3-roman", + "python3" + ], + "provides": [ + "cmd:rst2html-3", + "cmd:rst2html4-3", + "cmd:rst2html5-3", + "cmd:rst2latex-3", + "cmd:rst2man-3", + "cmd:rst2odt-3", + "cmd:rst2odt_prepstyles-3", + "cmd:rst2pseudoxml-3", + "cmd:rst2s5-3", + "cmd:rst2xetex-3", + "cmd:rst2xml-3", + "cmd:rstpep2html-3" + ] + }, + "mkinitfs": { + "versions": { + "3.4.1-r0": 1551202041 + }, + "origin": "mkinitfs", + "dependencies": [ + "busybox>=1.28.2-r1", + "apk-tools>=2.9.1", + "lddtree>=1.25", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcryptsetup.so.12", + "so:libkmod.so.2" + ], + "provides": [ + "cmd:bootchartd", + "cmd:mkinitfs", + "cmd:nlplug-findfs" + ] + }, + "perl-test-identity-doc": { + "versions": { + "0.01-r0": 1544799038 + }, + "origin": "perl-test-identity" + }, + "util-linux-doc": { + "versions": { + "2.33-r0": 1545307436 + }, + "origin": "util-linux" + }, + "nginx-mod-http-vod": { + "versions": { + "1.14.2-r1": 1557179822 + }, + "origin": "nginx", + "dependencies": [ + "nginx", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libxml2.so.2", + "so:libz.so.1" + ] + }, + "squid-lang-uz": { + "versions": { + "4.4-r1": 1545216330 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "linux-firmware-radeon": { + "versions": { + "20190322-r0": 1554980649 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "gnutls": { + "versions": { + "3.6.7-r0": 1555049965 + }, + "origin": "gnutls", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libhogweed.so.4", + "so:libnettle.so.6", + "so:libp11-kit.so.0", + "so:libtasn1.so.6", + "so:libunistring.so.2" + ], + "provides": [ + "so:libgnutls.so.30=30.23.2" + ] + }, + "musl-utils": { + "versions": { + "1.1.20-r4": 1552989415 + }, + "origin": "musl", + "dependencies": [ + "scanelf", + "musl=1.1.20-r4", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:getconf", + "cmd:getent", + "cmd:iconv", + "cmd:ldconfig" + ] + }, + "libarchive-doc": { + "versions": { + "3.3.2-r4": 1542820310 + }, + "origin": "libarchive" + }, + "perl-html-tagset-doc": { + "versions": { + "3.20-r1": 1542821827 + }, + "origin": "perl-html-tagset" + }, + "nfdump": { + "versions": { + "1.6.17-r0": 1545223026 + }, + "origin": "nfdump", + "dependencies": [ + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnfdump-1.6.17.so=0", + "cmd:nfanon", + "cmd:nfcapd", + "cmd:nfdump", + "cmd:nfexpire", + "cmd:nfreplay" + ] + }, + "libebml-dev": { + "versions": { + "1.3.6-r1": 1543226528 + }, + "origin": "libebml", + "dependencies": [ + "libebml=1.3.6-r1", + "pkgconfig" + ], + "provides": [ + "pc:libebml=1.3.6" + ] + }, + "freeswitch-sounds-fr-fr-sibylle-8000": { + "versions": { + "0.1.3-r0": 1543999304 + }, + "origin": "freeswitch-sounds-fr-fr-sibylle-8000" + }, + "varnish-geoip": { + "versions": { + "6.1.1-r0": 1545300752 + }, + "origin": "varnish", + "dependencies": [ + "libmaxminddb-dev" + ] + }, + "libnl": { + "versions": { + "1.1.4-r0": 1543924491 + }, + "origin": "libnl", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnl.so.1=1.1.4" + ] + }, + "ghostscript": { + "versions": { + "9.26-r2": 1554362638 + }, + "origin": "ghostscript", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcups.so.2", + "so:libcupsimage.so.2", + "so:libfontconfig.so.1", + "so:libfreetype.so.6", + "so:libjbig2dec.so.0", + "so:libjpeg.so.8", + "so:liblcms2.so.2", + "so:libpng16.so.16", + "so:libtiff.so.5", + "so:libz.so.1" + ], + "provides": [ + "so:libgs.so.9=9.26", + "so:libijs-0.35.so=0", + "cmd:dvipdf", + "cmd:eps2eps", + "cmd:gs", + "cmd:gsbj", + "cmd:gsc", + "cmd:gsdj", + "cmd:gsdj500", + "cmd:gslj", + "cmd:gslp", + "cmd:gsnd", + "cmd:ijs_client_example", + "cmd:ijs_server_example", + "cmd:lprsetup.sh", + "cmd:pdf2dsc", + "cmd:pdf2ps", + "cmd:pf2afm", + "cmd:pfbtopfa", + "cmd:pphs", + "cmd:printafm", + "cmd:ps2ascii", + "cmd:ps2epsi", + "cmd:ps2pdf", + "cmd:ps2pdf12", + "cmd:ps2pdf13", + "cmd:ps2pdf14", + "cmd:ps2pdfwr", + "cmd:ps2ps", + "cmd:ps2ps2", + "cmd:unix-lpr.sh" + ] + }, + "lua5.2-struct": { + "versions": { + "0.2-r2": 1545076453 + }, + "origin": "lua-struct", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "lddtree": { + "versions": { + "1.26-r1": 1542845870 + }, + "origin": "lddtree", + "dependencies": [ + "scanelf" + ], + "provides": [ + "cmd:lddtree" + ] + }, + "libassuan": { + "versions": { + "2.5.1-r0": 1543932161 + }, + "origin": "libassuan", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgpg-error.so.0" + ], + "provides": [ + "so:libassuan.so.0=0.8.1" + ] + }, + "perl-test-mockobject": { + "versions": { + "1.20180705-r0": 1545116825 + }, + "origin": "perl-test-mockobject", + "dependencies": [ + "perl" + ] + }, + "mlmmj-doc": { + "versions": { + "1.3.0-r0": 1542883218 + }, + "origin": "mlmmj" + }, + "gcr": { + "versions": { + "3.28.0-r0": 1545300013 + }, + "origin": "gcr", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgcrypt.so.20", + "so:libgdk-3.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0", + "so:libintl.so.8", + "so:libp11-kit.so.0", + "so:libpango-1.0.so.0" + ], + "provides": [ + "so:libgck-1.so.0=0.0.0", + "so:libgcr-base-3.so.1=1.0.0", + "so:libgcr-ui-3.so.1=1.0.0", + "cmd:gcr-viewer" + ] + }, + "gettext-doc": { + "versions": { + "0.19.8.1-r4": 1542304411 + }, + "origin": "gettext" + }, + "fsarchiver": { + "versions": { + "0.8.5-r0": 1543226651 + }, + "origin": "fsarchiver", + "dependencies": [ + "so:libblkid.so.1", + "so:libbz2.so.1", + "so:libc.musl-x86_64.so.1", + "so:libe2p.so.2", + "so:libext2fs.so.2", + "so:libgcrypt.so.20", + "so:liblz4.so.1", + "so:liblzma.so.5", + "so:liblzo2.so.2", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:fsarchiver" + ] + }, + "lua5.1-pc": { + "versions": { + "1.0.0-r9": 1543928869 + }, + "origin": "lua-pc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-parse-syslog": { + "versions": { + "1.10-r3": 1545073947 + }, + "origin": "perl-parse-syslog", + "dependencies": [ + "perl" + ] + }, + "asterisk": { + "versions": { + "15.7.1-r0": 1546247586 + }, + "origin": "asterisk", + "dependencies": [ + "/bin/sh", + "so:libc-client.so.1", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libedit.so.0", + "so:libjansson.so.4", + "so:liblua.so.5", + "so:libpj.so.2", + "so:libpjlib-util.so.2", + "so:libpjmedia.so.2", + "so:libpjnath.so.2", + "so:libpjsip-simple.so.2", + "so:libpjsip-ua.so.2", + "so:libpjsip.so.2", + "so:libportaudio.so.2", + "so:libsqlite3.so.0", + "so:libssl.so.1.1", + "so:libuuid.so.1", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libasteriskssl.so.1=1", + "cmd:astcanary", + "cmd:astdb2bdb", + "cmd:astdb2sqlite3", + "cmd:asterisk", + "cmd:astgenkey", + "cmd:astversion", + "cmd:autosupport", + "cmd:rasterisk", + "cmd:safe_asterisk" + ] + }, + "libdvbpsi-dev": { + "versions": { + "1.3.2-r0": 1544792492 + }, + "origin": "libdvbpsi", + "dependencies": [ + "libdvbpsi=1.3.2-r0", + "pkgconfig" + ], + "provides": [ + "pc:libdvbpsi=1.3.2" + ] + }, + "lua-inspect": { + "versions": { + "3.1.1-r1": 1545075418 + }, + "origin": "lua-inspect", + "dependencies": [ + "lua" + ], + "provides": [ + "lua5.1-inspect=3.1.1-r1", + "lua5.2-inspect=3.1.1-r1", + "lua5.3-inspect=3.1.1-r1" + ] + }, + "py-scandir": { + "versions": { + "1.9.0-r1": 1542824907 + }, + "origin": "py-scandir" + }, + "dahdi-linux-dev": { + "versions": { + "3.0.0-r0": 1545911874 + }, + "origin": "dahdi-linux" + }, + "squid-lang-fi": { + "versions": { + "4.4-r1": 1545216325 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "qemu-ppc": { + "versions": { + "3.1.0-r3": 1551107301 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-ppc" + ] + }, + "perl-module-build-tiny": { + "versions": { + "0.039-r0": 1543238869 + }, + "origin": "perl-module-build-tiny", + "dependencies": [ + "perl-extutils-installpaths", + "perl-extutils-config", + "perl-extutils-helpers", + "perl-test-harness" + ] + }, + "qemu-riscv64": { + "versions": { + "3.1.0-r3": 1551107302 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-riscv64" + ] + }, + "openldap-passwd-pbkdf2": { + "versions": { + "2.4.47-r2": 1546016477 + }, + "origin": "openldap", + "dependencies": [ + "openldap", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "valgrind-doc": { + "versions": { + "3.14.0-r0": 1545300865 + }, + "origin": "valgrind" + }, + "iproute2-doc": { + "versions": { + "4.19.0-r0": 1543223211 + }, + "origin": "iproute2" + }, + "libseccomp-doc": { + "versions": { + "2.3.3-r1": 1543923553 + }, + "origin": "libseccomp" + }, + "po4a-doc": { + "versions": { + "0.52-r1": 1542304729 + }, + "origin": "po4a" + }, + "xf86-video-sis-doc": { + "versions": { + "0.10.9-r3": 1545253989 + }, + "origin": "xf86-video-sis" + }, + "syslog-ng-python2": { + "versions": { + "3.19.1-r0": 1548543150 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "perl-apache-logformat-compiler-doc": { + "versions": { + "0.32-r0": 1544792421 + }, + "origin": "perl-apache-logformat-compiler" + }, + "freshclam": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libclamav.so.7", + "so:libcrypto.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:freshclam" + ] + }, + "squid-lang-tr": { + "versions": { + "4.4-r1": 1545216330 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "isl": { + "versions": { + "0.18-r0": 1542301497 + }, + "origin": "isl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10" + ], + "provides": [ + "so:libisl.so.15=15.3.0" + ] + }, + "openipmi-libs": { + "versions": { + "2.0.25-r1": 1545214271 + }, + "origin": "openipmi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "so:libOpenIPMI.so.0=0.0.5", + "so:libOpenIPMIcmdlang.so.0=0.0.5", + "so:libOpenIPMIglib.so.0=0.0.1", + "so:libOpenIPMIposix.so.0=0.0.1", + "so:libOpenIPMIpthread.so.0=0.0.1", + "so:libOpenIPMIui.so.1=1.0.1", + "so:libOpenIPMIutils.so.0=0.0.1" + ] + }, + "atf": { + "versions": { + "0.21-r1": 1542304875 + }, + "origin": "atf", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libatf-c++.so.2=2.0.0", + "so:libatf-c.so.1=1.0.0", + "cmd:atf-sh" + ] + }, + "openobex-dev": { + "versions": { + "1.7.2-r1": 1545069090 + }, + "origin": "openobex", + "dependencies": [ + "libusb-dev", + "openobex=1.7.2-r1", + "pc:libusb-1.0", + "pkgconfig" + ], + "provides": [ + "pc:openobex=1.7.2" + ] + }, + "perl-net-server": { + "versions": { + "2.009-r1": 1545213954 + }, + "origin": "perl-net-server", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:net-server" + ] + }, + "qemu-doc": { + "versions": { + "3.1.0-r3": 1551107299 + }, + "origin": "qemu" + }, + "xcb-proto": { + "versions": { + "1.13-r2": 1542822452 + }, + "origin": "xcb-proto", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xcb-proto=1.13" + ] + }, + "ivshmem-tools": { + "versions": { + "3.1.0-r3": 1551107299 + }, + "origin": "qemu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0" + ], + "provides": [ + "cmd:ivshmem-client", + "cmd:ivshmem-server" + ] + }, + "cdw": { + "versions": { + "0.8.1-r0": 1545302173 + }, + "origin": "cdw", + "dependencies": [ + "cdrkit", + "so:libburn.so.4", + "so:libc.musl-x86_64.so.1", + "so:libcdio.so.16", + "so:libformw.so.6", + "so:libintl.so.8", + "so:libiso9660.so.10", + "so:libmenuw.so.6", + "so:libncursesw.so.6", + "so:libpanelw.so.6" + ], + "provides": [ + "cmd:cdw" + ] + }, + "lua5.3-dev": { + "versions": { + "5.3.5-r2": 1557162501 + }, + "origin": "lua5.3", + "dependencies": [ + "lua5.3", + "lua5.3-libs=5.3.5-r2", + "pkgconfig" + ], + "provides": [ + "pc:lua5.3=5.3.5" + ] + }, + "usb-modeswitch": { + "versions": { + "2.5.2-r0": 1543932713 + }, + "origin": "usb-modeswitch", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "cmd:usb_modeswitch" + ] + }, + "bacula-sqlite": { + "versions": { + "9.4.1-r1": 1546944087 + }, + "origin": "bacula", + "dependencies": [ + "bacula", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ], + "provides": [ + "so:libbaccats-sqlite3-9.4.1.so=0" + ] + }, + "qemu-xtensa": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-xtensa" + ] + }, + "fping": { + "versions": { + "4.1-r0": 1545746006 + }, + "origin": "fping", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:fping" + ] + }, + "libzmq": { + "versions": { + "4.3.1-r0": 1548271359 + }, + "origin": "zeromq", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libsodium.so.23", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libzmq.so.5=5.2.1" + ] + }, + "dhcrelay": { + "versions": { + "4.4.1-r1": 1543928454 + }, + "origin": "dhcp", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "cmd:dhcrelay" + ] + }, + "setxkbmap-doc": { + "versions": { + "1.3.1-r1": 1545209626 + }, + "origin": "setxkbmap" + }, + "zfs-utils-py": { + "versions": { + "0.7.12-r1": 1552933680 + }, + "origin": "zfs", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:arc_summary.py", + "cmd:arcstat.py", + "cmd:dbufstat.py" + ] + }, + "kamailio-debugger": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libprint.so.1", + "so:libsrutils.so.1" + ] + }, + "libproxy-bin": { + "versions": { + "0.4.15-r1": 1544001156 + }, + "origin": "libproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libproxy.so.1" + ], + "provides": [ + "cmd:proxy" + ] + }, + "qt-postgresql": { + "versions": { + "4.8.7-r11": 1545075087 + }, + "origin": "qt", + "dependencies": [ + "ca-certificates", + "so:libQtCore.so.4", + "so:libQtSql.so.4", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpq.so.5", + "so:libstdc++.so.6" + ] + }, + "transmission-cli": { + "versions": { + "2.94-r1": 1545208748 + }, + "origin": "transmission", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libcurl.so.4", + "so:libevent-2.1.so.6", + "so:libintl.so.8", + "so:libz.so.1" + ], + "provides": [ + "cmd:transmission-cli", + "cmd:transmission-create", + "cmd:transmission-edit", + "cmd:transmission-remote", + "cmd:transmission-show" + ] + }, + "ldns-doc": { + "versions": { + "1.7.0-r2": 1545117117 + }, + "origin": "ldns" + }, + "iperf3": { + "versions": { + "3.6-r0": 1543932136 + }, + "origin": "iperf3", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libiperf.so.0=0.0.0", + "cmd:iperf3" + ] + }, + "tinc": { + "versions": { + "1.0.35-r1": 1545299617 + }, + "origin": "tinc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:liblzo2.so.2", + "so:libz.so.1" + ], + "provides": [ + "cmd:tincd" + ] + }, + "libfprint-dev": { + "versions": { + "0.7.0-r1": 1545207878 + }, + "origin": "libfprint", + "dependencies": [ + "libusb-dev", + "libfprint=0.7.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:libfprint=0.7.0" + ] + }, + "ppp-chat": { + "versions": { + "2.4.7-r6": 1543999020 + }, + "origin": "ppp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:chat" + ] + }, + "mini_httpd": { + "versions": { + "1.29-r2": 1544000654 + }, + "origin": "mini_httpd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:mini_htpasswd", + "cmd:mini_httpd" + ] + }, + "py2-gv": { + "versions": { + "2.40.1-r1": 1543926741 + }, + "origin": "graphviz", + "dependencies": [ + "python2", + "so:libc.musl-x86_64.so.1", + "so:libcgraph.so.6", + "so:libgcc_s.so.1", + "so:libgvc.so.6", + "so:libstdc++.so.6" + ], + "provides": [ + "py-graphviz=2.40.1-r1" + ] + }, + "apache2-proxy-html": { + "versions": { + "2.4.39-r0": 1554306835 + }, + "origin": "apache2", + "dependencies": [ + "apache2", + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ] + }, + "lua5.1-cqueues": { + "versions": { + "20171014-r3": 1546520954 + }, + "origin": "lua-cqueues", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ] + }, + "xcb-util-cursor-dev": { + "versions": { + "0.1.3-r1": 1545163433 + }, + "origin": "xcb-util-cursor", + "dependencies": [ + "xcb-util-dev", + "pc:xcb-image", + "pc:xcb-render", + "pc:xcb-renderutil", + "pkgconfig", + "xcb-util-cursor=0.1.3-r1" + ], + "provides": [ + "pc:xcb-cursor=0.1.3" + ] + }, + "libdaemon-doc": { + "versions": { + "0.14-r2": 1543925257 + }, + "origin": "libdaemon" + }, + "py2-requests": { + "versions": { + "2.19.1-r0": 1542825031 + }, + "origin": "py-requests", + "dependencies": [ + "py2-chardet", + "py2-idna", + "py2-certifi", + "py2-urllib3", + "python2" + ] + }, + "ppp-doc": { + "versions": { + "2.4.7-r6": 1543999019 + }, + "origin": "ppp" + }, + "aspell-doc": { + "versions": { + "0.60.6.1-r13": 1542965830 + }, + "origin": "aspell" + }, + "perl-mime-base64": { + "versions": { + "3.15-r4": 1545061197 + }, + "origin": "perl-mime-base64", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "py2-pytest": { + "versions": { + "4.1.0-r0": 1548112483 + }, + "origin": "pytest", + "dependencies": [ + "py2-atomicwrites", + "py2-py", + "py2-six", + "py2-attrs", + "py2-more-itertools", + "py2-pluggy", + "py2-setuptools", + "py2-funcsigs", + "py2-pathlib2", + "python2" + ], + "provides": [ + "cmd:py.test-2", + "cmd:pytest-2" + ] + }, + "cppunit-doc": { + "versions": { + "1.14.0-r0": 1543932038 + }, + "origin": "cppunit" + }, + "python3-dbg": { + "versions": { + "3.6.8-r2": 1554747637 + }, + "origin": "python3" + }, + "rsyslog-pmsnare": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "font-screen-cyrillic": { + "versions": { + "1.0.4-r0": 1545207697 + }, + "origin": "font-screen-cyrillic", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "rtpproxy-debug": { + "versions": { + "2.0.0-r4": 1545301079 + }, + "origin": "rtpproxy", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rtpproxy_debug" + ] + }, + "libnfsidmap": { + "versions": { + "2.3.2-r1": 1543933422 + }, + "origin": "nfs-utils", + "dependencies": [ + "rpcbind", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libnfsidmap.so.1=1.0.0" + ] + }, + "perl-json-maybexs": { + "versions": { + "1.004000-r0": 1542893316 + }, + "origin": "perl-json-maybexs", + "dependencies": [ + "perl-cpanel-json-xs" + ] + }, + "imlib2-dev": { + "versions": { + "1.5.1-r0": 1543221705 + }, + "origin": "imlib2", + "dependencies": [ + "freetype-dev", + "libxext-dev", + "libsm-dev", + "imlib2=1.5.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:imlib2=1.5.1", + "cmd:imlib2-config" + ] + }, + "ruby-xmlrpc": { + "versions": { + "2.5.5-r0": 1557164838 + }, + "origin": "ruby", + "dependencies": [ + "ruby-libs" + ] + }, + "qpdf-fix-qdf": { + "versions": { + "8.3.0-r0": 1547117323 + }, + "origin": "qpdf", + "dependencies": [ + "qpdf", + "perl" + ], + "provides": [ + "cmd:fix-qdf" + ] + }, + "py-constantly": { + "versions": { + "15.1.0-r0": 1546513613 + }, + "origin": "py-constantly" + }, + "mpg123": { + "versions": { + "1.25.10-r0": 1545117150 + }, + "origin": "mpg123", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmpg123.so.0=0.44.8", + "so:libout123.so.0=0.2.2", + "cmd:mpg123", + "cmd:mpg123-id3dump", + "cmd:mpg123-strip", + "cmd:out123" + ] + }, + "squark": { + "versions": { + "0.6.1-r1": 1545060929 + }, + "origin": "squark", + "dependencies": [ + "haserl", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcmph.so.0", + "so:liblua.so.5", + "so:libnetsnmp.so.35" + ], + "provides": [ + "cmd:sqdb-build.lua", + "cmd:squark-auth-ip", + "cmd:squark-auth-snmp", + "cmd:squark-filter" + ] + }, + "pacman-lang": { + "versions": { + "5.0.2-r4": 1543932339 + }, + "origin": "pacman", + "dependencies": [ + "libarchive-tools" + ] + }, + "xf86-input-keyboard-doc": { + "versions": { + "1.9.0-r1": 1545073857 + }, + "origin": "xf86-input-keyboard" + }, + "glfw-dev": { + "versions": { + "3.2.1-r2": 1545299813 + }, + "origin": "glfw", + "dependencies": [ + "libxinerama-dev", + "linux-headers", + "mesa-dev", + "glfw=3.2.1-r2", + "pc:x11", + "pc:xcursor", + "pc:xinerama", + "pc:xrandr", + "pc:xxf86vm", + "pkgconfig" + ], + "provides": [ + "pc:glfw3=3.2.1" + ] + }, + "mpc1-dev": { + "versions": { + "1.0.3-r1": 1542301580 + }, + "origin": "mpc1", + "dependencies": [ + "mpc1=1.0.3-r1" + ] + }, + "libvirt-common-drivers": { + "versions": { + "4.10.0-r1": 1547051658 + }, + "origin": "libvirt", + "dependencies": [ + "lvm2", + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdbus-1.so.3", + "so:libintl.so.8", + "so:libnetcf.so.1", + "so:libpcap.so.1", + "so:libpciaccess.so.0", + "so:libudev.so.1", + "so:libvirt.so.0", + "so:libxml2.so.2" + ] + }, + "mpc1-doc": { + "versions": { + "1.0.3-r1": 1542301580 + }, + "origin": "mpc1" + }, + "llvm5": { + "versions": { + "5.0.2-r0": 1546874981 + }, + "origin": "llvm5", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "llvm=5.0.2-r0", + "cmd:bugpoint", + "cmd:llc", + "cmd:lli", + "cmd:llvm-ar", + "cmd:llvm-as", + "cmd:llvm-bcanalyzer", + "cmd:llvm-c-test", + "cmd:llvm-cat", + "cmd:llvm-cov", + "cmd:llvm-cvtres", + "cmd:llvm-cxxdump", + "cmd:llvm-cxxfilt", + "cmd:llvm-diff", + "cmd:llvm-dis", + "cmd:llvm-dlltool", + "cmd:llvm-dsymutil", + "cmd:llvm-dwarfdump", + "cmd:llvm-dwp", + "cmd:llvm-extract", + "cmd:llvm-lib", + "cmd:llvm-link", + "cmd:llvm-lto", + "cmd:llvm-lto2", + "cmd:llvm-mc", + "cmd:llvm-mcmarkup", + "cmd:llvm-modextract", + "cmd:llvm-mt", + "cmd:llvm-nm", + "cmd:llvm-objdump", + "cmd:llvm-opt-report", + "cmd:llvm-pdbutil", + "cmd:llvm-profdata", + "cmd:llvm-ranlib", + "cmd:llvm-readelf", + "cmd:llvm-readobj", + "cmd:llvm-rtdyld", + "cmd:llvm-size", + "cmd:llvm-split", + "cmd:llvm-stress", + "cmd:llvm-strings", + "cmd:llvm-symbolizer", + "cmd:llvm-tblgen", + "cmd:llvm-xray", + "cmd:opt", + "cmd:sancov", + "cmd:sanstats", + "cmd:verify-uselistorder" + ] + }, + "lua5.1-ldap": { + "versions": { + "1.2.3-r5": 1543223250 + }, + "origin": "lua-ldap", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2" + ] + }, + "gnome-vfs-dev": { + "versions": { + "2.24.4-r6": 1545299689 + }, + "origin": "gnome-vfs", + "dependencies": [ + "gconf-dev", + "libxml2-dev", + "dbus-glib-dev", + "gamin-dev", + "gnome-vfs=2.24.4-r6", + "pc:gconf-2.0", + "pc:gmodule-no-export-2.0", + "pc:gobject-2.0", + "pc:gthread-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gnome-vfs-2.0=2.24.4", + "pc:gnome-vfs-module-2.0=2.24.4" + ] + }, + "jbig2dec-dev": { + "versions": { + "0.15-r0": 1545207929 + }, + "origin": "jbig2dec", + "dependencies": [ + "jbig2dec=0.15-r0" + ] + }, + "ipset": { + "versions": { + "7.1-r0": 1545163450 + }, + "origin": "ipset", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmnl.so.0" + ], + "provides": [ + "so:libipset.so.13=13.1.0", + "cmd:ipset" + ] + }, + "syslog-ng-redis": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libhiredis.so.0.14", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "xf86-input-evdev-dev": { + "versions": { + "2.10.6-r0": 1545229728 + }, + "origin": "xf86-input-evdev", + "dependencies": [ + "pkgconfig" + ], + "provides": [ + "pc:xorg-evdev=2.10.6" + ] + }, + "flac": { + "versions": { + "1.3.2-r2": 1545068441 + }, + "origin": "flac", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libogg.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libFLAC++.so.6=6.3.0", + "so:libFLAC.so.8=8.3.0", + "cmd:flac", + "cmd:metaflac" + ] + }, + "mpfr-dev": { + "versions": { + "3.1.5-r1": 1542301548 + }, + "origin": "mpfr3", + "dependencies": [ + "mpfr3=3.1.5-r1" + ] + }, + "py-redis": { + "versions": { + "2.10.6-r0": 1545301347 + }, + "origin": "py-redis" + }, + "liblcms": { + "versions": { + "1.19-r8": 1546249798 + }, + "origin": "lcms", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:liblcms.so.1=1.0.19" + ] + }, + "py-chardet": { + "versions": { + "3.0.4-r0": 1542825004 + }, + "origin": "py-chardet" + }, + "pciutils-doc": { + "versions": { + "3.6.2-r0": 1543921866 + }, + "origin": "pciutils" + }, + "linux-firmware-intel": { + "versions": { + "20190322-r0": 1554980651 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "xf86-video-ati-doc": { + "versions": { + "18.1.0-r0": 1545223268 + }, + "origin": "xf86-video-ati" + }, + "arpon": { + "versions": { + "3.0-r2": 1545224126 + }, + "origin": "arpon", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdnet.so.1", + "so:libnet.so.1", + "so:libpcap.so.1" + ], + "provides": [ + "cmd:arpon" + ] + }, + "perl-snmp-session": { + "versions": { + "1.13-r2": 1543999432 + }, + "origin": "perl-snmp-session", + "dependencies": [ + "perl" + ] + }, + "mpeg2dec": { + "versions": { + "0.5.1-r8": 1544000975 + }, + "origin": "libmpeg2", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libc.musl-x86_64.so.1", + "so:libmpeg2.so.0", + "so:libmpeg2convert.so.0" + ], + "provides": [ + "cmd:mpeg2dec" + ] + }, + "uwsgi-router_http": { + "versions": { + "2.0.17.1-r0": 1545062207 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "qemu-sparc64": { + "versions": { + "3.1.0-r3": 1551107302 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-sparc64" + ] + }, + "cd-discid-doc": { + "versions": { + "1.4-r2": 1545117997 + }, + "origin": "cd-discid" + }, + "libelf": { + "versions": { + "0.8.13-r3": 1542900243 + }, + "origin": "libelf", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libelf.so.0=0" + ] + }, + "patchutils": { + "versions": { + "0.3.4-r1": 1548427864 + }, + "origin": "patchutils", + "dependencies": [ + "perl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:combinediff", + "cmd:dehtmldiff", + "cmd:editdiff", + "cmd:espdiff", + "cmd:filterdiff", + "cmd:fixcvsdiff", + "cmd:flipdiff", + "cmd:grepdiff", + "cmd:interdiff", + "cmd:lsdiff", + "cmd:recountdiff", + "cmd:rediff", + "cmd:splitdiff", + "cmd:unwrapdiff" + ] + }, + "mqtt-exec-openrc": { + "versions": { + "0.4-r4": 1545162970 + }, + "origin": "mqtt-exec" + }, + "mcabber-doc": { + "versions": { + "1.1.0-r2": 1545214061 + }, + "origin": "mcabber" + }, + "nagios-plugins-breeze": { + "versions": { + "2.2.1-r6": 1543933902 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "perl" + ] + }, + "ghostscript-fonts": { + "versions": { + "8.11-r1": 1545062808 + }, + "origin": "ghostscript-fonts" + }, + "wireless-regdb-doc": { + "versions": { + "2018.05.31-r1": 1550837521 + }, + "origin": "wireless-regdb" + }, + "mpt-status-doc": { + "versions": { + "1.2.0-r0": 1545069138 + }, + "origin": "mpt-status" + }, + "py2-atomicwrites": { + "versions": { + "1.1.5-r0": 1542824872 + }, + "origin": "py-atomicwrites", + "dependencies": [ + "python2" + ] + }, + "a2ps": { + "versions": { + "4.14-r7": 1545209605 + }, + "origin": "a2ps", + "dependencies": [ + "ghostscript", + "imagemagick", + "perl", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:a2ps", + "cmd:card", + "cmd:composeglyphs", + "cmd:fixnt", + "cmd:fixps", + "cmd:ogonkify", + "cmd:pdiff", + "cmd:psmandup", + "cmd:psset", + "cmd:texi2dvi4a2ps" + ] + }, + "libxfont2": { + "versions": { + "2.0.3-r1": 1542924621 + }, + "origin": "libxfont2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfontenc.so.1", + "so:libfreetype.so.6", + "so:libz.so.1" + ], + "provides": [ + "so:libXfont2.so.2=2.0.0" + ] + }, + "tdb-doc": { + "versions": { + "1.3.16-r0": 1543933255 + }, + "origin": "tdb" + }, + "xdriinfo-doc": { + "versions": { + "1.0.5-r1": 1543927863 + }, + "origin": "xdriinfo" + }, + "mesa-dri-ati": { + "versions": { + "18.1.7-r2": 1554455969 + }, + "origin": "mesa", + "dependencies": [ + "so:libLLVM-5.0.so", + "so:libX11-xcb.so.1", + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libdrm_amdgpu.so.1", + "so:libdrm_freedreno.so.1", + "so:libdrm_intel.so.1", + "so:libdrm_nouveau.so.2", + "so:libdrm_radeon.so.1", + "so:libelf.so.0", + "so:libexpat.so.1", + "so:libgcc_s.so.1", + "so:libglapi.so.0", + "so:libstdc++.so.6", + "so:libxcb-dri2.so.0", + "so:libxcb-dri3.so.0", + "so:libxcb-present.so.0", + "so:libxcb-sync.so.1", + "so:libxcb-xfixes.so.0", + "so:libxcb.so.1", + "so:libxshmfence.so.1", + "so:libz.so.1" + ] + }, + "perl-proc-wait3-doc": { + "versions": { + "0.05-r1": 1543249816 + }, + "origin": "perl-proc-wait3" + }, + "lua5.1-yaml": { + "versions": { + "1.1.2-r1": 1544000067 + }, + "origin": "lua-yaml", + "dependencies": [ + "lua5.1", + "lua5.1-lub", + "so:libc.musl-x86_64.so.1" + ] + }, + "lxpolkit": { + "versions": { + "0.1.0-r1": 1545229284 + }, + "origin": "lxpolkit", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpolkit-agent-1.so.0", + "so:libpolkit-gobject-1.so.0" + ] + }, + "kamailio-python": { + "versions": { + "5.2.0-r1": 1546423173 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "spice-dev": { + "versions": { + "0.14.1-r6": 1548919556 + }, + "origin": "spice", + "dependencies": [ + "spice-protocol", + "pixman-dev", + "celt051-dev", + "libxinerama-dev", + "pc:gio-2.0>=2.32", + "pc:glib-2.0>=2.32", + "pc:gobject-2.0>=2.32", + "pc:openssl", + "pc:pixman-1>=0.17.7", + "pc:spice-protocol>=0.12.14", + "pkgconfig", + "spice-server=0.14.1-r6" + ], + "provides": [ + "pc:spice-server=0.14.1" + ] + }, + "xbanish": { + "versions": { + "1.6-r0": 1545290830 + }, + "origin": "xbanish", + "dependencies": [ + "so:libX11.so.6", + "so:libXfixes.so.3", + "so:libXi.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xbanish" + ] + }, + "boost-math": { + "versions": { + "1.67.0-r2": 1542823632 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_math_c99-mt.so.1.67.0=1.67.0", + "so:libboost_math_c99.so.1.67.0=1.67.0", + "so:libboost_math_c99f-mt.so.1.67.0=1.67.0", + "so:libboost_math_c99f.so.1.67.0=1.67.0", + "so:libboost_math_c99l-mt.so.1.67.0=1.67.0", + "so:libboost_math_c99l.so.1.67.0=1.67.0", + "so:libboost_math_tr1-mt.so.1.67.0=1.67.0", + "so:libboost_math_tr1.so.1.67.0=1.67.0", + "so:libboost_math_tr1f-mt.so.1.67.0=1.67.0", + "so:libboost_math_tr1f.so.1.67.0=1.67.0", + "so:libboost_math_tr1l-mt.so.1.67.0=1.67.0", + "so:libboost_math_tr1l.so.1.67.0=1.67.0" + ] + }, + "apr-util": { + "versions": { + "1.6.1-r5": 1545061010 + }, + "origin": "apr-util", + "dependencies": [ + "so:libapr-1.so.0", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libexpat.so.1" + ], + "provides": [ + "so:libaprutil-1.so.0=0.6.1" + ] + }, + "dhcpcd-ui": { + "versions": { + "0.7.5-r2": 1545060723 + }, + "origin": "dhcpcd-ui", + "dependencies": [ + "dhcpcd-dbus", + "hicolor-icon-theme", + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libnotify.so.4" + ], + "provides": [ + "cmd:dhcpcd-gtk", + "cmd:dhcpcd-online" + ] + }, + "powertop-doc": { + "versions": { + "2.9-r1": 1543927724 + }, + "origin": "powertop" + }, + "keyutils-libs": { + "versions": { + "1.6-r0": 1545837151 + }, + "origin": "keyutils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libkeyutils.so.1=1.8" + ] + }, + "bluez-btmon": { + "versions": { + "5.50-r0": 1545822205 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1", + "so:libudev.so.1" + ], + "provides": [ + "cmd:btmon" + ] + }, + "liboping": { + "versions": { + "1.10.0-r0": 1545214324 + }, + "origin": "liboping", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "so:liboping.so.0=0.3.0", + "cmd:noping", + "cmd:oping" + ] + }, + "rsyslog-mmjsonparse": { + "versions": { + "8.40.0-r3": 1548686790 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "avahi-ui-dev": { + "versions": { + "0.6.31-r7": 1545163746 + }, + "origin": "avahi-ui", + "dependencies": [ + "gtk+3.0-dev", + "gtk+2.0-dev", + "gdbm-dev", + "avahi-dev", + "avahi-ui-gtk3=0.6.31-r7", + "avahi-ui=0.6.31-r7", + "pc:avahi-client", + "pc:avahi-glib", + "pc:gtk+-2.0", + "pc:gtk+-3.0", + "pkgconfig" + ], + "provides": [ + "pc:avahi-ui-gtk3=0.6.31", + "pc:avahi-ui=0.6.31" + ] + }, + "libxau-dev": { + "versions": { + "1.0.8-r3": 1542822449 + }, + "origin": "libxau", + "dependencies": [ + "libxau=1.0.8-r3", + "pc:xproto", + "pkgconfig" + ], + "provides": [ + "pc:xau=1.0.8" + ] + }, + "libucontext": { + "versions": { + "0.1.3-r0": 1545207700 + }, + "origin": "libucontext", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libucontext.so.0=0" + ] + }, + "libgcc": { + "versions": { + "8.3.0-r0": 1554745497 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgcc_s.so.1=1" + ] + }, + "py-sphinx_rtd_theme": { + "versions": { + "0.4.2-r0": 1544791909 + }, + "origin": "py-sphinx_rtd_theme" + }, + "ninja": { + "versions": { + "1.8.2-r1": 1542822429 + }, + "origin": "ninja", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:ninja" + ] + }, + "apr-util-dbm_db": { + "versions": { + "1.6.1-r5": 1545061009 + }, + "origin": "apr-util", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ] + }, + "unifont-doc": { + "versions": { + "11.0.01-r1": 1543077262 + }, + "origin": "unifont" + }, + "vanessa_socket-dev": { + "versions": { + "0.0.13-r0": 1545116977 + }, + "origin": "vanessa_socket", + "dependencies": [ + "popt-dev", + "vanessa_logger-dev", + "pkgconfig", + "vanessa_socket=0.0.13-r0" + ], + "provides": [ + "pc:vanessa-socket=0.0.13" + ] + }, + "perl-test-needs": { + "versions": { + "0.002005-r1": 1542821779 + }, + "origin": "perl-test-needs" + }, + "samba-server": { + "versions": { + "4.8.11-r1": 1555334975 + }, + "origin": "samba", + "dependencies": [ + "samba-common=4.8.11-r1", + "samba-initscript=4.8.11-r1", + "so:libCHARSET3-samba4.so", + "so:libauth-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libcli-cldap-samba4.so", + "so:libcli-nbt-samba4.so", + "so:libcli-smb-common-samba4.so", + "so:libcliauth-samba4.so", + "so:libcmdline-contexts-samba4.so", + "so:libdbwrap-samba4.so", + "so:libgenrand-samba4.so", + "so:libgse-samba4.so", + "so:liblibsmb-samba4.so", + "so:libmessages-dgm-samba4.so", + "so:libmsghdr-samba4.so", + "so:libndr-nbt.so.0", + "so:libndr-samba-samba4.so", + "so:libndr-standard.so.0", + "so:libndr.so.0", + "so:libpopt-samba3-samba4.so", + "so:libpopt.so.0", + "so:libreplace-samba4.so", + "so:libsamba-cluster-support-samba4.so", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-sockets-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libsmb-transport-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbd-shim-samba4.so", + "so:libsocket-blocking-samba4.so", + "so:libsys-rw-samba4.so", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-reg-samba4.so", + "so:libutil-tdb-samba4.so" + ], + "provides": [ + "so:libxattr-tdb-samba4.so=0", + "cmd:eventlogadm", + "cmd:nmbd", + "cmd:smbd", + "cmd:smbstatus" + ] + }, + "py2-libxslt": { + "versions": { + "1.1.33-r1": 1555485888 + }, + "origin": "libxslt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libexslt.so.0", + "so:libpython2.7.so.1.0", + "so:libxml2.so.2", + "so:libxslt.so.1" + ], + "provides": [ + "py-libxslt=1.1.33-r1" + ] + }, + "libiec61883-dev": { + "versions": { + "1.2.0-r2": 1543999348 + }, + "origin": "libiec61883", + "dependencies": [ + "libiec61883=1.2.0-r2", + "pc:libraw1394", + "pkgconfig" + ], + "provides": [ + "pc:libiec61883=1.2.0" + ] + }, + "wpa_supplicant": { + "versions": { + "2.7-r2": 1555501504 + }, + "origin": "wpa_supplicant", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libdbus-1.so.3", + "so:libnl-3.so.200", + "so:libnl-genl-3.so.200", + "so:libpcsclite.so.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:eapol_test", + "cmd:wpa_cli", + "cmd:wpa_passphrase", + "cmd:wpa_supplicant" + ] + }, + "boost-atomic": { + "versions": { + "1.67.0-r2": 1542823630 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libboost_atomic-mt.so.1.67.0=1.67.0" + ] + }, + "libpcrecpp": { + "versions": { + "8.42-r1": 1545067004 + }, + "origin": "pcre", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libpcre.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libpcrecpp.so.0=0.0.1" + ] + }, + "perl-class-method-modifiers-doc": { + "versions": { + "2.12-r0": 1545073304 + }, + "origin": "perl-class-method-modifiers" + }, + "taskd-doc": { + "versions": { + "1.1.0-r4": 1545163048 + }, + "origin": "taskd" + }, + "font-xfree86-type1": { + "versions": { + "1.0.4-r0": 1543242219 + }, + "origin": "font-xfree86-type1", + "dependencies": [ + "encodings", + "font-alias", + "mkfontscale", + "mkfontdir", + "fontconfig" + ] + }, + "libxkbfile-dev": { + "versions": { + "1.0.9-r2": 1542973200 + }, + "origin": "libxkbfile", + "dependencies": [ + "libxkbfile=1.0.9-r2", + "pc:kbproto", + "pc:x11", + "pkgconfig" + ], + "provides": [ + "pc:xkbfile=1.0.9" + ] + }, + "git-subtree-doc": { + "versions": { + "2.20.1-r0": 1545214197 + }, + "origin": "git" + }, + "libverto": { + "versions": { + "0.3.0-r1": 1543223528 + }, + "origin": "libverto", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libverto.so.1=1.0.0" + ] + }, + "pgtcl-doc": { + "versions": { + "2.1.0-r0": 1545301843 + }, + "origin": "pgtcl" + }, + "perl-net-ip": { + "versions": { + "1.26-r1": 1543924419 + }, + "origin": "perl-net-ip", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:ipcount", + "cmd:iptab" + ] + }, + "smem-doc": { + "versions": { + "1.4-r1": 1545060969 + }, + "origin": "smem" + }, + "py-unbound": { + "versions": { + "1.8.3-r1": 1555953584 + }, + "origin": "unbound", + "dependencies": [ + "dnssec-root", + "so:libc.musl-x86_64.so.1", + "so:libunbound.so.8" + ] + }, + "isl-dev": { + "versions": { + "0.18-r0": 1542301496 + }, + "origin": "isl", + "dependencies": [ + "gmp-dev", + "isl=0.18-r0", + "pkgconfig" + ], + "provides": [ + "pc:isl=0.18" + ] + }, + "libedit-dev": { + "versions": { + "20181209.3.1-r0": 1545839273 + }, + "origin": "libedit", + "dependencies": [ + "ncurses-dev", + "libedit=20181209.3.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:libedit=3.1" + ] + }, + "lxsession-doc": { + "versions": { + "0.5.3-r0": 1545299865 + }, + "origin": "lxsession" + }, + "cjdns-doc": { + "versions": { + "20.2-r1": 1546423249 + }, + "origin": "cjdns" + }, + "iniparser-dev": { + "versions": { + "4.1-r0": 1543928460 + }, + "origin": "iniparser", + "dependencies": [ + "iniparser=4.1-r0" + ] + }, + "libxrender": { + "versions": { + "0.9.10-r3": 1542822736 + }, + "origin": "libxrender", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXrender.so.1=1.3.0" + ] + }, + "nagios-plugins-pgsql": { + "versions": { + "2.2.1-r6": 1543933910 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5" + ] + }, + "mp3splt-doc": { + "versions": { + "2.6.2-r0": 1545207912 + }, + "origin": "mp3splt" + }, + "lua-mqtt-publish": { + "versions": { + "0.3-r0": 1542820949 + }, + "origin": "lua-mqtt-publish" + }, + "bluez-bccmd": { + "versions": { + "5.50-r0": 1545822205 + }, + "origin": "bluez", + "dependencies": [ + "consolekit2", + "dbus", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:bccmd" + ] + }, + "libgnome-lang": { + "versions": { + "2.32.1-r7": 1545299767 + }, + "origin": "libgnome" + }, + "libidl": { + "versions": { + "0.8.14-r2": 1543223347 + }, + "origin": "libidl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ], + "provides": [ + "so:libIDL-2.so.0=0.0.0", + "cmd:libIDL-config-2" + ] + }, + "sfcapd": { + "versions": { + "1.6.17-r0": 1545223025 + }, + "origin": "nfdump", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libnfdump-1.6.17.so" + ], + "provides": [ + "cmd:sfcapd" + ] + }, + "squid-lang-fr": { + "versions": { + "4.4-r1": 1545216325 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "nagios-plugins-real": { + "versions": { + "2.2.1-r6": 1543933911 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-io-stringy-doc": { + "versions": { + "2.111-r1": 1542924626 + }, + "origin": "perl-io-stringy" + }, + "snappy": { + "versions": { + "1.1.7-r1": 1545208922 + }, + "origin": "snappy", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libsnappy.so.1=1.1.7" + ] + }, + "nagios-plugins-ifstatus": { + "versions": { + "2.2.1-r6": 1543933906 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "perl" + ] + }, + "qemu-aarch64": { + "versions": { + "3.1.0-r3": 1551107299 + }, + "origin": "qemu", + "provides": [ + "cmd:qemu-aarch64" + ] + }, + "newsboat": { + "versions": { + "2.13-r0": 1545075360 + }, + "origin": "newsboat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libgcc_s.so.1", + "so:libintl.so.8", + "so:libjson-c.so.4", + "so:libncursesw.so.6", + "so:libsqlite3.so.0", + "so:libstdc++.so.6", + "so:libstfl.so.0", + "so:libxml2.so.2" + ], + "provides": [ + "newsbeuter=2.13-r0", + "cmd:newsboat", + "cmd:podboat" + ] + }, + "qemu-system-mipsel": { + "versions": { + "3.1.0-r3": 1551107304 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libstdc++.so.6", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-mipsel" + ] + }, + "py3-bcrypt": { + "versions": { + "3.1.4-r0": 1545216386 + }, + "origin": "py-bcrypt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "ppp-dev": { + "versions": { + "2.4.7-r6": 1543999019 + }, + "origin": "ppp" + }, + "mg": { + "versions": { + "20180824-r0": 1545208623 + }, + "origin": "mg", + "dependencies": [ + "so:libbsd.so.0", + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:mg" + ] + }, + "perl-test-harness-utils": { + "versions": { + "3.42-r0": 1543238857 + }, + "origin": "perl-test-harness", + "dependencies": [ + "perl-test-harness", + "perl" + ], + "provides": [ + "cmd:prove" + ] + }, + "libgcrypt-dev": { + "versions": { + "1.8.4-r0": 1545838380 + }, + "origin": "libgcrypt", + "dependencies": [ + "libgpg-error-dev", + "libgcrypt=1.8.4-r0" + ], + "provides": [ + "cmd:libgcrypt-config" + ] + }, + "micro-tetris": { + "versions": { + "1.2.1-r0": 1545209639 + }, + "origin": "micro-tetris", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:tetris" + ] + }, + "subunit": { + "versions": { + "1.2.0-r0": 1543933439 + }, + "origin": "subunit", + "dependencies": [ + "python2" + ], + "provides": [ + "cmd:subunit-1to2", + "cmd:subunit-2to1", + "cmd:subunit-diff", + "cmd:subunit-filter", + "cmd:subunit-ls", + "cmd:subunit-notify", + "cmd:subunit-output", + "cmd:subunit-stats", + "cmd:subunit-tags", + "cmd:subunit2csv", + "cmd:subunit2disk", + "cmd:subunit2gtk", + "cmd:subunit2junitxml", + "cmd:subunit2pyunit", + "cmd:tap2subunit" + ] + }, + "perl-want": { + "versions": { + "0.29-r1": 1543932635 + }, + "origin": "perl-want", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "libc6-compat": { + "versions": { + "1.1.20-r4": 1552989415 + }, + "origin": "musl", + "dependencies": [ + "musl=1.1.20-r4" + ] + }, + "py2-flask-wtf": { + "versions": { + "0.14.2-r0": 1545213678 + }, + "origin": "py-flask-wtf", + "dependencies": [ + "py2-flask", + "py2-wtforms", + "python2" + ] + }, + "samba-winbind-krb5-locator": { + "versions": { + "4.8.11-r1": 1555334974 + }, + "origin": "samba", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libwbclient.so.0" + ] + }, + "mtu": { + "versions": { + "1.5-r2": 1544000081 + }, + "origin": "pingu", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:mtu" + ] + }, + "ppp-daemon": { + "versions": { + "2.4.7-r6": 1543999024 + }, + "origin": "ppp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:poff", + "cmd:pon", + "cmd:pppd", + "cmd:pppdump", + "cmd:pppstats" + ] + }, + "py-flup": { + "versions": { + "1.0.2-r0": 1545163559 + }, + "origin": "py-flup", + "dependencies": [ + "python2" + ] + }, + "xmlrpc-c-abyss": { + "versions": { + "1.39.13-r0": 1545665219 + }, + "origin": "xmlrpc-c", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxmlrpc.so.3", + "so:libxmlrpc_server.so.3", + "so:libxmlrpc_util.so.3" + ], + "provides": [ + "so:libxmlrpc_abyss.so.3=3.39", + "so:libxmlrpc_server_abyss.so.3=3.39" + ] + }, + "smartmontools-openrc": { + "versions": { + "6.6-r1": 1545163012 + }, + "origin": "smartmontools" + }, + "email-doc": { + "versions": { + "3.1.4-r7": 1543925853 + }, + "origin": "email" + }, + "perl-yaml-syck": { + "versions": { + "1.31-r0": 1545060527 + }, + "origin": "perl-yaml-syck", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "jwm-lang": { + "versions": { + "2.3.7-r0": 1545292615 + }, + "origin": "jwm" + }, + "cadaver": { + "versions": { + "0.23.3-r4": 1545076658 + }, + "origin": "cadaver", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libneon.so.27" + ], + "provides": [ + "cmd:cadaver" + ] + }, + "logrotate-openrc": { + "versions": { + "3.15.0-r0": 1545857314 + }, + "origin": "logrotate" + }, + "texinfo-doc": { + "versions": { + "6.5-r1": 1542300391 + }, + "origin": "texinfo" + }, + "terminus-font": { + "versions": { + "4.47-r0": 1546434641 + }, + "origin": "terminus-font" + }, + "debian-archive-keyring-doc": { + "versions": { + "2018.1-r0": 1543999031 + }, + "origin": "debian-archive-keyring" + }, + "ltrace": { + "versions": { + "0.7.3-r1": 1544798548 + }, + "origin": "ltrace", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libelf.so.0" + ], + "provides": [ + "cmd:ltrace" + ] + }, + "fail2ban-doc": { + "versions": { + "0.10.3.1-r2": 1545300387 + }, + "origin": "fail2ban" + }, + "perl-digest-hmac": { + "versions": { + "1.03-r0": 1542924833 + }, + "origin": "perl-digest-hmac", + "dependencies": [ + "perl", + "perl-digest-sha1" + ] + }, + "gtk-engines-clearlooks": { + "versions": { + "2.21.0-r2": 1545289336 + }, + "origin": "gtk-engines", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ] + }, + "libxmu-dev": { + "versions": { + "1.1.2-r1": 1542883345 + }, + "origin": "libxmu", + "dependencies": [ + "util-linux-dev", + "libxmu=1.1.2-r1", + "pc:x11", + "pc:xext", + "pc:xproto", + "pc:xt", + "pkgconfig" + ], + "provides": [ + "pc:xmu=1.1.2", + "pc:xmuu=1.1.2" + ] + }, + "boost-thread": { + "versions": { + "1.67.0-r2": 1542823634 + }, + "origin": "boost", + "dependencies": [ + "so:libboost_system-mt.so.1.67.0", + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_thread-mt.so.1.67.0=1.67.0" + ] + }, + "lua5.3-sqlite": { + "versions": { + "0.9.5-r2": 1545214288 + }, + "origin": "lua-sqlite", + "dependencies": [ + "lua5.3", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "py2-cparser": { + "versions": { + "2.19-r0": 1546791397 + }, + "origin": "py-cparser", + "dependencies": [ + "python2" + ] + }, + "perl-module-pluggable": { + "versions": { + "5.2-r0": 1542973009 + }, + "origin": "perl-module-pluggable" + }, + "perl-test-script-doc": { + "versions": { + "1.25-r0": 1545061104 + }, + "origin": "perl-test-script" + }, + "libspf2-dev": { + "versions": { + "1.2.10-r3": 1546267711 + }, + "origin": "libspf2", + "dependencies": [ + "libspf2=1.2.10-r3" + ] + }, + "perl-socket6": { + "versions": { + "0.28-r0": 1543932730 + }, + "origin": "perl-socket6", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "awall-masquerade": { + "versions": { + "1.6.9-r0": 1548501700 + }, + "origin": "awall", + "dependencies": [ + "awall" + ] + }, + "ircservices-doc": { + "versions": { + "5.1.24-r4": 1545069548 + }, + "origin": "ircservices" + }, + "tevent": { + "versions": { + "0.9.37-r1": 1543933087 + }, + "origin": "tevent", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libtalloc.so.2" + ], + "provides": [ + "so:libtevent.so.0=0.9.37" + ] + }, + "farstream0.1-doc": { + "versions": { + "0.1.2-r2": 1545069353 + }, + "origin": "farstream0.1" + }, + "libssh-dev": { + "versions": { + "0.7.6-r1": 1545300250 + }, + "origin": "libssh", + "dependencies": [ + "libssh=0.7.6-r1", + "pkgconfig" + ], + "provides": [ + "pc:libssh=0.7.6", + "pc:libssh_threads=0.7.6" + ] + }, + "libsigc++-dev": { + "versions": { + "2.10.0-r1": 1543077169 + }, + "origin": "libsigc++", + "dependencies": [ + "libsigc++=2.10.0-r1", + "pkgconfig" + ], + "provides": [ + "pc:sigc++-2.0=2.10.0" + ] + }, + "perl-plack-doc": { + "versions": { + "1.0033-r0": 1544792449 + }, + "origin": "perl-plack" + }, + "qemu-system-moxie": { + "versions": { + "3.1.0-r3": 1551107304 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-moxie" + ] + }, + "perl-test-notabs": { + "versions": { + "2.02-r0": 1545069439 + }, + "origin": "perl-test-notabs" + }, + "py2-certifi": { + "versions": { + "2018.4.16-r0": 1542825013 + }, + "origin": "py-certifi", + "dependencies": [ + "python2" + ] + }, + "augeas-static": { + "versions": { + "1.11.0-r0": 1542924580 + }, + "origin": "augeas" + }, + "py-olefile": { + "versions": { + "0.43-r2": 1542883726 + }, + "origin": "py-olefile" + }, + "device-mapper": { + "versions": { + "2.02.182-r0": 1543928934 + }, + "origin": "lvm2", + "dependencies": [ + "lvm2-libs=2.02.182-r0", + "so:libc.musl-x86_64.so.1", + "so:libdevmapper.so.1.02" + ], + "provides": [ + "cmd:dmsetup", + "cmd:dmstats" + ] + }, + "npth-dev": { + "versions": { + "1.6-r0": 1543932200 + }, + "origin": "npth", + "dependencies": [ + "npth=1.6-r0" + ], + "provides": [ + "cmd:npth-config" + ] + }, + "xf86-video-fbdev-doc": { + "versions": { + "0.5.0-r2": 1545067179 + }, + "origin": "xf86-video-fbdev" + }, + "perl-package-stash-xs-doc": { + "versions": { + "0.28-r4": 1542845714 + }, + "origin": "perl-package-stash-xs" + }, + "py2-py": { + "versions": { + "1.5.3-r0": 1542824877 + }, + "origin": "py-py", + "dependencies": [ + "python2" + ] + }, + "imagemagick-doc": { + "versions": { + "7.0.8.44-r0": 1557126216 + }, + "origin": "imagemagick" + }, + "fluxbox-doc": { + "versions": { + "1.3.7-r3": 1545165090 + }, + "origin": "fluxbox" + }, + "mesa": { + "versions": { + "18.1.7-r2": 1554455972 + }, + "origin": "mesa" + }, + "aconf-mod-dns-zone": { + "versions": { + "0.7.1-r0": 1553432497 + }, + "origin": "aconf", + "dependencies": [ + "aconf" + ] + }, + "py-zope-interface": { + "versions": { + "4.6.0-r0": 1548537850 + }, + "origin": "py-zope-interface" + }, + "po4a-lang": { + "versions": { + "0.52-r1": 1542304730 + }, + "origin": "po4a", + "dependencies": [ + "perl", + "gettext" + ] + }, + "perl-package-anon-doc": { + "versions": { + "0.05-r4": 1545116806 + }, + "origin": "perl-package-anon" + }, + "pssh-doc": { + "versions": { + "2.3.1-r1": 1545302362 + }, + "origin": "pssh" + }, + "wavpack-dev": { + "versions": { + "5.1.0-r7": 1548939905 + }, + "origin": "wavpack", + "dependencies": [ + "pkgconfig", + "wavpack=5.1.0-r7" + ], + "provides": [ + "pc:wavpack=5.1.0" + ] + }, + "nagios-plugins-dns": { + "versions": { + "2.2.1-r6": 1543933904 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "bind-tools", + "so:libc.musl-x86_64.so.1" + ] + }, + "perl-future": { + "versions": { + "0.39-r0": 1544799049 + }, + "origin": "perl-future" + }, + "nrpe-plugin": { + "versions": { + "3.2.1-r0": 1545249892 + }, + "origin": "nrpe", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:nrpe-uninstall" + ] + }, + "liboping-dev": { + "versions": { + "1.10.0-r0": 1545214321 + }, + "origin": "liboping", + "dependencies": [ + "liboping=1.10.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:liboping=1.10.0" + ] + }, + "memcached": { + "versions": { + "1.5.12-r0": 1543923567 + }, + "origin": "memcached", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libsasl2.so.3", + "so:libseccomp.so.2" + ], + "provides": [ + "cmd:memcached" + ] + }, + "xorg-cf-files-doc": { + "versions": { + "1.0.6-r0": 1543927847 + }, + "origin": "xorg-cf-files" + }, + "elfutils-dev": { + "versions": { + "0.168-r2": 1546849158 + }, + "origin": "elfutils", + "dependencies": [ + "elfutils-libelf=0.168-r2" + ] + }, + "nagios-plugins-ide_smart": { + "versions": { + "2.2.1-r6": 1543933906 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "readline": { + "versions": { + "7.0.003-r1": 1542301065 + }, + "origin": "readline", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "so:libreadline.so.7=7.0" + ] + }, + "opus": { + "versions": { + "1.3-r0": 1545069004 + }, + "origin": "opus", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libopus.so.0=0.7.0" + ] + }, + "nodejs-doc": { + "versions": { + "10.14.2-r0": 1545345077 + }, + "origin": "nodejs" + }, + "cyrus-sasl-openrc": { + "versions": { + "2.1.27-r1": 1550353527 + }, + "origin": "cyrus-sasl" + }, + "perl-git-svn": { + "versions": { + "2.20.1-r0": 1545214194 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0" + ] + }, + "perl-dbix-dbschema": { + "versions": { + "0.39-r0": 1543998795 + }, + "origin": "perl-dbix-dbschema", + "dependencies": [ + "perl", + "perl-dbi" + ] + }, + "py2-pluggy": { + "versions": { + "0.7.1-r0": 1542824892 + }, + "origin": "py-pluggy", + "dependencies": [ + "python2" + ] + }, + "lxdm": { + "versions": { + "0.5.3-r1": 1545215914 + }, + "origin": "lxdm", + "dependencies": [ + "bash", + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libck-connector.so.0", + "so:libdbus-1.so.3", + "so:libgdk-x11-2.0.so.0", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0", + "so:libintl.so.8", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libxcb.so.1" + ], + "provides": [ + "cmd:lxdm", + "cmd:lxdm-binary", + "cmd:lxdm-config" + ] + }, + "uwsgi-python": { + "versions": { + "2.0.17.1-r0": 1545062205 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "perl-module-versions-report-doc": { + "versions": { + "1.06-r0": 1545067095 + }, + "origin": "perl-module-versions-report" + }, + "libressl2.7-libtls": { + "versions": { + "2.7.5-r0": 1551116832 + }, + "origin": "libressl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.43", + "so:libssl.so.45" + ], + "provides": [ + "so:libtls.so.17=17.0.1" + ] + }, + "libunique": { + "versions": { + "1.1.6-r6": 1545299842 + }, + "origin": "libunique", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libdbus-glib-1.so.2", + "so:libgdk-x11-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-x11-2.0.so.0" + ], + "provides": [ + "so:libunique-1.0.so.0=0.100.6" + ] + }, + "liblognorm-dev": { + "versions": { + "2.0.6-r1": 1548783946 + }, + "origin": "liblognorm", + "dependencies": [ + "libestr-dev", + "liblognorm=2.0.6-r1", + "pc:libfastjson", + "pkgconfig" + ], + "provides": [ + "pc:lognorm=2.0.6" + ] + }, + "fcgiwrap-doc": { + "versions": { + "1.1.0-r3": 1545665779 + }, + "origin": "fcgiwrap" + }, + "libstdc++": { + "versions": { + "8.3.0-r0": 1554745497 + }, + "origin": "gcc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libstdc++.so.6=6.0.25" + ] + }, + "abuild-rootbld": { + "versions": { + "3.3.1-r0": 1551786762 + }, + "origin": "abuild", + "dependencies": [ + "abuild", + "bubblewrap", + "gettext", + "git" + ] + }, + "iproute2": { + "versions": { + "4.19.0-r0": 1543223212 + }, + "origin": "iproute2", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libelf.so.0", + "so:libxtables.so.12" + ], + "provides": [ + "cmd:bridge", + "cmd:ctstat", + "cmd:genl", + "cmd:ifcfg", + "cmd:ifstat", + "cmd:ip", + "cmd:lnstat", + "cmd:nstat", + "cmd:routef", + "cmd:routel", + "cmd:rtacct", + "cmd:rtmon", + "cmd:rtpr", + "cmd:rtstat", + "cmd:ss", + "cmd:tc" + ] + }, + "pstree-doc": { + "versions": { + "2.39-r0": 1543926517 + }, + "origin": "pstree" + }, + "py3-pathlib2": { + "versions": { + "2.3.2-r0": 1542824911 + }, + "origin": "py-pathlib2", + "dependencies": [ + "py3-six", + "py3-scandir", + "python3" + ] + }, + "libcdio-paranoia-dev": { + "versions": { + "0.94_p1-r1": 1543248403 + }, + "origin": "libcdio-paranoia", + "dependencies": [ + "libcdio-paranoia=0.94_p1-r1", + "pc:libcdio", + "pkgconfig" + ] + }, + "sngtc_client-dev": { + "versions": { + "1.3.7-r1": 1545117158 + }, + "origin": "sngtc_client", + "dependencies": [ + "sngtc_client" + ] + }, + "postgresql-plperl": { + "versions": { + "11.2-r0": 1554274176 + }, + "origin": "postgresql", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libperl.so" + ] + }, + "perl-variable-magic-doc": { + "versions": { + "0.62-r0": 1542973100 + }, + "origin": "perl-variable-magic" + }, + "lua5.3-sircbot": { + "versions": { + "0.4-r2": 1543924833 + }, + "origin": "sircbot", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "galculator-doc": { + "versions": { + "2.1.4-r0": 1545076844 + }, + "origin": "galculator" + }, + "sdl_image": { + "versions": { + "1.2.12-r4": 1545302048 + }, + "origin": "sdl_image", + "dependencies": [ + "so:libSDL-1.2.so.0", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libSDL_image-1.2.so.0=0.8.4" + ] + }, + "espeak-dev": { + "versions": { + "1.48.04-r1": 1542973182 + }, + "origin": "espeak", + "dependencies": [ + "espeak=1.48.04-r1" + ] + }, + "squid-lang-de": { + "versions": { + "4.4-r1": 1545216324 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "perl-text-soundex-doc": { + "versions": { + "3.05-r0": 1543081697 + }, + "origin": "perl-text-soundex" + }, + "liblastfm": { + "versions": { + "1.0.9-r1": 1545207824 + }, + "origin": "liblastfm", + "dependencies": [ + "so:libQtCore.so.4", + "so:libQtDBus.so.4", + "so:libQtNetwork.so.4", + "so:libQtSql.so.4", + "so:libQtXml.so.4", + "so:libc.musl-x86_64.so.1", + "so:libfftw3f.so.3", + "so:libgcc_s.so.1", + "so:libsamplerate.so.0", + "so:libstdc++.so.6" + ], + "provides": [ + "so:liblastfm.so.1=1.0.9", + "so:liblastfm_fingerprint.so.1=1.0.9" + ] + }, + "haserl-lua5.1": { + "versions": { + "0.9.35-r1": 1542883797 + }, + "origin": "haserl", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblua.so.5" + ], + "provides": [ + "cmd:haserl-lua5.1" + ] + }, + "py3-click": { + "versions": { + "6.7-r2": 1545116990 + }, + "origin": "py-click", + "dependencies": [ + "python3" + ] + }, + "usbutils-doc": { + "versions": { + "010-r0": 1545075125 + }, + "origin": "usbutils" + }, + "libburn": { + "versions": { + "1.5.0-r0": 1545837389 + }, + "origin": "libburn", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libburn.so.4=4.103.0", + "cmd:cdrskin" + ] + }, + "gtk-update-icon-cache": { + "versions": { + "2.24.32-r1": 1543925514 + }, + "origin": "gtk+2.0", + "dependencies": [ + "hicolor-icon-theme", + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8" + ], + "provides": [ + "cmd:gtk-update-icon-cache" + ] + }, + "perl-extutils-cchecker": { + "versions": { + "0.10-r0": 1542883384 + }, + "origin": "perl-extutils-cchecker", + "dependencies": [ + "perl-test-exception" + ] + }, + "libfprint": { + "versions": { + "0.7.0-r1": 1545207879 + }, + "origin": "libfprint", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libnss3.so", + "so:libpixman-1.so.0", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libfprint.so.0=0.0.0" + ] + }, + "d-feet": { + "versions": { + "0.3.13-r1": 1545062610 + }, + "origin": "d-feet", + "dependencies": [ + "python2", + "py-gobject3" + ], + "provides": [ + "cmd:d-feet" + ] + }, + "perl-unix-syslog-doc": { + "versions": { + "1.1-r11": 1545067185 + }, + "origin": "perl-unix-syslog" + }, + "libwebsockets": { + "versions": { + "3.1.0-r0": 1545856933 + }, + "origin": "libwebsockets", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libssl.so.1.1" + ], + "provides": [ + "so:libwebsockets.so.14=14" + ] + }, + "bridge-utils": { + "versions": { + "1.6-r0": 1543935360 + }, + "origin": "bridge-utils", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:brctl" + ] + }, + "bluez-doc": { + "versions": { + "5.50-r0": 1545822205 + }, + "origin": "bluez" + }, + "gtk+3.0-doc": { + "versions": { + "3.24.1-r0": 1543927434 + }, + "origin": "gtk+3.0" + }, + "talloc": { + "versions": { + "2.1.14-r0": 1543220641 + }, + "origin": "talloc", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libtalloc.so.2=2.1.14" + ] + }, + "cairomm-dev": { + "versions": { + "1.12.2-r0": 1543077181 + }, + "origin": "cairomm", + "dependencies": [ + "libsigc++-dev", + "cairomm=1.12.2-r0", + "pc:cairo-ft", + "pc:cairo-pdf", + "pc:cairo-png", + "pc:cairo-ps", + "pc:cairo-svg", + "pc:cairo-xlib", + "pc:cairo-xlib-xrender", + "pc:cairo>=1.10.0", + "pc:sigc++-2.0>=2.5.1", + "pkgconfig" + ], + "provides": [ + "pc:cairomm-1.0=1.12.2", + "pc:cairomm-ft-1.0=1.12.2", + "pc:cairomm-pdf-1.0=1.12.2", + "pc:cairomm-png-1.0=1.12.2", + "pc:cairomm-ps-1.0=1.12.2", + "pc:cairomm-svg-1.0=1.12.2", + "pc:cairomm-xlib-1.0=1.12.2", + "pc:cairomm-xlib-xrender-1.0=1.12.2" + ] + }, + "aspell": { + "versions": { + "0.60.6.1-r13": 1542965830 + }, + "origin": "aspell", + "dependencies": [ + "so:libaspell.so.15", + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libncursesw.so.6", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:aspell" + ] + }, + "libcdio-doc": { + "versions": { + "0.94-r2": 1543248376 + }, + "origin": "libcdio" + }, + "py-unidecode": { + "versions": { + "1.0.23-r0": 1545299544 + }, + "origin": "py-unidecode", + "dependencies": [ + "py3-unidecode", + "py3-unidecode=1.0.23-r0" + ] + }, + "mcpp-libs": { + "versions": { + "2.7.2-r1": 1542883471 + }, + "origin": "mcpp", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libmcpp.so.0=0.3.0" + ] + }, + "perl-yaml-libyaml": { + "versions": { + "0.72-r0": 1545061179 + }, + "origin": "perl-yaml-libyaml", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "perl-yaml-xs=0.72-r0" + ] + }, + "lua5.1-bit32": { + "versions": { + "5.3.0-r2": 1546011988 + }, + "origin": "lua-bit32", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1" + ] + }, + "libmtp-dev": { + "versions": { + "1.1.15-r0": 1543924440 + }, + "origin": "libmtp", + "dependencies": [ + "libusb-compat-dev", + "libmtp=1.1.15-r0", + "pc:libusb-1.0", + "pkgconfig" + ], + "provides": [ + "pc:libmtp=1.1.15" + ] + }, + "lua5.1-rex-posix": { + "versions": { + "2.9.0-r0": 1545209201 + }, + "origin": "lua-rex", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "acf-mariadb": { + "versions": { + "0.2.0-r2": 1543998687 + }, + "origin": "acf-mariadb", + "dependencies": [ + "acf-core", + "mariadb", + "mariadb-client", + "lua-sql-mysql", + "acf-db-lib" + ] + }, + "sdl2-dev": { + "versions": { + "2.0.9-r0": 1543934457 + }, + "origin": "sdl2", + "dependencies": [ + "directfb-dev", + "pkgconfig", + "sdl2=2.0.9-r0" + ], + "provides": [ + "pc:sdl2=2.0.9", + "cmd:sdl2-config" + ] + }, + "py-funcsigs": { + "versions": { + "1.0.2-r1": 1542824900 + }, + "origin": "py-funcsigs" + }, + "xdg-utils-doc": { + "versions": { + "1.1.3-r0": 1545154524 + }, + "origin": "xdg-utils" + }, + "openssl-dbg": { + "versions": { + "1.1.1b-r1": 1552660099 + }, + "origin": "openssl" + }, + "qemu-system-ppc": { + "versions": { + "3.1.0-r3": 1551107304 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusb-1.0.so.0", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-ppc" + ] + }, + "samba-dev": { + "versions": { + "4.8.11-r1": 1555334971 + }, + "origin": "samba", + "dependencies": [ + "libsmbclient=4.8.11-r1", + "libwbclient=4.8.11-r1", + "pc:ldb", + "pc:talloc", + "pc:tevent", + "pkgconfig", + "samba-client-libs=4.8.11-r1", + "samba-common-libs=4.8.11-r1", + "samba-common-server-libs=4.8.11-r1", + "samba-dc-libs=4.8.11-r1", + "samba-libnss-winbind=4.8.11-r1", + "samba-libs=4.8.11-r1" + ], + "provides": [ + "pc:dcerpc=0.0.1", + "pc:dcerpc_samr=0.0.1", + "pc:dcerpc_server=0.0.1", + "pc:ndr=0.1.0", + "pc:ndr_krb5pac=0.0.1", + "pc:ndr_nbt=0.0.1", + "pc:ndr_standard=0.0.1", + "pc:netapi=0", + "pc:samba-credentials=0.0.1", + "pc:samba-hostconfig=0.0.1", + "pc:samba-policy=0.0.1", + "pc:samba-util=0.0.1", + "pc:samdb=0.0.1", + "pc:smbclient=0.3.1", + "pc:wbclient=0.14" + ] + }, + "rsyslog-mmsequence": { + "versions": { + "8.40.0-r3": 1548686791 + }, + "origin": "rsyslog", + "dependencies": [ + "rsyslog=8.40.0-r3", + "so:libc.musl-x86_64.so.1" + ] + }, + "radvd": { + "versions": { + "2.17-r2": 1545254223 + }, + "origin": "radvd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:radvd", + "cmd:radvdump" + ] + }, + "tumbler": { + "versions": { + "0.2.3-r0": 1545859329 + }, + "origin": "tumbler", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libcurl.so.4", + "so:libfreetype.so.6", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libintl.so.8", + "so:libjpeg.so.8", + "so:libpng16.so.16", + "so:libpoppler-glib.so.8" + ], + "provides": [ + "so:libtumbler-1.so.0=0.0.0" + ] + }, + "perl-log-dispatch": { + "versions": { + "2.68-r0": 1545209654 + }, + "origin": "perl-log-dispatch", + "dependencies": [ + "perl-module-runtime", + "perl-params-validate", + "perl-dist-checkconflicts", + "perl-devel-globaldestruction" + ] + }, + "qemu-system-or1k": { + "versions": { + "3.1.0-r3": 1551107304 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-or1k" + ] + }, + "nettle": { + "versions": { + "3.4.1-r0": 1544791824 + }, + "origin": "nettle", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10" + ], + "provides": [ + "so:libhogweed.so.4=4.5", + "so:libnettle.so.6=6.5" + ] + }, + "libtls-standalone-dev": { + "versions": { + "2.7.4-r6": 1546784623 + }, + "origin": "libtls-standalone", + "dependencies": [ + "openssl-dev", + "libtls-standalone=2.7.4-r6", + "pc:libcrypto", + "pc:libssl", + "pkgconfig" + ], + "provides": [ + "pc:libtls-standalone=2.7.4" + ] + }, + "rtnppd": { + "versions": { + "1.7b-r8": 1543935345 + }, + "origin": "rtnppd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:rtnppd", + "cmd:testq", + "cmd:tnpppage" + ] + }, + "mp3splt": { + "versions": { + "2.6.2-r0": 1545207912 + }, + "origin": "mp3splt", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libmp3splt.so.0" + ], + "provides": [ + "cmd:mp3splt", + "cmd:oggsplt" + ] + }, + "json-c-dev": { + "versions": { + "0.13.1-r0": 1543923376 + }, + "origin": "json-c", + "dependencies": [ + "json-c=0.13.1-r0", + "pkgconfig" + ], + "provides": [ + "pc:json-c=0.13.1" + ] + }, + "libogg-doc": { + "versions": { + "1.3.3-r2": 1543925775 + }, + "origin": "libogg" + }, + "kbd-doc": { + "versions": { + "2.0.4-r3": 1545215189 + }, + "origin": "kbd" + }, + "lua5.3-stdlib": { + "versions": { + "41.2.1-r0": 1545292925 + }, + "origin": "lua-stdlib" + }, + "libunwind-doc": { + "versions": { + "1.2.1-r3": 1545915765 + }, + "origin": "libunwind" + }, + "xprop-doc": { + "versions": { + "1.2.3-r0": 1543254086 + }, + "origin": "xprop" + }, + "perl-test-sharedfork": { + "versions": { + "0.35-r0": 1543249824 + }, + "origin": "perl-test-sharedfork", + "dependencies": [ + "perl", + "perl-test-requires" + ] + }, + "uwsgi-stats_pusher_statsd": { + "versions": { + "2.0.17.1-r0": 1545062211 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "cksfv-doc": { + "versions": { + "1.3.14-r4": 1545069444 + }, + "origin": "cksfv" + }, + "py-ttystatus": { + "versions": { + "0.23-r1": 1545060579 + }, + "origin": "py-ttystatus", + "dependencies": [ + "python2" + ] + }, + "tinyxml-dev": { + "versions": { + "2.6.2-r1": 1545292911 + }, + "origin": "tinyxml", + "dependencies": [ + "tinyxml=2.6.2-r1" + ] + }, + "libsecret-doc": { + "versions": { + "0.18.6-r0": 1545291103 + }, + "origin": "libsecret" + }, + "perl-string-shellquote": { + "versions": { + "1.04-r0": 1545302152 + }, + "origin": "perl-string-shellquote", + "provides": [ + "cmd:shell-quote" + ] + }, + "boost-doc": { + "versions": { + "1.67.0-r2": 1542823630 + }, + "origin": "boost" + }, + "linux-firmware-qcom": { + "versions": { + "20190322-r0": 1554980649 + }, + "origin": "linux-firmware", + "provides": [ + "linux-firmware-any" + ] + }, + "libexecinfo": { + "versions": { + "1.1-r0": 1545117123 + }, + "origin": "libexecinfo", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libexecinfo.so.1=1" + ] + }, + "xf86-video-vmware": { + "versions": { + "13.3.0-r0": 1545076717 + }, + "origin": "xf86-video-vmware", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdrm.so.2", + "so:libudev.so.1", + "so:libxatracker.so.2" + ] + }, + "sed-doc": { + "versions": { + "4.5-r0": 1542822960 + }, + "origin": "sed" + }, + "farstream0.1-dev": { + "versions": { + "0.1.2-r2": 1545069353 + }, + "origin": "farstream0.1", + "dependencies": [ + "libnice-dev", + "gstreamer0.10-dev", + "gst-plugins-base0.10-dev", + "farstream0.1=0.1.2-r2", + "pc:gstreamer-0.10", + "pc:gstreamer-base-0.10", + "pkgconfig" + ], + "provides": [ + "pc:farstream-0.1=0.1.2" + ] + }, + "py3-libxml2": { + "versions": { + "2.9.9-r1": 1551105514 + }, + "origin": "libxml2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0", + "so:libxml2.so.2" + ] + }, + "statserial": { + "versions": { + "1.1-r4": 1545293154 + }, + "origin": "statserial", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6" + ], + "provides": [ + "cmd:statserial" + ] + }, + "perl-type-tiny-doc": { + "versions": { + "1.000006-r0": 1542973080 + }, + "origin": "perl-type-tiny" + }, + "perl-class-inspector": { + "versions": { + "1.32-r0": 1543238829 + }, + "origin": "perl-class-inspector", + "dependencies": [ + "perl" + ] + }, + "lmdb-dev": { + "versions": { + "0.9.23-r0": 1547025175 + }, + "origin": "lmdb", + "dependencies": [ + "lmdb=0.9.23-r0", + "pkgconfig" + ] + }, + "libxml2-utils": { + "versions": { + "2.9.9-r1": 1551105514 + }, + "origin": "libxml2", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libxml2.so.2" + ], + "provides": [ + "cmd:xmlcatalog", + "cmd:xmllint" + ] + }, + "obex-data-server": { + "versions": { + "0.4.6-r4": 1545069104 + }, + "origin": "obex-data-server", + "dependencies": [ + "so:libbluetooth.so.3", + "so:libc.musl-x86_64.so.1", + "so:libdbus-glib-1.so.2", + "so:libgdk_pixbuf-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libopenobex.so.2", + "so:libusb-0.1.so.4" + ], + "provides": [ + "cmd:obex-data-server" + ] + }, + "libdvdread-dev": { + "versions": { + "6.0.0-r0": 1545838475 + }, + "origin": "libdvdread", + "dependencies": [ + "libdvdread=6.0.0-r0", + "pc:libdvdcss>=1.2", + "pkgconfig" + ], + "provides": [ + "pc:dvdread=6.0.0" + ] + }, + "lynx": { + "versions": { + "2.8.8_p2-r8": 1545154518 + }, + "origin": "lynx", + "dependencies": [ + "gzip", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1", + "so:libncursesw.so.6", + "so:libssl.so.1.1" + ], + "provides": [ + "cmd:lynx" + ] + }, + "clamav-dev": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav", + "dependencies": [ + "openssl-dev", + "clamav-libs=0.100.3-r0", + "pkgconfig" + ], + "provides": [ + "pc:libclamav=0.100.3", + "cmd:clamav-config" + ] + }, + "py-django": { + "versions": { + "1.11.20-r0": 1551267364 + }, + "origin": "py-django", + "dependencies": [ + "py-tz", + "py3-django=1.11.20-r0" + ] + }, + "libsrtp": { + "versions": { + "1.5.4-r1": 1544000954 + }, + "origin": "libsrtp" + }, + "krb5-libs": { + "versions": { + "1.15.5-r0": 1548094458 + }, + "origin": "krb5", + "dependencies": [ + "krb5-conf", + "so:libc.musl-x86_64.so.1", + "so:libcom_err.so.2", + "so:libcrypto.so.1.1", + "so:libkeyutils.so.1", + "so:libssl.so.1.1", + "so:libverto.so.1" + ], + "provides": [ + "so:libgssapi_krb5.so.2=2.2", + "so:libgssrpc.so.4=4.2", + "so:libk5crypto.so.3=3.1", + "so:libkadm5clnt_mit.so.11=11.0", + "so:libkadm5srv_mit.so.11=11.0", + "so:libkdb5.so.8=8.0", + "so:libkrad.so.0=0.0", + "so:libkrb5.so.3=3.3", + "so:libkrb5support.so.0=0.1" + ] + }, + "terminus-font-doc": { + "versions": { + "4.47-r0": 1546434641 + }, + "origin": "terminus-font" + }, + "avahi-lang": { + "versions": { + "0.7-r1": 1543925312 + }, + "origin": "avahi" + }, + "dhcp-dbg": { + "versions": { + "4.4.1-r1": 1543928453 + }, + "origin": "dhcp" + }, + "squid-lang-nl": { + "versions": { + "4.4-r1": 1545216328 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "uwsgi-tuntap": { + "versions": { + "2.0.17.1-r0": 1545062213 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "py-twisted-doc": { + "versions": { + "17.1.0-r1": 1546513851 + }, + "origin": "py-twisted" + }, + "rtapd-dbg": { + "versions": { + "1.7-r6": 1545301051 + }, + "origin": "rtapd", + "dependencies": [ + "rtnppd" + ] + }, + "db-utils": { + "versions": { + "5.3.28-r1": 1542819053 + }, + "origin": "db", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libdb-5.3.so" + ], + "provides": [ + "cmd:db_archive", + "cmd:db_checkpoint", + "cmd:db_deadlock", + "cmd:db_dump", + "cmd:db_hotbackup", + "cmd:db_load", + "cmd:db_log_verify", + "cmd:db_printlog", + "cmd:db_recover", + "cmd:db_replicate", + "cmd:db_stat", + "cmd:db_tuner", + "cmd:db_upgrade", + "cmd:db_verify" + ] + }, + "s6-linux-init-dev": { + "versions": { + "0.4.0.0-r0": 1545292929 + }, + "origin": "s6-linux-init" + }, + "libcddb-dev": { + "versions": { + "1.3.2-r3": 1543248357 + }, + "origin": "libcddb", + "dependencies": [ + "libcddb=1.3.2-r3", + "pkgconfig" + ], + "provides": [ + "pc:libcddb=1.3.2" + ] + }, + "py-dbus-dev": { + "versions": { + "1.2.8-r1": 1543925184 + }, + "origin": "py-dbus", + "dependencies": [ + "py-dbus", + "pc:dbus-1>=1.0", + "pkgconfig" + ], + "provides": [ + "pc:dbus-python=1.2.8" + ] + }, + "xfontsel": { + "versions": { + "1.0.6-r0": 1545215013 + }, + "origin": "xfontsel", + "dependencies": [ + "so:libX11.so.6", + "so:libXaw.so.7", + "so:libXmu.so.6", + "so:libXt.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xfontsel" + ] + }, + "postgresql-bdr-client": { + "versions": { + "9.4.14_p1-r1": 1543223164 + }, + "origin": "postgresql-bdr", + "dependencies": [ + "bash", + "libpq", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5", + "so:libreadline.so.7", + "so:libssl.so.1.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:clusterdb", + "cmd:createdb", + "cmd:createuser", + "cmd:dropdb", + "cmd:dropuser", + "cmd:pg_basebackup", + "cmd:pg_dump", + "cmd:pg_dumpall", + "cmd:pg_isready", + "cmd:pg_recvlogical", + "cmd:pg_restore", + "cmd:psql", + "cmd:reindexdb", + "cmd:vacuumdb" + ] + }, + "py2-pylast": { + "versions": { + "2.4.0-r0": 1545301101 + }, + "origin": "py-pylast", + "dependencies": [ + "python2" + ] + }, + "kamailio-json": { + "versions": { + "5.2.0-r1": 1546423171 + }, + "origin": "kamailio", + "dependencies": [ + "kamailio", + "so:libc.musl-x86_64.so.1", + "so:libevent-2.1.so.6", + "so:libjson-c.so.4", + "so:libsrutils.so.1" + ] + }, + "dbus-doc": { + "versions": { + "1.10.24-r1": 1542824378 + }, + "origin": "dbus" + }, + "pingu": { + "versions": { + "1.5-r2": 1544000081 + }, + "origin": "pingu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libev.so.4" + ], + "provides": [ + "cmd:pingu", + "cmd:pinguctl" + ] + }, + "librsync-doc": { + "versions": { + "2.0.2-r1": 1543932723 + }, + "origin": "librsync" + }, + "libvorbis-doc": { + "versions": { + "1.3.6-r2": 1549615842 + }, + "origin": "libvorbis" + }, + "libmpeg2-dev": { + "versions": { + "0.5.1-r8": 1544000975 + }, + "origin": "libmpeg2", + "dependencies": [ + "libmpeg2=0.5.1-r8", + "pkgconfig" + ], + "provides": [ + "pc:libmpeg2=0.5.1", + "pc:libmpeg2convert=0.5.1" + ] + }, + "libunistring-dev": { + "versions": { + "0.9.10-r0": 1542304035 + }, + "origin": "libunistring", + "dependencies": [ + "libunistring=0.9.10-r0" + ] + }, + "lua5.2-sql-sqlite3": { + "versions": { + "2.3.5-r2": 1543924402 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.2", + "so:libc.musl-x86_64.so.1", + "so:libsqlite3.so.0" + ] + }, + "perl-package-stash-xs": { + "versions": { + "0.28-r4": 1542845717 + }, + "origin": "perl-package-stash-xs", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ] + }, + "samba-server-libs": { + "versions": { + "4.8.11-r1": 1555334975 + }, + "origin": "samba", + "dependencies": [ + "so:libMESSAGING-SEND-samba4.so", + "so:libc.musl-x86_64.so.1", + "so:libdbwrap-samba4.so", + "so:libdcerpc-binding.so.0", + "so:libldap-2.4.so.2", + "so:libmessages-dgm-samba4.so", + "so:libmessages-util-samba4.so", + "so:libndr-samba-samba4.so", + "so:libndr-samba4.so", + "so:libndr.so.0", + "so:libsamba-debug-samba4.so", + "so:libsamba-errors.so.1", + "so:libsamba-hostconfig.so.0", + "so:libsamba-modules-samba4.so", + "so:libsamba-passdb.so.0", + "so:libsamba-security-samba4.so", + "so:libsamba-util.so.0", + "so:libsamba3-util-samba4.so", + "so:libsecrets3-samba4.so", + "so:libserver-id-db-samba4.so", + "so:libsmbconf.so.0", + "so:libsmbd-base-samba4.so", + "so:libsmbldap.so.2", + "so:libsmbldaphelper-samba4.so", + "so:libtalloc-report-samba4.so", + "so:libtalloc.so.2", + "so:libtevent-util.so.0", + "so:libtevent.so.0", + "so:libutil-tdb-samba4.so" + ], + "provides": [ + "so:libMESSAGING-samba4.so=0", + "so:libdcerpc-samba4.so=0", + "so:libidmap-samba4.so=0", + "so:libnon-posix-acls-samba4.so=0", + "so:libnss-info-samba4.so=0" + ] + }, + "meson-doc": { + "versions": { + "0.48.2-r0": 1544791717 + }, + "origin": "meson" + }, + "squid-lang-ka": { + "versions": { + "4.4-r1": 1545216327 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "libvorbis": { + "versions": { + "1.3.6-r2": 1549615842 + }, + "origin": "libvorbis", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libogg.so.0" + ], + "provides": [ + "so:libvorbis.so.0=0.4.8", + "so:libvorbisenc.so.2=2.0.11", + "so:libvorbisfile.so.3=3.3.7" + ] + }, + "dnstop-doc": { + "versions": { + "20140915-r3": 1545165119 + }, + "origin": "dnstop" + }, + "perl-ipc-system-simple": { + "versions": { + "1.25-r0": 1542924670 + }, + "origin": "perl-ipc-system-simple" + }, + "nagios-plugins-mysql": { + "versions": { + "2.2.1-r6": 1543933908 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "libc-utils": { + "versions": { + "0.7.1-r0": 1542302753 + }, + "origin": "libc-dev", + "dependencies": [ + "musl-utils" + ] + }, + "rabbitmq-c-doc": { + "versions": { + "0.8.0-r5": 1543923901 + }, + "origin": "rabbitmq-c" + }, + "boost-regex": { + "versions": { + "1.67.0-r2": 1542823633 + }, + "origin": "boost", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1" + ], + "provides": [ + "so:libboost_regex-mt.so.1.67.0=1.67.0", + "so:libboost_regex.so.1.67.0=1.67.0" + ] + }, + "nagios-plugins-disk": { + "versions": { + "2.2.1-r6": 1543933904 + }, + "origin": "nagios-plugins", + "dependencies": [ + "nagios-plugins", + "so:libc.musl-x86_64.so.1" + ] + }, + "graphviz-dev": { + "versions": { + "2.40.1-r1": 1543926740 + }, + "origin": "graphviz", + "dependencies": [ + "zlib-dev", + "libpng-dev", + "libjpeg-turbo-dev", + "expat-dev", + "fontconfig-dev", + "libsm-dev", + "libxext-dev", + "cairo-dev", + "pango-dev", + "librsvg-dev", + "gmp-dev", + "freetype-dev", + "graphviz=2.40.1-r1", + "pkgconfig" + ], + "provides": [ + "pc:libcdt=2.40.1", + "pc:libcgraph=2.40.1", + "pc:libgvc=2.40.1", + "pc:libgvpr=2.40.1", + "pc:liblab_gamut=2.40.1", + "pc:libpathplan=2.40.1", + "pc:libxdot=2.40.1" + ] + }, + "perl-inline-doc": { + "versions": { + "0.80-r1": 1545067070 + }, + "origin": "perl-inline" + }, + "perl-sub-exporter": { + "versions": { + "0.987-r0": 1542883253 + }, + "origin": "perl-sub-exporter", + "dependencies": [ + "perl-data-optlist", + "perl-sub-install", + "perl-params-util" + ] + }, + "lua-discount": { + "versions": { + "1.2.10.1-r4": 1545209125 + }, + "origin": "lua-discount" + }, + "mtools": { + "versions": { + "4.0.23-r0": 1545858117 + }, + "origin": "mtools", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:amuFormat.sh", + "cmd:lz", + "cmd:mattrib", + "cmd:mbadblocks", + "cmd:mcat", + "cmd:mcd", + "cmd:mcheck", + "cmd:mclasserase", + "cmd:mcomp", + "cmd:mcopy", + "cmd:mdel", + "cmd:mdeltree", + "cmd:mdir", + "cmd:mdu", + "cmd:mformat", + "cmd:minfo", + "cmd:mkmanifest", + "cmd:mlabel", + "cmd:mmd", + "cmd:mmount", + "cmd:mmove", + "cmd:mpartition", + "cmd:mrd", + "cmd:mren", + "cmd:mshortname", + "cmd:mshowfat", + "cmd:mtools", + "cmd:mtoolstest", + "cmd:mtype", + "cmd:mxtar", + "cmd:mzip", + "cmd:tgz", + "cmd:uz" + ] + }, + "libnotify": { + "versions": { + "0.7.7-r2": 1544001122 + }, + "origin": "libnotify", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgdk_pixbuf-2.0.so.0", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0" + ], + "provides": [ + "so:libnotify.so.4=4.0.0", + "cmd:notify-send" + ] + }, + "glib-doc": { + "versions": { + "2.58.1-r2": 1545290824 + }, + "origin": "glib" + }, + "libusb": { + "versions": { + "1.0.22-r0": 1543223279 + }, + "origin": "libusb", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libusb-1.0.so.0=0.1.0" + ] + }, + "py-gobject": { + "versions": { + "2.28.7-r0": 1543927528 + }, + "origin": "py-gobject", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libffi.so.6", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0" + ], + "provides": [ + "so:libpyglib-2.0-python.so.0=0.0.0", + "cmd:pygobject-codegen-2.0" + ] + }, + "slang": { + "versions": { + "2.3.2-r0": 1543924691 + }, + "origin": "slang", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpcre.so.1" + ], + "provides": [ + "so:libslang.so.2=2.3.2", + "cmd:slsh" + ] + }, + "cups-filters-doc": { + "versions": { + "1.21.6-r0": 1545820385 + }, + "origin": "cups-filters" + }, + "samba-libs": { + "versions": { + "4.8.11-r1": 1555334977 + }, + "origin": "samba", + "dependencies": [ + "so:libasn1-samba4.so.8", + "so:libc.musl-x86_64.so.1", + "so:libcap.so.2", + "so:libcom_err.so.2", + "so:libgnutls.so.30", + "so:libgssapi-samba4.so.2", + "so:libkrb5-samba4.so.26", + "so:liblber-2.4.so.2", + "so:libldap-2.4.so.2", + "so:libldb.so.1", + "so:libpopt.so.0", + "so:libreplace-samba4.so", + "so:libtalloc.so.2", + "so:libtdb.so.1", + "so:libtevent.so.0", + "so:libwbclient.so.0", + "so:libz.so.1" + ], + "provides": [ + "so:libCHARSET3-samba4.so=0", + "so:libaddns-samba4.so=0", + "so:libasn1util-samba4.so=0", + "so:libauthkrb5-samba4.so=0", + "so:libcli-cldap-samba4.so=0", + "so:libcli-ldap-common-samba4.so=0", + "so:libcli-nbt-samba4.so=0", + "so:libcli-smb-common-samba4.so=0", + "so:libcliauth-samba4.so=0", + "so:libcmocka-samba4.so=0", + "so:libcommon-auth-samba4.so=0", + "so:libdbwrap-samba4.so=0", + "so:libflag-mapping-samba4.so=0", + "so:libgenrand-samba4.so=0", + "so:libgensec-samba4.so=0", + "so:libgse-samba4.so=0", + "so:libinterfaces-samba4.so=0", + "so:libiov-buf-samba4.so=0", + "so:libkrb5samba-samba4.so=0", + "so:libldbsamba-samba4.so=0", + "so:libmessages-dgm-samba4.so=0", + "so:libmessages-util-samba4.so=0", + "so:libmsghdr-samba4.so=0", + "so:libndr-krb5pac.so.0=0.0.1", + "so:libndr-nbt.so.0=0.0.1", + "so:libndr-samba-samba4.so=0", + "so:libndr-standard.so.0=0.0.1", + "so:libndr.so.0=0.1.0", + "so:libpopt-samba3-samba4.so=0", + "so:libsamba-cluster-support-samba4.so=0", + "so:libsamba-credentials.so.0=0.0.1", + "so:libsamba-debug-samba4.so=0", + "so:libsamba-errors.so.1=1", + "so:libsamba-hostconfig.so.0=0.0.1", + "so:libsamba-modules-samba4.so=0", + "so:libsamba-security-samba4.so=0", + "so:libsamba-sockets-samba4.so=0", + "so:libsamba-util.so.0=0.0.1", + "so:libsamba3-util-samba4.so=0", + "so:libsamdb-common-samba4.so=0", + "so:libsamdb.so.0=0.0.1", + "so:libsecrets3-samba4.so=0", + "so:libserver-id-db-samba4.so=0", + "so:libserver-role-samba4.so=0", + "so:libsmb-transport-samba4.so=0", + "so:libsmbconf.so.0=0", + "so:libsmbd-shim-samba4.so=0", + "so:libsocket-blocking-samba4.so=0", + "so:libsys-rw-samba4.so=0", + "so:libtalloc-report-samba4.so=0", + "so:libtdb-wrap-samba4.so=0", + "so:libtevent-util.so.0=0.0.1", + "so:libtime-basic-samba4.so=0", + "so:libutil-cmdline-samba4.so=0", + "so:libutil-reg-samba4.so=0", + "so:libutil-setid-samba4.so=0", + "so:libutil-tdb-samba4.so=0" + ] + }, + "newt-lang": { + "versions": { + "0.52.20-r0": 1543924704 + }, + "origin": "newt" + }, + "pound-doc": { + "versions": { + "2.8-r0": 1545207004 + }, + "origin": "pound" + }, + "perl-apache-session": { + "versions": { + "1.93-r0": 1543927699 + }, + "origin": "perl-apache-session" + }, + "librsvg-doc": { + "versions": { + "2.40.20-r0": 1543926595 + }, + "origin": "librsvg" + }, + "py-twisted": { + "versions": { + "17.1.0-r1": 1546513851 + }, + "origin": "py-twisted", + "dependencies": [ + "py-crypto", + "py-zope-interface", + "py-constantly", + "py-incremental", + "so:libc.musl-x86_64.so.1", + "so:libpython2.7.so.1.0" + ], + "provides": [ + "cmd:cftp", + "cmd:ckeygen", + "cmd:conch", + "cmd:mailmail", + "cmd:pyhtmlizer", + "cmd:tkconch", + "cmd:trial", + "cmd:twist", + "cmd:twistd" + ] + }, + "lua5.3": { + "versions": { + "5.3.5-r2": 1557162502 + }, + "origin": "lua5.3", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblinenoise.so.0", + "so:liblua-5.3.so.0" + ], + "provides": [ + "lua", + "cmd:lua5.3", + "cmd:luac5.3" + ] + }, + "pstree": { + "versions": { + "2.39-r0": 1543926521 + }, + "origin": "pstree", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pstree" + ] + }, + "py-libmount": { + "versions": { + "2.33-r0": 1545307437 + }, + "origin": "util-linux", + "dependencies": [ + "findmnt", + "so:libc.musl-x86_64.so.1", + "so:libmount.so.1", + "so:libpython2.7.so.1.0" + ] + }, + "libffi-doc": { + "versions": { + "3.2.1-r6": 1545154420 + }, + "origin": "libffi" + }, + "api-sanity-checker": { + "versions": { + "1.98.7-r0": 1545069120 + }, + "origin": "api-sanity-checker", + "dependencies": [ + "perl", + "build-base" + ], + "provides": [ + "cmd:api-sanity-checker" + ] + }, + "zfs-doc": { + "versions": { + "0.7.12-r1": 1552933679 + }, + "origin": "zfs" + }, + "collectd-doc": { + "versions": { + "5.8.0-r3": 1546422675 + }, + "origin": "collectd" + }, + "lua-sql-sqlite3": { + "versions": { + "2.3.5-r2": 1543924409 + }, + "origin": "lua-sql" + }, + "fail2ban": { + "versions": { + "0.10.3.1-r2": 1545300388 + }, + "origin": "fail2ban", + "dependencies": [ + "python3", + "iptables", + "ip6tables", + "logrotate" + ], + "provides": [ + "cmd:fail2ban-client", + "cmd:fail2ban-python", + "cmd:fail2ban-regex", + "cmd:fail2ban-server", + "cmd:fail2ban-testcases" + ] + }, + "keybinder-dev": { + "versions": { + "0.3.0-r1": 1545076730 + }, + "origin": "keybinder", + "dependencies": [ + "gtk+2.0-dev", + "libxext-dev", + "keybinder=0.3.0-r1", + "pc:gtk+-2.0", + "pkgconfig" + ], + "provides": [ + "pc:keybinder=0.3.0" + ] + }, + "libxkbui": { + "versions": { + "1.0.2-r9": 1543927890 + }, + "origin": "libxkbui", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1", + "so:libxkbfile.so.1" + ], + "provides": [ + "so:libxkbui.so.1=1.0.0" + ] + }, + "uwsgi-cheaper_backlog2": { + "versions": { + "2.0.17.1-r0": 1545062197 + }, + "origin": "uwsgi", + "dependencies": [ + "uwsgi", + "so:libc.musl-x86_64.so.1" + ] + }, + "lua5.2-dev": { + "versions": { + "5.2.4-r7": 1542304841 + }, + "origin": "lua5.2", + "dependencies": [ + "lua5.2", + "lua5.2-libs=5.2.4-r7", + "pkgconfig" + ], + "provides": [ + "pc:lua5.2=5.2.4" + ] + }, + "py2-tz": { + "versions": { + "2018.9-r0": 1548109648 + }, + "origin": "py-tz", + "dependencies": [ + "python2" + ] + }, + "perl-error": { + "versions": { + "0.17027-r0": 1545062330 + }, + "origin": "perl-error", + "dependencies": [ + "perl" + ] + }, + "xmodmap": { + "versions": { + "1.0.9-r2": 1542973230 + }, + "origin": "xmodmap", + "dependencies": [ + "so:libX11.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:xmodmap" + ] + }, + "xdg-utils": { + "versions": { + "1.1.3-r0": 1545154525 + }, + "origin": "xdg-utils", + "dependencies": [ + "xset", + "xprop" + ], + "provides": [ + "cmd:xdg-desktop-icon", + "cmd:xdg-desktop-menu", + "cmd:xdg-email", + "cmd:xdg-icon-resource", + "cmd:xdg-mime", + "cmd:xdg-open", + "cmd:xdg-screensaver", + "cmd:xdg-settings" + ] + }, + "libfontenc": { + "versions": { + "1.1.3-r3": 1542924593 + }, + "origin": "libfontenc", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libfontenc.so.1=1.0.0" + ] + }, + "linux-headers": { + "versions": { + "4.18.13-r1": 1542301615 + }, + "origin": "linux-headers" + }, + "zfs-libs": { + "versions": { + "0.7.12-r1": 1552933679 + }, + "origin": "zfs", + "dependencies": [ + "so:libblkid.so.1", + "so:libc.musl-x86_64.so.1", + "so:libintl.so.8", + "so:libtirpc.so.3", + "so:libuuid.so.1", + "so:libz.so.1" + ], + "provides": [ + "so:libnvpair.so.1=1.0.1", + "so:libuutil.so.1=1.0.1", + "so:libzfs.so.2=2.0.0", + "so:libzfs_core.so.1=1.0.0", + "so:libzpool.so.2=2.0.0" + ] + }, + "collectd-write_http": { + "versions": { + "5.8.0-r3": 1546422676 + }, + "origin": "collectd", + "dependencies": [ + "collectd", + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libyajl.so.2" + ] + }, + "patch": { + "versions": { + "2.7.6-r4": 1548420016 + }, + "origin": "patch", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:patch" + ] + }, + "iwlwifi-1000-ucode": { + "versions": { + "39.31.5.1-r0": 1545076629 + }, + "origin": "iwlwifi-1000-ucode" + }, + "py2-psycopg2": { + "versions": { + "2.7.5-r0": 1544000428 + }, + "origin": "py-psycopg2", + "dependencies": [ + "py-egenix-mx-base", + "so:libc.musl-x86_64.so.1", + "so:libpq.so.5", + "so:libpython2.7.so.1.0" + ] + }, + "qemu-audio-alsa": { + "versions": { + "3.1.0-r3": 1551107306 + }, + "origin": "qemu", + "dependencies": [ + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0" + ] + }, + "cairomm-doc": { + "versions": { + "1.12.2-r0": 1543077182 + }, + "origin": "cairomm" + }, + "doxygen": { + "versions": { + "1.8.15-r0": 1545993768 + }, + "origin": "doxygen", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "cmd:doxygen" + ] + }, + "a52dec-doc": { + "versions": { + "0.7.4-r7": 1545214242 + }, + "origin": "a52dec" + }, + "glew-dev": { + "versions": { + "2.1.0-r0": 1543935773 + }, + "origin": "glew", + "dependencies": [ + "libxmu-dev", + "libxi-dev", + "mesa-dev", + "glu-dev", + "glew=2.1.0-r0", + "pc:glu", + "pkgconfig" + ], + "provides": [ + "pc:glew=2.1.0" + ] + }, + "gtk3-xfce-engine": { + "versions": { + "3.2.0-r2": 1545292906 + }, + "origin": "gtk-xfce-engine", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgdk-3.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgtk-3.so.0" + ] + }, + "nfprofile": { + "versions": { + "1.6.17-r0": 1545223024 + }, + "origin": "nfdump", + "dependencies": [ + "nfdump", + "so:libc.musl-x86_64.so.1", + "so:libnfdump-1.6.17.so", + "so:librrd.so.8" + ], + "provides": [ + "cmd:nfprofile" + ] + }, + "clamav-doc": { + "versions": { + "0.100.3-r0": 1555507335 + }, + "origin": "clamav" + }, + "perl-file-sharedir-install": { + "versions": { + "0.13-r0": 1543238880 + }, + "origin": "perl-file-sharedir-install" + }, + "py3-olefile": { + "versions": { + "0.43-r2": 1542883721 + }, + "origin": "py-olefile" + }, + "perl-cps": { + "versions": { + "0.18-r0": 1545289364 + }, + "origin": "perl-cps", + "dependencies": [ + "perl-future" + ] + }, + "pwgen": { + "versions": { + "2.08-r0": 1545290834 + }, + "origin": "pwgen", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pwgen" + ] + }, + "lua5.1-sql-odbc": { + "versions": { + "2.3.5-r2": 1543924399 + }, + "origin": "lua-sql", + "dependencies": [ + "lua5.1", + "so:libc.musl-x86_64.so.1", + "so:libodbc.so.2" + ] + }, + "perl-class-container": { + "versions": { + "0.13-r0": 1543254090 + }, + "origin": "perl-class-container", + "dependencies": [ + "perl-params-validate" + ] + }, + "ulogd-mysql": { + "versions": { + "2.0.7-r0": 1545300896 + }, + "origin": "ulogd", + "dependencies": [ + "ulogd", + "so:libc.musl-x86_64.so.1", + "so:libmariadb.so.3" + ] + }, + "squid-lang-cs": { + "versions": { + "4.4-r1": 1545216324 + }, + "origin": "squid", + "dependencies": [ + "logrotate" + ] + }, + "clamav": { + "versions": { + "0.100.3-r0": 1555507336 + }, + "origin": "clamav", + "dependencies": [ + "clamav-scanner", + "clamav-daemon" + ] + }, + "syslog-ng-openrc": { + "versions": { + "3.19.1-r0": 1548543150 + }, + "origin": "syslog-ng" + }, + "s6-networking": { + "versions": { + "2.3.0.3-r1": 1546957736 + }, + "origin": "s6-networking", + "dependencies": [ + "so:libbearssl.so.0", + "so:libc.musl-x86_64.so.1", + "so:libs6.so.2.7", + "so:libs6dns.so.2.3", + "so:libskarnet.so.2.7" + ], + "provides": [ + "so:libs6net.so.2.3=2.3.0.3", + "so:libsbearssl.so.2.3=2.3.0.3", + "cmd:minidentd", + "cmd:s6-clockadd", + "cmd:s6-clockview", + "cmd:s6-getservbyname", + "cmd:s6-ident-client", + "cmd:s6-sntpclock", + "cmd:s6-taiclock", + "cmd:s6-taiclockd", + "cmd:s6-tcpclient", + "cmd:s6-tcpserver", + "cmd:s6-tcpserver-access", + "cmd:s6-tcpserver4", + "cmd:s6-tcpserver4-socketbinder", + "cmd:s6-tcpserver4d", + "cmd:s6-tcpserver6", + "cmd:s6-tcpserver6-socketbinder", + "cmd:s6-tcpserver6d", + "cmd:s6-tlsc", + "cmd:s6-tlsclient", + "cmd:s6-tlsd", + "cmd:s6-tlsserver" + ] + }, + "lua-curl": { + "versions": { + "0.3.8-r1": 1545224117 + }, + "origin": "lua-curl" + }, + "py-mako": { + "versions": { + "1.0.7-r0": 1543220557 + }, + "origin": "py-mako" + }, + "perl-javascript-minifier-doc": { + "versions": { + "1.14-r0": 1545300393 + }, + "origin": "perl-javascript-minifier" + }, + "qemu-block-nfs": { + "versions": { + "3.1.0-r3": 1551107307 + }, + "origin": "qemu", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglib-2.0.so.0", + "so:libnfs.so.12" + ] + }, + "udns-dev": { + "versions": { + "0.4-r0": 1542924398 + }, + "origin": "udns", + "dependencies": [ + "udns=0.4-r0" + ] + }, + "freeswitch-sounds-music-8000": { + "versions": { + "1.0.8-r1": 1545116730 + }, + "origin": "freeswitch-sounds-music-8000" + }, + "iso-codes-lang": { + "versions": { + "4.1-r0": 1545257086 + }, + "origin": "iso-codes" + }, + "perl-net-dns": { + "versions": { + "1.19-r0": 1545061164 + }, + "origin": "perl-net-dns", + "dependencies": [ + "perl" + ] + }, + "bcache-tools-doc": { + "versions": { + "1.0.8-r1": 1545060582 + }, + "origin": "bcache-tools" + }, + "ciwiki": { + "versions": { + "2.0.5-r1": 1545292637 + }, + "origin": "ciwiki", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:ciwiki" + ] + }, + "mariadb-mytop": { + "versions": { + "10.3.13-r1": 1557431735 + }, + "origin": "mariadb", + "dependencies": [ + "perl", + "perl-dbi", + "perl-dbd-mysql", + "perl-getopt-long", + "perl-socket", + "perl-term-readkey" + ], + "provides": [ + "cmd:mytop" + ] + }, + "asciidoctor": { + "versions": { + "1.5.8-r0": 1545665243 + }, + "origin": "asciidoctor", + "dependencies": [ + "ruby" + ] + }, + "tiny-ec2-bootstrap": { + "versions": { + "1.2.0-r0": 1545300595 + }, + "origin": "tiny-ec2-bootstrap", + "dependencies": [ + "openrc", + "e2fsprogs-extra" + ] + }, + "gpicview-lang": { + "versions": { + "0.2.5-r0": 1543932504 + }, + "origin": "gpicview" + }, + "python3-doc": { + "versions": { + "3.6.8-r2": 1554747638 + }, + "origin": "python3" + }, + "bzr-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "libdvdcss": { + "versions": { + "1.4.2-r0": 1545838429 + }, + "origin": "libdvdcss", + "dependencies": [ + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libdvdcss.so.2=2.2.0" + ] + }, + "syslog-ng-tags-parser": { + "versions": { + "3.19.1-r0": 1548543151 + }, + "origin": "syslog-ng", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libsyslog-ng-3.19.so.0" + ] + }, + "perl-text-password-pronounceable": { + "versions": { + "0.30-r1": 1543928465 + }, + "origin": "perl-text-password-pronounceable", + "dependencies": [ + "perl" + ] + }, + "py3-vobject": { + "versions": { + "0.9.6.1-r0": 1545165275 + }, + "origin": "py-vobject", + "dependencies": [ + "python3", + "py3-icu", + "py3-dateutil" + ] + }, + "py3-oauth2client": { + "versions": { + "4.1.2-r2": 1545229292 + }, + "origin": "py-oauth2client", + "dependencies": [ + "py3-asn1", + "py3-httplib2", + "py3-asn1-modules", + "py3-rsa", + "py3-six", + "python3" + ] + }, + "c-ares-dev": { + "versions": { + "1.15.0-r0": 1544791695 + }, + "origin": "c-ares", + "dependencies": [ + "c-ares=1.15.0-r0", + "pkgconfig" + ], + "provides": [ + "pc:libcares=1.15.0" + ] + }, + "libgnome": { + "versions": { + "2.32.1-r7": 1545299768 + }, + "origin": "libgnome", + "dependencies": [ + "/bin/sh", + "so:libORBit-2.so.0", + "so:libbonobo-2.so.0", + "so:libbonobo-activation.so.4", + "so:libc.musl-x86_64.so.1", + "so:libcanberra.so.0", + "so:libgconf-2.so.4", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnomevfs-2.so.0", + "so:libgobject-2.0.so.0", + "so:libgthread-2.0.so.0", + "so:libintl.so.8", + "so:libpopt.so.0" + ], + "provides": [ + "so:libgnome-2.so.0=0.3200.1", + "cmd:gnome-open" + ] + }, + "qemu-system-s390x": { + "versions": { + "3.1.0-r3": 1551107305 + }, + "origin": "qemu", + "dependencies": [ + "qemu", + "so:libaio.so.1", + "so:libc.musl-x86_64.so.1", + "so:libepoxy.so.0", + "so:libgbm.so.1", + "so:libgcc_s.so.1", + "so:libglib-2.0.so.0", + "so:libgmodule-2.0.so.0", + "so:libgnutls.so.30", + "so:libjpeg.so.8", + "so:liblzo2.so.2", + "so:libnettle.so.6", + "so:libpixman-1.so.0", + "so:libpng16.so.16", + "so:libseccomp.so.2", + "so:libsnappy.so.1", + "so:libspice-server.so.1", + "so:libusbredirparser.so.1", + "so:libvdeplug.so.3", + "so:libvirglrenderer.so.0", + "so:libz.so.1" + ], + "provides": [ + "cmd:qemu-system-s390x" + ] + }, + "the_silver_searcher": { + "versions": { + "2.1.0-r2": 1543998782 + }, + "origin": "the_silver_searcher", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:liblzma.so.5", + "so:libpcre.so.1", + "so:libz.so.1" + ], + "provides": [ + "cmd:ag" + ] + }, + "gst-plugins-base0.10": { + "versions": { + "0.10.36-r4": 1543928811 + }, + "origin": "gst-plugins-base0.10", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libXv.so.1", + "so:libasound.so.2", + "so:libc.musl-x86_64.so.1", + "so:libcairo.so.2", + "so:libgio-2.0.so.0", + "so:libglib-2.0.so.0", + "so:libgobject-2.0.so.0", + "so:libgstbase-0.10.so.0", + "so:libgstcontroller-0.10.so.0", + "so:libgstdataprotocol-0.10.so.0", + "so:libgstreamer-0.10.so.0", + "so:libintl.so.8", + "so:libogg.so.0", + "so:liborc-0.4.so.0", + "so:liborc-test-0.4.so.0", + "so:libpango-1.0.so.0", + "so:libpangocairo-1.0.so.0", + "so:libtheoradec.so.1", + "so:libtheoraenc.so.1", + "so:libvorbis.so.0", + "so:libvorbisenc.so.2", + "so:libxml2.so.2", + "so:libz.so.1" + ], + "provides": [ + "so:libgstapp-0.10.so.0=0.25.0", + "so:libgstaudio-0.10.so.0=0.25.0", + "so:libgstcdda-0.10.so.0=0.25.0", + "so:libgstfft-0.10.so.0=0.25.0", + "so:libgstinterfaces-0.10.so.0=0.25.0", + "so:libgstnetbuffer-0.10.so.0=0.25.0", + "so:libgstpbutils-0.10.so.0=0.25.0", + "so:libgstriff-0.10.so.0=0.25.0", + "so:libgstrtp-0.10.so.0=0.25.0", + "so:libgstrtsp-0.10.so.0=0.25.0", + "so:libgstsdp-0.10.so.0=0.25.0", + "so:libgsttag-0.10.so.0=0.25.0", + "so:libgstvideo-0.10.so.0=0.25.0", + "cmd:gst-discoverer-0.10", + "cmd:gst-visualise-0.10" + ] + }, + "mdadm-udev": { + "versions": { + "4.1-r0": 1545858162 + }, + "origin": "mdadm" + }, + "yasm-doc": { + "versions": { + "1.3.0-r1": 1545117179 + }, + "origin": "yasm" + }, + "libxinerama": { + "versions": { + "1.1.4-r1": 1543077318 + }, + "origin": "libxinerama", + "dependencies": [ + "so:libX11.so.6", + "so:libXext.so.6", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libXinerama.so.1=1.0.0" + ] + }, + "newsboat-lang": { + "versions": { + "2.13-r0": 1545075359 + }, + "origin": "newsboat" + }, + "vde2-dev": { + "versions": { + "2.3.2-r10": 1545074024 + }, + "origin": "vde2", + "dependencies": [ + "pkgconfig", + "vde2-libs=2.3.2-r10" + ], + "provides": [ + "pc:vdehist=2.3.2", + "pc:vdemgmt=2.3.2", + "pc:vdeplug=2.3.2", + "pc:vdesnmp=2.3.2" + ] + }, + "virglrenderer-dev": { + "versions": { + "0.7.0-r1": 1547472270 + }, + "origin": "virglrenderer", + "dependencies": [ + "pkgconfig", + "virglrenderer=0.7.0-r1" + ], + "provides": [ + "pc:virglrenderer=0.7.0" + ] + }, + "perl-utils": { + "versions": { + "5.26.3-r0": 1543998673 + }, + "origin": "perl", + "dependencies": [ + "perl" + ], + "provides": [ + "cmd:corelist", + "cmd:cpan", + "cmd:encguess", + "cmd:h2ph", + "cmd:instmodsh", + "cmd:json_pp", + "cmd:libnetcfg", + "cmd:perlbug", + "cmd:perlthanks", + "cmd:piconv", + "cmd:pl2pm", + "cmd:prove", + "cmd:ptar", + "cmd:ptardiff", + "cmd:ptargrep", + "cmd:shasum", + "cmd:splain", + "cmd:zipdetails" + ] + }, + "freetype-static": { + "versions": { + "2.9.1-r2": 1542822031 + }, + "origin": "freetype" + }, + "perl-test-file-sharedir": { + "versions": { + "1.001002-r0": 1543238933 + }, + "origin": "perl-test-file-sharedir", + "dependencies": [ + "perl-class-tiny", + "perl-file-sharedir", + "perl-file-copy-recursive", + "perl-path-tiny", + "perl-scope-guard" + ] + }, + "glamor-egl": { + "versions": { + "0.6.0-r4": 1545060559 + }, + "origin": "glamor-egl", + "dependencies": [ + "so:libEGL.so.1", + "so:libGL.so.1", + "so:libc.musl-x86_64.so.1", + "so:libgbm.so.1" + ], + "provides": [ + "so:libglamor.so.0=0.0.0" + ] + }, + "gconf-dev": { + "versions": { + "3.2.6-r4": 1545075211 + }, + "origin": "gconf", + "dependencies": [ + "libxml2-dev", + "gtk+3.0-dev", + "polkit-dev", + "orbit2-dev", + "gconf=3.2.6-r4", + "pc:ORBit-2.0", + "pc:gio-2.0", + "pc:glib-2.0", + "pkgconfig" + ], + "provides": [ + "pc:gconf-2.0=3.2.6" + ] + }, + "perl-exception-class-doc": { + "versions": { + "1.44-r0": 1542845808 + }, + "origin": "perl-exception-class" + }, + "perl-exporter-doc": { + "versions": { + "5.73-r0": 1544001442 + }, + "origin": "perl-exporter" + }, + "py-docutils-doc": { + "versions": { + "0.14-r1": 1543925153 + }, + "origin": "py-docutils", + "dependencies": [ + "py3-docutils" + ] + }, + "py3-tornado": { + "versions": { + "4.5.2-r1": 1542845329 + }, + "origin": "py-tornado", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libpython3.6m.so.1.0" + ] + }, + "perl-stream-buffered-doc": { + "versions": { + "0.03-r0": 1544792335 + }, + "origin": "perl-stream-buffered" + }, + "s6-openrc": { + "versions": { + "2.7.2.0-r0": 1545062680 + }, + "origin": "s6" + }, + "libcap-ng-doc": { + "versions": { + "0.7.9-r1": 1545918386 + }, + "origin": "libcap-ng" + }, + "mesa-gles": { + "versions": { + "18.1.7-r2": 1554455971 + }, + "origin": "mesa", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libglapi.so.0" + ], + "provides": [ + "so:libGLESv1_CM.so.1=1.1.0", + "so:libGLESv2.so.2=2.0.0" + ] + }, + "lua-rex-pcre": { + "versions": { + "2.9.0-r0": 1545209197 + }, + "origin": "lua-rex" + }, + "py3-gflags": { + "versions": { + "3.1.1-r1": 1545208595 + }, + "origin": "py-gflags", + "dependencies": [ + "python3" + ] + }, + "fcgi++": { + "versions": { + "2.4.0-r8": 1543238825 + }, + "origin": "fcgi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libfcgi.so.0", + "so:libgcc_s.so.1", + "so:libstdc++.so.6" + ], + "provides": [ + "so:libfcgi++.so.0=0.0.0" + ] + }, + "alpine-keys": { + "versions": { + "2.1-r1": 1543248499 + }, + "origin": "alpine-keys" + }, + "perl-email-address-doc": { + "versions": { + "1.909-r0": 1542893277 + }, + "origin": "perl-email-address" + }, + "py2-backports.ssl_match_hostname": { + "versions": { + "3.5.0.1-r1": 1543223292 + }, + "origin": "py2-backports.ssl_match_hostname", + "dependencies": [ + "python2" + ], + "provides": [ + "py-backports.ssl_match_hostname" + ] + }, + "giblib": { + "versions": { + "1.2.4-r10": 1543226558 + }, + "origin": "giblib", + "dependencies": [ + "so:libImlib2.so.1", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "so:libgiblib.so.1=1.0.6" + ] + }, + "pdnsd": { + "versions": { + "1.2.9a-r5": 1543077306 + }, + "origin": "pdnsd", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1" + ], + "provides": [ + "cmd:pdnsd", + "cmd:pdnsd-ctl" + ] + }, + "lynx-zsh-completion": { + "versions": { + "5.6.2-r0": 1545308086 + }, + "origin": "zsh", + "dependencies": [ + "zsh" + ] + }, + "ca-certificates": { + "versions": { + "20190108-r0": 1548779239 + }, + "origin": "ca-certificates", + "dependencies": [ + "/bin/sh", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ], + "provides": [ + "cmd:c_rehash", + "cmd:update-ca-certificates" + ] + }, + "weechat": { + "versions": { + "2.3-r0": 1545299934 + }, + "origin": "weechat", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libcurl.so.4", + "so:libgcrypt.so.20", + "so:libgnutls.so.30", + "so:libncursesw.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:weechat", + "cmd:weechat-curses", + "cmd:weechat-headless" + ] + }, + "perl-crypt-openssl-rsa": { + "versions": { + "0.31-r1": 1545061169 + }, + "origin": "perl-crypt-openssl-rsa", + "dependencies": [ + "perl", + "perl-crypt-openssl-random", + "perl-crypt-openssl-guess", + "so:libc.musl-x86_64.so.1", + "so:libcrypto.so.1.1" + ] + }, + "perl-params-validate": { + "versions": { + "1.29-r0": 1543238894 + }, + "origin": "perl-params-validate", + "dependencies": [ + "perl-module-implementation", + "so:libc.musl-x86_64.so.1" + ] + }, + "nftables": { + "versions": { + "0.9.0-r0": 1542893221 + }, + "origin": "nftables", + "dependencies": [ + "/bin/sh", + "pkgconfig", + "so:libc.musl-x86_64.so.1", + "so:libgmp.so.10", + "so:libmnl.so.0", + "so:libnftnl.so.7", + "so:libreadline.so.7" + ], + "provides": [ + "so:libnftables.so.0=0.0.0", + "pc:libnftables=0.9.0", + "cmd:nft" + ] + }, + "perl-class-mix": { + "versions": { + "0.006-r0": 1545163344 + }, + "origin": "perl-class-mix", + "dependencies": [ + "perl-params-classify" + ] + }, + "xf86-video-tdfx-doc": { + "versions": { + "1.4.7-r3": 1545073943 + }, + "origin": "xf86-video-tdfx" + }, + "atop": { + "versions": { + "2.3.0-r1": 1543226695 + }, + "origin": "atop", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libncursesw.so.6", + "so:libz.so.1" + ], + "provides": [ + "cmd:atop", + "cmd:atop-2.3.0", + "cmd:atopacctd", + "cmd:atopsar", + "cmd:atopsar-2.3.0" + ] + }, + "open-iscsi": { + "versions": { + "2.0.874-r2": 1547188207 + }, + "origin": "open-iscsi", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libisns.so.0", + "so:libmount.so.1" + ], + "provides": [ + "cmd:iscsi-iname", + "cmd:iscsi_discovery", + "cmd:iscsiadm", + "cmd:iscsid", + "cmd:iscsistart", + "cmd:iscsiuio" + ] + }, + "usbredir": { + "versions": { + "0.7.1-r0": 1543223287 + }, + "origin": "usbredir", + "dependencies": [ + "so:libc.musl-x86_64.so.1", + "so:libusb-1.0.so.0" + ], + "provides": [ + "so:libusbredirhost.so.1=1.0.0", + "so:libusbredirparser.so.1=1.0.0" + ] + }, + "perl-test-eol-doc": { + "versions": { + "2.00-r0": 1545075951 + }, + "origin": "perl-test-eol" + }, + "git-perl": { + "versions": { + "2.20.1-r0": 1545214200 + }, + "origin": "git", + "dependencies": [ + "git=2.20.1-r0", + "perl-git=2.20.1-r0", + "perl" + ] + } + }, + "provide": { + "so": { + "libglut.so.3": { + "package": "freeglut", + "version": "3.10.0" + }, + "librabbitmq.so.4": { + "package": "rabbitmq-c", + "version": "4.2.0" + }, + "libgtksourceview-2.0.so.0": { + "package": "gtksourceview2", + "version": "0.0.0" + }, + "liba52.so.0": { + "package": "a52dec", + "version": "0.0.0" + }, + "cat.c32": { + "package": "syslinux", + "version": "0" + }, + "chain.c32": { + "package": "syslinux", + "version": "0" + }, + "cmd.c32": { + "package": "syslinux", + "version": "0" + }, + "cmenu.c32": { + "package": "syslinux", + "version": "0" + }, + "config.c32": { + "package": "syslinux", + "version": "0" + }, + "cptime.c32": { + "package": "syslinux", + "version": "0" + }, + "cpu.c32": { + "package": "syslinux", + "version": "0" + }, + "cpuid.c32": { + "package": "syslinux", + "version": "0" + }, + "cpuidtest.c32": { + "package": "syslinux", + "version": "0" + }, + "debug.c32": { + "package": "syslinux", + "version": "0" + }, + "dhcp.c32": { + "package": "syslinux", + "version": "0" + }, + "dir.c32": { + "package": "syslinux", + "version": "0" + }, + "disk.c32": { + "package": "syslinux", + "version": "0" + }, + "dmi.c32": { + "package": "syslinux", + "version": "0" + }, + "dmitest.c32": { + "package": "syslinux", + "version": "0" + }, + "elf.c32": { + "package": "syslinux", + "version": "0" + }, + "ethersel.c32": { + "package": "syslinux", + "version": "0" + }, + "gfxboot.c32": { + "package": "syslinux", + "version": "0" + }, + "gpxecmd.c32": { + "package": "syslinux", + "version": "0" + }, + "hdt.c32": { + "package": "syslinux", + "version": "0" + }, + "hexdump.c32": { + "package": "syslinux", + "version": "0" + }, + "host.c32": { + "package": "syslinux", + "version": "0" + }, + "ifcpu.c32": { + "package": "syslinux", + "version": "0" + }, + "ifcpu64.c32": { + "package": "syslinux", + "version": "0" + }, + "ifmemdsk.c32": { + "package": "syslinux", + "version": "0" + }, + "ifplop.c32": { + "package": "syslinux", + "version": "0" + }, + "kbdmap.c32": { + "package": "syslinux", + "version": "0" + }, + "kontron_wdt.c32": { + "package": "syslinux", + "version": "0" + }, + "ldlinux.c32": { + "package": "syslinux", + "version": "0" + }, + "lfs.c32": { + "package": "syslinux", + "version": "0" + }, + "libcom32.c32": { + "package": "syslinux", + "version": "0" + }, + "libgpl.c32": { + "package": "syslinux", + "version": "0" + }, + "liblua.c32": { + "package": "syslinux", + "version": "0" + }, + "libmenu.c32": { + "package": "syslinux", + "version": "0" + }, + "libutil.c32": { + "package": "syslinux", + "version": "0" + }, + "linux.c32": { + "package": "syslinux", + "version": "0" + }, + "ls.c32": { + "package": "syslinux", + "version": "0" + }, + "lua.c32": { + "package": "syslinux", + "version": "0" + }, + "mboot.c32": { + "package": "syslinux", + "version": "0" + }, + "meminfo.c32": { + "package": "syslinux", + "version": "0" + }, + "menu.c32": { + "package": "syslinux", + "version": "0" + }, + "pci.c32": { + "package": "syslinux", + "version": "0" + }, + "pcitest.c32": { + "package": "syslinux", + "version": "0" + }, + "pmload.c32": { + "package": "syslinux", + "version": "0" + }, + "poweroff.c32": { + "package": "syslinux", + "version": "0" + }, + "prdhcp.c32": { + "package": "syslinux", + "version": "0" + }, + "pwd.c32": { + "package": "syslinux", + "version": "0" + }, + "pxechn.c32": { + "package": "syslinux", + "version": "0" + }, + "reboot.c32": { + "package": "syslinux", + "version": "0" + }, + "rosh.c32": { + "package": "syslinux", + "version": "0" + }, + "sanboot.c32": { + "package": "syslinux", + "version": "0" + }, + "sdi.c32": { + "package": "syslinux", + "version": "0" + }, + "sysdump.c32": { + "package": "syslinux", + "version": "0" + }, + "syslinux.c32": { + "package": "syslinux", + "version": "0" + }, + "vesa.c32": { + "package": "syslinux", + "version": "0" + }, + "vesainfo.c32": { + "package": "syslinux", + "version": "0" + }, + "vesamenu.c32": { + "package": "syslinux", + "version": "0" + }, + "vpdtest.c32": { + "package": "syslinux", + "version": "0" + }, + "whichsys.c32": { + "package": "syslinux", + "version": "0" + }, + "zzjson.c32": { + "package": "syslinux", + "version": "0" + }, + "libfribidi.so.0": { + "package": "fribidi", + "version": "0.4.0" + }, + "libbluetooth.so.3": { + "package": "bluez-libs", + "version": "3.18.16" + }, + "libXdamage.so.1": { + "package": "libxdamage", + "version": "1.1.0" + }, + "libgmpxx.so.4": { + "package": "libgmpxx", + "version": "4.5.2" + }, + "libgailutil.so.18": { + "package": "gtk+2.0", + "version": "18.0.1" + }, + "libgdk-x11-2.0.so.0": { + "package": "gtk+2.0", + "version": "0.2400.32" + }, + "libgtk-x11-2.0.so.0": { + "package": "gtk+2.0", + "version": "0.2400.32" + }, + "libtiffxx.so.5": { + "package": "libtiffxx", + "version": "5.4.0" + }, + "libxcb-cursor.so.0": { + "package": "xcb-util-cursor", + "version": "0.0.0" + }, + "libetpan.so.20": { + "package": "libetpan", + "version": "20.3.0" + }, + "libnghttp2.so.14": { + "package": "nghttp2-libs", + "version": "14.17.1" + }, + "libmariadb.so.3": { + "package": "mariadb-connector-c", + "version": "3" + }, + "libalpm.so.10": { + "package": "pacman", + "version": "10.0.2" + }, + "libxmlrpc.so.3": { + "package": "xmlrpc-c", + "version": "3.39" + }, + "libxmlrpc_server.so.3": { + "package": "xmlrpc-c", + "version": "3.39" + }, + "libxmlrpc_util.so.3": { + "package": "xmlrpc-c", + "version": "3.39" + }, + "libxkbcommon-x11.so.0": { + "package": "libxkbcommon", + "version": "0.0.0" + }, + "libxkbcommon.so.0": { + "package": "libxkbcommon", + "version": "0.0.0" + }, + "libcli-ldap-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libcmdline-contexts-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libcmdline-credentials-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libdcerpc.so.0": { + "package": "samba-client-libs", + "version": "0.0.1" + }, + "libdsdb-garbage-collect-tombstones-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libevents-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libhttp-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libnetif-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libpopt-samba3-cmdline-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libregistry-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libsmbclient-raw-samba4.so": { + "package": "samba-client-libs", + "version": "0" + }, + "libpci.so.3": { + "package": "pciutils-libs", + "version": "3.6.2" + }, + "libMagick++-7.Q16HDRI.so.4": { + "package": "imagemagick-c++", + "version": "4.0.0" + }, + "libgdkglext-x11-1.0.so.0": { + "package": "gtkglext", + "version": "0.0.0" + }, + "libgtkglext-x11-1.0.so.0": { + "package": "gtkglext", + "version": "0.0.0" + }, + "lib01_acl_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib02_imap_acl_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib02_lazy_expunge_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib05_mail_crypt_acl_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib05_pop3_migration_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib05_snarf_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib10_last_login_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib10_mail_crypt_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib10_mail_filter_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib10_quota_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib11_imap_quota_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib11_trash_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib15_notify_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_autocreate_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_charset_alias_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_expire_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_fts_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_listescape_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_mail_log_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_mailbox_alias_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_notify_status_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_push_notification_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_quota_clone_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_replication_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_var_expand_crypt.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_virtual_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib20_zlib_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib21_fts_squat_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib30_imap_zlib_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib90_old_stats_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib95_imap_old_stats_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "lib99_welcome_plugin.so": { + "package": "dovecot", + "version": "0" + }, + "libdcrypt_openssl.so": { + "package": "dovecot", + "version": "0" + }, + "libdovecot-compression.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-dsync.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-fts.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-lda.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-login.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot-storage.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libdovecot.so.0": { + "package": "dovecot", + "version": "0.0.0" + }, + "libfs_compress.so": { + "package": "dovecot", + "version": "0" + }, + "libfs_crypt.so": { + "package": "dovecot", + "version": "0" + }, + "libfs_mail_crypt.so": { + "package": "dovecot", + "version": "0" + }, + "libold_stats_mail.so": { + "package": "dovecot", + "version": "0" + }, + "libssl_iostream_openssl.so": { + "package": "dovecot", + "version": "0" + }, + "libstats_auth.so": { + "package": "dovecot", + "version": "0" + }, + "libfdisk.so.1": { + "package": "libfdisk", + "version": "1.1.0" + }, + "lib21_fts_solr_plugin.so": { + "package": "dovecot-fts-solr", + "version": "0" + }, + "libgpgme.so.11": { + "package": "gpgme", + "version": "11.21.0" + }, + "libstfl.so.0": { + "package": "stfl", + "version": "0.24" + }, + "libquadmath.so.0": { + "package": "libquadmath", + "version": "0.0.0" + }, + "libenca.so.0": { + "package": "enca", + "version": "0.5.1" + }, + "libudev.so.1": { + "package": "eudev-libs", + "version": "1.6.3" + }, + "libtxc_dxtn.so": { + "package": "libtxc_dxtn", + "version": "0" + }, + "libsngtc_node.so": { + "package": "sngtc_client", + "version": "0" + }, + "libXpm.so.4": { + "package": "libxpm", + "version": "4.11.0" + }, + "libgmock.so": { + "package": "gmock", + "version": "0" + }, + "libgmock_main.so": { + "package": "gmock", + "version": "0" + }, + "libhunspell-1.6.so.0": { + "package": "hunspell", + "version": "0.0.1" + }, + "libee.so.0": { + "package": "libee", + "version": "0.0.0" + }, + "libfreetdm.so.1": { + "package": "freeswitch-freetdm", + "version": "1.0.0" + }, + "libchromeXvMC.so.1": { + "package": "xf86-video-openchrome", + "version": "1.0.0" + }, + "libchromeXvMCPro.so.1": { + "package": "xf86-video-openchrome", + "version": "1.0.0" + }, + "libMagickCore-7.Q16HDRI.so.6": { + "package": "imagemagick-libs", + "version": "6.0.0" + }, + "libMagickWand-7.Q16HDRI.so.6": { + "package": "imagemagick-libs", + "version": "6.0.0" + }, + "libhistory.so.7": { + "package": "libhistory", + "version": "7.0" + }, + "libdvdread.so.4": { + "package": "libdvdread", + "version": "4.2.0" + }, + "libdevmapper.so.1.02": { + "package": "device-mapper-libs", + "version": "1.02" + }, + "libhowl.so.0": { + "package": "avahi-compat-howl", + "version": "0.0.0" + }, + "libwayland-cursor.so.0": { + "package": "wayland-libs-cursor", + "version": "0.0.0" + }, + "libsctp.so.1": { + "package": "lksctp-tools", + "version": "1.0.17" + }, + "libmariadbd.so.19": { + "package": "mariadb-embedded", + "version": "19" + }, + "libcord.so.1": { + "package": "gc", + "version": "1.3.0" + }, + "libgc.so.1": { + "package": "gc", + "version": "1.3.2" + }, + "libjack.so.0": { + "package": "jack", + "version": "0.1.0" + }, + "libjacknet.so.0": { + "package": "jack", + "version": "0.1.0" + }, + "libjackserver.so.0": { + "package": "jack", + "version": "0.1.0" + }, + "libpython3.6m.so.1.0": { + "package": "python3", + "version": "1.0" + }, + "libpython3.so": { + "package": "python3", + "version": "0" + }, + "libldb.so.1": { + "package": "ldb", + "version": "1.3.8" + }, + "libnetcf.so.1": { + "package": "netcf-libs", + "version": "1.4.0" + }, + "libdvdnav.so.4": { + "package": "libdvdnav", + "version": "4.2.0" + }, + "libnewt.so.0.52": { + "package": "newt", + "version": "0.52.20" + }, + "libmenu-cache.so.3": { + "package": "menu-cache", + "version": "3.0.1" + }, + "libts.so.0": { + "package": "tslib", + "version": "0.10.0" + }, + "libudisks2.so.0": { + "package": "udisks2-libs", + "version": "0.0.0" + }, + "libglapi.so.0": { + "package": "mesa-glapi", + "version": "0.0.0" + }, + "libharfbuzz-icu.so.0": { + "package": "harfbuzz-icu", + "version": "0.20200.0" + }, + "libserf-1.so.1": { + "package": "serf", + "version": "1.3.0" + }, + "libXxf86vm.so.1": { + "package": "libxxf86vm", + "version": "1.0.0" + }, + "libconfuse.so.2": { + "package": "confuse", + "version": "2.0.0" + }, + "libattr.so.1": { + "package": "libattr", + "version": "1.1.0" + }, + "libqextserialport.so.1": { + "package": "qextserialport", + "version": "1.2.0" + }, + "libhiredis.so.0.14": { + "package": "hiredis", + "version": "0.14" + }, + "libpcre2-32.so.0": { + "package": "libpcre2-32", + "version": "0.7.1" + }, + "libtasn1.so.6": { + "package": "libtasn1", + "version": "6.5.5" + }, + "libcdio_cdda.so.2": { + "package": "libcdio-paranoia", + "version": "2.0.0" + }, + "libcdio_paranoia.so.2": { + "package": "libcdio-paranoia", + "version": "2.0.0" + }, + "libip4tc.so.0": { + "package": "iptables", + "version": "0.1.0" + }, + "libip6tc.so.0": { + "package": "iptables", + "version": "0.1.0" + }, + "libipq.so.0": { + "package": "iptables", + "version": "0.0.0" + }, + "libiptc.so.0": { + "package": "iptables", + "version": "0.0.0" + }, + "libxtables.so.12": { + "package": "iptables", + "version": "12.0.0" + }, + "libXv.so.1": { + "package": "libxv", + "version": "1.0.0" + }, + "libcrypto.so.43": { + "package": "libressl2.7-libcrypto", + "version": "43.0.1" + }, + "rlm_krb5.so": { + "package": "freeradius-krb5", + "version": "0" + }, + "libgstbase-1.0.so.0": { + "package": "gstreamer", + "version": "0.1404.0" + }, + "libgstcheck-1.0.so.0": { + "package": "gstreamer", + "version": "0.1404.0" + }, + "libgstcontroller-1.0.so.0": { + "package": "gstreamer", + "version": "0.1404.0" + }, + "libgstnet-1.0.so.0": { + "package": "gstreamer", + "version": "0.1404.0" + }, + "libgstreamer-1.0.so.0": { + "package": "gstreamer", + "version": "0.1404.0" + }, + "libgdl-3.so.5": { + "package": "gdl", + "version": "5.0.9" + }, + "libtls-standalone.so.1": { + "package": "libtls-standalone", + "version": "1.0.0" + }, + "libdevmapper-event.so.1.02": { + "package": "device-mapper-event-libs", + "version": "1.02" + }, + "libfreeradius-dhcp.so": { + "package": "freeradius-lib", + "version": "0" + }, + "libfreeradius-eap.so": { + "package": "freeradius-lib", + "version": "0" + }, + "libfreeradius-radius.so": { + "package": "freeradius-lib", + "version": "0" + }, + "libfreeradius-server.so": { + "package": "freeradius-lib", + "version": "0" + }, + "grosscheck.so": { + "package": "gross", + "version": "0" + }, + "libgconf-2.so.4": { + "package": "gconf", + "version": "4.1.5" + }, + "libxshmfence.so.1": { + "package": "libxshmfence", + "version": "1.0.0" + }, + "rlm_sql_postgresql.so": { + "package": "freeradius-postgresql", + "version": "0" + }, + "libloudmouth-1.so.0": { + "package": "loudmouth", + "version": "0.1.0" + }, + "libsasl2.so.3": { + "package": "libsasl", + "version": "3.0.0" + }, + "libfastjson.so.4": { + "package": "libfastjson", + "version": "4.2.0" + }, + "librhash.so.0": { + "package": "rhash-libs", + "version": "0" + }, + "libqrencode.so.4": { + "package": "libqrencode", + "version": "4.0.2" + }, + "libisofs.so.6": { + "package": "libisofs", + "version": "6.84.0" + }, + "libarchive.so.13": { + "package": "libarchive", + "version": "13.3.2" + }, + "libcpufreq.so.0": { + "package": "cpufrequtils", + "version": "0.0.0" + }, + "libunistring.so.2": { + "package": "libunistring", + "version": "2.1.0" + }, + "libbind9.so.1201": { + "package": "bind-libs", + "version": "1201.0.1" + }, + "libdns.so.1208": { + "package": "bind-libs", + "version": "1208.0.0" + }, + "libirs.so.1201": { + "package": "bind-libs", + "version": "1201.0.1" + }, + "libisc.so.1204": { + "package": "bind-libs", + "version": "1204.1.0" + }, + "libisccc.so.1201": { + "package": "bind-libs", + "version": "1201.0.1" + }, + "libisccfg.so.1203": { + "package": "bind-libs", + "version": "1203.0.1" + }, + "libns.so.1207": { + "package": "bind-libs", + "version": "1207.0.0" + }, + "libspeexdsp.so.1": { + "package": "speexdsp", + "version": "1.5.0" + }, + "libgnutlsxx.so.28": { + "package": "gnutls-c++", + "version": "28.1.0" + }, + "libgnarl-8.so": { + "package": "libgnat", + "version": "0" + }, + "libgnat-8.so": { + "package": "libgnat", + "version": "0" + }, + "libasn1-samba4.so.8": { + "package": "samba-heimdal-libs", + "version": "8.0.0" + }, + "libgssapi-samba4.so.2": { + "package": "samba-heimdal-libs", + "version": "2.0.0" + }, + "libhcrypto-samba4.so.5": { + "package": "samba-heimdal-libs", + "version": "5.0.1" + }, + "libheimbase-samba4.so.1": { + "package": "samba-heimdal-libs", + "version": "1.0.0" + }, + "libheimntlm-samba4.so.1": { + "package": "samba-heimdal-libs", + "version": "1.0.1" + }, + "libhx509-samba4.so.5": { + "package": "samba-heimdal-libs", + "version": "5.0.0" + }, + "libkrb5-samba4.so.26": { + "package": "samba-heimdal-libs", + "version": "26.0.0" + }, + "libroken-samba4.so.19": { + "package": "samba-heimdal-libs", + "version": "19.0.1" + }, + "libwind-samba4.so.0": { + "package": "samba-heimdal-libs", + "version": "0.0.0" + }, + "libdovecot-sql.so.0": { + "package": "dovecot-sql", + "version": "0.0.0" + }, + "libX11-xcb.so.1": { + "package": "libx11", + "version": "1.0.0" + }, + "libX11.so.6": { + "package": "libx11", + "version": "6.3.0" + }, + "libinput.so.10": { + "package": "libinput-libs", + "version": "10.13.0" + }, + "libfsimage.so.1.0": { + "package": "xen-libs", + "version": "1.0.0" + }, + "libxencall.so.1": { + "package": "xen-libs", + "version": "1.1" + }, + "libxenctrl.so.4.11": { + "package": "xen-libs", + "version": "4.11.0" + }, + "libxendevicemodel.so.1": { + "package": "xen-libs", + "version": "1.2" + }, + "libxenevtchn.so.1": { + "package": "xen-libs", + "version": "1.1" + }, + "libxenforeignmemory.so.1": { + "package": "xen-libs", + "version": "1.3" + }, + "libxengnttab.so.1": { + "package": "xen-libs", + "version": "1.1" + }, + "libxenguest.so.4.11": { + "package": "xen-libs", + "version": "4.11.0" + }, + "libxenlight.so.4.11": { + "package": "xen-libs", + "version": "4.11.0" + }, + "libxenstat.so.0": { + "package": "xen-libs", + "version": "0.0" + }, + "libxenstore.so.3.0": { + "package": "xen-libs", + "version": "3.0.3" + }, + "libxentoolcore.so.1": { + "package": "xen-libs", + "version": "1.0" + }, + "libxentoollog.so.1": { + "package": "xen-libs", + "version": "1.0" + }, + "libxenvchan.so.4.11": { + "package": "xen-libs", + "version": "4.11.0" + }, + "libxlutil.so.4.11": { + "package": "xen-libs", + "version": "4.11.0" + }, + "libpcp.so.1": { + "package": "pgpool", + "version": "1.0.0" + }, + "libfltk.so.1.3": { + "package": "fltk", + "version": "1.3" + }, + "libfltk_forms.so.1.3": { + "package": "fltk", + "version": "1.3" + }, + "libfltk_gl.so.1.3": { + "package": "fltk", + "version": "1.3" + }, + "libfltk_images.so.1.3": { + "package": "fltk", + "version": "1.3" + }, + "libpam.so.0": { + "package": "linux-pam", + "version": "0.84.2" + }, + "libpam_misc.so.0": { + "package": "linux-pam", + "version": "0.82.1" + }, + "libpamc.so.0": { + "package": "linux-pam", + "version": "0.82.1" + }, + "libcrack.so.2": { + "package": "cracklib", + "version": "2.9.0" + }, + "rlm_python.so": { + "package": "freeradius-python", + "version": "0" + }, + "libpytalloc-util.so.2": { + "package": "py2-talloc", + "version": "2.1.14" + }, + "libpq.so.5": { + "package": "libpq", + "version": "5.11" + }, + "libkdb_ldap.so.1": { + "package": "krb5-server-ldap", + "version": "1.0" + }, + "libminizip.so.1": { + "package": "minizip", + "version": "1.0.0" + }, + "libavahi-glib.so.1": { + "package": "avahi-glib", + "version": "1.0.2" + }, + "libavahi-gobject.so.0": { + "package": "avahi-glib", + "version": "0.0.5" + }, + "libdovecot-ldap.so.0": { + "package": "dovecot-ldap", + "version": "0.0.0" + }, + "libbstring.so.0": { + "package": "coova-chilli", + "version": "0.0.0" + }, + "libchilli.so.0": { + "package": "coova-chilli", + "version": "0.0.0" + }, + "libhandle.so.1": { + "package": "xfsprogs-libs", + "version": "1.0.3" + }, + "libxcb-util.so.1": { + "package": "xcb-util", + "version": "1.0.0" + }, + "libgladeui-1.so.11": { + "package": "glade3", + "version": "11.2.0" + }, + "libgccpp.so.1": { + "package": "libgc++", + "version": "1.3.1" + }, + "libdaemon.so.0": { + "package": "libdaemon", + "version": "0.5.0" + }, + "liborc-0.4.so.0": { + "package": "orc", + "version": "0.28.0" + }, + "liborc-test-0.4.so.0": { + "package": "orc", + "version": "0.28.0" + }, + "libQtWebKit.so.4": { + "package": "qt-webkit", + "version": "4.9.4" + }, + "libssh.so.4": { + "package": "libssh", + "version": "4.4.3" + }, + "libssh_threads.so.4": { + "package": "libssh", + "version": "4.4.3" + }, + "libasprintf.so.0": { + "package": "gettext-asprintf", + "version": "0.0.0" + }, + "libads-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libauth-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libdfs-server-ad-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libnetapi.so.0": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libnpa-tstream-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libprinting-migrate-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libsmbd-base-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libsmbd-conn-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libsmbldap.so.2": { + "package": "samba-common-server-libs", + "version": "2" + }, + "libsmbldaphelper-samba4.so": { + "package": "samba-common-server-libs", + "version": "0" + }, + "libg7221codec.so.2": { + "package": "pjproject", + "version": "2" + }, + "libilbccodec.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpj.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjlib-util.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjmedia-audiodev.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjmedia-codec.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjmedia-videodev.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjmedia.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjnath.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsip-simple.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsip-ua.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsip.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsua.so.2": { + "package": "pjproject", + "version": "2" + }, + "libpjsua2.so.2": { + "package": "pjproject", + "version": "2" + }, + "libyuv.so.2": { + "package": "pjproject", + "version": "2" + }, + "libGeoIP.so.1": { + "package": "geoip", + "version": "1.6.12" + }, + "liblber-2.4.so.2": { + "package": "libldap", + "version": "2.10.10" + }, + "libldap-2.4.so.2": { + "package": "libldap", + "version": "2.10.10" + }, + "libldap_r-2.4.so.2": { + "package": "libldap", + "version": "2.10.10" + }, + "libdriver_pgsql.so": { + "package": "dovecot-pgsql", + "version": "0" + }, + "libXau.so.6": { + "package": "libxau", + "version": "6.0.0" + }, + "libva-glx.so.2": { + "package": "libva-glx", + "version": "2.200.0" + }, + "libjson-c.so.2": { + "package": "json-c0.12", + "version": "2.0.2" + }, + "libQtCore.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtDBus.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtNetwork.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtScript.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtSql.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtTest.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtXml.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libQtXmlPatterns.so.4": { + "package": "qt", + "version": "4.8.7" + }, + "libnl-cli-3.so.200": { + "package": "libnl3-cli", + "version": "200.26.0" + }, + "libgstbase-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstcheck-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstcontroller-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstdataprotocol-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstnet-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libgstreamer-0.10.so.0": { + "package": "gstreamer0.10", + "version": "0.30.0" + }, + "libglfw.so.3": { + "package": "glfw", + "version": "3.2" + }, + "libsvn_client-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_delta-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_diff-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs_base-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs_fs-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs_util-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_fs_x-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_ra-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_ra_local-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_ra_serf-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_ra_svn-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_repos-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_subr-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libsvn_wc-1.so.0": { + "package": "subversion-libs", + "version": "0.0.0" + }, + "libgailutil-3.so.0": { + "package": "gtk+3.0", + "version": "0.0.0" + }, + "libgdk-3.so.0": { + "package": "gtk+3.0", + "version": "0.2400.1" + }, + "libgtk-3.so.0": { + "package": "gtk+3.0", + "version": "0.2400.1" + }, + "libEGL.so.1": { + "package": "mesa-egl", + "version": "1.0.0" + }, + "libxklavier.so.16": { + "package": "libxklavier", + "version": "16.4.0" + }, + "libswish-e.so.2": { + "package": "swish-e", + "version": "2.0.0" + }, + "libevdev.so.2": { + "package": "libevdev", + "version": "2.2.0" + }, + "libslim.so.1.3.6": { + "package": "slim", + "version": "1.3.6" + }, + "libatkmm-1.6.so.1": { + "package": "atkmm", + "version": "1.1.0" + }, + "liblzo2.so.2": { + "package": "lzo", + "version": "2.0.0" + }, + "postfix-sqlite.so": { + "package": "postfix-sqlite", + "version": "0" + }, + "libell.so.0": { + "package": "libell", + "version": "0.0.2" + }, + "libmnl.so.0": { + "package": "libmnl", + "version": "0.2.0" + }, + "liblognorm.so.5": { + "package": "liblognorm", + "version": "5.1.0" + }, + "libxt_ACCOUNT_cl.so.0": { + "package": "xtables-addons", + "version": "0.0.0" + }, + "libboost_fiber-mt.so.1.67.0": { + "package": "boost-fiber", + "version": "1.67.0" + }, + "libpolkit-agent-1.so.0": { + "package": "polkit", + "version": "0.0.0" + }, + "libpolkit-backend-1.so.0": { + "package": "polkit", + "version": "0.0.0" + }, + "libpolkit-gobject-1.so.0": { + "package": "polkit", + "version": "0.0.0" + }, + "libupower-glib.so.3": { + "package": "upower", + "version": "3.0.1" + }, + "libboost_filesystem-mt.so.1.67.0": { + "package": "boost-filesystem", + "version": "1.67.0" + }, + "libboost_filesystem.so.1.67.0": { + "package": "boost-filesystem", + "version": "1.67.0" + }, + "libtdb.so.1": { + "package": "tdb-libs", + "version": "1.3.16" + }, + "libeggdbus-1.so.0": { + "package": "eggdbus", + "version": "0.0.0" + }, + "libnice.so.10": { + "package": "libnice", + "version": "10.7.0" + }, + "libopenjp2.so.7": { + "package": "openjpeg", + "version": "2.3.0" + }, + "libotr.so.2": { + "package": "libotr3", + "version": "2.2.1" + }, + "libMrm.so.4": { + "package": "motif", + "version": "4.0.4" + }, + "libUil.so.4": { + "package": "motif", + "version": "4.0.4" + }, + "libXm.so.4": { + "package": "motif", + "version": "4.0.4" + }, + "liblinenoise.so.0": { + "package": "linenoise", + "version": "0.0.0" + }, + "libjq.so.1": { + "package": "jq", + "version": "1.0.4" + }, + "libonig.so.5": { + "package": "oniguruma", + "version": "5.0.0" + }, + "libboost_date_time-mt.so.1.67.0": { + "package": "boost-date_time", + "version": "1.67.0" + }, + "libboost_date_time.so.1.67.0": { + "package": "boost-date_time", + "version": "1.67.0" + }, + "libcmph.so.0": { + "package": "libcmph", + "version": "0.0.0" + }, + "librlog.so.5": { + "package": "rlog", + "version": "5.0.0" + }, + "libperl.so": { + "package": "perl", + "version": "0" + }, + "libboost_python36-mt.so.1.67.0": { + "package": "boost-python3", + "version": "1.67.0" + }, + "libboost_python36.so.1.67.0": { + "package": "boost-python3", + "version": "1.67.0" + }, + "libgfortran.so.5": { + "package": "libgfortran", + "version": "5.0.0" + }, + "liblcms2.so.2": { + "package": "lcms2", + "version": "2.0.8" + }, + "liblxc.so.1": { + "package": "lxc-libs", + "version": "1.5.0" + }, + "libgpg-error.so.0": { + "package": "libgpg-error", + "version": "0.25.0" + }, + "libjbig2dec.so.0": { + "package": "jbig2dec", + "version": "0.0.0" + }, + "libzstd.so.1": { + "package": "zstd-libs", + "version": "1.3.8" + }, + "rlm_rest.so": { + "package": "freeradius-rest", + "version": "0" + }, + "libkmod.so.2": { + "package": "kmod", + "version": "2.3.2" + }, + "libtcl8.6.so": { + "package": "tcl", + "version": "0" + }, + "libwx_baseu-2.8.so.0": { + "package": "wxgtk2.8-base", + "version": "0.8.0" + }, + "libwx_baseu_net-2.8.so.0": { + "package": "wxgtk2.8-base", + "version": "0.8.0" + }, + "libwx_baseu_xml-2.8.so.0": { + "package": "wxgtk2.8-base", + "version": "0.8.0" + }, + "rlm_redis.so": { + "package": "freeradius-redis", + "version": "0" + }, + "rlm_rediswho.so": { + "package": "freeradius-redis", + "version": "0" + }, + "libavahi-ui.so.0": { + "package": "avahi-ui", + "version": "0.1.4" + }, + "liburiparser.so.1": { + "package": "uriparser", + "version": "1.0.24" + }, + "libunique-3.0.so.0": { + "package": "libunique3", + "version": "0.0.2" + }, + "liburcu-bp.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-cds.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-common.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-mb.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-qsbr.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu-signal.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "liburcu.so.6": { + "package": "userspace-rcu", + "version": "6.0.0" + }, + "libjson-glib-1.0.so.0": { + "package": "json-glib", + "version": "0.400.4" + }, + "libcap-ng.so.0": { + "package": "libcap-ng", + "version": "0.0.0" + }, + "libguile-2.0.so.22": { + "package": "guile-libs", + "version": "22.8.1" + }, + "libz.so.1": { + "package": "zlib", + "version": "1.2.11" + }, + "libuuid.so.1": { + "package": "libuuid", + "version": "1.3.0" + }, + "libSDL2_image-2.0.so.0": { + "package": "sdl2_image", + "version": "0.2.2" + }, + "librecode.so.0": { + "package": "recode", + "version": "0.0.0" + }, + "liblttng-ust-ctl.so.4": { + "package": "lttng-ust", + "version": "4.0.0" + }, + "liblttng-ust-cyg-profile-fast.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-cyg-profile.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-dl.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-fd.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-fork.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-libc-wrapper.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-pthread-wrapper.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust-tracepoint.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "liblttng-ust.so.0": { + "package": "lttng-ust", + "version": "0.0.0" + }, + "libgio-2.0.so.0": { + "package": "glib", + "version": "0.5800.1" + }, + "libglib-2.0.so.0": { + "package": "glib", + "version": "0.5800.1" + }, + "libgmodule-2.0.so.0": { + "package": "glib", + "version": "0.5800.1" + }, + "libgobject-2.0.so.0": { + "package": "glib", + "version": "0.5800.1" + }, + "libgthread-2.0.so.0": { + "package": "glib", + "version": "0.5800.1" + }, + "libp11-kit.so.0": { + "package": "p11-kit", + "version": "0.3.0" + }, + "libc.musl-x86_64.so.1": { + "package": "musl", + "version": "1" + }, + "libratbox.so": { + "package": "charybdis", + "version": "0" + }, + "libavc1394.so.0": { + "package": "libavc1394", + "version": "0.3.0" + }, + "librom1394.so.0": { + "package": "libavc1394", + "version": "0.3.0" + }, + "libwebp.so.7": { + "package": "libwebp", + "version": "7.0.3" + }, + "libwebpdecoder.so.3": { + "package": "libwebp", + "version": "3.0.3" + }, + "libwebpdemux.so.2": { + "package": "libwebp", + "version": "2.0.5" + }, + "libwebpmux.so.3": { + "package": "libwebp", + "version": "3.0.3" + }, + "libtheora.so.0": { + "package": "libtheora", + "version": "0.3.10" + }, + "libtheoradec.so.1": { + "package": "libtheora", + "version": "1.1.4" + }, + "libtheoraenc.so.1": { + "package": "libtheora", + "version": "1.1.2" + }, + "libboost_system-mt.so.1.67.0": { + "package": "boost-system", + "version": "1.67.0" + }, + "libboost_system.so.1.67.0": { + "package": "boost-system", + "version": "1.67.0" + }, + "libexpat.so.1": { + "package": "expat", + "version": "1.6.8" + }, + "libGLEW.so.2.1": { + "package": "glew", + "version": "2.1.0" + }, + "libprotobuf-lite.so.17": { + "package": "protobuf", + "version": "17.0.0" + }, + "libprotobuf.so.17": { + "package": "protobuf", + "version": "17.0.0" + }, + "libprotoc.so.17": { + "package": "protobuf", + "version": "17.0.0" + }, + "libflite.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_grapheme_lang.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_grapheme_lex.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_indic_lang.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_indic_lex.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_time_awb.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_us_awb.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_us_kal.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_us_kal16.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_us_rms.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmu_us_slt.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_cmulex.so.1": { + "package": "flite", + "version": "2.1" + }, + "libflite_usenglish.so.1": { + "package": "flite", + "version": "2.1" + }, + "libqpdf.so.21": { + "package": "qpdf-libs", + "version": "21.3.0" + }, + "libev.so.4": { + "package": "libev", + "version": "4.0.0" + }, + "libmspack.so.0": { + "package": "libmspack", + "version": "0.1.0" + }, + "libxkbfile.so.1": { + "package": "libxkbfile", + "version": "1.0.2" + }, + "libe2p.so.2": { + "package": "e2fsprogs-libs", + "version": "2.3" + }, + "libext2fs.so.2": { + "package": "e2fsprogs-libs", + "version": "2.4" + }, + "libss.so.2": { + "package": "e2fsprogs-libs", + "version": "2.0" + }, + "libcryptsetup.so.12": { + "package": "cryptsetup-libs", + "version": "12.3.0" + }, + "libQt3Support.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtCLucene.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtDeclarative.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtDesigner.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtDesignerComponents.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtGui.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtHelp.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtMultimedia.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtOpenGL.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtScriptTools.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "libQtSvg.so.4": { + "package": "qt-x11", + "version": "4.8.7" + }, + "psqlodbca.so": { + "package": "psqlodbc", + "version": "0" + }, + "psqlodbcw.so": { + "package": "psqlodbc", + "version": "0" + }, + "libinotifytools.so.0": { + "package": "inotify-tools", + "version": "0.4.1" + }, + "libmpeg2.so.0": { + "package": "libmpeg2", + "version": "0.1.0" + }, + "libmpeg2convert.so.0": { + "package": "libmpeg2", + "version": "0.0.0" + }, + "libmng.so.2": { + "package": "libmng", + "version": "2.0.2" + }, + "libmms.so.0": { + "package": "libmms", + "version": "0.0.2" + }, + "libfaac.so.0": { + "package": "faac", + "version": "0.0.0" + }, + "libedit.so.0": { + "package": "libedit", + "version": "0.0.59" + }, + "libcares.so.2": { + "package": "c-ares", + "version": "2.3.0" + }, + "libtinyxml.so.0": { + "package": "tinyxml", + "version": "0.2.6.2" + }, + "libmosquittopp.so.1": { + "package": "mosquitto-libs++", + "version": "1" + }, + "libs6rc.so.0.4": { + "package": "s6-rc", + "version": "0.4.1.0" + }, + "libparted-fs-resize.so.0": { + "package": "parted", + "version": "0.0.1" + }, + "libparted.so.2": { + "package": "parted", + "version": "2.0.1" + }, + "rlm_sql_unixodbc.so": { + "package": "freeradius-unixodbc", + "version": "0" + }, + "libepoxy.so.0": { + "package": "libepoxy", + "version": "0.0.0" + }, + "libnfnetlink.so.0": { + "package": "libnfnetlink", + "version": "0.2.0" + }, + "libatomic.so.1": { + "package": "libatomic", + "version": "1.2.0" + }, + "libintl.so.8": { + "package": "libintl", + "version": "8.1.5" + }, + "libltdl.so.7": { + "package": "libltdl", + "version": "7.3.1" + }, + "libgpext-samba4.so": { + "package": "samba-common-tools", + "version": "0" + }, + "libnet-keytab-samba4.so": { + "package": "samba-common-tools", + "version": "0" + }, + "libvdehist.so.0": { + "package": "vde2-libs", + "version": "0.0.1" + }, + "libvdemgmt.so.0": { + "package": "vde2-libs", + "version": "0.0.1" + }, + "libvdeplug.so.3": { + "package": "vde2-libs", + "version": "3.0.1" + }, + "libvdesnmp.so.0": { + "package": "vde2-libs", + "version": "0.0.1" + }, + "libfakeroot-0.so": { + "package": "fakeroot", + "version": "0" + }, + "libseccomp.so.2": { + "package": "libseccomp", + "version": "2.3.3" + }, + "libudns.so.0": { + "package": "udns", + "version": "0" + }, + "libjasper.so.4": { + "package": "jasper-libs", + "version": "4.0.0" + }, + "libXrandr.so.2": { + "package": "libxrandr", + "version": "2.2.0" + }, + "liblutok.so.3": { + "package": "lutok", + "version": "3.0.0" + }, + "libtirpc.so.3": { + "package": "libtirpc", + "version": "3.0.0" + }, + "libass.so.9": { + "package": "libass", + "version": "9.0.2" + }, + "libmodplug.so.1": { + "package": "libmodplug", + "version": "1.0.0" + }, + "libcroco-0.6.so.3": { + "package": "libcroco", + "version": "3.0.1" + }, + "libXext.so.6": { + "package": "libxext", + "version": "6.4.0" + }, + "libtiff.so.5": { + "package": "tiff", + "version": "5.4.0" + }, + "libfftw3l.so.3": { + "package": "fftw-long-double-libs", + "version": "3.5.8" + }, + "libfftw3l_threads.so.3": { + "package": "fftw-long-double-libs", + "version": "3.5.8" + }, + "libgsasl.so.7": { + "package": "libgsasl", + "version": "7.9.6" + }, + "libgtkspell.so.0": { + "package": "gtkspell", + "version": "0.0.0" + }, + "libformw.so.6": { + "package": "ncurses-libs", + "version": "6.1" + }, + "libmenuw.so.6": { + "package": "ncurses-libs", + "version": "6.1" + }, + "libncursesw.so.6": { + "package": "ncurses-libs", + "version": "6.1" + }, + "libpanelw.so.6": { + "package": "ncurses-libs", + "version": "6.1" + }, + "libgomp.so.1": { + "package": "libgomp", + "version": "1.0.0" + }, + "libnetsnmpagent.so.35": { + "package": "net-snmp-agent-libs", + "version": "35.0.0" + }, + "libnetsnmphelpers.so.35": { + "package": "net-snmp-agent-libs", + "version": "35.0.0" + }, + "libnetsnmpmibs.so.35": { + "package": "net-snmp-agent-libs", + "version": "35.0.0" + }, + "libnetsnmptrapd.so.35": { + "package": "net-snmp-agent-libs", + "version": "35.0.0" + }, + "libmicrohttpd.so.12": { + "package": "libmicrohttpd", + "version": "12.49.0" + }, + "libffi.so.6": { + "package": "libffi", + "version": "6.0.4" + }, + "libnih-dbus.so.1": { + "package": "libnih", + "version": "1.0.0" + }, + "libnih.so.1": { + "package": "libnih", + "version": "1.0.0" + }, + "libspandsp.so.2": { + "package": "spandsp", + "version": "2.0.0" + }, + "librarian.so.0": { + "package": "rarian", + "version": "0.0.0" + }, + "libboost_chrono-mt.so.1.67.0": { + "package": "boost-chrono", + "version": "1.67.0" + }, + "libboost_chrono.so.1.67.0": { + "package": "boost-chrono", + "version": "1.67.0" + }, + "libvanessa_adt.so.1": { + "package": "vanessa_adt", + "version": "1.0.0" + }, + "libclucene-core.so.1": { + "package": "clucene", + "version": "2.3.3.4" + }, + "libclucene-shared.so.1": { + "package": "clucene", + "version": "2.3.3.4" + }, + "libnss_winbind.so.2": { + "package": "samba-libnss-winbind", + "version": "2" + }, + "libnss_wins.so.2": { + "package": "samba-libnss-winbind", + "version": "2" + }, + "libgit2.so.27": { + "package": "libgit2", + "version": "0.27.7" + }, + "libvirt-admin.so.0": { + "package": "libvirt-libs", + "version": "0.4010.0" + }, + "libvirt-lxc.so.0": { + "package": "libvirt-libs", + "version": "0.4010.0" + }, + "libvirt-qemu.so.0": { + "package": "libvirt-libs", + "version": "0.4010.0" + }, + "libvirt.so.0": { + "package": "libvirt-libs", + "version": "0.4010.0" + }, + "libvala-0.42.so.0": { + "package": "vala", + "version": "0.0.0" + }, + "libvalaccodegen.so": { + "package": "vala", + "version": "0" + }, + "libvaladoc-0.42.so.0": { + "package": "vala", + "version": "0.0.0" + }, + "libverto-libev.so.1": { + "package": "libverto-libev", + "version": "1.0.0" + }, + "libsoxr-lsr.so.0": { + "package": "soxr", + "version": "0.1.9" + }, + "libsoxr.so.0": { + "package": "soxr", + "version": "0.1.2" + }, + "libobrender.so.32": { + "package": "openbox-libs", + "version": "32.0.0" + }, + "libobt.so.2": { + "package": "openbox-libs", + "version": "2.0.2" + }, + "libtonezone.so.2": { + "package": "dahdi-tools", + "version": "2.0.0" + }, + "libgtest1.so.41": { + "package": "nss-dev", + "version": "41" + }, + "libnsssysinit.so": { + "package": "nss-dev", + "version": "41" + }, + "libXxf86dga.so.1": { + "package": "libxxf86dga", + "version": "1.0.0" + }, + "libtsocks.so.1.8": { + "package": "tsocks", + "version": "1.8" + }, + "libsvn_swig_py-1.so.0": { + "package": "py-subversion", + "version": "0.0.0" + }, + "libssl.so.1.1": { + "package": "libssl1.1", + "version": "1.1" + }, + "libwayland-server.so.0": { + "package": "wayland-libs-server", + "version": "0.1.0" + }, + "libisns.so.0": { + "package": "open-isns-lib", + "version": "0" + }, + "libsamba-net-samba4.so": { + "package": "samba-libs-py", + "version": "0" + }, + "libsamba-python-samba4.so": { + "package": "samba-libs-py", + "version": "0" + }, + "liblua-5.2.so.0": { + "package": "lua5.2-libs", + "version": "0.0.0" + }, + "libxmlrpc++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_abyss++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_cpp.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_packetsocket.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_server++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_server_abyss++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_server_cgi++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_server_pstream++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libxmlrpc_util++.so.8": { + "package": "xmlrpc-c++", + "version": "8.39" + }, + "libgss.so.1": { + "package": "libgss", + "version": "1.1.0" + }, + "libcunit.so.1": { + "package": "cunit", + "version": "1.0.1" + }, + "libct.so.4": { + "package": "freetds", + "version": "4.0.0" + }, + "libsybdb.so.5": { + "package": "freetds", + "version": "5.1.0" + }, + "libtdsodbc.so.0": { + "package": "freetds", + "version": "0.0.0" + }, + "libSDL-1.2.so.0": { + "package": "sdl", + "version": "0.11.4" + }, + "liblucene++-contrib.so.0": { + "package": "lucene++", + "version": "3.0.7" + }, + "liblucene++.so.0": { + "package": "lucene++", + "version": "3.0.7" + }, + "libboost_iostreams-mt.so.1.67.0": { + "package": "boost-iostreams", + "version": "1.67.0" + }, + "libboost_iostreams.so.1.67.0": { + "package": "boost-iostreams", + "version": "1.67.0" + }, + "libelf.so.1": { + "package": "elfutils-libelf", + "version": "0" + }, + "libboost_graph-mt.so.1.67.0": { + "package": "boost-graph", + "version": "1.67.0" + }, + "libboost_graph.so.1.67.0": { + "package": "boost-graph", + "version": "1.67.0" + }, + "libpcre16.so.0": { + "package": "libpcre16", + "version": "0.2.10" + }, + "libgsl.so.23": { + "package": "gsl", + "version": "23.1.0" + }, + "libgslcblas.so.0": { + "package": "gsl", + "version": "0.0.0" + }, + "libf2fs.so.6": { + "package": "f2fs-tools-libs", + "version": "6.0.0" + }, + "libf2fs_format.so.5": { + "package": "f2fs-tools-libs", + "version": "5.0.0" + }, + "libc-client.so.1": { + "package": "c-client", + "version": "1.0.0" + }, + "libssh2.so.1": { + "package": "libssh2", + "version": "1.0.1" + }, + "libxcb-ewmh.so.2": { + "package": "xcb-util-wm", + "version": "2.0.0" + }, + "libxcb-icccm.so.4": { + "package": "xcb-util-wm", + "version": "4.0.0" + }, + "libsigc-2.0.so.0": { + "package": "libsigc++", + "version": "0.0.0" + }, + "libboost_wserialization-mt.so.1.67.0": { + "package": "boost-wserialization", + "version": "1.67.0" + }, + "libboost_wserialization.so.1.67.0": { + "package": "boost-wserialization", + "version": "1.67.0" + }, + "libcelt051.so.0": { + "package": "celt051", + "version": "0.0.0" + }, + "libfl.so.2": { + "package": "flex-libs", + "version": "2.0.0" + }, + "libmtp.so.9": { + "package": "libmtp", + "version": "9.4.0" + }, + "libblkid.so.1": { + "package": "libblkid", + "version": "1.1.0" + }, + "libvulkan_intel.so": { + "package": "mesa-vulkan-intel", + "version": "0" + }, + "libcanberra.so.0": { + "package": "libcanberra", + "version": "0.2.5" + }, + "libpciaccess.so.0": { + "package": "libpciaccess", + "version": "0.11.1" + }, + "libXcursor.so.1": { + "package": "libxcursor", + "version": "1.0.2" + }, + "libisoburn.so.1": { + "package": "libisoburn", + "version": "1.105.0" + }, + "libfreeswitch.so.1": { + "package": "freeswitch", + "version": "1.0.0" + }, + "libgiomm-2.4.so.1": { + "package": "glibmm", + "version": "1.3.0" + }, + "libglibmm-2.4.so.1": { + "package": "glibmm", + "version": "1.3.0" + }, + "libglibmm_generate_extra_defs-2.4.so.1": { + "package": "glibmm", + "version": "1.3.0" + }, + "libOSMesa.so.8": { + "package": "mesa-osmesa", + "version": "8.0.0" + }, + "libxcb-composite.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-damage.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-dpms.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-dri2.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-dri3.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-glx.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-present.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-randr.so.0": { + "package": "libxcb", + "version": "0.1.0" + }, + "libxcb-record.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-render.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-res.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-screensaver.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-shape.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-shm.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-sync.so.1": { + "package": "libxcb", + "version": "1.0.0" + }, + "libxcb-xf86dri.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xfixes.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xinerama.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xinput.so.0": { + "package": "libxcb", + "version": "0.1.0" + }, + "libxcb-xkb.so.1": { + "package": "libxcb", + "version": "1.0.0" + }, + "libxcb-xtest.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xv.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb-xvmc.so.0": { + "package": "libxcb", + "version": "0.0.0" + }, + "libxcb.so.1": { + "package": "libxcb", + "version": "1.1.0" + }, + "libXt.so.6": { + "package": "libxt", + "version": "6.0.0" + }, + "libebt_802_3.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_among.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_arp.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_arpreply.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_ip.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_ip6.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_limit.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_log.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_mark.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_mark_m.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_nat.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_nflog.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_pkttype.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_redirect.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_standard.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_stp.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_ulog.so": { + "package": "ebtables", + "version": "0" + }, + "libebt_vlan.so": { + "package": "ebtables", + "version": "0" + }, + "libebtable_broute.so": { + "package": "ebtables", + "version": "0" + }, + "libebtable_filter.so": { + "package": "ebtables", + "version": "0" + }, + "libebtable_nat.so": { + "package": "ebtables", + "version": "0" + }, + "libebtc.so": { + "package": "ebtables", + "version": "0" + }, + "libmandoc.so": { + "package": "mdocml", + "version": "0" + }, + "libsgutils2.so.2": { + "package": "sg3_utils", + "version": "2.0.0" + }, + "libmupdf.so.0": { + "package": "mupdf", + "version": "0" + }, + "libmupdfthird.so.0": { + "package": "mupdf", + "version": "0" + }, + "librrd.so.8": { + "package": "librrd", + "version": "8.1.0" + }, + "libdns_sd.so.1": { + "package": "avahi-compat-libdns_sd", + "version": "1.0.0" + }, + "libverto-libevent.so.1": { + "package": "libverto-libevent", + "version": "1.0.0" + }, + "libXi.so.6": { + "package": "libxi", + "version": "6.1.0" + }, + "libcap.so.2": { + "package": "libcap", + "version": "2.26" + }, + "libboost_signals-mt.so.1.67.0": { + "package": "boost-signals", + "version": "1.67.0" + }, + "libboost_signals.so.1.67.0": { + "package": "boost-signals", + "version": "1.67.0" + }, + "libhavege.so.1": { + "package": "haveged", + "version": "1.1.0" + }, + "libGL.so.1": { + "package": "mesa-gl", + "version": "1.2.0" + }, + "libgdk_pixbuf-2.0.so.0": { + "package": "gdk-pixbuf", + "version": "0.3611.0" + }, + "libgdk_pixbuf_xlib-2.0.so.0": { + "package": "gdk-pixbuf", + "version": "0.3611.0" + }, + "libksba.so.8": { + "package": "libksba", + "version": "8.11.6" + }, + "libespeak.so.1": { + "package": "espeak", + "version": "1.1.48" + }, + "libboost_unit_test_framework-mt.so.1.67.0": { + "package": "boost-unit_test_framework", + "version": "1.67.0" + }, + "libboost_unit_test_framework.so.1.67.0": { + "package": "boost-unit_test_framework", + "version": "1.67.0" + }, + "libgssglue.so.1": { + "package": "libgssglue", + "version": "1.0.0" + }, + "libestr.so.0": { + "package": "libestr", + "version": "0.0.0" + }, + "libargon2.so.1": { + "package": "argon2-libs", + "version": "1" + }, + "libvte-2.91.so.0": { + "package": "vte3", + "version": "0.5200.2" + }, + "libfcgi.so.0": { + "package": "fcgi", + "version": "0.0.0" + }, + "libspf2.so.2": { + "package": "libspf2", + "version": "2.1.0" + }, + "libreplace-samba4.so": { + "package": "libwbclient", + "version": "0" + }, + "libwbclient.so.0": { + "package": "libwbclient", + "version": "0.14" + }, + "libwinbind-client-samba4.so": { + "package": "libwbclient", + "version": "0" + }, + "libopusfile.so.0": { + "package": "opusfile", + "version": "0.4.4" + }, + "libopusurl.so.0": { + "package": "opusfile", + "version": "0.4.4" + }, + "libfreebl3.so": { + "package": "nss", + "version": "41" + }, + "libgtestutil.so.41": { + "package": "nss", + "version": "41" + }, + "libnss3.so": { + "package": "nss", + "version": "41" + }, + "libnssckbi.so": { + "package": "nss", + "version": "41" + }, + "libnssdbm3.so": { + "package": "nss", + "version": "41" + }, + "libnssutil3.so": { + "package": "nss", + "version": "41" + }, + "libsmime3.so": { + "package": "nss", + "version": "41" + }, + "libsoftokn3.so": { + "package": "nss", + "version": "41" + }, + "libssl3.so": { + "package": "nss", + "version": "41" + }, + "libatk-1.0.so.0": { + "package": "atk", + "version": "0.23009.1" + }, + "libnetfilter_queue.so.1": { + "package": "libnetfilter_queue", + "version": "1.4.0" + }, + "lib90_sieve_plugin.so": { + "package": "dovecot-pigeonhole-plugin", + "version": "0" + }, + "lib95_imap_filter_sieve_plugin.so": { + "package": "dovecot-pigeonhole-plugin", + "version": "0" + }, + "lib95_imap_sieve_plugin.so": { + "package": "dovecot-pigeonhole-plugin", + "version": "0" + }, + "libdovecot-sieve.so.0": { + "package": "dovecot-pigeonhole-plugin", + "version": "0.0.0" + }, + "libzdb.so.11": { + "package": "libzdb", + "version": "11.0.0" + }, + "libsamplerate.so.0": { + "package": "libsamplerate", + "version": "0.1.8" + }, + "libnl-3.so.200": { + "package": "libnl3", + "version": "200.26.0" + }, + "libnl-genl-3.so.200": { + "package": "libnl3", + "version": "200.26.0" + }, + "libnl-idiag-3.so.200": { + "package": "libnl3", + "version": "200.26.0" + }, + "libnl-nf-3.so.200": { + "package": "libnl3", + "version": "200.26.0" + }, + "libnl-route-3.so.200": { + "package": "libnl3", + "version": "200.26.0" + }, + "libnl-xfrm-3.so.200": { + "package": "libnl3", + "version": "200.26.0" + }, + "libeinfo.so.1": { + "package": "openrc", + "version": "1" + }, + "librc.so.1": { + "package": "openrc", + "version": "1" + }, + "libformw.so.5": { + "package": "ncurses5-widec-libs", + "version": "5.9" + }, + "libmenuw.so.5": { + "package": "ncurses5-widec-libs", + "version": "5.9" + }, + "libncursesw.so.5": { + "package": "ncurses5-widec-libs", + "version": "5.9" + }, + "libpanelw.so.5": { + "package": "ncurses5-widec-libs", + "version": "5.9" + }, + "libavahi-client.so.3": { + "package": "avahi-libs", + "version": "3.2.9" + }, + "libavahi-common.so.3": { + "package": "avahi-libs", + "version": "3.5.3" + }, + "libfdt.so.1": { + "package": "libfdt", + "version": "0" + }, + "libfuse3.so.3": { + "package": "fuse3", + "version": "3.2.6" + }, + "libjpeg.so.8": { + "package": "libjpeg-turbo", + "version": "8.1.2" + }, + "libturbojpeg.so.0": { + "package": "libjpeg-turbo", + "version": "0.1.0" + }, + "libexiv2.so.26": { + "package": "exiv2", + "version": "26.0.0" + }, + "libxml2.so.2": { + "package": "libxml2", + "version": "2.9.9" + }, + "libogg.so.0": { + "package": "libogg", + "version": "0.8.3" + }, + "libcupsfilters.so.1": { + "package": "cups-filters-libs", + "version": "1.0.0" + }, + "libfontembed.so.1": { + "package": "cups-filters-libs", + "version": "1.0.0" + }, + "libwv-1.2.so.4": { + "package": "wv", + "version": "4.0.5" + }, + "libapr-1.so.0": { + "package": "apr", + "version": "0.6.5" + }, + "libmikmod.so.3": { + "package": "libmikmod", + "version": "3.3.0" + }, + "rlm_sql_sqlite.so": { + "package": "freeradius-sqlite", + "version": "0" + }, + "libpoppler-cpp.so.0": { + "package": "poppler", + "version": "0.3.0" + }, + "libpoppler.so.67": { + "package": "poppler", + "version": "67.0.0" + }, + "libecpg.so.6": { + "package": "postgresql-libs", + "version": "6.11" + }, + "libecpg_compat.so.3": { + "package": "postgresql-libs", + "version": "3.11" + }, + "libpgtypes.so.3": { + "package": "postgresql-libs", + "version": "3.11" + }, + "libboost_context-mt.so.1.67.0": { + "package": "boost-context", + "version": "1.67.0" + }, + "libnetfilter_cttimeout.so.1": { + "package": "libnetfilter_cttimeout", + "version": "1.0.0" + }, + "libboost_python27-mt.so.1.67.0": { + "package": "boost-python", + "version": "1.67.0" + }, + "libboost_python27.so.1.67.0": { + "package": "boost-python", + "version": "1.67.0" + }, + "libSDL2_mixer-2.0.so.0": { + "package": "sdl2_mixer", + "version": "0.2.2" + }, + "libfaad.so.2": { + "package": "faad2", + "version": "2.0.0" + }, + "libhylafax-6.0.so.7": { + "package": "hylafax", + "version": "7" + }, + "libcogl-gles2.so.20": { + "package": "cogl", + "version": "20.4.2" + }, + "libcogl-pango.so.20": { + "package": "cogl", + "version": "20.4.2" + }, + "libcogl-path.so.20": { + "package": "cogl", + "version": "20.4.2" + }, + "libcogl.so.20": { + "package": "cogl", + "version": "20.4.2" + }, + "libyajl.so.2": { + "package": "yajl", + "version": "2.1.0" + }, + "libcommon.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libmc.so": { + "package": "xrdp", + "version": "0" + }, + "libpainter.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "librfxencode.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libscp.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libvnc.so": { + "package": "xrdp", + "version": "0" + }, + "libxrdp.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libxrdpapi.so.0": { + "package": "xrdp", + "version": "0.0.0" + }, + "libxup.so": { + "package": "xrdp", + "version": "0" + }, + "libwavpack.so.1": { + "package": "wavpack", + "version": "1.2.0" + }, + "libmilter.so.1.0.2": { + "package": "libmilter", + "version": "1.0.2" + }, + "libboost_serialization-mt.so.1.67.0": { + "package": "boost-serialization", + "version": "1.67.0" + }, + "libboost_serialization.so.1.67.0": { + "package": "boost-serialization", + "version": "1.67.0" + }, + "libgsf-1.so.114": { + "package": "libgsf", + "version": "114.0.44" + }, + "libluajit-5.1.so.2": { + "package": "luajit", + "version": "2.1.0" + }, + "libgirepository-1.0.so.1": { + "package": "gobject-introspection", + "version": "1.0.0" + }, + "libXaw.so.7": { + "package": "libxaw", + "version": "7.0.0" + }, + "libwayland-client.so.0": { + "package": "wayland-libs-client", + "version": "0.3.0" + }, + "proto_dhcp.so": { + "package": "freeradius", + "version": "0" + }, + "proto_vmps.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_always.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_attr_filter.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_cache.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_cache_rbtree.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_chap.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_counter.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_cram.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_date.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_detail.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_dhcp.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_digest.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_dynamic_clients.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_exec.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_expiration.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_expr.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_files.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_ippool.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_linelog.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_logintime.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_mschap.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_otp.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_pap.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_passwd.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_preprocess.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_radutmp.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_realm.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_replicate.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_soh.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_sometimes.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_test.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_unix.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_unpack.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_utf8.so": { + "package": "freeradius", + "version": "0" + }, + "rlm_wimax.so": { + "package": "freeradius", + "version": "0" + }, + "libatk-bridge-2.0.so.0": { + "package": "at-spi2-atk", + "version": "0.0.0" + }, + "postfix-ldap.so": { + "package": "postfix-ldap", + "version": "0" + }, + "libsylph-0.so.1": { + "package": "sylpheed", + "version": "1.3.0" + }, + "libsylpheed-plugin-0.so.1": { + "package": "sylpheed", + "version": "1.3.0" + }, + "libssl.so.45": { + "package": "libressl2.7-libssl", + "version": "45.0.1" + }, + "libcryptsetup.so.4": { + "package": "cryptsetup1-libs", + "version": "4.7.0" + }, + "libgphoto2.so.6": { + "package": "libgphoto2", + "version": "6.0.0" + }, + "libgphoto2_port.so.12": { + "package": "libgphoto2", + "version": "12.0.0" + }, + "libpoppler-qt4.so.4": { + "package": "poppler-qt4", + "version": "4.11.0" + }, + "libexecline.so.2.5": { + "package": "execline", + "version": "2.5.0.1" + }, + "libskarnet.so.2.7": { + "package": "skalibs", + "version": "2.7.0.0" + }, + "rlm_sql.so": { + "package": "freeradius-sql", + "version": "0" + }, + "rlm_sql_null.so": { + "package": "freeradius-sql", + "version": "0" + }, + "rlm_sqlcounter.so": { + "package": "freeradius-sql", + "version": "0" + }, + "rlm_sqlippool.so": { + "package": "freeradius-sql", + "version": "0" + }, + "libcom_err.so.2": { + "package": "libcom_err", + "version": "2.1" + }, + "libobjc.so.4": { + "package": "libobjc", + "version": "4.0.0" + }, + "libfuse.so.2": { + "package": "fuse", + "version": "2.9.8" + }, + "libulockmgr.so.1": { + "package": "fuse", + "version": "1.0.1" + }, + "libsndfile.so.1": { + "package": "libsndfile", + "version": "1.0.28" + }, + "libaudit.so.1": { + "package": "audit-libs", + "version": "1.0.0" + }, + "libauparse.so.0": { + "package": "audit-libs", + "version": "0.0.0" + }, + "libsmbclient.so.0": { + "package": "libsmbclient", + "version": "0.3.1" + }, + "libspice-server.so.1": { + "package": "spice-server", + "version": "1.12.5" + }, + "libencfs.so.1.9": { + "package": "encfs", + "version": "1.9.5" + }, + "libdmmp.so.0.2.0": { + "package": "multipath-tools", + "version": "0.2.0" + }, + "libmpathcmd.so.0": { + "package": "multipath-tools", + "version": "0" + }, + "libmpathpersist.so.0": { + "package": "multipath-tools", + "version": "0" + }, + "libmultipath.so.0": { + "package": "multipath-tools", + "version": "0" + }, + "libgps.so.24": { + "package": "gpsd", + "version": "24.0.0" + }, + "libdbi.so.1": { + "package": "libdbi", + "version": "1.1.0" + }, + "libiniparser.so.1": { + "package": "iniparser", + "version": "1" + }, + "libdaq.so.2": { + "package": "daq", + "version": "2.0.4" + }, + "libsfbpf.so.0": { + "package": "daq", + "version": "0.0.1" + }, + "librsvg-2.so.2": { + "package": "librsvg", + "version": "2.40.20" + }, + "libraw1394.so.11": { + "package": "libraw1394", + "version": "11.1.0" + }, + "libtag.so.1": { + "package": "taglib", + "version": "1.17.0" + }, + "libtag_c.so.0": { + "package": "taglib", + "version": "0.0.0" + }, + "libboost_wave-mt.so.1.67.0": { + "package": "boost-wave", + "version": "1.67.0" + }, + "libsqlite3.so.0": { + "package": "sqlite-libs", + "version": "0.8.6" + }, + "libgee-0.8.so.2": { + "package": "libgee", + "version": "2.6.1" + }, + "libkeybinder.so.0": { + "package": "keybinder", + "version": "0.1.0" + }, + "libvpx.so.4": { + "package": "libvpx", + "version": "4.1.0" + }, + "libjansson.so.4": { + "package": "jansson", + "version": "4.11.0" + }, + "libSM.so.6": { + "package": "libsm", + "version": "6.0.1" + }, + "libasound.so.2": { + "package": "alsa-lib", + "version": "2.0.0" + }, + "libdb_cxx-5.3.so": { + "package": "db-c++", + "version": "0" + }, + "libexif.so.12": { + "package": "libexif", + "version": "12.3.3" + }, + "libpcre2-16.so.0": { + "package": "libpcre2-16", + "version": "0.7.1" + }, + "libcdio++.so.0": { + "package": "libcdio++", + "version": "0.0.2" + }, + "libiso9660++.so.0": { + "package": "libcdio++", + "version": "0.0.0" + }, + "libdb-5.3.so": { + "package": "db", + "version": "0" + }, + "libcddb.so.2": { + "package": "libcddb", + "version": "2.2.3" + }, + "libfarstream-0.2.so.5": { + "package": "farstream", + "version": "5.1.0" + }, + "libcppunit_subunit.so.0": { + "package": "subunit-libs", + "version": "0.0.0" + }, + "libsubunit.so.0": { + "package": "subunit-libs", + "version": "0.0.0" + }, + "libnetfilter_cthelper.so.0": { + "package": "libnetfilter_cthelper", + "version": "0.0.0" + }, + "libmp3lame.so.0": { + "package": "lame", + "version": "0.0.0" + }, + "libodbc.so.2": { + "package": "unixodbc", + "version": "2.0.0" + }, + "libodbccr.so.2": { + "package": "unixodbc", + "version": "2.0.0" + }, + "libodbcinst.so.2": { + "package": "unixodbc", + "version": "2.0.0" + }, + "rlm_ldap.so": { + "package": "freeradius-ldap", + "version": "0" + }, + "libsmbpasswdparser-samba4.so": { + "package": "samba-dc", + "version": "0" + }, + "libmatroska.so.6": { + "package": "libmatroska", + "version": "6.0.0" + }, + "libconfig.so.9": { + "package": "libconfig", + "version": "9.2.0" + }, + "libLLVM-5.0.so": { + "package": "llvm5-libs", + "version": "0" + }, + "libsecret-1.so.0": { + "package": "libsecret", + "version": "0.0.0" + }, + "libnpth.so.0": { + "package": "npth", + "version": "0.1.2" + }, + "libgbm.so.1": { + "package": "mesa-gbm", + "version": "1.0.0" + }, + "libenchant.so.1": { + "package": "enchant", + "version": "1.6.0" + }, + "libcairo-script-interpreter.so.2": { + "package": "cairo", + "version": "2.11600.0" + }, + "libcairo.so.2": { + "package": "cairo", + "version": "2.11600.0" + }, + "libvte.so.9": { + "package": "vte", + "version": "9.2800.2" + }, + "libjsoncpp.so.20": { + "package": "jsoncpp", + "version": "20" + }, + "libvncclient.so.1": { + "package": "libvncserver", + "version": "1.0.0" + }, + "libvncserver.so.1": { + "package": "libvncserver", + "version": "1.0.0" + }, + "rlm_perl.so": { + "package": "freeradius-perl", + "version": "0" + }, + "libdriver_sqlite.so": { + "package": "dovecot-sqlite", + "version": "0" + }, + "libxdg-basedir.so.1": { + "package": "libxdg-basedir", + "version": "1.2.0" + }, + "libfftw3f.so.3": { + "package": "fftw-single-libs", + "version": "3.5.8" + }, + "libfftw3f_threads.so.3": { + "package": "fftw-single-libs", + "version": "3.5.8" + }, + "libxcb-render-util.so.0": { + "package": "xcb-util-renderutil", + "version": "0.0.0" + }, + "libverto-glib.so.1": { + "package": "libverto-glib", + "version": "1.0.0" + }, + "libpcre32.so.0": { + "package": "libpcre32", + "version": "0.0.10" + }, + "libXft.so.2": { + "package": "libxft", + "version": "2.3.2" + }, + "libiptcdata.so.0": { + "package": "libiptcdata", + "version": "0.3.3" + }, + "libvulkan_radeon.so": { + "package": "mesa-vulkan-ati", + "version": "0" + }, + "libGLU.so.1": { + "package": "glu", + "version": "1.3.1" + }, + "libevent-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libevent_core-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libevent_extra-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libevent_openssl-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libevent_pthreads-2.1.so.6": { + "package": "libevent", + "version": "6.0.2" + }, + "libgstadder.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstallocators-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstalsa.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstapp-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstapp.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstaudio-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstaudioconvert.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstaudiomixer.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstaudiorate.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstaudioresample.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstaudiotestsrc.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstcdparanoia.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstencoding.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstfft-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstgio.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstgl-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstogg.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstopengl.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstopus.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstpango.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstpbtypes.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstpbutils-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstplayback.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstrawparse.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstriff-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstrtp-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstrtsp-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstsdp-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstsubparse.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgsttag-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgsttcp.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgsttheora.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgsttypefindfunctions.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstvideo-1.0.so.0": { + "package": "gst-plugins-base", + "version": "0.1404.0" + }, + "libgstvideoconvert.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstvideorate.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstvideoscale.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstvideotestsrc.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstvolume.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstvorbis.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstximagesink.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libgstxvimagesink.so": { + "package": "gst-plugins-base", + "version": "0" + }, + "libosp.so.5": { + "package": "opensp", + "version": "5.0.0" + }, + "libgettextpo.so.0": { + "package": "gettext-libs", + "version": "0.5.4" + }, + "libpostfix-dns.so": { + "package": "postfix", + "version": "0" + }, + "libpostfix-global.so": { + "package": "postfix", + "version": "0" + }, + "libpostfix-master.so": { + "package": "postfix", + "version": "0" + }, + "libpostfix-tls.so": { + "package": "postfix", + "version": "0" + }, + "libpostfix-util.so": { + "package": "postfix", + "version": "0" + }, + "libboost_random-mt.so.1.67.0": { + "package": "boost-random", + "version": "1.67.0" + }, + "libboost_random.so.1.67.0": { + "package": "boost-random", + "version": "1.67.0" + }, + "libunbound.so.8": { + "package": "unbound-libs", + "version": "8.0.3" + }, + "postfix-mysql.so": { + "package": "postfix-mysql", + "version": "0" + }, + "libwx_gtk2u_media-2.8.so.0": { + "package": "wxgtk2.8-media", + "version": "0.8.0" + }, + "cpufreqd_acpi.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_apm.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_cpu.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_exec.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_governor_parameters.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_nforce2.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_pmu.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_programs.so": { + "package": "cpufreqd", + "version": "0" + }, + "cpufreqd_tau.so": { + "package": "cpufreqd", + "version": "0" + }, + "libexslt.so.0": { + "package": "libxslt", + "version": "0.8.20" + }, + "libxslt.so.1": { + "package": "libxslt", + "version": "1.1.33" + }, + "libortp.so.10": { + "package": "ortp", + "version": "10.0.0" + }, + "cmd-mirror.so": { + "package": "lftp", + "version": "0" + }, + "cmd-sleep.so": { + "package": "lftp", + "version": "0" + }, + "cmd-torrent.so": { + "package": "lftp", + "version": "0" + }, + "liblftp-jobs.so.0": { + "package": "lftp", + "version": "0.0.0" + }, + "liblftp-network.so": { + "package": "lftp", + "version": "0" + }, + "liblftp-pty.so": { + "package": "lftp", + "version": "0" + }, + "liblftp-tasks.so.0": { + "package": "lftp", + "version": "0.0.0" + }, + "proto-file.so": { + "package": "lftp", + "version": "0" + }, + "proto-fish.so": { + "package": "lftp", + "version": "0" + }, + "proto-ftp.so": { + "package": "lftp", + "version": "0" + }, + "proto-http.so": { + "package": "lftp", + "version": "0" + }, + "proto-sftp.so": { + "package": "lftp", + "version": "0" + }, + "libclucene-contribs-lib.so.1": { + "package": "clucene-contribs", + "version": "2.3.3.4" + }, + "libcups.so.2": { + "package": "cups-libs", + "version": "2" + }, + "libcupsimage.so.2": { + "package": "cups-libs", + "version": "2" + }, + "libvanessa_logger.so.0": { + "package": "vanessa_logger", + "version": "0.0.5" + }, + "libboost_coroutine-mt.so.1.67.0": { + "package": "boost-coroutine", + "version": "1.67.0" + }, + "libboost_coroutine.so.1.67.0": { + "package": "boost-coroutine", + "version": "1.67.0" + }, + "libxmlrpc_client++.so.8": { + "package": "xmlrpc-c-client++", + "version": "8.39" + }, + "libck-connector.so.0": { + "package": "consolekit2", + "version": "0.0.0" + }, + "libconsolekit.so.1": { + "package": "consolekit2", + "version": "1.0.0" + }, + "libhttp_parser.so.2.8": { + "package": "http-parser", + "version": "2.8.1" + }, + "libotr.so.5": { + "package": "libotr", + "version": "5.1.1" + }, + "libmcrypt.so.4": { + "package": "libmcrypt", + "version": "4.4.8" + }, + "liblogging-stdlog.so.0": { + "package": "liblogging", + "version": "0.1.0" + }, + "libconfig++.so.9": { + "package": "libconfig++", + "version": "9.2.0" + }, + "libfts.so.0": { + "package": "fts", + "version": "0.0.0" + }, + "libvirglrenderer.so.0": { + "package": "virglrenderer", + "version": "0.2.0" + }, + "libpcsclite.so.1": { + "package": "pcsc-lite-libs", + "version": "1.0.0" + }, + "libpcscspy.so.0": { + "package": "pcsc-lite-libs", + "version": "0.0.0" + }, + "libvdpau.so.1": { + "package": "libvdpau", + "version": "1.0.0" + }, + "libczmq.so.4": { + "package": "czmq", + "version": "4.1.1" + }, + "libXRes.so.1": { + "package": "libxres", + "version": "1.0.0" + }, + "libXfixes.so.3": { + "package": "libxfixes", + "version": "3.1.0" + }, + "libbaccats-postgresql-9.4.1.so": { + "package": "bacula-pgsql", + "version": "0" + }, + "libdbus-glib-1.so.2": { + "package": "dbus-glib", + "version": "2.3.3" + }, + "libXfont.so.1": { + "package": "libxfont", + "version": "1.4.1" + }, + "libwayland-egl.so.1": { + "package": "wayland-libs-egl", + "version": "1.0.0" + }, + "libnjb.so.5": { + "package": "libnjb", + "version": "5.1.1" + }, + "libglade-2.0.so.0": { + "package": "libglade", + "version": "0.0.7" + }, + "libportaudio.so.2": { + "package": "portaudio", + "version": "2.0.0" + }, + "libpaper.so.1": { + "package": "libpaper", + "version": "1.1.2" + }, + "librsync.so.2": { + "package": "librsync", + "version": "2.0.2" + }, + "libvarnishapi.so.2": { + "package": "varnish-libs", + "version": "2.0.0" + }, + "libHDB-SAMBA4-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libLIBWBCLIENT-OLD-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libauth-unix-token-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libauth4-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libcluster-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libdb-glue-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libdcerpc-samr.so.0": { + "package": "samba-dc-libs", + "version": "0.0.1" + }, + "libdcerpc-server.so.0": { + "package": "samba-dc-libs", + "version": "0.0.1" + }, + "libdnsserver-common-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libdsdb-module-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libhdb-samba4.so.11": { + "package": "samba-dc-libs", + "version": "11.0.2" + }, + "libkdc-samba4.so.2": { + "package": "samba-dc-libs", + "version": "2.0.0" + }, + "libpac-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libposix-eadb-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libprocess-model-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libsamba-policy.so.0": { + "package": "samba-dc-libs", + "version": "0.0.1" + }, + "libservice-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libshares-samba4.so": { + "package": "samba-dc-libs", + "version": "0" + }, + "libgsm.so.1": { + "package": "gsm", + "version": "1.0.12" + }, + "libpython2.7.so.1.0": { + "package": "python2", + "version": "1.0" + }, + "postfix-lmdb.so": { + "package": "postfix-lmdb", + "version": "0" + }, + "libXdmcp.so.6": { + "package": "libxdmcp", + "version": "6.0.0" + }, + "libopenobex.so.2": { + "package": "openobex", + "version": "1.7.2" + }, + "libgettextlib-0.19.8.1.so": { + "package": "gettext", + "version": "0" + }, + "libgettextsrc-0.19.8.1.so": { + "package": "gettext", + "version": "0" + }, + "libcheck.so.0": { + "package": "check", + "version": "0.0.0" + }, + "libatspi.so.0": { + "package": "at-spi2-core", + "version": "0.0.1" + }, + "libdrm.so.2": { + "package": "libdrm", + "version": "2.4.0" + }, + "libdrm_amdgpu.so.1": { + "package": "libdrm", + "version": "1.0.0" + }, + "libdrm_freedreno.so.1": { + "package": "libdrm", + "version": "1.0.0" + }, + "libdrm_intel.so.1": { + "package": "libdrm", + "version": "1.0.0" + }, + "libdrm_nouveau.so.2": { + "package": "libdrm", + "version": "2.0.0" + }, + "libdrm_radeon.so.1": { + "package": "libdrm", + "version": "1.0.1" + }, + "libkms.so.1": { + "package": "libdrm", + "version": "1.0.0" + }, + "libdvbcsa.so.1": { + "package": "libdvbcsa", + "version": "1.0.1" + }, + "liblua.so.5": { + "package": "lua5.1-libs", + "version": "5.1.4" + }, + "libmad.so.0": { + "package": "libmad", + "version": "0.2.1" + }, + "libpcre.so.1": { + "package": "pcre", + "version": "1.2.10" + }, + "libpcreposix.so.0": { + "package": "pcre", + "version": "0.0.6" + }, + "libevtlog-3.19.so.0": { + "package": "syslog-ng", + "version": "0.0.0" + }, + "libloggen_helper-3.19.so.0": { + "package": "syslog-ng", + "version": "0.0.0" + }, + "libloggen_plugin-3.19.so.0": { + "package": "syslog-ng", + "version": "0.0.0" + }, + "libsecret-storage.so.0": { + "package": "syslog-ng", + "version": "0.0.0" + }, + "libsyslog-ng-3.19.so.0": { + "package": "syslog-ng", + "version": "0.0.0" + }, + "libvanessa_socket.so.2": { + "package": "vanessa_socket", + "version": "2.1.0" + }, + "libdnet.so.1": { + "package": "libdnet", + "version": "1.0.1" + }, + "postgresql-bdr:libecpg.so.6": { + "package": "postgresql-bdr", + "version": "6.6" + }, + "postgresql-bdr:libecpg_compat.so.3": { + "package": "postgresql-bdr", + "version": "3.6" + }, + "postgresql-bdr:libpgtypes.so.3": { + "package": "postgresql-bdr", + "version": "3.5" + }, + "libxcb-keysyms.so.1": { + "package": "xcb-util-keysyms", + "version": "1.0.0" + }, + "libsvn_swig_perl-1.so.0": { + "package": "perl-subversion", + "version": "0.0.0" + }, + "liblmdb.so.0": { + "package": "lmdb", + "version": "0.0.0" + }, + "libIPMIlanserv.so.0": { + "package": "openipmi-lanserv", + "version": "0.0.1" + }, + "libpcre2-8.so.0": { + "package": "pcre2", + "version": "0.7.1" + }, + "libpcre2-posix.so.2": { + "package": "pcre2", + "version": "2.0.1" + }, + "libevtlog.so.0": { + "package": "eventlog", + "version": "0.0.0" + }, + "libSDL2_ttf-2.0.so.0": { + "package": "sdl2_ttf", + "version": "0.14.0" + }, + "libbz2.so.1": { + "package": "libbz2", + "version": "1.0.6" + }, + "libboost_program_options-mt.so.1.67.0": { + "package": "boost-program_options", + "version": "1.67.0" + }, + "libboost_program_options.so.1.67.0": { + "package": "boost-program_options", + "version": "1.67.0" + }, + "libMESSAGING-SEND-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libcli-spoolss-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libdcerpc-binding.so.0": { + "package": "samba-common-libs", + "version": "0.0.1" + }, + "libdcerpc-samba-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "liblibcli-lsa3-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "liblibcli-netlogon3-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "liblibsmb-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libmsrpc3-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libndr-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libsamba-passdb.so.0": { + "package": "samba-common-libs", + "version": "0.27.2" + }, + "libtrusts-util-samba4.so": { + "package": "samba-common-libs", + "version": "0" + }, + "libyaml-0.so.2": { + "package": "yaml", + "version": "2.0.5" + }, + "libatasmart.so.4": { + "package": "libatasmart", + "version": "4.0.5" + }, + "libgif.so.7": { + "package": "giflib", + "version": "7.0.0" + }, + "libmpc.so.3": { + "package": "mpc1", + "version": "3.0.0" + }, + "bpipe-fd.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbac-9.4.1.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbaccfg-9.4.1.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbacfind-9.4.1.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbacsd-9.4.1.so": { + "package": "bacula-libs", + "version": "0" + }, + "libbacsql-9.4.1.so": { + "package": "bacula-libs", + "version": "0" + }, + "libclang.so.5.0": { + "package": "clang-libs", + "version": "5.0" + }, + "group_file.so": { + "package": "sudo", + "version": "0" + }, + "libsudo_noexec.so": { + "package": "sudo", + "version": "0" + }, + "libsudo_util.so.0": { + "package": "sudo", + "version": "0.0.0" + }, + "sudoers.so": { + "package": "sudo", + "version": "0" + }, + "system_group.so": { + "package": "sudo", + "version": "0" + }, + "libs6dns.so.2.3": { + "package": "s6-dns", + "version": "2.3.0.1" + }, + "libskadns.so.2.3": { + "package": "s6-dns", + "version": "2.3.0.1" + }, + "libI810XvMC.so.1": { + "package": "xf86-video-intel", + "version": "1.0.0" + }, + "libIntelXvMC.so.1": { + "package": "xf86-video-intel", + "version": "1.0.0" + }, + "libasn1.so.8": { + "package": "heimdal-libs", + "version": "8.0.0" + }, + "libgssapi.so.3": { + "package": "heimdal-libs", + "version": "3.0.0" + }, + "libhcrypto.so.4": { + "package": "heimdal-libs", + "version": "4.1.0" + }, + "libhdb.so.9": { + "package": "heimdal-libs", + "version": "9.2.0" + }, + "libheimbase.so.1": { + "package": "heimdal-libs", + "version": "1.0.0" + }, + "libheimntlm.so.0": { + "package": "heimdal-libs", + "version": "0.1.0" + }, + "libhx509.so.5": { + "package": "heimdal-libs", + "version": "5.0.0" + }, + "libkadm5clnt.so.7": { + "package": "heimdal-libs", + "version": "7.0.1" + }, + "libkadm5srv.so.8": { + "package": "heimdal-libs", + "version": "8.0.1" + }, + "libkafs.so.0": { + "package": "heimdal-libs", + "version": "0.5.1" + }, + "libkdc.so.2": { + "package": "heimdal-libs", + "version": "2.0.0" + }, + "libkrb5.so.26": { + "package": "heimdal-libs", + "version": "26.0.0" + }, + "libotp.so.0": { + "package": "heimdal-libs", + "version": "0.1.5" + }, + "libroken.so.18": { + "package": "heimdal-libs", + "version": "18.1.0" + }, + "libsl.so.0": { + "package": "heimdal-libs", + "version": "0.2.1" + }, + "libwind.so.0": { + "package": "heimdal-libs", + "version": "0.0.0" + }, + "windc.so.0": { + "package": "heimdal-libs", + "version": "0.0.0" + }, + "libcharon.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libradius.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libsimaka.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libstrongswan.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libtls.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libvici.so.0": { + "package": "strongswan", + "version": "0.0.0" + }, + "libXvMC.so.1": { + "package": "libxvmc", + "version": "1.0.0" + }, + "libXvMCW.so.1": { + "package": "libxvmc", + "version": "1.0.0" + }, + "libORBit-2.so.0": { + "package": "orbit2", + "version": "0.1.0" + }, + "libORBit-imodule-2.so.0": { + "package": "orbit2", + "version": "0.0.0" + }, + "libORBitCosNaming-2.so.0": { + "package": "orbit2", + "version": "0.1.0" + }, + "libquazip.so.1": { + "package": "quazip", + "version": "1.0.0" + }, + "liblldp_clif.so.1": { + "package": "open-lldp", + "version": "1.0.0" + }, + "libSDL_mixer-1.2.so.0": { + "package": "sdl_mixer", + "version": "0.12.0" + }, + "libfftw3.so.3": { + "package": "fftw-double-libs", + "version": "3.5.8" + }, + "libfftw3_threads.so.3": { + "package": "fftw-double-libs", + "version": "3.5.8" + }, + "libgcrypt.so.20": { + "package": "libgcrypt", + "version": "20.2.4" + }, + "libpyldb-util.so.1": { + "package": "py2-ldb", + "version": "1.3.8" + }, + "libshout.so.3": { + "package": "libshout", + "version": "3.2.0" + }, + "libidn.so.12": { + "package": "libidn", + "version": "12.6.0" + }, + "libmp3splt.so.0": { + "package": "libmp3splt", + "version": "0.0.9" + }, + "libpangomm-1.4.so.1": { + "package": "pangomm", + "version": "1.0.30" + }, + "libXp.so.6": { + "package": "libxp", + "version": "6.2.0" + }, + "libnetfilter_acct.so.1": { + "package": "libnetfilter_acct", + "version": "1.0.0" + }, + "libImlib2.so.1": { + "package": "imlib2", + "version": "1.5.1" + }, + "libbearssl.so.0": { + "package": "bearssl", + "version": "0.6" + }, + "libaspell.so.15": { + "package": "aspell-libs", + "version": "15.1.5" + }, + "libpspell.so.15": { + "package": "aspell-libs", + "version": "15.1.5" + }, + "libSDL2-2.0.so.0": { + "package": "sdl2", + "version": "0.9.0" + }, + "libproxy.so.1": { + "package": "libproxy", + "version": "1.0.0" + }, + "libical.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "libical_cxx.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "libicalss.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "libicalss_cxx.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "libicalvcal.so.2": { + "package": "libical", + "version": "2.0.0" + }, + "libcdt.so.5": { + "package": "graphviz", + "version": "5.0.0" + }, + "libcgraph.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvc.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_core.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_dot_layout.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_neato_layout.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_pango.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvplugin_xlib.so.6": { + "package": "graphviz", + "version": "6.0.0" + }, + "libgvpr.so.2": { + "package": "graphviz", + "version": "2.0.0" + }, + "liblab_gamut.so.1": { + "package": "graphviz", + "version": "1.0.0" + }, + "libpathplan.so.4": { + "package": "graphviz", + "version": "4.0.0" + }, + "libxdot.so.4": { + "package": "graphviz", + "version": "4.0.0" + }, + "libcloog-isl.so.4": { + "package": "cloog", + "version": "4.0.0" + }, + "libsysfs.so.2": { + "package": "sysfsutils", + "version": "2.0.1" + }, + "libnftnl.so.7": { + "package": "libnftnl-libs", + "version": "7.3.0" + }, + "libspeex.so.1": { + "package": "speex", + "version": "1.5.1" + }, + "libmowgli-2.so.0": { + "package": "libmowgli", + "version": "0.0.0" + }, + "libfpm_pb.so.0": { + "package": "quagga", + "version": "0.0.0" + }, + "libospf.so.0": { + "package": "quagga", + "version": "0.0.0" + }, + "libospfapiclient.so.0": { + "package": "quagga", + "version": "0.0.0" + }, + "libquagga_pb.so.0": { + "package": "quagga", + "version": "0.0.0" + }, + "libzebra.so.1": { + "package": "quagga", + "version": "1.0.0" + }, + "libdevmapper-event-lvm2.so.2.02": { + "package": "lvm2-libs", + "version": "2.02" + }, + "liblvm2app.so.2.2": { + "package": "lvm2-libs", + "version": "2.2" + }, + "liblvm2cmd.so.2.02": { + "package": "lvm2-libs", + "version": "2.02" + }, + "libgtest.so": { + "package": "gtest", + "version": "0" + }, + "libgtest_main.so": { + "package": "gtest", + "version": "0" + }, + "libbonobo-2.so.0": { + "package": "libbonobo", + "version": "0.0.0" + }, + "libbonobo-activation.so.4": { + "package": "libbonobo", + "version": "4.0.0" + }, + "libXcomposite.so.1": { + "package": "libxcomposite", + "version": "1.0.0" + }, + "libprotobuf-c.so.1": { + "package": "protobuf-c", + "version": "1.0.0" + }, + "libcurl.so.4": { + "package": "libcurl", + "version": "4.5.0" + }, + "libntfs-3g.so.88": { + "package": "ntfs-3g-libs", + "version": "88.0.0" + }, + "liblua-5.3.so.0": { + "package": "lua5.3-libs", + "version": "0.0.0" + }, + "libneon.so.27": { + "package": "neon", + "version": "27.3.2" + }, + "libgpgmepp.so.6": { + "package": "gpgmepp", + "version": "6.8.0" + }, + "libruby.so.2.5": { + "package": "ruby-libs", + "version": "2.5.5" + }, + "libatomic_ops.so.1": { + "package": "libatomic_ops", + "version": "1.1.1" + }, + "libatomic_ops_gpl.so.1": { + "package": "libatomic_ops", + "version": "1.1.2" + }, + "libncftp.so.3": { + "package": "ncftp", + "version": "3" + }, + "libusb-0.1.so.4": { + "package": "libusb-compat", + "version": "4.4.4" + }, + "libnetfilter_log.so.1": { + "package": "libnetfilter_log", + "version": "1.1.0" + }, + "libnetfilter_log_libipulog.so.1": { + "package": "libnetfilter_log", + "version": "1.0.0" + }, + "rlm_pam.so": { + "package": "freeradius-pam", + "version": "0" + }, + "libCEGUIBase-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUIExpatParser-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUIFalagardWRBase-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUILuaScriptModule-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUIOpenGLRenderer-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUITGAImageCodec-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUITinyXMLParser-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "libCEGUItoluapp-0.6.2.so": { + "package": "cegui06", + "version": "0" + }, + "postfix-pcre.so": { + "package": "postfix-pcre", + "version": "0" + }, + "liblz4.so.1": { + "package": "lz4-libs", + "version": "1.8.3" + }, + "libasr.so.0": { + "package": "libasr", + "version": "0.0.2" + }, + "libsexy.so.2": { + "package": "libsexy", + "version": "2.0.4" + }, + "libboost_prg_exec_monitor-mt.so.1.67.0": { + "package": "boost-prg_exec_monitor", + "version": "1.67.0" + }, + "libboost_prg_exec_monitor.so.1.67.0": { + "package": "boost-prg_exec_monitor", + "version": "1.67.0" + }, + "libharfbuzz-gobject.so.0": { + "package": "harfbuzz", + "version": "0.20200.0" + }, + "libharfbuzz-subset.so.0": { + "package": "harfbuzz", + "version": "0.20200.0" + }, + "libharfbuzz.so.0": { + "package": "harfbuzz", + "version": "0.20200.0" + }, + "postfix-pgsql.so": { + "package": "postfix-pgsql", + "version": "0" + }, + "libclutter-1.0.so.0": { + "package": "clutter", + "version": "0.2600.2" + }, + "libfreetype.so.6": { + "package": "freetype", + "version": "6.16.1" + }, + "libcanberra-gtk.so.0": { + "package": "libcanberra-gtk2", + "version": "0.1.9" + }, + "librevenge-0.0.so.0": { + "package": "librevenge", + "version": "0.0.4" + }, + "librevenge-generators-0.0.so.0": { + "package": "librevenge", + "version": "0.0.4" + }, + "librevenge-stream-0.0.so.0": { + "package": "librevenge", + "version": "0.0.4" + }, + "libkamailio_ims.so.0": { + "package": "kamailio", + "version": "0.1" + }, + "libprint.so.1": { + "package": "kamailio", + "version": "1.2" + }, + "libsrdb1.so.1": { + "package": "kamailio", + "version": "1.0" + }, + "libsrdb2.so.1": { + "package": "kamailio", + "version": "1.0" + }, + "libsrutils.so.1": { + "package": "kamailio", + "version": "1.0" + }, + "libtrie.so.1": { + "package": "kamailio", + "version": "1.0" + }, + "libXtst.so.6": { + "package": "libxtst", + "version": "6.1.0" + }, + "libmagic.so.1": { + "package": "libmagic", + "version": "1.0.0" + }, + "libdriver_mysql.so": { + "package": "dovecot-mysql", + "version": "0" + }, + "libdv.so.4": { + "package": "libdv", + "version": "4.0.3" + }, + "libnet.so.1": { + "package": "libnet", + "version": "1.7.0" + }, + "libpyldb-util.cpython-36m-x86-64-linux-gnu.so.1": { + "package": "py3-ldb", + "version": "1.3.8" + }, + "libpoppler-glib.so.8": { + "package": "poppler-glib", + "version": "8.9.0" + }, + "libnfs.so.12": { + "package": "libnfs", + "version": "12.0.0" + }, + "libgnokii.so.7": { + "package": "gnokii-libs", + "version": "7.0.0" + }, + "rlm_eap.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_fast.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_gtc.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_leap.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_md5.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_mschapv2.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_peap.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_pwd.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_sim.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_tls.so": { + "package": "freeradius-eap", + "version": "0" + }, + "rlm_eap_ttls.so": { + "package": "freeradius-eap", + "version": "0" + }, + "libpopt.so.0": { + "package": "popt", + "version": "0.0.0" + }, + "libfontconfig.so.1": { + "package": "fontconfig", + "version": "1.12.0" + }, + "libsensors.so.4": { + "package": "lm_sensors", + "version": "4.4.0" + }, + "libXmu.so.6": { + "package": "libxmu", + "version": "6.2.0" + }, + "libXmuu.so.1": { + "package": "libxmu", + "version": "1.0.0" + }, + "libXxf86misc.so.1": { + "package": "libxxf86misc", + "version": "1.1.0" + }, + "libicudata.so.62": { + "package": "icu-libs", + "version": "62.1" + }, + "libicui18n.so.62": { + "package": "icu-libs", + "version": "62.1" + }, + "libicuio.so.62": { + "package": "icu-libs", + "version": "62.1" + }, + "libicutest.so.62": { + "package": "icu-libs", + "version": "62.1" + }, + "libicutu.so.62": { + "package": "icu-libs", + "version": "62.1" + }, + "libicuuc.so.62": { + "package": "icu-libs", + "version": "62.1" + }, + "libechonest.so.2.3": { + "package": "libechonest", + "version": "2.3.1" + }, + "libavfs.so.0": { + "package": "avfs", + "version": "0.0.2" + }, + "libdconf.so.1": { + "package": "dconf", + "version": "1.0.0" + }, + "libcc1.so.0": { + "package": "gcc", + "version": "0.0.0" + }, + "libitm.so.1": { + "package": "gcc", + "version": "1.0.0" + }, + "libdlz-bind9-for-torture-samba4.so": { + "package": "samba-test", + "version": "0" + }, + "libtorture-samba4.so": { + "package": "samba-test", + "version": "0" + }, + "libmtdev.so.1": { + "package": "mtdev", + "version": "1.0.0" + }, + "libbfd-2.31.1.so": { + "package": "binutils", + "version": "0" + }, + "libopcodes-2.31.1.so": { + "package": "binutils", + "version": "0" + }, + "libcdda_interface.so.0": { + "package": "cdparanoia-libs", + "version": "0.10.2" + }, + "libcdda_paranoia.so.0": { + "package": "cdparanoia-libs", + "version": "0.10.2" + }, + "libaio.so.1": { + "package": "libaio", + "version": "1.0.1" + }, + "libmaa.so.3": { + "package": "libmaa", + "version": "3.0.0" + }, + "libqjson.so.0": { + "package": "qjson", + "version": "0.9.0" + }, + "libXss.so.1": { + "package": "libxscrnsaver", + "version": "1.0.0" + }, + "libgvplugin_gdk.so.6": { + "package": "graphviz-gtk", + "version": "6.0.0" + }, + "libgvplugin_gtk.so.6": { + "package": "graphviz-gtk", + "version": "6.0.0" + }, + "libgvplugin_rsvg.so.6": { + "package": "graphviz-gtk", + "version": "6.0.0" + }, + "libpango-1.0.so.0": { + "package": "pango", + "version": "0.4200.4" + }, + "libpangocairo-1.0.so.0": { + "package": "pango", + "version": "0.4200.4" + }, + "libpangoft2-1.0.so.0": { + "package": "pango", + "version": "0.4200.4" + }, + "libpangoxft-1.0.so.0": { + "package": "pango", + "version": "0.4200.4" + }, + "libcairomm-1.0.so.1": { + "package": "cairomm", + "version": "1.4.0" + }, + "libexpect5.45.4.so": { + "package": "expect", + "version": "0" + }, + "libpixman-1.so.0": { + "package": "pixman", + "version": "0.34.0" + }, + "libfarstream-0.1.so.0": { + "package": "farstream0.1", + "version": "0.0.1" + }, + "libcairo-gobject.so.2": { + "package": "cairo-gobject", + "version": "2.11600.0" + }, + "libbaccats-mysql-9.4.1.so": { + "package": "bacula-mysql", + "version": "0" + }, + "liblzma.so.5": { + "package": "xz-libs", + "version": "5.2.4" + }, + "libpytalloc-util.cpython-36m-x86-64-linux-gnu.so.2": { + "package": "py3-talloc", + "version": "2.1.14" + }, + "libICE.so.6": { + "package": "libice", + "version": "6.3.0" + }, + "libiec61883.so.0": { + "package": "libiec61883", + "version": "0.1.1" + }, + "libproxychains4.so": { + "package": "proxychains-ng", + "version": "0" + }, + "libcrystalhd.so.3": { + "package": "libcrystalhd", + "version": "3.6" + }, + "libaugeas.so.0": { + "package": "augeas-libs", + "version": "0.24.1" + }, + "libfa.so.1": { + "package": "augeas-libs", + "version": "1.5.2" + }, + "libacl.so.1": { + "package": "libacl", + "version": "1.1.0" + }, + "libnetsnmp.so.35": { + "package": "net-snmp-libs", + "version": "35.0.0" + }, + "libtk8.6.so": { + "package": "tk", + "version": "0" + }, + "libgraphite2.so.3": { + "package": "graphite2", + "version": "3.2.1" + }, + "openvpn-auth-ldap.so": { + "package": "openvpn-auth-ldap", + "version": "0" + }, + "libsmartcols.so.1": { + "package": "libsmartcols", + "version": "1.1.0" + }, + "libgmp.so.10": { + "package": "gmp", + "version": "10.3.2" + }, + "libebml.so.4": { + "package": "libebml", + "version": "4.0.0" + }, + "libdbus-1.so.3": { + "package": "dbus-libs", + "version": "3.14.14" + }, + "libasyncns.so.0": { + "package": "libasyncns", + "version": "0.3.1" + }, + "libmpfr.so.4": { + "package": "mpfr3", + "version": "4.1.5" + }, + "libpng16.so.16": { + "package": "libpng", + "version": "16.37.0" + }, + "libilbc.so.0": { + "package": "ilbc", + "version": "0.0.2" + }, + "libivykis.so.0": { + "package": "ivykis", + "version": "0.5.5" + }, + "libnetfilter_conntrack.so.3": { + "package": "libnetfilter_conntrack", + "version": "3.6.0" + }, + "libpangox-1.0.so.0": { + "package": "pangox-compat", + "version": "0.0.0" + }, + "libmount.so.1": { + "package": "libmount", + "version": "1.1.0" + }, + "libcanberra-gtk3.so.0": { + "package": "libcanberra-gtk3", + "version": "0.1.9" + }, + "libavahi-ui-gtk3.so.0": { + "package": "avahi-ui-gtk3", + "version": "0.1.4" + }, + "libva-drm.so.2": { + "package": "libva", + "version": "2.200.0" + }, + "libva-x11.so.2": { + "package": "libva", + "version": "2.200.0" + }, + "libva.so.2": { + "package": "libva", + "version": "2.200.0" + }, + "libboost_contract-mt.so.1.67.0": { + "package": "boost-contract", + "version": "1.67.0" + }, + "libboost_contract.so.1.67.0": { + "package": "boost-contract", + "version": "1.67.0" + }, + "libfam.so.0": { + "package": "gamin", + "version": "0.0.0" + }, + "libgamin-1.so.0": { + "package": "gamin", + "version": "0.1.10" + }, + "libxatracker.so.2": { + "package": "mesa-xatracker", + "version": "2.3.0" + }, + "libnspr4.so": { + "package": "nspr", + "version": "0" + }, + "libplc4.so": { + "package": "nspr", + "version": "0" + }, + "libplds4.so": { + "package": "nspr", + "version": "0" + }, + "libform.so.5": { + "package": "ncurses5-libs", + "version": "5.9" + }, + "libmenu.so.5": { + "package": "ncurses5-libs", + "version": "5.9" + }, + "libncurses.so.5": { + "package": "ncurses5-libs", + "version": "5.9" + }, + "libpanel.so.5": { + "package": "ncurses5-libs", + "version": "5.9" + }, + "libboost_locale-mt.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_log-mt.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_log.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_log_setup-mt.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_log_setup.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_stacktrace_basic-mt.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_stacktrace_basic.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_stacktrace_noop-mt.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_stacktrace_noop.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_timer-mt.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_timer.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_type_erasure-mt.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_type_erasure.so.1.67.0": { + "package": "boost", + "version": "1.67.0" + }, + "libboost_container-mt.so.1.67.0": { + "package": "boost-container", + "version": "1.67.0" + }, + "libboost_container.so.1.67.0": { + "package": "boost-container", + "version": "1.67.0" + }, + "libldb-cmdline.so": { + "package": "ldb-tools", + "version": "0" + }, + "libgnomevfs-2.so.0": { + "package": "gnome-vfs", + "version": "0.2400.4" + }, + "libxmlrpc_client.so.3": { + "package": "xmlrpc-c-client", + "version": "3.39" + }, + "libao.so.4": { + "package": "libao", + "version": "4.1.0" + }, + "libs6.so.2.7": { + "package": "s6", + "version": "2.7.2.0" + }, + "lib++dfb-1.7.so.7": { + "package": "directfb", + "version": "7.0.0" + }, + "libdirect-1.7.so.7": { + "package": "directfb", + "version": "7.0.0" + }, + "libdirectfb-1.7.so.7": { + "package": "directfb", + "version": "7.0.0" + }, + "libfusion-1.7.so.7": { + "package": "directfb", + "version": "7.0.0" + }, + "libstartup-notification-1.so.0": { + "package": "startup-notification", + "version": "0.0.0" + }, + "libuv.so.1": { + "package": "libuv", + "version": "1.0.0" + }, + "libcdio.so.16": { + "package": "libcdio", + "version": "16.0.0" + }, + "libiso9660.so.10": { + "package": "libcdio", + "version": "10.0.0" + }, + "libudf.so.0": { + "package": "libcdio", + "version": "0.0.0" + }, + "libpgm-5.2.so.0": { + "package": "openpgm", + "version": "0.0.122" + }, + "librtmp.so.1": { + "package": "librtmp", + "version": "1" + }, + "libbtrfs.so.0": { + "package": "btrfs-progs-libs", + "version": "0.1" + }, + "libbtrfsutil.so.1": { + "package": "btrfs-progs-libs", + "version": "1.1.0" + }, + "libid3tag.so.0": { + "package": "libid3tag", + "version": "0.3.0" + }, + "libbsd.so.0": { + "package": "libbsd", + "version": "0.8.6" + }, + "libhashkit.so.2": { + "package": "libmemcached-libs", + "version": "2.0.0" + }, + "libmemcached.so.11": { + "package": "libmemcached-libs", + "version": "11.0.0" + }, + "libmemcachedprotocol.so.0": { + "package": "libmemcached-libs", + "version": "0.0.0" + }, + "libmemcachedutil.so.2": { + "package": "libmemcached-libs", + "version": "2.0.0" + }, + "libavahi-core.so.7": { + "package": "avahi", + "version": "7.0.2" + }, + "libgd.so.3": { + "package": "libgd", + "version": "3.0.5" + }, + "libunwind-coredump.so.0": { + "package": "libunwind", + "version": "0.0.0" + }, + "libunwind-ptrace.so.0": { + "package": "libunwind", + "version": "0.0.0" + }, + "libunwind-setjmp.so.0": { + "package": "libunwind", + "version": "0.0.0" + }, + "libunwind-x86_64.so.8": { + "package": "libunwind", + "version": "8.0.1" + }, + "libunwind.so.8": { + "package": "libunwind", + "version": "8.0.1" + }, + "libx264.so.152": { + "package": "x264-libs", + "version": "152" + }, + "librelp.so.0": { + "package": "librelp", + "version": "0.4.0" + }, + "libdc1394.so.22": { + "package": "libdc1394", + "version": "22.2.1" + }, + "libart_lgpl_2.so.2": { + "package": "libart-lgpl", + "version": "2.3.21" + }, + "libxcb-image.so.0": { + "package": "xcb-util-image", + "version": "0.0.0" + }, + "libgudev-1.0.so.0": { + "package": "libgudev", + "version": "0.2.0" + }, + "libwx_gtk2u_adv-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_aui-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_core-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_html-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_qa-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_richtext-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libwx_gtk2u_xrc-2.8.so.0": { + "package": "wxgtk2.8", + "version": "0.8.0" + }, + "libfreeradius-client.so.2": { + "package": "freeradius-client", + "version": "2.0.0" + }, + "libgcab-1.0.so.0": { + "package": "libgcab", + "version": "0.0.0" + }, + "libpkgconf.so.3": { + "package": "pkgconf", + "version": "3.0.0" + }, + "libcrypto.so.1.1": { + "package": "libcrypto1.1", + "version": "1.1" + }, + "libsodium.so.23": { + "package": "libsodium", + "version": "23.1.0" + }, + "lib21_fts_lucene_plugin.so": { + "package": "dovecot-fts-lucene", + "version": "0" + }, + "libmaxminddb.so.0": { + "package": "libmaxminddb", + "version": "0.0.7" + }, + "libmosquitto.so.1": { + "package": "mosquitto-libs", + "version": "1" + }, + "libgdbm.so.4": { + "package": "gdbm", + "version": "4.0.0" + }, + "libgdbm_compat.so.4": { + "package": "gdbm", + "version": "4.0.0" + }, + "libprocps.so.7": { + "package": "libproc", + "version": "7.1.0" + }, + "libldns.so.2": { + "package": "ldns", + "version": "2.0.0" + }, + "libclamav.so.7": { + "package": "clamav-libs", + "version": "7.1.2" + }, + "libclammspack.so.0": { + "package": "clamav-libs", + "version": "0.1.0" + }, + "libclamunrar.so.7": { + "package": "clamav-libs", + "version": "7.1.2" + }, + "libclamunrar_iface.so.7": { + "package": "clamav-libs", + "version": "7.1.2" + }, + "rlm_sql_mysql.so": { + "package": "freeradius-mysql", + "version": "0" + }, + "libgdkmm-2.4.so.1": { + "package": "gtkmm", + "version": "1.1.0" + }, + "libgtkmm-2.4.so.1": { + "package": "gtkmm", + "version": "1.1.0" + }, + "libperditiondb_gdbm.so.0": { + "package": "perdition", + "version": "0.0.0" + }, + "libtty.so.0": { + "package": "termrec", + "version": "0.0.0" + }, + "libcmocka.so.0": { + "package": "cmocka", + "version": "0.4.1" + }, + "libguess.so.1": { + "package": "libguess", + "version": "1.0.0" + }, + "libdvbpsi.so.10": { + "package": "libdvbpsi", + "version": "10.0.0" + }, + "libjson-c.so.4": { + "package": "json-c", + "version": "4.0.0" + }, + "libtinyxml2.so.7": { + "package": "tinyxml2", + "version": "7.0.1" + }, + "libpcap.so.1": { + "package": "libpcap", + "version": "1.9.0" + }, + "libnetflowPlugin-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "libntop-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "libntopreport-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "librrdPlugin-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "libsflowPlugin-5.0.1.so": { + "package": "ntop", + "version": "0" + }, + "libcollectdclient.so.1": { + "package": "collectd-libs", + "version": "1.1.0" + }, + "libpri.so.1.4": { + "package": "libpri", + "version": "1.4" + }, + "libxvidcore.so.4": { + "package": "xvidcore", + "version": "4.3" + }, + "libogrove.so.0": { + "package": "openjade-libs", + "version": "0.0.1" + }, + "libospgrove.so.0": { + "package": "openjade-libs", + "version": "0.0.1" + }, + "libostyle.so.0": { + "package": "openjade-libs", + "version": "0.0.1" + }, + "libcppunit-1.14.so.0": { + "package": "cppunit", + "version": "0.0.0" + }, + "libgnutls.so.30": { + "package": "gnutls", + "version": "30.23.2" + }, + "libnfdump-1.6.17.so": { + "package": "nfdump", + "version": "0" + }, + "libnl.so.1": { + "package": "libnl", + "version": "1.1.4" + }, + "libgs.so.9": { + "package": "ghostscript", + "version": "9.26" + }, + "libijs-0.35.so": { + "package": "ghostscript", + "version": "0" + }, + "libassuan.so.0": { + "package": "libassuan", + "version": "0.8.1" + }, + "libgck-1.so.0": { + "package": "gcr", + "version": "0.0.0" + }, + "libgcr-base-3.so.1": { + "package": "gcr", + "version": "1.0.0" + }, + "libgcr-ui-3.so.1": { + "package": "gcr", + "version": "1.0.0" + }, + "libasteriskssl.so.1": { + "package": "asterisk", + "version": "1" + }, + "libisl.so.15": { + "package": "isl", + "version": "15.3.0" + }, + "libOpenIPMI.so.0": { + "package": "openipmi-libs", + "version": "0.0.5" + }, + "libOpenIPMIcmdlang.so.0": { + "package": "openipmi-libs", + "version": "0.0.5" + }, + "libOpenIPMIglib.so.0": { + "package": "openipmi-libs", + "version": "0.0.1" + }, + "libOpenIPMIposix.so.0": { + "package": "openipmi-libs", + "version": "0.0.1" + }, + "libOpenIPMIpthread.so.0": { + "package": "openipmi-libs", + "version": "0.0.1" + }, + "libOpenIPMIui.so.1": { + "package": "openipmi-libs", + "version": "1.0.1" + }, + "libOpenIPMIutils.so.0": { + "package": "openipmi-libs", + "version": "0.0.1" + }, + "libatf-c++.so.2": { + "package": "atf", + "version": "2.0.0" + }, + "libatf-c.so.1": { + "package": "atf", + "version": "1.0.0" + }, + "libbaccats-sqlite3-9.4.1.so": { + "package": "bacula-sqlite", + "version": "0" + }, + "libzmq.so.5": { + "package": "libzmq", + "version": "5.2.1" + }, + "libiperf.so.0": { + "package": "iperf3", + "version": "0.0.0" + }, + "libnfsidmap.so.1": { + "package": "libnfsidmap", + "version": "1.0.0" + }, + "libmpg123.so.0": { + "package": "mpg123", + "version": "0.44.8" + }, + "libout123.so.0": { + "package": "mpg123", + "version": "0.2.2" + }, + "libipset.so.13": { + "package": "ipset", + "version": "13.1.0" + }, + "libFLAC++.so.6": { + "package": "flac", + "version": "6.3.0" + }, + "libFLAC.so.8": { + "package": "flac", + "version": "8.3.0" + }, + "liblcms.so.1": { + "package": "liblcms", + "version": "1.0.19" + }, + "libelf.so.0": { + "package": "libelf", + "version": "0" + }, + "libXfont2.so.2": { + "package": "libxfont2", + "version": "2.0.0" + }, + "libboost_math_c99-mt.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_c99.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_c99f-mt.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_c99f.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_c99l-mt.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_c99l.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_tr1-mt.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_tr1.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_tr1f-mt.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_tr1f.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_tr1l-mt.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libboost_math_tr1l.so.1.67.0": { + "package": "boost-math", + "version": "1.67.0" + }, + "libaprutil-1.so.0": { + "package": "apr-util", + "version": "0.6.1" + }, + "libkeyutils.so.1": { + "package": "keyutils-libs", + "version": "1.8" + }, + "liboping.so.0": { + "package": "liboping", + "version": "0.3.0" + }, + "libucontext.so.0": { + "package": "libucontext", + "version": "0" + }, + "libgcc_s.so.1": { + "package": "libgcc", + "version": "1" + }, + "libxattr-tdb-samba4.so": { + "package": "samba-server", + "version": "0" + }, + "libboost_atomic-mt.so.1.67.0": { + "package": "boost-atomic", + "version": "1.67.0" + }, + "libpcrecpp.so.0": { + "package": "libpcrecpp", + "version": "0.0.1" + }, + "libverto.so.1": { + "package": "libverto", + "version": "1.0.0" + }, + "libXrender.so.1": { + "package": "libxrender", + "version": "1.3.0" + }, + "libIDL-2.so.0": { + "package": "libidl", + "version": "0.0.0" + }, + "libsnappy.so.1": { + "package": "snappy", + "version": "1.1.7" + }, + "libxmlrpc_abyss.so.3": { + "package": "xmlrpc-c-abyss", + "version": "3.39" + }, + "libxmlrpc_server_abyss.so.3": { + "package": "xmlrpc-c-abyss", + "version": "3.39" + }, + "libboost_thread-mt.so.1.67.0": { + "package": "boost-thread", + "version": "1.67.0" + }, + "libtevent.so.0": { + "package": "tevent", + "version": "0.9.37" + }, + "libreadline.so.7": { + "package": "readline", + "version": "7.0" + }, + "libopus.so.0": { + "package": "opus", + "version": "0.7.0" + }, + "libtls.so.17": { + "package": "libressl2.7-libtls", + "version": "17.0.1" + }, + "libunique-1.0.so.0": { + "package": "libunique", + "version": "0.100.6" + }, + "libstdc++.so.6": { + "package": "libstdc++", + "version": "6.0.25" + }, + "libSDL_image-1.2.so.0": { + "package": "sdl_image", + "version": "0.8.4" + }, + "liblastfm.so.1": { + "package": "liblastfm", + "version": "1.0.9" + }, + "liblastfm_fingerprint.so.1": { + "package": "liblastfm", + "version": "1.0.9" + }, + "libburn.so.4": { + "package": "libburn", + "version": "4.103.0" + }, + "libfprint.so.0": { + "package": "libfprint", + "version": "0.0.0" + }, + "libwebsockets.so.14": { + "package": "libwebsockets", + "version": "14" + }, + "libtalloc.so.2": { + "package": "talloc", + "version": "2.1.14" + }, + "libmcpp.so.0": { + "package": "mcpp-libs", + "version": "0.3.0" + }, + "libtumbler-1.so.0": { + "package": "tumbler", + "version": "0.0.0" + }, + "libhogweed.so.4": { + "package": "nettle", + "version": "4.5" + }, + "libnettle.so.6": { + "package": "nettle", + "version": "6.5" + }, + "libexecinfo.so.1": { + "package": "libexecinfo", + "version": "1" + }, + "libgssapi_krb5.so.2": { + "package": "krb5-libs", + "version": "2.2" + }, + "libgssrpc.so.4": { + "package": "krb5-libs", + "version": "4.2" + }, + "libk5crypto.so.3": { + "package": "krb5-libs", + "version": "3.1" + }, + "libkadm5clnt_mit.so.11": { + "package": "krb5-libs", + "version": "11.0" + }, + "libkadm5srv_mit.so.11": { + "package": "krb5-libs", + "version": "11.0" + }, + "libkdb5.so.8": { + "package": "krb5-libs", + "version": "8.0" + }, + "libkrad.so.0": { + "package": "krb5-libs", + "version": "0.0" + }, + "libkrb5.so.3": { + "package": "krb5-libs", + "version": "3.3" + }, + "libkrb5support.so.0": { + "package": "krb5-libs", + "version": "0.1" + }, + "libMESSAGING-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libdcerpc-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libidmap-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libnon-posix-acls-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libnss-info-samba4.so": { + "package": "samba-server-libs", + "version": "0" + }, + "libvorbis.so.0": { + "package": "libvorbis", + "version": "0.4.8" + }, + "libvorbisenc.so.2": { + "package": "libvorbis", + "version": "2.0.11" + }, + "libvorbisfile.so.3": { + "package": "libvorbis", + "version": "3.3.7" + }, + "libboost_regex-mt.so.1.67.0": { + "package": "boost-regex", + "version": "1.67.0" + }, + "libboost_regex.so.1.67.0": { + "package": "boost-regex", + "version": "1.67.0" + }, + "libnotify.so.4": { + "package": "libnotify", + "version": "4.0.0" + }, + "libusb-1.0.so.0": { + "package": "libusb", + "version": "0.1.0" + }, + "libpyglib-2.0-python.so.0": { + "package": "py-gobject", + "version": "0.0.0" + }, + "libslang.so.2": { + "package": "slang", + "version": "2.3.2" + }, + "libCHARSET3-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libaddns-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libasn1util-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libauthkrb5-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcli-cldap-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcli-ldap-common-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcli-nbt-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcli-smb-common-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcliauth-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcmocka-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libcommon-auth-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libdbwrap-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libflag-mapping-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libgenrand-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libgensec-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libgse-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libinterfaces-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libiov-buf-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libkrb5samba-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libldbsamba-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libmessages-dgm-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libmessages-util-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libmsghdr-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libndr-krb5pac.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libndr-nbt.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libndr-samba-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libndr-standard.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libndr.so.0": { + "package": "samba-libs", + "version": "0.1.0" + }, + "libpopt-samba3-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-cluster-support-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-credentials.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libsamba-debug-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-errors.so.1": { + "package": "samba-libs", + "version": "1" + }, + "libsamba-hostconfig.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libsamba-modules-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-security-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-sockets-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamba-util.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libsamba3-util-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamdb-common-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsamdb.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libsecrets3-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libserver-id-db-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libserver-role-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsmb-transport-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsmbconf.so.0": { + "package": "samba-libs", + "version": "0" + }, + "libsmbd-shim-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsocket-blocking-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libsys-rw-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libtalloc-report-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libtdb-wrap-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libtevent-util.so.0": { + "package": "samba-libs", + "version": "0.0.1" + }, + "libtime-basic-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libutil-cmdline-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libutil-reg-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libutil-setid-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libutil-tdb-samba4.so": { + "package": "samba-libs", + "version": "0" + }, + "libxkbui.so.1": { + "package": "libxkbui", + "version": "1.0.0" + }, + "libfontenc.so.1": { + "package": "libfontenc", + "version": "1.0.0" + }, + "libnvpair.so.1": { + "package": "zfs-libs", + "version": "1.0.1" + }, + "libuutil.so.1": { + "package": "zfs-libs", + "version": "1.0.1" + }, + "libzfs.so.2": { + "package": "zfs-libs", + "version": "2.0.0" + }, + "libzfs_core.so.1": { + "package": "zfs-libs", + "version": "1.0.0" + }, + "libzpool.so.2": { + "package": "zfs-libs", + "version": "2.0.0" + }, + "libs6net.so.2.3": { + "package": "s6-networking", + "version": "2.3.0.3" + }, + "libsbearssl.so.2.3": { + "package": "s6-networking", + "version": "2.3.0.3" + }, + "libdvdcss.so.2": { + "package": "libdvdcss", + "version": "2.2.0" + }, + "libgnome-2.so.0": { + "package": "libgnome", + "version": "0.3200.1" + }, + "libgstapp-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstaudio-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstcdda-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstfft-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstinterfaces-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstnetbuffer-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstpbutils-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstriff-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstrtp-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstrtsp-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstsdp-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgsttag-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libgstvideo-0.10.so.0": { + "package": "gst-plugins-base0.10", + "version": "0.25.0" + }, + "libXinerama.so.1": { + "package": "libxinerama", + "version": "1.0.0" + }, + "libglamor.so.0": { + "package": "glamor-egl", + "version": "0.0.0" + }, + "libGLESv1_CM.so.1": { + "package": "mesa-gles", + "version": "1.1.0" + }, + "libGLESv2.so.2": { + "package": "mesa-gles", + "version": "2.0.0" + }, + "libfcgi++.so.0": { + "package": "fcgi++", + "version": "0.0.0" + }, + "libgiblib.so.1": { + "package": "giblib", + "version": "1.0.6" + }, + "libnftables.so.0": { + "package": "nftables", + "version": "0.0.0" + }, + "libusbredirhost.so.1": { + "package": "usbredir", + "version": "1.0.0" + }, + "libusbredirparser.so.1": { + "package": "usbredir", + "version": "1.0.0" + } + }, + "package": { + "linux-firmware-any": { + "package": "linux-firmware-qcom", + "version": 0 + }, + "py-avahi": { + "package": "py2-avahi", + "version": "0.7-r1" + }, + "rsyslog-omhttp": { + "package": "rsyslog-http", + "version": "8.40.0-r3" + }, + "rsyslog-fmhttp": { + "package": "rsyslog-http", + "version": "8.40.0-r3" + }, + "drbd": { + "package": "drbd-utils", + "version": "9.7.1-r0" + }, + "bigreqsproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "compositeproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "damageproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "dri2proto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "dri3proto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "fixesproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "fontsproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "glproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "inputproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "kbproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "presentproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "printproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "randrproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "recordproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "renderproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "resourceproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "scrnsaverproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "videoproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "xcmiscproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "xextproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "xf86bigfontproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "xf86dgaproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "xf86driproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "xf86miscproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "xf86vidmodeproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "xineramaproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "xproto": { + "package": "xorgproto", + "version": "2018.4-r0" + }, + "py3-pip": { + "package": "python3", + "version": 0 + }, + "rsyslog-lmgssutil": { + "package": "rsyslog-gssapi", + "version": "8.40.0-r3" + }, + "rsyslog-imgssapi": { + "package": "rsyslog-gssapi", + "version": "8.40.0-r3" + }, + "rsyslog-omgssapi": { + "package": "rsyslog-gssapi", + "version": "8.40.0-r3" + }, + "perl-test-tester": { + "package": "perl-test-simple", + "version": 0 + }, + "lua-graphviz": { + "package": "lua5.3-graphviz", + "version": "2.40.1-r1" + }, + "lua-penlight-shared": { + "package": "lua-penlight", + "version": "1.5.4-r2" + }, + "lua5.1-penlight": { + "package": "lua-penlight", + "version": "1.5.4-r2" + }, + "lua5.2-penlight": { + "package": "lua-penlight", + "version": "1.5.4-r2" + }, + "lua5.3-penlight": { + "package": "lua-penlight", + "version": "1.5.4-r2" + }, + "freeradius3-krb5": { + "package": "freeradius-krb5", + "version": "3.0.17-r5" + }, + "lua5.1-json4": { + "package": "lua-json4", + "version": "1.0.0-r3" + }, + "lua5.2-json4": { + "package": "lua-json4", + "version": "1.0.0-r3" + }, + "lua5.3-json4": { + "package": "lua-json4", + "version": "1.0.0-r3" + }, + "freeradius3-postgresql": { + "package": "freeradius-postgresql", + "version": "3.0.17-r5" + }, + "lua-lustache-common": { + "package": "lua-lustache", + "version": "1.3.1-r2" + }, + "lua5.1-lustache": { + "package": "lua-lustache", + "version": "1.3.1-r2" + }, + "lua5.2-lustache": { + "package": "lua-lustache", + "version": "1.3.1-r2" + }, + "lua5.3-lustache": { + "package": "lua-lustache", + "version": "1.3.1-r2" + }, + "python-tests": { + "package": "python2-tests", + "version": "2.7.16-r1" + }, + "lua-doc": { + "package": "lua5.1-doc", + "version": 0 + }, + "collectd-libvirt": { + "package": "collectd-virt", + "version": 0 + }, + "python-dev": { + "package": "python2-dev", + "version": "2.7.16-r1" + }, + "freeradius3-python": { + "package": "freeradius-python", + "version": "3.0.17-r5" + }, + "py-talloc": { + "package": "py2-talloc", + "version": "2.1.14-r0" + }, + "bkeymaps": { + "package": "kbd-bkeymaps", + "version": 0 + }, + "llvm-static": { + "package": "llvm5-static", + "version": "5.0.2-r0" + }, + "py3-uritemplate.py": { + "package": "py3-uritemplate", + "version": 0 + }, + "/bin/sh": { + "package": "busybox", + "version": 0 + }, + "rsyslog-omrabbitmq": { + "package": "rsyslog-rabbitmq", + "version": "8.40.0-r3" + }, + "rsyslog-omelasticsearch": { + "package": "rsyslog-elasticsearch", + "version": "8.40.0-r3" + }, + "rsyslog-omlibdbi": { + "package": "rsyslog-libdbi", + "version": "8.40.0-r3" + }, + "rsyslog-omtesting": { + "package": "rsyslog-testing", + "version": "8.40.0-r3" + }, + "pyflakes": { + "package": "py2-pyflakes", + "version": "1.6.0-r3" + }, + "perl-time-date": { + "package": "perl-timedate", + "version": "2.30" + }, + "freeradius3-unixodbc": { + "package": "freeradius-unixodbc", + "version": "3.0.17-r5" + }, + "py-django-tables2-r1.21.2": { + "package": "py2-django-tables2", + "version": 0 + }, + "pllua": { + "package": "postgresql-pllua", + "version": "2.0-r0" + }, + "rsyslog-omuxsock": { + "package": "rsyslog-uxsock", + "version": "8.40.0-r3" + }, + "ncurses-widec-libs": { + "package": "ncurses-libs", + "version": "6.1_p20190105-r0" + }, + "perl-mail-tools": { + "package": "perl-mailtools", + "version": "2.19" + }, + "libgit2-libs": { + "package": "libgit2", + "version": 0 + }, + "udev": { + "package": "eudev", + "version": "176" + }, + "rsyslog-omsnmp": { + "package": "rsyslog-snmp", + "version": "8.40.0-r3" + }, + "py-attrs-tools": { + "package": "py2-attrs", + "version": 0 + }, + "py-pip": { + "package": "py2-pip", + "version": "18.1-r0" + }, + "nodejs-lts": { + "package": "nodejs", + "version": "10.14.2" + }, + "freeradius3-sqlite": { + "package": "freeradius-sqlite", + "version": "3.0.17-r5" + }, + "wayland-protocols-dev": { + "package": "wayland-protocols", + "version": 0 + }, + "lua": { + "package": "lua5.3", + "version": 0 + }, + "gnupg-doc": { + "package": "gnupg1-doc", + "version": "1.4.23-r0" + }, + "freeradius3": { + "package": "freeradius", + "version": "3.0.17-r5" + }, + "freeradius3-sql": { + "package": "freeradius-sql", + "version": "3.0.17-r5" + }, + "llvm-dev": { + "package": "llvm5-dev", + "version": "5.0.2-r0" + }, + "nodejs-lts-dev": { + "package": "nodejs-dev", + "version": "10.14.2" + }, + "lua5.1-stdlib-debug": { + "package": "lua-stdlib-debug", + "version": "1.0.1-r0" + }, + "lua5.2-stdlib-debug": { + "package": "lua-stdlib-debug", + "version": "1.0.1-r0" + }, + "lua5.3-stdlib-debug": { + "package": "lua-stdlib-debug", + "version": "1.0.1-r0" + }, + "rsyslog-lmcry_gcry": { + "package": "rsyslog-crypto", + "version": "8.40.0-r3" + }, + "freeradius3-ldap": { + "package": "freeradius-ldap", + "version": "3.0.17-r5" + }, + "llvm-libs": { + "package": "llvm5-libs", + "version": "5.0.2-r0" + }, + "py-singledispatch": { + "package": "py2-singledispatch", + "version": 0 + }, + "freeradius3-perl": { + "package": "freeradius-perl", + "version": "3.0.17-r5" + }, + "py-btrfs-progs": { + "package": "py3-btrfs-progs", + "version": "4.19.1-r0" + }, + "nginx-rtmp": { + "package": "nginx-mod-rtmp", + "version": 0 + }, + "nagios-plugins-clamdnagios-plugins-ftpnagios-plugins-imapnagios-plugins-jabbernagios-plugins-nntpnagios-plugins-nntpsnagios-plugins-popnagios-plugins-simapnagios-plugins-spopnagios-plugins-ssmtpnagios-plugins-udp": { + "package": "nagios-plugins-tcp", + "version": 0 + }, + "mysql-dev": { + "package": "mariadb-dev", + "version": "10.3.13-r1" + }, + "python-gdbm": { + "package": "py-gdbm", + "version": "2.7.16-r1" + }, + "consolekit": { + "package": "consolekit2", + "version": "1.2.1" + }, + "py2-setuptools": { + "package": "py-setuptools", + "version": "40.6.3-r0" + }, + "freeradius3-radclient": { + "package": "freeradius-radclient", + "version": "3.0.17-r5" + }, + "python": { + "package": "python2", + "version": "2.7.16-r1" + }, + "py-tevent": { + "package": "py2-tevent", + "version": "0.9.37-r1" + }, + "lua-feedparser-common": { + "package": "lua-feedparser", + "version": "0.71-r1" + }, + "lua5.1-feedparser": { + "package": "lua-feedparser", + "version": "0.71-r1" + }, + "lua5.2-feedparser": { + "package": "lua-feedparser", + "version": "0.71-r1" + }, + "lua5.3-feedparser": { + "package": "lua-feedparser", + "version": "0.71-r1" + }, + "lua-libs": { + "package": "lua5.1-libs", + "version": 0 + }, + "freeradius3-mssql": { + "package": "freeradius-mssql", + "version": "3.0.17-r5" + }, + "postgresql": { + "package": "postgresql-bdr", + "version": 0 + }, + "rsyslog-ompgsql": { + "package": "rsyslog-pgsql", + "version": "8.40.0-r3" + }, + "rsyslog-lmnsd_gtls": { + "package": "rsyslog-tls", + "version": "8.40.0-r3" + }, + "py-ldb": { + "package": "py2-ldb", + "version": "1.3.8-r0" + }, + "rsyslog-imrelp": { + "package": "rsyslog-relp", + "version": "8.40.0-r3" + }, + "rsyslog-omrelp": { + "package": "rsyslog-relp", + "version": "8.40.0-r3" + }, + "perl-encode-piconv": { + "package": "perl-encode-utils", + "version": 0 + }, + "flake8": { + "package": "py2-flake8", + "version": "3.4.1-r2" + }, + "py-pep8": { + "package": "py2-pep8", + "version": "1.7.1-r0" + }, + "quagga-nhrp": { + "package": "quagga", + "version": "1.2.4" + }, + "freeradius3-pam": { + "package": "freeradius-pam", + "version": "3.0.17-r5" + }, + "gnupg": { + "package": "gnupg1", + "version": "1.4.23-r0" + }, + "rsyslog-omhiredis": { + "package": "rsyslog-hiredis", + "version": "8.40.0-r3" + }, + "lua5.1-stdlib-normalize": { + "package": "lua-stdlib-normalize", + "version": "2.0.2-r0" + }, + "lua5.2-stdlib-normalize": { + "package": "lua-stdlib-normalize", + "version": "2.0.2-r0" + }, + "lua5.3-stdlib-normalize": { + "package": "lua-stdlib-normalize", + "version": "2.0.2-r0" + }, + "freeradius3-eap": { + "package": "freeradius-eap", + "version": "3.0.17-r5" + }, + "lua-dev": { + "package": "lua5.1-dev", + "version": 0 + }, + "py2-uritemplate.py": { + "package": "py2-uritemplate", + "version": 0 + }, + "llvm-test-utils": { + "package": "llvm5-test-utils", + "version": "5.0.2-r0" + }, + "lit": { + "package": "llvm5-test-utils", + "version": "0.6.0-r0" + }, + "lua5.1-gversion": { + "package": "lua-gversion", + "version": "0.2.0-r2" + }, + "lua5.2-gversion": { + "package": "lua-gversion", + "version": "0.2.0-r2" + }, + "lua5.3-gversion": { + "package": "lua-gversion", + "version": "0.2.0-r2" + }, + "py-tdb": { + "package": "py2-tdb", + "version": "1.3.16-r0" + }, + "rsyslog-omudpspoof": { + "package": "rsyslog-udpspoof", + "version": "8.40.0-r3" + }, + "pkgconfig": { + "package": "pkgconf", + "version": "1" + }, + "rsyslog-ommysql": { + "package": "rsyslog-mysql", + "version": "8.40.0-r3" + }, + "rsyslog-imczmq": { + "package": "rsyslog-zmq", + "version": "8.40.0-r3" + }, + "rsyslog-omczmq": { + "package": "rsyslog-zmq", + "version": "8.40.0-r3" + }, + "freeradius3-mysql": { + "package": "freeradius-mysql", + "version": "3.0.17-r5" + }, + "perl-net-ldap": { + "package": "perl-ldap", + "version": 0 + }, + "nginx-lua": { + "package": "nginx-mod-http-lua", + "version": 0 + }, + "nodejs-npm": { + "package": "npm", + "version": "10.14.2-r0" + }, + "nodejs-current-npm": { + "package": "npm", + "version": "10.14.2-r0" + }, + "lua5.1-inspect": { + "package": "lua-inspect", + "version": "3.1.1-r1" + }, + "lua5.2-inspect": { + "package": "lua-inspect", + "version": "3.1.1-r1" + }, + "lua5.3-inspect": { + "package": "lua-inspect", + "version": "3.1.1-r1" + }, + "py-graphviz": { + "package": "py2-gv", + "version": "2.40.1-r1" + }, + "llvm": { + "package": "llvm5", + "version": "5.0.2-r0" + }, + "py-libxslt": { + "package": "py2-libxslt", + "version": "1.1.33-r1" + }, + "newsbeuter": { + "package": "newsboat", + "version": "2.13-r0" + }, + "perl-yaml-xs": { + "package": "perl-yaml-libyaml", + "version": "0.72-r0" + }, + "py-backports.ssl_match_hostname": { + "package": "py2-backports.ssl_match_hostname", + "version": 0 + } + } + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile.go b/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile.go new file mode 100644 index 000000000000..43f9d497bffe --- /dev/null +++ b/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile.go @@ -0,0 +1,213 @@ +package dockerfile + +import ( + "bytes" + "context" + "fmt" + "regexp" + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/image" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/detection" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/mapfs" + "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/version/doc" +) + +var ( + disabledChecks = set.New("DS-0007", "DS-0016") + reason = "See " + doc.URL("guide/target/container_image", "disabled-checks") +) + +const analyzerVersion = 1 + +func init() { + analyzer.RegisterConfigAnalyzer(analyzer.TypeHistoryDockerfile, newHistoryAnalyzer) +} + +type historyAnalyzer struct { + scanner *misconf.Scanner +} + +func newHistoryAnalyzer(opts analyzer.ConfigAnalyzerOptions) (analyzer.ConfigAnalyzer, error) { + s, err := misconf.NewScanner(detection.FileTypeDockerfile, opts.MisconfScannerOption) + if err != nil { + return nil, xerrors.Errorf("misconfiguration scanner error: %w", err) + } + return &historyAnalyzer{ + scanner: s, + }, nil +} + +func (a *historyAnalyzer) Analyze(ctx context.Context, input analyzer.ConfigAnalysisInput) (*analyzer. + ConfigAnalysisResult, error) { + if input.Config == nil { + return nil, nil + } + + fsys := mapfs.New() + if err := fsys.WriteVirtualFile( + "Dockerfile", imageConfigToDockerfile(input.Config), 0o600); err != nil { + return nil, xerrors.Errorf("mapfs write error: %w", err) + } + + misconfs, err := a.scanner.Scan(ctx, fsys) + if err != nil { + return nil, xerrors.Errorf("history scan error: %w", err) + } + // The result should be a single element as it passes one Dockerfile. + if len(misconfs) != 1 { + return nil, nil + } + + misconfig := misconfs[0] + misconfig.Failures = filterDisabledChecks(misconfig.Failures) + return &analyzer.ConfigAnalysisResult{ + Misconfiguration: &misconfig, + }, nil +} + +func imageConfigToDockerfile(cfg *v1.ConfigFile) []byte { + dockerfile := new(bytes.Buffer) + baseLayerIndex := image.GuessBaseImageIndex(cfg.History) + for i := baseLayerIndex + 1; i < len(cfg.History); i++ { + h := cfg.History[i] + var createdBy string + switch { + case strings.HasPrefix(h.CreatedBy, "/bin/sh -c #(nop)"): + // Instruction other than RUN + createdBy = strings.TrimPrefix(h.CreatedBy, "/bin/sh -c #(nop)") + if strings.HasPrefix(createdBy, " COPY") || strings.HasPrefix(createdBy, " ADD") { + createdBy = normalizeCopyCreatedBy(createdBy) + } + case strings.HasPrefix(h.CreatedBy, "/bin/sh -c"): + // RUN instruction + createdBy = buildRunInstruction(createdBy) + case strings.HasSuffix(h.CreatedBy, "# buildkit"): + // buildkit instructions + // COPY ./foo /foo # buildkit + // ADD ./foo.txt /foo.txt # buildkit + // RUN /bin/sh -c ls -hl /foo # buildkit + createdBy = strings.TrimSuffix(h.CreatedBy, "# buildkit") + createdBy = buildRunInstruction(createdBy) + case strings.HasPrefix(h.CreatedBy, "USER"): + // USER instruction + createdBy = h.CreatedBy + case strings.HasPrefix(h.CreatedBy, "HEALTHCHECK"): + // HEALTHCHECK instruction + createdBy = buildHealthcheckInstruction(cfg.Config.Healthcheck) + case strings.HasPrefix(h.CreatedBy, "ENV"): + createdBy = restoreEnvLine(h.CreatedBy) + default: + for _, prefix := range []string{"ARG", "ENTRYPOINT"} { + if strings.HasPrefix(h.CreatedBy, prefix) { + createdBy = h.CreatedBy + break + } + } + } + + createdBy = stripBuildMetadata(createdBy) + dockerfile.WriteString(strings.TrimSpace(createdBy) + "\n") + } + + // The user can be changed from the config file or with the `--user` flag (for docker CLI), so we need to add this user to avoid incorrect user detection + if cfg.Config.User != "" { + user := fmt.Sprintf("USER %s", cfg.Config.User) + dockerfile.WriteString(user) + } + + return dockerfile.Bytes() +} + +var metadataRe = regexp.MustCompile(`\|[a-zA-Z0-9_-]+=[^ \t]+`) + +// stripBuildMetadata removes build metadata suffixes appended by container build backends +// (e.g., Buildah, Buildkit). Each suffix has the form "|key=value". +// Example: "/bin/sh -c #(nop) HEALTHCHECK NONE|unsetLabel=true|inheritLabels=false|force-mtime=10" +// c.f. Buildah source for metadata construction: +// https://github.com/containers/buildah/blob/fb473e4d538f693f8b3ee3f8f2ed93a2abed5064/imagebuildah/stage_executor.go#L2616 +func stripBuildMetadata(line string) string { + return metadataRe.ReplaceAllString(line, "") +} + +func buildRunInstruction(s string) string { + pos := strings.Index(s, "/bin/sh -c") + if pos == -1 { + return s + } + return "RUN" + s[pos+len("/bin/sh -c"):] +} + +func buildHealthcheckInstruction(health *v1.HealthConfig) string { + var interval, timeout, startPeriod, retries, command string + if health.Interval != 0 { + interval = fmt.Sprintf("--interval=%s ", health.Interval) + } + if health.Timeout != 0 { + timeout = fmt.Sprintf("--timeout=%s ", health.Timeout) + } + if health.StartPeriod != 0 { + startPeriod = fmt.Sprintf("--start-period=%s ", health.StartPeriod) + } + if health.Retries != 0 { + retries = fmt.Sprintf("--retries=%d ", health.Retries) + } + command = strings.Join(health.Test, " ") + command = strings.ReplaceAll(command, "CMD-SHELL", "CMD") + return fmt.Sprintf("HEALTHCHECK %s%s%s%s%s", interval, timeout, startPeriod, retries, command) +} + +var copyInRe = regexp.MustCompile(`\b((?:file|dir):\S+) in `) + +func normalizeCopyCreatedBy(input string) string { + return copyInRe.ReplaceAllString(input, `$1 `) +} + +// restoreEnvLine normalizes a Dockerfile ENV instruction from image history. +// +// Legacy Dockerfiles may use ENV in the form: "ENV key val1 val2". +// Docker stores this in image history as: "ENV key=val1 val2". +// This can cause parsing errors, because extra tokens are treated as separate keys. +// restoreEnvLine converts such lines into a valid `ENV =""` format +// and wraps the value in quotes to preserve internal spaces and tabs. +// e.g. `ENV tags=latest v0.0.0` -> `ENV key="latest v0.0.0"` +func restoreEnvLine(createdBy string) string { + k, v, ok := strings.Cut(createdBy, "=") + if !ok { + return createdBy + } + return fmt.Sprintf("%s=%q", k, v) +} + +func (a *historyAnalyzer) Required(_ types.OS) bool { + return true +} + +func (a *historyAnalyzer) Type() analyzer.Type { + return analyzer.TypeHistoryDockerfile +} + +func (a *historyAnalyzer) Version() int { + return analyzerVersion +} + +func filterDisabledChecks(results types.MisconfResults) types.MisconfResults { + var filtered types.MisconfResults + for _, r := range results { + if disabledChecks.Contains(r.ID) { + log.WithPrefix("image history analyzer").Info("Skip disabled check", + log.String("ID", r.ID), log.String("reason", reason)) + continue + } + filtered = append(filtered, r) + } + return filtered +} diff --git a/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile_test.go b/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile_test.go new file mode 100644 index 000000000000..eacb0fec68c2 --- /dev/null +++ b/pkg/fanal/analyzer/imgconf/dockerfile/dockerfile_test.go @@ -0,0 +1,484 @@ +package dockerfile + +import ( + "bytes" + "testing" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/scanners/dockerfile/parser" +) + +func Test_historyAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + input analyzer.ConfigAnalysisInput + want *analyzer.ConfigAnalysisResult + wantErr bool + }{ + { + name: "happy path no policy failure", + input: analyzer.ConfigAnalysisInput{ + Config: &v1.ConfigFile{ + Config: v1.Config{ + Healthcheck: &v1.HealthConfig{ + Test: []string{"CMD-SHELL", "curl --fail http://localhost:3000 || exit 1"}, + Interval: time.Second * 10, + Timeout: time.Second * 3, + }, + }, + History: []v1.History{ + { + // this is fine, see https://github.com/aquasecurity/trivy-checks/pull/60 for details + CreatedBy: "/bin/sh -c #(nop) ADD file:e4d600fc4c9c293efe360be7b30ee96579925d1b4634c94332e2ec73f7d8eca1 in /", + EmptyLayer: false, + }, + { + CreatedBy: `HEALTHCHECK &{["CMD-SHELL" "curl --fail http://localhost:3000 || exit 1"] "10s" "3s" "0s" '\x00'}`, + EmptyLayer: false, + }, + { + CreatedBy: `USER user`, + EmptyLayer: true, + }, + { + CreatedBy: `/bin/sh -c #(nop) CMD [\"/bin/sh\"]`, + EmptyLayer: true, + }, + }, + }, + }, + want: &analyzer.ConfigAnalysisResult{ + Misconfiguration: &types.Misconfiguration{ + FileType: "dockerfile", + FilePath: "Dockerfile", + }, + }, + }, + { + name: "happy path with policy failure", + input: analyzer.ConfigAnalysisInput{ + Config: &v1.ConfigFile{ + Config: v1.Config{ + Healthcheck: &v1.HealthConfig{ + Test: []string{"CMD-SHELL", "curl --fail http://localhost:3000 || exit 1"}, + Interval: time.Second * 10, + Timeout: time.Second * 3, + }, + }, + History: []v1.History{ + { + CreatedBy: "/bin/sh -c #(nop) ADD foo.txt /", + EmptyLayer: false, + }, + { + CreatedBy: `HEALTHCHECK &{["CMD-SHELL" "curl --fail http://localhost:3000 || exit 1"] "10s" "3s" "0s" '\x00'}`, + EmptyLayer: false, + }, + { + CreatedBy: `USER user`, + EmptyLayer: true, + }, + { + CreatedBy: `/bin/sh -c #(nop) CMD [\"/bin/sh\"]`, + EmptyLayer: true, + }, + }, + }, + }, + want: &analyzer.ConfigAnalysisResult{ + Misconfiguration: &types.Misconfiguration{ + FileType: "dockerfile", + FilePath: "Dockerfile", + Failures: types.MisconfResults{ + types.MisconfResult{ + Namespace: "builtin.dockerfile.DS005", + Query: "data.builtin.dockerfile.DS005.deny", + Message: "Consider using 'COPY foo.txt /' command instead of 'ADD foo.txt /'", + PolicyMetadata: types.PolicyMetadata{ + ID: "DS-0005", + Aliases: []string{"AVD-DS-0005", "DS005", "use-copy-over-add"}, + Type: "Dockerfile Security Check", + Title: "ADD instead of COPY", + Description: "You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.", + Severity: "LOW", + RecommendedActions: "Use COPY instead of ADD", + References: []string{"https://docs.docker.com/engine/reference/builder/#add"}, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + StartLine: 1, + EndLine: 1, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "ADD foo.txt /", + IsCause: true, + Truncated: false, + Highlighted: "\x1b[38;5;64mADD\x1b[0m foo.txt /", + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with buildkit instructions", + input: analyzer.ConfigAnalysisInput{ + Config: &v1.ConfigFile{ + Config: v1.Config{ + Healthcheck: &v1.HealthConfig{ + Test: []string{"CMD-SHELL", "curl --fail http://localhost:3000 || exit 1"}, + Interval: time.Second * 10, + Timeout: time.Second * 3, + }, + User: "1002", + }, + History: []v1.History{ + { + CreatedBy: "/bin/sh -c #(nop) ADD file:289c2fac17119508ced527225d445747cd177111b4a0018a6b04948ecb3b5e29 in / ", + EmptyLayer: false, + }, + { + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + EmptyLayer: true, + }, + { + CreatedBy: "ADD ./foo.txt /foo.txt # buildkit", + EmptyLayer: false, + }, + { + CreatedBy: "COPY ./foo /foo # buildkit", + EmptyLayer: false, + }, + { + CreatedBy: "RUN /bin/sh -c ls -hl /foo # buildkit", + EmptyLayer: false, + }, + { + CreatedBy: "USER root", // .Config.User takes precedence over this line + EmptyLayer: true, + }, + { + CreatedBy: `HEALTHCHECK &{["CMD-SHELL" "curl -sS 127.0.0.1 || exit 1"] "10s" "3s" "0s" '\x00'}`, + EmptyLayer: true, + }, + }, + }, + }, + want: &analyzer.ConfigAnalysisResult{ + Misconfiguration: &types.Misconfiguration{ + FileType: "dockerfile", + FilePath: "Dockerfile", + Failures: types.MisconfResults{ + types.MisconfResult{ + Namespace: "builtin.dockerfile.DS005", + Query: "data.builtin.dockerfile.DS005.deny", + Message: "Consider using 'COPY ./foo.txt /foo.txt' command instead of 'ADD ./foo.txt /foo.txt'", + PolicyMetadata: types.PolicyMetadata{ + ID: "DS-0005", + Aliases: []string{"AVD-DS-0005", "DS005", "use-copy-over-add"}, + Type: "Dockerfile Security Check", + Title: "ADD instead of COPY", + Description: "You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.", + Severity: "LOW", + RecommendedActions: "Use COPY instead of ADD", + References: []string{"https://docs.docker.com/engine/reference/builder/#add"}, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + StartLine: 1, + EndLine: 1, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "ADD ./foo.txt /foo.txt", + IsCause: true, + Truncated: false, + Highlighted: "\x1b[38;5;64mADD\x1b[0m ./foo.txt /foo.txt", + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path. Base layer is found", + input: analyzer.ConfigAnalysisInput{ + Config: &v1.ConfigFile{ + Config: v1.Config{ + Healthcheck: &v1.HealthConfig{ + Test: []string{"CMD-SHELL", "curl --fail http://localhost:3000 || exit 1"}, + Interval: time.Second * 10, + Timeout: time.Second * 3, + }, + }, + History: []v1.History{ + { + CreatedBy: "/bin/sh -c #(nop) ADD file:e4d600fc4c9c293efe360be7b30ee96579925d1b4634c94332e2ec73f7d8eca1 in /", + EmptyLayer: false, + }, + { + CreatedBy: `/bin/sh -c #(nop) CMD [\"/bin/sh\"]`, + EmptyLayer: true, + }, + { + CreatedBy: `HEALTHCHECK &{["CMD-SHELL" "curl --fail http://localhost:3000 || exit 1"] "10s" "3s" "0s" '\x00'}`, + EmptyLayer: false, + }, + { + CreatedBy: `/bin/sh -c #(nop) CMD [\"/bin/sh\"]`, + EmptyLayer: true, + }, + }, + }, + }, + want: &analyzer.ConfigAnalysisResult{ + Misconfiguration: &types.Misconfiguration{ + FileType: "dockerfile", + FilePath: "Dockerfile", + Failures: types.MisconfResults{ + types.MisconfResult{ + Namespace: "builtin.dockerfile.DS002", + Query: "data.builtin.dockerfile.DS002.deny", + Message: "Specify at least 1 USER command in Dockerfile with non-root user as argument", + PolicyMetadata: types.PolicyMetadata{ + ID: "DS-0002", + Aliases: []string{"AVD-DS-0002", "DS002", "least-privilege-user"}, + Type: "Dockerfile Security Check", + Title: "Image user should not be 'root'", + Description: "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", + Severity: "HIGH", + RecommendedActions: "Add 'USER ' line to the Dockerfile", + References: []string{ + "https://docs.docker." + + "com/develop/develop-images/dockerfile_best-practices/", + }, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + }, + }, + }, + }, + }, + }, + { + name: "nil config", + input: analyzer.ConfigAnalysisInput{ + Config: nil, + }, + }, + { + name: "DS016 check not detected", + input: analyzer.ConfigAnalysisInput{ + Config: &v1.ConfigFile{ + Config: v1.Config{ + Healthcheck: &v1.HealthConfig{ + Test: []string{"CMD-SHELL", "curl --fail http://localhost:3000 || exit 1"}, + Interval: time.Second * 10, + Timeout: time.Second * 3, + }, + }, + History: []v1.History{ + { + // duplicate command from another layer + CreatedBy: `/bin/sh -c #(nop) CMD [\"/bin/bash\"]`, + EmptyLayer: true, + }, + { + CreatedBy: "/bin/sh -c #(nop) ADD file:e4d600fc4c9c293efe360be7b30ee96579925d1b4634c94332e2ec73f7d8eca1 in /", + }, + { + CreatedBy: `HEALTHCHECK &{["CMD-SHELL" "curl --fail http://localhost:3000 || exit 1"] "10s" "3s" "0s" '\x00'}`, + }, + { + CreatedBy: `USER user`, + EmptyLayer: true, + }, + { + CreatedBy: `/bin/sh -c #(nop) CMD [\"/bin/sh\"]`, + EmptyLayer: true, + }, + }, + }, + }, + want: &analyzer.ConfigAnalysisResult{ + Misconfiguration: &types.Misconfiguration{ + FileType: types.Dockerfile, + FilePath: "Dockerfile", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newHistoryAnalyzer(analyzer.ConfigAnalyzerOptions{}) + require.NoError(t, err) + got, err := a.Analyze(t.Context(), tt.input) + if tt.wantErr { + require.Error(t, err) + return + } + if got != nil && got.Misconfiguration != nil { + got.Misconfiguration.Successes = nil // Not compare successes in this test + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_ImageConfigToDockerfile(t *testing.T) { + tests := []struct { + name string + input *v1.ConfigFile + expected string + }{ + { + name: "run instruction with build args", + input: &v1.ConfigFile{ + History: []v1.History{ + { + CreatedBy: "RUN |1 pkg=curl /bin/sh -c apk add $pkg # buildkit", + }, + }, + }, + expected: "RUN apk add $pkg\n", + }, + { + name: "healthcheck instruction with system's default shell", + input: &v1.ConfigFile{ + History: []v1.History{ + { + CreatedBy: "HEALTHCHECK &{[\"CMD-SHELL\" \"curl -f http://localhost/ || exit 1\"] \"5m0s\" \"3s\" \"1s\" \"5s\" '\\x03'}", + }, + }, + Config: v1.Config{ + Healthcheck: &v1.HealthConfig{ + Test: []string{"CMD-SHELL", "curl -f http://localhost/ || exit 1"}, + Interval: time.Minute * 5, + Timeout: time.Second * 3, + StartPeriod: time.Second * 1, + Retries: 3, + }, + }, + }, + expected: "HEALTHCHECK --interval=5m0s --timeout=3s --start-period=1s --retries=3 CMD curl -f http://localhost/ || exit 1\n", + }, + { + name: "healthcheck instruction exec arguments directly", + input: &v1.ConfigFile{ + History: []v1.History{ + { + CreatedBy: "HEALTHCHECK &{[\"CMD\" \"curl\" \"-f\" \"http://localhost/\" \"||\" \"exit 1\"] \"0s\" \"0s\" \"0s\" \"0s\" '\x03'}", + }, + }, + Config: v1.Config{ + Healthcheck: &v1.HealthConfig{ + Test: []string{"CMD", "curl", "-f", "http://localhost/", "||", "exit 1"}, + Retries: 3, + }, + }, + }, + expected: "HEALTHCHECK --retries=3 CMD curl -f http://localhost/ || exit 1\n", + }, + { + name: "nop, no run instruction", + input: &v1.ConfigFile{ + History: []v1.History{ + { + CreatedBy: "/bin/sh -c #(nop) ARG TAG=latest", + }, + }, + }, + expected: "ARG TAG=latest\n", + }, + { + name: "buildkit metadata instructions", + input: &v1.ConfigFile{ + History: []v1.History{ + { + CreatedBy: "ARG TAG=latest", + }, + { + CreatedBy: "ENV TAG=latest", + }, + { + CreatedBy: "ENTRYPOINT [\"/bin/sh\" \"-c\" \"echo test\"]", + }, + }, + }, + expected: `ARG TAG=latest +ENV TAG="latest" +ENTRYPOINT ["/bin/sh" "-c" "echo test"] +`, + }, + { + name: "remove backend-specific metadata suffixes", + input: &v1.ConfigFile{ + History: []v1.History{ + { + CreatedBy: "/bin/sh -c #(nop) COPY dir:3a024d8085bc39741a0a094a8e287a00a760975c7c2e6b5dc6c7d3174b7d1ab6 in ./files |inheritLabels=false", + }, + { + CreatedBy: "/bin/sh -c #(nop) ADD file:24d346633efc860b5011cefa5c0af73006e74e5dfb3c5c0e9cb0e90a927931e1 in readme |inheritLabels=false", + }, + { + CreatedBy: "/bin/sh -c #(nop) HEALTHCHECK NONE|unsetLabel=true|inheritLabels=false|force-mtime=10", + }, + { + CreatedBy: `/bin/sh -c #(nop) ENTRYPOINT ["/bin/sh"]|inheritLabels=false`, + }, + }, + }, + expected: `COPY dir:3a024d8085bc39741a0a094a8e287a00a760975c7c2e6b5dc6c7d3174b7d1ab6 ./files +ADD file:24d346633efc860b5011cefa5c0af73006e74e5dfb3c5c0e9cb0e90a927931e1 readme +HEALTHCHECK NONE +ENTRYPOINT ["/bin/sh"] +`, + }, + { + name: "legacy env format", + input: &v1.ConfigFile{ + History: []v1.History{ + { + CreatedBy: "ENV TEST=foo bar", + }, + }, + }, + expected: "ENV TEST=\"foo bar\"\n", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := imageConfigToDockerfile(tt.input) + p := parser.NewParser(parser.WithStrict()) + _, err := p.Parse(t.Context(), bytes.NewReader(got), "Dockerfile") + require.NoError(t, err) + + assert.Equal(t, tt.expected, string(got)) + }) + } +} diff --git a/pkg/fanal/analyzer/imgconf/secret/secret.go b/pkg/fanal/analyzer/imgconf/secret/secret.go new file mode 100644 index 000000000000..7642712c94d6 --- /dev/null +++ b/pkg/fanal/analyzer/imgconf/secret/secret.go @@ -0,0 +1,75 @@ +package secret + +import ( + "bytes" + "context" + "encoding/json" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/secret" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +const analyzerVersion = 1 + +func init() { + analyzer.RegisterConfigAnalyzer(analyzer.TypeImageConfigSecret, newSecretAnalyzer) +} + +// secretAnalyzer detects secrets in container image config. +type secretAnalyzer struct { + scanner secret.Scanner +} + +func newSecretAnalyzer(opts analyzer.ConfigAnalyzerOptions) (analyzer.ConfigAnalyzer, error) { + configPath := opts.SecretScannerOption.ConfigPath + c, err := secret.ParseConfig(configPath) + if err != nil { + return nil, xerrors.Errorf("secret config error: %w", err) + } + scanner := secret.NewScanner(c) + + return &secretAnalyzer{ + scanner: scanner, + }, nil +} + +func (a *secretAnalyzer) Analyze(_ context.Context, input analyzer.ConfigAnalysisInput) (*analyzer. + ConfigAnalysisResult, error) { + if input.Config == nil { + return nil, nil + } + b, err := json.MarshalIndent(input.Config, " ", "") + if err != nil { + return nil, xerrors.Errorf("json marshal error: %w", err) + } + + result := a.scanner.Scan(secret.ScanArgs{ + FilePath: "config.json", + Content: bytes.NewReader(b), + }) + + if len(result.Findings) == 0 { + log.Debug("No secrets found in container image config") + return nil, nil + } + + return &analyzer.ConfigAnalysisResult{ + Secret: &result, + }, nil +} + +func (a *secretAnalyzer) Required(_ types.OS) bool { + return true +} + +func (a *secretAnalyzer) Type() analyzer.Type { + return analyzer.TypeImageConfigSecret +} + +func (a *secretAnalyzer) Version() int { + return analyzerVersion +} diff --git a/pkg/fanal/analyzer/imgconf/secret/secret_test.go b/pkg/fanal/analyzer/imgconf/secret/secret_test.go new file mode 100644 index 000000000000..b5c599ec3439 --- /dev/null +++ b/pkg/fanal/analyzer/imgconf/secret/secret_test.go @@ -0,0 +1,109 @@ +package secret + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_secretAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + config *v1.ConfigFile + want *analyzer.ConfigAnalysisResult + wantErr bool + }{ + { + name: "happy path", + config: &v1.ConfigFile{ + Config: v1.Config{ + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "secret=ghp_eifae6eigh3aeSah1shahd6oi1tague6vaey", // dummy token + }, + }, + }, + want: &analyzer.ConfigAnalysisResult{ + Secret: &types.Secret{ + FilePath: "config.json", + Findings: []types.SecretFinding{ + { + RuleID: "github-pat", + Category: "GitHub", + Severity: "CRITICAL", + Title: "GitHub Personal Access Token", + StartLine: 12, + EndLine: 12, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 10, + Content: " \"Env\": [", + Highlighted: " \"Env\": [", + }, + { + Number: 11, + Content: " \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",", + Highlighted: " \"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\",", + }, + { + Number: 12, + Content: " \"secret=****************************************\"", + IsCause: true, + Highlighted: " \"secret=****************************************\"", + FirstCause: true, + LastCause: true, + }, + { + Number: 13, + Content: " ]", + Highlighted: " ]", + }, + }, + }, + Match: " \"secret=****************************************\"", + Offset: 231, + }, + }, + }, + }, + }, + { + name: "no secret", + config: &v1.ConfigFile{ + Config: v1.Config{ + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + }, + }, + }, + want: nil, + }, + { + name: "nil config", + config: nil, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newSecretAnalyzer(analyzer.ConfigAnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.Analyze(t.Context(), analyzer.ConfigAnalysisInput{ + Config: tt.config, + }) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/analyze.go b/pkg/fanal/analyzer/language/analyze.go new file mode 100644 index 000000000000..c0157e4a612c --- /dev/null +++ b/pkg/fanal/analyzer/language/analyze.go @@ -0,0 +1,134 @@ +package language + +import ( + "context" + "io" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type Parser interface { + // Parse parses the dependency file + Parse(_ context.Context, r xio.ReadSeekerAt) ([]types.Package, []types.Dependency, error) +} + +// Analyze returns an analysis result of the lock file +func Analyze(ctx context.Context, fileType types.LangType, filePath string, r xio.ReadSeekerAt, parser Parser) (*analyzer.AnalysisResult, error) { + app, err := Parse(ctx, fileType, filePath, r, parser) + if err != nil { + return nil, xerrors.Errorf("failed to parse %s: %w", filePath, err) + } + + if app == nil { + return nil, nil + } + + return &analyzer.AnalysisResult{Applications: []types.Application{*app}}, nil +} + +// AnalyzePackage returns an analysis result of the package file other than lock files +func AnalyzePackage(ctx context.Context, fileType types.LangType, filePath string, r xio.ReadSeekerAt, parser Parser, checksum bool) (*analyzer.AnalysisResult, error) { + app, err := ParsePackage(ctx, fileType, filePath, r, parser, checksum) + if err != nil { + return nil, xerrors.Errorf("failed to parse %s: %w", filePath, err) + } + + if app == nil { + return nil, nil + } + + return &analyzer.AnalysisResult{Applications: []types.Application{*app}}, nil +} + +// Parse returns a parsed result of the lock file +func Parse(ctx context.Context, fileType types.LangType, filePath string, r io.Reader, parser Parser) (*types.Application, error) { + rr, err := xio.NewReadSeekerAt(r) + if err != nil { + return nil, xerrors.Errorf("reader error: %w", err) + } + parsedPkgs, parsedDependencies, err := parser.Parse(ctx, rr) + if err != nil { + return nil, xerrors.Errorf("failed to parse %s: %w", filePath, err) + } + + // The file path of each library should be empty in case of dependency list such as lock file + // since they all will be the same path. + return toApplication(fileType, filePath, "", nil, parsedPkgs, parsedDependencies), nil +} + +// ParsePackage returns a parsed result of the package file +func ParsePackage(ctx context.Context, fileType types.LangType, filePath string, r xio.ReadSeekerAt, parser Parser, checksum bool) (*types.Application, error) { + parsedPkgs, parsedDependencies, err := parser.Parse(ctx, r) + if err != nil { + return nil, xerrors.Errorf("failed to parse %s: %w", filePath, err) + } + + // The reader is not passed if the checksum is not necessarily calculated. + if !checksum { + r = nil + } + + // The file path of each library should be empty in case of dependency list such as lock file + // since they all will be the same path. + return toApplication(fileType, filePath, filePath, r, parsedPkgs, parsedDependencies), nil +} + +func toApplication(fileType types.LangType, filePath, libFilePath string, r xio.ReadSeekerAt, pkgs []types.Package, depGraph []types.Dependency) *types.Application { + if len(pkgs) == 0 { + return nil + } + + // Calculate the file digest when one of `spdx` formats is selected + d, err := calculateDigest(r) + if err != nil { + log.Warn("Unable to get checksum", log.FilePath(filePath), log.Err(err)) + } + + deps := make(map[string][]string) + for _, dep := range depGraph { + deps[dep.ID] = dep.DependsOn + } + + for i, pkg := range pkgs { + // This file path is populated for virtual file paths within archives, such as nested JAR files. + if pkg.FilePath == "" { + pkgs[i].FilePath = libFilePath + } + pkgs[i].DependsOn = deps[pkg.ID] + pkgs[i].Digest = d + pkgs[i].Indirect = isIndirect(pkg.Relationship) // For backward compatibility + } + + return &types.Application{ + Type: fileType, + FilePath: filePath, + Packages: pkgs, + } +} + +func calculateDigest(r xio.ReadSeekerAt) (digest.Digest, error) { + if r == nil { + return "", nil + } + // return reader to start after it has been read in analyzer + if _, err := r.Seek(0, io.SeekStart); err != nil { + return "", xerrors.Errorf("unable to seek: %w", err) + } + + return digest.CalcSHA1(r) +} + +func isIndirect(rel types.Relationship) bool { + switch rel { + case types.RelationshipIndirect: + return true + default: + return false + } +} diff --git a/pkg/fanal/analyzer/language/analyze_test.go b/pkg/fanal/analyzer/language/analyze_test.go new file mode 100644 index 000000000000..1f3ae4516bb2 --- /dev/null +++ b/pkg/fanal/analyzer/language/analyze_test.go @@ -0,0 +1,109 @@ +package language_test + +import ( + "context" + "io" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +type mockParser struct { + t *testing.T +} + +func (p *mockParser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]types.Package, []types.Dependency, error) { + b, err := io.ReadAll(r) + require.NoError(p.t, err) + + switch string(b) { + case "happy": + return []types.Package{ + { + Name: "test", + Version: "1.2.3", + }, + }, nil, nil + case "sad": + return nil, nil, xerrors.New("unexpected error") + } + + return nil, nil, nil +} + +func TestAnalyze(t *testing.T) { + type args struct { + fileType types.LangType + filePath string + content xio.ReadSeekerAt + } + tests := []struct { + name string + args args + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + args: args{ + fileType: types.GoBinary, + filePath: "app/myweb", + content: strings.NewReader("happy"), + }, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GoBinary, + FilePath: "app/myweb", + Packages: types.Packages{ + { + Name: "test", + Version: "1.2.3", + }, + }, + }, + }, + }, + }, + { + name: "empty", + args: args{ + fileType: types.GoBinary, + filePath: "app/myweb", + content: strings.NewReader(""), + }, + want: nil, + }, + { + name: "sad path", + args: args{ + fileType: types.Jar, + filePath: "app/myweb", + content: strings.NewReader("sad"), + }, + wantErr: "unexpected error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mp := &mockParser{t: t} + + got, err := language.Analyze(t.Context(), tt.args.fileType, tt.args.filePath, tt.args.content, mp) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/c/conan/conan.go b/pkg/fanal/analyzer/language/c/conan/conan.go new file mode 100644 index 000000000000..76c12f5da991 --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/conan.go @@ -0,0 +1,196 @@ +package conan + +import ( + "bufio" + "context" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/c/conan" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeConanLock, newConanLockAnalyzer) +} + +const ( + version = 2 +) + +// conanLockAnalyzer analyzes conan.lock +type conanLockAnalyzer struct { + logger *log.Logger + parser language.Parser +} + +func newConanLockAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return conanLockAnalyzer{ + logger: log.WithPrefix("conan"), + parser: conan.NewParser(), + }, nil +} + +func (a conanLockAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + required := func(_ string, _ fs.DirEntry) bool { + // Parse all required files: `conan.lock` (from a.Required func) + input.FilePatterns.Match() + return true + } + + licenses, err := licensesFromCache() + if err != nil { + a.logger.Debug("Unable to parse cache directory to obtain licenses", log.Err(err)) + } + + var apps []types.Application + if err = fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error { + app, err := language.Parse(ctx, types.Conan, filePath, r, a.parser) + if err != nil { + return xerrors.Errorf("%s parse error: %w", filePath, err) + } + + if app == nil { + return nil + } + + // Fill licenses + for i, lib := range app.Packages { + if license, ok := licenses[lib.Name]; ok { + app.Packages[i].Licenses = []string{ + license, + } + } + } + + sort.Sort(app.Packages) + apps = append(apps, *app) + return nil + }); err != nil { + return nil, xerrors.Errorf("unable to parse conan lock file: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func licensesFromCache() (map[string]string, error) { + cacheDir, err := detectCacheDir() + if err != nil { + return nil, err + } + + required := func(filePath string, _ fs.DirEntry) bool { + return filepath.Base(filePath) == "conanfile.py" + } + + licenses := make(map[string]string) + if err := fsutils.WalkDir(os.DirFS(cacheDir), ".", required, func(_ string, _ fs.DirEntry, r io.Reader) error { + scanner := bufio.NewScanner(r) + var name, license string + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + + // cf. https://docs.conan.io/1/reference/conanfile/attributes.html#name + if n := detectAttribute("name", line); n != "" { + name = n + // Check that the license is already found + if license != "" { + break + } + } + // cf. https://docs.conan.io/1/reference/conanfile/attributes.html#license + if l := detectAttribute("license", line); l != "" { + license = l + // Check that the name is already found + if name != "" { + break + } + } + } + + // Skip files without name/license + if name == "" || license == "" { + return nil + } + + licenses[name] = license + return nil + }); err != nil { + return nil, xerrors.Errorf("the Conan cache dir (%s) walk error: %w", cacheDir, err) + } + return licenses, nil +} + +// detectAttribute detects conan attribute (name, license, etc.) from line +// cf. https://docs.conan.io/1/reference/conanfile/attributes.html +func detectAttribute(attributeName, line string) string { + if !strings.HasPrefix(line, attributeName) { + return "" + } + + // e.g. `license = "Apache or MIT"` -> ` "Apache or MIT"` -> `"Apache or MIT"` -> `Apache or MIT` + if name, v, ok := strings.Cut(line, "="); ok && strings.TrimSpace(name) == attributeName { + attr := strings.TrimSpace(v) + return strings.Trim(attr, `"`) + } + + return "" +} + +func detectCacheDir() (string, error) { + home, _ := os.UserHomeDir() + dirs := []string{ + // conan v2 uses `CONAN_HOME` env + // cf. https://docs.conan.io/2/reference/environment.html#conan-home + // `.conan2` dir is omitted for this env + lo.Ternary(os.Getenv("CONAN_HOME") != "", path.Join(os.Getenv("CONAN_HOME"), "p"), ""), + // conan v1 uses `CONAN_USER_HOME` env + // cf. https://docs.conan.io/en/1.64/reference/env_vars.html#conan-user-home + // `.conan` dir is used for this env + lo.Ternary(os.Getenv("CONAN_USER_HOME") != "", path.Join(os.Getenv("CONAN_USER_HOME"), ".conan", "data"), ""), + // `/.conan2` is default directory for conan v2 + // cf. https://docs.conan.io/2/reference/environment.html#conan-home + path.Join(home, ".conan2", "p"), + // `/.conan` is default directory for conan v1 + // cf. https://docs.conan.io/1/mastering/custom_cache.html + path.Join(home, ".conan", "data"), + } + + for _, dir := range dirs { + if dir != "" { + if fsutils.DirExists(dir) { + return dir, nil + } + } + } + + return "", xerrors.Errorf("the Conan cache directory was not found.") +} + +func (a conanLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { + // Lock file name can be anything + // cf. https://docs.conan.io/1/versioning/lockfiles/introduction.html#locking-dependencies + // By default, we only check the default filename - `conan.lock`. + return filepath.Base(filePath) == types.ConanLock +} + +func (a conanLockAnalyzer) Type() analyzer.Type { + return analyzer.TypeConanLock +} + +func (a conanLockAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/c/conan/conan_test.go b/pkg/fanal/analyzer/language/c/conan/conan_test.go new file mode 100644 index 000000000000..0e32f5541280 --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/conan_test.go @@ -0,0 +1,310 @@ +package conan + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_conanLockAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + cacheDir map[string]string + want *analyzer.AnalysisResult + }{ + { + name: "happy path V1", + dir: "testdata/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Conan, + FilePath: "conan.lock", + Packages: types.Packages{ + { + ID: "openssl/3.0.5", + Name: "openssl", + Version: "3.0.5", + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "zlib/1.2.12", + }, + Locations: []types.Location{ + { + StartLine: 12, + EndLine: 21, + }, + }, + }, + { + ID: "zlib/1.2.12", + Name: "zlib", + Version: "1.2.12", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 28, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path V1 with cache dir", + dir: "testdata/happy", + cacheDir: map[string]string{ + "CONAN_USER_HOME": "testdata/cacheDir", + }, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Conan, + FilePath: "conan.lock", + Packages: types.Packages{ + { + ID: "openssl/3.0.5", + Name: "openssl", + Version: "3.0.5", + Licenses: []string{ + "Apache-2.0", + }, + DependsOn: []string{ + "zlib/1.2.12", + }, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 12, + EndLine: 21, + }, + }, + }, + { + ID: "zlib/1.2.12", + Name: "zlib", + Version: "1.2.12", + Licenses: []string{ + "Zlib", + }, + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 28, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path V2", + dir: "testdata/happy_v2", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Conan, + FilePath: "release.lock", + Packages: types.Packages{ + { + ID: "openssl/3.2.2", + Name: "openssl", + Version: "3.2.2", + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + ID: "zlib/1.3.1", + Name: "zlib", + Version: "1.3.1", + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path V2 with cache dir", + dir: "testdata/happy_v2", + cacheDir: map[string]string{ + "CONAN_HOME": "testdata/cacheDir_v2", + }, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Conan, + FilePath: "release.lock", + Packages: types.Packages{ + + { + ID: "openssl/3.2.2", + Name: "openssl", + Version: "3.2.2", + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + Licenses: []string{ + "Apache-2.0", + }, + }, + { + ID: "zlib/1.3.1", + Name: "zlib", + Version: "1.3.1", + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + Licenses: []string{ + "Zlib", + }, + }, + }, + }, + }, + }, + }, + { + name: "empty file", + dir: "testdata/empty", + want: &analyzer.AnalysisResult{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if len(tt.cacheDir) > 0 { + for env, path := range tt.cacheDir { + t.Setenv(env, path) + break + } + } + a, err := newConanLockAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_conanLockAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "default name", + filePath: "conan.lock", + want: true, + }, + { + name: "name with prefix", + filePath: "pkga_deps.lock", + want: false, + }, + { + name: "txt", + filePath: "test.txt", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := conanLockAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_detectAttribute(t *testing.T) { + tests := []struct { + name string + attrName string + line string + want string + }{ + { + name: "without spaces near `=`", + attrName: "license", + line: `license="bar"`, + want: "bar", + }, + { + name: "with space before `=`", + attrName: "license", + line: `license ="bar"`, + want: "bar", + }, + { + name: "with space after `=`", + attrName: "license", + line: `license= "bar"`, + want: "bar", + }, + { + name: "with space before and after `=`", + attrName: "license", + line: `license = "bar"`, + want: "bar", + }, + { + name: "license with spaces", + attrName: "license", + line: `license = "foo and bar"`, + want: "foo and bar", + }, + { + name: "another attribute", + attrName: "license", + line: `license_contents = "foo"`, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := detectAttribute(tt.attrName, tt.line) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py new file mode 100644 index 000000000000..ba393a6668fe --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/openssl/3.0.5/_/_/export/conanfile.py @@ -0,0 +1,675 @@ +from conan import ConanFile +from conan.errors import ConanInvalidConfiguration +from conan.tools.apple import fix_apple_shared_install_name, is_apple_os, XCRun +from conan.tools.build import build_jobs +from conan.tools.files import chdir, copy, get, rename, replace_in_file, rmdir, save +from conan.tools.gnu import AutotoolsToolchain +from conan.tools.layout import basic_layout +from conan.tools.microsoft import is_msvc, msvc_runtime_flag, unix_path + +import fnmatch +import os +import textwrap + +required_conan_version = ">=1.57.0" + + +class OpenSSLConan(ConanFile): + name="openssl" + settings = "os", "arch", "compiler", "build_type" + url = "https://github.com/conan-io/conan-center-index" + homepage = "https://github.com/openssl/openssl" + license="Apache-2.0" + topics = ("ssl", "tls", "encryption", "security") + description = "A toolkit for the Transport Layer Security (TLS) and Secure Sockets Layer (SSL) protocols" + options = { + "shared": [True, False], + "fPIC": [True, False], + "enable_weak_ssl_ciphers": [True, False], + "386": [True, False], + "capieng_dialog": [True, False], + "enable_capieng": [True, False], + "no_aria": [True, False], + "no_asm": [True, False], + "no_async": [True, False], + "no_blake2": [True, False], + "no_bf": [True, False], + "no_camellia": [True, False], + "no_chacha": [True, False], + "no_cms": [True, False], + "no_comp": [True, False], + "no_ct": [True, False], + "no_cast": [True, False], + "no_deprecated": [True, False], + "no_des": [True, False], + "no_dgram": [True, False], + "no_dh": [True, False], + "no_dsa": [True, False], + "no_dso": [True, False], + "no_ec": [True, False], + "no_ecdh": [True, False], + "no_ecdsa": [True, False], + "no_engine": [True, False], + "no_filenames": [True, False], + "no_fips": [True, False], + "no_gost": [True, False], + "no_idea": [True, False], + "no_legacy": [True, False], + "no_md2": [True, False], + "no_md4": [True, False], + "no_mdc2": [True, False], + "no_module": [True, False], + "no_ocsp": [True, False], + "no_pinshared": [True, False], + "no_rc2": [True, False], + "no_rc4": [True, False], + "no_rc5": [True, False], + "no_rfc3779": [True, False], + "no_rmd160": [True, False], + "no_sm2": [True, False], + "no_sm3": [True, False], + "no_sm4": [True, False], + "no_srp": [True, False], + "no_srtp": [True, False], + "no_sse2": [True, False], + "no_ssl": [True, False], + "no_stdio": [True, False], + "no_seed": [True, False], + "no_sock": [True, False], + "no_ssl3": [True, False], + "no_threads": [True, False], + "no_tls1": [True, False], + "no_ts": [True, False], + "no_whirlpool": [True, False], + "no_zlib": [True, False], + "openssldir": [None, "ANY"], + } + default_options = {key: False for key in options.keys()} + default_options["fPIC"] = True + default_options["no_md2"] = True + default_options["openssldir"] = None + + @property + def _settings_build(self): + return getattr(self, "settings_build", self.settings) + + def config_options(self): + if self.settings.os != "Windows": + self.options.rm_safe("capieng_dialog") + self.options.rm_safe("enable_capieng") + else: + self.options.rm_safe("fPIC") + + if self.settings.os == "Emscripten": + self.options.no_asm = True + self.options.no_threads = True + self.options.no_stdio = True + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + self.settings.rm_safe("compiler.libcxx") + self.settings.rm_safe("compiler.cppstd") + + def requirements(self): + if not self.options.no_zlib: + self.requires("zlib/1.2.13") + + def build_requirements(self): + if self._settings_build.os == "Windows": + if not self.options.no_asm: + self.tool_requires("nasm/2.15.05") + if self._use_nmake: + self.tool_requires("strawberryperl/5.32.1.1") + else: + self.win_bash = True + if not self.conf.get("tools.microsoft.bash:path", check_type=str): + self.tool_requires("msys2/cci.latest") + + def validate(self): + if self.settings.os == "Emscripten": + if not all((self.options.no_asm, self.options.no_threads, self.options.no_stdio)): + raise ConanInvalidConfiguration("os=Emscripten requires openssl:{no_asm,no_threads,no_stdio}=True") + + if self.settings.os == "iOS" and self.options.shared: + raise ConanInvalidConfiguration("OpenSSL 3 does not support building shared libraries for iOS") + + def layout(self): + basic_layout(self, src_folder="src") + + @property + def _is_clangcl(self): + return self.settings.compiler == "clang" and self.settings.os == "Windows" + + @property + def _is_mingw(self): + return self.settings.os == "Windows" and self.settings.compiler == "gcc" + + @property + def _use_nmake(self): + return self._is_clangcl or is_msvc(self) + + def source(self): + get(self, **self.conan_data["sources"][self.version], + destination=self.source_folder, strip_root=True) + + @property + def _target(self): + target = f"conan-{self.settings.build_type}-{self.settings.os}-{self.settings.arch}-{self.settings.compiler}-{self.settings.compiler.version}" + if self._use_nmake: + target = f"VC-{target}" # VC- prefix is important as it's checked by Configure + if self._is_mingw: + target = f"mingw-{target}" + return target + + @property + def _perlasm_scheme(self): + # right now, we need to tweak this for iOS & Android only, as they inherit from generic targets + if self.settings.os in ("iOS", "watchOS", "tvOS"): + return { + "armv7": "ios32", + "armv7s": "ios32", + "armv8": "ios64", + "armv8_32": "ios64", + "armv8.3": "ios64", + "armv7k": "ios32", + }.get(str(self.settings.arch), None) + elif self.settings.os == "Android": + return { + "armv7": "void", + "armv8": "linux64", + "mips": "o32", + "mips64": "64", + "x86": "android", + "x86_64": "elf", + }.get(str(self.settings.arch), None) + return None + + @property + def _asm_target(self): + if self.settings.os in ("Android", "iOS", "watchOS", "tvOS"): + return { + "x86": "x86_asm" if self.settings.os == "Android" else None, + "x86_64": "x86_64_asm" if self.settings.os == "Android" else None, + "armv5el": "armv4_asm", + "armv5hf": "armv4_asm", + "armv6": "armv4_asm", + "armv7": "armv4_asm", + "armv7hf": "armv4_asm", + "armv7s": "armv4_asm", + "armv7k": "armv4_asm", + "armv8": "aarch64_asm", + "armv8_32": "aarch64_asm", + "armv8.3": "aarch64_asm", + "mips": "mips32_asm", + "mips64": "mips64_asm", + "sparc": "sparcv8_asm", + "sparcv9": "sparcv9_asm", + "ia64": "ia64_asm", + "ppc32be": "ppc32_asm", + "ppc32": "ppc32_asm", + "ppc64le": "ppc64_asm", + "ppc64": "ppc64_asm", + "s390": "s390x_asm", + "s390x": "s390x_asm" + }.get(str(self.settings.os), None) + + @property + def _targets(self): + is_cygwin = self.settings.get_safe("os.subsystem") == "cygwin" + return { + "Linux-x86-clang": "linux-x86-clang", + "Linux-x86_64-clang": "linux-x86_64-clang", + "Linux-x86-*": "linux-x86", + "Linux-x86_64-*": "linux-x86_64", + "Linux-armv4-*": "linux-armv4", + "Linux-armv4i-*": "linux-armv4", + "Linux-armv5el-*": "linux-armv4", + "Linux-armv5hf-*": "linux-armv4", + "Linux-armv6-*": "linux-armv4", + "Linux-armv7-*": "linux-armv4", + "Linux-armv7hf-*": "linux-armv4", + "Linux-armv7s-*": "linux-armv4", + "Linux-armv7k-*": "linux-armv4", + "Linux-armv8-*": "linux-aarch64", + "Linux-armv8.3-*": "linux-aarch64", + "Linux-armv8-32-*": "linux-arm64ilp32", + "Linux-mips-*": "linux-mips32", + "Linux-mips64-*": "linux-mips64", + "Linux-ppc32-*": "linux-ppc32", + "Linux-ppc32le-*": "linux-pcc32", + "Linux-ppc32be-*": "linux-ppc32", + "Linux-ppc64-*": "linux-ppc64", + "Linux-ppc64le-*": "linux-ppc64le", + "Linux-pcc64be-*": "linux-pcc64", + "Linux-s390x-*": "linux64-s390x", + "Linux-e2k-*": "linux-generic64", + "Linux-sparc-*": "linux-sparcv8", + "Linux-sparcv9-*": "linux64-sparcv9", + "Linux-*-*": "linux-generic32", + "Macos-x86-*": "darwin-i386-cc", + "Macos-x86_64-*": "darwin64-x86_64-cc", + "Macos-ppc32-*": "darwin-ppc-cc", + "Macos-ppc32be-*": "darwin-ppc-cc", + "Macos-ppc64-*": "darwin64-ppc-cc", + "Macos-ppc64be-*": "darwin64-ppc-cc", + "Macos-armv8-*": "darwin64-arm64-cc", + "Macos-*-*": "darwin-common", + "iOS-x86_64-*": "darwin64-x86_64-cc", + "iOS-*-*": "iphoneos-cross", + "watchOS-*-*": "iphoneos-cross", + "tvOS-*-*": "iphoneos-cross", + # Android targets are very broken, see https://github.com/openssl/openssl/issues/7398 + "Android-armv7-*": "linux-generic32", + "Android-armv7hf-*": "linux-generic32", + "Android-armv8-*": "linux-generic64", + "Android-x86-*": "linux-x86-clang", + "Android-x86_64-*": "linux-x86_64-clang", + "Android-mips-*": "linux-generic32", + "Android-mips64-*": "linux-generic64", + "Android-*-*": "linux-generic32", + "Windows-x86-gcc": "Cygwin-x86" if is_cygwin else "mingw", + "Windows-x86_64-gcc": "Cygwin-x86_64" if is_cygwin else "mingw64", + "Windows-*-gcc": "Cygwin-common" if is_cygwin else "mingw-common", + "Windows-ia64-Visual Studio": "VC-WIN64I", # Itanium + "Windows-x86-Visual Studio": "VC-WIN32", + "Windows-x86_64-Visual Studio": "VC-WIN64A", + "Windows-armv7-Visual Studio": "VC-WIN32-ARM", + "Windows-armv8-Visual Studio": "VC-WIN64-ARM", + "Windows-*-Visual Studio": "VC-noCE-common", + "Windows-ia64-clang": "VC-WIN64I", # Itanium + "Windows-x86-clang": "VC-WIN32", + "Windows-x86_64-clang": "VC-WIN64A", + "Windows-armv7-clang": "VC-WIN32-ARM", + "Windows-armv8-clang": "VC-WIN64-ARM", + "Windows-*-clang": "VC-noCE-common", + "WindowsStore-x86-*": "VC-WIN32-UWP", + "WindowsStore-x86_64-*": "VC-WIN64A-UWP", + "WindowsStore-armv7-*": "VC-WIN32-ARM-UWP", + "WindowsStore-armv8-*": "VC-WIN64-ARM-UWP", + "WindowsStore-*-*": "VC-WIN32-ONECORE", + "WindowsCE-*-*": "VC-CE", + "SunOS-x86-gcc": "solaris-x86-gcc", + "SunOS-x86_64-gcc": "solaris64-x86_64-gcc", + "SunOS-sparc-gcc": "solaris-sparcv8-gcc", + "SunOS-sparcv9-gcc": "solaris64-sparcv9-gcc", + "SunOS-x86-suncc": "solaris-x86-cc", + "SunOS-x86_64-suncc": "solaris64-x86_64-cc", + "SunOS-sparc-suncc": "solaris-sparcv8-cc", + "SunOS-sparcv9-suncc": "solaris64-sparcv9-cc", + "SunOS-*-*": "solaris-common", + "*BSD-x86-*": "BSD-x86", + "*BSD-x86_64-*": "BSD-x86_64", + "*BSD-ia64-*": "BSD-ia64", + "*BSD-sparc-*": "BSD-sparcv8", + "*BSD-sparcv9-*": "BSD-sparcv9", + "*BSD-armv8-*": "BSD-generic64", + "*BSD-mips64-*": "BSD-generic64", + "*BSD-ppc64-*": "BSD-generic64", + "*BSD-ppc64le-*": "BSD-generic64", + "*BSD-ppc64be-*": "BSD-generic64", + "AIX-ppc32-gcc": "aix-gcc", + "AIX-ppc64-gcc": "aix64-gcc", + "AIX-pcc32-*": "aix-cc", + "AIX-ppc64-*": "aix64-cc", + "AIX-*-*": "aix-common", + "*BSD-*-*": "BSD-generic32", + "Emscripten-*-*": "cc", + "Neutrino-*-*": "BASE_unix", + } + + @property + def _ancestor_target(self): + if "CONAN_OPENSSL_CONFIGURATION" in os.environ: + return os.environ["CONAN_OPENSSL_CONFIGURATION"] + compiler = "Visual Studio" if self.settings.compiler == "msvc" else self.settings.compiler + query = f"{self.settings.os}-{self.settings.arch}-{compiler}" + ancestor = next((self._targets[i] for i in self._targets if fnmatch.fnmatch(query, i)), None) + if not ancestor: + raise ConanInvalidConfiguration( + f"Unsupported configuration ({self.settings.os}/{self.settings.arch}/{self.settings.compiler}).\n" + f"Please open an issue at {self.url}.\n" + f"Alternatively, set the CONAN_OPENSSL_CONFIGURATION environment variable into your conan profile." + ) + return ancestor + + def _get_default_openssl_dir(self): + if self.settings.os == "Linux": + return "/etc/ssl" + return os.path.join(self.package_folder, "res") + + @property + def _configure_args(self): + openssldir = self.options.openssldir or self._get_default_openssl_dir() + openssldir = unix_path(self, openssldir) if self.win_bash else openssldir + args = [ + '"%s"' % (self._target), + "shared" if self.options.shared else "no-shared", + "--prefix=/", + "--libdir=lib", + "--openssldir=\"%s\"" % openssldir, + "no-unit-test", + "no-threads" if self.options.no_threads else "threads", + "PERL=%s" % self._perl, + "no-tests", + "--debug" if self.settings.build_type == "Debug" else "--release", + ] + + if self.settings.os == "Android": + args.append(" -D__ANDROID_API__=%s" % str(self.settings.os.api_level)) # see NOTES.ANDROID + if self.settings.os == "Emscripten": + args.append("-D__STDC_NO_ATOMICS__=1") + if self.settings.os == "Windows": + if self.options.enable_capieng: + args.append("enable-capieng") + if self.options.capieng_dialog: + args.append("-DOPENSSL_CAPIENG_DIALOG=1") + else: + args.append("-fPIC" if self.options.get_safe("fPIC", True) else "no-pic") + + args.append("no-fips" if self.options.get_safe("no_fips", True) else "enable-fips") + args.append("no-md2" if self.options.get_safe("no_md2", True) else "enable-md2") + + if self.settings.os == "Neutrino": + args.append("no-asm -lsocket -latomic") + + if not self.options.no_zlib: + zlib_info = self.dependencies["zlib"].cpp_info.aggregated_components() + include_path = zlib_info.includedirs[0] + if self.settings.os == "Windows": + lib_path = "%s/%s.lib" % (zlib_info.libdirs[0], zlib_info.libs[0]) + else: + # Just path, linux will find the right file + lib_path = zlib_info.libdirs[0] + if self._settings_build.os == "Windows": + # clang-cl doesn't like backslashes in #define CFLAGS (builldinf.h -> cversion.c) + include_path = include_path.replace("\\", "/") + lib_path = lib_path.replace("\\", "/") + + if self.dependencies["zlib"].options.shared: + args.append("zlib-dynamic") + else: + args.append("zlib") + + args.extend([ + '--with-zlib-include="%s"' % include_path, + '--with-zlib-lib="%s"' % lib_path + ]) + + for option_name in self.default_options.keys(): + if self.options.get_safe(option_name, False) and option_name not in ("shared", "fPIC", "openssldir", "capieng_dialog", "enable_capieng", "zlib", "no_fips", "no_md2"): + self.output.info(f"Activated option: {option_name}") + args.append(option_name.replace("_", "-")) + return args + + def generate(self): + tc = AutotoolsToolchain(self) + env = tc.environment() + env.define_path("PERL", self._perl) + if self.settings.compiler == "apple-clang": + xcrun = XCRun(self) + env.define_path("CROSS_SDK", os.path.basename(xcrun.sdk_path)) + env.define_path("CROSS_TOP", os.path.dirname(os.path.dirname(xcrun.sdk_path))) + + self._create_targets(tc.cflags, tc.cxxflags, tc.defines, tc.ldflags) + tc.generate(env) + + def _create_targets(self, cflags, cxxflags, defines, ldflags): + config_template = textwrap.dedent("""\ + {targets} = ( + "{target}" => {{ + inherit_from => {ancestor}, + cflags => add("{cflags}"), + cxxflags => add("{cxxflags}"), + {defines} + lflags => add("{lflags}"), + {shared_target} + {shared_cflag} + {shared_extension} + {perlasm_scheme} + }}, + ); + """) + + perlasm_scheme = "" + if self._perlasm_scheme: + perlasm_scheme = 'perlasm_scheme => "%s",' % self._perlasm_scheme + + defines = " ".join(defines) + defines = 'defines => add("%s"),' % defines if defines else "" + targets = "my %targets" + includes = "" + if self.settings.os == "Windows": + includes = includes.replace("\\", "/") # OpenSSL doesn't like backslashes + + if self._asm_target: + ancestor = '[ "%s", asm("%s") ]' % (self._ancestor_target, self._asm_target) + else: + ancestor = '[ "%s" ]' % self._ancestor_target + shared_cflag = "" + shared_extension = "" + shared_target = "" + if self.settings.os == "Neutrino": + if self.options.shared: + shared_extension = 'shared_extension => ".so.\$(SHLIB_VERSION_NUMBER)",' + shared_target = 'shared_target => "gnu-shared",' + if self.options.get_safe("fPIC", True): + shared_cflag = 'shared_cflag => "-fPIC",' + + if self.settings.os in ["iOS", "tvOS", "watchOS"] and self.conf.get("tools.apple:enable_bitcode", check_type=bool): + cflags.append("-fembed-bitcode") + cxxflags.append("-fembed-bitcode") + + config = config_template.format( + targets=targets, + target=self._target, + ancestor=ancestor, + cflags=" ".join(cflags), + cxxflags=" ".join(cxxflags), + defines=defines, + perlasm_scheme=perlasm_scheme, + shared_target=shared_target, + shared_extension=shared_extension, + shared_cflag=shared_cflag, + lflags=" ".join(ldflags) + ) + self.output.info("using target: %s -> %s" % (self._target, self._ancestor_target)) + self.output.info(config) + + save(self, os.path.join(self.source_folder, "Configurations", "20-conan.conf"), config) + + def _run_make(self, targets=None, parallel=True, install=False): + command = [self._make_program] + if install: + command.append(f"DESTDIR={self.package_folder}") + if targets: + command.extend(targets) + if not self._use_nmake: + command.append(("-j%s" % build_jobs(self)) if parallel else "-j1") + self.run(" ".join(command), env="conanbuild") + + @property + def _perl(self): + if self._use_nmake: + return self.dependencies.build["strawberryperl"].conf_info.get("user.strawberryperl:perl", check_type=str) + return "perl" + + def _make(self): + with chdir(self, self.source_folder): + # workaround for clang-cl not producing .pdb files + if self._is_clangcl: + save(self, "ossl_static.pdb", "") + args = " ".join(self._configure_args) + + if self._use_nmake: + self._replace_runtime_in_file(os.path.join("Configurations", "10-main.conf")) + + self.run("{perl} ./Configure {args}".format(perl=self._perl, args=args), env="conanbuild") + if self._use_nmake: + # When `--prefix=/`, the scripts derive `\` without escaping, which + # causes issues on Windows + replace_in_file(self, "Makefile", "INSTALLTOP_dir=\\", "INSTALLTOP_dir=\\\\") + self._run_make() + + def _make_install(self): + with chdir(self, self.source_folder): + self._run_make(targets=["install_sw"], parallel=False, install=True) + + def build(self): + self._make() + self.run(f"{self._perl} {self.source_folder}/configdata.pm --dump") + + @property + def _make_program(self): + return "nmake" if self._use_nmake else "make" + + def _replace_runtime_in_file(self, filename): + runtime = msvc_runtime_flag(self) + for e in ["MDd", "MTd", "MD", "MT"]: + replace_in_file(self, filename, f"/{e} ", f"/{runtime} ", strict=False) + replace_in_file(self, filename, f"/{e}\"", f"/{runtime}\"", strict=False) + + def package(self): + copy(self, "*LICENSE*", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses")) + self._make_install() + if is_apple_os(self): + fix_apple_shared_install_name(self) + + for root, _, files in os.walk(self.package_folder): + for filename in files: + if fnmatch.fnmatch(filename, "*.pdb"): + os.unlink(os.path.join(self.package_folder, root, filename)) + if self._use_nmake: + if self.settings.build_type == "Debug": + with chdir(self, os.path.join(self.package_folder, "lib")): + rename(self, "libssl.lib", "libssld.lib") + rename(self, "libcrypto.lib", "libcryptod.lib") + + if self.options.shared: + libdir = os.path.join(self.package_folder, "lib") + for file in os.listdir(libdir): + if self._is_mingw and file.endswith(".dll.a"): + continue + if file.endswith(".a"): + os.unlink(os.path.join(libdir, file)) + + if not self.options.no_fips: + provdir = os.path.join(self.source_folder, "providers") + modules_dir = os.path.join(self.package_folder, "lib", "ossl-modules") + if self.settings.os == "Macos": + copy(self, "fips.dylib", src=provdir, dst=modules_dir) + elif self.settings.os == "Windows": + copy(self, "fips.dll", src=provdir, dst=modules_dir) + else: + copy(self, "fips.so", src=provdir, dst=modules_dir) + + rmdir(self, os.path.join(self.package_folder, "lib", "pkgconfig")) + + self._create_cmake_module_variables( + os.path.join(self.package_folder, self._module_file_rel_path) + ) + + def _create_cmake_module_variables(self, module_file): + content = textwrap.dedent("""\ + set(OPENSSL_FOUND TRUE) + if(DEFINED OpenSSL_INCLUDE_DIR) + set(OPENSSL_INCLUDE_DIR ${OpenSSL_INCLUDE_DIR}) + endif() + if(DEFINED OpenSSL_Crypto_LIBS) + set(OPENSSL_CRYPTO_LIBRARY ${OpenSSL_Crypto_LIBS}) + set(OPENSSL_CRYPTO_LIBRARIES ${OpenSSL_Crypto_LIBS} + ${OpenSSL_Crypto_DEPENDENCIES} + ${OpenSSL_Crypto_FRAMEWORKS} + ${OpenSSL_Crypto_SYSTEM_LIBS}) + elseif(DEFINED openssl_OpenSSL_Crypto_LIBS_%(config)s) + set(OPENSSL_CRYPTO_LIBRARY ${openssl_OpenSSL_Crypto_LIBS_%(config)s}) + set(OPENSSL_CRYPTO_LIBRARIES ${openssl_OpenSSL_Crypto_LIBS_%(config)s} + ${openssl_OpenSSL_Crypto_DEPENDENCIES_%(config)s} + ${openssl_OpenSSL_Crypto_FRAMEWORKS_%(config)s} + ${openssl_OpenSSL_Crypto_SYSTEM_LIBS_%(config)s}) + endif() + if(DEFINED OpenSSL_SSL_LIBS) + set(OPENSSL_SSL_LIBRARY ${OpenSSL_SSL_LIBS}) + set(OPENSSL_SSL_LIBRARIES ${OpenSSL_SSL_LIBS} + ${OpenSSL_SSL_DEPENDENCIES} + ${OpenSSL_SSL_FRAMEWORKS} + ${OpenSSL_SSL_SYSTEM_LIBS}) + elseif(DEFINED openssl_OpenSSL_SSL_LIBS_%(config)s) + set(OPENSSL_SSL_LIBRARY ${openssl_OpenSSL_SSL_LIBS_%(config)s}) + set(OPENSSL_SSL_LIBRARIES ${openssl_OpenSSL_SSL_LIBS_%(config)s} + ${openssl_OpenSSL_SSL_DEPENDENCIES_%(config)s} + ${openssl_OpenSSL_SSL_FRAMEWORKS_%(config)s} + ${openssl_OpenSSL_SSL_SYSTEM_LIBS_%(config)s}) + endif() + if(DEFINED OpenSSL_LIBRARIES) + set(OPENSSL_LIBRARIES ${OpenSSL_LIBRARIES}) + endif() + if(DEFINED OpenSSL_VERSION) + set(OPENSSL_VERSION ${OpenSSL_VERSION}) + endif() + """% {"config":str(self.settings.build_type).upper()}) + save(self, module_file, content) + + @property + def _module_subfolder(self): + return os.path.join("lib", "cmake") + + @property + def _module_file_rel_path(self): + return os.path.join(self._module_subfolder, + "conan-official-{}-variables.cmake".format(self.name)) + + def package_info(self): + self.cpp_info.set_property("cmake_file_name", "OpenSSL") + self.cpp_info.set_property("cmake_find_mode", "both") + self.cpp_info.set_property("pkg_config_name", "openssl") + self.cpp_info.set_property("cmake_build_modules", [self._module_file_rel_path]) + self.cpp_info.names["cmake_find_package"] = "OpenSSL" + self.cpp_info.names["cmake_find_package_multi"] = "OpenSSL" + self.cpp_info.components["ssl"].builddirs.append(self._module_subfolder) + self.cpp_info.components["ssl"].build_modules["cmake_find_package"] = [self._module_file_rel_path] + self.cpp_info.components["ssl"].set_property("cmake_build_modules", [self._module_file_rel_path]) + self.cpp_info.components["crypto"].builddirs.append(self._module_subfolder) + self.cpp_info.components["crypto"].build_modules["cmake_find_package"] = [self._module_file_rel_path] + self.cpp_info.components["crypto"].set_property("cmake_build_modules", [self._module_file_rel_path]) + + if self._use_nmake: + libsuffix = "d" if self.settings.build_type == "Debug" else "" + self.cpp_info.components["ssl"].libs = ["libssl" + libsuffix] + self.cpp_info.components["crypto"].libs = ["libcrypto" + libsuffix] + else: + self.cpp_info.components["ssl"].libs = ["ssl"] + self.cpp_info.components["crypto"].libs = ["crypto"] + + self.cpp_info.components["ssl"].requires = ["crypto"] + + if not self.options.no_zlib: + self.cpp_info.components["crypto"].requires.append("zlib::zlib") + + if self.settings.os == "Windows": + self.cpp_info.components["crypto"].system_libs.extend(["crypt32", "ws2_32", "advapi32", "user32", "bcrypt"]) + elif self.settings.os == "Linux": + self.cpp_info.components["crypto"].system_libs.extend(["dl", "rt"]) + self.cpp_info.components["ssl"].system_libs.append("dl") + if not self.options.no_threads: + self.cpp_info.components["crypto"].system_libs.append("pthread") + self.cpp_info.components["ssl"].system_libs.append("pthread") + elif self.settings.os == "Neutrino": + self.cpp_info.components["crypto"].system_libs.append("atomic") + self.cpp_info.components["ssl"].system_libs.append("atomic") + + self.cpp_info.components["crypto"].set_property("cmake_target_name", "OpenSSL::Crypto") + self.cpp_info.components["crypto"].set_property("pkg_config_name", "libcrypto") + self.cpp_info.components["ssl"].set_property("cmake_target_name", "OpenSSL::SSL") + self.cpp_info.components["ssl"].set_property("pkg_config_name", "libssl") + self.cpp_info.components["crypto"].names["cmake_find_package"] = "Crypto" + self.cpp_info.components["crypto"].names["cmake_find_package_multi"] = "Crypto" + self.cpp_info.components["ssl"].names["cmake_find_package"] = "SSL" + self.cpp_info.components["ssl"].names["cmake_find_package_multi"] = "SSL" + + openssl_modules_dir = os.path.join(self.package_folder, "lib", "ossl-modules") + self.runenv_info.define_path("OPENSSL_MODULES", openssl_modules_dir) + + # For legacy 1.x downstream consumers, remove once recipe is 2.0 only: + self.env_info.OPENSSL_MODULES = openssl_modules_dir + diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/zlib/1.2.13/_/_/export/conanfile.py b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/zlib/1.2.13/_/_/export/conanfile.py new file mode 100644 index 000000000000..ead39ff73661 --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir/.conan/data/zlib/1.2.13/_/_/export/conanfile.py @@ -0,0 +1,110 @@ +from conan import ConanFile +from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout +from conan.tools.files import apply_conandata_patches, export_conandata_patches, get, load, replace_in_file, save +from conan.tools.scm import Version +import os + +required_conan_version = ">=1.53.0" + + +class ZlibConan(ConanFile): + license = "Zlib" + name = "zlib" + package_type = "library" + url = "https://github.com/conan-io/conan-center-index" + homepage = "https://zlib.net" + description = ("A Massively Spiffy Yet Delicately Unobtrusive Compression Library " + "(Also Free, Not to Mention Unencumbered by Patents)") + topics = ("zlib", "compression") + + settings = "os", "arch", "compiler", "build_type" + options = { + "shared": [True, False], + "fPIC": [True, False], + } + default_options = { + "shared": False, + "fPIC": True, + } + + @property + def _is_mingw(self): + return self.settings.os == "Windows" and self.settings.compiler == "gcc" + + def export_sources(self): + export_conandata_patches(self) + + def config_options(self): + if self.settings.os == "Windows": + del self.options.fPIC + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + self.settings.rm_safe("compiler.libcxx") + self.settings.rm_safe("compiler.cppstd") + + def layout(self): + cmake_layout(self, src_folder="src") + + def source(self): + get(self, **self.conan_data["sources"][self.version], + destination=self.source_folder, strip_root=True) + + def generate(self): + tc = CMakeToolchain(self) + tc.variables["SKIP_INSTALL_ALL"] = False + tc.variables["SKIP_INSTALL_LIBRARIES"] = False + tc.variables["SKIP_INSTALL_HEADERS"] = False + tc.variables["SKIP_INSTALL_FILES"] = True + # Correct for misuse of "${CMAKE_INSTALL_PREFIX}/" in CMakeLists.txt + tc.variables["INSTALL_LIB_DIR"] = "lib" + tc.variables["INSTALL_INC_DIR"] = "include" + tc.variables["ZLIB_BUILD_EXAMPLES"] = False + tc.generate() + + def _patch_sources(self): + apply_conandata_patches(self) + + is_apple_clang12 = self.settings.compiler == "apple-clang" and Version(self.settings.compiler.version) >= "12.0" + if not is_apple_clang12: + for filename in ['zconf.h', 'zconf.h.cmakein', 'zconf.h.in']: + filepath = os.path.join(self.source_folder, filename) + replace_in_file(self, filepath, + '#ifdef HAVE_UNISTD_H ' + '/* may be set to #if 1 by ./configure */', + '#if defined(HAVE_UNISTD_H) && (1-HAVE_UNISTD_H-1 != 0)') + replace_in_file(self, filepath, + '#ifdef HAVE_STDARG_H ' + '/* may be set to #if 1 by ./configure */', + '#if defined(HAVE_STDARG_H) && (1-HAVE_STDARG_H-1 != 0)') + + def build(self): + self._patch_sources() + cmake = CMake(self) + cmake.configure() + cmake.build() + + def _extract_license(self): + tmp = load(self, os.path.join(self.source_folder, "zlib.h")) + license_contents = tmp[2:tmp.find("*/", 1)] + return license_contents + + def package(self): + save(self, os.path.join(self.package_folder, "licenses", "LICENSE"), self._extract_license()) + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.set_property("cmake_find_mode", "both") + self.cpp_info.set_property("cmake_file_name", "ZLIB") + self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") + self.cpp_info.set_property("pkg_config_name", "zlib") + if self.settings.os == "Windows" and not self._is_mingw: + libname = "zdll" if self.options.shared else "zlib" + else: + libname = "z" + self.cpp_info.libs = [libname] + + self.cpp_info.names["cmake_find_package"] = "ZLIB" + self.cpp_info.names["cmake_find_package_multi"] = "ZLIB" diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir_v2/p/opens464b5c427ce9d/e/conanfile.py b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir_v2/p/opens464b5c427ce9d/e/conanfile.py new file mode 100644 index 000000000000..ba393a6668fe --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir_v2/p/opens464b5c427ce9d/e/conanfile.py @@ -0,0 +1,675 @@ +from conan import ConanFile +from conan.errors import ConanInvalidConfiguration +from conan.tools.apple import fix_apple_shared_install_name, is_apple_os, XCRun +from conan.tools.build import build_jobs +from conan.tools.files import chdir, copy, get, rename, replace_in_file, rmdir, save +from conan.tools.gnu import AutotoolsToolchain +from conan.tools.layout import basic_layout +from conan.tools.microsoft import is_msvc, msvc_runtime_flag, unix_path + +import fnmatch +import os +import textwrap + +required_conan_version = ">=1.57.0" + + +class OpenSSLConan(ConanFile): + name="openssl" + settings = "os", "arch", "compiler", "build_type" + url = "https://github.com/conan-io/conan-center-index" + homepage = "https://github.com/openssl/openssl" + license="Apache-2.0" + topics = ("ssl", "tls", "encryption", "security") + description = "A toolkit for the Transport Layer Security (TLS) and Secure Sockets Layer (SSL) protocols" + options = { + "shared": [True, False], + "fPIC": [True, False], + "enable_weak_ssl_ciphers": [True, False], + "386": [True, False], + "capieng_dialog": [True, False], + "enable_capieng": [True, False], + "no_aria": [True, False], + "no_asm": [True, False], + "no_async": [True, False], + "no_blake2": [True, False], + "no_bf": [True, False], + "no_camellia": [True, False], + "no_chacha": [True, False], + "no_cms": [True, False], + "no_comp": [True, False], + "no_ct": [True, False], + "no_cast": [True, False], + "no_deprecated": [True, False], + "no_des": [True, False], + "no_dgram": [True, False], + "no_dh": [True, False], + "no_dsa": [True, False], + "no_dso": [True, False], + "no_ec": [True, False], + "no_ecdh": [True, False], + "no_ecdsa": [True, False], + "no_engine": [True, False], + "no_filenames": [True, False], + "no_fips": [True, False], + "no_gost": [True, False], + "no_idea": [True, False], + "no_legacy": [True, False], + "no_md2": [True, False], + "no_md4": [True, False], + "no_mdc2": [True, False], + "no_module": [True, False], + "no_ocsp": [True, False], + "no_pinshared": [True, False], + "no_rc2": [True, False], + "no_rc4": [True, False], + "no_rc5": [True, False], + "no_rfc3779": [True, False], + "no_rmd160": [True, False], + "no_sm2": [True, False], + "no_sm3": [True, False], + "no_sm4": [True, False], + "no_srp": [True, False], + "no_srtp": [True, False], + "no_sse2": [True, False], + "no_ssl": [True, False], + "no_stdio": [True, False], + "no_seed": [True, False], + "no_sock": [True, False], + "no_ssl3": [True, False], + "no_threads": [True, False], + "no_tls1": [True, False], + "no_ts": [True, False], + "no_whirlpool": [True, False], + "no_zlib": [True, False], + "openssldir": [None, "ANY"], + } + default_options = {key: False for key in options.keys()} + default_options["fPIC"] = True + default_options["no_md2"] = True + default_options["openssldir"] = None + + @property + def _settings_build(self): + return getattr(self, "settings_build", self.settings) + + def config_options(self): + if self.settings.os != "Windows": + self.options.rm_safe("capieng_dialog") + self.options.rm_safe("enable_capieng") + else: + self.options.rm_safe("fPIC") + + if self.settings.os == "Emscripten": + self.options.no_asm = True + self.options.no_threads = True + self.options.no_stdio = True + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + self.settings.rm_safe("compiler.libcxx") + self.settings.rm_safe("compiler.cppstd") + + def requirements(self): + if not self.options.no_zlib: + self.requires("zlib/1.2.13") + + def build_requirements(self): + if self._settings_build.os == "Windows": + if not self.options.no_asm: + self.tool_requires("nasm/2.15.05") + if self._use_nmake: + self.tool_requires("strawberryperl/5.32.1.1") + else: + self.win_bash = True + if not self.conf.get("tools.microsoft.bash:path", check_type=str): + self.tool_requires("msys2/cci.latest") + + def validate(self): + if self.settings.os == "Emscripten": + if not all((self.options.no_asm, self.options.no_threads, self.options.no_stdio)): + raise ConanInvalidConfiguration("os=Emscripten requires openssl:{no_asm,no_threads,no_stdio}=True") + + if self.settings.os == "iOS" and self.options.shared: + raise ConanInvalidConfiguration("OpenSSL 3 does not support building shared libraries for iOS") + + def layout(self): + basic_layout(self, src_folder="src") + + @property + def _is_clangcl(self): + return self.settings.compiler == "clang" and self.settings.os == "Windows" + + @property + def _is_mingw(self): + return self.settings.os == "Windows" and self.settings.compiler == "gcc" + + @property + def _use_nmake(self): + return self._is_clangcl or is_msvc(self) + + def source(self): + get(self, **self.conan_data["sources"][self.version], + destination=self.source_folder, strip_root=True) + + @property + def _target(self): + target = f"conan-{self.settings.build_type}-{self.settings.os}-{self.settings.arch}-{self.settings.compiler}-{self.settings.compiler.version}" + if self._use_nmake: + target = f"VC-{target}" # VC- prefix is important as it's checked by Configure + if self._is_mingw: + target = f"mingw-{target}" + return target + + @property + def _perlasm_scheme(self): + # right now, we need to tweak this for iOS & Android only, as they inherit from generic targets + if self.settings.os in ("iOS", "watchOS", "tvOS"): + return { + "armv7": "ios32", + "armv7s": "ios32", + "armv8": "ios64", + "armv8_32": "ios64", + "armv8.3": "ios64", + "armv7k": "ios32", + }.get(str(self.settings.arch), None) + elif self.settings.os == "Android": + return { + "armv7": "void", + "armv8": "linux64", + "mips": "o32", + "mips64": "64", + "x86": "android", + "x86_64": "elf", + }.get(str(self.settings.arch), None) + return None + + @property + def _asm_target(self): + if self.settings.os in ("Android", "iOS", "watchOS", "tvOS"): + return { + "x86": "x86_asm" if self.settings.os == "Android" else None, + "x86_64": "x86_64_asm" if self.settings.os == "Android" else None, + "armv5el": "armv4_asm", + "armv5hf": "armv4_asm", + "armv6": "armv4_asm", + "armv7": "armv4_asm", + "armv7hf": "armv4_asm", + "armv7s": "armv4_asm", + "armv7k": "armv4_asm", + "armv8": "aarch64_asm", + "armv8_32": "aarch64_asm", + "armv8.3": "aarch64_asm", + "mips": "mips32_asm", + "mips64": "mips64_asm", + "sparc": "sparcv8_asm", + "sparcv9": "sparcv9_asm", + "ia64": "ia64_asm", + "ppc32be": "ppc32_asm", + "ppc32": "ppc32_asm", + "ppc64le": "ppc64_asm", + "ppc64": "ppc64_asm", + "s390": "s390x_asm", + "s390x": "s390x_asm" + }.get(str(self.settings.os), None) + + @property + def _targets(self): + is_cygwin = self.settings.get_safe("os.subsystem") == "cygwin" + return { + "Linux-x86-clang": "linux-x86-clang", + "Linux-x86_64-clang": "linux-x86_64-clang", + "Linux-x86-*": "linux-x86", + "Linux-x86_64-*": "linux-x86_64", + "Linux-armv4-*": "linux-armv4", + "Linux-armv4i-*": "linux-armv4", + "Linux-armv5el-*": "linux-armv4", + "Linux-armv5hf-*": "linux-armv4", + "Linux-armv6-*": "linux-armv4", + "Linux-armv7-*": "linux-armv4", + "Linux-armv7hf-*": "linux-armv4", + "Linux-armv7s-*": "linux-armv4", + "Linux-armv7k-*": "linux-armv4", + "Linux-armv8-*": "linux-aarch64", + "Linux-armv8.3-*": "linux-aarch64", + "Linux-armv8-32-*": "linux-arm64ilp32", + "Linux-mips-*": "linux-mips32", + "Linux-mips64-*": "linux-mips64", + "Linux-ppc32-*": "linux-ppc32", + "Linux-ppc32le-*": "linux-pcc32", + "Linux-ppc32be-*": "linux-ppc32", + "Linux-ppc64-*": "linux-ppc64", + "Linux-ppc64le-*": "linux-ppc64le", + "Linux-pcc64be-*": "linux-pcc64", + "Linux-s390x-*": "linux64-s390x", + "Linux-e2k-*": "linux-generic64", + "Linux-sparc-*": "linux-sparcv8", + "Linux-sparcv9-*": "linux64-sparcv9", + "Linux-*-*": "linux-generic32", + "Macos-x86-*": "darwin-i386-cc", + "Macos-x86_64-*": "darwin64-x86_64-cc", + "Macos-ppc32-*": "darwin-ppc-cc", + "Macos-ppc32be-*": "darwin-ppc-cc", + "Macos-ppc64-*": "darwin64-ppc-cc", + "Macos-ppc64be-*": "darwin64-ppc-cc", + "Macos-armv8-*": "darwin64-arm64-cc", + "Macos-*-*": "darwin-common", + "iOS-x86_64-*": "darwin64-x86_64-cc", + "iOS-*-*": "iphoneos-cross", + "watchOS-*-*": "iphoneos-cross", + "tvOS-*-*": "iphoneos-cross", + # Android targets are very broken, see https://github.com/openssl/openssl/issues/7398 + "Android-armv7-*": "linux-generic32", + "Android-armv7hf-*": "linux-generic32", + "Android-armv8-*": "linux-generic64", + "Android-x86-*": "linux-x86-clang", + "Android-x86_64-*": "linux-x86_64-clang", + "Android-mips-*": "linux-generic32", + "Android-mips64-*": "linux-generic64", + "Android-*-*": "linux-generic32", + "Windows-x86-gcc": "Cygwin-x86" if is_cygwin else "mingw", + "Windows-x86_64-gcc": "Cygwin-x86_64" if is_cygwin else "mingw64", + "Windows-*-gcc": "Cygwin-common" if is_cygwin else "mingw-common", + "Windows-ia64-Visual Studio": "VC-WIN64I", # Itanium + "Windows-x86-Visual Studio": "VC-WIN32", + "Windows-x86_64-Visual Studio": "VC-WIN64A", + "Windows-armv7-Visual Studio": "VC-WIN32-ARM", + "Windows-armv8-Visual Studio": "VC-WIN64-ARM", + "Windows-*-Visual Studio": "VC-noCE-common", + "Windows-ia64-clang": "VC-WIN64I", # Itanium + "Windows-x86-clang": "VC-WIN32", + "Windows-x86_64-clang": "VC-WIN64A", + "Windows-armv7-clang": "VC-WIN32-ARM", + "Windows-armv8-clang": "VC-WIN64-ARM", + "Windows-*-clang": "VC-noCE-common", + "WindowsStore-x86-*": "VC-WIN32-UWP", + "WindowsStore-x86_64-*": "VC-WIN64A-UWP", + "WindowsStore-armv7-*": "VC-WIN32-ARM-UWP", + "WindowsStore-armv8-*": "VC-WIN64-ARM-UWP", + "WindowsStore-*-*": "VC-WIN32-ONECORE", + "WindowsCE-*-*": "VC-CE", + "SunOS-x86-gcc": "solaris-x86-gcc", + "SunOS-x86_64-gcc": "solaris64-x86_64-gcc", + "SunOS-sparc-gcc": "solaris-sparcv8-gcc", + "SunOS-sparcv9-gcc": "solaris64-sparcv9-gcc", + "SunOS-x86-suncc": "solaris-x86-cc", + "SunOS-x86_64-suncc": "solaris64-x86_64-cc", + "SunOS-sparc-suncc": "solaris-sparcv8-cc", + "SunOS-sparcv9-suncc": "solaris64-sparcv9-cc", + "SunOS-*-*": "solaris-common", + "*BSD-x86-*": "BSD-x86", + "*BSD-x86_64-*": "BSD-x86_64", + "*BSD-ia64-*": "BSD-ia64", + "*BSD-sparc-*": "BSD-sparcv8", + "*BSD-sparcv9-*": "BSD-sparcv9", + "*BSD-armv8-*": "BSD-generic64", + "*BSD-mips64-*": "BSD-generic64", + "*BSD-ppc64-*": "BSD-generic64", + "*BSD-ppc64le-*": "BSD-generic64", + "*BSD-ppc64be-*": "BSD-generic64", + "AIX-ppc32-gcc": "aix-gcc", + "AIX-ppc64-gcc": "aix64-gcc", + "AIX-pcc32-*": "aix-cc", + "AIX-ppc64-*": "aix64-cc", + "AIX-*-*": "aix-common", + "*BSD-*-*": "BSD-generic32", + "Emscripten-*-*": "cc", + "Neutrino-*-*": "BASE_unix", + } + + @property + def _ancestor_target(self): + if "CONAN_OPENSSL_CONFIGURATION" in os.environ: + return os.environ["CONAN_OPENSSL_CONFIGURATION"] + compiler = "Visual Studio" if self.settings.compiler == "msvc" else self.settings.compiler + query = f"{self.settings.os}-{self.settings.arch}-{compiler}" + ancestor = next((self._targets[i] for i in self._targets if fnmatch.fnmatch(query, i)), None) + if not ancestor: + raise ConanInvalidConfiguration( + f"Unsupported configuration ({self.settings.os}/{self.settings.arch}/{self.settings.compiler}).\n" + f"Please open an issue at {self.url}.\n" + f"Alternatively, set the CONAN_OPENSSL_CONFIGURATION environment variable into your conan profile." + ) + return ancestor + + def _get_default_openssl_dir(self): + if self.settings.os == "Linux": + return "/etc/ssl" + return os.path.join(self.package_folder, "res") + + @property + def _configure_args(self): + openssldir = self.options.openssldir or self._get_default_openssl_dir() + openssldir = unix_path(self, openssldir) if self.win_bash else openssldir + args = [ + '"%s"' % (self._target), + "shared" if self.options.shared else "no-shared", + "--prefix=/", + "--libdir=lib", + "--openssldir=\"%s\"" % openssldir, + "no-unit-test", + "no-threads" if self.options.no_threads else "threads", + "PERL=%s" % self._perl, + "no-tests", + "--debug" if self.settings.build_type == "Debug" else "--release", + ] + + if self.settings.os == "Android": + args.append(" -D__ANDROID_API__=%s" % str(self.settings.os.api_level)) # see NOTES.ANDROID + if self.settings.os == "Emscripten": + args.append("-D__STDC_NO_ATOMICS__=1") + if self.settings.os == "Windows": + if self.options.enable_capieng: + args.append("enable-capieng") + if self.options.capieng_dialog: + args.append("-DOPENSSL_CAPIENG_DIALOG=1") + else: + args.append("-fPIC" if self.options.get_safe("fPIC", True) else "no-pic") + + args.append("no-fips" if self.options.get_safe("no_fips", True) else "enable-fips") + args.append("no-md2" if self.options.get_safe("no_md2", True) else "enable-md2") + + if self.settings.os == "Neutrino": + args.append("no-asm -lsocket -latomic") + + if not self.options.no_zlib: + zlib_info = self.dependencies["zlib"].cpp_info.aggregated_components() + include_path = zlib_info.includedirs[0] + if self.settings.os == "Windows": + lib_path = "%s/%s.lib" % (zlib_info.libdirs[0], zlib_info.libs[0]) + else: + # Just path, linux will find the right file + lib_path = zlib_info.libdirs[0] + if self._settings_build.os == "Windows": + # clang-cl doesn't like backslashes in #define CFLAGS (builldinf.h -> cversion.c) + include_path = include_path.replace("\\", "/") + lib_path = lib_path.replace("\\", "/") + + if self.dependencies["zlib"].options.shared: + args.append("zlib-dynamic") + else: + args.append("zlib") + + args.extend([ + '--with-zlib-include="%s"' % include_path, + '--with-zlib-lib="%s"' % lib_path + ]) + + for option_name in self.default_options.keys(): + if self.options.get_safe(option_name, False) and option_name not in ("shared", "fPIC", "openssldir", "capieng_dialog", "enable_capieng", "zlib", "no_fips", "no_md2"): + self.output.info(f"Activated option: {option_name}") + args.append(option_name.replace("_", "-")) + return args + + def generate(self): + tc = AutotoolsToolchain(self) + env = tc.environment() + env.define_path("PERL", self._perl) + if self.settings.compiler == "apple-clang": + xcrun = XCRun(self) + env.define_path("CROSS_SDK", os.path.basename(xcrun.sdk_path)) + env.define_path("CROSS_TOP", os.path.dirname(os.path.dirname(xcrun.sdk_path))) + + self._create_targets(tc.cflags, tc.cxxflags, tc.defines, tc.ldflags) + tc.generate(env) + + def _create_targets(self, cflags, cxxflags, defines, ldflags): + config_template = textwrap.dedent("""\ + {targets} = ( + "{target}" => {{ + inherit_from => {ancestor}, + cflags => add("{cflags}"), + cxxflags => add("{cxxflags}"), + {defines} + lflags => add("{lflags}"), + {shared_target} + {shared_cflag} + {shared_extension} + {perlasm_scheme} + }}, + ); + """) + + perlasm_scheme = "" + if self._perlasm_scheme: + perlasm_scheme = 'perlasm_scheme => "%s",' % self._perlasm_scheme + + defines = " ".join(defines) + defines = 'defines => add("%s"),' % defines if defines else "" + targets = "my %targets" + includes = "" + if self.settings.os == "Windows": + includes = includes.replace("\\", "/") # OpenSSL doesn't like backslashes + + if self._asm_target: + ancestor = '[ "%s", asm("%s") ]' % (self._ancestor_target, self._asm_target) + else: + ancestor = '[ "%s" ]' % self._ancestor_target + shared_cflag = "" + shared_extension = "" + shared_target = "" + if self.settings.os == "Neutrino": + if self.options.shared: + shared_extension = 'shared_extension => ".so.\$(SHLIB_VERSION_NUMBER)",' + shared_target = 'shared_target => "gnu-shared",' + if self.options.get_safe("fPIC", True): + shared_cflag = 'shared_cflag => "-fPIC",' + + if self.settings.os in ["iOS", "tvOS", "watchOS"] and self.conf.get("tools.apple:enable_bitcode", check_type=bool): + cflags.append("-fembed-bitcode") + cxxflags.append("-fembed-bitcode") + + config = config_template.format( + targets=targets, + target=self._target, + ancestor=ancestor, + cflags=" ".join(cflags), + cxxflags=" ".join(cxxflags), + defines=defines, + perlasm_scheme=perlasm_scheme, + shared_target=shared_target, + shared_extension=shared_extension, + shared_cflag=shared_cflag, + lflags=" ".join(ldflags) + ) + self.output.info("using target: %s -> %s" % (self._target, self._ancestor_target)) + self.output.info(config) + + save(self, os.path.join(self.source_folder, "Configurations", "20-conan.conf"), config) + + def _run_make(self, targets=None, parallel=True, install=False): + command = [self._make_program] + if install: + command.append(f"DESTDIR={self.package_folder}") + if targets: + command.extend(targets) + if not self._use_nmake: + command.append(("-j%s" % build_jobs(self)) if parallel else "-j1") + self.run(" ".join(command), env="conanbuild") + + @property + def _perl(self): + if self._use_nmake: + return self.dependencies.build["strawberryperl"].conf_info.get("user.strawberryperl:perl", check_type=str) + return "perl" + + def _make(self): + with chdir(self, self.source_folder): + # workaround for clang-cl not producing .pdb files + if self._is_clangcl: + save(self, "ossl_static.pdb", "") + args = " ".join(self._configure_args) + + if self._use_nmake: + self._replace_runtime_in_file(os.path.join("Configurations", "10-main.conf")) + + self.run("{perl} ./Configure {args}".format(perl=self._perl, args=args), env="conanbuild") + if self._use_nmake: + # When `--prefix=/`, the scripts derive `\` without escaping, which + # causes issues on Windows + replace_in_file(self, "Makefile", "INSTALLTOP_dir=\\", "INSTALLTOP_dir=\\\\") + self._run_make() + + def _make_install(self): + with chdir(self, self.source_folder): + self._run_make(targets=["install_sw"], parallel=False, install=True) + + def build(self): + self._make() + self.run(f"{self._perl} {self.source_folder}/configdata.pm --dump") + + @property + def _make_program(self): + return "nmake" if self._use_nmake else "make" + + def _replace_runtime_in_file(self, filename): + runtime = msvc_runtime_flag(self) + for e in ["MDd", "MTd", "MD", "MT"]: + replace_in_file(self, filename, f"/{e} ", f"/{runtime} ", strict=False) + replace_in_file(self, filename, f"/{e}\"", f"/{runtime}\"", strict=False) + + def package(self): + copy(self, "*LICENSE*", src=self.source_folder, dst=os.path.join(self.package_folder, "licenses")) + self._make_install() + if is_apple_os(self): + fix_apple_shared_install_name(self) + + for root, _, files in os.walk(self.package_folder): + for filename in files: + if fnmatch.fnmatch(filename, "*.pdb"): + os.unlink(os.path.join(self.package_folder, root, filename)) + if self._use_nmake: + if self.settings.build_type == "Debug": + with chdir(self, os.path.join(self.package_folder, "lib")): + rename(self, "libssl.lib", "libssld.lib") + rename(self, "libcrypto.lib", "libcryptod.lib") + + if self.options.shared: + libdir = os.path.join(self.package_folder, "lib") + for file in os.listdir(libdir): + if self._is_mingw and file.endswith(".dll.a"): + continue + if file.endswith(".a"): + os.unlink(os.path.join(libdir, file)) + + if not self.options.no_fips: + provdir = os.path.join(self.source_folder, "providers") + modules_dir = os.path.join(self.package_folder, "lib", "ossl-modules") + if self.settings.os == "Macos": + copy(self, "fips.dylib", src=provdir, dst=modules_dir) + elif self.settings.os == "Windows": + copy(self, "fips.dll", src=provdir, dst=modules_dir) + else: + copy(self, "fips.so", src=provdir, dst=modules_dir) + + rmdir(self, os.path.join(self.package_folder, "lib", "pkgconfig")) + + self._create_cmake_module_variables( + os.path.join(self.package_folder, self._module_file_rel_path) + ) + + def _create_cmake_module_variables(self, module_file): + content = textwrap.dedent("""\ + set(OPENSSL_FOUND TRUE) + if(DEFINED OpenSSL_INCLUDE_DIR) + set(OPENSSL_INCLUDE_DIR ${OpenSSL_INCLUDE_DIR}) + endif() + if(DEFINED OpenSSL_Crypto_LIBS) + set(OPENSSL_CRYPTO_LIBRARY ${OpenSSL_Crypto_LIBS}) + set(OPENSSL_CRYPTO_LIBRARIES ${OpenSSL_Crypto_LIBS} + ${OpenSSL_Crypto_DEPENDENCIES} + ${OpenSSL_Crypto_FRAMEWORKS} + ${OpenSSL_Crypto_SYSTEM_LIBS}) + elseif(DEFINED openssl_OpenSSL_Crypto_LIBS_%(config)s) + set(OPENSSL_CRYPTO_LIBRARY ${openssl_OpenSSL_Crypto_LIBS_%(config)s}) + set(OPENSSL_CRYPTO_LIBRARIES ${openssl_OpenSSL_Crypto_LIBS_%(config)s} + ${openssl_OpenSSL_Crypto_DEPENDENCIES_%(config)s} + ${openssl_OpenSSL_Crypto_FRAMEWORKS_%(config)s} + ${openssl_OpenSSL_Crypto_SYSTEM_LIBS_%(config)s}) + endif() + if(DEFINED OpenSSL_SSL_LIBS) + set(OPENSSL_SSL_LIBRARY ${OpenSSL_SSL_LIBS}) + set(OPENSSL_SSL_LIBRARIES ${OpenSSL_SSL_LIBS} + ${OpenSSL_SSL_DEPENDENCIES} + ${OpenSSL_SSL_FRAMEWORKS} + ${OpenSSL_SSL_SYSTEM_LIBS}) + elseif(DEFINED openssl_OpenSSL_SSL_LIBS_%(config)s) + set(OPENSSL_SSL_LIBRARY ${openssl_OpenSSL_SSL_LIBS_%(config)s}) + set(OPENSSL_SSL_LIBRARIES ${openssl_OpenSSL_SSL_LIBS_%(config)s} + ${openssl_OpenSSL_SSL_DEPENDENCIES_%(config)s} + ${openssl_OpenSSL_SSL_FRAMEWORKS_%(config)s} + ${openssl_OpenSSL_SSL_SYSTEM_LIBS_%(config)s}) + endif() + if(DEFINED OpenSSL_LIBRARIES) + set(OPENSSL_LIBRARIES ${OpenSSL_LIBRARIES}) + endif() + if(DEFINED OpenSSL_VERSION) + set(OPENSSL_VERSION ${OpenSSL_VERSION}) + endif() + """% {"config":str(self.settings.build_type).upper()}) + save(self, module_file, content) + + @property + def _module_subfolder(self): + return os.path.join("lib", "cmake") + + @property + def _module_file_rel_path(self): + return os.path.join(self._module_subfolder, + "conan-official-{}-variables.cmake".format(self.name)) + + def package_info(self): + self.cpp_info.set_property("cmake_file_name", "OpenSSL") + self.cpp_info.set_property("cmake_find_mode", "both") + self.cpp_info.set_property("pkg_config_name", "openssl") + self.cpp_info.set_property("cmake_build_modules", [self._module_file_rel_path]) + self.cpp_info.names["cmake_find_package"] = "OpenSSL" + self.cpp_info.names["cmake_find_package_multi"] = "OpenSSL" + self.cpp_info.components["ssl"].builddirs.append(self._module_subfolder) + self.cpp_info.components["ssl"].build_modules["cmake_find_package"] = [self._module_file_rel_path] + self.cpp_info.components["ssl"].set_property("cmake_build_modules", [self._module_file_rel_path]) + self.cpp_info.components["crypto"].builddirs.append(self._module_subfolder) + self.cpp_info.components["crypto"].build_modules["cmake_find_package"] = [self._module_file_rel_path] + self.cpp_info.components["crypto"].set_property("cmake_build_modules", [self._module_file_rel_path]) + + if self._use_nmake: + libsuffix = "d" if self.settings.build_type == "Debug" else "" + self.cpp_info.components["ssl"].libs = ["libssl" + libsuffix] + self.cpp_info.components["crypto"].libs = ["libcrypto" + libsuffix] + else: + self.cpp_info.components["ssl"].libs = ["ssl"] + self.cpp_info.components["crypto"].libs = ["crypto"] + + self.cpp_info.components["ssl"].requires = ["crypto"] + + if not self.options.no_zlib: + self.cpp_info.components["crypto"].requires.append("zlib::zlib") + + if self.settings.os == "Windows": + self.cpp_info.components["crypto"].system_libs.extend(["crypt32", "ws2_32", "advapi32", "user32", "bcrypt"]) + elif self.settings.os == "Linux": + self.cpp_info.components["crypto"].system_libs.extend(["dl", "rt"]) + self.cpp_info.components["ssl"].system_libs.append("dl") + if not self.options.no_threads: + self.cpp_info.components["crypto"].system_libs.append("pthread") + self.cpp_info.components["ssl"].system_libs.append("pthread") + elif self.settings.os == "Neutrino": + self.cpp_info.components["crypto"].system_libs.append("atomic") + self.cpp_info.components["ssl"].system_libs.append("atomic") + + self.cpp_info.components["crypto"].set_property("cmake_target_name", "OpenSSL::Crypto") + self.cpp_info.components["crypto"].set_property("pkg_config_name", "libcrypto") + self.cpp_info.components["ssl"].set_property("cmake_target_name", "OpenSSL::SSL") + self.cpp_info.components["ssl"].set_property("pkg_config_name", "libssl") + self.cpp_info.components["crypto"].names["cmake_find_package"] = "Crypto" + self.cpp_info.components["crypto"].names["cmake_find_package_multi"] = "Crypto" + self.cpp_info.components["ssl"].names["cmake_find_package"] = "SSL" + self.cpp_info.components["ssl"].names["cmake_find_package_multi"] = "SSL" + + openssl_modules_dir = os.path.join(self.package_folder, "lib", "ossl-modules") + self.runenv_info.define_path("OPENSSL_MODULES", openssl_modules_dir) + + # For legacy 1.x downstream consumers, remove once recipe is 2.0 only: + self.env_info.OPENSSL_MODULES = openssl_modules_dir + diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir_v2/p/zlib41bd3946e7341/e/conanfile.py b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir_v2/p/zlib41bd3946e7341/e/conanfile.py new file mode 100644 index 000000000000..ead39ff73661 --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/testdata/cacheDir_v2/p/zlib41bd3946e7341/e/conanfile.py @@ -0,0 +1,110 @@ +from conan import ConanFile +from conan.tools.cmake import CMake, CMakeToolchain, cmake_layout +from conan.tools.files import apply_conandata_patches, export_conandata_patches, get, load, replace_in_file, save +from conan.tools.scm import Version +import os + +required_conan_version = ">=1.53.0" + + +class ZlibConan(ConanFile): + license = "Zlib" + name = "zlib" + package_type = "library" + url = "https://github.com/conan-io/conan-center-index" + homepage = "https://zlib.net" + description = ("A Massively Spiffy Yet Delicately Unobtrusive Compression Library " + "(Also Free, Not to Mention Unencumbered by Patents)") + topics = ("zlib", "compression") + + settings = "os", "arch", "compiler", "build_type" + options = { + "shared": [True, False], + "fPIC": [True, False], + } + default_options = { + "shared": False, + "fPIC": True, + } + + @property + def _is_mingw(self): + return self.settings.os == "Windows" and self.settings.compiler == "gcc" + + def export_sources(self): + export_conandata_patches(self) + + def config_options(self): + if self.settings.os == "Windows": + del self.options.fPIC + + def configure(self): + if self.options.shared: + self.options.rm_safe("fPIC") + self.settings.rm_safe("compiler.libcxx") + self.settings.rm_safe("compiler.cppstd") + + def layout(self): + cmake_layout(self, src_folder="src") + + def source(self): + get(self, **self.conan_data["sources"][self.version], + destination=self.source_folder, strip_root=True) + + def generate(self): + tc = CMakeToolchain(self) + tc.variables["SKIP_INSTALL_ALL"] = False + tc.variables["SKIP_INSTALL_LIBRARIES"] = False + tc.variables["SKIP_INSTALL_HEADERS"] = False + tc.variables["SKIP_INSTALL_FILES"] = True + # Correct for misuse of "${CMAKE_INSTALL_PREFIX}/" in CMakeLists.txt + tc.variables["INSTALL_LIB_DIR"] = "lib" + tc.variables["INSTALL_INC_DIR"] = "include" + tc.variables["ZLIB_BUILD_EXAMPLES"] = False + tc.generate() + + def _patch_sources(self): + apply_conandata_patches(self) + + is_apple_clang12 = self.settings.compiler == "apple-clang" and Version(self.settings.compiler.version) >= "12.0" + if not is_apple_clang12: + for filename in ['zconf.h', 'zconf.h.cmakein', 'zconf.h.in']: + filepath = os.path.join(self.source_folder, filename) + replace_in_file(self, filepath, + '#ifdef HAVE_UNISTD_H ' + '/* may be set to #if 1 by ./configure */', + '#if defined(HAVE_UNISTD_H) && (1-HAVE_UNISTD_H-1 != 0)') + replace_in_file(self, filepath, + '#ifdef HAVE_STDARG_H ' + '/* may be set to #if 1 by ./configure */', + '#if defined(HAVE_STDARG_H) && (1-HAVE_STDARG_H-1 != 0)') + + def build(self): + self._patch_sources() + cmake = CMake(self) + cmake.configure() + cmake.build() + + def _extract_license(self): + tmp = load(self, os.path.join(self.source_folder, "zlib.h")) + license_contents = tmp[2:tmp.find("*/", 1)] + return license_contents + + def package(self): + save(self, os.path.join(self.package_folder, "licenses", "LICENSE"), self._extract_license()) + cmake = CMake(self) + cmake.install() + + def package_info(self): + self.cpp_info.set_property("cmake_find_mode", "both") + self.cpp_info.set_property("cmake_file_name", "ZLIB") + self.cpp_info.set_property("cmake_target_name", "ZLIB::ZLIB") + self.cpp_info.set_property("pkg_config_name", "zlib") + if self.settings.os == "Windows" and not self._is_mingw: + libname = "zdll" if self.options.shared else "zlib" + else: + libname = "z" + self.cpp_info.libs = [libname] + + self.cpp_info.names["cmake_find_package"] = "ZLIB" + self.cpp_info.names["cmake_find_package_multi"] = "ZLIB" diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/empty/conan.lock b/pkg/fanal/analyzer/language/c/conan/testdata/empty/conan.lock new file mode 100644 index 000000000000..106ca0aa06b2 --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/testdata/empty/conan.lock @@ -0,0 +1,14 @@ +{ + "graph_lock": { + "nodes": { + "0": { + "options": "o", + "path": "conanfile.txt", + "context": "host" + } + }, + "revisions_enabled": false + }, + "version": "0.4", + "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=9\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/happy/conan.lock b/pkg/fanal/analyzer/language/c/conan/testdata/happy/conan.lock new file mode 100644 index 000000000000..eb17995310e1 --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/testdata/happy/conan.lock @@ -0,0 +1,34 @@ +{ + "graph_lock": { + "nodes": { + "0": { + "options": "o", + "requires": [ + "1" + ], + "path": "conanfile.txt", + "context": "host" + }, + "1": { + "ref": "openssl/3.0.5", + "options": "", + "requires": [ + "2" + ], + "package_id": "", + "prev": "0", + "context": "host" + }, + "2": { + "ref": "zlib/1.2.12", + "options": "", + "package_id": "", + "prev": "0", + "context": "host" + } + }, + "revisions_enabled": false + }, + "version": "0.4", + "profile_host": "[settings]\narch=x86_64\narch_build=x86_64\nbuild_type=Release\ncompiler=gcc\ncompiler.libcxx=libstdc++\ncompiler.version=9\nos=Linux\nos_build=Linux\n[options]\n[build_requires]\n[env]\n" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/c/conan/testdata/happy_v2/release.lock b/pkg/fanal/analyzer/language/c/conan/testdata/happy_v2/release.lock new file mode 100644 index 000000000000..c05aa48b0e3e --- /dev/null +++ b/pkg/fanal/analyzer/language/c/conan/testdata/happy_v2/release.lock @@ -0,0 +1,10 @@ +{ + "version": "0.5", + "requires": [ + "zlib/1.3.1#f52e03ae3d251dec704634230cd806a2%1708593606.497", + "openssl/3.2.2#899583c694f9deccec74dbe0bbc65a15%1717540517.968" + ], + "build_requires": [], + "python_requires": [], + "config_requires": [] +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/conda/environment/environment.go b/pkg/fanal/analyzer/language/conda/environment/environment.go new file mode 100644 index 000000000000..e00e8eb36d4f --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/environment.go @@ -0,0 +1,126 @@ +package environment + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/bmatcuk/doublestar/v4" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/conda/environment" + "github.com/aquasecurity/trivy/pkg/dependency/parser/conda/meta" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/version/doc" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +func init() { + analyzer.RegisterAnalyzer(&environmentAnalyzer{}) +} + +const version = 2 + +type parser struct{} + +func (*parser) Parse(ctx context.Context, r xio.ReadSeekerAt) ([]types.Package, []types.Dependency, error) { + p := environment.NewParser() + pkgs, err := p.Parse(r) + if err != nil { + return nil, nil, err + } + + once := sync.Once{} + for i, pkg := range pkgs.Packages { + // Skip packages without a version, because in this case we will not be able to get the correct file name. + if pkg.Version != "" { + licenses, err := findLicenseFromEnvDir(ctx, pkg, pkgs.Prefix) + if err != nil { + // Show log once per file + once.Do(func() { + // e.g. https://trivy.dev/docs/latest/coverage/os/conda/#license_1 + log.WithPrefix("conda").Debug(fmt.Sprintf("License not found. See %s for details.", doc.URL("guide/coverage/os/conda/", "license_1")), + log.String("pkg", pkg.Name), log.Err(err)) + }) + } + pkg.Licenses = licenses + } + pkgs.Packages[i] = pkg + } + + return pkgs.Packages, nil, nil +} + +type environmentAnalyzer struct{} + +func (a environmentAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + res, err := language.Analyze(ctx, types.CondaEnv, input.FilePath, input.Content, &parser{}) + if err != nil { + return nil, xerrors.Errorf("unable to parse environment.yaml: %w", err) + } + + if res == nil { + return nil, nil + } + return res, nil +} + +func findLicenseFromEnvDir(ctx context.Context, pkg types.Package, prefix string) ([]string, error) { + if prefix == "" { + return nil, xerrors.Errorf("`prefix` field doesn't exist") + } + condaMetaDir := filepath.Join(prefix, "conda-meta") + entries, err := os.ReadDir(condaMetaDir) + if err != nil { + return nil, xerrors.Errorf("unable to read conda-meta dir: %w", err) + } + for _, entry := range entries { + if entry.IsDir() { + continue + } + + pattern := fmt.Sprintf("%s-%s-*.json", pkg.Name, pkg.Version) + matched, err := doublestar.Match(pattern, entry.Name()) + if err != nil { + return nil, xerrors.Errorf("incorrect packageJSON file pattern: %w", err) + } + if matched { + return licenseFromPackageJson(ctx, condaMetaDir, entry.Name()) + } + } + return nil, xerrors.Errorf("meta file didn't find") +} + +func (a environmentAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Base(filePath) == types.CondaEnvYml || filepath.Base(filePath) == types.CondaEnvYaml +} + +func (a environmentAnalyzer) Type() analyzer.Type { + return analyzer.TypeCondaEnv +} + +func (a environmentAnalyzer) Version() int { + return version +} + +func licenseFromPackageJson(ctx context.Context, condaMetaDir, fileName string) ([]string, error) { + file, err := os.Open(filepath.Join(condaMetaDir, fileName)) + if err != nil { + return nil, xerrors.Errorf("unable to open packageJSON file: %w", err) + } + + defer file.Close() + + packageJson, _, err := meta.NewParser().Parse(ctx, file) + if err != nil { + return nil, xerrors.Errorf("unable to parse packageJSON file: %w", err) + } + // packageJson always contain only 1 element + // cf. https://github.com/aquasecurity/trivy/blob/c3192f061d7e84eaf38df8df7c879dc00b4ca137/pkg/dependency/parser/conda/meta/parse.go#L39-L45 + return packageJson[0].Licenses, nil +} diff --git a/pkg/fanal/analyzer/language/conda/environment/environment_test.go b/pkg/fanal/analyzer/language/conda/environment/environment_test.go new file mode 100644 index 000000000000..480f7d5a253f --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/environment_test.go @@ -0,0 +1,195 @@ +package environment + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_environmentAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/environment.yaml", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.CondaEnv, + FilePath: "testdata/environment.yaml", + Packages: types.Packages{ + { + Name: "_libgcc_mutex", + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + Name: "_openmp_mutex", + Version: "5.1", + Locations: []types.Location{ + { + StartLine: 6, + EndLine: 6, + }, + }, + }, + { + Name: "blas", + Version: "1.0", + Locations: []types.Location{ + { + StartLine: 7, + EndLine: 7, + }, + }, + }, + { + Name: "bzip2", + Version: "1.0.8", + Locations: []types.Location{ + { + StartLine: 8, + EndLine: 8, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with licenses", + inputFile: "testdata/environment-with-licenses.yaml", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.CondaEnv, + FilePath: "testdata/environment-with-licenses.yaml", + Packages: types.Packages{ + { + Name: "_libgcc_mutex", + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + { + Name: "_openmp_mutex", + Version: "5.1", + Locations: []types.Location{ + { + StartLine: 6, + EndLine: 6, + }, + }, + Licenses: []string{ + "BSD-3-Clause", + }, + }, + { + Name: "blas", + Version: "1.0", + Locations: []types.Location{ + { + StartLine: 7, + EndLine: 7, + }, + }, + }, + { + Name: "bzip2", + Version: "1.0.8", + Locations: []types.Location{ + { + StartLine: 8, + EndLine: 8, + }, + }, + Licenses: []string{ + "bzip2-1.0.8", + }, + }, + }, + }, + }, + }, + }, + { + name: "empty", + inputFile: "testdata/empty.yaml", + }, + { + name: "invalid", + inputFile: "testdata/invalid.yaml", + wantErr: "unable to parse environment.yaml", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := environmentAnalyzer{} + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_environmentAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path `yaml`", + filePath: "foo/environment.yaml", + want: true, + }, + { + name: "happy path `yml`", + filePath: "bar/environment.yaml", + want: true, + }, + { + name: "sad path `json` ", + filePath: "environment.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := environmentAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/_libgcc_mutex-0.1-main.json b/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/_libgcc_mutex-0.1-main.json new file mode 100644 index 000000000000..bfc2daa841d5 --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/_libgcc_mutex-0.1-main.json @@ -0,0 +1,5 @@ +{ + "license": "MIT", + "name": "_libgcc_mutex", + "version": "0.1" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/_openmp_mutex-5.1-1_gnu.json b/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/_openmp_mutex-5.1-1_gnu.json new file mode 100644 index 000000000000..a61f193ee669 --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/_openmp_mutex-5.1-1_gnu.json @@ -0,0 +1,5 @@ +{ + "license": "BSD-3-Clause", + "name": "_openmp_mutex", + "version": "5.1" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/blas-1.0-openblas.json b/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/blas-1.0-openblas.json new file mode 100644 index 000000000000..9a14025cd5b2 --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/blas-1.0-openblas.json @@ -0,0 +1,5 @@ +{ + "license": "", + "name": "blas", + "version": "1.0" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/bzip2-1.0.8-h5eee18b_6.json b/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/bzip2-1.0.8-h5eee18b_6.json new file mode 100644 index 000000000000..413243843b73 --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/testdata/conda-meta/bzip2-1.0.8-h5eee18b_6.json @@ -0,0 +1,5 @@ +{ + "license": "bzip2-1.0.8", + "name": "bzip2", + "version": "1.0.8" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/conda/environment/testdata/empty.yaml b/pkg/fanal/analyzer/language/conda/environment/testdata/empty.yaml new file mode 100644 index 000000000000..f9622b8a1030 --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/testdata/empty.yaml @@ -0,0 +1,5 @@ +name: test-env +channels: + - defaults +dependencies: +prefix: /opt/conda/envs/test-env diff --git a/pkg/fanal/analyzer/language/conda/environment/testdata/environment-with-licenses.yaml b/pkg/fanal/analyzer/language/conda/environment/testdata/environment-with-licenses.yaml new file mode 100644 index 000000000000..a6db3a8a9e7c --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/testdata/environment-with-licenses.yaml @@ -0,0 +1,9 @@ +name: test-env +channels: + - defaults +dependencies: + - _libgcc_mutex + - _openmp_mutex=5.1 + - blas=1.0=openblas + - bzip2=1.0.8=h998d150_5 +prefix: testdata diff --git a/pkg/fanal/analyzer/language/conda/environment/testdata/environment.yaml b/pkg/fanal/analyzer/language/conda/environment/testdata/environment.yaml new file mode 100644 index 000000000000..62cd7ff599bd --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/testdata/environment.yaml @@ -0,0 +1,9 @@ +name: test-env +channels: + - defaults +dependencies: + - _libgcc_mutex + - _openmp_mutex=5.1 + - blas=1.0=openblas + - bzip2=1.0.8=h998d150_5 +prefix: /opt/conda/envs/test-env diff --git a/pkg/fanal/analyzer/language/conda/environment/testdata/invalid.yaml b/pkg/fanal/analyzer/language/conda/environment/testdata/invalid.yaml new file mode 100644 index 000000000000..9977a2836c1a --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/environment/testdata/invalid.yaml @@ -0,0 +1 @@ +invalid diff --git a/pkg/fanal/analyzer/language/conda/meta/meta.go b/pkg/fanal/analyzer/language/conda/meta/meta.go new file mode 100644 index 000000000000..243b6394d153 --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/meta/meta.go @@ -0,0 +1,39 @@ +package meta + +import ( + "context" + "os" + "path/filepath" + "regexp" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/conda/meta" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&metaAnalyzer{}) +} + +const version = 1 + +var fileRegex = regexp.MustCompile(`.*/envs/.+/conda-meta/.+-.+-.+\.json`) + +type metaAnalyzer struct{} + +func (a metaAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + p := meta.NewParser() + return language.AnalyzePackage(ctx, types.CondaPkg, input.FilePath, input.Content, p, input.Options.FileChecksum) +} +func (a metaAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return fileRegex.MatchString(filepath.ToSlash(filePath)) +} + +func (a metaAnalyzer) Type() analyzer.Type { + return analyzer.TypeCondaPkg +} + +func (a metaAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/conda/meta/meta_test.go b/pkg/fanal/analyzer/language/conda/meta/meta_test.go new file mode 100644 index 000000000000..7efb482b46ef --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/meta/meta_test.go @@ -0,0 +1,100 @@ +package meta + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_packagingAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "pip", + inputFile: "testdata/pip-22.2.2-py38h06a4308_0.json", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.CondaPkg, + FilePath: "testdata/pip-22.2.2-py38h06a4308_0.json", + Packages: types.Packages{ + { + Name: "pip", + Version: "22.2.2", + Licenses: []string{"MIT"}, + FilePath: "testdata/pip-22.2.2-py38h06a4308_0.json", + }, + }, + }, + }, + }, + }, + { + name: "invalid", + inputFile: "testdata/invalid.json", + wantErr: "unable to parse conda package", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + stat, err := f.Stat() + require.NoError(t, err) + + a := metaAnalyzer{} + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Info: stat, + Content: f, + }) + + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } + +} + +func Test_packagingAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "pip", + filePath: "/home//miniconda3/envs//conda-meta/pip-22.2.2-py38h06a4308_0.json", + want: true, + }, + { + name: "invalid", + filePath: "/home//miniconda3/envs//conda-meta/invalid.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := metaAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/conda/meta/testdata/invalid.json b/pkg/fanal/analyzer/language/conda/meta/testdata/invalid.json new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/meta/testdata/invalid.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/conda/meta/testdata/pip-22.2.2-py38h06a4308_0.json b/pkg/fanal/analyzer/language/conda/meta/testdata/pip-22.2.2-py38h06a4308_0.json new file mode 100644 index 000000000000..4788d70e21aa --- /dev/null +++ b/pkg/fanal/analyzer/language/conda/meta/testdata/pip-22.2.2-py38h06a4308_0.json @@ -0,0 +1,62 @@ +{ + "build": "py38h06a4308_0", + "build_number": 0, + "channel": "https://repo.anaconda.com/pkgs/main/linux-64", + "constrains": [], + "depends": [ + "python >=3.8,<3.9.0a0", + "setuptools", + "wheel" + ], + "extracted_package_dir": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0", + "features": "", + "files": [ + "bin/pip", + "", + "lib/python3.8/site-packages/pip/py.typed" + ], + "fn": "pip-22.2.2-py38h06a4308_0.conda", + "legacy_bz2_md5": "2ac9f1cfec65a1e4ef00cc0132ecd753", + "legacy_bz2_size": 2849993, + "license": "MIT", + "license_family": "MIT", + "link": { + "source": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0", + "type": 1 + }, + "md5": "ed3e0331e7c614b3148c9911e1fc15e3", + "name": "pip", + "package_tarball_full_path": "/home/user/miniconda3/pkgs/pip-22.2.2-py38h06a4308_0.conda", + "paths_data": { + "paths": [ + { + "_path": "bin/pip", + "file_mode": "text", + "path_type": "hardlink", + "prefix_placeholder": "/opt/conda/conda-bld/pip_1664552683240/_h_env_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold_placehold", + "sha256": "1110c03ca2fb86e43e8b52a61cd9d7c722b2817fc893a613e1a947f5d7049008", + "sha256_in_prefix": "14ed5ba79d096035b83a469e863acbbaadd3ea6ca5f23f79eefa91fa857d3f19", + "size_in_bytes": 475 + }, + { + "_path": "" + }, + { + "_path": "lib/python3.8/site-packages/pip/py.typed", + "path_type": "hardlink", + "sha256": "10156fbcf4539ff788a73e5ee50ced48276b317ed0c1ded53fddd14a82256762", + "sha256_in_prefix": "10156fbcf4539ff788a73e5ee50ced48276b317ed0c1ded53fddd14a82256762", + "size_in_bytes": 286 + } + ], + "paths_version": 1 + }, + "requested_spec": "None", + "sha256": "3fb76b94cfa5ea9732bfb241b3d234ec0a5a48d16755c3c1ef3c94630f91eb26", + "size": 2417732, + "subdir": "linux-64", + "timestamp": 1664552878795, + "track_features": "", + "url": "https://repo.anaconda.com/pkgs/main/linux-64/pip-22.2.2-py38h06a4308_0.conda", + "version": "22.2.2" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dart/pub/pubspec.go b/pkg/fanal/analyzer/language/dart/pub/pubspec.go new file mode 100644 index 000000000000..d67ee62f2532 --- /dev/null +++ b/pkg/fanal/analyzer/language/dart/pub/pubspec.go @@ -0,0 +1,184 @@ +package pub + +import ( + "context" + "io" + "io/fs" + "os" + "path/filepath" + "runtime" + "sort" + + "github.com/samber/lo" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/dart/pub" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypePubSpecLock, newPubSpecLockAnalyzer) +} + +const ( + version = 2 + pubSpecYamlFileName = "pubspec.yaml" +) + +// pubSpecLockAnalyzer analyzes `pubspec.lock` +type pubSpecLockAnalyzer struct { + logger *log.Logger + parser language.Parser +} + +func newPubSpecLockAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return pubSpecLockAnalyzer{ + logger: log.WithPrefix("pub"), + parser: pub.NewParser(opts.DetectionPriority == types.PriorityComprehensive), + }, nil +} + +func (a pubSpecLockAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + + // get all DependsOn from cache dir + // lib ID -> DependsOn names + allDependsOn, err := a.findDependsOn() + if err != nil { + a.logger.Warn("Unable to parse cache dir", log.Err(err)) + } + + required := func(_ string, _ fs.DirEntry) bool { + // Parse all required files: `pubspec.lock` (from a.Required func) + input.FilePatterns.Match() + return true + } + + err = fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error { + app, err := language.Parse(ctx, types.Pub, path, r, a.parser) + if err != nil { + return xerrors.Errorf("unable to parse %q: %w", path, err) + } + + if app == nil { + return nil + } + + if allDependsOn != nil { + // Required to search for library versions for DependsOn. + pkgs := lo.SliceToMap(app.Packages, func(lib types.Package) (string, string) { + return lib.Name, lib.ID + }) + + for i, lib := range app.Packages { + var dependsOn []string + for _, depName := range allDependsOn[lib.ID] { + if depID, ok := pkgs[depName]; ok { + dependsOn = append(dependsOn, depID) + } + } + app.Packages[i].DependsOn = dependsOn + } + } + + sort.Sort(app.Packages) + apps = append(apps, *app) + return nil + }) + if err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a pubSpecLockAnalyzer) findDependsOn() (map[string][]string, error) { + dir := cacheDir() + if !fsutils.DirExists(dir) { + a.logger.Debug("Cache dir not found. Need 'dart pub get' to fill dependency relationships", + log.String("dir", dir)) + return nil, nil + } + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == pubSpecYamlFileName + } + + deps := make(map[string][]string) + if err := fsutils.WalkDir(os.DirFS(dir), ".", required, func(path string, _ fs.DirEntry, r io.Reader) error { + id, dependsOn, err := parsePubSpecYaml(r) + if err != nil { + a.logger.Debug("Unable to parse pubspec.yaml", log.FilePath(path), log.Err(err)) + return nil + } + if id != "" { + deps[id] = dependsOn + } + return nil + + }); err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + return deps, nil +} + +// https://dart.dev/tools/pub/glossary#system-cache +func cacheDir() string { + if dir := os.Getenv("PUB_CACHE"); dir != "" { + return dir + } + + // `%LOCALAPPDATA%\Pub\Cache` for Windows + if runtime.GOOS == "windows" { + return filepath.Join(os.Getenv("LOCALAPPDATA"), "Pub", "Cache") + } + + // `~/.pub-cache` for Linux or Mac + return filepath.Join(os.Getenv("HOME"), ".pub_cache") +} + +type pubSpecYaml struct { + Name string `yaml:"name"` + Version string `yaml:"version,omitempty"` + Dependencies map[string]any `yaml:"dependencies,omitempty"` +} + +func parsePubSpecYaml(r io.Reader) (string, []string, error) { + var spec pubSpecYaml + if err := yaml.NewDecoder(r).Decode(&spec); err != nil { + return "", nil, xerrors.Errorf("unable to decode: %w", err) + } + + // Version is a required field only for packages from pub.dev: + // https://dart.dev/tools/pub/pubspec#version + // We can skip packages without version, + // because we compare packages by ID (name+version) + if spec.Version == "" || len(spec.Dependencies) == 0 { + return "", nil, nil + } + + // pubspec.yaml uses version ranges + // save only dependencies names + dependsOn := lo.Keys(spec.Dependencies) + + return dependency.ID(types.Pub, spec.Name, spec.Version), dependsOn, nil +} + +func (a pubSpecLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Base(filePath) == types.PubSpecLock +} + +func (a pubSpecLockAnalyzer) Type() analyzer.Type { + return analyzer.TypePubSpecLock +} + +func (a pubSpecLockAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/dart/pub/pubspec_test.go b/pkg/fanal/analyzer/language/dart/pub/pubspec_test.go new file mode 100644 index 000000000000..4d6105cc5569 --- /dev/null +++ b/pkg/fanal/analyzer/language/dart/pub/pubspec_test.go @@ -0,0 +1,221 @@ +package pub + +import ( + "os" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_pubSpecLockAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + pubCacheEnv string + want *analyzer.AnalysisResult + wantErr assert.ErrorAssertionFunc + }{ + { + // Supports only absolute paths for `rootUri` in package_config.json + // But for this test this field was changed + name: "happy path with cache", + dir: "testdata/happy", + pubCacheEnv: "testdata/happy/cache", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pub, + FilePath: "pubspec.lock", + Packages: types.Packages{ + { + ID: "crypto@3.0.3", + Name: "crypto", + Version: "3.0.3", + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "typed_data@1.3.2", + }, + }, + { + ID: "meta@1.11.0", + Name: "meta", + Version: "1.11.0", + Relationship: types.RelationshipDirect, + }, + { + ID: "collection@1.17.0", + Name: "collection", + Version: "1.17.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "typed_data@1.3.2", + Name: "typed_data", + Version: "1.3.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "collection@1.17.0", + }, + }, + }, + }, + }, + }, + wantErr: assert.NoError, + }, + { + name: "happy path without cache", + dir: "testdata/happy", + pubCacheEnv: "testdata/happy/empty", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pub, + FilePath: "pubspec.lock", + Packages: types.Packages{ + { + ID: "crypto@3.0.3", + Name: "crypto", + Version: "3.0.3", + Relationship: types.RelationshipDirect, + }, + { + ID: "meta@1.11.0", + Name: "meta", + Version: "1.11.0", + Relationship: types.RelationshipDirect, + }, + { + ID: "collection@1.17.0", + Name: "collection", + Version: "1.17.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "typed_data@1.3.2", + Name: "typed_data", + Version: "1.3.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + }, + }, + }, + }, + wantErr: assert.NoError, + }, + { + name: "empty file", + dir: "testdata/empty", + want: &analyzer.AnalysisResult{}, + wantErr: assert.NoError, + }, + { + name: "broken file", + dir: "testdata/broken", + want: &analyzer.AnalysisResult{}, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("PUB_CACHE", tt.pubCacheEnv) + a, err := newPubSpecLockAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_pubSpecLockAnalyzer_cacheDir(t *testing.T) { + tests := []struct { + name string + pubCacheEnv string + localAppDataEnv string + windowsTest bool + wantDir string + }{ + { + name: "default cache dir for Linux/MacOS", + wantDir: "/root/.pub_cache", + }, + { + name: "default cache dir Windows", + windowsTest: true, + wantDir: "C:\\Users\\User\\AppData\\Local\\Pub\\Cache", + }, + { + name: "PUB_CACHE is used", + pubCacheEnv: "/root/cache", + wantDir: "/root/cache", + }, + { + name: "PUB_CACHE is used in Windows", + pubCacheEnv: "C:\\Cache", + windowsTest: true, + wantDir: "C:\\Cache", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if runtime.GOOS == "windows" { + if !tt.windowsTest { + t.Skipf("This test is not used for %s", runtime.GOOS) + } + t.Setenv("LOCALAPPDATA", "C:\\Users\\User\\AppData\\Local") + } else { + if tt.windowsTest { + t.Skipf("This test is not used for %s", runtime.GOOS) + } + t.Setenv("HOME", "/root") + } + + t.Setenv("PUB_CACHE", tt.pubCacheEnv) + + dir := cacheDir() + assert.Equal(t, tt.wantDir, dir) + }) + } +} + +func Test_pubSpecLockAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path", + filePath: "pubspec.lock", + want: true, + }, + { + name: "sad path", + filePath: "test.txt", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := pubSpecLockAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/dart/pub/testdata/broken/pubspec.lock b/pkg/fanal/analyzer/language/dart/pub/testdata/broken/pubspec.lock new file mode 100644 index 000000000000..66c0ff41b8d1 --- /dev/null +++ b/pkg/fanal/analyzer/language/dart/pub/testdata/broken/pubspec.lock @@ -0,0 +1 @@ +it is broken \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dart/pub/testdata/empty/pubspec.lock b/pkg/fanal/analyzer/language/dart/pub/testdata/empty/pubspec.lock new file mode 100644 index 000000000000..eb4cea8c2089 --- /dev/null +++ b/pkg/fanal/analyzer/language/dart/pub/testdata/empty/pubspec.lock @@ -0,0 +1,6 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.3.0" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/collection-1.18.0/pubspec.yaml b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/collection-1.18.0/pubspec.yaml new file mode 100644 index 000000000000..2e54e00344a6 --- /dev/null +++ b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/collection-1.18.0/pubspec.yaml @@ -0,0 +1,16 @@ +name: collection +version: 1.18.0 +description: >- + Collections and utilities functions and classes related to collections. +repository: https://github.com/dart-lang/collection + +topics: + - data-structures + - collections + +environment: + sdk: ">=2.18.0 <4.0.0" + +dev_dependencies: + lints: ^2.0.1 + test: ^1.16.0 \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/crypto-3.0.3/pubspec.yaml b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/crypto-3.0.3/pubspec.yaml new file mode 100644 index 000000000000..29cc438636a8 --- /dev/null +++ b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/crypto-3.0.3/pubspec.yaml @@ -0,0 +1,17 @@ +name: crypto +version: 3.0.3 +description: Implementations of SHA, MD5, and HMAC cryptographic functions. +repository: https://github.com/dart-lang/crypto +topics: + - crypto + +environment: + sdk: '>=2.19.0 <3.0.0' + +dependencies: + typed_data: ^1.3.0 + +dev_dependencies: + convert: ^3.0.0 + dart_flutter_team_lints: ^1.0.0 + test: ^1.16.0 \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/meta-1.11.0/pubspec.yaml b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/meta-1.11.0/pubspec.yaml new file mode 100644 index 000000000000..d4c6f0f86340 --- /dev/null +++ b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/meta-1.11.0/pubspec.yaml @@ -0,0 +1,17 @@ +name: meta +# Note, because version `2.0.0` was mistakenly released, the next major version must be `3.x.y`. +version: 1.11.0 +description: >- + Annotations used to express developer intentions that can't otherwise be + deduced by statically analyzing source code. +repository: https://github.com/dart-lang/sdk/tree/main/pkg/meta + +environment: + sdk: ">=2.12.0 <4.0.0" + +# We use 'any' version constraints here as we get our package versions from +# the dart-lang/sdk repo's DEPS file. Note that this is a special case; the +# best practice for packages is to specify their compatible version ranges. +# See also https://dart.dev/tools/pub/dependencies. +dev_dependencies: + lints: any diff --git a/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/typed_data-1.3.2/pubspec.yaml b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/typed_data-1.3.2/pubspec.yaml new file mode 100644 index 000000000000..642bf644115c --- /dev/null +++ b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/cache/hosted/pub.dev/typed_data-1.3.2/pubspec.yaml @@ -0,0 +1,18 @@ +name: typed_data +version: 1.3.2 +description: >- + Utility functions and classes related to the dart:typed_data library. +repository: https://github.com/dart-lang/typed_data + +topics: + - data-structures + +environment: + sdk: '>=2.17.0 <4.0.0' + +dependencies: + collection: ^1.15.0 + +dev_dependencies: + dart_flutter_team_lints: ^0.1.0 + test: ^1.16.0 \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dart/pub/testdata/happy/pubspec.lock b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/pubspec.lock new file mode 100644 index 000000000000..d3bd9670042c --- /dev/null +++ b/pkg/fanal/analyzer/language/dart/pub/testdata/happy/pubspec.lock @@ -0,0 +1,37 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + collection: + dependency: transitive + description: + name: collection + sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + url: "https://pub.dev" + source: hosted + version: "1.17.0" + crypto: + dependency: "direct main" + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" + meta: + dependency: "direct dev" + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" +sdks: + dart: ">=3.1.0 <4.0.0" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dotnet/deps/deps.go b/pkg/fanal/analyzer/language/dotnet/deps/deps.go new file mode 100644 index 000000000000..4cccd3268641 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/deps/deps.go @@ -0,0 +1,47 @@ +package deps + +import ( + "context" + "os" + "strings" + + "golang.org/x/xerrors" + + core "github.com/aquasecurity/trivy/pkg/dependency/parser/dotnet/core_deps" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&depsLibraryAnalyzer{}) +} + +const ( + version = 1 + depsExtension = ".deps.json" +) + +type depsLibraryAnalyzer struct{} + +func (a depsLibraryAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + parser := core.NewParser() + res, err := language.Analyze(ctx, types.DotNetCore, input.FilePath, input.Content, parser) + if err != nil { + return nil, xerrors.Errorf(".Net Core dependencies analysis error: %w", err) + } + + return res, nil +} + +func (a depsLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return strings.HasSuffix(filePath, depsExtension) +} + +func (a depsLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeDotNetCore +} + +func (a depsLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/dotnet/deps/deps_test.go b/pkg/fanal/analyzer/language/dotnet/deps/deps_test.go new file mode 100644 index 000000000000..014488ada3e7 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/deps/deps_test.go @@ -0,0 +1,112 @@ +package deps + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_depsLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/datacollector.deps.json", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.DotNetCore, + FilePath: "testdata/datacollector.deps.json", + Packages: types.Packages{ + { + ID: "Microsoft.VisualStudio.TestPlatform.Common/17.2.0-release-20220408-11", + Name: "Microsoft.VisualStudio.TestPlatform.Common", + Version: "17.2.0-release-20220408-11", + Relationship: types.RelationshipRoot, + Locations: []types.Location{ + { + StartLine: 15, + EndLine: 19, + }, + }, + }, + { + ID: "Newtonsoft.Json/9.0.1", + Name: "Newtonsoft.Json", + Version: "9.0.1", + Locations: []types.Location{ + { + StartLine: 8, + EndLine: 14, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "sad path", + inputFile: "testdata/invalid.txt", + wantErr: ".Net Core dependencies analysis error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := depsLibraryAnalyzer{} + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_depsLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "config", + filePath: "test/datacollector.deps.json", + want: true, + }, + { + name: "zip", + filePath: "test.zip", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := depsLibraryAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/dotnet/deps/testdata/datacollector.deps.json b/pkg/fanal/analyzer/language/dotnet/deps/testdata/datacollector.deps.json new file mode 100644 index 000000000000..08699fee4a7a --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/deps/testdata/datacollector.deps.json @@ -0,0 +1,21 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v2.1", + "signature": "" + }, + "compilationOptions": {}, + "libraries": { + "Newtonsoft.Json/9.0.1": { + "type": "package", + "serviceable": true, + "sha512": "sha512-U82mHQSKaIk+lpSVCbWYKNavmNH1i5xrExDEquU1i6I5pV6UMOqRnJRSlKO3cMPfcpp0RgDY+8jUXHdQ4IfXvw==", + "path": "newtonsoft.json/9.0.1", + "hashPath": "newtonsoft.json.9.0.1.nupkg.sha512" + }, + "Microsoft.VisualStudio.TestPlatform.Common/17.2.0-release-20220408-11": { + "type": "project", + "serviceable": false, + "sha512": "" + } + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dotnet/deps/testdata/invalid.txt b/pkg/fanal/analyzer/language/dotnet/deps/testdata/invalid.txt new file mode 100644 index 000000000000..9daeafb9864c --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/deps/testdata/invalid.txt @@ -0,0 +1 @@ +test diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/nuget.go b/pkg/fanal/analyzer/language/dotnet/nuget/nuget.go new file mode 100644 index 000000000000..6c0fcc672457 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/nuget.go @@ -0,0 +1,123 @@ +package nuget + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path/filepath" + "slices" + "sort" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/nuget/config" + "github.com/aquasecurity/trivy/pkg/dependency/parser/nuget/lock" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeNuget, newNugetLibraryAnalyzer) +} + +const ( + version = 3 + lockFile = types.NuGetPkgsLock + configFile = types.NuGetPkgsConfig +) + +var requiredFiles = []string{ + lockFile, + configFile, +} + +type nugetLibraryAnalyzer struct { + lockParser language.Parser + configParser language.Parser + licenseParser nuspecParser + logger *log.Logger +} + +func newNugetLibraryAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &nugetLibraryAnalyzer{ + lockParser: lock.NewParser(), + configParser: config.NewParser(), + licenseParser: newNuspecParser(), + logger: log.WithPrefix("nuget"), + }, nil +} + +func (a *nugetLibraryAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + foundLicenses := make(map[string][]string) + if a.licenseParser.packagesDir == "" { + a.logger.Debug("The nuget packages directory couldn't be found. License search disabled") + } + + required := func(_ string, _ fs.DirEntry) bool { + // Parse all required files: `packages.lock.json`, `packages.config` (from a.Required func) + input.FilePatterns.Match() + return true + } + + err := fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error { + // Set the default parser + parser := a.lockParser + + targetFile := filepath.Base(path) + if targetFile == configFile { + parser = a.configParser + } + + app, err := language.Parse(ctx, types.NuGet, path, r, parser) + if err != nil { + return xerrors.Errorf("NuGet parse error: %w", err) + } + + // nuget file doesn't contain dependencies + if app == nil { + return nil + } + + for i, lib := range app.Packages { + license, ok := foundLicenses[lib.ID] + if !ok { + license, err = a.licenseParser.findLicense(lib.Name, lib.Version) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return xerrors.Errorf("license find error: %w", err) + } + foundLicenses[lib.ID] = license + } + + app.Packages[i].Licenses = license + } + + sort.Sort(app.Packages) + apps = append(apps, *app) + return nil + }) + if err != nil { + return nil, xerrors.Errorf("NuGet walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a *nugetLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + return slices.Contains(requiredFiles, fileName) +} + +func (a *nugetLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeNuget +} + +func (a *nugetLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/nuget_test.go b/pkg/fanal/analyzer/language/dotnet/nuget/nuget_test.go new file mode 100644 index 000000000000..dd028bbaab32 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/nuget_test.go @@ -0,0 +1,238 @@ +package nuget + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_nugetLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + env map[string]string + want *analyzer.AnalysisResult + }{ + { + name: "happy path config file.", + dir: "testdata/config", + env: map[string]string{ + "HOME": "testdata/repository", + }, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.NuGet, + FilePath: "packages.config", + Packages: types.Packages{ + { + Name: "Microsoft.AspNet.WebApi", + Version: "5.2.2", + }, + { + Name: "Newtonsoft.Json", + Version: "6.0.4", + }, + }, + }, + }, + }, + }, + { + name: "happy path lock file.", + dir: "testdata/lock", + env: map[string]string{ + "HOME": "testdata/repository", + }, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.NuGet, + FilePath: "packages.lock.json", + Packages: types.Packages{ + { + ID: "Newtonsoft.Json@12.0.3", + Name: "Newtonsoft.Json", + Version: "12.0.3", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + Licenses: []string{"MIT"}, + }, + { + ID: "NuGet.Frameworks@5.7.0", + Name: "NuGet.Frameworks", + Version: "5.7.0", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 11, + EndLine: 19, + }, + }, + DependsOn: []string{"Newtonsoft.Json@12.0.3"}, + }, + }, + }, + }, + }, + }, + { + name: "happy path lock file. `NUGET_PACKAGES` env is used", + dir: "testdata/lock", + env: map[string]string{ + "NUGET_PACKAGES": "testdata/repository/.nuget/packages", + }, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.NuGet, + FilePath: "packages.lock.json", + Packages: types.Packages{ + { + ID: "Newtonsoft.Json@12.0.3", + Name: "Newtonsoft.Json", + Version: "12.0.3", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + Licenses: []string{"MIT"}, + }, + { + ID: "NuGet.Frameworks@5.7.0", + Name: "NuGet.Frameworks", + Version: "5.7.0", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 11, + EndLine: 19, + }, + }, + DependsOn: []string{"Newtonsoft.Json@12.0.3"}, + }, + }, + }, + }, + }, + }, + { + name: "happy path lock file. `.nuget` directory doesn't exist", + dir: "testdata/lock", + env: map[string]string{ + "HOME": "testdata/invalid", + }, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.NuGet, + FilePath: "packages.lock.json", + Packages: types.Packages{ + { + ID: "Newtonsoft.Json@12.0.3", + Name: "Newtonsoft.Json", + Version: "12.0.3", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + }, + { + ID: "NuGet.Frameworks@5.7.0", + Name: "NuGet.Frameworks", + Version: "5.7.0", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 11, + EndLine: 19, + }, + }, + DependsOn: []string{"Newtonsoft.Json@12.0.3"}, + }, + }, + }, + }, + }, + }, + { + name: "happy path lock file without dependencies.", + dir: "testdata/lock-without-deps", + env: map[string]string{ + "HOME": "testdata/repository", + }, + want: &analyzer.AnalysisResult{}, + }, + { + name: "sad path", + dir: "testdata/sad", + env: map[string]string{ + "HOME": "testdata/repository", + }, + want: &analyzer.AnalysisResult{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for env, path := range tt.env { + t.Setenv(env, path) + } + a, err := newNugetLibraryAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_nugetLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "config", + filePath: "test/packages.config", + want: true, + }, + { + name: "lock", + filePath: "test/packages.lock.json", + want: true, + }, + { + name: "zip", + filePath: "test.zip", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := nugetLibraryAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/nuspec.go b/pkg/fanal/analyzer/language/dotnet/nuget/nuspec.go new file mode 100644 index 000000000000..98def24ab8d2 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/nuspec.go @@ -0,0 +1,80 @@ +package nuget + +import ( + "encoding/xml" + "fmt" + "os" + "path/filepath" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +const nuspecExt = "nuspec" + +// https://learn.microsoft.com/en-us/nuget/reference/nuspec +type Package struct { + Metadata Metadata `xml:"metadata"` +} + +type Metadata struct { + License License `xml:"license"` +} + +type License struct { + Text string `xml:",chardata"` + Type string `xml:"type,attr"` +} + +type nuspecParser struct { + packagesDir string // global packages folder - https: //learn.microsoft.com/en-us/nuget/consume-packages/managing-the-global-packages-and-cache-folders +} + +func newNuspecParser() nuspecParser { + // cf. https: //learn.microsoft.com/en-us/nuget/consume-packages/managing-the-global-packages-and-cache-folders + packagesDir := os.Getenv("NUGET_PACKAGES") + if packagesDir == "" { + packagesDir = filepath.Join(os.Getenv("HOME"), ".nuget", "packages") + } + + if !fsutils.DirExists(packagesDir) { + return nuspecParser{} + } + + return nuspecParser{ + packagesDir: packagesDir, + } +} + +func (p nuspecParser) findLicense(name, version string) ([]string, error) { + if p.packagesDir == "" { + return nil, nil + } + + // package path uses lowercase letters only + // e.g. `$HOME/.nuget/packages/newtonsoft.json/13.0.3/newtonsoft.json.nuspec` + // for `Newtonsoft.Json` v13.0.3 + name = strings.ToLower(name) + version = strings.ToLower(version) + + nuspecFileName := fmt.Sprintf("%s.%s", name, nuspecExt) + path := filepath.Join(p.packagesDir, name, version, nuspecFileName) + + f, err := os.Open(path) + if err != nil { + return nil, xerrors.Errorf("unable to open %q file: %w", path, err) + } + defer func() { _ = f.Close() }() + + var pkg Package + if err = xml.NewDecoder(f).Decode(&pkg); err != nil { + return nil, xerrors.Errorf("unable to decode %q file: %w", path, err) + } + + if license := pkg.Metadata.License; license.Type != "expression" || license.Text == "" { + return nil, nil + } + return []string{pkg.Metadata.License.Text}, nil +} diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/testdata/config/packages.config b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/config/packages.config new file mode 100644 index 000000000000..51ea35d31a83 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/config/packages.config @@ -0,0 +1,5 @@ + + + + + diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/testdata/lock-without-deps/packages.lock.json b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/lock-without-deps/packages.lock.json new file mode 100644 index 000000000000..2fca7fd98a29 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/lock-without-deps/packages.lock.json @@ -0,0 +1,6 @@ +{ + "version": 1, + "dependencies": { + "net6.0": {} + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/testdata/lock/packages.lock.json b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/lock/packages.lock.json new file mode 100644 index 000000000000..7c06bd9d435f --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/lock/packages.lock.json @@ -0,0 +1,22 @@ +{ + "version": 1, + "dependencies": { + ".NETCoreApp,Version=v5.0": { + "Newtonsoft.Json": { + "type": "Direct", + "requested": "[12.0.3, )", + "resolved": "12.0.3", + "contentHash": "6mgjfnRB4jKMlzHSl+VD+oUc1IebOZabkbyWj2RiTgWwYPPuaK1H97G1sHqGwPlS5npiF5Q0OrxN1wni2n5QWg==" + }, + "NuGet.Frameworks": { + "type": "Direct", + "requested": "[5.7.0, )", + "resolved": "5.7.0", + "contentHash": "7Q/wUoB3jCBcq9zoBOBGHFhe78C13jViPmvjvzTwthVV8DAjMfpXnqAYtgwdaRLJMkTXrtdLxfPBIFFhmlsnIQ==", + "dependencies": { + "Newtonsoft.Json": "12.0.3" + } + } + } + } +} diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/testdata/repository/.nuget/packages/newtonsoft.json/12.0.3/newtonsoft.json.nuspec b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/repository/.nuget/packages/newtonsoft.json/12.0.3/newtonsoft.json.nuspec new file mode 100755 index 000000000000..c215566e4e41 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/repository/.nuget/packages/newtonsoft.json/12.0.3/newtonsoft.json.nuspec @@ -0,0 +1,42 @@ + + + + Newtonsoft.Json + 12.0.3 + Json.NET + James Newton-King + James Newton-King + false + MIT + https://licenses.nuget.org/MIT + packageIcon.png + https://www.newtonsoft.com/json + Json.NET is a popular high-performance JSON framework for .NET + Copyright © James Newton-King 2008 + json + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/testdata/repository/.nuget/packages/newtonsoft.json/6.0.4/newtonsoft.json.nuspec b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/repository/.nuget/packages/newtonsoft.json/6.0.4/newtonsoft.json.nuspec new file mode 100755 index 000000000000..4f361ab9c2d5 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/repository/.nuget/packages/newtonsoft.json/6.0.4/newtonsoft.json.nuspec @@ -0,0 +1,16 @@ + + + + Newtonsoft.Json + 6.0.4 + Json.NET + James Newton-King + James Newton-King + https://raw.github.com/JamesNK/Newtonsoft.Json/master/LICENSE.md + http://james.newtonking.com/json + false + Json.NET is a popular high-performance JSON framework for .NET + en-US + json + + \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/testdata/repository/.nuget/packages/nuget.frameworks/5.7.0/nuget.frameworks.nuspec b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/repository/.nuget/packages/nuget.frameworks/5.7.0/nuget.frameworks.nuspec new file mode 100755 index 000000000000..21ad4997758e --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/repository/.nuget/packages/nuget.frameworks/5.7.0/nuget.frameworks.nuspec @@ -0,0 +1,23 @@ + + + + NuGet.Frameworks + 5.7.0+b804bf4ba62c0b47c77bbf3e22e196b57cd7a556 + Microsoft + Microsoft + true + LICENSE.txt + https://aka.ms/nugetprj + https://raw.githubusercontent.com/NuGet/Media/master/Images/MainLogo/256x256/nuget_256.png + NuGet's understanding of target frameworks. + © Microsoft Corporation. All rights reserved. + nuget + true + + + + + + + + \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dotnet/nuget/testdata/sad/invalid.txt b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/sad/invalid.txt new file mode 100644 index 000000000000..9daeafb9864c --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/nuget/testdata/sad/invalid.txt @@ -0,0 +1 @@ +test diff --git a/pkg/fanal/analyzer/language/dotnet/packagesprops/packagesprops.go b/pkg/fanal/analyzer/language/dotnet/packagesprops/packagesprops.go new file mode 100644 index 000000000000..8ff8982e2fad --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/packagesprops/packagesprops.go @@ -0,0 +1,49 @@ +package packagesprops + +import ( + "context" + "os" + "strings" + + "golang.org/x/xerrors" + + props "github.com/aquasecurity/trivy/pkg/dependency/parser/nuget/packagesprops" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&packagesPropsAnalyzer{}) +} + +const ( + version = 1 + packagesPropsSuffix = "packages.props" // https://github.com/dotnet/roslyn-tools/blob/b4c5220f5dfc4278847b6d38eff91cc1188f8066/src/RoslynInsertionTool/RoslynInsertionTool/CoreXT.cs#L39-L40 +) + +type packagesPropsAnalyzer struct{} + +func (a packagesPropsAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + parser := props.NewParser() + res, err := language.Analyze(ctx, types.PackagesProps, input.FilePath, input.Content, parser) + if err != nil { + return nil, xerrors.Errorf("*Packages.props dependencies analysis error: %w", err) + } + + return res, nil +} + +func (a packagesPropsAnalyzer) Required(filePath string, _ os.FileInfo) bool { + // There is no information about this in the documentation, + // but NuGet works correctly with lowercase filenames + return strings.HasSuffix(strings.ToLower(filePath), packagesPropsSuffix) +} + +func (a packagesPropsAnalyzer) Type() analyzer.Type { + return analyzer.TypePackagesProps +} + +func (a packagesPropsAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/dotnet/packagesprops/packagesprops_test.go b/pkg/fanal/analyzer/language/dotnet/packagesprops/packagesprops_test.go new file mode 100644 index 000000000000..1b4cf93215a9 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/packagesprops/packagesprops_test.go @@ -0,0 +1,133 @@ +package packagesprops + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_packagesPropsAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path packages props", + inputFile: "testdata/Packages.props", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PackagesProps, + FilePath: "testdata/Packages.props", + Packages: types.Packages{ + { + ID: "Package1@22.1.4", + Name: "Package1", + Version: "22.1.4", + }, + { + ID: "Package2@2.3.0", + Name: "Package2", + Version: "2.3.0", + }, + }, + }, + }, + }, + }, + { + name: "happy path directory packages props", + inputFile: "testdata/Directory.Packages.props", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PackagesProps, + FilePath: "testdata/Directory.Packages.props", + Packages: types.Packages{ + { + ID: "Package1@4.2.1", + Name: "Package1", + Version: "4.2.1", + }, + { + ID: "Package2@8.2.0", + Name: "Package2", + Version: "8.2.0", + }, + }, + }, + }, + }, + }, + { + name: "sad path", + inputFile: "testdata/invalid.txt", + wantErr: "*Packages.props dependencies analysis error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := packagesPropsAnalyzer{} + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_packagesPropsAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "directory packages props", + filePath: "test/Directory.Packages.props", + want: true, + }, + { + name: "packages props", + filePath: "test/Packages.props", + want: true, + }, + { + name: "packages props lower case", + filePath: "test/packages.props", + want: true, + }, + { + name: "zip", + filePath: "test.zip", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := packagesPropsAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/dotnet/packagesprops/testdata/Directory.Packages.props b/pkg/fanal/analyzer/language/dotnet/packagesprops/testdata/Directory.Packages.props new file mode 100644 index 000000000000..90adf042fde1 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/packagesprops/testdata/Directory.Packages.props @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dotnet/packagesprops/testdata/Packages.props b/pkg/fanal/analyzer/language/dotnet/packagesprops/testdata/Packages.props new file mode 100644 index 000000000000..2532bf1b1dc0 --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/packagesprops/testdata/Packages.props @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/dotnet/packagesprops/testdata/invalid.txt b/pkg/fanal/analyzer/language/dotnet/packagesprops/testdata/invalid.txt new file mode 100644 index 000000000000..9daeafb9864c --- /dev/null +++ b/pkg/fanal/analyzer/language/dotnet/packagesprops/testdata/invalid.txt @@ -0,0 +1 @@ +test diff --git a/pkg/fanal/analyzer/language/elixir/mix/mix.go b/pkg/fanal/analyzer/language/elixir/mix/mix.go new file mode 100644 index 000000000000..34ecc0e67f87 --- /dev/null +++ b/pkg/fanal/analyzer/language/elixir/mix/mix.go @@ -0,0 +1,49 @@ +package mix + +import ( + "context" + "os" + "path/filepath" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/hex/mix" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&mixLockAnalyzer{}) +} + +const ( + version = 1 +) + +// mixLockAnalyzer analyzes 'mix.lock' +type mixLockAnalyzer struct{} + +func (a mixLockAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + p := mix.NewParser() + res, err := language.Analyze(ctx, types.Hex, input.FilePath, input.Content, p) + if err != nil { + return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err) + } + return res, nil +} + +func (a mixLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { + // Lock file name can be anything. + // cf. https://hexdocs.pm/mix/Mix.Project.html#module-configuration + // By default, we only check the default file name - `mix.lock`. + return filepath.Base(filePath) == types.MixLock +} + +func (a mixLockAnalyzer) Type() analyzer.Type { + return analyzer.TypeMixLock +} + +func (a mixLockAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/elixir/mix/mix_test.go b/pkg/fanal/analyzer/language/elixir/mix/mix_test.go new file mode 100644 index 000000000000..b450abe9269d --- /dev/null +++ b/pkg/fanal/analyzer/language/elixir/mix/mix_test.go @@ -0,0 +1,96 @@ +package mix + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_mixLockAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + inputFile: "testdata/happy.mix.lock", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Hex, + FilePath: "testdata/happy.mix.lock", + Packages: types.Packages{ + { + ID: "bunt@0.2.0", + Name: "bunt", + Version: "0.2.0", + Locations: []types.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty file", + inputFile: "testdata/empty.mix.lock", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer func() { + err = f.Close() + require.NoError(t, err) + }() + + a := mixLockAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_mixLockAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path", + filePath: "mix.lock", + want: true, + }, + { + name: "sad path", + filePath: "test.txt", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := mixLockAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/elixir/mix/testdata/empty.mix.lock b/pkg/fanal/analyzer/language/elixir/mix/testdata/empty.mix.lock new file mode 100644 index 000000000000..15081287722e --- /dev/null +++ b/pkg/fanal/analyzer/language/elixir/mix/testdata/empty.mix.lock @@ -0,0 +1,2 @@ +%{ +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/elixir/mix/testdata/happy.mix.lock b/pkg/fanal/analyzer/language/elixir/mix/testdata/happy.mix.lock new file mode 100644 index 000000000000..873a3a7fc7c8 --- /dev/null +++ b/pkg/fanal/analyzer/language/elixir/mix/testdata/happy.mix.lock @@ -0,0 +1,3 @@ +%{ + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"}, +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/golang/binary/binary.go b/pkg/fanal/analyzer/language/golang/binary/binary.go new file mode 100644 index 000000000000..b3d31c2b963c --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/binary/binary.go @@ -0,0 +1,47 @@ +package binary + +import ( + "context" + "errors" + "os" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/binary" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/utils" +) + +func init() { + analyzer.RegisterAnalyzer(&gobinaryLibraryAnalyzer{}) +} + +const version = 1 + +type gobinaryLibraryAnalyzer struct{} + +func (a gobinaryLibraryAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + p := binary.NewParser() + res, err := language.Analyze(ctx, types.GoBinary, input.FilePath, input.Content, p) + if errors.Is(err, binary.ErrUnrecognizedExe) || errors.Is(err, binary.ErrNonGoBinary) { + return nil, nil + } else if err != nil { + return nil, xerrors.Errorf("go binary (filepath: %s) parse error: %w", input.FilePath, err) + } + + return res, nil +} + +func (a gobinaryLibraryAnalyzer) Required(_ string, fileInfo os.FileInfo) bool { + return utils.IsExecutable(fileInfo) +} + +func (a gobinaryLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeGoBinary +} + +func (a gobinaryLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/golang/binary/binary_test.go b/pkg/fanal/analyzer/language/golang/binary/binary_test.go new file mode 100644 index 000000000000..f4a7a4242a5c --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/binary/binary_test.go @@ -0,0 +1,128 @@ +package binary + +import ( + "os" + "runtime" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_gobinaryLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + inputFile: "testdata/executable_gobinary", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GoBinary, + FilePath: "testdata/executable_gobinary", + Packages: types.Packages{ + { + ID: "github.com/aquasecurity/test", + Name: "github.com/aquasecurity/test", + Version: "", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "stdlib@v1.15.2", + }, + }, + { + ID: "stdlib@v1.15.2", + Name: "stdlib", + Version: "v1.15.2", + Relationship: types.RelationshipDirect, + }, + { + ID: "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + Name: "github.com/aquasecurity/go-pep440-version", + Version: "v0.0.0-20210121094942-22b2f8951d46", + }, + { + ID: "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + Name: "github.com/aquasecurity/go-version", + Version: "v0.0.0-20210121072130-637058cfe492", + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + }, + }, + }, + }, + }, + }, + { + name: "not go binary", + inputFile: "testdata/executable_bash", + }, + { + name: "broken elf", + inputFile: "testdata/broken_elf", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := gobinaryLibraryAnalyzer{} + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_gobinaryLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "executable file", + filePath: lo.Ternary(runtime.GOOS == "windows", "testdata/binary.exe", "testdata/0755"), + want: true, + }, + { + name: "file perm 0644", + filePath: "testdata/0644", + want: false, + }, + { + name: "symlink", + filePath: "testdata/symlink", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := gobinaryLibraryAnalyzer{} + fileInfo, err := os.Lstat(tt.filePath) + require.NoError(t, err) + got := a.Required(tt.filePath, fileInfo) + assert.Equal(t, tt.want, got, fileInfo.Mode().Perm()) + }) + } +} diff --git a/pkg/fanal/analyzer/language/golang/binary/testdata/0644 b/pkg/fanal/analyzer/language/golang/binary/testdata/0644 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/language/golang/binary/testdata/0755 b/pkg/fanal/analyzer/language/golang/binary/testdata/0755 new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/language/golang/binary/testdata/binary.exe b/pkg/fanal/analyzer/language/golang/binary/testdata/binary.exe new file mode 100644 index 000000000000..f8e020bf1ebe --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/binary/testdata/binary.exe @@ -0,0 +1 @@ +exe \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/golang/binary/testdata/broken_elf b/pkg/fanal/analyzer/language/golang/binary/testdata/broken_elf new file mode 100755 index 000000000000..1a736bf298d6 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/binary/testdata/broken_elf @@ -0,0 +1 @@ +ELF diff --git a/pkg/fanal/analyzer/language/golang/binary/testdata/executable_bash b/pkg/fanal/analyzer/language/golang/binary/testdata/executable_bash new file mode 100755 index 000000000000..ca3b9f0d2cda --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/binary/testdata/executable_bash @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "hello" diff --git a/pkg/fanal/analyzer/language/golang/binary/testdata/executable_gobinary b/pkg/fanal/analyzer/language/golang/binary/testdata/executable_gobinary new file mode 100755 index 000000000000..2890300b9205 Binary files /dev/null and b/pkg/fanal/analyzer/language/golang/binary/testdata/executable_gobinary differ diff --git a/pkg/fanal/analyzer/language/golang/binary/testdata/symlink b/pkg/fanal/analyzer/language/golang/binary/testdata/symlink new file mode 120000 index 000000000000..19102815663d --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/binary/testdata/symlink @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/golang/mod/mod.go b/pkg/fanal/analyzer/language/golang/mod/mod.go new file mode 100644 index 000000000000..b035f8b45296 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/mod.go @@ -0,0 +1,484 @@ +package mod + +import ( + "cmp" + "context" + "errors" + "fmt" + "go/build" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "regexp" + "slices" + "unicode" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/mod" + "github.com/aquasecurity/trivy/pkg/dependency/parser/golang/sum" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xpath "github.com/aquasecurity/trivy/pkg/x/path" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeGoMod, newGoModAnalyzer) +} + +const version = 2 + +var ( + requiredFiles = []string{ + types.GoMod, + types.GoSum, + } + licenseRegexp = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING|README|NOTICE).*$`) +) + +type gomodAnalyzer struct { + // root go.mod/go.sum + modParser language.Parser + sumParser language.Parser + + // go.mod/go.sum in dependencies + leafModParser language.Parser + + licenseClassifierConfidenceLevel float64 + + // gopathFS represents the $GOPATH directory as an fs.FS. + // It should contain the "pkg/mod" subdirectory structure. + gopathFS fs.FS + + logger *log.Logger +} + +func newGoModAnalyzer(opt analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &gomodAnalyzer{ + modParser: mod.NewParser(true, opt.DetectionPriority == types.PriorityComprehensive), // Only the root module should replace + sumParser: sum.NewParser(), + leafModParser: mod.NewParser(false, false), // Don't detect stdlib for non-root go.mod files + licenseClassifierConfidenceLevel: opt.LicenseScannerOption.ClassifierConfidenceLevel, + gopathFS: os.DirFS(cmp.Or(os.Getenv("GOPATH"), build.Default.GOPATH)), + logger: log.WithPrefix("golang"), + }, nil +} + +func (a *gomodAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.GoMod || input.FilePatterns.Match(path) + } + + err := fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, _ io.Reader) error { + // Parse go.mod + gomod, err := parse(ctx, input.FS, path, a.modParser) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if gomod == nil { + return nil + } + + if lessThanGo117(gomod) { + // e.g. /app/go.mod => /app/go.sum + sumPath := filepath.Join(filepath.Dir(path), types.GoSum) + gosum, err := parse(ctx, input.FS, sumPath, a.sumParser) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return xerrors.Errorf("parse error: %w", err) + } + mergeGoSum(gomod, gosum) + } + + apps = append(apps, *gomod) + return nil + }) + if err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + + if err = a.fillAdditionalData(ctx, input.FS, apps); err != nil { + a.logger.Warn("Unable to collect additional info", log.Err(err)) + } + + // Add orphan indirect dependencies under the main module + a.addOrphanIndirectDepsUnderRoot(apps) + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a *gomodAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + + // Save required files (go.mod/go.sum) + // Note: vendor directory doesn't contain these files, so we can skip checking for this. + // See: https://github.com/aquasecurity/trivy/issues/8527#issuecomment-2777848027 + if slices.Contains(requiredFiles, fileName) { + return true + } + + // Save license files from vendor directory + if licenseRegexp.MatchString(fileName) && xpath.Contains(filePath, "vendor") { + return true + } + + return false +} + +func (a *gomodAnalyzer) Type() analyzer.Type { + return analyzer.TypeGoMod +} + +func (a *gomodAnalyzer) Version() int { + return version +} + +// fillAdditionalData collects licenses and dependency relationships, then update applications. +func (a *gomodAnalyzer) fillAdditionalData(ctx context.Context, fsys fs.FS, apps []types.Application) error { + var modSearchDirs []searchDir + + // $GOPATH/pkg/mod + if gopath, err := newGOPATH(a.gopathFS); err != nil { + a.logger.Debug("GOPATH not found. Run 'go mod download' or 'go mod tidy' for identifying dependency graph and licenses", log.Err(err)) + } else { + modSearchDirs = append(modSearchDirs, gopath) + } + + licenses := make(map[string][]string) + for i, app := range apps { + licenseSearchDirs := modSearchDirs + + // vendor directory next to go.mod + if vendorDir, err := newVendorDir(fsys, app.FilePath); err == nil { + licenseSearchDirs = append(licenseSearchDirs, vendorDir) + } + + // Actually used dependencies + usedPkgs := lo.SliceToMap(app.Packages, func(pkg types.Package) (string, types.Package) { + return pkg.Name, pkg + }) + + // Check if either $GOPATH/pkg/mod or the vendor directory exists + if len(licenseSearchDirs) == 0 { + continue + } + + for j, pkg := range app.Packages { + // Collect licenses + if licenseNames, err := findLicense(licenseSearchDirs, pkg, licenses, a.licenseClassifierConfidenceLevel); err != nil { + return xerrors.Errorf("unable to collect license: %w", err) + } else { + // Fill licenses + apps[i].Packages[j].Licenses = licenseNames + } + + // Collect dependencies of the direct dependency from $GOPATH/pkg/mod because the vendor directory doesn't have go.mod files. + dep, err := a.collectDeps(ctx, modSearchDirs, pkg) + if err != nil { + return xerrors.Errorf("dependency graph error: %w", err) + } else if dep.ID == "" { + // go.mod not found + continue + } + // Filter out unused dependencies and convert module names to module IDs + apps[i].Packages[j].DependsOn = lo.FilterMap(dep.DependsOn, func(modName string, _ int) (string, bool) { + m, ok := usedPkgs[modName] + if !ok { + return "", false + } + return m.ID, true + }) + } + } + return nil +} + +func (a *gomodAnalyzer) collectDeps(ctx context.Context, searchDirs []searchDir, pkg types.Package) (types.Dependency, error) { + for _, searchDir := range searchDirs { + // e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.1.0 + modDir, err := searchDir.Resolve(pkg) + if err != nil { + continue + } + + dependsOn, err := a.resolveDeps(ctx, modDir) + if errors.Is(err, fs.ErrNotExist) { + a.logger.Debug("Unable to identify dependencies as it doesn't support Go modules", + log.String("module", pkg.ID)) + return types.Dependency{}, nil + } else if err != nil { + return types.Dependency{}, xerrors.Errorf("resolve deps error: %w", err) + } + + return types.Dependency{ + ID: pkg.ID, + DependsOn: dependsOn, + }, nil + } + return types.Dependency{}, nil +} + +// resolveDeps parses go.mod under $GOPATH/pkg/mod and returns the dependencies +func (a *gomodAnalyzer) resolveDeps(ctx context.Context, modDir fs.FS) ([]string, error) { + // e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.1.0/go.mod + f, err := modDir.Open("go.mod") + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + file, ok := f.(xio.ReadSeekCloserAt) + if !ok { + return nil, xerrors.Errorf("type assertion error: %w", err) + } + + // Parse go.mod under $GOPATH/pkg/mod + pkgs, _, err := a.leafModParser.Parse(ctx, file) + if err != nil { + return nil, xerrors.Errorf("parse error: %w", err) + } + + // Filter out indirect dependencies + dependsOn := lo.FilterMap(pkgs, func(lib types.Package, _ int) (string, bool) { + return lib.Name, lib.Relationship == types.RelationshipDirect + }) + + return dependsOn, nil + +} + +// addOrphanIndirectDepsUnderRoot handles indirect dependencies that have no identifiable parent packages in the dependency tree. +// This situation can occur when: +// - $GOPATH/pkg directory doesn't exist +// - Module cache is incomplete +// - etc. +// +// In such cases, indirect packages become "orphaned" - they exist in the dependency list +// but have no connection to the dependency tree. This function resolves this issue by: +// 1. Finding the root (main) module +// 2. Identifying all indirect dependencies that have no parent packages +// 3. Adding these orphaned indirect dependencies under the main module +// +// This ensures that all packages remain visible in the dependency tree, even when the complete +// dependency chain cannot be determined. +func (a *gomodAnalyzer) addOrphanIndirectDepsUnderRoot(apps []types.Application) { + for _, app := range apps { + // Find the main module + _, rootIdx, found := lo.FindIndexOf(app.Packages, func(pkg types.Package) bool { + return pkg.Relationship == types.RelationshipRoot + }) + if !found { + continue + } + + // Collect all orphan indirect dependencies that are unable to identify parents + parents := app.Packages.ParentDeps() + orphanDeps := lo.FilterMap(app.Packages, func(pkg types.Package, _ int) (string, bool) { + return pkg.ID, pkg.Relationship == types.RelationshipIndirect && len(parents[pkg.ID]) == 0 + }) + // Add orphan indirect dependencies under the main module + app.Packages[rootIdx].DependsOn = append(app.Packages[rootIdx].DependsOn, orphanDeps...) + } +} + +func parse(ctx context.Context, fsys fs.FS, path string, parser language.Parser) (*types.Application, error) { + f, err := fsys.Open(path) + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + file, ok := f.(xio.ReadSeekCloserAt) + if !ok { + return nil, xerrors.Errorf("type assertion error: %w", err) + } + + // Parse go.mod or go.sum + return language.Parse(ctx, types.GoModule, path, file, parser) +} + +func lessThanGo117(gomod *types.Application) bool { + for _, lib := range gomod.Packages { + // The indirect field is populated only in Go 1.17+ + if lib.Relationship == types.RelationshipIndirect { + return false + } + } + return true +} + +func mergeGoSum(gomod, gosum *types.Application) { + if gomod == nil || gosum == nil { + return + } + uniq := make(map[string]types.Package) + for _, lib := range gomod.Packages { + // It will be used for merging go.sum. + uniq[lib.Name] = lib + } + + // For Go 1.16 or less, we need to merge go.sum into go.mod. + for _, lib := range gosum.Packages { + // Skip dependencies in go.mod so that go.mod should be preferred. + if _, ok := uniq[lib.Name]; ok { + continue + } + + // This dependency doesn't exist in go.mod, so it must be an indirect dependency. + lib.Indirect = true + lib.Relationship = types.RelationshipIndirect + uniq[lib.Name] = lib + } + + gomod.Packages = lo.Values(uniq) +} + +func findLicense(searchDirs []searchDir, pkg types.Package, licenses map[string][]string, classifierConfidenceLevel float64) ([]string, error) { + if licenses[pkg.ID] != nil { + return licenses[pkg.ID], nil + } + + var license *types.LicenseFile + for _, searchDir := range searchDirs { + sub, err := searchDir.Resolve(pkg) + if err != nil { + continue + } + + err = fs.WalkDir(sub, ".", func(path string, d fs.DirEntry, err error) error { + switch { + case err != nil: + return err + case !d.Type().IsRegular(): + return nil + case !licenseRegexp.MatchString(filepath.Base(path)): + return nil + } + + // e.g. $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v0.1.0/LICENSE + f, err := sub.Open(path) + if err != nil { + return xerrors.Errorf("file (%s) open error: %w", path, err) + } + defer f.Close() + + if l, err := licensing.Classify(path, f, classifierConfidenceLevel); err != nil { + return xerrors.Errorf("license classify error: %w", err) + } else if l != nil && len(l.Findings) > 0 { // License found + license = l + return fs.SkipAll // Stop walking + } + return nil + }) + + switch { + // The module path may not exist + case errors.Is(err, os.ErrNotExist): + continue + case err != nil && !errors.Is(err, io.EOF): + return nil, xerrors.Errorf("unable to find a known open source license: %w", err) + case license == nil || len(license.Findings) == 0: + continue + } + + // License found + licenseNames := license.Findings.Names() + licenses[pkg.ID] = licenseNames + return licenseNames, nil + } + return nil, nil +} + +// normalizeModName escapes upper characters +// e.g. 'github.com/BurntSushi/toml' => 'github.com/!burnt!sushi' +func normalizeModName(name string) string { + var newName []rune + for _, c := range name { + if unicode.IsUpper(c) { + // 'A' => '!a' + newName = append(newName, '!', unicode.ToLower(c)) + } else { + newName = append(newName, c) + } + } + return string(newName) +} + +type searchDir interface { + Resolve(pkg types.Package) (fs.FS, error) +} + +type gopathDir struct { + root fs.FS // $GOPATH/pkg/mod as fs.FS (can be os.DirFS or test fixture) +} + +func newGOPATH(gopathFS fs.FS) (searchDir, error) { + // $GOPATH/pkg/mod + // Use path.Join instead of filepath.Join because fs.FS always uses forward slashes, + // regardless of the operating system. + modFS, err := fs.Sub(gopathFS, path.Join("pkg", "mod")) + if err != nil { + return nil, xerrors.Errorf("failed to access $GOPATH/pkg/mod: %w", err) + } + + // Check if the directory exists. + // fs.Sub doesn't return an error for non-existent directories, + // so we need to explicitly verify the directory exists. + if _, err := fs.Stat(modFS, "."); err != nil { + return nil, xerrors.Errorf("$GOPATH/pkg/mod does not exist: %w", err) + } + + return &gopathDir{root: modFS}, nil +} + +// Resolve resolves the module directory for a given package. +// It adds the version suffix to the module name and returns the directory as an fs.FS. +// e.g. $GOPATH/pkg/mod => $GOPATH/pkg/mod/github.com/aquasecurity/go-dep-parser@v1.0.0 +func (d *gopathDir) Resolve(pkg types.Package) (fs.FS, error) { + name := normalizeModName(pkg.Name) + + // Add version suffix for packages from $GOPATH + // e.g. github.com/aquasecurity/go-dep-parser@v1.0.0 + modDirName := fmt.Sprintf("%s@%s", name, pkg.Version) + + return fs.Sub(d.root, modDirName) +} + +type vendorDir struct { + root fs.FS +} + +func newVendorDir(fsys fs.FS, modPath string) (vendorDir, error) { + // vendor directory is in the same directory as go.mod + vendor := filepath.Join(filepath.Dir(modPath), "vendor") + sub, err := fs.Sub(fsys, vendor) + if err != nil { + return vendorDir{}, xerrors.Errorf("failed to access vendor directory: %w", err) + } + + // Check if the directory exists. + // fs.Sub doesn't return an error for non-existent directories, + // so we need to explicitly verify the directory exists. + if _, err := fs.Stat(sub, "."); err != nil { + return vendorDir{}, xerrors.Errorf("vendor directory does not exist: %w", err) + } + + return vendorDir{root: sub}, nil +} + +// Resolve resolves the module directory for a given package. +// It doesn't add the version suffix to the module name. +// e.g. vendor/ => vendor/github.com/aquasecurity/go-dep-parser +func (d vendorDir) Resolve(pkg types.Package) (fs.FS, error) { + return fs.Sub(d.root, normalizeModName(pkg.Name)) +} diff --git a/pkg/fanal/analyzer/language/golang/mod/mod_test.go b/pkg/fanal/analyzer/language/golang/mod/mod_test.go new file mode 100644 index 000000000000..9b08e13b3657 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/mod_test.go @@ -0,0 +1,399 @@ +package mod + +import ( + "sort" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const gopathFixture = "testdata/gopath.txtar" + +func Test_gomodAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + txtar string + gopath bool + want *analyzer.AnalysisResult + }{ + { + name: "happy", + txtar: "testdata/happy.txtar", + gopath: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GoModule, + FilePath: "go.mod", + Packages: types.Packages{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237", + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20220406074731-71021a481237", + Relationship: types.RelationshipDirect, + Licenses: []string{"MIT"}, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + DependsOn: []string{ + "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Relationship: types.RelationshipIndirect, + Indirect: true, + }, + }, + }, + }, + }, + }, + { + name: "wrong go.mod from `pkg`", + txtar: "testdata/wrong-gomod-in-pkg.txtar", + gopath: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GoModule, + FilePath: "go.mod", + Packages: types.Packages{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "github.com/sad/sad@v0.0.1", + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/sad/sad@v0.0.1", + Name: "github.com/sad/sad", + Version: "v0.0.1", + Relationship: types.RelationshipDirect, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/sad/sad", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "no pkg dir found", + txtar: "testdata/no-pkg-found.txtar", + gopath: false, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GoModule, + FilePath: "go.mod", + Packages: types.Packages{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "github.com/aquasecurity/go-dep-parser@v1.0.0", + "github.com/aquasecurity/go-version@v1.0.1", + "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", // No parent found, so it's added here. + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v1.0.0", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v1.0.0", + Relationship: types.RelationshipDirect, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-version@v1.0.1", + Name: "github.com/aquasecurity/go-version", + Version: "v1.0.1", + Relationship: types.RelationshipDirect, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/aquasecurity/go-version", + }, + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Relationship: types.RelationshipIndirect, + Indirect: true, + }, + }, + }, + }, + }, + }, + { + name: "less than 1.17", + txtar: "testdata/merge.txtar", + gopath: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GoModule, + FilePath: "go.mod", + Packages: types.Packages{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "github.com/aquasecurity/go-dep-parser@v0.0.0-20230219131432-590b1dfb6edd", + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20230219131432-590b1dfb6edd", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20230219131432-590b1dfb6edd", + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "github.com/BurntSushi/toml@v0.3.1", + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + { + ID: "github.com/BurntSushi/toml@v0.3.1", + Name: "github.com/BurntSushi/toml", + Version: "v0.3.1", + Relationship: types.RelationshipIndirect, + Indirect: true, + Licenses: []string{ + "MIT", + }, + }, + }, + }, + }, + }, + }, + { + name: "no go.sum", + txtar: "testdata/no-go-sum.txtar", + gopath: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GoModule, + FilePath: "go.mod", + Packages: types.Packages{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "github.com/aquasecurity/go-dep-parser@v0.0.0-20230219131432-590b1dfb6edd", + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.0-20230219131432-590b1dfb6edd", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.0-20230219131432-590b1dfb6edd", + Relationship: types.RelationshipDirect, + DependsOn: []string{}, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "sad go.mod", + txtar: "testdata/sad.txtar", + gopath: false, + want: &analyzer.AnalysisResult{}, + }, + { + name: "deps from GOPATH and license from vendor dir", + txtar: "testdata/vendor-dir-exists.txtar", + gopath: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GoModule, + FilePath: "go.mod", + Packages: types.Packages{ + { + ID: "github.com/org/repo", + Name: "github.com/org/repo", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "github.com/aquasecurity/go-dep-parser@v0.0.1", + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/org/repo", + }, + }, + }, + { + ID: "github.com/aquasecurity/go-dep-parser@v0.0.1", + Name: "github.com/aquasecurity/go-dep-parser", + Version: "v0.0.1", + Relationship: types.RelationshipDirect, + Licenses: []string{"Apache-2.0"}, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefVCS, + URL: "https://github.com/aquasecurity/go-dep-parser", + }, + }, + DependsOn: []string{ + "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Relationship: types.RelationshipIndirect, + Indirect: true, + }, + }, + }, + }, + }, + }, + } + + // Load GOPATH fixture once as fs.FS (represents $GOPATH/pkg/mod) + gopathFS := testutil.TxtarToFS(t, gopathFixture) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Load test case txtar as fs.FS + fsys := testutil.TxtarToFS(t, tt.txtar) + + a, err := newGoModAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + // Set GOPATH fs.FS for testing + ma := a.(*gomodAnalyzer) + // Use empty fs.FS to simulate no GOPATH scenario + ma.gopathFS = fstest.MapFS{} + if tt.gopath { + ma.gopathFS = gopathFS + } + + ctx := t.Context() + got, err := ma.PostAnalyze(ctx, analyzer.PostAnalysisInput{ + FS: fsys, + }) + require.NoError(t, err) + + if len(got.Applications) > 0 { + sort.Sort(got.Applications[0].Packages) + sort.Sort(tt.want.Applications[0].Packages) + } + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_gomodAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "go.mod", + filePath: "test/go.mod", + want: true, + }, + { + name: "go.sum", + filePath: "test/foo/go.sum", + want: true, + }, + { + name: "sad", + filePath: "a/b/c/d/test.sum", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := gomodAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/gopath.txtar b/pkg/fanal/analyzer/language/golang/mod/testdata/gopath.txtar new file mode 100644 index 000000000000..714be02f1b1b --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/gopath.txtar @@ -0,0 +1,122 @@ +GOPATH directory structure containing pkg/mod with cached Go module dependencies. +This is used as a shared test fixture across multiple test cases. +All file paths must include the "pkg/mod/" prefix to properly represent the GOPATH structure. + +-- pkg/mod/github.com/!burnt!sushi/toml@v0.3.1/COPYING -- +The MIT License (MIT) + +Copyright (c) 2013 TOML authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +-- pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237/LICENSE -- +MIT License + +Copyright (c) 2019 Teppei Fukuda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +-- pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20220406074731-71021a481237/go.mod -- +module github.com/aquasecurity/go-dep-parser + +go 1.18 + +require ( + github.com/BurntSushi/toml v1.2.1 + github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-retryablehttp v0.7.2 + github.com/liamg/jfather v0.0.7 + github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 + github.com/samber/lo v1.37.0 + github.com/stretchr/testify v1.8.1 + go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20220407100705-7b9b53b0aca4 + golang.org/x/mod v0.8.0 + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/text v0.3.8 // indirect +) +-- pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.0-20230219131432-590b1dfb6edd/go.mod -- +module github.com/aquasecurity/go-dep-parser + +go 1.18 + +require ( + github.com/BurntSushi/toml v1.2.1 + github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 + github.com/hashicorp/go-multierror v1.1.1 + github.com/hashicorp/go-retryablehttp v0.7.2 + github.com/liamg/jfather v0.0.7 + github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 + github.com/samber/lo v1.37.0 + github.com/stretchr/testify v1.8.1 + go.uber.org/zap v1.24.0 + golang.org/x/exp v0.0.0-20220407100705-7b9b53b0aca4 + golang.org/x/mod v0.8.0 + golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/text v0.3.8 // indirect +) +-- pkg/mod/github.com/aquasecurity/go-dep-parser@v0.0.1/go.mod -- +module github.com/aquasecurity/go-dep-parser + +go 1.18 + +require ( + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 +) +-- pkg/mod/github.com/sad/sad@v0.0.1/go.mod -- +wrong diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/happy.txtar b/pkg/fanal/analyzer/language/golang/mod/testdata/happy.txtar new file mode 100644 index 000000000000..4fbae2701ec5 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/happy.txtar @@ -0,0 +1,38 @@ +Test case with go.mod, go.sum, and GOPATH pkg/mod dependencies. +This tests the happy path where all dependencies can be resolved from GOPATH. + +-- go.mod -- +module github.com/org/repo + +go 1.17 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211110174639-8257534ffed3 + +require golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect + +replace github.com/aquasecurity/go-dep-parser => github.com/aquasecurity/go-dep-parser v0.0.0-20220406074731-71021a481237 +-- go.sum -- +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/urfave/cli v1.22.5 h1:lNq9sAHXK2qfdI8W+GRItjCEkI+2oR4d+MEHy1CKXoU= +github.com/urfave/cli v1.22.5/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/merge.txtar b/pkg/fanal/analyzer/language/golang/mod/testdata/merge.txtar new file mode 100644 index 000000000000..8f8824baa0c7 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/merge.txtar @@ -0,0 +1,16 @@ +Test case for Go version < 1.17 with go.sum. +This tests merging dependency information from both go.mod and go.sum. + +-- go.mod -- +module github.com/org/repo + +go 1.15 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211110174639-8257534ffed3 + +replace github.com/aquasecurity/go-dep-parser => github.com/aquasecurity/go-dep-parser v0.0.0-20230219131432-590b1dfb6edd +-- go.sum -- +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/aquasecurity/go-dep-parser v0.0.0-20230219131432-590b1dfb6edd h1:H9IR14rR3+Z13ZH7ay9bs2hHBL7WAqdEJLLr8nhx/Rs= +github.com/aquasecurity/go-dep-parser v0.0.0-20230219131432-590b1dfb6edd/go.mod h1:4dZHU2Ntsh9EopNVdTKf8UjSGDNTMVoyB5B34RjD75g= diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/no-go-sum.txtar b/pkg/fanal/analyzer/language/golang/mod/testdata/no-go-sum.txtar new file mode 100644 index 000000000000..8c5a230e83ec --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/no-go-sum.txtar @@ -0,0 +1,11 @@ +Test case without go.sum file. +This tests that the analyzer works when only go.mod is present. + +-- go.mod -- +module github.com/org/repo + +go 1.15 + +require github.com/aquasecurity/go-dep-parser v0.0.0-20211110174639-8257534ffed3 + +replace github.com/aquasecurity/go-dep-parser => github.com/aquasecurity/go-dep-parser v0.0.0-20230219131432-590b1dfb6edd diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/no-pkg-found.txtar b/pkg/fanal/analyzer/language/golang/mod/testdata/no-pkg-found.txtar new file mode 100644 index 000000000000..54e916e1ab65 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/no-pkg-found.txtar @@ -0,0 +1,14 @@ +Test case where no GOPATH pkg/mod directory is found. +This tests the fallback behavior when dependencies cannot be found in GOPATH. + +-- go.mod -- +module github.com/org/repo + +go 1.23 + +require ( + github.com/aquasecurity/go-dep-parser v1.0.0 + github.com/aquasecurity/go-version v1.0.1 +) + +require golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/sad.txtar b/pkg/fanal/analyzer/language/golang/mod/testdata/sad.txtar new file mode 100644 index 000000000000..b752639590f8 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/sad.txtar @@ -0,0 +1,5 @@ +Test case with invalid go.mod file. +This tests error handling for malformed go.mod files. + +-- go.mod -- +invalid diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/vendor-dir-exists.txtar b/pkg/fanal/analyzer/language/golang/mod/testdata/vendor-dir-exists.txtar new file mode 100644 index 000000000000..a8c8029935b6 --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/vendor-dir-exists.txtar @@ -0,0 +1,213 @@ +Test case with vendor directory. +This tests getting dependency licenses from the vendor directory instead of GOPATH. + +-- go.mod -- +module github.com/org/repo + +go 1.17 + +require github.com/aquasecurity/go-dep-parser v0.0.1 + +require golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect +-- vendor/github.com/aquasecurity/go-dep-parser/LICENSE -- + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/pkg/fanal/analyzer/language/golang/mod/testdata/wrong-gomod-in-pkg.txtar b/pkg/fanal/analyzer/language/golang/mod/testdata/wrong-gomod-in-pkg.txtar new file mode 100644 index 000000000000..9993f94040bb --- /dev/null +++ b/pkg/fanal/analyzer/language/golang/mod/testdata/wrong-gomod-in-pkg.txtar @@ -0,0 +1,9 @@ +Test case with invalid go.mod in GOPATH pkg/mod directory. +This tests handling of broken dependency go.mod files. + +-- go.mod -- +module github.com/org/repo + +go 1.17 + +require github.com/sad/sad v0.0.1 diff --git a/pkg/fanal/analyzer/language/java/gradle/lockfile.go b/pkg/fanal/analyzer/language/java/gradle/lockfile.go new file mode 100644 index 000000000000..7743046ccad8 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/lockfile.go @@ -0,0 +1,122 @@ +package gradle + +import ( + "context" + "fmt" + "io" + "io/fs" + "os" + "sort" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/gradle/lockfile" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeGradleLock, newGradleLockAnalyzer) +} + +const ( + version = 2 + fileNameSuffix = "gradle.lockfile" +) + +// gradleLockAnalyzer analyzes '*gradle.lockfile' +type gradleLockAnalyzer struct { + logger *log.Logger + parser language.Parser +} + +func newGradleLockAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &gradleLockAnalyzer{ + logger: log.WithPrefix("gradle"), + parser: lockfile.NewParser(), + }, nil +} + +func (a gradleLockAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + poms, err := a.parsePoms() + if err != nil { + a.logger.Warn("Unable to get licenses and dependencies", log.Err(err)) + } + + required := func(_ string, _ fs.DirEntry) bool { + // Parse all required files: `*gradle.lockfile` (from a.Required func) + input.FilePatterns.Match() + return true + } + + var apps []types.Application + err = fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error { + var app *types.Application + app, err = language.Parse(ctx, types.Gradle, filePath, r, a.parser) + if err != nil { + return xerrors.Errorf("%s parse error: %w", filePath, err) + } + + if app == nil { + return nil + } + + pkgs := lo.SliceToMap(app.Packages, func(lib types.Package) (string, struct{}) { + return lib.ID, struct{}{} + }) + + for i, lib := range app.Packages { + pom := poms[lib.ID] + + // Fill licenses from pom file + if len(pom.Licenses.License) > 0 { + app.Packages[i].Licenses = xslices.Map(pom.Licenses.License, func(license License) string { + return license.Name + }) + } + + // File child deps from pom file + var deps []string + for _, dep := range pom.Dependencies.Dependency { + id := packageID(dep.GroupID, dep.ArtifactID, dep.Version) + if _, ok := pkgs[id]; ok { + deps = append(deps, id) + } + } + sort.Strings(deps) + app.Packages[i].DependsOn = deps + } + + sort.Sort(app.Packages) + apps = append(apps, *app) + return nil + }) + if err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a gradleLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return strings.HasSuffix(filePath, fileNameSuffix) +} + +func (a gradleLockAnalyzer) Type() analyzer.Type { + return analyzer.TypeGradleLock +} + +func (a gradleLockAnalyzer) Version() int { + return version +} + +func packageID(groupId, artifactId, ver string) string { + return fmt.Sprintf("%s:%s:%s", groupId, artifactId, ver) +} diff --git a/pkg/fanal/analyzer/language/java/gradle/lockfile_test.go b/pkg/fanal/analyzer/language/java/gradle/lockfile_test.go new file mode 100644 index 000000000000..09414d42afcc --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/lockfile_test.go @@ -0,0 +1,159 @@ +package gradle + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_gradleLockAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + cacheDir string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + dir: "testdata/lockfiles/happy", + cacheDir: "testdata/cache", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Gradle, + FilePath: "gradle.lockfile", + Packages: types.Packages{ + { + ID: "junit:junit:4.13", + Name: "junit:junit", + Version: "4.13", + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + Licenses: []string{ + "Eclipse Public License 1.0", + }, + DependsOn: []string{ + "org.hamcrest:hamcrest-core:1.3", + }, + }, + { + ID: "org.hamcrest:hamcrest-core:1.3", + Name: "org.hamcrest:hamcrest-core", + Version: "1.3", + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path without cache", + dir: "testdata/lockfiles/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Gradle, + FilePath: "gradle.lockfile", + Packages: types.Packages{ + { + ID: "junit:junit:4.13", + Name: "junit:junit", + Version: "4.13", + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + { + ID: "org.hamcrest:hamcrest-core:1.3", + Name: "org.hamcrest:hamcrest-core", + Version: "1.3", + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 5, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty file", + dir: "testdata/lockfiles/empty", + want: &analyzer.AnalysisResult{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.cacheDir != "" { + t.Setenv("GRADLE_USER_HOME", tt.cacheDir) + } + + a, err := newGradleLockAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_nugetLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "default name", + filePath: "test/gradle.lockfile", + want: true, + }, + { + name: "name with prefix", + filePath: "test/settings-gradle.lockfile", + want: true, + }, + { + name: "txt", + filePath: "test/test.txt", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := gradleLockAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/java/gradle/pom.go b/pkg/fanal/analyzer/language/java/gradle/pom.go new file mode 100644 index 000000000000..de40ce743c61 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/pom.go @@ -0,0 +1,166 @@ +package gradle + +import ( + "encoding/xml" + "io" + "io/fs" + "os" + "path/filepath" + "runtime" + "strings" + + "golang.org/x/net/html/charset" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +type pomXML struct { + GroupId string `xml:"groupId"` + ArtifactId string `xml:"artifactId"` + Version string `xml:"version"` + Properties Properties `xml:"properties"` + Dependencies Dependencies `xml:"dependencies"` + Licenses Licenses `xml:"licenses"` +} +type Dependencies struct { + Dependency []Dependency `xml:"dependency"` +} + +type Dependency struct { + GroupID string `xml:"groupId"` + ArtifactID string `xml:"artifactId"` + Version string `xml:"version"` +} + +type Licenses struct { + License []License `xml:"license"` +} + +type License struct { + Name string `xml:"name"` +} + +type Properties map[string]string + +type property struct { + XMLName xml.Name + Value string `xml:",chardata"` +} + +func (props *Properties) UnmarshalXML(d *xml.Decoder, _ xml.StartElement) error { + *props = Properties{} + for { + var p property + err := d.Decode(&p) + if err == io.EOF { + break + } else if err != nil { + return xerrors.Errorf("XML decode error: %w", err) + } + + (*props)[p.XMLName.Local] = p.Value + } + return nil +} + +func (a gradleLockAnalyzer) parsePoms() (map[string]pomXML, error) { + cacheDir := a.detectCacheDir() + // Cache dir is not found + if cacheDir == "" { + return nil, nil + } + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Ext(path) == ".pom" + } + + var poms = make(map[string]pomXML) + err := fsutils.WalkDir(os.DirFS(cacheDir), ".", required, func(path string, _ fs.DirEntry, r io.Reader) error { + pom, err := parsePom(r, path) + if err != nil { + a.logger.Debug("Unable to parse pom", log.FilePath(path), log.Err(err)) + return nil + } + + if pom.ArtifactId != "" { + poms[packageID(pom.GroupId, pom.ArtifactId, pom.Version)] = pom + } + return nil + }) + if err != nil { + return nil, xerrors.Errorf("gradle licenses walk error: %w", err) + } + + return poms, nil +} + +func parsePom(r io.Reader, path string) (pomXML, error) { + pom := pomXML{} + decoder := xml.NewDecoder(r) + decoder.CharsetReader = charset.NewReaderLabel + if err := decoder.Decode(&pom); err != nil { + return pomXML{}, xerrors.Errorf("xml decode error: %w", err) + } + + // We only need pom's with licenses or dependencies + if len(pom.Licenses.License) == 0 && len(pom.Dependencies.Dependency) == 0 { + return pomXML{}, nil + } + + // If pom file doesn't contain GroupID or Version: + // find these values from filepath + // e.g. caches/modules-2/files-2.1/com.google.code.gson/gson/2.9.1/f0cf3edcef8dcb74d27cb427544a309eb718d772/gson-2.9.1.pom + dirs := strings.Split(filepath.ToSlash(path), "/") + if pom.GroupId == "" { + pom.GroupId = dirs[len(dirs)-5] + } + if pom.Version == "" { + pom.Version = dirs[len(dirs)-3] + } + + if err := pom.resolveDependencyVersions(); err != nil { + return pomXML{}, xerrors.Errorf("unable to resolve dependency version: %w", err) + } + + return pom, nil +} + +// resolveDependencyVersions resolves versions from properties +func (pom *pomXML) resolveDependencyVersions() error { + for i, dep := range pom.Dependencies.Dependency { + if strings.HasPrefix(dep.Version, "${") && strings.HasSuffix(dep.Version, "}") { + dep.Version = strings.TrimPrefix(strings.TrimSuffix(dep.Version, "}"), "${") + if resolvedVer, ok := pom.Properties[dep.Version]; ok { + pom.Dependencies.Dependency[i].Version = resolvedVer + } else if dep.Version == "${project.version}" { + pom.Dependencies.Dependency[i].Version = dep.Version + } else { + // We use simplified logic to resolve properties. + // If necessary, update and use the logic for maven pom's + return xerrors.Errorf("Unable to resolve %q version. Please open a new discussion to update the Trivy logic.", dep.Version) + } + } + } + return nil +} + +func (a gradleLockAnalyzer) detectCacheDir() string { + // https://docs.gradle.org/current/userguide/directory_layout.html + dir := os.Getenv("GRADLE_USER_HOME") + if dir == "" { + if runtime.GOOS == "windows" { + dir = filepath.Join(os.Getenv("%HOMEPATH%"), ".gradle") + } else { + dir = filepath.Join(os.Getenv("HOME"), ".gradle") + } + } + dir = filepath.Join(dir, "caches") + + if !fsutils.DirExists(dir) { + a.logger.Debug("Unable to get licenses and dependencies. Gradle cache dir doesn't exist.") + return "" + } + return dir +} diff --git a/pkg/fanal/analyzer/language/java/gradle/pom_test.go b/pkg/fanal/analyzer/language/java/gradle/pom_test.go new file mode 100644 index 000000000000..90591e96d66a --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/pom_test.go @@ -0,0 +1,102 @@ +package gradle + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_parsePom(t *testing.T) { + tests := []struct { + name string + inputFile string + inputPath string + want pomXML + }{ + { + name: "happy path", + inputFile: filepath.Join("testdata", "poms", "happy.pom"), + inputPath: "cache/caches/modules-2/files-2.1/org.example/example-core/1.0/872e413497b906e7c9fa85ccc96046c5d1ef7ece/example-core-1.0.pom", + want: pomXML{ + GroupId: "org.example", + ArtifactId: "example-core", + Version: "1.0.0", + Licenses: Licenses{ + License: []License{ + { + Name: "Apache License, Version 2.0", + }, + }, + }, + Dependencies: Dependencies{ + Dependency: []Dependency{ + { + GroupID: "org.example", + ArtifactID: "example-api", + Version: "2.0.0", + }, + }, + }, + }, + }, + { + name: "happy path. Take GroupID and Version from path", + inputFile: filepath.Join("testdata", "poms", "without-groupid-and-version.pom"), + inputPath: "cache/caches/modules-2/files-2.1/org.example/example-core/1.0.0/872e413497b906e7c9fa85ccc96046c5d1ef7ece/example-core-1.0.pom", + want: pomXML{ + GroupId: "org.example", + ArtifactId: "example-core", + Version: "1.0.0", + Licenses: Licenses{ + License: []License{ + { + Name: "Apache License, Version 2.0", + }, + }, + }, + }, + }, + { + name: "happy path. Dependency version as property.", + inputFile: filepath.Join("testdata", "poms", "dep-version-as-property.pom"), + inputPath: "cache/caches/modules-2/files-2.1/org.example/example-core/1.0.0/872e413497b906e7c9fa85ccc96046c5d1ef7ece/example-core-1.0.pom", + want: pomXML{ + GroupId: "org.example", + ArtifactId: "example-core", + Version: "1.0.0", + Properties: Properties{ + "coreVersion": "2.0.1", + }, + Dependencies: Dependencies{ + Dependency: []Dependency{ + { + GroupID: "org.example", + ArtifactID: "example-api", + Version: "2.0.1", + }, + }, + }, + }, + }, + { + name: "happy path. Dependency version as property.", + inputFile: filepath.Join("testdata", "poms", "without-licenses-and-deps.pom"), + inputPath: "cache/caches/modules-2/files-2.1/org.example/example-core/1.0.0/872e413497b906e7c9fa85ccc96046c5d1ef7ece/example-core-1.0.pom", + want: pomXML{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + pom, err := parsePom(f, tt.inputPath) + require.NoError(t, err) + + require.Equal(t, tt.want, pom) + }) + } +} diff --git a/pkg/fanal/analyzer/language/java/gradle/testdata/cache/caches/modules-2/files-2.1/junit/junit/4.13/5c17760663fae422643fc859fd352c68a1d91bfc/junit-4.13.pom b/pkg/fanal/analyzer/language/java/gradle/testdata/cache/caches/modules-2/files-2.1/junit/junit/4.13/5c17760663fae422643fc859fd352c68a1d91bfc/junit-4.13.pom new file mode 100644 index 000000000000..40d49278c416 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/testdata/cache/caches/modules-2/files-2.1/junit/junit/4.13/5c17760663fae422643fc859fd352c68a1d91bfc/junit-4.13.pom @@ -0,0 +1,587 @@ + + + 4.0.0 + + junit + junit + 4.13 + + JUnit + JUnit is a unit testing framework for Java, created by Erich Gamma and Kent Beck. + http://junit.org + 2002 + + JUnit + http://www.junit.org + + + + Eclipse Public License 1.0 + http://www.eclipse.org/legal/epl-v10.html + repo + + + + + + dsaff + David Saff + david@saff.net + + + kcooney + Kevin Cooney + kcooney@google.com + + + stefanbirkner + Stefan Birkner + mail@stefan-birkner.de + + + marcphilipp + Marc Philipp + mail@marcphilipp.de + + + + + JUnit contributors + JUnit + team@junit.org + https://github.com/junit-team/junit4/graphs/contributors + + developers + + + + + + 3.0.4 + + + + scm:git:git://github.com/junit-team/junit4.git + scm:git:git@github.com:junit-team/junit4.git + http://github.com/junit-team/junit4/tree/master + r4.13 + + + github + https://github.com/junit-team/junit4/issues + + + travis + https://travis-ci.org/junit-team/junit4 + + + https://github.com/junit-team/junit4/wiki/Download-and-Install + + junit-snapshot-repo + Nexus Snapshot Repository + https://oss.sonatype.org/content/repositories/snapshots/ + + + junit-releases-repo + Nexus Release Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + junit.github.io + gitsite:git@github.com/junit-team/junit4.git + + + + + 1.5 + 2.19.1 + 1.3 + ISO-8859-1 + + 67893CC4 + + + + + org.hamcrest + hamcrest-core + ${hamcrestVersion} + + + + org.hamcrest + hamcrest-library + ${hamcrestVersion} + test + + + + + + + ${project.basedir}/src/main/resources + + + ${project.basedir} + + LICENSE-junit.txt + + + + + + + + maven-enforcer-plugin + 1.4 + + + enforce-versions + initialize + + enforce + + + true + + + + Current version of Maven ${maven.version} required to build the project + should be ${project.prerequisites.maven}, or higher! + + [${project.prerequisites.maven},) + + + Current JDK version ${java.version} should be ${jdkVersion}, or higher! + + ${jdkVersion} + + + Best Practice is to never define repositories in pom.xml (use a repository + manager instead). + + + + No Snapshots Dependencies Allowed! + + + + + + + + + com.google.code.maven-replacer-plugin + replacer + 1.5.3 + + + process-sources + + replace + + + + + false + ${project.build.sourceDirectory}/junit/runner/Version.java.template + ${project.build.sourceDirectory}/junit/runner/Version.java + false + @version@ + ${project.version} + + + + + maven-compiler-plugin + 3.3 + + ${project.build.sourceEncoding} + ${jdkVersion} + ${jdkVersion} + ${jdkVersion} + ${jdkVersion} + 1.5 + true + true + true + true + + -Xlint:unchecked + + 128m + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + 1.14 + + + signature-check + test + + check + + + + org.codehaus.mojo.signature + java15 + 1.0 + + + + + + + + maven-surefire-plugin + ${surefireVersion} + + org/junit/tests/AllTests.java + true + false + + + + org.apache.maven.surefire + surefire-junit47 + ${surefireVersion} + + + + + + maven-source-plugin + 2.4 + + + + maven-javadoc-plugin + 2.10.3 + + ${basedir}/src/main/javadoc/stylesheet.css + protected + false + false + false + true + true + true + JUnit API + UTF-8 + en + ${jdkVersion} + + + api_${jdkVersion} + http://docs.oracle.com/javase/${jdkVersion}.0/docs/api/ + + + *.internal.* + true + 32m + 128m + true + true + + org.hamcrest:hamcrest-core:* + + + + + maven-release-plugin + 2.5.2 + + forked-path + false + -Pgenerate-docs,junit-release ${arguments} + r@{project.version} + + + + maven-site-plugin + 3.4 + + + com.github.stephenc.wagon + wagon-gitsite + 0.4.1 + + + org.apache.maven.doxia + doxia-module-markdown + 1.5 + + + + + maven-jar-plugin + 2.6 + + + false + + true + + + junit + + + + + + maven-clean-plugin + 2.6.1 + + + maven-deploy-plugin + 2.8.2 + + + maven-install-plugin + 2.5.2 + + + maven-resources-plugin + 2.7 + + + + + + + + maven-project-info-reports-plugin + 2.8 + + false + + + + + + index + dependency-info + modules + license + project-team + scm + issue-tracking + mailing-list + dependency-management + dependencies + dependency-convergence + cim + distribution-management + + + + + + maven-javadoc-plugin + 2.10.3 + + javadoc/latest + ${basedir}/src/main/javadoc/stylesheet.css + protected + false + false + false + true + true + true + JUnit API + UTF-8 + en + ${jdkVersion} + + + api_${jdkVersion} + http://docs.oracle.com/javase/${jdkVersion}.0/docs/api/ + + + junit.*,*.internal.* + true + 32m + 128m + true + true + + org.hamcrest:hamcrest-core:* + + + + + + javadoc + + + + + + + + + + junit-release + + + + + + maven-gpg-plugin + 1.6 + + + gpg-sign + verify + + sign + + + + + + + + + generate-docs + + + + + maven-source-plugin + + + attach-sources + prepare-package + + jar-no-fork + + + + + + maven-javadoc-plugin + + + attach-javadoc + package + + jar + + + + + + + + + restrict-doclint + + + [1.8,) + + + + + maven-compiler-plugin + + + -Xlint:unchecked + -Xdoclint:accessibility,reference,syntax + + + + + maven-javadoc-plugin + + -Xdoclint:accessibility -Xdoclint:reference + + + + + + + + maven-javadoc-plugin + + -Xdoclint:accessibility -Xdoclint:reference + + + + + + + java9 + + [1.9,) + + + + 1.6 + + + + + maven-javadoc-plugin + + 1.6 + + + + + + + + maven-javadoc-plugin + + 1.6 + + + + + + + diff --git a/pkg/fanal/analyzer/language/java/gradle/testdata/cache/caches/modules-2/files-2.1/org.hamcrest/hamcrest-core/1.3/872e413497b906e7c9fa85ccc96046c5d1ef7ece/hamcrest-core-1.3.pom b/pkg/fanal/analyzer/language/java/gradle/testdata/cache/caches/modules-2/files-2.1/org.hamcrest/hamcrest-core/1.3/872e413497b906e7c9fa85ccc96046c5d1ef7ece/hamcrest-core-1.3.pom new file mode 100644 index 000000000000..0721781c99a0 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/testdata/cache/caches/modules-2/files-2.1/org.hamcrest/hamcrest-core/1.3/872e413497b906e7c9fa85ccc96046c5d1ef7ece/hamcrest-core-1.3.pom @@ -0,0 +1,18 @@ + + + 4.0.0 + + + org.hamcrest + hamcrest-parent + 1.3 + + + hamcrest-core + jar + Hamcrest Core + + This is the core API of hamcrest matcher framework to be used by third-party framework providers. This includes the a foundation set of matcher implementations for common operations. + + diff --git a/pkg/fanal/analyzer/language/java/gradle/testdata/lockfiles/empty/gradle.lockfile b/pkg/fanal/analyzer/language/java/gradle/testdata/lockfiles/empty/gradle.lockfile new file mode 100644 index 000000000000..138b79c580c2 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/testdata/lockfiles/empty/gradle.lockfile @@ -0,0 +1,4 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +empty= \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/java/gradle/testdata/lockfiles/happy/gradle.lockfile b/pkg/fanal/analyzer/language/java/gradle/testdata/lockfiles/happy/gradle.lockfile new file mode 100644 index 000000000000..957bb968cc8f --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/testdata/lockfiles/happy/gradle.lockfile @@ -0,0 +1,6 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +junit:junit:4.13=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:1.3=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor,testAnnotationProcessor \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/java/gradle/testdata/poms/dep-version-as-property.pom b/pkg/fanal/analyzer/language/java/gradle/testdata/poms/dep-version-as-property.pom new file mode 100644 index 000000000000..7b2cd75b39f1 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/testdata/poms/dep-version-as-property.pom @@ -0,0 +1,21 @@ + + + 4.0.0 + + org.example + example-core + 1.0.0 + + + 2.0.1 + + + + + org.example + example-api + ${coreVersion} + + + diff --git a/pkg/fanal/analyzer/language/java/gradle/testdata/poms/happy.pom b/pkg/fanal/analyzer/language/java/gradle/testdata/poms/happy.pom new file mode 100644 index 000000000000..896fb1df5981 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/testdata/poms/happy.pom @@ -0,0 +1,23 @@ + + + 4.0.0 + + org.example + example-core + 1.0.0 + + + + Apache License, Version 2.0 + + + + + + org.example + example-api + 2.0.0 + + + diff --git a/pkg/fanal/analyzer/language/java/gradle/testdata/poms/without-groupid-and-version.pom b/pkg/fanal/analyzer/language/java/gradle/testdata/poms/without-groupid-and-version.pom new file mode 100644 index 000000000000..e94fcbaaaca2 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/testdata/poms/without-groupid-and-version.pom @@ -0,0 +1,19 @@ + + + 4.0.0 + + + org.example + example-parent + 1.3 + + + example-core + + + + Apache License, Version 2.0 + + + diff --git a/pkg/fanal/analyzer/language/java/gradle/testdata/poms/without-licenses-and-deps.pom b/pkg/fanal/analyzer/language/java/gradle/testdata/poms/without-licenses-and-deps.pom new file mode 100644 index 000000000000..5c83a401353d --- /dev/null +++ b/pkg/fanal/analyzer/language/java/gradle/testdata/poms/without-licenses-and-deps.pom @@ -0,0 +1,10 @@ + + + 4.0.0 + + org.example + example-core + 1.0.0 + + diff --git a/pkg/fanal/analyzer/language/java/jar/jar.go b/pkg/fanal/analyzer/language/java/jar/jar.go new file mode 100644 index 000000000000..8bf8c8846c24 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/jar/jar.go @@ -0,0 +1,98 @@ +package jar + +import ( + "context" + "io/fs" + "os" + "path/filepath" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/java/jar" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/javadb" + "github.com/aquasecurity/trivy/pkg/parallel" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeJar, newJavaLibraryAnalyzer) +} + +const version = 1 + +var requiredExtensions = []string{ + ".jar", + ".war", + ".ear", + ".par", +} + +// javaLibraryAnalyzer analyzes jar/war/ear/par files +type javaLibraryAnalyzer struct { + parallel int +} + +func newJavaLibraryAnalyzer(options analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &javaLibraryAnalyzer{ + parallel: options.Parallel, + }, nil +} + +func (a *javaLibraryAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + // TODO: think about the sonatype API and "--offline" + client, err := javadb.NewClient() + if err != nil { + return nil, xerrors.Errorf("Unable to initialize the Java DB: %s", err) + } + defer func() { _ = client.Close() }() + + // Skip analyzing JAR files as the nil client means the Java DB was not downloaded successfully. + if client == nil { + return nil, nil + } + + // It will be called on each JAR file + onFile := func(path string, info fs.FileInfo, r xio.ReadSeekerAt) (*types.Application, error) { + p := jar.NewParser(client, jar.WithSize(info.Size()), jar.WithFilePath(path)) + return language.ParsePackage(ctx, types.Jar, path, r, p, input.Options.FileChecksum) + } + + var apps []types.Application + onResult := func(app *types.Application) error { + if app == nil { + return nil + } + apps = append(apps, *app) + return nil + } + + if err = parallel.WalkDir(ctx, input.FS, ".", a.parallel, onFile, onResult); err != nil { + return nil, xerrors.Errorf("walk dir error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a *javaLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + ext := filepath.Ext(filePath) + for _, required := range requiredExtensions { + if strings.EqualFold(ext, required) { + return true + } + } + return false +} + +func (a *javaLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeJar +} + +func (a *javaLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/java/jar/jar_test.go b/pkg/fanal/analyzer/language/java/jar/jar_test.go new file mode 100644 index 000000000000..c8d5b3cbdd66 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/jar/jar_test.go @@ -0,0 +1,193 @@ +package jar + +import ( + "os" + "path/filepath" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/javadb" + "github.com/aquasecurity/trivy/pkg/mapfs" + + _ "modernc.org/sqlite" +) + +func Test_javaLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + includeChecksum bool + want *analyzer.AnalysisResult + }{ + { + name: "happy path (WAR file)", + inputFile: "testdata/test.war", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Jar, + FilePath: "testdata/test.war", + Packages: types.Packages{ + { + Name: "org.glassfish:javax.el", + FilePath: "testdata/test.war/WEB-INF/lib/javax.el-3.0.0.jar", + Version: "3.0.0", + }, + { + Name: "com.fasterxml.jackson.core:jackson-databind", + FilePath: "testdata/test.war/WEB-INF/lib/jackson-databind-2.9.10.6.jar", + Version: "2.9.10.6", + }, + { + Name: "com.fasterxml.jackson.core:jackson-annotations", + FilePath: "testdata/test.war/WEB-INF/lib/jackson-annotations-2.9.10.jar", + Version: "2.9.10", + }, + { + Name: "com.fasterxml.jackson.core:jackson-core", + FilePath: "testdata/test.war/WEB-INF/lib/jackson-core-2.9.10.jar", + Version: "2.9.10", + }, + { + Name: "org.slf4j:slf4j-api", + FilePath: "testdata/test.war/WEB-INF/lib/slf4j-api-1.7.30.jar", + Version: "1.7.30", + }, + { + Name: "com.cronutils:cron-utils", + FilePath: "testdata/test.war/WEB-INF/lib/cron-utils-9.1.2.jar", + Version: "9.1.2", + }, + { + Name: "org.apache.commons:commons-lang3", + FilePath: "testdata/test.war/WEB-INF/lib/commons-lang3-3.11.jar", + Version: "3.11", + }, + { + Name: "com.example:web-app", + FilePath: "testdata/test.war", + Version: "1.0-SNAPSHOT", + }, + }, + }, + }, + }, + }, + { + name: "happy path (PAR file)", + inputFile: "testdata/test.par", + includeChecksum: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Jar, + FilePath: "testdata/test.par", + Packages: types.Packages{ + { + Name: "com.fasterxml.jackson.core:jackson-core", + FilePath: "testdata/test.par/lib/jackson-core-2.9.10.jar", + Version: "2.9.10", + Digest: "sha1:d40913470259cfba6dcc90f96bcaa9bcff1b72e0", + }, + }, + }, + }, + }, + }, + { + name: "happy path (package found in trivy-java-db by sha1)", + inputFile: "testdata/test.jar", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Jar, + FilePath: "testdata/test.jar", + Packages: types.Packages{ + { + Name: "org.apache.tomcat.embed:tomcat-embed-websocket", + FilePath: "testdata/test.jar", + Version: "9.0.65", + }, + }, + }, + }, + }, + }, + { + name: "sad path", + inputFile: "testdata/test.txt", + want: &analyzer.AnalysisResult{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // init java-trivy-db with skip update + repo, err := name.NewTag(javadb.DefaultGHCRRepository) + require.NoError(t, err) + javadb.Init("testdata", []name.Reference{repo}, true, false, types.RegistryOptions{Insecure: false}) + + a := javaLibraryAnalyzer{} + ctx := t.Context() + + mfs := mapfs.New() + err = mfs.MkdirAll(filepath.Dir(tt.inputFile), os.ModePerm) + require.NoError(t, err) + err = mfs.WriteFile(tt.inputFile, tt.inputFile) + require.NoError(t, err) + + got, err := a.PostAnalyze(ctx, analyzer.PostAnalysisInput{ + FS: mfs, + Options: analyzer.AnalysisOptions{FileChecksum: tt.includeChecksum}, + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_javaLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "war", + filePath: "test/test.war", + want: true, + }, + { + name: "jar", + filePath: "test.jar", + want: true, + }, + { + name: "ear", + filePath: "a/b/c/d/test.ear", + want: true, + }, + { + name: "capital jar", + filePath: "a/b/c/d/test.JAR", + want: true, + }, + { + name: "zip", + filePath: "test.zip", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := javaLibraryAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/java/jar/testdata/java-db/metadata.json b/pkg/fanal/analyzer/language/java/jar/testdata/java-db/metadata.json new file mode 100644 index 000000000000..d810cb77937f --- /dev/null +++ b/pkg/fanal/analyzer/language/java/jar/testdata/java-db/metadata.json @@ -0,0 +1 @@ +{"Version":1,"NextUpdate":"0001-01-01T00:00:00Z","UpdatedAt":"0001-01-01T00:00:00Z","DownloadedAt":"0001-01-01T00:00:00Z"} diff --git a/pkg/fanal/analyzer/language/java/jar/testdata/java-db/trivy-java.db b/pkg/fanal/analyzer/language/java/jar/testdata/java-db/trivy-java.db new file mode 100644 index 000000000000..eb415104625d Binary files /dev/null and b/pkg/fanal/analyzer/language/java/jar/testdata/java-db/trivy-java.db differ diff --git a/pkg/fanal/analyzer/language/java/jar/testdata/test.jar b/pkg/fanal/analyzer/language/java/jar/testdata/test.jar new file mode 100644 index 000000000000..f2bd484e20d4 Binary files /dev/null and b/pkg/fanal/analyzer/language/java/jar/testdata/test.jar differ diff --git a/pkg/fanal/analyzer/language/java/jar/testdata/test.par b/pkg/fanal/analyzer/language/java/jar/testdata/test.par new file mode 100644 index 000000000000..a6d1e0122c2c Binary files /dev/null and b/pkg/fanal/analyzer/language/java/jar/testdata/test.par differ diff --git a/pkg/fanal/analyzer/language/java/jar/testdata/test.txt b/pkg/fanal/analyzer/language/java/jar/testdata/test.txt new file mode 100644 index 000000000000..9daeafb9864c --- /dev/null +++ b/pkg/fanal/analyzer/language/java/jar/testdata/test.txt @@ -0,0 +1 @@ +test diff --git a/pkg/fanal/analyzer/language/java/jar/testdata/test.war b/pkg/fanal/analyzer/language/java/jar/testdata/test.war new file mode 100644 index 000000000000..787d517174b7 Binary files /dev/null and b/pkg/fanal/analyzer/language/java/jar/testdata/test.war differ diff --git a/pkg/fanal/analyzer/language/java/pom/pom.go b/pkg/fanal/analyzer/language/java/pom/pom.go new file mode 100644 index 000000000000..f4e19a4379eb --- /dev/null +++ b/pkg/fanal/analyzer/language/java/pom/pom.go @@ -0,0 +1,67 @@ +package pom + +import ( + "context" + "os" + "path/filepath" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/java/pom" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&pomAnalyzer{}) +} + +const version = 1 + +// pomAnalyzer analyzes pom.xml +type pomAnalyzer struct{} + +func (a pomAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + filePath := filepath.Join(input.Dir, input.FilePath) + p := pom.NewParser(filePath, pom.WithOffline(input.Options.Offline)) + res, err := language.Analyze(ctx, types.Pom, input.FilePath, input.Content, p) + if err != nil { + return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err) + } + + // Mark integration test pom files for `maven-invoker-plugin` as Dev to skip them by default. + if isIntegrationTestDir(filePath) && res != nil { + for i := range res.Applications { + for j := range res.Applications[i].Packages { + res.Applications[i].Packages[j].Dev = true + } + } + } + + return res, nil +} + +func (a pomAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Base(filePath) == types.MavenPom +} + +func (a pomAnalyzer) Type() analyzer.Type { + return analyzer.TypePom +} + +func (a pomAnalyzer) Version() int { + return version +} + +// isIntegrationTestDir checks that pom file is in directory with integration tests of `maven-invoker-plugin` +// https://maven.apache.org/plugins/maven-invoker-plugin/usage.html +func isIntegrationTestDir(filePath string) bool { + dirs := strings.Split(filepath.ToSlash(filePath), "/") + // filepath pattern: `**/[src|target]/it/*/pom.xml` + if len(dirs) < 4 { + return false + } + return (dirs[len(dirs)-4] == "src" || dirs[len(dirs)-4] == "target") && dirs[len(dirs)-3] == "it" +} diff --git a/pkg/fanal/analyzer/language/java/pom/pom_test.go b/pkg/fanal/analyzer/language/java/pom/pom_test.go new file mode 100644 index 000000000000..81f052681a77 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/pom/pom_test.go @@ -0,0 +1,232 @@ +package pom + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_pomAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputDir string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/happy/pom.xml", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pom, + FilePath: "testdata/happy/pom.xml", + Packages: types.Packages{ + { + ID: "com.example:example:1.0.0::775be61e", + Name: "com.example:example", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "com.example:example-api:2.0.0::3f5226c1", + }, + }, + { + ID: "com.example:example-api:2.0.0::3f5226c1", + Name: "com.example:example-api", + Version: "2.0.0", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 28, + EndLine: 32, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy dir path", + inputDir: "testdata/happy", + inputFile: "pom.xml", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pom, + FilePath: "pom.xml", + Packages: types.Packages{ + { + ID: "com.example:example:1.0.0::775be61e", + Name: "com.example:example", + Version: "1.0.0", + Relationship: types.RelationshipRoot, + Licenses: []string{"Apache 2.0"}, + DependsOn: []string{ + "com.example:example-api:2.0.0::3f5226c1", + }, + }, + { + ID: "com.example:example-api:2.0.0::3f5226c1", + Name: "com.example:example-api", + Version: "2.0.0", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 28, + EndLine: 32, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for maven-invoker-plugin integration tests", + inputFile: "testdata/mark-as-dev/src/it/example/pom.xml", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pom, + FilePath: "testdata/mark-as-dev/src/it/example/pom.xml", + Packages: types.Packages{ + { + ID: "com.example:example:1.0.0::c6140fc9", + Name: "com.example:example", + Version: "1.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "com.example:example-api:@example.version@::ea8c6bb9", + }, + Dev: true, + }, + { + ID: "com.example:example-api:@example.version@::ea8c6bb9", + Name: "com.example:example-api", + Version: "@example.version@", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 28, + EndLine: 32, + }, + }, + Dev: true, + }, + }, + }, + }, + }, + }, + { + name: "unsupported requirement", + inputFile: "testdata/requirements/pom.xml", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pom, + FilePath: "testdata/requirements/pom.xml", + Packages: types.Packages{ + { + ID: "com.example:example:2.0.0::729d323a", + Name: "com.example:example", + Version: "2.0.0", + Licenses: []string{"Apache 2.0"}, + Relationship: types.RelationshipRoot, + }, + { + ID: "org.example:example-api::fc45c739", + Name: "org.example:example-api", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 21, + EndLine: 25, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "sad path", + inputFile: "testdata/broken/pom.xml", + wantErr: "xml decode error", + }, + { + name: "sad dir path", + inputDir: "testdata/broken", + inputFile: "pom.xml", + wantErr: "xml decode error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(filepath.Join(tt.inputDir, tt.inputFile)) + require.NoError(t, err) + defer f.Close() + + a := pomAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + Dir: tt.inputDir, + FilePath: tt.inputFile, + Content: f, + Options: analyzer.AnalysisOptions{ + Offline: true, + }, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_pomAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy", + filePath: "test/pom.xml", + want: true, + }, + { + name: "no extension", + filePath: "test/pom", + want: false, + }, + { + name: "json", + filePath: "test/pom.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := pomAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/java/pom/testdata/broken/pom.xml b/pkg/fanal/analyzer/language/java/pom/testdata/broken/pom.xml new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/pom/testdata/broken/pom.xml @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/java/pom/testdata/happy/pom.xml b/pkg/fanal/analyzer/language/java/pom/testdata/happy/pom.xml new file mode 100644 index 000000000000..aa5f1066295c --- /dev/null +++ b/pkg/fanal/analyzer/language/java/pom/testdata/happy/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + + com.example + example + 1.0.0 + + example + Example + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + knqyf263 + https://github.com/knqyf263 + + + + + + com.example + example-api + 2.0.0 + + + diff --git a/pkg/fanal/analyzer/language/java/pom/testdata/mark-as-dev/src/it/example/pom.xml b/pkg/fanal/analyzer/language/java/pom/testdata/mark-as-dev/src/it/example/pom.xml new file mode 100644 index 000000000000..3717c917a1f2 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/pom/testdata/mark-as-dev/src/it/example/pom.xml @@ -0,0 +1,34 @@ + + 4.0.0 + + com.example + example + 1.0.0 + + example + Example + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + knqyf263 + https://github.com/knqyf263 + + + + + + com.example + example-api + @example.version@ + + + diff --git a/pkg/fanal/analyzer/language/java/pom/testdata/requirements/pom.xml b/pkg/fanal/analyzer/language/java/pom/testdata/requirements/pom.xml new file mode 100644 index 000000000000..dc6d6ebc2240 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/pom/testdata/requirements/pom.xml @@ -0,0 +1,27 @@ + + 4.0.0 + + com.example + example + 2.0.0 + + example + Example + + + + Apache 2.0 + http://www.apache.org/licenses/LICENSE-2.0.html + repo + + + + + + org.example + example-api + (,1.0] + + + diff --git a/pkg/fanal/analyzer/language/java/sbt/lockfile.go b/pkg/fanal/analyzer/language/java/sbt/lockfile.go new file mode 100644 index 000000000000..4275d8b70093 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/sbt/lockfile.go @@ -0,0 +1,47 @@ +package sbt + +import ( + "context" + "os" + "path/filepath" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/sbt/lockfile" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&sbtDependencyLockAnalyzer{}) +} + +const version = 1 + +// sbtDependencyLockAnalyzer analyzes '*.sbt.lock' +type sbtDependencyLockAnalyzer struct{} + +func (a sbtDependencyLockAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + parser := lockfile.NewParser() + + res, err := language.Analyze(ctx, types.Sbt, input.FilePath, input.Content, parser) + + if err != nil { + return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err) + } + + return res, nil +} + +func (a sbtDependencyLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return types.SbtLock == filepath.Base(filePath) +} + +func (a sbtDependencyLockAnalyzer) Type() analyzer.Type { + return analyzer.TypeSbtLock +} + +func (a sbtDependencyLockAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/java/sbt/lockfile_test.go b/pkg/fanal/analyzer/language/java/sbt/lockfile_test.go new file mode 100644 index 000000000000..aaea6547d4f0 --- /dev/null +++ b/pkg/fanal/analyzer/language/java/sbt/lockfile_test.go @@ -0,0 +1,91 @@ +package sbt + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_sbtDependencyLockAnalyzer(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + }{ + { + name: "v1 lockfile", + inputFile: "testdata/v1/build.sbt.lock", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Sbt, + FilePath: "testdata/v1/build.sbt.lock", + Packages: types.Packages{ + { + ID: "org.apache.commons:commons-lang3:3.9", + Name: "org.apache.commons:commons-lang3", + Version: "3.9", + Locations: []types.Location{ + { + StartLine: 10, + EndLine: 25, + }, + }, + }, + { + ID: "org.scala-lang:scala-library:2.12.10", + Name: "org.scala-lang:scala-library", + Version: "2.12.10", + Locations: []types.Location{ + { + StartLine: 26, + EndLine: 41, + }, + }, + }, + { + ID: "org.typelevel:cats-core_2.12:2.9.0", + Name: "org.typelevel:cats-core_2.12", + Version: "2.9.0", + Locations: []types.Location{ + { + StartLine: 42, + EndLine: 57, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty lockfile", + inputFile: "testdata/empty/build.sbt.lock", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + + a := sbtDependencyLockAnalyzer{} + ctx := t.Context() + + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/java/sbt/testdata/empty/build.sbt.lock b/pkg/fanal/analyzer/language/java/sbt/testdata/empty/build.sbt.lock new file mode 100644 index 000000000000..6125547882da --- /dev/null +++ b/pkg/fanal/analyzer/language/java/sbt/testdata/empty/build.sbt.lock @@ -0,0 +1,10 @@ +{ + "lockVersion": 1, + "timestamp": "2024-06-05T13:41:10.992Z", + "configurations": [ + "compile", + "runtime", + "test" + ], + "dependencies": [] +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/java/sbt/testdata/v1/build.sbt.lock b/pkg/fanal/analyzer/language/java/sbt/testdata/v1/build.sbt.lock new file mode 100644 index 000000000000..26b5ef401aeb --- /dev/null +++ b/pkg/fanal/analyzer/language/java/sbt/testdata/v1/build.sbt.lock @@ -0,0 +1,59 @@ +{ + "lockVersion": 1, + "timestamp": "2024-06-05T13:41:10.992Z", + "configurations": [ + "compile", + "runtime", + "test" + ], + "dependencies": [ + { + "org": "org.apache.commons", + "name": "commons-lang3", + "version": "3.9", + "artifacts": [ + { + "name": "commons-lang3.jar", + "hash": "sha1:0122c7cee69b53ed4a7681c03d4ee4c0e2765da5" + } + ], + "configurations": [ + "test", + "compile", + "runtime" + ] + }, + { + "org": "org.scala-lang", + "name": "scala-library", + "version": "2.12.10", + "artifacts": [ + { + "name": "scala-library.jar", + "hash": "sha1:3509860bc2e5b3da001ed45aca94ffbe5694dbda" + } + ], + "configurations": [ + "test", + "compile", + "runtime" + ] + }, + { + "org" : "org.typelevel", + "name" : "cats-core_2.12", + "version" : "2.9.0", + "artifacts" : [ + { + "name" : "cats-core_2.12.jar", + "hash" : "sha1:844f21541d1809008586fbc1172dc02c96476639" + } + ], + "configurations" : [ + "compile", + "runtime", + "test" + ] + } + ] +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/julia/pkg/pkg.go b/pkg/fanal/analyzer/language/julia/pkg/pkg.go new file mode 100644 index 000000000000..3d0be09f122f --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/pkg.go @@ -0,0 +1,191 @@ +package pkgjl + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path/filepath" + "slices" + "sort" + + "github.com/BurntSushi/toml" + "github.com/samber/lo" + "golang.org/x/xerrors" + + julia "github.com/aquasecurity/trivy/pkg/dependency/parser/julia/manifest" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeJulia, newJuliaAnalyzer) +} + +const version = 1 + +var requiredFiles = []string{ + types.JuliaManifest, + types.JuliaProject, +} + +type juliaAnalyzer struct { + lockParser language.Parser + logger *log.Logger +} + +type Project struct { + Dependencies map[string]string `toml:"deps"` + Extras map[string]string `toml:"extras"` +} + +func newJuliaAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &juliaAnalyzer{ + lockParser: julia.NewParser(), + logger: log.WithPrefix("julia"), + }, nil +} + +func (a juliaAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.JuliaManifest || input.FilePatterns.Match(path) + } + + err := fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error { + // Parse Manifest.toml + app, err := a.parseJuliaManifest(ctx, path, r) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + // Parse Project.toml alongside Manifest.toml to identify the direct dependencies. This mutates `app`. + if err = a.analyzeDependencies(input.FS, filepath.Dir(path), app); err != nil { + a.logger.Warn("Unable to parse file to analyze dependencies", + log.FilePath(filepath.Join(filepath.Dir(path), types.JuliaProject)), log.Err(err)) + } + + sort.Sort(app.Packages) + apps = append(apps, *app) + return nil + }) + if err != nil { + return nil, xerrors.Errorf("julia walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a juliaAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + return slices.Contains(requiredFiles, fileName) +} + +func (a juliaAnalyzer) Type() analyzer.Type { + return analyzer.TypeJulia +} + +func (a juliaAnalyzer) Version() int { + return version +} + +func (a juliaAnalyzer) parseJuliaManifest(ctx context.Context, path string, r io.Reader) (*types.Application, error) { + return language.Parse(ctx, types.Julia, path, r, a.lockParser) +} + +func (a juliaAnalyzer) analyzeDependencies(fsys fs.FS, dir string, app *types.Application) error { + deps, devDeps, err := a.getProjectDeps(fsys, dir) + if err != nil { + return err + } + + pkgs := walkDependencies(deps, app.Packages, false) + devPkgs := walkDependencies(devDeps, app.Packages, true) + app.Packages = append(pkgs, devPkgs...) + return nil +} + +// getProjectDeps parses project.toml and returns root and dev dependencies. +func (a juliaAnalyzer) getProjectDeps(fsys fs.FS, dir string) (map[string]string, map[string]string, error) { + projectPath := filepath.Join(dir, types.JuliaProject) + project, err := parseJuliaProject(fsys, projectPath) + if errors.Is(err, fs.ErrNotExist) { + a.logger.Debug("Julia project not found", log.String("PROJECT_PATH", projectPath)) + return nil, nil, nil + } else if err != nil { + return nil, nil, xerrors.Errorf("unable to parse %s: %w", projectPath, err) + } + return project.Dependencies, project.Extras, nil +} + +// Parses Project.toml +func parseJuliaProject(fsys fs.FS, path string) (Project, error) { + proj := Project{} + f, err := fsys.Open(path) + if err != nil { + return proj, xerrors.Errorf("file open error: %w", err) + } + defer func() { _ = f.Close() }() + + if _, err = toml.NewDecoder(f).Decode(&proj); err != nil { + return proj, xerrors.Errorf("decode error: %w", err) + } + return proj, nil +} + +// Marks the given direct dependencies as direct, then marks those packages' dependencies as indirect. +// Marks all encountered packages' Dev flag according to `dev`. +// Modifies the packages in `allPackages`. +func walkDependencies(directDeps map[string]string, allPackages types.Packages, dev bool) []types.Package { + pkgsByID := lo.SliceToMap(allPackages, func(pkg types.Package) (string, types.Package) { + return pkg.ID, pkg + }) + + // Identify direct dependencies + // Everything in `directDeps` is assumed to be direct + visited := make(map[string]types.Package) + for _, uuid := range directDeps { + if pkg, ok := pkgsByID[uuid]; ok { + pkg.Relationship = types.RelationshipDirect + pkg.Indirect = false + pkg.Dev = dev + visited[pkg.ID] = pkg + } + } + + // Identify indirect dependencies + for _, pkg := range visited { + walkIndirectDependencies(pkg, pkgsByID, visited) + } + + return lo.Values(visited) +} + +// Marks all indirect dependencies as indirect. Starts from `rootPkg`. Visited deps are added to `visited`. +func walkIndirectDependencies(rootPkg types.Package, allPkgIDs, visited map[string]types.Package) { + for _, pkgID := range rootPkg.DependsOn { + if _, ok := visited[pkgID]; ok { + continue + } + + dep, ok := allPkgIDs[pkgID] + if !ok { + continue + } + + dep.Relationship = types.RelationshipIndirect + dep.Indirect = true + dep.Dev = rootPkg.Dev + visited[dep.ID] = dep + walkIndirectDependencies(dep, allPkgIDs, visited) + } +} diff --git a/pkg/fanal/analyzer/language/julia/pkg/pkg_test.go b/pkg/fanal/analyzer/language/julia/pkg/pkg_test.go new file mode 100644 index 000000000000..67971ab004a3 --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/pkg_test.go @@ -0,0 +1,229 @@ +package pkgjl + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_juliaAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + dir: "testdata/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Julia, + FilePath: "Manifest.toml", + Packages: types.Packages{ + { + ID: "ade2ca70-3891-5945-98fb-dc099432e06a", + Name: "Dates", + Version: "1.9.0", + Relationship: types.RelationshipDirect, + Indirect: false, + Locations: []types.Location{{StartLine: 7, EndLine: 9}}, + DependsOn: []string{"de0858da-6303-5e67-8744-51eddeeeb8d7"}, + }, + { + ID: "d9a60922-03b4-4a1b-81be-b8d05b827236", + Name: "DevDep", + Version: "1.0.0", + Relationship: types.RelationshipDirect, + Indirect: false, + Dev: true, + Locations: []types.Location{{StartLine: 65, EndLine: 68}}, + DependsOn: []string{"b637660b-5035-4894-8335-b3805a4b50d8"}, + }, + { + ID: "682c06a0-de6a-54ab-a142-c8b1cf79cde6", + Name: "JSON", + Version: "0.21.4", + Relationship: types.RelationshipDirect, + Indirect: false, + Locations: []types.Location{{StartLine: 11, EndLine: 15}}, + DependsOn: []string{ + "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5", + "69de0a69-1ddd-5017-9359-2bf0b02dc9f0", + "a63ad114-7e13-5084-954f-fe012c677804", + "ade2ca70-3891-5945-98fb-dc099432e06a", + }, + }, + { + ID: "b637660b-5035-4894-8335-b3805a4b50d8", + Name: "IndirectDevDep", + Version: "2.0.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + Dev: true, + Locations: []types.Location{{StartLine: 70, EndLine: 72}}, + }, + { + ID: "a63ad114-7e13-5084-954f-fe012c677804", + Name: "Mmap", + Version: "1.9.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 17, EndLine: 18}}, + }, + { + ID: "69de0a69-1ddd-5017-9359-2bf0b02dc9f0", + Name: "Parsers", + Version: "2.5.10", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 20, EndLine: 24}}, + DependsOn: []string{ + "ade2ca70-3891-5945-98fb-dc099432e06a", + "aea7be01-6a6a-4083-8856-8a6e6704d82a", + "cf7118a7-6976-5b1a-9a39-7adc72f591a4", + }, + }, + { + ID: "aea7be01-6a6a-4083-8856-8a6e6704d82a", + Name: "PrecompileTools", + Version: "1.1.1", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 26, EndLine: 30}}, + DependsOn: []string{"21216c6a-2e73-6563-6e65-726566657250"}, + }, + { + ID: "21216c6a-2e73-6563-6e65-726566657250", + Name: "Preferences", + Version: "1.4.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 32, EndLine: 36}}, + DependsOn: []string{"fa267f1f-6049-4f14-aa54-33bafae1ed76"}, + }, + { + ID: "de0858da-6303-5e67-8744-51eddeeeb8d7", + Name: "Printf", + Version: "1.9.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 38, EndLine: 40}}, + DependsOn: []string{"4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"}, + }, + { + ID: "9a3f8284-a2c9-5f02-9a11-845980a1fd5c", + Name: "Random", + Version: "1.9.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 42, EndLine: 44}}, + DependsOn: []string{ + "9e88b42a-f829-5b0c-bbe9-9e923198166b", "ea8e919c-243c-51af-8825-aaa63cd721ce", + }, + }, + { + ID: "ea8e919c-243c-51af-8825-aaa63cd721ce", + Name: "SHA", + Version: "0.7.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 46, EndLine: 48}}, + }, + { + ID: "9e88b42a-f829-5b0c-bbe9-9e923198166b", + Name: "Serialization", + Version: "1.9.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 50, EndLine: 51}}, + }, + { + ID: "fa267f1f-6049-4f14-aa54-33bafae1ed76", + Name: "TOML", + Version: "1.0.3", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 53, EndLine: 56}}, + DependsOn: []string{"ade2ca70-3891-5945-98fb-dc099432e06a"}, + }, + { + ID: "cf7118a7-6976-5b1a-9a39-7adc72f591a4", + Name: "UUIDs", + Version: "1.9.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 58, EndLine: 60}}, + DependsOn: []string{ + "9a3f8284-a2c9-5f02-9a11-845980a1fd5c", "ea8e919c-243c-51af-8825-aaa63cd721ce", + }, + }, + { + ID: "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5", + Name: "Unicode", + Version: "1.9.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + Locations: []types.Location{{StartLine: 62, EndLine: 63}}, + }, + }, + }, + }, + }, + }, + { + name: "no_deps_v1.6", + dir: "testdata/no_deps_v1.6", + want: &analyzer.AnalysisResult{}, + }, + { + name: "dep_ext_v1.9", + dir: "testdata/dep_ext_v1.9", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Julia, + FilePath: "Manifest.toml", + Packages: types.Packages{ + { + ID: "621f4979-c628-5d54-868e-fcf4e3e8185c", + Name: "AbstractFFTs", + Version: "1.3.1", + Relationship: types.RelationshipDirect, + Indirect: false, + Locations: []types.Location{{StartLine: 7, EndLine: 10}}, + DependsOn: nil, + }, + }, + }, + }, + }, + }, + { + name: "no_manifest", + dir: "testdata/no_manifest", + want: &analyzer.AnalysisResult{ + Applications: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newJuliaAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/julia/pkg/testdata/dep_ext_v1.9/Manifest.toml b/pkg/fanal/analyzer/language/julia/pkg/testdata/dep_ext_v1.9/Manifest.toml new file mode 100644 index 000000000000..c3b22a724c15 --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/testdata/dep_ext_v1.9/Manifest.toml @@ -0,0 +1,16 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.9.0" +manifest_format = "2.0" +project_hash = "f0a796fb78285c02ad123fec6e14c8bac09a2ccc" + +[[deps.AbstractFFTs]] +git-tree-sha1 = "16b6dbc4cf7caee4e1e75c49485ec67b667098a0" +uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" +version = "1.3.1" + + [deps.AbstractFFTs.extensions] + AbstractFFTsChainRulesCoreExt = "ChainRulesCore" + + [deps.AbstractFFTs.weakdeps] + ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" diff --git a/pkg/fanal/analyzer/language/julia/pkg/testdata/dep_ext_v1.9/Project.toml b/pkg/fanal/analyzer/language/julia/pkg/testdata/dep_ext_v1.9/Project.toml new file mode 100644 index 000000000000..7d99fdb4f598 --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/testdata/dep_ext_v1.9/Project.toml @@ -0,0 +1,9 @@ +name = "packageName" +uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773" +version = "0.1.0" + +[deps] +AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c" + +[compat] +AbstractFFTs = "1.3" diff --git a/pkg/fanal/analyzer/language/julia/pkg/testdata/happy/Manifest.toml b/pkg/fanal/analyzer/language/julia/pkg/testdata/happy/Manifest.toml new file mode 100644 index 000000000000..316bf1497e44 --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/testdata/happy/Manifest.toml @@ -0,0 +1,72 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.9.0" +manifest_format = "2.0" +project_hash = "f65b9de676a27ce78ee011db6d477b3d44d1a7c5" + +[[deps.Dates]] +deps = ["Printf"] +uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" + +[[deps.JSON]] +deps = ["Dates", "Mmap", "Parsers", "Unicode"] +git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a" +uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" +version = "0.21.4" + +[[deps.Mmap]] +uuid = "a63ad114-7e13-5084-954f-fe012c677804" + +[[deps.Parsers]] +deps = ["Dates", "PrecompileTools", "UUIDs"] +git-tree-sha1 = "a5aef8d4a6e8d81f171b2bd4be5265b01384c74c" +uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" +version = "2.5.10" + +[[deps.PrecompileTools]] +deps = ["Preferences"] +git-tree-sha1 = "259e206946c293698122f63e2b513a7c99a244e8" +uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +version = "1.1.1" + +[[deps.Preferences]] +deps = ["TOML"] +git-tree-sha1 = "7eb1686b4f04b82f96ed7a4ea5890a4f0c7a09f1" +uuid = "21216c6a-2e73-6563-6e65-726566657250" +version = "1.4.0" + +[[deps.Printf]] +deps = ["Unicode"] +uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7" + +[[deps.Random]] +deps = ["SHA", "Serialization"] +uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[[deps.SHA]] +uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" +version = "0.7.0" + +[[deps.Serialization]] +uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b" + +[[deps.TOML]] +deps = ["Dates"] +uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" +version = "1.0.3" + +[[deps.UUIDs]] +deps = ["Random", "SHA"] +uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" + +[[deps.Unicode]] +uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" + +[[deps.DevDep]] +deps = ["IndirectDevDep"] +uuid = "d9a60922-03b4-4a1b-81be-b8d05b827236" +version = "1.0.0" + +[[deps.IndirectDevDep]] +uuid = "b637660b-5035-4894-8335-b3805a4b50d8" +version = "2.0.0" diff --git a/pkg/fanal/analyzer/language/julia/pkg/testdata/happy/Project.toml b/pkg/fanal/analyzer/language/julia/pkg/testdata/happy/Project.toml new file mode 100644 index 000000000000..f24e31bc961a --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/testdata/happy/Project.toml @@ -0,0 +1,17 @@ +name = "packageName" +uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773" +version = "0.1.0" + +[deps] +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" + +[compat] +JSON = "0.21" +julia = "1.8" + +[extras] +DevDep = "d9a60922-03b4-4a1b-81be-b8d05b827236" + +[targets] +test = ["DevDep"] diff --git a/pkg/fanal/analyzer/language/julia/pkg/testdata/no_deps_v1.6/Manifest.toml b/pkg/fanal/analyzer/language/julia/pkg/testdata/no_deps_v1.6/Manifest.toml new file mode 100644 index 000000000000..f45eecff031f --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/testdata/no_deps_v1.6/Manifest.toml @@ -0,0 +1,2 @@ +# This file is machine-generated - editing it directly is not advised + diff --git a/pkg/fanal/analyzer/language/julia/pkg/testdata/no_deps_v1.6/Project.toml b/pkg/fanal/analyzer/language/julia/pkg/testdata/no_deps_v1.6/Project.toml new file mode 100644 index 000000000000..84c8138906dd --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/testdata/no_deps_v1.6/Project.toml @@ -0,0 +1,3 @@ +name = "packageName" +uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773" +version = "0.1.0" diff --git a/pkg/fanal/analyzer/language/julia/pkg/testdata/no_manifest/Project.toml b/pkg/fanal/analyzer/language/julia/pkg/testdata/no_manifest/Project.toml new file mode 100644 index 000000000000..dec02c4d6cd3 --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/testdata/no_manifest/Project.toml @@ -0,0 +1,2 @@ +[deps] +A = "ead4f63c-334e-11e9-00e6-e7f0a5f21b62" diff --git a/pkg/fanal/analyzer/language/julia/pkg/testdata/shadowed_dep_v1.9/Manifest.toml b/pkg/fanal/analyzer/language/julia/pkg/testdata/shadowed_dep_v1.9/Manifest.toml new file mode 100644 index 000000000000..dd4ea00b943d --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/testdata/shadowed_dep_v1.9/Manifest.toml @@ -0,0 +1,16 @@ +# This file is machine-generated - editing it directly is not advised + +julia_version = "1.9.0" +manifest_format = "2.0" +project_hash = "f0a796fb78285c02ad123fec6e14c8bac09a2ccc" + +[[deps.A]] +uuid = "ead4f63c-334e-11e9-00e6-e7f0a5f21b60" + + [deps.A.deps] + B = "f41f7b98-334e-11e9-1257-49272045fb24" + +[[deps.B]] +uuid = "f41f7b98-334e-11e9-1257-49272045fb24" +[[deps.B]] +uuid = "edca9bc6-334e-11e9-3554-9595dbb4349c" diff --git a/pkg/fanal/analyzer/language/julia/pkg/testdata/shadowed_dep_v1.9/Project.toml b/pkg/fanal/analyzer/language/julia/pkg/testdata/shadowed_dep_v1.9/Project.toml new file mode 100644 index 000000000000..24fe6178c480 --- /dev/null +++ b/pkg/fanal/analyzer/language/julia/pkg/testdata/shadowed_dep_v1.9/Project.toml @@ -0,0 +1,7 @@ +name = "packageName" +uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773" +version = "0.1.0" + +[deps] +A = "ead4f63c-334e-11e9-00e6-e7f0a5f21b60" +B = "edca9bc6-334e-11e9-3554-9595dbb4349c" diff --git a/pkg/fanal/analyzer/language/nodejs/bun/bun.go b/pkg/fanal/analyzer/language/nodejs/bun/bun.go new file mode 100644 index 000000000000..43bd5756d76b --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/bun.go @@ -0,0 +1,158 @@ +package bun + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "sort" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/bun" + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/packagejson" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xpath "github.com/aquasecurity/trivy/pkg/x/path" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeBun, newBunLibraryAnalyzer) +} + +const ( + version = 1 +) + +type bunLibraryAnalyzer struct { + logger *log.Logger + lockParser language.Parser + packageParser *packagejson.Parser +} + +func newBunLibraryAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &bunLibraryAnalyzer{ + logger: log.WithPrefix("bun"), + lockParser: bun.NewParser(), + packageParser: packagejson.NewParser(), + }, nil +} + +func (a bunLibraryAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + // Parse bun.lock + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.BunLock || input.FilePatterns.Match(path) + } + + var apps []types.Application + err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, _ io.Reader) error { + // Find all licenses from package.json files under node_modules dirs + licenses, err := a.findLicenses(input.FS, filePath) + if err != nil { + a.logger.Error("Unable to collect licenses", log.Err(err)) + licenses = make(map[string][]string) + } + + app, err := a.parseBunLock(ctx, input.FS, filePath) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + // Fill licenses + for i, lib := range app.Packages { + if ll, ok := licenses[lib.ID]; ok { + app.Packages[i].Licenses = ll + } + } + sort.Sort(app.Packages) + apps = append(apps, *app) + return nil + }) + if err != nil { + return nil, xerrors.Errorf("bun.lock/package.json walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a bunLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + if fileName == types.BunLock { + return true + } + + // Save package.json files only from the `node_modules` directory. + // Required to search for licenses. + if fileName == types.NpmPkg && xpath.Contains(filePath, "node_modules") { + return true + } + return false +} + +func (a bunLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeBun +} + +func (a bunLibraryAnalyzer) Version() int { + return version +} + +func (a bunLibraryAnalyzer) parseBunLock(ctx context.Context, fsys fs.FS, filePath string) (*types.Application, error) { + f, err := fsys.Open(filePath) + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + defer func() { _ = f.Close() }() + + file, ok := f.(xio.ReadSeekCloserAt) + if !ok { + return nil, xerrors.New("type assertion error: file does not implement xio.ReadSeekCloserAt") + } + + // parse bun.lock + return language.Parse(ctx, types.Bun, filePath, file, a.lockParser) +} + +func (a bunLibraryAnalyzer) findLicenses(fsys fs.FS, lockPath string) (map[string][]string, error) { + dir := path.Dir(lockPath) + root := path.Join(dir, "node_modules") + if _, err := fs.Stat(fsys, root); errors.Is(err, fs.ErrNotExist) { + a.logger.Info(`To collect the license information of packages, "bun install" needs to be performed beforehand`, + log.String("dir", root)) + return nil, nil + } + + // Parse package.json + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.NpmPkg + } + + // Traverse node_modules dir and find licenses + // Note that fs.FS is always slashed regardless of the platform, + // and path.Join should be used rather than filepath.Join. + licenses := make(map[string][]string) + err := fsutils.WalkDir(fsys, root, required, func(filePath string, _ fs.DirEntry, r io.Reader) error { + pkg, err := a.packageParser.Parse(r) + if err != nil { + return xerrors.Errorf("unable to parse %q: %w", filePath, err) + } + + licenses[pkg.ID] = pkg.Licenses + return nil + }) + if err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + return licenses, nil +} diff --git a/pkg/fanal/analyzer/language/nodejs/bun/bun_test.go b/pkg/fanal/analyzer/language/nodejs/bun/bun_test.go new file mode 100644 index 000000000000..8b43d4bdac4f --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/bun_test.go @@ -0,0 +1,206 @@ +package bun + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, true) + os.Exit(m.Run()) +} + +func Test_bunLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + want *analyzer.AnalysisResult + }{ + { + name: "with node_modules", + dir: "testdata/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Bun, + FilePath: "bun.lock", + Packages: types.Packages{ + { + ID: "@types/bun@1.2.13", + Name: "@types/bun", + Version: "1.2.13", + Dev: true, + Relationship: types.RelationshipDirect, + DependsOn: []string{"bun-types@1.2.13"}, + Licenses: []string{"MIT"}, + Locations: types.Locations{types.Location{StartLine: 18, EndLine: 18}}, + }, + { + ID: "typescript@5.8.3", + Name: "typescript", + Version: "5.8.3", + Relationship: types.RelationshipDirect, + Licenses: []string{"Apache-2.0"}, + Locations: types.Locations{types.Location{StartLine: 24, EndLine: 24}}, + }, + { + ID: "zod@3.24.4", + Name: "zod", + Version: "3.24.4", + Relationship: types.RelationshipDirect, + Licenses: []string{"MIT"}, + Locations: types.Locations{types.Location{StartLine: 28, EndLine: 28}}, + }, + { + ID: "@types/node@22.15.18", + Name: "@types/node", + Version: "22.15.18", + Dev: true, + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"undici-types@6.21.0"}, + Licenses: []string{"MIT"}, + Locations: types.Locations{types.Location{StartLine: 20, EndLine: 20}}, + }, + { + ID: "bun-types@1.2.13", + Name: "bun-types", + Version: "1.2.13", + Dev: true, + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"@types/node@22.15.18"}, + Licenses: []string{"MIT"}, + Locations: types.Locations{types.Location{StartLine: 22, EndLine: 22}}, + }, + { + ID: "undici-types@6.21.0", + Name: "undici-types", + Version: "6.21.0", + Dev: true, + Indirect: true, + Relationship: types.RelationshipIndirect, + Licenses: []string{"MIT"}, + Locations: types.Locations{types.Location{StartLine: 26, EndLine: 26}}, + }, + }, + }, + }, + }, + }, + { + name: "sad", + dir: "testdata/sad", + want: &analyzer.AnalysisResult{}, + }, + { + name: "without node_modules", + dir: "testdata/no-node-modules", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Bun, + FilePath: "bun.lock", + Packages: types.Packages{ + { + ID: "@types/bun@1.2.14", + Name: "@types/bun", + Version: "1.2.14", + Dev: true, + Relationship: types.RelationshipDirect, + DependsOn: []string{"bun-types@1.2.14"}, + Locations: types.Locations{types.Location{StartLine: 15, EndLine: 15}}, + }, + { + ID: "typescript@5.8.3", + Name: "typescript", + Version: "5.8.3", + Relationship: types.RelationshipDirect, + Locations: types.Locations{types.Location{StartLine: 21, EndLine: 21}}, + }, + { + ID: "@types/node@22.15.21", + Name: "@types/node", + Version: "22.15.21", + Dev: true, + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"undici-types@6.21.0"}, + Locations: types.Locations{types.Location{StartLine: 17, EndLine: 17}}, + }, + { + ID: "bun-types@1.2.14", + Name: "bun-types", + Version: "1.2.14", + Dev: true, + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"@types/node@22.15.21"}, + Locations: types.Locations{types.Location{StartLine: 19, EndLine: 19}}, + }, + { + ID: "undici-types@6.21.0", + Name: "undici-types", + Version: "6.21.0", + Dev: true, + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: types.Locations{types.Location{StartLine: 23, EndLine: 23}}, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newBunLibraryAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + if len(got.Applications) > 0 { + sort.Sort(got.Applications[0].Packages) + } + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_bunLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "lock file", + filePath: "bun/bun.lock", + want: true, + }, + { + name: "sad path", + filePath: "bun/package-lock.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := bunLibraryAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/bun.lock b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/bun.lock new file mode 100644 index 000000000000..3ff92288130c --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/bun.lock @@ -0,0 +1,30 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "buntest", + "devDependencies": { + "@types/bun": "latest", + }, + "optionalDependencies": { + "zod": "^3.24.4", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.13", "", { "dependencies": { "bun-types": "1.2.13" } }, "sha512-u6vXep/i9VBxoJl3GjZsl/BFIsvML8DfVDO0RYLEwtSZSp981kEO1V5NwRcO1CPJ7AmvpbnDCiMKo3JvbDEjAg=="], + + "@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="], + + "bun-types": ["bun-types@1.2.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-rRjA1T6n7wto4gxhAO/ErZEtOXyEZEmnIHQfl0Dt1QQSB4QV0iP6BZ9/YB5fZaHFQ2dwHFrmPaRQ9GGMX01k9Q=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/@types/bun/package.json b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/@types/bun/package.json new file mode 100644 index 000000000000..c57d8f81cbbc --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/@types/bun/package.json @@ -0,0 +1,63 @@ +{ + "name": "@types/bun", + "version": "1.2.13", + "description": "TypeScript definitions for bun", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/bun", + "license": "MIT", + "contributors": [ + { + "name": "Jarred Sumner", + "githubUsername": "Jarred-Sumner", + "url": "https://github.com/Jarred-Sumner" + }, + { + "name": "Ashcon Partovi", + "githubUsername": "electroid", + "url": "https://github.com/electroid" + }, + { + "name": "Chloe Caruso", + "githubUsername": "paperclover", + "url": "https://github.com/paperclover" + }, + { + "name": "Robobun", + "githubUsername": "robobun", + "url": "https://github.com/robobun" + }, + { + "name": "Dylan Conway", + "githubUsername": "dylan-conway", + "url": "https://github.com/dylan-conway" + }, + { + "name": "Meghan Denny", + "githubUsername": "nektro", + "url": "https://github.com/nektro" + }, + { + "name": "Michael H", + "githubUsername": "RiskyMH", + "url": "https://github.com/RiskyMH" + }, + { + "name": "Alistair Smith", + "githubUsername": "alii", + "url": "https://github.com/alii" + } + ], + "main": "", + "types": "index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/bun" + }, + "scripts": {}, + "dependencies": { + "bun-types": "1.2.13" + }, + "peerDependencies": {}, + "typesPublisherContentHash": "8a6526d76212b81f34802d6687cb606f8cfb541ad90a916f7b17da292e07fbc2", + "typeScriptVersion": "5.1" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/@types/node/package.json b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/@types/node/package.json new file mode 100644 index 000000000000..3d263ad7e3e7 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/@types/node/package.json @@ -0,0 +1,225 @@ +{ + "name": "@types/node", + "version": "22.15.18", + "description": "TypeScript definitions for node", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node", + "license": "MIT", + "contributors": [ + { + "name": "Microsoft TypeScript", + "githubUsername": "Microsoft", + "url": "https://github.com/Microsoft" + }, + { + "name": "Alberto Schiabel", + "githubUsername": "jkomyno", + "url": "https://github.com/jkomyno" + }, + { + "name": "Alvis HT Tang", + "githubUsername": "alvis", + "url": "https://github.com/alvis" + }, + { + "name": "Andrew Makarov", + "githubUsername": "r3nya", + "url": "https://github.com/r3nya" + }, + { + "name": "Benjamin Toueg", + "githubUsername": "btoueg", + "url": "https://github.com/btoueg" + }, + { + "name": "Chigozirim C.", + "githubUsername": "smac89", + "url": "https://github.com/smac89" + }, + { + "name": "David Junger", + "githubUsername": "touffy", + "url": "https://github.com/touffy" + }, + { + "name": "Deividas Bakanas", + "githubUsername": "DeividasBakanas", + "url": "https://github.com/DeividasBakanas" + }, + { + "name": "Eugene Y. Q. Shen", + "githubUsername": "eyqs", + "url": "https://github.com/eyqs" + }, + { + "name": "Hannes Magnusson", + "githubUsername": "Hannes-Magnusson-CK", + "url": "https://github.com/Hannes-Magnusson-CK" + }, + { + "name": "Huw", + "githubUsername": "hoo29", + "url": "https://github.com/hoo29" + }, + { + "name": "Kelvin Jin", + "githubUsername": "kjin", + "url": "https://github.com/kjin" + }, + { + "name": "Klaus Meinhardt", + "githubUsername": "ajafff", + "url": "https://github.com/ajafff" + }, + { + "name": "Lishude", + "githubUsername": "islishude", + "url": "https://github.com/islishude" + }, + { + "name": "Mariusz Wiktorczyk", + "githubUsername": "mwiktorczyk", + "url": "https://github.com/mwiktorczyk" + }, + { + "name": "Mohsen Azimi", + "githubUsername": "mohsen1", + "url": "https://github.com/mohsen1" + }, + { + "name": "Nikita Galkin", + "githubUsername": "galkin", + "url": "https://github.com/galkin" + }, + { + "name": "Parambir Singh", + "githubUsername": "parambirs", + "url": "https://github.com/parambirs" + }, + { + "name": "Sebastian Silbermann", + "githubUsername": "eps1lon", + "url": "https://github.com/eps1lon" + }, + { + "name": "Thomas den Hollander", + "githubUsername": "ThomasdenH", + "url": "https://github.com/ThomasdenH" + }, + { + "name": "Wilco Bakker", + "githubUsername": "WilcoBakker", + "url": "https://github.com/WilcoBakker" + }, + { + "name": "wwwy3y3", + "githubUsername": "wwwy3y3", + "url": "https://github.com/wwwy3y3" + }, + { + "name": "Samuel Ainsworth", + "githubUsername": "samuela", + "url": "https://github.com/samuela" + }, + { + "name": "Kyle Uehlein", + "githubUsername": "kuehlein", + "url": "https://github.com/kuehlein" + }, + { + "name": "Thanik Bhongbhibhat", + "githubUsername": "bhongy", + "url": "https://github.com/bhongy" + }, + { + "name": "Marcin Kopacz", + "githubUsername": "chyzwar", + "url": "https://github.com/chyzwar" + }, + { + "name": "Trivikram Kamat", + "githubUsername": "trivikr", + "url": "https://github.com/trivikr" + }, + { + "name": "Junxiao Shi", + "githubUsername": "yoursunny", + "url": "https://github.com/yoursunny" + }, + { + "name": "Ilia Baryshnikov", + "githubUsername": "qwelias", + "url": "https://github.com/qwelias" + }, + { + "name": "ExE Boss", + "githubUsername": "ExE-Boss", + "url": "https://github.com/ExE-Boss" + }, + { + "name": "Piotr Błażejewicz", + "githubUsername": "peterblazejewicz", + "url": "https://github.com/peterblazejewicz" + }, + { + "name": "Anna Henningsen", + "githubUsername": "addaleax", + "url": "https://github.com/addaleax" + }, + { + "name": "Victor Perin", + "githubUsername": "victorperin", + "url": "https://github.com/victorperin" + }, + { + "name": "NodeJS Contributors", + "githubUsername": "NodeJS", + "url": "https://github.com/NodeJS" + }, + { + "name": "Linus Unnebäck", + "githubUsername": "LinusU", + "url": "https://github.com/LinusU" + }, + { + "name": "wafuwafu13", + "githubUsername": "wafuwafu13", + "url": "https://github.com/wafuwafu13" + }, + { + "name": "Matteo Collina", + "githubUsername": "mcollina", + "url": "https://github.com/mcollina" + }, + { + "name": "Dmitry Semigradsky", + "githubUsername": "Semigradsky", + "url": "https://github.com/Semigradsky" + }, + { + "name": "René", + "githubUsername": "Renegade334", + "url": "https://github.com/Renegade334" + } + ], + "main": "", + "types": "index.d.ts", + "typesVersions": { + "<=5.6": { + "*": [ + "ts5.6/*" + ] + } + }, + "repository": { + "type": "git", + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/node" + }, + "scripts": {}, + "dependencies": { + "undici-types": "~6.21.0" + }, + "peerDependencies": {}, + "typesPublisherContentHash": "2b90948f1fb3116c8907f2beaa3f3e5508eb0101dc27013c967a068001011e3e", + "typeScriptVersion": "5.1" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/bun-types/package.json b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/bun-types/package.json new file mode 100644 index 000000000000..9e888591c122 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/bun-types/package.json @@ -0,0 +1,39 @@ +{ + "version": "1.2.13", + "name": "bun-types", + "license": "MIT", + "types": "./index.d.ts", + "description": "Type definitions and documentation for Bun, an incredibly fast JavaScript runtime", + "repository": { + "type": "git", + "url": "https://github.com/oven-sh/bun", + "directory": "packages/bun-types" + }, + "files": [ + "./*.d.ts", + "docs/**/*.md", + "docs/*.md" + ], + "homepage": "https://bun.sh", + "dependencies": { + "@types/node": "*" + }, + "devDependencies": { + "@biomejs/biome": "^1.5.3", + "@definitelytyped/dtslint": "^0.0.199", + "@definitelytyped/eslint-plugin": "^0.0.197", + "typescript": "^5.0.2" + }, + "scripts": { + "prebuild": "echo $(pwd)", + "copy-docs": "rm -rf docs && cp -rL ../../docs/ ./docs && find ./docs -type f -name '*.md' -exec sed -i 's/\\$BUN_LATEST_VERSION/'\"${BUN_VERSION#bun-v}\"'/g' {} +", + "build": "bun run copy-docs && bun scripts/build.ts", + "test": "tsc", + "fmt": "echo $(which biome) && biome format --write ." + }, + "keywords": [ + "bun", + "bun.js", + "types" + ] +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/typescript/package.json b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/typescript/package.json new file mode 100644 index 000000000000..282058f4484f --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/typescript/package.json @@ -0,0 +1,120 @@ +{ + "name": "typescript", + "author": "Microsoft Corp.", + "homepage": "https://www.typescriptlang.org/", + "version": "5.8.3", + "license": "Apache-2.0", + "description": "TypeScript is a language for application scale JavaScript development", + "keywords": [ + "TypeScript", + "Microsoft", + "compiler", + "language", + "javascript" + ], + "bugs": { + "url": "https://github.com/microsoft/TypeScript/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/TypeScript.git" + }, + "main": "./lib/typescript.js", + "typings": "./lib/typescript.d.ts", + "bin": { + "tsc": "./bin/tsc", + "tsserver": "./bin/tsserver" + }, + "engines": { + "node": ">=14.17" + }, + "files": [ + "bin", + "lib", + "!lib/enu", + "LICENSE.txt", + "README.md", + "SECURITY.md", + "ThirdPartyNoticeText.txt", + "!**/.gitattributes" + ], + "devDependencies": { + "@dprint/formatter": "^0.4.1", + "@dprint/typescript": "0.93.3", + "@esfx/canceltoken": "^1.0.0", + "@eslint/js": "^9.17.0", + "@octokit/rest": "^21.0.2", + "@types/chai": "^4.3.20", + "@types/diff": "^5.2.3", + "@types/minimist": "^1.2.5", + "@types/mocha": "^10.0.10", + "@types/ms": "^0.7.34", + "@types/node": "latest", + "@types/source-map-support": "^0.5.10", + "@types/which": "^3.0.4", + "@typescript-eslint/rule-tester": "^8.18.1", + "@typescript-eslint/type-utils": "^8.18.1", + "@typescript-eslint/utils": "^8.18.1", + "azure-devops-node-api": "^14.1.0", + "c8": "^10.1.3", + "chai": "^4.5.0", + "chalk": "^4.1.2", + "chokidar": "^3.6.0", + "diff": "^5.2.0", + "dprint": "^0.47.6", + "esbuild": "^0.24.0", + "eslint": "^9.17.0", + "eslint-formatter-autolinkable-stylish": "^1.4.0", + "eslint-plugin-regexp": "^2.7.0", + "fast-xml-parser": "^4.5.1", + "glob": "^10.4.5", + "globals": "^15.13.0", + "hereby": "^1.10.0", + "jsonc-parser": "^3.3.1", + "knip": "^5.41.0", + "minimist": "^1.2.8", + "mocha": "^10.8.2", + "mocha-fivemat-progress-reporter": "^0.1.0", + "monocart-coverage-reports": "^2.11.4", + "ms": "^2.1.3", + "playwright": "^1.49.1", + "source-map-support": "^0.5.21", + "tslib": "^2.8.1", + "typescript": "^5.7.2", + "typescript-eslint": "^8.18.1", + "which": "^3.0.1" + }, + "overrides": { + "typescript@*": "$typescript" + }, + "scripts": { + "test": "hereby runtests-parallel --light=false", + "test:eslint-rules": "hereby run-eslint-rules-tests", + "build": "npm run build:compiler && npm run build:tests", + "build:compiler": "hereby local", + "build:tests": "hereby tests", + "build:tests:notypecheck": "hereby tests --no-typecheck", + "clean": "hereby clean", + "gulp": "hereby", + "lint": "hereby lint", + "knip": "hereby knip", + "format": "dprint fmt", + "setup-hooks": "node scripts/link-hooks.mjs" + }, + "browser": { + "fs": false, + "os": false, + "path": false, + "crypto": false, + "buffer": false, + "source-map-support": false, + "inspector": false, + "perf_hooks": false + }, + "packageManager": "npm@8.19.4", + "volta": { + "node": "20.1.0", + "npm": "8.19.4" + }, + "gitHead": "68cead182cc24afdc3f1ce7c8ff5853aba14b65a" +} diff --git a/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/undici-types/package.json b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/undici-types/package.json new file mode 100644 index 000000000000..d2f33bc7edc5 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/undici-types/package.json @@ -0,0 +1,55 @@ +{ + "name": "undici-types", + "version": "6.21.0", + "description": "A stand-alone types package for Undici", + "homepage": "https://undici.nodejs.org", + "bugs": { + "url": "https://github.com/nodejs/undici/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/undici.git" + }, + "license": "MIT", + "types": "index.d.ts", + "files": [ + "*.d.ts" + ], + "contributors": [ + { + "name": "Daniele Belardi", + "url": "https://github.com/dnlup", + "author": true + }, + { + "name": "Ethan Arrowood", + "url": "https://github.com/ethan-arrowood", + "author": true + }, + { + "name": "Matteo Collina", + "url": "https://github.com/mcollina", + "author": true + }, + { + "name": "Matthew Aitken", + "url": "https://github.com/KhafraDev", + "author": true + }, + { + "name": "Robert Nagy", + "url": "https://github.com/ronag", + "author": true + }, + { + "name": "Szymon Marczak", + "url": "https://github.com/szmarczak", + "author": true + }, + { + "name": "Tomas Della Vedova", + "url": "https://github.com/delvedor", + "author": true + } + ] +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/zod/package.json b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/zod/package.json new file mode 100644 index 000000000000..de0c7806877e --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/testdata/happy/node_modules/zod/package.json @@ -0,0 +1,122 @@ +{ + "name": "zod", + "version": "3.24.4", + "author": "Colin McDonnell ", + "repository": { + "type": "git", + "url": "git+https://github.com/colinhacks/zod.git" + }, + "main": "./lib/index.js", + "module": "./lib/index.mjs", + "devDependencies": { + "@babel/core": "^7.22.5", + "@babel/preset-env": "^7.22.5", + "@babel/preset-typescript": "^7.22.5", + "@jest/globals": "^29.4.3", + "@rollup/plugin-typescript": "^8.2.0", + "@standard-schema/spec": "^1.0.0-beta.4", + "@swc/core": "^1.3.66", + "@swc/jest": "^0.2.26", + "@types/benchmark": "^2.1.0", + "@types/jest": "^29.2.2", + "@types/node": "14", + "@typescript-eslint/eslint-plugin": "^5.15.0", + "@typescript-eslint/parser": "^5.15.0", + "babel-jest": "^29.5.0", + "benchmark": "^2.1.4", + "dependency-cruiser": "^9.19.0", + "eslint": "^8.11.0", + "eslint-config-prettier": "^8.5.0", + "eslint-plugin-ban": "^1.6.0", + "eslint-plugin-import": "^2.25.4", + "eslint-plugin-simple-import-sort": "^7.0.0", + "eslint-plugin-unused-imports": "^2.0.0", + "husky": "^7.0.4", + "jest": "^29.3.1", + "lint-staged": "^12.3.7", + "netlify-cli": "^17.26.2", + "nodemon": "^2.0.15", + "prettier": "^2.6.0", + "pretty-quick": "^3.1.3", + "rollup": "^2.70.1", + "ts-jest": "^29.1.0", + "ts-morph": "^14.0.0", + "ts-node": "^10.9.1", + "tslib": "^2.3.1", + "tsx": "^4.19.4", + "typescript": "^5.0.0", + "vitest": "^0.32.2" + }, + "exports": { + ".": { + "types": "./index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.mjs" + }, + "./package.json": "./package.json", + "./locales/*": "./lib/locales/*" + }, + "bugs": { + "url": "https://github.com/colinhacks/zod/issues" + }, + "description": "TypeScript-first schema declaration and validation library with static type inference", + "files": [ + "/lib", + "/index.d.ts" + ], + "funding": "https://github.com/sponsors/colinhacks", + "homepage": "https://zod.dev", + "keywords": [ + "typescript", + "schema", + "validation", + "type", + "inference" + ], + "license": "MIT", + "lint-staged": { + "src/*.ts": [ + "eslint --cache --fix", + "prettier --ignore-unknown --write" + ], + "*.md": [ + "prettier --ignore-unknown --write" + ] + }, + "scripts": { + "prettier:check": "prettier --check src/**/*.ts deno/lib/**/*.ts *.md --no-error-on-unmatched-pattern", + "prettier:fix": "prettier --write src/**/*.ts deno/lib/**/*.ts *.md --ignore-unknown --no-error-on-unmatched-pattern", + "lint:check": "eslint --cache --ext .ts ./src", + "lint:fix": "eslint --cache --fix --ext .ts ./src", + "check": "yarn lint:check && yarn prettier:check", + "fix": "yarn lint:fix && yarn prettier:fix", + "clean": "rm -rf lib/* deno/lib/*", + "build": "yarn run clean && npm run build:cjs && npm run build:esm && npm run build:deno", + "build:deno": "node ./deno-build.mjs && cp ./README.md ./deno/lib", + "build:esm": "rollup --config ./configs/rollup.config.js", + "build:cjs": "tsc -p ./configs/tsconfig.cjs.json", + "build:types": "tsc -p ./configs/tsconfig.types.json", + "build:test": "tsc -p ./configs/tsconfig.test.json", + "test:watch": "yarn test:ts-jest --watch", + "test": "yarn test:ts-jest", + "test:babel": "jest --coverage --config ./configs/babel-jest.config.json", + "test:bun": "bun test src/", + "test:vitest": "npx vitest --config ./configs/vitest.config.ts", + "test:ts-jest": "npx jest --config ./configs/ts-jest.config.json", + "test:swc": "npx jest --config ./configs/swc-jest.config.json", + "test:deno": "cd deno && deno test", + "prepublishOnly": "npm run test && npm run build && npm run build:deno", + "play": "nodemon -e ts -w . -x tsx playground.ts", + "depcruise": "depcruise -c .dependency-cruiser.js src", + "benchmark": "tsx src/benchmarks/index.ts", + "prepare": "husky install", + "docs": "netlify dev" + }, + "sideEffects": false, + "support": { + "backing": { + "npm-funding": true + } + }, + "types": "./index.d.ts" +} diff --git a/pkg/fanal/analyzer/language/nodejs/bun/testdata/no-node-modules/bun.lock b/pkg/fanal/analyzer/language/nodejs/bun/testdata/no-node-modules/bun.lock new file mode 100644 index 000000000000..a70a11dd24ab --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/testdata/no-node-modules/bun.lock @@ -0,0 +1,25 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "no-node-modules", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.14", "", { "dependencies": { "bun-types": "1.2.14" } }, "sha512-VsFZKs8oKHzI7zwvECiAJ5oSorWndIWEVhfbYqZd4HI/45kzW7PN2Rr5biAzvGvRuNmYLSANY+H59ubHq8xw7Q=="], + + "@types/node": ["@types/node@22.15.21", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ=="], + + "bun-types": ["bun-types@1.2.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-Kuh4Ub28ucMRWeiUUWMHsT9Wcbr4H3kLIO72RZZElSDxSu7vpetRvxIUDUaW6QtaIeixIpm7OXtNnZPf82EzwA=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/bun/testdata/sad/bun.lock b/pkg/fanal/analyzer/language/nodejs/bun/testdata/sad/bun.lock new file mode 100644 index 000000000000..615c01efd8ce --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/bun/testdata/sad/bun.lock @@ -0,0 +1,5 @@ +{ + "lockfileVersion": "broken", + "workspaces": {}, + "packages": {} +} diff --git a/pkg/fanal/analyzer/language/nodejs/license/license.go b/pkg/fanal/analyzer/language/nodejs/license/license.go new file mode 100644 index 000000000000..d70dd92ea741 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/license.go @@ -0,0 +1,109 @@ +package license + +import ( + "errors" + "io" + "io/fs" + "path" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/packagejson" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +type License struct { + logger *log.Logger + parser *packagejson.Parser + classifierConfidenceLevel float64 +} + +func NewLicense(classifierConfidenceLevel float64) *License { + return &License{ + logger: log.WithPrefix("npm"), + parser: packagejson.NewParser(), + classifierConfidenceLevel: classifierConfidenceLevel, + } +} + +func (l *License) Traverse(fsys fs.FS, root string) (map[string][]string, error) { + licenses := make(map[string][]string) + walkDirFunc := func(pkgJSONPath string, _ fs.DirEntry, r io.Reader) error { + pkg, err := l.parser.Parse(r) + if err != nil { + return xerrors.Errorf("unable to parse %q: %w", pkgJSONPath, err) + } + + ok, licenseFileName := IsLicenseRefToFile(pkg.Licenses) + if !ok { + licenses[pkg.ID] = pkg.Licenses + return nil + } + + l.logger.Debug("License names are missing, an attempt to find them in the license file", + log.FilePath(pkgJSONPath), log.String("license_file", licenseFileName)) + licenseFilePath := path.Join(path.Dir(pkgJSONPath), licenseFileName) + + if findings, err := classifyLicense(licenseFilePath, l.classifierConfidenceLevel, fsys); err != nil { + return xerrors.Errorf("unable to classify the license: %w", err) + } else if len(findings) > 0 { + // License found + licenses[pkg.ID] = findings.Names() + } else { + l.logger.Debug("The license file was not found or the license could not be classified", + log.String("license_file", licenseFilePath)) + } + return nil + } + if err := fsutils.WalkDir(fsys, root, fsutils.RequiredFile(types.NpmPkg), walkDirFunc); err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + + return licenses, nil +} + +// IsLicenseRefToFile The license field can refer to a file +// https://docs.npmjs.com/cli/v9/configuring-npm/package-json +func IsLicenseRefToFile(maybeLicenses []string) (bool, string) { + if len(maybeLicenses) != 1 { + // trying to find at least the LICENSE file + return true, "LICENSE" + } + + var licenseFileName string + if strings.HasPrefix(maybeLicenses[0], "LicenseRef-") { + // LicenseRef- + licenseFileName = strings.Split(maybeLicenses[0], "-")[1] + } else if strings.HasPrefix(maybeLicenses[0], "SEE LICENSE IN ") { + // SEE LICENSE IN + parts := strings.Split(maybeLicenses[0], " ") + licenseFileName = parts[len(parts)-1] + } + + return licenseFileName != "", licenseFileName +} + +func classifyLicense(filePath string, classifierConfidenceLevel float64, fsys fs.FS) (types.LicenseFindings, error) { + f, err := fsys.Open(filePath) + if errors.Is(err, fs.ErrNotExist) { + return nil, nil + } else if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + l, err := licensing.Classify(filePath, f, classifierConfidenceLevel) + if err != nil { + return nil, xerrors.Errorf("license classify error: %w", err) + } + + if l == nil { + return nil, nil + } + + return l.Findings, nil +} diff --git a/pkg/fanal/analyzer/language/nodejs/license/license_test.go b/pkg/fanal/analyzer/language/nodejs/license/license_test.go new file mode 100644 index 000000000000..b4febec85d94 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/license_test.go @@ -0,0 +1,98 @@ +package license_test + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/license" + "github.com/aquasecurity/trivy/pkg/mapfs" +) + +func Test_ParseLicenses(t *testing.T) { + tests := []struct { + name string + dir string + want map[string][]string + wantErr string + }{ + { + name: "happy", + dir: filepath.Join("testdata", "happy"), + want: map[string][]string{ + "package-a@0.0.1": {"CC-BY-SA-4.0"}, + "package-b@0.0.1": {"MIT"}, + "package-c@0.0.1": {"BSD-3-Clause"}, + "package-d@0.0.1": {"BSD-3-Clause"}, + "package-e@0.0.1": {"(GPL-3.0 OR LGPL-3.0 OR MPL-1.1 OR SEE LICENSE IN LICENSE)"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := mapfs.New() + require.NoError(t, fsys.CopyFilesUnder(tt.dir)) + + l := license.NewLicense(0.9) + licenses, err := l.Traverse(fsys, ".") + if tt.wantErr != "" { + assert.ErrorContainsf(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, licenses) + }) + } +} + +func Test_IsLicenseRefToFile(t *testing.T) { + tests := []struct { + name string + input []string + wantOk bool + wantFileName string + }{ + { + name: "no ref to file", + input: []string{"MIT"}, + }, + { + name: "empty input", + wantOk: true, + wantFileName: "LICENSE", + }, + { + name: "happy `SEE LICENSE IN`", + input: []string{"SEE LICENSE IN LICENSE.md"}, + wantOk: true, + wantFileName: "LICENSE.md", + }, + { + name: "sad `SEE LICENSE IN`", + input: []string{"SEE LICENSE IN "}, + wantOk: false, + }, + { + name: "happy `LicenseRef-`", + input: []string{"LicenseRef-LICENSE.txt"}, + wantOk: true, + wantFileName: "LICENSE.txt", + }, + { + name: "sad `LicenseRef-`", + input: []string{"LicenseRef-"}, + wantOk: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ok, licenseFileName := license.IsLicenseRefToFile(tt.input) + assert.Equal(t, tt.wantOk, ok) + assert.Equal(t, tt.wantFileName, licenseFileName) + }) + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-default-license-file/LICENSE b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-default-license-file/LICENSE new file mode 100644 index 000000000000..45cf32fb3848 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-default-license-file/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright 2023 test +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +- Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-default-license-file/package.json b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-default-license-file/package.json new file mode 100644 index 000000000000..2475c8237a41 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-default-license-file/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-d", + "version": "0.0.1" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-as-is/package.json b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-as-is/package.json new file mode 100644 index 000000000000..9ba9baf3c11a --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-as-is/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-e", + "version": "0.0.1", + "license": "(GPL-3.0 OR LGPL-3.0 OR MPL-1.1 OR SEE LICENSE IN LICENSE)" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-name/package.json b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-name/package.json new file mode 100644 index 000000000000..2070fead3b74 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-name/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-a", + "version": "0.0.1", + "license": "CC-BY-SA-4.0" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-ref/LICENSE.txt b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-ref/LICENSE.txt new file mode 100644 index 000000000000..edc19225c4cc --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-ref/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright 2023 test + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-ref/package.json b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-ref/package.json new file mode 100644 index 000000000000..20bb37bcf3d3 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-license-ref/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-b", + "version": "0.0.1", + "license": "LicenseRef-LICENSE.txt" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-no-licenses/package.json b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-no-licenses/package.json new file mode 100644 index 000000000000..d597b4b31aeb --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-no-licenses/package.json @@ -0,0 +1,4 @@ +{ + "name": "package-f", + "version": "0.0.1" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-see-license/LICENSE.md b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-see-license/LICENSE.md new file mode 100644 index 000000000000..45cf32fb3848 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-see-license/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright 2023 test +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +- Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +- Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-see-license/package.json b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-see-license/package.json new file mode 100644 index 000000000000..0e425322da0b --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/license/testdata/happy/node_modules/package-see-license/package.json @@ -0,0 +1,5 @@ +{ + "name": "package-c", + "version": "0.0.1", + "license": "SEE LICENSE IN LICENSE.md" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/npm/npm.go b/pkg/fanal/analyzer/language/nodejs/npm/npm.go new file mode 100644 index 000000000000..bdb3c2340a17 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/npm.go @@ -0,0 +1,160 @@ +package npm + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path" + "path/filepath" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/npm" + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/packagejson" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xpath "github.com/aquasecurity/trivy/pkg/x/path" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeNpmPkgLock, newNpmLibraryAnalyzer) +} + +const ( + version = 1 +) + +type npmLibraryAnalyzer struct { + logger *log.Logger + lockParser language.Parser + packageParser *packagejson.Parser +} + +func newNpmLibraryAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &npmLibraryAnalyzer{ + logger: log.WithPrefix("npm"), + lockParser: npm.NewParser(), + packageParser: packagejson.NewParser(), + }, nil +} + +func (a npmLibraryAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + // Parse package-lock.json + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.NpmPkgLock || input.FilePatterns.Match(path) + } + + var apps []types.Application + err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, _ io.Reader) error { + // Find all licenses from package.json files under node_modules dirs + licenses, err := a.findLicenses(input.FS, filePath) + if err != nil { + a.logger.Error("Unable to collect licenses", log.Err(err)) + licenses = make(map[string][]string) + } + + app, err := a.parseNpmPkgLock(ctx, input.FS, filePath) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + // Fill licenses from package.json only if not present in lock file + for i, pkg := range app.Packages { + if len(pkg.Licenses) == 0 { + if ll, ok := licenses[pkg.ID]; ok { + app.Packages[i].Licenses = ll + } + } + } + + apps = append(apps, *app) + return nil + }) + if err != nil { + return nil, xerrors.Errorf("package-lock.json/package.json walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a npmLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + // Don't save package-lock.json from the `node_modules` directory to avoid duplication and mistakes. + if fileName == types.NpmPkgLock && !xpath.Contains(filePath, "node_modules") { + return true + } + + // Save package.json files only from the `node_modules` directory. + // Required to search for licenses. + if fileName == types.NpmPkg && xpath.Contains(filePath, "node_modules") { + return true + } + return false +} + +func (a npmLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeNpmPkgLock +} + +func (a npmLibraryAnalyzer) Version() int { + return version +} + +func (a npmLibraryAnalyzer) parseNpmPkgLock(ctx context.Context, fsys fs.FS, filePath string) (*types.Application, error) { + f, err := fsys.Open(filePath) + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + defer func() { _ = f.Close() }() + + file, ok := f.(xio.ReadSeekCloserAt) + if !ok { + return nil, xerrors.Errorf("type assertion error: %w", err) + } + + // parse package-lock.json file + return language.Parse(ctx, types.Npm, filePath, file, a.lockParser) +} + +func (a npmLibraryAnalyzer) findLicenses(fsys fs.FS, lockPath string) (map[string][]string, error) { + dir := path.Dir(lockPath) + root := path.Join(dir, "node_modules") + if _, err := fs.Stat(fsys, root); errors.Is(err, fs.ErrNotExist) { + a.logger.Info(`To collect the license information of packages, "npm install" needs to be performed beforehand`, + log.String("dir", root)) + return nil, nil + } + + // Parse package.json + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.NpmPkg + } + + // Traverse node_modules dir and find licenses + // Note that fs.FS is always slashed regardless of the platform, + // and path.Join should be used rather than filepath.Join. + licenses := make(map[string][]string) + err := fsutils.WalkDir(fsys, root, required, func(filePath string, _ fs.DirEntry, r io.Reader) error { + pkg, err := a.packageParser.Parse(r) + if err != nil { + return xerrors.Errorf("unable to parse %q: %w", filePath, err) + } + + licenses[pkg.ID] = pkg.Licenses + return nil + }) + if err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + return licenses, nil +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/npm_test.go b/pkg/fanal/analyzer/language/nodejs/npm/npm_test.go new file mode 100644 index 000000000000..1812b7bdd35f --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/npm_test.go @@ -0,0 +1,289 @@ +package npm + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, true) + os.Exit(m.Run()) +} + +func Test_npmLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + want *analyzer.AnalysisResult + }{ + { + name: "with node_modules", + dir: "testdata/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Npm, + FilePath: "package-lock.json", + Packages: types.Packages{ + { + ID: "@babel/parser@7.23.6", + Name: "@babel/parser", + Version: "7.23.6", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 6, + EndLine: 10, + }, + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefOther, + URL: "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + }, + }, + }, + { + ID: "ansi-colors@3.2.3", + Name: "ansi-colors", + Version: "3.2.3", + Dev: true, + Locations: []types.Location{ + { + StartLine: 11, + EndLine: 16, + }, + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefOther, + URL: "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + }, + }, + }, + { + ID: "array-flatten@1.1.1", + Name: "array-flatten", + Version: "1.1.1", + Locations: []types.Location{ + { + StartLine: 17, + EndLine: 21, + }, + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefOther, + URL: "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + }, + }, + }, + { + ID: "body-parser@1.18.3", + Name: "body-parser", + Version: "1.18.3", + DependsOn: []string{"debug@2.6.9"}, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 44, + }, + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefOther, + URL: "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + }, + }, + }, + { + ID: "debug@2.6.9", + Name: "debug", + Version: "2.6.9", + DependsOn: []string{"ms@2.0.0"}, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 30, + EndLine: 37, + }, + { + StartLine: 53, + EndLine: 60, + }, + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefOther, + URL: "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + }, + }, + }, + { + ID: "express@4.16.4", + Name: "express", + Version: "4.16.4", + DependsOn: []string{"debug@2.6.9"}, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 45, + EndLine: 67, + }, + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefOther, + URL: "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + }, + }, + }, + { + ID: "ms@2.0.0", + Name: "ms", + Version: "2.0.0", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 38, + EndLine: 42, + }, + { + StartLine: 61, + EndLine: 65, + }, + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + }, + }, + }, + { + ID: "ms@2.1.1", + Name: "ms", + Version: "2.1.1", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 68, + EndLine: 72, + }, + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "without node_modules", + dir: "testdata/no-node_modules", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Npm, + FilePath: "package-lock.json", + Packages: types.Packages{ + { + ID: "ms@2.1.1", + Name: "ms", + Version: "2.1.1", + Locations: []types.Location{ + { + StartLine: 6, + EndLine: 10, + }, + }, + ExternalReferences: []types.ExternalRef{ + { + Type: types.RefOther, + URL: "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "sad path", + dir: "testdata/sad", + want: &analyzer.AnalysisResult{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newNpmLibraryAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + if len(got.Applications) > 0 { + sort.Sort(got.Applications[0].Packages) + } + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_nodePkgLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "lock file", + filePath: "npm/package-lock.json", + want: true, + }, + { + name: "package.json", + filePath: "npm/node_modules/ms/package.json", + want: true, + }, + { + name: "package.json with `/` in name", + filePath: "npm/node_modules/@babel/parser/package.json", + want: true, + }, + { + name: "sad path", + filePath: "npm/package.json", + want: false, + }, + { + name: "lock file in node_modules", + filePath: "npm/node_modules/html2canvas/package-lock.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := npmLibraryAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/@babel/parser/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/@babel/parser/package.json new file mode 100644 index 000000000000..5dc69d812815 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/@babel/parser/package.json @@ -0,0 +1,46 @@ +{ + "name": "@babel/parser", + "version": "7.23.6", + "description": "A JavaScript parser", + "author": "The Babel Team (https://babel.dev/team)", + "homepage": "https://babel.dev/docs/en/next/babel-parser", + "bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A+parser+%28babylon%29%22+is%3Aopen", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "babel", + "javascript", + "parser", + "tc39", + "ecmascript", + "@babel/parser" + ], + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-parser" + }, + "main": "./lib/index.js", + "types": "./typings/babel-parser.d.ts", + "files": [ + "bin", + "lib", + "typings/babel-parser.d.ts", + "index.cjs" + ], + "engines": { + "node": ">=6.0.0" + }, + "devDependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/helper-check-duplicate-nodes": "^7.22.5", + "@babel/helper-fixtures": "^7.23.4", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", + "charcodes": "^0.2.0" + }, + "bin": "./bin/babel-parser.js", + "type": "commonjs" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/debug/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/debug/package.json new file mode 100644 index 000000000000..dc787ba76781 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/debug/package.json @@ -0,0 +1,49 @@ +{ + "name": "debug", + "version": "2.6.9", + "repository": { + "type": "git", + "url": "git://github.com/visionmedia/debug.git" + }, + "description": "small debugging utility", + "keywords": [ + "debug", + "log", + "debugger" + ], + "author": "TJ Holowaychuk ", + "contributors": [ + "Nathan Rajlich (http://n8.io)", + "Andrew Rhyne " + ], + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + }, + "devDependencies": { + "browserify": "9.0.3", + "chai": "^3.5.0", + "concurrently": "^3.1.0", + "coveralls": "^2.11.15", + "eslint": "^3.12.1", + "istanbul": "^0.4.5", + "karma": "^1.3.0", + "karma-chai": "^0.1.0", + "karma-mocha": "^1.3.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-sinon": "^1.0.5", + "mocha": "^3.2.0", + "mocha-lcov-reporter": "^1.2.0", + "rimraf": "^2.5.4", + "sinon": "^1.17.6", + "sinon-chai": "^2.8.0" + }, + "main": "./src/index.js", + "browser": "./src/browser.js", + "component": { + "scripts": { + "debug/index.js": "browser.js", + "debug/debug.js": "debug.js" + } + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/ms/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/ms/package.json new file mode 100644 index 000000000000..6a31c81fac43 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/node_modules/ms/package.json @@ -0,0 +1,37 @@ +{ + "name": "ms", + "version": "2.0.0", + "description": "Tiny milisecond conversion utility", + "repository": "zeit/ms", + "main": "./index", + "files": [ + "index.js" + ], + "scripts": { + "precommit": "lint-staged", + "lint": "eslint lib/* bin/*", + "test": "mocha tests.js" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + } + }, + "lint-staged": { + "*.js": [ + "npm run lint", + "prettier --single-quote --write", + "git add" + ] + }, + "license": "MIT", + "devDependencies": { + "eslint": "3.19.0", + "expect.js": "0.3.1", + "husky": "0.13.3", + "lint-staged": "3.4.1", + "mocha": "3.4.1" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/package.json new file mode 100644 index 000000000000..db78b735baee --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/body-parser/package.json @@ -0,0 +1,52 @@ +{ + "name": "body-parser", + "description": "Node.js body parsing middleware", + "version": "1.18.3", + "contributors": [ + "Douglas Christopher Wilson ", + "Jonathan Ong (http://jongleberry.com)" + ], + "license": "MIT", + "repository": "expressjs/body-parser", + "dependencies": { + "bytes": "3.0.0", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "~1.1.2", + "http-errors": "~1.6.3", + "iconv-lite": "0.4.23", + "on-finished": "~2.3.0", + "qs": "6.5.2", + "raw-body": "2.3.3", + "type-is": "~1.6.16" + }, + "devDependencies": { + "eslint": "4.19.1", + "eslint-config-standard": "11.0.0", + "eslint-plugin-import": "2.11.0", + "eslint-plugin-markdown": "1.0.0-beta.6", + "eslint-plugin-node": "6.0.1", + "eslint-plugin-promise": "3.7.0", + "eslint-plugin-standard": "3.1.0", + "istanbul": "0.4.5", + "methods": "1.1.2", + "mocha": "2.5.3", + "safe-buffer": "5.1.2", + "supertest": "1.1.0" + }, + "files": [ + "lib/", + "LICENSE", + "HISTORY.md", + "index.js" + ], + "engines": { + "node": ">= 0.8" + }, + "scripts": { + "lint": "eslint --plugin markdown --ext js,md .", + "test": "mocha --require test/support/env --reporter spec --check-leaks --bail test/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks test/", + "test-travis": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks test/" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/debug/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/debug/package.json new file mode 100644 index 000000000000..dc787ba76781 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/debug/package.json @@ -0,0 +1,49 @@ +{ + "name": "debug", + "version": "2.6.9", + "repository": { + "type": "git", + "url": "git://github.com/visionmedia/debug.git" + }, + "description": "small debugging utility", + "keywords": [ + "debug", + "log", + "debugger" + ], + "author": "TJ Holowaychuk ", + "contributors": [ + "Nathan Rajlich (http://n8.io)", + "Andrew Rhyne " + ], + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + }, + "devDependencies": { + "browserify": "9.0.3", + "chai": "^3.5.0", + "concurrently": "^3.1.0", + "coveralls": "^2.11.15", + "eslint": "^3.12.1", + "istanbul": "^0.4.5", + "karma": "^1.3.0", + "karma-chai": "^0.1.0", + "karma-mocha": "^1.3.0", + "karma-phantomjs-launcher": "^1.0.2", + "karma-sinon": "^1.0.5", + "mocha": "^3.2.0", + "mocha-lcov-reporter": "^1.2.0", + "rimraf": "^2.5.4", + "sinon": "^1.17.6", + "sinon-chai": "^2.8.0" + }, + "main": "./src/index.js", + "browser": "./src/browser.js", + "component": { + "scripts": { + "debug/index.js": "browser.js", + "debug/debug.js": "debug.js" + } + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/ms/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/ms/package.json new file mode 100644 index 000000000000..6a31c81fac43 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/node_modules/ms/package.json @@ -0,0 +1,37 @@ +{ + "name": "ms", + "version": "2.0.0", + "description": "Tiny milisecond conversion utility", + "repository": "zeit/ms", + "main": "./index", + "files": [ + "index.js" + ], + "scripts": { + "precommit": "lint-staged", + "lint": "eslint lib/* bin/*", + "test": "mocha tests.js" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + } + }, + "lint-staged": { + "*.js": [ + "npm run lint", + "prettier --single-quote --write", + "git add" + ] + }, + "license": "MIT", + "devDependencies": { + "eslint": "3.19.0", + "expect.js": "0.3.1", + "husky": "0.13.3", + "lint-staged": "3.4.1", + "mocha": "3.4.1" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/package.json new file mode 100644 index 000000000000..74196ad68e24 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/express/package.json @@ -0,0 +1,98 @@ +{ + "name": "express", + "description": "Fast, unopinionated, minimalist web framework", + "version": "4.16.4", + "author": "TJ Holowaychuk ", + "contributors": [ + "Aaron Heckmann ", + "Ciaran Jessup ", + "Douglas Christopher Wilson ", + "Guillermo Rauch ", + "Jonathan Ong ", + "Roman Shtylman ", + "Young Jae Sim " + ], + "license": "MIT", + "repository": "expressjs/express", + "homepage": "http://expressjs.com/", + "keywords": [ + "express", + "framework", + "sinatra", + "web", + "rest", + "restful", + "router", + "app", + "api" + ], + "dependencies": { + "accepts": "~1.3.5", + "array-flatten": "1.1.1", + "body-parser": "1.18.3", + "content-disposition": "0.5.2", + "content-type": "~1.0.4", + "cookie": "0.3.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.1.1", + "fresh": "0.5.2", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.4", + "qs": "6.5.2", + "range-parser": "~1.2.0", + "safe-buffer": "5.1.2", + "send": "0.16.2", + "serve-static": "1.13.2", + "setprototypeof": "1.1.0", + "statuses": "~1.4.0", + "type-is": "~1.6.16", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "devDependencies": { + "after": "0.8.2", + "connect-redis": "3.4.0", + "cookie-parser": "~1.4.3", + "cookie-session": "1.3.2", + "ejs": "2.6.1", + "eslint": "2.13.1", + "express-session": "1.15.6", + "hbs": "4.0.1", + "istanbul": "0.4.5", + "marked": "0.5.1", + "method-override": "3.0.0", + "mocha": "5.2.0", + "morgan": "1.9.1", + "multiparty": "4.2.1", + "pbkdf2-password": "1.2.1", + "should": "13.2.3", + "supertest": "3.3.0", + "vhost": "~3.0.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "files": [ + "LICENSE", + "History.md", + "Readme.md", + "index.js", + "lib/" + ], + "scripts": { + "lint": "eslint .", + "test": "mocha --require test/support/env --reporter spec --bail --check-leaks test/ test/acceptance/", + "test-ci": "istanbul cover node_modules/mocha/bin/_mocha --report lcovonly -- --require test/support/env --reporter spec --check-leaks test/ test/acceptance/", + "test-cov": "istanbul cover node_modules/mocha/bin/_mocha -- --require test/support/env --reporter dot --check-leaks test/ test/acceptance/", + "test-tap": "mocha --require test/support/env --reporter tap --check-leaks test/ test/acceptance/" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/ms/package.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/ms/package.json new file mode 100644 index 000000000000..fc28cb39d38a --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/node_modules/ms/package.json @@ -0,0 +1,37 @@ +{ + "name": "ms", + "version": "2.1.1", + "description": "Tiny millisecond conversion utility", + "repository": "zeit/ms", + "main": "./index", + "files": [ + "index.js" + ], + "scripts": { + "precommit": "lint-staged", + "lint": "eslint lib/* bin/*", + "test": "mocha tests.js" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + } + }, + "lint-staged": { + "*.js": [ + "npm run lint", + "prettier --single-quote --write", + "git add" + ] + }, + "license": "MIT", + "devDependencies": { + "eslint": "4.12.1", + "expect.js": "0.3.1", + "husky": "0.14.3", + "lint-staged": "5.0.0", + "mocha": "4.0.1" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/package-lock.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/package-lock.json new file mode 100644 index 000000000000..60c78b708d60 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/happy/package-lock.json @@ -0,0 +1,74 @@ +{ + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "@babel/parser": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==" + }, + "ansi-colors": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", + "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" + }, + "body-parser": { + "version": "1.18.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", + "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", + "requires": { + "debug": "2.6.9" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "express": { + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/express/-/express-4.16.4.tgz", + "integrity": "sha512-j12Uuyb4FMrd/qQAm6uCHAkPtO8FDTRJZBDd5D2KOL2eLaz1yUNdUB/NOIyq0iU4q4cFarsUCrnFDPBcnksuOg==", + "requires": { + "debug": "2.6.9" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/no-node_modules/package-lock.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/no-node_modules/package-lock.json new file mode 100644 index 000000000000..78bd8378678f --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/no-node_modules/package-lock.json @@ -0,0 +1,12 @@ +{ + "version": "1.0.0", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/npm/testdata/sad/package-lock.json b/pkg/fanal/analyzer/language/nodejs/npm/testdata/sad/package-lock.json new file mode 100644 index 000000000000..80c4ed2eb6fa --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/npm/testdata/sad/package-lock.json @@ -0,0 +1 @@ +{broken} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/pkg/pkg.go b/pkg/fanal/analyzer/language/nodejs/pkg/pkg.go new file mode 100644 index 000000000000..801e9f3df9dc --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pkg/pkg.go @@ -0,0 +1,59 @@ +package pkg + +import ( + "context" + "os" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/packagejson" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +func init() { + analyzer.RegisterAnalyzer(&nodePkgLibraryAnalyzer{}) +} + +const ( + version = 1 + requiredFile = "package.json" +) + +type parser struct{} + +func (*parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]types.Package, []types.Dependency, error) { + p := packagejson.NewParser() + pkg, err := p.Parse(r) + if err != nil { + return nil, nil, err + } + // skip packages without name/version + if pkg.Package.ID == "" { + return nil, nil, nil + } + // package.json may contain version range in `dependencies` fields + // e.g. "devDependencies": { "mocha": "^5.2.0", } + // so we get only information about project + return []types.Package{pkg.Package}, nil, nil +} + +type nodePkgLibraryAnalyzer struct{} + +// Analyze analyzes package.json for node packages +func (a nodePkgLibraryAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + return language.AnalyzePackage(ctx, types.NodePkg, input.FilePath, input.Content, &parser{}, input.Options.FileChecksum) +} + +func (a nodePkgLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return requiredFile == filepath.Base(filePath) +} + +func (a nodePkgLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeNodePkg +} + +func (a nodePkgLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/nodejs/pkg/pkg_test.go b/pkg/fanal/analyzer/language/nodejs/pkg/pkg_test.go new file mode 100644 index 000000000000..b25365be5c0a --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pkg/pkg_test.go @@ -0,0 +1,125 @@ +package pkg + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_nodePkgLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + includeChecksum bool + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/package.json", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.NodePkg, + FilePath: "testdata/package.json", + Packages: types.Packages{ + { + ID: "lodash@5.0.0", + Name: "lodash", + Version: "5.0.0", + Licenses: []string{"MIT"}, + FilePath: "testdata/package.json", + }, + }, + }, + }, + }, + }, + { + name: "happy path with checksum", + inputFile: "testdata/package.json", + includeChecksum: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.NodePkg, + FilePath: "testdata/package.json", + Packages: types.Packages{ + { + ID: "lodash@5.0.0", + Name: "lodash", + Version: "5.0.0", + Licenses: []string{"MIT"}, + FilePath: "testdata/package.json", + Digest: "sha1:901a7b55410321c4d35543506cff2a8613ef5aa2", + }, + }, + }, + }, + }, + }, + { + name: "happy path without name", + inputFile: "testdata/noname.json", + }, + { + name: "sad path", + inputFile: "testdata/sad.json", + wantErr: "JSON decode error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := nodePkgLibraryAnalyzer{} + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + Options: analyzer.AnalysisOptions{FileChecksum: tt.includeChecksum}, + }) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_nodePkgLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path", + filePath: "nodejs/package.json", + want: true, + }, + { + name: "sad path", + filePath: "nodejs/package-lock.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := nodePkgLibraryAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/pkg/testdata/noname.json b/pkg/fanal/analyzer/language/nodejs/pkg/testdata/noname.json new file mode 100644 index 000000000000..6efcbfe1b3e5 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pkg/testdata/noname.json @@ -0,0 +1,22 @@ +{ + "version": "5.0.0", + "license": "MIT", + "private": true, + "main": "lodash.js", + "engines": { + "node": ">=4.0.0" + }, + "sideEffects": false, + "scripts": { + "style": "eslint *.js .internal/**/*.js", + "test": "mocha -r esm test/*.test.js", + "validate": "npm run style && npm run test" + }, + "devDependencies": { + "mocha": "^5.2.0", + "eslint": "^7.16.0", + "eslint-plugin-import": "^2.22.1", + "lodash": "4.17.20", + "esm": "^3.2.25" + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/pkg/testdata/package.json b/pkg/fanal/analyzer/language/nodejs/pkg/testdata/package.json new file mode 100644 index 000000000000..26d216257252 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pkg/testdata/package.json @@ -0,0 +1,23 @@ +{ + "name": "lodash", + "version": "5.0.0", + "license": "MIT", + "private": true, + "main": "lodash.js", + "engines": { + "node": ">=4.0.0" + }, + "sideEffects": false, + "scripts": { + "style": "eslint *.js .internal/**/*.js", + "test": "mocha -r esm test/*.test.js", + "validate": "npm run style && npm run test" + }, + "devDependencies": { + "mocha": "^5.2.0", + "eslint": "^7.16.0", + "eslint-plugin-import": "^2.22.1", + "lodash": "4.17.20", + "esm": "^3.2.25" + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/pkg/testdata/sad.json b/pkg/fanal/analyzer/language/nodejs/pkg/testdata/sad.json new file mode 100644 index 000000000000..81750b96f9d8 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pkg/testdata/sad.json @@ -0,0 +1 @@ +{ \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/pnpm/pnpm.go b/pkg/fanal/analyzer/language/nodejs/pnpm/pnpm.go new file mode 100644 index 000000000000..75a2f47713e4 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pnpm/pnpm.go @@ -0,0 +1,146 @@ +package pnpm + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path" + "path/filepath" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/packagejson" + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/pnpm" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xpath "github.com/aquasecurity/trivy/pkg/x/path" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypePnpm, newPnpmAnalyzer) +} + +const version = 2 + +type pnpmAnalyzer struct { + logger *log.Logger + packageJsonParser *packagejson.Parser + lockParser language.Parser +} + +func newPnpmAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &pnpmAnalyzer{ + logger: log.WithPrefix("pnpm"), + packageJsonParser: packagejson.NewParser(), + lockParser: pnpm.NewParser(), + }, nil +} + +func (a pnpmAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.PnpmLock || input.FilePatterns.Match(path) + } + + err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error { + // Find licenses + licenses, err := a.findLicenses(input.FS, filePath) + if err != nil { + a.logger.Error("Unable to collect licenses", log.Err(err)) + licenses = make(map[string][]string) + } + + // Parse pnpm-lock.yaml + app, err := language.Parse(ctx, types.Pnpm, filePath, r, a.lockParser) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + // Fill licenses + for i, pkg := range app.Packages { + // We use snapshots for pnpm package IDs. + // But to match licenses, we need to use the ID-building logic as for `package.json` files. + id := dependency.ID(types.NodePkg, pkg.Name, pkg.Version) + if l, ok := licenses[id]; ok { + app.Packages[i].Licenses = l + } + } + + apps = append(apps, *app) + + return nil + }) + if err != nil { + return nil, xerrors.Errorf("pnpm walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a pnpmAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + // Don't save pnpm-lock.yaml from the `node_modules` directory to avoid duplication and mistakes. + if fileName == types.PnpmLock && !xpath.Contains(filePath, "node_modules") { + return true + } + + // Save package.json files only from the `node_modules` directory. + // Required to search for licenses. + if fileName == types.NpmPkg && xpath.Contains(filePath, "node_modules") { + return true + } + + return false +} + +func (a pnpmAnalyzer) Type() analyzer.Type { + return analyzer.TypePnpm +} + +func (a pnpmAnalyzer) Version() int { + return version +} + +func (a pnpmAnalyzer) findLicenses(fsys fs.FS, lockPath string) (map[string][]string, error) { + dir := path.Dir(lockPath) + root := path.Join(dir, "node_modules") + if _, err := fs.Stat(fsys, root); errors.Is(err, fs.ErrNotExist) { + a.logger.Info(`To collect the license information of packages, "pnpm install" needs to be performed beforehand`, + log.String("dir", root)) + return nil, nil + } + + // Parse package.json + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.NpmPkg + } + + // Traverse node_modules dir and find licenses + // Note that fs.FS is always slashed regardless of the platform, + // and path.Join should be used rather than filepath.Join. + licenses := make(map[string][]string) + err := fsutils.WalkDir(fsys, root, required, func(filePath string, _ fs.DirEntry, r io.Reader) error { + pkg, err := a.packageJsonParser.Parse(r) + if err != nil { + return xerrors.Errorf("unable to parse %q: %w", filePath, err) + } + + licenses[pkg.ID] = pkg.Licenses + return nil + }) + if err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + return licenses, nil +} diff --git a/pkg/fanal/analyzer/language/nodejs/pnpm/pnpm_test.go b/pkg/fanal/analyzer/language/nodejs/pnpm/pnpm_test.go new file mode 100644 index 000000000000..f18fa0e65076 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pnpm/pnpm_test.go @@ -0,0 +1,183 @@ +package pnpm + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_pnpmPkgLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + want *analyzer.AnalysisResult + }{ + { + name: "with node_modules", + dir: "testdata/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pnpm, + FilePath: "pnpm-lock.yaml", + Packages: types.Packages{ + { + ID: "ms@2.1.3", + Name: "ms", + Version: "2.1.3", + Licenses: []string{"MIT"}, + Relationship: types.RelationshipDirect, + }, + }, + }, + }, + }, + }, + { + name: "V9 with node_modules", + dir: "testdata/happy-v9", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pnpm, + FilePath: "pnpm-lock.yaml", + Packages: types.Packages{ + { + ID: "vue-router@4.5.1(vue@3.5.22)", + Name: "vue-router", + Version: "4.5.1", + Licenses: []string{"MIT"}, + Relationship: types.RelationshipDirect, + }, + }, + }, + }, + }, + }, + { + name: "without node_modules", + dir: "testdata/no-node_modules", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pnpm, + FilePath: "pnpm-lock.yaml", + Packages: types.Packages{ + { + ID: "@babel/parser@7.24.7", + Name: "@babel/parser", + Version: "7.24.7", + Relationship: types.RelationshipDirect, + DependsOn: []string{"@babel/types@7.24.7"}, + }, + { + ID: "ms@2.1.3", + Name: "ms", + Version: "2.1.3", + Relationship: types.RelationshipDirect, + }, + { + ID: "@babel/helper-string-parser@7.24.7", + Name: "@babel/helper-string-parser", + Version: "7.24.7", + Relationship: types.RelationshipIndirect, + Indirect: true, + }, + { + ID: "@babel/helper-validator-identifier@7.24.7", + Name: "@babel/helper-validator-identifier", + Version: "7.24.7", + Relationship: types.RelationshipIndirect, + Indirect: true, + }, + { + ID: "@babel/types@7.24.7", + Name: "@babel/types", + Version: "7.24.7", + Relationship: types.RelationshipIndirect, + Indirect: true, + DependsOn: []string{ + "@babel/helper-string-parser@7.24.7", + "@babel/helper-validator-identifier@7.24.7", + "to-fast-properties@2.0.0", + }, + }, + { + ID: "to-fast-properties@2.0.0", + Name: "to-fast-properties", + Version: "2.0.0", + Relationship: types.RelationshipIndirect, + Indirect: true, + }, + }, + }, + }, + }, + }, + { + name: "sad path", + dir: "testdata/sad", + want: &analyzer.AnalysisResult{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newPnpmAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + if len(got.Applications) > 0 { + sort.Sort(got.Applications[0].Packages) + } + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_pnpmPkgLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "lock file", + filePath: "pnpm/pnpm-lock.yaml", + want: true, + }, + { + name: "lock file in node_modules", + filePath: "pnpm/node_modules/html2canvas/pnpm-lock.yaml", + want: false, + }, + { + name: "package.json in node_modules", + filePath: "pnpm/node_modules/ms/package.json", + want: true, + }, + { + name: "sad path", + filePath: "pnpm/package.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newPnpmAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy-v9/node_modules/vue-router/package.json b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy-v9/node_modules/vue-router/package.json new file mode 100644 index 000000000000..c28b5c3822ad --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy-v9/node_modules/vue-router/package.json @@ -0,0 +1,24 @@ +{ + "name": "vue-router", + "version": "4.5.1", + "main": "index.js", + "unpkg": "dist/vue-router.global.js", + "jsdelivr": "dist/vue-router.global.js", + "module": "dist/vue-router.mjs", + "types": "dist/vue-router.d.ts", + "sideEffects": false, + "author": { + "name": "Eduardo San Martin Morote", + "email": "posva13@gmail.com" + }, + "funding": "https://github.com/sponsors/posva", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/router.git" + }, + "bugs": { + "url": "https://github.com/vuejs/router/issues" + }, + "homepage": "https://github.com/vuejs/router#readme" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy-v9/pnpm-lock.yaml b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy-v9/pnpm-lock.yaml new file mode 100644 index 000000000000..3d89ccf18ce0 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy-v9/pnpm-lock.yaml @@ -0,0 +1,24 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + vue-router: + specifier: ^4.5.1 + version: 4.5.1(vue@3.5.22) + +packages: + + vue-router@4.5.1: + resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} + peerDependencies: + vue: ^3.2.0 + +snapshots: + + vue-router@4.5.1(vue@3.5.22): {} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy/node_modules/.pnpm/ms@2.1.3/node_modules/ms/package.json b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy/node_modules/.pnpm/ms@2.1.3/node_modules/ms/package.json new file mode 100644 index 000000000000..49971890df8e --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy/node_modules/.pnpm/ms@2.1.3/node_modules/ms/package.json @@ -0,0 +1,38 @@ +{ + "name": "ms", + "version": "2.1.3", + "description": "Tiny millisecond conversion utility", + "repository": "vercel/ms", + "main": "./index", + "files": [ + "index.js" + ], + "scripts": { + "precommit": "lint-staged", + "lint": "eslint lib/* bin/*", + "test": "mocha tests.js" + }, + "eslintConfig": { + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + } + }, + "lint-staged": { + "*.js": [ + "npm run lint", + "prettier --single-quote --write", + "git add" + ] + }, + "license": "MIT", + "devDependencies": { + "eslint": "4.18.2", + "expect.js": "0.3.1", + "husky": "0.14.3", + "lint-staged": "5.0.0", + "mocha": "4.0.1", + "prettier": "2.0.5" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy/pnpm-lock.yaml b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy/pnpm-lock.yaml new file mode 100644 index 000000000000..50dfa1c43db3 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/happy/pnpm-lock.yaml @@ -0,0 +1,16 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + ms: + specifier: ^2.1.3 + version: 2.1.3 + +packages: + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false diff --git a/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/no-node_modules/pnpm-lock.yaml b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/no-node_modules/pnpm-lock.yaml new file mode 100644 index 000000000000..31997b0756cf --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/no-node_modules/pnpm-lock.yaml @@ -0,0 +1,51 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + '@babel/parser': + specifier: ^7.24.7 + version: 7.24.7 + ms: + specifier: ^2.1.3 + version: 2.1.3 + +packages: + + /@babel/helper-string-parser@7.24.7: + resolution: {integrity: sha512-7MbVt6xrwFQbunH2DNQsAP5sTGxfqQtErvBIvIMi6EQnbgUOuVYanvREcmFrOPhoXBrTtjhhP+lW+o5UfK+tDg==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/helper-validator-identifier@7.24.7: + resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + engines: {node: '>=6.9.0'} + dev: false + + /@babel/parser@7.24.7: + resolution: {integrity: sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==} + engines: {node: '>=6.0.0'} + hasBin: true + dependencies: + '@babel/types': 7.24.7 + dev: false + + /@babel/types@7.24.7: + resolution: {integrity: sha512-XEFXSlxiG5td2EJRe8vOmRbaXVgfcBlszKujvVmWIK/UpywWljQCfzAv3RQCGujWQ1RD4YYWEAqDXfuJiy8f5Q==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/helper-string-parser': 7.24.7 + '@babel/helper-validator-identifier': 7.24.7 + to-fast-properties: 2.0.0 + dev: false + + /ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + dev: false + + /to-fast-properties@2.0.0: + resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} + engines: {node: '>=4'} + dev: false diff --git a/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/sad/pnpm-lock.yaml b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/sad/pnpm-lock.yaml new file mode 100644 index 000000000000..80c4ed2eb6fa --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/pnpm/testdata/sad/pnpm-lock.yaml @@ -0,0 +1 @@ +{broken} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/package.json new file mode 100644 index 000000000000..caa6e0f3f637 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/package.json @@ -0,0 +1,14 @@ +{ + "name": "test", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "devDependencies": { + "foo-json": "npm:@types/jsonstream@0.8.33", + "foo-uuid": "npm:@types/uuid" + }, + "dependencies": { + "foo-debug": "npm:debug@^4.3", + "foo-ms": "npm:ms" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/yarn.lock new file mode 100644 index 000000000000..cb68abb657f9 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/alias/yarn.lock @@ -0,0 +1,44 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/node@*": + version "20.10.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.10.5.tgz#47ad460b514096b7ed63a1dae26fad0914ed3ab2" + integrity sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw== + dependencies: + undici-types "~5.26.4" + +"foo-debug@npm:debug@^4.3": + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +"foo-json@npm:@types/jsonstream@0.8.33": + version "0.8.33" + resolved "https://registry.yarnpkg.com/@types/jsonstream/-/jsonstream-0.8.33.tgz#7d37a16a78cf68a67858110dc1767023436fca23" + integrity sha512-yhg1SNOgJ8y2nOkvAQ1zZ1Z2xibxgFs7984+EeBPuWgo/TbuYo79+rj2wUVch3KF4GhhcwAi/AlJcehmLCXb3g== + dependencies: + "@types/node" "*" + +"foo-ms@npm:ms": + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +"foo-uuid@npm:@types/uuid": + version "9.0.7" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.7.tgz#b14cebc75455eeeb160d5fe23c2fcc0c64f724d8" + integrity sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/happy/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/happy/package.json new file mode 100644 index 000000000000..ce1af9d253e8 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/happy/package.json @@ -0,0 +1,13 @@ +{ + "name": "90", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "js-tokens": "^2.0.0", + "scheduler": "0.13.6" + }, + "devDependencies": { + "prop-types": "15.7.2" + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/happy/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/happy/yarn.lock new file mode 100644 index 000000000000..ae9f0ec16b89 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/happy/yarn.lock @@ -0,0 +1,47 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +js-tokens@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" + integrity sha512-kk4RjoztHFeAlCNPdTr/uF04oN5z53L18LJfADBQ7YHkO3iR2rP2ZwOH0n33r2FRw0psRmjTDEkOWPE//zmujg== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +prop-types@15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +scheduler@0.13.6: + version "0.13.6" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" + integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/latest-version/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/latest-version/package.json new file mode 100644 index 000000000000..211be0da7db9 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/latest-version/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "debug": "latest" + }, + "devDependencies" : { + "js-tokens": "^9.0.0" + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/latest-version/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/latest-version/yarn.lock new file mode 100644 index 000000000000..9f7b3d06857c --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/latest-version/yarn.lock @@ -0,0 +1,20 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +debug@latest: + version "4.3.5" + resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" + integrity sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg== + dependencies: + ms "2.1.2" + +js-tokens@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz" + integrity sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/c/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/c/package.json new file mode 100644 index 000000000000..7fc842765891 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/c/package.json @@ -0,0 +1,7 @@ +{ + "name": "c", + "version": "0.0.0", + "dependencies": { + "is-number": "^7.0.0" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/package.json new file mode 100644 index 000000000000..ce55f5435751 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/package.json @@ -0,0 +1,13 @@ +{ + "name": "yarn-workspace-test", + "version": "1.0.0", + "packageManager": "yarn@3.4.1", + "private": true, + "workspaces": [ + "packages/**", + "c" + ], + "devDependencies": { + "prettier": "^2.8.8" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/packages/package1/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/packages/package1/package.json new file mode 100644 index 000000000000..0983a7733942 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/packages/package1/package.json @@ -0,0 +1,8 @@ +{ + "name": "package1", + "version": "0.0.0", + "private": true, + "dependencies": { + "scheduler": "^0.23.0" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/packages/package2/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/packages/package2/package.json new file mode 100644 index 000000000000..cd5d2069c69c --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/packages/package2/package.json @@ -0,0 +1,7 @@ +{ + "private": true, + "type": "module", + "dependencies": { + "is-odd": "^3.0.1" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/packages/utils/util1/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/packages/utils/util1/package.json new file mode 100644 index 000000000000..7bcf16729401 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/packages/utils/util1/package.json @@ -0,0 +1,10 @@ +{ + "name": "util1", + "version": "0.0.0", + "dependencies": { + "js-tokens": "^8.0.1" + }, + "devDependencies": { + "prop-types": "^15.8.1" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/yarn.lock new file mode 100644 index 000000000000..ff1ff18ef57c --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/monorepo/yarn.lock @@ -0,0 +1,138 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 + +"c@workspace:c": + version: 0.0.0-use.local + resolution: "c@workspace:c" + dependencies: + is-number: ^7.0.0 + languageName: unknown + linkType: soft + +"is-number@npm:^6.0.0": + version: 6.0.0 + resolution: "is-number@npm:6.0.0" + checksum: f73bfced022128b5684bf77e0266a74e5222522bbc40f81cc1e949170c774a3c14b59a208be025d2d97a9c6b79c7c45fe351ab1c2c780872464fdedde0ae067a + languageName: node + linkType: hard + +"is-number@npm:^7.0.0": + version: 7.0.0 + resolution: "is-number@npm:7.0.0" + checksum: 456ac6f8e0f3111ed34668a624e45315201dff921e5ac181f8ec24923b99e9f32ca1a194912dc79d539c97d33dba17dc635202ff0b2cf98326f608323276d27a + languageName: node + linkType: hard + +"is-odd@npm:^3.0.1": + version: 3.0.1 + resolution: "is-odd@npm:3.0.1" + dependencies: + is-number: ^6.0.0 + checksum: 4e2b20764dd2296bafe44823d127f281c7039b37d2feaf5caffc1bf162502ef2920bcd4ad171490f371d3f15f52232c763a8ffc0b3633d4c83385fe20f3493af + languageName: node + linkType: hard + +"js-tokens@npm:^3.0.0 || ^4.0.0": + version: 4.0.0 + resolution: "js-tokens@npm:4.0.0" + checksum: 8a95213a5a77deb6cbe94d86340e8d9ace2b93bc367790b260101d2f36a2eaf4e4e22d9fa9cf459b38af3a32fb4190e638024cf82ec95ef708680e405ea7cc78 + languageName: node + linkType: hard + +"js-tokens@npm:^8.0.1": + version: 8.0.1 + resolution: "js-tokens@npm:8.0.1" + checksum: fb7bcd476c5b902ffb766382ca85aecb86ec66a607e419377026293b5877774e465f6cbe4229c8d85db3776ccc91c3aee518a0e04a005e260e57353f6f9278a8 + languageName: node + linkType: hard + +"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0": + version: 1.4.0 + resolution: "loose-envify@npm:1.4.0" + dependencies: + js-tokens: ^3.0.0 || ^4.0.0 + bin: + loose-envify: cli.js + checksum: 6517e24e0cad87ec9888f500c5b5947032cdfe6ef65e1c1936a0c48a524b81e65542c9c3edc91c97d5bddc806ee2a985dbc79be89215d613b1de5db6d1cfe6f4 + languageName: node + linkType: hard + +"object-assign@npm:^4.1.1": + version: 4.1.1 + resolution: "object-assign@npm:4.1.1" + checksum: fcc6e4ea8c7fe48abfbb552578b1c53e0d194086e2e6bbbf59e0a536381a292f39943c6e9628af05b5528aa5e3318bb30d6b2e53cadaf5b8fe9e12c4b69af23f + languageName: node + linkType: hard + +"package1@workspace:packages/package1": + version: 0.0.0-use.local + resolution: "package1@workspace:packages/package1" + dependencies: + scheduler: ^0.23.0 + languageName: unknown + linkType: soft + +"package2@workspace:packages/package2": + version: 0.0.0-use.local + resolution: "package2@workspace:packages/package2" + dependencies: + is-odd: ^3.0.1 + languageName: unknown + linkType: soft + +"prettier@npm:^2.8.8": + version: 2.8.8 + resolution: "prettier@npm:2.8.8" + bin: + prettier: bin-prettier.js + checksum: b49e409431bf129dd89238d64299ba80717b57ff5a6d1c1a8b1a28b590d998a34e083fa13573bc732bb8d2305becb4c9a4407f8486c81fa7d55100eb08263cf8 + languageName: node + linkType: hard + +"prop-types@npm:^15.8.1": + version: 15.8.1 + resolution: "prop-types@npm:15.8.1" + dependencies: + loose-envify: ^1.4.0 + object-assign: ^4.1.1 + react-is: ^16.13.1 + checksum: c056d3f1c057cb7ff8344c645450e14f088a915d078dcda795041765047fa080d38e5d626560ccaac94a4e16e3aa15f3557c1a9a8d1174530955e992c675e459 + languageName: node + linkType: hard + +"react-is@npm:^16.13.1": + version: 16.13.1 + resolution: "react-is@npm:16.13.1" + checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f + languageName: node + linkType: hard + +"scheduler@npm:^0.23.0": + version: 0.23.0 + resolution: "scheduler@npm:0.23.0" + dependencies: + loose-envify: ^1.1.0 + checksum: d79192eeaa12abef860c195ea45d37cbf2bbf5f66e3c4dcd16f54a7da53b17788a70d109ee3d3dde1a0fd50e6a8fc171f4300356c5aee4fc0171de526bf35f8a + languageName: node + linkType: hard + +"util1@workspace:packages/utils/util1": + version: 0.0.0-use.local + resolution: "util1@workspace:packages/utils/util1" + dependencies: + js-tokens: ^8.0.1 + prop-types: ^15.8.1 + languageName: unknown + linkType: soft + +"yarn-workspace-test@workspace:.": + version: 0.0.0-use.local + resolution: "yarn-workspace-test@workspace:." + dependencies: + prettier: ^2.8.8 + languageName: unknown + linkType: soft diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/no-packagejson/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/no-packagejson/yarn.lock new file mode 100644 index 000000000000..ae9f0ec16b89 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/no-packagejson/yarn.lock @@ -0,0 +1,47 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +js-tokens@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" + integrity sha512-kk4RjoztHFeAlCNPdTr/uF04oN5z53L18LJfADBQ7YHkO3iR2rP2ZwOH0n33r2FRw0psRmjTDEkOWPE//zmujg== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +prop-types@15.7.2: + version "15.7.2" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" + integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.8.1" + +react-is@^16.8.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +scheduler@0.13.6: + version "0.13.6" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.13.6.tgz#466a4ec332467b31a91b9bf74e5347072e4cd889" + integrity sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ== + dependencies: + loose-envify "^1.1.0" + object-assign "^4.1.1" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/project-with-workspace-in-subdir/foo/bar/generators/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/project-with-workspace-in-subdir/foo/bar/generators/package.json new file mode 100644 index 000000000000..1a1f420b0888 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/project-with-workspace-in-subdir/foo/bar/generators/package.json @@ -0,0 +1,7 @@ +{ + "name": "@test/bar-generators", + "version": "0.0.1", + "dependencies": { + "hoek": "6.1.3" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/project-with-workspace-in-subdir/foo/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/project-with-workspace-in-subdir/foo/package.json new file mode 100644 index 000000000000..bc453af12531 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/project-with-workspace-in-subdir/foo/package.json @@ -0,0 +1,10 @@ +{ + "name": "@test/foo", + "version": "1.0.0", + "repository": "ssh://git@test.com/test.git/foo", + "workspaces": [ + "bar/*" + ], + "main": "index.js", + "license": "MIT" +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/project-with-workspace-in-subdir/foo/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/project-with-workspace-in-subdir/foo/yarn.lock new file mode 100644 index 000000000000..0074e5c84d84 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/project-with-workspace-in-subdir/foo/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +hoek@6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-6.1.3.tgz#73b7d33952e01fe27a38b0457294b79dd8da242c" + integrity sha512-YXXAAhmF9zpQbC7LEcREFtXfGq5K1fmd+4PHkBq8NUqmzW3G+Dq10bI/i0KucLRwss3YYFQ0fSfoxBZYiGUqtQ== diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/unsupported_protocol/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/unsupported_protocol/yarn.lock new file mode 100644 index 000000000000..b66e215e825e --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/unsupported_protocol/yarn.lock @@ -0,0 +1,2 @@ + asap@unsupported:~2.0.6: + version "2.0.6" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/wrong-packagejson/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/wrong-packagejson/package.json new file mode 100644 index 000000000000..81750b96f9d8 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/wrong-packagejson/package.json @@ -0,0 +1 @@ +{ \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/wrong-packagejson/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/wrong-packagejson/yarn.lock new file mode 100644 index 000000000000..a56ac5d16517 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/wrong-packagejson/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +js-tokens@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-2.0.0.tgz#79903f5563ee778cc1162e6dcf1a0027c97f9cb5" + integrity sha512-kk4RjoztHFeAlCNPdTr/uF04oN5z53L18LJfADBQ7YHkO3iR2rP2ZwOH0n33r2FRw0psRmjTDEkOWPE//zmujg== \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@babel/parser/LICENSE b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@babel/parser/LICENSE new file mode 100644 index 000000000000..d4c7fc583804 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@babel/parser/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2012-2014 by various contributors (see AUTHORS) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@babel/parser/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@babel/parser/package.json new file mode 100644 index 000000000000..627a683bd652 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@babel/parser/package.json @@ -0,0 +1,46 @@ +{ + "name": "@babel/parser", + "version": "7.22.7", + "description": "A JavaScript parser", + "author": "The Babel Team (https://babel.dev/team)", + "homepage": "https://babel.dev/docs/en/next/babel-parser", + "bugs": "https://github.com/babel/babel/issues?utf8=%E2%9C%93&q=is%3Aissue+label%3A%22pkg%3A+parser+%28babylon%29%22+is%3Aopen", + "license": "MIT", + "publishConfig": { + "access": "public" + }, + "keywords": [ + "babel", + "javascript", + "parser", + "tc39", + "ecmascript", + "@babel/parser" + ], + "repository": { + "type": "git", + "url": "https://github.com/babel/babel.git", + "directory": "packages/babel-parser" + }, + "main": "./lib/index.js", + "types": "./typings/babel-parser.d.ts", + "files": [ + "bin", + "lib", + "typings/babel-parser.d.ts", + "index.cjs" + ], + "engines": { + "node": ">=6.0.0" + }, + "devDependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/helper-check-duplicate-nodes": "^7.22.5", + "@babel/helper-fixtures": "^7.22.6", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "charcodes": "^0.2.0" + }, + "bin": "./bin/babel-parser.js", + "type": "commonjs" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@vue/compiler-sfc/LICENSE b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@vue/compiler-sfc/LICENSE new file mode 100644 index 000000000000..b65dd9e62e21 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@vue/compiler-sfc/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013-present, Yuxi (Evan) You + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@vue/compiler-sfc/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@vue/compiler-sfc/package.json new file mode 100644 index 000000000000..e02b3934fd82 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/@vue/compiler-sfc/package.json @@ -0,0 +1,34 @@ +{ + "name": "@vue/compiler-sfc", + "version": "2.7.14", + "description": "compiler-sfc for Vue 2", + "main": "dist/compiler-sfc.js", + "types": "dist/compiler-sfc.d.ts", + "files": [ + "dist" + ], + "dependencies": { + "@babel/parser": "^7.18.4", + "postcss": "^8.4.14", + "source-map": "^0.6.1" + }, + "devDependencies": { + "@babel/types": "^7.19.4", + "@types/estree": "^0.0.48", + "@types/hash-sum": "^1.0.0", + "@types/lru-cache": "^5.1.1", + "@vue/consolidate": "^0.17.3", + "de-indent": "^1.0.2", + "estree-walker": "^2.0.2", + "hash-sum": "^2.0.0", + "less": "^4.1.3", + "lru-cache": "^5.1.1", + "magic-string": "^0.25.9", + "merge-source-map": "^1.1.0", + "postcss-modules": "^4.3.1", + "postcss-selector-parser": "^6.0.10", + "pug": "^3.0.2", + "sass": "^1.52.3", + "stylus": "^0.58.1" + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/nanoid/LICENSE b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/nanoid/LICENSE new file mode 100644 index 000000000000..37f56aa49f8e --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/nanoid/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright 2017 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/nanoid/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/nanoid/package.json new file mode 100644 index 000000000000..19d7d7a5e9f1 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/nanoid/package.json @@ -0,0 +1,66 @@ +{ + "name": "nanoid", + "version": "3.3.6", + "description": "A tiny (116 bytes), secure URL-friendly unique string ID generator", + "keywords": [ + "uuid", + "random", + "id", + "url" + ], + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + }, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "author": "Andrey Sitnik ", + "license": "MIT", + "repository": "ai/nanoid", + "browser": { + "./index.js": "./index.browser.js", + "./async/index.js": "./async/index.browser.js", + "./async/index.cjs": "./async/index.browser.cjs", + "./index.cjs": "./index.browser.cjs" + }, + "react-native": "index.js", + "bin": "./bin/nanoid.cjs", + "sideEffects": false, + "types": "./index.d.ts", + "type": "module", + "main": "index.cjs", + "module": "index.js", + "exports": { + ".": { + "types": "./index.d.ts", + "browser": "./index.browser.js", + "require": "./index.cjs", + "import": "./index.js", + "default": "./index.js" + }, + "./index.d.ts": "./index.d.ts", + "./package.json": "./package.json", + "./async/package.json": "./async/package.json", + "./async": { + "browser": "./async/index.browser.js", + "require": "./async/index.cjs", + "import": "./async/index.js", + "default": "./async/index.js" + }, + "./non-secure/package.json": "./non-secure/package.json", + "./non-secure": { + "require": "./non-secure/index.cjs", + "import": "./non-secure/index.js", + "default": "./non-secure/index.js" + }, + "./url-alphabet/package.json": "./url-alphabet/package.json", + "./url-alphabet": { + "require": "./url-alphabet/index.cjs", + "import": "./url-alphabet/index.js", + "default": "./url-alphabet/index.js" + } + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/picocolors/LICENSE b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/picocolors/LICENSE new file mode 100644 index 000000000000..496098c6e895 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/picocolors/LICENSE @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2021 Alexey Raspopov, Kostiantyn Denysov, Anton Verinov + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/picocolors/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/picocolors/package.json new file mode 100644 index 000000000000..85a12d523497 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/picocolors/package.json @@ -0,0 +1,25 @@ +{ + "name": "picocolors", + "version": "1.0.0", + "main": "./picocolors.js", + "types": "./picocolors.d.ts", + "browser": { + "./picocolors.js": "./picocolors.browser.js" + }, + "sideEffects": false, + "description": "The tiniest and the fastest library for terminal output formatting with ANSI colors", + "files": [ + "picocolors.*", + "types.ts" + ], + "keywords": [ + "terminal", + "colors", + "formatting", + "cli", + "console" + ], + "author": "Alexey Raspopov", + "repository": "alexeyraspopov/picocolors", + "license": "ISC" +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/postcss/LICENSE b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/postcss/LICENSE new file mode 100644 index 000000000000..da057b4562a8 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/postcss/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright 2013 Andrey Sitnik + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/postcss/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/postcss/package.json new file mode 100755 index 000000000000..cfee014cefa3 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/postcss/package.json @@ -0,0 +1,88 @@ +{ + "name": "postcss", + "version": "8.4.27", + "description": "Tool for transforming styles with JS plugins", + "engines": { + "node": "^10 || ^12 || >=14" + }, + "exports": { + ".": { + "require": "./lib/postcss.js", + "import": "./lib/postcss.mjs" + }, + "./lib/at-rule": "./lib/at-rule.js", + "./lib/comment": "./lib/comment.js", + "./lib/container": "./lib/container.js", + "./lib/css-syntax-error": "./lib/css-syntax-error.js", + "./lib/declaration": "./lib/declaration.js", + "./lib/fromJSON": "./lib/fromJSON.js", + "./lib/input": "./lib/input.js", + "./lib/lazy-result": "./lib/lazy-result.js", + "./lib/no-work-result": "./lib/no-work-result.js", + "./lib/list": "./lib/list.js", + "./lib/map-generator": "./lib/map-generator.js", + "./lib/node": "./lib/node.js", + "./lib/parse": "./lib/parse.js", + "./lib/parser": "./lib/parser.js", + "./lib/postcss": "./lib/postcss.js", + "./lib/previous-map": "./lib/previous-map.js", + "./lib/processor": "./lib/processor.js", + "./lib/result": "./lib/result.js", + "./lib/root": "./lib/root.js", + "./lib/rule": "./lib/rule.js", + "./lib/stringifier": "./lib/stringifier.js", + "./lib/stringify": "./lib/stringify.js", + "./lib/symbols": "./lib/symbols.js", + "./lib/terminal-highlight": "./lib/terminal-highlight.js", + "./lib/tokenize": "./lib/tokenize.js", + "./lib/warn-once": "./lib/warn-once.js", + "./lib/warning": "./lib/warning.js", + "./package.json": "./package.json" + }, + "main": "./lib/postcss.js", + "types": "./lib/postcss.d.ts", + "keywords": [ + "css", + "postcss", + "rework", + "preprocessor", + "parser", + "source map", + "transform", + "manipulation", + "transpiler" + ], + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "author": "Andrey Sitnik ", + "license": "MIT", + "homepage": "https://postcss.org/", + "repository": "postcss/postcss", + "bugs": { + "url": "https://github.com/postcss/postcss/issues" + }, + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "browser": { + "./lib/terminal-highlight": false, + "source-map-js": false, + "path": false, + "url": false, + "fs": false + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/source-map-js/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/source-map-js/package.json new file mode 100644 index 000000000000..501fafe16159 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/source-map-js/package.json @@ -0,0 +1,71 @@ +{ + "name": "source-map-js", + "description": "Generates and consumes source maps", + "version": "1.0.2", + "homepage": "https://github.com/7rulnik/source-map-js", + "author": "Valentin 7rulnik Semirulnik ", + "contributors": [ + "Nick Fitzgerald ", + "Tobias Koppers ", + "Duncan Beevers ", + "Stephen Crane ", + "Ryan Seddon ", + "Miles Elam ", + "Mihai Bazon ", + "Michael Ficarra ", + "Todd Wolfson ", + "Alexander Solovyov ", + "Felix Gnass ", + "Conrad Irwin ", + "usrbincc ", + "David Glasser ", + "Chase Douglas ", + "Evan Wallace ", + "Heather Arthur ", + "Hugh Kennedy ", + "David Glasser ", + "Simon Lydell ", + "Jmeas Smith ", + "Michael Z Goddard ", + "azu ", + "John Gozde ", + "Adam Kirkton ", + "Chris Montgomery ", + "J. Ryan Stinnett ", + "Jack Herrington ", + "Chris Truter ", + "Daniel Espeset ", + "Jamie Wong ", + "Eddy Bruël ", + "Hawken Rives ", + "Gilad Peleg ", + "djchie ", + "Gary Ye ", + "Nicolas Lalevée " + ], + "repository": "7rulnik/source-map-js", + "main": "./source-map.js", + "files": [ + "source-map.js", + "source-map.d.ts", + "lib/" + ], + "engines": { + "node": ">=0.10.0" + }, + "license": "BSD-3-Clause", + "scripts": { + "test": "npm run build && node test/run-tests.js", + "build": "webpack --color", + "toc": "doctoc --title '## Table of Contents' README.md && doctoc --title '## Table of Contents' CONTRIBUTING.md" + }, + "devDependencies": { + "clean-publish": "^3.1.0", + "doctoc": "^0.15.0", + "webpack": "^1.12.0" + }, + "clean-publish": { + "cleanDocs": true + }, + "typings": "source-map.d.ts" +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/source-map/LICENSE b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/source-map/LICENSE new file mode 100644 index 000000000000..ed1b7cf27e97 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/source-map/LICENSE @@ -0,0 +1,28 @@ + +Copyright (c) 2009-2011, Mozilla Foundation and contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the names of the Mozilla Foundation nor the names of project + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/source-map/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/source-map/package.json new file mode 100644 index 000000000000..24663417e7ac --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/node_modules/source-map/package.json @@ -0,0 +1,73 @@ +{ + "name": "source-map", + "description": "Generates and consumes source maps", + "version": "0.6.1", + "homepage": "https://github.com/mozilla/source-map", + "author": "Nick Fitzgerald ", + "contributors": [ + "Tobias Koppers ", + "Duncan Beevers ", + "Stephen Crane ", + "Ryan Seddon ", + "Miles Elam ", + "Mihai Bazon ", + "Michael Ficarra ", + "Todd Wolfson ", + "Alexander Solovyov ", + "Felix Gnass ", + "Conrad Irwin ", + "usrbincc ", + "David Glasser ", + "Chase Douglas ", + "Evan Wallace ", + "Heather Arthur ", + "Hugh Kennedy ", + "David Glasser ", + "Simon Lydell ", + "Jmeas Smith ", + "Michael Z Goddard ", + "azu ", + "John Gozde ", + "Adam Kirkton ", + "Chris Montgomery ", + "J. Ryan Stinnett ", + "Jack Herrington ", + "Chris Truter ", + "Daniel Espeset ", + "Jamie Wong ", + "Eddy Bruël ", + "Hawken Rives ", + "Gilad Peleg ", + "djchie ", + "Gary Ye ", + "Nicolas Lalevée " + ], + "repository": { + "type": "git", + "url": "http://github.com/mozilla/source-map.git" + }, + "main": "./source-map.js", + "files": [ + "source-map.js", + "source-map.d.ts", + "lib/", + "dist/source-map.debug.js", + "dist/source-map.js", + "dist/source-map.min.js", + "dist/source-map.min.js.map" + ], + "engines": { + "node": ">=0.10.0" + }, + "license": "BSD-3-Clause", + "scripts": { + "test": "npm run build && node test/run-tests.js", + "build": "webpack --color", + "toc": "doctoc --title '## Table of Contents' README.md && doctoc --title '## Table of Contents' CONTRIBUTING.md" + }, + "devDependencies": { + "doctoc": "^0.15.0", + "webpack": "^1.12.0" + }, + "typings": "source-map" +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/package.json new file mode 100644 index 000000000000..395444cb6b18 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/package.json @@ -0,0 +1,6 @@ +{ + "packageManager": "yarn@1.22.19", + "dependencies": { + "@vue/compiler-sfc": "2.7.14" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/yarn.lock new file mode 100644 index 000000000000..59692dac7cad --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-classic-licenses/yarn.lock @@ -0,0 +1,46 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/parser@^7.18.4": + version "7.22.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae" + integrity sha512-7NF8pOkHP5o2vpmGgNGcfAeCvOYhGLyA3Z4eBQkT1RJlWu47n63bCs93QfJ2hIAFCil7L5P2IWhs1oToVgrL0Q== + +"@vue/compiler-sfc@2.7.14": + version "2.7.14" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz#3446fd2fbb670d709277fc3ffa88efc5e10284fd" + integrity sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA== + dependencies: + "@babel/parser" "^7.18.4" + postcss "^8.4.14" + source-map "^0.6.1" + +nanoid@^3.3.6: + version "3.3.6" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" + integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +postcss@^8.4.14: + version "8.4.27" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.27.tgz#234d7e4b72e34ba5a92c29636734349e0d9c3057" + integrity sha512-gY/ACJtJPSmUFPDCHtX78+01fHa64FaU4zaaWfuh1MhGJISufJAH4cun6k/8fwsHYeK4UQmENQK+tRLCFJE8JQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/.yarn/cache/is-number-npm-6.0.0-30881e83e6-f73bfced02.zip b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/.yarn/cache/is-number-npm-6.0.0-30881e83e6-f73bfced02.zip new file mode 100644 index 000000000000..3f2dc0150d3b Binary files /dev/null and b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/.yarn/cache/is-number-npm-6.0.0-30881e83e6-f73bfced02.zip differ diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/.yarn/cache/is-odd-npm-3.0.1-93c3c3f41b-4e2b20764d.zip b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/.yarn/cache/is-odd-npm-3.0.1-93c3c3f41b-4e2b20764d.zip new file mode 100644 index 000000000000..14718cba0519 Binary files /dev/null and b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/.yarn/cache/is-odd-npm-3.0.1-93c3c3f41b-4e2b20764d.zip differ diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/.yarn/unplugged/is-callable-npm-1.2.7-808a303e61/node_modules/is-callable/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/.yarn/unplugged/is-callable-npm-1.2.7-808a303e61/node_modules/is-callable/package.json new file mode 100644 index 000000000000..3255bf0efceb --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/.yarn/unplugged/is-callable-npm-1.2.7-808a303e61/node_modules/is-callable/package.json @@ -0,0 +1,25 @@ +{ + "name": "is-callable", + "version": "1.2.7", + "license": "MIT", + "devDependencies": { + "@ljharb/eslint-config": "^21.0.0", + "aud": "^2.0.0", + "auto-changelog": "^2.4.0", + "available-typed-arrays": "^1.0.5", + "eclint": "^2.8.1", + "es-value-fixtures": "^1.4.2", + "eslint": "=8.8.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0", + "make-arrow-function": "^1.2.0", + "make-async-function": "^1.0.0", + "make-generator-function": "^2.0.0", + "npmignore": "^0.3.0", + "nyc": "^10.3.2", + "object-inspect": "^1.12.2", + "rimraf": "^2.7.1", + "safe-publish-latest": "^2.0.0", + "tape": "^5.6.0" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/package.json b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/package.json new file mode 100644 index 000000000000..a955ffc0c193 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/package.json @@ -0,0 +1,9 @@ +{ + "name": "yarn-3-licenses", + "version": "1.0.0", + "packageManager": "yarn@3.4.1", + "dependencies": { + "is-callable": "1.2.7", + "is-odd": "^3.0.1" + } +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/yarn.lock b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/yarn.lock new file mode 100644 index 000000000000..e4b2313250db --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/testdata/yarn-licenses/yarn.lock @@ -0,0 +1,41 @@ +# This file is generated by running "yarn install" inside your project. +# Manual changes might be lost - proceed with caution! + +__metadata: + version: 6 + cacheKey: 8 + +"is-callable@npm:1.2.7": + version: 1.2.7 + resolution: "is-callable@npm:1.2.7" + checksum: 61fd57d03b0d984e2ed3720fb1c7a897827ea174bd44402878e059542ea8c4aeedee0ea0985998aa5cc2736b2fa6e271c08587addb5b3959ac52cf665173d1ac + languageName: node + linkType: hard + +"is-number@npm:^6.0.0": + version: 6.0.0 + resolution: "is-number@npm:6.0.0" + checksum: f73bfced022128b5684bf77e0266a74e5222522bbc40f81cc1e949170c774a3c14b59a208be025d2d97a9c6b79c7c45fe351ab1c2c780872464fdedde0ae067a + languageName: node + linkType: hard + +"is-odd@npm:3.0.1": + version: 3.0.1 + resolution: "is-odd@npm:3.0.1" + dependencies: + is-number: ^6.0.0 + checksum: 4e2b20764dd2296bafe44823d127f281c7039b37d2feaf5caffc1bf162502ef2920bcd4ad171490f371d3f15f52232c763a8ffc0b3633d4c83385fe20f3493af + languageName: node + linkType: hard + +"root-workspace-0b6124@workspace:.": + version: 0.0.0-use.local + resolution: "root-workspace-0b6124@workspace:." + dependencies: + is-callable: 1.2.7 + is-odd: 3.0.1 + dependenciesMeta: + is-callable@1.2.7: + unplugged: true + languageName: unknown + linkType: soft \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/yarn.go b/pkg/fanal/analyzer/language/nodejs/yarn/yarn.go new file mode 100644 index 000000000000..1fd65c2de2bc --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/yarn.go @@ -0,0 +1,489 @@ +package yarn + +import ( + "archive/zip" + "cmp" + "context" + "errors" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "regexp" + "slices" + "sort" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/packagejson" + "github.com/aquasecurity/trivy/pkg/dependency/parser/nodejs/yarn" + "github.com/aquasecurity/trivy/pkg/detector/library/compare/npm" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/license" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeYarn, newYarnAnalyzer) +} + +const version = 2 + +// Taken from Yarn +// cf. https://github.com/yarnpkg/yarn/blob/328fd596de935acc6c3e134741748fcc62ec3739/src/resolvers/exotics/registry-resolver.js#L12 +var fragmentRegexp = regexp.MustCompile(`(\S+):(@?.*?)(@(.*?)|)$`) + +type yarnAnalyzer struct { + logger *log.Logger + packageJsonParser *packagejson.Parser + comparer npm.Comparer + license *license.License +} + +func newYarnAnalyzer(opt analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &yarnAnalyzer{ + logger: log.WithPrefix("yarn"), + packageJsonParser: packagejson.NewParser(), + comparer: npm.Comparer{}, + license: license.NewLicense(opt.LicenseScannerOption.ClassifierConfidenceLevel), + }, nil +} + +type parserWithPatterns struct { + patterns map[string][]string +} + +func (p *parserWithPatterns) Parse(ctx context.Context, r xio.ReadSeekerAt) ([]types.Package, []types.Dependency, error) { + pkgs, deps, patterns, err := yarn.NewParser().Parse(ctx, r) + p.patterns = patterns + return pkgs, deps, err +} + +func (a yarnAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.YarnLock || input.FilePatterns.Match(path) + } + + err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error { + parser := &parserWithPatterns{} + // Parse yarn.lock + app, err := language.Parse(ctx, types.Yarn, filePath, r, parser) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + licenses, err := a.traverseLicenses(input.FS, filePath) + if err != nil { + a.logger.Debug("Unable to traverse licenses", log.Err(err)) + } + + // Parse package.json alongside yarn.lock to find direct deps and mark dev deps + if err = a.analyzeDependencies(input.FS, path.Dir(filePath), app, parser.patterns); err != nil { + a.logger.Warn("Unable to parse package.json to remove dev dependencies", + log.FilePath(path.Join(path.Dir(filePath), types.NpmPkg)), log.Err(err)) + } + + // Fill licenses + for i, lib := range app.Packages { + if l, ok := licenses[lib.ID]; ok { + app.Packages[i].Licenses = l + } + } + + apps = append(apps, *app) + + return nil + }) + if err != nil { + return nil, xerrors.Errorf("yarn walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a yarnAnalyzer) Required(filePath string, _ os.FileInfo) bool { + dirs, fileName := splitPath(filePath) + + if fileName == types.YarnLock && + // skipping yarn.lock in cache folders + lo.Some(dirs, []string{ + "node_modules", + ".yarn", + }) { + return false + } + + if fileName == types.YarnLock || + fileName == types.NpmPkg || + strings.HasPrefix(strings.ToLower(fileName), "license") { + return true + } + + // The path is slashed in analyzers. + l := len(dirs) + // Valid path to the zip file - **/.yarn/cache/*.zip + if l > 1 && dirs[l-2] == ".yarn" && dirs[l-1] == "cache" && path.Ext(fileName) == ".zip" { + return true + } + + return false +} + +func splitPath(filePath string) (dirs []string, fileName string) { + fileName = filepath.Base(filePath) + // The path is slashed in analyzers. + dirs = strings.Split(path.Dir(filePath), "/") + return dirs, fileName +} + +func (a yarnAnalyzer) Type() analyzer.Type { + return analyzer.TypeYarn +} + +func (a yarnAnalyzer) Version() int { + return version +} + +// analyzeDependencies analyzes the package.json file next to yarn.lock, +// distinguishing between direct and transitive dependencies as well as production and development dependencies. +func (a yarnAnalyzer) analyzeDependencies(fsys fs.FS, dir string, app *types.Application, patterns map[string][]string) error { + packageJsonPath := path.Join(dir, types.NpmPkg) + root, workspaces, err := a.parsePackageJSON(fsys, packageJsonPath) + if errors.Is(err, fs.ErrNotExist) { + a.logger.Debug("package.json not found", log.FilePath(packageJsonPath)) + return nil + } else if err != nil { + return xerrors.Errorf("unable to parse root package.json: %w", err) + } + + // Since yarn.lock file can contain same packages with different versions + // we need to save versions separately for version comparison. + pkgs := lo.SliceToMap(app.Packages, func(pkg types.Package) (string, types.Package) { + return pkg.ID, pkg + }) + + if err := a.resolveRootDependencies(&root, pkgs, patterns); err != nil { + return xerrors.Errorf("unable to resolve root dependencies: %w", err) + } + + if err := a.resolveWorkspaceDependencies(workspaces, pkgs, patterns); err != nil { + return xerrors.Errorf("unable to resolve workspace dependencies: %w", err) + } + + pkgSlice := lo.Values(pkgs) + sort.Sort(types.Packages(pkgSlice)) + + // Save packages + app.Packages = pkgSlice + return nil +} + +func (a yarnAnalyzer) parsePackageJSON(fsys fs.FS, filePath string) (packagejson.Package, []packagejson.Package, error) { + // Parse package.json + f, err := fsys.Open(filePath) + if err != nil { + return packagejson.Package{}, nil, xerrors.Errorf("file open error: %w", err) + } + defer func() { _ = f.Close() }() + + root, err := a.packageJsonParser.Parse(f) + if err != nil { + return packagejson.Package{}, nil, xerrors.Errorf("parse error: %w", err) + } + + root.Package.ID = cmp.Or(root.Package.ID, filePath) // In case the package.json doesn't have a name or version + root.Package.Relationship = types.RelationshipRoot + + workspaces, err := a.traverseWorkspaces(fsys, path.Dir(filePath), root.Workspaces) + if err != nil { + return packagejson.Package{}, nil, xerrors.Errorf("traverse workspaces error: %w", err) + } + for i := range workspaces { + workspaces[i].Package.Relationship = types.RelationshipWorkspace + + // Add workspace as a child of root + root.DependsOn = append(root.DependsOn, workspaces[i].ID) + } + + return root, workspaces, nil +} + +func (a yarnAnalyzer) resolveRootDependencies(root *packagejson.Package, pkgs map[string]types.Package, + patterns map[string][]string) error { + if err := a.resolveDependencies(root, pkgs, patterns); err != nil { + return xerrors.Errorf("unable to resolve dependencies: %w", err) + } + + // Add root package to the package map + slices.Sort(root.Package.DependsOn) + pkgs[root.Package.ID] = root.Package + + return nil +} + +func (a yarnAnalyzer) resolveWorkspaceDependencies(workspaces []packagejson.Package, pkgs map[string]types.Package, + patterns map[string][]string) error { + if len(workspaces) == 0 { + return nil + } + + for _, workspace := range workspaces { + if err := a.resolveDependencies(&workspace, pkgs, patterns); err != nil { + return xerrors.Errorf("unable to resolve dependencies: %w", err) + } + + // Add workspace to the package map + slices.Sort(workspace.Package.DependsOn) + pkgs[workspace.ID] = workspace.Package + } + + return nil +} + +// resolveDependencies resolves production and development dependencies from direct dependencies and patterns. +// It also flags dependencies as direct or indirect and updates the dependencies of the parent package. +func (a yarnAnalyzer) resolveDependencies(pkg *packagejson.Package, pkgs map[string]types.Package, patterns map[string][]string) error { + // Recursively walk dependencies and flags development dependencies. + // Walk development dependencies first to avoid overwriting production dependencies. + directDevDeps := pkg.DevDependencies + if err := a.walkDependencies(&pkg.Package, pkgs, directDevDeps, patterns, true); err != nil { + return xerrors.Errorf("unable to walk dependencies: %w", err) + } + + // Recursively walk dependencies and flags production dependencies. + directProdDeps := lo.Assign(pkg.Dependencies, pkg.OptionalDependencies) + if err := a.walkDependencies(&pkg.Package, pkgs, directProdDeps, patterns, false); err != nil { + return xerrors.Errorf("unable to walk dependencies: %w", err) + } + + return nil +} + +// walkDependencies recursively walk dependencies and flags them as direct or indirect. +// Note that it overwrites the existing package map. +func (a yarnAnalyzer) walkDependencies(parent *types.Package, pkgs map[string]types.Package, directDeps map[string]string, + patterns map[string][]string, dev bool) error { + + // Identify direct dependencies + seenIDs := set.New[string]() + for _, pkg := range pkgs { + constraint, ok := directDeps[pkg.Name] + if !ok { + continue + } + + // Handle aliases + // cf. https://classic.yarnpkg.com/lang/en/docs/cli/add/#toc-yarn-add-alias + if m := fragmentRegexp.FindStringSubmatch(constraint); len(m) == 5 { + pkg.Name = m[2] // original name + constraint = m[4] + } + + // Try to find an exact match to the pattern. + // In some cases, patterns from yarn.lock and package.json may not match (e.g., yarn v2 uses the allowed version for ID). + // Therefore, if the patterns don't match - compare versions. + if pkgPatterns, found := patterns[pkg.ID]; !found || !slices.Contains(pkgPatterns, dependency.ID(types.Yarn, pkg.Name, constraint)) { + // npm has own comparer to compare versions + if match, err := a.comparer.MatchVersion(pkg.Version, constraint); err != nil { + return xerrors.Errorf("unable to match version for %s", pkg.Name) + } else if !match { + continue + } + } + + // If the package is already marked as a production dependency, skip overwriting it. + // Since the dev field is boolean, it cannot determine if the package is already processed, + // so we need to check the relationship field. + if pkg.Relationship == types.RelationshipUnknown || pkg.Dev { + pkg.Dev = dev + } + + // Mark as a direct dependency + pkg.Indirect = false + pkg.Relationship = types.RelationshipDirect + + pkgs[pkg.ID] = pkg + seenIDs.Append(pkg.ID) + + // Add a direct dependency to the parent package + parent.DependsOn = append(parent.DependsOn, pkg.ID) + + // Walk indirect dependencies + a.walkIndirectDependencies(pkg, pkgs, seenIDs) + } + + return nil +} + +func (a yarnAnalyzer) walkIndirectDependencies(pkg types.Package, pkgs map[string]types.Package, seenIDs set.Set[string]) { + for _, pkgID := range pkg.DependsOn { + if seenIDs.Contains(pkgID) { + continue // Skip if we've already seen this package + } + + dep, ok := pkgs[pkgID] + if !ok { + continue + } + + if dep.Relationship == types.RelationshipUnknown || dep.Dev { + dep.Dev = pkg.Dev + } + dep.Indirect = true + dep.Relationship = types.RelationshipIndirect + pkgs[dep.ID] = dep + + seenIDs.Append(dep.ID) + + // Recursively walk dependencies + a.walkIndirectDependencies(dep, pkgs, seenIDs) + } +} + +func (a yarnAnalyzer) traverseWorkspaces(fsys fs.FS, dir string, workspaces []string) ([]packagejson.Package, error) { + var pkgs []packagejson.Package + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.NpmPkg + } + + walkDirFunc := func(path string, _ fs.DirEntry, r io.Reader) error { + pkg, err := a.packageJsonParser.Parse(r) + if err != nil { + return xerrors.Errorf("unable to parse %q: %w", path, err) + } + pkg.Package.ID = cmp.Or(pkg.Package.ID, path) // In case the package.json doesn't have a name or version + pkgs = append(pkgs, pkg) + return nil + } + + for _, workspace := range workspaces { + // We need to add the path to the `package.json` file + // to properly get the pattern to search in `fs` + workspace = path.Join(dir, workspace) + matches, err := fs.Glob(fsys, workspace) + if err != nil { + return nil, err + } + for _, match := range matches { + if err := fsutils.WalkDir(fsys, match, required, walkDirFunc); err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + } + + } + return pkgs, nil +} + +func (a yarnAnalyzer) traverseLicenses(fsys fs.FS, lockPath string) (map[string][]string, error) { + sub, err := fs.Sub(fsys, path.Dir(lockPath)) + if err != nil { + return nil, xerrors.Errorf("fs error: %w", err) + } + var errs error + + // Yarn v1 + licenses, err := a.traverseYarnClassicPkgs(sub) + if err == nil { + return licenses, nil + } + errs = multierror.Append(errs, err) + + // Yarn v2+ + licenses, err = a.traverseYarnModernPkgs(sub) + if err == nil { + return licenses, nil + } + errs = multierror.Append(errs, err) + + return nil, errs +} + +func (a yarnAnalyzer) traverseYarnClassicPkgs(fsys fs.FS) (map[string][]string, error) { + return a.license.Traverse(fsys, "node_modules") +} + +func (a yarnAnalyzer) traverseYarnModernPkgs(fsys fs.FS) (map[string][]string, error) { + sub, err := fs.Sub(fsys, ".yarn") + if err != nil { + return nil, xerrors.Errorf("fs error: %w", err) + } + + var errs error + licenses := make(map[string][]string) + + if ll, err := a.traverseUnpluggedDir(sub); err != nil { + errs = multierror.Append(errs, err) + } else { + licenses = lo.Assign(licenses, ll) + } + + if ll, err := a.traverseCacheDir(sub); err != nil { + errs = multierror.Append(errs, err) + } else { + licenses = lo.Assign(licenses, ll) + } + + if len(licenses) == 0 { + return nil, errs + } + + return licenses, nil +} + +func (a yarnAnalyzer) traverseUnpluggedDir(fsys fs.FS) (map[string][]string, error) { + // `unplugged` hold machine-specific build artifacts + // Traverse .yarn/unplugged dir + return a.license.Traverse(fsys, "unplugged") +} + +func (a yarnAnalyzer) traverseCacheDir(fsys fs.FS) (map[string][]string, error) { + // Traverse .yarn/cache dir + licenses := make(map[string][]string) + err := fsutils.WalkDir(fsys, "cache", fsutils.RequiredExt(".zip"), + func(_ string, d fs.DirEntry, r io.Reader) error { + fi, err := d.Info() + if err != nil { + return xerrors.Errorf("file stat error: %w", err) + } + + rr, err := xio.NewReadSeekerAt(r) + if err != nil { + return xerrors.Errorf("reader error: %w", err) + } + + zr, err := zip.NewReader(rr, fi.Size()) + if err != nil { + return xerrors.Errorf("zip reader error: %w", err) + } + + if l, err := a.license.Traverse(zr, "node_modules"); err != nil { + return xerrors.Errorf("license traverse error: %w", err) + } else { + licenses = lo.Assign(licenses, l) + } + return nil + }) + + if err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + + return licenses, nil +} diff --git a/pkg/fanal/analyzer/language/nodejs/yarn/yarn_test.go b/pkg/fanal/analyzer/language/nodejs/yarn/yarn_test.go new file mode 100644 index 000000000000..e8efb274c3b4 --- /dev/null +++ b/pkg/fanal/analyzer/language/nodejs/yarn/yarn_test.go @@ -0,0 +1,1016 @@ +package yarn + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_yarnLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + dir: "testdata/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "yarn.lock", + Packages: types.Packages{ + { + ID: "90@1.0.0", + Name: "90", + Version: "1.0.0", + Relationship: types.RelationshipRoot, + Licenses: []string{ + "MIT", + }, + DependsOn: []string{ + "js-tokens@2.0.0", + "prop-types@15.7.2", + "scheduler@0.13.6", + }, + }, + { + ID: "js-tokens@2.0.0", + Name: "js-tokens", + Version: "2.0.0", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 8, + }, + }, + }, + { + ID: "prop-types@15.7.2", + Name: "prop-types", + Version: "15.7.2", + Dev: true, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 27, + EndLine: 34, + }, + }, + DependsOn: []string{ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "react-is@16.13.1", + }, + }, + { + ID: "scheduler@0.13.6", + Name: "scheduler", + Version: "0.13.6", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 41, + EndLine: 47, + }, + }, + DependsOn: []string{ + "loose-envify@1.4.0", + "object-assign@4.1.1", + }, + }, + { + ID: "js-tokens@4.0.0", + Name: "js-tokens", + Version: "4.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 10, + EndLine: 13, + }, + }, + }, + { + ID: "loose-envify@1.4.0", + Name: "loose-envify", + Version: "1.4.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 15, + EndLine: 20, + }, + }, + DependsOn: []string{ + "js-tokens@4.0.0", + }, + }, + { + ID: "object-assign@4.1.1", + Name: "object-assign", + Version: "4.1.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 25, + }, + }, + }, + { + ID: "react-is@16.13.1", + Name: "react-is", + Version: "16.13.1", + Dev: true, + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 36, + EndLine: 39, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "project with workspace placed in sub dir", + dir: "testdata/project-with-workspace-in-subdir", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "foo/yarn.lock", + Packages: types.Packages{ + { + ID: "@test/foo@1.0.0", + Name: "@test/foo", + Version: "1.0.0", + Relationship: types.RelationshipRoot, + Licenses: []string{ + "MIT", + }, + DependsOn: []string{ + "@test/bar-generators@0.0.1", + }, + }, + { + ID: "@test/bar-generators@0.0.1", + Name: "@test/bar-generators", + Version: "0.0.1", + Relationship: types.RelationshipWorkspace, + DependsOn: []string{ + "hoek@6.1.3", + }, + }, + { + ID: "hoek@6.1.3", + Name: "hoek", + Version: "6.1.3", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 8, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "no package.json", + dir: "testdata/no-packagejson", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "yarn.lock", + Packages: types.Packages{ + { + ID: "js-tokens@2.0.0", + Name: "js-tokens", + Version: "2.0.0", + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 8, + }, + }, + }, + { + ID: "js-tokens@4.0.0", + Name: "js-tokens", + Version: "4.0.0", + Locations: []types.Location{ + { + StartLine: 10, + EndLine: 13, + }, + }, + }, + { + ID: "loose-envify@1.4.0", + Name: "loose-envify", + Version: "1.4.0", + Locations: []types.Location{ + { + StartLine: 15, + EndLine: 20, + }, + }, + DependsOn: []string{ + "js-tokens@4.0.0", + }, + }, + { + ID: "object-assign@4.1.1", + Name: "object-assign", + Version: "4.1.1", + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 25, + }, + }, + }, + { + ID: "prop-types@15.7.2", + Name: "prop-types", + Version: "15.7.2", + Locations: []types.Location{ + { + StartLine: 27, + EndLine: 34, + }, + }, + DependsOn: []string{ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "react-is@16.13.1", + }, + }, + { + ID: "react-is@16.13.1", + Name: "react-is", + Version: "16.13.1", + Locations: []types.Location{ + { + StartLine: 36, + EndLine: 39, + }, + }, + }, + { + ID: "scheduler@0.13.6", + Name: "scheduler", + Version: "0.13.6", + Locations: []types.Location{ + { + StartLine: 41, + EndLine: 47, + }, + }, + DependsOn: []string{ + "loose-envify@1.4.0", + "object-assign@4.1.1", + }, + }, + }, + }, + }, + }, + }, + { + name: "wrong package.json", + dir: "testdata/wrong-packagejson", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "yarn.lock", + Packages: types.Packages{ + { + ID: "js-tokens@2.0.0", + Name: "js-tokens", + Version: "2.0.0", + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 8, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "unsupported_protocol", + dir: "testdata/unsupported_protocol", + want: &analyzer.AnalysisResult{}, + }, + // docker run --rm -it node@sha256:2d5e8a8a51bc341fd5f2eed6d91455c3a3d147e91a14298fc564b5dc519c1666 sh + // mkdir test && cd "$_" + // yarn set version 3.4.1 + // yarn add is-callable@1.2.7 is-odd@3.0.1 + // yarn unplug is-callable@1.2.7 + // rm .yarn/cache/is-callable-npm* + { + name: "parse licenses (yarn v2+)", + dir: "testdata/yarn-licenses", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "yarn.lock", + Packages: []types.Package{ + { + ID: "yarn-3-licenses@1.0.0", + Name: "yarn-3-licenses", + Version: "1.0.0", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "is-callable@1.2.7", + "is-odd@3.0.1", + }, + }, + { + ID: "is-callable@1.2.7", + Name: "is-callable", + Version: "1.2.7", + Relationship: types.RelationshipDirect, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 8, + EndLine: 13, + }, + }, + }, + { + ID: "is-odd@3.0.1", + Name: "is-odd", + Version: "3.0.1", + Licenses: []string{"MIT"}, + Relationship: types.RelationshipDirect, + DependsOn: []string{"is-number@6.0.0"}, + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 29, + }, + }, + }, + { + ID: "is-number@6.0.0", + Name: "is-number", + Version: "6.0.0", + Licenses: []string{"MIT"}, + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 15, + EndLine: 20, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "package uses `latest` version", + dir: "testdata/latest-version", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "yarn.lock", + Packages: types.Packages{ + { + ID: "package.json", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "debug@4.3.5", + "js-tokens@9.0.0", + }, + }, + { + ID: "debug@4.3.5", + Name: "debug", + Version: "4.3.5", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + DependsOn: []string{ + "ms@2.1.2", + }, + }, + { + ID: "js-tokens@9.0.0", + Name: "js-tokens", + Version: "9.0.0", + Relationship: types.RelationshipDirect, + Dev: true, + Locations: []types.Location{ + { + StartLine: 12, + EndLine: 15, + }, + }, + }, + { + ID: "ms@2.1.2", + Name: "ms", + Version: "2.1.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 17, + EndLine: 20, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with alias rewrite", + dir: "testdata/alias", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "yarn.lock", + Packages: types.Packages{ + { + ID: "test@1.0.0", + Name: "test", + Version: "1.0.0", + Relationship: types.RelationshipRoot, + Licenses: []string{ + "MIT", + }, + DependsOn: []string{ + "foo-debug@4.3.4", + "foo-json@0.8.33", + "foo-ms@2.1.3", + "foo-uuid@9.0.7", + }, + }, + { + ID: "foo-json@0.8.33", + Name: "@types/jsonstream", + Version: "0.8.33", + Indirect: false, + Relationship: types.RelationshipDirect, + Dev: true, + Locations: []types.Location{ + { + StartLine: 19, + EndLine: 24, + }, + }, + DependsOn: []string{ + "@types/node@20.10.5", + }, + }, + { + ID: "foo-uuid@9.0.7", + Name: "@types/uuid", + Version: "9.0.7", + Indirect: false, + Relationship: types.RelationshipDirect, + Dev: true, + Locations: []types.Location{ + { + StartLine: 31, + EndLine: 34, + }, + }, + }, + { + ID: "foo-debug@4.3.4", + Name: "debug", + Version: "4.3.4", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 12, + EndLine: 17, + }, + }, + DependsOn: []string{ + "ms@2.1.2", + }, + }, + { + ID: "foo-ms@2.1.3", + Name: "ms", + Version: "2.1.3", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 26, + EndLine: 29, + }, + }, + }, + { + ID: "@types/node@20.10.5", + Name: "@types/node", + Version: "20.10.5", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + DependsOn: []string{ + "undici-types@5.26.5", + }, + }, + { + ID: "ms@2.1.2", + Name: "ms", + Version: "2.1.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 36, + EndLine: 39, + }, + }, + }, + { + ID: "undici-types@5.26.5", + Name: "undici-types", + Version: "5.26.5", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + Locations: []types.Location{ + { + StartLine: 41, + EndLine: 44, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "monorepo", + dir: "testdata/monorepo", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "yarn.lock", + Packages: types.Packages{ + { + ID: "yarn-workspace-test@1.0.0", + Name: "yarn-workspace-test", + Version: "1.0.0", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "c@0.0.0", + "package1@0.0.0", + "packages/package2/package.json", + "prettier@2.8.8", + "util1@0.0.0", + }, + }, + { + ID: "packages/package2/package.json", + Relationship: types.RelationshipWorkspace, + DependsOn: []string{ + "is-odd@3.0.1", + }, + }, + { + ID: "c@0.0.0", + Name: "c", + Version: "0.0.0", + Relationship: types.RelationshipWorkspace, + DependsOn: []string{ + "is-number@7.0.0", + }, + }, + { + ID: "package1@0.0.0", + Name: "package1", + Version: "0.0.0", + Relationship: types.RelationshipWorkspace, + DependsOn: []string{ + "scheduler@0.23.0", + }, + }, + { + ID: "util1@0.0.0", + Name: "util1", + Version: "0.0.0", + Relationship: types.RelationshipWorkspace, + DependsOn: []string{ + "js-tokens@8.0.1", + "prop-types@15.8.1", + }, + }, + { + ID: "is-number@7.0.0", + Name: "is-number", + Version: "7.0.0", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 23, + EndLine: 28, + }, + }, + }, + { + ID: "is-odd@3.0.1", + Name: "is-odd", + Version: "3.0.1", + Relationship: types.RelationshipDirect, + DependsOn: []string{"is-number@6.0.0"}, + Locations: []types.Location{ + { + StartLine: 30, + EndLine: 37, + }, + }, + }, + { + ID: "js-tokens@8.0.1", + Name: "js-tokens", + Version: "8.0.1", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 46, + EndLine: 51, + }, + }, + }, + { + ID: "prettier@2.8.8", + Name: "prettier", + Version: "2.8.8", + Relationship: types.RelationshipDirect, + Dev: true, + Locations: []types.Location{ + { + StartLine: 87, + EndLine: 94, + }, + }, + }, + { + ID: "prop-types@15.8.1", + Name: "prop-types", + Version: "15.8.1", + Relationship: types.RelationshipDirect, + Dev: true, + Locations: []types.Location{ + { + StartLine: 96, + EndLine: 105, + }, + }, + DependsOn: []string{ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "react-is@16.13.1", + }, + }, + { + ID: "scheduler@0.23.0", + Name: "scheduler", + Version: "0.23.0", + Relationship: types.RelationshipDirect, + DependsOn: []string{"loose-envify@1.4.0"}, + Locations: []types.Location{ + { + StartLine: 114, + EndLine: 121, + }, + }, + }, + { + ID: "is-number@6.0.0", + Name: "is-number", + Version: "6.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 16, + EndLine: 21, + }, + }, + }, + { + ID: "js-tokens@4.0.0", + Name: "js-tokens", + Version: "4.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 39, + EndLine: 44, + }, + }, + }, + { + ID: "loose-envify@1.4.0", + Name: "loose-envify", + Version: "1.4.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"js-tokens@4.0.0"}, + Locations: []types.Location{ + { + StartLine: 53, + EndLine: 62, + }, + }, + }, + { + ID: "object-assign@4.1.1", + Name: "object-assign", + Version: "4.1.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + Locations: []types.Location{ + { + StartLine: 64, + EndLine: 69, + }, + }, + }, + { + ID: "react-is@16.13.1", + Name: "react-is", + Version: "16.13.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + Locations: []types.Location{ + { + StartLine: 107, + EndLine: 112, + }, + }, + }, + }, + }, + }, + }, + }, + // docker run --rm -it node@sha256:2d5e8a8a51bc341fd5f2eed6d91455c3a3d147e91a14298fc564b5dc519c1666 sh + // mkdir test && cd "$_" + // yarn set version 1.22.19 + // yarn add @vue/compiler-sfc@2.7.14 + { + name: "parse licenses (yarn classic)", + dir: "testdata/yarn-classic-licenses", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Yarn, + FilePath: "yarn.lock", + Packages: []types.Package{ + { + ID: "package.json", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "@vue/compiler-sfc@2.7.14", + }, + }, + { + ID: "@vue/compiler-sfc@2.7.14", + Name: "@vue/compiler-sfc", + Version: "2.7.14", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 10, + EndLine: 17, + }, + }, + Licenses: []string{"MIT"}, + DependsOn: []string{ + "@babel/parser@7.22.7", + "postcss@8.4.27", + "source-map@0.6.1", + }, + }, + { + ID: "@babel/parser@7.22.7", + Name: "@babel/parser", + Version: "7.22.7", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 8, + }, + }, + Licenses: []string{"MIT"}, + }, + { + ID: "nanoid@3.3.6", + Name: "nanoid", + Version: "3.3.6", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 19, + EndLine: 22, + }, + }, + Licenses: []string{"MIT"}, + }, + { + ID: "picocolors@1.0.0", + Name: "picocolors", + Version: "1.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 24, + EndLine: 27, + }, + }, + Licenses: []string{"ISC"}, + }, + { + ID: "postcss@8.4.27", + Name: "postcss", + Version: "8.4.27", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 29, + EndLine: 36, + }, + }, + Licenses: []string{"MIT"}, + DependsOn: []string{ + "nanoid@3.3.6", + "picocolors@1.0.0", + "source-map-js@1.0.2", + }, + }, + { + ID: "source-map@0.6.1", + Name: "source-map", + Version: "0.6.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 43, + EndLine: 46, + }, + }, + Licenses: []string{"BSD-3-Clause"}, + }, + { + ID: "source-map-js@1.0.2", + Name: "source-map-js", + Version: "1.0.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 38, + EndLine: 41, + }, + }, + Licenses: []string{"BSD-3-Clause"}, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newYarnAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_yarnLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path yarn.lock", + filePath: "test/yarn.lock", + want: true, + }, + { + name: "sad path", + filePath: "test/package-lock.json", + want: false, + }, + { + name: "yarn cache", + filePath: ".yarn/cache/websocket-driver-npm-0.7.4-a72739da70-fffe5a33fe.zip", + want: true, + }, + { + name: "not a yarn cache", + filePath: "cache/is-number-npm-6.0.0-30881e83e6-f73bfced03.zip", + want: false, + }, + { + name: "yarn.lock in node_modules", + filePath: "somedir/node_modules/uri-js/yarn.lock", + want: false, + }, + { + name: "yarn.lock in unplugged", + filePath: "somedir/.yarn/unplugged/uri-js/yarn.lock", + want: false, + }, + { + name: "deep package.json", + filePath: "somedir/node_modules/canvg/node_modules/parse5/package.json", + want: true, + }, + { + name: "license file", + filePath: "node_modules/@vue/compiler-sfc/LICENSE", + want: true, + }, + { + name: "txt license file", + filePath: "node_modules/@vue/compiler-sfc/LICENSE.txt", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := yarnAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/php/composer/composer.go b/pkg/fanal/analyzer/language/php/composer/composer.go new file mode 100644 index 000000000000..8eada9dfe761 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/composer.go @@ -0,0 +1,152 @@ +package composer + +import ( + "context" + "encoding/json" + "errors" + "io" + "io/fs" + "os" + "path/filepath" + "slices" + "sort" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/php/composer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeComposer, newComposerAnalyzer) +} + +const composerAnalyzerVersion = 1 + +var requiredFiles = []string{ + types.ComposerLock, + types.ComposerJson, +} + +type composerAnalyzer struct { + lockParser language.Parser +} + +func newComposerAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &composerAnalyzer{ + lockParser: composer.NewParser(), + }, nil +} + +func (a composerAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.ComposerLock || input.FilePatterns.Match(path) + } + + err := fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error { + // Parse composer.lock + app, err := a.parseComposerLock(ctx, path, r) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + // Parse composer.json alongside composer.lock to identify the direct dependencies + if err = a.mergeComposerJson(input.FS, filepath.Dir(path), app); err != nil { + log.Warn("Unable to parse composer.json to identify direct dependencies", + log.FilePath(filepath.Join(filepath.Dir(path), types.ComposerJson)), log.Err(err)) + } + sort.Sort(app.Packages) + apps = append(apps, *app) + + return nil + }) + if err != nil { + return nil, xerrors.Errorf("composer walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a composerAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + if !slices.Contains(requiredFiles, fileName) { + return false + } + + // Skip `composer.lock` inside `vendor` folder + if slices.Contains(strings.Split(filePath, "/"), "vendor") { + return false + } + return true +} + +func (a composerAnalyzer) Type() analyzer.Type { + return analyzer.TypeComposer +} + +func (a composerAnalyzer) Version() int { + return composerAnalyzerVersion +} + +func (a composerAnalyzer) parseComposerLock(ctx context.Context, path string, r io.Reader) (*types.Application, error) { + return language.Parse(ctx, types.Composer, path, r, a.lockParser) +} + +func (a composerAnalyzer) mergeComposerJson(fsys fs.FS, dir string, app *types.Application) error { + // Parse composer.json to identify the direct dependencies + path := filepath.Join(dir, types.ComposerJson) + cj, err := a.parseComposerJson(fsys, path) + if errors.Is(err, fs.ErrNotExist) { + // Assume all the packages are direct dependencies as it cannot identify them from composer.lock + log.Debug("Unable to determine the direct dependencies, composer.json not found", log.FilePath(path)) + return nil + } else if err != nil { + return xerrors.Errorf("unable to parse %s: %w", path, err) + } + + for i, pkg := range app.Packages { + // Identify the direct/transitive dependencies + if _, ok := cj.Require[pkg.Name]; ok { + app.Packages[i].Relationship = types.RelationshipDirect + } else if _, ok := cj.RequireDev[pkg.Name]; ok { + app.Packages[i].Relationship = types.RelationshipDirect + } else { + app.Packages[i].Indirect = true + app.Packages[i].Relationship = types.RelationshipIndirect + } + } + + return nil +} + +type composerJson struct { + Require map[string]string `json:"require"` + RequireDev map[string]string `json:"require-dev"` +} + +func (a composerAnalyzer) parseComposerJson(fsys fs.FS, path string) (composerJson, error) { + // Parse composer.json + f, err := fsys.Open(path) + if err != nil { + return composerJson{}, xerrors.Errorf("file open error: %w", err) + } + defer func() { _ = f.Close() }() + + var jsonFile composerJson + err = json.NewDecoder(f).Decode(&jsonFile) + if err != nil { + return composerJson{}, xerrors.Errorf("json decode error: %w", err) + } + return jsonFile, nil +} diff --git a/pkg/fanal/analyzer/language/php/composer/composer_test.go b/pkg/fanal/analyzer/language/php/composer/composer_test.go new file mode 100644 index 000000000000..68fe5906df45 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/composer_test.go @@ -0,0 +1,228 @@ +package composer + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_composerAnalyzer_PostAnalyze(t *testing.T) { + tests := []struct { + name string + dir string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + dir: "testdata/composer/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Composer, + FilePath: "composer.lock", + Packages: types.Packages{ + { + ID: "pear/log@1.13.3", + Name: "pear/log", + Version: "1.13.3", + Indirect: false, + Relationship: types.RelationshipDirect, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 9, + EndLine: 68, + }, + }, + DependsOn: []string{"pear/pear_exception@v1.0.2"}, + }, + { + ID: "pear/pear_exception@v1.0.2", + Name: "pear/pear_exception", + Version: "v1.0.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Licenses: []string{"BSD-2-Clause"}, + Locations: []types.Location{ + { + StartLine: 69, + EndLine: 127, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "no composer.json", + dir: "testdata/composer/no-composer-json", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Composer, + FilePath: "composer.lock", + Packages: types.Packages{ + { + ID: "pear/log@1.13.3", + Name: "pear/log", + Version: "1.13.3", + Indirect: false, + Relationship: types.RelationshipUnknown, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 9, + EndLine: 68, + }, + }, + DependsOn: []string{"pear/pear_exception@v1.0.2"}, + }, + { + ID: "pear/pear_exception@v1.0.2", + Name: "pear/pear_exception", + Version: "v1.0.2", + Indirect: false, + Relationship: types.RelationshipUnknown, + Licenses: []string{"BSD-2-Clause"}, + Locations: []types.Location{ + { + StartLine: 69, + EndLine: 127, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "wrong composer.json", + dir: "testdata/composer/wrong-composer-json", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Composer, + FilePath: "composer.lock", + Packages: types.Packages{ + { + ID: "pear/log@1.13.3", + Name: "pear/log", + Version: "1.13.3", + Indirect: false, + Relationship: types.RelationshipUnknown, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 9, + EndLine: 68, + }, + }, + DependsOn: []string{"pear/pear_exception@v1.0.2"}, + }, + { + ID: "pear/pear_exception@v1.0.2", + Name: "pear/pear_exception", + Version: "v1.0.2", + Indirect: false, + Relationship: types.RelationshipUnknown, + Licenses: []string{"BSD-2-Clause"}, + Locations: []types.Location{ + { + StartLine: 69, + EndLine: 127, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "broken composer.lock", + dir: "testdata/composer/sad", + want: &analyzer.AnalysisResult{}, + }, + { + name: "with dev dependencies", + dir: "testdata/composer/with-dev", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Composer, + FilePath: "composer.lock", + Packages: types.Packages{ + { + ID: "pear/log@1.14.6", + Name: "pear/log", + Version: "1.14.6", + Dev: true, + Indirect: false, + Relationship: types.RelationshipDirect, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 61, + EndLine: 121, + }, + }, + DependsOn: []string{"pear/pear_exception@v1.0.2"}, + }, + { + ID: "psr/log@1.1.4", + Name: "psr/log", + Version: "1.1.4", + Indirect: false, + Relationship: types.RelationshipDirect, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 9, + EndLine: 58, + }, + }, + }, + { + ID: "pear/pear_exception@v1.0.2", + Name: "pear/pear_exception", + Version: "v1.0.2", + Dev: true, + Indirect: true, + Relationship: types.RelationshipIndirect, + Licenses: []string{"BSD-2-Clause"}, + Locations: []types.Location{ + { + StartLine: 122, + EndLine: 180, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newComposerAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer-vendor/happy/installed.json b/pkg/fanal/analyzer/language/php/composer/testdata/composer-vendor/happy/installed.json new file mode 100644 index 000000000000..e44e60d9050a --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer-vendor/happy/installed.json @@ -0,0 +1,131 @@ +{ + "packages": [ + { + "name": "pear/log", + "version": "1.13.3", + "version_normalized": "1.13.3.0", + "source": { + "type": "git", + "url": "https://github.com/pear/Log.git", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Log/zipball/21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "shasum": "" + }, + "require": { + "pear/pear_exception": "1.0.1 || 1.0.2", + "php": ">5.2" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "pear/db": "Install optionally via your project's composer.json" + }, + "time": "2021-05-04T23:51:30+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Log": "./" + }, + "exclude-from-classmap": [ + "/examples/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jon Parise", + "email": "jon@php.net", + "homepage": "http://www.indelible.org", + "role": "Developer" + } + ], + "description": "PEAR Logging Framework", + "homepage": "http://pear.github.io/Log/", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/pear/Log/issues", + "source": "https://github.com/pear/Log" + }, + "install-path": "../pear/log" + }, + { + "name": "pear/pear_exception", + "version": "v1.0.2", + "version_normalized": "1.0.2.0", + "source": { + "type": "git", + "url": "https://github.com/pear/PEAR_Exception.git", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "<9" + }, + "time": "2021-03-21T15:43:46+00:00", + "type": "class", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "classmap": [ + "PEAR/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "." + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Helgi Thormar", + "email": "dufuz@php.net" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net" + } + ], + "description": "The PEAR Exception base class.", + "homepage": "https://github.com/pear/PEAR_Exception", + "keywords": [ + "exception" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", + "source": "https://github.com/pear/PEAR_Exception" + }, + "install-path": "../pear/pear_exception" + } + ], + "dev": true, + "dev-package-names": [] +} diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer-vendor/sad/installed.json b/pkg/fanal/analyzer/language/php/composer/testdata/composer-vendor/sad/installed.json new file mode 100644 index 000000000000..81750b96f9d8 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer-vendor/sad/installed.json @@ -0,0 +1 @@ +{ \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer/happy/composer.json b/pkg/fanal/analyzer/language/php/composer/testdata/composer/happy/composer.json new file mode 100644 index 000000000000..b2311cac15d4 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer/happy/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "pear/log": "^1.13" + } +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer/happy/composer.lock b/pkg/fanal/analyzer/language/php/composer/testdata/composer/happy/composer.lock new file mode 100644 index 000000000000..d842a30b9231 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer/happy/composer.lock @@ -0,0 +1,138 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2450af46c78f9ecbca6976876bdd04c2", + "packages": [ + { + "name": "pear/log", + "version": "1.13.3", + "source": { + "type": "git", + "url": "https://github.com/pear/Log.git", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Log/zipball/21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "shasum": "" + }, + "require": { + "pear/pear_exception": "1.0.1 || 1.0.2", + "php": ">5.2" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "pear/db": "Install optionally via your project's composer.json" + }, + "type": "library", + "autoload": { + "psr-0": { + "Log": "./" + }, + "exclude-from-classmap": [ + "/examples/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jon Parise", + "email": "jon@php.net", + "homepage": "http://www.indelible.org", + "role": "Developer" + } + ], + "description": "PEAR Logging Framework", + "homepage": "http://pear.github.io/Log/", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/pear/Log/issues", + "source": "https://github.com/pear/Log" + }, + "time": "2021-05-04T23:51:30+00:00" + }, + { + "name": "pear/pear_exception", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/pear/PEAR_Exception.git", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "<9" + }, + "type": "class", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "PEAR/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "." + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Helgi Thormar", + "email": "dufuz@php.net" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net" + } + ], + "description": "The PEAR Exception base class.", + "homepage": "https://github.com/pear/PEAR_Exception", + "keywords": [ + "exception" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", + "source": "https://github.com/pear/PEAR_Exception" + }, + "time": "2021-03-21T15:43:46+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer/no-composer-json/composer.lock b/pkg/fanal/analyzer/language/php/composer/testdata/composer/no-composer-json/composer.lock new file mode 100644 index 000000000000..d842a30b9231 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer/no-composer-json/composer.lock @@ -0,0 +1,138 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2450af46c78f9ecbca6976876bdd04c2", + "packages": [ + { + "name": "pear/log", + "version": "1.13.3", + "source": { + "type": "git", + "url": "https://github.com/pear/Log.git", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Log/zipball/21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "shasum": "" + }, + "require": { + "pear/pear_exception": "1.0.1 || 1.0.2", + "php": ">5.2" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "pear/db": "Install optionally via your project's composer.json" + }, + "type": "library", + "autoload": { + "psr-0": { + "Log": "./" + }, + "exclude-from-classmap": [ + "/examples/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jon Parise", + "email": "jon@php.net", + "homepage": "http://www.indelible.org", + "role": "Developer" + } + ], + "description": "PEAR Logging Framework", + "homepage": "http://pear.github.io/Log/", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/pear/Log/issues", + "source": "https://github.com/pear/Log" + }, + "time": "2021-05-04T23:51:30+00:00" + }, + { + "name": "pear/pear_exception", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/pear/PEAR_Exception.git", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "<9" + }, + "type": "class", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "PEAR/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "." + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Helgi Thormar", + "email": "dufuz@php.net" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net" + } + ], + "description": "The PEAR Exception base class.", + "homepage": "https://github.com/pear/PEAR_Exception", + "keywords": [ + "exception" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", + "source": "https://github.com/pear/PEAR_Exception" + }, + "time": "2021-03-21T15:43:46+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer/sad/composer.lock b/pkg/fanal/analyzer/language/php/composer/testdata/composer/sad/composer.lock new file mode 100644 index 000000000000..81750b96f9d8 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer/sad/composer.lock @@ -0,0 +1 @@ +{ \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.json b/pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.json new file mode 100644 index 000000000000..dfd478b82ae1 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.json @@ -0,0 +1,8 @@ +{ + "require": { + "psr/log": "^1.0" + }, + "require-dev": { + "pear/log": "^1.13" + } +} diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.lock b/pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.lock new file mode 100644 index 000000000000..9101629c47df --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.lock @@ -0,0 +1,190 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2c9e13a2460669ca09226814c0aefb51", + "packages": [ + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" + } + ], + "packages-dev": [ + { + "name": "pear/log", + "version": "1.14.6", + "source": { + "type": "git", + "url": "https://github.com/pear/Log.git", + "reference": "e136d31ff6d5991e9707862f5fbfb97d40cd37a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Log/zipball/e136d31ff6d5991e9707862f5fbfb97d40cd37a3", + "reference": "e136d31ff6d5991e9707862f5fbfb97d40cd37a3", + "shasum": "" + }, + "require": { + "pear/pear_exception": "1.0.1 || 1.0.2", + "php": ">=7.4" + }, + "require-dev": { + "phpunit/phpunit": "*", + "rector/rector": "*" + }, + "suggest": { + "pear/db": "Install optionally via your project's composer.json" + }, + "type": "library", + "autoload": { + "psr-0": { + "Log": "./" + }, + "exclude-from-classmap": [ + "/examples/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jon Parise", + "email": "jon@php.net", + "homepage": "https://www.indelible.org/", + "role": "Developer" + } + ], + "description": "PEAR Logging Framework", + "homepage": "https://pear.github.io/Log/", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/pear/Log/issues", + "source": "https://github.com/pear/Log" + }, + "time": "2025-07-27T00:25:20+00:00" + }, + { + "name": "pear/pear_exception", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/pear/PEAR_Exception.git", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "<9" + }, + "type": "class", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "PEAR/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "." + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Helgi Thormar", + "email": "dufuz@php.net" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net" + } + ], + "description": "The PEAR Exception base class.", + "homepage": "https://github.com/pear/PEAR_Exception", + "keywords": [ + "exception" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", + "source": "https://github.com/pear/PEAR_Exception" + }, + "time": "2021-03-21T15:43:46+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": {}, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer/wrong-composer-json/composer.json b/pkg/fanal/analyzer/language/php/composer/testdata/composer/wrong-composer-json/composer.json new file mode 100644 index 000000000000..81750b96f9d8 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer/wrong-composer-json/composer.json @@ -0,0 +1 @@ +{ \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/php/composer/testdata/composer/wrong-composer-json/composer.lock b/pkg/fanal/analyzer/language/php/composer/testdata/composer/wrong-composer-json/composer.lock new file mode 100644 index 000000000000..d842a30b9231 --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/testdata/composer/wrong-composer-json/composer.lock @@ -0,0 +1,138 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "2450af46c78f9ecbca6976876bdd04c2", + "packages": [ + { + "name": "pear/log", + "version": "1.13.3", + "source": { + "type": "git", + "url": "https://github.com/pear/Log.git", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/Log/zipball/21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "reference": "21af0be11669194d72d88b5ee9d5f176dc75d9a3", + "shasum": "" + }, + "require": { + "pear/pear_exception": "1.0.1 || 1.0.2", + "php": ">5.2" + }, + "require-dev": { + "phpunit/phpunit": "*" + }, + "suggest": { + "pear/db": "Install optionally via your project's composer.json" + }, + "type": "library", + "autoload": { + "psr-0": { + "Log": "./" + }, + "exclude-from-classmap": [ + "/examples/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "" + ], + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jon Parise", + "email": "jon@php.net", + "homepage": "http://www.indelible.org", + "role": "Developer" + } + ], + "description": "PEAR Logging Framework", + "homepage": "http://pear.github.io/Log/", + "keywords": [ + "log", + "logging" + ], + "support": { + "issues": "https://github.com/pear/Log/issues", + "source": "https://github.com/pear/Log" + }, + "time": "2021-05-04T23:51:30+00:00" + }, + { + "name": "pear/pear_exception", + "version": "v1.0.2", + "source": { + "type": "git", + "url": "https://github.com/pear/PEAR_Exception.git", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "<9" + }, + "type": "class", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "PEAR/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "." + ], + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Helgi Thormar", + "email": "dufuz@php.net" + }, + { + "name": "Greg Beaver", + "email": "cellog@php.net" + } + ], + "description": "The PEAR Exception base class.", + "homepage": "https://github.com/pear/PEAR_Exception", + "keywords": [ + "exception" + ], + "support": { + "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception", + "source": "https://github.com/pear/PEAR_Exception" + }, + "time": "2021-03-21T15:43:46+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.3.0" +} diff --git a/pkg/fanal/analyzer/language/php/composer/vendor.go b/pkg/fanal/analyzer/language/php/composer/vendor.go new file mode 100644 index 000000000000..5eeedcf06f7a --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/vendor.go @@ -0,0 +1,39 @@ +package composer + +import ( + "context" + "os" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/php/composer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&composerVendorAnalyzer{}) +} + +const ( + composerInstalledAnalyzerVersion = 1 +) + +// composerVendorAnalyzer analyzes 'installed.json' +type composerVendorAnalyzer struct{} + +func (a composerVendorAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + return language.Analyze(ctx, types.ComposerVendor, input.FilePath, input.Content, composer.NewParser()) +} + +func (a composerVendorAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Base(filePath) == types.ComposerInstalledJson +} + +func (a composerVendorAnalyzer) Type() analyzer.Type { + return analyzer.TypeComposerVendor +} + +func (a composerVendorAnalyzer) Version() int { + return composerInstalledAnalyzerVersion +} diff --git a/pkg/fanal/analyzer/language/php/composer/vendor_test.go b/pkg/fanal/analyzer/language/php/composer/vendor_test.go new file mode 100644 index 000000000000..bb7a051bfc2b --- /dev/null +++ b/pkg/fanal/analyzer/language/php/composer/vendor_test.go @@ -0,0 +1,120 @@ +package composer + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_composerVendorAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/composer-vendor/happy/installed.json", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.ComposerVendor, + FilePath: "testdata/composer-vendor/happy/installed.json", + Packages: []types.Package{ + { + ID: "pear/log@1.13.3", + Name: "pear/log", + Version: "1.13.3", + Indirect: false, + Relationship: types.RelationshipUnknown, + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 3, + EndLine: 65, + }, + }, + DependsOn: []string{"pear/pear_exception@v1.0.2"}, + }, + { + ID: "pear/pear_exception@v1.0.2", + Name: "pear/pear_exception", + Version: "v1.0.2", + Indirect: false, + Relationship: types.RelationshipUnknown, + Licenses: []string{"BSD-2-Clause"}, + Locations: []types.Location{ + { + StartLine: 66, + EndLine: 127, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "sad path", + inputFile: "testdata/composer-vendor/sad/installed.json", + wantErr: "decode error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer func() { + err = f.Close() + require.NoError(t, err) + }() + + a := composerVendorAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + require.Equal(t, tt.want, got) + }) + } +} + +func Test_composerVendorAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path", + filePath: "app/vendor/composer/installed.json", + want: true, + }, + { + name: "sad path", + filePath: "composer.json", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := composerVendorAnalyzer{} + got := a.Required(tt.filePath, nil) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/python/packaging/egg.go b/pkg/fanal/analyzer/language/python/packaging/egg.go new file mode 100644 index 000000000000..1f707f3a971d --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/egg.go @@ -0,0 +1,126 @@ +package packaging + +import ( + "archive/zip" + "context" + "io" + "os" + "path" + "path/filepath" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/packaging" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +func init() { + analyzer.RegisterAnalyzer(&eggAnalyzer{}) +} + +const ( + eggAnalyzerVersion = 1 + eggExt = ".egg" +) + +type eggAnalyzer struct { + logger *log.Logger + licenseClassifierConfidenceLevel float64 +} + +func (a *eggAnalyzer) Init(opt analyzer.AnalyzerOptions) error { + a.logger = log.WithPrefix("python") + a.licenseClassifierConfidenceLevel = opt.LicenseScannerOption.ClassifierConfidenceLevel + return nil +} + +// Analyze analyzes egg archive files +func (a *eggAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + // .egg file is zip format and PKG-INFO needs to be extracted from the zip file. + pkginfoInZip, err := findFileInZip(input.Content, input.Info.Size(), isEggFile) + if err != nil { + return nil, xerrors.Errorf("unable to open `.egg` archive: %w", err) + } + + // Egg archive may not contain required files, then we will get nil. Skip this archives + if pkginfoInZip == nil { + return nil, nil + } + + rsa, err := xio.NewReadSeekerAt(pkginfoInZip) + if err != nil { + return nil, xerrors.Errorf("unable to convert PKG-INFO reader: %w", err) + } + + app, err := language.ParsePackage(ctx, types.PythonPkg, input.FilePath, rsa, packaging.NewParser(), input.Options.FileChecksum) + if err != nil { + return nil, xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil, nil + } + + opener := func(licPath string) (io.ReadCloser, error) { + required := func(filePath string) bool { + return path.Base(filePath) == licPath + } + + f, err := findFileInZip(input.Content, input.Info.Size(), required) + if err != nil { + return nil, xerrors.Errorf("unable to find license file in `*.egg` file: %w", err) + } else if f == nil { // zip doesn't contain license file + return nil, nil + } + + return f, nil + } + + if err = fillAdditionalData(opener, app, a.licenseClassifierConfidenceLevel); err != nil { + a.logger.Warn("Unable to collect additional info", log.Err(err)) + } + + return &analyzer.AnalysisResult{ + Applications: []types.Application{*app}, + }, nil +} + +func findFileInZip(r xio.ReadSeekerAt, zipSize int64, required func(filePath string) bool) (io.ReadCloser, error) { + if _, err := r.Seek(0, io.SeekStart); err != nil { + return nil, xerrors.Errorf("file seek error: %w", err) + } + + zr, err := zip.NewReader(r, zipSize) + if err != nil { + return nil, xerrors.Errorf("zip reader error: %w", err) + } + + found, ok := lo.Find(zr.File, func(f *zip.File) bool { + return required(f.Name) + }) + if !ok { + return nil, nil + } + + f, err := found.Open() + if err != nil { + return nil, xerrors.Errorf("unable to open file in zip: %w", err) + } + + return f, nil +} + +func (a *eggAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Ext(filePath) == eggExt +} + +func (a *eggAnalyzer) Type() analyzer.Type { + return analyzer.TypePythonPkgEgg +} + +func (a *eggAnalyzer) Version() int { + return eggAnalyzerVersion +} diff --git a/pkg/fanal/analyzer/language/python/packaging/egg_test.go b/pkg/fanal/analyzer/language/python/packaging/egg_test.go new file mode 100644 index 000000000000..709705f88e49 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/egg_test.go @@ -0,0 +1,145 @@ +package packaging + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_eggAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + includeChecksum bool + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "egg zip", + inputFile: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PythonPkg, + FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg", + Packages: types.Packages{ + { + Name: "kitchen", + Version: "1.2.6", + Licenses: []string{ + "GNU Library or Lesser General Public License (LGPL)", + }, + FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg", + }, + }, + }, + }, + }, + }, + { + name: "egg zip with checksum", + inputFile: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg", + includeChecksum: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PythonPkg, + FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg", + Packages: types.Packages{ + { + Name: "kitchen", + Version: "1.2.6", + Licenses: []string{ + "GNU Library or Lesser General Public License (LGPL)", + }, + FilePath: "testdata/egg-zip/kitchen-1.2.6-py2.7.egg", + Digest: "sha1:4e13b6e379966771e896ee43cf8e240bf6083dca", + }, + }, + }, + }, + }, + }, + { + name: "egg zip with license file", + inputFile: "testdata/egg-zip-with-license-file/sample_package.egg", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PythonPkg, + FilePath: "testdata/egg-zip-with-license-file/sample_package.egg", + Packages: types.Packages{ + { + Name: "sample_package", + Version: "0.1", + Licenses: []string{ + "MIT", + }, + FilePath: "testdata/egg-zip-with-license-file/sample_package.egg", + }, + }, + }, + }, + }, + }, + { + name: "egg zip doesn't contain required files", + inputFile: "testdata/no-req-files/no-required-files.egg", + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + fileInfo, err := os.Lstat(tt.inputFile) + require.NoError(t, err) + + a := &eggAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + Content: f, + FilePath: tt.inputFile, + Info: fileInfo, + Options: analyzer.AnalysisOptions{ + FileChecksum: tt.includeChecksum, + }, + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } + +} + +func Test_eggAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "egg zip", + filePath: "python2.7/site-packages/cssutils-1.0-py2.7.egg", + want: true, + }, + { + name: "egg-info PKG-INFO", + filePath: "python3.8/site-packages/wrapt-1.12.1.egg-info/PKG-INFO", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := eggAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/python/packaging/packaging.go b/pkg/fanal/analyzer/language/python/packaging/packaging.go new file mode 100644 index 000000000000..69d52a4444d7 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/packaging.go @@ -0,0 +1,182 @@ +package packaging + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path" + "path/filepath" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/packaging" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypePythonPkg, newPackagingAnalyzer) +} + +const version = 2 + +func newPackagingAnalyzer(opt analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &packagingAnalyzer{ + logger: log.WithPrefix("python"), + pkgParser: packaging.NewParser(), + licenseClassifierConfidenceLevel: opt.LicenseScannerOption.ClassifierConfidenceLevel, + }, nil +} + +var ( + eggFiles = []string{ + // .egg format + // https://setuptools.readthedocs.io/en/latest/deprecated/python_eggs.html#eggs-and-their-formats + // ".egg" is zip format. We check it in `eggAnalyzer`. + "EGG-INFO/PKG-INFO", + + // .egg-info format: .egg-info can be a file or directory + // https://setuptools.readthedocs.io/en/latest/deprecated/python_eggs.html#eggs-and-their-formats + ".egg-info", + ".egg-info/PKG-INFO", + // https://github.com/aquasecurity/trivy/issues/9171 + ".egg-info/METADATA", + } +) + +type packagingAnalyzer struct { + logger *log.Logger + pkgParser language.Parser + licenseClassifierConfidenceLevel float64 +} + +// PostAnalyze analyzes egg and wheel files. +func (a packagingAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + + var apps []types.Application + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == "METADATA" || isEggFile(path) || input.FilePatterns.Match(path) + } + + err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error { + rsa, ok := r.(xio.ReadSeekerAt) + if !ok { + return xerrors.New("invalid reader") + } + + app, err := a.parse(ctx, filePath, rsa, input.Options.FileChecksum) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + opener := func(licPath string) (io.ReadCloser, error) { + // Note that fs.FS is always slashed regardless of the platform, + // and path.Join should be used rather than filepath.Join. + f, err := input.FS.Open(path.Join(path.Dir(filePath), licPath)) + if errors.Is(err, fs.ErrNotExist) { + return nil, nil + } else if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + return f, nil + } + + if err = fillAdditionalData(opener, app, a.licenseClassifierConfidenceLevel); err != nil { + a.logger.Warn("Unable to collect additional info", log.Err(err)) + } + + apps = append(apps, *app) + return nil + }) + + if err != nil { + return nil, xerrors.Errorf("python package walk error: %w", err) + } + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +type fileOpener func(filePath string) (io.ReadCloser, error) + +func fillAdditionalData(opener fileOpener, app *types.Application, licenseClassifierConfidenceLevel float64) error { + for i, pkg := range app.Packages { + var licenses []string + for _, lic := range pkg.Licenses { + // Parser adds `file://` prefix to filepath from `License-File` field + // We need to read this file to find licenses + // Otherwise, this is the name of the license + if !strings.HasPrefix(lic, licensing.LicenseFilePrefix) { + licenses = append(licenses, lic) + continue + } + licensePath := path.Base(strings.TrimPrefix(lic, licensing.LicenseFilePrefix)) + + foundLicenses, err := classifyLicenses(opener, licensePath, licenseClassifierConfidenceLevel) + if err != nil { + return xerrors.Errorf("unable to classify licenses: %w", err) + } + licenses = append(licenses, foundLicenses...) + } + app.Packages[i].Licenses = licenses + } + + return nil +} + +func classifyLicenses(opener fileOpener, licPath string, licenseClassifierConfidenceLevel float64) ([]string, error) { + f, err := opener(licPath) + if err != nil { + return nil, xerrors.Errorf("unable to open license file: %w", err) + } else if f == nil { // File doesn't exist + return nil, nil + } + defer f.Close() + + l, err := licensing.Classify("", f, licenseClassifierConfidenceLevel) + if err != nil { + return nil, xerrors.Errorf("license classify error: %w", err) + } else if l == nil { // No licenses found + return nil, nil + } + + // License found + return xslices.Map(l.Findings, func(finding types.LicenseFinding) string { + return finding.Name + }), nil +} + +func (a packagingAnalyzer) parse(ctx context.Context, filePath string, r xio.ReadSeekerAt, checksum bool) (*types.Application, error) { + return language.ParsePackage(ctx, types.PythonPkg, filePath, r, a.pkgParser, checksum) +} + +func (a packagingAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return strings.Contains(filePath, ".dist-info") || isEggFile(filePath) +} + +func isEggFile(filePath string) bool { + return lo.SomeBy(eggFiles, func(fileName string) bool { + return strings.HasSuffix(filePath, fileName) + }) +} + +func (a packagingAnalyzer) Type() analyzer.Type { + return analyzer.TypePythonPkg +} + +func (a packagingAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/python/packaging/packaging_test.go b/pkg/fanal/analyzer/language/python/packaging/packaging_test.go new file mode 100644 index 000000000000..c0623040e5fd --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/packaging_test.go @@ -0,0 +1,202 @@ +package packaging + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_packagingAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + includeChecksum bool + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "egg-info", + dir: "testdata/happy-egg", + includeChecksum: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PythonPkg, + FilePath: "distlib-0.3.1.egg-info/PKG-INFO", + Packages: types.Packages{ + { + Name: "distlib", + Version: "0.3.1", + Licenses: []string{"Python license"}, + FilePath: "distlib-0.3.1.egg-info/PKG-INFO", + Digest: "sha1:d9d89d8ed3b2b683767c96814c9c5d3e57ef2e1b", + }, + }, + }, + }, + }, + }, + { + name: "egg-info license classifiers", + dir: "testdata/classifier-license-egg", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PythonPkg, + FilePath: "setuptools-51.3.3.egg-info/PKG-INFO", + Packages: types.Packages{ + { + Name: "setuptools", + Version: "51.3.3", + Licenses: []string{"MIT License"}, + FilePath: "setuptools-51.3.3.egg-info/PKG-INFO", + }, + }, + }, + }, + }, + }, + { + name: "dist-info license classifiers", + dir: "testdata/classifier-license-dist", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PythonPkg, + FilePath: "setuptools-51.3.3.dist-info/METADATA", + Packages: types.Packages{ + { + Name: "setuptools", + Version: "51.3.3", + Licenses: []string{"MIT License"}, + FilePath: "setuptools-51.3.3.dist-info/METADATA", + }, + }, + }, + }, + }, + }, + { + name: "wheel", + dir: "testdata/happy-dist", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PythonPkg, + FilePath: "distlib-0.3.1.dist-info/METADATA", + Packages: types.Packages{ + { + Name: "distlib", + Version: "0.3.1", + Licenses: []string{"Python license"}, + FilePath: "distlib-0.3.1.dist-info/METADATA", + }, + }, + }, + }, + }, + }, + { + name: "license file in dist.info", + dir: "testdata/license-file-dist", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.PythonPkg, + FilePath: "typing_extensions-4.4.0.dist-info/METADATA", + Packages: []types.Package{ + { + Name: "typing_extensions", + Version: "4.4.0", + Licenses: []string{ + "BeOpen", + "CNRI-Python-GPL-Compatible", + "LicenseRef-MIT-Lucent", + "Python-2.0", + }, + FilePath: "typing_extensions-4.4.0.dist-info/METADATA", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + a, err := newPackagingAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + Options: analyzer.AnalysisOptions{ + FileChecksum: tt.includeChecksum, + }, + }) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } + +} + +func Test_packagingAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "egg", + filePath: "python2.7/site-packages/cssutils-1.0-py2.7.egg/EGG-INFO/PKG-INFO", + want: true, + }, + { + name: "egg-info/METADATA", + filePath: "Amazon/AWSCLIV2/cryptography-3.3.2-py3.8.egg-info/METADATA", + want: true, + }, + { + name: "egg-info", + filePath: "python3.8/site-packages/wrapt-1.12.1.egg-info", + want: true, + }, + { + name: "egg-info PKG-INFO", + filePath: "python3.8/site-packages/wrapt-1.12.1.egg-info/PKG-INFO", + want: true, + }, + { + name: "wheel", + filePath: "python3.8/site-packages/wrapt-1.12.1.dist-info/METADATA", + want: true, + }, + { + name: "wheel license", + filePath: "python3.8/site-packages/wrapt-1.12.1.dist-info/LICENSE", + want: true, + }, + { + name: "sad", + filePath: "random/PKG-INFO", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := packagingAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/python/packaging/testdata/classifier-license-dist/setuptools-51.3.3.dist-info/METADATA b/pkg/fanal/analyzer/language/python/packaging/testdata/classifier-license-dist/setuptools-51.3.3.dist-info/METADATA new file mode 100644 index 000000000000..fa35ca3301a7 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/testdata/classifier-license-dist/setuptools-51.3.3.dist-info/METADATA @@ -0,0 +1,90 @@ +Metadata-Version: 2.1 +Name: setuptools +Version: 51.3.3 +Summary: Easily download, build, install, upgrade, and uninstall Python packages +Home-page: https://github.com/pypa/setuptools +Author: Python Packaging Authority +Author-email: distutils-sig@python.org +Project-URL: Documentation, https://setuptools.readthedocs.io/ +Keywords: CPAN PyPI distutils eggs package management +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Archiving :: Packaging +Classifier: Topic :: System :: Systems Administration +Classifier: Topic :: Utilities +Requires-Python: >=3.6 +Provides-Extra: testing +Provides-Extra: docs +Provides-Extra: ssl +Provides-Extra: certs + +.. image:: https://img.shields.io/pypi/v/setuptools.svg + :target: https://pypi.org/project/setuptools + +.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg + +.. image:: https://github.com/pypa/setuptools/workflows/tests/badge.svg + :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://img.shields.io/readthedocs/setuptools/latest.svg + :target: https://setuptools.pypa.io + +.. image:: https://img.shields.io/badge/skeleton-2023-informational + :target: https://blog.jaraco.com/skeleton + +.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white + :target: https://codecov.io/gh/pypa/setuptools + +.. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme + +.. image:: https://img.shields.io/discord/803025117553754132 + :target: https://discord.com/channels/803025117553754132/815945031150993468 + :alt: Discord + +See the `Installation Instructions +`_ in the Python Packaging +User's Guide for instructions on installing, upgrading, and uninstalling +Setuptools. + +Questions and comments should be directed to `GitHub Discussions +`_. +Bug reports and especially tested patches may be +submitted directly to the `bug tracker +`_. + + +Code of Conduct +=============== + +Everyone interacting in the setuptools project's codebases, issue trackers, +chat rooms, and fora is expected to follow the +`PSF Code of Conduct `_. + + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +Setuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. + + +Security Contact +================ + +To report a security vulnerability, please use the +`Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. diff --git a/pkg/fanal/analyzer/language/python/packaging/testdata/classifier-license-egg/setuptools-51.3.3.egg-info/PKG-INFO b/pkg/fanal/analyzer/language/python/packaging/testdata/classifier-license-egg/setuptools-51.3.3.egg-info/PKG-INFO new file mode 100644 index 000000000000..3c775e5909e8 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/testdata/classifier-license-egg/setuptools-51.3.3.egg-info/PKG-INFO @@ -0,0 +1,89 @@ +Metadata-Version: 2.1 +Name: setuptools +Version: 51.3.3 +Summary: Easily download, build, install, upgrade, and uninstall Python packages +Home-page: https://github.com/pypa/setuptools +Author: Python Packaging Authority +Author-email: distutils-sig@python.org +Project-URL: Documentation, https://setuptools.readthedocs.io/ +Keywords: CPAN PyPI distutils eggs package management +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Archiving :: Packaging +Classifier: Topic :: System :: Systems Administration +Classifier: Topic :: Utilities +Requires-Python: >=3.6 +Provides-Extra: testing +Provides-Extra: docs +Provides-Extra: ssl +Provides-Extra: certs +.. image:: https://img.shields.io/pypi/v/setuptools.svg + :target: https://pypi.org/project/setuptools + +.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg + +.. image:: https://github.com/pypa/setuptools/workflows/tests/badge.svg + :target: https://github.com/pypa/setuptools/actions?query=workflow%3A%22tests%22 + :alt: tests + +.. image:: https://img.shields.io/badge/code%20style-black-000000.svg + :target: https://github.com/psf/black + :alt: Code style: Black + +.. image:: https://img.shields.io/readthedocs/setuptools/latest.svg + :target: https://setuptools.pypa.io + +.. image:: https://img.shields.io/badge/skeleton-2023-informational + :target: https://blog.jaraco.com/skeleton + +.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white + :target: https://codecov.io/gh/pypa/setuptools + +.. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat + :target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme + +.. image:: https://img.shields.io/discord/803025117553754132 + :target: https://discord.com/channels/803025117553754132/815945031150993468 + :alt: Discord + +See the `Installation Instructions +`_ in the Python Packaging +User's Guide for instructions on installing, upgrading, and uninstalling +Setuptools. + +Questions and comments should be directed to `GitHub Discussions +`_. +Bug reports and especially tested patches may be +submitted directly to the `bug tracker +`_. + + +Code of Conduct +=============== + +Everyone interacting in the setuptools project's codebases, issue trackers, +chat rooms, and fora is expected to follow the +`PSF Code of Conduct `_. + + +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +Setuptools and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. + + +Security Contact +================ + +To report a security vulnerability, please use the +`Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. diff --git a/pkg/fanal/analyzer/language/python/packaging/testdata/egg-zip-with-license-file/sample_package.egg b/pkg/fanal/analyzer/language/python/packaging/testdata/egg-zip-with-license-file/sample_package.egg new file mode 100644 index 000000000000..91d67dc5947b Binary files /dev/null and b/pkg/fanal/analyzer/language/python/packaging/testdata/egg-zip-with-license-file/sample_package.egg differ diff --git a/pkg/fanal/analyzer/language/python/packaging/testdata/egg-zip/kitchen-1.2.6-py2.7.egg b/pkg/fanal/analyzer/language/python/packaging/testdata/egg-zip/kitchen-1.2.6-py2.7.egg new file mode 100644 index 000000000000..b40d7822f7b6 Binary files /dev/null and b/pkg/fanal/analyzer/language/python/packaging/testdata/egg-zip/kitchen-1.2.6-py2.7.egg differ diff --git a/pkg/fanal/analyzer/language/python/packaging/testdata/happy-dist/distlib-0.3.1.dist-info/METADATA b/pkg/fanal/analyzer/language/python/packaging/testdata/happy-dist/distlib-0.3.1.dist-info/METADATA new file mode 100644 index 000000000000..95c76f43e0dd --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/testdata/happy-dist/distlib-0.3.1.dist-info/METADATA @@ -0,0 +1,30 @@ +Metadata-Version: 1.1 +Name: distlib +Version: 0.3.1 +Summary: Distribution utilities +Home-page: https://bitbucket.org/pypa/distlib +Author: Vinay Sajip +Author-email: vinay_sajip@red-dove.com +License: Python license +Download-URL: https://bitbucket.org/pypa/distlib/downloads/distlib-0.3.1.zip +Description: Low-level components of distutils2/packaging, augmented with higher-level APIs for making packaging easier. + +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Topic :: Software Development +Classifier: Topic :: Utilities diff --git a/pkg/fanal/analyzer/language/python/packaging/testdata/happy-egg/distlib-0.3.1.egg-info/PKG-INFO b/pkg/fanal/analyzer/language/python/packaging/testdata/happy-egg/distlib-0.3.1.egg-info/PKG-INFO new file mode 100644 index 000000000000..95c76f43e0dd --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/testdata/happy-egg/distlib-0.3.1.egg-info/PKG-INFO @@ -0,0 +1,30 @@ +Metadata-Version: 1.1 +Name: distlib +Version: 0.3.1 +Summary: Distribution utilities +Home-page: https://bitbucket.org/pypa/distlib +Author: Vinay Sajip +Author-email: vinay_sajip@red-dove.com +License: Python license +Download-URL: https://bitbucket.org/pypa/distlib/downloads/distlib-0.3.1.zip +Description: Low-level components of distutils2/packaging, augmented with higher-level APIs for making packaging easier. + +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Python Software Foundation License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.2 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Topic :: Software Development +Classifier: Topic :: Utilities diff --git a/pkg/fanal/analyzer/language/python/packaging/testdata/license-file-dist/typing_extensions-4.4.0.dist-info/LICENSE.txt b/pkg/fanal/analyzer/language/python/packaging/testdata/license-file-dist/typing_extensions-4.4.0.dist-info/LICENSE.txt new file mode 100644 index 000000000000..1df6b3b8de0e --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/testdata/license-file-dist/typing_extensions-4.4.0.dist-info/LICENSE.txt @@ -0,0 +1,254 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. All rights reserved. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, +provided that the above copyright notice appear in all copies and that +both that copyright notice and this permission notice appear in +supporting documentation, and that the name of Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE +FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/pkg/fanal/analyzer/language/python/packaging/testdata/license-file-dist/typing_extensions-4.4.0.dist-info/METADATA b/pkg/fanal/analyzer/language/python/packaging/testdata/license-file-dist/typing_extensions-4.4.0.dist-info/METADATA new file mode 100644 index 000000000000..e4fc961591e5 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/packaging/testdata/license-file-dist/typing_extensions-4.4.0.dist-info/METADATA @@ -0,0 +1,189 @@ +Metadata-Version: 2.1 +Name: typing_extensions +Version: 4.4.0 +Summary: Backported and Experimental Type Hints for Python 3.7+ +Keywords: annotations,backport,checker,checking,function,hinting,hints,type,typechecking,typehinting,typehints,typing +Author-email: "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee" +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +License-File: licenses/LICENSE.txt +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Topic :: Software Development +Project-URL: Bug Tracker, https://github.com/python/typing_extensions/issues +Project-URL: Changes, https://github.com/python/typing_extensions/blob/main/CHANGELOG.md +Project-URL: Documentation, https://typing.readthedocs.io/ +Project-URL: Home, https://github.com/python/typing_extensions +Project-URL: Q & A, https://github.com/python/typing/discussions +Project-URL: Repository, https://github.com/python/typing_extensions + +# Typing Extensions + +[![Chat at https://gitter.im/python/typing](https://badges.gitter.im/python/typing.svg)](https://gitter.im/python/typing) + +## Overview + +The `typing_extensions` module serves two related purposes: + +- Enable use of new type system features on older Python versions. For example, + `typing.TypeGuard` is new in Python 3.10, but `typing_extensions` allows + users on previous Python versions to use it too. +- Enable experimentation with new type system PEPs before they are accepted and + added to the `typing` module. + +New features may be added to `typing_extensions` as soon as they are specified +in a PEP that has been added to the [python/peps](https://github.com/python/peps) +repository. If the PEP is accepted, the feature will then be added to `typing` +for the next CPython release. No typing PEP has been rejected so far, so we +haven't yet figured out how to deal with that possibility. + +Starting with version 4.0.0, `typing_extensions` uses +[Semantic Versioning](https://semver.org/). The +major version is incremented for all backwards-incompatible changes. +Therefore, it's safe to depend +on `typing_extensions` like this: `typing_extensions >=x.y, <(x+1)`, +where `x.y` is the first version that includes all features you need. + +`typing_extensions` supports Python versions 3.7 and higher. In the future, +support for older Python versions will be dropped some time after that version +reaches end of life. + +## Included items + +This module currently contains the following: + +- Experimental features + + - `override` (see PEP 698) + - The `default=` argument to `TypeVar`, `ParamSpec`, and `TypeVarTuple` (see PEP 696) + - The `infer_variance=` argument to `TypeVar` (see PEP 695) + +- In `typing` since Python 3.11 + + - `assert_never` + - `assert_type` + - `clear_overloads` + - `@dataclass_transform()` (see PEP 681) + - `get_overloads` + - `LiteralString` (see PEP 675) + - `Never` + - `NotRequired` (see PEP 655) + - `reveal_type` + - `Required` (see PEP 655) + - `Self` (see PEP 673) + - `TypeVarTuple` (see PEP 646; the `typing_extensions` version supports the `default=` argument from PEP 696) + - `Unpack` (see PEP 646) + +- In `typing` since Python 3.10 + + - `Concatenate` (see PEP 612) + - `ParamSpec` (see PEP 612; the `typing_extensions` version supports the `default=` argument from PEP 696) + - `ParamSpecArgs` (see PEP 612) + - `ParamSpecKwargs` (see PEP 612) + - `TypeAlias` (see PEP 613) + - `TypeGuard` (see PEP 647) + - `is_typeddict` + +- In `typing` since Python 3.9 + + - `Annotated` (see PEP 593) + +- In `typing` since Python 3.8 + + - `final` (see PEP 591) + - `Final` (see PEP 591) + - `Literal` (see PEP 586) + - `Protocol` (see PEP 544) + - `runtime_checkable` (see PEP 544) + - `TypedDict` (see PEP 589) + - `get_origin` (`typing_extensions` provides this function only in Python 3.7+) + - `get_args` (`typing_extensions` provides this function only in Python 3.7+) + +- In `typing` since Python 3.7 + + - `OrderedDict` + +- In `typing` since Python 3.5 or 3.6 (see [the typing documentation](https://docs.python.org/3.10/library/typing.html) for details) + + - `AsyncContextManager` + - `AsyncGenerator` + - `AsyncIterable` + - `AsyncIterator` + - `Awaitable` + - `ChainMap` + - `ClassVar` (see PEP 526) + - `ContextManager` + - `Coroutine` + - `Counter` + - `DefaultDict` + - `Deque` + - `NewType` + - `NoReturn` + - `overload` + - `Text` + - `Type` + - `TYPE_CHECKING` + - `get_type_hints` + +- The following have always been present in `typing`, but the `typing_extensions` versions provide + additional features: + + - `Any` (supports inheritance since Python 3.11) + - `NamedTuple` (supports multiple inheritance with `Generic` since Python 3.11) + - `TypeVar` (see PEPs 695 and 696) + +# Other Notes and Limitations + +Certain objects were changed after they were added to `typing`, and +`typing_extensions` provides a backport even on newer Python versions: + +- `TypedDict` does not store runtime information + about which (if any) keys are non-required in Python 3.8, and does not + honor the `total` keyword with old-style `TypedDict()` in Python + 3.9.0 and 3.9.1. `TypedDict` also does not support multiple inheritance + with `typing.Generic` on Python <3.11. +- `get_origin` and `get_args` lack support for `Annotated` in + Python 3.8 and lack support for `ParamSpecArgs` and `ParamSpecKwargs` + in 3.9. +- `@final` was changed in Python 3.11 to set the `.__final__` attribute. +- `@overload` was changed in Python 3.11 to make function overloads + introspectable at runtime. In order to access overloads with + `typing_extensions.get_overloads()`, you must use + `@typing_extensions.overload`. +- `NamedTuple` was changed in Python 3.11 to allow for multiple inheritance + with `typing.Generic`. +- Since Python 3.11, it has been possible to inherit from `Any` at + runtime. `typing_extensions.Any` also provides this capability. +- `TypeVar` gains two additional parameters, `default=` and `infer_variance=`, + in the draft PEPs 695 and 696, which are being considered for inclusion + in Python 3.12. + +There are a few types whose interface was modified between different +versions of typing. For example, `typing.Sequence` was modified to +subclass `typing.Reversible` as of Python 3.5.3. + +These changes are _not_ backported to prevent subtle compatibility +issues when mixing the differing implementations of modified classes. + +Certain types have incorrect runtime behavior due to limitations of older +versions of the typing module: + +- `ParamSpec` and `Concatenate` will not work with `get_args` and + `get_origin`. Certain PEP 612 special cases in user-defined + `Generic`s are also not available. + +These types are only guaranteed to work for static type checking. + +## Running tests + +To run tests, navigate into the appropriate source directory and run +`test_typing_extensions.py`. + diff --git a/pkg/fanal/analyzer/language/python/packaging/testdata/no-req-files/no-required-files.egg b/pkg/fanal/analyzer/language/python/packaging/testdata/no-req-files/no-required-files.egg new file mode 100644 index 000000000000..8cd2acb25451 Binary files /dev/null and b/pkg/fanal/analyzer/language/python/packaging/testdata/no-req-files/no-required-files.egg differ diff --git a/pkg/fanal/analyzer/language/python/pip/pip.go b/pkg/fanal/analyzer/language/python/pip/pip.go new file mode 100644 index 000000000000..7338c8e35290 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pip/pip.go @@ -0,0 +1,264 @@ +package pip + +import ( + "context" + "io" + "io/fs" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + + "golang.org/x/xerrors" + + goversion "github.com/aquasecurity/go-version/pkg/version" + "github.com/aquasecurity/trivy/pkg/dependency/parser/python" + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/packaging" + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/pip" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypePip, newPipLibraryAnalyzer) +} + +const version = 1 + +var pythonExecNames = []string{ + "python3", + "python", + "python2", + "python.exe", +} + +type pipLibraryAnalyzer struct { + logger *log.Logger + metadataParser packaging.Parser + detectionPriority types.DetectionPriority +} + +func newPipLibraryAnalyzer(opts analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return pipLibraryAnalyzer{ + logger: log.WithPrefix("pip"), + metadataParser: *packaging.NewParser(), + detectionPriority: opts.DetectionPriority, + }, nil +} + +func (a pipLibraryAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + + sitePackagesDir, err := a.pythonSitePackagesDir() + if err != nil { + a.logger.Warn("Unable to find python `site-packages` directory. License detection is skipped.", log.Err(err)) + } + + required := func(_ string, _ fs.DirEntry) bool { + // Parse all required files: `conan.lock` (from a.Required func) + input.FilePatterns.Match() + return true + } + + useMinVersion := a.detectionPriority == types.PriorityComprehensive + + if err = fsutils.WalkDir(input.FS, ".", required, func(pathPath string, _ fs.DirEntry, r io.Reader) error { + app, err := language.Parse(ctx, types.Pip, pathPath, r, pip.NewParser(useMinVersion)) + if err != nil { + return xerrors.Errorf("unable to parse requirements.txt: %w", err) + } + + if app == nil { + return nil + } + + // Fill licenses + if sitePackagesDir != "" { + for i := range app.Packages { + app.Packages[i].Licenses = a.pkgLicense(ctx, app.Packages[i].Name, app.Packages[i].Version, sitePackagesDir) + } + } + + apps = append(apps, *app) + return nil + }); err != nil { + return nil, xerrors.Errorf("pip walt error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a pipLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + return fileName == types.PipRequirements +} + +func (a pipLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypePip +} + +func (a pipLibraryAnalyzer) Version() int { + return version +} + +// pkgLicense parses `METADATA` pkg file to look for licenses +func (a pipLibraryAnalyzer) pkgLicense(ctx context.Context, pkgName, pkgVer, spDir string) []string { + metadataFile := a.metadataFile(pkgName, pkgVer, spDir) + if metadataFile == nil { + return nil + } + defer metadataFile.Close() + + metadataPkg, _, err := a.metadataParser.Parse(ctx, metadataFile) + if err != nil { + a.logger.Warn("Unable to parse METADATA file", log.FilePath(metadataFile.Name()), log.Err(err)) + return nil + } + + // METADATA file contains info about only 1 package + // cf. https://github.com/aquasecurity/trivy/blob/e66dbb935764908f0b2b9a55cbfe6c107f101a31/pkg/dependency/parser/python/packaging/parse.go#L86-L92 + return metadataPkg[0].Licenses +} + +// pythonSitePackagesDir returns path to site-packages dir +func (a pipLibraryAnalyzer) pythonSitePackagesDir() (string, error) { + // check VIRTUAL_ENV first + if venv := os.Getenv("VIRTUAL_ENV"); venv != "" { + libDir := filepath.Join(venv, "lib") + if _, err := os.Stat(libDir); os.IsNotExist(err) { + return "", xerrors.Errorf("unable to detect `lib` dir for %q venv: %w", venv, err) + } + + spDir, err := a.findSitePackagesDir(libDir) + if err != nil { + return "", xerrors.Errorf("unable to detect `site-packages` dir for %q venv: %w", spDir, err) + } else if spDir != "" { + return spDir, nil + } + } + + // Find path to Python executable + pythonExecPath, err := pythonExecutablePath() + if err != nil { + return "", err + } + pythonExecDir := filepath.Dir(pythonExecPath) + + // Search for a directory starting with "python" in the lib directory + libDir := filepath.Join(pythonExecDir, "..", "lib") + spDir, err := a.findSitePackagesDir(libDir) + if err != nil { + return "", xerrors.Errorf("unable to detect `site-packages` dir for %q: %w", pythonExecPath, err) + } else if spDir != "" { + return spDir, nil + } + + // Try another common pattern if the Python library directory is not found + spDir = filepath.Join(pythonExecDir, "..", "..", "lib", "site-packages") + if fsutils.DirExists(spDir) { + return spDir, nil + } + + return "", xerrors.Errorf("site-packages directory not found") +} + +// pythonExecutablePath returns path to Python executable +func pythonExecutablePath() (string, error) { + for _, execName := range pythonExecNames { + // Get the absolute path of the python command + pythonPath, err := exec.LookPath(execName) + if err != nil { + continue + } + return pythonPath, nil + } + return "", xerrors.Errorf("unable to find path to Python executable") +} + +// findSitePackagesDir finds `site-packages` dir in `lib` dir +func (a pipLibraryAnalyzer) findSitePackagesDir(libDir string) (string, error) { + entries, err := os.ReadDir(libDir) + if err != nil { + if !os.IsNotExist(err) { + return "", xerrors.Errorf("failed to read lib directory: %w", err) + } + return "", nil + } + + // Find python dir which contains `site-packages` dir + // First check for newer versions + pythonDirs := a.sortPythonDirs(entries) + for i := len(pythonDirs) - 1; i >= 0; i-- { + dir := filepath.Join(libDir, pythonDirs[i], "site-packages") + if fsutils.DirExists(dir) { + return dir, nil + } + } + return "", nil +} + +// sortPythonDirs finds dirs starting with `python` and sorts them +// e.g. python2.7 => python3.9 => python3.11 +func (a pipLibraryAnalyzer) sortPythonDirs(entries []os.DirEntry) []string { + var pythonVers []goversion.Version + for _, entry := range entries { + // Found a directory starting with "python", assume it's the Python library directory + if entry.IsDir() && strings.HasPrefix(entry.Name(), "python") { + ver := strings.TrimPrefix(entry.Name(), "python") + v, err := goversion.Parse(ver) + if err != nil { + a.logger.Debug("Unable to parse version from Python dir name", log.String("dir", entry.Name()), log.Err(err)) + continue + } + pythonVers = append(pythonVers, v) + } + } + + // Sort Python version + sort.Sort(goversion.Collection(pythonVers)) + + return xslices.Map(pythonVers, func(v goversion.Version) string { + return "python" + v.String() + }) +} + +// metadataFile returns METADATA file for package (if exists) +func (a pipLibraryAnalyzer) metadataFile(pkgName, pkgVer, spDir string) *os.File { + pkgDirs := distInfoDirs(pkgName, pkgVer) + for _, pkgDir := range distInfoDirs(pkgName, pkgVer) { + metadataPath := filepath.Join(spDir, pkgDir, "METADATA") + metadataFile, err := os.Open(metadataPath) + if err == nil { + return metadataFile + } + } + + a.logger.Debug("No package metadata found", log.String("site-packages", spDir), + log.String("dist-info", strings.Join(pkgDirs, ", ")), log.String("name", pkgName), log.String("version", pkgVer)) + return nil +} + +// distInfoDir returns normalized dist-info dir name for package +// cf. https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-dist-info-directory +// e.g. `foo-1.0.dist-info` or `foo_bar-1.0.dist-info` +func distInfoDirs(name, version string) []string { + dirs := []string{ + // Any packages don't use lower case. + // e.g. Flask uses `Flask-2.0.1.dist-info` + python.NormalizePkgName(name, false), + python.NormalizePkgName(name, true), + } + + for i := range dirs { + dirs[i] = strings.ReplaceAll(dirs[i], "-", "_") + dirs[i] = dirs[i] + "-" + version + ".dist-info" + } + + return dirs +} diff --git a/pkg/fanal/analyzer/language/python/pip/pip_test.go b/pkg/fanal/analyzer/language/python/pip/pip_test.go new file mode 100644 index 000000000000..955c3c69d308 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pip/pip_test.go @@ -0,0 +1,322 @@ +package pip + +import ( + "os" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +func Test_pipAnalyzer_Analyze(t *testing.T) { + resultWithLicenses := &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pip, + FilePath: "requirements.txt", + Packages: types.Packages{ + { + Name: "annotated-types", + Version: "0.7.0", + Locations: []types.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + Licenses: []string{ + "MIT License", + }, + }, + { + Name: "click", + Version: "8.0.0", + Locations: []types.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + Licenses: []string{ + "BSD License", + }, + }, + { + Name: "Flask", + Version: "2.0.0", + Locations: []types.Location{ + { + StartLine: 3, + EndLine: 3, + }, + }, + Licenses: []string{ + "BSD License", + }, + }, + { + Name: "itsdangerous", + Version: "2.0.0", + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + dir string + venv string + pythonExecDir string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path with licenses from venv", + dir: filepath.Join("testdata", "happy"), + venv: filepath.Join("testdata", "libs", "python-dir"), + pythonExecDir: filepath.Join("testdata", "libs", "python-dir", "bin"), + want: resultWithLicenses, + }, + { + name: "happy path with licenses from python dir", + dir: filepath.Join("testdata", "happy"), + pythonExecDir: filepath.Join("testdata", "libs", "python-dir", "bin"), + want: resultWithLicenses, + }, + { + name: "happy path with licenses from common dir", + dir: filepath.Join("testdata", "happy"), + pythonExecDir: filepath.Join("testdata", "libs", "common-dir", "foo", "bar"), + want: resultWithLicenses, + }, + { + name: "happy path without licenses", + dir: filepath.Join("testdata", "happy"), + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Pip, + FilePath: "requirements.txt", + Packages: types.Packages{ + { + Name: "annotated-types", + Version: "0.7.0", + Locations: []types.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + }, + { + Name: "click", + Version: "8.0.0", + Locations: []types.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + }, + { + Name: "Flask", + Version: "2.0.0", + Locations: []types.Location{ + { + StartLine: 3, + EndLine: 3, + }, + }, + }, + { + Name: "itsdangerous", + Version: "2.0.0", + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with not related filename", + dir: "testdata/empty", + want: &analyzer.AnalysisResult{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.venv != "" { + t.Setenv("VIRTUAL_ENV", tt.venv) + } + + var newPATH string + if tt.pythonExecDir != "" { + err := os.MkdirAll(tt.pythonExecDir, os.ModePerm) + require.NoError(t, err) + defer func() { + if strings.HasSuffix(tt.pythonExecDir, "bar") { // for `happy path with licenses from common dir` test + tt.pythonExecDir = filepath.Dir(tt.pythonExecDir) + } + err = os.RemoveAll(tt.pythonExecDir) + require.NoError(t, err) + }() + + pythonExecFileName := "python" + if runtime.GOOS == "windows" { + pythonExecFileName = "python.exe" + } + // create temp python3 Executable + err = os.WriteFile(filepath.Join(tt.pythonExecDir, pythonExecFileName), nil, 0o755) + require.NoError(t, err) + + newPATH, err = filepath.Abs(tt.pythonExecDir) + require.NoError(t, err) + + } + t.Setenv("PATH", newPATH) + + a, err := newPipLibraryAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_pipAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy", + filePath: "test/requirements.txt", + want: true, + }, + { + name: "sad", + filePath: "a/b/c/d/test.sum", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := pipLibraryAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_pythonExecutablePath(t *testing.T) { + tests := []struct { + name string + execName string + wantErr string + }{ + { + name: "happy path with `python` filename", + execName: "python", + }, + { + name: "happy path with `python3` filename", + execName: "python3", + }, + { + name: "happy path with `python2` filename", + execName: "python2", + }, + { + name: "sad path. Python executable not found", + execName: "python-wrong", + wantErr: "unable to find path to Python executable", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + binDir := filepath.Join(tmpDir, "bin") + err := os.MkdirAll(binDir, os.ModePerm) + require.NoError(t, err) + + if runtime.GOOS == "windows" { + tt.execName += ".exe" + } + err = os.WriteFile(filepath.Join(binDir, tt.execName), nil, 0o755) + require.NoError(t, err) + + t.Setenv("PATH", binDir) + + path, err := pythonExecutablePath() + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + require.Equal(t, tt.execName, filepath.Base(path)) + }) + } +} + +func Test_sortPythonDirs(t *testing.T) { + dirs := []string{ + "wrong", + "wrong2.7", + "python3.11", + "python3.10", + "python2.7", + "python3.9", + "python3", + "python2", + "pythonBadVer", + } + wantDirs := []string{ + "python2", + "python2.7", + "python3", + "python3.9", + "python3.10", + "python3.11", + } + + tmp := t.TempDir() + for _, dir := range dirs { + err := os.Mkdir(filepath.Join(tmp, dir), os.ModePerm) + require.NoError(t, err) + } + + tmpDir, err := os.ReadDir(tmp) + require.NoError(t, err) + + a := pipLibraryAnalyzer{ + logger: log.WithPrefix("pip"), + } + got := a.sortPythonDirs(tmpDir) + require.Equal(t, wantDirs, got) +} diff --git a/pkg/fanal/analyzer/language/python/pip/testdata/empty/requirements.txt b/pkg/fanal/analyzer/language/python/pip/testdata/empty/requirements.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/language/python/pip/testdata/happy/requirements.txt b/pkg/fanal/analyzer/language/python/pip/testdata/happy/requirements.txt new file mode 100644 index 000000000000..e2766ceeb137 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pip/testdata/happy/requirements.txt @@ -0,0 +1,7 @@ +annotated-types==0.7.0 +click==8.0.0 +Flask==2.0.0 +itsdangerous==2.0.0 +Jinja2<3.0.0 +MarkupSafe>2.0.0 +Werkzeug diff --git a/pkg/fanal/analyzer/language/python/pip/testdata/libs/common-dir/lib/site-packages/Flask-2.0.0.dist-info/METADATA b/pkg/fanal/analyzer/language/python/pip/testdata/libs/common-dir/lib/site-packages/Flask-2.0.0.dist-info/METADATA new file mode 100644 index 000000000000..5df0132dde56 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pip/testdata/libs/common-dir/lib/site-packages/Flask-2.0.0.dist-info/METADATA @@ -0,0 +1,124 @@ +Metadata-Version: 2.1 +Name: Flask +Version: 2.0.0 +Summary: A simple framework for building complex web applications. +Home-page: https://palletsprojects.com/p/flask +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://flask.palletsprojects.com/ +Project-URL: Changes, https://flask.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/flask/ +Project-URL: Issue Tracker, https://github.com/pallets/flask/issues/ +Project-URL: Twitter, https://twitter.com/PalletsTeam +Project-URL: Chat, https://discord.gg/pallets +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Framework :: Flask +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +Requires-Dist: Werkzeug (>=2.0) +Requires-Dist: Jinja2 (>=3.0) +Requires-Dist: itsdangerous (>=2.0) +Requires-Dist: click (>=7.1.2) +Provides-Extra: async +Requires-Dist: asgiref (>=3.2) ; extra == 'async' +Provides-Extra: dotenv +Requires-Dist: python-dotenv ; extra == 'dotenv' + +Flask +===== + +Flask is a lightweight `WSGI`_ web application framework. It is designed +to make getting started quick and easy, with the ability to scale up to +complex applications. It began as a simple wrapper around `Werkzeug`_ +and `Jinja`_ and has become one of the most popular Python web +application frameworks. + +Flask offers suggestions, but doesn't enforce any dependencies or +project layout. It is up to the developer to choose the tools and +libraries they want to use. There are many extensions provided by the +community that make adding new functionality easy. + +.. _WSGI: https://wsgi.readthedocs.io/ +.. _Werkzeug: https://werkzeug.palletsprojects.com/ +.. _Jinja: https://jinja.palletsprojects.com/ + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U Flask + +.. _pip: https://pip.pypa.io/en/stable/quickstart/ + + +A Simple Example +---------------- + +.. code-block:: python + + # save this as app.py + from flask import Flask + + app = Flask(__name__) + + @app.route("/") + def hello(): + return "Hello, World!" + +.. code-block:: text + + $ flask run + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + + +Contributing +------------ + +For guidance on setting up a development environment and how to make a +contribution to Flask, see the `contributing guidelines`_. + +.. _contributing guidelines: https://github.com/pallets/flask/blob/master/CONTRIBUTING.rst + + +Donate +------ + +The Pallets organization develops and supports Flask and the libraries +it uses. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://flask.palletsprojects.com/ +- Changes: https://flask.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/Flask/ +- Source Code: https://github.com/pallets/flask/ +- Issue Tracker: https://github.com/pallets/flask/issues/ +- Website: https://palletsprojects.com/p/flask/ +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + + diff --git a/pkg/fanal/analyzer/language/python/pip/testdata/libs/common-dir/lib/site-packages/annotated_types-0.7.0.dist-info/METADATA b/pkg/fanal/analyzer/language/python/pip/testdata/libs/common-dir/lib/site-packages/annotated_types-0.7.0.dist-info/METADATA new file mode 100644 index 000000000000..ba324aef51b1 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pip/testdata/libs/common-dir/lib/site-packages/annotated_types-0.7.0.dist-info/METADATA @@ -0,0 +1,28 @@ +Metadata-Version: 2.3 +Name: annotated-types +Version: 0.7.0 +Summary: Reusable constraint types to use with typing.Annotated +Project-URL: Homepage, https://github.com/annotated-types/annotated-types +Project-URL: Source, https://github.com/annotated-types/annotated-types +Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases +Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin , Zac Hatfield-Dodds +License-File: LICENSE +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console +Classifier: Environment :: MacOS X +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Unix +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Typing :: Typed +Requires-Python: >=3.8 +Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9' +Description-Content-Type: text/markdown diff --git a/pkg/fanal/analyzer/language/python/pip/testdata/libs/common-dir/lib/site-packages/click-8.0.0.dist-info/METADATA b/pkg/fanal/analyzer/language/python/pip/testdata/libs/common-dir/lib/site-packages/click-8.0.0.dist-info/METADATA new file mode 100644 index 000000000000..5e67f0446795 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pip/testdata/libs/common-dir/lib/site-packages/click-8.0.0.dist-info/METADATA @@ -0,0 +1,109 @@ +Metadata-Version: 2.1 +Name: click +Version: 8.0.0 +Summary: Composable command line interface toolkit +Home-page: https://palletsprojects.com/p/click/ +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://click.palletsprojects.com/ +Project-URL: Changes, https://click.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/click/ +Project-URL: Issue Tracker, https://github.com/pallets/click/issues/ +Project-URL: Twitter, https://twitter.com/PalletsTeam +Project-URL: Chat, https://discord.gg/pallets +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +Requires-Dist: colorama ; platform_system == "Windows" + +\$ click\_ +========== + +Click is a Python package for creating beautiful command line interfaces +in a composable way with as little code as necessary. It's the "Command +Line Interface Creation Kit". It's highly configurable but comes with +sensible defaults out of the box. + +It aims to make the process of writing command line tools quick and fun +while also preventing any frustration caused by the inability to +implement an intended CLI API. + +Click in three points: + +- Arbitrary nesting of commands +- Automatic help page generation +- Supports lazy loading of subcommands at runtime + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U click + +.. _pip: https://pip.pypa.io/en/stable/quickstart/ + + +A Simple Example +---------------- + +.. code-block:: python + + import click + + @click.command() + @click.option("--count", default=1, help="Number of greetings.") + @click.option("--name", prompt="Your name", help="The person to greet.") + def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for _ in range(count): + click.echo(f"Hello, {name}!") + + if __name__ == '__main__': + hello() + +.. code-block:: text + + $ python hello.py --count=3 + Your name: Click + Hello, Click! + Hello, Click! + Hello, Click! + + +Donate +------ + +The Pallets organization develops and supports Click and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://click.palletsprojects.com/ +- Changes: https://click.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/click/ +- Source Code: https://github.com/pallets/click +- Issue Tracker: https://github.com/pallets/click/issues +- Website: https://palletsprojects.com/p/click +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + + diff --git a/pkg/fanal/analyzer/language/python/pip/testdata/libs/python-dir/lib/python3.10/site-packages/Flask-2.0.0.dist-info/METADATA b/pkg/fanal/analyzer/language/python/pip/testdata/libs/python-dir/lib/python3.10/site-packages/Flask-2.0.0.dist-info/METADATA new file mode 100644 index 000000000000..5df0132dde56 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pip/testdata/libs/python-dir/lib/python3.10/site-packages/Flask-2.0.0.dist-info/METADATA @@ -0,0 +1,124 @@ +Metadata-Version: 2.1 +Name: Flask +Version: 2.0.0 +Summary: A simple framework for building complex web applications. +Home-page: https://palletsprojects.com/p/flask +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://flask.palletsprojects.com/ +Project-URL: Changes, https://flask.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/flask/ +Project-URL: Issue Tracker, https://github.com/pallets/flask/issues/ +Project-URL: Twitter, https://twitter.com/PalletsTeam +Project-URL: Chat, https://discord.gg/pallets +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Framework :: Flask +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +Requires-Dist: Werkzeug (>=2.0) +Requires-Dist: Jinja2 (>=3.0) +Requires-Dist: itsdangerous (>=2.0) +Requires-Dist: click (>=7.1.2) +Provides-Extra: async +Requires-Dist: asgiref (>=3.2) ; extra == 'async' +Provides-Extra: dotenv +Requires-Dist: python-dotenv ; extra == 'dotenv' + +Flask +===== + +Flask is a lightweight `WSGI`_ web application framework. It is designed +to make getting started quick and easy, with the ability to scale up to +complex applications. It began as a simple wrapper around `Werkzeug`_ +and `Jinja`_ and has become one of the most popular Python web +application frameworks. + +Flask offers suggestions, but doesn't enforce any dependencies or +project layout. It is up to the developer to choose the tools and +libraries they want to use. There are many extensions provided by the +community that make adding new functionality easy. + +.. _WSGI: https://wsgi.readthedocs.io/ +.. _Werkzeug: https://werkzeug.palletsprojects.com/ +.. _Jinja: https://jinja.palletsprojects.com/ + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U Flask + +.. _pip: https://pip.pypa.io/en/stable/quickstart/ + + +A Simple Example +---------------- + +.. code-block:: python + + # save this as app.py + from flask import Flask + + app = Flask(__name__) + + @app.route("/") + def hello(): + return "Hello, World!" + +.. code-block:: text + + $ flask run + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) + + +Contributing +------------ + +For guidance on setting up a development environment and how to make a +contribution to Flask, see the `contributing guidelines`_. + +.. _contributing guidelines: https://github.com/pallets/flask/blob/master/CONTRIBUTING.rst + + +Donate +------ + +The Pallets organization develops and supports Flask and the libraries +it uses. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://flask.palletsprojects.com/ +- Changes: https://flask.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/Flask/ +- Source Code: https://github.com/pallets/flask/ +- Issue Tracker: https://github.com/pallets/flask/issues/ +- Website: https://palletsprojects.com/p/flask/ +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + + diff --git a/pkg/fanal/analyzer/language/python/pip/testdata/libs/python-dir/lib/python3.10/site-packages/annotated_types-0.7.0.dist-info/METADATA b/pkg/fanal/analyzer/language/python/pip/testdata/libs/python-dir/lib/python3.10/site-packages/annotated_types-0.7.0.dist-info/METADATA new file mode 100644 index 000000000000..ba324aef51b1 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pip/testdata/libs/python-dir/lib/python3.10/site-packages/annotated_types-0.7.0.dist-info/METADATA @@ -0,0 +1,28 @@ +Metadata-Version: 2.3 +Name: annotated-types +Version: 0.7.0 +Summary: Reusable constraint types to use with typing.Annotated +Project-URL: Homepage, https://github.com/annotated-types/annotated-types +Project-URL: Source, https://github.com/annotated-types/annotated-types +Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases +Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin , Zac Hatfield-Dodds +License-File: LICENSE +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console +Classifier: Environment :: MacOS X +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Unix +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Typing :: Typed +Requires-Python: >=3.8 +Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9' +Description-Content-Type: text/markdown diff --git a/pkg/fanal/analyzer/language/python/pip/testdata/libs/python-dir/lib/python3.10/site-packages/click-8.0.0.dist-info/METADATA b/pkg/fanal/analyzer/language/python/pip/testdata/libs/python-dir/lib/python3.10/site-packages/click-8.0.0.dist-info/METADATA new file mode 100644 index 000000000000..5e67f0446795 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pip/testdata/libs/python-dir/lib/python3.10/site-packages/click-8.0.0.dist-info/METADATA @@ -0,0 +1,109 @@ +Metadata-Version: 2.1 +Name: click +Version: 8.0.0 +Summary: Composable command line interface toolkit +Home-page: https://palletsprojects.com/p/click/ +Author: Armin Ronacher +Author-email: armin.ronacher@active-4.com +Maintainer: Pallets +Maintainer-email: contact@palletsprojects.com +License: BSD-3-Clause +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Documentation, https://click.palletsprojects.com/ +Project-URL: Changes, https://click.palletsprojects.com/changes/ +Project-URL: Source Code, https://github.com/pallets/click/ +Project-URL: Issue Tracker, https://github.com/pallets/click/issues/ +Project-URL: Twitter, https://twitter.com/PalletsTeam +Project-URL: Chat, https://discord.gg/pallets +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Requires-Python: >=3.6 +Description-Content-Type: text/x-rst +Requires-Dist: colorama ; platform_system == "Windows" + +\$ click\_ +========== + +Click is a Python package for creating beautiful command line interfaces +in a composable way with as little code as necessary. It's the "Command +Line Interface Creation Kit". It's highly configurable but comes with +sensible defaults out of the box. + +It aims to make the process of writing command line tools quick and fun +while also preventing any frustration caused by the inability to +implement an intended CLI API. + +Click in three points: + +- Arbitrary nesting of commands +- Automatic help page generation +- Supports lazy loading of subcommands at runtime + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U click + +.. _pip: https://pip.pypa.io/en/stable/quickstart/ + + +A Simple Example +---------------- + +.. code-block:: python + + import click + + @click.command() + @click.option("--count", default=1, help="Number of greetings.") + @click.option("--name", prompt="Your name", help="The person to greet.") + def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for _ in range(count): + click.echo(f"Hello, {name}!") + + if __name__ == '__main__': + hello() + +.. code-block:: text + + $ python hello.py --count=3 + Your name: Click + Hello, Click! + Hello, Click! + Hello, Click! + + +Donate +------ + +The Pallets organization develops and supports Click and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, `please +donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://click.palletsprojects.com/ +- Changes: https://click.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/click/ +- Source Code: https://github.com/pallets/click +- Issue Tracker: https://github.com/pallets/click/issues +- Website: https://palletsprojects.com/p/click +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + + diff --git a/pkg/fanal/analyzer/language/python/pipenv/pipenv.go b/pkg/fanal/analyzer/language/python/pipenv/pipenv.go new file mode 100644 index 000000000000..54874b47eea0 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/pipenv/pipenv.go @@ -0,0 +1,46 @@ +package pipenv + +import ( + "context" + "os" + "path/filepath" + "slices" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/pipenv" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&pipenvLibraryAnalyzer{}) +} + +const version = 1 + +var requiredFiles = []string{types.PipfileLock} + +type pipenvLibraryAnalyzer struct{} + +func (a pipenvLibraryAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + res, err := language.Analyze(ctx, types.Pipenv, input.FilePath, input.Content, pipenv.NewParser()) + if err != nil { + return nil, xerrors.Errorf("unable to parse Pipfile.lock: %w", err) + } + return res, nil +} + +func (a pipenvLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + return slices.Contains(requiredFiles, fileName) +} + +func (a pipenvLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypePipenv +} + +func (a pipenvLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/python/poetry/poetry.go b/pkg/fanal/analyzer/language/python/poetry/poetry.go new file mode 100644 index 000000000000..9444220793b4 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/poetry.go @@ -0,0 +1,172 @@ +package poetry + +import ( + "context" + "errors" + "io" + "io/fs" + "os" + "path/filepath" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/poetry" + "github.com/aquasecurity/trivy/pkg/dependency/parser/python/pyproject" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypePoetry, newPoetryAnalyzer) +} + +const version = 1 + +type poetryAnalyzer struct { + logger *log.Logger + pyprojectParser *pyproject.Parser + lockParser language.Parser +} + +func newPoetryAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &poetryAnalyzer{ + logger: log.WithPrefix("poetry"), + pyprojectParser: pyproject.NewParser(), + lockParser: poetry.NewParser(), + }, nil +} + +func (a poetryAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.PoetryLock || input.FilePatterns.Match(path) + } + + err := fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error { + // Parse poetry.lock + app, err := a.parsePoetryLock(ctx, path, r) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + // Parse pyproject.toml alongside poetry.lock to identify the direct dependencies + if err = a.mergePyProject(input.FS, filepath.Dir(path), app); err != nil { + a.logger.Warn("Unable to parse pyproject.toml to identify direct dependencies", + log.FilePath(filepath.Join(filepath.Dir(path), types.PyProject)), log.Err(err)) + } + apps = append(apps, *app) + + return nil + }) + if err != nil { + return nil, xerrors.Errorf("poetry walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a poetryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + return fileName == types.PoetryLock || fileName == types.PyProject +} + +func (a poetryAnalyzer) Type() analyzer.Type { + return analyzer.TypePoetry +} + +func (a poetryAnalyzer) Version() int { + return version +} + +func (a poetryAnalyzer) parsePoetryLock(ctx context.Context, path string, r io.Reader) (*types.Application, error) { + return language.Parse(ctx, types.Poetry, path, r, a.lockParser) +} + +func (a poetryAnalyzer) mergePyProject(fsys fs.FS, dir string, app *types.Application) error { + // Parse pyproject.toml to identify the direct dependencies + path := filepath.Join(dir, types.PyProject) + project, err := a.parsePyProject(fsys, path) + if errors.Is(err, fs.ErrNotExist) { + // Assume all the packages are direct dependencies as it cannot identify them from poetry.lock + a.logger.Debug("pyproject.toml not found", log.FilePath(path)) + return nil + } else if err != nil { + return xerrors.Errorf("unable to parse %s: %w", path, err) + } + + dirDeps := directDeps(project) + prodDeps := prodPackages(app, project.MainDeps()) + + // Identify the direct/transitive/dev dependencies + for i, pkg := range app.Packages { + app.Packages[i].Dev = !prodDeps.Contains(pkg.ID) || app.Packages[i].Dev + if dirDeps.Contains(pkg.Name) { + app.Packages[i].Relationship = types.RelationshipDirect + } else { + app.Packages[i].Indirect = true + app.Packages[i].Relationship = types.RelationshipIndirect + } + } + return nil +} + +func directDeps(project pyproject.PyProject) set.Set[string] { + deps := project.MainDeps() + for _, groupDeps := range project.Tool.Poetry.Groups { + deps.Append(groupDeps.Dependencies.Items()...) + } + return deps +} + +func prodPackages(app *types.Application, prodRootDeps set.Set[string]) set.Set[string] { + packages := lo.SliceToMap(app.Packages, func(pkg types.Package) (string, types.Package) { + return pkg.ID, pkg + }) + + visited := set.New[string]() + + for _, pkg := range packages { + if !prodRootDeps.Contains(pkg.Name) { + continue + } + walkPackageDeps(pkg.ID, packages, visited) + } + + return visited +} + +func walkPackageDeps(pkgID string, packages map[string]types.Package, visited set.Set[string]) { + if visited.Contains(pkgID) { + return + } + visited.Append(pkgID) + for _, dep := range packages[pkgID].DependsOn { + walkPackageDeps(dep, packages, visited) + } +} + +func (a poetryAnalyzer) parsePyProject(fsys fs.FS, path string) (pyproject.PyProject, error) { + // Parse pyproject.toml + f, err := fsys.Open(path) + if err != nil { + return pyproject.PyProject{}, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + project, err := a.pyprojectParser.Parse(f) + if err != nil { + return pyproject.PyProject{}, err + } + + return project, nil +} diff --git a/pkg/fanal/analyzer/language/python/poetry/poetry_test.go b/pkg/fanal/analyzer/language/python/poetry/poetry_test.go new file mode 100644 index 000000000000..d8dbcfb48157 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/poetry_test.go @@ -0,0 +1,567 @@ +package poetry + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_poetryLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + dir string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + dir: "testdata/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Poetry, + FilePath: "poetry.lock", + Packages: types.Packages{ + { + ID: "certifi@2022.12.7", + Name: "certifi", + Version: "2022.12.7", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "charset-normalizer@2.1.1", + Name: "charset-normalizer", + Version: "2.1.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "click@7.1.2", + Name: "click", + Version: "7.1.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "flask@1.1.4", + Name: "flask", + Version: "1.1.4", + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "click@7.1.2", + "itsdangerous@1.1.0", + "jinja2@2.11.3", + "werkzeug@1.0.1", + }, + }, + { + ID: "idna@3.4", + Name: "idna", + Version: "3.4", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "itsdangerous@1.1.0", + Name: "itsdangerous", + Version: "1.1.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "jinja2@2.11.3", + Name: "jinja2", + Version: "2.11.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "markupsafe@2.1.2", + }, + }, + { + ID: "markupsafe@2.1.2", + Name: "markupsafe", + Version: "2.1.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "requests@2.28.1", + Name: "requests", + Version: "2.28.1", + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "certifi@2022.12.7", + "charset-normalizer@2.1.1", + "idna@3.4", + "urllib3@1.26.14", + }, + }, + { + ID: "urllib3@1.26.14", + Name: "urllib3", + Version: "1.26.14", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "werkzeug@1.0.1", + Name: "werkzeug", + Version: "1.0.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + }, + }, + }, + }, + }, + { + name: "happy path with poetry v2", + dir: "testdata/happy-v2", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Poetry, + FilePath: "poetry.lock", + Packages: types.Packages{ + { + ID: "atomicwrites@1.4.1", + Name: "atomicwrites", + Version: "1.4.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "attrs@25.1.0", + Name: "attrs", + Version: "25.1.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "click@8.1.8", + Name: "click", + Version: "8.1.8", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "colorama@0.4.6", + }, + }, + { + ID: "colorama@0.4.6", + Name: "colorama", + Version: "0.4.6", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "flask@1.0.3", + Name: "flask", + Version: "1.0.3", + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "click@8.1.8", + "itsdangerous@2.2.0", + "jinja2@3.1.5", + "werkzeug@3.1.3", + }, + }, + { + ID: "itsdangerous@2.2.0", + Name: "itsdangerous", + Version: "2.2.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "jinja2@3.1.5", + Name: "jinja2", + Version: "3.1.5", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "markupsafe@3.0.2", + }, + }, + { + ID: "markupsafe@3.0.2", + Name: "markupsafe", + Version: "3.0.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "more-itertools@10.6.0", + Name: "more-itertools", + Version: "10.6.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "packaging@24.2", + Name: "packaging", + Version: "24.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "pluggy@0.13.1", + Name: "pluggy", + Version: "0.13.1", + Relationship: types.RelationshipDirect, + }, + { + ID: "py@1.11.0", + Name: "py", + Version: "1.11.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "pytest@5.4.3", + Name: "pytest", + Version: "5.4.3", + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "atomicwrites@1.4.1", + "attrs@25.1.0", + "colorama@0.4.6", + "more-itertools@10.6.0", + "packaging@24.2", + "pluggy@0.13.1", + "py@1.11.0", + "wcwidth@0.2.13", + }, + Dev: true, + }, + { + ID: "ruff@0.8.3", + Name: "ruff", + Version: "0.8.3", + Relationship: types.RelationshipDirect, + Dev: true, + }, + { + ID: "wcwidth@0.2.13", + Name: "wcwidth", + Version: "0.2.13", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "werkzeug@3.1.3", + Name: "werkzeug", + Version: "3.1.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "markupsafe@3.0.2", + }, + }, + }, + }, + }, + }, + }, + { + name: "no pyproject.toml", + dir: "testdata/no-pyproject", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Poetry, + FilePath: "poetry.lock", + Packages: types.Packages{ + { + ID: "click@8.1.3", + Name: "click", + Version: "8.1.3", + DependsOn: []string{ + "colorama@0.4.6", + }, + }, + { + ID: "colorama@0.4.6", + Name: "colorama", + Version: "0.4.6", + }, + }, + }, + }, + }, + }, + { + name: "wrong pyproject.toml", + dir: "testdata/wrong-pyproject", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Poetry, + FilePath: "poetry.lock", + Packages: types.Packages{ + { + ID: "click@8.1.3", + Name: "click", + Version: "8.1.3", + DependsOn: []string{ + "colorama@0.4.6", + }, + }, + { + ID: "colorama@0.4.6", + Name: "colorama", + Version: "0.4.6", + }, + }, + }, + }, + }, + }, + { + name: "broken poetry.lock", + dir: "testdata/sad", + want: &analyzer.AnalysisResult{}, + }, + { + // docker run --name poetry --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh + // wget -qO- https://install.python-poetry.org | POETRY_VERSION=1.8.5 python3 - + // export PATH="/root/.local/bin:$PATH" + // poetry new groups && cd groups + // poetry add requests@2.32.3 + // poetry add httpx@0.28.1 --extras socks + // poetry add --optional typing-inspect@0.9.0 + // poetry add --group dev pytest@8.3.4 + // poetry add --group lint ruff@0.8.3 + name: "with groups", + dir: "testdata/with-groups", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Poetry, + FilePath: "poetry.lock", + Packages: types.Packages{ + { + ID: "anyio@4.7.0", + Name: "anyio", + Version: "4.7.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "exceptiongroup@1.2.2", + "idna@3.10", + "sniffio@1.3.1", + "typing-extensions@4.12.2", + }, + }, + { + ID: "certifi@2024.12.14", + Name: "certifi", + Version: "2024.12.14", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "charset-normalizer@3.4.0", + Name: "charset-normalizer", + Version: "3.4.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "colorama@0.4.6", + Name: "colorama", + Version: "0.4.6", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "exceptiongroup@1.2.2", + Name: "exceptiongroup", + Version: "1.2.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "h11@0.14.0", + Name: "h11", + Version: "0.14.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "httpcore@1.0.7", + Name: "httpcore", + Version: "1.0.7", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "certifi@2024.12.14", + "h11@0.14.0", + }, + }, + { + ID: "httpx@0.28.1", + Name: "httpx", + Version: "0.28.1", + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "anyio@4.7.0", + "certifi@2024.12.14", + "httpcore@1.0.7", + "idna@3.10", + "socksio@1.0.0", + }, + }, + { + ID: "idna@3.10", + Name: "idna", + Version: "3.10", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "iniconfig@2.0.0", + Name: "iniconfig", + Version: "2.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "mypy-extensions@1.0.0", + Name: "mypy-extensions", + Version: "1.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "packaging@24.2", + Name: "packaging", + Version: "24.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "pluggy@1.5.0", + Name: "pluggy", + Version: "1.5.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "pytest@8.3.4", + Name: "pytest", + Version: "8.3.4", + Relationship: types.RelationshipDirect, + Dev: true, + DependsOn: []string{ + "colorama@0.4.6", + "exceptiongroup@1.2.2", + "iniconfig@2.0.0", + "packaging@24.2", + "pluggy@1.5.0", + "tomli@2.2.1", + }, + }, + { + ID: "requests@2.32.3", + Name: "requests", + Version: "2.32.3", + DependsOn: []string{ + "certifi@2024.12.14", + "charset-normalizer@3.4.0", + "idna@3.10", + "urllib3@2.2.3", + }, + Relationship: types.RelationshipDirect, + }, + { + ID: "ruff@0.8.3", + Name: "ruff", + Version: "0.8.3", + Relationship: types.RelationshipDirect, + Dev: true, + }, + { + ID: "sniffio@1.3.1", + Name: "sniffio", + Version: "1.3.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "socksio@1.0.0", + Name: "socksio", + Version: "1.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "tomli@2.2.1", + Name: "tomli", + Version: "2.2.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "typing-extensions@4.12.2", + Name: "typing-extensions", + Version: "4.12.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "typing-inspect@0.9.0", + Name: "typing-inspect", + Version: "0.9.0", + DependsOn: []string{ + "mypy-extensions@1.0.0", + "typing-extensions@4.12.2", + }, + Relationship: types.RelationshipDirect, + }, + { + ID: "urllib3@2.2.3", + Name: "urllib3", + Version: "2.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newPoetryAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/happy-v2/poetry.lock b/pkg/fanal/analyzer/language/python/poetry/testdata/happy-v2/poetry.lock new file mode 100644 index 000000000000..38ec790b6a50 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/happy-v2/poetry.lock @@ -0,0 +1,325 @@ +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. + +[[package]] +name = "atomicwrites" +version = "1.4.1" +description = "Atomic file writes." +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +markers = "sys_platform == \"win32\"" +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] + +[[package]] +name = "attrs" +version = "25.1.0" +description = "Classes Without Boilerplate" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, + {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, +] + +[package.extras] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "flask" +version = "1.0.3" +description = "A simple framework for building complex web applications." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "Flask-1.0.3-py2.py3-none-any.whl", hash = "sha256:e7d32475d1de5facaa55e3958bc4ec66d3762076b074296aa50ef8fdc5b9df61"}, + {file = "Flask-1.0.3.tar.gz", hash = "sha256:ad7c6d841e64296b962296c2c2dabc6543752985727af86a975072dea984b6f3"}, +] + +[package.dependencies] +click = ">=5.1" +itsdangerous = ">=0.24" +Jinja2 = ">=2.10" +Werkzeug = ">=0.14" + +[package.extras] +dev = ["coverage", "pallets-sphinx-themes", "pytest (>=3)", "sphinx", "sphinxcontrib-log-cabinet", "tox"] +docs = ["pallets-sphinx-themes", "sphinx", "sphinxcontrib-log-cabinet"] +dotenv = ["python-dotenv"] + +[[package]] +name = "itsdangerous" +version = "2.2.0" +description = "Safely pass data to untrusted environments and back." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"}, + {file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"}, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, + {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "more-itertools" +version = "10.6.0" +description = "More routines for operating on iterables, beyond itertools" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "more-itertools-10.6.0.tar.gz", hash = "sha256:2cd7fad1009c31cc9fb6a035108509e6547547a7a738374f10bd49a09eb3ee3b"}, + {file = "more_itertools-10.6.0-py3-none-any.whl", hash = "sha256:6eb054cb4b6db1473f6e15fcc676a08e4732548acd47c708f0e179c2c7c01e89"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["main", "dev"] +files = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "py" +version = "1.11.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["dev"] +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] + +[[package]] +name = "pytest" +version = "5.4.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=17.4.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +more-itertools = ">=4.0.0" +packaging = "*" +pluggy = ">=0.12,<1.0" +py = ">=1.5.0" +wcwidth = "*" + +[package.extras] +checkqa-mypy = ["mypy (==v0.761)"] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "ruff" +version = "0.8.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["lint"] +files = [ + {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +description = "Measures the displayed width of unicode strings in a terminal" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +description = "The comprehensive WSGI web application library." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, + {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, +] + +[package.dependencies] +MarkupSafe = ">=2.1.1" + +[package.extras] +watchdog = ["watchdog (>=2.3)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.9" +content-hash = "c1f6d8c5f464b9a8148389d8f9b7f7fbad8dbaecf9592b93bd0e07e3223d1c02" diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/happy-v2/pyproject.toml b/pkg/fanal/analyzer/language/python/poetry/testdata/happy-v2/pyproject.toml new file mode 100644 index 000000000000..af4ecc7a2da6 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/happy-v2/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "web" +version = "0.1.0" +description = "" +authors = [ + {name = "Your Name",email = "you@example.com"} +] +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "flask (==1.0.3)", + "pluggy (==0.13.1)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" + +[tool.poetry.group.dev.dependencies] +pytest = "5.4.3" + + +[tool.poetry.group.lint.dependencies] +ruff = "0.8.3" + diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/happy/poetry.lock b/pkg/fanal/analyzer/language/python/poetry/testdata/happy/poetry.lock new file mode 100644 index 000000000000..39fcaab78167 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/happy/poetry.lock @@ -0,0 +1,225 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = true +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[[package]] +name = "charset-normalizer" +version = "2.1.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +category = "main" +optional = true +python-versions = ">=3.6.0" +files = [ + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, +] + +[package.extras] +unicode-backport = ["unicodedata2"] + +[[package]] +name = "click" +version = "7.1.2" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "click-7.1.2-py2.py3-none-any.whl", hash = "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"}, + {file = "click-7.1.2.tar.gz", hash = "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a"}, +] + +[[package]] +name = "flask" +version = "1.1.4" +description = "A simple framework for building complex web applications." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "Flask-1.1.4-py2.py3-none-any.whl", hash = "sha256:c34f04500f2cbbea882b1acb02002ad6fe6b7ffa64a6164577995657f50aed22"}, + {file = "Flask-1.1.4.tar.gz", hash = "sha256:0fbeb6180d383a9186d0d6ed954e0042ad9f18e0e8de088b2b419d526927d196"}, +] + +[package.dependencies] +click = ">=5.1,<8.0" +itsdangerous = ">=0.24,<2.0" +Jinja2 = ">=2.10.1,<3.0" +Werkzeug = ">=0.15,<2.0" + +[package.extras] +dev = ["coverage", "pallets-sphinx-themes", "pytest", "sphinx", "sphinx-issues", "sphinxcontrib-log-cabinet", "tox"] +docs = ["pallets-sphinx-themes", "sphinx", "sphinx-issues", "sphinxcontrib-log-cabinet"] +dotenv = ["python-dotenv"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = true +python-versions = ">=3.5" +files = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] + +[[package]] +name = "itsdangerous" +version = "1.1.0" +description = "Various helpers to pass data to untrusted environments and back." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "itsdangerous-1.1.0-py2.py3-none-any.whl", hash = "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749"}, + {file = "itsdangerous-1.1.0.tar.gz", hash = "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19"}, +] + +[[package]] +name = "jinja2" +version = "2.11.3" +description = "A very fast and expressive template engine." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "Jinja2-2.11.3-py2.py3-none-any.whl", hash = "sha256:03e47ad063331dd6a3f04a43eddca8a966a26ba0c5b7207a9a9e4e08f1b29419"}, + {file = "Jinja2-2.11.3.tar.gz", hash = "sha256:a6d58433de0ae800347cab1fa3043cebbabe8baa9d29e668f1c768cb87a333c6"}, +] + +[package.dependencies] +MarkupSafe = ">=0.23" + +[package.extras] +i18n = ["Babel (>=0.8)"] + +[[package]] +name = "markupsafe" +version = "2.1.2" +description = "Safely add untrusted strings to HTML/XML markup." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win32.whl", hash = "sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603"}, + {file = "MarkupSafe-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win32.whl", hash = "sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625"}, + {file = "MarkupSafe-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win32.whl", hash = "sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859"}, + {file = "MarkupSafe-2.1.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win32.whl", hash = "sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2"}, + {file = "MarkupSafe-2.1.2-cp38-cp38-win_amd64.whl", hash = "sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win32.whl", hash = "sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7"}, + {file = "MarkupSafe-2.1.2-cp39-cp39-win_amd64.whl", hash = "sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed"}, + {file = "MarkupSafe-2.1.2.tar.gz", hash = "sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d"}, +] + +[[package]] +name = "requests" +version = "2.28.1" +description = "Python HTTP for Humans." +category = "main" +optional = true +python-versions = ">=3.7, <4" +files = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<3" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "urllib3" +version = "1.26.14" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = true +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.14-py2.py3-none-any.whl", hash = "sha256:75edcdc2f7d85b137124a6c3c9fc3933cdeaa12ecb9a6a959f22797a0feca7e1"}, + {file = "urllib3-1.26.14.tar.gz", hash = "sha256:076907bf8fd355cde77728471316625a4d2f7e713c125f51953bb5b3eecf4f72"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "werkzeug" +version = "1.0.1" +description = "The comprehensive WSGI web application library." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "Werkzeug-1.0.1-py2.py3-none-any.whl", hash = "sha256:2de2a5db0baeae7b2d2664949077c2ac63fbd16d98da0ff71837f7d1dea3fd43"}, + {file = "Werkzeug-1.0.1.tar.gz", hash = "sha256:6c80b1e5ad3665290ea39320b91e1be1e0d5f60652b964a3070216de83d2e47c"}, +] + +[package.extras] +dev = ["coverage", "pallets-sphinx-themes", "pytest", "pytest-timeout", "sphinx", "sphinx-issues", "tox"] +watchdog = ["watchdog"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "0ee6cb4bc2d84091d5dcb0a0110a65f244987ed427933b2f49949195e3ef69c7" diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/happy/pyproject.toml b/pkg/fanal/analyzer/language/python/poetry/testdata/happy/pyproject.toml new file mode 100644 index 000000000000..9d23492736e4 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/happy/pyproject.toml @@ -0,0 +1,16 @@ +[tool.poetry] +name = "example" +version = "0.1.0" +description = "My Hello World Example" +authors = ["Trivy"] + +[tool.poetry.dependencies] +python = "^3.9" +Flask = "^1.0" +requests = {version = "2.28.1", optional = true} + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/no-pyproject/poetry.lock b/pkg/fanal/analyzer/language/python/poetry/testdata/no-pyproject/poetry.lock new file mode 100644 index 000000000000..232e8289fa3a --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/no-pyproject/poetry.lock @@ -0,0 +1,33 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "c84861cc8679600635c65a32b5079dbfdf0c615c25a7db3d94c23156df8c56e9" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/sad/poetry.lock b/pkg/fanal/analyzer/language/python/poetry/testdata/sad/poetry.lock new file mode 100644 index 000000000000..8e2f0bef135b --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/sad/poetry.lock @@ -0,0 +1 @@ +[ \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/with-groups/poetry.lock b/pkg/fanal/analyzer/language/python/poetry/testdata/with-groups/poetry.lock new file mode 100644 index 000000000000..f90fb4513a70 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/with-groups/poetry.lock @@ -0,0 +1,473 @@ +# This file is automatically @generated by Poetry 1.8.5 and should not be changed by hand. + +[[package]] +name = "anyio" +version = "4.7.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.9" +files = [ + {file = "anyio-4.7.0-py3-none-any.whl", hash = "sha256:ea60c3723ab42ba6fff7e8ccb0488c898ec538ff4df1f1d5e642c3601d07e352"}, + {file = "anyio-4.7.0.tar.gz", hash = "sha256:2f834749c602966b7d456a7567cafcb309f96482b5081d14ac93ccd457f9dd48"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "certifi" +version = "2024.12.14" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, + {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc"}, + {file = "charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99"}, + {file = "charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7"}, + {file = "charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67"}, + {file = "charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dbe03226baf438ac4fda9e2d0715022fd579cb641c4cf639fa40d53b2fe6f3e2"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dd9a8bd8900e65504a305bf8ae6fa9fbc66de94178c420791d0293702fce2df7"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8831399554b92b72af5932cdbbd4ddc55c55f631bb13ff8fe4e6536a06c5c51"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a14969b8691f7998e74663b77b4c36c0337cb1df552da83d5c9004a93afdb574"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcaf7c1524c0542ee2fc82cc8ec337f7a9f7edee2532421ab200d2b920fc97cf"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:425c5f215d0eecee9a56cdb703203dda90423247421bf0d67125add85d0c4455"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:d5b054862739d276e09928de37c79ddeec42a6e1bfc55863be96a36ba22926f6"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:f3e73a4255342d4eb26ef6df01e3962e73aa29baa3124a8e824c5d3364a65748"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:2f6c34da58ea9c1a9515621f4d9ac379871a8f21168ba1b5e09d74250de5ad62"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:f09cb5a7bbe1ecae6e87901a2eb23e0256bb524a79ccc53eb0b7629fbe7677c4"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:0099d79bdfcf5c1f0c2c72f91516702ebf8b0b8ddd8905f97a8aecf49712c621"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win32.whl", hash = "sha256:9c98230f5042f4945f957d006edccc2af1e03ed5e37ce7c373f00a5a4daa6149"}, + {file = "charset_normalizer-3.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:62f60aebecfc7f4b82e3f639a7d1433a20ec32824db2199a11ad4f5e146ef5ee"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:af73657b7a68211996527dbfeffbb0864e043d270580c5aef06dc4b659a4b578"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cab5d0b79d987c67f3b9e9c53f54a61360422a5a0bc075f43cab5621d530c3b6"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9289fd5dddcf57bab41d044f1756550f9e7cf0c8e373b8cdf0ce8773dc4bd417"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b493a043635eb376e50eedf7818f2f322eabbaa974e948bd8bdd29eb7ef2a51"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9fa2566ca27d67c86569e8c85297aaf413ffab85a8960500f12ea34ff98e4c41"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8e538f46104c815be19c975572d74afb53f29650ea2025bbfaef359d2de2f7f"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fd30dc99682dc2c603c2b315bded2799019cea829f8bf57dc6b61efde6611c8"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2006769bd1640bdf4d5641c69a3d63b71b81445473cac5ded39740a226fa88ab"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:dc15e99b2d8a656f8e666854404f1ba54765871104e50c8e9813af8a7db07f12"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:ab2e5bef076f5a235c3774b4f4028a680432cded7cad37bba0fd90d64b187d19"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:4ec9dd88a5b71abfc74e9df5ebe7921c35cbb3b641181a531ca65cdb5e8e4dea"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:43193c5cda5d612f247172016c4bb71251c784d7a4d9314677186a838ad34858"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:aa693779a8b50cd97570e5a0f343538a8dbd3e496fa5dcb87e29406ad0299654"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win32.whl", hash = "sha256:7706f5850360ac01d80c89bcef1640683cc12ed87f42579dab6c5d3ed6888613"}, + {file = "charset_normalizer-3.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:c3e446d253bd88f6377260d07c895816ebf33ffffd56c1c792b13bff9c3e1ade"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2"}, + {file = "charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca"}, + {file = "charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079"}, + {file = "charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e"}, +] + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.7" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd"}, + {file = "httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +socksio = {version = "==1.*", optional = true, markers = "extra == \"socks\""} + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = true +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pytest" +version = "8.3.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, + {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "ruff" +version = "0.8.3" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"}, + {file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"}, + {file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"}, + {file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"}, + {file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"}, + {file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"}, + {file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"}, + {file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"}, + {file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"}, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "socksio" +version = "1.0.0" +description = "Sans-I/O implementation of SOCKS4, SOCKS4A, and SOCKS5." +optional = false +python-versions = ">=3.6" +files = [ + {file = "socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3"}, + {file = "socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + +[[package]] +name = "typing-inspect" +version = "0.9.0" +description = "Runtime inspection utilities for typing module." +optional = true +python-versions = "*" +files = [ + {file = "typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f"}, + {file = "typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78"}, +] + +[package.dependencies] +mypy-extensions = ">=0.3.0" +typing-extensions = ">=3.7.4" + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "eb9992c7bdd3f378ae8559495b790f625f0368d978b5664820fa06b2db854bff" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/with-groups/pyproject.toml b/pkg/fanal/analyzer/language/python/poetry/testdata/with-groups/pyproject.toml new file mode 100644 index 000000000000..919afb32bb68 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/with-groups/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "groups" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.9" +requests = "2.32.3" +typing-inspect = {version = "0.9.0", optional = true} +httpx = {version = "0.28.1", extras = ["socks"]} + + +[tool.poetry.group.dev.dependencies] +pytest = "8.3.4" + + +[tool.poetry.group.lint.dependencies] +ruff = "0.8.3" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/wrong-pyproject/poetry.lock b/pkg/fanal/analyzer/language/python/poetry/testdata/wrong-pyproject/poetry.lock new file mode 100644 index 000000000000..232e8289fa3a --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/wrong-pyproject/poetry.lock @@ -0,0 +1,33 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "c84861cc8679600635c65a32b5079dbfdf0c615c25a7db3d94c23156df8c56e9" \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/python/poetry/testdata/wrong-pyproject/pyproject.toml b/pkg/fanal/analyzer/language/python/poetry/testdata/wrong-pyproject/pyproject.toml new file mode 100644 index 000000000000..558ed37d93c5 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/poetry/testdata/wrong-pyproject/pyproject.toml @@ -0,0 +1 @@ +[ diff --git a/pkg/fanal/analyzer/language/python/uv/testdata/broken-lock/uv.lock b/pkg/fanal/analyzer/language/python/uv/testdata/broken-lock/uv.lock new file mode 100644 index 000000000000..74e0f12e3246 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/uv/testdata/broken-lock/uv.lock @@ -0,0 +1 @@ +! \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/python/uv/testdata/happy/uv.lock b/pkg/fanal/analyzer/language/python/uv/testdata/happy/uv.lock new file mode 100644 index 000000000000..ac34c2270333 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/uv/testdata/happy/uv.lock @@ -0,0 +1,252 @@ +version = 1 +requires-python = ">=3.9" + +[[package]] +name = "certifi" +version = "2024.12.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/4f/e1808dc01273379acc506d18f1504eb2d299bd4131743b9fc54d7be4df1e/charset_normalizer-3.4.0.tar.gz", hash = "sha256:223217c3d4f82c3ac5e29032b3f1c2eb0fb591b72161f86d93f5719079dae93e", size = 106620 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/8b/825cc84cf13a28bfbcba7c416ec22bf85a9584971be15b21dd8300c65b7f/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4f9fc98dad6c2eaa32fc3af1417d95b5e3d08aff968df0cd320066def971f9a6", size = 196363 }, + { url = "https://files.pythonhosted.org/packages/23/81/d7eef6a99e42c77f444fdd7bc894b0ceca6c3a95c51239e74a722039521c/charset_normalizer-3.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0de7b687289d3c1b3e8660d0741874abe7888100efe14bd0f9fd7141bcbda92b", size = 125639 }, + { url = "https://files.pythonhosted.org/packages/21/67/b4564d81f48042f520c948abac7079356e94b30cb8ffb22e747532cf469d/charset_normalizer-3.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5ed2e36c3e9b4f21dd9422f6893dec0abf2cca553af509b10cd630f878d3eb99", size = 120451 }, + { url = "https://files.pythonhosted.org/packages/c2/72/12a7f0943dd71fb5b4e7b55c41327ac0a1663046a868ee4d0d8e9c369b85/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:40d3ff7fc90b98c637bda91c89d51264a3dcf210cade3a2c6f838c7268d7a4ca", size = 140041 }, + { url = "https://files.pythonhosted.org/packages/67/56/fa28c2c3e31217c4c52158537a2cf5d98a6c1e89d31faf476c89391cd16b/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1110e22af8ca26b90bd6364fe4c763329b0ebf1ee213ba32b68c73de5752323d", size = 150333 }, + { url = "https://files.pythonhosted.org/packages/f9/d2/466a9be1f32d89eb1554cf84073a5ed9262047acee1ab39cbaefc19635d2/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86f4e8cca779080f66ff4f191a685ced73d2f72d50216f7112185dc02b90b9b7", size = 142921 }, + { url = "https://files.pythonhosted.org/packages/f8/01/344ec40cf5d85c1da3c1f57566c59e0c9b56bcc5566c08804a95a6cc8257/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f683ddc7eedd742e2889d2bfb96d69573fde1d92fcb811979cdb7165bb9c7d3", size = 144785 }, + { url = "https://files.pythonhosted.org/packages/73/8b/2102692cb6d7e9f03b9a33a710e0164cadfce312872e3efc7cfe22ed26b4/charset_normalizer-3.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27623ba66c183eca01bf9ff833875b459cad267aeeb044477fedac35e19ba907", size = 146631 }, + { url = "https://files.pythonhosted.org/packages/d8/96/cc2c1b5d994119ce9f088a9a0c3ebd489d360a2eb058e2c8049f27092847/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f606a1881d2663630ea5b8ce2efe2111740df4b687bd78b34a8131baa007f79b", size = 140867 }, + { url = "https://files.pythonhosted.org/packages/c9/27/cde291783715b8ec30a61c810d0120411844bc4c23b50189b81188b273db/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0b309d1747110feb25d7ed6b01afdec269c647d382c857ef4663bbe6ad95a912", size = 149273 }, + { url = "https://files.pythonhosted.org/packages/3a/a4/8633b0fc1a2d1834d5393dafecce4a1cc56727bfd82b4dc18fc92f0d3cc3/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:136815f06a3ae311fae551c3df1f998a1ebd01ddd424aa5603a4336997629e95", size = 152437 }, + { url = "https://files.pythonhosted.org/packages/64/ea/69af161062166b5975ccbb0961fd2384853190c70786f288684490913bf5/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:14215b71a762336254351b00ec720a8e85cada43b987da5a042e4ce3e82bd68e", size = 150087 }, + { url = "https://files.pythonhosted.org/packages/3b/fd/e60a9d9fd967f4ad5a92810138192f825d77b4fa2a557990fd575a47695b/charset_normalizer-3.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:79983512b108e4a164b9c8d34de3992f76d48cadc9554c9e60b43f308988aabe", size = 145142 }, + { url = "https://files.pythonhosted.org/packages/6d/02/8cb0988a1e49ac9ce2eed1e07b77ff118f2923e9ebd0ede41ba85f2dcb04/charset_normalizer-3.4.0-cp310-cp310-win32.whl", hash = "sha256:c94057af19bc953643a33581844649a7fdab902624d2eb739738a30e2b3e60fc", size = 94701 }, + { url = "https://files.pythonhosted.org/packages/d6/20/f1d4670a8a723c46be695dff449d86d6092916f9e99c53051954ee33a1bc/charset_normalizer-3.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:55f56e2ebd4e3bc50442fbc0888c9d8c94e4e06a933804e2af3e89e2f9c1c749", size = 102191 }, + { url = "https://files.pythonhosted.org/packages/9c/61/73589dcc7a719582bf56aae309b6103d2762b526bffe189d635a7fcfd998/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0d99dd8ff461990f12d6e42c7347fd9ab2532fb70e9621ba520f9e8637161d7c", size = 193339 }, + { url = "https://files.pythonhosted.org/packages/77/d5/8c982d58144de49f59571f940e329ad6e8615e1e82ef84584c5eeb5e1d72/charset_normalizer-3.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c57516e58fd17d03ebe67e181a4e4e2ccab1168f8c2976c6a334d4f819fe5944", size = 124366 }, + { url = "https://files.pythonhosted.org/packages/bf/19/411a64f01ee971bed3231111b69eb56f9331a769072de479eae7de52296d/charset_normalizer-3.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6dba5d19c4dfab08e58d5b36304b3f92f3bd5d42c1a3fa37b5ba5cdf6dfcbcee", size = 118874 }, + { url = "https://files.pythonhosted.org/packages/4c/92/97509850f0d00e9f14a46bc751daabd0ad7765cff29cdfb66c68b6dad57f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf4475b82be41b07cc5e5ff94810e6a01f276e37c2d55571e3fe175e467a1a1c", size = 138243 }, + { url = "https://files.pythonhosted.org/packages/e2/29/d227805bff72ed6d6cb1ce08eec707f7cfbd9868044893617eb331f16295/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce031db0408e487fd2775d745ce30a7cd2923667cf3b69d48d219f1d8f5ddeb6", size = 148676 }, + { url = "https://files.pythonhosted.org/packages/13/bc/87c2c9f2c144bedfa62f894c3007cd4530ba4b5351acb10dc786428a50f0/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ff4e7cdfdb1ab5698e675ca622e72d58a6fa2a8aa58195de0c0061288e6e3ea", size = 141289 }, + { url = "https://files.pythonhosted.org/packages/eb/5b/6f10bad0f6461fa272bfbbdf5d0023b5fb9bc6217c92bf068fa5a99820f5/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3710a9751938947e6327ea9f3ea6332a09bf0ba0c09cae9cb1f250bd1f1549bc", size = 142585 }, + { url = "https://files.pythonhosted.org/packages/3b/a0/a68980ab8a1f45a36d9745d35049c1af57d27255eff8c907e3add84cf68f/charset_normalizer-3.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82357d85de703176b5587dbe6ade8ff67f9f69a41c0733cf2425378b49954de5", size = 144408 }, + { url = "https://files.pythonhosted.org/packages/d7/a1/493919799446464ed0299c8eef3c3fad0daf1c3cd48bff9263c731b0d9e2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47334db71978b23ebcf3c0f9f5ee98b8d65992b65c9c4f2d34c2eaf5bcaf0594", size = 139076 }, + { url = "https://files.pythonhosted.org/packages/fb/9d/9c13753a5a6e0db4a0a6edb1cef7aee39859177b64e1a1e748a6e3ba62c2/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8ce7fd6767a1cc5a92a639b391891bf1c268b03ec7e021c7d6d902285259685c", size = 146874 }, + { url = "https://files.pythonhosted.org/packages/75/d2/0ab54463d3410709c09266dfb416d032a08f97fd7d60e94b8c6ef54ae14b/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f1a2f519ae173b5b6a2c9d5fa3116ce16e48b3462c8b96dfdded11055e3d6365", size = 150871 }, + { url = "https://files.pythonhosted.org/packages/8d/c9/27e41d481557be53d51e60750b85aa40eaf52b841946b3cdeff363105737/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:63bc5c4ae26e4bc6be6469943b8253c0fd4e4186c43ad46e713ea61a0ba49129", size = 148546 }, + { url = "https://files.pythonhosted.org/packages/ee/44/4f62042ca8cdc0cabf87c0fc00ae27cd8b53ab68be3605ba6d071f742ad3/charset_normalizer-3.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bcb4f8ea87d03bc51ad04add8ceaf9b0f085ac045ab4d74e73bbc2dc033f0236", size = 143048 }, + { url = "https://files.pythonhosted.org/packages/01/f8/38842422988b795220eb8038745d27a675ce066e2ada79516c118f291f07/charset_normalizer-3.4.0-cp311-cp311-win32.whl", hash = "sha256:9ae4ef0b3f6b41bad6366fb0ea4fc1d7ed051528e113a60fa2a65a9abb5b1d99", size = 94389 }, + { url = "https://files.pythonhosted.org/packages/0b/6e/b13bd47fa9023b3699e94abf565b5a2f0b0be6e9ddac9812182596ee62e4/charset_normalizer-3.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cee4373f4d3ad28f1ab6290684d8e2ebdb9e7a1b74fdc39e4c211995f77bec27", size = 101752 }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4b7a70987abf9b8196845806198975b6aab4ce016632f817ad758a5aa056/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0713f3adb9d03d49d365b70b84775d0a0d18e4ab08d12bc46baa6132ba78aaf6", size = 194445 }, + { url = "https://files.pythonhosted.org/packages/50/89/354cc56cf4dd2449715bc9a0f54f3aef3dc700d2d62d1fa5bbea53b13426/charset_normalizer-3.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:de7376c29d95d6719048c194a9cf1a1b0393fbe8488a22008610b0361d834ecf", size = 125275 }, + { url = "https://files.pythonhosted.org/packages/fa/44/b730e2a2580110ced837ac083d8ad222343c96bb6b66e9e4e706e4d0b6df/charset_normalizer-3.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a51b48f42d9358460b78725283f04bddaf44a9358197b889657deba38f329db", size = 119020 }, + { url = "https://files.pythonhosted.org/packages/9d/e4/9263b8240ed9472a2ae7ddc3e516e71ef46617fe40eaa51221ccd4ad9a27/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b295729485b06c1a0683af02a9e42d2caa9db04a373dc38a6a58cdd1e8abddf1", size = 139128 }, + { url = "https://files.pythonhosted.org/packages/6b/e3/9f73e779315a54334240353eaea75854a9a690f3f580e4bd85d977cb2204/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ee803480535c44e7f5ad00788526da7d85525cfefaf8acf8ab9a310000be4b03", size = 149277 }, + { url = "https://files.pythonhosted.org/packages/1a/cf/f1f50c2f295312edb8a548d3fa56a5c923b146cd3f24114d5adb7e7be558/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d59d125ffbd6d552765510e3f31ed75ebac2c7470c7274195b9161a32350284", size = 142174 }, + { url = "https://files.pythonhosted.org/packages/16/92/92a76dc2ff3a12e69ba94e7e05168d37d0345fa08c87e1fe24d0c2a42223/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cda06946eac330cbe6598f77bb54e690b4ca93f593dee1568ad22b04f347c15", size = 143838 }, + { url = "https://files.pythonhosted.org/packages/a4/01/2117ff2b1dfc61695daf2babe4a874bca328489afa85952440b59819e9d7/charset_normalizer-3.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07afec21bbbbf8a5cc3651aa96b980afe2526e7f048fdfb7f1014d84acc8b6d8", size = 146149 }, + { url = "https://files.pythonhosted.org/packages/f6/9b/93a332b8d25b347f6839ca0a61b7f0287b0930216994e8bf67a75d050255/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6b40e8d38afe634559e398cc32b1472f376a4099c75fe6299ae607e404c033b2", size = 140043 }, + { url = "https://files.pythonhosted.org/packages/ab/f6/7ac4a01adcdecbc7a7587767c776d53d369b8b971382b91211489535acf0/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b8dcd239c743aa2f9c22ce674a145e0a25cb1566c495928440a181ca1ccf6719", size = 148229 }, + { url = "https://files.pythonhosted.org/packages/9d/be/5708ad18161dee7dc6a0f7e6cf3a88ea6279c3e8484844c0590e50e803ef/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:84450ba661fb96e9fd67629b93d2941c871ca86fc38d835d19d4225ff946a631", size = 151556 }, + { url = "https://files.pythonhosted.org/packages/5a/bb/3d8bc22bacb9eb89785e83e6723f9888265f3a0de3b9ce724d66bd49884e/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:44aeb140295a2f0659e113b31cfe92c9061622cadbc9e2a2f7b8ef6b1e29ef4b", size = 149772 }, + { url = "https://files.pythonhosted.org/packages/f7/fa/d3fc622de05a86f30beea5fc4e9ac46aead4731e73fd9055496732bcc0a4/charset_normalizer-3.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1db4e7fefefd0f548d73e2e2e041f9df5c59e178b4c72fbac4cc6f535cfb1565", size = 144800 }, + { url = "https://files.pythonhosted.org/packages/9a/65/bdb9bc496d7d190d725e96816e20e2ae3a6fa42a5cac99c3c3d6ff884118/charset_normalizer-3.4.0-cp312-cp312-win32.whl", hash = "sha256:5726cf76c982532c1863fb64d8c6dd0e4c90b6ece9feb06c9f202417a31f7dd7", size = 94836 }, + { url = "https://files.pythonhosted.org/packages/3e/67/7b72b69d25b89c0b3cea583ee372c43aa24df15f0e0f8d3982c57804984b/charset_normalizer-3.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:b197e7094f232959f8f20541ead1d9862ac5ebea1d58e9849c1bf979255dfac9", size = 102187 }, + { url = "https://files.pythonhosted.org/packages/f3/89/68a4c86f1a0002810a27f12e9a7b22feb198c59b2f05231349fbce5c06f4/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dd4eda173a9fcccb5f2e2bd2a9f423d180194b1bf17cf59e3269899235b2a114", size = 194617 }, + { url = "https://files.pythonhosted.org/packages/4f/cd/8947fe425e2ab0aa57aceb7807af13a0e4162cd21eee42ef5b053447edf5/charset_normalizer-3.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9e3c4c9e1ed40ea53acf11e2a386383c3304212c965773704e4603d589343ed", size = 125310 }, + { url = "https://files.pythonhosted.org/packages/5b/f0/b5263e8668a4ee9becc2b451ed909e9c27058337fda5b8c49588183c267a/charset_normalizer-3.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92a7e36b000bf022ef3dbb9c46bfe2d52c047d5e3f3343f43204263c5addc250", size = 119126 }, + { url = "https://files.pythonhosted.org/packages/ff/6e/e445afe4f7fda27a533f3234b627b3e515a1b9429bc981c9a5e2aa5d97b6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b6a92d009cbe2fb11054ba694bc9e284dad30a26757b1e372a1fdddaf21920", size = 139342 }, + { url = "https://files.pythonhosted.org/packages/a1/b2/4af9993b532d93270538ad4926c8e37dc29f2111c36f9c629840c57cd9b3/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ffd9493de4c922f2a38c2bf62b831dcec90ac673ed1ca182fe11b4d8e9f2a64", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/fb/6f/4e78c3b97686b871db9be6f31d64e9264e889f8c9d7ab33c771f847f79b7/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35c404d74c2926d0287fbd63ed5d27eb911eb9e4a3bb2c6d294f3cfd4a9e0c23", size = 142214 }, + { url = "https://files.pythonhosted.org/packages/2b/c9/1c8fe3ce05d30c87eff498592c89015b19fade13df42850aafae09e94f35/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4796efc4faf6b53a18e3d46343535caed491776a22af773f366534056c4e1fbc", size = 144104 }, + { url = "https://files.pythonhosted.org/packages/ee/68/efad5dcb306bf37db7db338338e7bb8ebd8cf38ee5bbd5ceaaaa46f257e6/charset_normalizer-3.4.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e7fdd52961feb4c96507aa649550ec2a0d527c086d284749b2f582f2d40a2e0d", size = 146255 }, + { url = "https://files.pythonhosted.org/packages/0c/75/1ed813c3ffd200b1f3e71121c95da3f79e6d2a96120163443b3ad1057505/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:92db3c28b5b2a273346bebb24857fda45601aef6ae1c011c0a997106581e8a88", size = 140251 }, + { url = "https://files.pythonhosted.org/packages/7d/0d/6f32255c1979653b448d3c709583557a4d24ff97ac4f3a5be156b2e6a210/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ab973df98fc99ab39080bfb0eb3a925181454d7c3ac8a1e695fddfae696d9e90", size = 148474 }, + { url = "https://files.pythonhosted.org/packages/ac/a0/c1b5298de4670d997101fef95b97ac440e8c8d8b4efa5a4d1ef44af82f0d/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4b67fdab07fdd3c10bb21edab3cbfe8cf5696f453afce75d815d9d7223fbe88b", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/04/4f/b3961ba0c664989ba63e30595a3ed0875d6790ff26671e2aae2fdc28a399/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:aa41e526a5d4a9dfcfbab0716c7e8a1b215abd3f3df5a45cf18a12721d31cb5d", size = 149781 }, + { url = "https://files.pythonhosted.org/packages/d8/90/6af4cd042066a4adad58ae25648a12c09c879efa4849c705719ba1b23d8c/charset_normalizer-3.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ffc519621dce0c767e96b9c53f09c5d215578e10b02c285809f76509a3931482", size = 144970 }, + { url = "https://files.pythonhosted.org/packages/cc/67/e5e7e0cbfefc4ca79025238b43cdf8a2037854195b37d6417f3d0895c4c2/charset_normalizer-3.4.0-cp313-cp313-win32.whl", hash = "sha256:f19c1585933c82098c2a520f8ec1227f20e339e33aca8fa6f956f6691b784e67", size = 94973 }, + { url = "https://files.pythonhosted.org/packages/65/97/fc9bbc54ee13d33dc54a7fcf17b26368b18505500fc01e228c27b5222d80/charset_normalizer-3.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:707b82d19e65c9bd28b81dde95249b07bf9f5b90ebe1ef17d9b57473f8a64b7b", size = 102308 }, + { url = "https://files.pythonhosted.org/packages/54/2f/28659eee7f5d003e0f5a3b572765bf76d6e0fe6601ab1f1b1dd4cba7e4f1/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:980b4f289d1d90ca5efcf07958d3eb38ed9c0b7676bf2831a54d4f66f9c27dfa", size = 196326 }, + { url = "https://files.pythonhosted.org/packages/d1/18/92869d5c0057baa973a3ee2af71573be7b084b3c3d428fe6463ce71167f8/charset_normalizer-3.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f28f891ccd15c514a0981f3b9db9aa23d62fe1a99997512b0491d2ed323d229a", size = 125614 }, + { url = "https://files.pythonhosted.org/packages/d6/27/327904c5a54a7796bb9f36810ec4173d2df5d88b401d2b95ef53111d214e/charset_normalizer-3.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8aacce6e2e1edcb6ac625fb0f8c3a9570ccc7bfba1f63419b3769ccf6a00ed0", size = 120450 }, + { url = "https://files.pythonhosted.org/packages/a4/23/65af317914a0308495133b2d654cf67b11bbd6ca16637c4e8a38f80a5a69/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7af3717683bea4c87acd8c0d3d5b44d56120b26fd3f8a692bdd2d5260c620a", size = 140135 }, + { url = "https://files.pythonhosted.org/packages/f2/41/6190102ad521a8aa888519bb014a74251ac4586cde9b38e790901684f9ab/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ff2ed8194587faf56555927b3aa10e6fb69d931e33953943bc4f837dfee2242", size = 150413 }, + { url = "https://files.pythonhosted.org/packages/7b/ab/f47b0159a69eab9bd915591106859f49670c75f9a19082505ff16f50efc0/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e91f541a85298cf35433bf66f3fab2a4a2cff05c127eeca4af174f6d497f0d4b", size = 142992 }, + { url = "https://files.pythonhosted.org/packages/28/89/60f51ad71f63aaaa7e51a2a2ad37919985a341a1d267070f212cdf6c2d22/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309a7de0a0ff3040acaebb35ec45d18db4b28232f21998851cfa709eeff49d62", size = 144871 }, + { url = "https://files.pythonhosted.org/packages/0c/48/0050550275fea585a6e24460b42465020b53375017d8596c96be57bfabca/charset_normalizer-3.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:285e96d9d53422efc0d7a17c60e59f37fbf3dfa942073f666db4ac71e8d726d0", size = 146756 }, + { url = "https://files.pythonhosted.org/packages/dc/b5/47f8ee91455946f745e6c9ddbb0f8f50314d2416dd922b213e7d5551ad09/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:5d447056e2ca60382d460a604b6302d8db69476fd2015c81e7c35417cfabe4cd", size = 141034 }, + { url = "https://files.pythonhosted.org/packages/84/79/5c731059ebab43e80bf61fa51666b9b18167974b82004f18c76378ed31a3/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:20587d20f557fe189b7947d8e7ec5afa110ccf72a3128d61a2a387c3313f46be", size = 149434 }, + { url = "https://files.pythonhosted.org/packages/ca/f3/0719cd09fc4dc42066f239cb3c48ced17fc3316afca3e2a30a4756fe49ab/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:130272c698667a982a5d0e626851ceff662565379baf0ff2cc58067b81d4f11d", size = 152443 }, + { url = "https://files.pythonhosted.org/packages/f7/0e/c6357297f1157c8e8227ff337e93fd0a90e498e3d6ab96b2782204ecae48/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:ab22fbd9765e6954bc0bcff24c25ff71dcbfdb185fcdaca49e81bac68fe724d3", size = 150294 }, + { url = "https://files.pythonhosted.org/packages/54/9a/acfa96dc4ea8c928040b15822b59d0863d6e1757fba8bd7de3dc4f761c13/charset_normalizer-3.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7782afc9b6b42200f7362858f9e73b1f8316afb276d316336c0ec3bd73312742", size = 145314 }, + { url = "https://files.pythonhosted.org/packages/73/1c/b10a63032eaebb8d7bcb8544f12f063f41f5f463778ac61da15d9985e8b6/charset_normalizer-3.4.0-cp39-cp39-win32.whl", hash = "sha256:2de62e8801ddfff069cd5c504ce3bc9672b23266597d4e4f50eda28846c322f2", size = 94724 }, + { url = "https://files.pythonhosted.org/packages/c5/77/3a78bf28bfaa0863f9cfef278dbeadf55efe064eafff8c7c424ae3c4c1bf/charset_normalizer-3.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:95c3c157765b031331dd4db3c775e58deaee050a3042fcad72cbc4189d7c8dca", size = 102159 }, + { url = "https://files.pythonhosted.org/packages/bf/9b/08c0432272d77b04803958a4598a51e2a4b51c06640af8b8f0f908c18bf2/charset_normalizer-3.4.0-py3-none-any.whl", hash = "sha256:fe9f97feb71aa9896b81973a7bbada8c49501dc73e58a10fcef6663af95e5079", size = 49446 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, +] + +[[package]] +name = "happy" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "pluggy" }, + { name = "requests" }, +] + +[package.dev-dependencies] +test = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "pluggy", specifier = "==1.5.0" }, + { name = "requests", specifier = "==2.32.3" }, +] + +[package.metadata.requires-dev] +test = [{ name = "pytest", specifier = "==8.3.4" }] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077 }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429 }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067 }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030 }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898 }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894 }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319 }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273 }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310 }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309 }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762 }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453 }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486 }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349 }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159 }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243 }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645 }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584 }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875 }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418 }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708 }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582 }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543 }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691 }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170 }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530 }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666 }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954 }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724 }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383 }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338 }, +] \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/python/uv/uv.go b/pkg/fanal/analyzer/language/python/uv/uv.go new file mode 100644 index 000000000000..353f63bb117c --- /dev/null +++ b/pkg/fanal/analyzer/language/python/uv/uv.go @@ -0,0 +1,78 @@ +package uv + +import ( + "context" + "io" + "io/fs" + "os" + "path/filepath" + + "golang.org/x/xerrors" + + uvparser "github.com/aquasecurity/trivy/pkg/dependency/parser/python/uv" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeUv, NewUvAnalyzer) +} + +const version = 1 + +type uvAnalyzer struct { + logger *log.Logger + lockParser language.Parser +} + +func NewUvAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &uvAnalyzer{ + logger: log.WithPrefix("uv"), + lockParser: uvparser.NewParser(), + }, nil +} + +func (a *uvAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + required := func(_ string, _ fs.DirEntry) bool { + // Parse all required files: `uv.lock` (from a.Required func) + input.FilePatterns.Match() + return true + } + + err := fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error { + // Parse uv.lock + app, err := language.Parse(ctx, types.Uv, path, r, a.lockParser) + if err != nil { + a.logger.Warn("Failed to parse uv lockfile", log.Err(err)) + return nil + } else if app == nil { + return nil + } + + apps = append(apps, *app) + + return nil + }) + if err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a *uvAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Base(filePath) == types.UvLock +} + +func (a *uvAnalyzer) Type() analyzer.Type { + return analyzer.TypeUv +} + +func (a *uvAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/python/uv/uv_test.go b/pkg/fanal/analyzer/language/python/uv/uv_test.go new file mode 100644 index 000000000000..fbff569160e7 --- /dev/null +++ b/pkg/fanal/analyzer/language/python/uv/uv_test.go @@ -0,0 +1,170 @@ +package uv_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/uv" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_uvAnalyzer_PostAnalyze(t *testing.T) { + tests := []struct { + dir string + want *analyzer.AnalysisResult + }{ + // docker run --name uv --rm -it python@sha256:e1141f10176d74d1a0e87a7c0a0a5a98dd98ec5ac12ce867768f40c6feae2fd9 sh + // wget -qO- https://github.com/astral-sh/uv/releases/download/0.5.8/uv-installer.sh | sh + // source $HOME/.local/bin/env + // uv init happy && cd happy + // uv add pluggy==1.5.0 requests==2.32.3 + // uv add --group test pytest==8.3.4 + { + dir: "testdata/happy", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Uv, + FilePath: "uv.lock", + Packages: types.Packages{ + { + ID: "happy@0.1.0", + Name: "happy", + Version: "0.1.0", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "pluggy@1.5.0", + "pytest@8.3.4", + "requests@2.32.3", + }, + }, + { + ID: "pluggy@1.5.0", + Name: "pluggy", + Version: "1.5.0", + Relationship: types.RelationshipDirect, + }, + { + ID: "pytest@8.3.4", + Name: "pytest", + Version: "8.3.4", + Relationship: types.RelationshipDirect, + Dev: true, + DependsOn: []string{ + "colorama@0.4.6", + "exceptiongroup@1.2.2", + "iniconfig@2.0.0", + "packaging@24.2", + "pluggy@1.5.0", + "tomli@2.2.1", + }, + }, + { + ID: "requests@2.32.3", + Name: "requests", + Version: "2.32.3", + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "certifi@2024.12.14", + "charset-normalizer@3.4.0", + "idna@3.10", + "urllib3@2.2.3", + }, + }, + { + ID: "certifi@2024.12.14", + Name: "certifi", + Version: "2024.12.14", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "charset-normalizer@3.4.0", + Name: "charset-normalizer", + Version: "3.4.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "colorama@0.4.6", + Name: "colorama", + Version: "0.4.6", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "exceptiongroup@1.2.2", + Name: "exceptiongroup", + Version: "1.2.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "idna@3.10", + Name: "idna", + Version: "3.10", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + { + ID: "iniconfig@2.0.0", + Name: "iniconfig", + Version: "2.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "packaging@24.2", + Name: "packaging", + Version: "24.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "tomli@2.2.1", + Name: "tomli", + Version: "2.2.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + Dev: true, + }, + { + ID: "urllib3@2.2.3", + Name: "urllib3", + Version: "2.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + }, + }, + }, + }, + }, + }, + { + dir: "testdata/broken-lock", + want: &analyzer.AnalysisResult{}, + }, + } + + for _, tt := range tests { + t.Run(tt.dir, func(t *testing.T) { + a, err := uv.NewUvAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: os.DirFS(tt.dir), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/ruby/bundler/bundler.go b/pkg/fanal/analyzer/language/ruby/bundler/bundler.go new file mode 100644 index 000000000000..a312c934cc13 --- /dev/null +++ b/pkg/fanal/analyzer/language/ruby/bundler/bundler.go @@ -0,0 +1,43 @@ +package bundler + +import ( + "context" + "os" + "path/filepath" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/ruby/bundler" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&bundlerLibraryAnalyzer{}) +} + +const version = 1 + +type bundlerLibraryAnalyzer struct{} + +func (a bundlerLibraryAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + res, err := language.Analyze(ctx, types.Bundler, input.FilePath, input.Content, bundler.NewParser()) + if err != nil { + return nil, xerrors.Errorf("unable to parse Gemfile.lock: %w", err) + } + return res, nil +} + +func (a bundlerLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + return fileName == types.GemfileLock +} + +func (a bundlerLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeBundler +} + +func (a bundlerLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/ruby/gemspec/gemspec.go b/pkg/fanal/analyzer/language/ruby/gemspec/gemspec.go new file mode 100644 index 000000000000..c4e407b9a32b --- /dev/null +++ b/pkg/fanal/analyzer/language/ruby/gemspec/gemspec.go @@ -0,0 +1,40 @@ +package gemspec + +import ( + "context" + "os" + "path/filepath" + "regexp" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/ruby/gemspec" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&gemspecLibraryAnalyzer{}) +} + +const version = 1 + +var fileRegex = regexp.MustCompile(`.*/specifications/.+\.gemspec`) + +type gemspecLibraryAnalyzer struct{} + +func (a gemspecLibraryAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + return language.AnalyzePackage(ctx, types.GemSpec, input.FilePath, input.Content, + gemspec.NewParser(), input.Options.FileChecksum) +} + +func (a gemspecLibraryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return fileRegex.MatchString(filepath.ToSlash(filePath)) +} + +func (a gemspecLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeGemSpec +} + +func (a gemspecLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/ruby/gemspec/gemspec_test.go b/pkg/fanal/analyzer/language/ruby/gemspec/gemspec_test.go new file mode 100644 index 000000000000..168e97ac8b66 --- /dev/null +++ b/pkg/fanal/analyzer/language/ruby/gemspec/gemspec_test.go @@ -0,0 +1,137 @@ +package gemspec + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_gemspecLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + includeChecksum bool + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/multiple_licenses.gemspec", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GemSpec, + FilePath: "testdata/multiple_licenses.gemspec", + Packages: types.Packages{ + { + Name: "test-unit", + Version: "3.3.7", + Licenses: []string{ + "Ruby", + "BSDL", + "PSFL", + }, + FilePath: "testdata/multiple_licenses.gemspec", + }, + }, + }, + }, + }, + }, + { + name: "happy path with checksum", + inputFile: "testdata/multiple_licenses.gemspec", + includeChecksum: true, + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GemSpec, + FilePath: "testdata/multiple_licenses.gemspec", + Packages: types.Packages{ + { + Name: "test-unit", + Version: "3.3.7", + Licenses: []string{ + "Ruby", + "BSDL", + "PSFL", + }, + FilePath: "testdata/multiple_licenses.gemspec", + Digest: "sha1:6ba7904180fad7e09f224cd3e4d449ea53401fb9", + }, + }, + }, + }, + }, + }, + { + name: "empty name", + inputFile: "testdata/empty_name.gemspec", + want: nil, + wantErr: "failed to parse", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := gemspecLibraryAnalyzer{} + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + Options: analyzer.AnalysisOptions{FileChecksum: tt.includeChecksum}, + }) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_gemspecLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "with default", + filePath: "usr/ank/specifications/default/ank.gemspec", + want: true, + }, + { + name: "without default", + filePath: "usr/ank/specifications/ank.gemspec", + want: true, + }, + { + name: "without dot", + filePath: "usr/ank/specifications/ankgemspec", + want: false, + }, + { + name: "source gemspec", + filePath: "/localtRepo/default/ank.gemspec", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := gemspecLibraryAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/ruby/gemspec/testdata/empty_name.gemspec b/pkg/fanal/analyzer/language/ruby/gemspec/testdata/empty_name.gemspec new file mode 100644 index 000000000000..c4b5921d736c --- /dev/null +++ b/pkg/fanal/analyzer/language/ruby/gemspec/testdata/empty_name.gemspec @@ -0,0 +1,41 @@ +# -*- encoding: utf-8 -*- +# stub: test-unit 3.3.7 ruby lib + +Gem::Specification.new do |s| + s.name = "".freeze + s.version = "3.3.7" + + s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= + s.metadata = { "source_code_uri" => "https://github.com/test-unit/test-unit" } if s.respond_to? :metadata= + s.require_paths = ["lib".freeze] + s.authors = ["Kouhei Sutou".freeze, "Haruka Yoshihara".freeze] + s.date = "2020-11-18" + s.description = "test-unit (Test::Unit) is unit testing framework for Ruby, based on xUnit\nprinciples. These were originally designed by Kent Beck, creator of extreme\nprogramming software development methodology, for Smalltalk's SUnit. It allows\nwriting tests, checking results and automated testing in Ruby.\n".freeze + s.email = ["kou@cozmixng.org".freeze, "yoshihara@clear-code.com".freeze] + s.homepage = "http://test-unit.github.io/".freeze + s.licenses = ["Ruby".freeze, "BSDL".freeze, "PSFL".freeze] + s.rubygems_version = "3.2.22".freeze + s.summary = "An xUnit family unit testing framework for Ruby.".freeze + + s.installed_by_version = "3.2.22" if s.respond_to? :installed_by_version + + if s.respond_to? :specification_version then + s.specification_version = 4 + end + + if s.respond_to? :add_runtime_dependency then + s.add_runtime_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + else + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + end +end diff --git a/pkg/fanal/analyzer/language/ruby/gemspec/testdata/multiple_licenses.gemspec b/pkg/fanal/analyzer/language/ruby/gemspec/testdata/multiple_licenses.gemspec new file mode 100644 index 000000000000..d8b8b7d090fa --- /dev/null +++ b/pkg/fanal/analyzer/language/ruby/gemspec/testdata/multiple_licenses.gemspec @@ -0,0 +1,41 @@ +# -*- encoding: utf-8 -*- +# stub: test-unit 3.3.7 ruby lib + +Gem::Specification.new do |s| + s.name = "test-unit".freeze + s.version = "3.3.7" + + s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= + s.metadata = { "source_code_uri" => "https://github.com/test-unit/test-unit" } if s.respond_to? :metadata= + s.require_paths = ["lib".freeze] + s.authors = ["Kouhei Sutou".freeze, "Haruka Yoshihara".freeze] + s.date = "2020-11-18" + s.description = "test-unit (Test::Unit) is unit testing framework for Ruby, based on xUnit\nprinciples. These were originally designed by Kent Beck, creator of extreme\nprogramming software development methodology, for Smalltalk's SUnit. It allows\nwriting tests, checking results and automated testing in Ruby.\n".freeze + s.email = ["kou@cozmixng.org".freeze, "yoshihara@clear-code.com".freeze] + s.homepage = "http://test-unit.github.io/".freeze + s.licenses = ["Ruby".freeze, "BSDL".freeze, "PSFL".freeze] + s.rubygems_version = "3.2.22".freeze + s.summary = "An xUnit family unit testing framework for Ruby.".freeze + + s.installed_by_version = "3.2.22" if s.respond_to? :installed_by_version + + if s.respond_to? :specification_version then + s.specification_version = 4 + end + + if s.respond_to? :add_runtime_dependency then + s.add_runtime_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + s.add_development_dependency(%q.freeze, [">= 0"]) + else + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + s.add_dependency(%q.freeze, [">= 0"]) + end +end diff --git a/pkg/fanal/analyzer/language/rust/binary/binary.go b/pkg/fanal/analyzer/language/rust/binary/binary.go new file mode 100644 index 000000000000..75e73905af90 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/binary.go @@ -0,0 +1,45 @@ +package binary + +import ( + "context" + "errors" + "os" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/rust/binary" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/utils" +) + +func init() { + analyzer.RegisterAnalyzer(&rustBinaryLibraryAnalyzer{}) +} + +const version = 1 + +type rustBinaryLibraryAnalyzer struct{} + +func (a rustBinaryLibraryAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + res, err := language.Analyze(ctx, types.RustBinary, input.FilePath, input.Content, binary.NewParser()) + if errors.Is(err, binary.ErrUnrecognizedExe) || errors.Is(err, binary.ErrNonRustBinary) { + return nil, nil + } else if err != nil { + return nil, xerrors.Errorf("rust binary parse error: %w", err) + } + return res, nil +} + +func (a rustBinaryLibraryAnalyzer) Required(_ string, fileInfo os.FileInfo) bool { + return utils.IsExecutable(fileInfo) +} + +func (a rustBinaryLibraryAnalyzer) Type() analyzer.Type { + return analyzer.TypeRustBinary +} + +func (a rustBinaryLibraryAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/rust/binary/binary_test.go b/pkg/fanal/analyzer/language/rust/binary/binary_test.go new file mode 100644 index 000000000000..d5d66c6dcea7 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/binary_test.go @@ -0,0 +1,108 @@ +package binary + +import ( + "os" + "runtime" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_rustBinaryLibraryAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + inputFile: "testdata/executable_rust", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.RustBinary, + FilePath: "testdata/executable_rust", + Packages: types.Packages{ + { + ID: "crate_with_features@0.1.0", + Name: "crate_with_features", + Version: "0.1.0", + Relationship: types.RelationshipRoot, + DependsOn: []string{"library_crate@0.1.0"}, + }, + { + ID: "library_crate@0.1.0", + Name: "library_crate", + Version: "0.1.0", + Relationship: types.RelationshipUnknown, + }, + }, + }, + }, + }, + }, + { + name: "not rust binary", + inputFile: "testdata/executable_bash", + }, + { + name: "broken elf", + inputFile: "testdata/broken_elf", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := rustBinaryLibraryAnalyzer{} + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_rustBinaryLibraryAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "executable file", + filePath: lo.Ternary(runtime.GOOS == "windows", "testdata/binary.exe", "testdata/0755"), + want: true, + }, + { + name: "file perm 0644", + filePath: "testdata/0644", + want: false, + }, + { + name: "symlink", + filePath: "testdata/symlink", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := rustBinaryLibraryAnalyzer{} + fileInfo, err := os.Lstat(tt.filePath) + require.NoError(t, err) + got := a.Required(tt.filePath, fileInfo) + assert.Equal(t, tt.want, got, fileInfo.Mode().Perm()) + }) + } +} diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/0644 b/pkg/fanal/analyzer/language/rust/binary/testdata/0644 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/0755 b/pkg/fanal/analyzer/language/rust/binary/testdata/0755 new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/binary.exe b/pkg/fanal/analyzer/language/rust/binary/testdata/binary.exe new file mode 100644 index 000000000000..f8e020bf1ebe --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/testdata/binary.exe @@ -0,0 +1 @@ +exe \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/broken_elf b/pkg/fanal/analyzer/language/rust/binary/testdata/broken_elf new file mode 100755 index 000000000000..1a736bf298d6 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/testdata/broken_elf @@ -0,0 +1 @@ +ELF diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/executable_bash b/pkg/fanal/analyzer/language/rust/binary/testdata/executable_bash new file mode 100755 index 000000000000..ca3b9f0d2cda --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/testdata/executable_bash @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "hello" diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/executable_rust b/pkg/fanal/analyzer/language/rust/binary/testdata/executable_rust new file mode 100755 index 000000000000..cadfee87b008 Binary files /dev/null and b/pkg/fanal/analyzer/language/rust/binary/testdata/executable_rust differ diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/foo b/pkg/fanal/analyzer/language/rust/binary/testdata/foo new file mode 100755 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/language/rust/binary/testdata/symlink b/pkg/fanal/analyzer/language/rust/binary/testdata/symlink new file mode 120000 index 000000000000..19102815663d --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/binary/testdata/symlink @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/rust/cargo/cargo.go b/pkg/fanal/analyzer/language/rust/cargo/cargo.go new file mode 100644 index 000000000000..d36e71ea4c14 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/cargo.go @@ -0,0 +1,393 @@ +package cargo + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + "maps" + "os" + "path" + "path/filepath" + "slices" + "sort" + "strconv" + + "github.com/BurntSushi/toml" + "github.com/mitchellh/hashstructure/v2" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/go-version/pkg/semver" + goversion "github.com/aquasecurity/go-version/pkg/version" + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/dependency/parser/rust/cargo" + "github.com/aquasecurity/trivy/pkg/detector/library/compare" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeCargo, newCargoAnalyzer) +} + +const version = 1 + +var requiredFiles = []string{ + types.CargoLock, + types.CargoToml, +} + +type cargoAnalyzer struct { + logger *log.Logger + lockParser language.Parser + comparer compare.GenericComparer +} + +func newCargoAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &cargoAnalyzer{ + logger: log.WithPrefix("cargo"), + lockParser: cargo.NewParser(), + comparer: compare.GenericComparer{}, + }, nil +} + +func (a cargoAnalyzer) PostAnalyze(ctx context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var apps []types.Application + + required := func(path string, _ fs.DirEntry) bool { + return filepath.Base(path) == types.CargoLock || input.FilePatterns.Match(path) + } + + err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, _ fs.DirEntry, r io.Reader) error { + // Parse Cargo.lock + app, err := a.parseCargoLock(ctx, filePath, r) + if err != nil { + return xerrors.Errorf("parse error: %w", err) + } else if app == nil { + return nil + } + + // Parse Cargo.toml alongside Cargo.lock to identify the direct dependencies + if err = a.removeDevDependencies(input.FS, path.Dir(filePath), app); err != nil { + a.logger.Warn("Unable to parse Cargo.toml to identify direct dependencies", + log.FilePath(path.Join(path.Dir(filePath), types.CargoToml)), log.Err(err)) + } + sort.Sort(app.Packages) + apps = append(apps, *app) + + return nil + }) + if err != nil { + return nil, xerrors.Errorf("cargo walk error: %w", err) + } + + return &analyzer.AnalysisResult{ + Applications: apps, + }, nil +} + +func (a cargoAnalyzer) Required(filePath string, _ os.FileInfo) bool { + fileName := filepath.Base(filePath) + return slices.Contains(requiredFiles, fileName) +} + +func (a cargoAnalyzer) Type() analyzer.Type { + return analyzer.TypeCargo +} + +func (a cargoAnalyzer) Version() int { + return version +} + +func (a cargoAnalyzer) parseCargoLock(ctx context.Context, filePath string, r io.Reader) (*types.Application, error) { + return language.Parse(ctx, types.Cargo, filePath, r, a.lockParser) +} + +func (a cargoAnalyzer) removeDevDependencies(fsys fs.FS, dir string, app *types.Application) error { + cargoTOMLPath := path.Join(dir, types.CargoToml) + root, workspaces, directDeps, err := a.parseRootCargoTOML(fsys, cargoTOMLPath) + if errors.Is(err, fs.ErrNotExist) { + a.logger.Debug("Cargo.toml not found", log.FilePath(cargoTOMLPath)) + return nil + } else if err != nil { + return xerrors.Errorf("unable to parse %s: %w", cargoTOMLPath, err) + } + + // Cargo.toml file can contain same packages with different versions. + // Save versions separately for version comparison by comparator + pkgIDs := lo.SliceToMap(app.Packages, func(pkg types.Package) (string, types.Package) { + return pkg.ID, pkg + }) + + // Identify direct dependencies + pkgs := make(map[string]types.Package) + for name, constraint := range directDeps { + for _, pkg := range app.Packages { + if pkg.Name != name { + continue + } + + if match, err := a.matchVersion(pkg.Version, constraint); err != nil { + a.logger.Warn("Unable to match Cargo version", log.String("package", pkg.ID), log.Err(err)) + continue + } else if match { + // Mark as a direct dependency + pkg.Indirect = false + pkg.Relationship = types.RelationshipDirect + pkgs[pkg.ID] = pkg + break + } + } + } + + // Walk indirect dependencies + // Since it starts from direct dependencies, devDependencies will not appear in this walk. + for _, pkg := range pkgs { + a.walkIndirectDependencies(pkg, pkgIDs, pkgs) + } + + // Identify root and workspace packages + for pkgID, pkg := range pkgIDs { + switch { + case pkgID == root: + pkg.Relationship = types.RelationshipRoot + case slices.Contains(workspaces, pkgID): + pkg.Relationship = types.RelationshipWorkspace + default: + continue + } + + // Root/workspace package may include dev dependencies in lock file, so we need to remove them. + pkg.DependsOn = lo.Filter(pkg.DependsOn, func(dep string, _ int) bool { + _, ok := pkgs[dep] + return ok + }) + pkgs[pkgID] = pkg + } + + // Cargo allows creating cargo.toml files without name and version. + // In this case, the lock file will not include this package. + // e.g. when root cargo.toml contains only workspaces. + // So we have to add it ourselves, and the ID in this case will be the hash of the toml file. + if _, ok := pkgs[root]; !ok { + pkgs[root] = types.Package{ + ID: root, + Relationship: types.RelationshipRoot, + DependsOn: workspaces, + } + } + + pkgSlice := lo.Values(pkgs) + sort.Sort(types.Packages(pkgSlice)) + + // Save only prod packages + app.Packages = pkgSlice + return nil +} + +type cargoToml struct { + Package Package `toml:"package"` + Dependencies Dependencies `toml:"dependencies"` + Target map[string]map[string]Dependencies `toml:"target"` + Workspace cargoTomlWorkspace `toml:"workspace"` +} + +type Package struct { + Name string `toml:"name"` + Version any `toml:"version"` +} + +type cargoTomlWorkspace struct { + Dependencies Dependencies `toml:"dependencies"` + Members []string `toml:"members"` + Package struct { + Version string `toml:"version"` + } `toml:"package"` +} + +type Dependencies map[string]any + +// parseRootCargoTOML parses top-level Cargo.toml and returns dependencies. +// It also parses workspace members and their dependencies. +func (a cargoAnalyzer) parseRootCargoTOML(fsys fs.FS, filePath string) (string, []string, map[string]string, error) { + rootPkg, dependencies, members, rootWorkspaceVersion, err := a.parseCargoTOML(fsys, filePath, "") + if err != nil { + return "", nil, nil, xerrors.Errorf("unable to parse %s: %w", filePath, err) + } + + // According to Cargo workspace RFC, workspaces can't be nested: + // https://github.com/nox/rust-rfcs/blob/master/text/1525-cargo-workspace.md#validating-a-workspace + var workspaces []string + for _, member := range members { + memberPath := path.Join(path.Dir(filePath), member, types.CargoToml) + + // Cargo workspaces can be globs: + // https://github.com/rust-lang/cargo/pull/3979 + resolvedPaths, err := fs.Glob(fsys, memberPath) + if err != nil { + a.logger.Warn("Invalid glob pattern in Cargo.toml member paths", log.String("member_path", memberPath), log.Err(err)) + continue + } + + if len(resolvedPaths) == 0 { + a.logger.Warn("Unable to find matching Cargo.toml files", log.String("member_path", memberPath)) + continue + } + + for _, pkg := range resolvedPaths { + memberPkg, memberDeps, _, _, err := a.parseCargoTOML(fsys, pkg, rootWorkspaceVersion) + if err != nil { + a.logger.Warn("Unable to parse Cargo.toml", log.String("member_path", pkg), log.Err(err)) + continue + } + workspaces = append(workspaces, memberPkg) + + // Member dependencies shouldn't overwrite dependencies from root cargo.toml file + maps.Copy(memberDeps, dependencies) + dependencies = memberDeps + } + } + + deps := make(map[string]string) + for name, value := range dependencies { + switch ver := value.(type) { + case string: + // e.g. regex = "1.5" + deps[name] = ver + case map[string]any: + // e.g. serde = { version = "1.0", features = ["derive"] } + for k, v := range ver { + if k == "version" { + if vv, ok := v.(string); ok { + deps[name] = vv + } + break + } + } + } + } + + return rootPkg, workspaces, deps, nil +} + +func (a cargoAnalyzer) walkIndirectDependencies(pkg types.Package, pkgIDs, deps map[string]types.Package) { + for _, pkgID := range pkg.DependsOn { + if _, ok := deps[pkgID]; ok { + continue + } + + dep, ok := pkgIDs[pkgID] + if !ok { + continue + } + + dep.Indirect = true + dep.Relationship = types.RelationshipIndirect + deps[dep.ID] = dep + a.walkIndirectDependencies(dep, pkgIDs, deps) + } +} + +// cf. https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html +func (a cargoAnalyzer) matchVersion(currentVersion, constraint string) (bool, error) { + // `` == `^` - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#caret-requirements + // Add `^` for correct version comparison + // - 1.2.3 -> ^1.2.3 + // - 1.2.* -> 1.2.* + // - ^1.2 -> ^1.2 + if _, err := goversion.Parse(constraint); err == nil { + constraint = fmt.Sprintf("^%s", constraint) + } + + ver, err := semver.Parse(currentVersion) + if err != nil { + return false, xerrors.Errorf("version error (%s): %s", currentVersion, err) + } + + c, err := semver.NewConstraints(constraint) + if err != nil { + return false, xerrors.Errorf("constraint error (%s): %s", currentVersion, err) + } + + return c.Check(ver), nil +} + +func (a cargoAnalyzer) parseCargoTOML(fsys fs.FS, filePath, workspaceVersion string) (string, Dependencies, []string, string, error) { + // Parse Cargo.toml + f, err := fsys.Open(filePath) + if err != nil { + return "", nil, nil, "", xerrors.Errorf("file open error: %w", err) + } + defer func() { _ = f.Close() }() + + var tomlFile cargoToml + var pkgVersion string + // There are cases when toml file doesn't include `Dependencies` field (then map will be nil). + // e.g. when only `workspace.Dependencies` are used + // declare `dependencies` to avoid panic + dependencies := Dependencies{} + if _, err = toml.NewDecoder(f).Decode(&tomlFile); err != nil { + return "", nil, nil, "", xerrors.Errorf("toml decode error: %w", err) + } + + // https://rust-lang.github.io/rfcs/2906-cargo-workspace-deduplicate.html + if workspaceVersion == "" { + workspaceVersion = tomlFile.Workspace.Package.Version + } + + switch ver := tomlFile.Package.Version.(type) { + // In case of purely virtual cargo workspace version only lives in `workspace.package.version` + case nil: + pkgVersion = workspaceVersion + // We assume a proper version string was used, like: `0.1.0` + // Empty version is not allowed in Cargo.toml + // cf. https://github.com/aquasecurity/trivy/pull/10011#discussion_r2740743095 + case string: + pkgVersion = ver + // There are cases when `package.version` uses `version.workspace = true`, + // which must inherit the version from `workspace.version` or workspaceVersion (from root Cargo.toml) + case map[string]any: + if verWorkspace, found := ver["workspace"]; found { + if wv, ok := verWorkspace.(bool); ok && wv { + pkgVersion = workspaceVersion + } + } + } + + pkgID := a.packageID(tomlFile, pkgVersion) + + maps.Copy(dependencies, tomlFile.Dependencies) + + // https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies + for _, target := range tomlFile.Target { + maps.Copy(dependencies, target["dependencies"]) + } + + // https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace + maps.Copy(dependencies, tomlFile.Workspace.Dependencies) + // https://doc.rust-lang.org/cargo/reference/workspaces.html#the-members-and-exclude-fields + return pkgID, dependencies, tomlFile.Workspace.Members, workspaceVersion, nil +} + +// packageID builds PackageID by Package name and version. +// If name is empty - use hash of cargoToml. +func (a cargoAnalyzer) packageID(cargoToml cargoToml, pkgVersion string) string { + if cargoToml.Package.Name != "" { + return dependency.ID(types.Cargo, cargoToml.Package.Name, pkgVersion) + } + + hash, err := hashstructure.Hash(cargoToml, hashstructure.FormatV2, &hashstructure.HashOptions{ + ZeroNil: true, + IgnoreZeroValue: true, + }) + if err != nil { + a.logger.Warn("unable to hash package", log.String("package", cargoToml.Package.Name), log.Err(err)) + } + + return strconv.FormatUint(hash, 16) +} diff --git a/pkg/fanal/analyzer/language/rust/cargo/cargo_test.go b/pkg/fanal/analyzer/language/rust/cargo/cargo_test.go new file mode 100644 index 000000000000..e563c7b1a752 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/cargo_test.go @@ -0,0 +1,927 @@ +package cargo + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/detector/library/compare" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_cargoAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + file string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + file: "testdata/happy.txtar", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Cargo, + FilePath: "Cargo.lock", + Packages: types.Packages{ + { + ID: "app@0.1.0", + Name: "app", + Version: "0.1.0", + Relationship: types.RelationshipRoot, + Locations: []types.Location{ + { + StartLine: 13, + EndLine: 20, + }, + }, + DependsOn: []string{ + "memchr@1.0.2", + "regex-syntax@0.5.6", + "regex@1.7.3", + }, + }, + { + ID: "memchr@1.0.2", + Name: "memchr", + Version: "1.0.2", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 28, + EndLine: 35, + }, + }, + DependsOn: []string{"libc@0.2.140"}, + }, + { + ID: "regex@1.7.3", + Name: "regex", + Version: "1.7.3", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 43, + EndLine: 52, + }, + }, + DependsOn: []string{ + "aho-corasick@0.7.20", + "memchr@2.5.0", + "regex-syntax@0.6.29", + }, + }, + { + ID: "regex-syntax@0.5.6", + Name: "regex-syntax", + Version: "0.5.6", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 54, + EndLine: 61, + }, + }, + DependsOn: []string{"ucd-util@0.1.10"}, + }, + { + ID: "aho-corasick@0.7.20", + Name: "aho-corasick", + Version: "0.7.20", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 11, + }, + }, + DependsOn: []string{"memchr@2.5.0"}, + }, + { + ID: "libc@0.2.140", + Name: "libc", + Version: "0.2.140", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 26, + }, + }, + }, + { + ID: "memchr@2.5.0", + Name: "memchr", + Version: "2.5.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 37, + EndLine: 41, + }, + }, + }, + { + ID: "regex-syntax@0.6.29", + Name: "regex-syntax", + Version: "0.6.29", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 63, + EndLine: 67, + }, + }, + }, + { + ID: "ucd-util@0.1.10", + Name: "ucd-util", + Version: "0.1.10", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 69, + EndLine: 73, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "Cargo.toml doesn't include `Dependencies` field", + file: "testdata/toml-only-workspace-deps.txtar", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Cargo, + FilePath: "Cargo.lock", + Packages: types.Packages{ + { + ID: "app@0.1.0", + Name: "app", + Version: "0.1.0", + Relationship: types.RelationshipRoot, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 9, + }, + }, + DependsOn: []string{ + "memchr@2.5.0", + }, + }, + { + ID: "memchr@2.5.0", + Name: "memchr", + Version: "2.5.0", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 11, + EndLine: 15, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "no Cargo.toml", + file: "testdata/no-cargo-toml.txtar", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Cargo, + FilePath: "Cargo.lock", + Packages: types.Packages{ + { + ID: "aho-corasick@0.7.20", + Name: "aho-corasick", + Version: "0.7.20", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 11, + }, + }, + DependsOn: []string{"memchr@2.5.0"}, + }, + { + ID: "app@0.1.0", + Name: "app", + Version: "0.1.0", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 13, + EndLine: 20, + }, + }, + DependsOn: []string{ + "memchr@1.0.2", + "regex-syntax@0.5.6", + "regex@1.7.3", + }, + }, + { + ID: "libc@0.2.140", + Name: "libc", + Version: "0.2.140", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 26, + }, + }, + }, + { + ID: "memchr@1.0.2", + Name: "memchr", + Version: "1.0.2", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 28, + EndLine: 35, + }, + }, + DependsOn: []string{"libc@0.2.140"}, + }, + { + ID: "memchr@2.5.0", + Name: "memchr", + Version: "2.5.0", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 37, + EndLine: 41, + }, + }, + }, + { + ID: "regex@1.7.3", + Name: "regex", + Version: "1.7.3", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 43, + EndLine: 52, + }, + }, + DependsOn: []string{ + "aho-corasick@0.7.20", + "memchr@2.5.0", + "regex-syntax@0.6.29", + }, + }, + { + ID: "regex-syntax@0.5.6", + Name: "regex-syntax", + Version: "0.5.6", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 54, + EndLine: 61, + }, + }, + DependsOn: []string{"ucd-util@0.1.10"}, + }, + { + ID: "regex-syntax@0.6.29", + Name: "regex-syntax", + Version: "0.6.29", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 63, + EndLine: 67, + }, + }, + }, + { + ID: "ucd-util@0.1.10", + Name: "ucd-util", + Version: "0.1.10", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 69, + EndLine: 73, + }, + }, + }, + { + ID: "winapi@0.3.9", + Name: "winapi", + Version: "0.3.9", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 75, + EndLine: 83, + }, + }, + DependsOn: []string{ + "winapi-i686-pc-windows-gnu@0.4.0", + "winapi-x86_64-pc-windows-gnu@0.4.0", + }, + }, + { + ID: "winapi-i686-pc-windows-gnu@0.4.0", + Name: "winapi-i686-pc-windows-gnu", + Version: "0.4.0", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 85, + EndLine: 89, + }, + }, + }, + { + ID: "winapi-x86_64-pc-windows-gnu@0.4.0", + Name: "winapi-x86_64-pc-windows-gnu", + Version: "0.4.0", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 91, + EndLine: 95, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "wrong Cargo.toml", + file: "testdata/wrong-cargo-toml.txtar", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Cargo, + FilePath: "Cargo.lock", + Packages: types.Packages{ + { + ID: "app@0.1.0", + Name: "app", + Version: "0.1.0", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + DependsOn: []string{"memchr@2.5.0"}, + }, + { + ID: "memchr@2.5.0", + Name: "memchr", + Version: "2.5.0", + Indirect: false, + Relationship: types.RelationshipUnknown, + Locations: []types.Location{ + { + StartLine: 12, + EndLine: 16, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "broken Cargo.lock", + file: "testdata/sad.txtar", + want: &analyzer.AnalysisResult{}, + }, + { + name: "version.workspace = true inherits from workspace.package.version", + file: "testdata/version-workspace-inherit.txtar", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Cargo, + FilePath: "Cargo.lock", + Packages: types.Packages{ + { + ID: "afc012b7e68b6638", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "myapp@2.0.0", + }, + }, + { + ID: "myapp@2.0.0", + Name: "myapp", + Version: "2.0.0", + Relationship: types.RelationshipWorkspace, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 10, + }, + }, + DependsOn: []string{ + "serde@1.0.195", + }, + }, + { + ID: "serde@1.0.195", + Name: "serde", + Version: "1.0.195", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 12, + EndLine: 16, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "workspace members", + file: "testdata/toml-workspace-members.txtar", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Cargo, + FilePath: "Cargo.lock", + Packages: types.Packages{ + { + ID: "eeae9dc40f83d7dd", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "member@0.1.0", + "member2@0.1.0", + }, + }, + { + ID: "member@0.1.0", + Name: "member", + Version: "0.1.0", + Relationship: types.RelationshipWorkspace, + Locations: []types.Location{ + { + StartLine: 30, + EndLine: 35, + }, + }, + DependsOn: []string{ + "gdb-command@0.7.6", + }, + }, + { + ID: "member2@0.1.0", + Name: "member2", + Version: "0.1.0", + Relationship: types.RelationshipWorkspace, + Locations: []types.Location{ + { + StartLine: 37, + EndLine: 42, + }, + }, + DependsOn: []string{ + "regex@1.10.2", + }, + }, + { + ID: "gdb-command@0.7.6", + Name: "gdb-command", + Version: "0.7.6", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 14, + EndLine: 22, + }, + }, + DependsOn: []string{ + "regex@1.10.2", + "wait-timeout@0.2.0", + }, + }, + { + ID: "regex@1.10.2", + Name: "regex", + Version: "1.10.2", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 50, + EndLine: 60, + }, + }, + DependsOn: []string{ + "aho-corasick@1.1.2", + "memchr@2.6.4", + "regex-automata@0.4.3", + "regex-syntax@0.8.2", + }, + }, + { + ID: "aho-corasick@1.1.2", + Name: "aho-corasick", + Version: "1.1.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 12, + }, + }, + DependsOn: []string{"memchr@2.6.4"}, + }, + { + ID: "libc@0.2.150", + Name: "libc", + Version: "0.2.150", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 24, + EndLine: 28, + }, + }, + }, + { + ID: "memchr@2.6.4", + Name: "memchr", + Version: "2.6.4", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 44, + EndLine: 48, + }, + }, + }, + { + ID: "regex-automata@0.4.3", + Name: "regex-automata", + Version: "0.4.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 62, + EndLine: 71, + }, + }, + DependsOn: []string{ + "aho-corasick@1.1.2", + "memchr@2.6.4", + "regex-syntax@0.8.2", + }, + }, + { + ID: "regex-syntax@0.8.2", + Name: "regex-syntax", + Version: "0.8.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 73, + EndLine: 77, + }, + }, + }, + { + ID: "wait-timeout@0.2.0", + Name: "wait-timeout", + Version: "0.2.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 79, + EndLine: 86, + }, + }, + DependsOn: []string{"libc@0.2.150"}, + }, + }, + }, + }, + }, + }, + { + name: "workspace members", + file: "testdata/toml-workspace-glob.txtar", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Cargo, + FilePath: "Cargo.lock", + Packages: types.Packages{ + { + ID: "fbcbfc8e1114c435", + Relationship: types.RelationshipRoot, + DependsOn: []string{ + "member1@0.1.0", + "member2@0.1.0", + }, + }, + { + ID: "member1@0.1.0", + Name: "member1", + Version: "0.1.0", + Relationship: types.RelationshipWorkspace, + Locations: []types.Location{ + { + StartLine: 30, + EndLine: 35, + }, + }, + DependsOn: []string{ + "gdb-command@0.7.6", + }, + }, + { + ID: "member2@0.1.0", + Name: "member2", + Version: "0.1.0", + Relationship: types.RelationshipWorkspace, + Locations: []types.Location{ + { + StartLine: 37, + EndLine: 42, + }, + }, + DependsOn: []string{ + "regex@1.10.2", + }, + }, + { + ID: "gdb-command@0.7.6", + Name: "gdb-command", + Version: "0.7.6", + Indirect: false, + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 14, + EndLine: 22, + }, + }, + DependsOn: []string{ + "regex@1.10.2", + "wait-timeout@0.2.0", + }, + }, + { + ID: "regex@1.10.2", + Name: "regex", + Version: "1.10.2", + Relationship: types.RelationshipDirect, + Locations: []types.Location{ + { + StartLine: 50, + EndLine: 60, + }, + }, + DependsOn: []string{ + "aho-corasick@1.1.2", + "memchr@2.6.4", + "regex-automata@0.4.3", + "regex-syntax@0.8.2", + }, + }, + { + ID: "aho-corasick@1.1.2", + Name: "aho-corasick", + Version: "1.1.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 5, + EndLine: 12, + }, + }, + DependsOn: []string{"memchr@2.6.4"}, + }, + { + ID: "libc@0.2.150", + Name: "libc", + Version: "0.2.150", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 24, + EndLine: 28, + }, + }, + }, + { + ID: "memchr@2.6.4", + Name: "memchr", + Version: "2.6.4", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 44, + EndLine: 48, + }, + }, + }, + { + ID: "regex-automata@0.4.3", + Name: "regex-automata", + Version: "0.4.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 62, + EndLine: 71, + }, + }, + DependsOn: []string{ + "aho-corasick@1.1.2", + "memchr@2.6.4", + "regex-syntax@0.8.2", + }, + }, + { + ID: "regex-syntax@0.8.2", + Name: "regex-syntax", + Version: "0.8.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 73, + EndLine: 77, + }, + }, + }, + { + ID: "wait-timeout@0.2.0", + Name: "wait-timeout", + Version: "0.2.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + Locations: []types.Location{ + { + StartLine: 79, + EndLine: 86, + }, + }, + DependsOn: []string{"libc@0.2.150"}, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newCargoAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: testutil.TxtarToFS(t, tt.file), + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestMatchVersion(t *testing.T) { + tests := []struct { + name string + version string // version from Cargo.lock + constraint string // version from Cargo.toml + want bool + }{ + { + name: "major version == 0.", + version: "0.0.4", + constraint: "0.0.3", + want: false, + }, + { + name: "major version > 0.", + version: "1.2.4", + constraint: "1.2.3", + want: true, + }, + { + name: "Caret prefix", + version: "1.5.0", + constraint: "^1.2", + want: true, + }, + { + name: "Tilde prefix. Minor version", + version: "1.3.4", + constraint: "~ 1.2", + want: false, + }, + { + name: "Tilde prefix. Patch version", + version: "1.2.4", + constraint: "~ 1.2.3", + want: true, + }, + { + name: "Comparison prefix", + version: "2.5.0", + constraint: "< 2.5.0", + want: false, + }, + { + name: "Multiple prefixes", + version: "2.5.0", + constraint: ">= 2.5, < 2.5.1", + want: true, + }, + { + name: "= prefix", + version: "2.5.0", + constraint: "= 2.5", + want: true, + }, + { + name: "`*` constraint", + version: "2.5.0", + constraint: "*", + want: true, + }, + { + name: "constraint with `.*`", + version: "2.5.0", + constraint: "2.5.*", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := cargoAnalyzer{ + comparer: compare.GenericComparer{}, + } + match, _ := a.matchVersion(tt.version, tt.constraint) + assert.Equal(t, tt.want, match) + }) + } +} diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/happy.txtar b/pkg/fanal/analyzer/language/rust/cargo/testdata/happy.txtar new file mode 100644 index 000000000000..f8949b8cc13b --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/happy.txtar @@ -0,0 +1,128 @@ +Happy path test case for Cargo analyzer. + +This test verifies that the analyzer correctly parses a standard Rust project with: +- Direct dependencies (regex, memchr, regex-syntax) from [dependencies], [target] and [workspace.dependencies] +- Dev dependencies (winapi) +- Transitive dependencies (aho-corasick, libc, ucd-util, etc.) + +Expected behavior: +- Root package "app" should have RelationshipRoot +- Direct dependencies should have RelationshipDirect +- Transitive dependencies should have RelationshipIndirect with Indirect=true +- All packages should have correct version, ID, and location information + +-- Cargo.lock -- +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc 0.2.140 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "aho-corasick 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.29 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "ucd-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +-- Cargo.toml -- +[package] +name = "app" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +regex = "=1.7.3" + +[target.'cfg(not(target_os = "windows"))'.dependencies] +memchr = { version = "1.*", optional = true } + +[workspace.dependencies] +regex-syntax = { version = "<0.6"} + +[dev-dependencies] +winapi = "*" diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/no-cargo-toml.txtar b/pkg/fanal/analyzer/language/rust/cargo/testdata/no-cargo-toml.txtar new file mode 100644 index 000000000000..00482a382c57 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/no-cargo-toml.txtar @@ -0,0 +1,106 @@ +Test case: Cargo.lock without Cargo.toml. + +This test verifies that the analyzer works when only Cargo.lock is present +(e.g., scanning a container image where Cargo.toml was not included). + +Expected behavior: +- All packages should have RelationshipUnknown (cannot determine direct vs indirect) +- Indirect field should be false for all packages +- Package information should still be correctly parsed from Cargo.lock + +-- Cargo.lock -- +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "memchr 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.5.6 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "memchr" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "148fab2e51b4f1cfc66da2a7c32981d1d3c083a803978268bb11fe4b86925e7a" +dependencies = [ + "libc 0.2.140 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "regex" +version = "1.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +dependencies = [ + "aho-corasick 0.7.20 (registry+https://github.com/rust-lang/crates.io-index)", + "memchr 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.29 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "ucd-util" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abd2fc5d32b590614af8b0a20d837f32eca055edd0bbead59a9cfe80858be003" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/sad.txtar b/pkg/fanal/analyzer/language/rust/cargo/testdata/sad.txtar new file mode 100644 index 000000000000..d08c87e4530d --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/sad.txtar @@ -0,0 +1,11 @@ +Test case: Broken/malformed Cargo.lock file. + +This test verifies that the analyzer gracefully handles a corrupted Cargo.lock file. +The file contains only "[" which is invalid TOML. + +Expected behavior: +- Return empty AnalysisResult (no error, just empty result) +- Do not crash or panic + +-- Cargo.lock -- +[ diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/toml-only-workspace-deps.txtar b/pkg/fanal/analyzer/language/rust/cargo/testdata/toml-only-workspace-deps.txtar new file mode 100644 index 000000000000..3840dda1e8cb --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/toml-only-workspace-deps.txtar @@ -0,0 +1,36 @@ +Test case: Cargo.toml with only [workspace.dependencies] section (no [dependencies]). + +This test verifies that the analyzer handles projects where: +- Cargo.toml has [workspace.dependencies] but no [dependencies] section +- Dependencies are still resolved correctly from Cargo.lock + +Expected behavior: +- Root package "app" should have RelationshipRoot +- memchr should have RelationshipDirect (resolved from workspace.dependencies) + +-- Cargo.lock -- +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. + +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "memchr 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +-- Cargo.toml -- +[package] +name = "app" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[workspace.dependencies] +memchr = "2.5" diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/toml-workspace-glob.txtar b/pkg/fanal/analyzer/language/rust/cargo/testdata/toml-workspace-glob.txtar new file mode 100644 index 000000000000..e73fd915584a --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/toml-workspace-glob.txtar @@ -0,0 +1,130 @@ +Test case: Cargo workspace with glob pattern in members. + +This test verifies that the analyzer correctly handles Cargo workspaces where +members are specified using glob patterns (e.g., members = ["member*"]). + +Structure: +- Root Cargo.toml: defines members = ["member*"] (glob pattern) +- member1/Cargo.toml: depends on gdb-command +- member2/Cargo.toml: uses { workspace = true } for regex dependency + +Expected behavior: +- Same as explicit member list - glob should be expanded +- Virtual workspace root should have RelationshipRoot +- member1 and member2 should have RelationshipWorkspace +- Direct dependencies (gdb-command, regex) should have RelationshipDirect + +-- Cargo.lock -- +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "gdb-command" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a067c49eb3dfc7c2357d3d37536d2f57c5250ac377672164286b8d5ea94c5d" +dependencies = [ + "regex", + "wait-timeout", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "member1" +version = "0.1.0" +dependencies = [ + "gdb-command", +] + +[[package]] +name = "member2" +version = "0.1.0" +dependencies = [ + "regex", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] +-- Cargo.toml -- +[workspace.package] +name = "toml-workspace-members" +version = "0.1.0" + +[workspace] +resolver = "2" +members = ["member*"] + +[workspace.dependencies] +regex = "1" +-- member1/Cargo.toml -- +[package] +name = "member1" +version = "0.1.0" +edition = "2021" + +[dependencies] +gdb-command = "0.7" +-- member2/Cargo.toml -- +[package] +edition = "2021" +name = "member2" +version = "0.1.0" + +[dependencies] +regex = { workspace = true} diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/toml-workspace-members.txtar b/pkg/fanal/analyzer/language/rust/cargo/testdata/toml-workspace-members.txtar new file mode 100644 index 000000000000..38a65dccf573 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/toml-workspace-members.txtar @@ -0,0 +1,130 @@ +Test case: Cargo workspace with explicit member list. + +This test verifies that the analyzer correctly handles Cargo workspaces with +explicitly listed members in [workspace].members. + +Structure: +- Root Cargo.toml: defines members = ["member", "member2"] +- member/Cargo.toml: depends on gdb-command +- member2/Cargo.toml: uses { workspace = true } for regex dependency + +Expected behavior: +- Virtual workspace root should have RelationshipRoot +- member and member2 should have RelationshipWorkspace +- gdb-command and regex should have RelationshipDirect +- Transitive dependencies should have RelationshipIndirect + +-- Cargo.lock -- +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "gdb-command" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a067c49eb3dfc7c2357d3d37536d2f57c5250ac377672164286b8d5ea94c5d" +dependencies = [ + "regex", + "wait-timeout", +] + +[[package]] +name = "libc" +version = "0.2.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" + +[[package]] +name = "member" +version = "0.1.0" +dependencies = [ + "gdb-command", +] + +[[package]] +name = "member2" +version = "0.1.0" +dependencies = [ + "regex", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] +-- Cargo.toml -- +[workspace.package] +name = "toml-workspace-members" +version = "0.1.0" + +[workspace] +resolver = "2" +members = ["member", "member2"] + +[workspace.dependencies] +regex = "1" +-- member/Cargo.toml -- +[package] +name = "member" +version = "0.1.0" +edition = "2021" + +[dependencies] +gdb-command = "0.7" +-- member2/Cargo.toml -- +[package] +edition = "2021" +name = "member2" +version = "0.1.0" + +[dependencies] +regex = { workspace = true} diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit.txtar b/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit.txtar new file mode 100644 index 000000000000..f6ee27de16d4 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/version-workspace-inherit.txtar @@ -0,0 +1,45 @@ +Test case: Workspace with version.workspace = true inheritance. + +This test verifies that the analyzer correctly handles Cargo workspaces where +member packages inherit version from [workspace.package]. + +Structure: +- Root Cargo.toml: defines [workspace.package] with version = "2.0.0" +- app/Cargo.toml: member package using version.workspace = true + +Expected behavior: +- Virtual workspace root should have RelationshipRoot with no name/version +- myapp should have RelationshipWorkspace with version "2.0.0" (inherited) +- serde should have RelationshipDirect + +-- Cargo.lock -- +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "myapp" +version = "2.0.0" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.195" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02" +-- Cargo.toml -- +[workspace.package] +version = "2.0.0" + +[workspace] +members = ["app"] +-- app/Cargo.toml -- +[package] +name = "myapp" +version.workspace = true +edition = "2021" + +[dependencies] +serde = "1.0" diff --git a/pkg/fanal/analyzer/language/rust/cargo/testdata/wrong-cargo-toml.txtar b/pkg/fanal/analyzer/language/rust/cargo/testdata/wrong-cargo-toml.txtar new file mode 100644 index 000000000000..f8211c3471e1 --- /dev/null +++ b/pkg/fanal/analyzer/language/rust/cargo/testdata/wrong-cargo-toml.txtar @@ -0,0 +1,29 @@ +Test case: Malformed Cargo.toml with valid Cargo.lock. + +This test verifies that the analyzer handles a corrupted Cargo.toml file gracefully. +The Cargo.toml contains only "[" which is invalid TOML, but Cargo.lock is valid. + +Expected behavior: +- Fall back to parsing only Cargo.lock +- All packages should have RelationshipUnknown (cannot determine relationships) +- Package information should still be extracted from Cargo.lock + +-- Cargo.lock -- +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "app" +version = "0.1.0" +dependencies = [ + "memchr 2.5.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +-- Cargo.toml -- +[ diff --git a/pkg/fanal/analyzer/language/swift/cocoapods/cocoapods.go b/pkg/fanal/analyzer/language/swift/cocoapods/cocoapods.go new file mode 100644 index 000000000000..29f55e9254de --- /dev/null +++ b/pkg/fanal/analyzer/language/swift/cocoapods/cocoapods.go @@ -0,0 +1,45 @@ +package cocoapods + +import ( + "context" + "os" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/swift/cocoapods" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&cocoaPodsLockAnalyzer{}) +} + +const ( + version = 1 +) + +// cocoaPodsLockAnalyzer analyzes Podfile.lock +type cocoaPodsLockAnalyzer struct{} + +func (a cocoaPodsLockAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + p := cocoapods.NewParser() + res, err := language.Analyze(ctx, types.Cocoapods, input.FilePath, input.Content, p) + if err != nil { + return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err) + } + return res, nil +} + +func (a cocoaPodsLockAnalyzer) Required(_ string, fileInfo os.FileInfo) bool { + return fileInfo.Name() == types.CocoaPodsLock +} + +func (a cocoaPodsLockAnalyzer) Type() analyzer.Type { + return analyzer.TypeCocoaPods +} + +func (a cocoaPodsLockAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/swift/cocoapods/cocoapods_test.go b/pkg/fanal/analyzer/language/swift/cocoapods/cocoapods_test.go new file mode 100644 index 000000000000..4e890acd4ec4 --- /dev/null +++ b/pkg/fanal/analyzer/language/swift/cocoapods/cocoapods_test.go @@ -0,0 +1,98 @@ +package cocoapods + +import ( + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_cocoaPodsLockAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + inputFile: "testdata/happy.lock", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Cocoapods, + FilePath: "testdata/happy.lock", + Packages: types.Packages{ + { + ID: "AppCenter@4.2.0", + Name: "AppCenter", + Version: "4.2.0", + DependsOn: []string{ + "AppCenter/Analytics@4.2.0", + "AppCenter/Crashes@4.2.0", + }, + }, + { + ID: "AppCenter/Analytics@4.2.0", + Name: "AppCenter/Analytics", + Version: "4.2.0", + DependsOn: []string{ + "AppCenter/Core@4.2.0", + }, + }, + { + ID: "AppCenter/Core@4.2.0", + Name: "AppCenter/Core", + Version: "4.2.0", + }, + { + ID: "AppCenter/Crashes@4.2.0", + Name: "AppCenter/Crashes", + Version: "4.2.0", + DependsOn: []string{ + "AppCenter/Core@4.2.0", + }, + }, + { + ID: "KeychainAccess@4.2.1", + Name: "KeychainAccess", + Version: "4.2.1", + }, + }, + }, + }, + }, + }, + { + name: "empty file", + inputFile: "testdata/empty.lock", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := cocoaPodsLockAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + if got != nil { + for _, app := range got.Applications { + sort.Sort(app.Packages) + } + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/swift/cocoapods/testdata/empty.lock b/pkg/fanal/analyzer/language/swift/cocoapods/testdata/empty.lock new file mode 100644 index 000000000000..142864b62d23 --- /dev/null +++ b/pkg/fanal/analyzer/language/swift/cocoapods/testdata/empty.lock @@ -0,0 +1 @@ +COCOAPODS: 1.11.2 \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/swift/cocoapods/testdata/happy.lock b/pkg/fanal/analyzer/language/swift/cocoapods/testdata/happy.lock new file mode 100644 index 000000000000..65600c35476c --- /dev/null +++ b/pkg/fanal/analyzer/language/swift/cocoapods/testdata/happy.lock @@ -0,0 +1,12 @@ +PODS: + - AppCenter (4.2.0): + - AppCenter/Analytics (= 4.2.0) + - AppCenter/Crashes (= 4.2.0) + - AppCenter/Analytics (4.2.0): + - AppCenter/Core + - AppCenter/Core (4.2.0) + - AppCenter/Crashes (4.2.0): + - AppCenter/Core + - KeychainAccess (4.2.1) + +COCOAPODS: 1.11.2 \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/swift/swift/swift.go b/pkg/fanal/analyzer/language/swift/swift/swift.go new file mode 100644 index 000000000000..5ad3980e24e0 --- /dev/null +++ b/pkg/fanal/analyzer/language/swift/swift/swift.go @@ -0,0 +1,46 @@ +package swift + +import ( + "context" + "os" + "path" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency/parser/swift/swift" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&swiftLockAnalyzer{}) +} + +const ( + version = 1 +) + +// swiftLockAnalyzer analyzes Package.resolved files +type swiftLockAnalyzer struct{} + +func (a swiftLockAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + p := swift.NewParser() + res, err := language.Analyze(ctx, types.Swift, input.FilePath, input.Content, p) + if err != nil { + return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err) + } + return res, nil +} + +func (a swiftLockAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return path.Base(filePath) == types.SwiftResolved +} + +func (a swiftLockAnalyzer) Type() analyzer.Type { + return analyzer.TypeSwift +} + +func (a swiftLockAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/language/swift/swift/swift_test.go b/pkg/fanal/analyzer/language/swift/swift/swift_test.go new file mode 100644 index 000000000000..b4b3c5d408a7 --- /dev/null +++ b/pkg/fanal/analyzer/language/swift/swift/swift_test.go @@ -0,0 +1,89 @@ +package swift + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_swiftLockAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + }{ + { + name: "happy path", + inputFile: "testdata/happy/Package.resolved", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Swift, + FilePath: "testdata/happy/Package.resolved", + Packages: types.Packages{ + { + ID: "github.com/Quick/Nimble@9.2.1", + Name: "github.com/Quick/Nimble", + Version: "9.2.1", + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 12, + }, + }, + }, + { + ID: "github.com/Quick/Quick@7.0.0", + Name: "github.com/Quick/Quick", + Version: "7.0.0", + Locations: []types.Location{ + { + StartLine: 13, + EndLine: 21, + }, + }, + }, + { + ID: "github.com/ReactiveCocoa/ReactiveSwift@7.1.1", + Name: "github.com/ReactiveCocoa/ReactiveSwift", + Version: "7.1.1", + Locations: []types.Location{ + { + StartLine: 22, + EndLine: 30, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "empty file", + inputFile: "testdata/empty/Package.resolved", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := swiftLockAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.inputFile, + Content: f, + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/language/swift/swift/testdata/empty/Package.resolved b/pkg/fanal/analyzer/language/swift/swift/testdata/empty/Package.resolved new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/pkg/fanal/analyzer/language/swift/swift/testdata/empty/Package.resolved @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkg/fanal/analyzer/language/swift/swift/testdata/happy/Package.resolved b/pkg/fanal/analyzer/language/swift/swift/testdata/happy/Package.resolved new file mode 100644 index 000000000000..73b088823abc --- /dev/null +++ b/pkg/fanal/analyzer/language/swift/swift/testdata/happy/Package.resolved @@ -0,0 +1,34 @@ +{ + "object": { + "pins": [ + { + "package": "Nimble", + "repositoryURL": "https://github.com/Quick/Nimble.git", + "state": { + "branch": null, + "revision": "c93f16c25af5770f0d3e6af27c9634640946b068", + "version": "9.2.1" + } + }, + { + "package": "Quick", + "repositoryURL": "https://github.com/Quick/Quick.git", + "state": { + "branch": null, + "revision": "e206b8deba0d01fce70388a6d9dc66cba5603958", + "version": "7.0.0" + } + }, + { + "package": "ReactiveSwift", + "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift", + "state": { + "branch": null, + "revision": "40c465af19b993344e84355c00669ba2022ca3cd", + "version": "7.1.1" + } + } + ] + }, + "version": 1 +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/licensing/license.go b/pkg/fanal/analyzer/licensing/license.go new file mode 100644 index 000000000000..d9e9ccd0280c --- /dev/null +++ b/pkg/fanal/analyzer/licensing/license.go @@ -0,0 +1,158 @@ +package licensing + +import ( + "context" + "io" + "math" + "os" + "path/filepath" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +const version = 1 + +var ( + skipDirs = []string{ + "node_modules/", // node scan will pick these up + "usr/share/doc/", // dpkg will pick these up + + // Some heuristic exclusion + "usr/lib", + "usr/local/include", + "usr/include", + "usr/lib/python", + "usr/local/go", + "opt/yarn", + "usr/lib/gems", + "usr/src/wordpress", + } + + acceptedExtensions = set.NewCaseInsensitive( + ".asp", + ".aspx", + ".bas", + ".bat", + ".b", + ".c", + ".cue", + ".cgi", + ".cs", + ".css", + ".fish", + ".html", + ".h", + ".ini", + ".java", + ".js", + ".jsx", + ".markdown", + ".md", + ".py", + ".php", + ".pl", + ".r", + ".rb", + ".sh", + ".sql", + ".ts", + ".tsx", + ".txt", + ".vue", + ".zsh", + ) + + acceptedFileNames = set.NewCaseInsensitive( + "license", + "licence", + "copyright", + ) +) + +func init() { + analyzer.RegisterAnalyzer(newLicenseFileAnalyzer()) +} + +// licenseFileAnalyzer is an analyzer for file headers and license files +type licenseFileAnalyzer struct { + classifierConfidenceLevel float64 +} + +func newLicenseFileAnalyzer() *licenseFileAnalyzer { + return &licenseFileAnalyzer{} +} + +func (a *licenseFileAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + ctx = log.WithContextPrefix(ctx, "license") + log.DebugContext(ctx, "License scanning", log.FilePath(input.FilePath)) + + // need files to be text based, readable files + readable, err := isHumanReadable(input.Content, input.Info.Size()) + if err != nil || !readable { + return nil, nil + } + lf, err := licensing.Classify(input.FilePath, input.Content, a.classifierConfidenceLevel) + if err != nil { + return nil, xerrors.Errorf("license classification error: %w", err) + } else if len(lf.Findings) == 0 { + return nil, nil + } + + return &analyzer.AnalysisResult{ + Licenses: []types.LicenseFile{*lf}, + }, nil +} + +func (a *licenseFileAnalyzer) Init(opt analyzer.AnalyzerOptions) error { + a.classifierConfidenceLevel = opt.LicenseScannerOption.ClassifierConfidenceLevel + return nil +} + +func (a *licenseFileAnalyzer) Required(filePath string, _ os.FileInfo) bool { + for _, skipDir := range skipDirs { + if strings.Contains(filePath, skipDir) { + return false + } + } + if acceptedExtensions.Contains(filepath.Ext(filePath)) { + return true + } + + return acceptedFileNames.Contains(filepath.Base(filePath)) +} + +func isHumanReadable(content xio.ReadSeekerAt, fileSize int64) (bool, error) { + headSize := int(math.Min(float64(fileSize), 300)) + head := make([]byte, headSize) + if _, err := content.Read(head); err != nil { + return false, err + } + if _, err := content.Seek(0, io.SeekStart); err != nil { + return false, err + } + + // cf. https://github.com/file/file/blob/f2a6e7cb7db9b5fd86100403df6b2f830c7f22ba/src/encoding.c#L151-L228 + for _, b := range head { + if b < 7 || b == 11 || (13 < b && b < 27) || (27 < b && b < 0x20) || b == 0x7f { + return false, nil + } + } + + return true, nil +} + +func (a *licenseFileAnalyzer) Type() analyzer.Type { + return analyzer.TypeLicenseFile +} + +func (a *licenseFileAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/licensing/license_test.go b/pkg/fanal/analyzer/licensing/license_test.go new file mode 100644 index 000000000000..f0bbe7f0690f --- /dev/null +++ b/pkg/fanal/analyzer/licensing/license_test.go @@ -0,0 +1,97 @@ +package licensing + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_licenseAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + filePath string + want *analyzer.AnalysisResult + }{ + { + name: "Licensed C file", + filePath: "testdata/licensed.c", + want: &analyzer.AnalysisResult{ + Licenses: []types.LicenseFile{ + { + Type: types.LicenseTypeHeader, + FilePath: "testdata/licensed.c", + Findings: []types.LicenseFinding{ + { + Name: "AGPL-3.0", + Confidence: 1, + Link: "https://spdx.org/licenses/AGPL-3.0.html", + }, + }, + }, + }, + }, + }, + { + name: "Non human readable binary file", + filePath: "testdata/binaryfile", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.filePath) + require.NoError(t, err) + defer f.Close() + + fi, err := f.Stat() + require.NoError(t, err) + + a := newLicenseFileAnalyzer() + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.filePath, + Content: f, + Info: fi, + }) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } + +} + +func Test_licenseAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "C file with license", + filePath: "testdata/licensed.c", + want: true, + }, + { + name: "C file without license", + filePath: "testdata/unlicensed.c", + want: true, + }, + { + name: "Unreadable file", + filePath: "testdata/binaryfile", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := licenseFileAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/licensing/testdata/binaryfile b/pkg/fanal/analyzer/licensing/testdata/binaryfile new file mode 100644 index 000000000000..df93f5f3f724 Binary files /dev/null and b/pkg/fanal/analyzer/licensing/testdata/binaryfile differ diff --git a/pkg/fanal/analyzer/licensing/testdata/licensed.c b/pkg/fanal/analyzer/licensing/testdata/licensed.c new file mode 100644 index 000000000000..f66b373b7e68 --- /dev/null +++ b/pkg/fanal/analyzer/licensing/testdata/licensed.c @@ -0,0 +1,24 @@ +/* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +int func1() { + /* implementation */ + return 0; +} + +int func2() { + /* implementation */ + return 0; +} diff --git a/pkg/fanal/analyzer/os/alpine/alpine.go b/pkg/fanal/analyzer/os/alpine/alpine.go new file mode 100644 index 000000000000..f5451ecf2da1 --- /dev/null +++ b/pkg/fanal/analyzer/os/alpine/alpine.go @@ -0,0 +1,55 @@ +package alpine + +import ( + "bufio" + "context" + "os" + "slices" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&alpineOSAnalyzer{}) +} + +const version = 1 + +var requiredFiles = []string{"etc/alpine-release"} + +type alpineOSAnalyzer struct{} + +func (a alpineOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + scanner := bufio.NewScanner(input.Content) + for scanner.Scan() { + line := scanner.Text() + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Alpine, + Name: line, + }, + }, nil + } + return nil, xerrors.Errorf("alpine: %w", fos.AnalyzeOSError) +} + +func (a alpineOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredFiles, filePath) +} + +func (a alpineOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeAlpine +} + +func (a alpineOSAnalyzer) Version() int { + return version +} + +// StaticPaths returns the static paths of the alpine analyzer +func (a alpineOSAnalyzer) StaticPaths() []string { + return requiredFiles +} diff --git a/pkg/fanal/analyzer/os/alpine/alpine_test.go b/pkg/fanal/analyzer/os/alpine/alpine_test.go new file mode 100644 index 000000000000..40db4197359f --- /dev/null +++ b/pkg/fanal/analyzer/os/alpine/alpine_test.go @@ -0,0 +1,50 @@ +package alpine + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestAlpineReleaseOSAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + input analyzer.AnalysisInput + wantResult *analyzer.AnalysisResult + wantError string + }{ + { + name: "happy path", + input: analyzer.AnalysisInput{ + FilePath: "/etc/alpine-release", + Content: strings.NewReader("3.15.4"), + }, + wantResult: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Alpine, + Name: "3.15.4", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + a := alpineOSAnalyzer{} + res, err := a.Analyze(t.Context(), test.input) + + if test.wantError != "" { + require.Error(t, err) + assert.Equal(t, test.wantError, err.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, test.wantResult, res) + } + }) + } +} diff --git a/pkg/fanal/analyzer/os/amazonlinux/amazonlinux.go b/pkg/fanal/analyzer/os/amazonlinux/amazonlinux.go new file mode 100644 index 000000000000..7ac62c7118dc --- /dev/null +++ b/pkg/fanal/analyzer/os/amazonlinux/amazonlinux.go @@ -0,0 +1,80 @@ +package amazonlinux + +import ( + "bufio" + "context" + "io" + "os" + "slices" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&amazonlinuxOSAnalyzer{}) +} + +const version = 1 + +var requiredFiles = []string{ + "etc/system-release", // for 1 and 2 versions + "usr/lib/system-release", // for 2022, 2023 version +} + +type amazonlinuxOSAnalyzer struct{} + +func (a amazonlinuxOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + foundOS, err := a.parseRelease(input.Content) + if err != nil { + return nil, err + } + return &analyzer.AnalysisResult{ + OS: foundOS, + }, nil +} + +func (a amazonlinuxOSAnalyzer) parseRelease(r io.Reader) (types.OS, error) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + fields := strings.Fields(line) + // Only Amazon Linux Prefix + if strings.HasPrefix(line, "Amazon Linux release 2") { + if len(fields) < 5 { + continue + } + return types.OS{ + Family: types.Amazon, + Name: strings.Join(fields[3:], " "), + }, nil + } else if strings.HasPrefix(line, "Amazon Linux") { + return types.OS{ + Family: types.Amazon, + Name: strings.Join(fields[2:], " "), + }, nil + } + } + return types.OS{}, xerrors.Errorf("amazon: %w", fos.AnalyzeOSError) +} + +func (a amazonlinuxOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredFiles, filePath) +} + +func (a amazonlinuxOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeAmazon +} + +func (a amazonlinuxOSAnalyzer) Version() int { + return version +} + +// StaticPaths returns the static paths of the amazonlinux analyzer +func (a amazonlinuxOSAnalyzer) StaticPaths() []string { + return requiredFiles +} diff --git a/pkg/fanal/analyzer/os/amazonlinux/amazonlinux_test.go b/pkg/fanal/analyzer/os/amazonlinux/amazonlinux_test.go new file mode 100644 index 000000000000..e26212df826e --- /dev/null +++ b/pkg/fanal/analyzer/os/amazonlinux/amazonlinux_test.go @@ -0,0 +1,104 @@ +package amazonlinux + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_amazonlinuxOSAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + input analyzer.AnalysisInput + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path amazon linux 1", + input: analyzer.AnalysisInput{ + FilePath: "etc/system-release", + Content: strings.NewReader(`Amazon Linux AMI release 2018.03`), + }, + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Amazon, + Name: "AMI release 2018.03", + }, + }, + }, + { + name: "happy path amazon linux 2", + input: analyzer.AnalysisInput{ + FilePath: "etc/system-release", + Content: strings.NewReader(`Amazon Linux release 2 (Karoo)`), + }, + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Amazon, + Name: "2 (Karoo)", + }, + }, + }, + { + name: "happy path amazon linux 2022", + input: analyzer.AnalysisInput{ + FilePath: "usr/lib/system-release", + Content: strings.NewReader(`Amazon Linux release 2022 (Amazon Linux)`), + }, + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Amazon, + Name: "2022 (Amazon Linux)", + }, + }, + }, + { + name: "happy path amazon linux 2023", + input: analyzer.AnalysisInput{ + FilePath: "usr/lib/system-release", + Content: strings.NewReader(`Amazon Linux release 2023 (Amazon Linux)`), + }, + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Amazon, + Name: "2023 (Amazon Linux)", + }, + }, + }, + { + name: "sad path amazon linux 2 without code name", + input: analyzer.AnalysisInput{ + FilePath: "etc/system-release", + Content: strings.NewReader(`Amazon Linux release 2`), + }, + wantErr: fos.AnalyzeOSError.Error(), + }, + { + name: "sad path", + input: analyzer.AnalysisInput{ + FilePath: "etc/system-release", + Content: strings.NewReader(`foo bar`), + }, + wantErr: fos.AnalyzeOSError.Error(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := amazonlinuxOSAnalyzer{} + ctx := t.Context() + got, err := a.Analyze(ctx, tt.input) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/os/const.go b/pkg/fanal/analyzer/os/const.go new file mode 100644 index 000000000000..82f908979b66 --- /dev/null +++ b/pkg/fanal/analyzer/os/const.go @@ -0,0 +1,5 @@ +package os + +import "golang.org/x/xerrors" + +var AnalyzeOSError = xerrors.New("unable to analyze OS information") diff --git a/pkg/fanal/analyzer/os/debian/debian.go b/pkg/fanal/analyzer/os/debian/debian.go new file mode 100644 index 000000000000..89fb0b9c16bc --- /dev/null +++ b/pkg/fanal/analyzer/os/debian/debian.go @@ -0,0 +1,55 @@ +package debian + +import ( + "bufio" + "context" + "os" + "slices" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&debianOSAnalyzer{}) +} + +const version = 1 + +var requiredFiles = []string{"etc/debian_version"} + +type debianOSAnalyzer struct{} + +func (a debianOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + scanner := bufio.NewScanner(input.Content) + for scanner.Scan() { + line := scanner.Text() + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Debian, + Name: line, + }, + }, nil + } + return nil, xerrors.Errorf("debian: %w", fos.AnalyzeOSError) +} + +func (a debianOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredFiles, filePath) +} + +func (a debianOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeDebian +} + +func (a debianOSAnalyzer) Version() int { + return version +} + +// StaticPaths returns the static paths of the debian analyzer +func (a debianOSAnalyzer) StaticPaths() []string { + return requiredFiles +} diff --git a/pkg/fanal/analyzer/os/debian/debian_test.go b/pkg/fanal/analyzer/os/debian/debian_test.go new file mode 100644 index 000000000000..f9137f28e45f --- /dev/null +++ b/pkg/fanal/analyzer/os/debian/debian_test.go @@ -0,0 +1,68 @@ +package debian + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_debianOSAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path with debian 9", + inputFile: "testdata/debian_9/etc/debian_version", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Debian, + Name: "9.8", + }, + }, + }, + { + name: "happy path with debian sid", + inputFile: "testdata/debian_sid/etc/debian_version", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Debian, + Name: "buster/sid", + }, + }, + }, + { + name: "sad path with empty file", + inputFile: "testdata/empty", + wantErr: "debian: unable to analyze OS information", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := debianOSAnalyzer{} + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + ctx := t.Context() + + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: "etc/debian_version", + Content: f, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/os/debian/testdata/debian_9/etc/debian_version b/pkg/fanal/analyzer/os/debian/testdata/debian_9/etc/debian_version new file mode 100644 index 000000000000..021debdfd4a0 --- /dev/null +++ b/pkg/fanal/analyzer/os/debian/testdata/debian_9/etc/debian_version @@ -0,0 +1 @@ +9.8 diff --git a/pkg/fanal/analyzer/os/debian/testdata/debian_sid/etc/debian_version b/pkg/fanal/analyzer/os/debian/testdata/debian_sid/etc/debian_version new file mode 100644 index 000000000000..2834e5822e25 --- /dev/null +++ b/pkg/fanal/analyzer/os/debian/testdata/debian_sid/etc/debian_version @@ -0,0 +1 @@ +buster/sid diff --git a/pkg/fanal/analyzer/os/debian/testdata/empty b/pkg/fanal/analyzer/os/debian/testdata/empty new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/os/redhatbase/alma.go b/pkg/fanal/analyzer/os/redhatbase/alma.go new file mode 100644 index 000000000000..7fa2548a50fd --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/alma.go @@ -0,0 +1,67 @@ +package redhatbase + +import ( + "bufio" + "context" + "os" + "slices" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const almaAnalyzerVersion = 1 + +func init() { + analyzer.RegisterAnalyzer(&almaOSAnalyzer{}) +} + +type almaOSAnalyzer struct{} + +func (a almaOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + scanner := bufio.NewScanner(input.Content) + for scanner.Scan() { + line := scanner.Text() + result := redhatRe.FindStringSubmatch(strings.TrimSpace(line)) + if len(result) != 3 { + return nil, xerrors.New("alma: invalid almalinux-release") + } + + switch strings.ToLower(result[1]) { + case "alma", "almalinux", "alma linux": + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Alma, + Name: result[2], + }, + }, nil + } + } + + return nil, xerrors.Errorf("alma: %w", fos.AnalyzeOSError) +} + +func (a almaOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(a.requiredFiles(), filePath) +} + +func (a almaOSAnalyzer) requiredFiles() []string { + return []string{"etc/almalinux-release"} +} + +func (a almaOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeAlma +} + +func (a almaOSAnalyzer) Version() int { + return almaAnalyzerVersion +} + +// StaticPaths returns the static paths of the alma analyzer +func (a almaOSAnalyzer) StaticPaths() []string { + return a.requiredFiles() +} diff --git a/pkg/fanal/analyzer/os/redhatbase/alma_test.go b/pkg/fanal/analyzer/os/redhatbase/alma_test.go new file mode 100644 index 000000000000..0ff4f428cfeb --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/alma_test.go @@ -0,0 +1,55 @@ +package redhatbase + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_almaOSAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/alma/almalinux-release", + want: &analyzer.AnalysisResult{ + OS: types.OS{Family: "alma", Name: "8.4"}, + }, + }, + { + name: "sad path", + inputFile: "testdata/not_redhatbase/empty", + wantErr: "alma: unable to analyze OS information", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := almaOSAnalyzer{} + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + ctx := t.Context() + + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: "etc/almalinux-release", + Content: f, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/os/redhatbase/centos.go b/pkg/fanal/analyzer/os/redhatbase/centos.go new file mode 100644 index 000000000000..52830c8b935e --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/centos.go @@ -0,0 +1,67 @@ +package redhatbase + +import ( + "bufio" + "context" + "os" + "slices" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const centosAnalyzerVersion = 1 + +func init() { + analyzer.RegisterAnalyzer(¢OSAnalyzer{}) +} + +type centOSAnalyzer struct{} + +func (a centOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + scanner := bufio.NewScanner(input.Content) + for scanner.Scan() { + line := scanner.Text() + result := redhatRe.FindStringSubmatch(strings.TrimSpace(line)) + if len(result) != 3 { + return nil, xerrors.New("centos: invalid centos-release") + } + + switch strings.ToLower(result[1]) { + case "centos", "centos linux": + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.CentOS, + Name: result[2], + }, + }, nil + } + } + + return nil, xerrors.Errorf("centos: %w", fos.AnalyzeOSError) +} + +func (a centOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(a.requiredFiles(), filePath) +} + +func (a centOSAnalyzer) requiredFiles() []string { + return []string{"etc/centos-release"} +} + +func (a centOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeCentOS +} + +func (a centOSAnalyzer) Version() int { + return centosAnalyzerVersion +} + +// StaticPaths returns the static paths of the centos analyzer +func (a centOSAnalyzer) StaticPaths() []string { + return a.requiredFiles() +} diff --git a/pkg/fanal/analyzer/os/redhatbase/centos_test.go b/pkg/fanal/analyzer/os/redhatbase/centos_test.go new file mode 100644 index 000000000000..f409740485cc --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/centos_test.go @@ -0,0 +1,54 @@ +package redhatbase + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_centosOSAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/centos/centos-release", + want: &analyzer.AnalysisResult{ + OS: types.OS{Family: "centos", Name: "7.6.1810"}, + }, + }, + { + name: "sad path", + inputFile: "testdata/not_redhatbase/empty", + wantErr: "centos: unable to analyze OS information", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := centOSAnalyzer{} + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + ctx := t.Context() + + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: "etc/centos-release", + Content: f, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/os/redhatbase/fedora.go b/pkg/fanal/analyzer/os/redhatbase/fedora.go new file mode 100644 index 000000000000..ef1031967653 --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/fedora.go @@ -0,0 +1,69 @@ +package redhatbase + +import ( + "bufio" + "context" + "os" + "slices" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const fedoraAnalyzerVersion = 1 + +func init() { + analyzer.RegisterAnalyzer(&fedoraOSAnalyzer{}) +} + +type fedoraOSAnalyzer struct{} + +func (a fedoraOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + scanner := bufio.NewScanner(input.Content) + for scanner.Scan() { + line := scanner.Text() + result := redhatRe.FindStringSubmatch(strings.TrimSpace(line)) + if len(result) != 3 { + return nil, xerrors.New("fedora: Invalid fedora-release") + } + + switch strings.ToLower(result[1]) { + case "fedora", "fedora linux": + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Fedora, + Name: result[2], + }, + }, nil + } + } + return nil, xerrors.Errorf("fedora: %w", fos.AnalyzeOSError) +} + +func (a fedoraOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(a.requiredFiles(), filePath) +} + +func (a fedoraOSAnalyzer) requiredFiles() []string { + return []string{ + "etc/fedora-release", + "usr/lib/fedora-release", + } +} + +func (a fedoraOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeFedora +} + +func (a fedoraOSAnalyzer) Version() int { + return fedoraAnalyzerVersion +} + +// StaticPaths returns the static paths of the fedora analyzer +func (a fedoraOSAnalyzer) StaticPaths() []string { + return a.requiredFiles() +} diff --git a/pkg/fanal/analyzer/os/redhatbase/fedora_test.go b/pkg/fanal/analyzer/os/redhatbase/fedora_test.go new file mode 100644 index 000000000000..0b84c66901a2 --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/fedora_test.go @@ -0,0 +1,54 @@ +package redhatbase + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_fedoraOSAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/fedora_29/fedora-release", + want: &analyzer.AnalysisResult{ + OS: types.OS{Family: "fedora", Name: "29"}, + }, + }, + { + name: "sad path", + inputFile: "testdata/not_redhatbase/empty", + wantErr: "fedora: unable to analyze OS information", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := fedoraOSAnalyzer{} + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: "etc/fedora-release", + Content: f, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/os/redhatbase/oracle.go b/pkg/fanal/analyzer/os/redhatbase/oracle.go new file mode 100644 index 000000000000..e1744c5a309e --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/oracle.go @@ -0,0 +1,63 @@ +package redhatbase + +import ( + "bufio" + "context" + "os" + "slices" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const oracleAnalyzerVersion = 1 + +func init() { + analyzer.RegisterAnalyzer(&oracleOSAnalyzer{}) +} + +type oracleOSAnalyzer struct{} + +func (a oracleOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + scanner := bufio.NewScanner(input.Content) + for scanner.Scan() { + line := scanner.Text() + result := redhatRe.FindStringSubmatch(strings.TrimSpace(line)) + if len(result) != 3 { + return nil, xerrors.New("oracle: invalid oracle-release") + } + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Oracle, + Name: result[2], + }, + }, nil + } + + return nil, xerrors.Errorf("oracle: %w", fos.AnalyzeOSError) +} + +func (a oracleOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(a.requiredFiles(), filePath) +} + +func (a oracleOSAnalyzer) requiredFiles() []string { + return []string{"etc/oracle-release"} +} + +func (a oracleOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeOracle +} + +func (a oracleOSAnalyzer) Version() int { + return oracleAnalyzerVersion +} + +// StaticPaths returns the static paths of the oracle analyzer +func (a oracleOSAnalyzer) StaticPaths() []string { + return a.requiredFiles() +} diff --git a/pkg/fanal/analyzer/os/redhatbase/oracle_test.go b/pkg/fanal/analyzer/os/redhatbase/oracle_test.go new file mode 100644 index 000000000000..a7e45132177a --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/oracle_test.go @@ -0,0 +1,54 @@ +package redhatbase + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_oracleOSAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/oracle_7/oracle-release", + want: &analyzer.AnalysisResult{ + OS: types.OS{Family: "oracle", Name: "7.6"}, + }, + }, + { + name: "sad path", + inputFile: "testdata/not_redhatbase/empty", + wantErr: "oracle: unable to analyze OS information", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := oracleOSAnalyzer{} + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: "etc/oracle-release", + Content: f, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/os/redhatbase/redhatbase.go b/pkg/fanal/analyzer/os/redhatbase/redhatbase.go new file mode 100644 index 000000000000..fb80a3c38831 --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/redhatbase.go @@ -0,0 +1,104 @@ +package redhatbase + +import ( + "bufio" + "context" + "io" + "os" + "regexp" + "slices" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const redhatAnalyzerVersion = 1 + +func init() { + analyzer.RegisterAnalyzer(&redhatOSAnalyzer{}) +} + +var redhatRe = regexp.MustCompile(`(.*) release (\d[\d\.]*)`) + +type redhatOSAnalyzer struct{} + +func (a redhatOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + foundOS, err := a.parseRelease(input.Content) + if err != nil { + return nil, err + } + return &analyzer.AnalysisResult{ + OS: foundOS, + }, nil + +} + +func (a redhatOSAnalyzer) parseRelease(r io.Reader) (types.OS, error) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + result := redhatRe.FindStringSubmatch(strings.TrimSpace(line)) + if len(result) != 3 { + return types.OS{}, xerrors.New("redhat: invalid redhat-release") + } + + switch strings.ToLower(result[1]) { + case "centos", "centos linux": + return types.OS{ + Family: types.CentOS, + Name: result[2], + }, nil + case "rocky", "rocky linux": + return types.OS{ + Family: types.Rocky, + Name: result[2], + }, nil + case "alma", "almalinux", "alma linux": + return types.OS{ + Family: types.Alma, + Name: result[2], + }, nil + case "oracle", "oracle linux", "oracle linux server": + return types.OS{ + Family: types.Oracle, + Name: result[2], + }, nil + case "fedora", "fedora linux": + return types.OS{ + Family: types.Fedora, + Name: result[2], + }, nil + default: + return types.OS{ + Family: types.RedHat, + Name: result[2], + }, nil + } + } + return types.OS{}, xerrors.Errorf("redhatbase: %w", fos.AnalyzeOSError) +} + +func (a redhatOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(a.requiredFiles(), filePath) +} + +func (a redhatOSAnalyzer) requiredFiles() []string { + return []string{"etc/redhat-release"} +} + +func (a redhatOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeRedHatBase +} + +func (a redhatOSAnalyzer) Version() int { + return redhatAnalyzerVersion +} + +// StaticPaths returns the static paths of the redhatbase analyzer +func (a redhatOSAnalyzer) StaticPaths() []string { + return a.requiredFiles() +} diff --git a/pkg/fanal/analyzer/os/redhatbase/redhatbase_test.go b/pkg/fanal/analyzer/os/redhatbase/redhatbase_test.go new file mode 100644 index 000000000000..589637a0c97b --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/redhatbase_test.go @@ -0,0 +1,54 @@ +package redhatbase + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_redhatOSAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/redhat_6/redhat-release", + want: &analyzer.AnalysisResult{ + OS: types.OS{Family: "redhat", Name: "6.2"}, + }, + }, + { + name: "sad path", + inputFile: "testdata/not_redhatbase/empty", + wantErr: "redhatbase: unable to analyze OS information", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := redhatOSAnalyzer{} + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: "etc/redhat-release", + Content: f, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/os/redhatbase/rocky.go b/pkg/fanal/analyzer/os/redhatbase/rocky.go new file mode 100644 index 000000000000..8e1360408d48 --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/rocky.go @@ -0,0 +1,67 @@ +package redhatbase + +import ( + "bufio" + "context" + "os" + "slices" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +const rockyAnalyzerVersion = 1 + +func init() { + analyzer.RegisterAnalyzer(&rockyOSAnalyzer{}) +} + +type rockyOSAnalyzer struct{} + +func (a rockyOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + scanner := bufio.NewScanner(input.Content) + for scanner.Scan() { + line := scanner.Text() + result := redhatRe.FindStringSubmatch(strings.TrimSpace(line)) + if len(result) != 3 { + return nil, xerrors.New("rocky: invalid rocky-release") + } + + switch strings.ToLower(result[1]) { + case "rocky", "rocky linux": + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Rocky, + Name: result[2], + }, + }, nil + } + } + + return nil, xerrors.Errorf("rocky: %w", fos.AnalyzeOSError) +} + +func (a rockyOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(a.requiredFiles(), filePath) +} + +func (a rockyOSAnalyzer) requiredFiles() []string { + return []string{"etc/rocky-release"} +} + +func (a rockyOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeRocky +} + +func (a rockyOSAnalyzer) Version() int { + return rockyAnalyzerVersion +} + +// StaticPaths returns the static paths of the rocky analyzer +func (a rockyOSAnalyzer) StaticPaths() []string { + return a.requiredFiles() +} diff --git a/pkg/fanal/analyzer/os/redhatbase/rocky_test.go b/pkg/fanal/analyzer/os/redhatbase/rocky_test.go new file mode 100644 index 000000000000..c512cd57cf0b --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/rocky_test.go @@ -0,0 +1,54 @@ +package redhatbase + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_rockyOSAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/rocky/rocky-release", + want: &analyzer.AnalysisResult{ + OS: types.OS{Family: "rocky", Name: "8.4"}, + }, + }, + { + name: "sad path", + inputFile: "testdata/not_redhatbase/empty", + wantErr: "rocky: unable to analyze OS information", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := rockyOSAnalyzer{} + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: "etc/rocky-release", + Content: f, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/os/redhatbase/testdata/alma/almalinux-release b/pkg/fanal/analyzer/os/redhatbase/testdata/alma/almalinux-release new file mode 100644 index 000000000000..078ddf097550 --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/testdata/alma/almalinux-release @@ -0,0 +1 @@ +AlmaLinux release 8.4 (Electric Cheetah) \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/redhatbase/testdata/centos/centos-release b/pkg/fanal/analyzer/os/redhatbase/testdata/centos/centos-release new file mode 100644 index 000000000000..aad37b610e3b --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/testdata/centos/centos-release @@ -0,0 +1 @@ +CentOS Linux release 7.6.1810 (Core) diff --git a/pkg/fanal/analyzer/os/redhatbase/testdata/fedora_29/fedora-release b/pkg/fanal/analyzer/os/redhatbase/testdata/fedora_29/fedora-release new file mode 100644 index 000000000000..b84fe96c8f4c --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/testdata/fedora_29/fedora-release @@ -0,0 +1 @@ +Fedora release 29 (Twenty Nine) diff --git a/pkg/fanal/analyzer/os/redhatbase/testdata/not_redhatbase/empty b/pkg/fanal/analyzer/os/redhatbase/testdata/not_redhatbase/empty new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/os/redhatbase/testdata/oracle_7/oracle-release b/pkg/fanal/analyzer/os/redhatbase/testdata/oracle_7/oracle-release new file mode 100644 index 000000000000..49ada11583a7 --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/testdata/oracle_7/oracle-release @@ -0,0 +1 @@ +Oracle Linux Server release 7.6 diff --git a/pkg/fanal/analyzer/os/redhatbase/testdata/redhat_6/redhat-release b/pkg/fanal/analyzer/os/redhatbase/testdata/redhat_6/redhat-release new file mode 100644 index 000000000000..574c7bd1ede5 --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/testdata/redhat_6/redhat-release @@ -0,0 +1 @@ +Red Hat Linux release 6.2 (Zoot) diff --git a/pkg/fanal/analyzer/os/redhatbase/testdata/rocky/rocky-release b/pkg/fanal/analyzer/os/redhatbase/testdata/rocky/rocky-release new file mode 100644 index 000000000000..e79c566541d6 --- /dev/null +++ b/pkg/fanal/analyzer/os/redhatbase/testdata/rocky/rocky-release @@ -0,0 +1 @@ +Rocky Linux release 8.4 (Green Obsidian) \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/release.go b/pkg/fanal/analyzer/os/release/release.go new file mode 100644 index 000000000000..30c572c88fbc --- /dev/null +++ b/pkg/fanal/analyzer/os/release/release.go @@ -0,0 +1,151 @@ +package release + +import ( + "bufio" + "context" + "os" + "slices" + "strings" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&osReleaseAnalyzer{}) +} + +const version = 1 + +var requiredFiles = []string{ + "etc/os-release", + "usr/lib/os-release", + "aarch64-bottlerocket-linux-gnu/sys-root/usr/lib/os-release", + "x86_64-bottlerocket-linux-gnu/sys-root/usr/lib/os-release", +} + +type osReleaseAnalyzer struct{} + +func (a osReleaseAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + var id, osName, versionID string + scanner := bufio.NewScanner(input.Content) + for scanner.Scan() { + line := scanner.Text() + + ss := strings.SplitN(line, "=", 2) + if len(ss) != 2 { + continue + } + key, value := strings.TrimSpace(ss[0]), strings.TrimSpace(ss[1]) + + switch key { + case "NAME": + osName = strings.Trim(value, `"'`) + case "ID": + id = strings.Trim(value, `"'`) + case "VERSION_ID": + versionID = strings.Trim(value, `"'`) + default: + continue + } + } + + // Determine OS family: ID is the primary source + family := idToOSFamily(id) + if family == "" || versionID == "" { + return nil, nil + } + // Refine OS family based on NAME field + family = refineOSFamily(family, osName) + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: family, + Name: versionID, + }, + }, nil + +} + +//nolint:gocyclo +func idToOSFamily(id string) types.OSType { + switch id { + case "activestate": + return types.ActiveState + case "rhel": + return types.RedHat + case "centos": + return types.CentOS + case "rocky": + return types.Rocky + case "almalinux": + return types.Alma + case "ol": + return types.Oracle + case "fedora": + return types.Fedora + case "alpine": + return types.Alpine + case "bottlerocket": + return types.Bottlerocket + case "opensuse-tumbleweed": + return types.OpenSUSETumbleweed + case "opensuse-leap", "opensuse": // opensuse for leap:42, opensuse-leap for leap:15 + return types.OpenSUSELeap + case "sles": + return types.SLES + // There are various rebrands of SLE Micro, there is also one brief (and reverted rebrand) + // for SLE Micro 6.0. which was called "SL Micro 6.0" until very short before release + // and there is a "SLE Micro for Rancher" rebrand, which is used by SUSEs K8S based offerings. + case "sle-micro", "sl-micro", "sle-micro-rancher": + return types.SLEMicro + case "photon": + return types.Photon + case "wolfi": + return types.Wolfi + case "chainguard": + return types.Chainguard + case "azurelinux": + return types.Azure + case "mariner": + return types.CBLMariner + case "echo": + return types.Echo + case "minimos": + return types.MinimOS + case "coreos": + return types.CoreOS + } + // This OS is not supported for this analyzer. + return "" +} + +// refineOSFamily refines the OS family based on the NAME field. +// This handles exceptional cases where the ID field doesn't provide enough specificity, +// and we need to refine the detection using the NAME field. +func refineOSFamily(family types.OSType, name string) types.OSType { + // CentOS: Distinguish between regular CentOS and CentOS Stream + // ID is "centos" for both, but NAME differs + if family == types.CentOS && name == "CentOS Stream" { + return types.CentOSStream + } + + // Return family unchanged for normal cases + return family +} + +func (a osReleaseAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredFiles, filePath) +} + +func (a osReleaseAnalyzer) Type() analyzer.Type { + return analyzer.TypeOSRelease +} + +func (a osReleaseAnalyzer) Version() int { + return version +} + +// StaticPaths returns the static paths of the os-release analyzer +func (a osReleaseAnalyzer) StaticPaths() []string { + return requiredFiles +} diff --git a/pkg/fanal/analyzer/os/release/release_test.go b/pkg/fanal/analyzer/os/release/release_test.go new file mode 100644 index 000000000000..9b31cb91b3eb --- /dev/null +++ b/pkg/fanal/analyzer/os/release/release_test.go @@ -0,0 +1,311 @@ +package release + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_osReleaseAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + input analyzer.AnalysisInput + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "ActiveState", + inputFile: "testdata/activestate", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.ActiveState, + Name: "1.0", + }, + }, + }, + { + name: "Fedora", + inputFile: "testdata/fedora", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Fedora, + Name: "42", + }, + }, + }, + { + name: "Red Hat Enterprise Linux", + inputFile: "testdata/rhel", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.RedHat, + Name: "9.4", + }, + }, + }, + { + name: "CentOS", + inputFile: "testdata/centos", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.CentOS, + Name: "7", + }, + }, + }, + { + name: "CentOS Stream", + inputFile: "testdata/centos-stream", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.CentOSStream, + Name: "8", + }, + }, + }, + { + name: "Rocky Linux", + inputFile: "testdata/rocky", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Rocky, + Name: "9.3", + }, + }, + }, + { + name: "AlmaLinux", + inputFile: "testdata/alma", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Alma, + Name: "9.4", + }, + }, + }, + { + name: "Oracle Linux", + inputFile: "testdata/oracle", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Oracle, + Name: "8.10", + }, + }, + }, + { + name: "alpine", + inputFile: "testdata/alpine", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Alpine, + Name: "3.15.4", + }, + }, + }, + { + name: "openSUSE-leap 15.2.1", + inputFile: "testdata/opensuseleap-15.2.1", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.OpenSUSELeap, + Name: "15.2.1", + }, + }, + }, + { + name: "openSUSE-leap 42.3", + inputFile: "testdata/opensuseleap-42.3", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.OpenSUSELeap, + Name: "42.3", + }, + }, + }, + { + name: "openSUSE-tumbleweed", + inputFile: "testdata/opensusetumbleweed", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.OpenSUSETumbleweed, + Name: "20220412", + }, + }, + }, + { + name: "SUSE Linux Enterprise Server", + inputFile: "testdata/sles", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.SLES, + Name: "15.3", + }, + }, + }, + { + name: "SUSE Linux Enterprise Micro", + inputFile: "testdata/slemicro", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.SLEMicro, + Name: "5.3", + }, + }, + }, + { + name: "SUSE Linux Enterprise Micro 6.0", + inputFile: "testdata/slemicro6.0", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.SLEMicro, + Name: "6.0", + }, + }, + }, + { + name: "SUSE Linux Enterprise Micro 5.4 for Rancher", + inputFile: "testdata/slemicro-rancher", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.SLEMicro, + Name: "5.4", + }, + }, + }, + { + name: "Photon OS", + inputFile: "testdata/photon", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Photon, + Name: "4.0", + }, + }, + }, + { + name: "Photon OS", + inputFile: "testdata/photon", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Photon, + Name: "4.0", + }, + }, + }, + { + name: "Azure Linux", + inputFile: "testdata/azurelinux-3.0", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Azure, + Name: "3.0", + }, + }, + }, + { + name: "Mariner 2.0", + inputFile: "testdata/mariner-2.0", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.CBLMariner, + Name: "2.0", + }, + }, + }, + { + name: "Mariner 1.0", + inputFile: "testdata/mariner-1.0", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.CBLMariner, + Name: "1.0", + }, + }, + }, + { + name: "Echo", + inputFile: "testdata/echo", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Echo, + Name: "1", + }, + }, + }, + { + name: "MinimOS", + inputFile: "testdata/minimos", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.MinimOS, + Name: "20241031", + }, + }, + }, + { + name: "Bottlerocket", + inputFile: "testdata/bottlerocket", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Bottlerocket, + Name: "1.34.0", + }, + }, + }, + { + name: "CoreOS", + inputFile: "testdata/coreos", + want: &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.CoreOS, + Name: "3.15.4", + }, + }, + }, + { + name: "Unknown OS", + inputFile: "testdata/unknown", + want: nil, + }, + { + name: "No 'ID' field", + inputFile: "testdata/no-id", + want: nil, + }, + { + name: "No 'VERSION_ID' field", + inputFile: "testdata/no-version", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + a := osReleaseAnalyzer{} + res, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: "etc/os-release", + Content: f, + }) + + if tt.wantErr != "" { + require.Error(t, err) + assert.Equal(t, tt.wantErr, err.Error()) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, res) + }) + } +} diff --git a/pkg/fanal/analyzer/os/release/testdata/activestate b/pkg/fanal/analyzer/os/release/testdata/activestate new file mode 100644 index 000000000000..7921b4aaa8c5 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/activestate @@ -0,0 +1,8 @@ +ID=activestate +ID_LIKE="buildroot busybox" +VERSION_ID="1.0" +IMAGE_ID="activestate-k3s" +IMAGE_VERSION="1.34.1" +RELEASE_TYPE=experiment +EXPERIMENT="Trivy integration" +ACTIVESTATE_SBOM_PATH=/opt/activestate/k3s.spdx.json diff --git a/pkg/fanal/analyzer/os/release/testdata/alma b/pkg/fanal/analyzer/os/release/testdata/alma new file mode 100644 index 000000000000..cfd715b8e5df --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/alma @@ -0,0 +1,19 @@ +NAME="AlmaLinux" +VERSION="9.4 (Seafoam Ocelot)" +ID="almalinux" +ID_LIKE="rhel centos fedora" +VERSION_ID="9.4" +PLATFORM_ID="platform:el9" +PRETTY_NAME="AlmaLinux 9.4 (Seafoam Ocelot)" +ANSI_COLOR="0;34" +LOGO="fedora-logo-icon" +CPE_NAME="cpe:/o:almalinux:almalinux:9::baseos" +HOME_URL="https://almalinux.org/" +DOCUMENTATION_URL="https://wiki.almalinux.org/" +BUG_REPORT_URL="https://bugs.almalinux.org/" + +ALMALINUX_MANTISBT_PROJECT="AlmaLinux-9" +ALMALINUX_MANTISBT_PROJECT_VERSION="9.4" +REDHAT_SUPPORT_PRODUCT="AlmaLinux" +REDHAT_SUPPORT_PRODUCT_VERSION="9.4" +SUPPORT_END=2032-06-01 \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/alpine b/pkg/fanal/analyzer/os/release/testdata/alpine new file mode 100644 index 000000000000..f53b6e704bb6 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/alpine @@ -0,0 +1,6 @@ +NAME="Alpine Linux" +ID=alpine +VERSION_ID=3.15.4 +PRETTY_NAME="Alpine Linux v3.15" +HOME_URL="https://alpinelinux.org/" +BUG_REPORT_URL="https://bugs.alpinelinux.org/" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/azurelinux-3.0 b/pkg/fanal/analyzer/os/release/testdata/azurelinux-3.0 new file mode 100644 index 000000000000..a033cb09377e --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/azurelinux-3.0 @@ -0,0 +1,9 @@ +NAME="Microsoft Azure Linux" +VERSION="3.0.20240624" +ID=azurelinux +VERSION_ID="3.0" +PRETTY_NAME="Microsoft Azure Linux 3.0" +ANSI_COLOR="1;34" +HOME_URL="https://aka.ms/azurelinux" +BUG_REPORT_URL="https://aka.ms/azurelinux" +SUPPORT_URL="https://aka.ms/azurelinux" diff --git a/pkg/fanal/analyzer/os/release/testdata/bottlerocket b/pkg/fanal/analyzer/os/release/testdata/bottlerocket new file mode 100644 index 000000000000..3bde7c6b6e3e --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/bottlerocket @@ -0,0 +1,11 @@ +NAME=Bottlerocket +ID=bottlerocket +VERSION="1.34.0 (aws-ecs-2)" +PRETTY_NAME="Bottlerocket OS 1.34.0 (aws-ecs-2)" +VARIANT_ID=aws-ecs-2 +VERSION_ID=1.34.0 +BUILD_ID=18d04e52 +HOME_URL="https://github.com/bottlerocket-os/bottlerocket" +SUPPORT_URL="https://github.com/bottlerocket-os/bottlerocket/discussions" +BUG_REPORT_URL="https://github.com/bottlerocket-os/bottlerocket/issues" +DOCUMENTATION_URL="https://bottlerocket.dev" diff --git a/pkg/fanal/analyzer/os/release/testdata/centos b/pkg/fanal/analyzer/os/release/testdata/centos new file mode 100644 index 000000000000..21d78261996f --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/centos @@ -0,0 +1,15 @@ +NAME="CentOS Linux" +VERSION="7 (Core)" +ID="centos" +ID_LIKE="rhel fedora" +VERSION_ID="7" +PRETTY_NAME="CentOS Linux 7 (Core)" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:centos:centos:7" +HOME_URL="https://www.centos.org/" +BUG_REPORT_URL="https://bugs.centos.org/" + +CENTOS_MANTISBT_PROJECT="CentOS-7" +CENTOS_MANTISBT_PROJECT_VERSION="7" +REDHAT_SUPPORT_PRODUCT="centos" +REDHAT_SUPPORT_PRODUCT_VERSION="7" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/centos-stream b/pkg/fanal/analyzer/os/release/testdata/centos-stream new file mode 100644 index 000000000000..131b4b5eba81 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/centos-stream @@ -0,0 +1,13 @@ +NAME="CentOS Stream" +VERSION="8" +ID="centos" +ID_LIKE="rhel fedora" +VERSION_ID="8" +PLATFORM_ID="platform:el8" +PRETTY_NAME="CentOS Stream 8" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:centos:centos:8" +HOME_URL="https://centos.org/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" +REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux 8" +REDHAT_SUPPORT_PRODUCT_VERSION="CentOS Stream" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/coreos b/pkg/fanal/analyzer/os/release/testdata/coreos new file mode 100644 index 000000000000..fc300e60fec4 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/coreos @@ -0,0 +1,2 @@ +ID=coreos +VERSION_ID=3.15.4 \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/echo b/pkg/fanal/analyzer/os/release/testdata/echo new file mode 100644 index 000000000000..9062610126f0 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/echo @@ -0,0 +1,2 @@ +ID=echo +VERSION_ID=1 \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/fedora b/pkg/fanal/analyzer/os/release/testdata/fedora new file mode 100644 index 000000000000..bf793b53a83a --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/fedora @@ -0,0 +1,23 @@ +NAME="Fedora Linux" +VERSION="42 (Container Image)" +RELEASE_TYPE=stable +ID=fedora +VERSION_ID=42 +VERSION_CODENAME="" +PLATFORM_ID="platform:f42" +PRETTY_NAME="Fedora Linux 42 (Container Image)" +ANSI_COLOR="0;38;2;60;110;180" +LOGO=fedora-logo-icon +CPE_NAME="cpe:/o:fedoraproject:fedora:42" +DEFAULT_HOSTNAME="fedora" +HOME_URL="https://fedoraproject.org/" +DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f42/" +SUPPORT_URL="https://ask.fedoraproject.org/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" +REDHAT_BUGZILLA_PRODUCT="Fedora" +REDHAT_BUGZILLA_PRODUCT_VERSION=42 +REDHAT_SUPPORT_PRODUCT="Fedora" +REDHAT_SUPPORT_PRODUCT_VERSION=42 +SUPPORT_END=2026-05-13 +VARIANT="Container Image" +VARIANT_ID=container \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/mariner-1.0 b/pkg/fanal/analyzer/os/release/testdata/mariner-1.0 new file mode 100644 index 000000000000..aef312e77294 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/mariner-1.0 @@ -0,0 +1,9 @@ +NAME="Common Base Linux Mariner" +VERSION="1.0.20230713" +ID=mariner +VERSION_ID="1.0" +PRETTY_NAME="CBL-Mariner/Linux" +ANSI_COLOR="1;34" +HOME_URL="https://aka.ms/cbl-mariner" +BUG_REPORT_URL="https://aka.ms/cbl-mariner" +SUPPORT_URL="https://aka.ms/cbl-mariner" diff --git a/pkg/fanal/analyzer/os/release/testdata/mariner-2.0 b/pkg/fanal/analyzer/os/release/testdata/mariner-2.0 new file mode 100644 index 000000000000..c8a70bc4464c --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/mariner-2.0 @@ -0,0 +1,9 @@ +NAME="Common Base Linux Mariner" +VERSION="2.0.20240123" +ID=mariner +VERSION_ID="2.0" +PRETTY_NAME="CBL-Mariner/Linux" +ANSI_COLOR="1;34" +HOME_URL="https://aka.ms/cbl-mariner" +BUG_REPORT_URL="https://aka.ms/cbl-mariner" +SUPPORT_URL="https://aka.ms/cbl-mariner" diff --git a/pkg/fanal/analyzer/os/release/testdata/minimos b/pkg/fanal/analyzer/os/release/testdata/minimos new file mode 100644 index 000000000000..d8c150fcaded --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/minimos @@ -0,0 +1,6 @@ +ID=minimos +NAME="MinimOS" +PRETTY_NAME="MinimOS" +VERSION_ID="20241031" +HOME_URL="https://minimus.io" +BUG_REPORT_URL="https://support.minimus.io" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/no-id b/pkg/fanal/analyzer/os/release/testdata/no-id new file mode 100644 index 000000000000..dc088d5994c9 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/no-id @@ -0,0 +1,5 @@ +NAME="Alpine Linux" +VERSION_ID=3.15.4 +PRETTY_NAME="Alpine Linux v3.15" +HOME_URL="https://alpinelinux.org/" +BUG_REPORT_URL="https://bugs.alpinelinux.org/" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/no-version b/pkg/fanal/analyzer/os/release/testdata/no-version new file mode 100644 index 000000000000..075d0208d18e --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/no-version @@ -0,0 +1,5 @@ +NAME="Alpine Linux" +ID=alpine +PRETTY_NAME="Alpine Linux v3.15" +HOME_URL="https://alpinelinux.org/" +BUG_REPORT_URL="https://bugs.alpinelinux.org/" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/opensuseleap-15.2.1 b/pkg/fanal/analyzer/os/release/testdata/opensuseleap-15.2.1 new file mode 100644 index 000000000000..cc1ee6bcb755 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/opensuseleap-15.2.1 @@ -0,0 +1,10 @@ +NAME="openSUSE Leap" +VERSION="15.2.1 Beta" +ID="opensuse-leap" +ID_LIKE="suse opensuse" +VERSION_ID="15.2.1" +PRETTY_NAME="openSUSE Leap 15.2.1 Beta" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:opensuse:leap:15.2.1" +BUG_REPORT_URL="https://bugs.opensuse.org" +HOME_URL="https://www.opensuse.org/" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/opensuseleap-42.3 b/pkg/fanal/analyzer/os/release/testdata/opensuseleap-42.3 new file mode 100644 index 000000000000..a449c2ce1415 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/opensuseleap-42.3 @@ -0,0 +1,10 @@ +NAME="openSUSE Leap" +VERSION="42.3" +ID=opensuse +ID_LIKE="suse" +VERSION_ID="42.3" +PRETTY_NAME="openSUSE Leap 42.3" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:opensuse:leap:42.3" +BUG_REPORT_URL="https://bugs.opensuse.org" +HOME_URL="https://www.opensuse.org/" diff --git a/pkg/fanal/analyzer/os/release/testdata/opensusetumbleweed b/pkg/fanal/analyzer/os/release/testdata/opensusetumbleweed new file mode 100644 index 000000000000..1929741767f0 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/opensusetumbleweed @@ -0,0 +1,12 @@ +NAME="openSUSE Tumbleweed" +# VERSION="20220412" +ID="opensuse-tumbleweed" +ID_LIKE="opensuse suse" +VERSION_ID="20220412" +PRETTY_NAME="openSUSE Tumbleweed" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:opensuse:tumbleweed:20220412" +BUG_REPORT_URL="https://bugs.opensuse.org" +HOME_URL="https://www.opensuse.org/" +DOCUMENTATION_URL="https://en.opensuse.org/Portal:Tumbleweed" +LOGO="distributor-logo-Tumbleweed" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/oracle b/pkg/fanal/analyzer/os/release/testdata/oracle new file mode 100644 index 000000000000..fdf08a4510bc --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/oracle @@ -0,0 +1,18 @@ +NAME="Oracle Linux Server" +VERSION="8.10" +ID="ol" +ID_LIKE="fedora" +VARIANT="Server" +VARIANT_ID="server" +VERSION_ID="8.10" +PLATFORM_ID="platform:el8" +PRETTY_NAME="Oracle Linux Server 8.10" +ANSI_COLOR="0;31" +CPE_NAME="cpe:/o:oracle:linux:8:10:server" +HOME_URL="https://linux.oracle.com/" +BUG_REPORT_URL="https://github.com/oracle/oracle-linux" + +ORACLE_BUGZILLA_PRODUCT="Oracle Linux 8" +ORACLE_BUGZILLA_PRODUCT_VERSION=8.10 +ORACLE_SUPPORT_PRODUCT="Oracle Linux" +ORACLE_SUPPORT_PRODUCT_VERSION=8.10 \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/photon b/pkg/fanal/analyzer/os/release/testdata/photon new file mode 100644 index 000000000000..abf0ba3fecea --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/photon @@ -0,0 +1,8 @@ +NAME="VMware Photon OS" +VERSION="4.0" +ID=photon +VERSION_ID=4.0 +PRETTY_NAME="VMware Photon OS/Linux" +ANSI_COLOR="1;34" +HOME_URL="https://vmware.github.io/photon/" +BUG_REPORT_URL="https://github.com/vmware/photon/issues" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/rhel b/pkg/fanal/analyzer/os/release/testdata/rhel new file mode 100644 index 000000000000..640db90d1617 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/rhel @@ -0,0 +1,18 @@ +NAME="Red Hat Enterprise Linux" +VERSION="9.4 (Plow)" +ID="rhel" +ID_LIKE="fedora" +VERSION_ID="9.4" +PLATFORM_ID="platform:el9" +PRETTY_NAME="Red Hat Enterprise Linux 9.4 (Plow)" +ANSI_COLOR="0;31" +LOGO="fedora-logo-icon" +CPE_NAME="cpe:/o:redhat:enterprise_linux:9::baseos" +HOME_URL="https://www.redhat.com/" +DOCUMENTATION_URL="https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/9" +BUG_REPORT_URL="https://issues.redhat.com/" + +REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 9" +REDHAT_BUGZILLA_PRODUCT_VERSION=9.4 +REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" +REDHAT_SUPPORT_PRODUCT_VERSION="9.4" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/rocky b/pkg/fanal/analyzer/os/release/testdata/rocky new file mode 100644 index 000000000000..597e6ea1fdfd --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/rocky @@ -0,0 +1,17 @@ +NAME="Rocky Linux" +VERSION="9.3 (Blue Onyx)" +ID="rocky" +ID_LIKE="rhel centos fedora" +VERSION_ID="9.3" +PLATFORM_ID="platform:el9" +PRETTY_NAME="Rocky Linux 9.3 (Blue Onyx)" +ANSI_COLOR="0;32" +LOGO="fedora-logo-icon" +CPE_NAME="cpe:/o:rocky:rocky:9::baseos" +HOME_URL="https://rockylinux.org/" +BUG_REPORT_URL="https://bugs.rockylinux.org/" +SUPPORT_END="2032-05-31" +ROCKY_SUPPORT_PRODUCT="Rocky-Linux-9" +ROCKY_SUPPORT_PRODUCT_VERSION="9.3" +REDHAT_SUPPORT_PRODUCT="Rocky Linux" +REDHAT_SUPPORT_PRODUCT_VERSION="9.3" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/slemicro b/pkg/fanal/analyzer/os/release/testdata/slemicro new file mode 100644 index 000000000000..3e3bdaa30de5 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/slemicro @@ -0,0 +1,8 @@ +NAME="SLE Micro" +VERSION="5.3" +VERSION_ID="5.3" +PRETTY_NAME="SUSE Linux Enterprise Micro 5.3" +ID="sle-micro" +ID_LIKE="suse" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:suse:sle-micro:5.3" diff --git a/pkg/fanal/analyzer/os/release/testdata/slemicro-rancher b/pkg/fanal/analyzer/os/release/testdata/slemicro-rancher new file mode 100644 index 000000000000..83ce34662c3d --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/slemicro-rancher @@ -0,0 +1,13 @@ +NAME="SLE Micro" +VERSION="5.4" +VERSION_ID="5.4" +PRETTY_NAME="SUSE Linux Enterprise Micro for Rancher 5.4" +ID="sle-micro-rancher" +ID_LIKE="suse" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:suse:sle-micro-rancher:5.4" +IMAGE_REPO="registry.suse.com/rancher/elemental-teal/5.4" +IMAGE_TAG="1.2.3-3.2.153" +IMAGE="registry.suse.com/rancher/elemental-teal/5.4:1.2.3-3.2.153" +TIMESTAMP=20240419051540 +GRUB_ENTRY_NAME="Elemental" diff --git a/pkg/fanal/analyzer/os/release/testdata/slemicro6.0 b/pkg/fanal/analyzer/os/release/testdata/slemicro6.0 new file mode 100644 index 000000000000..a18632b128a6 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/slemicro6.0 @@ -0,0 +1,10 @@ +NAME="SL-Micro" +VERSION="6.0" +VERSION_ID="6.0" +PRETTY_NAME="SUSE Linux Micro 6.0" +ID="sl-micro" +ID_LIKE="suse" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:suse:sl-micro:6.0" +HOME_URL="https://www.suse.com/products/micro/" +DOCUMENTATION_URL="https://documentation.suse.com/sl-micro/6.0/" diff --git a/pkg/fanal/analyzer/os/release/testdata/sles b/pkg/fanal/analyzer/os/release/testdata/sles new file mode 100644 index 000000000000..e41d21049bfa --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/sles @@ -0,0 +1,9 @@ +NAME="SLES" +VERSION="15-SP3" +VERSION_ID="15.3" +PRETTY_NAME="SUSE Linux Enterprise Server 15 SP3" +ID="sles" +ID_LIKE="suse" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:suse:sles:15:sp3" +DOCUMENTATION_URL="https://documentation.suse.com/" \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/release/testdata/unknown b/pkg/fanal/analyzer/os/release/testdata/unknown new file mode 100644 index 000000000000..20658f0975a5 --- /dev/null +++ b/pkg/fanal/analyzer/os/release/testdata/unknown @@ -0,0 +1,2 @@ +ID=unknown +VERSION_ID=4.3.2 \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/ubuntu/esm.go b/pkg/fanal/analyzer/os/ubuntu/esm.go new file mode 100644 index 000000000000..21961f2d2cea --- /dev/null +++ b/pkg/fanal/analyzer/os/ubuntu/esm.go @@ -0,0 +1,84 @@ +package ubuntu + +import ( + "context" + "encoding/json" + "os" + "slices" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&ubuntuESMAnalyzer{}) +} + +const ( + ESMAnalyzerVersion = 1 + esmConfFilePath = "var/lib/ubuntu-advantage/status.json" + esmServiceName = "esm-infra" + esmStatusEnabled = "enabled" +) + +var ESMRequiredFiles = []string{ + esmConfFilePath, +} + +type ubuntuESMAnalyzer struct{} + +func (a ubuntuESMAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + st := status{} + err := json.NewDecoder(input.Content).Decode(&st) + if err != nil { + return nil, xerrors.Errorf("ubuntu ESM analyze error: %w", err) + } + if esmEnabled(st) { + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Ubuntu, + Extended: true, + }, + }, nil + } + // if ESM is disabled - return nil to reduce the amount of logic in the OS.Merge function + return nil, nil +} + +func (a ubuntuESMAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(ESMRequiredFiles, filePath) +} + +func (a ubuntuESMAnalyzer) Type() analyzer.Type { + return analyzer.TypeUbuntuESM +} + +func (a ubuntuESMAnalyzer) Version() int { + return ESMAnalyzerVersion +} + +// StaticPaths returns the static paths of the ubuntu ESM analyzer +func (a ubuntuESMAnalyzer) StaticPaths() []string { + return ESMRequiredFiles +} + +// structs to parse ESM status +type status struct { + Services []service `json:"services"` +} + +type service struct { + Name string `json:"name"` + Status string `json:"status"` +} + +func esmEnabled(st status) bool { + for _, s := range st.Services { // Find ESM Service + if s.Name == esmServiceName && s.Status == esmStatusEnabled { + return true + } + } + return false +} diff --git a/pkg/fanal/analyzer/os/ubuntu/esm_test.go b/pkg/fanal/analyzer/os/ubuntu/esm_test.go new file mode 100644 index 000000000000..c169aecea24f --- /dev/null +++ b/pkg/fanal/analyzer/os/ubuntu/esm_test.go @@ -0,0 +1,89 @@ +package ubuntu + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_ubuntuESMAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + filePath string + testFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path. Parse status.json file(ESM enabled)", + filePath: "var/lib/ubuntu-advantage/status.json", + testFile: "testdata/esm_enabled_status.json", + want: &analyzer.AnalysisResult{ + OS: types.OS{Family: "ubuntu", Extended: true}, + }, + }, + { + name: "happy path. Parse status.json file(ESM disabled)", + filePath: "var/lib/ubuntu-advantage/status.json", + testFile: "testdata/esm_disabled_status.json", + want: nil, + }, + { + name: "sad path", + filePath: "var/lib/ubuntu-advantage/status.json", + testFile: "testdata/invalid", + wantErr: "ubuntu ESM analyze error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := ubuntuESMAnalyzer{} + f, err := os.Open(tt.testFile) + require.NoError(t, err) + defer f.Close() + + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: tt.filePath, + Content: f, + }) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_ubuntuESMAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path status.json", + filePath: "var/lib/ubuntu-advantage/status.json", + want: true, + }, + { + name: "sad path", + filePath: "etc/invalid", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := ubuntuESMAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/os/ubuntu/testdata/esm_disabled_status.json b/pkg/fanal/analyzer/os/ubuntu/testdata/esm_disabled_status.json new file mode 100644 index 000000000000..dcc5e54dd4df --- /dev/null +++ b/pkg/fanal/analyzer/os/ubuntu/testdata/esm_disabled_status.json @@ -0,0 +1 @@ +{"machine_id": "1", "version": "27.6~16.04.1", "notices": [["", "Operation in progress: ua attach"]], "account": {"external_account_ids": [], "name": "trivy", "id": "11", "created_at": "2002-06-11T05:31:56+00:00"}, "execution_details": "Operation in progress: ua attach (pid:7)", "services": [{"status_details": "CC EAL2 is not configured", "name": "cc-eal", "description_override": null, "status": "disabled", "entitled": "yes", "description": "Common Criteria EAL2 Provisioning Packages", "available": "yes"}, {"status_details": "Ubuntu Security Guide is not configured", "name": "cis", "description_override": null, "status": "disabled", "entitled": "yes", "description": "Security compliance and audit tools", "available": "yes"}, {"status_details": "UA Infra: ESM is active", "name": "esm-infra", "description_override": null, "status": "disabled", "entitled": "yes", "description": "UA Infra: Extended Security Maintenance (ESM)", "available": "yes"}, {"status_details": "Cannot install FIPS on a container.", "name": "fips", "description_override": null, "status": "n/a", "entitled": "yes", "description": "NIST-certified core packages", "available": "yes"}, {"status_details": "Cannot install FIPS Updates on a container.", "name": "fips-updates", "description_override": null, "status": "n/a", "entitled": "yes", "description": "NIST-certified core packages with priority security updates", "available": "yes"}, {"status_details": "Cannot install Livepatch on a container.", "name": "livepatch", "description_override": null, "status": "n/a", "entitled": "yes", "description": "Canonical Livepatch service", "available": "yes"}], "_doc": "Content provided in json response is currently considered Experimental and may change", "contract": {"tech_support_level": "n/a", "name": "trivy", "id": "11", "created_at": "2022-03-11T05:31:57+00:00", "products": ["free"]}, "config": {"ua_config": {"update_status_timer": 43200, "https_proxy": null, "http_proxy": null, "apt_http_proxy": null, "metering_timer": 14400, "update_messaging_timer": 21600, "apt_https_proxy": null}, "data_dir": "/var/lib/ubuntu-advantage", "log_file": "/var/log/ubuntu-advantage.log", "security_url": "https://ubuntu.com/security", "log_level": "debug", "timer_log_file": "/var/log/ubuntu-advantage-timer.log", "license_check_log_file": "/var/log/ubuntu-advantage-license-check.log", "contract_url": "https://contracts.canonical.com"}, "_schema_version": "0.1", "attached": true, "execution_status": "active", "effective": "2022-03-11T05:31:57+00:00", "config_path": "/etc/ubuntu-advantage/uaclient.conf", "origin": "free", "simulated": false, "expires": "9999-12-31T00:00:00+00:00"} \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/ubuntu/testdata/esm_enabled_status.json b/pkg/fanal/analyzer/os/ubuntu/testdata/esm_enabled_status.json new file mode 100644 index 000000000000..5d292414bdf4 --- /dev/null +++ b/pkg/fanal/analyzer/os/ubuntu/testdata/esm_enabled_status.json @@ -0,0 +1 @@ +{"machine_id": "1", "version": "27.6~16.04.1", "notices": [["", "Operation in progress: ua attach"]], "account": {"external_account_ids": [], "name": "trivy", "id": "11", "created_at": "2002-06-11T05:31:56+00:00"}, "execution_details": "Operation in progress: ua attach (pid:7)", "services": [{"status_details": "CC EAL2 is not configured", "name": "cc-eal", "description_override": null, "status": "disabled", "entitled": "yes", "description": "Common Criteria EAL2 Provisioning Packages", "available": "yes"}, {"status_details": "Ubuntu Security Guide is not configured", "name": "cis", "description_override": null, "status": "disabled", "entitled": "yes", "description": "Security compliance and audit tools", "available": "yes"}, {"status_details": "UA Infra: ESM is active", "name": "esm-infra", "description_override": null, "status": "enabled", "entitled": "yes", "description": "UA Infra: Extended Security Maintenance (ESM)", "available": "yes"}, {"status_details": "Cannot install FIPS on a container.", "name": "fips", "description_override": null, "status": "n/a", "entitled": "yes", "description": "NIST-certified core packages", "available": "yes"}, {"status_details": "Cannot install FIPS Updates on a container.", "name": "fips-updates", "description_override": null, "status": "n/a", "entitled": "yes", "description": "NIST-certified core packages with priority security updates", "available": "yes"}, {"status_details": "Cannot install Livepatch on a container.", "name": "livepatch", "description_override": null, "status": "n/a", "entitled": "yes", "description": "Canonical Livepatch service", "available": "yes"}], "_doc": "Content provided in json response is currently considered Experimental and may change", "contract": {"tech_support_level": "n/a", "name": "trivy", "id": "11", "created_at": "2022-03-11T05:31:57+00:00", "products": ["free"]}, "config": {"ua_config": {"update_status_timer": 43200, "https_proxy": null, "http_proxy": null, "apt_http_proxy": null, "metering_timer": 14400, "update_messaging_timer": 21600, "apt_https_proxy": null}, "data_dir": "/var/lib/ubuntu-advantage", "log_file": "/var/log/ubuntu-advantage.log", "security_url": "https://ubuntu.com/security", "log_level": "debug", "timer_log_file": "/var/log/ubuntu-advantage-timer.log", "license_check_log_file": "/var/log/ubuntu-advantage-license-check.log", "contract_url": "https://contracts.canonical.com"}, "_schema_version": "0.1", "attached": true, "execution_status": "active", "effective": "2022-03-11T05:31:57+00:00", "config_path": "/etc/ubuntu-advantage/uaclient.conf", "origin": "free", "simulated": false, "expires": "9999-12-31T00:00:00+00:00"} \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/ubuntu/testdata/invalid b/pkg/fanal/analyzer/os/ubuntu/testdata/invalid new file mode 100644 index 000000000000..e466dcbd8e8f --- /dev/null +++ b/pkg/fanal/analyzer/os/ubuntu/testdata/invalid @@ -0,0 +1 @@ +invalid \ No newline at end of file diff --git a/pkg/fanal/analyzer/os/ubuntu/testdata/lsb-release b/pkg/fanal/analyzer/os/ubuntu/testdata/lsb-release new file mode 100644 index 000000000000..2cb44ac6a582 --- /dev/null +++ b/pkg/fanal/analyzer/os/ubuntu/testdata/lsb-release @@ -0,0 +1,4 @@ +DISTRIB_ID=Ubuntu +DISTRIB_RELEASE=18.04 +DISTRIB_CODENAME=bionic +DISTRIB_DESCRIPTION="Ubuntu 18.04.2 LTS" diff --git a/pkg/fanal/analyzer/os/ubuntu/ubuntu.go b/pkg/fanal/analyzer/os/ubuntu/ubuntu.go new file mode 100644 index 000000000000..9cc9cbb84c0b --- /dev/null +++ b/pkg/fanal/analyzer/os/ubuntu/ubuntu.go @@ -0,0 +1,69 @@ +package ubuntu + +import ( + "bufio" + "context" + "os" + "slices" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + fos "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&ubuntuOSAnalyzer{}) +} + +const ( + version = 1 + ubuntuConfFilePath = "etc/lsb-release" +) + +var requiredFiles = []string{ + ubuntuConfFilePath, +} + +type ubuntuOSAnalyzer struct{} + +func (a ubuntuOSAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + isUbuntu := false + scanner := bufio.NewScanner(input.Content) + for scanner.Scan() { + line := scanner.Text() + if line == "DISTRIB_ID=Ubuntu" { + isUbuntu = true + continue + } + + if isUbuntu && strings.HasPrefix(line, "DISTRIB_RELEASE=") { + return &analyzer.AnalysisResult{ + OS: types.OS{ + Family: types.Ubuntu, + Name: strings.TrimSpace(line[16:]), + }, + }, nil + } + } + return nil, xerrors.Errorf("ubuntu: %w", fos.AnalyzeOSError) +} + +func (a ubuntuOSAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredFiles, filePath) +} + +func (a ubuntuOSAnalyzer) Type() analyzer.Type { + return analyzer.TypeUbuntu +} + +func (a ubuntuOSAnalyzer) Version() int { + return version +} + +// StaticPaths returns the static paths of the ubuntu analyzer +func (a ubuntuOSAnalyzer) StaticPaths() []string { + return requiredFiles +} diff --git a/pkg/fanal/analyzer/os/ubuntu/ubuntu_test.go b/pkg/fanal/analyzer/os/ubuntu/ubuntu_test.go new file mode 100644 index 000000000000..27a159b12c39 --- /dev/null +++ b/pkg/fanal/analyzer/os/ubuntu/ubuntu_test.go @@ -0,0 +1,80 @@ +package ubuntu + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_ubuntuOSAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + inputFile string + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/lsb-release", + want: &analyzer.AnalysisResult{ + OS: types.OS{Family: "ubuntu", Name: "18.04"}, + }, + }, + { + name: "sad path", + inputFile: "testdata/invalid", + wantErr: "ubuntu: unable to analyze OS information", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := ubuntuOSAnalyzer{} + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + ctx := t.Context() + got, err := a.Analyze(ctx, analyzer.AnalysisInput{ + FilePath: "etc/lsb-release", + Content: f, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_ubuntuOSAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path", + filePath: "etc/lsb-release", + want: true, + }, + { + name: "sad path", + filePath: "etc/invalid", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := ubuntuOSAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/pkg/apk/apk.go b/pkg/fanal/analyzer/pkg/apk/apk.go new file mode 100644 index 000000000000..547009eaf1f8 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/apk/apk.go @@ -0,0 +1,241 @@ +package apk + +import ( + "bufio" + "context" + "encoding/base64" + "encoding/hex" + "fmt" + "os" + "path" + "slices" + "sort" + "strings" + + apkVersion "github.com/knqyf263/go-apk-version" + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +func init() { + analyzer.RegisterAnalyzer(newAlpinePkgAnalyzer()) +} + +const analyzerVersion = 3 + +var requiredFiles = []string{"lib/apk/db/installed", "usr/lib/apk/db/installed"} + +type alpinePkgAnalyzer struct{} + +func newAlpinePkgAnalyzer() *alpinePkgAnalyzer { return &alpinePkgAnalyzer{} } + +func (a alpinePkgAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + ctx = log.WithContextPrefix(ctx, "apk") + scanner := bufio.NewScanner(input.Content) + parsedPkgs, installedFiles := a.parseApkInfo(ctx, scanner) + + return &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: input.FilePath, + Packages: parsedPkgs, + }, + }, + SystemInstalledFiles: installedFiles, + }, nil +} + +func (a alpinePkgAnalyzer) parseApkInfo(ctx context.Context, scanner *bufio.Scanner) ([]types.Package, []string) { + var ( + pkgs []types.Package + pkg types.Package + version string + dir string + installedFiles []string + provides = make(map[string]string) // for dependency graph + ) + + for scanner.Scan() { + line := scanner.Text() + + // check package if paragraph end + if len(line) < 2 { + if !pkg.Empty() { + pkgs = append(pkgs, pkg) + } + pkg = types.Package{} + continue + } + + // ref. https://wiki.alpinelinux.org/wiki/Apk_spec + switch line[:2] { + case "P:": + pkg.Name = line[2:] + case "V:": + version = line[2:] + if !apkVersion.Valid(version) { + log.WarnContext(ctx, "Invalid version found", + log.String("name", pkg.Name), log.String("version", version)) + continue + } + pkg.Version = version + case "o:": + origin := line[2:] + pkg.SrcName = origin + pkg.SrcVersion = version + case "L:": + pkg.Licenses = a.parseLicense(line) + case "F:": + dir = line[2:] + case "R:": + absPath := path.Join(dir, line[2:]) + pkg.InstalledFiles = append(pkg.InstalledFiles, absPath) + installedFiles = append(installedFiles, absPath) + case "p:": // provides (corresponds to provides in PKGINFO, concatenated by spaces into a single line) + a.parseProvides(line, pkg.ID, provides) + case "D:": // dependencies (corresponds to depend in PKGINFO, concatenated by spaces into a single line) + pkg.DependsOn = a.parseDependencies(line) + case "A:": + pkg.Arch = line[2:] + case "C:": + d := a.decodeChecksumLine(ctx, line) + if d != "" { + pkg.Digest = d + } + case "m:": + pkg.Maintainer = line[2:] + } + + if pkg.Name != "" && pkg.Version != "" { + pkg.ID = fmt.Sprintf("%s@%s", pkg.Name, pkg.Version) + + // Dependencies could be package names or provides, so package names are stored as provides here. + // e.g. D:scanelf so:libc.musl-x86_64.so.1 + provides[pkg.Name] = pkg.ID + } + } + // in case of last paragraph + if !pkg.Empty() { + pkgs = append(pkgs, pkg) + } + + pkgs = a.uniquePkgs(pkgs) + + // Replace dependencies with package IDs + a.consolidateDependencies(pkgs, provides) + + return pkgs, installedFiles +} + +func (a alpinePkgAnalyzer) trimRequirement(s string) string { + // Trim version requirements + // e.g. + // so:libssl.so.1.1=1.1 => so:libssl.so.1.1 + // musl>=1.2 => musl + if strings.ContainsAny(s, "<>=") { + s = s[:strings.IndexAny(s, "><=")] + } + return s +} + +func (a alpinePkgAnalyzer) parseLicense(line string) []string { + // Remove "L:" before split + return licensing.LaxSplitLicenses(line[2:]) +} + +func (a alpinePkgAnalyzer) parseProvides(line, pkgID string, provides map[string]string) { + for p := range strings.FieldsSeq(line[2:]) { + p = a.trimRequirement(p) + + // Assume name ("P:") and version ("V:") are defined before provides ("p:") + provides[p] = pkgID + } +} + +func (a alpinePkgAnalyzer) parseDependencies(line string) []string { + line = line[2:] // Remove "D:" + return lo.FilterMap(strings.Fields(line), func(d string, _ int) (string, bool) { + // e.g. D:!uclibc-utils scanelf musl=1.1.14-r10 so:libc.musl-x86_64.so.1 + if strings.HasPrefix(d, "!") { + return "", false + } + return a.trimRequirement(d), true + }) +} + +func (a alpinePkgAnalyzer) consolidateDependencies(pkgs []types.Package, provides map[string]string) { + for i := range pkgs { + // e.g. libc6 => libc6@2.31-13+deb11u4 + pkgs[i].DependsOn = lo.FilterMap(pkgs[i].DependsOn, func(d string, _ int) (string, bool) { + if pkgID, ok := provides[d]; ok { + return pkgID, true + } + return "", false + }) + sort.Strings(pkgs[i].DependsOn) + pkgs[i].DependsOn = slices.Compact(pkgs[i].DependsOn) + + if len(pkgs[i].DependsOn) == 0 { + pkgs[i].DependsOn = nil + } + } +} + +func (a alpinePkgAnalyzer) uniquePkgs(pkgs []types.Package) (uniqPkgs []types.Package) { + uniq := set.New[string]() + for _, pkg := range pkgs { + if uniq.Contains(pkg.Name) { + continue + } + uniqPkgs = append(uniqPkgs, pkg) + uniq.Append(pkg.Name) + } + return uniqPkgs +} + +func (a alpinePkgAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredFiles, filePath) +} + +func (a alpinePkgAnalyzer) Type() analyzer.Type { + return analyzer.TypeApk +} + +func (a alpinePkgAnalyzer) Version() int { + return analyzerVersion +} + +// StaticPaths returns a list of static file paths to analyze +func (a alpinePkgAnalyzer) StaticPaths() []string { + return requiredFiles +} + +// decodeChecksumLine decodes checksum line +func (a alpinePkgAnalyzer) decodeChecksumLine(ctx context.Context, line string) digest.Digest { + if len(line) < 2 { + log.DebugContext(ctx, "Unable to decode checksum line of apk package", log.String("line", line)) + return "" + } + // https://wiki.alpinelinux.org/wiki/Apk_spec#Package_Checksum_Field + // https://stackoverflow.com/a/71712569 + alg := digest.MD5 + d := line[2:] + if strings.HasPrefix(d, "Q1") { + alg = digest.SHA1 + d = d[2:] // remove `Q1` prefix + } + + decodedDigestString, err := base64.StdEncoding.DecodeString(d) + if err != nil { + log.DebugContext(ctx, "Unable to decode digest", log.Err(err)) + return "" + } + h := hex.EncodeToString(decodedDigestString) + return digest.NewDigestFromString(alg, h) +} diff --git a/pkg/fanal/analyzer/pkg/apk/apk_test.go b/pkg/fanal/analyzer/pkg/apk/apk_test.go new file mode 100644 index 000000000000..fa73142e4d44 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/apk/apk_test.go @@ -0,0 +1,503 @@ +package apk + +import ( + "bufio" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var pkgs = []types.Package{ + { + ID: "musl@1.1.14-r10", + Name: "musl", + Version: "1.1.14-r10", + SrcName: "musl", + SrcVersion: "1.1.14-r10", + Licenses: []string{"MIT"}, + Maintainer: "Timo Teräs ", + Arch: "x86_64", + Digest: "sha1:d68b402f35f57750f49156b0cb4e886a2ad35d2d", + InstalledFiles: []string{ + "lib/libc.musl-x86_64.so.1", + "lib/ld-musl-x86_64.so.1", + }, + }, + { + ID: "busybox@1.24.2-r9", + Name: "busybox", + Version: "1.24.2-r9", + SrcName: "busybox", + SrcVersion: "1.24.2-r9", + Licenses: []string{"GPL-2.0-only"}, + Maintainer: "Natanael Copa ", + DependsOn: []string{"musl@1.1.14-r10"}, + Arch: "x86_64", + Digest: "sha1:ca124719267cd0bedc2f4cb850a286ac13f0ad44", + InstalledFiles: []string{ + "bin/busybox", + "bin/sh", + "etc/securetty", + "etc/udhcpd.conf", + "etc/logrotate.d/acpid", + }, + }, + { + ID: "alpine-baselayout@3.0.3-r0", + Name: "alpine-baselayout", + Version: "3.0.3-r0", + SrcName: "alpine-baselayout", + SrcVersion: "3.0.3-r0", + Licenses: []string{"GPL-2.0-only"}, + Maintainer: "Natanael Copa ", + DependsOn: []string{ + "busybox@1.24.2-r9", + "musl@1.1.14-r10", + }, + Arch: "x86_64", + Digest: "sha1:a214896150411d72dd1fafdb32d1c6c4855cccfa", + InstalledFiles: []string{ + "etc/hosts", + "etc/sysctl.conf", + "etc/group", + "etc/protocols", + "etc/fstab", + "etc/mtab", + "etc/profile", + "etc/TZ", + "etc/shells", + "etc/motd", + "etc/inittab", + "etc/hostname", + "etc/modules", + "etc/services", + "etc/shadow", + "etc/passwd", + "etc/profile.d/color_prompt", + "etc/sysctl.d/00-alpine.conf", + "etc/modprobe.d/i386.conf", + "etc/modprobe.d/blacklist.conf", + "etc/modprobe.d/aliases.conf", + "etc/modprobe.d/kms.conf", + "etc/crontabs/root", + "sbin/mkmntdirs", + "var/spool/cron/crontabs", + }, + }, + { + ID: "alpine-keys@1.1-r0", + Name: "alpine-keys", + Version: "1.1-r0", + SrcName: "alpine-keys", + SrcVersion: "1.1-r0", + Licenses: []string{"GPL-2.0-or-later"}, + Maintainer: "Natanael Copa ", + Arch: "x86_64", + Digest: "sha1:4def7ffaee6aeba700c1d62570326f75cbb8fa25", + InstalledFiles: []string{ + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-4d07755e.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + }, + }, + { + ID: "zlib@1.2.8-r2", + Name: "zlib", + Version: "1.2.8-r2", + SrcName: "zlib", + SrcVersion: "1.2.8-r2", + Licenses: []string{"Zlib"}, + Maintainer: "Natanael Copa ", + DependsOn: []string{"musl@1.1.14-r10"}, + Arch: "x86_64", + Digest: "sha1:efd04d34d40aa8eb331480127364c27a8ba760ef", + InstalledFiles: []string{ + "lib/libz.so.1.2.8", + "lib/libz.so.1", + }, + }, + { + ID: "libcrypto1.0@1.0.2h-r1", + Name: "libcrypto1.0", + Version: "1.0.2h-r1", + SrcName: "openssl", + SrcVersion: "1.0.2h-r1", + Licenses: []string{"OpenSSL"}, + Maintainer: "Timo Teras ", + DependsOn: []string{ + "musl@1.1.14-r10", + "zlib@1.2.8-r2", + }, + Arch: "x86_64", + Digest: "sha1:65c860ff8f103b664f40ba849a3f5a51c69c8beb", + InstalledFiles: []string{ + "lib/libcrypto.so.1.0.0", + "usr/bin/c_rehash", + "usr/lib/libcrypto.so.1.0.0", + "usr/lib/engines/libubsec.so", + "usr/lib/engines/libatalla.so", + "usr/lib/engines/libcapi.so", + "usr/lib/engines/libgost.so", + "usr/lib/engines/libcswift.so", + "usr/lib/engines/libchil.so", + "usr/lib/engines/libgmp.so", + "usr/lib/engines/libnuron.so", + "usr/lib/engines/lib4758cca.so", + "usr/lib/engines/libsureware.so", + "usr/lib/engines/libpadlock.so", + "usr/lib/engines/libaep.so", + }, + }, + { + ID: "libssl1.0@1.0.2h-r1", + Name: "libssl1.0", + Version: "1.0.2h-r1", + SrcName: "openssl", + SrcVersion: "1.0.2h-r1", + Licenses: []string{"OpenSSL"}, + Maintainer: "Timo Teras ", + Digest: "sha1:7120f337e93b2b4c44e0f5f31a15b60dc678ca14", + DependsOn: []string{ + "libcrypto1.0@1.0.2h-r1", + "musl@1.1.14-r10", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "lib/libssl.so.1.0.0", + "usr/lib/libssl.so.1.0.0", + }, + }, + { + ID: "apk-tools@2.6.7-r0", + Name: "apk-tools", + Version: "2.6.7-r0", + SrcName: "apk-tools", + SrcVersion: "2.6.7-r0", + Licenses: []string{"GPL-2.0-only"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:0990c0acd62b4175818c3a4cc60ed11f14e23bd8", + DependsOn: []string{ + "libcrypto1.0@1.0.2h-r1", + "libssl1.0@1.0.2h-r1", + "musl@1.1.14-r10", + "zlib@1.2.8-r2", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "sbin/apk", + }, + }, + { + ID: "scanelf@1.1.6-r0", + Name: "scanelf", + Version: "1.1.6-r0", + SrcName: "pax-utils", + SrcVersion: "1.1.6-r0", + Licenses: []string{"GPL-2.0-only"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:f9bab817c5ad93e92a6218bc0f7596b657c02d90", + DependsOn: []string{"musl@1.1.14-r10"}, + Arch: "x86_64", + InstalledFiles: []string{ + "usr/bin/scanelf", + }, + }, + { + ID: "musl-utils@1.1.14-r10", + Name: "musl-utils", + Version: "1.1.14-r10", + SrcName: "musl", + SrcVersion: "1.1.14-r10", + Licenses: []string{ + "MIT", + "BSD-3-Clause", + "GPL-2.0-or-later", + }, + Maintainer: "Timo Teräs ", + Digest: "sha1:608aa1dd39eff7bc6615d3e5e33383750f8f5ecc", + DependsOn: []string{ + "musl@1.1.14-r10", + "scanelf@1.1.6-r0", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "sbin/ldconfig", + "usr/bin/iconv", + "usr/bin/ldd", + "usr/bin/getconf", + "usr/bin/getent", + }, + }, + { + ID: "libc-utils@0.7-r0", + Name: "libc-utils", + Version: "0.7-r0", + SrcName: "libc-dev", + SrcVersion: "0.7-r0", + Licenses: []string{"GPL-2.0-or-later"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:9055bc7afd76cf2672198042f72fc4a5ed4fa961", + DependsOn: []string{"musl-utils@1.1.14-r10"}, + Arch: "x86_64", + //InstalledFiles: []string{}, + }, + { + ID: "pkgconf@1.6.0-r0", + Name: "pkgconf", + Version: "1.6.0-r0", + SrcName: "pkgconf", + SrcVersion: "1.6.0-r0", + Licenses: []string{"ISC"}, + Maintainer: "William Pitcock ", + Digest: "sha1:e6242ac29589c8a84a4b179b491ea7c29fce66a9", + DependsOn: []string{"musl@1.1.14-r10"}, + Arch: "x86_64", + InstalledFiles: []string{ + "usr/bin/pkgconf", + "usr/bin/pkg-config", + "usr/lib/libpkgconf.so.3.0.0", + "usr/lib/libpkgconf.so.3", + "usr/share/aclocal/pkg.m4", + }, + }, + { + ID: "sqlite-libs@3.26.0-r3", + Name: "sqlite-libs", + Version: "3.26.0-r3", + SrcName: "sqlite", + SrcVersion: "3.26.0-r3", + Licenses: []string{"Public-Domain"}, + Maintainer: "Carlo Landmeter ", + Digest: "sha1:1464946c3a5f0dd5a67ca1af930fc17af7a74474", + DependsOn: []string{"musl@1.1.14-r10"}, + Arch: "x86_64", + InstalledFiles: []string{ + "usr/lib/libsqlite3.so.0", + "usr/lib/libsqlite3.so.0.8.6", + }, + }, + { + ID: "test@2.9.11_pre20061021-r2", + Name: "test", + Version: "2.9.11_pre20061021-r2", + SrcName: "test-parent", + SrcVersion: "2.9.11_pre20061021-r2", + Licenses: []string{"Public-Domain"}, + Maintainer: "Carlo Landmeter ", + Digest: "sha1:f0bf315ec54828188910e4a665c00bc48bdbdd7d", + DependsOn: []string{ + "pkgconf@1.6.0-r0", + "sqlite-libs@3.26.0-r3", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "usr/lib/libsqlite3.so", + "usr/lib/pkgconfig/sqlite3.pc", + "usr/include/sqlite3ext.h", + "usr/include/sqlite3.h", + }, + }, + { + ID: "ada-libs@2.7.4-r0", + Name: "ada-libs", + Version: "2.7.4-r0", + SrcName: "ada", + SrcVersion: "2.7.4-r0", + Licenses: []string{ + "Apache-2.0", + "MIT", + "MPL-2.0", + }, + Maintainer: "Jakub Jirutka ", + Digest: "sha1:593154f80c440685448e0f52479725d7bc9b678d", + DependsOn: []string{ + "musl@1.1.14-r10", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "usr/lib/libada.so.2", + "usr/lib/libada.so.2.7.4", + }, + }, +} + +var files = []string{ + // musl-1.1.14-r10 + "lib/libc.musl-x86_64.so.1", + "lib/ld-musl-x86_64.so.1", + + // busybox-1.24.2-r9 + "bin/busybox", + "bin/sh", + "etc/securetty", + "etc/udhcpd.conf", + "etc/logrotate.d/acpid", + + // alpine-baselayout-3.0.3-r0 + "etc/hosts", + "etc/sysctl.conf", + "etc/group", + "etc/protocols", + "etc/fstab", + "etc/mtab", + "etc/profile", + "etc/TZ", + "etc/shells", + "etc/motd", + "etc/inittab", + "etc/hostname", + "etc/modules", + "etc/services", + "etc/shadow", + "etc/passwd", + "etc/profile.d/color_prompt", + "etc/sysctl.d/00-alpine.conf", + "etc/modprobe.d/i386.conf", + "etc/modprobe.d/blacklist.conf", + "etc/modprobe.d/aliases.conf", + "etc/modprobe.d/kms.conf", + "etc/crontabs/root", + "sbin/mkmntdirs", + "var/spool/cron/crontabs", + + // alpine-keys-1.1-r0 + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-4d07755e.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + + // zlib-1.2.8-r2 + "lib/libz.so.1.2.8", + "lib/libz.so.1", + + // libcrypto1.0-1.0.2h-r1 + "lib/libcrypto.so.1.0.0", + "usr/bin/c_rehash", + "usr/lib/libcrypto.so.1.0.0", + "usr/lib/engines/libubsec.so", + "usr/lib/engines/libatalla.so", + "usr/lib/engines/libcapi.so", + "usr/lib/engines/libgost.so", + "usr/lib/engines/libcswift.so", + "usr/lib/engines/libchil.so", + "usr/lib/engines/libgmp.so", + "usr/lib/engines/libnuron.so", + "usr/lib/engines/lib4758cca.so", + "usr/lib/engines/libsureware.so", + "usr/lib/engines/libpadlock.so", + "usr/lib/engines/libaep.so", + + // libssl1.0-1.0.2h-r1 + "lib/libssl.so.1.0.0", + "usr/lib/libssl.so.1.0.0", + + // apk-tools-2.6.7-r0 + "sbin/apk", + + // scanelf-1.1.6-r0 + "usr/bin/scanelf", + + // musl-utils-1.1.14-r10 + "sbin/ldconfig", + "usr/bin/iconv", + "usr/bin/ldd", + "usr/bin/getconf", + "usr/bin/getent", + + // libc-utils-0.7-r0 + + // pkgconf-1.6.0-r0 + "usr/bin/pkgconf", + "usr/bin/pkg-config", + "usr/lib/libpkgconf.so.3.0.0", + "usr/lib/libpkgconf.so.3", + "usr/share/aclocal/pkg.m4", + + // sqlite-libs-3.26.0-r3 + "usr/lib/libsqlite3.so.0", + "usr/lib/libsqlite3.so.0.8.6", + + // test-2.9.11_pre20061021-r2 + "usr/lib/libsqlite3.so", + "usr/lib/pkgconfig/sqlite3.pc", + "usr/include/sqlite3ext.h", + "usr/include/sqlite3.h", + + // ada-libs@2.5.1-r0 + "usr/lib/libada.so.2", + "usr/lib/libada.so.2.7.4", +} + +func TestParseApkInfo(t *testing.T) { + var tests = []struct { + name string + path string + wantPkgs []types.Package + wantFiles []string + }{ + { + name: "happy path", + path: "./testdata/apk", + wantPkgs: pkgs, + wantFiles: files, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := alpinePkgAnalyzer{} + f, err := os.Open(tt.path) + require.NoError(t, err) + defer f.Close() + scanner := bufio.NewScanner(f) + gotPkgs, gotFiles := a.parseApkInfo(t.Context(), scanner) + + assert.Equal(t, tt.wantPkgs, gotPkgs) + assert.Equal(t, tt.wantFiles, gotFiles) + }) + } +} + +func TestRequired(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "legacy APK database location", + filePath: "lib/apk/db/installed", + want: true, + }, + { + name: "new APK database location", + filePath: "usr/lib/apk/db/installed", + want: true, + }, + { + name: "unrelated file", + filePath: "etc/os-release", + want: false, + }, + { + name: "similar but different path", + filePath: "lib/apk/db/other", + want: false, + }, + } + + a := alpinePkgAnalyzer{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/pkg/apk/testdata/apk b/pkg/fanal/analyzer/pkg/apk/testdata/apk new file mode 100644 index 000000000000..633fe29aa012 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/apk/testdata/apk @@ -0,0 +1,564 @@ +C:Q11otALzX1d1D0kVawy06IairTXS0= +P:musl +V:1.1.14-r10 +A:x86_64 +S:445080 +I:569344 +T:the musl c library (libc) implementation +U:http://www.musl-libc.org/ +L:MIT +o:musl +m:Timo Teräs +t:1466181580 +c:e6c226fbe17bb8f856dce5f0d9bc088b333e6225 +p:so:libc.musl-x86_64.so.1=1 +F:lib +R:libc.musl-x86_64.so.1 +a:0:0:777 +Z:Q17yJ3JFNypA4mxhJJr0ou6CzsJVI= +R:ld-musl-x86_64.so.1 +a:0:0:755 +Z:Q1KUwsFGLHn/enpN9+QIpK/FmixtQ= + +P:invalidPackageWithoutAVersion + +C:Q1yhJHGSZ80L7cL0y4UKKGrBPwrUQ= +P:busybox +V:1.24.2-r9 +A:x86_64 +S:642121 +I:909312 +T:Size optimized toolbox of many common UNIX utilities +U:http://busybox.net +L:GPL2 +o:busybox +m:Natanael Copa +t:1466671780 +c:386b639b3917a9d1b8588dd87f09ed446501cddf +D:so:libc.musl-x86_64.so.1 +F:bin +R:busybox +a:0:0:755 +Z:Q1xOlCsdvx4O0gnKWoFCNKjz2quRE= +R:sh +a:0:0:777 +Z:Q1pcfTfDNEbNKQc2s1tia7da05M8Q= +F:etc +R:securetty +Z:Q14VDshgWFleuDbp4jqXk+UNES65Q= +R:udhcpd.conf +Z:Q1PWhOJ+TaEzAXw+XC6kkz/FXI/KA= +F:etc/logrotate.d +R:acpid +Z:Q1bPM2hPZy1LntZ/YdI4ZFJzVl1Y8= +F:etc/network +F:etc/network/if-up.d +F:etc/network/if-post-up.d +F:etc/network/if-pre-up.d +F:etc/network/if-post-down.d +F:etc/network/if-pre-down.d +F:etc/network/if-down.d +F:sbin +F:tmp +M:0:0:1777 +F:usr +F:usr/sbin +F:usr/bin +F:var +F:var/lib +F:var/lib/udhcpd +F:var/cache +F:var/cache/misc + +C:Q1ohSJYVBBHXLdH6/bMtHGxIVczPo= +P:alpine-baselayout +V:3.0.3-r0 +A:x86_64 +S:74697 +I:401408 +T:Alpine base dir structure and init scripts +U:http://git.alpinelinux.org/cgit/aports/tree/main/alpine-baselayout +L:GPL2 +o:alpine-baselayout +m:Natanael Copa +t:1466181584 +c:d0bef446b94220475c60e78f2e081f38390b89ca +D:busybox so:libc.musl-x86_64.so.1 +F:dev +F:dev/shm +F:dev/pts +F:etc +R:hosts +Z:Q1S93L8EsQ/7zGSnfGDfj5I7bjCS4= +R:sysctl.conf +Z:Q14upz3tfnNxZkIEsUhWn7Xoiw96g= +R:group +Z:Q1zNuxdqs1x+nJO8WucpZfQrdiapA= +R:protocols +Z:Q13FqXUnvuOpMDrH/6rehxuYAEE34= +R:fstab +Z:Q11Q7hNe8QpDS531guqCdrXBzoA/o= +R:mtab +a:0:0:777 +Z:Q1kiljhXXH1LlQroHsEJIkPZg2eiw= +R:profile +Z:Q1FrM1yy3WJbQTc9LgnKTn5tRovlE= +R:TZ +Z:Q1uHH18uOLEzp56qFUP843WSoKM9E= +R:shells +Z:Q1ojm2YdpCJ6B/apGDaZ/Sdb2xJkA= +R:motd +Z:Q1MaUHN/Rf32Lf67Owrq1BXQU7usE= +R:inittab +Z:Q1TsthbhW7QzWRe1E/NKwTOuD4pHc= +R:hostname +Z:Q16nVwYVXP/tChvUPdukVD2ifXOmc= +R:modules +Z:Q1C7P4uPQo8B6P6V+O78ybHl0AHhA= +R:services +Z:Q1NBe0rrC8HMzNmVf4ybSENcsdey0= +R:shadow +a:0:42:640 +Z:Q1LG0ii8vP4gQgDmSnK0WBtjtovlg= +R:passwd +Z:Q11HpI0rBp2zsun4+LIIBENg8JQUE= +F:etc/profile.d +R:color_prompt +Z:Q10wL23GuSCVfumMRgakabUI6EsSk= +F:etc/init.d +F:etc/apk +F:etc/sysctl.d +R:00-alpine.conf +Z:Q1kZy9KEvjykp1vCw1kWgdvBuEXvg= +F:etc/modprobe.d +R:i386.conf +Z:Q1pnay/njn6ol9cCssL7KiZZ8etlc= +R:blacklist.conf +Z:Q1+TdC1pulajuYy0ebcos8V/aMeqk= +R:aliases.conf +Z:Q1udaZLaeaalyuCcnBgCKPIybDO08= +R:kms.conf +Z:Q1yH/c6fBvCWn0Huny5Rf/GET2Jbs= +F:etc/modules-load.d +F:etc/opt +F:etc/conf.d +F:etc/crontabs +R:root +a:0:0:600 +Z:Q1vfk1apUWI4yLJGhhNRd0kJixfvY= +F:etc/periodic +F:etc/periodic/hourly +F:etc/periodic/weekly +F:etc/periodic/monthly +F:etc/periodic/15min +F:etc/periodic/daily +F:etc/network +F:etc/network/if-up.d +F:etc/network/if-pre-up.d +F:etc/network/if-post-down.d +F:etc/network/if-down.d +F:home +F:lib +F:lib/mdev +F:lib/firmware +F:media +F:media/floppy +F:media/cdrom +F:media/usb +F:mnt +F:proc +F:root +M:0:0:700 +F:run +F:sbin +R:mkmntdirs +a:0:0:755 +Z:Q1lGBnGMsnB3SEZL/oHeN99F1/ie8= +F:srv +F:sys +F:tmp +M:0:0:1777 +F:usr +F:usr/sbin +F:usr/bin +F:usr/local +F:usr/local/bin +F:usr/local/lib +F:usr/local/share +F:usr/share +F:usr/share/man +F:usr/share/misc +F:var +F:var/lock +F:var/lock/subsys +F:var/tmp +M:0:0:1777 +F:var/log +F:var/lib +F:var/lib/misc +F:var/local +F:var/opt +F:var/cache +F:var/cache/misc +F:var/spool +F:var/spool/cron +R:crontabs +a:0:0:777 +Z:Q1OFZt+ZMp7j0Gny0rqSKuWJyqYmA= +F:var/empty +F:var/run + +C:Q1Te9/+u5q66cAwdYlcDJvdcu4+iU= +P:alpine-keys +V:1.1-r0 +A:x86_64 +S:7787 +I:36864 +T:Public keys for Alpine Linux packages +U:http://alpinelinux.org +L:GPL +o:alpine-keys +m:Natanael Copa +t:1461964035 +c:d0b08b1e17d40d21196df7709fdb95f37165615d +r:alpine-base +F:etc +F:etc/apk +F:etc/apk/keys +R:alpine-devel@lists.alpinelinux.org-4d07755e.rsa.pub +Z:Q1XfH9IG0ZFgbOIssIhpiWqDlSspY= +R:alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub +Z:Q1BTqS+H/UUyhQuzHwiBl47+BTKuU= +R:alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub +Z:Q1v7YWZYzAWoclaLDI45jEguI7YN0= +R:alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub +Z:Q1NnGuDsdQOx4ZNYfB3N97eLyGPkI= +R:alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub +Z:Q1OvCFSO94z97c80mIDCxqGkh2Og4= + +C:Q179BNNNQKqOszFIASc2TCeounYO8= +P:zlib +V:1.2.8-r2 +A:x86_64 +S:71733 +I:98304 +T:A compression/decompression Library +U:http://zlib.net +L:zlib +o:zlib +m:Natanael Copa +t:1461931151 +c:a7329a4de4d10d99206c78c229dc3742880cd042 +D:so:libc.musl-x86_64.so.1 +p:so:libz.so.1=1.2.8 +F:lib +R:libz.so.1.2.8 +a:0:0:755 +Z:Q1bh8VMdNZcPKyIQTtLMtz/0VL2H0= +R:libz.so.1 +a:0:0:777 +Z:Q1x1VqGi0rW5lxmvPQOjnqNapz/OU= + +C:Q1Zchg/48QO2ZPQLqEmj9aUcaci+s= +P:libcrypto1.0 +V:1.0.2h-r1 +A:x86_64 +S:1714530 +I:2527232 +T:Crypto library from openssl +U:http://openssl.org +L:openssl +o:openssl +m:Timo Teras +t:1466620012 +c:38c6e1fd86f4d9cba4c146b8bdcd71f84e1a4ee7 +D:so:libc.musl-x86_64.so.1 so:libz.so.1 +p:so:libcrypto.so.1.0.0=1.0.0 +F:lib +R:libcrypto.so.1.0.0 +a:0:0:555 +Z:Q1DgSoxP0AWq64XJSPXT0yRTjSSBk= +F:usr +F:usr/bin +R:c_rehash +a:0:0:755 +Z:Q1XZt0LbTr44grnBtK+Yihjynh2EE= +F:usr/lib +R:libcrypto.so.1.0.0 +a:0:0:777 +Z:Q1jLDKGBtunzKi5FKmK/QTAqfh6uI= +F:usr/lib/engines +R:libubsec.so +a:0:0:555 +Z:Q1SaoP91RpISCN8KO3AxuB8Tzyc/A= +R:libatalla.so +a:0:0:555 +Z:Q1W31yRhAZE9X5Z5zy9l6mkn1tbcQ= +R:libcapi.so +a:0:0:555 +Z:Q1iriyqA2XunZb8pxmsOeRML2ZsRg= +R:libgost.so +a:0:0:555 +Z:Q1zwS6yHAzzdnrHb9BV8pIsE2yIgg= +R:libcswift.so +a:0:0:555 +Z:Q16/TBTN+WkIFQeFlCPTDc5Xs33bU= +R:libchil.so +a:0:0:555 +Z:Q1fMssNRSfAgg4nZVYm0IzYG2gTLk= +R:libgmp.so +a:0:0:555 +Z:Q1onZiPB/LsF3Xqn8rwH5FcYLkuf4= +R:libnuron.so +a:0:0:555 +Z:Q189GVmg2NFt2nTCqfcSl7Wtoym1o= +R:lib4758cca.so +a:0:0:555 +Z:Q1aNXgEbvxfvZdZpa4frZ9p6eq2Y4= +R:libsureware.so +a:0:0:555 +Z:Q14Z9HkfG+WqvGRIb42iugBuBKOag= +R:libpadlock.so +a:0:0:555 +Z:Q1OAwUUirl7Q1OiqTjB96lKBQYbMc= +R:libaep.so +a:0:0:555 +Z:Q1nrvE4qxl4AXEC/psF1Eh9n6E7Pg= + +C:Q1cSDzN+k7K0xE4PXzGhW2DcZ4yhQ= +P:libssl1.0 +V:1.0.2h-r1 +A:x86_64 +S:274743 +I:442368 +T:SSL shared libraries +U:http://openssl.org +L:openssl +o:openssl +m:Timo Teras +t:1466620012 +c:38c6e1fd86f4d9cba4c146b8bdcd71f84e1a4ee7 +D:so:libc.musl-x86_64.so.1 so:libcrypto.so.1.0.0 +p:so:libssl.so.1.0.0=1.0.0 +F:lib +R:libssl.so.1.0.0 +a:0:0:555 +Z:Q1wdqn4897nQP5l/f3SV4DWf9QOkQ= +F:usr +F:usr/lib +R:libssl.so.1.0.0 +a:0:0:777 +Z:Q1ke5dnHGVWcEyRpOe0/lKEqizHHQ= + +C:Q1CZDArNYrQXWBjDpMxg7RHxTiO9g= +P:apk-tools +V:2.6.7-r0 +A:x86_64 +S:146592 +I:253952 +T:Alpine Package Keeper - package manager for alpine +U:http://git.alpinelinux.org/cgit/apk-tools/ +L:GPL2 +o:apk-tools +m:Natanael Copa +t:1464341138 +c:08b6e2ae43356fbb24ef5c262fb08bfe09d675b0 +D:so:libc.musl-x86_64.so.1 so:libcrypto.so.1.0.0 so:libssl.so.1.0.0 so:libz.so.1 +F:etc +F:etc/apk +F:etc/apk/keys +F:etc/apk/protected_paths.d +F:sbin +R:apk +a:0:0:755 +Z:Q1ozOGvVOspzXfX1bUFjrjZ6ygEB0= +F:usr +F:var +F:var/lib +F:var/lib/apk +F:var/cache +F:var/cache/misc + +C:Q1+bq4F8Wtk+kqYhi8D3WWtlfALZA= +P:scanelf +V:1.1.6-r0 +A:x86_64 +S:53666 +I:90112 +T:Scan ELF binaries for stuff +U:https://wiki.gentoo.org/wiki/Hardened/PaX_Utilities +L:GPL2 +o:pax-utils +m:Natanael Copa +t:1461934341 +c:891f6254fb6e1f11f62ee2c9fd35784fd313b9d1 +D:so:libc.musl-x86_64.so.1 +r:pax-utils +F:usr +F:usr/bin +R:scanelf +a:0:0:755 +Z:Q12S+aDBkRA63GvTmx45HqqbOKBz0= + +C:Q1YIqh3Tnv97xmFdPl4zODdQ+PXsw= +P:musl-utils +V:1.1.14-r10 +A:x86_64 +S:50427 +I:110592 +T:the musl c library (libc) implementation +U:http://www.musl-libc.org/ +L:MIT BSD GPL2+ +o:musl +m:Timo Teräs +t:1466181579 +c:e6c226fbe17bb8f856dce5f0d9bc088b333e6225 +D:!uclibc-utils scanelf musl=1.1.14-r10 so:libc.musl-x86_64.so.1 +r:libiconv uclibc-utils +F:sbin +R:ldconfig +a:0:0:755 +Z:Q1Kja2+POZKxEkUOZqwSjC6kmaED4= +F:usr +F:usr/bin +R:iconv +a:0:0:755 +Z:Q1DmLsMEsBDtwb8S0z1pl0MYH29+o= +R:ldd +a:0:0:777 +Z:Q1a/NMxsyXbxLcmrTyGQtovas5gJk= +R:getconf +a:0:0:755 +Z:Q1iub9vwJRjlaCnu21SWjb504fUqk= +R:getent +a:0:0:755 +Z:Q1Z5dnRfQ7nvRbRRSpM1k0J7UMdng= + +C:Q1kFW8ev12zyZyGYBC9y/Epe1PqWE= +P:libc-utils +V:0.7-r0 +A:x86_64 +S:2804 +I:4096 +T:Meta package to pull in correct libc +U:http://alpinelinux.org +L:GPL +o:libc-dev +m:Natanael Copa +t:1461934274 +c:e3725c0af137717d6883265a92db3838900b5cee +D:musl-utils + +C:Q15iQqwpWJyKhKSxebSR6nwp/OZqk= +P:pkgconf +V:1.6.0-r0 +A:x86_64 +S:43001 +I:143360 +T:development framework configuration tools +U:https://git.dereferenced.org/pkgconf/pkgconf +L:ISC +o:pkgconf +m:William Pitcock +t:1547496958 +c:810b2b4cab3aad63cb338988d9d976012b6ad062 +D:so:libc.musl-x86_64.so.1 +p:pkgconfig=1 so:libpkgconf.so.3=3.0.0 cmd:pkg-config cmd:pkgconf +r:pkgconfig +F:usr +F:usr/bin +R:pkgconf +a:0:0:755 +Z:Q1h2xvgvvUejQFBbqDvOMhcdazFGI= +R:pkg-config +a:0:0:777 +Z:Q18oszBu4K1Rwi+tYKAA91sDfWTzE= +F:usr/lib +R:libpkgconf.so.3.0.0 +a:0:0:755 +Z:Q1HQLOx86sW3RRtphIYcWcBBmJo6M= +R:libpkgconf.so.3 +a:0:0:777 +Z:Q1NQYO0TsKR3JRQxUrYhF/izzKEOU= +F:usr/share +F:usr/share/aclocal +R:pkg.m4 +Z:Q1pVlmIMTTArohUPZPu3OCLGaH+e0= + +C:Q1FGSUbDpfDdWmfKGvkw/BevenRHQ= +P:sqlite-libs +V:3.26.0-r3 +A:x86_64 +S:481468 +I:917504 +T:Sqlite3 library +U:http://www.sqlite.org +L:Public-Domain +o:sqlite +m:Carlo Landmeter +t:1546255353 +c:856c64b1dc1f9b8176c60e28808482f8503c4e98 +D:so:libc.musl-x86_64.so.1 +p:so:libsqlite3.so.0=0.8.6 +r:sqlite +F:usr +F:usr/lib +R:libsqlite3.so.0 +a:0:0:777 +Z:Q1MZwGMEis9uc78EhxmxyozT2ZxZM= +R:libsqlite3.so.0.8.6 +a:0:0:755 +Z:Q1BOGonxBB2SdxbKENvJNl9ifknaE= + +C:Q18L8xXsVIKBiJEOSmZcALxIvb3X0= +P:test +V:2.9.11_pre20061021-r2 +A:x86_64 +S:151185 +I:618496 +T:C library that implements an SQL database engine (development files) +U:http://www.sqlite.org +L:Public-Domain +o:test-parent +m:Carlo Landmeter +t:1546255353 +c:856c64b1dc1f9b8176c60e28808482f8503c4e98 +D:pkgconfig sqlite-libs=3.26.0-r3 +p:pc:sqlite3=3.26.0 +F:usr +F:usr/lib +R:libsqlite3.so +a:0:0:777 +Z:Q1MZwGMEis9uc78EhxmxyozT2ZxZM= +F:usr/lib/pkgconfig +R:sqlite3.pc +Z:Q1Gu7uz+QOw0X+WZ8MKc8iNVSnsBA= +F:usr/include +R:sqlite3ext.h +Z:Q1riWNHq9ufQzhyMXm7raBW+ZL9z0= +R:sqlite3.h +Z:Q11MT2xE8JuMfBRYu6BDTz2PY95Vw= + +C:Q1WTFU+AxEBoVEjg9SR5cl17ybZ40= +P:ada-libs +V:2.7.4-r0 +A:x86_64 +S:166703 +I:491520 +T:WHATWG-compliant and fast URL parser written in modern C++ (libraries) +U:https://ada-url.github.io/ada +L:( Apache-2.0 OR MIT ) AND MPL-2.0 +o:ada +m:Jakub Jirutka +t:1701726025 +c:fa40f3454f9c60870d54115aac4161f9ab7c667f +D:so:libc.musl-x86_64.so.1 +p:so:libada.so.2=2.7.4 +F:usr +F:usr/lib +R:libada.so.2 +a:0:0:777 +Z:Q1qDM97tDEseDtdayU2yr3eJjOl5I= +R:libada.so.2.7.4 +a:0:0:755 +Z:Q1LUjWSS3wH8zDBHd0pYxED/hWBhk= \ No newline at end of file diff --git a/pkg/fanal/analyzer/pkg/bottlerocket_inventory/bottlerocket_inventory.go b/pkg/fanal/analyzer/pkg/bottlerocket_inventory/bottlerocket_inventory.go new file mode 100644 index 000000000000..ec83b1f4ac8f --- /dev/null +++ b/pkg/fanal/analyzer/pkg/bottlerocket_inventory/bottlerocket_inventory.go @@ -0,0 +1,110 @@ +package bottlerocket_inventory + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "slices" + "strconv" + "time" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(newBottlerocketInventoryAnalyzer()) +} + +const analyzerVersion = 1 + +var requiredFiles = []string{ + "aarch64-bottlerocket-linux-gnu/sys-root/usr/share/bottlerocket/application-inventory.json", + "x86_64-bottlerocket-linux-gnu/sys-root/usr/share/bottlerocket/application-inventory.json", +} + +type bottlerocketInventoryAnalyzer struct{} + +func newBottlerocketInventoryAnalyzer() *bottlerocketInventoryAnalyzer { + return &bottlerocketInventoryAnalyzer{} +} + +type ApplicationInventory struct { + Content []struct { + Name string `json:"Name"` + Publisher string `json:"Publisher"` + Version string `json:"Version"` + Release string `json:"Release"` + Epoch string `json:"Epoch"` + InstalledTime time.Time `json:"InstalledTime"` + ApplicationType string `json:"ApplicationType"` + Architecture string `json:"Architecture"` + URL string `json:"Url"` + Summary string `json:"Summary"` + } `json:"Content"` +} + +func (a bottlerocketInventoryAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + parsedInventorys, err := a.parseApplicationInventory(ctx, input.Content) + if err != nil { + return nil, err + } + + return &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: input.FilePath, + Packages: parsedInventorys, + }, + }, + }, nil +} + +func (a bottlerocketInventoryAnalyzer) parseApplicationInventory(_ context.Context, r io.Reader) ([]types.Package, error) { + var pkgs []types.Package + + var applicationInventory ApplicationInventory + if err := json.NewDecoder(r).Decode(&applicationInventory); err != nil { + return nil, err + } + + for _, app := range applicationInventory.Content { + epoch, err := strconv.Atoi(app.Epoch) + if err != nil { + return nil, err + } + pkg := types.Package{ + Arch: app.Architecture, + Epoch: epoch, + Name: app.Name, + Version: app.Version, + } + + if pkg.Name != "" && pkg.Version != "" { + pkg.ID = fmt.Sprintf("%s@%s", pkg.Name, pkg.Version) + } + + pkgs = append(pkgs, pkg) + } + + return pkgs, nil +} + +func (a bottlerocketInventoryAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredFiles, filePath) +} + +func (a bottlerocketInventoryAnalyzer) Type() analyzer.Type { + return analyzer.TypeBottlerocketInventory +} + +func (a bottlerocketInventoryAnalyzer) Version() int { + return analyzerVersion +} + +// StaticPaths returns a list of static file paths to analyze +func (a bottlerocketInventoryAnalyzer) StaticPaths() []string { + return requiredFiles +} diff --git a/pkg/fanal/analyzer/pkg/bottlerocket_inventory/bottlerocket_inventory_test.go b/pkg/fanal/analyzer/pkg/bottlerocket_inventory/bottlerocket_inventory_test.go new file mode 100644 index 000000000000..8e570303326c --- /dev/null +++ b/pkg/fanal/analyzer/pkg/bottlerocket_inventory/bottlerocket_inventory_test.go @@ -0,0 +1,62 @@ +package bottlerocket_inventory + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var pkgs = []types.Package{ + { + ID: "glibc@2.40", + Name: "glibc", + Version: "2.40", + Epoch: 1, + Arch: "x86_64", + }, + { + ID: "kernel-6.1@6.1.128", + Name: "kernel-6.1", + Version: "6.1.128", + Epoch: 0, + Arch: "x86_64", + }, + { + ID: "systemd@252.22", + Name: "systemd", + Version: "252.22", + Epoch: 0, + Arch: "x86_64", + }, +} + +func TestParseApplicationInventory(t *testing.T) { + var tests = []struct { + name string + path string + wantPkgs []types.Package + }{ + { + name: "happy path", + path: "./testdata/application-inventory.json", + wantPkgs: pkgs, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := bottlerocketInventoryAnalyzer{} + f, err := os.Open(tt.path) + require.NoError(t, err) + defer f.Close() + gotPkgs, err := a.parseApplicationInventory(t.Context(), f) + require.NoError(t, err) + + assert.Equal(t, tt.wantPkgs, gotPkgs) + }) + } +} diff --git a/pkg/fanal/analyzer/pkg/bottlerocket_inventory/testdata/application-inventory.json b/pkg/fanal/analyzer/pkg/bottlerocket_inventory/testdata/application-inventory.json new file mode 100644 index 000000000000..3183c4e1e186 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/bottlerocket_inventory/testdata/application-inventory.json @@ -0,0 +1,40 @@ +{ + "Content": [ + { + "Name": "glibc", + "Publisher": "bottlerocket-core-kit", + "Version": "2.40", + "Release": "1.1740525475.e3a5862c.br1", + "Epoch": "1", + "InstalledTime": "2025-02-27T20:55:41Z", + "ApplicationType": "Unspecified", + "Architecture": "x86_64", + "Url": "http://www.gnu.org/software/glibc/", + "Summary": "The GNU libc libraries" + }, + { + "Name": "kernel-6.1", + "Publisher": "bottlerocket-kernel-kit", + "Version": "6.1.128", + "Release": "1.1740603423.4d405dc9.br1", + "Epoch": "0", + "InstalledTime": "2025-02-27T20:55:41Z", + "ApplicationType": "Unspecified", + "Architecture": "x86_64", + "Url": "https://www.kernel.org/", + "Summary": "The Linux kernel" + }, + { + "Name": "systemd", + "Publisher": "bottlerocket-core-kit", + "Version": "252.22", + "Release": "1.1740525475.e3a5862c.br1", + "Epoch": "0", + "InstalledTime": "2025-02-27T20:55:41Z", + "ApplicationType": "Unspecified", + "Architecture": "x86_64", + "Url": "https://www.freedesktop.org/wiki/Software/systemd", + "Summary": "System and Service Manager" + } + ] +} diff --git a/pkg/fanal/analyzer/pkg/dpkg/copyright.go b/pkg/fanal/analyzer/pkg/dpkg/copyright.go new file mode 100644 index 000000000000..b5e2932ff3aa --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/copyright.go @@ -0,0 +1,148 @@ +package dpkg + +import ( + "bufio" + "context" + "io" + "os" + "path" + "regexp" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func init() { + analyzer.RegisterAnalyzer(&dpkgLicenseAnalyzer{}) +} + +var ( + dpkgLicenseAnalyzerVersion = 1 + + commonLicenseReferenceRegexp = regexp.MustCompile(`/?usr/share/common-licenses/([0-9A-Za-z_.+-]+[0-9A-Za-z+])`) +) + +// dpkgLicenseAnalyzer parses copyright files and detect licenses +type dpkgLicenseAnalyzer struct { + licenseFull bool + classifierConfidenceLevel float64 +} + +// Analyze parses /usr/share/doc/*/copyright files +func (a *dpkgLicenseAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + findings, err := a.parseCopyright(input.Content) + if err != nil { + return nil, xerrors.Errorf("parse copyright %s: %w", input.FilePath, err) + } + + // If licenses are not found, fallback to the classifier + if len(findings) == 0 && a.licenseFull { + // Rewind the reader to the beginning of the stream after saving + if _, err = input.Content.Seek(0, io.SeekStart); err != nil { + return nil, xerrors.Errorf("seek error: %w", err) + } + licenseFile, err := licensing.Classify(input.FilePath, input.Content, a.classifierConfidenceLevel) + if err != nil { + return nil, xerrors.Errorf("license classification error: %w", err) + } + findings = licenseFile.Findings + } + + if len(findings) == 0 { + return nil, nil + } + + // e.g. "usr/share/doc/zlib1g/copyright" => "zlib1g" + pkgName := strings.Split(input.FilePath, "/")[3] + + return &analyzer.AnalysisResult{ + Licenses: []types.LicenseFile{ + { + Type: types.LicenseTypeDpkg, + FilePath: input.FilePath, + Findings: findings, + PkgName: pkgName, + }, + }, + }, nil +} + +// parseCopyright parses /usr/share/doc/*/copyright files +func (a *dpkgLicenseAnalyzer) parseCopyright(r xio.ReadSeekerAt) ([]types.LicenseFinding, error) { + scanner := bufio.NewScanner(r) + var licenses []string + for scanner.Scan() { + line := scanner.Text() + + switch { + case strings.HasPrefix(line, "License:"): + // Machine-readable format + // cf. https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#:~:text=The%20debian%2Fcopyright%20file%20must,in%20the%20Debian%20Policy%20Manual. + l := strings.TrimSpace(line[8:]) + + l = normalizeLicense(l) + licenses = append(licenses, licensing.SplitLicenses(l)...) + case strings.Contains(line, "/usr/share/common-licenses/"): + // Common license pattern + license := commonLicenseReferenceRegexp.FindStringSubmatch(line) + if len(license) == 2 { + licenses = append(licenses, license[1]) + } + } + } + + licenses = licensing.NormalizeLicenses(licenses) + licenses = lo.Uniq(licenses) + + return xslices.Map(licenses, func(license string) types.LicenseFinding { + return types.LicenseFinding{Name: license} + }), nil + +} + +func (a *dpkgLicenseAnalyzer) Init(opt analyzer.AnalyzerOptions) error { + a.licenseFull = opt.LicenseScannerOption.Full + a.classifierConfidenceLevel = opt.LicenseScannerOption.ClassifierConfidenceLevel + return nil +} + +func (a *dpkgLicenseAnalyzer) Required(filePath string, _ os.FileInfo) bool { + // To exclude files from subfolders + // e.g. usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/copyright + match, err := path.Match("usr/share/doc/*/copyright", filePath) + if err != nil { + return false + } + return match +} + +func (a *dpkgLicenseAnalyzer) Type() analyzer.Type { + return analyzer.TypeDpkgLicense +} + +func (a *dpkgLicenseAnalyzer) Version() int { + return dpkgLicenseAnalyzerVersion +} + +// normalizeLicense returns a normalized license identifier in a heuristic way +func normalizeLicense(s string) string { + // "The MIT License (MIT)" => "The MIT License" + s, _, _ = strings.Cut(s, "(") + + // Very rarely has below phrases + s = strings.TrimPrefix(s, "The main library is licensed under ") + s = strings.TrimSuffix(s, " license") + + return strings.TrimSpace(s) +} + +func (a *dpkgLicenseAnalyzer) StaticPaths() []string { + return []string{"usr/share/doc/"} +} diff --git a/pkg/fanal/analyzer/pkg/dpkg/copyright_test.go b/pkg/fanal/analyzer/pkg/dpkg/copyright_test.go new file mode 100644 index 000000000000..483111c94a71 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/copyright_test.go @@ -0,0 +1,135 @@ +package dpkg + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_dpkgLicenseAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + filePath string + testFile string + want *analyzer.AnalysisResult + }{ + { + name: "machine-readable format", + filePath: "usr/share/doc/zlib1g/copyright", + testFile: "testdata/license-pattern-and-classifier-copyright", + want: &analyzer.AnalysisResult{ + Licenses: []types.LicenseFile{ + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/zlib1g/copyright", + Findings: []types.LicenseFinding{ + {Name: "GPL-1.0-or-later"}, + {Name: "Artistic-2.0"}, + {Name: "BSD-4-clause-POWERDOG"}, + {Name: "Zlib"}, + }, + PkgName: "zlib1g", + }, + }, + }, + }, + { + name: "common-licenses format", + filePath: "usr/share/doc/adduser/copyright", + testFile: "testdata/common-license-copyright", + want: &analyzer.AnalysisResult{ + Licenses: []types.LicenseFile{ + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/adduser/copyright", + Findings: []types.LicenseFinding{ + {Name: "GPL-2.0-only"}, + }, + PkgName: "adduser", + }, + }, + }, + }, + { + name: "machine-readable and common-licenses format", + filePath: "usr/share/doc/apt/copyright", + testFile: "testdata/all-patterns-copyright", + want: &analyzer.AnalysisResult{ + Licenses: []types.LicenseFile{ + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/apt/copyright", + Findings: []types.LicenseFinding{ + {Name: "GPL-2.0-or-later"}, + {Name: "GPL-2.0-only"}, + }, + PkgName: "apt", + }, + }, + }, + }, + { + name: "no license found", + filePath: "usr/share/doc/tzdata/copyright", + testFile: "testdata/no-license-copyright", + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.testFile) + require.NoError(t, err) + + input := analyzer.AnalysisInput{ + Content: f, + FilePath: tt.filePath, + } + a := dpkgLicenseAnalyzer{} + + license, err := a.Analyze(t.Context(), input) + require.NoError(t, err) + assert.Equal(t, tt.want, license) + }) + } +} + +func Test_dpkgLicenseAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "happy path", + filePath: "usr/share/doc/eject/copyright", + want: true, + }, + { + name: "copyright files in subfolder", + filePath: "usr/share/doc/ca-certificates/examples/ca-certificates-local/debian/copyright", + want: false, + }, + { + name: "bad prefix", + filePath: "usr/share/doc/eject/copyright/file", + want: false, + }, + { + name: "bad file name", + filePath: "usr/share/doc/eject/copyright/foo", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := dpkgLicenseAnalyzer{} + assert.Equal(t, tt.want, a.Required(tt.filePath, nil)) + }) + } +} diff --git a/pkg/fanal/analyzer/pkg/dpkg/dpkg.go b/pkg/fanal/analyzer/pkg/dpkg/dpkg.go new file mode 100644 index 000000000000..c3a8bb94e804 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/dpkg.go @@ -0,0 +1,451 @@ +package dpkg + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "io/fs" + "net/textproto" + "os" + "path/filepath" + "regexp" + "slices" + "sort" + "strings" + + debVersion "github.com/knqyf263/go-deb-version" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func init() { + analyzer.RegisterPostAnalyzer(analyzer.TypeDpkg, newDpkgAnalyzer) +} + +type dpkgAnalyzer struct { + logger *log.Logger +} + +func newDpkgAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) { + return &dpkgAnalyzer{ + logger: log.WithPrefix("dpkg"), + }, nil +} + +const ( + analyzerVersion = 6 + + statusFile = "var/lib/dpkg/status" + statusDir = "var/lib/dpkg/status.d/" + infoDir = "var/lib/dpkg/info/" + availableFile = "var/lib/dpkg/available" + + md5sumsExtension = ".md5sums" +) + +var ( + dpkgSrcCaptureRegexp = regexp.MustCompile(`(?P[^\s]*)( \((?P.*)\))?`) + dpkgSrcCaptureRegexpNames = dpkgSrcCaptureRegexp.SubexpNames() + + // thirdPartyMaintainerPatterns contains patterns that indicate a package is from a third-party repository. + // Packages with maintainers matching these patterns will NOT have their InstalledFiles tracked, + // allowing language scanners to properly analyze files installed by those packages. + // See https://github.com/aquasecurity/trivy/issues/9916 for more details. + thirdPartyMaintainerPatterns = []string{ + // Container & orchestration + "support@docker.com", // Docker + + // Cloud providers & infrastructure + "@nvidia.com", // NVIDIA CUDA + "Google Cloud CLI Authors", // Google Cloud SDK + "sapmachine@sap.com", // SAP Machine JDK + "@hashicorp.com", // HashiCorp (Terraform, Vault, Consul, etc.) + "@microsoft.com", // Microsoft (VS Code, Azure CLI, .NET, etc.) + + // Databases + "@mongodb.com", // MongoDB + "developers@lists.mariadb.org", // MariaDB + "dev@couchdb.apache.org", // Apache CouchDB + "info@elastic.co", // Elastic (Elasticsearch, Kibana, etc.) + + // Web servers & API gateways + "nginx-packaging@f5.com", // NGINX (from nginx.org, not Debian) + "@konghq.com", // Kong + "@cloudflare.com", // Cloudflare (cloudflared, WARP) + + // Monitoring & observability + "support@influxdb.com", // InfluxData (InfluxDB, Telegraf) + "support@gitlab.com", // GitLab + "contact@grafana.com", // Grafana Labs + "@datadoghq.com", // Datadog + + // Language runtimes (third-party repos) + "@nodesource.com", // NodeSource (Node.js) + + // Networking & VPN + "info@tailscale.com", // Tailscale + + // Robotics + "@openrobotics.org", // ROS (Robot Operating System) + "@osrfoundation.org", // ROS (Robot Operating System) + } + + // thirdPartyMaintainerExact contains maintainer strings that require exact match. + // These are too short or generic for substring matching. + thirdPartyMaintainerExact = []string{ + "GitHub", // GitHub CLI + "HashiCorp", // HashiCorp (Terraform, Vault, Consul, etc.) + } +) + +func (a dpkgAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) { + var systemInstalledFiles []string + var packageInfos []types.PackageInfo + + // parse `available` file to get digest for packages + digests, err := a.parseDpkgAvailable(input.FS) + if err != nil { + a.logger.Debug("Unable to parse the available file", log.FilePath(availableFile), log.Err(err)) + } + + required := func(path string, _ fs.DirEntry) bool { + return path != availableFile + } + + packageFiles := make(map[string][]string) + + // parse other files + err = fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error { + // parse *md5sums files + if a.isMd5SumsFile(filepath.Split(path)) { + scanner := bufio.NewScanner(r) + systemFiles, err := a.parseDpkgMd5sums(scanner) + if err != nil { + return xerrors.Errorf("failed to parse %s file: %w", path, err) + } + packageFiles[strings.TrimSuffix(filepath.Base(path), md5sumsExtension)] = systemFiles + // Note: systemInstalledFiles will be populated later based on maintainer check + return nil + } + // parse status files + infos, err := a.parseDpkgStatus(path, r, digests) + if err != nil { + return err + } + packageInfos = append(packageInfos, infos...) + return nil + }) + if err != nil { + return nil, xerrors.Errorf("dpkg walk error: %w", err) + } + + // Map packages to their respective files. + // Third-party packages will NOT have their InstalledFiles populated to avoid filtering out + // language packages (npm, pip, etc.) installed by those third-party OS packages. + for i, pkgInfo := range packageInfos { + for j, pkg := range pkgInfo.Packages { + installedFiles, found := packageFiles[pkg.Name] + if !found { + installedFiles = packageFiles[pkg.Name+":"+pkg.Arch] + } + + // Skip InstalledFiles for third-party packages + if isThirdPartyPackage(pkg.Maintainer) { + a.logger.Debug("Third-party package detected", + log.String("package", pkg.Name), + log.String("maintainer", pkg.Maintainer)) + packageInfos[i].Packages[j].Repository = types.PackageRepository{ + Class: types.RepositoryClassThirdParty, + } + continue + } + + packageInfos[i].Packages[j].Repository = types.PackageRepository{ + Class: types.RepositoryClassOfficial, + } + packageInfos[i].Packages[j].InstalledFiles = installedFiles + systemInstalledFiles = append(systemInstalledFiles, installedFiles...) + } + } + + return &analyzer.AnalysisResult{ + PackageInfos: packageInfos, + SystemInstalledFiles: systemInstalledFiles, + }, nil + +} + +// parseDpkgMd5sums parses `/var/lib/dpkg/*/*.md5sums` file. +// +// `*.md5sums` files don't contain links (see https://github.com/aquasecurity/trivy/pull/9131#discussion_r2182557288). +// But Trivy doesn't support links, so this will not cause problems. +// TODO use `*.list` files instead of `*.md5sums` files when Trivy will support links. +func (a dpkgAnalyzer) parseDpkgMd5sums(scanner *bufio.Scanner) ([]string, error) { + var installedFiles []string + for scanner.Scan() { + current := scanner.Text() + + // md5sums file use the following format: + // (2 spaces) + // cf. https://man7.org/linux/man-pages/man5/deb-md5sums.5.html + _, file, ok := strings.Cut(current, " ") + if !ok { + return nil, xerrors.Errorf("invalid md5sums line format: %s", current) + } + installedFiles = append(installedFiles, "/"+file) // md5sums files don't contain leading slash + } + + if err := scanner.Err(); err != nil { + return nil, xerrors.Errorf("scan error: %w", err) + } + + sort.Strings(installedFiles) + return installedFiles, nil +} + +// parseDpkgAvailable parses /var/lib/dpkg/available +func (a dpkgAnalyzer) parseDpkgAvailable(fsys fs.FS) (map[string]digest.Digest, error) { + f, err := fsys.Open(availableFile) + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + pkgs := make(map[string]digest.Digest) + scanner := NewScanner(f) + for scanner.Scan() { + header, err := scanner.Header() + if !errors.Is(err, io.EOF) && err != nil { + a.logger.Warn("Parse error", log.FilePath(availableFile), log.Err(err)) + continue + } + name, version, checksum := header.Get("Package"), header.Get("Version"), header.Get("SHA256") + pkgID := a.pkgID(name, version) + if pkgID != "" && checksum != "" { + pkgs[pkgID] = digest.NewDigestFromString(digest.SHA256, checksum) + } + } + if err = scanner.Err(); err != nil { + return nil, xerrors.Errorf("scan error: %w", err) + } + + return pkgs, nil +} + +// parseDpkgStatus parses /var/lib/dpkg/status or /var/lib/dpkg/status/* +func (a dpkgAnalyzer) parseDpkgStatus(filePath string, r io.Reader, digests map[string]digest.Digest) ([]types.PackageInfo, error) { + var pkg *types.Package + pkgs := make(map[string]*types.Package) + pkgIDs := make(map[string]string) + + scanner := NewScanner(r) + for scanner.Scan() { + header, err := scanner.Header() + if !errors.Is(err, io.EOF) && err != nil { + a.logger.Warn("Parse error", log.FilePath(filePath), log.Err(err)) + continue + } + + pkg = a.parseDpkgPkg(header) + if pkg != nil { + pkg.Digest = digests[pkg.ID] + pkgs[pkg.ID] = pkg + pkgIDs[pkg.Name] = pkg.ID + } + } + + if err := scanner.Err(); err != nil { + return nil, xerrors.Errorf("scan error: %w", err) + } + + a.consolidateDependencies(pkgs, pkgIDs) + + return []types.PackageInfo{ + { + FilePath: filePath, + Packages: lo.MapToSlice(pkgs, func(_ string, p *types.Package) types.Package { + return *p + }), + }, + }, nil +} + +func (a dpkgAnalyzer) parseDpkgPkg(header textproto.MIMEHeader) *types.Package { + if isInstalled := a.parseStatus(header.Get("Status")); !isInstalled { + return nil + } + + pkg := &types.Package{ + Name: header.Get("Package"), + Version: header.Get("Version"), // Will be parsed later + DependsOn: a.parseDepends(header.Get("Depends")), // Will be updated later + Maintainer: header.Get("Maintainer"), + Arch: header.Get("Architecture"), + } + if pkg.Name == "" || pkg.Version == "" { + return nil + } + + // Source line (Optional) + // Gives the name of the source package + // May also specifies a version + if src := header.Get("Source"); src != "" { + srcCapture := dpkgSrcCaptureRegexp.FindAllStringSubmatch(src, -1)[0] + md := make(map[string]string) + for i, n := range srcCapture { + md[dpkgSrcCaptureRegexpNames[i]] = strings.TrimSpace(n) + } + pkg.SrcName = md["name"] + pkg.SrcVersion = md["version"] + } + + // Source version and names are computed from binary package names and versions in dpkg. + // Source package name: + // https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/pkg-format.c#n338 + // Source package version: + // https://git.dpkg.org/cgit/dpkg/dpkg.git/tree/lib/dpkg/pkg-format.c#n355 + if pkg.SrcName == "" { + pkg.SrcName = pkg.Name + } + if pkg.SrcVersion == "" { + pkg.SrcVersion = pkg.Version + } + + if v, err := debVersion.NewVersion(pkg.Version); err != nil { + a.logger.Warn("Invalid version", log.String("OS", "debian"), + log.String("package", pkg.Name), log.String("version", pkg.Version)) + return nil + } else { + pkg.ID = a.pkgID(pkg.Name, pkg.Version) + pkg.Version = v.Version() + pkg.Epoch = v.Epoch() + pkg.Release = v.Revision() + } + + if v, err := debVersion.NewVersion(pkg.SrcVersion); err != nil { + a.logger.Warn("Invalid source version", log.String("OS", "debian"), + log.String("package", pkg.Name), log.String("version", pkg.SrcVersion)) + return nil + } else { + pkg.SrcVersion = v.Version() + pkg.SrcEpoch = v.Epoch() + pkg.SrcRelease = v.Revision() + } + + return pkg +} + +func (a dpkgAnalyzer) Required(filePath string, _ os.FileInfo) bool { + dir, fileName := filepath.Split(filePath) + if a.isMd5SumsFile(dir, fileName) || filePath == statusFile || filePath == availableFile || dir == statusDir { + return true + } + + return false +} + +func (a dpkgAnalyzer) pkgID(name, version string) string { + return fmt.Sprintf("%s@%s", name, version) +} + +func (a dpkgAnalyzer) parseStatus(s string) bool { + for ss := range strings.FieldsSeq(s) { + if ss == "deinstall" || ss == "purge" { + return false + } + } + return true +} + +func (a dpkgAnalyzer) parseDepends(s string) []string { + // e.g. passwd, debconf (>= 0.5) | debconf-2.0 + var dependencies []string + for dep := range strings.SplitSeq(s, ",") { + // e.g. gpgv | gpgv2 | gpgv1 + for d := range strings.SplitSeq(dep, "|") { + d = a.trimVersionRequirement(d) + + // Store only uniq package names here + d = strings.TrimSpace(d) + if !slices.Contains(dependencies, d) { + dependencies = append(dependencies, d) + } + } + } + return dependencies +} + +func (a dpkgAnalyzer) trimVersionRequirement(s string) string { + // e.g. + // libapt-pkg6.0 (>= 2.2.4) => libapt-pkg6.0 + // adduser => adduser + s, _, _ = strings.Cut(s, "(") + return s +} + +func (a dpkgAnalyzer) consolidateDependencies(pkgs map[string]*types.Package, pkgIDs map[string]string) { + for _, pkg := range pkgs { + // e.g. libc6 => libc6@2.31-13+deb11u4 + pkg.DependsOn = lo.FilterMap(pkg.DependsOn, func(d string, _ int) (string, bool) { + if pkgID, ok := pkgIDs[d]; ok { + return pkgID, true + } + return "", false + }) + sort.Strings(pkg.DependsOn) + if len(pkg.DependsOn) == 0 { + pkg.DependsOn = nil + } + } +} + +func (a dpkgAnalyzer) isMd5SumsFile(dir, fileName string) bool { + // - var/lib/dpkg/info/*.md5sums is default path + // - var/lib/dpkg/status.d/*.md5sums path in distroless images (see https://github.com/GoogleContainerTools/distroless/blob/5c119701429fb742ab45682cfc3073f911bad4bf/PACKAGE_METADATA.md#omitted-files) + if dir != infoDir && dir != statusDir { + return false + } + + return strings.HasSuffix(fileName, md5sumsExtension) +} + +// isThirdPartyPackage checks if a package is from a third-party repository +// by examining the Maintainer field against known third-party patterns. +// +// Unlike RPM which has a dedicated "Vendor" field, dpkg packages don't have a reliable +// way to identify their origin. We use a heuristic approach based on maintainer patterns. +// See https://github.com/aquasecurity/trivy/issues/9916 for more details. +func isThirdPartyPackage(maintainer string) bool { + if slices.Contains(thirdPartyMaintainerExact, maintainer) { + return true + } + return slices.ContainsFunc(thirdPartyMaintainerPatterns, func(pattern string) bool { + return strings.Contains(maintainer, pattern) + }) +} + +func (a dpkgAnalyzer) Type() analyzer.Type { + return analyzer.TypeDpkg +} + +func (a dpkgAnalyzer) Version() int { + return analyzerVersion +} + +// StaticPaths returns a list of static file paths to analyze +func (a dpkgAnalyzer) StaticPaths() []string { + return []string{ + statusFile, + availableFile, + statusDir, + infoDir, + } +} diff --git a/pkg/fanal/analyzer/pkg/dpkg/dpkg_test.go b/pkg/fanal/analyzer/pkg/dpkg/dpkg_test.go new file mode 100644 index 000000000000..a91954d220d8 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/dpkg_test.go @@ -0,0 +1,1693 @@ +package dpkg + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_dpkgAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + txtar string + want *analyzer.AnalysisResult + wantErr bool + }{ + { + name: "valid", + txtar: "testdata/valid.txtar", + want: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: []types.Package{ + { + ID: "adduser@3.116ubuntu1", + Name: "adduser", + Version: "3.116ubuntu1", + SrcName: "adduser", + SrcVersion: "3.116ubuntu1", + DependsOn: []string{ + "debconf@1.5.66", + "passwd@1:4.5-1ubuntu1", + }, + Maintainer: "Ubuntu Core Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "apt@1.6.3ubuntu0.1", + Name: "apt", + Version: "1.6.3ubuntu0.1", + SrcName: "apt", + SrcVersion: "1.6.3ubuntu0.1", + DependsOn: []string{ + "adduser@3.116ubuntu1", + "gpgv@2.2.4-1ubuntu1.1", + "libapt-pkg5.0@1.6.3ubuntu0.1", + "libc6@2.27-3ubuntu1", + "libgcc1@1:8-20180414-1ubuntu2", + "libgnutls30@3.5.18-1ubuntu1", + "libseccomp2@2.3.1-2.1ubuntu4", + "libstdc++6@8-20180414-1ubuntu2", + "ubuntu-keyring@2018.02.28", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "base-files@10.1ubuntu2.2", + Name: "base-files", + Version: "10.1ubuntu2.2", + SrcName: "base-files", + SrcVersion: "10.1ubuntu2.2", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "base-passwd@3.5.44", + Name: "base-passwd", + Version: "3.5.44", + SrcName: "base-passwd", + SrcVersion: "3.5.44", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libdebconfclient0@0.213ubuntu1", + }, + Maintainer: "Colin Watson ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "bash@4.4.18-2ubuntu1", + Name: "bash", + Version: "4.4.18", + Release: "2ubuntu1", + SrcName: "bash", + SrcVersion: "4.4.18", + SrcRelease: "2ubuntu1", + DependsOn: []string{ + "base-files@10.1ubuntu2.2", + "debianutils@4.8.4", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "bsdutils@1:2.31.1-0.4ubuntu3.1", + Name: "bsdutils", + Version: "2.31.1", + Epoch: 1, + Release: "0.4ubuntu3.1", + SrcName: "util-linux", + SrcVersion: "2.31.1", + SrcRelease: "0.4ubuntu3.1", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "bzip2@1.0.6-8.1", + Name: "bzip2", + Version: "1.0.6", + Release: "8.1", + SrcName: "bzip2", + SrcVersion: "1.0.6", + SrcRelease: "8.1", + DependsOn: []string{ + "libbz2-1.0@1.0.6-8.1", + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "coreutils@8.28-1ubuntu1", + Name: "coreutils", + Version: "8.28", + Release: "1ubuntu1", + SrcName: "coreutils", + SrcVersion: "8.28", + SrcRelease: "1ubuntu1", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "dash@0.5.8-2.10", + Name: "dash", + Version: "0.5.8", + Release: "2.10", + SrcName: "dash", + SrcVersion: "0.5.8", + SrcRelease: "2.10", + DependsOn: []string{ + "debianutils@4.8.4", + "dpkg@1.19.0.5ubuntu2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "debconf@1.5.66", + Name: "debconf", + Version: "1.5.66", + SrcName: "debconf", + SrcVersion: "1.5.66", + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "debianutils@4.8.4", + Name: "debianutils", + Version: "4.8.4", + SrcName: "debianutils", + SrcVersion: "4.8.4", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "diffutils@1:3.6-1", + Name: "diffutils", + Epoch: 1, + Version: "3.6", + Release: "1", + SrcName: "diffutils", + SrcEpoch: 1, + SrcVersion: "3.6", + SrcRelease: "1", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "dpkg@1.19.0.5ubuntu2", + Name: "dpkg", + Version: "1.19.0.5ubuntu2", + SrcName: "dpkg", + SrcVersion: "1.19.0.5ubuntu2", + DependsOn: []string{ + "tar@1.29b-2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "e2fsprogs@1.44.1-1", + Name: "e2fsprogs", + Version: "1.44.1", + Release: "1", + SrcName: "e2fsprogs", + SrcVersion: "1.44.1", + SrcRelease: "1", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "fdisk@2.31.1-0.4ubuntu3.1", + Name: "fdisk", + Version: "2.31.1", + Release: "0.4ubuntu3.1", + SrcName: "util-linux", + SrcVersion: "2.31.1", + SrcRelease: "0.4ubuntu3.1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libfdisk1@2.31.1-0.4ubuntu3.1", + "libmount1@2.31.1-0.4ubuntu3.1", + "libncursesw5@6.1-1ubuntu1.18.04", + "libsmartcols1@2.31.1-0.4ubuntu3.1", + "libtinfo5@6.1-1ubuntu1.18.04", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "findutils@4.6.0+git+20170828-2", + Name: "findutils", + Version: "4.6.0+git+20170828", + Release: "2", + SrcName: "findutils", + SrcVersion: "4.6.0+git+20170828", + SrcRelease: "2", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "gcc-8-base@8-20180414-1ubuntu2", + Name: "gcc-8-base", + Version: "8-20180414", + Release: "1ubuntu2", + SrcName: "gcc-8", + SrcVersion: "8-20180414", + SrcRelease: "1ubuntu2", + Maintainer: "Ubuntu Core developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "gpgv@2.2.4-1ubuntu1.1", + Name: "gpgv", + Version: "2.2.4", + Release: "1ubuntu1.1", + SrcName: "gnupg2", + SrcVersion: "2.2.4", + SrcRelease: "1ubuntu1.1", + DependsOn: []string{ + "libbz2-1.0@1.0.6-8.1", + "libc6@2.27-3ubuntu1", + "libgcrypt20@1.8.1-4ubuntu1.1", + "libgpg-error0@1.27-6", + "zlib1g@1:1.2.11.dfsg-0ubuntu2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "grep@3.1-2", + Name: "grep", + Version: "3.1", + Release: "2", + SrcName: "grep", + SrcVersion: "3.1", + SrcRelease: "2", + DependsOn: []string{ + "dpkg@1.19.0.5ubuntu2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "gzip@1.6-5ubuntu1", + Name: "gzip", + Version: "1.6", + Release: "5ubuntu1", + SrcName: "gzip", + SrcVersion: "1.6", + SrcRelease: "5ubuntu1", + DependsOn: []string{ + "dpkg@1.19.0.5ubuntu2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "hostname@3.20", + Name: "hostname", + Version: "3.20", + SrcName: "hostname", + SrcVersion: "3.20", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "init-system-helpers@1.51", + Name: "init-system-helpers", + Version: "1.51", + SrcName: "init-system-helpers", + SrcVersion: "1.51", + DependsOn: []string{ + "perl-base@5.26.1-6ubuntu0.2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libacl1@2.2.52-3build1", + Name: "libacl1", + Version: "2.2.52", + Release: "3build1", + SrcName: "acl", + SrcVersion: "2.2.52", + SrcRelease: "3build1", + DependsOn: []string{ + "libattr1@1:2.4.47-2build1", + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libapt-pkg5.0@1.6.3ubuntu0.1", + Name: "libapt-pkg5.0", + Version: "1.6.3ubuntu0.1", + SrcName: "apt", + SrcVersion: "1.6.3ubuntu0.1", + DependsOn: []string{ + "libbz2-1.0@1.0.6-8.1", + "libc6@2.27-3ubuntu1", + "libgcc1@1:8-20180414-1ubuntu2", + "liblz4-1@0.0~r131-2ubuntu3", + "liblzma5@5.1.1alpha+20120614-2+b3", + "libstdc++6@8-20180414-1ubuntu2", + "libudev1@237-3ubuntu10.3", + "libzstd1@1.3.3+dfsg-2ubuntu1", + "zlib1g@1:1.2.11.dfsg-0ubuntu2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libattr1@1:2.4.47-2build1", + Name: "libattr1", + Epoch: 1, + Version: "2.4.47", + Release: "2build1", + SrcName: "attr", + SrcEpoch: 1, + SrcVersion: "2.4.47", + SrcRelease: "2build1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libaudit-common@1:2.8.2-1ubuntu1", + Name: "libaudit-common", + Version: "2.8.2", + Epoch: 1, + Release: "1ubuntu1", + SrcName: "audit", + SrcVersion: "2.8.2", + SrcEpoch: 1, + SrcRelease: "1ubuntu1", + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libaudit1@1:2.8.2-1ubuntu1", + Name: "libaudit1", + Epoch: 1, + Version: "2.8.2", + Release: "1ubuntu1", + SrcName: "audit", + SrcEpoch: 1, + SrcVersion: "2.8.2", + SrcRelease: "1ubuntu1", + DependsOn: []string{ + "libaudit-common@1:2.8.2-1ubuntu1", + "libc6@2.27-3ubuntu1", + "libcap-ng0@0.7.7-3.1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libblkid1@2.31.1-0.4ubuntu3.1", + Name: "libblkid1", + Version: "2.31.1", + Release: "0.4ubuntu3.1", + SrcName: "util-linux", + SrcVersion: "2.31.1", + SrcRelease: "0.4ubuntu3.1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libuuid1@2.31.1-0.4ubuntu3.1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libbz2-1.0@1.0.6-8.1", + Name: "libbz2-1.0", + Version: "1.0.6", + Release: "8.1", + SrcName: "bzip2", + SrcVersion: "1.0.6", + SrcRelease: "8.1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libc-bin@2.27-3ubuntu1", + Name: "libc-bin", + Version: "2.27", + Release: "3ubuntu1", + SrcName: "glibc", + SrcVersion: "2.27", + SrcRelease: "3ubuntu1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libc6@2.27-3ubuntu1", + Name: "libc6", + Version: "2.27", + Release: "3ubuntu1", + SrcName: "glibc", + SrcVersion: "2.27", + SrcRelease: "3ubuntu1", + DependsOn: []string{ + "libgcc1@1:8-20180414-1ubuntu2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libcap-ng0@0.7.7-3.1", + Name: "libcap-ng0", + Version: "0.7.7", + Release: "3.1", + SrcName: "libcap-ng", + SrcVersion: "0.7.7", + SrcRelease: "3.1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libcom-err2@1.44.1-1", + Name: "libcom-err2", + Version: "1.44.1", + Release: "1", + SrcName: "e2fsprogs", + SrcVersion: "1.44.1", + SrcRelease: "1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libdb5.3@5.3.28-13.1ubuntu1", + Name: "libdb5.3", + Version: "5.3.28", + Release: "13.1ubuntu1", + SrcName: "db5.3", + SrcVersion: "5.3.28", + SrcRelease: "13.1ubuntu1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libdebconfclient0@0.213ubuntu1", + Name: "libdebconfclient0", + Version: "0.213ubuntu1", + SrcName: "cdebconf", + SrcVersion: "0.213ubuntu1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libext2fs2@1.44.1-1", + Name: "libext2fs2", + Version: "1.44.1", + Release: "1", + SrcName: "e2fsprogs", + SrcVersion: "1.44.1", + SrcRelease: "1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libfdisk1@2.31.1-0.4ubuntu3.1", + Name: "libfdisk1", + Version: "2.31.1", + Release: "0.4ubuntu3.1", + SrcName: "util-linux", + SrcVersion: "2.31.1", + SrcRelease: "0.4ubuntu3.1", + DependsOn: []string{ + "libblkid1@2.31.1-0.4ubuntu3.1", + "libc6@2.27-3ubuntu1", + "libuuid1@2.31.1-0.4ubuntu3.1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libffi6@3.2.1-8", + Name: "libffi6", + Version: "3.2.1", + Release: "8", + SrcName: "libffi", + SrcVersion: "3.2.1", + SrcRelease: "8", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libgcc1@1:8-20180414-1ubuntu2", + Name: "libgcc1", + Epoch: 1, + Version: "8-20180414", + Release: "1ubuntu2", + SrcName: "gcc-8", + SrcRelease: "1ubuntu2", + SrcVersion: "8-20180414", + DependsOn: []string{ + "gcc-8-base@8-20180414-1ubuntu2", + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Core developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libgcrypt20@1.8.1-4ubuntu1.1", + Name: "libgcrypt20", + Version: "1.8.1", + Release: "4ubuntu1.1", + SrcName: "libgcrypt20", + SrcVersion: "1.8.1", + SrcRelease: "4ubuntu1.1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libgpg-error0@1.27-6", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libgmp10@2:6.1.2+dfsg-2", + Name: "libgmp10", + Epoch: 2, + Version: "6.1.2+dfsg", + Release: "2", + SrcName: "gmp", + SrcEpoch: 2, + SrcVersion: "6.1.2+dfsg", + SrcRelease: "2", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libgnutls30@3.5.18-1ubuntu1", + Name: "libgnutls30", + Version: "3.5.18", + Release: "1ubuntu1", + SrcName: "gnutls28", + SrcVersion: "3.5.18", + SrcRelease: "1ubuntu1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libgmp10@2:6.1.2+dfsg-2", + "libhogweed4@3.4-1", + "libidn2-0@2.0.4-1.1build2", + "libnettle6@3.4-1", + "libp11-kit0@0.23.9-2", + "libtasn1-6@4.13-2", + "libunistring2@0.9.9-0ubuntu1", + "zlib1g@1:1.2.11.dfsg-0ubuntu2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libgpg-error0@1.27-6", + Name: "libgpg-error0", + + Version: "1.27", + Release: "6", + SrcName: "libgpg-error", + SrcVersion: "1.27", + SrcRelease: "6", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libhogweed4@3.4-1", + Name: "libhogweed4", + Version: "3.4", + Release: "1", + SrcName: "nettle", + SrcVersion: "3.4", + SrcRelease: "1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libgmp10@2:6.1.2+dfsg-2", + "libnettle6@3.4-1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libidn2-0@2.0.4-1.1build2", + Name: "libidn2-0", + Version: "2.0.4", + Release: "1.1build2", + SrcName: "libidn2", + SrcVersion: "2.0.4", + SrcRelease: "1.1build2", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libunistring2@0.9.9-0ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "liblz4-1@0.0~r131-2ubuntu3", + Name: "liblz4-1", + Version: "0.0~r131", + Release: "2ubuntu3", + SrcName: "lz4", + SrcVersion: "0.0~r131", + SrcRelease: "2ubuntu3", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "liblzma5@5.1.1alpha+20120614-2+b3", + Name: "liblzma5", + Version: "5.1.1alpha+20120614", + Release: "2+b3", + SrcName: "xz-utils", + SrcVersion: "5.1.1alpha+20120614", + SrcRelease: "2", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Jonathan Nieder ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libmount1@2.31.1-0.4ubuntu3.1", + Name: "libmount1", + Version: "2.31.1", + Release: "0.4ubuntu3.1", + SrcName: "util-linux", + SrcVersion: "2.31.1", + SrcRelease: "0.4ubuntu3.1", + DependsOn: []string{ + "libblkid1@2.31.1-0.4ubuntu3.1", + "libc6@2.27-3ubuntu1", + "libselinux1@2.7-2build2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libncurses5@6.1-1ubuntu1.18.04", + Name: "libncurses5", + Version: "6.1", + Release: "1ubuntu1.18.04", + SrcName: "ncurses", + SrcVersion: "6.1", + SrcRelease: "1ubuntu1.18.04", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libtinfo5@6.1-1ubuntu1.18.04", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libncursesw5@6.1-1ubuntu1.18.04", + Name: "libncursesw5", + Version: "6.1", + Release: "1ubuntu1.18.04", + SrcName: "ncurses", + SrcVersion: "6.1", + SrcRelease: "1ubuntu1.18.04", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libtinfo5@6.1-1ubuntu1.18.04", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libnettle6@3.4-1", + Name: "libnettle6", + Version: "3.4", + Release: "1", + SrcName: "nettle", + SrcVersion: "3.4", + SrcRelease: "1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libp11-kit0@0.23.9-2", + Name: "libp11-kit0", + Version: "0.23.9", + Release: "2", + SrcName: "p11-kit", + SrcVersion: "0.23.9", + SrcRelease: "2", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libffi6@3.2.1-8", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libpam-modules@1.1.8-3.6ubuntu2", + Name: "libpam-modules", + Version: "1.1.8", + Release: "3.6ubuntu2", + SrcName: "pam", + SrcVersion: "1.1.8", + SrcRelease: "3.6ubuntu2", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libpam-modules-bin@1.1.8-3.6ubuntu2", + Name: "libpam-modules-bin", + Version: "1.1.8", + Release: "3.6ubuntu2", + SrcName: "pam", + SrcVersion: "1.1.8", + SrcRelease: "3.6ubuntu2", + DependsOn: []string{ + "libaudit1@1:2.8.2-1ubuntu1", + "libc6@2.27-3ubuntu1", + "libpam0g@1.1.8-3.6ubuntu2", + "libselinux1@2.7-2build2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libpam-runtime@1.1.8-3.6ubuntu2", + Name: "libpam-runtime", + Version: "1.1.8", + Release: "3.6ubuntu2", + SrcName: "pam", + SrcVersion: "1.1.8", + SrcRelease: "3.6ubuntu2", + DependsOn: []string{ + "debconf@1.5.66", + "libpam-modules@1.1.8-3.6ubuntu2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libpam0g@1.1.8-3.6ubuntu2", + Name: "libpam0g", + Version: "1.1.8", + Release: "3.6ubuntu2", + SrcName: "pam", + SrcVersion: "1.1.8", + SrcRelease: "3.6ubuntu2", + DependsOn: []string{ + "debconf@1.5.66", + "libaudit1@1:2.8.2-1ubuntu1", + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libpcre3@2:8.39-9", + Name: "libpcre3", + Version: "8.39", + Epoch: 2, + Release: "9", + SrcName: "pcre3", + SrcVersion: "8.39", + SrcEpoch: 2, + SrcRelease: "9", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libprocps6@2:3.3.12-3ubuntu1.1", + Name: "libprocps6", + Version: "3.3.12", + Epoch: 2, + Release: "3ubuntu1.1", + SrcName: "procps", + SrcVersion: "3.3.12", + SrcRelease: "3ubuntu1.1", + SrcEpoch: 2, + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libsystemd0@237-3ubuntu10.3", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libseccomp2@2.3.1-2.1ubuntu4", + Name: "libseccomp2", + Version: "2.3.1", + Release: "2.1ubuntu4", + SrcName: "libseccomp", + SrcVersion: "2.3.1", + SrcRelease: "2.1ubuntu4", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libselinux1@2.7-2build2", + Name: "libselinux1", + Version: "2.7", + Release: "2build2", + SrcName: "libselinux", + SrcVersion: "2.7", + SrcRelease: "2build2", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libpcre3@2:8.39-9", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libsemanage-common@2.7-2build2", + Name: "libsemanage-common", + Version: "2.7", + Release: "2build2", + SrcName: "libsemanage", + SrcVersion: "2.7", + SrcRelease: "2build2", + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libsemanage1@2.7-2build2", + Name: "libsemanage1", + Version: "2.7", + Release: "2build2", + SrcName: "libsemanage", + SrcVersion: "2.7", + SrcRelease: "2build2", + DependsOn: []string{ + "libaudit1@1:2.8.2-1ubuntu1", + "libbz2-1.0@1.0.6-8.1", + "libc6@2.27-3ubuntu1", + "libselinux1@2.7-2build2", + "libsemanage-common@2.7-2build2", + "libsepol1@2.7-1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libsepol1@2.7-1", + Name: "libsepol1", + Version: "2.7", + Release: "1", + SrcName: "libsepol", + SrcVersion: "2.7", + SrcRelease: "1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libsmartcols1@2.31.1-0.4ubuntu3.1", + Name: "libsmartcols1", + Version: "2.31.1", + Release: "0.4ubuntu3.1", + SrcName: "util-linux", + SrcVersion: "2.31.1", + SrcRelease: "0.4ubuntu3.1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libss2@1.44.1-1", + Name: "libss2", + Version: "1.44.1", + Release: "1", + SrcName: "e2fsprogs", + SrcVersion: "1.44.1", + SrcRelease: "1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + "libcom-err2@1.44.1-1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libstdc++6@8-20180414-1ubuntu2", + Name: "libstdc++6", + Version: "8-20180414", + Release: "1ubuntu2", + SrcName: "gcc-8", + SrcVersion: "8-20180414", + SrcRelease: "1ubuntu2", + DependsOn: []string{ + "gcc-8-base@8-20180414-1ubuntu2", + "libc6@2.27-3ubuntu1", + "libgcc1@1:8-20180414-1ubuntu2", + }, + Maintainer: "Ubuntu Core developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libsystemd0@237-3ubuntu10.3", + Name: "libsystemd0", + Version: "237", + Release: "3ubuntu10.3", + SrcName: "systemd", + SrcVersion: "237", + SrcRelease: "3ubuntu10.3", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libtasn1-6@4.13-2", + Name: "libtasn1-6", + Version: "4.13", + Release: "2", + SrcName: "libtasn1-6", + SrcVersion: "4.13", + SrcRelease: "2", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libtinfo5@6.1-1ubuntu1.18.04", + Name: "libtinfo5", + Version: "6.1", + Release: "1ubuntu1.18.04", + SrcName: "ncurses", + SrcVersion: "6.1", + SrcRelease: "1ubuntu1.18.04", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libudev1@237-3ubuntu10.3", + Name: "libudev1", + Version: "237", + Release: "3ubuntu10.3", + SrcName: "systemd", + SrcVersion: "237", + SrcRelease: "3ubuntu10.3", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libunistring2@0.9.9-0ubuntu1", + Name: "libunistring2", + Version: "0.9.9", + Release: "0ubuntu1", + SrcName: "libunistring", + SrcVersion: "0.9.9", + SrcRelease: "0ubuntu1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libustr-1.0-1@1.0.4-3+b2", + Name: "libustr-1.0-1", + Version: "1.0.4", + Release: "3+b2", + SrcName: "ustr", + SrcVersion: "1.0.4", + SrcRelease: "3", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Vaclav Ovsik ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libuuid1@2.31.1-0.4ubuntu3.1", + Name: "libuuid1", + Version: "2.31.1", + Release: "0.4ubuntu3.1", + SrcName: "util-linux", + SrcVersion: "2.31.1", + SrcRelease: "0.4ubuntu3.1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libzstd1@1.3.3+dfsg-2ubuntu1", + Name: "libzstd1", + Version: "1.3.3+dfsg", + Release: "2ubuntu1", + SrcName: "libzstd", + SrcVersion: "1.3.3+dfsg", + SrcRelease: "2ubuntu1", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "login@1:4.5-1ubuntu1", + Name: "login", + Version: "4.5", + Epoch: 1, + Release: "1ubuntu1", + SrcName: "shadow", + SrcEpoch: 1, + SrcVersion: "4.5", + SrcRelease: "1ubuntu1", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "lsb-base@9.20170808ubuntu1", + Name: "lsb-base", + Version: "9.20170808ubuntu1", + SrcName: "lsb", + SrcVersion: "9.20170808ubuntu1", + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "mawk@1.3.3-17ubuntu3", + Name: "mawk", + Version: "1.3.3", + Release: "17ubuntu3", + SrcName: "mawk", + SrcVersion: "1.3.3", + SrcRelease: "17ubuntu3", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "mount@2.31.1-0.4ubuntu3.1", + Name: "mount", + Version: "2.31.1", + Release: "0.4ubuntu3.1", + SrcName: "util-linux", + SrcVersion: "2.31.1", + SrcRelease: "0.4ubuntu3.1", + DependsOn: []string{ + "util-linux@2.31.1-0.4ubuntu3.1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "ncurses-base@6.1-1ubuntu1.18.04", + Name: "ncurses-base", + Version: "6.1", + Release: "1ubuntu1.18.04", + SrcName: "ncurses", + SrcVersion: "6.1", + SrcRelease: "1ubuntu1.18.04", + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "ncurses-bin@6.1-1ubuntu1.18.04", + Name: "ncurses-bin", + Version: "6.1", + Release: "1ubuntu1.18.04", + SrcName: "ncurses", + SrcVersion: "6.1", + SrcRelease: "1ubuntu1.18.04", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "passwd@1:4.5-1ubuntu1", + Name: "passwd", + Epoch: 1, + Version: "4.5", + Release: "1ubuntu1", + SrcName: "shadow", + SrcEpoch: 1, + SrcVersion: "4.5", + SrcRelease: "1ubuntu1", + DependsOn: []string{ + "libaudit1@1:2.8.2-1ubuntu1", + "libc6@2.27-3ubuntu1", + "libpam-modules@1.1.8-3.6ubuntu2", + "libpam0g@1.1.8-3.6ubuntu2", + "libselinux1@2.7-2build2", + "libsemanage1@2.7-2build2", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "perl-base@5.26.1-6ubuntu0.2", + Name: "perl-base", + Version: "5.26.1", + Release: "6ubuntu0.2", + SrcName: "perl", + SrcVersion: "5.26.1", + SrcRelease: "6ubuntu0.2", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "procps@2:3.3.12-3ubuntu1.1", + Name: "procps", + Epoch: 2, + Version: "3.3.12", + Release: "3ubuntu1.1", + SrcName: "procps", + SrcEpoch: 2, + SrcVersion: "3.3.12", + SrcRelease: "3ubuntu1.1", + DependsOn: []string{ + "init-system-helpers@1.51", + "libc6@2.27-3ubuntu1", + "libncurses5@6.1-1ubuntu1.18.04", + "libncursesw5@6.1-1ubuntu1.18.04", + "libprocps6@2:3.3.12-3ubuntu1.1", + "libtinfo5@6.1-1ubuntu1.18.04", + "lsb-base@9.20170808ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "sed@4.4-2", + Name: "sed", + Version: "4.4", + Release: "2", + SrcName: "sed", + SrcVersion: "4.4", + SrcRelease: "2", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "sensible-utils@0.0.12", + Name: "sensible-utils", + Version: "0.0.12", + SrcName: "sensible-utils", + SrcVersion: "0.0.12", + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "sysvinit-utils@2.88dsf-59.10ubuntu1", + Name: "sysvinit-utils", + Version: "2.88dsf", + Release: "59.10ubuntu1", + SrcName: "sysvinit", + SrcVersion: "2.88dsf", + SrcRelease: "59.10ubuntu1", + DependsOn: []string{ + "init-system-helpers@1.51", + "libc6@2.27-3ubuntu1", + "util-linux@2.31.1-0.4ubuntu3.1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "tar@1.29b-2", + Name: "tar", + Version: "1.29b", + Release: "2", + SrcName: "tar", + SrcVersion: "1.29b", + SrcRelease: "2", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "ubuntu-keyring@2018.02.28", + Name: "ubuntu-keyring", + Version: "2018.02.28", + SrcName: "ubuntu-keyring", + SrcVersion: "2018.02.28", + Maintainer: "Dimitri John Ledkov ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "util-linux@2.31.1-0.4ubuntu3.1", + Name: "util-linux", + Version: "2.31.1", + Release: "0.4ubuntu3.1", + SrcName: "util-linux", + SrcVersion: "2.31.1", + SrcRelease: "0.4ubuntu3.1", + DependsOn: []string{ + "fdisk@2.31.1-0.4ubuntu3.1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "zlib1g@1:1.2.11.dfsg-0ubuntu2", + Name: "zlib1g", + Epoch: 1, + Version: "1.2.11.dfsg", + Release: "0ubuntu2", + SrcName: "zlib", + SrcEpoch: 1, + SrcVersion: "1.2.11.dfsg", + SrcRelease: "0ubuntu2", + DependsOn: []string{ + "libc6@2.27-3ubuntu1", + }, + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + }, + }, + }, + }, + }, + { + name: "corrupsed", + txtar: "testdata/corrupsed.txtar", + want: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: []types.Package{ + { + ID: "libgcc1@1:5.1.1-12ubuntu1", + Name: "libgcc1", + Version: "5.1.1", + Epoch: 1, + Release: "12ubuntu1", + SrcName: "gcc-5", + SrcVersion: "5.1.1", + SrcRelease: "12ubuntu1", + Maintainer: "Ubuntu Core developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libpam-modules-bin@1.1.8-3.1ubuntu3", + Name: "libpam-modules-bin", + Version: "1.1.8", + Release: "3.1ubuntu3", + SrcName: "pam", + SrcVersion: "1.1.8", + SrcRelease: "3.1ubuntu3", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "libpam-runtime@1.1.8-3.1ubuntu3", + Name: "libpam-runtime", + Version: "1.1.8", + Release: "3.1ubuntu3", + SrcName: "pam", + SrcVersion: "1.1.8", + SrcRelease: "3.1ubuntu3", + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "makedev@2.3.1-93ubuntu1", + Name: "makedev", + Version: "2.3.1", + Release: "93ubuntu1", + SrcName: "makedev", + SrcVersion: "2.3.1", + SrcRelease: "93ubuntu1", + Maintainer: "Ubuntu Developers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + }, + }, + }, + }, + }, + { + name: "only apt", + txtar: "testdata/only-apt.txtar", + want: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: []types.Package{ + { + ID: "apt@1.6.3ubuntu0.1", Name: "apt", Version: "1.6.3ubuntu0.1", + SrcName: "apt", SrcVersion: "1.6.3ubuntu0.1", + Maintainer: "Ubuntu Developers ", Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + }, + }, + }, + }, + }, + { + name: "happy path with digests", + txtar: "testdata/digest.txtar", + want: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: []types.Package{ + { + ID: "sed@4.4-2", + Name: "sed", + Version: "4.4", + Release: "2", + SrcName: "sed", + SrcVersion: "4.4", + SrcRelease: "2", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "tar@1.34+dfsg-1", + Name: "tar", + Version: "1.34+dfsg", + Release: "1", + SrcName: "tar", + SrcVersion: "1.34+dfsg", + SrcRelease: "1", + Maintainer: "Janos Lenart ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + Digest: "sha256:bd8e963c6edcf1c806df97cd73560794c347aa94b9aaaf3b88eea585bb2d2f3c", + }, + }, + }, + }, + }, + }, + { + name: "md5sums", + txtar: "testdata/md5sums.txtar", + want: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: types.Packages{ + { + ID: "tar@1.29b-2", + Name: "tar", + Version: "1.29b", + Release: "2", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + SrcName: "tar", + SrcVersion: "1.29b", + SrcRelease: "2", + Maintainer: "Ubuntu Developers ", + InstalledFiles: []string{ + "/usr/bin/tar", + "/usr/lib/mime/packages/tar", + "/usr/sbin/rmt-tar", + "/usr/sbin/tarcat", + "/usr/share/doc/tar/AUTHORS", + "/usr/share/doc/tar/NEWS.gz", + "/usr/share/doc/tar/README.Debian", + "/usr/share/doc/tar/THANKS.gz", + "/usr/share/doc/tar/changelog.Debian.gz", + "/usr/share/doc/tar/copyright", + "/usr/share/man/man1/tar.1.gz", + "/usr/share/man/man1/tarcat.1.gz", + "/usr/share/man/man8/rmt-tar.8.gz", + }, + }, + }, + }, + }, + SystemInstalledFiles: []string{ + "/usr/bin/tar", + "/usr/lib/mime/packages/tar", + "/usr/sbin/rmt-tar", + "/usr/sbin/tarcat", + "/usr/share/doc/tar/AUTHORS", + "/usr/share/doc/tar/NEWS.gz", + "/usr/share/doc/tar/README.Debian", + "/usr/share/doc/tar/THANKS.gz", + "/usr/share/doc/tar/changelog.Debian.gz", + "/usr/share/doc/tar/copyright", + "/usr/share/man/man1/tar.1.gz", + "/usr/share/man/man1/tarcat.1.gz", + "/usr/share/man/man8/rmt-tar.8.gz", + }, + }, + }, + { + name: "third-party package", + txtar: "testdata/third-party.txtar", + want: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: []types.Package{ + { + ID: "apt@1.6.3ubuntu0.1", + Name: "apt", + Version: "1.6.3ubuntu0.1", + SrcName: "apt", + SrcVersion: "1.6.3ubuntu0.1", + Maintainer: "Ubuntu Developers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + }, + { + ID: "docker-ce@5:20.10.7~3-0~debian-buster", + Name: "docker-ce", + Version: "20.10.7~3-0~debian", + Release: "buster", + Epoch: 5, + SrcName: "docker-ce", + SrcVersion: "20.10.7~3-0~debian", + SrcRelease: "buster", + SrcEpoch: 5, + Maintainer: "Docker ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassThirdParty}, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newDpkgAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + fsys := testutil.TxtarToFS(t, tt.txtar) + got, err := a.PostAnalyze(t.Context(), analyzer.PostAnalysisInput{ + FS: fsys, + }) + require.NoError(t, err) + + // Sort the result for consistency + for i := range got.PackageInfos { + sort.Sort(got.PackageInfos[i].Packages) + } + + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_isThirdPartyPackage(t *testing.T) { + tests := []struct { + name string + maintainer string + want bool + }{ + {"third-party (Docker)", "Docker ", true}, + {"third-party (GitHub - exact match)", "GitHub", true}, + {"official (Ubuntu)", "Ubuntu Developers ", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, isThirdPartyPackage(tt.maintainer)) + }) + } +} + +func Test_dpkgAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "status", + filePath: "var/lib/dpkg/status", + want: true, + }, + { + name: "status dir", + filePath: "var/lib/dpkg/status.d/gcc", + want: true, + }, + { + name: "*.md5sums file in status dir", + filePath: "var/lib/dpkg/status.d/base-files.md5sums", + want: true, + }, + { + name: "*.md5sums file in info dir", + filePath: "var/lib/dpkg/info/bash.md5sums", + want: true, + }, + { + name: "list file", + filePath: "var/lib/dpkg/info/bash.list", + want: false, + }, + { + name: "available file", + filePath: "var/lib/dpkg/available", + want: true, + }, + { + name: "sad path", + filePath: "var/lib/dpkg/status/bash.md5sums", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := newDpkgAnalyzer(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/pkg/dpkg/scanner.go b/pkg/fanal/analyzer/pkg/dpkg/scanner.go new file mode 100644 index 000000000000..d29fe951f652 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/scanner.go @@ -0,0 +1,55 @@ +package dpkg + +import ( + "bufio" + "bytes" + "io" + "net/textproto" +) + +type dpkgScanner struct { + *bufio.Scanner +} + +// NewScanner returns a new scanner that splits on empty lines. +func NewScanner(r io.Reader) *dpkgScanner { + s := bufio.NewScanner(r) + // Package data may exceed default buffer size + // Increase the buffer default size by 2 times + buf := make([]byte, 0, 128*1024) + s.Buffer(buf, 128*1024) + + s.Split(emptyLineSplit) + return &dpkgScanner{Scanner: s} +} + +// Scan advances the scanner to the next token. +func (s *dpkgScanner) Scan() bool { + return s.Scanner.Scan() +} + +// Header returns the MIME header of the current scan. +func (s *dpkgScanner) Header() (textproto.MIMEHeader, error) { + b := s.Bytes() + reader := textproto.NewReader(bufio.NewReader(bytes.NewReader(b))) + return reader.ReadMIMEHeader() +} + +// emptyLineSplit is a bufio.SplitFunc that splits on empty lines. +func emptyLineSplit(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + + if i := bytes.Index(data, []byte("\n\n")); i >= 0 { + // We have a full empty line terminated block. + return i + 2, data[0:i], nil + } + + if atEOF { + // Return the rest of the data if we're at EOF. + return len(data), data, nil + } + + return +} diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/all-patterns-copyright b/pkg/fanal/analyzer/pkg/dpkg/testdata/all-patterns-copyright new file mode 100644 index 000000000000..2f9ab10e8f81 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/all-patterns-copyright @@ -0,0 +1,22 @@ +Apt is copyright 1997, 1998, 1999 Jason Gunthorpe and others. +Apt is currently developed by APT Development Team . + +License: GPLv2+ + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +See /usr/share/common-licenses/GPL-2, or + for the terms of the latest version +of the GNU General Public License. diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/common-license-copyright b/pkg/fanal/analyzer/pkg/dpkg/testdata/common-license-copyright new file mode 100644 index 000000000000..1e82b8d57e6f --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/common-license-copyright @@ -0,0 +1,47 @@ +This package was first put together by Ian Murdock + and was maintained by Steve Phillips + from sources written for the Debian Project by Ian +Murdock, Ted Hajek , and Sven Rudolph +. + +Since Nov 27 1996, it was maintained by Guy Maor . He +rewrote most of it. + +Since May 20 2000, it is maintained by Roland Bauerschmidt +. + +Since March 24 2004, it is maintained by Roland Bauerschmidt +, and co-maintained by Marc Haber + + +Since 23 Oct 2005, it has been maintained by Joerg Hoh + +Since June 2006, it has been maintained by Stephen Gran + +deluser is Copyright (C) 2000 Roland Bauerschmidt +and based on the source code of adduser. + +adduser is Copyright (C) 1997, 1998, 1999 Guy Maor . +adduser is Copyright (C) 1995 Ted Hajek +with portions Copyright (C) 1994 Debian Association, Inc. + +The examples directory has been contributed by John Zaitseff, and is +GPL V2 as well. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the + Free Software Foundation, Inc., + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +On Debian GNU/Linux systems, the complete text of the GNU General +Public License can be found in `/usr/share/common-licenses/GPL-2'. diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/corrupsed.txtar b/pkg/fanal/analyzer/pkg/dpkg/testdata/corrupsed.txtar new file mode 100644 index 000000000000..b90915b39326 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/corrupsed.txtar @@ -0,0 +1,90 @@ +Test data for corrupted dpkg status file parsing. + +-- var/lib/dpkg/status -- +Package: libpam-runtime +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 300 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Source: pam +Version: 1.1.8-3.1ubuntu3 +Replaces: libpam0g-dev, libpam0g-util +Depends: debconf (>= 0.5) | debconf-2.0, debconf (>= 1.5.19) | cdebconf, libpam-modules (>= 1.0.1-6) +Conflicts: libpam0g-util +Conffiles: + /etc/pam.conf 87fc76f18e98ee7d3848f6b81b3391e5 + /etc/pam.d/other 31aa7f2181889ffb00b87df4126d1701 +Description: Runtime support for the PAM library + Contains configuration files and directories required for + authentication to work on Debian systems. This package is required + on almost all installations. +Homepage: http://pam.sourceforge.net/ +Original-Maintainer: Steve Langasek + +Package: libpam-modules-bin +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 299 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: pam +Version: 1.1.8-3.1ubuntu3 +Replaces: libpam-modules (<< 1.1.3-8) +Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.14), libpam0g (>= 0.99.7.1), libselinux1 (>= 1.32) +Description: Pluggable Authentication Modules for PAM - helper binaries + This package contains helper binaries used by the standard set of PAM + modules in the libpam-modules package. +Homepage: http://pam.sourceforge.net/ +Original-Maintainer: Steve Langasek + +Package: makedev +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 125 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Version: 2.3.1-93ubuntu1 +Depends: base-passwd (>= 3.0.4) +Conflicts: udev (<= 0.024-7) +Description: creates device files in /dev + The MAKEDEV executable is used to create device files, often in /dev. + . + Device files are special files through which applications can interact + with hardware. + . + This package is not necessary for most modern Linux systems, where the udev + subsystem provides a more dynamic mechanism for device file management. +Original-Maintainer: Debian QA Group + +Package: brokenPackageWithNoVersionThatShouldGetThrownOut + +Package: libgcc1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 131 +Maintainer: Ubuntu Core developers +Architecture: amd64 +Multi-Arch: same +Source: gcc-5 (5.1.1-12ubuntu1) +Version: 1:5.1.1-12ubuntu1 +Depends: gcc-5-base (= 5.1.1-12ubuntu1), libc6 (>= 2.14) +Pre-Depends: multiarch-support +Breaks: gcc-4.3 (<< 4.3.6-1), gcc-4.4 (<< 4.4.6-4), gcc-4.5 (<< 4.5.3-2) +Description: GCC support library + Shared version of the support library, a library of internal subroutines + that GCC uses to overcome shortcomings of particular machines, or + special needs for some languages. +Homepage: http://gcc.gnu.org/ +Original-Maintainer: Debian GCC Maintainers + +Package: invalidpkg +Source: invalidpkg-5 (5.#) +Version: 1:5.# diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/digest.txtar b/pkg/fanal/analyzer/pkg/dpkg/testdata/digest.txtar new file mode 100644 index 000000000000..884b7e622ff4 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/digest.txtar @@ -0,0 +1,92 @@ +Test data for dpkg status with digests from available file. + +-- var/lib/dpkg/status -- +Package: sed +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 320 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 4.4-2 +Pre-Depends: libc6 (>= 2.14), libselinux1 (>= 1.32) +Description: GNU stream editor for filtering/transforming text + sed reads the specified files or the standard input if no + files are specified, makes editing changes according to a + list of commands, and writes the results to the standard + output. +Original-Maintainer: Clint Adams +Homepage: https://www.gnu.org/software/sed/ + +Package: tar +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 3152 +Maintainer: Janos Lenart +Architecture: amd64 +Multi-Arch: foreign +Version: 1.34+dfsg-1 +Replaces: cpio (<< 2.4.2-39) +Pre-Depends: libacl1 (>= 2.2.23), libc6 (>= 2.28), libselinux1 (>= 3.1~) +Suggests: bzip2, ncompress, xz-utils, tar-scripts, tar-doc +Breaks: dpkg-dev (<< 1.14.26) +Conflicts: cpio (<= 2.4.2-38) +Description: GNU version of the tar archiving utility + Tar is a program for packaging a set of files as a single archive in tar + format. The function it performs is conceptually similar to cpio, and to + things like PKZIP in the DOS world. It is heavily used by the Debian package + management system, and is useful for performing system backups and exchanging + sets of files with others. +Homepage: https://www.gnu.org/software/tar/ + +-- var/lib/dpkg/available -- +Package: tar +Version: 1.34+dfsg-1 +Essential: yes +Installed-Size: 3152 +Maintainer: Janos Lenart +Architecture: amd64 +Replaces: cpio (<< 2.4.2-39) +Pre-Depends: libacl1 (>= 2.2.23), libc6 (>= 2.28), libselinux1 (>= 3.1~) +Suggests: bzip2, ncompress, xz-utils, tar-scripts, tar-doc +Conflicts: cpio (<= 2.4.2-38) +Breaks: dpkg-dev (<< 1.14.26) +Description: GNU version of the tar archiving utility +Multi-Arch: foreign +Homepage: https://www.gnu.org/software/tar/ +Description-md5: 48033bf96442788d1f697785773ad9bb +Tag: admin::backup, admin::file-distribution, devel::packaging, + implemented-in::c, interface::commandline, role::program, + scope::utility, suite::gnu, use::storing, works-with-format::tar, + works-with::archive, works-with::file +Section: utils +Priority: required +Filename: pool/main/t/tar/tar_1.34+dfsg-1_amd64.deb +Size: 846756 +MD5sum: 483cd0fa2cc7139193af5fdfb9e1d7c1 +SHA256: bd8e963c6edcf1c806df97cd73560794c347aa94b9aaaf3b88eea585bb2d2f3c + +Package: sed +Version: 4.7-1 +Essential: yes +Installed-Size: 883 +Maintainer: Clint Adams +Architecture: amd64 +Pre-Depends: libacl1 (>= 2.2.51-8), libc6 (>= 2.14), libselinux1 (>= 1.32) +Description: GNU stream editor for filtering/transforming text +Multi-Arch: foreign +Homepage: https://www.gnu.org/software/sed/ +Description-md5: 2ed71305ee7a49ce4438c58140980d2f +Tag: implemented-in::c, interface::commandline, role::program, + scope::utility, suite::gnu, use::editing, works-with::file, + works-with::text +Section: utils +Priority: required +Filename: pool/main/s/sed/sed_4.7-1_amd64.deb +Size: 309632 +MD5sum: dee55bfb08935c00028641671bcaffeb +SHA256: 37917c63d062cd924ba5084820353b256377d88371fbb89537ff06f7a0b5a6c7 \ No newline at end of file diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/license-pattern-and-classifier-copyright b/pkg/fanal/analyzer/pkg/dpkg/testdata/license-pattern-and-classifier-copyright new file mode 100644 index 000000000000..e87793deb237 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/license-pattern-and-classifier-copyright @@ -0,0 +1,83 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: zlib +Upstream-Contact: zlib@gzip.org +Source: http://zlib.net/ +Comment: This is the pre-packaged Debian Linux version of the zlib compression + library. It was packaged by Michael Alan Dorman + from sources originally retrieved from ftp.uu.net in the directory + /pub/archiving/zip/zlib as the file zlib-1.0.4.tar.gz. + . + The deflate format used by zlib was defined by Phil Katz. The deflate + and zlib specifications were written by Peter Deutsch. Thanks to all the + people who reported problems and suggested various improvements in zlib; + they are too numerous to cite here. +Files-Excluded: + contrib/ada + contrib/amd64 + contrib/asm686 + contrib/blast + contrib/delphi + contrib/dotzlib + contrib/gcc_gvmat64 + contrib/infback9 + contrib/inflate86 + contrib/iostream + contrib/iostream2 + contrib/iostream3 + contrib/masmx64 + contrib/masmx86 + contrib/pascal + contrib/puff + contrib/testzlib + contrib/untgz + contrib/vstudio + doc/rfc1950.txt + doc/rfc1951.txt + doc/rfc1952.txt + +Files: * +Copyright: 1995-2013 Jean-loup Gailly and Mark Adler +License: GPL-1+ or Artistic, and BSD-4-clause-POWERDOG + +Files: amiga/Makefile.pup +Copyright: 1998 by Andreas R. Kleinert +License: Zlib + +Files: contrib/minizip/* +Copyright: 1998-2010 Gilles Vollant + 2007-2008 Even Rouault + 2009-2010 Mathias Svensson +License: Zlib, + +Files: debian/* +Copyright: 2000-2017 Mark Brown +License: Zlib + +License: Zlib + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + . + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + . + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + . + Jean-loup Gailly Mark Adler + jloup@gzip.org madler@alumni.caltech.edu + . + If you use the zlib library in a product, we would appreciate *not* receiving + lengthy legal documents to sign. The sources are provided for free but without + warranty of any kind. The library has been entirely written by Jean-loup + Gailly and Mark Adler; it does not include third-party code. + . + If you redistribute modified sources, we would appreciate that you include in + the file ChangeLog history information documenting your changes. Please read + the FAQ for more information on the distribution of modified source versions. diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/md5sums.txtar b/pkg/fanal/analyzer/pkg/dpkg/testdata/md5sums.txtar new file mode 100644 index 000000000000..878eebc5a475 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/md5sums.txtar @@ -0,0 +1,34 @@ +Test data for dpkg with md5sums file. + +-- var/lib/dpkg/status -- +Package: tar +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 864 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 1.29b-2 +Replaces: cpio (<< 2.4.2-39) +Pre-Depends: libacl1 (>= 2.2.51-8), libc6 (>= 2.17), libselinux1 (>= 1.32) +Suggests: bzip2, ncompress, xz-utils, tar-scripts, tar-doc +Breaks: dpkg-dev (<< 1.14.26) +Conflicts: cpio (<= 2.4.2-38) +Description: GNU version of the tar archiving utility + +-- var/lib/dpkg/info/tar.md5sums -- +25de5fcdc3c8ebd9c9f599fb7a899b40 usr/bin/tar +5bf0e62990e0b668830ceb2c8615b497 usr/lib/mime/packages/tar +de1096fbccdc14324196fc6829324ebc usr/sbin/rmt-tar +fd2fab77cf4da2288c11a4de2c0c7fe0 usr/sbin/tarcat +020511595e8fb6a77b29032b3ec4ab10 usr/share/doc/tar/AUTHORS +5386c77b19064999a340c62ec50b9307 usr/share/doc/tar/NEWS.gz +882d20d9ef9c5baf1557a921bb9c6251 usr/share/doc/tar/README.Debian +63b372367a085f8d07c4127828c56999 usr/share/doc/tar/THANKS.gz +e9c4ef9b5dffd7b5b94f019d28bf86d6 usr/share/doc/tar/changelog.Debian.gz +4546fba65a596c8793dca393b32ad9ee usr/share/doc/tar/copyright +0502616f764289571b974ecc5adc1209 usr/share/man/man1/tar.1.gz +9376d82eb54e507863d32114dddd3de6 usr/share/man/man1/tarcat.1.gz +4892b2039586e61ef7b290591e9743e5 usr/share/man/man8/rmt-tar.8.gz \ No newline at end of file diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/no-license-copyright b/pkg/fanal/analyzer/pkg/dpkg/testdata/no-license-copyright new file mode 100644 index 000000000000..c536ab78f3b6 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/no-license-copyright @@ -0,0 +1,10 @@ +This is the Debian prepackaged version of the Time Zone and Daylight +Saving Time Data. + +It was downloaded from http://www.iana.org/time-zones + +Upstream Author: The Internet Assigned Numbers Authority (IANA) +Commentary should be addressed to tz@iana.org + +Copyright: This database is in the public domain. + diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/only-apt.txtar b/pkg/fanal/analyzer/pkg/dpkg/testdata/only-apt.txtar new file mode 100644 index 000000000000..0eafb4f4e98a --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/only-apt.txtar @@ -0,0 +1,38 @@ +Test data for single apt package. + +-- var/lib/dpkg/status -- +Package: apt +Status: install ok installed +Priority: important +Section: admin +Installed-Size: 3806 +Maintainer: Ubuntu Developers +Architecture: amd64 +Version: 1.6.3ubuntu0.1 +Replaces: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~) +Provides: apt-transport-https (= 1.6.3ubuntu0.1) +Depends: adduser, gpgv | gpgv2 | gpgv1, ubuntu-keyring, libapt-pkg5.0 (>= 1.6.3ubuntu0.1), libc6 (>= 2.15), libgcc1 (>= 1:3.0), libgnutls30 (>= 3.5.6), libseccomp2 (>= 1.0.1), libstdc++6 (>= 5.2) +Recommends: ca-certificates +Suggests: apt-doc, aptitude | synaptic | wajig, dpkg-dev (>= 1.17.2), gnupg | gnupg2 | gnupg1, powermgmt-base +Breaks: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~), aptitude (<< 0.8.10) +Conffiles: + /etc/apt/apt.conf.d/01-vendor-ubuntu 5232396660502461fc834c0a1229dbe4 + /etc/apt/apt.conf.d/01autoremove 735050108d38dbf99e933a9f647f6892 + /etc/cron.daily/apt-compat 49e9b2cfa17849700d4db735d04244f3 + /etc/kernel/postinst.d/apt-auto-removal 4ad976a68f045517cf4696cec7b8aa3a + /etc/logrotate.d/apt 179f2ed4f85cbaca12fa3d69c2a4a1c3 +Description: commandline package manager + This package provides commandline tools for searching and + managing as well as querying information about packages + as a low-level access to all features of the libapt-pkg library. + . + These include: + * apt-get for retrieval of packages and information about them + from authenticated sources and for installation, upgrade and + removal of packages together with their dependencies + * apt-cache for querying available information about installed + as well as installable packages + * apt-cdrom to use removable media as a source for packages + * apt-config as an interface to the configuration settings + * apt-key as an interface to manage authentication keys +Original-Maintainer: APT Development Team diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/third-party.txtar b/pkg/fanal/analyzer/pkg/dpkg/testdata/third-party.txtar new file mode 100644 index 000000000000..cc6d81a2bb94 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/third-party.txtar @@ -0,0 +1,30 @@ +Test data for third-party package detection. + +-- var/lib/dpkg/status -- +Package: docker-ce +Status: install ok installed +Priority: optional +Section: admin +Installed-Size: 83560 +Maintainer: Docker +Architecture: amd64 +Version: 5:20.10.7~3-0~debian-buster +Replaces: docker, docker-ce +Depends: containerd.io (>= 1.4.1), docker-ce-cli, iptables, libc6 (>= 2.8), libseccomp2 (>= 2.4.1), libc6 (>= 2.17), libdevmapper1.02.1 (>= 2:1.02.97) +Recommends: ca-certificates, docker-ce-rootless-extras, git, pigz, xz-utils +Description: Docker: the open-source application container engine + Docker is a product for you to build, ship and run any application as a + lightweight container. +Homepage: https://www.docker.com + +Package: apt +Status: install ok installed +Priority: important +Section: admin +Installed-Size: 4148 +Maintainer: Ubuntu Developers +Architecture: amd64 +Version: 1.6.3ubuntu0.1 +Depends: base-files (>= 7.2ubuntu5.6) +Description: commandline package manager + diff --git a/pkg/fanal/analyzer/pkg/dpkg/testdata/valid.txtar b/pkg/fanal/analyzer/pkg/dpkg/testdata/valid.txtar new file mode 100644 index 000000000000..8d98696b3f6d --- /dev/null +++ b/pkg/fanal/analyzer/pkg/dpkg/testdata/valid.txtar @@ -0,0 +1,2148 @@ +Test data for valid dpkg status file parsing. + +-- var/lib/dpkg/status -- +Package: fdisk +Status: install ok installed +Priority: important +Section: utils +Installed-Size: 427 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: util-linux +Version: 2.31.1-0.4ubuntu3.1 +Replaces: util-linux (<< 2.30.1-0ubuntu4~) +Depends: libc6 (>= 2.14), libfdisk1 (>= 2.31.1), libmount1 (>= 2.24.2), libncursesw5 (>= 6), libsmartcols1 (>= 2.28~rc1), libtinfo5 (>= 6) +Breaks: util-linux (<< 2.30.1-0ubuntu4~) +Description: collection of partitioning utilities + This package contains the classic fdisk, sfdisk and cfdisk partitioning + utilities from the util-linux suite. + . + The utilities included in this package allow you to partition + your hard disk. The utilities supports both modern and legacy + partition tables (eg. GPT, MBR, etc). + . + The fdisk utility is the classical text-mode utility. + The cfdisk utilitity gives a more userfriendly curses based interface. + The sfdisk utility is mostly for automation and scripting uses. +Important: yes +Original-Maintainer: LaMont Jones + +Package: libpam-runtime +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 300 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Source: pam +Version: 1.1.8-3.6ubuntu2 +Replaces: libpam0g-dev, libpam0g-util +Depends: debconf (>= 0.5) | debconf-2.0, debconf (>= 1.5.19) | cdebconf, libpam-modules (>= 1.0.1-6) +Conflicts: libpam0g-util +Conffiles: + /etc/pam.conf 87fc76f18e98ee7d3848f6b81b3391e5 + /etc/pam.d/other 31aa7f2181889ffb00b87df4126d1701 +Description: Runtime support for the PAM library + Contains configuration files and directories required for + authentication to work on Debian systems. This package is required + on almost all installations. +Homepage: http://www.linux-pam.org/ +Original-Maintainer: Steve Langasek + +Package: libncurses5 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 283 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: ncurses +Version: 6.1-1ubuntu1.18.04 +Depends: libtinfo5 (= 6.1-1ubuntu1.18.04), libc6 (>= 2.14) +Recommends: libgpm2 +Description: shared libraries for terminal handling + The ncurses library routines are a terminal-independent method of + updating character screens with reasonable optimization. + . + This package contains the shared libraries necessary to run programs + compiled with ncurses. +Homepage: https://invisible-island.net/ncurses/ +Original-Maintainer: Craig Small + +Package: libcom-err2 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 86 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: e2fsprogs +Version: 1.44.1-1 +Replaces: libcomerr2 (<< 1.43.9-1~) +Provides: libcomerr2 (= 1.44.1-1) +Depends: libc6 (>= 2.17) +Breaks: libcomerr2 (<< 1.43.9-1~) +Description: common error description library + libcomerr is an attempt to present a common error-handling mechanism to + manipulate the most common form of error code in a fashion that does not + have the problems identified with mechanisms commonly in use. +Original-Maintainer: Theodore Y. Ts'o +Homepage: http://e2fsprogs.sourceforge.net + +Package: libapt-pkg5.0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 3108 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: apt +Version: 1.6.3ubuntu0.1 +Provides: libapt-pkg (= 1.6.3ubuntu0.1) +Depends: libbz2-1.0, libc6 (>= 2.27), libgcc1 (>= 1:3.0), liblz4-1 (>= 0.0~r127), liblzma5 (>= 5.1.1alpha+20120614), libstdc++6 (>= 5.2), libudev1 (>= 183), libzstd1 (>= 1.3.2), zlib1g (>= 1:1.2.2.3) +Recommends: apt (>= 1.6.3ubuntu0.1) +Breaks: appstream (<< 0.9.0-3~), apt (<< 1.1~exp14), libapt-inst1.5 (<< 0.9.9~) +Description: package management runtime library + This library provides the common functionality for searching and + managing packages as well as information about packages. + Higher-level package managers can depend upon this library. + . + This includes: + * retrieval of information about packages from multiple sources + * retrieval of packages and all dependent packages + needed to satisfy a request either through an internal + solver or by interfacing with an external one + * authenticating the sources and validating the retrieved data + * installation and removal of packages in the system + * providing different transports to retrieve data over cdrom, ftp, + http(s), rsh as well as an interface to add more transports like + tor+http(s) (apt-transport-tor). +Original-Maintainer: APT Development Team + +Package: libaudit1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 147 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: audit +Version: 1:2.8.2-1ubuntu1 +Depends: libaudit-common (>= 1:2.8.2-1ubuntu1), libc6 (>= 2.14), libcap-ng0 +Description: Dynamic library for security auditing + The audit-libs package contains the dynamic libraries needed for + applications to use the audit framework. It is used to monitor systems for + security related events. +Homepage: https://people.redhat.com/sgrubb/audit/ +Original-Maintainer: Laurent Bigonville + +Package: libtinfo5 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 497 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: ncurses +Version: 6.1-1ubuntu1.18.04 +Replaces: libncurses5 (<< 5.9-3) +Depends: libc6 (>= 2.16) +Breaks: dialog (<< 1.2-20130523) +Description: shared low-level terminfo library for terminal handling + The ncurses library routines are a terminal-independent method of + updating character screens with reasonable optimization. + . + This package contains the shared low-level terminfo library. +Homepage: https://invisible-island.net/ncurses/ +Original-Maintainer: Craig Small + +Package: perl-base +Essential: yes +Status: install ok installed +Priority: required +Section: perl +Installed-Size: 7818 +Maintainer: Ubuntu Developers +Architecture: amd64 +Source: perl +Version: 5.26.1-6ubuntu0.2 +Replaces: libfile-path-perl (<< 2.12.01), libfile-temp-perl (<< 0.2304), libio-socket-ip-perl (<< 0.38), libscalar-list-utils-perl (<< 1:1.46.02), libsocket-perl (<< 2.020.03), libxsloader-perl (<< 0.27), perl (<< 5.10.1-12), perl-modules (<< 5.20.1-3) +Provides: libfile-path-perl, libfile-temp-perl, libio-socket-ip-perl, libscalar-list-utils-perl, libsocket-perl, libxsloader-perl, perlapi-5.26.0, perlapi-5.26.1 +Pre-Depends: libc6 (>= 2.23), dpkg (>= 1.17.17) +Suggests: perl +Breaks: amanda-common (<< 1:3.3.9-2), autoconf2.13 (<< 2.13-45), backuppc (<< 3.3.1-2), debconf (<< 1.5.61), dh-haskell (<< 0.3), intltool (<< 0.51.0-4), libalien-wxwidgets-perl (<< 0.65+dfsg-2), libanyevent-perl (<< 7.070-2), libcommon-sense-perl (<< 3.72-2~), libexception-class-perl (<< 1.42), libfile-path-perl (<< 2.12.01), libfile-spec-perl (<< 3.6700), libfile-temp-perl (<< 0.2304), libgtk2-perl-doc (<< 2:1.2491-4), libio-socket-ip-perl (<< 0.38), libjcode-perl (<< 2.13-3), libmarc-charset-perl (<< 1.2), libsbuild-perl (<< 0.67.0-1), libscalar-list-utils-perl (<< 1:1.46.02), libsocket-perl (<< 2.020.03), libxsloader-perl (<< 0.27), mailagent (<< 1:3.1-81-2), pdl (<< 1:2.007-4), perl (<< 5.26.1~), perl-modules (<< 5.26.1~), slic3r (<< 1.2.9+dfsg-6.1), slic3r-prusa (<< 1.37.0+dfsg-1.1), texinfo (<< 6.1.0.dfsg.1-8) +Conflicts: defoma (<< 0.11.12), doc-base (<< 0.10.3), mono-gac (<< 2.10.8.1-3), safe-rm (<< 0.8), update-inetd (<< 4.41) +Description: minimal Perl system + Perl is a scripting language used in many system scripts and utilities. + . + This package provides a Perl interpreter and the small subset of the + standard run-time library required to perform basic tasks. For a full + Perl installation, install "perl" (and its dependencies, "perl-modules-5.26" + and "perl-doc"). +Homepage: http://dev.perl.org/perl5/ +Original-Maintainer: Niko Tyni + +Package: libudev1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 225 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: systemd +Version: 237-3ubuntu10.3 +Depends: libc6 (>= 2.25) +Description: libudev shared library + This library provides access to udev device information. +Homepage: https://www.freedesktop.org/wiki/Software/systemd +Original-Maintainer: Debian systemd Maintainers + +Package: libunistring2 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 1569 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libunistring +Version: 0.9.9-0ubuntu1 +Depends: libc6 (>= 2.14) +Description: Unicode string library for C + The 'libunistring' library implements Unicode strings (in the UTF-8, + UTF-16, and UTF-32 encodings), together with functions for Unicode + characters (character names, classifications, properties) and + functions for string processing (formatted output, width, word + breaks, line breaks, normalization, case folding, regular + expressions). + . + This package contains the shared library. +Original-Maintainer: Jörg Frings-Fürst +Homepage: http://www.gnu.org/software/libunistring/ + +Package: libnettle6 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 373 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: nettle +Version: 3.4-1 +Depends: libc6 (>= 2.14) +Description: low level cryptographic library (symmetric and one-way cryptos) + Nettle is a cryptographic library that is designed to fit easily in more or + less any context: In crypto toolkits for object-oriented languages (C++, + Python, Pike, ...), in applications like LSH or GNUPG, or even in kernel + space. + . + It tries to solve a problem of providing a common set of cryptographic + algorithms for higher-level applications by implementing a + context-independent set of cryptographic algorithms. In that light, Nettle + doesn't do any memory allocation or I/O, it simply provides the + cryptographic algorithms for the application to use in any environment and + in any way it needs. + . + This package contains the symmetric and one-way cryptographic + algorithms. To avoid having this package depend on libgmp, the + asymmetric cryptos reside in a separate library, libhogweed. +Original-Maintainer: Magnus Holmgren +Homepage: http://www.lysator.liu.se/~nisse/nettle/ + +Package: libattr1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 36 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: attr +Version: 1:2.4.47-2build1 +Depends: libc6 (>= 2.4) +Conflicts: attr (<< 2.0.0) +Description: Extended attribute shared library + Contains the runtime environment required by programs that make use + of extended attributes. +Original-Maintainer: Anibal Monsalve Salazar +Homepage: http://savannah.nongnu.org/projects/attr/ + +Package: libss2 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 98 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: e2fsprogs +Version: 1.44.1-1 +Replaces: e2fsprogs (<< 1.34-1) +Depends: libcom-err2, libc6 (>= 2.17) +Description: command-line interface parsing library + libss provides a simple command-line interface parser which will + accept input from the user, parse the command into an argv argument + vector, and then dispatch it to a handler function. + . + It was originally inspired by the Multics SubSystem library. +Original-Maintainer: Theodore Y. Ts'o +Homepage: http://e2fsprogs.sourceforge.net + +Package: liblzma5 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 309 +Maintainer: Jonathan Nieder +Architecture: amd64 +Multi-Arch: same +Source: xz-utils (5.1.1alpha+20120614-2) +Version: 5.1.1alpha+20120614-2+b3 +Depends: libc6 (>= 2.14) +Pre-Depends: multiarch-support +Description: XZ-format compression library + XZ is the successor to the Lempel-Ziv/Markov-chain Algorithm + compression format, which provides memory-hungry but powerful + compression (often better than bzip2) and fast, easy decompression. + . + The native format of liblzma is XZ; it also supports raw (headerless) + streams and the older LZMA format used by lzma. (For 7-Zip's related + format, use the p7zip package instead.) +Homepage: http://tukaani.org/xz/ + +Package: libidn2-0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 146 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libidn2 +Version: 2.0.4-1.1build2 +Depends: libc6 (>= 2.14), libunistring2 (>= 0.9.7) +Description: Internationalized domain names (IDNA2008/TR46) library + Libidn2 implements the revised algorithm for internationalized domain + names called IDNA2008/TR46. + . + This package contains runtime libraries. +Original-Maintainer: Debian Libidn team +Homepage: https://www.gnu.org/software/libidn/#libidn2 + +Package: libpam-modules-bin +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 267 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: pam +Version: 1.1.8-3.6ubuntu2 +Replaces: libpam-modules (<< 1.1.3-8) +Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.14), libpam0g (>= 0.99.7.1), libselinux1 (>= 1.32) +Description: Pluggable Authentication Modules for PAM - helper binaries + This package contains helper binaries used by the standard set of PAM + modules in the libpam-modules package. +Homepage: http://www.linux-pam.org/ +Original-Maintainer: Steve Langasek + +Package: grep +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 508 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 3.1-2 +Provides: rgrep +Depends: dpkg (>= 1.15.4) | install-info +Pre-Depends: libc6 (>= 2.14), libpcre3 +Suggests: libpcre3 (>= 7.7) +Conflicts: rgrep +Description: GNU grep, egrep and fgrep + 'grep' is a utility to search for text in files; it can be used from the + command line or in scripts. Even if you don't want to use it, other packages + on your system probably will. + . + The GNU family of grep utilities may be the "fastest grep in the west". + GNU grep is based on a fast lazy-state deterministic matcher (about + twice as fast as stock Unix egrep) hybridized with a Boyer-Moore-Gosper + search for a fixed string that eliminates impossible text from being + considered by the full regexp matcher without necessarily having to + look at every character. The result is typically many times faster + than Unix grep or egrep. (Regular expressions containing backreferencing + will run more slowly, however.) +Original-Maintainer: Anibal Monsalve Salazar +Homepage: http://www.gnu.org/software/grep/ + +Package: base-passwd +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 228 +Maintainer: Colin Watson +Architecture: amd64 +Multi-Arch: foreign +Version: 3.5.44 +Replaces: base +Depends: libc6 (>= 2.8), libdebconfclient0 (>= 0.145) +Recommends: debconf (>= 0.5) | debconf-2.0 +Description: Debian base system master password and group files + These are the canonical master copies of the user database files + (/etc/passwd and /etc/group), containing the Debian-allocated user and + group IDs. The update-passwd tool is provided to keep the system databases + synchronized with these master files. + +Package: liblz4-1 +Status: install ok installed +Priority: extra +Section: libs +Installed-Size: 132 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: lz4 +Version: 0.0~r131-2ubuntu3 +Depends: libc6 (>= 2.14) +Description: Fast LZ compression algorithm library - runtime + LZ4 is a very fast lossless compression algorithm, providing compression speed + at 400 MB/s per core, scalable with multi-cores CPU. It also features an + extremely fast decoder, with speed in multiple GB/s per core, typically + reaching RAM speed limits on multi-core systems. + . + This package includes the shared library. +Homepage: https://github.com/Cyan4973/lz4 +Original-Maintainer: Nobuhiro Iwamatsu + +Package: debianutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 212 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 4.8.4 +Pre-Depends: libc6 (>= 2.15) +Description: Miscellaneous utilities specific to Debian + This package provides a number of small utilities which are used + primarily by the installation scripts of Debian packages, although + you may use them directly. + . + The specific utilities included are: + add-shell installkernel ischroot remove-shell run-parts savelog + tempfile which +Original-Maintainer: Clint Adams + +Package: libgcrypt20 +Status: install ok installed +Priority: standard +Section: libs +Installed-Size: 1209 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Version: 1.8.1-4ubuntu1.1 +Depends: libc6 (>= 2.15), libgpg-error0 (>= 1.25) +Suggests: rng-tools +Description: LGPL Crypto library - runtime library + libgcrypt contains cryptographic functions. Many important free + ciphers, hash algorithms and public key signing algorithms have been + implemented: + . + Arcfour, Blowfish, CAST5, DES, AES, Twofish, Serpent, rfc2268 (rc2), SEED, + Poly1305, Camellia, ChaCha20, IDEA, Salsa, Blake-2, CRC, MD2, MD4, MD5, + RIPE-MD160, SHA-1, SHA-256, SHA-512, SHA3-224, SHA3-256, SHA3-384, SHA3-512, + SHAKE128, SHAKE256, Tiger, Whirlpool, DSA, DSA2, ElGamal, RSA, ECC + (Curve25519, sec256k1, GOST R 34.10-2001 and GOST R 34.10-2012, etc.) +Homepage: http://directory.fsf.org/project/libgcrypt/ +Original-Maintainer: Debian GnuTLS Maintainers + +Package: libncursesw5 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 343 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: ncurses +Version: 6.1-1ubuntu1.18.04 +Depends: libtinfo5 (= 6.1-1ubuntu1.18.04), libc6 (>= 2.14) +Recommends: libgpm2 +Description: shared libraries for terminal handling (wide character support) + The ncurses library routines are a terminal-independent method of + updating character screens with reasonable optimization. + . + This package contains the shared libraries necessary to run programs + compiled with ncursesw, which includes support for wide characters. +Homepage: https://invisible-island.net/ncurses/ +Original-Maintainer: Craig Small + +Package: bash +Essential: yes +Status: install ok installed +Priority: required +Section: shells +Installed-Size: 1588 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 4.4.18-2ubuntu1 +Replaces: bash-completion (<< 20060301-0), bash-doc (<= 2.05-1) +Depends: base-files (>= 2.1.12), debianutils (>= 2.15) +Pre-Depends: libc6 (>= 2.15), libtinfo5 (>= 6) +Recommends: bash-completion (>= 20060301-0) +Suggests: bash-doc +Conflicts: bash-completion (<< 20060301-0) +Conffiles: + /etc/bash.bashrc 3aa8b92d1dd6ddf4daaedc019662f1dc + /etc/skel/.bash_logout 22bfb8c1dd94b5f3813a2b25da67463f + /etc/skel/.bashrc 1f98b8f3f3c8f8927eca945d59dcc1c6 + /etc/skel/.profile f4e81ade7d6f9fb342541152d08e7a97 +Description: GNU Bourne Again SHell + Bash is an sh-compatible command language interpreter that executes + commands read from the standard input or from a file. Bash also + incorporates useful features from the Korn and C shells (ksh and csh). + . + Bash is ultimately intended to be a conformant implementation of the + IEEE POSIX Shell and Tools specification (IEEE Working Group 1003.2). + . + The Programmable Completion Code, by Ian Macdonald, is now found in + the bash-completion package. +Homepage: http://tiswww.case.edu/php/chet/bash/bashtop.html +Original-Maintainer: Matthias Klose + +Package: libuuid1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 116 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.31.1-0.4ubuntu3.1 +Replaces: e2fsprogs (<< 1.34-1) +Depends: libc6 (>= 2.25) +Recommends: uuid-runtime +Description: Universally Unique ID library + The libuuid library generates and parses 128-bit Universally Unique + IDs (UUIDs). A UUID is an identifier that is unique within the space + of all such identifiers across both space and time. It can be used for + multiple purposes, from tagging objects with an extremely short lifetime + to reliably identifying very persistent objects across a network. + . + See RFC 4122 for more information. +Original-Maintainer: LaMont Jones + +Package: libdb5.3 +Status: install ok installed +Priority: standard +Section: libs +Installed-Size: 1729 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: db5.3 +Version: 5.3.28-13.1ubuntu1 +Depends: libc6 (>= 2.17) +Description: Berkeley v5.3 Database Libraries [runtime] + This is the runtime package for programs that use the v5.3 Berkeley + database library. +Homepage: http://www.oracle.com/technetwork/database/database-technologies/berkeleydb/overview/index.html +Original-Maintainer: Debian Berkeley DB Group + +Package: debconf +Status: install ok installed +Priority: important +Section: admin +Installed-Size: 545 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Version: 1.5.66 +Replaces: debconf-tiny +Provides: debconf-2.0 +Pre-Depends: perl-base (>= 5.20.1-3~) +Recommends: apt-utils (>= 0.5.1), debconf-i18n +Suggests: debconf-doc, debconf-utils, whiptail | dialog, libterm-readline-gnu-perl, libgtk3-perl, libnet-ldap-perl, perl, libqtgui4-perl, libqtcore4-perl +Breaks: apt-listchanges (<< 3.14), ubiquity (<< 17.10.2), update-notifier-common (<< 3.187~) +Conflicts: apt (<< 0.3.12.1), cdebconf (<< 0.96), debconf-tiny, debconf-utils (<< 1.3.22), dialog (<< 0.9b-20020814-1), menu (<= 2.1.3-1), whiptail (<< 0.51.4-11), whiptail-utf8 (<= 0.50.17-13) +Conffiles: + /etc/apt/apt.conf.d/70debconf 7e9d09d5801a42b4926b736b8eeabb73 + /etc/debconf.conf 8c0619be413824f1fc7698cee0f23811 +Description: Debian configuration management system + Debconf is a configuration management system for debian packages. Packages + use Debconf to ask questions when they are installed. +Original-Maintainer: Debconf Developers + +Package: zlib1g +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 169 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: zlib +Version: 1:1.2.11.dfsg-0ubuntu2 +Provides: libz1 +Depends: libc6 (>= 2.14) +Breaks: libxml2 (<< 2.7.6.dfsg-2), texlive-binaries (<< 2009-12) +Conflicts: zlib1 (<= 1:1.0.4-7) +Description: compression library - runtime + zlib is a library implementing the deflate compression method found + in gzip and PKZIP. This package includes the shared library. +Homepage: http://zlib.net/ +Original-Maintainer: Mark Brown + +Package: hostname +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 46 +Maintainer: Ubuntu Developers +Architecture: amd64 +Version: 3.20 +Replaces: nis (<< 3.17-30) +Pre-Depends: libc6 (>= 2.4) +Breaks: nis (<< 3.17-30) +Description: utility to set/show the host name or domain name + This package provides commands which can be used to display the system's + DNS name, and to display or set its hostname or NIS domain name. +Original-Maintainer: Michael Meskes + +Package: mawk +Status: install ok installed +Priority: required +Section: interpreters +Installed-Size: 184 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 1.3.3-17ubuntu3 +Provides: awk +Pre-Depends: libc6 (>= 2.14) +Description: a pattern scanning and text processing language + Mawk is an interpreter for the AWK Programming Language. The AWK + language is useful for manipulation of data files, text retrieval and + processing, and for prototyping and experimenting with algorithms. Mawk + is a new awk meaning it implements the AWK language as defined in Aho, + Kernighan and Weinberger, The AWK Programming Language, Addison-Wesley + Publishing, 1988. (Hereafter referred to as the AWK book.) Mawk conforms + to the POSIX 1003.2 (draft 11.3) definition of the AWK language + which contains a few features not described in the AWK book, and mawk + provides a small number of extensions. + . + Mawk is smaller and much faster than gawk. It has some compile-time + limits such as NF = 32767 and sprintf buffer = 1020. +Original-Maintainer: Steve Langasek + +Package: gzip +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 229 +Maintainer: Ubuntu Developers +Architecture: amd64 +Version: 1.6-5ubuntu1 +Depends: dpkg (>= 1.15.4) | install-info +Pre-Depends: libc6 (>= 2.17) +Suggests: less +Description: GNU compression utilities + This package provides the standard GNU file compression utilities, which + are also the default compression tools for Debian. They typically operate + on files with names ending in '.gz', but can also decompress files ending + in '.Z' created with 'compress'. +Original-Maintainer: Bdale Garbee + +Package: gpgv +Status: install ok installed +Priority: important +Section: utils +Installed-Size: 475 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: gnupg2 +Version: 2.2.4-1ubuntu1.1 +Replaces: gnupg2 (<< 2.0.21-2), gpgv2 (<< 2.1.11-7+exp1) +Depends: libbz2-1.0, libc6 (>= 2.14), libgcrypt20 (>= 1.8.0), libgpg-error0 (>= 1.14), zlib1g (>= 1:1.1.4) +Suggests: gnupg +Breaks: gnupg2 (<< 2.0.21-2), gpgv2 (<< 2.1.11-7+exp1), python-debian (<< 0.1.29) +Description: GNU privacy guard - signature verification tool + GnuPG is GNU's tool for secure communication and data storage. + . + gpgv is actually a stripped-down version of gpg which is only able + to check signatures. It is somewhat smaller than the fully-blown gpg + and uses a different (and simpler) way to check that the public keys + used to make the signature are valid. There are no configuration + files and only a few options are implemented. +Homepage: https://www.gnupg.org/ +Original-Maintainer: Debian GnuPG Maintainers + +Package: bsdutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 264 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: util-linux (2.31.1-0.4ubuntu3.1) +Version: 1:2.31.1-0.4ubuntu3.1 +Replaces: bash-completion (<< 1:2.1-4.1~) +Pre-Depends: libc6 (>= 2.14), libsystemd0 +Recommends: bsdmainutils +Breaks: bash-completion (<< 1:2.1-4.1~) +Description: basic utilities from 4.4BSD-Lite + This package contains the bare minimum of BSD utilities needed for a + Debian system: logger, renice, script, scriptreplay, and wall. The + remaining standard BSD utilities are provided by bsdmainutils. +Original-Maintainer: LaMont Jones + +Package: dash +Essential: yes +Status: install ok installed +Priority: required +Section: shells +Installed-Size: 214 +Maintainer: Ubuntu Developers +Architecture: amd64 +Version: 0.5.8-2.10 +Depends: debianutils (>= 2.15), dpkg (>= 1.15.0) +Pre-Depends: libc6 (>= 2.14) +Description: POSIX-compliant shell + The Debian Almquist Shell (dash) is a POSIX-compliant shell derived + from ash. + . + Since it executes scripts faster than bash, and has fewer library + dependencies (making it more robust against software or hardware + failures), it is used as the default system shell on Debian systems. +Original-Maintainer: Gerrit Pape +Homepage: http://gondor.apana.org.au/~herbert/dash/ + +Package: mount +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 370 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: util-linux +Version: 2.31.1-0.4ubuntu3.1 +Replaces: bash-completion (<< 1:2.1-4.3~) +Depends: util-linux (>= 2.30.1-0ubuntu4~) +Pre-Depends: libblkid1 (>= 2.17.2), libc6 (>= 2.17), libmount1 (>= 2.30.2), libsmartcols1 (>= 2.27~rc1) +Suggests: nfs-common (>= 1:1.1.0-13) +Breaks: bash-completion (<< 1:2.1-4.3~) +Description: tools for mounting and manipulating filesystems + This package provides the mount(8), umount(8), swapon(8), + swapoff(8), and losetup(8) commands. +Important: yes +Original-Maintainer: LaMont Jones + +Package: libgnutls30 +Status: install ok installed +Priority: standard +Section: libs +Installed-Size: 1708 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: gnutls28 +Version: 3.5.18-1ubuntu1 +Depends: libc6 (>= 2.25), libgmp10 (>= 2:6), libhogweed4, libidn2-0 (>= 0.6), libnettle6, libp11-kit0 (>= 0.23.1), libtasn1-6 (>= 4.12), libunistring2 (>= 0.9.7), zlib1g (>= 1:1.1.4) +Suggests: gnutls-bin +Description: GNU TLS library - main runtime library + GnuTLS is a portable library which implements the Transport Layer + Security (TLS 1.0, 1.1, 1.2) and Secure Sockets Layer (SSL) 3.0 and Datagram + Transport Layer Security (DTLS 1.0, 1.2) protocols. + . + GnuTLS features support for: + - TLS extensions: server name indication, max record size, opaque PRF + input, etc. + - authentication using the SRP protocol. + - authentication using both X.509 certificates and OpenPGP keys. + - TLS Pre-Shared-Keys (PSK) extension. + - Inner Application (TLS/IA) extension. + - X.509 and OpenPGP certificate handling. + - X.509 Proxy Certificates (RFC 3820). + - all the strong encryption algorithms (including SHA-256/384/512 and + Camellia (RFC 4132)). + . + This package contains the main runtime library. +Homepage: http://www.gnutls.org/ +Original-Maintainer: Debian GnuTLS Maintainers + +Package: libsystemd0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 646 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: systemd +Version: 237-3ubuntu10.3 +Pre-Depends: libc6 (>= 2.27), libgcrypt20 (>= 1.8.0), liblz4-1 (>= 0.0~r113), liblzma5 (>= 5.1.1alpha+20120614) +Description: systemd utility library + The libsystemd0 library provides interfaces to various systemd components. +Homepage: https://www.freedesktop.org/wiki/Software/systemd +Original-Maintainer: Debian systemd Maintainers + +Package: libzstd1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 519 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libzstd +Version: 1.3.3+dfsg-2ubuntu1 +Depends: libc6 (>= 2.14) +Description: fast lossless compression algorithm + Zstd, short for Zstandard, is a fast lossless compression algorithm, targeting + real-time compression scenarios at zlib-level compression ratio. + . + This package contains the shared library. +Homepage: https://github.com/facebook/zstd +Original-Maintainer: Debian Med Packaging Team + +Package: libc6 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 11877 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: glibc +Version: 2.27-3ubuntu1 +Replaces: libc6-amd64 +Depends: libgcc1 +Suggests: glibc-doc, debconf | debconf-2.0, locales +Breaks: hurd (<< 1:0.5.git20140203-1), libtirpc1 (<< 0.2.3), locales (<< 2.27), locales-all (<< 2.27), nscd (<< 2.27) +Conflicts: openrc (<< 0.27-2~) +Conffiles: + /etc/ld.so.conf.d/x86_64-linux-gnu.conf d4e7a7b88a71b5ffd9e2644e71a0cfab +Description: GNU C Library: Shared libraries + Contains the standard libraries that are used by nearly all programs on + the system. This package includes shared versions of the standard C library + and the standard math library, as well as many others. +Homepage: https://www.gnu.org/software/libc/libc.html +Original-Maintainer: GNU Libc Maintainers + +Package: libfdisk1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 508 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.31.1-0.4ubuntu3.1 +Depends: libblkid1 (>= 2.24.2), libc6 (>= 2.25), libuuid1 (>= 2.16) +Description: fdisk partitioning library + The libfdisk library is used for manipulating partition tables. It is + the core of the fdisk, cfdisk, and sfdisk tools. +Original-Maintainer: LaMont Jones + +Package: libpcre3 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 665 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: pcre3 +Version: 2:8.39-9 +Depends: libc6 (>= 2.14) +Breaks: approx (<< 4.4-1~), cduce (<< 0.5.3-2~), cmigrep (<< 1.5-7~), galax (<< 1.1-7~), libpcre-ocaml (<< 6.0.1~), liquidsoap (<< 0.9.2-3~), ocsigen (<< 1.3.3-1~) +Conflicts: libpcre3-dev (<= 4.3-3) +Description: Old Perl 5 Compatible Regular Expression Library - runtime files + This is a library of functions to support regular expressions whose syntax + and semantics are as close as possible to those of the Perl 5 language. + . + New packages should use the newer pcre2 packages, and existing + packages should migrate to pcre2. + . + This package contains the runtime libraries. +Original-Maintainer: Matthew Vernon + +Package: coreutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 6560 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 8.28-1ubuntu1 +Pre-Depends: libacl1 (>= 2.2.51-8), libattr1 (>= 1:2.4.46-8), libc6 (>= 2.25), libselinux1 (>= 2.1.13) +Description: GNU core utilities + This package contains the basic file, shell and text manipulation + utilities which are expected to exist on every operating system. + . + Specifically, this package includes: + arch base64 basename cat chcon chgrp chmod chown chroot cksum comm cp + csplit cut date dd df dir dircolors dirname du echo env expand expr + factor false flock fmt fold groups head hostid id install join link ln + logname ls md5sum mkdir mkfifo mknod mktemp mv nice nl nohup nproc numfmt + od paste pathchk pinky pr printenv printf ptx pwd readlink realpath rm + rmdir runcon sha*sum seq shred sleep sort split stat stty sum sync tac + tail tee test timeout touch tr true truncate tsort tty uname unexpand + uniq unlink users vdir wc who whoami yes +Homepage: http://gnu.org/software/coreutils +Original-Maintainer: Michael Stone + +Package: e2fsprogs +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 1222 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 1.44.1-1 +Pre-Depends: libblkid1 (>= 2.17.2), libc6 (>= 2.14), libcom-err2 (>= 1.42~WIP-2011-10-05-1), libext2fs2 (= 1.44.1-1), libss2 (>= 1.34-1), libuuid1 (>= 2.16) +Recommends: e2fsprogs-l10n +Suggests: gpart, parted, fuse2fs, e2fsck-static +Conffiles: + /etc/mke2fs.conf 72b349d890a9b5cca06c7804cd0c8d1d +Description: ext2/ext3/ext4 file system utilities + The ext2, ext3 and ext4 file systems are successors of the original ext + ("extended") file system. They are the main file system types used for + hard disks on Debian and other Linux systems. + . + This package contains programs for creating, checking, and maintaining + ext2/3/4-based file systems. It also includes the "badblocks" program, + which can be used to scan for bad blocks on a disk or other storage device. +Original-Maintainer: Theodore Y. Ts'o +Homepage: http://e2fsprogs.sourceforge.net + +Package: tar +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 864 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 1.29b-2 +Replaces: cpio (<< 2.4.2-39) +Pre-Depends: libacl1 (>= 2.2.51-8), libc6 (>= 2.17), libselinux1 (>= 1.32) +Suggests: bzip2, ncompress, xz-utils, tar-scripts, tar-doc +Breaks: dpkg-dev (<< 1.14.26) +Conflicts: cpio (<= 2.4.2-38) +Conffiles: + /etc/rmt 3c58b7cd13da1085eff0acc6a00f43c7 +Description: GNU version of the tar archiving utility + Tar is a program for packaging a set of files as a single archive in tar + format. The function it performs is conceptually similar to cpio, and to + things like PKZIP in the DOS world. It is heavily used by the Debian package + management system, and is useful for performing system backups and exchanging + sets of files with others. +Original-Maintainer: Bdale Garbee + +Package: libprocps6 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 118 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: procps +Version: 2:3.3.12-3ubuntu1.1 +Replaces: procps (<< 1:3.3.2-1) +Depends: libc6 (>= 2.14), libsystemd0 (>= 209) +Description: library for accessing process information from /proc + The libprocps library is a way of accessing information out of the /proc + filesystem. + . + This package contains the shared libraries necessary to run programs + compiled with libprocps. +Homepage: https://gitlab.com/procps-ng/procps +Original-Maintainer: Craig Small + +Package: libbz2-1.0 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 90 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: bzip2 +Version: 1.0.6-8.1 +Depends: libc6 (>= 2.4) +Description: high-quality block-sorting file compressor library - runtime + This package contains libbzip2 which is used by the bzip2 compressor. + . + bzip2 is a freely available, patent free, high-quality data compressor. + It typically compresses files to within 10% to 15% of the best available + techniques, whilst being around twice as fast at compression and six + times faster at decompression. + . + bzip2 compresses files using the Burrows-Wheeler block-sorting text + compression algorithm, and Huffman coding. Compression is generally + considerably better than that achieved by more conventional + LZ77/LZ78-based compressors, and approaches the performance of the PPM + family of statistical compressors. + . + The archive file format of bzip2 (.bz2) is incompatible with that of its + predecessor, bzip (.bz). +Original-Maintainer: Anibal Monsalve Salazar +Homepage: http://www.bzip.org/ + +Package: libblkid1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 398 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.31.1-0.4ubuntu3.1 +Depends: libc6 (>= 2.25), libuuid1 (>= 2.16) +Description: block device ID library + The blkid library allows system programs such as fsck and mount to + quickly and easily find block devices by filesystem UUID or label. + This allows system administrators to avoid specifying filesystems by + hard-coded device names and use a logical naming system instead. +Original-Maintainer: LaMont Jones + +Package: libtasn1-6 +Status: install ok installed +Priority: standard +Section: libs +Installed-Size: 112 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Version: 4.13-2 +Depends: libc6 (>= 2.14) +Description: Manage ASN.1 structures (runtime) + Manage ASN1 (Abstract Syntax Notation One) structures. + The main features of this library are: + * on-line ASN1 structure management that doesn't require any C code + file generation. + * off-line ASN1 structure management with C code file generation + containing an array. + * DER (Distinguish Encoding Rules) encoding + * no limits for INTEGER and ENUMERATED values + . + This package contains runtime libraries. +Original-Maintainer: Debian GnuTLS Maintainers +Homepage: http://www.gnu.org/software/libtasn1/ + +Package: bzip2 +Status: install ok installed +Priority: standard +Section: utils +Installed-Size: 177 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 1.0.6-8.1 +Replaces: libbz2 (<< 0.9.5d-3) +Depends: libbz2-1.0 (= 1.0.6-8.1), libc6 (>= 2.14) +Suggests: bzip2-doc +Description: high-quality block-sorting file compressor - utilities + bzip2 is a freely available, patent free, high-quality data compressor. + It typically compresses files to within 10% to 15% of the best available + techniques, whilst being around twice as fast at compression and six + times faster at decompression. + . + bzip2 compresses files using the Burrows-Wheeler block-sorting text + compression algorithm, and Huffman coding. Compression is generally + considerably better than that achieved by more conventional + LZ77/LZ78-based compressors, and approaches the performance of the PPM + family of statistical compressors. + . + The archive file format of bzip2 (.bz2) is incompatible with that of its + predecessor, bzip (.bz). +Original-Maintainer: Anibal Monsalve Salazar +Homepage: http://www.bzip.org/ + +Package: libhogweed4 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 228 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: nettle +Version: 3.4-1 +Depends: libc6 (>= 2.14), libgmp10 (>= 2:6.0.0), libnettle6 (= 3.4-1) +Description: low level cryptographic library (public-key cryptos) + Nettle is a cryptographic library that is designed to fit easily in more or + less any context: In crypto toolkits for object-oriented languages (C++, + Python, Pike, ...), in applications like LSH or GNUPG, or even in kernel + space. + . + It tries to solve a problem of providing a common set of cryptographic + algorithms for higher-level applications by implementing a + context-independent set of cryptographic algorithms. In that light, Nettle + doesn't do any memory allocation or I/O, it simply provides the + cryptographic algorithms for the application to use in any environment and + in any way it needs. + . + This package contains the asymmetric cryptographic algorithms, which, + require the GNU multiple precision arithmetic library (libgmp) for + their large integer computations. +Original-Maintainer: Magnus Holmgren +Homepage: http://www.lysator.liu.se/~nisse/nettle/ + +Package: lsb-base +Status: install ok installed +Priority: required +Section: misc +Installed-Size: 58 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Source: lsb +Version: 9.20170808ubuntu1 +Description: Linux Standard Base init script functionality + The Linux Standard Base (http://www.linuxbase.org/) is a standard + core system that third-party applications written for Linux can + depend upon. + . + This package only includes the init-functions shell library, which + may be used by other packages' initialization scripts for console + logging and other purposes. +Homepage: https://wiki.linuxfoundation.org/lsb/start +Original-Maintainer: Debian LSB Team + +Package: procps +Status: install ok installed +Priority: important +Section: admin +Installed-Size: 709 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 2:3.3.12-3ubuntu1.1 +Provides: watch +Depends: libc6 (>= 2.27), libncurses5 (>= 6), libncursesw5 (>= 6), libprocps6, libtinfo5 (>= 6), lsb-base (>= 3.0-10), init-system-helpers (>= 1.29~) +Recommends: psmisc +Breaks: guymager (<= 0.5.9-1), open-vm-tools (<= 2011.12.20-562307-1) +Conflicts: pgrep (<< 3.3-5), w-bassman (<< 1.0-3) +Conffiles: + /etc/init.d/procps 49fbfd237be2a2f09576f1f9374580be + /etc/sysctl.conf 36547fde818f251846b0198564060927 + /etc/sysctl.d/10-console-messages.conf 154f6f5c5810d10bb303fb6a8e907c6a + /etc/sysctl.d/10-ipv6-privacy.conf e9473d12b4a7069d6a3ca8b694511ddf + /etc/sysctl.d/10-kernel-hardening.conf 5c1388f00011a287cdeba60208c674e1 + /etc/sysctl.d/10-link-restrictions.conf 8568316f2baa8db06554dab91f93a161 + /etc/sysctl.d/10-magic-sysrq.conf b3059f2835f17c97265433fdfdee358f + /etc/sysctl.d/10-network-security.conf 4ac7258f5336e7eeaf448c05ab668d3c + /etc/sysctl.d/10-ptrace.conf 47f40494b2fc698e15549e0a4a79e81c + /etc/sysctl.d/10-zeropage.conf 8d7193abcc4dfedaf519dd03016a5e59 + /etc/sysctl.d/README c20074b9b11a5202758c69d7bcb6996f +Description: /proc file system utilities + This package provides command line and full screen utilities for browsing + procfs, a "pseudo" file system dynamically generated by the kernel to + provide information about the status of entries in its process table + (such as whether the process is running, stopped, or a "zombie"). + . + It contains free, kill, pkill, pgrep, pmap, ps, pwdx, skill, slabtop, + snice, sysctl, tload, top, uptime, vmstat, w, and watch. +Homepage: https://gitlab.com/procps-ng/procps +Original-Maintainer: Craig Small + +Package: libgpg-error0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 164 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libgpg-error +Version: 1.27-6 +Depends: libc6 (>= 2.15) +Description: library for common error values and messages in GnuPG components + Library that defines common error values for all GnuPG + components. Among these are GPG, GPGSM, GPGME, GPG-Agent, libgcrypt, + pinentry, SmartCard Daemon and possibly more in the future. +Original-Maintainer: Debian GnuPG Maintainers +Homepage: https://www.gnupg.org/related_software/libgpg-error/ + +Package: base-files +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 375 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 10.1ubuntu2.2 +Replaces: base, dpkg (<= 1.15.0), miscutils +Provides: base +Pre-Depends: awk +Breaks: initscripts (<< 2.88dsf-13.3), sendfile (<< 2.1b.20080616-5.2~) +Conffiles: + /etc/debian_version 71f3b31c52b52a91784e496ddf94b019 + /etc/default/motd-news c08a329a603b640095da5ffe4e73491c + /etc/dpkg/origins/debian 731423fa8ba067262f8ef37882d1e742 + /etc/dpkg/origins/ubuntu ea35901c45553c3451f60476be94d2d8 + /etc/host.conf 89408008f2585c957c031716600d5a80 + /etc/issue 38103f9a76bb14e2abbc051b65722cff + /etc/issue.net 4c773f83594f2a3f47059a348a0af0a5 + /etc/legal 0110925f6e068836ef2e09356e3651d9 + /etc/lsb-release d1b7cd24192250d97c4699cbe7a47cfa + /etc/update-motd.d/00-header 4a1e6eed7a59f200b4267085721750a3 + /etc/update-motd.d/10-help-text d95d18b11ac12cf6582d08a1643034f3 + /etc/update-motd.d/50-motd-news c1d89e86b0eed1ffb1835bcdd78dfe32 +Description: Debian base system miscellaneous files + This package contains the basic filesystem hierarchy of a Debian system, and + several important miscellaneous files, such as /etc/debian_version, + /etc/host.conf, /etc/issue, /etc/motd, /etc/profile, and others, + and the text of several common licenses in use on Debian systems. +Original-Maintainer: Santiago Vila + +Package: libgmp10 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 558 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: gmp +Version: 2:6.1.2+dfsg-2 +Depends: libc6 (>= 2.14) +Description: Multiprecision arithmetic library + GNU MP is a programmer's library for arbitrary precision + arithmetic (ie, a bignum package). It can operate on signed + integer, rational, and floating point numeric types. + . + It has a rich set of functions, and the functions have a regular + interface. +Original-Maintainer: Debian Science Team +Homepage: http://gmplib.org/ + +Package: sensible-utils +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 62 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Version: 0.0.12 +Replaces: debianutils (<= 2.32.3), manpages-pl (<= 20060617-3~) +Description: Utilities for sensible alternative selection + This package provides a number of small utilities which are used + by programs to sensibly select and spawn an appropriate browser, + editor, or pager. + . + The specific utilities included are: sensible-browser sensible-editor + sensible-pager +Original-Maintainer: Anibal Monsalve Salazar + +Package: passwd +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 2541 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: shadow +Version: 1:4.5-1ubuntu1 +Replaces: manpages-tr (<< 1.0.5), manpages-zh (<< 1.5.1-1) +Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.14), libpam0g (>= 0.99.7.1), libselinux1 (>= 1.32), libsemanage1 (>= 2.0.3), libpam-modules +Conffiles: + /etc/cron.daily/passwd db990990933b6f56322725223f13c2bc + /etc/default/useradd cc9f9a7713ab62a32cd38363d958f396 + /etc/pam.d/chfn 4d466e00a348ba426130664d795e8afa + /etc/pam.d/chpasswd 9900720564cb4ee98b7da29e2d183cb2 + /etc/pam.d/chsh a6e9b589e90009334ffd030d819290a6 + /etc/pam.d/newusers 1454e29bfa9f2a10836563e76936cea5 + /etc/pam.d/passwd eaf2ad85b5ccd06cceb19a3e75f40c63 +Description: change and administer password and group data + This package includes passwd, chsh, chfn, and many other programs to + maintain password and group data. + . + Shadow passwords are supported. See /usr/share/doc/passwd/README.Debian +Homepage: https://github.com/shadow-maint/shadow +Original-Maintainer: Shadow package maintainers + +Package: init-system-helpers +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 129 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Version: 1.51 +Replaces: sysv-rc (<< 2.88dsf-59.3~), sysvinit-utils (<< 2.88dsf-59.3) +Depends: perl-base (>= 5.20.1-3) +Breaks: systemd (<< 44-12), sysvinit-utils (<< 2.88dsf-59.3~) +Conflicts: file-rc (<< 0.8.17~), openrc (<= 0.18.3-1) +Description: helper tools for all init systems + This package contains helper tools that are necessary for switching between + the various init systems that Debian contains (e. g. sysvinit or + systemd). An example is deb-systemd-helper, a script that enables systemd unit + files without depending on a running systemd. + . + It also includes the "service", "invoke-rc.d", and "update-rc.d" scripts which + provide an abstraction for enabling, disabling, starting, and stopping + services for all supported Debian init systems as specified by the policy. + . + While this package is maintained by pkg-systemd-maintainers, it is NOT + specific to systemd at all. Maintainers of other init systems are welcome to + include their helpers in this package. +Original-Maintainer: Debian systemd Maintainers + +Package: ncurses-base +Essential: yes +Status: install ok installed +Priority: required +Section: misc +Installed-Size: 364 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Source: ncurses +Version: 6.1-1ubuntu1.18.04 +Provides: ncurses-runtime +Breaks: ncurses-term (<< 5.7+20100313-3) +Conffiles: + /etc/terminfo/README 45b6df19fb5e21f55717482fa7a30171 +Description: basic terminal type definitions + The ncurses library routines are a terminal-independent method of + updating character screens with reasonable optimization. + . + This package contains terminfo data files to support the most common types of + terminal, including ansi, dumb, linux, rxvt, screen, sun, vt100, vt102, vt220, + vt52, and xterm. +Homepage: https://invisible-island.net/ncurses/ +Original-Maintainer: Craig Small + +Package: libc-bin +Essential: yes +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 3631 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: glibc +Version: 2.27-3ubuntu1 +Depends: libc6 (>> 2.27), libc6 (<< 2.28) +Suggests: manpages +Conffiles: + /etc/bindresvport.blacklist 4c09213317e4e3dd3c71d74404e503c5 + /etc/default/nss d6d5d6f621fb3ead2548076ce81e309c + /etc/gai.conf 28fa76ff5a9e0566eaa1e11f1ce51f09 + /etc/ld.so.conf 4317c6de8564b68d628c21efa96b37e4 + /etc/ld.so.conf.d/libc.conf d4d833fd095fb7b90e1bb4a547f16de6 +Description: GNU C Library: Binaries + This package contains utility programs related to the GNU C Library. + . + * catchsegv: catch segmentation faults in programs + * getconf: query system configuration variables + * getent: get entries from administrative databases + * iconv, iconvconfig: convert between character encodings + * ldd, ldconfig: print/configure shared library dependencies + * locale, localedef: show/generate locale definitions + * tzselect, zdump, zic: select/dump/compile time zones +Homepage: https://www.gnu.org/software/libc/libc.html +Original-Maintainer: GNU Libc Maintainers + +Package: libsemanage1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 292 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libsemanage +Version: 2.7-2build2 +Depends: libsemanage-common (= 2.7-2build2), libaudit1 (>= 1:2.2.1), libbz2-1.0, libc6 (>= 2.14), libselinux1 (>= 2.7), libsepol1 (>= 2.7) +Breaks: policycoreutils (<< 2.4), selinux-policy-default (<< 2:2.20140421-10~), selinux-policy-mls (<< 2:2.20140421-10~) +Description: SELinux policy management library + This package provides the shared libraries for SELinux policy management. + It uses libsepol for binary policy manipulation and libselinux for + interacting with the SELinux system. It also exec's helper programs + for loading policy and for checking whether the file_contexts + configuration is valid (load_policy and setfiles from + policycoreutils) presently, although this may change at least for the + bootstrapping case + . + Security-enhanced Linux is a patch of the Linux kernel and a + number of utilities with enhanced security functionality designed to + add mandatory access controls to Linux. The Security-enhanced Linux + kernel contains new architectural components originally developed to + improve the security of the Flask operating system. These + architectural components provide general support for the enforcement + of many kinds of mandatory access control policies, including those + based on the concepts of Type Enforcement, Role-based Access + Control, and Multi-level Security. +Homepage: http://userspace.selinuxproject.org/ +Original-Maintainer: Debian SELinux maintainers + +Package: libseccomp2 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 293 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libseccomp +Version: 2.3.1-2.1ubuntu4 +Depends: libc6 (>= 2.14) +Description: high level interface to Linux seccomp filter + This library provides a high level interface to constructing, analyzing + and installing seccomp filters via a BPF passed to the Linux Kernel's + prctl() syscall. +Homepage: https://github.com/seccomp/libseccomp +Original-Maintainer: Kees Cook + +Package: sysvinit-utils +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 60 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: sysvinit +Version: 2.88dsf-59.10ubuntu1 +Replaces: initscripts (<< 2.88dsf-59.5) +Depends: libc6 (>= 2.14), init-system-helpers (>= 1.25~), util-linux (>> 2.28-2~) +Breaks: systemd (<< 215) +Description: System-V-like utilities + This package contains the important System-V-like utilities. + . + Specifically, this package includes: + killall5, pidof +Homepage: http://savannah.nongnu.org/projects/sysvinit +Original-Maintainer: Debian sysvinit maintainers + +Package: libsemanage-common +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 29 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Source: libsemanage +Version: 2.7-2build2 +Replaces: libsemanage1 (<= 2.0.41-1), libsemanage1-dev (<< 2.1.6-3~) +Breaks: libsemanage1 (<= 2.0.41-1), libsemanage1-dev (<< 2.1.6-3~) +Conffiles: + /etc/selinux/semanage.conf f6f9b97af233c90ca127f406fb6f0932 +Description: Common files for SELinux policy management libraries + This package provides the common files used by the shared libraries + for SELinux policy management. + . + Security-enhanced Linux is a patch of the Linux kernel and a + number of utilities with enhanced security functionality designed to + add mandatory access controls to Linux. The Security-enhanced Linux + kernel contains new architectural components originally developed to + improve the security of the Flask operating system. These + architectural components provide general support for the enforcement + of many kinds of mandatory access control policies, including those + based on the concepts of Type Enforcement, Role-based Access + Control, and Multi-level Security. +Homepage: http://userspace.selinuxproject.org/ +Original-Maintainer: Debian SELinux maintainers + +Package: libp11-kit0 +Status: install ok installed +Priority: standard +Section: libs +Installed-Size: 1246 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: p11-kit +Version: 0.23.9-2 +Depends: libc6 (>= 2.26), libffi6 (>= 3.0.4) +Breaks: opencryptoki (<= 3.6.1+dfsg-1) +Description: library for loading and coordinating access to PKCS#11 modules - runtime + The p11-kit library provides a way to load and enumerate Public-Key + Cryptography Standard #11 modules, along with a standard configuration + setup for installing PKCS#11 modules so that they're discoverable. It + also solves problems with coordinating the use of PKCS#11 by different + components or libraries living in the same process. + . + This package contains the shared library required for applications loading + and accessing PKCS#11 modules. +Original-Maintainer: Debian GnuTLS Maintainers +Homepage: http://p11-glue.freedesktop.org/p11-kit.html + +Package: libdebconfclient0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 68 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: cdebconf +Version: 0.213ubuntu1 +Depends: libc6 (>= 2.4) +Description: Debian Configuration Management System (C-implementation library) + Debconf is a configuration management system for Debian packages. It is + used by some packages to prompt you for information before they are + installed. cdebconf is a reimplementation of the original debconf in C. + . + This library allows C programs to interface with cdebconf. +Original-Maintainer: Debian Install System Team + +Package: libselinux1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 193 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libselinux +Version: 2.7-2build2 +Depends: libc6 (>= 2.14), libpcre3 +Description: SELinux runtime shared libraries + This package provides the shared libraries for Security-enhanced + Linux that provides interfaces (e.g. library functions for the + SELinux kernel APIs like getcon(), other support functions like + getseuserbyname()) to SELinux-aware applications. Security-enhanced + Linux is a patch of the Linux kernel and a number of utilities with + enhanced security functionality designed to add mandatory access + controls to Linux. The Security-enhanced Linux kernel contains new + architectural components originally developed to improve the security + of the Flask operating system. These architectural components provide + general support for the enforcement of many kinds of mandatory access + control policies, including those based on the concepts of Type + Enforcement, Role-based Access Control, and Multi-level Security. + . + libselinux1 provides an API for SELinux applications to get and set + process and file security contexts and to obtain security policy + decisions. Required for any applications that use the SELinux + API. libselinux may use the shared libsepol to manipulate the binary + policy if necessary (e.g. to downgrade the policy format to an older + version supported by the kernel) when loading policy. +Homepage: http://userspace.selinuxproject.org/ +Original-Maintainer: Debian SELinux maintainers + +Package: dpkg +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 6773 +Origin: debian +Maintainer: Ubuntu Developers +Bugs: debbugs://bugs.debian.org +Architecture: amd64 +Multi-Arch: foreign +Version: 1.19.0.5ubuntu2 +Depends: tar (>= 1.28-1) +Pre-Depends: libbz2-1.0, libc6 (>= 2.14), liblzma5 (>= 5.2.2), libselinux1 (>= 2.3), libzstd1 (>= 1.3.2), zlib1g (>= 1:1.1.4) +Suggests: apt, debsig-verify +Breaks: acidbase (<= 1.4.5-4), amule (<< 2.3.1+git1a369e47-3), beep (<< 1.3-4), im (<< 1:151-4), libdpkg-perl (<< 1.18.11), netselect (<< 0.3.ds1-27), pconsole (<< 1.0-12), phpgacl (<< 3.3.7-7.3), pure-ftpd (<< 1.0.43-1), systemtap (<< 2.8-1), terminatorx (<< 4.0.1-1), xvt (<= 2.1-20.1) +Conffiles: + /etc/alternatives/README 7be88b21f7e386c8d5a8790c2461c92b + /etc/cron.daily/dpkg 4a75e177a3662e2efd8d477ae8e8533b + /etc/dpkg/dpkg.cfg f4413ffb515f8f753624ae3bb365b81b + /etc/logrotate.d/alternatives 5fe0af6ce1505fefdc158d9e5dbf6286 + /etc/logrotate.d/dpkg 9e25c8505966b5829785f34a548ae11f +Description: Debian package management system + This package provides the low-level infrastructure for handling the + installation and removal of Debian software packages. + . + For Debian package development tools, install dpkg-dev. +Homepage: https://wiki.debian.org/Teams/Dpkg +Original-Maintainer: Dpkg Developers + +Package: gcc-8-base +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 106 +Maintainer: Ubuntu Core developers +Architecture: amd64 +Multi-Arch: same +Source: gcc-8 +Version: 8-20180414-1ubuntu2 +Breaks: gcc-4.4-base (<< 4.4.7), gcc-4.7-base (<< 4.7.3), gcj-4.4-base (<< 4.4.6-9~), gcj-4.6-base (<< 4.6.1-4~), gnat-4.4-base (<< 4.4.6-3~), gnat-4.6 (<< 4.6.1-5~) +Description: GCC, the GNU Compiler Collection (base package) + This package contains files common to all languages and libraries + contained in the GNU Compiler Collection (GCC). +Homepage: http://gcc.gnu.org/ +Original-Maintainer: Debian GCC Maintainers + +Package: apt +Status: install ok installed +Priority: important +Section: admin +Installed-Size: 3806 +Maintainer: Ubuntu Developers +Architecture: amd64 +Version: 1.6.3ubuntu0.1 +Replaces: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~) +Provides: apt-transport-https (= 1.6.3ubuntu0.1) +Depends: adduser, gpgv | gpgv2 | gpgv1, ubuntu-keyring, libapt-pkg5.0 (>= 1.6.3ubuntu0.1), libc6 (>= 2.15), libgcc1 (>= 1:3.0), libgnutls30 (>= 3.5.6), libseccomp2 (>= 1.0.1), libstdc++6 (>= 5.2) +Recommends: ca-certificates +Suggests: apt-doc, aptitude | synaptic | wajig, dpkg-dev (>= 1.17.2), gnupg | gnupg2 | gnupg1, powermgmt-base +Breaks: apt-transport-https (<< 1.5~alpha4~), apt-utils (<< 1.3~exp2~), aptitude (<< 0.8.10) +Conffiles: + /etc/apt/apt.conf.d/01-vendor-ubuntu 5232396660502461fc834c0a1229dbe4 + /etc/apt/apt.conf.d/01autoremove 735050108d38dbf99e933a9f647f6892 + /etc/cron.daily/apt-compat 49e9b2cfa17849700d4db735d04244f3 + /etc/kernel/postinst.d/apt-auto-removal 4ad976a68f045517cf4696cec7b8aa3a + /etc/logrotate.d/apt 179f2ed4f85cbaca12fa3d69c2a4a1c3 +Description: commandline package manager + This package provides commandline tools for searching and + managing as well as querying information about packages + as a low-level access to all features of the libapt-pkg library. + . + These include: + * apt-get for retrieval of packages and information about them + from authenticated sources and for installation, upgrade and + removal of packages together with their dependencies + * apt-cache for querying available information about installed + as well as installable packages + * apt-cdrom to use removable media as a source for packages + * apt-config as an interface to the configuration settings + * apt-key as an interface to manage authentication keys +Original-Maintainer: APT Development Team + +Package: diffutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 452 +Maintainer: Ubuntu Developers +Architecture: amd64 +Version: 1:3.6-1 +Replaces: diff +Pre-Depends: libc6 (>= 2.17) +Suggests: diffutils-doc, wdiff +Description: File comparison utilities + The diffutils package provides the diff, diff3, sdiff, and cmp programs. + . + `diff' shows differences between two files, or each corresponding file + in two directories. `cmp' shows the offsets and line numbers where + two files differ. `cmp' can also show all the characters that + differ between the two files, side by side. `diff3' shows differences + among three files. `sdiff' merges two files interactively. + . + The set of differences produced by `diff' can be used to distribute + updates to text files (such as program source code) to other people. + This method is especially useful when the differences are small compared + to the complete files. Given `diff' output, the `patch' program can + update, or "patch", a copy of the file. +Original-Maintainer: Santiago Vila +Homepage: http://www.gnu.org/software/diffutils/ + +Package: libpam-modules +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 912 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: pam +Version: 1.1.8-3.6ubuntu2 +Replaces: libpam-umask, libpam0g-util +Provides: libpam-mkhomedir, libpam-motd, libpam-umask +Pre-Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.27), libdb5.3, libpam0g (>= 1.1.3-2), libselinux1 (>= 2.1.9), debconf (>= 0.5) | debconf-2.0, libpam-modules-bin (= 1.1.8-3.6ubuntu2) +Recommends: update-motd +Conflicts: libpam-mkhomedir, libpam-motd, libpam-umask +Conffiles: + /etc/security/access.conf 13ec4d189f0ed9acf3433977a53d446b + /etc/security/group.conf f1e26e8db6f7abd2d697d7dad3422c36 + /etc/security/limits.conf 11c27ba00b7bd6a255f33126f75c5005 + /etc/security/namespace.conf 6424c99a62ddf4b7d3ca713bb06ded89 + /etc/security/namespace.init d9e6a7c85e966427ef23a04ec6c7000f + /etc/security/pam_env.conf ddee4a931170dc21b4e0b9bb28e02a7b + /etc/security/sepermit.conf d41c74654734a5c069a37bfc02f0a6d4 + /etc/security/time.conf 06e05c6079e839c8833ac7c3abfde192 +Description: Pluggable Authentication Modules for PAM + This package completes the set of modules for PAM. It includes the + pam_unix.so module as well as some specialty modules. +Homepage: http://www.linux-pam.org/ +Original-Maintainer: Steve Langasek + +Package: libstdc++6 +Status: install ok installed +Priority: important +Section: libs +Installed-Size: 2060 +Maintainer: Ubuntu Core developers +Architecture: amd64 +Multi-Arch: same +Source: gcc-8 +Version: 8-20180414-1ubuntu2 +Replaces: libstdc++6-8-dbg (<< 4.9.0-3) +Depends: gcc-8-base (= 8-20180414-1ubuntu2), libc6 (>= 2.18), libgcc1 (>= 1:4.2) +Breaks: blockattack (<= 1.4.1+ds1-2.1build2), boo (<= 0.9.5~git20110729.r1.202a430-2), c++-annotations (<= 10.2.0-1), chromium-browser (<= 43.0.2357.130-0ubuntu2), clustalx (<= 2.1+lgpl-2), dff (<= 1.3.0+dfsg.1-4.1build2), emscripten (<= 1.22.1-1), ergo (<= 3.4.0-1), fceux (<= 2.2.2+dfsg0-1), flush (<= 0.9.12-3.1ubuntu1), freeorion (<= 0.4.4+git20150327-2), fslview (<= 4.0.1-4), fwbuilder (<= 5.1.0-4), gcc-4.3 (<< 4.3.6-1), gcc-4.4 (<< 4.4.6-4), gcc-4.5 (<< 4.5.3-2), gnote (<= 3.16.2-1), gnudatalanguage (<= 0.9.5-2build1), innoextract (<= 1.4-1build1), libantlr-dev (<= 2.7.7+dfsg-6), libapache2-mod-passenger (<= 4.0.53-1), libaqsis1 (<= 1.8.2-1), libassimp3 (<= 3.0~dfsg-4), libboost-date-time1.55.0, libcpprest2.2 (<= 2.2.0-1), libdap17 (<= 3.14.0-2), libdapclient6 (<= 3.14.0-2), libdapserver7 (<= 3.14.0-2), libdavix0 (<= 0.4.0-1build1), libdballe6 (<= 6.8-1), libdiet-admin2.8 (<= 2.8.0-1build3), libdiet-client2.8 (<= 2.8.0-1build3), libdiet-sed2.8 (<= 2.8.0-1build3), libfreefem++ (<= 3.37.1-1), libgazebo5 (<= 5.0.1+dfsg-2.1), libgetfem4++ (<= 4.2.1~beta1~svn4482~dfsg-3ubuntu3), libgmsh2 (<= 2.8.5+dfsg-1.1ubuntu1), libinsighttoolkit4.6 (<= 4.6.0-3ubuntu3), libkgeomap2 (<= 4:15.04.2-0ubuntu1), libkolabxml1 (<= 1.1.0-3), libkvkontakte1 (<= 1.0~digikam4.10.0-0ubuntu2), libmarisa0 (<= 0.2.4-8build1), libmediawiki1 (<= 1.0~digikam4.10.0-0ubuntu2), libogre-1.8.0 (<= 1.8.1+dfsg-0ubuntu5), libogre-1.9.0 (<= 1.9.0+dfsg1-4), libopenwalnut1 (<= 1.4.0~rc1+hg3a3147463ee2-1ubuntu2), libpqxx-4.0 (<= 4.0.1+dfsg-3ubuntu1), libreoffice-core (<= 1:4.4.4~rc3-0ubuntu1), librime1 (<= 1.2+dfsg-2), libwibble-dev (<= 1.1-1), libwreport2 (<= 2.14-1), libxmltooling6 (<= 1.5.3-2.1), lightspark (<= 0.7.2+git20150512-2), mira-assembler (<= 4.9.5-1), mongodb (<= 1:2.6.3-0ubuntu7), mongodb-server (<= 1:2.6.3-0ubuntu7), ncbi-blast+ (<= 2.2.30-4), openscad (<= 2014.03+dfsg-1build1), passepartout (<= 0.7.1-1.1), pdf2djvu (<= 0.7.19-1ubuntu2), photoprint (<= 0.4.2~pre2-2.3), plastimatch (<= 1.6.2+dfsg-1), plee-the-bear (<= 0.6.0-3.1), povray (<= 1:3.7.0.0-8), powertop (<= 2.6.1-1), printer-driver-brlaser (<= 3-3), psi4 (<= 4.0~beta5+dfsg-2build1), python-healpy (<= 1.8.1-1), python3-taglib (<= 0.3.6+dfsg-2build2), realtimebattle (<= 1.0.8-14), ruby-passenger (<= 4.0.53-1), sqlitebrowser (<= 3.5.1-3), tecnoballz (<= 0.93.1-6), wesnoth-1.12-core (<= 1:1.12.4-1), widelands (<= 1:18-3build1), xflr5 (<= 6.09.06-2) +Conflicts: scim (<< 1.4.2-1) +Description: GNU Standard C++ Library v3 + This package contains an additional runtime library for C++ programs + built with the GNU compiler. + . + libstdc++-v3 is a complete rewrite from the previous libstdc++-v2, which + was included up to g++-2.95. The first version of libstdc++-v3 appeared + in g++-3.0. +Homepage: http://gcc.gnu.org/ +Original-Maintainer: Debian GCC Maintainers + +Package: libffi6 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 52 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libffi +Version: 3.2.1-8 +Depends: libc6 (>= 2.14) +Description: Foreign Function Interface library runtime + A foreign function interface is the popular name for the interface that + allows code written in one language to call code written in another + language. +Original-Maintainer: Debian GCC Maintainers + +Package: libaudit-common +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 23 +Maintainer: Ubuntu Developers +Architecture: all +Multi-Arch: foreign +Source: audit +Version: 1:2.8.2-1ubuntu1 +Replaces: libaudit0, libaudit1 (<< 1:2.2.1-2) +Breaks: libaudit0, libaudit1 (<< 1:2.2.1-2) +Conffiles: + /etc/libaudit.conf cdc703f9d27f0d980271a9e95d0f18b2 +Description: Dynamic library for security auditing - common files + The audit-libs package contains the dynamic libraries needed for + applications to use the audit framework. It is used to monitor systems for + security related events. + . + This package contains the libaudit.conf configuration file and the associated + manpage. +Homepage: https://people.redhat.com/sgrubb/audit/ +Original-Maintainer: Laurent Bigonville + +Package: findutils +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 580 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 4.6.0+git+20170828-2 +Pre-Depends: libc6 (>= 2.17), libselinux1 (>= 1.32) +Suggests: mlocate | locate +Breaks: binstats (<< 1.08-8.1), guilt (<< 0.36-0.2), libpython3.4-minimal (<< 3.4.4-2), libpython3.5-minimal (<< 3.5.1-3), lsat (<< 0.9.7.1-2.1), mc (<< 3:4.8.11-1), switchconf (<< 0.0.9-2.1) +Description: utilities for finding files--find, xargs + GNU findutils provides utilities to find files meeting specified + criteria and perform various actions on the files which are found. + This package contains 'find' and 'xargs'; however, 'locate' has + been split off into a separate package. +Original-Maintainer: Andreas Metzler +Homepage: https://savannah.gnu.org/projects/findutils/ + +Package: libpam0g +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 212 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: pam +Version: 1.1.8-3.6ubuntu2 +Replaces: libpam0g-util +Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.14), debconf (>= 0.5) | debconf-2.0 +Suggests: libpam-doc +Description: Pluggable Authentication Modules library + Contains the shared library for Linux-PAM, a library that enables the + local system administrator to choose how applications authenticate users. + In other words, without rewriting or recompiling a PAM-aware application, + it is possible to switch between the authentication mechanism(s) it uses. + One may entirely upgrade the local authentication system without touching + the applications themselves. +Homepage: http://www.linux-pam.org/ +Original-Maintainer: Steve Langasek + +Package: libcap-ng0 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 36 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libcap-ng +Version: 0.7.7-3.1 +Depends: libc6 (>= 2.8) +Description: An alternate POSIX capabilities library + This library implements the user-space interfaces to the POSIX + 1003.1e capabilities available in Linux kernels. These capabilities are + a partitioning of the all powerful root privilege into a set of distinct + privileges. + . + The libcap-ng library is intended to make programming with POSIX + capabilities much easier than the traditional libcap library. + . + This package contains header files and libraries for libcap-ng. +Original-Maintainer: Pierre Chifflier +Homepage: http://people.redhat.com/sgrubb/libcap-ng + +Package: libmount1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 433 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.31.1-0.4ubuntu3.1 +Depends: libblkid1 (>= 2.17.2), libc6 (>= 2.25), libselinux1 (>= 2.6-3~) +Description: device mounting library + This device mounting library is used by mount and umount helpers. +Original-Maintainer: LaMont Jones + +Package: login +Essential: yes +Status: install ok installed +Priority: required +Section: admin +Installed-Size: 1212 +Maintainer: Ubuntu Developers +Architecture: amd64 +Source: shadow +Version: 1:4.5-1ubuntu1 +Replaces: manpages-de (<< 0.5-3), manpages-tr (<< 1.0.5), manpages-zh (<< 1.5.1-1) +Pre-Depends: libaudit1 (>= 1:2.2.1), libc6 (>= 2.14), libpam0g (>= 0.99.7.1), libpam-runtime, libpam-modules (>= 1.1.8-1) +Conflicts: amavisd-new (<< 2.3.3-8), backupninja (<< 0.9.3-5), echolot (<< 2.1.8-4), gnunet (<< 0.7.0c-2), python-4suite (<< 0.99cvs20060405-1) +Conffiles: + /etc/login.defs 2a8f6cd8e00f54df72dc345a23f9db20 + /etc/pam.d/login 1fd6cb4d4267a68148ee9973510a9d3e + /etc/pam.d/su ce6dcfda3b190a27a455bb38a45ff34a + /etc/securetty d0124b1d2fb22d4ac9a91aa02ae6d6db +Description: system login tools + These tools are required to be able to login and use your system. The + login program invokes your user shell and enables command execution. The + newgrp program is used to change your effective group ID (useful for + workgroup type situations). The su program allows changing your effective + user ID (useful being able to execute commands as another user). +Homepage: https://github.com/shadow-maint/shadow +Original-Maintainer: Shadow package maintainers + +Package: adduser +Status: install ok installed +Priority: important +Section: admin +Installed-Size: 624 +Maintainer: Ubuntu Core Developers +Architecture: all +Multi-Arch: foreign +Version: 3.116ubuntu1 +Depends: passwd, debconf (>= 0.5) | debconf-2.0 +Suggests: liblocale-gettext-perl, perl, ecryptfs-utils (>= 67-1) +Conffiles: + /etc/deluser.conf 773fb95e98a27947de4a95abb3d3f2a2 +Description: add and remove users and groups + This package includes the 'adduser' and 'deluser' commands for creating + and removing users. + . + - 'adduser' creates new users and groups and adds existing users to + existing groups; + - 'deluser' removes users and groups and removes users from a given + group. + . + Adding users with 'adduser' is much easier than adding them manually. + Adduser will choose appropriate UID and GID values, create a home + directory, copy skeletal user configuration, and automate setting + initial values for the user's password, real name and so on. + . + Deluser can back up and remove users' home directories + and mail spool or all the files they own on the system. + . + A custom script can be executed after each of the commands. + . + Development mailing list: + http://lists.alioth.debian.org/mailman/listinfo/adduser-devel/ +Homepage: http://alioth.debian.org/projects/adduser/ +Original-Maintainer: Debian Adduser Developers + +Package: libext2fs2 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 451 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: e2fsprogs +Version: 1.44.1-1 +Replaces: e2fslibs (<< 1.43.9-1~) +Provides: e2fslibs (= 1.44.1-1) +Depends: libc6 (>= 2.17) +Breaks: e2fslibs (<< 1.43.9-1~) +Description: ext2/ext3/ext4 file system libraries + The ext2, ext3 and ext4 file systems are successors of the original ext + ("extended") file system. They are the main file system types used for + hard disks on Debian and other Linux systems. + . + This package provides the ext2fs and e2p libraries, for userspace software + that directly accesses extended file systems. Programs that use libext2fs + include e2fsck, mke2fs, and tune2fs. Programs that use libe2p include + dumpe2fs, chattr, and lsattr. +Original-Maintainer: Theodore Y. Ts'o +Homepage: http://e2fsprogs.sourceforge.net + +Package: libacl1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 57 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: acl +Version: 2.2.52-3build1 +Depends: libattr1 (>= 1:2.4.46-8), libc6 (>= 2.14) +Conflicts: acl (<< 2.0.0), libacl1-kerberos4kth +Description: Access control list shared library + This package contains the libacl.so dynamic library containing + the POSIX 1003.1e draft standard 17 functions for manipulating + access control lists. +Original-Maintainer: Anibal Monsalve Salazar +Homepage: http://savannah.nongnu.org/projects/acl/ + +Package: ncurses-bin +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 576 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Source: ncurses +Version: 6.1-1ubuntu1.18.04 +Pre-Depends: libc6 (>= 2.14), libtinfo5 (>= 6.1) +Description: terminal-related programs and man pages + The ncurses library routines are a terminal-independent method of + updating character screens with reasonable optimization. + . + This package contains the programs used for manipulating the terminfo + database and individual terminfo entries, as well as some programs for + resetting terminals and such. +Homepage: https://invisible-island.net/ncurses/ +Original-Maintainer: Craig Small + +Package: libsepol1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 720 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: libsepol +Version: 2.7-1 +Depends: libc6 (>= 2.14) +Description: SELinux library for manipulating binary security policies + Security-enhanced Linux is a patch of the Linux kernel and a number + of utilities with enhanced security functionality designed to add + mandatory access controls to Linux. The Security-enhanced Linux + kernel contains new architectural components originally developed to + improve the security of the Flask operating system. These + architectural components provide general support for the enforcement + of many kinds of mandatory access control policies, including those + based on the concepts of Type Enforcement®, Role-based Access + Control, and Multi-level Security. + . + libsepol provides an API for the manipulation of SELinux binary policies. + It is used by checkpolicy (the policy compiler) and similar tools, as well + as by programs like load_policy that need to perform specific transformations + on binary policies such as customizing policy boolean settings. +Original-Maintainer: Debian SELinux maintainers +Homepage: http://userspace.selinuxproject.org/ + +Package: ubuntu-keyring +Status: install ok installed +Priority: important +Section: misc +Installed-Size: 42 +Maintainer: Dimitri John Ledkov +Architecture: all +Multi-Arch: foreign +Version: 2018.02.28 +Replaces: ubuntu-cloudimage-keyring (<< 2018.02.05) +Breaks: ubuntu-cloudimage-keyring (<< 2018.02.05) +Description: GnuPG keys of the Ubuntu archive + The Ubuntu project digitally signs its Release files. This package + contains the archive keys used for that. + +Package: libgcc1 +Status: install ok installed +Priority: required +Section: libs +Installed-Size: 112 +Maintainer: Ubuntu Core developers +Architecture: amd64 +Multi-Arch: same +Source: gcc-8 (8-20180414-1ubuntu2) +Version: 1:8-20180414-1ubuntu2 +Depends: gcc-8-base (= 8-20180414-1ubuntu2), libc6 (>= 2.14) +Breaks: gcc-4.3 (<< 4.3.6-1), gcc-4.4 (<< 4.4.6-4), gcc-4.5 (<< 4.5.3-2) +Description: GCC support library + Shared version of the support library, a library of internal subroutines + that GCC uses to overcome shortcomings of particular machines, or + special needs for some languages. +Homepage: http://gcc.gnu.org/ +Original-Maintainer: Debian GCC Maintainers + +Package: util-linux +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 3374 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 2.31.1-0.4ubuntu3.1 +Replaces: bash-completion (<< 1:2.1-4.1~), initscripts (<< 2.88dsf-59.2~), mount (<< 2.29.2-3~), s390-tools (<< 1.37.0-0ubuntu7~), sysvinit-utils (<< 2.88dsf-59.1~) +Depends: fdisk +Pre-Depends: libaudit1 (>= 1:2.2.1), libblkid1 (>= 2.31.1), libc6 (>= 2.25), libmount1 (>= 2.25), libpam0g (>= 0.99.7.1), libselinux1 (>= 2.6-3~), libsmartcols1 (>= 2.30.2), libsystemd0, libtinfo5 (>= 6), libudev1 (>= 183), libuuid1 (>= 2.16), zlib1g (>= 1:1.1.4) +Suggests: dosfstools, kbd | console-tools, util-linux-locales +Breaks: bash-completion (<< 1:2.1-4.1~), grml-debootstrap (<< 0.68), mount (<< 2.29.2-3~), s390-tools (<< 1.37.0-0ubuntu7~), sysvinit-utils (<< 2.88dsf-59.4~) +Conffiles: + /etc/init.d/hwclock.sh 1ca5c0743fa797ffa364db95bb8d8d8e + /etc/pam.d/runuser b8b44b045259525e0fae9e38fdb2aeeb + /etc/pam.d/runuser-l 2106ea05877e8913f34b2c77fa02be45 +Description: miscellaneous system utilities + This package contains a number of important utilities, most of which + are oriented towards maintenance of your system. Some of the more + important utilities included in this package allow you to view kernel + messages, create new filesystems, view block device information, + interface with real time clock, etc. +Original-Maintainer: LaMont Jones + +Package: sed +Essential: yes +Status: install ok installed +Priority: required +Section: utils +Installed-Size: 320 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: foreign +Version: 4.4-2 +Pre-Depends: libc6 (>= 2.14), libselinux1 (>= 1.32) +Description: GNU stream editor for filtering/transforming text + sed reads the specified files or the standard input if no + files are specified, makes editing changes according to a + list of commands, and writes the results to the standard + output. +Original-Maintainer: Clint Adams +Homepage: https://www.gnu.org/software/sed/ + +Package: libsmartcols1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 287 +Maintainer: Ubuntu Developers +Architecture: amd64 +Multi-Arch: same +Source: util-linux +Version: 2.31.1-0.4ubuntu3.1 +Depends: libc6 (>= 2.25) +Description: smart column output alignment library + This smart column output alignment library is used by fdisk utilities. +Original-Maintainer: LaMont Jones + +Package: libustr-1.0-1 +Status: install ok installed +Priority: optional +Section: libs +Installed-Size: 287 +Maintainer: Vaclav Ovsik +Architecture: amd64 +Multi-Arch: same +Source: ustr (1.0.4-3) +Version: 1.0.4-3+b2 +Depends: libc6 (>= 2.14) +Pre-Depends: multiarch-support +Description: Micro string library: shared library + ustr (Micro string library) is a string API for C. It has tiny overhead over + just plain strdup(), is much safer, is easier to use, is faster for many + operations, can be used with read-only or automatically allocated data. You + don't even need to link to the library to use it (so there are no + dependencies). + . + This package contains the shared library for ustr. +Homepage: http://www.and.org/ustr/ + +Package: git +Status: deinstall ok config-files +Priority: optional +Section: vcs +Installed-Size: 35396 +Maintainer: Gerrit Pape +Architecture: amd64 +Multi-Arch: foreign +Version: 1:2.20.1-2+deb10u3 +Config-Version: 1:2.20.1-2+deb10u3 +Replaces: git-core (<< 1:1.7.0.4-1.), gitweb (<< 1:1.7.4~rc1) + + diff --git a/pkg/fanal/analyzer/pkg/rpm/archive.go b/pkg/fanal/analyzer/pkg/rpm/archive.go new file mode 100644 index 000000000000..0fe4f6bb580b --- /dev/null +++ b/pkg/fanal/analyzer/pkg/rpm/archive.go @@ -0,0 +1,168 @@ +package rpm + +import ( + "context" + "errors" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/package-url/packageurl-go" + "github.com/sassoftware/go-rpmutils" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/utils" +) + +const archiveVersion = 1 + +func init() { + analyzer.RegisterAnalyzer(newRPMArchiveAnalyzer()) +} + +type rpmArchiveAnalyzer struct { + logger *log.Logger +} + +func newRPMArchiveAnalyzer() *rpmArchiveAnalyzer { + return &rpmArchiveAnalyzer{ + logger: log.WithPrefix("rpm-archive"), + } +} + +func (a *rpmArchiveAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + rpm, err := rpmutils.ReadRpm(input.Content) + if err != nil { + return nil, xerrors.Errorf("failed to read rpm (%s): %w", input.FilePath, err) + } + pkg, err := a.parseHeader(rpm.Header) + if err != nil { + return nil, xerrors.Errorf("failed to parse rpm header: %w", err) + } + pkg.FilePath = input.FilePath + pkg.Identifier.PURL = a.generatePURL(&pkg) + + return &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: input.FilePath, + Packages: types.Packages{pkg}, + }, + }, + }, nil +} + +func (a *rpmArchiveAnalyzer) parseHeader(h *rpmutils.RpmHeader) (types.Package, error) { + if h == nil { + return types.Package{}, errors.New("rpm header is nil") + } + // Getting metadata + nevra, err := h.GetNEVRA() + if a.unexpectedError(err) != nil { + return types.Package{}, xerrors.Errorf("failed to get NEVRA: %w", err) + } + epoch, err := strconv.Atoi(nevra.Epoch) + if a.unexpectedError(err) != nil { + return types.Package{}, xerrors.Errorf("failed to convert epoch to int (%s): %w", nevra.Name, err) + } + licenses, err := h.GetStrings(rpmutils.LICENSE) + if a.unexpectedError(err) != nil { + return types.Package{}, xerrors.Errorf("failed to get licenses: %w", err) + } + srcName, srcVer, srcRel, err := a.parseSourceRPM(h) + if err != nil { + return types.Package{}, xerrors.Errorf("failed to parse source rpm: %w", err) + } + vendor, err := h.GetString(rpmutils.VENDOR) + if a.unexpectedError(err) != nil { + return types.Package{}, xerrors.Errorf("failed to get vendor: %w", err) + } + + // TODO: add the const to go-rpmutils + // cf. https://github.com/rpm-software-management/rpm/blob/rpm-4.16.0-release/lib/rpmtag.h#L375 + const modularityLabelTag = 5096 + modularityLabel, err := h.GetString(modularityLabelTag) + if a.unexpectedError(err) != nil { + return types.Package{}, xerrors.Errorf("failed to get modularitylabel: %w", err) + } + + return types.Package{ + Name: nevra.Name, + Version: nevra.Version, + Release: nevra.Release, + Epoch: epoch, + Arch: nevra.Arch, + SrcName: srcName, + SrcVersion: srcVer, + SrcRelease: srcRel, + SrcEpoch: epoch, + Licenses: licenses, + Maintainer: vendor, + Modularitylabel: modularityLabel, + }, nil +} + +func (a *rpmArchiveAnalyzer) parseSourceRPM(h *rpmutils.RpmHeader) (string, string, string, error) { + sourceRpm, err := h.GetString(rpmutils.SOURCERPM) + if a.unexpectedError(err) != nil { + return "", "", "", xerrors.Errorf("failed to get source rpm: %w", err) + } else if sourceRpm == "(none)" || sourceRpm == "" { + return "", "", "", nil + } + + srcName, srcVer, srcRel, err := splitFileName(sourceRpm) + if err != nil { + a.logger.Debug("Invalid Source RPM Found", log.String("sourcerpm", sourceRpm)) + } + return srcName, srcVer, srcRel, nil +} + +func (a *rpmArchiveAnalyzer) generatePURL(pkg *types.Package) *packageurl.PackageURL { + vendor := strings.ToLower(pkg.Maintainer) + + // TODO: Handle more vendors + var ns string + switch { + case strings.Contains(vendor, "red hat"): + ns = "redhat" + case strings.Contains(vendor, "fedora"): + ns = "fedora" + case strings.Contains(vendor, "opensuse"): + ns = "opensuse" + case strings.Contains(vendor, "suse"): + ns = "suse" + } + var qualifiers packageurl.Qualifiers + if pkg.Arch != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "arch", + Value: pkg.Arch, + }) + } + return packageurl.NewPackageURL(packageurl.TypeRPM, ns, pkg.Name, utils.FormatVersion(*pkg), qualifiers, "") +} + +func (a *rpmArchiveAnalyzer) unexpectedError(err error) error { + var rerr rpmutils.NoSuchTagError + if errors.As(err, &rerr) { + a.logger.Debug("RPM tag not found", log.Err(rerr)) + return nil + } + return err +} + +func (a *rpmArchiveAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return filepath.Ext(filePath) == ".rpm" +} + +func (a *rpmArchiveAnalyzer) Type() analyzer.Type { + return analyzer.TypeRpmArchive +} + +func (a *rpmArchiveAnalyzer) Version() int { + return archiveVersion +} diff --git a/pkg/fanal/analyzer/pkg/rpm/archive_test.go b/pkg/fanal/analyzer/pkg/rpm/archive_test.go new file mode 100644 index 000000000000..469e62045a89 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/rpm/archive_test.go @@ -0,0 +1,86 @@ +package rpm + +import ( + "os" + "strings" + "testing" + + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_rpmArchiveAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + input analyzer.AnalysisInput + want *analyzer.AnalysisResult + wantErr require.ErrorAssertionFunc + }{ + { + name: "valid", + input: analyzer.AnalysisInput{ + FilePath: "testdata/valid.rpm", + Content: lo.Must(os.Open("testdata/socat-1.7.3.2-2.el7.x86_64.rpm")), // Must run 'mage rpm:fixtures' before this test + }, + want: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "testdata/valid.rpm", + Packages: types.Packages{ + { + Name: "socat", + Version: "1.7.3.2", + Release: "2.el7", + Arch: "x86_64", + SrcName: "socat", + SrcVersion: "1.7.3.2", + SrcRelease: "2.el7", + FilePath: "testdata/valid.rpm", + Licenses: []string{ + "GPLv2", + }, + Maintainer: "Red Hat, Inc.", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "socat", + Version: "1.7.3.2-2.el7", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "x86_64", + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "broken", + input: analyzer.AnalysisInput{ + FilePath: "testdata/broken.rpm", + Content: strings.NewReader(`broken`), + }, + wantErr: require.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := newRPMArchiveAnalyzer() + got, err := a.Analyze(t.Context(), tt.input) + tt.wantErr(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/pkg/rpm/rpm.go b/pkg/fanal/analyzer/pkg/rpm/rpm.go new file mode 100644 index 000000000000..e6fa93ebe957 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/rpm/rpm.go @@ -0,0 +1,312 @@ +package rpm + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "slices" + "sort" + "strings" + + rpmdb "github.com/knqyf263/go-rpmdb/pkg" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +func init() { + analyzer.RegisterAnalyzer(newRPMPkgAnalyzer()) +} + +const version = 3 + +var ( + requiredFiles = []string{ + // Berkeley DB + "usr/lib/sysimage/rpm/Packages", + "var/lib/rpm/Packages", + + // NDB + "usr/lib/sysimage/rpm/Packages.db", + "var/lib/rpm/Packages.db", + + // SQLite3 + "usr/lib/sysimage/rpm/rpmdb.sqlite", + "var/lib/rpm/rpmdb.sqlite", + + // CoreOS + "usr/share/rpm/rpmdb.sqlite", + } + + errUnexpectedNameFormat = xerrors.New("unexpected name format") +) + +var osVendors = []string{ + "Amazon Linux", // Amazon Linux 1 + "Amazon.com", // Amazon Linux 2 + "CentOS", // CentOS + "Fedora Project", // Fedora + "Oracle America", // Oracle Linux + "Red Hat", // Red Hat + "AlmaLinux", // AlmaLinux + "CloudLinux", // AlmaLinux + "VMware", // Photon OS + "SUSE", // SUSE Linux Enterprise + "openSUSE", // openSUSE + "Microsoft Corporation", // CBL-Mariner + "Rocky", // Rocky Linux +} + +type rpmPkgAnalyzer struct{} + +func newRPMPkgAnalyzer() *rpmPkgAnalyzer { + return &rpmPkgAnalyzer{} +} + +type RPMDB interface { + ListPackages() ([]*rpmdb.PackageInfo, error) +} + +func (a rpmPkgAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + ctx = log.WithContextPrefix(ctx, "rpm") + parsedPkgs, installedFiles, err := a.parsePkgInfo(ctx, input.Content) + if err != nil { + return nil, xerrors.Errorf("failed to parse rpmdb: %w", err) + } + + return &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: input.FilePath, + Packages: parsedPkgs, + }, + }, + SystemInstalledFiles: installedFiles, + }, nil +} + +func (a rpmPkgAnalyzer) parsePkgInfo(ctx context.Context, rc io.Reader) (types.Packages, []string, error) { + filePath, err := writeToTempFile(rc) + if err != nil { + return nil, nil, xerrors.Errorf("temp file error: %w", err) + } + defer os.RemoveAll(filepath.Dir(filePath)) // Remove the temp dir + + // rpm-python 4.11.3 rpm-4.11.3-35.el7.src.rpm + // Extract binary package names because RHSA refers to binary package names. + db, err := rpmdb.Open(filePath) + if err != nil { + return nil, nil, xerrors.Errorf("failed to open RPM DB: %w", err) + } + defer db.Close() + + return a.listPkgs(ctx, db) +} + +func (a rpmPkgAnalyzer) listPkgs(ctx context.Context, db RPMDB) (types.Packages, []string, error) { + // equivalent: + // new version: rpm -qa --qf "%{NAME} %{EPOCHNUM} %{VERSION} %{RELEASE} %{SOURCERPM} %{ARCH}\n" + // old version: rpm -qa --qf "%{NAME} %{EPOCH} %{VERSION} %{RELEASE} %{SOURCERPM} %{ARCH}\n" + pkgList, err := db.ListPackages() + if err != nil { + return nil, nil, xerrors.Errorf("failed to list packages: %w", err) + } + + var pkgs []types.Package + var installedFiles []string + provides := make(map[string]string) + for _, pkg := range pkgList { + arch := pkg.Arch + if arch == "" { + arch = "None" + } + + // parse source rpm + var srcName, srcVer, srcRel string + if pkg.SourceRpm != "(none)" && pkg.SourceRpm != "" { + // source epoch is not included in SOURCERPM + srcName, srcVer, srcRel, err = splitFileName(pkg.SourceRpm) + if err != nil { + log.DebugContext(ctx, "Invalid Source RPM Found", log.String("sourcerpm", pkg.SourceRpm)) + } + } + + // Check if the package is vendor-provided. + // If the package is not provided by vendor, the installed files should not be skipped. + repo := types.PackageRepository{ + Class: types.RepositoryClassThirdParty, + } + var files []string + if packageProvidedByVendor(pkg) { + repo.Class = types.RepositoryClassOfficial + files, err = pkg.InstalledFileNames() + if err != nil { + return nil, nil, xerrors.Errorf("unable to get installed files: %w", err) + } + + for i, file := range files { + files[i] = filepath.ToSlash(file) + } + } + + // RPM DB uses MD5 digest + // https://rpm-software-management.github.io/rpm/manual/tags.html#signatures-and-digests + var d digest.Digest + if pkg.SigMD5 != "" { + d = digest.NewDigestFromString(digest.MD5, pkg.SigMD5) + } + + var licenses []string + if pkg.License != "" { + licenses = []string{pkg.License} + } + + p := types.Package{ + ID: fmt.Sprintf("%s@%s-%s.%s", pkg.Name, pkg.Version, pkg.Release, pkg.Arch), + Name: pkg.Name, + Epoch: pkg.EpochNum(), + Version: pkg.Version, + Release: pkg.Release, + Arch: arch, + SrcName: srcName, + SrcEpoch: pkg.EpochNum(), // NOTE: use epoch of binary package as epoch of src package + SrcVersion: srcVer, + SrcRelease: srcRel, + Modularitylabel: pkg.Modularitylabel, + Licenses: licenses, + DependsOn: pkg.Requires, // Will be replaced with package IDs + Maintainer: pkg.Vendor, + Repository: repo, + Digest: d, + InstalledFiles: files, + } + pkgs = append(pkgs, p) + installedFiles = append(installedFiles, files...) + + // It contains mappings between package-providing files and package IDs + // e.g. + // "libc.so.6()(64bit)" => "glibc-2.12-1.212.el6.x86_64" + // "rtld(GNU_HASH)" => "glibc-2.12-1.212.el6.x86_64" + for _, provide := range pkg.Provides { + provides[provide] = p.ID + } + } + + // Replace required files with package IDs + consolidateDependencies(pkgs, provides) + + return pkgs, installedFiles, nil +} + +func (a rpmPkgAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredFiles, filePath) +} + +func (a rpmPkgAnalyzer) Type() analyzer.Type { + return analyzer.TypeRpm +} + +func (a rpmPkgAnalyzer) Version() int { + return version +} + +// StaticPaths returns a list of static file paths to analyze +func (a rpmPkgAnalyzer) StaticPaths() []string { + return requiredFiles +} + +// splitFileName returns a name, version, release, epoch, arch: +// +// e.g. +// foo-1.0-1.i386.rpm => foo, 1.0, 1, i386 +// 1:bar-9-123a.ia64.rpm => bar, 9, 123a, 1, ia64 +// +// https://github.com/rpm-software-management/yum/blob/043e869b08126c1b24e392f809c9f6871344c60d/rpmUtils/miscutils.py#L301 +func splitFileName(filename string) (name, ver, rel string, err error) { + filename = strings.TrimSuffix(filename, ".rpm") + + archIndex := strings.LastIndex(filename, ".") + if archIndex == -1 { + return "", "", "", errUnexpectedNameFormat + } + + relIndex := strings.LastIndex(filename[:archIndex], "-") + if relIndex == -1 { + return "", "", "", errUnexpectedNameFormat + } + rel = filename[relIndex+1 : archIndex] + + verIndex := strings.LastIndex(filename[:relIndex], "-") + if verIndex == -1 { + return "", "", "", errUnexpectedNameFormat + } + ver = filename[verIndex+1 : relIndex] + + name = filename[:verIndex] + return name, ver, rel, nil +} + +func packageProvidedByVendor(pkg *rpmdb.PackageInfo) bool { + if pkg.Vendor == "" { + // Official Amazon packages may not contain `Vendor` field: + // https://github.com/aquasecurity/trivy/issues/5887 + return strings.Contains(pkg.Release, "amzn") + } + + for _, vendor := range osVendors { + if strings.HasPrefix(pkg.Vendor, vendor) { + return true + } + } + + return false +} + +func writeToTempFile(rc io.Reader) (string, error) { + tmpDir, err := xos.MkdirTemp("", "rpmdb-") + if err != nil { + return "", xerrors.Errorf("failed to create a temp dir: %w", err) + } + + filePath := filepath.Join(tmpDir, "Packages") + f, err := os.Create(filePath) + if err != nil { + return "", xerrors.Errorf("failed to create a package file: %w", err) + } + + if _, err = io.Copy(f, rc); err != nil { + return "", xerrors.Errorf("failed to copy a package file: %w", err) + } + + // The temp file must be closed before being opened as Berkeley DB. + if err = f.Close(); err != nil { + return "", xerrors.Errorf("failed to close a temp file: %w", err) + } + + return filePath, nil +} + +func consolidateDependencies(pkgs []types.Package, provides map[string]string) { + for i := range pkgs { + // e.g. "libc.so.6()(64bit)" => "glibc-2.12-1.212.el6.x86_64" + pkgs[i].DependsOn = lo.FilterMap(pkgs[i].DependsOn, func(d string, _ int) (string, bool) { + if pkgID, ok := provides[d]; ok && pkgs[i].ID != pkgID { + return pkgID, true + } + return "", false + }) + sort.Strings(pkgs[i].DependsOn) + pkgs[i].DependsOn = slices.Compact(pkgs[i].DependsOn) + + if len(pkgs[i].DependsOn) == 0 { + pkgs[i].DependsOn = nil + } + } +} diff --git a/pkg/fanal/analyzer/pkg/rpm/rpm_test.go b/pkg/fanal/analyzer/pkg/rpm/rpm_test.go new file mode 100644 index 000000000000..8d87e2510e92 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/rpm/rpm_test.go @@ -0,0 +1,287 @@ +package rpm + +import ( + "errors" + "os" + "strings" + "testing" + + rpmdb "github.com/knqyf263/go-rpmdb/pkg" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type mockRPMDB struct { + packages []*rpmdb.PackageInfo + err error +} + +func (m *mockRPMDB) ListPackages() ([]*rpmdb.PackageInfo, error) { + if m.err != nil { + return nil, m.err + } + return m.packages, nil +} + +func Test_rpmPkgAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + input analyzer.AnalysisInput + want int + wantErr string + }{ + { + name: "valid", + input: analyzer.AnalysisInput{ + FilePath: "testdata/valid", + Content: lo.Must(os.Open("testdata/valid")), + }, + want: 1, + }, + { + name: "broken", + input: analyzer.AnalysisInput{ + FilePath: "testdata/valid", + Content: strings.NewReader("broken"), + }, + want: 0, + wantErr: "unexpected EOF", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := rpmPkgAnalyzer{} + got, err := a.Analyze(t.Context(), tt.input) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Len(t, got.PackageInfos, tt.want) + }) + } +} + +func Test_splitFileName(t *testing.T) { + tests := []struct { + name string + filename string + wantName string + wantVer string + wantRel string + wantErr bool + }{ + { + name: "valid name", + filename: "glibc-2.17-307.el7.1.src.rpm", + wantName: "glibc", + wantVer: "2.17", + wantRel: "307.el7.1", + wantErr: false, + }, + { + name: "invalid name", + filename: "elasticsearch-5.6.16-1-src.rpm", + wantName: "", + wantVer: "", + wantRel: "", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotName, gotVer, gotRel, err := splitFileName(tt.filename) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + assert.Equal(t, tt.wantName, gotName) + assert.Equal(t, tt.wantVer, gotVer) + assert.Equal(t, tt.wantRel, gotRel) + }) + } +} + +func Test_rpmPkgAnalyzer_listPkgs(t *testing.T) { + type mock struct { + packages []*rpmdb.PackageInfo + err error + } + tests := []struct { + name string + mock mock + wantPkgs types.Packages + wantFiles []string + wantErr string + }{ + { + name: "normal", + mock: mock{ + packages: []*rpmdb.PackageInfo{ + { + Name: "glibc", + Version: "2.17", + Release: "307.el7.1", + Arch: "x86_64", + SourceRpm: "glibc-2.17-317.el7.src.rpm", + DirNames: []string{ + "/etc", + "/lib64", + }, + DirIndexes: []int32{ + 0, + 0, + 1, + }, + BaseNames: []string{ + "ld.so.conf", + "rpc", + "libm-2.27.so", + }, + Vendor: "Red Hat", + }, + }, + }, + wantPkgs: types.Packages{ + { + ID: "glibc@2.17-307.el7.1.x86_64", + Name: "glibc", + Version: "2.17", + Release: "307.el7.1", + Arch: "x86_64", + SrcName: "glibc", + SrcVersion: "2.17", + SrcRelease: "317.el7", + Maintainer: "Red Hat", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + InstalledFiles: []string{ + "/etc/ld.so.conf", + "/etc/rpc", + "/lib64/libm-2.27.so", + }, + }, + }, + wantFiles: []string{ + "/etc/ld.so.conf", + "/etc/rpc", + "/lib64/libm-2.27.so", + }, + }, + { + name: "Amazon official package without `Vendor` field", + mock: mock{ + packages: []*rpmdb.PackageInfo{ + { + Name: "curl-minimal", + Version: "8.3.0", + Release: "1.amzn2023.0.2", + Arch: "aarch64", + SourceRpm: "curl-8.3.0-1.amzn2023.0.2.src.rpm", + DirNames: []string{ + "/usr/bin/", + "/usr/lib/", + "/usr/lib/.build-id/", + "/usr/lib/.build-id/aa/", + "/usr/share/man/man1/", + }, + DirIndexes: []int32{ + 0, + 1, + 2, + 3, + 4, + }, + BaseNames: []string{ + "curl", + ".build-id", + "aa", + "d987ea9bc1c73706d12c7a143ee792117851ff", + "curl.1.gz", + }, + Vendor: "", + }, + }, + }, + wantPkgs: types.Packages{ + { + ID: "curl-minimal@8.3.0-1.amzn2023.0.2.aarch64", + Name: "curl-minimal", + Version: "8.3.0", + Release: "1.amzn2023.0.2", + Arch: "aarch64", + SrcName: "curl", + SrcVersion: "8.3.0", + SrcRelease: "1.amzn2023.0.2", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + InstalledFiles: []string{ + "/usr/bin/curl", + "/usr/lib/.build-id", + "/usr/lib/.build-id/aa", + "/usr/lib/.build-id/aa/d987ea9bc1c73706d12c7a143ee792117851ff", + "/usr/share/man/man1/curl.1.gz", + }, + }, + }, + wantFiles: []string{ + "/usr/bin/curl", + "/usr/lib/.build-id", + "/usr/lib/.build-id/aa", + "/usr/lib/.build-id/aa/d987ea9bc1c73706d12c7a143ee792117851ff", + "/usr/share/man/man1/curl.1.gz", + }, + }, + { + name: "invalid source rpm", + mock: mock{ + packages: []*rpmdb.PackageInfo{ + { + Name: "glibc", + Version: "2.17", + Release: "307.el7.1", + Arch: "x86_64", + SourceRpm: "invalid", + }, + }, + }, + wantPkgs: types.Packages{ + { + ID: "glibc@2.17-307.el7.1.x86_64", + Name: "glibc", + Version: "2.17", + Release: "307.el7.1", + Arch: "x86_64", + Repository: types.PackageRepository{Class: types.RepositoryClassThirdParty}, + }, + }, + }, + { + name: "sad path", + mock: mock{ + err: errors.New("unexpected error"), + }, + wantErr: "unexpected error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &mockRPMDB{ + packages: tt.mock.packages, + err: tt.mock.err, + } + + a := newRPMPkgAnalyzer() + gotPkgs, gotFiles, err := a.listPkgs(t.Context(), m) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + assert.Equal(t, tt.wantPkgs, gotPkgs) + assert.Equal(t, tt.wantFiles, gotFiles) + }) + } +} diff --git a/pkg/fanal/analyzer/pkg/rpm/rpmqa.go b/pkg/fanal/analyzer/pkg/rpm/rpmqa.go new file mode 100644 index 000000000000..093cea24b615 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/rpm/rpmqa.go @@ -0,0 +1,106 @@ +package rpm + +import ( + "bufio" + "context" + "os" + "slices" + "strconv" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +func init() { + analyzer.RegisterAnalyzer(&rpmqaPkgAnalyzer{}) +} + +const versionRpmqa = 1 + +var ( + // For CBL-Mariner Distroless + requiredRpmqaFiles = []string{"var/lib/rpmmanifest/container-manifest-2"} +) + +// rpmqaPkgAnalyzer parses the output of +// "rpm -qa --qf %{NAME}\t%{VERSION}-%{RELEASE}\t%{INSTALLTIME}\t%{BUILDTIME}\t%{VENDOR}\t(none)\t%{SIZE}\t%{ARCH}\t%{EPOCHNUM}\t%{SOURCERPM}". +type rpmqaPkgAnalyzer struct{} + +func (a rpmqaPkgAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + pkgs, err := a.parseRpmqaManifest(input.Content) + if err != nil { + return nil, xerrors.Errorf("failed to parse rpmqa manifest: %w", err) + } + return &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: input.FilePath, + Packages: pkgs, + }, + }, + }, nil +} + +func (a rpmqaPkgAnalyzer) parseRpmqaManifest(r xio.ReadSeekerAt) ([]types.Package, error) { + var pkgs []types.Package + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + var name, ver, rel, sourceRpm, arch string + var epoch int + // %{NAME}\t%{VERSION}-%{RELEASE}\t%{INSTALLTIME}\t%{BUILDTIME}\t%{VENDOR}\t(none)\t%{SIZE}\t%{ARCH}\t%{EPOCHNUM}\t%{SOURCERPM} + s := strings.Split(line, "\t") + if len(s) != 10 { + return nil, xerrors.Errorf("failed to parse a line (%s)", line) + } + name = s[0] + arch = s[7] + sourceRpm = s[9] + if verRel := strings.Split(s[1], "-"); len(verRel) == 2 { + ver = verRel[0] + rel = verRel[1] + } else { + return nil, xerrors.Errorf("failed to split a version (%s)", s[1]) + } + srcName, srcVer, srcRel, err := splitFileName(sourceRpm) + if err != nil { + return nil, xerrors.Errorf("failed to split source rpm: %w", err) + } + epoch, err = strconv.Atoi(s[8]) + if err != nil { + return nil, xerrors.Errorf("failed to parse epoch number (%s): %w", s[8], err) + } + pkgs = append(pkgs, types.Package{ + Name: name, + Version: ver, + Epoch: epoch, + Release: rel, + Arch: arch, + SrcName: srcName, + SrcEpoch: epoch, + SrcVersion: srcVer, + SrcRelease: srcRel, + }) + } + return pkgs, nil +} + +func (a rpmqaPkgAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredRpmqaFiles, filePath) +} + +func (a rpmqaPkgAnalyzer) Type() analyzer.Type { + return analyzer.TypeRpmqa +} + +func (a rpmqaPkgAnalyzer) Version() int { + return versionRpmqa +} + +func (a rpmqaPkgAnalyzer) StaticPaths() []string { + return requiredRpmqaFiles +} diff --git a/pkg/fanal/analyzer/pkg/rpm/rpmqa_test.go b/pkg/fanal/analyzer/pkg/rpm/rpmqa_test.go new file mode 100644 index 000000000000..5c933bcb2364 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/rpm/rpmqa_test.go @@ -0,0 +1,87 @@ +package rpm + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestParseMarinerDistrolessManifest(t *testing.T) { + tests := []struct { + name string + content string + wantPkgs []types.Package + wantErr string + }{ + { + name: "happy path", + content: `mariner-release 2.0-12.cm2 1653816591 1653753130 Microsoft Corporation (none) 580 noarch 0 mariner-release-2.0-12.cm2.src.rpm +filesystem 1.1-9.cm2 1653816591 1653628924 Microsoft Corporation (none) 7596 x86_64 0 filesystem-1.1-9.cm2.src.rpm +glibc 2.35-2.cm2 1653816591 1653628955 Microsoft Corporation (none) 10855265 x86_64 0 glibc-2.35-2.cm2.src.rpm +ca-certificates-base 3.0.0-8.azl3 1748892790 1735838940 Microsoft Corporation (none) 130628 noarch 1 ca-certificates-3.0.0-8.azl3.src.rpm`, + wantPkgs: []types.Package{ + { + Name: "mariner-release", + Version: "2.0", + Release: "12.cm2", + Arch: "noarch", + SrcName: "mariner-release", + SrcVersion: "2.0", + SrcRelease: "12.cm2", + }, + { + Name: "filesystem", + Version: "1.1", + Release: "9.cm2", + Arch: "x86_64", + SrcName: "filesystem", + SrcVersion: "1.1", + SrcRelease: "9.cm2", + }, + { + Name: "glibc", + Version: "2.35", + Release: "2.cm2", + Arch: "x86_64", + SrcName: "glibc", + SrcVersion: "2.35", + SrcRelease: "2.cm2", + }, + { + Name: "ca-certificates-base", + Version: "3.0.0", + Epoch: 1, + Release: "8.azl3", + Arch: "noarch", + SrcName: "ca-certificates", + SrcEpoch: 1, + SrcVersion: "3.0.0", + SrcRelease: "8.azl3", + }, + }, + }, + { + name: "sab path", + content: "filesystem\t1.1-7.cm1\t1653164283\t1599428094", + wantErr: "failed to parse a line (filesystem\t1.1-7.cm1\t1653164283\t1599428094)", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + a := rpmqaPkgAnalyzer{} + result, err := a.parseRpmqaManifest(strings.NewReader(test.content)) + if test.wantErr != "" { + require.Error(t, err) + assert.Equal(t, test.wantErr, err.Error()) + } else { + require.NoError(t, err) + assert.Equal(t, test.wantPkgs, result) + } + }) + } +} diff --git a/pkg/fanal/analyzer/pkg/rpm/testdata/fixture.go b/pkg/fanal/analyzer/pkg/rpm/testdata/fixture.go new file mode 100644 index 000000000000..c9b1c0779f61 --- /dev/null +++ b/pkg/fanal/analyzer/pkg/rpm/testdata/fixture.go @@ -0,0 +1,63 @@ +package rpm + +import ( + "io" + "log/slog" + "net/http" + "os" + "path" + "path/filepath" + "runtime" + + "github.com/magefile/mage/target" + "golang.org/x/xerrors" +) + +const url = "https://mirror.openshift.com/pub/openshift-v4/amd64/dependencies/rpms/4.10-beta/socat-1.7.3.2-2.el7.x86_64.rpm" + +// Fixtures downloads RPM files for unit tests +func Fixtures() error { + _, filePath, _, _ := runtime.Caller(0) + dir := filepath.Dir(filePath) + dst := filepath.Join(dir, path.Base(url)) + + // Download the file only when it is needed + if updated, err := target.Path(dst, filePath); err != nil { + return err + } else if !updated { + return nil + } + return downloadFile(url, dst) +} + +// downloadFile downloads a file from the given URL and saves it with the original filename +// TODO: move this function to a common package for Mage +func downloadFile(url, dst string) error { + slog.Info("Downloading...", slog.String("url", url)) + + // Send a GET request to the URL + resp, err := http.Get(url) + if err != nil { + return xerrors.Errorf("error sending GET request: %v", err) + } + defer resp.Body.Close() + + // Check if the response status code is OK (200) + if resp.StatusCode != http.StatusOK { + return xerrors.Errorf("unexpected status code: %d", resp.StatusCode) + } + + // Create a new file with the extracted filename + out, err := os.Create(dst) + if err != nil { + return xerrors.Errorf("error creating file: %v", err) + } + defer out.Close() + + // Copy the response body to the file + if _, err = io.Copy(out, resp.Body); err != nil { + return xerrors.Errorf("error writing to file: %v", err) + } + + return nil +} diff --git a/pkg/fanal/analyzer/pkg/rpm/testdata/valid b/pkg/fanal/analyzer/pkg/rpm/testdata/valid new file mode 100644 index 000000000000..35a61a826c70 Binary files /dev/null and b/pkg/fanal/analyzer/pkg/rpm/testdata/valid differ diff --git a/pkg/fanal/analyzer/repo/apk/apk.go b/pkg/fanal/analyzer/repo/apk/apk.go new file mode 100644 index 000000000000..c7538d538158 --- /dev/null +++ b/pkg/fanal/analyzer/repo/apk/apk.go @@ -0,0 +1,102 @@ +package apk + +import ( + "bufio" + "context" + "os" + "regexp" + "slices" + + "golang.org/x/xerrors" + + ver "github.com/aquasecurity/go-version/pkg/version" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + analyzer.RegisterAnalyzer(&apkRepoAnalyzer{}) +} + +const version = 1 +const edgeVersion = "edge" + +var ( + requiredFiles = []string{"etc/apk/repositories"} + urlParseRegexp = regexp.MustCompile(`(https*|ftp)://[0-9A-Za-z.-]+/([A-Za-z]+)/v?([0-9A-Za-z_.-]+)/`) +) + +type apkRepoAnalyzer struct{} + +func (a apkRepoAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + scanner := bufio.NewScanner(input.Content) + var osFamily types.OSType + var repoVer string + for scanner.Scan() { + line := scanner.Text() + + m := urlParseRegexp.FindStringSubmatch(line) + if len(m) != 4 { + continue + } + + newOSFamily := types.OSType(m[2]) + newVersion := m[3] + + // Find OS Family + if osFamily != "" && osFamily != newOSFamily { + return nil, xerrors.Errorf("mixing different distributions in etc/apk/repositories: %s != %s", osFamily, newOSFamily) + } + osFamily = newOSFamily + + // Find max Release version + switch { + case repoVer == "": + repoVer = newVersion + case repoVer == edgeVersion || newVersion == edgeVersion: + repoVer = edgeVersion + default: + oldVer, err := ver.Parse(repoVer) + if err != nil { + continue + } + newVer, err := ver.Parse(newVersion) + if err != nil { + continue + } + + // Take the maximum version in apk repositories + if newVer.GreaterThan(oldVer) { + repoVer = newVersion + } + } + } + + // Currently, we support only Alpine Linux in apk repositories. + if osFamily != types.Alpine || repoVer == "" { + return nil, nil + } + + return &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: osFamily, + Release: repoVer, + }, + }, nil +} + +func (a apkRepoAnalyzer) Required(filePath string, _ os.FileInfo) bool { + return slices.Contains(requiredFiles, filePath) +} + +func (a apkRepoAnalyzer) Type() analyzer.Type { + return analyzer.TypeApkRepo +} + +func (a apkRepoAnalyzer) Version() int { + return version +} + +func (a apkRepoAnalyzer) StaticPaths() []string { + return requiredFiles +} diff --git a/pkg/fanal/analyzer/repo/apk/apk_test.go b/pkg/fanal/analyzer/repo/apk/apk_test.go new file mode 100644 index 000000000000..5734badede05 --- /dev/null +++ b/pkg/fanal/analyzer/repo/apk/apk_test.go @@ -0,0 +1,163 @@ +package apk + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_apkRepoAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + input analyzer.AnalysisInput + want *analyzer.AnalysisResult + wantErr string + }{ + { + name: "alpine", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader("http://nl.alpinelinux.org/alpine/v3.7/main"), + }, + want: &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: types.Alpine, + Release: "3.7", + }, + }, + }, + { + name: "adelie", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader("https://distfiles.adelielinux.org/adelie/1.0-beta4/system/"), + }, + want: nil, + }, + { + name: "repository has 'http' schema", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader("http://nl.alpinelinux.org/alpine/v3.7/main"), + }, + want: &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: types.Alpine, + Release: "3.7", + }, + }, + }, + { + name: "repository has 'https' schema", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader("https://dl-cdn.alpinelinux.org/alpine/v3.15/main"), + }, + want: &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: types.Alpine, + Release: "3.15", + }, + }, + }, + { + name: "repository has 'ftp' schema", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader("ftp://dl-3.alpinelinux.org/alpine/v2.6/main"), + }, + want: &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: types.Alpine, + Release: "2.6", + }, + }, + }, + { + name: "edge version", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader("https://dl-cdn.alpinelinux.org/alpine/edge/main"), + }, + want: &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: types.Alpine, + Release: "edge", + }, + }, + }, + { + name: "happy path. 'etc/apk/repositories' contains some line with v* versions", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader(`https://dl-cdn.alpinelinux.org/alpine/v3.1/main + +https://dl-cdn.alpinelinux.org/alpine/v3.10/main +`), + }, + want: &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: types.Alpine, + Release: "3.10", + }, + }, + }, + { + name: "multiple v* versions", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader(`https://dl-cdn.alpinelinux.org/alpine/v3.10/main +https://dl-cdn.alpinelinux.org/alpine/v3.1/main +`), + }, + want: &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: types.Alpine, + Release: "3.10", + }, + }, + }, + { + name: "multiple v* and edge versions", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader(`https://dl-cdn.alpinelinux.org/alpine/edge/main +https://dl-cdn.alpinelinux.org/alpine/v3.10/main +`), + }, + want: &analyzer.AnalysisResult{ + Repository: &types.Repository{ + Family: types.Alpine, + Release: "edge", + }, + }, + }, + { + name: "sad path", + input: analyzer.AnalysisInput{ + FilePath: "/etc/apk/repositories", + Content: strings.NewReader("https://dl-cdn.alpinelinux.org/alpine//edge/main"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + a := apkRepoAnalyzer{} + got, err := a.Analyze(t.Context(), test.input) + + if test.wantErr != "" { + require.Error(t, err) + assert.Equal(t, test.wantErr, err.Error()) + return + } + require.NoError(t, err) + assert.Equal(t, test.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/sbom/sbom.go b/pkg/fanal/analyzer/sbom/sbom.go new file mode 100644 index 000000000000..74dee0a1f575 --- /dev/null +++ b/pkg/fanal/analyzer/sbom/sbom.go @@ -0,0 +1,120 @@ +package sbom + +import ( + "context" + "os" + "path" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom" + "github.com/aquasecurity/trivy/pkg/types" +) + +func init() { + analyzer.RegisterAnalyzer(&sbomAnalyzer{}) +} + +const version = 1 + +var requiredSuffixes = []string{ + ".spdx", + ".spdx.json", + ".cdx", + ".cdx.json", +} + +type sbomAnalyzer struct{} + +func (a sbomAnalyzer) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + // Format auto-detection + format, err := sbom.DetectFormat(input.Content) + if err != nil { + return nil, xerrors.Errorf("failed to detect SBOM format: %w", err) + } + + ctx = log.WithContextAttrs(ctx, log.FilePath(input.FilePath)) + bom, err := sbom.Decode(ctx, input.Content, format) + if err != nil { + return nil, xerrors.Errorf("SBOM decode error: %w", err) + } + + // Bitnami images + // SPDX files are located under the /opt/bitnami/ directory + // and named with the pattern .spdx-.spdx + // ref: https://github.com/bitnami/vulndb#how-to-consume-this-cve-feed + if strings.HasPrefix(input.FilePath, "opt/bitnami/") { + handleBitnamiImages(path.Dir(input.FilePath), bom) + } + + // Add the filePath to avoid overwriting OS packages when merging packages from multiple SBOM files. + // cf. https://github.com/aquasecurity/trivy/issues/8324 + for i, pkgInfo := range bom.Packages { + bom.Packages[i].FilePath = path.Join(input.FilePath, pkgInfo.FilePath) + } + + // There are cases when FilePath for Application is empty: + // - FilePath for apps with aggregatingTypes is empty. + // - Third party SBOM without Application component. + // We need to use SBOM file path as Application.FilePath to correctly: + // - overwrite applications when merging layers. See https://github.com/aquasecurity/trivy/issues/7851 + // - show FilePath as Target of Application. See https://github.com/aquasecurity/trivy/issues/8189. + for i, app := range bom.Applications { + if app.FilePath == "" { + bom.Applications[i].FilePath = input.FilePath + } + } + + return &analyzer.AnalysisResult{ + PackageInfos: bom.Packages, + Applications: bom.Applications, + }, nil +} + +func (a sbomAnalyzer) Required(filePath string, _ os.FileInfo) bool { + // Exclude PEP 770 SBOMs in .dist-info/sboms/ directories. + // These are handled by the Python packaging analyzer instead. + // cf. https://peps.python.org/pep-0770/ + if strings.Contains(filePath, ".dist-info/sboms/") { + return false + } + + for _, suffix := range requiredSuffixes { + if strings.HasSuffix(filePath, suffix) { + return true + } + } + return false +} + +func (a sbomAnalyzer) Type() analyzer.Type { + return analyzer.TypeSBOM +} + +func (a sbomAnalyzer) Version() int { + return version +} + +func handleBitnamiImages(componentPath string, bom types.SBOM) { + for i, app := range bom.Applications { + if app.Type == ftypes.Bitnami { + // Set the component dir path to the application + bom.Applications[i].FilePath = componentPath + // Either Application.FilePath or Application.Packages[].FilePath should be set + continue + } + + for j, pkg := range app.Packages { + // Set the absolute path since SBOM in Bitnami images contain a relative path + // e.g. modules/apm/elastic-apm-agent-1.36.0.jar + // => opt/bitnami/elasticsearch/modules/apm/elastic-apm-agent-1.36.0.jar + // If the file path is empty, the file path will be set to the component dir path. + filePath := path.Join(componentPath, pkg.FilePath) + bom.Applications[i].Packages[j].FilePath = filePath + } + } +} diff --git a/pkg/fanal/analyzer/sbom/sbom_test.go b/pkg/fanal/analyzer/sbom/sbom_test.go new file mode 100644 index 000000000000..78b6ed87b1b9 --- /dev/null +++ b/pkg/fanal/analyzer/sbom/sbom_test.go @@ -0,0 +1,415 @@ +package sbom + +import ( + "os" + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_sbomAnalyzer_Analyze(t *testing.T) { + tests := []struct { + name string + file string + filePath string + want *analyzer.AnalysisResult + wantErr require.ErrorAssertionFunc + }{ + { + name: "valid elasticsearch spdx file", + file: "testdata/elasticsearch.spdx.json", + filePath: "opt/bitnami/elasticsearch/.spdx-elasticsearch.spdx", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Bitnami, + FilePath: "opt/bitnami/elasticsearch", + Packages: types.Packages{ + { + ID: "elasticsearch@8.9.1", + Name: "elasticsearch", + Version: "8.9.1", + Arch: "arm64", + Licenses: []string{"Elastic-2.0"}, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeBitnami, + Name: "elasticsearch", + Version: "8.9.1", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "arm64", + }, + }, + }, + }, + }, + }, + }, + { + Type: types.Jar, + FilePath: "opt/bitnami/elasticsearch/.spdx-elasticsearch.spdx", + Packages: types.Packages{ + { + ID: "co.elastic.apm:apm-agent:1.36.0", + Name: "co.elastic.apm:apm-agent", + Version: "1.36.0", + FilePath: "opt/bitnami/elasticsearch", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "co.elastic.apm", + Name: "apm-agent", + Version: "1.36.0", + }, + }, + }, + { + ID: "co.elastic.apm:apm-agent-cached-lookup-key:1.36.0", + Name: "co.elastic.apm:apm-agent-cached-lookup-key", + Version: "1.36.0", + FilePath: "opt/bitnami/elasticsearch", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "co.elastic.apm", + Name: "apm-agent-cached-lookup-key", + Version: "1.36.0", + }, + }, + }, + { + ID: "co.elastic.apm:apm-agent-common:1.36.0", + Name: "co.elastic.apm:apm-agent-common", + Version: "1.36.0", + FilePath: "opt/bitnami/elasticsearch", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "co.elastic.apm", + Name: "apm-agent-common", + Version: "1.36.0", + }, + }, + }, + { + ID: "co.elastic.apm:apm-agent-core:1.36.0", + Name: "co.elastic.apm:apm-agent-core", + Version: "1.36.0", + FilePath: "opt/bitnami/elasticsearch", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "co.elastic.apm", + Name: "apm-agent-core", + Version: "1.36.0", + }, + }, + }, + }, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "valid elasticsearch cdx file", + file: "testdata/cdx.json", + filePath: "opt/bitnami/elasticsearch/.spdx-elasticsearch.cdx", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Jar, + FilePath: "opt/bitnami/elasticsearch/.spdx-elasticsearch.cdx", + Packages: types.Packages{ + { + FilePath: "opt/bitnami/elasticsearch/modules/apm/elastic-apm-agent-1.36.0.jar", + ID: "co.elastic.apm:apm-agent:1.36.0", + Name: "co.elastic.apm:apm-agent", + Version: "1.36.0", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "co.elastic.apm", + Name: "apm-agent", + Version: "1.36.0", + }, + BOMRef: "pkg:maven/co.elastic.apm/apm-agent@1.36.0", + }, + }, + { + FilePath: "opt/bitnami/elasticsearch/modules/apm/elastic-apm-agent-1.36.0.jar", + ID: "co.elastic.apm:apm-agent-cached-lookup-key:1.36.0", + Name: "co.elastic.apm:apm-agent-cached-lookup-key", + Version: "1.36.0", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "co.elastic.apm", + Name: "apm-agent-cached-lookup-key", + Version: "1.36.0", + }, + BOMRef: "pkg:maven/co.elastic.apm/apm-agent-cached-lookup-key@1.36.0", + }, + }, + }, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "valid sbom spdx file without application component", + file: "testdata/sbom-without-app-component.spdx.json", + filePath: "layers/sbom/launch/buildpacksio_lifecycle/launcher/sbom.spdx.json", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.GoBinary, + FilePath: "layers/sbom/launch/buildpacksio_lifecycle/launcher/sbom.spdx.json", + Packages: types.Packages{ + { + ID: "github.com/buildpacks/lifecycle@v0.20.2", + Name: "github.com/buildpacks/lifecycle", + Version: "v0.20.2", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/buildpacks", + Name: "lifecycle", + Version: "v0.20.2", + }, + }, + Licenses: []string{ + "NOASSERTION", + }, + }, + }, + }, + { + Type: types.Jar, + FilePath: "layers/sbom/launch/buildpacksio_lifecycle/launcher/sbom.spdx.json", + Packages: types.Packages{ + { + ID: "co.elastic.apm:apm-agent:1.36.0", + Name: "co.elastic.apm:apm-agent", + Version: "1.36.0", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "co.elastic.apm", + Name: "apm-agent", + Version: "1.36.0", + }, + }, + }, + }, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "valid postgresql spdx file", + file: "testdata/postgresql.spdx.json", + filePath: "opt/bitnami/postgresql/.spdx-postgresql.spdx", + want: &analyzer.AnalysisResult{ + Applications: []types.Application{ + { + Type: types.Bitnami, + FilePath: "opt/bitnami/postgresql", + Packages: types.Packages{ + { + ID: "gdal@3.7.1", + Name: "gdal", + Version: "3.7.1", + Licenses: []string{"MIT"}, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeBitnami, + Name: "gdal", + Version: "3.7.1", + }, + }, + }, + { + ID: "geos@3.8.3", + Name: "geos", + Version: "3.8.3", + Licenses: []string{"LGPL-2.1-only"}, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeBitnami, + Name: "geos", + Version: "3.8.3", + }, + }, + }, + { + ID: "postgresql@15.3.0", + Name: "postgresql", + Version: "15.3.0", + Licenses: []string{"PostgreSQL"}, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeBitnami, + Name: "postgresql", + Version: "15.3.0", + }, + }, + DependsOn: []string{ + "geos@3.8.3", + "proj@6.3.2", + "gdal@3.7.1", + }, + }, + { + ID: "proj@6.3.2", + Name: "proj", + Version: "6.3.2", + Licenses: []string{"MIT"}, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeBitnami, + Name: "proj", + Version: "6.3.2", + }, + }, + }, + }, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "valid ca-certificates spdx file", + file: "testdata/ca-certificates.spdx.json", + filePath: "opt/bitnami/ca-certificates/.spdx-ca-certificates.spdx", + want: &analyzer.AnalysisResult{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "opt/bitnami/ca-certificates/.spdx-ca-certificates.spdx", + Packages: types.Packages{ + { + ID: "ca-certificates@20230311", + Name: "ca-certificates", + Version: "20230311", + Arch: "all", + SrcName: "ca-certificates", + SrcVersion: "20230311", + Licenses: []string{"GPL-2.0-or-later AND GPL-2.0-only AND MPL-2.0"}, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "ca-certificates", + Version: "20230311", + Qualifiers: packageurl.Qualifiers{ + {Key: "arch", Value: "all"}, + {Key: "distro", Value: "debian-12.9"}, + }, + }, + }, + }, + }, + }, + }, + }, + wantErr: require.NoError, + }, + { + name: "invalid spdx file", + file: "testdata/invalid_spdx.json", + filePath: "opt/bitnami/elasticsearch/.spdx-elasticsearch.spdx", + want: nil, + wantErr: require.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.file) + require.NoError(t, err) + defer f.Close() + + a := sbomAnalyzer{} + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.filePath, + Content: f, + }) + tt.wantErr(t, err) + + if got != nil { + got.Sort() + } + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_packagingAnalyzer_Required(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "cdx", + filePath: "/test/result.cdx", + want: true, + }, + { + name: "spdx", + filePath: "/test/result.spdx", + want: true, + }, + { + name: "cdx.json", + filePath: "/test/result.cdx.json", + want: true, + }, + { + name: "spdx.json", + filePath: "/test/result.spdx.json", + want: true, + }, + { + name: "json", + filePath: "/test/result.json", + want: false, + }, + { + name: "pep770 cdx.json in dist-info/sboms", + filePath: "python3.8/site-packages/xgboost-3.1.2.dist-info/sboms/auditwheel.cdx.json", + want: false, + }, + { + name: "pep770 spdx.json in dist-info/sboms", + filePath: "python3.8/site-packages/xgboost-3.1.2.dist-info/sboms/auditwheel.spdx.json", + want: false, + }, + { + name: "pep770 cdx in dist-info/sboms", + filePath: "python3.8/site-packages/xgboost-3.1.2.dist-info/sboms/auditwheel.cdx", + want: false, + }, + { + name: "pep770 spdx in dist-info/sboms", + filePath: "python3.8/site-packages/xgboost-3.1.2.dist-info/sboms/auditwheel.spdx", + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := sbomAnalyzer{} + got := a.Required(tt.filePath, nil) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/sbom/testdata/ca-certificates.spdx.json b/pkg/fanal/analyzer/sbom/testdata/ca-certificates.spdx.json new file mode 100644 index 000000000000..1e5f45ae5976 --- /dev/null +++ b/pkg/fanal/analyzer/sbom/testdata/ca-certificates.spdx.json @@ -0,0 +1,55 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "host", + "documentNamespace": "http://trivy.dev/filesystem/host-1022538e-83fe-4aa7-8718-a78839114c83", + "creationInfo": { + "creators": [ + "Organization: aquasecurity", + "Tool: trivy-0.58.1" + ], + "created": "2025-01-30T11:30:05Z" + }, + "packages": [ + { + "name": "ca-certificates", + "SPDXID": "SPDXRef-Package-c1d4029824045f75", + "versionInfo": "20230311", + "supplier": "Organization: Julien Cristau ", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "sourceInfo": "built package from: ca-certificates 20230311", + "licenseConcluded": "GPL-2.0-or-later AND GPL-2.0-only AND MPL-2.0", + "licenseDeclared": "GPL-2.0-or-later AND GPL-2.0-only AND MPL-2.0", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:deb/debian/ca-certificates@20230311?arch=all&distro=debian-12.9" + } + ], + "primaryPackagePurpose": "LIBRARY" + }, + { + "name": "debian", + "SPDXID": "SPDXRef-OperatingSystem-23c1e7dfb45d904f", + "versionInfo": "12.9", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "primaryPackagePurpose": "OPERATING-SYSTEM" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-OperatingSystem-23c1e7dfb45d904f", + "relatedSpdxElement": "SPDXRef-Package-c1d4029824045f75", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-OperatingSystem-23c1e7dfb45d904f", + "relationshipType": "DESCRIBES" + } + ] +} diff --git a/pkg/fanal/analyzer/sbom/testdata/cdx.json b/pkg/fanal/analyzer/sbom/testdata/cdx.json new file mode 100644 index 000000000000..a006bf7053e2 --- /dev/null +++ b/pkg/fanal/analyzer/sbom/testdata/cdx.json @@ -0,0 +1,61 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:73f26314-e86a-4f5a-befc-f853a15b64e7", + "version": 1, + "metadata": { + "timestamp": "2023-06-01T13:10:23+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.41.0-80-g1c03982fe" + } + ] + }, + "component": { + "bom-ref": "pkg:oci/elasticsearch@sha256:d4b68b602eb3d92ea3256886761752ae1159dc01fd391f4c4a87ebf6ba9d3895?repository_url=index.docker.io%2Fbitnami%2Felasticsearch\u0026arch=arm64", + "type": "container", + "name": "bitnami/elasticsearch:8.7.1-debian-11-r7", + "purl": "pkg:oci/elasticsearch@sha256:d4b68b602eb3d92ea3256886761752ae1159dc01fd391f4c4a87ebf6ba9d3895?repository_url=index.docker.io%2Fbitnami%2Felasticsearch\u0026arch=arm64" + } + }, + "components": [ + { + "bom-ref": "pkg:maven/co.elastic.apm/apm-agent@1.36.0", + "type": "library", + "name": "co.elastic.apm:apm-agent", + "version": "1.36.0", + "purl": "pkg:maven/co.elastic.apm/apm-agent@1.36.0", + "properties": [ + { + "name": "aquasecurity:trivy:PkgType", + "value": "jar" + }, + { + "name": "aquasecurity:trivy:FilePath", + "value": "modules/apm/elastic-apm-agent-1.36.0.jar" + } + ] + }, + { + "bom-ref": "pkg:maven/co.elastic.apm/apm-agent-cached-lookup-key@1.36.0", + "type": "library", + "name": "co.elastic.apm:apm-agent-cached-lookup-key", + "version": "1.36.0", + "purl": "pkg:maven/co.elastic.apm/apm-agent-cached-lookup-key@1.36.0", + "properties": [ + { + "name": "aquasecurity:trivy:PkgType", + "value": "jar" + }, + { + "name": "aquasecurity:trivy:FilePath", + "value": "modules/apm/elastic-apm-agent-1.36.0.jar" + } + ] + } + ] +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/sbom/testdata/elasticsearch.spdx.json b/pkg/fanal/analyzer/sbom/testdata/elasticsearch.spdx.json new file mode 100644 index 000000000000..3dc6df904d07 --- /dev/null +++ b/pkg/fanal/analyzer/sbom/testdata/elasticsearch.spdx.json @@ -0,0 +1,150 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.3", + "creationInfo": { + "created": "2023-08-18T20:09:40.708Z", + "creators": [ + "Organization: VMware, Inc." + ] + }, + "name": "SPDX document for Elasticsearch 8.9.1", + "dataLicense": "CC0-1.0", + "documentDescribes": [ + "SPDXRef-elasticsearch" + ], + "documentNamespace": "elasticsearch-8.9.1", + "packages": [ + { + "SPDXID": "SPDXRef-elasticsearch", + "name": "Elasticsearch", + "versionInfo": "8.9.1", + "downloadLocation": "https://github.com/elastic/elasticsearch/archive/v8.9.1.tar.gz", + "licenseConcluded": "Elastic-2.0", + "licenseDeclared": "Elastic-2.0", + "filesAnalyzed": false, + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:elastic:elasticsearch:8.9.1:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:bitnami/elasticsearch@8.9.1?arch=arm64" + } + ], + "copyrightText": "NOASSERTION" + }, + { + "name": "jar", + "SPDXID": "SPDXRef-Application-150e605f5f17224d", + "downloadLocation": "NONE", + "sourceInfo": "Java", + "copyrightText": "NOASSERTION", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "filesAnalyzed": false + }, + { + "name": "co.elastic.apm:apm-agent", + "SPDXID": "SPDXRef-Package-f0db45781e6813a1", + "versionInfo": "1.36.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:maven/co.elastic.apm/apm-agent@1.36.0" + } + ], + "filesAnalyzed": false + }, + { + "name": "co.elastic.apm:apm-agent-cached-lookup-key", + "SPDXID": "SPDXRef-Package-efe22bf5916f985f", + "versionInfo": "1.36.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:maven/co.elastic.apm/apm-agent-cached-lookup-key@1.36.0" + } + ], + "filesAnalyzed": false + }, + { + "name": "co.elastic.apm:apm-agent-common", + "SPDXID": "SPDXRef-Package-33d86d2d11abe114", + "versionInfo": "1.36.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:maven/co.elastic.apm/apm-agent-common@1.36.0" + } + ], + "filesAnalyzed": false + }, + { + "name": "co.elastic.apm:apm-agent-core", + "SPDXID": "SPDXRef-Package-b905fcf69ca61281", + "versionInfo": "1.36.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:maven/co.elastic.apm/apm-agent-core@1.36.0" + } + ], + "filesAnalyzed": false + } + ], + "files": [], + "relationships": [ + { + "spdxElementId": "SPDXRef-elasticsearch", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-Application-150e605f5f17224d" + }, + { + "spdxElementId": "SPDXRef-Application-150e605f5f17224d", + "relatedSpdxElement": "SPDXRef-Package-f0db45781e6813a1", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Application-150e605f5f17224d", + "relatedSpdxElement": "SPDXRef-Package-efe22bf5916f985f", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Application-150e605f5f17224d", + "relatedSpdxElement": "SPDXRef-Package-33d86d2d11abe114", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Application-150e605f5f17224d", + "relatedSpdxElement": "SPDXRef-Package-b905fcf69ca61281", + "relationshipType": "CONTAINS" + } + ] +} diff --git a/pkg/fanal/analyzer/sbom/testdata/invalid_spdx.json b/pkg/fanal/analyzer/sbom/testdata/invalid_spdx.json new file mode 100644 index 000000000000..dc9255659ea7 --- /dev/null +++ b/pkg/fanal/analyzer/sbom/testdata/invalid_spdx.json @@ -0,0 +1,3 @@ +{ + "invalid": "data" +} \ No newline at end of file diff --git a/pkg/fanal/analyzer/sbom/testdata/postgresql.spdx.json b/pkg/fanal/analyzer/sbom/testdata/postgresql.spdx.json new file mode 100644 index 000000000000..3173e5d2d52f --- /dev/null +++ b/pkg/fanal/analyzer/sbom/testdata/postgresql.spdx.json @@ -0,0 +1,120 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.3", + "creationInfo": { + "created": "2023-07-13T19:24:23.609Z", + "creators": [ + "Organization: VMware, Inc." + ] + }, + "name": "SPDX document for PostgreSQL 15.3.0", + "dataLicense": "CC0-1.0", + "documentDescribes": [ + "SPDXRef-postgresql" + ], + "documentNamespace": "postgresql-15.3.0", + "packages": [ + { + "SPDXID": "SPDXRef-postgresql", + "name": "PostgreSQL", + "versionInfo": "15.3.0", + "downloadLocation": "https://ftp.postgresql.org/pub/source/v15.3/postgresql-15.3.tar.gz", + "licenseConcluded": "PostgreSQL", + "licenseDeclared": "PostgreSQL", + "filesAnalyzed": false, + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:postgresql:postgresql:15.3.0:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:bitnami/postgresql@15.3.0" + } + ] + }, + { + "SPDXID": "SPDXRef-geos", + "name": "GEOS", + "versionInfo": "3.8.3", + "downloadLocation": "https://github.com/libgeos/geos/archive/3.8.3.tar.gz", + "licenseConcluded": "LGPL-2.1-only", + "licenseDeclared": "LGPL-2.1-only", + "filesAnalyzed": false, + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:libgeos:geos:3.8.3:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:bitnami/geos@3.8.3" + } + ] + }, + { + "SPDXID": "SPDXRef-proj", + "name": "Proj", + "versionInfo": "6.3.2", + "downloadLocation": "https://github.com/OSGeo/PROJ/archive/6.3.2.tar.gz", + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "filesAnalyzed": false, + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:proj:proj:6.3.2:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:bitnami/proj@6.3.2" + } + ] + }, + { + "SPDXID": "SPDXRef-gdal", + "name": "GDAL", + "versionInfo": "3.7.1", + "downloadLocation": "https://github.com/OSGeo/gdal/releases/download/v3.7.1/gdal-3.7.1.tar.gz", + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "filesAnalyzed": false, + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:osgeo:gdal:3.7.1:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:bitnami/gdal@3.7.1" + } + ] + } + ], + "files": [], + "relationships": [ + { + "spdxElementId": "SPDXRef-postgresql", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-geos" + }, + { + "spdxElementId": "SPDXRef-postgresql", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-proj" + }, + { + "spdxElementId": "SPDXRef-postgresql", + "relationshipType": "CONTAINS", + "relatedSpdxElement": "SPDXRef-gdal" + } + ] +} diff --git a/pkg/fanal/analyzer/sbom/testdata/sbom-without-app-component.spdx.json b/pkg/fanal/analyzer/sbom/testdata/sbom-without-app-component.spdx.json new file mode 100644 index 000000000000..e1e162eeb825 --- /dev/null +++ b/pkg/fanal/analyzer/sbom/testdata/sbom-without-app-component.spdx.json @@ -0,0 +1,111 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "launcher", + "documentNamespace": "https://anchore.com/syft/file/launcher-268e7779-ba66-4422-a5b0-d4d83f7b5d8c", + "creationInfo": { + "licenseListVersion": "3.25", + "creators": [ + "Organization: Anchore, Inc", + "Tool: syft-1.13.0" + ], + "created": "2024-09-25T21:11:50Z" + }, + "packages": [ + { + "name": "co.elastic.apm:apm-agent", + "SPDXID": "SPDXRef-Package-f0db45781e6813a1", + "versionInfo": "1.36.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "PACKAGE_MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:maven/co.elastic.apm/apm-agent@1.36.0" + } + ], + "filesAnalyzed": false + }, + { + "name": "github.com/buildpacks/lifecycle", + "SPDXID": "SPDXRef-Package-go-module-github.com-buildpacks-lifecycle-89c3bd8d4c2e75b7", + "versionInfo": "v0.20.2", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "sourceInfo": "acquired package info from go module information: /launcher", + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "copyrightText": "NOASSERTION", + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:a:buildpacks:lifecycle:v0.20.2:*:*:*:*:*:*:*" + }, + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:golang/github.com/buildpacks/lifecycle@v0.20.2" + } + ] + }, + { + "name": "launcher", + "SPDXID": "SPDXRef-DocumentRoot-File-launcher", + "versionInfo": "sha256:716665ae98fb4b4675d5184f80884547597d47be1395d1049dc9e16035f32cc1", + "supplier": "NOASSERTION", + "downloadLocation": "NOASSERTION", + "filesAnalyzed": false, + "checksums": [ + { + "algorithm": "SHA256", + "checksumValue": "716665ae98fb4b4675d5184f80884547597d47be1395d1049dc9e16035f32cc1" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "primaryPackagePurpose": "FILE" + } + ], + "files": [ + { + "fileName": "/launcher", + "SPDXID": "SPDXRef-File-launcher-361242815a383bec", + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "0000000000000000000000000000000000000000" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseInfoInFiles": [ + "NOASSERTION" + ], + "copyrightText": "" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-Package-go-module-github.com-buildpacks-lifecycle-89c3bd8d4c2e75b7", + "relatedSpdxElement": "SPDXRef-File-launcher-361242815a383bec", + "relationshipType": "OTHER", + "comment": "evident-by: indicates the package's existence is evident by the given file" + }, + { + "spdxElementId": "SPDXRef-DocumentRoot-File-launcher", + "relatedSpdxElement": "SPDXRef-Package-go-module-github.com-buildpacks-lifecycle-89c3bd8d4c2e75b7", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-DocumentRoot-File-launcher", + "relationshipType": "DESCRIBES" + } + ] +} diff --git a/pkg/fanal/analyzer/secret/secret.go b/pkg/fanal/analyzer/secret/secret.go new file mode 100644 index 000000000000..8115c61a8072 --- /dev/null +++ b/pkg/fanal/analyzer/secret/secret.go @@ -0,0 +1,191 @@ +package secret + +import ( + "bytes" + "context" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/secret" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/utils" + "github.com/aquasecurity/trivy/pkg/log" +) + +// To make sure SecretAnalyzer implements analyzer.Initializer +var _ analyzer.Initializer = &SecretAnalyzer{} + +const version = 1 + +var ( + skipFiles = []string{ + "go.mod", + "go.sum", + "package-lock.json", + "yarn.lock", + "pnpm-lock.yaml", + "Pipfile.lock", + "Gemfile.lock", + } + skipDirs = []string{ + ".git", + "node_modules", + } + skipExts = []string{ + ".jpg", + ".png", + ".gif", + ".doc", + ".pdf", + ".bin", + ".svg", + ".socket", + ".deb", + ".rpm", + ".zip", + ".gz", + ".gzip", + ".tar", + } + + allowedBinaries = []string{ + ".pyc", + } +) + +func init() { + // The scanner will be initialized later via InitScanner() + analyzer.RegisterAnalyzer(NewSecretAnalyzer(secret.Scanner{}, "")) +} + +func allowedBinary(filename string) bool { + return slices.Contains(allowedBinaries, filepath.Ext(filename)) +} + +// SecretAnalyzer is an analyzer for secrets +type SecretAnalyzer struct { + scanner secret.Scanner + configPath string +} + +func NewSecretAnalyzer(s secret.Scanner, configPath string) *SecretAnalyzer { + return &SecretAnalyzer{ + scanner: s, + configPath: configPath, + } +} + +// Init initializes and sets a secret scanner +func (a *SecretAnalyzer) Init(opt analyzer.AnalyzerOptions) error { + if opt.SecretScannerOption.ConfigPath == a.configPath && !lo.IsEmpty(a.scanner) { + // This check is for tools importing Trivy and customize analyzers + // Never reach here in Trivy OSS + return nil + } + configPath := opt.SecretScannerOption.ConfigPath + c, err := secret.ParseConfig(configPath) + if err != nil { + return xerrors.Errorf("secret config error: %w", err) + } + a.scanner = secret.NewScanner(c) + a.configPath = configPath + return nil +} + +func (a *SecretAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + // Do not scan binaries + binary, err := utils.IsBinary(input.Content, input.Info.Size()) + if err != nil || (binary && !allowedBinary(input.FilePath)) { + return nil, nil + } + + if size := input.Info.Size(); size > 10485760 { // 10MB + log.WithPrefix("secret").Warn("The size of the scanned file is too large. It is recommended to use `--skip-files` for this file to avoid high memory consumption.", log.FilePath(input.FilePath), log.Int64("size (MB)", size/1048576)) + } + + filePath := input.FilePath + // Files extracted from the image have an empty input.Dir. + // Also, paths to these files do not have "/" prefix. + // We need to add a "/" prefix to properly filter paths from the config file. + if input.Dir == "" { // add leading `/` for files extracted from image + filePath = fmt.Sprintf("/%s", filePath) + } + + reader := input.Content + if binary { + content, err := utils.ExtractPrintableBytes(input.Content) + if err != nil { + return nil, xerrors.Errorf("binary read error %s: %w", input.FilePath, err) + } + reader = bytes.NewReader(content) + } + + result := a.scanner.Scan(secret.ScanArgs{ + FilePath: filePath, + Content: reader, + Binary: binary, + }) + + if len(result.Findings) == 0 { + return nil, nil + } + + return &analyzer.AnalysisResult{ + Secrets: []types.Secret{result}, + }, nil +} + +func (a *SecretAnalyzer) Required(filePath string, fi os.FileInfo) bool { + // Skip small files + if fi.Size() < 10 { + return false + } + + dir, fileName := filepath.Split(filePath) + dir = filepath.ToSlash(dir) + dirs := strings.Split(dir, "/") + + // Check if the directory should be skipped + for _, skipDir := range skipDirs { + if slices.Contains(dirs, skipDir) { + return false + } + } + + // Check if the file should be skipped + if slices.Contains(skipFiles, fileName) { + return false + } + + // Skip the config file for secret scanning + if filepath.Base(a.configPath) == filePath { + return false + } + + // Check if the file extension should be skipped + ext := filepath.Ext(fileName) + if slices.Contains(skipExts, ext) { + return false + } + + if a.scanner.AllowPath(filePath) { + return false + } + + return true +} + +func (a *SecretAnalyzer) Type() analyzer.Type { + return analyzer.TypeSecret +} + +func (a *SecretAnalyzer) Version() int { + return version +} diff --git a/pkg/fanal/analyzer/secret/secret_test.go b/pkg/fanal/analyzer/secret/secret_test.go new file mode 100644 index 000000000000..77daf9c7fd8f --- /dev/null +++ b/pkg/fanal/analyzer/secret/secret_test.go @@ -0,0 +1,260 @@ +package secret_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestSecretAnalyzer(t *testing.T) { + wantFinding1 := types.SecretFinding{ + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "generic secret line secret=\"*********\"", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "--- ignore block start ---", + IsCause: false, + Annotation: "", + Truncated: false, + Highlighted: "--- ignore block start ---", + FirstCause: false, + LastCause: false, + }, + { + Number: 2, + Content: "generic secret line secret=\"*********\"", + IsCause: true, + Annotation: "", + Truncated: false, + Highlighted: "generic secret line secret=\"*********\"", + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "--- ignore block stop ---", + IsCause: false, + Annotation: "", + Truncated: false, + Highlighted: "--- ignore block stop ---", + FirstCause: false, + LastCause: false, + }, + }, + }, + Offset: 55, + } + wantFinding2 := types.SecretFinding{ + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 4, + EndLine: 4, + Match: "secret=\"**********\"", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 2, + Content: "generic secret line secret=\"*********\"", + IsCause: false, + Highlighted: "generic secret line secret=\"*********\"", + }, + { + Number: 3, + Content: "--- ignore block stop ---", + IsCause: false, + Highlighted: "--- ignore block stop ---", + }, + { + Number: 4, + Content: "secret=\"**********\"", + IsCause: true, + Highlighted: "secret=\"**********\"", + FirstCause: true, + LastCause: true, + }, + { + Number: 5, + Content: "credentials: { user: \"username\" password: \"123456789\" }", + Highlighted: "credentials: { user: \"username\" password: \"123456789\" }", + }, + }, + }, + Offset: 100, + } + wantFindingGH_PAT := types.SecretFinding{ + RuleID: "github-fine-grained-pat", + Category: "GitHub", + Title: "GitHub Fine-grained personal access tokens", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: "Binary file \"/testdata/secret.cpython-310.pyc\" matches a rule \"GitHub Fine-grained personal access tokens\"", + Offset: 2, + } + + tests := []struct { + name string + configPath string + filePath string + dir string + want *analyzer.AnalysisResult + }{ + { + name: "return results", + configPath: "testdata/config.yaml", + filePath: "testdata/secret.txt", + dir: ".", + want: &analyzer.AnalysisResult{ + Secrets: []types.Secret{ + { + FilePath: "testdata/secret.txt", + Findings: []types.SecretFinding{ + wantFinding1, + wantFinding2, + }, + }, + }, + }, + }, + { + name: "image scan return result", + configPath: "testdata/image-config.yaml", + filePath: "testdata/secret.txt", + want: &analyzer.AnalysisResult{ + Secrets: []types.Secret{ + { + FilePath: "/testdata/secret.txt", + Findings: []types.SecretFinding{ + wantFinding1, + wantFinding2, + }, + }, + }, + }, + }, + { + name: "image scan return nil", + configPath: "testdata/image-config.yaml", + filePath: "testdata/secret.doc", + want: nil, + }, + { + name: "return nil when no results", + configPath: "", + filePath: "testdata/secret.txt", + want: nil, + }, + { + name: "skip binary file", + configPath: "", + filePath: "testdata/binaryfile", + want: nil, + }, + { + name: "python binary file", + configPath: "testdata/skip-tests-config.yaml", + filePath: "testdata/secret.cpython-310.pyc", + want: &analyzer.AnalysisResult{ + Secrets: []types.Secret{ + { + FilePath: "/testdata/secret.cpython-310.pyc", + Findings: []types.SecretFinding{ + wantFindingGH_PAT, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := secret.SecretAnalyzer{} + err := a.Init(analyzer.AnalyzerOptions{ + SecretScannerOption: analyzer.SecretScannerOption{ConfigPath: tt.configPath}, + }) + require.NoError(t, err) + content, err := os.Open(tt.filePath) + require.NoError(t, err) + fi, err := content.Stat() + require.NoError(t, err) + + got, err := a.Analyze(t.Context(), analyzer.AnalysisInput{ + FilePath: tt.filePath, + Dir: tt.dir, + Content: content, + Info: fi, + }) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSecretRequire(t *testing.T) { + tests := []struct { + name string + filePath string + want bool + }{ + { + name: "pass regular file", + filePath: "testdata/secret.txt", + want: true, + }, + { + name: "skip small file", + filePath: "testdata/emptyfile", + want: false, + }, + { + name: "skip folder", + filePath: "testdata/node_modules/secret.txt", + want: false, + }, + { + name: "skip file", + filePath: "testdata/package-lock.json", + want: false, + }, + { + name: "skip extension", + filePath: "testdata/secret.doc", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a := secret.SecretAnalyzer{} + err := a.Init(analyzer.AnalyzerOptions{ + SecretScannerOption: analyzer.SecretScannerOption{ + ConfigPath: "testdata/skip-tests-config.yaml", + }, + }) + require.NoError(t, err) + + fi, err := os.Stat(tt.filePath) + require.NoError(t, err) + + got := a.Required(tt.filePath, fi) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/analyzer/secret/testdata/binaryfile b/pkg/fanal/analyzer/secret/testdata/binaryfile new file mode 100644 index 000000000000..df93f5f3f724 Binary files /dev/null and b/pkg/fanal/analyzer/secret/testdata/binaryfile differ diff --git a/pkg/fanal/analyzer/secret/testdata/config.yaml b/pkg/fanal/analyzer/secret/testdata/config.yaml new file mode 100644 index 000000000000..d656427e6b0c --- /dev/null +++ b/pkg/fanal/analyzer/secret/testdata/config.yaml @@ -0,0 +1,10 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret + +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/analyzer/secret/testdata/emptyfile b/pkg/fanal/analyzer/secret/testdata/emptyfile new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/secret/testdata/image-config.yaml b/pkg/fanal/analyzer/secret/testdata/image-config.yaml new file mode 100644 index 000000000000..b5640c223ad0 --- /dev/null +++ b/pkg/fanal/analyzer/secret/testdata/image-config.yaml @@ -0,0 +1,10 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + path: /testdata/secret.txt + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/analyzer/secret/testdata/node_modules/secret.txt b/pkg/fanal/analyzer/secret/testdata/node_modules/secret.txt new file mode 100644 index 000000000000..f0bad73a2d29 --- /dev/null +++ b/pkg/fanal/analyzer/secret/testdata/node_modules/secret.txt @@ -0,0 +1,5 @@ +--- ignore block start --- +generic secret line secret="somevalue" +--- ignore block stop --- +secret="othervalue" +credentials: { user: "username" password: "123456789" } \ No newline at end of file diff --git a/pkg/fanal/analyzer/secret/testdata/package-lock.json b/pkg/fanal/analyzer/secret/testdata/package-lock.json new file mode 100644 index 000000000000..6480ec74d711 --- /dev/null +++ b/pkg/fanal/analyzer/secret/testdata/package-lock.json @@ -0,0 +1 @@ +{"name": "just a field to check file skip"} \ No newline at end of file diff --git a/pkg/fanal/analyzer/secret/testdata/secret.cpython-310.pyc b/pkg/fanal/analyzer/secret/testdata/secret.cpython-310.pyc new file mode 100755 index 000000000000..2bb659e7eb21 Binary files /dev/null and b/pkg/fanal/analyzer/secret/testdata/secret.cpython-310.pyc differ diff --git a/pkg/fanal/analyzer/secret/testdata/secret.doc b/pkg/fanal/analyzer/secret/testdata/secret.doc new file mode 100644 index 000000000000..a52b5e7228ca --- /dev/null +++ b/pkg/fanal/analyzer/secret/testdata/secret.doc @@ -0,0 +1 @@ +mock doc file \ No newline at end of file diff --git a/pkg/fanal/analyzer/secret/testdata/secret.txt b/pkg/fanal/analyzer/secret/testdata/secret.txt new file mode 100644 index 000000000000..f0bad73a2d29 --- /dev/null +++ b/pkg/fanal/analyzer/secret/testdata/secret.txt @@ -0,0 +1,5 @@ +--- ignore block start --- +generic secret line secret="somevalue" +--- ignore block stop --- +secret="othervalue" +credentials: { user: "username" password: "123456789" } \ No newline at end of file diff --git a/pkg/fanal/analyzer/secret/testdata/skip-tests-config.yaml b/pkg/fanal/analyzer/secret/testdata/skip-tests-config.yaml new file mode 100644 index 000000000000..48cb99db27f0 --- /dev/null +++ b/pkg/fanal/analyzer/secret/testdata/skip-tests-config.yaml @@ -0,0 +1,2 @@ +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/analyzer/testdata/app/Gemfile.lock b/pkg/fanal/analyzer/testdata/app/Gemfile.lock new file mode 100644 index 000000000000..f4515a6b4cc5 --- /dev/null +++ b/pkg/fanal/analyzer/testdata/app/Gemfile.lock @@ -0,0 +1,15 @@ +GEM + remote: https://rubygems.org/ + specs: + actioncable (5.2.3) + actionpack (= 5.2.3) + actionpack (5.2.3) + +PLATFORMS + ruby + +DEPENDENCIES + actioncable (~> 5.0) + +BUNDLED WITH + 1.17.2 \ No newline at end of file diff --git a/pkg/fanal/analyzer/testdata/error b/pkg/fanal/analyzer/testdata/error new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/testdata/etc/alpine-release b/pkg/fanal/analyzer/testdata/etc/alpine-release new file mode 100644 index 000000000000..375f5cabfe6c --- /dev/null +++ b/pkg/fanal/analyzer/testdata/etc/alpine-release @@ -0,0 +1 @@ +3.11.6 diff --git a/pkg/fanal/analyzer/testdata/etc/hostname b/pkg/fanal/analyzer/testdata/etc/hostname new file mode 100644 index 000000000000..c70dc2dfaf07 --- /dev/null +++ b/pkg/fanal/analyzer/testdata/etc/hostname @@ -0,0 +1 @@ +host diff --git a/pkg/fanal/analyzer/testdata/lib/apk/db/installed b/pkg/fanal/analyzer/testdata/lib/apk/db/installed new file mode 100644 index 000000000000..dc23126b6a1b --- /dev/null +++ b/pkg/fanal/analyzer/testdata/lib/apk/db/installed @@ -0,0 +1,21 @@ +C:Q1yyMWoYnr7lKCxKm9mHlMwkd6dMY= +P:musl +V:1.1.24-r2 +A:x86_64 +S:377123 +I:614400 +T:the musl c library (libc) implementation +U:https://musl.libc.org/ +L:MIT +o:musl +m:Timo Teräs +t:1584790550 +c:4024cc3b29ad4c65544ad068b8f59172b5494306 +p:so:libc.musl-x86_64.so.1=1 +F:lib +R:libc.musl-x86_64.so.1 +a:0:0:777 +Z:Q17yJ3JFNypA4mxhJJr0ou6CzsJVI= +R:ld-musl-x86_64.so.1 +a:0:0:755 +Z:Q19mQZaYKY6yTQWQm0hkvsrh39O7Y= diff --git a/pkg/fanal/analyzer/testdata/no-permission b/pkg/fanal/analyzer/testdata/no-permission new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/analyzer/testdata/post-apps/jar/invalid.jar b/pkg/fanal/analyzer/testdata/post-apps/jar/invalid.jar new file mode 100644 index 000000000000..d800886d9c86 --- /dev/null +++ b/pkg/fanal/analyzer/testdata/post-apps/jar/invalid.jar @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/pkg/fanal/analyzer/testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar b/pkg/fanal/analyzer/testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar new file mode 100644 index 000000000000..b56a46744a66 Binary files /dev/null and b/pkg/fanal/analyzer/testdata/post-apps/jar/jackson-annotations-2.15.0-rc2.jar differ diff --git a/pkg/fanal/analyzer/testdata/post-apps/poetry/happy/poetry-pattern.lock b/pkg/fanal/analyzer/testdata/post-apps/poetry/happy/poetry-pattern.lock new file mode 100644 index 000000000000..221c62aef3e4 --- /dev/null +++ b/pkg/fanal/analyzer/testdata/post-apps/poetry/happy/poetry-pattern.lock @@ -0,0 +1,18 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = true +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "0ee6cb4bc2d84091d5dcb0a0110a65f244987ed427933b2f49949195e3ef69c7" diff --git a/pkg/fanal/analyzer/testdata/post-apps/poetry/happy/poetry.lock b/pkg/fanal/analyzer/testdata/post-apps/poetry/happy/poetry.lock new file mode 100644 index 000000000000..221c62aef3e4 --- /dev/null +++ b/pkg/fanal/analyzer/testdata/post-apps/poetry/happy/poetry.lock @@ -0,0 +1,18 @@ +# This file is automatically @generated by Poetry and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2022.12.7" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = true +python-versions = ">=3.6" +files = [ + {file = "certifi-2022.12.7-py3-none-any.whl", hash = "sha256:4ad3232f5e926d6718ec31cfc1fcadfde020920e278684144551c91769c7bc18"}, + {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "0ee6cb4bc2d84091d5dcb0a0110a65f244987ed427933b2f49949195e3ef69c7" diff --git a/pkg/fanal/analyzer/testdata/post-apps/poetry/sad/poetry.lock b/pkg/fanal/analyzer/testdata/post-apps/poetry/sad/poetry.lock new file mode 100644 index 000000000000..8e2f0bef135b --- /dev/null +++ b/pkg/fanal/analyzer/testdata/post-apps/poetry/sad/poetry.lock @@ -0,0 +1 @@ +[ \ No newline at end of file diff --git a/pkg/fanal/applier/applier.go b/pkg/fanal/applier/applier.go new file mode 100644 index 000000000000..6031e03f536e --- /dev/null +++ b/pkg/fanal/applier/applier.go @@ -0,0 +1,60 @@ +package applier + +import ( + "context" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +// Applier defines operation to scan image layers +type Applier interface { + ApplyLayers(ctx context.Context, artifactID string, blobIDs []string) (detail ftypes.ArtifactDetail, err error) +} + +type applier struct { + cache cache.LocalArtifactCache +} + +func NewApplier(c cache.LocalArtifactCache) Applier { + return &applier{cache: c} +} + +func (a *applier) ApplyLayers(ctx context.Context, imageID string, layerKeys []string) (ftypes.ArtifactDetail, error) { + var layers []ftypes.BlobInfo + var layerInfoList ftypes.Layers + for _, key := range layerKeys { + blob, _ := a.cache.GetBlob(ctx, key) // nolint + if blob.SchemaVersion == 0 { + return ftypes.ArtifactDetail{}, xerrors.Errorf("layer cache missing: %s", key) + } + if l := blob.Layer(); !lo.IsEmpty(l) { + layerInfoList = append(layerInfoList, l) + } + layers = append(layers, blob) + } + + mergedLayer := ApplyLayers(layers) + + imageInfo, _ := a.cache.GetArtifact(ctx, imageID) // nolint + mergedLayer.ImageConfig = ftypes.ImageConfigDetail{ + Packages: imageInfo.HistoryPackages, + Misconfiguration: imageInfo.Misconfiguration, + Secret: imageInfo.Secret, + } + + // Fill layers info + mergedLayer.Layers = layerInfoList + + if !mergedLayer.OS.Detected() { + return mergedLayer, analyzer.ErrUnknownOS // send back package and apps info regardless + } else if mergedLayer.Packages == nil { + return mergedLayer, analyzer.ErrNoPkgsDetected // send back package and apps info regardless + } + + return mergedLayer, nil +} diff --git a/pkg/fanal/applier/applier_test.go b/pkg/fanal/applier/applier_test.go new file mode 100644 index 000000000000..ceba100d76be --- /dev/null +++ b/pkg/fanal/applier/applier_test.go @@ -0,0 +1,1034 @@ +package applier_test + +import ( + "sort" + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/cachetest" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type dummyData struct { + data string +} + +func TestApplier_ApplyLayers(t *testing.T) { + type args struct { + imageID string + layerIDs []string + } + tests := []struct { + name string + args args + setUpCache func(t *testing.T) cache.Cache + want types.ArtifactDetail + wantErr string + }{ + { + name: "happy path", + args: args{ + imageID: "sha256:4791503518dff090d6a82f7a5c1fd71c41146920e2562fb64308e17ab6834b7e", + layerIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + + require.NoError(t, c.PutArtifact(t.Context(), "sha256:4791503518dff090d6a82f7a5c1fd71c41146920e2562fb64308e17ab6834b7e", types.ArtifactInfo{ + SchemaVersion: 1, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", types.BlobInfo{ + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + OS: types.OS{ + Family: "debian", + Name: "9.9", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/tzdata", + Packages: types.Packages{ + { + Name: "tzdata", + Version: "2019a-0+deb9u1", + SrcName: "tzdata", + SrcVersion: "2019a-0+deb9u1", + }, + }, + }, + }, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", types.BlobInfo{ + SchemaVersion: 1, + Size: 2000, + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/libc6", + Packages: types.Packages{ + { + Name: "libc6", + Version: "2.24-11+deb9u4", + SrcName: "glibc", + SrcVersion: "2.24-11+deb9u4", + }, + }, + }, + }, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", types.BlobInfo{ + SchemaVersion: 1, + Size: 3000, + Digest: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + Applications: []types.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: types.Packages{ + { + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + }, + { + Name: "symfony/process", + Version: "v4.2.7", + }, + }, + }, + }, + })) + + return c + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "debian", + Name: "9.9", + }, + Layers: types.Layers{ + { + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + { + Size: 2000, + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + { + Size: 3000, + Digest: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + Packages: types.Packages{ + { + Name: "libc6", + Version: "2.24-11+deb9u4", + SrcName: "glibc", + SrcVersion: "2.24-11+deb9u4", + Identifier: types.PkgIdentifier{ + UID: "1565c6a375877d3d", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "libc6", + Version: "2.24-11+deb9u4", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "debian-9.9", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + }, + { + Name: "tzdata", + Version: "2019a-0+deb9u1", + SrcName: "tzdata", + SrcVersion: "2019a-0+deb9u1", + Identifier: types.PkgIdentifier{ + UID: "15974c575bfa26a7", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "tzdata", + Version: "2019a-0+deb9u1", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "debian-9.9", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + }, + }, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: types.Packages{ + { + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + Layer: types.Layer{ + Digest: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + Identifier: types.PkgIdentifier{ + UID: "38462330435c69bc", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "guzzlehttp", + Name: "guzzle", + Version: "6.2.0", + }, + }, + }, + { + Name: "symfony/process", + Version: "v4.2.7", + Layer: types.Layer{ + Digest: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + Identifier: types.PkgIdentifier{ + UID: "ef7e3567678854cb", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "symfony", + Name: "process", + Version: "v4.2.7", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with history packages", + args: args{ + imageID: "sha256:3bb70bd5fb37e05b8ecaaace5d6a6b5ec7834037c07ecb5907355c23ab70352d", + layerIDs: []string{ + "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:3bb70bd5fb37e05b8ecaaace5d6a6b5ec7834037c07ecb5907355c23ab70352d", types.ArtifactInfo{ + SchemaVersion: 1, + HistoryPackages: types.Packages{ + { + Name: "musl", + Version: "1.1.23", + }, + { + Name: "busybox", + Version: "1.31", + }, + { + Name: "ncurses-libs", + Version: "6.1_p20190518-r0", + }, + { + Name: "ncurses-terminfo-base", + Version: "6.1_p20190518-r0", + }, + { + Name: "ncurses", + Version: "6.1_p20190518-r0", + }, + { + Name: "ncurses-terminfo", + Version: "6.1_p20190518-r0", + }, + { + Name: "bash", + Version: "5.0.0-r0", + }, + { + Name: "readline", + Version: "8.0.0-r0", + }, + }, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", types.BlobInfo{ + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + DiffID: "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + OS: types.OS{ + Family: "alpine", + Name: "3.10.4", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: types.Packages{ + { + Name: "musl", + Version: "1.1.22-r3", + }, + { + Name: "busybox", + Version: "1.30.1-r3", + }, + { + Name: "openssl", + Version: "1.1.1d-r2", + }, + { + Name: "libcrypto1.1", + Version: "1.1.1d-r2", + }, + { + Name: "libssl1.1", + Version: "1.1.1d-r2", + }, + }, + }, + }, + })) + + return c + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "alpine", + Name: "3.10.4", + }, + Layers: types.Layers{ + { + Size: 1000, + Digest: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + DiffID: "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + Packages: types.Packages{ + { + Name: "busybox", + Version: "1.30.1-r3", + Identifier: types.PkgIdentifier{ + UID: "3bfef897b9fcc058", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "busybox", + Version: "1.30.1-r3", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.10.4", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + DiffID: "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + { + Name: "libcrypto1.1", + Version: "1.1.1d-r2", + Identifier: types.PkgIdentifier{ + UID: "a4495e1af163f55a", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "libcrypto1.1", + Version: "1.1.1d-r2", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.10.4", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + DiffID: "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + { + Name: "libssl1.1", + Version: "1.1.1d-r2", + Identifier: types.PkgIdentifier{ + UID: "4c683a33e3b7899c", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "libssl1.1", + Version: "1.1.1d-r2", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.10.4", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + DiffID: "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + { + Name: "musl", + Version: "1.1.22-r3", + Identifier: types.PkgIdentifier{ + UID: "bb9bd4dfce8858bf", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.1.22-r3", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.10.4", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + DiffID: "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + { + Name: "openssl", + Version: "1.1.1d-r2", + Identifier: types.PkgIdentifier{ + UID: "3f6c865591e06595", + //PURL: "pkg:apk/alpine/openssl@1.1.1d-r2?distro=3.10.4", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "openssl", + Version: "1.1.1d-r2", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.10.4", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + DiffID: "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + }, + ImageConfig: types.ImageConfigDetail{ + Packages: types.Packages{ + { + Name: "musl", + Version: "1.1.23", + }, + { + Name: "busybox", + Version: "1.31", + }, + { + Name: "ncurses-libs", + Version: "6.1_p20190518-r0", + }, + { + Name: "ncurses-terminfo-base", + Version: "6.1_p20190518-r0", + }, + { + Name: "ncurses", + Version: "6.1_p20190518-r0", + }, + { + Name: "ncurses-terminfo", + Version: "6.1_p20190518-r0", + }, + { + Name: "bash", + Version: "5.0.0-r0", + }, + { + Name: "readline", + Version: "8.0.0-r0", + }, + }, + }, + }, + }, + { + name: "sad path GetBlob returns an error", + args: args{ + layerIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + setUpCache: func(_ *testing.T) cache.Cache { + return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{ + GetBlob: true, + }) + }, + wantErr: "layer cache missing", + }, + { + name: "sad path GetBlob returns empty layer info", + args: args{ + layerIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", types.BlobInfo{})) + return c + }, + wantErr: "layer cache missing", + }, + { + name: "happy path with some packages but unknown OS", + args: args{ + imageID: "sha256:4791503518dff090d6a82f7a5c1fd71c41146920e2562fb64308e17ab6834b7e", + layerIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:4791503518dff090d6a82f7a5c1fd71c41146920e2562fb64308e17ab6834b7e", types.ArtifactInfo{ + SchemaVersion: 1, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", types.BlobInfo{ + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/tzdata", + Packages: types.Packages{ + { + Name: "tzdata", + Version: "2019a-0+deb9u1", + SrcName: "tzdata", + SrcVersion: "2019a-0+deb9u1", + }, + }, + }, + }, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", types.BlobInfo{ + SchemaVersion: 1, + Size: 2000, + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/libc6", + Packages: types.Packages{ + { + Name: "libc6", + Version: "2.24-11+deb9u4", + SrcName: "glibc", + SrcVersion: "2.24-11+deb9u4", + }, + }, + }, + }, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", types.BlobInfo{ + SchemaVersion: 1, + Size: 3000, + Digest: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + Applications: []types.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: types.Packages{ + { + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + }, + { + Name: "symfony/process", + Version: "v4.2.7", + }, + }, + }, + }, + })) + + return c + }, + want: types.ArtifactDetail{ + Packages: types.Packages{ + { + Name: "libc6", + Version: "2.24-11+deb9u4", + SrcName: "glibc", + SrcVersion: "2.24-11+deb9u4", + Layer: types.Layer{ + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + Identifier: types.PkgIdentifier{ + UID: "1565c6a375877d3d", + }, + }, + { + Name: "tzdata", + Version: "2019a-0+deb9u1", + SrcName: "tzdata", + SrcVersion: "2019a-0+deb9u1", + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + Identifier: types.PkgIdentifier{ + UID: "15974c575bfa26a7", + }, + }, + }, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: types.Packages{ + { + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + Layer: types.Layer{ + Digest: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + Identifier: types.PkgIdentifier{ + UID: "38462330435c69bc", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "guzzlehttp", + Name: "guzzle", + Version: "6.2.0", + }, + }, + }, + { + Name: "symfony/process", + Version: "v4.2.7", + Layer: types.Layer{ + Digest: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + Identifier: types.PkgIdentifier{ + UID: "ef7e3567678854cb", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "symfony", + Name: "process", + Version: "v4.2.7", + }, + }, + }, + }, + }, + }, + Layers: types.Layers{ + { + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + { + Size: 2000, + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + { + Size: 3000, + Digest: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + }, + }, + wantErr: "unknown OS", + }, + { + name: "sad path no package detected", + args: args{ + imageID: "sha256:4791503518dff090d6a82f7a5c1fd71c41146920e2562fb64308e17ab6834b7e", + layerIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + }, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:4791503518dff090d6a82f7a5c1fd71c41146920e2562fb64308e17ab6834b7e", types.ArtifactInfo{ + SchemaVersion: 1, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", types.BlobInfo{ + SchemaVersion: 1, + OS: types.OS{ + Family: "debian", + Name: "9.9", + }, + })) + return c + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "debian", + Name: "9.9", + }, + }, + wantErr: "no packages detected", + }, + { + name: "happy path with custom resources", + args: args{ + imageID: "sha256:4791503518dff090d6a82f7a5c1fd71c41146920e2562fb64308e17ab6834b7e", + layerIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + }, + }, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:4791503518dff090d6a82f7a5c1fd71c41146920e2562fb64308e17ab6834b7e", types.ArtifactInfo{ + SchemaVersion: 1, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", types.BlobInfo{ + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/tzdata", + Packages: types.Packages{ + { + Name: "tzdata", + Version: "2019a-0+deb9u1", + SrcName: "tzdata", + SrcVersion: "2019a-0+deb9u1", + }, + }, + }, + }, + CustomResources: []types.CustomResource{ + { + Type: "type-A", + FilePath: "var/lib/dpkg/status.d/tzdata", + Data: dummyData{ + data: "Common Package type-A var/lib/dpkg/status.d/tzdata", + }, + }, + { + Type: "type-B", + FilePath: "var/lib/dpkg/status.d/tzdata", + Data: dummyData{ + data: "Common Package type-B, overidden in next layer", + }, + }, + }, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", types.BlobInfo{ + SchemaVersion: 1, + Size: 2000, + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + Applications: []types.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: types.Packages{ + { + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + }, + { + Name: "symfony/process", + Version: "v4.2.7", + }, + }, + }, + }, + CustomResources: []types.CustomResource{ + { + Type: "type-A", + FilePath: "php-app/composer.lock", + Data: dummyData{ + data: "Common Application type-A php-app/composer.lock", + }, + }, + { + Type: "type-B", + FilePath: "var/lib/dpkg/status.d/tzdata", + Data: dummyData{ + data: "Type B application which replaces earlier detected resource", + }, + }, + }, + })) + + return c + }, + want: types.ArtifactDetail{ + Packages: types.Packages{ + { + Name: "tzdata", + Version: "2019a-0+deb9u1", + SrcName: "tzdata", + SrcVersion: "2019a-0+deb9u1", + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + Identifier: types.PkgIdentifier{ + UID: "15974c575bfa26a7", + }, + }, + }, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: types.Packages{ + { + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + Layer: types.Layer{ + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + Identifier: types.PkgIdentifier{ + UID: "38462330435c69bc", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "guzzlehttp", + Name: "guzzle", + Version: "6.2.0", + }, + }, + }, + { + Name: "symfony/process", + Version: "v4.2.7", + Layer: types.Layer{ + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + Identifier: types.PkgIdentifier{ + UID: "ef7e3567678854cb", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "symfony", + Name: "process", + Version: "v4.2.7", + }, + }, + }, + }, + }, + }, + CustomResources: []types.CustomResource{ + { + Type: "type-A", + FilePath: "php-app/composer.lock", + Layer: types.Layer{ + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + Data: dummyData{ + data: "Common Application type-A php-app/composer.lock", + }, + }, + { + Type: "type-A", + FilePath: "var/lib/dpkg/status.d/tzdata", + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + Data: dummyData{ + data: "Common Package type-A var/lib/dpkg/status.d/tzdata", + }, + }, + { + Type: "type-B", + FilePath: "var/lib/dpkg/status.d/tzdata", + Layer: types.Layer{ + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + Data: dummyData{ + data: "Type B application which replaces earlier detected resource", + }, + }, + }, + Layers: types.Layers{ + { + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + { + Size: 2000, + Digest: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + }, + }, + wantErr: "unknown OS", + }, + { + name: "SUSE images - legacy OS name with backward compatibility", + args: args{ + imageID: "sha256:fb44d01953611ba18d43d88e158c25579d18eff42db671182245010620a283f3", + layerIDs: []string{ + "sha256:2615f175cf3da67c48c6542914744943ee5e9c253547b03e3cfe8aae605c3199", + }, + }, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:fb44d01953611ba18d43d88e158c25579d18eff42db671182245010620a283f3", types.ArtifactInfo{ + SchemaVersion: 1, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:2615f175cf3da67c48c6542914744943ee5e9c253547b03e3cfe8aae605c3199", types.BlobInfo{ + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:fb44d01953611ba18d43d88e158c25579d18eff42db671182245010620a283f3", + DiffID: "sha256:d555e1b0b42f21a1cf198e52bcb12fe66aa015348e4390d2d5acddd327d79073", + OS: types.OS{ + Family: "suse linux enterprise server", + Name: "15.4", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "usr/lib/sysimage/rpm/Packages.db", + Packages: types.Packages{ + { + Name: "curl", + Version: "7.79.1", + SrcName: "curl", + SrcVersion: "7.79.1", + }, + }, + }, + }, + })) + + return c + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "sles", + Name: "15.4", + }, + Packages: types.Packages{ + { + Name: "curl", + Version: "7.79.1", + SrcName: "curl", + SrcVersion: "7.79.1", + Identifier: types.PkgIdentifier{ + UID: "1e9b3d3a73785651", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "suse", + Name: "curl", + Version: "7.79.1", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "sles-15.4", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:fb44d01953611ba18d43d88e158c25579d18eff42db671182245010620a283f3", + DiffID: "sha256:d555e1b0b42f21a1cf198e52bcb12fe66aa015348e4390d2d5acddd327d79073", + }, + }, + }, + Layers: types.Layers{ + { + Size: 1000, + Digest: "sha256:fb44d01953611ba18d43d88e158c25579d18eff42db671182245010620a283f3", + DiffID: "sha256:d555e1b0b42f21a1cf198e52bcb12fe66aa015348e4390d2d5acddd327d79073", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cachetest.NewCache(t, tt.setUpCache) + + a := applier.NewApplier(c) + + got, err := a.ApplyLayers(t.Context(), tt.args.imageID, tt.args.layerIDs) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + + require.NoError(t, err, tt.name) + sort.Sort(got.Packages) + for _, app := range got.Applications { + sort.Sort(app.Packages) + } + + sort.Slice(got.CustomResources, func(i, j int) bool { + if got.CustomResources[i].FilePath == got.CustomResources[j].FilePath { + return got.CustomResources[i].Type < got.CustomResources[j].Type + } + return got.CustomResources[i].FilePath < got.CustomResources[j].FilePath + }) + + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/applier/docker.go b/pkg/fanal/applier/docker.go new file mode 100644 index 000000000000..1d29e7ceeefe --- /dev/null +++ b/pkg/fanal/applier/docker.go @@ -0,0 +1,386 @@ +package applier + +import ( + "cmp" + "fmt" + "strings" + "time" + + "github.com/knqyf263/nested" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +type Config struct { + ContainerConfig containerConfig `json:"container_config"` + History []History +} + +type containerConfig struct { + Env []string +} + +type History struct { + Created time.Time + CreatedBy string `json:"created_by"` +} + +func findPackage(e ftypes.Package, s []ftypes.Package) *ftypes.Package { + for i := range s { + a := &s[i] // do not range by value to avoid heap allocations + if a.Name == e.Name && a.Version == e.Version && a.Release == e.Release { + return a + } + } + return nil +} + +func lookupOriginLayerForPkg(pkg ftypes.Package, layers []ftypes.BlobInfo) (string, string, []string, *ftypes.BuildInfo) { + for i, layer := range layers { + for _, info := range layer.PackageInfos { + if p := findPackage(pkg, info.Packages); p != nil { + return layer.Digest, layer.DiffID, p.InstalledFiles, lookupBuildInfo(i, layers) + } + } + } + return "", "", nil, nil +} + +// lookupBuildInfo looks up Red Hat content sets from all layers +func lookupBuildInfo(index int, layers []ftypes.BlobInfo) *ftypes.BuildInfo { + if layers[index].BuildInfo != nil { + return layers[index].BuildInfo + } + + // Base layer (layers[0]) is missing content sets + // - it needs to be shared from layers[1] + if index == 0 { + if len(layers) > 1 { + return layers[1].BuildInfo + } + return nil + } + + // Customer's layers build on top of Red Hat image are also missing content sets + // - it needs to be shared from the last Red Hat's layers which contains content sets + for i := index - 1; i >= 0; i-- { + if layers[i].BuildInfo != nil { + return layers[i].BuildInfo + } + } + return nil +} + +func lookupOriginLayerForLib(filePath string, lib ftypes.Package, layers []ftypes.BlobInfo) (string, string) { + for _, layer := range layers { + for _, layerApp := range layer.Applications { + if filePath != layerApp.FilePath { + continue + } + if findPackage(lib, layerApp.Packages) != nil { + return layer.Digest, layer.DiffID + } + } + } + return "", "" +} + +// ApplyLayers returns the merged layer +// nolint: gocyclo +func ApplyLayers(layers []ftypes.BlobInfo) ftypes.ArtifactDetail { + sep := "/" + nestedMap := nested.Nested{} + secretsMap := make(map[string]ftypes.Secret) + var mergedLayer ftypes.ArtifactDetail + + for _, layer := range layers { + for _, opqDir := range layer.OpaqueDirs { + opqDir = strings.TrimSuffix(opqDir, sep) // this is necessary so that an empty element is not contribute into the array of the DeleteByString function + _ = nestedMap.DeleteByString(opqDir, sep) // nolint + } + for _, whFile := range layer.WhiteoutFiles { + _ = nestedMap.DeleteByString(whFile, sep) // nolint + } + + mergedLayer.OS.Merge(layer.OS) + + if layer.Repository != nil { + mergedLayer.Repository = layer.Repository + } + + // Apply OS packages + for _, pkgInfo := range layer.PackageInfos { + key := fmt.Sprintf("%s/type:ospkg", pkgInfo.FilePath) + nestedMap.SetByString(key, sep, pkgInfo) + } + + // Apply language-specific packages + for _, app := range layer.Applications { + key := fmt.Sprintf("%s/type:%s", app.FilePath, app.Type) + nestedMap.SetByString(key, sep, app) + } + + // Apply misconfigurations + for _, config := range layer.Misconfigurations { + config.Layer = ftypes.Layer{ + Digest: layer.Digest, + DiffID: layer.DiffID, + } + key := fmt.Sprintf("%s/type:config", config.FilePath) + nestedMap.SetByString(key, sep, config) + } + + // Apply secrets + for _, secret := range layer.Secrets { + l := ftypes.Layer{ + Digest: layer.Digest, + DiffID: layer.DiffID, + CreatedBy: layer.CreatedBy, + } + secretsMap = mergeSecrets(secretsMap, secret, l) + } + + // Apply license files + for _, license := range layer.Licenses { + license.Layer = ftypes.Layer{ + Digest: layer.Digest, + DiffID: layer.DiffID, + } + key := fmt.Sprintf("%s/type:license,%s", license.FilePath, license.Type) + nestedMap.SetByString(key, sep, license) + } + + // Apply custom resources + for _, customResource := range layer.CustomResources { + key := fmt.Sprintf("%s/custom:%s", customResource.FilePath, customResource.Type) + customResource.Layer = ftypes.Layer{ + Digest: layer.Digest, + DiffID: layer.DiffID, + } + nestedMap.SetByString(key, sep, customResource) + } + } + + // nolint + _ = nestedMap.Walk(func(keys []string, value any) error { + switch v := value.(type) { + case ftypes.PackageInfo: + mergedLayer.Packages = append(mergedLayer.Packages, v.Packages...) + case ftypes.Application: + mergedLayer.Applications = append(mergedLayer.Applications, v) + case ftypes.Misconfiguration: + mergedLayer.Misconfigurations = append(mergedLayer.Misconfigurations, v) + case ftypes.LicenseFile: + mergedLayer.Licenses = append(mergedLayer.Licenses, v) + case ftypes.CustomResource: + mergedLayer.CustomResources = append(mergedLayer.CustomResources, v) + } + return nil + }) + + for _, s := range secretsMap { + mergedLayer.Secrets = append(mergedLayer.Secrets, s) + } + + // Extract dpkg licenses + // The license information is not stored in the dpkg database and in a separate file, + // so we have to merge the license information into the package. + dpkgLicenses := make(map[string][]string) + mergedLayer.Licenses = lo.Reject(mergedLayer.Licenses, func(license ftypes.LicenseFile, _ int) bool { + if license.Type != ftypes.LicenseTypeDpkg { + return false + } + // e.g. + // "adduser" => {"GPL-2"} + // "openssl" => {"MIT", "BSD"} + dpkgLicenses[license.PkgName] = xslices.Map(license.Findings, func(finding ftypes.LicenseFinding) string { + return finding.Name + }) + // Remove this license in the merged result as it is merged into the package information. + return true + }) + if len(mergedLayer.Licenses) == 0 { + mergedLayer.Licenses = nil + } + + for i, pkg := range mergedLayer.Packages { + // Skip lookup for SBOM + if lo.IsEmpty(pkg.Layer) { + originLayerDigest, originLayerDiffID, installedFiles, buildInfo := lookupOriginLayerForPkg(pkg, layers) + mergedLayer.Packages[i].Layer = ftypes.Layer{ + Digest: originLayerDigest, + DiffID: originLayerDiffID, + } + mergedLayer.Packages[i].BuildInfo = buildInfo + // Debian/Ubuntu has the installed files only in the first layer where the package is installed. + mergedLayer.Packages[i].InstalledFiles = installedFiles + } + + if mergedLayer.OS.Family != "" && pkg.Identifier.PURL == nil { + mergedLayer.Packages[i].Identifier.PURL = newPURL(mergedLayer.OS.Family, types.Metadata{OS: &mergedLayer.OS}, pkg) + } + mergedLayer.Packages[i].Identifier.UID = dependency.UID("", pkg) + + // Only debian packages + if licenses, ok := dpkgLicenses[pkg.Name]; ok { + mergedLayer.Packages[i].Licenses = licenses + } + } + + // Filter OS packages with mismatched PURL namespace + mergedLayer.Packages = filterMismatchedOSPkgs(mergedLayer.OS.Family, mergedLayer.Packages) + + // De-duplicate same debian packages from different dirs + // cf. https://github.com/aquasecurity/trivy/issues/8297 + mergedLayer.Packages = xslices.ZeroToNil(lo.UniqBy(mergedLayer.Packages, func(pkg ftypes.Package) string { + id := cmp.Or(pkg.ID, fmt.Sprintf("%s@%s", pkg.Name, utils.FormatVersion(pkg))) + // To avoid deduplicating packages with the same ID but from different locations (e.g. RPM archives), check the file path. + return fmt.Sprintf("%s/%s", id, pkg.FilePath) + })) + + for _, app := range mergedLayer.Applications { + for i, pkg := range app.Packages { + // Skip lookup for SBOM + if lo.IsEmpty(pkg.Layer) { + originLayerDigest, originLayerDiffID := lookupOriginLayerForLib(app.FilePath, pkg, layers) + app.Packages[i].Layer = ftypes.Layer{ + Digest: originLayerDigest, + DiffID: originLayerDiffID, + } + } + if pkg.Identifier.PURL == nil { + app.Packages[i].Identifier.PURL = newPURL(app.Type, types.Metadata{}, pkg) + } + app.Packages[i].Identifier.UID = dependency.UID(app.FilePath, pkg) + } + } + + // Aggregate python/ruby/node.js packages and JAR files + aggregate(&mergedLayer) + + mergedLayer.Sort() + + return mergedLayer +} + +func newPURL(pkgType ftypes.TargetType, metadata types.Metadata, pkg ftypes.Package) *packageurl.PackageURL { + // Possible cases when package doesn't have name/version (e.g. local package.json). + // For these cases we don't need to create PURL, because this PURL will be incorrect. + // TODO Dmitriy - move to `purl` package + if pkg.Name == "" { + return nil + } + + p, err := purl.New(pkgType, metadata, pkg) + if err != nil { + log.Error("Failed to create PackageURL", log.Err(err)) + return nil + } + return p.Unwrap() +} + +// aggregate merges all packages installed by pip/gem/npm/jar/conda into each application +func aggregate(detail *ftypes.ArtifactDetail) { + var apps []ftypes.Application + + aggregatedApps := make(map[ftypes.LangType]*ftypes.Application) + for _, t := range ftypes.AggregatingTypes { + aggregatedApps[t] = &ftypes.Application{Type: t} + } + + for _, app := range detail.Applications { + a, ok := aggregatedApps[app.Type] + if !ok { + apps = append(apps, app) + continue + } + a.Packages = append(a.Packages, app.Packages...) + } + + for _, app := range aggregatedApps { + if len(app.Packages) > 0 { + apps = append(apps, *app) + } + } + + // Overwrite Applications + detail.Applications = apps +} + +// We must save secrets from all layers even though they are removed in the uppler layer. +// If the secret was changed at the top level, we need to overwrite it. +func mergeSecrets(secretsMap map[string]ftypes.Secret, newSecret ftypes.Secret, layer ftypes.Layer) map[string]ftypes.Secret { + for i := range newSecret.Findings { // add layer to the Findings from the new secret + newSecret.Findings[i].Layer = layer + } + + secret, ok := secretsMap[newSecret.FilePath] + if !ok { + // Add the new finding if its file doesn't exist before + secretsMap[newSecret.FilePath] = newSecret + } else { + // If the new finding has the same `RuleID` as the finding in the previous layers - use the new finding + for _, previousFinding := range secret.Findings { // secrets from previous layers + if !secretFindingsContains(newSecret.Findings, previousFinding) { + newSecret.Findings = append(newSecret.Findings, previousFinding) + } + } + secretsMap[newSecret.FilePath] = newSecret + } + return secretsMap +} + +func secretFindingsContains(findings []ftypes.SecretFinding, finding ftypes.SecretFinding) bool { + for _, f := range findings { + if f.RuleID == finding.RuleID { + return true + } + } + return false +} + +// purlMatchesOS checks if a package's PURL namespace matches the detected OS family. +// Returns true if the package should be kept (matches OS or has no PURL/namespace). +// Returns false if the package should be filtered out (has PURL with mismatched namespace). +func purlMatchesOS(pkg ftypes.Package, osFamily ftypes.OSType) bool { + if pkg.Identifier.PURL == nil || osFamily == "" { + return true // Keep packages without PURL or when OS is not detected + } + if pkg.Identifier.PURL.Namespace == "" { + return true // Keep packages without namespace + } + return pkg.Identifier.PURL.Namespace == osFamily.PurlNamespace() +} + +// filterMismatchedOSPkgs removes OS packages whose PURL namespace doesn't match the detected OS. +// Packages with pre-existing PURLs are typically from SBOM files embedded in the image. +func filterMismatchedOSPkgs(osFamily ftypes.OSType, pkgs ftypes.Packages) ftypes.Packages { + if osFamily == "" { + return pkgs // No OS detected, keep all packages + } + + var filtered int + result := lo.Filter(pkgs, func(pkg ftypes.Package, _ int) bool { + if purlMatchesOS(pkg, osFamily) { + return true + } + filtered++ + return false + }) + + if filtered > 0 { + log.WithPrefix("applier").Warn("Some OS packages were skipped due to mismatched PURL namespace", + log.Int("pkg_count", filtered), + log.String("detected_os", string(osFamily))) + } + + return result +} diff --git a/pkg/fanal/applier/docker_test.go b/pkg/fanal/applier/docker_test.go new file mode 100644 index 000000000000..1d5c71bdb6c7 --- /dev/null +++ b/pkg/fanal/applier/docker_test.go @@ -0,0 +1,1530 @@ +package applier_test + +import ( + "sort" + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/fanal/applier" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestApplyLayers(t *testing.T) { + tests := []struct { + name string + inputLayers []types.BlobInfo + want types.ArtifactDetail + }{ + { + name: "happy path", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + OS: types.OS{ + Family: "alpine", + Name: "3.10", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: types.Packages{ + { + Name: "openssl", + Version: "1.2.3", + Release: "4.5.6", + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: types.Bundler, + FilePath: "app/Gemfile.lock", + Packages: types.Packages{ + { + Name: "gemlibrary1", + Version: "1.2.3", + }, + }, + }, + { + Type: types.Composer, + FilePath: "app/composer.lock", + Packages: types.Packages{ + { + Name: "phplibrary1", + Version: "6.6.6", + }, + }, + }, + { + Type: types.GemSpec, + FilePath: "usr/local/bundle/specifications/gon-6.3.2.gemspec", + Packages: types.Packages{ + { + Name: "gon", + Version: "6.3.2", + FilePath: "usr/local/bundle/specifications/gon-6.3.2.gemspec", + }, + }, + }, + }, + }, + { + SchemaVersion: 1, + Size: 2000, + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + WhiteoutFiles: []string{"app/composer.lock"}, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: types.Packages{ + { + Name: "openssl", + Version: "1.2.3", + Release: "4.5.6", + }, + { + // added + Name: "musl", + Version: "1.2.4", + Release: "4.5.7", + }, + }, + }, + }, + }, + { + SchemaVersion: 1, + Size: 3000, + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: types.Packages{ + { + Name: "openssl", + Version: "1.2.3", + Release: "4.5.6", + }, + { + Name: "musl", + Version: "1.2.4", + Release: "4.5.8", // updated + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: types.GemSpec, + FilePath: "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec", + Packages: types.Packages{ + { + Name: "activesupport", + Version: "6.0.2.1", + FilePath: "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec", + }, + }, + }, + }, + }, + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "alpine", + Name: "3.10", + }, + Packages: types.Packages{ + { + Name: "musl", + Version: "1.2.4", + Release: "4.5.8", + Identifier: types.PkgIdentifier{ + UID: "108c0f3943d7bc9", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.2.4-4.5.8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.10", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + }, + { + Name: "openssl", + Version: "1.2.3", + Release: "4.5.6", + Identifier: types.PkgIdentifier{ + UID: "9d77cb17d1fc8736", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "openssl", + Version: "1.2.3-4.5.6", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.10", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + }, + }, + Applications: []types.Application{ + { + Type: types.GemSpec, + Packages: types.Packages{ + { + Name: "activesupport", + Version: "6.0.2.1", + FilePath: "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec", + Layer: types.Layer{ + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + Identifier: types.PkgIdentifier{ + UID: "b3549e98a3094a66", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "activesupport", + Version: "6.0.2.1", + }, + }, + }, + { + Name: "gon", + Version: "6.3.2", + FilePath: "usr/local/bundle/specifications/gon-6.3.2.gemspec", + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + Identifier: types.PkgIdentifier{ + UID: "f27f3b46e09fc2e2", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "gon", + Version: "6.3.2", + }, + }, + }, + }, + }, + { + Type: types.Bundler, + FilePath: "app/Gemfile.lock", + Packages: types.Packages{ + { + Name: "gemlibrary1", + Version: "1.2.3", + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + Identifier: types.PkgIdentifier{ + UID: "a3363562b587cfa2", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "gemlibrary1", + Version: "1.2.3", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with duplicate packages", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 2, + Size: 1000, + DiffID: "sha256:96e320b34b5478d8b369ca43ffaa88ff6dd9499ec72b792ca21b1e8b0c55670f", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/libssl1", + Packages: types.Packages{ + { + ID: "libssl1.1@1.1.1n-0+deb11u3", + Name: "libssl1.1", + Version: "1.1.1n", + Release: "0+deb11u3", + }, + }, + }, + { + FilePath: "foo/socat-1.7.3.2-2.el7.x86_64.rpm", + Packages: types.Packages{ + { + Name: "socat", + Version: "1.7.3.2", + Release: "2.el7", + Arch: "x86_64", + SrcName: "socat", + SrcVersion: "1.7.3.2", + SrcRelease: "2.el7", + FilePath: "foo/socat-1.7.3.2-2.el7.x86_64.rpm", + }, + }, + }, + { + FilePath: "bar/socat-1.7.3.2-2.el7.x86_64.rpm", + Packages: types.Packages{ + { + Name: "socat", + Version: "1.7.3.2", + Release: "2.el7", + Arch: "x86_64", + SrcName: "socat", + SrcVersion: "1.7.3.2", + SrcRelease: "2.el7", + FilePath: "bar/socat-1.7.3.2-2.el7.x86_64.rpm", + }, + }, + }, + }, + }, + { + SchemaVersion: 2, + Size: 2000, + DiffID: "sha256:5e087d956f3e62bd034dd0712bc4cbef8fda55fba0b11a7d0564f294887c7079", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/libssl1.1", + Packages: types.Packages{ + { + ID: "libssl1.1@1.1.1n-0+deb11u3", + Name: "libssl1.1", + Version: "1.1.1n", + Release: "0+deb11u3", + }, + }, + }, + }, + }, + }, + want: types.ArtifactDetail{ + Packages: types.Packages{ + { + ID: "libssl1.1@1.1.1n-0+deb11u3", + Name: "libssl1.1", + Version: "1.1.1n", + Release: "0+deb11u3", + Identifier: types.PkgIdentifier{ + UID: "522a5c3b263d1357", + }, + Layer: types.Layer{ + DiffID: "sha256:96e320b34b5478d8b369ca43ffaa88ff6dd9499ec72b792ca21b1e8b0c55670f", + }, + }, + { + Name: "socat", + Version: "1.7.3.2", + Release: "2.el7", + Arch: "x86_64", + SrcName: "socat", + SrcVersion: "1.7.3.2", + SrcRelease: "2.el7", + FilePath: "bar/socat-1.7.3.2-2.el7.x86_64.rpm", + Layer: types.Layer{ + DiffID: "sha256:96e320b34b5478d8b369ca43ffaa88ff6dd9499ec72b792ca21b1e8b0c55670f", + }, + Identifier: types.PkgIdentifier{ + UID: "bfb68335f6284b36", + }, + }, + { + Name: "socat", + Version: "1.7.3.2", + Release: "2.el7", + Arch: "x86_64", + SrcName: "socat", + SrcVersion: "1.7.3.2", + SrcRelease: "2.el7", + FilePath: "foo/socat-1.7.3.2-2.el7.x86_64.rpm", + Layer: types.Layer{ + DiffID: "sha256:96e320b34b5478d8b369ca43ffaa88ff6dd9499ec72b792ca21b1e8b0c55670f", + }, + Identifier: types.PkgIdentifier{ + UID: "4d8db4fac0caf460", + }, + }, + }, + }, + }, + { + name: "happy path with digests in libs/packages (as for SBOM)", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 2, + OS: types.OS{ + Family: "debian", + Name: "11.8", + }, + PackageInfos: []types.PackageInfo{ + { + Packages: types.Packages{ + { + ID: "adduser@3.118+deb11u1", + Name: "adduser", + Version: "3.118+deb11u1", + Arch: "all", + SrcName: "adduser", + SrcVersion: "3.118+deb11u1", + Layer: types.Layer{ + Digest: "sha256:e67fdae3559346105027c63e7fb032bba57e62b1fe9f2da23e6fdfb56384e00b", + DiffID: "sha256:633f5bf471f7595b236a21e62dc60beef321db45916363a02ad5af02d794d497", + }, + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: types.PythonPkg, + Packages: types.Packages{ + { + Name: "pip", + Version: "23.0.1", + Layer: types.Layer{ + DiffID: "sha256:1def056a3160854c9395aa76282dd62172ec08c18a5fa03bb7d50a777c15ba99", + }, + FilePath: "usr/local/lib/python3.9/site-packages/pip-23.0.1.dist-info/METADATA", + }, + }, + }, + }, + }, + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "debian", + Name: "11.8", + }, + Packages: types.Packages{ + { + ID: "adduser@3.118+deb11u1", + Name: "adduser", + Version: "3.118+deb11u1", + Arch: "all", + SrcName: "adduser", + SrcVersion: "3.118+deb11u1", + Layer: types.Layer{ + Digest: "sha256:e67fdae3559346105027c63e7fb032bba57e62b1fe9f2da23e6fdfb56384e00b", + DiffID: "sha256:633f5bf471f7595b236a21e62dc60beef321db45916363a02ad5af02d794d497", + }, + Identifier: types.PkgIdentifier{ + UID: "e984be704d7e13ef", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "adduser", + Version: "3.118+deb11u1", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "all", + }, + { + Key: "distro", + Value: "debian-11.8", + }, + }, + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: types.PythonPkg, + Packages: types.Packages{ + { + Name: "pip", + Version: "23.0.1", + FilePath: "usr/local/lib/python3.9/site-packages/pip-23.0.1.dist-info/METADATA", + Layer: types.Layer{ + DiffID: "sha256:1def056a3160854c9395aa76282dd62172ec08c18a5fa03bb7d50a777c15ba99", + }, + Identifier: types.PkgIdentifier{ + UID: "8d8c54cecea3dd33", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypePyPi, + Name: "pip", + Version: "23.0.1", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with merging ubuntu version and ESM", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + OS: types.OS{ + Family: "ubuntu", + Extended: true, + }, + }, + { + SchemaVersion: 1, + Size: 2000, + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + OS: types.OS{ + Family: "ubuntu", + Name: "16.04", + }, + }, + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "ubuntu", + Name: "16.04", + Extended: true, + }, + }, + }, + { + name: "happy path with removed and updated lockfile", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + OS: types.OS{ + Family: "alpine", + Name: "3.10", + }, + Applications: []types.Application{ + { + Type: types.Bundler, + FilePath: "app/Gemfile.lock", + Packages: types.Packages{ + { + Name: "rails", + Version: "5.0.0", + }, + { + Name: "rack", + Version: "4.0.0", + }, + }, + }, + { + Type: types.Composer, + FilePath: "app/composer.lock", + Packages: types.Packages{ + { + Name: "phplibrary1", + Version: "6.6.6", + }, + }, + }, + { + Type: types.GemSpec, + FilePath: "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec", + Packages: types.Packages{ + { + Name: "activesupport", + Version: "6.0.2.1", + FilePath: "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec", + }, + }, + }, + }, + }, + { + SchemaVersion: 1, + Size: 2000, + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + WhiteoutFiles: []string{ + "app/composer.lock", + "var/lib/gems/2.5.0/specifications/activesupport-6.0.2.1.gemspec", + }, + Applications: []types.Application{ + { + Type: types.Bundler, + FilePath: "app/Gemfile.lock", + Packages: types.Packages{ + { + Name: "rails", + Version: "6.0.0", + }, + { + Name: "rack", + Version: "4.0.0", + }, + }, + }, + { + Type: "composer", + FilePath: "app/composer2.lock", + Packages: types.Packages{ + { + Name: "phplibrary1", + Version: "6.6.6", + }, + }, + }, + }, + }, + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "alpine", + Name: "3.10", + }, + Applications: []types.Application{ + { + Type: types.Bundler, + FilePath: "app/Gemfile.lock", + Packages: types.Packages{ + { + Name: "rack", + Version: "4.0.0", + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + Identifier: types.PkgIdentifier{ + UID: "9744e21755aea0ef", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "rack", + Version: "4.0.0", + }, + }, + }, + { + Name: "rails", + Version: "6.0.0", + Layer: types.Layer{ + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + Identifier: types.PkgIdentifier{ + UID: "7e9712137f044ffe", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "rails", + Version: "6.0.0", + }, + }, + }, + }, + }, + { + Type: types.Composer, + FilePath: "app/composer2.lock", + Packages: types.Packages{ + { + Name: "phplibrary1", + Version: "6.6.6", + Layer: types.Layer{ + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + Identifier: types.PkgIdentifier{ + UID: "940351428c1fed49", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Name: "phplibrary1", + Version: "6.6.6", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with removed and updated secret", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 2, + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + CreatedBy: "Line_1", + Secrets: []types.Secret{ + { + FilePath: "usr/secret.txt", + Findings: []types.SecretFinding{ + { + RuleID: "aws-access-key-id", + Category: "AWS", + Severity: "CRITICAL", + Title: "AWS Access Key ID", + StartLine: 1, + EndLine: 1, + Match: "AWS_ACCESS_KEY_ID=********************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "AWS_ACCESS_KEY_ID=********************", + IsCause: true, + Highlighted: "AWS_ACCESS_KEY_ID=********************", + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + }, + }, + }, + }, + { + SchemaVersion: 2, + Size: 2000, + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + CreatedBy: "Line_2", + Secrets: []types.Secret{ + { + FilePath: "usr/secret.txt", + Findings: []types.SecretFinding{ + { + RuleID: "github-pat", + Category: "GitHub", + Severity: "CRITICAL", + Title: "GitHub Personal Access Token", + StartLine: 1, + EndLine: 1, + Match: "GITHUB_PAT=****************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "GITHUB_PAT=****************************************", + IsCause: true, + Highlighted: "GITHUB_PAT=****************************************", + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + { + RuleID: "aws-access-key-id", + Category: "AWS", + Severity: "CRITICAL", + Title: "AWS Access Key ID", + StartLine: 2, + EndLine: 2, + Match: "AWS_ACCESS_KEY_ID=********************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "AWS_ACCESS_KEY_ID=********************", + IsCause: true, + Highlighted: "AWS_ACCESS_KEY_ID=********************", + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + }, + }, + }, + }, + { + SchemaVersion: 2, + Size: 3000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + CreatedBy: "Line_3", + WhiteoutFiles: []string{ + "usr/secret.txt", + }, + }, + }, + want: types.ArtifactDetail{ + Secrets: []types.Secret{ + { + FilePath: "usr/secret.txt", + Findings: []types.SecretFinding{ + { + RuleID: "github-pat", + Category: "GitHub", + Severity: "CRITICAL", + Title: "GitHub Personal Access Token", + StartLine: 1, + EndLine: 1, + Match: "GITHUB_PAT=****************************************", + Layer: types.Layer{ + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + CreatedBy: "Line_2", + }, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "GITHUB_PAT=****************************************", + IsCause: true, + Highlighted: "GITHUB_PAT=****************************************", + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + { + RuleID: "aws-access-key-id", + Category: "AWS", + Severity: "CRITICAL", + Title: "AWS Access Key ID", + StartLine: 2, + EndLine: 2, + Match: "AWS_ACCESS_KEY_ID=********************", + Layer: types.Layer{ + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + CreatedBy: "Line_2", + }, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "AWS_ACCESS_KEY_ID=********************", + IsCause: true, + Highlighted: "AWS_ACCESS_KEY_ID=********************", + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with status.d and opaque dirs without the trailing slash", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + OS: types.OS{ + Family: "debian", + Name: "8", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/openssl", + Packages: types.Packages{ + { + Name: "openssl", + Version: "1.2.3", + Release: "4.5.6", + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "app/composer.lock", + Packages: types.Packages{ + { + Name: "phplibrary1", + Version: "6.6.6", + }, + }, + }, + }, + Licenses: []types.LicenseFile{ + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/openssl/copyright", + Findings: []types.LicenseFinding{ + {Name: "OpenSSL"}, + }, + PkgName: "openssl", + }, + }, + }, + { + SchemaVersion: 1, + Size: 2000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + OpaqueDirs: []string{"app"}, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/libc", + Packages: types.Packages{ + { + Name: "libc", + Version: "1.2.4", + Release: "4.5.7", + }, + }, + }, + }, + Licenses: []types.LicenseFile{ + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/libc/copyright", + Findings: []types.LicenseFinding{ + {Name: "GPL-2"}, + }, + PkgName: "libc", + }, + }, + }, + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "debian", + Name: "8", + }, + Packages: types.Packages{ + { + Name: "libc", + Version: "1.2.4", + Release: "4.5.7", + Licenses: []string{"GPL-2"}, + Identifier: types.PkgIdentifier{ + UID: "c3c9ea1442ead294", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "libc", + Version: "1.2.4-4.5.7", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "debian-8", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + }, + { + Name: "openssl", + Version: "1.2.3", + Release: "4.5.6", + Licenses: []string{"OpenSSL"}, + Identifier: types.PkgIdentifier{ + UID: "9d77cb17d1fc8736", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "openssl", + Version: "1.2.3-4.5.6", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "debian-8", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + }, + }, + }, + }, + { + name: "happy path with filling system files for debian packages", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 2, + Size: 1000, + DiffID: "sha256:cdd7c73923174e45ea648d66996665c288e1b17a0f45efdbeca860f6dafdf731", + OS: types.OS{ + Family: "ubuntu", + Name: "24.04", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: types.Packages{ + { + ID: "apt@2.4.9", + Name: "apt", + Version: "2.4.9", + Arch: "amd64", + SrcName: "apt", + SrcVersion: "2.4.9", + InstalledFiles: []string{ + "/etc/apt/apt.conf.d/01-vendor-ubuntu", + "/etc/apt/apt.conf.d/01autoremove", + "/etc/apt/auth.conf.d", + "/etc/apt/keyrings", + }, + }, + }, + }, + }, + }, + // Install `curl` + { + SchemaVersion: 2, + Size: 2000, + DiffID: "sha256:faf30fa9c41c10f93b3b134d7b2c16e07753320393e020c481f0c97d10db067d", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: types.Packages{ + { + ID: "apt@2.4.9", + Name: "apt", + Version: "2.4.9", + Arch: "amd64", + SrcName: "apt", + SrcVersion: "2.4.9", + }, + { + ID: "curl@8.5.0-2ubuntu10.1", + Name: "curl", + Version: "8.5.0", + Release: "2ubuntu10.1", + Arch: "arm64", + SrcName: "curl", + SrcVersion: "8.5.0", + SrcRelease: "2ubuntu10.1", + InstalledFiles: []string{ + "/usr/bin/curl", + "/usr/share/doc/curl/README.Debian", + "/usr/share/doc/curl/changelog.Debian.gz", + "/usr/share/doc/curl/copyright", + "/usr/share/man/man1/curl.1.gz", + "/usr/share/zsh/vendor-completions/_curl", + }, + }, + }, + }, + }, + }, + // Upgrade `apt` + { + SchemaVersion: 2, + Size: 3000, + DiffID: "sha256:440e26edc0eb9b4fee6e1d40d8af9eb59500d38e25edfc5d5302c55f59394c1e", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: types.Packages{ + { + ID: "apt@2.4.12", + Name: "apt", + Version: "2.4.12", + Arch: "amd64", + SrcName: "apt", + SrcVersion: "2.4.12", + InstalledFiles: []string{ + "/etc/apt/apt.conf.d/01-vendor-ubuntu", + "/etc/apt/apt.conf.d/01autoremove", + "/etc/apt/auth.conf.d", + "/etc/apt/keyrings", + "/usr/share/man/it/man5/sources.list.5.gz", + }, + }, + { + ID: "curl@8.5.0-2ubuntu10.1", + Name: "curl", + Version: "8.5.0", + Release: "2ubuntu10.1", + Arch: "arm64", + SrcName: "curl", + SrcVersion: "8.5.0", + SrcRelease: "2ubuntu10.1", + }, + }, + }, + }, + }, + // Remove curl + { + SchemaVersion: 2, + Size: 4000, + DiffID: "sha256:cb04e1d437de723d8d04bc7df89dc42271530c5f8ea1724c6072e3f0e7d6d38a", + WhiteoutFiles: []string{ + "usr/bin/curl", + "usr/share/doc/curl", + "usr/share/zsh", + "var/lib/dpkg/info/curl.list", + "var/lib/dpkg/info/curl.md5sums", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status", + Packages: types.Packages{ + { + ID: "apt@2.4.12", + Name: "apt", + Version: "2.4.12", + Arch: "amd64", + SrcName: "apt", + SrcVersion: "2.4.12", + }, + }, + }, + }, + }, + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "ubuntu", + Name: "24.04", + }, + Packages: types.Packages{ + { + ID: "apt@2.4.12", + Name: "apt", + Version: "2.4.12", + Arch: "amd64", + SrcName: "apt", + SrcVersion: "2.4.12", + + Identifier: types.PkgIdentifier{ + UID: "80bc98a8f3159db9", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "ubuntu", + Name: "apt", + Version: "2.4.12", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "amd64", + }, + { + Key: "distro", + Value: "ubuntu-24.04", + }, + }, + }, + }, + Layer: types.Layer{ + DiffID: "sha256:440e26edc0eb9b4fee6e1d40d8af9eb59500d38e25edfc5d5302c55f59394c1e", + }, + InstalledFiles: []string{ + "/etc/apt/apt.conf.d/01-vendor-ubuntu", + "/etc/apt/apt.conf.d/01autoremove", + "/etc/apt/auth.conf.d", + "/etc/apt/keyrings", + "/usr/share/man/it/man5/sources.list.5.gz", + }, + }, + }, + }, + }, + { + name: "happy path, opaque dirs with the trailing slash", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + Applications: []types.Application{ + { + Type: "composer", + FilePath: "app/composer.lock", + Packages: types.Packages{ + { + Name: "phplibrary1", + Version: "6.6.6", + }, + }, + }, + }, + }, + { + SchemaVersion: 1, + Size: 2000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + OpaqueDirs: []string{"app/"}, + }, + }, + want: types.ArtifactDetail{}, + }, + { + name: "happy path with Red Hat content sets", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + OS: types.OS{ + Family: "redhat", + Name: "8", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/rpm/Packages", + Packages: types.Packages{ + { + Name: "openssl", + Version: "1.2.3", + Release: "4", + }, + }, + }, + }, + }, + { + SchemaVersion: 1, + Size: 2000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + BuildInfo: &types.BuildInfo{ + ContentSets: []string{ + "rhel-8-for-x86_64-baseos-rpms", + "rhel-8-for-x86_64-appstream-rpms", + }, + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/rpm/Packages", + Packages: types.Packages{ + { + Name: "openssl", + Version: "1.2.3", + Release: "4", + }, + { + Name: "libc", + Version: "1.2.4", + Release: "5", + }, + }, + }, + }, + }, + { + SchemaVersion: 1, + Size: 3000, + Digest: "sha256:a64e5f34c33ed4c5121498e721e24d95dae2c9599bee4aa6d07850702b401406", + DiffID: "sha256:0abd3f2c73de6f02e033f410590111f9339b9500dc07270234f283f2d9a2694b", + BuildInfo: &types.BuildInfo{ + Nvr: "3scale-amp-apicast-gateway-container-1.11-1", + Arch: "x86_64", + }, + }, + { + SchemaVersion: 1, + Size: 4000, + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/rpm/Packages", + Packages: types.Packages{ + { + Name: "openssl", + Version: "1.2.3", + Release: "4", + }, + { + Name: "libc", + Version: "1.2.4", + Release: "5", + }, + { + Name: "bash", + Version: "5.6.7", + Release: "8", + }, + }, + }, + }, + }, + }, + want: types.ArtifactDetail{ + OS: types.OS{ + Family: "redhat", + Name: "8", + }, + Packages: types.Packages{ + { + Name: "bash", + Version: "5.6.7", + Release: "8", + Identifier: types.PkgIdentifier{ + UID: "3982c06acacff066", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "bash", + Version: "5.6.7-8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "redhat-8", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + BuildInfo: &types.BuildInfo{ + Nvr: "3scale-amp-apicast-gateway-container-1.11-1", + Arch: "x86_64", + }, + }, + { + Name: "libc", + Version: "1.2.4", + Release: "5", + Identifier: types.PkgIdentifier{ + UID: "8a72001605297eac", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "libc", + Version: "1.2.4-5", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "redhat-8", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + BuildInfo: &types.BuildInfo{ + ContentSets: []string{ + "rhel-8-for-x86_64-baseos-rpms", + "rhel-8-for-x86_64-appstream-rpms", + }, + }, + }, + { + Name: "openssl", + Version: "1.2.3", + Release: "4", + Identifier: types.PkgIdentifier{ + UID: "8de1ca1c33881bac", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "openssl", + Version: "1.2.3-4", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "redhat-8", + }, + }, + }, + }, + Layer: types.Layer{ + Digest: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + DiffID: "sha256:aad63a9339440e7c3e1fff2b988991b9bfb81280042fa7f39a5e327023056819", + }, + BuildInfo: &types.BuildInfo{ + ContentSets: []string{ + "rhel-8-for-x86_64-baseos-rpms", + "rhel-8-for-x86_64-appstream-rpms", + }, + }, + }, + }, + }, + }, + { + name: "same package but different file path", // different hashes + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 1, + Size: 1000, + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + Applications: []types.Application{ + { + Type: types.Bundler, + FilePath: "app1/Gemfile.lock", + Packages: types.Packages{ + { + Name: "gemlibrary1", + Version: "1.2.3", + }, + }, + }, + { + Type: types.Bundler, + FilePath: "app2/Gemfile.lock", + Packages: types.Packages{ + { + Name: "gemlibrary1", + Version: "1.2.3", + }, + }, + }, + }, + }, + }, + want: types.ArtifactDetail{ + Applications: []types.Application{ + { + Type: types.Bundler, + FilePath: "app1/Gemfile.lock", + Packages: types.Packages{ + { + Name: "gemlibrary1", + Version: "1.2.3", + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + Identifier: types.PkgIdentifier{ + UID: "176111c6c0c6488", // different hash + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "gemlibrary1", + Version: "1.2.3", + }, + }, + }, + }, + }, + { + Type: types.Bundler, + FilePath: "app2/Gemfile.lock", + Packages: types.Packages{ + { + Name: "gemlibrary1", + Version: "1.2.3", + Layer: types.Layer{ + Digest: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + DiffID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + Identifier: types.PkgIdentifier{ + UID: "e1416731a0829253", // different hash + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "gemlibrary1", + Version: "1.2.3", + }, + }, + }, + }, + }, + }, + }, + }, + { + // Duplicate packages with different PURL namespaces, prefer OS-matching PURL + name: "prefer OS-matching PURL during deduplication", + inputLayers: []types.BlobInfo{ + { + SchemaVersion: 2, + OS: types.OS{Family: "chainguard", Name: "20230201"}, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: types.Packages{ + {Name: "libcrypto3", Version: "3.0.0"}, // No PURL - will get chainguard + { + Name: "libcrypto3", + Version: "3.0.0", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "wolfi", // Mismatched + Name: "libcrypto3", + Version: "3.0.0", + Qualifiers: packageurl.Qualifiers{{Key: "distro", Value: "wolfi"}}, + }, + }, + }, + }, + }, + }, + }, + }, + want: types.ArtifactDetail{ + OS: types.OS{Family: "chainguard", Name: "20230201"}, + Packages: types.Packages{ + { + Name: "libcrypto3", + Version: "3.0.0", + Identifier: types.PkgIdentifier{ + UID: "bdca9f208c0174a0", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "chainguard", // Matches OS + Name: "libcrypto3", + Version: "3.0.0", + Qualifiers: packageurl.Qualifiers{{Key: "distro", Value: "20230201"}}, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := applier.ApplyLayers(tt.inputLayers) + sort.Sort(got.Packages) + sort.Slice(got.Applications, func(i, j int) bool { + return got.Applications[i].FilePath < got.Applications[j].FilePath + }) + for _, app := range got.Applications { + sort.Sort(app.Packages) + } + assert.Equal(t, tt.want, got, tt.name) + }) + } +} diff --git a/pkg/fanal/artifact/artifact.go b/pkg/fanal/artifact/artifact.go new file mode 100644 index 000000000000..4d9e4b3e00f3 --- /dev/null +++ b/pkg/fanal/artifact/artifact.go @@ -0,0 +1,119 @@ +package artifact + +import ( + "context" + "slices" + "sort" + + "github.com/google/go-containerregistry/pkg/v1" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/image/name" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/sbom/core" +) + +type Option struct { + Type types.ArtifactType + AnalyzerGroup analyzer.Group // It is empty in OSS + DisabledAnalyzers []analyzer.Type + DisabledHandlers []types.HandlerType + FilePatterns []string + Parallel int + NoProgress bool + Insecure bool + Offline bool + AppDirs []string + SBOMSources []string + RekorURL string + AWSRegion string + AWSEndpoint string + FileChecksum bool // For SPDX + DetectionPriority types.DetectionPriority + + // Original is the original target location, e.g. "github.com/aquasecurity/trivy" + // Currently, it is used only for remote git repositories + Original string + + // Git repositories + RepoBranch string + RepoCommit string + RepoTag string + + // For image scanning + ImageOption types.ImageOptions + + MisconfScannerOption misconf.ScannerOption + SecretScannerOption analyzer.SecretScannerOption + LicenseScannerOption analyzer.LicenseScannerOption + + WalkerOption walker.Option +} + +func (o *Option) AnalyzerOptions() analyzer.AnalyzerOptions { + return analyzer.AnalyzerOptions{ + Group: o.AnalyzerGroup, + FilePatterns: o.FilePatterns, + Parallel: o.Parallel, + DisabledAnalyzers: o.DisabledAnalyzers, + DetectionPriority: o.DetectionPriority, + MisconfScannerOption: o.MisconfScannerOption, + SecretScannerOption: o.SecretScannerOption, + LicenseScannerOption: o.LicenseScannerOption, + } +} + +func (o *Option) ConfigAnalyzerOptions() analyzer.ConfigAnalyzerOptions { + return analyzer.ConfigAnalyzerOptions{ + FilePatterns: o.FilePatterns, + DisabledAnalyzers: o.DisabledAnalyzers, + MisconfScannerOption: o.MisconfScannerOption, + SecretScannerOption: o.SecretScannerOption, + } +} + +func (o *Option) Sort() { + slices.Sort(o.DisabledAnalyzers) + sort.Strings(o.WalkerOption.SkipFiles) + sort.Strings(o.WalkerOption.SkipDirs) + sort.Strings(o.FilePatterns) +} + +type Artifact interface { + Inspect(ctx context.Context) (reference Reference, err error) + Clean(reference Reference) error +} + +// Reference represents a reference of container image, local filesystem and repository +type Reference struct { + Name string // image name, tar file name, directory or repository name + Type types.ArtifactType + ID string + BlobIDs []string + ImageMetadata ImageMetadata + RepoMetadata RepoMetadata + + // SBOM + BOM *core.BOM +} + +type ImageMetadata struct { + ID string // image ID + DiffIDs []string // uncompressed layer IDs + RepoTags []string + RepoDigests []string + Reference name.Reference // image reference matching the artifact name + ConfigFile v1.ConfigFile +} + +type RepoMetadata struct { + RepoURL string // repository URL (from upstream/origin) + Branch string // current branch name + Tags []string // tag names pointing to HEAD + Commit string // commit hash + CommitMsg string // commit message + Author string // commit author + Committer string // commit committer +} diff --git a/pkg/fanal/artifact/image/image.go b/pkg/fanal/artifact/image/image.go new file mode 100644 index 000000000000..201b91cf9dc6 --- /dev/null +++ b/pkg/fanal/artifact/image/image.go @@ -0,0 +1,638 @@ +package image + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "reflect" + "slices" + "strings" + + "github.com/docker/go-units" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/samber/lo" + "golang.org/x/sync/errgroup" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler" + "github.com/aquasecurity/trivy/pkg/fanal/image" + "github.com/aquasecurity/trivy/pkg/fanal/image/name" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/parallel" + "github.com/aquasecurity/trivy/pkg/semaphore" + trivyTypes "github.com/aquasecurity/trivy/pkg/types" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xos "github.com/aquasecurity/trivy/pkg/x/os" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +const artifactVersion = 1 + +type Artifact struct { + logger *log.Logger + image types.Image + cache cache.ArtifactCache + walker walker.LayerTar + analyzer analyzer.AnalyzerGroup // analyzer for files in container image + configAnalyzer analyzer.ConfigAnalyzerGroup // analyzer for container image config + handlerManager handler.Manager + + artifactOption artifact.Option + + layerCacheDir string +} + +func NewArtifact(img types.Image, c cache.ArtifactCache, opt artifact.Option) (artifact.Artifact, error) { + // Initialize handlers + handlerManager, err := handler.NewManager(opt) + if err != nil { + return nil, xerrors.Errorf("handler init error: %w", err) + } + + a, err := analyzer.NewAnalyzerGroup(opt.AnalyzerOptions()) + if err != nil { + return nil, xerrors.Errorf("analyzer group error: %w", err) + } + + ca, err := analyzer.NewConfigAnalyzerGroup(opt.ConfigAnalyzerOptions()) + if err != nil { + return nil, xerrors.Errorf("config analyzer group error: %w", err) + } + + cacheDir, err := xos.MkdirTemp("", "image-layers-") + if err != nil { + return nil, xerrors.Errorf("failed to create a cache layers temp dir: %w", err) + } + + return Artifact{ + logger: log.WithPrefix("image"), + image: img, + cache: c, + walker: walker.NewLayerTar(opt.WalkerOption), + analyzer: a, + configAnalyzer: ca, + handlerManager: handlerManager, + + artifactOption: opt, + layerCacheDir: cacheDir, + }, nil +} + +func (a Artifact) Inspect(ctx context.Context) (ref artifact.Reference, err error) { + imageID, err := a.image.ID() + if err != nil { + return artifact.Reference{}, xerrors.Errorf("unable to get the image ID: %w", err) + } + a.logger.Debug("Detected image ID", log.String("image_id", imageID)) + + configFile, err := a.image.ConfigFile() + if err != nil { + return artifact.Reference{}, xerrors.Errorf("unable to get the image's config file: %w", err) + } + + diffIDs := a.diffIDs(configFile) + a.logger.Debug("Detected diff ID", log.Any("diff_ids", diffIDs)) + + defer func() { + if rerr := os.RemoveAll(a.layerCacheDir); rerr != nil { + log.Error("Failed to remove layer cache", log.Err(rerr)) + } + }() + if err := a.checkImageSize(ctx, diffIDs); err != nil { + return artifact.Reference{}, err + } + + // Try retrieving a remote SBOM document + if res, err := a.retrieveRemoteSBOM(ctx); err == nil { + // Found SBOM + return res, nil + } else if !errors.Is(err, errNoSBOMFound) { + // Fail on unexpected error, otherwise it falls into the usual scanning. + return artifact.Reference{}, xerrors.Errorf("remote SBOM fetching error: %w", err) + } + + // Try to detect base layers. + baseDiffIDs := a.guessBaseLayers(diffIDs, configFile) + a.logger.Debug("Detected base layers", log.Any("diff_ids", baseDiffIDs)) + + // Convert image ID and layer IDs to cache keys + imageKey, layerKeys, err := a.calcCacheKeys(imageID, diffIDs) + if err != nil { + return artifact.Reference{}, err + } + + // Parse histories and extract a list of "created_by" + layerKeyMap := a.consolidateCreatedBy(diffIDs, layerKeys, configFile) + + missingImage, missingLayers, err := a.cache.MissingBlobs(ctx, imageKey, layerKeys) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("unable to get missing layers: %w", err) + } + + missingImageKey := imageKey + if missingImage { + a.logger.Debug("Missing image ID in cache", log.String("image_id", imageID)) + } else { + missingImageKey = "" + } + + if err = a.inspect(ctx, missingImageKey, missingLayers, baseDiffIDs, layerKeyMap, configFile); err != nil { + return artifact.Reference{}, xerrors.Errorf("analyze error: %w", err) + } + + repoTags := a.image.RepoTags() + repoDigests := a.image.RepoDigests() + imgRef := a.findMatchingReference(a.image.Name(), repoTags, repoDigests) + + return artifact.Reference{ + Name: a.image.Name(), + Type: types.TypeContainerImage, + ID: imageKey, + BlobIDs: layerKeys, + ImageMetadata: artifact.ImageMetadata{ + ID: imageID, + DiffIDs: diffIDs, + RepoTags: repoTags, + RepoDigests: repoDigests, + Reference: imgRef, + ConfigFile: *configFile, + }, + }, nil +} + +func (a Artifact) Clean(_ artifact.Reference) error { + return nil +} + +// findMatchingReference finds a RepoTag or RepoDigest that matches the artifact name +func (a Artifact) findMatchingReference(artifactName string, repoTags, repoDigests []string) name.Reference { + // Convert strings to typed references + references := a.parseImageReferences(slices.Concat(repoTags, repoDigests)) + return a.findMatchingRepoReference(artifactName, references) +} + +// parseImageReferences parses repo tags/digests into name.Reference +func (a Artifact) parseImageReferences(references []string) []name.Reference { + return lo.FilterMap(references, func(ref string, _ int) (name.Reference, bool) { + tag, err := name.ParseReference(ref) + if err != nil { + a.logger.Debug("Failed to parse repo tag/digest", log.String("ref", ref), log.Err(err)) + return name.Reference{}, false + } + return tag, true + }) +} + +// findMatchingRepoReference finds a RepoTag or RepoDigest that matches the artifact name +func (a Artifact) findMatchingRepoReference(artifactName string, references []name.Reference) name.Reference { + // If there are no RepoTags or RepoDigests, return nil + if len(references) == 0 { + return name.Reference{} + } + + // Use the first available reference as fallback (tags take precedence over digests) + fallback := lo.FirstOrEmpty(references) + + // TODO(knqyf263): refactor to use a more robust method instead of suffix-based detection + // Check if artifact name looks like a file path (tar archive) + archiveExts := []string{ + ".tar", + ".gz", + ".gzip", + ".tgz", + } + ext := strings.ToLower(filepath.Ext(artifactName)) + if slices.Contains(archiveExts, ext) { + // For file paths, use the first RepoTag or RepoDigest + return fallback + } + + // Try to parse the artifact name as an image reference + artifactRef, err := name.ParseReference(artifactName) + if err != nil { + // If parsing fails, use the first RepoTag or RepoDigest + a.logger.Debug("Failed to parse artifact name as image reference, using first RepoTag", + log.String("name", artifactName), log.Err(err)) + return fallback + } + + artifactRefName := artifactRef.Name() + // Try to find a matching digest from RepoTags/RepoDigests + if ref, ok := lo.Find(references, func(d name.Reference) bool { + return artifactRefName == d.Name() + }); ok { + return ref + } + + // If no matching tag/digest found, use the first RepoTag or RepoDigest as fallback + // This also handles the case when the artifact is specified by image ID (e.g., `trivy image sha256:abc123`) + a.logger.Debug("No matching repo tag/digest found for artifact, using first one", + log.String("name", artifactName), + log.String("ref", artifactRefName), + log.String("fallback", fallback.String())) + return fallback +} + +func (a Artifact) calcCacheKeys(imageID string, diffIDs []string) (string, []string, error) { + // Pass an empty config scanner option so that the cache key can be the same, even when policies are updated. + imageKey, err := cache.CalcKey(imageID, artifactVersion, a.configAnalyzer.AnalyzerVersions(), nil, artifact.Option{}) + if err != nil { + return "", nil, err + } + + hookVersions := a.handlerManager.Versions() + var layerKeys []string + for _, diffID := range diffIDs { + blobKey, err := cache.CalcKey(diffID, artifactVersion, a.analyzer.AnalyzerVersions(), hookVersions, a.artifactOption) + if err != nil { + return "", nil, err + } + layerKeys = append(layerKeys, blobKey) + } + return imageKey, layerKeys, nil +} + +func (a Artifact) consolidateCreatedBy(diffIDs, layerKeys []string, configFile *v1.ConfigFile) map[string]types.Layer { + // save createdBy fields in order of layers + var createdBy []string + for _, h := range configFile.History { + // skip histories for empty layers + if h.EmptyLayer { + continue + } + c := strings.TrimPrefix(h.CreatedBy, "/bin/sh -c ") + c = strings.TrimPrefix(c, "#(nop) ") + createdBy = append(createdBy, c) + } + + // If history detected incorrect - use only diffID + // TODO: our current logic may not detect empty layers correctly in rare cases. + validCreatedBy := len(diffIDs) == len(createdBy) + + layerKeyMap := make(map[string]types.Layer) + for i, diffID := range diffIDs { + + var c string + if validCreatedBy { + c = createdBy[i] + } + + layerKey := layerKeys[i] + layerKeyMap[layerKey] = types.Layer{ + DiffID: diffID, + CreatedBy: c, + } + } + return layerKeyMap +} + +func (a Artifact) imageSizeError(typ string, size int64) error { + return &trivyTypes.UserError{ + Message: fmt.Sprintf( + "%s size %s exceeds maximum allowed size %s", typ, + units.HumanSizeWithPrecision(float64(size), 3), + units.HumanSize(float64(a.artifactOption.ImageOption.MaxImageSize)), + ), + } +} + +func (a Artifact) checkImageSize(ctx context.Context, diffIDs []string) error { + if a.artifactOption.ImageOption.MaxImageSize == 0 { + return nil + } + + if err := a.checkCompressedImageSize(diffIDs); err != nil { + return xerrors.Errorf("failed to get compressed image size: %w", err) + } + + if err := a.checkUncompressedImageSize(ctx, diffIDs); err != nil { + return xerrors.Errorf("failed to calculate image size: %w", err) + } + return nil +} + +func (a Artifact) checkCompressedImageSize(diffIDs []string) error { + var totalSize int64 + + for _, diffID := range diffIDs { + h, err := v1.NewHash(diffID) + if err != nil { + return xerrors.Errorf("invalid layer ID (%s): %w", diffID, err) + } + + layer, err := a.image.LayerByDiffID(h) + if err != nil { + return xerrors.Errorf("failed to get the layer (%s): %w", diffID, err) + } + layerSize, err := layer.Size() + if err != nil { + return xerrors.Errorf("failed to get layer size: %w", err) + } + totalSize += layerSize + } + + if totalSize > a.artifactOption.ImageOption.MaxImageSize { + return a.imageSizeError("compressed image", totalSize) + } + + return nil +} + +func (a Artifact) checkUncompressedImageSize(ctx context.Context, diffIDs []string) error { + var totalSize int64 + + p := parallel.NewPipeline(a.artifactOption.Parallel, false, diffIDs, + func(_ context.Context, diffID string) (int64, error) { + layerSize, err := a.saveLayer(diffID) + if err != nil { + return -1, xerrors.Errorf("failed to save layer: %w", err) + } + return layerSize, nil + }, + func(layerSize int64) error { + totalSize += layerSize + if totalSize > a.artifactOption.ImageOption.MaxImageSize { + return a.imageSizeError("uncompressed layers", totalSize) + } + return nil + }, + ) + + if err := p.Do(ctx); err != nil { + return xerrors.Errorf("pipeline error: %w", err) + } + + return nil +} + +func (a Artifact) saveLayer(diffID string) (int64, error) { + a.logger.Debug("Pulling the layer to the local cache", log.String("diff_id", diffID)) + _, rc, err := a.uncompressedLayer(diffID) + if err != nil { + return -1, xerrors.Errorf("unable to get uncompressed layer %s: %w", diffID, err) + } + defer rc.Close() + + f, err := os.Create(filepath.Join(a.layerCacheDir, diffID)) + if err != nil { + return -1, xerrors.Errorf("failed to create a file: %w", err) + } + defer f.Close() + + return io.Copy(f, rc) +} + +func (a Artifact) inspect(ctx context.Context, missingImage string, layerKeys, baseDiffIDs []string, + layerKeyMap map[string]types.Layer, configFile *v1.ConfigFile) error { + + var osFound types.OS + p := parallel.NewPipeline(a.artifactOption.Parallel, false, layerKeys, func(ctx context.Context, + layerKey string) (any, error) { + layer := layerKeyMap[layerKey] + + // If it is a base layer, secret scanning should not be performed. + var disabledAnalyzers []analyzer.Type + if slices.Contains(baseDiffIDs, layer.DiffID) { + disabledAnalyzers = append(disabledAnalyzers, analyzer.TypeSecret) + } + + layerInfo, err := a.inspectLayer(ctx, layer, disabledAnalyzers) + if err != nil { + return nil, xerrors.Errorf("failed to analyze layer (%s): %w", layer.DiffID, err) + } + if err = a.cache.PutBlob(ctx, layerKey, layerInfo); err != nil { + return nil, xerrors.Errorf("failed to store layer: %s in cache: %w", layerKey, err) + } + return layerInfo.OS, nil + + }, func(res any) error { + // To avoid race condition, merge OS info in the onResult function (main goroutine) + osInfo := res.(types.OS) + osFound.Merge(osInfo) + return nil + }) + + if err := p.Do(ctx); err != nil { + return xerrors.Errorf("pipeline error: %w", err) + } + + if missingImage != "" { + if err := a.inspectConfig(ctx, missingImage, osFound, configFile); err != nil { + return xerrors.Errorf("unable to analyze config: %w", err) + } + } + + return nil +} + +func (a Artifact) inspectLayer(ctx context.Context, layer types.Layer, disabled []analyzer.Type) (types.BlobInfo, error) { + a.logger.Debug("Missing diff ID in cache", log.String("diff_id", layer.DiffID)) + + layerDigest, rc, err := a.uncompressedLayer(layer.DiffID) + if err != nil { + return types.BlobInfo{}, xerrors.Errorf("unable to get uncompressed layer %s: %w", layer.DiffID, err) + } + defer rc.Close() + + // Count the bytes read from the layer + cr := xio.NewCountingReader(rc) + + // `errgroup` cancels the context after Wait returns, so it can’t be use later. + // We need a separate context specifically for Analyze. + eg, egCtx := errgroup.WithContext(ctx) + + // Prepare variables + opts := analyzer.AnalysisOptions{ + Offline: a.artifactOption.Offline, + FileChecksum: a.artifactOption.FileChecksum, + } + result := analyzer.NewAnalysisResult() + limit := semaphore.New(a.artifactOption.Parallel) + + // Prepare filesystem for post analysis + composite, err := a.analyzer.PostAnalyzerFS() + if err != nil { + return types.BlobInfo{}, xerrors.Errorf("unable to get post analysis filesystem: %w", err) + } + defer composite.Cleanup() + + // Walk a tar layer + opqDirs, whFiles, err := a.walker.Walk(cr, func(filePath string, info os.FileInfo, opener analyzer.Opener) error { + if err = a.analyzer.AnalyzeFile(egCtx, eg, limit, result, "", filePath, info, opener, disabled, opts); err != nil { + return xerrors.Errorf("failed to analyze %s: %w", filePath, err) + } + + // Skip post analysis if the file is not required + analyzerTypes := a.analyzer.RequiredPostAnalyzers(filePath, info) + if len(analyzerTypes) == 0 { + return nil + } + + // Build filesystem for post analysis + tmpFilePath, err := composite.CopyFileToTemp(opener, info) + if err != nil { + return xerrors.Errorf("failed to copy file to temp: %w", err) + } + if err = composite.CreateLink(analyzerTypes, "", filePath, tmpFilePath); err != nil { + return xerrors.Errorf("failed to write a file: %w", err) + } + + return nil + }) + if err != nil { + return types.BlobInfo{}, xerrors.Errorf("walk error: %w", err) + } + + // Wait for all the goroutine to finish and check errors + if err = eg.Wait(); err != nil { + return types.BlobInfo{}, xerrors.Errorf("analyze error: %w", err) + } + + // Post-analysis + if err = a.analyzer.PostAnalyze(ctx, composite, result, opts); err != nil { + return types.BlobInfo{}, xerrors.Errorf("post analysis error: %w", err) + } + + // Read the remaining bytes for blocking factor to calculate the correct layer size + // cf. https://www.reddit.com/r/devops/comments/1gwpvrm/a_deep_dive_into_the_tar_format/ + _, _ = io.Copy(io.Discard, cr) + + // Sort the analysis result for consistent results + result.Sort() + + blobInfo := types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: cr.BytesRead(), + Digest: layerDigest, + DiffID: layer.DiffID, + CreatedBy: layer.CreatedBy, + OpaqueDirs: opqDirs, + WhiteoutFiles: whFiles, + OS: result.OS, + Repository: result.Repository, + PackageInfos: result.PackageInfos, + Applications: result.Applications, + Misconfigurations: result.Misconfigurations, + Secrets: result.Secrets, + Licenses: result.Licenses, + CustomResources: result.CustomResources, + + // For Red Hat + BuildInfo: result.BuildInfo, + } + + // Call post handlers to modify blob info + if err = a.handlerManager.PostHandle(ctx, result, &blobInfo); err != nil { + return types.BlobInfo{}, xerrors.Errorf("post handler error: %w", err) + } + + return blobInfo, nil +} + +func (a Artifact) diffIDs(configFile *v1.ConfigFile) []string { + if configFile == nil { + return nil + } + return xslices.Map(configFile.RootFS.DiffIDs, v1.Hash.String) +} + +func (a Artifact) uncompressedLayer(diffID string) (string, io.ReadCloser, error) { + // diffID is a hash of the uncompressed layer + h, err := v1.NewHash(diffID) + if err != nil { + return "", nil, xerrors.Errorf("invalid layer ID (%s): %w", diffID, err) + } + + layer, err := a.image.LayerByDiffID(h) + if err != nil { + return "", nil, xerrors.Errorf("failed to get the layer (%s): %w", diffID, err) + } + + // digest is a hash of the compressed layer + var digest string + if a.isCompressed(layer) { + d, err := layer.Digest() + if err != nil { + return "", nil, xerrors.Errorf("failed to get the digest (%s): %w", diffID, err) + } + digest = d.String() + } + + f, err := os.Open(filepath.Join(a.layerCacheDir, diffID)) + if err == nil { + a.logger.Debug("Loaded the layer from the local cache", log.String("diff_id", diffID)) + return digest, f, nil + } + + rc, err := layer.Uncompressed() + if err != nil { + return "", nil, xerrors.Errorf("failed to get the layer content (%s): %w", diffID, err) + } + return digest, rc, nil +} + +// ref. https://github.com/google/go-containerregistry/issues/701 +func (a Artifact) isCompressed(l v1.Layer) bool { + _, uncompressed := reflect.TypeOf(l).Elem().FieldByName("UncompressedLayer") + return !uncompressed +} + +func (a Artifact) inspectConfig(ctx context.Context, imageID string, osFound types.OS, config *v1.ConfigFile) error { + result := lo.FromPtr(a.configAnalyzer.AnalyzeImageConfig(ctx, osFound, config)) + + info := types.ArtifactInfo{ + SchemaVersion: types.ArtifactJSONSchemaVersion, + Architecture: config.Architecture, + Created: config.Created.Time, + DockerVersion: config.DockerVersion, + OS: config.OS, + Misconfiguration: result.Misconfiguration, + Secret: result.Secret, + HistoryPackages: result.HistoryPackages, + } + + if err := a.cache.PutArtifact(ctx, imageID, info); err != nil { + return xerrors.Errorf("failed to put image info into the cache: %w", err) + } + + return nil +} + +// guessBaseLayers guesses layers in base image (call base layers). +func (a Artifact) guessBaseLayers(diffIDs []string, configFile *v1.ConfigFile) []string { + if configFile == nil { + return nil + } + + baseImageIndex := image.GuessBaseImageIndex(configFile.History) + + // Diff IDs don't include empty layers, so the index is different from histories + var diffIDIndex int + var baseDiffIDs []string + for i, h := range configFile.History { + // It is no longer base layer. + if i > baseImageIndex { + break + } + // Empty layers are not included in diff IDs. + if h.EmptyLayer { + continue + } + + if diffIDIndex >= len(diffIDs) { + // something wrong... + return nil + } + baseDiffIDs = append(baseDiffIDs, diffIDs[diffIDIndex]) + diffIDIndex++ + } + return baseDiffIDs +} diff --git a/pkg/fanal/artifact/image/image_test.go b/pkg/fanal/artifact/image/image_test.go new file mode 100644 index 000000000000..5ddacd3521c0 --- /dev/null +++ b/pkg/fanal/artifact/image/image_test.go @@ -0,0 +1,2205 @@ +package image_test + +import ( + "math/rand" + "testing" + "time" + + "github.com/docker/go-units" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/cachetest" + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" + "github.com/aquasecurity/trivy/pkg/fanal/image" + "github.com/aquasecurity/trivy/pkg/fanal/types" + + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/all" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/imgconf/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/php/composer" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/ruby/bundler" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/licensing" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/alpine" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/debian" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/dpkg" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/repo/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret" + _ "github.com/aquasecurity/trivy/pkg/fanal/handler/sysfile" +) + +// Common blob IDs used across multiple test cases to reduce duplication +const ( + alpineBaseLayerID = "sha256:6c42077a82b21707f581759b12a99cc9a593ce35a0d7be4c19c01eb48bd5ba33" + alpineBaseLayerDiffID = "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203" + alpineArtifactID = "sha256:3c709d2a158be3a97051e10cd0e30f047225cb9505101feb3fadcd395c2e0408" + composerImageID = "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72" +) + +func TestArtifact_Inspect(t *testing.T) { + alpinePkgs := types.Packages{ + { + ID: "alpine-baselayout@3.2.0-r3", + Name: "alpine-baselayout", + Version: "3.2.0-r3", + SrcName: "alpine-baselayout", + SrcVersion: "3.2.0-r3", + Licenses: []string{"GPL-2.0-only"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:8f373f5b329c3aaf136eb30c63a387661ee0f3d0", + DependsOn: []string{ + "busybox@1.31.1-r9", + "musl@1.1.24-r2", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "etc/hosts", + "etc/sysctl.conf", + "etc/group", + "etc/protocols", + "etc/fstab", + "etc/mtab", + "etc/profile", + "etc/shells", + "etc/motd", + "etc/inittab", + "etc/hostname", + "etc/modules", + "etc/services", + "etc/shadow", + "etc/passwd", + "etc/profile.d/locale", + "etc/profile.d/color_prompt", + "etc/sysctl.d/00-alpine.conf", + "etc/modprobe.d/i386.conf", + "etc/modprobe.d/blacklist.conf", + "etc/modprobe.d/aliases.conf", + "etc/modprobe.d/kms.conf", + "etc/crontabs/root", + "sbin/mkmntdirs", + "var/run", + "var/spool/mail", + "var/spool/cron/crontabs", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "alpine-keys@2.1-r2", + Name: "alpine-keys", + Version: "2.1-r2", + SrcName: "alpine-keys", + SrcVersion: "2.1-r2", + Licenses: []string{"MIT"}, + Maintainer: "Natanael Copa ", + Arch: "x86_64", + Digest: "sha1:64929f85b7f8b4adbb664d905410312936b79d9b", + InstalledFiles: []string{ + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", + "etc/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + "usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub", + "usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", + "usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", + "usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-58cbb476.rsa.pub", + "usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-58199dcc.rsa.pub", + "usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + "usr/share/apk/keys/alpine-devel@lists.alpinelinux.org-58e4f17d.rsa.pub", + "usr/share/apk/keys/aarch64/alpine-devel@lists.alpinelinux.org-58199dcc.rsa.pub", + "usr/share/apk/keys/ppc64le/alpine-devel@lists.alpinelinux.org-58cbb476.rsa.pub", + "usr/share/apk/keys/x86/alpine-devel@lists.alpinelinux.org-5243ef4b.rsa.pub", + "usr/share/apk/keys/x86/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + "usr/share/apk/keys/s390x/alpine-devel@lists.alpinelinux.org-58e4f17d.rsa.pub", + "usr/share/apk/keys/armhf/alpine-devel@lists.alpinelinux.org-524d27bb.rsa.pub", + "usr/share/apk/keys/x86_64/alpine-devel@lists.alpinelinux.org-5261cecb.rsa.pub", + "usr/share/apk/keys/x86_64/alpine-devel@lists.alpinelinux.org-4a6a0840.rsa.pub", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "apk-tools@2.10.4-r3", + Name: "apk-tools", + Version: "2.10.4-r3", + SrcName: "apk-tools", + SrcVersion: "2.10.4-r3", + Licenses: []string{"GPL-2.0-only"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:b15ad0c90e4493dfdc948d6b90a8e020da8936ef", + DependsOn: []string{ + "libcrypto1.1@1.1.1d-r3", + "libssl1.1@1.1.1d-r3", + "musl@1.1.24-r2", + "zlib@1.2.11-r3", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "sbin/apk", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "busybox@1.31.1-r9", + Name: "busybox", + Version: "1.31.1-r9", + SrcName: "busybox", + SrcVersion: "1.31.1-r9", + Licenses: []string{"GPL-2.0-only"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:a457703d71654811ea28d8d27a5cfc49ece27b34", + DependsOn: []string{ + "musl@1.1.24-r2", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "bin/busybox", + "bin/sh", + "etc/securetty", + "etc/udhcpd.conf", + "etc/logrotate.d/acpid", + "etc/network/if-up.d/dad", + "usr/share/udhcpc/default.script", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "ca-certificates-cacert@20191127-r1", + Name: "ca-certificates-cacert", + Version: "20191127-r1", + SrcName: "ca-certificates", + SrcVersion: "20191127-r1", + Licenses: []string{ + "MPL-2.0", + "GPL-2.0-or-later", + }, + Maintainer: "Natanael Copa ", + Arch: "x86_64", + Digest: "sha1:3aeb8a90d7179d2a187782e980a964494e08c5fb", + InstalledFiles: []string{ + "etc/ssl/cert.pem", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "libc-utils@0.7.2-r0", + Name: "libc-utils", + Version: "0.7.2-r0", + SrcName: "libc-dev", + SrcVersion: "0.7.2-r0", + Licenses: []string{"BSD-3-Clause"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:a7bf32bd32c6d3de2d1c4d7e753a0919b998cd01", + DependsOn: []string{ + "musl-utils@1.1.24-r2", + }, + Arch: "x86_64", + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "libcrypto1.1@1.1.1d-r3", + Name: "libcrypto1.1", + Version: "1.1.1d-r3", + SrcName: "openssl", + SrcVersion: "1.1.1d-r3", + Licenses: []string{"OpenSSL"}, + Maintainer: "Timo Teras ", + Digest: "sha1:dd8fb9a3cce7b2bcf954271da62fb85dac2b106a", + DependsOn: []string{ + "musl@1.1.24-r2", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "etc/ssl/openssl.cnf.dist", + "etc/ssl/ct_log_list.cnf", + "etc/ssl/ct_log_list.cnf.dist", + "etc/ssl/openssl.cnf", + "etc/ssl/misc/CA.pl", + "etc/ssl/misc/tsget.pl", + "etc/ssl/misc/tsget", + "lib/libcrypto.so.1.1", + "usr/lib/libcrypto.so.1.1", + "usr/lib/engines-1.1/capi.so", + "usr/lib/engines-1.1/padlock.so", + "usr/lib/engines-1.1/afalg.so", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "libssl1.1@1.1.1d-r3", + Name: "libssl1.1", + Version: "1.1.1d-r3", + SrcName: "openssl", + SrcVersion: "1.1.1d-r3", + Licenses: []string{"OpenSSL"}, + Maintainer: "Timo Teras ", + Digest: "sha1:938d46e41b3e56b339a3aeb2d02fad3d75728f35", + DependsOn: []string{ + "libcrypto1.1@1.1.1d-r3", + "musl@1.1.24-r2", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "lib/libssl.so.1.1", + "usr/lib/libssl.so.1.1", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "libtls-standalone@2.9.1-r0", + Name: "libtls-standalone", + Version: "2.9.1-r0", + SrcName: "libtls-standalone", + SrcVersion: "2.9.1-r0", + Licenses: []string{"ISC"}, + Digest: "sha1:b2e5627a56378ea6eeb962a8f33722df9393c1c5", + DependsOn: []string{ + "ca-certificates-cacert@20191127-r1", + "libcrypto1.1@1.1.1d-r3", + "libssl1.1@1.1.1d-r3", + "musl@1.1.24-r2", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "usr/lib/libtls-standalone.so.1.0.0", + "usr/lib/libtls-standalone.so.1", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "musl@1.1.24-r2", + Name: "musl", + Version: "1.1.24-r2", + SrcName: "musl", + SrcVersion: "1.1.24-r2", + Licenses: []string{"MIT"}, + Maintainer: "Timo Teräs ", + Arch: "x86_64", + Digest: "sha1:cb2316a189ebee5282c4a9bd98794cc2477a74c6", + InstalledFiles: []string{ + "lib/libc.musl-x86_64.so.1", + "lib/ld-musl-x86_64.so.1", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "musl-utils@1.1.24-r2", + Name: "musl-utils", + Version: "1.1.24-r2", + SrcName: "musl", + SrcVersion: "1.1.24-r2", + Licenses: []string{ + "MIT", + "BSD-3-Clause", + "GPL-2.0-or-later", + }, + Maintainer: "Timo Teräs ", + Digest: "sha1:6d3b45e79dbab444ca7cbfa59e2833203be6fb6a", + DependsOn: []string{ + "musl@1.1.24-r2", + "scanelf@1.2.4-r0", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "sbin/ldconfig", + "usr/bin/iconv", + "usr/bin/ldd", + "usr/bin/getconf", + "usr/bin/getent", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "scanelf@1.2.4-r0", + Name: "scanelf", + Version: "1.2.4-r0", + SrcName: "pax-utils", + SrcVersion: "1.2.4-r0", + Licenses: []string{"GPL-2.0-only"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:d6147beb32bff803b5d9f83a3bec7ab319087185", + DependsOn: []string{ + "musl@1.1.24-r2", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "usr/bin/scanelf", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "ssl_client@1.31.1-r9", + Name: "ssl_client", + Version: "1.31.1-r9", + SrcName: "busybox", + SrcVersion: "1.31.1-r9", + Licenses: []string{"GPL-2.0-only"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:3b685152af320120ae8941c740d3376b54e43c10", + DependsOn: []string{ + "libtls-standalone@2.9.1-r0", + "musl@1.1.24-r2", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "usr/bin/ssl_client", + }, + AnalyzedBy: analyzer.TypeApk, + }, + { + ID: "zlib@1.2.11-r3", + Name: "zlib", + Version: "1.2.11-r3", + SrcName: "zlib", + SrcVersion: "1.2.11-r3", + Licenses: []string{"Zlib"}, + Maintainer: "Natanael Copa ", + Digest: "sha1:acca078ee8baa93e005f57b2fae359c1efd443cd", + DependsOn: []string{ + "musl@1.1.24-r2", + }, + Arch: "x86_64", + InstalledFiles: []string{ + "lib/libz.so.1.2.11", + "lib/libz.so.1", + }, + AnalyzedBy: analyzer.TypeApk, + }, + } + + tests := []struct { + name string + imagePath string + artifactOpt artifact.Option + setupCache func(t *testing.T) cache.Cache + wantArtifact cachetest.WantArtifact + wantBlobs []cachetest.WantBlob + want artifact.Reference + wantErr string + }{ + { + name: "happy path", + imagePath: "../../test/testdata/alpine-311.tar.gz", + artifactOpt: artifact.Option{ + LicenseScannerOption: analyzer.LicenseScannerOption{Full: true}, + ImageOption: types.ImageOptions{MaxImageSize: units.GB}, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: alpineBaseLayerID, + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: 5861888, + Digest: "", + DiffID: alpineBaseLayerDiffID, + CreatedBy: "ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ", + OS: types.OS{ + Family: "alpine", + Name: "3.11.5", + }, + Repository: &types.Repository{ + Family: "alpine", + Release: "3.11", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: alpinePkgs, + }, + }, + Licenses: []types.LicenseFile{ + { + Type: "header", + FilePath: "etc/ssl/misc/CA.pl", + Findings: []types.LicenseFinding{ + { + Name: "OpenSSL", + Confidence: 1, + Link: "https://spdx.org/licenses/OpenSSL.html", + }, + }, + }, + { + Type: "header", + FilePath: "etc/ssl/misc/tsget.pl", + Findings: []types.LicenseFinding{ + { + Name: "OpenSSL", + Confidence: 1, + Link: "https://spdx.org/licenses/OpenSSL.html", + }, + }, + }, + }, + }, + }, + }, + wantArtifact: cachetest.WantArtifact{ + ID: alpineArtifactID, + ArtifactInfo: types.ArtifactInfo{ + SchemaVersion: types.ArtifactJSONSchemaVersion, + Architecture: "amd64", + Created: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC), + DockerVersion: "18.09.7", + OS: "linux", + }, + }, + want: artifact.Reference{ + Name: "../../test/testdata/alpine-311.tar.gz", + Type: types.TypeContainerImage, + ID: alpineArtifactID, + BlobIDs: []string{alpineBaseLayerID}, + ImageMetadata: artifact.ImageMetadata{ + ID: composerImageID, + DiffIDs: []string{ + alpineBaseLayerDiffID, + }, + RepoTags: []string{"alpine:3.11"}, + RepoDigests: nil, + Reference: testutil.MustParseReference(t, "alpine:3.11"), + ConfigFile: v1.ConfigFile{ + Architecture: "amd64", + Author: "", + Container: "fb71ddde5f6411a82eb056a9190f0cc1c80d7f77a8509ee90a2054428edb0024", + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)}, + DockerVersion: "18.09.7", + History: []v1.History{ + { + Author: "", + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 27725872, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ", + Comment: "", + EmptyLayer: false, + }, + { + Author: "", + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + Comment: "", + EmptyLayer: true, + }, + }, + OS: "linux", + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{"/bin/sh"}, + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + Hostname: "", + Image: "sha256:74df73bb19fbfc7fb5ab9a8234b3d98ee2fb92df5b824496679802685205ab8c", + ArgsEscaped: true, + }, + }, + }, + }, + }, + { + name: "happy path: include lock files", + imagePath: "../../test/testdata/vuln-image.tar.gz", + artifactOpt: artifact.Option{ + LicenseScannerOption: analyzer.LicenseScannerOption{Full: true}, + }, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:0bebf0773ffd87baa7c64fbdbdf79a24ae125e3f99a8adebe52d1ccbe6bed16b", types.ArtifactInfo{ + SchemaVersion: types.ArtifactJSONSchemaVersion, + })) + return c + }, + wantArtifact: cachetest.WantArtifact{ + ID: "sha256:0bebf0773ffd87baa7c64fbdbdf79a24ae125e3f99a8adebe52d1ccbe6bed16b", + ArtifactInfo: types.ArtifactInfo{ + SchemaVersion: types.ArtifactJSONSchemaVersion, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:75a461ca76eecc6cea981889d69aa1c2dd78c436108be8be1bbc29295520c7d4", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: 3061760, + Digest: "", + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + CreatedBy: "bazel build ...", + OS: types.OS{ + Family: "debian", + Name: "9.9", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/base", + Packages: types.Packages{ + { + ID: "base-files@9.9+deb9u9", + Name: "base-files", + Version: "9.9+deb9u9", + SrcName: "base-files", + SrcVersion: "9.9+deb9u9", + Maintainer: "Santiago Vila ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + AnalyzedBy: analyzer.TypeDpkg, + }, + }, + }, + { + FilePath: "var/lib/dpkg/status.d/netbase", + Packages: types.Packages{ + { + ID: "netbase@5.4", + Name: "netbase", + Version: "5.4", + SrcName: "netbase", + SrcVersion: "5.4", + Maintainer: "Marco d'Itri ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + AnalyzedBy: analyzer.TypeDpkg, + }, + }, + }, + { + FilePath: "var/lib/dpkg/status.d/tzdata", + Packages: types.Packages{ + { + ID: "tzdata@2019a-0+deb9u1", + Name: "tzdata", + Version: "2019a", + SrcName: "tzdata", + Release: "0+deb9u1", + SrcVersion: "2019a", + SrcRelease: "0+deb9u1", + Maintainer: "GNU Libc Maintainers ", + Arch: "all", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + AnalyzedBy: analyzer.TypeDpkg, + }, + }, + }, + }, + Licenses: []types.LicenseFile{ + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/base-files/copyright", + Findings: []types.LicenseFinding{ + {Name: "GPL-2.0-or-later"}, + }, + PkgName: "base-files", + }, + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/ca-certificates/copyright", + Findings: []types.LicenseFinding{ + {Name: "GPL-2.0-or-later"}, + {Name: "GPL-2.0-only"}, + {Name: "MPL-2.0"}, + }, + PkgName: "ca-certificates", + }, + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/netbase/copyright", + Findings: []types.LicenseFinding{ + {Name: "GPL-2.0-only"}, + }, + PkgName: "netbase", + }, + }, + }, + }, + { + ID: "sha256:81afc1747d0fdec7a606c27570313634ae331fab6f13566b23d0f6b3e498c050", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: 15441920, + Digest: "", + DiffID: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + CreatedBy: "bazel build ...", + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/dpkg/status.d/libc6", + Packages: types.Packages{ + { + ID: "libc6@2.24-11+deb9u4", + Name: "libc6", + Version: "2.24", + Release: "11+deb9u4", + SrcName: "glibc", + SrcVersion: "2.24", + SrcRelease: "11+deb9u4", + Maintainer: "GNU Libc Maintainers ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + AnalyzedBy: analyzer.TypeDpkg, + }, + }, + }, + { + FilePath: "var/lib/dpkg/status.d/libssl1", + Packages: types.Packages{ + { + ID: "libssl1.1@1.1.0k-1~deb9u1", + Name: "libssl1.1", + Version: "1.1.0k", + SrcName: "openssl", + Release: "1~deb9u1", + SrcVersion: "1.1.0k", + SrcRelease: "1~deb9u1", + Maintainer: "Debian OpenSSL Team ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + AnalyzedBy: analyzer.TypeDpkg, + }, + }, + }, + { + FilePath: "var/lib/dpkg/status.d/openssl", + Packages: types.Packages{ + { + ID: "openssl@1.1.0k-1~deb9u1", + Name: "openssl", + Version: "1.1.0k", + SrcName: "openssl", + Release: "1~deb9u1", + SrcVersion: "1.1.0k", + SrcRelease: "1~deb9u1", + Maintainer: "Debian OpenSSL Team ", + Arch: "amd64", + Repository: types.PackageRepository{Class: types.RepositoryClassOfficial}, + AnalyzedBy: analyzer.TypeDpkg, + }, + }, + }, + }, + Licenses: []types.LicenseFile{ + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/libc6/copyright", + Findings: []types.LicenseFinding{ + {Name: "LGPL-2.1-only"}, + {Name: "GPL-2.0-only"}, + }, + PkgName: "libc6", + }, + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/libssl1.1/copyright", + Findings: []types.LicenseFinding{ + { + Name: "OpenSSL", + Confidence: 0.9960474308300395, + Link: "https://spdx.org/licenses/OpenSSL.html", + }, + }, + PkgName: "libssl1.1", + }, + { + Type: types.LicenseTypeDpkg, + FilePath: "usr/share/doc/openssl/copyright", + Findings: []types.LicenseFinding{ + { + Name: "OpenSSL", + Confidence: 0.9960474308300395, + Link: "https://spdx.org/licenses/OpenSSL.html", + }, + }, + PkgName: "openssl", + }, + }, + }, + }, + { + ID: "sha256:0778c3e388c54f736a3d6e74ed390a91fdb42c6809f8fb743d4f72acb41a5d6d", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: 29696, + Digest: "", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + CreatedBy: "COPY file:842584685f26edb24dc305d76894f51cfda2bad0c24a05e727f9d4905d184a70 in /php-app/composer.lock ", + OpaqueDirs: []string{"php-app/"}, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: types.Packages{ + { + ID: "guzzlehttp/guzzle@6.2.0", + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + Licenses: []string{"MIT"}, + DependsOn: []string{ + "guzzlehttp/promises@v1.3.1", + "guzzlehttp/psr7@1.5.2", + }, + Locations: []types.Location{ + { + StartLine: 9, + EndLine: 73, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "guzzlehttp/promises@v1.3.1", + Name: "guzzlehttp/promises", + Version: "v1.3.1", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 74, + EndLine: 124, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "guzzlehttp/psr7@1.5.2", + Name: "guzzlehttp/psr7", + Version: "1.5.2", + Licenses: []string{"MIT"}, + DependsOn: []string{ + "psr/http-message@1.0.1", + "ralouphie/getallheaders@2.0.5", + }, + Locations: []types.Location{ + { + StartLine: 125, + EndLine: 191, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "laravel/installer@v2.0.1", + Name: "laravel/installer", + Version: "v2.0.1", + Licenses: []string{"MIT"}, + DependsOn: []string{ + "guzzlehttp/guzzle@6.2.0", + "symfony/console@v4.2.7", + "symfony/filesystem@v4.2.7", + "symfony/process@v4.2.7", + }, + Locations: []types.Location{ + { + StartLine: 192, + EndLine: 237, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Licenses: []string{"MIT"}, + DependsOn: []string{"pear/pear_exception@v1.0.0"}, + Locations: []types.Location{ + { + StartLine: 238, + EndLine: 290, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "pear/pear_exception@v1.0.0", + Name: "pear/pear_exception", + Version: "v1.0.0", + Licenses: []string{"BSD-2-Clause"}, + Locations: []types.Location{ + { + StartLine: 291, + EndLine: 345, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "psr/http-message@1.0.1", + Name: "psr/http-message", + Version: "1.0.1", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 346, + EndLine: 395, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "ralouphie/getallheaders@2.0.5", + Name: "ralouphie/getallheaders", + Version: "2.0.5", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 396, + EndLine: 435, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "symfony/console@v4.2.7", + Name: "symfony/console", + Version: "v4.2.7", + Licenses: []string{"MIT"}, + DependsOn: []string{ + "symfony/contracts@v1.0.2", + "symfony/polyfill-mbstring@v1.11.0", + }, + Locations: []types.Location{ + { + StartLine: 436, + EndLine: 507, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "symfony/contracts@v1.0.2", + Name: "symfony/contracts", + Version: "v1.0.2", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 508, + EndLine: 575, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "symfony/filesystem@v4.2.7", + Name: "symfony/filesystem", + Version: "v4.2.7", + Licenses: []string{"MIT"}, + DependsOn: []string{"symfony/polyfill-ctype@v1.11.0"}, + Locations: []types.Location{ + { + StartLine: 576, + EndLine: 625, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "symfony/polyfill-ctype@v1.11.0", + Name: "symfony/polyfill-ctype", + Version: "v1.11.0", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 626, + EndLine: 683, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "symfony/polyfill-mbstring@v1.11.0", + Name: "symfony/polyfill-mbstring", + Version: "v1.11.0", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 684, + EndLine: 742, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + { + ID: "symfony/process@v4.2.7", + Name: "symfony/process", + Version: "v4.2.7", + Licenses: []string{"MIT"}, + Locations: []types.Location{ + { + StartLine: 743, + EndLine: 791, + }, + }, + AnalyzedBy: analyzer.TypeComposer, + }, + }, + }, + }, + }, + }, + { + ID: "sha256:5a3e3f25fdc97a14d69d99c63dd640cd2d38af5b987b7a95084cce3d835970fb", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: 6656, + Digest: "", + DiffID: "sha256:a4595c43a874856bf95f3bfc4fbf78bbaa04c92c726276d4f64193a47ced0566", + CreatedBy: "COPY file:c6d0373d380252b91829a5bb3c81d5b1afa574c91cef7752d18170a231c31f6d in /ruby-app/Gemfile.lock ", + OpaqueDirs: []string{ + "ruby-app/", + }, + Applications: []types.Application{ + { + Type: "bundler", + FilePath: "ruby-app/Gemfile.lock", + Packages: types.Packages{ + { + ID: "dotenv@2.7.2", + Name: "dotenv", + Version: "2.7.2", + Indirect: false, + Relationship: types.RelationshipDirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 51, + EndLine: 51, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "faker@1.9.3", + Name: "faker", + Version: "1.9.3", + Indirect: false, + Relationship: types.RelationshipDirect, + DependsOn: []string{"i18n@1.6.0"}, + Locations: []types.Location{ + { + StartLine: 53, + EndLine: 53, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "json@2.2.0", + Name: "json", + Version: "2.2.0", + Indirect: false, + Relationship: types.RelationshipDirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 60, + EndLine: 60, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "pry@0.12.2", + Name: "pry", + Version: "0.12.2", + Indirect: false, + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "coderay@1.1.2", + "method_source@0.9.2", + }, + Locations: []types.Location{ + { + StartLine: 79, + EndLine: 79, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "rails@5.2.0", + Name: "rails", + Version: "5.2.0", + Indirect: false, + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "actioncable@5.2.3", + "actionmailer@5.2.3", + "actionpack@5.2.3", + "actionview@5.2.3", + "activejob@5.2.3", + "activemodel@5.2.3", + "activerecord@5.2.3", + "activestorage@5.2.3", + "activesupport@5.2.3", + "railties@5.2.3", + "sprockets-rails@3.2.1", + }, + Locations: []types.Location{ + { + StartLine: 86, + EndLine: 86, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "rubocop@0.67.2", + Name: "rubocop", + Version: "0.67.2", + Indirect: false, + Relationship: types.RelationshipDirect, + DependsOn: []string{ + "jaro_winkler@1.5.2", + "parallel@1.17.0", + "parser@2.6.3.0", + "psych@3.1.0", + "rainbow@3.0.0", + "ruby-progressbar@1.10.0", + "unicode-display_width@1.5.0", + }, + Locations: []types.Location{ + { + StartLine: 112, + EndLine: 112, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "actioncable@5.2.3", + Name: "actioncable", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "actionpack@5.2.3", + "nio4r@2.3.1", + "websocket-driver@0.7.0", + }, + Locations: []types.Location{ + { + StartLine: 4, + EndLine: 4, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "actionmailer@5.2.3", + Name: "actionmailer", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "actionpack@5.2.3", + "actionview@5.2.3", + "activejob@5.2.3", + "mail@2.7.1", + "rails-dom-testing@2.0.3", + }, + Locations: []types.Location{ + { + StartLine: 8, + EndLine: 8, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "actionpack@5.2.3", + Name: "actionpack", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "actionview@5.2.3", + "activesupport@5.2.3", + "rack@2.0.7", + "rack-test@1.1.0", + "rails-dom-testing@2.0.3", + "rails-html-sanitizer@1.0.3", + }, + Locations: []types.Location{ + { + StartLine: 14, + EndLine: 14, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "actionview@5.2.3", + Name: "actionview", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "activesupport@5.2.3", + "builder@3.2.3", + "erubi@1.8.0", + "rails-dom-testing@2.0.3", + "rails-html-sanitizer@1.0.3", + }, + Locations: []types.Location{ + { + StartLine: 21, + EndLine: 21, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "activejob@5.2.3", + Name: "activejob", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "activesupport@5.2.3", + "globalid@0.4.2", + }, + Locations: []types.Location{ + { + StartLine: 27, + EndLine: 27, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "activemodel@5.2.3", + Name: "activemodel", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"activesupport@5.2.3"}, + Locations: []types.Location{ + { + StartLine: 30, + EndLine: 30, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "activerecord@5.2.3", + Name: "activerecord", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "activemodel@5.2.3", + "activesupport@5.2.3", + "arel@9.0.0", + }, + Locations: []types.Location{ + { + StartLine: 32, + EndLine: 32, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "activestorage@5.2.3", + Name: "activestorage", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "actionpack@5.2.3", + "activerecord@5.2.3", + "marcel@0.3.3", + }, + Locations: []types.Location{ + { + StartLine: 36, + EndLine: 36, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "activesupport@5.2.3", + Name: "activesupport", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "concurrent-ruby@1.1.5", + "i18n@1.6.0", + "minitest@5.11.3", + "tzinfo@1.2.5", + }, + Locations: []types.Location{ + { + StartLine: 40, + EndLine: 40, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "arel@9.0.0", + Name: "arel", + Version: "9.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 45, + EndLine: 45, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "ast@2.4.0", + Name: "ast", + Version: "2.4.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 46, + EndLine: 46, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "builder@3.2.3", + Name: "builder", + Version: "3.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 47, + EndLine: 47, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "coderay@1.1.2", + Name: "coderay", + Version: "1.1.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 48, + EndLine: 48, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "concurrent-ruby@1.1.5", + Name: "concurrent-ruby", + Version: "1.1.5", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 49, + EndLine: 49, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "crass@1.0.4", + Name: "crass", + Version: "1.0.4", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 50, + EndLine: 50, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "erubi@1.8.0", + Name: "erubi", + Version: "1.8.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 52, + EndLine: 52, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "globalid@0.4.2", + Name: "globalid", + Version: "0.4.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"activesupport@5.2.3"}, + Locations: []types.Location{ + { + StartLine: 55, + EndLine: 55, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "i18n@1.6.0", + Name: "i18n", + Version: "1.6.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"concurrent-ruby@1.1.5"}, + Locations: []types.Location{ + { + StartLine: 57, + EndLine: 57, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "jaro_winkler@1.5.2", + Name: "jaro_winkler", + Version: "1.5.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 59, + EndLine: 59, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "loofah@2.2.3", + Name: "loofah", + Version: "2.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "crass@1.0.4", + "nokogiri@1.10.3", + }, + Locations: []types.Location{ + { + StartLine: 61, + EndLine: 61, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "mail@2.7.1", + Name: "mail", + Version: "2.7.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"mini_mime@1.0.1"}, + Locations: []types.Location{ + { + StartLine: 64, + EndLine: 64, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "marcel@0.3.3", + Name: "marcel", + Version: "0.3.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"mimemagic@0.3.3"}, + Locations: []types.Location{ + { + StartLine: 66, + EndLine: 66, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "method_source@0.9.2", + Name: "method_source", + Version: "0.9.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 68, + EndLine: 68, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "mimemagic@0.3.3", + Name: "mimemagic", + Version: "0.3.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 69, + EndLine: 69, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "mini_mime@1.0.1", + Name: "mini_mime", + Version: "1.0.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 70, + EndLine: 70, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "mini_portile2@2.4.0", + Name: "mini_portile2", + Version: "2.4.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 71, + EndLine: 71, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "minitest@5.11.3", + Name: "minitest", + Version: "5.11.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 72, + EndLine: 72, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "nio4r@2.3.1", + Name: "nio4r", + Version: "2.3.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 73, + EndLine: 73, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "nokogiri@1.10.3", + Name: "nokogiri", + Version: "1.10.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"mini_portile2@2.4.0"}, + Locations: []types.Location{ + { + StartLine: 74, + EndLine: 74, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "parallel@1.17.0", + Name: "parallel", + Version: "1.17.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 76, + EndLine: 76, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "parser@2.6.3.0", + Name: "parser", + Version: "2.6.3.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"ast@2.4.0"}, + Locations: []types.Location{ + { + StartLine: 77, + EndLine: 77, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "psych@3.1.0", + Name: "psych", + Version: "3.1.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 82, + EndLine: 82, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "rack@2.0.7", + Name: "rack", + Version: "2.0.7", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 83, + EndLine: 83, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "rack-test@1.1.0", + Name: "rack-test", + Version: "1.1.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"rack@2.0.7"}, + Locations: []types.Location{ + { + StartLine: 84, + EndLine: 84, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "rails-dom-testing@2.0.3", + Name: "rails-dom-testing", + Version: "2.0.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "activesupport@5.2.3", + "nokogiri@1.10.3", + }, + Locations: []types.Location{ + { + StartLine: 99, + EndLine: 99, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "rails-html-sanitizer@1.0.3", + Name: "rails-html-sanitizer", + Version: "1.0.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"loofah@2.2.3"}, + Locations: []types.Location{ + { + StartLine: 102, + EndLine: 102, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "railties@5.2.3", + Name: "railties", + Version: "5.2.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "actionpack@5.2.3", + "activesupport@5.2.3", + "method_source@0.9.2", + "rake@12.3.2", + "thor@0.20.3", + }, + Locations: []types.Location{ + { + StartLine: 104, + EndLine: 104, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "rainbow@3.0.0", + Name: "rainbow", + Version: "3.0.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 110, + EndLine: 110, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "rake@12.3.2", + Name: "rake", + Version: "12.3.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 111, + EndLine: 111, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "ruby-progressbar@1.10.0", + Name: "ruby-progressbar", + Version: "1.10.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 120, + EndLine: 120, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "sprockets@3.7.2", + Name: "sprockets", + Version: "3.7.2", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "concurrent-ruby@1.1.5", + "rack@2.0.7", + }, + Locations: []types.Location{ + { + StartLine: 121, + EndLine: 121, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "sprockets-rails@3.2.1", + Name: "sprockets-rails", + Version: "3.2.1", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{ + "actionpack@5.2.3", + "activesupport@5.2.3", + "sprockets@3.7.2", + }, + Locations: []types.Location{ + { + StartLine: 124, + EndLine: 124, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "thor@0.20.3", + Name: "thor", + Version: "0.20.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 128, + EndLine: 128, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "thread_safe@0.3.6", + Name: "thread_safe", + Version: "0.3.6", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 129, + EndLine: 129, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "tzinfo@1.2.5", + Name: "tzinfo", + Version: "1.2.5", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"thread_safe@0.3.6"}, + Locations: []types.Location{ + { + StartLine: 130, + EndLine: 130, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "unicode-display_width@1.5.0", + Name: "unicode-display_width", + Version: "1.5.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 132, + EndLine: 132, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "websocket-driver@0.7.0", + Name: "websocket-driver", + Version: "0.7.0", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string{"websocket-extensions@0.1.3"}, + Locations: []types.Location{ + { + StartLine: 133, + EndLine: 133, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + { + ID: "websocket-extensions@0.1.3", + Name: "websocket-extensions", + Version: "0.1.3", + Indirect: true, + Relationship: types.RelationshipIndirect, + DependsOn: []string(nil), + Locations: []types.Location{ + { + StartLine: 135, + EndLine: 135, + }, + }, + AnalyzedBy: analyzer.TypeBundler, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "../../test/testdata/vuln-image.tar.gz", + Type: types.TypeContainerImage, + ID: "sha256:0bebf0773ffd87baa7c64fbdbdf79a24ae125e3f99a8adebe52d1ccbe6bed16b", + BlobIDs: []string{ + "sha256:75a461ca76eecc6cea981889d69aa1c2dd78c436108be8be1bbc29295520c7d4", + "sha256:81afc1747d0fdec7a606c27570313634ae331fab6f13566b23d0f6b3e498c050", + "sha256:0778c3e388c54f736a3d6e74ed390a91fdb42c6809f8fb743d4f72acb41a5d6d", + "sha256:5a3e3f25fdc97a14d69d99c63dd640cd2d38af5b987b7a95084cce3d835970fb", + }, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", + DiffIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + "sha256:a4595c43a874856bf95f3bfc4fbf78bbaa04c92c726276d4f64193a47ced0566", + }, + RepoTags: []string{"vuln-image:latest"}, + RepoDigests: nil, + Reference: testutil.MustParseReference(t, "vuln-image:latest"), + ConfigFile: v1.ConfigFile{ + Architecture: "amd64", + Author: "", + Created: v1.Time{Time: time.Date(2020, 2, 16, 10, 38, 41, 114114788, time.UTC)}, + DockerVersion: "19.03.5", + History: []v1.History{ + { + Author: "Bazel", + Created: v1.Time{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + CreatedBy: "bazel build ...", + EmptyLayer: false, + }, + { + Author: "Bazel", + Created: v1.Time{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + CreatedBy: "bazel build ...", + EmptyLayer: false, + }, + { + Author: "", + Created: v1.Time{Time: time.Date(2020, 2, 16, 10, 38, 40, 976530082, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY file:842584685f26edb24dc305d76894f51cfda2bad0c24a05e727f9d4905d184a70 in /php-app/composer.lock ", + Comment: "", + EmptyLayer: false, + }, + { + Author: "", + Created: v1.Time{Time: time.Date(2020, 2, 16, 10, 38, 41, 114114788, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY file:c6d0373d380252b91829a5bb3c81d5b1afa574c91cef7752d18170a231c31f6d in /ruby-app/Gemfile.lock ", + Comment: "", + EmptyLayer: false, + }, + }, + OS: "linux", + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + { + Algorithm: "sha256", + Hex: "dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + }, + { + Algorithm: "sha256", + Hex: "24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + { + Algorithm: "sha256", + Hex: "a4595c43a874856bf95f3bfc4fbf78bbaa04c92c726276d4f64193a47ced0566", + }, + }, + }, + Config: v1.Config{ + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt", + }, + Image: "sha256:916390dcf84a1c7852e298f24fb5389a6e7801102086924e55eb08cd58d6a741", + }, + }, + }, + }, + }, + { + name: "happy path: disable analyzers", + imagePath: "../../test/testdata/vuln-image.tar.gz", + artifactOpt: artifact.Option{ + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeDebian, + analyzer.TypeDpkg, + analyzer.TypeDpkgLicense, + analyzer.TypeComposer, + analyzer.TypeBundler, + analyzer.TypeLicenseFile, + }, + LicenseScannerOption: analyzer.LicenseScannerOption{Full: true}, + }, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:0bebf0773ffd87baa7c64fbdbdf79a24ae125e3f99a8adebe52d1ccbe6bed16b", types.ArtifactInfo{ + SchemaVersion: types.ArtifactJSONSchemaVersion, + })) + return c + }, + wantArtifact: cachetest.WantArtifact{ + ID: "sha256:0bebf0773ffd87baa7c64fbdbdf79a24ae125e3f99a8adebe52d1ccbe6bed16b", + ArtifactInfo: types.ArtifactInfo{ + SchemaVersion: types.ArtifactJSONSchemaVersion, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:a83985cade3970577a9af328db9c88c0bf15cad40f7d2cf6d76e83882bc8146d", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: 3061760, + Digest: "", + DiffID: "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + CreatedBy: "bazel build ...", + }, + }, + { + ID: "sha256:b109622c2d106193db505762f1f3e78cf0035a69e559caf07c305c92ddb89356", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: 15441920, + Digest: "", + DiffID: "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + CreatedBy: "bazel build ...", + }, + }, + { + ID: "sha256:115f689385cb66077c338c52f2c9d6f3018a18c89be7fe7d23f1645422d7d59d", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: 29696, + Digest: "", + DiffID: "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + CreatedBy: "COPY file:842584685f26edb24dc305d76894f51cfda2bad0c24a05e727f9d4905d184a70 in /php-app/composer.lock ", + OpaqueDirs: []string{"php-app/"}, + }, + }, + { + ID: "sha256:60129d309cd4f16d69262106d6074f37c6d37f6c9089a9710ec96ae067716636", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Size: 6656, + Digest: "", + DiffID: "sha256:a4595c43a874856bf95f3bfc4fbf78bbaa04c92c726276d4f64193a47ced0566", + CreatedBy: "COPY file:c6d0373d380252b91829a5bb3c81d5b1afa574c91cef7752d18170a231c31f6d in /ruby-app/Gemfile.lock ", + OpaqueDirs: []string{"ruby-app/"}, + }, + }, + }, + want: artifact.Reference{ + Name: "../../test/testdata/vuln-image.tar.gz", + Type: types.TypeContainerImage, + ID: "sha256:0bebf0773ffd87baa7c64fbdbdf79a24ae125e3f99a8adebe52d1ccbe6bed16b", + BlobIDs: []string{ + "sha256:a83985cade3970577a9af328db9c88c0bf15cad40f7d2cf6d76e83882bc8146d", + "sha256:b109622c2d106193db505762f1f3e78cf0035a69e559caf07c305c92ddb89356", + "sha256:115f689385cb66077c338c52f2c9d6f3018a18c89be7fe7d23f1645422d7d59d", + "sha256:60129d309cd4f16d69262106d6074f37c6d37f6c9089a9710ec96ae067716636", + }, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:58701fd185bda36cab0557bb6438661831267aa4a9e0b54211c4d5317a48aff4", + DiffIDs: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + "sha256:24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + "sha256:a4595c43a874856bf95f3bfc4fbf78bbaa04c92c726276d4f64193a47ced0566", + }, + RepoTags: []string{"vuln-image:latest"}, + RepoDigests: nil, + Reference: testutil.MustParseReference(t, "vuln-image:latest"), + ConfigFile: v1.ConfigFile{ + Architecture: "amd64", + Author: "", + Created: v1.Time{Time: time.Date(2020, 2, 16, 10, 38, 41, 114114788, time.UTC)}, + DockerVersion: "19.03.5", + History: []v1.History{ + { + Author: "Bazel", + Created: v1.Time{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + CreatedBy: "bazel build ...", + Comment: "", + EmptyLayer: false, + }, + { + Author: "Bazel", + Created: v1.Time{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + CreatedBy: "bazel build ...", + Comment: "", + EmptyLayer: false, + }, + { + Created: v1.Time{Time: time.Date(2020, 2, 16, 10, 38, 40, 976530082, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY file:842584685f26edb24dc305d76894f51cfda2bad0c24a05e727f9d4905d184a70 in /php-app/composer.lock ", + Comment: "", + EmptyLayer: false, + }, + { + Created: v1.Time{Time: time.Date(2020, 2, 16, 10, 38, 41, 114114788, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY file:c6d0373d380252b91829a5bb3c81d5b1afa574c91cef7752d18170a231c31f6d in /ruby-app/Gemfile.lock ", + Comment: "", + EmptyLayer: false, + }, + }, + OS: "linux", + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + }, + { + Algorithm: "sha256", + Hex: "dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + }, + { + Algorithm: "sha256", + Hex: "24df0d4e20c0f42d3703bf1f1db2bdd77346c7956f74f423603d651e8e5ae8a7", + }, + { + Algorithm: "sha256", + Hex: "a4595c43a874856bf95f3bfc4fbf78bbaa04c92c726276d4f64193a47ced0566", + }, + }, + }, + Config: v1.Config{ + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt", + }, + Hostname: "", + Image: "sha256:916390dcf84a1c7852e298f24fb5389a6e7801102086924e55eb08cd58d6a741", + }, + }, + }, + }, + }, + { + name: "sad path, MissingBlobs returns an error", + imagePath: "../../test/testdata/alpine-311.tar.gz", + setupCache: func(_ *testing.T) cache.Cache { + return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{ + MissingBlobs: true, + }) + }, + wantErr: "MissingBlobs failed", + }, + { + name: "sad path, PutBlob returns an error", + imagePath: "../../test/testdata/alpine-311.tar.gz", + setupCache: func(_ *testing.T) cache.Cache { + return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{ + PutBlob: true, + }) + }, + wantErr: "PutBlob failed", + }, + { + name: "sad path, PutBlob returns an error with multiple layers", + imagePath: "../../test/testdata/vuln-image.tar.gz", + setupCache: func(_ *testing.T) cache.Cache { + return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{ + PutBlob: true, + }) + }, + wantErr: "PutBlob failed", + }, + { + name: "sad path, PutArtifact returns an error", + imagePath: "../../test/testdata/alpine-311.tar.gz", + setupCache: func(_ *testing.T) cache.Cache { + return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{ + PutArtifact: true, + }) + }, + wantErr: "PutArtifact failed", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cachetest.NewCache(t, tt.setupCache) + + img, err := image.NewArchiveImage(tt.imagePath) + require.NoError(t, err) + + a, err := image2.NewArtifact(img, c, tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + defer a.Clean(got) + + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got) + + cachetest.AssertArtifact(t, c, tt.wantArtifact) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func TestArtifact_InspectWithMaxImageSize(t *testing.T) { + randomImage, err := random.Image(1000, 2, random.WithSource(rand.NewSource(0))) + require.NoError(t, err) + + img := &fakeImage{Image: randomImage} + c := cachetest.NewCache(t, nil) + + tests := []struct { + name string + artifactOpt artifact.Option + wantErr string + }{ + { + name: "compressed image size is larger than the maximum", + artifactOpt: artifact.Option{ + ImageOption: types.ImageOptions{MaxImageSize: units.KB * 1}, + }, + wantErr: "compressed image size 2.44kB exceeds maximum allowed size 1kB", + }, + { + name: "uncompressed layers size is larger than the maximum", + artifactOpt: artifact.Option{ + ImageOption: types.ImageOptions{MaxImageSize: units.KB * 3}, + }, + wantErr: "uncompressed layers size 5.12kB exceeds maximum allowed size 3kB", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + artifact, err := image2.NewArtifact(img, c, tt.artifactOpt) + require.NoError(t, err) + + _, err = artifact.Inspect(t.Context()) + require.ErrorContains(t, err, tt.wantErr) + }) + } +} diff --git a/pkg/fanal/artifact/image/remote_sbom.go b/pkg/fanal/artifact/image/remote_sbom.go new file mode 100644 index 000000000000..63fb014275c5 --- /dev/null +++ b/pkg/fanal/artifact/image/remote_sbom.go @@ -0,0 +1,196 @@ +package image + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/samber/lo" + "golang.org/x/xerrors" + + sbomatt "github.com/aquasecurity/trivy/pkg/attestation/sbom" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/sbom" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/oci" + "github.com/aquasecurity/trivy/pkg/remote" + "github.com/aquasecurity/trivy/pkg/types" + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +var errNoSBOMFound = xerrors.New("remote SBOM not found") + +type inspectRemoteSBOM func(context.Context) (artifact.Reference, error) + +func (a Artifact) retrieveRemoteSBOM(ctx context.Context) (artifact.Reference, error) { + for _, sbomSource := range a.artifactOption.SBOMSources { + var inspect inspectRemoteSBOM + switch sbomSource { + case types.SBOMSourceOCI: + inspect = a.inspectOCIReferrerSBOM + case types.SBOMSourceRekor: + inspect = a.inspectRekorSBOMAttestation + default: + // Never reach here as the "--sbom-sources" values are validated beforehand + continue + } + + ref, err := inspect(ctx) + if errors.Is(err, errNoSBOMFound) { + // Try the next SBOM source + a.logger.Debug("No SBOM found in the source", log.String("source", sbomSource)) + continue + } else if err != nil { + return artifact.Reference{}, xerrors.Errorf("SBOM searching error: %w", err) + } + return ref, nil + } + return artifact.Reference{}, errNoSBOMFound +} + +func (a Artifact) inspectOCIReferrerSBOM(ctx context.Context) (artifact.Reference, error) { + digest, err := repoDigest(a.image, a.artifactOption.Insecure) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("repo digest error: %w", err) + } + + // Fetch referrers + index, err := remote.Referrers(ctx, digest, a.artifactOption.ImageOption.RegistryOptions) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("unable to fetch referrers: %w", err) + } + manifest, err := index.IndexManifest() + if err != nil { + return artifact.Reference{}, xerrors.Errorf("unable to get manifest: %w", err) + } + for _, m := range lo.FromPtr(manifest).Manifests { + // Unsupported artifact type + if !slices.Contains(oci.SupportedSBOMArtifactTypes, m.ArtifactType) { + continue + } + res, err := a.parseReferrer(ctx, digest.Context().String(), m) + if err != nil { + a.logger.Warn("Error with SBOM via OCI referrers", + log.String("digest", m.Digest.String()), log.Err(err)) + continue + } + return res, nil + } + return artifact.Reference{}, errNoSBOMFound +} + +func (a Artifact) parseReferrer(ctx context.Context, repo string, desc v1.Descriptor) (artifact.Reference, error) { + const fileName string = "referrer.sbom" + repoName := fmt.Sprintf("%s@%s", repo, desc.Digest) + + tmpDir, err := xos.MkdirTemp("", "sbom-referrer-") + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to create temp directory: %w", err) + } + defer os.RemoveAll(tmpDir) + + // Download SBOM to local filesystem + referrer := oci.NewArtifact(repoName, a.artifactOption.ImageOption.RegistryOptions) + if err = referrer.Download(ctx, tmpDir, oci.DownloadOption{ + MediaType: desc.ArtifactType, + Filename: fileName, + Quiet: true, + }); err != nil { + return artifact.Reference{}, xerrors.Errorf("SBOM download error: %w", err) + } + + filePath := filepath.Join(tmpDir, fileName) + + res, err := a.inspectSBOMFile(ctx, filePath) + if err != nil { + return res, xerrors.Errorf("SBOM error: %w", err) + } + + // Found SBOM + a.logger.Info("Found SBOM in the OCI referrers", log.String("type", string(res.Type))) + + return res, nil +} + +func (a Artifact) inspectRekorSBOMAttestation(ctx context.Context) (artifact.Reference, error) { + digest, err := repoDigest(a.image, a.artifactOption.Insecure) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("repo digest error: %w", err) + } + + client, err := sbomatt.NewRekor(a.artifactOption.RekorURL) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to create rekor client: %w", err) + } + + raw, err := client.RetrieveSBOM(ctx, digest.DigestStr()) + if errors.Is(err, sbomatt.ErrNoSBOMAttestation) { + return artifact.Reference{}, errNoSBOMFound + } else if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to retrieve SBOM attestation: %w", err) + } + + f, err := xos.CreateTemp("", "sbom-attestation-") + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to create a temporary file: %w", err) + } + defer os.Remove(f.Name()) + + if _, err = f.Write(raw); err != nil { + return artifact.Reference{}, xerrors.Errorf("copy error: %w", err) + } + if err = f.Close(); err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to close %s: %w", f.Name(), err) + } + res, err := a.inspectSBOMFile(ctx, f.Name()) + if err != nil { + return res, xerrors.Errorf("SBOM error: %w", err) + } + + // Found SBOM + a.logger.Info("Found SBOM in Rekor", log.String("type", string(res.Type)), + log.String("url", a.artifactOption.RekorURL)) + + return res, nil +} + +func (a Artifact) inspectSBOMFile(ctx context.Context, filePath string) (artifact.Reference, error) { + ar, err := sbom.NewArtifact(filePath, a.cache, a.artifactOption) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to new artifact: %w", err) + } + + results, err := ar.Inspect(ctx) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to inspect: %w", err) + } + results.Name = a.image.Name() + + return results, nil +} + +func repoDigest(img ftypes.Image, insecure bool) (name.Digest, error) { + repoNameFull := img.Name() + ref, err := name.ParseReference(repoNameFull) + if err != nil { + return name.Digest{}, xerrors.Errorf("image name parse error: %w", err) + } + + for _, rd := range img.RepoDigests() { + opts := lo.Ternary(insecure, []name.Option{name.Insecure}, nil) + digest, err := name.NewDigest(rd, opts...) + if err != nil { + continue + } + if ref.Context().String() == digest.Context().String() { + return digest, nil + } + } + return name.Digest{}, xerrors.Errorf("no repo digest found: %w", errNoSBOMFound) +} diff --git a/pkg/fanal/artifact/image/remote_sbom_test.go b/pkg/fanal/artifact/image/remote_sbom_test.go new file mode 100644 index 000000000000..181a121b31aa --- /dev/null +++ b/pkg/fanal/artifact/image/remote_sbom_test.go @@ -0,0 +1,321 @@ +package image_test + +import ( + "fmt" + "net/url" + "os" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + fakei "github.com/google/go-containerregistry/pkg/v1/fake" + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/cachetest" + "github.com/aquasecurity/trivy/internal/registrytest" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/oci" + "github.com/aquasecurity/trivy/pkg/rekortest" + "github.com/aquasecurity/trivy/pkg/sbom" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, true) + os.Exit(m.Run()) +} + +type fakeImage struct { + name string + repoDigests []string + v1.Image + ftypes.ImageExtension +} + +func (f fakeImage) ID() (string, error) { + return "", nil +} + +func (f fakeImage) Name() string { + return f.name +} + +func (f fakeImage) RepoDigests() []string { + return f.repoDigests +} + +func (f fakeImage) RepoTags() []string { + return nil +} + +func TestArtifact_InspectRekorAttestation(t *testing.T) { + tests := []struct { + name string + imageName string + repoDigests []string + wantBlobs []cachetest.WantBlob + want artifact.Reference + wantErr string + }{ + { + name: "happy path", + imageName: "test/image:10", + repoDigests: []string{ + "test/image@sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da", + BlobInfo: ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.16.2", + }, + PackageInfos: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "musl@1.2.3-r0", + Name: "musl", + Version: "1.2.3-r0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.2.3-r0", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.16.2", + }, + }, + }, + BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2", + }, + SrcName: "musl", + SrcVersion: "1.2.3-r0", + Licenses: []string{"MIT"}, + Layer: ftypes.Layer{ + DiffID: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "test/image:10", + Type: ftypes.TypeCycloneDX, + ID: "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da", + BlobIDs: []string{ + "sha256:066b9998617ffb7dfe0a3219ac5c3efc1008a6223606fcf474e7d5c965e4e8da", + }, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5", + DiffIDs: []string{ + "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7", + }, + RepoTags: []string{ + "alpine:3.16", + }, + RepoDigests: []string{ + "alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad", + }, + }, + }, + }, + { + name: "attestation not found", + imageName: "test/image:10", + repoDigests: []string{ + "test/image@sha256:123456e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02", + }, + wantErr: "remote SBOM fetching error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := rekortest.NewServer(t) + defer ts.Close() + + c := cachetest.NewCache(t, nil) + + fi := &fakei.FakeImage{} + fi.ConfigFileReturns(&v1.ConfigFile{}, nil) + + img := &fakeImage{ + name: tt.imageName, + repoDigests: tt.repoDigests, + Image: fi, + } + a, err := image2.NewArtifact(img, c, artifact.Option{ + SBOMSources: []string{"rekor"}, + RekorURL: ts.URL(), + }) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + defer a.Clean(got) + + got.BOM = nil + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +// Common test data for CycloneDX SBOM (used by OCI referrer tests) +var wantBlobsCycloneDX = []cachetest.WantBlob{ + { + ID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d", + BlobInfo: ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Applications: []ftypes.Application{ + { + Type: ftypes.GoBinary, + Packages: ftypes.Packages{ + { + ID: "github.com/opencontainers/go-digest@v1.0.0", + Name: "github.com/opencontainers/go-digest", + Version: "v1.0.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/opencontainers", + Name: "go-digest", + Version: "v1.0.0", + }, + BOMRef: "pkg:golang/github.com/opencontainers/go-digest@v1.0.0", + }, + }, + { + ID: "golang.org/x/sync@v0.1.0", + Name: "golang.org/x/sync", + Version: "v0.1.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "golang.org/x", + Name: "sync", + Version: "v0.1.0", + }, + BOMRef: "pkg:golang/golang.org/x/sync@v0.1.0", + }, + }, + }, + }, + }, + }, + }, +} + +func TestArtifact_InspectOCIReferrerSBOM(t *testing.T) { + // Start a test registry with referrers support + ts := registrytest.NewServer(t) + defer ts.Close() + + u, err := url.Parse(ts.URL) + require.NoError(t, err) + registryHost := u.Host + + tests := []struct { + name string + setup func(t *testing.T) (imageName string, repoDigests []string) + wantType ftypes.ArtifactType + wantID string + wantBlobs []cachetest.WantBlob + }{ + { + name: "CycloneDX SBOM", + setup: func(t *testing.T) (string, []string) { + ref, subjectDesc := registrytest.PushRandomImage(t, registryHost, "test/cyclonedx", "latest") + + sbomContent, err := os.ReadFile("testdata/cyclonedx.json") + require.NoError(t, err) + + registrytest.PushReferrer(t, registryHost, "test/cyclonedx", subjectDesc, oci.CycloneDXArtifactType, sbomContent) + + return ref.String(), + []string{fmt.Sprintf("%s/test/cyclonedx@%s", registryHost, subjectDesc.Digest.String())} + }, + wantType: ftypes.TypeCycloneDX, + wantID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d", + wantBlobs: wantBlobsCycloneDX, + }, + { + name: "Sigstore bundle", + setup: func(t *testing.T) (string, []string) { + ref, subjectDesc := registrytest.PushRandomImage(t, registryHost, "test/sigstore", "latest") + + bundleContent, err := os.ReadFile("testdata/sigstore-bundle.json") + require.NoError(t, err) + + registrytest.PushReferrer(t, registryHost, "test/sigstore", subjectDesc, sbom.SigstoreBundleMediaType, bundleContent) + + return ref.String(), + []string{fmt.Sprintf("%s/test/sigstore@%s", registryHost, subjectDesc.Digest.String())} + }, + wantType: ftypes.TypeCycloneDX, + wantID: "sha256:2171d8ccf798e94d09aca9c6abf15d28abd3236def1caa4a394b6f0a69c4266d", + wantBlobs: wantBlobsCycloneDX, + }, + { + name: "no referrers", + setup: func(t *testing.T) (string, []string) { + // Push image without any referrers + ref, subjectDesc := registrytest.PushRandomImage(t, registryHost, "test/no-referrers", "latest") + + return ref.String(), + []string{fmt.Sprintf("%s/test/no-referrers@%s", registryHost, subjectDesc.Digest.String())} + }, + // Falls back to normal image scanning when no referrers found + wantType: ftypes.TypeContainerImage, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + imageName, repoDigests := tt.setup(t) + + c := cachetest.NewCache(t, nil) + + fi := &fakei.FakeImage{} + fi.ConfigFileReturns(&v1.ConfigFile{}, nil) + + img := &fakeImage{ + name: imageName, + repoDigests: repoDigests, + Image: fi, + } + a, err := image2.NewArtifact(img, c, artifact.Option{ + SBOMSources: []string{"oci"}, + }) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + require.NoError(t, err) + defer a.Clean(got) + + assert.Equal(t, tt.wantType, got.Type) + if tt.wantID != "" { + assert.Equal(t, tt.wantID, got.ID) + } + if tt.wantBlobs != nil { + cachetest.AssertBlobs(t, c, tt.wantBlobs) + } + }) + } +} diff --git a/pkg/fanal/artifact/image/testdata/cyclonedx.json b/pkg/fanal/artifact/image/testdata/cyclonedx.json new file mode 100644 index 000000000000..dc6165ec226f --- /dev/null +++ b/pkg/fanal/artifact/image/testdata/cyclonedx.json @@ -0,0 +1,72 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "serialNumber": "urn:uuid:2e3c684c-1f99-466c-ad6b-b783942eb9d1", + "version": 1, + "metadata": { + "timestamp": "2023-03-22T11:35:38+00:00", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "0.39.0" + } + ], + "component": { + "bom-ref": "pkg:oci/demo-referrers-2023@sha256:e76a13475c6b4a713a0e4a7a8574ce450274d340357a2c40b8221cfcfedf8b19?repository_url=localhost:5001%2Fdemo-referrers-2023\u0026arch=arm64", + "type": "container", + "name": "localhost:5001/demo-referrers-2023:app", + "purl": "pkg:oci/demo-referrers-2023@sha256:e76a13475c6b4a713a0e4a7a8574ce450274d340357a2c40b8221cfcfedf8b19?repository_url=localhost:5001%2Fdemo-referrers-2023\u0026arch=arm64", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:golang/github.com/opencontainers/go-digest@v1.0.0", + "type": "library", + "name": "github.com/opencontainers/go-digest", + "version": "v1.0.0", + "purl": "pkg:golang/github.com/opencontainers/go-digest@v1.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/golang.org/x/sync@v0.1.0", + "type": "library", + "name": "golang.org/x/sync", + "version": "v0.1.0", + "purl": "pkg:golang/golang.org/x/sync@v0.1.0", + "properties": [ + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + } + ], + "dependencies": [ + { + "ref": "c6309d4a-f6db-4acc-8e44-f0d271492b65", + "dependsOn": [ + "pkg:golang/github.com/opencontainers/go-digest@v1.0.0", + "pkg:golang/golang.org/x/sync@v0.1.0" + ] + }, + { + "ref": "pkg:oci/demo-referrers-2023@sha256:e76a13475c6b4a713a0e4a7a8574ce450274d340357a2c40b8221cfcfedf8b19?repository_url=localhost:5001%2Fdemo-referrers-2023\u0026arch=arm64", + "dependsOn": [ + "c6309d4a-f6db-4acc-8e44-f0d271492b65" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/fanal/artifact/image/testdata/sigstore-bundle.json b/pkg/fanal/artifact/image/testdata/sigstore-bundle.json new file mode 100644 index 000000000000..ef881bebce70 --- /dev/null +++ b/pkg/fanal/artifact/image/testdata/sigstore-bundle.json @@ -0,0 +1,59 @@ +{ + "dsseEnvelope": { + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6ImdoY3IuaW8vcmluZ29kZXYvZGVtby1zYm9tIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImVhOWM2NTdjYTQ0MzcxZmFkZDAyNzUxMGYyYTFhNjhiNzk5MDAzMjk0YmY2YTQ4Mjc4Mjc5NzFjYzQzOTBhZDQifX1dLCJwcmVkaWNhdGUiOnsiYm9tRm9ybWF0IjoiQ3ljbG9uZURYIiwiY29tcG9uZW50cyI6W3siYm9tLXJlZiI6InBrZzpnb2xhbmcvZ2l0aHViLmNvbS9vcGVuY29udGFpbmVycy9nby1kaWdlc3RAdjEuMC4wIiwibmFtZSI6ImdpdGh1Yi5jb20vb3BlbmNvbnRhaW5lcnMvZ28tZGlnZXN0IiwicHJvcGVydGllcyI6W3sibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpQa2dUeXBlIiwidmFsdWUiOiJnb2JpbmFyeSJ9XSwicHVybCI6InBrZzpnb2xhbmcvZ2l0aHViLmNvbS9vcGVuY29udGFpbmVycy9nby1kaWdlc3RAdjEuMC4wIiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoidjEuMC4wIn0seyJib20tcmVmIjoicGtnOmdvbGFuZy9nb2xhbmcub3JnL3gvc3luY0B2MC4xLjAiLCJuYW1lIjoiZ29sYW5nLm9yZy94L3N5bmMiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlBrZ1R5cGUiLCJ2YWx1ZSI6ImdvYmluYXJ5In1dLCJwdXJsIjoicGtnOmdvbGFuZy9nb2xhbmcub3JnL3gvc3luY0B2MC4xLjAiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiJ2MC4xLjAifV0sImRlcGVuZGVuY2llcyI6W3siZGVwZW5kc09uIjpbInBrZzpnb2xhbmcvZ2l0aHViLmNvbS9vcGVuY29udGFpbmVycy9nby1kaWdlc3RAdjEuMC4wIiwicGtnOmdvbGFuZy9nb2xhbmcub3JnL3gvc3luY0B2MC4xLjAiXSwicmVmIjoiYzYzMDlkNGEtZjZkYi00YWNjLThlNDQtZjBkMjcxNDkyYjY1In0seyJkZXBlbmRzT24iOlsiYzYzMDlkNGEtZjZkYi00YWNjLThlNDQtZjBkMjcxNDkyYjY1Il0sInJlZiI6InBrZzpvY2kvZGVtby1yZWZlcnJlcnMtMjAyM0BzaGEyNTY6ZTc2YTEzNDc1YzZiNGE3MTNhMGU0YTdhODU3NGNlNDUwMjc0ZDM0MDM1N2EyYzQwYjgyMjFjZmNmZWRmOGIxOT9yZXBvc2l0b3J5X3VybD1sb2NhbGhvc3Q6NTAwMSUyRmRlbW8tcmVmZXJyZXJzLTIwMjNcdTAwMjZhcmNoPWFybTY0In1dLCJtZXRhZGF0YSI6eyJjb21wb25lbnQiOnsiYm9tLXJlZiI6InBrZzpvY2kvZGVtby1yZWZlcnJlcnMtMjAyM0BzaGEyNTY6ZTc2YTEzNDc1YzZiNGE3MTNhMGU0YTdhODU3NGNlNDUwMjc0ZDM0MDM1N2EyYzQwYjgyMjFjZmNmZWRmOGIxOT9yZXBvc2l0b3J5X3VybD1sb2NhbGhvc3Q6NTAwMSUyRmRlbW8tcmVmZXJyZXJzLTIwMjNcdTAwMjZhcmNoPWFybTY0IiwibmFtZSI6ImxvY2FsaG9zdDo1MDAxL2RlbW8tcmVmZXJyZXJzLTIwMjM6YXBwIiwicHJvcGVydGllcyI6W3sibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpTY2hlbWFWZXJzaW9uIiwidmFsdWUiOiIyIn1dLCJwdXJsIjoicGtnOm9jaS9kZW1vLXJlZmVycmVycy0yMDIzQHNoYTI1NjplNzZhMTM0NzVjNmI0YTcxM2EwZTRhN2E4NTc0Y2U0NTAyNzRkMzQwMzU3YTJjNDBiODIyMWNmY2ZlZGY4YjE5P3JlcG9zaXRvcnlfdXJsPWxvY2FsaG9zdDo1MDAxJTJGZGVtby1yZWZlcnJlcnMtMjAyM1x1MDAyNmFyY2g9YXJtNjQiLCJ0eXBlIjoiY29udGFpbmVyIn0sInRpbWVzdGFtcCI6IjIwMjMtMDMtMjJUMTE6MzU6MzgrMDA6MDAiLCJ0b29scyI6W3sibmFtZSI6InRyaXZ5IiwidmVuZG9yIjoiYXF1YXNlY3VyaXR5IiwidmVyc2lvbiI6IjAuMzkuMCJ9XX0sInNlcmlhbE51bWJlciI6InVybjp1dWlkOjJlM2M2ODRjLTFmOTktNDY2Yy1hZDZiLWI3ODM5NDJlYjlkMSIsInNwZWNWZXJzaW9uIjoiMS40IiwidmVyc2lvbiI6MSwidnVsbmVyYWJpbGl0aWVzIjpbXX19", + "payloadType": "application/vnd.in-toto+json", + "signatures": [ + { + "sig": "MEUCIQCixpYXidhSSGs+WhNVjF6eHiTj6eo0KxF8ihhHlyoHKgIgFv/p/hIhoS/yNTo4Xb/+Ac3fTfd1xNcGB+IJqTNyP/I=" + } + ] + }, + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "verificationMaterial": { + "certificate": { + "rawBytes": "MIIC2zCCAmGgAwIBAgIUC8iVf2cW24GNi2YlrpI5xMh/3TMwCgYIKoZIzj0EAwMwNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRlcm1lZGlhdGUwHhcNMjUwOTIzMDUwMzQyWhcNMjUwOTIzMDUxMzQyWjAAMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAED4Q9U5ucpN780p9nUCiupZSrMpTVQW04/S+AFZVEKkBi3vndyRRXmogyQBDmpFO+ST8NZDBEJCKhEvfg/ZWKZaOCAYAwggF8MA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUtHnkcI9f9YhXO7sRWkqqnKWaMBYwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4YZD8wKwYDVR0RAQH/BCEwH4EddGhvbWFzLmdyaW5pbmdlckByaW5nb2Rldi5jb20wLAYKKwYBBAGDvzABAQQeaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMC4GCisGAQQBg78wAQgEIAweaHR0cHM6Ly9naXRodWIuY29tL2xvZ2luL29hdXRoMIGJBgorBgEEAdZ5AgQCBHsEeQB3AHUA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGZdPTFfQAABAMARjBEAiAZakMhKO+tfDvTmISGjfgGaO+Ehj6u1hQPSTBQGpoSPgIgAZeiL6xH76MTSWoYww3EUheSgOwoKIYTMaNh57PqmEAwCgYIKoZIzj0EAwMDaAAwZQIwQBdf+r3InPKcMDBwMjZkdMarevw4IfC0PYGKBf3u0/Y0RK4cyDAuCS4y81is2WoGAjEAnVymG/CTeF4Ff/jqxqIWaMCoKFiJ85NsNi4+NJ7m5qr/UT1LTeuly075bSGHH9zj" + }, + "tlogEntries": [ + { + "canonicalizedBody": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiZHNzZSIsInNwZWMiOnsiZW52ZWxvcGVIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZjYxZDkzMjkzMWVjNDliMTNiNTBjODgzZGQ3OTEzMzM3ZmMzZDQ2ZTdiYmI0YTczNDZiYTM4YmJmZmY1MjA1ZiJ9LCJwYXlsb2FkSGFzaCI6eyJhbGdvcml0aG0iOiJzaGEyNTYiLCJ2YWx1ZSI6IjI4OTBmZmEyYmVlODFjYTg4OWQxM2Y3ZjRjNDJiN2NkODc1YTJiYWM4ZDIyODcyYzcyZjJlMjNmZDJhZTkzNWEifSwic2lnbmF0dXJlcyI6W3sic2lnbmF0dXJlIjoiTUVVQ0lRQ2l4cFlYaWRoU1NHcytXaE5WakY2ZUhpVGo2ZW8wS3hGOGloaEhseW9IS2dJZ0Z2L3AvaElob1MveU5UbzRYYi8rQWMzZlRmZDF4TmNHQitJSnFUTnlQL0k9IiwidmVyaWZpZXIiOiJMUzB0TFMxQ1JVZEpUaUJEUlZKVVNVWkpRMEZVUlMwdExTMHRDazFKU1VNeWVrTkRRVzFIWjBGM1NVSkJaMGxWUXpocFZtWXlZMWN5TkVkT2FUSlpiSEp3U1RWNFRXZ3ZNMVJOZDBObldVbExiMXBKZW1vd1JVRjNUWGNLVG5wRlZrMUNUVWRCTVZWRlEyaE5UV015Ykc1ak0xSjJZMjFWZFZwSFZqSk5ValIzU0VGWlJGWlJVVVJGZUZaNllWZGtlbVJIT1hsYVV6RndZbTVTYkFwamJURnNXa2RzYUdSSFZYZElhR05PVFdwVmQwOVVTWHBOUkZWM1RYcFJlVmRvWTA1TmFsVjNUMVJKZWsxRVZYaE5lbEY1VjJwQlFVMUdhM2RGZDFsSUNrdHZXa2w2YWpCRFFWRlpTVXR2V2tsNmFqQkVRVkZqUkZGblFVVkVORkU1VlRWMVkzQk9Oemd3Y0RsdVZVTnBkWEJhVTNKTmNGUldVVmN3TkM5VEswRUtSbHBXUlV0clFta3pkbTVrZVZKU1dHMXZaM2xSUWtSdGNFWlBLMU5VT0U1YVJFSkZTa05MYUVWMlptY3ZXbGRMV21GUFEwRlpRWGRuWjBZNFRVRTBSd3BCTVZWa1JIZEZRaTkzVVVWQmQwbElaMFJCVkVKblRsWklVMVZGUkVSQlMwSm5aM0pDWjBWR1FsRmpSRUY2UVdSQ1owNVdTRkUwUlVablVWVjBTRzVyQ21OSk9XWTVXV2hZVHpkelVsZHJjWEZ1UzFkaFRVSlpkMGgzV1VSV1VqQnFRa0puZDBadlFWVXpPVkJ3ZWpGWmEwVmFZalZ4VG1wd1MwWlhhWGhwTkZrS1drUTRkMHQzV1VSV1VqQlNRVkZJTDBKRFJYZElORVZrWkVkb2RtSlhSbnBNYldSNVlWYzFjR0p0Wkd4amEwSjVZVmMxYm1JeVVteGthVFZxWWpJd2R3cE1RVmxMUzNkWlFrSkJSMFIyZWtGQ1FWRlJaV0ZJVWpCalNFMDJUSGs1Ym1GWVVtOWtWMGwxV1RJNWRFd3llSFphTW14MVRESTVhR1JZVW05TlF6UkhDa05wYzBkQlVWRkNaemM0ZDBGUlowVkpRWGRsWVVoU01HTklUVFpNZVRsdVlWaFNiMlJYU1hWWk1qbDBUREo0ZGxveWJIVk1NamxvWkZoU2IwMUpSMG9LUW1kdmNrSm5SVVZCWkZvMVFXZFJRMEpJYzBWbFVVSXpRVWhWUVROVU1IZGhjMkpJUlZSS2FrZFNOR050VjJNelFYRktTMWh5YW1WUVN6TXZhRFJ3ZVFwblF6aHdOMjgwUVVGQlIxcGtVRlJHWmxGQlFVSkJUVUZTYWtKRlFXbEJXbUZyVFdoTFR5dDBaa1IyVkcxSlUwZHFabWRIWVU4clJXaHFOblV4YUZGUUNsTlVRbEZIY0c5VFVHZEpaMEZhWldsTU5uaElOelpOVkZOWGIxbDNkek5GVldobFUyZFBkMjlMU1ZsVVRXRk9hRFUzVUhGdFJVRjNRMmRaU1V0dldra0tlbW93UlVGM1RVUmhRVUYzV2xGSmQxRkNaR1lyY2pOSmJsQkxZMDFFUW5kTmFscHJaRTFoY21WMmR6Ukpaa013VUZsSFMwSm1NM1V3TDFrd1VrczBZd3A1UkVGMVExTTBlVGd4YVhNeVYyOUhRV3BGUVc1V2VXMUhMME5VWlVZMFJtWXZhbkY0Y1VsWFlVMURiMHRHYVVvNE5VNXpUbWswSzA1S04yMDFjWEl2Q2xWVU1VeFVaWFZzZVRBM05XSlRSMGhJT1hwcUNpMHRMUzB0UlU1RUlFTkZVbFJKUmtsRFFWUkZMUzB0TFMwSyJ9XX19", + "inclusionPromise": { + "signedEntryTimestamp": "MEQCHwyPMnKpy/yUQ0ZwR9gK4a2uckTuTUdLxu4evTPuVd4CIQCzhKnq3MeJbnQ+UKne4pLB+zkvN+e/Feb5EPc8WnyueQ==" + }, + "inclusionProof": { + "checkpoint": { + "envelope": "rekor.sigstore.dev - 1193050959916656506\n427690837\nsJPSNHZNtTPycokyg+lqN2e6YlRLlJ57qNvKbfgu4DA=\n\n— rekor.sigstore.dev wNI9ajBFAiEAsJNoQ9+KC5oVw5F8m6TJsMWRICYM/b5GtvdlEUzfKewCIEZ4yKxVTQnS8fOeS1wdxrENfB+ExH45cYvvUElK6lyZ\n" + }, + "hashes": [ + "LiSPCqC8Ls8vziACm+javgVtDbCunAnG231n36f+rmA=", + "SOInOiumiLsg+YEerz/uDUJxNsOKe+RoCBn66/8CAx0=", + "L7DlG4/e8uoLsciiN10/PQ9Dp11pI20zjdqLZkL+qes=", + "LF7kYuNdnJjUcOI8W65buH30EMIADhu/Kxl5u6ZEDxE=", + "jS89+x6mwua8j9KN5Dv6CO55M78GF8hi8oSIr0Bf2nw=", + "QnOvO+8sDYohFzr5WL8YeAUXNvpPvu8uJ94IvrDkCW0=", + "4ctDWxRuy9Mz4TnkTNS+M6IbMvBeMJ4nOhlt6OKpRjE=", + "CNxxnyEXbd2FPeCFwuWruKKVDmiib5NLbmERibRvCQ0=", + "hfVaV9xEkKbkj2E2/qTT82VHIma+u9NCAFqqsRiinIw=", + "2ZZMmiqKJ0oGmxZHgx7TcGakJLUPNXVLRdD2U48OC7g=", + "E7mN474KbNy0VYJOJx8PWikuCKMn+miLKMG6HAtUY6o=", + "fg+F12WPA6GgbJ0+XKaR8IkjWVPhNcjTcpgtCjk0a9A=", + "rO8wDSOjmY8VkspFqYaJS4TV5HxywICMlHM8gTxXkAA=", + "1mfy94KpcItqshH9+gwqV6jccupcaMpVsF28New8zDY=", + "vS7O4ozHIQZJWBiov+mkpI27GE8zAmVCEkRcP3NDyNE=" + ], + "logIndex": "427690836", + "rootHash": "sJPSNHZNtTPycokyg+lqN2e6YlRLlJ57qNvKbfgu4DA=", + "treeSize": "427690837" + }, + "integratedTime": "1758603823", + "kindVersion": { + "kind": "dsse", + "version": "0.0.1" + }, + "logId": { + "keyId": "wNI9atQGlz+VWfO6LRygH4QUfY/8W4RFwiT5i5WRgB0=" + }, + "logIndex": "549595098" + } + ] + } +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/fs.go b/pkg/fanal/artifact/local/fs.go new file mode 100644 index 000000000000..5ea145c9d480 --- /dev/null +++ b/pkg/fanal/artifact/local/fs.go @@ -0,0 +1,378 @@ +package local + +import ( + "cmp" + "context" + "crypto/sha256" + "errors" + "io/fs" + "net/url" + "os" + "path" + "path/filepath" + "strings" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/samber/lo" + "golang.org/x/sync/errgroup" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/semaphore" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + "github.com/aquasecurity/trivy/pkg/uuid" +) + +const artifactVersion = 0 + +var _ Walker = (*walker.FS)(nil) + +type Walker interface { + Walk(root string, opt walker.Option, fn walker.WalkFunc) error +} + +type Artifact struct { + rootPath string + logger *log.Logger + cache cache.ArtifactCache + walker Walker + analyzer analyzer.AnalyzerGroup + handlerManager handler.Manager + + artifactOption artifact.Option + + isClean bool // whether git repository is clean (for caching) + repoMetadata artifact.RepoMetadata // git repository metadata +} + +func NewArtifact(rootPath string, c cache.ArtifactCache, w Walker, opt artifact.Option) (artifact.Artifact, error) { + handlerManager, err := handler.NewManager(opt) + if err != nil { + return nil, xerrors.Errorf("handler initialize error: %w", err) + } + + a, err := analyzer.NewAnalyzerGroup(opt.AnalyzerOptions()) + if err != nil { + return nil, xerrors.Errorf("analyzer group error: %w", err) + } + + opt.Type = cmp.Or(opt.Type, types.TypeFilesystem) + prefix := lo.Ternary(opt.Type == types.TypeRepository, "repo", "fs") + + art := Artifact{ + rootPath: filepath.ToSlash(filepath.Clean(rootPath)), + logger: log.WithPrefix(prefix), + cache: c, + walker: w, + analyzer: a, + handlerManager: handlerManager, + artifactOption: opt, + } + + art.logger.Debug("Analyzing...", log.String("root", art.rootPath), + lo.Ternary(opt.Original != "", log.String("original", opt.Original), log.Nil)) + + // Check if the directory is a git repository and extract metadata + if art.isClean, art.repoMetadata, err = extractGitInfo(art.rootPath); err == nil { + // If git info is detected, change artifact type to repository + art.artifactOption.Type = types.TypeRepository + if art.isClean { + art.logger.Debug("Using the latest commit hash for calculating cache key", + log.String("commit_hash", art.repoMetadata.Commit)) + } else { + art.logger.Debug("Repository is dirty, random cache key will be used") + } + } else if !errors.Is(err, git.ErrRepositoryNotExists) { + // Only log if the file path is a git repository + art.logger.Debug("Random cache key will be used", log.Err(err)) + } + + return art, nil +} + +// extractGitInfo extracts git repository information including clean status and metadata +// Returns clean status (for caching), metadata, and error +func extractGitInfo(dir string) (bool, artifact.RepoMetadata, error) { + var metadata artifact.RepoMetadata + + repo, err := git.PlainOpen(dir) + if err != nil { + return false, metadata, xerrors.Errorf("failed to open git repository: %w", err) + } + + // Get HEAD commit + head, err := repo.Head() + if err != nil { + return false, metadata, xerrors.Errorf("failed to get HEAD: %w", err) + } + + commit, err := repo.CommitObject(head.Hash()) + if err != nil { + return false, metadata, xerrors.Errorf("failed to get commit object: %w", err) + } + + // Extract basic commit metadata + metadata.Commit = head.Hash().String() + metadata.CommitMsg = strings.TrimSpace(commit.Message) + metadata.Author = commit.Author.String() + metadata.Committer = commit.Committer.String() + + // Get branch name + if head.Name().IsBranch() { + metadata.Branch = head.Name().Short() + } + + // Get all tag names that point to HEAD + if tags, err := repo.Tags(); err == nil { + var headTags []string + _ = tags.ForEach(func(tag *plumbing.Reference) error { + if tag.Hash() == head.Hash() { + headTags = append(headTags, tag.Name().Short()) + } + return nil + }) + metadata.Tags = headTags + } + + // Get repository URL - prefer upstream, fallback to origin + remoteConfig, err := repo.Remote("upstream") + if err != nil { + remoteConfig, err = repo.Remote("origin") + } + if err == nil && len(remoteConfig.Config().URLs) > 0 { + metadata.RepoURL = sanitizeRemoteURL(remoteConfig.Config().URLs[0]) + } + + // Check if repository is clean for caching purposes + worktree, err := repo.Worktree() + if err != nil { + return false, metadata, xerrors.Errorf("failed to get worktree: %w", err) + } + + status, err := worktree.Status() + if err != nil { + return false, metadata, xerrors.Errorf("failed to get status: %w", err) + } + + // Return clean status and metadata + return status.IsClean(), metadata, nil +} + +func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) { + // Calculate cache key + cacheKey, err := a.calcCacheKey() + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to calculate a cache key: %w", err) + } + + // Check if the cache exists only when it's a clean git repository + if a.isClean && a.repoMetadata.Commit != "" { + _, missingBlobs, err := a.cache.MissingBlobs(ctx, cacheKey, []string{cacheKey}) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("unable to get missing blob: %w", err) + } + + if len(missingBlobs) == 0 { + // Cache hit + a.logger.DebugContext(ctx, "Cache hit", log.String("key", cacheKey)) + return artifact.Reference{ + Name: cmp.Or(a.artifactOption.Original, a.rootPath), + Type: a.artifactOption.Type, + ID: cacheKey, + BlobIDs: []string{cacheKey}, + RepoMetadata: a.repoMetadata, + }, nil + } + } + + // `errgroup` cancels the context after Wait returns, so it can’t be use later. + // We need a separate context specifically for Analyze. + eg, egCtx := errgroup.WithContext(ctx) + result := analyzer.NewAnalysisResult() + limit := semaphore.New(a.artifactOption.Parallel) + opts := analyzer.AnalysisOptions{ + Offline: a.artifactOption.Offline, + FileChecksum: a.artifactOption.FileChecksum, + } + + // Prepare filesystem for post analysis + composite, err := a.analyzer.PostAnalyzerFS() + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to prepare filesystem for post analysis: %w", err) + } + defer composite.Cleanup() + + // Use static paths instead of traversing the filesystem when all analyzers implement StaticPathAnalyzer + // so that we can analyze files faster + if paths, canUseStaticPaths := a.analyzer.StaticPaths(a.artifactOption.DisabledAnalyzers); canUseStaticPaths { + // Analyze files in static paths + a.logger.Debug("Analyzing files in static paths") + if err = a.analyzeWithStaticPaths(egCtx, eg, limit, result, composite, opts, paths); err != nil { + return artifact.Reference{}, xerrors.Errorf("analyze with static paths: %w", err) + } + } else { + // Analyze files by traversing the root directory + if err = a.analyzeWithRootDir(egCtx, eg, limit, result, composite, opts); err != nil { + return artifact.Reference{}, xerrors.Errorf("analyze with traversal: %w", err) + } + } + + // Wait for all the goroutine to finish. + if err = eg.Wait(); err != nil { + return artifact.Reference{}, xerrors.Errorf("analyze error: %w", err) + } + + // Post-analysis + if err = a.analyzer.PostAnalyze(ctx, composite, result, opts); err != nil { + return artifact.Reference{}, xerrors.Errorf("post analysis error: %w", err) + } + + // Sort the analysis result for consistent results + result.Sort() + + blobInfo := types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: result.OS, + Repository: result.Repository, + PackageInfos: result.PackageInfos, + Applications: result.Applications, + Misconfigurations: result.Misconfigurations, + Secrets: result.Secrets, + Licenses: result.Licenses, + CustomResources: result.CustomResources, + + // For Red Hat + BuildInfo: result.BuildInfo, + } + + if err = a.handlerManager.PostHandle(ctx, result, &blobInfo); err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to call hooks: %w", err) + } + + if err = a.cache.PutBlob(ctx, cacheKey, blobInfo); err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to store blob (%s) in cache: %w", cacheKey, err) + } + + // get hostname + var hostName string + b, err := os.ReadFile(filepath.Join(a.rootPath, "etc", "hostname")) + if err == nil && len(b) != 0 { + hostName = strings.TrimSpace(string(b)) + } else { + target := cmp.Or(a.artifactOption.Original, a.rootPath) + hostName = filepath.ToSlash(target) // To slash for Windows + } + + return artifact.Reference{ + Name: hostName, + Type: a.artifactOption.Type, + ID: cacheKey, // use a cache key as pseudo artifact ID + BlobIDs: []string{cacheKey}, + RepoMetadata: a.repoMetadata, + }, nil +} + +func (a Artifact) analyzeWithRootDir(ctx context.Context, eg *errgroup.Group, limit *semaphore.Weighted, + result *analyzer.AnalysisResult, composite *analyzer.CompositeFS, opts analyzer.AnalysisOptions) error { + + root := a.rootPath + relativePath := "" + + // When the root path is a file, rewrite the root path and relative path + if fsutils.FileExists(a.rootPath) { + root, relativePath = path.Split(a.rootPath) + } + return a.analyzeWithTraversal(ctx, root, relativePath, eg, limit, result, composite, opts) +} + +// analyzeWithStaticPaths analyzes files using static paths from analyzers +func (a Artifact) analyzeWithStaticPaths(ctx context.Context, eg *errgroup.Group, limit *semaphore.Weighted, + result *analyzer.AnalysisResult, composite *analyzer.CompositeFS, opts analyzer.AnalysisOptions, + staticPaths []string) error { + + // Process each static path + for _, relativePath := range staticPaths { + if err := a.analyzeWithTraversal(ctx, a.rootPath, relativePath, eg, limit, result, composite, opts); errors.Is(err, fs.ErrNotExist) { + continue + } else if err != nil { + return xerrors.Errorf("analyze with traversal: %w", err) + } + } + + return nil +} + +// analyzeWithTraversal analyzes files by traversing the entire filesystem +func (a Artifact) analyzeWithTraversal(ctx context.Context, root, relativePath string, eg *errgroup.Group, limit *semaphore.Weighted, + result *analyzer.AnalysisResult, composite *analyzer.CompositeFS, opts analyzer.AnalysisOptions) error { + + return a.walker.Walk(filepath.Join(root, relativePath), a.artifactOption.WalkerOption, func(filePath string, info os.FileInfo, opener analyzer.Opener) error { + filePath = path.Join(relativePath, filePath) + if err := a.analyzer.AnalyzeFile(ctx, eg, limit, result, root, filePath, info, opener, nil, opts); err != nil { + return xerrors.Errorf("analyze file (%s): %w", filePath, err) + } + + // Skip post analysis if the file is not required + analyzerTypes := a.analyzer.RequiredPostAnalyzers(filePath, info) + if len(analyzerTypes) == 0 { + return nil + } + + // Build filesystem for post analysis + if err := composite.CreateLink(analyzerTypes, root, filePath, filepath.Join(root, filePath)); err != nil { + return xerrors.Errorf("failed to create link: %w", err) + } + + return nil + }) +} + +func (a Artifact) Clean(reference artifact.Reference) error { + // Don't delete cache if it's a clean git repository + if a.isClean && a.repoMetadata.Commit != "" { + return nil + } + return a.cache.DeleteBlobs(context.TODO(), reference.BlobIDs) +} + +func (a Artifact) calcCacheKey() (string, error) { + // If this is a clean git repository, use the commit hash as cache key + if a.isClean && a.repoMetadata.Commit != "" { + return cache.CalcKey(a.repoMetadata.Commit, artifactVersion, a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption) + } + + // For non-git repositories or dirty git repositories, use UUID as cache key + h := sha256.New() + if _, err := h.Write([]byte(uuid.New().String())); err != nil { + return "", xerrors.Errorf("sha256 calculation error: %w", err) + } + + // Format as sha256 digest + d := digest.NewDigest(digest.SHA256, h) + return d.String(), nil +} + +// sanitizeRemoteURL removes credentials (userinfo) from URLs. +func sanitizeRemoteURL(gitUrl string) string { + // Only attempt sanitization for URLs with an explicit scheme. + if !strings.Contains(gitUrl, "://") { + return gitUrl + } + + // Try URL parsing first. + if u, err := url.Parse(gitUrl); err == nil { + // Clear userinfo (username:password) + u.User = nil + gitUrl = u.String() + } + + return gitUrl +} diff --git a/pkg/fanal/artifact/local/fs_test.go b/pkg/fanal/artifact/local/fs_test.go new file mode 100644 index 000000000000..4370f7ca3faa --- /dev/null +++ b/pkg/fanal/artifact/local/fs_test.go @@ -0,0 +1,2665 @@ +package local + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/cachetest" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/misconf" + "github.com/aquasecurity/trivy/pkg/uuid" + + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/all" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/python/pip" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/alpine" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/apk" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret" + _ "github.com/aquasecurity/trivy/pkg/fanal/handler/sysfile" +) + +func TestArtifact_Inspect(t *testing.T) { + type fields struct { + dir string + } + tests := []struct { + name string + fields fields + setupCache func(t *testing.T) cache.Cache + artifactOpt artifact.Option + scannerOpt misconf.ScannerOption + disabledAnalyzers []analyzer.Type + disabledHandlers []types.HandlerType + wantBlobs []cachetest.WantBlob + want artifact.Reference + wantErr string + }{ + { + name: "happy path", + fields: fields{ + dir: "./testdata/alpine", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: types.OS{ + Family: "alpine", + Name: "3.11.6", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: types.Packages{ + { + ID: "musl@1.1.24-r2", + Name: "musl", + Version: "1.1.24-r2", + SrcName: "musl", + SrcVersion: "1.1.24-r2", + Licenses: []string{"MIT"}, + Maintainer: "Timo Teräs ", + Arch: "x86_64", + Digest: "sha1:cb2316a189ebee5282c4a9bd98794cc2477a74c6", + InstalledFiles: []string{ + "lib/libc.musl-x86_64.so.1", + "lib/ld-musl-x86_64.so.1", + }, + AnalyzedBy: analyzer.TypeApk, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "host", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "disable analyzers", + fields: fields{ + dir: "./testdata/alpine", + }, + artifactOpt: artifact.Option{ + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeAlpine, + analyzer.TypeApk, + analyzer.TypePip, + analyzer.TypeNpmPkgLock, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + }, + want: artifact.Reference{ + Name: "host", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "sad path PutBlob returns an error", + fields: fields{ + dir: "./testdata/alpine", + }, + setupCache: func(_ *testing.T) cache.Cache { + return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{ + PutBlob: true, + }) + }, + wantErr: "PutBlob failed", + }, + { + name: "sad path with no such directory", + fields: fields{ + dir: "./testdata/unknown", + }, + wantErr: "walk dir error", + }, + { + name: "happy path with single file", + fields: fields{ + dir: "testdata/requirements.txt", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Applications: []types.Application{ + { + Type: "pip", + FilePath: "requirements.txt", + Packages: types.Packages{ + { + Name: "Flask", + Version: "2.0.0", + Locations: []types.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + AnalyzedBy: analyzer.TypePip, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/requirements.txt", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "happy path with single file using relative path", + fields: fields{ + dir: "./testdata/requirements.txt", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Applications: []types.Application{ + { + Type: "pip", + FilePath: "requirements.txt", + Packages: types.Packages{ + { + Name: "Flask", + Version: "2.0.0", + Locations: []types.Location{ + { + StartLine: 1, + EndLine: 1, + }, + }, + AnalyzedBy: analyzer.TypePip, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/requirements.txt", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "git repository: artifact type is changed to repository", + fields: fields{ + dir: "../../../../internal/gittest/testdata/test-repo", + }, + wantBlobs: []cachetest.WantBlob{ + { + // Cache key is based on commit hash (8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35) + ID: "sha256:d37c788d6fe832712cce9020943746b8764c04f7e323ed4ad68de36c5bf7d846", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + }, + want: artifact.Reference{ + Name: "../../../../internal/gittest/testdata/test-repo", + Type: types.TypeRepository, + ID: "sha256:d37c788d6fe832712cce9020943746b8764c04f7e323ed4ad68de36c5bf7d846", + BlobIDs: []string{ + "sha256:d37c788d6fe832712cce9020943746b8764c04f7e323ed4ad68de36c5bf7d846", + }, + RepoMetadata: artifact.RepoMetadata{ + RepoURL: "https://github.com/aquasecurity/trivy-test-repo/", + Branch: "main", + Tags: []string{"v0.0.1"}, + Commit: "8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35", + CommitMsg: "Update README.md", + Author: "Teppei Fukuda ", + Committer: "GitHub ", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + c := cachetest.NewCache(t, tt.setupCache) + + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +var terraformPolicyMetadata = types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Terraform Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, +} + +func TestTerraformMisconfigurationScan(t *testing.T) { + type fields struct { + dir string + } + tests := []struct { + name string + fields fields + wantBlobs []cachetest.WantBlob + artifactOpt artifact.Option + want artifact.Reference + }{ + { + name: "single failure", + fields: fields{ + dir: "./testdata/misconfig/terraform/single-failure", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "terraform", + FilePath: "main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.asd", + Provider: "Generic", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraform/single-failure", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "multiple failures", + fields: fields{ + dir: "./testdata/misconfig/terraform/multiple-failures", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "terraform", + FilePath: "main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.one", + Provider: "Generic", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.two", + Provider: "Generic", + Service: "general", + StartLine: 5, + EndLine: 7, + }, + }, + }, + }, + { + FileType: "terraform", + FilePath: "more.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.three", + Provider: "Generic", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraform/multiple-failures", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "no results", + fields: fields{ + dir: "./testdata/misconfig/terraform/no-results", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraform/no-results", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "passed", + fields: fields{ + dir: "./testdata/misconfig/terraform/passed", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "terraform", + FilePath: ".", + Successes: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraform/passed", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "multiple failures busted relative paths", + fields: fields{ + dir: "./testdata/misconfig/terraform/busted-relative-paths/child/main.tf", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "terraform", + FilePath: "main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.one", + Provider: "Generic", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.two", + Provider: "Generic", + Service: "general", + StartLine: 5, + EndLine: 7, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraform/busted-relative-paths/child/main.tf", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "tfvars outside the scan folder", + fields: fields{ + dir: "./testdata/misconfig/terraform/tfvar-outside/tf", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + TerraformTFVars: []string{"./testdata/misconfig/terraform/tfvar-outside/main.tfvars"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.Terraform, + FilePath: ".", + Successes: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraform/tfvar-outside/tf", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "relative paths", + fields: fields{ + dir: "./testdata/misconfig/terraform/relative-paths/child", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.Terraform, + FilePath: "../parent/main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.three", + Provider: "Generic", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + }, + }, + { + FileType: types.Terraform, + FilePath: "main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.one", + Provider: "Generic", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + }, + }, + { + FileType: types.Terraform, + FilePath: "nested/main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.two", + Provider: "Generic", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraform/relative-paths/child", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "scan raw config", + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + RawConfigScanners: []types.ConfigType{types.Terraform}, + }, + }, + fields: fields{ + dir: "./testdata/misconfig/terraform/single-failure", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "terraform", + FilePath: "main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.asd", + Provider: "Generic", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + { + Namespace: "user.test002", + Query: "data.user.test002.deny", + Message: "Empty bucket name!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST002", + AVDID: "AVD-TEST-0002", + Type: "Terraform Security Check", + Title: "Test policy", + Severity: "LOW", + }, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.asd", + Provider: "Terraform-Raw", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraform/single-failure", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + c := cachetest.NewCache(t, nil) + tt.artifactOpt.DisabledHandlers = []types.HandlerType{ + types.SystemFileFilteringPostHandler, + } + tt.artifactOpt.MisconfScannerOption.DisableEmbeddedPolicies = true + tt.artifactOpt.MisconfScannerOption.Namespaces = []string{"user"} + tt.artifactOpt.MisconfScannerOption.PolicyPaths = []string{"./testdata/misconfig/terraform/rego"} + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +const emptyBucketCheck = `package user.something + +__rego_metadata__ := { + "id": "TEST001", + "avd_id": "AVD-TEST-0001", + "title": "Test policy", + "short_code": "empty-bucket-name", + "severity": "LOW", + "description": "This is a test policy.", + "recommended_actions": "Have a cup of tea.", + "url": "https://trivy.dev/", +} + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + bucket := input.aws.s3.buckets[_] + bucket.name.value == "" + res := result("Empty bucket name!", bucket) +}` + +var terraformPlanPolicyMetadata = types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Terraform Plan Snapshot Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, +} + +func TestTerraformPlanSnapshotMisconfScan(t *testing.T) { + type fields struct { + dir string + } + tests := []struct { + name string + fields fields + wantBlobs []cachetest.WantBlob + want artifact.Reference + }{ + { + name: "single failure", + fields: fields{ + dir: "./testdata/misconfig/terraformplan/snapshots/single-failure", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.TerraformPlanSnapshot, + FilePath: "main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPlanPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.this", + Provider: "Generic", + Service: "general", + StartLine: 10, + EndLine: 12, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraformplan/snapshots/single-failure", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "multiple failures", + fields: fields{ + dir: "./testdata/misconfig/terraformplan/snapshots/multiple-failures", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.TerraformPlanSnapshot, + FilePath: "main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPlanPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.one", + Provider: "Generic", + Service: "general", + StartLine: 10, + EndLine: 12, + }, + }, + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPlanPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.two", + Provider: "Generic", + Service: "general", + StartLine: 14, + EndLine: 16, + }, + }, + }, + }, + { + FileType: types.TerraformPlanSnapshot, + FilePath: "more.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "Empty bucket name!", + PolicyMetadata: terraformPlanPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.three", + Provider: "Generic", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraformplan/snapshots/multiple-failures", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "passed", + fields: fields{ + dir: "./testdata/misconfig/terraformplan/snapshots/passed", + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.TerraformPlanSnapshot, + FilePath: ".", + Successes: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: terraformPlanPolicyMetadata, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/terraformplan/snapshots/passed", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + tmpDir := t.TempDir() + f, err := os.Create(filepath.Join(tmpDir, "policy.rego")) + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString(emptyBucketCheck) + require.NoError(t, err) + + c := cachetest.NewCache(t, nil) + + opt := artifact.Option{ + DisabledHandlers: []types.HandlerType{ + types.SystemFileFilteringPostHandler, + }, + MisconfScannerOption: misconf.ScannerOption{ + DisableEmbeddedPolicies: true, + DisableEmbeddedLibraries: false, + Namespaces: []string{"user"}, + PolicyPaths: []string{tmpDir}, + }, + WalkerOption: walker.Option{ + SkipFiles: []string{"*.tf"}, + }, + } + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), opt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func TestCloudFormationMisconfigurationScan(t *testing.T) { + type fields struct { + dir string + } + tests := []struct { + name string + fields fields + wantBlobs []cachetest.WantBlob + artifactOpt artifact.Option + want artifact.Reference + }{ + { + name: "single failure", + fields: fields{ + dir: "./testdata/misconfig/cloudformation/single-failure/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/cloudformation/single-failure/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "cloudformation", + FilePath: "main.yaml", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No buckets allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "CloudFormation Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Resource: "main.yaml:3-6", + Provider: "Cloud", + Service: "general", + StartLine: 3, + EndLine: 6, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/cloudformation/single-failure/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "multiple failures", + fields: fields{ + dir: "./testdata/misconfig/cloudformation/multiple-failures/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/cloudformation/multiple-failures/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "cloudformation", + FilePath: "main.yaml", + Failures: types.MisconfResults{ + types.MisconfResult{ + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No buckets allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "CloudFormation Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Resource: "main.yaml:2-5", + Provider: "Cloud", + Service: "general", + StartLine: 2, + EndLine: 5, + }, + }, + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No buckets allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "CloudFormation Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Resource: "main.yaml:6-9", + Provider: "Cloud", + Service: "general", + StartLine: 6, + EndLine: 9, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/cloudformation/multiple-failures/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "no results", + fields: fields{ + dir: "./testdata/misconfig/cloudformation/no-results/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/cloudformation/no-results/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/cloudformation/no-results/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "CloudFormation parameters outside the scan directory", + fields: fields{ + dir: "./testdata/misconfig/cloudformation/params/code/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/cloudformation/params/code/rego"}, + CloudFormationParamVars: []string{"./testdata/misconfig/cloudformation/params/cfparams.json"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "cloudformation", + FilePath: "main.yaml", + Successes: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "CloudFormation Security Check", + Title: "Bad stuff is bad", + Description: "Its not good!", + Severity: "HIGH", + RecommendedActions: "Remove bad stuff", + }, + CauseMetadata: types.CauseMetadata{ + Provider: "AWS", + Service: "sqs", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/cloudformation/params/code/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "passed", + fields: fields{ + dir: "./testdata/misconfig/cloudformation/passed/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/cloudformation/passed/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "cloudformation", + FilePath: "main.yaml", + Successes: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "CloudFormation Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Cloud", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/cloudformation/passed/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + c := cachetest.NewCache(t, nil) + tt.artifactOpt.DisabledHandlers = []types.HandlerType{ + types.SystemFileFilteringPostHandler, + } + tt.artifactOpt.MisconfScannerOption.DisableEmbeddedPolicies = true + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func TestDockerfileMisconfigurationScan(t *testing.T) { + type fields struct { + dir string + } + tests := []struct { + name string + fields fields + wantBlobs []cachetest.WantBlob + artifactOpt artifact.Option + want artifact.Reference + }{ + { + name: "single failure", + fields: fields{ + dir: "./testdata/misconfig/dockerfile/single-failure/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/dockerfile/single-failure/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "dockerfile", + FilePath: "Dockerfile", + Successes: types.MisconfResults{ + types.MisconfResult{ + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Dockerfile Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/dockerfile/single-failure/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "multiple failures", + fields: fields{ + dir: "./testdata/misconfig/dockerfile/multiple-failures/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/dockerfile/multiple-failures/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "dockerfile", + FilePath: "Dockerfile", + Successes: types.MisconfResults{ + types.MisconfResult{ + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Dockerfile Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/dockerfile/multiple-failures/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "no results", + fields: fields{ + dir: "./testdata/misconfig/dockerfile/no-results/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/dockerfile/no-results/rego"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/dockerfile/no-results/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "passed", + fields: fields{ + dir: "./testdata/misconfig/dockerfile/passed/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/dockerfile/passed/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "dockerfile", + FilePath: "Dockerfile", + Successes: []types.MisconfResult{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Dockerfile Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{ + "https://trivy.dev/", + }, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/dockerfile/passed/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + c := cachetest.NewCache(t, nil) + tt.artifactOpt.DisabledHandlers = []types.HandlerType{ + types.SystemFileFilteringPostHandler, + } + tt.artifactOpt.MisconfScannerOption.DisableEmbeddedPolicies = true + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func TestKubernetesMisconfigurationScan(t *testing.T) { + type fields struct { + dir string + } + tests := []struct { + name string + fields fields + wantBlobs []cachetest.WantBlob + artifactOpt artifact.Option + want artifact.Reference + }{ + { + name: "single failure", + fields: fields{ + dir: "./testdata/misconfig/kubernetes/single-failure/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/kubernetes/single-failure/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "kubernetes", + FilePath: "test.yaml", + Failures: []types.MisconfResult{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No evil containers allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Kubernetes Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{ + "https://trivy.dev/", + }, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Kubernetes", + Service: "general", + StartLine: 7, + EndLine: 9, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/kubernetes/single-failure/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "multiple failures", + fields: fields{ + dir: "./testdata/misconfig/kubernetes/multiple-failures/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/kubernetes/multiple-failures/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "kubernetes", + FilePath: "test.yaml", + Failures: []types.MisconfResult{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No evil containers allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Kubernetes Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{ + "https://trivy.dev/", + }, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Kubernetes", + Service: "general", + StartLine: 7, + EndLine: 9, + }, + }, + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No evil containers allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Kubernetes Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{ + "https://trivy.dev/", + }, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Kubernetes", + Service: "general", + StartLine: 10, + EndLine: 12, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/kubernetes/multiple-failures/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "no results", + fields: fields{ + dir: "./testdata/misconfig/kubernetes/no-results/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/kubernetes/no-results/rego"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/kubernetes/no-results/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "passed", + fields: fields{ + dir: "./testdata/misconfig/kubernetes/passed/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/kubernetes/passed/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "kubernetes", + FilePath: "test.yaml", + Successes: []types.MisconfResult{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Kubernetes Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{ + "https://trivy.dev/", + }, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Kubernetes", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/kubernetes/passed/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + c := cachetest.NewCache(t, nil) + tt.artifactOpt.DisabledHandlers = []types.HandlerType{ + types.SystemFileFilteringPostHandler, + } + tt.artifactOpt.MisconfScannerOption.DisableEmbeddedPolicies = true + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func TestAzureARMMisconfigurationScan(t *testing.T) { + type fields struct { + dir string + } + tests := []struct { + name string + fields fields + wantBlobs []cachetest.WantBlob + artifactOpt artifact.Option + want artifact.Reference + }{ + { + name: "single failure", + fields: fields{ + dir: "./testdata/misconfig/azurearm/single-failure/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/azurearm/single-failure/rego"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "azure-arm", + FilePath: "deploy.json", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No account allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Azure ARM Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Resource: "resources[0]", + Provider: "Cloud", + Service: "general", + StartLine: 30, + EndLine: 40, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/azurearm/single-failure/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "multiple failures", + fields: fields{ + dir: "./testdata/misconfig/azurearm/multiple-failures/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/azurearm/multiple-failures/rego"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "azure-arm", + FilePath: "deploy.json", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No account allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Azure ARM Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Resource: "resources[0]", + Provider: "Cloud", + Service: "general", + StartLine: 30, + EndLine: 40, + }, + }, + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No account allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Azure ARM Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Resource: "resources[1]", + Provider: "Cloud", + Service: "general", + StartLine: 41, + EndLine: 51, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/azurearm/multiple-failures/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "no results", + fields: fields{ + dir: "./testdata/misconfig/azurearm/no-results/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/azurearm/no-results/rego"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/azurearm/no-results/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + { + name: "passed", + fields: fields{ + dir: "./testdata/misconfig/azurearm/passed/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/azurearm/passed/rego"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "azure-arm", + FilePath: "deploy.json", + Successes: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Azure ARM Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Cloud", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/azurearm/passed/src", + Type: types.TypeFilesystem, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + c := cachetest.NewCache(t, nil) + tt.artifactOpt.DisabledHandlers = []types.HandlerType{ + types.SystemFileFilteringPostHandler, + } + tt.artifactOpt.MisconfScannerOption.DisableEmbeddedPolicies = true + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func TestMixedConfigurationScan(t *testing.T) { + type fields struct { + dir string + } + tests := []struct { + name string + fields fields + wantBlobs []cachetest.WantBlob + artifactOpt artifact.Option + want artifact.Reference + }{ + { + name: "single failure each within terraform and cloudformation", + fields: fields{ + dir: "./testdata/misconfig/mixed/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/mixed/rego"}, + DisableEmbeddedLibraries: true, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: 2, + Misconfigurations: []types.Misconfiguration{ + { + FileType: "cloudformation", + FilePath: "main.yaml", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No buckets allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "CloudFormation Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Resource: "main.yaml:3-6", + Provider: "Cloud", + Service: "general", + StartLine: 3, + EndLine: 6, + }, + }, + }, + }, + { + FileType: "terraform", + FilePath: "main.tf", + Failures: types.MisconfResults{ + { + Namespace: "user.something", + Query: "data.user.something.deny", + Message: "No buckets allowed!", + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "AVD-TEST-0001", + Type: "Terraform Security Check", + Title: "Test policy", + Description: "This is a test policy.", + Severity: "LOW", + RecommendedActions: "Have a cup of tea.", + References: []string{"https://trivy.dev/"}, + }, + CauseMetadata: types.CauseMetadata{ + Resource: "aws_s3_bucket.asd", + Provider: "Cloud", + Service: "general", + StartLine: 1, + EndLine: 3, + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/mixed/src", + Type: types.TypeFilesystem, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + c := cachetest.NewCache(t, nil) + tt.artifactOpt.DisabledHandlers = []types.HandlerType{ + types.SystemFileFilteringPostHandler, + } + tt.artifactOpt.MisconfScannerOption.DisableEmbeddedPolicies = true + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + require.NoError(t, err) + require.NotNil(t, got) + + assert.Equal(t, tt.want.Name, got.Name) + assert.Equal(t, tt.want.Type, got.Type) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func TestJSONConfigScan(t *testing.T) { + type fields struct { + dir string + schemas []string + } + + tests := []struct { + name string + fields fields + artifactOpt artifact.Option + wantBlobs []cachetest.WantBlob + want artifact.Reference + }{ + { + name: "happy path without custom schema", + fields: fields{ + dir: "./testdata/misconfig/json/passed/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/json/passed/checks"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.JSON, + FilePath: "test1.json", + Failures: types.MisconfResults{ + { + Namespace: "user.test_json_check", + Query: "data.user.test_json_check.deny", + Message: `Service "foo" should not be used`, + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "TEST001", + Type: "JSON Security Check", + Title: "Test check", + Severity: "LOW", + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + { + FileType: types.JSON, + FilePath: "test2.json", + Failures: types.MisconfResults{ + { + Namespace: "user.test_json_check", + Query: "data.user.test_json_check.deny", + Message: `Provider "bar" should not be used`, + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "TEST001", + Type: "JSON Security Check", + Title: "Test check", + Severity: "LOW", + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/json/passed/src", + Type: types.TypeFilesystem, + }, + }, + { + name: "happy path with custom schema", + fields: fields{ + dir: "./testdata/misconfig/json/with-schema/src", + schemas: []string{"./testdata/misconfig/json/with-schema/schemas"}, + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/json/with-schema/checks"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.JSON, + FilePath: "test1.json", + Failures: types.MisconfResults{ + { + Namespace: "user.test_json_check", + Query: "data.user.test_json_check.deny", + Message: `Service "foo" should not be used`, + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "TEST001", + Type: "JSON Security Check", + Title: "Test check", + Severity: "LOW", + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Generic", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/json/with-schema/src", + Type: types.TypeFilesystem, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + c := cachetest.NewCache(t, nil) + + tt.artifactOpt.MisconfScannerOption.DisableEmbeddedPolicies = true + if len(tt.fields.schemas) > 0 { + schemas, err := misconf.LoadConfigSchemas(tt.fields.schemas) + require.NoError(t, err) + tt.artifactOpt.MisconfScannerOption.ConfigFileSchemas = schemas + } + + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + require.NoError(t, err) + require.NotNil(t, got) + + assert.Equal(t, tt.want.Name, got.Name) + assert.Equal(t, tt.want.Type, got.Type) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func TestYAMLConfigScan(t *testing.T) { + type fields struct { + dir string + schemas []string + } + + tests := []struct { + name string + fields fields + artifactOpt artifact.Option + wantBlobs []cachetest.WantBlob + want artifact.Reference + wantErr string + }{ + { + name: "happy path without custom schema", + fields: fields{ + dir: "./testdata/misconfig/yaml/passed/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/yaml/passed/checks"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.YAML, + FilePath: "test1.yaml", + Failures: types.MisconfResults{ + { + Namespace: "user.test_yaml_check", + Query: "data.user.test_yaml_check.deny", + Message: `Service "foo" should not be used`, + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "TEST001", + Type: "YAML Security Check", + Title: "Test check", + Severity: "LOW", + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Yaml", + Service: "general", + }, + }, + }, + }, + { + FileType: types.YAML, + FilePath: "test2.yml", + Failures: types.MisconfResults{ + { + Namespace: "user.test_yaml_check", + Query: "data.user.test_yaml_check.deny", + Message: `Provider "bar" should not be used`, + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "TEST001", + Type: "YAML Security Check", + Title: "Test check", + Severity: "LOW", + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Yaml", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/yaml/passed/src", + Type: types.TypeFilesystem, + }, + }, + { + name: "happy path with custom schema", + fields: fields{ + dir: "./testdata/misconfig/yaml/with-schema/src", + schemas: []string{"./testdata/misconfig/yaml/with-schema/schemas"}, + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{"./testdata/misconfig/yaml/with-schema/checks"}, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + Misconfigurations: []types.Misconfiguration{ + { + FileType: types.YAML, + FilePath: "test1.yaml", + Failures: types.MisconfResults{ + { + Namespace: "user.test_yaml_check", + Query: "data.user.test_yaml_check.deny", + Message: `Service "foo" should not be used`, + PolicyMetadata: types.PolicyMetadata{ + ID: "TEST001", + AVDID: "TEST001", + Type: "YAML Security Check", + Title: "Test check", + Severity: "LOW", + }, + CauseMetadata: types.CauseMetadata{ + Provider: "Yaml", + Service: "general", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: "testdata/misconfig/yaml/with-schema/src", + Type: types.TypeFilesystem, + }, + }, + { + name: "with rego error limit", + fields: fields{ + dir: "./testdata/misconfig/yaml/with-rego-error-limit/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{ + "./testdata/misconfig/yaml/with-rego-error-limit/checks/test_2_errors.rego", + "./testdata/misconfig/yaml/with-rego-error-limit/checks/test.json", + }, + RegoErrorLimit: 1, + }, + }, + wantErr: "ego_type_error: undefined ref: input.wrong_ref", + }, + { + name: "with Rego error limit 0", + fields: fields{ + dir: "./testdata/misconfig/yaml/with-rego-error-limit/src", + }, + artifactOpt: artifact.Option{ + MisconfScannerOption: misconf.ScannerOption{ + Namespaces: []string{"user"}, + PolicyPaths: []string{ + "./testdata/misconfig/yaml/with-rego-error-limit/checks/test_1_error.rego", + "./testdata/misconfig/yaml/with-rego-error-limit/checks/test.json", + }, + RegoErrorLimit: 0, + }, + }, + wantErr: "ego_type_error: undefined ref: input.wrong_ref", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistent test results + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + c := cachetest.NewCache(t, nil) + + tt.artifactOpt.MisconfScannerOption.DisableEmbeddedPolicies = true + if len(tt.fields.schemas) > 0 { + schemas, err := misconf.LoadConfigSchemas(tt.fields.schemas) + require.NoError(t, err) + tt.artifactOpt.MisconfScannerOption.ConfigFileSchemas = schemas + } + + a, err := NewArtifact(tt.fields.dir, c, walker.NewFS(), tt.artifactOpt) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + require.NotNil(t, got) + + assert.Equal(t, tt.want.Name, got.Name) + assert.Equal(t, tt.want.Type, got.Type) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +// recordingWalker wraps an existing walker and records which paths were walked +type recordingWalker struct { + base Walker + walkedRoots []string +} + +func newRecordingWalker(base Walker) *recordingWalker { + return &recordingWalker{ + base: base, + } +} + +func (w *recordingWalker) Walk(root string, option walker.Option, walkFn walker.WalkFunc) error { + w.walkedRoots = append(w.walkedRoots, filepath.ToSlash(root)) + // Call the original walker + return w.base.Walk(root, option, walkFn) +} + +// TestArtifact_AnalysisStrategy tests the different analysis strategies +func TestArtifact_AnalysisStrategy(t *testing.T) { + // Use testdata/alpine directly + testDir := "testdata/alpine" + + tests := []struct { + name string + disabledAnalyzers []analyzer.Type + wantRoots []string + }{ + { + name: "static paths", + disabledAnalyzers: append(analyzer.TypeConfigFiles, analyzer.TypePip, analyzer.TypeSecret), + wantRoots: []string{ + "testdata/alpine/etc/alpine-release", + "testdata/alpine/lib/apk/db/installed", + "testdata/alpine/usr/lib/apk/db/installed", + }, + }, + { + name: "traversing root dir", + wantRoots: []string{ + testDir, // only the root directory is walked + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a new artifact with the recording walker + baseWalker := walker.NewFS() + rw := newRecordingWalker(baseWalker) + + // Create artifact with recording walker + a, err := NewArtifact(testDir, cache.NewMemoryCache(), rw, artifact.Option{ + DisabledAnalyzers: tt.disabledAnalyzers, + }) + require.NoError(t, err) + + // Run the inspection + _, err = a.Inspect(t.Context()) + require.NoError(t, err) + + // Check if the walked roots match the expected roots + assert.ElementsMatch(t, tt.wantRoots, rw.walkedRoots) + }) + } +} + +func Test_sanitizeRemoteURL(t *testing.T) { + tests := []struct { + name string + in string + want string + }{ + { + name: "https with user:pass", + in: "https://user:token@github.com/org/repo.git", + want: "https://github.com/org/repo.git", + }, + { + name: "port in authority with userinfo", + in: "https://user:pass@host:8443/repo.git", + want: "https://host:8443/repo.git", + }, + { + name: "http with username only", + in: "http://user@github.com/org/repo", + want: "http://github.com/org/repo", + }, + { + name: "double scheme after userinfo", + in: "https://gitlab-ci-token:glcbt-64_QwERTyuiOp-AsD2NgCJ7@example.com/gitrepo.git", + want: "https://example.com/gitrepo.git", + }, + { + name: "ssh scheme with username", + in: "ssh://git@github.com/org/repo.git", + want: "ssh://github.com/org/repo.git", + }, + { + name: "scp-like ssh unchanged", + in: "git@github.com:org/repo.git", + want: "git@github.com:org/repo.git", + }, + { + name: "already clean https", + in: "https://github.com/org/repo.git", + want: "https://github.com/org/repo.git", + }, + { + name: "no scheme left as-is", + in: "github.com/org/repo.git", + want: "github.com/org/repo.git", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got := sanitizeRemoteURL(tt.in) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/artifact/local/testdata/alpine/etc/alpine-release b/pkg/fanal/artifact/local/testdata/alpine/etc/alpine-release new file mode 100644 index 000000000000..375f5cabfe6c --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/alpine/etc/alpine-release @@ -0,0 +1 @@ +3.11.6 diff --git a/pkg/fanal/artifact/local/testdata/alpine/etc/hostname b/pkg/fanal/artifact/local/testdata/alpine/etc/hostname new file mode 100644 index 000000000000..c70dc2dfaf07 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/alpine/etc/hostname @@ -0,0 +1 @@ +host diff --git a/pkg/fanal/artifact/local/testdata/alpine/lib/apk/db/installed b/pkg/fanal/artifact/local/testdata/alpine/lib/apk/db/installed new file mode 100644 index 000000000000..dc23126b6a1b --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/alpine/lib/apk/db/installed @@ -0,0 +1,21 @@ +C:Q1yyMWoYnr7lKCxKm9mHlMwkd6dMY= +P:musl +V:1.1.24-r2 +A:x86_64 +S:377123 +I:614400 +T:the musl c library (libc) implementation +U:https://musl.libc.org/ +L:MIT +o:musl +m:Timo Teräs +t:1584790550 +c:4024cc3b29ad4c65544ad068b8f59172b5494306 +p:so:libc.musl-x86_64.so.1=1 +F:lib +R:libc.musl-x86_64.so.1 +a:0:0:777 +Z:Q17yJ3JFNypA4mxhJJr0ou6CzsJVI= +R:ld-musl-x86_64.so.1 +a:0:0:755 +Z:Q19mQZaYKY6yTQWQm0hkvsrh39O7Y= diff --git a/pkg/fanal/artifact/local/testdata/misconfig/azurearm/multiple-failures/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/multiple-failures/rego/policy.rego new file mode 100644 index 000000000000..4b5158649568 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/multiple-failures/rego/policy.rego @@ -0,0 +1,35 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: cloud +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + account := input.azure.storage.accounts[_] + res := result("No account allowed!", account) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/azurearm/multiple-failures/src/deploy.json b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/multiple-failures/src/deploy.json new file mode 100644 index 000000000000..8978cca2f43c --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/multiple-failures/src/deploy.json @@ -0,0 +1,59 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountType": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS", + "Standard_ZRS", + "Premium_LRS" + ], + "metadata": { + "description": "Storage Account type" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + } + }, + "variables": { + "storageAccountName": "[concat('store', uniquestring(resourceGroup().id))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "apiVersion": "2018-07-01", + "sku": { + "name": "[parameters('storageAccountType')]" + }, + "kind": "StorageV2", + "properties": {} + }, + { + "type": "Microsoft.Storage/storageAccounts", + "name": "bucket2", + "location": "[parameters('location')]", + "apiVersion": "2018-07-01", + "sku": { + "name": "[parameters('storageAccountType')]" + }, + "kind": "StorageV2", + "properties": {} + } + ], + "outputs": { + "storageAccountName": { + "type": "string", + "value": "[variables('storageAccountName')]" + } + } +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/azurearm/no-results/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/no-results/rego/policy.rego new file mode 100644 index 000000000000..22c65aaf287c --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/no-results/rego/policy.rego @@ -0,0 +1,35 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: cloud +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + bucket := input.aws.s3.buckets[_] + res := result("No buckets allowed!", bucket) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/azurearm/no-results/src/.gitkeep b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/no-results/src/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/artifact/local/testdata/misconfig/azurearm/passed/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/passed/rego/policy.rego new file mode 100644 index 000000000000..243eb8e0d9ec --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/passed/rego/policy.rego @@ -0,0 +1,36 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: cloud +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + account := input.azure.storage.accounts[_] + true == false + res := result("No accounts allowed!", account) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/azurearm/passed/src/deploy.json b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/passed/src/deploy.json new file mode 100644 index 000000000000..fbe9fa898e95 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/passed/src/deploy.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountType": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS", + "Standard_ZRS", + "Premium_LRS" + ], + "metadata": { + "description": "Storage Account type" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + } + }, + "variables": { + "storageAccountName": "[concat('store', uniquestring(resourceGroup().id))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "apiVersion": "2018-07-01", + "sku": { + "name": "[parameters('storageAccountType')]" + }, + "kind": "StorageV2", + "properties": {} + } + ], + "outputs": { + "storageAccountName": { + "type": "string", + "value": "[variables('storageAccountName')]" + } + } +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/azurearm/single-failure/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/single-failure/rego/policy.rego new file mode 100644 index 000000000000..4b5158649568 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/single-failure/rego/policy.rego @@ -0,0 +1,35 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: cloud +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + account := input.azure.storage.accounts[_] + res := result("No account allowed!", account) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/azurearm/single-failure/src/deploy.json b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/single-failure/src/deploy.json new file mode 100644 index 000000000000..fbe9fa898e95 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/azurearm/single-failure/src/deploy.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "storageAccountType": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS", + "Standard_ZRS", + "Premium_LRS" + ], + "metadata": { + "description": "Storage Account type" + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + } + }, + "variables": { + "storageAccountName": "[concat('store', uniquestring(resourceGroup().id))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "apiVersion": "2018-07-01", + "sku": { + "name": "[parameters('storageAccountType')]" + }, + "kind": "StorageV2", + "properties": {} + } + ], + "outputs": { + "storageAccountName": { + "type": "string", + "value": "[variables('storageAccountName')]" + } + } +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/multiple-failures/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/multiple-failures/rego/policy.rego new file mode 100644 index 000000000000..22c65aaf287c --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/multiple-failures/rego/policy.rego @@ -0,0 +1,35 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: cloud +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + bucket := input.aws.s3.buckets[_] + res := result("No buckets allowed!", bucket) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/multiple-failures/src/main.yaml b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/multiple-failures/src/main.yaml new file mode 100644 index 000000000000..f07fec238b87 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/multiple-failures/src/main.yaml @@ -0,0 +1,9 @@ +Resources: + S3BucketOne: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: bucket-one + S3BucketTwo: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: bucket-two diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/no-results/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/no-results/rego/policy.rego new file mode 100644 index 000000000000..22c65aaf287c --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/no-results/rego/policy.rego @@ -0,0 +1,35 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: cloud +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + bucket := input.aws.s3.buckets[_] + res := result("No buckets allowed!", bucket) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/no-results/src/.gitkeep b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/no-results/src/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/params/cfparams.json b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/params/cfparams.json new file mode 100644 index 000000000000..b9276cb36eb2 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/params/cfparams.json @@ -0,0 +1,6 @@ +[ + { + "ParameterKey": "KmsMasterKeyId", + "ParameterValue": "some_id" + } +] \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/params/code/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/params/code/rego/policy.rego new file mode 100644 index 000000000000..1d8cd4a1463b --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/params/code/rego/policy.rego @@ -0,0 +1,24 @@ +# METADATA +# title: "Bad stuff is bad" +# description: "Its not good!" +# scope: package +# schemas: +# - input: schema["cloud"] +# custom: +# avd_id: AVD-TEST-0001 +# id: TEST001 +# provider: aws +# service: sqs +# severity: HIGH +# short_code: foo-bar-baz +# recommended_action: "Remove bad stuff" +# input: +# selector: +# - type: cloud +package user.something + +deny[res] { + qs := input.aws.sqs.queues[_] + qs.encryption.kmskeyid.value == "" + res := "No unencrypted queues allowed!" +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/params/code/src/main.yaml b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/params/code/src/main.yaml new file mode 100644 index 000000000000..45047944d8c9 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/params/code/src/main.yaml @@ -0,0 +1,10 @@ +AWSTemplateFormatVersion: 2010-09-09 +Parameters: + KmsMasterKeyId: + Type: String +Resources: + TestQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: worst-possible-queue + KmsMasterKeyId: !Ref KmsMasterKeyId \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/passed/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/passed/rego/policy.rego new file mode 100644 index 000000000000..ce267bc752a6 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/passed/rego/policy.rego @@ -0,0 +1,36 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: cloud +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + bucket := input.aws.s3.buckets[_] + true == false + res := result("No buckets allowed!", bucket) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/passed/src/main.yaml b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/passed/src/main.yaml new file mode 100644 index 000000000000..5406b2c9f2a1 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/passed/src/main.yaml @@ -0,0 +1,5 @@ +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: public-bucket diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/single-failure/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/single-failure/rego/policy.rego new file mode 100644 index 000000000000..22c65aaf287c --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/single-failure/rego/policy.rego @@ -0,0 +1,35 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: cloud +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + bucket := input.aws.s3.buckets[_] + res := result("No buckets allowed!", bucket) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/single-failure/src/main.yaml b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/single-failure/src/main.yaml new file mode 100644 index 000000000000..2135e1c51922 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/cloudformation/single-failure/src/main.yaml @@ -0,0 +1,6 @@ +--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: public-bucket diff --git a/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/multiple-failures/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/multiple-failures/rego/policy.rego new file mode 100644 index 000000000000..ab4daa223c8b --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/multiple-failures/rego/policy.rego @@ -0,0 +1,32 @@ +package user.something + +__rego_metadata__ := { + "id": "TEST001", + "avd_id": "AVD-TEST-0001", + "title": "Test policy", + "short_code": "no-buckets", + "severity": "LOW", + "description": "This is a test policy.", + "recommended_actions": "Have a cup of tea.", + "url": "https://trivy.dev/", +} + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", object.get(metadata, "StartLine", 0)), + "endline": object.get(metadata, "endline", object.get(metadata, "EndLine", 0)), + "filepath": object.get(metadata, "filepath", object.get(metadata, "Path", "")), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + cmd := input.stages[_][_] + res := result("No commands allowed!", cmd) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/multiple-failures/src/Dockerfile b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/multiple-failures/src/Dockerfile new file mode 100644 index 000000000000..200e856568eb --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/multiple-failures/src/Dockerfile @@ -0,0 +1,3 @@ +FROM ubuntu + +FROM alpine \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/no-results/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/no-results/rego/policy.rego new file mode 100644 index 000000000000..ecf4506727a3 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/no-results/rego/policy.rego @@ -0,0 +1,32 @@ +package user.something + +__rego_metadata__ := { + "id": "TEST001", + "avd_id": "AVD-TEST-0001", + "title": "Test policy", + "short_code": "no-buckets", + "severity": "LOW", + "description": "This is a test policy.", + "recommended_actions": "Have a cup of tea.", + "url": "https://trivy.dev/", +} + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + bucket := input.aws.s3.buckets[_] + res := result("No buckets allowed!", bucket) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/no-results/src/.gitkeep b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/no-results/src/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/passed/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/passed/rego/policy.rego new file mode 100644 index 000000000000..4ea9908bff21 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/passed/rego/policy.rego @@ -0,0 +1,32 @@ +package user.something + +__rego_metadata__ := { + "id": "TEST001", + "avd_id": "AVD-TEST-0001", + "title": "Test policy", + "short_code": "no-buckets", + "severity": "LOW", + "description": "This is a test policy.", + "recommended_actions": "Have a cup of tea.", + "url": "https://trivy.dev/", +} + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + true == false + res := result("No nothing allowed!", {}) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/passed/src/Dockerfile b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/passed/src/Dockerfile new file mode 100644 index 000000000000..7078ce2076df --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/passed/src/Dockerfile @@ -0,0 +1 @@ +FROM nowhere \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/single-failure/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/single-failure/rego/policy.rego new file mode 100644 index 000000000000..ab4daa223c8b --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/single-failure/rego/policy.rego @@ -0,0 +1,32 @@ +package user.something + +__rego_metadata__ := { + "id": "TEST001", + "avd_id": "AVD-TEST-0001", + "title": "Test policy", + "short_code": "no-buckets", + "severity": "LOW", + "description": "This is a test policy.", + "recommended_actions": "Have a cup of tea.", + "url": "https://trivy.dev/", +} + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", object.get(metadata, "StartLine", 0)), + "endline": object.get(metadata, "endline", object.get(metadata, "EndLine", 0)), + "filepath": object.get(metadata, "filepath", object.get(metadata, "Path", "")), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + cmd := input.stages[_][_] + res := result("No commands allowed!", cmd) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/single-failure/src/Dockerfile b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/single-failure/src/Dockerfile new file mode 100644 index 000000000000..6d4b4c49ea2e --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/dockerfile/single-failure/src/Dockerfile @@ -0,0 +1 @@ +FROM ubuntu \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/json/passed/checks/test.rego b/pkg/fanal/artifact/local/testdata/misconfig/json/passed/checks/test.rego new file mode 100644 index 000000000000..b8e61860e873 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/json/passed/checks/test.rego @@ -0,0 +1,17 @@ +# METADATA +# title: Test check +# custom: +# id: TEST001 +# avd_id: TEST001 +# severity: LOW +package user.test_json_check + +deny[res] { + input.service == "foo" + res := result.new(`Service "foo" should not be used`, input.service) +} + +deny[res] { + input.provider == "bar" + res := result.new(`Provider "bar" should not be used`, input.provider) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/json/passed/src/test1.json b/pkg/fanal/artifact/local/testdata/misconfig/json/passed/src/test1.json new file mode 100644 index 000000000000..95b9d38deb71 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/json/passed/src/test1.json @@ -0,0 +1,3 @@ +{ + "service": "foo" +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/json/passed/src/test2.json b/pkg/fanal/artifact/local/testdata/misconfig/json/passed/src/test2.json new file mode 100644 index 000000000000..22e90bf288b3 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/json/passed/src/test2.json @@ -0,0 +1,3 @@ +{ + "provider": "bar" +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/checks/test.rego b/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/checks/test.rego new file mode 100644 index 000000000000..b8e61860e873 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/checks/test.rego @@ -0,0 +1,17 @@ +# METADATA +# title: Test check +# custom: +# id: TEST001 +# avd_id: TEST001 +# severity: LOW +package user.test_json_check + +deny[res] { + input.service == "foo" + res := result.new(`Service "foo" should not be used`, input.service) +} + +deny[res] { + input.provider == "bar" + res := result.new(`Provider "bar" should not be used`, input.provider) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/schemas/test.json b/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/schemas/test.json new file mode 100644 index 000000000000..f72cdb0d2016 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/schemas/test.json @@ -0,0 +1,9 @@ +{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "service": { "type": "string" } + }, + "required": ["service"] +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/src/test1.json b/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/src/test1.json new file mode 100644 index 000000000000..95b9d38deb71 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/src/test1.json @@ -0,0 +1,3 @@ +{ + "service": "foo" +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/src/test2.json b/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/src/test2.json new file mode 100644 index 000000000000..22e90bf288b3 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/json/with-schema/src/test2.json @@ -0,0 +1,3 @@ +{ + "provider": "bar" +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/multiple-failures/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/multiple-failures/rego/policy.rego new file mode 100644 index 000000000000..2e914843a266 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/multiple-failures/rego/policy.rego @@ -0,0 +1,36 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: kubernetes +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", object.get(metadata, "StartLine", 0)), + "endline": object.get(metadata, "endline", object.get(metadata, "EndLine", 0)), + "filepath": object.get(metadata, "filepath", object.get(metadata, "Path", "")), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + container := input.spec.containers[_] + container.image == "evil" + res := result("No evil containers allowed!", container) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/multiple-failures/src/test.yaml b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/multiple-failures/src/test.yaml new file mode 100644 index 000000000000..b8eb522009ce --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/multiple-failures/src/test.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: evil-pod +spec: + containers: + - command: [ "sh", "-c", "echo 'Hello' && sleep 1h" ] + image: evil + name: evil1 + - command: [ "sh", "-c", "echo 'Hello' && sleep 1h" ] + image: evil + name: evil2 diff --git a/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/no-results/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/no-results/rego/policy.rego new file mode 100644 index 000000000000..e46dea74b0a9 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/no-results/rego/policy.rego @@ -0,0 +1,36 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: kubernetes +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + container := input.spec.containers[_] + container.image == "evil" + res := result("No evil containers allowed!", container) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/no-results/src/.gitkeep b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/no-results/src/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/passed/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/passed/rego/policy.rego new file mode 100644 index 000000000000..fd312f3d26d5 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/passed/rego/policy.rego @@ -0,0 +1,37 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: kubernetes +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + true == false + container := input.spec.containers[_] + container.image == "evil" + res := result("No evil containers allowed!", container) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/passed/src/test.yaml b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/passed/src/test.yaml new file mode 100644 index 000000000000..e50c03986d23 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/passed/src/test.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: good-pod +spec: + containers: + - command: [ "sh", "-c", "echo 'Hello' && sleep 1h" ] + image: good + name: good diff --git a/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/single-failure/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/single-failure/rego/policy.rego new file mode 100644 index 000000000000..d01bb39ce435 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/single-failure/rego/policy.rego @@ -0,0 +1,36 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - https://trivy.dev/ +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-evil +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: kubernetes +package user.something + +# taken from defsec rego lib to mimic behaviour +result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", object.get(metadata, "StartLine", 0)), + "endline": object.get(metadata, "endline", object.get(metadata, "EndLine", 0)), + "filepath": object.get(metadata, "filepath", object.get(metadata, "Path", "")), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } +} + +deny[res] { + container := input.spec.containers[_] + container.image == "evil" + res := result("No evil containers allowed!", container) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/single-failure/src/test.yaml b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/single-failure/src/test.yaml new file mode 100644 index 000000000000..1874c10c48a4 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/kubernetes/single-failure/src/test.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Pod +metadata: + name: evil-pod +spec: + containers: + - command: [ "sh", "-c", "echo 'Hello' && sleep 1h" ] + image: evil + name: evil diff --git a/pkg/fanal/artifact/local/testdata/misconfig/mixed/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/mixed/rego/policy.rego new file mode 100644 index 000000000000..410a3c8ada8d --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/mixed/rego/policy.rego @@ -0,0 +1,35 @@ +# METADATA +# title: Test policy +# description: This is a test policy. +# related_resources: +# - "https://trivy.dev/" +# custom: +# id: TEST001 +# avd_id: AVD-TEST-0001 +# severity: LOW +# short_code: no-buckets +# recommended_actions: Have a cup of tea. +# input: +# selector: +# - type: cloud +package user.something + + # taken from defsec rego lib to mimic behaviour + result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } + } + + deny[res] { + bucket := input.aws.s3.buckets[_] + res := result("No buckets allowed!", bucket) + } \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/mixed/src/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/mixed/src/main.tf new file mode 100644 index 000000000000..62697f2ab0bc --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/mixed/src/main.tf @@ -0,0 +1,3 @@ +resource "aws_s3_bucket" "asd" { + + } \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/mixed/src/main.yaml b/pkg/fanal/artifact/local/testdata/misconfig/mixed/src/main.yaml new file mode 100644 index 000000000000..c3e9adcc2fd9 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/mixed/src/main.yaml @@ -0,0 +1,6 @@ +--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: public-bucket \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/busted-relative-paths/child/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/busted-relative-paths/child/main.tf new file mode 100644 index 000000000000..192ffbe802a3 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/busted-relative-paths/child/main.tf @@ -0,0 +1,11 @@ +resource "aws_s3_bucket" "one" { + + } + + resource "aws_s3_bucket" "two" { + + } + + module "module_in_parent_dir" { + source = "../does not exist anywhere/" +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/busted-relative-paths/parent/more.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/busted-relative-paths/parent/more.tf new file mode 100644 index 000000000000..b06a4528a493 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/busted-relative-paths/parent/more.tf @@ -0,0 +1,3 @@ +resource "aws_s3_bucket" "three" { + + } \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/multiple-failures/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/multiple-failures/main.tf new file mode 100644 index 000000000000..6d6aa02ece44 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/multiple-failures/main.tf @@ -0,0 +1,7 @@ +resource "aws_s3_bucket" "one" { + +} + +resource "aws_s3_bucket" "two" { + +} diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/multiple-failures/more.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/multiple-failures/more.tf new file mode 100644 index 000000000000..e5cc0c1ea195 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/multiple-failures/more.tf @@ -0,0 +1,3 @@ +resource "aws_s3_bucket" "three" { + +} diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/no-results/.gitkeep b/pkg/fanal/artifact/local/testdata/misconfig/terraform/no-results/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/passed/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/passed/main.tf new file mode 100644 index 000000000000..28149d8b5608 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/passed/main.tf @@ -0,0 +1,3 @@ +resource "aws_s3_bucket" "asd" { + bucket = "asd" +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/rego/policy-raw.rego b/pkg/fanal/artifact/local/testdata/misconfig/terraform/rego/policy-raw.rego new file mode 100644 index 000000000000..15cc54dfd006 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/rego/policy-raw.rego @@ -0,0 +1,23 @@ +# METADATA +# title: Test policy +# schemas: +# - input: schema["terraform-raw"] +# custom: +# id: TEST002 +# avd_id: AVD-TEST-0002 +# short_code: empty-bucket-name +# severity: LOW +# input: +# selector: +# - type: terraform-raw +package user.test002 + +import rego.v1 + +deny contains res if { + some block in input.modules[_].blocks + block.kind == "resource" + block.type == "aws_s3_bucket" + not "bucket" in block.attributes + res := result.new("Empty bucket name!", block) +} diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/rego/policy.rego b/pkg/fanal/artifact/local/testdata/misconfig/terraform/rego/policy.rego new file mode 100644 index 000000000000..b1cd80249c9e --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/rego/policy.rego @@ -0,0 +1,33 @@ +package user.something + + __rego_metadata__ := { + "id": "TEST001", + "avd_id": "AVD-TEST-0001", + "title": "Test policy", + "short_code": "empty-bucket-name", + "severity": "LOW", + "description": "This is a test policy.", + "recommended_actions": "Have a cup of tea.", + "url": "https://trivy.dev/", + } + + # taken from defsec rego lib to mimic behaviour + result(msg, cause) = result { + metadata := object.get(cause, "__defsec_metadata", cause) + result := { + "msg": msg, + "startline": object.get(metadata, "startline", 0), + "endline": object.get(metadata, "endline", 0), + "filepath": object.get(metadata, "filepath", ""), + "explicit": object.get(metadata, "explicit", false), + "managed": object.get(metadata, "managed", true), + "fskey": object.get(metadata, "fskey", ""), + "resource": object.get(metadata, "resource", ""), + } + } + + deny[res] { + bucket := input.aws.s3.buckets[_] + bucket.name.value == "" + res := result("Empty bucket name!", bucket) + } \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/relative-paths/child/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/relative-paths/child/main.tf new file mode 100644 index 000000000000..4e7522b5c20f --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/relative-paths/child/main.tf @@ -0,0 +1,7 @@ +resource "aws_s3_bucket" "one" { + +} + +module "module_in_nested_dir" { + source = "./nested/" +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/relative-paths/child/nested/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/relative-paths/child/nested/main.tf new file mode 100644 index 000000000000..45aab16e2bda --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/relative-paths/child/nested/main.tf @@ -0,0 +1,7 @@ +resource "aws_s3_bucket" "two" { + +} + +module "module_in_parent_dir" { + source = "../../parent/" +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/relative-paths/parent/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/relative-paths/parent/main.tf new file mode 100644 index 000000000000..7d6126ed3c8e --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/relative-paths/parent/main.tf @@ -0,0 +1,3 @@ +resource "aws_s3_bucket" "three" { + +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/single-failure/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/single-failure/main.tf new file mode 100644 index 000000000000..4a0a3a1fcd29 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/single-failure/main.tf @@ -0,0 +1,3 @@ +resource "aws_s3_bucket" "asd" { + +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/tfvar-outside/main.tfvars b/pkg/fanal/artifact/local/testdata/misconfig/terraform/tfvar-outside/main.tfvars new file mode 100644 index 000000000000..6eb0b00dcbd0 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/tfvar-outside/main.tfvars @@ -0,0 +1 @@ +bucket_name = "test" diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraform/tfvar-outside/tf/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraform/tfvar-outside/tf/main.tf new file mode 100644 index 000000000000..d9e4a4745359 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraform/tfvar-outside/tf/main.tf @@ -0,0 +1,7 @@ +resource "aws_s3_bucket" "this" { + bucket = var.bucket_name +} + +variable "bucket_name" { + type = string +} diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/multiple-failures/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/multiple-failures/main.tf new file mode 100644 index 000000000000..9eb5363beea5 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/multiple-failures/main.tf @@ -0,0 +1,16 @@ +terraform { + required_providers { + aws = { + source = "aws" + version = "5.35.0" + } + } +} + +resource "aws_s3_bucket" "one" { + +} + +resource "aws_s3_bucket" "two" { + +} diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/multiple-failures/more.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/multiple-failures/more.tf new file mode 100644 index 000000000000..e5cc0c1ea195 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/multiple-failures/more.tf @@ -0,0 +1,3 @@ +resource "aws_s3_bucket" "three" { + +} diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/multiple-failures/tfplan b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/multiple-failures/tfplan new file mode 100644 index 000000000000..80b80a786b3d Binary files /dev/null and b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/multiple-failures/tfplan differ diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/passed/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/passed/main.tf new file mode 100644 index 000000000000..64e9f0d2b965 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/passed/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + aws = { + source = "aws" + version = "5.35.0" + } + } +} + +resource "aws_s3_bucket" "this" { + bucket = "test-bucket" +} diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/passed/tfplan b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/passed/tfplan new file mode 100644 index 000000000000..ee00c44a36b3 Binary files /dev/null and b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/passed/tfplan differ diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/single-failure/main.tf b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/single-failure/main.tf new file mode 100644 index 000000000000..156495110856 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/single-failure/main.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + aws = { + source = "aws" + version = "5.35.0" + } + } +} + +resource "aws_s3_bucket" "this" { + +} diff --git a/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/single-failure/tfplan b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/single-failure/tfplan new file mode 100644 index 000000000000..c07b8d1757f5 Binary files /dev/null and b/pkg/fanal/artifact/local/testdata/misconfig/terraformplan/snapshots/single-failure/tfplan differ diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/passed/checks/test.rego b/pkg/fanal/artifact/local/testdata/misconfig/yaml/passed/checks/test.rego new file mode 100644 index 000000000000..2c417a40c0bb --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/passed/checks/test.rego @@ -0,0 +1,20 @@ +# METADATA +# title: Test check +# custom: +# id: TEST001 +# avd_id: TEST001 +# severity: LOW +# input: +# selector: +# - type: yaml +package user.test_yaml_check + +deny[res] { + input.service == "foo" + res := result.new(`Service "foo" should not be used`, input.service) +} + +deny[res] { + input.provider == "bar" + res := result.new(`Provider "bar" should not be used`, input.provider) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/passed/src/test1.yaml b/pkg/fanal/artifact/local/testdata/misconfig/yaml/passed/src/test1.yaml new file mode 100644 index 000000000000..18d05964b79b --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/passed/src/test1.yaml @@ -0,0 +1 @@ +service: foo \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/passed/src/test2.yml b/pkg/fanal/artifact/local/testdata/misconfig/yaml/passed/src/test2.yml new file mode 100644 index 000000000000..067ebe333ddf --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/passed/src/test2.yml @@ -0,0 +1 @@ +provider: bar \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/checks/test.json b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/checks/test.json new file mode 100644 index 000000000000..f72cdb0d2016 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/checks/test.json @@ -0,0 +1,9 @@ +{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "service": { "type": "string" } + }, + "required": ["service"] +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/checks/test_1_error.rego b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/checks/test_1_error.rego new file mode 100644 index 000000000000..316d834674f7 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/checks/test_1_error.rego @@ -0,0 +1,13 @@ +# METADATA +# title: Test check +# schemas: +# - input: schema.test +# custom: +# id: TEST001 +# severity: LOW +package user.test_yaml_check + +deny[res] { + input.wrong_ref == "foo" + res := result.new(`Service "foo" should not be used`, input.service) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/checks/test_2_errors.rego b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/checks/test_2_errors.rego new file mode 100644 index 000000000000..f4651d2a249a --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/checks/test_2_errors.rego @@ -0,0 +1,18 @@ +# METADATA +# title: Test check +# schemas: +# - input: schema.test +# custom: +# id: TEST001 +# severity: LOW +package user.test_yaml_check + +deny[res] { + input.wrong_ref == "foo" + res := result.new(`Service "foo" should not be used`, input.service) +} + +deny[res] { + input.wrong_ref == "bar" + res := result.new(`Provider "bar" should not be used`, input.provider) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/src/test1.yaml b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/src/test1.yaml new file mode 100644 index 000000000000..18d05964b79b --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-rego-error-limit/src/test1.yaml @@ -0,0 +1 @@ +service: foo \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/checks/test.rego b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/checks/test.rego new file mode 100644 index 000000000000..2c417a40c0bb --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/checks/test.rego @@ -0,0 +1,20 @@ +# METADATA +# title: Test check +# custom: +# id: TEST001 +# avd_id: TEST001 +# severity: LOW +# input: +# selector: +# - type: yaml +package user.test_yaml_check + +deny[res] { + input.service == "foo" + res := result.new(`Service "foo" should not be used`, input.service) +} + +deny[res] { + input.provider == "bar" + res := result.new(`Provider "bar" should not be used`, input.provider) +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/schemas/test.json b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/schemas/test.json new file mode 100644 index 000000000000..f72cdb0d2016 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/schemas/test.json @@ -0,0 +1,9 @@ +{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "service": { "type": "string" } + }, + "required": ["service"] +} \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/src/test1.yaml b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/src/test1.yaml new file mode 100644 index 000000000000..18d05964b79b --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/src/test1.yaml @@ -0,0 +1 @@ +service: foo \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/src/test2.yml b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/src/test2.yml new file mode 100644 index 000000000000..067ebe333ddf --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/misconfig/yaml/with-schema/src/test2.yml @@ -0,0 +1 @@ +provider: bar \ No newline at end of file diff --git a/pkg/fanal/artifact/local/testdata/requirements.txt b/pkg/fanal/artifact/local/testdata/requirements.txt new file mode 100644 index 000000000000..2e0479535644 --- /dev/null +++ b/pkg/fanal/artifact/local/testdata/requirements.txt @@ -0,0 +1 @@ +Flask==2.0.0 diff --git a/pkg/fanal/artifact/repo/git.go b/pkg/fanal/artifact/repo/git.go new file mode 100644 index 000000000000..804f675049f5 --- /dev/null +++ b/pkg/fanal/artifact/repo/git.go @@ -0,0 +1,183 @@ +package repo + +import ( + "net/url" + "os" + + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/transport/http" + "github.com/hashicorp/go-multierror" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/local" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +var _ Walker = (*walker.FS)(nil) + +type Walker interface { + Walk(root string, opt walker.Option, fn walker.WalkFunc) error +} + +func NewArtifact(target string, c cache.ArtifactCache, w Walker, artifactOpt artifact.Option) (artifact.Artifact, func(), error) { + var cleanup func() + var errs error + + artifactOpt.Type = types.TypeRepository + + // Try the local repository + art, err := tryLocalRepo(target, c, w, artifactOpt) + if err == nil { + return art, func() {}, nil + } + errs = multierror.Append(errs, err) + + // Try the remote git repository + art, cleanup, err = tryRemoteRepo(target, c, w, artifactOpt) + if err == nil { + return art, cleanup, nil + } + errs = multierror.Append(errs, err) + + // Return errors + return nil, cleanup, errs +} + +func tryLocalRepo(target string, c cache.ArtifactCache, w Walker, artifactOpt artifact.Option) (artifact.Artifact, error) { + if _, err := os.Stat(target); err != nil { + return nil, xerrors.Errorf("no such path: %w", err) + } + + art, err := local.NewArtifact(target, c, w, artifactOpt) + if err != nil { + return nil, xerrors.Errorf("local repo artifact error: %w", err) + } + return art, nil +} + +func tryRemoteRepo(target string, c cache.ArtifactCache, w Walker, artifactOpt artifact.Option) (artifact.Artifact, func(), error) { + cleanup := func() {} + u, err := newURL(target) + if err != nil { + return nil, cleanup, err + } + + tmpDir, err := cloneRepo(u, artifactOpt) + if err != nil { + return nil, cleanup, xerrors.Errorf("repository clone error: %w", err) + } + + cleanup = func() { _ = os.RemoveAll(tmpDir) } + + artifactOpt.Original = target + art, err := local.NewArtifact(tmpDir, c, w, artifactOpt) + if err != nil { + return nil, cleanup, xerrors.Errorf("fs artifact: %w", err) + } + + return art, cleanup, nil + +} + +func cloneRepo(u *url.URL, artifactOpt artifact.Option) (string, error) { + tmpDir, err := xos.MkdirTemp("", "git-clone-") + if err != nil { + return "", xerrors.Errorf("failed to create a temp dir: %w", err) + } + + cloneOptions := git.CloneOptions{ + URL: u.String(), + Auth: gitAuth(), + Progress: os.Stderr, + InsecureSkipTLS: artifactOpt.Insecure, + } + + // suppress clone output if noProgress + if artifactOpt.NoProgress { + cloneOptions.Progress = nil + } + + if artifactOpt.RepoCommit == "" { + cloneOptions.Depth = 1 + } + + if artifactOpt.RepoBranch != "" { + cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(artifactOpt.RepoBranch) + cloneOptions.SingleBranch = true + } + + if artifactOpt.RepoTag != "" { + cloneOptions.ReferenceName = plumbing.NewTagReferenceName(artifactOpt.RepoTag) + cloneOptions.SingleBranch = true + } + + r, err := git.PlainClone(tmpDir, false, &cloneOptions) + if err != nil { + return "", xerrors.Errorf("git clone error: %w", err) + } + + if artifactOpt.RepoCommit != "" { + w, err := r.Worktree() + if err != nil { + return "", xerrors.Errorf("git worktree error: %w", err) + } + + err = w.Checkout(&git.CheckoutOptions{ + Hash: plumbing.NewHash(artifactOpt.RepoCommit), + }) + if err != nil { + return "", xerrors.Errorf("git checkout error: %w", err) + } + } + + return tmpDir, nil +} + +func newURL(rawurl string) (*url.URL, error) { + u, err := url.Parse(rawurl) + if err != nil { + return nil, xerrors.Errorf("url parse error: %w", err) + } + // "https://" can be omitted + // e.g. github.com/aquasecurity/trivy + if u.Scheme == "" { + u.Scheme = "https" + } + + return u, nil +} + +// Helper function to check for a GitHub/GitLab token from env vars in order to +// make authenticated requests to access private repos +func gitAuth() http.AuthMethod { + // The username can be anything for HTTPS Git operations + gitUsername := "fanal-aquasecurity-scan" + + // We first check if a GitHub token was provided + githubToken := os.Getenv("GITHUB_TOKEN") + if githubToken != "" { + return &http.BasicAuth{ + Username: gitUsername, + Password: githubToken, + } + } + + // Otherwise we check if a GitLab token was provided + gitlabToken := os.Getenv("GITLAB_TOKEN") + if gitlabToken != "" { + return &http.BasicAuth{ + Username: gitUsername, + Password: gitlabToken, + } + } + + // If no token was provided, we simply return a nil, + // which will make the request to be unauthenticated + return nil + +} diff --git a/pkg/fanal/artifact/repo/git_test.go b/pkg/fanal/artifact/repo/git_test.go new file mode 100644 index 000000000000..166e127ab06f --- /dev/null +++ b/pkg/fanal/artifact/repo/git_test.go @@ -0,0 +1,550 @@ +//go:build unix + +package repo + +import ( + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/go-git/go-git/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/gittest" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/uuid" + + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/config/all" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/secret" +) + +func TestNewArtifact(t *testing.T) { + ts := gittest.NewTestServer(t, gittest.Options{}) + defer ts.Close() + + type args struct { + target string + c cache.ArtifactCache + noProgress bool + repoBranch string + repoTag string + repoCommit string + } + tests := []struct { + name string + args args + assertion assert.ErrorAssertionFunc + }{ + { + name: "remote repo", + args: args{ + target: ts.URL + "/test-repo.git", + c: nil, + noProgress: false, + }, + assertion: assert.NoError, + }, + { + name: "local repo", + args: args{ + target: "../../../../internal/gittest/testdata/test-repo", + c: nil, + noProgress: false, + }, + assertion: assert.NoError, + }, + { + name: "no progress", + args: args{ + target: ts.URL + "/test-repo.git", + c: nil, + noProgress: true, + }, + assertion: assert.NoError, + }, + { + name: "branch", + args: args{ + target: ts.URL + "/test-repo.git", + c: nil, + repoBranch: "valid-branch", + }, + assertion: assert.NoError, + }, + { + name: "tag", + args: args{ + target: ts.URL + "/test-repo.git", + c: nil, + repoTag: "v0.0.1", + }, + assertion: assert.NoError, + }, + { + name: "commit", + args: args{ + target: ts.URL + "/test-repo.git", + c: nil, + repoCommit: "8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35", + }, + assertion: assert.NoError, + }, + { + name: "sad path", + args: args{ + target: ts.URL + "/unknown.git", + c: nil, + noProgress: false, + }, + assertion: func(t assert.TestingT, err error, _ ...any) bool { + return assert.ErrorContains(t, err, "repository not found") + }, + }, + { + name: "invalid url", + args: args{ + target: "ht tp://foo.com", + c: nil, + noProgress: false, + }, + assertion: func(t assert.TestingT, err error, _ ...any) bool { + return assert.ErrorContains(t, err, "url parse error") + }, + }, + { + name: "invalid branch", + args: args{ + target: ts.URL + "/test-repo.git", + c: nil, + repoBranch: "invalid-branch", + }, + assertion: func(t assert.TestingT, err error, _ ...any) bool { + return assert.ErrorContains(t, err, `couldn't find remote ref "refs/heads/invalid-branch"`) + }, + }, + { + name: "invalid tag", + args: args{ + target: ts.URL + "/test-repo.git", + c: nil, + repoTag: "v1.0.9", + }, + assertion: func(t assert.TestingT, err error, _ ...any) bool { + return assert.ErrorContains(t, err, `couldn't find remote ref "refs/tags/v1.0.9"`) + }, + }, + { + name: "invalid commit", + args: args{ + target: ts.URL + "/test-repo.git", + c: nil, + repoCommit: "6ac152fe2b87cb5e243414df71790a32912e778e", + }, + assertion: func(t assert.TestingT, err error, _ ...any) bool { + return assert.ErrorContains(t, err, "git checkout error: object not found") + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, cleanup, err := NewArtifact(tt.args.target, tt.args.c, walker.NewFS(), artifact.Option{ + NoProgress: tt.args.noProgress, + RepoBranch: tt.args.repoBranch, + RepoTag: tt.args.repoTag, + RepoCommit: tt.args.repoCommit, + }) + tt.assertion(t, err) + defer cleanup() + }) + } +} + +func TestArtifact_Inspect(t *testing.T) { + ts := gittest.NewTestServer(t, gittest.Options{}) + defer ts.Close() + + tests := []struct { + name string + rawurl string + setup func(t *testing.T, dir string, c cache.ArtifactCache) + want artifact.Reference + wantBlobInfo *types.BlobInfo + wantErr bool + }{ + { + name: "remote repo", + rawurl: ts.URL + "/test-repo.git", + want: artifact.Reference{ + Name: ts.URL + "/test-repo.git", + Type: types.TypeRepository, + ID: "sha256:1587f4be90cf95b3e1b733512d674301f5fe4200055f10efa4dbf0d5e590d32d", // Calculated from commit hash + BlobIDs: []string{ + "sha256:1587f4be90cf95b3e1b733512d674301f5fe4200055f10efa4dbf0d5e590d32d", // Calculated from commit hash + }, + RepoMetadata: artifact.RepoMetadata{ + RepoURL: ts.URL + "/test-repo.git", + Branch: "main", + Tags: []string{"v0.0.1"}, + Commit: "8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35", + CommitMsg: "Update README.md", + Author: "Teppei Fukuda ", + Committer: "GitHub ", + }, + }, + wantBlobInfo: &types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + { + name: "local repo", + rawurl: "../../../../internal/gittest/testdata/test-repo", + want: artifact.Reference{ + Name: "../../../../internal/gittest/testdata/test-repo", + Type: types.TypeRepository, + ID: "sha256:1587f4be90cf95b3e1b733512d674301f5fe4200055f10efa4dbf0d5e590d32d", // Calculated from commit hash + BlobIDs: []string{ + "sha256:1587f4be90cf95b3e1b733512d674301f5fe4200055f10efa4dbf0d5e590d32d", // Calculated from commit hash + }, + RepoMetadata: artifact.RepoMetadata{ + RepoURL: "https://github.com/aquasecurity/trivy-test-repo/", + Branch: "main", + Tags: []string{"v0.0.1"}, + Commit: "8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35", + CommitMsg: "Update README.md", + Author: "Teppei Fukuda ", + Committer: "GitHub ", + }, + }, + wantBlobInfo: &types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + { + name: "dirty repository", + rawurl: "../../../../internal/gittest/testdata/test-repo", + setup: func(t *testing.T, dir string, _ cache.ArtifactCache) { + require.NoError(t, os.WriteFile(filepath.Join(dir, "new-file.txt"), []byte("test"), 0o644)) + t.Cleanup(func() { + require.NoError(t, os.Remove(filepath.Join(dir, "new-file.txt"))) + }) + }, + want: artifact.Reference{ + Name: "../../../../internal/gittest/testdata/test-repo", + Type: types.TypeRepository, + ID: "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + BlobIDs: []string{ + "sha256:6f4672e139d4066fd00391df614cdf42bda5f7a3f005d39e1d8600be86157098", + }, + RepoMetadata: artifact.RepoMetadata{ + RepoURL: "https://github.com/aquasecurity/trivy-test-repo/", + Branch: "main", + Tags: []string{"v0.0.1"}, + Commit: "8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35", + CommitMsg: "Update README.md", + Author: "Teppei Fukuda ", + Committer: "GitHub ", + }, + }, + wantBlobInfo: &types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + }, + }, + { + name: "cache hit", + rawurl: "../../../../internal/gittest/testdata/test-repo", + setup: func(t *testing.T, _ string, c cache.ArtifactCache) { + blobInfo := types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: types.OS{ + Family: types.Alpine, + Name: "3.16.0", + }, + } + // Store the blob info in the cache to test cache hit + cacheKey := "sha256:1587f4be90cf95b3e1b733512d674301f5fe4200055f10efa4dbf0d5e590d32d" + err := c.PutBlob(t.Context(), cacheKey, blobInfo) + require.NoError(t, err) + }, + want: artifact.Reference{ + Name: "../../../../internal/gittest/testdata/test-repo", + Type: types.TypeRepository, + ID: "sha256:1587f4be90cf95b3e1b733512d674301f5fe4200055f10efa4dbf0d5e590d32d", + BlobIDs: []string{ + "sha256:1587f4be90cf95b3e1b733512d674301f5fe4200055f10efa4dbf0d5e590d32d", + }, + RepoMetadata: artifact.RepoMetadata{ + RepoURL: "https://github.com/aquasecurity/trivy-test-repo/", + Branch: "main", + Tags: []string{"v0.0.1"}, + Commit: "8a19b492a589955c3e70c6ad8efd1e4ec6ae0d35", + CommitMsg: "Update README.md", + Author: "Teppei Fukuda ", + Committer: "GitHub ", + }, + }, + wantBlobInfo: &types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: types.OS{ + Family: types.Alpine, + Name: "3.16.0", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID for consistency + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + // Create memory cache + c := cache.NewMemoryCache() + + // Apply setup if specified + if tt.setup != nil { + tt.setup(t, tt.rawurl, c) + } + + art, cleanup, err := NewArtifact(tt.rawurl, c, walker.NewFS(), artifact.Option{}) + require.NoError(t, err) + defer cleanup() + + ref, err := art.Inspect(t.Context()) + if tt.wantErr { + assert.Error(t, err) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, ref) + + // Verify cache contents after inspection + blobInfo, err := c.GetBlob(t.Context(), tt.want.BlobIDs[0]) + require.NoError(t, err) + assert.Equal(t, tt.wantBlobInfo, &blobInfo, "cache content mismatch") + }) + } +} + +// setupAuthTestServer creates a test server with authentication and returns parsed URL with /test-repo.git path +func setupAuthTestServer(t *testing.T, username, password string) *url.URL { + t.Helper() + ts := gittest.NewTestServer(t, gittest.Options{ + Username: username, + Password: password, + }) + t.Cleanup(ts.Close) + + tsURL, err := url.Parse(ts.URL) + require.NoError(t, err) + tsURL.Path = "/test-repo.git" + + return tsURL +} + +// testInspectArtifact is a helper function to inspect an artifact and assert the results +func testInspectArtifact(t *testing.T, target, wantRepoURL, wantErr string) { + t.Helper() + art, cleanup, err := NewArtifact(target, cache.NewMemoryCache(), walker.NewFS(), artifact.Option{}) + t.Cleanup(cleanup) + + if wantErr != "" { + require.ErrorContains(t, err, wantErr) + return + } + require.NoError(t, err) + + // Verify Inspect works + ref, err := art.Inspect(t.Context()) + require.NoError(t, err) + + // Verify the RepoURL + assert.Equal(t, wantRepoURL, ref.RepoMetadata.RepoURL) + + // Verify we have blob IDs (indicating successful scan) + assert.NotEmpty(t, ref.BlobIDs) +} + +func TestArtifact_InspectWithAuth(t *testing.T) { + const ( + testUsername = "testuser" + testPassword = "testpass" + ) + + // Test with environment variable authentication (GITHUB_TOKEN, GITLAB_TOKEN) + t.Run("environment variable authentication", func(t *testing.T) { + const testGitUsername = "fanal-aquasecurity-scan" // This is the username used by Trivy + + // Setup test server with authentication + tsURL := setupAuthTestServer(t, testGitUsername, testPassword) + + tests := []struct { + name string + target string + envVars map[string]string + wantErr string + wantRepoURL string + }{ + { + name: "success with GITHUB_TOKEN", + target: tsURL.String(), + envVars: map[string]string{ + "GITHUB_TOKEN": testPassword, + }, + wantRepoURL: tsURL.String(), + }, + { + name: "success with GITLAB_TOKEN", + target: tsURL.String(), + envVars: map[string]string{ + "GITLAB_TOKEN": testPassword, + }, + wantRepoURL: tsURL.String(), + }, + { + name: "failure without token", + target: tsURL.String(), + wantErr: "authentication required", + }, + { + name: "failure with wrong token", + target: tsURL.String(), + envVars: map[string]string{ + "GITHUB_TOKEN": "wrongpassword", + }, + wantErr: "authentication required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set test environment variables + for key, value := range tt.envVars { + t.Setenv(key, value) + } + + // Test using helper function + testInspectArtifact(t, tt.target, tt.wantRepoURL, tt.wantErr) + }) + } + }) + + // Test with URL-embedded authentication + t.Run("URL embedded authentication", func(t *testing.T) { + // Setup test server with authentication + tsURL := setupAuthTestServer(t, testUsername, testPassword) + + // Helper function to generate target URL with credentials + makeTarget := func(username, password string) string { + u := *tsURL // Copy the URL + if username != "" && password != "" { + u.User = url.UserPassword(username, password) + } + return u.String() + } + + tests := []struct { + name string + target string + wantRepoURL string + wantErr string + }{ + { + name: "success with embedded credentials", + target: makeTarget(testUsername, testPassword), + wantRepoURL: tsURL.String(), + }, + { + name: "failure with wrong password", + target: makeTarget(testUsername, "wrongpass"), + wantErr: "authentication required", + }, + { + name: "failure without credentials", + target: makeTarget("", ""), + wantErr: "authentication required", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test using helper function + testInspectArtifact(t, tt.target, tt.wantRepoURL, tt.wantErr) + }) + } + }) + + // Test cloning with embedded credentials and then scanning the local directory + t.Run("clone with credentials then scan local", func(t *testing.T) { + // Setup test server with authentication + tsURL := setupAuthTestServer(t, testUsername, testPassword) + + // Add credentials to URL + u := *tsURL // Copy the URL + u.User = url.UserPassword(testUsername, testPassword) + targetWithCreds := u.String() + + // Clone the repository with URL-embedded credentials + cloneDir := filepath.Join(t.TempDir(), "cloned-repo") + + // Use go-git directly to clone with URL-embedded credentials + _, err := git.PlainClone(cloneDir, false, &git.CloneOptions{ + URL: targetWithCreds, + }) + require.NoError(t, err) + + // Scan and verify the local cloned directory + testInspectArtifact(t, cloneDir, tsURL.String(), "") + }) +} + +func Test_newURL(t *testing.T) { + type args struct { + rawurl string + } + tests := []struct { + name string + args args + want string + wantErr string + }{ + { + name: "happy path", + args: args{ + rawurl: "https://github.com/aquasecurity/fanal", + }, + want: "https://github.com/aquasecurity/fanal", + }, + { + name: "happy path: no scheme", + args: args{ + rawurl: "github.com/aquasecurity/fanal", + }, + want: "https://github.com/aquasecurity/fanal", + }, + { + name: "sad path: invalid url", + args: args{ + rawurl: "ht tp://foo.com", + }, + wantErr: "first path segment in URL cannot contain colon", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := newURL(tt.args.rawurl) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got.String()) + }) + } +} diff --git a/pkg/fanal/artifact/sbom/sbom.go b/pkg/fanal/artifact/sbom/sbom.go new file mode 100644 index 000000000000..f6377b44a033 --- /dev/null +++ b/pkg/fanal/artifact/sbom/sbom.go @@ -0,0 +1,123 @@ +package sbom + +import ( + "context" + "crypto/sha256" + "encoding/json" + "os" + "path/filepath" + + "github.com/opencontainers/go-digest" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom" +) + +const artifactVersion = 0 + +type Artifact struct { + filePath string + cache cache.ArtifactCache + analyzer analyzer.AnalyzerGroup + handlerManager handler.Manager + + artifactOption artifact.Option +} + +func NewArtifact(filePath string, c cache.ArtifactCache, opt artifact.Option) (artifact.Artifact, error) { + return Artifact{ + filePath: filepath.Clean(filePath), + cache: c, + artifactOption: opt, + }, nil +} + +func (a Artifact) Inspect(ctx context.Context) (artifact.Reference, error) { + f, err := os.Open(a.filePath) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to open sbom file error: %w", err) + } + defer f.Close() + + // Format auto-detection + format, err := sbom.DetectFormat(f) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to detect SBOM format: %w", err) + } + log.Info("Detected SBOM format", log.String("format", string(format))) + + ctx = log.WithContextAttrs(ctx, log.FilePath(a.filePath)) + bom, err := sbom.Decode(ctx, f, format) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("SBOM decode error: %w", err) + } + + blobInfo := types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: lo.FromPtr(bom.Metadata.OS), + PackageInfos: bom.Packages, + Applications: bom.Applications, + } + + cacheKey, err := a.calcCacheKey(blobInfo) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to calculate a cache key: %w", err) + } + + if err = a.cache.PutBlob(ctx, cacheKey, blobInfo); err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to store blob (%s) in cache: %w", cacheKey, err) + } + + var artifactType types.ArtifactType + switch format { + case sbom.FormatCycloneDXJSON, sbom.FormatCycloneDXXML, sbom.FormatAttestCycloneDXJSON, + sbom.FormatLegacyCosignAttestCycloneDXJSON, sbom.FormatSigstoreBundleCycloneDXJSON: + artifactType = types.TypeCycloneDX + case sbom.FormatSPDXTV, sbom.FormatSPDXJSON, sbom.FormatAttestSPDXJSON, sbom.FormatSigstoreBundleSPDXJSON: + artifactType = types.TypeSPDX + } + + return artifact.Reference{ + Name: a.filePath, + Type: artifactType, + ID: cacheKey, // use a cache key as pseudo artifact ID + BlobIDs: []string{cacheKey}, + ImageMetadata: artifact.ImageMetadata{ + ID: bom.Metadata.ImageID, + DiffIDs: bom.Metadata.DiffIDs, + RepoTags: bom.Metadata.RepoTags, + RepoDigests: bom.Metadata.RepoDigests, + Reference: bom.Metadata.Reference, + }, + + // Keep an original report + BOM: bom.BOM, + }, nil +} + +func (a Artifact) Clean(reference artifact.Reference) error { + return a.cache.DeleteBlobs(context.TODO(), reference.BlobIDs) +} + +func (a Artifact) calcCacheKey(blobInfo types.BlobInfo) (string, error) { + // calculate hash of JSON and use it as pseudo artifactID and blobID + h := sha256.New() + if err := json.NewEncoder(h).Encode(blobInfo); err != nil { + return "", xerrors.Errorf("json error: %w", err) + } + + d := digest.NewDigest(digest.SHA256, h) + cacheKey, err := cache.CalcKey(d.String(), artifactVersion, a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption) + if err != nil { + return "", xerrors.Errorf("cache key: %w", err) + } + + return cacheKey, nil +} diff --git a/pkg/fanal/artifact/sbom/sbom_test.go b/pkg/fanal/artifact/sbom/sbom_test.go new file mode 100644 index 000000000000..9b1c3c64683a --- /dev/null +++ b/pkg/fanal/artifact/sbom/sbom_test.go @@ -0,0 +1,523 @@ +package sbom_test + +import ( + "path/filepath" + "strings" + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/cachetest" + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/sbom" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestArtifact_Inspect(t *testing.T) { + tests := []struct { + name string + filePath string + setUpCache func(t *testing.T) cache.Cache + wantBlobs []cachetest.WantBlob + want artifact.Reference + wantErr []string + }{ + { + name: "happy path", + filePath: filepath.Join("testdata", "bom.json"), + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: types.OS{ + Family: "alpine", + Name: "3.16.0", + }, + PackageInfos: []types.PackageInfo{ + { + Packages: types.Packages{ + { + ID: "musl@1.2.3-r0", + Name: "musl", + Version: "1.2.3-r0", + SrcName: "musl", + SrcVersion: "1.2.3-r0", + Licenses: []string{"MIT"}, + Layer: types.Layer{ + DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.2.3-r0", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.16.0", + }, + }, + }, + BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + }, + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "app/composer/composer.lock", + Packages: types.Packages{ + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + BOMRef: "pkg:composer/pear/log@1.13.1", + }, + }, + { + ID: "pear/pear_exception@v1.0.0", + Name: "pear/pear_exception", + Version: "v1.0.0", + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "pear_exception", + Version: "v1.0.0", + }, + BOMRef: "pkg:composer/pear/pear_exception@v1.0.0", + }, + }, + }, + }, + { + Type: "gobinary", + FilePath: "app/gobinary/gobinary", + Packages: types.Packages{ + { + ID: "github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + Name: "github.com/package-url/packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/package-url", + Name: "packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + }, + BOMRef: "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + }, + }, + }, + }, + { + Type: "jar", + FilePath: "", + Packages: types.Packages{ + { + ID: "org.codehaus.mojo:child-project:1.0", + Name: "org.codehaus.mojo:child-project", + Version: "1.0", + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + FilePath: "app/maven/target/child-project-1.0.jar", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.codehaus.mojo", + Name: "child-project", + Version: "1.0", + }, + // Keep the original value + BOMRef: "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + }, + }, + }, + }, + { + Type: "node-pkg", + FilePath: "", + Packages: types.Packages{ + { + ID: "bootstrap@5.0.2", + Name: "bootstrap", + Version: "5.0.2", + Licenses: []string{"MIT"}, + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + FilePath: "app/app/package.json", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "bootstrap", + Version: "5.0.2", + }, + // Keep the original value + BOMRef: "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: filepath.Join("testdata", "bom.json"), + Type: types.TypeCycloneDX, + ID: "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca", + BlobIDs: []string{ + "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca", + }, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5", + DiffIDs: []string{ + "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + RepoTags: []string{ + "maven-test-project:latest", + }, + Reference: testutil.MustParseReference(t, "maven-test-project:latest"), + }, + }, + }, + { + name: "happy path for sbom attestation", + filePath: filepath.Join("testdata", "sbom.cdx.intoto.jsonl"), + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: types.OS{ + Family: "alpine", + Name: "3.16.0", + }, + PackageInfos: []types.PackageInfo{ + { + Packages: types.Packages{ + { + ID: "musl@1.2.3-r0", + Name: "musl", + Version: "1.2.3-r0", + SrcName: "musl", + SrcVersion: "1.2.3-r0", + Licenses: []string{"MIT"}, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.2.3-r0", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.16.0", + }, + }, + }, + BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + }, + Layer: types.Layer{ + DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "app/composer/composer.lock", + Packages: types.Packages{ + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + BOMRef: "pkg:composer/pear/log@1.13.1", + }, + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + { + ID: "pear/pear_exception@v1.0.0", + Name: "pear/pear_exception", + Version: "v1.0.0", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "pear_exception", + Version: "v1.0.0", + }, + BOMRef: "pkg:composer/pear/pear_exception@v1.0.0", + }, + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "gobinary", + FilePath: "app/gobinary/gobinary", + Packages: types.Packages{ + { + ID: "github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + Name: "github.com/package-url/packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/package-url", + Name: "packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + }, + BOMRef: "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + }, + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "jar", + FilePath: "", + Packages: types.Packages{ + { + ID: "org.codehaus.mojo:child-project:1.0", + Name: "org.codehaus.mojo:child-project", + Version: "1.0", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.codehaus.mojo", + Name: "child-project", + Version: "1.0", + }, + // Keep the original value + BOMRef: "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + }, + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + FilePath: "app/maven/target/child-project-1.0.jar", + }, + }, + }, + { + Type: "node-pkg", + FilePath: "", + Packages: types.Packages{ + { + ID: "bootstrap@5.0.2", + Name: "bootstrap", + Version: "5.0.2", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "bootstrap", + Version: "5.0.2", + }, + // Keep the original value + BOMRef: "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + }, + Licenses: []string{"MIT"}, + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + FilePath: "app/app/package.json", + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: filepath.Join("testdata", "sbom.cdx.intoto.jsonl"), + Type: types.TypeCycloneDX, + ID: "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca", + BlobIDs: []string{ + "sha256:76bc49ae239d24c6a122e730bafb9d5295d0af380492aeb92a3bf34bea3a14ca", + }, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5", + DiffIDs: []string{ + "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + RepoTags: []string{ + "maven-test-project:latest", + }, + }, + }, + }, + { + name: "components with missing BOM-REF", + filePath: filepath.Join("testdata", "bom-missing-refs.json"), + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:512b9e999c9d7b4880c63ce55c2c74ea5c22b05cdbcb486097a16ec692c746a0", + BlobInfo: types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: types.OS{ + Family: "alpine", + Name: "3.16.0", + }, + PackageInfos: []types.PackageInfo{ + { + Packages: types.Packages{ + { + ID: "musl@1.2.3-r0", + Name: "musl", + Version: "1.2.3-r0", + SrcName: "musl", + SrcVersion: "1.2.3-r0", + Licenses: []string{"MIT"}, + Layer: types.Layer{ + DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.2.3-r0", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.16.0", + }, + }, + }, + // BOM-Ref should be auto-generated from PURL + BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + }, + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: "composer", + FilePath: "", + Packages: types.Packages{ + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Layer: types.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + // BOM-Ref should be auto-generated from PURL + BOMRef: "pkg:composer/pear/log@1.13.1", + }, + }, + }, + }, + }, + }, + }, + }, + want: artifact.Reference{ + Name: filepath.Join("testdata", "bom-missing-refs.json"), + Type: types.TypeCycloneDX, + ID: "sha256:512b9e999c9d7b4880c63ce55c2c74ea5c22b05cdbcb486097a16ec692c746a0", + BlobIDs: []string{ + "sha256:512b9e999c9d7b4880c63ce55c2c74ea5c22b05cdbcb486097a16ec692c746a0", + }, + }, + }, + { + name: "sad path with no such directory", + filePath: filepath.Join("testdata", "unknown.json"), + wantErr: []string{ + "no such file or directory", + "The system cannot find the file specified", + }, + }, + { + name: "sad path PutBlob returns an error", + filePath: filepath.Join("testdata", "os-only-bom.json"), + setUpCache: func(_ *testing.T) cache.Cache { + return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{ + PutBlob: true, + }) + }, + wantErr: []string{"failed to store blob"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cachetest.NewCache(t, tt.setUpCache) + + a, err := sbom.NewArtifact(tt.filePath, c, artifact.Option{}) + require.NoError(t, err) + + got, err := a.Inspect(t.Context()) + if len(tt.wantErr) > 0 { + require.Error(t, err) + found := false + for _, wantErr := range tt.wantErr { + if strings.Contains(err.Error(), wantErr) { + found = true + break + } + } + assert.True(t, found) + return + } + + // Not compare the original CycloneDX report + got.BOM = nil + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} diff --git a/pkg/fanal/artifact/sbom/testdata/bom-missing-refs.json b/pkg/fanal/artifact/sbom/testdata/bom-missing-refs.json new file mode 100644 index 000000000000..949c89644971 --- /dev/null +++ b/pkg/fanal/artifact/sbom/testdata/bom-missing-refs.json @@ -0,0 +1,89 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "type": "library", + "name": "musl", + "version": "1.2.3-r0", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + } + ] + }, + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + }, + { + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + } + ], + "dependencies": [ + { + "ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182" + }, + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "dependsOn": [ + "60e9f57b-d4a6-4f71-ad14-0893ac609182" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/fanal/artifact/sbom/testdata/bom.json b/pkg/fanal/artifact/sbom/testdata/bom.json new file mode 100644 index 000000000000..b5645eca53e2 --- /dev/null +++ b/pkg/fanal/artifact/sbom/testdata/bom.json @@ -0,0 +1,242 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + }, + { + "name": "aquasecurity:trivy:Reference", + "value": "maven-test-project:latest" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "type": "library", + "name": "musl", + "version": "1.2.3-r0", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "musl" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.2.3-r0" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + } + ] + }, + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + }, + { + "bom-ref": "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + "type": "library", + "name": "org.codehaus.mojo:child-project", + "version": "1.0", + "purl": "pkg:maven/org.codehaus.mojo/child-project@1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "app/maven/target/child-project-1.0.jar" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "jar" + } + ] + }, + { + "bom-ref": "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "type": "library", + "name": "bootstrap", + "version": "5.0.2", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:npm/bootstrap@5.0.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "app/app/package.json" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "node-pkg" + } + ] + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "pkg:composer/pear/pear_exception@v1.0.0", + "type": "library", + "name": "pear/pear_exception", + "version": "v1.0.0", + "purl": "pkg:composer/pear/pear_exception@v1.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "type": "application", + "name": "app/composer/composer.lock", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "composer" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "type": "library", + "name": "github.com/package-url/packageurl-go", + "version": "v0.1.1-0.20220203205134-d70459300c8a", + "purl": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "1a111e6b-a682-470e-8b0e-aaa49d93cd39", + "type": "application", + "name": "app/gobinary/gobinary", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "dependsOn": [ + "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0" + ] + }, + { + "ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1", + "pkg:composer/pear/pear_exception@v1.0.0" + ] + }, + { + "ref": "1a111e6b-a682-470e-8b0e-aaa49d93cd39", + "dependsOn": [ + "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a" + ] + }, + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "dependsOn": [ + "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + "pkg:npm/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "100925ff-7c0a-470f-a725-8fb973b40e7b", + "1a111e6b-a682-470e-8b0e-aaa49d93cd39" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/fanal/artifact/sbom/testdata/os-only-bom.json b/pkg/fanal/artifact/sbom/testdata/os-only-bom.json new file mode 100644 index 000000000000..837c16754211 --- /dev/null +++ b/pkg/fanal/artifact/sbom/testdata/os-only-bom.json @@ -0,0 +1,77 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + } + ] + } + }, + "components": [ + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "dependsOn": [] + }, + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "dependsOn": [ + "60e9f57b-d4a6-4f71-ad14-0893ac609182" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/fanal/artifact/sbom/testdata/sbom.cdx.intoto.jsonl b/pkg/fanal/artifact/sbom/testdata/sbom.cdx.intoto.jsonl new file mode 100644 index 000000000000..3f2b64b667a3 --- /dev/null +++ b/pkg/fanal/artifact/sbom/testdata/sbom.cdx.intoto.jsonl @@ -0,0 +1,10 @@ +{ + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6ImluZGV4LmRvY2tlci5pby9vdG1zNjEvaGVsbG8tMSIsImRpZ2VzdCI6eyJzaGEyNTYiOiIyMGQzZjY5M2RjZmZhNDRkNmIyNGVhZTg4NzgzMzI0ZDI1Y2MxMzJjMjIwODlmNzBlNGZiZmI4NTg2MjViMDYyIn19XSwicHJlZGljYXRlIjp7IkRhdGEiOnsiYm9tRm9ybWF0IjoiQ3ljbG9uZURYIiwiY29tcG9uZW50cyI6W3siYm9tLXJlZiI6InBrZzphcGsvYWxwaW5lL211c2xAMS4yLjMtcjA/ZGlzdHJvPTMuMTYuMCIsImxpY2Vuc2VzIjpbeyJleHByZXNzaW9uIjoiTUlUIn1dLCJuYW1lIjoibXVzbCIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6U3JjTmFtZSIsInZhbHVlIjoibXVzbCJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpTcmNWZXJzaW9uIiwidmFsdWUiOiIxLjIuMy1yMCJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpMYXllckRpZmZJRCIsInZhbHVlIjoic2hhMjU2OmRkNTY1ZmY4NTBlNzAwMzM1NmUyYjI1Mjc1OGY5YmRjMWZmMjgwM2Y2MWU5OTVlMjRjNzg0NGY2Mjk3ZjhmYzMifV0sInB1cmwiOiJwa2c6YXBrL2FscGluZS9tdXNsQDEuMi4zLXIwP2Rpc3Rybz0zLjE2LjAiLCJ0eXBlIjoibGlicmFyeSIsInZlcnNpb24iOiIxLjIuMy1yMCJ9LHsiYm9tLXJlZiI6IjYwZTlmNTdiLWQ0YTYtNGY3MS1hZDE0LTA4OTNhYzYwOTE4MiIsIm5hbWUiOiJhbHBpbmUiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlR5cGUiLCJ2YWx1ZSI6ImFscGluZSJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpDbGFzcyIsInZhbHVlIjoib3MtcGtncyJ9XSwidHlwZSI6Im9wZXJhdGluZy1zeXN0ZW0iLCJ2ZXJzaW9uIjoiMy4xNi4wIn0seyJib20tcmVmIjoicGtnOm1hdmVuL29yZy5jb2RlaGF1cy5tb2pvL2NoaWxkLXByb2plY3RAMS4wP2ZpbGVfcGF0aD1hcHAlMkZtYXZlbiUyRnRhcmdldCUyRmNoaWxkLXByb2plY3QtMS4wLmphciIsIm5hbWUiOiJvcmcuY29kZWhhdXMubW9qbzpjaGlsZC1wcm9qZWN0IiwicHJvcGVydGllcyI6W3sibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpGaWxlUGF0aCIsInZhbHVlIjoiYXBwL21hdmVuL3RhcmdldC9jaGlsZC1wcm9qZWN0LTEuMC5qYXIifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6TGF5ZXJEaWZmSUQiLCJ2YWx1ZSI6InNoYTI1NjozYzc5ZTgzMmIxYjQ4OTFhMWNiNGEzMjZlZjg1MjRlMGJkMTRhMjUzNzE1MGFjMGUyMDNhNTY3NzE3NmMxY2ExIn0seyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlR5cGUiLCJ2YWx1ZSI6ImphciJ9XSwicHVybCI6InBrZzptYXZlbi9vcmcuY29kZWhhdXMubW9qby9jaGlsZC1wcm9qZWN0QDEuMCIsInR5cGUiOiJsaWJyYXJ5IiwidmVyc2lvbiI6IjEuMCJ9LHsiYm9tLXJlZiI6InBrZzpucG0vYm9vdHN0cmFwQDUuMC4yP2ZpbGVfcGF0aD1hcHAlMkZhcHAlMkZwYWNrYWdlLmpzb24iLCJsaWNlbnNlcyI6W3siZXhwcmVzc2lvbiI6Ik1JVCJ9XSwibmFtZSI6ImJvb3RzdHJhcCIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6RmlsZVBhdGgiLCJ2YWx1ZSI6ImFwcC9hcHAvcGFja2FnZS5qc29uIn0seyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OkxheWVyRGlmZklEIiwidmFsdWUiOiJzaGEyNTY6M2M3OWU4MzJiMWI0ODkxYTFjYjRhMzI2ZWY4NTI0ZTBiZDE0YTI1MzcxNTBhYzBlMjAzYTU2NzcxNzZjMWNhMSJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpUeXBlIiwidmFsdWUiOiJub2RlLXBrZyJ9XSwicHVybCI6InBrZzpucG0vYm9vdHN0cmFwQDUuMC4yIiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiNS4wLjIifSx7ImJvbS1yZWYiOiJwa2c6Y29tcG9zZXIvcGVhci9sb2dAMS4xMy4xIiwibmFtZSI6InBlYXIvbG9nIiwicHJvcGVydGllcyI6W3sibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpMYXllckRpZmZJRCIsInZhbHVlIjoic2hhMjU2OjNjNzllODMyYjFiNDg5MWExY2I0YTMyNmVmODUyNGUwYmQxNGEyNTM3MTUwYWMwZTIwM2E1Njc3MTc2YzFjYTEifV0sInB1cmwiOiJwa2c6Y29tcG9zZXIvcGVhci9sb2dAMS4xMy4xIiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoiMS4xMy4xIn0seyJib20tcmVmIjoicGtnOmNvbXBvc2VyL3BlYXIvcGVhcl9leGNlcHRpb25AdjEuMC4wIiwibmFtZSI6InBlYXIvcGVhcl9leGNlcHRpb24iLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OkxheWVyRGlmZklEIiwidmFsdWUiOiJzaGEyNTY6M2M3OWU4MzJiMWI0ODkxYTFjYjRhMzI2ZWY4NTI0ZTBiZDE0YTI1MzcxNTBhYzBlMjAzYTU2NzcxNzZjMWNhMSJ9XSwicHVybCI6InBrZzpjb21wb3Nlci9wZWFyL3BlYXJfZXhjZXB0aW9uQHYxLjAuMCIsInR5cGUiOiJsaWJyYXJ5IiwidmVyc2lvbiI6InYxLjAuMCJ9LHsiYm9tLXJlZiI6IjEwMDkyNWZmLTdjMGEtNDcwZi1hNzI1LThmYjk3M2I0MGU3YiIsIm5hbWUiOiJhcHAvY29tcG9zZXIvY29tcG9zZXIubG9jayIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6VHlwZSIsInZhbHVlIjoiY29tcG9zZXIifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6Q2xhc3MiLCJ2YWx1ZSI6ImxhbmctcGtncyJ9XSwidHlwZSI6ImFwcGxpY2F0aW9uIn0seyJib20tcmVmIjoicGtnOmdvbGFuZy9naXRodWIuY29tL3BhY2thZ2UtdXJsL3BhY2thZ2V1cmwtZ29AdjAuMS4xLTAuMjAyMjAyMDMyMDUxMzQtZDcwNDU5MzAwYzhhIiwibmFtZSI6ImdpdGh1Yi5jb20vcGFja2FnZS11cmwvcGFja2FnZXVybC1nbyIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6TGF5ZXJEaWZmSUQiLCJ2YWx1ZSI6InNoYTI1NjozYzc5ZTgzMmIxYjQ4OTFhMWNiNGEzMjZlZjg1MjRlMGJkMTRhMjUzNzE1MGFjMGUyMDNhNTY3NzE3NmMxY2ExIn1dLCJwdXJsIjoicGtnOmdvbGFuZy9naXRodWIuY29tL3BhY2thZ2UtdXJsL3BhY2thZ2V1cmwtZ29AdjAuMS4xLTAuMjAyMjAyMDMyMDUxMzQtZDcwNDU5MzAwYzhhIiwidHlwZSI6ImxpYnJhcnkiLCJ2ZXJzaW9uIjoidjAuMS4xLTAuMjAyMjAyMDMyMDUxMzQtZDcwNDU5MzAwYzhhIn0seyJib20tcmVmIjoiMWExMTFlNmItYTY4Mi00NzBlLThiMGUtYWFhNDlkOTNjZDM5IiwibmFtZSI6ImFwcC9nb2JpbmFyeS9nb2JpbmFyeSIsInByb3BlcnRpZXMiOlt7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6VHlwZSIsInZhbHVlIjoiZ29iaW5hcnkifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6Q2xhc3MiLCJ2YWx1ZSI6ImxhbmctcGtncyJ9XSwidHlwZSI6ImFwcGxpY2F0aW9uIn1dLCJkZXBlbmRlbmNpZXMiOlt7ImRlcGVuZHNPbiI6WyJwa2c6YXBrL2FscGluZS9tdXNsQDEuMi4zLXIwP2Rpc3Rybz0zLjE2LjAiXSwicmVmIjoiNjBlOWY1N2ItZDRhNi00ZjcxLWFkMTQtMDg5M2FjNjA5MTgyIn0seyJkZXBlbmRzT24iOlsicGtnOmNvbXBvc2VyL3BlYXIvbG9nQDEuMTMuMSIsInBrZzpjb21wb3Nlci9wZWFyL3BlYXJfZXhjZXB0aW9uQHYxLjAuMCJdLCJyZWYiOiIxMDA5MjVmZi03YzBhLTQ3MGYtYTcyNS04ZmI5NzNiNDBlN2IifSx7ImRlcGVuZHNPbiI6WyJwa2c6Z29sYW5nL2dpdGh1Yi5jb20vcGFja2FnZS11cmwvcGFja2FnZXVybC1nb0B2MC4xLjEtMC4yMDIyMDIwMzIwNTEzNC1kNzA0NTkzMDBjOGEiXSwicmVmIjoiMWExMTFlNmItYTY4Mi00NzBlLThiMGUtYWFhNDlkOTNjZDM5In0seyJkZXBlbmRzT24iOlsiNjBlOWY1N2ItZDRhNi00ZjcxLWFkMTQtMDg5M2FjNjA5MTgyIiwicGtnOm1hdmVuL29yZy5jb2RlaGF1cy5tb2pvL2NoaWxkLXByb2plY3RAMS4wP2ZpbGVfcGF0aD1hcHAlMkZtYXZlbiUyRnRhcmdldCUyRmNoaWxkLXByb2plY3QtMS4wLmphciIsInBrZzpucG0vYm9vdHN0cmFwQDUuMC4yP2ZpbGVfcGF0aD1hcHAlMkZhcHAlMkZwYWNrYWdlLmpzb24iLCIxMDA5MjVmZi03YzBhLTQ3MGYtYTcyNS04ZmI5NzNiNDBlN2IiLCIxYTExMWU2Yi1hNjgyLTQ3MGUtOGIwZS1hYWE0OWQ5M2NkMzkiXSwicmVmIjoiMGY1ODVkNjQtNDgxNS00YjcyLTkyYzUtOTdkYWUxOTFmYTRhIn1dLCJtZXRhZGF0YSI6eyJjb21wb25lbnQiOnsiYm9tLXJlZiI6IjBmNTg1ZDY0LTQ4MTUtNGI3Mi05MmM1LTk3ZGFlMTkxZmE0YSIsIm5hbWUiOiJtYXZlbi10ZXN0LXByb2plY3QiLCJwcm9wZXJ0aWVzIjpbeyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlNjaGVtYVZlcnNpb24iLCJ2YWx1ZSI6IjIifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6SW1hZ2VJRCIsInZhbHVlIjoic2hhMjU2OjQ5MTkzYTIzMTBkYmFkNGMwMjM4MmRhODdhYzYyNGE4MGE5MjM4N2E0Zjc1MzYyMzVmOWJhNTkwZTViY2Q3YjUifSx7Im5hbWUiOiJhcXVhc2VjdXJpdHk6dHJpdnk6RGlmZklEIiwidmFsdWUiOiJzaGEyNTY6ZGQ1NjVmZjg1MGU3MDAzMzU2ZTJiMjUyNzU4ZjliZGMxZmYyODAzZjYxZTk5NWUyNGM3ODQ0ZjYyOTdmOGZjMyJ9LHsibmFtZSI6ImFxdWFzZWN1cml0eTp0cml2eTpEaWZmSUQiLCJ2YWx1ZSI6InNoYTI1NjozYzc5ZTgzMmIxYjQ4OTFhMWNiNGEzMjZlZjg1MjRlMGJkMTRhMjUzNzE1MGFjMGUyMDNhNTY3NzE3NmMxY2ExIn0seyJuYW1lIjoiYXF1YXNlY3VyaXR5OnRyaXZ5OlJlcG9UYWciLCJ2YWx1ZSI6Im1hdmVuLXRlc3QtcHJvamVjdDpsYXRlc3QifV0sInR5cGUiOiJjb250YWluZXIifSwidGltZXN0YW1wIjoiMjAyMi0wNS0yOFQxMDoyMDowMy43OTUyN1oiLCJ0b29scyI6W3sibmFtZSI6InRyaXZ5IiwidmVuZG9yIjoiYXF1YXNlY3VyaXR5IiwidmVyc2lvbiI6ImRldiJ9XX0sInNlcmlhbE51bWJlciI6InVybjp1dWlkOmM5ODZiYTk0LWUzN2QtNDljOC05ZTMwLTk2ZGFjY2QwNDE1YiIsInNwZWNWZXJzaW9uIjoiMS40IiwidmVyc2lvbiI6MSwidnVsbmVyYWJpbGl0aWVzIjpbXX0sIlRpbWVzdGFtcCI6IiJ9fQ==", + "signatures": [ + { + "keyid": "", + "sig": "MEUCIQD57kCtdvc8gCX9CU3yMO/areJwRf7U52SatgsY5+Z+uAIgU3EQv/qAC+oj2bp3HusTrOAMjMN9RajsuFmifbahZeo=" + } + ] +} diff --git a/pkg/fanal/artifact/vm/ami.go b/pkg/fanal/artifact/vm/ami.go new file mode 100644 index 000000000000..49a85a026ecb --- /dev/null +++ b/pkg/fanal/artifact/vm/ami.go @@ -0,0 +1,65 @@ +package vm + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ec2" + "golang.org/x/xerrors" + + awsConfig "github.com/aquasecurity/trivy/pkg/config/aws" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/log" +) + +type AMI struct { + *EBS + + imageID string +} + +func newAMI(imageID string, storage Storage, region, endpoint string) (*AMI, error) { + // TODO: propagate context + ctx := context.TODO() + cfg, err := awsConfig.LoadDefaultAWSConfig(ctx, region, endpoint) + if err != nil { + return nil, err + } + client := ec2.NewFromConfig(cfg) + output, err := client.DescribeImages(ctx, &ec2.DescribeImagesInput{ + ImageIds: []string{imageID}, + }) + if err != nil { + return nil, xerrors.Errorf("ec2.DescribeImages: %w", err) + } else if len(output.Images) == 0 { + return nil, xerrors.Errorf("%s not found", imageID) + } + + // Take the first snapshot + for _, mapping := range output.Images[0].BlockDeviceMappings { + snapshotID := aws.ToString(mapping.Ebs.SnapshotId) + if snapshotID == "" { + continue + } + log.WithPrefix("ami").Info("Snapshot found", log.String("snapshot_id", snapshotID)) + ebs, err := newEBS(snapshotID, storage, region, endpoint) + if err != nil { + return nil, xerrors.Errorf("new EBS error: %w", err) + } + return &AMI{ + EBS: ebs, + imageID: imageID, + }, nil + } + + return nil, xerrors.New("no snapshot found") +} + +func (a *AMI) Inspect(ctx context.Context) (artifact.Reference, error) { + ref, err := a.EBS.Inspect(ctx) + if err != nil { + return artifact.Reference{}, err + } + ref.Name = a.imageID + return ref, nil +} diff --git a/pkg/fanal/artifact/vm/ebs.go b/pkg/fanal/artifact/vm/ebs.go new file mode 100644 index 000000000000..693e1ee755a8 --- /dev/null +++ b/pkg/fanal/artifact/vm/ebs.go @@ -0,0 +1,126 @@ +package vm + +import ( + "context" + "io" + + lru "github.com/hashicorp/golang-lru/v2" + ebsfile "github.com/masahiro331/go-ebs-file" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + config "github.com/aquasecurity/trivy/pkg/config/aws" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +// default block size 512 KB +// Max cache memory size 64 MB +const storageEBSCacheSize = 128 + +const ebsArtifactVersion = 0 + +// EBS represents an artifact for AWS EBS snapshots +type EBS struct { + Storage + logger *log.Logger + snapshotID string + ebs ebsfile.EBSAPI +} + +func newEBS(snapshotID string, vm Storage, region, endpoint string) (*EBS, error) { + ebs, err := ebsfile.New(context.TODO(), config.MakeAWSOptions(region, endpoint)...) + if err != nil { + return nil, xerrors.Errorf("new ebsfile error: %w", err) + } + + return &EBS{ + Storage: vm, + logger: log.WithPrefix("ebs"), + snapshotID: snapshotID, + ebs: ebs, + }, nil +} + +func (a *EBS) Inspect(ctx context.Context) (artifact.Reference, error) { + sr, err := a.openEBS(ctx) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("EBS open error: %w", err) + } + + cacheKey, err := a.calcCacheKey(a.snapshotID) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("cache key calculation error: %w", err) + } + + if a.hasCache(ctx, cacheKey) { + return artifact.Reference{ + Name: a.snapshotID, + Type: types.TypeVM, + ID: cacheKey, // use a cache key as pseudo artifact ID + BlobIDs: []string{cacheKey}, + }, nil + } + + blobInfo, err := a.Analyze(ctx, sr) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("inspection error: %w", err) + } + + if err = a.cache.PutBlob(ctx, cacheKey, blobInfo); err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to store blob (%s) in cache: %w", cacheKey, err) + } + + return artifact.Reference{ + Name: a.snapshotID, + Type: types.TypeVM, + ID: cacheKey, // use a cache key as pseudo artifact ID + BlobIDs: []string{cacheKey}, + }, nil +} + +func (a *EBS) openEBS(ctx context.Context) (*io.SectionReader, error) { + c, err := lru.New[string, []byte](storageEBSCacheSize) + if err != nil { + return nil, xerrors.Errorf("lru cache error: %w", err) + } + + r, err := ebsfile.Open(a.snapshotID, ctx, c, a.ebs) + if err != nil { + return nil, xerrors.Errorf("EBS error: %w", err) + } + return r, nil +} + +func (a *EBS) Clean(_ artifact.Reference) error { + return nil +} + +func (a *EBS) SetEBS(ebs ebsfile.EBSAPI) { + a.ebs = ebs +} + +func (a *EBS) calcCacheKey(key string) (string, error) { + s, err := cache.CalcKey(key, ebsArtifactVersion, a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption) + if err != nil { + return "", xerrors.Errorf("failed to calculate cache key: %w", err) + } + return s, nil +} + +func (a *EBS) hasCache(ctx context.Context, cacheKey string) bool { + _, missingCacheKeys, err := a.cache.MissingBlobs(ctx, cacheKey, []string{cacheKey}) + if err != nil { + a.logger.Debug("Unable to query missing cache", log.Err(err)) + return false + } + + // Cache exists + if len(missingCacheKeys) == 0 { + return true + } + + a.logger.Debug("Missing virtual machine cache", log.String("key", cacheKey)) + return false +} diff --git a/pkg/fanal/artifact/vm/file.go b/pkg/fanal/artifact/vm/file.go new file mode 100644 index 000000000000..c75b16af48ff --- /dev/null +++ b/pkg/fanal/artifact/vm/file.go @@ -0,0 +1,123 @@ +package vm + +import ( + "context" + "crypto/sha256" + "encoding/json" + "errors" + "io" + "os" + + lru "github.com/hashicorp/golang-lru/v2" + "github.com/opencontainers/go-digest" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/vm" + "github.com/aquasecurity/trivy/pkg/fanal/vm/disk" + "github.com/aquasecurity/trivy/pkg/log" +) + +// default vmdk block size 64 KB +// If vm type vmdk max cache memory size 64 MB +const storageFILECacheSize = 1024 + +const imageFileArtifactVersion = 0 + +// ImageFile represents an local VM image file +type ImageFile struct { + Storage + + filePath string + file *os.File + reader *io.SectionReader +} + +func newFile(filePath string, storage Storage) (imgFile *ImageFile, err error) { + f, err := os.Open(filePath) + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + + // Close file on error + defer func() { + if err != nil && f != nil { + f.Close() + } + }() + + c, err := lru.New[string, []byte](storageFILECacheSize) + if err != nil { + return nil, xerrors.Errorf("failed to create new lru cache: %w", err) + } + + reader, err := disk.New(f, c) + if err != nil { + if errors.Is(err, vm.ErrUnsupportedType) { + return nil, err + } + + logger := log.WithPrefix("vm") + logger.Debug("VM image not detected", log.Err(err)) + logger.Debug("Assume raw image") + fi, err := f.Stat() + if err != nil { + return nil, xerrors.Errorf("file stat error: %w", err) + } + reader = io.NewSectionReader(f, 0, fi.Size()) + } + + return &ImageFile{ + Storage: storage, + + filePath: filePath, + file: f, + reader: reader, + }, nil +} + +func (a *ImageFile) Inspect(ctx context.Context) (artifact.Reference, error) { + blobInfo, err := a.Analyze(ctx, a.reader) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("inspection error: %w", err) + } + + cacheKey, err := a.calcCacheKey(blobInfo) + if err != nil { + return artifact.Reference{}, xerrors.Errorf("cache calculation error: %w", err) + } + + if err = a.cache.PutBlob(ctx, cacheKey, blobInfo); err != nil { + return artifact.Reference{}, xerrors.Errorf("failed to store blob (%s) in cache: %w", cacheKey, err) + } + + return artifact.Reference{ + Name: a.filePath, + Type: types.TypeVM, + ID: cacheKey, // use a cache key as pseudo artifact ID + BlobIDs: []string{cacheKey}, + }, nil +} + +func (a *ImageFile) calcCacheKey(blobInfo types.BlobInfo) (string, error) { + // calculate hash of JSON and use it as pseudo artifactID and blobID + h := sha256.New() + if err := json.NewEncoder(h).Encode(blobInfo); err != nil { + return "", xerrors.Errorf("json error: %w", err) + } + + d := digest.NewDigest(digest.SHA256, h) + cacheKey, err := cache.CalcKey(d.String(), imageFileArtifactVersion, a.analyzer.AnalyzerVersions(), a.handlerManager.Versions(), a.artifactOption) + if err != nil { + return "", xerrors.Errorf("cache key: %w", err) + } + + return cacheKey, nil +} + +func (a *ImageFile) Clean(reference artifact.Reference) error { + _ = a.file.Close() + return a.cache.DeleteBlobs(context.TODO(), reference.BlobIDs) +} diff --git a/pkg/fanal/artifact/vm/testdata/alpine/etc/alpine-release b/pkg/fanal/artifact/vm/testdata/alpine/etc/alpine-release new file mode 100644 index 000000000000..0f84b16be2f5 --- /dev/null +++ b/pkg/fanal/artifact/vm/testdata/alpine/etc/alpine-release @@ -0,0 +1 @@ +3.17.5 \ No newline at end of file diff --git a/pkg/fanal/artifact/vm/testdata/alpine/lib/apk/db/installed b/pkg/fanal/artifact/vm/testdata/alpine/lib/apk/db/installed new file mode 100644 index 000000000000..9b91634fd159 --- /dev/null +++ b/pkg/fanal/artifact/vm/testdata/alpine/lib/apk/db/installed @@ -0,0 +1,21 @@ +C:Q1dCsKJvMnxtpg1CoCw+thiaWORo8= +P:musl +V:1.2.3-r5 +A:aarch64 +S:397125 +I:675840 +T:the musl c library (libc) implementation +U:https://musl.libc.org/ +L:MIT +o:musl +m:Timo Teräs +t:1684510151 +c:b12380f8608f8cdd44347db413e8937ac4a5565b +p:so:libc.musl-aarch64.so.1=1 +F:lib +R:ld-musl-aarch64.so.1 +a:0:0:755 +Z:Q17HWoHxeSxkUYleHiHuyks+G3edE= +R:libc.musl-aarch64.so.1 +a:0:0:777 +Z:Q14RpiCEfZIqcg1XDcVqp8QEpc9ks= \ No newline at end of file diff --git a/pkg/fanal/artifact/vm/testdata/mock.img b/pkg/fanal/artifact/vm/testdata/mock.img new file mode 100644 index 000000000000..23ee03b20946 --- /dev/null +++ b/pkg/fanal/artifact/vm/testdata/mock.img @@ -0,0 +1 @@ +mock \ No newline at end of file diff --git a/pkg/fanal/artifact/vm/testdata/monolithicSparse.vmdk b/pkg/fanal/artifact/vm/testdata/monolithicSparse.vmdk new file mode 100644 index 000000000000..b64f95dbdedf Binary files /dev/null and b/pkg/fanal/artifact/vm/testdata/monolithicSparse.vmdk differ diff --git a/pkg/fanal/artifact/vm/vm.go b/pkg/fanal/artifact/vm/vm.go new file mode 100644 index 000000000000..934c6dd9bd26 --- /dev/null +++ b/pkg/fanal/artifact/vm/vm.go @@ -0,0 +1,177 @@ +package vm + +import ( + "context" + "io" + "os" + "strings" + + "golang.org/x/sync/errgroup" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/semaphore" +) + +type Type string + +func (t Type) Prefix() string { + return string(t) + ":" +} + +const ( + TypeAMI Type = "ami" + TypeEBS Type = "ebs" + TypeFile Type = "file" +) + +var _ Walker = (*walker.VM)(nil) + +type Walker interface { + Walk(*io.SectionReader, string, walker.Option, walker.WalkFunc) error +} + +func NewArtifact(target string, c cache.ArtifactCache, w Walker, opt artifact.Option) (artifact.Artifact, error) { + handlerManager, err := handler.NewManager(opt) + if err != nil { + return nil, xerrors.Errorf("handler init error: %w", err) + } + a, err := analyzer.NewAnalyzerGroup(opt.AnalyzerOptions()) + if err != nil { + return nil, xerrors.Errorf("analyzer group error: %w", err) + } + + storage := Storage{ + cache: c, + analyzer: a, + handlerManager: handlerManager, + walker: w, + artifactOption: opt, + } + + targetType := detectType(target) + switch targetType { + case TypeAMI: + target = strings.TrimPrefix(target, TypeAMI.Prefix()) + return newAMI(target, storage, opt.AWSRegion, opt.AWSEndpoint) + case TypeEBS: + target = strings.TrimPrefix(target, TypeEBS.Prefix()) + e, err := newEBS(target, storage, opt.AWSRegion, opt.AWSEndpoint) + if err != nil { + return nil, xerrors.Errorf("new EBS error: %w", err) + } + return e, nil + case TypeFile: + target = strings.TrimPrefix(target, TypeFile.Prefix()) + return newFile(target, storage) + } + return nil, xerrors.Errorf("unsupported format") +} + +type Storage struct { + cache cache.ArtifactCache + analyzer analyzer.AnalyzerGroup + handlerManager handler.Manager + walker Walker + + artifactOption artifact.Option +} + +func (a *Storage) Analyze(ctx context.Context, r *io.SectionReader) (types.BlobInfo, error) { + // `errgroup` cancels the context after Wait returns, so it can’t be use later. + // We need a separate context specifically for Analyze. + eg, egCtx := errgroup.WithContext(ctx) + limit := semaphore.New(a.artifactOption.Parallel) + result := analyzer.NewAnalysisResult() + + opts := analyzer.AnalysisOptions{ + Offline: a.artifactOption.Offline, + FileChecksum: a.artifactOption.FileChecksum, + } + + // Prepare filesystem for post analysis + composite, err := a.analyzer.PostAnalyzerFS() + if err != nil { + return types.BlobInfo{}, xerrors.Errorf("unable to get post analysis filesystem: %w", err) + } + defer composite.Cleanup() + + // TODO: Always walk from the root directory. Consider whether there is a need to be able to set optional + err = a.walker.Walk(r, "/", a.artifactOption.WalkerOption, func(filePath string, info os.FileInfo, opener analyzer.Opener) error { + path := strings.TrimPrefix(filePath, "/") + if err = a.analyzer.AnalyzeFile(egCtx, eg, limit, result, "/", path, info, opener, nil, opts); err != nil { + return xerrors.Errorf("analyze file (%s): %w", path, err) + } + + // Skip post analysis if the file is not required + analyzerTypes := a.analyzer.RequiredPostAnalyzers(path, info) + if len(analyzerTypes) == 0 { + return nil + } + + // Build filesystem for post analysis + tmpFilePath, err := composite.CopyFileToTemp(opener, info) + if err != nil { + return xerrors.Errorf("failed to copy file to temp: %w", err) + } + + if err = composite.CreateLink(analyzerTypes, "", path, tmpFilePath); err != nil { + return xerrors.Errorf("failed to write a file: %w", err) + } + + return nil + }) + if err != nil { + return types.BlobInfo{}, xerrors.Errorf("walk vm error: %w", err) + } + + // Wait for all the goroutine to finish. + if err = eg.Wait(); err != nil { + return types.BlobInfo{}, xerrors.Errorf("analyze error: %w", err) + } + + // Post-analysis + if err = a.analyzer.PostAnalyze(ctx, composite, result, opts); err != nil { + return types.BlobInfo{}, xerrors.Errorf("post analysis error: %w", err) + } + + result.Sort() + + blobInfo := types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: result.OS, + Repository: result.Repository, + PackageInfos: result.PackageInfos, + Applications: result.Applications, + Secrets: result.Secrets, + Licenses: result.Licenses, + CustomResources: result.CustomResources, + + // For Red Hat + BuildInfo: result.BuildInfo, + } + + if err = a.handlerManager.PostHandle(ctx, result, &blobInfo); err != nil { + return types.BlobInfo{}, xerrors.Errorf("failed to call hooks: %w", err) + } + + return blobInfo, nil +} + +func detectType(target string) Type { + switch { + case strings.HasPrefix(target, TypeAMI.Prefix()): + return TypeAMI + case strings.HasPrefix(target, TypeEBS.Prefix()): + return TypeEBS + case strings.HasPrefix(target, TypeFile.Prefix()): + return TypeFile + default: + return TypeFile + } +} diff --git a/pkg/fanal/artifact/vm/vm_test.go b/pkg/fanal/artifact/vm/vm_test.go new file mode 100644 index 000000000000..3e942ecfc800 --- /dev/null +++ b/pkg/fanal/artifact/vm/vm_test.go @@ -0,0 +1,217 @@ +package vm_test + +import ( + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "strings" + "testing" + + ebsfile "github.com/masahiro331/go-ebs-file" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/cachetest" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/artifact/vm" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fanal/walker" + "github.com/aquasecurity/trivy/pkg/misconf" + xio "github.com/aquasecurity/trivy/pkg/x/io" + + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/os/alpine" + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/pkg/apk" +) + +const ( + ebsPrefix = string(vm.TypeEBS) + ":" + filePrefix = string(vm.TypeFile) + ":" +) + +type mockWalker struct { + root string +} + +func (m *mockWalker) Walk(_ *io.SectionReader, _ string, _ walker.Option, fn walker.WalkFunc) error { + return filepath.WalkDir(m.root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } else if d.IsDir() { + return nil + } + + info, err := d.Info() + if err != nil { + return err + } + opener := func() (xio.ReadSeekCloserAt, error) { + return os.Open(path) + } + relPath, err := filepath.Rel(m.root, path) + if err != nil { + return err + } + relPath = filepath.ToSlash(relPath) + return fn(relPath, info, opener) + }) +} + +func TestNewArtifact(t *testing.T) { + tests := []struct { + name string + target string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "happy path for file", + target: "testdata/mock.img", + wantErr: assert.NoError, + }, + { + name: "happy path for EBS", + target: "ebs:ebs-012345", + wantErr: assert.NoError, + }, + { + name: "sad path unsupported vm format", + target: "testdata/monolithicSparse.vmdk", + wantErr: func(t assert.TestingT, err error, _ ...any) bool { + return assert.ErrorContains(t, err, "unsupported type error") + }, + }, + { + name: "sad path file not found", + target: "testdata/no-file", + wantErr: func(t assert.TestingT, err error, _ ...any) bool { + return assert.ErrorContains(t, err, "file open error") + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := &mockWalker{root: "testdata"} + _, err := vm.NewArtifact(tt.target, nil, w, artifact.Option{Parallel: 3}) + tt.wantErr(t, err, fmt.Sprintf("NewArtifact(%v, nil, nil)", tt.target)) + }) + } +} + +func TestArtifact_Inspect(t *testing.T) { + tests := []struct { + name string + target string + rootDir string + artifactOpt artifact.Option + scannerOpt misconf.ScannerOption + disabledAnalyzers []analyzer.Type + disabledHandlers []types.HandlerType + wantBlobs []cachetest.WantBlob + want artifact.Reference + wantErr string + }{ + { + name: "happy path for raw image", + target: "testdata/mock.img", + rootDir: "testdata/alpine", + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:e04ee0f05d1384ec4b48c99f9d05491bff1ddbc2331b8419bcde7548070e314b", + BlobInfo: expectedBlobInfo, + }, + }, + want: artifact.Reference{ + Name: "rawdata.img", + Type: types.TypeVM, + ID: "sha256:e04ee0f05d1384ec4b48c99f9d05491bff1ddbc2331b8419bcde7548070e314b", + BlobIDs: []string{ + "sha256:e04ee0f05d1384ec4b48c99f9d05491bff1ddbc2331b8419bcde7548070e314b", + }, + }, + }, + { + name: "happy path for ebs", + target: "ebs:ebs-012345", + rootDir: "testdata/alpine", + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:d1690d3201420ddb690be85be011afd36be4c8bff47c474d7fcfe9c7efea9a3f", + BlobInfo: expectedBlobInfo, + }, + }, + want: artifact.Reference{ + Name: "ebs-012345", + Type: types.TypeVM, + ID: "sha256:d1690d3201420ddb690be85be011afd36be4c8bff47c474d7fcfe9c7efea9a3f", + BlobIDs: []string{ + "sha256:d1690d3201420ddb690be85be011afd36be4c8bff47c474d7fcfe9c7efea9a3f", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cachetest.NewCache(t, nil) + + m := &mockWalker{root: tt.rootDir} + + a, err := vm.NewArtifact(tt.target, c, m, tt.artifactOpt) + require.NoError(t, err) + + if aa, ok := a.(*vm.EBS); ok { + ebs := ebsfile.NewMockEBS("testdata/mock.img", 1, 2) + aa.SetEBS(ebs) + } + + got, err := a.Inspect(t.Context()) + defer a.Clean(got) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + tt.want.Name = trimPrefix(tt.target) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func trimPrefix(s string) string { + s = strings.TrimPrefix(s, ebsPrefix) + s = strings.TrimPrefix(s, filePrefix) + return s +} + +var expectedBlobInfo = types.BlobInfo{ + SchemaVersion: types.BlobJSONSchemaVersion, + OS: types.OS{ + Family: "alpine", + Name: "3.17.5", + }, + PackageInfos: []types.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: types.Packages{ + { + ID: "musl@1.2.3-r5", + Name: "musl", + Version: "1.2.3-r5", + SrcName: "musl", + SrcVersion: "1.2.3-r5", + Licenses: []string{"MIT"}, + Maintainer: "Timo Teräs ", + Arch: "aarch64", + Digest: "sha1:742b0a26f327c6da60d42a02c3eb6189a58e468f", + InstalledFiles: []string{ + "lib/ld-musl-aarch64.so.1", + "lib/libc.musl-aarch64.so.1", + }, + AnalyzedBy: analyzer.TypeApk, + }, + }, + }, + }, +} diff --git a/pkg/fanal/handler/all/import.go b/pkg/fanal/handler/all/import.go new file mode 100644 index 000000000000..37c7c5545708 --- /dev/null +++ b/pkg/fanal/handler/all/import.go @@ -0,0 +1,6 @@ +package all + +import ( + _ "github.com/aquasecurity/trivy/pkg/fanal/handler/sysfile" + _ "github.com/aquasecurity/trivy/pkg/fanal/handler/unpackaged" +) diff --git a/pkg/fanal/handler/handler.go b/pkg/fanal/handler/handler.go new file mode 100644 index 000000000000..84629b66e242 --- /dev/null +++ b/pkg/fanal/handler/handler.go @@ -0,0 +1,79 @@ +package handler + +import ( + "context" + "slices" + "sort" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + postHandlerInits = make(map[types.HandlerType]postHandlerInit) +) + +type postHandlerInit func(artifact.Option) (PostHandler, error) + +type PostHandler interface { + Type() types.HandlerType + Version() int + Handle(context.Context, *analyzer.AnalysisResult, *types.BlobInfo) error + Priority() int +} + +// RegisterPostHandlerInit adds a constructor of post handler +func RegisterPostHandlerInit(t types.HandlerType, init postHandlerInit) { + postHandlerInits[t] = init +} + +func DeregisterPostHandler(t types.HandlerType) { + delete(postHandlerInits, t) +} + +type Manager struct { + postHandlers []PostHandler +} + +func NewManager(artifactOpt artifact.Option) (Manager, error) { + var m Manager + for t, handlerInit := range postHandlerInits { + // Skip the handler if it is disabled + if slices.Contains(artifactOpt.DisabledHandlers, t) { + continue + } + handler, err := handlerInit(artifactOpt) + if err != nil { + return Manager{}, xerrors.Errorf("post handler %s initialize error: %w", t, err) + } + + m.postHandlers = append(m.postHandlers, handler) + } + + // Sort post handlers by priority + sort.Slice(m.postHandlers, func(i, j int) bool { + return m.postHandlers[i].Priority() > m.postHandlers[j].Priority() + }) + + return m, nil +} + +func (m Manager) Versions() map[string]int { + versions := make(map[string]int) + for _, h := range m.postHandlers { + versions[string(h.Type())] = h.Version() + } + return versions +} + +func (m Manager) PostHandle(ctx context.Context, result *analyzer.AnalysisResult, blob *types.BlobInfo) error { + for _, h := range m.postHandlers { + if err := h.Handle(ctx, result, blob); err != nil { + return xerrors.Errorf("post handler error: %w", err) + } + } + return nil +} diff --git a/pkg/fanal/handler/handler_test.go b/pkg/fanal/handler/handler_test.go new file mode 100644 index 000000000000..b182584a145a --- /dev/null +++ b/pkg/fanal/handler/handler_test.go @@ -0,0 +1,107 @@ +package handler_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type fakeHook struct{} + +func (h fakeHook) Handle(_ context.Context, _ *analyzer.AnalysisResult, info *types.BlobInfo) error { + info.DiffID = "fake" + return nil +} + +func (h fakeHook) Priority() int { + return 1 +} + +func (h fakeHook) Version() int { return 1 } + +func (h fakeHook) Type() types.HandlerType { return "fake" } + +func fakehookInit(_ artifact.Option) (handler.PostHandler, error) { + return fakeHook{}, nil +} + +func TestManager_Versions(t *testing.T) { + tests := []struct { + name string + disable []types.HandlerType + want map[string]int + }{ + { + name: "happy path", + want: map[string]int{ + "fake": 1, + }, + }, + { + name: "disable hooks", + disable: []types.HandlerType{"fake"}, + want: make(map[string]int), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler.RegisterPostHandlerInit("fake", fakehookInit) + defer handler.DeregisterPostHandler("fake") + m, err := handler.NewManager(artifact.Option{ + DisabledHandlers: tt.disable, + }) + require.NoError(t, err) + got := m.Versions() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestManager_CallHooks(t *testing.T) { + tests := []struct { + name string + disable []types.HandlerType + want types.BlobInfo + }{ + { + name: "happy path", + want: types.BlobInfo{ + Digest: "digest", + DiffID: "fake", + }, + }, + { + name: "disable hooks", + disable: []types.HandlerType{"fake"}, + want: types.BlobInfo{ + Digest: "digest", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + handler.RegisterPostHandlerInit("fake", fakehookInit) + defer handler.DeregisterPostHandler("fake") + blob := types.BlobInfo{ + Digest: "digest", + } + m, err := handler.NewManager(artifact.Option{ + DisabledHandlers: tt.disable, + }) + require.NoError(t, err) + + err = m.PostHandle(t.Context(), nil, &blob) + require.NoError(t, err) + assert.Equal(t, tt.want, blob) + }) + } +} diff --git a/pkg/fanal/handler/sysfile/filter.go b/pkg/fanal/handler/sysfile/filter.go new file mode 100644 index 000000000000..7788483271b9 --- /dev/null +++ b/pkg/fanal/handler/sysfile/filter.go @@ -0,0 +1,97 @@ +package nodejs + +import ( + "context" + "slices" + "strings" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func init() { + handler.RegisterPostHandlerInit(types.SystemFileFilteringPostHandler, NewSystemFileFilteringPostHandler) +} + +const version = 1 + +var defaultSystemFiles = []string{ + // TODO: Google Distroless removes /var/lib/dpkg/info/*.list, so we cannot know which files are installed by dpkg. + // We have to hardcode these files at the moment, but should look for the better way. + "/usr/lib/python2.7/argparse.egg-info", + "/usr/lib/python2.7/lib-dynload/Python-2.7.egg-info", + "/usr/lib/python2.7/wsgiref.egg-info", +} + +type systemFileFilteringPostHandler struct{} + +func NewSystemFileFilteringPostHandler(artifact.Option) (handler.PostHandler, error) { + return systemFileFilteringPostHandler{}, nil +} + +// Handle removes files installed by OS package manager such as yum. +func (h systemFileFilteringPostHandler) Handle(_ context.Context, result *analyzer.AnalysisResult, blob *types.BlobInfo) error { + var systemFiles []string + for _, file := range append(result.SystemInstalledFiles, defaultSystemFiles...) { + // Trim leading slashes to be the same format as the path in container images. + systemFile := strings.TrimPrefix(file, "/") + // We should check the root filepath ("/") and ignore it. + // Otherwise, packages with an empty filePath will be removed. + if systemFile != "" { + systemFiles = append(systemFiles, systemFile) + } + } + + var apps []types.Application + for _, app := range blob.Applications { + // If the lang-specific package was installed by OS package manager, it should not be taken. + // Otherwise, the package version will be wrong, then it will lead to false positive. + if slices.Contains(systemFiles, app.FilePath) { + continue + } + + var pkgs []types.Package + for _, lib := range app.Packages { + // If the lang-specific package was installed by OS package manager, it should not be taken. + // Otherwise, the package version will be wrong, then it will lead to false positive. + if slices.Contains(systemFiles, lib.FilePath) { + continue + } + pkgs = append(pkgs, lib) + } + + // Overwrite Packages + app.Packages = pkgs + apps = append(apps, app) + } + + // Iterate and delete unnecessary customResource + i := 0 + for _, res := range blob.CustomResources { + if slices.Contains(systemFiles, res.FilePath) { + continue + } + blob.CustomResources[i] = res + i++ + } + blob.CustomResources = blob.CustomResources[:i] + + // Overwrite Applications + blob.Applications = apps + + return nil +} + +func (h systemFileFilteringPostHandler) Version() int { + return version +} + +func (h systemFileFilteringPostHandler) Type() types.HandlerType { + return types.SystemFileFilteringPostHandler +} + +func (h systemFileFilteringPostHandler) Priority() int { + return types.SystemFileFilteringPostHandlerPriority +} diff --git a/pkg/fanal/handler/sysfile/filter_test.go b/pkg/fanal/handler/sysfile/filter_test.go new file mode 100644 index 000000000000..5bbee9432dca --- /dev/null +++ b/pkg/fanal/handler/sysfile/filter_test.go @@ -0,0 +1,231 @@ +package nodejs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_systemFileFilterHook_Hook(t *testing.T) { + tests := []struct { + name string + result *analyzer.AnalysisResult + blob *types.BlobInfo + want *types.BlobInfo + }{ + { + name: "happy path", + result: &analyzer.AnalysisResult{ + SystemInstalledFiles: []string{ + "/", + "/usr/bin/pydoc", + "/usr/bin/python", + "/usr/bin/python2", + "/usr/bin/python2.7", + "/usr/libexec/platform-python", + "/usr/share/doc/python-2.7.5", + "/usr/share/doc/python-2.7.5/LICENSE", + "/usr/share/doc/python-2.7.5/README", + "/usr/share/man/man1/python.1.gz", + "/usr/share/man/man1/python2.1.gz", + "/usr/share/man/man1/python2.7.1.gz", + "/usr/lib64/python2.7/distutils/command/install_egg_info.py", + "/usr/lib64/python2.7/distutils/command/install_egg_info.pyc", + "/usr/lib64/python2.7/distutils/command/install_egg_info.pyo", + "/usr/lib64/python2.7/lib-dynload/Python-2.7.5-py2.7.egg-info", + "usr/lib64/python2.7/wsgiref.egg-info", // without the leading slash + }, + }, + blob: &types.BlobInfo{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/rpm/Packages", + Packages: types.Packages{ + { + Name: "python", + Version: "2.7.5", + Release: "89.el7", + }, + { + Name: "python-libs", + Version: "2.7.5", + Release: "89.el7", + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: types.Pipenv, + FilePath: "app/Pipfile.lock", + Packages: types.Packages{ + { + Name: "django", + Version: "3.1.2", + }, + }, + }, + { + Type: types.PythonPkg, + Packages: types.Packages{ + { + Name: "python", + Version: "2.7.5", + FilePath: "usr/lib64/python2.7/lib-dynload/Python-2.7.5-py2.7.egg-info", + }, + { + Name: "pycurl", + Version: "7.19.0", + FilePath: "usr/lib64/python2.7/site-packages/pycurl-7.19.0-py2.7.egg-info", + }, + }, + }, + { + Type: types.PythonPkg, + FilePath: "usr/lib64/python2.7/wsgiref.egg-info", + Packages: types.Packages{ + { + Name: "wsgiref", + Version: "0.1.2", + }, + }, + }, + { + Type: types.GoBinary, + FilePath: "usr/local/bin/goBinariryFile", + Packages: types.Packages{ + { + Name: "cloud.google.com/go", + Version: "v0.81.0", + FilePath: "", + }, + }, + }, + }, + CustomResources: []types.CustomResource{ + { + FilePath: "usr/bin/pydoc", + Data: "remove", + }, + { + FilePath: "usr/bin/pydoc/needed", + Data: "shouldNotRemove", + }, + }, + }, + want: &types.BlobInfo{ + PackageInfos: []types.PackageInfo{ + { + FilePath: "var/lib/rpm/Packages", + Packages: types.Packages{ + { + Name: "python", + Version: "2.7.5", + Release: "89.el7", + }, + { + Name: "python-libs", + Version: "2.7.5", + Release: "89.el7", + }, + }, + }, + }, + Applications: []types.Application{ + { + Type: types.Pipenv, + FilePath: "app/Pipfile.lock", + Packages: types.Packages{ + { + Name: "django", + Version: "3.1.2", + }, + }, + }, + { + Type: types.PythonPkg, + Packages: types.Packages{ + { + Name: "pycurl", + Version: "7.19.0", + FilePath: "usr/lib64/python2.7/site-packages/pycurl-7.19.0-py2.7.egg-info", + }, + }, + }, + { + Type: types.GoBinary, + FilePath: "usr/local/bin/goBinariryFile", + Packages: types.Packages{ + { + Name: "cloud.google.com/go", + Version: "v0.81.0", + }, + }, + }, + }, + CustomResources: []types.CustomResource{ + { + FilePath: "usr/bin/pydoc/needed", + Data: "shouldNotRemove", + Layer: types.Layer{}, + }, + }, + }, + }, + { + name: "distroless", + result: &analyzer.AnalysisResult{}, + blob: &types.BlobInfo{ + Applications: []types.Application{ + { + Type: types.PythonPkg, + FilePath: "usr/lib/python2.7/lib-dynload/Python-2.7.egg-info", + Packages: types.Packages{ + { + Name: "python", + Version: "2.7.14", + FilePath: "usr/lib/python2.7/lib-dynload/Python-2.7.egg-info", + }, + }, + }, + }, + }, + want: &types.BlobInfo{}, + }, + { + name: "go binaries", + result: &analyzer.AnalysisResult{ + SystemInstalledFiles: []string{ + "usr/local/bin/goreleaser", + }, + }, + blob: &types.BlobInfo{ + Applications: []types.Application{ + { + Type: types.GoBinary, + FilePath: "usr/local/bin/goreleaser", + Packages: types.Packages{ + { + Name: "github.com/sassoftware/go-rpmutils", + Version: "v0.0.0-20190420191620-a8f1baeba37b", + }, + }, + }, + }, + }, + want: &types.BlobInfo{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + h := systemFileFilteringPostHandler{} + err := h.Handle(t.Context(), tt.result, tt.blob) + require.NoError(t, err) + assert.Equal(t, tt.want, tt.blob) + }) + } +} diff --git a/pkg/fanal/handler/unpackaged/unpackaged.go b/pkg/fanal/handler/unpackaged/unpackaged.go new file mode 100644 index 000000000000..07925d9596f0 --- /dev/null +++ b/pkg/fanal/handler/unpackaged/unpackaged.go @@ -0,0 +1,95 @@ +package unpackaged + +import ( + "bytes" + "context" + "errors" + "slices" + + "golang.org/x/xerrors" + + sbomatt "github.com/aquasecurity/trivy/pkg/attestation/sbom" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom" +) + +func init() { + handler.RegisterPostHandlerInit(types.UnpackagedPostHandler, NewUnpackagedHandler) +} + +const version = 1 + +type unpackagedHook struct { + logger *log.Logger + client sbomatt.Rekor +} + +func NewUnpackagedHandler(opt artifact.Option) (handler.PostHandler, error) { + c, err := sbomatt.NewRekor(opt.RekorURL) + if err != nil { + return nil, xerrors.Errorf("rekor client error: %w", err) + } + return unpackagedHook{ + logger: log.WithPrefix("unpackaged"), + client: c, + }, nil +} + +// Handle retrieves SBOM of unpackaged executable files in Rekor. +func (h unpackagedHook) Handle(ctx context.Context, res *analyzer.AnalysisResult, blob *types.BlobInfo) error { + for filePath, digest := range res.Digests { + // Skip files installed by OS package managers. + if slices.Contains(res.SystemInstalledFiles, filePath) { + continue + } + + // Retrieve SBOM from Rekor according to the file digest. + raw, err := h.client.RetrieveSBOM(ctx, digest) + if errors.Is(err, sbomatt.ErrNoSBOMAttestation) { + continue + } else if err != nil { + return err + } + + r := bytes.NewReader(raw) + + // Detect the SBOM format like CycloneDX, SPDX, etc. + format, err := sbom.DetectFormat(r) + if err != nil { + return err + } + + // Parse the fetched SBOM + ctx = log.WithContextAttrs(ctx, log.FilePath(filePath)) + bom, err := sbom.Decode(ctx, bytes.NewReader(raw), format) + if err != nil { + return err + } + + if len(bom.Applications) > 0 { + h.logger.Info("Found SBOM attestation in Rekor", log.FilePath(filePath)) + // Take the first app since this SBOM should contain a single application. + app := bom.Applications[0] + app.FilePath = filePath // Use the original file path rather than the one in the SBOM. + blob.Applications = append(blob.Applications, app) + } + } + + return nil +} + +func (h unpackagedHook) Version() int { + return version +} + +func (h unpackagedHook) Type() types.HandlerType { + return types.UnpackagedPostHandler +} + +func (h unpackagedHook) Priority() int { + return types.UnpackagedPostHandlerPriority +} diff --git a/pkg/fanal/handler/unpackaged/unpackaged_test.go b/pkg/fanal/handler/unpackaged/unpackaged_test.go new file mode 100644 index 000000000000..cc522c678c46 --- /dev/null +++ b/pkg/fanal/handler/unpackaged/unpackaged_test.go @@ -0,0 +1,100 @@ +package unpackaged_test + +import ( + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + "github.com/aquasecurity/trivy/pkg/fanal/handler/unpackaged" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rekortest" +) + +func Test_unpackagedHook_Handle(t *testing.T) { + type args struct { + res *analyzer.AnalysisResult + blob *types.BlobInfo + } + tests := []struct { + name string + args args + want *types.BlobInfo + wantErr string + }{ + { + name: "happy path", + args: args{ + res: &analyzer.AnalysisResult{ + Digests: map[string]string{ + "go.mod": "sha256:23f4e10c43c7654e33a3c9570913c8c9c528292762f1a5c4a97253e9e4e4b238", + }, + }, + }, + want: &types.BlobInfo{ + Applications: []types.Application{ + { + Type: types.GoModule, + FilePath: "go.mod", + Packages: types.Packages{ + { + ID: "github.com/spf13/cobra@v1.5.0", + Name: "github.com/spf13/cobra", + Version: "1.5.0", + Identifier: types.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/spf13", + Name: "cobra", + Version: "1.5.0", + }, + BOMRef: "pkg:golang/github.com/spf13/cobra@1.5.0", + }, + }, + }, + }, + }, + }, + }, + { + name: "404", + args: args{ + res: &analyzer.AnalysisResult{ + Digests: map[string]string{ + "go.mod": "sha256:unknown", + }, + }, + }, + wantErr: "failed to search", + }, + } + + log.InitLogger(false, true) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := rekortest.NewServer(t) + defer ts.Close() + + // Set the testing URL + opt := artifact.Option{ + RekorURL: ts.URL(), + } + + got := &types.BlobInfo{} + h, err := unpackaged.NewUnpackagedHandler(opt) + require.NoError(t, err) + + err = h.Handle(t.Context(), tt.args.res, got) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/image/archive.go b/pkg/fanal/image/archive.go new file mode 100644 index 000000000000..f5953aa67907 --- /dev/null +++ b/pkg/fanal/image/archive.go @@ -0,0 +1,74 @@ +package image + +import ( + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/hashicorp/go-multierror" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func NewArchiveImage(fileName string) (types.Image, error) { + img, err := newImage(fileName) + if err != nil { + return nil, err + } + return archiveImage{ + name: fileName, + Image: img, + }, nil +} + +func newImage(fileName string) (v1.Image, error) { + var errs error + + // Docker archive + img, err := tryDockerArchive(fileName) + if err == nil { + // Return v1.Image if the file can be opened as Docker archive + return img, nil + } + errs = multierror.Append(errs, err) + + // OCI layout + img, err = tryOCI(fileName) + if err == nil { + // Return v1.Image if the directory can be opened as OCI Image Format + return img, nil + } + errs = multierror.Append(errs, err) + + return nil, errs +} + +type archiveImage struct { + v1.Image + name string +} + +func (img archiveImage) Name() string { + return img.name +} + +func (img archiveImage) ID() (string, error) { + return ID(img) +} + +// RepoTags returns the repository tags stored in the Docker archive's manifest.json +// For OCI layouts, this returns nil as the ref.name annotation is unreliable. +func (img archiveImage) RepoTags() []string { + // Check if the underlying image is a Docker archive + if da, ok := img.Image.(dockerArchive); ok { + return da.repoTags + } + // For OCI layout, return nil as org.opencontainers.image.ref.name is unreliable + return nil +} + +// RepoDigests returns nil as both Docker and OCI archives do not contain RepoDigests. +// RepoDigests are registry-specific metadata representing the manifest digest as stored in a registry. +// Archives only export image content and RepoTags, not RepoDigests. +// Note: While digest information may exist in OCI index.json annotations, +// these annotations are tool-specific and cannot be reliably used as RepoDigests. +func (archiveImage) RepoDigests() []string { + return nil +} diff --git a/pkg/fanal/image/archive_test.go b/pkg/fanal/image/archive_test.go new file mode 100644 index 000000000000..e926f02d67e9 --- /dev/null +++ b/pkg/fanal/image/archive_test.go @@ -0,0 +1,66 @@ +package image + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestArchiveImage_RepoTags(t *testing.T) { + tests := []struct { + name string + fileName string + wantRepoTags []string + }{ + { + name: "docker archive with tags", + fileName: "../test/testdata/alpine-311.tar.gz", + wantRepoTags: []string{"alpine:3.11"}, + }, + { + name: "oci archive without tags", + fileName: "../test/testdata/test.oci", + wantRepoTags: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + img, err := NewArchiveImage(tt.fileName) + require.NoError(t, err) + + gotRepoTags := img.RepoTags() + assert.Equal(t, tt.wantRepoTags, gotRepoTags) + }) + } +} + +func TestArchiveImage_RepoDigests(t *testing.T) { + tests := []struct { + name string + fileName string + wantRepoDigests []string + }{ + { + name: "docker archive", + fileName: "../test/testdata/alpine-311.tar.gz", + wantRepoDigests: nil, // Docker archives don't contain digest information + }, + { + name: "oci archive", + fileName: "../test/testdata/test.oci", + wantRepoDigests: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + img, err := NewArchiveImage(tt.fileName) + require.NoError(t, err) + + gotRepoDigests := img.RepoDigests() + assert.Equal(t, tt.wantRepoDigests, gotRepoDigests) + }) + } +} diff --git a/pkg/fanal/image/daemon.go b/pkg/fanal/image/daemon.go new file mode 100644 index 000000000000..64a2057467a4 --- /dev/null +++ b/pkg/fanal/image/daemon.go @@ -0,0 +1,58 @@ +package image + +import ( + "context" + + "github.com/google/go-containerregistry/pkg/name" + + "github.com/aquasecurity/trivy/pkg/fanal/image/daemon" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func tryDockerDaemon(ctx context.Context, imageName string, ref name.Reference, opt types.ImageOptions) (types.Image, func(), error) { + img, cleanup, err := daemon.DockerImage(ctx, ref, opt.DockerOptions.Host) + if err != nil { + return nil, nil, err + } + return daemonImage{ + Image: img, + name: imageName, + }, cleanup, nil + +} + +func tryPodmanDaemon(ctx context.Context, imageName string, _ name.Reference, opts types.ImageOptions) (types.Image, func(), error) { + img, cleanup, err := daemon.PodmanImage(ctx, imageName, opts.PodmanOptions.Host) + if err != nil { + return nil, nil, err + } + return daemonImage{ + Image: img, + name: imageName, + }, cleanup, nil +} + +func tryContainerdDaemon(ctx context.Context, imageName string, _ name.Reference, opts types.ImageOptions) (types.Image, func(), error) { + img, cleanup, err := daemon.ContainerdImage(ctx, imageName, opts) + if err != nil { + return nil, cleanup, err + } + + return daemonImage{ + Image: img, + name: imageName, + }, cleanup, nil +} + +type daemonImage struct { + daemon.Image + name string +} + +func (d daemonImage) Name() string { + return d.name +} + +func (d daemonImage) ID() (string, error) { + return ID(d) +} diff --git a/pkg/fanal/image/daemon/containerd.go b/pkg/fanal/image/daemon/containerd.go new file mode 100644 index 000000000000..8b1108b3de59 --- /dev/null +++ b/pkg/fanal/image/daemon/containerd.go @@ -0,0 +1,273 @@ +package daemon + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "os" + "strings" + "time" + + "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/content" + "github.com/containerd/containerd/v2/core/images/archive" + "github.com/containerd/containerd/v2/pkg/namespaces" + "github.com/containerd/platforms" + "github.com/distribution/reference" + v1 "github.com/google/go-containerregistry/pkg/v1" + dockerspec "github.com/moby/docker-image-spec/specs-go/v1" + dockerimage "github.com/moby/moby/api/types/image" + dockerClient "github.com/moby/moby/client" + "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + xos "github.com/aquasecurity/trivy/pkg/x/os" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +const ( + defaultContainerdSocket = "/run/containerd/containerd.sock" + defaultContainerdNamespace = "default" +) + +type familiarNamed string + +func (n familiarNamed) Name() string { + return strings.Split(string(n), ":")[0] +} + +func (n familiarNamed) Tag() string { + s := strings.Split(string(n), ":") + if len(s) < 2 { + return "" + } + + return s[1] +} + +func (n familiarNamed) String() string { + return string(n) +} + +func imageWriter(c *client.Client, img client.Image, platform types.Platform) imageSave { + return func(ctx context.Context, ref []string, _ ...dockerClient.ImageSaveOption) (dockerClient.ImageSaveResult, error) { + if len(ref) < 1 { + return nil, xerrors.New("no image reference") + } + imgOpts := archive.WithImage(c.ImageService(), ref[0]) + manifestOpts := archive.WithManifest(img.Target()) + + var platformMatchComparer platforms.MatchComparer + if platform.Platform == nil { + platformMatchComparer = platforms.DefaultStrict() + } else { + platformMatchComparer = img.Platform() + } + platOpts := archive.WithPlatform(platformMatchComparer) + pr, pw := io.Pipe() + go func() { + pw.CloseWithError(archive.Export(ctx, c.ContentStore(), pw, imgOpts, manifestOpts, platOpts)) + }() + return pr, nil + } +} + +// ContainerdImage implements v1.Image +func ContainerdImage(ctx context.Context, imageName string, opts types.ImageOptions) (Image, func(), error) { + cleanup := func() {} + + addr := os.Getenv("CONTAINERD_ADDRESS") + if addr == "" { + // TODO: support rootless + addr = defaultContainerdSocket + } + + if _, err := os.Stat(addr); errors.Is(err, os.ErrNotExist) { + return nil, cleanup, xerrors.Errorf("containerd socket not found: %s", addr) + } + + ref, searchFilters, err := parseReference(imageName) + if err != nil { + return nil, cleanup, err + } + + var options []client.Opt + if opts.RegistryOptions.Platform.Platform != nil { + ociPlatform, err := platforms.Parse(opts.RegistryOptions.Platform.String()) + if err != nil { + return nil, cleanup, err + } + + options = append(options, client.WithDefaultPlatform(platforms.OnlyStrict(ociPlatform))) + } + + c, err := client.New(addr, options...) + if err != nil { + return nil, cleanup, xerrors.Errorf("failed to initialize a containerd client: %w", err) + } + + namespace := os.Getenv("CONTAINERD_NAMESPACE") + if namespace == "" { + namespace = defaultContainerdNamespace + } + + ctx = namespaces.WithNamespace(ctx, namespace) + + imgs, err := c.ListImages(ctx, searchFilters...) + if err != nil { + return nil, cleanup, xerrors.Errorf("failed to list images from containerd client: %w", err) + } + + if len(imgs) < 1 { + return nil, cleanup, xerrors.Errorf("image not found in containerd store: %s", imageName) + } + + img := imgs[0] + + f, err := xos.CreateTemp("", "containerd-export-") + if err != nil { + return nil, cleanup, xerrors.Errorf("failed to create a temporary file: %w", err) + } + + cleanup = func() { + _ = c.Close() + _ = f.Close() + _ = os.Remove(f.Name()) + } + + insp, history, ref, err := inspect(ctx, img, ref) + if err != nil { + return nil, cleanup, xerrors.Errorf("inspect error: %w", err) + } + + return &image{ + opener: imageOpener(ctx, ref.String(), f, imageWriter(c, img, opts.RegistryOptions.Platform)), + inspect: insp, + history: history, + }, cleanup, nil +} + +func parseReference(imageName string) (reference.Reference, []string, error) { + ref, err := reference.ParseAnyReference(imageName) + if err != nil { + return nil, nil, xerrors.Errorf("parse error: %w", err) + } + + d, isDigested := ref.(reference.Digested) + n, isNamed := ref.(reference.Named) + nt, isNamedAndTagged := ref.(reference.NamedTagged) + + // a name plus a digest + // example: name@sha256:41adb3ef... + if isDigested && isNamed { + dgst := d.Digest() + // for the filters, each slice entry is logically or'd. each + // comma-separated filter is logically anded + return ref, []string{ + fmt.Sprintf(`name~="^%s(:|@).*",target.digest==%q`, n.Name(), dgst), + fmt.Sprintf(`name~="^%s(:|@).*",target.digest==%q`, reference.FamiliarName(n), dgst), + }, nil + } + + // digested, but not named. i.e. a plain digest + // example: sha256:41adb3ef... + if isDigested { + return ref, []string{fmt.Sprintf(`target.digest==%q`, d.Digest())}, nil + } + + // a name plus a tag + // example: name:tag + if isNamedAndTagged { + tag := nt.Tag() + return familiarNamed(imageName), []string{ + fmt.Sprintf(`name=="%s:%s"`, nt.Name(), tag), + fmt.Sprintf(`name=="%s:%s"`, reference.FamiliarName(nt), tag), + }, nil + } + + return nil, nil, xerrors.Errorf("failed to parse image reference: %s", imageName) +} + +// readImageConfig reads the config spec (`application/vnd.oci.image.config.v1+json`) for img.platform from content store. +// ported from https://github.com/containerd/nerdctl/blob/7dfbaa2122628921febeb097e7a8a86074dc931d/pkg/imgutil/imgutil.go#L377-L393 +func readImageConfig(ctx context.Context, img client.Image) (ocispec.Image, ocispec.Descriptor, error) { + var config ocispec.Image + + configDesc, err := img.Config(ctx) // aware of img.platform + if err != nil { + return config, configDesc, err + } + p, err := content.ReadBlob(ctx, img.ContentStore(), configDesc) + if err != nil { + return config, configDesc, err + } + if err = json.Unmarshal(p, &config); err != nil { + return config, configDesc, err + } + return config, configDesc, nil +} + +// ported from https://github.com/containerd/nerdctl/blob/d110fea18018f13c3f798fa6565e482f3ff03591/pkg/inspecttypes/dockercompat/dockercompat.go#L279-L321 +func inspect(ctx context.Context, img client.Image, ref reference.Reference) (dockerimage.InspectResponse, []v1.History, reference.Reference, error) { + if _, ok := ref.(reference.Digested); ok { + ref = familiarNamed(img.Name()) + } + + var tag string + if tagged, ok := ref.(reference.Tagged); ok { + tag = tagged.Tag() + } + + var repository string + if n, isNamed := ref.(reference.Named); isNamed { + repository = reference.FamiliarName(n) + } + + imgConfig, imgConfigDesc, err := readImageConfig(ctx, img) + if err != nil { + return dockerimage.InspectResponse{}, nil, nil, err + } + + var lastHistory ocispec.History + if len(imgConfig.History) > 0 { + lastHistory = imgConfig.History[len(imgConfig.History)-1] + } + + var history []v1.History + for _, h := range imgConfig.History { + history = append(history, v1.History{ + Author: h.Author, + Created: v1.Time{Time: *h.Created}, + CreatedBy: h.CreatedBy, + Comment: h.Comment, + EmptyLayer: h.EmptyLayer, + }) + } + + created := "" + if lastHistory.Created != nil { + created = lastHistory.Created.Format(time.RFC3339Nano) + } + + return dockerimage.InspectResponse{ + ID: imgConfigDesc.Digest.String(), + RepoTags: []string{fmt.Sprintf("%s:%s", repository, tag)}, + RepoDigests: []string{fmt.Sprintf("%s@%s", repository, img.Target().Digest)}, + Comment: lastHistory.Comment, + Created: created, + Author: lastHistory.Author, + Config: &dockerspec.DockerOCIImageConfig{ + ImageConfig: imgConfig.Config, + }, + Architecture: imgConfig.Architecture, + Os: imgConfig.OS, + RootFS: dockerimage.RootFS{ + Type: imgConfig.RootFS.Type, + Layers: xslices.Map(imgConfig.RootFS.DiffIDs, digest.Digest.String), + }, + }, history, ref, nil +} diff --git a/pkg/fanal/image/daemon/context.go b/pkg/fanal/image/daemon/context.go new file mode 100644 index 000000000000..707c35eb632f --- /dev/null +++ b/pkg/fanal/image/daemon/context.go @@ -0,0 +1,39 @@ +package daemon + +import ( + "github.com/docker/cli/cli/command" + "github.com/docker/cli/cli/config" + cliflags "github.com/docker/cli/cli/flags" + "golang.org/x/xerrors" +) + +// resolveDockerHost resolves the Docker daemon host based on priority: +// 1. --docker-host flag (highest priority) +// 2. DOCKER_HOST environment variable (handled by NewAPIClientFromFlags) +// 3. DOCKER_CONTEXT environment variable (handled by NewAPIClientFromFlags) +// 4. Current Docker context (default, handled by NewAPIClientFromFlags) +func resolveDockerHost(hostFlag string) (string, error) { + // --docker-host flag + if hostFlag != "" { + return hostFlag, nil + } + + // For DOCKER_HOST, DOCKER_CONTEXT and current context resolution, use docker/cli + // This approach validates context existence and returns proper errors + opts := &cliflags.ClientOptions{} + + // Load config from DOCKER_CONFIG or default location + configFile, err := config.Load("") + if err != nil { + return "", xerrors.Errorf("failed to load Docker config: %w", err) + } + + apiClient, err := command.NewAPIClientFromFlags(opts, configFile) + if err != nil { + return "", xerrors.Errorf("failed to create Docker API client: %w", err) + } + defer apiClient.Close() + + // Get the host from the client + return apiClient.DaemonHost(), nil +} diff --git a/pkg/fanal/image/daemon/context_test.go b/pkg/fanal/image/daemon/context_test.go new file mode 100644 index 000000000000..8e0a2ee142ff --- /dev/null +++ b/pkg/fanal/image/daemon/context_test.go @@ -0,0 +1,147 @@ +package daemon_test + +import ( + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/docker/cli/cli/config" + "github.com/docker/cli/cli/context/docker" + "github.com/docker/cli/cli/context/store" + dockerclient "github.com/moby/moby/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/image/daemon" +) + +const testContextName = "test-context" + +// createTestContext creates a Docker context using docker/cli context store API +func createTestContext(dockerConfigDir string) error { + // Create context store with proper configuration + cfg := store.NewConfig( + func() any { return &store.Metadata{} }, + store.EndpointTypeGetter(docker.DockerEndpoint, func() any { return &docker.EndpointMeta{} }), + ) + contextStore := store.New(dockerConfigDir, cfg) + + // Create context metadata + contextMetadata := store.Metadata{ + Name: testContextName, + Endpoints: map[string]any{ + docker.DockerEndpoint: docker.EndpointMeta{ + Host: testContextHost, + }, + }, + } + + // Create or update the context + return contextStore.CreateOrUpdate(contextMetadata) +} + +// TestResolveDockerHost tests Docker host resolution with various scenarios +// It's challenging to test it through DockerImage due to the need for a Docker daemon, +// so we test the resolveDockerHost function directly, although it's private. +func TestResolveDockerHost(t *testing.T) { + tests := []struct { + name string + hostFlag string + hostEnv string + contextEnv string + currentContext string + want string + wantErr string + }{ + { + name: "flag takes highest priority", + hostFlag: testFlagHost, + hostEnv: testEnvHost, + contextEnv: "", + currentContext: "", + want: testFlagHost, + }, + { + name: "DOCKER_HOST takes priority over context", + hostFlag: "", + hostEnv: testEnvHost, + contextEnv: "", + currentContext: "", + want: testEnvHost, + }, + { + name: "valid context is used", + hostFlag: "", + hostEnv: "", + contextEnv: testContextName, + currentContext: "", + want: testContextHost, + }, + { + name: "current context is used when no options", + hostFlag: "", + hostEnv: "", + contextEnv: "", + currentContext: testContextName, + want: testContextHost, + }, + { + name: "default context uses default socket when no options", + hostFlag: "", + hostEnv: "", + contextEnv: "", + currentContext: "", + want: dockerclient.DefaultDockerHost, + }, + { + name: "invalid context fails", + hostFlag: "", + hostEnv: "", + contextEnv: "non-existent-context", + currentContext: "", + wantErr: "failed to create Docker API client", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary Docker config directory + testDir := t.TempDir() + + t.Setenv("DOCKER_CONFIG", testDir) + t.Setenv("DOCKER_HOST", tt.hostEnv) + t.Setenv("DOCKER_CONTEXT", tt.contextEnv) + + // Set the config directory for docker/cli to use + // This is required to handle global state in docker/cli config. + // Due to sync.Once in docker/cli, this cannot be fully cleaned up after tests. + config.SetDir(testDir) + + // Always create a test context + contextDir := filepath.Join(testDir, "contexts") + + err := createTestContext(contextDir) + require.NoError(t, err) + + // Create config.json + configData := map[string]any{ + "currentContext": tt.currentContext, + } + + configJSON, err := json.MarshalIndent(configData, "", " ") + require.NoError(t, err) + require.NoError(t, os.WriteFile(filepath.Join(testDir, "config.json"), configJSON, 0o644)) + + // Test resolveDockerHost + got, err := daemon.ResolveDockerHost(tt.hostFlag) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/image/daemon/context_unix_test.go b/pkg/fanal/image/daemon/context_unix_test.go new file mode 100644 index 000000000000..87779903dbb2 --- /dev/null +++ b/pkg/fanal/image/daemon/context_unix_test.go @@ -0,0 +1,11 @@ +//go:build !windows + +package daemon_test + +const ( + testContextHost = "unix:///tmp/test-context.sock" + + // Test socket paths for Unix systems + testFlagHost = "unix:///tmp/flag-docker.sock" + testEnvHost = "unix:///tmp/env-docker.sock" +) diff --git a/pkg/fanal/image/daemon/context_windows_test.go b/pkg/fanal/image/daemon/context_windows_test.go new file mode 100644 index 000000000000..4ca6ef39e31c --- /dev/null +++ b/pkg/fanal/image/daemon/context_windows_test.go @@ -0,0 +1,9 @@ +package daemon_test + +const ( + testContextHost = "npipe:////./pipe/test_docker_engine" + + // Test socket paths for Windows systems + testFlagHost = "npipe:////./pipe/flag_docker" + testEnvHost = "npipe:////./pipe/env_docker" +) diff --git a/pkg/fanal/image/daemon/docker.go b/pkg/fanal/image/daemon/docker.go new file mode 100644 index 000000000000..bc3e24484da7 --- /dev/null +++ b/pkg/fanal/image/daemon/docker.go @@ -0,0 +1,76 @@ +package daemon + +import ( + "context" + "os" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/moby/moby/client" + "golang.org/x/xerrors" + + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +// DockerImage implements v1.Image by extending daemon.Image. +// The caller must call cleanup() to remove a temporary file. +func DockerImage(ctx context.Context, ref name.Reference, host string) (Image, func(), error) { + cleanup := func() {} + + // Resolve Docker host based on priority: --docker-host > DOCKER_HOST > DOCKER_CONTEXT > current context + resolvedHost, err := resolveDockerHost(host) + if err != nil { + return nil, cleanup, xerrors.Errorf("failed to resolve Docker host: %w", err) + } + + opts := []client.Opt{ + client.FromEnv, + } + if resolvedHost != "" { + opts = append(opts, client.WithHost(resolvedHost)) + } + c, err := client.New(opts...) + + if err != nil { + return nil, cleanup, xerrors.Errorf("failed to initialize a docker client: %w", err) + } + defer func() { + if err != nil { + _ = c.Close() + } + }() + + // : pattern like "alpine:3.15" + // or + // @ pattern like "alpine@sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300" + imageID := ref.Name() + inspect, err := c.ImageInspect(ctx, imageID) + if err != nil { + imageID = ref.String() // pattern like `5ac716b05a9c` + inspect, err = c.ImageInspect(ctx, imageID) + if err != nil { + return nil, cleanup, xerrors.Errorf("unable to inspect the image (%s): %w", imageID, err) + } + } + + history, err := c.ImageHistory(ctx, imageID) + if err != nil { + return nil, cleanup, xerrors.Errorf("unable to get history (%s): %w", imageID, err) + } + + f, err := xos.CreateTemp("", "docker-export-") + if err != nil { + return nil, cleanup, xerrors.Errorf("failed to create a temporary file: %w", err) + } + + cleanup = func() { + _ = c.Close() + _ = f.Close() + _ = os.Remove(f.Name()) + } + + return &image{ + opener: imageOpener(ctx, imageID, f, c.ImageSave), + inspect: inspect.InspectResponse, + history: configHistory(history.Items), + }, cleanup, nil +} diff --git a/pkg/fanal/image/daemon/docker_test.go b/pkg/fanal/image/daemon/docker_test.go new file mode 100644 index 000000000000..121bf6b1c9bf --- /dev/null +++ b/pkg/fanal/image/daemon/docker_test.go @@ -0,0 +1,49 @@ +package daemon + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDockerImage(t *testing.T) { + type fields struct { + Image v1.Image + opener opener + } + tests := []struct { + name string + imageName string + fields fields + want *v1.ConfigFile + wantErr bool + }{ + { + name: "happy path", + imageName: "alpine:3.11", + wantErr: false, + }, + { + name: "unknown image", + imageName: "alpine:unknown", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := name.ParseReference(tt.imageName) + require.NoError(t, err) + + _, cleanup, err := DockerImage(t.Context(), ref, "") + assert.Equal(t, tt.wantErr, err != nil, err) + defer func() { + if cleanup != nil { + cleanup() + } + }() + }) + } +} diff --git a/pkg/fanal/image/daemon/export_test.go b/pkg/fanal/image/daemon/export_test.go new file mode 100644 index 000000000000..bddd8e9f8372 --- /dev/null +++ b/pkg/fanal/image/daemon/export_test.go @@ -0,0 +1,6 @@ +package daemon + +// Bridge to expose daemon internals to tests in the daemon_test package. + +// ResolveDockerHost exports resolveDockerHost for testing. +var ResolveDockerHost = resolveDockerHost diff --git a/pkg/fanal/image/daemon/image.go b/pkg/fanal/image/daemon/image.go new file mode 100644 index 000000000000..901f0e75a67b --- /dev/null +++ b/pkg/fanal/image/daemon/image.go @@ -0,0 +1,284 @@ +package daemon + +import ( + "context" + "io" + "os" + "strings" + "sync" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" + dockerspec "github.com/moby/docker-image-spec/specs-go/v1" + dimage "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" +) + +type Image interface { + v1.Image + RepoTags() []string + RepoDigests() []string +} + +var mu sync.Mutex + +type opener func() (v1.Image, error) + +type imageSave func(context.Context, []string, ...client.ImageSaveOption) (client.ImageSaveResult, error) + +func imageOpener(ctx context.Context, ref string, f *os.File, imageSave imageSave) opener { + return func() (v1.Image, error) { + // Store the tarball in local filesystem and return a new reader into the bytes each time we need to access something. + rc, err := imageSave(ctx, []string{ref}) + if err != nil { + return nil, xerrors.Errorf("unable to export the image: %w", err) + } + defer rc.Close() + + if _, err = io.Copy(f, rc); err != nil { + return nil, xerrors.Errorf("failed to copy the image: %w", err) + } + defer f.Close() + + img, err := tarball.ImageFromPath(f.Name(), nil) + if err != nil { + return nil, xerrors.Errorf("failed to initialize the struct from the temporary file: %w", err) + } + + return img, nil + } +} + +// image is a wrapper for github.com/google/go-containerregistry/pkg/v1/daemon.Image +// daemon.Image loads the entire image into the memory at first, +// but it doesn't need to load it if the information is already in the cache, +// To avoid entire loading, this wrapper uses ImageInspectWithRaw and checks image ID and layer IDs. +type image struct { + v1.Image + opener opener + inspect dimage.InspectResponse + history []v1.History +} + +// populateImage initializes an "image" struct. +// This method is called by some goroutines at the same time. +// To prevent multiple heavy initializations, the lock is necessary. +func (img *image) populateImage() (err error) { + mu.Lock() + defer mu.Unlock() + + // img.Image is already initialized, so we don't have to do it again. + if img.Image != nil { + return nil + } + + img.Image, err = img.opener() + if err != nil { + return xerrors.Errorf("unable to open: %w", err) + } + + return nil +} + +func (img *image) ConfigName() (v1.Hash, error) { + return v1.NewHash(img.inspect.ID) +} + +func (img *image) ConfigFile() (*v1.ConfigFile, error) { + if len(img.inspect.RootFS.Layers) == 0 { + // Podman doesn't return RootFS... + return img.configFile() + } + + nonEmptyLayerCount := lo.CountBy(img.history, func(history v1.History) bool { + return !history.EmptyLayer + }) + + if len(img.inspect.RootFS.Layers) != nonEmptyLayerCount { + // In cases where empty layers are not correctly determined from the history API. + // There are some edge cases where we cannot guess empty layers well. + return img.configFile() + } + + diffIDs, err := img.diffIDs() + if err != nil { + return nil, xerrors.Errorf("unable to get diff IDs: %w", err) + } + + var created v1.Time + // `Created` field can be empty. Skip parsing to avoid error. + // cf. https://github.com/moby/moby/blob/8e96db1c328d0467b015768e42a62c0f834970bb/api/types/types.go#L76-L77 + if img.inspect.Created != "" { + var t time.Time + t, err = time.Parse(time.RFC3339Nano, img.inspect.Created) + if err != nil { + return nil, xerrors.Errorf("failed parsing created %s: %w", img.inspect.Created, err) + } + created = v1.Time{ + Time: t, + } + } + + return &v1.ConfigFile{ + Architecture: img.inspect.Architecture, + Author: img.inspect.Author, + Created: created, + Config: img.imageConfig(lo.FromPtr(img.inspect.Config)), + History: img.history, + OS: img.inspect.Os, + RootFS: v1.RootFS{ + Type: img.inspect.RootFS.Type, + DiffIDs: diffIDs, + }, + }, nil +} + +func (img *image) configFile() (*v1.ConfigFile, error) { + log.Debug("Saving the container image to a local file to obtain the image config...") + + // Need to fall back into expensive operations like "docker save" + // because the config file cannot be generated properly from container engine API for some reason. + if err := img.populateImage(); err != nil { + return nil, xerrors.Errorf("unable to populate: %w", err) + } + return img.Image.ConfigFile() +} + +func (img *image) LayerByDiffID(h v1.Hash) (v1.Layer, error) { + if err := img.populateImage(); err != nil { + return nil, xerrors.Errorf("unable to populate: %w", err) + } + return img.Image.LayerByDiffID(h) +} + +func (img *image) RawConfigFile() ([]byte, error) { + if err := img.populateImage(); err != nil { + return nil, xerrors.Errorf("unable to populate: %w", err) + } + return img.Image.RawConfigFile() +} + +func (img *image) RepoTags() []string { + return img.inspect.RepoTags +} + +func (img *image) RepoDigests() []string { + return img.inspect.RepoDigests +} + +func (img *image) diffIDs() ([]v1.Hash, error) { + var diffIDs []v1.Hash + for _, l := range img.inspect.RootFS.Layers { + h, err := v1.NewHash(l) + if err != nil { + return nil, xerrors.Errorf("invalid hash %s: %w", l, err) + } + diffIDs = append(diffIDs, h) + } + return diffIDs, nil +} + +func (img *image) imageConfig(config dockerspec.DockerOCIImageConfig) v1.Config { + c := v1.Config{ + // OCI-compliant fields + User: config.User, + Cmd: config.Cmd, + Entrypoint: config.Entrypoint, + Env: config.Env, + Labels: config.Labels, + WorkingDir: config.WorkingDir, + StopSignal: config.StopSignal, + ArgsEscaped: config.ArgsEscaped, + OnBuild: config.OnBuild, + Shell: config.Shell, + } + + if len(config.ExposedPorts) > 0 { + c.ExposedPorts = make(map[string]struct{}) //nolint: gocritic + for port := range config.ExposedPorts { + c.ExposedPorts[port] = struct{}{} + } + } + + if len(config.Volumes) > 0 { + c.Volumes = make(map[string]struct{}) //nolint: gocritic + for volume := range config.Volumes { + c.Volumes[volume] = struct{}{} + } + } + + if config.Healthcheck != nil { + c.Healthcheck = &v1.HealthConfig{ + Test: config.Healthcheck.Test, + Interval: config.Healthcheck.Interval, + Timeout: config.Healthcheck.Timeout, + StartPeriod: config.Healthcheck.StartPeriod, + Retries: config.Healthcheck.Retries, + } + } + + return c +} + +func configHistory(dhistory []dimage.HistoryResponseItem) []v1.History { + // Fill only required metadata + var history []v1.History + + for i := len(dhistory) - 1; i >= 0; i-- { + h := dhistory[i] + history = append(history, v1.History{ + Created: v1.Time{ + Time: time.Unix(h.Created, 0).UTC(), + }, + CreatedBy: h.CreatedBy, + Comment: h.Comment, + EmptyLayer: emptyLayer(h), + }) + } + return history +} + +// emptyLayer tries to determine if the layer is empty from the history API, but may return a wrong result. +// The non-empty layers will be compared to diffIDs later so that results can be validated. +func emptyLayer(history dimage.HistoryResponseItem) bool { + if history.Size != 0 { + return false + } + createdBy := strings.TrimSpace(strings.TrimLeft(history.CreatedBy, "/bin/sh -c #(nop)")) + // This logic is taken from https://github.com/moby/buildkit/blob/2942d13ff489a2a49082c99e6104517e357e53ad/frontend/dockerfile/dockerfile2llb/convert.go + if strings.HasPrefix(createdBy, "ENV") || + strings.HasPrefix(createdBy, "MAINTAINER") || + strings.HasPrefix(createdBy, "LABEL") || + strings.HasPrefix(createdBy, "CMD") || + strings.HasPrefix(createdBy, "ENTRYPOINT") || + strings.HasPrefix(createdBy, "HEALTHCHECK") || + strings.HasPrefix(createdBy, "EXPOSE") || + strings.HasPrefix(createdBy, "USER") || + strings.HasPrefix(createdBy, "VOLUME") || + strings.HasPrefix(createdBy, "STOPSIGNAL") || + strings.HasPrefix(createdBy, "SHELL") || + strings.HasPrefix(createdBy, "ARG") { + return true + } + // buildkit layers with "WORKDIR /" command are empty, + if strings.HasPrefix(history.Comment, "buildkit.dockerfile") { + if createdBy == "WORKDIR /" { + return true + } + } else if strings.HasPrefix(createdBy, "WORKDIR") { // layers build with docker and podman, WORKDIR command is always empty layer. + return true + } + // The following instructions could reach here: + // - "ADD" + // - "COPY" + // - "RUN" + // - "RUN" may not include even 'RUN' prefix + // e.g. '/bin/sh -c mkdir test ' + // - "WORKDIR", which doesn't meet the above conditions + return false +} diff --git a/pkg/fanal/image/daemon/image_test.go b/pkg/fanal/image/daemon/image_test.go new file mode 100644 index 000000000000..a78d905d54e4 --- /dev/null +++ b/pkg/fanal/image/daemon/image_test.go @@ -0,0 +1,426 @@ +package daemon + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + dimage "github.com/moby/moby/api/types/image" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/testdocker/engine" +) + +var imagePaths = map[string]string{ + "alpine:3.10": "../../test/testdata/alpine-310.tar.gz", + "alpine:3.11": "../../test/testdata/alpine-311.tar.gz", + "gcr.io/distroless/base": "../../test/testdata/distroless.tar.gz", +} + +// for Docker +var opt = engine.Option{ + APIVersion: "1.45", + ImagePaths: imagePaths, +} + +func TestMain(m *testing.M) { + te := engine.NewDockerEngine(opt) + defer te.Close() + + os.Setenv("DOCKER_HOST", fmt.Sprintf("tcp://%s", te.Listener.Addr().String())) + + os.Exit(m.Run()) +} + +func Test_image_ConfigName(t *testing.T) { + tests := []struct { + name string + imageName string + want v1.Hash + wantErr bool + }{ + { + name: "happy path", + imageName: "alpine:3.11", + want: v1.Hash{ + Algorithm: "sha256", + Hex: "a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := name.ParseReference(tt.imageName) + require.NoError(t, err) + + img, cleanup, err := DockerImage(t.Context(), ref, "") + require.NoError(t, err) + defer cleanup() + + conf, err := img.ConfigName() + assert.Equal(t, tt.want, conf) + assert.Equal(t, tt.wantErr, err != nil) + }) + } +} + +func Test_image_ConfigNameWithCustomDockerHost(t *testing.T) { + + ref, err := name.ParseReference("alpine:3.11") + require.NoError(t, err) + + eo := engine.Option{ + APIVersion: opt.APIVersion, + ImagePaths: opt.ImagePaths, + } + + var dockerHostParam string + + if runtime.GOOS != "windows" { + dir, err := os.MkdirTemp("", "image") //nolint:usetesting // Too long file paths created by t.TempDir() cause an invalid argument error with socket binding + require.NoError(t, err) + err = os.MkdirAll(dir, os.ModePerm) + require.NoError(t, err) + + customDockerHost := filepath.Join(dir, "image-test-unix-socket.sock") + eo.UnixDomainSocket = customDockerHost + dockerHostParam = "unix://" + customDockerHost + } + + te := engine.NewDockerEngine(eo) + defer te.Close() + + if runtime.GOOS == "windows" { + dockerHostParam = te.Listener.Addr().Network() + "://" + te.Listener.Addr().String() + } + + img, cleanup, err := DockerImage(t.Context(), ref, dockerHostParam) + require.NoError(t, err) + defer cleanup() + + conf, err := img.ConfigName() + assert.Equal(t, v1.Hash{ + Algorithm: "sha256", + Hex: "a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, conf) + require.NoError(t, err) +} + +func Test_image_ConfigNameWithCustomPodmanHost(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("podman.sock is not available for Windows CI") + } + + ref, err := name.ParseReference("alpine:3.11") + require.NoError(t, err) + + eo := engine.Option{ + APIVersion: opt.APIVersion, + ImagePaths: map[string]string{ + "index.docker.io/library/alpine:3.11": "../../test/testdata/alpine-311.tar.gz", + }, + } + + dir, err := os.MkdirTemp("", "image") //nolint:usetesting // Too long file paths created by t.TempDir() cause an invalid argument error with socket binding + require.NoError(t, err) + err = os.MkdirAll(dir, os.ModePerm) + require.NoError(t, err) + + podmanSocket := filepath.Join(dir, "image-test-podman-socket.sock") + eo.UnixDomainSocket = podmanSocket + + te := engine.NewDockerEngine(eo) + defer te.Close() + + img, cleanup, err := PodmanImage(t.Context(), ref.Name(), podmanSocket) + require.NoError(t, err) + defer cleanup() + + conf, err := img.ConfigName() + assert.Equal(t, v1.Hash{ + Algorithm: "sha256", + Hex: "a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + }, conf) + require.NoError(t, err) +} + +func Test_image_ConfigFile(t *testing.T) { + tests := []struct { + name string + imageName string + want *v1.ConfigFile + wantErr bool + }{ + { + name: "one diff_id", + imageName: "alpine:3.11", + want: &v1.ConfigFile{ + Architecture: "amd64", + OS: "linux", + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)}, + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + Comment: "", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ", + EmptyLayer: false, + }, + }, + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{"/bin/sh"}, + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + ArgsEscaped: true, + }, + OSVersion: "", + }, + wantErr: false, + }, + { + name: "multiple diff_ids", + imageName: "gcr.io/distroless/base", + want: &v1.ConfigFile{ + Architecture: "amd64", + OS: "linux", + Author: "Bazel", + Created: v1.Time{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + CreatedBy: "bazel build ...", + EmptyLayer: false, + }, + { + Created: v1.Time{Time: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)}, + CreatedBy: "bazel build ...", + EmptyLayer: false, + }, + }, + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + {Algorithm: "sha256", Hex: "42a3027eaac150d2b8f516100921f4bd83b3dbc20bfe64124f686c072b49c602"}, + {Algorithm: "sha256", Hex: "f47163e8de57e3e3ccfe89d5dfbd9c252d9eca53dc7906b8db60eddcb876c592"}, + }, + }, + Config: v1.Config{ + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt", + }, + }, + OSVersion: "", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := name.ParseReference(tt.imageName) + require.NoError(t, err) + + img, cleanup, err := DockerImage(t.Context(), ref, "") + require.NoError(t, err) + defer cleanup() + + conf, err := img.ConfigFile() + require.Equal(t, tt.wantErr, err != nil, err) + assert.Equal(t, tt.want, conf) + }) + } +} + +func Test_image_LayerByDiffID(t *testing.T) { + type args struct { + h v1.Hash + } + tests := []struct { + name string + imageName string + args args + wantErr bool + }{ + { + name: "happy path", + imageName: "alpine:3.10", + args: args{ + h: v1.Hash{ + Algorithm: "sha256", + Hex: "531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + wantErr: false, + }, + { + name: "ImageSave returns 404", + imageName: "alpine:3.11", + args: args{ + h: v1.Hash{ + Algorithm: "sha256", + Hex: "531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := name.ParseReference(tt.imageName) + require.NoError(t, err) + + img, cleanup, err := DockerImage(t.Context(), ref, "") + require.NoError(t, err) + defer cleanup() + + _, err = img.LayerByDiffID(tt.args.h) + assert.Equal(t, tt.wantErr, err != nil, err) + }) + } +} + +func Test_image_RawConfigFile(t *testing.T) { + tests := []struct { + name string + imageName string + goldenFile string + wantErr bool + }{ + { + name: "happy path", + imageName: "alpine:3.10", + goldenFile: "testdata/golden/config-alpine310.json", + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := name.ParseReference(tt.imageName) + require.NoError(t, err) + + img, cleanup, err := DockerImage(t.Context(), ref, "") + require.NoError(t, err) + defer cleanup() + + got, err := img.RawConfigFile() + assert.Equal(t, tt.wantErr, err != nil, err) + + if err != nil { + return + } + + want, err := os.ReadFile(tt.goldenFile) + require.NoError(t, err) + + require.JSONEq(t, string(want), string(got)) + }) + } +} + +func Test_image_emptyLayer(t *testing.T) { + tests := []struct { + name string + history dimage.HistoryResponseItem + want bool + }{ + { + name: "size != 0", + history: dimage.HistoryResponseItem{ + Size: 10, + }, + want: false, + }, + { + name: "ENV", + history: dimage.HistoryResponseItem{ + CreatedBy: "/bin/sh -c #(nop) ENV TESTENV=TEST", + }, + want: true, + }, + { + name: "ENV created with buildkit", + history: dimage.HistoryResponseItem{ + CreatedBy: "ENV BUILDKIT_ENV=TEST", + Comment: "buildkit.dockerfile.v0", + }, + want: true, + }, + { + name: "ENV", + history: dimage.HistoryResponseItem{ + CreatedBy: "/bin/sh -c #(nop) ENV TESTENV=TEST", + }, + want: true, + }, + { + name: "CMD", + history: dimage.HistoryResponseItem{ + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + }, + want: true, + }, + { + name: "WORKDIR == '/'", + history: dimage.HistoryResponseItem{ + CreatedBy: "/bin/sh -c #(nop) WORKDIR /", + }, + want: true, + }, + { + name: "WORKDIR != '/'", + history: dimage.HistoryResponseItem{ + CreatedBy: "/bin/sh -c #(nop) WORKDIR /app", + }, + want: true, + }, + { + name: "WORKDIR =='/' buildkit", + history: dimage.HistoryResponseItem{ + CreatedBy: "/bin/sh -c #(nop) WORKDIR /", + Comment: "buildkit.dockerfile.v0", + }, + want: true, + }, + { + name: "WORKDIR == '/app' buildkit", + history: dimage.HistoryResponseItem{ + CreatedBy: "/bin/sh -c #(nop) WORKDIR /app", + Comment: "buildkit.dockerfile.v0", + }, + want: false, + }, + { + name: "without command", + history: dimage.HistoryResponseItem{ + CreatedBy: "/bin/sh -c mkdir test", + }, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + empty := emptyLayer(tt.history) + assert.Equal(t, tt.want, empty) + }) + } +} diff --git a/pkg/fanal/image/daemon/podman.go b/pkg/fanal/image/daemon/podman.go new file mode 100644 index 000000000000..8616c86fe1d6 --- /dev/null +++ b/pkg/fanal/image/daemon/podman.go @@ -0,0 +1,160 @@ +package daemon + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "path/filepath" + + dimage "github.com/moby/moby/api/types/image" + "github.com/moby/moby/client" + "golang.org/x/xerrors" + + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +var ( + inspectURL = "http://podman/images/%s/json" + historyURL = "http://podman/images/%s/history" + saveURL = "http://podman/images/%s/get" +) + +type podmanClient struct { + c http.Client +} + +func newPodmanClient(host string) (podmanClient, error) { + // Get Podman socket location + sockDir := os.Getenv("XDG_RUNTIME_DIR") + socket := filepath.Join(sockDir, "podman", "podman.sock") + if host != "" { + socket = host + } + + if _, err := os.Stat(socket); err != nil { + return podmanClient{}, xerrors.Errorf("no podman socket found: %w", err) + } + + return podmanClient{ + c: http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial("unix", socket) + }, + }, + }, + }, nil +} + +type errResponse struct { + Message string +} + +func (p podmanClient) imageInspect(ctx context.Context, imageName string) (dimage.InspectResponse, error) { + url := fmt.Sprintf(inspectURL, imageName) + resp, err := p.get(ctx, url) + if err != nil { + return dimage.InspectResponse{}, xerrors.Errorf("http error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var res errResponse + if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + return dimage.InspectResponse{}, xerrors.Errorf("unknown status code from Podman: %d", resp.StatusCode) + } + return dimage.InspectResponse{}, xerrors.New(res.Message) + } + + var inspect dimage.InspectResponse + if err = json.NewDecoder(resp.Body).Decode(&inspect); err != nil { + return dimage.InspectResponse{}, xerrors.Errorf("unable to decode JSON: %w", err) + } + return inspect, nil +} + +func (p podmanClient) imageHistoryInspect(ctx context.Context, imageName string) ([]dimage.HistoryResponseItem, error) { + url := fmt.Sprintf(historyURL, imageName) + resp, err := p.get(ctx, url) + if err != nil { + return []dimage.HistoryResponseItem{}, xerrors.Errorf("http error: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + var res errResponse + if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + return []dimage.HistoryResponseItem{}, xerrors.Errorf("unknown status code from Podman: %d", resp.StatusCode) + } + return []dimage.HistoryResponseItem{}, xerrors.New(res.Message) + } + + var history []dimage.HistoryResponseItem + if err = json.NewDecoder(resp.Body).Decode(&history); err != nil { + return []dimage.HistoryResponseItem{}, xerrors.Errorf("unable to decode JSON: %w", err) + } + return history, nil +} + +func (p podmanClient) imageSave(ctx context.Context, imageNames []string, _ ...client.ImageSaveOption) (client.ImageSaveResult, error) { + if len(imageNames) < 1 { + return nil, xerrors.Errorf("no specified image") + } + url := fmt.Sprintf(saveURL, imageNames[0]) + resp, err := p.get(ctx, url) + if err != nil { + if resp != nil { + resp.Body.Close() + } + return nil, xerrors.Errorf("http error: %w", err) + } + return resp.Body, nil +} + +func (p podmanClient) get(ctx context.Context, url string) (*http.Response, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) + if err != nil { + return nil, err + } + + return p.c.Do(req) +} + +// PodmanImage implements v1.Image by extending daemon.Image. +// The caller must call cleanup() to remove a temporary file. +func PodmanImage(ctx context.Context, ref, host string) (Image, func(), error) { + cleanup := func() {} + + c, err := newPodmanClient(host) + if err != nil { + return nil, cleanup, xerrors.Errorf("unable to initialize Podman client: %w", err) + } + inspect, err := c.imageInspect(ctx, ref) + if err != nil { + return nil, cleanup, xerrors.Errorf("unable to inspect the image (%s): %w", ref, err) + } + + history, err := c.imageHistoryInspect(ctx, ref) + if err != nil { + return nil, cleanup, xerrors.Errorf("unable to inspect the image (%s): %w", ref, err) + } + + f, err := xos.CreateTemp("", "podman-export-") + if err != nil { + return nil, cleanup, xerrors.Errorf("failed to create a temporary file: %w", err) + } + + cleanup = func() { + _ = f.Close() + _ = os.Remove(f.Name()) + } + + return &image{ + opener: imageOpener(ctx, ref, f, c.imageSave), + inspect: inspect, + history: configHistory(history), + }, cleanup, nil +} diff --git a/pkg/fanal/image/daemon/podman_test.go b/pkg/fanal/image/daemon/podman_test.go new file mode 100644 index 000000000000..599be5344319 --- /dev/null +++ b/pkg/fanal/image/daemon/podman_test.go @@ -0,0 +1,108 @@ +package daemon + +import ( + "net/http/httptest" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + dockerimage "github.com/moby/moby/api/types/image" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/testdocker/engine" +) + +func setupPodmanSock(t *testing.T) *httptest.Server { + t.Helper() + + runtimeDir := t.TempDir() + + t.Setenv("XDG_RUNTIME_DIR", runtimeDir) + + dir := filepath.Join(runtimeDir, "podman") + err := os.MkdirAll(dir, os.ModePerm) + require.NoError(t, err) + + sockPath := filepath.Join(dir, "podman.sock") + + opt := engine.Option{ + APIVersion: "1.45", + ImagePaths: map[string]string{ + "index.docker.io/library/alpine:3.11": "../../test/testdata/alpine-311.tar.gz", + }, + UnixDomainSocket: sockPath, + } + te := engine.NewDockerEngine(opt) + return te +} + +func TestPodmanImage(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("podman.sock is not available for Windows CI") + } + + type fields struct { + Image v1.Image + opener opener + inspect dockerimage.InspectResponse + } + tests := []struct { + name string + imageName string + fields fields + wantConfigName string + wantCreateBy []string + wantErr bool + }{ + { + name: "happy path", + imageName: "alpine:3.11", + wantConfigName: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + wantCreateBy: []string{ + "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ", + }, + wantErr: false, + }, + { + name: "unknown image", + imageName: "alpine:unknown", + wantErr: true, + }, + } + + te := setupPodmanSock(t) + defer te.Close() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := name.ParseReference(tt.imageName) + require.NoError(t, err) + + img, cleanup, err := PodmanImage(t.Context(), ref.Name(), "") + defer cleanup() + + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + confName, err := img.ConfigName() + require.NoError(t, err) + assert.Equal(t, tt.wantConfigName, confName.String()) + + confFile, err := img.ConfigFile() + require.NoError(t, err) + + assert.Len(t, tt.wantCreateBy, len(confFile.History)) + for _, h := range confFile.History { + assert.Contains(t, tt.wantCreateBy, h.CreatedBy) + } + }) + } +} diff --git a/pkg/fanal/image/daemon/testdata/golden/config-alpine310.json b/pkg/fanal/image/daemon/testdata/golden/config-alpine310.json new file mode 100644 index 000000000000..6565cd403075 --- /dev/null +++ b/pkg/fanal/image/daemon/testdata/golden/config-alpine310.json @@ -0,0 +1,75 @@ +{ + "architecture": "amd64", + "config": { + "Hostname": "", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/sh" + ], + "ArgsEscaped": true, + "Image": "sha256:7c41e139ba64dd2eba852a2e963ee86f2e8da3a5bbfaf10cf4349535dbf0ff08", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": null + }, + "container": "7f4a36a667d138b079b5ff059485ff65bfbb5ebc48f24a89f983b918e73f4f28", + "container_config": { + "Hostname": "7f4a36a667d1", + "Domainname": "", + "User": "", + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false, + "OpenStdin": false, + "StdinOnce": false, + "Env": [ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + ], + "Cmd": [ + "/bin/sh", + "-c", + "#(nop) ", + "CMD [\"/bin/sh\"]" + ], + "ArgsEscaped": true, + "Image": "sha256:7c41e139ba64dd2eba852a2e963ee86f2e8da3a5bbfaf10cf4349535dbf0ff08", + "Volumes": null, + "WorkingDir": "", + "Entrypoint": null, + "OnBuild": null, + "Labels": {} + }, + "created": "2020-01-23T16:53:06.686519038Z", + "docker_version": "18.06.1-ce", + "history": [ + { + "created": "2020-01-23T16:53:06.551172402Z", + "created_by": "/bin/sh -c #(nop) ADD file:d48cac34fac385cbc1de6adfdd88300f76f9bbe346cd17e64fd834d042a98326 in / " + }, + { + "created": "2020-01-23T16:53:06.686519038Z", + "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + "empty_layer": true + } + ], + "os": "linux", + "rootfs": { + "type": "layers", + "diff_ids": [ + "sha256:531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028" + ] + } +} diff --git a/pkg/fanal/image/docker.go b/pkg/fanal/image/docker.go new file mode 100644 index 000000000000..d0a97eff89e0 --- /dev/null +++ b/pkg/fanal/image/docker.go @@ -0,0 +1,44 @@ +package image + +import ( + "io" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/samber/lo" + "golang.org/x/xerrors" + + gzutil "github.com/aquasecurity/trivy/pkg/fanal/utils/gzip" +) + +type dockerArchive struct { + v1.Image + repoTags []string +} + +func tryDockerArchive(fileName string) (v1.Image, error) { + opener := fileOpener(fileName) + + // Load the image + img, err := tarball.Image(opener, nil) + if err != nil { + return nil, xerrors.Errorf("unable to open %s as a Docker image: %w", fileName, err) + } + + // Load the manifest to get RepoTags + manifest, err := tarball.LoadManifest(opener) + if err != nil { + return nil, xerrors.Errorf("unable to load manifest from %s: %w", fileName, err) + } + + return dockerArchive{ + Image: img, + repoTags: lo.FirstOrEmpty(manifest).RepoTags, // Take RepoTags from the first manifest entry + }, nil +} + +func fileOpener(fileName string) func() (io.ReadCloser, error) { + return func() (io.ReadCloser, error) { + return gzutil.OpenFile(fileName) + } +} diff --git a/pkg/fanal/image/image.go b/pkg/fanal/image/image.go new file mode 100644 index 000000000000..813503fd904c --- /dev/null +++ b/pkg/fanal/image/image.go @@ -0,0 +1,140 @@ +package image + +import ( + "context" + "fmt" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/hashicorp/go-multierror" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +type imageSourceFunc func(ctx context.Context, imageName string, ref name.Reference, option types.ImageOptions) (types.Image, func(), error) + +var imageSourceFuncs = map[types.ImageSource]imageSourceFunc{ + types.ContainerdImageSource: tryContainerdDaemon, + types.PodmanImageSource: tryPodmanDaemon, + types.DockerImageSource: tryDockerDaemon, + types.RemoteImageSource: tryRemote, +} + +func NewContainerImage(ctx context.Context, imageName string, opt types.ImageOptions) (types.Image, func(), error) { + ctx = log.WithContextPrefix(ctx, log.PrefixContainerImage) + + if len(opt.ImageSources) == 0 { + return nil, func() {}, xerrors.New("no image sources supplied") + } + + var errs error + var nameOpts []name.Option + if opt.RegistryOptions.Insecure { + nameOpts = append(nameOpts, name.Insecure) + } + + ref, err := name.ParseReference(imageName, nameOpts...) + if err != nil { + return nil, func() {}, xerrors.Errorf("failed to parse the image name: %w", err) + } + + for _, src := range opt.ImageSources { + trySrc, ok := imageSourceFuncs[src] + if !ok { + log.WarnContext(ctx, "Unknown image source", log.String("source", string(src))) + continue + } + + img, cleanup, err := trySrc(ctx, imageName, ref, opt) + if err == nil { + // Return v1.Image if the image is found + log.DebugContext(ctx, "Image found", log.String("image", imageName), log.String("source", string(src))) + return img, cleanup, nil + } + err = multierror.Prefix(err, fmt.Sprintf("%s error:", src)) + errs = multierror.Append(errs, err) + } + + return nil, func() {}, xerrors.Errorf("unable to find the specified image %q in %q: %w", imageName, opt.ImageSources, errs) +} + +func ID(img v1.Image) (string, error) { + h, err := img.ConfigName() + if err != nil { + return "", xerrors.Errorf("unable to get the image ID: %w", err) + } + return h.String(), nil +} + +func LayerIDs(img v1.Image) ([]string, error) { + conf, err := img.ConfigFile() + if err != nil { + return nil, xerrors.Errorf("unable to get the config file: %w", err) + } + + var layerIDs []string + for _, d := range conf.RootFS.DiffIDs { + layerIDs = append(layerIDs, d.String()) + } + return layerIDs, nil +} + +// GuessBaseImageIndex tries to guess index of base layer +// +// e.g. In the following example, we should detect layers in debian:8. +// +// FROM debian:8 +// RUN apt-get update +// COPY mysecret / +// ENTRYPOINT ["entrypoint.sh"] +// CMD ["somecmd"] +// +// debian:8 may be like +// +// ADD file:5d673d25da3a14ce1f6cf66e4c7fd4f4b85a3759a9d93efb3fd9ff852b5b56e4 in / +// CMD ["/bin/sh"] +// +// In total, it would be like: +// +// ADD file:5d673d25da3a14ce1f6cf66e4c7fd4f4b85a3759a9d93efb3fd9ff852b5b56e4 in / +// CMD ["/bin/sh"] # empty layer (detected) +// RUN apt-get update +// COPY mysecret / +// ENTRYPOINT ["entrypoint.sh"] # empty layer (skipped) +// CMD ["somecmd"] # empty layer (skipped) +// +// This method tries to detect CMD in the second line and assume the first line is a base layer. +// 1. Iterate histories from the bottom. +// 2. Skip all the empty layers at the bottom. In the above example, "entrypoint.sh" and "somecmd" will be skipped +// 3. If it finds CMD, it assumes that it is the end of base layers. +// 4. It gets all the layers as base layers above the CMD found in #3. +func GuessBaseImageIndex(histories []v1.History) int { + baseImageIndex := -1 + var foundNonEmpty bool + for i := len(histories) - 1; i >= 0; i-- { + h := histories[i] + + // Skip the last CMD, ENTRYPOINT, etc. + if !foundNonEmpty { + if h.EmptyLayer { + continue + } + foundNonEmpty = true + } + + if !h.EmptyLayer { + continue + } + + // Detect CMD instruction in base image + if strings.HasPrefix(h.CreatedBy, "/bin/sh -c #(nop) CMD") || + strings.HasPrefix(h.CreatedBy, "CMD") { // BuildKit + baseImageIndex = i + break + } + } + return baseImageIndex +} diff --git a/pkg/fanal/image/image_test.go b/pkg/fanal/image/image_test.go new file mode 100644 index 000000000000..99f0947771fb --- /dev/null +++ b/pkg/fanal/image/image_test.go @@ -0,0 +1,557 @@ +package image + +import ( + "fmt" + "net/http/httptest" + "testing" + "time" + + "github.com/golang-jwt/jwt/v5" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/testdocker/auth" + "github.com/aquasecurity/testdocker/engine" + "github.com/aquasecurity/testdocker/registry" + "github.com/aquasecurity/testdocker/tarfile" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func setupEngineAndRegistry(t *testing.T) (*httptest.Server, *httptest.Server) { + imagePaths := map[string]string{ + "alpine:3.10": "../test/testdata/alpine-310.tar.gz", + "alpine:3.11": "../test/testdata/alpine-311.tar.gz", + "a187dde48cd2": "../test/testdata/alpine-311.tar.gz", + } + opt := engine.Option{ + APIVersion: "1.45", + ImagePaths: imagePaths, + } + te := engine.NewDockerEngine(opt) + + images := map[string]v1.Image{ + "v2/library/alpine:3.10": localImage(t), + } + tr := registry.NewDockerRegistry(registry.Option{ + Images: images, + Auth: auth.Auth{}, + }) + + t.Setenv("DOCKER_HOST", fmt.Sprintf("tcp://%s", te.Listener.Addr().String())) + + return te, tr +} + +func TestNewDockerImage(t *testing.T) { + te, tr := setupEngineAndRegistry(t) + defer func() { + te.Close() + tr.Close() + }() + serverAddr := tr.Listener.Addr().String() + + type args struct { + imageName string + option types.ImageOptions + } + tests := []struct { + name string + args args + wantID string + wantConfigFile *v1.ConfigFile + wantRepoTags []string + wantRepoDigests []string + wantErr bool + }{ + { + name: "happy path with Docker Engine (use pattern : for image name)", + args: args{ + imageName: "alpine:3.11", + }, + wantID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + wantRepoTags: []string{"alpine:3.11"}, + wantConfigFile: &v1.ConfigFile{ + Architecture: "amd64", + OS: "linux", + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)}, + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + Comment: "", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ", + EmptyLayer: false, + }, + }, + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{"/bin/sh"}, + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + ArgsEscaped: true, + }, + OSVersion: "", + }, + }, + { + name: "happy path with Docker Engine (use pattern for image name)", + args: args{ + imageName: "a187dde48cd2", + }, + wantID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + wantRepoTags: []string{"alpine:3.11"}, + wantConfigFile: &v1.ConfigFile{ + Architecture: "amd64", + OS: "linux", + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)}, + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + Comment: "", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 0, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ", + EmptyLayer: false, + }, + }, + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{"/bin/sh"}, + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + ArgsEscaped: true, + }, + OSVersion: "", + }, + }, + { + name: "happy path with Docker Registry", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + }, + wantID: "sha256:af341ccd2df8b0e2d67cf8dd32e087bfda4e5756ebd1c76bbf3efa0dc246590e", + wantRepoTags: []string{serverAddr + "/library/alpine:3.10"}, + wantRepoDigests: []string{ + serverAddr + "/library/alpine@sha256:e10ea963554297215478627d985466ada334ed15c56d3d6bb808ceab98374d91", + }, + wantConfigFile: &v1.ConfigFile{ + Architecture: "amd64", + Container: "7f4a36a667d138b079b5ff059485ff65bfbb5ebc48f24a89f983b918e73f4f28", + Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 686519038, time.UTC)}, + DockerVersion: "18.06.1-ce", + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 551172402, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:d48cac34fac385cbc1de6adfdd88300f76f9bbe346cd17e64fd834d042a98326 in / ", + EmptyLayer: false, + }, + { + Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 686519038, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + Comment: "", + EmptyLayer: true, + }, + }, + OS: "linux", + + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + }, + Config: v1.Config{ + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + Cmd: []string{"/bin/sh"}, + Image: "sha256:7c41e139ba64dd2eba852a2e963ee86f2e8da3a5bbfaf10cf4349535dbf0ff08", + ArgsEscaped: true, + }, + OSVersion: "", + }, + }, + { + name: "happy path with insecure Docker Registry", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "test", + }, + }, + Insecure: true, + }, + }, + }, + wantID: "sha256:af341ccd2df8b0e2d67cf8dd32e087bfda4e5756ebd1c76bbf3efa0dc246590e", + wantRepoTags: []string{serverAddr + "/library/alpine:3.10"}, + wantRepoDigests: []string{ + serverAddr + "/library/alpine@sha256:e10ea963554297215478627d985466ada334ed15c56d3d6bb808ceab98374d91", + }, + wantConfigFile: &v1.ConfigFile{ + Architecture: "amd64", + Container: "7f4a36a667d138b079b5ff059485ff65bfbb5ebc48f24a89f983b918e73f4f28", + Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 686519038, time.UTC)}, + DockerVersion: "18.06.1-ce", + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 551172402, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:d48cac34fac385cbc1de6adfdd88300f76f9bbe346cd17e64fd834d042a98326 in / ", + EmptyLayer: false, + }, + { + Created: v1.Time{Time: time.Date(2020, 1, 23, 16, 53, 6, 686519038, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + Comment: "", + EmptyLayer: true, + }, + }, + OS: "linux", + + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "531743b7098cb2aaf615641007a129173f63ed86ca32fe7b5a246a1c47286028", + }, + }, + }, + Config: v1.Config{ + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + Cmd: []string{"/bin/sh"}, + Image: "sha256:7c41e139ba64dd2eba852a2e963ee86f2e8da3a5bbfaf10cf4349535dbf0ff08", + ArgsEscaped: true, + }, + }, + }, + { + name: "sad path with invalid tag", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.11!!!", serverAddr), + }, + wantErr: true, + }, + { + name: "sad path with non-exist image", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:100", serverAddr), + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.option.ImageSources = types.AllImageSources + img, cleanup, err := NewContainerImage(t.Context(), tt.args.imageName, tt.args.option) + defer cleanup() + + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + gotID, err := img.ID() + require.NoError(t, err) + assert.Equal(t, tt.wantID, gotID) + + gotConfigFile, err := img.ConfigFile() + require.NoError(t, err) + assert.Equal(t, tt.wantConfigFile, gotConfigFile) + + gotRepoTags := img.RepoTags() + assert.Equal(t, tt.wantRepoTags, gotRepoTags) + + gotRepoDigests := img.RepoDigests() + assert.Equal(t, tt.wantRepoDigests, gotRepoDigests) + }) + } +} + +func setupPrivateRegistry(t *testing.T) *httptest.Server { + images := map[string]v1.Image{ + "v2/library/alpine:3.10": localImage(t), + } + tr := registry.NewDockerRegistry(registry.Option{ + Images: images, + Auth: auth.Auth{ + User: "test", + Password: "testpass", + Secret: "secret", + }, + }) + + return tr +} + +func TestNewDockerImageWithPrivateRegistry(t *testing.T) { + tr := setupPrivateRegistry(t) + defer tr.Close() + + serverAddr := tr.Listener.Addr().String() + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "iss": "testdocker", + }) + + registryToken, err := token.SignedString([]byte("secret")) + require.NoError(t, err) + + type args struct { + imageName string + option types.ImageOptions + } + tests := []struct { + name string + args args + want v1.Image + wantErr string + }{ + { + name: "happy path with private Docker Registry", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + Insecure: true, + }, + }, + }, + }, + { + name: "happy path with registry token", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + RegistryToken: registryToken, + Insecure: true, + }, + }, + }, + }, + { + name: "sad path without a credential", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.11", serverAddr), + }, + wantErr: "unexpected status code 401", + }, + { + name: "sad path with invalid registry token", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.11", serverAddr), + option: types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + RegistryToken: registryToken + "invalid", + Insecure: true, + }, + }, + }, + wantErr: "signature is invalid", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.args.option.ImageSources = types.AllImageSources + _, cleanup, err := NewContainerImage(t.Context(), tt.args.imageName, tt.args.option) + defer cleanup() + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestNewArchiveImage(t *testing.T) { + type args struct { + fileName string + } + tests := []struct { + name string + args args + want v1.Image + wantRepoTags []string + wantErr string + }{ + { + name: "happy path", + args: args{ + fileName: "../test/testdata/alpine-310.tar.gz", + }, + wantRepoTags: []string{"alpine:3.10"}, + }, + { + name: "happy path with OCI Image Format", + args: args{ + fileName: "../test/testdata/test.oci", + }, + }, + { + name: "happy path with OCI Image and tag Format", + args: args{ + fileName: "../test/testdata/test_image_tag.oci:0.0.1", + }, + }, + { + name: "happy path with OCI Image only", + args: args{ + fileName: "../test/testdata/test_image_tag.oci", + }, + }, + { + name: "sad path with OCI Image and invalid tagFormat", + args: args{ + fileName: "../test/testdata/test_image_tag.oci:0.0.0", + }, + wantErr: "invalid OCI image ref", + }, + { + name: "sad path, oci image not found", + args: args{ + fileName: "../test/testdata/invalid.tar.gz", + }, + wantErr: "unable to open", + }, + { + name: "sad path with OCI Image Format index.json directory", + args: args{ + fileName: "../test/testdata/test_index_json_dir.oci", + }, + wantErr: "unable to retrieve index.json", + }, + { + name: "sad path with OCI Image Format invalid index.json", + args: args{ + fileName: "../test/testdata/test_bad_index_json.oci", + }, + wantErr: "invalid index.json", + }, + { + name: "sad path with OCI Image Format no valid manifests", + args: args{ + fileName: "../test/testdata/test_no_valid_manifests.oci", + }, + wantErr: "no valid manifest", + }, + { + name: "sad path with OCI Image Format with invalid oci image digest", + args: args{ + fileName: "../test/testdata/test_invalid_oci_image.oci", + }, + wantErr: "invalid OCI image", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + img, err := NewArchiveImage(tt.args.fileName) + switch { + case tt.wantErr != "": + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + default: + require.NoError(t, err, tt.name) + } + + assert.Equal(t, tt.wantRepoTags, img.RepoTags()) + assert.Empty(t, img.RepoDigests()) + }) + } +} + +func TestDockerPlatformArguments(t *testing.T) { + tr := setupPrivateRegistry(t) + defer tr.Close() + + serverAddr := tr.Listener.Addr().String() + + type args struct { + option types.ImageOptions + } + tests := []struct { + name string + args args + want v1.Image + wantErr string + }{ + { + name: "happy path with valid platform", + args: args{ + option: types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + Insecure: true, + Platform: types.Platform{ + Platform: &v1.Platform{ + Architecture: "arm", + OS: "linux", + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + imageName := fmt.Sprintf("%s/library/alpine:3.10", serverAddr) + tt.args.option.ImageSources = types.AllImageSources + _, cleanup, err := NewContainerImage(t.Context(), imageName, tt.args.option) + defer cleanup() + + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func localImage(t *testing.T) v1.Image { + img, err := tarfile.ImageFromPath("../test/testdata/alpine-310.tar.gz") + require.NoError(t, err) + return img +} diff --git a/pkg/fanal/image/name/reference.go b/pkg/fanal/image/name/reference.go new file mode 100644 index 000000000000..102a7ee208fc --- /dev/null +++ b/pkg/fanal/image/name/reference.go @@ -0,0 +1,57 @@ +package name + +import ( + "encoding/json" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/samber/lo" + "golang.org/x/xerrors" +) + +// Reference wraps name.Reference to support JSON marshaling/unmarshaling +type Reference struct { + name.Reference +} + +// ParseReference parses a string into a Reference +func ParseReference(s string) (Reference, error) { + if s == "" { + return Reference{}, nil + } + ref, err := name.ParseReference(s) + if err != nil { + return Reference{}, xerrors.Errorf("failed to parse reference: %w", err) + } + return Reference{Reference: ref}, nil +} + +// MarshalJSON implements json.Marshaler +func (r Reference) MarshalJSON() ([]byte, error) { + if lo.IsNil(r.Reference) { + return json.Marshal("") + } + return json.Marshal(r.Reference.String()) +} + +// UnmarshalJSON implements json.Unmarshaler +func (r *Reference) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return xerrors.Errorf("failed to unmarshal reference: %w", err) + } + if s == "" { + r.Reference = nil + return nil + } + ref, err := name.ParseReference(s) + if err != nil { + return xerrors.Errorf("failed to parse reference: %w", err) + } + r.Reference = ref + return nil +} + +// IsZero returns true if the reference is empty (for omitzero support) +func (r Reference) IsZero() bool { + return lo.IsNil(r.Reference) +} diff --git a/pkg/fanal/image/name/reference_test.go b/pkg/fanal/image/name/reference_test.go new file mode 100644 index 000000000000..1d1da96c08e7 --- /dev/null +++ b/pkg/fanal/image/name/reference_test.go @@ -0,0 +1,198 @@ +package name_test + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/fanal/image/name" +) + +func TestReference_MarshalJSON(t *testing.T) { + tests := []struct { + name string + ref name.Reference + want string + }{ + { + name: "valid reference with tag", + ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"), + want: `"ghcr.io/aquasecurity/trivy:latest"`, + }, + { + name: "valid reference with digest", + ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + want: `"ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"`, + }, + { + name: "empty reference", + ref: name.Reference{}, + want: `""`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := json.Marshal(tt.ref) + require.NoError(t, err) + assert.Equal(t, tt.want, string(got)) + }) + } +} + +func TestReference_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + json string + want string + wantIsEmpty bool + wantErr assert.ErrorAssertionFunc + }{ + { + name: "valid reference with tag", + json: `"ghcr.io/aquasecurity/trivy:latest"`, + want: "ghcr.io/aquasecurity/trivy:latest", + wantIsEmpty: false, + wantErr: assert.NoError, + }, + { + name: "valid reference with digest", + json: `"ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"`, + want: "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000", + wantIsEmpty: false, + wantErr: assert.NoError, + }, + { + name: "empty reference", + json: `""`, + want: "", + wantIsEmpty: true, + wantErr: assert.NoError, + }, + { + name: "invalid reference", + json: `"not a valid reference!"`, + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var r name.Reference + err := json.Unmarshal([]byte(tt.json), &r) + tt.wantErr(t, err) + if err != nil { + return + } + assert.Equal(t, tt.wantIsEmpty, r.IsZero()) + if !r.IsZero() { + assert.Equal(t, tt.want, r.String()) + } + }) + } +} + +func TestReference_String(t *testing.T) { + tests := []struct { + name string + ref name.Reference + want string + }{ + { + name: "ghcr.io with tag", + ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"), + want: "ghcr.io/aquasecurity/trivy:latest", + }, + { + name: "ghcr.io with digest", + ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + want: "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000", + }, + { + name: "docker.io implicit", + ref: testutil.MustParseReference(t, "aquasecurity/trivy:latest"), + want: "aquasecurity/trivy:latest", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.ref.String()) + }) + } +} + +func TestReference_Context(t *testing.T) { + tests := []struct { + name string + ref name.Reference + want string + }{ + { + name: "ghcr.io with tag", + ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"), + want: "ghcr.io/aquasecurity/trivy", + }, + { + name: "ghcr.io with digest", + ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + want: "ghcr.io/aquasecurity/trivy", + }, + { + name: "docker.io implicit", + ref: testutil.MustParseReference(t, "aquasecurity/trivy:latest"), + want: "index.docker.io/aquasecurity/trivy", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.ref.Context().String()) + }) + } +} + +func TestReference_IsEmpty(t *testing.T) { + tests := []struct { + name string + ref name.Reference + want bool + }{ + { + name: "non-empty reference", + ref: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"), + want: false, + }, + { + name: "empty reference", + ref: name.Reference{}, + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, tt.ref.IsZero()) + }) + } +} + +func TestReference_JSONRoundTrip(t *testing.T) { + ref := testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:v0.65.0") + + // Marshal to JSON + data, err := json.Marshal(ref) + require.NoError(t, err) + + // Unmarshal from JSON + var decoded name.Reference + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + // Verify the decoded reference matches the original + assert.Equal(t, ref.String(), decoded.String()) + assert.Equal(t, ref.Context().String(), decoded.Context().String()) +} diff --git a/pkg/fanal/image/oci.go b/pkg/fanal/image/oci.go new file mode 100644 index 000000000000..3031810a059b --- /dev/null +++ b/pkg/fanal/image/oci.go @@ -0,0 +1,82 @@ +package image + +import ( + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/layout" + ispec "github.com/opencontainers/image-spec/specs-go/v1" + "golang.org/x/xerrors" +) + +func tryOCI(fileName string) (v1.Image, error) { + var inputRef, inputFileName string + + // Check if tag is specified in input + // e.g. /path/to/oci:0.0.1 + + inputFileName, inputRef, found := strings.Cut(fileName, "@") + + if !found { + inputFileName, inputRef, found = strings.Cut(fileName, ":") + } + + if !found { + inputFileName = fileName + inputRef = "" + } + + lp, err := layout.FromPath(inputFileName) + if err != nil { + return nil, xerrors.Errorf("unable to open %s as an OCI Image: %w", fileName, err) + } + + index, err := lp.ImageIndex() + if err != nil { + return nil, xerrors.Errorf("unable to retrieve index.json: %w", err) + } + + m, err := index.IndexManifest() + if err != nil { + return nil, xerrors.Errorf("invalid index.json: %w", err) + } + + if len(m.Manifests) == 0 { + return nil, xerrors.New("no valid manifest") + } + + // Support image having tag separated by : , otherwise support first image + return getOCIImage(m, index, inputRef) +} + +func getOCIImage(m *v1.IndexManifest, index v1.ImageIndex, inputRef string) (v1.Image, error) { + for _, manifest := range m.Manifests { + annotation := manifest.Annotations + tag := annotation[ispec.AnnotationRefName] + if inputRef == "" || // always select the first digest + tag == inputRef || + manifest.Digest.String() == inputRef { + h := manifest.Digest + if manifest.MediaType.IsIndex() { + childIndex, err := index.ImageIndex(h) + if err != nil { + return nil, xerrors.Errorf("unable to retrieve a child image %q: %w", h.String(), err) + } + childManifest, err := childIndex.IndexManifest() + if err != nil { + return nil, xerrors.Errorf("invalid a child manifest for %q: %w", h.String(), err) + } + return getOCIImage(childManifest, childIndex, "") + } + + img, err := index.Image(h) + if err != nil { + return nil, xerrors.Errorf("invalid OCI image: %w", err) + } + + return img, nil + } + } + + return nil, xerrors.New("invalid OCI image ref") +} diff --git a/pkg/fanal/image/oci_test.go b/pkg/fanal/image/oci_test.go new file mode 100644 index 000000000000..a527e34f256f --- /dev/null +++ b/pkg/fanal/image/oci_test.go @@ -0,0 +1,70 @@ +package image + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestTryOCI(t *testing.T) { + tests := []struct { + name string + ociImagePath string + wantErr string + }{ + { + name: "correct path to index without tag", + ociImagePath: filepath.Join("testdata", "multi"), + wantErr: "", + }, + { + name: "correct path to index with correct tag", + ociImagePath: filepath.Join("testdata", "multi:tg11"), + wantErr: "", + }, + { + name: "correct path to index with incorrect tag", + ociImagePath: filepath.Join("testdata", "multi:tg12"), + wantErr: "invalid OCI image ref", + }, + { + name: "correct path to manifest without tag", + ociImagePath: filepath.Join("testdata", "single"), + wantErr: "", + }, + { + name: "correct path to manifest with correct tag", + ociImagePath: filepath.Join("testdata", "single:3.14"), + wantErr: "", + }, + { + name: "correct path to manifest with incorrect tag", + ociImagePath: filepath.Join("testdata", "single:3.11"), + wantErr: "invalid OCI image ref", + }, + { + name: "correct path to manifest with correct digest", + ociImagePath: filepath.Join("testdata", + "single@sha256:56ae38f2f5c54b98311b8b2463d4861368c451ac17098f4227d84946b42ab96d"), + wantErr: "", + }, + { + name: "correct path to manifest with incorrect digest", + ociImagePath: filepath.Join("testdata", + "single@sha256:1111111111111111111111111111111111111111111111111111111111111111"), + wantErr: "invalid OCI image ref", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := tryOCI(test.ociImagePath) + if test.wantErr != "" { + require.ErrorContains(t, err, test.wantErr, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/fanal/image/registry/azure/azure.go b/pkg/fanal/image/registry/azure/azure.go new file mode 100644 index 000000000000..2c526abb741c --- /dev/null +++ b/pkg/fanal/image/registry/azure/azure.go @@ -0,0 +1,98 @@ +package azure + +import ( + "context" + "errors" + "fmt" + "os" + "strings" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/containers/azcontainerregistry" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/image/registry/intf" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type RegistryClient struct { + domain string + scope string + cloud cloud.Configuration +} + +type Registry struct { +} + +const ( + azureURL = ".azurecr.io" + chinaAzureURL = ".azurecr.cn" + scope = "https://management.azure.com/.default" + chinaScope = "https://management.chinacloudapi.cn/.default" + scheme = "https" +) + +func (r *Registry) CheckOptions(domain string, _ types.RegistryOptions) (intf.RegistryClient, error) { + if strings.HasSuffix(domain, azureURL) { + return &RegistryClient{ + domain: domain, + scope: scope, + cloud: cloud.AzurePublic, + }, nil + } else if strings.HasSuffix(domain, chinaAzureURL) { + return &RegistryClient{ + domain: domain, + scope: chinaScope, + cloud: cloud.AzureChina, + }, nil + } + + return nil, xerrors.Errorf("Azure registry: %w", types.InvalidURLPattern) +} + +func (r *RegistryClient) GetCredential(ctx context.Context) (string, string, error) { + opts := azcore.ClientOptions{Cloud: r.cloud} + cred, err := azidentity.NewDefaultAzureCredential(&azidentity.DefaultAzureCredentialOptions{ClientOptions: opts}) + if err != nil { + return "", "", xerrors.Errorf("unable to generate acr credential error: %w", err) + } + aadToken, err := cred.GetToken(ctx, policy.TokenRequestOptions{Scopes: []string{r.scope}}) + if err != nil { + return "", "", xerrors.Errorf("unable to get an access token: %w", err) + } + rt, err := refreshToken(ctx, aadToken.Token, r.domain) + if err != nil { + return "", "", xerrors.Errorf("unable to refresh token: %w", err) + } + return "00000000-0000-0000-0000-000000000000", *rt.RefreshToken, err +} + +func refreshToken(ctx context.Context, accessToken, domain string) (azcontainerregistry.ACRRefreshToken, error) { + tenantID := os.Getenv("AZURE_TENANT_ID") + if tenantID == "" { + return azcontainerregistry.ACRRefreshToken{}, errors.New("missing environment variable AZURE_TENANT_ID") + } + + client, err := azcontainerregistry.NewAuthenticationClient(fmt.Sprintf("%s://%s", scheme, domain), nil) + if err != nil { + return azcontainerregistry.ACRRefreshToken{}, xerrors.Errorf("create auth client: %w", err) + } + + resp, err := client.ExchangeAADAccessTokenForACRRefreshToken(ctx, + azcontainerregistry.PostContentSchemaGrantTypeAccessToken, + domain, + &azcontainerregistry.AuthenticationClientExchangeAADAccessTokenForACRRefreshTokenOptions{ + AccessToken: lo.ToPtr(accessToken), + Tenant: lo.ToPtr(tenantID), + }, + ) + if err != nil { + return azcontainerregistry.ACRRefreshToken{}, xerrors.Errorf("exchange access token: %w", err) + } + + return resp.ACRRefreshToken, nil +} diff --git a/pkg/fanal/image/registry/azure/azure_test.go b/pkg/fanal/image/registry/azure/azure_test.go new file mode 100644 index 000000000000..5be56f777a51 --- /dev/null +++ b/pkg/fanal/image/registry/azure/azure_test.go @@ -0,0 +1,49 @@ +package azure_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/image/registry/azure" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestRegistry_CheckOptions(t *testing.T) { + tests := []struct { + name string + domain string + wantErr string + }{ + { + name: "happy path", + domain: "test.azurecr.io", + }, + { + name: "china happy path", + domain: "test.azurecr.cn", + }, + { + name: "invalidURL", + domain: "not-azurecr.io", + wantErr: "Azure registry: invalid url pattern", + }, + { + name: "invalidURL", + domain: "alpine:3.9", + wantErr: "Azure registry: invalid url pattern", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := azure.Registry{} + _, err := r.CheckOptions(tt.domain, types.RegistryOptions{}) + if tt.wantErr != "" { + assert.EqualError(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/fanal/image/registry/ecr/ecr.go b/pkg/fanal/image/registry/ecr/ecr.go new file mode 100644 index 000000000000..553823cf5d2b --- /dev/null +++ b/pkg/fanal/image/registry/ecr/ecr.go @@ -0,0 +1,116 @@ +package ecr + +import ( + "context" + "encoding/base64" + "net/http" + "regexp" + "strings" + + "github.com/aws/aws-sdk-go-v2/aws" + awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/ecr" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/image/registry/intf" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +type ecrAPI interface { + GetAuthorizationToken(ctx context.Context, params *ecr.GetAuthorizationTokenInput, optFns ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) +} + +type ECR struct { +} + +type ECRClient struct { + Client ecrAPI +} + +func getSession(domain, region string, option types.RegistryOptions) (aws.Config, error) { + // Use BuildableClient to configure a custom Transport. + // See: https://docs.aws.amazon.com/sdk-for-go/v2/developer-guide/configure-http.html + // This is required because the xhttp.Client can cause issues when accessing IMDS. + // cf. https://github.com/aquasecurity/trivy/discussions/9429 + client := awshttp.NewBuildableClient().WithTransportOptions(func(transport *http.Transport) { + transport.TLSClientConfig.InsecureSkipVerify = option.Insecure + transport.TLSClientConfig.RootCAs = option.CACerts + }) + // create custom credential information if option is valid + if option.AWSSecretKey != "" && option.AWSAccessKey != "" && option.AWSRegion != "" { + if region != option.AWSRegion { + log.Warnf("The region from AWS_REGION (%s) is being overridden. The region from domain (%s) was used.", option.AWSRegion, domain) + } + return config.LoadDefaultConfig( + context.TODO(), + config.WithRegion(region), + config.WithHTTPClient(client), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(option.AWSAccessKey, option.AWSSecretKey, option.AWSSessionToken)), + ) + } + return config.LoadDefaultConfig( + context.TODO(), + config.WithRegion(region), + config.WithHTTPClient(client), + ) +} + +func (e *ECR) CheckOptions(domain string, option types.RegistryOptions) (intf.RegistryClient, error) { + region := determineRegion(domain) + if region == "" { + return nil, xerrors.Errorf("ECR : %w", types.InvalidURLPattern) + } + + cfg, err := getSession(domain, region, option) + if err != nil { + return nil, err + } + + svc := ecr.NewFromConfig(cfg) + return &ECRClient{Client: svc}, nil +} + +// Endpoints take the form +// .dkr.ecr..amazonaws.com +// .dkr.ecr-fips..amazonaws.com +// .dkr.ecr..amazonaws.com.cn +// .dkr.ecr..sc2s.sgov.gov +// .dkr.ecr..c2s.ic.gov +// .dkr-ecr..on.aws +// see +// - https://docs.aws.amazon.com/general/latest/gr/ecr.html +// - https://docs.amazonaws.cn/en_us/aws/latest/userguide/endpoints-arns.html +// - https://docs.aws.amazon.com/AmazonECR/latest/userguide/ecr-requests.html +// - https://github.com/boto/botocore/blob/1.34.51/botocore/data/endpoints.json +var ecrEndpointMatch = regexp.MustCompile(`^[^.]+\.dkr[.-]ecr(?:-fips)?\.([^.]+)\.(?:amazonaws\.com(?:\.cn)?|sc2s\.sgov\.gov|c2s\.ic\.gov|on\.aws)$`) + +func determineRegion(domain string) string { + matches := ecrEndpointMatch.FindStringSubmatch(domain) + if matches != nil { + return matches[1] + } + return "" +} + +func (e *ECRClient) GetCredential(ctx context.Context) (username, password string, err error) { + input := &ecr.GetAuthorizationTokenInput{} + result, err := e.Client.GetAuthorizationToken(ctx, input) + if err != nil { + return "", "", xerrors.Errorf("failed to get authorization token: %w", err) + } + for _, data := range result.AuthorizationData { + b, err := base64.StdEncoding.DecodeString(*data.AuthorizationToken) + if err != nil { + return "", "", xerrors.Errorf("base64 decode failed: %w", err) + } + // e.g. AWS:eyJwYXlsb2... + split := strings.SplitN(string(b), ":", 2) + if len(split) == 2 { + return split[0], split[1], nil + } + } + return "", "", nil +} diff --git a/pkg/fanal/image/registry/ecr/ecr_test.go b/pkg/fanal/image/registry/ecr/ecr_test.go new file mode 100644 index 000000000000..2df162d73b5a --- /dev/null +++ b/pkg/fanal/image/registry/ecr/ecr_test.go @@ -0,0 +1,169 @@ +package ecr + +import ( + "context" + "errors" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/ecr" + awstypes "github.com/aws/aws-sdk-go-v2/service/ecr/types" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type testECRClient interface { + Options() ecr.Options +} + +func TestCheckOptions(t *testing.T) { + tests := map[string]struct { + domain string + expectedRegion string + wantErr error + }{ + "InvalidURL": { + domain: "alpine:3.9", + wantErr: types.InvalidURLPattern, + }, + "InvalidDomain": { + domain: "xxx.ecr.ap-northeast-1.not-amazonaws.com", + wantErr: types.InvalidURLPattern, + }, + "InvalidSubdomain": { + domain: "xxx.s3.ap-northeast-1.amazonaws.com", + wantErr: types.InvalidURLPattern, + }, + "NoOption": { + domain: "xxx.dkr.ecr.ap-northeast-1.amazonaws.com", + expectedRegion: "ap-northeast-1", + }, + "region-1": { + domain: "xxx.dkr.ecr.region-1.amazonaws.com", + expectedRegion: "region-1", + }, + "region-2": { + domain: "xxx.dkr.ecr.region-2.amazonaws.com", + expectedRegion: "region-2", + }, + "fips-region-1": { + domain: "xxx.dkr.ecr-fips.fips-region.amazonaws.com", + expectedRegion: "fips-region", + }, + "dualstack-region-1": { + domain: "xxx.dkr-ecr.region-1.on.aws", + expectedRegion: "region-1", + }, + "cn-region-1": { + domain: "xxx.dkr.ecr.region-1.amazonaws.com.cn", + expectedRegion: "region-1", + }, + "cn-region-2": { + domain: "xxx.dkr.ecr.region-2.amazonaws.com.cn", + expectedRegion: "region-2", + }, + "sc2s-region-1": { + domain: "xxx.dkr.ecr.sc2s-region.sc2s.sgov.gov", + expectedRegion: "sc2s-region", + }, + "c2s-region-1": { + domain: "xxx.dkr.ecr.c2s-region.c2s.ic.gov", + expectedRegion: "c2s-region", + }, + "invalid-ecr": { + domain: "xxx.dkrecr.region-1.amazonaws.com", + wantErr: types.InvalidURLPattern, + }, + "invalid-fips": { + domain: "xxx.dkr.ecrfips.fips-region.amazonaws.com", + wantErr: types.InvalidURLPattern, + }, + "invalid-cn": { + domain: "xxx.dkr.ecr.region-2.amazonaws.cn", + wantErr: types.InvalidURLPattern, + }, + "invalid-sc2s": { + domain: "xxx.dkr.ecr.sc2s-region.sc2s.sgov", + wantErr: types.InvalidURLPattern, + }, + "invalid-cs2": { + domain: "xxx.dkr.ecr.c2s-region.c2s.ic", + wantErr: types.InvalidURLPattern, + }, + } + + for testname, v := range tests { + a := &ECR{} + ecrClient, err := a.CheckOptions(v.domain, types.RegistryOptions{}) + if err != nil { + if !errors.Is(err, v.wantErr) { + t.Errorf("[%s]\nexpected error based on %v\nactual : %v", testname, v.wantErr, err) + } + continue + } + + client := (ecrClient.(*ECRClient)).Client.(testECRClient) + require.Equal(t, v.expectedRegion, client.Options().Region) + } +} + +type mockedECR struct { + Resp ecr.GetAuthorizationTokenOutput +} + +func (m mockedECR) GetAuthorizationToken(_ context.Context, _ *ecr.GetAuthorizationTokenInput, _ ...func(*ecr.Options)) (*ecr.GetAuthorizationTokenOutput, error) { + return &m.Resp, nil +} + +func TestECRGetCredential(t *testing.T) { + cases := []struct { + Resp ecr.GetAuthorizationTokenOutput + expectedUser string + expectedPassword string + }{ + { + Resp: ecr.GetAuthorizationTokenOutput{ + AuthorizationData: []awstypes.AuthorizationData{ + {AuthorizationToken: aws.String("YXdzOnBhc3N3b3Jk")}, + }, + }, + expectedUser: "aws", + expectedPassword: "password", + }, + { + Resp: ecr.GetAuthorizationTokenOutput{ + AuthorizationData: []awstypes.AuthorizationData{ + {AuthorizationToken: aws.String("YXdzOnBhc3N3b3JkOmJhZA==")}, + }, + }, + expectedUser: "aws", + expectedPassword: "password:bad", + }, + { + Resp: ecr.GetAuthorizationTokenOutput{ + AuthorizationData: []awstypes.AuthorizationData{ + {AuthorizationToken: aws.String("YXdzcGFzc3dvcmQ=")}, + }, + }, + expectedUser: "", + expectedPassword: "", + }, + } + + for i, c := range cases { + e := ECRClient{ + Client: mockedECR{Resp: c.Resp}, + } + username, password, err := e.GetCredential(t.Context()) + if err != nil { + t.Fatalf("%d, unexpected error", err) + } + if username != c.expectedUser { + t.Fatalf("%d, username: expected %s, got %s", i, c.expectedUser, username) + } + if password != c.expectedPassword { + t.Fatalf("%d, password: expected %s, got %s", i, c.expectedPassword, password) + } + } +} diff --git a/pkg/fanal/image/registry/google/google.go b/pkg/fanal/image/registry/google/google.go new file mode 100644 index 000000000000..3e6cf0ed0cc2 --- /dev/null +++ b/pkg/fanal/image/registry/google/google.go @@ -0,0 +1,66 @@ +package google + +import ( + "context" + "strings" + + "github.com/GoogleCloudPlatform/docker-credential-gcr/v2/config" + "github.com/GoogleCloudPlatform/docker-credential-gcr/v2/credhelper" + "github.com/GoogleCloudPlatform/docker-credential-gcr/v2/store" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/image/registry/intf" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type GoogleRegistryClient struct { + Store store.GCRCredStore + domain string +} + +type Registry struct { +} + +// Google container registry +const gcrURLDomain = "gcr.io" +const gcrURLSuffix = ".gcr.io" + +// Google artifact registry +const garURLSuffix = "-docker.pkg.dev" + +// Google mirror registry +const gmrURLDomain = "mirror.gcr.io" + +func (g *Registry) CheckOptions(domain string, option types.RegistryOptions) (intf.RegistryClient, error) { + // We assume there is no chance that `mirror.gcr.io` will require authentication. + // So we need to skip `mirror.gcr.io` to avoid errors confusing users when downloading DB's. + if domain == gmrURLDomain { + return nil, xerrors.Errorf("mirror.gcr.io doesn't require authentication") + } + if domain != gcrURLDomain && !strings.HasSuffix(domain, gcrURLSuffix) && !strings.HasSuffix(domain, garURLSuffix) { + return nil, xerrors.Errorf("Google registry: %w", types.InvalidURLPattern) + } + client := GoogleRegistryClient{domain: domain} + if option.GCPCredPath != "" { + client.Store = store.NewGCRCredStore(option.GCPCredPath) + } + return &client, nil +} + +func (g *GoogleRegistryClient) GetCredential(_ context.Context) (username, password string, err error) { + var credStore store.GCRCredStore + if g.Store == nil { + credStore, err = store.DefaultGCRCredStore() + if err != nil { + return "", "", xerrors.Errorf("failed to get GCRCredStore: %w", err) + } + } else { + credStore = g.Store + } + userCfg, err := config.LoadUserConfig() + if err != nil { + return "", "", xerrors.Errorf("failed to load user config: %w", err) + } + helper := credhelper.NewGCRCredentialHelper(credStore, userCfg) + return helper.Get(g.domain) +} diff --git a/pkg/fanal/image/registry/google/google_test.go b/pkg/fanal/image/registry/google/google_test.go new file mode 100644 index 000000000000..cda37a39a766 --- /dev/null +++ b/pkg/fanal/image/registry/google/google_test.go @@ -0,0 +1,59 @@ +package google + +import ( + "errors" + "reflect" + "testing" + + "github.com/GoogleCloudPlatform/docker-credential-gcr/v2/store" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestCheckOptions(t *testing.T) { + var tests = map[string]struct { + domain string + opt types.RegistryOptions + grc *GoogleRegistryClient + wantErr error + }{ + "InvalidURL": { + domain: "alpine:3.9", + wantErr: types.InvalidURLPattern, + }, + "InvalidDomain": { + domain: "not-gcr.io", + wantErr: types.InvalidURLPattern, + }, + "NoOption": { + domain: "gcr.io", + grc: &GoogleRegistryClient{domain: "gcr.io"}, + }, + "CredOption": { + domain: "gcr.io", + opt: types.RegistryOptions{GCPCredPath: "/path/to/file.json"}, + grc: &GoogleRegistryClient{ + domain: "gcr.io", + Store: store.NewGCRCredStore("/path/to/file.json"), + }, + }, + } + + for testname, v := range tests { + g := &Registry{} + grc, err := g.CheckOptions(v.domain, v.opt) + if v.wantErr != nil { + if err == nil { + t.Errorf("%s : expected error but no error", testname) + continue + } + if !errors.Is(err, v.wantErr) { + t.Errorf("[%s]\nexpected error based on %v\nactual : %v", testname, v.wantErr, err) + } + continue + } + if !reflect.DeepEqual(v.grc, grc) { + t.Errorf("[%s]\nexpected : %v\nactual : %v", testname, v.grc, grc) + } + } +} diff --git a/pkg/fanal/image/registry/intf/registry.go b/pkg/fanal/image/registry/intf/registry.go new file mode 100644 index 000000000000..da8c5d3c1789 --- /dev/null +++ b/pkg/fanal/image/registry/intf/registry.go @@ -0,0 +1,15 @@ +package intf + +import ( + "context" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type RegistryClient interface { + GetCredential(ctx context.Context) (string, string, error) +} + +type Registry interface { + CheckOptions(domain string, option types.RegistryOptions) (RegistryClient, error) +} diff --git a/pkg/fanal/image/registry/token.go b/pkg/fanal/image/registry/token.go new file mode 100644 index 000000000000..72c569890196 --- /dev/null +++ b/pkg/fanal/image/registry/token.go @@ -0,0 +1,49 @@ +package registry + +import ( + "context" + + "github.com/google/go-containerregistry/pkg/authn" + + "github.com/aquasecurity/trivy/pkg/fanal/image/registry/azure" + "github.com/aquasecurity/trivy/pkg/fanal/image/registry/ecr" + "github.com/aquasecurity/trivy/pkg/fanal/image/registry/google" + "github.com/aquasecurity/trivy/pkg/fanal/image/registry/intf" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +var ( + registries []intf.Registry +) + +func init() { + RegisterRegistry(&google.Registry{}) + RegisterRegistry(&ecr.ECR{}) + RegisterRegistry(&azure.Registry{}) +} + +func RegisterRegistry(registry intf.Registry) { + registries = append(registries, registry) +} + +func GetToken(ctx context.Context, domain string, opt types.RegistryOptions) (auth authn.Basic) { + // check registry which particular to get credential + for _, registry := range registries { + client, err := registry.CheckOptions(domain, opt) + if err != nil { + continue + } + username, password, err := client.GetCredential(ctx) + if err != nil { + // only skip check registry if error occurred + log.Debug("Credential error", log.Err(err)) + break + } + return authn.Basic{ + Username: username, + Password: password, + } + } + return authn.Basic{} +} diff --git a/pkg/fanal/image/registry/token_test.go b/pkg/fanal/image/registry/token_test.go new file mode 100644 index 000000000000..67693c98d59f --- /dev/null +++ b/pkg/fanal/image/registry/token_test.go @@ -0,0 +1,36 @@ +package registry + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestGetToken(t *testing.T) { + type args struct { + domain string + opt types.RegistryOptions + } + tests := []struct { + name string + args args + wantAuth authn.Basic + }{ + { + name: "happy path", + args: args{ + domain: "docker.io", + }, + wantAuth: authn.Basic{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotAuth := GetToken(t.Context(), tt.args.domain, tt.args.opt) + assert.Equal(t, tt.wantAuth, gotAuth) + }) + } +} diff --git a/pkg/fanal/image/remote.go b/pkg/fanal/image/remote.go new file mode 100644 index 000000000000..83d7e71d09c2 --- /dev/null +++ b/pkg/fanal/image/remote.go @@ -0,0 +1,96 @@ +package image + +import ( + "context" + "fmt" + "strings" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/remote" +) + +func tryRemote(ctx context.Context, imageName string, ref name.Reference, option types.ImageOptions) (types.Image, func(), error) { + // This function doesn't need cleanup + cleanup := func() {} + + desc, err := remote.Get(ctx, ref, option.RegistryOptions) + if err != nil { + return nil, cleanup, err + } + // ArtifactType being non-empty indicates this is not a regular container image + // (e.g., Helm charts, WASM modules, or other OCI artifacts) + if desc.ArtifactType != "" { + return nil, cleanup, xerrors.Errorf("unsupported artifact type %q for image %q", desc.ArtifactType, imageName) + } + img, err := desc.Image() + if err != nil { + return nil, cleanup, err + } + + // Return v1.Image if the image is found in Docker Registry + return remoteImage{ + name: imageName, + Image: img, + ref: implicitReference{ref: ref}, + descriptor: desc, + }, cleanup, nil + +} + +type remoteImage struct { + name string + ref implicitReference + descriptor *remote.Descriptor + v1.Image +} + +func (img remoteImage) Name() string { + return img.name +} + +func (img remoteImage) ID() (string, error) { + return ID(img) +} + +func (img remoteImage) RepoTags() []string { + tag := img.ref.TagName() + if tag == "" { + return []string{} + } + return []string{fmt.Sprintf("%s:%s", img.ref.RepositoryName(), tag)} +} + +func (img remoteImage) RepoDigests() []string { + repoDigest := fmt.Sprintf("%s@%s", img.ref.RepositoryName(), img.descriptor.Digest.String()) + return []string{repoDigest} +} + +type implicitReference struct { + ref name.Reference +} + +func (r implicitReference) TagName() string { + if t, ok := r.ref.(name.Tag); ok { + return t.TagStr() + } + return "" +} + +func (r implicitReference) RepositoryName() string { + ctx := r.ref.Context() + reg := ctx.RegistryStr() + repo := ctx.RepositoryStr() + + // Default registry + if reg != name.DefaultRegistry { + return fmt.Sprintf("%s/%s", reg, repo) + } + + // Trim default namespace + // See https://docs.docker.com/docker-hub/official_repos + return strings.TrimPrefix(repo, "library/") +} diff --git a/pkg/fanal/image/remote_test.go b/pkg/fanal/image/remote_test.go new file mode 100644 index 000000000000..059356515868 --- /dev/null +++ b/pkg/fanal/image/remote_test.go @@ -0,0 +1,191 @@ +package image + +import ( + "io" + "log" + "net/http/httptest" + "net/url" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/google/go-containerregistry/pkg/registry" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/remote" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func Test_implicitReference_TagName(t *testing.T) { + tests := []struct { + name string + image string + want string + }{ + { + name: "explicit tag", + image: "aquasec/trivy:0.15.0", + want: "0.15.0", + }, + { + name: "implicit tag", + image: "aquasec/trivy", + want: "latest", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := name.ParseReference(tt.image) + require.NoError(t, err) + + ref := implicitReference{ref: r} + + got := ref.TagName() + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_implicitReference_RepositoryName(t *testing.T) { + tests := []struct { + name string + image string + want string + }{ + { + name: "explicit default registry", + image: "index.docker.io/aquasec/trivy:0.15.0", + want: "aquasec/trivy", + }, + { + name: "explicit default namespace", + image: "library/alpine:3.12", + want: "alpine", + }, + { + name: "implicit registry", + image: "aquasec/trivy:latest", + want: "aquasec/trivy", + }, + { + name: "implicit namespace", + image: "alpine:latest", + want: "alpine", + }, + { + name: "non-default registry", + image: "gcr.io/library/alpine:3.12", + want: "gcr.io/library/alpine", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r, err := name.ParseReference(tt.image) + require.NoError(t, err) + + ref := implicitReference{ref: r} + + got := ref.RepositoryName() + assert.Equal(t, tt.want, got) + }) + } +} + +func Test_tryRemote(t *testing.T) { + // Create a test image + img, err := random.Image(1024, 5) + require.NoError(t, err) + + // Get the image digest for test expectations + digest, err := img.Digest() + require.NoError(t, err) + + // Set up registry server with null logger to suppress log output + nullLogger := log.New(io.Discard, "", 0) + s := httptest.NewServer(registry.New(registry.Logger(nullLogger))) + t.Cleanup(s.Close) + + u, err := url.Parse(s.URL) + require.NoError(t, err) + + tests := []struct { + name string + imageName string + setupImage func(t *testing.T, ref name.Reference) + wantName string + wantErr string + }{ + { + name: "successful image retrieval", + imageName: "test/alpine:3.10", + setupImage: func(t *testing.T, ref name.Reference) { + err := remote.Write(ref, img) + require.NoError(t, err) + }, + wantName: "/test/alpine:3.10", + }, + { + name: "helm chart config media type", + imageName: "test/helm:chart", + setupImage: func(t *testing.T, ref name.Reference) { + configFile, err := img.ConfigFile() + require.NoError(t, err) + + // Create a new config with helm chart media type + imageToWrite, err := mutate.Config(img, configFile.Config) + require.NoError(t, err) + + imageToWrite = mutate.ConfigMediaType(imageToWrite, "application/vnd.cncf.helm.chart") + + err = remote.Write(ref, imageToWrite) + require.NoError(t, err) + }, + wantErr: "unsupported artifact type", + }, + { + name: "image not found", + imageName: "test/notfound:latest", + wantErr: "NAME_UNKNOWN", + setupImage: func(*testing.T, name.Reference) {}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Parse the image name with the test server address + fullImageName := u.Host + "/" + tt.imageName + ref, err := name.ParseReference(fullImageName) + require.NoError(t, err) + + // Set up the image in registry if needed + tt.setupImage(t, ref) + + ctx := t.Context() + got, cleanup, err := tryRemote(ctx, fullImageName, ref, types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + Insecure: true, + }, + }) + t.Cleanup(cleanup) + + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.NotNil(t, got) + assert.Contains(t, got.Name(), tt.wantName) + + // Verify RepoTags and RepoDigests contain expected values + repoTags := got.RepoTags() + repoDigests := got.RepoDigests() + assert.Len(t, repoTags, 1) + assert.Contains(t, repoTags[0], tt.imageName) + assert.Len(t, repoDigests, 1) + assert.Contains(t, repoDigests[0], digest.String()) + }) + } +} diff --git a/pkg/fanal/image/testdata/manifest.json b/pkg/fanal/image/testdata/manifest.json new file mode 100644 index 000000000000..58fc349934c9 --- /dev/null +++ b/pkg/fanal/image/testdata/manifest.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 2, + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { + "mediaType": "application/vnd.docker.container.image.v1+json", + "size": 6666, + "digest": "sha256:2073e0bcb60ee98548d313ead5eacbfe16d9054f8800a32bedd859922a99a6e1" + }, + "layers": [ + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 27092260, + "digest": "sha256:bc51dd8edc1b1132bb97827ed6bd16aac332b03fb03d4c02d2088067a5fbb499" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 23882249, + "digest": "sha256:66ba67045f57970205d6b5a6956fe5225565cbbcbf1900fc81612e66d30180ef" + }, + { + "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", + "size": 204, + "digest": "sha256:bf317aa10aa501e00f87c2ff5a414079c4f3083ab086c91f57878dbbffa70507" + } + ] +} \ No newline at end of file diff --git a/pkg/fanal/image/testdata/multi/blobs/sha256/56f658ee7c94c1a65099c680916c12f6b81ae4c586c662a8146791054fa466ab b/pkg/fanal/image/testdata/multi/blobs/sha256/56f658ee7c94c1a65099c680916c12f6b81ae4c586c662a8146791054fa466ab new file mode 100644 index 000000000000..4db0c4521917 --- /dev/null +++ b/pkg/fanal/image/testdata/multi/blobs/sha256/56f658ee7c94c1a65099c680916c12f6b81ae4c586c662a8146791054fa466ab @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:56ae38f2f5c54b98311b8b2463d4861368c451ac17098f4227d84946b42ab96d","size":348,"platform":{"architecture":"amd64","os":"linux"}},{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:d4797ba2a3f15e3fe4c13398104f2199cc3fe22da004c9d382a60b74990136ad","size":348,"platform":{"architecture":"arm","os":"linux"}}]} \ No newline at end of file diff --git a/pkg/fanal/image/testdata/multi/index.json b/pkg/fanal/image/testdata/multi/index.json new file mode 100644 index 000000000000..26373522f9ba --- /dev/null +++ b/pkg/fanal/image/testdata/multi/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.index.v1+json","digest":"sha256:56f658ee7c94c1a65099c680916c12f6b81ae4c586c662a8146791054fa466ab","size":435,"annotations":{"org.opencontainers.image.ref.name":"tg11"}}]} \ No newline at end of file diff --git a/pkg/fanal/image/testdata/single/index.json b/pkg/fanal/image/testdata/single/index.json new file mode 100644 index 000000000000..aa32a93f3903 --- /dev/null +++ b/pkg/fanal/image/testdata/single/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:56ae38f2f5c54b98311b8b2463d4861368c451ac17098f4227d84946b42ab96d","size":348,"annotations":{"org.opencontainers.image.ref.name":"3.14"}}]} \ No newline at end of file diff --git a/pkg/fanal/secret/builtin-allow-rules.go b/pkg/fanal/secret/builtin-allow-rules.go new file mode 100644 index 000000000000..4708033e424b --- /dev/null +++ b/pkg/fanal/secret/builtin-allow-rules.go @@ -0,0 +1,72 @@ +package secret + +var builtinAllowRules = []AllowRule{ + { + // `.dist-info` dir contains only metadata files such as version, license, and entry points. + // cf. https://github.com/aquasecurity/trivy/issues/8212 + ID: "dist-info", + Description: "Ignore Python .dist-info metadata directories", + Path: MustCompile(`\.dist-info\/`), + }, + { + ID: "tests", + Description: "Avoid test files and paths", + Path: MustCompile(`(^(?i)test|\/test|-test|_test|\.test)`), + }, + { + ID: "examples", + Description: "Avoid example files and paths", // e.g. https://github.com/boto/botocore/blob/develop/botocore/data/organizations/2016-11-28/examples-1.json + Path: MustCompile(`example`), + Regex: MustCompile("(?i)example"), + }, + { + ID: "vendor", + Description: "Vendor dirs", + Path: MustCompile(`\/vendor\/`), + }, + { + ID: "usr-dirs", + Description: "System dirs", + Path: MustCompile(`^usr\/(?:share|include|lib)\/`), + }, + { + ID: "locale-dir", + Description: "Locales directory contains locales file", + Path: MustCompile(`\/locales?\/`), + }, + { + ID: "markdown", + Description: "Markdown files", + Path: MustCompile(`\.md$`), + }, + { + ID: "node.js", + Description: "Node container images", + Path: MustCompile(`^opt\/yarn-v[\d.]+\/`), + }, + { + ID: "golang", + Description: "Go container images", + Path: MustCompile(`^usr\/local\/go\/`), + }, + { + ID: "python", + Description: "Python container images", + Path: MustCompile(`^usr\/local\/lib\/python[\d.]+\/`), + }, + { + ID: "rubygems", + Description: "Ruby container images", + Path: MustCompile(`^usr\/lib\/gems\/`), + }, + { + ID: "wordpress", + Description: "Wordpress container images", + Path: MustCompile(`^usr\/src\/wordpress\/`), + }, + { + ID: "anaconda-log", + Description: "Anaconda CI Logs in container images", + Path: MustCompile(`^var\/log\/anaconda\/`), + }, +} diff --git a/pkg/fanal/secret/builtin-rules.go b/pkg/fanal/secret/builtin-rules.go new file mode 100644 index 000000000000..0725f9f44f47 --- /dev/null +++ b/pkg/fanal/secret/builtin-rules.go @@ -0,0 +1,858 @@ +package secret + +import ( + "fmt" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + iacRules "github.com/aquasecurity/trivy/pkg/iac/rules" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +var ( + CategoryAWS = types.SecretRuleCategory("AWS") + CategoryGitHub = types.SecretRuleCategory("GitHub") + CategoryGitLab = types.SecretRuleCategory("GitLab") + CategoryAsymmetricPrivateKey = types.SecretRuleCategory("AsymmetricPrivateKey") + CategoryShopify = types.SecretRuleCategory("Shopify") + CategorySlack = types.SecretRuleCategory("Slack") + CategoryGoogle = types.SecretRuleCategory("Google") + CategoryStripe = types.SecretRuleCategory("Stripe") + CategoryPyPI = types.SecretRuleCategory("PyPI") + CategoryHeroku = types.SecretRuleCategory("Heroku") + CategoryTwilio = types.SecretRuleCategory("Twilio") + CategoryAge = types.SecretRuleCategory("Age") + CategoryFacebook = types.SecretRuleCategory("Facebook") + CategoryTwitter = types.SecretRuleCategory("Twitter") + CategoryAdobe = types.SecretRuleCategory("Adobe") + CategoryAlibaba = types.SecretRuleCategory("Alibaba") + CategoryAsana = types.SecretRuleCategory("Asana") + CategoryAtlassian = types.SecretRuleCategory("Atlassian") + CategoryBitbucket = types.SecretRuleCategory("Bitbucket") + CategoryBeamer = types.SecretRuleCategory("Beamer") + CategoryClojars = types.SecretRuleCategory("Clojars") + CategoryContentfulDelivery = types.SecretRuleCategory("ContentfulDelivery") + CategoryDatabricks = types.SecretRuleCategory("Databricks") + CategoryDiscord = types.SecretRuleCategory("Discord") + CategoryDoppler = types.SecretRuleCategory("Doppler") + CategoryDropbox = types.SecretRuleCategory("Dropbox") + CategoryDuffel = types.SecretRuleCategory("Duffel") + CategoryDynatrace = types.SecretRuleCategory("Dynatrace") + CategoryEasypost = types.SecretRuleCategory("Easypost") + CategoryFastly = types.SecretRuleCategory("Fastly") + CategoryFinicity = types.SecretRuleCategory("Finicity") + CategoryFlutterwave = types.SecretRuleCategory("Flutterwave") + CategoryFrameio = types.SecretRuleCategory("Frameio") + CategoryGoCardless = types.SecretRuleCategory("GoCardless") + CategoryGrafana = types.SecretRuleCategory("Grafana") + CategoryHashiCorp = types.SecretRuleCategory("HashiCorp") + CategoryHubSpot = types.SecretRuleCategory("HubSpot") + CategoryIntercom = types.SecretRuleCategory("Intercom") + CategoryIonic = types.SecretRuleCategory("Ionic") + CategoryJWT = types.SecretRuleCategory("JWT") + CategoryLinear = types.SecretRuleCategory("Linear") + CategoryLob = types.SecretRuleCategory("Lob") + CategoryMailchimp = types.SecretRuleCategory("Mailchimp") + CategoryMailgun = types.SecretRuleCategory("Mailgun") + CategoryMapbox = types.SecretRuleCategory("Mapbox") + CategoryMessageBird = types.SecretRuleCategory("MessageBird") + CategoryNewRelic = types.SecretRuleCategory("NewRelic") + CategoryNpm = types.SecretRuleCategory("Npm") + CategoryPlanetscale = types.SecretRuleCategory("Planetscale") + CategoryPrivatePackagist = types.SecretRuleCategory("Private Packagist") + CategoryPostman = types.SecretRuleCategory("Postman") + CategoryPulumi = types.SecretRuleCategory("Pulumi") + CategoryRubyGems = types.SecretRuleCategory("RubyGems") + CategorySendGrid = types.SecretRuleCategory("SendGrid") + CategorySendinblue = types.SecretRuleCategory("Sendinblue") + CategoryShippo = types.SecretRuleCategory("Shippo") + CategoryLinkedIn = types.SecretRuleCategory("LinkedIn") + CategoryTwitch = types.SecretRuleCategory("Twitch") + CategoryTypeform = types.SecretRuleCategory("Typeform") + CategoryDocker = types.SecretRuleCategory("Docker") + CategoryHuggingFace = types.SecretRuleCategory("HuggingFace") + CategorySymfony = types.SecretRuleCategory("Symfony") +) + +// Reusable regex patterns +const ( + quote = `["']?` + connect = `\s*(:|=>|=)?\s*` + endSecret = `[.,]?(\s+|$)` + startWord = "([^0-9a-zA-Z_]|^)" + endWord = "([^0-9a-zA-Z_]|$)" + + aws = `aws_?` +) + +// This function is exported for trivy-plugin-aqua purposes only +func GetBuiltinRules() []Rule { + return builtinRules +} + +// This function is exported for trivy-plugin-aqua purposes only +func GetSecretRulesMetadata() []iacRules.Check { + return xslices.Map(builtinRules, func(rule Rule) iacRules.Check { + return iacRules.Check{ + Name: rule.ID, + Description: rule.Title, + } + }) +} + +var builtinRules = []Rule{ + { + ID: "aws-access-key-id", + Category: CategoryAWS, + Severity: "CRITICAL", + Title: "AWS Access Key ID", + Regex: MustCompileWithoutWordPrefix(fmt.Sprintf(`(?P(A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16})%s%s`, quote, endSecret)), + SecretGroupName: "secret", + Keywords: []string{"AKIA", "AGPA", "AIDA", "AROA", "AIPA", "ANPA", "ANVA", "ASIA"}, + }, + { + ID: "aws-secret-access-key", + Category: CategoryAWS, + Severity: "CRITICAL", + Title: "AWS Secret Access Key", + Regex: MustCompile(fmt.Sprintf(`(?i)%s%s(sec(ret)?)?_?(access)?_?key%s%s%s(?P[A-Za-z0-9\/\+=]{40})%s%s`, quote, aws, quote, connect, quote, quote, endSecret)), + SecretGroupName: "secret", + Keywords: []string{"key"}, + }, + { + ID: "github-pat", + Category: CategoryGitHub, + Title: "GitHub Personal Access Token", + Severity: "CRITICAL", + Regex: MustCompileWithoutWordPrefix(`?Pghp_[0-9a-zA-Z]{36}`), + SecretGroupName: "secret", + Keywords: []string{"ghp_"}, + }, + { + ID: "github-oauth", + Category: CategoryGitHub, + Title: "GitHub OAuth Access Token", + Severity: "CRITICAL", + Regex: MustCompileWithoutWordPrefix(`?Pgho_[0-9a-zA-Z]{36}`), + SecretGroupName: "secret", + Keywords: []string{"gho_"}, + }, + { + ID: "github-app-token", + Category: CategoryGitHub, + Title: "GitHub App Token", + Severity: "CRITICAL", + Regex: MustCompileWithoutWordPrefix(`?P(ghu|ghs)_[0-9a-zA-Z]{36}`), + SecretGroupName: "secret", + Keywords: []string{"ghu_", "ghs_"}, + }, + { + ID: "github-refresh-token", + Category: CategoryGitHub, + Title: "GitHub Refresh Token", + Severity: "CRITICAL", + Regex: MustCompileWithoutWordPrefix(`?Pghr_[0-9a-zA-Z]{76}`), + SecretGroupName: "secret", + Keywords: []string{"ghr_"}, + }, + { + ID: "github-fine-grained-pat", + Category: CategoryGitHub, + Title: "GitHub Fine-grained personal access tokens", + Severity: "CRITICAL", + Regex: MustCompile(`github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}`), + Keywords: []string{"github_pat_"}, + }, + { + ID: "gitlab-pat", + Category: CategoryGitLab, + Title: "GitLab Personal Access Token", + Severity: "CRITICAL", + Regex: MustCompileWithoutWordPrefix(`?Pglpat-[0-9a-zA-Z\-\_]{20}`), + SecretGroupName: "secret", + Keywords: []string{"glpat-"}, + }, + { + // cf. https://huggingface.co/docs/hub/en/security-tokens + ID: "hugging-face-access-token", + Category: CategoryHuggingFace, + Severity: "CRITICAL", + Title: "Hugging Face Access Token", + Regex: MustCompileWithBoundaries(`?Phf_[A-Za-z0-9]{34,40}`), + SecretGroupName: "secret", + Keywords: []string{"hf_"}, + }, + { + ID: "private-key", + Category: CategoryAsymmetricPrivateKey, + Title: "Asymmetric Private Key", + Severity: "HIGH", + Regex: MustCompile(`(?i)-----\s*?BEGIN[ A-Z0-9_-]*?PRIVATE KEY( BLOCK)?\s*?-----[\s]*?(?P[A-Za-z0-9=+/\\][A-Za-z0-9=+/\\\s]{30,}[A-Za-z0-9=+/\\])[\s]*?-----\s*?END[ A-Z0-9_-]*? PRIVATE KEY( BLOCK)?\s*?-----`), + SecretGroupName: "secret", + Keywords: []string{"-----"}, + }, + { + ID: "shopify-token", + Category: CategoryShopify, + Title: "Shopify token", + Severity: "HIGH", + Regex: MustCompile(`shp(ss|at|ca|pa)_[a-fA-F0-9]{32}`), + Keywords: []string{"shpss_", "shpat_", "shpca_", "shppa_"}, + }, + { + ID: "slack-access-token", + Category: CategorySlack, + Title: "Slack token", + Severity: "HIGH", + Regex: MustCompileWithoutWordPrefix(`?Pxox[baprs]-([0-9a-zA-Z]{10,48})`), + SecretGroupName: "secret", + Keywords: []string{"xoxb-", "xoxa-", "xoxp-", "xoxr-", "xoxs-"}, + }, + { + ID: "stripe-publishable-token", + Category: CategoryStripe, + Title: "Stripe Publishable Key", + Severity: "LOW", + Regex: MustCompileWithoutWordPrefix(`?P(?i)pk_(test|live)_[0-9a-z]{10,32}`), + SecretGroupName: "secret", + Keywords: []string{"pk_test_", "pk_live_"}, + }, + { + ID: "stripe-secret-token", + Category: CategoryStripe, + Title: "Stripe Secret Key", + Severity: "CRITICAL", + Regex: MustCompileWithoutWordPrefix(`?P(?i)sk_(test|live)_[0-9a-z]{10,32}`), + SecretGroupName: "secret", + Keywords: []string{"sk_test_", "sk_live_"}, + }, + { + ID: "pypi-upload-token", + Category: CategoryPyPI, + Title: "PyPI upload token", + Severity: "HIGH", + Regex: MustCompile(`pypi-AgEIcHlwaS5vcmc[A-Za-z0-9\-_]{50,1000}`), + Keywords: []string{"pypi-AgEIcHlwaS5vcmc"}, + }, + { + ID: "gcp-service-account", + Category: CategoryGoogle, + Title: "Google (GCP) Service-account", + Severity: "CRITICAL", + Regex: MustCompile(`\"type\": \"service_account\"`), + Keywords: []string{"\"type\": \"service_account\""}, + }, + { + ID: "heroku-api-key", + Category: CategoryHeroku, + Title: "Heroku API Key", + Severity: "HIGH", + Regex: MustCompile(` (?i)(?Pheroku[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"heroku"}, + }, + { + ID: "slack-web-hook", + Category: CategorySlack, + Title: "Slack Webhook", + Severity: "MEDIUM", + Regex: MustCompile(`https:\/\/hooks.slack.com\/services\/[A-Za-z0-9+\/]{44,48}`), + Keywords: []string{"hooks.slack.com"}, + }, + { + ID: "twilio-api-key", + Category: CategoryTwilio, + Title: "Twilio API Key", + Severity: "MEDIUM", + Regex: MustCompile(`SK[0-9a-fA-F]{32}`), + Keywords: []string{"SK"}, + }, + { + ID: "age-secret-key", + Category: CategoryAge, + Title: "Age secret key", + Severity: "MEDIUM", + Regex: MustCompile(`AGE-SECRET-KEY-1[QPZRY9X8GF2TVDW0S3JN54KHCE6MUA7L]{58}`), + Keywords: []string{"AGE-SECRET-KEY-1"}, + }, + { + ID: "facebook-token", + Category: CategoryFacebook, + Title: "Facebook token", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Pfacebook[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-f0-9]{32})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"facebook"}, + }, + { + ID: "twitter-token", + Category: CategoryTwitter, + Title: "Twitter token", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Ptwitter[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-f0-9]{35,44})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"twitter"}, + }, + { + ID: "adobe-client-id", + Category: CategoryAdobe, + Title: "Adobe Client ID (Oauth Web)", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Padobe[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-f0-9]{32})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"adobe"}, + }, + { + ID: "adobe-client-secret", + Category: CategoryAdobe, + Title: "Adobe Client Secret", + Severity: "LOW", + Regex: MustCompile(`(p8e-)(?i)[a-z0-9]{32}`), + Keywords: []string{"p8e-"}, + }, + { + ID: "alibaba-access-key-id", + Category: CategoryAlibaba, + Title: "Alibaba AccessKey ID", + Severity: "HIGH", + Regex: MustCompile(`([^0-9A-Za-z]|^)(?P(LTAI)(?i)[a-z0-9]{20})([^0-9A-Za-z]|$)`), + SecretGroupName: "secret", + Keywords: []string{"LTAI"}, + }, + { + ID: "alibaba-secret-key", + Category: CategoryAlibaba, + Title: "Alibaba Secret Key", + Severity: "HIGH", + Regex: MustCompile(`(?i)(?Palibaba[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9]{30})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"alibaba"}, + }, + { + ID: "asana-client-id", + Category: CategoryAsana, + Title: "Asana Client ID", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pasana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[0-9]{16})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"asana"}, + }, + { + ID: "asana-client-secret", + Category: CategoryAsana, + Title: "Asana Client Secret", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pasana[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9]{32})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"asana"}, + }, + { + ID: "atlassian-api-token", + Category: CategoryAtlassian, + Title: "Atlassian API token", + Severity: "HIGH", + Regex: MustCompile(`(?i)(?Patlassian[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9]{24})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"atlassian"}, + }, + { + ID: "bitbucket-client-id", + Category: CategoryBitbucket, + Title: "Bitbucket client ID", + Severity: "HIGH", + Regex: MustCompile(`(?i)(?Pbitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9]{32})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"bitbucket"}, + }, + { + ID: "bitbucket-client-secret", + Category: CategoryBitbucket, + Title: "Bitbucket client secret", + Severity: "HIGH", + Regex: MustCompile(`(?i)(?Pbitbucket[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9_\-]{64})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"bitbucket"}, + }, + { + ID: "beamer-api-token", + Category: CategoryBeamer, + Title: "Beamer API token", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Pbeamer[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?Pb_[a-z0-9=_\-]{44})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"beamer"}, + }, + { + ID: "clojars-api-token", + Category: CategoryClojars, + Title: "Clojars API token", + Severity: "MEDIUM", + Regex: MustCompile(`(CLOJARS_)(?i)[a-z0-9]{60}`), + Keywords: []string{"CLOJARS_"}, + }, + { + ID: "contentful-delivery-api-token", + Category: CategoryContentfulDelivery, + Title: "Contentful delivery API token", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Pcontentful[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9\-=_]{43})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"contentful"}, + }, + { + ID: "databricks-api-token", + Category: CategoryDatabricks, + Title: "Databricks API token", + Severity: "MEDIUM", + Regex: MustCompile(`dapi[a-h0-9]{32}`), + Keywords: []string{"dapi"}, + }, + { + ID: "discord-api-token", + Category: CategoryDiscord, + Title: "Discord API key", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pdiscord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-h0-9]{64})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"discord"}, + }, + { + ID: "discord-client-id", + Category: CategoryDiscord, + Title: "Discord client ID", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pdiscord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[0-9]{18})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"discord"}, + }, + { + ID: "discord-client-secret", + Category: CategoryDiscord, + Title: "Discord client secret", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pdiscord[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9=_\-]{32})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"discord"}, + }, + { + ID: "doppler-api-token", + Category: CategoryDoppler, + Title: "Doppler API token", + Severity: "MEDIUM", + Regex: MustCompile(`['\"](dp\.pt\.)(?i)[a-z0-9]{43}['\"]`), + Keywords: []string{"dp.pt."}, + }, + { + ID: "dropbox-api-secret", + Category: CategoryDropbox, + Title: "Dropbox API secret/key", + Severity: "HIGH", + Regex: MustCompile(`(?i)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"]([a-z0-9]{15})['\"]`), + Keywords: []string{"dropbox"}, + }, + { + ID: "dropbox-short-lived-api-token", + Category: CategoryDropbox, + Title: "Dropbox short lived API token", + Severity: "HIGH", + Regex: MustCompile(`(?i)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](sl\.[a-z0-9\-=_]{135})['\"]`), + Keywords: []string{"dropbox"}, + }, + { + ID: "dropbox-long-lived-api-token", + Category: CategoryDropbox, + Title: "Dropbox long lived API token", + Severity: "HIGH", + Regex: MustCompile(`(?i)(dropbox[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"][a-z0-9]{11}(AAAAAAAAAA)[a-z0-9\-_=]{43}['\"]`), + Keywords: []string{"dropbox"}, + }, + { + ID: "duffel-api-token", + Category: CategoryDuffel, + Title: "Duffel API token", + Severity: "LOW", + Regex: MustCompile(`['\"]duffel_(test|live)_(?i)[a-z0-9_-]{43}['\"]`), + Keywords: []string{"duffel_test_", "duffel_live_"}, + }, + { + ID: "dynatrace-api-token", + Category: CategoryDynatrace, + Title: "Dynatrace API token", + Severity: "MEDIUM", + Regex: MustCompile(`['\"]dt0c01\.(?i)[a-z0-9]{24}\.[a-z0-9]{64}['\"]`), + Keywords: []string{"dt0c01."}, + }, + { + ID: "easypost-api-token", + Category: CategoryEasypost, + Title: "EasyPost API token", + Severity: "LOW", + Regex: MustCompile(`['\"]EZ[AT]K(?i)[a-z0-9]{54}['\"]`), + Keywords: []string{"EZAK", "EZAT"}, + }, + { + ID: "fastly-api-token", + Category: CategoryFastly, + Title: "Fastly API token", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pfastly[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9\-=_]{32})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"fastly"}, + }, + { + ID: "finicity-client-secret", + Category: CategoryFinicity, + Title: "Finicity client secret", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pfinicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9]{20})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"finicity"}, + }, + { + ID: "finicity-api-token", + Category: CategoryFinicity, + Title: "Finicity API token", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pfinicity[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-f0-9]{32})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"finicity"}, + }, + { + ID: "flutterwave-public-key", + Category: CategoryFlutterwave, + Title: "Flutterwave public/secret key", + Severity: "MEDIUM", + Regex: MustCompileWithoutWordPrefix(`?PFLW(PUB|SEC)K_TEST-(?i)[a-h0-9]{32}-X`), + SecretGroupName: "secret", + Keywords: []string{"FLWSECK_TEST-", "FLWPUBK_TEST-"}, + }, + { + ID: "flutterwave-enc-key", + Category: CategoryFlutterwave, + Title: "Flutterwave encrypted key", + Severity: "MEDIUM", + Regex: MustCompileWithoutWordPrefix(`?PFLWSECK_TEST[a-h0-9]{12}`), + SecretGroupName: "secret", + Keywords: []string{"FLWSECK_TEST"}, + }, + { + ID: "frameio-api-token", + Category: CategoryFrameio, + Title: "Frame.io API token", + Severity: "LOW", + Regex: MustCompile(`fio-u-(?i)[a-z0-9\-_=]{64}`), + Keywords: []string{"fio-u-"}, + }, + { + ID: "gocardless-api-token", + Category: CategoryGoCardless, + Title: "GoCardless API token", + Severity: "MEDIUM", + Regex: MustCompile(`['\"]live_(?i)[a-z0-9\-_=]{40}['\"]`), + Keywords: []string{"live_"}, + }, + { + ID: "grafana-api-token", + Category: CategoryGrafana, + Title: "Grafana API token", + Severity: "MEDIUM", + Regex: MustCompile(`['\"]?eyJrIjoi(?i)[a-z0-9\-_=]{72,92}['\"]?`), + Keywords: []string{"eyJrIjoi"}, + }, + { + ID: "hashicorp-tf-api-token", + Category: CategoryHashiCorp, + Title: "HashiCorp Terraform user/org API token", + Severity: "MEDIUM", + Regex: MustCompile(`['\"](?i)[a-z0-9]{14}\.atlasv1\.[a-z0-9\-_=]{60,70}['\"]`), + Keywords: []string{"atlasv1."}, + }, + { + ID: "hubspot-api-token", + Title: "HubSpot API token", + Category: CategoryHubSpot, + Severity: "LOW", + Regex: MustCompile(`(?i)(?Phubspot[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"hubspot"}, + }, + { + ID: "intercom-api-token", + Category: CategoryIntercom, + Title: "Intercom API token", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Pintercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9=_]{60})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"intercom"}, + }, + { + ID: "intercom-client-secret", + Category: CategoryIntercom, + Title: "Intercom client secret/ID", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Pintercom[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"intercom"}, + }, + { + ID: "ionic-api-token", + Category: CategoryIonic, + Title: "Ionic API token", + Regex: MustCompile(`(?i)(ionic[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](ion_[a-z0-9]{42})['\"]`), + Keywords: []string{"ionic"}, + }, + { + ID: "jwt-token", + Category: CategoryJWT, + Title: "JWT token", + Severity: "MEDIUM", + Regex: MustCompile(`ey[a-zA-Z0-9]{17,}\.ey[a-zA-Z0-9\/\\_-]{17,}\.(?:[a-zA-Z0-9\/\\_-]{10,}={0,2})?`), + Keywords: []string{".eyJ"}, + }, + { + ID: "linear-api-token", + Category: CategoryLinear, + Title: "Linear API token", + Severity: "MEDIUM", + Regex: MustCompile(`lin_api_(?i)[a-z0-9]{40}`), + Keywords: []string{"lin_api_"}, + }, + { + ID: "linear-client-secret", + Category: CategoryLinear, + Title: "Linear client secret/ID", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Plinear[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-f0-9]{32})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"linear"}, + }, + { + ID: "lob-api-key", + Category: CategoryLob, + Title: "Lob API Key", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Plob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P(live|test)_[a-f0-9]{35})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"lob"}, + }, + { + ID: "lob-pub-api-key", + Category: CategoryLob, + Title: "Lob Publishable API Key", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Plob[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P(test|live)_pub_[a-f0-9]{31})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"lob"}, + }, + { + ID: "mailchimp-api-key", + Category: CategoryMailchimp, + Title: "Mailchimp API key", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pmailchimp[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-f0-9]{32}-us20)['\"]`), + SecretGroupName: "secret", + Keywords: []string{"mailchimp"}, + }, + { + ID: "mailgun-token", + Category: CategoryMailgun, + Title: "Mailgun private API token", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pmailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P(pub)?key-[a-f0-9]{32})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"mailgun"}, + }, + { + ID: "mailgun-signing-key", + Category: CategoryMailgun, + Title: "Mailgun webhook signing key", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pmailgun[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-h0-9]{32}-[a-h0-9]{8}-[a-h0-9]{8})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"mailgun"}, + }, + { + ID: "mapbox-api-token", + Category: CategoryMapbox, + Title: "Mapbox API token", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(pk\.[a-z0-9]{60}\.[a-z0-9]{22})`), + Keywords: []string{"pk."}, + }, + { + ID: "messagebird-api-token", + Category: CategoryMessageBird, + Title: "MessageBird API token", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pmessagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9]{25})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"messagebird"}, + }, + { + ID: "messagebird-client-id", + Category: CategoryMessageBird, + Title: "MessageBird API client ID", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pmessagebird[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-h0-9]{8}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{4}-[a-h0-9]{12})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"messagebird"}, + }, + { + ID: "new-relic-user-api-key", + Category: CategoryNewRelic, + Title: "New Relic user API Key", + Severity: "MEDIUM", + Regex: MustCompile(`['\"](NRAK-[A-Z0-9]{27})['\"]`), + Keywords: []string{"NRAK-"}, + }, + { + ID: "new-relic-user-api-id", + Category: CategoryNewRelic, + Title: "New Relic user API ID", + Severity: "MEDIUM", + Regex: MustCompile(`(?i)(?Pnewrelic[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[A-Z0-9]{64})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"newrelic"}, + }, + { + ID: "new-relic-browser-api-token", + Category: CategoryNewRelic, + Title: "New Relic ingest browser API token", + Severity: "MEDIUM", + Regex: MustCompile(`['\"](NRJS-[a-f0-9]{19})['\"]`), + Keywords: []string{"NRJS-"}, + }, + { + ID: "npm-access-token", + Category: CategoryNpm, + Title: "npm access token", + Severity: "CRITICAL", + Regex: MustCompile(`['\"](npm_(?i)[a-z0-9]{36})['\"]`), + Keywords: []string{"npm_"}, + }, + { + ID: "planetscale-password", + Category: CategoryPlanetscale, + Title: "PlanetScale password", + Severity: "MEDIUM", + Regex: MustCompile(`pscale_pw_(?i)[a-z0-9\-_\.]{43}`), + Keywords: []string{"pscale_pw_"}, + }, + { + ID: "planetscale-api-token", + Category: CategoryPlanetscale, + Title: "PlanetScale API token", + Severity: "MEDIUM", + Regex: MustCompile(`pscale_tkn_(?i)[a-z0-9\-_\.]{43}`), + Keywords: []string{"pscale_tkn_"}, + }, + { + ID: "private-packagist-token", + Category: CategoryPrivatePackagist, + Title: "Private Packagist token", + Severity: "HIGH", + // https://packagist.com/docs/composer-authentication#token-format + Regex: MustCompile(`packagist_[ou][ru]t_(?i)[a-f0-9]{68}`), + Keywords: []string{"packagist_uut_", "packagist_ort_", "packagist_out_"}, + }, + { + ID: "postman-api-token", + Category: CategoryPostman, + Title: "Postman API token", + Severity: "MEDIUM", + Regex: MustCompile(`PMAK-(?i)[a-f0-9]{24}\-[a-f0-9]{34}`), + Keywords: []string{"PMAK-"}, + }, + { + ID: "pulumi-api-token", + Category: CategoryPulumi, + Title: "Pulumi API token", + Severity: "HIGH", + Regex: MustCompile(`pul-[a-f0-9]{40}`), + Keywords: []string{"pul-"}, + }, + { + ID: "rubygems-api-token", + Category: CategoryRubyGems, + Title: "Rubygem API token", + Severity: "MEDIUM", + Regex: MustCompile(`rubygems_[a-f0-9]{48}`), + Keywords: []string{"rubygems_"}, + }, + { + ID: "sendgrid-api-token", + Category: CategorySendGrid, + Title: "SendGrid API token", + Severity: "MEDIUM", + Regex: MustCompile(`SG\.(?i)[a-z0-9_\-\.]{66}`), + Keywords: []string{"SG."}, + }, + { + ID: "sendinblue-api-token", + Category: CategorySendinblue, + Title: "Sendinblue API token", + Severity: "LOW", + Regex: MustCompile(`xkeysib-[a-f0-9]{64}\-(?i)[a-z0-9]{16}`), + Keywords: []string{"xkeysib-"}, + }, + { + ID: "shippo-api-token", + Category: CategoryShippo, + Title: "Shippo API token", + Severity: "LOW", + Regex: MustCompile(`shippo_(live|test)_[a-f0-9]{40}`), + Keywords: []string{"shippo_live_", "shippo_test_"}, + }, + { + ID: "linkedin-client-secret", + Category: CategoryLinkedIn, + Title: "LinkedIn Client secret", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Plinkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z]{16})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"linkedin"}, + }, + { + ID: "linkedin-client-id", + Category: CategoryLinkedIn, + Title: "LinkedIn Client ID", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Plinkedin[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9]{14})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"linkedin"}, + }, + { + ID: "twitch-api-token", + Category: CategoryTwitch, + Title: "Twitch API token", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Ptwitch[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}['\"](?P[a-z0-9]{30})['\"]`), + SecretGroupName: "secret", + Keywords: []string{"twitch"}, + }, + { + ID: "typeform-api-token", + Category: CategoryTypeform, + Title: "Typeform API token", + Severity: "LOW", + Regex: MustCompile(`(?i)(?Ptypeform[a-z0-9_ .\-,]{0,25})(=|>|:=|\|\|:|<=|=>|:).{0,5}(?Ptfp_[a-z0-9\-_\.=]{59})`), + SecretGroupName: "secret", + Keywords: []string{"typeform"}, + }, + { + ID: "dockerconfig-secret", + Category: CategoryDocker, + Title: "Dockerconfig secret exposed", + Severity: "HIGH", + Regex: MustCompile(`(?i)(\.(dockerconfigjson|dockercfg):\s*\|*\s*(?P(ey|ew)+[A-Za-z0-9\/\+=]+))`), + SecretGroupName: "secret", + Keywords: []string{"dockerc"}, + }, + { + ID: "symfony-default-secret", + Category: CategorySymfony, + Title: "Symfony Default Secret", + Severity: "HIGH", + Regex: MustCompile(`ThisTokenIsNotSoSecretChangeIt|ThisEzPlatformTokenIsNotSoSecret_PleaseChangeIt`), + Keywords: []string{"TokenIsNotSoSecret"}, + }, +} diff --git a/pkg/fanal/secret/scanner.go b/pkg/fanal/secret/scanner.go new file mode 100644 index 000000000000..41f665d08704 --- /dev/null +++ b/pkg/fanal/secret/scanner.go @@ -0,0 +1,873 @@ +package secret + +import ( + "bytes" + "errors" + "fmt" + "io" + "os" + "regexp" + "slices" + "sort" + "strings" + "sync" + "unicode/utf8" + + "github.com/samber/lo" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +var ( + lineSep = []byte{'\n'} + warnUTF8Once = sync.OnceFunc(func() { + log.WithPrefix(log.PrefixSecret).Warn("Invalid UTF-8 sequences detected in file content, replacing with empty string") + }) +) + +const ( + // DefaultBufferSize is the default chunk size for streaming secret scanning + // 64KB provides a good balance between memory usage and I/O efficiency + // Larger buffers reduce I/O operations but use more memory + // Smaller buffers use less memory but may increase I/O overhead + DefaultBufferSize = 64 * 1024 // 64KB default buffer size + + // DefaultOverlap is the number of bytes to overlap between chunks + // This ensures that secrets spanning chunk boundaries are not missed + // Must be large enough to contain the longest possible secret pattern + // 4KB is sufficient for all secret types including RSA 4096-bit keys + DefaultOverlap = 4096 // 4KB overlap for boundary handling +) + +type Scanner struct { + logger *log.Logger + bufferSize int + overlapSize int + *Global +} + +type Config struct { + // Enable only specified built-in rules. If only one ID is specified, all other rules are disabled. + // All the built-in rules are enabled if this field is not specified. It doesn't affect custom rules. + EnableBuiltinRuleIDs []string `yaml:"enable-builtin-rules"` + + // Disable rules. It is applied to enabled IDs. + DisableRuleIDs []string `yaml:"disable-rules"` + + // Disable allow rules. + DisableAllowRuleIDs []string `yaml:"disable-allow-rules"` + + CustomRules []Rule `yaml:"rules"` + CustomAllowRules AllowRules `yaml:"allow-rules"` + ExcludeBlock ExcludeBlock `yaml:"exclude-block"` +} + +type Global struct { + Rules []Rule + AllowRules AllowRules + ExcludeBlock ExcludeBlock +} + +// Allow checks if the match is allowed +func (g Global) Allow(match string) bool { + return g.AllowRules.Allow(match) +} + +// AllowPath checks if the path is allowed +func (g Global) AllowPath(path string) bool { + return g.AllowRules.AllowPath(path) +} + +// Regexp adds unmarshalling from YAML for regexp.Regexp +type Regexp struct { + *regexp.Regexp +} + +func MustCompileWithoutWordPrefix(str string) *Regexp { + return MustCompile(fmt.Sprintf("%s(%s)", startWord, str)) +} + +func MustCompileWithBoundaries(str string) *Regexp { + return MustCompile(fmt.Sprintf("%s(%s)%s", startWord, str, endWord)) +} + +func MustCompile(str string) *Regexp { + return &Regexp{regexp.MustCompile(str)} +} + +// UnmarshalYAML unmarshals YAML into a regexp.Regexp +func (r *Regexp) UnmarshalYAML(value *yaml.Node) error { + var v string + if err := value.Decode(&v); err != nil { + return err + } + regex, err := regexp.Compile(v) + if err != nil { + return xerrors.Errorf("regexp compile error: %w", err) + } + + r.Regexp = regex + return nil +} + +type Rule struct { + ID string `yaml:"id"` + Category types.SecretRuleCategory `yaml:"category"` + Title string `yaml:"title"` + Severity string `yaml:"severity"` + Regex *Regexp `yaml:"regex"` + Keywords []string `yaml:"keywords"` + Path *Regexp `yaml:"path"` + AllowRules AllowRules `yaml:"allow-rules"` + ExcludeBlock ExcludeBlock `yaml:"exclude-block"` + SecretGroupName string `yaml:"secret-group-name"` + keywordsLower [][]byte // Pre-computed lowercase keywords +} + +func (s *Scanner) FindLocations(r Rule, content []byte) []Location { + if r.Regex == nil { + return nil + } + + if r.SecretGroupName != "" { + return s.FindSubmatchLocations(r, content) + } + + var locs []Location + indices := r.Regex.FindAllIndex(content, -1) + for _, index := range indices { + loc := Location{ + Start: index[0], + End: index[1], + } + + if s.AllowLocation(r, content, loc) { + continue + } + + locs = append(locs, loc) + } + return locs +} + +func (s *Scanner) FindSubmatchLocations(r Rule, content []byte) []Location { + var submatchLocations []Location + matchsIndices := r.Regex.FindAllSubmatchIndex(content, -1) + for _, matchIndices := range matchsIndices { + matchLocation := Location{ + // first two indexes are always start and end of the whole match + Start: matchIndices[0], + End: matchIndices[1], + } + + if s.AllowLocation(r, content, matchLocation) { + continue + } + + matchSubgroupsLocations := r.getMatchSubgroupsLocations(matchIndices) + if len(matchSubgroupsLocations) > 0 { + submatchLocations = append(submatchLocations, matchSubgroupsLocations...) + } + } + return submatchLocations +} + +func (s *Scanner) AllowLocation(r Rule, content []byte, loc Location) bool { + match := string(content[loc.Start:loc.End]) + return s.Allow(match) || r.Allow(match) +} + +func (r *Rule) getMatchSubgroupsLocations(matchLocs []int) []Location { + var locations []Location + for i, name := range r.Regex.SubexpNames() { + if name == r.SecretGroupName { + startLocIndex := 2 * i + endLocIndex := startLocIndex + 1 + locations = append(locations, Location{ + Start: matchLocs[startLocIndex], + End: matchLocs[endLocIndex], + }) + } + } + return locations +} + +func (r *Rule) MatchPath(path string) bool { + return r.Path == nil || r.Path.MatchString(path) +} + +func (r *Rule) MatchKeywords(contentLower []byte) bool { + if len(r.Keywords) == 0 { + return true + } + + for _, kwLower := range r.keywordsLower { + if bytes.Contains(contentLower, kwLower) { + return true + } + } + + return false +} + +func (r *Rule) AllowPath(path string) bool { + return r.AllowRules.AllowPath(path) +} + +func (r *Rule) Allow(match string) bool { + return r.AllowRules.Allow(match) +} + +type AllowRule struct { + ID string `yaml:"id"` + Description string `yaml:"description"` + Regex *Regexp `yaml:"regex"` + Path *Regexp `yaml:"path"` +} + +type AllowRules []AllowRule + +func (rules AllowRules) AllowPath(path string) bool { + for _, rule := range rules { + if rule.Path != nil && rule.Path.MatchString(path) { + return true + } + } + return false +} + +func (rules AllowRules) Allow(match string) bool { + for _, rule := range rules { + if rule.Regex != nil && rule.Regex.MatchString(match) { + return true + } + } + return false +} + +type ExcludeBlock struct { + Description string `yaml:"description"` + Regexes []*Regexp `yaml:"regexes"` +} + +type Location struct { + Start int + End int +} + +func (l Location) Match(loc Location) bool { + return l.Start <= loc.Start && loc.End <= l.End +} + +type Blocks struct { + content []byte + regexes []*Regexp + locs []Location + once *sync.Once +} + +func newBlocks(content []byte, regexes []*Regexp) Blocks { + return Blocks{ + content: content, + regexes: regexes, + once: new(sync.Once), + } +} + +func (b *Blocks) Match(block Location) bool { + b.once.Do(b.find) + for _, loc := range b.locs { + if loc.Match(block) { + return true + } + } + return false +} + +func (b *Blocks) find() { + for _, regex := range b.regexes { + results := regex.FindAllIndex(b.content, -1) + if len(results) == 0 { + continue + } + for _, r := range results { + b.locs = append(b.locs, Location{ + Start: r[0], + End: r[1], + }) + } + } +} + +func ParseConfig(configPath string) (*Config, error) { + // If no config is passed, use built-in rules and allow rules. + if configPath == "" { + return nil, nil + } + + logger := log.WithPrefix(log.PrefixSecret).With("config_path", configPath) + f, err := os.Open(configPath) + if errors.Is(err, os.ErrNotExist) { + // If the specified file doesn't exist, it just uses built-in rules and allow rules. + logger.Debug("No secret config detected") + return nil, nil + } else if err != nil { + return nil, xerrors.Errorf("file open error %s: %w", configPath, err) + } + defer f.Close() + + logger.Info("Loading the config file for secret scanning...") + + var config Config + if err = yaml.NewDecoder(f).Decode(&config); err != nil { + return nil, xerrors.Errorf("secrets config decode error: %w", err) + } + + // Update severity for custom rules + for i := range config.CustomRules { + config.CustomRules[i].Severity = convertSeverity(logger, config.CustomRules[i].Severity) + } + + return &config, nil +} + +// convertSeverity checks the severity and converts it to uppercase or uses "UNKNOWN" for the wrong severity. +func convertSeverity(logger *log.Logger, severity string) string { + switch strings.ToLower(severity) { + case "low", "medium", "high", "critical", "unknown": + return strings.ToUpper(severity) + default: + logger.Warn("Incorrect severity", log.String("severity", severity)) + return "UNKNOWN" + } +} + +// Option represents a functional option for configuring Scanner +type Option func(*Scanner) + +// WithBufferSize configures the buffer size for streaming secret scanning +func WithBufferSize(size int) Option { + return func(s *Scanner) { + s.bufferSize = size + } +} + +// WithOverlapSize configures the overlap size between chunks +func WithOverlapSize(size int) Option { + return func(s *Scanner) { + s.overlapSize = size + } +} + +// precomputeLowercaseKeywords pre-computes lowercase versions of keywords for a slice of rules +func precomputeLowercaseKeywords(rules []Rule) { + for i := range rules { + rules[i].keywordsLower = make([][]byte, len(rules[i].Keywords)) + for j, kw := range rules[i].Keywords { + rules[i].keywordsLower[j] = []byte(strings.ToLower(kw)) + } + } +} + +func NewScanner(config *Config, opts ...Option) Scanner { + scanner := Scanner{ + logger: log.WithPrefix(log.PrefixSecret), + bufferSize: DefaultBufferSize, + overlapSize: DefaultOverlap, + } + + // Apply functional options + for _, opt := range opts { + opt(&scanner) + } + + // Validate configuration + if scanner.overlapSize >= scanner.bufferSize { + scanner.logger.Warn("Overlap size exceeds buffer size, adjusting to 1/4 of buffer size", + log.Int("overlap_size", scanner.overlapSize), + log.Int("buffer_size", scanner.bufferSize)) + scanner.overlapSize = scanner.bufferSize / 4 + } + + // Use the default rules + if config == nil { + // Pre-compute lowercase keywords for builtin rules + precomputeLowercaseKeywords(builtinRules) + + scanner.Global = &Global{ + Rules: builtinRules, + AllowRules: builtinAllowRules, + } + return scanner + } + + enabledRules := builtinRules + if len(config.EnableBuiltinRuleIDs) != 0 { + // Enable only specified built-in rules + enabledRules = lo.Filter(builtinRules, func(v Rule, _ int) bool { + return slices.Contains(config.EnableBuiltinRuleIDs, v.ID) + }) + } + + // Custom rules are enabled regardless of "enable-builtin-rules". + enabledRules = append(enabledRules, config.CustomRules...) + + // Disable specified rules + rules := lo.Filter(enabledRules, func(v Rule, _ int) bool { + return !slices.Contains(config.DisableRuleIDs, v.ID) + }) + + // Disable specified allow rules + allowRules := append(builtinAllowRules, config.CustomAllowRules...) + allowRules = lo.Filter(allowRules, func(v AllowRule, _ int) bool { + return !slices.Contains(config.DisableAllowRuleIDs, v.ID) + }) + + // Pre-compute lowercase keywords for all rules + precomputeLowercaseKeywords(rules) + + scanner.Global = &Global{ + Rules: rules, + AllowRules: allowRules, + ExcludeBlock: config.ExcludeBlock, + } + + return scanner +} + +type ScanArgs struct { + FilePath string + Content io.Reader + Binary bool +} + +type Match struct { + Rule Rule + Location Location +} + +// Scan performs secret scanning on the provided content using streaming approach +// This method processes files in configurable chunks to maintain constant memory usage +// regardless of file size, making it suitable for scanning very large files +// +// The streaming approach: +// 1. Reads file content in chunks (default 64KB) +// 2. Maintains overlap between chunks to catch secrets at boundaries +// 3. Processes each chunk independently for secrets +// 4. Adjusts line numbers to account for chunk positioning +// 5. Combines results from all chunks +func (s *Scanner) Scan(args ScanArgs) types.Secret { + logger := s.logger.With(log.FilePath(args.FilePath)) + + // Check if path is globally allowed (skip scanning entirely) + if s.AllowPath(args.FilePath) { + logger.Debug("Skipped secret scanning matching allowed paths") + return types.Secret{ + FilePath: args.FilePath, + } + } + + // Perform streaming secret scanning + // This approach keeps memory usage constant regardless of file size + logger.Debug("Using streaming scanner") + result := s.scanStream(args.FilePath, args.Content, args.Binary) + return result +} + +// scanStream performs streaming secret scanning by processing files in chunks +// This approach keeps memory usage constant (O(buffer_size)) regardless of file size +func (s *Scanner) scanStream(filePath string, reader io.Reader, binary bool) types.Secret { + logger := s.logger.With(log.FilePath(filePath)) + logger.Debug("scanStream called", log.Int("buffer_size", s.bufferSize), log.Int("overlap_size", s.overlapSize)) + + // Initialize streaming context + state := s.initializeStreamingContext() + + // Process file in chunks until EOF + var allFindings []types.SecretFinding + chunkNum := 0 + for { + // Read next chunk with overlap from previous chunk + chunk, isEOF, err := s.readNextChunk(reader, state) + if err != nil { + logger.Error("Failed to read content during streaming", log.Err(err)) + break + } + + // Process the chunk for secrets if we have data + if len(chunk) > 0 { + chunkFindings := s.processChunkForSecrets(filePath, chunk, state, binary) + allFindings = append(allFindings, chunkFindings...) + chunkNum++ + + // Only update context if not EOF (no more chunks to process) + if !isEOF { + // Prepare for next iteration by updating context + s.updateStreamingContext(chunk, state) + } + } + + // Break if we've reached end of file + if isEOF { + break + } + } + + // Return empty result if no secrets found + if len(allFindings) == 0 { + return types.Secret{} + } + + // Debug: log findings before deduplication + s.logger.Debug("Findings before deduplication", log.Int("count", len(allFindings))) + for i, f := range allFindings { + s.logger.Debug("Finding", log.Int("index", i), log.String("rule_id", f.RuleID), log.Int("offset", f.Offset), log.Int("line", f.StartLine)) + } + + // Clean up and sort findings + allFindings = s.finalizeScanResults(allFindings) + + // Debug: log findings after deduplication + s.logger.Debug("Findings after deduplication", log.Int("count", len(allFindings))) + + return types.Secret{ + FilePath: filePath, + Findings: allFindings, + } +} + +// chunkState holds the state for streaming secret scanning +type chunkState struct { + buffer []byte // Main buffer for reading chunks + overlapBuffer []byte // Buffer to store overlap from previous chunk + lineOffset int // Running count of lines processed so far + byteOffset int // Running count of bytes processed so far +} + +// initializeStreamingContext sets up the initial state for streaming +func (s *Scanner) initializeStreamingContext() *chunkState { + return &chunkState{ + buffer: make([]byte, s.bufferSize), + overlapBuffer: make([]byte, 0, s.overlapSize), + lineOffset: 0, + byteOffset: 0, + } +} + +// readNextChunk reads the next chunk of data, incorporating overlap from the previous chunk +// Returns the chunk data, whether EOF was reached, and any error +func (s *Scanner) readNextChunk(reader io.Reader, state *chunkState) ([]byte, bool, error) { + // Copy overlap data from previous chunk to beginning of buffer + overlapLen := len(state.overlapBuffer) + copy(state.buffer[:overlapLen], state.overlapBuffer) + + // Read new data after the overlap + n, err := reader.Read(state.buffer[overlapLen:]) + isEOF := errors.Is(err, io.EOF) + + // Handle the case where we get EOF immediately with no new data + if n == 0 && isEOF { + // Handle final overlap data if any exists + if overlapLen > 0 { + // Return the remaining overlap data as the final chunk + return state.overlapBuffer, true, nil + } + // No more data to process + return nil, true, nil + } + + if err != nil && !isEOF { + // Read error occurred + return nil, false, xerrors.Errorf("failed to read next chunk: %w", err) + } + + // Combine overlap and new data + totalLen := overlapLen + n + chunk := state.buffer[:totalLen] + + // If we have data, return it along with the EOF flag + // This allows us to process the last chunk even if it's smaller than buffer size + return chunk, isEOF, nil +} + +// processChunkForSecrets scans a chunk for secrets and adjusts line numbers based on global offset +func (s *Scanner) processChunkForSecrets(filePath string, chunk []byte, state *chunkState, binary bool) []types.SecretFinding { + // Scan the chunk + chunkResult := s.scanChunk(filePath, chunk, binary) + + // Debug logging + if len(chunkResult.Findings) > 0 { + s.logger.Debug("Found secrets in chunk", log.Int("count", len(chunkResult.Findings)), log.Int("line_offset", state.lineOffset)) + } + + // Adjust line numbers and byte offsets to account for previous chunks + for i := range chunkResult.Findings { + // Adjust finding line numbers by adding the cumulative line offset + chunkResult.Findings[i].StartLine += state.lineOffset + chunkResult.Findings[i].EndLine += state.lineOffset + + // Adjust byte offset by adding the cumulative byte offset + chunkResult.Findings[i].Offset += state.byteOffset + + // Adjust code context line numbers as well + for j := range chunkResult.Findings[i].Code.Lines { + chunkResult.Findings[i].Code.Lines[j].Number += state.lineOffset + } + } + + return chunkResult.Findings +} + +// updateStreamingContext prepares the context for the next iteration +// This involves setting up overlap and updating line/chunk offsets +func (s *Scanner) updateStreamingContext(chunk []byte, state *chunkState) { + totalLen := len(chunk) + + // Prepare overlap for next iteration to ensure secrets spanning chunk boundaries are detected + if totalLen > s.overlapSize { + // Save the last 'overlapSize' bytes for the next chunk + state.overlapBuffer = state.overlapBuffer[:s.overlapSize] + copy(state.overlapBuffer, chunk[totalLen-s.overlapSize:]) + + // Update line and byte offset based on non-overlapping part + // We only count lines/bytes that won't be reprocessed in the next chunk + nonOverlapPart := chunk[:totalLen-s.overlapSize] + state.lineOffset += bytes.Count(nonOverlapPart, lineSep) + state.byteOffset += len(nonOverlapPart) + } else { + // If chunk is smaller than overlap size, keep entire chunk as overlap + // This can happen with very small chunks near EOF + state.overlapBuffer = state.overlapBuffer[:totalLen] + copy(state.overlapBuffer, chunk) + // Don't update offsets since entire chunk will be reprocessed + } +} + +// finalizeScanResults performs cleanup and sorting of all findings +func (s *Scanner) finalizeScanResults(findings []types.SecretFinding) []types.SecretFinding { + // Remove duplicate findings that might occur at chunk boundaries + // Note: Currently we preserve all findings to avoid losing legitimate secrets + findings = s.deduplicateFindings(findings) + + // Sort findings for consistent output + sort.Slice(findings, func(i, j int) bool { + if findings[i].RuleID != findings[j].RuleID { + return findings[i].RuleID < findings[j].RuleID + } + return findings[i].Match < findings[j].Match + }) + + return findings +} + +func (s *Scanner) scanChunk(filePath string, content []byte, binary bool) types.Secret { + logger := s.logger.With(log.FilePath(filePath)) + logger.Debug("scanChunk called", log.Int("content_len", len(content)), log.Int("num_rules", len(s.Rules))) + + var censored []byte + var copyCensored sync.Once + var matched []Match + + var findings []types.SecretFinding + globalExcludedBlocks := newBlocks(content, s.ExcludeBlock.Regexes) + + // Convert content to lowercase once for all keyword matching + contentLower := bytes.ToLower(content) + + for _, rule := range s.Rules { + ruleLogger := logger.With("rule_id", rule.ID) + // Check if the file path should be scanned by this rule + if !rule.MatchPath(filePath) { + ruleLogger.Debug("Skipped secret scanning as non-compliant to the rule") + continue + } + + // Check if the file path should be allowed + if rule.AllowPath(filePath) { + ruleLogger.Debug("Skipped secret scanning as allowed") + continue + } + + // Check if the file content contains keywords and should be scanned + if !rule.MatchKeywords(contentLower) { + continue + } + + // Detect secrets + locs := s.FindLocations(rule, content) + if len(locs) == 0 { + continue + } + ruleLogger.Debug("Found locations", log.Int("count", len(locs))) + + localExcludedBlocks := newBlocks(content, rule.ExcludeBlock.Regexes) + + for _, loc := range locs { + // Skip the secret if it is within excluded blocks. + if globalExcludedBlocks.Match(loc) || localExcludedBlocks.Match(loc) { + continue + } + + matched = append(matched, Match{ + Rule: rule, + Location: loc, + }) + logger.Debug("Found secret in chunk", log.String("rule_id", rule.ID), log.Int("start", loc.Start), log.Int("end", loc.End)) + copyCensored.Do(func() { + censored = make([]byte, len(content)) + copy(censored, content) + }) + censored = censorLocation(loc, censored) + } + } + + for _, match := range matched { + finding := toFinding(match.Rule, match.Location, censored) + // Rewrite unreadable fields for binary files + if binary { + finding.Match = fmt.Sprintf("Binary file %q matches a rule %q", filePath, match.Rule.Title) + finding.Code = types.Code{} + } + findings = append(findings, finding) + } + + if len(findings) == 0 { + return types.Secret{} + } + + sort.Slice(findings, func(i, j int) bool { + if findings[i].RuleID != findings[j].RuleID { + return findings[i].RuleID < findings[j].RuleID + } + return findings[i].Match < findings[j].Match + }) + + return types.Secret{ + FilePath: filePath, + Findings: findings, + } +} + +// deduplicateFindings removes duplicate secret findings that may occur at chunk boundaries +func (s *Scanner) deduplicateFindings(findings []types.SecretFinding) []types.SecretFinding { + // Deduplicate based on rule ID and byte offset + // This accurately identifies the same secret across chunk boundaries + // Different secrets at the same offset are impossible, so this is safe + return lo.UniqBy(findings, func(f types.SecretFinding) string { + return fmt.Sprintf("%s:%d-%d-%d", f.RuleID, f.StartLine, f.EndLine, f.Offset) + }) +} + +func censorLocation(loc Location, input []byte) []byte { + for i := loc.Start; i < loc.End; i++ { + if input[i] != '\n' { + input[i] = '*' + } + } + return input +} + +func toFinding(rule Rule, loc Location, content []byte) types.SecretFinding { + startLine, endLine, code, matchLine := findLocation(loc.Start, loc.End, content) + + return types.SecretFinding{ + RuleID: rule.ID, + Category: rule.Category, + Severity: lo.Ternary(rule.Severity == "", "UNKNOWN", rule.Severity), + Title: rule.Title, + Match: matchLine, + StartLine: startLine, + EndLine: endLine, + Code: code, + Offset: loc.Start, + } +} + +const ( + secretHighlightRadius = 2 // number of lines above + below each secret to include in code output + maxLineLength = 100 // all lines longer will be cut off +) + +func findLocation(start, end int, content []byte) (int, int, types.Code, string) { + startLineNum := bytes.Count(content[:start], lineSep) + + lineStart := bytes.LastIndex(content[:start], lineSep) + if lineStart == -1 { + lineStart = 0 + } else { + lineStart++ + } + + lineEnd := bytes.Index(content[start:], lineSep) + if lineEnd == -1 { + lineEnd = len(content) + } else { + lineEnd += start + } + + if lineEnd-lineStart > 100 { + lineStart = lo.Ternary(start-lineStart-30 < 0, lineStart, start-30) + lineEnd = lo.Ternary(end+20 > lineEnd, lineEnd, end+20) + } + matchLine := sanitizeString(content[lineStart:lineEnd]) + endLineNum := startLineNum + bytes.Count(content[start:end], lineSep) + + var code types.Code + + lines := bytes.Split(content, lineSep) + codeStart := lo.Ternary(startLineNum-secretHighlightRadius < 0, 0, startLineNum-secretHighlightRadius) + codeEnd := lo.Ternary(endLineNum+secretHighlightRadius > len(lines), len(lines), endLineNum+secretHighlightRadius) + + rawLines := lines[codeStart:codeEnd] + var foundFirst bool + for i, rawLine := range rawLines { + realLine := codeStart + i + inCause := realLine >= startLineNum && realLine <= endLineNum + + var strRawLine string + if len(rawLine) > maxLineLength { + strRawLine = lo.Ternary(inCause, matchLine, sanitizeString(rawLine[:maxLineLength])) + } else { + strRawLine = sanitizeString(rawLine) + } + + code.Lines = append(code.Lines, types.Line{ + Number: codeStart + i + 1, + Content: strRawLine, + IsCause: inCause, + Highlighted: strRawLine, + FirstCause: !foundFirst && inCause, + LastCause: false, + }) + foundFirst = foundFirst || inCause + } + if len(code.Lines) > 0 { + for i := len(code.Lines) - 1; i >= 0; i-- { + if code.Lines[i].IsCause { + code.Lines[i].LastCause = true + break + } + } + } + + return startLineNum + 1, endLineNum + 1, code, matchLine +} + +// sanitizeString converts bytes to a valid string for display +// It removes carriage return characters for consistent output across different line ending styles +// and ensures valid UTF-8 encoding, logging a warning once if invalid sequences are found +func sanitizeString(data []byte) string { + // Remove carriage returns for consistent display + data = bytes.ReplaceAll(data, []byte("\r"), []byte("")) + + if utf8.Valid(data) { + return string(data) + } + + warnUTF8Once() + + return strings.ToValidUTF8(string(data), string(utf8.RuneError)) +} diff --git a/pkg/fanal/secret/scanner_test.go b/pkg/fanal/secret/scanner_test.go new file mode 100644 index 000000000000..b8b396a634fd --- /dev/null +++ b/pkg/fanal/secret/scanner_test.go @@ -0,0 +1,1976 @@ +package secret_test + +import ( + "bytes" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/secret" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestMain(m *testing.M) { + log.InitLogger(false, true) + os.Exit(m.Run()) +} + +func TestSecretScanner(t *testing.T) { + wantFinding1 := types.SecretFinding{ + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "generic secret line secret=\"*********\"", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "--- ignore block start ---", + Highlighted: "--- ignore block start ---", + }, + { + Number: 2, + Content: "generic secret line secret=\"*********\"", + Highlighted: "generic secret line secret=\"*********\"", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "--- ignore block stop ---", + Highlighted: "--- ignore block stop ---", + }, + }, + }, + Offset: 55, + } + wantFinding2 := types.SecretFinding{ + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 4, + EndLine: 4, + Match: "secret=\"**********\"", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 2, + Content: "generic secret line secret=\"*********\"", + Highlighted: "generic secret line secret=\"*********\"", + }, + { + Number: 3, + Content: "--- ignore block stop ---", + Highlighted: "--- ignore block stop ---", + }, + { + Number: 4, + Content: "secret=\"**********\"", + Highlighted: "secret=\"**********\"", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 5, + Content: "credentials: { user: \"username\" password: \"123456789\" }", + Highlighted: "credentials: { user: \"username\" password: \"123456789\" }", + }, + }, + }, + Offset: 100, + } + wantFindingRegexDisabled := types.SecretFinding{ + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 4, + EndLine: 4, + Match: "secret=\"**********\"", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 2, + Content: "generic secret line secret=\"somevalue\"", + Highlighted: "generic secret line secret=\"somevalue\"", + }, + { + Number: 3, + Content: "--- ignore block stop ---", + Highlighted: "--- ignore block stop ---", + }, + { + Number: 4, + Content: "secret=\"**********\"", + Highlighted: "secret=\"**********\"", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 5, + Content: "credentials: { user: \"username\" password: \"123456789\" }", + Highlighted: "credentials: { user: \"username\" password: \"123456789\" }", + }, + }, + }, + Offset: 100, + } + wantFindingMultipleGroupsUsername := types.SecretFinding{ + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 5, + EndLine: 5, + Match: "credentials: { user: \"********\" password: \"*********\" }", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 3, + Content: "--- ignore block stop ---", + Highlighted: "--- ignore block stop ---", + }, + { + Number: 4, + Content: "secret=\"othervalue\"", + Highlighted: "secret=\"othervalue\"", + }, + { + Number: 5, + Content: "credentials: { user: \"********\" password: \"*********\" }", + Highlighted: "credentials: { user: \"********\" password: \"*********\" }", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 134, + } + wantFindingMultipleGroupsPassword := types.SecretFinding{ + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 5, + EndLine: 5, + Match: "credentials: { user: \"********\" password: \"*********\" }", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 3, + Content: "--- ignore block stop ---", + Highlighted: "--- ignore block stop ---", + }, + { + Number: 4, + Content: "secret=\"othervalue\"", + Highlighted: "secret=\"othervalue\"", + }, + { + Number: 5, + Content: "credentials: { user: \"********\" password: \"*********\" }", + Highlighted: "credentials: { user: \"********\" password: \"*********\" }", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 155, + } + wantFinding5 := types.SecretFinding{ + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Title: "AWS Access Key ID", + Severity: "CRITICAL", + StartLine: 2, + EndLine: 2, + Match: "AWS_ACCESS_KEY_ID=********************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "'AWS_secret_KEY'=\"****************************************\"", + Highlighted: "'AWS_secret_KEY'=\"****************************************\"", + }, + { + Number: 2, + Content: "AWS_ACCESS_KEY_ID=********************", + Highlighted: "AWS_ACCESS_KEY_ID=********************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "\"aws_account_ID\":'1234-5678-9123'", + Highlighted: "\"aws_account_ID\":'1234-5678-9123'", + }, + }, + }, + Offset: 78, + } + wantFinding5a := types.SecretFinding{ + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Title: "AWS Access Key ID", + Severity: "CRITICAL", + StartLine: 2, + EndLine: 2, + Match: "AWS_ACCESS_KEY_ID=********************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "GITHUB_PAT=****************************************", + Highlighted: "GITHUB_PAT=****************************************", + }, + { + Number: 2, + Content: "AWS_ACCESS_KEY_ID=********************", + Highlighted: "AWS_ACCESS_KEY_ID=********************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 70, + } + wantFindingPATDisabled := types.SecretFinding{ + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Title: "AWS Access Key ID", + Severity: "CRITICAL", + StartLine: 2, + EndLine: 2, + Match: "AWS_ACCESS_KEY_ID=********************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "GITHUB_PAT=ghp_012345678901234567890123456789abcdef", + Highlighted: "GITHUB_PAT=ghp_012345678901234567890123456789abcdef", + }, + { + Number: 2, + Content: "AWS_ACCESS_KEY_ID=********************", + Highlighted: "AWS_ACCESS_KEY_ID=********************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 70, + } + wantFinding6 := types.SecretFinding{ + RuleID: "github-pat", + Category: secret.CategoryGitHub, + Title: "GitHub Personal Access Token", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: "GITHUB_PAT=****************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "GITHUB_PAT=****************************************", + Highlighted: "GITHUB_PAT=****************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 2, + Content: "AWS_ACCESS_KEY_ID=********************", + Highlighted: "AWS_ACCESS_KEY_ID=********************", + }, + }, + }, + Offset: 11, + } + wantFindingGitHubPAT := types.SecretFinding{ + RuleID: "github-fine-grained-pat", + Category: secret.CategoryGitHub, + Title: "GitHub Fine-grained personal access tokens", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: "GITHUB_TOKEN=*********************************************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "GITHUB_TOKEN=*********************************************************************************************", + Highlighted: "GITHUB_TOKEN=*********************************************************************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 13, + } + wantFindingMyAwsAccessKey := types.SecretFinding{ + RuleID: "aws-secret-access-key", + Category: secret.CategoryAWS, + Title: "AWS Secret Access Key", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: `MyAWS_secret_KEY="****************************************"`, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "MyAWS_secret_KEY=\"****************************************\"", + Highlighted: "MyAWS_secret_KEY=\"****************************************\"", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 2, + Content: "our*********************************************************************************************", + Highlighted: "our*********************************************************************************************", + }, + }, + }, + Offset: 18, + } + wantFindingMyGitHubPAT := types.SecretFinding{ + RuleID: "github-fine-grained-pat", + Category: secret.CategoryGitHub, + Title: "GitHub Fine-grained personal access tokens", + Severity: "CRITICAL", + StartLine: 2, + EndLine: 2, + Match: "our*********************************************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "MyAWS_secret_KEY=\"****************************************\"", + Highlighted: "MyAWS_secret_KEY=\"****************************************\"", + }, + { + Number: 2, + Content: "our*********************************************************************************************", + Highlighted: "our*********************************************************************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 63, + } + wantFindingGHButDisableAWS := types.SecretFinding{ + RuleID: "github-pat", + Category: secret.CategoryGitHub, + Title: "GitHub Personal Access Token", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: "GITHUB_PAT=****************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "GITHUB_PAT=****************************************", + Highlighted: "GITHUB_PAT=****************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 2, + Content: "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF", + Highlighted: "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF", + }, + }, + }, + Offset: 11, + } + wantFinding7 := types.SecretFinding{ + RuleID: "github-pat", + Category: secret.CategoryGitHub, + Title: "GitHub Personal Access Token", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: "aaaaaaaaaaaaaaaaaa GITHUB_PAT=**************************************** bbbbbbbbbbbbbbbbbbb", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "aaaaaaaaaaaaaaaaaa GITHUB_PAT=**************************************** bbbbbbbbbbbbbbbbbbb", + Highlighted: "aaaaaaaaaaaaaaaaaa GITHUB_PAT=**************************************** bbbbbbbbbbbbbbbbbbb", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 67, + } + wantFinding8 := types.SecretFinding{ + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "UNKNOWN", + StartLine: 2, + EndLine: 2, + Match: "generic secret line secret=\"*********\"", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "--- ignore block start ---", + Highlighted: "--- ignore block start ---", + }, + { + Number: 2, + Content: "generic secret line secret=\"*********\"", + Highlighted: "generic secret line secret=\"*********\"", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "--- ignore block stop ---", + Highlighted: "--- ignore block stop ---", + }, + }, + }, + Offset: 55, + } + wantFinding9 := types.SecretFinding{ + RuleID: "aws-secret-access-key", + Category: secret.CategoryAWS, + Title: "AWS Secret Access Key", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: `'AWS_secret_KEY'="****************************************"`, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "'AWS_secret_KEY'=\"****************************************\"", + Highlighted: "'AWS_secret_KEY'=\"****************************************\"", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 2, + Content: "AWS_ACCESS_KEY_ID=********************", + Highlighted: "AWS_ACCESS_KEY_ID=********************", + }, + }, + }, + Offset: 18, + } + wantFinding10 := types.SecretFinding{ + RuleID: "aws-secret-access-key", + Category: secret.CategoryAWS, + Title: "AWS Secret Access Key", + Severity: "CRITICAL", + StartLine: 5, + EndLine: 5, + Match: ` "created_by": "ENV aws_sec_key "****************************************",`, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 3, + Content: "\"aws_account_ID\":'1234-5678-9123'", + Highlighted: "\"aws_account_ID\":'1234-5678-9123'", + }, + { + Number: 4, + Content: "AWS_example=AKIAIOSFODNN7EXAMPLE", + Highlighted: "AWS_example=AKIAIOSFODNN7EXAMPLE", + }, + { + Number: 5, + Content: " \"created_by\": \"ENV aws_sec_key \"****************************************\",", + Highlighted: " \"created_by\": \"ENV aws_sec_key \"****************************************\",", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 200, + } + wantFindingAsymmetricPrivateKeyJson := types.SecretFinding{ + RuleID: "private-key", + Category: secret.CategoryAsymmetricPrivateKey, + Title: "Asymmetric Private Key", + Severity: "HIGH", + StartLine: 1, + EndLine: 1, + Match: "----BEGIN RSA PRIVATE KEY-----**************************************************************************************************************************-----END RSA PRIVATE", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "----BEGIN RSA PRIVATE KEY-----**************************************************************************************************************************-----END RSA PRIVATE", + Highlighted: "----BEGIN RSA PRIVATE KEY-----**************************************************************************************************************************-----END RSA PRIVATE", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 40, + } + wantFindingAsymmetricPrivateKey := types.SecretFinding{ + RuleID: "private-key", + Category: secret.CategoryAsymmetricPrivateKey, + Title: "Asymmetric Private Key", + Severity: "HIGH", + StartLine: 2, + EndLine: 4, + Match: "****************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "-----BEGIN RSA PRIVATE KEY-----", + Highlighted: "-----BEGIN RSA PRIVATE KEY-----", + }, + { + Number: 2, + Content: "****************************************************************", + Highlighted: "****************************************************************", + IsCause: true, + FirstCause: true, + LastCause: false, + }, + { + Number: 3, + Content: "****************************************************************", + Highlighted: "****************************************************************", + IsCause: true, + FirstCause: false, + LastCause: false, + }, + { + Number: 4, + Content: "****************************************************", + Highlighted: "****************************************************", + IsCause: true, + FirstCause: false, + LastCause: true, + }, + { + Number: 5, + Content: "-----END RSA PRIVATE KEY-----", + Highlighted: "-----END RSA PRIVATE KEY-----", + }, + }, + }, + Offset: 32, + } + wantFindingAsymmSecretKey := types.SecretFinding{ + RuleID: "private-key", + Category: secret.CategoryAsymmetricPrivateKey, + Title: "Asymmetric Private Key", + Severity: "HIGH", + StartLine: 2, + EndLine: 3, + Match: "***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "-----BEGIN RSA PRIVATE KEY-----", + Highlighted: "-----BEGIN RSA PRIVATE KEY-----", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + { + Number: 2, + Content: "***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************", + Highlighted: "***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************", + IsCause: true, + FirstCause: true, + LastCause: false, + }, + { + Number: 3, + Content: "********************************************************************", + Highlighted: "********************************************************************", + IsCause: true, + FirstCause: false, + LastCause: true, + }, + { + Number: 4, + Content: "-----END RSA PRIVATE KEY-----", + Highlighted: "-----END RSA PRIVATE KEY-----", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + }, + }, + Offset: 32, + } + wantFindingMinimumAsymmSecretKey := types.SecretFinding{ + RuleID: "private-key", + Category: secret.CategoryAsymmetricPrivateKey, + Title: "Asymmetric Private Key", + Severity: "HIGH", + StartLine: 9, + EndLine: 9, + Match: "-----BEGIN PRIVATE KEY-----********************************-----END PRIVATE KEY-----", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 7, + Content: "", + Highlighted: "", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + { + Number: 8, + Content: "## Theoretically Asymmetric Private Key of minimum length (RSA 128 bits)", + Highlighted: "## Theoretically Asymmetric Private Key of minimum length (RSA 128 bits)", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + { + Number: 9, + Content: "-----BEGIN PRIVATE KEY-----********************************-----END PRIVATE KEY-----", + Highlighted: "-----BEGIN PRIVATE KEY-----********************************-----END PRIVATE KEY-----", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 1842, + } + wantFindingAlibabaAccessKeyId := types.SecretFinding{ + RuleID: "alibaba-access-key-id", + Category: secret.CategoryAlibaba, + Title: "Alibaba AccessKey ID", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "key = ************************,", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "key : LTAI1234567890ABCDEFG123asd", + Highlighted: "key : LTAI1234567890ABCDEFG123asd", + }, + { + Number: 2, + Content: "key = ************************,", + Highlighted: "key = ************************,", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "asdLTAI1234567890ABCDEFG123", + Highlighted: "asdLTAI1234567890ABCDEFG123", + }, + }, + }, + Offset: 40, + } + wantFindingDockerKey1 := types.SecretFinding{ + RuleID: "dockerconfig-secret", + Category: secret.CategoryDocker, + Title: "Dockerconfig secret exposed", + Severity: "HIGH", + StartLine: 4, + EndLine: 4, + Match: " .dockercfg: ************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 2, + Content: " .dockerconfigjson: ************", + Highlighted: " .dockerconfigjson: ************", + }, + { + Number: 3, + Content: "data2:", + Highlighted: "data2:", + }, + { + Number: 4, + Content: " .dockercfg: ************", + Highlighted: " .dockercfg: ************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 62, + } + wantFindingDockerKey2 := types.SecretFinding{ + RuleID: "dockerconfig-secret", + Category: secret.CategoryDocker, + Title: "Dockerconfig secret exposed", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: " .dockerconfigjson: ************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "data1:", + Highlighted: "data1:", + }, + { + Number: 2, + Content: " .dockerconfigjson: ************", + Highlighted: " .dockerconfigjson: ************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "data2:", + Highlighted: "data2:", + }, + }, + }, + Offset: 28, + } + wantFindingPrivatePackagistOrgReadToken := types.SecretFinding{ + RuleID: "private-packagist-token", + Category: secret.CategoryPrivatePackagist, + Title: "Private Packagist token", + Severity: "HIGH", + StartLine: 1, + EndLine: 1, + Match: "ORG_READ_TOKEN=**********************************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "ORG_READ_TOKEN=**********************************************************************************", + Highlighted: "ORG_READ_TOKEN=**********************************************************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 2, + Content: "ORG_WRITE_TOKEN=**********************************************************************************", + Highlighted: "ORG_WRITE_TOKEN=**********************************************************************************", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + }, + }, + Offset: 15, + } + wantFindingPrivatePackagistOrgUpdateToken := types.SecretFinding{ + RuleID: "private-packagist-token", + Category: secret.CategoryPrivatePackagist, + Title: "Private Packagist token", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "ORG_WRITE_TOKEN=**********************************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "ORG_READ_TOKEN=**********************************************************************************", + Highlighted: "ORG_READ_TOKEN=**********************************************************************************", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + { + Number: 2, + Content: "ORG_WRITE_TOKEN=**********************************************************************************", + Highlighted: "ORG_WRITE_TOKEN=**********************************************************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "USER_TOKEN=**********************************************************************************", + Highlighted: "USER_TOKEN=**********************************************************************************", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + }, + }, + Offset: 114, + } + wantFindingPrivatePackagistUserToken := types.SecretFinding{ + RuleID: "private-packagist-token", + Category: secret.CategoryPrivatePackagist, + Title: "Private Packagist token", + Severity: "HIGH", + StartLine: 3, + EndLine: 3, + Match: "USER_TOKEN=**********************************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "ORG_READ_TOKEN=**********************************************************************************", + Highlighted: "ORG_READ_TOKEN=**********************************************************************************", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + { + Number: 2, + Content: "ORG_WRITE_TOKEN=**********************************************************************************", + Highlighted: "ORG_WRITE_TOKEN=**********************************************************************************", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + { + Number: 3, + Content: "USER_TOKEN=**********************************************************************************", + Highlighted: "USER_TOKEN=**********************************************************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 4, + Content: "", + Highlighted: "", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + }, + }, + Offset: 208, + } + wantFindingHuggingFace := types.SecretFinding{ + RuleID: "hugging-face-access-token", + Category: secret.CategoryHuggingFace, + Title: "Hugging Face Access Token", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: "HF_example_token: ******************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "HF_example_token: ******************************************", + Highlighted: "HF_example_token: ******************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 2, + Content: "", + Highlighted: "", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + }, + }, + Offset: 18, + } + wantFindingGrafanaQuoted := types.SecretFinding{ + RuleID: "grafana-api-token", + Category: secret.CategoryGrafana, + Title: "Grafana API token", + Severity: "MEDIUM", + StartLine: 1, + EndLine: 1, + Match: "GRAFANA_TOKEN=**********************************************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "GRAFANA_TOKEN=**********************************************************************************************", + Highlighted: "GRAFANA_TOKEN=**********************************************************************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 2, + Content: "GRAFANA_TOKEN=**************************************************************************************", + Highlighted: "GRAFANA_TOKEN=**************************************************************************************", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + }, + }, + Offset: 14, + } + wantFindingGrafanaUnquoted := types.SecretFinding{ + RuleID: "grafana-api-token", + Category: secret.CategoryGrafana, + Title: "Grafana API token", + Severity: "MEDIUM", + StartLine: 2, + EndLine: 2, + Match: "GRAFANA_TOKEN=********************************************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "GRAFANA_TOKEN=**************************************************************************************", + Highlighted: "GRAFANA_TOKEN=**************************************************************************************", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + { + Number: 2, + Content: "GRAFANA_TOKEN=********************************************************************************************", + Highlighted: "GRAFANA_TOKEN=********************************************************************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "", + Highlighted: "", + }, + }, + }, + Offset: 123, + } + wantFindingSymfonySecret := types.SecretFinding{ + RuleID: "symfony-default-secret", + Category: secret.CategorySymfony, + Title: "Symfony Default Secret", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: " secret: ******************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "parameters:", + Highlighted: "parameters:", + IsCause: false, + FirstCause: false, + LastCause: false, + }, + { + Number: 2, + Content: " secret: ******************************", + Highlighted: " secret: ******************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 22, + } + wantMultiLine := types.SecretFinding{ + RuleID: "multi-line-secret", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "***************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "123", + Highlighted: "123", + }, + { + Number: 2, + Content: "***************", + Highlighted: "***************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "123", + Highlighted: "123", + }, + }, + }, + Offset: 4, + } + wantFindingTokenInsideJs := types.SecretFinding{ + RuleID: "stripe-publishable-token", + Category: "Stripe", + Title: "Stripe Publishable Key", + Severity: "LOW", + StartLine: 1, + EndLine: 1, + Match: "){case a.ez.PRODUCTION:return\"********************************\";case a.ez.TEST:cas", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "){case a.ez.PRODUCTION:return\"********************************\";case a.ez.TEST:cas", + Highlighted: "){case a.ez.PRODUCTION:return\"********************************\";case a.ez.TEST:cas", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Offset: 4016, + } + wantFindingJWT := types.SecretFinding{ + RuleID: "jwt-token", + Category: "JWT", + Title: "JWT token", + Severity: "MEDIUM", + StartLine: 3, + EndLine: 3, + Match: "jwt: ***********************************************************************************************************************************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "asd", + Highlighted: "asd", + }, + { + Number: 2, + Content: "aaaa", + Highlighted: "aaaa", + }, + { + Number: 3, + Content: "jwt: ***********************************************************************************************************************************************************", + Highlighted: "jwt: ***********************************************************************************************************************************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 4, + Content: "asda", + Highlighted: "asda", + }, + }, + }, + Offset: 14, + } + + tests := []struct { + name string + configPath string + inputFilePath string + want types.Secret + }{ + { + name: "find match", + configPath: filepath.Join("testdata", "config.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{ + wantFinding1, + wantFinding2, + }, + }, + }, + { + name: "find aws secrets", + configPath: filepath.Join("testdata", "config.yaml"), + inputFilePath: filepath.Join("testdata", "aws-secrets.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "aws-secrets.txt"), + Findings: []types.SecretFinding{ + wantFinding5, + wantFinding10, + wantFinding9, + }, + }, + }, + { + name: "find secrets in CRLF line endings file", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "crlf-line-endings.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "crlf-line-endings.txt"), + Findings: []types.SecretFinding{ + { + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Title: "AWS Access Key ID", + Severity: "CRITICAL", + StartLine: 2, + EndLine: 2, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "This is a test file with CRLF line endings.", + IsCause: false, + Highlighted: "This is a test file with CRLF line endings.", + FirstCause: false, + LastCause: false, + }, + { + Number: 2, + Content: "AWS Access Key ID: ********************", + IsCause: true, + Highlighted: "AWS Access Key ID: ********************", + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "This line has no secrets.", + IsCause: false, + Highlighted: "This line has no secrets.", + FirstCause: false, + LastCause: false, + }, + }, + }, + Match: "AWS Access Key ID: ********************", + Offset: 63, + }, + { + RuleID: "github-pat", + Category: secret.CategoryGitHub, + Title: "GitHub Personal Access Token", + Severity: "CRITICAL", + StartLine: 4, + EndLine: 4, + Code: types.Code{ + Lines: []types.Line{ + { + Number: 2, + Content: "AWS Access Key ID: ********************", + IsCause: false, + Highlighted: "AWS Access Key ID: ********************", + FirstCause: false, + LastCause: false, + }, + { + Number: 3, + Content: "This line has no secrets.", + IsCause: false, + Highlighted: "This line has no secrets.", + FirstCause: false, + LastCause: false, + }, + { + Number: 4, + Content: "GitHub PAT: ****************************************12", + IsCause: true, + Highlighted: "GitHub PAT: ****************************************12", + FirstCause: true, + LastCause: true, + }, + { + Number: 5, + Content: "End of file.", + IsCause: false, + Highlighted: "End of file.", + FirstCause: false, + LastCause: false, + }, + }, + }, + Match: "GitHub PAT: ****************************************12", + Offset: 122, + }, + }, + }, + }, + { + name: "find Asymmetric Private Key secrets", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "asymmetric-private-secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "asymmetric-private-secret.txt"), + Findings: []types.SecretFinding{wantFindingAsymmetricPrivateKey}, + }, + }, + { + name: "find Alibaba AccessKey ID txt", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: "testdata/alibaba-access-key-id.txt", + want: types.Secret{ + FilePath: "testdata/alibaba-access-key-id.txt", + Findings: []types.SecretFinding{wantFindingAlibabaAccessKeyId}, + }, + }, + { + name: "find Asymmetric Private Key secrets json", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "asymmetric-private-secret.json"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "asymmetric-private-secret.json"), + Findings: []types.SecretFinding{wantFindingAsymmetricPrivateKeyJson}, + }, + }, + { + name: "find Docker registry credentials", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "docker-secrets.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "docker-secrets.txt"), + Findings: []types.SecretFinding{ + wantFindingDockerKey1, + wantFindingDockerKey2, + }, + }, + }, + { + name: "find Hugging face secret", + configPath: filepath.Join("testdata", "config.yaml"), + inputFilePath: filepath.Join("testdata", "hugging-face-secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "hugging-face-secret.txt"), + Findings: []types.SecretFinding{wantFindingHuggingFace}, + }, + }, + { + name: "find grafana secret", + configPath: filepath.Join("testdata", "config.yaml"), + inputFilePath: filepath.Join("testdata", "grafana-env.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "grafana-env.txt"), + Findings: []types.SecretFinding{ + wantFindingGrafanaUnquoted, + wantFindingGrafanaQuoted, + }, + }, + }, + { + name: "find JWT token", + configPath: filepath.Join("testdata", "config.yaml"), + inputFilePath: filepath.Join("testdata", "jwt-secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "jwt-secret.txt"), + Findings: []types.SecretFinding{wantFindingJWT}, + }, + }, + { + name: "find Symfony default secret", + configPath: filepath.Join("testdata", "config.yaml"), + inputFilePath: filepath.Join("testdata", "symfony-secret.yaml"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "symfony-secret.yaml"), + Findings: []types.SecretFinding{wantFindingSymfonySecret}, + }, + }, + { + name: "find Private Packagist tokens", + configPath: filepath.Join("testdata", "config.yaml"), + inputFilePath: filepath.Join("testdata", "private-packagist.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "private-packagist.txt"), + Findings: []types.SecretFinding{ + wantFindingPrivatePackagistOrgReadToken, + wantFindingPrivatePackagistOrgUpdateToken, + wantFindingPrivatePackagistUserToken, + }, + }, + }, + { + name: "include when keyword found", + configPath: filepath.Join("testdata", "config-happy-keywords.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{ + wantFinding1, + wantFinding2, + }, + }, + }, + { + name: "exclude when no keyword found", + configPath: filepath.Join("testdata", "config-sad-keywords.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{}, + }, + { + name: "should ignore .md files by default", + configPath: filepath.Join("testdata", "config.yaml"), + inputFilePath: filepath.Join("testdata", "secret.md"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.md"), + }, + }, + { + name: "should disable .md allow rule", + configPath: filepath.Join("testdata", "config-disable-allow-rule-md.yaml"), + inputFilePath: filepath.Join("testdata", "secret.md"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.md"), + Findings: []types.SecretFinding{ + wantFinding1, + wantFinding2, + }, + }, + }, + { + name: "should find ghp builtin secret", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "builtin-rule-secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "builtin-rule-secret.txt"), + Findings: []types.SecretFinding{ + wantFinding5a, + wantFinding6, + }, + }, + }, + { + name: "should find GitHub Personal Access Token (classic)", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: "testdata/github-token.txt", + want: types.Secret{ + FilePath: "testdata/github-token.txt", + Findings: []types.SecretFinding{wantFindingGitHubPAT}, + }, + }, + { + name: "should enable github-pat builtin rule, but disable aws-access-key-id rule", + configPath: filepath.Join("testdata", "config-enable-ghp.yaml"), + inputFilePath: filepath.Join("testdata", "builtin-rule-secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "builtin-rule-secret.txt"), + Findings: []types.SecretFinding{wantFindingGHButDisableAWS}, + }, + }, + { + name: "should disable github-pat builtin rule", + configPath: filepath.Join("testdata", "config-disable-ghp.yaml"), + inputFilePath: filepath.Join("testdata", "builtin-rule-secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "builtin-rule-secret.txt"), + Findings: []types.SecretFinding{wantFindingPATDisabled}, + }, + }, + { + name: "should disable custom rule", + configPath: filepath.Join("testdata", "config-disable-rule1.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{}, + }, + { + name: "allow-rule path", + configPath: filepath.Join("testdata", "allow-path.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{}, + }, + { + name: "allow-rule regex inside group", + configPath: filepath.Join("testdata", "allow-regex.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{wantFinding1}, + }, + }, + { + name: "allow-rule regex outside group", + configPath: filepath.Join("testdata", "allow-regex-outside-group.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{}, + }, + { + name: "exclude-block regexes", + configPath: filepath.Join("testdata", "exclude-block.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{wantFindingRegexDisabled}, + }, + }, + { + name: "skip examples file", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "example-secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "example-secret.txt"), + }, + }, + { + name: "global allow-rule path", + configPath: filepath.Join("testdata", "global-allow-path.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: nil, + }, + }, + { + name: "global allow-rule regex", + configPath: filepath.Join("testdata", "global-allow-regex.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{wantFinding1}, + }, + }, + { + name: "global exclude-block regexes", + configPath: filepath.Join("testdata", "global-exclude-block.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{wantFindingRegexDisabled}, + }, + }, + { + name: "multiple secret groups", + configPath: filepath.Join("testdata", "multiple-secret-groups.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{ + wantFindingMultipleGroupsUsername, + wantFindingMultipleGroupsPassword, + }, + }, + }, + { + name: "truncate long line", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "long-line-secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "long-line-secret.txt"), + Findings: []types.SecretFinding{wantFinding7}, + }, + }, + { + name: "add unknown severity when rule has no severity", + configPath: filepath.Join("testdata", "config-without-severity.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{wantFinding8}, + }, + }, + { + name: "add unknown severity when rule has no severity", + configPath: filepath.Join("testdata", "config-with-incorrect-severity.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{wantFinding8}, + }, + }, + { + name: "update severity if rule severity is not in uppercase", + configPath: filepath.Join("testdata", "config-with-non-uppercase-severity.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{wantFinding8}, + }, + }, + { + name: "use unknown severity when rule has incorrect severity", + configPath: filepath.Join("testdata", "config-with-incorrect-severity.yaml"), + inputFilePath: filepath.Join("testdata", "secret.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "secret.txt"), + Findings: []types.SecretFinding{wantFinding8}, + }, + }, + { + name: "invalid aws secrets", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "invalid-aws-secrets.txt"), + want: types.Secret{}, + }, + { + name: "secret inside another word", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "wrapped-secrets.txt"), + want: types.Secret{}, + }, + { + name: "sensitive secret inside another word", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "wrapped-secrets-sensitive.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "wrapped-secrets-sensitive.txt"), + Findings: []types.SecretFinding{ + wantFindingMyAwsAccessKey, + wantFindingMyGitHubPAT, + }, + }, + }, + { + name: "asymmetric file", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: "testdata/asymmetric-private-key.txt", + want: types.Secret{ + FilePath: "testdata/asymmetric-private-key.txt", + Findings: []types.SecretFinding{ + wantFindingAsymmSecretKey, + wantFindingMinimumAsymmSecretKey, + }, + }, + }, + { + name: "begin/end line symbols without multi-line mode", + configPath: filepath.Join("testdata", "multi-line-off.yaml"), + inputFilePath: "testdata/multi-line.txt", + want: types.Secret{}, + }, + { + name: "begin/end line symbols with multi-line mode", + configPath: filepath.Join("testdata", "multi-line-on.yaml"), + inputFilePath: "testdata/multi-line.txt", + want: types.Secret{ + FilePath: "testdata/multi-line.txt", + Findings: []types.SecretFinding{wantMultiLine}, + }, + }, + { + name: "long obfuscated js code with secrets", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "obfuscated.js"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "obfuscated.js"), + Findings: []types.SecretFinding{wantFindingTokenInsideJs}, + }, + }, + { + name: "invalid UTF-8 sequences in secrets", + configPath: filepath.Join("testdata", "skip-test.yaml"), + inputFilePath: filepath.Join("testdata", "invalid-utf8.txt"), + want: types.Secret{ + FilePath: filepath.Join("testdata", "invalid-utf8.txt"), + Findings: []types.SecretFinding{ + { + RuleID: "github-pat", + Category: secret.CategoryGitHub, + Title: "GitHub Personal Access Token", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: "token=****************************************", + Code: types.Code{ + Lines: []types.Line{ + { + Number: 1, + Content: "token=****************************************", + Highlighted: "token=****************************************", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 2, + Content: "# Comment with invalid UTF-8: �", + Highlighted: "# Comment with invalid UTF-8: �", + }, + }, + }, + Offset: 6, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + content, err := os.ReadFile(tt.inputFilePath) + require.NoError(t, err) + + content = bytes.ReplaceAll(content, []byte("\r"), []byte("")) + + c, err := secret.ParseConfig(tt.configPath) + require.NoError(t, err) + + s := secret.NewScanner(c) + got := s.Scan(secret.ScanArgs{ + FilePath: tt.inputFilePath, + Content: bytes.NewReader(content), + }) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSecretScannerWithStreaming(t *testing.T) { + tests := []struct { + name string + input string + bufferSize int + overlapSize int + configPath string + want types.Secret + }{ + { + name: "secret in second chunk", + input: strings.Repeat("x", 520) + "\n" + // 520 bytes to push secret to second chunk + "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF\n" + // at offset 521 + strings.Repeat("y", 100), // padding + bufferSize: 512, + overlapSize: 128, + configPath: filepath.Join("testdata", "skip-test.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: []types.SecretFinding{ + { + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Title: "AWS Access Key ID", + Severity: "CRITICAL", + StartLine: 2, + EndLine: 2, + Match: "AWS_ACCESS_KEY_ID=********************", + Offset: 539, // 521 + 18 (position of the actual key value) + }, + }, + }, + }, + { + name: "secret spanning chunk boundary", + input: strings.Repeat("x", 480) + "\n" + // 480 bytes + "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF\n" + // at offset 481, spans chunk boundary at 512 + strings.Repeat("y", 200), // padding + bufferSize: 512, // Boundary will be in the middle of the secret + overlapSize: 128, + configPath: filepath.Join("testdata", "skip-test.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: []types.SecretFinding{ + { + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Title: "AWS Access Key ID", + Severity: "CRITICAL", + StartLine: 2, + EndLine: 2, + Match: "AWS_ACCESS_KEY_ID=********************", + Offset: 499, // 481 + 18 + }, + }, + }, + }, + { + name: "multiple secrets across chunks", + input: "GITHUB_PAT=ghp_012345678901234567890123456789abcdef\n" + // at offset 0, 52 bytes + strings.Repeat("x", 1200) + "\n" + // 1200 bytes padding + "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF\n" + // at offset 1253 + strings.Repeat("y", 1400) + "\n" + // 1400 bytes padding + "stripe_key=sk_test_51H5Z3jGXvP5CVwYOLLllllllllllllllllllllllll\n" + // at offset 2693 + strings.Repeat("z", 200), // final padding + bufferSize: 1024, + overlapSize: 256, + configPath: filepath.Join("testdata", "skip-test.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: []types.SecretFinding{ + { + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Title: "AWS Access Key ID", + Severity: "CRITICAL", + StartLine: 3, + EndLine: 3, + Match: "AWS_ACCESS_KEY_ID=********************", + Offset: 1271, // 1253 + 18 + }, + { + RuleID: "github-pat", + Category: secret.CategoryGitHub, + Title: "GitHub Personal Access Token", + Severity: "CRITICAL", + StartLine: 1, + EndLine: 1, + Match: "GITHUB_PAT=****************************************", // 40 asterisks for 40-char token + Offset: 11, // position of the token value + }, + { + RuleID: "stripe-secret-token", + Category: secret.CategoryStripe, + Title: "Stripe Secret Key", + Severity: "CRITICAL", + StartLine: 5, + EndLine: 5, + Match: "stripe_key=****************************************lllllllllll", // Stripe key pattern includes literal 'l' chars + Offset: 2704, // 2693 + 11 + }, + }, + }, + }, + { + name: "secret at exact chunk boundary", + input: strings.Repeat("x", 383) + "\n" + // 383 bytes + newline = 384 + "AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF", // Starts exactly at chunk boundary (384) + bufferSize: 384, // Boundary right after the newline + overlapSize: 96, + configPath: filepath.Join("testdata", "skip-test.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: []types.SecretFinding{ + { + RuleID: "aws-access-key-id", + Category: secret.CategoryAWS, + Title: "AWS Access Key ID", + Severity: "CRITICAL", + StartLine: 2, + EndLine: 2, + Match: "AWS_ACCESS_KEY_ID=********************", + Offset: 402, // 384 + 18 + }, + }, + }, + }, + { + name: "very small buffer and overlap", + input: strings.Repeat("x", 150) + "\n" + // 150 bytes padding + "secret=\"mysecret123\"\n" + // at offset 151 + strings.Repeat("y", 345) + "\n" + // 345 bytes padding + "secret=\"anothersecret456\"\n" + // at offset 517 + strings.Repeat("z", 150), // suffix padding + bufferSize: 128, + overlapSize: 32, + configPath: filepath.Join("testdata", "config.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: []types.SecretFinding{ + { + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "secret=\"***********\"", + Offset: 159, // 151 + 8 (position of "mysecret123") + }, + { + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 4, + EndLine: 4, + Match: "secret=\"****************\"", + Offset: 526, // 517 + 9 + }, + }, + }, + }, + { + name: "multi-line secret with small chunks", + input: strings.Repeat("x", 720) + "\n" + // 720 bytes padding + "-----BEGIN RSA PRIVATE KEY-----\n" + // at offset 721 + "MIIEpAIBAAKCAQEA1234567890abcdefghijklmnopqrstuvwxyz\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnop\n" + + "qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12345678\n" + + "-----END RSA PRIVATE KEY-----\n" + + strings.Repeat("z", 460), // suffix padding + bufferSize: 1024, // Ensure the entire key fits in one chunk + overlapSize: 256, + configPath: filepath.Join("testdata", "skip-test.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: []types.SecretFinding{ + { + RuleID: "private-key", + Category: secret.CategoryAsymmetricPrivateKey, + Title: "Asymmetric Private Key", + Severity: "HIGH", + StartLine: 3, + EndLine: 5, + Match: "****************************************************", // Multi-line secret content + Offset: 753, // 721 + 32 (after BEGIN line) + }, + }, + }, + }, + { + name: "multi-line secret exceeding overlap size (known limitation)", + input: strings.Repeat("x", 920) + "\n" + // Position key at chunk boundary (1024 - ~100 bytes for key) + "-----BEGIN RSA PRIVATE KEY-----\n" + // at offset 921 + "MIIEpAIBAAKCAQEA1234567890abcdefghijklmnopqrstuvwxyz\n" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890abcdefghijklmnop\n" + + "qrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ12345678\n" + + "-----END RSA PRIVATE KEY-----\n" + + strings.Repeat("z", 460), // suffix padding + bufferSize: 1024, + overlapSize: 100, // Too small to capture the entire key + configPath: filepath.Join("testdata", "skip-test.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: nil, // Key won't be detected as it spans beyond the overlap + }, + }, + { + name: "no secrets in any chunk", + input: strings.Repeat("this is just normal content without any secrets in it at all\n", 50), + bufferSize: 512, + overlapSize: 128, // 1/4 of buffer + configPath: filepath.Join("testdata", "config.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: nil, + }, + }, + { + name: "secret in last chunk with EOF", + input: strings.Repeat("x", 1500) + "\n" + // 1500 bytes to push secret to end + "final_secret=\"supersecretvalue\"", // at offset 1501, no newline at end + bufferSize: 640, + overlapSize: 160, // 1/4 of buffer + configPath: filepath.Join("testdata", "config.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: []types.SecretFinding{ + { + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "final_secret=\"****************\"", + Offset: 1515, // 1501 + 14 (position of "supersecretvalue") + }, + }, + }, + }, + { + name: "deduplicate findings at chunk boundaries", + input: strings.Repeat("x", 330) + "\n" + // 330 bytes + "secret=\"duplicatetest123\"\n" + // at offset 331, ends at 356 + strings.Repeat("y", 200), // padding + bufferSize: 400, // First chunk: 0-399, overlap: 300-399 + overlapSize: 100, // Secret (331-356) is within overlap region + configPath: filepath.Join("testdata", "config.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: []types.SecretFinding{ + { + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "secret=\"****************\"", + Offset: 339, // 331 + 8 (position of "duplicatetest123") + }, + }, + }, + }, + { + name: "multiple secrets on same line", + input: strings.Repeat("x", 480) + "\n" + // 480 bytes padding + "secret=\"first123\" and secret=\"second456\" on same line\n" + // at offset 481 + strings.Repeat("y", 1300), // large padding to force multiple chunks + bufferSize: 512, + overlapSize: 128, // 1/4 of buffer + configPath: filepath.Join("testdata", "config.yaml"), + want: types.Secret{ + FilePath: "test.txt", + Findings: []types.SecretFinding{ + { + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "secret=\"********\" and secret=\"*********\" on same line", // Full line match for second secret + Offset: 511, // 481 + 30 (position of "second456") + }, + { + RuleID: "rule1", + Category: "general", + Title: "Generic Rule", + Severity: "HIGH", + StartLine: 2, + EndLine: 2, + Match: "secret=\"********\" and secret=\"s", // Truncated match for first secret + Offset: 489, // 481 + 8 (position of "first123") + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Parse config + c, err := secret.ParseConfig(tt.configPath) + require.NoError(t, err) + + // Create scanner with custom buffer and overlap sizes + s := secret.NewScanner(c, + secret.WithBufferSize(tt.bufferSize), + secret.WithOverlapSize(tt.overlapSize)) + + // Scan with streaming + reader := strings.NewReader(tt.input) + got := s.Scan(secret.ScanArgs{ + FilePath: "test.txt", + Content: reader, + }) + + // Clear Code field as it's too verbose to specify in test expectations + for i := range got.Findings { + got.Findings[i].Code = types.Code{} + } + for i := range tt.want.Findings { + tt.want.Findings[i].Code = types.Code{} + } + + // Compare all findings at once + assert.Equal(t, tt.want.Findings, got.Findings, "unexpected findings") + }) + } +} diff --git a/pkg/fanal/secret/testdata/alibaba-access-key-id.txt b/pkg/fanal/secret/testdata/alibaba-access-key-id.txt new file mode 100644 index 000000000000..f7d92c446ea9 --- /dev/null +++ b/pkg/fanal/secret/testdata/alibaba-access-key-id.txt @@ -0,0 +1,6 @@ +key : LTAI1234567890ABCDEFG123asd +key = LTAI1234567890ABCDEFG123, +asdLTAI1234567890ABCDEFG123 +asDLTAI1234567890ABCDEFG123 +as1LTAI1234567890ABCDEFG123 +key : LTAI1234567890ABCDEFG123Asd \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/allow-path.yaml b/pkg/fanal/secret/testdata/allow-path.yaml new file mode 100644 index 000000000000..520b028a30a7 --- /dev/null +++ b/pkg/fanal/secret/testdata/allow-path.yaml @@ -0,0 +1,12 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret + allow-rules: + - description: skip text files + path: .*\.txt +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/allow-regex-outside-group.yaml b/pkg/fanal/secret/testdata/allow-regex-outside-group.yaml new file mode 100644 index 000000000000..25162d8254e0 --- /dev/null +++ b/pkg/fanal/secret/testdata/allow-regex-outside-group.yaml @@ -0,0 +1,12 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)line secret(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret + allow-rules: + - description: skip line + regex: line +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/allow-regex.yaml b/pkg/fanal/secret/testdata/allow-regex.yaml new file mode 100644 index 000000000000..cad82e86b0c6 --- /dev/null +++ b/pkg/fanal/secret/testdata/allow-regex.yaml @@ -0,0 +1,12 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret + allow-rules: + - description: skip other + regex: other +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/asymmetric-private-key.txt b/pkg/fanal/secret/testdata/asymmetric-private-key.txt new file mode 100644 index 000000000000..f24f1dc440f9 --- /dev/null +++ b/pkg/fanal/secret/testdata/asymmetric-private-key.txt @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAu/Nua0/1y08gkbnBfKd6VDHia8Na0ATgMQqZ4YEbi/t73g84IEPQPkLbPF3X De++JA1QzqTZbbePLsW44DbbgMX/5jj+Sh2SvGe5IXNeNwyyMox+DKQccJUPxbCxnhF/gSnF76cM BXJC63nDnGawz4g3qwU1+0sfyKG0ixFI3e3992fk6QnF49Cv5iqwjgKPIZjgfoM70r71XDKJjVTC rJdSBfyQwX2TU0CncglvJSzhhcuTeQZWldbk/BHjxINrqQIxKaG2OfBgkupPjnrImzSAUE9a/gIS REUVSamc69qqQnXER3Jmoy8HXiAQdPI+CpVVkI7FCCq4qD7fVqsNhwIDAQABAoIBAQC5707zNr1Q jk0IHR3+9agdFuSJ+08hr1Ei8vvcjN71kqqtuZyqvquKjJVamPMhRGV0QQAKDidTVV5+xPfqSBrK wBYyaXuXUr5RSMNrBjjUeOjo/PfOBaRk8/IQfoaYe3MKEotQVI+d67WsQl9zoFuWU4nO1G7c1Sry TpbPZSAS+6J7fUClUgT9pvg+EpoboXs+voeWTh9r9eracxUmlclVAdS3tP7xMv5R29EBYtjGKbF6 r0Ku/hXJjPu5Eck4/BeciEinVWn/yqSsqd5XKOUwTuLlUyAGWhJKcn/zWgaBYUvknzSmwePvW/W8 iwrEhP4GNHBEHisJHdWPtbCDdOVxAoGBAPqyR/9ocwZ3GhHz3dI53Z6UjKUPtRnxJb19ZS8UVN57 P7yCXpH+L6KhIxo9yx0D5Z4bdNSYTyjl6eFnv0FZA3UXsM2tyY+Ylih1LOqcttehJkK2JaFmuefx d6bcpPJG00EKFDZoTH5bbnrB3uGKUVJ5TMFlUbOgkATJL652VTNTAoGBAL/tVWwlO5ET80BSheJ/ V88rSF4AxK48ZNt5EG7RHph46KukwywPUnWRoFLxRtVP/udZf9Qq164IPGgDrn4E6VTpZwmp7HDv 6P8sSLwJj/YW3y9c57lA4SMoowO2ik09fbBJVvWLeev4n6taDNwgCZ4fuLUtMf/mUU3r80okeUp9 AoGAIySIyTn4HejmQ6v+5XBtK8TBLoZUKc3PL4/7di0QdJusZJ2V6jtKrC6QgCY3adrY/l/08bRk LGSGc62aduume2yVwU9iWPnX2tYKNN1BGFsjxOhJwCVpXCVSU5bMnJXnGU/zY2kdh/0DMLwqpU1B dyE/7EBqwpZ4eeNGBtvZt7cCgYB8jaZJJ6SPkzXiwWtXwTKYJMuzDaaWOGVvtRKACEBlzNmaQrPS jSMDX31/Nku0tVSEiSWW6DLOI1QoYHNGHyPZ0hrnP5pM9LTtnKybM0109ATlNNLA+6Tf70hTaYw5 cjV2STIg6eI2zEO6rRb5Z+U18/onwevX2X1cJ0rdC+yW9QKBgH0xSLUGFZwFDCPE+jKGgqJQme5Q 8oxHs1CTkV4SxeLFNldA9c6uESMppSUwO7wx+NaFAJw9m2Q9Ifmo57pncAx2iVXOA9Jxaa7YFIsL +vKftqLPCPbAPPxkaqQi0Ico/1fzD10znRy66aosPBrbleduiynubgk+GVm9y/R6bDYhR +-----END RSA PRIVATE KEY----- +#5 line +-----BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY----- + +## Theoretically Asymmetric Private Key of minimum length (RSA 128 bits) +-----BEGIN PRIVATE KEY-----4c6f4a5cb2d395f828384c2ab29d1f55-----END PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/asymmetric-private-secret.json b/pkg/fanal/secret/testdata/asymmetric-private-secret.json new file mode 100644 index 000000000000..9bb6975245d4 --- /dev/null +++ b/pkg/fanal/secret/testdata/asymmetric-private-secret.json @@ -0,0 +1 @@ +{"key": "-----BEGIN RSA PRIVATE KEY-----\n7eUHc9npsdql24xsVK8huo9VKPu/mrDStv8JYRWP4cSUXWWBx5oGpPBP7uaMgQCx\nuks3gKrUL/3LFndE0egKslgGSkoyYb3iHP1X3IFsOtTQz6ZPmzc=\n-----END RSA PRIVATE KEY-----\n"} \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/asymmetric-private-secret.txt b/pkg/fanal/secret/testdata/asymmetric-private-secret.txt new file mode 100644 index 000000000000..4ad9b5cc29e0 --- /dev/null +++ b/pkg/fanal/secret/testdata/asymmetric-private-secret.txt @@ -0,0 +1,5 @@ +-----BEGIN RSA PRIVATE KEY----- +YObVAoGAGsWtK3K2Q4OSf6Z1az3AuDfo8xj9/UBuBQe1Op9RFTXps84foAqEgFwj +7eUHc9npsdql24xsVK8huo9VKPu/mrDStv8JYRWP4cSUXWWBx5oGpPBP7uaMgQCx +uks3gKrUL/3LFndE0egKslgGSkoyYb3iHP1X3IFsOtTQz6ZPmzc= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/aws-secrets.txt b/pkg/fanal/secret/testdata/aws-secrets.txt new file mode 100644 index 000000000000..737708284303 --- /dev/null +++ b/pkg/fanal/secret/testdata/aws-secrets.txt @@ -0,0 +1,5 @@ +'AWS_secret_KEY'="12ASD34qwe56CXZ78tyH10Tna543VBokN85RHCas" +AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF +"aws_account_ID":'1234-5678-9123' +AWS_example=AKIAIOSFODNN7EXAMPLE + "created_by": "ENV aws_sec_key "KEYKEYKEYKEYKEYKEYKEYKEYKEYKEYKEYKEYKEYK", \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/builtin-rule-secret.txt b/pkg/fanal/secret/testdata/builtin-rule-secret.txt new file mode 100644 index 000000000000..a8172ee8628d --- /dev/null +++ b/pkg/fanal/secret/testdata/builtin-rule-secret.txt @@ -0,0 +1,2 @@ +GITHUB_PAT=ghp_012345678901234567890123456789abcdef +AWS_ACCESS_KEY_ID=AKIA0123456789ABCDEF \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/config-disable-allow-rule-md.yaml b/pkg/fanal/secret/testdata/config-disable-allow-rule-md.yaml new file mode 100644 index 000000000000..df54e72b1704 --- /dev/null +++ b/pkg/fanal/secret/testdata/config-disable-allow-rule-md.yaml @@ -0,0 +1,12 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret +disable-allow-rules: + - markdown + - tests + + diff --git a/pkg/fanal/secret/testdata/config-disable-ghp.yaml b/pkg/fanal/secret/testdata/config-disable-ghp.yaml new file mode 100644 index 000000000000..cd3c5009ced1 --- /dev/null +++ b/pkg/fanal/secret/testdata/config-disable-ghp.yaml @@ -0,0 +1,5 @@ +disable-rules: + - github-pat + +disable-allow-rules: + - tests diff --git a/pkg/fanal/secret/testdata/config-disable-rule1.yaml b/pkg/fanal/secret/testdata/config-disable-rule1.yaml new file mode 100644 index 000000000000..d1287510229e --- /dev/null +++ b/pkg/fanal/secret/testdata/config-disable-rule1.yaml @@ -0,0 +1,12 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret +disable-rules: + - rule1 + +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/config-enable-ghp.yaml b/pkg/fanal/secret/testdata/config-enable-ghp.yaml new file mode 100644 index 000000000000..ad2390e9dafe --- /dev/null +++ b/pkg/fanal/secret/testdata/config-enable-ghp.yaml @@ -0,0 +1,5 @@ +enable-builtin-rules: + - github-pat + +disable-allow-rules: + - tests diff --git a/pkg/fanal/secret/testdata/config-happy-keywords.yaml b/pkg/fanal/secret/testdata/config-happy-keywords.yaml new file mode 100644 index 000000000000..b430410fb801 --- /dev/null +++ b/pkg/fanal/secret/testdata/config-happy-keywords.yaml @@ -0,0 +1,11 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret + keywords: [secret] + +disable-allow-rules: + - tests diff --git a/pkg/fanal/secret/testdata/config-sad-keywords.yaml b/pkg/fanal/secret/testdata/config-sad-keywords.yaml new file mode 100644 index 000000000000..d0320e910f1d --- /dev/null +++ b/pkg/fanal/secret/testdata/config-sad-keywords.yaml @@ -0,0 +1,11 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret + keywords: [bla] + +disable-allow-rules: + - tests diff --git a/pkg/fanal/secret/testdata/config-with-incorrect-severity.yaml b/pkg/fanal/secret/testdata/config-with-incorrect-severity.yaml new file mode 100644 index 000000000000..faf80f8424fa --- /dev/null +++ b/pkg/fanal/secret/testdata/config-with-incorrect-severity.yaml @@ -0,0 +1,9 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: bad + regex: (?i)(?P(secret))(=|:).{0,5}['"](?Psomevalue)['"] + secret-group-name: secret +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/config-with-non-uppercase-severity.yaml b/pkg/fanal/secret/testdata/config-with-non-uppercase-severity.yaml new file mode 100644 index 000000000000..666536a8537f --- /dev/null +++ b/pkg/fanal/secret/testdata/config-with-non-uppercase-severity.yaml @@ -0,0 +1,9 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: uNknown + regex: (?i)(?P(secret))(=|:).{0,5}['"](?Psomevalue)['"] + secret-group-name: secret +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/config-without-severity.yaml b/pkg/fanal/secret/testdata/config-without-severity.yaml new file mode 100644 index 000000000000..38443f858442 --- /dev/null +++ b/pkg/fanal/secret/testdata/config-without-severity.yaml @@ -0,0 +1,8 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + regex: (?i)(?P(secret))(=|:).{0,5}['"](?Psomevalue)['"] + secret-group-name: secret +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/config.yaml b/pkg/fanal/secret/testdata/config.yaml new file mode 100644 index 000000000000..d14f45b97512 --- /dev/null +++ b/pkg/fanal/secret/testdata/config.yaml @@ -0,0 +1,10 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret + +disable-allow-rules: + - tests diff --git a/pkg/fanal/secret/testdata/crlf-line-endings.txt b/pkg/fanal/secret/testdata/crlf-line-endings.txt new file mode 100644 index 000000000000..040819c57685 --- /dev/null +++ b/pkg/fanal/secret/testdata/crlf-line-endings.txt @@ -0,0 +1,5 @@ +This is a test file with CRLF line endings. +AWS Access Key ID: AKIA0123456789ABCDEF +This line has no secrets. +GitHub PAT: ghp_1234567890abcdefghijklmnopqrstuvwxyz12 +End of file. \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/docker-secrets.txt b/pkg/fanal/secret/testdata/docker-secrets.txt new file mode 100644 index 000000000000..9bd5b12cfd38 --- /dev/null +++ b/pkg/fanal/secret/testdata/docker-secrets.txt @@ -0,0 +1,4 @@ +data1: + .dockerconfigjson: eyE1x2a3MpLe +data2: + .dockercfg: ewE1x2a3MpLe \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/example-secret.txt b/pkg/fanal/secret/testdata/example-secret.txt new file mode 100644 index 000000000000..05dd97aa1046 --- /dev/null +++ b/pkg/fanal/secret/testdata/example-secret.txt @@ -0,0 +1 @@ +'AWS_secret_KEY'="12ASD34qwe56CXZ78tyH10Tna543VBokN85RHCas" \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/exclude-block.yaml b/pkg/fanal/secret/testdata/exclude-block.yaml new file mode 100644 index 000000000000..26ba2ac28e9d --- /dev/null +++ b/pkg/fanal/secret/testdata/exclude-block.yaml @@ -0,0 +1,13 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret + exclude-block: + description: exclude blocks + regexes: + - --- ignore block start ---(.|\s)*--- ignore block stop --- +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/github-token.txt b/pkg/fanal/secret/testdata/github-token.txt new file mode 100644 index 000000000000..6c7026200bbb --- /dev/null +++ b/pkg/fanal/secret/testdata/github-token.txt @@ -0,0 +1 @@ +GITHUB_TOKEN=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 \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/global-allow-path.yaml b/pkg/fanal/secret/testdata/global-allow-path.yaml new file mode 100644 index 000000000000..a5e630ae8577 --- /dev/null +++ b/pkg/fanal/secret/testdata/global-allow-path.yaml @@ -0,0 +1,10 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret +allow-rules: + - description: skip text files + path: .*\.txt \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/global-allow-regex.yaml b/pkg/fanal/secret/testdata/global-allow-regex.yaml new file mode 100644 index 000000000000..dd44e2460d37 --- /dev/null +++ b/pkg/fanal/secret/testdata/global-allow-regex.yaml @@ -0,0 +1,12 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret +allow-rules: + - description: skip other + regex: other +disable-allow-rules: + - tests diff --git a/pkg/fanal/secret/testdata/global-exclude-block.yaml b/pkg/fanal/secret/testdata/global-exclude-block.yaml new file mode 100644 index 000000000000..0c9e5f09c914 --- /dev/null +++ b/pkg/fanal/secret/testdata/global-exclude-block.yaml @@ -0,0 +1,13 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)(?P(secret))(=|:).{0,5}['"](?P[0-9a-zA-Z\-_=]{8,64})['"] + secret-group-name: secret +exclude-block: + description: exclude blocks + regexes: + - --- ignore block start ---(.|\s)*--- ignore block stop --- +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/grafana-env.txt b/pkg/fanal/secret/testdata/grafana-env.txt new file mode 100644 index 000000000000..ede29f880a6b --- /dev/null +++ b/pkg/fanal/secret/testdata/grafana-env.txt @@ -0,0 +1,2 @@ +GRAFANA_TOKEN="eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbkT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk" +GRAFANA_TOKEN=eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbkT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk diff --git a/pkg/fanal/secret/testdata/hugging-face-secret.txt b/pkg/fanal/secret/testdata/hugging-face-secret.txt new file mode 100644 index 000000000000..d7fbd162d3b4 --- /dev/null +++ b/pkg/fanal/secret/testdata/hugging-face-secret.txt @@ -0,0 +1,3 @@ +HF_example_token: hf_Testpoiqazwsxedcrfvtgbyhn12345ujmik6789 + +_call_hf_processorz1DeepseekOCRMultiModalProcessor._call_hf_processor \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/invalid-aws-secrets.txt b/pkg/fanal/secret/testdata/invalid-aws-secrets.txt new file mode 100644 index 000000000000..4549e696d792 --- /dev/null +++ b/pkg/fanal/secret/testdata/invalid-aws-secrets.txt @@ -0,0 +1,6 @@ +base64: +YXdzLWFjY2AIPA1rZXktaWQgdGVzdCBzdHJpbmc= +web.config file in windows containers: +publicKey="F1645C4C0C93C1AB99285D622CAA652C1DFAD63D745D6F2DE5F17E5EAF0FC4963D261C8A12436518206DC093344D5AD293" +length is too long: +"AWS_key":HGM724ngha9785NGKbbar6jk76mnLL80BHJnabyhdngha9785NGKbb6jk76mnLL8 \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/invalid-utf8.txt b/pkg/fanal/secret/testdata/invalid-utf8.txt new file mode 100644 index 000000000000..9f3f600af2ef --- /dev/null +++ b/pkg/fanal/secret/testdata/invalid-utf8.txt @@ -0,0 +1,3 @@ +token=ghp_abcdef1234567890ABCDEF1234567890abcd +# Comment with invalid UTF-8: ���� +token2=ghp_1234567890abcdef1234567890ABCDEF��abcd diff --git a/pkg/fanal/secret/testdata/jwt-secret.txt b/pkg/fanal/secret/testdata/jwt-secret.txt new file mode 100644 index 000000000000..b23e942b3d98 --- /dev/null +++ b/pkg/fanal/secret/testdata/jwt-secret.txt @@ -0,0 +1,4 @@ +asd +aaaa +jwt: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c +asda \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/long-line-secret.txt b/pkg/fanal/secret/testdata/long-line-secret.txt new file mode 100644 index 000000000000..5349ada402d4 --- /dev/null +++ b/pkg/fanal/secret/testdata/long-line-secret.txt @@ -0,0 +1 @@ +aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa GITHUB_PAT=ghp_012345678901234567890123456789abcdef bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/multi-line-off.yaml b/pkg/fanal/secret/testdata/multi-line-off.yaml new file mode 100644 index 000000000000..9cde681d92d0 --- /dev/null +++ b/pkg/fanal/secret/testdata/multi-line-off.yaml @@ -0,0 +1,8 @@ +rules: + - id: multi-line-sect + category: general + title: Generic Rule + severity: HIGH + regex: '^multi-line: \d+$' +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/multi-line-on.yaml b/pkg/fanal/secret/testdata/multi-line-on.yaml new file mode 100644 index 000000000000..cf27a9faebe4 --- /dev/null +++ b/pkg/fanal/secret/testdata/multi-line-on.yaml @@ -0,0 +1,8 @@ +rules: + - id: multi-line-secret + category: general + title: Generic Rule + severity: HIGH + regex: '(?m)^multi-line: \d+$' +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/multi-line.txt b/pkg/fanal/secret/testdata/multi-line.txt new file mode 100644 index 000000000000..0b710dd6d4bd --- /dev/null +++ b/pkg/fanal/secret/testdata/multi-line.txt @@ -0,0 +1,3 @@ +123 +multi-line: 123 +123 \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/multiple-secret-groups.yaml b/pkg/fanal/secret/testdata/multiple-secret-groups.yaml new file mode 100644 index 000000000000..bd246ae24c1d --- /dev/null +++ b/pkg/fanal/secret/testdata/multiple-secret-groups.yaml @@ -0,0 +1,9 @@ +rules: + - id: rule1 + category: general + title: Generic Rule + severity: HIGH + regex: (?i)credentials:\s*\{\s*user:\s*['"](?P[0-9a-zA-Z\-_=]{8,64})['"]\s*password:\s*['"](?P[0-9a-zA-Z\-_=]{8,64})['"]\s*\} + secret-group-name: secret +disable-allow-rules: + - tests \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/obfuscated.js b/pkg/fanal/secret/testdata/obfuscated.js new file mode 100644 index 000000000000..f3e1c53be227 --- /dev/null +++ b/pkg/fanal/secret/testdata/obfuscated.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkmattermost_webapp=self.webpackChunkmattermost_webapp||[]).push([[8055],{59713:(e,t,n)=>{n.d(t,{$d:()=>b,DF:()=>G,Du:()=>B,GA:()=>F,Is:()=>M,JJ:()=>J,K6:()=>re,KB:()=>f,KO:()=>x,Md:()=>ae,NB:()=>ce,O$:()=>E,OT:()=>ee,QQ:()=>oe,SP:()=>K,Sm:()=>se,T8:()=>Y,UV:()=>pe,W3:()=>C,X$:()=>X,_9:()=>de,a6:()=>Q,d1:()=>ne,d5:()=>$,dA:()=>te,gY:()=>ie,go:()=>T,h5:()=>z,iB:,n(61418);var a=n(17554),s=n(98644),i=n(80139),r=n(23712);function o(e){return{type:r.MF.NEEDS_LOGGED_IN_LIMIT_REACHED_CHECK,data:e}}function l(e).func,onInputChange:s().func,onInputBlur:s().func,buttonFooter:s().element},M.defaultProps={className:""};const T=M},30736:(e,t,n)=>{n.d(t,{Z:()=>u});var a=n(23615),s=n.n(a),i=(n(48410),n(84390)),r=n.n(i),o=n(72060),l=n(80623),c=n(83398);const d=[{code:"AL",name:"Alabama"},{code:"AK",name:"Alaska"},{code:"AZ",name:"Arizona"},{code:"AR",name:"Arkansas"},{code:"CA",name:"California"},{code:"CO",name:"Colorado"},{code:"CT",name:"Connecticut"},{code:"DE",name:"Delaware"},{code:"DC",name:"District of Columbia"},{code:"FL",name:"Florida"},{code:"GA",name:"Georgia"},{code:"HI",name:"Hawaii"},{code:"ID",name:"Idaho"},{code:"IL",name:"Illinois"},{code:"IN",name:"Indiana"},{code:"IA",name:"Iowa"},{code:"KS",name:"Kansas"},{code:"KY",name:"Kentucky"},{code:"LA",name:"Louisiana"},{code:"ME",name:"Maine"},{code:"MD",name:"Maryland"},{code:"MA",name:"Massachusetts"},{code:"MI",name:"Michigan"},{code:"MN",name:"Minnesota"},{code:"MS",name:"Mississippi"},{code:"MO",name:"Missouri"},{code:"MT",name:"Montana"},{code:"NE",name:"Nebraska"},{code:"NV",name:"Nevada"},{code:"NH",name:"New Hampshire"},{code:"NJ",name:"New Jersey"},{code:"NM",name:"New Mexico"},{code:"NY",name:"New York"},{code:"NC",name:"North Carolina"},{code:"ND",name:"North Dakota"},{code:"OH",name:"Ohio"},{code:"OK",name:"Oklahoma"},{code:"OR",name:"Oregon"},{code:"PA",name:"Pennsylvania"},{code:"PR",name:"Puerto Rico"},{code:"RI",name:"Rhode Island"},{code:"SC",name:"South Carolina"},{code:"SD",name:"South Dakota"},{code:"TN",name:"Tennessee"},{code:"TX",name:"Texas"},{code:"UT",name:"Utah"},{code:"VT",name:"Vermont"},{code:"VA",name:"Virginia"},{code:"WA",name:"Washington"},{code:"WV",name:"West Virginia"},{code:"WI",name:"Wisconsin"},{code:"WY",name:"Wyoming"}],m=[{code:"AB",name:"Alberta"},{code:"BC",name:"British Columbia"},{code:"MB",name:"Manitoba"},{code:"NB",name:"New Brunswick"},{code:"NL",name:"Newfoundland and Labrador"},{code:"NT",name:"Northwest Territories"},{code:"NS",name:"Nova Scotia"},{code:"NU",name:"Nunavut"},{code:"ON",name:"Ontario"},{code:"PE",name:"Prince Edward Island"},{code:"QC",name:"Quebec"},{code:"SK",name:"Saskatchewan"},{code:"YT",name:"Yukon Territory"}];function u(e){const{formatMessage:t}=(0,o.useIntl)(),n=t=>{e.onChange(t.value)};let a=[];if("US"===e.country?a=d:"CA"===e.country&&(a=m),a.length>0){const s={};return e.testId&&(s.testId=e.testId),r().createElement(l.Z,Object.assign({},s,{onChange:n,value:e.state?{value:e.state,label:e.state}:void 0,options:a.map((e=>({value:e.code,label:e.name}))),legend:t({id:"admin.billing.subscription.stateprovince",defaultMessage:"State/Province"}),placeholder:t({id:"admin.billing.subscription.stateprovince",defaultMessage:"State/Province"}),name:"country_dropdown"}))}return r().createElement(c.Z,{name:"state",type:"text",value:e.state,onChange:t=>{e.onChange(t.target.value)},onBlur:e.onBlur,placeholder:t({id:"admin.billing.subscription.stateprovince",defaultMessage:"State/Province"}),required:!0})}u.propTypes={country:s().string.isRequired,state:s().string.isRequired,testId:s().string,onChange:s().func.isRequired,onBlur:s().func}},35154:(e,t,n)=>{n.d(t,{li:()=>r,tt:()=>l,wW:()=>o}),n(61418);var a=n(2507);function s(e){return e}function i(e){return async(e,t,n)=>({setupIntent:{id:"testid",status:"succeeded"}})}const r=e=>e?i:s,o="https://fonts.googleapis.com/css?family=Open+Sans:400,400i,600,600i&display=swap",l=e=>{switch(e.entities.general.config.ServiceEnvironment){case a.ez.PRODUCTION:return"pk_live_xXX1xXXXx1xXxXxxx11x1XXX";case a.ez.TEST:case a.ez.DEV:return""}return""}},21500:(e,t,n)=>{n.r(t),n.d(t,{default:()=>h});var a=n(23615),s=n.n(a),i=(n(61418),n(14078),n(48410),n(92189),n(66726)),r=n.n(i),o=n(84390),l=n.n(o),c=n(47407),d=n(83765),m=n(26337),u=n(41894);function p(e,t,n){return(t=function(e){var t=function(e,t){if("object"!=typeof e||null===e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var a=n.call(e,"string");if("object"!=typeof a)return a;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(e)}(e);return"symbol"==typeof t?t:String(t)}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}class h extends l().PureComponent{constructor(e){super(e),p(this,"parentNode",null),p(this,"pdfCanvasRef",{}),p(this,"downloadFile",(e=>{const t=this.props.fileInfo.link||(0,c.gN)(this.props.fileInfo.id);e.preventDefault(),window.location.href=t})),p(this,"isInViewport",(e=>{var t,n,a,s;const i=e.getBoundingClientRect(),r=null!==(t=null===(n=this.container.current)||void 0===n?void 0:n.scrollTop)&&void 0!==t?t:0,o=r+(null!==(a=null===(s=this.parentNode)||void 0===s?void 0:s.clientHeight)&&void 0!==a?a:0);return i.top>=r&&i.top<=o||i.bottom>=r&&i.bottom<=o||i.top<=r&&i.bottom>=o})),p(this,"renderPDFPage",(async e=>{const t=this.pdfCanvasRef["pdfCanvasRef-".concat(e)].current;if(!t)return;if(e>=3&&!this.isInViewport(t))return;if(this.pdfPagesRendered[e])return;const n=await this.loadPage(this.state.pdf,e),a=t.getContext("2d"),s=n.getViewport({scale:this.props.scale});t.height=s.height,t.width=s.width;const i={canvasContext:a,viewport:s};await n.render(i).promise,this.pdfPagesRendered[e]=!0})),p(this,"getPdfDocument",(async()=>{try{const e=await Promise.all([n.e(7803),n.e(9707)]).then(n.t.bind(n,47803,23)),t=await n.e(5456).then(n.t.bind(n,65456,23));e.GlobalWorkerOptions.workerSrc=t;const a=await e.getDocument({url:this.props.fileUrl,cMapUrl:(0,u.fO)()+"/static/cmaps/",cMapPacked:!0}).promise;this.onDocumentLoad(a)}catch(e){this.onDocumentLoadError(e)}})),p(this,"onDocumentLoad",(e=>{this.setState({pdf:e,numPages:e.numPages});for(let t=0;t{console.log("Unable to load PDF preview: "+e),this.setState({loading:!1,success:!1})})),p(this,"loadPage",(async(e,t)=>{if(this.state.pdfPagesLoaded[t])return this.state.pdfPages[t];const n=await e.getPage(t+1),a=Object.assign({},this.state.pdfPages);a[t]=n;) \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/private-packagist.txt b/pkg/fanal/secret/testdata/private-packagist.txt new file mode 100644 index 000000000000..cfcdb2169a28 --- /dev/null +++ b/pkg/fanal/secret/testdata/private-packagist.txt @@ -0,0 +1,3 @@ +ORG_READ_TOKEN=packagist_ort_6675e11a686c692f3f2e3b6ce528c3d122d22d912ea69a20713cdf51714ba710ad74 +ORG_WRITE_TOKEN=packagist_out_d63BD7be741c67ca810f924225b525fa5d20e6e1b316c8bfc0a1b33c68e4861bd5a4 +USER_TOKEN=packagist_uut_02f17e5917451dcdcc2995157e08cac2976a0373097b95d7021ba7a6844437973421 diff --git a/pkg/fanal/secret/testdata/secret.md b/pkg/fanal/secret/testdata/secret.md new file mode 100644 index 000000000000..f0bad73a2d29 --- /dev/null +++ b/pkg/fanal/secret/testdata/secret.md @@ -0,0 +1,5 @@ +--- ignore block start --- +generic secret line secret="somevalue" +--- ignore block stop --- +secret="othervalue" +credentials: { user: "username" password: "123456789" } \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/secret.txt b/pkg/fanal/secret/testdata/secret.txt new file mode 100644 index 000000000000..f0bad73a2d29 --- /dev/null +++ b/pkg/fanal/secret/testdata/secret.txt @@ -0,0 +1,5 @@ +--- ignore block start --- +generic secret line secret="somevalue" +--- ignore block stop --- +secret="othervalue" +credentials: { user: "username" password: "123456789" } \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/skip-test.yaml b/pkg/fanal/secret/testdata/skip-test.yaml new file mode 100644 index 000000000000..deb523b387fb --- /dev/null +++ b/pkg/fanal/secret/testdata/skip-test.yaml @@ -0,0 +1,2 @@ +disable-allow-rules: + - tests diff --git a/pkg/fanal/secret/testdata/symfony-secret.yaml b/pkg/fanal/secret/testdata/symfony-secret.yaml new file mode 100644 index 000000000000..a9a1e58f542b --- /dev/null +++ b/pkg/fanal/secret/testdata/symfony-secret.yaml @@ -0,0 +1,2 @@ +parameters: + secret: ThisTokenIsNotSoSecretChangeIt \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/wrapped-secrets-sensitive.txt b/pkg/fanal/secret/testdata/wrapped-secrets-sensitive.txt new file mode 100644 index 000000000000..cda3a74921d3 --- /dev/null +++ b/pkg/fanal/secret/testdata/wrapped-secrets-sensitive.txt @@ -0,0 +1,2 @@ +MyAWS_secret_KEY="12ASD34qwe56CXZ78tyH10Tna543VBokN85RHCas" +our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 \ No newline at end of file diff --git a/pkg/fanal/secret/testdata/wrapped-secrets.txt b/pkg/fanal/secret/testdata/wrapped-secrets.txt new file mode 100644 index 000000000000..7e6e0e2c5e74 --- /dev/null +++ b/pkg/fanal/secret/testdata/wrapped-secrets.txt @@ -0,0 +1,11 @@ +DISPID_ICANVASRENDERINGCONTEXT2D_CANVAS DISPID_CANVASRENDERCONTEXT2D +ABCDFLWSECK_TEST-CANVASRENDERCONTEXT2DCANVASRENDA +ABCFLWSECK_TEST123456789012 +Rought_ICANVASRENDERINGVIACONTEXT2D3D5D7D8D +Sogho_ICANVASRENDERINGVIACONTEXT2D3D5D7D8D +Soghu_ICANVASRENDERINGVIACONTEXT2D3D5D7D8D +Bighr_ICANVASRENDERINGVIACONTEXT2D3D5D7D8DICANVASRENDERINGVIACONTEXT2D3D5D7D8D9D22 +Surhf_ICANVASRENDERINGVIACONTEXT2D3D5D6D7D8D9 +abcdexoxb-1234567890 +APK_TEST_1234567890 +ask_live_superlive1 diff --git a/pkg/fanal/test/README.md b/pkg/fanal/test/README.md new file mode 100644 index 000000000000..180c75a73521 --- /dev/null +++ b/pkg/fanal/test/README.md @@ -0,0 +1,7 @@ +# test + +## Directory structure +- integration + - Integration test +- testdata + - This directory contains test images that are used in multiple unit tests \ No newline at end of file diff --git a/pkg/fanal/test/integration/containerd_test.go b/pkg/fanal/test/integration/containerd_test.go new file mode 100644 index 000000000000..7a6beb5eae1c --- /dev/null +++ b/pkg/fanal/test/integration/containerd_test.go @@ -0,0 +1,866 @@ +//go:build integration && linux + +package integration + +import ( + "compress/gzip" + "context" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "testing" + "time" + + "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/images" + "github.com/containerd/containerd/v2/pkg/namespaces" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + aimage "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" + "github.com/aquasecurity/trivy/pkg/fanal/image" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func setupContainerd(t *testing.T, ctx context.Context, namespace string) *client.Client { + t.Helper() + tmpDir := t.TempDir() + + containerdDir := filepath.Join(tmpDir, "containerd") + err := os.MkdirAll(containerdDir, os.ModePerm) + require.NoError(t, err) + + socketPath := filepath.Join(containerdDir, "containerd.sock") + + // Set a containerd socket + t.Setenv("CONTAINERD_ADDRESS", socketPath) + t.Setenv("CONTAINERD_NAMESPACE", namespace) + + startContainerd(t, ctx, tmpDir) + + // Retry up to 3 times until containerd is ready + var c *client.Client + iteration, _, err := lo.AttemptWhileWithDelay(3, 1*time.Second, func(int, time.Duration) (error, bool) { + c, err = client.New(socketPath) + if err != nil { + if !errors.Is(err, os.ErrPermission) { + return err, false // unexpected error + } + return err, true + } + t.Cleanup(func() { + require.NoError(t, c.Close()) + }) + return nil, false + }) + require.NoErrorf(t, err, "attempted %d times ", iteration) + + return c +} + +func startContainerd(t *testing.T, ctx context.Context, hostPath string) { + t.Helper() + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + + // Load `containerd` image from tar file to avoid fetching it from remote registry + cli := testutil.NewDockerClient(t) + loadedImage := cli.ImageLoad(t, ctx, "../../../../integration/testdata/fixtures/images/containerd.tar.gz") + + req := testcontainers.ContainerRequest{ + Name: "containerd", + Image: loadedImage, + Entrypoint: []string{ + "/bin/sh", + "-c", + "/usr/local/bin/containerd", + }, + Mounts: testcontainers.Mounts( + testcontainers.BindMount(hostPath, "/run"), + ), + AutoRemove: true, + WaitingFor: wait.ForLog("containerd successfully booted"), + } + containerdC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + require.NoError(t, err) + testcontainers.CleanupContainer(t, containerdC) + + _, _, err = containerdC.Exec(ctx, []string{ + "chmod", + "666", + "/run/containerd/containerd.sock", + }) + require.NoError(t, err) +} + +// Each of these tests imports an image and tags it with the name found in the +// `imageName` field. Then, the containerd store is searched by the reference +// provided in the `searchName` field. +func TestContainerd_SearchLocalStoreByNameOrDigest(t *testing.T) { + // Each architecture needs different images and test cases. + // Currently only amd64 architecture is supported + if runtime.GOARCH != "amd64" { + t.Skip("'Containerd' test only supports amd64 architecture") + } + + digest := "sha256:f12582b2f2190f350e3904462c1c23aaf366b4f76705e97b199f9bbded1d816a" + basename := "hello" + tag := "world" + importedImageOriginalName := testutil.ImageName("", "alpine-310", "") + + tests := []struct { + name string + imageName string + searchName string + wantErr bool + }{ + { + name: "familiarName:tag", + imageName: fmt.Sprintf("%s:%s", basename, tag), + searchName: fmt.Sprintf("%s:%s", basename, tag), + }, + { + name: "compound/name:tag", + imageName: fmt.Sprintf("say/%s:%s", basename, tag), + searchName: fmt.Sprintf("say/%s:%s", basename, tag), + }, + { + name: "docker.io/library/name:tag", + imageName: fmt.Sprintf("docker.io/library/%s:%s", basename, tag), + searchName: fmt.Sprintf("docker.io/library/%s:%s", basename, tag), + }, + { + name: "other-registry.io/library/name:tag", + imageName: fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag), + searchName: fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag), + }, + { + name: "other-registry.io/library/name:wrongTag should fail", + imageName: fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag), + searchName: fmt.Sprintf("other-registry.io/library/%s:badtag", basename), + wantErr: true, + }, + { + name: "other-registry.io/library/wrongName:tag should fail", + imageName: fmt.Sprintf("other-registry.io/library/%s:%s", basename, tag), + searchName: fmt.Sprintf("other-registry.io/library/badname:%s", tag), + wantErr: true, + }, + { + name: "digest should succeed", + imageName: "", + searchName: digest, + }, + { + name: "wrong digest should fail", + imageName: "", + searchName: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + wantErr: true, + }, + { + name: "name@digest", + imageName: fmt.Sprintf("%s:%s", basename, tag), + searchName: fmt.Sprintf("hello@%s", digest), + }, + { + name: "compound/name@digest", + imageName: fmt.Sprintf("compound/%s:%s", basename, tag), + searchName: fmt.Sprintf("compound/%s@%s", basename, digest), + }, + { + name: "docker.io/library/name@digest", + imageName: fmt.Sprintf("docker.io/library/%s:%s", basename, tag), + searchName: fmt.Sprintf("docker.io/library/%s@%s", basename, digest), + }, + { + name: "otherdomain.io/name@digest", + imageName: fmt.Sprintf("otherdomain.io/%s:%s", basename, tag), + searchName: fmt.Sprintf("otherdomain.io/%s@%s", basename, digest), + }, + { + name: "wrongName@digest should fail", + imageName: fmt.Sprintf("%s:%s", basename, tag), + searchName: fmt.Sprintf("badname@%s", digest), + wantErr: true, + }, + { + name: "compound/wrongName@digest should fail", + imageName: fmt.Sprintf("compound/%s:%s", basename, tag), + searchName: fmt.Sprintf("compound/badname@%s", digest), + wantErr: true, + }, + } + + namespace := "default" + ctx := namespaces.WithNamespace(context.Background(), namespace) + client := setupContainerd(t, ctx, namespace) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + archive, err := os.Open("../../../../integration/testdata/fixtures/images/alpine-310.tar.gz") + require.NoError(t, err) + + uncompressedArchive, err := gzip.NewReader(archive) + require.NoError(t, err) + defer archive.Close() + + cacheDir := t.TempDir() + c, err := cache.NewFSCache(cacheDir) + require.NoError(t, err) + + imgs, err := client.Import(ctx, uncompressedArchive) + require.NoError(t, err) + _ = imgs + + digestTest := tt.imageName == "" + + if !digestTest { + // Tag the image, taken from the code in `ctr image tag...` + imgs[0].Name = tt.imageName + _, err = client.ImageService().Create(ctx, imgs[0]) + require.NoError(t, err) + + // Remove the image by its original name, to ensure the image + // is known only by the tag we have given it. + err = client.ImageService().Delete(ctx, importedImageOriginalName, images.SynchronousDelete()) + require.NoError(t, err) + } + + t.Cleanup(func() { + for _, img := range imgs { + err = client.ImageService().Delete(ctx, img.Name, images.SynchronousDelete()) + require.NoError(t, err) + } + }) + + // enable only containerd + img, cleanup, err := image.NewContainerImage(ctx, tt.searchName, + types.ImageOptions{ImageSources: types.ImageSources{types.ContainerdImageSource}}) + defer cleanup() + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + ar, err := aimage.NewArtifact(img, c, artifact.Option{}) + require.NoError(t, err) + + ref, err := ar.Inspect(ctx) + require.NoError(t, err) + + if digestTest { + actualDigest := strings.Split(ref.ImageMetadata.RepoDigests[0], "@")[1] + require.Equal(t, tt.searchName, actualDigest) + return + } + + require.Equal(t, tt.searchName, ref.Name) + }) + } +} + +func TestContainerd_LocalImage(t *testing.T) { + localImageTestWithNamespace(t, "default") +} + +func TestContainerd_LocalImage_Alternative_Namespace(t *testing.T) { + localImageTestWithNamespace(t, "test") +} + +func localImageTestWithNamespace(t *testing.T, namespace string) { + // Each architecture needs different images and test cases. + // Currently only amd64 architecture is supported + if runtime.GOARCH != "amd64" { + t.Skip("'Containerd' test only supports amd64 architecture") + } + + tests := []struct { + name string + imageName string + tarArchive string + wantMetadata artifact.ImageMetadata + }{ + { + name: "alpine 3.10", + imageName: testutil.ImageName("", "alpine-310", ""), + tarArchive: "../../../../integration/testdata/fixtures/images/alpine-310.tar.gz", + wantMetadata: artifact.ImageMetadata{ + ID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", + DiffIDs: []string{ + "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + }, + RepoTags: []string{testutil.ImageName("", "alpine-310", "")}, + RepoDigests: []string{testutil.ImageName("", "", "sha256:f12582b2f2190f350e3904462c1c23aaf366b4f76705e97b199f9bbded1d816a")}, + Reference: testutil.MustParseReference(t, testutil.ImageName("", "alpine-310", "")), + ConfigFile: v1.ConfigFile{ + Architecture: "amd64", + Created: v1.Time{ + Time: time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC), + }, + OS: "linux", + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{ + "/bin/sh", + }, + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + }, + ArgsEscaped: true, + }, + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(2019, 8, 20, 20, 19, 55, 62606894, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / ", + }, + { + Created: v1.Time{Time: time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + EmptyLayer: true, + }, + }, + }, + }, + }, + { + name: "vulnimage", + imageName: testutil.ImageName("", "vulnimage", ""), + tarArchive: "../../../../integration/testdata/fixtures/images/vulnimage.tar.gz", + wantMetadata: artifact.ImageMetadata{ + ID: "sha256:c17083664da903e13e9092fa3a3a1aeee2431aa2728298e3dbcec72f26369c41", + Reference: testutil.MustParseReference(t, testutil.ImageName("", "vulnimage", "")), + DiffIDs: []string{ + "sha256:ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888", + "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + "sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303", + "sha256:dc00fbef458ad3204bbb548e2d766813f593d857b845a940a0de76aed94c94d1", + "sha256:5cb2a5009179b1e78ecfef81a19756328bb266456cf9a9dbbcf9af8b83b735f0", + "sha256:9bdb2c849099a99c8ab35f6fd7469c623635e8f4479a0a5a3df61e22bae509f6", + "sha256:6408527580eade39c2692dbb6b0f6a9321448d06ea1c2eef06bb7f37da9c5013", + "sha256:83abef706f5ae199af65d1c13d737d0eb36219f0d18e36c6d8ff06159df39a63", + "sha256:c03283c257abd289a30b4f5e9e1345da0e9bfdc6ca398ee7e8fac6d2c1456227", + "sha256:2da3602d664dd3f71fae83cbc566d4e80b432c6ee8bb4efd94c8e85122f503d4", + "sha256:82c59ac8ee582542648e634ca5aff9a464c68ff8a054f105a58689fb52209e34", + "sha256:2f4a5c9187c249834ebc28783bd3c65bdcbacaa8baa6620ddaa27846dd3ef708", + "sha256:6ca56f561e677ae06c3bc87a70792642d671a4416becb9a101577c1a6e090e36", + "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + "sha256:4d116f47cb2cc77a88d609b9805f2b011a5d42339b67300166654b3922685ac9", + "sha256:9b1326af1cf81505fd8e596b7f622b679ae5d290e46b25214ba26e4f7c661d60", + "sha256:a66245f885f2a210071e415f0f8ac4f21f5e4eab6c0435b4082e5c3637c411cb", + "sha256:ba17950e91742d6ac7055ea3a053fe764486658ca1ce8188f1e427b1fe2bc4da", + "sha256:6ef42db7800507577383edf1937cb203b9b85f619feed6046594208748ceb52c", + }, + RepoTags: []string{testutil.ImageName("", "vulnimage", "")}, + RepoDigests: []string{testutil.ImageName("", "", "sha256:e74abbfd81e00baaf464cf9e09f8b24926e5255171e3150a60aa341ce064688f")}, + ConfigFile: v1.ConfigFile{ + Architecture: "amd64", + Created: v1.Time{ + Time: time.Date(2019, 8, 7, 7, 25, 58, 651649800, time.UTC), + }, + OS: "linux", + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "ebf12965380b39889c99a9c02e82ba465f887b45975b6e389d42e9e6a3857888", + }, + { + Algorithm: "sha256", + Hex: "0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + { + Algorithm: "sha256", + Hex: "9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303", + }, + { + Algorithm: "sha256", + Hex: "dc00fbef458ad3204bbb548e2d766813f593d857b845a940a0de76aed94c94d1", + }, + { + Algorithm: "sha256", + Hex: "5cb2a5009179b1e78ecfef81a19756328bb266456cf9a9dbbcf9af8b83b735f0", + }, + { + Algorithm: "sha256", + Hex: "9bdb2c849099a99c8ab35f6fd7469c623635e8f4479a0a5a3df61e22bae509f6", + }, + { + Algorithm: "sha256", + Hex: "6408527580eade39c2692dbb6b0f6a9321448d06ea1c2eef06bb7f37da9c5013", + }, + { + Algorithm: "sha256", + Hex: "83abef706f5ae199af65d1c13d737d0eb36219f0d18e36c6d8ff06159df39a63", + }, + { + Algorithm: "sha256", + Hex: "c03283c257abd289a30b4f5e9e1345da0e9bfdc6ca398ee7e8fac6d2c1456227", + }, + { + Algorithm: "sha256", + Hex: "2da3602d664dd3f71fae83cbc566d4e80b432c6ee8bb4efd94c8e85122f503d4", + }, + { + Algorithm: "sha256", + Hex: "82c59ac8ee582542648e634ca5aff9a464c68ff8a054f105a58689fb52209e34", + }, + { + Algorithm: "sha256", + Hex: "2f4a5c9187c249834ebc28783bd3c65bdcbacaa8baa6620ddaa27846dd3ef708", + }, + { + Algorithm: "sha256", + Hex: "6ca56f561e677ae06c3bc87a70792642d671a4416becb9a101577c1a6e090e36", + }, + { + Algorithm: "sha256", + Hex: "154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + }, + { + Algorithm: "sha256", + Hex: "b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + { + Algorithm: "sha256", + Hex: "4d116f47cb2cc77a88d609b9805f2b011a5d42339b67300166654b3922685ac9", + }, + { + Algorithm: "sha256", + Hex: "9b1326af1cf81505fd8e596b7f622b679ae5d290e46b25214ba26e4f7c661d60", + }, + { + Algorithm: "sha256", + Hex: "a66245f885f2a210071e415f0f8ac4f21f5e4eab6c0435b4082e5c3637c411cb", + }, + { + Algorithm: "sha256", + Hex: "ba17950e91742d6ac7055ea3a053fe764486658ca1ce8188f1e427b1fe2bc4da", + }, + { + Algorithm: "sha256", + Hex: "6ef42db7800507577383edf1937cb203b9b85f619feed6046594208748ceb52c", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{"composer"}, + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + "PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c", + "PHP_INI_DIR=/usr/local/etc/php", + "PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2", + "PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2", + "PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie", + "GPG_KEYS=1729F83938DA44E27BA0F4D3DBDB397470D12172 B1B44D8F021E4E2D6021E995DC9FF8D3EE5AF27F", + "PHP_VERSION=7.2.11", + "PHP_URL=https://secure.php.net/get/php-7.2.11.tar.xz/from/this/mirror", + "PHP_ASC_URL=https://secure.php.net/get/php-7.2.11.tar.xz.asc/from/this/mirror", + "PHP_SHA256=da1a705c0bc46410e330fc6baa967666c8cd2985378fb9707c01a8e33b01d985", + "PHP_MD5=", + "COMPOSER_ALLOW_SUPERUSER=1", + "COMPOSER_HOME=/tmp", + "COMPOSER_VERSION=1.7.2", + }, + WorkingDir: "/app", + Entrypoint: []string{ + "/bin/sh", + "/docker-entrypoint.sh", + }, + ArgsEscaped: true, + }, + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(2018, 9, 11, 22, 19, 38, 885299940, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:49f9e47e678d868d5b023482aa8dded71276a241a665c4f8b55ca77269321b34 in / ", + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 11, 22, 19, 39, 58628442, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 12, 1, 26, 59, 951316015, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHPIZE_DEPS=autoconf \t\tdpkg-dev dpkg \t\tfile \t\tg++ \t\tgcc \t\tlibc-dev \t\tmake \t\tpkgconf \t\tre2c", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 1, 470388635, time.UTC)}, + CreatedBy: "/bin/sh -c apk add --no-cache --virtual .persistent-deps \t\tca-certificates \t\tcurl \t\ttar \t\txz \t\tlibressl", + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 2, 432381785, time.UTC)}, + CreatedBy: "/bin/sh -c set -x \t&& addgroup -g 82 -S www-data \t&& adduser -u 82 -D -S -G www-data www-data", + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 2, 715120309, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_INI_DIR=/usr/local/etc/php", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 3, 655421341, time.UTC)}, + CreatedBy: "/bin/sh -c mkdir -p $PHP_INI_DIR/conf.d", + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 3, 931799562, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_CFLAGS=-fstack-protector-strong -fpic -fpie -O2", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 4, 210945499, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_CPPFLAGS=-fstack-protector-strong -fpic -fpie -O2", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 4, 523116501, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_LDFLAGS=-Wl,-O1 -Wl,--hash-style=both -pie", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 9, 12, 1, 27, 4, 795176159, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV GPG_KEYS=1729F83938DA44E27BA0F4D3DBDB397470D12172 B1B44D8F021E4E2D6021E995DC9FF8D3EE5AF27F", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 18, 415761689, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_VERSION=7.2.11", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 18, 599097853, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_URL=https://secure.php.net/get/php-7.2.11.tar.xz/from/this/mirror PHP_ASC_URL=https://secure.php.net/get/php-7.2.11.tar.xz.asc/from/this/mirror", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 18, 782890412, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV PHP_SHA256=da1a705c0bc46410e330fc6baa967666c8cd2985378fb9707c01a8e33b01d985 PHP_MD5=", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 22, 795846753, time.UTC)}, + CreatedBy: "/bin/sh -c set -xe; \t\tapk add --no-cache --virtual .fetch-deps \t\tgnupg \t\twget \t; \t\tmkdir -p /usr/src; \tcd /usr/src; \t\twget -O php.tar.xz \"$PHP_URL\"; \t\tif [ -n \"$PHP_SHA256\" ]; then \t\techo \"$PHP_SHA256 *php.tar.xz\" | sha256sum -c -; \tfi; \tif [ -n \"$PHP_MD5\" ]; then \t\techo \"$PHP_MD5 *php.tar.xz\" | md5sum -c -; \tfi; \t\tif [ -n \"$PHP_ASC_URL\" ]; then \t\twget -O php.tar.xz.asc \"$PHP_ASC_URL\"; \t\texport GNUPGHOME=\"$(mktemp -d)\"; \t\tfor key in $GPG_KEYS; do \t\t\tgpg --keyserver ha.pool.sks-keyservers.net --recv-keys \"$key\"; \t\tdone; \t\tgpg --batch --verify php.tar.xz.asc php.tar.xz; \t\tcommand -v gpgconf > /dev/null && gpgconf --kill all; \t\trm -rf \"$GNUPGHOME\"; \tfi; \t\tapk del .fetch-deps", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 2, 23, 71406376, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY file:207c686e3fed4f71f8a7b245d8dcae9c9048d276a326d82b553c12a90af0c0ca in /usr/local/bin/ ", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 13, 93396680, time.UTC)}, + CreatedBy: "/bin/sh -c set -xe \t&& apk add --no-cache --virtual .build-deps \t\t$PHPIZE_DEPS \t\tcoreutils \t\tcurl-dev \t\tlibedit-dev \t\tlibressl-dev \t\tlibsodium-dev \t\tlibxml2-dev \t\tsqlite-dev \t\t&& export CFLAGS=\"$PHP_CFLAGS\" \t\tCPPFLAGS=\"$PHP_CPPFLAGS\" \t\tLDFLAGS=\"$PHP_LDFLAGS\" \t&& docker-php-source extract \t&& cd /usr/src/php \t&& gnuArch=\"$(dpkg-architecture --query DEB_BUILD_GNU_TYPE)\" \t&& ./configure \t\t--build=\"$gnuArch\" \t\t--with-config-file-path=\"$PHP_INI_DIR\" \t\t--with-config-file-scan-dir=\"$PHP_INI_DIR/conf.d\" \t\t\t\t--enable-option-checking=fatal \t\t\t\t--with-mhash \t\t\t\t--enable-ftp \t\t--enable-mbstring \t\t--enable-mysqlnd \t\t--with-sodium=shared \t\t\t\t--with-curl \t\t--with-libedit \t\t--with-openssl \t\t--with-zlib \t\t\t\t$(test \"$gnuArch\" = 's390x-linux-gnu' && echo '--without-pcre-jit') \t\t\t\t$PHP_EXTRA_CONFIGURE_ARGS \t&& make -j \"$(nproc)\" \t&& make install \t&& { find /usr/local/bin /usr/local/sbin -type f -perm +0111 -exec strip --strip-all '{}' + || true; } \t&& make clean \t\t&& cp -v php.ini-* \"$PHP_INI_DIR/\" \t\t&& cd / \t&& docker-php-source delete \t\t&& runDeps=\"$( \t\tscanelf --needed --nobanner --format '%n#p' --recursive /usr/local \t\t\t| tr ',' '\\n' \t\t\t| sort -u \t\t\t| awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' \t)\" \t&& apk add --no-cache --virtual .php-rundeps $runDeps \t\t&& apk del .build-deps \t\t&& pecl update-channels \t&& rm -rf /tmp/pear ~/.pearrc", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 13, 722586262, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY multi:2cdcedabcf5a3b9ae610fab7848e94bc2f64b4d85710d55fd6f79e44dacf73d8 in /usr/local/bin/ ", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 14, 618087104, time.UTC)}, + CreatedBy: "/bin/sh -c docker-php-ext-enable sodium", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 14, 826981756, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENTRYPOINT [\"docker-php-entrypoint\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 19, 7, 15, 10831572, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"php\" \"-a\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 21, 919735971, time.UTC)}, + CreatedBy: "/bin/sh -c apk --no-cache add git subversion openssh mercurial tini bash patch", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 22, 611763893, time.UTC)}, + CreatedBy: "/bin/sh -c echo \"memory_limit=-1\" > \"$PHP_INI_DIR/conf.d/memory-limit.ini\" && echo \"date.timezone=${PHP_TIMEZONE:-UTC}\" > \"$PHP_INI_DIR/conf.d/date_timezone.ini\"", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 50, 224278478, time.UTC)}, + CreatedBy: "/bin/sh -c apk add --no-cache --virtual .build-deps zlib-dev && docker-php-ext-install zip && runDeps=\"$( scanelf --needed --nobanner --format '%n#p' --recursive /usr/local/lib/php/extensions | tr ',' '\\n' | sort -u | awk 'system(\"[ -e /usr/local/lib/\" $1 \" ]\") == 0 { next } { print \"so:\" $1 }' )\" && apk add --virtual .composer-phpext-rundeps $runDeps && apk del .build-deps", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 50, 503010161, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV COMPOSER_ALLOW_SUPERUSER=1", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 50, 775378559, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV COMPOSER_HOME=/tmp", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 51, 35012363, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENV COMPOSER_VERSION=1.7.2", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 52, 491402624, time.UTC)}, + CreatedBy: "/bin/sh -c curl --silent --fail --location --retry 3 --output /tmp/installer.php --url https://raw.githubusercontent.com/composer/getcomposer.org/b107d959a5924af895807021fcef4ffec5a76aa9/web/installer && php -r \" \\$signature = '544e09ee996cdf60ece3804abc52599c22b1f40f4323403c44d44fdfdd586475ca9813a858088ffbc1f233e9b180f061'; \\$hash = hash('SHA384', file_get_contents('/tmp/installer.php')); if (!hash_equals(\\$signature, \\$hash)) { unlink('/tmp/installer.php'); echo 'Integrity check failed, installer is either corrupt or worse.' . PHP_EOL; exit(1); }\" && php /tmp/installer.php --no-ansi --install-dir=/usr/bin --filename=composer --version=${COMPOSER_VERSION} && composer --ansi --version --no-interaction && rm -rf /tmp/* /tmp/.htaccess", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 52, 948859545, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) COPY file:295943a303e8f27de4302b6aa3687bce4b1d1392335efaaab9ecd37bec5ab4c5 in /docker-entrypoint.sh ", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 53, 295399872, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) WORKDIR /app", + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 53, 582920705, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ENTRYPOINT [\"/bin/sh\" \"/docker-entrypoint.sh\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2018, 10, 15, 21, 28, 53, 798628678, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"composer\"]", + EmptyLayer: true, + }, + { + Created: v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 57, 211142800, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:842584685f26edb24dc305d76894f51cfda2bad0c24a05e727f9d4905d184a70 in /php-app/composer.lock ", + }, + { + Created: v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 57, 583779000, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:c6d0373d380252b91829a5bb3c81d5b1afa574c91cef7752d18170a231c31f6d in /ruby-app/Gemfile.lock ", + }, + { + Created: v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 57, 921730100, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:54a1c52556a5ebe98fd124f51c25d071f9e29e2714c72c80d6d3d254b9e83386 in /node-app/package-lock.json ", + }, + { + Created: v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 58, 311593100, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:097d32f46acde76c4da9e55f17110d69d02cc6d16c86da907980da335fc0fc5f in /python-app/Pipfile.lock ", + }, + { + Created: v1.Time{Time: time.Date(2019, 8, 7, 7, 25, 58, 651649800, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:7f147d85de19bfb905c260a0c175f227a433259022c163017b96d0efacdcd105 in /rust-app/Cargo.lock ", + }, + }, + }, + }, + }, + } + + t.Helper() + ctx := namespaces.WithNamespace(context.Background(), namespace) + client := setupContainerd(t, ctx, namespace) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cacheDir := t.TempDir() + c, err := cache.NewFSCache(cacheDir) + require.NoError(t, err) + + defer func() { + c.Clear(t.Context()) + c.Close() + }() + + archive, err := os.Open(tt.tarArchive) + require.NoError(t, err) + + uncompressedArchive, err := gzip.NewReader(archive) + require.NoError(t, err) + defer archive.Close() + + _, err = client.Import(ctx, uncompressedArchive) + require.NoError(t, err) + + // Enable only containerd + img, cleanup, err := image.NewContainerImage(ctx, tt.imageName, types.ImageOptions{ + ImageSources: types.ImageSources{types.ContainerdImageSource}, + }) + require.NoError(t, err) + defer cleanup() + + ar, err := aimage.NewArtifact(img, c, artifact.Option{ + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, + }) + require.NoError(t, err) + + ref, err := ar.Inspect(ctx) + require.NoError(t, err) + require.Equal(t, tt.wantMetadata, ref.ImageMetadata) + + a := applier.NewApplier(c) + got, err := a.ApplyLayers(ctx, ref.ID, ref.BlobIDs) + require.NoError(t, err) + + // Clear package detail fields + clearPackageDetailFields(got.Packages) + + tag := strings.Split(tt.imageName, ":")[1] + goldenFile := fmt.Sprintf("testdata/goldens/packages/%s.json.golden", tag) + + if *update { + b, err := json.MarshalIndent(got.Packages, "", " ") + require.NoError(t, err) + err = os.WriteFile(goldenFile, b, 0666) + require.NoError(t, err) + } + + // Parse a golden file + golden, err := os.Open(goldenFile) + require.NoError(t, err) + defer golden.Close() + + var wantPkgs types.Packages + err = json.NewDecoder(golden).Decode(&wantPkgs) + require.NoError(t, err) + + // Assert + assert.Equal(t, wantPkgs, got.Packages) + }) + } +} + +func TestContainerd_PullImage(t *testing.T) { + // Each architecture needs different images and test cases. + // Currently only amd64 architecture is supported + if runtime.GOARCH != "amd64" { + t.Skip("'Containerd' test only supports amd64 architecture") + } + + tests := []struct { + name string + imageName string + wantMetadata artifact.ImageMetadata + }{ + { + name: "remote alpine 3.10", + imageName: testutil.ImageName("", "alpine-310", ""), + wantMetadata: artifact.ImageMetadata{ + ID: "sha256:961769676411f082461f9ef46626dd7a2d1e2b2a38e6a44364bcbecf51e66dd4", + DiffIDs: []string{ + "sha256:03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + }, + RepoTags: []string{testutil.ImageName("", "alpine-310", "")}, + RepoDigests: []string{testutil.ImageName("", "", "sha256:72c42ed48c3a2db31b7dafe17d275b634664a708d901ec9fd57b1529280f01fb")}, + Reference: testutil.MustParseReference(t, testutil.ImageName("", "alpine-310", "")), + ConfigFile: v1.ConfigFile{ + Architecture: "amd64", + Created: v1.Time{ + Time: time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC), + }, + OS: "linux", + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "03901b4a2ea88eeaad62dbe59b072b28b6efa00491962b8741081c5df50c65e0", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{ + "/bin/sh", + }, + Env: []string{ + "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin", + }, + ArgsEscaped: true, + }, + History: []v1.History{ + { + Created: v1.Time{time.Date(2019, 8, 20, 20, 19, 55, 62606894, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:fe64057fbb83dccb960efabbf1cd8777920ef279a7fa8dbca0a8801c651bdf7c in / ", + }, + { + Created: v1.Time{time.Date(2019, 8, 20, 20, 19, 55, 211423266, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + EmptyLayer: true, + }, + }, + }, + }, + }, + } + + namespace := "default" + ctx := namespaces.WithNamespace(context.Background(), namespace) + client := setupContainerd(t, ctx, namespace) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cacheDir := t.TempDir() + c, err := cache.NewFSCache(cacheDir) + require.NoError(t, err) + + defer func() { + c.Clear(t.Context()) + c.Close() + }() + + _, err = client.Pull(ctx, tt.imageName) + require.NoError(t, err) + + // Enable only containerd + img, cleanup, err := image.NewContainerImage(ctx, tt.imageName, types.ImageOptions{ + ImageSources: types.ImageSources{types.ContainerdImageSource}, + }) + require.NoError(t, err) + defer cleanup() + + art, err := aimage.NewArtifact(img, c, artifact.Option{ + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, + }) + require.NoError(t, err) + require.NotNil(t, art) + + ref, err := art.Inspect(context.Background()) + require.NoError(t, err) + require.Equal(t, tt.wantMetadata, ref.ImageMetadata) + + a := applier.NewApplier(c) + got, err := a.ApplyLayers(ctx, ref.ID, ref.BlobIDs) + require.NoError(t, err) + + // Parse a golden file + tag := strings.Split(tt.imageName, ":")[1] + golden, err := os.Open(fmt.Sprintf("testdata/goldens/packages/%s.json.golden", tag)) + require.NoError(t, err) + + var wantPkgs types.Packages + err = json.NewDecoder(golden).Decode(&wantPkgs) + require.NoError(t, err) + + // Clear package detail fields for comparison + clearPackageDetailFields(got.Packages) + + // Assert + assert.Equal(t, wantPkgs, got.Packages) + }) + } +} diff --git a/pkg/fanal/test/integration/data/registry/auth/htpasswd b/pkg/fanal/test/integration/data/registry/auth/htpasswd new file mode 100644 index 000000000000..87fc507be943 --- /dev/null +++ b/pkg/fanal/test/integration/data/registry/auth/htpasswd @@ -0,0 +1,2 @@ +testuser:$2y$05$6PfNBO51NB6RIVOmy14WSed7tiHV/pETiYldZ/cT78KLPpFnSUWTi + diff --git a/pkg/fanal/test/integration/data/registry/certs/cert.pem b/pkg/fanal/test/integration/data/registry/certs/cert.pem new file mode 100644 index 000000000000..66d2421c2159 --- /dev/null +++ b/pkg/fanal/test/integration/data/registry/certs/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC+DCCAeCgAwIBAgIQdRfi50B0buW3v2MxX5WIlDANBgkqhkiG9w0BAQsFADAS +MRAwDgYDVQQKEwdBY21lIENvMB4XDTE5MTAxMzE3MjIxMFoXDTIwMTAxMjE3MjIx +MFowEjEQMA4GA1UEChMHQWNtZSBDbzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBAKtXwwgUzfBtf1Hj41cUBkHbwR2hnlGfAXH00LxO9EQdP8zvA1u98aEJ +5zDSSo6g3O+at4izkWC9Niik9kUJiqDJ6DYcN/mZe1UZ6QclsdZrqmLY3C6qD8Zx +M+LyEA67b2OHYKACk9jXk+8XmVhFFI1svZNMo6Ci3soXv32tR2Ydb4OTBhfLk2i5 +w/MNqzoQIanF9zyXAiPnX8SdbrdLV9DAYlOABBDAtSabhffgZSUD9ThYtvS/Sx2M +sT5TAbqbAVRhfBxCK23+6+HebISYUygkdCtvfSSuzdp4gSH3yet/3oaxGiXawcrS +TKJna0MMiWJC769OsdvUgNpELUnoRZ8CAwEAAaNKMEgwDgYDVR0PAQH/BAQDAgWg +MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwEwYDVR0RBAwwCoII +cmVnaXN0cnkwDQYJKoZIhvcNAQELBQADggEBACOR2nkmztD3bZh9nx2MVRE1tkHt +06k5kDPnvYo89Q4R0Qx6fJ6g+aTXEOTbHkwPMR/ZRAswZg6ZMkpo4hq0qf5ueL0G +qHJT6GH0YYr2SqVyxbNHZZqs7nuz5lJLJTQYF95OwEo1qKEfwdA1BU6+p1S95COK +tZngyuQGWp6qGgnzpOhrkC4O6tpXA2lXbgpR87HyzD2GVIt5WXZJX99FXiUp2ZhQ +0yPJwnDUGCbTui1UpBRrM62PwwFNKGg9+pGF+0A6gUWX4ofFApcxKDSdcynT9duO ++4yh/t1KTalzv7W0KtBIGeJWHQAskn2EBFjRrhgmPAlXjP85RVVHHrM2FWc= +-----END CERTIFICATE----- diff --git a/pkg/fanal/test/integration/data/registry/certs/key.pem b/pkg/fanal/test/integration/data/registry/certs/key.pem new file mode 100644 index 000000000000..d8f95ed3f520 --- /dev/null +++ b/pkg/fanal/test/integration/data/registry/certs/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCrV8MIFM3wbX9R +4+NXFAZB28EdoZ5RnwFx9NC8TvREHT/M7wNbvfGhCecw0kqOoNzvmreIs5FgvTYo +pPZFCYqgyeg2HDf5mXtVGekHJbHWa6pi2Nwuqg/GcTPi8hAOu29jh2CgApPY15Pv +F5lYRRSNbL2TTKOgot7KF799rUdmHW+DkwYXy5NoucPzDas6ECGpxfc8lwIj51/E +nW63S1fQwGJTgAQQwLUmm4X34GUlA/U4WLb0v0sdjLE+UwG6mwFUYXwcQitt/uvh +3myEmFMoJHQrb30krs3aeIEh98nrf96GsRol2sHK0kyiZ2tDDIliQu+vTrHb1IDa +RC1J6EWfAgMBAAECggEAPnjeNFkZiTrzPBFh94LmkSalr78YsVvPNBR18uK6rwcX +FwpiaBXGPtgUgjl6U6yVQYtgtaf7LpFuaL1UyXEkKWHUl74MLqZmUG4FB8UXpT1W +P3ogNadt3wWxTGLDtgtjHp/ifBTUgTCA0KEDGSr+xl3KdSVxV4Vqw7aK7DC8op8q +CkqHMHW/tbmC/52sbo8rPCDoPcdBiotJuGWrBy3pvvinv4PegPjbdgMskneII7JB +7fyN9O543ViASO6OPQeKXHCefDVU+Bu6jCobZSHyd9ofP9ODYNh2eehWDrLcxUiI +Fv2k9/OYKTkiAZt4n6uOeBIjADQPW3O+pFhIzyh10QKBgQDaLC1ndASTzxjPhCxk +F6KzZKgTfZTESYmcZRWjOdPN+uLZmQ0vUDP8ww1BBKsNWiT1DG+b5t0AcV8GhtRH +SDHfsyUk4+SmX+DSbq7d08SPbXuBw9qj5e4TwLbuhRpEzC6Y/FJowdcIJBDA/Bdl +eFGLKGK4uSEmIf3HMHwuE22sSQKBgQDJDP79U32hW9dkrXCaYwuLGNCTp7OUUvhr +xZ2XpiAd4xTiHArS5oD+uYOtl89gLcC3rl04ONR2czXo1wbUzoGxfc5mMFfDwbu4 +flpc2Unf+OC7MAYI8tweNvPaCh9yM162kA/HBpCmXnU7AvrFxCv8varUD7JouuM6 +GJ0Tq8HSpwKBgAwXRqzlERvf7lAR0yP461tlyKrL2uLF8IpqT7isC8DuloPYp5Jn +r3nilWl1CMXEqSereMP7F2Re5BVIg5svtRPgqb5RoupVhfAjki1Y/xuzxZR7djJ/ +G6Kcm4a8eag2aJ6x3R9RHsmktgj775AACwTbSB1UpTGn0JipZzhmez2BAoGBALNG +om20MFddNPEyczFH1ng/NWYk6U3+ukQnoAIlAYiFAA8roD81iZsaQ5/cue9yfOGN +WtSSYGoLC+xHKXFDf4SMwSqAOxJ0w3adPOiEXYqZO5PPG+KI5N0QdVtdIXNM+Uzj +ox3+7adcnT50xrbo2axyWEzmfllDJPGeFxr+UkQLAoGAU+e8RnpTzSabcCSLI5lN +XZ/YZzylwyFQtuwP4CkWY5ZamRI4xbAGgUrQhJLdALJqa6gY48MjLyiMeREPpmPB +Qy2kGvbvRn6twQApxgwbqDKzHH33nx9PSuZhTR/GQyONGIcpXptrqoDCFChQs7hK +js5gSQbPU3clpXeP3Oq7IAY= +-----END PRIVATE KEY----- diff --git a/pkg/fanal/test/integration/docker/docker.go b/pkg/fanal/test/integration/docker/docker.go new file mode 100644 index 000000000000..125226f56c25 --- /dev/null +++ b/pkg/fanal/test/integration/docker/docker.go @@ -0,0 +1,131 @@ +package docker + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/url" + "os" + "os/exec" + + apiregistry "github.com/moby/moby/api/types/registry" + "github.com/moby/moby/client" +) + +type RegistryConfig struct { + URL *url.URL + Username string + Password string +} + +func (c RegistryConfig) GetAuthConfig() apiregistry.AuthConfig { + return apiregistry.AuthConfig{ + Username: c.Username, + Password: c.Password, + ServerAddress: c.URL.Host, + } +} + +func (c RegistryConfig) GetRegistryAuth() (string, error) { + authConfig := apiregistry.AuthConfig{ + Username: c.Username, + Password: c.Password, + } + encodedJSON, err := json.Marshal(authConfig) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(encodedJSON), nil +} + +func (c RegistryConfig) GetBasicAuthorization() string { + return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString(fmt.Appendf(nil, "%s:%s", c.Username, c.Password))) +} + +type Docker struct { + cli *client.Client +} + +func New() (Docker, error) { + cli, err := client.New(client.FromEnv) + if err != nil { + return Docker{}, err + } + return Docker{ + cli: cli, + }, nil +} + +func (d Docker) Login(conf RegistryConfig) error { + auth := conf.GetAuthConfig() + return exec.Command("docker", "login", "-u", auth.Username, "-p", auth.Password, auth.ServerAddress).Run() +} + +func (d Docker) Logout(conf RegistryConfig) error { + auth := conf.GetAuthConfig() + return exec.Command("docker", "logout", auth.ServerAddress).Run() +} + +// ReplicateImage tags the given imagePath and pushes it to the given dest registry. +func (d Docker) ReplicateImage(ctx context.Context, imageRef, imagePath string, dest RegistryConfig) error { + // remove existing Image if any + _, _ = d.cli.ImageRemove(ctx, imageRef, client.ImageRemoveOptions{ + Force: true, + PruneChildren: true, + }) + + testfile, err := os.Open(imagePath) + if err != nil { + return err + } + defer testfile.Close() + + // load image into docker engine + resp, err := d.cli.ImageLoad(ctx, testfile, client.ImageLoadWithQuiet(true)) + if err != nil { + return err + } + if _, err := io.Copy(io.Discard, resp); err != nil { + return err + } + defer resp.Close() + + targetImageRef := fmt.Sprintf("%s/%s", dest.URL.Host, imageRef) + + if _, err = d.cli.ImageTag(ctx, client.ImageTagOptions{ + Source: imageRef, + Target: targetImageRef, + }); err != nil { + return err + } + defer func() { + _, _ = d.cli.ImageRemove(ctx, imageRef, client.ImageRemoveOptions{ + Force: true, + PruneChildren: true, + }) + _, _ = d.cli.ImageRemove(ctx, targetImageRef, client.ImageRemoveOptions{ + Force: true, + PruneChildren: true, + }) + }() + + auth, err := dest.GetRegistryAuth() + if err != nil { + return err + } + + pushOut, err := d.cli.ImagePush(ctx, targetImageRef, client.ImagePushOptions{ + RegistryAuth: auth, + }) + if err != nil { + return err + } + defer pushOut.Close() + + if _, err = io.Copy(io.Discard, pushOut); err != nil { + return err + } + return nil +} diff --git a/pkg/fanal/test/integration/library_test.go b/pkg/fanal/test/integration/library_test.go new file mode 100644 index 000000000000..bef083377960 --- /dev/null +++ b/pkg/fanal/test/integration/library_test.go @@ -0,0 +1,339 @@ +//go:build integration + +package integration + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + aimage "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" + "github.com/aquasecurity/trivy/pkg/fanal/image" + "github.com/aquasecurity/trivy/pkg/fanal/types" + + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/all" + _ "github.com/aquasecurity/trivy/pkg/fanal/handler/all" + _ "modernc.org/sqlite" +) + +var update = flag.Bool("update", false, "update golden files") + +type testCase struct { + name string + imageTag string + imageFile string + wantOS types.OS + wantPkgsFromCmds string + wantApplicationFile string +} + +var tests = []testCase{ + { + name: "happy path, alpine:3.10", + imageTag: "alpine-310", + imageFile: "../../../../integration/testdata/fixtures/images/alpine-310.tar.gz", + wantOS: types.OS{ + Name: "3.10.2", + Family: "alpine", + }, + }, + { + name: "happy path, amazonlinux:2", + imageTag: "amazon-2", + imageFile: "../../../../integration/testdata/fixtures/images/amazon-2.tar.gz", + wantOS: types.OS{ + Name: "2 (Karoo)", + Family: "amazon", + }, + }, + { + name: "happy path, debian:buster", + imageTag: "debian-buster", + imageFile: "../../../../integration/testdata/fixtures/images/debian-buster.tar.gz", + wantOS: types.OS{ + Name: "10.1", + Family: "debian", + }, + }, + { + name: "happy path, photon:3.0", + imageTag: "photon-30", + imageFile: "../../../../integration/testdata/fixtures/images/photon-30.tar.gz", + wantOS: types.OS{ + Name: "3.0", + Family: "photon", + }, + }, + { + name: "happy path, registry.redhat.io/ubi7", + imageTag: "ubi-7", + imageFile: "../../../../integration/testdata/fixtures/images/ubi-7.tar.gz", + wantOS: types.OS{ + Name: "7.7", + Family: "redhat", + }, + }, + { + name: "happy path, opensuse leap 15.1", + imageTag: "opensuse-leap-151", + imageFile: "../../../../integration/testdata/fixtures/images/opensuse-leap-151.tar.gz", + wantOS: types.OS{ + Name: "15.1", + Family: "opensuse-leap", + }, + }, + { + name: "happy path, opensuse tumbleweed", + imageTag: "opensuse-tumbleweed", + imageFile: "../../../../integration/testdata/fixtures/images/opensuse-tumbleweed.tar.gz", + wantOS: types.OS{ + Name: "20240607", + Family: "opensuse-tumbleweed", + }, + }, + { + // from registry.suse.com/suse/sle15:15.3.17.8.16 + name: "happy path, suse 15.3 (NDB)", + imageTag: "suse-15.3_ndb", + imageFile: "../../../../integration/testdata/fixtures/images/suse-15.3_ndb.tar.gz", + wantOS: types.OS{ + Name: "15.3", + Family: "sles", + }, + }, + { + name: "happy path, Fedora 35", + imageTag: "fedora-35", + imageFile: "../../../../integration/testdata/fixtures/images/fedora-35.tar.gz", + wantOS: types.OS{ + Name: "35", + Family: "fedora", + }, + }, + { + name: "happy path, vulnimage with lock files", + imageTag: "vulnimage", + imageFile: "../../../../integration/testdata/fixtures/images/vulnimage.tar.gz", + wantOS: types.OS{ + Name: "3.7.1", + Family: "alpine", + }, + wantApplicationFile: "testdata/goldens/vuln-image1.2.3.expectedlibs.golden", + wantPkgsFromCmds: "testdata/goldens/vuln-image1.2.3.expectedpkgsfromcmds.golden", + }, +} + +func TestFanal_Library_DockerMode(t *testing.T) { + // Disable updating golden files because local images don't have compressed layer digests, + // and updating golden files in this function results in incomplete files. + if *update { + t.Skipf("This test creates wrong golden file") + } + + cli := testutil.NewDockerClient(t) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := t.Context() + d := t.TempDir() + + c, err := cache.NewFSCache(d) + require.NoError(t, err) + + loadedImage := cli.ImageLoad(t, ctx, tt.imageFile) + + // Enable only dockerd scanning + img, cleanup, err := image.NewContainerImage(ctx, loadedImage, types.ImageOptions{ + ImageSources: types.ImageSources{types.DockerImageSource}, + }) + require.NoError(t, err, tt.name) + defer cleanup() + + // run tests twice, one without cache and with cache + for i := 1; i <= 2; i++ { + ar, err := aimage.NewArtifact(img, c, artifact.Option{ + // disable license checking in the test - in parallel it will fail because of resource requirement + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, + }) + require.NoError(t, err) + + applier := applier.NewApplier(c) + + runChecks(t, ctx, ar, applier, tt) + } + + // clear Cache + require.NoError(t, c.Clear(t.Context()), tt.name) + }) + } +} + +func TestFanal_Library_TarMode(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := t.Context() + d := t.TempDir() + + c, err := cache.NewFSCache(d) + require.NoError(t, err) + + img, err := image.NewArchiveImage(tt.imageFile) + require.NoError(t, err, tt.name) + + ar, err := aimage.NewArtifact(img, c, artifact.Option{ + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, + }) + require.NoError(t, err) + + applier := applier.NewApplier(c) + + runChecks(t, ctx, ar, applier, tt) + + // clear Cache + require.NoError(t, c.Close()) + }) + } +} + +func runChecks(t *testing.T, ctx context.Context, ar artifact.Artifact, applier applier.Applier, tc testCase) { + imageInfo, err := ar.Inspect(ctx) + require.NoError(t, err, tc.name) + imageDetail, err := applier.ApplyLayers(ctx, imageInfo.ID, imageInfo.BlobIDs) + require.NoError(t, err, tc.name) + commonChecks(t, imageDetail, tc) +} + +func commonChecks(t *testing.T, detail types.ArtifactDetail, tc testCase) { + assert.Equal(t, tc.wantOS, detail.OS, tc.name) + checkOSPackages(t, detail, tc) + checkPackageFromCommands(t, detail, tc) + checkLangPkgs(detail, t, tc) +} + +// clearPackageDetailFields clears package detail fields to keep golden files manageable. +// Fields cleared: Identifier (UID, PURL, BOMRef), Layer, InstalledFiles, DependsOn, Digest +// Fields kept for comparison: ID, Name, Version, Epoch, Release, Arch, SrcName, SrcEpoch, SrcVersion, SrcRelease, Licenses, Maintainer, Modularitylabel, Indirect +func clearPackageDetailFields(packages []types.Package) { + for i := range packages { + packages[i].Identifier = types.PkgIdentifier{} // Clear entire Identifier (UID, PURL, BOMRef) + packages[i].Layer = types.Layer{} + packages[i].InstalledFiles = nil + packages[i].DependsOn = nil + packages[i].Digest = "" + } +} + +func checkOSPackages(t *testing.T, detail types.ArtifactDetail, tc testCase) { + // Sort OS packages for consistency + sort.Sort(detail.Packages) + + // Clear package detail fields to keep golden files manageable in size. + // Cleared fields: Identifier.UID, Layer, InstalledFiles, DependsOn, Digest + // These fields are either too large (InstalledFiles, Layer) or not critical for comparison (UID, Digest, DependsOn). + // All other fields including ID, Name, Version, Licenses, Maintainer, etc. are compared. + clearPackageDetailFields(detail.Packages) + + goldenFile := fmt.Sprintf("testdata/goldens/packages/%s.json.golden", tc.imageTag) + + if *update { + b, err := json.MarshalIndent(detail.Packages, "", " ") + require.NoError(t, err) + err = os.WriteFile(goldenFile, b, 0o666) + require.NoError(t, err) + return + } + data, err := os.ReadFile(goldenFile) + require.NoError(t, err, tc.name) + + var expectedPkgs types.Packages + err = json.Unmarshal(data, &expectedPkgs) + require.NoError(t, err) + + sort.Sort(detail.Packages) + sort.Sort(expectedPkgs) + + assert.Equal(t, expectedPkgs, detail.Packages, tc.name) +} + +func checkLangPkgs(detail types.ArtifactDetail, t *testing.T, tc testCase) { + if tc.wantApplicationFile != "" { + // Sort applications for consistency + sort.Slice(detail.Applications, func(i, j int) bool { + if detail.Applications[i].Type != detail.Applications[j].Type { + return detail.Applications[i].Type < detail.Applications[j].Type + } + return detail.Applications[i].FilePath < detail.Applications[j].FilePath + }) + + for _, app := range detail.Applications { + sort.Sort(app.Packages) + for i := range app.Packages { + sort.Strings(app.Packages[i].DependsOn) + } + } + + // Do not compare layers + for _, app := range detail.Applications { + for i := range app.Packages { + app.Packages[i].Layer = types.Layer{} + } + } + + if *update { + b, err := json.MarshalIndent(detail.Applications, "", " ") + require.NoError(t, err) + err = os.WriteFile(tc.wantApplicationFile, b, 0o666) + require.NoError(t, err) + return + } + + var wantApps types.Applications + data, err := os.ReadFile(tc.wantApplicationFile) + require.NoError(t, err) + err = json.Unmarshal(data, &wantApps) + require.NoError(t, err) + + require.Equal(t, wantApps, detail.Applications, tc.name) + } else { + assert.Nil(t, detail.Applications, tc.name) + } +} + +func checkPackageFromCommands(t *testing.T, detail types.ArtifactDetail, tc testCase) { + if tc.wantPkgsFromCmds != "" { + if *update { + sort.Sort(types.Packages(detail.ImageConfig.Packages)) + b, err := json.MarshalIndent(detail.ImageConfig.Packages, "", " ") + require.NoError(t, err) + err = os.WriteFile(tc.wantPkgsFromCmds, b, 0o666) + require.NoError(t, err) + return + } + data, _ := os.ReadFile(tc.wantPkgsFromCmds) + var expectedPkgsFromCmds []types.Package + + err := json.Unmarshal(data, &expectedPkgsFromCmds) + require.NoError(t, err) + assert.ElementsMatch(t, expectedPkgsFromCmds, detail.ImageConfig.Packages, tc.name) + } else { + assert.Equal(t, []types.Package(nil), detail.ImageConfig.Packages, tc.name) + } +} diff --git a/pkg/fanal/test/integration/registry_test.go b/pkg/fanal/test/integration/registry_test.go new file mode 100644 index 000000000000..0352e11d5389 --- /dev/null +++ b/pkg/fanal/test/integration/registry_test.go @@ -0,0 +1,256 @@ +//go:build integration + +package integration + +import ( + "context" + "fmt" + "net/url" + "path/filepath" + "testing" + + "github.com/docker/go-connections/nat" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/wait" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + aimage "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" + "github.com/aquasecurity/trivy/pkg/fanal/image" + testdocker "github.com/aquasecurity/trivy/pkg/fanal/test/integration/docker" + "github.com/aquasecurity/trivy/pkg/fanal/types" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" + + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/all" +) + +const ( + registryImage = "registry:2" + registryPort = "5443/tcp" + registryUsername = "testuser" + registryPassword = "testpassword" +) + +func TestTLSRegistry(t *testing.T) { + ctx := t.Context() + + baseDir, err := filepath.Abs(".") + require.NoError(t, err) + + t.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true") + req := testcontainers.ContainerRequest{ + Name: "registry", + Image: registryImage, + ExposedPorts: []string{registryPort}, + Env: map[string]string{ + "REGISTRY_HTTP_ADDR": "0.0.0.0:5443", + "REGISTRY_HTTP_TLS_CERTIFICATE": "/certs/cert.pem", + "REGISTRY_HTTP_TLS_KEY": "/certs/key.pem", + "REGISTRY_AUTH": "htpasswd", + "REGISTRY_AUTH_HTPASSWD_PATH": "/auth/htpasswd", + "REGISTRY_AUTH_HTPASSWD_REALM": "Registry Realm", + }, + Mounts: testcontainers.Mounts( + testcontainers.BindMount(filepath.Join(baseDir, "data", "registry", "certs"), "/certs"), + testcontainers.BindMount(filepath.Join(baseDir, "data", "registry", "auth"), "/auth"), + ), + AutoRemove: true, + WaitingFor: wait.ForLog("listening on [::]:5443"), + } + + registryC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: req, + Started: true, + }) + require.NoError(t, err) + testcontainers.CleanupContainer(t, registryC) + + registryURL, err := getRegistryURL(ctx, registryC, registryPort) + require.NoError(t, err) + + config := testdocker.RegistryConfig{ + URL: registryURL, + Username: registryUsername, + Password: registryPassword, + } + + testCases := []struct { + name string + imageName string + imageFile string + option types.ImageOptions + login bool + expectedOS types.OS + expectedRepo types.Repository + wantErr bool + }{ + { + name: "happy path", + imageName: testutil.ImageName("", "alpine-310", ""), + imageFile: "../../../../integration/testdata/fixtures/images/alpine-310.tar.gz", + option: types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: registryUsername, + Password: registryPassword, + }, + }, + Insecure: true, + }, + }, + expectedOS: types.OS{ + Name: "3.10.2", + Family: "alpine", + }, + expectedRepo: types.Repository{ + Family: "alpine", + Release: "3.10", + }, + wantErr: false, + }, + { + name: "happy path with docker login", + imageName: testutil.ImageName("", "alpine-310", ""), + imageFile: "../../../../integration/testdata/fixtures/images/alpine-310.tar.gz", + option: types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + Insecure: true, + }, + }, + login: true, + expectedOS: types.OS{ + Name: "3.10.2", + Family: "alpine", + }, + expectedRepo: types.Repository{ + Family: "alpine", + Release: "3.10", + }, + wantErr: false, + }, + { + name: "sad path: tls verify", + imageName: testutil.ImageName("", "alpine-310", ""), + imageFile: "../../../../integration/testdata/fixtures/images/alpine-310.tar.gz", + option: types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: registryUsername, + Password: registryPassword, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "sad path: no credential", + imageName: testutil.ImageName("", "alpine-310", ""), + imageFile: "../../../../integration/testdata/fixtures/images/alpine-310.tar.gz", + option: types.ImageOptions{ + RegistryOptions: types.RegistryOptions{ + Insecure: true, + }, + }, + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + d, err := testdocker.New() + require.NoError(t, err) + + // 0. Set the image source to remote + tc.option.ImageSources = types.ImageSources{types.RemoteImageSource} + + // 1. Load a test image from the tar file, tag it and push to the test registry. + err = d.ReplicateImage(ctx, tc.imageName, tc.imageFile, config) + require.NoError(t, err) + + if tc.login { + err = d.Login(config) + require.NoError(t, err) + + defer d.Logout(config) + } + + // 2. Analyze it + imageRef := fmt.Sprintf("%s/%s", registryURL.Host, tc.imageName) + imageDetail, err := analyze(t, ctx, imageRef, tc.option) + require.Equal(t, tc.wantErr, err != nil, err) + if err != nil { + return + } + + assert.Equal(t, tc.expectedOS, imageDetail.OS) + assert.Equal(t, &tc.expectedRepo, imageDetail.Repository) + }) + } +} + +func getRegistryURL(ctx context.Context, registryC testcontainers.Container, exposedPort nat.Port) (*url.URL, error) { + ip, err := registryC.Host(ctx) + if err != nil { + return nil, err + } + + port, err := registryC.MappedPort(ctx, exposedPort) + if err != nil { + return nil, err + } + + urlStr := fmt.Sprintf("https://%s:%s", ip, port.Port()) + return url.Parse(urlStr) +} + +func analyze(t *testing.T, ctx context.Context, imageRef string, opt types.ImageOptions) (*types.ArtifactDetail, error) { + d := t.TempDir() + + c, err := cache.NewFSCache(d) + if err != nil { + return nil, err + } + + // Configure custom transport with insecure option + ctx = xhttp.WithTransport(ctx, xhttp.NewTransport(xhttp.Options{ + Insecure: opt.RegistryOptions.Insecure, + })) + + img, cleanup, err := image.NewContainerImage(ctx, imageRef, opt) + if err != nil { + return nil, err + } + defer cleanup() + + ar, err := aimage.NewArtifact(img, c, artifact.Option{ + DisabledAnalyzers: []analyzer.Type{ + analyzer.TypeExecutable, + analyzer.TypeLicenseFile, + }, + }) + if err != nil { + return nil, err + } + + ap := applier.NewApplier(c) + + imageInfo, err := ar.Inspect(ctx) + if err != nil { + return nil, err + } + defer ar.Clean(imageInfo) + + imageDetail, err := ap.ApplyLayers(ctx, imageInfo.ID, imageInfo.BlobIDs) + if err != nil { + return nil, err + } + return &imageDetail, nil +} diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/alpine-310.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/alpine-310.json.golden new file mode 100644 index 000000000000..68659bcae53f --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/alpine-310.json.golden @@ -0,0 +1,186 @@ +[ + { + "ID": "alpine-baselayout@3.1.2-r0", + "Name": "alpine-baselayout", + "Version": "3.1.2-r0", + "Arch": "x86_64", + "SrcName": "alpine-baselayout", + "SrcVersion": "3.1.2-r0", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "alpine-keys@2.1-r2", + "Name": "alpine-keys", + "Version": "2.1-r2", + "Arch": "x86_64", + "SrcName": "alpine-keys", + "SrcVersion": "2.1-r2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "apk-tools@2.10.4-r2", + "Name": "apk-tools", + "Version": "2.10.4-r2", + "Arch": "x86_64", + "SrcName": "apk-tools", + "SrcVersion": "2.10.4-r2", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "busybox@1.30.1-r2", + "Name": "busybox", + "Version": "1.30.1-r2", + "Arch": "x86_64", + "SrcName": "busybox", + "SrcVersion": "1.30.1-r2", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "ca-certificates-cacert@20190108-r0", + "Name": "ca-certificates-cacert", + "Version": "20190108-r0", + "Arch": "x86_64", + "SrcName": "ca-certificates", + "SrcVersion": "20190108-r0", + "Licenses": [ + "MPL-2.0", + "GPL-2.0-or-later" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libc-utils@0.7.1-r0", + "Name": "libc-utils", + "Version": "0.7.1-r0", + "Arch": "x86_64", + "SrcName": "libc-dev", + "SrcVersion": "0.7.1-r0", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libcrypto1.1@1.1.1c-r0", + "Name": "libcrypto1.1", + "Version": "1.1.1c-r0", + "Arch": "x86_64", + "SrcName": "openssl", + "SrcVersion": "1.1.1c-r0", + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "Timo Teras \u003ctimo.teras@iki.fi\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libssl1.1@1.1.1c-r0", + "Name": "libssl1.1", + "Version": "1.1.1c-r0", + "Arch": "x86_64", + "SrcName": "openssl", + "SrcVersion": "1.1.1c-r0", + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "Timo Teras \u003ctimo.teras@iki.fi\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libtls-standalone@2.9.1-r0", + "Name": "libtls-standalone", + "Version": "2.9.1-r0", + "Arch": "x86_64", + "SrcName": "libtls-standalone", + "SrcVersion": "2.9.1-r0", + "Licenses": [ + "ISC" + ], + "AnalyzedBy": "apk" + }, + { + "ID": "musl@1.1.22-r3", + "Name": "musl", + "Version": "1.1.22-r3", + "Arch": "x86_64", + "SrcName": "musl", + "SrcVersion": "1.1.22-r3", + "Licenses": [ + "MIT" + ], + "Maintainer": "Timo Teräs \u003ctimo.teras@iki.fi\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "musl-utils@1.1.22-r3", + "Name": "musl-utils", + "Version": "1.1.22-r3", + "Arch": "x86_64", + "SrcName": "musl", + "SrcVersion": "1.1.22-r3", + "Licenses": [ + "MIT", + "BSD-3-Clause", + "GPL-2.0-or-later" + ], + "Maintainer": "Timo Teräs \u003ctimo.teras@iki.fi\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "scanelf@1.2.3-r0", + "Name": "scanelf", + "Version": "1.2.3-r0", + "Arch": "x86_64", + "SrcName": "pax-utils", + "SrcVersion": "1.2.3-r0", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "ssl_client@1.30.1-r2", + "Name": "ssl_client", + "Version": "1.30.1-r2", + "Arch": "x86_64", + "SrcName": "busybox", + "SrcVersion": "1.30.1-r2", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "zlib@1.2.11-r1", + "Name": "zlib", + "Version": "1.2.11-r1", + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.11-r1", + "Licenses": [ + "Zlib" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/amazon-2.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/amazon-2.json.golden new file mode 100644 index 000000000000..08e0c010693a --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/amazon-2.json.golden @@ -0,0 +1,1882 @@ +[ + { + "ID": "amazon-linux-extras@1.6.7-1.amzn2.noarch", + "Name": "amazon-linux-extras", + "Version": "1.6.7", + "Release": "1.amzn2", + "Arch": "noarch", + "SrcName": "amazon-linux-extras", + "SrcVersion": "1.6.7", + "SrcRelease": "1.amzn2", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "basesystem@10.0-7.amzn2.0.1.noarch", + "Name": "basesystem", + "Version": "10.0", + "Release": "7.amzn2.0.1", + "Arch": "noarch", + "SrcName": "basesystem", + "SrcVersion": "10.0", + "SrcRelease": "7.amzn2.0.1", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bash@4.2.46-30.amzn2.x86_64", + "Name": "bash", + "Version": "4.2.46", + "Release": "30.amzn2", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.2.46", + "SrcRelease": "30.amzn2", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bzip2-libs@1.0.6-13.amzn2.0.2.x86_64", + "Name": "bzip2-libs", + "Version": "1.0.6", + "Release": "13.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.6", + "SrcRelease": "13.amzn2.0.2", + "Licenses": [ + "BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates@2018.2.22-70.0.amzn2.noarch", + "Name": "ca-certificates", + "Version": "2018.2.22", + "Release": "70.0.amzn2", + "Arch": "noarch", + "SrcName": "ca-certificates", + "SrcVersion": "2018.2.22", + "SrcRelease": "70.0.amzn2", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "chkconfig@1.7.4-1.amzn2.0.2.x86_64", + "Name": "chkconfig", + "Version": "1.7.4", + "Release": "1.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "chkconfig", + "SrcVersion": "1.7.4", + "SrcRelease": "1.amzn2.0.2", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "coreutils@8.22-21.amzn2.x86_64", + "Name": "coreutils", + "Version": "8.22", + "Release": "21.amzn2", + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "8.22", + "SrcRelease": "21.amzn2", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cpio@2.11-27.amzn2.x86_64", + "Name": "cpio", + "Version": "2.11", + "Release": "27.amzn2", + "Arch": "x86_64", + "SrcName": "cpio", + "SrcVersion": "2.11", + "SrcRelease": "27.amzn2", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "curl@7.61.1-9.amzn2.0.1.x86_64", + "Name": "curl", + "Version": "7.61.1", + "Release": "9.amzn2.0.1", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.61.1", + "SrcRelease": "9.amzn2.0.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cyrus-sasl-lib@2.1.26-23.amzn2.x86_64", + "Name": "cyrus-sasl-lib", + "Version": "2.1.26", + "Release": "23.amzn2", + "Arch": "x86_64", + "SrcName": "cyrus-sasl", + "SrcVersion": "2.1.26", + "SrcRelease": "23.amzn2", + "Licenses": [ + "BSD with advertising" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "diffutils@3.3-4.amzn2.0.2.x86_64", + "Name": "diffutils", + "Version": "3.3", + "Release": "4.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "diffutils", + "SrcVersion": "3.3", + "SrcRelease": "4.amzn2.0.2", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "elfutils-libelf@0.170-4.amzn2.x86_64", + "Name": "elfutils-libelf", + "Version": "0.170", + "Release": "4.amzn2", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.170", + "SrcRelease": "4.amzn2", + "Licenses": [ + "GPLv2+ or LGPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "expat@2.1.0-10.amzn2.0.2.x86_64", + "Name": "expat", + "Version": "2.1.0", + "Release": "10.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "expat", + "SrcVersion": "2.1.0", + "SrcRelease": "10.amzn2.0.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "file-libs@5.11-33.amzn2.0.2.x86_64", + "Name": "file-libs", + "Version": "5.11", + "Release": "33.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "file", + "SrcVersion": "5.11", + "SrcRelease": "33.amzn2.0.2", + "Licenses": [ + "BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "filesystem@3.2-25.amzn2.0.4.x86_64", + "Name": "filesystem", + "Version": "3.2", + "Release": "25.amzn2.0.4", + "Arch": "x86_64", + "SrcName": "filesystem", + "SrcVersion": "3.2", + "SrcRelease": "25.amzn2.0.4", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "findutils@4.5.11-5.amzn2.0.2.x86_64", + "Name": "findutils", + "Version": "4.5.11", + "Release": "5.amzn2.0.2", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "findutils", + "SrcVersion": "4.5.11", + "SrcRelease": "5.amzn2.0.2", + "SrcEpoch": 1, + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gawk@4.0.2-4.amzn2.1.2.x86_64", + "Name": "gawk", + "Version": "4.0.2", + "Release": "4.amzn2.1.2", + "Arch": "x86_64", + "SrcName": "gawk", + "SrcVersion": "4.0.2", + "SrcRelease": "4.amzn2.1.2", + "Licenses": [ + "GPLv3+ and GPL and LGPLv3+ and LGPL and BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gdbm@1.13-6.amzn2.0.2.x86_64", + "Name": "gdbm", + "Version": "1.13", + "Release": "6.amzn2.0.2", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "gdbm", + "SrcVersion": "1.13", + "SrcRelease": "6.amzn2.0.2", + "SrcEpoch": 1, + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glib2@2.54.2-2.amzn2.x86_64", + "Name": "glib2", + "Version": "2.54.2", + "Release": "2.amzn2", + "Arch": "x86_64", + "SrcName": "glib2", + "SrcVersion": "2.54.2", + "SrcRelease": "2.amzn2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc@2.26-32.amzn2.0.1.x86_64", + "Name": "glibc", + "Version": "2.26", + "Release": "32.amzn2.0.1", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.26", + "SrcRelease": "32.amzn2.0.1", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc-common@2.26-32.amzn2.0.1.x86_64", + "Name": "glibc-common", + "Version": "2.26", + "Release": "32.amzn2.0.1", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.26", + "SrcRelease": "32.amzn2.0.1", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc-langpack-en@2.26-32.amzn2.0.1.x86_64", + "Name": "glibc-langpack-en", + "Version": "2.26", + "Release": "32.amzn2.0.1", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.26", + "SrcRelease": "32.amzn2.0.1", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc-minimal-langpack@2.26-32.amzn2.0.1.x86_64", + "Name": "glibc-minimal-langpack", + "Version": "2.26", + "Release": "32.amzn2.0.1", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.26", + "SrcRelease": "32.amzn2.0.1", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gmp@6.0.0-15.amzn2.0.2.x86_64", + "Name": "gmp", + "Version": "6.0.0", + "Release": "15.amzn2.0.2", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "gmp", + "SrcVersion": "6.0.0", + "SrcRelease": "15.amzn2.0.2", + "SrcEpoch": 1, + "Licenses": [ + "LGPLv3+ or GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gnupg2@2.0.22-5.amzn2.0.3.x86_64", + "Name": "gnupg2", + "Version": "2.0.22", + "Release": "5.amzn2.0.3", + "Arch": "x86_64", + "SrcName": "gnupg2", + "SrcVersion": "2.0.22", + "SrcRelease": "5.amzn2.0.3", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@c87f5b1a-593863f8.", + "Name": "gpg-pubkey", + "Version": "c87f5b1a", + "Release": "593863f8", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpgme@1.3.2-5.amzn2.0.2.x86_64", + "Name": "gpgme", + "Version": "1.3.2", + "Release": "5.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "gpgme", + "SrcVersion": "1.3.2", + "SrcRelease": "5.amzn2.0.2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "grep@2.20-3.amzn2.0.2.x86_64", + "Name": "grep", + "Version": "2.20", + "Release": "3.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "grep", + "SrcVersion": "2.20", + "SrcRelease": "3.amzn2.0.2", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "info@5.1-5.amzn2.x86_64", + "Name": "info", + "Version": "5.1", + "Release": "5.amzn2", + "Arch": "x86_64", + "SrcName": "texinfo", + "SrcVersion": "5.1", + "SrcRelease": "5.amzn2", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "keyutils-libs@1.5.8-3.amzn2.0.2.x86_64", + "Name": "keyutils-libs", + "Version": "1.5.8", + "Release": "3.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "keyutils", + "SrcVersion": "1.5.8", + "SrcRelease": "3.amzn2.0.2", + "Licenses": [ + "GPLv2+ and LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "krb5-libs@1.15.1-20.amzn2.0.1.x86_64", + "Name": "krb5-libs", + "Version": "1.15.1", + "Release": "20.amzn2.0.1", + "Arch": "x86_64", + "SrcName": "krb5", + "SrcVersion": "1.15.1", + "SrcRelease": "20.amzn2.0.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libacl@2.2.51-14.amzn2.x86_64", + "Name": "libacl", + "Version": "2.2.51", + "Release": "14.amzn2", + "Arch": "x86_64", + "SrcName": "acl", + "SrcVersion": "2.2.51", + "SrcRelease": "14.amzn2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libassuan@2.1.0-3.amzn2.0.2.x86_64", + "Name": "libassuan", + "Version": "2.1.0", + "Release": "3.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libassuan", + "SrcVersion": "2.1.0", + "SrcRelease": "3.amzn2.0.2", + "Licenses": [ + "LGPLv2+ and GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libattr@2.4.46-12.amzn2.0.2.x86_64", + "Name": "libattr", + "Version": "2.4.46", + "Release": "12.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "attr", + "SrcVersion": "2.4.46", + "SrcRelease": "12.amzn2.0.2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libblkid@2.30.2-2.amzn2.0.4.x86_64", + "Name": "libblkid", + "Version": "2.30.2", + "Release": "2.amzn2.0.4", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.30.2", + "SrcRelease": "2.amzn2.0.4", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap@2.22-9.amzn2.0.2.x86_64", + "Name": "libcap", + "Version": "2.22", + "Release": "9.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libcap", + "SrcVersion": "2.22", + "SrcRelease": "9.amzn2.0.2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcom_err@1.42.9-12.amzn2.0.2.x86_64", + "Name": "libcom_err", + "Version": "1.42.9", + "Release": "12.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.42.9", + "SrcRelease": "12.amzn2.0.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcrypt@2.26-32.amzn2.0.1.x86_64", + "Name": "libcrypt", + "Version": "2.26", + "Release": "32.amzn2.0.1", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.26", + "SrcRelease": "32.amzn2.0.1", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcurl@7.61.1-9.amzn2.0.1.x86_64", + "Name": "libcurl", + "Version": "7.61.1", + "Release": "9.amzn2.0.1", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.61.1", + "SrcRelease": "9.amzn2.0.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libdb@5.3.21-24.amzn2.0.3.x86_64", + "Name": "libdb", + "Version": "5.3.21", + "Release": "24.amzn2.0.3", + "Arch": "x86_64", + "SrcName": "libdb", + "SrcVersion": "5.3.21", + "SrcRelease": "24.amzn2.0.3", + "Licenses": [ + "BSD and LGPLv2 and Sleepycat" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libdb-utils@5.3.21-24.amzn2.0.3.x86_64", + "Name": "libdb-utils", + "Version": "5.3.21", + "Release": "24.amzn2.0.3", + "Arch": "x86_64", + "SrcName": "libdb", + "SrcVersion": "5.3.21", + "SrcRelease": "24.amzn2.0.3", + "Licenses": [ + "BSD and LGPLv2 and Sleepycat" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libffi@3.0.13-18.amzn2.0.2.x86_64", + "Name": "libffi", + "Version": "3.0.13", + "Release": "18.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libffi", + "SrcVersion": "3.0.13", + "SrcRelease": "18.amzn2.0.2", + "Licenses": [ + "MIT and Public Domain" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcc@7.3.1-5.amzn2.0.2.x86_64", + "Name": "libgcc", + "Version": "7.3.1", + "Release": "5.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "7.3.1", + "SrcRelease": "5.amzn2.0.2", + "Licenses": [ + "GPLv3+ and GPLv3+ with exceptions and GPLv2+ with exceptions and LGPLv2+ and BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcrypt@1.5.3-14.amzn2.0.2.x86_64", + "Name": "libgcrypt", + "Version": "1.5.3", + "Release": "14.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libgcrypt", + "SrcVersion": "1.5.3", + "SrcRelease": "14.amzn2.0.2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgpg-error@1.12-3.amzn2.0.3.x86_64", + "Name": "libgpg-error", + "Version": "1.12", + "Release": "3.amzn2.0.3", + "Arch": "x86_64", + "SrcName": "libgpg-error", + "SrcVersion": "1.12", + "SrcRelease": "3.amzn2.0.3", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libidn2@2.0.4-1.amzn2.0.2.x86_64", + "Name": "libidn2", + "Version": "2.0.4", + "Release": "1.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libidn2", + "SrcVersion": "2.0.4", + "SrcRelease": "1.amzn2.0.2", + "Licenses": [ + "(GPLv2+ or LGPLv3+) and GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmetalink@0.1.2-7.amzn2.0.2.x86_64", + "Name": "libmetalink", + "Version": "0.1.2", + "Release": "7.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libmetalink", + "SrcVersion": "0.1.2", + "SrcRelease": "7.amzn2.0.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmount@2.30.2-2.amzn2.0.4.x86_64", + "Name": "libmount", + "Version": "2.30.2", + "Release": "2.amzn2.0.4", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.30.2", + "SrcRelease": "2.amzn2.0.4", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnghttp2@1.31.1-1.amzn2.0.2.x86_64", + "Name": "libnghttp2", + "Version": "1.31.1", + "Release": "1.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "nghttp2", + "SrcVersion": "1.31.1", + "SrcRelease": "1.amzn2.0.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libselinux@2.5-12.amzn2.0.2.x86_64", + "Name": "libselinux", + "Version": "2.5", + "Release": "12.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libselinux", + "SrcVersion": "2.5", + "SrcRelease": "12.amzn2.0.2", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsepol@2.5-8.1.amzn2.0.2.x86_64", + "Name": "libsepol", + "Version": "2.5", + "Release": "8.1.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libsepol", + "SrcVersion": "2.5", + "SrcRelease": "8.1.amzn2.0.2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libssh2@1.4.3-12.amzn2.2.x86_64", + "Name": "libssh2", + "Version": "1.4.3", + "Release": "12.amzn2.2", + "Arch": "x86_64", + "SrcName": "libssh2", + "SrcVersion": "1.4.3", + "SrcRelease": "12.amzn2.2", + "Licenses": [ + "BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libstdc++@7.3.1-5.amzn2.0.2.x86_64", + "Name": "libstdc++", + "Version": "7.3.1", + "Release": "5.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "7.3.1", + "SrcRelease": "5.amzn2.0.2", + "Licenses": [ + "GPLv3+ and GPLv3+ with exceptions and GPLv2+ with exceptions and LGPLv2+ and BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtasn1@4.10-1.amzn2.0.2.x86_64", + "Name": "libtasn1", + "Version": "4.10", + "Release": "1.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "4.10", + "SrcRelease": "1.amzn2.0.2", + "Licenses": [ + "GPLv3+ and LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libunistring@0.9.3-9.amzn2.0.2.x86_64", + "Name": "libunistring", + "Version": "0.9.3", + "Release": "9.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libunistring", + "SrcVersion": "0.9.3", + "SrcRelease": "9.amzn2.0.2", + "Licenses": [ + "LGPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libuuid@2.30.2-2.amzn2.0.4.x86_64", + "Name": "libuuid", + "Version": "2.30.2", + "Release": "2.amzn2.0.4", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.30.2", + "SrcRelease": "2.amzn2.0.4", + "Licenses": [ + "BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libverto@0.2.5-4.amzn2.0.2.x86_64", + "Name": "libverto", + "Version": "0.2.5", + "Release": "4.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "libverto", + "SrcVersion": "0.2.5", + "SrcRelease": "4.amzn2.0.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libxml2@2.9.1-6.amzn2.3.2.x86_64", + "Name": "libxml2", + "Version": "2.9.1", + "Release": "6.amzn2.3.2", + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.9.1", + "SrcRelease": "6.amzn2.3.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "lua@5.1.4-15.amzn2.0.2.x86_64", + "Name": "lua", + "Version": "5.1.4", + "Release": "15.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "lua", + "SrcVersion": "5.1.4", + "SrcRelease": "15.amzn2.0.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses@6.0-8.20170212.amzn2.1.2.x86_64", + "Name": "ncurses", + "Version": "6.0", + "Release": "8.20170212.amzn2.1.2", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.0", + "SrcRelease": "8.20170212.amzn2.1.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-base@6.0-8.20170212.amzn2.1.2.noarch", + "Name": "ncurses-base", + "Version": "6.0", + "Release": "8.20170212.amzn2.1.2", + "Arch": "noarch", + "SrcName": "ncurses", + "SrcVersion": "6.0", + "SrcRelease": "8.20170212.amzn2.1.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-libs@6.0-8.20170212.amzn2.1.2.x86_64", + "Name": "ncurses-libs", + "Version": "6.0", + "Release": "8.20170212.amzn2.1.2", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.0", + "SrcRelease": "8.20170212.amzn2.1.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nspr@4.19.0-1.amzn2.x86_64", + "Name": "nspr", + "Version": "4.19.0", + "Release": "1.amzn2", + "Arch": "x86_64", + "SrcName": "nspr", + "SrcVersion": "4.19.0", + "SrcRelease": "1.amzn2", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss@3.36.0-7.amzn2.x86_64", + "Name": "nss", + "Version": "3.36.0", + "Release": "7.amzn2", + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.36.0", + "SrcRelease": "7.amzn2", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-pem@1.0.3-5.amzn2.x86_64", + "Name": "nss-pem", + "Version": "1.0.3", + "Release": "5.amzn2", + "Arch": "x86_64", + "SrcName": "nss-pem", + "SrcVersion": "1.0.3", + "SrcRelease": "5.amzn2", + "Licenses": [ + "MPLv1.1" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-softokn@3.36.0-5.amzn2.x86_64", + "Name": "nss-softokn", + "Version": "3.36.0", + "Release": "5.amzn2", + "Arch": "x86_64", + "SrcName": "nss-softokn", + "SrcVersion": "3.36.0", + "SrcRelease": "5.amzn2", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-softokn-freebl@3.36.0-5.amzn2.x86_64", + "Name": "nss-softokn-freebl", + "Version": "3.36.0", + "Release": "5.amzn2", + "Arch": "x86_64", + "SrcName": "nss-softokn", + "SrcVersion": "3.36.0", + "SrcRelease": "5.amzn2", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-sysinit@3.36.0-7.amzn2.x86_64", + "Name": "nss-sysinit", + "Version": "3.36.0", + "Release": "7.amzn2", + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.36.0", + "SrcRelease": "7.amzn2", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-tools@3.36.0-7.amzn2.x86_64", + "Name": "nss-tools", + "Version": "3.36.0", + "Release": "7.amzn2", + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.36.0", + "SrcRelease": "7.amzn2", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-util@3.36.0-1.amzn2.x86_64", + "Name": "nss-util", + "Version": "3.36.0", + "Release": "1.amzn2", + "Arch": "x86_64", + "SrcName": "nss-util", + "SrcVersion": "3.36.0", + "SrcRelease": "1.amzn2", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openldap@2.4.44-15.amzn2.x86_64", + "Name": "openldap", + "Version": "2.4.44", + "Release": "15.amzn2", + "Arch": "x86_64", + "SrcName": "openldap", + "SrcVersion": "2.4.44", + "SrcRelease": "15.amzn2", + "Licenses": [ + "OpenLDAP" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openssl-libs@1.0.2k-16.amzn2.1.1.x86_64", + "Name": "openssl-libs", + "Version": "1.0.2k", + "Release": "16.amzn2.1.1", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "openssl", + "SrcVersion": "1.0.2k", + "SrcRelease": "16.amzn2.1.1", + "SrcEpoch": 1, + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit@0.23.5-3.amzn2.0.2.x86_64", + "Name": "p11-kit", + "Version": "0.23.5", + "Release": "3.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.5", + "SrcRelease": "3.amzn2.0.2", + "Licenses": [ + "BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit-trust@0.23.5-3.amzn2.0.2.x86_64", + "Name": "p11-kit-trust", + "Version": "0.23.5", + "Release": "3.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.5", + "SrcRelease": "3.amzn2.0.2", + "Licenses": [ + "BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pcre@8.32-17.amzn2.0.2.x86_64", + "Name": "pcre", + "Version": "8.32", + "Release": "17.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "pcre", + "SrcVersion": "8.32", + "SrcRelease": "17.amzn2.0.2", + "Licenses": [ + "BSD" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pinentry@0.8.1-17.amzn2.0.2.x86_64", + "Name": "pinentry", + "Version": "0.8.1", + "Release": "17.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "pinentry", + "SrcVersion": "0.8.1", + "SrcRelease": "17.amzn2.0.2", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "popt@1.13-16.amzn2.0.2.x86_64", + "Name": "popt", + "Version": "1.13", + "Release": "16.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "popt", + "SrcVersion": "1.13", + "SrcRelease": "16.amzn2.0.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pth@2.0.7-23.amzn2.0.2.x86_64", + "Name": "pth", + "Version": "2.0.7", + "Release": "23.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "pth", + "SrcVersion": "2.0.7", + "SrcRelease": "23.amzn2.0.2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pygpgme@0.3-9.amzn2.0.2.x86_64", + "Name": "pygpgme", + "Version": "0.3", + "Release": "9.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "pygpgme", + "SrcVersion": "0.3", + "SrcRelease": "9.amzn2.0.2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pyliblzma@0.5.3-11.amzn2.0.2.x86_64", + "Name": "pyliblzma", + "Version": "0.5.3", + "Release": "11.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "pyliblzma", + "SrcVersion": "0.5.3", + "SrcRelease": "11.amzn2.0.2", + "Licenses": [ + "LGPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python@2.7.14-58.amzn2.0.4.x86_64", + "Name": "python", + "Version": "2.7.14", + "Release": "58.amzn2.0.4", + "Arch": "x86_64", + "SrcName": "python", + "SrcVersion": "2.7.14", + "SrcRelease": "58.amzn2.0.4", + "Licenses": [ + "Python" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-iniparse@0.4-9.amzn2.noarch", + "Name": "python-iniparse", + "Version": "0.4", + "Release": "9.amzn2", + "Arch": "noarch", + "SrcName": "python-iniparse", + "SrcVersion": "0.4", + "SrcRelease": "9.amzn2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-libs@2.7.14-58.amzn2.0.4.x86_64", + "Name": "python-libs", + "Version": "2.7.14", + "Release": "58.amzn2.0.4", + "Arch": "x86_64", + "SrcName": "python", + "SrcVersion": "2.7.14", + "SrcRelease": "58.amzn2.0.4", + "Licenses": [ + "Python" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-pycurl@7.19.0-19.amzn2.0.2.x86_64", + "Name": "python-pycurl", + "Version": "7.19.0", + "Release": "19.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "python-pycurl", + "SrcVersion": "7.19.0", + "SrcRelease": "19.amzn2.0.2", + "Licenses": [ + "LGPLv2+ or MIT" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-urlgrabber@3.10-8.amzn2.noarch", + "Name": "python-urlgrabber", + "Version": "3.10", + "Release": "8.amzn2", + "Arch": "noarch", + "SrcName": "python-urlgrabber", + "SrcVersion": "3.10", + "SrcRelease": "8.amzn2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pyxattr@0.5.1-5.amzn2.0.2.x86_64", + "Name": "pyxattr", + "Version": "0.5.1", + "Release": "5.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "pyxattr", + "SrcVersion": "0.5.1", + "SrcRelease": "5.amzn2.0.2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "readline@6.2-10.amzn2.0.2.x86_64", + "Name": "readline", + "Version": "6.2", + "Release": "10.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "readline", + "SrcVersion": "6.2", + "SrcRelease": "10.amzn2.0.2", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm@4.11.3-25.amzn2.0.3.x86_64", + "Name": "rpm", + "Version": "4.11.3", + "Release": "25.amzn2.0.3", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.11.3", + "SrcRelease": "25.amzn2.0.3", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-build-libs@4.11.3-25.amzn2.0.3.x86_64", + "Name": "rpm-build-libs", + "Version": "4.11.3", + "Release": "25.amzn2.0.3", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.11.3", + "SrcRelease": "25.amzn2.0.3", + "Licenses": [ + "GPLv2+ and LGPLv2+ with exceptions" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-libs@4.11.3-25.amzn2.0.3.x86_64", + "Name": "rpm-libs", + "Version": "4.11.3", + "Release": "25.amzn2.0.3", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.11.3", + "SrcRelease": "25.amzn2.0.3", + "Licenses": [ + "GPLv2+ and LGPLv2+ with exceptions" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-python@4.11.3-25.amzn2.0.3.x86_64", + "Name": "rpm-python", + "Version": "4.11.3", + "Release": "25.amzn2.0.3", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.11.3", + "SrcRelease": "25.amzn2.0.3", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sed@4.2.2-5.amzn2.0.2.x86_64", + "Name": "sed", + "Version": "4.2.2", + "Release": "5.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "sed", + "SrcVersion": "4.2.2", + "SrcRelease": "5.amzn2.0.2", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "setup@2.8.71-10.amzn2.noarch", + "Name": "setup", + "Version": "2.8.71", + "Release": "10.amzn2", + "Arch": "noarch", + "SrcName": "setup", + "SrcVersion": "2.8.71", + "SrcRelease": "10.amzn2", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "shared-mime-info@1.8-4.amzn2.x86_64", + "Name": "shared-mime-info", + "Version": "1.8", + "Release": "4.amzn2", + "Arch": "x86_64", + "SrcName": "shared-mime-info", + "SrcVersion": "1.8", + "SrcRelease": "4.amzn2", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sqlite@3.7.17-8.amzn2.0.2.x86_64", + "Name": "sqlite", + "Version": "3.7.17", + "Release": "8.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "sqlite", + "SrcVersion": "3.7.17", + "SrcRelease": "8.amzn2.0.2", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "system-release@2-10.amzn2.x86_64", + "Name": "system-release", + "Version": "2", + "Release": "10.amzn2", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "system-release", + "SrcVersion": "2", + "SrcRelease": "10.amzn2", + "SrcEpoch": 1, + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "tzdata@2018i-1.amzn2.noarch", + "Name": "tzdata", + "Version": "2018i", + "Release": "1.amzn2", + "Arch": "noarch", + "SrcName": "tzdata", + "SrcVersion": "2018i", + "SrcRelease": "1.amzn2", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "vim-minimal@7.4.160-4.amzn2.0.16.x86_64", + "Name": "vim-minimal", + "Version": "7.4.160", + "Release": "4.amzn2.0.16", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "vim", + "SrcVersion": "7.4.160", + "SrcRelease": "4.amzn2.0.16", + "SrcEpoch": 2, + "Licenses": [ + "Vim" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "xz-libs@5.2.2-1.amzn2.0.2.x86_64", + "Name": "xz-libs", + "Version": "5.2.2", + "Release": "1.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.2", + "SrcRelease": "1.amzn2.0.2", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "yum@3.4.3-158.amzn2.0.2.noarch", + "Name": "yum", + "Version": "3.4.3", + "Release": "158.amzn2.0.2", + "Arch": "noarch", + "SrcName": "yum", + "SrcVersion": "3.4.3", + "SrcRelease": "158.amzn2.0.2", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "yum-metadata-parser@1.1.4-10.amzn2.0.2.x86_64", + "Name": "yum-metadata-parser", + "Version": "1.1.4", + "Release": "10.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "yum-metadata-parser", + "SrcVersion": "1.1.4", + "SrcRelease": "10.amzn2.0.2", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "yum-plugin-ovl@1.1.31-46.amzn2.0.1.noarch", + "Name": "yum-plugin-ovl", + "Version": "1.1.31", + "Release": "46.amzn2.0.1", + "Arch": "noarch", + "SrcName": "yum-utils", + "SrcVersion": "1.1.31", + "SrcRelease": "46.amzn2.0.1", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "yum-plugin-priorities@1.1.31-46.amzn2.0.1.noarch", + "Name": "yum-plugin-priorities", + "Version": "1.1.31", + "Release": "46.amzn2.0.1", + "Arch": "noarch", + "SrcName": "yum-utils", + "SrcVersion": "1.1.31", + "SrcRelease": "46.amzn2.0.1", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "zlib@1.2.7-17.amzn2.0.2.x86_64", + "Name": "zlib", + "Version": "1.2.7", + "Release": "17.amzn2.0.2", + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.7", + "SrcRelease": "17.amzn2.0.2", + "Licenses": [ + "zlib and Boost" + ], + "Maintainer": "Amazon Linux", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/centos-6.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/centos-6.json.golden new file mode 100644 index 000000000000..9fe3c3bb8b3b --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/centos-6.json.golden @@ -0,0 +1,1550 @@ +[ + { + "Name": "setup", + "Version": "2.8.14", + "Release": "23.el6", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "setup", + "SrcVersion": "2.8.14", + "SrcRelease": "23.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "basesystem", + "Version": "10.0", + "Release": "4.el6", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "basesystem", + "SrcVersion": "10.0", + "SrcRelease": "4.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "tzdata", + "Version": "2018e", + "Release": "3.el6", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "tzdata", + "SrcVersion": "2018e", + "SrcRelease": "3.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "glibc-common", + "Version": "2.12", + "Release": "1.212.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.12", + "SrcRelease": "1.212.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "glibc", + "Version": "2.12", + "Release": "1.212.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.12", + "SrcRelease": "1.212.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "bash", + "Version": "4.1.2", + "Release": "48.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.1.2", + "SrcRelease": "48.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libcap", + "Version": "2.16", + "Release": "5.5.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libcap", + "SrcVersion": "2.16", + "SrcRelease": "5.5.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "info", + "Version": "4.13a", + "Release": "8.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "texinfo", + "SrcVersion": "4.13a", + "SrcRelease": "8.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libacl", + "Version": "2.2.49", + "Release": "7.el6_9.1", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "acl", + "SrcVersion": "2.2.49", + "SrcRelease": "7.el6_9.1", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "nspr", + "Version": "4.19.0", + "Release": "1.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "nspr", + "SrcVersion": "4.19.0", + "SrcRelease": "1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libcom_err", + "Version": "1.41.12", + "Release": "24.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.41.12", + "SrcRelease": "24.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libsepol", + "Version": "2.0.41", + "Release": "4.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libsepol", + "SrcVersion": "2.0.41", + "SrcRelease": "4.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "chkconfig", + "Version": "1.3.49.5", + "Release": "1.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "chkconfig", + "SrcVersion": "1.3.49.5", + "SrcRelease": "1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "audit-libs", + "Version": "2.4.5", + "Release": "6.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "audit", + "SrcVersion": "2.4.5", + "SrcRelease": "6.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "readline", + "Version": "6.0", + "Release": "4.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "readline", + "SrcVersion": "6.0", + "SrcRelease": "4.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "file-libs", + "Version": "5.04", + "Release": "30.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "file", + "SrcVersion": "5.04", + "SrcRelease": "30.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "dbus-libs", + "Version": "1.2.24", + "Release": "9.el6", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "dbus", + "SrcVersion": "1.2.24", + "SrcRelease": "9.el6", + "SrcEpoch":1, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "sqlite", + "Version": "3.6.20", + "Release": "1.el6_7.2", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "sqlite", + "SrcVersion": "3.6.20", + "SrcRelease": "1.el6_7.2", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libuuid", + "Version": "2.17.2", + "Release": "12.28.el6_9.2", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "util-linux-ng", + "SrcVersion": "2.17.2", + "SrcRelease": "12.28.el6_9.2", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "xz-libs", + "Version": "4.999.9", + "Release": "0.5.beta.20091007git.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "4.999.9", + "SrcRelease": "0.5.beta.20091007git.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libgpg-error", + "Version": "1.7", + "Release": "4.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libgpg-error", + "SrcVersion": "1.7", + "SrcRelease": "4.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "pcre", + "Version": "7.8", + "Release": "7.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "pcre", + "SrcVersion": "7.8", + "SrcRelease": "7.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "findutils", + "Version": "4.4.2", + "Release": "9.el6", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "findutils", + "SrcVersion": "4.4.2", + "SrcRelease": "9.el6", + "SrcEpoch":1, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "expat", + "Version": "2.0.1", + "Release": "13.el6_8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "expat", + "SrcVersion": "2.0.1", + "SrcRelease": "13.el6_8", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "p11-kit", + "Version": "0.18.5", + "Release": "2.el6_5.2", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.18.5", + "SrcRelease": "2.el6_5.2", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libgcrypt", + "Version": "1.4.5", + "Release": "12.el6_8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libgcrypt", + "SrcVersion": "1.4.5", + "SrcRelease": "12.el6_8", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libusb", + "Version": "0.1.12", + "Release": "23.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libusb", + "SrcVersion": "0.1.12", + "SrcRelease": "23.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "pinentry", + "Version": "0.7.6", + "Release": "8.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "pinentry", + "SrcVersion": "0.7.6", + "SrcRelease": "8.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "procps", + "Version": "3.2.8", + "Release": "45.el6_9.3", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "procps", + "SrcVersion": "3.2.8", + "SrcRelease": "45.el6_9.3", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "tar", + "Version": "1.23", + "Release": "15.el6_8", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "tar", + "SrcVersion": "1.23", + "SrcRelease": "15.el6_8", + "SrcEpoch": 2, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "checkpolicy", + "Version": "2.0.22", + "Release": "1.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "checkpolicy", + "SrcVersion": "2.0.22", + "SrcRelease": "1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "db4-utils", + "Version": "4.7.25", + "Release": "22.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "db4", + "SrcVersion": "4.7.25", + "SrcRelease": "22.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "binutils", + "Version": "2.20.51.0.2", + "Release": "5.48.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "binutils", + "SrcVersion": "2.20.51.0.2", + "SrcRelease": "5.48.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "which", + "Version": "2.19", + "Release": "6.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "which", + "SrcVersion": "2.19", + "SrcRelease": "6.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "dash", + "Version": "0.5.5.1", + "Release": "4.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "dash", + "SrcVersion": "0.5.5.1", + "SrcRelease": "4.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "groff", + "Version": "1.18.1.4", + "Release": "21.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "groff", + "SrcVersion": "1.18.1.4", + "SrcRelease": "21.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "coreutils-libs", + "Version": "8.4", + "Release": "47.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "8.4", + "SrcRelease": "47.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "cracklib", + "Version": "2.8.16", + "Release": "4.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.8.16", + "SrcRelease": "4.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "coreutils", + "Version": "8.4", + "Release": "47.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "8.4", + "SrcRelease": "47.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "module-init-tools", + "Version": "3.9", + "Release": "26.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "module-init-tools", + "SrcVersion": "3.9", + "SrcRelease": "26.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "nss", + "Version": "3.36.0", + "Release": "8.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.36.0", + "SrcRelease": "8.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "nss-tools", + "Version": "3.36.0", + "Release": "8.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.36.0", + "SrcRelease": "8.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "mingetty", + "Version": "1.08", + "Release": "5.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "mingetty", + "SrcVersion": "1.08", + "SrcRelease": "5.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "krb5-libs", + "Version": "1.10.3", + "Release": "65.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "krb5", + "SrcVersion": "1.10.3", + "SrcRelease": "65.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libssh2", + "Version": "1.4.2", + "Release": "2.el6_7.1", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libssh2", + "SrcVersion": "1.4.2", + "SrcRelease": "2.el6_7.1", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "rpm-libs", + "Version": "4.8.0", + "Release": "59.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.8.0", + "SrcRelease": "59.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "rpm", + "Version": "4.8.0", + "Release": "59.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.8.0", + "SrcRelease": "59.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "gnupg2", + "Version": "2.0.14", + "Release": "9.el6_10", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gnupg2", + "SrcVersion": "2.0.14", + "SrcRelease": "9.el6_10", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "bind-libs", + "Version": "9.8.2", + "Release": "0.68.rc1.el6_10.1", + "Epoch": 32, + "Arch": "x86_64", + "SrcName": "bind", + "SrcVersion": "9.8.2", + "SrcRelease": "0.68.rc1.el6_10.1", + "SrcEpoch": 32, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libsemanage", + "Version": "2.0.43", + "Release": "5.1.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libsemanage", + "SrcVersion": "2.0.43", + "SrcRelease": "5.1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libutempter", + "Version": "1.1.5", + "Release": "4.1.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libutempter", + "SrcVersion": "1.1.5", + "SrcRelease": "4.1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "plymouth-core-libs", + "Version": "0.8.3", + "Release": "29.el6.centos", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "plymouth", + "SrcVersion": "0.8.3", + "SrcRelease": "29.el6.centos", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libffi", + "Version": "3.0.5", + "Release": "3.2.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libffi", + "SrcVersion": "3.0.5", + "SrcRelease": "3.2.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "python-libs", + "Version": "2.6.6", + "Release": "66.el6_8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "python", + "SrcVersion": "2.6.6", + "SrcRelease": "66.el6_8", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "pygpgme", + "Version": "0.1", + "Release": "18.20090824bzr68.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "pygpgme", + "SrcVersion": "0.1", + "SrcRelease": "18.20090824bzr68.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "python-urlgrabber", + "Version": "3.9.1", + "Release": "11.el6", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "python-urlgrabber", + "SrcVersion": "3.9.1", + "SrcRelease": "11.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "pkgconfig", + "Version": "0.23", + "Release": "9.1.el6", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "pkgconfig", + "SrcVersion": "0.23", + "SrcRelease": "9.1.el6", + "SrcEpoch":1, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "glib2", + "Version": "2.28.8", + "Release": "10.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "glib2", + "SrcVersion": "2.28.8", + "SrcRelease": "10.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "yum-metadata-parser", + "Version": "1.1.2", + "Release": "16.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "yum-metadata-parser", + "SrcVersion": "1.1.2", + "SrcRelease": "16.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "yum", + "Version": "3.2.29", + "Release": "81.el6.centos", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "yum", + "SrcVersion": "3.2.29", + "SrcRelease": "81.el6.centos", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "passwd", + "Version": "0.77", + "Release": "7.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "passwd", + "SrcVersion": "0.77", + "SrcRelease": "7.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "bind-utils", + "Version": "9.8.2", + "Release": "0.68.rc1.el6_10.1", + "Epoch": 32, + "Arch": "x86_64", + "SrcName": "bind", + "SrcVersion": "9.8.2", + "SrcRelease": "0.68.rc1.el6_10.1", + "SrcEpoch": 32, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "vim-minimal", + "Version": "7.4.629", + "Release": "5.el6_8.1", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "vim", + "SrcVersion": "7.4.629", + "SrcRelease": "5.el6_8.1", + "SrcEpoch": 2, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libgcc", + "Version": "4.4.7", + "Release": "23.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "4.4.7", + "SrcRelease": "23.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "filesystem", + "Version": "2.4.30", + "Release": "3.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "filesystem", + "SrcVersion": "2.4.30", + "SrcRelease": "3.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "ncurses-base", + "Version": "5.7", + "Release": "4.20090207.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "5.7", + "SrcRelease": "4.20090207.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "nss-softokn-freebl", + "Version": "3.14.3", + "Release": "23.3.el6_8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "nss-softokn", + "SrcVersion": "3.14.3", + "SrcRelease": "23.3.el6_8", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "ncurses-libs", + "Version": "5.7", + "Release": "4.20090207.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "5.7", + "SrcRelease": "4.20090207.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libattr", + "Version": "2.4.44", + "Release": "7.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "attr", + "SrcVersion": "2.4.44", + "SrcRelease": "7.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "zlib", + "Version": "1.2.3", + "Release": "29.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.3", + "SrcRelease": "29.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "popt", + "Version": "1.13", + "Release": "7.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "popt", + "SrcVersion": "1.13", + "SrcRelease": "7.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "db4", + "Version": "4.7.25", + "Release": "22.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "db4", + "SrcVersion": "4.7.25", + "SrcRelease": "22.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "nss-util", + "Version": "3.36.0", + "Release": "1.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "nss-util", + "SrcVersion": "3.36.0", + "SrcRelease": "1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "bzip2-libs", + "Version": "1.0.5", + "Release": "7.el6_0", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.5", + "SrcRelease": "7.el6_0", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libselinux", + "Version": "2.0.94", + "Release": "7.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libselinux", + "SrcVersion": "2.0.94", + "SrcRelease": "7.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "sed", + "Version": "4.2.1", + "Release": "10.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "sed", + "SrcVersion": "4.2.1", + "SrcRelease": "10.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libidn", + "Version": "1.18", + "Release": "2.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libidn", + "SrcVersion": "1.18", + "SrcRelease": "2.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libxml2", + "Version": "2.7.6", + "Release": "21.el6_8.1", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.7.6", + "SrcRelease": "21.el6_8.1", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libstdc++", + "Version": "4.4.7", + "Release": "23.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "4.4.7", + "SrcRelease": "23.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "lua", + "Version": "5.1.4", + "Release": "4.1.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "lua", + "SrcVersion": "5.1.4", + "SrcRelease": "4.1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "gawk", + "Version": "3.1.7", + "Release": "10.el6_7.3", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gawk", + "SrcVersion": "3.1.7", + "SrcRelease": "10.el6_7.3", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libblkid", + "Version": "2.17.2", + "Release": "12.28.el6_9.2", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "util-linux-ng", + "SrcVersion": "2.17.2", + "SrcRelease": "12.28.el6_9.2", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "elfutils-libelf", + "Version": "0.164", + "Release": "2.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.164", + "SrcRelease": "2.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "nss-softokn", + "Version": "3.14.3", + "Release": "23.3.el6_8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "nss-softokn", + "SrcVersion": "3.14.3", + "SrcRelease": "23.3.el6_8", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "grep", + "Version": "2.20", + "Release": "6.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "grep", + "SrcVersion": "2.20", + "SrcRelease": "6.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "cpio", + "Version": "2.10", + "Release": "13.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "cpio", + "SrcVersion": "2.10", + "SrcRelease": "13.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "pth", + "Version": "2.0.7", + "Release": "9.3.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "pth", + "SrcVersion": "2.0.7", + "SrcRelease": "9.3.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libtasn1", + "Version": "2.3", + "Release": "6.el6_5", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "2.3", + "SrcRelease": "6.el6_5", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "p11-kit-trust", + "Version": "0.18.5", + "Release": "2.el6_5.2", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.18.5", + "SrcRelease": "2.el6_5.2", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libnih", + "Version": "1.0.1", + "Release": "8.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libnih", + "SrcVersion": "1.0.1", + "SrcRelease": "8.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "gmp", + "Version": "4.3.1", + "Release": "13.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gmp", + "SrcVersion": "4.3.1", + "SrcRelease": "13.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "file", + "Version": "5.04", + "Release": "30.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "file", + "SrcVersion": "5.04", + "SrcRelease": "30.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "net-tools", + "Version": "1.60", + "Release": "114.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "net-tools", + "SrcVersion": "1.60", + "SrcRelease": "114.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "psmisc", + "Version": "22.6", + "Release": "24.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "psmisc", + "SrcVersion": "22.6", + "SrcRelease": "24.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libselinux-utils", + "Version": "2.0.94", + "Release": "7.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libselinux", + "SrcVersion": "2.0.94", + "SrcRelease": "7.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "bzip2", + "Version": "1.0.5", + "Release": "7.el6_0", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.5", + "SrcRelease": "7.el6_0", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "cyrus-sasl-lib", + "Version": "2.1.23", + "Release": "15.el6_6.2", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "cyrus-sasl", + "SrcVersion": "2.1.23", + "SrcRelease": "15.el6_6.2", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "make", + "Version": "3.81", + "Release": "23.el6", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "make", + "SrcVersion": "3.81", + "SrcRelease": "23.el6", + "SrcEpoch":1, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "diffutils", + "Version": "2.8.1", + "Release": "28.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "diffutils", + "SrcVersion": "2.8.1", + "SrcRelease": "28.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "ncurses", + "Version": "5.7", + "Release": "4.20090207.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "5.7", + "SrcRelease": "4.20090207.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "less", + "Version": "436", + "Release": "13.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "less", + "SrcVersion": "436", + "SrcRelease": "13.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "gzip", + "Version": "1.3.12", + "Release": "24.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gzip", + "SrcVersion": "1.3.12", + "SrcRelease": "24.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "cracklib-dicts", + "Version": "2.8.16", + "Release": "4.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.8.16", + "SrcRelease": "4.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "pam", + "Version": "1.1.1", + "Release": "24.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "pam", + "SrcVersion": "1.1.1", + "SrcRelease": "24.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "plymouth-scripts", + "Version": "0.8.3", + "Release": "29.el6.centos", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "plymouth", + "SrcVersion": "0.8.3", + "SrcRelease": "29.el6.centos", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "ca-certificates", + "Version": "2018.2.22", + "Release": "65.1.el6", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "ca-certificates", + "SrcVersion": "2018.2.22", + "SrcRelease": "65.1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "nss-sysinit", + "Version": "3.36.0", + "Release": "8.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.36.0", + "SrcRelease": "8.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "ethtool", + "Version": "3.5", + "Release": "6.el6", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "ethtool", + "SrcVersion": "3.5", + "SrcRelease": "6.el6", + "SrcEpoch": 2, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "keyutils-libs", + "Version": "1.4", + "Release": "5.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "keyutils", + "SrcVersion": "1.4", + "SrcRelease": "5.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "openssl", + "Version": "1.0.1e", + "Release": "57.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "openssl", + "SrcVersion": "1.0.1e", + "SrcRelease": "57.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libcurl", + "Version": "7.19.7", + "Release": "53.el6_9", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.19.7", + "SrcRelease": "53.el6_9", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "curl", + "Version": "7.19.7", + "Release": "53.el6_9", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.19.7", + "SrcRelease": "53.el6_9", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "openldap", + "Version": "2.4.40", + "Release": "16.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "openldap", + "SrcVersion": "2.4.40", + "SrcRelease": "16.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "gpgme", + "Version": "1.1.8", + "Release": "3.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gpgme", + "SrcVersion": "1.1.8", + "SrcRelease": "3.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "ustr", + "Version": "1.0.4", + "Release": "9.1.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "ustr", + "SrcVersion": "1.0.4", + "SrcRelease": "9.1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "shadow-utils", + "Version": "4.1.5.1", + "Release": "5.el6", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "shadow-utils", + "SrcVersion": "4.1.5.1", + "SrcRelease": "5.el6", + "SrcEpoch": 2, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "MAKEDEV", + "Version": "3.24", + "Release": "6.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "MAKEDEV", + "SrcVersion": "3.24", + "SrcRelease": "6.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "gdbm", + "Version": "1.8.0", + "Release": "39.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gdbm", + "SrcVersion": "1.8.0", + "SrcRelease": "39.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "python", + "Version": "2.6.6", + "Release": "66.el6_8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "python", + "SrcVersion": "2.6.6", + "SrcRelease": "66.el6_8", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "rpm-python", + "Version": "4.8.0", + "Release": "59.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.8.0", + "SrcRelease": "59.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "python-pycurl", + "Version": "7.19.0", + "Release": "9.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "python-pycurl", + "SrcVersion": "7.19.0", + "SrcRelease": "9.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "python-iniparse", + "Version": "0.3.1", + "Release": "2.1.el6", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "python-iniparse", + "SrcVersion": "0.3.1", + "SrcRelease": "2.1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "gamin", + "Version": "0.1.10", + "Release": "9.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gamin", + "SrcVersion": "0.1.10", + "SrcRelease": "9.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "shared-mime-info", + "Version": "0.70", + "Release": "6.el6", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "shared-mime-info", + "SrcVersion": "0.70", + "SrcRelease": "6.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "libuser", + "Version": "0.56.13", + "Release": "8.el6_7", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libuser", + "SrcVersion": "0.56.13", + "SrcRelease": "8.el6_7", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "yum-plugin-fastestmirror", + "Version": "1.1.30", + "Release": "42.el6_10", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "yum-utils", + "SrcVersion": "1.1.30", + "SrcRelease": "42.el6_10", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "centos-release", + "Version": "6", + "Release": "10.el6.centos.12.3", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "centos-release", + "SrcVersion": "6", + "SrcRelease": "10.el6.centos.12.3", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "yum-plugin-ovl", + "Version": "1.1.30", + "Release": "42.el6_10", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "yum-utils", + "SrcVersion": "1.1.30", + "SrcRelease": "42.el6_10", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + }, + { + "Name": "rootfiles", + "Version": "8.1", + "Release": "6.1.el6", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "rootfiles", + "SrcVersion": "8.1", + "SrcRelease": "6.1.el6", + "SrcEpoch":0, + "LayerID":"sha256:af6bf1987c2eb07d73f33836b0d8fd825d7c785273526b077e46780e8b4b2ae9" + } +] diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/debian-buster.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/debian-buster.json.golden new file mode 100644 index 000000000000..027523f0fa5e --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/debian-buster.json.golden @@ -0,0 +1,1870 @@ +[ + { + "ID": "adduser@3.118", + "Name": "adduser", + "Version": "3.118", + "Arch": "all", + "SrcName": "adduser", + "SrcVersion": "3.118", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Debian Adduser Developers \u003cadduser@packages.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "apt@1.8.2", + "Name": "apt", + "Version": "1.8.2", + "Arch": "amd64", + "SrcName": "apt", + "SrcVersion": "1.8.2", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "APT Development Team \u003cdeity@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "base-files@10.3+deb10u1", + "Name": "base-files", + "Version": "10.3+deb10u1", + "Arch": "amd64", + "SrcName": "base-files", + "SrcVersion": "10.3+deb10u1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Santiago Vila \u003csanvila@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "base-passwd@3.5.46", + "Name": "base-passwd", + "Version": "3.5.46", + "Arch": "amd64", + "SrcName": "base-passwd", + "SrcVersion": "3.5.46", + "Licenses": [ + "GPL-2.0-only", + "PD" + ], + "Maintainer": "Colin Watson \u003ccjwatson@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "bash@5.0-4", + "Name": "bash", + "Version": "5.0", + "Release": "4", + "Arch": "amd64", + "SrcName": "bash", + "SrcVersion": "5.0", + "SrcRelease": "4", + "Licenses": [ + "GPL-3.0-only" + ], + "Maintainer": "Matthias Klose \u003cdoko@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "bsdutils@1:2.33.1-0.1", + "Name": "bsdutils", + "Version": "2.33.1", + "Release": "0.1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "0.1", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "BSD-4-Clause", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "LGPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "LaMont Jones \u003clamont@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "coreutils@8.30-3", + "Name": "coreutils", + "Version": "8.30", + "Release": "3", + "Arch": "amd64", + "SrcName": "coreutils", + "SrcVersion": "8.30", + "SrcRelease": "3", + "Licenses": [ + "GPL-3.0-only" + ], + "Maintainer": "Michael Stone \u003cmstone@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "dash@0.5.10.2-5", + "Name": "dash", + "Version": "0.5.10.2", + "Release": "5", + "Arch": "amd64", + "SrcName": "dash", + "SrcVersion": "0.5.10.2", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Andrej Shadura \u003candrewsh@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "debconf@1.5.71", + "Name": "debconf", + "Version": "1.5.71", + "Arch": "all", + "SrcName": "debconf", + "SrcVersion": "1.5.71", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "Debconf Developers \u003cdebconf-devel@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "debian-archive-keyring@2019.1", + "Name": "debian-archive-keyring", + "Version": "2019.1", + "Arch": "all", + "SrcName": "debian-archive-keyring", + "SrcVersion": "2019.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Debian Release Team \u003cpackages@release.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "debianutils@4.8.6.1", + "Name": "debianutils", + "Version": "4.8.6.1", + "Arch": "amd64", + "SrcName": "debianutils", + "SrcVersion": "4.8.6.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Clint Adams \u003cclint@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "diffutils@1:3.7-3", + "Name": "diffutils", + "Version": "3.7", + "Release": "3", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "diffutils", + "SrcVersion": "3.7", + "SrcRelease": "3", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-or-later", + "GFDL-1.3-or-later" + ], + "Maintainer": "Santiago Vila \u003csanvila@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "dpkg@1.19.7", + "Name": "dpkg", + "Version": "1.19.7", + "Arch": "amd64", + "SrcName": "dpkg", + "SrcVersion": "1.19.7", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "BSD-2-Clause", + "public-domain-s-s-d", + "public-domain-md5" + ], + "Maintainer": "Dpkg Developers \u003cdebian-dpkg@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "e2fsprogs@1.44.5-1+deb10u1", + "Name": "e2fsprogs", + "Version": "1.44.5", + "Release": "1+deb10u1", + "Arch": "amd64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.5", + "SrcRelease": "1+deb10u1", + "Licenses": [ + "GPL-2.0-only", + "LGPL-2.0-only" + ], + "Maintainer": "Theodore Y. Ts'o \u003ctytso@mit.edu\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "fdisk@2.33.1-0.1", + "Name": "fdisk", + "Version": "2.33.1", + "Release": "0.1", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "0.1", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "BSD-4-Clause", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "LGPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "LaMont Jones \u003clamont@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "findutils@4.6.0+git+20190209-2", + "Name": "findutils", + "Version": "4.6.0+git+20190209", + "Release": "2", + "Arch": "amd64", + "SrcName": "findutils", + "SrcVersion": "4.6.0+git+20190209", + "SrcRelease": "2", + "Licenses": [ + "GPL-3.0-only", + "GFDL-1.3-only" + ], + "Maintainer": "Andreas Metzler \u003cametzler@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "gcc-8-base@8.3.0-6", + "Name": "gcc-8-base", + "Version": "8.3.0", + "Release": "6", + "Arch": "amd64", + "SrcName": "gcc-8", + "SrcVersion": "8.3.0", + "SrcRelease": "6", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-3.0-only", + "GFDL-1.2-only", + "GPL-2.0-only", + "Artistic-2.0", + "LGPL-2.0-or-later" + ], + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "gpgv@2.2.12-1+deb10u1", + "Name": "gpgv", + "Version": "2.2.12", + "Release": "1+deb10u1", + "Arch": "amd64", + "SrcName": "gnupg2", + "SrcVersion": "2.2.12", + "SrcRelease": "1+deb10u1", + "Licenses": [ + "GPL-3.0-or-later", + "permissive", + "LGPL-2.1-or-later", + "MIT", + "BSD-3-Clause", + "LGPL-3.0-or-later", + "RFC-Reference", + "TinySCHEME", + "CC0-1.0", + "GPL-3.0-only", + "LGPL-3.0-only", + "LGPL-2.1-only" + ], + "Maintainer": "Debian GnuPG Maintainers \u003cpkg-gnupg-maint@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "grep@3.3-1", + "Name": "grep", + "Version": "3.3", + "Release": "1", + "Arch": "amd64", + "SrcName": "grep", + "SrcVersion": "3.3", + "SrcRelease": "1", + "Licenses": [ + "GPL-3.0-or-later", + "GPL-3.0-only" + ], + "Maintainer": "Anibal Monsalve Salazar \u003canibal@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "gzip@1.9-3", + "Name": "gzip", + "Version": "1.9", + "Release": "3", + "Arch": "amd64", + "SrcName": "gzip", + "SrcVersion": "1.9", + "SrcRelease": "3", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Bdale Garbee \u003cbdale@gag.com\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "hostname@3.21", + "Name": "hostname", + "Version": "3.21", + "Arch": "amd64", + "SrcName": "hostname", + "SrcVersion": "3.21", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Michael Meskes \u003cmeskes@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "init-system-helpers@1.56+nmu1", + "Name": "init-system-helpers", + "Version": "1.56+nmu1", + "Arch": "all", + "SrcName": "init-system-helpers", + "SrcVersion": "1.56+nmu1", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Debian systemd Maintainers \u003cpkg-systemd-maintainers@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "iproute2@4.20.0-2", + "Name": "iproute2", + "Version": "4.20.0", + "Release": "2", + "Arch": "amd64", + "SrcName": "iproute2", + "SrcVersion": "4.20.0", + "SrcRelease": "2", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Alexander Wirt \u003cformorer@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "iputils-ping@3:20180629-2", + "Name": "iputils-ping", + "Version": "20180629", + "Release": "2", + "Epoch": 3, + "Arch": "amd64", + "SrcName": "iputils", + "SrcVersion": "20180629", + "SrcRelease": "2", + "SrcEpoch": 3, + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Noah Meyerhans \u003cnoahm@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libacl1@2.2.53-4", + "Name": "libacl1", + "Version": "2.2.53", + "Release": "4", + "Arch": "amd64", + "SrcName": "acl", + "SrcVersion": "2.2.53", + "SrcRelease": "4", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1-only" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libapt-pkg5.0@1.8.2", + "Name": "libapt-pkg5.0", + "Version": "1.8.2", + "Arch": "amd64", + "SrcName": "apt", + "SrcVersion": "1.8.2", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "APT Development Team \u003cdeity@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libattr1@1:2.4.48-4", + "Name": "libattr1", + "Version": "2.4.48", + "Release": "4", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "attr", + "SrcVersion": "2.4.48", + "SrcRelease": "4", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "LGPL-2.0-or-later", + "LGPL-2.1-only" + ], + "Maintainer": "Guillem Jover \u003cguillem@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libaudit-common@1:2.8.4-3", + "Name": "libaudit-common", + "Version": "2.8.4", + "Release": "3", + "Epoch": 1, + "Arch": "all", + "SrcName": "audit", + "SrcVersion": "2.8.4", + "SrcRelease": "3", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-only", + "LGPL-2.1-only", + "GPL-1.0-only" + ], + "Maintainer": "Laurent Bigonville \u003cbigon@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libaudit1@1:2.8.4-3", + "Name": "libaudit1", + "Version": "2.8.4", + "Release": "3", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "audit", + "SrcVersion": "2.8.4", + "SrcRelease": "3", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-only", + "LGPL-2.1-only", + "GPL-1.0-only" + ], + "Maintainer": "Laurent Bigonville \u003cbigon@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libblkid1@2.33.1-0.1", + "Name": "libblkid1", + "Version": "2.33.1", + "Release": "0.1", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "0.1", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "BSD-4-Clause", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "LGPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "LaMont Jones \u003clamont@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libbz2-1.0@1.0.6-9.2~deb10u1", + "Name": "libbz2-1.0", + "Version": "1.0.6", + "Release": "9.2~deb10u1", + "Arch": "amd64", + "SrcName": "bzip2", + "SrcVersion": "1.0.6", + "SrcRelease": "9.2~deb10u1", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only" + ], + "Maintainer": "Anibal Monsalve Salazar \u003canibal@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libc-bin@2.28-10", + "Name": "libc-bin", + "Version": "2.28", + "Release": "10", + "Arch": "amd64", + "SrcName": "glibc", + "SrcVersion": "2.28", + "SrcRelease": "10", + "Licenses": [ + "LGPL-2.1-only", + "GPL-2.0-only" + ], + "Maintainer": "GNU Libc Maintainers \u003cdebian-glibc@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libc6@2.28-10", + "Name": "libc6", + "Version": "2.28", + "Release": "10", + "Arch": "amd64", + "SrcName": "glibc", + "SrcVersion": "2.28", + "SrcRelease": "10", + "Licenses": [ + "LGPL-2.1-only", + "GPL-2.0-only" + ], + "Maintainer": "GNU Libc Maintainers \u003cdebian-glibc@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libcap-ng0@0.7.9-2", + "Name": "libcap-ng0", + "Version": "0.7.9", + "Release": "2", + "Arch": "amd64", + "SrcName": "libcap-ng", + "SrcVersion": "0.7.9", + "SrcRelease": "2", + "Licenses": [ + "LGPL-2.1-only", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "Maintainer": "Pierre Chifflier \u003cpollux@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libcap2@1:2.25-2", + "Name": "libcap2", + "Version": "2.25", + "Release": "2", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "libcap2", + "SrcVersion": "2.25", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-2.0-or-later" + ], + "Maintainer": "Christian Kastner \u003cckk@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libcap2-bin@1:2.25-2", + "Name": "libcap2-bin", + "Version": "2.25", + "Release": "2", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "libcap2", + "SrcVersion": "2.25", + "SrcRelease": "2", + "SrcEpoch": 1, + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-2.0-or-later" + ], + "Maintainer": "Christian Kastner \u003cckk@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libcom-err2@1.44.5-1+deb10u1", + "Name": "libcom-err2", + "Version": "1.44.5", + "Release": "1+deb10u1", + "Arch": "amd64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.5", + "SrcRelease": "1+deb10u1", + "Maintainer": "Theodore Y. Ts'o \u003ctytso@mit.edu\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libdb5.3@5.3.28+dfsg1-0.5", + "Name": "libdb5.3", + "Version": "5.3.28+dfsg1", + "Release": "0.5", + "Arch": "amd64", + "SrcName": "db5.3", + "SrcVersion": "5.3.28+dfsg1", + "SrcRelease": "0.5", + "Maintainer": "Debian Berkeley DB Team \u003cteam+bdb@tracker.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libdebconfclient0@0.249", + "Name": "libdebconfclient0", + "Version": "0.249", + "Arch": "amd64", + "SrcName": "cdebconf", + "SrcVersion": "0.249", + "Maintainer": "Debian Install System Team \u003cdebian-boot@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libelf1@0.176-1.1", + "Name": "libelf1", + "Version": "0.176", + "Release": "1.1", + "Arch": "amd64", + "SrcName": "elfutils", + "SrcVersion": "0.176", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-2.0-only", + "GPL-3.0-only", + "LGPL-2.0-or-later" + ], + "Maintainer": "Kurt Roeckx \u003ckurt@roeckx.be\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libext2fs2@1.44.5-1+deb10u1", + "Name": "libext2fs2", + "Version": "1.44.5", + "Release": "1+deb10u1", + "Arch": "amd64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.5", + "SrcRelease": "1+deb10u1", + "Licenses": [ + "GPL-2.0-only", + "LGPL-2.0-only" + ], + "Maintainer": "Theodore Y. Ts'o \u003ctytso@mit.edu\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libfdisk1@2.33.1-0.1", + "Name": "libfdisk1", + "Version": "2.33.1", + "Release": "0.1", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "0.1", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "BSD-4-Clause", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "LGPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "LaMont Jones \u003clamont@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libffi6@3.2.1-9", + "Name": "libffi6", + "Version": "3.2.1", + "Release": "9", + "Arch": "amd64", + "SrcName": "libffi", + "SrcVersion": "3.2.1", + "SrcRelease": "9", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libgcc1@1:8.3.0-6", + "Name": "libgcc1", + "Version": "8.3.0", + "Release": "6", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "gcc-8", + "SrcVersion": "8.3.0", + "SrcRelease": "6", + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libgcrypt20@1.8.4-5", + "Name": "libgcrypt20", + "Version": "1.8.4", + "Release": "5", + "Arch": "amd64", + "SrcName": "libgcrypt20", + "SrcVersion": "1.8.4", + "SrcRelease": "5", + "Licenses": [ + "LGPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Debian GnuTLS Maintainers \u003cpkg-gnutls-maint@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libgmp10@2:6.1.2+dfsg-4", + "Name": "libgmp10", + "Version": "6.1.2+dfsg", + "Release": "4", + "Epoch": 2, + "Arch": "amd64", + "SrcName": "gmp", + "SrcVersion": "6.1.2+dfsg", + "SrcRelease": "4", + "SrcEpoch": 2, + "Licenses": [ + "LGPL-3.0-only", + "GPL-2.0-only", + "GPL-3.0-only", + "GPL-2.0-or-later" + ], + "Maintainer": "Debian Science Team \u003cdebian-science-maintainers@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libgnutls30@3.6.7-4", + "Name": "libgnutls30", + "Version": "3.6.7", + "Release": "4", + "Arch": "amd64", + "SrcName": "gnutls28", + "SrcVersion": "3.6.7", + "SrcRelease": "4", + "Licenses": [ + "LGPL-2.1-only", + "LGPL-2.0-or-later", + "LGPL-3.0-only", + "GPL-2.0-or-later", + "GPL-3.0-only", + "GFDL-1.3-only", + "CC0-1.0", + "MIT", + "LGPL-3.0-or-later", + "Apache-2.0", + "GPL-3.0-or-later" + ], + "Maintainer": "Debian GnuTLS Maintainers \u003cpkg-gnutls-maint@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libgpg-error0@1.35-1", + "Name": "libgpg-error0", + "Version": "1.35", + "Release": "1", + "Arch": "amd64", + "SrcName": "libgpg-error", + "SrcVersion": "1.35", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.1-or-later", + "BSD-3-Clause", + "g10-permissive", + "GPL-3.0-or-later", + "LGPL-2.1-only", + "GPL-3.0-only" + ], + "Maintainer": "Debian GnuPG Maintainers \u003cpkg-gnupg-maint@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libhogweed4@3.4.1-1", + "Name": "libhogweed4", + "Version": "3.4.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "nettle", + "SrcVersion": "3.4.1", + "SrcRelease": "1", + "Maintainer": "Magnus Holmgren \u003cholmgren@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libidn2-0@2.0.5-1", + "Name": "libidn2-0", + "Version": "2.0.5", + "Release": "1", + "Arch": "amd64", + "SrcName": "libidn2", + "SrcVersion": "2.0.5", + "SrcRelease": "1", + "Licenses": [ + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "Unicode", + "GPL-3.0-only", + "GPL-2.0-only", + "LGPL-3.0-only" + ], + "Maintainer": "Debian Libidn team \u003chelp-libidn@gnu.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "liblz4-1@1.8.3-1", + "Name": "liblz4-1", + "Version": "1.8.3", + "Release": "1", + "Arch": "amd64", + "SrcName": "lz4", + "SrcVersion": "1.8.3", + "SrcRelease": "1", + "Licenses": [ + "BSD-2-Clause", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Nobuhiro Iwamatsu \u003ciwamatsu@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "liblzma5@5.2.4-1", + "Name": "liblzma5", + "Version": "5.2.4", + "Release": "1", + "Arch": "amd64", + "SrcName": "xz-utils", + "SrcVersion": "5.2.4", + "SrcRelease": "1", + "Licenses": [ + "PD", + "probably-PD", + "GPL-2.0-or-later", + "LGPL-2.1-or-later", + "permissive-fsf", + "Autoconf", + "permissive-nowarranty", + "GPL-2.0-only", + "none", + "config-h", + "LGPL-2.0-only", + "LGPL-2.1-only", + "noderivs", + "PD-debian", + "GPL-3.0-only" + ], + "Maintainer": "Jonathan Nieder \u003cjrnieder@gmail.com\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libmnl0@1.0.4-2", + "Name": "libmnl0", + "Version": "1.0.4", + "Release": "2", + "Arch": "amd64", + "SrcName": "libmnl", + "SrcVersion": "1.0.4", + "SrcRelease": "2", + "Licenses": [ + "LGPL-2.1-only", + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Anibal Monsalve Salazar \u003canibal@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libmount1@2.33.1-0.1", + "Name": "libmount1", + "Version": "2.33.1", + "Release": "0.1", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "0.1", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "BSD-4-Clause", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "LGPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "LaMont Jones \u003clamont@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libncursesw6@6.1+20181013-2+deb10u1", + "Name": "libncursesw6", + "Version": "6.1+20181013", + "Release": "2+deb10u1", + "Arch": "amd64", + "SrcName": "ncurses", + "SrcVersion": "6.1+20181013", + "SrcRelease": "2+deb10u1", + "Maintainer": "Craig Small \u003ccsmall@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libnettle6@3.4.1-1", + "Name": "libnettle6", + "Version": "3.4.1", + "Release": "1", + "Arch": "amd64", + "SrcName": "nettle", + "SrcVersion": "3.4.1", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.1-or-later", + "LGPL-2.0-or-later", + "LGPL-2.0-only", + "other", + "GPL-2.0-or-later", + "GPL-2.0-with-autoconf-exception+", + "public-domain", + "GPL-2.0-only", + "GAP" + ], + "Maintainer": "Magnus Holmgren \u003cholmgren@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libp11-kit0@0.23.15-2", + "Name": "libp11-kit0", + "Version": "0.23.15", + "Release": "2", + "Arch": "amd64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.15", + "SrcRelease": "2", + "Licenses": [ + "BSD-3-Clause", + "permissive-like-automake-output", + "ISC", + "ISC+IBM", + "same-as-rest-of-p11kit" + ], + "Maintainer": "Debian GnuTLS Maintainers \u003cpkg-gnutls-maint@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libpam-modules@1.3.1-5", + "Name": "libpam-modules", + "Version": "1.3.1", + "Release": "5", + "Arch": "amd64", + "SrcName": "pam", + "SrcVersion": "1.3.1", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Steve Langasek \u003cvorlon@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libpam-modules-bin@1.3.1-5", + "Name": "libpam-modules-bin", + "Version": "1.3.1", + "Release": "5", + "Arch": "amd64", + "SrcName": "pam", + "SrcVersion": "1.3.1", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Steve Langasek \u003cvorlon@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libpam-runtime@1.3.1-5", + "Name": "libpam-runtime", + "Version": "1.3.1", + "Release": "5", + "Arch": "all", + "SrcName": "pam", + "SrcVersion": "1.3.1", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Steve Langasek \u003cvorlon@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libpam0g@1.3.1-5", + "Name": "libpam0g", + "Version": "1.3.1", + "Release": "5", + "Arch": "amd64", + "SrcName": "pam", + "SrcVersion": "1.3.1", + "SrcRelease": "5", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Steve Langasek \u003cvorlon@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libpcre3@2:8.39-12", + "Name": "libpcre3", + "Version": "8.39", + "Release": "12", + "Epoch": 2, + "Arch": "amd64", + "SrcName": "pcre3", + "SrcVersion": "8.39", + "SrcRelease": "12", + "SrcEpoch": 2, + "Maintainer": "Matthew Vernon \u003cmatthew@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libseccomp2@2.3.3-4", + "Name": "libseccomp2", + "Version": "2.3.3", + "Release": "4", + "Arch": "amd64", + "SrcName": "libseccomp", + "SrcVersion": "2.3.3", + "SrcRelease": "4", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "Kees Cook \u003ckees@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libselinux1@2.8-1+b1", + "Name": "libselinux1", + "Version": "2.8", + "Release": "1+b1", + "Arch": "amd64", + "SrcName": "libselinux", + "SrcVersion": "2.8", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.1-only", + "GPL-2.0-only" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libsemanage-common@2.8-2", + "Name": "libsemanage-common", + "Version": "2.8", + "Release": "2", + "Arch": "all", + "SrcName": "libsemanage", + "SrcVersion": "2.8", + "SrcRelease": "2", + "Licenses": [ + "LGPL-2.0-or-later", + "GPL-2.0-or-later" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libsemanage1@2.8-2", + "Name": "libsemanage1", + "Version": "2.8", + "Release": "2", + "Arch": "amd64", + "SrcName": "libsemanage", + "SrcVersion": "2.8", + "SrcRelease": "2", + "Licenses": [ + "LGPL-2.0-or-later", + "GPL-2.0-or-later" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libsepol1@2.8-1", + "Name": "libsepol1", + "Version": "2.8", + "Release": "1", + "Arch": "amd64", + "SrcName": "libsepol", + "SrcVersion": "2.8", + "SrcRelease": "1", + "Licenses": [ + "LGPL-2.0-or-later", + "GPL-2.0-or-later" + ], + "Maintainer": "Debian SELinux maintainers \u003cselinux-devel@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libsmartcols1@2.33.1-0.1", + "Name": "libsmartcols1", + "Version": "2.33.1", + "Release": "0.1", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "0.1", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "BSD-4-Clause", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "LGPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "LaMont Jones \u003clamont@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libss2@1.44.5-1+deb10u1", + "Name": "libss2", + "Version": "1.44.5", + "Release": "1+deb10u1", + "Arch": "amd64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.5", + "SrcRelease": "1+deb10u1", + "Maintainer": "Theodore Y. Ts'o \u003ctytso@mit.edu\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libstdc++6@8.3.0-6", + "Name": "libstdc++6", + "Version": "8.3.0", + "Release": "6", + "Arch": "amd64", + "SrcName": "gcc-8", + "SrcVersion": "8.3.0", + "SrcRelease": "6", + "Maintainer": "Debian GCC Maintainers \u003cdebian-gcc@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libsystemd0@241-7~deb10u1", + "Name": "libsystemd0", + "Version": "241", + "Release": "7~deb10u1", + "Arch": "amd64", + "SrcName": "systemd", + "SrcVersion": "241", + "SrcRelease": "7~deb10u1", + "Licenses": [ + "LGPL-2.1-or-later", + "CC0-1.0", + "GPL-2.0-only", + "GPL-2.0-or-later", + "MIT", + "public-domain", + "LGPL-2.1-only" + ], + "Maintainer": "Debian systemd Maintainers \u003cpkg-systemd-maintainers@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libtasn1-6@4.13-3", + "Name": "libtasn1-6", + "Version": "4.13", + "Release": "3", + "Arch": "amd64", + "SrcName": "libtasn1-6", + "SrcVersion": "4.13", + "SrcRelease": "3", + "Licenses": [ + "LGPL-2.0-or-later", + "LGPL-2.1-only", + "GPL-3.0-only", + "GFDL-1.3-only" + ], + "Maintainer": "Debian GnuTLS Maintainers \u003cpkg-gnutls-maint@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libtinfo6@6.1+20181013-2+deb10u1", + "Name": "libtinfo6", + "Version": "6.1+20181013", + "Release": "2+deb10u1", + "Arch": "amd64", + "SrcName": "ncurses", + "SrcVersion": "6.1+20181013", + "SrcRelease": "2+deb10u1", + "Maintainer": "Craig Small \u003ccsmall@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libudev1@241-7~deb10u1", + "Name": "libudev1", + "Version": "241", + "Release": "7~deb10u1", + "Arch": "amd64", + "SrcName": "systemd", + "SrcVersion": "241", + "SrcRelease": "7~deb10u1", + "Licenses": [ + "LGPL-2.1-or-later", + "CC0-1.0", + "GPL-2.0-only", + "GPL-2.0-or-later", + "MIT", + "public-domain", + "LGPL-2.1-only" + ], + "Maintainer": "Debian systemd Maintainers \u003cpkg-systemd-maintainers@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libunistring2@0.9.10-1", + "Name": "libunistring2", + "Version": "0.9.10", + "Release": "1", + "Arch": "amd64", + "SrcName": "libunistring", + "SrcVersion": "0.9.10", + "SrcRelease": "1", + "Licenses": [ + "LGPL-3.0-or-later", + "GPL-2.0-or-later", + "FreeSoftware", + "GPL-2+ with distribution exception", + "GPL-3.0-or-later", + "GFDL-1.2-or-later", + "MIT", + "LGPL-3.0-only", + "GPL-3.0-only", + "GPL-2.0-only", + "GFDL-1.2-only" + ], + "Maintainer": "Jörg Frings-Fürst \u003cdebian@jff.email\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libuuid1@2.33.1-0.1", + "Name": "libuuid1", + "Version": "2.33.1", + "Release": "0.1", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "0.1", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "BSD-4-Clause", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "LGPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "LaMont Jones \u003clamont@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libxtables12@1.8.2-4", + "Name": "libxtables12", + "Version": "1.8.2", + "Release": "4", + "Arch": "amd64", + "SrcName": "iptables", + "SrcVersion": "1.8.2", + "SrcRelease": "4", + "Licenses": [ + "GPL-2.0-only", + "Artistic-2", + "GPL-2.0-or-later", + "custom" + ], + "Maintainer": "Debian Netfilter Packaging Team \u003cpkg-netfilter-team@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "libzstd1@1.3.8+dfsg-3", + "Name": "libzstd1", + "Version": "1.3.8+dfsg", + "Release": "3", + "Arch": "amd64", + "SrcName": "libzstd", + "SrcVersion": "1.3.8+dfsg", + "SrcRelease": "3", + "Licenses": [ + "BSD-3-Clause", + "GPL-2.0-only", + "Zlib", + "GPL-2.0-or-later", + "MIT" + ], + "Maintainer": "Debian Med Packaging Team \u003cdebian-med-packaging@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "login@1:4.5-1.1", + "Name": "login", + "Version": "4.5", + "Release": "1.1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "shadow", + "SrcVersion": "4.5", + "SrcRelease": "1.1", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Shadow package maintainers \u003cpkg-shadow-devel@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "mawk@1.3.3-17+b3", + "Name": "mawk", + "Version": "1.3.3", + "Release": "17+b3", + "Arch": "amd64", + "SrcName": "mawk", + "SrcVersion": "1.3.3", + "SrcRelease": "17", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Steve Langasek \u003cvorlon@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "mount@2.33.1-0.1", + "Name": "mount", + "Version": "2.33.1", + "Release": "0.1", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "0.1", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "BSD-4-Clause", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "LGPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "LaMont Jones \u003clamont@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "ncurses-base@6.1+20181013-2+deb10u1", + "Name": "ncurses-base", + "Version": "6.1+20181013", + "Release": "2+deb10u1", + "Arch": "all", + "SrcName": "ncurses", + "SrcVersion": "6.1+20181013", + "SrcRelease": "2+deb10u1", + "Maintainer": "Craig Small \u003ccsmall@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "ncurses-bin@6.1+20181013-2+deb10u1", + "Name": "ncurses-bin", + "Version": "6.1+20181013", + "Release": "2+deb10u1", + "Arch": "amd64", + "SrcName": "ncurses", + "SrcVersion": "6.1+20181013", + "SrcRelease": "2+deb10u1", + "Maintainer": "Craig Small \u003ccsmall@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "passwd@1:4.5-1.1", + "Name": "passwd", + "Version": "4.5", + "Release": "1.1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "shadow", + "SrcVersion": "4.5", + "SrcRelease": "1.1", + "SrcEpoch": 1, + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Shadow package maintainers \u003cpkg-shadow-devel@lists.alioth.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "perl-base@5.28.1-6", + "Name": "perl-base", + "Version": "5.28.1", + "Release": "6", + "Arch": "amd64", + "SrcName": "perl", + "SrcVersion": "5.28.1", + "SrcRelease": "6", + "Maintainer": "Niko Tyni \u003cntyni@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "sed@4.7-1", + "Name": "sed", + "Version": "4.7", + "Release": "1", + "Arch": "amd64", + "SrcName": "sed", + "SrcVersion": "4.7", + "SrcRelease": "1", + "Licenses": [ + "GPL-3.0-only" + ], + "Maintainer": "Clint Adams \u003cclint@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "sysvinit-utils@2.93-8", + "Name": "sysvinit-utils", + "Version": "2.93", + "Release": "8", + "Arch": "amd64", + "SrcName": "sysvinit", + "SrcVersion": "2.93", + "SrcRelease": "8", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only" + ], + "Maintainer": "Debian sysvinit maintainers \u003cdebian-init-diversity@chiark.greenend.org.uk\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "tar@1.30+dfsg-6", + "Name": "tar", + "Version": "1.30+dfsg", + "Release": "6", + "Arch": "amd64", + "SrcName": "tar", + "SrcVersion": "1.30+dfsg", + "SrcRelease": "6", + "Licenses": [ + "GPL-3.0-only", + "GPL-2.0-only" + ], + "Maintainer": "Bdale Garbee \u003cbdale@gag.com\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "tzdata@2019b-0+deb10u1", + "Name": "tzdata", + "Version": "2019b", + "Release": "0+deb10u1", + "Arch": "all", + "SrcName": "tzdata", + "SrcVersion": "2019b", + "SrcRelease": "0+deb10u1", + "Maintainer": "GNU Libc Maintainers \u003cdebian-glibc@lists.debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "util-linux@2.33.1-0.1", + "Name": "util-linux", + "Version": "2.33.1", + "Release": "0.1", + "Arch": "amd64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "0.1", + "Licenses": [ + "GPL-2.0-or-later", + "GPL-2.0-only", + "public-domain", + "BSD-4-Clause", + "MIT", + "BSD-2-Clause", + "BSD-3-Clause", + "LGPL-2.0-or-later", + "LGPL-2.1-or-later", + "GPL-3.0-or-later", + "LGPL-3.0-or-later", + "GPL-3.0-only", + "LGPL-2.0-only", + "LGPL-2.1-only", + "LGPL-3.0-only" + ], + "Maintainer": "LaMont Jones \u003clamont@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + }, + { + "ID": "zlib1g@1:1.2.11.dfsg-1", + "Name": "zlib1g", + "Version": "1.2.11.dfsg", + "Release": "1", + "Epoch": 1, + "Arch": "amd64", + "SrcName": "zlib", + "SrcVersion": "1.2.11.dfsg", + "SrcRelease": "1", + "SrcEpoch": 1, + "Licenses": [ + "Zlib" + ], + "Maintainer": "Mark Brown \u003cbroonie@debian.org\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "dpkg" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/fedora-35.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/fedora-35.json.golden new file mode 100644 index 000000000000..0dff7255ae2c --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/fedora-35.json.golden @@ -0,0 +1,2514 @@ +[ + { + "ID": "alternatives@1.19-1.fc35.x86_64", + "Name": "alternatives", + "Version": "1.19", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "chkconfig", + "SrcVersion": "1.19", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "audit-libs@3.0.7-2.fc35.x86_64", + "Name": "audit-libs", + "Version": "3.0.7", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "audit", + "SrcVersion": "3.0.7", + "SrcRelease": "2.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "basesystem@11-12.fc35.noarch", + "Name": "basesystem", + "Version": "11", + "Release": "12.fc35", + "Arch": "noarch", + "SrcName": "basesystem", + "SrcVersion": "11", + "SrcRelease": "12.fc35", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bash@5.1.8-2.fc35.x86_64", + "Name": "bash", + "Version": "5.1.8", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "5.1.8", + "SrcRelease": "2.fc35", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bzip2-libs@1.0.8-9.fc35.x86_64", + "Name": "bzip2-libs", + "Version": "1.0.8", + "Release": "9.fc35", + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.8", + "SrcRelease": "9.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates@2021.2.52-1.0.fc35.noarch", + "Name": "ca-certificates", + "Version": "2021.2.52", + "Release": "1.0.fc35", + "Arch": "noarch", + "SrcName": "ca-certificates", + "SrcVersion": "2021.2.52", + "SrcRelease": "1.0.fc35", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "coreutils@8.32-31.fc35.x86_64", + "Name": "coreutils", + "Version": "8.32", + "Release": "31.fc35", + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "8.32", + "SrcRelease": "31.fc35", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "coreutils-common@8.32-31.fc35.x86_64", + "Name": "coreutils-common", + "Version": "8.32", + "Release": "31.fc35", + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "8.32", + "SrcRelease": "31.fc35", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cracklib@2.9.6-27.fc35.x86_64", + "Name": "cracklib", + "Version": "2.9.6", + "Release": "27.fc35", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.6", + "SrcRelease": "27.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "crypto-policies@20210819-1.gitd0fdcfb.fc35.noarch", + "Name": "crypto-policies", + "Version": "20210819", + "Release": "1.gitd0fdcfb.fc35", + "Arch": "noarch", + "SrcName": "crypto-policies", + "SrcVersion": "20210819", + "SrcRelease": "1.gitd0fdcfb.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "curl@7.79.1-1.fc35.x86_64", + "Name": "curl", + "Version": "7.79.1", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.79.1", + "SrcRelease": "1.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cyrus-sasl-lib@2.1.27-13.fc35.x86_64", + "Name": "cyrus-sasl-lib", + "Version": "2.1.27", + "Release": "13.fc35", + "Arch": "x86_64", + "SrcName": "cyrus-sasl", + "SrcVersion": "2.1.27", + "SrcRelease": "13.fc35", + "Licenses": [ + "BSD with advertising" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "dnf@4.9.0-1.fc35.noarch", + "Name": "dnf", + "Version": "4.9.0", + "Release": "1.fc35", + "Arch": "noarch", + "SrcName": "dnf", + "SrcVersion": "4.9.0", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "dnf-data@4.9.0-1.fc35.noarch", + "Name": "dnf-data", + "Version": "4.9.0", + "Release": "1.fc35", + "Arch": "noarch", + "SrcName": "dnf", + "SrcVersion": "4.9.0", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "elfutils-default-yama-scope@0.186-1.fc35.noarch", + "Name": "elfutils-default-yama-scope", + "Version": "0.186", + "Release": "1.fc35", + "Arch": "noarch", + "SrcName": "elfutils", + "SrcVersion": "0.186", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2+ or LGPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "elfutils-libelf@0.186-1.fc35.x86_64", + "Name": "elfutils-libelf", + "Version": "0.186", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.186", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2+ or LGPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "elfutils-libs@0.186-1.fc35.x86_64", + "Name": "elfutils-libs", + "Version": "0.186", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.186", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2+ or LGPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "expat@2.4.4-1.fc35.x86_64", + "Name": "expat", + "Version": "2.4.4", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "expat", + "SrcVersion": "2.4.4", + "SrcRelease": "1.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "fedora-gpg-keys@35-1.noarch", + "Name": "fedora-gpg-keys", + "Version": "35", + "Release": "1", + "Arch": "noarch", + "SrcName": "fedora-repos", + "SrcVersion": "35", + "SrcRelease": "1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "fedora-release-common@35-36.noarch", + "Name": "fedora-release-common", + "Version": "35", + "Release": "36", + "Arch": "noarch", + "SrcName": "fedora-release", + "SrcVersion": "35", + "SrcRelease": "36", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "fedora-release-container@35-36.noarch", + "Name": "fedora-release-container", + "Version": "35", + "Release": "36", + "Arch": "noarch", + "SrcName": "fedora-release", + "SrcVersion": "35", + "SrcRelease": "36", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "fedora-release-identity-container@35-36.noarch", + "Name": "fedora-release-identity-container", + "Version": "35", + "Release": "36", + "Arch": "noarch", + "SrcName": "fedora-release", + "SrcVersion": "35", + "SrcRelease": "36", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "fedora-repos@35-1.noarch", + "Name": "fedora-repos", + "Version": "35", + "Release": "1", + "Arch": "noarch", + "SrcName": "fedora-repos", + "SrcVersion": "35", + "SrcRelease": "1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "fedora-repos-modular@35-1.noarch", + "Name": "fedora-repos-modular", + "Version": "35", + "Release": "1", + "Arch": "noarch", + "SrcName": "fedora-repos", + "SrcVersion": "35", + "SrcRelease": "1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "file-libs@5.40-9.fc35.x86_64", + "Name": "file-libs", + "Version": "5.40", + "Release": "9.fc35", + "Arch": "x86_64", + "SrcName": "file", + "SrcVersion": "5.40", + "SrcRelease": "9.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "filesystem@3.14-7.fc35.x86_64", + "Name": "filesystem", + "Version": "3.14", + "Release": "7.fc35", + "Arch": "x86_64", + "SrcName": "filesystem", + "SrcVersion": "3.14", + "SrcRelease": "7.fc35", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gawk@5.1.0-4.fc35.x86_64", + "Name": "gawk", + "Version": "5.1.0", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "gawk", + "SrcVersion": "5.1.0", + "SrcRelease": "4.fc35", + "Licenses": [ + "GPLv3+ and GPLv2+ and LGPLv2+ and BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gdbm-libs@1.22-1.fc35.x86_64", + "Name": "gdbm-libs", + "Version": "1.22", + "Release": "1.fc35", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "gdbm", + "SrcVersion": "1.22", + "SrcRelease": "1.fc35", + "SrcEpoch": 1, + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glib2@2.70.4-1.fc35.x86_64", + "Name": "glib2", + "Version": "2.70.4", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "glib2", + "SrcVersion": "2.70.4", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc@2.34-25.fc35.x86_64", + "Name": "glibc", + "Version": "2.34", + "Release": "25.fc35", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.34", + "SrcRelease": "25.fc35", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+ and GPLv2+ with exceptions and BSD and Inner-Net and ISC and Public Domain and GFDL" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc-common@2.34-25.fc35.x86_64", + "Name": "glibc-common", + "Version": "2.34", + "Release": "25.fc35", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.34", + "SrcRelease": "25.fc35", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+ and GPLv2+ with exceptions and BSD and Inner-Net and ISC and Public Domain and GFDL" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc-minimal-langpack@2.34-25.fc35.x86_64", + "Name": "glibc-minimal-langpack", + "Version": "2.34", + "Release": "25.fc35", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.34", + "SrcRelease": "25.fc35", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+ and GPLv2+ with exceptions and BSD and Inner-Net and ISC and Public Domain and GFDL" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gmp@6.2.0-7.fc35.x86_64", + "Name": "gmp", + "Version": "6.2.0", + "Release": "7.fc35", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "gmp", + "SrcVersion": "6.2.0", + "SrcRelease": "7.fc35", + "SrcEpoch": 1, + "Licenses": [ + "LGPLv3+ or GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gnupg2@2.3.4-1.fc35.x86_64", + "Name": "gnupg2", + "Version": "2.3.4", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "gnupg2", + "SrcVersion": "2.3.4", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gnutls@3.7.2-2.fc35.x86_64", + "Name": "gnutls", + "Version": "3.7.2", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "gnutls", + "SrcVersion": "3.7.2", + "SrcRelease": "2.fc35", + "Licenses": [ + "GPLv3+ and LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@9867c58f-601c49ca.", + "Name": "gpg-pubkey", + "Version": "9867c58f", + "Release": "601c49ca", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpgme@1.15.1-6.fc35.x86_64", + "Name": "gpgme", + "Version": "1.15.1", + "Release": "6.fc35", + "Arch": "x86_64", + "SrcName": "gpgme", + "SrcVersion": "1.15.1", + "SrcRelease": "6.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "grep@3.6-4.fc35.x86_64", + "Name": "grep", + "Version": "3.6", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "grep", + "SrcVersion": "3.6", + "SrcRelease": "4.fc35", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gzip@1.10-5.fc35.x86_64", + "Name": "gzip", + "Version": "1.10", + "Release": "5.fc35", + "Arch": "x86_64", + "SrcName": "gzip", + "SrcVersion": "1.10", + "SrcRelease": "5.fc35", + "Licenses": [ + "GPLv3+ and GFDL" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ima-evm-utils@1.3.2-3.fc35.x86_64", + "Name": "ima-evm-utils", + "Version": "1.3.2", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "ima-evm-utils", + "SrcVersion": "1.3.2", + "SrcRelease": "3.fc35", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "json-c@0.15-2.fc35.x86_64", + "Name": "json-c", + "Version": "0.15", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "json-c", + "SrcVersion": "0.15", + "SrcRelease": "2.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "keyutils-libs@1.6.1-3.fc35.x86_64", + "Name": "keyutils-libs", + "Version": "1.6.1", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "keyutils", + "SrcVersion": "1.6.1", + "SrcRelease": "3.fc35", + "Licenses": [ + "GPLv2+ and LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "krb5-libs@1.19.2-2.fc35.x86_64", + "Name": "krb5-libs", + "Version": "1.19.2", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "krb5", + "SrcVersion": "1.19.2", + "SrcRelease": "2.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libacl@2.3.1-2.fc35.x86_64", + "Name": "libacl", + "Version": "2.3.1", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "acl", + "SrcVersion": "2.3.1", + "SrcRelease": "2.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libarchive@3.5.2-2.fc35.x86_64", + "Name": "libarchive", + "Version": "3.5.2", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "libarchive", + "SrcVersion": "3.5.2", + "SrcRelease": "2.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libassuan@2.5.5-3.fc35.x86_64", + "Name": "libassuan", + "Version": "2.5.5", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "libassuan", + "SrcVersion": "2.5.5", + "SrcRelease": "3.fc35", + "Licenses": [ + "LGPLv2+ and GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libattr@2.5.1-3.fc35.x86_64", + "Name": "libattr", + "Version": "2.5.1", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "attr", + "SrcVersion": "2.5.1", + "SrcRelease": "3.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libblkid@2.37.4-1.fc35.x86_64", + "Name": "libblkid", + "Version": "2.37.4", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.37.4", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libbrotli@1.0.9-6.fc35.x86_64", + "Name": "libbrotli", + "Version": "1.0.9", + "Release": "6.fc35", + "Arch": "x86_64", + "SrcName": "brotli", + "SrcVersion": "1.0.9", + "SrcRelease": "6.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap@2.48-3.fc35.x86_64", + "Name": "libcap", + "Version": "2.48", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "libcap", + "SrcVersion": "2.48", + "SrcRelease": "3.fc35", + "Licenses": [ + "BSD or GPLv2" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap-ng@0.8.2-8.fc35.x86_64", + "Name": "libcap-ng", + "Version": "0.8.2", + "Release": "8.fc35", + "Arch": "x86_64", + "SrcName": "libcap-ng", + "SrcVersion": "0.8.2", + "SrcRelease": "8.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcom_err@1.46.3-1.fc35.x86_64", + "Name": "libcom_err", + "Version": "1.46.3", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.46.3", + "SrcRelease": "1.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcomps@0.1.18-1.fc35.x86_64", + "Name": "libcomps", + "Version": "0.1.18", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libcomps", + "SrcVersion": "0.1.18", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcurl@7.79.1-1.fc35.x86_64", + "Name": "libcurl", + "Version": "7.79.1", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.79.1", + "SrcRelease": "1.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libdb@5.3.28-50.fc35.x86_64", + "Name": "libdb", + "Version": "5.3.28", + "Release": "50.fc35", + "Arch": "x86_64", + "SrcName": "libdb", + "SrcVersion": "5.3.28", + "SrcRelease": "50.fc35", + "Licenses": [ + "BSD and LGPLv2 and Sleepycat" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libdnf@0.64.0-1.fc35.x86_64", + "Name": "libdnf", + "Version": "0.64.0", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libdnf", + "SrcVersion": "0.64.0", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libeconf@0.4.0-2.fc35.x86_64", + "Name": "libeconf", + "Version": "0.4.0", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "libeconf", + "SrcVersion": "0.4.0", + "SrcRelease": "2.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libffi@3.1-29.fc35.x86_64", + "Name": "libffi", + "Version": "3.1", + "Release": "29.fc35", + "Arch": "x86_64", + "SrcName": "libffi", + "SrcVersion": "3.1", + "SrcRelease": "29.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libfsverity@1.4-6.fc35.x86_64", + "Name": "libfsverity", + "Version": "1.4", + "Release": "6.fc35", + "Arch": "x86_64", + "SrcName": "fsverity-utils", + "SrcVersion": "1.4", + "SrcRelease": "6.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcc@11.2.1-9.fc35.x86_64", + "Name": "libgcc", + "Version": "11.2.1", + "Release": "9.fc35", + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "11.2.1", + "SrcRelease": "9.fc35", + "Licenses": [ + "GPLv3+ and GPLv3+ with exceptions and GPLv2+ with exceptions and LGPLv2+ and BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcrypt@1.9.4-1.fc35.x86_64", + "Name": "libgcrypt", + "Version": "1.9.4", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libgcrypt", + "SrcVersion": "1.9.4", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgomp@11.2.1-9.fc35.x86_64", + "Name": "libgomp", + "Version": "11.2.1", + "Release": "9.fc35", + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "11.2.1", + "SrcRelease": "9.fc35", + "Licenses": [ + "GPLv3+ and GPLv3+ with exceptions and GPLv2+ with exceptions and LGPLv2+ and BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgpg-error@1.43-1.fc35.x86_64", + "Name": "libgpg-error", + "Version": "1.43", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libgpg-error", + "SrcVersion": "1.43", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libidn2@2.3.2-3.fc35.x86_64", + "Name": "libidn2", + "Version": "2.3.2", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "libidn2", + "SrcVersion": "2.3.2", + "SrcRelease": "3.fc35", + "Licenses": [ + "(GPLv2+ or LGPLv3+) and GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libksba@1.6.0-2.fc35.x86_64", + "Name": "libksba", + "Version": "1.6.0", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "libksba", + "SrcVersion": "1.6.0", + "SrcRelease": "2.fc35", + "Licenses": [ + "(LGPLv3+ or GPLv2+) and GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmodulemd@2.14.0-1.fc35.x86_64", + "Name": "libmodulemd", + "Version": "2.14.0", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libmodulemd", + "SrcVersion": "2.14.0", + "SrcRelease": "1.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmount@2.37.4-1.fc35.x86_64", + "Name": "libmount", + "Version": "2.37.4", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.37.4", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnghttp2@1.45.1-1.fc35.x86_64", + "Name": "libnghttp2", + "Version": "1.45.1", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "nghttp2", + "SrcVersion": "1.45.1", + "SrcRelease": "1.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnsl2@1.3.0-4.fc35.x86_64", + "Name": "libnsl2", + "Version": "1.3.0", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "libnsl2", + "SrcVersion": "1.3.0", + "SrcRelease": "4.fc35", + "Licenses": [ + "BSD and LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpsl@0.21.1-4.fc35.x86_64", + "Name": "libpsl", + "Version": "0.21.1", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "libpsl", + "SrcVersion": "0.21.1", + "SrcRelease": "4.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpwquality@1.4.4-6.fc35.x86_64", + "Name": "libpwquality", + "Version": "1.4.4", + "Release": "6.fc35", + "Arch": "x86_64", + "SrcName": "libpwquality", + "SrcVersion": "1.4.4", + "SrcRelease": "6.fc35", + "Licenses": [ + "BSD or GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "librepo@1.14.2-1.fc35.x86_64", + "Name": "librepo", + "Version": "1.14.2", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "librepo", + "SrcVersion": "1.14.2", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libreport-filesystem@2.15.2-6.fc35.noarch", + "Name": "libreport-filesystem", + "Version": "2.15.2", + "Release": "6.fc35", + "Arch": "noarch", + "SrcName": "libreport", + "SrcVersion": "2.15.2", + "SrcRelease": "6.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libselinux@3.3-1.fc35.x86_64", + "Name": "libselinux", + "Version": "3.3", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libselinux", + "SrcVersion": "3.3", + "SrcRelease": "1.fc35", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsemanage@3.3-1.fc35.x86_64", + "Name": "libsemanage", + "Version": "3.3", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libsemanage", + "SrcVersion": "3.3", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsepol@3.3-2.fc35.x86_64", + "Name": "libsepol", + "Version": "3.3", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "libsepol", + "SrcVersion": "3.3", + "SrcRelease": "2.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsigsegv@2.13-3.fc35.x86_64", + "Name": "libsigsegv", + "Version": "2.13", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "libsigsegv", + "SrcVersion": "2.13", + "SrcRelease": "3.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsmartcols@2.37.4-1.fc35.x86_64", + "Name": "libsmartcols", + "Version": "2.37.4", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.37.4", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsolv@0.7.19-3.fc35.x86_64", + "Name": "libsolv", + "Version": "0.7.19", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "libsolv", + "SrcVersion": "0.7.19", + "SrcRelease": "3.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libssh@0.9.6-1.fc35.x86_64", + "Name": "libssh", + "Version": "0.9.6", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libssh", + "SrcVersion": "0.9.6", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libssh-config@0.9.6-1.fc35.noarch", + "Name": "libssh-config", + "Version": "0.9.6", + "Release": "1.fc35", + "Arch": "noarch", + "SrcName": "libssh", + "SrcVersion": "0.9.6", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libstdc++@11.2.1-9.fc35.x86_64", + "Name": "libstdc++", + "Version": "11.2.1", + "Release": "9.fc35", + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "11.2.1", + "SrcRelease": "9.fc35", + "Licenses": [ + "GPLv3+ and GPLv3+ with exceptions and GPLv2+ with exceptions and LGPLv2+ and BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtasn1@4.16.0-6.fc35.x86_64", + "Name": "libtasn1", + "Version": "4.16.0", + "Release": "6.fc35", + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "4.16.0", + "SrcRelease": "6.fc35", + "Licenses": [ + "GPLv3+ and LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtirpc@1.3.2-1.fc35.x86_64", + "Name": "libtirpc", + "Version": "1.3.2", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libtirpc", + "SrcVersion": "1.3.2", + "SrcRelease": "1.fc35", + "Licenses": [ + "SISSL and BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libunistring@0.9.10-14.fc35.x86_64", + "Name": "libunistring", + "Version": "0.9.10", + "Release": "14.fc35", + "Arch": "x86_64", + "SrcName": "libunistring", + "SrcVersion": "0.9.10", + "SrcRelease": "14.fc35", + "Licenses": [ + "GPLv2+ or LGPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libuuid@2.37.4-1.fc35.x86_64", + "Name": "libuuid", + "Version": "2.37.4", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.37.4", + "SrcRelease": "1.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libverto@0.3.2-2.fc35.x86_64", + "Name": "libverto", + "Version": "0.3.2", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "libverto", + "SrcVersion": "0.3.2", + "SrcRelease": "2.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libxcrypt@4.4.28-1.fc35.x86_64", + "Name": "libxcrypt", + "Version": "4.4.28", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libxcrypt", + "SrcVersion": "4.4.28", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+ and BSD and Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libxml2@2.9.12-6.fc35.x86_64", + "Name": "libxml2", + "Version": "2.9.12", + "Release": "6.fc35", + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.9.12", + "SrcRelease": "6.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libyaml@0.2.5-6.fc35.x86_64", + "Name": "libyaml", + "Version": "0.2.5", + "Release": "6.fc35", + "Arch": "x86_64", + "SrcName": "libyaml", + "SrcVersion": "0.2.5", + "SrcRelease": "6.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzstd@1.5.2-1.fc35.x86_64", + "Name": "libzstd", + "Version": "1.5.2", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "zstd", + "SrcVersion": "1.5.2", + "SrcRelease": "1.fc35", + "Licenses": [ + "BSD and GPLv2" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "lua-libs@5.4.4-1.fc35.x86_64", + "Name": "lua-libs", + "Version": "5.4.4", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "lua", + "SrcVersion": "5.4.4", + "SrcRelease": "1.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "lz4-libs@1.9.3-3.fc35.x86_64", + "Name": "lz4-libs", + "Version": "1.9.3", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "lz4", + "SrcVersion": "1.9.3", + "SrcRelease": "3.fc35", + "Licenses": [ + "GPLv2+ and BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "mpdecimal@2.5.1-2.fc35.x86_64", + "Name": "mpdecimal", + "Version": "2.5.1", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "mpdecimal", + "SrcVersion": "2.5.1", + "SrcRelease": "2.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "mpfr@4.1.0-8.fc35.x86_64", + "Name": "mpfr", + "Version": "4.1.0", + "Release": "8.fc35", + "Arch": "x86_64", + "SrcName": "mpfr", + "SrcVersion": "4.1.0", + "SrcRelease": "8.fc35", + "Licenses": [ + "LGPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-base@6.2-8.20210508.fc35.noarch", + "Name": "ncurses-base", + "Version": "6.2", + "Release": "8.20210508.fc35", + "Arch": "noarch", + "SrcName": "ncurses", + "SrcVersion": "6.2", + "SrcRelease": "8.20210508.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-libs@6.2-8.20210508.fc35.x86_64", + "Name": "ncurses-libs", + "Version": "6.2", + "Release": "8.20210508.fc35", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.2", + "SrcRelease": "8.20210508.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nettle@3.7.3-2.fc35.x86_64", + "Name": "nettle", + "Version": "3.7.3", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "nettle", + "SrcVersion": "3.7.3", + "SrcRelease": "2.fc35", + "Licenses": [ + "LGPLv3+ or GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "npth@1.6-7.fc35.x86_64", + "Name": "npth", + "Version": "1.6", + "Release": "7.fc35", + "Arch": "x86_64", + "SrcName": "npth", + "SrcVersion": "1.6", + "SrcRelease": "7.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openldap@2.4.59-3.fc35.x86_64", + "Name": "openldap", + "Version": "2.4.59", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "openldap", + "SrcVersion": "2.4.59", + "SrcRelease": "3.fc35", + "Licenses": [ + "OpenLDAP" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openssl-libs@1.1.1l-2.fc35.x86_64", + "Name": "openssl-libs", + "Version": "1.1.1l", + "Release": "2.fc35", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "openssl", + "SrcVersion": "1.1.1l", + "SrcRelease": "2.fc35", + "SrcEpoch": 1, + "Licenses": [ + "OpenSSL and ASL 2.0" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit@0.23.22-4.fc35.x86_64", + "Name": "p11-kit", + "Version": "0.23.22", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.22", + "SrcRelease": "4.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit-trust@0.23.22-4.fc35.x86_64", + "Name": "p11-kit-trust", + "Version": "0.23.22", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.22", + "SrcRelease": "4.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pam@1.5.2-7.fc35.x86_64", + "Name": "pam", + "Version": "1.5.2", + "Release": "7.fc35", + "Arch": "x86_64", + "SrcName": "pam", + "SrcVersion": "1.5.2", + "SrcRelease": "7.fc35", + "Licenses": [ + "BSD and GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pcre@8.45-1.fc35.x86_64", + "Name": "pcre", + "Version": "8.45", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "pcre", + "SrcVersion": "8.45", + "SrcRelease": "1.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pcre2@10.39-1.fc35.x86_64", + "Name": "pcre2", + "Version": "10.39", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "pcre2", + "SrcVersion": "10.39", + "SrcRelease": "1.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pcre2-syntax@10.39-1.fc35.noarch", + "Name": "pcre2-syntax", + "Version": "10.39", + "Release": "1.fc35", + "Arch": "noarch", + "SrcName": "pcre2", + "SrcVersion": "10.39", + "SrcRelease": "1.fc35", + "Licenses": [ + "BSD" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "popt@1.18-6.fc35.x86_64", + "Name": "popt", + "Version": "1.18", + "Release": "6.fc35", + "Arch": "x86_64", + "SrcName": "popt", + "SrcVersion": "1.18", + "SrcRelease": "6.fc35", + "Licenses": [ + "MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "publicsuffix-list-dafsa@20210518-2.fc35.noarch", + "Name": "publicsuffix-list-dafsa", + "Version": "20210518", + "Release": "2.fc35", + "Arch": "noarch", + "SrcName": "publicsuffix-list", + "SrcVersion": "20210518", + "SrcRelease": "2.fc35", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-pip-wheel@21.2.3-4.fc35.noarch", + "Name": "python-pip-wheel", + "Version": "21.2.3", + "Release": "4.fc35", + "Arch": "noarch", + "SrcName": "python-pip", + "SrcVersion": "21.2.3", + "SrcRelease": "4.fc35", + "Licenses": [ + "MIT and Python and ASL 2.0 and BSD and ISC and LGPLv2 and MPLv2.0 and (ASL 2.0 or BSD)" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-setuptools-wheel@57.4.0-1.fc35.noarch", + "Name": "python-setuptools-wheel", + "Version": "57.4.0", + "Release": "1.fc35", + "Arch": "noarch", + "SrcName": "python-setuptools", + "SrcVersion": "57.4.0", + "SrcRelease": "1.fc35", + "Licenses": [ + "MIT and (BSD or ASL 2.0)" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python3@3.10.2-1.fc35.x86_64", + "Name": "python3", + "Version": "3.10.2", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "python3.10", + "SrcVersion": "3.10.2", + "SrcRelease": "1.fc35", + "Licenses": [ + "Python" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python3-dnf@4.9.0-1.fc35.noarch", + "Name": "python3-dnf", + "Version": "4.9.0", + "Release": "1.fc35", + "Arch": "noarch", + "SrcName": "dnf", + "SrcVersion": "4.9.0", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python3-gpg@1.15.1-6.fc35.x86_64", + "Name": "python3-gpg", + "Version": "1.15.1", + "Release": "6.fc35", + "Arch": "x86_64", + "SrcName": "gpgme", + "SrcVersion": "1.15.1", + "SrcRelease": "6.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python3-hawkey@0.64.0-1.fc35.x86_64", + "Name": "python3-hawkey", + "Version": "0.64.0", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libdnf", + "SrcVersion": "0.64.0", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python3-libcomps@0.1.18-1.fc35.x86_64", + "Name": "python3-libcomps", + "Version": "0.1.18", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libcomps", + "SrcVersion": "0.1.18", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python3-libdnf@0.64.0-1.fc35.x86_64", + "Name": "python3-libdnf", + "Version": "0.64.0", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "libdnf", + "SrcVersion": "0.64.0", + "SrcRelease": "1.fc35", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python3-libs@3.10.2-1.fc35.x86_64", + "Name": "python3-libs", + "Version": "3.10.2", + "Release": "1.fc35", + "Arch": "x86_64", + "SrcName": "python3.10", + "SrcVersion": "3.10.2", + "SrcRelease": "1.fc35", + "Licenses": [ + "Python" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python3-rpm@4.17.0-4.fc35.x86_64", + "Name": "python3-rpm", + "Version": "4.17.0", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.17.0", + "SrcRelease": "4.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "readline@8.1-3.fc35.x86_64", + "Name": "readline", + "Version": "8.1", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "readline", + "SrcVersion": "8.1", + "SrcRelease": "3.fc35", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rootfiles@8.1-30.fc35.noarch", + "Name": "rootfiles", + "Version": "8.1", + "Release": "30.fc35", + "Arch": "noarch", + "SrcName": "rootfiles", + "SrcVersion": "8.1", + "SrcRelease": "30.fc35", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm@4.17.0-4.fc35.x86_64", + "Name": "rpm", + "Version": "4.17.0", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.17.0", + "SrcRelease": "4.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-build-libs@4.17.0-4.fc35.x86_64", + "Name": "rpm-build-libs", + "Version": "4.17.0", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.17.0", + "SrcRelease": "4.fc35", + "Licenses": [ + "GPLv2+ and LGPLv2+ with exceptions" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-libs@4.17.0-4.fc35.x86_64", + "Name": "rpm-libs", + "Version": "4.17.0", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.17.0", + "SrcRelease": "4.fc35", + "Licenses": [ + "GPLv2+ and LGPLv2+ with exceptions" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-sign-libs@4.17.0-4.fc35.x86_64", + "Name": "rpm-sign-libs", + "Version": "4.17.0", + "Release": "4.fc35", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.17.0", + "SrcRelease": "4.fc35", + "Licenses": [ + "GPLv2+ and LGPLv2+ with exceptions" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sed@4.8-8.fc35.x86_64", + "Name": "sed", + "Version": "4.8", + "Release": "8.fc35", + "Arch": "x86_64", + "SrcName": "sed", + "SrcVersion": "4.8", + "SrcRelease": "8.fc35", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "setup@2.13.9.1-2.fc35.noarch", + "Name": "setup", + "Version": "2.13.9.1", + "Release": "2.fc35", + "Arch": "noarch", + "SrcName": "setup", + "SrcVersion": "2.13.9.1", + "SrcRelease": "2.fc35", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "shadow-utils@4.9-9.fc35.x86_64", + "Name": "shadow-utils", + "Version": "4.9", + "Release": "9.fc35", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "shadow-utils", + "SrcVersion": "4.9", + "SrcRelease": "9.fc35", + "SrcEpoch": 2, + "Licenses": [ + "BSD and GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sqlite-libs@3.36.0-3.fc35.x86_64", + "Name": "sqlite-libs", + "Version": "3.36.0", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "sqlite", + "SrcVersion": "3.36.0", + "SrcRelease": "3.fc35", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sudo@1.9.7p2-2.fc35.x86_64", + "Name": "sudo", + "Version": "1.9.7p2", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "sudo", + "SrcVersion": "1.9.7p2", + "SrcRelease": "2.fc35", + "Licenses": [ + "ISC" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "tar@1.34-2.fc35.x86_64", + "Name": "tar", + "Version": "1.34", + "Release": "2.fc35", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "tar", + "SrcVersion": "1.34", + "SrcRelease": "2.fc35", + "SrcEpoch": 2, + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "tpm2-tss@3.1.0-3.fc35.x86_64", + "Name": "tpm2-tss", + "Version": "3.1.0", + "Release": "3.fc35", + "Arch": "x86_64", + "SrcName": "tpm2-tss", + "SrcVersion": "3.1.0", + "SrcRelease": "3.fc35", + "Licenses": [ + "BSD and TCGL" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "tzdata@2021e-1.fc35.noarch", + "Name": "tzdata", + "Version": "2021e", + "Release": "1.fc35", + "Arch": "noarch", + "SrcName": "tzdata", + "SrcVersion": "2021e", + "SrcRelease": "1.fc35", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "vim-data@8.2.4386-1.fc35.noarch", + "Name": "vim-data", + "Version": "8.2.4386", + "Release": "1.fc35", + "Epoch": 2, + "Arch": "noarch", + "SrcName": "vim", + "SrcVersion": "8.2.4386", + "SrcRelease": "1.fc35", + "SrcEpoch": 2, + "Licenses": [ + "Vim and MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "vim-minimal@8.2.4386-1.fc35.x86_64", + "Name": "vim-minimal", + "Version": "8.2.4386", + "Release": "1.fc35", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "vim", + "SrcVersion": "8.2.4386", + "SrcRelease": "1.fc35", + "SrcEpoch": 2, + "Licenses": [ + "Vim and MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "xz-libs@5.2.5-7.fc35.x86_64", + "Name": "xz-libs", + "Version": "5.2.5", + "Release": "7.fc35", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.5", + "SrcRelease": "7.fc35", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "yum@4.9.0-1.fc35.noarch", + "Name": "yum", + "Version": "4.9.0", + "Release": "1.fc35", + "Arch": "noarch", + "SrcName": "dnf", + "SrcVersion": "4.9.0", + "SrcRelease": "1.fc35", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "zchunk-libs@1.1.15-2.fc35.x86_64", + "Name": "zchunk-libs", + "Version": "1.1.15", + "Release": "2.fc35", + "Arch": "x86_64", + "SrcName": "zchunk", + "SrcVersion": "1.1.15", + "SrcRelease": "2.fc35", + "Licenses": [ + "BSD and MIT" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "zlib@1.2.11-30.fc35.x86_64", + "Name": "zlib", + "Version": "1.2.11", + "Release": "30.fc35", + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.11", + "SrcRelease": "30.fc35", + "Licenses": [ + "zlib and Boost" + ], + "Maintainer": "Fedora Project", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/gcr.io-distroless-base-latest.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/gcr.io-distroless-base-latest.json.golden new file mode 100644 index 000000000000..55b5e30880c2 --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/gcr.io-distroless-base-latest.json.golden @@ -0,0 +1,74 @@ +[ + { + "Name": "tzdata", + "Version": "2019a-0+deb9u1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "tzdata", + "SrcVersion": "2019a-0+deb9u1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02" + }, + { + "Name": "base-files", + "Version": "9.9+deb9u9", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "base-files", + "SrcVersion": "9.9+deb9u9", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02" + }, + { + "Name": "netbase", + "Version": "5.4", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "netbase", + "SrcVersion": "5.4", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02" + }, + { + "Name": "libc6", + "Version": "2.24-11+deb9u4", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "glibc", + "SrcVersion": "2.24-11+deb9u4", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + { + "Name": "libssl1.1", + "Version": "1.1.0k-1~deb9u1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "openssl", + "SrcVersion": "1.1.0k-1~deb9u1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + }, + { + "Name": "openssl", + "Version": "1.1.0k-1~deb9u1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "openssl", + "SrcVersion": "1.1.0k-1~deb9u1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5" + } +] diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-leap-151.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-leap-151.json.golden new file mode 100644 index 000000000000..57d51e6d4917 --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-leap-151.json.golden @@ -0,0 +1,2186 @@ +[ + { + "ID": "aaa_base@84.87+git20180409.04c9dae-lp151.5.12.1.x86_64", + "Name": "aaa_base", + "Version": "84.87+git20180409.04c9dae", + "Release": "lp151.5.12.1", + "Arch": "x86_64", + "SrcName": "aaa_base", + "SrcVersion": "84.87+git20180409.04c9dae", + "SrcRelease": "lp151.5.12.1", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bash@4.4-lp151.10.3.1.x86_64", + "Name": "bash", + "Version": "4.4", + "Release": "lp151.10.3.1", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.4", + "SrcRelease": "lp151.10.3.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "boost-license1_66_0@1.66.0-lp151.4.5.noarch", + "Name": "boost-license1_66_0", + "Version": "1.66.0", + "Release": "lp151.4.5", + "Arch": "noarch", + "SrcName": "boost-base", + "SrcVersion": "1.66.0", + "SrcRelease": "lp151.4.5", + "Licenses": [ + "BSL-1.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates@2+git20170807.10b2785-lp151.7.1.noarch", + "Name": "ca-certificates", + "Version": "2+git20170807.10b2785", + "Release": "lp151.7.1", + "Arch": "noarch", + "SrcName": "ca-certificates", + "SrcVersion": "2+git20170807.10b2785", + "SrcRelease": "lp151.7.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates-mozilla@2.34-lp151.2.3.1.noarch", + "Name": "ca-certificates-mozilla", + "Version": "2.34", + "Release": "lp151.2.3.1", + "Arch": "noarch", + "SrcName": "ca-certificates-mozilla", + "SrcVersion": "2.34", + "SrcRelease": "lp151.2.3.1", + "Licenses": [ + "MPL-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "coreutils@8.29-lp151.3.3.x86_64", + "Name": "coreutils", + "Version": "8.29", + "Release": "lp151.3.3", + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "8.29", + "SrcRelease": "lp151.3.3", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cpio@2.12-lp151.2.68.x86_64", + "Name": "cpio", + "Version": "2.12", + "Release": "lp151.2.68", + "Arch": "x86_64", + "SrcName": "cpio", + "SrcVersion": "2.12", + "SrcRelease": "lp151.2.68", + "Licenses": [ + "GPL-3.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cracklib@2.9.6-lp151.3.69.x86_64", + "Name": "cracklib", + "Version": "2.9.6", + "Release": "lp151.3.69", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.6", + "SrcRelease": "lp151.3.69", + "Licenses": [ + "LGPL-2.1" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cracklib-dict-small@2.9.6-lp151.3.69.x86_64", + "Name": "cracklib-dict-small", + "Version": "2.9.6", + "Release": "lp151.3.69", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.6", + "SrcRelease": "lp151.3.69", + "Licenses": [ + "LGPL-2.1" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "diffutils@3.6-lp151.3.3.x86_64", + "Name": "diffutils", + "Version": "3.6", + "Release": "lp151.3.3", + "Arch": "x86_64", + "SrcName": "diffutils", + "SrcVersion": "3.6", + "SrcRelease": "lp151.3.3", + "Licenses": [ + "GFDL-1.2 and GPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "file-magic@5.32-lp151.7.22.noarch", + "Name": "file-magic", + "Version": "5.32", + "Release": "lp151.7.22", + "Arch": "noarch", + "SrcName": "file", + "SrcVersion": "5.32", + "SrcRelease": "lp151.7.22", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "filesystem@15.0-lp151.9.2.x86_64", + "Name": "filesystem", + "Version": "15.0", + "Release": "lp151.9.2", + "Arch": "x86_64", + "SrcName": "filesystem", + "SrcVersion": "15.0", + "SrcRelease": "lp151.9.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "fillup@1.42-lp151.3.2.x86_64", + "Name": "fillup", + "Version": "1.42", + "Release": "lp151.3.2", + "Arch": "x86_64", + "SrcName": "fillup", + "SrcVersion": "1.42", + "SrcRelease": "lp151.3.2", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "findutils@4.6.0-lp151.3.71.x86_64", + "Name": "findutils", + "Version": "4.6.0", + "Release": "lp151.3.71", + "Arch": "x86_64", + "SrcName": "findutils", + "SrcVersion": "4.6.0", + "SrcRelease": "lp151.3.71", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gawk@4.2.1-lp151.2.70.x86_64", + "Name": "gawk", + "Version": "4.2.1", + "Release": "lp151.2.70", + "Arch": "x86_64", + "SrcName": "gawk", + "SrcVersion": "4.2.1", + "SrcRelease": "lp151.2.70", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc@2.26-lp151.18.7.x86_64", + "Name": "glibc", + "Version": "2.26", + "Release": "lp151.18.7", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.26", + "SrcRelease": "lp151.18.7", + "Licenses": [ + "LGPL-2.1+ AND SUSE-LGPL-2.1+-with-GCC-exception AND GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@307e3d54-5aaa90a5.", + "Name": "gpg-pubkey", + "Version": "307e3d54", + "Release": "5aaa90a5", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@39db7c82-5847eb1f.", + "Name": "gpg-pubkey", + "Version": "39db7c82", + "Release": "5847eb1f", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@3dbdc284-53674dd4.", + "Name": "gpg-pubkey", + "Version": "3dbdc284", + "Release": "53674dd4", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg2@2.2.5-lp151.6.3.1.x86_64", + "Name": "gpg2", + "Version": "2.2.5", + "Release": "lp151.6.3.1", + "Arch": "x86_64", + "SrcName": "gpg2", + "SrcVersion": "2.2.5", + "SrcRelease": "lp151.6.3.1", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "grep@3.1-lp151.3.29.x86_64", + "Name": "grep", + "Version": "3.1", + "Release": "lp151.3.29", + "Arch": "x86_64", + "SrcName": "grep", + "SrcVersion": "3.1", + "SrcRelease": "lp151.3.29", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "info@6.5-lp151.5.69.x86_64", + "Name": "info", + "Version": "6.5", + "Release": "lp151.5.69", + "Arch": "x86_64", + "SrcName": "texinfo", + "SrcVersion": "6.5", + "SrcRelease": "lp151.5.69", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "krb5@1.16.3-lp151.2.6.1.x86_64", + "Name": "krb5", + "Version": "1.16.3", + "Release": "lp151.2.6.1", + "Arch": "x86_64", + "SrcName": "krb5", + "SrcVersion": "1.16.3", + "SrcRelease": "lp151.2.6.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "kubic-locale-archive@2.26-lp151.4.1.noarch", + "Name": "kubic-locale-archive", + "Version": "2.26", + "Release": "lp151.4.1", + "Arch": "noarch", + "SrcName": "kubic-locale-archive", + "SrcVersion": "2.26", + "SrcRelease": "lp151.4.1", + "Licenses": [ + "GPL-2.0+ AND MIT AND LGPL-2.1+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libacl1@2.2.52-lp151.4.29.x86_64", + "Name": "libacl1", + "Version": "2.2.52", + "Release": "lp151.4.29", + "Arch": "x86_64", + "SrcName": "acl", + "SrcVersion": "2.2.52", + "SrcRelease": "lp151.4.29", + "Licenses": [ + "GPL-2.0+ and LGPL-2.1+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libassuan0@2.5.1-lp151.3.3.x86_64", + "Name": "libassuan0", + "Version": "2.5.1", + "Release": "lp151.3.3", + "Arch": "x86_64", + "SrcName": "libassuan", + "SrcVersion": "2.5.1", + "SrcRelease": "lp151.3.3", + "Licenses": [ + "GPL-3.0+ and LGPL-2.1+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libattr1@2.4.47-lp151.3.70.x86_64", + "Name": "libattr1", + "Version": "2.4.47", + "Release": "lp151.3.70", + "Arch": "x86_64", + "SrcName": "attr", + "SrcVersion": "2.4.47", + "SrcRelease": "lp151.3.70", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libaudit1@2.8.1-lp151.4.66.x86_64", + "Name": "libaudit1", + "Version": "2.8.1", + "Release": "lp151.4.66", + "Arch": "x86_64", + "SrcName": "audit", + "SrcVersion": "2.8.1", + "SrcRelease": "lp151.4.66", + "Licenses": [ + "LGPL-2.1+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libaugeas0@1.10.1-lp151.2.3.x86_64", + "Name": "libaugeas0", + "Version": "1.10.1", + "Release": "lp151.2.3", + "Arch": "x86_64", + "SrcName": "augeas", + "SrcVersion": "1.10.1", + "SrcRelease": "lp151.2.3", + "Licenses": [ + "GPL-3.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libblkid1@2.33.1-lp151.3.3.2.x86_64", + "Name": "libblkid1", + "Version": "2.33.1", + "Release": "lp151.3.3.2", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "lp151.3.3.2", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libboost_system1_66_0@1.66.0-lp151.4.5.x86_64", + "Name": "libboost_system1_66_0", + "Version": "1.66.0", + "Release": "lp151.4.5", + "Arch": "x86_64", + "SrcName": "boost-base", + "SrcVersion": "1.66.0", + "SrcRelease": "lp151.4.5", + "Licenses": [ + "BSL-1.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libboost_thread1_66_0@1.66.0-lp151.4.5.x86_64", + "Name": "libboost_thread1_66_0", + "Version": "1.66.0", + "Release": "lp151.4.5", + "Arch": "x86_64", + "SrcName": "boost-base", + "SrcVersion": "1.66.0", + "SrcRelease": "lp151.4.5", + "Licenses": [ + "BSL-1.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libbz2-1@1.0.6-lp151.5.9.1.x86_64", + "Name": "libbz2-1", + "Version": "1.0.6", + "Release": "lp151.5.9.1", + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.6", + "SrcRelease": "lp151.5.9.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap-ng0@0.7.9-lp151.3.39.x86_64", + "Name": "libcap-ng0", + "Version": "0.7.9", + "Release": "lp151.3.39", + "Arch": "x86_64", + "SrcName": "libcap-ng", + "SrcVersion": "0.7.9", + "SrcRelease": "lp151.3.39", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap2@2.25-lp151.3.70.x86_64", + "Name": "libcap2", + "Version": "2.25", + "Release": "lp151.3.70", + "Arch": "x86_64", + "SrcName": "libcap", + "SrcVersion": "2.25", + "SrcRelease": "lp151.3.70", + "Licenses": [ + "BSD-3-Clause and GPL-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcom_err2@1.43.8-lp151.5.6.1.x86_64", + "Name": "libcom_err2", + "Version": "1.43.8", + "Release": "lp151.5.6.1", + "Arch": "x86_64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.43.8", + "SrcRelease": "lp151.5.6.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcrack2@2.9.6-lp151.3.69.x86_64", + "Name": "libcrack2", + "Version": "2.9.6", + "Release": "lp151.3.69", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.6", + "SrcRelease": "lp151.3.69", + "Licenses": [ + "LGPL-2.1" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcurl4@7.60.0-lp151.5.6.1.x86_64", + "Name": "libcurl4", + "Version": "7.60.0", + "Release": "lp151.5.6.1", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.60.0", + "SrcRelease": "lp151.5.6.1", + "Licenses": [ + "curl" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libdw1@0.168-lp151.4.3.1.x86_64", + "Name": "libdw1", + "Version": "0.168", + "Release": "lp151.4.3.1", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.168", + "SrcRelease": "lp151.4.3.1", + "Licenses": [ + "SUSE-GPL-2.0-with-OSI-exception" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libebl-plugins@0.168-lp151.4.3.1.x86_64", + "Name": "libebl-plugins", + "Version": "0.168", + "Release": "lp151.4.3.1", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.168", + "SrcRelease": "lp151.4.3.1", + "Licenses": [ + "SUSE-GPL-2.0-with-OSI-exception" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libelf1@0.168-lp151.4.3.1.x86_64", + "Name": "libelf1", + "Version": "0.168", + "Release": "lp151.4.3.1", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.168", + "SrcRelease": "lp151.4.3.1", + "Licenses": [ + "SUSE-GPL-2.0-with-OSI-exception" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libfdisk1@2.33.1-lp151.3.3.2.x86_64", + "Name": "libfdisk1", + "Version": "2.33.1", + "Release": "lp151.3.3.2", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "lp151.3.3.2", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libffi7@3.2.1.git259-lp151.5.2.x86_64", + "Name": "libffi7", + "Version": "3.2.1.git259", + "Release": "lp151.5.2", + "Arch": "x86_64", + "SrcName": "libffi", + "SrcVersion": "3.2.1.git259", + "SrcRelease": "lp151.5.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcc_s1@8.2.1+r264010-lp151.1.33.x86_64", + "Name": "libgcc_s1", + "Version": "8.2.1+r264010", + "Release": "lp151.1.33", + "Arch": "x86_64", + "SrcName": "gcc8", + "SrcVersion": "8.2.1+r264010", + "SrcRelease": "lp151.1.33", + "Licenses": [ + "GPL-3.0-with-GCC-exception" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcrypt20@1.8.2-lp151.9.4.1.x86_64", + "Name": "libgcrypt20", + "Version": "1.8.2", + "Release": "lp151.9.4.1", + "Arch": "x86_64", + "SrcName": "libgcrypt", + "SrcVersion": "1.8.2", + "SrcRelease": "lp151.9.4.1", + "Licenses": [ + "GPL-2.0+ AND LGPL-2.1+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgmp10@6.1.2-lp151.3.70.x86_64", + "Name": "libgmp10", + "Version": "6.1.2", + "Release": "lp151.3.70", + "Arch": "x86_64", + "SrcName": "gmp", + "SrcVersion": "6.1.2", + "SrcRelease": "lp151.3.70", + "Licenses": [ + "GPL-3.0+ and LGPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgnutls30@3.6.7-lp151.2.3.1.x86_64", + "Name": "libgnutls30", + "Version": "3.6.7", + "Release": "lp151.2.3.1", + "Arch": "x86_64", + "SrcName": "gnutls", + "SrcVersion": "3.6.7", + "SrcRelease": "lp151.2.3.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgpg-error0@1.29-lp151.2.4.x86_64", + "Name": "libgpg-error0", + "Version": "1.29", + "Release": "lp151.2.4", + "Arch": "x86_64", + "SrcName": "libgpg-error", + "SrcVersion": "1.29", + "SrcRelease": "lp151.2.4", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgpgme11@1.10.0-lp151.4.1.x86_64", + "Name": "libgpgme11", + "Version": "1.10.0", + "Release": "lp151.4.1", + "Arch": "x86_64", + "SrcName": "gpgme", + "SrcVersion": "1.10.0", + "SrcRelease": "lp151.4.1", + "Licenses": [ + "LGPL-2.1-or-later AND GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libhogweed4@3.4.1-lp151.1.1.x86_64", + "Name": "libhogweed4", + "Version": "3.4.1", + "Release": "lp151.1.1", + "Arch": "x86_64", + "SrcName": "libnettle", + "SrcVersion": "3.4.1", + "SrcRelease": "lp151.1.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libidn2-0@2.0.4-lp151.2.3.x86_64", + "Name": "libidn2-0", + "Version": "2.0.4", + "Release": "lp151.2.3", + "Arch": "x86_64", + "SrcName": "libidn2", + "SrcVersion": "2.0.4", + "SrcRelease": "lp151.2.3", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libkeyutils1@1.5.10-lp151.4.70.x86_64", + "Name": "libkeyutils1", + "Version": "1.5.10", + "Release": "lp151.4.70", + "Arch": "x86_64", + "SrcName": "keyutils", + "SrcVersion": "1.5.10", + "SrcRelease": "lp151.4.70", + "Licenses": [ + "GPL-2.0+ and LGPL-2.1+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libksba8@1.3.5-lp151.3.3.x86_64", + "Name": "libksba8", + "Version": "1.3.5", + "Release": "lp151.3.3", + "Arch": "x86_64", + "SrcName": "libksba", + "SrcVersion": "1.3.5", + "SrcRelease": "lp151.3.3", + "Licenses": [ + "(LGPL-3.0+ or GPL-2.0+) and GPL-3.0+ and MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libldap-2_4-2@2.4.46-lp151.10.3.1.x86_64", + "Name": "libldap-2_4-2", + "Version": "2.4.46", + "Release": "lp151.10.3.1", + "Arch": "x86_64", + "SrcName": "openldap2", + "SrcVersion": "2.4.46", + "SrcRelease": "lp151.10.3.1", + "Licenses": [ + "OLDAP-2.8" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "liblua5_3-5@5.3.4-lp151.3.25.x86_64", + "Name": "liblua5_3-5", + "Version": "5.3.4", + "Release": "lp151.3.25", + "Arch": "x86_64", + "SrcName": "lua53", + "SrcVersion": "5.3.4", + "SrcRelease": "lp151.3.25", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "liblz4-1@1.8.0-lp151.3.3.1.x86_64", + "Name": "liblz4-1", + "Version": "1.8.0", + "Release": "lp151.3.3.1", + "Arch": "x86_64", + "SrcName": "lz4", + "SrcVersion": "1.8.0", + "SrcRelease": "lp151.3.3.1", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "liblzma5@5.2.3-lp151.4.3.1.x86_64", + "Name": "liblzma5", + "Version": "5.2.3", + "Release": "lp151.4.3.1", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.3", + "SrcRelease": "lp151.4.3.1", + "Licenses": [ + "SUSE-Public-Domain" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmagic1@5.32-lp151.7.22.x86_64", + "Name": "libmagic1", + "Version": "5.32", + "Release": "lp151.7.22", + "Arch": "x86_64", + "SrcName": "file", + "SrcVersion": "5.32", + "SrcRelease": "lp151.7.22", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmodman1@2.0.1-lp151.2.3.x86_64", + "Name": "libmodman1", + "Version": "2.0.1", + "Release": "lp151.2.3", + "Arch": "x86_64", + "SrcName": "libmodman", + "SrcVersion": "2.0.1", + "SrcRelease": "lp151.2.3", + "Licenses": [ + "LGPL-2.1+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmount1@2.33.1-lp151.3.3.2.x86_64", + "Name": "libmount1", + "Version": "2.33.1", + "Release": "lp151.3.3.2", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "lp151.3.3.2", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libncurses6@6.1-lp151.5.41.x86_64", + "Name": "libncurses6", + "Version": "6.1", + "Release": "lp151.5.41", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.1", + "SrcRelease": "lp151.5.41", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnettle6@3.4.1-lp151.1.1.x86_64", + "Name": "libnettle6", + "Version": "3.4.1", + "Release": "lp151.1.1", + "Arch": "x86_64", + "SrcName": "libnettle", + "SrcVersion": "3.4.1", + "SrcRelease": "lp151.1.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnghttp2-14@1.39.2-lp151.3.3.1.x86_64", + "Name": "libnghttp2-14", + "Version": "1.39.2", + "Release": "lp151.3.3.1", + "Arch": "x86_64", + "SrcName": "nghttp2", + "SrcVersion": "1.39.2", + "SrcRelease": "lp151.3.3.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnpth0@1.5-lp151.3.2.x86_64", + "Name": "libnpth0", + "Version": "1.5", + "Release": "lp151.3.2", + "Arch": "x86_64", + "SrcName": "npth", + "SrcVersion": "1.5", + "SrcRelease": "lp151.3.2", + "Licenses": [ + "LGPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnsl2@1.2.0-lp151.3.67.x86_64", + "Name": "libnsl2", + "Version": "1.2.0", + "Release": "lp151.3.67", + "Arch": "x86_64", + "SrcName": "libnsl", + "SrcVersion": "1.2.0", + "SrcRelease": "lp151.3.67", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libopenssl1_1@1.1.0i-lp151.8.3.1.x86_64", + "Name": "libopenssl1_1", + "Version": "1.1.0i", + "Release": "lp151.8.3.1", + "Arch": "x86_64", + "SrcName": "openssl-1_1", + "SrcVersion": "1.1.0i", + "SrcRelease": "lp151.8.3.1", + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libp11-kit0@0.23.2-lp151.3.3.x86_64", + "Name": "libp11-kit0", + "Version": "0.23.2", + "Release": "lp151.3.3", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.2", + "SrcRelease": "lp151.3.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpcre1@8.41-lp151.5.67.x86_64", + "Name": "libpcre1", + "Version": "8.41", + "Release": "lp151.5.67", + "Arch": "x86_64", + "SrcName": "pcre", + "SrcVersion": "8.41", + "SrcRelease": "lp151.5.67", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpopt0@1.16-lp151.3.67.x86_64", + "Name": "libpopt0", + "Version": "1.16", + "Release": "lp151.3.67", + "Arch": "x86_64", + "SrcName": "popt", + "SrcVersion": "1.16", + "SrcRelease": "lp151.3.67", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libprocps7@3.3.15-lp151.6.3.1.x86_64", + "Name": "libprocps7", + "Version": "3.3.15", + "Release": "lp151.6.3.1", + "Arch": "x86_64", + "SrcName": "procps", + "SrcVersion": "3.3.15", + "SrcRelease": "lp151.6.3.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libproxy1@0.4.15-lp151.3.3.x86_64", + "Name": "libproxy1", + "Version": "0.4.15", + "Release": "lp151.3.3", + "Arch": "x86_64", + "SrcName": "libproxy", + "SrcVersion": "0.4.15", + "SrcRelease": "lp151.3.3", + "Licenses": [ + "GPL-2.0+ AND LGPL-2.1+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpsl5@0.20.1-lp151.2.43.x86_64", + "Name": "libpsl5", + "Version": "0.20.1", + "Release": "lp151.2.43", + "Arch": "x86_64", + "SrcName": "libpsl", + "SrcVersion": "0.20.1", + "SrcRelease": "lp151.2.43", + "Licenses": [ + "MIT AND MPL-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libreadline7@7.0-lp151.10.3.1.x86_64", + "Name": "libreadline7", + "Version": "7.0", + "Release": "lp151.10.3.1", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.4", + "SrcRelease": "lp151.10.3.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsasl2-3@2.1.26-lp151.5.1.x86_64", + "Name": "libsasl2-3", + "Version": "2.1.26", + "Release": "lp151.5.1", + "Arch": "x86_64", + "SrcName": "cyrus-sasl", + "SrcVersion": "2.1.26", + "SrcRelease": "lp151.5.1", + "Licenses": [ + "BSD-4-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libselinux1@2.8-lp151.1.30.x86_64", + "Name": "libselinux1", + "Version": "2.8", + "Release": "lp151.1.30", + "Arch": "x86_64", + "SrcName": "libselinux", + "SrcVersion": "2.8", + "SrcRelease": "lp151.1.30", + "Licenses": [ + "GPL-2.0-only AND SUSE-Public-Domain" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsemanage1@2.8-lp151.1.32.x86_64", + "Name": "libsemanage1", + "Version": "2.8", + "Release": "lp151.1.32", + "Arch": "x86_64", + "SrcName": "libsemanage", + "SrcVersion": "2.8", + "SrcRelease": "lp151.1.32", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsepol1@2.8-lp151.1.36.x86_64", + "Name": "libsepol1", + "Version": "2.8", + "Release": "lp151.1.36", + "Arch": "x86_64", + "SrcName": "libsepol", + "SrcVersion": "2.8", + "SrcRelease": "lp151.1.36", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsmartcols1@2.33.1-lp151.3.3.2.x86_64", + "Name": "libsmartcols1", + "Version": "2.33.1", + "Release": "lp151.3.3.2", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "lp151.3.3.2", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsolv-tools@0.7.6-lp151.2.3.2.x86_64", + "Name": "libsolv-tools", + "Version": "0.7.6", + "Release": "lp151.2.3.2", + "Arch": "x86_64", + "SrcName": "libsolv", + "SrcVersion": "0.7.6", + "SrcRelease": "lp151.2.3.2", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsqlite3-0@3.28.0-lp151.2.3.1.x86_64", + "Name": "libsqlite3-0", + "Version": "3.28.0", + "Release": "lp151.2.3.1", + "Arch": "x86_64", + "SrcName": "sqlite3", + "SrcVersion": "3.28.0", + "SrcRelease": "lp151.2.3.1", + "Licenses": [ + "SUSE-Public-Domain" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libssh4@0.8.7-lp151.2.3.1.x86_64", + "Name": "libssh4", + "Version": "0.8.7", + "Release": "lp151.2.3.1", + "Arch": "x86_64", + "SrcName": "libssh", + "SrcVersion": "0.8.7", + "SrcRelease": "lp151.2.3.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libstdc++6@8.2.1+r264010-lp151.1.33.x86_64", + "Name": "libstdc++6", + "Version": "8.2.1+r264010", + "Release": "lp151.1.33", + "Arch": "x86_64", + "SrcName": "gcc8", + "SrcVersion": "8.2.1+r264010", + "SrcRelease": "lp151.1.33", + "Licenses": [ + "GPL-3.0-with-GCC-exception" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsystemd0@234-lp151.26.4.1.x86_64", + "Name": "libsystemd0", + "Version": "234", + "Release": "lp151.26.4.1", + "Arch": "x86_64", + "SrcName": "systemd", + "SrcVersion": "234", + "SrcRelease": "lp151.26.4.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtasn1@4.13-lp151.4.3.1.x86_64", + "Name": "libtasn1", + "Version": "4.13", + "Release": "lp151.4.3.1", + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "4.13", + "SrcRelease": "lp151.4.3.1", + "Licenses": [ + "LGPL-2.1-or-later AND GPL-3.0-only" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtasn1-6@4.13-lp151.4.3.1.x86_64", + "Name": "libtasn1-6", + "Version": "4.13", + "Release": "lp151.4.3.1", + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "4.13", + "SrcRelease": "lp151.4.3.1", + "Licenses": [ + "LGPL-2.1-or-later AND GPL-3.0-only" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtirpc-netconfig@1.0.2-lp151.4.8.x86_64", + "Name": "libtirpc-netconfig", + "Version": "1.0.2", + "Release": "lp151.4.8", + "Arch": "x86_64", + "SrcName": "libtirpc", + "SrcVersion": "1.0.2", + "SrcRelease": "lp151.4.8", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtirpc3@1.0.2-lp151.4.8.x86_64", + "Name": "libtirpc3", + "Version": "1.0.2", + "Release": "lp151.4.8", + "Arch": "x86_64", + "SrcName": "libtirpc", + "SrcVersion": "1.0.2", + "SrcRelease": "lp151.4.8", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libudev1@234-lp151.26.4.1.x86_64", + "Name": "libudev1", + "Version": "234", + "Release": "lp151.26.4.1", + "Arch": "x86_64", + "SrcName": "systemd", + "SrcVersion": "234", + "SrcRelease": "lp151.26.4.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libunistring2@0.9.9-lp151.2.3.x86_64", + "Name": "libunistring2", + "Version": "0.9.9", + "Release": "lp151.2.3", + "Arch": "x86_64", + "SrcName": "libunistring", + "SrcVersion": "0.9.9", + "SrcRelease": "lp151.2.3", + "Licenses": [ + "LGPL-3.0-or-later OR GPL-2.0-only" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libusb-1_0-0@1.0.21-lp151.2.3.x86_64", + "Name": "libusb-1_0-0", + "Version": "1.0.21", + "Release": "lp151.2.3", + "Arch": "x86_64", + "SrcName": "libusb-1_0", + "SrcVersion": "1.0.21", + "SrcRelease": "lp151.2.3", + "Licenses": [ + "LGPL-2.1+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libutempter0@1.1.6-lp151.4.70.x86_64", + "Name": "libutempter0", + "Version": "1.1.6", + "Release": "lp151.4.70", + "Arch": "x86_64", + "SrcName": "utempter", + "SrcVersion": "1.1.6", + "SrcRelease": "lp151.4.70", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libuuid1@2.33.1-lp151.3.3.2.x86_64", + "Name": "libuuid1", + "Version": "2.33.1", + "Release": "lp151.3.3.2", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "lp151.3.3.2", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libverto1@0.2.6-lp151.4.70.x86_64", + "Name": "libverto1", + "Version": "0.2.6", + "Release": "lp151.4.70", + "Arch": "x86_64", + "SrcName": "libverto", + "SrcVersion": "0.2.6", + "SrcRelease": "lp151.4.70", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libxml2-2@2.9.7-lp151.5.3.1.x86_64", + "Name": "libxml2-2", + "Version": "2.9.7", + "Release": "lp151.5.3.1", + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.9.7", + "SrcRelease": "lp151.5.3.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libz1@1.2.11-lp151.5.3.1.x86_64", + "Name": "libz1", + "Version": "1.2.11", + "Release": "lp151.5.3.1", + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.11", + "SrcRelease": "lp151.5.3.1", + "Licenses": [ + "Zlib" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzio1@1.06-lp151.3.71.x86_64", + "Name": "libzio1", + "Version": "1.06", + "Release": "lp151.3.71", + "Arch": "x86_64", + "SrcName": "libzio", + "SrcVersion": "1.06", + "SrcRelease": "lp151.3.71", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzstd1@1.4.2-lp151.3.3.1.x86_64", + "Name": "libzstd1", + "Version": "1.4.2", + "Release": "lp151.3.3.1", + "Arch": "x86_64", + "SrcName": "zstd", + "SrcVersion": "1.4.2", + "SrcRelease": "lp151.3.3.1", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0-only" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzypp@17.15.0-lp151.2.3.2.x86_64", + "Name": "libzypp", + "Version": "17.15.0", + "Release": "lp151.2.3.2", + "Arch": "x86_64", + "SrcName": "libzypp", + "SrcVersion": "17.15.0", + "SrcRelease": "lp151.2.3.2", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-utils@6.1-lp151.5.41.x86_64", + "Name": "ncurses-utils", + "Version": "6.1", + "Release": "lp151.5.41", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.1", + "SrcRelease": "lp151.5.41", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "netcfg@11.6-lp151.2.2.noarch", + "Name": "netcfg", + "Version": "11.6", + "Release": "lp151.2.2", + "Arch": "noarch", + "SrcName": "netcfg", + "SrcVersion": "11.6", + "SrcRelease": "lp151.2.2", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openSUSE-build-key@1.0-lp151.3.1.noarch", + "Name": "openSUSE-build-key", + "Version": "1.0", + "Release": "lp151.3.1", + "Arch": "noarch", + "SrcName": "openSUSE-build-key", + "SrcVersion": "1.0", + "SrcRelease": "lp151.3.1", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openSUSE-release@15.1-lp151.301.1.x86_64", + "Name": "openSUSE-release", + "Version": "15.1", + "Release": "lp151.301.1", + "Arch": "x86_64", + "SrcName": "openSUSE-release", + "SrcVersion": "15.1", + "SrcRelease": "lp151.301.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openSUSE-release-appliance-docker@15.1-lp151.301.1.x86_64", + "Name": "openSUSE-release-appliance-docker", + "Version": "15.1", + "Release": "lp151.301.1", + "Arch": "x86_64", + "SrcName": "openSUSE-release", + "SrcVersion": "15.1", + "SrcRelease": "lp151.301.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openssl@1.1.0i-lp151.1.1.noarch", + "Name": "openssl", + "Version": "1.1.0i", + "Release": "lp151.1.1", + "Arch": "noarch", + "SrcName": "openssl", + "SrcVersion": "1.1.0i", + "SrcRelease": "lp151.1.1", + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openssl-1_1@1.1.0i-lp151.8.3.1.x86_64", + "Name": "openssl-1_1", + "Version": "1.1.0i", + "Release": "lp151.8.3.1", + "Arch": "x86_64", + "SrcName": "openssl-1_1", + "SrcVersion": "1.1.0i", + "SrcRelease": "lp151.8.3.1", + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit@0.23.2-lp151.3.3.x86_64", + "Name": "p11-kit", + "Version": "0.23.2", + "Release": "lp151.3.3", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.2", + "SrcRelease": "lp151.3.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit-tools@0.23.2-lp151.3.3.x86_64", + "Name": "p11-kit-tools", + "Version": "0.23.2", + "Release": "lp151.3.3", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.2", + "SrcRelease": "lp151.3.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pam@1.3.0-lp151.7.38.x86_64", + "Name": "pam", + "Version": "1.3.0", + "Release": "lp151.7.38", + "Arch": "x86_64", + "SrcName": "pam", + "SrcVersion": "1.3.0", + "SrcRelease": "lp151.7.38", + "Licenses": [ + "GPL-2.0+ or BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "perl-base@5.26.1-lp151.8.37.x86_64", + "Name": "perl-base", + "Version": "5.26.1", + "Release": "lp151.8.37", + "Arch": "x86_64", + "SrcName": "perl", + "SrcVersion": "5.26.1", + "SrcRelease": "lp151.8.37", + "Licenses": [ + "Artistic-1.0 or GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "permissions@20181116-lp151.4.6.1.x86_64", + "Name": "permissions", + "Version": "20181116", + "Release": "lp151.4.6.1", + "Arch": "x86_64", + "SrcName": "permissions", + "SrcVersion": "20181116", + "SrcRelease": "lp151.4.6.1", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pinentry@1.1.0-lp151.4.3.1.x86_64", + "Name": "pinentry", + "Version": "1.1.0", + "Release": "lp151.4.3.1", + "Arch": "x86_64", + "SrcName": "pinentry", + "SrcVersion": "1.1.0", + "SrcRelease": "lp151.4.3.1", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "procps@3.3.15-lp151.6.3.1.x86_64", + "Name": "procps", + "Version": "3.3.15", + "Release": "lp151.6.3.1", + "Arch": "x86_64", + "SrcName": "procps", + "SrcVersion": "3.3.15", + "SrcRelease": "lp151.6.3.1", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm@4.14.1-lp151.13.10.x86_64", + "Name": "rpm", + "Version": "4.14.1", + "Release": "lp151.13.10", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.14.1", + "SrcRelease": "lp151.13.10", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sed@4.4-lp151.3.4.x86_64", + "Name": "sed", + "Version": "4.4", + "Release": "lp151.3.4", + "Arch": "x86_64", + "SrcName": "sed", + "SrcVersion": "4.4", + "SrcRelease": "lp151.3.4", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "shadow@4.6-lp151.2.3.2.x86_64", + "Name": "shadow", + "Version": "4.6", + "Release": "lp151.2.3.2", + "Arch": "x86_64", + "SrcName": "shadow", + "SrcVersion": "4.6", + "SrcRelease": "lp151.2.3.2", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "system-group-hardware@20170617-lp151.4.70.noarch", + "Name": "system-group-hardware", + "Version": "20170617", + "Release": "lp151.4.70", + "Arch": "noarch", + "SrcName": "system-users", + "SrcVersion": "20170617", + "SrcRelease": "lp151.4.70", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "system-user-root@20190513-lp151.3.3.1.noarch", + "Name": "system-user-root", + "Version": "20190513", + "Release": "lp151.3.3.1", + "Arch": "noarch", + "SrcName": "system-user-root", + "SrcVersion": "20190513", + "SrcRelease": "lp151.3.3.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sysuser-shadow@2.0-lp151.3.70.noarch", + "Name": "sysuser-shadow", + "Version": "2.0", + "Release": "lp151.3.70", + "Arch": "noarch", + "SrcName": "sysuser-tools", + "SrcVersion": "2.0", + "SrcRelease": "lp151.3.70", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "terminfo-base@6.1-lp151.5.41.x86_64", + "Name": "terminfo-base", + "Version": "6.1", + "Release": "lp151.5.41", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.1", + "SrcRelease": "lp151.5.41", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "update-alternatives@1.19.0.4-lp151.3.67.x86_64", + "Name": "update-alternatives", + "Version": "1.19.0.4", + "Release": "lp151.3.67", + "Arch": "x86_64", + "SrcName": "update-alternatives", + "SrcVersion": "1.19.0.4", + "SrcRelease": "lp151.3.67", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "util-linux@2.33.1-lp151.3.3.2.x86_64", + "Name": "util-linux", + "Version": "2.33.1", + "Release": "lp151.3.3.2", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.33.1", + "SrcRelease": "lp151.3.3.2", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "zypper@1.14.30-lp151.2.3.1.x86_64", + "Name": "zypper", + "Version": "1.14.30", + "Release": "lp151.2.3.1", + "Arch": "x86_64", + "SrcName": "zypper", + "SrcVersion": "1.14.30", + "SrcRelease": "lp151.2.3.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-tumbleweed.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-tumbleweed.json.golden new file mode 100644 index 000000000000..5f7d3c2db1ad --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/opensuse-tumbleweed.json.golden @@ -0,0 +1,2262 @@ +[ + { + "ID": "aaa_base@84.87+git20240523.10a5692-1.1.x86_64", + "Name": "aaa_base", + "Version": "84.87+git20240523.10a5692", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "aaa_base", + "SrcVersion": "84.87+git20240523.10a5692", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bash@5.2.26-12.1.x86_64", + "Name": "bash", + "Version": "5.2.26", + "Release": "12.1", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "5.2.26", + "SrcRelease": "12.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bash-sh@5.2.26-12.1.noarch", + "Name": "bash-sh", + "Version": "5.2.26", + "Release": "12.1", + "Arch": "noarch", + "SrcName": "bash", + "SrcVersion": "5.2.26", + "SrcRelease": "12.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "boost-license1_85_0@1.85.0-1.2.noarch", + "Name": "boost-license1_85_0", + "Version": "1.85.0", + "Release": "1.2", + "Arch": "noarch", + "SrcName": "boost-base", + "SrcVersion": "1.85.0", + "SrcRelease": "1.2", + "Licenses": [ + "BSL-1.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "branding-openSUSE@84.87.20240405-1.2.noarch", + "Name": "branding-openSUSE", + "Version": "84.87.20240405", + "Release": "1.2", + "Arch": "noarch", + "SrcName": "branding-openSUSE", + "SrcVersion": "84.87.20240405", + "SrcRelease": "1.2", + "Licenses": [ + "BSD-3-Clause AND CC-BY-SA-3.0 AND GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates@2+git20240415.3fe9324-1.1.noarch", + "Name": "ca-certificates", + "Version": "2+git20240415.3fe9324", + "Release": "1.1", + "Arch": "noarch", + "SrcName": "ca-certificates", + "SrcVersion": "2+git20240415.3fe9324", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates-mozilla@2.66-1.2.noarch", + "Name": "ca-certificates-mozilla", + "Version": "2.66", + "Release": "1.2", + "Arch": "noarch", + "SrcName": "ca-certificates-mozilla", + "SrcVersion": "2.66", + "SrcRelease": "1.2", + "Licenses": [ + "MPL-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "coreutils@9.5-1.1.x86_64", + "Name": "coreutils", + "Version": "9.5", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "9.5", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cracklib-dict-small@2.9.11-1.4.x86_64", + "Name": "cracklib-dict-small", + "Version": "2.9.11", + "Release": "1.4", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.11", + "SrcRelease": "1.4", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "crypto-policies@20230920.570ea89-3.2.noarch", + "Name": "crypto-policies", + "Version": "20230920.570ea89", + "Release": "3.2", + "Arch": "noarch", + "SrcName": "crypto-policies", + "SrcVersion": "20230920.570ea89", + "SrcRelease": "3.2", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "curl@8.8.0-1.1.x86_64", + "Name": "curl", + "Version": "8.8.0", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "8.8.0", + "SrcRelease": "1.1", + "Licenses": [ + "curl" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "file-magic@5.45-2.2.noarch", + "Name": "file-magic", + "Version": "5.45", + "Release": "2.2", + "Arch": "noarch", + "SrcName": "file", + "SrcVersion": "5.45", + "SrcRelease": "2.2", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "filesystem@84.87-15.3.x86_64", + "Name": "filesystem", + "Version": "84.87", + "Release": "15.3", + "Arch": "x86_64", + "SrcName": "filesystem", + "SrcVersion": "84.87", + "SrcRelease": "15.3", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "fillup@1.42-281.1.x86_64", + "Name": "fillup", + "Version": "1.42", + "Release": "281.1", + "Arch": "x86_64", + "SrcName": "fillup", + "SrcVersion": "1.42", + "SrcRelease": "281.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc@2.39-9.1.x86_64", + "Name": "glibc", + "Version": "2.39", + "Release": "9.1", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.39", + "SrcRelease": "9.1", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later AND LGPL-2.1-or-later WITH GCC-exception-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc-locale-base@2.39-9.1.x86_64", + "Name": "glibc-locale-base", + "Version": "2.39", + "Release": "9.1", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.39", + "SrcRelease": "9.1", + "Licenses": [ + "GPL-2.0-or-later AND MIT AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@29b700a4-62b07e22.", + "Name": "gpg-pubkey", + "Version": "29b700a4", + "Release": "62b07e22", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@39db7c82-510a966b.", + "Name": "gpg-pubkey", + "Version": "39db7c82", + "Release": "510a966b", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg2@2.4.5-1.1.x86_64", + "Name": "gpg2", + "Version": "2.4.5", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "gpg2", + "SrcVersion": "2.4.5", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "grep@3.11-3.1.x86_64", + "Name": "grep", + "Version": "3.11", + "Release": "3.1", + "Arch": "x86_64", + "SrcName": "grep", + "SrcVersion": "3.11", + "SrcRelease": "3.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gzip@1.13-3.1.x86_64", + "Name": "gzip", + "Version": "1.13", + "Release": "3.1", + "Arch": "x86_64", + "SrcName": "gzip", + "SrcVersion": "1.13", + "SrcRelease": "3.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "krb5@1.21.2-5.1.x86_64", + "Name": "krb5", + "Version": "1.21.2", + "Release": "5.1", + "Arch": "x86_64", + "SrcName": "krb5", + "SrcVersion": "1.21.2", + "SrcRelease": "5.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libabsl_lite_2401_0_0@20240116.2-2.1.x86_64", + "Name": "libabsl_lite_2401_0_0", + "Version": "20240116.2", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "abseil-cpp", + "SrcVersion": "20240116.2", + "SrcRelease": "2.1", + "Licenses": [ + "Apache-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libacl1@2.3.2-2.1.x86_64", + "Name": "libacl1", + "Version": "2.3.2", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "acl", + "SrcVersion": "2.3.2", + "SrcRelease": "2.1", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libassuan0@2.5.7-1.1.x86_64", + "Name": "libassuan0", + "Version": "2.5.7", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libassuan", + "SrcVersion": "2.5.7", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-3.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libattr1@2.5.2-1.2.x86_64", + "Name": "libattr1", + "Version": "2.5.2", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "attr", + "SrcVersion": "2.5.2", + "SrcRelease": "1.2", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libaudit1@3.1.1-1.6.x86_64", + "Name": "libaudit1", + "Version": "3.1.1", + "Release": "1.6", + "Arch": "x86_64", + "SrcName": "audit", + "SrcVersion": "3.1.1", + "SrcRelease": "1.6", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libaugeas0@1.14.1-1.3.x86_64", + "Name": "libaugeas0", + "Version": "1.14.1", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "augeas", + "SrcVersion": "1.14.1", + "SrcRelease": "1.3", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libblkid1@2.40.1-2.1.x86_64", + "Name": "libblkid1", + "Version": "2.40.1", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.40.1", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libboost_thread1_85_0@1.85.0-1.2.x86_64", + "Name": "libboost_thread1_85_0", + "Version": "1.85.0", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "boost-base", + "SrcVersion": "1.85.0", + "SrcRelease": "1.2", + "Licenses": [ + "BSL-1.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libbrotlicommon1@1.1.0-1.3.x86_64", + "Name": "libbrotlicommon1", + "Version": "1.1.0", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "brotli", + "SrcVersion": "1.1.0", + "SrcRelease": "1.3", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libbrotlidec1@1.1.0-1.3.x86_64", + "Name": "libbrotlidec1", + "Version": "1.1.0", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "brotli", + "SrcVersion": "1.1.0", + "SrcRelease": "1.3", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libbz2-1@1.0.8-5.10.x86_64", + "Name": "libbz2-1", + "Version": "1.0.8", + "Release": "5.10", + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.8", + "SrcRelease": "5.10", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap-ng0@0.8.5-1.1.x86_64", + "Name": "libcap-ng0", + "Version": "0.8.5", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libcap-ng", + "SrcVersion": "0.8.5", + "SrcRelease": "1.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap2@2.70-1.1.x86_64", + "Name": "libcap2", + "Version": "2.70", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libcap", + "SrcVersion": "2.70", + "SrcRelease": "1.1", + "Licenses": [ + "BSD-3-Clause OR GPL-2.0-only" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcom_err2@1.47.0-4.2.x86_64", + "Name": "libcom_err2", + "Version": "1.47.0", + "Release": "4.2", + "Arch": "x86_64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.47.0", + "SrcRelease": "4.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcrypt1@4.4.36-1.6.x86_64", + "Name": "libcrypt1", + "Version": "4.4.36", + "Release": "1.6", + "Arch": "x86_64", + "SrcName": "libxcrypt", + "SrcVersion": "4.4.36", + "SrcRelease": "1.6", + "Licenses": [ + "BSD-2-Clause AND LGPL-2.1-or-later AND BSD-3-Clause AND SUSE-Public-Domain" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcurl4@8.8.0-1.1.x86_64", + "Name": "libcurl4", + "Version": "8.8.0", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "8.8.0", + "SrcRelease": "1.1", + "Licenses": [ + "curl" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libeconf0@0.6.3-1.1.x86_64", + "Name": "libeconf0", + "Version": "0.6.3", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libeconf", + "SrcVersion": "0.6.3", + "SrcRelease": "1.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libfa1@1.14.1-1.3.x86_64", + "Name": "libfa1", + "Version": "1.14.1", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "augeas", + "SrcVersion": "1.14.1", + "SrcRelease": "1.3", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libfdisk1@2.40.1-2.1.x86_64", + "Name": "libfdisk1", + "Version": "2.40.1", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.40.1", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libffi8@3.4.6-1.1.x86_64", + "Name": "libffi8", + "Version": "3.4.6", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libffi", + "SrcVersion": "3.4.6", + "SrcRelease": "1.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcc_s1@14.1.0+git10173-1.1.x86_64", + "Name": "libgcc_s1", + "Version": "14.1.0+git10173", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "gcc14", + "SrcVersion": "14.1.0+git10173", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-3.0-or-later WITH GCC-exception-3.1" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcrypt20@1.10.3-3.3.x86_64", + "Name": "libgcrypt20", + "Version": "1.10.3", + "Release": "3.3", + "Arch": "x86_64", + "SrcName": "libgcrypt", + "SrcVersion": "1.10.3", + "SrcRelease": "3.3", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libglib-2_0-0@2.80.2-1.1.x86_64", + "Name": "libglib-2_0-0", + "Version": "2.80.2", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "glib2", + "SrcVersion": "2.80.2", + "SrcRelease": "1.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgmp10@6.3.0-3.2.x86_64", + "Name": "libgmp10", + "Version": "6.3.0", + "Release": "3.2", + "Arch": "x86_64", + "SrcName": "gmp", + "SrcVersion": "6.3.0", + "SrcRelease": "3.2", + "Licenses": [ + "GPL-2.0-or-later OR LGPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgpg-error0@1.49-1.1.x86_64", + "Name": "libgpg-error0", + "Version": "1.49", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libgpg-error", + "SrcVersion": "1.49", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgpgme11@1.23.2-4.2.x86_64", + "Name": "libgpgme11", + "Version": "1.23.2", + "Release": "4.2", + "Arch": "x86_64", + "SrcName": "gpgme", + "SrcVersion": "1.23.2", + "SrcRelease": "4.2", + "Licenses": [ + "GPL-3.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libidn2-0@2.3.7-1.2.x86_64", + "Name": "libidn2-0", + "Version": "2.3.7", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "libidn2", + "SrcVersion": "2.3.7", + "SrcRelease": "1.2", + "Licenses": [ + "GPL-2.0-or-later OR LGPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libkeyutils1@1.6.3-7.2.x86_64", + "Name": "libkeyutils1", + "Version": "1.6.3", + "Release": "7.2", + "Arch": "x86_64", + "SrcName": "keyutils", + "SrcVersion": "1.6.3", + "SrcRelease": "7.2", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libksba8@1.6.6-1.1.x86_64", + "Name": "libksba8", + "Version": "1.6.6", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libksba", + "SrcVersion": "1.6.6", + "SrcRelease": "1.1", + "Licenses": [ + "(GPL-2.0-or-later OR LGPL-3.0-or-later) AND GPL-3.0-or-later AND MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libldap2@2.6.7-2.1.x86_64", + "Name": "libldap2", + "Version": "2.6.7", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "openldap2", + "SrcVersion": "2.6.7", + "SrcRelease": "2.1", + "Licenses": [ + "OLDAP-2.8" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "liblua5_4-5@5.4.6-3.3.x86_64", + "Name": "liblua5_4-5", + "Version": "5.4.6", + "Release": "3.3", + "Arch": "x86_64", + "SrcName": "lua54", + "SrcVersion": "5.4.6", + "SrcRelease": "3.3", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "liblz4-1@1.9.4-2.8.x86_64", + "Name": "liblz4-1", + "Version": "1.9.4", + "Release": "2.8", + "Arch": "x86_64", + "SrcName": "lz4", + "SrcVersion": "1.9.4", + "SrcRelease": "2.8", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "liblzma5@5.6.2-1.1.x86_64", + "Name": "liblzma5", + "Version": "5.6.2", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.6.2", + "SrcRelease": "1.1", + "Licenses": [ + "0BSD" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmagic1@5.45-2.2.x86_64", + "Name": "libmagic1", + "Version": "5.45", + "Release": "2.2", + "Arch": "x86_64", + "SrcName": "file", + "SrcVersion": "5.45", + "SrcRelease": "2.2", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmount1@2.40.1-2.1.x86_64", + "Name": "libmount1", + "Version": "2.40.1", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.40.1", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libncurses6@6.5.20240601-38.1.x86_64", + "Name": "libncurses6", + "Version": "6.5.20240601", + "Release": "38.1", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.5.20240601", + "SrcRelease": "38.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnghttp2-14@1.61.0-1.1.x86_64", + "Name": "libnghttp2-14", + "Version": "1.61.0", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "nghttp2", + "SrcVersion": "1.61.0", + "SrcRelease": "1.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnpth0@1.7-1.1.x86_64", + "Name": "libnpth0", + "Version": "1.7", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "npth", + "SrcVersion": "1.7", + "SrcRelease": "1.1", + "Licenses": [ + "LGPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnss_usrfiles2@2.27.1-1.2.x86_64", + "Name": "libnss_usrfiles2", + "Version": "2.27.1", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "libnss_usrfiles", + "SrcVersion": "2.27.1", + "SrcRelease": "1.2", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libopenssl-3-fips-provider@3.1.4-9.1.x86_64", + "Name": "libopenssl-3-fips-provider", + "Version": "3.1.4", + "Release": "9.1", + "Arch": "x86_64", + "SrcName": "openssl-3", + "SrcVersion": "3.1.4", + "SrcRelease": "9.1", + "Licenses": [ + "Apache-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libopenssl3@3.1.4-9.1.x86_64", + "Name": "libopenssl3", + "Version": "3.1.4", + "Release": "9.1", + "Arch": "x86_64", + "SrcName": "openssl-3", + "SrcVersion": "3.1.4", + "SrcRelease": "9.1", + "Licenses": [ + "Apache-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libp11-kit0@0.25.3-1.3.x86_64", + "Name": "libp11-kit0", + "Version": "0.25.3", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.25.3", + "SrcRelease": "1.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpcre2-8-0@10.43-3.1.x86_64", + "Name": "libpcre2-8-0", + "Version": "10.43", + "Release": "3.1", + "Arch": "x86_64", + "SrcName": "pcre2", + "SrcVersion": "10.43", + "SrcRelease": "3.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpopt0@1.19-1.8.x86_64", + "Name": "libpopt0", + "Version": "1.19", + "Release": "1.8", + "Arch": "x86_64", + "SrcName": "popt", + "SrcVersion": "1.19", + "SrcRelease": "1.8", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libprocps8@3.3.17-17.1.x86_64", + "Name": "libprocps8", + "Version": "3.3.17", + "Release": "17.1", + "Arch": "x86_64", + "SrcName": "procps", + "SrcVersion": "3.3.17", + "SrcRelease": "17.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libprotobuf-lite25_3_0@25.3-11.2.x86_64", + "Name": "libprotobuf-lite25_3_0", + "Version": "25.3", + "Release": "11.2", + "Arch": "x86_64", + "SrcName": "protobuf", + "SrcVersion": "25.3", + "SrcRelease": "11.2", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpsl5@0.21.5-1.2.x86_64", + "Name": "libpsl5", + "Version": "0.21.5", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "libpsl", + "SrcVersion": "0.21.5", + "SrcRelease": "1.2", + "Licenses": [ + "MIT AND MPL-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libreadline8@8.2.10-1.3.x86_64", + "Name": "libreadline8", + "Version": "8.2.10", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "readline", + "SrcVersion": "8.2.10", + "SrcRelease": "1.3", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsasl2-3@2.1.28-8.1.x86_64", + "Name": "libsasl2-3", + "Version": "2.1.28", + "Release": "8.1", + "Arch": "x86_64", + "SrcName": "cyrus-sasl", + "SrcVersion": "2.1.28", + "SrcRelease": "8.1", + "Licenses": [ + "BSD-4-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libselinux1@3.6-1.3.x86_64", + "Name": "libselinux1", + "Version": "3.6", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "libselinux", + "SrcVersion": "3.6", + "SrcRelease": "1.3", + "Licenses": [ + "SUSE-Public-Domain" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsemanage-conf@3.6-2.1.x86_64", + "Name": "libsemanage-conf", + "Version": "3.6", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "libsemanage", + "SrcVersion": "3.6", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsemanage2@3.6-2.1.x86_64", + "Name": "libsemanage2", + "Version": "3.6", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "libsemanage", + "SrcVersion": "3.6", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsepol2@3.6-1.3.x86_64", + "Name": "libsepol2", + "Version": "3.6", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "libsepol", + "SrcVersion": "3.6", + "SrcRelease": "1.3", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsigc-2_0-0@2.12.1-2.3.x86_64", + "Name": "libsigc-2_0-0", + "Version": "2.12.1", + "Release": "2.3", + "Arch": "x86_64", + "SrcName": "libsigc++2", + "SrcVersion": "2.12.1", + "SrcRelease": "2.3", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsmartcols1@2.40.1-2.1.x86_64", + "Name": "libsmartcols1", + "Version": "2.40.1", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.40.1", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsolv-tools-base@0.7.29-1.1.x86_64", + "Name": "libsolv-tools-base", + "Version": "0.7.29", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libsolv", + "SrcVersion": "0.7.29", + "SrcRelease": "1.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsqlite3-0@3.46.0-1.1.x86_64", + "Name": "libsqlite3-0", + "Version": "3.46.0", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "sqlite3", + "SrcVersion": "3.46.0", + "SrcRelease": "1.1", + "Licenses": [ + "SUSE-Public-Domain" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libssh-config@0.10.6-2.1.x86_64", + "Name": "libssh-config", + "Version": "0.10.6", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "libssh", + "SrcVersion": "0.10.6", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libssh4@0.10.6-2.1.x86_64", + "Name": "libssh4", + "Version": "0.10.6", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "libssh", + "SrcVersion": "0.10.6", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libstdc++6@14.1.0+git10173-1.1.x86_64", + "Name": "libstdc++6", + "Version": "14.1.0+git10173", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "gcc14", + "SrcVersion": "14.1.0+git10173", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-3.0-or-later WITH GCC-exception-3.1" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsubid4@4.15.1-1.2.x86_64", + "Name": "libsubid4", + "Version": "4.15.1", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "shadow", + "SrcVersion": "4.15.1", + "SrcRelease": "1.2", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsystemd0@255.7-2.1.x86_64", + "Name": "libsystemd0", + "Version": "255.7", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "systemd", + "SrcVersion": "255.7", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtasn1-6@4.19.0-1.7.x86_64", + "Name": "libtasn1-6", + "Version": "4.19.0", + "Release": "1.7", + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "4.19.0", + "SrcRelease": "1.7", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libudev1@255.7-2.1.x86_64", + "Name": "libudev1", + "Version": "255.7", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "systemd", + "SrcVersion": "255.7", + "SrcRelease": "2.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libunistring5@1.2-1.1.x86_64", + "Name": "libunistring5", + "Version": "1.2", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libunistring", + "SrcVersion": "1.2", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-3.0-or-later OR LGPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libusb-1_0-0@1.0.27-1.2.x86_64", + "Name": "libusb-1_0-0", + "Version": "1.0.27", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "libusb-1_0", + "SrcVersion": "1.0.27", + "SrcRelease": "1.2", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libuuid1@2.40.1-2.1.x86_64", + "Name": "libuuid1", + "Version": "2.40.1", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.40.1", + "SrcRelease": "2.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libverto1@0.3.2-3.3.x86_64", + "Name": "libverto1", + "Version": "0.3.2", + "Release": "3.3", + "Arch": "x86_64", + "SrcName": "libverto", + "SrcVersion": "0.3.2", + "SrcRelease": "3.3", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libxml2-2@2.12.7-1.1.x86_64", + "Name": "libxml2-2", + "Version": "2.12.7", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.12.7", + "SrcRelease": "1.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libyaml-cpp0_8@0.8.0-1.3.x86_64", + "Name": "libyaml-cpp0_8", + "Version": "0.8.0", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "yaml-cpp", + "SrcVersion": "0.8.0", + "SrcRelease": "1.3", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libz1@1.3.1-1.1.x86_64", + "Name": "libz1", + "Version": "1.3.1", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.3.1", + "SrcRelease": "1.1", + "Licenses": [ + "Zlib" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzck1@1.4.0-2.1.x86_64", + "Name": "libzck1", + "Version": "1.4.0", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "zchunk", + "SrcVersion": "1.4.0", + "SrcRelease": "2.1", + "Licenses": [ + "BSD-2-Clause AND MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzstd1@1.5.6-1.1.x86_64", + "Name": "libzstd1", + "Version": "1.5.6", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "zstd", + "SrcVersion": "1.5.6", + "SrcRelease": "1.1", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0-only" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzypp@17.34.1-1.1.x86_64", + "Name": "libzypp", + "Version": "17.34.1", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libzypp", + "SrcVersion": "17.34.1", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "login_defs@4.15.1-1.2.noarch", + "Name": "login_defs", + "Version": "4.15.1", + "Release": "1.2", + "Arch": "noarch", + "SrcName": "shadow", + "SrcVersion": "4.15.1", + "SrcRelease": "1.2", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "lsb-release@3.3-1.3.noarch", + "Name": "lsb-release", + "Version": "3.3", + "Release": "1.3", + "Arch": "noarch", + "SrcName": "lsb-release", + "SrcVersion": "3.3", + "SrcRelease": "1.3", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-utils@6.5.20240601-38.1.x86_64", + "Name": "ncurses-utils", + "Version": "6.5.20240601", + "Release": "38.1", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.5.20240601", + "SrcRelease": "38.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "netcfg@11.6-13.3.noarch", + "Name": "netcfg", + "Version": "11.6", + "Release": "13.3", + "Arch": "noarch", + "SrcName": "netcfg", + "SrcVersion": "11.6", + "SrcRelease": "13.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openSUSE-build-key@1.0-53.1.x86_64", + "Name": "openSUSE-build-key", + "Version": "1.0", + "Release": "53.1", + "Arch": "x86_64", + "SrcName": "openSUSE-build-key", + "SrcVersion": "1.0", + "SrcRelease": "53.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openSUSE-release@20240607-2943.1.x86_64", + "Name": "openSUSE-release", + "Version": "20240607", + "Release": "2943.1", + "Arch": "x86_64", + "SrcName": "openSUSE-release", + "SrcVersion": "20240607", + "SrcRelease": "2943.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openSUSE-release-appliance-docker@20240607-2943.1.x86_64", + "Name": "openSUSE-release-appliance-docker", + "Version": "20240607", + "Release": "2943.1", + "Arch": "x86_64", + "SrcName": "openSUSE-release", + "SrcVersion": "20240607", + "SrcRelease": "2943.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openssl@3.1.4-3.2.noarch", + "Name": "openssl", + "Version": "3.1.4", + "Release": "3.2", + "Arch": "noarch", + "SrcName": "openssl", + "SrcVersion": "3.1.4", + "SrcRelease": "3.2", + "Licenses": [ + "Apache-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openssl-3@3.1.4-9.1.x86_64", + "Name": "openssl-3", + "Version": "3.1.4", + "Release": "9.1", + "Arch": "x86_64", + "SrcName": "openssl-3", + "SrcVersion": "3.1.4", + "SrcRelease": "9.1", + "Licenses": [ + "Apache-2.0" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit@0.25.3-1.3.x86_64", + "Name": "p11-kit", + "Version": "0.25.3", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.25.3", + "SrcRelease": "1.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit-tools@0.25.3-1.3.x86_64", + "Name": "p11-kit-tools", + "Version": "0.25.3", + "Release": "1.3", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.25.3", + "SrcRelease": "1.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pam@1.6.1-1.1.x86_64", + "Name": "pam", + "Version": "1.6.1", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "pam", + "SrcVersion": "1.6.1", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-2.0-or-later OR BSD-3-Clause" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "patterns-base-fips@20200505-51.1.x86_64", + "Name": "patterns-base-fips", + "Version": "20200505", + "Release": "51.1", + "Arch": "x86_64", + "SrcName": "patterns-base", + "SrcVersion": "20200505", + "SrcRelease": "51.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "patterns-base-minimal_base@20200505-51.1.x86_64", + "Name": "patterns-base-minimal_base", + "Version": "20200505", + "Release": "51.1", + "Arch": "x86_64", + "SrcName": "patterns-base", + "SrcVersion": "20200505", + "SrcRelease": "51.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "permctl@1699_20240522-1.1.x86_64", + "Name": "permctl", + "Version": "1699_20240522", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "permissions", + "SrcVersion": "1699_20240522", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "permissions@1699_20240522-1.1.x86_64", + "Name": "permissions", + "Version": "1699_20240522", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "permissions", + "SrcVersion": "1699_20240522", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "permissions-config@1699_20240522-1.1.noarch", + "Name": "permissions-config", + "Version": "1699_20240522", + "Release": "1.1", + "Arch": "noarch", + "SrcName": "permissions", + "SrcVersion": "1699_20240522", + "SrcRelease": "1.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pinentry@1.2.1-3.5.x86_64", + "Name": "pinentry", + "Version": "1.2.1", + "Release": "3.5", + "Arch": "x86_64", + "SrcName": "pinentry", + "SrcVersion": "1.2.1", + "SrcRelease": "3.5", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "procps@3.3.17-17.1.x86_64", + "Name": "procps", + "Version": "3.3.17", + "Release": "17.1", + "Arch": "x86_64", + "SrcName": "procps", + "SrcVersion": "3.3.17", + "SrcRelease": "17.1", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm@4.19.1.1-3.2.x86_64", + "Name": "rpm", + "Version": "4.19.1.1", + "Release": "3.2", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.19.1.1", + "SrcRelease": "3.2", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-config-SUSE@20240214-1.2.noarch", + "Name": "rpm-config-SUSE", + "Version": "20240214", + "Release": "1.2", + "Arch": "noarch", + "SrcName": "rpm-config-SUSE", + "SrcVersion": "20240214", + "SrcRelease": "1.2", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sed@4.9-2.6.x86_64", + "Name": "sed", + "Version": "4.9", + "Release": "2.6", + "Arch": "x86_64", + "SrcName": "sed", + "SrcVersion": "4.9", + "SrcRelease": "2.6", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "shadow@4.15.1-1.2.x86_64", + "Name": "shadow", + "Version": "4.15.1", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "shadow", + "SrcVersion": "4.15.1", + "SrcRelease": "1.2", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "system-user-root@20190513-2.16.noarch", + "Name": "system-user-root", + "Version": "20190513", + "Release": "2.16", + "Arch": "noarch", + "SrcName": "system-user-root", + "SrcVersion": "20190513", + "SrcRelease": "2.16", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "tar@1.35-2.2.x86_64", + "Name": "tar", + "Version": "1.35", + "Release": "2.2", + "Arch": "x86_64", + "SrcName": "tar", + "SrcVersion": "1.35", + "SrcRelease": "2.2", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "terminfo-base@6.5.20240601-38.1.x86_64", + "Name": "terminfo-base", + "Version": "6.5.20240601", + "Release": "38.1", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.5.20240601", + "SrcRelease": "38.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "timezone@2024a-3.2.x86_64", + "Name": "timezone", + "Version": "2024a", + "Release": "3.2", + "Arch": "x86_64", + "SrcName": "timezone", + "SrcVersion": "2024a", + "SrcRelease": "3.2", + "Licenses": [ + "BSD-3-Clause AND SUSE-Public-Domain" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "util-linux@2.40.1-2.1.x86_64", + "Name": "util-linux", + "Version": "2.40.1", + "Release": "2.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.40.1", + "SrcRelease": "2.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "xz@5.6.2-1.1.x86_64", + "Name": "xz", + "Version": "5.6.2", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.6.2", + "SrcRelease": "1.1", + "Licenses": [ + "0BSD AND GPL-2.0-or-later AND GPL-3.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "zypper@1.14.73-1.2.x86_64", + "Name": "zypper", + "Version": "1.14.73", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "zypper", + "SrcVersion": "1.14.73", + "SrcRelease": "1.2", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "openSUSE", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/oraclelinux-8-slim.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/oraclelinux-8-slim.json.golden new file mode 100644 index 000000000000..0780d41d550d --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/oraclelinux-8-slim.json.golden @@ -0,0 +1,1190 @@ +[ + { + "Name": "ncurses-base", + "Version": "6.1", + "Release": "7.20180224.el8", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "ncurses", + "SrcVersion": "6.1", + "SrcRelease": "7.20180224.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "publicsuffix-list-dafsa", + "Version": "20180723", + "Release": "1.el8", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "publicsuffix-list", + "SrcVersion": "20180723", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "pcre2", + "Version": "10.32", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "pcre2", + "SrcVersion": "10.32", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "setup", + "Version": "2.12.2", + "Release": "2.el8", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "setup", + "SrcVersion": "2.12.2", + "SrcRelease": "2.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "basesystem", + "Version": "11", + "Release": "5.el8", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "basesystem", + "SrcVersion": "11", + "SrcRelease": "5.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libselinux", + "Version": "2.8", + "Release": "6.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libselinux", + "SrcVersion": "2.8", + "SrcRelease": "6.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "bash", + "Version": "4.4.19", + "Release": "8.el8_0", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.4.19", + "SrcRelease": "8.el8_0", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "glibc", + "Version": "2.28", + "Release": "42.0.1.el8_0.1", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.28", + "SrcRelease": "42.0.1.el8_0.1", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "zlib", + "Version": "1.2.11", + "Release": "10.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.11", + "SrcRelease": "10.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "info", + "Version": "6.5", + "Release": "4.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "texinfo", + "SrcVersion": "6.5", + "SrcRelease": "4.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "bzip2-libs", + "Version": "1.0.6", + "Release": "26.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.6", + "SrcRelease": "26.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libcom_err", + "Version": "1.44.3", + "Release": "2.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.3", + "SrcRelease": "2.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libffi", + "Version": "3.1", + "Release": "18.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libffi", + "SrcVersion": "3.1", + "SrcRelease": "18.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libxml2", + "Version": "2.9.7", + "Release": "5.0.1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.9.7", + "SrcRelease": "5.0.1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libidn2", + "Version": "2.0.5", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libidn2", + "SrcVersion": "2.0.5", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "readline", + "Version": "7.0", + "Release": "10.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "readline", + "SrcVersion": "7.0", + "SrcRelease": "10.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libassuan", + "Version": "2.5.1", + "Release": "3.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libassuan", + "SrcVersion": "2.5.1", + "SrcRelease": "3.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "elfutils-libelf", + "Version": "0.174", + "Release": "6.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.174", + "SrcRelease": "6.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "lua-libs", + "Version": "5.3.4", + "Release": "10.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "lua", + "SrcVersion": "5.3.4", + "SrcRelease": "10.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libacl", + "Version": "2.2.53", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "acl", + "SrcVersion": "2.2.53", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "sed", + "Version": "4.5", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "sed", + "SrcVersion": "4.5", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "grep", + "Version": "3.1", + "Release": "6.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "grep", + "SrcVersion": "3.1", + "SrcRelease": "6.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libtasn1", + "Version": "4.13", + "Release": "3.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "4.13", + "SrcRelease": "3.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "lz4-libs", + "Version": "1.8.1.2", + "Release": "4.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "lz4", + "SrcVersion": "1.8.1.2", + "SrcRelease": "4.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libmount", + "Version": "2.32.1", + "Release": "8.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.32.1", + "SrcRelease": "8.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "gobject-introspection", + "Version": "1.56.1", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gobject-introspection", + "SrcVersion": "1.56.1", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "systemd-libs", + "Version": "239", + "Release": "13.0.1.el8_0.5", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "systemd", + "SrcVersion": "239", + "SrcRelease": "13.0.1.el8_0.5", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "chkconfig", + "Version": "1.11", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "chkconfig", + "SrcVersion": "1.11", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "ca-certificates", + "Version": "2018.2.24", + "Release": "6.el8", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "ca-certificates", + "SrcVersion": "2018.2.24", + "SrcRelease": "6.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libdb", + "Version": "5.3.28", + "Release": "36.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libdb", + "SrcVersion": "5.3.28", + "SrcRelease": "36.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libarchive", + "Version": "3.3.2", + "Release": "3.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libarchive", + "SrcVersion": "3.3.2", + "SrcRelease": "3.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "mpfr", + "Version": "3.1.6", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "mpfr", + "SrcVersion": "3.1.6", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "gnutls", + "Version": "3.6.5", + "Release": "2.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gnutls", + "SrcVersion": "3.6.5", + "SrcRelease": "2.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "expat", + "Version": "2.2.5", + "Release": "3.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "expat", + "SrcVersion": "2.2.5", + "SrcRelease": "3.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "json-c", + "Version": "0.13.1", + "Release": "0.2.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "json-c", + "SrcVersion": "0.13.1", + "SrcRelease": "0.2.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "brotli", + "Version": "1.0.6", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "brotli", + "SrcVersion": "1.0.6", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "npth", + "Version": "1.5", + "Release": "4.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "npth", + "SrcVersion": "1.5", + "SrcRelease": "4.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libnghttp2", + "Version": "1.33.0", + "Release": "1.el8_0.1", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "nghttp2", + "SrcVersion": "1.33.0", + "SrcRelease": "1.el8_0.1", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libyaml", + "Version": "0.1.7", + "Release": "5.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libyaml", + "SrcVersion": "0.1.7", + "SrcRelease": "5.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libsigsegv", + "Version": "2.11", + "Release": "5.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libsigsegv", + "SrcVersion": "2.11", + "SrcRelease": "5.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "krb5-libs", + "Version": "1.16.1", + "Release": "22.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "krb5", + "SrcVersion": "1.16.1", + "SrcRelease": "22.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "cyrus-sasl-lib", + "Version": "2.1.27", + "Release": "0.3rc7.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "cyrus-sasl", + "SrcVersion": "2.1.27", + "SrcRelease": "0.3rc7.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libcurl", + "Version": "7.61.1", + "Release": "8.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.61.1", + "SrcRelease": "8.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "gnupg2", + "Version": "2.2.9", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gnupg2", + "SrcVersion": "2.2.9", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "librepo", + "Version": "1.9.2", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "librepo", + "SrcVersion": "1.9.2", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "audit-libs", + "Version": "3.0", + "Release": "0.10.20180831git0047a6c.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "audit", + "SrcVersion": "3.0", + "SrcRelease": "0.10.20180831git0047a6c.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "rpm", + "Version": "4.14.2", + "Release": "11.el8_0", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.14.2", + "SrcRelease": "11.el8_0", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libdnf", + "Version": "0.22.5", + "Release": "5.0.2.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libdnf", + "SrcVersion": "0.22.5", + "SrcRelease": "5.0.2.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "oraclelinux-release-el8", + "Version": "1.0", + "Release": "4.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "oraclelinux-release-el8", + "SrcVersion": "1.0", + "SrcRelease": "4.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libgcc", + "Version": "8.2.1", + "Release": "3.5.0.1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "8.2.1", + "SrcRelease": "3.5.0.1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "tzdata", + "Version": "2019c", + "Release": "1.el8", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "tzdata", + "SrcVersion": "2019c", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "redhat-release", + "Version": "8.0", + "Release": "0.44.0.1.el8", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "redhat-release", + "SrcVersion": "8.0", + "SrcRelease": "0.44.0.1.el8", + "SrcEpoch": 2, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "oraclelinux-release", + "Version": "8.0", + "Release": "1.0.14.el8", + "Epoch": 8, + "Arch": "x86_64", + "SrcName": "oraclelinux-release", + "SrcVersion": "8.0", + "SrcRelease": "1.0.14.el8", + "SrcEpoch": 8, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "filesystem", + "Version": "3.8", + "Release": "2.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "filesystem", + "SrcVersion": "3.8", + "SrcRelease": "2.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "ncurses-libs", + "Version": "6.1", + "Release": "7.20180224.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.1", + "SrcRelease": "7.20180224.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "glibc-minimal-langpack", + "Version": "2.28", + "Release": "42.0.1.el8_0.1", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.28", + "SrcRelease": "42.0.1.el8_0.1", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "glibc-common", + "Version": "2.28", + "Release": "42.0.1.el8_0.1", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.28", + "SrcRelease": "42.0.1.el8_0.1", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libsepol", + "Version": "2.8", + "Release": "2.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libsepol", + "SrcVersion": "2.8", + "SrcRelease": "2.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libgpg-error", + "Version": "1.31", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libgpg-error", + "SrcVersion": "1.31", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "xz-libs", + "Version": "5.2.4", + "Release": "3.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.4", + "SrcRelease": "3.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "gmp", + "Version": "6.1.2", + "Release": "8.el8", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "gmp", + "SrcVersion": "6.1.2", + "SrcRelease": "8.el8", + "SrcEpoch":1, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libcap", + "Version": "2.25", + "Release": "9.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libcap", + "SrcVersion": "2.25", + "SrcRelease": "9.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "p11-kit", + "Version": "0.23.14", + "Release": "5.el8_0", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.14", + "SrcRelease": "5.el8_0", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libunistring", + "Version": "0.9.9", + "Release": "3.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libunistring", + "SrcVersion": "0.9.9", + "SrcRelease": "3.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "popt", + "Version": "1.16", + "Release": "14.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "popt", + "SrcVersion": "1.16", + "SrcRelease": "14.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libgcrypt", + "Version": "1.8.3", + "Release": "2.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libgcrypt", + "SrcVersion": "1.8.3", + "SrcRelease": "2.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "sqlite-libs", + "Version": "3.26.0", + "Release": "3.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "sqlite", + "SrcVersion": "3.26.0", + "SrcRelease": "3.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libuuid", + "Version": "2.32.1", + "Release": "8.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.32.1", + "SrcRelease": "8.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libattr", + "Version": "2.4.48", + "Release": "3.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "attr", + "SrcVersion": "2.4.48", + "SrcRelease": "3.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "coreutils-single", + "Version": "8.30", + "Release": "6.0.1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "8.30", + "SrcRelease": "6.0.1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "pcre", + "Version": "8.42", + "Release": "4.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "pcre", + "SrcVersion": "8.42", + "SrcRelease": "4.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "crypto-policies", + "Version": "20181217", + "Release": "6.git9a35207.el8", + "Epoch": 0, + "Arch": "noarch", + "SrcName": "crypto-policies", + "SrcVersion": "20181217", + "SrcRelease": "6.git9a35207.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libsmartcols", + "Version": "2.32.1", + "Release": "8.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.32.1", + "SrcRelease": "8.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libblkid", + "Version": "2.32.1", + "Release": "8.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.32.1", + "SrcRelease": "8.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "glib2", + "Version": "2.56.4", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "glib2", + "SrcVersion": "2.56.4", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libpeas", + "Version": "1.22.0", + "Release": "6.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libpeas", + "SrcVersion": "1.22.0", + "SrcRelease": "6.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libusbx", + "Version": "1.0.22", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libusbx", + "SrcVersion": "1.0.22", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "p11-kit-trust", + "Version": "0.23.14", + "Release": "5.el8_0", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.14", + "SrcRelease": "5.el8_0", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "openssl-libs", + "Version": "1.1.1", + "Release": "8.0.1.el8", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "openssl", + "SrcVersion": "1.1.1", + "SrcRelease": "8.0.1.el8", + "SrcEpoch":1, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libdb-utils", + "Version": "5.3.28", + "Release": "36.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libdb", + "SrcVersion": "5.3.28", + "SrcRelease": "36.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libpsl", + "Version": "0.20.2", + "Release": "5.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libpsl", + "SrcVersion": "0.20.2", + "SrcRelease": "5.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "nettle", + "Version": "3.4.1", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "nettle", + "SrcVersion": "3.4.1", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libksba", + "Version": "1.3.5", + "Release": "7.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libksba", + "SrcVersion": "1.3.5", + "SrcRelease": "7.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libmetalink", + "Version": "0.1.3", + "Release": "7.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libmetalink", + "SrcVersion": "0.1.3", + "SrcRelease": "7.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "keyutils-libs", + "Version": "1.5.10", + "Release": "6.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "keyutils", + "SrcVersion": "1.5.10", + "SrcRelease": "6.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libstdc++", + "Version": "8.2.1", + "Release": "3.5.0.1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "8.2.1", + "SrcRelease": "3.5.0.1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libverto", + "Version": "0.3.0", + "Release": "5.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libverto", + "SrcVersion": "0.3.0", + "SrcRelease": "5.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libxcrypt", + "Version": "4.1.1", + "Release": "4.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libxcrypt", + "SrcVersion": "4.1.1", + "SrcRelease": "4.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libmodulemd1", + "Version": "1.8.0", + "Release": "5.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libmodulemd", + "SrcVersion": "2.0.0", + "SrcRelease": "5.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "gawk", + "Version": "4.2.1", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gawk", + "SrcVersion": "4.2.1", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libssh", + "Version": "0.8.5", + "Release": "2.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libssh", + "SrcVersion": "0.8.5", + "SrcRelease": "2.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "openldap", + "Version": "2.4.46", + "Release": "9.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "openldap", + "SrcVersion": "2.4.46", + "SrcRelease": "9.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "curl", + "Version": "7.61.1", + "Release": "8.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.61.1", + "SrcRelease": "8.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "gpgme", + "Version": "1.10.0", + "Release": "6.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "gpgme", + "SrcVersion": "1.10.0", + "SrcRelease": "6.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libcap-ng", + "Version": "0.7.9", + "Release": "4.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libcap-ng", + "SrcVersion": "0.7.9", + "SrcRelease": "4.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "rpm-libs", + "Version": "4.14.2", + "Release": "11.el8_0", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.14.2", + "SrcRelease": "11.el8_0", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "libsolv", + "Version": "0.6.35", + "Release": "6.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "libsolv", + "SrcVersion": "0.6.35", + "SrcRelease": "6.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "microdnf", + "Version": "3.0.1", + "Release": "1.el8", + "Epoch": 0, + "Arch": "x86_64", + "SrcName": "microdnf", + "SrcVersion": "3.0.1", + "SrcRelease": "1.el8", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + }, + { + "Name": "gpg-pubkey", + "Version": "ad986da3", + "Release": "5cabf60d", + "Epoch": 0, + "Arch": "(none)", + "SrcName": "", + "SrcVersion": "", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:e3196b7450602f5547c52d197255dfa96a006ea9c52c19bf3ba2d5412a4b161e" + } +] diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/photon-30.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/photon-30.json.golden new file mode 100644 index 000000000000..5af2622d99b4 --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/photon-30.json.golden @@ -0,0 +1,592 @@ +[ + { + "ID": "bash@4.4.18-1.ph3.x86_64", + "Name": "bash", + "Version": "4.4.18", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.4.18", + "SrcRelease": "1.ph3", + "Licenses": [ + "GPLv3" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bzip2-libs@1.0.6-10.ph3.x86_64", + "Name": "bzip2-libs", + "Version": "1.0.6", + "Release": "10.ph3", + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.6", + "SrcRelease": "10.ph3", + "Licenses": [ + "BSD" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates@20190521-1.ph3.x86_64", + "Name": "ca-certificates", + "Version": "20190521", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "ca-certificates", + "SrcVersion": "20190521", + "SrcRelease": "1.ph3", + "Licenses": [ + "Custom" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates-pki@20190521-1.ph3.x86_64", + "Name": "ca-certificates-pki", + "Version": "20190521", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "ca-certificates", + "SrcVersion": "20190521", + "SrcRelease": "1.ph3", + "Licenses": [ + "Custom" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "curl@7.61.1-4.ph3.x86_64", + "Name": "curl", + "Version": "7.61.1", + "Release": "4.ph3", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.61.1", + "SrcRelease": "4.ph3", + "Licenses": [ + "MIT" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "curl-libs@7.61.1-4.ph3.x86_64", + "Name": "curl-libs", + "Version": "7.61.1", + "Release": "4.ph3", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.61.1", + "SrcRelease": "4.ph3", + "Licenses": [ + "MIT" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "e2fsprogs-libs@1.44.3-2.ph3.x86_64", + "Name": "e2fsprogs-libs", + "Version": "1.44.3", + "Release": "2.ph3", + "Arch": "x86_64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.3", + "SrcRelease": "2.ph3", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "elfutils-libelf@0.176-1.ph3.x86_64", + "Name": "elfutils-libelf", + "Version": "0.176", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.176", + "SrcRelease": "1.ph3", + "Licenses": [ + "GPLv2+ or LGPLv3+" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "expat-libs@2.2.6-2.ph3.x86_64", + "Name": "expat-libs", + "Version": "2.2.6", + "Release": "2.ph3", + "Arch": "x86_64", + "SrcName": "expat", + "SrcVersion": "2.2.6", + "SrcRelease": "2.ph3", + "Licenses": [ + "MIT" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "filesystem@1.1-4.ph3.x86_64", + "Name": "filesystem", + "Version": "1.1", + "Release": "4.ph3", + "Arch": "x86_64", + "SrcName": "filesystem", + "SrcVersion": "1.1", + "SrcRelease": "4.ph3", + "Licenses": [ + "GPLv3" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc@2.28-3.ph3.x86_64", + "Name": "glibc", + "Version": "2.28", + "Release": "3.ph3", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.28", + "SrcRelease": "3.ph3", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@66fd4949-4803fe57.", + "Name": "gpg-pubkey", + "Version": "66fd4949", + "Release": "4803fe57", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "krb5@1.17-1.ph3.x86_64", + "Name": "krb5", + "Version": "1.17", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "krb5", + "SrcVersion": "1.17", + "SrcRelease": "1.ph3", + "Licenses": [ + "MIT" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap@2.25-8.ph3.x86_64", + "Name": "libcap", + "Version": "2.25", + "Release": "8.ph3", + "Arch": "x86_64", + "SrcName": "libcap", + "SrcVersion": "2.25", + "SrcRelease": "8.ph3", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libdb@5.3.28-2.ph3.x86_64", + "Name": "libdb", + "Version": "5.3.28", + "Release": "2.ph3", + "Arch": "x86_64", + "SrcName": "libdb", + "SrcVersion": "5.3.28", + "SrcRelease": "2.ph3", + "Licenses": [ + "BSD and LGPLv2 and Sleepycat" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcc@7.3.0-4.ph3.x86_64", + "Name": "libgcc", + "Version": "7.3.0", + "Release": "4.ph3", + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "7.3.0", + "SrcRelease": "4.ph3", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsolv@0.6.26-5.ph3.x86_64", + "Name": "libsolv", + "Version": "0.6.26", + "Release": "5.ph3", + "Arch": "x86_64", + "SrcName": "libsolv", + "SrcVersion": "0.6.26", + "SrcRelease": "5.ph3", + "Licenses": [ + "BSD" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libssh2@1.9.0-1.ph3.x86_64", + "Name": "libssh2", + "Version": "1.9.0", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "libssh2", + "SrcVersion": "1.9.0", + "SrcRelease": "1.ph3", + "Licenses": [ + "BSD" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-libs@6.1-1.ph3.x86_64", + "Name": "ncurses-libs", + "Version": "6.1", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.1", + "SrcRelease": "1.ph3", + "Licenses": [ + "MIT" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nspr@4.21-1.ph3.x86_64", + "Name": "nspr", + "Version": "4.21", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "nspr", + "SrcVersion": "4.21", + "SrcRelease": "1.ph3", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-libs@3.44-2.ph3.x86_64", + "Name": "nss-libs", + "Version": "3.44", + "Release": "2.ph3", + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.44", + "SrcRelease": "2.ph3", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openssl@1.0.2s-1.ph3.x86_64", + "Name": "openssl", + "Version": "1.0.2s", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "openssl", + "SrcVersion": "1.0.2s", + "SrcRelease": "1.ph3", + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "photon-release@3.0-3.ph3.noarch", + "Name": "photon-release", + "Version": "3.0", + "Release": "3.ph3", + "Arch": "noarch", + "SrcName": "photon-release", + "SrcVersion": "3.0", + "SrcRelease": "3.ph3", + "Licenses": [ + "Apache License" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "photon-repos@3.0-3.ph3.noarch", + "Name": "photon-repos", + "Version": "3.0", + "Release": "3.ph3", + "Arch": "noarch", + "SrcName": "photon-repos", + "SrcVersion": "3.0", + "SrcRelease": "3.ph3", + "Licenses": [ + "Apache License" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "popt@1.16-5.ph3.x86_64", + "Name": "popt", + "Version": "1.16", + "Release": "5.ph3", + "Arch": "x86_64", + "SrcName": "popt", + "SrcVersion": "1.16", + "SrcRelease": "5.ph3", + "Licenses": [ + "MIT" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "readline@7.0-2.ph3.x86_64", + "Name": "readline", + "Version": "7.0", + "Release": "2.ph3", + "Arch": "x86_64", + "SrcName": "readline", + "SrcVersion": "7.0", + "SrcRelease": "2.ph3", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-libs@4.14.2-4.ph3.x86_64", + "Name": "rpm-libs", + "Version": "4.14.2", + "Release": "4.ph3", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.14.2", + "SrcRelease": "4.ph3", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sqlite-libs@3.27.2-3.ph3.x86_64", + "Name": "sqlite-libs", + "Version": "3.27.2", + "Release": "3.ph3", + "Arch": "x86_64", + "SrcName": "sqlite", + "SrcVersion": "3.27.2", + "SrcRelease": "3.ph3", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "tdnf@2.0.0-10.ph3.x86_64", + "Name": "tdnf", + "Version": "2.0.0", + "Release": "10.ph3", + "Arch": "x86_64", + "SrcName": "tdnf", + "SrcVersion": "2.0.0", + "SrcRelease": "10.ph3", + "Licenses": [ + "LGPLv2.1,GPLv2" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "tdnf-cli-libs@2.0.0-10.ph3.x86_64", + "Name": "tdnf-cli-libs", + "Version": "2.0.0", + "Release": "10.ph3", + "Arch": "x86_64", + "SrcName": "tdnf", + "SrcVersion": "2.0.0", + "SrcRelease": "10.ph3", + "Licenses": [ + "LGPLv2.1,GPLv2" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "toybox@0.7.7-1.ph3.x86_64", + "Name": "toybox", + "Version": "0.7.7", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "toybox", + "SrcVersion": "0.7.7", + "SrcRelease": "1.ph3", + "Licenses": [ + "BSD" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "xz-libs@5.2.4-1.ph3.x86_64", + "Name": "xz-libs", + "Version": "5.2.4", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.4", + "SrcRelease": "1.ph3", + "Licenses": [ + "GPLv2+ and GPLv3+ and LGPLv2+" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "zlib@1.2.11-1.ph3.x86_64", + "Name": "zlib", + "Version": "1.2.11", + "Release": "1.ph3", + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.11", + "SrcRelease": "1.ph3", + "Licenses": [ + "zlib" + ], + "Maintainer": "VMware, Inc.", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/suse-15.3_ndb.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/suse-15.3_ndb.json.golden new file mode 100644 index 000000000000..128771653698 --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/suse-15.3_ndb.json.golden @@ -0,0 +1,2258 @@ +[ + { + "ID": "aaa_base@84.87+git20180409.04c9dae-3.45.1.x86_64", + "Name": "aaa_base", + "Version": "84.87+git20180409.04c9dae", + "Release": "3.45.1", + "Arch": "x86_64", + "SrcName": "aaa_base", + "SrcVersion": "84.87+git20180409.04c9dae", + "SrcRelease": "3.45.1", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bash@4.4-19.6.1.x86_64", + "Name": "bash", + "Version": "4.4", + "Release": "19.6.1", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.4", + "SrcRelease": "19.6.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "boost-license1_66_0@1.66.0-10.1.noarch", + "Name": "boost-license1_66_0", + "Version": "1.66.0", + "Release": "10.1", + "Arch": "noarch", + "SrcName": "boost-base", + "SrcVersion": "1.66.0", + "SrcRelease": "10.1", + "Licenses": [ + "BSL-1.0" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates@2+git20210309.21162a6-2.1.noarch", + "Name": "ca-certificates", + "Version": "2+git20210309.21162a6", + "Release": "2.1", + "Arch": "noarch", + "SrcName": "ca-certificates", + "SrcVersion": "2+git20210309.21162a6", + "SrcRelease": "2.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates-mozilla@2.44-21.1.noarch", + "Name": "ca-certificates-mozilla", + "Version": "2.44", + "Release": "21.1", + "Arch": "noarch", + "SrcName": "ca-certificates-mozilla", + "SrcVersion": "2.44", + "SrcRelease": "21.1", + "Licenses": [ + "MPL-2.0" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "container-suseconnect@2.3.0-4.15.2.x86_64", + "Name": "container-suseconnect", + "Version": "2.3.0", + "Release": "4.15.2", + "Arch": "x86_64", + "SrcName": "container-suseconnect", + "SrcVersion": "2.3.0", + "SrcRelease": "4.15.2", + "Licenses": [ + "Apache-2.0" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "coreutils@8.32-3.2.1.x86_64", + "Name": "coreutils", + "Version": "8.32", + "Release": "3.2.1", + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "8.32", + "SrcRelease": "3.2.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cpio@2.12-3.9.1.x86_64", + "Name": "cpio", + "Version": "2.12", + "Release": "3.9.1", + "Arch": "x86_64", + "SrcName": "cpio", + "SrcVersion": "2.12", + "SrcRelease": "3.9.1", + "Licenses": [ + "GPL-3.0" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cracklib@2.9.7-11.3.1.x86_64", + "Name": "cracklib", + "Version": "2.9.7", + "Release": "11.3.1", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.7", + "SrcRelease": "11.3.1", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cracklib-dict-small@2.9.7-11.3.1.x86_64", + "Name": "cracklib-dict-small", + "Version": "2.9.7", + "Release": "11.3.1", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.7", + "SrcRelease": "11.3.1", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "diffutils@3.6-4.3.1.x86_64", + "Name": "diffutils", + "Version": "3.6", + "Release": "4.3.1", + "Arch": "x86_64", + "SrcName": "diffutils", + "SrcVersion": "3.6", + "SrcRelease": "4.3.1", + "Licenses": [ + "GFDL-1.2 and GPL-3.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "file-magic@5.32-7.14.1.noarch", + "Name": "file-magic", + "Version": "5.32", + "Release": "7.14.1", + "Arch": "noarch", + "SrcName": "file", + "SrcVersion": "5.32", + "SrcRelease": "7.14.1", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "filesystem@15.0-11.3.2.x86_64", + "Name": "filesystem", + "Version": "15.0", + "Release": "11.3.2", + "Arch": "x86_64", + "SrcName": "filesystem", + "SrcVersion": "15.0", + "SrcRelease": "11.3.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "fillup@1.42-2.18.x86_64", + "Name": "fillup", + "Version": "1.42", + "Release": "2.18", + "Arch": "x86_64", + "SrcName": "fillup", + "SrcVersion": "1.42", + "SrcRelease": "2.18", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "findutils@4.8.0-1.20.x86_64", + "Name": "findutils", + "Version": "4.8.0", + "Release": "1.20", + "Arch": "x86_64", + "SrcName": "findutils", + "SrcVersion": "4.8.0", + "SrcRelease": "1.20", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc@2.31-9.3.2.x86_64", + "Name": "glibc", + "Version": "2.31", + "Release": "9.3.2", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.31", + "SrcRelease": "9.3.2", + "Licenses": [ + "LGPL-2.1-or-later AND LGPL-2.1-or-later WITH GCC-exception-2.0 AND GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@307e3d54-5aaa90a5.", + "Name": "gpg-pubkey", + "Version": "307e3d54", + "Release": "5aaa90a5", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@39db7c82-5f68629b.", + "Name": "gpg-pubkey", + "Version": "39db7c82", + "Release": "5f68629b", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg-pubkey@50a3dd1c-50f35137.", + "Name": "gpg-pubkey", + "Version": "50a3dd1c", + "Release": "50f35137", + "Arch": "None", + "Licenses": [ + "pubkey" + ], + "Repository": { + "Class": "third-party" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpg2@2.2.27-1.2.x86_64", + "Name": "gpg2", + "Version": "2.2.27", + "Release": "1.2", + "Arch": "x86_64", + "SrcName": "gpg2", + "SrcVersion": "2.2.27", + "SrcRelease": "1.2", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "grep@3.1-4.3.12.x86_64", + "Name": "grep", + "Version": "3.1", + "Release": "4.3.12", + "Arch": "x86_64", + "SrcName": "grep", + "SrcVersion": "3.1", + "SrcRelease": "4.3.12", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "info@6.5-4.17.x86_64", + "Name": "info", + "Version": "6.5", + "Release": "4.17", + "Arch": "x86_64", + "SrcName": "texinfo", + "SrcVersion": "6.5", + "SrcRelease": "4.17", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "krb5@1.16.3-3.24.1.x86_64", + "Name": "krb5", + "Version": "1.16.3", + "Release": "3.24.1", + "Arch": "x86_64", + "SrcName": "krb5", + "SrcVersion": "1.16.3", + "SrcRelease": "3.24.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "kubic-locale-archive@2.31-10.36.noarch", + "Name": "kubic-locale-archive", + "Version": "2.31", + "Release": "10.36", + "Arch": "noarch", + "SrcName": "kubic-locale-archive", + "SrcVersion": "2.31", + "SrcRelease": "10.36", + "Licenses": [ + "GPL-2.0+ AND MIT AND LGPL-2.1+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libacl1@2.2.52-4.3.1.x86_64", + "Name": "libacl1", + "Version": "2.2.52", + "Release": "4.3.1", + "Arch": "x86_64", + "SrcName": "acl", + "SrcVersion": "2.2.52", + "SrcRelease": "4.3.1", + "Licenses": [ + "GPL-2.0+ and LGPL-2.1+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libassuan0@2.5.1-2.14.x86_64", + "Name": "libassuan0", + "Version": "2.5.1", + "Release": "2.14", + "Arch": "x86_64", + "SrcName": "libassuan", + "SrcVersion": "2.5.1", + "SrcRelease": "2.14", + "Licenses": [ + "GPL-3.0+ and LGPL-2.1+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libattr1@2.4.47-2.19.x86_64", + "Name": "libattr1", + "Version": "2.4.47", + "Release": "2.19", + "Arch": "x86_64", + "SrcName": "attr", + "SrcVersion": "2.4.47", + "SrcRelease": "2.19", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libaudit1@2.8.5-3.43.x86_64", + "Name": "libaudit1", + "Version": "2.8.5", + "Release": "3.43", + "Arch": "x86_64", + "SrcName": "audit", + "SrcVersion": "2.8.5", + "SrcRelease": "3.43", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libaugeas0@1.10.1-1.11.x86_64", + "Name": "libaugeas0", + "Version": "1.10.1", + "Release": "1.11", + "Arch": "x86_64", + "SrcName": "augeas", + "SrcVersion": "1.10.1", + "SrcRelease": "1.11", + "Licenses": [ + "GPL-3.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libblkid1@2.36.2-4.5.1.x86_64", + "Name": "libblkid1", + "Version": "2.36.2", + "Release": "4.5.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.36.2", + "SrcRelease": "4.5.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libboost_system1_66_0@1.66.0-10.1.x86_64", + "Name": "libboost_system1_66_0", + "Version": "1.66.0", + "Release": "10.1", + "Arch": "x86_64", + "SrcName": "boost-base", + "SrcVersion": "1.66.0", + "SrcRelease": "10.1", + "Licenses": [ + "BSL-1.0" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libboost_thread1_66_0@1.66.0-10.1.x86_64", + "Name": "libboost_thread1_66_0", + "Version": "1.66.0", + "Release": "10.1", + "Arch": "x86_64", + "SrcName": "boost-base", + "SrcVersion": "1.66.0", + "SrcRelease": "10.1", + "Licenses": [ + "BSL-1.0" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libbz2-1@1.0.6-5.11.1.x86_64", + "Name": "libbz2-1", + "Version": "1.0.6", + "Release": "5.11.1", + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.6", + "SrcRelease": "5.11.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap-ng0@0.7.9-4.37.x86_64", + "Name": "libcap-ng0", + "Version": "0.7.9", + "Release": "4.37", + "Arch": "x86_64", + "SrcName": "libcap-ng", + "SrcVersion": "0.7.9", + "SrcRelease": "4.37", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap2@2.26-4.6.1.x86_64", + "Name": "libcap2", + "Version": "2.26", + "Release": "4.6.1", + "Arch": "x86_64", + "SrcName": "libcap", + "SrcVersion": "2.26", + "SrcRelease": "4.6.1", + "Licenses": [ + "BSD-3-Clause or GPL-2.0" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcom_err2@1.43.8-4.26.1.x86_64", + "Name": "libcom_err2", + "Version": "1.43.8", + "Release": "4.26.1", + "Arch": "x86_64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.43.8", + "SrcRelease": "4.26.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcrack2@2.9.7-11.3.1.x86_64", + "Name": "libcrack2", + "Version": "2.9.7", + "Release": "11.3.1", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.7", + "SrcRelease": "11.3.1", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcrypt1@4.4.15-2.51.x86_64", + "Name": "libcrypt1", + "Version": "4.4.15", + "Release": "2.51", + "Arch": "x86_64", + "SrcName": "libxcrypt", + "SrcVersion": "4.4.15", + "SrcRelease": "2.51", + "Licenses": [ + "LGPL-2.1-or-later AND BSD-2-Clause AND BSD-3-Clause AND SUSE-Public-Domain" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcurl4@7.66.0-4.27.1.x86_64", + "Name": "libcurl4", + "Version": "7.66.0", + "Release": "4.27.1", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.66.0", + "SrcRelease": "4.27.1", + "Licenses": [ + "curl" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libdw1@0.168-4.5.3.x86_64", + "Name": "libdw1", + "Version": "0.168", + "Release": "4.5.3", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.168", + "SrcRelease": "4.5.3", + "Licenses": [ + "SUSE-GPL-2.0-with-OSI-exception" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libebl-plugins@0.168-4.5.3.x86_64", + "Name": "libebl-plugins", + "Version": "0.168", + "Release": "4.5.3", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.168", + "SrcRelease": "4.5.3", + "Licenses": [ + "SUSE-GPL-2.0-with-OSI-exception" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libelf1@0.168-4.5.3.x86_64", + "Name": "libelf1", + "Version": "0.168", + "Release": "4.5.3", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.168", + "SrcRelease": "4.5.3", + "Licenses": [ + "SUSE-GPL-2.0-with-OSI-exception" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libfdisk1@2.36.2-4.5.1.x86_64", + "Name": "libfdisk1", + "Version": "2.36.2", + "Release": "4.5.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.36.2", + "SrcRelease": "4.5.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libffi7@3.2.1.git259-10.8.x86_64", + "Name": "libffi7", + "Version": "3.2.1.git259", + "Release": "10.8", + "Arch": "x86_64", + "SrcName": "libffi", + "SrcVersion": "3.2.1.git259", + "SrcRelease": "10.8", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcc_s1@10.3.0+git1587-1.6.4.x86_64", + "Name": "libgcc_s1", + "Version": "10.3.0+git1587", + "Release": "1.6.4", + "Arch": "x86_64", + "SrcName": "gcc10", + "SrcVersion": "10.3.0+git1587", + "SrcRelease": "1.6.4", + "Licenses": [ + "GPL-3.0 WITH GCC-exception-3.1" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcrypt20@1.8.2-8.39.1.x86_64", + "Name": "libgcrypt20", + "Version": "1.8.2", + "Release": "8.39.1", + "Arch": "x86_64", + "SrcName": "libgcrypt", + "SrcVersion": "1.8.2", + "SrcRelease": "8.39.1", + "Licenses": [ + "GPL-2.0+ AND LGPL-2.1+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcrypt20-hmac@1.8.2-8.39.1.x86_64", + "Name": "libgcrypt20-hmac", + "Version": "1.8.2", + "Release": "8.39.1", + "Arch": "x86_64", + "SrcName": "libgcrypt", + "SrcVersion": "1.8.2", + "SrcRelease": "8.39.1", + "Licenses": [ + "GPL-2.0+ AND LGPL-2.1+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libglib-2_0-0@2.62.6-3.6.1.x86_64", + "Name": "libglib-2_0-0", + "Version": "2.62.6", + "Release": "3.6.1", + "Arch": "x86_64", + "SrcName": "glib2", + "SrcVersion": "2.62.6", + "SrcRelease": "3.6.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgmp10@6.1.2-4.6.1.x86_64", + "Name": "libgmp10", + "Version": "6.1.2", + "Release": "4.6.1", + "Arch": "x86_64", + "SrcName": "gmp", + "SrcVersion": "6.1.2", + "SrcRelease": "4.6.1", + "Licenses": [ + "LGPL-3.0-or-later OR GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgpg-error0@1.29-1.8.x86_64", + "Name": "libgpg-error0", + "Version": "1.29", + "Release": "1.8", + "Arch": "x86_64", + "SrcName": "libgpg-error", + "SrcVersion": "1.29", + "SrcRelease": "1.8", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgpgme11@1.13.1-4.3.1.x86_64", + "Name": "libgpgme11", + "Version": "1.13.1", + "Release": "4.3.1", + "Arch": "x86_64", + "SrcName": "gpgme", + "SrcVersion": "1.13.1", + "SrcRelease": "4.3.1", + "Licenses": [ + "LGPL-2.1-or-later AND GPL-3.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libidn2-0@2.2.0-3.6.1.x86_64", + "Name": "libidn2-0", + "Version": "2.2.0", + "Release": "3.6.1", + "Arch": "x86_64", + "SrcName": "libidn2", + "SrcVersion": "2.2.0", + "SrcRelease": "3.6.1", + "Licenses": [ + "GPL-2.0-or-later OR LGPL-3.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libkeyutils1@1.5.10-5.3.1.x86_64", + "Name": "libkeyutils1", + "Version": "1.5.10", + "Release": "5.3.1", + "Arch": "x86_64", + "SrcName": "keyutils", + "SrcVersion": "1.5.10", + "SrcRelease": "5.3.1", + "Licenses": [ + "LGPL-2.1+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libksba8@1.3.5-2.14.x86_64", + "Name": "libksba8", + "Version": "1.3.5", + "Release": "2.14", + "Arch": "x86_64", + "SrcName": "libksba", + "SrcVersion": "1.3.5", + "SrcRelease": "2.14", + "Licenses": [ + "(LGPL-3.0+ or GPL-2.0+) and GPL-3.0+ and MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libldap-2_4-2@2.4.46-9.58.1.x86_64", + "Name": "libldap-2_4-2", + "Version": "2.4.46", + "Release": "9.58.1", + "Arch": "x86_64", + "SrcName": "openldap2", + "SrcVersion": "2.4.46", + "SrcRelease": "9.58.1", + "Licenses": [ + "OLDAP-2.8" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libldap-data@2.4.46-9.58.1.noarch", + "Name": "libldap-data", + "Version": "2.4.46", + "Release": "9.58.1", + "Arch": "noarch", + "SrcName": "openldap2", + "SrcVersion": "2.4.46", + "SrcRelease": "9.58.1", + "Licenses": [ + "OLDAP-2.8" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "liblua5_3-5@5.3.6-3.6.1.x86_64", + "Name": "liblua5_3-5", + "Version": "5.3.6", + "Release": "3.6.1", + "Arch": "x86_64", + "SrcName": "lua53", + "SrcVersion": "5.3.6", + "SrcRelease": "3.6.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "liblz4-1@1.9.2-3.3.1.x86_64", + "Name": "liblz4-1", + "Version": "1.9.2", + "Release": "3.3.1", + "Arch": "x86_64", + "SrcName": "lz4", + "SrcVersion": "1.9.2", + "SrcRelease": "3.3.1", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "liblzma5@5.2.3-4.3.1.x86_64", + "Name": "liblzma5", + "Version": "5.2.3", + "Release": "4.3.1", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.3", + "SrcRelease": "4.3.1", + "Licenses": [ + "SUSE-Public-Domain" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmagic1@5.32-7.14.1.x86_64", + "Name": "libmagic1", + "Version": "5.32", + "Release": "7.14.1", + "Arch": "x86_64", + "SrcName": "file", + "SrcVersion": "5.32", + "SrcRelease": "7.14.1", + "Licenses": [ + "BSD-2-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmodman1@2.0.1-1.27.x86_64", + "Name": "libmodman1", + "Version": "2.0.1", + "Release": "1.27", + "Arch": "x86_64", + "SrcName": "libmodman", + "SrcVersion": "2.0.1", + "SrcRelease": "1.27", + "Licenses": [ + "LGPL-2.1+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmount1@2.36.2-4.5.1.x86_64", + "Name": "libmount1", + "Version": "2.36.2", + "Release": "4.5.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.36.2", + "SrcRelease": "4.5.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libncurses6@6.1-5.6.2.x86_64", + "Name": "libncurses6", + "Version": "6.1", + "Release": "5.6.2", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.1", + "SrcRelease": "5.6.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnghttp2-14@1.40.0-6.1.x86_64", + "Name": "libnghttp2-14", + "Version": "1.40.0", + "Release": "6.1", + "Arch": "x86_64", + "SrcName": "nghttp2", + "SrcVersion": "1.40.0", + "SrcRelease": "6.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnpth0@1.5-2.11.x86_64", + "Name": "libnpth0", + "Version": "1.5", + "Release": "2.11", + "Arch": "x86_64", + "SrcName": "npth", + "SrcVersion": "1.5", + "SrcRelease": "2.11", + "Licenses": [ + "LGPL-2.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnsl2@1.2.0-2.44.x86_64", + "Name": "libnsl2", + "Version": "1.2.0", + "Release": "2.44", + "Arch": "x86_64", + "SrcName": "libnsl", + "SrcVersion": "1.2.0", + "SrcRelease": "2.44", + "Licenses": [ + "LGPL-2.1-only" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libopenssl1_1@1.1.1d-11.30.1.x86_64", + "Name": "libopenssl1_1", + "Version": "1.1.1d", + "Release": "11.30.1", + "Arch": "x86_64", + "SrcName": "openssl-1_1", + "SrcVersion": "1.1.1d", + "SrcRelease": "11.30.1", + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libopenssl1_1-hmac@1.1.1d-11.30.1.x86_64", + "Name": "libopenssl1_1-hmac", + "Version": "1.1.1d", + "Release": "11.30.1", + "Arch": "x86_64", + "SrcName": "openssl-1_1", + "SrcVersion": "1.1.1d", + "SrcRelease": "11.30.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libp11-kit0@0.23.2-4.8.3.x86_64", + "Name": "libp11-kit0", + "Version": "0.23.2", + "Release": "4.8.3", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.2", + "SrcRelease": "4.8.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpcre1@8.41-6.4.2.x86_64", + "Name": "libpcre1", + "Version": "8.41", + "Release": "6.4.2", + "Arch": "x86_64", + "SrcName": "pcre", + "SrcVersion": "8.41", + "SrcRelease": "6.4.2", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpopt0@1.16-3.22.x86_64", + "Name": "libpopt0", + "Version": "1.16", + "Release": "3.22", + "Arch": "x86_64", + "SrcName": "popt", + "SrcVersion": "1.16", + "SrcRelease": "3.22", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libprocps7@3.3.15-7.19.1.x86_64", + "Name": "libprocps7", + "Version": "3.3.15", + "Release": "7.19.1", + "Arch": "x86_64", + "SrcName": "procps", + "SrcVersion": "3.3.15", + "SrcRelease": "7.19.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libproxy1@0.4.15-12.41.x86_64", + "Name": "libproxy1", + "Version": "0.4.15", + "Release": "12.41", + "Arch": "x86_64", + "SrcName": "libproxy", + "SrcVersion": "0.4.15", + "SrcRelease": "12.41", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpsl5@0.20.1-1.20.x86_64", + "Name": "libpsl5", + "Version": "0.20.1", + "Release": "1.20", + "Arch": "x86_64", + "SrcName": "libpsl", + "SrcVersion": "0.20.1", + "SrcRelease": "1.20", + "Licenses": [ + "MIT AND MPL-2.0" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libreadline7@7.0-19.6.1.x86_64", + "Name": "libreadline7", + "Version": "7.0", + "Release": "19.6.1", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.4", + "SrcRelease": "19.6.1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsasl2-3@2.1.27-2.2.x86_64", + "Name": "libsasl2-3", + "Version": "2.1.27", + "Release": "2.2", + "Arch": "x86_64", + "SrcName": "cyrus-sasl", + "SrcVersion": "2.1.27", + "SrcRelease": "2.2", + "Licenses": [ + "BSD-4-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libselinux1@3.0-1.31.x86_64", + "Name": "libselinux1", + "Version": "3.0", + "Release": "1.31", + "Arch": "x86_64", + "SrcName": "libselinux", + "SrcVersion": "3.0", + "SrcRelease": "1.31", + "Licenses": [ + "SUSE-Public-Domain" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsemanage1@3.0-1.27.x86_64", + "Name": "libsemanage1", + "Version": "3.0", + "Release": "1.27", + "Arch": "x86_64", + "SrcName": "libsemanage", + "SrcVersion": "3.0", + "SrcRelease": "1.27", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsepol1@3.0-1.31.x86_64", + "Name": "libsepol1", + "Version": "3.0", + "Release": "1.31", + "Arch": "x86_64", + "SrcName": "libsepol", + "SrcVersion": "3.0", + "SrcRelease": "1.31", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsigc-2_0-0@2.10.2-1.18.x86_64", + "Name": "libsigc-2_0-0", + "Version": "2.10.2", + "Release": "1.18", + "Arch": "x86_64", + "SrcName": "libsigc++2", + "SrcVersion": "2.10.2", + "SrcRelease": "1.18", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsmartcols1@2.36.2-4.5.1.x86_64", + "Name": "libsmartcols1", + "Version": "2.36.2", + "Release": "4.5.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.36.2", + "SrcRelease": "4.5.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsolv-tools@0.7.19-6.1.x86_64", + "Name": "libsolv-tools", + "Version": "0.7.19", + "Release": "6.1", + "Arch": "x86_64", + "SrcName": "libsolv", + "SrcVersion": "0.7.19", + "SrcRelease": "6.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsqlite3-0@3.36.0-3.12.1.x86_64", + "Name": "libsqlite3-0", + "Version": "3.36.0", + "Release": "3.12.1", + "Arch": "x86_64", + "SrcName": "sqlite3", + "SrcVersion": "3.36.0", + "SrcRelease": "3.12.1", + "Licenses": [ + "SUSE-Public-Domain" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libssh4@0.8.7-10.12.1.x86_64", + "Name": "libssh4", + "Version": "0.8.7", + "Release": "10.12.1", + "Arch": "x86_64", + "SrcName": "libssh", + "SrcVersion": "0.8.7", + "SrcRelease": "10.12.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libstdc++6@10.3.0+git1587-1.6.4.x86_64", + "Name": "libstdc++6", + "Version": "10.3.0+git1587", + "Release": "1.6.4", + "Arch": "x86_64", + "SrcName": "gcc10", + "SrcVersion": "10.3.0+git1587", + "SrcRelease": "1.6.4", + "Licenses": [ + "GPL-3.0 WITH GCC-exception-3.1" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsystemd0@246.16-7.14.1.x86_64", + "Name": "libsystemd0", + "Version": "246.16", + "Release": "7.14.1", + "Arch": "x86_64", + "SrcName": "systemd", + "SrcVersion": "246.16", + "SrcRelease": "7.14.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtasn1@4.13-4.5.1.x86_64", + "Name": "libtasn1", + "Version": "4.13", + "Release": "4.5.1", + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "4.13", + "SrcRelease": "4.5.1", + "Licenses": [ + "LGPL-2.1-or-later AND GPL-3.0-only" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtasn1-6@4.13-4.5.1.x86_64", + "Name": "libtasn1-6", + "Version": "4.13", + "Release": "4.5.1", + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "4.13", + "SrcRelease": "4.5.1", + "Licenses": [ + "LGPL-2.1-or-later AND GPL-3.0-only" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtirpc-netconfig@1.2.6-1.131.x86_64", + "Name": "libtirpc-netconfig", + "Version": "1.2.6", + "Release": "1.131", + "Arch": "x86_64", + "SrcName": "libtirpc", + "SrcVersion": "1.2.6", + "SrcRelease": "1.131", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtirpc3@1.2.6-1.131.x86_64", + "Name": "libtirpc3", + "Version": "1.2.6", + "Release": "1.131", + "Arch": "x86_64", + "SrcName": "libtirpc", + "SrcVersion": "1.2.6", + "SrcRelease": "1.131", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libudev1@246.16-7.14.1.x86_64", + "Name": "libudev1", + "Version": "246.16", + "Release": "7.14.1", + "Arch": "x86_64", + "SrcName": "systemd", + "SrcVersion": "246.16", + "SrcRelease": "7.14.1", + "Licenses": [ + "LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libunistring2@0.9.10-1.1.x86_64", + "Name": "libunistring2", + "Version": "0.9.10", + "Release": "1.1", + "Arch": "x86_64", + "SrcName": "libunistring", + "SrcVersion": "0.9.10", + "SrcRelease": "1.1", + "Licenses": [ + "LGPL-3.0-or-later OR GPL-2.0-only" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libusb-1_0-0@1.0.21-3.3.1.x86_64", + "Name": "libusb-1_0-0", + "Version": "1.0.21", + "Release": "3.3.1", + "Arch": "x86_64", + "SrcName": "libusb-1_0", + "SrcVersion": "1.0.21", + "SrcRelease": "3.3.1", + "Licenses": [ + "LGPL-2.1+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libutempter0@1.1.6-3.42.x86_64", + "Name": "libutempter0", + "Version": "1.1.6", + "Release": "3.42", + "Arch": "x86_64", + "SrcName": "utempter", + "SrcVersion": "1.1.6", + "SrcRelease": "3.42", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libuuid1@2.36.2-4.5.1.x86_64", + "Name": "libuuid1", + "Version": "2.36.2", + "Release": "4.5.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.36.2", + "SrcRelease": "4.5.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libverto1@0.2.6-3.20.x86_64", + "Name": "libverto1", + "Version": "0.2.6", + "Release": "3.20", + "Arch": "x86_64", + "SrcName": "libverto", + "SrcVersion": "0.2.6", + "SrcRelease": "3.20", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libxml2-2@2.9.7-3.37.1.x86_64", + "Name": "libxml2-2", + "Version": "2.9.7", + "Release": "3.37.1", + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.9.7", + "SrcRelease": "3.37.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libyaml-cpp0_6@0.6.1-4.2.1.x86_64", + "Name": "libyaml-cpp0_6", + "Version": "0.6.1", + "Release": "4.2.1", + "Arch": "x86_64", + "SrcName": "yaml-cpp", + "SrcVersion": "0.6.1", + "SrcRelease": "4.2.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libz1@1.2.11-3.21.1.x86_64", + "Name": "libz1", + "Version": "1.2.11", + "Release": "3.21.1", + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.11", + "SrcRelease": "3.21.1", + "Licenses": [ + "Zlib" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzio1@1.06-2.20.x86_64", + "Name": "libzio1", + "Version": "1.06", + "Release": "2.20", + "Arch": "x86_64", + "SrcName": "libzio", + "SrcVersion": "1.06", + "SrcRelease": "2.20", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzstd1@1.4.4-1.6.1.x86_64", + "Name": "libzstd1", + "Version": "1.4.4", + "Release": "1.6.1", + "Arch": "x86_64", + "SrcName": "zstd", + "SrcVersion": "1.4.4", + "SrcRelease": "1.6.1", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0-only" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libzypp@17.27.0-12.1.x86_64", + "Name": "libzypp", + "Version": "17.27.0", + "Release": "12.1", + "Arch": "x86_64", + "SrcName": "libzypp", + "SrcVersion": "17.27.0", + "SrcRelease": "12.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "login_defs@4.8.1-2.43.noarch", + "Name": "login_defs", + "Version": "4.8.1", + "Release": "2.43", + "Arch": "noarch", + "SrcName": "shadow", + "SrcVersion": "4.8.1", + "SrcRelease": "2.43", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-utils@6.1-5.6.2.x86_64", + "Name": "ncurses-utils", + "Version": "6.1", + "Release": "5.6.2", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.1", + "SrcRelease": "5.6.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "netcfg@11.6-3.3.1.noarch", + "Name": "netcfg", + "Version": "11.6", + "Release": "3.3.1", + "Arch": "noarch", + "SrcName": "netcfg", + "SrcVersion": "11.6", + "SrcRelease": "3.3.1", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openssl-1_1@1.1.1d-11.30.1.x86_64", + "Name": "openssl-1_1", + "Version": "1.1.1d", + "Release": "11.30.1", + "Arch": "x86_64", + "SrcName": "openssl-1_1", + "SrcVersion": "1.1.1d", + "SrcRelease": "11.30.1", + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit@0.23.2-4.8.3.x86_64", + "Name": "p11-kit", + "Version": "0.23.2", + "Release": "4.8.3", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.2", + "SrcRelease": "4.8.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit-tools@0.23.2-4.8.3.x86_64", + "Name": "p11-kit-tools", + "Version": "0.23.2", + "Release": "4.8.3", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.2", + "SrcRelease": "4.8.3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pam@1.3.0-6.38.1.x86_64", + "Name": "pam", + "Version": "1.3.0", + "Release": "6.38.1", + "Arch": "x86_64", + "SrcName": "pam", + "SrcVersion": "1.3.0", + "SrcRelease": "6.38.1", + "Licenses": [ + "GPL-2.0+ or BSD-3-Clause" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "patterns-base-fips@20200124-10.5.1.x86_64", + "Name": "patterns-base-fips", + "Version": "20200124", + "Release": "10.5.1", + "Arch": "x86_64", + "SrcName": "patterns-base", + "SrcVersion": "20200124", + "SrcRelease": "10.5.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "perl-base@5.26.1-15.87.x86_64", + "Name": "perl-base", + "Version": "5.26.1", + "Release": "15.87", + "Arch": "x86_64", + "SrcName": "perl", + "SrcVersion": "5.26.1", + "SrcRelease": "15.87", + "Licenses": [ + "Artistic-1.0 or GPL-2.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "permissions@20181225-23.6.1.x86_64", + "Name": "permissions", + "Version": "20181225", + "Release": "23.6.1", + "Arch": "x86_64", + "SrcName": "permissions", + "SrcVersion": "20181225", + "SrcRelease": "23.6.1", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pinentry@1.1.0-4.3.1.x86_64", + "Name": "pinentry", + "Version": "1.1.0", + "Release": "4.3.1", + "Arch": "x86_64", + "SrcName": "pinentry", + "SrcVersion": "1.1.0", + "SrcRelease": "4.3.1", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "procps@3.3.15-7.19.1.x86_64", + "Name": "procps", + "Version": "3.3.15", + "Release": "7.19.1", + "Arch": "x86_64", + "SrcName": "procps", + "SrcVersion": "3.3.15", + "SrcRelease": "7.19.1", + "Licenses": [ + "GPL-2.0-or-later AND LGPL-2.1-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-config-SUSE@1-3.61.noarch", + "Name": "rpm-config-SUSE", + "Version": "1", + "Release": "3.61", + "Arch": "noarch", + "SrcName": "rpm-config-SUSE", + "SrcVersion": "1", + "SrcRelease": "3.61", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-ndb@4.14.3-40.1.x86_64", + "Name": "rpm-ndb", + "Version": "4.14.3", + "Release": "40.1", + "Arch": "x86_64", + "SrcName": "rpm-ndb", + "SrcVersion": "4.14.3", + "SrcRelease": "40.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sed@4.4-11.6.x86_64", + "Name": "sed", + "Version": "4.4", + "Release": "11.6", + "Arch": "x86_64", + "SrcName": "sed", + "SrcVersion": "4.4", + "SrcRelease": "11.6", + "Licenses": [ + "GPL-3.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "shadow@4.8.1-2.43.x86_64", + "Name": "shadow", + "Version": "4.8.1", + "Release": "2.43", + "Arch": "x86_64", + "SrcName": "shadow", + "SrcVersion": "4.8.1", + "SrcRelease": "2.43", + "Licenses": [ + "BSD-3-Clause AND GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sles-release@15.3-55.4.1.x86_64", + "Name": "sles-release", + "Version": "15.3", + "Release": "55.4.1", + "Arch": "x86_64", + "SrcName": "sles-release", + "SrcVersion": "15.3", + "SrcRelease": "55.4.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "suse-build-key@12.0-8.16.1.noarch", + "Name": "suse-build-key", + "Version": "12.0", + "Release": "8.16.1", + "Arch": "noarch", + "SrcName": "suse-build-key", + "SrcVersion": "12.0", + "SrcRelease": "8.16.1", + "Licenses": [ + "GPL-2.0+" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "system-group-hardware@20170617-15.86.noarch", + "Name": "system-group-hardware", + "Version": "20170617", + "Release": "15.86", + "Arch": "noarch", + "SrcName": "system-users", + "SrcVersion": "20170617", + "SrcRelease": "15.86", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "system-user-root@20190513-3.3.1.noarch", + "Name": "system-user-root", + "Version": "20190513", + "Release": "3.3.1", + "Arch": "noarch", + "SrcName": "system-user-root", + "SrcVersion": "20190513", + "SrcRelease": "3.3.1", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sysuser-shadow@2.0-4.2.8.noarch", + "Name": "sysuser-shadow", + "Version": "2.0", + "Release": "4.2.8", + "Arch": "noarch", + "SrcName": "sysuser-tools", + "SrcVersion": "2.0", + "SrcRelease": "4.2.8", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "terminfo-base@6.1-5.6.2.x86_64", + "Name": "terminfo-base", + "Version": "6.1", + "Release": "5.6.2", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.1", + "SrcRelease": "5.6.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "util-linux@2.36.2-4.5.1.x86_64", + "Name": "util-linux", + "Version": "2.36.2", + "Release": "4.5.1", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.36.2", + "SrcRelease": "4.5.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "zypper@1.14.46-13.1.x86_64", + "Name": "zypper", + "Version": "1.14.46", + "Release": "13.1", + "Arch": "x86_64", + "SrcName": "zypper", + "SrcVersion": "1.14.46", + "SrcRelease": "13.1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "SUSE LLC \u003chttps://www.suse.com/\u003e", + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/ubi-7.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/ubi-7.json.golden new file mode 100644 index 000000000000..e49ef13bb481 --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/ubi-7.json.golden @@ -0,0 +1,3592 @@ +[ + { + "ID": "acl@2.2.51-14.el7.x86_64", + "Name": "acl", + "Version": "2.2.51", + "Release": "14.el7", + "Arch": "x86_64", + "SrcName": "acl", + "SrcVersion": "2.2.51", + "SrcRelease": "14.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "audit-libs@2.8.5-4.el7.x86_64", + "Name": "audit-libs", + "Version": "2.8.5", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "audit", + "SrcVersion": "2.8.5", + "SrcRelease": "4.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "basesystem@10.0-7.el7.noarch", + "Name": "basesystem", + "Version": "10.0", + "Release": "7.el7", + "Arch": "noarch", + "SrcName": "basesystem", + "SrcVersion": "10.0", + "SrcRelease": "7.el7", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bash@4.2.46-33.el7.x86_64", + "Name": "bash", + "Version": "4.2.46", + "Release": "33.el7", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.2.46", + "SrcRelease": "33.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "binutils@2.27-41.base.el7.x86_64", + "Name": "binutils", + "Version": "2.27", + "Release": "41.base.el7", + "Arch": "x86_64", + "SrcName": "binutils", + "SrcVersion": "2.27", + "SrcRelease": "41.base.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "bzip2-libs@1.0.6-13.el7.x86_64", + "Name": "bzip2-libs", + "Version": "1.0.6", + "Release": "13.el7", + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.6", + "SrcRelease": "13.el7", + "Licenses": [ + "BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ca-certificates@2018.2.22-70.0.el7_5.noarch", + "Name": "ca-certificates", + "Version": "2018.2.22", + "Release": "70.0.el7_5", + "Arch": "noarch", + "SrcName": "ca-certificates", + "SrcVersion": "2018.2.22", + "SrcRelease": "70.0.el7_5", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "chkconfig@1.7.4-1.el7.x86_64", + "Name": "chkconfig", + "Version": "1.7.4", + "Release": "1.el7", + "Arch": "x86_64", + "SrcName": "chkconfig", + "SrcVersion": "1.7.4", + "SrcRelease": "1.el7", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "coreutils@8.22-24.el7.x86_64", + "Name": "coreutils", + "Version": "8.22", + "Release": "24.el7", + "Arch": "x86_64", + "SrcName": "coreutils", + "SrcVersion": "8.22", + "SrcRelease": "24.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cpio@2.11-27.el7.x86_64", + "Name": "cpio", + "Version": "2.11", + "Release": "27.el7", + "Arch": "x86_64", + "SrcName": "cpio", + "SrcVersion": "2.11", + "SrcRelease": "27.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cracklib@2.9.0-11.el7.x86_64", + "Name": "cracklib", + "Version": "2.9.0", + "Release": "11.el7", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.0", + "SrcRelease": "11.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cracklib-dicts@2.9.0-11.el7.x86_64", + "Name": "cracklib-dicts", + "Version": "2.9.0", + "Release": "11.el7", + "Arch": "x86_64", + "SrcName": "cracklib", + "SrcVersion": "2.9.0", + "SrcRelease": "11.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cryptsetup-libs@2.0.3-5.el7.x86_64", + "Name": "cryptsetup-libs", + "Version": "2.0.3", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "cryptsetup", + "SrcVersion": "2.0.3", + "SrcRelease": "5.el7", + "Licenses": [ + "GPLv2+ and LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "curl@7.29.0-54.el7.x86_64", + "Name": "curl", + "Version": "7.29.0", + "Release": "54.el7", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.29.0", + "SrcRelease": "54.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "cyrus-sasl-lib@2.1.26-23.el7.x86_64", + "Name": "cyrus-sasl-lib", + "Version": "2.1.26", + "Release": "23.el7", + "Arch": "x86_64", + "SrcName": "cyrus-sasl", + "SrcVersion": "2.1.26", + "SrcRelease": "23.el7", + "Licenses": [ + "BSD with advertising" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "dbus@1.10.24-13.el7_6.x86_64", + "Name": "dbus", + "Version": "1.10.24", + "Release": "13.el7_6", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "dbus", + "SrcVersion": "1.10.24", + "SrcRelease": "13.el7_6", + "SrcEpoch": 1, + "Licenses": [ + "(GPLv2+ or AFL) and GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "dbus-glib@0.100-7.el7.x86_64", + "Name": "dbus-glib", + "Version": "0.100", + "Release": "7.el7", + "Arch": "x86_64", + "SrcName": "dbus-glib", + "SrcVersion": "0.100", + "SrcRelease": "7.el7", + "Licenses": [ + "AFL and GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "dbus-libs@1.10.24-13.el7_6.x86_64", + "Name": "dbus-libs", + "Version": "1.10.24", + "Release": "13.el7_6", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "dbus", + "SrcVersion": "1.10.24", + "SrcRelease": "13.el7_6", + "SrcEpoch": 1, + "Licenses": [ + "(GPLv2+ or AFL) and GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "dbus-python@1.1.1-9.el7.x86_64", + "Name": "dbus-python", + "Version": "1.1.1", + "Release": "9.el7", + "Arch": "x86_64", + "SrcName": "dbus-python", + "SrcVersion": "1.1.1", + "SrcRelease": "9.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "device-mapper@1.02.158-2.el7.x86_64", + "Name": "device-mapper", + "Version": "1.02.158", + "Release": "2.el7", + "Epoch": 7, + "Arch": "x86_64", + "SrcName": "lvm2", + "SrcVersion": "2.02.185", + "SrcRelease": "2.el7", + "SrcEpoch": 7, + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "device-mapper-libs@1.02.158-2.el7.x86_64", + "Name": "device-mapper-libs", + "Version": "1.02.158", + "Release": "2.el7", + "Epoch": 7, + "Arch": "x86_64", + "SrcName": "lvm2", + "SrcVersion": "2.02.185", + "SrcRelease": "2.el7", + "SrcEpoch": 7, + "Licenses": [ + "LGPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "diffutils@3.3-5.el7.x86_64", + "Name": "diffutils", + "Version": "3.3", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "diffutils", + "SrcVersion": "3.3", + "SrcRelease": "5.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "dmidecode@3.2-3.el7.x86_64", + "Name": "dmidecode", + "Version": "3.2", + "Release": "3.el7", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "dmidecode", + "SrcVersion": "3.2", + "SrcRelease": "3.el7", + "SrcEpoch": 1, + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "dracut@033-564.el7.x86_64", + "Name": "dracut", + "Version": "033", + "Release": "564.el7", + "Arch": "x86_64", + "SrcName": "dracut", + "SrcVersion": "033", + "SrcRelease": "564.el7", + "Licenses": [ + "GPLv2+ and LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "elfutils-default-yama-scope@0.176-2.el7.noarch", + "Name": "elfutils-default-yama-scope", + "Version": "0.176", + "Release": "2.el7", + "Arch": "noarch", + "SrcName": "elfutils", + "SrcVersion": "0.176", + "SrcRelease": "2.el7", + "Licenses": [ + "GPLv2+ or LGPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "elfutils-libelf@0.176-2.el7.x86_64", + "Name": "elfutils-libelf", + "Version": "0.176", + "Release": "2.el7", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.176", + "SrcRelease": "2.el7", + "Licenses": [ + "GPLv2+ or LGPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "elfutils-libs@0.176-2.el7.x86_64", + "Name": "elfutils-libs", + "Version": "0.176", + "Release": "2.el7", + "Arch": "x86_64", + "SrcName": "elfutils", + "SrcVersion": "0.176", + "SrcRelease": "2.el7", + "Licenses": [ + "GPLv2+ or LGPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "expat@2.1.0-10.el7_3.x86_64", + "Name": "expat", + "Version": "2.1.0", + "Release": "10.el7_3", + "Arch": "x86_64", + "SrcName": "expat", + "SrcVersion": "2.1.0", + "SrcRelease": "10.el7_3", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "file-libs@5.11-35.el7.x86_64", + "Name": "file-libs", + "Version": "5.11", + "Release": "35.el7", + "Arch": "x86_64", + "SrcName": "file", + "SrcVersion": "5.11", + "SrcRelease": "35.el7", + "Licenses": [ + "BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "filesystem@3.2-25.el7.x86_64", + "Name": "filesystem", + "Version": "3.2", + "Release": "25.el7", + "Arch": "x86_64", + "SrcName": "filesystem", + "SrcVersion": "3.2", + "SrcRelease": "25.el7", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "findutils@4.5.11-6.el7.x86_64", + "Name": "findutils", + "Version": "4.5.11", + "Release": "6.el7", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "findutils", + "SrcVersion": "4.5.11", + "SrcRelease": "6.el7", + "SrcEpoch": 1, + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gawk@4.0.2-4.el7_3.1.x86_64", + "Name": "gawk", + "Version": "4.0.2", + "Release": "4.el7_3.1", + "Arch": "x86_64", + "SrcName": "gawk", + "SrcVersion": "4.0.2", + "SrcRelease": "4.el7_3.1", + "Licenses": [ + "GPLv3+ and GPL and LGPLv3+ and LGPL and BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gdb-gdbserver@7.6.1-115.el7.x86_64", + "Name": "gdb-gdbserver", + "Version": "7.6.1", + "Release": "115.el7", + "Arch": "x86_64", + "SrcName": "gdb", + "SrcVersion": "7.6.1", + "SrcRelease": "115.el7", + "Licenses": [ + "GPLv3+ and GPLv3+ with exceptions and GPLv2+ and GPLv2+ with exceptions and GPL+ and LGPLv2+ and BSD and Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gdbm@1.10-8.el7.x86_64", + "Name": "gdbm", + "Version": "1.10", + "Release": "8.el7", + "Arch": "x86_64", + "SrcName": "gdbm", + "SrcVersion": "1.10", + "SrcRelease": "8.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glib2@2.56.1-5.el7.x86_64", + "Name": "glib2", + "Version": "2.56.1", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "glib2", + "SrcVersion": "2.56.1", + "SrcRelease": "5.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc@2.17-292.el7.x86_64", + "Name": "glibc", + "Version": "2.17", + "Release": "292.el7", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.17", + "SrcRelease": "292.el7", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "glibc-common@2.17-292.el7.x86_64", + "Name": "glibc-common", + "Version": "2.17", + "Release": "292.el7", + "Arch": "x86_64", + "SrcName": "glibc", + "SrcVersion": "2.17", + "SrcRelease": "292.el7", + "Licenses": [ + "LGPLv2+ and LGPLv2+ with exceptions and GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gmp@6.0.0-15.el7.x86_64", + "Name": "gmp", + "Version": "6.0.0", + "Release": "15.el7", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "gmp", + "SrcVersion": "6.0.0", + "SrcRelease": "15.el7", + "SrcEpoch": 1, + "Licenses": [ + "LGPLv3+ or GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gnupg2@2.0.22-5.el7_5.x86_64", + "Name": "gnupg2", + "Version": "2.0.22", + "Release": "5.el7_5", + "Arch": "x86_64", + "SrcName": "gnupg2", + "SrcVersion": "2.0.22", + "SrcRelease": "5.el7_5", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gobject-introspection@1.56.1-1.el7.x86_64", + "Name": "gobject-introspection", + "Version": "1.56.1", + "Release": "1.el7", + "Arch": "x86_64", + "SrcName": "gobject-introspection", + "SrcVersion": "1.56.1", + "SrcRelease": "1.el7", + "Licenses": [ + "GPLv2+, LGPLv2+, MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gpgme@1.3.2-5.el7.x86_64", + "Name": "gpgme", + "Version": "1.3.2", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "gpgme", + "SrcVersion": "1.3.2", + "SrcRelease": "5.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "grep@2.20-3.el7.x86_64", + "Name": "grep", + "Version": "2.20", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "grep", + "SrcVersion": "2.20", + "SrcRelease": "3.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "gzip@1.5-10.el7.x86_64", + "Name": "gzip", + "Version": "1.5", + "Release": "10.el7", + "Arch": "x86_64", + "SrcName": "gzip", + "SrcVersion": "1.5", + "SrcRelease": "10.el7", + "Licenses": [ + "GPLv3+ and GFDL" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "hardlink@1.0-19.el7.x86_64", + "Name": "hardlink", + "Version": "1.0", + "Release": "19.el7", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "hardlink", + "SrcVersion": "1.0", + "SrcRelease": "19.el7", + "SrcEpoch": 1, + "Licenses": [ + "GPL+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "info@5.1-5.el7.x86_64", + "Name": "info", + "Version": "5.1", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "texinfo", + "SrcVersion": "5.1", + "SrcRelease": "5.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "json-c@0.11-4.el7_0.x86_64", + "Name": "json-c", + "Version": "0.11", + "Release": "4.el7_0", + "Arch": "x86_64", + "SrcName": "json-c", + "SrcVersion": "0.11", + "SrcRelease": "4.el7_0", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "keyutils-libs@1.5.8-3.el7.x86_64", + "Name": "keyutils-libs", + "Version": "1.5.8", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "keyutils", + "SrcVersion": "1.5.8", + "SrcRelease": "3.el7", + "Licenses": [ + "GPLv2+ and LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "kmod@20-25.el7.x86_64", + "Name": "kmod", + "Version": "20", + "Release": "25.el7", + "Arch": "x86_64", + "SrcName": "kmod", + "SrcVersion": "20", + "SrcRelease": "25.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "kmod-libs@20-25.el7.x86_64", + "Name": "kmod-libs", + "Version": "20", + "Release": "25.el7", + "Arch": "x86_64", + "SrcName": "kmod", + "SrcVersion": "20", + "SrcRelease": "25.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "kpartx@0.4.9-127.el7.x86_64", + "Name": "kpartx", + "Version": "0.4.9", + "Release": "127.el7", + "Arch": "x86_64", + "SrcName": "device-mapper-multipath", + "SrcVersion": "0.4.9", + "SrcRelease": "127.el7", + "Licenses": [ + "GPL+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "krb5-libs@1.15.1-37.el7_7.2.x86_64", + "Name": "krb5-libs", + "Version": "1.15.1", + "Release": "37.el7_7.2", + "Arch": "x86_64", + "SrcName": "krb5", + "SrcVersion": "1.15.1", + "SrcRelease": "37.el7_7.2", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libacl@2.2.51-14.el7.x86_64", + "Name": "libacl", + "Version": "2.2.51", + "Release": "14.el7", + "Arch": "x86_64", + "SrcName": "acl", + "SrcVersion": "2.2.51", + "SrcRelease": "14.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libassuan@2.1.0-3.el7.x86_64", + "Name": "libassuan", + "Version": "2.1.0", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "libassuan", + "SrcVersion": "2.1.0", + "SrcRelease": "3.el7", + "Licenses": [ + "LGPLv2+ and GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libattr@2.4.46-13.el7.x86_64", + "Name": "libattr", + "Version": "2.4.46", + "Release": "13.el7", + "Arch": "x86_64", + "SrcName": "attr", + "SrcVersion": "2.4.46", + "SrcRelease": "13.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libblkid@2.23.2-61.el7.x86_64", + "Name": "libblkid", + "Version": "2.23.2", + "Release": "61.el7", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.23.2", + "SrcRelease": "61.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap@2.22-10.el7.x86_64", + "Name": "libcap", + "Version": "2.22", + "Release": "10.el7", + "Arch": "x86_64", + "SrcName": "libcap", + "SrcVersion": "2.22", + "SrcRelease": "10.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcap-ng@0.7.5-4.el7.x86_64", + "Name": "libcap-ng", + "Version": "0.7.5", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "libcap-ng", + "SrcVersion": "0.7.5", + "SrcRelease": "4.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcom_err@1.42.9-16.el7.x86_64", + "Name": "libcom_err", + "Version": "1.42.9", + "Release": "16.el7", + "Arch": "x86_64", + "SrcName": "e2fsprogs", + "SrcVersion": "1.42.9", + "SrcRelease": "16.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libcurl@7.29.0-54.el7.x86_64", + "Name": "libcurl", + "Version": "7.29.0", + "Release": "54.el7", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.29.0", + "SrcRelease": "54.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libdb@5.3.21-25.el7.x86_64", + "Name": "libdb", + "Version": "5.3.21", + "Release": "25.el7", + "Arch": "x86_64", + "SrcName": "libdb", + "SrcVersion": "5.3.21", + "SrcRelease": "25.el7", + "Licenses": [ + "BSD and LGPLv2 and Sleepycat" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libdb-utils@5.3.21-25.el7.x86_64", + "Name": "libdb-utils", + "Version": "5.3.21", + "Release": "25.el7", + "Arch": "x86_64", + "SrcName": "libdb", + "SrcVersion": "5.3.21", + "SrcRelease": "25.el7", + "Licenses": [ + "BSD and LGPLv2 and Sleepycat" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libffi@3.0.13-18.el7.x86_64", + "Name": "libffi", + "Version": "3.0.13", + "Release": "18.el7", + "Arch": "x86_64", + "SrcName": "libffi", + "SrcVersion": "3.0.13", + "SrcRelease": "18.el7", + "Licenses": [ + "MIT and Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcc@4.8.5-39.el7.x86_64", + "Name": "libgcc", + "Version": "4.8.5", + "Release": "39.el7", + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "4.8.5", + "SrcRelease": "39.el7", + "Licenses": [ + "GPLv3+ and GPLv3+ with exceptions and GPLv2+ with exceptions and LGPLv2+ and BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgcrypt@1.5.3-14.el7.x86_64", + "Name": "libgcrypt", + "Version": "1.5.3", + "Release": "14.el7", + "Arch": "x86_64", + "SrcName": "libgcrypt", + "SrcVersion": "1.5.3", + "SrcRelease": "14.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libgpg-error@1.12-3.el7.x86_64", + "Name": "libgpg-error", + "Version": "1.12", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "libgpg-error", + "SrcVersion": "1.12", + "SrcRelease": "3.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libidn@1.28-4.el7.x86_64", + "Name": "libidn", + "Version": "1.28", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "libidn", + "SrcVersion": "1.28", + "SrcRelease": "4.el7", + "Licenses": [ + "LGPLv2+ and GPLv3+ and GFDL" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libmount@2.23.2-61.el7.x86_64", + "Name": "libmount", + "Version": "2.23.2", + "Release": "61.el7", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.23.2", + "SrcRelease": "61.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libnl@1.1.4-3.el7.x86_64", + "Name": "libnl", + "Version": "1.1.4", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "libnl", + "SrcVersion": "1.1.4", + "SrcRelease": "3.el7", + "Licenses": [ + "LGPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libpwquality@1.2.3-5.el7.x86_64", + "Name": "libpwquality", + "Version": "1.2.3", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "libpwquality", + "SrcVersion": "1.2.3", + "SrcRelease": "5.el7", + "Licenses": [ + "BSD or GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libselinux@2.5-14.1.el7.x86_64", + "Name": "libselinux", + "Version": "2.5", + "Release": "14.1.el7", + "Arch": "x86_64", + "SrcName": "libselinux", + "SrcVersion": "2.5", + "SrcRelease": "14.1.el7", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsemanage@2.5-14.el7.x86_64", + "Name": "libsemanage", + "Version": "2.5", + "Release": "14.el7", + "Arch": "x86_64", + "SrcName": "libsemanage", + "SrcVersion": "2.5", + "SrcRelease": "14.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsepol@2.5-10.el7.x86_64", + "Name": "libsepol", + "Version": "2.5", + "Release": "10.el7", + "Arch": "x86_64", + "SrcName": "libsepol", + "SrcVersion": "2.5", + "SrcRelease": "10.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libsmartcols@2.23.2-61.el7.x86_64", + "Name": "libsmartcols", + "Version": "2.23.2", + "Release": "61.el7", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.23.2", + "SrcRelease": "61.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libssh2@1.8.0-3.el7.x86_64", + "Name": "libssh2", + "Version": "1.8.0", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "libssh2", + "SrcVersion": "1.8.0", + "SrcRelease": "3.el7", + "Licenses": [ + "BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libstdc++@4.8.5-39.el7.x86_64", + "Name": "libstdc++", + "Version": "4.8.5", + "Release": "39.el7", + "Arch": "x86_64", + "SrcName": "gcc", + "SrcVersion": "4.8.5", + "SrcRelease": "39.el7", + "Licenses": [ + "GPLv3+ and GPLv3+ with exceptions and GPLv2+ with exceptions and LGPLv2+ and BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libtasn1@4.10-1.el7.x86_64", + "Name": "libtasn1", + "Version": "4.10", + "Release": "1.el7", + "Arch": "x86_64", + "SrcName": "libtasn1", + "SrcVersion": "4.10", + "SrcRelease": "1.el7", + "Licenses": [ + "GPLv3+ and LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libuser@0.60-9.el7.x86_64", + "Name": "libuser", + "Version": "0.60", + "Release": "9.el7", + "Arch": "x86_64", + "SrcName": "libuser", + "SrcVersion": "0.60", + "SrcRelease": "9.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libutempter@1.1.6-4.el7.x86_64", + "Name": "libutempter", + "Version": "1.1.6", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "libutempter", + "SrcVersion": "1.1.6", + "SrcRelease": "4.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libuuid@2.23.2-61.el7.x86_64", + "Name": "libuuid", + "Version": "2.23.2", + "Release": "61.el7", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.23.2", + "SrcRelease": "61.el7", + "Licenses": [ + "BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libverto@0.2.5-4.el7.x86_64", + "Name": "libverto", + "Version": "0.2.5", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "libverto", + "SrcVersion": "0.2.5", + "SrcRelease": "4.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libxml2@2.9.1-6.el7_2.3.x86_64", + "Name": "libxml2", + "Version": "2.9.1", + "Release": "6.el7_2.3", + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.9.1", + "SrcRelease": "6.el7_2.3", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "libxml2-python@2.9.1-6.el7_2.3.x86_64", + "Name": "libxml2-python", + "Version": "2.9.1", + "Release": "6.el7_2.3", + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.9.1", + "SrcRelease": "6.el7_2.3", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "lua@5.1.4-15.el7.x86_64", + "Name": "lua", + "Version": "5.1.4", + "Release": "15.el7", + "Arch": "x86_64", + "SrcName": "lua", + "SrcVersion": "5.1.4", + "SrcRelease": "15.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "lz4@1.7.5-3.el7.x86_64", + "Name": "lz4", + "Version": "1.7.5", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "lz4", + "SrcVersion": "1.7.5", + "SrcRelease": "3.el7", + "Licenses": [ + "GPLv2+ and BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses@5.9-14.20130511.el7_4.x86_64", + "Name": "ncurses", + "Version": "5.9", + "Release": "14.20130511.el7_4", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "5.9", + "SrcRelease": "14.20130511.el7_4", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-base@5.9-14.20130511.el7_4.noarch", + "Name": "ncurses-base", + "Version": "5.9", + "Release": "14.20130511.el7_4", + "Arch": "noarch", + "SrcName": "ncurses", + "SrcVersion": "5.9", + "SrcRelease": "14.20130511.el7_4", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ncurses-libs@5.9-14.20130511.el7_4.x86_64", + "Name": "ncurses-libs", + "Version": "5.9", + "Release": "14.20130511.el7_4", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "5.9", + "SrcRelease": "14.20130511.el7_4", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nspr@4.21.0-1.el7.x86_64", + "Name": "nspr", + "Version": "4.21.0", + "Release": "1.el7", + "Arch": "x86_64", + "SrcName": "nspr", + "SrcVersion": "4.21.0", + "SrcRelease": "1.el7", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss@3.44.0-4.el7.x86_64", + "Name": "nss", + "Version": "3.44.0", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.44.0", + "SrcRelease": "4.el7", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-pem@1.0.3-7.el7.x86_64", + "Name": "nss-pem", + "Version": "1.0.3", + "Release": "7.el7", + "Arch": "x86_64", + "SrcName": "nss-pem", + "SrcVersion": "1.0.3", + "SrcRelease": "7.el7", + "Licenses": [ + "MPLv1.1" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-softokn@3.44.0-5.el7.x86_64", + "Name": "nss-softokn", + "Version": "3.44.0", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "nss-softokn", + "SrcVersion": "3.44.0", + "SrcRelease": "5.el7", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-softokn-freebl@3.44.0-5.el7.x86_64", + "Name": "nss-softokn-freebl", + "Version": "3.44.0", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "nss-softokn", + "SrcVersion": "3.44.0", + "SrcRelease": "5.el7", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-sysinit@3.44.0-4.el7.x86_64", + "Name": "nss-sysinit", + "Version": "3.44.0", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.44.0", + "SrcRelease": "4.el7", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-tools@3.44.0-4.el7.x86_64", + "Name": "nss-tools", + "Version": "3.44.0", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "nss", + "SrcVersion": "3.44.0", + "SrcRelease": "4.el7", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "nss-util@3.44.0-3.el7.x86_64", + "Name": "nss-util", + "Version": "3.44.0", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "nss-util", + "SrcVersion": "3.44.0", + "SrcRelease": "3.el7", + "Licenses": [ + "MPLv2.0" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openldap@2.4.44-21.el7_6.x86_64", + "Name": "openldap", + "Version": "2.4.44", + "Release": "21.el7_6", + "Arch": "x86_64", + "SrcName": "openldap", + "SrcVersion": "2.4.44", + "SrcRelease": "21.el7_6", + "Licenses": [ + "OpenLDAP" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "openssl-libs@1.0.2k-19.el7.x86_64", + "Name": "openssl-libs", + "Version": "1.0.2k", + "Release": "19.el7", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "openssl", + "SrcVersion": "1.0.2k", + "SrcRelease": "19.el7", + "SrcEpoch": 1, + "Licenses": [ + "OpenSSL" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit@0.23.5-3.el7.x86_64", + "Name": "p11-kit", + "Version": "0.23.5", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.5", + "SrcRelease": "3.el7", + "Licenses": [ + "BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "p11-kit-trust@0.23.5-3.el7.x86_64", + "Name": "p11-kit-trust", + "Version": "0.23.5", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "p11-kit", + "SrcVersion": "0.23.5", + "SrcRelease": "3.el7", + "Licenses": [ + "BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pam@1.1.8-22.el7.x86_64", + "Name": "pam", + "Version": "1.1.8", + "Release": "22.el7", + "Arch": "x86_64", + "SrcName": "pam", + "SrcVersion": "1.1.8", + "SrcRelease": "22.el7", + "Licenses": [ + "BSD and GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "passwd@0.79-5.el7.x86_64", + "Name": "passwd", + "Version": "0.79", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "passwd", + "SrcVersion": "0.79", + "SrcRelease": "5.el7", + "Licenses": [ + "BSD or GPL+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pcre@8.32-17.el7.x86_64", + "Name": "pcre", + "Version": "8.32", + "Release": "17.el7", + "Arch": "x86_64", + "SrcName": "pcre", + "SrcVersion": "8.32", + "SrcRelease": "17.el7", + "Licenses": [ + "BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pinentry@0.8.1-17.el7.x86_64", + "Name": "pinentry", + "Version": "0.8.1", + "Release": "17.el7", + "Arch": "x86_64", + "SrcName": "pinentry", + "SrcVersion": "0.8.1", + "SrcRelease": "17.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pkgconfig@0.27.1-4.el7.x86_64", + "Name": "pkgconfig", + "Version": "0.27.1", + "Release": "4.el7", + "Epoch": 1, + "Arch": "x86_64", + "SrcName": "pkgconfig", + "SrcVersion": "0.27.1", + "SrcRelease": "4.el7", + "SrcEpoch": 1, + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "popt@1.13-16.el7.x86_64", + "Name": "popt", + "Version": "1.13", + "Release": "16.el7", + "Arch": "x86_64", + "SrcName": "popt", + "SrcVersion": "1.13", + "SrcRelease": "16.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "procps-ng@3.3.10-26.el7.x86_64", + "Name": "procps-ng", + "Version": "3.3.10", + "Release": "26.el7", + "Arch": "x86_64", + "SrcName": "procps-ng", + "SrcVersion": "3.3.10", + "SrcRelease": "26.el7", + "Licenses": [ + "GPL+ and GPLv2 and GPLv2+ and GPLv3+ and LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pth@2.0.7-23.el7.x86_64", + "Name": "pth", + "Version": "2.0.7", + "Release": "23.el7", + "Arch": "x86_64", + "SrcName": "pth", + "SrcVersion": "2.0.7", + "SrcRelease": "23.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pygpgme@0.3-9.el7.x86_64", + "Name": "pygpgme", + "Version": "0.3", + "Release": "9.el7", + "Arch": "x86_64", + "SrcName": "pygpgme", + "SrcVersion": "0.3", + "SrcRelease": "9.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pyliblzma@0.5.3-11.el7.x86_64", + "Name": "pyliblzma", + "Version": "0.5.3", + "Release": "11.el7", + "Arch": "x86_64", + "SrcName": "pyliblzma", + "SrcVersion": "0.5.3", + "SrcRelease": "11.el7", + "Licenses": [ + "LGPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python@2.7.5-86.el7.x86_64", + "Name": "python", + "Version": "2.7.5", + "Release": "86.el7", + "Arch": "x86_64", + "SrcName": "python", + "SrcVersion": "2.7.5", + "SrcRelease": "86.el7", + "Licenses": [ + "Python" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-backports@1.0-8.el7.x86_64", + "Name": "python-backports", + "Version": "1.0", + "Release": "8.el7", + "Arch": "x86_64", + "SrcName": "python-backports", + "SrcVersion": "1.0", + "SrcRelease": "8.el7", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-backports-ssl_match_hostname@3.5.0.1-1.el7.noarch", + "Name": "python-backports-ssl_match_hostname", + "Version": "3.5.0.1", + "Release": "1.el7", + "Arch": "noarch", + "SrcName": "python-backports-ssl_match_hostname", + "SrcVersion": "3.5.0.1", + "SrcRelease": "1.el7", + "Licenses": [ + "Python" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-chardet@2.2.1-3.el7.noarch", + "Name": "python-chardet", + "Version": "2.2.1", + "Release": "3.el7", + "Arch": "noarch", + "SrcName": "python-chardet", + "SrcVersion": "2.2.1", + "SrcRelease": "3.el7", + "Licenses": [ + "LGPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-dateutil@1.5-7.el7.noarch", + "Name": "python-dateutil", + "Version": "1.5", + "Release": "7.el7", + "Arch": "noarch", + "SrcName": "python-dateutil", + "SrcVersion": "1.5", + "SrcRelease": "7.el7", + "Licenses": [ + "Python" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-decorator@3.4.0-3.el7.noarch", + "Name": "python-decorator", + "Version": "3.4.0", + "Release": "3.el7", + "Arch": "noarch", + "SrcName": "python-decorator", + "SrcVersion": "3.4.0", + "SrcRelease": "3.el7", + "Licenses": [ + "BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-dmidecode@3.12.2-3.el7.x86_64", + "Name": "python-dmidecode", + "Version": "3.12.2", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "python-dmidecode", + "SrcVersion": "3.12.2", + "SrcRelease": "3.el7", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-ethtool@0.8-8.el7.x86_64", + "Name": "python-ethtool", + "Version": "0.8", + "Release": "8.el7", + "Arch": "x86_64", + "SrcName": "python-ethtool", + "SrcVersion": "0.8", + "SrcRelease": "8.el7", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-gobject-base@3.22.0-1.el7_4.1.x86_64", + "Name": "python-gobject-base", + "Version": "3.22.0", + "Release": "1.el7_4.1", + "Arch": "x86_64", + "SrcName": "pygobject3", + "SrcVersion": "3.22.0", + "SrcRelease": "1.el7_4.1", + "Licenses": [ + "LGPLv2+ and MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-iniparse@0.4-9.el7.noarch", + "Name": "python-iniparse", + "Version": "0.4", + "Release": "9.el7", + "Arch": "noarch", + "SrcName": "python-iniparse", + "SrcVersion": "0.4", + "SrcRelease": "9.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-inotify@0.9.4-4.el7.noarch", + "Name": "python-inotify", + "Version": "0.9.4", + "Release": "4.el7", + "Arch": "noarch", + "SrcName": "python-inotify", + "SrcVersion": "0.9.4", + "SrcRelease": "4.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-ipaddress@1.0.16-2.el7.noarch", + "Name": "python-ipaddress", + "Version": "1.0.16", + "Release": "2.el7", + "Arch": "noarch", + "SrcName": "python-ipaddress", + "SrcVersion": "1.0.16", + "SrcRelease": "2.el7", + "Licenses": [ + "Python" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-kitchen@1.1.1-5.el7.noarch", + "Name": "python-kitchen", + "Version": "1.1.1", + "Release": "5.el7", + "Arch": "noarch", + "SrcName": "python-kitchen", + "SrcVersion": "1.1.1", + "SrcRelease": "5.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-libs@2.7.5-86.el7.x86_64", + "Name": "python-libs", + "Version": "2.7.5", + "Release": "86.el7", + "Arch": "x86_64", + "SrcName": "python", + "SrcVersion": "2.7.5", + "SrcRelease": "86.el7", + "Licenses": [ + "Python" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-pycurl@7.19.0-19.el7.x86_64", + "Name": "python-pycurl", + "Version": "7.19.0", + "Release": "19.el7", + "Arch": "x86_64", + "SrcName": "python-pycurl", + "SrcVersion": "7.19.0", + "SrcRelease": "19.el7", + "Licenses": [ + "LGPLv2+ or MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-setuptools@0.9.8-7.el7.noarch", + "Name": "python-setuptools", + "Version": "0.9.8", + "Release": "7.el7", + "Arch": "noarch", + "SrcName": "python-setuptools", + "SrcVersion": "0.9.8", + "SrcRelease": "7.el7", + "Licenses": [ + "Python or ZPLv2.0" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-six@1.9.0-2.el7.noarch", + "Name": "python-six", + "Version": "1.9.0", + "Release": "2.el7", + "Arch": "noarch", + "SrcName": "python-six", + "SrcVersion": "1.9.0", + "SrcRelease": "2.el7", + "Licenses": [ + "MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-syspurpose@1.24.13-3.el7_7.x86_64", + "Name": "python-syspurpose", + "Version": "1.24.13", + "Release": "3.el7_7", + "Arch": "x86_64", + "SrcName": "subscription-manager", + "SrcVersion": "1.24.13", + "SrcRelease": "3.el7_7", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "python-urlgrabber@3.10-9.el7.noarch", + "Name": "python-urlgrabber", + "Version": "3.10", + "Release": "9.el7", + "Arch": "noarch", + "SrcName": "python-urlgrabber", + "SrcVersion": "3.10", + "SrcRelease": "9.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "pyxattr@0.5.1-5.el7.x86_64", + "Name": "pyxattr", + "Version": "0.5.1", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "pyxattr", + "SrcVersion": "0.5.1", + "SrcRelease": "5.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "qrencode-libs@3.4.1-3.el7.x86_64", + "Name": "qrencode-libs", + "Version": "3.4.1", + "Release": "3.el7", + "Arch": "x86_64", + "SrcName": "qrencode", + "SrcVersion": "3.4.1", + "SrcRelease": "3.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "readline@6.2-11.el7.x86_64", + "Name": "readline", + "Version": "6.2", + "Release": "11.el7", + "Arch": "x86_64", + "SrcName": "readline", + "SrcVersion": "6.2", + "SrcRelease": "11.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "redhat-release-server@7.7-10.el7.x86_64", + "Name": "redhat-release-server", + "Version": "7.7", + "Release": "10.el7", + "Arch": "x86_64", + "SrcName": "redhat-release-server", + "SrcVersion": "7.7", + "SrcRelease": "10.el7", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rootfiles@8.1-11.el7.noarch", + "Name": "rootfiles", + "Version": "8.1", + "Release": "11.el7", + "Arch": "noarch", + "SrcName": "rootfiles", + "SrcVersion": "8.1", + "SrcRelease": "11.el7", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm@4.11.3-40.el7.x86_64", + "Name": "rpm", + "Version": "4.11.3", + "Release": "40.el7", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.11.3", + "SrcRelease": "40.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-build-libs@4.11.3-40.el7.x86_64", + "Name": "rpm-build-libs", + "Version": "4.11.3", + "Release": "40.el7", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.11.3", + "SrcRelease": "40.el7", + "Licenses": [ + "GPLv2+ and LGPLv2+ with exceptions" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-libs@4.11.3-40.el7.x86_64", + "Name": "rpm-libs", + "Version": "4.11.3", + "Release": "40.el7", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.11.3", + "SrcRelease": "40.el7", + "Licenses": [ + "GPLv2+ and LGPLv2+ with exceptions" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "rpm-python@4.11.3-40.el7.x86_64", + "Name": "rpm-python", + "Version": "4.11.3", + "Release": "40.el7", + "Arch": "x86_64", + "SrcName": "rpm", + "SrcVersion": "4.11.3", + "SrcRelease": "40.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sed@4.2.2-5.el7.x86_64", + "Name": "sed", + "Version": "4.2.2", + "Release": "5.el7", + "Arch": "x86_64", + "SrcName": "sed", + "SrcVersion": "4.2.2", + "SrcRelease": "5.el7", + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "setup@2.8.71-10.el7.noarch", + "Name": "setup", + "Version": "2.8.71", + "Release": "10.el7", + "Arch": "noarch", + "SrcName": "setup", + "SrcVersion": "2.8.71", + "SrcRelease": "10.el7", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "shadow-utils@4.6-5.el7.x86_64", + "Name": "shadow-utils", + "Version": "4.6", + "Release": "5.el7", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "shadow-utils", + "SrcVersion": "4.6", + "SrcRelease": "5.el7", + "SrcEpoch": 2, + "Licenses": [ + "BSD and GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "shared-mime-info@1.8-4.el7.x86_64", + "Name": "shared-mime-info", + "Version": "1.8", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "shared-mime-info", + "SrcVersion": "1.8", + "SrcRelease": "4.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "sqlite@3.7.17-8.el7.x86_64", + "Name": "sqlite", + "Version": "3.7.17", + "Release": "8.el7", + "Arch": "x86_64", + "SrcName": "sqlite", + "SrcVersion": "3.7.17", + "SrcRelease": "8.el7", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "subscription-manager@1.24.13-3.el7_7.x86_64", + "Name": "subscription-manager", + "Version": "1.24.13", + "Release": "3.el7_7", + "Arch": "x86_64", + "SrcName": "subscription-manager", + "SrcVersion": "1.24.13", + "SrcRelease": "3.el7_7", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "subscription-manager-rhsm@1.24.13-3.el7_7.x86_64", + "Name": "subscription-manager-rhsm", + "Version": "1.24.13", + "Release": "3.el7_7", + "Arch": "x86_64", + "SrcName": "subscription-manager", + "SrcVersion": "1.24.13", + "SrcRelease": "3.el7_7", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "subscription-manager-rhsm-certificates@1.24.13-3.el7_7.x86_64", + "Name": "subscription-manager-rhsm-certificates", + "Version": "1.24.13", + "Release": "3.el7_7", + "Arch": "x86_64", + "SrcName": "subscription-manager", + "SrcVersion": "1.24.13", + "SrcRelease": "3.el7_7", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "systemd@219-67.el7_7.1.x86_64", + "Name": "systemd", + "Version": "219", + "Release": "67.el7_7.1", + "Arch": "x86_64", + "SrcName": "systemd", + "SrcVersion": "219", + "SrcRelease": "67.el7_7.1", + "Licenses": [ + "LGPLv2+ and MIT and GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "systemd-libs@219-67.el7_7.1.x86_64", + "Name": "systemd-libs", + "Version": "219", + "Release": "67.el7_7.1", + "Arch": "x86_64", + "SrcName": "systemd", + "SrcVersion": "219", + "SrcRelease": "67.el7_7.1", + "Licenses": [ + "LGPLv2+ and MIT" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "tar@1.26-35.el7.x86_64", + "Name": "tar", + "Version": "1.26", + "Release": "35.el7", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "tar", + "SrcVersion": "1.26", + "SrcRelease": "35.el7", + "SrcEpoch": 2, + "Licenses": [ + "GPLv3+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "tzdata@2019b-1.el7.noarch", + "Name": "tzdata", + "Version": "2019b", + "Release": "1.el7", + "Arch": "noarch", + "SrcName": "tzdata", + "SrcVersion": "2019b", + "SrcRelease": "1.el7", + "Licenses": [ + "Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "usermode@1.111-6.el7.x86_64", + "Name": "usermode", + "Version": "1.111", + "Release": "6.el7", + "Arch": "x86_64", + "SrcName": "usermode", + "SrcVersion": "1.111", + "SrcRelease": "6.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "ustr@1.0.4-16.el7.x86_64", + "Name": "ustr", + "Version": "1.0.4", + "Release": "16.el7", + "Arch": "x86_64", + "SrcName": "ustr", + "SrcVersion": "1.0.4", + "SrcRelease": "16.el7", + "Licenses": [ + "MIT or LGPLv2+ or BSD" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "util-linux@2.23.2-61.el7.x86_64", + "Name": "util-linux", + "Version": "2.23.2", + "Release": "61.el7", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.23.2", + "SrcRelease": "61.el7", + "Licenses": [ + "GPLv2 and GPLv2+ and LGPLv2+ and BSD with advertising and Public Domain" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "vim-minimal@7.4.629-6.el7.x86_64", + "Name": "vim-minimal", + "Version": "7.4.629", + "Release": "6.el7", + "Epoch": 2, + "Arch": "x86_64", + "SrcName": "vim", + "SrcVersion": "7.4.629", + "SrcRelease": "6.el7", + "SrcEpoch": 2, + "Licenses": [ + "Vim" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "virt-what@1.18-4.el7.x86_64", + "Name": "virt-what", + "Version": "1.18", + "Release": "4.el7", + "Arch": "x86_64", + "SrcName": "virt-what", + "SrcVersion": "1.18", + "SrcRelease": "4.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "which@2.20-7.el7.x86_64", + "Name": "which", + "Version": "2.20", + "Release": "7.el7", + "Arch": "x86_64", + "SrcName": "which", + "SrcVersion": "2.20", + "SrcRelease": "7.el7", + "Licenses": [ + "GPLv3" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "xz@5.2.2-1.el7.x86_64", + "Name": "xz", + "Version": "5.2.2", + "Release": "1.el7", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.2", + "SrcRelease": "1.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "xz-libs@5.2.2-1.el7.x86_64", + "Name": "xz-libs", + "Version": "5.2.2", + "Release": "1.el7", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.2", + "SrcRelease": "1.el7", + "Licenses": [ + "LGPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "yum@3.4.3-163.el7.noarch", + "Name": "yum", + "Version": "3.4.3", + "Release": "163.el7", + "Arch": "noarch", + "SrcName": "yum", + "SrcVersion": "3.4.3", + "SrcRelease": "163.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "yum-metadata-parser@1.1.4-10.el7.x86_64", + "Name": "yum-metadata-parser", + "Version": "1.1.4", + "Release": "10.el7", + "Arch": "x86_64", + "SrcName": "yum-metadata-parser", + "SrcVersion": "1.1.4", + "SrcRelease": "10.el7", + "Licenses": [ + "GPLv2" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "yum-plugin-ovl@1.1.31-52.el7.noarch", + "Name": "yum-plugin-ovl", + "Version": "1.1.31", + "Release": "52.el7", + "Arch": "noarch", + "SrcName": "yum-utils", + "SrcVersion": "1.1.31", + "SrcRelease": "52.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "yum-utils@1.1.31-52.el7.noarch", + "Name": "yum-utils", + "Version": "1.1.31", + "Release": "52.el7", + "Arch": "noarch", + "SrcName": "yum-utils", + "SrcVersion": "1.1.31", + "SrcRelease": "52.el7", + "Licenses": [ + "GPLv2+" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + }, + { + "ID": "zlib@1.2.7-18.el7.x86_64", + "Name": "zlib", + "Version": "1.2.7", + "Release": "18.el7", + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.7", + "SrcRelease": "18.el7", + "Licenses": [ + "zlib and Boost" + ], + "Maintainer": "Red Hat, Inc.", + "BuildInfo": { + "Nvr": "ubi7-container-7.7-140", + "Arch": "x86_64" + }, + "Repository": { + "Class": "official" + }, + "AnalyzedBy": "rpm" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/ubuntu-18.04.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/ubuntu-18.04.json.golden new file mode 100644 index 000000000000..ecf13d0f996b --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/ubuntu-18.04.json.golden @@ -0,0 +1,1070 @@ +[ + { + "Name": "dash", + "Version": "0.5.8-2.10", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "dash", + "SrcVersion": "0.5.8-2.10", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "passwd", + "Version": "1:4.5-1ubuntu2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "shadow", + "SrcVersion": "1:4.5-1ubuntu2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "liblz4-1", + "Version": "0.0~r131-2ubuntu3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "lz4", + "SrcVersion": "0.0~r131-2ubuntu3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libgcrypt20", + "Version": "1.8.1-4ubuntu1.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libgcrypt20", + "SrcVersion": "1.8.1-4ubuntu1.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libfdisk1", + "Version": "2.31.1-0.4ubuntu3.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "util-linux", + "SrcVersion": "2.31.1-0.4ubuntu3.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "coreutils", + "Version": "8.28-1ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "coreutils", + "SrcVersion": "8.28-1ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "tar", + "Version": "1.29b-2ubuntu0.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "tar", + "SrcVersion": "1.29b-2ubuntu0.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "ncurses-base", + "Version": "6.1-1ubuntu1.18.04", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "ncurses", + "SrcVersion": "6.1-1ubuntu1.18.04", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libapt-pkg5.0", + "Version": "1.6.11", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "apt", + "SrcVersion": "1.6.11", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "bash", + "Version": "4.4.18-2ubuntu1.2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "bash", + "SrcVersion": "4.4.18-2ubuntu1.2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "zlib1g", + "Version": "1:1.2.11.dfsg-0ubuntu2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "zlib", + "SrcVersion": "1:1.2.11.dfsg-0ubuntu2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "gzip", + "Version": "1.6-5ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "gzip", + "SrcVersion": "1.6-5ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "bzip2", + "Version": "1.0.6-8.1ubuntu0.2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "bzip2", + "SrcVersion": "1.0.6-8.1ubuntu0.2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libhogweed4", + "Version": "3.4-1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "nettle", + "SrcVersion": "3.4-1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "diffutils", + "Version": "1:3.6-1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "diffutils", + "SrcVersion": "1:3.6-1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libncursesw5", + "Version": "6.1-1ubuntu1.18.04", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "ncurses", + "SrcVersion": "6.1-1ubuntu1.18.04", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "hostname", + "Version": "3.20", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "hostname", + "SrcVersion": "3.20", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libselinux1", + "Version": "2.7-2build2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libselinux", + "SrcVersion": "2.7-2build2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libpam0g", + "Version": "1.1.8-3.6ubuntu2.18.04.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "pam", + "SrcVersion": "1.1.8-3.6ubuntu2.18.04.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libnettle6", + "Version": "3.4-1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "nettle", + "SrcVersion": "3.4-1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libgnutls30", + "Version": "3.5.18-1ubuntu1.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "gnutls28", + "SrcVersion": "3.5.18-1ubuntu1.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libc6", + "Version": "2.27-3ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "glibc", + "SrcVersion": "2.27-3ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libc-bin", + "Version": "2.27-3ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "glibc", + "SrcVersion": "2.27-3ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "dpkg", + "Version": "1.19.0.5ubuntu2.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "dpkg", + "SrcVersion": "1.19.0.5ubuntu2.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libgcc1", + "Version": "1:8.3.0-6ubuntu1~18.04.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "gcc-8", + "SrcVersion": "8.3.0-6ubuntu1~18.04.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "util-linux", + "Version": "2.31.1-0.4ubuntu3.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "util-linux", + "SrcVersion": "2.31.1-0.4ubuntu3.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "base-passwd", + "Version": "3.5.44", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "base-passwd", + "SrcVersion": "3.5.44", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "bsdutils", + "Version": "1:2.31.1-0.4ubuntu3.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "util-linux", + "SrcVersion": "2.31.1-0.4ubuntu3.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libgpg-error0", + "Version": "1.27-6", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libgpg-error", + "SrcVersion": "1.27-6", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "gcc-8-base", + "Version": "8.3.0-6ubuntu1~18.04.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "gcc-8", + "SrcVersion": "8.3.0-6ubuntu1~18.04.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "ncurses-bin", + "Version": "6.1-1ubuntu1.18.04", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "ncurses", + "SrcVersion": "6.1-1ubuntu1.18.04", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libudev1", + "Version": "237-3ubuntu10.25", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "systemd", + "SrcVersion": "237-3ubuntu10.25", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libuuid1", + "Version": "2.31.1-0.4ubuntu3.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "util-linux", + "SrcVersion": "2.31.1-0.4ubuntu3.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libsystemd0", + "Version": "237-3ubuntu10.25", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "systemd", + "SrcVersion": "237-3ubuntu10.25", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libbz2-1.0", + "Version": "1.0.6-8.1ubuntu0.2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "bzip2", + "SrcVersion": "1.0.6-8.1ubuntu0.2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "init-system-helpers", + "Version": "1.51", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "init-system-helpers", + "SrcVersion": "1.51", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "apt", + "Version": "1.6.11", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "apt", + "SrcVersion": "1.6.11", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "ubuntu-keyring", + "Version": "2018.09.18.1~18.04.0", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "ubuntu-keyring", + "SrcVersion": "2018.09.18.1~18.04.0", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "grep", + "Version": "3.1-2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "grep", + "SrcVersion": "3.1-2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "debianutils", + "Version": "4.8.4", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "debianutils", + "SrcVersion": "4.8.4", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libdb5.3", + "Version": "5.3.28-13.1ubuntu1.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "db5.3", + "SrcVersion": "5.3.28-13.1ubuntu1.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "gpgv", + "Version": "2.2.4-1ubuntu1.2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "gnupg2", + "SrcVersion": "2.2.4-1ubuntu1.2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "mount", + "Version": "2.31.1-0.4ubuntu3.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "util-linux", + "SrcVersion": "2.31.1-0.4ubuntu3.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libtasn1-6", + "Version": "4.13-2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libtasn1-6", + "SrcVersion": "4.13-2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "adduser", + "Version": "3.116ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "adduser", + "SrcVersion": "3.116ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libcom-err2", + "Version": "1.44.1-1ubuntu1.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.1-1ubuntu1.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libaudit1", + "Version": "1:2.8.2-1ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "audit", + "SrcVersion": "1:2.8.2-1ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libpcre3", + "Version": "2:8.39-9", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "pcre3", + "SrcVersion": "2:8.39-9", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "base-files", + "Version": "10.1ubuntu2.6", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "base-files", + "SrcVersion": "10.1ubuntu2.6", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libss2", + "Version": "1.44.1-1ubuntu1.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.1-1ubuntu1.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "debconf", + "Version": "1.5.66ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "debconf", + "SrcVersion": "1.5.66ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libzstd1", + "Version": "1.3.3+dfsg-2ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libzstd", + "SrcVersion": "1.3.3+dfsg-2ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "procps", + "Version": "2:3.3.12-3ubuntu1.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "procps", + "SrcVersion": "2:3.3.12-3ubuntu1.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libffi6", + "Version": "3.2.1-8", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libffi", + "SrcVersion": "3.2.1-8", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libacl1", + "Version": "2.2.52-3build1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "acl", + "SrcVersion": "2.2.52-3build1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libsemanage1", + "Version": "2.7-2build2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libsemanage", + "SrcVersion": "2.7-2build2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libdebconfclient0", + "Version": "0.213ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "cdebconf", + "SrcVersion": "0.213ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libstdc++6", + "Version": "8.3.0-6ubuntu1~18.04.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "gcc-8", + "SrcVersion": "8.3.0-6ubuntu1~18.04.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libmount1", + "Version": "2.31.1-0.4ubuntu3.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "util-linux", + "SrcVersion": "2.31.1-0.4ubuntu3.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "login", + "Version": "1:4.5-1ubuntu2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "shadow", + "SrcVersion": "1:4.5-1ubuntu2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libncurses5", + "Version": "6.1-1ubuntu1.18.04", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "ncurses", + "SrcVersion": "6.1-1ubuntu1.18.04", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libattr1", + "Version": "1:2.4.47-2build1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "attr", + "SrcVersion": "1:2.4.47-2build1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "e2fsprogs", + "Version": "1.44.1-1ubuntu1.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.1-1ubuntu1.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libsepol1", + "Version": "2.7-1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libsepol", + "SrcVersion": "2.7-1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libpam-runtime", + "Version": "1.1.8-3.6ubuntu2.18.04.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "pam", + "SrcVersion": "1.1.8-3.6ubuntu2.18.04.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libunistring2", + "Version": "0.9.9-0ubuntu2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libunistring", + "SrcVersion": "0.9.9-0ubuntu2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libprocps6", + "Version": "2:3.3.12-3ubuntu1.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "procps", + "SrcVersion": "2:3.3.12-3ubuntu1.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "sensible-utils", + "Version": "0.0.12", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "sensible-utils", + "SrcVersion": "0.0.12", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libsemanage-common", + "Version": "2.7-2build2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libsemanage", + "SrcVersion": "2.7-2build2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "sed", + "Version": "4.4-2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "sed", + "SrcVersion": "4.4-2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libsmartcols1", + "Version": "2.31.1-0.4ubuntu3.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "util-linux", + "SrcVersion": "2.31.1-0.4ubuntu3.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "perl-base", + "Version": "5.26.1-6ubuntu0.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "perl", + "SrcVersion": "5.26.1-6ubuntu0.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "liblzma5", + "Version": "5.2.2-1.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "xz-utils", + "SrcVersion": "5.2.2-1.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "lsb-base", + "Version": "9.20170808ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "lsb", + "SrcVersion": "9.20170808ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libpam-modules", + "Version": "1.1.8-3.6ubuntu2.18.04.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "pam", + "SrcVersion": "1.1.8-3.6ubuntu2.18.04.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libaudit-common", + "Version": "1:2.8.2-1ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "audit", + "SrcVersion": "1:2.8.2-1ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "findutils", + "Version": "4.6.0+git+20170828-2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "findutils", + "SrcVersion": "4.6.0+git+20170828-2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libcap-ng0", + "Version": "0.7.7-3.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libcap-ng", + "SrcVersion": "0.7.7-3.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "fdisk", + "Version": "2.31.1-0.4ubuntu3.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "util-linux", + "SrcVersion": "2.31.1-0.4ubuntu3.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libidn2-0", + "Version": "2.0.4-1.1build2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libidn2", + "SrcVersion": "2.0.4-1.1build2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libblkid1", + "Version": "2.31.1-0.4ubuntu3.3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "util-linux", + "SrcVersion": "2.31.1-0.4ubuntu3.3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libgmp10", + "Version": "2:6.1.2+dfsg-2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "gmp", + "SrcVersion": "2:6.1.2+dfsg-2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libseccomp2", + "Version": "2.4.1-0ubuntu0.18.04.2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "libseccomp", + "SrcVersion": "2.4.1-0ubuntu0.18.04.2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libtinfo5", + "Version": "6.1-1ubuntu1.18.04", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "ncurses", + "SrcVersion": "6.1-1ubuntu1.18.04", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libpam-modules-bin", + "Version": "1.1.8-3.6ubuntu2.18.04.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "pam", + "SrcVersion": "1.1.8-3.6ubuntu2.18.04.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "mawk", + "Version": "1.3.3-17ubuntu3", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "mawk", + "SrcVersion": "1.3.3-17ubuntu3", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "sysvinit-utils", + "Version": "2.88dsf-59.10ubuntu1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "sysvinit", + "SrcVersion": "2.88dsf-59.10ubuntu1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libp11-kit0", + "Version": "0.23.9-2", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "p11-kit", + "SrcVersion": "0.23.9-2", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + }, + { + "Name": "libext2fs2", + "Version": "1.44.1-1ubuntu1.1", + "Release": "", + "Epoch": 0, + "Arch": "", + "SrcName": "e2fsprogs", + "SrcVersion": "1.44.1-1ubuntu1.1", + "SrcRelease": "", + "SrcEpoch":0, + "LayerID":"sha256:6cebf3abed5fac58d2e792ce8461454e92c245d5312c42118f02e231a73b317f" + } +] diff --git a/pkg/fanal/test/integration/testdata/goldens/packages/vulnimage.json.golden b/pkg/fanal/test/integration/testdata/goldens/packages/vulnimage.json.golden new file mode 100644 index 000000000000..de06dbf3614c --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/packages/vulnimage.json.golden @@ -0,0 +1,748 @@ +[ + { + "ID": ".composer-phpext-rundeps@0", + "Name": ".composer-phpext-rundeps", + "Version": "0", + "Arch": "noarch", + "AnalyzedBy": "apk" + }, + { + "ID": ".persistent-deps@0", + "Name": ".persistent-deps", + "Version": "0", + "Arch": "noarch", + "AnalyzedBy": "apk" + }, + { + "ID": ".php-rundeps@0", + "Name": ".php-rundeps", + "Version": "0", + "Arch": "noarch", + "AnalyzedBy": "apk" + }, + { + "ID": "alpine-baselayout@3.0.5-r2", + "Name": "alpine-baselayout", + "Version": "3.0.5-r2", + "Arch": "x86_64", + "SrcName": "alpine-baselayout", + "SrcVersion": "3.0.5-r2", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "alpine-keys@2.1-r1", + "Name": "alpine-keys", + "Version": "2.1-r1", + "Arch": "x86_64", + "SrcName": "alpine-keys", + "SrcVersion": "2.1-r1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "apk-tools@2.10.1-r0", + "Name": "apk-tools", + "Version": "2.10.1-r0", + "Arch": "x86_64", + "SrcName": "apk-tools", + "SrcVersion": "2.10.1-r0", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "apr@1.6.3-r0", + "Name": "apr", + "Version": "1.6.3-r0", + "Arch": "x86_64", + "SrcName": "apr", + "SrcVersion": "1.6.3-r0", + "Licenses": [ + "Apache-2.0" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "apr-util@1.6.1-r1", + "Name": "apr-util", + "Version": "1.6.1-r1", + "Arch": "x86_64", + "SrcName": "apr-util", + "SrcVersion": "1.6.1-r1", + "Licenses": [ + "Apache-2.0" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "bash@4.4.19-r1", + "Name": "bash", + "Version": "4.4.19-r1", + "Arch": "x86_64", + "SrcName": "bash", + "SrcVersion": "4.4.19-r1", + "Licenses": [ + "GPL-3.0-or-later" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "busybox@1.27.2-r11", + "Name": "busybox", + "Version": "1.27.2-r11", + "Arch": "x86_64", + "SrcName": "busybox", + "SrcVersion": "1.27.2-r11", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "ca-certificates@20171114-r0", + "Name": "ca-certificates", + "Version": "20171114-r0", + "Arch": "x86_64", + "SrcName": "ca-certificates", + "SrcVersion": "20171114-r0", + "Licenses": [ + "MPL-2.0", + "GPL-2.0-or-later" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "curl@7.61.0-r0", + "Name": "curl", + "Version": "7.61.0-r0", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.61.0-r0", + "Licenses": [ + "MIT" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "db@5.3.28-r0", + "Name": "db", + "Version": "5.3.28-r0", + "Arch": "x86_64", + "SrcName": "db", + "SrcVersion": "5.3.28-r0", + "Licenses": [ + "custom" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "expat@2.2.5-r0", + "Name": "expat", + "Version": "2.2.5-r0", + "Arch": "x86_64", + "SrcName": "expat", + "SrcVersion": "2.2.5-r0", + "Licenses": [ + "MIT" + ], + "Maintainer": "Carlo Landmeter \u003cclandmeter@gmail.com\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "gdbm@1.13-r1", + "Name": "gdbm", + "Version": "1.13-r1", + "Arch": "x86_64", + "SrcName": "gdbm", + "SrcVersion": "1.13-r1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "git@2.15.2-r0", + "Name": "git", + "Version": "2.15.2-r0", + "Arch": "x86_64", + "SrcName": "git", + "SrcVersion": "2.15.2-r0", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libbz2@1.0.6-r6", + "Name": "libbz2", + "Version": "1.0.6-r6", + "Arch": "x86_64", + "SrcName": "bzip2", + "SrcVersion": "1.0.6-r6", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libc-utils@0.7.1-r0", + "Name": "libc-utils", + "Version": "0.7.1-r0", + "Arch": "x86_64", + "SrcName": "libc-dev", + "SrcVersion": "0.7.1-r0", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libcurl@7.61.1-r0", + "Name": "libcurl", + "Version": "7.61.1-r0", + "Arch": "x86_64", + "SrcName": "curl", + "SrcVersion": "7.61.1-r0", + "Licenses": [ + "MIT" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libedit@20170329.3.1-r3", + "Name": "libedit", + "Version": "20170329.3.1-r3", + "Arch": "x86_64", + "SrcName": "libedit", + "SrcVersion": "20170329.3.1-r3", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "William Pitcock \u003cnenolod@dereferenced.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libffi@3.2.1-r4", + "Name": "libffi", + "Version": "3.2.1-r4", + "Arch": "x86_64", + "SrcName": "libffi", + "SrcVersion": "3.2.1-r4", + "Licenses": [ + "MIT" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libressl@2.6.5-r0", + "Name": "libressl", + "Version": "2.6.5-r0", + "Arch": "x86_64", + "SrcName": "libressl", + "SrcVersion": "2.6.5-r0", + "Licenses": [ + "custom" + ], + "Maintainer": "Orion \u003csystmkor@gmail.com\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libressl2.6-libcrypto@2.6.5-r0", + "Name": "libressl2.6-libcrypto", + "Version": "2.6.5-r0", + "Arch": "x86_64", + "SrcName": "libressl", + "SrcVersion": "2.6.5-r0", + "Licenses": [ + "custom" + ], + "Maintainer": "Orion \u003csystmkor@gmail.com\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libressl2.6-libssl@2.6.5-r0", + "Name": "libressl2.6-libssl", + "Version": "2.6.5-r0", + "Arch": "x86_64", + "SrcName": "libressl", + "SrcVersion": "2.6.5-r0", + "Licenses": [ + "custom" + ], + "Maintainer": "Orion \u003csystmkor@gmail.com\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libressl2.6-libtls@2.6.5-r0", + "Name": "libressl2.6-libtls", + "Version": "2.6.5-r0", + "Arch": "x86_64", + "SrcName": "libressl", + "SrcVersion": "2.6.5-r0", + "Licenses": [ + "custom" + ], + "Maintainer": "Orion \u003csystmkor@gmail.com\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libsasl@2.1.26-r11", + "Name": "libsasl", + "Version": "2.1.26-r11", + "Arch": "x86_64", + "SrcName": "cyrus-sasl", + "SrcVersion": "2.1.26-r11", + "Licenses": [ + "custom" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libsodium@1.0.15-r0", + "Name": "libsodium", + "Version": "1.0.15-r0", + "Arch": "x86_64", + "SrcName": "libsodium", + "SrcVersion": "1.0.15-r0", + "Licenses": [ + "ISC" + ], + "Maintainer": "Stuart Cardall \u003cdeveloper@it-offshore.co.uk\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libssh2@1.8.0-r2", + "Name": "libssh2", + "Version": "1.8.0-r2", + "Arch": "x86_64", + "SrcName": "libssh2", + "SrcVersion": "1.8.0-r2", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libuuid@2.31-r0", + "Name": "libuuid", + "Version": "2.31-r0", + "Arch": "x86_64", + "SrcName": "util-linux", + "SrcVersion": "2.31-r0", + "Licenses": [ + "GPL-2.0-only", + "GPL-2.0-or-later", + "LGPL-2.0-or-later", + "BSD-3-Clause", + "Public", + "Domain" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "libxml2@2.9.7-r0", + "Name": "libxml2", + "Version": "2.9.7-r0", + "Arch": "x86_64", + "SrcName": "libxml2", + "SrcVersion": "2.9.7-r0", + "Licenses": [ + "MIT" + ], + "Maintainer": "Carlo Landmeter \u003cclandmeter@gmail.com\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "mercurial@4.5.2-r0", + "Name": "mercurial", + "Version": "4.5.2-r0", + "Arch": "x86_64", + "SrcName": "mercurial", + "SrcVersion": "4.5.2-r0", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "musl@1.1.18-r3", + "Name": "musl", + "Version": "1.1.18-r3", + "Arch": "x86_64", + "SrcName": "musl", + "SrcVersion": "1.1.18-r3", + "Licenses": [ + "MIT" + ], + "Maintainer": "Timo Teräs \u003ctimo.teras@iki.fi\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "musl-utils@1.1.18-r3", + "Name": "musl-utils", + "Version": "1.1.18-r3", + "Arch": "x86_64", + "SrcName": "musl", + "SrcVersion": "1.1.18-r3", + "Licenses": [ + "MIT", + "BSD-3-Clause", + "GPL-2.0-or-later" + ], + "Maintainer": "Timo Teräs \u003ctimo.teras@iki.fi\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "ncurses-libs@6.0_p20171125-r1", + "Name": "ncurses-libs", + "Version": "6.0_p20171125-r1", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.0_p20171125-r1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "ncurses-terminfo@6.0_p20171125-r1", + "Name": "ncurses-terminfo", + "Version": "6.0_p20171125-r1", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.0_p20171125-r1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "ncurses-terminfo-base@6.0_p20171125-r1", + "Name": "ncurses-terminfo-base", + "Version": "6.0_p20171125-r1", + "Arch": "x86_64", + "SrcName": "ncurses", + "SrcVersion": "6.0_p20171125-r1", + "Licenses": [ + "MIT" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "openssh@7.5_p1-r9", + "Name": "openssh", + "Version": "7.5_p1-r9", + "Arch": "x86_64", + "SrcName": "openssh", + "SrcVersion": "7.5_p1-r9", + "Licenses": [ + "as-is" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "openssh-client@7.5_p1-r9", + "Name": "openssh-client", + "Version": "7.5_p1-r9", + "Arch": "x86_64", + "SrcName": "openssh", + "SrcVersion": "7.5_p1-r9", + "Licenses": [ + "as-is" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "openssh-keygen@7.5_p1-r9", + "Name": "openssh-keygen", + "Version": "7.5_p1-r9", + "Arch": "x86_64", + "SrcName": "openssh", + "SrcVersion": "7.5_p1-r9", + "Licenses": [ + "as-is" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "openssh-server@7.5_p1-r9", + "Name": "openssh-server", + "Version": "7.5_p1-r9", + "Arch": "x86_64", + "SrcName": "openssh", + "SrcVersion": "7.5_p1-r9", + "Licenses": [ + "as-is" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "openssh-server-common@7.5_p1-r9", + "Name": "openssh-server-common", + "Version": "7.5_p1-r9", + "Arch": "x86_64", + "SrcName": "openssh", + "SrcVersion": "7.5_p1-r9", + "Licenses": [ + "as-is" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "openssh-sftp-server@7.5_p1-r9", + "Name": "openssh-sftp-server", + "Version": "7.5_p1-r9", + "Arch": "x86_64", + "SrcName": "openssh", + "SrcVersion": "7.5_p1-r9", + "Licenses": [ + "as-is" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "patch@2.7.5-r2", + "Name": "patch", + "Version": "2.7.5-r2", + "Arch": "x86_64", + "SrcName": "patch", + "SrcVersion": "2.7.5-r2", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "pcre2@10.30-r0", + "Name": "pcre2", + "Version": "10.30-r0", + "Arch": "x86_64", + "SrcName": "pcre2", + "SrcVersion": "10.30-r0", + "Licenses": [ + "BSD-3-Clause" + ], + "Maintainer": "Jakub Jirutka \u003cjakub@jirutka.cz\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "pkgconf@1.3.10-r0", + "Name": "pkgconf", + "Version": "1.3.10-r0", + "Arch": "x86_64", + "SrcName": "pkgconf", + "SrcVersion": "1.3.10-r0", + "Licenses": [ + "ISC" + ], + "Maintainer": "William Pitcock \u003cnenolod@dereferenced.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "python2@2.7.15-r2", + "Name": "python2", + "Version": "2.7.15-r2", + "Arch": "x86_64", + "SrcName": "python2", + "SrcVersion": "2.7.15-r2", + "Licenses": [ + "custom" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "readline@7.0.003-r0", + "Name": "readline", + "Version": "7.0.003-r0", + "Arch": "x86_64", + "SrcName": "readline", + "SrcVersion": "7.0.003-r0", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "scanelf@1.2.2-r1", + "Name": "scanelf", + "Version": "1.2.2-r1", + "Arch": "x86_64", + "SrcName": "pax-utils", + "SrcVersion": "1.2.2-r1", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "serf@1.3.9-r3", + "Name": "serf", + "Version": "1.3.9-r3", + "Arch": "x86_64", + "SrcName": "serf", + "SrcVersion": "1.3.9-r3", + "Licenses": [ + "Apache-2.0" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "sqlite-libs@3.21.0-r1", + "Name": "sqlite-libs", + "Version": "3.21.0-r1", + "Arch": "x86_64", + "SrcName": "sqlite", + "SrcVersion": "3.21.0-r1", + "Licenses": [ + "custom" + ], + "Maintainer": "Carlo Landmeter \u003cclandmeter@gmail.com\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "ssl_client@1.27.2-r11", + "Name": "ssl_client", + "Version": "1.27.2-r11", + "Arch": "x86_64", + "SrcName": "busybox", + "SrcVersion": "1.27.2-r11", + "Licenses": [ + "GPL-2.0-only" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "subversion@1.9.7-r0", + "Name": "subversion", + "Version": "1.9.7-r0", + "Arch": "x86_64", + "SrcName": "subversion", + "SrcVersion": "1.9.7-r0", + "Licenses": [ + "Apache-2.0", + "BSD-3-Clause" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "subversion-libs@1.9.7-r0", + "Name": "subversion-libs", + "Version": "1.9.7-r0", + "Arch": "x86_64", + "SrcName": "subversion", + "SrcVersion": "1.9.7-r0", + "Licenses": [ + "Apache-2.0", + "BSD-3-Clause" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "tar@1.29-r1", + "Name": "tar", + "Version": "1.29-r1", + "Arch": "x86_64", + "SrcName": "tar", + "SrcVersion": "1.29-r1", + "Licenses": [ + "GPL-2.0-or-later" + ], + "Maintainer": "Carlo Landmeter \u003cclandmeter@gmail.com\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "tini@0.16.1-r0", + "Name": "tini", + "Version": "0.16.1-r0", + "Arch": "x86_64", + "SrcName": "tini", + "SrcVersion": "0.16.1-r0", + "Licenses": [ + "MIT" + ], + "Maintainer": "Danilo Bürger \u003cdanilo@feastr.de\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "xz@5.2.3-r1", + "Name": "xz", + "Version": "5.2.3-r1", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.3-r1", + "Licenses": [ + "custom" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "xz-libs@5.2.3-r1", + "Name": "xz-libs", + "Version": "5.2.3-r1", + "Arch": "x86_64", + "SrcName": "xz", + "SrcVersion": "5.2.3-r1", + "Licenses": [ + "custom" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + }, + { + "ID": "zlib@1.2.11-r1", + "Name": "zlib", + "Version": "1.2.11-r1", + "Arch": "x86_64", + "SrcName": "zlib", + "SrcVersion": "1.2.11-r1", + "Licenses": [ + "Zlib" + ], + "Maintainer": "Natanael Copa \u003cncopa@alpinelinux.org\u003e", + "AnalyzedBy": "apk" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/vuln-image1.2.3.expectedlibs.golden b/pkg/fanal/test/integration/testdata/goldens/vuln-image1.2.3.expectedlibs.golden new file mode 100644 index 000000000000..c87def3a38f0 --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/vuln-image1.2.3.expectedlibs.golden @@ -0,0 +1,3884 @@ +[ + { + "Type": "bundler", + "FilePath": "ruby-app/Gemfile.lock", + "Packages": [ + { + "ID": "dotenv@2.7.2", + "Name": "dotenv", + "Identifier": { + "PURL": "pkg:gem/dotenv@2.7.2", + "UID": "51ccd3b7f80b12fd" + }, + "Version": "2.7.2", + "Relationship": "direct", + "Locations": [ + { + "StartLine": 51, + "EndLine": 51 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "faker@1.9.3", + "Name": "faker", + "Identifier": { + "PURL": "pkg:gem/faker@1.9.3", + "UID": "28034eeff60d676b" + }, + "Version": "1.9.3", + "Relationship": "direct", + "DependsOn": [ + "i18n@1.6.0" + ], + "Locations": [ + { + "StartLine": 53, + "EndLine": 53 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "json@2.2.0", + "Name": "json", + "Identifier": { + "PURL": "pkg:gem/json@2.2.0", + "UID": "f374520ee972d1b5" + }, + "Version": "2.2.0", + "Relationship": "direct", + "Locations": [ + { + "StartLine": 60, + "EndLine": 60 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "pry@0.12.2", + "Name": "pry", + "Identifier": { + "PURL": "pkg:gem/pry@0.12.2", + "UID": "8c3d33db0ab343a6" + }, + "Version": "0.12.2", + "Relationship": "direct", + "DependsOn": [ + "coderay@1.1.2", + "method_source@0.9.2" + ], + "Locations": [ + { + "StartLine": 79, + "EndLine": 79 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "rails@5.2.0", + "Name": "rails", + "Identifier": { + "PURL": "pkg:gem/rails@5.2.0", + "UID": "219fc5b3952c8cec" + }, + "Version": "5.2.0", + "Relationship": "direct", + "DependsOn": [ + "actioncable@5.2.3", + "actionmailer@5.2.3", + "actionpack@5.2.3", + "actionview@5.2.3", + "activejob@5.2.3", + "activemodel@5.2.3", + "activerecord@5.2.3", + "activestorage@5.2.3", + "activesupport@5.2.3", + "railties@5.2.3", + "sprockets-rails@3.2.1" + ], + "Locations": [ + { + "StartLine": 86, + "EndLine": 86 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "rubocop@0.67.2", + "Name": "rubocop", + "Identifier": { + "PURL": "pkg:gem/rubocop@0.67.2", + "UID": "3de3102ed6539f53" + }, + "Version": "0.67.2", + "Relationship": "direct", + "DependsOn": [ + "jaro_winkler@1.5.2", + "parallel@1.17.0", + "parser@2.6.3.0", + "psych@3.1.0", + "rainbow@3.0.0", + "ruby-progressbar@1.10.0", + "unicode-display_width@1.5.0" + ], + "Locations": [ + { + "StartLine": 112, + "EndLine": 112 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "actioncable@5.2.3", + "Name": "actioncable", + "Identifier": { + "PURL": "pkg:gem/actioncable@5.2.3", + "UID": "766eb7543b30ff60" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "actionpack@5.2.3", + "nio4r@2.3.1", + "websocket-driver@0.7.0" + ], + "Locations": [ + { + "StartLine": 4, + "EndLine": 4 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "actionmailer@5.2.3", + "Name": "actionmailer", + "Identifier": { + "PURL": "pkg:gem/actionmailer@5.2.3", + "UID": "7cc09a125f112a8" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "actionpack@5.2.3", + "actionview@5.2.3", + "activejob@5.2.3", + "mail@2.7.1", + "rails-dom-testing@2.0.3" + ], + "Locations": [ + { + "StartLine": 8, + "EndLine": 8 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "actionpack@5.2.3", + "Name": "actionpack", + "Identifier": { + "PURL": "pkg:gem/actionpack@5.2.3", + "UID": "94ac3076aabbca0f" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "actionview@5.2.3", + "activesupport@5.2.3", + "rack-test@1.1.0", + "rack@2.0.7", + "rails-dom-testing@2.0.3", + "rails-html-sanitizer@1.0.3" + ], + "Locations": [ + { + "StartLine": 14, + "EndLine": 14 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "actionview@5.2.3", + "Name": "actionview", + "Identifier": { + "PURL": "pkg:gem/actionview@5.2.3", + "UID": "d6d01c2444874c03" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "activesupport@5.2.3", + "builder@3.2.3", + "erubi@1.8.0", + "rails-dom-testing@2.0.3", + "rails-html-sanitizer@1.0.3" + ], + "Locations": [ + { + "StartLine": 21, + "EndLine": 21 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "activejob@5.2.3", + "Name": "activejob", + "Identifier": { + "PURL": "pkg:gem/activejob@5.2.3", + "UID": "1681d460c8bc6622" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "activesupport@5.2.3", + "globalid@0.4.2" + ], + "Locations": [ + { + "StartLine": 27, + "EndLine": 27 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "activemodel@5.2.3", + "Name": "activemodel", + "Identifier": { + "PURL": "pkg:gem/activemodel@5.2.3", + "UID": "b605d2a21b57c91d" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "activesupport@5.2.3" + ], + "Locations": [ + { + "StartLine": 30, + "EndLine": 30 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "activerecord@5.2.3", + "Name": "activerecord", + "Identifier": { + "PURL": "pkg:gem/activerecord@5.2.3", + "UID": "91a0fa50c9769d7d" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "activemodel@5.2.3", + "activesupport@5.2.3", + "arel@9.0.0" + ], + "Locations": [ + { + "StartLine": 32, + "EndLine": 32 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "activestorage@5.2.3", + "Name": "activestorage", + "Identifier": { + "PURL": "pkg:gem/activestorage@5.2.3", + "UID": "cf033bb502055fde" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "actionpack@5.2.3", + "activerecord@5.2.3", + "marcel@0.3.3" + ], + "Locations": [ + { + "StartLine": 36, + "EndLine": 36 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "activesupport@5.2.3", + "Name": "activesupport", + "Identifier": { + "PURL": "pkg:gem/activesupport@5.2.3", + "UID": "1c994e4d475fb58" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "concurrent-ruby@1.1.5", + "i18n@1.6.0", + "minitest@5.11.3", + "tzinfo@1.2.5" + ], + "Locations": [ + { + "StartLine": 40, + "EndLine": 40 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "arel@9.0.0", + "Name": "arel", + "Identifier": { + "PURL": "pkg:gem/arel@9.0.0", + "UID": "67050a5333ca1978" + }, + "Version": "9.0.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 45, + "EndLine": 45 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "ast@2.4.0", + "Name": "ast", + "Identifier": { + "PURL": "pkg:gem/ast@2.4.0", + "UID": "5e276abf734de240" + }, + "Version": "2.4.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 46, + "EndLine": 46 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "builder@3.2.3", + "Name": "builder", + "Identifier": { + "PURL": "pkg:gem/builder@3.2.3", + "UID": "b2010bbf9b3687c6" + }, + "Version": "3.2.3", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 47, + "EndLine": 47 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "coderay@1.1.2", + "Name": "coderay", + "Identifier": { + "PURL": "pkg:gem/coderay@1.1.2", + "UID": "947b3348bf87b137" + }, + "Version": "1.1.2", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 48, + "EndLine": 48 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "concurrent-ruby@1.1.5", + "Name": "concurrent-ruby", + "Identifier": { + "PURL": "pkg:gem/concurrent-ruby@1.1.5", + "UID": "d1368fecc729df70" + }, + "Version": "1.1.5", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 49, + "EndLine": 49 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "crass@1.0.4", + "Name": "crass", + "Identifier": { + "PURL": "pkg:gem/crass@1.0.4", + "UID": "9c8ae1248fd78119" + }, + "Version": "1.0.4", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 50, + "EndLine": 50 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "erubi@1.8.0", + "Name": "erubi", + "Identifier": { + "PURL": "pkg:gem/erubi@1.8.0", + "UID": "bb9a9ca1981e02b7" + }, + "Version": "1.8.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 52, + "EndLine": 52 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "globalid@0.4.2", + "Name": "globalid", + "Identifier": { + "PURL": "pkg:gem/globalid@0.4.2", + "UID": "a9caafd94dbfccde" + }, + "Version": "0.4.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "activesupport@5.2.3" + ], + "Locations": [ + { + "StartLine": 55, + "EndLine": 55 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "i18n@1.6.0", + "Name": "i18n", + "Identifier": { + "PURL": "pkg:gem/i18n@1.6.0", + "UID": "1f71205168288b65" + }, + "Version": "1.6.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "concurrent-ruby@1.1.5" + ], + "Locations": [ + { + "StartLine": 57, + "EndLine": 57 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "jaro_winkler@1.5.2", + "Name": "jaro_winkler", + "Identifier": { + "PURL": "pkg:gem/jaro_winkler@1.5.2", + "UID": "69562ca84551bf62" + }, + "Version": "1.5.2", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 59, + "EndLine": 59 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "loofah@2.2.3", + "Name": "loofah", + "Identifier": { + "PURL": "pkg:gem/loofah@2.2.3", + "UID": "92b0441e7c571ef" + }, + "Version": "2.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "crass@1.0.4", + "nokogiri@1.10.3" + ], + "Locations": [ + { + "StartLine": 61, + "EndLine": 61 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "mail@2.7.1", + "Name": "mail", + "Identifier": { + "PURL": "pkg:gem/mail@2.7.1", + "UID": "c446a2837b53d2be" + }, + "Version": "2.7.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "mini_mime@1.0.1" + ], + "Locations": [ + { + "StartLine": 64, + "EndLine": 64 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "marcel@0.3.3", + "Name": "marcel", + "Identifier": { + "PURL": "pkg:gem/marcel@0.3.3", + "UID": "b9123bf921653d9d" + }, + "Version": "0.3.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "mimemagic@0.3.3" + ], + "Locations": [ + { + "StartLine": 66, + "EndLine": 66 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "method_source@0.9.2", + "Name": "method_source", + "Identifier": { + "PURL": "pkg:gem/method_source@0.9.2", + "UID": "8e23dd5f4439a4ee" + }, + "Version": "0.9.2", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 68, + "EndLine": 68 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "mimemagic@0.3.3", + "Name": "mimemagic", + "Identifier": { + "PURL": "pkg:gem/mimemagic@0.3.3", + "UID": "ec25ad93630e1b3a" + }, + "Version": "0.3.3", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 69, + "EndLine": 69 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "mini_mime@1.0.1", + "Name": "mini_mime", + "Identifier": { + "PURL": "pkg:gem/mini_mime@1.0.1", + "UID": "5dcb2dbb9d7d7373" + }, + "Version": "1.0.1", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 70, + "EndLine": 70 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "mini_portile2@2.4.0", + "Name": "mini_portile2", + "Identifier": { + "PURL": "pkg:gem/mini_portile2@2.4.0", + "UID": "a6c4ccfb8ba99fa6" + }, + "Version": "2.4.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 71, + "EndLine": 71 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "minitest@5.11.3", + "Name": "minitest", + "Identifier": { + "PURL": "pkg:gem/minitest@5.11.3", + "UID": "4761c3a4ef7f9da8" + }, + "Version": "5.11.3", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 72, + "EndLine": 72 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "nio4r@2.3.1", + "Name": "nio4r", + "Identifier": { + "PURL": "pkg:gem/nio4r@2.3.1", + "UID": "2109a0ea6c9e7d04" + }, + "Version": "2.3.1", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 73, + "EndLine": 73 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "nokogiri@1.10.3", + "Name": "nokogiri", + "Identifier": { + "PURL": "pkg:gem/nokogiri@1.10.3", + "UID": "c03b5922c7d09be7" + }, + "Version": "1.10.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "mini_portile2@2.4.0" + ], + "Locations": [ + { + "StartLine": 74, + "EndLine": 74 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "parallel@1.17.0", + "Name": "parallel", + "Identifier": { + "PURL": "pkg:gem/parallel@1.17.0", + "UID": "4b75efe0f58fbce5" + }, + "Version": "1.17.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 76, + "EndLine": 76 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "parser@2.6.3.0", + "Name": "parser", + "Identifier": { + "PURL": "pkg:gem/parser@2.6.3.0", + "UID": "78c97812ca0c3bc6" + }, + "Version": "2.6.3.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "ast@2.4.0" + ], + "Locations": [ + { + "StartLine": 77, + "EndLine": 77 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "psych@3.1.0", + "Name": "psych", + "Identifier": { + "PURL": "pkg:gem/psych@3.1.0", + "UID": "9a1ce93e479d9615" + }, + "Version": "3.1.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 82, + "EndLine": 82 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "rack@2.0.7", + "Name": "rack", + "Identifier": { + "PURL": "pkg:gem/rack@2.0.7", + "UID": "7ecd516bd031ae33" + }, + "Version": "2.0.7", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 83, + "EndLine": 83 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "rack-test@1.1.0", + "Name": "rack-test", + "Identifier": { + "PURL": "pkg:gem/rack-test@1.1.0", + "UID": "f100c98bff0fd65d" + }, + "Version": "1.1.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "rack@2.0.7" + ], + "Locations": [ + { + "StartLine": 84, + "EndLine": 84 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "rails-dom-testing@2.0.3", + "Name": "rails-dom-testing", + "Identifier": { + "PURL": "pkg:gem/rails-dom-testing@2.0.3", + "UID": "40cc36bdf8f667c5" + }, + "Version": "2.0.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "activesupport@5.2.3", + "nokogiri@1.10.3" + ], + "Locations": [ + { + "StartLine": 99, + "EndLine": 99 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "rails-html-sanitizer@1.0.3", + "Name": "rails-html-sanitizer", + "Identifier": { + "PURL": "pkg:gem/rails-html-sanitizer@1.0.3", + "UID": "e2d181414e7cb479" + }, + "Version": "1.0.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "loofah@2.2.3" + ], + "Locations": [ + { + "StartLine": 102, + "EndLine": 102 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "railties@5.2.3", + "Name": "railties", + "Identifier": { + "PURL": "pkg:gem/railties@5.2.3", + "UID": "7aeb8224e622b806" + }, + "Version": "5.2.3", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "actionpack@5.2.3", + "activesupport@5.2.3", + "method_source@0.9.2", + "rake@12.3.2", + "thor@0.20.3" + ], + "Locations": [ + { + "StartLine": 104, + "EndLine": 104 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "rainbow@3.0.0", + "Name": "rainbow", + "Identifier": { + "PURL": "pkg:gem/rainbow@3.0.0", + "UID": "96989d4d4a24e83e" + }, + "Version": "3.0.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 110, + "EndLine": 110 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "rake@12.3.2", + "Name": "rake", + "Identifier": { + "PURL": "pkg:gem/rake@12.3.2", + "UID": "eb7d1060017b8bb6" + }, + "Version": "12.3.2", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 111, + "EndLine": 111 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "ruby-progressbar@1.10.0", + "Name": "ruby-progressbar", + "Identifier": { + "PURL": "pkg:gem/ruby-progressbar@1.10.0", + "UID": "bb68e1a0616e3f39" + }, + "Version": "1.10.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 120, + "EndLine": 120 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "sprockets@3.7.2", + "Name": "sprockets", + "Identifier": { + "PURL": "pkg:gem/sprockets@3.7.2", + "UID": "90564a741dafb5f5" + }, + "Version": "3.7.2", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "concurrent-ruby@1.1.5", + "rack@2.0.7" + ], + "Locations": [ + { + "StartLine": 121, + "EndLine": 121 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "sprockets-rails@3.2.1", + "Name": "sprockets-rails", + "Identifier": { + "PURL": "pkg:gem/sprockets-rails@3.2.1", + "UID": "f29f1b9b09eecf81" + }, + "Version": "3.2.1", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "actionpack@5.2.3", + "activesupport@5.2.3", + "sprockets@3.7.2" + ], + "Locations": [ + { + "StartLine": 124, + "EndLine": 124 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "thor@0.20.3", + "Name": "thor", + "Identifier": { + "PURL": "pkg:gem/thor@0.20.3", + "UID": "5d2199013355ab13" + }, + "Version": "0.20.3", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 128, + "EndLine": 128 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "thread_safe@0.3.6", + "Name": "thread_safe", + "Identifier": { + "PURL": "pkg:gem/thread_safe@0.3.6", + "UID": "1d90f78865adc099" + }, + "Version": "0.3.6", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 129, + "EndLine": 129 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "tzinfo@1.2.5", + "Name": "tzinfo", + "Identifier": { + "PURL": "pkg:gem/tzinfo@1.2.5", + "UID": "7c1519298ff4e3d8" + }, + "Version": "1.2.5", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "thread_safe@0.3.6" + ], + "Locations": [ + { + "StartLine": 130, + "EndLine": 130 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "unicode-display_width@1.5.0", + "Name": "unicode-display_width", + "Identifier": { + "PURL": "pkg:gem/unicode-display_width@1.5.0", + "UID": "27c8eabadae422a6" + }, + "Version": "1.5.0", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 132, + "EndLine": 132 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "websocket-driver@0.7.0", + "Name": "websocket-driver", + "Identifier": { + "PURL": "pkg:gem/websocket-driver@0.7.0", + "UID": "70cdd15ab0702399" + }, + "Version": "0.7.0", + "Indirect": true, + "Relationship": "indirect", + "DependsOn": [ + "websocket-extensions@0.1.3" + ], + "Locations": [ + { + "StartLine": 133, + "EndLine": 133 + } + ], + "AnalyzedBy": "bundler" + }, + { + "ID": "websocket-extensions@0.1.3", + "Name": "websocket-extensions", + "Identifier": { + "PURL": "pkg:gem/websocket-extensions@0.1.3", + "UID": "adb09a5be959e4e9" + }, + "Version": "0.1.3", + "Indirect": true, + "Relationship": "indirect", + "Locations": [ + { + "StartLine": 135, + "EndLine": 135 + } + ], + "AnalyzedBy": "bundler" + } + ] + }, + { + "Type": "cargo", + "FilePath": "rust-app/Cargo.lock", + "Packages": [ + { + "ID": "ammonia@1.9.0", + "Name": "ammonia", + "Identifier": { + "PURL": "pkg:cargo/ammonia@1.9.0", + "UID": "b138a6a2e82b6798" + }, + "Version": "1.9.0", + "DependsOn": [ + "html5ever@0.23.0", + "lazy_static@1.3.0", + "maplit@1.0.1", + "matches@0.1.8", + "tendril@0.4.1", + "url@1.7.2" + ], + "Locations": [ + { + "StartLine": 2, + "EndLine": 13 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "autocfg@0.1.2", + "Name": "autocfg", + "Identifier": { + "PURL": "pkg:cargo/autocfg@0.1.2", + "UID": "24c33fdbccb75f9a" + }, + "Version": "0.1.2", + "Locations": [ + { + "StartLine": 15, + "EndLine": 18 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "bitflags@0.7.0", + "Name": "bitflags", + "Identifier": { + "PURL": "pkg:cargo/bitflags@0.7.0", + "UID": "6d339610964e7bc8" + }, + "Version": "0.7.0", + "Locations": [ + { + "StartLine": 20, + "EndLine": 23 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "bitflags@1.0.4", + "Name": "bitflags", + "Identifier": { + "PURL": "pkg:cargo/bitflags@1.0.4", + "UID": "3014171e1eb71267" + }, + "Version": "1.0.4", + "Locations": [ + { + "StartLine": 25, + "EndLine": 28 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "cfg-if@0.1.7", + "Name": "cfg-if", + "Identifier": { + "PURL": "pkg:cargo/cfg-if@0.1.7", + "UID": "6ce1cb014761a51d" + }, + "Version": "0.1.7", + "Locations": [ + { + "StartLine": 30, + "EndLine": 33 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "cloudabi@0.0.3", + "Name": "cloudabi", + "Identifier": { + "PURL": "pkg:cargo/cloudabi@0.0.3", + "UID": "d36fd26bbf20be96" + }, + "Version": "0.0.3", + "DependsOn": [ + "bitflags@1.0.4" + ], + "Locations": [ + { + "StartLine": 35, + "EndLine": 41 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "fuchsia-cprng@0.1.1", + "Name": "fuchsia-cprng", + "Identifier": { + "PURL": "pkg:cargo/fuchsia-cprng@0.1.1", + "UID": "7ef7d40353d8316e" + }, + "Version": "0.1.1", + "Locations": [ + { + "StartLine": 43, + "EndLine": 46 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "futf@0.1.4", + "Name": "futf", + "Identifier": { + "PURL": "pkg:cargo/futf@0.1.4", + "UID": "b36f32b4942904a4" + }, + "Version": "0.1.4", + "DependsOn": [ + "mac@0.1.1", + "new_debug_unreachable@1.0.3" + ], + "Locations": [ + { + "StartLine": 48, + "EndLine": 55 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "gdi32-sys@0.2.0", + "Name": "gdi32-sys", + "Identifier": { + "PURL": "pkg:cargo/gdi32-sys@0.2.0", + "UID": "608be31767312430" + }, + "Version": "0.2.0", + "DependsOn": [ + "winapi-build@0.1.1", + "winapi@0.2.8" + ], + "Locations": [ + { + "StartLine": 57, + "EndLine": 64 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "html5ever@0.23.0", + "Name": "html5ever", + "Identifier": { + "PURL": "pkg:cargo/html5ever@0.23.0", + "UID": "eef20cbff4275640" + }, + "Version": "0.23.0", + "DependsOn": [ + "log@0.4.6", + "mac@0.1.1", + "markup5ever@0.8.1", + "proc-macro2@0.4.30", + "quote@0.6.12", + "syn@0.15.34" + ], + "Locations": [ + { + "StartLine": 66, + "EndLine": 77 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "idna@0.1.5", + "Name": "idna", + "Identifier": { + "PURL": "pkg:cargo/idna@0.1.5", + "UID": "d4b1a4ad61b509fa" + }, + "Version": "0.1.5", + "DependsOn": [ + "matches@0.1.8", + "unicode-bidi@0.3.4", + "unicode-normalization@0.1.8" + ], + "Locations": [ + { + "StartLine": 79, + "EndLine": 87 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "itoa@0.4.4", + "Name": "itoa", + "Identifier": { + "PURL": "pkg:cargo/itoa@0.4.4", + "UID": "66285d4e71c748b9" + }, + "Version": "0.4.4", + "Locations": [ + { + "StartLine": 89, + "EndLine": 92 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "kernel32-sys@0.2.2", + "Name": "kernel32-sys", + "Identifier": { + "PURL": "pkg:cargo/kernel32-sys@0.2.2", + "UID": "cd50bb61e67dc2f6" + }, + "Version": "0.2.2", + "DependsOn": [ + "winapi-build@0.1.1", + "winapi@0.2.8" + ], + "Locations": [ + { + "StartLine": 94, + "EndLine": 101 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "lazy_static@0.2.11", + "Name": "lazy_static", + "Identifier": { + "PURL": "pkg:cargo/lazy_static@0.2.11", + "UID": "cffaa44833b3362f" + }, + "Version": "0.2.11", + "Locations": [ + { + "StartLine": 103, + "EndLine": 106 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "lazy_static@1.3.0", + "Name": "lazy_static", + "Identifier": { + "PURL": "pkg:cargo/lazy_static@1.3.0", + "UID": "c2ce98690a7c9ba0" + }, + "Version": "1.3.0", + "Locations": [ + { + "StartLine": 108, + "EndLine": 111 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "libc@0.2.54", + "Name": "libc", + "Identifier": { + "PURL": "pkg:cargo/libc@0.2.54", + "UID": "b5ac8e855bcf326e" + }, + "Version": "0.2.54", + "Locations": [ + { + "StartLine": 113, + "EndLine": 116 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "libressl-pnacl-sys@2.1.6", + "Name": "libressl-pnacl-sys", + "Identifier": { + "PURL": "pkg:cargo/libressl-pnacl-sys@2.1.6", + "UID": "85d2501e2e54f3a3" + }, + "Version": "2.1.6", + "DependsOn": [ + "pnacl-build-helper@1.4.11" + ], + "Locations": [ + { + "StartLine": 118, + "EndLine": 124 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "log@0.4.6", + "Name": "log", + "Identifier": { + "PURL": "pkg:cargo/log@0.4.6", + "UID": "937373b7562137a7" + }, + "Version": "0.4.6", + "DependsOn": [ + "cfg-if@0.1.7" + ], + "Locations": [ + { + "StartLine": 126, + "EndLine": 132 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "mac@0.1.1", + "Name": "mac", + "Identifier": { + "PURL": "pkg:cargo/mac@0.1.1", + "UID": "2fb0c8dbecca3a0" + }, + "Version": "0.1.1", + "Locations": [ + { + "StartLine": 134, + "EndLine": 137 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "maplit@1.0.1", + "Name": "maplit", + "Identifier": { + "PURL": "pkg:cargo/maplit@1.0.1", + "UID": "c36bfaad24462c49" + }, + "Version": "1.0.1", + "Locations": [ + { + "StartLine": 139, + "EndLine": 142 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "markup5ever@0.8.1", + "Name": "markup5ever", + "Identifier": { + "PURL": "pkg:cargo/markup5ever@0.8.1", + "UID": "f35b7bf2d42587a1" + }, + "Version": "0.8.1", + "DependsOn": [ + "log@0.4.6", + "phf@0.7.24", + "phf_codegen@0.7.24", + "serde@1.0.91", + "serde_derive@1.0.91", + "serde_json@1.0.39", + "string_cache@0.7.3", + "string_cache_codegen@0.4.2", + "tendril@0.4.1" + ], + "Locations": [ + { + "StartLine": 144, + "EndLine": 158 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "matches@0.1.8", + "Name": "matches", + "Identifier": { + "PURL": "pkg:cargo/matches@0.1.8", + "UID": "fe0a8344e86fd2ad" + }, + "Version": "0.1.8", + "Locations": [ + { + "StartLine": 160, + "EndLine": 163 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "new_debug_unreachable@1.0.3", + "Name": "new_debug_unreachable", + "Identifier": { + "PURL": "pkg:cargo/new_debug_unreachable@1.0.3", + "UID": "edf1087865999f63" + }, + "Version": "1.0.3", + "Locations": [ + { + "StartLine": 165, + "EndLine": 168 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "normal@0.1.0", + "Name": "normal", + "Identifier": { + "PURL": "pkg:cargo/normal@0.1.0", + "UID": "987b0ea9f7ca9d77" + }, + "Version": "0.1.0", + "DependsOn": [ + "ammonia@2.0.0", + "libc@0.2.54", + "openssl@0.8.3" + ], + "Locations": [ + { + "StartLine": 170, + "EndLine": 177 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "openssl@0.8.3", + "Name": "openssl", + "Identifier": { + "PURL": "pkg:cargo/openssl@0.8.3", + "UID": "51168183eb3025b9" + }, + "Version": "0.8.3", + "DependsOn": [ + "bitflags@0.7.0", + "lazy_static@0.2.11", + "libc@0.2.54", + "openssl-sys@0.7.17" + ], + "Locations": [ + { + "StartLine": 179, + "EndLine": 188 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "openssl-sys@0.7.17", + "Name": "openssl-sys", + "Identifier": { + "PURL": "pkg:cargo/openssl-sys@0.7.17", + "UID": "ad8ede471d71ec52" + }, + "Version": "0.7.17", + "DependsOn": [ + "gdi32-sys@0.2.0", + "libc@0.2.54", + "libressl-pnacl-sys@2.1.6", + "pkg-config@0.3.14", + "user32-sys@0.2.0" + ], + "Locations": [ + { + "StartLine": 190, + "EndLine": 200 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "percent-encoding@1.0.1", + "Name": "percent-encoding", + "Identifier": { + "PURL": "pkg:cargo/percent-encoding@1.0.1", + "UID": "7b6ad0ae7b0f8484" + }, + "Version": "1.0.1", + "Locations": [ + { + "StartLine": 202, + "EndLine": 205 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "phf@0.7.24", + "Name": "phf", + "Identifier": { + "PURL": "pkg:cargo/phf@0.7.24", + "UID": "ab63f12f43c63dfa" + }, + "Version": "0.7.24", + "DependsOn": [ + "phf_shared@0.7.24" + ], + "Locations": [ + { + "StartLine": 207, + "EndLine": 213 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "phf_codegen@0.7.24", + "Name": "phf_codegen", + "Identifier": { + "PURL": "pkg:cargo/phf_codegen@0.7.24", + "UID": "476d3f1b10749392" + }, + "Version": "0.7.24", + "DependsOn": [ + "phf_generator@0.7.24", + "phf_shared@0.7.24" + ], + "Locations": [ + { + "StartLine": 215, + "EndLine": 222 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "phf_generator@0.7.24", + "Name": "phf_generator", + "Identifier": { + "PURL": "pkg:cargo/phf_generator@0.7.24", + "UID": "cd5ceab0466829ec" + }, + "Version": "0.7.24", + "DependsOn": [ + "phf_shared@0.7.24", + "rand@0.6.5" + ], + "Locations": [ + { + "StartLine": 224, + "EndLine": 231 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "phf_shared@0.7.24", + "Name": "phf_shared", + "Identifier": { + "PURL": "pkg:cargo/phf_shared@0.7.24", + "UID": "dedd4fb670ca214c" + }, + "Version": "0.7.24", + "DependsOn": [ + "siphasher@0.2.3" + ], + "Locations": [ + { + "StartLine": 233, + "EndLine": 239 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "pkg-config@0.3.14", + "Name": "pkg-config", + "Identifier": { + "PURL": "pkg:cargo/pkg-config@0.3.14", + "UID": "77c6c8446f50b32e" + }, + "Version": "0.3.14", + "Locations": [ + { + "StartLine": 241, + "EndLine": 244 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "pnacl-build-helper@1.4.11", + "Name": "pnacl-build-helper", + "Identifier": { + "PURL": "pkg:cargo/pnacl-build-helper@1.4.11", + "UID": "6c11835275117225" + }, + "Version": "1.4.11", + "DependsOn": [ + "tempdir@0.3.7", + "walkdir@1.0.7" + ], + "Locations": [ + { + "StartLine": 246, + "EndLine": 253 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "precomputed-hash@0.1.1", + "Name": "precomputed-hash", + "Identifier": { + "PURL": "pkg:cargo/precomputed-hash@0.1.1", + "UID": "e358ee964ef84be" + }, + "Version": "0.1.1", + "Locations": [ + { + "StartLine": 255, + "EndLine": 258 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "proc-macro2@0.4.30", + "Name": "proc-macro2", + "Identifier": { + "PURL": "pkg:cargo/proc-macro2@0.4.30", + "UID": "29b4324f7ddb5eb1" + }, + "Version": "0.4.30", + "DependsOn": [ + "unicode-xid@0.1.0" + ], + "Locations": [ + { + "StartLine": 260, + "EndLine": 266 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "quote@0.6.12", + "Name": "quote", + "Identifier": { + "PURL": "pkg:cargo/quote@0.6.12", + "UID": "607438616321a350" + }, + "Version": "0.6.12", + "DependsOn": [ + "proc-macro2@0.4.30" + ], + "Locations": [ + { + "StartLine": 268, + "EndLine": 274 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand@0.4.6", + "Name": "rand", + "Identifier": { + "PURL": "pkg:cargo/rand@0.4.6", + "UID": "489e2fdf6923fe7e" + }, + "Version": "0.4.6", + "DependsOn": [ + "fuchsia-cprng@0.1.1", + "libc@0.2.54", + "rand_core@0.3.1", + "rdrand@0.4.0", + "winapi@0.3.7" + ], + "Locations": [ + { + "StartLine": 276, + "EndLine": 286 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand@0.6.5", + "Name": "rand", + "Identifier": { + "PURL": "pkg:cargo/rand@0.6.5", + "UID": "32191138e0f3906a" + }, + "Version": "0.6.5", + "DependsOn": [ + "autocfg@0.1.2", + "libc@0.2.54", + "rand_chacha@0.1.1", + "rand_core@0.4.0", + "rand_hc@0.1.0", + "rand_isaac@0.1.1", + "rand_jitter@0.1.4", + "rand_os@0.1.3", + "rand_pcg@0.1.2", + "rand_xorshift@0.1.1", + "winapi@0.3.7" + ], + "Locations": [ + { + "StartLine": 288, + "EndLine": 304 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand_chacha@0.1.1", + "Name": "rand_chacha", + "Identifier": { + "PURL": "pkg:cargo/rand_chacha@0.1.1", + "UID": "6ba2a6b28c84c638" + }, + "Version": "0.1.1", + "DependsOn": [ + "autocfg@0.1.2", + "rand_core@0.3.1" + ], + "Locations": [ + { + "StartLine": 306, + "EndLine": 313 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand_core@0.3.1", + "Name": "rand_core", + "Identifier": { + "PURL": "pkg:cargo/rand_core@0.3.1", + "UID": "c7ca6d44417bac8d" + }, + "Version": "0.3.1", + "DependsOn": [ + "rand_core@0.4.0" + ], + "Locations": [ + { + "StartLine": 315, + "EndLine": 321 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand_core@0.4.0", + "Name": "rand_core", + "Identifier": { + "PURL": "pkg:cargo/rand_core@0.4.0", + "UID": "c8f6cdecf34d2daf" + }, + "Version": "0.4.0", + "Locations": [ + { + "StartLine": 323, + "EndLine": 326 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand_hc@0.1.0", + "Name": "rand_hc", + "Identifier": { + "PURL": "pkg:cargo/rand_hc@0.1.0", + "UID": "66d85ae1d836a62" + }, + "Version": "0.1.0", + "DependsOn": [ + "rand_core@0.3.1" + ], + "Locations": [ + { + "StartLine": 328, + "EndLine": 334 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand_isaac@0.1.1", + "Name": "rand_isaac", + "Identifier": { + "PURL": "pkg:cargo/rand_isaac@0.1.1", + "UID": "cb497f0b2b1fb549" + }, + "Version": "0.1.1", + "DependsOn": [ + "rand_core@0.3.1" + ], + "Locations": [ + { + "StartLine": 336, + "EndLine": 342 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand_jitter@0.1.4", + "Name": "rand_jitter", + "Identifier": { + "PURL": "pkg:cargo/rand_jitter@0.1.4", + "UID": "cd416031cae4fbc2" + }, + "Version": "0.1.4", + "DependsOn": [ + "libc@0.2.54", + "rand_core@0.4.0", + "winapi@0.3.7" + ], + "Locations": [ + { + "StartLine": 344, + "EndLine": 352 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand_os@0.1.3", + "Name": "rand_os", + "Identifier": { + "PURL": "pkg:cargo/rand_os@0.1.3", + "UID": "80dd268a71dc1957" + }, + "Version": "0.1.3", + "DependsOn": [ + "cloudabi@0.0.3", + "fuchsia-cprng@0.1.1", + "libc@0.2.54", + "rand_core@0.4.0", + "rdrand@0.4.0", + "winapi@0.3.7" + ], + "Locations": [ + { + "StartLine": 354, + "EndLine": 365 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand_pcg@0.1.2", + "Name": "rand_pcg", + "Identifier": { + "PURL": "pkg:cargo/rand_pcg@0.1.2", + "UID": "fcf8ab0e9566752" + }, + "Version": "0.1.2", + "DependsOn": [ + "autocfg@0.1.2", + "rand_core@0.4.0" + ], + "Locations": [ + { + "StartLine": 367, + "EndLine": 374 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rand_xorshift@0.1.1", + "Name": "rand_xorshift", + "Identifier": { + "PURL": "pkg:cargo/rand_xorshift@0.1.1", + "UID": "a5c5ac44e310c758" + }, + "Version": "0.1.1", + "DependsOn": [ + "rand_core@0.3.1" + ], + "Locations": [ + { + "StartLine": 376, + "EndLine": 382 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "rdrand@0.4.0", + "Name": "rdrand", + "Identifier": { + "PURL": "pkg:cargo/rdrand@0.4.0", + "UID": "84a16fb1fbdb8643" + }, + "Version": "0.4.0", + "DependsOn": [ + "rand_core@0.3.1" + ], + "Locations": [ + { + "StartLine": 384, + "EndLine": 390 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "remove_dir_all@0.5.1", + "Name": "remove_dir_all", + "Identifier": { + "PURL": "pkg:cargo/remove_dir_all@0.5.1", + "UID": "666bee82ec1430fb" + }, + "Version": "0.5.1", + "DependsOn": [ + "winapi@0.3.7" + ], + "Locations": [ + { + "StartLine": 392, + "EndLine": 398 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "ryu@0.2.8", + "Name": "ryu", + "Identifier": { + "PURL": "pkg:cargo/ryu@0.2.8", + "UID": "828ecf48323337b8" + }, + "Version": "0.2.8", + "Locations": [ + { + "StartLine": 400, + "EndLine": 403 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "same-file@0.1.3", + "Name": "same-file", + "Identifier": { + "PURL": "pkg:cargo/same-file@0.1.3", + "UID": "b3915f9917bd84e2" + }, + "Version": "0.1.3", + "DependsOn": [ + "kernel32-sys@0.2.2", + "winapi@0.2.8" + ], + "Locations": [ + { + "StartLine": 405, + "EndLine": 412 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "serde@1.0.91", + "Name": "serde", + "Identifier": { + "PURL": "pkg:cargo/serde@1.0.91", + "UID": "5fe9b175e7ce6689" + }, + "Version": "1.0.91", + "Locations": [ + { + "StartLine": 414, + "EndLine": 417 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "serde_derive@1.0.91", + "Name": "serde_derive", + "Identifier": { + "PURL": "pkg:cargo/serde_derive@1.0.91", + "UID": "f4a1f7ac7e51f2b5" + }, + "Version": "1.0.91", + "DependsOn": [ + "proc-macro2@0.4.30", + "quote@0.6.12", + "syn@0.15.34" + ], + "Locations": [ + { + "StartLine": 419, + "EndLine": 427 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "serde_json@1.0.39", + "Name": "serde_json", + "Identifier": { + "PURL": "pkg:cargo/serde_json@1.0.39", + "UID": "1d74071458cda47" + }, + "Version": "1.0.39", + "DependsOn": [ + "itoa@0.4.4", + "ryu@0.2.8", + "serde@1.0.91" + ], + "Locations": [ + { + "StartLine": 429, + "EndLine": 437 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "siphasher@0.2.3", + "Name": "siphasher", + "Identifier": { + "PURL": "pkg:cargo/siphasher@0.2.3", + "UID": "f54fd406ff8538a0" + }, + "Version": "0.2.3", + "Locations": [ + { + "StartLine": 439, + "EndLine": 442 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "smallvec@0.6.9", + "Name": "smallvec", + "Identifier": { + "PURL": "pkg:cargo/smallvec@0.6.9", + "UID": "e168f878d8be26f6" + }, + "Version": "0.6.9", + "Locations": [ + { + "StartLine": 444, + "EndLine": 447 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "string_cache@0.7.3", + "Name": "string_cache", + "Identifier": { + "PURL": "pkg:cargo/string_cache@0.7.3", + "UID": "6c8b8f60b768923b" + }, + "Version": "0.7.3", + "DependsOn": [ + "lazy_static@1.3.0", + "new_debug_unreachable@1.0.3", + "phf_shared@0.7.24", + "precomputed-hash@0.1.1", + "serde@1.0.91", + "string_cache_codegen@0.4.2", + "string_cache_shared@0.3.0" + ], + "Locations": [ + { + "StartLine": 449, + "EndLine": 461 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "string_cache_codegen@0.4.2", + "Name": "string_cache_codegen", + "Identifier": { + "PURL": "pkg:cargo/string_cache_codegen@0.4.2", + "UID": "508e921d64ec050d" + }, + "Version": "0.4.2", + "DependsOn": [ + "phf_generator@0.7.24", + "phf_shared@0.7.24", + "proc-macro2@0.4.30", + "quote@0.6.12", + "string_cache_shared@0.3.0" + ], + "Locations": [ + { + "StartLine": 463, + "EndLine": 473 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "string_cache_shared@0.3.0", + "Name": "string_cache_shared", + "Identifier": { + "PURL": "pkg:cargo/string_cache_shared@0.3.0", + "UID": "3861d176e2576a73" + }, + "Version": "0.3.0", + "Locations": [ + { + "StartLine": 475, + "EndLine": 478 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "syn@0.15.34", + "Name": "syn", + "Identifier": { + "PURL": "pkg:cargo/syn@0.15.34", + "UID": "82538c67fffb61c9" + }, + "Version": "0.15.34", + "DependsOn": [ + "proc-macro2@0.4.30", + "quote@0.6.12", + "unicode-xid@0.1.0" + ], + "Locations": [ + { + "StartLine": 480, + "EndLine": 488 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "tempdir@0.3.7", + "Name": "tempdir", + "Identifier": { + "PURL": "pkg:cargo/tempdir@0.3.7", + "UID": "804103e64391581a" + }, + "Version": "0.3.7", + "DependsOn": [ + "rand@0.4.6", + "remove_dir_all@0.5.1" + ], + "Locations": [ + { + "StartLine": 490, + "EndLine": 497 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "tendril@0.4.1", + "Name": "tendril", + "Identifier": { + "PURL": "pkg:cargo/tendril@0.4.1", + "UID": "e4ba83802c3d449" + }, + "Version": "0.4.1", + "DependsOn": [ + "futf@0.1.4", + "mac@0.1.1", + "utf-8@0.7.5" + ], + "Locations": [ + { + "StartLine": 499, + "EndLine": 507 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "unicode-bidi@0.3.4", + "Name": "unicode-bidi", + "Identifier": { + "PURL": "pkg:cargo/unicode-bidi@0.3.4", + "UID": "403a21fc8938ad22" + }, + "Version": "0.3.4", + "DependsOn": [ + "matches@0.1.8" + ], + "Locations": [ + { + "StartLine": 509, + "EndLine": 515 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "unicode-normalization@0.1.8", + "Name": "unicode-normalization", + "Identifier": { + "PURL": "pkg:cargo/unicode-normalization@0.1.8", + "UID": "f0b9269ef289b9a0" + }, + "Version": "0.1.8", + "DependsOn": [ + "smallvec@0.6.9" + ], + "Locations": [ + { + "StartLine": 517, + "EndLine": 523 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "unicode-xid@0.1.0", + "Name": "unicode-xid", + "Identifier": { + "PURL": "pkg:cargo/unicode-xid@0.1.0", + "UID": "4d23448b5e26381c" + }, + "Version": "0.1.0", + "Locations": [ + { + "StartLine": 525, + "EndLine": 528 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "url@1.7.2", + "Name": "url", + "Identifier": { + "PURL": "pkg:cargo/url@1.7.2", + "UID": "5fe85390802c59c9" + }, + "Version": "1.7.2", + "DependsOn": [ + "idna@0.1.5", + "matches@0.1.8", + "percent-encoding@1.0.1" + ], + "Locations": [ + { + "StartLine": 530, + "EndLine": 538 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "user32-sys@0.2.0", + "Name": "user32-sys", + "Identifier": { + "PURL": "pkg:cargo/user32-sys@0.2.0", + "UID": "b87e0b467488f75" + }, + "Version": "0.2.0", + "DependsOn": [ + "winapi-build@0.1.1", + "winapi@0.2.8" + ], + "Locations": [ + { + "StartLine": 540, + "EndLine": 547 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "utf-8@0.7.5", + "Name": "utf-8", + "Identifier": { + "PURL": "pkg:cargo/utf-8@0.7.5", + "UID": "a010f7107833ac4f" + }, + "Version": "0.7.5", + "Locations": [ + { + "StartLine": 549, + "EndLine": 552 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "walkdir@1.0.7", + "Name": "walkdir", + "Identifier": { + "PURL": "pkg:cargo/walkdir@1.0.7", + "UID": "5f24031f7c182709" + }, + "Version": "1.0.7", + "DependsOn": [ + "kernel32-sys@0.2.2", + "same-file@0.1.3", + "winapi@0.2.8" + ], + "Locations": [ + { + "StartLine": 554, + "EndLine": 562 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "winapi@0.2.8", + "Name": "winapi", + "Identifier": { + "PURL": "pkg:cargo/winapi@0.2.8", + "UID": "11a6ab5a8cb502c1" + }, + "Version": "0.2.8", + "Locations": [ + { + "StartLine": 564, + "EndLine": 567 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "winapi@0.3.7", + "Name": "winapi", + "Identifier": { + "PURL": "pkg:cargo/winapi@0.3.7", + "UID": "9a608b202d62cd74" + }, + "Version": "0.3.7", + "DependsOn": [ + "winapi-i686-pc-windows-gnu@0.4.0", + "winapi-x86_64-pc-windows-gnu@0.4.0" + ], + "Locations": [ + { + "StartLine": 569, + "EndLine": 576 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "winapi-build@0.1.1", + "Name": "winapi-build", + "Identifier": { + "PURL": "pkg:cargo/winapi-build@0.1.1", + "UID": "76d61c1f4e44b85d" + }, + "Version": "0.1.1", + "Locations": [ + { + "StartLine": 578, + "EndLine": 581 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "winapi-i686-pc-windows-gnu@0.4.0", + "Name": "winapi-i686-pc-windows-gnu", + "Identifier": { + "PURL": "pkg:cargo/winapi-i686-pc-windows-gnu@0.4.0", + "UID": "c5672bc13d29308e" + }, + "Version": "0.4.0", + "Locations": [ + { + "StartLine": 583, + "EndLine": 586 + } + ], + "AnalyzedBy": "cargo" + }, + { + "ID": "winapi-x86_64-pc-windows-gnu@0.4.0", + "Name": "winapi-x86_64-pc-windows-gnu", + "Identifier": { + "PURL": "pkg:cargo/winapi-x86_64-pc-windows-gnu@0.4.0", + "UID": "bae2e882935f8973" + }, + "Version": "0.4.0", + "Locations": [ + { + "StartLine": 588, + "EndLine": 591 + } + ], + "AnalyzedBy": "cargo" + } + ] + }, + { + "Type": "composer", + "FilePath": "php-app/composer.lock", + "Packages": [ + { + "ID": "guzzlehttp/guzzle@6.2.0", + "Name": "guzzlehttp/guzzle", + "Identifier": { + "PURL": "pkg:composer/guzzlehttp/guzzle@6.2.0", + "UID": "ac39d14e87a3f1d4" + }, + "Version": "6.2.0", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "guzzlehttp/promises@v1.3.1", + "guzzlehttp/psr7@1.5.2" + ], + "Locations": [ + { + "StartLine": 9, + "EndLine": 73 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "guzzlehttp/promises@v1.3.1", + "Name": "guzzlehttp/promises", + "Identifier": { + "PURL": "pkg:composer/guzzlehttp/promises@v1.3.1", + "UID": "36737ccf260db328" + }, + "Version": "v1.3.1", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 74, + "EndLine": 124 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "guzzlehttp/psr7@1.5.2", + "Name": "guzzlehttp/psr7", + "Identifier": { + "PURL": "pkg:composer/guzzlehttp/psr7@1.5.2", + "UID": "119ab6de62c844c5" + }, + "Version": "1.5.2", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "psr/http-message@1.0.1", + "ralouphie/getallheaders@2.0.5" + ], + "Locations": [ + { + "StartLine": 125, + "EndLine": 191 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "laravel/installer@v2.0.1", + "Name": "laravel/installer", + "Identifier": { + "PURL": "pkg:composer/laravel/installer@v2.0.1", + "UID": "b41ed8667187a7e5" + }, + "Version": "v2.0.1", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "guzzlehttp/guzzle@6.2.0", + "symfony/console@v4.2.7", + "symfony/filesystem@v4.2.7", + "symfony/process@v4.2.7" + ], + "Locations": [ + { + "StartLine": 192, + "EndLine": 237 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "pear/log@1.13.1", + "Name": "pear/log", + "Identifier": { + "PURL": "pkg:composer/pear/log@1.13.1", + "UID": "b0cd4defb5f26c95" + }, + "Version": "1.13.1", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "pear/pear_exception@v1.0.0" + ], + "Locations": [ + { + "StartLine": 238, + "EndLine": 290 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "pear/pear_exception@v1.0.0", + "Name": "pear/pear_exception", + "Identifier": { + "PURL": "pkg:composer/pear/pear_exception@v1.0.0", + "UID": "dea61352fd729a53" + }, + "Version": "v1.0.0", + "Licenses": [ + "BSD-2-Clause" + ], + "Locations": [ + { + "StartLine": 291, + "EndLine": 345 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "psr/http-message@1.0.1", + "Name": "psr/http-message", + "Identifier": { + "PURL": "pkg:composer/psr/http-message@1.0.1", + "UID": "6d2dcda1cba472a2" + }, + "Version": "1.0.1", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 346, + "EndLine": 395 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "ralouphie/getallheaders@2.0.5", + "Name": "ralouphie/getallheaders", + "Identifier": { + "PURL": "pkg:composer/ralouphie/getallheaders@2.0.5", + "UID": "8224e8d4f17efdec" + }, + "Version": "2.0.5", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 396, + "EndLine": 435 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "symfony/console@v4.2.7", + "Name": "symfony/console", + "Identifier": { + "PURL": "pkg:composer/symfony/console@v4.2.7", + "UID": "e9cf12709469792a" + }, + "Version": "v4.2.7", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "symfony/contracts@v1.0.2", + "symfony/polyfill-mbstring@v1.11.0" + ], + "Locations": [ + { + "StartLine": 436, + "EndLine": 507 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "symfony/contracts@v1.0.2", + "Name": "symfony/contracts", + "Identifier": { + "PURL": "pkg:composer/symfony/contracts@v1.0.2", + "UID": "1e2f183e4752d9ad" + }, + "Version": "v1.0.2", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 508, + "EndLine": 575 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "symfony/filesystem@v4.2.7", + "Name": "symfony/filesystem", + "Identifier": { + "PURL": "pkg:composer/symfony/filesystem@v4.2.7", + "UID": "3448bf72b1395572" + }, + "Version": "v4.2.7", + "Licenses": [ + "MIT" + ], + "DependsOn": [ + "symfony/polyfill-ctype@v1.11.0" + ], + "Locations": [ + { + "StartLine": 576, + "EndLine": 625 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "symfony/polyfill-ctype@v1.11.0", + "Name": "symfony/polyfill-ctype", + "Identifier": { + "PURL": "pkg:composer/symfony/polyfill-ctype@v1.11.0", + "UID": "3ada0aa6437c75ac" + }, + "Version": "v1.11.0", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 626, + "EndLine": 683 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "symfony/polyfill-mbstring@v1.11.0", + "Name": "symfony/polyfill-mbstring", + "Identifier": { + "PURL": "pkg:composer/symfony/polyfill-mbstring@v1.11.0", + "UID": "2cc2563d9123b17d" + }, + "Version": "v1.11.0", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 684, + "EndLine": 742 + } + ], + "AnalyzedBy": "composer" + }, + { + "ID": "symfony/process@v4.2.7", + "Name": "symfony/process", + "Identifier": { + "PURL": "pkg:composer/symfony/process@v4.2.7", + "UID": "8daf5612abf52b" + }, + "Version": "v4.2.7", + "Licenses": [ + "MIT" + ], + "Locations": [ + { + "StartLine": 743, + "EndLine": 791 + } + ], + "AnalyzedBy": "composer" + } + ] + }, + { + "Type": "npm", + "FilePath": "node-app/package-lock.json", + "Packages": [ + { + "ID": "asap@2.0.6", + "Name": "asap", + "Identifier": { + "PURL": "pkg:npm/asap@2.0.6", + "UID": "e762d7c1b1951ae8" + }, + "Version": "2.0.6", + "Locations": [ + { + "StartLine": 6, + "EndLine": 10 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "jquery@3.3.9", + "Name": "jquery", + "Identifier": { + "PURL": "pkg:npm/jquery@3.3.9", + "UID": "b7401f5201d8b2a5" + }, + "Version": "3.3.9", + "Locations": [ + { + "StartLine": 16, + "EndLine": 20 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "js-tokens@4.0.0", + "Name": "js-tokens", + "Identifier": { + "PURL": "pkg:npm/js-tokens@4.0.0", + "UID": "7a7cba6ecea81c6b" + }, + "Version": "4.0.0", + "Locations": [ + { + "StartLine": 21, + "EndLine": 25 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "lodash@4.17.4", + "Name": "lodash", + "Identifier": { + "PURL": "pkg:npm/lodash@4.17.4", + "UID": "57b667cef554709e" + }, + "Version": "4.17.4", + "Locations": [ + { + "StartLine": 11, + "EndLine": 15 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "loose-envify@1.4.0", + "Name": "loose-envify", + "Identifier": { + "PURL": "pkg:npm/loose-envify@1.4.0", + "UID": "28d76414d7949e7a" + }, + "Version": "1.4.0", + "DependsOn": [ + "js-tokens@4.0.0" + ], + "Locations": [ + { + "StartLine": 26, + "EndLine": 33 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "object-assign@4.1.1", + "Name": "object-assign", + "Identifier": { + "PURL": "pkg:npm/object-assign@4.1.1", + "UID": "9f57c5934ef7336a" + }, + "Version": "4.1.1", + "Locations": [ + { + "StartLine": 34, + "EndLine": 38 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "promise@8.0.3", + "Name": "promise", + "Identifier": { + "PURL": "pkg:npm/promise@8.0.3", + "UID": "a6ccee23123010ec" + }, + "Version": "8.0.3", + "DependsOn": [ + "asap@2.0.6" + ], + "Locations": [ + { + "StartLine": 39, + "EndLine": 46 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "prop-types@15.7.2", + "Name": "prop-types", + "Identifier": { + "PURL": "pkg:npm/prop-types@15.7.2", + "UID": "ee13953e01ee65fa" + }, + "Version": "15.7.2", + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "react-is@16.8.6" + ], + "Locations": [ + { + "StartLine": 47, + "EndLine": 56 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "react@16.8.6", + "Name": "react", + "Identifier": { + "PURL": "pkg:npm/react@16.8.6", + "UID": "6cbae25492e380b" + }, + "Version": "16.8.6", + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1", + "prop-types@15.7.2", + "scheduler@0.13.6" + ], + "Locations": [ + { + "StartLine": 57, + "EndLine": 67 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "react-is@16.8.6", + "Name": "react-is", + "Identifier": { + "PURL": "pkg:npm/react-is@16.8.6", + "UID": "7de7d9d88046153f" + }, + "Version": "16.8.6", + "Locations": [ + { + "StartLine": 68, + "EndLine": 72 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "redux@4.0.1", + "Name": "redux", + "Identifier": { + "PURL": "pkg:npm/redux@4.0.1", + "UID": "44df29ef3a103eea" + }, + "Version": "4.0.1", + "DependsOn": [ + "loose-envify@1.4.0", + "symbol-observable@1.2.0" + ], + "Locations": [ + { + "StartLine": 73, + "EndLine": 81 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "scheduler@0.13.6", + "Name": "scheduler", + "Identifier": { + "PURL": "pkg:npm/scheduler@0.13.6", + "UID": "e24aba513da769d8" + }, + "Version": "0.13.6", + "DependsOn": [ + "loose-envify@1.4.0", + "object-assign@4.1.1" + ], + "Locations": [ + { + "StartLine": 82, + "EndLine": 90 + } + ], + "AnalyzedBy": "npm" + }, + { + "ID": "symbol-observable@1.2.0", + "Name": "symbol-observable", + "Identifier": { + "PURL": "pkg:npm/symbol-observable@1.2.0", + "UID": "98ca0c09e26aeac2" + }, + "Version": "1.2.0", + "Locations": [ + { + "StartLine": 91, + "EndLine": 95 + } + ], + "AnalyzedBy": "npm" + } + ] + }, + { + "Type": "pipenv", + "FilePath": "python-app/Pipfile.lock", + "Packages": [ + { + "Name": "amqp", + "Identifier": { + "PURL": "pkg:pypi/amqp@2.4.2", + "UID": "9baf4ce77c407941" + }, + "Version": "2.4.2", + "Locations": [ + { + "StartLine": 19, + "EndLine": 25 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "autopep8", + "Identifier": { + "PURL": "pkg:pypi/autopep8@1.4.3", + "UID": "1a2da9b87c95d2d4" + }, + "Version": "1.4.3", + "Locations": [ + { + "StartLine": 26, + "EndLine": 31 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "babel", + "Identifier": { + "PURL": "pkg:pypi/babel@2.6.0", + "UID": "8c7022376c5941e3" + }, + "Version": "2.6.0", + "Locations": [ + { + "StartLine": 32, + "EndLine": 38 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "billiard", + "Identifier": { + "PURL": "pkg:pypi/billiard@3.6.0.0", + "UID": "4fea47802ce3931d" + }, + "Version": "3.6.0.0", + "Locations": [ + { + "StartLine": 39, + "EndLine": 44 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "boto3", + "Identifier": { + "PURL": "pkg:pypi/boto3@1.9.130", + "UID": "fa6a31406750e2d9" + }, + "Version": "1.9.130", + "Locations": [ + { + "StartLine": 45, + "EndLine": 52 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "botocore", + "Identifier": { + "PURL": "pkg:pypi/botocore@1.12.130", + "UID": "b7ce7380a5c94442" + }, + "Version": "1.12.130", + "Locations": [ + { + "StartLine": 53, + "EndLine": 59 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "celery", + "Identifier": { + "PURL": "pkg:pypi/celery@4.3.0", + "UID": "5b8b567d1334ddf9" + }, + "Version": "4.3.0", + "Locations": [ + { + "StartLine": 60, + "EndLine": 70 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "certifi", + "Identifier": { + "PURL": "pkg:pypi/certifi@2019.3.9", + "UID": "72be2a08a90a6b38" + }, + "Version": "2019.3.9", + "Locations": [ + { + "StartLine": 71, + "EndLine": 77 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "chardet", + "Identifier": { + "PURL": "pkg:pypi/chardet@3.0.4", + "UID": "dd48e213a44b6a6a" + }, + "Version": "3.0.4", + "Locations": [ + { + "StartLine": 78, + "EndLine": 84 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "decorator", + "Identifier": { + "PURL": "pkg:pypi/decorator@4.4.0", + "UID": "df0c4dcd57d294f2" + }, + "Version": "4.4.0", + "Locations": [ + { + "StartLine": 85, + "EndLine": 91 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "django", + "Identifier": { + "PURL": "pkg:pypi/django@2.0.9", + "UID": "e86cef9dd44263e3" + }, + "Version": "2.0.9", + "Locations": [ + { + "StartLine": 92, + "EndLine": 99 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "django-celery-beat", + "Identifier": { + "PURL": "pkg:pypi/django-celery-beat@1.4.0", + "UID": "edf3c07b5af6e42b" + }, + "Version": "1.4.0", + "Locations": [ + { + "StartLine": 100, + "EndLine": 107 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "django-cors-headers", + "Identifier": { + "PURL": "pkg:pypi/django-cors-headers@2.5.2", + "UID": "8b73fee78deec86c" + }, + "Version": "2.5.2", + "Locations": [ + { + "StartLine": 108, + "EndLine": 115 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "django-extensions", + "Identifier": { + "PURL": "pkg:pypi/django-extensions@2.1.6", + "UID": "62ad8a951fac1ab0" + }, + "Version": "2.1.6", + "Locations": [ + { + "StartLine": 116, + "EndLine": 123 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "django-postgres-extra", + "Identifier": { + "PURL": "pkg:pypi/django-postgres-extra", + "UID": "12ca123a26ca8d5e" + }, + "Locations": [ + { + "StartLine": 124, + "EndLine": 128 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "django-redis-cache", + "Identifier": { + "PURL": "pkg:pypi/django-redis-cache@2.0.0", + "UID": "141e09abf66456fe" + }, + "Version": "2.0.0", + "Locations": [ + { + "StartLine": 129, + "EndLine": 135 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "django-silk", + "Identifier": { + "PURL": "pkg:pypi/django-silk@3.0.1", + "UID": "d734f07fba2dba73" + }, + "Version": "3.0.1", + "Locations": [ + { + "StartLine": 136, + "EndLine": 143 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "django-timezone-field", + "Identifier": { + "PURL": "pkg:pypi/django-timezone-field@3.0", + "UID": "569e7ef488355353" + }, + "Version": "3.0", + "Locations": [ + { + "StartLine": 144, + "EndLine": 150 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "djangorestframework", + "Identifier": { + "PURL": "pkg:pypi/djangorestframework@3.9.2", + "UID": "adcfc3371eb703f4" + }, + "Version": "3.9.2", + "Locations": [ + { + "StartLine": 151, + "EndLine": 158 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "djangorestframework-jwt", + "Identifier": { + "PURL": "pkg:pypi/djangorestframework-jwt@1.11.0", + "UID": "d80fe562af4286d0" + }, + "Version": "1.11.0", + "Locations": [ + { + "StartLine": 159, + "EndLine": 166 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "docutils", + "Identifier": { + "PURL": "pkg:pypi/docutils@0.14", + "UID": "91d6485f173d6173" + }, + "Version": "0.14", + "Locations": [ + { + "StartLine": 167, + "EndLine": 174 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "flower", + "Identifier": { + "PURL": "pkg:pypi/flower@0.9.3", + "UID": "32a7c87f79b0839" + }, + "Version": "0.9.3", + "Locations": [ + { + "StartLine": 175, + "EndLine": 181 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "gprof2dot", + "Identifier": { + "PURL": "pkg:pypi/gprof2dot@2016.10.13", + "UID": "b4b8f2af48ce1580" + }, + "Version": "2016.10.13", + "Locations": [ + { + "StartLine": 182, + "EndLine": 187 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "gunicorn", + "Identifier": { + "PURL": "pkg:pypi/gunicorn@19.9.0", + "UID": "5ededdc0bd0b919e" + }, + "Version": "19.9.0", + "Locations": [ + { + "StartLine": 188, + "EndLine": 195 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "hiredis", + "Identifier": { + "PURL": "pkg:pypi/hiredis@1.0.0", + "UID": "8888e27b4cd1f811" + }, + "Version": "1.0.0", + "Locations": [ + { + "StartLine": 196, + "EndLine": 229 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "httplib2", + "Identifier": { + "PURL": "pkg:pypi/httplib2@0.12.1", + "UID": "d9ecad9f56ca17d6" + }, + "Version": "0.12.1", + "Locations": [ + { + "StartLine": 230, + "EndLine": 235 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "idna", + "Identifier": { + "PURL": "pkg:pypi/idna@2.8", + "UID": "25611d269b47ca02" + }, + "Version": "2.8", + "Locations": [ + { + "StartLine": 236, + "EndLine": 242 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "jinja2", + "Identifier": { + "PURL": "pkg:pypi/jinja2@2.10.1", + "UID": "79ef3c32974068b9" + }, + "Version": "2.10.1", + "Locations": [ + { + "StartLine": 243, + "EndLine": 249 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "jmespath", + "Identifier": { + "PURL": "pkg:pypi/jmespath@0.9.4", + "UID": "43914f696b4355e6" + }, + "Version": "0.9.4", + "Locations": [ + { + "StartLine": 250, + "EndLine": 256 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "kombu", + "Identifier": { + "PURL": "pkg:pypi/kombu@4.5.0", + "UID": "f48d0b2795397953" + }, + "Version": "4.5.0", + "Locations": [ + { + "StartLine": 257, + "EndLine": 263 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "markupsafe", + "Identifier": { + "PURL": "pkg:pypi/markupsafe@1.1.1", + "UID": "5cf2024e695d83e8" + }, + "Version": "1.1.1", + "Locations": [ + { + "StartLine": 264, + "EndLine": 296 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "oauth2", + "Identifier": { + "PURL": "pkg:pypi/oauth2@1.9.0.post1", + "UID": "19177dcd235262cc" + }, + "Version": "1.9.0.post1", + "Locations": [ + { + "StartLine": 297, + "EndLine": 304 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "psycopg2-binary", + "Identifier": { + "PURL": "pkg:pypi/psycopg2-binary@2.8.1", + "UID": "f138613f8be7afb1" + }, + "Version": "2.8.1", + "Locations": [ + { + "StartLine": 305, + "EndLine": 338 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "py", + "Identifier": { + "PURL": "pkg:pypi/py@1.8.0", + "UID": "6b0a3e00c69db213" + }, + "Version": "1.8.0", + "Locations": [ + { + "StartLine": 339, + "EndLine": 345 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "pycodestyle", + "Identifier": { + "PURL": "pkg:pypi/pycodestyle@2.5.0", + "UID": "73b8e99f43005c36" + }, + "Version": "2.5.0", + "Locations": [ + { + "StartLine": 346, + "EndLine": 352 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "pycurl", + "Identifier": { + "PURL": "pkg:pypi/pycurl@7.43.0.2", + "UID": "72315985f01ce227" + }, + "Version": "7.43.0.2", + "Locations": [ + { + "StartLine": 353, + "EndLine": 365 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "pygments", + "Identifier": { + "PURL": "pkg:pypi/pygments@2.3.1", + "UID": "c1cb03a99767c2e3" + }, + "Version": "2.3.1", + "Locations": [ + { + "StartLine": 366, + "EndLine": 372 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "pyjwt", + "Identifier": { + "PURL": "pkg:pypi/pyjwt@1.7.1", + "UID": "257142733bd0e747" + }, + "Version": "1.7.1", + "Locations": [ + { + "StartLine": 373, + "EndLine": 379 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "python-crontab", + "Identifier": { + "PURL": "pkg:pypi/python-crontab@2.3.6", + "UID": "45b728c96c1645cd" + }, + "Version": "2.3.6", + "Locations": [ + { + "StartLine": 380, + "EndLine": 385 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "python-dateutil", + "Identifier": { + "PURL": "pkg:pypi/python-dateutil@2.8.0", + "UID": "d252731fceb6a9c7" + }, + "Version": "2.8.0", + "Locations": [ + { + "StartLine": 386, + "EndLine": 393 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "python-http-client", + "Identifier": { + "PURL": "pkg:pypi/python-http-client@3.1.0", + "UID": "a9784b1b916ad3b6" + }, + "Version": "3.1.0", + "Locations": [ + { + "StartLine": 394, + "EndLine": 399 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "pytz", + "Identifier": { + "PURL": "pkg:pypi/pytz@2019.1", + "UID": "b8c0d0ffedda0d09" + }, + "Version": "2019.1", + "Locations": [ + { + "StartLine": 400, + "EndLine": 406 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "pyyaml", + "Identifier": { + "PURL": "pkg:pypi/pyyaml@5.1", + "UID": "4e4bd3b3d22ba69a" + }, + "Version": "5.1", + "Locations": [ + { + "StartLine": 407, + "EndLine": 423 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "redis", + "Identifier": { + "PURL": "pkg:pypi/redis@3.2.1", + "UID": "c2bbc785e877a9b6" + }, + "Version": "3.2.1", + "Locations": [ + { + "StartLine": 424, + "EndLine": 430 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "requests", + "Identifier": { + "PURL": "pkg:pypi/requests@2.21.0", + "UID": "b6db077c02e6131a" + }, + "Version": "2.21.0", + "Locations": [ + { + "StartLine": 431, + "EndLine": 438 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "retry", + "Identifier": { + "PURL": "pkg:pypi/retry@0.9.2", + "UID": "c4d0a6dc950b0844" + }, + "Version": "0.9.2", + "Locations": [ + { + "StartLine": 439, + "EndLine": 446 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "s3transfer", + "Identifier": { + "PURL": "pkg:pypi/s3transfer@0.2.0", + "UID": "adcb5d625aeb94b4" + }, + "Version": "0.2.0", + "Locations": [ + { + "StartLine": 447, + "EndLine": 453 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "sendgrid", + "Identifier": { + "PURL": "pkg:pypi/sendgrid@6.0.4", + "UID": "198b2829df6c3727" + }, + "Version": "6.0.4", + "Locations": [ + { + "StartLine": 454, + "EndLine": 461 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "sentry-sdk", + "Identifier": { + "PURL": "pkg:pypi/sentry-sdk@0.7.10", + "UID": "491f6048848fa097" + }, + "Version": "0.7.10", + "Locations": [ + { + "StartLine": 462, + "EndLine": 469 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "six", + "Identifier": { + "PURL": "pkg:pypi/six@1.12.0", + "UID": "bdded6c8ee5bfaa7" + }, + "Version": "1.12.0", + "Locations": [ + { + "StartLine": 470, + "EndLine": 476 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "sqlparse", + "Identifier": { + "PURL": "pkg:pypi/sqlparse@0.3.0", + "UID": "e883750010b639d3" + }, + "Version": "0.3.0", + "Locations": [ + { + "StartLine": 477, + "EndLine": 483 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "tornado", + "Identifier": { + "PURL": "pkg:pypi/tornado@5.1.1", + "UID": "244851c9871a8ea" + }, + "Version": "5.1.1", + "Locations": [ + { + "StartLine": 484, + "EndLine": 495 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "urllib3", + "Identifier": { + "PURL": "pkg:pypi/urllib3@1.24.1", + "UID": "9c45cb2cc68843ed" + }, + "Version": "1.24.1", + "Locations": [ + { + "StartLine": 496, + "EndLine": 503 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "vine", + "Identifier": { + "PURL": "pkg:pypi/vine@1.3.0", + "UID": "2f2618598d946d5b" + }, + "Version": "1.3.0", + "Locations": [ + { + "StartLine": 504, + "EndLine": 510 + } + ], + "AnalyzedBy": "pipenv" + }, + { + "Name": "xmltodict", + "Identifier": { + "PURL": "pkg:pypi/xmltodict@0.12.0", + "UID": "654fc5d352f80771" + }, + "Version": "0.12.0", + "Locations": [ + { + "StartLine": 511, + "EndLine": 518 + } + ], + "AnalyzedBy": "pipenv" + } + ] + } +] \ No newline at end of file diff --git a/pkg/fanal/test/integration/testdata/goldens/vuln-image1.2.3.expectedpkgsfromcmds.golden b/pkg/fanal/test/integration/testdata/goldens/vuln-image1.2.3.expectedpkgsfromcmds.golden new file mode 100644 index 000000000000..7778a04a737d --- /dev/null +++ b/pkg/fanal/test/integration/testdata/goldens/vuln-image1.2.3.expectedpkgsfromcmds.golden @@ -0,0 +1,583 @@ +[ + { + "Name": "acl", + "Identifier": { + "UID": "181b417cffad616f" + }, + "Version": "2.2.52-r3" + }, + { + "Name": "apr", + "Identifier": { + "UID": "5083f2ffc2b7a814" + }, + "Version": "1.6.3-r0" + }, + { + "Name": "apr-util", + "Identifier": { + "UID": "1e0af1c3510210ba" + }, + "Version": "1.6.1-r1" + }, + { + "Name": "attr", + "Identifier": { + "UID": "a47bcc9298df6cb9" + }, + "Version": "2.4.47-r6" + }, + { + "Name": "autoconf", + "Identifier": { + "UID": "3c70caeaed9a6ff9" + }, + "Version": "2.69-r0" + }, + { + "Name": "bash", + "Identifier": { + "UID": "b9623518df2580d7" + }, + "Version": "4.4.19-r1" + }, + { + "Name": "binutils", + "Identifier": { + "UID": "9c6d3cbf28294d8c" + }, + "Version": "2.30-r1" + }, + { + "Name": "binutils-libs", + "Identifier": { + "UID": "bd787142d4ac226b" + }, + "Version": "2.30-r1" + }, + { + "Name": "busybox", + "Identifier": { + "UID": "7d2e7c1078ba7eb" + }, + "Version": "1.27.2-r11" + }, + { + "Name": "bzip2", + "Identifier": { + "UID": "f10a7652e98de81" + }, + "Version": "1.0.6-r6" + }, + { + "Name": "ca-certificates", + "Identifier": { + "UID": "15130b963760c251" + }, + "Version": "20171114-r0" + }, + { + "Name": "coreutils", + "Identifier": { + "UID": "62b3a9f524fb42ae" + }, + "Version": "8.28-r0" + }, + { + "Name": "cyrus-sasl", + "Identifier": { + "UID": "3d54d810df71de08" + }, + "Version": "2.1.26-r11" + }, + { + "Name": "db", + "Identifier": { + "UID": "c516bbf8a0460592" + }, + "Version": "5.3.28-r0" + }, + { + "Name": "dpkg", + "Identifier": { + "UID": "48abb525a6e6484c" + }, + "Version": "1.18.24-r0" + }, + { + "Name": "dpkg-dev", + "Identifier": { + "UID": "751199098a59e38a" + }, + "Version": "1.18.24-r0" + }, + { + "Name": "expat", + "Identifier": { + "UID": "39fb7474be7cbbd1" + }, + "Version": "2.2.5-r0" + }, + { + "Name": "file", + "Identifier": { + "UID": "fce3c3b5b4ca8c61" + }, + "Version": "5.32-r0" + }, + { + "Name": "g++", + "Identifier": { + "UID": "e38d4fbb4801e54" + }, + "Version": "6.4.0-r5" + }, + { + "Name": "gcc", + "Identifier": { + "UID": "7e7a1343cbee2437" + }, + "Version": "6.4.0-r5" + }, + { + "Name": "gdbm", + "Identifier": { + "UID": "ddf8257d2b4ffc7b" + }, + "Version": "1.13-r1" + }, + { + "Name": "gmp", + "Identifier": { + "UID": "9543ab8b3ef71c6b" + }, + "Version": "6.1.2-r1" + }, + { + "Name": "gnupg", + "Identifier": { + "UID": "dc05954810cd6512" + }, + "Version": "2.2.3-r1" + }, + { + "Name": "gnutls", + "Identifier": { + "UID": "3895e3c8d3c4eec5" + }, + "Version": "3.6.1-r0" + }, + { + "Name": "isl", + "Identifier": { + "UID": "ff5808fa3be09223" + }, + "Version": "0.18-r0" + }, + { + "Name": "libacl", + "Identifier": { + "UID": "8496f22e32d90dd9" + }, + "Version": "2.2.52-r3" + }, + { + "Name": "libassuan", + "Identifier": { + "UID": "7c44d43ad91014bb" + }, + "Version": "2.4.4-r0" + }, + { + "Name": "libatomic", + "Identifier": { + "UID": "cc7cbb7bdeaceb7a" + }, + "Version": "6.4.0-r5" + }, + { + "Name": "libattr", + "Identifier": { + "UID": "6ab5fd8ad7ea3579" + }, + "Version": "2.4.47-r6" + }, + { + "Name": "libbz2", + "Identifier": { + "UID": "b88167f64940af66" + }, + "Version": "1.0.6-r6" + }, + { + "Name": "libc-dev", + "Identifier": { + "UID": "804732077a4c662b" + }, + "Version": "0.7.1-r0" + }, + { + "Name": "libcap", + "Identifier": { + "UID": "d0374637d7ee148" + }, + "Version": "2.25-r1" + }, + { + "Name": "libedit", + "Identifier": { + "UID": "a9e7bdfc780a5205" + }, + "Version": "20170329.3.1-r3" + }, + { + "Name": "libedit-dev", + "Identifier": { + "UID": "cc15a0075f19fb29" + }, + "Version": "20170329.3.1-r3" + }, + { + "Name": "libffi", + "Identifier": { + "UID": "2e7d2428b7208794" + }, + "Version": "3.2.1-r4" + }, + { + "Name": "libgcc", + "Identifier": { + "UID": "337db1c98d7a2b24" + }, + "Version": "6.4.0-r5" + }, + { + "Name": "libgcrypt", + "Identifier": { + "UID": "607b2546f0faa0dd" + }, + "Version": "1.8.3-r0" + }, + { + "Name": "libgomp", + "Identifier": { + "UID": "6405f9ce160ce36" + }, + "Version": "6.4.0-r5" + }, + { + "Name": "libgpg-error", + "Identifier": { + "UID": "2e7c4543143270ba" + }, + "Version": "1.27-r1" + }, + { + "Name": "libksba", + "Identifier": { + "UID": "2bdff3fcdb38fcc9" + }, + "Version": "1.3.5-r0" + }, + { + "Name": "libldap", + "Identifier": { + "UID": "9a1c31386d4c51d1" + }, + "Version": "2.4.45-r3" + }, + { + "Name": "libmagic", + "Identifier": { + "UID": "ce8c19b21901c2ec" + }, + "Version": "5.32-r0" + }, + { + "Name": "libressl", + "Identifier": { + "UID": "e0990fc64593fc34" + }, + "Version": "2.6.5-r0" + }, + { + "Name": "libressl-dev", + "Identifier": { + "UID": "2fc58a084fad510b" + }, + "Version": "2.6.5-r0" + }, + { + "Name": "libressl2.6-libcrypto", + "Identifier": { + "UID": "b9b01ba0fd3c2f96" + }, + "Version": "2.6.5-r0" + }, + { + "Name": "libressl2.6-libssl", + "Identifier": { + "UID": "7450166187e3c122" + }, + "Version": "2.6.5-r0" + }, + { + "Name": "libressl2.6-libtls", + "Identifier": { + "UID": "31e6fafea5aee605" + }, + "Version": "2.6.5-r0" + }, + { + "Name": "libsasl", + "Identifier": { + "UID": "a5d9eed6b200ec9e" + }, + "Version": "2.1.26-r11" + }, + { + "Name": "libsodium", + "Identifier": { + "UID": "d37655d3df6e7f60" + }, + "Version": "1.0.15-r0" + }, + { + "Name": "libsodium-dev", + "Identifier": { + "UID": "9ad59860b74f3bc9" + }, + "Version": "1.0.15-r0" + }, + { + "Name": "libstdc++", + "Identifier": { + "UID": "3cb038e2e3f8f2d3" + }, + "Version": "6.4.0-r5" + }, + { + "Name": "libtasn1", + "Identifier": { + "UID": "d64e086f11523544" + }, + "Version": "4.12-r3" + }, + { + "Name": "libunistring", + "Identifier": { + "UID": "f011d575a1de2df6" + }, + "Version": "0.9.7-r0" + }, + { + "Name": "m4", + "Identifier": { + "UID": "9a0327634e852d10" + }, + "Version": "1.4.18-r0" + }, + { + "Name": "make", + "Identifier": { + "UID": "5563a4c45ccc0ca6" + }, + "Version": "4.2.1-r0" + }, + { + "Name": "mercurial", + "Identifier": { + "UID": "5b844daeeb0ae32c" + }, + "Version": "4.5.2-r0" + }, + { + "Name": "mpc1", + "Identifier": { + "UID": "4ac00bb3c9d7b863" + }, + "Version": "1.0.3-r1" + }, + { + "Name": "mpfr3", + "Identifier": { + "UID": "7351997d7d25f69a" + }, + "Version": "3.1.5-r1" + }, + { + "Name": "musl", + "Identifier": { + "UID": "61c9bbf17ebf0ec5" + }, + "Version": "1.1.18-r3" + }, + { + "Name": "musl-dev", + "Identifier": { + "UID": "bddc3ce8e670295c" + }, + "Version": "1.1.18-r3" + }, + { + "Name": "ncurses", + "Identifier": { + "UID": "268c30a59b31f30f" + }, + "Version": "6.0_p20171125-r1" + }, + { + "Name": "ncurses-dev", + "Identifier": { + "UID": "35e387a2169b6c35" + }, + "Version": "6.0_p20171125-r1" + }, + { + "Name": "ncurses-libs", + "Identifier": { + "UID": "a698bd167c8edb63" + }, + "Version": "6.0_p20171125-r1" + }, + { + "Name": "ncurses-terminfo", + "Identifier": { + "UID": "1d756cb96659dfe8" + }, + "Version": "6.0_p20171125-r1" + }, + { + "Name": "ncurses-terminfo-base", + "Identifier": { + "UID": "70b90293a1ffd5c" + }, + "Version": "6.0_p20171125-r1" + }, + { + "Name": "nettle", + "Identifier": { + "UID": "2279fda8e0f37088" + }, + "Version": "3.3-r0" + }, + { + "Name": "npth", + "Identifier": { + "UID": "5d5e661f25ccd3bb" + }, + "Version": "1.5-r1" + }, + { + "Name": "openldap", + "Identifier": { + "UID": "f951698c7542567d" + }, + "Version": "2.4.45-r3" + }, + { + "Name": "p11-kit", + "Identifier": { + "UID": "f9a3b6ef16728be3" + }, + "Version": "0.23.2-r2" + }, + { + "Name": "patch", + "Identifier": { + "UID": "59097ece63a1532" + }, + "Version": "2.7.5-r2" + }, + { + "Name": "pcre2", + "Identifier": { + "UID": "46c51355357283bd" + }, + "Version": "10.30-r0" + }, + { + "Name": "pinentry", + "Identifier": { + "UID": "e0aa8991cc0d7ea9" + }, + "Version": "1.0.0-r0" + }, + { + "Name": "pkgconf", + "Identifier": { + "UID": "558f6d8317744a54" + }, + "Version": "1.3.10-r0" + }, + { + "Name": "python2", + "Identifier": { + "UID": "7e33eccd3ce9ae3f" + }, + "Version": "2.7.15-r2" + }, + { + "Name": "re2c", + "Identifier": { + "UID": "6dbcd72a6ade1945" + }, + "Version": "1.0.2-r0" + }, + { + "Name": "readline", + "Identifier": { + "UID": "c1cfb597544b76a5" + }, + "Version": "7.0.003-r0" + }, + { + "Name": "serf", + "Identifier": { + "UID": "94872f984b4a0583" + }, + "Version": "1.3.9-r3" + }, + { + "Name": "subversion", + "Identifier": { + "UID": "8fa2da2cba41429c" + }, + "Version": "1.9.7-r0" + }, + { + "Name": "subversion-libs", + "Identifier": { + "UID": "2591b779b8cc1ec5" + }, + "Version": "1.9.7-r0" + }, + { + "Name": "xz", + "Identifier": { + "UID": "74d9b4a853a25311" + }, + "Version": "5.2.3-r1" + }, + { + "Name": "xz-libs", + "Identifier": { + "UID": "e6072b890db87763" + }, + "Version": "5.2.3-r1" + }, + { + "Name": "zlib", + "Identifier": { + "UID": "b742ac29b1f34e" + }, + "Version": "1.2.11-r1" + }, + { + "Name": "zlib-dev", + "Identifier": { + "UID": "e9a8669a86602c9d" + }, + "Version": "1.2.11-r1" + } +] \ No newline at end of file diff --git a/pkg/fanal/test/testdata/alpine-310.tar.gz b/pkg/fanal/test/testdata/alpine-310.tar.gz new file mode 100644 index 000000000000..5a1750b24751 Binary files /dev/null and b/pkg/fanal/test/testdata/alpine-310.tar.gz differ diff --git a/pkg/fanal/test/testdata/alpine-311.tar.gz b/pkg/fanal/test/testdata/alpine-311.tar.gz new file mode 100644 index 000000000000..381d2bbb84dd Binary files /dev/null and b/pkg/fanal/test/testdata/alpine-311.tar.gz differ diff --git a/pkg/fanal/test/testdata/distroless.tar.gz b/pkg/fanal/test/testdata/distroless.tar.gz new file mode 100644 index 000000000000..594568b1e898 Binary files /dev/null and b/pkg/fanal/test/testdata/distroless.tar.gz differ diff --git a/pkg/fanal/test/testdata/image1.tar b/pkg/fanal/test/testdata/image1.tar new file mode 100644 index 000000000000..5e2aedf33636 Binary files /dev/null and b/pkg/fanal/test/testdata/image1.tar differ diff --git a/pkg/fanal/test/testdata/test.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab b/pkg/fanal/test/testdata/test.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab new file mode 100644 index 000000000000..8967e307376a --- /dev/null +++ b/pkg/fanal/test/testdata/test.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab @@ -0,0 +1 @@ +{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604","size":584},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c","size":2610}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 b/pkg/fanal/test/testdata/test.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 new file mode 100644 index 000000000000..1156328a5b74 --- /dev/null +++ b/pkg/fanal/test/testdata/test.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 @@ -0,0 +1 @@ +{"created":"2020-01-03T01:21:37.263809283Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"]},"rootfs":{"type":"layers","diff_ids":["sha256:9c27e219663c25e0f28493790cc0b88bc973ba3b1686355f221c38a36978ac63"]},"history":[{"created":"2020-01-03T01:21:37.132606296Z","created_by":"/bin/sh -c #(nop) COPY file:7bf12aab75c3867a023fe3b8bd6d113d43a4fcc415f3cc27cbcf0fff37b65a02 in / "},{"created":"2020-01-03T01:21:37.263809283Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c b/pkg/fanal/test/testdata/test.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c new file mode 100644 index 000000000000..097c1a433865 Binary files /dev/null and b/pkg/fanal/test/testdata/test.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c differ diff --git a/pkg/fanal/test/testdata/test.oci/index.json b/pkg/fanal/test/testdata/test.oci/index.json new file mode 100644 index 000000000000..f76e19c9274c --- /dev/null +++ b/pkg/fanal/test/testdata/test.oci/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/vnd.oci.image.manifest.v1+json","digest":"sha256:afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab","size":345}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test.oci/oci-layout b/pkg/fanal/test/testdata/test.oci/oci-layout new file mode 100644 index 000000000000..21b1439d1c6d --- /dev/null +++ b/pkg/fanal/test/testdata/test.oci/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_bad_index_json.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab b/pkg/fanal/test/testdata/test_bad_index_json.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab new file mode 100644 index 000000000000..8967e307376a --- /dev/null +++ b/pkg/fanal/test/testdata/test_bad_index_json.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab @@ -0,0 +1 @@ +{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604","size":584},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c","size":2610}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_bad_index_json.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 b/pkg/fanal/test/testdata/test_bad_index_json.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 new file mode 100644 index 000000000000..1156328a5b74 --- /dev/null +++ b/pkg/fanal/test/testdata/test_bad_index_json.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 @@ -0,0 +1 @@ +{"created":"2020-01-03T01:21:37.263809283Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"]},"rootfs":{"type":"layers","diff_ids":["sha256:9c27e219663c25e0f28493790cc0b88bc973ba3b1686355f221c38a36978ac63"]},"history":[{"created":"2020-01-03T01:21:37.132606296Z","created_by":"/bin/sh -c #(nop) COPY file:7bf12aab75c3867a023fe3b8bd6d113d43a4fcc415f3cc27cbcf0fff37b65a02 in / "},{"created":"2020-01-03T01:21:37.263809283Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_bad_index_json.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c b/pkg/fanal/test/testdata/test_bad_index_json.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c new file mode 100644 index 000000000000..097c1a433865 Binary files /dev/null and b/pkg/fanal/test/testdata/test_bad_index_json.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c differ diff --git a/pkg/fanal/test/testdata/test_bad_index_json.oci/index.json b/pkg/fanal/test/testdata/test_bad_index_json.oci/index.json new file mode 100644 index 000000000000..f6ea04951876 --- /dev/null +++ b/pkg/fanal/test/testdata/test_bad_index_json.oci/index.json @@ -0,0 +1 @@ +foobar \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_bad_index_json.oci/oci-layout b/pkg/fanal/test/testdata/test_bad_index_json.oci/oci-layout new file mode 100644 index 000000000000..21b1439d1c6d --- /dev/null +++ b/pkg/fanal/test/testdata/test_bad_index_json.oci/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_image_tag.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab b/pkg/fanal/test/testdata/test_image_tag.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab new file mode 100644 index 000000000000..8967e307376a --- /dev/null +++ b/pkg/fanal/test/testdata/test_image_tag.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab @@ -0,0 +1 @@ +{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604","size":584},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c","size":2610}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_image_tag.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 b/pkg/fanal/test/testdata/test_image_tag.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 new file mode 100644 index 000000000000..1156328a5b74 --- /dev/null +++ b/pkg/fanal/test/testdata/test_image_tag.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 @@ -0,0 +1 @@ +{"created":"2020-01-03T01:21:37.263809283Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"]},"rootfs":{"type":"layers","diff_ids":["sha256:9c27e219663c25e0f28493790cc0b88bc973ba3b1686355f221c38a36978ac63"]},"history":[{"created":"2020-01-03T01:21:37.132606296Z","created_by":"/bin/sh -c #(nop) COPY file:7bf12aab75c3867a023fe3b8bd6d113d43a4fcc415f3cc27cbcf0fff37b65a02 in / "},{"created":"2020-01-03T01:21:37.263809283Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_image_tag.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c b/pkg/fanal/test/testdata/test_image_tag.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c new file mode 100644 index 000000000000..097c1a433865 Binary files /dev/null and b/pkg/fanal/test/testdata/test_image_tag.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c differ diff --git a/pkg/fanal/test/testdata/test_image_tag.oci/index.json b/pkg/fanal/test/testdata/test_image_tag.oci/index.json new file mode 100644 index 000000000000..1655f80927d0 --- /dev/null +++ b/pkg/fanal/test/testdata/test_image_tag.oci/index.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 2, + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "digest": "sha256:afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab", + "size": 345, + "annotations": { + "org.opencontainers.image.ref.name": "0.0.1" + } + } + ] +} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_image_tag.oci/oci-layout b/pkg/fanal/test/testdata/test_image_tag.oci/oci-layout new file mode 100644 index 000000000000..21b1439d1c6d --- /dev/null +++ b/pkg/fanal/test/testdata/test_image_tag.oci/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab b/pkg/fanal/test/testdata/test_index_json_dir.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab new file mode 100644 index 000000000000..8967e307376a --- /dev/null +++ b/pkg/fanal/test/testdata/test_index_json_dir.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab @@ -0,0 +1 @@ +{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604","size":584},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c","size":2610}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 b/pkg/fanal/test/testdata/test_index_json_dir.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 new file mode 100644 index 000000000000..1156328a5b74 --- /dev/null +++ b/pkg/fanal/test/testdata/test_index_json_dir.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 @@ -0,0 +1 @@ +{"created":"2020-01-03T01:21:37.263809283Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"]},"rootfs":{"type":"layers","diff_ids":["sha256:9c27e219663c25e0f28493790cc0b88bc973ba3b1686355f221c38a36978ac63"]},"history":[{"created":"2020-01-03T01:21:37.132606296Z","created_by":"/bin/sh -c #(nop) COPY file:7bf12aab75c3867a023fe3b8bd6d113d43a4fcc415f3cc27cbcf0fff37b65a02 in / "},{"created":"2020-01-03T01:21:37.263809283Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c b/pkg/fanal/test/testdata/test_index_json_dir.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c new file mode 100644 index 000000000000..097c1a433865 Binary files /dev/null and b/pkg/fanal/test/testdata/test_index_json_dir.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c differ diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/index.json/.keep b/pkg/fanal/test/testdata/test_index_json_dir.oci/index.json/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/oci-layout b/pkg/fanal/test/testdata/test_index_json_dir.oci/oci-layout new file mode 100644 index 000000000000..21b1439d1c6d --- /dev/null +++ b/pkg/fanal/test/testdata/test_index_json_dir.oci/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab new file mode 100644 index 000000000000..8967e307376a --- /dev/null +++ b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab @@ -0,0 +1 @@ +{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604","size":584},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c","size":2610}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 new file mode 100644 index 000000000000..1156328a5b74 --- /dev/null +++ b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 @@ -0,0 +1 @@ +{"created":"2020-01-03T01:21:37.263809283Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"]},"rootfs":{"type":"layers","diff_ids":["sha256:9c27e219663c25e0f28493790cc0b88bc973ba3b1686355f221c38a36978ac63"]},"history":[{"created":"2020-01-03T01:21:37.132606296Z","created_by":"/bin/sh -c #(nop) COPY file:7bf12aab75c3867a023fe3b8bd6d113d43a4fcc415f3cc27cbcf0fff37b65a02 in / "},{"created":"2020-01-03T01:21:37.263809283Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c new file mode 100644 index 000000000000..097c1a433865 Binary files /dev/null and b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c differ diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/index.json b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/index.json new file mode 100644 index 000000000000..f6ea04951876 --- /dev/null +++ b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/index.json @@ -0,0 +1 @@ +foobar \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/oci-layout b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/oci-layout new file mode 100644 index 000000000000..21b1439d1c6d --- /dev/null +++ b/pkg/fanal/test/testdata/test_index_json_dir.oci/test_bad_index_json.oci/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_invalid_oci_image.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab new file mode 100644 index 000000000000..8967e307376a --- /dev/null +++ b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab @@ -0,0 +1 @@ +{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604","size":584},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c","size":2610}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_invalid_oci_image.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 new file mode 100644 index 000000000000..1156328a5b74 --- /dev/null +++ b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 @@ -0,0 +1 @@ +{"created":"2020-01-03T01:21:37.263809283Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"]},"rootfs":{"type":"layers","diff_ids":["sha256:9c27e219663c25e0f28493790cc0b88bc973ba3b1686355f221c38a36978ac63"]},"history":[{"created":"2020-01-03T01:21:37.132606296Z","created_by":"/bin/sh -c #(nop) COPY file:7bf12aab75c3867a023fe3b8bd6d113d43a4fcc415f3cc27cbcf0fff37b65a02 in / "},{"created":"2020-01-03T01:21:37.263809283Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_invalid_oci_image.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c new file mode 100644 index 000000000000..097c1a433865 Binary files /dev/null and b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c differ diff --git a/pkg/fanal/test/testdata/test_invalid_oci_image.oci/index.json b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/index.json new file mode 100644 index 000000000000..8fa56d9857ad --- /dev/null +++ b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/index.json @@ -0,0 +1 @@ +{"schemaVersion":2,"manifests":[{"mediaType":"application/badmedia+json","digest":"sha256:bad744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab","size":345}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_invalid_oci_image.oci/oci-layout b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/oci-layout new file mode 100644 index 000000000000..21b1439d1c6d --- /dev/null +++ b/pkg/fanal/test/testdata/test_invalid_oci_image.oci/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_no_valid_manifests.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab new file mode 100644 index 000000000000..8967e307376a --- /dev/null +++ b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/blobs/sha256/afb744871f99e0ff8e6f253244836ed34c5d805fdb096d3a205ffaf5e9073cab @@ -0,0 +1 @@ +{"schemaVersion":2,"config":{"mediaType":"application/vnd.oci.image.config.v1+json","digest":"sha256:b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604","size":584},"layers":[{"mediaType":"application/vnd.oci.image.layer.v1.tar+gzip","digest":"sha256:cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c","size":2610}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_no_valid_manifests.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 new file mode 100644 index 000000000000..1156328a5b74 --- /dev/null +++ b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/blobs/sha256/b23a8f6569ae9ae331226205fa72f480ce5310707d0bc97e611f83fbbbde4604 @@ -0,0 +1 @@ +{"created":"2020-01-03T01:21:37.263809283Z","architecture":"amd64","os":"linux","config":{"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/hello"]},"rootfs":{"type":"layers","diff_ids":["sha256:9c27e219663c25e0f28493790cc0b88bc973ba3b1686355f221c38a36978ac63"]},"history":[{"created":"2020-01-03T01:21:37.132606296Z","created_by":"/bin/sh -c #(nop) COPY file:7bf12aab75c3867a023fe3b8bd6d113d43a4fcc415f3cc27cbcf0fff37b65a02 in / "},{"created":"2020-01-03T01:21:37.263809283Z","created_by":"/bin/sh -c #(nop) CMD [\"/hello\"]","empty_layer":true}]} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_no_valid_manifests.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c new file mode 100644 index 000000000000..097c1a433865 Binary files /dev/null and b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/blobs/sha256/cdd16bd695eda2819e7637648f573c2ca64896c4f7bff9732ac9db734ca3bc2c differ diff --git a/pkg/fanal/test/testdata/test_no_valid_manifests.oci/index.json b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/index.json new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/index.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/test_no_valid_manifests.oci/oci-layout b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/oci-layout new file mode 100644 index 000000000000..21b1439d1c6d --- /dev/null +++ b/pkg/fanal/test/testdata/test_no_valid_manifests.oci/oci-layout @@ -0,0 +1 @@ +{"imageLayoutVersion": "1.0.0"} \ No newline at end of file diff --git a/pkg/fanal/test/testdata/vuln-image.tar.gz b/pkg/fanal/test/testdata/vuln-image.tar.gz new file mode 100644 index 000000000000..4603140c6469 Binary files /dev/null and b/pkg/fanal/test/testdata/vuln-image.tar.gz differ diff --git a/pkg/fanal/types/analyzer_type.go b/pkg/fanal/types/analyzer_type.go new file mode 100644 index 000000000000..c83f83c9d6ce --- /dev/null +++ b/pkg/fanal/types/analyzer_type.go @@ -0,0 +1,4 @@ +package types + +// Type represents the type of analyzer +type AnalyzerType string diff --git a/pkg/fanal/types/artifact.go b/pkg/fanal/types/artifact.go new file mode 100644 index 000000000000..bb721956fda9 --- /dev/null +++ b/pkg/fanal/types/artifact.go @@ -0,0 +1,288 @@ +package types + +import ( + "sort" + "time" + + "github.com/samber/lo" +) + +// ArtifactType represents a type of artifact +type ArtifactType string + +const ( + TypeContainerImage ArtifactType = "container_image" + TypeFilesystem ArtifactType = "filesystem" + TypeRepository ArtifactType = "repository" + TypeCycloneDX ArtifactType = "cyclonedx" + TypeSPDX ArtifactType = "spdx" + TypeAWSAccount ArtifactType = "aws_account" + TypeVM ArtifactType = "vm" +) + +type OS struct { + Family OSType + Name string + Eosl bool `json:"EOSL,omitempty"` + + // This field is used for enhanced security maintenance programs such as Ubuntu ESM, Debian Extended LTS. + Extended bool `json:"extended,omitempty"` +} + +func (o *OS) String() string { + s := string(o.Family) + if o.Name != "" { + s += "/" + o.Name + } + return s +} + +func (o *OS) Detected() bool { + return o.Family != "" +} + +// Normalize normalizes OS family names for backward compatibility +func (o *OS) Normalize() { + if alias, ok := OSTypeAliases[o.Family]; ok { + o.Family = alias + } +} + +// Merge merges OS version and enhanced security maintenance programs +func (o *OS) Merge(newOS OS) { + if lo.IsEmpty(newOS) { + return + } + + switch o.Family { + // OLE also has /etc/redhat-release and it detects OLE as RHEL by mistake. + // In that case, OS must be overwritten with the content of /etc/oracle-release. + // There is the same problem between Debian and Ubuntu. + case RedHat, Debian: + *o = newOS + default: + if o.Family == "" { + o.Family = newOS.Family + } + if o.Name == "" { + o.Name = newOS.Name + } + // Ubuntu has ESM program: https://ubuntu.com/security/esm + // OS version and esm status are stored in different files. + // We have to merge OS version after parsing these files. + if o.Extended || newOS.Extended { + o.Extended = true + } + } + // When merging layers, there are cases when a layer contains an OS with an old name: + // - Cache contains a layer derived from an old version of Trivy. + // - `client` uses an old version of Trivy, but `server` is a new version of Trivy (for `client/server` mode). + // So we need to normalize the OS name for backward compatibility. + o.Normalize() +} + +type Repository struct { + Family OSType `json:",omitempty"` + Release string `json:",omitempty"` +} + +type Layer struct { + Size int64 `json:",omitempty"` + Digest string `json:",omitempty"` + DiffID string `json:",omitempty"` + CreatedBy string `json:",omitempty"` +} + +type Layers []Layer + +func (lm Layers) TotalSize() int64 { + var totalSize int64 + for _, layer := range lm { + totalSize += layer.Size + } + return totalSize +} + +type PackageInfo struct { + FilePath string + Packages Packages +} + +type Application struct { + // e.g. bundler and pipenv + Type LangType + + // Lock files have the file path here, while each package metadata do not have + FilePath string `json:",omitempty"` + + // Packages is a list of lang-specific packages + Packages Packages +} + +type Applications []Application + +func (apps Applications) Len() int { + return len(apps) +} + +func (apps Applications) Swap(i, j int) { + apps[i], apps[j] = apps[j], apps[i] +} + +func (apps Applications) Less(i, j int) bool { + switch { + case apps[i].Type != apps[j].Type: + return apps[i].Type < apps[j].Type + case apps[i].FilePath != apps[j].FilePath: + return apps[i].FilePath < apps[j].FilePath + default: + return len(apps[i].Packages) < len(apps[j].Packages) + } +} + +type File struct { + Type string + Path string + Content []byte +} + +// ArtifactInfo is stored in cache +type ArtifactInfo struct { + SchemaVersion int + Architecture string + Created time.Time + DockerVersion string + OS string + + // Misconfiguration holds misconfiguration in container image config + Misconfiguration *Misconfiguration `json:",omitempty"` + + // Secret holds secrets in container image config such as environment variables + Secret *Secret `json:",omitempty"` + + // HistoryPackages are packages extracted from RUN instructions + HistoryPackages Packages `json:",omitempty"` +} + +// BlobInfo is stored in cache +type BlobInfo struct { + SchemaVersion int + + // Layer info + Size int64 `json:",omitempty"` + Digest string `json:",omitempty"` + DiffID string `json:",omitempty"` + CreatedBy string `json:",omitempty"` + OpaqueDirs []string `json:",omitempty"` + WhiteoutFiles []string `json:",omitempty"` + + // Analysis result + OS OS `json:",omitzero"` + Repository *Repository `json:",omitempty"` + PackageInfos []PackageInfo `json:",omitempty"` + Applications []Application `json:",omitempty"` + Misconfigurations []Misconfiguration `json:",omitempty"` + Secrets []Secret `json:",omitempty"` + Licenses []LicenseFile `json:",omitempty"` + + // Red Hat distributions have build info per layer. + // This information will be embedded into packages when applying layers. + // ref. https://redhat-connect.gitbook.io/partner-guide-for-adopting-red-hat-oval-v2/determining-common-platform-enumeration-cpe + BuildInfo *BuildInfo `json:",omitempty"` + + // CustomResources hold analysis results from custom analyzers. + // It is for extensibility and not used in OSS. + CustomResources []CustomResource `json:",omitempty"` +} + +func (b BlobInfo) Layer() Layer { + return Layer{ + Size: b.Size, + Digest: b.Digest, + DiffID: b.DiffID, + CreatedBy: b.CreatedBy, + } +} + +// ArtifactDetail represents the analysis result. +type ArtifactDetail struct { + OS OS `json:",omitzero"` + Repository *Repository `json:",omitempty"` + Packages Packages `json:",omitempty"` + Applications Applications `json:",omitempty"` + Misconfigurations []Misconfiguration `json:",omitempty"` + Secrets Secrets `json:",omitempty"` + Licenses LicenseFiles `json:",omitempty"` + + // ImageConfig has information from container image config + ImageConfig ImageConfigDetail + + // CustomResources hold analysis results from custom analyzers. + // It is for extensibility and not used in OSS. + CustomResources []CustomResource `json:",omitempty"` + + Layers Layers `json:",omitzero"` +} + +// Sort sorts packages and applications in ArtifactDetail +func (a *ArtifactDetail) Sort() { + sort.Sort(a.Packages) + sort.Sort(a.Applications) + sort.Sort(a.Secrets) + sort.Sort(a.Licenses) + // Misconfigurations will be sorted later +} + +type Secrets []Secret + +func (s Secrets) Len() int { + return len(s) +} + +func (s Secrets) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s Secrets) Less(i, j int) bool { + return s[i].FilePath < s[j].FilePath +} + +type LicenseFiles []LicenseFile + +func (l LicenseFiles) Len() int { + return len(l) +} + +func (l LicenseFiles) Swap(i, j int) { + l[i], l[j] = l[j], l[i] +} + +func (l LicenseFiles) Less(i, j int) bool { + switch { + case l[i].Type != l[j].Type: + return l[i].Type < l[j].Type + default: + return l[i].FilePath < l[j].FilePath + } +} + +// ImageConfigDetail has information from container image config +type ImageConfigDetail struct { + // Packages are packages extracted from RUN instructions in history + Packages []Package `json:",omitempty"` + + // Misconfiguration holds misconfigurations in container image config + Misconfiguration *Misconfiguration `json:",omitempty"` + + // Secret holds secrets in container image config + Secret *Secret `json:",omitempty"` +} + +// CustomResource holds the analysis result from a custom analyzer. +// It is for extensibility and not used in OSS. +type CustomResource struct { + Type string + FilePath string + Layer Layer + Data any +} diff --git a/pkg/fanal/types/artifact_test.go b/pkg/fanal/types/artifact_test.go new file mode 100644 index 000000000000..7da117699c4c --- /dev/null +++ b/pkg/fanal/types/artifact_test.go @@ -0,0 +1,53 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOS_String(t *testing.T) { + type fields struct { + Family OSType + Name string + } + tests := []struct { + name string + fields fields + want string + }{ + { + name: "family and name", + fields: fields{ + Family: OSType("ubuntu"), + Name: "22.04", + }, + want: "ubuntu/22.04", + }, + { + name: "empty name", + fields: fields{ + Family: OSType("ubuntu"), + Name: "", + }, + want: "ubuntu", + }, + { + name: "empty", + fields: fields{ + Family: "", + Name: "", + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := &OS{ + Family: tt.fields.Family, + Name: tt.fields.Name, + } + assert.Equal(t, tt.want, o.String()) + }) + } +} diff --git a/pkg/fanal/types/const.go b/pkg/fanal/types/const.go new file mode 100644 index 000000000000..8a12d96d00b9 --- /dev/null +++ b/pkg/fanal/types/const.go @@ -0,0 +1,238 @@ +package types + +type ( + // TargetType represents the type of target + TargetType string + + // OSType is an alias of TargetType for operating systems + OSType = TargetType + + // LangType is an alias of TargetType for programming languages + LangType = TargetType + + // ConfigType is an alias of TargetType for configuration files + ConfigType = TargetType +) + +const ( + ArtifactJSONSchemaVersion = 1 + BlobJSONSchemaVersion = 2 +) + +// Operating systems +const ( + ActiveState OSType = "activestate" + Alma OSType = "alma" + Alpine OSType = "alpine" + Amazon OSType = "amazon" + Azure OSType = "azurelinux" + Bottlerocket OSType = "bottlerocket" + CBLMariner OSType = "cbl-mariner" + CentOS OSType = "centos" + CentOSStream OSType = "centos-stream" + Chainguard OSType = "chainguard" + CoreOS OSType = "coreos" + Debian OSType = "debian" + Echo OSType = "echo" + Fedora OSType = "fedora" + MinimOS OSType = "minimos" + OpenSUSE OSType = "opensuse" + OpenSUSELeap OSType = "opensuse-leap" + OpenSUSETumbleweed OSType = "opensuse-tumbleweed" + Oracle OSType = "oracle" + Photon OSType = "photon" + RedHat OSType = "redhat" + Rocky OSType = "rocky" + SLEMicro OSType = "slem" + SLES OSType = "sles" + Ubuntu OSType = "ubuntu" + Wolfi OSType = "wolfi" +) + +// HasOSPackages returns true if the OS type has OS-level packages managed by a package manager. +// Some OS types like ActiveState only contain language-specific packages. +func (o OSType) HasOSPackages() bool { + switch o { + case ActiveState: + return false + default: + return true + } +} + +// PurlNamespace returns the normalized namespace for Package URL (PURL) representation. +// For SUSE-based distributions (SLES, SLE Micro), it returns "suse". +// For openSUSE variants (Tumbleweed, Leap), it returns "opensuse". +// For all other OSTypes, it returns the string representation of the OSType. +func (o OSType) PurlNamespace() string { + // SLES string has whitespace, also highlevel family is not the same as distro + if o == SLES || o == SLEMicro { + return "suse" + } + if o == OpenSUSETumbleweed || o == OpenSUSELeap { + return "opensuse" + } + + return string(o) +} + +// OSTypeAliases is a map of aliases for operating systems. +var OSTypeAliases = map[OSType]OSType{ + // This is used to map the old family names to the new ones for backward compatibility. + "opensuse.leap": OpenSUSELeap, + "opensuse.tumbleweed": OpenSUSETumbleweed, + "suse linux enterprise micro": SLEMicro, + "suse linux enterprise server": SLES, + // This is used to map OS names in EKS + "amazon linux": Amazon, + // This is used to map OS names in Kind + "debian gnu/linux": Debian, +} + +// Programming language dependencies +const ( + Bundler LangType = "bundler" + GemSpec LangType = "gemspec" + Cargo LangType = "cargo" + Composer LangType = "composer" + ComposerVendor LangType = "composer-vendor" + Npm LangType = "npm" + Bun LangType = "bun" + NuGet LangType = "nuget" + DotNetCore LangType = "dotnet-core" + PackagesProps LangType = "packages-props" + Pip LangType = "pip" + Pipenv LangType = "pipenv" + Poetry LangType = "poetry" + Uv LangType = "uv" + CondaPkg LangType = "conda-pkg" + CondaEnv LangType = "conda-environment" + PythonPkg LangType = "python-pkg" + NodePkg LangType = "node-pkg" + Yarn LangType = "yarn" + Pnpm LangType = "pnpm" + Jar LangType = "jar" + Pom LangType = "pom" + Gradle LangType = "gradle" + Sbt LangType = "sbt" + GoBinary LangType = "gobinary" + GoModule LangType = "gomod" + JavaScript LangType = "javascript" + RustBinary LangType = "rustbinary" + Conan LangType = "conan" + Cocoapods LangType = "cocoapods" + Swift LangType = "swift" + Pub LangType = "pub" + Hex LangType = "hex" + Bitnami LangType = "bitnami" + Julia LangType = "julia" + + K8sUpstream LangType = "kubernetes" + EKS LangType = "eks" // Amazon Elastic Kubernetes Service + GKE LangType = "gke" // Google Kubernetes Engine + AKS LangType = "aks" // Azure Kubernetes Service + RKE LangType = "rke" // Rancher Kubernetes Engine + OCP LangType = "ocp" // Red Hat OpenShift Container Platform +) + +var ( + OSTypes = []OSType{ + ActiveState, + Alma, + Alpine, + Amazon, + Azure, + CBLMariner, + CentOS, + CentOSStream, + Chainguard, + CoreOS, + Debian, + Echo, + Fedora, + MinimOS, + OpenSUSE, + OpenSUSELeap, + OpenSUSETumbleweed, + Oracle, + Photon, + RedHat, + Rocky, + SLEMicro, + SLES, + Ubuntu, + Wolfi, + } + AggregatingTypes = []LangType{ + PythonPkg, + CondaPkg, + GemSpec, + NodePkg, + Jar, + } +) + +// Config files +const ( + JSON ConfigType = "json" + YAML ConfigType = "yaml" + Dockerfile ConfigType = "dockerfile" + Terraform ConfigType = "terraform" + TerraformPlanJSON ConfigType = "terraformplan" + TerraformPlanSnapshot ConfigType = "terraformplan-snapshot" + CloudFormation ConfigType = "cloudformation" + Kubernetes ConfigType = "kubernetes" + Helm ConfigType = "helm" + Cloud ConfigType = "cloud" + AzureARM ConfigType = "azure-arm" + Ansible ConfigType = "ansible" +) + +// Language-specific file names +const ( + NuGetPkgsLock = "packages.lock.json" + NuGetPkgsConfig = "packages.config" + + GoMod = "go.mod" + GoSum = "go.sum" + + MavenPom = "pom.xml" + SbtLock = "build.sbt.lock" + + NpmPkg = "package.json" + NpmPkgLock = "package-lock.json" + YarnLock = "yarn.lock" + PnpmLock = "pnpm-lock.yaml" + BunLock = "bun.lock" + + ComposerLock = "composer.lock" + ComposerJson = "composer.json" + ComposerInstalledJson = "installed.json" + + PyProject = "pyproject.toml" + PipRequirements = "requirements.txt" + PipfileLock = "Pipfile.lock" + PoetryLock = "poetry.lock" + UvLock = "uv.lock" + PyLock = "pylock.toml" + + GemfileLock = "Gemfile.lock" + + CargoLock = "Cargo.lock" + CargoToml = "Cargo.toml" + + ConanLock = "conan.lock" + + CocoaPodsLock = "Podfile.lock" + SwiftResolved = "Package.resolved" + + PubSpecLock = "pubspec.lock" + + MixLock = "mix.lock" + + CondaEnvYaml = "environment.yaml" + CondaEnvYml = "environment.yml" + + JuliaProject = "Project.toml" + JuliaManifest = "Manifest.toml" +) diff --git a/pkg/fanal/types/detection.go b/pkg/fanal/types/detection.go new file mode 100644 index 000000000000..38d1634ed2a3 --- /dev/null +++ b/pkg/fanal/types/detection.go @@ -0,0 +1,10 @@ +package types + +// DetectionPriority represents the priority of detection +type DetectionPriority string + +// PriorityPrecise tries to minimize false positives +const PriorityPrecise DetectionPriority = "precise" + +// PriorityComprehensive tries to minimize false negatives +const PriorityComprehensive DetectionPriority = "comprehensive" diff --git a/pkg/fanal/types/error.go b/pkg/fanal/types/error.go new file mode 100644 index 000000000000..02272d6fbd5d --- /dev/null +++ b/pkg/fanal/types/error.go @@ -0,0 +1,8 @@ +package types + +import "golang.org/x/xerrors" + +var ( + InvalidURLPattern = xerrors.New("invalid url pattern") + ErrNoRpmCmd = xerrors.New("no rpm command") +) diff --git a/pkg/fanal/types/handler.go b/pkg/fanal/types/handler.go new file mode 100644 index 000000000000..d53847c896c4 --- /dev/null +++ b/pkg/fanal/types/handler.go @@ -0,0 +1,13 @@ +package types + +type HandlerType string + +const ( + SystemFileFilteringPostHandler HandlerType = "system-file-filter" + UnpackagedPostHandler HandlerType = "unpackaged" + + // SystemFileFilteringPostHandlerPriority should be higher than other handlers. + // Otherwise, other handlers need to process unnecessary files. + SystemFileFilteringPostHandlerPriority = 100 + UnpackagedPostHandlerPriority = 50 +) diff --git a/pkg/fanal/types/image.go b/pkg/fanal/types/image.go new file mode 100644 index 000000000000..08dd8e99355c --- /dev/null +++ b/pkg/fanal/types/image.go @@ -0,0 +1,110 @@ +package types + +import ( + "crypto/x509" + + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +const ( + // DockerImageSource is the docker runtime + DockerImageSource ImageSource = "docker" + + // ContainerdImageSource is the containerd runtime + ContainerdImageSource ImageSource = "containerd" + + // PodmanImageSource is the podman runtime + PodmanImageSource ImageSource = "podman" + + // RemoteImageSource represents a remote scan + RemoteImageSource ImageSource = "remote" +) + +var ( + AllImageSources = ImageSources{ + DockerImageSource, + ContainerdImageSource, + PodmanImageSource, + RemoteImageSource, + } +) + +type Platform struct { + *v1.Platform + + // Force returns an error if the specified platform is not found. + // This option is for Aqua, and cannot be configured via Trivy CLI. + Force bool +} + +type Image interface { + v1.Image + ImageExtension +} + +type ImageExtension interface { + Name() string + ID() (string, error) + RepoTags() []string + RepoDigests() []string +} + +type ImageOptions struct { + RegistryOptions RegistryOptions + DockerOptions DockerOptions + PodmanOptions PodmanOptions + ContainerdOptions ContainerdOptions + ImageSources ImageSources + MaxImageSize int64 +} + +type DockerOptions struct { + Host string +} + +type PodmanOptions struct { + Host string +} + +type ContainerdOptions struct { + // Add Containerd-specific options +} + +// ImageSource represents the source of an image. It can be a string that identifies +// the container registry or a type of container runtime. +type ImageSource string + +// ImageSources is a slice of image sources +type ImageSources []ImageSource + +type RegistryOptions struct { + // Auth for registries + Credentials []Credential + + // RegistryToken is a bearer token to be sent to a registry + RegistryToken string + + // RegistryMirrors is a map of hosts with mirrors for them + RegistryMirrors map[string][]string + + // SSL/TLS + Insecure bool + CACerts *x509.CertPool + + // Architecture + Platform Platform + + // ECR + AWSAccessKey string + AWSSecretKey string + AWSSessionToken string + AWSRegion string + + // GCP + GCPCredPath string +} + +type Credential struct { + Username string + Password string +} diff --git a/pkg/fanal/types/license.go b/pkg/fanal/types/license.go new file mode 100644 index 000000000000..46f482ddc54c --- /dev/null +++ b/pkg/fanal/types/license.go @@ -0,0 +1,58 @@ +package types + +import xslices "github.com/aquasecurity/trivy/pkg/x/slices" + +type LicenseType string + +const ( + LicenseTypeDpkg LicenseType = "dpkg" // From /usr/share/doc/*/copyright + LicenseTypeHeader LicenseType = "header" // From file headers + LicenseTypeFile LicenseType = "license-file" // From LICENSE, COPYRIGHT, etc. +) + +type LicenseCategory string + +const ( + CategoryForbidden LicenseCategory = "forbidden" + CategoryRestricted LicenseCategory = "restricted" + CategoryReciprocal LicenseCategory = "reciprocal" + CategoryNotice LicenseCategory = "notice" + CategoryPermissive LicenseCategory = "permissive" + CategoryUnencumbered LicenseCategory = "unencumbered" + CategoryUnknown LicenseCategory = "unknown" +) + +type LicenseFile struct { + Type LicenseType + FilePath string + PkgName string + Findings LicenseFindings + Layer Layer `json:",omitzero"` +} + +type LicenseFindings []LicenseFinding + +func (findings LicenseFindings) Len() int { + return len(findings) +} + +func (findings LicenseFindings) Swap(i, j int) { + findings[i], findings[j] = findings[j], findings[i] +} + +func (findings LicenseFindings) Less(i, j int) bool { + return findings[i].Name < findings[j].Name +} + +func (findings LicenseFindings) Names() []string { + return xslices.Map(findings, func(finding LicenseFinding) string { + return finding.Name + }) +} + +type LicenseFinding struct { + Category LicenseCategory // such as "forbidden" + Name string + Confidence float64 + Link string +} diff --git a/pkg/fanal/types/misconf.go b/pkg/fanal/types/misconf.go new file mode 100644 index 000000000000..34b3e75d76bd --- /dev/null +++ b/pkg/fanal/types/misconf.go @@ -0,0 +1,150 @@ +package types + +import ( + "fmt" + "sort" + + "github.com/samber/lo" +) + +type Misconfiguration struct { + FileType ConfigType `json:",omitempty"` + FilePath string `json:",omitempty"` + Successes MisconfResults `json:",omitempty"` + Warnings MisconfResults `json:",omitempty"` + Failures MisconfResults `json:",omitempty"` + Layer Layer `json:",omitzero"` +} + +type MisconfResult struct { + Namespace string `json:",omitempty"` + Query string `json:",omitempty"` + Message string `json:",omitempty"` + PolicyMetadata `json:",omitzero"` + CauseMetadata `json:",omitzero"` + + // For debugging + Traces []string `json:",omitempty"` +} + +type MisconfResults []MisconfResult + +type CauseMetadata struct { + Resource string `json:",omitempty"` + Provider string `json:",omitempty"` + Service string `json:",omitempty"` + StartLine int `json:",omitempty"` + EndLine int `json:",omitempty"` + Code Code `json:",omitzero"` + Occurrences []Occurrence `json:",omitempty"` + RenderedCause RenderedCause `json:",omitzero"` +} + +type Occurrence struct { + Resource string `json:",omitempty"` + Filename string `json:",omitempty"` + Location Location +} + +type RenderedCause struct { + Raw string `json:",omitempty"` + Highlighted string `json:",omitempty"` +} + +type Code struct { + Lines []Line +} + +type Line struct { + Number int `json:"Number"` + Content string `json:"Content"` + IsCause bool `json:"IsCause"` + Annotation string `json:"Annotation"` + Truncated bool `json:"Truncated"` + Highlighted string `json:"Highlighted,omitempty"` + FirstCause bool `json:"FirstCause"` + LastCause bool `json:"LastCause"` +} + +type PolicyMetadata struct { + ID string `json:",omitempty"` + // Deprecated: Use the ID field instead. + AVDID string `json:",omitempty"` + Aliases []string `json:",omitempty"` + Type string `json:",omitempty"` + Title string `json:",omitempty"` + Description string `json:",omitempty"` + Severity string `json:",omitempty"` + RecommendedActions string `json:",omitempty" mapstructure:"recommended_actions"` + References []string `json:",omitempty"` +} + +type PolicyInputOption struct { + Combine bool `mapstructure:"combine"` + Selectors []PolicyInputSelector `mapstructure:"selector"` +} + +type PolicyInputSelector struct { + Type string `mapstructure:"type"` +} + +func (r MisconfResults) Len() int { + return len(r) +} + +func (r MisconfResults) Swap(i, j int) { + r[i], r[j] = r[j], r[i] +} + +func (r MisconfResults) Less(i, j int) bool { + switch { + case r[i].Type != r[j].Type: + return r[i].Type < r[j].Type + case r[i].AVDID != r[j].AVDID: + return r[i].AVDID < r[j].AVDID + case r[i].ID != r[j].ID: + return r[i].ID < r[j].ID + case r[i].Severity != r[j].Severity: + return r[i].Severity < r[j].Severity + case r[i].Resource != r[j].Resource: + return r[i].Resource < r[j].Resource + } + return r[i].Message < r[j].Message +} + +func ToMisconfigurations(misconfs map[string]Misconfiguration) []Misconfiguration { + var results []Misconfiguration + for _, misconf := range misconfs { + // Remove duplicates + misconf.Successes = uniqueResults(misconf.Successes) + misconf.Warnings = uniqueResults(misconf.Warnings) + misconf.Failures = uniqueResults(misconf.Failures) + + // Sort results + sort.Sort(misconf.Successes) + sort.Sort(misconf.Warnings) + sort.Sort(misconf.Failures) + + results = append(results, misconf) + } + + // Sort misconfigurations + sort.Slice(results, func(i, j int) bool { + if results[i].FileType != results[j].FileType { + return results[i].FileType < results[j].FileType + } + return results[i].FilePath < results[j].FilePath + }) + + return results +} + +func uniqueResults(results []MisconfResult) []MisconfResult { + if len(results) == 0 { + return results + } + return lo.UniqBy(results, func(result MisconfResult) string { + return fmt.Sprintf("ID: %s, Namespace: %s, Messsage: %s, Cause: %v", + result.ID, result.Namespace, result.Message, result.CauseMetadata) + }) +} diff --git a/pkg/fanal/types/package.go b/pkg/fanal/types/package.go new file mode 100644 index 000000000000..4a2b11ebca81 --- /dev/null +++ b/pkg/fanal/types/package.go @@ -0,0 +1,306 @@ +package types + +import ( + "encoding/json" + "strings" + + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/digest" +) + +type Relationship int + +const ( + RelationshipUnknown Relationship = iota + RelationshipRoot + RelationshipWorkspace // For maven `modules`. TODO use it for cargo and npm workspaces + RelationshipDirect + RelationshipIndirect +) + +var ( + Relationships = []Relationship{ + RelationshipUnknown, + RelationshipRoot, + RelationshipWorkspace, + RelationshipDirect, + RelationshipIndirect, + } + + relationshipNames = [...]string{ + "unknown", + "root", + "workspace", + "direct", + "indirect", + } +) + +func NewRelationship(s string) (Relationship, error) { + for i, name := range relationshipNames { + if s == name { + return Relationship(i), nil + } + } + return RelationshipUnknown, xerrors.Errorf("invalid relationship (%s)", s) +} + +func (r Relationship) String() string { + if r <= RelationshipUnknown || int(r) >= len(relationshipNames) { + return "unknown" + } + return relationshipNames[r] +} + +func (r Relationship) MarshalJSON() ([]byte, error) { + return json.Marshal(r.String()) +} + +func (r *Relationship) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + for i, name := range relationshipNames { + if s == name { + *r = Relationship(i) + return nil + } + } + return xerrors.Errorf("invalid relationship (%s)", s) +} + +// PkgIdentifier represents a software identifiers in one of more of the supported formats. +type PkgIdentifier struct { + UID string `json:",omitempty"` // Calculated by the package struct + PURL *packageurl.PackageURL `json:"-"` + BOMRef string `json:",omitempty"` // For CycloneDX +} + +// MarshalJSON customizes the JSON encoding of PkgIdentifier. +func (id PkgIdentifier) MarshalJSON() ([]byte, error) { + var p string + if id.PURL != nil { + p = id.PURL.String() + } + + type Alias PkgIdentifier + return json.Marshal(&struct { + PURL string `json:",omitempty"` + *Alias + }{ + PURL: p, + Alias: (*Alias)(&id), + }) +} + +// UnmarshalJSON customizes the JSON decoding of PkgIdentifier. +func (id *PkgIdentifier) UnmarshalJSON(data []byte) error { + type Alias PkgIdentifier + aux := &struct { + PURL string `json:",omitempty"` + *Alias + }{ + Alias: (*Alias)(id), + } + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + if aux.PURL != "" { + p, err := packageurl.FromString(aux.PURL) + if err != nil { + return err + } else if len(p.Qualifiers) == 0 { + p.Qualifiers = nil + } + id.PURL = &p + } + + return nil +} + +func (id *PkgIdentifier) Empty() bool { + return id.UID == "" && id.PURL == nil && id.BOMRef == "" +} + +func (id *PkgIdentifier) Match(s string) bool { + // Encode string as PURL + if strings.HasPrefix(s, "pkg:") { + if p, err := packageurl.FromString(s); err == nil { + s = p.String() + } + } + + switch { + case id.BOMRef == s: + return true + case id.PURL != nil && id.PURL.String() == s: + return true + } + return false +} + +type Location struct { + StartLine int `json:",omitempty"` + EndLine int `json:",omitempty"` +} + +type Locations []Location + +func (locs Locations) Len() int { return len(locs) } +func (locs Locations) Less(i, j int) bool { + return locs[i].StartLine < locs[j].StartLine +} +func (locs Locations) Swap(i, j int) { locs[i], locs[j] = locs[j], locs[i] } + +type ExternalRef struct { + Type RefType + URL string +} + +type RefType string + +const ( + RefVCS RefType = "vcs" + RefOther RefType = "other" +) + +// BuildInfo represents information under /root/buildinfo in RHEL +type BuildInfo struct { + ContentSets []string `json:",omitempty"` + Nvr string `json:",omitempty"` + Arch string `json:",omitempty"` +} + +// RepositoryClass indicates the class of repository from which the package was distributed. +type RepositoryClass string + +const ( + RepositoryClassUnknown RepositoryClass = "" // Unknown or not set + RepositoryClassOfficial RepositoryClass = "official" // Official OS repository (Debian, Ubuntu, Red Hat, etc.) + RepositoryClassThirdParty RepositoryClass = "third-party" // Third-party repository (Docker, NVIDIA, etc.) +) + +// PackageRepository indicates where the package was distributed from. +type PackageRepository struct { + Class RepositoryClass `json:",omitempty"` + // Future: Add Name field for specific repository identification (e.g., "epel", "docker") + // Name string `json:",omitempty"` +} + +type Package struct { + ID string `json:",omitempty"` + Name string `json:",omitempty"` + Identifier PkgIdentifier `json:",omitzero"` + Version string `json:",omitempty"` + Release string `json:",omitempty"` + Epoch int `json:",omitempty"` + Arch string `json:",omitempty"` + Dev bool `json:",omitempty"` + SrcName string `json:",omitempty"` + SrcVersion string `json:",omitempty"` + SrcRelease string `json:",omitempty"` + SrcEpoch int `json:",omitempty"` + Licenses []string `json:",omitempty"` + Maintainer string `json:",omitempty"` + ExternalReferences []ExternalRef `json:"-" hash:"ignore"` + + Modularitylabel string `json:",omitempty"` // only for Red Hat based distributions + BuildInfo *BuildInfo `json:",omitempty"` // only for Red Hat + + // Repository indicates where the package was distributed from. + Repository PackageRepository `json:",omitzero"` + + Indirect bool `json:",omitempty"` // Deprecated: Use relationship. Kept for backward compatibility. + Relationship Relationship `json:",omitempty"` + + // Dependencies of this package + // Note: it may have interdependencies, which may lead to infinite loops. + DependsOn []string `json:",omitempty"` + + Layer Layer `json:",omitzero"` + + // Each package metadata have the file path, while the package from lock files does not have. + FilePath string `json:",omitempty"` + + // This is required when using SPDX formats. Otherwise, it will be empty. + Digest digest.Digest `json:",omitempty"` + + // lines from the lock file where the dependency is written + Locations Locations `json:",omitempty"` + + // Files installed by the package + InstalledFiles []string `json:",omitempty"` + + // AnalyzedBy indicates which analyzer detected this package. + // This is not a property of the package itself, but metadata about the detection process. + AnalyzedBy AnalyzerType `json:",omitempty"` +} + +func (pkg *Package) Empty() bool { + return pkg.Name == "" || pkg.Version == "" +} + +type Packages []Package + +func (pkgs Packages) Len() int { + return len(pkgs) +} + +func (pkgs Packages) Swap(i, j int) { + pkgs[i], pkgs[j] = pkgs[j], pkgs[i] +} + +func (pkgs Packages) Less(i, j int) bool { + switch { + case pkgs[i].Relationship != pkgs[j].Relationship: + if pkgs[i].Relationship == RelationshipUnknown { + return false + } else if pkgs[j].Relationship == RelationshipUnknown { + return true + } + return pkgs[i].Relationship < pkgs[j].Relationship + case pkgs[i].Name != pkgs[j].Name: + return pkgs[i].Name < pkgs[j].Name + case pkgs[i].Version != pkgs[j].Version: + return pkgs[i].Version < pkgs[j].Version + case pkgs[i].ID != pkgs[j].ID: + return pkgs[i].ID < pkgs[j].ID + } + return pkgs[i].FilePath < pkgs[j].FilePath +} + +// ParentDeps returns a map where the keys are package IDs and the values are the packages +// that depend on the respective package ID (parent dependencies). +func (pkgs Packages) ParentDeps() map[string]Packages { + parents := make(map[string]Packages) + for _, pkg := range pkgs { + for _, dependOn := range pkg.DependsOn { + parents[dependOn] = append(parents[dependOn], pkg) + } + } + + for k, v := range parents { + parents[k] = lo.UniqBy(v, func(pkg Package) string { + return pkg.ID + }) + } + return parents +} + +type Dependency struct { + ID string + DependsOn []string +} + +type Dependencies []Dependency + +func (deps Dependencies) Len() int { return len(deps) } +func (deps Dependencies) Less(i, j int) bool { + return deps[i].ID < deps[j].ID +} +func (deps Dependencies) Swap(i, j int) { deps[i], deps[j] = deps[j], deps[i] } diff --git a/pkg/fanal/types/secret.go b/pkg/fanal/types/secret.go new file mode 100644 index 000000000000..88d3dad991b0 --- /dev/null +++ b/pkg/fanal/types/secret.go @@ -0,0 +1,21 @@ +package types + +type SecretRuleCategory string + +type Secret struct { + FilePath string + Findings []SecretFinding +} + +type SecretFinding struct { + RuleID string + Category SecretRuleCategory + Severity string + Title string + StartLine int + EndLine int + Code Code + Match string + Layer Layer `json:",omitzero"` + Offset int `json:",omitempty"` // Byte offset in the original file +} diff --git a/pkg/fanal/utils/gzip/gzip.go b/pkg/fanal/utils/gzip/gzip.go new file mode 100644 index 000000000000..9e3cd377c968 --- /dev/null +++ b/pkg/fanal/utils/gzip/gzip.go @@ -0,0 +1,54 @@ +package gzip + +import ( + "bufio" + "compress/gzip" + "io" + "os" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/utils" +) + +// multiCloser wraps a reader and manages multiple closers for proper cleanup +type multiCloser struct { + io.Reader + closers []io.Closer +} + +func (mc *multiCloser) Close() error { + for _, c := range mc.closers { + if err := c.Close(); err != nil { + return err + } + } + return nil +} + +// OpenFile opens a file (optionally gzipped) by file path +func OpenFile(fileName string) (io.ReadCloser, error) { + f, err := os.Open(fileName) + if err != nil { + return nil, xerrors.Errorf("unable to open the file: %w", err) + } + + mc := &multiCloser{ + closers: []io.Closer{f}, + } + + br := bufio.NewReader(f) + mc.Reader = br + + if utils.IsGzip(br) { + gzr, err := gzip.NewReader(br) + if err != nil { + _ = f.Close() + return nil, xerrors.Errorf("failed to open gzip: %w", err) + } + mc.Reader = gzr + mc.closers = append(mc.closers, gzr) + } + + return mc, nil +} diff --git a/pkg/fanal/utils/testdata/aqua.png b/pkg/fanal/utils/testdata/aqua.png new file mode 100644 index 000000000000..80c2f5fded03 Binary files /dev/null and b/pkg/fanal/utils/testdata/aqua.png differ diff --git a/pkg/fanal/utils/testdata/test.tar.gz b/pkg/fanal/utils/testdata/test.tar.gz new file mode 100644 index 000000000000..ad92b40a48c4 Binary files /dev/null and b/pkg/fanal/utils/testdata/test.tar.gz differ diff --git a/pkg/fanal/utils/testdata/test.txt b/pkg/fanal/utils/testdata/test.txt new file mode 100644 index 000000000000..06d740502001 Binary files /dev/null and b/pkg/fanal/utils/testdata/test.txt differ diff --git a/pkg/fanal/utils/testdata/test.txt.gz b/pkg/fanal/utils/testdata/test.txt.gz new file mode 100644 index 000000000000..ec1e1784b1e3 Binary files /dev/null and b/pkg/fanal/utils/testdata/test.txt.gz differ diff --git a/pkg/fanal/utils/testdata/test.txt.zst b/pkg/fanal/utils/testdata/test.txt.zst new file mode 100644 index 000000000000..2981873cdf95 Binary files /dev/null and b/pkg/fanal/utils/testdata/test.txt.zst differ diff --git a/pkg/fanal/utils/testdata/testdir.tar.gz b/pkg/fanal/utils/testdata/testdir.tar.gz new file mode 100644 index 000000000000..d6ae289abac0 Binary files /dev/null and b/pkg/fanal/utils/testdata/testdir.tar.gz differ diff --git a/pkg/fanal/utils/utils.go b/pkg/fanal/utils/utils.go new file mode 100644 index 000000000000..9958b8c2f1b6 --- /dev/null +++ b/pkg/fanal/utils/utils.go @@ -0,0 +1,141 @@ +package utils + +import ( + "bufio" + "bytes" + "fmt" + "io" + "math" + "os" + "os/exec" + "path/filepath" + "strings" + "unicode" + + "github.com/bmatcuk/doublestar/v4" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +var PathSeparator = fmt.Sprintf("%c", os.PathSeparator) + +func CacheDir() string { + cacheDir, err := os.UserCacheDir() + if err != nil { + cacheDir = os.TempDir() + } + return cacheDir +} + +func IsCommandAvailable(name string) bool { + if _, err := exec.LookPath(name); err != nil { + return false + } + return true +} + +func IsGzip(f *bufio.Reader) bool { + buf, err := f.Peek(3) + if err != nil { + return false + } + return buf[0] == 0x1F && buf[1] == 0x8B && buf[2] == 0x8 +} + +func IsExecutable(fileInfo os.FileInfo) bool { + // For Windows + if filepath.Ext(fileInfo.Name()) == ".exe" { + return true + } + + mode := fileInfo.Mode() + if !mode.IsRegular() { + return false + } + + // Check unpackaged file + if mode.Perm()&0o111 != 0 { + return true + } + return false +} + +func IsBinary(content xio.ReadSeekerAt, fileSize int64) (bool, error) { + headSize := int(math.Min(float64(fileSize), 300)) + head := make([]byte, headSize) + if _, err := content.Read(head); err != nil { + return false, err + } + if _, err := content.Seek(0, io.SeekStart); err != nil { + return false, err + } + + // cf. https://github.com/file/file/blob/f2a6e7cb7db9b5fd86100403df6b2f830c7f22ba/src/encoding.c#L151-L228 + for _, b := range head { + if b < 7 || b == 11 || (13 < b && b < 27) || (27 < b && b < 0x20) || b == 0x7f { + return true, nil + } + } + + return false, nil +} + +func CleanSkipPaths(skipPaths []string) []string { + return xslices.Map(skipPaths, func(skipPath string) string { + skipPath = filepath.ToSlash(filepath.Clean(skipPath)) + return strings.TrimLeft(skipPath, "/") + }) +} + +func SkipPath(path string, skipPaths []string) bool { + path = strings.TrimLeft(path, "/") + + // skip files + for _, pattern := range skipPaths { + match, err := doublestar.Match(pattern, path) + if err != nil { + return false // return early if bad pattern + } else if match { + log.Debug("Skipping path", log.String("path", path)) + return true + } + } + return false +} + +func ExtractPrintableBytes(content xio.ReadSeekerAt) ([]byte, error) { + const minLength = 4 // Minimum length of strings to extract + var result []byte + currentPrintableLine := new(bytes.Buffer) + + current := make([]byte, 1) // buffer for 1 byte reading + + for { + if n, err := content.Read(current); err == io.EOF { + break + } else if n != 1 { + continue + } else if err != nil { + return nil, xerrors.Errorf("failed to read a byte: %w", err) + } + if unicode.IsPrint(rune(current[0])) { + _ = currentPrintableLine.WriteByte(current[0]) + continue + } + if currentPrintableLine.Len() > minLength { + // add a newline between printable lines to separate them + _ = currentPrintableLine.WriteByte('\n') + result = append(result, currentPrintableLine.Bytes()...) + } + currentPrintableLine.Reset() + } + if currentPrintableLine.Len() > minLength { + // add a newline between printable lines to separate them + _ = currentPrintableLine.WriteByte('\n') + result = append(result, currentPrintableLine.Bytes()...) + } + return result, nil +} diff --git a/pkg/fanal/utils/utils_test.go b/pkg/fanal/utils/utils_test.go new file mode 100644 index 000000000000..0b497369db71 --- /dev/null +++ b/pkg/fanal/utils/utils_test.go @@ -0,0 +1,34 @@ +package utils + +import ( + "bufio" + "os" + "path/filepath" + "testing" +) + +func TestIsGzip(t *testing.T) { + var tests = []struct { + in string + want bool + }{ + {filepath.Join("testdata", "test.txt.gz"), true}, + {filepath.Join("testdata", "test.tar.gz"), true}, + {filepath.Join("testdata", "test.txt"), false}, + {filepath.Join("testdata", "test.txt.zst"), false}, + {filepath.Join("testdata", "aqua.png"), false}, + } + for _, tt := range tests { + t.Run(tt.in, func(t *testing.T) { + f, err := os.Open(tt.in) + if err != nil { + t.Fatalf("unknown error: %s", err) + } + + got := IsGzip(bufio.NewReader(f)) + if got != tt.want { + t.Errorf("got %t, want %t", got, tt.want) + } + }) + } +} diff --git a/pkg/fanal/vm/disk/disk.go b/pkg/fanal/vm/disk/disk.go new file mode 100644 index 000000000000..bf39707e2eb5 --- /dev/null +++ b/pkg/fanal/vm/disk/disk.go @@ -0,0 +1,37 @@ +package disk + +import ( + "errors" + "io" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/vm" +) + +var ( + vmDisks = []Disk{ + VMDK{}, + } +) + +// Disk defines virtual machine disk images like VMDK, VDI and VHD. +type Disk interface { + NewReader(io.ReadSeeker, vm.Cache[string, []byte]) (*io.SectionReader, error) +} + +func New(rs io.ReadSeeker, cache vm.Cache[string, []byte]) (*io.SectionReader, error) { + + for _, vmdisk := range vmDisks { + var vreader, err = vmdisk.NewReader(rs, cache) + if err != nil { + if errors.Is(err, vm.ErrInvalidSignature) { + continue + } + return nil, xerrors.Errorf("open virtual machine error: %w", err) + } + + return vreader, nil + } + return nil, xerrors.New("virtual machine can not be detected") +} diff --git a/pkg/fanal/vm/disk/disk_test.go b/pkg/fanal/vm/disk/disk_test.go new file mode 100644 index 000000000000..df6f906bf812 --- /dev/null +++ b/pkg/fanal/vm/disk/disk_test.go @@ -0,0 +1,43 @@ +package disk_test + +import ( + "io" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/vm" + "github.com/aquasecurity/trivy/pkg/fanal/vm/disk" +) + +func TestNew(t *testing.T) { + type args struct { + rs io.ReadSeeker + cache vm.Cache[string, []byte] + } + tests := []struct { + name string + fileName string + wantErr string + }{ + { + name: "invalid vm file", + fileName: "testdata/invalid.vmdk", + wantErr: "virtual machine can not be detected", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.fileName) + require.NoError(t, err) + + _, err = disk.New(f, nil) + if err == nil { + assert.Fail(t, "required error test") + } + assert.ErrorContains(t, err, tt.wantErr) + }) + } +} diff --git a/pkg/fanal/vm/disk/testdata/invalid.vmdk b/pkg/fanal/vm/disk/testdata/invalid.vmdk new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/vm/disk/vmdk.go b/pkg/fanal/vm/disk/vmdk.go new file mode 100644 index 000000000000..3be79780e43f --- /dev/null +++ b/pkg/fanal/vm/disk/vmdk.go @@ -0,0 +1,36 @@ +package disk + +import ( + "errors" + "io" + + "github.com/masahiro331/go-vmdk-parser/pkg/virtualization/vmdk" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/vm" +) + +type VMDK struct{} + +func (VMDK) NewReader(rs io.ReadSeeker, cache vm.Cache[string, []byte]) (*io.SectionReader, error) { + if _, err := rs.Seek(0, io.SeekStart); err != nil { + return nil, xerrors.Errorf("seek error: %w", err) + } + + if _, err := vmdk.Check(rs); err != nil { + return nil, vm.ErrInvalidSignature + } + + if _, err := rs.Seek(0, io.SeekStart); err != nil { + return nil, xerrors.Errorf("seek error: %w", err) + } + + reader, err := vmdk.Open(rs, cache) + if err != nil { + if errors.Is(err, vmdk.ErrUnSupportedType) { + return nil, xerrors.Errorf("%s: %w", err.Error(), vm.ErrUnsupportedType) + } + return nil, xerrors.Errorf("failed to open vmdk: %w", err) + } + return reader, nil +} diff --git a/pkg/fanal/vm/disk/vmdk_test.go b/pkg/fanal/vm/disk/vmdk_test.go new file mode 100644 index 000000000000..4c62a202b423 --- /dev/null +++ b/pkg/fanal/vm/disk/vmdk_test.go @@ -0,0 +1,41 @@ +package disk_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/vm/disk" +) + +func TestVMDK_NewReader(t *testing.T) { + tests := []struct { + name string + fileName string + wantErr string + }{ + // TODO: add valid tests + { + name: "invalid vmdk file", + fileName: "testdata/invalid.vmdk", + wantErr: "invalid signature error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := disk.VMDK{} + + f, err := os.Open(tt.fileName) + require.NoError(t, err) + defer f.Close() + + _, err = v.NewReader(f, nil) + if err == nil { + assert.Fail(t, "required error test") + } + assert.ErrorContains(t, err, tt.wantErr) + }) + } +} diff --git a/pkg/fanal/vm/filesystem/ext4.go b/pkg/fanal/vm/filesystem/ext4.go new file mode 100644 index 000000000000..f2476988450a --- /dev/null +++ b/pkg/fanal/vm/filesystem/ext4.go @@ -0,0 +1,34 @@ +package filesystem + +import ( + "io" + "io/fs" + + "github.com/masahiro331/go-ext4-filesystem/ext4" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/vm" +) + +type EXT4 struct{} + +func (e EXT4) New(sr io.SectionReader, cache vm.Cache[string, any]) (fs.FS, error) { + _, err := sr.Seek(0, io.SeekStart) + if err != nil { + return nil, xerrors.Errorf("failed to seek offset error: %w", err) + } + ok := ext4.Check(&sr) + if !ok { + return nil, ErrInvalidHeader + } + + _, err = sr.Seek(0, io.SeekStart) + if err != nil { + return nil, xerrors.Errorf("failed to seek offset error: %w", err) + } + f, err := ext4.NewFS(sr, cache) + if err != nil { + return nil, xerrors.Errorf("new ext4 filesystem error: %w", err) + } + return f, nil +} diff --git a/pkg/fanal/vm/filesystem/filesystem.go b/pkg/fanal/vm/filesystem/filesystem.go new file mode 100644 index 000000000000..ae77610579c4 --- /dev/null +++ b/pkg/fanal/vm/filesystem/filesystem.go @@ -0,0 +1,50 @@ +package filesystem + +import ( + "errors" + "io" + "io/fs" + + lru "github.com/hashicorp/golang-lru/v2" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/vm" +) + +const cacheSize = 2048 + +var ( + ErrInvalidHeader = xerrors.New("invalid Header error") + filesystems = []Filesystem{ + EXT4{}, + XFS{}, + } +) + +type Filesystem interface { + New(io.SectionReader, vm.Cache[string, any]) (fs.FS, error) +} + +func New(sr io.SectionReader) (fs.FS, func(), error) { + var clean func() + + // Initialize LRU cache for filesystem walking + lruCache, err := lru.New[string, any](cacheSize) + if err != nil { + return nil, clean, xerrors.Errorf("failed to create a LRU cache: %w", err) + } + clean = lruCache.Purge + + for _, filesystem := range filesystems { + // TODO: implement LVM handler + fsys, err := filesystem.New(sr, lruCache) + if err != nil { + if errors.Is(err, ErrInvalidHeader) { + continue + } + return nil, clean, xerrors.Errorf("unexpected fs error: %w", err) + } + return fsys, clean, nil + } + return nil, clean, xerrors.New("unable to detect filesystem") +} diff --git a/pkg/fanal/vm/filesystem/xfs.go b/pkg/fanal/vm/filesystem/xfs.go new file mode 100644 index 000000000000..d1344e846354 --- /dev/null +++ b/pkg/fanal/vm/filesystem/xfs.go @@ -0,0 +1,34 @@ +package filesystem + +import ( + "io" + "io/fs" + + "github.com/masahiro331/go-xfs-filesystem/xfs" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/vm" +) + +type XFS struct{} + +func (x XFS) New(sr io.SectionReader, cache vm.Cache[string, any]) (fs.FS, error) { + _, err := sr.Seek(0, io.SeekStart) + if err != nil { + return nil, xerrors.Errorf("failed to seek offset error: %w", err) + } + ok := xfs.Check(&sr) + if !ok { + return nil, ErrInvalidHeader + } + + _, err = sr.Seek(0, io.SeekStart) + if err != nil { + return nil, xerrors.Errorf("failed to seek offset error: %w", err) + } + f, err := xfs.NewFS(sr, cache) + if err != nil { + return nil, xerrors.Errorf("new xfs filesystem error: %w", err) + } + return f, nil +} diff --git a/pkg/fanal/vm/vm.go b/pkg/fanal/vm/vm.go new file mode 100644 index 000000000000..bfb414263712 --- /dev/null +++ b/pkg/fanal/vm/vm.go @@ -0,0 +1,18 @@ +package vm + +import ( + "golang.org/x/xerrors" +) + +var ( + ErrInvalidSignature = xerrors.New("invalid signature error") + ErrUnsupportedType = xerrors.New("unsupported type error") +) + +type Cache[K comparable, V any] interface { + // Add stores data in the cache + Add(key K, value V) (evicted bool) + + // Get returns key's value from the cache + Get(key K) (value V, ok bool) +} diff --git a/pkg/fanal/walker/cached_file.go b/pkg/fanal/walker/cached_file.go new file mode 100644 index 000000000000..20184e5b6e4a --- /dev/null +++ b/pkg/fanal/walker/cached_file.go @@ -0,0 +1,83 @@ +package walker + +import ( + "bytes" + "io" + "os" + "sync" + + "golang.org/x/xerrors" + + xio "github.com/aquasecurity/trivy/pkg/x/io" + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +// cachedFile represents a file cached in memory or storage according to the file size. +type cachedFile struct { + once sync.Once + err error + + size int64 + reader io.Reader + + content []byte // It will be populated if this file is small + filePath string // It will be populated if this file is large +} + +func newCachedFile(size int64, r io.Reader) *cachedFile { + return &cachedFile{ + size: size, + reader: r, + } +} + +// Open opens a file and cache the file. +// If the file size is greater than or equal to threshold, it copies the content to a temp file and opens it next time. +// If the file size is less than threshold, it opens the file once and the content will be shared so that others analyzers can use the same data. +func (o *cachedFile) Open() (xio.ReadSeekCloserAt, error) { + o.once.Do(func() { + // When the file is large, it will be written down to a temp file. + if o.size >= defaultSizeThreshold { + f, err := xos.CreateTemp("", "cached-file-") + if err != nil { + o.err = xerrors.Errorf("failed to create the temp file: %w", err) + return + } + + if _, err = io.Copy(f, o.reader); err != nil { + o.err = xerrors.Errorf("failed to copy: %w", err) + return + } + + o.filePath = f.Name() + } else { + b, err := io.ReadAll(o.reader) + if err != nil { + o.err = xerrors.Errorf("unable to read the file: %w", err) + return + } + o.content = b + } + }) + if o.err != nil { + return nil, xerrors.Errorf("failed to open: %w", o.err) + } + + return o.open() +} + +func (o *cachedFile) open() (xio.ReadSeekCloserAt, error) { + if o.filePath != "" { + f, err := os.Open(o.filePath) + if err != nil { + return nil, xerrors.Errorf("failed to open the temp file: %w", err) + } + return f, nil + } + + return xio.NopCloser(bytes.NewReader(o.content)), nil +} + +func (o *cachedFile) Clean() error { + return os.Remove(o.filePath) +} diff --git a/pkg/fanal/walker/fs.go b/pkg/fanal/walker/fs.go new file mode 100644 index 000000000000..3ebfb7258459 --- /dev/null +++ b/pkg/fanal/walker/fs.go @@ -0,0 +1,161 @@ +package walker + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/utils" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +// FS is the filesystem walker +type FS struct{} + +func NewFS() *FS { + return &FS{} +} + +// Walk walks the filesystem rooted at root, calling fn for each unfiltered file. +func (w *FS) Walk(root string, opt Option, fn WalkFunc) error { + opt.SkipFiles = w.BuildSkipPaths(root, opt.SkipFiles) + opt.SkipDirs = w.BuildSkipPaths(root, opt.SkipDirs) + opt.SkipDirs = append(opt.SkipDirs, defaultSkipDirs...) + + walkDirFunc := w.WalkDirFunc(root, fn, opt) + walkDirFunc = w.onError(walkDirFunc) + + // Walk the filesystem + if err := filepath.WalkDir(root, walkDirFunc); err != nil { + return xerrors.Errorf("walk dir error: %w", err) + } + + return nil +} + +func (w *FS) WalkDirFunc(root string, fn WalkFunc, opt Option) fs.WalkDirFunc { + return func(filePath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + // For exported rootfs (e.g. images/alpine/etc/alpine-release) + relPath, err := filepath.Rel(root, filePath) + if err != nil { + return xerrors.Errorf("filepath rel (%s): %w", relPath, err) + } + relPath = filepath.ToSlash(relPath) + + // Skip unnecessary files + switch { + case d.IsDir(): + if utils.SkipPath(relPath, opt.SkipDirs) { + return filepath.SkipDir + } + return nil + case !d.Type().IsRegular(): + return nil + case utils.SkipPath(relPath, opt.SkipFiles): + return nil + } + + info, err := d.Info() + if err != nil { + return xerrors.Errorf("file info error: %w", err) + } + + if err = fn(relPath, info, fileOpener(filePath)); err != nil { + return xerrors.Errorf("failed to analyze file: %w", err) + } + + return nil + } +} + +func (w *FS) onError(wrapped fs.WalkDirFunc) fs.WalkDirFunc { + return func(filePath string, d fs.DirEntry, err error) error { + err = wrapped(filePath, d, err) + switch { + // Unwrap fs.SkipDir error + case errors.Is(err, fs.SkipDir): + return fs.SkipDir + // Ignore permission errors + case os.IsPermission(err): + return nil + case err != nil: + // halt traversal on any other error + return xerrors.Errorf("unknown error with %s: %w", filePath, err) + } + return nil + } +} + +// BuildSkipPaths builds correct patch for defaultSkipDirs and skipFiles +func (w *FS) BuildSkipPaths(base string, paths []string) []string { + var relativePaths []string + absBase, err := filepath.Abs(base) + if err != nil { + log.Warn("Failed to get an absolute path", log.String("base", base), log.Err(err)) + return nil + } + for _, path := range paths { + // Supports three types of flag specification. + // All of them are converted into the relative path from the root directory. + // 1. Relative skip dirs/files from the root directory + // The specified dirs and files will be used as is. + // e.g. $ trivy fs --skip-dirs bar ./foo + // The skip dir from the root directory will be `bar/`. + // 2. Relative skip dirs/files from the working directory + // The specified dirs and files wll be converted to the relative path from the root directory. + // e.g. $ trivy fs --skip-dirs ./foo/bar ./foo + // The skip dir will be converted to `bar/`. + // 3. Absolute skip dirs/files + // The specified dirs and files wll be converted to the relative path from the root directory. + // e.g. $ trivy fs --skip-dirs /bar/foo/baz ./foo + // When the working directory is + // 3.1 /bar: the skip dir will be converted to `baz/`. + // 3.2 /hoge : the skip dir will be converted to `../../bar/foo/baz/`. + + absSkipPath, err := filepath.Abs(path) + if err != nil { + log.Warn("Failed to get an absolute path", log.String("base", base), log.Err(err)) + continue + } + rel, err := filepath.Rel(absBase, absSkipPath) + if err != nil { + log.Warn("Failed to get a relative path", log.String("from", base), + log.String("to", path), log.Err(err)) + continue + } + + var relPath string + switch { + case !filepath.IsAbs(path) && strings.HasPrefix(rel, ".."): + // #1: Use the path as is + relPath = path + case !filepath.IsAbs(path) && !strings.HasPrefix(rel, ".."): + // #2: Use the relative path from the root directory + relPath = rel + case filepath.IsAbs(path): + // #3: Use the relative path from the root directory + relPath = rel + } + relPath = filepath.ToSlash(relPath) + relativePaths = append(relativePaths, relPath) + } + + relativePaths = utils.CleanSkipPaths(relativePaths) + return relativePaths +} + +// fileOpener returns a function opening a file. +func fileOpener(filePath string) func() (xio.ReadSeekCloserAt, error) { + return func() (xio.ReadSeekCloserAt, error) { + return os.Open(filePath) + } +} diff --git a/pkg/fanal/walker/fs_test.go b/pkg/fanal/walker/fs_test.go new file mode 100644 index 000000000000..714748cb4ac2 --- /dev/null +++ b/pkg/fanal/walker/fs_test.go @@ -0,0 +1,191 @@ +package walker_test + +import ( + "errors" + "io" + "os" + "path/filepath" + "runtime" + "slices" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/walker" +) + +func TestFS_Walk(t *testing.T) { + tests := []struct { + name string + option walker.Option + rootDir string + analyzeFn walker.WalkFunc + wantErr string + }{ + { + name: "happy path", + rootDir: "testdata/fs", + analyzeFn: func(filePath string, _ os.FileInfo, opener analyzer.Opener) error { + if filePath == "testdata/fs/bar" { + got, err := opener() + require.NoError(t, err) + + b, err := io.ReadAll(got) + require.NoError(t, err) + + assert.Equal(t, "bar", string(b)) + } + return nil + }, + }, + { + name: "skip file", + rootDir: "testdata/fs", + option: walker.Option{ + SkipFiles: []string{"testdata/fs/bar"}, + }, + analyzeFn: func(filePath string, _ os.FileInfo, _ analyzer.Opener) error { + if filePath == "testdata/fs/bar" { + assert.Fail(t, "skip files error", "%s should be skipped", filePath) + } + return nil + }, + }, + { + name: "skip dir", + rootDir: "testdata/fs/", + option: walker.Option{ + SkipDirs: []string{"/testdata/fs/app"}, + }, + analyzeFn: func(filePath string, _ os.FileInfo, _ analyzer.Opener) error { + if strings.HasPrefix(filePath, "testdata/fs/app") { + assert.Fail(t, "skip dirs error", "%s should be skipped", filePath) + } + return nil + }, + }, + { + name: "sad path", + rootDir: "testdata/fs", + analyzeFn: func(string, os.FileInfo, analyzer.Opener) error { + return errors.New("error") + }, + wantErr: "failed to analyze file", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + w := walker.NewFS() + err := w.Walk(tt.rootDir, tt.option, tt.analyzeFn) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestFS_BuildSkipPaths(t *testing.T) { + tests := []struct { + name string + oses []string + paths []string + base string + want []string + }{ + // Linux/macOS + { + name: "path - abs, base - abs, not joining paths", + oses: []string{ + "linux", + "darwin", + }, + base: "/foo", + paths: []string{"/foo/bar"}, + want: []string{"bar"}, + }, + { + name: "path - abs, base - rel", + oses: []string{ + "linux", + "darwin", + }, + base: "foo", + paths: func() []string { + abs, err := filepath.Abs("foo/bar") + require.NoError(t, err) + return []string{abs} + }(), + want: []string{"bar"}, + }, + { + name: "path - rel, base - rel, joining paths", + oses: []string{ + "linux", + "darwin", + }, + base: "foo", + paths: []string{"bar"}, + want: []string{"bar"}, + }, + { + name: "path - rel, base - rel, not joining paths", + oses: []string{ + "linux", + "darwin", + }, + base: "foo", + paths: []string{"foo/bar/bar"}, + want: []string{"bar/bar"}, + }, + { + name: "path - rel with dot, base - rel, removing the leading dot and not joining paths", + oses: []string{ + "linux", + "darwin", + }, + base: "foo", + paths: []string{"./foo/bar"}, + want: []string{"bar"}, + }, + { + name: "path - rel, base - dot", + oses: []string{ + "linux", + "darwin", + }, + base: ".", + paths: []string{"foo/bar"}, + want: []string{"foo/bar"}, + }, + // Windows + { + name: "path - rel, base - rel. Skip common prefix", + oses: []string{"windows"}, + base: "foo", + paths: []string{"foo\\bar\\bar"}, + want: []string{"bar/bar"}, + }, + { + name: "path - rel, base - dot, windows", + oses: []string{"windows"}, + base: ".", + paths: []string{"foo\\bar"}, + want: []string{"foo/bar"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if !slices.Contains(tt.oses, runtime.GOOS) { + t.Skipf("Skip path tests for %q", tt.oses) + } + got := walker.NewFS().BuildSkipPaths(tt.base, tt.paths) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/fanal/walker/tar.go b/pkg/fanal/walker/tar.go new file mode 100644 index 000000000000..228b1b7f3ddb --- /dev/null +++ b/pkg/fanal/walker/tar.go @@ -0,0 +1,116 @@ +package walker + +import ( + "archive/tar" + "io" + "io/fs" + "path" + "path/filepath" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/utils" +) + +const ( + opq string = ".wh..wh..opq" + wh string = ".wh." +) + +var parentDir = ".." + utils.PathSeparator + +type LayerTar struct { + skipFiles []string + skipDirs []string +} + +func NewLayerTar(opt Option) LayerTar { + return LayerTar{ + skipFiles: utils.CleanSkipPaths(opt.SkipFiles), + skipDirs: utils.CleanSkipPaths(opt.SkipDirs), + } +} + +func (w LayerTar) Walk(layer io.Reader, analyzeFn WalkFunc) ([]string, []string, error) { + var opqDirs, whFiles, skippedDirs []string + tr := tar.NewReader(layer) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } else if err != nil { + return nil, nil, xerrors.Errorf("failed to extract the archive: %w", err) + } + + // filepath.Clean cannot be used since tar file paths should be OS-agnostic. + filePath := path.Clean(hdr.Name) + filePath = strings.TrimLeft(filePath, "/") + fileDir, fileName := path.Split(filePath) + + // e.g. etc/.wh..wh..opq + if opq == fileName { + opqDirs = append(opqDirs, fileDir) + continue + } + // etc/.wh.hostname + if after, ok := strings.CutPrefix(fileName, wh); ok { + name := after + fpath := path.Join(fileDir, name) + whFiles = append(whFiles, fpath) + continue + } + + switch hdr.Typeflag { + case tar.TypeDir: + if utils.SkipPath(filePath, w.skipDirs) { + skippedDirs = append(skippedDirs, filePath) + continue + } + case tar.TypeReg: + if utils.SkipPath(filePath, w.skipFiles) { + continue + } + // symlinks and hardlinks have no content in reader, skip them + default: + continue + } + + if underSkippedDir(filePath, skippedDirs) { + continue + } + + // A regular file will reach here. + if err = w.processFile(filePath, tr, hdr.FileInfo(), analyzeFn); err != nil { + return nil, nil, xerrors.Errorf("failed to process the file: %w", err) + } + } + return opqDirs, whFiles, nil +} + +func (w LayerTar) processFile(filePath string, tr *tar.Reader, fi fs.FileInfo, analyzeFn WalkFunc) error { + cf := newCachedFile(fi.Size(), tr) + defer func() { + // nolint + _ = cf.Clean() + }() + + if err := analyzeFn(filePath, fi, cf.Open); err != nil { + return xerrors.Errorf("failed to analyze file: %w", err) + } + + return nil +} + +func underSkippedDir(filePath string, skipDirs []string) bool { + for _, skipDir := range skipDirs { + rel, err := filepath.Rel(skipDir, filePath) + if err != nil { + return false + } + if !strings.HasPrefix(rel, parentDir) { + return true + } + } + return false +} diff --git a/pkg/fanal/walker/tar_test.go b/pkg/fanal/walker/tar_test.go new file mode 100644 index 000000000000..245d3b36170e --- /dev/null +++ b/pkg/fanal/walker/tar_test.go @@ -0,0 +1,91 @@ +package walker_test + +import ( + "errors" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/walker" +) + +func TestLayerTar_Walk(t *testing.T) { + tests := []struct { + name string + option walker.Option + inputFile string + analyzeFn walker.WalkFunc + wantOpqDirs []string + wantWhFiles []string + wantErr string + }{ + { + name: "happy path", + inputFile: filepath.Join("testdata", "test.tar"), + analyzeFn: func(_ string, _ os.FileInfo, _ analyzer.Opener) error { + return nil + }, + wantOpqDirs: []string{"etc/"}, + wantWhFiles: []string{"foo/foo"}, + }, + { + name: "skip file", + inputFile: filepath.Join("testdata", "test.tar"), + option: walker.Option{ + SkipFiles: []string{"/app/myweb/index.html"}, + }, + analyzeFn: func(filePath string, _ os.FileInfo, _ analyzer.Opener) error { + if filePath == "app/myweb/index.html" { + assert.Fail(t, "skip files error", "%s should be skipped", filePath) + } + return nil + }, + wantOpqDirs: []string{"etc/"}, + wantWhFiles: []string{"foo/foo"}, + }, + { + name: "skip dir", + inputFile: filepath.Join("testdata", "test.tar"), + option: walker.Option{ + SkipDirs: []string{"/app"}, + }, + analyzeFn: func(filePath string, _ os.FileInfo, _ analyzer.Opener) error { + if strings.HasPrefix(filePath, "app") { + assert.Fail(t, "skip dirs error", "%s should be skipped", filePath) + } + return nil + }, + wantOpqDirs: []string{"etc/"}, + wantWhFiles: []string{"foo/foo"}, + }, + { + name: "sad path", + inputFile: "testdata/test.tar", + analyzeFn: func(_ string, _ os.FileInfo, _ analyzer.Opener) error { + return errors.New("error") + }, + wantErr: "failed to analyze file", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open("testdata/test.tar") + require.NoError(t, err) + + w := walker.NewLayerTar(tt.option) + gotOpqDirs, gotWhFiles, err := w.Walk(f, tt.analyzeFn) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.wantOpqDirs, gotOpqDirs) + assert.Equal(t, tt.wantWhFiles, gotWhFiles) + }) + } +} diff --git a/pkg/fanal/walker/testdata/fs/app/myweb/test.txt b/pkg/fanal/walker/testdata/fs/app/myweb/test.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/fanal/walker/testdata/fs/bar b/pkg/fanal/walker/testdata/fs/bar new file mode 100644 index 000000000000..ba0e162e1c47 --- /dev/null +++ b/pkg/fanal/walker/testdata/fs/bar @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/pkg/fanal/walker/testdata/fs/sym.txt b/pkg/fanal/walker/testdata/fs/sym.txt new file mode 120000 index 000000000000..ba0e162e1c47 --- /dev/null +++ b/pkg/fanal/walker/testdata/fs/sym.txt @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/pkg/fanal/walker/testdata/test.tar b/pkg/fanal/walker/testdata/test.tar new file mode 100644 index 000000000000..72d5e6fce956 Binary files /dev/null and b/pkg/fanal/walker/testdata/test.tar differ diff --git a/pkg/fanal/walker/vm.go b/pkg/fanal/walker/vm.go new file mode 100644 index 000000000000..f8ef63ce0252 --- /dev/null +++ b/pkg/fanal/walker/vm.go @@ -0,0 +1,239 @@ +package walker + +import ( + "bytes" + "io" + "io/fs" + "path/filepath" + "slices" + "strings" + + "github.com/masahiro331/go-disk" + diskFs "github.com/masahiro331/go-disk/fs" + "github.com/masahiro331/go-disk/gpt" + "github.com/masahiro331/go-disk/mbr" + "github.com/masahiro331/go-disk/types" + "github.com/masahiro331/go-ext4-filesystem/ext4" + "github.com/masahiro331/go-xfs-filesystem/xfs" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/utils" + "github.com/aquasecurity/trivy/pkg/fanal/vm/filesystem" + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +var requiredDiskName = []string{ + "Linux", // AmazonLinux image name + "p.lxroot", // SLES image name + "primary", // Common image name + "0", // Common image name + "1", // Common image name + "2", // Common image name + "3", // Common image name +} + +var checkFsFuncs = []diskFs.CheckFsFunc{ + ext4.Check, + xfs.Check, +} + +func AppendPermitDiskName(s ...string) { + requiredDiskName = append(requiredDiskName, s...) +} + +type VM struct { + logger *log.Logger + skipFiles []string + skipDirs []string + analyzeFn WalkFunc +} + +func NewVM() *VM { + return &VM{ + logger: log.WithPrefix("vm"), + } +} + +func (w *VM) Walk(vreader *io.SectionReader, root string, opt Option, fn WalkFunc) error { + w.skipFiles = opt.SkipFiles + w.skipDirs = append(opt.SkipDirs, defaultSkipDirs...) + + // This function will be called on each file. + w.analyzeFn = fn + + driver, err := disk.NewDriver(vreader, checkFsFuncs...) + if err != nil { + return xerrors.Errorf("failed to new disk driver: %w", err) + } + + for { + partition, err := driver.Next() + if err != nil { + if err == io.EOF { + break + } + return xerrors.Errorf("failed to get a next partition: %w", err) + } + + // skip boot partition + if shouldSkip(partition) { + continue + } + + // Walk each partition + if err = w.diskWalk(root, partition); err != nil { + w.logger.Warn("Partition error", log.Err(err)) + } + } + return nil +} + +// Inject disk partitioning processes from externally with diskWalk. +func (w *VM) diskWalk(root string, partition types.Partition) error { + w.logger.Debug("Found partition", log.String("name", partition.Name())) + + sr := partition.GetSectionReader() + + // Trivy does not support LVM scanning. It is skipped at the moment. + foundLVM, err := w.detectLVM(sr) + if err != nil { + return xerrors.Errorf("LVM detection error: %w", err) + } else if foundLVM { + w.logger.Error("LVM is not supported, skipping", log.String("name", partition.Name()+".img")) + return nil + } + + // Auto-detect filesystem such as ext4 and xfs + fsys, clean, err := filesystem.New(sr) + if err != nil { + return xerrors.Errorf("filesystem error: %w", err) + } + defer clean() + + err = fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error { + // Walk filesystem + return w.fsWalk(fsys, path, d, err) + }) + if err != nil { + return xerrors.Errorf("filesystem walk error: %w", err) + } + return nil +} + +func (w *VM) fsWalk(fsys fs.FS, path string, d fs.DirEntry, err error) error { + if err != nil { + return xerrors.Errorf("fs.Walk error: %w", err) + } + fi, err := d.Info() + if err != nil { + return xerrors.Errorf("dir entry info error: %w", err) + } + pathName := strings.TrimPrefix(filepath.Clean(path), "/") + switch { + case fi.IsDir(): + if utils.SkipPath(pathName, w.skipDirs) { + return filepath.SkipDir + } + return nil + case !fi.Mode().IsRegular(): + return nil + case utils.SkipPath(pathName, w.skipFiles): + return nil + case fi.Mode()&0x1000 == 0x1000 || + fi.Mode()&0x2000 == 0x2000 || + fi.Mode()&0x6000 == 0x6000 || + fi.Mode()&0xA000 == 0xA000 || + fi.Mode()&0xc000 == 0xc000: + // 0x1000: S_IFIFO (FIFO) + // 0x2000: S_IFCHR (Character device) + // 0x6000: S_IFBLK (Block device) + // 0xA000: S_IFLNK (Symbolic link) + // 0xC000: S_IFSOCK (Socket) + return nil + } + + cvf := newCachedVMFile(fsys, pathName) + defer cvf.Clean() + + if err = w.analyzeFn(path, fi, cvf.Open); err != nil { + return xerrors.Errorf("failed to analyze file: %w", err) + } + return nil +} + +type cachedVMFile struct { + fs fs.FS + filePath string + + cf *cachedFile +} + +func newCachedVMFile(fsys fs.FS, filePath string) *cachedVMFile { + return &cachedVMFile{ + fs: fsys, + filePath: filePath, + } +} + +func (cvf *cachedVMFile) Open() (xio.ReadSeekCloserAt, error) { + if cvf.cf != nil { + return cvf.cf.Open() + } + + f, err := cvf.fs.Open(cvf.filePath) + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + fi, err := f.Stat() + if err != nil { + return nil, xerrors.Errorf("file stat error: %w", err) + } + + cvf.cf = newCachedFile(fi.Size(), f) + return cvf.cf.Open() +} + +func (cvf *cachedVMFile) Clean() error { + if cvf.cf == nil { + return nil + } + return cvf.cf.Clean() +} + +func (w *VM) detectLVM(sr io.SectionReader) (bool, error) { + buf := make([]byte, 512) + _, err := sr.ReadAt(buf, 512) + if err != nil { + return false, xerrors.Errorf("read header block error: %w", err) + } + _, err = sr.Seek(0, io.SeekStart) + if err != nil { + return false, xerrors.Errorf("seek error: %w", err) + } + + // LABELONE is LVM signature + if string(buf[:8]) == "LABELONE" { + return true, nil + } + return false, nil +} + +func shouldSkip(partition types.Partition) bool { + // skip empty partition + if bytes.Equal(partition.GetType(), []byte{0x00}) { + return true + } + + if !slices.Contains(requiredDiskName, partition.Name()) { + return true + } + + switch p := partition.(type) { + case *gpt.PartitionEntry: + return p.Bootable() + case *mbr.Partition: + return false + } + return false +} diff --git a/pkg/fanal/walker/walk.go b/pkg/fanal/walker/walk.go new file mode 100644 index 000000000000..0785cef41606 --- /dev/null +++ b/pkg/fanal/walker/walk.go @@ -0,0 +1,23 @@ +package walker + +import ( + "os" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" +) + +const defaultSizeThreshold = int64(100) << 20 // 200MB + +var defaultSkipDirs = []string{ + "**/.git", + "proc", + "sys", + "dev", +} + +type Option struct { + SkipFiles []string + SkipDirs []string +} + +type WalkFunc func(filePath string, info os.FileInfo, opener analyzer.Opener) error diff --git a/pkg/fanal/walker/walk_test.go b/pkg/fanal/walker/walk_test.go new file mode 100644 index 000000000000..3c066529393f --- /dev/null +++ b/pkg/fanal/walker/walk_test.go @@ -0,0 +1,151 @@ +package walker_test + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/fanal/utils" +) + +func TestSkipFile(t *testing.T) { + tests := []struct { + name string + skipFiles []string + wants map[string]bool + }{ + { + name: "single star", + skipFiles: []string{"/etc/*"}, + wants: map[string]bool{ + "/etc/foo": true, + "/etc/foo/bar": false, + }, + }, + { + name: "two stars", + skipFiles: []string{"/etc/*/*"}, + wants: map[string]bool{ + "/etc/foo": false, + "/etc/foo/bar": true, + }, + }, + { + name: "double star", + skipFiles: []string{"**/*.txt"}, + wants: map[string]bool{ + "/etc/foo": false, + "/etc/foo/bar": false, + "/var/log/bar.txt": true, + }, + }, + { + name: "multiple skip files", + skipFiles: []string{ + "/etc/*/*", + "/var/log/*.txt", + }, + wants: map[string]bool{ + "/etc/foo": false, + "/etc/foo/bar": true, + "/var/log/bar.txt": true, + }, + }, + { + name: "error bad pattern", + skipFiles: []string{`[^etc`}, // filepath.Match returns ErrBadPattern + wants: map[string]bool{ + "/etc/foo": false, + "/etc/foo/bar": false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for file, want := range tt.wants { + file = filepath.ToSlash(filepath.Clean(file)) + got := utils.SkipPath(file, utils.CleanSkipPaths(tt.skipFiles)) + assert.Equal(t, want, got, "skipFiles: %s, file: %s", tt.skipFiles, file) + } + }) + } +} + +func TestSkipDir(t *testing.T) { + tests := []struct { + name string + skipDirs []string + wants map[string]bool + }{ + { + name: "default skip dirs", + skipDirs: []string{ + "**/.git", + "proc", + "sys", + "dev", + }, + wants: map[string]bool{ + ".git": true, + "proc": true, + "foo.bar": false, + }, + }, + { + name: "single star", + skipDirs: []string{"/*"}, + wants: map[string]bool{ + "/etc": true, + "/etc/foo/bar": false, + }, + }, + { + name: "two stars", + skipDirs: []string{"/etc/*/*"}, + wants: map[string]bool{ + "/etc/foo": false, + "/etc/foo/bar": true, + }, + }, + { + name: "multiple dirs", + skipDirs: []string{ + "/etc/*/*", + "/var/log/*", + }, + wants: map[string]bool{ + "/etc/foo": false, + "/etc/foo/bar": true, + "/var/log/bar": true, + }, + }, + { + name: "double star", + skipDirs: []string{"**/.terraform"}, + wants: map[string]bool{ + ".terraform": true, + "test/foo/bar/.terraform": true, + }, + }, + { + name: "error bad pattern", + skipDirs: []string{`[^etc`}, // filepath.Match returns ErrBadPattern + wants: map[string]bool{ + "/etc/foo": false, + "/etc/foo/bar": false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for dir, want := range tt.wants { + dir = filepath.ToSlash(filepath.Clean(dir)) + got := utils.SkipPath(dir, utils.CleanSkipPaths(tt.skipDirs)) + assert.Equal(t, want, got, "defaultSkipDirs: %s, dir: %s", tt.skipDirs, dir) + } + }) + } +} diff --git a/pkg/fingerprint/fingerprint.go b/pkg/fingerprint/fingerprint.go new file mode 100644 index 000000000000..506025c6bfff --- /dev/null +++ b/pkg/fingerprint/fingerprint.go @@ -0,0 +1,57 @@ +package fingerprint + +import ( + "fmt" + + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Fill generates and fills fingerprints for all findings in the report +func Fill(report *types.Report) { + artifactID := report.ArtifactID + + for i := range report.Results { + result := &report.Results[i] + target := result.Target + + // Fill vulnerability fingerprints + fillVulnerabilities(artifactID, target, result.Vulnerabilities) + + // TODO: Future implementation + // fillMisconfigurations(artifactID, target, result.Misconfigurations) + // fillSecrets(artifactID, target, result.Secrets) + // fillLicenses(artifactID, target, result.Licenses) + } +} + +// fillVulnerabilities generates and assigns fingerprints to all vulnerabilities in the slice. +// Each vulnerability is processed in place to ensure the fingerprint is added to the original +// vulnerability object in the report. +func fillVulnerabilities(artifactID, target string, vulns []types.DetectedVulnerability) { + for i := range vulns { + vulns[i].Fingerprint = generateVulnFingerprint(artifactID, target, &vulns[i]) + } +} + +// generateVulnFingerprint creates a unique fingerprint for a vulnerability. +// The fingerprint is a SHA256 hash of the concatenation of: +// - artifact ID: Unique identifier for the scanned artifact (e.g., image digest with registry/repository) +// - target: Scan target path (e.g., "app/package.json" or "alpine 3.18.0") +// - package ID: Package identifier with version (e.g., "lodash@4.17.0" or "libssl3@3.0.8-r0") +// - vulnerability ID: CVE or vulnerability identifier (e.g., "CVE-2021-1234") +// +// The fingerprint is deterministic - the same inputs always produce the same hash. +// This allows external systems to track and deduplicate vulnerabilities across multiple scans. +// +// Example: For a vulnerability in lodash@4.17.0 found in app/package.json of sha256:abc123, +// the fingerprint would be SHA256("sha256:abc123:app/package.json:lodash@4.17.0:CVE-2021-1234") +// resulting in "sha256:..." format. +func generateVulnFingerprint(artifactID, target string, vuln *types.DetectedVulnerability) string { + data := fmt.Sprintf("%s:%s:%s:%s", + artifactID, + target, + vuln.PkgID, + vuln.VulnerabilityID) + return digest.CalcSHA256([]byte(data)).String() +} diff --git a/pkg/fingerprint/fingerprint_test.go b/pkg/fingerprint/fingerprint_test.go new file mode 100644 index 000000000000..14d0ac2bf52f --- /dev/null +++ b/pkg/fingerprint/fingerprint_test.go @@ -0,0 +1,281 @@ +package fingerprint + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestFill(t *testing.T) { + tests := []struct { + name string + report *types.Report + wantReport *types.Report + }{ + { + name: "single vulnerability", + report: &types.Report{ + ArtifactID: "sha256:abc123", + Results: []types.Result{ + { + Target: "app/package.json", + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + }, + }, + }, + }, + }, + wantReport: &types.Report{ + ArtifactID: "sha256:abc123", + Results: []types.Result{ + { + Target: "app/package.json", + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + Fingerprint: "sha256:7bf63097f9e930e203cfcb74b3ae9cf51c52cc016fa81da297a4d695dadd728f", // hash(sha256:abc123:app/package.json:lodash@4.17.0:CVE-2021-1234) + }, + }, + }, + }, + }, + }, + { + name: "multiple vulnerabilities in multiple results", + report: &types.Report{ + ArtifactID: "sha256:def456", + Results: []types.Result{ + { + Target: "app1/package.json", + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgID: "express@4.17.0", + VulnerabilityID: "CVE-2021-5678", + }, + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + }, + }, + }, + { + Target: "app2/package.json", + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + }, + }, + }, + }, + }, + wantReport: &types.Report{ + ArtifactID: "sha256:def456", + Results: []types.Result{ + { + Target: "app1/package.json", + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgID: "express@4.17.0", + VulnerabilityID: "CVE-2021-5678", + Fingerprint: "sha256:6666c513c31c1155541e73f806edccb717773e0839d2631e8758de34ed4bf9f7", // hash(sha256:def456:app1/package.json:express@4.17.0:CVE-2021-5678) + }, + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + Fingerprint: "sha256:c4d943760ae3cd4b8782bff29a44f67823b7579bae83e2abce1fdd2f11516527", // hash(sha256:def456:app1/package.json:lodash@4.17.0:CVE-2021-1234) + }, + }, + }, + { + Target: "app2/package.json", + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + Fingerprint: "sha256:1e0a807019aeeecabf004de2bb1bf22646752e7905be5a742377f4760d852520", // hash(sha256:def456:app2/package.json:lodash@4.17.0:CVE-2021-1234) + }, + }, + }, + }, + }, + }, + { + name: "empty report", + report: &types.Report{ + ArtifactID: "sha256:empty", + Results: []types.Result{}, + }, + wantReport: &types.Report{ + ArtifactID: "sha256:empty", + Results: []types.Result{}, + }, + }, + { + name: "result without vulnerabilities", + report: &types.Report{ + ArtifactID: "sha256:novulns", + Results: []types.Result{ + { + Target: "app/package.json", + Vulnerabilities: []types.DetectedVulnerability{}, + }, + }, + }, + wantReport: &types.Report{ + ArtifactID: "sha256:novulns", + Results: []types.Result{ + { + Target: "app/package.json", + Vulnerabilities: []types.DetectedVulnerability{}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Fill(tt.report) + + // Verify the entire report matches expected + assert.Equal(t, tt.wantReport, tt.report) + }) + } +} + +func Test_fillVulnerabilities(t *testing.T) { + tests := []struct { + name string + artifactID string + target string + vulns []types.DetectedVulnerability + wantVulns []types.DetectedVulnerability + }{ + { + name: "multiple vulnerabilities with unique fingerprints", + artifactID: "sha256:test123", + target: "test-target", + vulns: []types.DetectedVulnerability{ + { + PkgID: "pkg1@1.0.0", + VulnerabilityID: "CVE-2021-0001", + }, + { + PkgID: "pkg2@2.0.0", + VulnerabilityID: "CVE-2021-0002", + }, + { + PkgID: "pkg3@3.0.0", + VulnerabilityID: "CVE-2021-0003", + }, + }, + wantVulns: []types.DetectedVulnerability{ + { + PkgID: "pkg1@1.0.0", + VulnerabilityID: "CVE-2021-0001", + Fingerprint: "sha256:099e52439185c144012d07bff4d3e6a840b6d0366c175da12308fd7e82c49f4b", // hash(sha256:test123:test-target:pkg1@1.0.0:CVE-2021-0001) + }, + { + PkgID: "pkg2@2.0.0", + VulnerabilityID: "CVE-2021-0002", + Fingerprint: "sha256:7afdba1b87fb21abfac82517a4fbaf6a472761af1c2376405580040da998e3b9", // hash(sha256:test123:test-target:pkg2@2.0.0:CVE-2021-0002) + }, + { + PkgID: "pkg3@3.0.0", + VulnerabilityID: "CVE-2021-0003", + Fingerprint: "sha256:0eea75192d06f745c4a22e85159747984dd6f6014aeb5e341f4108b2bd12db18", // hash(sha256:test123:test-target:pkg3@3.0.0:CVE-2021-0003) + }, + }, + }, + { + name: "same vulnerability in different targets produces different fingerprints", + artifactID: "sha256:abc", + target: "app1/package.json", + vulns: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + }, + }, + wantVulns: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + Fingerprint: "sha256:f1616ffbc37b9762a217e36f926137a5dd1bcdfa203f5d8f8cd67b787dee969d", // hash(sha256:abc:app1/package.json:lodash@4.17.0:CVE-2021-1234) + }, + }, + }, + { + name: "same vulnerability different artifact produces different fingerprints", + artifactID: "sha256:xyz", + target: "app1/package.json", + vulns: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + }, + }, + wantVulns: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + Fingerprint: "sha256:2ec939f202e5187422d4bdfe2d9f5677d5caefab26713168e238f63d682700dd", // hash(sha256:xyz:app1/package.json:lodash@4.17.0:CVE-2021-1234) + }, + }, + }, + { + name: "same CVE different package version produces different fingerprints", + artifactID: "sha256:test", + target: "app/package.json", + vulns: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + }, + { + PkgID: "lodash@4.17.1", + VulnerabilityID: "CVE-2021-1234", + }, + }, + wantVulns: []types.DetectedVulnerability{ + { + PkgID: "lodash@4.17.0", + VulnerabilityID: "CVE-2021-1234", + Fingerprint: "sha256:31a2f37866b537085b9f9bb341e943f079b0c1457a30fc4864d9355e9c84bc72", // hash(sha256:test:app/package.json:lodash@4.17.0:CVE-2021-1234) + }, + { + PkgID: "lodash@4.17.1", + VulnerabilityID: "CVE-2021-1234", + Fingerprint: "sha256:b1efc0cce609f418597be82f5c9840c53ae8ae592ba1672c4f07297d86dbfe7b", // hash(sha256:test:app/package.json:lodash@4.17.1:CVE-2021-1234) + }, + }, + }, + { + name: "empty vulnerabilities", + artifactID: "sha256:empty", + target: "target", + vulns: []types.DetectedVulnerability{}, + wantVulns: []types.DetectedVulnerability{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fillVulnerabilities(tt.artifactID, tt.target, tt.vulns) + + require.Len(t, tt.vulns, len(tt.wantVulns)) + for i, vuln := range tt.vulns { + // Verify the entire vulnerability object matches expected + assert.Equal(t, tt.wantVulns[i], vuln, "vulnerability %d mismatch", i) + } + }) + } +} diff --git a/pkg/flag/aws_flags.go b/pkg/flag/aws_flags.go new file mode 100644 index 000000000000..f6b7c113932e --- /dev/null +++ b/pkg/flag/aws_flags.go @@ -0,0 +1,92 @@ +package flag + +var ( + awsRegionFlag = Flag[string]{ + Name: "region", + ConfigName: "cloud.aws.region", + Usage: "AWS Region to scan", + } + awsEndpointFlag = Flag[string]{ + Name: "endpoint", + ConfigName: "cloud.aws.endpoint", + Usage: "AWS Endpoint override", + } + awsServiceFlag = Flag[[]string]{ + Name: "service", + ConfigName: "cloud.aws.service", + Usage: "Only scan AWS Service(s) specified with this flag. Can specify multiple services using --service A --service B etc.", + TelemetrySafe: true, + } + awsSkipServicesFlag = Flag[[]string]{ + Name: "skip-service", + ConfigName: "cloud.aws.skip-service", + Usage: "Skip selected AWS Service(s) specified with this flag. Can specify multiple services using --skip-service A --skip-service B etc.", + TelemetrySafe: true, + } + awsAccountFlag = Flag[string]{ + Name: "account", + ConfigName: "cloud.aws.account", + Usage: "The AWS account to scan. It's useful to specify this when reviewing cached results for multiple accounts.", + } + awsARNFlag = Flag[string]{ + Name: "arn", + ConfigName: "cloud.aws.arn", + Usage: "The AWS ARN to show results for. Useful to filter results once a scan is cached.", + } +) + +type AWSFlagGroup struct { + Region *Flag[string] + Endpoint *Flag[string] + Services *Flag[[]string] + SkipServices *Flag[[]string] + Account *Flag[string] + ARN *Flag[string] +} + +type AWSOptions struct { + Region string + Endpoint string + Services []string + SkipServices []string + Account string + ARN string +} + +func NewAWSFlagGroup() *AWSFlagGroup { + return &AWSFlagGroup{ + Region: awsRegionFlag.Clone(), + Endpoint: awsEndpointFlag.Clone(), + Services: awsServiceFlag.Clone(), + SkipServices: awsSkipServicesFlag.Clone(), + Account: awsAccountFlag.Clone(), + ARN: awsARNFlag.Clone(), + } +} + +func (f *AWSFlagGroup) Name() string { + return "AWS" +} + +func (f *AWSFlagGroup) Flags() []Flagger { + return []Flagger{ + f.Region, + f.Endpoint, + f.Services, + f.SkipServices, + f.Account, + f.ARN, + } +} + +func (f *AWSFlagGroup) ToOptions(opts *Options) error { + opts.AWSOptions = AWSOptions{ + Region: f.Region.Value(), + Endpoint: f.Endpoint.Value(), + Services: f.Services.Value(), + SkipServices: f.SkipServices.Value(), + Account: f.Account.Value(), + ARN: f.ARN.Value(), + } + return nil +} diff --git a/pkg/flag/cache_flags.go b/pkg/flag/cache_flags.go new file mode 100644 index 000000000000..1b96ead71d17 --- /dev/null +++ b/pkg/flag/cache_flags.go @@ -0,0 +1,122 @@ +package flag + +import ( + "time" +) + +// e.g. config yaml: +// +// cache: +// clear: true +// backend: "redis://localhost:6379" +// redis: +// ca: ca-cert.pem +// cert: cert.pem +// key: key.pem +var ( + // Deprecated + ClearCacheFlag = Flag[bool]{ + Name: "clear-cache", + ConfigName: "cache.clear", + Usage: "clear image caches without scanning", + Removed: `Use "trivy clean --scan-cache" instead`, + TelemetrySafe: true, + } + CacheBackendFlag = Flag[string]{ + Name: "cache-backend", + ConfigName: "cache.backend", + Default: "fs", + Usage: "[EXPERIMENTAL] cache backend (e.g. redis://localhost:6379)", + } + CacheTTLFlag = Flag[time.Duration]{ + Name: "cache-ttl", + ConfigName: "cache.ttl", + Usage: "cache TTL when using redis as cache backend", + } + RedisTLSFlag = Flag[bool]{ + Name: "redis-tls", + ConfigName: "cache.redis.tls", + Usage: "enable redis TLS with public certificates, if using redis as cache backend", + TelemetrySafe: true, + } + RedisCACertFlag = Flag[string]{ + Name: "redis-ca", + ConfigName: "cache.redis.ca", + Usage: "redis ca file location, if using redis as cache backend", + } + RedisCertFlag = Flag[string]{ + Name: "redis-cert", + ConfigName: "cache.redis.cert", + Usage: "redis certificate file location, if using redis as cache backend", + } + RedisKeyFlag = Flag[string]{ + Name: "redis-key", + ConfigName: "cache.redis.key", + Usage: "redis key file location, if using redis as cache backend", + } +) + +// CacheFlagGroup composes common printer flag structs used for commands requiring cache logic. +type CacheFlagGroup struct { + ClearCache *Flag[bool] + CacheBackend *Flag[string] + CacheTTL *Flag[time.Duration] + + RedisTLS *Flag[bool] + RedisCACert *Flag[string] + RedisCert *Flag[string] + RedisKey *Flag[string] +} + +type CacheOptions struct { + ClearCache bool + + CacheBackend string + CacheTTL time.Duration + RedisTLS bool + RedisCACert string + RedisCert string + RedisKey string +} + +// NewCacheFlagGroup returns a default CacheFlagGroup +func NewCacheFlagGroup() *CacheFlagGroup { + return &CacheFlagGroup{ + ClearCache: ClearCacheFlag.Clone(), + CacheBackend: CacheBackendFlag.Clone(), + CacheTTL: CacheTTLFlag.Clone(), + RedisTLS: RedisTLSFlag.Clone(), + RedisCACert: RedisCACertFlag.Clone(), + RedisCert: RedisCertFlag.Clone(), + RedisKey: RedisKeyFlag.Clone(), + } +} + +func (fg *CacheFlagGroup) Name() string { + return "Cache" +} + +func (fg *CacheFlagGroup) Flags() []Flagger { + return []Flagger{ + fg.ClearCache, + fg.CacheBackend, + fg.CacheTTL, + fg.RedisTLS, + fg.RedisCACert, + fg.RedisCert, + fg.RedisKey, + } +} + +func (fg *CacheFlagGroup) ToOptions(opts *Options) error { + opts.CacheOptions = CacheOptions{ + ClearCache: fg.ClearCache.Value(), + CacheBackend: fg.CacheBackend.Value(), + CacheTTL: fg.CacheTTL.Value(), + RedisTLS: fg.RedisTLS.Value(), + RedisCACert: fg.RedisCACert.Value(), + RedisCert: fg.RedisCert.Value(), + RedisKey: fg.RedisKey.Value(), + } + return nil +} diff --git a/pkg/flag/clean_flags.go b/pkg/flag/clean_flags.go new file mode 100644 index 000000000000..40b9c99f24b4 --- /dev/null +++ b/pkg/flag/clean_flags.go @@ -0,0 +1,91 @@ +package flag + +var ( + CleanAll = Flag[bool]{ + Name: "all", + Shorthand: "a", + ConfigName: "clean.all", + Usage: "remove all caches", + } + CleanScanCache = Flag[bool]{ + Name: "scan-cache", + ConfigName: "clean.scan-cache", + Usage: "remove scan cache (container and VM image analysis results)", + } + CleanVulnerabilityDB = Flag[bool]{ + Name: "vuln-db", + ConfigName: "clean.vuln-db", + Usage: "remove vulnerability database", + } + CleanJavaDB = Flag[bool]{ + Name: "java-db", + ConfigName: "clean.java-db", + Usage: "remove Java database", + } + CleanChecksBundle = Flag[bool]{ + Name: "checks-bundle", + ConfigName: "clean.checks-bundle", + Usage: "remove checks bundle", + } + CleanVEXRepo = Flag[bool]{ + Name: "vex-repo", + ConfigName: "clean.vex-repo", + Usage: "remove VEX repositories", + } +) + +type CleanFlagGroup struct { + CleanAll *Flag[bool] + CleanScanCache *Flag[bool] + CleanVulnerabilityDB *Flag[bool] + CleanJavaDB *Flag[bool] + CleanChecksBundle *Flag[bool] + CleanVEXRepositories *Flag[bool] +} + +type CleanOptions struct { + CleanAll bool + CleanScanCache bool + CleanVulnerabilityDB bool + CleanJavaDB bool + CleanChecksBundle bool + CleanVEXRepositories bool +} + +func NewCleanFlagGroup() *CleanFlagGroup { + return &CleanFlagGroup{ + CleanAll: CleanAll.Clone(), + CleanScanCache: CleanScanCache.Clone(), + CleanVulnerabilityDB: CleanVulnerabilityDB.Clone(), + CleanJavaDB: CleanJavaDB.Clone(), + CleanChecksBundle: CleanChecksBundle.Clone(), + CleanVEXRepositories: CleanVEXRepo.Clone(), + } +} + +func (fg *CleanFlagGroup) Name() string { + return "Clean" +} + +func (fg *CleanFlagGroup) Flags() []Flagger { + return []Flagger{ + fg.CleanAll, + fg.CleanScanCache, + fg.CleanVulnerabilityDB, + fg.CleanJavaDB, + fg.CleanChecksBundle, + fg.CleanVEXRepositories, + } +} + +func (fg *CleanFlagGroup) ToOptions(opts *Options) error { + opts.CleanOptions = CleanOptions{ + CleanAll: fg.CleanAll.Value(), + CleanVulnerabilityDB: fg.CleanVulnerabilityDB.Value(), + CleanJavaDB: fg.CleanJavaDB.Value(), + CleanChecksBundle: fg.CleanChecksBundle.Value(), + CleanScanCache: fg.CleanScanCache.Value(), + CleanVEXRepositories: fg.CleanVEXRepositories.Value(), + } + return nil +} diff --git a/pkg/flag/db_flags.go b/pkg/flag/db_flags.go new file mode 100644 index 000000000000..3b00f8a5f994 --- /dev/null +++ b/pkg/flag/db_flags.go @@ -0,0 +1,193 @@ +package flag + +import ( + "strconv" + + "github.com/google/go-containerregistry/pkg/name" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/javadb" + "github.com/aquasecurity/trivy/pkg/log" +) + +var ( + // Deprecated + ResetFlag = Flag[bool]{ + Name: "reset", + ConfigName: "reset", + Usage: "remove all caches and database", + Removed: `Use "trivy clean --all" instead.`, + } + DownloadDBOnlyFlag = Flag[bool]{ + Name: "download-db-only", + ConfigName: "db.download-only", + Usage: "download/update vulnerability database but don't run a scan", + } + SkipDBUpdateFlag = Flag[bool]{ + Name: "skip-db-update", + ConfigName: "db.skip-update", + Usage: "skip updating vulnerability database", + Aliases: []Alias{ + { + Name: "skip-update", + Deprecated: true, // --skip-update was renamed to --skip-db-update + }, + }, + } + DownloadJavaDBOnlyFlag = Flag[bool]{ + Name: "download-java-db-only", + ConfigName: "db.download-java-only", + Usage: "download/update Java index database but don't run a scan", + } + SkipJavaDBUpdateFlag = Flag[bool]{ + Name: "skip-java-db-update", + ConfigName: "db.java-skip-update", + Usage: "skip updating Java index database", + } + NoProgressFlag = Flag[bool]{ + Name: "no-progress", + ConfigName: "db.no-progress", + Usage: "suppress progress bar", + } + DBRepositoryFlag = Flag[[]string]{ + Name: "db-repository", + ConfigName: "db.repository", + Default: []string{db.DefaultGCRRepository, db.DefaultGHCRRepository}, + Usage: "OCI repository(ies) to retrieve trivy-db in order of priority", + } + JavaDBRepositoryFlag = Flag[[]string]{ + Name: "java-db-repository", + ConfigName: "db.java-repository", + Default: []string{javadb.DefaultGCRRepository, javadb.DefaultGHCRRepository}, + Usage: "OCI repository(ies) to retrieve trivy-java-db in order of priority", + } + LightFlag = Flag[bool]{ + Name: "light", + ConfigName: "db.light", + Usage: "deprecated", + Deprecated: `This flag is ignored.`, + } +) + +// DBFlagGroup composes common printer flag structs used for commands requiring DB logic. +type DBFlagGroup struct { + Reset *Flag[bool] + DownloadDBOnly *Flag[bool] + SkipDBUpdate *Flag[bool] + DownloadJavaDBOnly *Flag[bool] + SkipJavaDBUpdate *Flag[bool] + NoProgress *Flag[bool] + DBRepositories *Flag[[]string] + JavaDBRepositories *Flag[[]string] + Light *Flag[bool] // deprecated +} + +type DBOptions struct { + Reset bool + DownloadDBOnly bool + SkipDBUpdate bool + DownloadJavaDBOnly bool + SkipJavaDBUpdate bool + NoProgress bool + DBRepositories []name.Reference + JavaDBRepositories []name.Reference +} + +// NewDBFlagGroup returns a default DBFlagGroup +func NewDBFlagGroup() *DBFlagGroup { + return &DBFlagGroup{ + Reset: ResetFlag.Clone(), + DownloadDBOnly: DownloadDBOnlyFlag.Clone(), + SkipDBUpdate: SkipDBUpdateFlag.Clone(), + DownloadJavaDBOnly: DownloadJavaDBOnlyFlag.Clone(), + SkipJavaDBUpdate: SkipJavaDBUpdateFlag.Clone(), + Light: LightFlag.Clone(), + NoProgress: NoProgressFlag.Clone(), + DBRepositories: DBRepositoryFlag.Clone(), + JavaDBRepositories: JavaDBRepositoryFlag.Clone(), + } +} + +func (f *DBFlagGroup) Name() string { + return "DB" +} + +func (f *DBFlagGroup) Flags() []Flagger { + return []Flagger{ + f.Reset, + f.DownloadDBOnly, + f.SkipDBUpdate, + f.DownloadJavaDBOnly, + f.SkipJavaDBUpdate, + f.NoProgress, + f.DBRepositories, + f.JavaDBRepositories, + f.Light, + } +} + +func (f *DBFlagGroup) ToOptions(opts *Options) error { + skipDBUpdate := f.SkipDBUpdate.Value() + skipJavaDBUpdate := f.SkipJavaDBUpdate.Value() + downloadDBOnly := f.DownloadDBOnly.Value() + downloadJavaDBOnly := f.DownloadJavaDBOnly.Value() + + if downloadDBOnly && downloadJavaDBOnly { + return xerrors.New("--download-db-only and --download-java-db-only options can not be specified both") + } + if downloadDBOnly && skipDBUpdate { + return xerrors.New("--skip-db-update and --download-db-only options can not be specified both") + } + if downloadJavaDBOnly && skipJavaDBUpdate { + return xerrors.New("--skip-java-db-update and --download-java-db-only options can not be specified both") + } + + var dbRepositories, javaDBRepositories []name.Reference + for _, repo := range f.DBRepositories.Value() { + ref, err := parseRepository(repo, db.SchemaVersion) + if err != nil { + return xerrors.Errorf("invalid DB repository: %w", err) + } + dbRepositories = append(dbRepositories, ref) + } + + for _, repo := range f.JavaDBRepositories.Value() { + ref, err := parseRepository(repo, javadb.SchemaVersion) + if err != nil { + return xerrors.Errorf("invalid javadb repository: %w", err) + } + javaDBRepositories = append(javaDBRepositories, ref) + } + + opts.DBOptions = DBOptions{ + Reset: f.Reset.Value(), + DownloadDBOnly: downloadDBOnly, + SkipDBUpdate: skipDBUpdate, + DownloadJavaDBOnly: downloadJavaDBOnly, + SkipJavaDBUpdate: skipJavaDBUpdate, + NoProgress: f.NoProgress.Value(), + DBRepositories: dbRepositories, + JavaDBRepositories: javaDBRepositories, + } + return nil +} + +func parseRepository(repo string, dbSchemaVersion int) (name.Reference, error) { + dbRepository, err := name.ParseReference(repo, name.WithDefaultTag("")) + if err != nil { + return nil, err + } + + // Add the schema version if the tag is not specified for backward compatibility. + t, ok := dbRepository.(name.Tag) + if !ok || t.TagStr() != "" { + return dbRepository, nil + } + + dbRepository = t.Tag(strconv.Itoa(dbSchemaVersion)) + log.Info("Adding schema version to the DB repository for backward compatibility", + log.String("repository", dbRepository.String())) + + return dbRepository, nil +} diff --git a/pkg/flag/db_flags_test.go b/pkg/flag/db_flags_test.go new file mode 100644 index 000000000000..2b8fae9c2193 --- /dev/null +++ b/pkg/flag/db_flags_test.go @@ -0,0 +1,118 @@ +package flag_test + +import ( + "testing" + + "github.com/google/go-containerregistry/pkg/name" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestDBFlagGroup_ToOptions(t *testing.T) { + type fields struct { + SkipDBUpdate bool + DownloadDBOnly bool + Light bool + DBRepository []string + JavaDBRepository []string + } + tests := []struct { + name string + fields fields + want flag.DBOptions + wantLogs []string + wantErr string + }{ + { + name: "happy", + fields: fields{ + SkipDBUpdate: true, + DownloadDBOnly: false, + DBRepository: []string{"ghcr.io/aquasecurity/trivy-db"}, + JavaDBRepository: []string{"ghcr.io/aquasecurity/trivy-java-db"}, + }, + want: flag.DBOptions{ + SkipDBUpdate: true, + DownloadDBOnly: false, + DBRepositories: []name.Reference{name.Tag{}}, // All fields are unexported + JavaDBRepositories: []name.Reference{name.Tag{}}, // All fields are unexported + }, + wantLogs: []string{ + `Adding schema version to the DB repository for backward compatibility repository="ghcr.io/aquasecurity/trivy-db:2"`, + `Adding schema version to the DB repository for backward compatibility repository="ghcr.io/aquasecurity/trivy-java-db:1"`, + }, + }, + { + name: "sad", + fields: fields{ + SkipDBUpdate: true, + DownloadDBOnly: true, + }, + wantErr: "--skip-db-update and --download-db-only options can not be specified both", + }, + { + name: "invalid repo", + fields: fields{ + SkipDBUpdate: true, + DownloadDBOnly: false, + DBRepository: []string{"foo:bar:baz"}, + }, + wantErr: "invalid DB repository", + }, + { + name: "multiple repos", + fields: fields{ + SkipDBUpdate: true, + DownloadDBOnly: false, + DBRepository: []string{ + "mirror.gcr.io/aquasec/trivy-db:2", + "ghcr.io/aquasecurity/trivy-db:2", + }, + JavaDBRepository: []string{ + "mirror.gcr.io/aquasec/trivy-java-db:1", + "ghcr.io/aquasecurity/trivy-java-db:1", + }, + }, + want: flag.DBOptions{ + SkipDBUpdate: true, + DownloadDBOnly: false, + DBRepositories: []name.Reference{name.Tag{}, name.Tag{}}, // All fields are unexported + JavaDBRepositories: []name.Reference{name.Tag{}, name.Tag{}}, // All fields are unexported + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := newLogger(log.LevelInfo) + + viper.Set(flag.SkipDBUpdateFlag.ConfigName, tt.fields.SkipDBUpdate) + viper.Set(flag.DownloadDBOnlyFlag.ConfigName, tt.fields.DownloadDBOnly) + viper.Set(flag.DBRepositoryFlag.ConfigName, tt.fields.DBRepository) + viper.Set(flag.JavaDBRepositoryFlag.ConfigName, tt.fields.JavaDBRepository) + + // Assert options + f := &flag.DBFlagGroup{ + DownloadDBOnly: flag.DownloadDBOnlyFlag.Clone(), + SkipDBUpdate: flag.SkipDBUpdateFlag.Clone(), + DBRepositories: flag.DBRepositoryFlag.Clone(), + JavaDBRepositories: flag.JavaDBRepositoryFlag.Clone(), + } + flags := flag.Flags{f} + got, err := flags.ToOptions(nil) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + assert.EqualExportedValues(t, tt.want, got.DBOptions) + + // Assert log messages + assert.Equal(t, tt.wantLogs, out.Messages(), tt.name) + }) + } +} diff --git a/pkg/flag/global_flags.go b/pkg/flag/global_flags.go new file mode 100644 index 000000000000..954e0cfe7880 --- /dev/null +++ b/pkg/flag/global_flags.go @@ -0,0 +1,213 @@ +package flag + +import ( + "crypto/x509" + "os" + "time" + + "github.com/spf13/cobra" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/log" +) + +var ( + ConfigFileFlag = Flag[string]{ + Name: "config", + ConfigName: "config", + Shorthand: "c", + Default: "trivy.yaml", + Usage: "config path", + Persistent: true, + } + ShowVersionFlag = Flag[bool]{ + Name: "version", + ConfigName: "version", + Shorthand: "v", + Usage: "show version", + Persistent: true, + } + QuietFlag = Flag[bool]{ + Name: "quiet", + ConfigName: "quiet", + Shorthand: "q", + Usage: "suppress progress bar and log output", + Persistent: true, + TelemetrySafe: true, + } + DebugFlag = Flag[bool]{ + Name: "debug", + ConfigName: "debug", + Shorthand: "d", + Usage: "debug mode", + Persistent: true, + TelemetrySafe: true, + } + InsecureFlag = Flag[bool]{ + Name: "insecure", + ConfigName: "insecure", + Usage: "allow insecure server connections", + Persistent: true, + TelemetrySafe: true, + } + CACertFlag = Flag[string]{ + Name: "cacert", + ConfigName: "cacert", + Usage: "Path to PEM-encoded CA certificate file", + Persistent: true, + } + TimeoutFlag = Flag[time.Duration]{ + Name: "timeout", + ConfigName: "timeout", + Default: time.Second * 300, // 5 mins + Usage: "timeout", + Persistent: true, + TelemetrySafe: true, + } + CacheDirFlag = Flag[string]{ + Name: "cache-dir", + ConfigName: "cache.dir", + Default: cache.DefaultDir(), + Usage: "cache directory", + Persistent: true, + } + GenerateDefaultConfigFlag = Flag[bool]{ + Name: "generate-default-config", + ConfigName: "generate-default-config", + Usage: "write the default config to trivy-default.yaml", + Persistent: true, + } + TraceHTTPFlag = Flag[bool]{ + Name: "trace-http", + ConfigName: "trace.http", + Usage: "[DANGEROUS] enable HTTP request/response trace logging (may expose sensitive data)", + Persistent: true, + TelemetrySafe: true, + Internal: true, // Hidden from help output, intended for maintainer debugging only + } +) + +// GlobalFlagGroup composes global flags +type GlobalFlagGroup struct { + ConfigFile *Flag[string] + ShowVersion *Flag[bool] // spf13/cobra can't override the logic of version printing like VersionPrinter in urfave/cli. -v needs to be defined ourselves. + Quiet *Flag[bool] + Debug *Flag[bool] + Insecure *Flag[bool] + CACert *Flag[string] + Timeout *Flag[time.Duration] + CacheDir *Flag[string] + GenerateDefaultConfig *Flag[bool] + TraceHTTP *Flag[bool] +} + +// GlobalOptions defines flags and other configuration parameters for all the subcommands +type GlobalOptions struct { + ConfigFile string + ShowVersion bool + Quiet bool + Debug bool + Insecure bool + CACerts *x509.CertPool + Timeout time.Duration + CacheDir string + GenerateDefaultConfig bool + TraceHTTP bool +} + +func NewGlobalFlagGroup() *GlobalFlagGroup { + return &GlobalFlagGroup{ + ConfigFile: ConfigFileFlag.Clone(), + ShowVersion: ShowVersionFlag.Clone(), + Quiet: QuietFlag.Clone(), + Debug: DebugFlag.Clone(), + Insecure: InsecureFlag.Clone(), + CACert: CACertFlag.Clone(), + Timeout: TimeoutFlag.Clone(), + CacheDir: CacheDirFlag.Clone(), + GenerateDefaultConfig: GenerateDefaultConfigFlag.Clone(), + TraceHTTP: TraceHTTPFlag.Clone(), + } +} + +func (f *GlobalFlagGroup) Name() string { + return "Global" +} + +func (f *GlobalFlagGroup) Flags() []Flagger { + return []Flagger{ + f.ConfigFile, + f.ShowVersion, + f.Quiet, + f.Debug, + f.Insecure, + f.CACert, + f.Timeout, + f.CacheDir, + f.GenerateDefaultConfig, + f.TraceHTTP, + } +} + +func (f *GlobalFlagGroup) AddFlags(cmd *cobra.Command) { + for _, flag := range f.Flags() { + flag.Add(cmd) + } +} + +func (f *GlobalFlagGroup) Bind(cmd *cobra.Command) error { + for _, flag := range f.Flags() { + if err := flag.Bind(cmd); err != nil { + return err + } + } + return nil +} + +func (f *GlobalFlagGroup) ToOptions(opts *Options) error { + // Keep TRIVY_NON_SSL for backward compatibility + insecure := f.Insecure.Value() || os.Getenv("TRIVY_NON_SSL") != "" + caCerts, err := loadRootCAs(f.CACert.Value()) + if err != nil { + return xerrors.Errorf("failed to load root CA certificates: %w", err) + } + + log.Debug("Cache dir", log.String("dir", f.CacheDir.Value())) + + opts.GlobalOptions = GlobalOptions{ + ConfigFile: f.ConfigFile.Value(), + ShowVersion: f.ShowVersion.Value(), + Quiet: f.Quiet.Value(), + Debug: f.Debug.Value(), + Insecure: insecure, + CACerts: caCerts, + Timeout: f.Timeout.Value(), + CacheDir: f.CacheDir.Value(), + GenerateDefaultConfig: f.GenerateDefaultConfig.Value(), + TraceHTTP: f.TraceHTTP.Value(), + } + return nil +} + +// loadRootCAs builds a cert pool from the system pool and the provided PEM bundle. +// Returns nil if caCertPath is empty or on failure. +func loadRootCAs(caCertPath string) (*x509.CertPool, error) { + if caCertPath == "" { + return nil, nil + } + + rootCAs, err := x509.SystemCertPool() + if err != nil || rootCAs == nil { + rootCAs = x509.NewCertPool() + } + + pem, err := os.ReadFile(caCertPath) + if err != nil { + return nil, xerrors.Errorf("failed to read root CA certificate: %w", err) + } + if ok := rootCAs.AppendCertsFromPEM(pem); !ok { + return nil, xerrors.Errorf("failed to append CA bundle") + } + return rootCAs, nil +} diff --git a/pkg/flag/image_flags.go b/pkg/flag/image_flags.go new file mode 100644 index 000000000000..353e10c227b6 --- /dev/null +++ b/pkg/flag/image_flags.go @@ -0,0 +1,156 @@ +package flag + +import ( + "github.com/docker/go-units" + v1 "github.com/google/go-containerregistry/pkg/v1" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" +) + +// e.g. config yaml +// image: +// removed-pkgs: true +// input: "/path/to/alpine" + +var ( + ImageConfigScannersFlag = Flag[[]string]{ + Name: "image-config-scanners", + ConfigName: "image.image-config-scanners", + Values: xstrings.ToStringSlice(types.Scanners{ + types.MisconfigScanner, + types.SecretScanner, + }), + Usage: "comma-separated list of what security issues to detect on container image configurations", + TelemetrySafe: true, + } + ScanRemovedPkgsFlag = Flag[bool]{ + Name: "removed-pkgs", + ConfigName: "image.removed-pkgs", + Usage: "detect vulnerabilities of removed packages (only for Alpine)", + TelemetrySafe: true, + } + InputFlag = Flag[string]{ + Name: "input", + ConfigName: "image.input", + Usage: "input file path instead of image name", + } + PlatformFlag = Flag[string]{ + Name: "platform", + ConfigName: "image.platform", + Usage: "set platform in the form os/arch if image is multi-platform capable", + } + DockerHostFlag = Flag[string]{ + Name: "docker-host", + ConfigName: "image.docker.host", + Default: "", + Usage: "unix domain socket path to use for docker scanning", + } + PodmanHostFlag = Flag[string]{ + Name: "podman-host", + ConfigName: "image.podman.host", + Default: "", + Usage: "unix podman socket path to use for podman scanning", + } + SourceFlag = Flag[[]string]{ + Name: "image-src", + ConfigName: "image.source", + Default: xstrings.ToStringSlice(ftypes.AllImageSources), + Values: xstrings.ToStringSlice(ftypes.AllImageSources), + Usage: "image source(s) to use, in priority order", + } + MaxImageSize = Flag[string]{ + Name: "max-image-size", + ConfigName: "image.max-size", + Default: "", + Usage: "[EXPERIMENTAL] maximum image size to process, specified in a human-readable format (e.g., '44kB', '17MB'); an error will be returned if the image exceeds this size", + } +) + +type ImageFlagGroup struct { + Input *Flag[string] // local image archive + ImageConfigScanners *Flag[[]string] + ScanRemovedPkgs *Flag[bool] + Platform *Flag[string] + DockerHost *Flag[string] + PodmanHost *Flag[string] + ImageSources *Flag[[]string] + MaxImageSize *Flag[string] +} + +type ImageOptions struct { + Input string + ImageConfigScanners types.Scanners + ScanRemovedPkgs bool + Platform ftypes.Platform + DockerHost string + PodmanHost string + ImageSources ftypes.ImageSources + MaxImageSize int64 +} + +func NewImageFlagGroup() *ImageFlagGroup { + return &ImageFlagGroup{ + Input: InputFlag.Clone(), + ImageConfigScanners: ImageConfigScannersFlag.Clone(), + ScanRemovedPkgs: ScanRemovedPkgsFlag.Clone(), + Platform: PlatformFlag.Clone(), + DockerHost: DockerHostFlag.Clone(), + PodmanHost: PodmanHostFlag.Clone(), + ImageSources: SourceFlag.Clone(), + MaxImageSize: MaxImageSize.Clone(), + } +} + +func (f *ImageFlagGroup) Name() string { + return "Image" +} + +func (f *ImageFlagGroup) Flags() []Flagger { + return []Flagger{ + f.Input, + f.ImageConfigScanners, + f.ScanRemovedPkgs, + f.Platform, + f.DockerHost, + f.PodmanHost, + f.ImageSources, + f.MaxImageSize, + } +} + +func (f *ImageFlagGroup) ToOptions(opts *Options) error { + var platform ftypes.Platform + if p := f.Platform.Value(); p != "" { + pl, err := v1.ParsePlatform(p) + if err != nil { + return xerrors.Errorf("unable to parse platform: %w", err) + } + if pl.OS == "*" { + pl.OS = "" // Empty OS means any OS + } + platform = ftypes.Platform{Platform: pl} + } + var maxSize int64 + if value := f.MaxImageSize.Value(); value != "" { + parsedSize, err := units.FromHumanSize(value) + if err != nil { + return xerrors.Errorf("invalid max image size %q: %w", value, err) + } + maxSize = parsedSize + } + + opts.ImageOptions = ImageOptions{ + Input: f.Input.Value(), + ImageConfigScanners: xstrings.ToTSlice[types.Scanner](f.ImageConfigScanners.Value()), + ScanRemovedPkgs: f.ScanRemovedPkgs.Value(), + Platform: platform, + DockerHost: f.DockerHost.Value(), + PodmanHost: f.PodmanHost.Value(), + ImageSources: xstrings.ToTSlice[ftypes.ImageSource](f.ImageSources.Value()), + MaxImageSize: maxSize, + } + return nil +} diff --git a/pkg/flag/image_flags_test.go b/pkg/flag/image_flags_test.go new file mode 100644 index 000000000000..d2bbe03ea575 --- /dev/null +++ b/pkg/flag/image_flags_test.go @@ -0,0 +1,92 @@ +package flag_test + +import ( + "testing" + + "github.com/docker/go-units" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/flag" +) + +func TestImageFlagGroup_ToOptions(t *testing.T) { + type fields struct { + maxImgSize string + platform string + } + tests := []struct { + name string + fields fields + want flag.ImageOptions + wantErr string + }{ + { + name: "happy default (without flags)", + fields: fields{}, + want: flag.ImageOptions{}, + }, + { + name: "happy path with max image size", + fields: fields{ + maxImgSize: "10mb", + }, + want: flag.ImageOptions{ + MaxImageSize: units.MB * 10, + }, + }, + { + name: "invalid max image size", + fields: fields{ + maxImgSize: "10foo", + }, + wantErr: "invalid max image size", + }, + { + name: "happy path with platform", + fields: fields{ + platform: "linux/amd64", + }, + want: flag.ImageOptions{ + Platform: types.Platform{ + Platform: &v1.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + }, + }, + { + name: "invalid platform", + fields: fields{ + platform: "unknown/unknown/unknown/unknown", + }, + wantErr: "unable to parse platform", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(viper.Reset) + + setValue(flag.MaxImageSize.ConfigName, tt.fields.maxImgSize) + setValue(flag.PlatformFlag.ConfigName, tt.fields.platform) + + f := &flag.ImageFlagGroup{ + MaxImageSize: flag.MaxImageSize.Clone(), + Platform: flag.PlatformFlag.Clone(), + } + + flags := flag.Flags{f} + got, err := flags.ToOptions(nil) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.EqualExportedValues(t, tt.want, got.ImageOptions) + }) + } +} diff --git a/pkg/flag/kubernetes_flags.go b/pkg/flag/kubernetes_flags.go new file mode 100644 index 000000000000..7ef328b9090b --- /dev/null +++ b/pkg/flag/kubernetes_flags.go @@ -0,0 +1,253 @@ +package flag + +import ( + "errors" + "strconv" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + corev1 "k8s.io/api/core/v1" +) + +var ( + KubeConfigFlag = Flag[string]{ + Name: "kubeconfig", + ConfigName: "kubernetes.kubeconfig", + Usage: "specify the kubeconfig file path to use", + } + K8sVersionFlag = Flag[string]{ + Name: "k8s-version", + ConfigName: "kubernetes.k8s-version", + Usage: "specify k8s version to validate outdated api by it (example: 1.21.0)", + } + TolerationsFlag = Flag[[]string]{ + Name: "tolerations", + ConfigName: "kubernetes.tolerations", + Usage: "specify node-collector job tolerations (example: key1=value1:NoExecute,key2=value2:NoSchedule)", + } + DisableNodeCollector = Flag[bool]{ + Name: "disable-node-collector", + ConfigName: "kubernetes.disableNodeCollector", + Usage: "When the flag is activated, the node-collector job will not be executed, thus skipping misconfiguration findings on the node.", + } + NodeCollectorNamespace = Flag[string]{ + Name: "node-collector-namespace", + ConfigName: "kubernetes.node-collector.namespace", + Default: "trivy-temp", + Usage: "specify the namespace in which the node-collector job should be deployed", + } + NodeCollectorImageRef = Flag[string]{ + Name: "node-collector-imageref", + ConfigName: "kubernetes.node-collector.imageref", + Default: "ghcr.io/aquasecurity/node-collector:0.3.1", + Usage: "indicate the image reference for the node-collector scan job", + } + ExcludeOwned = Flag[bool]{ + Name: "exclude-owned", + ConfigName: "kubernetes.exclude.owned", + Usage: "exclude resources that have an owner reference", + } + SkipImages = Flag[bool]{ + Name: "skip-images", + ConfigName: "kubernetes.skipImages", + Usage: "skip the downloading and scanning of images (vulnerabilities and secrets) in the cluster resources", + } + ExcludeNodes = Flag[[]string]{ + Name: "exclude-nodes", + ConfigName: "kubernetes.exclude.nodes", + Usage: "indicate the node labels that the node-collector job should exclude from scanning (example: kubernetes.io/arch:arm64,team:dev)", + } + + ExcludeKinds = Flag[[]string]{ + Name: "exclude-kinds", + ConfigName: "kubernetes.excludeKinds", + Usage: "indicate the kinds exclude from scanning (example: node)", + } + IncludeKinds = Flag[[]string]{ + Name: "include-kinds", + ConfigName: "kubernetes.includeKinds", + Usage: "indicate the kinds included in scanning (example: node)", + } + ExcludeNamespaces = Flag[[]string]{ + Name: "exclude-namespaces", + ConfigName: "kubernetes.excludeNamespaces", + Usage: "indicate the namespaces excluded from scanning (example: kube-system)", + } + IncludeNamespaces = Flag[[]string]{ + Name: "include-namespaces", + ConfigName: "kubernetes.includeNamespaces", + Usage: "indicate the namespaces included in scanning (example: kube-system)", + } + QPS = Flag[float64]{ + Name: "qps", + ConfigName: "kubernetes.qps", + Default: 5.0, + Usage: "specify the maximum QPS to the master from this client", + } + Burst = Flag[int]{ + Name: "burst", + ConfigName: "kubernetes.burst", + Default: 10, + Usage: "specify the maximum burst for throttle", + } +) + +type K8sFlagGroup struct { + KubeConfig *Flag[string] + K8sVersion *Flag[string] + Tolerations *Flag[[]string] + DisableNodeCollector *Flag[bool] + NodeCollectorImageRef *Flag[string] + NodeCollectorNamespace *Flag[string] + ExcludeOwned *Flag[bool] + SkipImages *Flag[bool] + ExcludeNodes *Flag[[]string] + ExcludeKinds *Flag[[]string] + IncludeKinds *Flag[[]string] + ExcludeNamespaces *Flag[[]string] + IncludeNamespaces *Flag[[]string] + QPS *Flag[float64] + Burst *Flag[int] +} + +type K8sOptions struct { + KubeConfig string + K8sVersion string + Tolerations []corev1.Toleration + NodeCollectorImageRef string + NodeCollectorNamespace string + ExcludeOwned bool + DisableNodeCollector bool + ExcludeNodes map[string]string + ExcludeKinds []string + IncludeKinds []string + ExcludeNamespaces []string + IncludeNamespaces []string + QPS float32 + SkipImages bool + Burst int +} + +func NewK8sFlagGroup() *K8sFlagGroup { + return &K8sFlagGroup{ + KubeConfig: KubeConfigFlag.Clone(), + K8sVersion: K8sVersionFlag.Clone(), + Tolerations: TolerationsFlag.Clone(), + DisableNodeCollector: DisableNodeCollector.Clone(), + NodeCollectorNamespace: NodeCollectorNamespace.Clone(), + ExcludeOwned: ExcludeOwned.Clone(), + ExcludeNodes: ExcludeNodes.Clone(), + ExcludeKinds: ExcludeKinds.Clone(), + IncludeKinds: IncludeKinds.Clone(), + ExcludeNamespaces: ExcludeNamespaces.Clone(), + IncludeNamespaces: IncludeNamespaces.Clone(), + NodeCollectorImageRef: NodeCollectorImageRef.Clone(), + QPS: QPS.Clone(), + SkipImages: SkipImages.Clone(), + Burst: Burst.Clone(), + } +} + +func (f *K8sFlagGroup) Name() string { + return "Kubernetes" +} + +func (f *K8sFlagGroup) Flags() []Flagger { + return []Flagger{ + f.KubeConfig, + f.K8sVersion, + f.DisableNodeCollector, + f.Tolerations, + f.NodeCollectorNamespace, + f.ExcludeOwned, + f.ExcludeNodes, + f.NodeCollectorImageRef, + f.ExcludeKinds, + f.IncludeKinds, + f.ExcludeNamespaces, + f.IncludeNamespaces, + f.QPS, + f.SkipImages, + f.Burst, + } +} + +func (f *K8sFlagGroup) ToOptions(opts *Options) error { + tolerations, err := optionToTolerations(f.Tolerations.Value()) + if err != nil { + return err + } + + exludeNodeLabels := make(map[string]string) + exludeNodes := f.ExcludeNodes.Value() + for _, exludeNodeValue := range exludeNodes { + excludeNodeParts := strings.Split(exludeNodeValue, ":") + if len(excludeNodeParts) != 2 { + return xerrors.Errorf("exclude node %s must be a key:value", exludeNodeValue) + } + exludeNodeLabels[excludeNodeParts[0]] = excludeNodeParts[1] + } + if len(f.ExcludeNamespaces.Value()) > 0 && len(f.IncludeNamespaces.Value()) > 0 { + return xerrors.New("include-namespaces and exclude-namespaces flags cannot be used together") + } + if len(f.ExcludeKinds.Value()) > 0 && len(f.IncludeKinds.Value()) > 0 { + return xerrors.New("include-kinds and exclude-kinds flags cannot be used together") + } + + opts.K8sOptions = K8sOptions{ + KubeConfig: f.KubeConfig.Value(), + K8sVersion: f.K8sVersion.Value(), + Tolerations: tolerations, + DisableNodeCollector: f.DisableNodeCollector.Value(), + NodeCollectorNamespace: f.NodeCollectorNamespace.Value(), + ExcludeOwned: f.ExcludeOwned.Value(), + ExcludeNodes: exludeNodeLabels, + NodeCollectorImageRef: f.NodeCollectorImageRef.Value(), + QPS: float32(f.QPS.Value()), + SkipImages: f.SkipImages.Value(), + ExcludeKinds: f.ExcludeKinds.Value(), + IncludeKinds: f.IncludeKinds.Value(), + ExcludeNamespaces: f.ExcludeNamespaces.Value(), + IncludeNamespaces: f.IncludeNamespaces.Value(), + Burst: f.Burst.Value(), + } + return nil +} + +func optionToTolerations(tolerationsOptions []string) ([]corev1.Toleration, error) { + var tolerations []corev1.Toleration + for _, toleration := range tolerationsOptions { + tolerationParts := strings.Split(toleration, ":") + if len(tolerationParts) < 2 { + return []corev1.Toleration{}, errors.New("toleration must include key and effect") + } + if corev1.TaintEffect(tolerationParts[1]) != corev1.TaintEffectNoSchedule && + corev1.TaintEffect(tolerationParts[1]) != corev1.TaintEffectPreferNoSchedule && + corev1.TaintEffect(tolerationParts[1]) != corev1.TaintEffectNoExecute { + return []corev1.Toleration{}, errors.New("toleration effect must be a valid value") + } + keyValue := strings.Split(tolerationParts[0], "=") + operator := corev1.TolerationOpEqual + if keyValue[1] == "" { + operator = corev1.TolerationOpExists + } + toleration := corev1.Toleration{ + Key: keyValue[0], + Value: keyValue[1], + Operator: operator, + Effect: corev1.TaintEffect(tolerationParts[1]), + } + var tolerationSec int + var err error + if len(tolerationParts) == 3 { + tolerationSec, err = strconv.Atoi(tolerationParts[2]) + if err != nil { + return nil, errors.New("TolerationSeconds must must be a number") + } + toleration.TolerationSeconds = lo.ToPtr(int64(tolerationSec)) + } + tolerations = append(tolerations, toleration) + } + return tolerations, nil +} diff --git a/pkg/flag/kubernetes_flags_test.go b/pkg/flag/kubernetes_flags_test.go new file mode 100644 index 000000000000..81b9fa94145a --- /dev/null +++ b/pkg/flag/kubernetes_flags_test.go @@ -0,0 +1,52 @@ +package flag + +import ( + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" +) + +func TestOptionToToleration(t *testing.T) { + + tests := []struct { + name string + tolerationsOptions []string + want []corev1.Toleration + }{ + { + name: "no execute", + tolerationsOptions: []string{"key1=CriticalAddonsOnly:NoExecute:3600"}, + want: []corev1.Toleration{ + { + Key: "key1", + Operator: "Equal", + Value: "CriticalAddonsOnly", + Effect: "NoExecute", + TolerationSeconds: lo.ToPtr(int64(3600)), + }, + }, + }, + { + name: "no schedule", + tolerationsOptions: []string{"key1=CriticalAddonsOnly:NoSchedule"}, + want: []corev1.Toleration{ + { + Key: "key1", + Operator: "Equal", + Value: "CriticalAddonsOnly", + Effect: "NoSchedule", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := optionToTolerations(tt.tolerationsOptions) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/flag/license_flags.go b/pkg/flag/license_flags.go new file mode 100644 index 000000000000..8325a8d557e5 --- /dev/null +++ b/pkg/flag/license_flags.go @@ -0,0 +1,135 @@ +package flag + +import ( + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing/expression" +) + +var ( + LicenseFull = Flag[bool]{ + Name: "license-full", + ConfigName: "license.full", + Usage: "eagerly look for licenses in source code headers and license files", + TelemetrySafe: true, + } + IgnoredLicenses = Flag[[]string]{ + Name: "ignored-licenses", + ConfigName: "license.ignored", + Usage: "specify a list of license to ignore", + } + LicenseConfidenceLevel = Flag[float64]{ + Name: "license-confidence-level", + ConfigName: "license.confidenceLevel", + Default: 0.9, + Usage: "specify license classifier's confidence level", + } + + // LicenseForbidden is an option only in a config file + LicenseForbidden = Flag[[]string]{ + ConfigName: "license.forbidden", + Default: expression.ForbiddenLicenses, + Usage: "forbidden licenses", + } + // LicenseRestricted is an option only in a config file + LicenseRestricted = Flag[[]string]{ + ConfigName: "license.restricted", + Default: expression.RestrictedLicenses, + Usage: "restricted licenses", + } + // LicenseReciprocal is an option only in a config file + LicenseReciprocal = Flag[[]string]{ + ConfigName: "license.reciprocal", + Default: expression.ReciprocalLicenses, + Usage: "reciprocal licenses", + } + // LicenseNotice is an option only in a config file + LicenseNotice = Flag[[]string]{ + ConfigName: "license.notice", + Default: expression.NoticeLicenses, + Usage: "notice licenses", + } + // LicensePermissive is an option only in a config file + LicensePermissive = Flag[[]string]{ + ConfigName: "license.permissive", + Default: expression.PermissiveLicenses, + Usage: "permissive licenses", + } + // LicenseUnencumbered is an option only in a config file + LicenseUnencumbered = Flag[[]string]{ + ConfigName: "license.unencumbered", + Default: expression.UnencumberedLicenses, + Usage: "unencumbered licenses", + } +) + +type LicenseFlagGroup struct { + LicenseFull *Flag[bool] + IgnoredLicenses *Flag[[]string] + LicenseConfidenceLevel *Flag[float64] + + // License Categories + LicenseForbidden *Flag[[]string] // mapped to CRITICAL + LicenseRestricted *Flag[[]string] // mapped to HIGH + LicenseReciprocal *Flag[[]string] // mapped to MEDIUM + LicenseNotice *Flag[[]string] // mapped to LOW + LicensePermissive *Flag[[]string] // mapped to LOW + LicenseUnencumbered *Flag[[]string] // mapped to LOW +} + +type LicenseOptions struct { + LicenseFull bool + IgnoredLicenses []string + LicenseConfidenceLevel float64 + LicenseRiskThreshold int + LicenseCategories map[types.LicenseCategory][]string +} + +func NewLicenseFlagGroup() *LicenseFlagGroup { + return &LicenseFlagGroup{ + LicenseFull: LicenseFull.Clone(), + IgnoredLicenses: IgnoredLicenses.Clone(), + LicenseConfidenceLevel: LicenseConfidenceLevel.Clone(), + LicenseForbidden: LicenseForbidden.Clone(), + LicenseRestricted: LicenseRestricted.Clone(), + LicenseReciprocal: LicenseReciprocal.Clone(), + LicenseNotice: LicenseNotice.Clone(), + LicensePermissive: LicensePermissive.Clone(), + LicenseUnencumbered: LicenseUnencumbered.Clone(), + } +} + +func (f *LicenseFlagGroup) Name() string { + return "License" +} + +func (f *LicenseFlagGroup) Flags() []Flagger { + return []Flagger{ + f.LicenseFull, + f.IgnoredLicenses, + f.LicenseForbidden, + f.LicenseRestricted, + f.LicenseReciprocal, + f.LicenseNotice, + f.LicensePermissive, + f.LicenseUnencumbered, + f.LicenseConfidenceLevel, + } +} + +func (f *LicenseFlagGroup) ToOptions(opts *Options) error { + licenseCategories := make(map[types.LicenseCategory][]string) + licenseCategories[types.CategoryForbidden] = f.LicenseForbidden.Value() + licenseCategories[types.CategoryRestricted] = f.LicenseRestricted.Value() + licenseCategories[types.CategoryReciprocal] = f.LicenseReciprocal.Value() + licenseCategories[types.CategoryNotice] = f.LicenseNotice.Value() + licenseCategories[types.CategoryPermissive] = f.LicensePermissive.Value() + licenseCategories[types.CategoryUnencumbered] = f.LicenseUnencumbered.Value() + + opts.LicenseOptions = LicenseOptions{ + LicenseFull: f.LicenseFull.Value(), + IgnoredLicenses: f.IgnoredLicenses.Value(), + LicenseConfidenceLevel: f.LicenseConfidenceLevel.Value(), + LicenseCategories: licenseCategories, + } + return nil +} diff --git a/pkg/flag/misconf_flags.go b/pkg/flag/misconf_flags.go new file mode 100644 index 000000000000..978b55fdb22e --- /dev/null +++ b/pkg/flag/misconf_flags.go @@ -0,0 +1,274 @@ +package flag + +import ( + "fmt" + + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/policy" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" +) + +// e.g. config yaml: +// +// misconfiguration: +// trace: true +// config-policy: "custom-policy/policy" +// policy-namespaces: "user" +var ( + // Deprecated + ResetChecksBundleFlag = Flag[bool]{ + Name: "reset-checks-bundle", + ConfigName: "misconfiguration.reset-checks-bundle", + Usage: "remove checks bundle", + Removed: `Use "trivy clean --checks-bundle" instead`, + Aliases: []Alias{ + { + Name: "reset-policy-bundle", + ConfigName: "misconfiguration.reset-policy-bundle", + Deprecated: true, + }, + }, + } + IncludeNonFailuresFlag = Flag[bool]{ + Name: "include-non-failures", + ConfigName: "misconfiguration.include-non-failures", + Usage: "include successes, available with '--scanners misconfig'", + TelemetrySafe: true, + } + HelmValuesFileFlag = Flag[[]string]{ + Name: "helm-values", + ConfigName: "misconfiguration.helm.values", + Usage: "specify paths to override the Helm values.yaml files", + } + HelmSetFlag = Flag[[]string]{ + Name: "helm-set", + ConfigName: "misconfiguration.helm.set", + Usage: "specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)", + } + HelmSetFileFlag = Flag[[]string]{ + Name: "helm-set-file", + ConfigName: "misconfiguration.helm.set-file", + Usage: "specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)", + } + HelmSetStringFlag = Flag[[]string]{ + Name: "helm-set-string", + ConfigName: "misconfiguration.helm.set-string", + Usage: "specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)", + } + HelmAPIVersionsFlag = Flag[[]string]{ + Name: "helm-api-versions", + ConfigName: "misconfiguration.helm.api-versions", + Usage: "Available API versions used for Capabilities.APIVersions. This flag is the same as the api-versions flag of the helm template command. (can specify multiple or separate values with commas: policy/v1/PodDisruptionBudget,apps/v1/Deployment)", + } + HelmKubeVersionFlag = Flag[string]{ + Name: "helm-kube-version", + ConfigName: "misconfiguration.helm.kube-version", + Usage: "Kubernetes version used for Capabilities.KubeVersion. This flag is the same as the kube-version flag of the helm template command.", + } + TfVarsFlag = Flag[[]string]{ + Name: "tf-vars", + ConfigName: "misconfiguration.terraform.vars", + Usage: "specify paths to override the Terraform tfvars files", + } + CfParamsFlag = Flag[[]string]{ + Name: "cf-params", + ConfigName: "misconfiguration.cloudformation.params", + Default: []string{}, + Usage: "specify paths to override the CloudFormation parameters files", + } + TerraformExcludeDownloaded = Flag[bool]{ + Name: "tf-exclude-downloaded-modules", + ConfigName: "misconfiguration.terraform.exclude-downloaded-modules", + Usage: "exclude misconfigurations for downloaded terraform modules", + TelemetrySafe: true, + } + ChecksBundleRepositoryFlag = Flag[string]{ + Name: "checks-bundle-repository", + ConfigName: "misconfiguration.checks-bundle-repository", + Default: fmt.Sprintf("%s:%d", policy.BundleRepository, policy.BundleVersion), + Usage: "OCI registry URL to retrieve checks bundle from", + Aliases: []Alias{ + { + Name: "policy-bundle-repository", + ConfigName: "misconfiguration.policy-bundle-repository", + Deprecated: true, + }, + }, + } + MisconfigScannersFlag = Flag[[]string]{ + Name: "misconfig-scanners", + ConfigName: "misconfiguration.scanners", + Default: xstrings.ToStringSlice( + lo.Without(analyzer.TypeConfigFiles, analyzer.TypeYAML, analyzer.TypeJSON), + ), + Usage: "comma-separated list of misconfig scanners to use for misconfiguration scanning", + TelemetrySafe: true, + } + ConfigFileSchemasFlag = Flag[[]string]{ + Name: "config-file-schemas", + ConfigName: "misconfiguration.config-file-schemas", + Usage: "specify paths to JSON configuration file schemas to determine that a file matches some configuration and pass the schema to Rego checks for type checking", + } + RenderCauseFlag = Flag[[]string]{ + Name: "render-cause", + ConfigName: "misconfiguration.render-cause", + Usage: "specify configuration types for which the rendered causes will be shown in the table report", + Values: xstrings.ToStringSlice([]types.ConfigType{types.Terraform, types.Ansible}), // TODO: add Plan and JSON? + Default: []string{}, + } + RawConfigScanners = Flag[[]string]{ + Name: "raw-config-scanners", + ConfigName: "misconfiguration.raw-config-scanners", + Usage: "specify the types of scanners that will also scan raw configurations. For example, scanners will scan a non-adapted configuration into a shared state", + Values: xstrings.ToStringSlice([]types.ConfigType{types.Terraform}), + Default: []string{}, + } + AnsiblePlaybooks = Flag[[]string]{ + Name: "ansible-playbook", + ConfigName: "ansible.playbooks", + Usage: "specify playbook file path(s) to scan", + } + AnsibleInventories = Flag[[]string]{ + Name: "ansible-inventory", + ConfigName: "ansible.inventories", + Usage: "specify inventory host path or comma separated host list", + } + AnsibleExtraVars = Flag[[]string]{ + Name: "ansible-extra-vars", + ConfigName: "ansible.extra-vars", + Usage: "set additional variables as key=value or @file (YAML/JSON)", + } +) + +// MisconfFlagGroup composes common printer flag structs used for commands providing misconfiguration scanning. +type MisconfFlagGroup struct { + IncludeNonFailures *Flag[bool] + ResetChecksBundle *Flag[bool] + ChecksBundleRepository *Flag[string] + + // Values Files + HelmValues *Flag[[]string] + HelmValueFiles *Flag[[]string] + HelmFileValues *Flag[[]string] + HelmStringValues *Flag[[]string] + HelmAPIVersions *Flag[[]string] + HelmKubeVersion *Flag[string] + TerraformTFVars *Flag[[]string] + CloudformationParamVars *Flag[[]string] + TerraformExcludeDownloaded *Flag[bool] + MisconfigScanners *Flag[[]string] + ConfigFileSchemas *Flag[[]string] + RenderCause *Flag[[]string] + RawConfigScanners *Flag[[]string] + + AnsiblePlaybooks *Flag[[]string] + AnsibleInventories *Flag[[]string] + AnsibleExtraVars *Flag[[]string] +} + +type MisconfOptions struct { + IncludeNonFailures bool + ResetChecksBundle bool + ChecksBundleRepository string + + // Values Files + HelmValues []string + HelmValueFiles []string + HelmFileValues []string + HelmStringValues []string + HelmAPIVersions []string + HelmKubeVersion string + TerraformTFVars []string + CloudFormationParamVars []string + TfExcludeDownloaded bool + MisconfigScanners []analyzer.Type + ConfigFileSchemas []string + RenderCause []types.ConfigType + RawConfigScanners []types.ConfigType + + AnsiblePlaybooks []string + AnsibleInventories []string + AnsibleExtraVars []string +} + +func NewMisconfFlagGroup() *MisconfFlagGroup { + return &MisconfFlagGroup{ + IncludeNonFailures: IncludeNonFailuresFlag.Clone(), + ResetChecksBundle: ResetChecksBundleFlag.Clone(), + ChecksBundleRepository: ChecksBundleRepositoryFlag.Clone(), + + HelmValues: HelmSetFlag.Clone(), + HelmFileValues: HelmSetFileFlag.Clone(), + HelmStringValues: HelmSetStringFlag.Clone(), + HelmValueFiles: HelmValuesFileFlag.Clone(), + HelmAPIVersions: HelmAPIVersionsFlag.Clone(), + HelmKubeVersion: HelmKubeVersionFlag.Clone(), + TerraformTFVars: TfVarsFlag.Clone(), + CloudformationParamVars: CfParamsFlag.Clone(), + TerraformExcludeDownloaded: TerraformExcludeDownloaded.Clone(), + MisconfigScanners: MisconfigScannersFlag.Clone(), + ConfigFileSchemas: ConfigFileSchemasFlag.Clone(), + RenderCause: RenderCauseFlag.Clone(), + RawConfigScanners: RawConfigScanners.Clone(), + + AnsiblePlaybooks: AnsiblePlaybooks.Clone(), + AnsibleInventories: AnsibleInventories.Clone(), + AnsibleExtraVars: AnsibleExtraVars.Clone(), + } +} + +func (f *MisconfFlagGroup) Name() string { + return "Misconfiguration" +} + +func (f *MisconfFlagGroup) Flags() []Flagger { + return []Flagger{ + f.IncludeNonFailures, + f.ResetChecksBundle, + f.ChecksBundleRepository, + f.HelmValues, + f.HelmValueFiles, + f.HelmFileValues, + f.HelmStringValues, + f.HelmAPIVersions, + f.HelmKubeVersion, + f.TerraformTFVars, + f.TerraformExcludeDownloaded, + f.CloudformationParamVars, + f.MisconfigScanners, + f.ConfigFileSchemas, + f.RenderCause, + f.RawConfigScanners, + f.AnsiblePlaybooks, + f.AnsibleInventories, + f.AnsibleExtraVars, + } +} + +func (f *MisconfFlagGroup) ToOptions(opts *Options) error { + opts.MisconfOptions = MisconfOptions{ + IncludeNonFailures: f.IncludeNonFailures.Value(), + ResetChecksBundle: f.ResetChecksBundle.Value(), + ChecksBundleRepository: f.ChecksBundleRepository.Value(), + HelmValues: f.HelmValues.Value(), + HelmValueFiles: f.HelmValueFiles.Value(), + HelmFileValues: f.HelmFileValues.Value(), + HelmStringValues: f.HelmStringValues.Value(), + HelmAPIVersions: f.HelmAPIVersions.Value(), + HelmKubeVersion: f.HelmKubeVersion.Value(), + TerraformTFVars: f.TerraformTFVars.Value(), + CloudFormationParamVars: f.CloudformationParamVars.Value(), + TfExcludeDownloaded: f.TerraformExcludeDownloaded.Value(), + MisconfigScanners: xstrings.ToTSlice[analyzer.Type](f.MisconfigScanners.Value()), + ConfigFileSchemas: f.ConfigFileSchemas.Value(), + RenderCause: xstrings.ToTSlice[types.ConfigType](f.RenderCause.Value()), + RawConfigScanners: xstrings.ToTSlice[types.ConfigType](f.RawConfigScanners.Value()), + AnsiblePlaybooks: f.AnsiblePlaybooks.Value(), + AnsibleInventories: f.AnsibleInventories.Value(), + AnsibleExtraVars: f.AnsibleExtraVars.Value(), + } + return nil +} diff --git a/pkg/flag/module_flags.go b/pkg/flag/module_flags.go new file mode 100644 index 000000000000..d0a5fd882430 --- /dev/null +++ b/pkg/flag/module_flags.go @@ -0,0 +1,67 @@ +package flag + +import ( + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +// e.g. config yaml +// module: +// dir: "/path/to/my_modules" +// enable-modules: +// - spring4shell + +var ( + ModuleDirFlag = Flag[string]{ + Name: "module-dir", + ConfigName: "module.dir", + Default: filepath.Join(fsutils.HomeDir(), ".trivy", "modules"), + Usage: "specify directory to the wasm modules that will be loaded", + Persistent: true, + } + EnableModulesFlag = Flag[[]string]{ + Name: "enable-modules", + ConfigName: "module.enable-modules", + Default: []string{}, + Usage: "[EXPERIMENTAL] module names to enable", + Persistent: true, + } +) + +// ModuleFlagGroup defines flags for modules +type ModuleFlagGroup struct { + Dir *Flag[string] + EnabledModules *Flag[[]string] +} + +type ModuleOptions struct { + ModuleDir string + EnabledModules []string +} + +func NewModuleFlagGroup() *ModuleFlagGroup { + return &ModuleFlagGroup{ + Dir: ModuleDirFlag.Clone(), + EnabledModules: EnableModulesFlag.Clone(), + } +} + +func (f *ModuleFlagGroup) Name() string { + return "Module" +} + +func (f *ModuleFlagGroup) Flags() []Flagger { + return []Flagger{ + f.Dir, + f.EnabledModules, + } +} + +func (f *ModuleFlagGroup) ToOptions(opts *Options) error { + opts.ModuleOptions = ModuleOptions{ + ModuleDir: f.Dir.Value(), + EnabledModules: f.EnabledModules.Value(), + } + return nil +} diff --git a/pkg/flag/options.go b/pkg/flag/options.go new file mode 100644 index 000000000000..6a16e8a162d2 --- /dev/null +++ b/pkg/flag/options.go @@ -0,0 +1,835 @@ +package flag + +import ( + "context" + "fmt" + "io" + "os" + "reflect" + "slices" + "strings" + "sync" + "time" + + "github.com/samber/lo" + "github.com/spf13/cast" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/plugin" + "github.com/aquasecurity/trivy/pkg/result" + "github.com/aquasecurity/trivy/pkg/rpc/client" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/version/app" +) + +type FlagType interface { + int | string | []string | bool | time.Duration | float64 | map[string][]string +} + +type Flag[T FlagType] struct { + // Name is for CLI flag and environment variable. + // If this field is empty, it will be available only in config file. + Name string + + // ConfigName is a key in config file. It is also used as a key of viper. + ConfigName string + + // Shorthand is a shorthand letter. + Shorthand string + + // Default is the default value. It should be defined when the value is different from the zero value. + Default T + + // Values is a list of allowed values. + // It currently supports string flags and string slice flags only. + Values []string + + // ValueNormalize is a function to normalize the value. + // It can be used for aliases, etc. + ValueNormalize func(T) T + + // Usage explains how to use the flag. + Usage string + + // Persistent represents if the flag is persistent. + Persistent bool + + // Deprecated represents if the flag is deprecated. + // It shows a warning message when the flag is used. + Deprecated string + + // Removed represents if the flag is removed and no longer works. + // It shows an error message when the flag is used. + Removed string + + // Internal represents if the flag is for internal use only. + // It is not shown in the usage message. + Internal bool + + // Aliases represents aliases + Aliases []Alias + + // TelemetrySafe indicates if the flag value is safe to be included in telemetry. + TelemetrySafe bool + + // value is the value passed through CLI flag, env, or config file. + // It is populated after flag.Parse() is called. + value T +} + +type Alias struct { + Name string + ConfigName string + Deprecated bool +} + +func (f *Flag[T]) Clone() *Flag[T] { + var t T + ff := *f + ff.value = t + fff := &ff + return fff +} + +func (f *Flag[T]) Parse() error { + if f == nil { + return nil + } + + v := f.parse() + if v == nil { + // parse() should have already handled defaults, + // so if it returns nil, use the zero value + f.value = lo.Empty[T]() + return nil + } + + value, ok := f.cast(v).(T) + if !ok { + return xerrors.Errorf("failed to parse flag %s", f.Name) + } + + if f.ValueNormalize != nil { + value = f.ValueNormalize(value) + } + + if f.isSet() && !f.allowedValue(value) { + return xerrors.Errorf(`invalid argument "%s" for "--%s" flag: must be one of %q`, value, f.Name, f.Values) + } + + if f.Deprecated != "" && f.isSet() { + log.Warnf(`"--%s" is deprecated. %s`, f.Name, f.Deprecated) + } + if f.Removed != "" && f.isSet() { + log.Errorf(`"--%s" was removed. %s`, f.Name, f.Removed) + return xerrors.Errorf(`removed flag ("--%s")`, f.Name) + } + + f.value = value + return nil +} + +func (f *Flag[T]) parse() any { + // First, looks for aliases in config file (trivy.yaml). + // Note that viper.RegisterAlias cannot be used for this purpose. + var v any + for _, alias := range f.Aliases { + if alias.ConfigName == "" { + continue + } + v = viper.Get(alias.ConfigName) + if v != nil { + log.Warnf("'%s' in config file is deprecated. Use '%s' instead.", alias.ConfigName, f.ConfigName) + return v + } + } + + v = viper.Get(f.ConfigName) + + // For config-only flags (f.Name == ""), manually handle default values + // since we can't use viper.SetDefault due to the IsSet() bug + // See: https://github.com/spf13/viper/discussions/1766 + if v == nil && f.Name == "" && !reflect.ValueOf(f.Default).IsZero() { + return f.Default + } + + return v +} + +// cast converts the value to the type of the flag. +func (f *Flag[T]) cast(val any) any { + switch any(f.Default).(type) { + case bool: + return cast.ToBool(val) + case string: + return cast.ToString(val) + case int: + return cast.ToInt(val) + case float64, float32: + return cast.ToFloat64(val) + case time.Duration: + return cast.ToDuration(val) + case map[string][]string: + return cast.ToStringMapStringSlice(val) + case []string: + if s, ok := val.(string); ok && strings.Contains(s, ",") { + // Split environmental variables by comma as it is not done by viper. + // cf. https://github.com/spf13/viper/issues/380 + // It is split by spaces only. + // https://github.com/spf13/cast/blob/48ddde5701366ade1d3aba346e09bb58430d37c6/caste.go#L1296-L1297 + val = strings.Split(s, ",") + } + return cast.ToStringSlice(val) + } + return val +} + +func (f *Flag[T]) isSet() bool { + configNames := lo.FilterMap(f.Aliases, func(alias Alias, _ int) (string, bool) { + return alias.ConfigName, alias.ConfigName != "" + }) + configNames = append(configNames, f.ConfigName) + + return lo.SomeBy(configNames, viper.IsSet) +} + +func (f *Flag[T]) allowedValue(v any) bool { + if len(f.Values) == 0 { + return true + } + switch value := v.(type) { + case string: + return slices.Contains(f.Values, value) + case []string: + for _, v := range value { + if !slices.Contains(f.Values, v) { + return false + } + } + } + return true +} + +func (f *Flag[T]) GetName() string { + return f.Name +} + +func (f *Flag[T]) GetConfigName() string { + return f.ConfigName +} + +func (f *Flag[T]) GetDefaultValue() any { + return f.Default +} + +func (f *Flag[T]) GetAliases() []Alias { + return f.Aliases +} + +func (f *Flag[T]) GetUsage() string { + return f.Usage +} + +func (f *Flag[T]) GetValues() []string { + return f.Values +} + +func (f *Flag[T]) IsTelemetrySafe() bool { + return f.TelemetrySafe +} + +func (f *Flag[T]) IsSet() bool { + if f == nil { + return false + } + return f.isSet() +} + +func (f *Flag[T]) Hidden() bool { + return f.Deprecated != "" || f.Removed != "" || f.Internal +} + +func (f *Flag[T]) Value() (t T) { + if f == nil { + return t + } + return f.value +} + +func (f *Flag[T]) Add(cmd *cobra.Command) { + if f == nil || f.Name == "" { + return + } + var flags *pflag.FlagSet + if f.Persistent { + flags = cmd.PersistentFlags() + } else { + flags = cmd.Flags() + } + + switch v := any(f.Default).(type) { + case int: + flags.IntP(f.Name, f.Shorthand, v, f.Usage) + case string: + usage := f.Usage + if len(f.Values) > 0 { + if len(f.Values) <= 4 { + // Display inline for a small number of choices + usage += fmt.Sprintf(" (allowed values: %s)", strings.Join(f.Values, ",")) + } else { + // Display as a bullet list for many choices + usage += "\nAllowed values:" + for _, val := range f.Values { + usage += fmt.Sprintf("\n - %s", val) + } + if v != "" { + usage += "\n" + } + } + } + flags.StringP(f.Name, f.Shorthand, v, usage) + case []string: + usage := f.Usage + if len(f.Values) > 0 { + if len(f.Values) <= 4 { + // Display inline for a small number of choices + usage += fmt.Sprintf(" (allowed values: %s)", strings.Join(f.Values, ",")) + } else { + // Display as a bullet list for many choices + usage += "\nAllowed values:" + for _, val := range f.Values { + usage += fmt.Sprintf("\n - %s", val) + } + if len(v) != 0 { + usage += "\n" + } + } + } + flags.StringSliceP(f.Name, f.Shorthand, v, usage) + case bool: + flags.BoolP(f.Name, f.Shorthand, v, f.Usage) + case time.Duration: + flags.DurationP(f.Name, f.Shorthand, v, f.Usage) + case float64: + flags.Float64P(f.Name, f.Shorthand, v, f.Usage) + } + + if f.Hidden() { + _ = flags.MarkHidden(f.Name) + } +} + +func (f *Flag[T]) Bind(cmd *cobra.Command) error { + if f == nil { + return nil + } else if f.Name == "" { + // This flag is available only in trivy.yaml + // NOTE: viper.SetDefault is not used here to avoid the bug where IsSet() + // always returns true after SetDefault is called. Defaults are handled + // manually in the parse() method instead. + // See: https://github.com/spf13/viper/discussions/1766 + return nil + } + + // Bind CLI flags + flag := cmd.Flags().Lookup(f.Name) + if flag == nil { + // Lookup local persistent flags + flag = cmd.PersistentFlags().Lookup(f.Name) + } + if err := viper.BindPFlag(f.ConfigName, flag); err != nil { + return xerrors.Errorf("bind flag error: %w", err) + } + + // Bind environmental variable + return f.BindEnv() +} + +func (f *Flag[T]) BindEnv() error { + // We don't use viper.AutomaticEnv, so we need to add a prefix manually here. + envName := strings.ToUpper("trivy_" + strings.ReplaceAll(f.Name, "-", "_")) + if err := viper.BindEnv(f.ConfigName, envName); err != nil { + return xerrors.Errorf("bind env error: %w", err) + } + + // Bind env aliases + for _, alias := range f.Aliases { + envAlias := strings.ToUpper("trivy_" + strings.ReplaceAll(alias.Name, "-", "_")) + if err := viper.BindEnv(f.ConfigName, envAlias); err != nil { + return xerrors.Errorf("bind env error: %w", err) + } + if alias.Deprecated { + if _, ok := os.LookupEnv(envAlias); ok { + log.Warnf("'%s' is deprecated. Use '%s' instead.", envAlias, envName) + } + } + } + return nil +} + +type FlagGroup interface { + Name() string + Flags() []Flagger + ToOptions(*Options) error +} + +type Flagger interface { + GetName() string + GetConfigName() string + GetDefaultValue() any + GetAliases() []Alias + GetUsage() string + GetValues() []string + Hidden() bool + IsTelemetrySafe() bool + IsSet() bool + + Parse() error + Add(cmd *cobra.Command) + Bind(cmd *cobra.Command) error +} + +type Flags []FlagGroup + +// Options holds all the runtime configuration +type Options struct { + GlobalOptions + AWSOptions + CacheOptions + CleanOptions + DBOptions + ImageOptions + K8sOptions + LicenseOptions + MisconfOptions + ModuleOptions + PackageOptions + RegistryOptions + RegoOptions + RemoteOptions + RepoOptions + ReportOptions + ScanOptions + SecretOptions + VulnerabilityOptions + + // Trivy's version, not populated via CLI flags + AppVersion string + + // We don't want to allow disabled analyzers to be passed by users, but it is necessary for internal use. + DisabledAnalyzers []analyzer.Type + + // outputWriter is not initialized via the CLI. + // It is mainly used for testing purposes or by tools that use Trivy as a library. + outputWriter io.Writer + + // args is the arguments passed to the command. + args []string + + // usedFlags allows us to get the underlying flags for the options + usedFlags []Flagger +} + +// Align takes consistency of options +func (o *Options) Align(f *Flags) error { + if scanFlagGroup, ok := findFlagGroup[*ScanFlagGroup](f); ok && scanFlagGroup.Scanners != nil { + o.enableSBOM() + } + + if packageFlagGroup, ok := findFlagGroup[*PackageFlagGroup](f); ok && + packageFlagGroup.PkgRelationships != nil && + slices.Compare(o.PkgRelationships, ftypes.Relationships) != 0 && + (o.DependencyTree || slices.Contains(types.SupportedSBOMFormats, o.Format) || len(o.VEXSources) != 0) { + return xerrors.Errorf("'--pkg-relationships' cannot be used with '--dependency-tree', '--vex' or SBOM formats") + } + + if o.Compliance.Spec.ID != "" { + if viper.IsSet(ScannersFlag.ConfigName) { + log.Info(`The option to change scanners is disabled for scanning with the "--compliance" flag. Default scanners used.`) + } + if viper.IsSet(ImageConfigScannersFlag.ConfigName) { + log.Info(`The option to change image config scanners is disabled for scanning with the "--compliance" flag. Default image config scanners used.`) + } + + // set scanners types by spec + scanners, err := o.Compliance.Scanners() + if err != nil { + return xerrors.Errorf("scanner error: %w", err) + } + + o.Scanners = scanners + o.ImageConfigScanners = nil + // TODO: define image-config-scanners in the spec + if o.Compliance.Spec.ID == types.ComplianceDockerCIS160 { + o.Scanners = types.Scanners{types.VulnerabilityScanner} + o.ImageConfigScanners = types.Scanners{ + types.MisconfigScanner, + types.SecretScanner, + } + } + } + + return nil +} + +func (o *Options) enableSBOM() { + // Always need packages when the vulnerability scanner is enabled + if o.Scanners.Enabled(types.VulnerabilityScanner) { + o.Scanners.Enable(types.SBOMScanner) + } + + // Enable the SBOM scanner when a list of packages is necessary. + if o.ListAllPkgs || slices.Contains(types.SupportedSBOMFormats, o.Format) { + o.Scanners.Enable(types.SBOMScanner) + } + + if o.Format == types.FormatCycloneDX || o.Format == types.FormatSPDX || o.Format == types.FormatSPDXJSON { + // Vulnerability scanning is disabled by default for CycloneDX. + if !viper.IsSet(ScannersFlag.ConfigName) { + log.Info(fmt.Sprintf(`"--format %[1]s" disables security scanning. Specify "--scanners vuln" explicitly if you want to include vulnerabilities in the "%[1]s" report.`, o.Format)) + o.Scanners = nil + } + o.Scanners.Enable(types.SBOMScanner) + } +} + +// ScanOpts returns options for scanning +func (o *Options) ScanOpts() types.ScanOptions { + return types.ScanOptions{ + PkgTypes: o.PkgTypes, + PkgRelationships: o.PkgRelationships, + Scanners: o.Scanners, + ImageConfigScanners: o.ImageConfigScanners, // this is valid only for 'image' subcommand + ScanRemovedPackages: o.ScanRemovedPkgs, // this is valid only for 'image' subcommand + LicenseCategories: o.LicenseCategories, + LicenseFull: o.LicenseFull, + FilePatterns: o.FilePatterns, + IncludeDevDeps: o.IncludeDevDeps, + Distro: o.Distro, + VulnSeveritySources: o.VulnSeveritySources, + } +} + +// RegistryOpts returns options for OCI registries +func (o *Options) RegistryOpts() ftypes.RegistryOptions { + return ftypes.RegistryOptions{ + Credentials: o.Credentials, + RegistryToken: o.RegistryToken, + Insecure: o.Insecure, + CACerts: o.CACerts, + Platform: o.Platform, + AWSRegion: o.AWSOptions.Region, + RegistryMirrors: o.RegistryMirrors, + } +} + +// FilterOpts returns options for filtering +func (o *Options) FilterOpts() result.FilterOptions { + return result.FilterOptions{ + Severities: o.Severities, + IgnoreStatuses: o.IgnoreStatuses, + IncludeNonFailures: o.IncludeNonFailures, + IgnoreFile: o.IgnoreFile, + PolicyFile: o.IgnorePolicy, + IgnoreLicenses: o.IgnoredLicenses, + CacheDir: o.CacheDir, + VEXSources: o.VEXSources, + } +} + +// CacheOpts returns options for scan cache +func (o *Options) CacheOpts() cache.Options { + return cache.Options{ + Backend: o.CacheBackend, + CacheDir: o.CacheDir, + RedisCACert: o.RedisCACert, + RedisCert: o.RedisCert, + RedisKey: o.RedisKey, + RedisTLS: o.RedisTLS, + TTL: o.CacheTTL, + } +} + +// RemoteCacheOpts returns options for remote scan cache +func (o *Options) RemoteCacheOpts() cache.RemoteOptions { + return cache.RemoteOptions{ + ServerAddr: o.ServerAddr, + CustomHeaders: o.CustomHeaders, + PathPrefix: o.PathPrefix, + } +} + +func (o *Options) ClientScannerOpts() client.ServiceOption { + return client.ServiceOption{ + RemoteURL: o.ServerAddr, + CustomHeaders: o.CustomHeaders, + PathPrefix: o.PathPrefix, + } +} + +// SetOutputWriter sets an output writer. +func (o *Options) SetOutputWriter(w io.Writer) { + o.outputWriter = w +} + +// OutputWriter returns an output writer. +// If the output file is not specified, it returns os.Stdout. +func (o *Options) OutputWriter(ctx context.Context) (io.Writer, func() error, error) { + cleanup := func() error { return nil } + switch { + case o.outputWriter != nil: + return o.outputWriter, cleanup, nil + case o.Output == "": + return os.Stdout, cleanup, nil + case strings.HasPrefix(o.Output, "plugin="): + return o.outputPluginWriter(ctx) + } + + f, err := os.Create(o.Output) + if err != nil { + return nil, nil, xerrors.Errorf("failed to create output file: %w", err) + } + return f, f.Close, nil +} + +// GetUsedFlags returns the explicitly set flags for the options. +func (o *Options) GetUsedFlags() []Flagger { + return o.usedFlags +} + +func (o *Options) outputPluginWriter(ctx context.Context) (writer io.Writer, cleanup func() error, err error) { + pluginName := strings.TrimPrefix(o.Output, "plugin=") + + pr, pw := io.Pipe() + + // Close pipes on error + defer func() { + if err != nil { + if pr != nil { + pr.Close() + } + if pw != nil { + pw.Close() + } + } + }() + + wait, err := plugin.Start(ctx, pluginName, plugin.Options{ + Args: o.OutputPluginArgs, + Stdin: pr, + }) + if err != nil { + return nil, nil, xerrors.Errorf("plugin start: %w", err) + } + + cleanup = func() error { + if err = pw.Close(); err != nil { + return xerrors.Errorf("failed to close pipe: %w", err) + } + if err = wait(); err != nil { + return xerrors.Errorf("plugin error: %w", err) + } + return nil + } + return pw, cleanup, nil +} + +// groups returns all the flag groups other than global flags +func (f *Flags) groups() []FlagGroup { + return lo.Filter(*f, func(group FlagGroup, _ int) bool { + return group != nil && group.Name() != "Global" + }) +} + +func (f *Flags) AddFlags(cmd *cobra.Command) { + aliases := make(flagAliases) + for _, group := range f.groups() { + for _, flag := range group.Flags() { + if lo.IsNil(flag) || flag.GetName() == "" { + continue + } + // Register the CLI flag + flag.Add(cmd) + + // Register flag aliases + aliases.Add(flag) + } + } + + cmd.Flags().SetNormalizeFunc(aliases.NormalizeFunc()) +} + +func (f *Flags) Usages(cmd *cobra.Command) string { + var usages string + for _, group := range f.groups() { + flags := pflag.NewFlagSet(cmd.Name(), pflag.ContinueOnError) + lflags := cmd.LocalFlags() + for _, flag := range group.Flags() { + if lo.IsNil(flag) || flag.GetName() == "" { + continue + } + flags.AddFlag(lflags.Lookup(flag.GetName())) + } + if !flags.HasAvailableFlags() { + continue + } + + usages += fmt.Sprintf("%s Flags\n", group.Name()) + usages += flags.FlagUsages() + "\n" + } + return strings.TrimSpace(usages) +} + +func (f *Flags) Bind(cmd *cobra.Command) error { + for _, group := range f.groups() { + if group == nil { + continue + } + for _, flag := range group.Flags() { + if err := flag.Bind(cmd); err != nil { + return xerrors.Errorf("flag groups: %w", err) + } + } + } + return nil +} + +// nolint: gocyclo +func (f *Flags) ToOptions(args []string) (Options, error) { + opts := Options{ + AppVersion: app.Version(), + args: args, + } + + for _, group := range *f { // Include global flags + if err := parseFlags(group); err != nil { + return Options{}, xerrors.Errorf("unable to parse flags: %w", err) + } + + opts.usedFlags = append(opts.usedFlags, usedFlags(group)...) + + if err := group.ToOptions(&opts); err != nil { + return Options{}, xerrors.Errorf("unable to convert flags to options: %w", err) + } + } + + if err := opts.Align(f); err != nil { + return Options{}, xerrors.Errorf("align options error: %w", err) + } + + return opts, nil +} + +func parseFlags(fg FlagGroup) error { + for _, flag := range fg.Flags() { + if err := flag.Parse(); err != nil { + return xerrors.Errorf("unable to parse flag: %w", err) + } + } + return nil +} + +type flagAlias struct { + formalName string + deprecated bool + once sync.Once +} + +// flagAliases have aliases for CLI flags +type flagAliases map[string]*flagAlias + +func (a flagAliases) Add(flag Flagger) { + for _, alias := range flag.GetAliases() { + a[alias.Name] = &flagAlias{ + formalName: flag.GetName(), + deprecated: alias.Deprecated, + } + } +} + +func (a flagAliases) NormalizeFunc() func(*pflag.FlagSet, string) pflag.NormalizedName { + return func(_ *pflag.FlagSet, name string) pflag.NormalizedName { + if alias, ok := a[name]; ok { + if alias.deprecated { + // NormalizeFunc is called several times + alias.once.Do(func() { + log.Warnf("'--%s' is deprecated. Use '--%s' instead.", name, alias.formalName) + }) + } + name = alias.formalName + } + return pflag.NormalizedName(name) + } +} + +func HiddenFlags() []string { + var allFlagGroups = []FlagGroup{ + NewGlobalFlagGroup(), + NewCacheFlagGroup(), + NewCleanFlagGroup(), + NewClientFlags(), + NewDBFlagGroup(), + NewImageFlagGroup(), + NewK8sFlagGroup(), + NewLicenseFlagGroup(), + NewMisconfFlagGroup(), + NewModuleFlagGroup(), + NewPackageFlagGroup(), + NewRegistryFlagGroup(), + NewRegoFlagGroup(), + NewReportFlagGroup(), + NewRepoFlagGroup(), + NewScanFlagGroup(), + NewSecretFlagGroup(), + NewServerFlags(), + NewVulnerabilityFlagGroup(), + } + + var hiddenFlags []string + for _, flagGroup := range allFlagGroups { + for _, flag := range flagGroup.Flags() { + if !reflect.ValueOf(flag).IsNil() && flag.Hidden() { + hiddenFlags = append(hiddenFlags, flag.GetConfigName()) + } + } + } + return hiddenFlags +} + +// findFlagGroup finds a flag group by type T +// Note that Go generics doesn't support methods today. +// cf. https://github.com/golang/go/issues/49085 +func findFlagGroup[T FlagGroup](f *Flags) (T, bool) { + for _, group := range *f { + if g, ok := group.(T); ok { + return g, true + } + } + var zero T + return zero, false +} + +// usedFlags returns a slice of flags that are set in the given FlagGroup. +func usedFlags(fg FlagGroup) []Flagger { + if fg == nil || fg.Flags() == nil { + return nil + } + + var flags []Flagger + for _, flag := range fg.Flags() { + if flag == nil { + continue + } + if flag.IsSet() { + flags = append(flags, flag) + } + } + return flags +} diff --git a/pkg/flag/options_test.go b/pkg/flag/options_test.go new file mode 100644 index 000000000000..5abd0ea75c97 --- /dev/null +++ b/pkg/flag/options_test.go @@ -0,0 +1,357 @@ +package flag_test + +import ( + "bytes" + "log/slog" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/samber/lo" + "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestFlag_Parse(t *testing.T) { + type kv struct { + key string + value any + } + tests := []struct { + name string + flag *kv + env *kv + want []string + wantErr string + }{ + { + name: "flag, string slice", + flag: &kv{ + key: "scan.scanners", + value: []string{ + "vuln", + "misconfig", + }, + }, + want: []string{ + string(types.VulnerabilityScanner), + string(types.MisconfigScanner), + }, + }, + { + name: "env, string", + env: &kv{ + key: "TRIVY_SCANNERS", + value: "vuln,misconfig", + }, + want: []string{ + string(types.VulnerabilityScanner), + string(types.MisconfigScanner), + }, + }, + { + name: "flag, alias", + flag: &kv{ + key: "scan.security-checks", + value: "vulnerability,config", + }, + want: []string{ + string(types.VulnerabilityScanner), + string(types.MisconfigScanner), + }, + }, + { + name: "env, alias", + env: &kv{ + key: "TRIVY_SECURITY_CHECKS", + value: "vulnerability,config", + }, + want: []string{ + string(types.VulnerabilityScanner), + string(types.MisconfigScanner), + }, + }, + { + name: "flag, invalid value", + flag: &kv{ + key: "scan.scanners", + value: "vuln,invalid", + }, + wantErr: `invalid argument "[vuln invalid]" for "--scanners" flag`, + }, + { + name: "env, invalid value", + env: &kv{ + key: "TRIVY_SCANNERS", + value: "vuln,invalid", + }, + wantErr: `invalid argument "[vuln invalid]" for "--scanners" flag`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(viper.Reset) + + if tt.flag != nil { + viper.Set(tt.flag.key, tt.flag.value) + } else { + t.Setenv(tt.env.key, tt.env.value.(string)) + } + + app := &cobra.Command{} + f := flag.ScannersFlag.Clone() + f.Add(app) + require.NoError(t, f.Bind(app)) + + err := f.Parse() + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + require.Equal(t, tt.want, f.Value()) + }) + } +} + +func setValue[T comparable](key string, value T) { + if !lo.IsEmpty(value) { + viper.Set(key, value) + } +} + +func setSliceValue[T any](key string, value []T) { + if len(value) > 0 { + viper.Set(key, value) + } +} + +type Output struct { + b *bytes.Buffer +} + +func (o Output) Messages() []string { + var messages []string + for line := range strings.SplitSeq(o.b.String(), "\n") { + if line == "" { + continue + } + ss := strings.Split(line, "\t") + messages = append(messages, strings.Join(ss[2:], "\t")) + } + return messages +} + +func newLogger(level slog.Level) Output { + out := bytes.NewBuffer(nil) + logger := log.New(log.NewHandler(out, &log.Options{Level: level})) + log.SetDefault(logger) + return Output{b: out} +} + +// testFlagGroup is a simple FlagGroup for testing using existing flag definitions +type testFlagGroup struct { + CacheDir *flag.Flag[string] + Quiet *flag.Flag[bool] + LicenseForbidden *flag.Flag[[]string] +} + +func newTestFlagGroup() *testFlagGroup { + return &testFlagGroup{ + CacheDir: flag.CacheDirFlag.Clone(), + Quiet: flag.QuietFlag.Clone(), + LicenseForbidden: flag.LicenseForbidden.Clone(), + } +} + +func (fg *testFlagGroup) Name() string { + return "Test" +} + +func (fg *testFlagGroup) Flags() []flag.Flagger { + return []flag.Flagger{ + fg.CacheDir, + fg.Quiet, + fg.LicenseForbidden, + } +} + +func (fg *testFlagGroup) ToOptions(_ *flag.Options) error { + // Not needed for testing + return nil +} + +// TestFlag_IsSet verifies that IsSet() correctly identifies whether a value was set +// via CLI flag, environment variable, or config file, and that defaults are handled +// properly for config-only flags +func TestFlag_IsSet(t *testing.T) { + // wantFlag represents the expected state of a flag for testing + type wantFlag[T any] struct { + IsSet bool + Value T + } + + // wantFlags represents the expected state of all flags for testing + type wantFlags struct { + CacheDir wantFlag[string] + Quiet wantFlag[bool] + LicenseForbidden wantFlag[[]string] + } + + tests := []struct { + name string + cliArgs []string + envs map[string]string + configYAML string + want wantFlags + }{ + { + name: "nothing set - use defaults", + want: wantFlags{ + CacheDir: wantFlag[string]{IsSet: false, Value: flag.CacheDirFlag.Default}, + Quiet: wantFlag[bool]{IsSet: false, Value: false}, + LicenseForbidden: wantFlag[[]string]{IsSet: false, Value: flag.LicenseForbidden.Default}, + }, + }, + { + name: "CLI flags - all set", + // Note: --license-forbidden is a config-only flag and cannot be set via CLI. + // It will be ignored even when explicitly passed. + cliArgs: []string{"--cache-dir", "/cli/cache", "-q", "--license-forbidden", "MIT"}, + want: wantFlags{ + CacheDir: wantFlag[string]{IsSet: true, Value: "/cli/cache"}, + Quiet: wantFlag[bool]{IsSet: true, Value: true}, + LicenseForbidden: wantFlag[[]string]{IsSet: false, Value: flag.LicenseForbidden.Default}, + }, + }, + { + name: "environment variables - all set", + envs: map[string]string{ + "TRIVY_CACHE_DIR": "/env/cache", + "TRIVY_QUIET": "true", + "TRIVY_LICENSE_FORBIDDEN": "MIT,Apache-2.0", // Will not be respected even when explicitly set, because ENVs are derived from flag.Name, and LicenseForbidden is a config-only flag (has no Name field). + }, + want: wantFlags{ + CacheDir: wantFlag[string]{IsSet: true, Value: "/env/cache"}, + Quiet: wantFlag[bool]{IsSet: true, Value: true}, + LicenseForbidden: wantFlag[[]string]{IsSet: false, Value: flag.LicenseForbidden.Default}, + }, + }, + { + name: "config file - all set", + configYAML: ` +cache: + dir: /config/cache +quiet: true +license: + forbidden: + - MIT + - Apache-2.0 +`, + want: wantFlags{ + CacheDir: wantFlag[string]{IsSet: true, Value: "/config/cache"}, + Quiet: wantFlag[bool]{IsSet: true, Value: true}, + LicenseForbidden: wantFlag[[]string]{IsSet: true, Value: []string{"MIT", "Apache-2.0"}}, + }, + }, + { + name: "CLI flags - only cache dir", + cliArgs: []string{"--cache-dir", "/cli/cache"}, + want: wantFlags{ + CacheDir: wantFlag[string]{IsSet: true, Value: "/cli/cache"}, + Quiet: wantFlag[bool]{IsSet: false, Value: false}, + LicenseForbidden: wantFlag[[]string]{IsSet: false, Value: flag.LicenseForbidden.Default}, + }, + }, + { + name: "config file - license forbidden empty array", + configYAML: ` +license: + forbidden: [] +`, + want: wantFlags{ + CacheDir: wantFlag[string]{IsSet: false, Value: flag.CacheDirFlag.Default}, + Quiet: wantFlag[bool]{IsSet: false, Value: false}, + LicenseForbidden: wantFlag[[]string]{IsSet: true, Value: []string{}}, + }, + }, + { + // When a YAML key has no value (e.g., "forbidden:"), viper treats it as nil + // rather than an empty array. This means IsSet() returns false and the + // default value is used. To explicitly set an empty array, use "forbidden: []". + name: "config file - license forbidden empty value", + configYAML: ` +license: + forbidden: +`, + want: wantFlags{ + CacheDir: wantFlag[string]{IsSet: false, Value: flag.CacheDirFlag.Default}, + Quiet: wantFlag[bool]{IsSet: false, Value: false}, + LicenseForbidden: wantFlag[[]string]{IsSet: false, Value: flag.LicenseForbidden.Default}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(viper.Reset) + + // Set up environment variables + for k, v := range tt.envs { + t.Setenv(k, v) + } + + // Set up config file if provided + if tt.configYAML != "" { + tmpDir := t.TempDir() + configPath := filepath.Join(tmpDir, "trivy.yaml") + require.NoError(t, os.WriteFile(configPath, []byte(tt.configYAML), 0o644)) + + viper.SetConfigFile(configPath) + require.NoError(t, viper.ReadInConfig()) + } + + cmd := &cobra.Command{ + Use: "test", + Run: func(_ *cobra.Command, _ []string) {}, + } + // Ignore unknown flags to allow testing specific flags without defining all + cmd.FParseErrWhitelist.UnknownFlags = true + + // Create a test flag group + fg := newTestFlagGroup() + testFlags := flag.Flags{fg} + + // Use AddFlags method from Flags type + testFlags.AddFlags(cmd) + + // Parse CLI arguments + require.NoError(t, cmd.ParseFlags(tt.cliArgs)) + + // Bind flags + require.NoError(t, testFlags.Bind(cmd)) + + // Call ToOptions to parse flags automatically + _, err := testFlags.ToOptions(tt.cliArgs) + require.NoError(t, err) + + // Check all flags + assert.Equal(t, tt.want.CacheDir.IsSet, fg.CacheDir.IsSet(), "CacheDir.IsSet mismatch") + assert.Equal(t, tt.want.CacheDir.Value, fg.CacheDir.Value(), "CacheDir.Value mismatch") + + assert.Equal(t, tt.want.Quiet.IsSet, fg.Quiet.IsSet(), "Quiet.IsSet mismatch") + assert.Equal(t, tt.want.Quiet.Value, fg.Quiet.Value(), "Quiet.Value mismatch") + + assert.Equal(t, tt.want.LicenseForbidden.IsSet, fg.LicenseForbidden.IsSet(), "LicenseForbidden.IsSet mismatch") + assert.Equal(t, tt.want.LicenseForbidden.Value, fg.LicenseForbidden.Value(), "LicenseForbidden.Value mismatch") + }) + } +} diff --git a/pkg/flag/package_flags.go b/pkg/flag/package_flags.go new file mode 100644 index 000000000000..256c35a4b0fa --- /dev/null +++ b/pkg/flag/package_flags.go @@ -0,0 +1,91 @@ +package flag + +import ( + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" +) + +var ( + IncludeDevDepsFlag = Flag[bool]{ + Name: "include-dev-deps", + ConfigName: "pkg.include-dev-deps", + Usage: "include development dependencies in the report (supported: npm, yarn, gradle)", + TelemetrySafe: true, + } + PkgTypesFlag = Flag[[]string]{ + Name: "pkg-types", + ConfigName: "pkg.types", + Default: types.PkgTypes, + Values: types.PkgTypes, + Usage: "list of package types", + Aliases: []Alias{ + { + Name: "vuln-type", + ConfigName: "vulnerability.type", + Deprecated: true, // --vuln-type was renamed to --pkg-types + }, + }, + TelemetrySafe: true, + } + PkgRelationshipsFlag = Flag[[]string]{ + Name: "pkg-relationships", + ConfigName: "pkg.relationships", + Default: xstrings.ToStringSlice(ftypes.Relationships), + Values: xstrings.ToStringSlice(ftypes.Relationships), + Usage: "list of package relationships", + TelemetrySafe: true, + } +) + +// PackageFlagGroup composes common package flag structs. +// These flags affect both SBOM and vulnerability scanning. +type PackageFlagGroup struct { + IncludeDevDeps *Flag[bool] + PkgTypes *Flag[[]string] + PkgRelationships *Flag[[]string] +} + +type PackageOptions struct { + IncludeDevDeps bool + PkgTypes []string + PkgRelationships []ftypes.Relationship +} + +func NewPackageFlagGroup() *PackageFlagGroup { + return &PackageFlagGroup{ + IncludeDevDeps: IncludeDevDepsFlag.Clone(), + PkgTypes: PkgTypesFlag.Clone(), + PkgRelationships: PkgRelationshipsFlag.Clone(), + } +} + +func (f *PackageFlagGroup) Name() string { + return "Package" +} + +func (f *PackageFlagGroup) Flags() []Flagger { + return []Flagger{ + f.IncludeDevDeps, + f.PkgTypes, + f.PkgRelationships, + } +} + +func (f *PackageFlagGroup) ToOptions(opts *Options) error { + var relationships []ftypes.Relationship + for _, r := range f.PkgRelationships.Value() { + relationship, err := ftypes.NewRelationship(r) + if err != nil { + return err + } + relationships = append(relationships, relationship) + } + + opts.PackageOptions = PackageOptions{ + IncludeDevDeps: f.IncludeDevDeps.Value(), + PkgTypes: f.PkgTypes.Value(), + PkgRelationships: relationships, + } + return nil +} diff --git a/pkg/flag/package_flags_test.go b/pkg/flag/package_flags_test.go new file mode 100644 index 000000000000..8cab1d1f765b --- /dev/null +++ b/pkg/flag/package_flags_test.go @@ -0,0 +1,85 @@ +package flag_test + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestPackageFlagGroup_ToOptions(t *testing.T) { + type fields struct { + pkgTypes string + pkgRelationships string + } + tests := []struct { + name string + fields fields + want flag.PackageOptions + wantLogs []string + }{ + { + name: "happy default (without flags)", + fields: fields{}, + want: flag.PackageOptions{}, + }, + { + name: "happy path for OS packages", + fields: fields{ + pkgTypes: "os", + }, + want: flag.PackageOptions{ + PkgTypes: []string{ + types.PkgTypeOS, + }, + }, + }, + { + name: "happy path for library packages", + fields: fields{ + pkgTypes: "library", + }, + want: flag.PackageOptions{ + PkgTypes: []string{ + types.PkgTypeLibrary, + }, + }, + }, + { + name: "root and indirect relationships", + fields: fields{ + pkgRelationships: "root,indirect", + }, + want: flag.PackageOptions{ + PkgRelationships: []ftypes.Relationship{ + ftypes.RelationshipRoot, + ftypes.RelationshipIndirect, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(viper.Reset) + + setValue(flag.PkgTypesFlag.ConfigName, tt.fields.pkgTypes) + setValue(flag.PkgRelationshipsFlag.ConfigName, tt.fields.pkgRelationships) + + // Assert options + f := &flag.PackageFlagGroup{ + PkgTypes: flag.PkgTypesFlag.Clone(), + PkgRelationships: flag.PkgRelationshipsFlag.Clone(), + } + + flags := flag.Flags{f} + got, err := flags.ToOptions(nil) + require.NoError(t, err) + assert.EqualExportedValues(t, tt.want, got.PackageOptions) + }) + } +} diff --git a/pkg/flag/registry_flags.go b/pkg/flag/registry_flags.go new file mode 100644 index 000000000000..fef7ddd54367 --- /dev/null +++ b/pkg/flag/registry_flags.go @@ -0,0 +1,110 @@ +package flag + +import ( + "io" + "os" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +var ( + UsernameFlag = Flag[[]string]{ + Name: "username", + ConfigName: "registry.username", + Usage: "username. Comma-separated usernames allowed.", + } + PasswordFlag = Flag[[]string]{ + Name: "password", + ConfigName: "registry.password", + Usage: "password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons.", + } + PasswordStdinFlag = Flag[bool]{ + Name: "password-stdin", + ConfigName: "registry.password-stdin", + Usage: "password from stdin. Comma-separated passwords are not supported.", + TelemetrySafe: true, + } + RegistryTokenFlag = Flag[string]{ + Name: "registry-token", + ConfigName: "registry.token", + Usage: "registry token", + } + RegistryMirrorsFlag = Flag[map[string][]string]{ + ConfigName: "registry.mirrors", + Usage: "map of hosts and registries for them.", + } +) + +type RegistryFlagGroup struct { + Username *Flag[[]string] + Password *Flag[[]string] + PasswordStdin *Flag[bool] + RegistryToken *Flag[string] + RegistryMirrors *Flag[map[string][]string] +} + +type RegistryOptions struct { + Credentials []types.Credential + RegistryToken string + RegistryMirrors map[string][]string +} + +func NewRegistryFlagGroup() *RegistryFlagGroup { + return &RegistryFlagGroup{ + Username: UsernameFlag.Clone(), + Password: PasswordFlag.Clone(), + PasswordStdin: PasswordStdinFlag.Clone(), + RegistryToken: RegistryTokenFlag.Clone(), + RegistryMirrors: RegistryMirrorsFlag.Clone(), + } +} + +func (f *RegistryFlagGroup) Name() string { + return "Registry" +} + +func (f *RegistryFlagGroup) Flags() []Flagger { + return []Flagger{ + f.Username, + f.Password, + f.PasswordStdin, + f.RegistryToken, + f.RegistryMirrors, + } +} + +func (f *RegistryFlagGroup) ToOptions(opts *Options) error { + var credentials []types.Credential + users := f.Username.Value() + passwords := f.Password.Value() + if f.PasswordStdin.Value() { + if len(passwords) != 0 { + return xerrors.New("'--password' and '--password-stdin' can't be used at the same time") + } + contents, err := io.ReadAll(os.Stdin) + if err != nil { + return xerrors.Errorf("failed to read from stdin: %w", err) + } + // "--password-stdin" doesn't support comma-separated passwords + passwords = []string{strings.TrimRight(string(contents), "\r\n")} + } + if len(users) != len(passwords) { + return xerrors.New("the number of usernames and passwords must match") + } + for i, user := range users { + credentials = append(credentials, types.Credential{ + Username: strings.TrimSpace(user), + Password: strings.TrimSpace(passwords[i]), + }) + } + + opts.RegistryOptions = RegistryOptions{ + Credentials: credentials, + RegistryToken: f.RegistryToken.Value(), + RegistryMirrors: f.RegistryMirrors.Value(), + } + return nil +} diff --git a/pkg/flag/rego_flags.go b/pkg/flag/rego_flags.go new file mode 100644 index 000000000000..64356bf29cfa --- /dev/null +++ b/pkg/flag/rego_flags.go @@ -0,0 +1,150 @@ +package flag + +import ( + "github.com/aquasecurity/trivy/pkg/iac/rego" +) + +// e.g. config yaml: +// +// rego: +// trace: true +// config-policy: "custom-policy/policy" +// policy-namespaces: "user" +var ( + IncludeDeprecatedChecksFlag = Flag[bool]{ + Name: "include-deprecated-checks", + ConfigName: "rego.include-deprecated-checks", + Usage: "include deprecated checks", + TelemetrySafe: true, + } + SkipCheckUpdateFlag = Flag[bool]{ + Name: "skip-check-update", + ConfigName: "rego.skip-check-update", + Usage: "skip fetching rego check updates", + Aliases: []Alias{ + { + Name: "skip-policy-update", + ConfigName: "rego.skip-policy-update", + Deprecated: true, + }, + }, + TelemetrySafe: true, + } + TraceRegoFlag = Flag[bool]{ + Name: "trace-rego", + ConfigName: "rego.trace", + Usage: "enable more verbose trace output for custom queries", + Persistent: true, + TelemetrySafe: true, + Aliases: []Alias{ + { + Name: "trace", + Deprecated: true, + }, + }, + } + ConfigCheckFlag = Flag[[]string]{ + Name: "config-check", + ConfigName: "rego.check", + Usage: "specify the paths to the Rego check files or to the directories containing them, applying config files", + Aliases: []Alias{ + { + Name: "policy", + Deprecated: true, + }, + { + Name: "config-policy", + Deprecated: true, + }, + }, + } + ConfigDataFlag = Flag[[]string]{ + Name: "config-data", + ConfigName: "rego.data", + Usage: "specify paths from which data for the Rego checks will be recursively loaded", + Aliases: []Alias{ + {Name: "data"}, + }, + } + CheckNamespaceFlag = Flag[[]string]{ + Name: "check-namespaces", + ConfigName: "rego.namespaces", + Usage: "Rego namespaces", + Aliases: []Alias{ + {Name: "namespaces"}, + { + Name: "policy-namespaces", + Deprecated: true, + }, + }, + } + RegoErrorLimitFlag = Flag[int]{ + Name: "rego-error-limit", + ConfigName: "rego.error-limit", + Usage: "maximum number of compile errors allowed during Rego policy evaluation", + TelemetrySafe: true, + Default: rego.DefaultAllowedRegoErrors, + } +) + +// RegoFlagGroup composes common printer flag structs used for commands providing misconfinguration scanning. +type RegoFlagGroup struct { + IncludeDeprecatedChecks *Flag[bool] + SkipCheckUpdate *Flag[bool] + Trace *Flag[bool] + CheckPaths *Flag[[]string] + DataPaths *Flag[[]string] + CheckNamespaces *Flag[[]string] + ErrorLimit *Flag[int] +} + +type RegoOptions struct { + IncludeDeprecatedChecks bool + SkipCheckUpdate bool + Trace bool + CheckPaths []string + DataPaths []string + CheckNamespaces []string + ErrorLimit int +} + +func NewRegoFlagGroup() *RegoFlagGroup { + return &RegoFlagGroup{ + IncludeDeprecatedChecks: IncludeDeprecatedChecksFlag.Clone(), + SkipCheckUpdate: SkipCheckUpdateFlag.Clone(), + Trace: TraceRegoFlag.Clone(), + CheckPaths: ConfigCheckFlag.Clone(), + DataPaths: ConfigDataFlag.Clone(), + CheckNamespaces: CheckNamespaceFlag.Clone(), + ErrorLimit: RegoErrorLimitFlag.Clone(), + } +} + +func (f *RegoFlagGroup) Name() string { + return "Rego" +} + +func (f *RegoFlagGroup) Flags() []Flagger { + return []Flagger{ + f.IncludeDeprecatedChecks, + f.SkipCheckUpdate, + f.Trace, + f.CheckPaths, + f.DataPaths, + f.CheckNamespaces, + f.ErrorLimit, + } +} + +func (f *RegoFlagGroup) ToOptions(opts *Options) error { + opts.RegoOptions = RegoOptions{ + IncludeDeprecatedChecks: f.IncludeDeprecatedChecks.Value(), + SkipCheckUpdate: f.SkipCheckUpdate.Value(), + Trace: f.Trace.Value(), + CheckPaths: f.CheckPaths.Value(), + DataPaths: f.DataPaths.Value(), + CheckNamespaces: f.CheckNamespaces.Value(), + ErrorLimit: f.ErrorLimit.Value(), + } + return nil +} diff --git a/pkg/flag/remote_flags.go b/pkg/flag/remote_flags.go new file mode 100644 index 000000000000..b214285b2de5 --- /dev/null +++ b/pkg/flag/remote_flags.go @@ -0,0 +1,189 @@ +package flag + +import ( + "net/http" + "net/url" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" +) + +const ( + DefaultTokenHeader = "Trivy-Token" +) + +var ( + ServerTokenFlag = Flag[string]{ + Name: "token", + ConfigName: "server.token", + Usage: "for authentication in client/server mode", + } + ServerTokenHeaderFlag = Flag[string]{ + Name: "token-header", + ConfigName: "server.token-header", + Default: DefaultTokenHeader, + Usage: "specify a header name for token in client/server mode", + } + ServerAddrFlag = Flag[string]{ + Name: "server", + ConfigName: "server.addr", + Usage: "server address in client mode", + } + ServerCustomHeadersFlag = Flag[[]string]{ + Name: "custom-headers", + ConfigName: "server.custom-headers", + Usage: "custom headers in client mode", + } + ServerListenFlag = Flag[string]{ + Name: "listen", + ConfigName: "server.listen", + Default: "localhost:4954", + Usage: "listen address in server mode", + } + ServerPathPrefixFlag = Flag[string]{ + Name: "path-prefix", + ConfigName: "server.path-prefix", + Usage: "prefix for the server endpoint", + Internal: true, // Internal use + } +) + +// RemoteFlagGroup composes common printer flag structs +// used for commands requiring reporting logic. +type RemoteFlagGroup struct { + // for client/server + Token *Flag[string] + TokenHeader *Flag[string] + PathPrefix *Flag[string] + + // for client + ServerAddr *Flag[string] + CustomHeaders *Flag[[]string] + + // for server + Listen *Flag[string] +} + +type RemoteOptions struct { + Token string + TokenHeader string + + ServerAddr string + Listen string + CustomHeaders http.Header + + // Server endpoint: []/./ (default prefix: /twirp) + // e.g., http://localhost:4954/twirp/trivy.scanner.v1.Scanner/Scan + PathPrefix string +} + +func NewClientFlags() *RemoteFlagGroup { + return &RemoteFlagGroup{ + Token: ServerTokenFlag.Clone(), + TokenHeader: ServerTokenHeaderFlag.Clone(), + PathPrefix: ServerPathPrefixFlag.Clone(), + ServerAddr: ServerAddrFlag.Clone(), + CustomHeaders: ServerCustomHeadersFlag.Clone(), + } +} + +func NewServerFlags() *RemoteFlagGroup { + return &RemoteFlagGroup{ + Token: ServerTokenFlag.Clone(), + TokenHeader: ServerTokenHeaderFlag.Clone(), + PathPrefix: ServerPathPrefixFlag.Clone(), + Listen: ServerListenFlag.Clone(), + } +} + +func (f *RemoteFlagGroup) Name() string { + return "Client/Server" +} + +func (f *RemoteFlagGroup) Flags() []Flagger { + return []Flagger{ + f.Token, + f.TokenHeader, + f.PathPrefix, + f.ServerAddr, + f.CustomHeaders, + f.Listen, + } +} + +func (f *RemoteFlagGroup) ToOptions(opts *Options) error { + serverAddr := f.ServerAddr.Value() + + // Validate server schema + if err := validateServerSchema(serverAddr); err != nil { + return err + } + + customHeaders := splitCustomHeaders(f.CustomHeaders.Value()) + listen := f.Listen.Value() + token := f.Token.Value() + tokenHeader := f.TokenHeader.Value() + + if serverAddr == "" && listen == "" { + switch { + case len(customHeaders) > 0: + log.Warn(`"--custom-header" can be used only with "--server"`) + case token != "": + log.Warn(`"--token" can be used only with "--server"`) + case tokenHeader != "" && tokenHeader != DefaultTokenHeader: + log.Warn(`"--token-header" can be used only with "--server"`) + } + } + + if token == "" && tokenHeader != DefaultTokenHeader { + log.Warn(`"--token-header" should be used with "--token"`) + } + + if token != "" && tokenHeader != "" { + customHeaders.Set(tokenHeader, token) + } + + opts.RemoteOptions = RemoteOptions{ + Token: token, + TokenHeader: tokenHeader, + PathPrefix: f.PathPrefix.Value(), + ServerAddr: serverAddr, + CustomHeaders: customHeaders, + Listen: listen, + } + return nil +} + +func splitCustomHeaders(headers []string) http.Header { + result := make(http.Header) + for _, header := range headers { + // e.g. x-api-token:XXX + s := strings.SplitN(header, ":", 2) + if len(s) != 2 { + continue + } + result.Set(s[0], s[1]) + } + return result +} + +func validateServerSchema(serverAddr string) error { + if serverAddr == "" { + return nil + } + + parsedURL, err := url.Parse(serverAddr) + if err != nil { + return xerrors.Errorf("invalid server address format: %w", err) + } + + if parsedURL.Scheme == "" { + return xerrors.Errorf("server address must include HTTP or HTTPS schema (e.g., http://localhost:4954 or https://localhost:4954)") + } else if parsedURL.Scheme != "http" && parsedURL.Scheme != "https" { + return xerrors.Errorf("server address must use HTTP or HTTPS schema, got '%s' (e.g., use http://localhost:4954 instead of %s)", parsedURL.Scheme, serverAddr) + } + + return nil +} diff --git a/pkg/flag/remote_flags_test.go b/pkg/flag/remote_flags_test.go new file mode 100644 index 000000000000..7137431f52d5 --- /dev/null +++ b/pkg/flag/remote_flags_test.go @@ -0,0 +1,162 @@ +package flag_test + +import ( + "net/http" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestRemoteFlagGroup_ToOptions(t *testing.T) { + type fields struct { + Server string + CustomHeaders []string + Token string + TokenHeader string + } + tests := []struct { + name string + fields fields + want flag.RemoteOptions + wantLogs []string + wantErr string + }{ + { + name: "happy", + fields: fields{ + Server: "http://localhost:4954", + CustomHeaders: []string{ + "x-api-token:foo bar", + "Authorization:user:password", + }, + Token: "token", + TokenHeader: "Trivy-Token", + }, + want: flag.RemoteOptions{ + ServerAddr: "http://localhost:4954", + CustomHeaders: http.Header{ + "X-Api-Token": []string{"foo bar"}, + "Authorization": []string{"user:password"}, + "Trivy-Token": []string{"token"}, + }, + Token: "token", + TokenHeader: "Trivy-Token", + }, + }, + { + name: "custom headers and no server", + fields: fields{ + CustomHeaders: []string{ + "Authorization:user:password", + }, + TokenHeader: "Trivy-Token", + }, + want: flag.RemoteOptions{ + CustomHeaders: http.Header{ + "Authorization": []string{"user:password"}, + }, + TokenHeader: "Trivy-Token", + }, + wantLogs: []string{ + `"--custom-header" can be used only with "--server"`, + }, + }, + { + name: "token and no server", + fields: fields{ + Token: "token", + }, + want: flag.RemoteOptions{ + CustomHeaders: http.Header{}, + Token: "token", + }, + wantLogs: []string{ + `"--token" can be used only with "--server"`, + }, + }, + { + name: "token header and no token", + fields: fields{ + Server: "http://localhost:4954", + TokenHeader: "Non-Default", + }, + want: flag.RemoteOptions{ + CustomHeaders: http.Header{}, + ServerAddr: "http://localhost:4954", + TokenHeader: "Non-Default", + }, + wantLogs: []string{ + `"--token-header" should be used with "--token"`, + }, + }, + { + name: "server address without schema", + fields: fields{ + Server: "localhost:8080", + }, + wantErr: "server address must use HTTP or HTTPS schema, got 'localhost'", + }, + { + name: "server address with invalid schema", + fields: fields{ + Server: "ftp://localhost:8080", + }, + wantErr: "server address must use HTTP or HTTPS schema, got 'ftp'", + }, + { + name: "server address with malformed URL", + fields: fields{ + Server: "http://[::1:8080", + }, + wantErr: "invalid server address format", + }, + { + name: "server address with https schema", + fields: fields{ + Server: "https://localhost:4954", + TokenHeader: "Trivy-Token", + }, + want: flag.RemoteOptions{ + CustomHeaders: http.Header{}, + ServerAddr: "https://localhost:4954", + TokenHeader: "Trivy-Token", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := newLogger(log.LevelWarn) + + viper.Set(flag.ServerAddrFlag.ConfigName, tt.fields.Server) + viper.Set(flag.ServerCustomHeadersFlag.ConfigName, tt.fields.CustomHeaders) + viper.Set(flag.ServerTokenFlag.ConfigName, tt.fields.Token) + viper.Set(flag.ServerTokenHeaderFlag.ConfigName, tt.fields.TokenHeader) + + // Assert options + f := &flag.RemoteFlagGroup{ + ServerAddr: flag.ServerAddrFlag.Clone(), + CustomHeaders: flag.ServerCustomHeadersFlag.Clone(), + Token: flag.ServerTokenFlag.Clone(), + TokenHeader: flag.ServerTokenHeaderFlag.Clone(), + } + flags := flag.Flags{f} + got, err := flags.ToOptions(nil) + + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equal(t, tt.want, got.RemoteOptions) + + // Assert log messages + assert.Equal(t, tt.wantLogs, out.Messages(), tt.name) + }) + } +} diff --git a/pkg/flag/repo_flags.go b/pkg/flag/repo_flags.go new file mode 100644 index 000000000000..4468bdbb3114 --- /dev/null +++ b/pkg/flag/repo_flags.go @@ -0,0 +1,60 @@ +package flag + +var ( + FetchBranchFlag = Flag[string]{ + Name: "branch", + ConfigName: "repository.branch", + Usage: "pass the branch name to be scanned", + } + FetchCommitFlag = Flag[string]{ + Name: "commit", + ConfigName: "repository.commit", + Usage: "pass the commit hash to be scanned", + } + FetchTagFlag = Flag[string]{ + Name: "tag", + ConfigName: "repository.tag", + Usage: "pass the tag name to be scanned", + } +) + +type RepoFlagGroup struct { + Branch *Flag[string] + Commit *Flag[string] + Tag *Flag[string] +} + +type RepoOptions struct { + RepoBranch string + RepoCommit string + RepoTag string +} + +func NewRepoFlagGroup() *RepoFlagGroup { + return &RepoFlagGroup{ + Branch: FetchBranchFlag.Clone(), + Commit: FetchCommitFlag.Clone(), + Tag: FetchTagFlag.Clone(), + } +} + +func (f *RepoFlagGroup) Name() string { + return "Repository" +} + +func (f *RepoFlagGroup) Flags() []Flagger { + return []Flagger{ + f.Branch, + f.Commit, + f.Tag, + } +} + +func (f *RepoFlagGroup) ToOptions(opts *Options) error { + opts.RepoOptions = RepoOptions{ + RepoBranch: f.Branch.Value(), + RepoCommit: f.Commit.Value(), + RepoTag: f.Tag.Value(), + } + return nil +} diff --git a/pkg/flag/report_flags.go b/pkg/flag/report_flags.go new file mode 100644 index 000000000000..3db1afee2fe5 --- /dev/null +++ b/pkg/flag/report_flags.go @@ -0,0 +1,315 @@ +package flag + +import ( + "slices" + "strings" + + "github.com/mattn/go-shellwords" + "github.com/spf13/viper" + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/compliance/spec" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/result" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" +) + +// e.g. config yaml: +// +// format: table +// dependency-tree: true +// severity: HIGH,CRITICAL +var ( + FormatFlag = Flag[string]{ + Name: "format", + ConfigName: "format", + Shorthand: "f", + Default: string(types.FormatTable), + Values: xstrings.ToStringSlice(types.SupportedFormats), + Usage: "format", + TelemetrySafe: true, + } + ReportFormatFlag = Flag[string]{ + Name: "report", + ConfigName: "report", + Default: "all", + Values: []string{ + "all", + "summary", + }, + Usage: "specify a report format for the output", + TelemetrySafe: true, + } + TemplateFlag = Flag[string]{ + Name: "template", + ConfigName: "template", + Shorthand: "t", + Usage: "output template", + } + DependencyTreeFlag = Flag[bool]{ + Name: "dependency-tree", + ConfigName: "dependency-tree", + Usage: "[EXPERIMENTAL] show dependency origin tree of vulnerable packages", + TelemetrySafe: true, + } + ListAllPkgsFlag = Flag[bool]{ + Name: "list-all-pkgs", + ConfigName: "list-all-pkgs", + Default: true, + Usage: "output all packages in the JSON report regardless of vulnerability", + TelemetrySafe: true, + } + IgnoreFileFlag = Flag[string]{ + Name: "ignorefile", + ConfigName: "ignorefile", + Default: result.DefaultIgnoreFile, + Usage: "specify .trivyignore file", + } + IgnorePolicyFlag = Flag[string]{ + Name: "ignore-policy", + ConfigName: "ignore-policy", + Usage: "specify the Rego file path to evaluate each vulnerability", + } + ExitCodeFlag = Flag[int]{ + Name: "exit-code", + ConfigName: "exit-code", + Usage: "specify exit code when any security issues are found", + TelemetrySafe: true, + } + ExitOnEOLFlag = Flag[int]{ + Name: "exit-on-eol", + ConfigName: "exit-on-eol", + Usage: "exit with the specified code when the OS reaches end of service/life", + TelemetrySafe: true, + } + OutputFlag = Flag[string]{ + Name: "output", + ConfigName: "output", + Shorthand: "o", + Usage: "output file name", + } + OutputPluginArgFlag = Flag[string]{ + Name: "output-plugin-arg", + ConfigName: "output-plugin-arg", + Usage: "[EXPERIMENTAL] output plugin arguments", + } + SeverityFlag = Flag[[]string]{ + Name: "severity", + ConfigName: "severity", + Shorthand: "s", + Default: dbTypes.SeverityNames, + Values: dbTypes.SeverityNames, + Usage: "severities of security issues to be displayed", + TelemetrySafe: true, + } + ComplianceFlag = Flag[string]{ + Name: "compliance", + ConfigName: "scan.compliance", + Usage: "compliance report to generate", + } + ShowSuppressedFlag = Flag[bool]{ + Name: "show-suppressed", + ConfigName: "scan.show-suppressed", + Usage: "[EXPERIMENTAL] show suppressed vulnerabilities", + TelemetrySafe: true, + } + TableModeFlag = Flag[[]string]{ + Name: "table-mode", + ConfigName: "table-mode", + Default: xstrings.ToStringSlice(types.SupportedTableModes), + Values: xstrings.ToStringSlice(types.SupportedTableModes), + Usage: "[EXPERIMENTAL] tables that will be displayed in 'table' format", + } +) + +// ReportFlagGroup composes common printer flag structs +// used for commands requiring reporting logic. +type ReportFlagGroup struct { + Format *Flag[string] + ReportFormat *Flag[string] + Template *Flag[string] + DependencyTree *Flag[bool] + ListAllPkgs *Flag[bool] + IgnoreFile *Flag[string] + IgnorePolicy *Flag[string] + ExitCode *Flag[int] + ExitOnEOL *Flag[int] + Output *Flag[string] + OutputPluginArg *Flag[string] + Severity *Flag[[]string] + Compliance *Flag[string] + ShowSuppressed *Flag[bool] + TableMode *Flag[[]string] +} + +type ReportOptions struct { + Format types.Format + ReportFormat string + Template string + DependencyTree bool + ListAllPkgs bool + IgnoreFile string + ExitCode int + ExitOnEOL int + IgnorePolicy string + Output string + OutputPluginArgs []string + Severities []dbTypes.Severity + Compliance spec.ComplianceSpec + ShowSuppressed bool + TableModes []types.TableMode +} + +func NewReportFlagGroup() *ReportFlagGroup { + return &ReportFlagGroup{ + Format: FormatFlag.Clone(), + ReportFormat: ReportFormatFlag.Clone(), + Template: TemplateFlag.Clone(), + DependencyTree: DependencyTreeFlag.Clone(), + ListAllPkgs: ListAllPkgsFlag.Clone(), + IgnoreFile: IgnoreFileFlag.Clone(), + IgnorePolicy: IgnorePolicyFlag.Clone(), + ExitCode: ExitCodeFlag.Clone(), + ExitOnEOL: ExitOnEOLFlag.Clone(), + Output: OutputFlag.Clone(), + OutputPluginArg: OutputPluginArgFlag.Clone(), + Severity: SeverityFlag.Clone(), + Compliance: ComplianceFlag.Clone(), + ShowSuppressed: ShowSuppressedFlag.Clone(), + TableMode: TableModeFlag.Clone(), + } +} + +func (f *ReportFlagGroup) Name() string { + return "Report" +} + +func (f *ReportFlagGroup) Flags() []Flagger { + return []Flagger{ + f.Format, + f.ReportFormat, + f.Template, + f.DependencyTree, + f.ListAllPkgs, + f.IgnoreFile, + f.IgnorePolicy, + f.ExitCode, + f.ExitOnEOL, + f.Output, + f.OutputPluginArg, + f.Severity, + f.Compliance, + f.ShowSuppressed, + f.TableMode, + } +} + +func (f *ReportFlagGroup) ToOptions(opts *Options) error { + format := types.Format(f.Format.Value()) + template := f.Template.Value() + dependencyTree := f.DependencyTree.Value() + listAllPkgs := f.ListAllPkgs.Value() + tableModes := f.TableMode.Value() + + if template != "" { + if format == "" { + log.Warn("'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.") + } else if format != "template" { + log.Warnf("'--template' is ignored because '--format %s' is specified. Use '--template' option with '--format template' option.", format) + } + } else { + if format == types.FormatTemplate { + log.Warn("'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.") + } + } + + // "--list-all-pkgs" option is unavailable with other than "--format json". + // If user explicitly specifies "--list-all-pkgs" with "--format table" or other formats, we should warn it. + // We check if the flag was explicitly set by the user to avoid warning when using the default value. + if f.ListAllPkgs.IsSet() && listAllPkgs && format != types.FormatJSON { + log.Warn(`"--list-all-pkgs" is only valid for the JSON format, for other formats a list of packages is automatically included.`) + } + + // "--dependency-tree" option is available only with "--format table". + if dependencyTree { + log.Info(`"--dependency-tree" only shows the dependents of vulnerable packages. ` + + `Note that it is the reverse of the usual dependency tree, which shows the packages that depend on the vulnerable package. ` + + `It supports limited package managers. Please see the document for the detail.`) + if format != types.FormatTable { + log.Warn(`"--dependency-tree" can be used only with "--format table".`) + } + } + + // "--table-mode" option is available only with "--format table". + if viper.IsSet(TableModeFlag.ConfigName) && format != types.FormatTable { + return xerrors.New(`"--table-mode" can be used only with "--format table".`) + } + + cs, err := loadComplianceTypes(f.Compliance.Value()) + if err != nil { + return xerrors.Errorf("unable to load compliance spec: %w", err) + } + + var outputPluginArgs []string + if arg := f.OutputPluginArg.Value(); arg != "" { + outputPluginArgs, err = shellwords.Parse(arg) + if err != nil { + return xerrors.Errorf("unable to parse output plugin argument: %w", err) + } + } + + if viper.IsSet(f.IgnoreFile.ConfigName) && !fsutils.FileExists(f.IgnoreFile.Value()) { + return xerrors.Errorf("ignore file not found: %s", f.IgnoreFile.Value()) + } + + opts.ReportOptions = ReportOptions{ + Format: format, + ReportFormat: f.ReportFormat.Value(), + Template: template, + DependencyTree: dependencyTree, + ListAllPkgs: listAllPkgs, + IgnoreFile: f.IgnoreFile.Value(), + ExitCode: f.ExitCode.Value(), + ExitOnEOL: f.ExitOnEOL.Value(), + IgnorePolicy: f.IgnorePolicy.Value(), + Output: f.Output.Value(), + OutputPluginArgs: outputPluginArgs, + Severities: toSeverity(f.Severity.Value()), + Compliance: cs, + ShowSuppressed: f.ShowSuppressed.Value(), + TableModes: xstrings.ToTSlice[types.TableMode](tableModes), + } + return nil +} + +func loadComplianceTypes(compliance string) (spec.ComplianceSpec, error) { + if compliance != "" && !slices.Contains(types.SupportedCompliances, compliance) && !strings.HasPrefix(compliance, "@") { + return spec.ComplianceSpec{}, xerrors.Errorf("unknown compliance : %v", compliance) + } + + cs, err := spec.GetComplianceSpec(compliance, cache.DefaultDir()) + if err != nil { + return spec.ComplianceSpec{}, xerrors.Errorf("spec loading from file system error: %w", err) + } + + return cs, nil +} + +func toSeverity(severity []string) []dbTypes.Severity { + if len(severity) == 0 { + return nil + } + severities := xslices.Map(severity, func(s string) dbTypes.Severity { + // Note that there is no need to check the error here + // since the severity value is already validated in the flag parser. + sev, _ := dbTypes.NewSeverity(s) + return sev + }) + log.Debug("Parsed severities", log.Any("severities", severities)) + return severities +} diff --git a/pkg/flag/report_flags_test.go b/pkg/flag/report_flags_test.go new file mode 100644 index 000000000000..fcced5fa0b43 --- /dev/null +++ b/pkg/flag/report_flags_test.go @@ -0,0 +1,243 @@ +package flag_test + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/compliance/spec" + "github.com/aquasecurity/trivy/pkg/flag" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" +) + +func TestReportFlagGroup_ToOptions(t *testing.T) { + type fields struct { + format types.Format + template string + dependencyTree bool + listAllPkgs bool + ignoreUnfixed bool + ignoreFile string + exitCode int + exitOnEOSL bool + ignorePolicy string + output string + outputPluginArgs string + severities string + compliance string + debug bool + pkgTypes string + tableModes []string + } + tests := []struct { + name string + fields fields + want flag.ReportOptions + wantErr string + wantLogs []string + }{ + { + name: "happy default (without flags)", + fields: fields{}, + want: flag.ReportOptions{}, + }, + { + name: "happy path with an cyclonedx", + fields: fields{ + severities: "CRITICAL", + format: "cyclonedx", + }, + want: flag.ReportOptions{ + Severities: []dbTypes.Severity{dbTypes.SeverityCritical}, + Format: types.FormatCycloneDX, + }, + }, + { + name: "invalid option combination: --template enabled without --format", + fields: fields{ + template: "@contrib/gitlab.tpl", + severities: "LOW", + }, + wantLogs: []string{ + "'--template' is ignored because '--format template' is not specified. Use '--template' option with '--format template' option.", + }, + want: flag.ReportOptions{ + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + Template: "@contrib/gitlab.tpl", + }, + }, + { + name: "invalid option combination: --template and --format json", + fields: fields{ + format: "json", + template: "@contrib/gitlab.tpl", + severities: "LOW", + }, + wantLogs: []string{ + "'--template' is ignored because '--format json' is specified. Use '--template' option with '--format template' option.", + }, + want: flag.ReportOptions{ + Format: "json", + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + Template: "@contrib/gitlab.tpl", + }, + }, + { + name: "invalid option combination: --format template without --template", + fields: fields{ + format: "template", + severities: "LOW", + }, + wantLogs: []string{ + "'--format template' is ignored because '--template' is not specified. Specify '--template' option when you use '--format template'.", + }, + want: flag.ReportOptions{ + Format: "template", + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + }, + }, + { + name: "invalid option combination: --list-all-pkgs with --format table", + fields: fields{ + format: "table", + severities: "LOW", + listAllPkgs: true, + }, + wantLogs: []string{ + `"--list-all-pkgs" is only valid for the JSON format, for other formats a list of packages is automatically included.`, + }, + want: flag.ReportOptions{ + Format: "table", + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + ListAllPkgs: true, + }, + }, + { + name: "happy path with output plugin args", + fields: fields{ + output: "plugin=count", + outputPluginArgs: "--publish-after 2023-10-01 --publish-before 2023-10-02", + }, + want: flag.ReportOptions{ + Output: "plugin=count", + OutputPluginArgs: []string{ + "--publish-after", + "2023-10-01", + "--publish-before", + "2023-10-02", + }, + }, + }, + { + name: "happy path with compliance", + fields: fields{ + compliance: "@testdata/example-spec.yaml", + severities: dbTypes.SeverityLow.String(), + }, + want: flag.ReportOptions{ + Compliance: spec.ComplianceSpec{ + Spec: iacTypes.Spec{ + ID: "0001", + Title: "my-custom-spec", + Description: "My fancy spec", + Version: "1.2", + Controls: []iacTypes.Control{ + { + ID: "1.1", + Name: "Unencrypted S3 bucket", + Description: "S3 Buckets should be encrypted to protect the data that is stored within them if access is compromised.", + Checks: []iacTypes.SpecCheck{ + {ID: "AVD-AWS-0088"}, + }, + Severity: "HIGH", + }, + }, + }, + }, + Severities: []dbTypes.Severity{dbTypes.SeverityLow}, + }, + }, + { + name: "invalid option combination: --table-modes with --format json", + fields: fields{ + format: "json", + tableModes: xstrings.ToStringSlice(types.SupportedTableModes), + }, + wantErr: `"--table-mode" can be used only with "--format table".`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(viper.Reset) + + level := log.LevelWarn + if tt.fields.debug { + level = log.LevelDebug + } + out := newLogger(level) + + setValue(flag.FormatFlag.ConfigName, string(tt.fields.format)) + setValue(flag.TemplateFlag.ConfigName, tt.fields.template) + setValue(flag.DependencyTreeFlag.ConfigName, tt.fields.dependencyTree) + setValue(flag.ListAllPkgsFlag.ConfigName, tt.fields.listAllPkgs) + setValue(flag.IgnoreFileFlag.ConfigName, tt.fields.ignoreFile) + setValue(flag.IgnoreUnfixedFlag.ConfigName, tt.fields.ignoreUnfixed) + setValue(flag.IgnorePolicyFlag.ConfigName, tt.fields.ignorePolicy) + setValue(flag.ExitCodeFlag.ConfigName, tt.fields.exitCode) + setValue(flag.ExitOnEOLFlag.ConfigName, tt.fields.exitOnEOSL) + setValue(flag.OutputFlag.ConfigName, tt.fields.output) + setValue(flag.OutputPluginArgFlag.ConfigName, tt.fields.outputPluginArgs) + setValue(flag.SeverityFlag.ConfigName, tt.fields.severities) + setValue(flag.ComplianceFlag.ConfigName, tt.fields.compliance) + setSliceValue(flag.TableModeFlag.ConfigName, tt.fields.tableModes) + + // Assert options + f := &flag.ReportFlagGroup{ + Format: flag.FormatFlag.Clone(), + Template: flag.TemplateFlag.Clone(), + DependencyTree: flag.DependencyTreeFlag.Clone(), + ListAllPkgs: flag.ListAllPkgsFlag.Clone(), + IgnoreFile: flag.IgnoreFileFlag.Clone(), + IgnorePolicy: flag.IgnorePolicyFlag.Clone(), + ExitCode: flag.ExitCodeFlag.Clone(), + ExitOnEOL: flag.ExitOnEOLFlag.Clone(), + Output: flag.OutputFlag.Clone(), + OutputPluginArg: flag.OutputPluginArgFlag.Clone(), + Severity: flag.SeverityFlag.Clone(), + Compliance: flag.ComplianceFlag.Clone(), + TableMode: flag.TableModeFlag.Clone(), + } + + flags := flag.Flags{f} + got, err := flags.ToOptions(nil) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + assert.EqualExportedValues(t, tt.want, got.ReportOptions) + + // Assert log messages + assert.Equal(t, tt.wantLogs, out.Messages(), tt.name) + }) + } + + t.Run("Error on non existing ignore file", func(t *testing.T) { + t.Cleanup(viper.Reset) + + setValue(flag.IgnoreFileFlag.ConfigName, "doesntexist") + f := &flag.ReportFlagGroup{ + IgnoreFile: flag.IgnoreFileFlag.Clone(), + } + + flags := flag.Flags{f} + _, err := flags.ToOptions(nil) + assert.ErrorContains(t, err, "ignore file not found: doesntexist") + }) +} diff --git a/pkg/flag/scan_flags.go b/pkg/flag/scan_flags.go new file mode 100644 index 000000000000..aae893a29992 --- /dev/null +++ b/pkg/flag/scan_flags.go @@ -0,0 +1,252 @@ +package flag + +import ( + "runtime" + "slices" + "strings" + + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" +) + +var ( + SkipDirsFlag = Flag[[]string]{ + Name: "skip-dirs", + ConfigName: "scan.skip-dirs", + Usage: "specify the directories or glob patterns to skip", + } + SkipFilesFlag = Flag[[]string]{ + Name: "skip-files", + ConfigName: "scan.skip-files", + Default: []string{}, + Usage: "specify the files or glob patterns to skip", + } + OfflineScanFlag = Flag[bool]{ + Name: "offline-scan", + ConfigName: "scan.offline", + Usage: "do not issue API requests to identify dependencies", + TelemetrySafe: true, + } + ScannersFlag = Flag[[]string]{ + Name: "scanners", + ConfigName: "scan.scanners", + Default: xstrings.ToStringSlice(types.Scanners{ + types.VulnerabilityScanner, + types.SecretScanner, + }), + Values: xstrings.ToStringSlice(types.Scanners{ + types.VulnerabilityScanner, + types.MisconfigScanner, + types.SecretScanner, + types.LicenseScanner, + }), + ValueNormalize: func(ss []string) []string { + return xslices.Map(ss, func(s string) string { + switch s { + case "vulnerability": + return string(types.VulnerabilityScanner) + case "misconf", "misconfiguration": + return string(types.MisconfigScanner) + case "config": + log.Warn("'--scanners config' is deprecated. Use '--scanners misconfig' instead. See https://github.com/aquasecurity/trivy/discussions/5586 for the detail.") + return string(types.MisconfigScanner) + } + return s + }) + }, + Aliases: []Alias{ + { + Name: "security-checks", + ConfigName: "scan.security-checks", + Deprecated: true, // --security-checks was renamed to --scanners + }, + }, + Usage: "comma-separated list of what security issues to detect", + TelemetrySafe: true, + } + FilePatternsFlag = Flag[[]string]{ + Name: "file-patterns", + ConfigName: "scan.file-patterns", + Usage: "specify config file patterns", + } + SlowFlag = Flag[bool]{ + Name: "slow", + ConfigName: "scan.slow", + Default: false, + Usage: "scan over time with lower CPU and memory utilization", + Deprecated: `Use "--parallel 1" instead.`, + TelemetrySafe: true, + } + ParallelFlag = Flag[int]{ + Name: "parallel", + ConfigName: "scan.parallel", + Default: 5, + Usage: "number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism", + TelemetrySafe: true, + } + SBOMSourcesFlag = Flag[[]string]{ + Name: "sbom-sources", + ConfigName: "scan.sbom-sources", + Values: []string{ + "oci", + "rekor", + }, + Usage: "[EXPERIMENTAL] try to retrieve SBOM from the specified sources", + } + RekorURLFlag = Flag[string]{ + Name: "rekor-url", + ConfigName: "scan.rekor-url", + Default: "https://rekor.sigstore.dev", + Usage: "[EXPERIMENTAL] address of rekor STL server", + } + DetectionPriority = Flag[string]{ + Name: "detection-priority", + ConfigName: "scan.detection-priority", + Default: string(ftypes.PriorityPrecise), + Values: xstrings.ToStringSlice([]ftypes.DetectionPriority{ + ftypes.PriorityPrecise, + ftypes.PriorityComprehensive, + }), + Usage: `specify the detection priority: + - "precise": Prioritizes precise by minimizing false positives. + - "comprehensive": Aims to detect more security findings at the cost of potential false positives. +`, + TelemetrySafe: true, + } + DistroFlag = Flag[string]{ + Name: "distro", + ConfigName: "scan.distro", + Usage: "[EXPERIMENTAL] specify a distribution, /", + TelemetrySafe: true, + } + SkipVersionCheckFlag = Flag[bool]{ + Name: "skip-version-check", + ConfigName: "scan.skip-version-check", + Usage: "suppress notices about version updates and Trivy announcements", + TelemetrySafe: true, + } + DisableTelemetryFlag = Flag[bool]{ + Name: "disable-telemetry", + ConfigName: "scan.disable-telemetry", + Usage: "disable sending anonymous usage data to Aqua", + } +) + +type ScanFlagGroup struct { + SkipDirs *Flag[[]string] + SkipFiles *Flag[[]string] + OfflineScan *Flag[bool] + Scanners *Flag[[]string] + FilePatterns *Flag[[]string] + Slow *Flag[bool] // deprecated + Parallel *Flag[int] + SBOMSources *Flag[[]string] + RekorURL *Flag[string] + DetectionPriority *Flag[string] + DistroFlag *Flag[string] + SkipVersionCheck *Flag[bool] + DisableTelemetry *Flag[bool] +} + +type ScanOptions struct { + Target string + SkipDirs []string + SkipFiles []string + OfflineScan bool + Scanners types.Scanners + FilePatterns []string + Parallel int + SBOMSources []string + RekorURL string + DetectionPriority ftypes.DetectionPriority + Distro ftypes.OS + SkipVersionCheck bool + DisableTelemetry bool +} + +func NewScanFlagGroup() *ScanFlagGroup { + return &ScanFlagGroup{ + SkipDirs: SkipDirsFlag.Clone(), + SkipFiles: SkipFilesFlag.Clone(), + OfflineScan: OfflineScanFlag.Clone(), + Scanners: ScannersFlag.Clone(), + FilePatterns: FilePatternsFlag.Clone(), + Parallel: ParallelFlag.Clone(), + SBOMSources: SBOMSourcesFlag.Clone(), + RekorURL: RekorURLFlag.Clone(), + Slow: SlowFlag.Clone(), + DetectionPriority: DetectionPriority.Clone(), + DistroFlag: DistroFlag.Clone(), + SkipVersionCheck: SkipVersionCheckFlag.Clone(), + DisableTelemetry: DisableTelemetryFlag.Clone(), + } +} + +func (f *ScanFlagGroup) Name() string { + return "Scan" +} + +func (f *ScanFlagGroup) Flags() []Flagger { + return []Flagger{ + f.SkipDirs, + f.SkipFiles, + f.OfflineScan, + f.Scanners, + f.FilePatterns, + f.Slow, + f.Parallel, + f.SBOMSources, + f.RekorURL, + f.DetectionPriority, + f.DistroFlag, + f.SkipVersionCheck, + f.DisableTelemetry, + } +} + +func (f *ScanFlagGroup) ToOptions(opts *Options) error { + var target string + if len(opts.args) == 1 { + target = opts.args[0] + } + + parallel := f.Parallel.Value() + if f.Parallel != nil && parallel == 0 { + log.Info("Set '--parallel' to the number of CPUs", log.Int("parallel", runtime.NumCPU())) + parallel = runtime.NumCPU() + } + + var distro ftypes.OS + if f.DistroFlag != nil && f.DistroFlag.Value() != "" { + family, version, _ := strings.Cut(f.DistroFlag.Value(), "/") + if !slices.Contains(ftypes.OSTypes, ftypes.OSType(family)) { + return xerrors.Errorf("unknown OS family: %s, must be %q", family, ftypes.OSTypes) + } + distro = ftypes.OS{ + Family: ftypes.OSType(family), + Name: version, + } + } + + opts.ScanOptions = ScanOptions{ + Target: target, + SkipDirs: f.SkipDirs.Value(), + SkipFiles: f.SkipFiles.Value(), + OfflineScan: f.OfflineScan.Value(), + Scanners: xstrings.ToTSlice[types.Scanner](f.Scanners.Value()), + FilePatterns: f.FilePatterns.Value(), + Parallel: parallel, + SBOMSources: f.SBOMSources.Value(), + RekorURL: f.RekorURL.Value(), + DetectionPriority: ftypes.DetectionPriority(f.DetectionPriority.Value()), + Distro: distro, + SkipVersionCheck: f.SkipVersionCheck.Value(), + DisableTelemetry: f.DisableTelemetry.Value(), + } + return nil +} diff --git a/pkg/flag/scan_flags_test.go b/pkg/flag/scan_flags_test.go new file mode 100644 index 000000000000..240dac14d9d6 --- /dev/null +++ b/pkg/flag/scan_flags_test.go @@ -0,0 +1,169 @@ +package flag_test + +import ( + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestScanFlagGroup_ToOptions(t *testing.T) { + type fields struct { + skipDirs []string + skipFiles []string + offlineScan bool + scanners string + distro string + skipVersionCheck bool + } + tests := []struct { + name string + args []string + fields fields + want flag.ScanOptions + assertion require.ErrorAssertionFunc + }{ + { + name: "happy path", + args: []string{"alpine:latest"}, + fields: fields{}, + want: flag.ScanOptions{ + Target: "alpine:latest", + }, + assertion: require.NoError, + }, + { + name: "happy path for configs", + args: []string{"alpine:latest"}, + fields: fields{ + scanners: "misconfig", + }, + want: flag.ScanOptions{ + Target: "alpine:latest", + Scanners: types.Scanners{types.MisconfigScanner}, + }, + assertion: require.NoError, + }, + { + name: "without target (args)", + args: []string{}, + fields: fields{}, + want: flag.ScanOptions{}, + assertion: require.NoError, + }, + { + name: "with two or more targets (args)", + args: []string{ + "alpine:latest", + "nginx:latest", + }, + fields: fields{}, + want: flag.ScanOptions{}, + assertion: require.NoError, + }, + { + name: "skip two files", + fields: fields{ + skipFiles: []string{ + "file1", + "file2", + }, + }, + want: flag.ScanOptions{ + SkipFiles: []string{ + "file1", + "file2", + }, + }, + assertion: require.NoError, + }, + { + name: "skip two folders", + fields: fields{ + skipDirs: []string{ + "dir1", + "dir2", + }, + }, + want: flag.ScanOptions{ + SkipDirs: []string{ + "dir1", + "dir2", + }, + }, + assertion: require.NoError, + }, + { + name: "offline scan", + fields: fields{ + offlineScan: true, + }, + want: flag.ScanOptions{ + OfflineScan: true, + }, + assertion: require.NoError, + }, + { + name: "happy path `distro` flag", + fields: fields{ + distro: "alpine/3.20", + }, + want: flag.ScanOptions{ + Distro: ftypes.OS{ + Family: "alpine", + Name: "3.20", + }, + }, + assertion: require.NoError, + }, + { + name: "sad distro flag", + fields: fields{ + distro: "sad", + }, + assertion: require.Error, + }, + { + name: "skip version check flag", + fields: fields{ + skipVersionCheck: true, + }, + want: flag.ScanOptions{ + SkipVersionCheck: true, + }, + assertion: require.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(viper.Reset) + setSliceValue(flag.SkipDirsFlag.ConfigName, tt.fields.skipDirs) + setSliceValue(flag.SkipFilesFlag.ConfigName, tt.fields.skipFiles) + setValue(flag.OfflineScanFlag.ConfigName, tt.fields.offlineScan) + setValue(flag.ScannersFlag.ConfigName, tt.fields.scanners) + setValue(flag.DistroFlag.ConfigName, tt.fields.distro) + setValue(flag.SkipVersionCheckFlag.ConfigName, tt.fields.skipVersionCheck) + + // Assert options + f := &flag.ScanFlagGroup{ + SkipDirs: flag.SkipDirsFlag.Clone(), + SkipFiles: flag.SkipFilesFlag.Clone(), + OfflineScan: flag.OfflineScanFlag.Clone(), + Scanners: flag.ScannersFlag.Clone(), + DistroFlag: flag.DistroFlag.Clone(), + SkipVersionCheck: flag.SkipVersionCheckFlag.Clone(), + } + + flags := flag.Flags{f} + got, err := flags.ToOptions(tt.args) + tt.assertion(t, err) + assert.Equal(t, tt.want, got.ScanOptions) + }) + } +} diff --git a/pkg/flag/secret_flags.go b/pkg/flag/secret_flags.go new file mode 100644 index 000000000000..517ecea6bd4e --- /dev/null +++ b/pkg/flag/secret_flags.go @@ -0,0 +1,39 @@ +package flag + +var ( + SecretConfigFlag = Flag[string]{ + Name: "secret-config", + ConfigName: "secret.config", + Default: "trivy-secret.yaml", + Usage: "specify a path to config file for secret scanning", + } +) + +type SecretFlagGroup struct { + SecretConfig *Flag[string] +} + +type SecretOptions struct { + SecretConfigPath string +} + +func NewSecretFlagGroup() *SecretFlagGroup { + return &SecretFlagGroup{ + SecretConfig: SecretConfigFlag.Clone(), + } +} + +func (f *SecretFlagGroup) Name() string { + return "Secret" +} + +func (f *SecretFlagGroup) Flags() []Flagger { + return []Flagger{f.SecretConfig} +} + +func (f *SecretFlagGroup) ToOptions(opts *Options) error { + opts.SecretOptions = SecretOptions{ + SecretConfigPath: f.SecretConfig.Value(), + } + return nil +} diff --git a/pkg/flag/testdata/example-spec.yaml b/pkg/flag/testdata/example-spec.yaml new file mode 100644 index 000000000000..19fbf0a3bf31 --- /dev/null +++ b/pkg/flag/testdata/example-spec.yaml @@ -0,0 +1,13 @@ +spec: + id: "0001" + title: my-custom-spec + description: My fancy spec + version: "1.2" + controls: + - id: "1.1" + name: Unencrypted S3 bucket + description: |- + S3 Buckets should be encrypted to protect the data that is stored within them if access is compromised. + checks: + - id: AVD-AWS-0088 + severity: HIGH \ No newline at end of file diff --git a/pkg/flag/vulnerability_flags.go b/pkg/flag/vulnerability_flags.go new file mode 100644 index 000000000000..e98b4ffa5580 --- /dev/null +++ b/pkg/flag/vulnerability_flags.go @@ -0,0 +1,119 @@ +package flag + +import ( + "github.com/samber/lo" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/vex" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" +) + +var ( + IgnoreUnfixedFlag = Flag[bool]{ + Name: "ignore-unfixed", + ConfigName: "vulnerability.ignore-unfixed", + Usage: "display only fixed vulnerabilities", + TelemetrySafe: true, + } + IgnoreStatusFlag = Flag[[]string]{ + Name: "ignore-status", + ConfigName: "vulnerability.ignore-status", + Values: dbTypes.Statuses, + Usage: "comma-separated list of vulnerability status to ignore", + TelemetrySafe: true, + } + VEXFlag = Flag[[]string]{ + Name: "vex", + ConfigName: "vulnerability.vex", + Usage: `[EXPERIMENTAL] VEX sources ("repo", "oci" or file path)`, + } + SkipVEXRepoUpdateFlag = Flag[bool]{ + Name: "skip-vex-repo-update", + ConfigName: "vulnerability.skip-vex-repo-update", + Usage: `[EXPERIMENTAL] Skip VEX Repository update`, + TelemetrySafe: true, + } + VulnSeveritySourceFlag = Flag[[]string]{ + Name: "vuln-severity-source", + ConfigName: "vulnerability.severity-source", + Default: []string{ + "auto", + }, + Values: append(xstrings.ToStringSlice(vulnerability.AllSourceIDs), "auto"), + Usage: "order of data sources for selecting vulnerability severity level", + TelemetrySafe: true, + } +) + +type VulnerabilityFlagGroup struct { + IgnoreUnfixed *Flag[bool] + IgnoreStatus *Flag[[]string] + VEX *Flag[[]string] + SkipVEXRepoUpdate *Flag[bool] + VulnSeveritySource *Flag[[]string] +} + +type VulnerabilityOptions struct { + IgnoreStatuses []dbTypes.Status + VEXSources []vex.Source + SkipVEXRepoUpdate bool + VulnSeveritySources []dbTypes.SourceID +} + +func NewVulnerabilityFlagGroup() *VulnerabilityFlagGroup { + return &VulnerabilityFlagGroup{ + IgnoreUnfixed: IgnoreUnfixedFlag.Clone(), + IgnoreStatus: IgnoreStatusFlag.Clone(), + VEX: VEXFlag.Clone(), + SkipVEXRepoUpdate: SkipVEXRepoUpdateFlag.Clone(), + VulnSeveritySource: VulnSeveritySourceFlag.Clone(), + } +} + +func (f *VulnerabilityFlagGroup) Name() string { + return "Vulnerability" +} + +func (f *VulnerabilityFlagGroup) Flags() []Flagger { + return []Flagger{ + f.IgnoreUnfixed, + f.IgnoreStatus, + f.VEX, + f.SkipVEXRepoUpdate, + f.VulnSeveritySource, + } +} + +func (f *VulnerabilityFlagGroup) ToOptions(opts *Options) error { + // Just convert string to dbTypes.Status as the validated values are passed here. + ignoreStatuses := xslices.Map(f.IgnoreStatus.Value(), dbTypes.NewStatus) + ignoreUnfixed := f.IgnoreUnfixed.Value() + + switch { + case ignoreUnfixed && len(ignoreStatuses) > 0: + log.Warn("'--ignore-unfixed' is ignored because '--ignore-status' is specified") + case ignoreUnfixed: + // '--ignore-unfixed' is a shorthand of '--ignore-status'. + ignoreStatuses = lo.FilterMap(dbTypes.Statuses, func(s string, _ int) (dbTypes.Status, bool) { + fixed := dbTypes.StatusFixed + if s == fixed.String() { + return 0, false + } + return dbTypes.NewStatus(s), true + }) + case len(ignoreStatuses) == 0: + ignoreStatuses = nil + } + log.Debug("Ignore statuses", log.Any("statuses", ignoreStatuses)) + + opts.VulnerabilityOptions = VulnerabilityOptions{ + IgnoreStatuses: ignoreStatuses, + VEXSources: xslices.Map(f.VEX.Value(), vex.NewSource), + SkipVEXRepoUpdate: f.SkipVEXRepoUpdate.Value(), + VulnSeveritySources: xstrings.ToTSlice[dbTypes.SourceID](f.VulnSeveritySource.Value()), + } + return nil +} diff --git a/pkg/git/git.go b/pkg/git/git.go deleted file mode 100644 index d2ade7a950cc..000000000000 --- a/pkg/git/git.go +++ /dev/null @@ -1,194 +0,0 @@ -package git - -import ( - "os" - "path/filepath" - "strings" - - "github.com/knqyf263/trivy/pkg/db" - - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/utils" - "golang.org/x/xerrors" - git "gopkg.in/src-d/go-git.v4" - "gopkg.in/src-d/go-git.v4/plumbing/object" - "gopkg.in/src-d/go-git.v4/plumbing/storer" -) - -func CloneOrPull(url, repoPath string) (map[string]struct{}, error) { - exists, err := utils.Exists(filepath.Join(repoPath, ".git")) - if err != nil { - return nil, xerrors.Errorf("failed to check if a file exists: %w", err) - } - - updatedFiles := map[string]struct{}{} - if exists { - log.Logger.Debug("git pull") - files, err := pull(repoPath) - if err != nil { - return nil, xerrors.Errorf("failed to pull repository: %w", err) - } - - for _, filename := range files { - updatedFiles[strings.TrimSpace(filename)] = struct{}{} - } - } else { - if !utils.IsCommandAvailable("git") { - log.Logger.Warn("Recommend installing git (if not, DB update is very slow)") - } - log.Logger.Debug("remove an existed directory") - - suffix := " The first time will take a while..." - s := utils.NewSpinner(suffix) - s.Start() - defer s.Stop() - - if err = os.RemoveAll(repoPath); err != nil { - return nil, xerrors.Errorf("failed to remove an existed directory: %w", err) - } - - if err = os.MkdirAll(repoPath, 0700); err != nil { - return nil, xerrors.Errorf("failed to mkdir: %w", err) - } - if err := clone(url, repoPath); err != nil { - return nil, xerrors.Errorf("failed to clone repository: %w", err) - } - - } - - // Need to refresh all vulnerabilities - if db.GetVersion() == "" { - err = filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - return nil - } - rel, err := filepath.Rel(repoPath, path) - if err != nil { - return xerrors.Errorf("failed to get a relative path: %w", err) - } - updatedFiles[rel] = struct{}{} - return nil - }) - if err != nil { - return nil, xerrors.Errorf("error in file walk: %w", err) - } - } - - return updatedFiles, nil -} - -func clone(url, repoPath string) error { - if utils.IsCommandAvailable("git") { - return cloneByOSCommand(url, repoPath) - } - - _, err := git.PlainClone(repoPath, false, &git.CloneOptions{ - URL: url, - }) - if err != nil && err != git.ErrRepositoryAlreadyExists { - return xerrors.Errorf("unexpected error in git clone: %w", err) - } - return nil -} - -func cloneByOSCommand(url, repoPath string) error { - commandAndArgs := []string{"clone", url, repoPath} - _, err := utils.Exec("git", commandAndArgs) - if err != nil { - return xerrors.Errorf("error in git clone: %w", err) - } - return nil -} - -func pull(repoPath string) ([]string, error) { - if utils.IsCommandAvailable("git") { - return pullByOSCommand(repoPath) - } - - r, err := git.PlainOpen(repoPath) - if err != nil { - return nil, xerrors.Errorf("failed to open repository: %w", err) - } - - log.Logger.Debug("Retrieve the branch being pointed by HEAD") - ref, err := r.Head() - if err != nil { - return nil, xerrors.Errorf("failed to get HEAD: %w", err) - } - - log.Logger.Debug("Get the working directory for the repository") - w, err := r.Worktree() - if err != nil { - return nil, xerrors.Errorf("failed to get the working directory: %w", err) - } - - log.Logger.Debug("Pull the latest changes from the origin remote and merge into the current branch") - err = w.Pull(&git.PullOptions{RemoteName: "origin"}) - if err != nil && err != git.NoErrAlreadyUpToDate { - return nil, err - } else if err == git.NoErrAlreadyUpToDate { - return []string{}, nil - } - - log.Logger.Debug("Retrieve the commit history") - commits, err := r.Log(&git.LogOptions{}) - if err != nil { - return nil, xerrors.Errorf("error in git log: %w", err) - } - - log.Logger.Debug("Detect the updated files") - var prevCommit *object.Commit - var updatedFiles []string - err = commits.ForEach(func(commit *object.Commit) error { - if prevCommit == nil { - prevCommit = commit - return nil - } - - patch, err := commit.Patch(prevCommit) - if err != nil { - return xerrors.Errorf("error in patch: %w", err) - } - for _, stat := range patch.Stats() { - updatedFiles = append(updatedFiles, stat.Name) - } - - if commit.Hash == ref.Hash() { - return storer.ErrStop - } - - prevCommit = commit - return nil - }) - if err != nil { - return nil, xerrors.Errorf("error in commit foreach: %w", err) - } - - return updatedFiles, nil -} - -func pullByOSCommand(repoPath string) ([]string, error) { - gitDir := filepath.Join(repoPath, ".git") - commandArgs := []string{"--git-dir", gitDir, "--work-tree", repoPath} - - revParseCmd := []string{"rev-parse", "HEAD"} - output, err := utils.Exec("git", append(commandArgs, revParseCmd...)) - if err != nil { - return nil, xerrors.Errorf("error in git rev-parse: %w", err) - } - commitHash := strings.TrimSpace(output) - - pullCmd := []string{"pull", "origin", "master"} - _, err = utils.Exec("git", append(commandArgs, pullCmd...)) - if err != nil { - return nil, xerrors.Errorf("error in git pull: %w", err) - } - - diffCmd := []string{"diff", commitHash, "HEAD", "--name-only"} - output, err = utils.Exec("git", append(commandArgs, diffCmd...)) - if err != nil { - return nil, xerrors.Errorf("error in git diff: %w", err) - } - updatedFiles := strings.Split(strings.TrimSpace(output), "\n") - return updatedFiles, nil -} diff --git a/pkg/iac/adapters/ansible/adapt.go b/pkg/iac/adapters/ansible/adapt.go new file mode 100644 index 000000000000..4b04951d87dd --- /dev/null +++ b/pkg/iac/adapters/ansible/adapt.go @@ -0,0 +1,17 @@ +package ansible + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/ansible/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/parser" + "github.com/aquasecurity/trivy/pkg/iac/state" +) + +func Adapt(tasks parser.ResolvedTasks) state.State { + return state.State{ + AWS: aws.AWS{ + S3: s3.Adapt(tasks), + // TODO(simar): Add other AWS services + }, + } +} diff --git a/pkg/iac/adapters/ansible/aws/s3/adapt.go b/pkg/iac/adapters/ansible/aws/s3/adapt.go new file mode 100644 index 000000000000..b97ac007fce7 --- /dev/null +++ b/pkg/iac/adapters/ansible/aws/s3/adapt.go @@ -0,0 +1,17 @@ +package s3 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/parser" +) + +func Adapt(tasks parser.ResolvedTasks) s3.S3 { + a := &adapter{ + tasks: tasks, + bucketMap: make(map[string]*s3.Bucket), + } + + return s3.S3{ + Buckets: a.adaptBuckets(), + } +} diff --git a/pkg/iac/adapters/ansible/aws/s3/adapt_test.go b/pkg/iac/adapters/ansible/aws/s3/adapt_test.go new file mode 100644 index 000000000000..961e226efe9c --- /dev/null +++ b/pkg/iac/adapters/ansible/aws/s3/adapt_test.go @@ -0,0 +1,63 @@ +package s3 + +import ( + "testing" + "testing/fstest" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + fsys := fstest.MapFS{ + "playbook.yaml": { + Data: []byte(`--- +- name: Update web servers + hosts: localhost + + tasks: + - name: Ensure apache is at the latest version + s3_bucket: + name: mys3bucket + encryption: "aws:kms" + encryption_key_id: "arn:aws:kms:us-east-1:1234/5678example" + public_access: + block_public_acls: true + block_public_policy: true + ignore_public_acls: true + restrict_public_buckets: true +`), + }, + } + + project, err := parser.New(fsys, ".").Parse() + require.NoError(t, err) + + tasks := project.ListTasks() + + got := Adapt(tasks) + want := s3.S3{ + Buckets: []s3.Bucket{ + { + Name: iacTypes.StringTest("mys3bucket"), + Encryption: s3.Encryption{ + Algorithm: iacTypes.StringTest("aws:kms"), + KMSKeyId: iacTypes.StringTest("arn:aws:kms:us-east-1:1234/5678example"), + }, + PublicAccessBlock: &s3.PublicAccessBlock{ + BlockPublicACLs: iacTypes.BoolTest(true), + BlockPublicPolicy: iacTypes.BoolTest(true), + IgnorePublicACLs: iacTypes.BoolTest(true), + RestrictPublicBuckets: iacTypes.BoolTest(true), + }, + Website: &s3.Website{}, + }, + }, + } + + testutil.AssertDefsecEqual(t, want, got) +} diff --git a/pkg/iac/adapters/ansible/aws/s3/bucket.go b/pkg/iac/adapters/ansible/aws/s3/bucket.go new file mode 100644 index 000000000000..2989d50d203f --- /dev/null +++ b/pkg/iac/adapters/ansible/aws/s3/bucket.go @@ -0,0 +1,82 @@ +package s3 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type adapter struct { + tasks parser.ResolvedTasks + bucketMap map[string]*s3.Bucket +} + +func (a *adapter) adaptBuckets() []s3.Bucket { + var buckets []s3.Bucket + + for _, module := range a.tasks.GetModules("s3_bucket", "amazon.aws.s3_bucket") { + buckets = append(buckets, a.adaptBucket(module)) + } + + return buckets +} + +func (a *adapter) adaptBucket(module parser.Module) s3.Bucket { + return s3.Bucket{ + Metadata: module.Metadata(), + Name: module.StringValue("name"), + Versioning: getVersioning(module), + Encryption: getEncryption(module), + PublicAccessBlock: getPublicAccessBlock(module), + Logging: a.getLogging(module), + ACL: module.StringValue("acl"), + Website: a.getWebsite(module), + } +} + +func getVersioning(module parser.Module) s3.Versioning { + return s3.Versioning{ + Metadata: module.Metadata(), + Enabled: module.BoolValue("versioning"), + MFADelete: iacTypes.BoolUnresolvable(module.Metadata()), + } +} + +func getEncryption(module parser.Module) s3.Encryption { + return s3.Encryption{ + Metadata: module.Metadata(), + Algorithm: module.StringValue("encryption"), + KMSKeyId: module.StringValue("encryption_key_id"), + Enabled: iacTypes.Bool(false, module.Metadata()), // TODO: handle + } +} + +func getPublicAccessBlock(module parser.Module) *s3.PublicAccessBlock { + publicAccess := module.NodeAt("public_access") + if publicAccess.IsNil() { + return &s3.PublicAccessBlock{ + Metadata: module.Metadata(), + } + } + return &s3.PublicAccessBlock{ + Metadata: publicAccess.Metadata(), + BlockPublicACLs: publicAccess.BoolValue("block_public_acls"), + BlockPublicPolicy: publicAccess.BoolValue("block_public_policy"), + IgnorePublicACLs: publicAccess.BoolValue("ignore_public_acls"), + RestrictPublicBuckets: publicAccess.BoolValue("restrict_public_buckets"), + } +} + +func (a *adapter) getLogging(module parser.Module) s3.Logging { + // TODO: adapt + return s3.Logging{ + Metadata: module.Metadata(), + } +} + +func (a *adapter) getWebsite(module parser.Module) *s3.Website { + // TODO: adapt + return &s3.Website{ + Metadata: module.Metadata(), + } +} diff --git a/pkg/iac/adapters/arm/adapt.go b/pkg/iac/adapters/arm/adapt.go new file mode 100644 index 000000000000..82d3cfafb800 --- /dev/null +++ b/pkg/iac/adapters/arm/adapt.go @@ -0,0 +1,49 @@ +package arm + +import ( + "context" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/appservice" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/authorization" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/compute" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/container" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/database" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/datafactory" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/datalake" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/keyvault" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/monitor" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/network" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/securitycenter" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/storage" + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/synapse" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure" + scanner "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/state" +) + +// Adapt adapts an azure arm instance +func Adapt(_ context.Context, deployment scanner.Deployment) *state.State { + return &state.State{ + Azure: adaptAzure(deployment), + } +} + +func adaptAzure(deployment scanner.Deployment) azure.Azure { + + return azure.Azure{ + AppService: appservice.Adapt(deployment), + Authorization: authorization.Adapt(deployment), + Compute: compute.Adapt(deployment), + Container: container.Adapt(deployment), + Database: database.Adapt(deployment), + DataFactory: datafactory.Adapt(deployment), + DataLake: datalake.Adapt(deployment), + KeyVault: keyvault.Adapt(deployment), + Monitor: monitor.Adapt(deployment), + Network: network.Adapt(deployment), + SecurityCenter: securitycenter.Adapt(deployment), + Storage: storage.Adapt(deployment), + Synapse: synapse.Adapt(deployment), + } + +} diff --git a/pkg/iac/adapters/arm/adaptertest/adaptertest.go b/pkg/iac/adapters/arm/adaptertest/adaptertest.go new file mode 100644 index 000000000000..d1edad127a63 --- /dev/null +++ b/pkg/iac/adapters/arm/adaptertest/adaptertest.go @@ -0,0 +1,26 @@ +package adaptertest + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure/arm/parser" +) + +type adaptFn[T any] func(deployment azure.Deployment) T + +func AdaptAndCompare[T any](t *testing.T, source string, expected any, fn adaptFn[T]) { + fsys := testutil.CreateFS(map[string]string{ + "test.json": source, + }) + + deployments, err := parser.New(fsys).ParseFS(t.Context(), ".") + require.NoError(t, err) + require.Len(t, deployments, 1) + + adapted := fn(deployments[0]) + testutil.AssertDefsecEqual(t, expected, adapted) +} diff --git a/pkg/iac/adapters/arm/appservice/adapt.go b/pkg/iac/adapters/arm/appservice/adapt.go new file mode 100644 index 000000000000..710be93200bf --- /dev/null +++ b/pkg/iac/adapters/arm/appservice/adapt.go @@ -0,0 +1,68 @@ +package appservice + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/appservice" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(deployment azure.Deployment) appservice.AppService { + return appservice.AppService{ + Services: adaptServices(deployment), + FunctionApps: adaptFunctionApps(deployment), + } +} + +func adaptFunctionApps(deployment azure.Deployment) []appservice.FunctionApp { + var functionApps []appservice.FunctionApp + + for _, resource := range deployment.GetResourcesByType("Microsoft.Web/sites") { + functionApps = append(functionApps, adaptFunctionApp(resource)) + } + return functionApps +} + +func adaptServices(deployment azure.Deployment) []appservice.Service { + var services []appservice.Service + for _, resource := range deployment.GetResourcesByType("Microsoft.Web/sites") { + services = append(services, adaptService(resource)) + } + return services +} + +func adaptFunctionApp(resource azure.Resource) appservice.FunctionApp { + return appservice.FunctionApp{ + Metadata: resource.Metadata, + HTTPSOnly: resource.Properties.GetMapValue("httpsOnly"). + AsBoolValue(false, resource.Properties.GetMetadata()), + } +} + +func adaptService(resource azure.Resource) appservice.Service { + props := resource.Properties + identity := props.GetMapValue("identity") + siteAuthSettings := props.GetMapValue("siteAuthSettings") + siteConfig := props.GetMapValue("siteConfig") + return appservice.Service{ + Metadata: resource.Metadata, + Resource: types.String("Microsoft.Web/sites", resource.Metadata), + EnableClientCert: props.GetMapValue("clientCertEnabled").AsBoolValue(false, props.GetMetadata()), + HTTPSOnly: props.GetMapValue("httpsOnly").AsBoolValue(false, props.GetMetadata()), + Identity: appservice.Identity{ + Metadata: identity.GetMetadata(), + Type: identity.GetMapValue("type"). + AsStringValue("", props.GetMetadata()), + }, + Authentication: appservice.Authentication{ + Metadata: siteAuthSettings.GetMetadata(), + Enabled: siteAuthSettings.GetMapValue("enabled").AsBoolValue(false, props.GetMetadata()), + }, + Site: appservice.Site{ + EnableHTTP2: siteConfig.GetMapValue("http20Enabled").AsBoolValue(false, siteConfig.GetMetadata()), + MinimumTLSVersion: siteConfig.GetMapValue("minTlsVersion").AsStringValue("", siteConfig.GetMetadata()), + PHPVersion: siteConfig.GetMapValue("phpVersion").AsStringValue("", siteConfig.GetMetadata()), + PythonVersion: siteConfig.GetMapValue("pythonVersion").AsStringValue("", siteConfig.GetMetadata()), + FTPSState: siteConfig.GetMapValue("ftpsState").AsStringValue("", siteConfig.GetMetadata()), + }, + } +} diff --git a/pkg/iac/adapters/arm/appservice/adapt_test.go b/pkg/iac/adapters/arm/appservice/adapt_test.go new file mode 100644 index 000000000000..19fe144a3318 --- /dev/null +++ b/pkg/iac/adapters/arm/appservice/adapt_test.go @@ -0,0 +1,92 @@ +package appservice + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/appservice" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected appservice.AppService + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.Web/sites", + "properties": {} + } + ] +}`, + expected: appservice.AppService{ + Services: []appservice.Service{{ + Resource: types.StringTest("Microsoft.Web/sites"), + }}, + FunctionApps: []appservice.FunctionApp{{}}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.Web/sites", + "properties": { + "httpsOnly": true, + "clientCertEnabled": true, + "identity": { + "type": "SystemAssigned" + }, + "siteAuthSettings": { + "enabled": true + }, + "minTlsVersion": "1.3", + "siteConfig": { + "http20Enabled": true, + "minTlsVersion": "1.2", + "phpVersion": "8.1", + "pythonVersion": "3.11", + "ftpsState": "FtpsOnly" + } + } + } + ] +}`, + expected: appservice.AppService{ + Services: []appservice.Service{{ + Resource: types.StringTest("Microsoft.Web/sites"), + EnableClientCert: types.BoolTest(true), + HTTPSOnly: types.BoolTest(true), + Identity: appservice.Identity{ + Type: types.StringTest("SystemAssigned"), + }, + Authentication: appservice.Authentication{ + Enabled: types.BoolTest(true), + }, + Site: appservice.Site{ + EnableHTTP2: types.BoolTest(true), + MinimumTLSVersion: types.StringTest("1.2"), + PHPVersion: types.StringTest("8.1"), + PythonVersion: types.StringTest("3.11"), + FTPSState: types.StringTest("FtpsOnly"), + }, + }}, + FunctionApps: []appservice.FunctionApp{{ + HTTPSOnly: types.BoolTest(true), + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/arm/authorization/adapt.go b/pkg/iac/adapters/arm/authorization/adapt.go new file mode 100644 index 000000000000..0c61299b3846 --- /dev/null +++ b/pkg/iac/adapters/arm/authorization/adapt.go @@ -0,0 +1,57 @@ +package authorization + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/authorization" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(deployment azure.Deployment) authorization.Authorization { + return authorization.Authorization{ + RoleDefinitions: adaptRoleDefinitions(deployment), + RoleAssignments: adaptRoleAssignments(deployment), + } +} + +func adaptRoleDefinitions(deployment azure.Deployment) (roleDefinitions []authorization.RoleDefinition) { + for _, resource := range deployment.GetResourcesByType("Microsoft.Authorization/roleDefinitions") { + roleDefinitions = append(roleDefinitions, adaptRoleDefinition(resource)) + } + return roleDefinitions +} + +func adaptRoleDefinition(resource azure.Resource) authorization.RoleDefinition { + + return authorization.RoleDefinition{ + Metadata: resource.Metadata, + Permissions: adaptPermissions(resource), + AssignableScopes: resource.Properties.GetMapValue("assignableScopes").AsStringValuesList(""), + } +} + +func adaptRoleAssignments(deployment azure.Deployment) (roleAssignments []authorization.RoleAssignment) { + for _, resource := range deployment.GetResourcesByType("Microsoft.Authorization/roleAssignments") { + roleAssignments = append(roleAssignments, adaptRoleAssignment(resource)) + } + return roleAssignments +} + +func adaptRoleAssignment(resource azure.Resource) authorization.RoleAssignment { + return authorization.RoleAssignment{ + Metadata: resource.Metadata, + RoleDefinitionId: resource.Properties.GetMapValue("roleDefinitionId").AsStringValue("", resource.Metadata), + RoleDefinitionName: iacTypes.String("", iacTypes.NewUnmanagedMetadata()), + PrincipalId: resource.Properties.GetMapValue("principalId").AsStringValue("", resource.Metadata), + PrincipalType: resource.Properties.GetMapValue("principalType").AsStringValue("", resource.Metadata), + } +} + +func adaptPermissions(resource azure.Resource) (permissions []authorization.Permission) { + for _, permission := range resource.Properties.GetMapValue("permissions").AsList() { + permissions = append(permissions, authorization.Permission{ + Metadata: resource.Metadata, + Actions: permission.GetMapValue("actions").AsStringValuesList(""), + }) + } + return permissions +} diff --git a/pkg/iac/adapters/arm/authorization/adapt_test.go b/pkg/iac/adapters/arm/authorization/adapt_test.go new file mode 100644 index 000000000000..8685e238a53b --- /dev/null +++ b/pkg/iac/adapters/arm/authorization/adapt_test.go @@ -0,0 +1,118 @@ +package authorization + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/authorization" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected authorization.Authorization + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.Authorization/roleDefinitions", + "properties": {} + } + ] +}`, + expected: authorization.Authorization{ + RoleDefinitions: []authorization.RoleDefinition{{}}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.Authorization/roleDefinitions", + "properties": { + "assignableScopes": [ + "/providers/Microsoft.Management/managementGroups/foo" + ], + "permissions": [ + { + "actions": [ + "Microsoft.Compute/virtualMachines/read" + ] + } + ] + } + } + ] +}`, + expected: authorization.Authorization{ + RoleDefinitions: []authorization.RoleDefinition{{ + AssignableScopes: []types.StringValue{ + types.StringTest("/providers/Microsoft.Management/managementGroups/foo"), + }, + Permissions: []authorization.Permission{{ + Actions: []types.StringValue{ + types.StringTest("Microsoft.Compute/virtualMachines/read"), + }, + }}, + }}, + }, + }, + { + name: "role assignment with complete properties", + source: `{ + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "properties": { + "scope": "/subscriptions/12345678-1234-1234-1234-123456789012", + "roleDefinitionId": "/subscriptions/12345678-1234-1234-1234-123456789012/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "principalId": "11111111-1111-1111-1111-111111111111", + "principalType": "User" + } + } + ] +}`, + expected: authorization.Authorization{ + RoleAssignments: []authorization.RoleAssignment{{ + RoleDefinitionId: types.StringTest("/subscriptions/12345678-1234-1234-1234-123456789012/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635"), + RoleDefinitionName: types.String("", types.NewUnmanagedMetadata()), + PrincipalId: types.StringTest("11111111-1111-1111-1111-111111111111"), + PrincipalType: types.StringTest("User"), + }}, + }, + }, + { + name: "role assignment with missing properties", + source: `{ + "resources": [ + { + "type": "Microsoft.Authorization/roleAssignments", + "properties": { + "scope": "/subscriptions/12345678-1234-1234-1234-123456789012" + } + } + ] +}`, + expected: authorization.Authorization{ + RoleAssignments: []authorization.RoleAssignment{{ + RoleDefinitionId: types.StringTest(""), + RoleDefinitionName: types.String("", types.NewUnmanagedMetadata()), + PrincipalId: types.StringTest(""), + PrincipalType: types.StringTest(""), + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/arm/compute/adapt.go b/pkg/iac/adapters/arm/compute/adapt.go new file mode 100644 index 000000000000..c6352e2162ec --- /dev/null +++ b/pkg/iac/adapters/arm/compute/adapt.go @@ -0,0 +1,99 @@ +package compute + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/compute" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(deployment azure.Deployment) compute.Compute { + return compute.Compute{ + LinuxVirtualMachines: adaptLinuxVirtualMachines(deployment), + WindowsVirtualMachines: adaptWindowsVirtualMachines(deployment), + ManagedDisks: adaptManagedDisks(deployment), + } +} + +func adaptManagedDisks(deployment azure.Deployment) []compute.ManagedDisk { + var managedDisks []compute.ManagedDisk + + for _, resource := range deployment.GetResourcesByType("Microsoft.Compute/disks") { + managedDisks = append(managedDisks, adaptManagedDisk(resource)) + } + + return managedDisks +} + +func adaptManagedDisk(resource azure.Resource) compute.ManagedDisk { + hasEncryption := resource.Properties.HasKey("encryption") + + return compute.ManagedDisk{ + Metadata: resource.Metadata, + Encryption: compute.Encryption{ + Metadata: resource.Metadata, + Enabled: iacTypes.Bool(hasEncryption, resource.Metadata), + }, + } +} + +func adaptWindowsVirtualMachines(deployment azure.Deployment) (windowsVirtualMachines []compute.WindowsVirtualMachine) { + for _, resource := range deployment.GetResourcesByType("Microsoft.Compute/virtualMachines") { + if resource.Properties.GetMapValue("osProfile").GetMapValue("windowsConfiguration").AsMap() != nil { + windowsVirtualMachines = append(windowsVirtualMachines, adaptWindowsVirtualMachine(resource)) + } + } + + return windowsVirtualMachines +} + +func adaptWindowsVirtualMachine(resource azure.Resource) compute.WindowsVirtualMachine { + networkProfile := resource.Properties.GetMapValue("networkProfile") + networkInterfaces := extractNetworkInterfaces(networkProfile, resource.Metadata) + + return compute.WindowsVirtualMachine{ + Metadata: resource.Metadata, + VirtualMachine: compute.VirtualMachine{ + Metadata: resource.Metadata, + CustomData: resource.Properties.GetMapValue("osProfile"). + GetMapValue("customData").AsStringValue("", resource.Metadata), + NetworkInterfaces: networkInterfaces, + }, + } +} + +func adaptLinuxVirtualMachines(deployment azure.Deployment) (linuxVirtualMachines []compute.LinuxVirtualMachine) { + for _, resource := range deployment.GetResourcesByType("Microsoft.Compute/virtualMachines") { + if resource.Properties.GetMapValue("osProfile").GetMapValue("linuxConfiguration").AsMap() != nil { + linuxVirtualMachines = append(linuxVirtualMachines, adaptLinuxVirtualMachine(resource)) + } + } + + return linuxVirtualMachines +} + +func adaptLinuxVirtualMachine(resource azure.Resource) compute.LinuxVirtualMachine { + networkProfile := resource.Properties.GetMapValue("networkProfile") + networkInterfaces := extractNetworkInterfaces(networkProfile, resource.Metadata) + + return compute.LinuxVirtualMachine{ + Metadata: resource.Metadata, + VirtualMachine: compute.VirtualMachine{ + Metadata: resource.Metadata, + CustomData: resource.Properties.GetMapValue("osProfile"). + GetMapValue("customData").AsStringValue("", resource.Metadata), + NetworkInterfaces: networkInterfaces, + }, + OSProfileLinuxConfig: compute.OSProfileLinuxConfig{ + Metadata: resource.Metadata, + DisablePasswordAuthentication: resource.Properties.GetMapValue("osProfile"). + GetMapValue("linuxConfiguration"). + GetMapValue("disablePasswordAuthentication").AsBoolValue(false, resource.Metadata), + }, + } + +} + +func extractNetworkInterfaces(_ azure.Value, _ iacTypes.Metadata) []network.NetworkInterface { + return nil +} diff --git a/pkg/iac/adapters/arm/compute/adapt_test.go b/pkg/iac/adapters/arm/compute/adapt_test.go new file mode 100644 index 000000000000..47ba54bf2cd2 --- /dev/null +++ b/pkg/iac/adapters/arm/compute/adapt_test.go @@ -0,0 +1,139 @@ +package compute + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/compute" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected compute.Compute + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.Compute/disks", + "properties": {} + }, + { + "type": "Microsoft.Compute/virtualMachines", + "properties": { + "osProfile": { + "linuxConfiguration": {}, + "windowsConfiguration": {} + } + } + } + ] +}`, + expected: compute.Compute{ + ManagedDisks: []compute.ManagedDisk{{}}, + LinuxVirtualMachines: []compute.LinuxVirtualMachine{{}}, + WindowsVirtualMachines: []compute.WindowsVirtualMachine{{}}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.Compute/disks", + "properties": { + "encryption": {} + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "properties": { + "osProfile": { + "customData": "Zm9v", + "linuxConfiguration": { + "disablePasswordAuthentication": true + }, + "windowsConfiguration": {} + } + } + } + ] +}`, + expected: compute.Compute{ + ManagedDisks: []compute.ManagedDisk{{ + Encryption: compute.Encryption{ + Enabled: types.BoolTest(true), + }, + }}, + LinuxVirtualMachines: []compute.LinuxVirtualMachine{{ + VirtualMachine: compute.VirtualMachine{ + CustomData: types.StringTest("Zm9v"), + }, + OSProfileLinuxConfig: compute.OSProfileLinuxConfig{ + DisablePasswordAuthentication: types.BoolTest(true), + }, + }}, + WindowsVirtualMachines: []compute.WindowsVirtualMachine{{ + VirtualMachine: compute.VirtualMachine{ + CustomData: types.StringTest("Zm9v"), + }, + }}, + }, + }, + { + name: "with network interfaces", + source: `{ + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "properties": { + "osProfile": { + "customData": "test", + "linuxConfiguration": { + "disablePasswordAuthentication": false + }, + "windowsConfiguration": {} + }, + "networkProfile": { + "networkInterfaces": [ + { + "id": "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/networkInterfaces/nic-1" + }, + { + "id": "/subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/networkInterfaces/nic-2" + } + ] + } + } + } + ] +}`, + expected: compute.Compute{ + LinuxVirtualMachines: []compute.LinuxVirtualMachine{{ + VirtualMachine: compute.VirtualMachine{ + CustomData: types.StringTest("test"), + }, + OSProfileLinuxConfig: compute.OSProfileLinuxConfig{ + DisablePasswordAuthentication: types.BoolTest(false), + }, + }}, + WindowsVirtualMachines: []compute.WindowsVirtualMachine{{ + VirtualMachine: compute.VirtualMachine{ + CustomData: types.StringTest("test"), + }, + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/arm/container/adapt.go b/pkg/iac/adapters/arm/container/adapt.go new file mode 100644 index 000000000000..480b43f6f9f7 --- /dev/null +++ b/pkg/iac/adapters/arm/container/adapt.go @@ -0,0 +1,70 @@ +package container + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/container" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" +) + +func Adapt(deployment azure.Deployment) container.Container { + return container.Container{ + KubernetesClusters: adaptKubernetesClusters(deployment), + } +} + +func adaptKubernetesClusters(deployment azure.Deployment) []container.KubernetesCluster { + var clusters []container.KubernetesCluster + for _, resource := range deployment.GetResourcesByType("Microsoft.ContainerService/managedClusters") { + props := resource.Properties + + cluster := container.KubernetesCluster{ + Metadata: resource.Metadata, + NetworkProfile: container.NetworkProfile{ + Metadata: props.GetMapValue("networkProfile").GetMetadata(), + NetworkPolicy: props.GetMapValue("networkProfile").GetMapValue("networkPolicy").AsStringValue("", props.GetMapValue("networkProfile").GetMetadata()), + }, + EnablePrivateCluster: props.GetMapValue("apiServerAccessProfile").GetMapValue("enablePrivateCluster").AsBoolValue(false, props.GetMapValue("apiServerAccessProfile").GetMetadata()), + APIServerAuthorizedIPRanges: nil, // Extracted below + RoleBasedAccessControl: container.RoleBasedAccessControl{ + Metadata: props.GetMetadata(), + Enabled: props.GetMapValue("enableRBAC").AsBoolValue(false, props.GetMetadata()), + }, + AddonProfile: container.AddonProfile{ + Metadata: props.GetMapValue("addonProfiles").GetMetadata(), + OMSAgent: container.OMSAgent{ + Metadata: props.GetMapValue("addonProfiles").GetMapValue("omsagent").GetMetadata(), + Enabled: props.GetMapValue("addonProfiles").GetMapValue("omsagent").GetMapValue("enabled").AsBoolValue(false, props.GetMapValue("addonProfiles").GetMapValue("omsagent").GetMetadata()), + }, + AzurePolicy: container.AzurePolicy{ + Metadata: props.GetMapValue("addonProfiles").GetMapValue("azurepolicy").GetMetadata(), + Enabled: props.GetMapValue("addonProfiles").GetMapValue("azurepolicy").GetMapValue("enabled").AsBoolValue(false, props.GetMapValue("addonProfiles").GetMapValue("azurepolicy").GetMetadata()), + }, + }, + DiskEncryptionSetID: props.GetMapValue("diskEncryptionSetID").AsStringValue("", props.GetMetadata()), + AgentPools: adaptAgentPools(resource), + } + + // API Server Authorized IP Ranges + if ranges := props.GetMapValue("apiServerAccessProfile").GetMapValue("authorizedIPRanges"); !ranges.IsNull() { + cluster.APIServerAuthorizedIPRanges = ranges.AsStringValuesList("") + } + + clusters = append(clusters, cluster) + } + return clusters +} + +func adaptAgentPools(resource azure.Resource) []container.AgentPool { + var pools []container.AgentPool + // agentPoolProfiles is an array of objects + profiles := resource.Properties.GetMapValue("agentPoolProfiles") + if profiles.Kind == azure.KindArray { + for _, profile := range profiles.AsList() { + pools = append(pools, container.AgentPool{ + Metadata: profile.GetMetadata(), + DiskEncryptionSetID: profile.GetMapValue("diskEncryptionSetID").AsStringValue("", profile.GetMetadata()), + NodeType: profile.GetMapValue("type").AsStringValue("VirtualMachineScaleSets", profile.GetMetadata()), + }) + } + } + return pools +} diff --git a/pkg/iac/adapters/arm/container/adapt_test.go b/pkg/iac/adapters/arm/container/adapt_test.go new file mode 100644 index 000000000000..ed3af057c617 --- /dev/null +++ b/pkg/iac/adapters/arm/container/adapt_test.go @@ -0,0 +1,121 @@ +package container + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/container" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected container.Container + }{ + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.ContainerService/managedClusters", + "properties": { + "networkProfile": { + "networkPolicy": "calico" + }, + "apiServerAccessProfile": { + "enablePrivateCluster": true, + "authorizedIPRanges": ["1.2.3.4/32"] + }, + "enableRBAC": true, + "addonProfiles": { + "omsagent": { "enabled": true }, + "azurepolicy": { "enabled": true } + }, + "diskEncryptionSetID": "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/diskEncryptionSets/des", + "agentPoolProfiles": [ + { + "name": "nodepool1", + "type": "VirtualMachineScaleSets", + "diskEncryptionSetID": "/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/diskEncryptionSets/des-pool" + } + ] + } + } + ] +}`, + expected: container.Container{ + KubernetesClusters: []container.KubernetesCluster{ + { + NetworkProfile: container.NetworkProfile{ + NetworkPolicy: types.StringTest("calico"), + }, + EnablePrivateCluster: types.BoolTest(true), + APIServerAuthorizedIPRanges: []types.StringValue{types.StringTest("1.2.3.4/32")}, + RoleBasedAccessControl: container.RoleBasedAccessControl{ + Enabled: types.BoolTest(true), + }, + AddonProfile: container.AddonProfile{ + OMSAgent: container.OMSAgent{ + Enabled: types.BoolTest(true), + }, + AzurePolicy: container.AzurePolicy{ + Enabled: types.BoolTest(true), + }, + }, + DiskEncryptionSetID: types.StringTest("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/diskEncryptionSets/des"), + AgentPools: []container.AgentPool{ + { + DiskEncryptionSetID: types.StringTest("/subscriptions/sub/resourceGroups/rg/providers/Microsoft.Compute/diskEncryptionSets/des-pool"), + NodeType: types.StringTest("VirtualMachineScaleSets"), + }, + }, + }, + }, + }, + }, + { + name: "defaults", + source: `{ + "resources": [ + { + "type": "Microsoft.ContainerService/managedClusters", + "properties": { + } + } + ] +}`, + expected: container.Container{ + KubernetesClusters: []container.KubernetesCluster{ + { + NetworkProfile: container.NetworkProfile{ + NetworkPolicy: types.StringTest(""), + }, + EnablePrivateCluster: types.BoolTest(false), + APIServerAuthorizedIPRanges: nil, + RoleBasedAccessControl: container.RoleBasedAccessControl{ + Enabled: types.BoolTest(false), + }, + AddonProfile: container.AddonProfile{ + OMSAgent: container.OMSAgent{ + Enabled: types.BoolTest(false), + }, + AzurePolicy: container.AzurePolicy{ + Enabled: types.BoolTest(false), + }, + }, + DiskEncryptionSetID: types.StringTest(""), + AgentPools: nil, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/arm/database/adapt.go b/pkg/iac/adapters/arm/database/adapt.go new file mode 100644 index 000000000000..8eab5d42f16a --- /dev/null +++ b/pkg/iac/adapters/arm/database/adapt.go @@ -0,0 +1,35 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" +) + +func Adapt(deployment azure.Deployment) database.Database { + return database.Database{ + MSSQLServers: adaptMSSQLServers(deployment), + MariaDBServers: adaptMariaDBServers(deployment), + MySQLServers: adaptMySQLServers(deployment), + PostgreSQLServers: adaptPostgreSQLServers(deployment), + } +} + +func adaptMySQLServers(deployment azure.Deployment) (mysqlDbServers []database.MySQLServer) { + for _, resource := range deployment.GetResourcesByType("Microsoft.DBforMySQL/servers") { + mysqlDbServers = append(mysqlDbServers, adaptMySQLServer(resource, deployment)) + } + return mysqlDbServers +} + +func adaptMySQLServer(resource azure.Resource, _ azure.Deployment) database.MySQLServer { + return database.MySQLServer{ + Metadata: resource.Metadata, + Server: database.Server{ + Metadata: resource.Metadata, + EnableSSLEnforcement: resource.Properties.GetMapValue("sslEnforcement").AsBoolValue(false, resource.Metadata), + MinimumTLSVersion: resource.Properties.GetMapValue("minimalTlsVersion").AsStringValue("TLSEnforcementDisabled", resource.Metadata), + EnablePublicNetworkAccess: resource.Properties.GetMapValue("publicNetworkAccess").AsBoolValue(false, resource.Metadata), + FirewallRules: addFirewallRule(resource), + }, + } +} diff --git a/pkg/iac/adapters/arm/database/adapt_test.go b/pkg/iac/adapters/arm/database/adapt_test.go new file mode 100644 index 000000000000..38510f4a142a --- /dev/null +++ b/pkg/iac/adapters/arm/database/adapt_test.go @@ -0,0 +1,193 @@ +package database + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected database.Database + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.DBforMySQL/servers", + "properties": {} + }, + { + "type": "Microsoft.Sql/servers", + "properties": {} + }, + { + "type": "Microsoft.DBforMariaDB/servers", + "properties": {} + }, + { + "type": "Microsoft.DBforPostgreSQL/servers", + "properties": {} + } + ] +}`, + expected: database.Database{ + MSSQLServers: []database.MSSQLServer{{ + Server: database.Server{ + MinimumTLSVersion: types.StringTest("TLSEnforcementDisabled"), + }, + }}, + MySQLServers: []database.MySQLServer{{ + Server: database.Server{ + MinimumTLSVersion: types.StringTest("TLSEnforcementDisabled"), + }, + }}, + MariaDBServers: []database.MariaDBServer{{ + Server: database.Server{ + MinimumTLSVersion: types.StringTest("TLSEnforcementDisabled"), + }, + }}, + PostgreSQLServers: []database.PostgreSQLServer{{ + Server: database.Server{ + MinimumTLSVersion: types.StringTest("TLSEnforcementDisabled"), + }, + }}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.DBforMySQL/servers", + "properties": { + "sslEnforcement": "Enabled", + "minimalTlsVersion": "TLS1_2", + "publicNetworkAccess": "Enabled" + } + }, + { + "type": "Microsoft.Sql/servers", + "properties": { + "minimalTlsVersion": "TLS1_2", + "publicNetworkAccess": "Enabled" + } + }, + { + "type": "Microsoft.Sql/servers/extendedAuditingSettings", + "properties": { + "retentionDays": 100 + } + }, + { + "type": "Microsoft.Sql/servers/securityAlertPolicies", + "properties": { + "emailAddresses": [ + "foo@bar.io" + ], + "disabledAlerts": [ + "Sql_Injection" + ], + "emailAccountAdmins": true + } + }, + { + "type": "Microsoft.DBforMariaDB/servers", + "properties": { + "sslEnforcement": "Enabled", + "minimalTlsVersion": "TLS1_2", + "publicNetworkAccess": "Enabled" + } + }, + { + "type": "Microsoft.DBforPostgreSQL/servers", + "name": "foo", + "properties": { + "sslEnforcement": "Enabled", + "minimalTlsVersion": "TLS1_2", + "publicNetworkAccess": "Enabled" + } + }, + { + "type": "Microsoft.DBforPostgreSQL/servers/configurations", + "name": "foo/log_connections", + "properties": { + "value": "ON", + "source": "user-override" + } + }, + { + "type": "Microsoft.DBforPostgreSQL/servers/configurations", + "name": "foo/log_checkpoints", + "properties": { + "value": "ON", + "source": "user-override" + } + }, + { + "type": "Microsoft.DBforPostgreSQL/servers/configurations", + "name": "foo/connection_throttling", + "properties": { + "value": "ON", + "source": "user-override" + } + } + ] +}`, + expected: database.Database{ + MSSQLServers: []database.MSSQLServer{{ + Server: database.Server{ + MinimumTLSVersion: types.StringTest("TLS1_2"), + EnablePublicNetworkAccess: types.BoolTest(true), + }, + ExtendedAuditingPolicies: []database.ExtendedAuditingPolicy{{ + RetentionInDays: types.IntTest(100), + }}, + SecurityAlertPolicies: []database.SecurityAlertPolicy{{ + EmailAddresses: []types.StringValue{types.StringTest("foo@bar.io")}, + DisabledAlerts: []types.StringValue{types.StringTest("Sql_Injection")}, + EmailAccountAdmins: types.BoolTest(true), + }}, + }}, + MySQLServers: []database.MySQLServer{{ + Server: database.Server{ + EnableSSLEnforcement: types.BoolTest(true), + MinimumTLSVersion: types.StringTest("TLS1_2"), + EnablePublicNetworkAccess: types.BoolTest(true), + }, + }}, + MariaDBServers: []database.MariaDBServer{{ + Server: database.Server{ + EnableSSLEnforcement: types.BoolTest(true), + MinimumTLSVersion: types.StringTest("TLS1_2"), + EnablePublicNetworkAccess: types.BoolTest(true), + }, + }}, + PostgreSQLServers: []database.PostgreSQLServer{{ + Server: database.Server{ + EnableSSLEnforcement: types.BoolTest(true), + MinimumTLSVersion: types.StringTest("TLS1_2"), + EnablePublicNetworkAccess: types.BoolTest(true), + }, + Config: database.PostgresSQLConfig{ + LogCheckpoints: types.BoolTest(true), + LogConnections: types.BoolTest(true), + ConnectionThrottling: types.BoolTest(true), + }, + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/arm/database/firewall.go b/pkg/iac/adapters/arm/database/firewall.go new file mode 100644 index 000000000000..e59f6374dfc2 --- /dev/null +++ b/pkg/iac/adapters/arm/database/firewall.go @@ -0,0 +1,20 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" +) + +// TODO: This is the "Microsoft.Sql/servers/firewallRules" resource +// https://learn.microsoft.com/en-us/azure/templates/microsoft.sql/servers/firewallrules?pivots=deployment-language-arm-template +func addFirewallRule(resource azure.Resource) []database.FirewallRule { + var rules []database.FirewallRule + for _, rule := range resource.Properties.GetMapValue("firewallRules").AsMap() { + rules = append(rules, database.FirewallRule{ + Metadata: rule.GetMetadata(), + StartIP: rule.GetMapValue("startIpAddress").AsStringValue("", rule.GetMetadata()), + EndIP: rule.GetMapValue("endIpAddress").AsStringValue("", rule.GetMetadata()), + }) + } + return rules +} diff --git a/pkg/iac/adapters/arm/database/maria.go b/pkg/iac/adapters/arm/database/maria.go new file mode 100644 index 000000000000..b576813be73b --- /dev/null +++ b/pkg/iac/adapters/arm/database/maria.go @@ -0,0 +1,27 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" +) + +func adaptMariaDBServers(deployment azure.Deployment) (mariaDbServers []database.MariaDBServer) { + for _, resource := range deployment.GetResourcesByType("Microsoft.DBforMariaDB/servers") { + mariaDbServers = append(mariaDbServers, adaptMariaDBServer(resource, deployment)) + } + return mariaDbServers + +} + +func adaptMariaDBServer(resource azure.Resource, _ azure.Deployment) database.MariaDBServer { + return database.MariaDBServer{ + Metadata: resource.Metadata, + Server: database.Server{ + Metadata: resource.Metadata, + EnableSSLEnforcement: resource.Properties.GetMapValue("sslEnforcement").AsBoolValue(false, resource.Metadata), + MinimumTLSVersion: resource.Properties.GetMapValue("minimalTlsVersion").AsStringValue("TLSEnforcementDisabled", resource.Metadata), + EnablePublicNetworkAccess: resource.Properties.GetMapValue("publicNetworkAccess").AsBoolValue(false, resource.Metadata), + FirewallRules: addFirewallRule(resource), + }, + } +} diff --git a/pkg/iac/adapters/arm/database/mssql.go b/pkg/iac/adapters/arm/database/mssql.go new file mode 100644 index 000000000000..4b545c13e5d3 --- /dev/null +++ b/pkg/iac/adapters/arm/database/mssql.go @@ -0,0 +1,80 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + azure2 "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptMSSQLServers(deployment azure2.Deployment) (msSQlServers []database.MSSQLServer) { + for _, resource := range deployment.GetResourcesByType("Microsoft.Sql/servers") { + msSQlServers = append(msSQlServers, adaptMSSQLServer(resource, deployment)) + } + return msSQlServers +} + +func adaptMSSQLServer(resource azure2.Resource, deployment azure2.Deployment) database.MSSQLServer { + properties := resource.Properties + administratorLogin := properties.GetMapValue("administratorLogin").AsStringValue("", resource.Metadata) + + // Support for azureadAdministrator block (ARM uses administrators property) + var adAdmins []database.ActiveDirectoryAdministrator + administrators := properties.GetMapValue("administrators") + if administrators.Kind != azure2.KindNull { + login := administrators.GetMapValue("login").AsStringValue("", administrators.GetMetadata()) + if !login.IsEmpty() { + adAdmins = append(adAdmins, database.ActiveDirectoryAdministrator{ + Metadata: administrators.GetMetadata(), + Login: login, + }) + } + } + + return database.MSSQLServer{ + Metadata: resource.Metadata, + Server: database.Server{ + Metadata: resource.Metadata, + // TODO: this property doesn't exist. + EnableSSLEnforcement: properties.GetMapValue("sslEnforcement").AsBoolValue(false, resource.Metadata), + MinimumTLSVersion: properties.GetMapValue("minimalTlsVersion").AsStringValue("TLSEnforcementDisabled", resource.Metadata), + EnablePublicNetworkAccess: properties.GetMapValue("publicNetworkAccess").AsBoolValue(false, resource.Metadata), + FirewallRules: addFirewallRule(resource), + }, + ExtendedAuditingPolicies: adaptExtendedAuditingPolicies(resource, deployment), + SecurityAlertPolicies: adaptSecurityAlertPolicies(resource, deployment), + AdministratorLogin: administratorLogin, + ActiveDirectoryAdministrators: adAdmins, + } +} + +func adaptExtendedAuditingPolicies(_ azure2.Resource, deployment azure2.Deployment) (policies []database.ExtendedAuditingPolicy) { + + for _, policy := range deployment.GetResourcesByType("Microsoft.Sql/servers/extendedAuditingSettings") { + policies = append(policies, database.ExtendedAuditingPolicy{ + Metadata: policy.Metadata, + RetentionInDays: policy.Properties.GetMapValue("retentionDays").AsIntValue(0, policy.Metadata), + }) + } + + return policies +} + +func adaptSecurityAlertPolicies(_ azure2.Resource, deployment azure2.Deployment) (policies []database.SecurityAlertPolicy) { + for _, policy := range deployment.GetResourcesByType("Microsoft.Sql/servers/securityAlertPolicies") { + policies = append(policies, database.SecurityAlertPolicy{ + Metadata: policy.Metadata, + EmailAddresses: adaptStringList(policy.Properties.GetMapValue("emailAddresses")), + DisabledAlerts: adaptStringList(policy.Properties.GetMapValue("disabledAlerts")), + EmailAccountAdmins: policy.Properties.GetMapValue("emailAccountAdmins").AsBoolValue(false, policy.Metadata), + }) + } + return policies +} + +func adaptStringList(value azure2.Value) []iacTypes.StringValue { + var list []iacTypes.StringValue + for _, v := range value.AsList() { + list = append(list, v.AsStringValue("", value.GetMetadata())) + } + return list +} diff --git a/pkg/iac/adapters/arm/database/postgresql.go b/pkg/iac/adapters/arm/database/postgresql.go new file mode 100644 index 000000000000..f4160aa58280 --- /dev/null +++ b/pkg/iac/adapters/arm/database/postgresql.go @@ -0,0 +1,104 @@ +package database + +import ( + "fmt" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptPostgreSQLServers(deployment azure.Deployment) (databases []database.PostgreSQLServer) { + for _, resource := range deployment.GetResourcesByType("Microsoft.DBforPostgreSQL/servers") { + databases = append(databases, adaptPostgreSQLServer(resource, deployment)) + } + + return databases +} + +func adaptPostgreSQLServer(resource azure.Resource, deployment azure.Deployment) database.PostgreSQLServer { + properties := resource.Properties + geoRedundantBackup := properties.GetMapValue("storageProfile").GetMapValue("geoRedundantBackup") + geoRedundantBackupEnabled := geoRedundantBackup.AsStringValue("Disabled", resource.Metadata) + + threatDetectionPolicy := adaptThreatDetectionPolicy(resource, deployment) + + return database.PostgreSQLServer{ + Metadata: resource.Metadata, + Server: database.Server{ + Metadata: resource.Metadata, + EnableSSLEnforcement: properties.GetMapValue("sslEnforcement").AsBoolValue(false, resource.Metadata), + MinimumTLSVersion: properties.GetMapValue("minimalTlsVersion").AsStringValue("TLSEnforcementDisabled", resource.Metadata), + EnablePublicNetworkAccess: properties.GetMapValue("publicNetworkAccess").AsBoolValue(false, resource.Metadata), + FirewallRules: addFirewallRule(resource), + }, + Config: adaptPostgreSQLConfiguration(resource, deployment), + GeoRedundantBackupEnabled: iacTypes.Bool(geoRedundantBackupEnabled.EqualTo("Enabled"), geoRedundantBackup.GetMetadata()), + ThreatDetectionPolicy: threatDetectionPolicy, + } +} + +func adaptPostgreSQLConfiguration(resource azure.Resource, deployment azure.Deployment) database.PostgresSQLConfig { + + parent := fmt.Sprintf("%s/", resource.Name.AsString()) + + config := database.PostgresSQLConfig{ + Metadata: resource.Metadata, + LogCheckpoints: iacTypes.BoolDefault(false, resource.Metadata), + ConnectionThrottling: iacTypes.BoolDefault(false, resource.Metadata), + LogConnections: iacTypes.BoolDefault(false, resource.Metadata), + LogDisconnections: iacTypes.BoolDefault(false, resource.Metadata), + } + + for _, configuration := range deployment.GetResourcesByType("Microsoft.DBforPostgreSQL/servers/configurations") { + if !strings.HasPrefix(configuration.Name.AsString(), parent) { + continue + } + val := configuration.Properties.GetMapValue("value") + if strings.HasSuffix(configuration.Name.AsString(), "log_checkpoints") { + config.LogCheckpoints = val.AsBoolValue(false, configuration.Metadata) + continue + } + if strings.HasSuffix(configuration.Name.AsString(), "log_connections") { + config.LogConnections = val.AsBoolValue(false, configuration.Metadata) + continue + } + if strings.HasSuffix(configuration.Name.AsString(), "connection_throttling") { + config.ConnectionThrottling = val.AsBoolValue(false, configuration.Metadata) + continue + } + if strings.HasSuffix(configuration.Name.AsString(), "log_disconnections") { + config.LogDisconnections = val.AsBoolValue(false, configuration.Metadata) + continue + } + } + + return config +} + +func adaptThreatDetectionPolicy(resource azure.Resource, deployment azure.Deployment) database.ThreatDetectionPolicy { + // Threat detection policy is configured via Microsoft.DBforPostgreSQL/servers/securityAlertPolicies + // This is a separate child resource, not a property of the server resource + parent := fmt.Sprintf("%s/", resource.Name.AsString()) + + enabled := false + metadata := resource.Metadata + + // Look for security alert policy resources that belong to this server + for _, policy := range deployment.GetResourcesByType("Microsoft.DBforPostgreSQL/servers/securityAlertPolicies") { + if !strings.HasPrefix(policy.Name.AsString(), parent) { + continue + } + // Found the security alert policy for this server + state := policy.Properties.GetMapValue("state").AsStringValue("Disabled", policy.Metadata) + enabled = state.EqualTo("Enabled") + metadata = policy.Properties.GetMapValue("state").GetMetadata() + break + } + + return database.ThreatDetectionPolicy{ + Metadata: metadata, + Enabled: iacTypes.Bool(enabled, metadata), + } +} diff --git a/pkg/iac/adapters/arm/datafactory/adapt.go b/pkg/iac/adapters/arm/datafactory/adapt.go new file mode 100644 index 000000000000..7539635f3871 --- /dev/null +++ b/pkg/iac/adapters/arm/datafactory/adapt.go @@ -0,0 +1,29 @@ +package datafactory + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datafactory" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" +) + +func Adapt(deployment azure.Deployment) datafactory.DataFactory { + + return datafactory.DataFactory{ + DataFactories: adaptDataFactories(deployment), + } +} + +func adaptDataFactories(deployment azure.Deployment) (factories []datafactory.Factory) { + for _, resource := range deployment.GetResourcesByType("Microsoft.DataFactory/factories") { + factories = append(factories, adaptDataFactory(resource)) + } + return factories +} + +func adaptDataFactory(resource azure.Resource) datafactory.Factory { + return datafactory.Factory{ + Metadata: resource.Metadata, + // TODO: publicNetworkAccess is string + // https://learn.microsoft.com/en-us/azure/templates/microsoft.datafactory/factories?pivots=deployment-language-arm-template#factoryproperties-1 + EnablePublicNetwork: resource.Properties.GetMapValue("publicNetworkAccess").AsBoolValue(true, resource.Metadata), + } +} diff --git a/pkg/iac/adapters/arm/datafactory/adapt_test.go b/pkg/iac/adapters/arm/datafactory/adapt_test.go new file mode 100644 index 000000000000..0dd12243cc8b --- /dev/null +++ b/pkg/iac/adapters/arm/datafactory/adapt_test.go @@ -0,0 +1,59 @@ +package datafactory + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datafactory" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected datafactory.DataFactory + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.DataFactory/factories", + "properties": {} + } + ] +}`, + expected: datafactory.DataFactory{ + DataFactories: []datafactory.Factory{{ + EnablePublicNetwork: types.BoolTest(true), + }}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.DataFactory/factories", + "properties": { + "publicNetworkAccess": "Disabled" + } + } + ] +}`, + expected: datafactory.DataFactory{ + DataFactories: []datafactory.Factory{{ + EnablePublicNetwork: types.BoolTest(true), + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/arm/datalake/adapt.go b/pkg/iac/adapters/arm/datalake/adapt.go new file mode 100644 index 000000000000..7567e930c58d --- /dev/null +++ b/pkg/iac/adapters/arm/datalake/adapt.go @@ -0,0 +1,30 @@ +package datalake + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datalake" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" +) + +func Adapt(deployment azure.Deployment) datalake.DataLake { + + return datalake.DataLake{ + Stores: adaptStores(deployment), + } +} + +func adaptStores(deployment azure.Deployment) (stores []datalake.Store) { + for _, resource := range deployment.GetResourcesByType("Microsoft.DataLakeStore/accounts") { + stores = append(stores, adaptStore(resource)) + } + + return stores +} + +func adaptStore(resource azure.Resource) datalake.Store { + return datalake.Store{ + Metadata: resource.Metadata, + // TODO: encryptionState is string + // https://learn.microsoft.com/en-us/azure/templates/microsoft.datalakestore/accounts?pivots=deployment-language-arm-template#createdatalakestoreaccountpropertiesordatalakestoreaccountproperties-1 + EnableEncryption: resource.Properties.GetMapValue("encryptionState").AsBoolValue(false, resource.Metadata), + } +} diff --git a/pkg/iac/adapters/arm/datalake/adapt_test.go b/pkg/iac/adapters/arm/datalake/adapt_test.go new file mode 100644 index 000000000000..cd4164439232 --- /dev/null +++ b/pkg/iac/adapters/arm/datalake/adapt_test.go @@ -0,0 +1,59 @@ +package datalake + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datalake" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected datalake.DataLake + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.DataLakeStore/accounts", + "properties": {} + } + ] +}`, + expected: datalake.DataLake{ + Stores: []datalake.Store{{ + EnableEncryption: types.BoolTest(false), + }}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.DataLakeStore/accounts", + "properties": { + "encryptionState": "Enabled" + } + } + ] +}`, + expected: datalake.DataLake{ + Stores: []datalake.Store{{ + EnableEncryption: types.BoolTest(true), + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/arm/keyvault/adapt.go b/pkg/iac/adapters/arm/keyvault/adapt.go new file mode 100644 index 000000000000..cae3db33a0ca --- /dev/null +++ b/pkg/iac/adapters/arm/keyvault/adapt.go @@ -0,0 +1,64 @@ +package keyvault + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/keyvault" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" +) + +func Adapt(deployment azure.Deployment) keyvault.KeyVault { + return keyvault.KeyVault{ + Vaults: adaptVaults(deployment), + } +} + +func adaptVaults(deployment azure.Deployment) (vaults []keyvault.Vault) { + for _, resource := range deployment.GetResourcesByType("Microsoft.KeyVault/vaults") { + vaults = append(vaults, adaptVault(resource, deployment)) + } + + return vaults +} + +func adaptVault(resource azure.Resource, deployment azure.Deployment) keyvault.Vault { + return keyvault.Vault{ + Metadata: resource.Metadata, + Secrets: adaptSecrets(resource, deployment), + Keys: adaptKeys(resource, deployment), + EnablePurgeProtection: resource.Properties.GetMapValue("enablePurgeProtection").AsBoolValue(false, resource.Metadata), + SoftDeleteRetentionDays: resource.Properties.GetMapValue("softDeleteRetentionInDays").AsIntValue(7, resource.Metadata), + NetworkACLs: keyvault.NetworkACLs{ + Metadata: resource.Metadata, + DefaultAction: resource.Properties.GetMapValue("properties").GetMapValue("networkAcls").GetMapValue("defaultAction").AsStringValue("", resource.Metadata), + }, + } +} + +func adaptKeys(_ azure.Resource, deployment azure.Deployment) (keys []keyvault.Key) { + for _, resource := range deployment.GetResourcesByType("Microsoft.KeyVault/vaults/keys") { + keys = append(keys, adaptKey(resource)) + } + + return keys +} + +func adaptKey(resource azure.Resource) keyvault.Key { + return keyvault.Key{ + Metadata: resource.Metadata, + ExpiryDate: resource.Properties.GetMapValue("attributes").GetMapValue("exp").AsTimeValue(resource.Metadata), + } +} + +func adaptSecrets(_ azure.Resource, deployment azure.Deployment) (secrets []keyvault.Secret) { + for _, resource := range deployment.GetResourcesByType("Microsoft.KeyVault/vaults/secrets") { + secrets = append(secrets, adaptSecret(resource)) + } + return secrets +} + +func adaptSecret(resource azure.Resource) keyvault.Secret { + return keyvault.Secret{ + Metadata: resource.Metadata, + ContentType: resource.Properties.GetMapValue("contentType").AsStringValue("", resource.Metadata), + ExpiryDate: resource.Properties.GetMapValue("attributes").GetMapValue("exp").AsTimeValue(resource.Metadata), + } +} diff --git a/pkg/iac/adapters/arm/keyvault/adapt_test.go b/pkg/iac/adapters/arm/keyvault/adapt_test.go new file mode 100644 index 000000000000..88bf3cd2991c --- /dev/null +++ b/pkg/iac/adapters/arm/keyvault/adapt_test.go @@ -0,0 +1,96 @@ +package keyvault + +import ( + "testing" + "time" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/keyvault" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected keyvault.KeyVault + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "properties": {} + }, + { + "type": "Microsoft.KeyVault/vaults/keys", + "properties": {} + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "properties": {} + } + ] +}`, + expected: keyvault.KeyVault{ + Vaults: []keyvault.Vault{{ + SoftDeleteRetentionDays: types.IntTest(7), + Keys: []keyvault.Key{{}}, + Secrets: []keyvault.Secret{{}}, + }}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.KeyVault/vaults", + "properties": { + "enablePurgeProtection": true, + "softDeleteRetentionInDays": 50 + } + }, + { + "type": "Microsoft.KeyVault/vaults/keys", + "properties": { + "attributes": { + "exp": 20 + } + } + }, + { + "type": "Microsoft.KeyVault/vaults/secrets", + "properties": { + "attributes": { + "exp": 20 + }, + "contentType": "text/plain" + } + } + ] +}`, + expected: keyvault.KeyVault{ + Vaults: []keyvault.Vault{{ + EnablePurgeProtection: types.BoolTest(true), + SoftDeleteRetentionDays: types.IntTest(50), + Keys: []keyvault.Key{{ + ExpiryDate: types.TimeTest(time.Unix(20, 0)), + }}, + Secrets: []keyvault.Secret{{ + ExpiryDate: types.TimeTest(time.Unix(20, 0)), + ContentType: types.StringTest("text/plain"), + }}, + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/arm/monitor/adapt.go b/pkg/iac/adapters/arm/monitor/adapt.go new file mode 100644 index 000000000000..e54465d32bae --- /dev/null +++ b/pkg/iac/adapters/arm/monitor/adapt.go @@ -0,0 +1,47 @@ +package monitor + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/monitor" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(deployment azure.Deployment) monitor.Monitor { + return monitor.Monitor{ + LogProfiles: adaptLogProfiles(deployment), + } +} + +func adaptLogProfiles(deployment azure.Deployment) (logProfiles []monitor.LogProfile) { + // TODO: resource is "Microsoft.Insights/logprofiles" + // https://learn.microsoft.com/en-us/azure/templates/microsoft.insights/logprofiles?pivots=deployment-language-arm-template + for _, resource := range deployment.GetResourcesByType("Microsoft.Insights/logProfiles") { + logProfiles = append(logProfiles, adaptLogProfile(resource)) + } + return logProfiles +} + +func adaptLogProfile(resource azure.Resource) monitor.LogProfile { + categories := resource.Properties.GetMapValue("categories").AsList() + var categoriesList []types.StringValue + for _, category := range categories { + categoriesList = append(categoriesList, category.AsStringValue("", category.GetMetadata())) + } + + locations := resource.Properties.GetMapValue("locations").AsList() + var locationsList []types.StringValue + for _, location := range locations { + locationsList = append(locationsList, location.AsStringValue("", location.GetMetadata())) + } + + return monitor.LogProfile{ + Metadata: resource.Metadata, + RetentionPolicy: monitor.RetentionPolicy{ + Metadata: resource.Metadata, + Enabled: resource.Properties.GetMapValue("retentionPolicy").GetMapValue("enabled").AsBoolValue(false, resource.Metadata), + Days: resource.Properties.GetMapValue("retentionPolicy").GetMapValue("days").AsIntValue(0, resource.Metadata), + }, + Categories: categoriesList, + Locations: locationsList, + } +} diff --git a/pkg/iac/adapters/arm/monitor/adapt_test.go b/pkg/iac/adapters/arm/monitor/adapt_test.go new file mode 100644 index 000000000000..5d781e61058f --- /dev/null +++ b/pkg/iac/adapters/arm/monitor/adapt_test.go @@ -0,0 +1,55 @@ +package monitor + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/monitor" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected monitor.Monitor + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.Insights/logprofiles", + "properties": {} + } + ] +}`, + expected: monitor.Monitor{}, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.Insights/logprofiles", + "properties": { + "retentionPolicy": { + "days": 20, + "enabled": true + }, + "categories": ["Write"], + "locations": ["global"] + } + } + ] +}`, + expected: monitor.Monitor{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/arm/network/adapt.go b/pkg/iac/adapters/arm/network/adapt.go new file mode 100644 index 000000000000..bb7f88de5638 --- /dev/null +++ b/pkg/iac/adapters/arm/network/adapt.go @@ -0,0 +1,160 @@ +package network + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/common" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func parsePortRange(input string, meta iacTypes.Metadata) common.PortRange { + return common.ParsePortRange(input, meta, common.WithWildcard()) +} + +func Adapt(deployment azure.Deployment) network.Network { + return network.Network{ + SecurityGroups: adaptSecurityGroups(deployment), + NetworkWatcherFlowLogs: adaptNetworkWatcherFlowLogs(deployment), + NetworkInterfaces: adaptNetworkInterfaces(deployment), + } +} + +func adaptSecurityGroups(deployment azure.Deployment) (sgs []network.SecurityGroup) { + for _, resource := range deployment.GetResourcesByType("Microsoft.Network/networkSecurityGroups") { + sgs = append(sgs, adaptSecurityGroup(resource, deployment)) + } + return sgs + +} + +func adaptSecurityGroup(resource azure.Resource, deployment azure.Deployment) network.SecurityGroup { + return network.SecurityGroup{ + Metadata: resource.Metadata, + Rules: adaptSecurityGroupRules(deployment), + } +} + +func adaptSecurityGroupRules(deployment azure.Deployment) (rules []network.SecurityGroupRule) { + for _, resource := range deployment.GetResourcesByType("Microsoft.Network/networkSecurityGroups/securityRules") { + rules = append(rules, adaptSecurityGroupRule(resource)) + } + return rules +} + +func adaptSecurityGroupRule(resource azure.Resource) network.SecurityGroupRule { + sourceAddressPrefixes := resource.Properties.GetMapValue("sourceAddressPrefixes").AsStringValuesList("") + if prefix := resource.Properties.GetMapValue("sourceAddressPrefix").AsStringValue("", resource.Metadata); prefix.IsNotEmpty() { + sourceAddressPrefixes = append(sourceAddressPrefixes, prefix) + } + + var sourcePortRanges []common.PortRange + for _, portRange := range resource.Properties.GetMapValue("sourcePortRanges").AsList() { + rng := parsePortRange(portRange.AsString(), resource.Metadata) + if rng.Valid() { + sourcePortRanges = append(sourcePortRanges, rng) + } + } + if rng := parsePortRange(resource.Properties.GetMapValue("sourcePortRange").AsString(), resource.Metadata); rng.Valid() { + sourcePortRanges = append(sourcePortRanges, rng) + } + + destinationAddressPrefixes := resource.Properties.GetMapValue("destinationAddressPrefixes").AsStringValuesList("") + if prefix := resource.Properties.GetMapValue("destinationAddressPrefix").AsStringValue("", resource.Metadata); prefix.IsNotEmpty() { + destinationAddressPrefixes = append(destinationAddressPrefixes, prefix) + } + + var destinationPortRanges []common.PortRange + for _, portRange := range resource.Properties.GetMapValue("destinationPortRanges").AsList() { + rng := parsePortRange(portRange.AsString(), resource.Metadata) + if rng.Valid() { + destinationPortRanges = append(destinationPortRanges, rng) + } + } + if rng := parsePortRange(resource.Properties.GetMapValue("destinationPortRange").AsString(), resource.Metadata); rng.Valid() { + destinationPortRanges = append(destinationPortRanges, rng) + } + + allow := iacTypes.BoolDefault(false, resource.Metadata) + if resource.Properties.GetMapValue("access").AsString() == "Allow" { + allow = iacTypes.Bool(true, resource.Metadata) + } + + outbound := iacTypes.BoolDefault(false, resource.Metadata) + if resource.Properties.GetMapValue("direction").AsString() == "Outbound" { + outbound = iacTypes.Bool(true, resource.Metadata) + } + + return network.SecurityGroupRule{ + Metadata: resource.Metadata, + Outbound: outbound, + Allow: allow, + SourceAddresses: sourceAddressPrefixes, + SourcePorts: sourcePortRanges, + DestinationAddresses: destinationAddressPrefixes, + DestinationPorts: destinationPortRanges, + Protocol: resource.Properties.GetMapValue("protocol").AsStringValue("", resource.Metadata), + } +} + +func adaptNetworkWatcherFlowLogs(deployment azure.Deployment) (flowLogs []network.NetworkWatcherFlowLog) { + for _, resource := range deployment.GetResourcesByType("Microsoft.Network/networkWatchers/flowLogs") { + flowLogs = append(flowLogs, adaptNetworkWatcherFlowLog(resource)) + } + return flowLogs +} + +func adaptNetworkWatcherFlowLog(resource azure.Resource) network.NetworkWatcherFlowLog { + enabled := resource.Properties.GetMapValue("enabled").AsBoolValue(false, resource.Metadata) + retentionPolicy := resource.Properties.GetMapValue("retentionPolicy") + + return network.NetworkWatcherFlowLog{ + Metadata: resource.Metadata, + Enabled: enabled, + RetentionPolicy: network.RetentionPolicy{ + Metadata: resource.Metadata, + Enabled: retentionPolicy.GetMapValue("enabled").AsBoolValue(false, resource.Metadata), + Days: retentionPolicy.GetMapValue("days").AsIntValue(0, resource.Metadata), + }, + } +} + +func adaptNetworkInterfaces(deployment azure.Deployment) []network.NetworkInterface { + var networkInterfaces []network.NetworkInterface + for _, resource := range deployment.GetResourcesByType("Microsoft.Network/networkInterfaces") { + networkInterfaces = append(networkInterfaces, adaptNetworkInterface(resource, deployment)) + } + return networkInterfaces +} + +func adaptNetworkInterface(resource azure.Resource, _ azure.Deployment) network.NetworkInterface { + ni := network.NetworkInterface{ + Metadata: resource.Metadata, + EnableIPForwarding: resource.Properties.GetMapValue("enableIPForwarding").AsBoolValue(false, resource.Metadata), + HasPublicIP: iacTypes.BoolDefault(false, resource.Metadata), + PublicIPAddress: iacTypes.StringDefault("", resource.Metadata), + SubnetID: iacTypes.StringDefault("", resource.Metadata), + } + + ipConfigs := resource.Properties.GetMapValue("ipConfigurations").AsList() + ni.IPConfigurations = make([]network.IPConfiguration, 0, len(ipConfigs)) + + for _, ipConfig := range ipConfigs { + if ipConfig.IsNull() { + continue + } + ipConfigProps := ipConfig.GetMapValue("properties") + ni.IPConfigurations = append(ni.IPConfigurations, network.IPConfiguration{ + Metadata: resource.Metadata, + PublicIPAddress: ipConfigProps.GetMapValue("publicIPAddress"). + GetMapValue("id").AsStringValue("", resource.Metadata), + SubnetID: ipConfigProps.GetMapValue("subnet"). + GetMapValue("id").AsStringValue("", resource.Metadata), + Primary: ipConfigProps.GetMapValue("primary").AsBoolValue(false, resource.Metadata), + }) + } + ni.Setup() + + // Note: SecurityGroups are not resolved for ARM templates as related resource search + // is not yet implemented for ARM (parser cannot evaluate expressions/references) + return ni +} diff --git a/pkg/iac/adapters/arm/network/adapt_test.go b/pkg/iac/adapters/arm/network/adapt_test.go new file mode 100644 index 000000000000..0b806e29051f --- /dev/null +++ b/pkg/iac/adapters/arm/network/adapt_test.go @@ -0,0 +1,216 @@ +package network + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/adapters/common" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected network.Network + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.Network/networkSecurityGroups", + "properties": {} + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "properties": {} + }, + { + "type": "Microsoft.Network/networkWatchers/flowLogs", + "properties": {} + } + ] +}`, + expected: network.Network{ + NetworkWatcherFlowLogs: []network.NetworkWatcherFlowLog{{ + RetentionPolicy: network.RetentionPolicy{}, + }}, + SecurityGroups: []network.SecurityGroup{{ + Rules: []network.SecurityGroupRule{{}}, + }}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.Network/networkWatchers/flowLogs", + "properties": { + "retentionPolicy": { + "days": 100, + "enabled": true + } + } + }, + { + "type": "Microsoft.Network/networkSecurityGroups" + }, + { + "type": "Microsoft.Network/networkSecurityGroups/securityRules", + "properties": { + "access": "Allow", + "direction": "Inbound", + "protocol": "Tcp", + "sourceAddressPrefix": "10.0.0.0/24", + "sourceAddressPrefixes": [ + "10.0.1.0/24", + "10.0.2.0/24" + ], + "sourcePortRange": "*", + "sourcePortRanges": [ + "1000-2000", + "3000" + ], + "destinationAddressPrefix": "172.16.0.0/16", + "destinationAddressPrefixes": [ + "172.16.1.0/24", + "172.16.2.0/24" + ], + "destinationPortRange": "80", + "destinationPortRanges": [ + "8080", + "443" + ] + } + } + ] +}`, + expected: network.Network{ + NetworkWatcherFlowLogs: []network.NetworkWatcherFlowLog{{ + RetentionPolicy: network.RetentionPolicy{ + Days: types.IntTest(100), + Enabled: types.BoolTest(true), + }, + }}, + SecurityGroups: []network.SecurityGroup{{ + Rules: []network.SecurityGroupRule{{ + Allow: types.BoolTest(true), + Protocol: types.StringTest("Tcp"), + SourceAddresses: []types.StringValue{ + types.StringTest("10.0.1.0/24"), + types.StringTest("10.0.2.0/24"), + types.StringTest("10.0.0.0/24"), + }, + SourcePorts: []common.PortRange{ + { + Start: types.IntTest(1000), + End: types.IntTest(2000), + }, + { + Start: types.IntTest(3000), + End: types.IntTest(3000), + }, + { + Start: types.IntTest(0), + End: types.IntTest(65535), + }, + }, + DestinationAddresses: []types.StringValue{ + types.StringTest("172.16.1.0/24"), + types.StringTest("172.16.2.0/24"), + types.StringTest("172.16.0.0/16"), + }, + DestinationPorts: []common.PortRange{ + { + Start: types.IntTest(8080), + End: types.IntTest(8080), + }, + { + Start: types.IntTest(443), + End: types.IntTest(443), + }, + { + Start: types.IntTest(80), + End: types.IntTest(80), + }, + }, + }}, + }}, + }, + }, + { + name: "network interface with ip configurations", + source: `{ + "resources": [ + { + "type": "Microsoft.Network/networkInterfaces", + "properties": { + "enableIPForwarding": true, + "ipConfigurations": [ + { + "name": "primary-ip", + "properties": { + "primary": true, + "subnet": { + "id": "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet-primary" + }, + "publicIPAddress": { + "id": "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/pip-primary" + } + } + }, + { + "name": "secondary-ip", + "properties": { + "primary": false, + "subnet": { + "id": "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet-secondary" + } + } + } + ] + } + } + ] + }`, + expected: network.Network{ + NetworkInterfaces: []network.NetworkInterface{ + { + EnableIPForwarding: types.BoolTest(true), + + // backward compatibility — filled from primary config + SubnetID: types.StringTest("/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet-primary"), + HasPublicIP: types.BoolTest(true), + PublicIPAddress: types.StringTest("/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/pip-primary"), + + IPConfigurations: []network.IPConfiguration{ + { + Primary: types.BoolTest(true), + SubnetID: types.StringTest( + "/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet-primary", + ), + HasPublicIP: types.BoolTest(true), + PublicIPAddress: types.StringTest("/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Network/publicIPAddresses/pip-primary"), + }, + { + Primary: types.BoolTest(false), + SubnetID: types.StringTest("/subscriptions/abc/resourceGroups/rg/providers/Microsoft.Network/virtualNetworks/vnet/subnets/subnet-secondary"), + HasPublicIP: types.BoolTest(false), + PublicIPAddress: types.StringTest(""), + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/arm/securitycenter/adapt.go b/pkg/iac/adapters/arm/securitycenter/adapt.go new file mode 100644 index 000000000000..cead82ea2e8c --- /dev/null +++ b/pkg/iac/adapters/arm/securitycenter/adapt.go @@ -0,0 +1,90 @@ +package securitycenter + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/securitycenter" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(deployment azure.Deployment) securitycenter.SecurityCenter { + return securitycenter.SecurityCenter{ + Contacts: adaptContacts(deployment), + Subscriptions: adaptSubscriptions(deployment), + } +} + +func adaptContacts(deployment azure.Deployment) []securitycenter.Contact { + var contacts []securitycenter.Contact + for _, resource := range deployment.GetResourcesByType("Microsoft.Security/securityContacts") { + contacts = append(contacts, adaptContact(resource)) + } + + return contacts +} + +func adaptContact(resource azure.Resource) securitycenter.Contact { + alertsToAdminsState := resource.Properties.GetMapValue("notificationsByRole").GetMapValue("state").AsStringValue("", resource.Metadata) + isEnabledValue := resource.Properties.GetMapValue("isEnabled").AsBoolValue(false, resource.Metadata) + + enableAlertNotifications, minimalSeverity := extractNotificationSettings(resource, isEnabledValue) + + return securitycenter.Contact{ + Metadata: resource.Metadata, + EnableAlertNotifications: enableAlertNotifications, + EnableAlertsToAdmins: iacTypes.Bool(alertsToAdminsState.EqualTo("On"), resource.Metadata), + Email: resource.Properties.GetMapValue("emails").AsStringValue("", resource.Metadata), + Phone: resource.Properties.GetMapValue("phone").AsStringValue("", resource.Metadata), + IsEnabled: isEnabledValue, + MinimalSeverity: minimalSeverity, + } +} + +func extractNotificationSettings(resource azure.Resource, isEnabled iacTypes.BoolValue) (iacTypes.BoolValue, iacTypes.StringValue) { + notificationsSources := resource.Properties.GetMapValue("notificationsSources") + if !notificationsSources.IsNull() { + return extractFromNotificationsSources(notificationsSources, isEnabled, resource) + } + + enableAlertNotifications := resource.Properties.GetMapValue("alertNotifications").AsBoolValue(false, resource.Metadata) + minimalSeverity := iacTypes.StringDefault("", resource.Metadata) + return enableAlertNotifications, minimalSeverity +} + +func extractFromNotificationsSources(notificationsSources azure.Value, isEnabled iacTypes.BoolValue, resource azure.Resource) (iacTypes.BoolValue, iacTypes.StringValue) { + minimalSeverity := iacTypes.StringDefault("", resource.Metadata) + + for _, source := range notificationsSources.AsList() { + sourceMap := source.AsMap() + if sourceMap == nil { + continue + } + + sourceType, hasSourceType := sourceMap["sourceType"] + if !hasSourceType || !sourceType.AsStringValue("", resource.Metadata).EqualTo("Alert") { + continue + } + + if minimalSeverityVal, hasMinimalSeverity := sourceMap["minimalSeverity"]; hasMinimalSeverity { + minimalSeverity = minimalSeverityVal.AsStringValue("", resource.Metadata) + } + break + } + + enableAlertNotifications := iacTypes.Bool(isEnabled.IsTrue() && !minimalSeverity.IsEmpty(), resource.Metadata) + return enableAlertNotifications, minimalSeverity +} + +func adaptSubscriptions(deployment azure.Deployment) []securitycenter.SubscriptionPricing { + var subscriptions []securitycenter.SubscriptionPricing + for _, resource := range deployment.GetResourcesByType("Microsoft.Security/pricings") { + subscriptions = append(subscriptions, adaptSubscription(resource)) + } + return subscriptions +} + +func adaptSubscription(resource azure.Resource) securitycenter.SubscriptionPricing { + return securitycenter.SubscriptionPricing{ + Metadata: resource.Metadata, + Tier: resource.Properties.GetMapValue("pricingTier").AsStringValue("Free", resource.Metadata), + } +} diff --git a/pkg/iac/adapters/arm/securitycenter/adapt_test.go b/pkg/iac/adapters/arm/securitycenter/adapt_test.go new file mode 100644 index 000000000000..55dfe26756cd --- /dev/null +++ b/pkg/iac/adapters/arm/securitycenter/adapt_test.go @@ -0,0 +1,158 @@ +package securitycenter + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/securitycenter" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected securitycenter.SecurityCenter + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.Security/securityContacts", + "properties": {} + }, + { + "type": "Microsoft.Security/pricings", + "properties": {} + } + ] +}`, + expected: securitycenter.SecurityCenter{ + Contacts: []securitycenter.Contact{{}}, + Subscriptions: []securitycenter.SubscriptionPricing{{ + Tier: types.StringTest("Free"), + }}, + }, + }, + { + name: "complete - legacy format", + source: `{ + "resources": [ + { + "type": "Microsoft.Security/securityContacts", + "properties": { + "emails": "security@example.com", + "phone": "buz", + "alertNotifications": true, + "notificationsByRole": { + "state": "On" + } + } + }, + { + "type": "Microsoft.Security/pricings", + "properties": { + "pricingTier": "Standard" + } + } + ] +}`, + expected: securitycenter.SecurityCenter{ + Contacts: []securitycenter.Contact{{ + Email: types.StringTest("security@example.com"), + Phone: types.StringTest("buz"), + EnableAlertNotifications: types.BoolTest(true), + EnableAlertsToAdmins: types.BoolTest(true), + }}, + Subscriptions: []securitycenter.SubscriptionPricing{{ + Tier: types.StringTest("Standard"), + }}, + }, + }, + { + name: "complete - new format", + source: `{ + "resources": [ + { + "type": "Microsoft.Security/securityContacts", + "properties": { + "emails": "security@example.com", + "phone": "+1-555-555-5555", + "isEnabled": true, + "notificationsSources": [ + { + "sourceType": "Alert", + "minimalSeverity": "High" + } + ], + "notificationsByRole": { + "state": "On" + } + } + }, + { + "type": "Microsoft.Security/pricings", + "properties": { + "pricingTier": "Standard" + } + } + ] +}`, + expected: securitycenter.SecurityCenter{ + Contacts: []securitycenter.Contact{{ + Email: types.StringTest("security@example.com"), + Phone: types.StringTest("+1-555-555-5555"), + EnableAlertNotifications: types.BoolTest(true), + EnableAlertsToAdmins: types.BoolTest(true), + IsEnabled: types.BoolTest(true), + MinimalSeverity: types.StringTest("High"), + }}, + Subscriptions: []securitycenter.SubscriptionPricing{{ + Tier: types.StringTest("Standard"), + }}, + }, + }, + { + name: "new format - disabled", + source: `{ + "resources": [ + { + "type": "Microsoft.Security/securityContacts", + "properties": { + "emails": "security@example.com", + "phone": "+1-555-555-5555", + "isEnabled": false, + "notificationsSources": [ + { + "sourceType": "Alert", + "minimalSeverity": "Medium" + } + ], + "notificationsByRole": { + "state": "Off" + } + } + } + ] +}`, + expected: securitycenter.SecurityCenter{ + Contacts: []securitycenter.Contact{{ + Email: types.StringTest("security@example.com"), + Phone: types.StringTest("+1-555-555-5555"), + EnableAlertNotifications: types.BoolTest(false), + EnableAlertsToAdmins: types.BoolTest(false), + IsEnabled: types.BoolTest(false), + MinimalSeverity: types.StringTest("Medium"), + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/arm/storage/adapt.go b/pkg/iac/adapters/arm/storage/adapt.go new file mode 100644 index 000000000000..d789368264a4 --- /dev/null +++ b/pkg/iac/adapters/arm/storage/adapt.go @@ -0,0 +1,130 @@ +package storage + +import ( + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/storage" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func Adapt(deployment azure.Deployment) storage.Storage { + return storage.Storage{ + Accounts: adaptAccounts(deployment), + } +} + +func adaptAccounts(deployment azure.Deployment) []storage.Account { + var accounts []storage.Account + for _, resource := range deployment.GetResourcesByType("Microsoft.Storage/storageAccounts") { + + var queues []storage.Queue + for _, queueResource := range resource.GetResourcesByType("queueServices/queues") { + queues = append(queues, storage.Queue{ + Metadata: queueResource.Metadata, + Name: queueResource.Name.AsStringValue("", queueResource.Metadata), + }) + } + + var containers []storage.Container + for _, containerResource := range resource.GetResourcesByType("containerServices/containers") { + containers = append(containers, storage.Container{ + Metadata: containerResource.Metadata, + PublicAccess: containerResource.Properties.GetMapValue("publicAccess").AsStringValue("None", containerResource.Metadata), + }) + } + + account := storage.Account{ + Metadata: resource.Metadata, + NetworkRules: xslices.ZeroToNil(adaptNetworkRules(resource)), + // The default value is true since API version 2019-04-01. + EnforceHTTPS: resource.Properties.GetMapValue("supportsHttpsTrafficOnly").AsBoolValue(true, resource.Properties.GetMetadata()), + Containers: containers, + QueueProperties: storage.QueueProperties{ + Metadata: resource.Properties.GetMetadata(), + EnableLogging: types.BoolDefault(false, resource.Properties.GetMetadata()), + }, + // The default interpretation is TLS 1.0 for this property. + MinimumTLSVersion: resource.Properties.GetMapValue("minimumTlsVersion"). + AsStringValue("TLS1_0", resource.Properties.GetMetadata()), + Queues: queues, + BlobProperties: storage.BlobProperties{ + Metadata: resource.Properties.GetMetadata(), + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Metadata: resource.Properties.GetMetadata(), + Days: resource.Properties.GetMapValue("blobServices").GetMapValue("properties").GetMapValue("deleteRetentionPolicy").GetMapValue("days").AsIntValue(0, resource.Properties.GetMetadata()), + }, + }, + AccountReplicationType: resource.Properties.GetMapValue("sku").GetMapValue("name").AsStringValue("", resource.Properties.GetMetadata()), + InfrastructureEncryptionEnabled: resource.Properties.GetMapValue("encryption").GetMapValue("requireInfrastructureEncryption").AsBoolValue(false, resource.Properties.GetMetadata()), + CustomerManagedKey: storage.CustomerManagedKey{ + Metadata: resource.Properties.GetMetadata(), + KeyVaultKeyId: resource.Properties.GetMapValue("encryption").GetMapValue("keyVaultProperties").GetMapValue("keyUri").AsStringValue("", resource.Properties.GetMetadata()), + UserAssignedIdentityId: resource.Properties.GetMapValue("encryption").GetMapValue("identity").GetMapValue("userAssignedIdentity").AsStringValue("", resource.Properties.GetMetadata()), + }, + } + + // Adapt queue properties logging + queueServiceLogging := resource.Properties.GetMapValue("queueServices").GetMapValue("properties").GetMapValue("logging") + if !queueServiceLogging.IsNull() { + account.QueueProperties.Logging = storage.QueueLogging{ + Metadata: queueServiceLogging.GetMetadata(), + Delete: queueServiceLogging.GetMapValue("delete").AsBoolValue(false, queueServiceLogging.GetMetadata()), + Read: queueServiceLogging.GetMapValue("read").AsBoolValue(false, queueServiceLogging.GetMetadata()), + Write: queueServiceLogging.GetMapValue("write").AsBoolValue(false, queueServiceLogging.GetMetadata()), + Version: queueServiceLogging.GetMapValue("version").AsStringValue("", queueServiceLogging.GetMetadata()), + RetentionPolicyDays: queueServiceLogging.GetMapValue("retentionPolicy").GetMapValue("days").AsIntValue(0, queueServiceLogging.GetMetadata()), + } + if account.QueueProperties.Logging.Delete.IsTrue() || account.QueueProperties.Logging.Read.IsTrue() || account.QueueProperties.Logging.Write.IsTrue() { + account.QueueProperties.EnableLogging = types.Bool(true, queueServiceLogging.GetMetadata()) + } + } + + publicNetworkAccess := resource.Properties.GetMapValue("publicNetworkAccess") + account.PublicNetworkAccess = types.Bool( + publicNetworkAccess.AsStringValue("Enabled", publicNetworkAccess.GetMetadata()).EqualTo("Enabled"), + publicNetworkAccess.GetMetadata(), + ) + accounts = append(accounts, account) + } + return accounts +} + +func adaptNetworkRules(resource azure.Resource) []storage.NetworkRule { + defaultBypasses := []types.StringValue{types.StringDefault("AzureServices", resource.Metadata)} + acl := resource.Properties.GetMapValue("networkAcls") + if acl.IsNull() { + // default network rule + return []storage.NetworkRule{{ + Metadata: resource.Metadata, + Bypass: defaultBypasses, + AllowByDefault: types.BoolDefault(true, resource.Metadata), + }} + } + + bypassProp := acl.GetMapValue("bypass") + bypassVal := bypassProp.AsString() + + var bypasses []types.StringValue + if bypassVal != "" { + // Possible values are any combination of Logging|Metrics|AzureServices (For example, "Logging, Metrics") + // See https://github.com/Azure/azure-resource-manager-schemas/blob/0cb6180c9646c91ca212de0e59568c00ee3a47ec/schemas/2021-01-01/Microsoft.Storage.json#L2379 + for bypass := range strings.SplitSeq(bypassVal, ",") { + bypasses = append(bypasses, types.String(strings.TrimSpace(bypass), bypassProp.GetMetadata())) + } + } else { + bypasses = defaultBypasses + } + + allowByDefault := types.Bool(true, acl.GetMetadata()) + if defaultAction := acl.GetMapValue("defaultAction"); !defaultAction.IsNull() { + allowByDefault = types.Bool(defaultAction.EqualTo("Allow"), defaultAction.GetMetadata()) + } + + return []storage.NetworkRule{{ + Metadata: acl.GetMetadata(), + Bypass: bypasses, + AllowByDefault: allowByDefault, + }} +} diff --git a/pkg/iac/adapters/arm/storage/adapt_test.go b/pkg/iac/adapters/arm/storage/adapt_test.go new file mode 100644 index 000000000000..bf3c4120ab44 --- /dev/null +++ b/pkg/iac/adapters/arm/storage/adapt_test.go @@ -0,0 +1,102 @@ +package storage + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/storage" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected storage.Storage + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "properties": {} + } + ] +}`, + expected: storage.Storage{ + Accounts: []storage.Account{{ + MinimumTLSVersion: types.StringTest("TLS1_0"), + EnforceHTTPS: types.BoolTest(true), + NetworkRules: []storage.NetworkRule{{ + Bypass: []types.StringValue{types.StringTest("AzureServices")}, + AllowByDefault: types.BoolTest(true), + }}, + PublicNetworkAccess: types.BoolTest(true), + AccountReplicationType: types.StringTest(""), + InfrastructureEncryptionEnabled: types.BoolTest(false), + BlobProperties: storage.BlobProperties{ + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Days: types.IntTest(0), + }, + }, + CustomerManagedKey: storage.CustomerManagedKey{ + KeyVaultKeyId: types.StringTest(""), + UserAssignedIdentityId: types.StringTest(""), + }, + }}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": null, + "properties": { + "minimumTlsVersion": "TLS1_2", + "supportsHttpsTrafficOnly": true, + "publicNetworkAccess": "Disabled", + "networkAcls": { + "bypass": "Logging, Metrics", + "defaultAction": "Allow" + } + } + } + ] +}`, + expected: storage.Storage{ + Accounts: []storage.Account{{ + MinimumTLSVersion: types.StringTest("TLS1_2"), + EnforceHTTPS: types.BoolTest(true), + PublicNetworkAccess: types.BoolTest(false), + AccountReplicationType: types.StringTest(""), + InfrastructureEncryptionEnabled: types.BoolTest(false), + NetworkRules: []storage.NetworkRule{{ + Bypass: []types.StringValue{ + types.StringTest("Logging"), + types.StringTest("Metrics"), + }, + AllowByDefault: types.BoolTest(true), + }}, + BlobProperties: storage.BlobProperties{ + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Days: types.IntTest(0), + }, + }, + CustomerManagedKey: storage.CustomerManagedKey{ + KeyVaultKeyId: types.StringTest(""), + UserAssignedIdentityId: types.StringTest(""), + }, + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/arm/synapse/adapt.go b/pkg/iac/adapters/arm/synapse/adapt.go new file mode 100644 index 000000000000..649831e16a9a --- /dev/null +++ b/pkg/iac/adapters/arm/synapse/adapt.go @@ -0,0 +1,34 @@ +package synapse + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/synapse" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(deployment azure.Deployment) synapse.Synapse { + return synapse.Synapse{ + Workspaces: adaptWorkspaces(deployment), + } +} + +func adaptWorkspaces(deployment azure.Deployment) (workspaces []synapse.Workspace) { + for _, resource := range deployment.GetResourcesByType("Microsoft.Synapse/workspaces") { + workspaces = append(workspaces, adaptWorkspace(resource)) + } + return workspaces +} + +func adaptWorkspace(resource azure.Resource) synapse.Workspace { + + managedVirtualNetwork := resource.Properties.GetMapValue("managedVirtualNetwork").AsString() + enableManagedVirtualNetwork := types.BoolDefault(false, resource.Metadata) + if managedVirtualNetwork == "default" { + enableManagedVirtualNetwork = types.Bool(true, resource.Metadata) + } + + return synapse.Workspace{ + Metadata: resource.Metadata, + EnableManagedVirtualNetwork: enableManagedVirtualNetwork, + } +} diff --git a/pkg/iac/adapters/arm/synapse/adapt_test.go b/pkg/iac/adapters/arm/synapse/adapt_test.go new file mode 100644 index 000000000000..d61eee5fef45 --- /dev/null +++ b/pkg/iac/adapters/arm/synapse/adapt_test.go @@ -0,0 +1,61 @@ +package synapse + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm/adaptertest" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/synapse" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected synapse.Synapse + }{ + { + name: "empty", + source: `{ + "resources": [ + { + "type": "Microsoft.Synapse/workspaces", + "properties": {} + } + ] +}`, + expected: synapse.Synapse{ + Workspaces: []synapse.Workspace{{ + EnableManagedVirtualNetwork: types.BoolTest(false), + }}, + }, + }, + { + name: "complete", + source: `{ + "resources": [ + { + "type": "Microsoft.Synapse/workspaces", + "properties": { + "managedVirtualNetwork": "default" + } + } + ] +}`, + expected: synapse.Synapse{ + Workspaces: []synapse.Workspace{ + { + EnableManagedVirtualNetwork: types.BoolTest(true), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + adaptertest.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/cloudformation/adapt.go b/pkg/iac/adapters/cloudformation/adapt.go new file mode 100644 index 000000000000..da2d2595892d --- /dev/null +++ b/pkg/iac/adapters/cloudformation/adapt.go @@ -0,0 +1,14 @@ +package cloudformation + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/state" +) + +// Adapt adapts the Cloudformation instance +func Adapt(cfFile parser.FileContext) *state.State { + return &state.State{ + AWS: aws.Adapt(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/accessanalyzer/accessanalyzer.go b/pkg/iac/adapters/cloudformation/aws/accessanalyzer/accessanalyzer.go new file mode 100644 index 000000000000..e6f7031a58d5 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/accessanalyzer/accessanalyzer.go @@ -0,0 +1,13 @@ +package accessanalyzer + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/accessanalyzer" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an AccessAnalyzer instance +func Adapt(cfFile parser.FileContext) accessanalyzer.AccessAnalyzer { + return accessanalyzer.AccessAnalyzer{ + Analyzers: getAccessAnalyzer(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/accessanalyzer/accessanalyzer_test.go b/pkg/iac/adapters/cloudformation/aws/accessanalyzer/accessanalyzer_test.go new file mode 100644 index 000000000000..04e67c2b6818 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/accessanalyzer/accessanalyzer_test.go @@ -0,0 +1,54 @@ +package accessanalyzer + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/accessanalyzer" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected accessanalyzer.AccessAnalyzer + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + Analyzer: + Type: 'AWS::AccessAnalyzer::Analyzer' + Properties: + AnalyzerName: MyAccountAnalyzer +`, + expected: accessanalyzer.AccessAnalyzer{ + Analyzers: []accessanalyzer.Analyzer{ + { + Name: types.StringTest("MyAccountAnalyzer"), + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + Analyzer: + Type: 'AWS::AccessAnalyzer::Analyzer' +`, + expected: accessanalyzer.AccessAnalyzer{ + Analyzers: []accessanalyzer.Analyzer{ + {}, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/accessanalyzer/analyzer.go b/pkg/iac/adapters/cloudformation/aws/accessanalyzer/analyzer.go new file mode 100644 index 000000000000..c81802d60e0e --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/accessanalyzer/analyzer.go @@ -0,0 +1,24 @@ +package accessanalyzer + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/accessanalyzer" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getAccessAnalyzer(ctx parser.FileContext) (analyzers []accessanalyzer.Analyzer) { + + analyzersList := ctx.GetResourcesByType("AWS::AccessAnalyzer::Analyzer") + + for _, r := range analyzersList { + aa := accessanalyzer.Analyzer{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("AnalyzerName"), + ARN: r.StringDefault(""), + Active: types.BoolDefault(false, r.Metadata()), + } + + analyzers = append(analyzers, aa) + } + return analyzers +} diff --git a/pkg/iac/adapters/cloudformation/aws/adapt.go b/pkg/iac/adapters/cloudformation/aws/adapt.go new file mode 100644 index 000000000000..e8c10edf0855 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/adapt.go @@ -0,0 +1,74 @@ +package aws + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/apigateway" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/athena" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/cloudfront" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/cloudtrail" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/cloudwatch" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/codebuild" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/config" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/documentdb" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/dynamodb" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/ecr" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/ecs" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/efs" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/eks" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/elasticache" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/elasticsearch" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/elb" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/kinesis" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/lambda" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/mq" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/msk" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/neptune" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/rds" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/redshift" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/sam" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/sns" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/sqs" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/ssm" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/aws/workspaces" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a Cloudformation AWS instance +func Adapt(cfFile parser.FileContext) aws.AWS { + return aws.AWS{ + APIGateway: apigateway.Adapt(cfFile), + Athena: athena.Adapt(cfFile), + Cloudfront: cloudfront.Adapt(cfFile), + CloudTrail: cloudtrail.Adapt(cfFile), + CloudWatch: cloudwatch.Adapt(cfFile), + CodeBuild: codebuild.Adapt(cfFile), + Config: config.Adapt(cfFile), + DocumentDB: documentdb.Adapt(cfFile), + DynamoDB: dynamodb.Adapt(cfFile), + EC2: ec2.Adapt(cfFile), + ECR: ecr.Adapt(cfFile), + ECS: ecs.Adapt(cfFile), + EFS: efs.Adapt(cfFile), + IAM: iam.Adapt(cfFile), + EKS: eks.Adapt(cfFile), + ElastiCache: elasticache.Adapt(cfFile), + Elasticsearch: elasticsearch.Adapt(cfFile), + ELB: elb.Adapt(cfFile), + MSK: msk.Adapt(cfFile), + MQ: mq.Adapt(cfFile), + Kinesis: kinesis.Adapt(cfFile), + Lambda: lambda.Adapt(cfFile), + Neptune: neptune.Adapt(cfFile), + RDS: rds.Adapt(cfFile), + Redshift: redshift.Adapt(cfFile), + S3: s3.Adapt(cfFile), + SAM: sam.Adapt(cfFile), + SNS: sns.Adapt(cfFile), + SQS: sqs.Adapt(cfFile), + SSM: ssm.Adapt(cfFile), + WorkSpaces: workspaces.Adapt(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/apigateway/apigateway.go b/pkg/iac/adapters/cloudformation/aws/apigateway/apigateway.go new file mode 100644 index 000000000000..096ad174a002 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/apigateway/apigateway.go @@ -0,0 +1,22 @@ +package apigateway + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway" + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an APIGateway instance +func Adapt(cfFile parser.FileContext) apigateway.APIGateway { + return apigateway.APIGateway{ + V1: v1.APIGateway{ + APIs: adaptAPIsV1(cfFile), + DomainNames: adaptDomainNamesV1(cfFile), + }, + V2: v2.APIGateway{ + APIs: adaptAPIsV2(cfFile), + DomainNames: adaptDomainNamesV2(cfFile), + }, + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/apigateway/apigateway_test.go b/pkg/iac/adapters/cloudformation/aws/apigateway/apigateway_test.go new file mode 100644 index 000000000000..4386a5baa51c --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/apigateway/apigateway_test.go @@ -0,0 +1,172 @@ +package apigateway + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway" + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected apigateway.APIGateway + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyRestApi: + Type: 'AWS::ApiGateway::RestApi' + Properties: + Description: A test API + Name: MyRestAPI + ApiResource: + Type: AWS::ApiGateway::Resource + Properties: + RestApiId: !Ref MyRestApi + MethodPOST: + Type: AWS::ApiGateway::Method + Properties: + RestApiId: !Ref MyRestApi + ResourceId: !Ref ApiResource + HttpMethod: POST + AuthorizationType: COGNITO_USER_POOLS + ApiKeyRequired: true + Stage: + Type: AWS::ApiGateway::Stage + Properties: + StageName: Prod + RestApiId: !Ref MyRestApi + TracingEnabled: true + AccessLogSetting: + DestinationArn: test-arn + MethodSettings: + - CacheDataEncrypted: true + CachingEnabled: true + HttpMethod: POST + MyDomainName: + Type: AWS::ApiGateway::DomainName + Properties: + DomainName: mydomainame.us-east-1.com + SecurityPolicy: "TLS_1_2" + + MyApi2: + Type: 'AWS::ApiGatewayV2::Api' + Properties: + Name: MyApi2 + ProtocolType: WEBSOCKET + MyStage2: + Type: 'AWS::ApiGatewayV2::Stage' + Properties: + StageName: Prod + ApiId: !Ref MyApi2 + AccessLogSettings: + DestinationArn: some-arn + MyDomainName2: + Type: 'AWS::ApiGatewayV2::DomainName' + Properties: + DomainName: mydomainame.us-east-1.com + DomainNameConfigurations: + - SecurityPolicy: "TLS_1_2" +`, + expected: apigateway.APIGateway{ + V1: v1.APIGateway{ + APIs: []v1.API{ + { + Name: types.StringTest("MyRestAPI"), + Stages: []v1.Stage{ + { + Name: types.StringTest("Prod"), + XRayTracingEnabled: types.BoolTest(true), + AccessLogging: v1.AccessLogging{ + CloudwatchLogGroupARN: types.StringTest("test-arn"), + }, + RESTMethodSettings: []v1.RESTMethodSettings{ + { + Method: types.StringTest("POST"), + CacheDataEncrypted: types.BoolTest(true), + CacheEnabled: types.BoolTest(true), + }, + }, + }, + }, + Resources: []v1.Resource{ + { + Methods: []v1.Method{ + { + HTTPMethod: types.StringTest("POST"), + AuthorizationType: types.StringTest("COGNITO_USER_POOLS"), + APIKeyRequired: types.BoolTest(true), + }, + }, + }, + }, + }, + }, + DomainNames: []v1.DomainName{ + { + Name: types.StringTest("mydomainame.us-east-1.com"), + SecurityPolicy: types.StringTest("TLS_1_2"), + }, + }, + }, + V2: v2.APIGateway{ + APIs: []v2.API{ + { + Name: types.StringTest("MyApi2"), + ProtocolType: types.StringTest("WEBSOCKET"), + Stages: []v2.Stage{ + { + Name: types.StringTest("Prod"), + AccessLogging: v2.AccessLogging{ + CloudwatchLogGroupARN: types.StringTest("some-arn"), + }, + }, + }, + }, + }, + DomainNames: []v2.DomainName{ + { + Name: types.StringTest("mydomainame.us-east-1.com"), + SecurityPolicy: types.StringTest("TLS_1_2"), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyApi: + Type: 'AWS::ApiGatewayV2::Api' + MyStage: + Type: 'AWS::ApiGatewayV2::Stage' + MyStage2: + Type: 'AWS::ApiGatewayV2::Stage' + Properties: + ApiId: !Ref MyApi +`, + expected: apigateway.APIGateway{ + V2: v2.APIGateway{ + APIs: []v2.API{ + { + Stages: []v2.Stage{{}}, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/apigateway/apiv1.go b/pkg/iac/adapters/cloudformation/aws/apigateway/apiv1.go new file mode 100644 index 000000000000..2a2c46f44f5e --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/apigateway/apiv1.go @@ -0,0 +1,108 @@ +package apigateway + +import ( + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func adaptAPIsV1(fctx parser.FileContext) []v1.API { + var apis []v1.API + + stages := make(map[string]*parser.Resource) + for _, stageResource := range fctx.GetResourcesByType("AWS::ApiGateway::Stage") { + restApiID := stageResource.GetStringProperty("RestApiId") + if restApiID.IsEmpty() { + continue + } + + stages[restApiID.Value()] = stageResource + } + + resources := make(map[string]*parser.Resource) + for _, resource := range fctx.GetResourcesByType("AWS::ApiGateway::Resource") { + restApiID := resource.GetStringProperty("RestApiId") + if restApiID.IsEmpty() { + continue + } + + resources[restApiID.Value()] = resource + } + + for _, apiResource := range fctx.GetResourcesByType("AWS::ApiGateway::RestApi") { + + api := v1.API{ + Metadata: apiResource.Metadata(), + Name: apiResource.GetStringProperty("Name"), + } + + if stageResource, exists := stages[apiResource.ID()]; exists { + stage := v1.Stage{ + Metadata: stageResource.Metadata(), + Name: stageResource.GetStringProperty("StageName"), + XRayTracingEnabled: stageResource.GetBoolProperty("TracingEnabled"), + } + + if logSetting := stageResource.GetProperty("AccessLogSetting"); logSetting.IsNotNil() { + stage.AccessLogging = v1.AccessLogging{ + Metadata: logSetting.Metadata(), + CloudwatchLogGroupARN: logSetting.GetStringProperty("DestinationArn"), + } + } + + if methodSettings := stageResource.GetProperty("MethodSettings"); methodSettings.IsList() { + for _, methodSetting := range methodSettings.AsList() { + stage.RESTMethodSettings = append(stage.RESTMethodSettings, v1.RESTMethodSettings{ + Metadata: methodSetting.Metadata(), + Method: methodSetting.GetStringProperty("HttpMethod"), + CacheDataEncrypted: methodSetting.GetBoolProperty("CacheDataEncrypted"), + CacheEnabled: methodSetting.GetBoolProperty("CachingEnabled"), + }) + } + } + + api.Stages = append(api.Stages, stage) + } + + if resource, exists := resources[apiResource.ID()]; exists { + res := v1.Resource{ + Metadata: resource.Metadata(), + } + + for _, methodResource := range fctx.GetResourcesByType("AWS::ApiGateway::Method") { + resourceID := methodResource.GetStringProperty("ResourceId") + // TODO: handle RootResourceId + if resourceID.Value() != resource.ID() { + continue + } + + res.Methods = append(res.Methods, v1.Method{ + Metadata: methodResource.Metadata(), + HTTPMethod: methodResource.GetStringProperty("HttpMethod"), + AuthorizationType: methodResource.GetStringProperty("AuthorizationType"), + APIKeyRequired: methodResource.GetBoolProperty("ApiKeyRequired"), + }) + + } + + api.Resources = append(api.Resources, res) + } + + apis = append(apis, api) + } + + return apis +} + +func adaptDomainNamesV1(fctx parser.FileContext) []v1.DomainName { + var domainNames []v1.DomainName + + for _, domainNameResource := range fctx.GetResourcesByType("AWS::ApiGateway::DomainName") { + domainNames = append(domainNames, v1.DomainName{ + Metadata: domainNameResource.Metadata(), + Name: domainNameResource.GetStringProperty("DomainName"), + SecurityPolicy: domainNameResource.GetStringProperty("SecurityPolicy"), + }) + } + + return domainNames +} diff --git a/pkg/iac/adapters/cloudformation/aws/apigateway/apiv2.go b/pkg/iac/adapters/cloudformation/aws/apigateway/apiv2.go new file mode 100644 index 000000000000..d3a34a98d91e --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/apigateway/apiv2.go @@ -0,0 +1,91 @@ +package apigateway + +import ( + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptAPIsV2(cfFile parser.FileContext) (apis []v2.API) { + + apiResources := cfFile.GetResourcesByType("AWS::ApiGatewayV2::Api") + for _, apiRes := range apiResources { + api := v2.API{ + Metadata: apiRes.Metadata(), + Name: apiRes.GetStringProperty("Name"), + ProtocolType: apiRes.GetStringProperty("ProtocolType"), + Stages: getStages(apiRes.ID(), cfFile), + } + apis = append(apis, api) + } + + return apis +} + +func getStages(apiId string, cfFile parser.FileContext) []v2.Stage { + var apiStages []v2.Stage + + stageResources := cfFile.GetResourcesByType("AWS::ApiGatewayV2::Stage") + for _, r := range stageResources { + stageApiId := r.GetStringProperty("ApiId") + if stageApiId.Value() != apiId { + continue + } + + s := v2.Stage{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("StageName"), + AccessLogging: getAccessLogging(r), + } + apiStages = append(apiStages, s) + } + + return apiStages +} + +func getAccessLogging(r *parser.Resource) v2.AccessLogging { + + loggingProp := r.GetProperty("AccessLogSettings") + if loggingProp.IsNil() { + return v2.AccessLogging{ + Metadata: r.Metadata(), + CloudwatchLogGroupARN: types.StringDefault("", r.Metadata()), + } + } + + destinationProp := r.GetProperty("AccessLogSettings.DestinationArn") + + if destinationProp.IsNil() { + return v2.AccessLogging{ + Metadata: loggingProp.Metadata(), + CloudwatchLogGroupARN: types.StringDefault("", r.Metadata()), + } + } + return v2.AccessLogging{ + Metadata: destinationProp.Metadata(), + CloudwatchLogGroupARN: destinationProp.AsStringValue(), + } +} + +func adaptDomainNamesV2(fctx parser.FileContext) []v2.DomainName { + var domainNames []v2.DomainName + + for _, domainNameResource := range fctx.GetResourcesByType("AWS::ApiGateway::DomainName") { + + domainName := v2.DomainName{ + Metadata: domainNameResource.Metadata(), + Name: domainNameResource.GetStringProperty("DomainName"), + SecurityPolicy: domainNameResource.GetStringProperty("SecurityPolicy"), + } + + if domainNameCfgs := domainNameResource.GetProperty("DomainNameConfigurations"); domainNameCfgs.IsList() { + for _, domainNameCfg := range domainNameCfgs.AsList() { + domainName.SecurityPolicy = domainNameCfg.GetStringProperty("SecurityPolicy") + } + } + + domainNames = append(domainNames, domainName) + } + + return domainNames +} diff --git a/pkg/iac/adapters/cloudformation/aws/athena/athena.go b/pkg/iac/adapters/cloudformation/aws/athena/athena.go new file mode 100644 index 000000000000..b23a1936c179 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/athena/athena.go @@ -0,0 +1,14 @@ +package athena + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/athena" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an Athena instance +func Adapt(cfFile parser.FileContext) athena.Athena { + return athena.Athena{ + Databases: nil, + Workgroups: getWorkGroups(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/athena/athena_test.go b/pkg/iac/adapters/cloudformation/aws/athena/athena_test.go new file mode 100644 index 000000000000..097de6fa303d --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/athena/athena_test.go @@ -0,0 +1,61 @@ +package athena + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/athena" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected athena.Athena + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyAthenaWorkGroup: + Type: AWS::Athena::WorkGroup + Properties: + Name: MyCustomWorkGroup + WorkGroupConfiguration: + EnforceWorkGroupConfiguration: true + ResultConfiguration: + EncryptionOption: SSE_KMS +`, + expected: athena.Athena{ + Workgroups: []athena.Workgroup{ + { + Name: types.StringTest("MyCustomWorkGroup"), + EnforceConfiguration: types.BoolTest(true), + Encryption: athena.EncryptionConfiguration{ + Type: types.StringTest("SSE_KMS"), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyAthenaWorkGroup: + Type: AWS::Athena::WorkGroup +`, + expected: athena.Athena{ + Workgroups: []athena.Workgroup{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/cloudformation/aws/athena/workgroup.go b/pkg/iac/adapters/cloudformation/aws/athena/workgroup.go new file mode 100644 index 000000000000..916e2ac4b6f8 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/athena/workgroup.go @@ -0,0 +1,30 @@ +package athena + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/athena" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getWorkGroups(cfFile parser.FileContext) []athena.Workgroup { + + var workgroups []athena.Workgroup + + workgroupResources := cfFile.GetResourcesByType("AWS::Athena::WorkGroup") + + for _, r := range workgroupResources { + + wg := athena.Workgroup{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("Name"), + Encryption: athena.EncryptionConfiguration{ + Metadata: r.Metadata(), + Type: r.GetStringProperty("WorkGroupConfiguration.ResultConfiguration.EncryptionConfiguration.EncryptionOption"), + }, + EnforceConfiguration: r.GetBoolProperty("WorkGroupConfiguration.EnforceWorkGroupConfiguration"), + } + + workgroups = append(workgroups, wg) + } + + return workgroups +} diff --git a/pkg/iac/adapters/cloudformation/aws/cloudfront/cloudfront.go b/pkg/iac/adapters/cloudformation/aws/cloudfront/cloudfront.go new file mode 100644 index 000000000000..0cfe00145907 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/cloudfront/cloudfront.go @@ -0,0 +1,13 @@ +package cloudfront + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudfront" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a CloudFront instance +func Adapt(cfFile parser.FileContext) cloudfront.Cloudfront { + return cloudfront.Cloudfront{ + Distributions: getDistributions(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/cloudfront/cloudfront_test.go b/pkg/iac/adapters/cloudformation/aws/cloudfront/cloudfront_test.go new file mode 100644 index 000000000000..6c0ec7348b33 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/cloudfront/cloudfront_test.go @@ -0,0 +1,68 @@ +package cloudfront + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudfront" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected cloudfront.Cloudfront + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + cloudfrontdistribution: + Type: AWS::CloudFront::Distribution + Properties: + DistributionConfig: + WebACLId: "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111" + Logging: + Bucket: "myawslogbucket.s3.amazonaws.com" + ViewerCertificate: + MinimumProtocolVersion: SSLv3 + DefaultCacheBehavior: + ViewerProtocolPolicy: "redirect-to-https" +`, + expected: cloudfront.Cloudfront{ + Distributions: []cloudfront.Distribution{ + { + WAFID: types.StringTest("a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"), + Logging: cloudfront.Logging{ + Bucket: types.StringTest("myawslogbucket.s3.amazonaws.com"), + }, + ViewerCertificate: cloudfront.ViewerCertificate{ + MinimumProtocolVersion: types.StringTest("SSLv3"), + }, + DefaultCacheBehaviour: cloudfront.CacheBehaviour{ + ViewerProtocolPolicy: types.StringTest("redirect-to-https"), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + cloudfrontdistribution: + Type: AWS::CloudFront::Distribution +`, + expected: cloudfront.Cloudfront{ + Distributions: []cloudfront.Distribution{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/cloudfront/distribution.go b/pkg/iac/adapters/cloudformation/aws/cloudfront/distribution.go new file mode 100644 index 000000000000..70c5052bcd55 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/cloudfront/distribution.go @@ -0,0 +1,45 @@ +package cloudfront + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudfront" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getDistributions(ctx parser.FileContext) (distributions []cloudfront.Distribution) { + + distributionResources := ctx.GetResourcesByType("AWS::CloudFront::Distribution") + + for _, r := range distributionResources { + distribution := cloudfront.Distribution{ + Metadata: r.Metadata(), + WAFID: r.GetStringProperty("DistributionConfig.WebACLId"), + Logging: cloudfront.Logging{ + Metadata: r.Metadata(), + Bucket: r.GetStringProperty("DistributionConfig.Logging.Bucket"), + }, + DefaultCacheBehaviour: getDefaultCacheBehaviour(r), + OrdererCacheBehaviours: nil, + ViewerCertificate: cloudfront.ViewerCertificate{ + Metadata: r.Metadata(), + MinimumProtocolVersion: r.GetStringProperty("DistributionConfig.ViewerCertificate.MinimumProtocolVersion"), + }, + } + + distributions = append(distributions, distribution) + } + + return distributions +} + +func getDefaultCacheBehaviour(r *parser.Resource) cloudfront.CacheBehaviour { + defaultCache := r.GetProperty("DistributionConfig.DefaultCacheBehavior") + if defaultCache.IsNil() { + return cloudfront.CacheBehaviour{ + Metadata: r.Metadata(), + } + } + return cloudfront.CacheBehaviour{ + Metadata: defaultCache.Metadata(), + ViewerProtocolPolicy: defaultCache.GetStringProperty("ViewerProtocolPolicy"), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/cloudtrail/cloudtrail.go b/pkg/iac/adapters/cloudformation/aws/cloudtrail/cloudtrail.go new file mode 100644 index 000000000000..04270579dbcd --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/cloudtrail/cloudtrail.go @@ -0,0 +1,13 @@ +package cloudtrail + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudtrail" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a CloudTrail instance +func Adapt(cfFile parser.FileContext) cloudtrail.CloudTrail { + return cloudtrail.CloudTrail{ + Trails: getCloudTrails(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/cloudtrail/cloudtrail_test.go b/pkg/iac/adapters/cloudformation/aws/cloudtrail/cloudtrail_test.go new file mode 100644 index 000000000000..5dcebb291035 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/cloudtrail/cloudtrail_test.go @@ -0,0 +1,64 @@ +package cloudtrail + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudtrail" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected cloudtrail.CloudTrail + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + Trail: + Type: AWS::CloudTrail::Trail + Properties: + S3BucketName: MyBucket + IsLogging: true + TrailName: MyTrail + EnableLogFileValidation: true + IsMultiRegionTrail: true + CloudWatchLogsLogGroupArn: cw-arn + KmsKeyId: my-kms-key +`, + expected: cloudtrail.CloudTrail{ + Trails: []cloudtrail.Trail{ + { + Name: types.StringTest("MyTrail"), + BucketName: types.StringTest("MyBucket"), + IsLogging: types.BoolTest(true), + IsMultiRegion: types.BoolTest(true), + EnableLogFileValidation: types.BoolTest(true), + CloudWatchLogsLogGroupArn: types.StringTest("cw-arn"), + KMSKeyID: types.StringTest("my-kms-key"), + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + Trail: + Type: AWS::CloudTrail::Trail + `, + expected: cloudtrail.CloudTrail{ + Trails: []cloudtrail.Trail{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/cloudtrail/trails.go b/pkg/iac/adapters/cloudformation/aws/cloudtrail/trails.go new file mode 100644 index 000000000000..fc9c3871cbba --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/cloudtrail/trails.go @@ -0,0 +1,27 @@ +package cloudtrail + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudtrail" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getCloudTrails(ctx parser.FileContext) (trails []cloudtrail.Trail) { + + cloudtrailResources := ctx.GetResourcesByType("AWS::CloudTrail::Trail") + + for _, r := range cloudtrailResources { + ct := cloudtrail.Trail{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("TrailName"), + EnableLogFileValidation: r.GetBoolProperty("EnableLogFileValidation"), + IsMultiRegion: r.GetBoolProperty("IsMultiRegionTrail"), + KMSKeyID: r.GetStringProperty("KmsKeyId"), + CloudWatchLogsLogGroupArn: r.GetStringProperty("CloudWatchLogsLogGroupArn"), + IsLogging: r.GetBoolProperty("IsLogging"), + BucketName: r.GetStringProperty("S3BucketName"), + } + + trails = append(trails, ct) + } + return trails +} diff --git a/pkg/iac/adapters/cloudformation/aws/cloudwatch/cloudwatch.go b/pkg/iac/adapters/cloudformation/aws/cloudwatch/cloudwatch.go new file mode 100644 index 000000000000..0c4a59e43189 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/cloudwatch/cloudwatch.go @@ -0,0 +1,13 @@ +package cloudwatch + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudwatch" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a Cloudwatch instance +func Adapt(cfFile parser.FileContext) cloudwatch.CloudWatch { + return cloudwatch.CloudWatch{ + LogGroups: getLogGroups(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/cloudwatch/cloudwatch_test.go b/pkg/iac/adapters/cloudformation/aws/cloudwatch/cloudwatch_test.go new file mode 100644 index 000000000000..c8a7bd95c9a3 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/cloudwatch/cloudwatch_test.go @@ -0,0 +1,57 @@ +package cloudwatch + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudwatch" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected cloudwatch.CloudWatch + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + myLogGroup: + Type: AWS::Logs::LogGroup + Properties: + LogGroupName: my-log-group + RetentionInDays: 7 + KmsKeyId: my-kms + +`, + expected: cloudwatch.CloudWatch{ + LogGroups: []cloudwatch.LogGroup{ + { + Name: types.StringTest("my-log-group"), + RetentionInDays: types.IntTest(7), + KMSKeyID: types.StringTest("my-kms"), + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + myLogGroup: + Type: AWS::Logs::LogGroup + `, + expected: cloudwatch.CloudWatch{ + LogGroups: []cloudwatch.LogGroup{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/cloudwatch/log_group.go b/pkg/iac/adapters/cloudformation/aws/cloudwatch/log_group.go new file mode 100644 index 000000000000..09e039129781 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/cloudwatch/log_group.go @@ -0,0 +1,23 @@ +package cloudwatch + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudwatch" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getLogGroups(ctx parser.FileContext) (logGroups []cloudwatch.LogGroup) { + + logGroupResources := ctx.GetResourcesByType("AWS::Logs::LogGroup") + + for _, r := range logGroupResources { + group := cloudwatch.LogGroup{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("LogGroupName"), + KMSKeyID: r.GetStringProperty("KmsKeyId"), + RetentionInDays: r.GetIntProperty("RetentionInDays"), + } + logGroups = append(logGroups, group) + } + + return logGroups +} diff --git a/pkg/iac/adapters/cloudformation/aws/codebuild/codebuild.go b/pkg/iac/adapters/cloudformation/aws/codebuild/codebuild.go new file mode 100644 index 000000000000..8951f230ee98 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/codebuild/codebuild.go @@ -0,0 +1,13 @@ +package codebuild + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/codebuild" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a CodeBuild instance +func Adapt(cfFile parser.FileContext) codebuild.CodeBuild { + return codebuild.CodeBuild{ + Projects: getProjects(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/codebuild/codebuild_test.go b/pkg/iac/adapters/cloudformation/aws/codebuild/codebuild_test.go new file mode 100644 index 000000000000..06eaa19402e6 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/codebuild/codebuild_test.go @@ -0,0 +1,68 @@ +package codebuild + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/codebuild" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected codebuild.CodeBuild + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + Project: + Type: AWS::CodeBuild::Project + Properties: + Artifacts: + EncryptionDisabled: true + SecondaryArtifacts: + - EncryptionDisabled: true +`, + expected: codebuild.CodeBuild{ + Projects: []codebuild.Project{ + { + ArtifactSettings: codebuild.ArtifactSettings{ + EncryptionEnabled: types.BoolTest(false), + }, + SecondaryArtifactSettings: []codebuild.ArtifactSettings{ + { + EncryptionEnabled: types.BoolTest(false), + }, + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + Project: + Type: AWS::CodeBuild::Project + `, + expected: codebuild.CodeBuild{ + Projects: []codebuild.Project{ + { + ArtifactSettings: codebuild.ArtifactSettings{ + EncryptionEnabled: types.BoolTest(true), + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/codebuild/project.go b/pkg/iac/adapters/cloudformation/aws/codebuild/project.go new file mode 100644 index 000000000000..554fc8afecea --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/codebuild/project.go @@ -0,0 +1,63 @@ +package codebuild + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/codebuild" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getProjects(ctx parser.FileContext) (projects []codebuild.Project) { + + projectResources := ctx.GetResourcesByType("AWS::CodeBuild::Project") + + for _, r := range projectResources { + project := codebuild.Project{ + Metadata: r.Metadata(), + ArtifactSettings: getArtifactSettings(r), + SecondaryArtifactSettings: getSecondaryArtifactSettings(r), + } + + projects = append(projects, project) + } + + return projects +} + +func getSecondaryArtifactSettings(r *parser.Resource) (secondaryArtifacts []codebuild.ArtifactSettings) { + secondaryArtifactsList := r.GetProperty("SecondaryArtifacts") + if secondaryArtifactsList.IsNil() || !secondaryArtifactsList.IsList() { + return + } + + for _, a := range secondaryArtifactsList.AsList() { + settings := codebuild.ArtifactSettings{ + Metadata: secondaryArtifactsList.Metadata(), + EncryptionEnabled: types.BoolDefault(true, secondaryArtifactsList.Metadata()), + } + encryptionDisabled := a.GetProperty("EncryptionDisabled") + if encryptionDisabled.IsBool() { + settings.EncryptionEnabled = types.Bool(!encryptionDisabled.AsBool(), encryptionDisabled.Metadata()) + } + secondaryArtifacts = append(secondaryArtifacts, settings) + } + + return secondaryArtifacts +} + +func getArtifactSettings(r *parser.Resource) codebuild.ArtifactSettings { + + settings := codebuild.ArtifactSettings{ + Metadata: r.Metadata(), + EncryptionEnabled: types.BoolDefault(true, r.Metadata()), + } + + artifactsProperty := r.GetProperty("Artifacts") + if artifactsProperty.IsNotNil() { + encryptionDisabled := artifactsProperty.GetProperty("EncryptionDisabled") + if encryptionDisabled.IsBool() { + settings.EncryptionEnabled = types.Bool(!encryptionDisabled.AsBool(), encryptionDisabled.Metadata()) + } + } + + return settings +} diff --git a/pkg/iac/adapters/cloudformation/aws/config/adapt_test.go b/pkg/iac/adapters/cloudformation/aws/config/adapt_test.go new file mode 100644 index 000000000000..e6dc652da7b1 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/config/adapt_test.go @@ -0,0 +1,57 @@ +package config + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/config" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected config.Config + }{ + { + name: "Config aggregator with AccountAggregationSources", + source: `AWSTemplateFormatVersion: "2010-09-09" +Resources: + ConfigurationAggregator: + Type: AWS::Config::ConfigurationAggregator + Properties: + AccountAggregationSources: + - AllAwsRegions: "true" +`, + expected: config.Config{ + ConfigurationAggregrator: config.ConfigurationAggregrator{ + SourceAllRegions: types.BoolTest(true), + }, + }, + }, + { + name: "Config aggregator with OrganizationAggregationSource", + source: `AWSTemplateFormatVersion: "2010-09-09" +Resources: + ConfigurationAggregator: + Type: AWS::Config::ConfigurationAggregator + Properties: + OrganizationAggregationSource: + AllAwsRegions: "true" +`, + expected: config.Config{ + ConfigurationAggregrator: config.ConfigurationAggregrator{ + SourceAllRegions: types.BoolTest(true), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/cloudformation/aws/config/aggregator.go b/pkg/iac/adapters/cloudformation/aws/config/aggregator.go new file mode 100644 index 000000000000..72447398b80f --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/config/aggregator.go @@ -0,0 +1,41 @@ +package config + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/config" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getConfigurationAggregator(ctx parser.FileContext) config.ConfigurationAggregrator { + + aggregator := config.ConfigurationAggregrator{ + Metadata: iacTypes.NewUnmanagedMetadata(), + SourceAllRegions: iacTypes.BoolDefault(false, ctx.Metadata()), + } + + aggregatorResources := ctx.GetResourcesByType("AWS::Config::ConfigurationAggregator") + + if len(aggregatorResources) == 0 { + return aggregator + } + + return config.ConfigurationAggregrator{ + Metadata: aggregatorResources[0].Metadata(), + SourceAllRegions: isSourcingAllRegions(aggregatorResources[0]), + } +} + +func isSourcingAllRegions(r *parser.Resource) iacTypes.BoolValue { + accountProp := r.GetProperty("AccountAggregationSources") + + if accountProp.IsNotNil() && accountProp.IsList() { + for _, a := range accountProp.AsList() { + regionsProp := a.GetProperty("AllAwsRegions") + if regionsProp.IsNotNil() { + return a.GetBoolProperty("AllAwsRegions") + } + } + } + + return r.GetBoolProperty("OrganizationAggregationSource.AllAwsRegions") +} diff --git a/pkg/iac/adapters/cloudformation/aws/config/config.go b/pkg/iac/adapters/cloudformation/aws/config/config.go new file mode 100644 index 000000000000..a9634252694e --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/config/config.go @@ -0,0 +1,13 @@ +package config + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/config" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a configurationaggregator instance +func Adapt(cfFile parser.FileContext) config.Config { + return config.Config{ + ConfigurationAggregrator: getConfigurationAggregator(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/documentdb/cluster.go b/pkg/iac/adapters/cloudformation/aws/documentdb/cluster.go new file mode 100644 index 000000000000..f37467dc4100 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/documentdb/cluster.go @@ -0,0 +1,58 @@ +package documentdb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/documentdb" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getClusters(ctx parser.FileContext) (clusters []documentdb.Cluster) { + + clusterResources := ctx.GetResourcesByType("AWS::DocDB::DBCluster") + + for _, r := range clusterResources { + cluster := documentdb.Cluster{ + Metadata: r.Metadata(), + Identifier: r.GetStringProperty("DBClusterIdentifier"), + EnabledLogExports: getLogExports(r), + Instances: nil, + BackupRetentionPeriod: r.GetIntProperty("BackupRetentionPeriod", 1), + StorageEncrypted: r.GetBoolProperty("StorageEncrypted"), + KMSKeyID: r.GetStringProperty("KmsKeyId"), + } + + updateInstancesOnCluster(&cluster, ctx) + + clusters = append(clusters, cluster) + } + return clusters +} + +func updateInstancesOnCluster(cluster *documentdb.Cluster, ctx parser.FileContext) { + + instanceResources := ctx.GetResourcesByType("AWS::DocDB::DBInstance") + + for _, r := range instanceResources { + clusterIdentifier := r.GetStringProperty("DBClusterIdentifier") + if cluster.Identifier.EqualTo(clusterIdentifier.Value()) { + cluster.Instances = append(cluster.Instances, documentdb.Instance{ + Metadata: r.Metadata(), + KMSKeyID: cluster.KMSKeyID, + }) + } + } +} + +func getLogExports(r *parser.Resource) (logExports []types.StringValue) { + + exportsList := r.GetProperty("EnableCloudwatchLogsExports") + + if exportsList.IsNil() || exportsList.IsNotList() { + return logExports + } + + for _, export := range exportsList.AsList() { + logExports = append(logExports, export.AsStringValue()) + } + return logExports +} diff --git a/pkg/iac/adapters/cloudformation/aws/documentdb/documentdb.go b/pkg/iac/adapters/cloudformation/aws/documentdb/documentdb.go new file mode 100644 index 000000000000..40b0c6ffae44 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/documentdb/documentdb.go @@ -0,0 +1,13 @@ +package documentdb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/documentdb" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adaps a documentDB instance +func Adapt(cfFile parser.FileContext) documentdb.DocumentDB { + return documentdb.DocumentDB{ + Clusters: getClusters(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/documentdb/documentdb_test.go b/pkg/iac/adapters/cloudformation/aws/documentdb/documentdb_test.go new file mode 100644 index 000000000000..3e60155e9dfb --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/documentdb/documentdb_test.go @@ -0,0 +1,79 @@ +package documentdb + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/documentdb" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected documentdb.DocumentDB + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + myDBCluster: + Type: 'AWS::DocDB::DBCluster' + Properties: + BackupRetentionPeriod: 8 + DBClusterIdentifier: sample-cluster + KmsKeyId: your-kms-key-id + StorageEncrypted: true + EnableCloudwatchLogsExports: + - audit + - general + myDBInstance: + Type: 'AWS::DocDB::DBInstance' + Properties: + DBClusterIdentifier: sample-cluster + KmsKeyId: your-kms-key-id +`, + expected: documentdb.DocumentDB{ + Clusters: []documentdb.Cluster{ + { + Identifier: types.StringTest("sample-cluster"), + BackupRetentionPeriod: types.IntTest(8), + KMSKeyID: types.StringTest("your-kms-key-id"), + StorageEncrypted: types.BoolTest(true), + EnabledLogExports: []types.StringValue{ + types.StringTest("audit"), + types.StringTest("general"), + }, + Instances: []documentdb.Instance{ + { + KMSKeyID: types.StringTest("your-kms-key-id"), + }, + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + myDBCluster: + Type: 'AWS::DocDB::DBCluster' + `, + expected: documentdb.DocumentDB{ + Clusters: []documentdb.Cluster{ + { + BackupRetentionPeriod: types.IntTest(1), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/dynamodb/cluster.go b/pkg/iac/adapters/cloudformation/aws/dynamodb/cluster.go new file mode 100644 index 000000000000..8a350236134c --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/dynamodb/cluster.go @@ -0,0 +1,36 @@ +package dynamodb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/dynamodb" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getClusters(file parser.FileContext) (clusters []dynamodb.DAXCluster) { + + clusterResources := file.GetResourcesByType("AWS::DAX::Cluster") + + for _, r := range clusterResources { + cluster := dynamodb.DAXCluster{ + Metadata: r.Metadata(), + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + KMSKeyID: iacTypes.StringDefault("", r.Metadata()), + }, + PointInTimeRecovery: iacTypes.BoolUnresolvable(r.Metadata()), + } + + if sseProp := r.GetProperty("SSESpecification"); sseProp.IsNotNil() { + cluster.ServerSideEncryption = dynamodb.ServerSideEncryption{ + Metadata: sseProp.Metadata(), + Enabled: r.GetBoolProperty("SSESpecification.SSEEnabled"), + KMSKeyID: iacTypes.StringUnresolvable(sseProp.Metadata()), + } + } + + clusters = append(clusters, cluster) + } + + return clusters +} diff --git a/pkg/iac/adapters/cloudformation/aws/dynamodb/dynamodb.go b/pkg/iac/adapters/cloudformation/aws/dynamodb/dynamodb.go new file mode 100644 index 000000000000..ceb5c0b711a4 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/dynamodb/dynamodb.go @@ -0,0 +1,14 @@ +package dynamodb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/dynamodb" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a dynamodb instance +func Adapt(cfFile parser.FileContext) dynamodb.DynamoDB { + return dynamodb.DynamoDB{ + DAXClusters: getClusters(cfFile), + Tables: getTables(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/dynamodb/dynamodb_test.go b/pkg/iac/adapters/cloudformation/aws/dynamodb/dynamodb_test.go new file mode 100644 index 000000000000..44cc8ea11e53 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/dynamodb/dynamodb_test.go @@ -0,0 +1,82 @@ +package dynamodb + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/dynamodb" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected dynamodb.DynamoDB + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + daxCluster: + Type: AWS::DAX::Cluster + Properties: + SSESpecification: + SSEEnabled: true + DDBTable: + Type: AWS::DynamoDB::Table + Properties: + SSESpecification: + SSEEnabled: true + KMSMasterKeyId: "test" + PointInTimeRecoverySpecification: + PointInTimeRecoveryEnabled: true + +`, + expected: dynamodb.DynamoDB{ + DAXClusters: []dynamodb.DAXCluster{ + { + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Enabled: types.BoolTest(true), + }, + }, + }, + Tables: []dynamodb.Table{ + { + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Enabled: types.BoolTest(true), + KMSKeyID: types.StringTest("test"), + }, + PointInTimeRecovery: types.BoolTest(true), + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + daxCluster: + Type: AWS::DAX::Cluster + DDBTable: + Type: AWS::DynamoDB::Table + `, + expected: dynamodb.DynamoDB{ + DAXClusters: []dynamodb.DAXCluster{{}}, + Tables: []dynamodb.Table{ + { + ServerSideEncryption: dynamodb.ServerSideEncryption{ + KMSKeyID: types.StringTest(dynamodb.DefaultKMSKeyID), + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/dynamodb/table.go b/pkg/iac/adapters/cloudformation/aws/dynamodb/table.go new file mode 100644 index 000000000000..e1d8fa10737b --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/dynamodb/table.go @@ -0,0 +1,24 @@ +package dynamodb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/dynamodb" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func getTables(fctx parser.FileContext) []dynamodb.Table { + return xslices.Map(fctx.GetResourcesByType("AWS::DynamoDB::Table"), func( + resource *parser.Resource, + ) dynamodb.Table { + sseSpec := resource.GetProperty("SSESpecification") + return dynamodb.Table{ + Metadata: resource.Metadata(), + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Metadata: sseSpec.Metadata(), + Enabled: sseSpec.GetBoolProperty("SSEEnabled"), + KMSKeyID: sseSpec.GetStringProperty("KMSMasterKeyId", dynamodb.DefaultKMSKeyID), + }, + PointInTimeRecovery: resource.GetBoolProperty("PointInTimeRecoverySpecification.PointInTimeRecoveryEnabled"), + } + }) +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/adapt_test.go b/pkg/iac/adapters/cloudformation/aws/ec2/adapt_test.go new file mode 100644 index 000000000000..3971150159ee --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/adapt_test.go @@ -0,0 +1,420 @@ +package ec2 + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected ec2.EC2 + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyEC2Instance: + Type: AWS::EC2::Instance + Properties: + ImageId: "ami-79fd7eee" + KeyName: "testkey" + BlockDeviceMappings: + - DeviceName: "/dev/sdm" + Ebs: + VolumeType: "io1" + Iops: "200" + DeleteOnTermination: "false" + VolumeSize: "20" + Encrypted: true + - DeviceName: "/dev/sdk" + NoDevice: {} + NewVolume: + Type: AWS::EC2::Volume + Properties: + KmsKeyId: alias/my_cmk + Encrypted: true + mySubnet: + Type: AWS::EC2::Subnet + Properties: + MapPublicIpOnLaunch: true + InstanceSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: default + GroupDescription: Allow http to client host + VpcId: vpc-id + SecurityGroupIngress: + - IpProtocol: tcp + Description: ingress + FromPort: "80" + ToPort: 80 + CidrIp: 0.0.0.0/0 + SecurityGroupEgress: + - IpProtocol: -1 + Description: egress + FromPort: 80 + ToPort: "80" + CidrIp: "0.0.0.0/0" + myNetworkAcl: + Type: AWS::EC2::NetworkAcl + Properties: + VpcId: vpc-1122334455aabbccd + InboundRule: + Type: AWS::EC2::NetworkAclEntry + Properties: + NetworkAclId: + Ref: myNetworkAcl + Egress: true + Protocol: 6 + RuleAction: allow + CidrBlock: 172.16.0.0/24 + PortRange: + From: 22 + To: "23" + myLaunchConfig: + Type: AWS::AutoScaling::LaunchConfiguration + Properties: + LaunchConfigurationName: test-cfg + InstanceId: !Ref MyEC2Instance + AssociatePublicIpAddress: true + SecurityGroups: + - !Ref InstanceSecurityGroup + UserData: test + BlockDeviceMappings: + - DeviceName: /dev/sda1 + Ebs: + VolumeSize: '30' + VolumeType: gp3 + Encrypted: true + - DeviceName: /dev/sdm + Ebs: + VolumeSize: '100' + DeleteOnTermination: false + MetadataOptions: + HttpTokens: required + HttpEndpoint: disabled + MyVPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + MyFlowLog: + Type: AWS::EC2::FlowLog + Properties: + LogGroupName: FlowLogsGroup + ResourceId: !Ref MyVPC + ResourceType: VPC + TrafficType: ALL +`, + expected: ec2.EC2{ + Instances: []ec2.Instance{ + { + MetadataOptions: ec2.MetadataOptions{ + HttpEndpoint: types.StringTest("enabled"), + HttpTokens: types.StringTest("optional"), + }, + RootBlockDevice: &ec2.BlockDevice{ + Encrypted: types.BoolTest(true), + }, + EBSBlockDevices: []*ec2.BlockDevice{ + { + Encrypted: types.BoolTest(false), + }, + }, + }, + }, + Volumes: []ec2.Volume{ + { + Encryption: ec2.Encryption{ + KMSKeyID: types.StringTest("alias/my_cmk"), + Enabled: types.BoolTest(true), + }, + }, + }, + Subnets: []ec2.Subnet{ + { + MapPublicIpOnLaunch: types.BoolTest(true), + }, + }, + SecurityGroups: []ec2.SecurityGroup{ + { + IsDefault: types.BoolTest(true), + Description: types.StringTest("Allow http to client host"), + VPCID: types.StringTest("vpc-id"), + IngressRules: []ec2.SecurityGroupRule{ + { + Description: types.StringTest("ingress"), + CIDRs: []types.StringValue{ + types.StringTest("0.0.0.0/0"), + }, + FromPort: types.IntTest(80), + ToPort: types.IntTest(80), + Protocol: types.StringTest("tcp"), + }, + }, + EgressRules: []ec2.SecurityGroupRule{ + { + Description: types.StringTest("egress"), + CIDRs: []types.StringValue{ + types.StringTest("0.0.0.0/0"), + }, + FromPort: types.IntTest(80), + ToPort: types.IntTest(80), + Protocol: types.StringTest("-1"), + }, + }, + }, + }, + NetworkACLs: []ec2.NetworkACL{ + { + Rules: []ec2.NetworkACLRule{ + { + Type: types.StringTest(ec2.TypeEgress), + Action: types.StringTest(ec2.ActionAllow), + Protocol: types.StringTest("6"), + CIDRs: []types.StringValue{ + types.StringTest("172.16.0.0/24"), + }, + FromPort: types.IntTest(22), + ToPort: types.IntTest(23), + }, + }, + }, + }, + LaunchConfigurations: []ec2.LaunchConfiguration{ + { + Name: types.StringTest("test-cfg"), + AssociatePublicIP: types.BoolTest(true), + RootBlockDevice: &ec2.BlockDevice{ + Encrypted: types.BoolTest(true), + }, + EBSBlockDevices: []*ec2.BlockDevice{ + { + Encrypted: types.BoolTest(false), + }, + }, + UserData: types.StringTest("test"), + MetadataOptions: ec2.MetadataOptions{ + HttpTokens: types.StringTest("required"), + HttpEndpoint: types.StringTest("disabled"), + }, + }, + }, + VPCs: []ec2.VPC{ + { + FlowLogsEnabled: types.BoolTest(true), + }, + }, + }, + }, + { + name: "ec2 instance with launch template, ref to name", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyLaunchTemplate: + Type: AWS::EC2::LaunchTemplate + Properties: + LaunchTemplateName: MyTemplate + LaunchTemplateData: + MetadataOptions: + HttpEndpoint: enabled + HttpTokens: required + MyEC2Instance: + Type: AWS::EC2::Instance + Properties: + ImageId: "ami-79fd7eee" + LaunchTemplate: + LaunchTemplateName: MyTemplate +`, + expected: ec2.EC2{ + LaunchTemplates: []ec2.LaunchTemplate{ + { + Name: types.StringTest("MyTemplate"), + Instance: ec2.Instance{ + MetadataOptions: ec2.MetadataOptions{ + HttpEndpoint: types.StringTest("enabled"), + HttpTokens: types.StringTest("required"), + }, + }, + }, + }, + Instances: []ec2.Instance{ + { + MetadataOptions: ec2.MetadataOptions{ + HttpEndpoint: types.StringTest("enabled"), + HttpTokens: types.StringTest("required"), + }, + RootBlockDevice: &ec2.BlockDevice{ + Encrypted: types.BoolTest(false), + }, + }, + }, + VPCs: nil, + }, + }, + { + name: "ec2 instance with launch template, ref to id", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyLaunchTemplate: + Type: AWS::EC2::LaunchTemplate + Properties: + LaunchTemplateName: MyTemplate + LaunchTemplateData: + MetadataOptions: + HttpEndpoint: enabled + HttpTokens: required + MyEC2Instance: + Type: AWS::EC2::Instance + Properties: + ImageId: "ami-79fd7eee" + LaunchTemplate: + LaunchTemplateId: !Ref MyLaunchTemplate +`, + expected: ec2.EC2{ + LaunchTemplates: []ec2.LaunchTemplate{ + { + Name: types.StringTest("MyTemplate"), + Instance: ec2.Instance{ + MetadataOptions: ec2.MetadataOptions{ + HttpEndpoint: types.StringTest("enabled"), + HttpTokens: types.StringTest("required"), + }, + }, + }, + }, + Instances: []ec2.Instance{ + { + MetadataOptions: ec2.MetadataOptions{ + HttpEndpoint: types.StringTest("enabled"), + HttpTokens: types.StringTest("required"), + }, + RootBlockDevice: &ec2.BlockDevice{ + Encrypted: types.BoolTest(false), + }, + }, + }, + VPCs: nil, + }, + }, + { + name: "security group with ingress and egress rules", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MySecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: MySecurityGroup + GroupDescription: MySecurityGroup + InboundRule: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref MySecurityGroup + Description: Inbound + CidrIp: 0.0.0.0/0 + OutboundRule: + Type: AWS::EC2::SecurityGroupEgress + Properties: + GroupId: !GetAtt MySecurityGroup.GroupId + Description: Outbound + CidrIp: 0.0.0.0/0 + RuleWithoutGroup: + Type: AWS::EC2::SecurityGroupIngress + Properties: + CidrIpv6: ::/0 + Description: Inbound +`, + expected: ec2.EC2{ + SecurityGroups: []ec2.SecurityGroup{ + { + Description: types.StringTest("MySecurityGroup"), + IngressRules: []ec2.SecurityGroupRule{ + { + Description: types.StringTest("Inbound"), + CIDRs: []types.StringValue{ + types.StringTest("0.0.0.0/0"), + }, + FromPort: types.IntTest(-1), + ToPort: types.IntTest(-1), + }, + }, + EgressRules: []ec2.SecurityGroupRule{ + { + Description: types.StringTest("Outbound"), + CIDRs: []types.StringValue{ + types.StringTest("0.0.0.0/0"), + }, + FromPort: types.IntTest(-1), + ToPort: types.IntTest(-1), + }, + }, + }, + }, + VPCs: nil, + }, + }, + { + name: "empty", + source: `--- +AWSTemplateFormatVersion: 2010-09-09 +Description: Godd example of excessive ports +Resources: + NetworkACL: + Type: AWS::EC2::NetworkAcl + Rule: + Type: AWS::EC2::NetworkAclEntry + Properties: + NetworkAclId: + Ref: NetworkACL`, + expected: ec2.EC2{ + NetworkACLs: []ec2.NetworkACL{ + { + Rules: []ec2.NetworkACLRule{ + { + Action: types.StringTest("allow"), + Type: types.StringTest("ingress"), + FromPort: types.IntTest(-1), + ToPort: types.IntTest(-1), + }, + }, + }, + }, + VPCs: nil, + }, + }, + { + name: "VPC flow log ref to other VPC", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyVPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + MyFlowLog: + Type: AWS::EC2::FlowLog + Properties: + LogGroupName: FlowLogsGroup + ResourceId: !Ref MyOtherVPC + ResourceType: VPC + TrafficType: ALL`, + expected: ec2.EC2{ + VPCs: []ec2.VPC{ + { + FlowLogsEnabled: types.BoolTest(false), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/ec2.go b/pkg/iac/adapters/cloudformation/aws/ec2/ec2.go new file mode 100644 index 000000000000..ffb0c37533d1 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/ec2.go @@ -0,0 +1,20 @@ +package ec2 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an EC2 instance +func Adapt(cfFile parser.FileContext) ec2.EC2 { + return ec2.EC2{ + LaunchConfigurations: getLaunchConfigurations(cfFile), + LaunchTemplates: getLaunchTemplates(cfFile), + Instances: getInstances(cfFile), + VPCs: getVPCs(cfFile), + NetworkACLs: getNetworkACLs(cfFile), + SecurityGroups: getSecurityGroups(cfFile), + Subnets: getSubnets(cfFile), + Volumes: getVolumes(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/instance.go b/pkg/iac/adapters/cloudformation/aws/ec2/instance.go new file mode 100644 index 000000000000..7b6f149e0168 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/instance.go @@ -0,0 +1,106 @@ +package ec2 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getInstances(ctx parser.FileContext) (instances []ec2.Instance) { + instanceResources := ctx.GetResourcesByType("AWS::EC2::Instance") + + for _, r := range instanceResources { + instance := ec2.Instance{ + Metadata: r.Metadata(), + // metadata not supported by CloudFormation at the moment - + // https://github.com/aws-cloudformation/cloudformation-coverage-roadmap/issues/655 + MetadataOptions: ec2.MetadataOptions{ + Metadata: r.Metadata(), + HttpTokens: iacTypes.StringDefault("optional", r.Metadata()), + HttpEndpoint: iacTypes.StringDefault("enabled", r.Metadata()), + }, + UserData: r.GetStringProperty("UserData"), + } + + if launchTemplate, ok := findRelatedLaunchTemplate(ctx, r); ok { + instance = launchTemplate.Instance + } + + if instance.RootBlockDevice == nil { + instance.RootBlockDevice = &ec2.BlockDevice{ + Metadata: r.Metadata(), + Encrypted: iacTypes.BoolDefault(false, r.Metadata()), + } + } + + blockDevices := getBlockDevices(r) + for i, device := range blockDevices { + copyDevice := device + if i == 0 { + instance.RootBlockDevice = copyDevice + continue + } + instance.EBSBlockDevices = append(instance.EBSBlockDevices, device) + } + instances = append(instances, instance) + } + + return instances +} + +func findRelatedLaunchTemplate(fctx parser.FileContext, r *parser.Resource) (ec2.LaunchTemplate, bool) { + launchTemplateRef := r.GetProperty("LaunchTemplate.LaunchTemplateName") + if launchTemplateRef.IsString() { + res := findLaunchTemplateByName(fctx, launchTemplateRef) + if res != nil { + return adaptLaunchTemplate(res), true + } + } + + launchTemplateRef = r.GetProperty("LaunchTemplate.LaunchTemplateId") + if !launchTemplateRef.IsString() { + return ec2.LaunchTemplate{}, false + } + + resource := fctx.GetResourceByLogicalID(launchTemplateRef.AsString()) + if resource == nil { + return ec2.LaunchTemplate{}, false + } + return adaptLaunchTemplate(resource), true +} + +func findLaunchTemplateByName(fctx parser.FileContext, prop *parser.Property) *parser.Resource { + for _, res := range fctx.GetResourcesByType("AWS::EC2::LaunchTemplate") { + templateName := res.GetProperty("LaunchTemplateName") + if templateName.IsNotString() { + continue + } + + if prop.EqualTo(templateName.AsString()) { + return res + } + } + + return nil +} + +func getBlockDevices(r *parser.Resource) []*ec2.BlockDevice { + var blockDevices []*ec2.BlockDevice + + devicesProp := r.GetProperty("BlockDeviceMappings") + + if devicesProp.IsNil() { + return blockDevices + } + + for _, d := range devicesProp.AsList() { + device := &ec2.BlockDevice{ + Metadata: d.Metadata(), + Encrypted: d.GetBoolProperty("Ebs.Encrypted"), + } + + blockDevices = append(blockDevices, device) + } + + return blockDevices +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/launch_configuration.go b/pkg/iac/adapters/cloudformation/aws/ec2/launch_configuration.go new file mode 100644 index 000000000000..e99459b5d4f0 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/launch_configuration.go @@ -0,0 +1,48 @@ +package ec2 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getLaunchConfigurations(file parser.FileContext) (launchConfigurations []ec2.LaunchConfiguration) { + launchConfigResources := file.GetResourcesByType("AWS::AutoScaling::LaunchConfiguration") + + for _, r := range launchConfigResources { + + launchConfig := ec2.LaunchConfiguration{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("LaunchConfigurationName"), + AssociatePublicIP: r.GetBoolProperty("AssociatePublicIpAddress"), + MetadataOptions: ec2.MetadataOptions{ + Metadata: r.Metadata(), + HttpTokens: types.StringDefault("optional", r.Metadata()), + HttpEndpoint: types.StringDefault("enabled", r.Metadata()), + }, + UserData: r.GetStringProperty("UserData"), + } + + if opts := r.GetProperty("MetadataOptions"); opts.IsNotNil() { + launchConfig.MetadataOptions = ec2.MetadataOptions{ + Metadata: opts.Metadata(), + HttpTokens: opts.GetStringProperty("HttpTokens", "optional"), + HttpEndpoint: opts.GetStringProperty("HttpEndpoint", "enabled"), + } + } + + blockDevices := getBlockDevices(r) + for i, device := range blockDevices { + copyDevice := device + if i == 0 { + launchConfig.RootBlockDevice = copyDevice + continue + } + launchConfig.EBSBlockDevices = append(launchConfig.EBSBlockDevices, device) + } + + launchConfigurations = append(launchConfigurations, launchConfig) + + } + return launchConfigurations +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/launch_template.go b/pkg/iac/adapters/cloudformation/aws/ec2/launch_template.go new file mode 100644 index 000000000000..c138ed3284e1 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/launch_template.go @@ -0,0 +1,56 @@ +package ec2 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getLaunchTemplates(file parser.FileContext) (templates []ec2.LaunchTemplate) { + launchConfigResources := file.GetResourcesByType("AWS::EC2::LaunchTemplate") + + for _, r := range launchConfigResources { + templates = append(templates, adaptLaunchTemplate(r)) + } + return templates +} + +func adaptLaunchTemplate(r *parser.Resource) ec2.LaunchTemplate { + launchTemplate := ec2.LaunchTemplate{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("LaunchTemplateName", ""), + Instance: ec2.Instance{ + Metadata: r.Metadata(), + MetadataOptions: ec2.MetadataOptions{ + Metadata: r.Metadata(), + HttpTokens: types.StringDefault("optional", r.Metadata()), + HttpEndpoint: types.StringDefault("enabled", r.Metadata()), + }, + UserData: types.StringDefault("", r.Metadata()), + }, + } + + if data := r.GetProperty("LaunchTemplateData"); data.IsNotNil() { + if opts := data.GetProperty("MetadataOptions"); opts.IsNotNil() { + launchTemplate.MetadataOptions = ec2.MetadataOptions{ + Metadata: opts.Metadata(), + HttpTokens: opts.GetStringProperty("HttpTokens", "optional"), + HttpEndpoint: opts.GetStringProperty("HttpEndpoint", "enabled"), + } + } + + launchTemplate.Instance.UserData = data.GetStringProperty("UserData", "") + + blockDevices := getBlockDevices(r) + for i, device := range blockDevices { + copyDevice := device + if i == 0 { + launchTemplate.RootBlockDevice = copyDevice + } else { + launchTemplate.EBSBlockDevices = append(launchTemplate.EBSBlockDevices, device) + } + } + } + + return launchTemplate +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/nacl.go b/pkg/iac/adapters/cloudformation/aws/ec2/nacl.go new file mode 100644 index 000000000000..a91a12569994 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/nacl.go @@ -0,0 +1,82 @@ +package ec2 + +import ( + "strconv" + + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getNetworkACLs(ctx parser.FileContext) (acls []ec2.NetworkACL) { + for _, aclResource := range ctx.GetResourcesByType("AWS::EC2::NetworkAcl") { + acl := ec2.NetworkACL{ + Metadata: aclResource.Metadata(), + Rules: getRules(aclResource.ID(), ctx), + IsDefaultRule: iacTypes.BoolDefault(false, aclResource.Metadata()), + } + acls = append(acls, acl) + } + return acls +} + +func getRules(id string, ctx parser.FileContext) (rules []ec2.NetworkACLRule) { + for _, ruleResource := range ctx.GetResourcesByType("AWS::EC2::NetworkAclEntry") { + aclID := ruleResource.GetProperty("NetworkAclId") + if aclID.IsString() && aclID.AsString() == id { + + rule := ec2.NetworkACLRule{ + Metadata: ruleResource.Metadata(), + Type: iacTypes.StringDefault(ec2.TypeIngress, ruleResource.Metadata()), + Action: iacTypes.StringDefault(ec2.ActionAllow, ruleResource.Metadata()), + FromPort: iacTypes.IntDefault(-1, ruleResource.Metadata()), + ToPort: iacTypes.IntDefault(-1, ruleResource.Metadata()), + CIDRs: nil, + } + + if egressProperty := ruleResource.GetProperty("Egress"); egressProperty.IsBool() { + if egressProperty.AsBool() { + rule.Type = iacTypes.String(ec2.TypeEgress, egressProperty.Metadata()) + } else { + rule.Type = iacTypes.String(ec2.TypeIngress, egressProperty.Metadata()) + } + } + + if actionProperty := ruleResource.GetProperty("RuleAction"); actionProperty.IsString() { + if actionProperty.AsString() == ec2.ActionAllow { + rule.Action = iacTypes.String(ec2.ActionAllow, actionProperty.Metadata()) + } else { + rule.Action = iacTypes.String(ec2.ActionDeny, actionProperty.Metadata()) + } + } + + if protocolProperty := ruleResource.GetProperty("Protocol"); protocolProperty.IsInt() { + protocol := protocolProperty.AsIntValue().Value() + rule.Protocol = iacTypes.String(strconv.Itoa(protocol), protocolProperty.Metadata()) + } + + if ipv4Cidr := ruleResource.GetProperty("CidrBlock"); ipv4Cidr.IsString() { + rule.CIDRs = append(rule.CIDRs, ipv4Cidr.AsStringValue()) + } + + if ipv6Cidr := ruleResource.GetProperty("Ipv6CidrBlock"); ipv6Cidr.IsString() { + rule.CIDRs = append(rule.CIDRs, ipv6Cidr.AsStringValue()) + } + + portRange := ruleResource.GetProperty("PortRange") + fromPort := portRange.GetProperty("From").ConvertTo(cftypes.Int) + if fromPort.IsInt() { + rule.FromPort = fromPort.AsIntValue() + } + + toPort := portRange.GetProperty("To").ConvertTo(cftypes.Int) + if toPort.IsInt() { + rule.ToPort = toPort.AsIntValue() + } + + rules = append(rules, rule) + } + } + return rules +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/security_group.go b/pkg/iac/adapters/cloudformation/aws/ec2/security_group.go new file mode 100644 index 000000000000..e85a91cdecb6 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/security_group.go @@ -0,0 +1,108 @@ +package ec2 + +import ( + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getSecurityGroups(ctx parser.FileContext) []ec2.SecurityGroup { + mGroups := make(map[string]ec2.SecurityGroup) + + for _, r := range ctx.GetResourcesByType("AWS::EC2::SecurityGroup") { + group := ec2.SecurityGroup{ + Metadata: r.Metadata(), + Description: r.GetStringProperty("GroupDescription"), + IngressRules: getIngressRules(r), + EgressRules: getEgressRules(r), + IsDefault: types.Bool(r.GetStringProperty("GroupName").EqualTo("default"), r.Metadata()), + VPCID: r.GetStringProperty("VpcId"), + } + + mGroups[r.ID()] = group + } + + for _, r := range ctx.GetResourcesByType("AWS::EC2::SecurityGroupIngress") { + groupID := r.GetProperty("GroupId").AsString() + + if group, ok := mGroups[groupID]; ok { + group.IngressRules = append(group.IngressRules, adaptRule(r)) + mGroups[groupID] = group + } + } + + for _, r := range ctx.GetResourcesByType("AWS::EC2::SecurityGroupEgress") { + groupID := r.GetProperty("GroupId").AsString() + + if group, ok := mGroups[groupID]; ok { + group.EgressRules = append(group.EgressRules, adaptRule(r)) + mGroups[groupID] = group + } + } + + if len(mGroups) > 0 { + return lo.Values(mGroups) + } + return nil +} + +func getIngressRules(r *parser.Resource) (sgRules []ec2.SecurityGroupRule) { + if ingressProp := r.GetProperty("SecurityGroupIngress"); ingressProp.IsList() { + for _, ingress := range ingressProp.AsList() { + sgRules = append(sgRules, adaptRule(ingress)) + } + } + + return sgRules +} + +func getEgressRules(r *parser.Resource) (sgRules []ec2.SecurityGroupRule) { + if egressProp := r.GetProperty("SecurityGroupEgress"); egressProp.IsList() { + for _, egress := range egressProp.AsList() { + sgRules = append(sgRules, adaptRule(egress)) + } + } + return sgRules +} + +func adaptRule(r interface { + GetProperty(string) *parser.Property + Metadata() types.Metadata + GetStringProperty(string, ...string) types.StringValue +}) ec2.SecurityGroupRule { + rule := ec2.SecurityGroupRule{ + Metadata: r.Metadata(), + Description: r.GetStringProperty("Description"), + FromPort: types.IntDefault(-1, r.Metadata()), + ToPort: types.IntDefault(-1, r.Metadata()), + } + + v4Cidr := r.GetProperty("CidrIp") + if v4Cidr.IsString() && v4Cidr.AsStringValue().IsNotEmpty() { + rule.CIDRs = append(rule.CIDRs, types.StringExplicit(v4Cidr.AsString(), v4Cidr.Metadata())) + } + v6Cidr := r.GetProperty("CidrIpv6") + if v6Cidr.IsString() && v6Cidr.AsStringValue().IsNotEmpty() { + rule.CIDRs = append(rule.CIDRs, types.StringExplicit(v6Cidr.AsString(), v6Cidr.Metadata())) + } + + fromPort := r.GetProperty("FromPort").ConvertTo(cftypes.Int) + if fromPort.IsInt() { + rule.FromPort = fromPort.AsIntValue() + } + + toPort := r.GetProperty("ToPort").ConvertTo(cftypes.Int) + if toPort.IsInt() { + rule.ToPort = toPort.AsIntValue() + } + + protocol := r.GetProperty("IpProtocol").ConvertTo(cftypes.String) + if protocol.IsString() { + rule.Protocol = protocol.AsStringValue() + } + + return rule +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/subnet.go b/pkg/iac/adapters/cloudformation/aws/ec2/subnet.go new file mode 100644 index 000000000000..725a441429e1 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/subnet.go @@ -0,0 +1,21 @@ +package ec2 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getSubnets(ctx parser.FileContext) (subnets []ec2.Subnet) { + + subnetResources := ctx.GetResourcesByType("AWS::EC2::Subnet") + for _, r := range subnetResources { + + subnet := ec2.Subnet{ + Metadata: r.Metadata(), + MapPublicIpOnLaunch: r.GetBoolProperty("MapPublicIpOnLaunch"), + } + + subnets = append(subnets, subnet) + } + return subnets +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/volume.go b/pkg/iac/adapters/cloudformation/aws/ec2/volume.go new file mode 100644 index 000000000000..35c22e053cfc --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/volume.go @@ -0,0 +1,25 @@ +package ec2 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getVolumes(ctx parser.FileContext) (volumes []ec2.Volume) { + + volumeResources := ctx.GetResourcesByType("AWS::EC2::Volume") + for _, r := range volumeResources { + + volume := ec2.Volume{ + Metadata: r.Metadata(), + Encryption: ec2.Encryption{ + Metadata: r.Metadata(), + Enabled: r.GetBoolProperty("Encrypted"), + KMSKeyID: r.GetStringProperty("KmsKeyId"), + }, + } + + volumes = append(volumes, volume) + } + return volumes +} diff --git a/pkg/iac/adapters/cloudformation/aws/ec2/vpc.go b/pkg/iac/adapters/cloudformation/aws/ec2/vpc.go new file mode 100644 index 000000000000..2e666fa817d1 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ec2/vpc.go @@ -0,0 +1,32 @@ +package ec2 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/set" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func getVPCs(fctx parser.FileContext) []ec2.VPC { + vpcFlowLogs := getVpcGlowLogs(fctx) + return xslices.Map(fctx.GetResourcesByType("AWS::EC2::VPC"), + func(resource *parser.Resource) ec2.VPC { + return ec2.VPC{ + Metadata: resource.Metadata(), + // CloudFormation does not provide direct management for the default VPC + IsDefault: types.BoolUnresolvable(resource.Metadata()), + FlowLogsEnabled: types.Bool(vpcFlowLogs.Contains(resource.ID()), resource.Metadata()), + } + }) +} + +func getVpcGlowLogs(fctx parser.FileContext) set.Set[string] { + ids := set.New[string]() + for _, resource := range fctx.GetResourcesByType("AWS::EC2::FlowLog") { + if resource.GetStringProperty("ResourceType").EqualTo("VPC") { + ids.Append(resource.GetStringProperty("ResourceId").Value()) + } + } + return ids +} diff --git a/pkg/iac/adapters/cloudformation/aws/ecr/ecr.go b/pkg/iac/adapters/cloudformation/aws/ecr/ecr.go new file mode 100644 index 000000000000..c7552132d862 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ecr/ecr.go @@ -0,0 +1,13 @@ +package ecr + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecr" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an ECR instance +func Adapt(cfFile parser.FileContext) ecr.ECR { + return ecr.ECR{ + Repositories: getRepositories(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/ecr/ecr_test.go b/pkg/iac/adapters/cloudformation/aws/ecr/ecr_test.go new file mode 100644 index 000000000000..bf411dddbf8a --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ecr/ecr_test.go @@ -0,0 +1,102 @@ +package ecr + +import ( + "testing" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecr" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected ecr.ECR + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + +`, + expected: ecr.ECR{}, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyRepository: + Type: AWS::ECR::Repository + Properties: + RepositoryName: "test-repository" + ImageScanningConfiguration: + ScanOnPush: true + EncryptionConfiguration: + EncryptionType: KMS + KmsKey: mykey + ImageTagMutability: IMMUTABLE + RepositoryPolicyText: + Version: "2012-10-17" + Statement: + - + Sid: AllowPushPull + Effect: Allow + Principal: + AWS: + - "arn:aws:iam::123456789012:user/Alice" + Action: + - "ecr:GetDownloadUrlForLayer" + - "ecr:BatchGetImage" + `, + expected: ecr.ECR{ + Repositories: []ecr.Repository{ + { + ImageTagsImmutable: types.BoolTest(true), + ImageScanning: ecr.ImageScanning{ + ScanOnPush: types.BoolTest(true), + }, + Encryption: ecr.Encryption{ + Type: types.StringTest("KMS"), + KMSKeyID: types.StringTest("mykey"), + }, + Policies: []iam.Policy{ + { + Document: func() iam.Document { + return iam.Document{ + Parsed: iamgo.NewPolicyBuilder(). + WithVersion("2012-10-17"). + WithStatement( + iamgo.NewStatementBuilder(). + WithSid("AllowPushPull"). + WithEffect("Allow"). + WithAWSPrincipals( + []string{"arn:aws:iam::123456789012:user/Alice"}, + ). + WithActions( + []string{ + "ecr:GetDownloadUrlForLayer", + "ecr:BatchGetImage", + }, + ). + Build(), + ). + Build(), + } + }(), + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/ecr/repository.go b/pkg/iac/adapters/cloudformation/aws/ecr/repository.go new file mode 100644 index 000000000000..e0491c121b4d --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ecr/repository.go @@ -0,0 +1,87 @@ +package ecr + +import ( + "errors" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecr" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getRepositories(ctx parser.FileContext) (repositories []ecr.Repository) { + + repositoryResources := ctx.GetResourcesByType("AWS::ECR::Repository") + + for _, r := range repositoryResources { + + repository := ecr.Repository{ + Metadata: r.Metadata(), + ImageScanning: ecr.ImageScanning{ + Metadata: r.Metadata(), + ScanOnPush: iacTypes.BoolDefault(false, r.Metadata()), + }, + ImageTagsImmutable: hasImmutableImageTags(r), + Policies: nil, + Encryption: ecr.Encryption{ + Metadata: r.Metadata(), + Type: iacTypes.StringDefault(ecr.EncryptionTypeAES256, r.Metadata()), + KMSKeyID: iacTypes.StringDefault("", r.Metadata()), + }, + } + + if imageScanningProp := r.GetProperty("ImageScanningConfiguration"); imageScanningProp.IsNotNil() { + repository.ImageScanning = ecr.ImageScanning{ + Metadata: imageScanningProp.Metadata(), + ScanOnPush: imageScanningProp.GetBoolProperty("ScanOnPush", false), + } + } + + if encProp := r.GetProperty("EncryptionConfiguration"); encProp.IsNotNil() { + repository.Encryption = ecr.Encryption{ + Metadata: encProp.Metadata(), + Type: encProp.GetStringProperty("EncryptionType", ecr.EncryptionTypeAES256), + KMSKeyID: encProp.GetStringProperty("KmsKey", ""), + } + } + + if policy, err := getPolicy(r); err == nil { + repository.Policies = append(repository.Policies, *policy) + } + + repositories = append(repositories, repository) + } + + return repositories +} + +func getPolicy(r *parser.Resource) (*iam.Policy, error) { + policyProp := r.GetProperty("RepositoryPolicyText") + if policyProp.IsNil() { + return nil, errors.New("missing policy") + } + + parsed, err := iamgo.Parse(policyProp.GetJsonBytes()) + if err != nil { + return nil, err + } + + return &iam.Policy{ + Metadata: policyProp.Metadata(), + Name: iacTypes.StringDefault("", policyProp.Metadata()), + Document: iam.Document{ + Metadata: policyProp.Metadata(), + Parsed: *parsed, + }, + Builtin: iacTypes.Bool(false, policyProp.Metadata()), + }, nil +} + +func hasImmutableImageTags(r *parser.Resource) iacTypes.BoolValue { + mutabilityProp := r.GetProperty("ImageTagMutability") + if mutabilityProp.IsNil() { + return iacTypes.BoolDefault(false, r.Metadata()) + } + return iacTypes.Bool(mutabilityProp.EqualTo("IMMUTABLE"), mutabilityProp.Metadata()) +} diff --git a/pkg/iac/adapters/cloudformation/aws/ecs/cluster.go b/pkg/iac/adapters/cloudformation/aws/ecs/cluster.go new file mode 100644 index 000000000000..ce000d8d1457 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ecs/cluster.go @@ -0,0 +1,57 @@ +package ecs + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecs" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getClusters(ctx parser.FileContext) (clusters []ecs.Cluster) { + + clusterResources := ctx.GetResourcesByType("AWS::ECS::Cluster") + + for _, r := range clusterResources { + + cluster := ecs.Cluster{ + Metadata: r.Metadata(), + Settings: getClusterSettings(r), + } + + clusters = append(clusters, cluster) + + } + + return clusters +} + +func getClusterSettings(r *parser.Resource) ecs.ClusterSettings { + + clusterSettings := ecs.ClusterSettings{ + Metadata: r.Metadata(), + ContainerInsightsEnabled: types.BoolDefault(false, r.Metadata()), + } + + clusterSettingMap := r.GetProperty("ClusterSettings") + if clusterSettingMap.IsNil() || clusterSettingMap.IsNotList() { + return clusterSettings + } + + clusterSettings.Metadata = clusterSettingMap.Metadata() + + for _, setting := range clusterSettingMap.AsList() { + checkProperty(setting, &clusterSettings) + } + + return clusterSettings +} + +func checkProperty(setting *parser.Property, clusterSettings *ecs.ClusterSettings) { + settingMap := setting.AsMap() + name := settingMap["Name"] + if name.IsNotNil() && name.EqualTo("containerInsights") { + value := settingMap["Value"] + if value.IsNotNil() && !value.EqualTo("disabled") { + clusterSettings.ContainerInsightsEnabled = types.Bool(true, value.Metadata()) + } + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/ecs/ecs.go b/pkg/iac/adapters/cloudformation/aws/ecs/ecs.go new file mode 100644 index 000000000000..5707cdddcd8e --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ecs/ecs.go @@ -0,0 +1,14 @@ +package ecs + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecs" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an ECS instance +func Adapt(cfFile parser.FileContext) ecs.ECS { + return ecs.ECS{ + Clusters: getClusters(cfFile), + TaskDefinitions: getTaskDefinitions(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/ecs/ecs_test.go b/pkg/iac/adapters/cloudformation/aws/ecs/ecs_test.go new file mode 100644 index 000000000000..5581c3a44be7 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ecs/ecs_test.go @@ -0,0 +1,130 @@ +package ecs + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecs" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected ecs.ECS + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + ECSCluster: + Type: 'AWS::ECS::Cluster' + Properties: + ClusterName: MyFargateCluster + ClusterSettings: + - Name: containerInsights + Value: enabled + taskdefinition: + Type: AWS::ECS::TaskDefinition + Properties: + ContainerDefinitions: + - + Name: "busybox" + Image: "busybox" + Cpu: "256" + Memory: "512" + Essential: true + Privileged: true + Environment: + - Name: entryPoint + Value: 'sh, -c' + Volumes: + - + Host: + SourcePath: "/var/lib/docker/vfs/dir/" + Name: "my-vol" + EFSVolumeConfiguration: + TransitEncryption: enabled +`, + expected: ecs.ECS{ + Clusters: []ecs.Cluster{ + { + Settings: ecs.ClusterSettings{ + ContainerInsightsEnabled: types.BoolTest(true), + }, + }, + }, + TaskDefinitions: []ecs.TaskDefinition{ + { + Volumes: []ecs.Volume{ + { + EFSVolumeConfiguration: ecs.EFSVolumeConfiguration{ + TransitEncryptionEnabled: types.BoolTest(true), + }, + }, + }, + ContainerDefinitions: []ecs.ContainerDefinition{ + { + Name: types.StringTest("busybox"), + Image: types.StringTest("busybox"), + CPU: types.StringTest("256"), + Memory: types.StringTest("512"), + Essential: types.BoolTest(true), + Privileged: types.BoolTest(true), + Environment: []ecs.EnvVar{ + { + Name: types.StringTest("entryPoint"), + Value: types.StringTest("sh, -c"), + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "ecs Cluster Enhanced Container Insights", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + ECSCluster: + Type: 'AWS::ECS::Cluster' + Properties: + ClusterName: MyFargateCluster + ClusterSettings: + - Name: containerInsights + Value: enhanced +`, + expected: ecs.ECS{ + Clusters: []ecs.Cluster{ + { + Settings: ecs.ClusterSettings{ + ContainerInsightsEnabled: types.BoolTest(true), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + ECSCluster: + Type: 'AWS::ECS::Cluster' + taskdefinition: + Type: AWS::ECS::TaskDefinition + `, + expected: ecs.ECS{ + Clusters: []ecs.Cluster{{}}, + TaskDefinitions: []ecs.TaskDefinition{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/ecs/task_definition.go b/pkg/iac/adapters/cloudformation/aws/ecs/task_definition.go new file mode 100644 index 000000000000..c9b8e59a4a86 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ecs/task_definition.go @@ -0,0 +1,86 @@ +package ecs + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecs" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getTaskDefinitions(ctx parser.FileContext) (taskDefinitions []ecs.TaskDefinition) { + + taskDefResources := ctx.GetResourcesByType("AWS::ECS::TaskDefinition") + + for _, r := range taskDefResources { + definitions, _ := getContainerDefinitions(r) + taskDef := ecs.TaskDefinition{ + Metadata: r.Metadata(), + Volumes: getVolumes(r), + ContainerDefinitions: definitions, + } + taskDefinitions = append(taskDefinitions, taskDef) + } + + return taskDefinitions +} + +func getContainerDefinitions(r *parser.Resource) ([]ecs.ContainerDefinition, error) { + var definitions []ecs.ContainerDefinition + containerDefs := r.GetProperty("ContainerDefinitions") + if containerDefs.IsNil() || containerDefs.IsNotList() { + return definitions, nil + } + for _, containerDef := range containerDefs.AsList() { + + var envVars []ecs.EnvVar + envVarsList := containerDef.GetProperty("Environment") + if envVarsList.IsNotNil() && envVarsList.IsList() { + for _, envVar := range envVarsList.AsList() { + envVars = append(envVars, ecs.EnvVar{ + Name: envVar.GetStringProperty("Name"), + Value: envVar.GetStringProperty("Value"), + }) + } + } + definition := ecs.ContainerDefinition{ + Metadata: containerDef.Metadata(), + Name: containerDef.GetStringProperty("Name"), + Image: containerDef.GetStringProperty("Image"), + CPU: containerDef.GetStringProperty("Cpu"), + Memory: containerDef.GetStringProperty("Memory"), + Essential: containerDef.GetBoolProperty("Essential"), + Privileged: containerDef.GetBoolProperty("Privileged"), + Environment: envVars, + PortMappings: nil, + } + definitions = append(definitions, definition) + } + if containerDefs.IsNotNil() && containerDefs.IsString() { + return ecs.CreateDefinitionsFromString(r.Metadata(), containerDefs.AsString()) + } + return definitions, nil +} + +func getVolumes(r *parser.Resource) (volumes []ecs.Volume) { + + volumesList := r.GetProperty("Volumes") + if volumesList.IsNil() || volumesList.IsNotList() { + return volumes + } + + for _, v := range volumesList.AsList() { + volume := ecs.Volume{ + Metadata: r.Metadata(), + EFSVolumeConfiguration: ecs.EFSVolumeConfiguration{ + Metadata: r.Metadata(), + TransitEncryptionEnabled: types.BoolDefault(false, r.Metadata()), + }, + } + transitProp := v.GetProperty("EFSVolumeConfiguration.TransitEncryption") + if transitProp.IsNotNil() && transitProp.EqualTo("enabled", parser.IgnoreCase) { + volume.EFSVolumeConfiguration.TransitEncryptionEnabled = types.Bool(true, transitProp.Metadata()) + } + + volumes = append(volumes, volume) + } + return volumes +} diff --git a/pkg/iac/adapters/cloudformation/aws/efs/efs.go b/pkg/iac/adapters/cloudformation/aws/efs/efs.go new file mode 100644 index 000000000000..bda1f15ffbfb --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/efs/efs.go @@ -0,0 +1,13 @@ +package efs + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/efs" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an EFS instance +func Adapt(cfFile parser.FileContext) efs.EFS { + return efs.EFS{ + FileSystems: getFileSystems(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/efs/efs_test.go b/pkg/iac/adapters/cloudformation/aws/efs/efs_test.go new file mode 100644 index 000000000000..a22d769020b6 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/efs/efs_test.go @@ -0,0 +1,52 @@ +package efs + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/efs" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected efs.EFS + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + FileSystemResource: + Type: 'AWS::EFS::FileSystem' + Properties: + Encrypted: true +`, + expected: efs.EFS{ + FileSystems: []efs.FileSystem{ + { + Encrypted: types.BoolTest(true), + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + FileSystemResource: + Type: 'AWS::EFS::FileSystem' + `, + expected: efs.EFS{ + FileSystems: []efs.FileSystem{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/efs/filesystem.go b/pkg/iac/adapters/cloudformation/aws/efs/filesystem.go new file mode 100644 index 000000000000..728b09079f2a --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/efs/filesystem.go @@ -0,0 +1,23 @@ +package efs + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/efs" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getFileSystems(ctx parser.FileContext) (filesystems []efs.FileSystem) { + + filesystemResources := ctx.GetResourcesByType("AWS::EFS::FileSystem") + + for _, r := range filesystemResources { + + filesystem := efs.FileSystem{ + Metadata: r.Metadata(), + Encrypted: r.GetBoolProperty("Encrypted"), + } + + filesystems = append(filesystems, filesystem) + } + + return filesystems +} diff --git a/pkg/iac/adapters/cloudformation/aws/eks/cluster.go b/pkg/iac/adapters/cloudformation/aws/eks/cluster.go new file mode 100644 index 000000000000..27e1c80b56c1 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/eks/cluster.go @@ -0,0 +1,98 @@ +package eks + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/eks" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getClusters(ctx parser.FileContext) (clusters []eks.Cluster) { + + clusterResources := ctx.GetResourcesByType("AWS::EKS::Cluster") + + for _, r := range clusterResources { + cluster := eks.Cluster{ + Metadata: r.Metadata(), + Logging: getLogging(r), + Encryption: getEncryptionConfig(r), + PublicAccessEnabled: r.GetBoolProperty("ResourcesVpcConfig.EndpointPublicAccess", true), + PublicAccessCIDRs: getPublicCIDRs(r), + } + + clusters = append(clusters, cluster) + } + return clusters +} + +func getPublicCIDRs(r *parser.Resource) []iacTypes.StringValue { + publicAccessCidrs := r.GetProperty("ResourcesVpcConfig.PublicAccessCidrs") + if publicAccessCidrs.IsNotList() { + return nil + } + + var cidrs []iacTypes.StringValue + for _, el := range publicAccessCidrs.AsList() { + cidrs = append(cidrs, el.AsStringValue()) + } + + if len(cidrs) == 0 { + return []iacTypes.StringValue{iacTypes.StringDefault("0.0.0.0/0", r.Metadata())} + } + + return cidrs +} + +func getEncryptionConfig(r *parser.Resource) eks.Encryption { + + encryptionConfigs := r.GetProperty("EncryptionConfig") + if encryptionConfigs.IsNotList() { + return eks.Encryption{ + Metadata: r.Metadata(), + } + } + + for _, encryptionConfig := range encryptionConfigs.AsList() { + resources := encryptionConfig.GetProperty("Resources") + hasSecrets := resources.IsList() && resources.Contains("secrets") + return eks.Encryption{ + Metadata: encryptionConfig.Metadata(), + KMSKeyID: encryptionConfig.GetStringProperty("Provider.KeyArn"), + Secrets: iacTypes.Bool(hasSecrets, resources.Metadata()), + } + } + + return eks.Encryption{ + Metadata: r.Metadata(), + } +} + +func getLogging(r *parser.Resource) eks.Logging { + enabledTypes := r.GetProperty("Logging.ClusterLogging.EnabledTypes") + if enabledTypes.IsNotList() { + return eks.Logging{ + Metadata: r.Metadata(), + } + } + + logging := eks.Logging{ + Metadata: enabledTypes.Metadata(), + } + + for _, typeConf := range enabledTypes.AsList() { + switch typ := typeConf.GetProperty("Type"); typ.AsString() { + case "api": + logging.API = iacTypes.Bool(true, typ.Metadata()) + case "audit": + logging.Audit = iacTypes.Bool(true, typ.Metadata()) + case "authenticator": + logging.Authenticator = iacTypes.Bool(true, typ.Metadata()) + case "controllerManager": + logging.ControllerManager = iacTypes.Bool(true, typ.Metadata()) + case "scheduler": + logging.Scheduler = iacTypes.Bool(true, typ.Metadata()) + } + + } + + return logging +} diff --git a/pkg/iac/adapters/cloudformation/aws/eks/eks.go b/pkg/iac/adapters/cloudformation/aws/eks/eks.go new file mode 100644 index 000000000000..fe6933c19a8e --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/eks/eks.go @@ -0,0 +1,13 @@ +package eks + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/eks" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an EKS instance +func Adapt(cfFile parser.FileContext) eks.EKS { + return eks.EKS{ + Clusters: getClusters(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/eks/eks_test.go b/pkg/iac/adapters/cloudformation/aws/eks/eks_test.go new file mode 100644 index 000000000000..87051521cc2f --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/eks/eks_test.go @@ -0,0 +1,83 @@ +package eks + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/eks" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected eks.EKS + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + EKSCluster: + Type: AWS::EKS::Cluster + Properties: + Logging: + ClusterLogging: + EnabledTypes: + - Type: api + - Type: audit + - Type: authenticator + - Type: controllerManager + - Type: scheduler + EncryptionConfig: + - Provider: + KeyArn: alias/mykey + Resources: [secrets] + ResourcesVpcConfig: + EndpointPublicAccess: True + PublicAccessCidrs: + - 0.0.0.0/0 +`, + expected: eks.EKS{ + Clusters: []eks.Cluster{ + { + Logging: eks.Logging{ + API: types.BoolTest(true), + Audit: types.BoolTest(true), + Authenticator: types.BoolTest(true), + ControllerManager: types.BoolTest(true), + Scheduler: types.BoolTest(true), + }, + Encryption: eks.Encryption{ + KMSKeyID: types.StringTest("alias/mykey"), + Secrets: types.BoolTest(true), + }, + PublicAccessEnabled: types.BoolTest(true), + PublicAccessCIDRs: []types.StringValue{ + types.StringTest("0.0.0.0/0"), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + EKSCluster: + Type: AWS::EKS::Cluster + `, + expected: eks.EKS{ + Clusters: []eks.Cluster{{ + PublicAccessEnabled: types.BoolTest(true), + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/elasticache/cluster.go b/pkg/iac/adapters/cloudformation/aws/elasticache/cluster.go new file mode 100644 index 000000000000..bdbefbbbb6c2 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elasticache/cluster.go @@ -0,0 +1,24 @@ +package elasticache + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticache" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getClusterGroups(ctx parser.FileContext) (clusters []elasticache.Cluster) { + + clusterResources := ctx.GetResourcesByType("AWS::ElastiCache::CacheCluster") + + for _, r := range clusterResources { + cluster := elasticache.Cluster{ + Metadata: r.Metadata(), + Engine: r.GetStringProperty("Engine"), + NodeType: r.GetStringProperty("CacheNodeType"), + SnapshotRetentionLimit: r.GetIntProperty("SnapshotRetentionLimit"), + } + + clusters = append(clusters, cluster) + } + + return clusters +} diff --git a/pkg/iac/adapters/cloudformation/aws/elasticache/elasticache.go b/pkg/iac/adapters/cloudformation/aws/elasticache/elasticache.go new file mode 100644 index 000000000000..1d69000ff9ea --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elasticache/elasticache.go @@ -0,0 +1,15 @@ +package elasticache + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticache" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an ElasticCache instance +func Adapt(cfFile parser.FileContext) elasticache.ElastiCache { + return elasticache.ElastiCache{ + Clusters: getClusterGroups(cfFile), + ReplicationGroups: getReplicationGroups(cfFile), + SecurityGroups: getSecurityGroups(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/elasticache/elasticache_test.go b/pkg/iac/adapters/cloudformation/aws/elasticache/elasticache_test.go new file mode 100644 index 000000000000..e7e3d018b14c --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elasticache/elasticache_test.go @@ -0,0 +1,82 @@ +package elasticache + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticache" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected elasticache.ElastiCache + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + ElasticacheCluster: + Type: 'AWS::ElastiCache::CacheCluster' + Properties: + Engine: memcached + CacheNodeType: cache.t2.micro + SnapshotRetentionLimit: 5 + myReplicationGroup: + Type: 'AWS::ElastiCache::ReplicationGroup' + Properties: + TransitEncryptionEnabled: true + AtRestEncryptionEnabled: true + mySecGroup: + Type: AWS::ElastiCache::SecurityGroup + Properties: + Description: test +`, + expected: elasticache.ElastiCache{ + Clusters: []elasticache.Cluster{ + { + Engine: types.StringTest("memcached"), + NodeType: types.StringTest("cache.t2.micro"), + SnapshotRetentionLimit: types.IntTest(5), + }, + }, + ReplicationGroups: []elasticache.ReplicationGroup{ + { + TransitEncryptionEnabled: types.BoolTest(true), + AtRestEncryptionEnabled: types.BoolTest(true), + }, + }, + SecurityGroups: []elasticache.SecurityGroup{ + { + Description: types.StringTest("test"), + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + ElasticacheCluster: + Type: 'AWS::ElastiCache::CacheCluster' + myReplicationGroup: + Type: 'AWS::ElastiCache::ReplicationGroup' + mySecGroup: + Type: AWS::ElastiCache::SecurityGroup + `, + expected: elasticache.ElastiCache{ + Clusters: []elasticache.Cluster{{}}, + ReplicationGroups: []elasticache.ReplicationGroup{{}}, + SecurityGroups: []elasticache.SecurityGroup{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/elasticache/replication_group.go b/pkg/iac/adapters/cloudformation/aws/elasticache/replication_group.go new file mode 100644 index 000000000000..95b6a7916370 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elasticache/replication_group.go @@ -0,0 +1,23 @@ +package elasticache + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticache" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getReplicationGroups(ctx parser.FileContext) (replicationGroups []elasticache.ReplicationGroup) { + + replicationGroupResources := ctx.GetResourcesByType("AWS::ElastiCache::ReplicationGroup") + + for _, r := range replicationGroupResources { + replicationGroup := elasticache.ReplicationGroup{ + Metadata: r.Metadata(), + TransitEncryptionEnabled: r.GetBoolProperty("TransitEncryptionEnabled"), + AtRestEncryptionEnabled: r.GetBoolProperty("AtRestEncryptionEnabled"), + } + + replicationGroups = append(replicationGroups, replicationGroup) + } + + return replicationGroups +} diff --git a/pkg/iac/adapters/cloudformation/aws/elasticache/security_group.go b/pkg/iac/adapters/cloudformation/aws/elasticache/security_group.go new file mode 100644 index 000000000000..8be6e68f6903 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elasticache/security_group.go @@ -0,0 +1,22 @@ +package elasticache + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticache" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getSecurityGroups(ctx parser.FileContext) (securityGroups []elasticache.SecurityGroup) { + + sgResources := ctx.GetResourcesByType("AWS::ElastiCache::SecurityGroup") + + for _, r := range sgResources { + + sg := elasticache.SecurityGroup{ + Metadata: r.Metadata(), + Description: r.GetStringProperty("Description"), + } + securityGroups = append(securityGroups, sg) + } + + return securityGroups +} diff --git a/pkg/iac/adapters/cloudformation/aws/elasticsearch/domain.go b/pkg/iac/adapters/cloudformation/aws/elasticsearch/domain.go new file mode 100644 index 000000000000..2ca77a5d7448 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elasticsearch/domain.go @@ -0,0 +1,88 @@ +package elasticsearch + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticsearch" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getDomains(ctx parser.FileContext) (domains []elasticsearch.Domain) { + + domainResources := ctx.GetResourcesByType("AWS::Elasticsearch::Domain", "AWS::OpenSearchService::Domain") + + for _, r := range domainResources { + + domain := elasticsearch.Domain{ + Metadata: r.Metadata(), + DomainName: r.GetStringProperty("DomainName"), + AccessPolicies: r.GetStringProperty("AccessPolicies"), + VpcId: iacTypes.String("", r.Metadata()), + LogPublishing: elasticsearch.LogPublishing{ + Metadata: r.Metadata(), + AuditEnabled: iacTypes.BoolDefault(false, r.Metadata()), + CloudWatchLogGroupArn: iacTypes.String("", r.Metadata()), + }, + TransitEncryption: elasticsearch.TransitEncryption{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + }, + AtRestEncryption: elasticsearch.AtRestEncryption{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + KmsKeyId: iacTypes.String("", r.Metadata()), + }, + Endpoint: elasticsearch.Endpoint{ + Metadata: r.Metadata(), + EnforceHTTPS: iacTypes.BoolDefault(false, r.Metadata()), + }, + ServiceSoftwareOptions: elasticsearch.ServiceSoftwareOptions{ + Metadata: r.Metadata(), + CurrentVersion: iacTypes.String("", r.Metadata()), + NewVersion: iacTypes.String("", r.Metadata()), + UpdateStatus: iacTypes.String("", r.Metadata()), + UpdateAvailable: iacTypes.Bool(false, r.Metadata()), + }, + } + + if r.Type() == "AWS::OpenSearchService::Domain" { + domain.DedicatedMasterEnabled = r.GetBoolProperty("ClusterConfig.DedicatedMasterEnabled") + } else { + domain.DedicatedMasterEnabled = r.GetBoolProperty("ElasticsearchClusterConfig.DedicatedMasterEnabled") + } + + if prop := r.GetProperty("LogPublishingOptions"); prop.IsNotNil() { + domain.LogPublishing = elasticsearch.LogPublishing{ + Metadata: prop.Metadata(), + AuditEnabled: prop.GetBoolProperty("AUDIT_LOGS.Enabled"), + CloudWatchLogGroupArn: prop.GetStringProperty("AUDIT_LOGS.CloudWatchLogsLogGroupArn"), + } + } + + if prop := r.GetProperty("NodeToNodeEncryptionOptions"); prop.IsNotNil() { + domain.TransitEncryption = elasticsearch.TransitEncryption{ + Metadata: prop.Metadata(), + Enabled: prop.GetBoolProperty("Enabled"), + } + } + + if prop := r.GetProperty("EncryptionAtRestOptions"); prop.IsNotNil() { + domain.AtRestEncryption = elasticsearch.AtRestEncryption{ + Metadata: prop.Metadata(), + Enabled: prop.GetBoolProperty("Enabled"), + KmsKeyId: prop.GetStringProperty("KmsKeyId"), + } + } + + if prop := r.GetProperty("DomainEndpointOptions"); prop.IsNotNil() { + domain.Endpoint = elasticsearch.Endpoint{ + Metadata: prop.Metadata(), + EnforceHTTPS: prop.GetBoolProperty("EnforceHTTPS"), + TLSPolicy: prop.GetStringProperty("TLSSecurityPolicy"), + } + } + + domains = append(domains, domain) + } + + return domains +} diff --git a/pkg/iac/adapters/cloudformation/aws/elasticsearch/elasticsearch.go b/pkg/iac/adapters/cloudformation/aws/elasticsearch/elasticsearch.go new file mode 100644 index 000000000000..b3e230b0435b --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elasticsearch/elasticsearch.go @@ -0,0 +1,13 @@ +package elasticsearch + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticsearch" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an ElasticSearch instance +func Adapt(cfFile parser.FileContext) elasticsearch.Elasticsearch { + return elasticsearch.Elasticsearch{ + Domains: getDomains(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/elasticsearch/elasticsearch_test.go b/pkg/iac/adapters/cloudformation/aws/elasticsearch/elasticsearch_test.go new file mode 100644 index 000000000000..afb9c3a81e22 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elasticsearch/elasticsearch_test.go @@ -0,0 +1,109 @@ +package elasticsearch + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticsearch" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected elasticsearch.Elasticsearch + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + OpenSearchServiceDomain: + Type: AWS::OpenSearchService::Domain + Properties: + DomainName: 'test' + ClusterConfig: + DedicatedMasterEnabled: true + NodeToNodeEncryptionOptions: + Enabled: true + EncryptionAtRestOptions: + Enabled: true + KmsKeyId: mykey + DomainEndpointOptions: + EnforceHTTPS: true + TLSSecurityPolicy: Policy-Min-TLS-1-0-2019-07 + AccessPolicies: + Version: '2012-10-17' + Statement: + - + Effect: 'Allow' + Principal: + AWS: 'arn:aws:iam::123456789012:user/opensearch-user' + Action: 'es:*' + Resource: 'arn:aws:es:us-east-1:846973539254:domain/test/*' + LogPublishingOptions: + AUDIT_LOGS: + CloudWatchLogsLogGroupArn: 'arn:aws:logs:us-east-1:123456789012:log-group:/aws/opensearch/domains/opensearch-application-logs' + Enabled: true +`, + expected: elasticsearch.Elasticsearch{ + Domains: []elasticsearch.Domain{ + { + DomainName: types.StringTest("test"), + DedicatedMasterEnabled: types.BoolTest(true), + LogPublishing: elasticsearch.LogPublishing{ + AuditEnabled: types.BoolTest(true), + CloudWatchLogGroupArn: types.StringTest("arn:aws:logs:us-east-1:123456789012:log-group:/aws/opensearch/domains/opensearch-application-logs"), + }, + TransitEncryption: elasticsearch.TransitEncryption{ + Enabled: types.BoolTest(true), + }, + AtRestEncryption: elasticsearch.AtRestEncryption{ + Enabled: types.BoolTest(true), + KmsKeyId: types.StringTest("mykey"), + }, + Endpoint: elasticsearch.Endpoint{ + EnforceHTTPS: types.BoolTest(true), + TLSPolicy: types.StringTest("Policy-Min-TLS-1-0-2019-07"), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + OpenSearchServiceDomain: + Type: AWS::OpenSearchService::Domain + `, + expected: elasticsearch.Elasticsearch{ + Domains: []elasticsearch.Domain{{}}, + }, + }, + { + name: "Elasticsearch", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + ElasticsearchDomain: + Type: AWS::Elasticsearch::Domain + Properties: + ElasticsearchClusterConfig: + DedicatedMasterEnabled: true + `, + expected: elasticsearch.Elasticsearch{ + Domains: []elasticsearch.Domain{ + { + DedicatedMasterEnabled: types.BoolTest(true), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/elb/adapt_test.go b/pkg/iac/adapters/cloudformation/aws/elb/adapt_test.go new file mode 100644 index 000000000000..a8764bfcca7f --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elb/adapt_test.go @@ -0,0 +1,85 @@ +package elb + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elb" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected elb.ELB + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: "2010-09-09" +Resources: + LoadBalancer: + Type: AWS::ElasticLoadBalancingV2::LoadBalancer + DependsOn: + - ALBLogsBucketPermission + Properties: + Name: "k8s-dev" + Scheme: internal + IpAddressType: ipv4 + LoadBalancerAttributes: + - Key: routing.http2.enabled + Value: "true" + - Key: deletion_protection.enabled + Value: "true" + - Key: routing.http.drop_invalid_header_fields.enabled + Value: "true" + - Key: access_logs.s3.enabled + Value: "true" + Tags: + - Key: ingress.k8s.aws/resource + Value: LoadBalancer + - Key: elbv2.k8s.aws/cluster + Value: "biomage-dev" + Type: application + Listener: + Type: AWS::ElasticLoadBalancingV2::Listener + Properties: + DefaultActions: + - Type: 'redirect' + RedirectConfig: + Port: 443 + Protocol: HTTPS + StatusCode: HTTP_302 + LoadBalancerArn: !Ref LoadBalancer + Protocol: HTTPS + SslPolicy: "ELBSecurityPolicy-TLS-1-2-2017-01" +`, + expected: elb.ELB{ + LoadBalancers: []elb.LoadBalancer{ + { + Type: types.StringTest("application"), + DropInvalidHeaderFields: types.BoolTest(true), + Internal: types.BoolTest(true), + Listeners: []elb.Listener{ + { + Protocol: types.StringTest("HTTPS"), + TLSPolicy: types.StringTest("ELBSecurityPolicy-TLS-1-2-2017-01"), + DefaultActions: []elb.Action{ + { + Type: types.StringTest("redirect"), + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/elb/elb.go b/pkg/iac/adapters/cloudformation/aws/elb/elb.go new file mode 100644 index 000000000000..2ff1d2c74777 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elb/elb.go @@ -0,0 +1,13 @@ +package elb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elb" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an ELB instance +func Adapt(cfFile parser.FileContext) elb.ELB { + return elb.ELB{ + LoadBalancers: getLoadBalancers(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/elb/loadbalancer.go b/pkg/iac/adapters/cloudformation/aws/elb/loadbalancer.go new file mode 100644 index 000000000000..002b6487ba43 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/elb/loadbalancer.go @@ -0,0 +1,81 @@ +package elb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elb" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getLoadBalancers(ctx parser.FileContext) (loadbalancers []elb.LoadBalancer) { + + loadBalanacerResources := ctx.GetResourcesByType("AWS::ElasticLoadBalancingV2::LoadBalancer") + + for _, r := range loadBalanacerResources { + lb := elb.LoadBalancer{ + Metadata: r.Metadata(), + Type: r.GetStringProperty("Type", "application"), + DropInvalidHeaderFields: checkForDropInvalidHeaders(r), + Internal: isInternal(r), + Listeners: getListeners(r, ctx), + } + loadbalancers = append(loadbalancers, lb) + } + + return loadbalancers +} + +func getListeners(lbr *parser.Resource, ctx parser.FileContext) (listeners []elb.Listener) { + + listenerResources := ctx.GetResourcesByType("AWS::ElasticLoadBalancingV2::Listener") + + for _, r := range listenerResources { + if r.GetStringProperty("LoadBalancerArn").Value() == lbr.ID() { + listener := elb.Listener{ + Metadata: r.Metadata(), + Protocol: r.GetStringProperty("Protocol", "HTTP"), + TLSPolicy: r.GetStringProperty("SslPolicy", ""), + DefaultActions: getDefaultListenerActions(r), + } + + listeners = append(listeners, listener) + } + } + return listeners +} + +func getDefaultListenerActions(r *parser.Resource) (actions []elb.Action) { + defaultActionsProp := r.GetProperty("DefaultActions") + if defaultActionsProp.IsNotList() { + return actions + } + for _, action := range defaultActionsProp.AsList() { + actions = append(actions, elb.Action{ + Metadata: action.Metadata(), + Type: action.GetProperty("Type").AsStringValue(), + }) + } + return actions +} + +func isInternal(r *parser.Resource) types.BoolValue { + schemeProp := r.GetProperty("Scheme") + if schemeProp.IsNotString() { + return r.BoolDefault(false) + } + return types.Bool(schemeProp.EqualTo("internal", parser.IgnoreCase), schemeProp.Metadata()) +} + +func checkForDropInvalidHeaders(r *parser.Resource) types.BoolValue { + attributesProp := r.GetProperty("LoadBalancerAttributes") + if attributesProp.IsNotList() { + return types.BoolDefault(false, r.Metadata()) + } + + for _, attr := range attributesProp.AsList() { + if attr.GetStringProperty("Key").Value() == "routing.http.drop_invalid_header_fields.enabled" { + return attr.GetBoolProperty("Value") + } + } + + return r.BoolDefault(false) +} diff --git a/pkg/iac/adapters/cloudformation/aws/iam/iam.go b/pkg/iac/adapters/cloudformation/aws/iam/iam.go new file mode 100644 index 000000000000..d3aab1646534 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/iam/iam.go @@ -0,0 +1,27 @@ +package iam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +// Adapt adapts an IAM instance +func Adapt(cfFile parser.FileContext) iam.IAM { + return iam.IAM{ + PasswordPolicy: iam.PasswordPolicy{ + Metadata: iacTypes.NewUnmanagedMetadata(), + ReusePreventionCount: iacTypes.IntDefault(0, iacTypes.NewUnmanagedMetadata()), + RequireLowercase: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + RequireUppercase: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + RequireNumbers: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + RequireSymbols: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + MaxAgeDays: iacTypes.IntDefault(0, iacTypes.NewUnmanagedMetadata()), + MinimumLength: iacTypes.IntDefault(0, iacTypes.NewUnmanagedMetadata()), + }, + Policies: getPolicies(cfFile), + Groups: getGroups(cfFile), + Users: getUsers(cfFile), + Roles: getRoles(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/iam/iam_test.go b/pkg/iac/adapters/cloudformation/aws/iam/iam_test.go new file mode 100644 index 000000000000..7f68e2c6aef0 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/iam/iam_test.go @@ -0,0 +1,189 @@ +package iam + +import ( + "testing" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected iam.IAM + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + myIAMPolicy: + Type: 'AWS::IAM::Policy' + Properties: + PolicyName: TestPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - 'cloudformation:Describe*' + Resource: '*' + Groups: + - !Ref MyGroup + Users: + - !Ref PublishUser + Roles: + - !Ref MyRole + MyGroup: + Type: AWS::IAM::Group + Properties: + GroupName: TestGroup + Policies: + - PolicyName: TestGroupPolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Resource: arn:*:cloudfront::*:distribution/* + Action: + - cloudfront:CreateDistribution + MyUser: + Type: AWS::IAM::User + Properties: + UserName: TestUser + Policies: + - PolicyName: TestUserPolicy + PolicyDocument: + Statement: + - Action: 's3:*' + Effect: Allow + Resource: + - 'arn:aws:s3:::testbucket' + MyRole: + Type: 'AWS::IAM::Role' + Properties: + RoleName: TestRole + Policies: + - PolicyName: TestRolePolicy + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - 'sts:AssumeRole' + AccessKey: + Type: AWS::IAM::AccessKey + Properties: + UserName: !Ref MyUser + Status: Active +`, + expected: iam.IAM{ + Policies: []iam.Policy{ + { + Name: types.StringTest("TestPolicy"), + Document: func() iam.Document { + return iam.Document{ + Parsed: iamgo.NewPolicyBuilder(). + WithVersion("2012-10-17"). + WithStatement( + iamgo.NewStatementBuilder(). + WithEffect("Allow"). + WithActions([]string{"cloudformation:Describe*"}). + WithResources([]string{"*"}). + Build(), + ). + Build(), + } + }(), + }, + }, + Users: []iam.User{ + { + Name: types.StringTest("TestUser"), + Policies: []iam.Policy{ + { + Name: types.StringTest("TestUserPolicy"), + Document: func() iam.Document { + return iam.Document{ + Parsed: iamgo.NewPolicyBuilder(). + WithStatement( + iamgo.NewStatementBuilder(). + WithEffect("Allow"). + WithActions([]string{"s3:*"}). + WithResources([]string{"arn:aws:s3:::testbucket"}). + Build(), + ). + Build(), + } + }(), + }, + }, + }, + }, + Groups: []iam.Group{ + { + Name: types.StringTest("TestGroup"), + Policies: []iam.Policy{ + { + Name: types.StringTest("TestGroupPolicy"), + Document: func() iam.Document { + return iam.Document{ + Parsed: iamgo.NewPolicyBuilder(). + WithVersion("2012-10-17"). + WithStatement( + iamgo.NewStatementBuilder(). + WithEffect("Allow"). + WithActions([]string{"cloudfront:CreateDistribution"}). + WithResources([]string{"arn:*:cloudfront::*:distribution/*"}). + Build(), + ). + Build(), + } + }(), + }, + }, + }, + }, + Roles: []iam.Role{ + { + Name: types.StringTest("TestRole"), + Policies: []iam.Policy{ + { + Name: types.StringTest("TestRolePolicy"), + Document: func() iam.Document { + return iam.Document{ + Parsed: iamgo.NewPolicyBuilder(). + WithVersion("2012-10-17"). + WithStatement( + iamgo.NewStatementBuilder(). + WithEffect("Allow"). + WithActions([]string{"sts:AssumeRole"}). + Build(), + ). + Build(), + } + }(), + }, + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + + `, + expected: iam.IAM{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/iam/policy.go b/pkg/iac/adapters/cloudformation/aws/iam/policy.go new file mode 100644 index 000000000000..d787dac2539b --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/iam/policy.go @@ -0,0 +1,126 @@ +package iam + +import ( + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getPolicies(ctx parser.FileContext) (policies []iam.Policy) { + for _, policyResource := range ctx.GetResourcesByType("AWS::IAM::Policy") { + + policy := iam.Policy{ + Metadata: policyResource.Metadata(), + Name: policyResource.GetStringProperty("PolicyName"), + Document: iam.Document{ + Metadata: policyResource.Metadata(), + Parsed: iamgo.Document{}, + }, + Builtin: iacTypes.Bool(false, policyResource.Metadata()), + } + + if policyProp := policyResource.GetProperty("PolicyDocument"); policyProp.IsNotNil() { + doc, err := iamgo.Parse(policyProp.GetJsonBytes()) + if err != nil { + continue + } + policy.Document.Parsed = *doc + } + + policies = append(policies, policy) + } + return policies +} + +func getRoles(ctx parser.FileContext) (roles []iam.Role) { + for _, roleResource := range ctx.GetResourcesByType("AWS::IAM::Role") { + policyProp := roleResource.GetProperty("Policies") + roleName := roleResource.GetStringProperty("RoleName") + + roles = append(roles, iam.Role{ + Metadata: roleResource.Metadata(), + Name: roleName, + Policies: getPoliciesDocs(policyProp), + }) + } + return roles +} + +func getUsers(ctx parser.FileContext) (users []iam.User) { + for _, userResource := range ctx.GetResourcesByType("AWS::IAM::User") { + policyProp := userResource.GetProperty("Policies") + userName := userResource.GetStringProperty("UserName") + + users = append(users, iam.User{ + Metadata: userResource.Metadata(), + Name: userName, + LastAccess: iacTypes.TimeUnresolvable(userResource.Metadata()), + Policies: getPoliciesDocs(policyProp), + AccessKeys: getAccessKeys(ctx, userName.Value()), + }) + } + return users +} + +func getAccessKeys(ctx parser.FileContext, username string) (accessKeys []iam.AccessKey) { + // TODO: also search for a key by the logical id of the resource + for _, keyResource := range ctx.GetResourcesByType("AWS::IAM::AccessKey") { + keyUsername := keyResource.GetStringProperty("UserName") + if !keyUsername.EqualTo(username) { + continue + } + active := iacTypes.BoolDefault(false, keyResource.Metadata()) + if statusProp := keyResource.GetProperty("Status"); statusProp.IsString() { + active = iacTypes.Bool(statusProp.AsString() == "Active", statusProp.Metadata()) + } + + accessKeys = append(accessKeys, iam.AccessKey{ + Metadata: keyResource.Metadata(), + AccessKeyId: iacTypes.StringUnresolvable(keyResource.Metadata()), + CreationDate: iacTypes.TimeUnresolvable(keyResource.Metadata()), + LastAccess: iacTypes.TimeUnresolvable(keyResource.Metadata()), + Active: active, + }) + } + return accessKeys +} + +func getGroups(ctx parser.FileContext) (groups []iam.Group) { + for _, groupResource := range ctx.GetResourcesByType("AWS::IAM::Group") { + policyProp := groupResource.GetProperty("Policies") + groupName := groupResource.GetStringProperty("GroupName") + + groups = append(groups, iam.Group{ + Metadata: groupResource.Metadata(), + Name: groupName, + Policies: getPoliciesDocs(policyProp), + }) + } + return groups +} + +func getPoliciesDocs(policiesProp *parser.Property) []iam.Policy { + var policies []iam.Policy + + for _, policy := range policiesProp.AsList() { + policyProp := policy.GetProperty("PolicyDocument") + policyName := policy.GetStringProperty("PolicyName") + + doc, err := iamgo.Parse(policyProp.GetJsonBytes()) + if err != nil { + continue + } + + policies = append(policies, iam.Policy{ + Metadata: policyProp.Metadata(), + Name: policyName, + Document: iam.Document{ + Metadata: policyProp.Metadata(), + Parsed: *doc, + }, + Builtin: iacTypes.Bool(false, policyProp.Metadata()), + }) + } + return policies +} diff --git a/pkg/iac/adapters/cloudformation/aws/kinesis/kinesis.go b/pkg/iac/adapters/cloudformation/aws/kinesis/kinesis.go new file mode 100644 index 000000000000..c48ad26046bf --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/kinesis/kinesis.go @@ -0,0 +1,13 @@ +package kinesis + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/kinesis" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a Kinesis instance +func Adapt(cfFile parser.FileContext) kinesis.Kinesis { + return kinesis.Kinesis{ + Streams: getStreams(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/kinesis/kinesis_test.go b/pkg/iac/adapters/cloudformation/aws/kinesis/kinesis_test.go new file mode 100644 index 000000000000..ce38afadf806 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/kinesis/kinesis_test.go @@ -0,0 +1,57 @@ +package kinesis + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/kinesis" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected kinesis.Kinesis + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + MyStream: + Type: 'AWS::Kinesis::Stream' + Properties: + StreamEncryption: + EncryptionType: KMS + KeyId: key +`, + expected: kinesis.Kinesis{ + Streams: []kinesis.Stream{ + { + Encryption: kinesis.Encryption{ + Type: types.StringTest("KMS"), + KMSKeyID: types.StringTest("key"), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyStream: + Type: 'AWS::Kinesis::Stream' + `, + expected: kinesis.Kinesis{ + Streams: []kinesis.Stream{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/kinesis/stream.go b/pkg/iac/adapters/cloudformation/aws/kinesis/stream.go new file mode 100644 index 000000000000..6c10ec6134c0 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/kinesis/stream.go @@ -0,0 +1,30 @@ +package kinesis + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/kinesis" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getStreams(ctx parser.FileContext) (streams []kinesis.Stream) { + + streamResources := ctx.GetResourcesByType("AWS::Kinesis::Stream") + + for _, r := range streamResources { + + stream := kinesis.Stream{ + Metadata: r.Metadata(), + } + + if prop := r.GetProperty("StreamEncryption"); prop.IsNotNil() { + stream.Encryption = kinesis.Encryption{ + Metadata: prop.Metadata(), + Type: prop.GetStringProperty("EncryptionType", "KMS"), + KMSKeyID: prop.GetStringProperty("KeyId"), + } + } + + streams = append(streams, stream) + } + + return streams +} diff --git a/pkg/iac/adapters/cloudformation/aws/lambda/function.go b/pkg/iac/adapters/cloudformation/aws/lambda/function.go new file mode 100644 index 000000000000..f91a565d193a --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/lambda/function.go @@ -0,0 +1,48 @@ +package lambda + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/lambda" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getFunctions(ctx parser.FileContext) (functions []lambda.Function) { + + functionResources := ctx.GetResourcesByType("AWS::Lambda::Function") + + for _, r := range functionResources { + + function := lambda.Function{ + Metadata: r.Metadata(), + Permissions: getPermissions(r, ctx), + } + + if prop := r.GetProperty("TracingConfig"); prop.IsNotNil() { + function.Tracing = lambda.Tracing{ + Metadata: prop.Metadata(), + Mode: prop.GetStringProperty("Mode"), + } + } + + functions = append(functions, function) + } + + return functions +} + +func getPermissions(funcR *parser.Resource, ctx parser.FileContext) (perms []lambda.Permission) { + + permissionResources := ctx.GetResourcesByType("AWS::Lambda::Permission") + + for _, r := range permissionResources { + if prop := r.GetStringProperty("FunctionName"); prop.EqualTo(funcR.ID()) { + perm := lambda.Permission{ + Metadata: r.Metadata(), + Principal: r.GetStringProperty("Principal"), + SourceARN: r.GetStringProperty("SourceArn"), + } + perms = append(perms, perm) + } + } + + return perms +} diff --git a/pkg/iac/adapters/cloudformation/aws/lambda/lambda.go b/pkg/iac/adapters/cloudformation/aws/lambda/lambda.go new file mode 100644 index 000000000000..f89710d47e1e --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/lambda/lambda.go @@ -0,0 +1,13 @@ +package lambda + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/lambda" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a lambda instance +func Adapt(cfFile parser.FileContext) lambda.Lambda { + return lambda.Lambda{ + Functions: getFunctions(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/lambda/lambda_test.go b/pkg/iac/adapters/cloudformation/aws/lambda/lambda_test.go new file mode 100644 index 000000000000..4262181f89ee --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/lambda/lambda_test.go @@ -0,0 +1,76 @@ +package lambda + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/lambda" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected lambda.Lambda + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + lambdaFunction: + Type: AWS::Lambda::Function + Properties: + TracingConfig: + Mode: Active + permission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !Ref lambdaFunction + Action: lambda:InvokeFunction + Principal: s3.amazonaws.com + SourceArn: arn +`, + expected: lambda.Lambda{ + Functions: []lambda.Function{ + { + Tracing: lambda.Tracing{ + Mode: types.StringTest("Active"), + }, + Permissions: []lambda.Permission{ + { + Principal: types.StringTest("s3.amazonaws.com"), + SourceARN: types.StringTest("arn"), + }, + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + lambdaFunction: + Type: AWS::Lambda::Function + permission: + Type: AWS::Lambda::Permission + Properties: + FunctionName: !Ref lambdaFunction + `, + expected: lambda.Lambda{ + Functions: []lambda.Function{ + { + Permissions: []lambda.Permission{{}}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/mq/broker.go b/pkg/iac/adapters/cloudformation/aws/mq/broker.go new file mode 100644 index 000000000000..9e52023c11da --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/mq/broker.go @@ -0,0 +1,33 @@ +package mq + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/mq" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getBrokers(ctx parser.FileContext) (brokers []mq.Broker) { + for _, r := range ctx.GetResourcesByType("AWS::AmazonMQ::Broker") { + + broker := mq.Broker{ + Metadata: r.Metadata(), + PublicAccess: r.GetBoolProperty("PubliclyAccessible"), + Logging: mq.Logging{ + Metadata: r.Metadata(), + General: types.BoolDefault(false, r.Metadata()), + Audit: types.BoolDefault(false, r.Metadata()), + }, + } + + if prop := r.GetProperty("Logs"); prop.IsNotNil() { + broker.Logging = mq.Logging{ + Metadata: prop.Metadata(), + General: prop.GetBoolProperty("General"), + Audit: prop.GetBoolProperty("Audit"), + } + } + + brokers = append(brokers, broker) + } + return brokers +} diff --git a/pkg/iac/adapters/cloudformation/aws/mq/mq.go b/pkg/iac/adapters/cloudformation/aws/mq/mq.go new file mode 100644 index 000000000000..744e86f69a11 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/mq/mq.go @@ -0,0 +1,13 @@ +package mq + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/mq" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an MQ instance +func Adapt(cfFile parser.FileContext) mq.MQ { + return mq.MQ{ + Brokers: getBrokers(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/mq/mq_test.go b/pkg/iac/adapters/cloudformation/aws/mq/mq_test.go new file mode 100644 index 000000000000..b4f1d5048898 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/mq/mq_test.go @@ -0,0 +1,59 @@ +package mq + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/mq" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected mq.MQ + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + BasicBroker: + Type: "AWS::AmazonMQ::Broker" + Properties: + PubliclyAccessible: true + Logs: + Audit: true + General: true +`, + expected: mq.MQ{ + Brokers: []mq.Broker{ + { + PublicAccess: types.BoolTest(true), + Logging: mq.Logging{ + Audit: types.BoolTest(true), + General: types.BoolTest(true), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + BasicBroker: + Type: "AWS::AmazonMQ::Broker" + `, + expected: mq.MQ{ + Brokers: []mq.Broker{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/msk/cluster.go b/pkg/iac/adapters/cloudformation/aws/msk/cluster.go new file mode 100644 index 000000000000..c55d5745e22b --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/msk/cluster.go @@ -0,0 +1,80 @@ +package msk + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/msk" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getClusters(ctx parser.FileContext) (clusters []msk.Cluster) { + for _, r := range ctx.GetResourcesByType("AWS::MSK::Cluster") { + + cluster := msk.Cluster{ + Metadata: r.Metadata(), + EncryptionInTransit: msk.EncryptionInTransit{ + Metadata: r.Metadata(), + ClientBroker: iacTypes.StringDefault("TLS", r.Metadata()), + }, + EncryptionAtRest: msk.EncryptionAtRest{ + Metadata: r.Metadata(), + KMSKeyARN: iacTypes.StringDefault("", r.Metadata()), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + }, + Logging: msk.Logging{ + Metadata: r.Metadata(), + Broker: msk.BrokerLogging{ + Metadata: r.Metadata(), + S3: msk.S3Logging{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + }, + Cloudwatch: msk.CloudwatchLogging{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + }, + Firehose: msk.FirehoseLogging{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + }, + }, + }, + } + + if encProp := r.GetProperty("EncryptionInfo.EncryptionInTransit"); encProp.IsNotNil() { + cluster.EncryptionInTransit = msk.EncryptionInTransit{ + Metadata: encProp.Metadata(), + ClientBroker: encProp.GetStringProperty("ClientBroker", "TLS"), + } + } + + if encAtRestProp := r.GetProperty("EncryptionInfo.EncryptionAtRest"); encAtRestProp.IsNotNil() { + cluster.EncryptionAtRest = msk.EncryptionAtRest{ + Metadata: encAtRestProp.Metadata(), + KMSKeyARN: encAtRestProp.GetStringProperty("DataVolumeKMSKeyId", ""), + Enabled: iacTypes.BoolDefault(true, encAtRestProp.Metadata()), + } + } + + if loggingProp := r.GetProperty("LoggingInfo"); loggingProp.IsNotNil() { + cluster.Logging.Metadata = loggingProp.Metadata() + if brokerLoggingProp := loggingProp.GetProperty("BrokerLogs"); brokerLoggingProp.IsNotNil() { + cluster.Logging.Broker.Metadata = brokerLoggingProp.Metadata() + if s3Prop := brokerLoggingProp.GetProperty("S3"); s3Prop.IsNotNil() { + cluster.Logging.Broker.S3.Metadata = s3Prop.Metadata() + cluster.Logging.Broker.S3.Enabled = s3Prop.GetBoolProperty("Enabled", false) + } + if cwProp := brokerLoggingProp.GetProperty("CloudWatchLogs"); cwProp.IsNotNil() { + cluster.Logging.Broker.Cloudwatch.Metadata = cwProp.Metadata() + cluster.Logging.Broker.Cloudwatch.Enabled = cwProp.GetBoolProperty("Enabled", false) + } + if fhProp := brokerLoggingProp.GetProperty("Firehose"); fhProp.IsNotNil() { + cluster.Logging.Broker.Firehose.Metadata = fhProp.Metadata() + cluster.Logging.Broker.Firehose.Enabled = fhProp.GetBoolProperty("Enabled", false) + } + } + } + + clusters = append(clusters, cluster) + } + return clusters +} diff --git a/pkg/iac/adapters/cloudformation/aws/msk/msk.go b/pkg/iac/adapters/cloudformation/aws/msk/msk.go new file mode 100644 index 000000000000..76d7964e17a7 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/msk/msk.go @@ -0,0 +1,13 @@ +package msk + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/msk" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an MSK instance +func Adapt(cfFile parser.FileContext) msk.MSK { + return msk.MSK{ + Clusters: getClusters(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/msk/msk_test.go b/pkg/iac/adapters/cloudformation/aws/msk/msk_test.go new file mode 100644 index 000000000000..2cc5a6cd0945 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/msk/msk_test.go @@ -0,0 +1,87 @@ +package msk + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/msk" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected msk.MSK + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + cluster: + Type: AWS::MSK::Cluster + Properties: + EncryptionInfo: + EncryptionInTransit: + ClientBroker: 'PLAINTEXT' + EncryptionAtRest: + DataVolumeKMSKeyId: key + LoggingInfo: + BrokerLogs: + S3: + Enabled: true + CloudWatchLogs: + Enabled: true + Firehose: + Enabled: true +`, + expected: msk.MSK{ + Clusters: []msk.Cluster{ + { + EncryptionInTransit: msk.EncryptionInTransit{ + ClientBroker: types.StringTest("PLAINTEXT"), + }, + EncryptionAtRest: msk.EncryptionAtRest{ + KMSKeyARN: types.StringTest("key"), + Enabled: types.BoolTest(true), + }, + Logging: msk.Logging{ + Broker: msk.BrokerLogging{ + S3: msk.S3Logging{ + Enabled: types.BoolTest(true), + }, + Firehose: msk.FirehoseLogging{ + Enabled: types.BoolTest(true), + }, + Cloudwatch: msk.CloudwatchLogging{ + Enabled: types.BoolTest(true), + }, + }, + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + cluster: + Type: AWS::MSK::Cluster + `, + expected: msk.MSK{ + Clusters: []msk.Cluster{{ + EncryptionInTransit: msk.EncryptionInTransit{ + ClientBroker: types.StringTest("TLS"), + }, + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/neptune/cluster.go b/pkg/iac/adapters/cloudformation/aws/neptune/cluster.go new file mode 100644 index 000000000000..33012cbd2236 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/neptune/cluster.go @@ -0,0 +1,34 @@ +package neptune + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/neptune" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getClusters(ctx parser.FileContext) (clusters []neptune.Cluster) { + for _, r := range ctx.GetResourcesByType("AWS::Neptune::DBCluster") { + + cluster := neptune.Cluster{ + Metadata: r.Metadata(), + Logging: neptune.Logging{ + Metadata: r.Metadata(), + Audit: getAuditLog(r), + }, + StorageEncrypted: r.GetBoolProperty("StorageEncrypted"), + KMSKeyID: r.GetStringProperty("KmsKeyId"), + } + clusters = append(clusters, cluster) + } + return clusters +} + +func getAuditLog(r *parser.Resource) types.BoolValue { + if logsProp := r.GetProperty("EnableCloudwatchLogsExports"); logsProp.IsList() { + if logsProp.Contains("audit") { + return types.Bool(true, logsProp.Metadata()) + } + } + + return types.BoolDefault(false, r.Metadata()) +} diff --git a/pkg/iac/adapters/cloudformation/aws/neptune/neptune.go b/pkg/iac/adapters/cloudformation/aws/neptune/neptune.go new file mode 100644 index 000000000000..2d63e6f45f69 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/neptune/neptune.go @@ -0,0 +1,13 @@ +package neptune + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/neptune" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a Neptune instance +func Adapt(cfFile parser.FileContext) neptune.Neptune { + return neptune.Neptune{ + Clusters: getClusters(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/neptune/neptune_test.go b/pkg/iac/adapters/cloudformation/aws/neptune/neptune_test.go new file mode 100644 index 000000000000..8e63a481ff2e --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/neptune/neptune_test.go @@ -0,0 +1,59 @@ +package neptune + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/neptune" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected neptune.Neptune + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + cluster: + Type: AWS::Neptune::DBCluster + Properties: + StorageEncrypted: true + KmsKeyId: key + EnableCloudwatchLogsExports: + - audit +`, + expected: neptune.Neptune{ + Clusters: []neptune.Cluster{ + { + StorageEncrypted: types.BoolTest(true), + KMSKeyID: types.StringTest("key"), + Logging: neptune.Logging{ + Audit: types.BoolTest(true), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + cluster: + Type: AWS::Neptune::DBCluster + `, + expected: neptune.Neptune{ + Clusters: []neptune.Cluster{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/rds/adapt_test.go b/pkg/iac/adapters/cloudformation/aws/rds/adapt_test.go new file mode 100644 index 000000000000..4875395c8506 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/rds/adapt_test.go @@ -0,0 +1,175 @@ +package rds + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/rds" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected rds.RDS + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + RDSCluster: + Type: 'AWS::RDS::DBCluster' + Properties: + DBClusterIdentifier: my-cluster1 + Engine: aurora-postgresql + StorageEncrypted: true + KmsKeyId: "your-kms-key-id" + PerformanceInsightsEnabled: true + PerformanceInsightsKmsKeyId: "test-kms-key-id" + PublicAccess: true + DeletionProtection: true + BackupRetentionPeriod: 2 + RDSDBInstance1: + Type: 'AWS::RDS::DBInstance' + Properties: + Engine: aurora-mysql + EngineVersion: "5.7.12" + DBInstanceIdentifier: test + DBClusterIdentifier: + Ref: RDSCluster + PubliclyAccessible: 'false' + DBInstanceClass: db.r3.xlarge + StorageEncrypted: true + KmsKeyId: "your-kms-key-id" + EnablePerformanceInsights: true + PerformanceInsightsKMSKeyId: "test-kms-key-id2" + MultiAZ: true + AutoMinorVersionUpgrade: true + DBInstanceArn: "arn:aws:rds:us-east-2:123456789012:db:my-mysql-instance-1" + EnableIAMDatabaseAuthentication: true + EnableCloudwatchLogsExports: + - "error" + - "general" + DBParameterGroupName: "testgroup" + Tags: + - Key: "keyname1" + Value: "value1" + - Key: "keyname2" + Value: "value2" + RDSDBParameterGroup: + Type: 'AWS::RDS::DBParameterGroup' + Properties: + Description: "CloudFormation Sample MySQL Parameter Group" + DBParameterGroupName: "testgroup" + Parameters: + sql_mode: IGNORE_SPACE + DbSecurityByEC2SecurityGroup: + Type: AWS::RDS::DBSecurityGroup + Properties: + GroupDescription: "Ingress for Amazon EC2 security group" +`, + expected: rds.RDS{ + Classic: rds.Classic{ + DBSecurityGroups: []rds.DBSecurityGroup{{}}, + }, + ParameterGroups: []rds.ParameterGroups{ + { + DBParameterGroupName: types.StringTest("testgroup"), + }, + }, + Clusters: []rds.Cluster{ + { + BackupRetentionPeriodDays: types.IntTest(2), + Engine: types.StringTest("aurora-postgresql"), + Encryption: rds.Encryption{ + EncryptStorage: types.BoolTest(true), + KMSKeyID: types.StringTest("your-kms-key-id"), + }, + PerformanceInsights: rds.PerformanceInsights{ + Enabled: types.BoolTest(true), + KMSKeyID: types.StringTest("test-kms-key-id"), + }, + PublicAccess: types.BoolTest(false), + DeletionProtection: types.BoolTest(true), + Instances: []rds.ClusterInstance{ + { + Instance: rds.Instance{ + StorageEncrypted: types.BoolTest(true), + Encryption: rds.Encryption{ + EncryptStorage: types.BoolTest(true), + KMSKeyID: types.StringTest("your-kms-key-id"), + }, + DBInstanceIdentifier: types.StringTest("test"), + PubliclyAccessible: types.BoolTest(false), + PublicAccess: types.BoolTest(false), + BackupRetentionPeriodDays: types.IntTest(1), + Engine: types.StringTest("aurora-mysql"), + EngineVersion: types.StringTest("5.7.12"), + MultiAZ: types.BoolTest(true), + AutoMinorVersionUpgrade: types.BoolTest(true), + DBInstanceArn: types.StringTest("arn:aws:rds:us-east-2:123456789012:db:my-mysql-instance-1"), + IAMAuthEnabled: types.BoolTest(true), + PerformanceInsights: rds.PerformanceInsights{ + Enabled: types.BoolTest(true), + KMSKeyID: types.StringTest("test-kms-key-id2"), + }, + EnabledCloudwatchLogsExports: []types.StringValue{ + types.StringTest("error"), + types.StringTest("general"), + }, + DBParameterGroups: []rds.DBParameterGroupsList{ + { + DBParameterGroupName: types.StringTest("testgroup"), + }, + }, + TagList: []rds.TagList{ + {}, + {}, + }, + }, + ClusterIdentifier: types.StringTest("RDSCluster"), + }, + }, + }, + }, + }, + }, + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + RDSCluster: + Type: 'AWS::RDS::DBCluster' + RDSDBInstance1: + Type: 'AWS::RDS::DBInstance' + RDSDBParameterGroup: + Type: 'AWS::RDS::DBParameterGroup' + DbSecurityByEC2SecurityGroup: + Type: AWS::RDS::DBSecurityGroup +`, + expected: rds.RDS{ + Classic: rds.Classic{ + DBSecurityGroups: []rds.DBSecurityGroup{{}}, + }, + ParameterGroups: []rds.ParameterGroups{{}}, + Clusters: []rds.Cluster{{ + Engine: types.StringTest("aurora"), + BackupRetentionPeriodDays: types.IntTest(1), + }}, + Instances: []rds.Instance{{ + BackupRetentionPeriodDays: types.IntTest(1), + PublicAccess: types.BoolTest(true), + DBParameterGroups: []rds.DBParameterGroupsList{{}}, + }}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/cloudformation/aws/rds/cluster.go b/pkg/iac/adapters/cloudformation/aws/rds/cluster.go new file mode 100644 index 000000000000..87bbc7ae64ba --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/rds/cluster.go @@ -0,0 +1,48 @@ +package rds + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/rds" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getClusters(ctx parser.FileContext) (clusters map[string]rds.Cluster) { + clusters = make(map[string]rds.Cluster) + for _, clusterResource := range ctx.GetResourcesByType("AWS::RDS::DBCluster") { + clusters[clusterResource.ID()] = rds.Cluster{ + Metadata: clusterResource.Metadata(), + BackupRetentionPeriodDays: clusterResource.GetIntProperty("BackupRetentionPeriod", 1), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: clusterResource.Metadata(), + Enabled: clusterResource.GetBoolProperty("PerformanceInsightsEnabled"), + KMSKeyID: clusterResource.GetStringProperty("PerformanceInsightsKmsKeyId"), + }, + Encryption: rds.Encryption{ + Metadata: clusterResource.Metadata(), + EncryptStorage: clusterResource.GetBoolProperty("StorageEncrypted"), + KMSKeyID: clusterResource.GetStringProperty("KmsKeyId"), + }, + PublicAccess: iacTypes.BoolDefault(false, clusterResource.Metadata()), + Engine: clusterResource.GetStringProperty("Engine", rds.EngineAurora), + LatestRestorableTime: iacTypes.TimeUnresolvable(clusterResource.Metadata()), + DeletionProtection: clusterResource.GetBoolProperty("DeletionProtection"), + } + } + return clusters +} + +func getClassic(ctx parser.FileContext) rds.Classic { + return rds.Classic{ + DBSecurityGroups: getClassicSecurityGroups(ctx), + } +} + +func getClassicSecurityGroups(ctx parser.FileContext) (groups []rds.DBSecurityGroup) { + for _, dbsgResource := range ctx.GetResourcesByType("AWS::RDS::DBSecurityGroup") { + group := rds.DBSecurityGroup{ + Metadata: dbsgResource.Metadata(), + } + groups = append(groups, group) + } + return groups +} diff --git a/pkg/iac/adapters/cloudformation/aws/rds/instance.go b/pkg/iac/adapters/cloudformation/aws/rds/instance.go new file mode 100644 index 000000000000..256eada02aac --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/rds/instance.go @@ -0,0 +1,133 @@ +package rds + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/rds" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getClustersAndInstances(ctx parser.FileContext) ([]rds.Cluster, []rds.Instance) { + + clusterMap := getClusters(ctx) + + var orphans []rds.Instance + + for _, r := range ctx.GetResourcesByType("AWS::RDS::DBInstance") { + + instance := rds.Instance{ + Metadata: r.Metadata(), + BackupRetentionPeriodDays: r.GetIntProperty("BackupRetentionPeriod", 1), + ReplicationSourceARN: r.GetStringProperty("SourceDBInstanceIdentifier"), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: r.Metadata(), + Enabled: r.GetBoolProperty("EnablePerformanceInsights"), + KMSKeyID: r.GetStringProperty("PerformanceInsightsKMSKeyId"), + }, + Encryption: rds.Encryption{ + Metadata: r.Metadata(), + EncryptStorage: r.GetBoolProperty("StorageEncrypted"), + KMSKeyID: r.GetStringProperty("KmsKeyId"), + }, + PublicAccess: r.GetBoolProperty("PubliclyAccessible", true), + Engine: r.GetStringProperty("Engine"), + IAMAuthEnabled: r.GetBoolProperty("EnableIAMDatabaseAuthentication"), + DeletionProtection: r.GetBoolProperty("DeletionProtection", false), + DBInstanceArn: r.GetStringProperty("DBInstanceArn"), + StorageEncrypted: r.GetBoolProperty("StorageEncrypted", false), + DBInstanceIdentifier: r.GetStringProperty("DBInstanceIdentifier"), + DBParameterGroups: getDBParameterGroups(ctx, r), + TagList: getTagList(r), + EnabledCloudwatchLogsExports: getEnabledCloudwatchLogsExports(r), + EngineVersion: r.GetStringProperty("EngineVersion"), + AutoMinorVersionUpgrade: r.GetBoolProperty("AutoMinorVersionUpgrade"), + MultiAZ: r.GetBoolProperty("MultiAZ"), + PubliclyAccessible: r.GetBoolProperty("PubliclyAccessible"), + LatestRestorableTime: types.TimeUnresolvable(r.Metadata()), + ReadReplicaDBInstanceIdentifiers: getReadReplicaDBInstanceIdentifiers(r), + } + + if clusterID := r.GetProperty("DBClusterIdentifier"); clusterID.IsString() { + if cluster, exist := clusterMap[clusterID.AsString()]; exist { + cluster.Instances = append(cluster.Instances, rds.ClusterInstance{ + Instance: instance, + ClusterIdentifier: clusterID.AsStringValue(), + }) + clusterMap[clusterID.AsString()] = cluster + } + } else { + orphans = append(orphans, instance) + } + } + + clusters := make([]rds.Cluster, 0, len(clusterMap)) + + for _, cluster := range clusterMap { + clusters = append(clusters, cluster) + } + + return clusters, orphans +} + +func getDBParameterGroups(ctx parser.FileContext, r *parser.Resource) (dbParameterGroup []rds.DBParameterGroupsList) { + + var parameterGroupList []rds.DBParameterGroupsList + + dbParameterGroupName := r.GetStringProperty("DBParameterGroupName") + + for _, r := range ctx.GetResourcesByType("AWS::RDS::DBParameterGroup") { + name := r.GetStringProperty("DBParameterGroupName") + // TODO: find by resource logical id + if !dbParameterGroupName.EqualTo(name.Value()) { + continue + } + dbpmgl := rds.DBParameterGroupsList{ + Metadata: r.Metadata(), + DBParameterGroupName: name, + KMSKeyID: types.StringUnresolvable(r.Metadata()), + } + parameterGroupList = append(dbParameterGroup, dbpmgl) + } + + return parameterGroupList +} + +func getEnabledCloudwatchLogsExports(r *parser.Resource) (enabledcloudwatchlogexportslist []types.StringValue) { + enabledCloudwatchLogExportList := r.GetProperty("EnableCloudwatchLogsExports") + + if enabledCloudwatchLogExportList.IsNil() || enabledCloudwatchLogExportList.IsNotList() { + return enabledcloudwatchlogexportslist + } + + for _, ecle := range enabledCloudwatchLogExportList.AsList() { + enabledcloudwatchlogexportslist = append(enabledcloudwatchlogexportslist, ecle.AsStringValue()) + } + return enabledcloudwatchlogexportslist +} + +func getTagList(r *parser.Resource) (taglist []rds.TagList) { + tagLists := r.GetProperty("Tags") + + if tagLists.IsNil() || tagLists.IsNotList() { + return taglist + } + + for _, tl := range tagLists.AsList() { + taglist = append(taglist, rds.TagList{ + Metadata: tl.Metadata(), + }) + } + return taglist +} + +func getReadReplicaDBInstanceIdentifiers(r *parser.Resource) (readreplicadbidentifier []types.StringValue) { + readReplicaDBIdentifier := r.GetProperty("SourceDBInstanceIdentifier") + + if readReplicaDBIdentifier.IsNil() || readReplicaDBIdentifier.IsNotList() { + return readreplicadbidentifier + } + + for _, rr := range readReplicaDBIdentifier.AsList() { + readreplicadbidentifier = append(readreplicadbidentifier, rr.AsStringValue()) + } + return readreplicadbidentifier +} diff --git a/pkg/iac/adapters/cloudformation/aws/rds/parameter_groups.go b/pkg/iac/adapters/cloudformation/aws/rds/parameter_groups.go new file mode 100644 index 000000000000..f47c2f70a706 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/rds/parameter_groups.go @@ -0,0 +1,44 @@ +package rds + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/rds" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getParameterGroups(ctx parser.FileContext) (parametergroups []rds.ParameterGroups) { + + for _, r := range ctx.GetResourcesByType("AWS::RDS::DBParameterGroup") { + + paramgroup := rds.ParameterGroups{ + Metadata: r.Metadata(), + DBParameterGroupName: r.GetStringProperty("DBParameterGroupName"), + DBParameterGroupFamily: r.GetStringProperty("DBParameterGroupFamily"), + Parameters: getParameters(r), + } + + parametergroups = append(parametergroups, paramgroup) + } + + return parametergroups +} + +func getParameters(r *parser.Resource) (parameters []rds.Parameters) { + + dBParam := r.GetProperty("Parameters") + + // TODO: parameters is JSON + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbparametergroup.html#cfn-rds-dbparametergroup-parameters + if dBParam.IsNil() || dBParam.IsNotList() { + return parameters + } + + for _, dbp := range dBParam.AsList() { + parameters = append(parameters, rds.Parameters{ + Metadata: dbp.Metadata(), + ParameterName: types.StringDefault("", dbp.Metadata()), + ParameterValue: types.StringDefault("", dbp.Metadata()), + }) + } + return parameters +} diff --git a/pkg/iac/adapters/cloudformation/aws/rds/rds.go b/pkg/iac/adapters/cloudformation/aws/rds/rds.go new file mode 100644 index 000000000000..25118c20bbba --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/rds/rds.go @@ -0,0 +1,18 @@ +package rds + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/rds" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an RDS instance +func Adapt(cfFile parser.FileContext) rds.RDS { + clusters, orphans := getClustersAndInstances(cfFile) + return rds.RDS{ + Instances: orphans, + Clusters: clusters, + Classic: getClassic(cfFile), + ParameterGroups: getParameterGroups(cfFile), + Snapshots: nil, + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/redshift/cluster.go b/pkg/iac/adapters/cloudformation/aws/redshift/cluster.go new file mode 100644 index 000000000000..c8acf8997af0 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/redshift/cluster.go @@ -0,0 +1,49 @@ +package redshift + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/redshift" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getClusters(ctx parser.FileContext) (clusters []redshift.Cluster) { + for _, r := range ctx.GetResourcesByType("AWS::Redshift::Cluster") { + + cluster := redshift.Cluster{ + Metadata: r.Metadata(), + ClusterIdentifier: r.GetStringProperty("ClusterIdentifier"), + AllowVersionUpgrade: r.GetBoolProperty("AllowVersionUpgrade", true), + NodeType: r.GetStringProperty("NodeType"), + NumberOfNodes: r.GetIntProperty("NumberOfNodes", 1), + PubliclyAccessible: r.GetBoolProperty("PubliclyAccessible"), + MasterUsername: r.GetStringProperty("MasterUsername"), + AutomatedSnapshotRetentionPeriod: r.GetIntProperty("AutomatedSnapshotRetentionPeriod", 1), + Encryption: redshift.Encryption{ + Metadata: r.Metadata(), + Enabled: r.GetBoolProperty("Encrypted"), + KMSKeyID: r.GetStringProperty("KmsKeyId"), + }, + EndPoint: redshift.EndPoint{ + Metadata: r.Metadata(), + Port: r.GetIntProperty("Endpoint.Port"), + }, + SubnetGroupName: r.GetStringProperty("ClusterSubnetGroupName", ""), + } + + clusters = append(clusters, cluster) + } + return clusters +} + +func getParameters(ctx parser.FileContext) (parameter []redshift.ClusterParameter) { + var parameters []redshift.ClusterParameter + for _, r := range ctx.GetResourcesByType("AWS::Redshift::ClusterParameterGroup") { + for _, par := range r.GetProperty("Parameters").AsList() { + parameters = append(parameters, redshift.ClusterParameter{ + Metadata: par.Metadata(), + ParameterName: par.GetStringProperty("ParameterName"), + ParameterValue: par.GetStringProperty("ParameterValue"), + }) + } + } + return parameters +} diff --git a/pkg/iac/adapters/cloudformation/aws/redshift/redshift.go b/pkg/iac/adapters/cloudformation/aws/redshift/redshift.go new file mode 100644 index 000000000000..899b6c50e147 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/redshift/redshift.go @@ -0,0 +1,16 @@ +package redshift + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/redshift" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a RedShift instance +func Adapt(cfFile parser.FileContext) redshift.Redshift { + return redshift.Redshift{ + Clusters: getClusters(cfFile), + SecurityGroups: getSecurityGroups(cfFile), + ClusterParameters: getParameters(cfFile), + ReservedNodes: nil, + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/redshift/redshift_test.go b/pkg/iac/adapters/cloudformation/aws/redshift/redshift_test.go new file mode 100644 index 000000000000..a14117a21961 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/redshift/redshift_test.go @@ -0,0 +1,111 @@ +package redshift + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/redshift" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected redshift.Redshift + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + myCluster: + Type: "AWS::Redshift::Cluster" + Properties: + DBName: "mydb" + ClusterIdentifier: myexamplecluster + AllowVersionUpgrade: false + MasterUsername: "master" + NodeType: "ds2.xlarge" + NumberOfNodes: 2 + PubliclyAccessible: true + AutomatedSnapshotRetentionPeriod: 2 + Encrypted: true + KmsKeyId: key + Endpoint: + Port: 2000 + ClusterSubnetGroupName: test + myClusterParameterGroup: + Type: "AWS::Redshift::ClusterParameterGroup" + Properties: + Parameters: + - + ParameterName: "enable_user_activity_logging" + ParameterValue: "true" + mySecGroup: + Type: AWS::Redshift::ClusterSecurityGroup + Properties: + Description: test + `, + expected: redshift.Redshift{ + Clusters: []redshift.Cluster{ + { + ClusterIdentifier: types.StringTest("myexamplecluster"), + AllowVersionUpgrade: types.BoolTest(false), + MasterUsername: types.StringTest("master"), + NodeType: types.StringTest("ds2.xlarge"), + NumberOfNodes: types.IntTest(2), + PubliclyAccessible: types.BoolTest(true), + AutomatedSnapshotRetentionPeriod: types.IntTest(2), + Encryption: redshift.Encryption{ + Enabled: types.BoolTest(true), + KMSKeyID: types.StringTest("key"), + }, + EndPoint: redshift.EndPoint{ + Port: types.IntTest(2000), + }, + SubnetGroupName: types.StringTest("test"), + }, + }, + ClusterParameters: []redshift.ClusterParameter{ + { + ParameterName: types.StringTest("enable_user_activity_logging"), + ParameterValue: types.StringTest("true"), + }, + }, + SecurityGroups: []redshift.SecurityGroup{ + { + Description: types.StringTest("test"), + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + myCluster: + Type: "AWS::Redshift::Cluster" + mySecGroup: + Type: AWS::Redshift::ClusterSecurityGroup + myClusterParameterGroup: + Type: "AWS::Redshift::ClusterParameterGroup" +`, + expected: redshift.Redshift{ + Clusters: []redshift.Cluster{ + { + AllowVersionUpgrade: types.BoolTest(true), + AutomatedSnapshotRetentionPeriod: types.IntTest(1), + NumberOfNodes: types.IntTest(1), + }, + }, + SecurityGroups: []redshift.SecurityGroup{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/redshift/security_group.go b/pkg/iac/adapters/cloudformation/aws/redshift/security_group.go new file mode 100644 index 000000000000..0223306783ae --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/redshift/security_group.go @@ -0,0 +1,17 @@ +package redshift + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/redshift" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getSecurityGroups(ctx parser.FileContext) (groups []redshift.SecurityGroup) { + for _, groupResource := range ctx.GetResourcesByType("AWS::Redshift::ClusterSecurityGroup") { + group := redshift.SecurityGroup{ + Metadata: groupResource.Metadata(), + Description: groupResource.GetProperty("Description").AsStringValue(), + } + groups = append(groups, group) + } + return groups +} diff --git a/pkg/iac/adapters/cloudformation/aws/s3/bucket.go b/pkg/iac/adapters/cloudformation/aws/s3/bucket.go new file mode 100644 index 000000000000..b7d82cb208f1 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/s3/bucket.go @@ -0,0 +1,189 @@ +package s3 + +import ( + "cmp" + "regexp" + "slices" + "strings" + + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +var aclConvertRegex = regexp.MustCompile(`[A-Z][^A-Z]*`) + +func getBuckets(cfFile parser.FileContext) []s3.Bucket { + var buckets []s3.Bucket + bucketResources := cfFile.GetResourcesByType("AWS::S3::Bucket") + + for _, r := range bucketResources { + s3b := s3.Bucket{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("BucketName"), + PublicAccessBlock: getPublicAccessBlock(r), + Encryption: getEncryption(r, cfFile), + Versioning: s3.Versioning{ + Metadata: r.Metadata(), + Enabled: hasVersioning(r), + MFADelete: iacTypes.BoolUnresolvable(r.Metadata()), + }, + Logging: getLogging(r), + ACL: convertAclValue(r.GetStringProperty("AccessControl", "private")), + LifecycleConfiguration: getLifecycle(r), + AccelerateConfigurationStatus: r.GetStringProperty("AccelerateConfiguration.AccelerationStatus"), + Website: getWebsite(r), + BucketLocation: iacTypes.String("", r.Metadata()), + Objects: nil, + BucketPolicies: getBucketPolicies(cfFile, r), + } + + buckets = append(buckets, s3b) + } + + slices.SortFunc(buckets, func(a, b s3.Bucket) int { + return cmp.Compare(a.Name.Value(), b.Name.Value()) + }) + + return buckets +} + +func getPublicAccessBlock(r *parser.Resource) *s3.PublicAccessBlock { + block := r.GetProperty("PublicAccessBlockConfiguration") + if block.IsNil() { + return nil + } + + return &s3.PublicAccessBlock{ + Metadata: block.Metadata(), + BlockPublicACLs: block.GetBoolProperty("BlockPublicAcls"), + BlockPublicPolicy: block.GetBoolProperty("BlockPublicPolicy"), + IgnorePublicACLs: block.GetBoolProperty("IgnorePublicAcls"), + RestrictPublicBuckets: block.GetBoolProperty("RestrictPublicBuckets"), + } +} + +func convertAclValue(aclValue iacTypes.StringValue) iacTypes.StringValue { + matches := aclConvertRegex.FindAllString(aclValue.Value(), -1) + + return iacTypes.String(strings.ToLower(strings.Join(matches, "-")), aclValue.GetMetadata()) +} + +func getLogging(r *parser.Resource) s3.Logging { + logging := s3.Logging{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + TargetBucket: iacTypes.StringDefault("", r.Metadata()), + } + + if config := r.GetProperty("LoggingConfiguration"); config.IsNotNil() { + logging.TargetBucket = config.GetStringProperty("DestinationBucketName") + if logging.TargetBucket.IsNotEmpty() || !logging.TargetBucket.GetMetadata().IsResolvable() { + logging.Enabled = iacTypes.Bool(true, config.Metadata()) + } + } + return logging +} + +func hasVersioning(r *parser.Resource) iacTypes.BoolValue { + versioningProp := r.GetProperty("VersioningConfiguration.Status") + + if versioningProp.IsNil() { + return iacTypes.BoolDefault(false, r.Metadata()) + } + + versioningEnabled := versioningProp.EqualTo("Enabled") + + return iacTypes.Bool(versioningEnabled, versioningProp.Metadata()) +} + +func getEncryption(r *parser.Resource, _ parser.FileContext) s3.Encryption { + encryption := s3.Encryption{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + Algorithm: iacTypes.StringDefault("", r.Metadata()), + KMSKeyId: iacTypes.StringDefault("", r.Metadata()), + } + + if encryptProps := r.GetProperty("BucketEncryption.ServerSideEncryptionConfiguration"); encryptProps.IsNotNil() { + for _, rule := range encryptProps.AsList() { + algo := rule.GetProperty("ServerSideEncryptionByDefault.SSEAlgorithm") + if algo.IsString() { + algoVal := algo.AsString() + isValidAlgo := slices.Contains(s3types.ServerSideEncryption("").Values(), s3types.ServerSideEncryption(algoVal)) + encryption.Enabled = iacTypes.Bool(isValidAlgo, algo.Metadata()) + encryption.Algorithm = algo.AsStringValue() + } + + kmsKeyProp := rule.GetProperty("ServerSideEncryptionByDefault.KMSMasterKeyID") + if !kmsKeyProp.IsEmpty() && kmsKeyProp.IsString() { + encryption.KMSKeyId = kmsKeyProp.AsStringValue() + } + } + } + + return encryption +} + +func getLifecycle(resource *parser.Resource) []s3.Rules { + RuleProp := resource.GetProperty("LifecycleConfiguration.Rules") + + var rule []s3.Rules + + if RuleProp.IsNil() || RuleProp.IsNotList() { + return rule + } + + for _, r := range RuleProp.AsList() { + rule = append(rule, s3.Rules{ + Metadata: r.Metadata(), + Status: r.GetStringProperty("Status"), + }) + } + return rule +} + +func getWebsite(r *parser.Resource) *s3.Website { + block := r.GetProperty("WebsiteConfiguration") + if block.IsNil() { + return nil + } + return &s3.Website{ + Metadata: block.Metadata(), + } +} + +func getBucketPolicies(fctx parser.FileContext, r *parser.Resource) []iam.Policy { + var policies []iam.Policy + for _, bucketPolicy := range fctx.GetResourcesByType("AWS::S3::BucketPolicy") { + bucket := bucketPolicy.GetStringProperty("Bucket") + if bucket.NotEqualTo(r.GetStringProperty("BucketName").Value()) && bucket.NotEqualTo(r.ID()) { + continue + } + + doc := bucketPolicy.GetProperty("PolicyDocument") + if doc.IsNil() { + continue + } + + parsed, err := iamgo.Parse(doc.GetJsonBytes()) + if err != nil { + continue + } + policies = append(policies, iam.Policy{ + Metadata: doc.Metadata(), + Name: iacTypes.StringDefault("", doc.Metadata()), + Document: iam.Document{ + Metadata: doc.Metadata(), + Parsed: *parsed, + }, + Builtin: iacTypes.Bool(false, doc.Metadata()), + }) + } + + return policies +} diff --git a/pkg/iac/adapters/cloudformation/aws/s3/s3.go b/pkg/iac/adapters/cloudformation/aws/s3/s3.go new file mode 100644 index 000000000000..988b51adc378 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/s3/s3.go @@ -0,0 +1,13 @@ +package s3 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an S3 instance +func Adapt(cfFile parser.FileContext) s3.S3 { + return s3.S3{ + Buckets: getBuckets(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/s3/s3_test.go b/pkg/iac/adapters/cloudformation/aws/s3/s3_test.go new file mode 100644 index 000000000000..6eafe95ba0e1 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/s3/s3_test.go @@ -0,0 +1,184 @@ +package s3 + +import ( + "testing" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected s3.S3 + }{ + { + name: "complete s3 bucket", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + Key: + Type: "AWS::KMS::Key" + LoggingBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: logging-bucket + Bucket: + Type: AWS::S3::Bucket + Properties: + BucketName: test-bucket + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + KMSMasterKeyID: + Fn::GetAtt: + - Key + - Arn + SSEAlgorithm: aws:kms + AccessControl: AwsExecRead + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + LoggingConfiguration: + DestinationBucketName: !Ref LoggingBucket + LogFilePrefix: testing-logs + LifecycleConfiguration: + Rules: + - Id: GlacierRule + Prefix: glacier + Status: Enabled + ExpirationInDays: 365 + AccelerateConfiguration: + AccelerationStatus: Enabled + VersioningConfiguration: + Status: Enabled + WebsiteConfiguration: + IndexDocument: index.html + SampleBucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: !Ref Bucket + PolicyDocument: + Version: 2012-10-17 + Statement: + - Action: + - 's3:GetObject' + Effect: Allow + Resource: !Join + - 'arn:aws:s3:::testbucket/*' + Principal: '*' +`, + expected: s3.S3{ + Buckets: []s3.Bucket{ + { + Name: types.StringTest("logging-bucket"), + }, + { + Name: types.StringTest("test-bucket"), + Encryption: s3.Encryption{ + Enabled: types.BoolTest(true), + Algorithm: types.StringTest("aws:kms"), + KMSKeyId: types.StringTest("Key"), + }, + ACL: types.StringTest("aws-exec-read"), + PublicAccessBlock: &s3.PublicAccessBlock{ + BlockPublicACLs: types.BoolTest(true), + BlockPublicPolicy: types.BoolTest(true), + IgnorePublicACLs: types.BoolTest(true), + RestrictPublicBuckets: types.BoolTest(true), + }, + Logging: s3.Logging{ + TargetBucket: types.StringTest("LoggingBucket"), + Enabled: types.BoolTest(true), + }, + LifecycleConfiguration: []s3.Rules{ + { + Status: types.StringTest("Enabled"), + }, + }, + AccelerateConfigurationStatus: types.StringTest("Enabled"), + Versioning: s3.Versioning{ + Enabled: types.BoolTest(true), + }, + Website: &s3.Website{}, + BucketPolicies: []iam.Policy{ + { + Document: iam.Document{ + Parsed: iamgo.NewPolicyBuilder(). + WithStatement( + iamgo.NewStatementBuilder(). + WithActions([]string{"s3:GetObject"}). + WithAllPrincipals(true). + WithEffect("Allow"). + WithResources([]string{"arn:aws:s3:::testbucket/*"}). + Build(), + ). + WithVersion("2012-10-17T00:00:00Z"). + Build(), + }, + }, + }, + }, + }, + }, + }, + { + name: "empty s3 bucket", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + Bucket: + Type: AWS::S3::Bucket + Properties: + BucketName: test-bucket`, + expected: s3.S3{ + Buckets: []s3.Bucket{ + { + Name: types.StringTest("test-bucket"), + Encryption: s3.Encryption{ + Enabled: types.BoolTest(false), + }, + }, + }, + }, + }, + { + name: "incorrect SSE algorithm", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + Bucket: + Type: AWS::S3::Bucket + Properties: + BucketName: test-bucket + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + KMSMasterKeyID: alias/my-key + SSEAlgorithm: aes256 +`, + expected: s3.S3{ + Buckets: []s3.Bucket{ + { + Name: types.StringTest("test-bucket"), + Encryption: s3.Encryption{ + Enabled: types.BoolTest(false), + KMSKeyId: types.StringTest("alias/my-key"), + Algorithm: types.StringTest("aes256"), + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } + +} diff --git a/pkg/iac/adapters/cloudformation/aws/sam/api.go b/pkg/iac/adapters/cloudformation/aws/sam/api.go new file mode 100644 index 000000000000..4d4f04e6e83a --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sam/api.go @@ -0,0 +1,96 @@ +package sam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sam" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getApis(cfFile parser.FileContext) (apis []sam.API) { + + apiResources := cfFile.GetResourcesByType("AWS::Serverless::Api") + for _, r := range apiResources { + api := sam.API{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("Name", ""), + TracingEnabled: r.GetBoolProperty("TracingEnabled"), + DomainConfiguration: getDomainConfiguration(r), + AccessLogging: getAccessLogging(r), + RESTMethodSettings: getRestMethodSettings(r), + } + + apis = append(apis, api) + } + + return apis +} + +func getRestMethodSettings(r *parser.Resource) sam.RESTMethodSettings { + + settings := sam.RESTMethodSettings{ + Metadata: r.Metadata(), + CacheDataEncrypted: iacTypes.BoolDefault(false, r.Metadata()), + LoggingEnabled: iacTypes.BoolDefault(false, r.Metadata()), + DataTraceEnabled: iacTypes.BoolDefault(false, r.Metadata()), + MetricsEnabled: iacTypes.BoolDefault(false, r.Metadata()), + } + + // TODO: MethodSettings is list + // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-stage.html#cfn-apigateway-stage-methodsettings + settingsProp := r.GetProperty("MethodSettings") + if settingsProp.IsNotNil() { + + settings = sam.RESTMethodSettings{ + Metadata: settingsProp.Metadata(), + CacheDataEncrypted: settingsProp.GetBoolProperty("CacheDataEncrypted"), + LoggingEnabled: iacTypes.BoolDefault(false, settingsProp.Metadata()), + DataTraceEnabled: settingsProp.GetBoolProperty("DataTraceEnabled"), + MetricsEnabled: settingsProp.GetBoolProperty("MetricsEnabled"), + } + + if loggingLevel := settingsProp.GetProperty("LoggingLevel"); loggingLevel.IsNotNil() { + if loggingLevel.EqualTo("OFF", parser.IgnoreCase) { + settings.LoggingEnabled = iacTypes.Bool(false, loggingLevel.Metadata()) + } else { + settings.LoggingEnabled = iacTypes.Bool(true, loggingLevel.Metadata()) + } + } + } + + return settings +} + +func getAccessLogging(r *parser.Resource) sam.AccessLogging { + + logging := sam.AccessLogging{ + Metadata: r.Metadata(), + CloudwatchLogGroupARN: iacTypes.StringDefault("", r.Metadata()), + } + + if access := r.GetProperty("AccessLogSetting"); access.IsNotNil() { + logging = sam.AccessLogging{ + Metadata: access.Metadata(), + CloudwatchLogGroupARN: access.GetStringProperty("DestinationArn", ""), + } + } + + return logging +} + +func getDomainConfiguration(r *parser.Resource) sam.DomainConfiguration { + + domainConfig := sam.DomainConfiguration{ + Metadata: r.Metadata(), + } + + if domain := r.GetProperty("Domain"); domain.IsNotNil() { + domainConfig = sam.DomainConfiguration{ + Metadata: domain.Metadata(), + Name: domain.GetStringProperty("DomainName"), + SecurityPolicy: domain.GetStringProperty("SecurityPolicy"), + } + } + + return domainConfig + +} diff --git a/pkg/iac/adapters/cloudformation/aws/sam/function.go b/pkg/iac/adapters/cloudformation/aws/sam/function.go new file mode 100644 index 000000000000..2bfc488e1a18 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sam/function.go @@ -0,0 +1,58 @@ +package sam + +import ( + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sam" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getFunctions(cfFile parser.FileContext) (functions []sam.Function) { + + functionResources := cfFile.GetResourcesByType("AWS::Serverless::Function") + for _, r := range functionResources { + function := sam.Function{ + Metadata: r.Metadata(), + FunctionName: r.GetStringProperty("FunctionName"), + Tracing: r.GetStringProperty("Tracing"), + ManagedPolicies: nil, + Policies: nil, + } + + setFunctionPolicies(r, &function) + functions = append(functions, function) + } + + return functions +} + +func setFunctionPolicies(r *parser.Resource, function *sam.Function) { + policies := r.GetProperty("Policies") + if policies.IsNotNil() { + if policies.IsString() { + function.ManagedPolicies = append(function.ManagedPolicies, policies.AsStringValue()) + } else if policies.IsList() { + for _, property := range policies.AsList() { + if property.IsMap() { + parsed, err := iamgo.Parse(property.GetJsonBytes(true)) + if err != nil { + continue + } + policy := iam.Policy{ + Metadata: property.Metadata(), + Name: iacTypes.StringDefault("", property.Metadata()), + Document: iam.Document{ + Metadata: property.Metadata(), + Parsed: *parsed, + }, + Builtin: iacTypes.Bool(false, property.Metadata()), + } + function.Policies = append(function.Policies, policy) + } else if property.IsString() { + function.ManagedPolicies = append(function.ManagedPolicies, property.AsStringValue()) + } + } + } + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/sam/http_api.go b/pkg/iac/adapters/cloudformation/aws/sam/http_api.go new file mode 100644 index 000000000000..02f9ba6c5ef9 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sam/http_api.go @@ -0,0 +1,65 @@ +package sam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sam" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getHttpApis(cfFile parser.FileContext) (apis []sam.HttpAPI) { + + apiResources := cfFile.GetResourcesByType("AWS::Serverless::HttpApi") + for _, r := range apiResources { + api := sam.HttpAPI{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("Name", ""), + DomainConfiguration: getDomainConfiguration(r), + AccessLogging: getAccessLoggingV2(r), + DefaultRouteSettings: getRouteSettings(r), + } + + apis = append(apis, api) + } + + return apis +} + +func getAccessLoggingV2(r *parser.Resource) sam.AccessLogging { + + logging := sam.AccessLogging{ + Metadata: r.Metadata(), + CloudwatchLogGroupARN: types.StringDefault("", r.Metadata()), + } + + if access := r.GetProperty("AccessLogSettings"); access.IsNotNil() { + logging = sam.AccessLogging{ + Metadata: access.Metadata(), + CloudwatchLogGroupARN: access.GetStringProperty("DestinationArn", ""), + } + } + + return logging +} + +func getRouteSettings(r *parser.Resource) sam.RouteSettings { + + routeSettings := sam.RouteSettings{ + Metadata: r.Metadata(), + LoggingEnabled: types.BoolDefault(false, r.Metadata()), + DataTraceEnabled: types.BoolDefault(false, r.Metadata()), + DetailedMetricsEnabled: types.BoolDefault(false, r.Metadata()), + } + + if route := r.GetProperty("DefaultRouteSettings"); route.IsNotNil() { + routeSettings = sam.RouteSettings{ + Metadata: route.Metadata(), + // TODO: LoggingLevel is string + LoggingEnabled: route.GetBoolProperty("LoggingLevel"), + DataTraceEnabled: route.GetBoolProperty("DataTraceEnabled"), + DetailedMetricsEnabled: route.GetBoolProperty("DetailedMetricsEnabled"), + } + } + + return routeSettings + +} diff --git a/pkg/iac/adapters/cloudformation/aws/sam/sam.go b/pkg/iac/adapters/cloudformation/aws/sam/sam.go new file mode 100644 index 000000000000..0f08854aa697 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sam/sam.go @@ -0,0 +1,17 @@ +package sam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sam" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an SAM instance +func Adapt(cfFile parser.FileContext) sam.SAM { + return sam.SAM{ + APIs: getApis(cfFile), + HttpAPIs: getHttpApis(cfFile), + Functions: getFunctions(cfFile), + StateMachines: getStateMachines(cfFile), + SimpleTables: getSimpleTables(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/sam/sam_test.go b/pkg/iac/adapters/cloudformation/aws/sam/sam_test.go new file mode 100644 index 000000000000..9bd8ab6bd641 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sam/sam_test.go @@ -0,0 +1,213 @@ +package sam + +import ( + "testing" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sam" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected sam.SAM + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + ApiGatewayApi: + Type: AWS::Serverless::Api + Properties: + StageName: prod + Name: test + TracingEnabled: true + Domain: + DomainName: domain + SecurityPolicy: "TLS_1_2" + MethodSettings: + - DataTraceEnabled: true + CacheDataEncrypted: true + MetricsEnabled: true + LoggingLevel: INFO + AccessLogSetting: + DestinationArn: 'arn:aws:logs:us-east-1:123456789:log-group:my-log-group' + HttpApi: + Type: AWS::Serverless::HttpApi + Properties: + Name: test + Domain: + DomainName: test + SecurityPolicy: "TLS_1_2" + AccessLogSettings: + DestinationArn: 'arn:aws:logs:us-east-1:123456789:log-group:my-log-group' + DefaultRouteSettings: + LoggingLevel: INFO + DataTraceEnabled: true + DetailedMetricsEnabled: true + myFunction: + Type: AWS::Serverless::Function + Properties: + FunctionName: test + Tracing: Active + Policies: + - AWSLambdaExecute + - Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - s3:GetObject + Resource: 'arn:aws:s3:::my-bucket/*' + MySampleStateMachine: + Type: AWS::Serverless::StateMachine + Properties: + Logging: + Level: ALL + Tracing: + Enabled: true + Policies: + - Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "cloudwatch:*" + Resource: "*" + myTable: + Type: AWS::Serverless::SimpleTable + Properties: + TableName: my-table + SSESpecification: + SSEEnabled: "true" + KMSMasterKeyId: "kmskey" +`, + expected: sam.SAM{ + APIs: []sam.API{ + { + Name: types.StringTest("test"), + TracingEnabled: types.BoolTest(true), + DomainConfiguration: sam.DomainConfiguration{ + Name: types.StringTest("domain"), + SecurityPolicy: types.StringTest("TLS_1_2"), + }, + AccessLogging: sam.AccessLogging{ + CloudwatchLogGroupARN: types.StringTest("arn:aws:logs:us-east-1:123456789:log-group:my-log-group"), + }, + }, + }, + HttpAPIs: []sam.HttpAPI{ + { + Name: types.StringTest("test"), + DomainConfiguration: sam.DomainConfiguration{ + Name: types.StringTest("test"), + SecurityPolicy: types.StringTest("TLS_1_2"), + }, + AccessLogging: sam.AccessLogging{ + CloudwatchLogGroupARN: types.StringTest("arn:aws:logs:us-east-1:123456789:log-group:my-log-group"), + }, + DefaultRouteSettings: sam.RouteSettings{ + DataTraceEnabled: types.BoolTest(true), + DetailedMetricsEnabled: types.BoolTest(true), + }, + }, + }, + Functions: []sam.Function{ + { + FunctionName: types.StringTest("test"), + Tracing: types.StringTest("Active"), + Policies: []iam.Policy{ + { + Document: func() iam.Document { + return iam.Document{ + Parsed: iamgo.NewPolicyBuilder(). + WithVersion("2012-10-17"). + WithStatement( + iamgo.NewStatementBuilder(). + WithEffect("Allow"). + WithActions([]string{"s3:GetObject"}). + WithResources([]string{"arn:aws:s3:::my-bucket/*"}). + Build(), + ). + Build(), + } + }(), + }, + }, + ManagedPolicies: []types.StringValue{ + types.StringTest("AWSLambdaExecute"), + }, + }, + }, + StateMachines: []sam.StateMachine{ + { + LoggingConfiguration: sam.LoggingConfiguration{ + LoggingEnabled: types.BoolTest(true), + }, + Tracing: sam.TracingConfiguration{ + Enabled: types.BoolTest(true), + }, + Policies: []iam.Policy{ + { + Document: func() iam.Document { + return iam.Document{ + Parsed: iamgo.NewPolicyBuilder(). + WithVersion("2012-10-17"). + WithStatement( + iamgo.NewStatementBuilder(). + WithEffect("Allow"). + WithActions([]string{"cloudwatch:*"}). + WithResources([]string{"*"}). + Build(), + ). + Build(), + } + }(), + }, + }, + }, + }, + SimpleTables: []sam.SimpleTable{ + { + TableName: types.StringTest("my-table"), + SSESpecification: sam.SSESpecification{ + Enabled: types.BoolTest(true), + KMSMasterKeyID: types.StringTest("kmskey"), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + ApiGatewayApi: + Type: AWS::Serverless::Api + HttpApi: + Type: AWS::Serverless::HttpApi + myFunction: + Type: AWS::Serverless::Function + MySampleStateMachine: + Type: AWS::Serverless::StateMachine + myTable: + Type: AWS::Serverless::SimpleTable +`, + expected: sam.SAM{ + APIs: []sam.API{{}}, + HttpAPIs: []sam.HttpAPI{{}}, + Functions: []sam.Function{{}}, + StateMachines: []sam.StateMachine{{}}, + SimpleTables: []sam.SimpleTable{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/sam/state_machines.go b/pkg/iac/adapters/cloudformation/aws/sam/state_machines.go new file mode 100644 index 000000000000..5b1be5618ec4 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sam/state_machines.go @@ -0,0 +1,81 @@ +package sam + +import ( + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sam" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getStateMachines(cfFile parser.FileContext) (stateMachines []sam.StateMachine) { + + stateMachineResources := cfFile.GetResourcesByType("AWS::Serverless::StateMachine") + for _, r := range stateMachineResources { + stateMachine := sam.StateMachine{ + Metadata: r.Metadata(), + Name: r.GetStringProperty("Name"), + LoggingConfiguration: sam.LoggingConfiguration{ + Metadata: r.Metadata(), + LoggingEnabled: iacTypes.BoolDefault(false, r.Metadata()), + }, + ManagedPolicies: nil, + Policies: nil, + Tracing: getTracingConfiguration(r), + } + + // TODO: By default, the level is set to OFF + if logging := r.GetProperty("Logging"); logging.IsNotNil() { + stateMachine.LoggingConfiguration.Metadata = logging.Metadata() + if level := logging.GetProperty("Level"); level.IsNotNil() { + stateMachine.LoggingConfiguration.LoggingEnabled = iacTypes.Bool(!level.EqualTo("OFF"), level.Metadata()) + } + } + + setStateMachinePolicies(r, &stateMachine) + stateMachines = append(stateMachines, stateMachine) + } + + return stateMachines +} + +func getTracingConfiguration(r *parser.Resource) sam.TracingConfiguration { + tracing := r.GetProperty("Tracing") + if tracing.IsNil() { + return sam.TracingConfiguration{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + } + } + + return sam.TracingConfiguration{ + Metadata: tracing.Metadata(), + Enabled: tracing.GetBoolProperty("Enabled"), + } +} + +func setStateMachinePolicies(r *parser.Resource, stateMachine *sam.StateMachine) { + policies := r.GetProperty("Policies") + if policies.IsNotNil() { + if policies.IsString() { + stateMachine.ManagedPolicies = append(stateMachine.ManagedPolicies, policies.AsStringValue()) + } else if policies.IsList() { + for _, property := range policies.AsList() { + parsed, err := iamgo.Parse(property.GetJsonBytes(true)) + if err != nil { + continue + } + policy := iam.Policy{ + Metadata: property.Metadata(), + Name: iacTypes.StringDefault("", property.Metadata()), + Document: iam.Document{ + Metadata: property.Metadata(), + Parsed: *parsed, + }, + Builtin: iacTypes.Bool(false, property.Metadata()), + } + stateMachine.Policies = append(stateMachine.Policies, policy) + } + } + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/sam/tables.go b/pkg/iac/adapters/cloudformation/aws/sam/tables.go new file mode 100644 index 000000000000..89e66acdf514 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sam/tables.go @@ -0,0 +1,39 @@ +package sam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sam" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getSimpleTables(cfFile parser.FileContext) (tables []sam.SimpleTable) { + + tableResources := cfFile.GetResourcesByType("AWS::Serverless::SimpleTable") + for _, r := range tableResources { + table := sam.SimpleTable{ + Metadata: r.Metadata(), + TableName: r.GetStringProperty("TableName"), + SSESpecification: getSSESpecification(r), + } + + tables = append(tables, table) + } + + return tables +} + +func getSSESpecification(r *parser.Resource) sam.SSESpecification { + if sse := r.GetProperty("SSESpecification"); sse.IsNotNil() { + return sam.SSESpecification{ + Metadata: sse.Metadata(), + Enabled: sse.GetBoolProperty("SSEEnabled"), + KMSMasterKeyID: sse.GetStringProperty("KMSMasterKeyId"), + } + } + + return sam.SSESpecification{ + Metadata: r.Metadata(), + Enabled: iacTypes.BoolDefault(false, r.Metadata()), + KMSMasterKeyID: iacTypes.StringDefault("", r.Metadata()), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/sns/sns.go b/pkg/iac/adapters/cloudformation/aws/sns/sns.go new file mode 100644 index 000000000000..4264077bca57 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sns/sns.go @@ -0,0 +1,13 @@ +package sns + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sns" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a SNS instance +func Adapt(cfFile parser.FileContext) sns.SNS { + return sns.SNS{ + Topics: getTopics(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/sns/sns_test.go b/pkg/iac/adapters/cloudformation/aws/sns/sns_test.go new file mode 100644 index 000000000000..25f271db2073 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sns/sns_test.go @@ -0,0 +1,54 @@ +package sns + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sns" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected sns.SNS + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + MySNSTopic: + Type: AWS::SNS::Topic + Properties: + KmsMasterKeyId: mykey +`, + expected: sns.SNS{ + Topics: []sns.Topic{ + { + Encryption: sns.Encryption{ + KMSKeyID: types.StringTest("mykey"), + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MySNSTopic: + Type: AWS::SNS::Topic + `, + expected: sns.SNS{ + Topics: []sns.Topic{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/sns/topic.go b/pkg/iac/adapters/cloudformation/aws/sns/topic.go new file mode 100644 index 000000000000..59c5672bea82 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sns/topic.go @@ -0,0 +1,24 @@ +package sns + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sns" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getTopics(ctx parser.FileContext) (topics []sns.Topic) { + for _, r := range ctx.GetResourcesByType("AWS::SNS::Topic") { + + topic := sns.Topic{ + Metadata: r.Metadata(), + ARN: types.StringDefault("", r.Metadata()), + Encryption: sns.Encryption{ + Metadata: r.Metadata(), + KMSKeyID: r.GetStringProperty("KmsMasterKeyId"), + }, + } + + topics = append(topics, topic) + } + return topics +} diff --git a/pkg/iac/adapters/cloudformation/aws/sqs/queue.go b/pkg/iac/adapters/cloudformation/aws/sqs/queue.go new file mode 100644 index 000000000000..a260fa00e08c --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sqs/queue.go @@ -0,0 +1,62 @@ +package sqs + +import ( + "errors" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sqs" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func getQueues(ctx parser.FileContext) (queues []sqs.Queue) { + for _, r := range ctx.GetResourcesByType("AWS::SQS::Queue") { + queue := sqs.Queue{ + Metadata: r.Metadata(), + QueueURL: iacTypes.StringDefault("", r.Metadata()), + Encryption: sqs.Encryption{ + Metadata: r.Metadata(), + ManagedEncryption: iacTypes.Bool(false, r.Metadata()), + KMSKeyID: r.GetStringProperty("KmsMasterKeyId"), + }, + } + if policy, err := getPolicy(r.ID(), ctx); err == nil { + queue.Policies = append(queue.Policies, *policy) + } + queues = append(queues, queue) + } + return queues +} + +func getPolicy(id string, ctx parser.FileContext) (*iam.Policy, error) { + for _, policyResource := range ctx.GetResourcesByType("AWS::SQS::QueuePolicy") { + documentProp := policyResource.GetProperty("PolicyDocument") + if documentProp.IsNil() { + continue + } + queuesProp := policyResource.GetProperty("Queues") + if queuesProp.IsNil() { + continue + } + for _, queueRef := range queuesProp.AsList() { + if queueRef.IsString() && queueRef.AsString() == id { + raw := documentProp.GetJsonBytes() + parsed, err := iamgo.Parse(raw) + if err != nil { + continue + } + return &iam.Policy{ + Metadata: documentProp.Metadata(), + Name: iacTypes.StringDefault("", documentProp.Metadata()), + Document: iam.Document{ + Metadata: documentProp.Metadata(), + Parsed: *parsed, + }, + Builtin: iacTypes.Bool(false, documentProp.Metadata()), + }, nil + } + } + } + return nil, errors.New("no matching policy found") +} diff --git a/pkg/iac/adapters/cloudformation/aws/sqs/sqs.go b/pkg/iac/adapters/cloudformation/aws/sqs/sqs.go new file mode 100644 index 000000000000..4f01ba6861b0 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sqs/sqs.go @@ -0,0 +1,13 @@ +package sqs + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sqs" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an SQS instance +func Adapt(cfFile parser.FileContext) sqs.SQS { + return sqs.SQS{ + Queues: getQueues(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/sqs/sqs_test.go b/pkg/iac/adapters/cloudformation/aws/sqs/sqs_test.go new file mode 100644 index 000000000000..634eddea9ab4 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/sqs/sqs_test.go @@ -0,0 +1,86 @@ +package sqs + +import ( + "testing" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sqs" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected sqs.SQS + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + MyQueue: + Type: AWS::SQS::Queue + Properties: + QueueName: "SampleQueue" + KmsMasterKeyId: mykey + SampleSQSPolicy: + Type: AWS::SQS::QueuePolicy + Properties: + Queues: + - !Ref MyQueue + PolicyDocument: + Statement: + - + Action: + - "SQS:SendMessage" + Effect: "Allow" + Resource: "arn:aws:sqs:us-east-2:444455556666:queue2" +`, + expected: sqs.SQS{ + Queues: []sqs.Queue{ + { + Encryption: sqs.Encryption{ + KMSKeyID: types.StringTest("mykey"), + }, + Policies: []iam.Policy{ + { + Document: func() iam.Document { + return iam.Document{ + Parsed: iamgo.NewPolicyBuilder(). + WithStatement( + iamgo.NewStatementBuilder(). + WithEffect("Allow"). + WithActions([]string{"SQS:SendMessage"}). + WithResources([]string{"arn:aws:sqs:us-east-2:444455556666:queue2"}). + Build(), + ). + Build(), + } + }(), + }, + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MySNSTopic: + Type: AWS::SQS::Queue + `, + expected: sqs.SQS{ + Queues: []sqs.Queue{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/ssm/secret.go b/pkg/iac/adapters/cloudformation/aws/ssm/secret.go new file mode 100644 index 000000000000..ae3a43ee28f7 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ssm/secret.go @@ -0,0 +1,18 @@ +package ssm + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ssm" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getSecrets(ctx parser.FileContext) (secrets []ssm.Secret) { + for _, r := range ctx.GetResourcesByType("AWS::SecretsManager::Secret") { + secret := ssm.Secret{ + Metadata: r.Metadata(), + KMSKeyID: r.GetStringProperty("KmsKeyId"), + } + + secrets = append(secrets, secret) + } + return secrets +} diff --git a/pkg/iac/adapters/cloudformation/aws/ssm/ssm.go b/pkg/iac/adapters/cloudformation/aws/ssm/ssm.go new file mode 100644 index 000000000000..88bc5485fe33 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ssm/ssm.go @@ -0,0 +1,13 @@ +package ssm + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ssm" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts an SSM instance +func Adapt(cfFile parser.FileContext) ssm.SSM { + return ssm.SSM{ + Secrets: getSecrets(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/ssm/ssm_test.go b/pkg/iac/adapters/cloudformation/aws/ssm/ssm_test.go new file mode 100644 index 000000000000..9709207fec66 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/ssm/ssm_test.go @@ -0,0 +1,53 @@ +package ssm + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ssm" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected ssm.SSM + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + MySecretA: + Type: 'AWS::SecretsManager::Secret' + Properties: + Name: MySecretForAppA + KmsKeyId: alias/exampleAlias +`, + expected: ssm.SSM{ + Secrets: []ssm.Secret{ + { + KMSKeyID: types.StringTest("alias/exampleAlias"), + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MySecretA: + Type: 'AWS::SecretsManager::Secret' + `, + expected: ssm.SSM{ + Secrets: []ssm.Secret{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/workspaces/workspace.go b/pkg/iac/adapters/cloudformation/aws/workspaces/workspace.go new file mode 100644 index 000000000000..3966468849cc --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/workspaces/workspace.go @@ -0,0 +1,31 @@ +package workspaces + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/workspaces" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +func getWorkSpaces(ctx parser.FileContext) (workSpaces []workspaces.WorkSpace) { + for _, r := range ctx.GetResourcesByType("AWS::WorkSpaces::Workspace") { + workspace := workspaces.WorkSpace{ + Metadata: r.Metadata(), + RootVolume: workspaces.Volume{ + Metadata: r.Metadata(), + Encryption: workspaces.Encryption{ + Metadata: r.Metadata(), + Enabled: r.GetBoolProperty("RootVolumeEncryptionEnabled"), + }, + }, + UserVolume: workspaces.Volume{ + Metadata: r.Metadata(), + Encryption: workspaces.Encryption{ + Metadata: r.Metadata(), + Enabled: r.GetBoolProperty("UserVolumeEncryptionEnabled"), + }, + }, + } + + workSpaces = append(workSpaces, workspace) + } + return workSpaces +} diff --git a/pkg/iac/adapters/cloudformation/aws/workspaces/workspaces.go b/pkg/iac/adapters/cloudformation/aws/workspaces/workspaces.go new file mode 100644 index 000000000000..578fa0d507c3 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/workspaces/workspaces.go @@ -0,0 +1,13 @@ +package workspaces + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/workspaces" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +// Adapt adapts a Workspaces instance +func Adapt(cfFile parser.FileContext) workspaces.WorkSpaces { + return workspaces.WorkSpaces{ + WorkSpaces: getWorkSpaces(cfFile), + } +} diff --git a/pkg/iac/adapters/cloudformation/aws/workspaces/workspaces_test.go b/pkg/iac/adapters/cloudformation/aws/workspaces/workspaces_test.go new file mode 100644 index 000000000000..41e821e6466d --- /dev/null +++ b/pkg/iac/adapters/cloudformation/aws/workspaces/workspaces_test.go @@ -0,0 +1,62 @@ +package workspaces + +import ( + "testing" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation/testutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/workspaces" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected workspaces.WorkSpaces + }{ + { + name: "complete", + source: `AWSTemplateFormatVersion: '2010-09-09' +Resources: + MyWorkSpace: + Type: AWS::WorkSpaces::Workspace + Properties: + RootVolumeEncryptionEnabled: true + UserVolumeEncryptionEnabled: true +`, + expected: workspaces.WorkSpaces{ + WorkSpaces: []workspaces.WorkSpace{ + { + RootVolume: workspaces.Volume{ + Encryption: workspaces.Encryption{ + Enabled: types.BoolTest(true), + }, + }, + UserVolume: workspaces.Volume{ + Encryption: workspaces.Encryption{ + Enabled: types.BoolTest(true), + }, + }, + }, + }, + }, + }, + { + name: "empty", + source: `AWSTemplateFormatVersion: 2010-09-09 +Resources: + MyWorkSpace: + Type: AWS::WorkSpaces::Workspace + `, + expected: workspaces.WorkSpaces{ + WorkSpaces: []workspaces.WorkSpace{{}}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testutil.AdaptAndCompare(t, tt.source, tt.expected, Adapt) + }) + } +} diff --git a/pkg/iac/adapters/cloudformation/testutil/testutil.go b/pkg/iac/adapters/cloudformation/testutil/testutil.go new file mode 100644 index 000000000000..b441afab7ee3 --- /dev/null +++ b/pkg/iac/adapters/cloudformation/testutil/testutil.go @@ -0,0 +1,24 @@ +package testutil + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" +) + +type adaptFn[T any] func(fctx parser.FileContext) T + +func AdaptAndCompare[T any](t *testing.T, source string, expected any, fn adaptFn[T]) { + fsys := testutil.CreateFS(map[string]string{ + "main.yaml": source, + }) + + fctx, err := parser.New().ParseFile(t.Context(), fsys, "main.yaml") + require.NoError(t, err) + + adapted := fn(*fctx) + testutil.AssertDefsecEqual(t, expected, adapted) +} diff --git a/pkg/iac/adapters/common/network.go b/pkg/iac/adapters/common/network.go new file mode 100644 index 000000000000..2f289505f14c --- /dev/null +++ b/pkg/iac/adapters/common/network.go @@ -0,0 +1,78 @@ +package common + +import ( + "strconv" + "strings" + + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type PortRange struct { + Metadata iacTypes.Metadata + Start iacTypes.IntValue + End iacTypes.IntValue +} + +func NewPortRange(start, end int, meta iacTypes.Metadata) PortRange { + return PortRange{ + Metadata: meta, + Start: iacTypes.Int(start, meta), + End: iacTypes.Int(end, meta), + } +} + +func FullPortRange(meta iacTypes.Metadata) PortRange { + return NewPortRange(0, 65535, meta) +} + +func InvalidPortRange(meta iacTypes.Metadata) PortRange { + return NewPortRange(-1, -1, meta) +} + +func (r PortRange) Valid() bool { + return !r.Start.EqualTo(-1) && !r.End.EqualTo(-1) +} + +type parseConfig struct { + allowWildcard bool +} + +type ParseOption func(*parseConfig) + +func WithWildcard() ParseOption { + return func(cfg *parseConfig) { + cfg.allowWildcard = true + } +} + +func ParsePortRange(input string, meta iacTypes.Metadata, opts ...ParseOption) PortRange { + cfg := &parseConfig{} + for _, opt := range opts { + opt(cfg) + } + + input = strings.TrimSpace(input) + + switch { + case input == "*" && cfg.allowWildcard: + return FullPortRange(meta) + case strings.Contains(input, "-"): + parts := strings.SplitN(input, "-", 2) + if len(parts) != 2 { + return InvalidPortRange(meta) + } + start, err1 := strconv.Atoi(strings.TrimSpace(parts[0])) + end, err2 := strconv.Atoi(strings.TrimSpace(parts[1])) + if err1 != nil || err2 != nil { + return InvalidPortRange(meta) + } + return NewPortRange(start, end, meta) + + default: + val, err := strconv.Atoi(input) + if err != nil { + return InvalidPortRange(meta) + } + return NewPortRange(val, val, meta) + } +} diff --git a/pkg/iac/adapters/common/network_test.go b/pkg/iac/adapters/common/network_test.go new file mode 100644 index 000000000000..47caa11ddd99 --- /dev/null +++ b/pkg/iac/adapters/common/network_test.go @@ -0,0 +1,93 @@ +package common_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/common" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestParsePortRange(t *testing.T) { + tests := []struct { + name string + input string + options []common.ParseOption + expected common.PortRange + valid bool + }{ + { + name: "single port", + input: "80", + expected: common.PortRange{ + Start: iacTypes.IntTest(80), + End: iacTypes.IntTest(80), + }, + valid: true, + }, + { + name: "port range", + input: "1000-2000", + expected: common.PortRange{ + Start: iacTypes.IntTest(1000), + End: iacTypes.IntTest(2000), + }, + valid: true, + }, + { + name: "port range with spaces", + input: " 22 - 80 ", + expected: common.PortRange{ + Start: iacTypes.IntTest(22), + End: iacTypes.IntTest(80), + }, + valid: true, + }, + { + name: "wildcard allowed", + input: "*", + options: []common.ParseOption{common.WithWildcard()}, + expected: common.PortRange{ + Start: iacTypes.IntTest(0), + End: iacTypes.IntTest(65535), + }, + valid: true, + }, + { + name: "wildcard disallowed", + input: "*", + valid: false, + }, + { + name: "invalid string", + input: "abc", + valid: false, + }, + { + name: "incomplete range", + input: "80-", + valid: false, + }, + { + name: "double dash", + input: "--", + valid: false, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + meta := iacTypes.NewTestMetadata() + pr := common.ParsePortRange(tc.input, meta, tc.options...) + + if tc.valid { + assert.True(t, pr.Valid()) + tc.expected.Metadata = meta + assert.Equal(t, tc.expected, pr) + } else { + assert.False(t, pr.Valid()) + } + }) + } +} diff --git a/pkg/iac/adapters/terraform/adapt.go b/pkg/iac/adapters/terraform/adapt.go new file mode 100644 index 000000000000..9028e7cc15f2 --- /dev/null +++ b/pkg/iac/adapters/terraform/adapt.go @@ -0,0 +1,31 @@ +package terraform + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/azure" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/cloudstack" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/digitalocean" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/github" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/google" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/kubernetes" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/nifcloud" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/openstack" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/oracle" + "github.com/aquasecurity/trivy/pkg/iac/state" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) *state.State { + return &state.State{ + AWS: aws.Adapt(modules), + Azure: azure.Adapt(modules), + CloudStack: cloudstack.Adapt(modules), + DigitalOcean: digitalocean.Adapt(modules), + GitHub: github.Adapt(modules), + Google: google.Adapt(modules), + Kubernetes: kubernetes.Adapt(modules), + Nifcloud: nifcloud.Adapt(modules), + OpenStack: openstack.Adapt(modules), + Oracle: oracle.Adapt(modules), + } +} diff --git a/pkg/iac/adapters/terraform/aws/accessanalyzer/accessanalyzer.go b/pkg/iac/adapters/terraform/aws/accessanalyzer/accessanalyzer.go new file mode 100644 index 000000000000..93c76f979d55 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/accessanalyzer/accessanalyzer.go @@ -0,0 +1,40 @@ +package accessanalyzer + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/accessanalyzer" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) accessanalyzer.AccessAnalyzer { + return accessanalyzer.AccessAnalyzer{ + Analyzers: adaptTrails(modules), + } +} + +func adaptTrails(modules terraform.Modules) []accessanalyzer.Analyzer { + var analyzer []accessanalyzer.Analyzer + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_accessanalyzer_analyzer") { + analyzer = append(analyzer, adaptAnalyzers(resource)) + } + } + return analyzer +} + +func adaptAnalyzers(resource *terraform.Block) accessanalyzer.Analyzer { + + analyzerName := resource.GetAttribute("analyzer_name") + analyzerNameAttr := analyzerName.AsStringValueOrDefault("", resource) + + arnAnalyzer := resource.GetAttribute("arn") + arnAnalyzerAttr := arnAnalyzer.AsStringValueOrDefault("", resource) + + return accessanalyzer.Analyzer{ + Metadata: resource.GetMetadata(), + Name: analyzerNameAttr, + ARN: arnAnalyzerAttr, + Active: types.BoolDefault(false, resource.GetMetadata()), + } +} diff --git a/pkg/iac/adapters/terraform/aws/adapt.go b/pkg/iac/adapters/terraform/aws/adapt.go new file mode 100644 index 000000000000..c9d9d94ecea6 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/adapt.go @@ -0,0 +1,79 @@ +package aws + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/apigateway" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/athena" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/cloudfront" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/cloudtrail" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/cloudwatch" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/codebuild" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/config" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/documentdb" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/dynamodb" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/ecr" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/ecs" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/efs" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/eks" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/elasticache" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/elasticsearch" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/elb" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/emr" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/kinesis" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/kms" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/lambda" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/mq" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/msk" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/neptune" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/provider" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/rds" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/redshift" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/sns" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/sqs" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/ssm" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/workspaces" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) aws.AWS { + return aws.AWS{ + Meta: aws.Meta{ + TFProviders: provider.Adapt(modules), + }, + APIGateway: apigateway.Adapt(modules), + Athena: athena.Adapt(modules), + Cloudfront: cloudfront.Adapt(modules), + CloudTrail: cloudtrail.Adapt(modules), + CloudWatch: cloudwatch.Adapt(modules), + CodeBuild: codebuild.Adapt(modules), + Config: config.Adapt(modules), + DocumentDB: documentdb.Adapt(modules), + DynamoDB: dynamodb.Adapt(modules), + EC2: ec2.Adapt(modules), + ECR: ecr.Adapt(modules), + ECS: ecs.Adapt(modules), + EFS: efs.Adapt(modules), + EKS: eks.Adapt(modules), + ElastiCache: elasticache.Adapt(modules), + Elasticsearch: elasticsearch.Adapt(modules), + ELB: elb.Adapt(modules), + EMR: emr.Adapt(modules), + IAM: iam.Adapt(modules), + Kinesis: kinesis.Adapt(modules), + KMS: kms.Adapt(modules), + Lambda: lambda.Adapt(modules), + MQ: mq.Adapt(modules), + MSK: msk.Adapt(modules), + Neptune: neptune.Adapt(modules), + RDS: rds.Adapt(modules), + Redshift: redshift.Adapt(modules), + S3: s3.Adapt(modules), + SNS: sns.Adapt(modules), + SQS: sqs.Adapt(modules), + SSM: ssm.Adapt(modules), + WorkSpaces: workspaces.Adapt(modules), + } +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/adapt.go b/pkg/iac/adapters/terraform/aws/apigateway/adapt.go new file mode 100644 index 000000000000..bf0e4ca86379 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/adapt.go @@ -0,0 +1,21 @@ +package apigateway + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway" + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) apigateway.APIGateway { + return apigateway.APIGateway{ + V1: v1.APIGateway{ + APIs: adaptAPIsV1(modules), + DomainNames: adaptDomainNamesV1(modules), + }, + V2: v2.APIGateway{ + APIs: adaptAPIsV2(modules), + DomainNames: adaptDomainNamesV2(modules), + }, + } +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/adapt_test.go b/pkg/iac/adapters/terraform/aws/apigateway/adapt_test.go new file mode 100644 index 000000000000..6bffcbae4d00 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/adapt_test.go @@ -0,0 +1,235 @@ +package apigateway + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway" + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected apigateway.APIGateway + }{ + { + name: "basic", + terraform: ` +resource "aws_api_gateway_rest_api" "MyDemoAPI" { + name = "MyDemoAPI" + description = "This is my API for demonstration purposes" +} +resource "aws_api_gateway_resource" "example" { + rest_api_id = aws_api_gateway_rest_api.MyDemoAPI.id +} +resource "aws_api_gateway_method" "example" { + rest_api_id = aws_api_gateway_rest_api.MyDemoAPI.id + resource_id = aws_api_gateway_resource.example.id + http_method = "GET" + authorization = "NONE" +} +resource "aws_apigatewayv2_api" "example" { + name = "tfsec" + protocol_type = "HTTP" +} + + +resource "aws_apigatewayv2_stage" "example" { + api_id = aws_apigatewayv2_api.example.id + name = "tfsec" + access_log_settings { + destination_arn = "arn:123" + } +} + +resource "aws_api_gateway_domain_name" "example" { + domain_name = "v1.com" + security_policy = "TLS_1_0" +} + +resource "aws_apigatewayv2_domain_name" "example" { + domain_name = "v2.com" + domain_name_configuration { + security_policy = "TLS_1_2" + } +} +`, + expected: apigateway.APIGateway{ + V1: v1.APIGateway{ + APIs: []v1.API{ + { + Metadata: iacTypes.Metadata{}, + Name: String("MyDemoAPI"), + Resources: []v1.Resource{ + { + Methods: []v1.Method{ + { + HTTPMethod: String(http.MethodGet), + AuthorizationType: String("NONE"), + APIKeyRequired: Bool(false), + }, + }, + }, + }, + }, + }, + DomainNames: []v1.DomainName{ + { + Name: String("v1.com"), + SecurityPolicy: String("TLS_1_0"), + }, + }, + }, + V2: v2.APIGateway{ + APIs: []v2.API{ + { + Name: String("tfsec"), + ProtocolType: String("HTTP"), + Stages: []v2.Stage{ + { + Name: String("tfsec"), + AccessLogging: v2.AccessLogging{ + CloudwatchLogGroupARN: String("arn:123"), + }, + }, + }, + }, + }, + DomainNames: []v2.DomainName{ + { + Name: String("v2.com"), + SecurityPolicy: String("TLS_1_2"), + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Int(i int) iacTypes.IntValue { + return iacTypes.IntTest(i) +} + +func Bool(b bool) iacTypes.BoolValue { + return iacTypes.BoolTest(b) +} + +func String(s string) iacTypes.StringValue { + return iacTypes.StringTest(s) +} +func TestLines(t *testing.T) { + src := ` + resource "aws_api_gateway_rest_api" "MyDemoAPI" { + name = "MyDemoAPI" + description = "This is my API for demonstration purposes" + } + + resource "aws_api_gateway_resource" "example" { + rest_api_id = aws_api_gateway_rest_api.MyDemoAPI.id + } + + resource "aws_api_gateway_method" "example" { + rest_api_id = aws_api_gateway_rest_api.MyDemoAPI.id + resource_id = aws_api_gateway_resource.example.id + http_method = "GET" + authorization = "NONE" + api_key_required = true + } + + resource "aws_apigatewayv2_api" "example" { + name = "tfsec" + protocol_type = "HTTP" + } + + resource "aws_apigatewayv2_stage" "example" { + api_id = aws_apigatewayv2_api.example.id + name = "tfsec" + access_log_settings { + destination_arn = "arn:123" + } + } + + resource "aws_api_gateway_domain_name" "example" { + domain_name = "v1.com" + security_policy = "TLS_1_0" + } + + ` + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.V1.APIs, 1) + require.Len(t, adapted.V2.APIs, 1) + require.Len(t, adapted.V1.DomainNames, 1) + + apiV1 := adapted.V1.APIs[0] + apiV2 := adapted.V2.APIs[0] + domainName := adapted.V1.DomainNames[0] + + assert.Equal(t, 2, apiV1.Metadata.Range().GetStartLine()) + assert.Equal(t, 5, apiV1.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, apiV1.Name.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, apiV1.Name.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, apiV1.Resources[0].Methods[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 17, apiV1.Resources[0].Methods[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 14, apiV1.Resources[0].Methods[0].HTTPMethod.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 14, apiV1.Resources[0].Methods[0].HTTPMethod.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 15, apiV1.Resources[0].Methods[0].AuthorizationType.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 15, apiV1.Resources[0].Methods[0].AuthorizationType.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 16, apiV1.Resources[0].Methods[0].APIKeyRequired.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 16, apiV1.Resources[0].Methods[0].APIKeyRequired.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 19, apiV2.Metadata.Range().GetStartLine()) + assert.Equal(t, 22, apiV2.Metadata.Range().GetEndLine()) + + assert.Equal(t, 20, apiV2.Name.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 20, apiV2.Name.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 21, apiV2.ProtocolType.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 21, apiV2.ProtocolType.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 24, apiV2.Stages[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 30, apiV2.Stages[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 26, apiV2.Stages[0].Name.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 26, apiV2.Stages[0].Name.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 27, apiV2.Stages[0].AccessLogging.Metadata.Range().GetStartLine()) + assert.Equal(t, 29, apiV2.Stages[0].AccessLogging.Metadata.Range().GetEndLine()) + + assert.Equal(t, 28, apiV2.Stages[0].AccessLogging.CloudwatchLogGroupARN.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 28, apiV2.Stages[0].AccessLogging.CloudwatchLogGroupARN.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 32, domainName.Metadata.Range().GetStartLine()) + assert.Equal(t, 35, domainName.Metadata.Range().GetEndLine()) + + assert.Equal(t, 33, domainName.Name.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 33, domainName.Name.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 34, domainName.SecurityPolicy.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 34, domainName.SecurityPolicy.GetMetadata().Range().GetEndLine()) + +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/apiv1.go b/pkg/iac/adapters/terraform/aws/apigateway/apiv1.go new file mode 100644 index 000000000000..3f03817b9e1b --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/apiv1.go @@ -0,0 +1,115 @@ +package apigateway + +import ( + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptAPIResourcesV1(modules terraform.Modules, apiBlock *terraform.Block) []v1.Resource { + var resources []v1.Resource + for _, resourceBlock := range modules.GetReferencingResources(apiBlock, "aws_api_gateway_resource", "rest_api_id") { + method := v1.Resource{ + Metadata: resourceBlock.GetMetadata(), + Methods: adaptAPIMethodsV1(modules, resourceBlock), + } + resources = append(resources, method) + } + return resources +} + +func adaptAPIMethodsV1(modules terraform.Modules, resourceBlock *terraform.Block) []v1.Method { + var methods []v1.Method + for _, methodBlock := range modules.GetReferencingResources(resourceBlock, "aws_api_gateway_method", "resource_id") { + method := v1.Method{ + Metadata: methodBlock.GetMetadata(), + HTTPMethod: methodBlock.GetAttribute("http_method").AsStringValueOrDefault("", methodBlock), + AuthorizationType: methodBlock.GetAttribute("authorization").AsStringValueOrDefault("", methodBlock), + APIKeyRequired: methodBlock.GetAttribute("api_key_required").AsBoolValueOrDefault(false, methodBlock), + } + methods = append(methods, method) + } + return methods +} + +func adaptAPIsV1(modules terraform.Modules) []v1.API { + + var apis []v1.API + apiStageIDs := modules.GetChildResourceIDMapByType("aws_api_gateway_stage") + + for _, apiBlock := range modules.GetResourcesByType("aws_api_gateway_rest_api") { + api := v1.API{ + Metadata: apiBlock.GetMetadata(), + Name: apiBlock.GetAttribute("name").AsStringValueOrDefault("", apiBlock), + Stages: nil, + Resources: adaptAPIResourcesV1(modules, apiBlock), + } + + for _, stageBlock := range modules.GetReferencingResources(apiBlock, "aws_api_gateway_stage", "rest_api_id") { + apiStageIDs.Resolve(stageBlock.ID()) + stage := adaptStageV1(stageBlock, modules) + + api.Stages = append(api.Stages, stage) + } + + apis = append(apis, api) + } + + orphanResources := modules.GetResourceByIDs(apiStageIDs.Orphans()...) + + if len(orphanResources) > 0 { + orphanage := v1.API{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Name: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + } + for _, stage := range orphanResources { + orphanage.Stages = append(orphanage.Stages, adaptStageV1(stage, modules)) + } + apis = append(apis, orphanage) + } + + return apis +} + +func adaptStageV1(stageBlock *terraform.Block, modules terraform.Modules) v1.Stage { + stage := v1.Stage{ + Metadata: stageBlock.GetMetadata(), + Name: stageBlock.GetAttribute("name").AsStringValueOrDefault("", stageBlock), + AccessLogging: v1.AccessLogging{ + Metadata: stageBlock.GetMetadata(), + CloudwatchLogGroupARN: iacTypes.StringDefault("", stageBlock.GetMetadata()), + }, + XRayTracingEnabled: stageBlock.GetAttribute("xray_tracing_enabled").AsBoolValueOrDefault(false, stageBlock), + } + for _, methodSettings := range modules.GetReferencingResources(stageBlock, "aws_api_gateway_method_settings", "stage_name") { + + restMethodSettings := v1.RESTMethodSettings{ + Metadata: methodSettings.GetMetadata(), + Method: iacTypes.String("", methodSettings.GetMetadata()), + CacheDataEncrypted: iacTypes.BoolDefault(false, methodSettings.GetMetadata()), + CacheEnabled: iacTypes.BoolDefault(false, methodSettings.GetMetadata()), + } + + if settings := methodSettings.GetBlock("settings"); settings.IsNotNil() { + if encrypted := settings.GetAttribute("cache_data_encrypted"); encrypted.IsNotNil() { + restMethodSettings.CacheDataEncrypted = settings.GetAttribute("cache_data_encrypted").AsBoolValueOrDefault(false, settings) + } + if encrypted := settings.GetAttribute("caching_enabled"); encrypted.IsNotNil() { + restMethodSettings.CacheEnabled = settings.GetAttribute("caching_enabled").AsBoolValueOrDefault(false, settings) + } + } + + stage.RESTMethodSettings = append(stage.RESTMethodSettings, restMethodSettings) + } + + stage.Name = stageBlock.GetAttribute("stage_name").AsStringValueOrDefault("", stageBlock) + if accessLogging := stageBlock.GetBlock("access_log_settings"); accessLogging.IsNotNil() { + stage.AccessLogging.Metadata = accessLogging.GetMetadata() + stage.AccessLogging.CloudwatchLogGroupARN = accessLogging.GetAttribute("destination_arn").AsStringValueOrDefault("", accessLogging) + } else { + stage.AccessLogging.Metadata = stageBlock.GetMetadata() + stage.AccessLogging.CloudwatchLogGroupARN = iacTypes.StringDefault("", stageBlock.GetMetadata()) + } + + return stage +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/apiv1_test.go b/pkg/iac/adapters/terraform/aws/apigateway/apiv1_test.go new file mode 100644 index 000000000000..9932ac864e7b --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/apiv1_test.go @@ -0,0 +1,126 @@ +package apigateway + +import ( + "net/http" + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" +) + +func Test_adaptAPIMethodsV1(t *testing.T) { + tests := []struct { + name string + terraform string + expected []v1.Method + }{ + { + name: "defaults", + terraform: ` +resource "aws_api_gateway_rest_api" "MyDemoAPI" { + name = "MyDemoAPI" + description = "This is my API for demonstration purposes" +} + +resource "aws_api_gateway_resource" "example" { + rest_api_id = aws_api_gateway_rest_api.MyDemoAPI.id +} + +resource "aws_api_gateway_method" "example" { + rest_api_id = aws_api_gateway_rest_api.MyDemoAPI.id + resource_id = aws_api_gateway_resource.example.id + http_method = "GET" + authorization = "NONE" +} +`, + expected: []v1.Method{ + { + HTTPMethod: String(http.MethodGet), + AuthorizationType: String("NONE"), + APIKeyRequired: Bool(false), + }, + }, + }, + { + name: "basic", + terraform: ` +resource "aws_api_gateway_rest_api" "MyDemoAPI" { + name = "MyDemoAPI" + description = "This is my API for demonstration purposes" +} + +resource "aws_api_gateway_resource" "example" { + rest_api_id = aws_api_gateway_rest_api.MyDemoAPI.id +} + +resource "aws_api_gateway_method" "example" { + rest_api_id = aws_api_gateway_rest_api.MyDemoAPI.id + resource_id = aws_api_gateway_resource.example.id + http_method = "GET" + authorization = "NONE" + api_key_required = true +} +`, + expected: []v1.Method{ + { + HTTPMethod: String(http.MethodGet), + AuthorizationType: String("NONE"), + APIKeyRequired: Bool(true), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + restApiBlock := modules.GetBlocks()[1] + adapted := adaptAPIMethodsV1(modules, restApiBlock) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptAPIsV1(t *testing.T) { + tests := []struct { + name string + terraform string + expected []v1.API + }{ + { + name: "defaults", + terraform: ` +resource "aws_api_gateway_rest_api" "example" { + +} +`, + expected: []v1.API{ + { + Name: String(""), + }, + }, + }, + { + name: "full", + terraform: ` +resource "aws_api_gateway_rest_api" "example" { + name = "tfsec" +} +`, + expected: []v1.API{ + { + Name: String("tfsec"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptAPIsV1(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/apiv2.go b/pkg/iac/adapters/terraform/aws/apigateway/apiv2.go new file mode 100644 index 000000000000..8a6d12679802 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/apiv2.go @@ -0,0 +1,69 @@ +package apigateway + +import ( + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptAPIsV2(modules terraform.Modules) []v2.API { + + var apis []v2.API + apiStageIDs := modules.GetChildResourceIDMapByType("aws_apigatewayv2_stage") + + for _, module := range modules { + for _, apiBlock := range module.GetResourcesByType("aws_apigatewayv2_api") { + api := v2.API{ + Metadata: apiBlock.GetMetadata(), + Name: apiBlock.GetAttribute("name").AsStringValueOrDefault("", apiBlock), + ProtocolType: apiBlock.GetAttribute("protocol_type").AsStringValueOrDefault("", apiBlock), + Stages: nil, + } + + for _, stageBlock := range module.GetReferencingResources(apiBlock, "aws_apigatewayv2_stage", "api_id") { + apiStageIDs.Resolve(stageBlock.ID()) + + stage := adaptStageV2(stageBlock) + + api.Stages = append(api.Stages, stage) + } + + apis = append(apis, api) + } + } + + orphanResources := modules.GetResourceByIDs(apiStageIDs.Orphans()...) + if len(orphanResources) > 0 { + orphanage := v2.API{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Name: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + ProtocolType: iacTypes.StringUnresolvable(iacTypes.NewUnmanagedMetadata()), + Stages: nil, + } + for _, stage := range orphanResources { + orphanage.Stages = append(orphanage.Stages, adaptStageV2(stage)) + } + apis = append(apis, orphanage) + } + + return apis +} + +func adaptStageV2(stageBlock *terraform.Block) v2.Stage { + stage := v2.Stage{ + Metadata: stageBlock.GetMetadata(), + Name: stageBlock.GetAttribute("name").AsStringValueOrDefault("", stageBlock), + AccessLogging: v2.AccessLogging{ + Metadata: stageBlock.GetMetadata(), + CloudwatchLogGroupARN: iacTypes.StringDefault("", stageBlock.GetMetadata()), + }, + } + if accessLogging := stageBlock.GetBlock("access_log_settings"); accessLogging.IsNotNil() { + stage.AccessLogging.Metadata = accessLogging.GetMetadata() + stage.AccessLogging.CloudwatchLogGroupARN = accessLogging.GetAttribute("destination_arn").AsStringValueOrDefault("", accessLogging) + } else { + stage.AccessLogging.Metadata = stageBlock.GetMetadata() + stage.AccessLogging.CloudwatchLogGroupARN = iacTypes.StringDefault("", stageBlock.GetMetadata()) + } + return stage +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/apiv2_test.go b/pkg/iac/adapters/terraform/aws/apigateway/apiv2_test.go new file mode 100644 index 000000000000..fa73981c80e3 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/apiv2_test.go @@ -0,0 +1,103 @@ +package apigateway + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" +) + +func Test_adaptAPIsV2(t *testing.T) { + tests := []struct { + name string + terraform string + expected []v2.API + }{ + { + name: "defaults", + terraform: ` +resource "aws_apigatewayv2_api" "example" { + protocol_type = "HTTP" +} +`, + expected: []v2.API{ + { + Name: String(""), + ProtocolType: String("HTTP"), + }, + }, + }, + { + name: "full", + terraform: ` +resource "aws_apigatewayv2_api" "example" { + name = "tfsec" + protocol_type = "HTTP" +} +`, + expected: []v2.API{ + { + Name: String("tfsec"), + ProtocolType: String("HTTP"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptAPIsV2(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptStageV2(t *testing.T) { + tests := []struct { + name string + terraform string + expected v2.Stage + }{ + { + name: "defaults", + terraform: ` +resource "aws_apigatewayv2_stage" "example" { + +} +`, + expected: v2.Stage{ + Name: String(""), + AccessLogging: v2.AccessLogging{ + CloudwatchLogGroupARN: String(""), + }, + }, + }, + { + name: "basics", + terraform: ` +resource "aws_apigatewayv2_stage" "example" { + name = "tfsec" + access_log_settings { + destination_arn = "arn:123" + } +} +`, + expected: v2.Stage{ + Name: String("tfsec"), + AccessLogging: v2.AccessLogging{ + CloudwatchLogGroupARN: String("arn:123"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptStageV2(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/namesv1.go b/pkg/iac/adapters/terraform/aws/apigateway/namesv1.go new file mode 100644 index 000000000000..7bc6af3edd4c --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/namesv1.go @@ -0,0 +1,24 @@ +package apigateway + +import ( + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func adaptDomainNamesV1(modules terraform.Modules) []v1.DomainName { + + var domainNames []v1.DomainName + + for _, module := range modules { + for _, nameBlock := range module.GetResourcesByType("aws_api_gateway_domain_name") { + domainName := v1.DomainName{ + Metadata: nameBlock.GetMetadata(), + Name: nameBlock.GetAttribute("domain_name").AsStringValueOrDefault("", nameBlock), + SecurityPolicy: nameBlock.GetAttribute("security_policy").AsStringValueOrDefault("TLS_1_0", nameBlock), + } + domainNames = append(domainNames, domainName) + } + } + + return domainNames +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/namesv1_test.go b/pkg/iac/adapters/terraform/aws/apigateway/namesv1_test.go new file mode 100644 index 000000000000..bff0ac47848e --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/namesv1_test.go @@ -0,0 +1,54 @@ +package apigateway + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" +) + +func Test_adaptDomainNamesV1(t *testing.T) { + tests := []struct { + name string + terraform string + expected []v1.DomainName + }{ + { + name: "defaults", + terraform: ` +resource "aws_api_gateway_domain_name" "example" { +} +`, + expected: []v1.DomainName{ + { + Name: String(""), + SecurityPolicy: String("TLS_1_0"), + }, + }, + }, + { + name: "basic", + terraform: ` +resource "aws_api_gateway_domain_name" "example" { + domain_name = "testing.com" + security_policy = "TLS_1_2" +} +`, + expected: []v1.DomainName{ + { + Name: String("testing.com"), + SecurityPolicy: String("TLS_1_2"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptDomainNamesV1(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/namesv2.go b/pkg/iac/adapters/terraform/aws/apigateway/namesv2.go new file mode 100644 index 000000000000..e7beb2d6ed2f --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/namesv2.go @@ -0,0 +1,28 @@ +package apigateway + +import ( + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptDomainNamesV2(modules terraform.Modules) []v2.DomainName { + + var domainNames []v2.DomainName + + for _, module := range modules { + for _, nameBlock := range module.GetResourcesByType("aws_apigatewayv2_domain_name") { + domainName := v2.DomainName{ + Metadata: nameBlock.GetMetadata(), + Name: nameBlock.GetAttribute("domain_name").AsStringValueOrDefault("", nameBlock), + SecurityPolicy: types.StringDefault("TLS_1_0", nameBlock.GetMetadata()), + } + if config := nameBlock.GetBlock("domain_name_configuration"); config.IsNotNil() { + domainName.SecurityPolicy = config.GetAttribute("security_policy").AsStringValueOrDefault("TLS_1_0", config) + } + domainNames = append(domainNames, domainName) + } + } + + return domainNames +} diff --git a/pkg/iac/adapters/terraform/aws/apigateway/namesv2_test.go b/pkg/iac/adapters/terraform/aws/apigateway/namesv2_test.go new file mode 100644 index 000000000000..25c40ebcc231 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/apigateway/namesv2_test.go @@ -0,0 +1,56 @@ +package apigateway + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" +) + +func Test_adaptDomainNamesV2(t *testing.T) { + tests := []struct { + name string + terraform string + expected []v2.DomainName + }{ + { + name: "defaults", + terraform: ` +resource "aws_apigatewayv2_domain_name" "example" { +} +`, + expected: []v2.DomainName{ + { + Name: String(""), + SecurityPolicy: String("TLS_1_0"), + }, + }, + }, + { + name: "fully populated", + terraform: ` +resource "aws_apigatewayv2_domain_name" "example" { + domain_name = "testing.com" + domain_name_configuration { + security_policy = "TLS_1_2" + } +} +`, + expected: []v2.DomainName{ + { + Name: String("testing.com"), + SecurityPolicy: String("TLS_1_2"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptDomainNamesV2(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/aws/athena/adapt.go b/pkg/iac/adapters/terraform/aws/athena/adapt.go new file mode 100644 index 000000000000..f8f56df99e87 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/athena/adapt.go @@ -0,0 +1,80 @@ +package athena + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/athena" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) athena.Athena { + return athena.Athena{ + Databases: adaptDatabases(modules), + Workgroups: adaptWorkgroups(modules), + } +} + +func adaptDatabases(modules terraform.Modules) []athena.Database { + var databases []athena.Database + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_athena_database") { + databases = append(databases, adaptDatabase(resource)) + } + } + return databases +} + +func adaptWorkgroups(modules terraform.Modules) []athena.Workgroup { + var workgroups []athena.Workgroup + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_athena_workgroup") { + workgroups = append(workgroups, adaptWorkgroup(resource)) + } + } + return workgroups +} + +func adaptDatabase(resource *terraform.Block) athena.Database { + database := athena.Database{ + Metadata: resource.GetMetadata(), + Name: resource.GetAttribute("name").AsStringValueOrDefault("", resource), + Encryption: athena.EncryptionConfiguration{ + Metadata: resource.GetMetadata(), + Type: iacTypes.StringDefault("", resource.GetMetadata()), + }, + } + if encryptionConfigBlock := resource.GetBlock("encryption_configuration"); encryptionConfigBlock.IsNotNil() { + database.Encryption.Metadata = encryptionConfigBlock.GetMetadata() + encryptionOptionAttr := encryptionConfigBlock.GetAttribute("encryption_option") + database.Encryption.Type = encryptionOptionAttr.AsStringValueOrDefault("", encryptionConfigBlock) + } + + return database +} + +func adaptWorkgroup(resource *terraform.Block) athena.Workgroup { + workgroup := athena.Workgroup{ + Metadata: resource.GetMetadata(), + Name: resource.GetAttribute("name").AsStringValueOrDefault("", resource), + Encryption: athena.EncryptionConfiguration{ + Metadata: resource.GetMetadata(), + Type: iacTypes.StringDefault("", resource.GetMetadata()), + }, + EnforceConfiguration: iacTypes.BoolDefault(false, resource.GetMetadata()), + } + + if configBlock := resource.GetBlock("configuration"); configBlock.IsNotNil() { + + enforceWGConfigAttr := configBlock.GetAttribute("enforce_workgroup_configuration") + workgroup.EnforceConfiguration = enforceWGConfigAttr.AsBoolValueOrDefault(true, configBlock) + + if resultConfigBlock := configBlock.GetBlock("result_configuration"); configBlock.IsNotNil() { + if encryptionConfigBlock := resultConfigBlock.GetBlock("encryption_configuration"); encryptionConfigBlock.IsNotNil() { + encryptionOptionAttr := encryptionConfigBlock.GetAttribute("encryption_option") + workgroup.Encryption.Metadata = encryptionConfigBlock.GetMetadata() + workgroup.Encryption.Type = encryptionOptionAttr.AsStringValueOrDefault("", encryptionConfigBlock) + } + } + } + + return workgroup +} diff --git a/pkg/iac/adapters/terraform/aws/athena/adapt_test.go b/pkg/iac/adapters/terraform/aws/athena/adapt_test.go new file mode 100644 index 000000000000..060c10b8dc05 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/athena/adapt_test.go @@ -0,0 +1,197 @@ +package athena + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/athena" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptDatabase(t *testing.T) { + tests := []struct { + name string + terraform string + expected athena.Database + }{ + { + name: "athena database", + terraform: ` + resource "aws_athena_database" "my_wg" { + name = "database_name" + + encryption_configuration { + encryption_option = "SSE_KMS" + } + } +`, + expected: athena.Database{ + Name: iacTypes.StringTest("database_name"), + Encryption: athena.EncryptionConfiguration{ + Type: iacTypes.StringTest(athena.EncryptionTypeSSEKMS), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptDatabase(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptWorkgroup(t *testing.T) { + tests := []struct { + name string + terraform string + expected athena.Workgroup + }{ + { + name: "encryption type SSE KMS", + terraform: ` + resource "aws_athena_workgroup" "my_wg" { + name = "example" + + configuration { + enforce_workgroup_configuration = true + + result_configuration { + encryption_configuration { + encryption_option = "SSE_KMS" + } + } + } + } +`, + expected: athena.Workgroup{ + Name: iacTypes.StringTest("example"), + Encryption: athena.EncryptionConfiguration{ + Type: iacTypes.StringTest(athena.EncryptionTypeSSEKMS), + }, + EnforceConfiguration: iacTypes.BoolTest(true), + }, + }, + { + name: "configuration not enforced", + terraform: ` + resource "aws_athena_workgroup" "my_wg" { + name = "example" + + configuration { + enforce_workgroup_configuration = false + + result_configuration { + encryption_configuration { + encryption_option = "SSE_KMS" + } + } + } + } +`, + expected: athena.Workgroup{ + Name: iacTypes.StringTest("example"), + Encryption: athena.EncryptionConfiguration{ + Type: iacTypes.StringTest(athena.EncryptionTypeSSEKMS), + }, + }, + }, + { + name: "enforce configuration defaults to true", + terraform: ` + resource "aws_athena_workgroup" "my_wg" { + name = "example" + + configuration { + result_configuration { + encryption_configuration { + encryption_option = "" + } + } + } + } +`, + expected: athena.Workgroup{ + Name: iacTypes.StringTest("example"), + Encryption: athena.EncryptionConfiguration{ + Type: iacTypes.StringTest(athena.EncryptionTypeNone), + }, + EnforceConfiguration: iacTypes.BoolTest(true), + }, + }, + { + name: "missing configuration block", + terraform: ` + resource "aws_athena_workgroup" "my_wg" { + name = "example" + } +`, + expected: athena.Workgroup{ + Name: iacTypes.StringTest("example"), + Encryption: athena.EncryptionConfiguration{ + Type: iacTypes.StringTest(athena.EncryptionTypeNone), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptWorkgroup(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_athena_database" "good_example" { + name = "database_name" + bucket = aws_s3_bucket.hoge.bucket + + encryption_configuration { + encryption_option = "SSE_KMS" + kms_key_arn = aws_kms_key.example.arn + } + } + + resource "aws_athena_workgroup" "good_example" { + name = "example" + + configuration { + enforce_workgroup_configuration = true + publish_cloudwatch_metrics_enabled = true + + result_configuration { + output_location = "s3://${aws_s3_bucket.example.bucket}/output/" + + encryption_configuration { + encryption_option = "SSE_KMS" + kms_key_arn = aws_kms_key.example.arn + } + } + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Databases, 1) + require.Len(t, adapted.Workgroups, 1) + + assert.Equal(t, 7, adapted.Databases[0].Encryption.Type.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, adapted.Databases[0].Encryption.Type.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 16, adapted.Workgroups[0].EnforceConfiguration.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 16, adapted.Workgroups[0].EnforceConfiguration.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 23, adapted.Workgroups[0].Encryption.Type.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 23, adapted.Workgroups[0].Encryption.Type.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/cloudfront/adapt.go b/pkg/iac/adapters/terraform/aws/cloudfront/adapt.go new file mode 100644 index 000000000000..721ecf991d55 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/cloudfront/adapt.go @@ -0,0 +1,79 @@ +package cloudfront + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudfront" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) cloudfront.Cloudfront { + return cloudfront.Cloudfront{ + Distributions: adaptDistributions(modules), + } +} + +func adaptDistributions(modules terraform.Modules) []cloudfront.Distribution { + var distributions []cloudfront.Distribution + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_cloudfront_distribution") { + distributions = append(distributions, adaptDistribution(resource)) + } + } + return distributions +} + +func adaptDistribution(resource *terraform.Block) cloudfront.Distribution { + + distribution := cloudfront.Distribution{ + Metadata: resource.GetMetadata(), + WAFID: types.StringDefault("", resource.GetMetadata()), + Logging: cloudfront.Logging{ + Metadata: resource.GetMetadata(), + Bucket: types.StringDefault("", resource.GetMetadata()), + }, + DefaultCacheBehaviour: cloudfront.CacheBehaviour{ + Metadata: resource.GetMetadata(), + ViewerProtocolPolicy: types.StringDefault("", resource.GetMetadata()), + }, + OrdererCacheBehaviours: nil, + ViewerCertificate: cloudfront.ViewerCertificate{ + Metadata: resource.GetMetadata(), + MinimumProtocolVersion: types.StringDefault("TLSv1", resource.GetMetadata()), + }, + } + + distribution.WAFID = resource.GetAttribute("web_acl_id").AsStringValueOrDefault("", resource) + + if loggingBlock := resource.GetBlock("logging_config"); loggingBlock.IsNotNil() { + distribution.Logging.Metadata = loggingBlock.GetMetadata() + bucketAttr := loggingBlock.GetAttribute("bucket") + distribution.Logging.Bucket = bucketAttr.AsStringValueOrDefault("", loggingBlock) + } + + if defaultCacheBlock := resource.GetBlock("default_cache_behavior"); defaultCacheBlock.IsNotNil() { + distribution.DefaultCacheBehaviour.Metadata = defaultCacheBlock.GetMetadata() + viewerProtocolPolicyAttr := defaultCacheBlock.GetAttribute("viewer_protocol_policy") + distribution.DefaultCacheBehaviour.ViewerProtocolPolicy = viewerProtocolPolicyAttr.AsStringValueOrDefault("", defaultCacheBlock) + } + + orderedCacheBlocks := resource.GetBlocks("ordered_cache_behavior") + for _, orderedCacheBlock := range orderedCacheBlocks { + viewerProtocolPolicyAttr := orderedCacheBlock.GetAttribute("viewer_protocol_policy") + viewerProtocolPolicyVal := viewerProtocolPolicyAttr.AsStringValueOrDefault("", orderedCacheBlock) + distribution.OrdererCacheBehaviours = append(distribution.OrdererCacheBehaviours, cloudfront.CacheBehaviour{ + Metadata: orderedCacheBlock.GetMetadata(), + ViewerProtocolPolicy: viewerProtocolPolicyVal, + }) + } + + if viewerCertBlock := resource.GetBlock("viewer_certificate"); viewerCertBlock.IsNotNil() { + distribution.ViewerCertificate = cloudfront.ViewerCertificate{ + Metadata: viewerCertBlock.GetMetadata(), + MinimumProtocolVersion: viewerCertBlock.GetAttribute("minimum_protocol_version").AsStringValueOrDefault("TLSv1", viewerCertBlock), + SSLSupportMethod: viewerCertBlock.GetAttribute("ssl_support_method").AsStringValueOrDefault("", viewerCertBlock), + CloudfrontDefaultCertificate: viewerCertBlock.GetAttribute("cloudfront_default_certificate").AsBoolValueOrDefault(false, viewerCertBlock), + } + } + + return distribution +} diff --git a/pkg/iac/adapters/terraform/aws/cloudfront/adapt_test.go b/pkg/iac/adapters/terraform/aws/cloudfront/adapt_test.go new file mode 100644 index 000000000000..0999f8b560fd --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/cloudfront/adapt_test.go @@ -0,0 +1,147 @@ +package cloudfront + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudfront" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptDistribution(t *testing.T) { + tests := []struct { + name string + terraform string + expected cloudfront.Distribution + }{ + { + name: "configured", + terraform: ` + resource "aws_cloudfront_distribution" "example" { + logging_config { + bucket = "mylogs.s3.amazonaws.com" + } + + web_acl_id = "waf_id" + + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + + ordered_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + + viewer_certificate { + cloudfront_default_certificate = true + minimum_protocol_version = "TLSv1.2_2021" + ssl_support_method = "sni-only" + } + } +`, + expected: cloudfront.Distribution{ + WAFID: iacTypes.StringTest("waf_id"), + Logging: cloudfront.Logging{ + Bucket: iacTypes.StringTest("mylogs.s3.amazonaws.com"), + }, + DefaultCacheBehaviour: cloudfront.CacheBehaviour{ + ViewerProtocolPolicy: iacTypes.StringTest("redirect-to-https"), + }, + OrdererCacheBehaviours: []cloudfront.CacheBehaviour{ + { + ViewerProtocolPolicy: iacTypes.StringTest("redirect-to-https"), + }, + }, + ViewerCertificate: cloudfront.ViewerCertificate{ + MinimumProtocolVersion: iacTypes.StringTest("TLSv1.2_2021"), + CloudfrontDefaultCertificate: iacTypes.BoolTest(true), + SSLSupportMethod: iacTypes.StringTest("sni-only"), + }, + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_cloudfront_distribution" "example" { + } +`, + expected: cloudfront.Distribution{ + Logging: cloudfront.Logging{}, + DefaultCacheBehaviour: cloudfront.CacheBehaviour{}, + + ViewerCertificate: cloudfront.ViewerCertificate{ + MinimumProtocolVersion: iacTypes.StringTest("TLSv1"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptDistribution(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_cloudfront_distribution" "example" { + logging_config { + bucket = "mylogs.s3.amazonaws.com" + } + + web_acl_id = "waf_id" + + default_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + + ordered_cache_behavior { + viewer_protocol_policy = "redirect-to-https" + } + + viewer_certificate { + cloudfront_default_certificate = true + minimum_protocol_version = "TLSv1.2_2021" + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Distributions, 1) + distribution := adapted.Distributions[0] + + assert.Equal(t, 2, distribution.Metadata.Range().GetStartLine()) + assert.Equal(t, 21, distribution.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, distribution.Logging.Metadata.Range().GetStartLine()) + assert.Equal(t, 5, distribution.Logging.Metadata.Range().GetEndLine()) + + assert.Equal(t, 7, distribution.WAFID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, distribution.WAFID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 9, distribution.DefaultCacheBehaviour.Metadata.Range().GetStartLine()) + assert.Equal(t, 11, distribution.DefaultCacheBehaviour.Metadata.Range().GetEndLine()) + + assert.Equal(t, 10, distribution.DefaultCacheBehaviour.ViewerProtocolPolicy.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, distribution.DefaultCacheBehaviour.ViewerProtocolPolicy.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, distribution.OrdererCacheBehaviours[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 15, distribution.OrdererCacheBehaviours[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 14, distribution.OrdererCacheBehaviours[0].ViewerProtocolPolicy.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 14, distribution.OrdererCacheBehaviours[0].ViewerProtocolPolicy.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 17, distribution.ViewerCertificate.Metadata.Range().GetStartLine()) + assert.Equal(t, 20, distribution.ViewerCertificate.Metadata.Range().GetEndLine()) + + assert.Equal(t, 19, distribution.ViewerCertificate.MinimumProtocolVersion.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 19, distribution.ViewerCertificate.MinimumProtocolVersion.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/cloudtrail/adapt.go b/pkg/iac/adapters/terraform/aws/cloudtrail/adapt.go new file mode 100644 index 000000000000..22464d85f80c --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/cloudtrail/adapt.go @@ -0,0 +1,67 @@ +package cloudtrail + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudtrail" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) cloudtrail.CloudTrail { + return cloudtrail.CloudTrail{ + Trails: adaptTrails(modules), + } +} + +func adaptTrails(modules terraform.Modules) []cloudtrail.Trail { + var trails []cloudtrail.Trail + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_cloudtrail") { + trails = append(trails, adaptTrail(resource)) + } + } + return trails +} + +func adaptTrail(resource *terraform.Block) cloudtrail.Trail { + nameAttr := resource.GetAttribute("name") + nameVal := nameAttr.AsStringValueOrDefault("", resource) + + enableLogFileValidationAttr := resource.GetAttribute("enable_log_file_validation") + enableLogFileValidationVal := enableLogFileValidationAttr.AsBoolValueOrDefault(false, resource) + + isMultiRegionAttr := resource.GetAttribute("is_multi_region_trail") + isMultiRegionVal := isMultiRegionAttr.AsBoolValueOrDefault(false, resource) + + KMSKeyIDAttr := resource.GetAttribute("kms_key_id") + KMSKeyIDVal := KMSKeyIDAttr.AsStringValueOrDefault("", resource) + + var selectors []cloudtrail.EventSelector + for _, selBlock := range resource.GetBlocks("event_selector") { + var resources []cloudtrail.DataResource + for _, resBlock := range selBlock.GetBlocks("data_resource") { + resources = append(resources, cloudtrail.DataResource{ + Metadata: resBlock.GetMetadata(), + Type: resBlock.GetAttribute("type").AsStringValueOrDefault("", resBlock), + Values: resBlock.GetAttribute("values").AsStringValues(), + }) + } + selector := cloudtrail.EventSelector{ + Metadata: selBlock.GetMetadata(), + DataResources: resources, + ReadWriteType: selBlock.GetAttribute("read_write_type").AsStringValueOrDefault("All", selBlock), + } + selectors = append(selectors, selector) + } + + return cloudtrail.Trail{ + Metadata: resource.GetMetadata(), + Name: nameVal, + EnableLogFileValidation: enableLogFileValidationVal, + IsMultiRegion: isMultiRegionVal, + KMSKeyID: KMSKeyIDVal, + CloudWatchLogsLogGroupArn: resource.GetAttribute("cloud_watch_logs_group_arn").AsStringValueOrDefault("", resource), + IsLogging: resource.GetAttribute("enable_logging").AsBoolValueOrDefault(true, resource), + BucketName: resource.GetAttribute("s3_bucket_name").AsStringValueOrDefault("", resource), + EventSelectors: selectors, + } +} diff --git a/pkg/iac/adapters/terraform/aws/cloudtrail/adapt_test.go b/pkg/iac/adapters/terraform/aws/cloudtrail/adapt_test.go new file mode 100644 index 000000000000..665032902a9f --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/cloudtrail/adapt_test.go @@ -0,0 +1,95 @@ +package cloudtrail + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudtrail" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptTrail(t *testing.T) { + tests := []struct { + name string + terraform string + expected cloudtrail.Trail + }{ + { + name: "configured", + terraform: ` + resource "aws_cloudtrail" "example" { + name = "example" + is_multi_region_trail = true + + enable_log_file_validation = true + kms_key_id = "kms-key" + s3_bucket_name = "abcdefgh" + cloud_watch_logs_group_arn = "abc" + enable_logging = false + } +`, + expected: cloudtrail.Trail{ + Name: iacTypes.StringTest("example"), + EnableLogFileValidation: iacTypes.BoolTest(true), + IsMultiRegion: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("kms-key"), + CloudWatchLogsLogGroupArn: iacTypes.StringTest("abc"), + BucketName: iacTypes.StringTest("abcdefgh"), + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_cloudtrail" "example" { + } +`, + expected: cloudtrail.Trail{ + IsLogging: iacTypes.BoolTest(true), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptTrail(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_cloudtrail" "example" { + name = "example" + is_multi_region_trail = true + + enable_log_file_validation = true + kms_key_id = "kms-key" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Trails, 1) + trail := adapted.Trails[0] + + assert.Equal(t, 2, trail.Metadata.Range().GetStartLine()) + assert.Equal(t, 8, trail.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, trail.Name.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, trail.Name.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, trail.IsMultiRegion.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, trail.IsMultiRegion.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, trail.EnableLogFileValidation.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, trail.EnableLogFileValidation.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 7, trail.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, trail.KMSKeyID.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/cloudwatch/adapt.go b/pkg/iac/adapters/terraform/aws/cloudwatch/adapt.go new file mode 100644 index 000000000000..c0b47b7f8d3e --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/cloudwatch/adapt.go @@ -0,0 +1,47 @@ +package cloudwatch + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudwatch" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) cloudwatch.CloudWatch { + return cloudwatch.CloudWatch{ + LogGroups: adaptLogGroups(modules), + } +} + +func adaptLogGroups(modules terraform.Modules) []cloudwatch.LogGroup { + var logGroups []cloudwatch.LogGroup + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_cloudwatch_log_group") { + logGroups = append(logGroups, adaptLogGroup(resource, module)) + } + } + return logGroups +} + +func adaptLogGroup(resource *terraform.Block, module *terraform.Module) cloudwatch.LogGroup { + nameAttr := resource.GetAttribute("name") + nameVal := nameAttr.AsStringValueOrDefault("", resource) + + KMSKeyIDAttr := resource.GetAttribute("kms_key_id") + KMSKeyIDVal := KMSKeyIDAttr.AsStringValueOrDefault("", resource) + + if keyBlock, err := module.GetReferencedBlock(KMSKeyIDAttr, resource); err == nil { + KMSKeyIDVal = types.String(keyBlock.FullName(), keyBlock.GetMetadata()) + } + + retentionInDaysAttr := resource.GetAttribute("retention_in_days") + retentionInDaysVal := retentionInDaysAttr.AsIntValueOrDefault(0, resource) + + return cloudwatch.LogGroup{ + Metadata: resource.GetMetadata(), + Arn: types.StringDefault("", resource.GetMetadata()), + Name: nameVal, + KMSKeyID: KMSKeyIDVal, + RetentionInDays: retentionInDaysVal, + MetricFilters: nil, + } +} diff --git a/pkg/iac/adapters/terraform/aws/cloudwatch/adapt_test.go b/pkg/iac/adapters/terraform/aws/cloudwatch/adapt_test.go new file mode 100644 index 000000000000..ae9379b29d98 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/cloudwatch/adapt_test.go @@ -0,0 +1,102 @@ +package cloudwatch + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudwatch" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptLogGroups(t *testing.T) { + tests := []struct { + name string + terraform string + expected []cloudwatch.LogGroup + }{ + { + name: "key referencing block", + terraform: ` + resource "aws_cloudwatch_log_group" "my-group" { + name = "my-group" + kms_key_id = aws_kms_key.log_key.arn + } + + resource "aws_kms_key" "log_key" { + } +`, + expected: []cloudwatch.LogGroup{ + { + Name: iacTypes.StringTest("my-group"), + KMSKeyID: iacTypes.StringTest("aws_kms_key.log_key"), + }, + }, + }, + { + name: "key as string", + terraform: ` + resource "aws_cloudwatch_log_group" "my-group" { + name = "my-group" + kms_key_id = "key-as-string" + } +`, + expected: []cloudwatch.LogGroup{ + { + Name: iacTypes.StringTest("my-group"), + KMSKeyID: iacTypes.StringTest("key-as-string"), + }, + }, + }, + { + name: "missing key", + terraform: ` + resource "aws_cloudwatch_log_group" "my-group" { + name = "my-group" + retention_in_days = 3 + } +`, + expected: []cloudwatch.LogGroup{ + { + Name: iacTypes.StringTest("my-group"), + RetentionInDays: iacTypes.IntTest(3), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptLogGroups(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_cloudwatch_log_group" "my-group" { + name = "my-group" + kms_key_id = aws_kms_key.log_key.arn + retention_in_days = 3 + + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + require.Len(t, adapted.LogGroups, 1) + logGroup := adapted.LogGroups[0] + + assert.Equal(t, 3, logGroup.Name.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, logGroup.Name.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, logGroup.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, logGroup.KMSKeyID.GetMetadata().Range().GetStartLine()) + + assert.Equal(t, 5, logGroup.RetentionInDays.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, logGroup.RetentionInDays.GetMetadata().Range().GetStartLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/codebuild/adapt.go b/pkg/iac/adapters/terraform/aws/codebuild/adapt.go new file mode 100644 index 000000000000..b026151ef8c1 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/codebuild/adapt.go @@ -0,0 +1,66 @@ +package codebuild + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/codebuild" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) codebuild.CodeBuild { + return codebuild.CodeBuild{ + Projects: adaptProjects(modules), + } +} + +func adaptProjects(modules terraform.Modules) []codebuild.Project { + var projects []codebuild.Project + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_codebuild_project") { + projects = append(projects, adaptProject(resource)) + } + } + return projects +} + +func adaptProject(resource *terraform.Block) codebuild.Project { + + project := codebuild.Project{ + Metadata: resource.GetMetadata(), + ArtifactSettings: codebuild.ArtifactSettings{ + Metadata: resource.GetMetadata(), + EncryptionEnabled: types.BoolDefault(true, resource.GetMetadata()), + }, + SecondaryArtifactSettings: nil, + } + + var hasArtifacts bool + + if artifactsBlock := resource.GetBlock("artifacts"); artifactsBlock.IsNotNil() { + project.ArtifactSettings.Metadata = artifactsBlock.GetMetadata() + typeAttr := artifactsBlock.GetAttribute("type") + encryptionDisabledAttr := artifactsBlock.GetAttribute("encryption_disabled") + hasArtifacts = !typeAttr.Equals("NO_ARTIFACTS") + if encryptionDisabledAttr.IsTrue() && hasArtifacts { + project.ArtifactSettings.EncryptionEnabled = types.Bool(false, artifactsBlock.GetMetadata()) + } else { + project.ArtifactSettings.EncryptionEnabled = types.Bool(true, artifactsBlock.GetMetadata()) + } + } + + secondaryArtifactBlocks := resource.GetBlocks("secondary_artifacts") + for _, secondaryArtifactBlock := range secondaryArtifactBlocks { + + secondaryEncryptionEnabled := types.BoolDefault(true, secondaryArtifactBlock.GetMetadata()) + secondaryEncryptionDisabledAttr := secondaryArtifactBlock.GetAttribute("encryption_disabled") + if secondaryEncryptionDisabledAttr.IsTrue() && hasArtifacts { + secondaryEncryptionEnabled = types.Bool(false, secondaryArtifactBlock.GetMetadata()) + } + + project.SecondaryArtifactSettings = append(project.SecondaryArtifactSettings, codebuild.ArtifactSettings{ + Metadata: secondaryArtifactBlock.GetMetadata(), + EncryptionEnabled: secondaryEncryptionEnabled, + }) + } + + return project +} diff --git a/pkg/iac/adapters/terraform/aws/codebuild/adapt_test.go b/pkg/iac/adapters/terraform/aws/codebuild/adapt_test.go new file mode 100644 index 000000000000..b1d6350f5a3a --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/codebuild/adapt_test.go @@ -0,0 +1,106 @@ +package codebuild + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/codebuild" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptProject(t *testing.T) { + tests := []struct { + name string + terraform string + expected codebuild.Project + }{ + { + name: "configured", + terraform: ` + resource "aws_codebuild_project" "codebuild" { + + artifacts { + encryption_disabled = false + } + + secondary_artifacts { + encryption_disabled = false + } + secondary_artifacts { + encryption_disabled = true + } + } +`, + expected: codebuild.Project{ + ArtifactSettings: codebuild.ArtifactSettings{ + EncryptionEnabled: iacTypes.BoolTest(true), + }, + SecondaryArtifactSettings: []codebuild.ArtifactSettings{ + { + EncryptionEnabled: iacTypes.BoolTest(true), + }, + {}, + }, + }, + }, + { + name: "defaults - encryption enabled", + terraform: ` + resource "aws_codebuild_project" "codebuild" { + } +`, + expected: codebuild.Project{ + ArtifactSettings: codebuild.ArtifactSettings{ + EncryptionEnabled: iacTypes.BoolTest(true), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptProject(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_codebuild_project" "codebuild" { + artifacts { + encryption_disabled = false + } + + secondary_artifacts { + encryption_disabled = false + } + + secondary_artifacts { + encryption_disabled = true + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Projects, 1) + project := adapted.Projects[0] + + assert.Equal(t, 2, project.Metadata.Range().GetStartLine()) + assert.Equal(t, 14, project.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, project.ArtifactSettings.Metadata.Range().GetStartLine()) + assert.Equal(t, 5, project.ArtifactSettings.Metadata.Range().GetEndLine()) + + assert.Equal(t, 7, project.SecondaryArtifactSettings[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 9, project.SecondaryArtifactSettings[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 11, project.SecondaryArtifactSettings[1].Metadata.Range().GetStartLine()) + assert.Equal(t, 13, project.SecondaryArtifactSettings[1].Metadata.Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/config/adapt.go b/pkg/iac/adapters/terraform/aws/config/adapt.go new file mode 100644 index 000000000000..643fbd86720c --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/config/adapt.go @@ -0,0 +1,33 @@ +package config + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/config" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) config.Config { + return config.Config{ + ConfigurationAggregrator: adaptConfigurationAggregrator(modules), + } +} + +func adaptConfigurationAggregrator(modules terraform.Modules) config.ConfigurationAggregrator { + configurationAggregrator := config.ConfigurationAggregrator{ + Metadata: iacTypes.NewUnmanagedMetadata(), + SourceAllRegions: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + } + + for _, resource := range modules.GetResourcesByType("aws_config_configuration_aggregator") { + configurationAggregrator.Metadata = resource.GetMetadata() + aggregationBlock := resource.GetFirstMatchingBlock("account_aggregation_source", "organization_aggregation_source") + if aggregationBlock.IsNil() { + configurationAggregrator.SourceAllRegions = iacTypes.Bool(false, resource.GetMetadata()) + } else { + allRegionsAttr := aggregationBlock.GetAttribute("all_regions") + allRegionsVal := allRegionsAttr.AsBoolValueOrDefault(false, aggregationBlock) + configurationAggregrator.SourceAllRegions = allRegionsVal + } + } + return configurationAggregrator +} diff --git a/pkg/iac/adapters/terraform/aws/config/adapt_test.go b/pkg/iac/adapters/terraform/aws/config/adapt_test.go new file mode 100644 index 000000000000..2470f1fc9573 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/config/adapt_test.go @@ -0,0 +1,75 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/config" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptConfigurationAggregrator(t *testing.T) { + tests := []struct { + name string + terraform string + expected config.ConfigurationAggregrator + }{ + { + name: "configured", + terraform: ` + resource "aws_config_configuration_aggregator" "example" { + name = "example" + + account_aggregation_source { + account_ids = ["123456789012"] + all_regions = true + } + } +`, + expected: config.ConfigurationAggregrator{ + SourceAllRegions: iacTypes.BoolTest(true), + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_config_configuration_aggregator" "example" { + } +`, + expected: config.ConfigurationAggregrator{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptConfigurationAggregrator(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_config_configuration_aggregator" "example" { + name = "example" + + account_aggregation_source { + account_ids = ["123456789012"] + all_regions = true + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + aggregator := adapted.ConfigurationAggregrator + + assert.Equal(t, 2, aggregator.Metadata.Range().GetStartLine()) + assert.Equal(t, 9, aggregator.Metadata.Range().GetEndLine()) + + assert.Equal(t, 7, aggregator.SourceAllRegions.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, aggregator.SourceAllRegions.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/documentdb/adapt.go b/pkg/iac/adapters/terraform/aws/documentdb/adapt.go new file mode 100644 index 000000000000..5c10b5195c97 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/documentdb/adapt.go @@ -0,0 +1,63 @@ +package documentdb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/documentdb" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) documentdb.DocumentDB { + return documentdb.DocumentDB{ + Clusters: adaptClusters(modules), + } +} + +func adaptClusters(modules terraform.Modules) []documentdb.Cluster { + var clusters []documentdb.Cluster + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_docdb_cluster") { + clusters = append(clusters, adaptCluster(resource, module)) + } + } + return clusters +} + +func adaptCluster(resource *terraform.Block, module *terraform.Module) documentdb.Cluster { + identifierAttr := resource.GetAttribute("cluster_identifier") + identifierVal := identifierAttr.AsStringValueOrDefault("", resource) + + var enabledLogExports []types.StringValue + var instances []documentdb.Instance + + enabledLogExportsAttr := resource.GetAttribute("enabled_cloudwatch_logs_exports") + for _, logExport := range enabledLogExportsAttr.AsStringValues() { + enabledLogExports = append(enabledLogExports, logExport) + } + + instancesRes := module.GetReferencingResources(resource, "aws_docdb_cluster_instance", "cluster_identifier") + for _, instanceRes := range instancesRes { + keyIDAttr := instanceRes.GetAttribute("kms_key_id") + keyIDVal := keyIDAttr.AsStringValueOrDefault("", instanceRes) + + instances = append(instances, documentdb.Instance{ + Metadata: instanceRes.GetMetadata(), + KMSKeyID: keyIDVal, + }) + } + + storageEncryptedAttr := resource.GetAttribute("storage_encrypted") + storageEncryptedVal := storageEncryptedAttr.AsBoolValueOrDefault(false, resource) + + KMSKeyIDAttr := resource.GetAttribute("kms_key_id") + KMSKeyIDVal := KMSKeyIDAttr.AsStringValueOrDefault("", resource) + + return documentdb.Cluster{ + Metadata: resource.GetMetadata(), + Identifier: identifierVal, + EnabledLogExports: enabledLogExports, + BackupRetentionPeriod: resource.GetAttribute("backup_retention_period").AsIntValueOrDefault(0, resource), + Instances: instances, + StorageEncrypted: storageEncryptedVal, + KMSKeyID: KMSKeyIDVal, + } +} diff --git a/pkg/iac/adapters/terraform/aws/documentdb/adapt_test.go b/pkg/iac/adapters/terraform/aws/documentdb/adapt_test.go new file mode 100644 index 000000000000..16096b0cd06d --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/documentdb/adapt_test.go @@ -0,0 +1,116 @@ +package documentdb + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/documentdb" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptCluster(t *testing.T) { + tests := []struct { + name string + terraform string + expected documentdb.Cluster + }{ + { + name: "configured", + terraform: ` + resource "aws_docdb_cluster" "docdb" { + cluster_identifier = "my-docdb-cluster" + kms_key_id = "kms-key" + enabled_cloudwatch_logs_exports = "audit" + storage_encrypted = true + } + + resource "aws_docdb_cluster_instance" "cluster_instances" { + count = 1 + identifier = "my-docdb-cluster" + cluster_identifier = aws_docdb_cluster.docdb.id + kms_key_id = "kms-key#1" + } +`, + expected: documentdb.Cluster{ + Identifier: iacTypes.StringTest("my-docdb-cluster"), + KMSKeyID: iacTypes.StringTest("kms-key"), + EnabledLogExports: []iacTypes.StringValue{ + iacTypes.StringTest("audit"), + }, + Instances: []documentdb.Instance{ + { + KMSKeyID: iacTypes.StringTest("kms-key#1"), + }, + }, + StorageEncrypted: iacTypes.BoolTest(true), + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_docdb_cluster" "docdb" { + } +`, + expected: documentdb.Cluster{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptCluster(modules.GetBlocks()[0], modules[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_docdb_cluster" "docdb" { + cluster_identifier = "my-docdb-cluster" + kms_key_id = "kms-key" + enabled_cloudwatch_logs_exports = "audit" + storage_encrypted = true + } + + resource "aws_docdb_cluster_instance" "cluster_instances" { + count = 1 + identifier = "my-docdb-cluster" + cluster_identifier = aws_docdb_cluster.docdb.id + kms_key_id = "kms-key" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Clusters, 1) + require.Len(t, adapted.Clusters[0].Instances, 1) + + cluster := adapted.Clusters[0] + instance := cluster.Instances[0] + + assert.Equal(t, 2, cluster.Metadata.Range().GetStartLine()) + assert.Equal(t, 7, cluster.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, cluster.Identifier.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, cluster.Identifier.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, cluster.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, cluster.KMSKeyID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 5, cluster.EnabledLogExports[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, cluster.EnabledLogExports[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, cluster.StorageEncrypted.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, cluster.StorageEncrypted.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 9, instance.Metadata.Range().GetStartLine()) + assert.Equal(t, 14, instance.Metadata.Range().GetEndLine()) + + assert.Equal(t, 13, instance.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 13, instance.KMSKeyID.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/dynamodb/adapt.go b/pkg/iac/adapters/terraform/aws/dynamodb/adapt.go new file mode 100644 index 000000000000..833587927413 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/dynamodb/adapt.go @@ -0,0 +1,94 @@ +package dynamodb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/dynamodb" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) dynamodb.DynamoDB { + return dynamodb.DynamoDB{ + DAXClusters: adaptClusters(modules), + Tables: adaptTables(modules), + } +} + +func adaptClusters(modules terraform.Modules) []dynamodb.DAXCluster { + var clusters []dynamodb.DAXCluster + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_dax_cluster") { + clusters = append(clusters, adaptCluster(resource, module)) + } + } + return clusters +} + +func adaptTables(modules terraform.Modules) []dynamodb.Table { + var tables []dynamodb.Table + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_dynamodb_table") { + tables = append(tables, adaptTable(resource, module)) + } + } + return tables +} + +func adaptCluster(resource *terraform.Block, _ *terraform.Module) dynamodb.DAXCluster { + + cluster := dynamodb.DAXCluster{ + Metadata: resource.GetMetadata(), + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + KMSKeyID: iacTypes.StringDefault("", resource.GetMetadata()), + }, + PointInTimeRecovery: iacTypes.BoolDefault(false, resource.GetMetadata()), + } + + if ssEncryptionBlock := resource.GetBlock("server_side_encryption"); ssEncryptionBlock.IsNotNil() { + cluster.ServerSideEncryption.Metadata = ssEncryptionBlock.GetMetadata() + enabledAttr := ssEncryptionBlock.GetAttribute("enabled") + cluster.ServerSideEncryption.Enabled = enabledAttr.AsBoolValueOrDefault(false, ssEncryptionBlock) + } + + if recoveryBlock := resource.GetBlock("point_in_time_recovery"); recoveryBlock.IsNotNil() { + recoveryEnabledAttr := recoveryBlock.GetAttribute("enabled") + cluster.PointInTimeRecovery = recoveryEnabledAttr.AsBoolValueOrDefault(false, recoveryBlock) + } + + return cluster +} + +func adaptTable(resource *terraform.Block, module *terraform.Module) dynamodb.Table { + + table := dynamodb.Table{ + Metadata: resource.GetMetadata(), + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + KMSKeyID: iacTypes.StringDefault("", resource.GetMetadata()), + }, + PointInTimeRecovery: iacTypes.BoolDefault(false, resource.GetMetadata()), + } + + if ssEncryptionBlock := resource.GetBlock("server_side_encryption"); ssEncryptionBlock.IsNotNil() { + table.ServerSideEncryption.Metadata = ssEncryptionBlock.GetMetadata() + enabledAttr := ssEncryptionBlock.GetAttribute("enabled") + table.ServerSideEncryption.Enabled = enabledAttr.AsBoolValueOrDefault(false, ssEncryptionBlock) + + kmsKeyIdAttr := ssEncryptionBlock.GetAttribute("kms_key_arn") + table.ServerSideEncryption.KMSKeyID = kmsKeyIdAttr.AsStringValueOrDefault(dynamodb.DefaultKMSKeyID, ssEncryptionBlock) + + kmsBlock, err := module.GetReferencedBlock(kmsKeyIdAttr, resource) + if err == nil && kmsBlock.IsNotNil() { + table.ServerSideEncryption.KMSKeyID = iacTypes.String(kmsBlock.FullName(), kmsBlock.GetMetadata()) + } + } + + if recoveryBlock := resource.GetBlock("point_in_time_recovery"); recoveryBlock.IsNotNil() { + recoveryEnabledAttr := recoveryBlock.GetAttribute("enabled") + table.PointInTimeRecovery = recoveryEnabledAttr.AsBoolValueOrDefault(false, recoveryBlock) + } + + return table +} diff --git a/pkg/iac/adapters/terraform/aws/dynamodb/adapt_test.go b/pkg/iac/adapters/terraform/aws/dynamodb/adapt_test.go new file mode 100644 index 000000000000..daf44e897b4b --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/dynamodb/adapt_test.go @@ -0,0 +1,162 @@ +package dynamodb + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/dynamodb" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptCluster(t *testing.T) { + tests := []struct { + name string + terraform string + expected dynamodb.DAXCluster + }{ + { + name: "cluster", + terraform: ` + resource "aws_dax_cluster" "example" { + server_side_encryption { + enabled = true + } + } +`, + expected: dynamodb.DAXCluster{ + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Enabled: iacTypes.BoolTest(true), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptCluster(modules.GetBlocks()[0], modules[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptTable(t *testing.T) { + tests := []struct { + name string + terraform string + expected dynamodb.Table + }{ + { + name: "table", + terraform: ` + resource "aws_dynamodb_table" "example" { + name = "example" + + server_side_encryption { + enabled = true + kms_key_arn = "key-string" + } + + point_in_time_recovery { + enabled = true + } + } +`, + expected: dynamodb.Table{ + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Enabled: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("key-string"), + }, + PointInTimeRecovery: iacTypes.BoolTest(true), + }, + }, + { + name: "table no kms", + terraform: ` + resource "aws_dax_cluster" "example" { + server_side_encryption { + enabled = true + } + } +`, + expected: dynamodb.Table{ + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Enabled: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest(dynamodb.DefaultKMSKeyID), + }, + }, + }, + { + name: "reference key", + terraform: ` + resource "aws_dynamodb_table" "example" { + name = "example" + + server_side_encryption { + enabled = true + kms_key_arn = aws_kms_key.a.arn + } + } + + resource "aws_kms_key" "a" { + } +`, + expected: dynamodb.Table{ + ServerSideEncryption: dynamodb.ServerSideEncryption{ + Enabled: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("aws_kms_key.a"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptTable(modules.GetBlocks()[0], modules[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_dynamodb_table" "example" { + name = "example" + + server_side_encryption { + enabled = true + kms_key_arn = "key-string" + } + + point_in_time_recovery { + enabled = true + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Empty(t, adapted.DAXClusters) + require.Len(t, adapted.Tables, 1) + table := adapted.Tables[0] + + assert.Equal(t, 2, table.Metadata.Range().GetStartLine()) + assert.Equal(t, 13, table.Metadata.Range().GetEndLine()) + + assert.Equal(t, 5, table.ServerSideEncryption.Metadata.Range().GetStartLine()) + assert.Equal(t, 8, table.ServerSideEncryption.Metadata.Range().GetEndLine()) + + assert.Equal(t, 6, table.ServerSideEncryption.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, table.ServerSideEncryption.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 7, table.ServerSideEncryption.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, table.ServerSideEncryption.KMSKeyID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, table.PointInTimeRecovery.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, table.PointInTimeRecovery.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/ec2/adapt.go b/pkg/iac/adapters/terraform/aws/ec2/adapt.go new file mode 100644 index 000000000000..92420c4dcf39 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/ec2/adapt.go @@ -0,0 +1,103 @@ +package ec2 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) ec2.EC2 { + + naclAdapter := naclAdapter{naclRuleIDs: modules.GetChildResourceIDMapByType("aws_network_acl_rule")} + sgAdapter := sgAdapter{sgRuleIDs: modules.GetChildResourceIDMapByType("aws_security_group_rule")} + + return ec2.EC2{ + Instances: getInstances(modules), + VPCs: adaptVPCs(modules), + SecurityGroups: sgAdapter.adaptSecurityGroups(modules), + Subnets: adaptSubnets(modules), + NetworkACLs: naclAdapter.adaptNetworkACLs(modules), + LaunchConfigurations: adaptLaunchConfigurations(modules), + LaunchTemplates: adaptLaunchTemplates(modules), + Volumes: adaptVolumes(modules), + RequestedAMIs: adaptRequestedAMIs(modules), + } +} + +func getInstances(modules terraform.Modules) []ec2.Instance { + var instances []ec2.Instance + + blocks := modules.GetResourcesByType("aws_instance") + + for _, b := range blocks { + instance := ec2.Instance{ + Metadata: b.GetMetadata(), + MetadataOptions: getMetadataOptions(b), + UserData: b.GetAttribute("user_data").AsStringValueOrDefault("", b), + } + + if launchTemplate := findRelatedLaunchTemplate(modules, b); launchTemplate != nil { + instance = launchTemplate.Instance + } + + if instance.RootBlockDevice == nil { + instance.RootBlockDevice = &ec2.BlockDevice{ + Metadata: b.GetMetadata(), + Encrypted: types.BoolDefault(false, b.GetMetadata()), + } + } + + if rootBlockDevice := b.GetBlock("root_block_device"); rootBlockDevice.IsNotNil() { + instance.RootBlockDevice = &ec2.BlockDevice{ + Metadata: rootBlockDevice.GetMetadata(), + Encrypted: rootBlockDevice.GetAttribute("encrypted").AsBoolValueOrDefault(false, b), + } + } + + for _, ebsBlock := range b.GetBlocks("ebs_block_device") { + instance.EBSBlockDevices = append(instance.EBSBlockDevices, &ec2.BlockDevice{ + Metadata: ebsBlock.GetMetadata(), + Encrypted: ebsBlock.GetAttribute("encrypted").AsBoolValueOrDefault(false, b), + }) + } + + for _, resource := range modules.GetResourcesByType("aws_ebs_encryption_by_default") { + if !resource.GetAttribute("enabled").Equals(false) { + instance.RootBlockDevice.Encrypted = types.BoolDefault(true, resource.GetMetadata()) + for i := 0; i < len(instance.EBSBlockDevices); i++ { + ebs := instance.EBSBlockDevices[i] + ebs.Encrypted = types.BoolDefault(true, resource.GetMetadata()) + } + } + } + + instances = append(instances, instance) + } + + return instances +} + +func findRelatedLaunchTemplate(modules terraform.Modules, instanceBlock *terraform.Block) *ec2.LaunchTemplate { + launchTemplateBlock := instanceBlock.GetBlock("launch_template") + if launchTemplateBlock.IsNil() { + return nil + } + + templateRef := launchTemplateBlock.GetAttribute("name") + + if !templateRef.IsResolvable() { + templateRef = launchTemplateBlock.GetAttribute("id") + } + + if templateRef.IsString() { + for _, r := range modules.GetResourcesByType("aws_launch_template") { + templateName := r.GetAttribute("name").AsStringValueOrDefault("", r).Value() + if templateRef.Equals(r.ID()) || templateRef.Equals(templateName) { + launchTemplate := adaptLaunchTemplate(r) + return &launchTemplate + } + } + } + + return nil +} diff --git a/pkg/iac/adapters/terraform/aws/ec2/adapt_test.go b/pkg/iac/adapters/terraform/aws/ec2/adapt_test.go new file mode 100644 index 000000000000..a36b31601ae3 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/ec2/adapt_test.go @@ -0,0 +1,227 @@ +package ec2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected ec2.EC2 + }{ + { + name: "configured", + terraform: ` + resource "aws_instance" "example" { + ami = "ami-7f89a64f" + instance_type = "t1.micro" + + root_block_device { + encrypted = true + } + + metadata_options { + http_tokens = "required" + http_endpoint = "disabled" + } + + ebs_block_device { + encrypted = true + } + + user_data = < 0 { + orphanage := ec2.SecurityGroup{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Description: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + IngressRules: nil, + EgressRules: nil, + IsDefault: iacTypes.BoolUnresolvable(iacTypes.NewUnmanagedMetadata()), + VPCID: iacTypes.StringUnresolvable(iacTypes.NewUnmanagedMetadata()), + } + for _, sgRule := range orphanResources { + if sgRule.GetAttribute("type").Equals("ingress") { + orphanage.IngressRules = append(orphanage.IngressRules, adaptSGRule(sgRule)) + } else if sgRule.GetAttribute("type").Equals("egress") { + orphanage.EgressRules = append(orphanage.EgressRules, adaptSGRule(sgRule)) + } + } + securityGroups = append(securityGroups, orphanage) + } + + return securityGroups +} + +func (a *naclAdapter) adaptNetworkACLs(modules terraform.Modules) []ec2.NetworkACL { + var networkACLs []ec2.NetworkACL + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_network_acl") { + networkACLs = append(networkACLs, a.adaptNetworkACL(resource, module)) + } + } + + orphanResources := modules.GetResourceByIDs(a.naclRuleIDs.Orphans()...) + if len(orphanResources) > 0 { + orphanage := ec2.NetworkACL{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Rules: nil, + IsDefaultRule: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + } + for _, naclRule := range orphanResources { + orphanage.Rules = append(orphanage.Rules, adaptNetworkACLRule(naclRule)) + } + networkACLs = append(networkACLs, orphanage) + } + + return networkACLs +} + +func (a *sgAdapter) adaptSecurityGroup(resource *terraform.Block, module terraform.Modules) ec2.SecurityGroup { + var ingressRules []ec2.SecurityGroupRule + var egressRules []ec2.SecurityGroupRule + + descriptionAttr := resource.GetAttribute("description") + descriptionVal := descriptionAttr.AsStringValueOrDefault("Managed by Terraform", resource) + + ingressBlocks := resource.GetBlocks("ingress") + for _, ingressBlock := range ingressBlocks { + ingressRules = append(ingressRules, adaptSGRule(ingressBlock)) + } + + egressBlocks := resource.GetBlocks("egress") + for _, egressBlock := range egressBlocks { + egressRules = append(egressRules, adaptSGRule(egressBlock)) + } + + rulesBlocks := module.GetReferencingResources(resource, "aws_security_group_rule", "security_group_id") + for _, ruleBlock := range rulesBlocks { + a.sgRuleIDs.Resolve(ruleBlock.ID()) + if ruleBlock.GetAttribute("type").Equals("ingress") { + ingressRules = append(ingressRules, adaptSGRule(ruleBlock)) + } else if ruleBlock.GetAttribute("type").Equals("egress") { + egressRules = append(egressRules, adaptSGRule(ruleBlock)) + } + } + + for _, r := range module.GetReferencingResources(resource, "aws_vpc_security_group_ingress_rule", "security_group_id") { + a.sgRuleIDs.Resolve(r.ID()) + ingressRules = append(ingressRules, adaptSingleSGRule(r)) + } + + for _, r := range module.GetReferencingResources(resource, "aws_vpc_security_group_egress_rule", "security_group_id") { + a.sgRuleIDs.Resolve(r.ID()) + egressRules = append(egressRules, adaptSingleSGRule(r)) + } + + return ec2.SecurityGroup{ + Metadata: resource.GetMetadata(), + Description: descriptionVal, + IngressRules: ingressRules, + EgressRules: egressRules, + IsDefault: iacTypes.Bool(false, iacTypes.NewUnmanagedMetadata()), + VPCID: resource.GetAttribute("vpc_id").AsStringValueOrDefault("", resource), + } +} + +func adaptSGRule(resource *terraform.Block) ec2.SecurityGroupRule { + ruleDescAttr := resource.GetAttribute("description") + ruleDescVal := ruleDescAttr.AsStringValueOrDefault("", resource) + + var cidrs []iacTypes.StringValue + + cidrBlocks := resource.GetAttribute("cidr_blocks") + ipv6cidrBlocks := resource.GetAttribute("ipv6_cidr_blocks") + + if cidrBlocks.IsNotNil() { + cidrs = cidrBlocks.AsStringValues() + } + + if ipv6cidrBlocks.IsNotNil() { + cidrs = append(cidrs, ipv6cidrBlocks.AsStringValues()...) + } + + protocolAddr := resource.GetAttribute("protocol") + protocol := protocolAddr.AsStringValueOrDefault("", resource) + if protocolAddr.IsNumber() { + protocol = iacTypes.String(strconv.Itoa(int(protocolAddr.AsNumber())), protocolAddr.GetMetadata()) + } + + return ec2.SecurityGroupRule{ + Metadata: resource.GetMetadata(), + Description: ruleDescVal, + CIDRs: cidrs, + FromPort: resource.GetAttribute("from_port").AsIntValueOrDefault(-1, resource), + ToPort: resource.GetAttribute("to_port").AsIntValueOrDefault(-1, resource), + Protocol: protocol, + } +} + +func adaptSingleSGRule(resource *terraform.Block) ec2.SecurityGroupRule { + description := resource.GetAttribute("description").AsStringValueOrDefault("", resource) + + var cidrs []iacTypes.StringValue + if ipv4 := resource.GetAttribute("cidr_ipv4"); ipv4.IsNotNil() { + cidrs = append(cidrs, ipv4.AsStringValueOrDefault("", resource)) + } + if ipv6 := resource.GetAttribute("cidr_ipv6"); ipv6.IsNotNil() { + cidrs = append(cidrs, ipv6.AsStringValueOrDefault("", resource)) + } + + return ec2.SecurityGroupRule{ + Metadata: resource.GetMetadata(), + Description: description, + CIDRs: cidrs, + FromPort: resource.GetAttribute("from_port").AsIntValueOrDefault(-1, resource), + ToPort: resource.GetAttribute("to_port").AsIntValueOrDefault(-1, resource), + Protocol: resource.GetAttribute("ip_protocol").AsStringValueOrDefault("", resource), + } +} + +func (a *naclAdapter) adaptNetworkACL(resource *terraform.Block, module *terraform.Module) ec2.NetworkACL { + var networkRules []ec2.NetworkACLRule + rulesBlocks := module.GetReferencingResources(resource, "aws_network_acl_rule", "network_acl_id") + for _, ruleBlock := range rulesBlocks { + a.naclRuleIDs.Resolve(ruleBlock.ID()) + networkRules = append(networkRules, adaptNetworkACLRule(ruleBlock)) + } + return ec2.NetworkACL{ + Metadata: resource.GetMetadata(), + Rules: networkRules, + IsDefaultRule: iacTypes.BoolDefault(false, resource.GetMetadata()), + } +} + +func adaptNetworkACLRule(resource *terraform.Block) ec2.NetworkACLRule { + var cidrs []iacTypes.StringValue + + typeVal := iacTypes.StringDefault("ingress", resource.GetMetadata()) + + egressAtrr := resource.GetAttribute("egress") + if egressAtrr.IsTrue() { + typeVal = iacTypes.String("egress", egressAtrr.GetMetadata()) + } else if egressAtrr.IsNotNil() { + typeVal = iacTypes.String("ingress", egressAtrr.GetMetadata()) + } + + actionAttr := resource.GetAttribute("rule_action") + actionVal := actionAttr.AsStringValueOrDefault("", resource) + + protocolAtrr := resource.GetAttribute("protocol") + protocolVal := protocolAtrr.AsStringValueOrDefault("", resource) + + cidrAttr := resource.GetAttribute("cidr_block") + if cidrAttr.IsNotNil() { + cidrs = append(cidrs, cidrAttr.AsStringValueOrDefault("", resource)) + } + ipv4cidrAttr := resource.GetAttribute("ipv6_cidr_block") + if ipv4cidrAttr.IsNotNil() { + cidrs = append(cidrs, ipv4cidrAttr.AsStringValueOrDefault("", resource)) + } + + return ec2.NetworkACLRule{ + Metadata: resource.GetMetadata(), + Type: typeVal, + Action: actionVal, + Protocol: protocolVal, + CIDRs: cidrs, + FromPort: resource.GetAttribute("from_port").AsIntValueOrDefault(-1, resource), + ToPort: resource.GetAttribute("to_port").AsIntValueOrDefault(-1, resource), + } +} diff --git a/pkg/iac/adapters/terraform/aws/ec2/vpc_test.go b/pkg/iac/adapters/terraform/aws/ec2/vpc_test.go new file mode 100644 index 000000000000..f7ed8ddd388a --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/ec2/vpc_test.go @@ -0,0 +1,401 @@ +package ec2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_AdaptVPC(t *testing.T) { + tests := []struct { + name string + terraform string + expected ec2.EC2 + }{ + { + name: "defined", + terraform: `resource "aws_flow_log" "this" { + vpc_id = aws_vpc.main.id +} +resource "aws_default_vpc" "default" { + tags = { + Name = "Default VPC" + } +} + +resource "aws_vpc" "main" { + cidr_block = "4.5.6.7/32" +} + +resource "aws_security_group" "example" { + name = "http" + description = "Allow inbound HTTP traffic" + + ingress { + description = "Rule #1" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } + + egress { + cidr_blocks = ["1.2.3.4/32"] + } +} + +resource "aws_network_acl_rule" "example" { + egress = false + protocol = "tcp" + from_port = 22 + to_port = 22 + rule_action = "allow" + cidr_block = "10.0.0.0/16" +} + +resource "aws_security_group_rule" "example" { + type = "ingress" + description = "Rule #2" + security_group_id = aws_security_group.example.id + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [ + "1.2.3.4/32", + "4.5.6.7/32", + ] +} + +resource "aws_default_security_group" "default" { + vpc_id = aws_vpc.main.id + + ingress { + protocol = -1 + self = true + from_port = 0 + to_port = 0 + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} +`, + expected: ec2.EC2{ + VPCs: []ec2.VPC{ + { + IsDefault: iacTypes.BoolTest(true), + }, + { + FlowLogsEnabled: iacTypes.BoolTest(true), + }, + }, + SecurityGroups: []ec2.SecurityGroup{ + { + Description: iacTypes.StringTest("Allow inbound HTTP traffic"), + IngressRules: []ec2.SecurityGroupRule{ + { + + Description: iacTypes.StringTest("Rule #1"), + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest("4.5.6.7/32"), + }, + FromPort: iacTypes.IntTest(80), + ToPort: iacTypes.IntTest(80), + Protocol: iacTypes.StringTest("tcp"), + }, + { + + Description: iacTypes.StringTest("Rule #2"), + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest("1.2.3.4/32"), + iacTypes.StringTest("4.5.6.7/32"), + }, + FromPort: iacTypes.IntTest(22), + ToPort: iacTypes.IntTest(22), + Protocol: iacTypes.StringTest("tcp"), + }, + }, + + EgressRules: []ec2.SecurityGroupRule{ + { + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest("1.2.3.4/32"), + }, + FromPort: iacTypes.IntTest(-1), + ToPort: iacTypes.IntTest(-1), + }, + }, + }, + { + IsDefault: iacTypes.BoolTest(true), + IngressRules: []ec2.SecurityGroupRule{ + { + Protocol: iacTypes.StringTest("-1"), + }, + }, + EgressRules: []ec2.SecurityGroupRule{ + { + Protocol: iacTypes.StringTest("-1"), + CIDRs: []iacTypes.StringValue{iacTypes.StringTest("0.0.0.0/0")}, + }, + }, + }, + }, + NetworkACLs: []ec2.NetworkACL{ + { + Rules: []ec2.NetworkACLRule{ + { + Type: iacTypes.StringTest("ingress"), + Action: iacTypes.StringTest("allow"), + Protocol: iacTypes.StringTest("tcp"), + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest("10.0.0.0/16"), + }, + FromPort: iacTypes.IntTest(22), + ToPort: iacTypes.IntTest(22), + }, + }, + }, + }, + }, + }, + { + name: "defaults", + terraform: `resource "aws_security_group" "example" { + ingress { + } + + egress { + } +} + +resource "aws_network_acl_rule" "example" { +} +`, + expected: ec2.EC2{ + SecurityGroups: []ec2.SecurityGroup{ + { + Description: iacTypes.StringTest("Managed by Terraform"), + IngressRules: []ec2.SecurityGroupRule{ + { + FromPort: iacTypes.IntTest(-1), + ToPort: iacTypes.IntTest(-1), + }, + }, + + EgressRules: []ec2.SecurityGroupRule{ + { + FromPort: iacTypes.IntTest(-1), + ToPort: iacTypes.IntTest(-1), + }, + }, + }, + }, + NetworkACLs: []ec2.NetworkACL{ + { + Rules: []ec2.NetworkACLRule{ + { + Type: iacTypes.StringTest("ingress"), + FromPort: iacTypes.IntTest(-1), + ToPort: iacTypes.IntTest(-1), + }, + }, + }, + }, + }, + }, + { + name: "aws_flow_log refer to locals", + terraform: `locals { + vpc_id = try(aws_vpc.this.id, "") +} + +resource "aws_vpc" "this" { +} + +resource "aws_flow_log" "this" { + vpc_id = local.vpc_id +} +`, + expected: ec2.EC2{ + VPCs: []ec2.VPC{ + { + FlowLogsEnabled: iacTypes.BoolTest(true), + }, + }, + }, + }, + { + name: "ingress and egress rules", + terraform: `resource "aws_security_group" "example" { + name = "example" + description = "example" +} + +resource "aws_vpc_security_group_egress_rule" "test" { + security_group_id = aws_security_group.example.id + cidr_ipv4 = "0.0.0.0/0" + ip_protocol = "-1" # semantically equivalent to all ports +} + +resource "aws_vpc_security_group_ingress_rule" "test" { + security_group_id = aws_security_group.example.id + cidr_ipv4 = "0.0.0.0/0" + from_port = "22" + to_port = "22" + ip_protocol = "tcp" +} +`, + expected: ec2.EC2{ + SecurityGroups: []ec2.SecurityGroup{ + { + Description: iacTypes.StringTest("example"), + IngressRules: []ec2.SecurityGroupRule{ + { + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest("0.0.0.0/0"), + }, + Protocol: iacTypes.StringTest("tcp"), + FromPort: iacTypes.IntTest(22), + ToPort: iacTypes.IntTest(22), + }, + }, + EgressRules: []ec2.SecurityGroupRule{ + { + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest("0.0.0.0/0"), + }, + Protocol: iacTypes.StringTest("-1"), + FromPort: iacTypes.IntTest(-1), + ToPort: iacTypes.IntTest(-1), + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestVPCLines(t *testing.T) { + src := ` +resource "aws_default_vpc" "default" { +} + +resource "aws_vpc" "main" { + cidr_block = "4.5.6.7/32" +} + +resource "aws_security_group" "example" { + name = "http" + description = "Allow inbound HTTP traffic" + + ingress { + description = "HTTP from VPC" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = [aws_vpc.main.cidr_block] + } + + egress { + cidr_blocks = ["1.2.3.4/32"] + } +} + +resource "aws_security_group_rule" "example" { + type = "ingress" + security_group_id = aws_security_group.example.id + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = [ + "1.2.3.4/32", + "4.5.6.7/32", + ] +} + +resource "aws_network_acl_rule" "example" { + egress = false + protocol = "tcp" + from_port = 22 + to_port = 22 + rule_action = "allow" + cidr_block = "10.0.0.0/16" +} +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.VPCs, 2) + require.Len(t, adapted.SecurityGroups, 1) + require.Len(t, adapted.NetworkACLs, 1) + + defaultVPC := adapted.VPCs[0] + securityGroup := adapted.SecurityGroups[0] + networkACL := adapted.NetworkACLs[0] + + assert.Equal(t, 2, defaultVPC.Metadata.Range().GetStartLine()) + assert.Equal(t, 3, defaultVPC.Metadata.Range().GetEndLine()) + + assert.Equal(t, 9, securityGroup.Metadata.Range().GetStartLine()) + assert.Equal(t, 24, securityGroup.Metadata.Range().GetEndLine()) + + assert.Equal(t, 11, securityGroup.Description.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, securityGroup.Description.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, securityGroup.IngressRules[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 19, securityGroup.IngressRules[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 14, securityGroup.IngressRules[0].Description.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 14, securityGroup.IngressRules[0].Description.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 18, securityGroup.IngressRules[0].CIDRs[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 18, securityGroup.IngressRules[0].CIDRs[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 26, securityGroup.IngressRules[1].Metadata.Range().GetStartLine()) + assert.Equal(t, 36, securityGroup.IngressRules[1].Metadata.Range().GetEndLine()) + + assert.Equal(t, 32, securityGroup.IngressRules[1].CIDRs[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 35, securityGroup.IngressRules[1].CIDRs[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 21, securityGroup.EgressRules[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 23, securityGroup.EgressRules[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 22, securityGroup.EgressRules[0].CIDRs[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 22, securityGroup.EgressRules[0].CIDRs[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 38, networkACL.Rules[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 45, networkACL.Rules[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 39, networkACL.Rules[0].Type.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 39, networkACL.Rules[0].Type.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 40, networkACL.Rules[0].Protocol.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 40, networkACL.Rules[0].Protocol.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 43, networkACL.Rules[0].Action.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 43, networkACL.Rules[0].Action.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 44, networkACL.Rules[0].CIDRs[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 44, networkACL.Rules[0].CIDRs[0].GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/ecr/adapt.go b/pkg/iac/adapters/terraform/aws/ecr/adapt.go new file mode 100644 index 000000000000..90a66ba098e1 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/ecr/adapt.go @@ -0,0 +1,113 @@ +package ecr + +import ( + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecr" + iamp "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) ecr.ECR { + return ecr.ECR{ + Repositories: adaptRepositories(modules), + } +} + +func adaptRepositories(modules terraform.Modules) []ecr.Repository { + var repositories []ecr.Repository + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_ecr_repository") { + repositories = append(repositories, adaptRepository(resource, module, modules)) + } + } + return repositories +} + +func adaptRepository(resource *terraform.Block, module *terraform.Module, modules terraform.Modules) ecr.Repository { + repo := ecr.Repository{ + Metadata: resource.GetMetadata(), + ImageScanning: ecr.ImageScanning{ + Metadata: resource.GetMetadata(), + ScanOnPush: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + ImageTagsImmutable: iacTypes.BoolDefault(false, resource.GetMetadata()), + Policies: nil, + Encryption: ecr.Encryption{ + Metadata: resource.GetMetadata(), + Type: iacTypes.StringDefault("AES256", resource.GetMetadata()), + KMSKeyID: iacTypes.StringDefault("", resource.GetMetadata()), + }, + } + + if imageScanningBlock := resource.GetBlock("image_scanning_configuration"); imageScanningBlock.IsNotNil() { + repo.ImageScanning.Metadata = imageScanningBlock.GetMetadata() + scanOnPushAttr := imageScanningBlock.GetAttribute("scan_on_push") + repo.ImageScanning.ScanOnPush = scanOnPushAttr.AsBoolValueOrDefault(false, imageScanningBlock) + } + + mutabilityAttr := resource.GetAttribute("image_tag_mutability") + if mutabilityAttr.Equals("IMMUTABLE") { + repo.ImageTagsImmutable = iacTypes.Bool(true, mutabilityAttr.GetMetadata()) + } else if mutabilityAttr.Equals("MUTABLE") { + repo.ImageTagsImmutable = iacTypes.Bool(false, mutabilityAttr.GetMetadata()) + } + + policyBlocks := module.GetReferencingResources(resource, "aws_ecr_repository_policy", "repository") + for _, policyRes := range policyBlocks { + if policyAttr := policyRes.GetAttribute("policy"); policyAttr.IsString() { + + dataBlock, err := module.GetBlockByID(policyAttr.Value().AsString()) + if err != nil { + + parsed, err := iamgo.ParseString(policyAttr.Value().AsString()) + if err != nil { + continue + } + + policy := iamp.Policy{ + Metadata: policyRes.GetMetadata(), + Name: iacTypes.StringDefault("", policyRes.GetMetadata()), + Document: iamp.Document{ + Parsed: *parsed, + Metadata: policyAttr.GetMetadata(), + }, + Builtin: iacTypes.Bool(false, policyRes.GetMetadata()), + } + + repo.Policies = append(repo.Policies, policy) + } else if dataBlock.Type() == "data" && dataBlock.TypeLabel() == "aws_iam_policy_document" { + if doc, err := iam.ConvertTerraformDocument(modules, dataBlock); err == nil { + policy := iamp.Policy{ + Metadata: policyRes.GetMetadata(), + Name: iacTypes.StringDefault("", policyRes.GetMetadata()), + Document: iamp.Document{ + Parsed: doc.Document, + Metadata: doc.Source.GetMetadata(), + IsOffset: true, + }, + Builtin: iacTypes.Bool(false, policyRes.GetMetadata()), + } + repo.Policies = append(repo.Policies, policy) + } + } + } + } + + if encryptBlock := resource.GetBlock("encryption_configuration"); encryptBlock.IsNotNil() { + repo.Encryption.Metadata = encryptBlock.GetMetadata() + encryptionTypeAttr := encryptBlock.GetAttribute("encryption_type") + repo.Encryption.Type = encryptionTypeAttr.AsStringValueOrDefault("AES256", encryptBlock) + + kmsKeyAttr := encryptBlock.GetAttribute("kms_key") + repo.Encryption.KMSKeyID = kmsKeyAttr.AsStringValueOrDefault("", encryptBlock) + if kmsKeyAttr.IsResourceBlockReference("aws_kms_key") { + if keyBlock, err := module.GetReferencedBlock(kmsKeyAttr, encryptBlock); err == nil { + repo.Encryption.KMSKeyID = iacTypes.String(keyBlock.FullName(), keyBlock.GetMetadata()) + } + } + } + + return repo +} diff --git a/pkg/iac/adapters/terraform/aws/ecr/adapt_test.go b/pkg/iac/adapters/terraform/aws/ecr/adapt_test.go new file mode 100644 index 000000000000..b0b0a683e6e6 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/ecr/adapt_test.go @@ -0,0 +1,231 @@ +package ecr + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecr" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptRepository(t *testing.T) { + tests := []struct { + name string + terraform string + expected ecr.Repository + }{ + { + name: "configured", + terraform: ` + resource "aws_kms_key" "ecr_kms" { + enable_key_rotation = true + } + + resource "aws_ecr_repository" "foo" { + name = "bar" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } + + encryption_configuration { + encryption_type = "KMS" + kms_key = aws_kms_key.ecr_kms.key_id + } + } + + resource "aws_ecr_repository_policy" "foopolicy" { + repository = aws_ecr_repository.foo.name + + policy = < 0 { + var volumes []ecs.Volume + for _, volumeBlock := range volumeBlocks { + volumes = append(volumes, ecs.Volume{ + Metadata: volumeBlock.GetMetadata(), + EFSVolumeConfiguration: adaptEFSVolumeConfiguration(volumeBlock), + }) + } + return volumes + } + + return []ecs.Volume{} +} + +func adaptEFSVolumeConfiguration(volumeBlock *terraform.Block) ecs.EFSVolumeConfiguration { + EFSVolumeConfiguration := ecs.EFSVolumeConfiguration{ + Metadata: volumeBlock.GetMetadata(), + TransitEncryptionEnabled: types.BoolDefault(true, volumeBlock.GetMetadata()), + } + + if EFSConfigBlock := volumeBlock.GetBlock("efs_volume_configuration"); EFSConfigBlock.IsNotNil() { + EFSVolumeConfiguration.Metadata = EFSConfigBlock.GetMetadata() + transitEncryptionAttr := EFSConfigBlock.GetAttribute("transit_encryption") + EFSVolumeConfiguration.TransitEncryptionEnabled = types.Bool(transitEncryptionAttr.Equals("ENABLED"), EFSConfigBlock.GetMetadata()) + if transitEncryptionAttr.IsNotNil() { + EFSVolumeConfiguration.TransitEncryptionEnabled = types.Bool(transitEncryptionAttr.Equals("ENABLED"), transitEncryptionAttr.GetMetadata()) + } + } + + return EFSVolumeConfiguration +} diff --git a/pkg/iac/adapters/terraform/aws/ecs/adapt_test.go b/pkg/iac/adapters/terraform/aws/ecs/adapt_test.go new file mode 100644 index 000000000000..8ac74081ec02 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/ecs/adapt_test.go @@ -0,0 +1,241 @@ +package ecs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecs" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptClusterSettings(t *testing.T) { + tests := []struct { + name string + terraform string + expected ecs.ClusterSettings + }{ + { + name: "container insights enabled", + terraform: ` + resource "aws_ecs_cluster" "example" { + name = "services-cluster" + + setting { + name = "containerInsights" + value = "enabled" + } + } +`, + expected: ecs.ClusterSettings{ + ContainerInsightsEnabled: iacTypes.BoolTest(true), + }, + }, + { + name: "container insights enhanced", + terraform: ` + resource "aws_ecs_cluster" "example" { + name = "services-cluster" + + setting { + name = "containerInsights" + value = "enhanced" + } + } +`, + expected: ecs.ClusterSettings{ + ContainerInsightsEnabled: iacTypes.BoolTest(true), + }, + }, + { + name: "invalid name", + terraform: ` + resource "aws_ecs_cluster" "example" { + name = "services-cluster" + + setting { + name = "invalidName" + value = "enabled" + } + } +`, + expected: ecs.ClusterSettings{}, + }, + { + name: "defaults", + terraform: ` + resource "aws_ecs_cluster" "example" { + } +`, + expected: ecs.ClusterSettings{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptClusterSettings(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptTaskDefinitionResource(t *testing.T) { + tests := []struct { + name string + terraform string + expected ecs.TaskDefinition + }{ + { + name: "configured", + terraform: ` + resource "aws_ecs_task_definition" "example" { + family = "service" + container_definitions = < 0 { + orphanage := elb.LoadBalancer{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Type: iacTypes.StringDefault(elb.TypeApplication, iacTypes.NewUnmanagedMetadata()), + DropInvalidHeaderFields: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + Internal: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + Listeners: nil, + } + for _, listenerResource := range orphanResources { + orphanage.Listeners = append(orphanage.Listeners, adaptListener(listenerResource, "application")) + } + loadBalancers = append(loadBalancers, orphanage) + } + + return loadBalancers +} + +func (a *adapter) adaptLoadBalancer(resource *terraform.Block, module terraform.Modules) elb.LoadBalancer { + var listeners []elb.Listener + + typeAttr := resource.GetAttribute("load_balancer_type") + typeVal := typeAttr.AsStringValueOrDefault("application", resource) + + dropInvalidHeadersAttr := resource.GetAttribute("drop_invalid_header_fields") + dropInvalidHeadersVal := dropInvalidHeadersAttr.AsBoolValueOrDefault(false, resource) + + internalAttr := resource.GetAttribute("internal") + internalVal := internalAttr.AsBoolValueOrDefault(false, resource) + + listenerBlocks := module.GetReferencingResources(resource, "aws_lb_listener", "load_balancer_arn") + listenerBlocks = append(listenerBlocks, module.GetReferencingResources(resource, "aws_alb_listener", "load_balancer_arn")...) + + for _, listenerBlock := range listenerBlocks { + a.listenerIDs.Resolve(listenerBlock.ID()) + listeners = append(listeners, adaptListener(listenerBlock, typeVal.Value())) + } + return elb.LoadBalancer{ + Metadata: resource.GetMetadata(), + Type: typeVal, + DropInvalidHeaderFields: dropInvalidHeadersVal, + Internal: internalVal, + Listeners: listeners, + } +} + +func (a *adapter) adaptClassicLoadBalancer(resource *terraform.Block, _ terraform.Modules) elb.LoadBalancer { + internalAttr := resource.GetAttribute("internal") + internalVal := internalAttr.AsBoolValueOrDefault(false, resource) + + return elb.LoadBalancer{ + Metadata: resource.GetMetadata(), + Type: iacTypes.String("classic", resource.GetMetadata()), + DropInvalidHeaderFields: iacTypes.BoolDefault(false, resource.GetMetadata()), + Internal: internalVal, + Listeners: nil, + } +} + +func adaptListener(listenerBlock *terraform.Block, typeVal string) elb.Listener { + listener := elb.Listener{ + Metadata: listenerBlock.GetMetadata(), + Protocol: iacTypes.StringDefault("", listenerBlock.GetMetadata()), + TLSPolicy: iacTypes.StringDefault("", listenerBlock.GetMetadata()), + DefaultActions: nil, + } + + protocolAttr := listenerBlock.GetAttribute("protocol") + if typeVal == "application" { + listener.Protocol = protocolAttr.AsStringValueOrDefault("HTTP", listenerBlock) + } + + sslPolicyAttr := listenerBlock.GetAttribute("ssl_policy") + listener.TLSPolicy = sslPolicyAttr.AsStringValueOrDefault("", listenerBlock) + + for _, defaultActionBlock := range listenerBlock.GetBlocks("default_action") { + action := elb.Action{ + Metadata: defaultActionBlock.GetMetadata(), + Type: defaultActionBlock.GetAttribute("type").AsStringValueOrDefault("", defaultActionBlock), + } + listener.DefaultActions = append(listener.DefaultActions, action) + } + + return listener +} diff --git a/pkg/iac/adapters/terraform/aws/elb/adapt_test.go b/pkg/iac/adapters/terraform/aws/elb/adapt_test.go new file mode 100644 index 000000000000..bb17b5d45425 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/elb/adapt_test.go @@ -0,0 +1,152 @@ +package elb + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elb" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected elb.ELB + }{ + { + name: "configured", + terraform: ` + resource "aws_alb" "example" { + name = "good_alb" + internal = true + load_balancer_type = "application" + + access_logs { + bucket = aws_s3_bucket.lb_logs.bucket + prefix = "test-lb" + enabled = true + } + + drop_invalid_header_fields = true + } + + resource "aws_alb_listener" "example" { + load_balancer_arn = aws_alb.example.arn + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS-1-1-2017-01" + + default_action { + type = "forward" + } + } +`, + expected: elb.ELB{ + LoadBalancers: []elb.LoadBalancer{ + { + Type: iacTypes.StringTest("application"), + DropInvalidHeaderFields: iacTypes.BoolTest(true), + Internal: iacTypes.BoolTest(true), + Listeners: []elb.Listener{ + { + Protocol: iacTypes.StringTest("HTTPS"), + TLSPolicy: iacTypes.StringTest("ELBSecurityPolicy-TLS-1-1-2017-01"), + DefaultActions: []elb.Action{ + { + Type: iacTypes.StringTest("forward"), + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_alb" "example" { + } +`, + expected: elb.ELB{ + LoadBalancers: []elb.LoadBalancer{ + { + Type: iacTypes.StringTest("application"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_alb" "example" { + name = "good_alb" + internal = true + load_balancer_type = "application" + drop_invalid_header_fields = true + + access_logs { + bucket = aws_s3_bucket.lb_logs.bucket + prefix = "test-lb" + enabled = true + } + } + + resource "aws_alb_listener" "example" { + load_balancer_arn = aws_alb.example.arn + protocol = "HTTPS" + ssl_policy = "ELBSecurityPolicy-TLS-1-1-2017-01" + + default_action { + type = "forward" + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.LoadBalancers, 1) + loadBalancer := adapted.LoadBalancers[0] + + assert.Equal(t, 2, loadBalancer.Metadata.Range().GetStartLine()) + assert.Equal(t, 13, loadBalancer.Metadata.Range().GetEndLine()) + + assert.Equal(t, 4, loadBalancer.Internal.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, loadBalancer.Internal.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 5, loadBalancer.Type.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, loadBalancer.Type.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, loadBalancer.DropInvalidHeaderFields.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, loadBalancer.DropInvalidHeaderFields.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 15, loadBalancer.Listeners[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 23, loadBalancer.Listeners[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 17, loadBalancer.Listeners[0].Protocol.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 17, loadBalancer.Listeners[0].Protocol.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 18, loadBalancer.Listeners[0].TLSPolicy.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 18, loadBalancer.Listeners[0].TLSPolicy.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 20, loadBalancer.Listeners[0].DefaultActions[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 22, loadBalancer.Listeners[0].DefaultActions[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 21, loadBalancer.Listeners[0].DefaultActions[0].Type.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 21, loadBalancer.Listeners[0].DefaultActions[0].Type.GetMetadata().Range().GetEndLine()) + +} diff --git a/pkg/iac/adapters/terraform/aws/emr/adapt.go b/pkg/iac/adapters/terraform/aws/emr/adapt.go new file mode 100644 index 000000000000..74bbc08516f4 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/emr/adapt.go @@ -0,0 +1,49 @@ +package emr + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/emr" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) emr.EMR { + return emr.EMR{ + Clusters: adaptClusters(modules), + SecurityConfiguration: adaptSecurityConfigurations(modules), + } +} +func adaptClusters(modules terraform.Modules) []emr.Cluster { + var clusters []emr.Cluster + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_emr_cluster") { + clusters = append(clusters, adaptCluster(resource)) + } + } + return clusters +} + +func adaptCluster(resource *terraform.Block) emr.Cluster { + + return emr.Cluster{ + Metadata: resource.GetMetadata(), + } +} + +func adaptSecurityConfigurations(modules terraform.Modules) []emr.SecurityConfiguration { + var securityConfiguration []emr.SecurityConfiguration + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_emr_security_configuration") { + securityConfiguration = append(securityConfiguration, adaptSecurityConfiguration(resource)) + } + } + return securityConfiguration +} + +func adaptSecurityConfiguration(resource *terraform.Block) emr.SecurityConfiguration { + + return emr.SecurityConfiguration{ + Metadata: resource.GetMetadata(), + Name: resource.GetAttribute("name").AsStringValueOrDefault("", resource), + Configuration: resource.GetAttribute("configuration").AsStringValueOrDefault("", resource), + } + +} diff --git a/pkg/iac/adapters/terraform/aws/emr/adapt_test.go b/pkg/iac/adapters/terraform/aws/emr/adapt_test.go new file mode 100644 index 000000000000..6b534e7efa2c --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/emr/adapt_test.go @@ -0,0 +1,114 @@ +package emr + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/emr" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptSecurityConfiguration(t *testing.T) { + tests := []struct { + name string + terraform string + expected emr.SecurityConfiguration + }{ + { + name: "test", + terraform: ` + resource "aws_emr_security_configuration" "foo" { + name = "emrsc_test" + configuration = < 0, + }, nil + } + + if dataBlock.Type() == "data" && dataBlock.TypeLabel() == "aws_iam_policy_document" { + if doc, err := ConvertTerraformDocument(modules, dataBlock); err == nil { + return &iam.Document{ + Metadata: dataBlock.GetMetadata(), + Parsed: doc.Document, + IsOffset: true, + HasRefs: false, + }, nil + } + } + + return &iam.Document{ + Metadata: owner.GetMetadata(), + }, nil +} + +func unescapeVars(input string) string { + return strings.ReplaceAll(input, "&{", "${") +} + +// ConvertTerraformDocument converts a terraform data policy into an iamgo policy https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document +func ConvertTerraformDocument(modules terraform.Modules, block *terraform.Block) (*wrappedDocument, error) { + + builder := iamgo.NewPolicyBuilder() + + if sourceAttr := block.GetAttribute("source_json"); sourceAttr.IsString() { + doc, err := iamgo.ParseString(sourceAttr.Value().AsString()) + if err != nil { + return nil, err + } + builder = iamgo.PolicyBuilderFromDocument(*doc) + } + + if sourceDocumentsAttr := block.GetAttribute("source_policy_documents"); sourceDocumentsAttr.IsIterable() { + docs := findAllPolicies(modules, sourceDocumentsAttr) + for _, doc := range docs { + statements, _ := doc.Document.Statements() + for _, statement := range statements { + builder.WithStatement(statement) + } + } + } + + if idAttr := block.GetAttribute("policy_id"); idAttr.IsString() { + r := idAttr.GetMetadata().Range() + builder.WithId(idAttr.Value().AsString(), r.GetStartLine(), r.GetEndLine()) + } + + if versionAttr := block.GetAttribute("version"); versionAttr.IsString() { + r := versionAttr.GetMetadata().Range() + builder.WithVersion(versionAttr.Value().AsString(), r.GetStartLine(), r.GetEndLine()) + } + + for _, statementBlock := range block.GetBlocks("statement") { + statement := parseStatement(statementBlock) + builder.WithStatement(statement, statement.Range().StartLine, statement.Range().EndLine) + } + + if overrideDocumentsAttr := block.GetAttribute("override_policy_documents"); overrideDocumentsAttr.IsIterable() { + docs := findAllPolicies(modules, overrideDocumentsAttr) + for _, doc := range docs { + statements, _ := doc.Document.Statements() + for _, statement := range statements { + builder.WithStatement(statement, statement.Range().StartLine, statement.Range().EndLine) + } + } + } + + return &wrappedDocument{Document: builder.Build(), Source: block}, nil +} + +// nolint +func parseStatement(statementBlock *terraform.Block) iamgo.Statement { + + metadata := statementBlock.GetMetadata() + + builder := iamgo.NewStatementBuilder() + builder.WithRange(metadata.Range().GetStartLine(), metadata.Range().GetEndLine()) + + if sidAttr := statementBlock.GetAttribute("sid"); sidAttr.IsString() { + r := sidAttr.GetMetadata().Range() + builder.WithSid(sidAttr.Value().AsString(), r.GetStartLine(), r.GetEndLine()) + } + if actionsAttr := statementBlock.GetAttribute("actions"); actionsAttr.IsIterable() { + r := actionsAttr.GetMetadata().Range() + values := actionsAttr.AsStringValues().AsStrings() + builder.WithActions(values, r.GetStartLine(), r.GetEndLine()) + } + if notActionsAttr := statementBlock.GetAttribute("not_actions"); notActionsAttr.IsIterable() { + r := notActionsAttr.GetMetadata().Range() + values := notActionsAttr.AsStringValues().AsStrings() + builder.WithNotActions(values, r.GetStartLine(), r.GetEndLine()) + } + if resourcesAttr := statementBlock.GetAttribute("resources"); resourcesAttr.IsIterable() { + r := resourcesAttr.GetMetadata().Range() + values := resourcesAttr.AsStringValues().AsStrings() + builder.WithResources(values, r.GetStartLine(), r.GetEndLine()) + } + if notResourcesAttr := statementBlock.GetAttribute("not_resources"); notResourcesAttr.IsIterable() { + r := notResourcesAttr.GetMetadata().Range() + values := notResourcesAttr.AsStringValues().AsStrings() + builder.WithNotResources(values, r.GetStartLine(), r.GetEndLine()) + } + if effectAttr := statementBlock.GetAttribute("effect"); effectAttr.IsString() { + r := effectAttr.GetMetadata().Range() + builder.WithEffect(effectAttr.Value().AsString(), r.GetStartLine(), r.GetEndLine()) + } else { + builder.WithEffect(iamgo.EffectAllow) + } + + for _, principalBlock := range statementBlock.GetBlocks("principals") { + typeAttr := principalBlock.GetAttribute("type") + if !typeAttr.IsString() { + continue + } + identifiersAttr := principalBlock.GetAttribute("identifiers") + if !identifiersAttr.IsIterable() { + continue + } + r := principalBlock.GetMetadata().Range() + switch typeAttr.Value().AsString() { + case "*": + builder.WithAllPrincipals(true, r.GetStartLine(), r.GetEndLine()) + case "AWS": + values := identifiersAttr.AsStringValues().AsStrings() + builder.WithAWSPrincipals(values, r.GetStartLine(), r.GetEndLine()) + case "Federated": + values := identifiersAttr.AsStringValues().AsStrings() + builder.WithFederatedPrincipals(values, r.GetStartLine(), r.GetEndLine()) + case "Service": + values := identifiersAttr.AsStringValues().AsStrings() + builder.WithServicePrincipals(values, r.GetStartLine(), r.GetEndLine()) + case "CanonicalUser": + values := identifiersAttr.AsStringValues().AsStrings() + builder.WithCanonicalUsersPrincipals(values, r.GetStartLine(), r.GetEndLine()) + } + } + + for _, conditionBlock := range statementBlock.GetBlocks("condition") { + testAttr := conditionBlock.GetAttribute("test") + if !testAttr.IsString() { + continue + } + variableAttr := conditionBlock.GetAttribute("variable") + if !variableAttr.IsString() { + continue + } + valuesAttr := conditionBlock.GetAttribute("values") + values := valuesAttr.AsStringValues().AsStrings() + if valuesAttr.IsNil() || len(values) == 0 { + continue + } + + r := conditionBlock.GetMetadata().Range() + + builder.WithCondition( + testAttr.Value().AsString(), + variableAttr.Value().AsString(), + values, + r.GetStartLine(), + r.GetEndLine(), + ) + + } + return builder.Build() +} + +func findAllPolicies(modules terraform.Modules, attr *terraform.Attribute) []wrappedDocument { + var documents []wrappedDocument + + if !attr.IsIterable() { + return documents + } + + policyDocIDs := attr.AsStringValues().AsStrings() + for _, policyDocID := range policyDocIDs { + if policyDoc, err := modules.GetBlockById(policyDocID); err == nil { + if document, err := ConvertTerraformDocument(modules, policyDoc); err == nil { + documents = append(documents, *document) + } + } else if parsed, err := iamgo.Parse([]byte(unescapeVars(policyDocID))); err == nil { + documents = append(documents, wrappedDocument{ + Document: *parsed, + Source: attr, + }) + } + } + return documents +} diff --git a/pkg/iac/adapters/terraform/aws/iam/groups.go b/pkg/iac/adapters/terraform/aws/iam/groups.go new file mode 100644 index 000000000000..9053bf83be78 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/iam/groups.go @@ -0,0 +1,32 @@ +package iam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func adaptGroups(modules terraform.Modules) []iam.Group { + var groups []iam.Group + + for _, groupBlock := range modules.GetResourcesByType("aws_iam_group") { + group := iam.Group{ + Metadata: groupBlock.GetMetadata(), + Name: groupBlock.GetAttribute("name").AsStringValueOrDefault("", groupBlock), + } + + if policy, ok := applyForDependentResource( + modules, groupBlock.ID(), "name", "aws_iam_group_policy", "group", findPolicy(modules), + ); ok && policy != nil { + group.Policies = append(group.Policies, *policy) + } + + if policy, ok := applyForDependentResource( + modules, groupBlock.ID(), "name", "aws_iam_group_policy_attachment", "group", findAttachmentPolicy(modules), + ); ok && policy != nil { + group.Policies = append(group.Policies, *policy) + } + + groups = append(groups, group) + } + return groups +} diff --git a/pkg/iac/adapters/terraform/aws/iam/groups_test.go b/pkg/iac/adapters/terraform/aws/iam/groups_test.go new file mode 100644 index 000000000000..2e4ab7d5caec --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/iam/groups_test.go @@ -0,0 +1,109 @@ +package iam + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptGroups(t *testing.T) { + tests := []struct { + name string + terraform string + expected []iam.Group + }{ + { + name: "policy", + terraform: ` + resource "aws_iam_group_policy" "my_developer_policy" { + name = "my_developer_policy" + group = aws_iam_group.my_developers.name + + policy = < { + role = role_res.name + } + } +} + +data "aws_iam_policy_document" "administrative_policy_doc" { + statement { + resources = ["*"] + actions = ["Tag:GetResources", "Tag:TagResources", "Tag:UntagResources"] + } +} + +resource "aws_iam_policy" "administrative_policy" { + name = "administrative-policy" + policy = data.aws_iam_policy_document.administrative_policy_doc.json +} + +resource "aws_iam_role_policy_attachment" "administrative_policy_attachment" { + for_each = local.platform_roles + role = each.value.role + policy_arn = aws_iam_policy.administrative_policy.arn +}`, + expectedCount: 2, + }, + { + name: "direct for_each reference", + terraform: ` +locals { + roles = { + lambda = "lambda.amazonaws.com" + ecs-tasks = "ecs-tasks.amazonaws.com" + } +} + +resource "aws_iam_role" "platform_role" { + for_each = local.roles + name = "platform-${each.key}" +} + +resource "aws_iam_role_policy_attachment" "test" { + for_each = aws_iam_role.platform_role + role = each.value.name + policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess" +}`, + expectedCount: 2, + }, + { + name: "for_each with computed local reference", + terraform: ` +locals { + role_principals = { + lambda = "lambda.amazonaws.com" + ecs-tasks = "ecs-tasks.amazonaws.com" + } +} + +resource "aws_iam_role" "platform_role" { + for_each = local.role_principals + name = "platform-${each.key}" +} + +locals { + platform_roles = { + for role_key, role_res in aws_iam_role.platform_role : + role_key => { + role = role_res.name + } + } +} + +resource "aws_iam_role_policy_attachment" "test" { + for_each = local.platform_roles + role = each.value.role + policy_arn = "arn:aws:iam::aws:policy/AmazonS3FullAccess" +}`, + expectedCount: 2, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + attachments := modules.GetResourcesByType("aws_iam_role_policy_attachment") + + // Debug output for troubleshooting + t.Logf("Total resources found: %d", len(attachments)) + for i, attachment := range attachments { + t.Logf("Attachment %d: %s", i, attachment.Reference().String()) + t.Logf(" - FullName: %s", attachment.FullName()) + t.Logf(" - TypeLabel: %s", attachment.TypeLabel()) + t.Logf(" - NameLabel: %s", attachment.NameLabel()) + if key := attachment.Reference().RawKey(); !key.IsNull() && key.IsKnown() { + t.Logf(" - Key: %s (%s)", key.GoString(), key.Type().GoString()) + } + } + + var attachmentRefs []string + for _, a := range attachments { + attachmentRefs = append(attachmentRefs, a.Reference().String()) + } + + sort.Strings(attachmentRefs) + + if len(attachments) != test.expectedCount { + t.Fatalf("expected %d policy attachments, got %d: %v", test.expectedCount, len(attachments), attachmentRefs) + } + + // Validate that both lambda and ecs-tasks keys are present + validateLambdaEcsKeys(t, attachmentRefs) + }) + } +} diff --git a/pkg/iac/adapters/terraform/aws/iam/users.go b/pkg/iac/adapters/terraform/aws/iam/users.go new file mode 100644 index 000000000000..ec311d21c5c0 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/iam/users.go @@ -0,0 +1,56 @@ +package iam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptUsers(modules terraform.Modules) []iam.User { + var users []iam.User + + for _, userBlock := range modules.GetResourcesByType("aws_iam_user") { + user := iam.User{ + Metadata: userBlock.GetMetadata(), + Name: userBlock.GetAttribute("name").AsStringValueOrDefault("", userBlock), + LastAccess: iacTypes.TimeUnresolvable(userBlock.GetMetadata()), + } + + if policy, ok := applyForDependentResource( + modules, userBlock.ID(), "name", "aws_iam_user_policy", "user", findPolicy(modules), + ); ok && policy != nil { + user.Policies = append(user.Policies, *policy) + } + + if policy, ok := applyForDependentResource( + modules, userBlock.ID(), "name", "aws_iam_user_policy_attachment", "user", findAttachmentPolicy(modules), + ); ok && policy != nil { + user.Policies = append(user.Policies, *policy) + } + + if accessKey, ok := applyForDependentResource( + modules, userBlock.ID(), "name", "aws_iam_access_key", "user", adaptAccessKey, + ); ok { + user.AccessKeys = append(user.AccessKeys, accessKey) + } + + users = append(users, user) + } + return users + +} + +func adaptAccessKey(block *terraform.Block) iam.AccessKey { + + active := iacTypes.BoolDefault(true, block.GetMetadata()) + if activeAttr := block.GetAttribute("status"); activeAttr.IsString() { + active = iacTypes.Bool(activeAttr.Equals("Active"), activeAttr.GetMetadata()) + } + return iam.AccessKey{ + Metadata: block.GetMetadata(), + AccessKeyId: iacTypes.StringUnresolvable(block.GetMetadata()), + CreationDate: iacTypes.TimeUnresolvable(block.GetMetadata()), + LastAccess: iacTypes.TimeUnresolvable(block.GetMetadata()), + Active: active, + } +} diff --git a/pkg/iac/adapters/terraform/aws/iam/users_test.go b/pkg/iac/adapters/terraform/aws/iam/users_test.go new file mode 100644 index 000000000000..38774c77d4ec --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/iam/users_test.go @@ -0,0 +1,156 @@ +package iam + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptUsers(t *testing.T) { + tests := []struct { + name string + terraform string + expected []iam.User + }{ + { + name: "policy", + terraform: ` +resource "aws_iam_user" "lb" { + name = "loadbalancer" + path = "/system/" +} + +resource "aws_iam_user_policy" "policy" { + name = "test" + user = aws_iam_user.lb.name + + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "ec2:Describe*", + ] + Effect = "Allow" + Resource = "*" + }, + ] + }) +} +`, + expected: []iam.User{ + { + Name: iacTypes.StringTest("loadbalancer"), + Policies: []iam.Policy{ + { + Name: iacTypes.StringTest("test"), + Document: defaultPolicyDocuemnt(false), + }, + }, + }, + }, + }, + { + name: "policy attachment", + terraform: ` +resource "aws_iam_user" "user" { + name = "test-user" +} + +resource "aws_iam_policy" "policy" { + name = "test-policy" + description = "A test policy" + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = [ + "ec2:Describe*", + ] + Effect = "Allow" + Resource = "*" + }, + ] + }) +} + +resource "aws_iam_user_policy_attachment" "test-attach" { + user = aws_iam_user.user.name + policy_arn = aws_iam_policy.policy.arn +} +`, + expected: []iam.User{ + { + Name: iacTypes.StringTest("test-user"), + Policies: []iam.Policy{ + { + Name: iacTypes.StringTest("test-policy"), + Document: defaultPolicyDocuemnt(false), + }, + }, + }, + }, + }, + { + name: "access key", + terraform: ` +resource "aws_iam_access_key" "lb" { + user = aws_iam_user.lb.name + pgp_key = "keybase:some_person_that_exists" + status = "Active" +} + +resource "aws_iam_user" "lb" { + name = "loadbalafncer" + path = "/system/" +} +`, + expected: []iam.User{ + { + Name: iacTypes.StringTest("loadbalafncer"), + AccessKeys: []iam.AccessKey{ + { + Active: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + { + name: "access key with default status", + terraform: ` +resource "aws_iam_access_key" "lb" { + user = aws_iam_user.lb.name + pgp_key = "keybase:some_person_that_exists" +} + +resource "aws_iam_user" "lb" { + name = "loadbalafncer" + path = "/system/" +} +`, + expected: []iam.User{ + { + Name: iacTypes.StringTest("loadbalafncer"), + AccessKeys: []iam.AccessKey{ + { + Active: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptUsers(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/aws/kinesis/adapt.go b/pkg/iac/adapters/terraform/aws/kinesis/adapt.go new file mode 100644 index 000000000000..f66f77505c07 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/kinesis/adapt.go @@ -0,0 +1,41 @@ +package kinesis + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/kinesis" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) kinesis.Kinesis { + return kinesis.Kinesis{ + Streams: adaptStreams(modules), + } +} + +func adaptStreams(modules terraform.Modules) []kinesis.Stream { + var streams []kinesis.Stream + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_kinesis_stream") { + streams = append(streams, adaptStream(resource)) + } + } + return streams +} + +func adaptStream(resource *terraform.Block) kinesis.Stream { + + stream := kinesis.Stream{ + Metadata: resource.GetMetadata(), + Encryption: kinesis.Encryption{ + Metadata: resource.GetMetadata(), + Type: types.StringDefault("NONE", resource.GetMetadata()), + KMSKeyID: types.StringDefault("", resource.GetMetadata()), + }, + } + + encryptionTypeAttr := resource.GetAttribute("encryption_type") + stream.Encryption.Type = encryptionTypeAttr.AsStringValueOrDefault("NONE", resource) + KMSKeyIDAttr := resource.GetAttribute("kms_key_id") + stream.Encryption.KMSKeyID = KMSKeyIDAttr.AsStringValueOrDefault("", resource) + return stream +} diff --git a/pkg/iac/adapters/terraform/aws/kinesis/adapt_test.go b/pkg/iac/adapters/terraform/aws/kinesis/adapt_test.go new file mode 100644 index 000000000000..619becd92d3f --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/kinesis/adapt_test.go @@ -0,0 +1,80 @@ +package kinesis + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/kinesis" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptStream(t *testing.T) { + tests := []struct { + name string + terraform string + expected kinesis.Stream + }{ + { + name: "configured", + terraform: ` + resource "aws_kinesis_stream" "example" { + encryption_type = "KMS" + kms_key_id = "my/special/key" + } +`, + expected: kinesis.Stream{ + Encryption: kinesis.Encryption{ + Type: iacTypes.StringTest("KMS"), + KMSKeyID: iacTypes.StringTest("my/special/key"), + }, + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_kinesis_stream" "example" { + } +`, + expected: kinesis.Stream{ + Encryption: kinesis.Encryption{ + Type: iacTypes.StringTest("NONE"), + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptStream(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_kinesis_stream" "example" { + encryption_type = "KMS" + kms_key_id = "my/special/key" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Streams, 1) + stream := adapted.Streams[0] + + assert.Equal(t, 2, stream.Metadata.Range().GetStartLine()) + assert.Equal(t, 5, stream.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, stream.Encryption.Type.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, stream.Encryption.Type.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, stream.Encryption.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, stream.Encryption.KMSKeyID.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/kms/adapt.go b/pkg/iac/adapters/terraform/aws/kms/adapt.go new file mode 100644 index 000000000000..95bc730ef2b1 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/kms/adapt.go @@ -0,0 +1,36 @@ +package kms + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/kms" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) kms.KMS { + return kms.KMS{ + Keys: adaptKeys(modules), + } +} + +func adaptKeys(modules terraform.Modules) []kms.Key { + var keys []kms.Key + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_kms_key") { + keys = append(keys, adaptKey(resource)) + } + } + return keys +} + +func adaptKey(resource *terraform.Block) kms.Key { + usageAttr := resource.GetAttribute("key_usage") + usageVal := usageAttr.AsStringValueOrDefault("ENCRYPT_DECRYPT", resource) + + enableKeyRotationAttr := resource.GetAttribute("enable_key_rotation") + enableKeyRotationVal := enableKeyRotationAttr.AsBoolValueOrDefault(false, resource) + + return kms.Key{ + Metadata: resource.GetMetadata(), + Usage: usageVal, + RotationEnabled: enableKeyRotationVal, + } +} diff --git a/pkg/iac/adapters/terraform/aws/kms/adapt_test.go b/pkg/iac/adapters/terraform/aws/kms/adapt_test.go new file mode 100644 index 000000000000..31c311d17cf6 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/kms/adapt_test.go @@ -0,0 +1,77 @@ +package kms + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/kms" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptKey(t *testing.T) { + tests := []struct { + name string + terraform string + expected kms.Key + }{ + { + name: "configured", + terraform: ` + resource "aws_kms_key" "example" { + enable_key_rotation = true + key_usage = "SIGN_VERIFY" + } +`, + expected: kms.Key{ + Usage: iacTypes.StringTest(kms.KeyUsageSignAndVerify), + RotationEnabled: iacTypes.BoolTest(true), + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_kms_key" "example" { + } +`, + expected: kms.Key{ + Usage: iacTypes.StringTest("ENCRYPT_DECRYPT"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptKey(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_kms_key" "example" { + enable_key_rotation = true + key_usage = SIGN_VERIFY + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Keys, 1) + key := adapted.Keys[0] + + assert.Equal(t, 2, key.Metadata.Range().GetStartLine()) + assert.Equal(t, 5, key.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, key.RotationEnabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, key.RotationEnabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, key.Usage.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, key.Usage.GetMetadata().Range().GetEndLine()) + +} diff --git a/pkg/iac/adapters/terraform/aws/lambda/adapt.go b/pkg/iac/adapters/terraform/aws/lambda/adapt.go new file mode 100644 index 000000000000..3545dcb192f4 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/lambda/adapt.go @@ -0,0 +1,98 @@ +package lambda + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/lambda" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) lambda.Lambda { + + adapter := adapter{ + permissionIDs: modules.GetChildResourceIDMapByType("aws_lambda_permission"), + } + + return lambda.Lambda{ + Functions: adapter.adaptFunctions(modules), + } +} + +type adapter struct { + permissionIDs terraform.ResourceIDResolutions +} + +func (a *adapter) adaptFunctions(modules terraform.Modules) []lambda.Function { + + var functions []lambda.Function + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_lambda_function") { + functions = append(functions, a.adaptFunction(resource, modules, a.permissionIDs)) + } + } + + orphanResources := modules.GetResourceByIDs(a.permissionIDs.Orphans()...) + + if len(orphanResources) > 0 { + orphanage := lambda.Function{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Tracing: lambda.Tracing{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Mode: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + Permissions: nil, + } + for _, permission := range orphanResources { + orphanage.Permissions = append(orphanage.Permissions, a.adaptPermission(permission)) + } + functions = append(functions, orphanage) + } + + return functions +} + +func (a *adapter) adaptFunction(function *terraform.Block, modules terraform.Modules, orphans terraform.ResourceIDResolutions) lambda.Function { + var permissions []lambda.Permission + for _, module := range modules { + for _, p := range module.GetResourcesByType("aws_lambda_permission") { + if referencedBlock, err := module.GetReferencedBlock(p.GetAttribute("function_name"), p); err == nil && referencedBlock == function { + permissions = append(permissions, a.adaptPermission(p)) + delete(orphans, p.ID()) + } + } + } + + return lambda.Function{ + Metadata: function.GetMetadata(), + Tracing: a.adaptTracing(function), + Permissions: permissions, + } +} + +func (a *adapter) adaptTracing(function *terraform.Block) lambda.Tracing { + if tracingConfig := function.GetBlock("tracing_config"); tracingConfig.IsNotNil() { + return lambda.Tracing{ + Metadata: tracingConfig.GetMetadata(), + Mode: tracingConfig.GetAttribute("mode").AsStringValueOrDefault("", tracingConfig), + } + } + + return lambda.Tracing{ + Metadata: function.GetMetadata(), + Mode: iacTypes.StringDefault("", function.GetMetadata()), + } +} + +func (a *adapter) adaptPermission(permission *terraform.Block) lambda.Permission { + sourceARNAttr := permission.GetAttribute("source_arn") + sourceARN := sourceARNAttr.AsStringValueOrDefault("", permission) + + if refs := sourceARNAttr.AllReferences(); len(refs) > 0 { + sourceARN = iacTypes.String(refs[0].NameLabel(), sourceARNAttr.GetMetadata()) + } + + return lambda.Permission{ + Metadata: permission.GetMetadata(), + Principal: permission.GetAttribute("principal").AsStringValueOrDefault("", permission), + SourceARN: sourceARN, + } +} diff --git a/pkg/iac/adapters/terraform/aws/lambda/adapt_test.go b/pkg/iac/adapters/terraform/aws/lambda/adapt_test.go new file mode 100644 index 000000000000..57ebbb036876 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/lambda/adapt_test.go @@ -0,0 +1,139 @@ +package lambda + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/lambda" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected lambda.Lambda + }{ + { + name: "reference arn", + terraform: ` + resource "aws_lambda_function" "example" { + filename = "lambda_function_payload.zip" + function_name = "lambda_function_name" + role = aws_iam_role.iam_for_lambda.arn + runtime = "nodejs12.x" + + tracing_config { + mode = "Passthrough" + } + } + + resource "aws_lambda_permission" "example" { + statement_id = "AllowExecutionFromSNS" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.example.function_name + principal = "sns.amazonaws.com" + source_arn = aws_sns_topic.default.arn + } +`, + expected: lambda.Lambda{ + Functions: []lambda.Function{ + { + Tracing: lambda.Tracing{ + Mode: iacTypes.StringTest("Passthrough"), + }, + Permissions: []lambda.Permission{ + { + Principal: iacTypes.StringTest("sns.amazonaws.com"), + SourceARN: iacTypes.StringTest("default"), + }, + }, + }, + }, + }, + }, + { + name: "defaults (with an orphan)", + terraform: ` + resource "aws_lambda_function" "example" { + tracing_config { + } + } + + resource "aws_lambda_permission" "example" { + } +`, + expected: lambda.Lambda{ + Functions: []lambda.Function{ + { + Tracing: lambda.Tracing{}, + }, + { + Tracing: lambda.Tracing{}, + Permissions: []lambda.Permission{ + {}, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_lambda_function" "example" { + filename = "lambda_function_payload.zip" + function_name = "lambda_function_name" + role = aws_iam_role.iam_for_lambda.arn + runtime = "nodejs12.x" + + tracing_config { + mode = "Passthrough" + } + } + + resource "aws_lambda_permission" "example" { + statement_id = "AllowExecutionFromSNS" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.example.function_name + principal = "sns.amazonaws.com" + source_arn = "string arn" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Functions, 1) + function := adapted.Functions[0] + + assert.Equal(t, 2, function.Metadata.Range().GetStartLine()) + assert.Equal(t, 11, function.Metadata.Range().GetEndLine()) + + assert.Equal(t, 8, function.Tracing.Metadata.Range().GetStartLine()) + assert.Equal(t, 10, function.Tracing.Metadata.Range().GetEndLine()) + + assert.Equal(t, 9, function.Tracing.Mode.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 9, function.Tracing.Mode.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, function.Permissions[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 19, function.Permissions[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 17, function.Permissions[0].Principal.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 17, function.Permissions[0].Principal.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 18, function.Permissions[0].SourceARN.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 18, function.Permissions[0].SourceARN.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/mq/adapt.go b/pkg/iac/adapters/terraform/aws/mq/adapt.go new file mode 100644 index 000000000000..d0fed28d5b83 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/mq/adapt.go @@ -0,0 +1,48 @@ +package mq + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/mq" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) mq.MQ { + return mq.MQ{ + Brokers: adaptBrokers(modules), + } +} + +func adaptBrokers(modules terraform.Modules) []mq.Broker { + var brokers []mq.Broker + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_mq_broker") { + brokers = append(brokers, adaptBroker(resource)) + } + } + return brokers +} + +func adaptBroker(resource *terraform.Block) mq.Broker { + + broker := mq.Broker{ + Metadata: resource.GetMetadata(), + PublicAccess: types.BoolDefault(false, resource.GetMetadata()), + Logging: mq.Logging{ + Metadata: resource.GetMetadata(), + General: types.BoolDefault(false, resource.GetMetadata()), + Audit: types.BoolDefault(false, resource.GetMetadata()), + }, + } + + publicAccessAttr := resource.GetAttribute("publicly_accessible") + broker.PublicAccess = publicAccessAttr.AsBoolValueOrDefault(false, resource) + if logsBlock := resource.GetBlock("logs"); logsBlock.IsNotNil() { + broker.Logging.Metadata = logsBlock.GetMetadata() + auditAttr := logsBlock.GetAttribute("audit") + broker.Logging.Audit = auditAttr.AsBoolValueOrDefault(false, logsBlock) + generalAttr := logsBlock.GetAttribute("general") + broker.Logging.General = generalAttr.AsBoolValueOrDefault(false, logsBlock) + } + + return broker +} diff --git a/pkg/iac/adapters/terraform/aws/mq/adapt_test.go b/pkg/iac/adapters/terraform/aws/mq/adapt_test.go new file mode 100644 index 000000000000..f8a4d9495b17 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/mq/adapt_test.go @@ -0,0 +1,104 @@ +package mq + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/mq" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptBroker(t *testing.T) { + tests := []struct { + name string + terraform string + expected mq.Broker + }{ + { + name: "audit logs", + terraform: ` + resource "aws_mq_broker" "example" { + logs { + audit = true + } + + publicly_accessible = false + } +`, + expected: mq.Broker{ + Logging: mq.Logging{ + Audit: iacTypes.BoolTest(true), + }, + }, + }, + { + name: "general logs", + terraform: ` + resource "aws_mq_broker" "example" { + logs { + general = true + } + + publicly_accessible = true + } +`, + expected: mq.Broker{ + PublicAccess: iacTypes.BoolTest(true), + Logging: mq.Logging{ + General: iacTypes.BoolTest(true), + }, + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_mq_broker" "example" { + } +`, + expected: mq.Broker{ + Logging: mq.Logging{}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptBroker(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_mq_broker" "example" { + logs { + general = true + } + + publicly_accessible = true + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Brokers, 1) + broker := adapted.Brokers[0] + + assert.Equal(t, 2, broker.Metadata.Range().GetStartLine()) + assert.Equal(t, 8, broker.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, broker.Logging.Metadata.Range().GetStartLine()) + assert.Equal(t, 5, broker.Logging.Metadata.Range().GetEndLine()) + + assert.Equal(t, 4, broker.Logging.General.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, broker.Logging.General.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 7, broker.PublicAccess.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, broker.PublicAccess.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/msk/adapt.go b/pkg/iac/adapters/terraform/aws/msk/adapt.go new file mode 100644 index 000000000000..37942296df02 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/msk/adapt.go @@ -0,0 +1,95 @@ +package msk + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/msk" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) msk.MSK { + return msk.MSK{ + Clusters: adaptClusters(modules), + } +} + +func adaptClusters(modules terraform.Modules) []msk.Cluster { + var clusters []msk.Cluster + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_msk_cluster") { + clusters = append(clusters, adaptCluster(resource)) + } + } + return clusters +} + +func adaptCluster(resource *terraform.Block) msk.Cluster { + cluster := msk.Cluster{ + Metadata: resource.GetMetadata(), + EncryptionInTransit: msk.EncryptionInTransit{ + Metadata: resource.GetMetadata(), + ClientBroker: iacTypes.StringDefault("TLS_PLAINTEXT", resource.GetMetadata()), + }, + EncryptionAtRest: msk.EncryptionAtRest{ + Metadata: resource.GetMetadata(), + KMSKeyARN: iacTypes.StringDefault("", resource.GetMetadata()), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + Logging: msk.Logging{ + Metadata: resource.GetMetadata(), + Broker: msk.BrokerLogging{ + Metadata: resource.GetMetadata(), + S3: msk.S3Logging{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + Cloudwatch: msk.CloudwatchLogging{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + Firehose: msk.FirehoseLogging{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + }, + }, + } + + if encryptBlock := resource.GetBlock("encryption_info"); encryptBlock.IsNotNil() { + if encryptionInTransitBlock := encryptBlock.GetBlock("encryption_in_transit"); encryptionInTransitBlock.IsNotNil() { + cluster.EncryptionInTransit.Metadata = encryptionInTransitBlock.GetMetadata() + if clientBrokerAttr := encryptionInTransitBlock.GetAttribute("client_broker"); clientBrokerAttr.IsNotNil() { + cluster.EncryptionInTransit.ClientBroker = clientBrokerAttr.AsStringValueOrDefault("TLS", encryptionInTransitBlock) + } + } + + if encryptionAtRestAttr := encryptBlock.GetAttribute("encryption_at_rest_kms_key_arn"); encryptionAtRestAttr.IsNotNil() { + cluster.EncryptionAtRest.Metadata = encryptionAtRestAttr.GetMetadata() + cluster.EncryptionAtRest.KMSKeyARN = encryptionAtRestAttr.AsStringValueOrDefault("", encryptBlock) + cluster.EncryptionAtRest.Enabled = iacTypes.Bool(true, encryptionAtRestAttr.GetMetadata()) + } + } + + if logBlock := resource.GetBlock("logging_info"); logBlock.IsNotNil() { + cluster.Logging.Metadata = logBlock.GetMetadata() + if brokerLogsBlock := logBlock.GetBlock("broker_logs"); brokerLogsBlock.IsNotNil() { + cluster.Logging.Broker.Metadata = brokerLogsBlock.GetMetadata() + if s3Block := brokerLogsBlock.GetBlock("s3"); s3Block.IsNotNil() { + s3enabledAttr := s3Block.GetAttribute("enabled") + cluster.Logging.Broker.S3.Metadata = s3Block.GetMetadata() + cluster.Logging.Broker.S3.Enabled = s3enabledAttr.AsBoolValueOrDefault(false, s3Block) + } + if cloudwatchBlock := brokerLogsBlock.GetBlock("cloudwatch_logs"); cloudwatchBlock.IsNotNil() { + cwEnabledAttr := cloudwatchBlock.GetAttribute("enabled") + cluster.Logging.Broker.Cloudwatch.Metadata = cloudwatchBlock.GetMetadata() + cluster.Logging.Broker.Cloudwatch.Enabled = cwEnabledAttr.AsBoolValueOrDefault(false, cloudwatchBlock) + } + if firehoseBlock := brokerLogsBlock.GetBlock("firehose"); firehoseBlock.IsNotNil() { + firehoseEnabledAttr := firehoseBlock.GetAttribute("enabled") + cluster.Logging.Broker.Firehose.Metadata = firehoseBlock.GetMetadata() + cluster.Logging.Broker.Firehose.Enabled = firehoseEnabledAttr.AsBoolValueOrDefault(false, firehoseBlock) + } + } + } + + return cluster +} diff --git a/pkg/iac/adapters/terraform/aws/msk/adapt_test.go b/pkg/iac/adapters/terraform/aws/msk/adapt_test.go new file mode 100644 index 000000000000..5d00de1423fc --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/msk/adapt_test.go @@ -0,0 +1,177 @@ +package msk + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/msk" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptCluster(t *testing.T) { + tests := []struct { + name string + terraform string + expected msk.Cluster + }{ + { + name: "configured", + terraform: ` + resource "aws_msk_cluster" "example" { + cluster_name = "example" + + encryption_info { + encryption_in_transit { + client_broker = "TLS" + in_cluster = true + } + encryption_at_rest_kms_key_arn = "foo-bar-key" + } + + logging_info { + broker_logs { + cloudwatch_logs { + enabled = true + log_group = aws_cloudwatch_log_group.test.name + } + firehose { + enabled = true + delivery_stream = aws_kinesis_firehose_delivery_stream.test_stream.name + } + s3 { + enabled = true + bucket = aws_s3_bucket.bucket.id + prefix = "logs/msk-" + } + } + } + } +`, + expected: msk.Cluster{ + EncryptionInTransit: msk.EncryptionInTransit{ + ClientBroker: iacTypes.StringTest("TLS"), + }, + EncryptionAtRest: msk.EncryptionAtRest{ + KMSKeyARN: iacTypes.StringTest("foo-bar-key"), + Enabled: iacTypes.BoolTest(true), + }, + Logging: msk.Logging{ + Broker: msk.BrokerLogging{ + S3: msk.S3Logging{ + Enabled: iacTypes.BoolTest(true), + }, + Cloudwatch: msk.CloudwatchLogging{ + Enabled: iacTypes.BoolTest(true), + }, + Firehose: msk.FirehoseLogging{ + Enabled: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_msk_cluster" "example" { + } +`, + expected: msk.Cluster{ + EncryptionInTransit: msk.EncryptionInTransit{ + ClientBroker: iacTypes.StringTest("TLS_PLAINTEXT"), + }, + Logging: msk.Logging{ + Broker: msk.BrokerLogging{ + S3: msk.S3Logging{}, + Cloudwatch: msk.CloudwatchLogging{}, + Firehose: msk.FirehoseLogging{}, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptCluster(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_msk_cluster" "example" { + cluster_name = "example" + + encryption_info { + encryption_in_transit { + client_broker = "TLS" + in_cluster = true + } + encryption_at_rest_kms_key_arn = "foo-bar-key" + } + + logging_info { + broker_logs { + cloudwatch_logs { + enabled = true + log_group = aws_cloudwatch_log_group.test.name + } + firehose { + enabled = true + delivery_stream = aws_kinesis_firehose_delivery_stream.test_stream.name + } + s3 { + enabled = true + bucket = aws_s3_bucket.bucket.id + prefix = "logs/msk-" + } + } + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Clusters, 1) + cluster := adapted.Clusters[0] + + assert.Equal(t, 2, cluster.Metadata.Range().GetStartLine()) + assert.Equal(t, 30, cluster.Metadata.Range().GetEndLine()) + + assert.Equal(t, 6, cluster.EncryptionInTransit.Metadata.Range().GetStartLine()) + assert.Equal(t, 9, cluster.EncryptionInTransit.Metadata.Range().GetEndLine()) + + assert.Equal(t, 10, cluster.EncryptionAtRest.Metadata.Range().GetStartLine()) + assert.Equal(t, 10, cluster.EncryptionAtRest.Metadata.Range().GetEndLine()) + + assert.Equal(t, 13, cluster.Logging.Metadata.Range().GetStartLine()) + assert.Equal(t, 29, cluster.Logging.Metadata.Range().GetEndLine()) + + assert.Equal(t, 14, cluster.Logging.Broker.Metadata.Range().GetStartLine()) + assert.Equal(t, 28, cluster.Logging.Broker.Metadata.Range().GetEndLine()) + + assert.Equal(t, 15, cluster.Logging.Broker.Cloudwatch.Metadata.Range().GetStartLine()) + assert.Equal(t, 18, cluster.Logging.Broker.Cloudwatch.Metadata.Range().GetEndLine()) + + assert.Equal(t, 16, cluster.Logging.Broker.Cloudwatch.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 16, cluster.Logging.Broker.Cloudwatch.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 19, cluster.Logging.Broker.Firehose.Metadata.Range().GetStartLine()) + assert.Equal(t, 22, cluster.Logging.Broker.Firehose.Metadata.Range().GetEndLine()) + + assert.Equal(t, 20, cluster.Logging.Broker.Firehose.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 20, cluster.Logging.Broker.Firehose.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 23, cluster.Logging.Broker.S3.Metadata.Range().GetStartLine()) + assert.Equal(t, 27, cluster.Logging.Broker.S3.Metadata.Range().GetEndLine()) + + assert.Equal(t, 24, cluster.Logging.Broker.S3.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 24, cluster.Logging.Broker.S3.Enabled.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/neptune/adapt.go b/pkg/iac/adapters/terraform/aws/neptune/adapt.go new file mode 100644 index 000000000000..2dc3e344b110 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/neptune/adapt.go @@ -0,0 +1,50 @@ +package neptune + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/neptune" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) neptune.Neptune { + return neptune.Neptune{ + Clusters: adaptClusters(modules), + } +} + +func adaptClusters(modules terraform.Modules) []neptune.Cluster { + var clusters []neptune.Cluster + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_neptune_cluster") { + clusters = append(clusters, adaptCluster(resource)) + } + } + return clusters +} + +func adaptCluster(resource *terraform.Block) neptune.Cluster { + cluster := neptune.Cluster{ + Metadata: resource.GetMetadata(), + Logging: neptune.Logging{ + Metadata: resource.GetMetadata(), + Audit: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + StorageEncrypted: iacTypes.BoolDefault(false, resource.GetMetadata()), + KMSKeyID: iacTypes.StringDefault("", resource.GetMetadata()), + } + + if enableLogExportsAttr := resource.GetAttribute("enable_cloudwatch_logs_exports"); enableLogExportsAttr.IsNotNil() { + cluster.Logging.Metadata = enableLogExportsAttr.GetMetadata() + if enableLogExportsAttr.Contains("audit") { + cluster.Logging.Audit = iacTypes.Bool(true, enableLogExportsAttr.GetMetadata()) + } + } + + storageEncryptedAttr := resource.GetAttribute("storage_encrypted") + cluster.StorageEncrypted = storageEncryptedAttr.AsBoolValueOrDefault(false, resource) + + KMSKeyAttr := resource.GetAttribute("kms_key_arn") + cluster.KMSKeyID = KMSKeyAttr.AsStringValueOrDefault("", resource) + + return cluster +} diff --git a/pkg/iac/adapters/terraform/aws/neptune/adapt_test.go b/pkg/iac/adapters/terraform/aws/neptune/adapt_test.go new file mode 100644 index 000000000000..77924183af99 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/neptune/adapt_test.go @@ -0,0 +1,87 @@ +package neptune + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/neptune" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptCluster(t *testing.T) { + tests := []struct { + name string + terraform string + expected neptune.Cluster + }{ + { + name: "configured", + terraform: ` + resource "aws_neptune_cluster" "example" { + enable_cloudwatch_logs_exports = ["audit"] + storage_encrypted = true + kms_key_arn = "kms-key" + } +`, + expected: neptune.Cluster{ + Logging: neptune.Logging{ + Audit: iacTypes.BoolTest(true), + }, + StorageEncrypted: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("kms-key"), + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_neptune_cluster" "example" { + } +`, + expected: neptune.Cluster{ + Logging: neptune.Logging{}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptCluster(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_neptune_cluster" "example" { + enable_cloudwatch_logs_exports = ["audit"] + storage_encrypted = true + kms_key_arn = "kms-key" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Clusters, 1) + cluster := adapted.Clusters[0] + + assert.Equal(t, 2, cluster.Metadata.Range().GetStartLine()) + assert.Equal(t, 6, cluster.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, cluster.Logging.Metadata.Range().GetStartLine()) + assert.Equal(t, 3, cluster.Logging.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, cluster.Logging.Audit.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, cluster.Logging.Audit.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, cluster.StorageEncrypted.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, cluster.StorageEncrypted.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 5, cluster.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, cluster.KMSKeyID.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/provider/adapt.go b/pkg/iac/adapters/terraform/aws/provider/adapt.go new file mode 100644 index 000000000000..2641a8299840 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/provider/adapt.go @@ -0,0 +1,166 @@ +package provider + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +const ( + defaultMaxRetires = 25 + defaultSharedConfigFile = "~/.aws/config" + //#nosec G101 -- False positive + defaultSharedCredentialsFile = "~/.aws/credentials" +) + +func Adapt(modules terraform.Modules) []aws.TerraformProvider { + return adaptProviders(modules) +} + +func adaptProviders(modules terraform.Modules) []aws.TerraformProvider { + var providers []aws.TerraformProvider + for _, providerBlock := range modules.GetBlocks().OfType("provider") { + if providerBlock.Label() == "aws" { + providers = append(providers, adaptProvider(providerBlock)) + } + } + + return providers +} + +func adaptProvider(b *terraform.Block) aws.TerraformProvider { + return aws.TerraformProvider{ + Metadata: b.GetMetadata(), + Alias: getStringAttrValue("alias", b), + Version: getStringAttrValue("version", b), + AccessKey: getStringAttrValue("access_key", b), + AllowedAccountsIDs: b.GetAttribute("allowed_account_ids").AsStringValueSliceOrEmpty(), + AssumeRole: adaptAssumeRole(b), + AssumeRoleWithWebIdentity: adaptAssumeRoleWithWebIdentity(b), + CustomCABundle: getStringAttrValue("custom_ca_bundle", b), + DefaultTags: adaptDefaultTags(b), + EC2MetadataServiceEndpoint: getStringAttrValue("ec2_metadata_service_endpoint", b), + EC2MetadataServiceEndpointMode: getStringAttrValue("ec2_metadata_service_endpoint_mode", b), + Endpoints: adaptEndpoints(b), + ForbiddenAccountIDs: b.GetAttribute("forbidden_account_ids").AsStringValueSliceOrEmpty(), + HttpProxy: getStringAttrValue("http_proxy", b), + IgnoreTags: adaptIgnoreTags(b), + Insecure: b.GetAttribute("insecure").AsBoolValueOrDefault(false, b), + MaxRetries: b.GetAttribute("max_retries").AsIntValueOrDefault(defaultMaxRetires, b), + Profile: getStringAttrValue("profile", b), + Region: getStringAttrValue("region", b), + RetryMode: getStringAttrValue("retry_mode", b), + S3UsePathStyle: b.GetAttribute("s3_use_path_style").AsBoolValueOrDefault(false, b), + S3USEast1RegionalEndpoint: getStringAttrValue("s3_us_east_1_regional_endpoint", b), + SecretKey: getStringAttrValue("secret_key", b), + SharedConfigFiles: b.GetAttribute("shared_config_files").AsStringValuesOrDefault(b, defaultSharedConfigFile), + SharedCredentialsFiles: b.GetAttribute("shared_credentials_files").AsStringValuesOrDefault(b, defaultSharedCredentialsFile), + SkipCredentialsValidation: b.GetAttribute("skip_credentials_validation").AsBoolValueOrDefault(false, b), + SkipMetadataAPICheck: b.GetAttribute("skip_metadata_api_check").AsBoolValueOrDefault(false, b), + SkipRegionValidation: b.GetAttribute("skip_region_validation").AsBoolValueOrDefault(false, b), + SkipRequestingAccountID: b.GetAttribute("skip_requesting_account_id").AsBoolValueOrDefault(false, b), + STSRegion: getStringAttrValue("sts_region", b), + Token: getStringAttrValue("token", b), + UseDualstackEndpoint: b.GetAttribute("use_dualstack_endpoint").AsBoolValueOrDefault(false, b), + UseFIPSEndpoint: b.GetAttribute("use_fips_endpoint").AsBoolValueOrDefault(false, b), + } +} + +func adaptAssumeRole(p *terraform.Block) aws.AssumeRole { + assumeRoleBlock := p.GetBlock("assume_role") + + if assumeRoleBlock.IsNil() { + return aws.AssumeRole{ + Metadata: p.GetMetadata(), + Duration: types.StringDefault("", p.GetMetadata()), + ExternalID: types.StringDefault("", p.GetMetadata()), + Policy: types.StringDefault("", p.GetMetadata()), + RoleARN: types.StringDefault("", p.GetMetadata()), + SessionName: types.StringDefault("", p.GetMetadata()), + SourceIdentity: types.StringDefault("", p.GetMetadata()), + } + } + + return aws.AssumeRole{ + Metadata: assumeRoleBlock.GetMetadata(), + Duration: getStringAttrValue("duration", p), + ExternalID: getStringAttrValue("external_id", p), + Policy: getStringAttrValue("policy", p), + PolicyARNs: p.GetAttribute("policy_arns").AsStringValueSliceOrEmpty(), + RoleARN: getStringAttrValue("role_arn", p), + SessionName: getStringAttrValue("session_name", p), + SourceIdentity: getStringAttrValue("source_identity", p), + Tags: p.GetAttribute("tags").AsMapValue(), + TransitiveTagKeys: p.GetAttribute("transitive_tag_keys").AsStringValueSliceOrEmpty(), + } +} + +func adaptAssumeRoleWithWebIdentity(p *terraform.Block) aws.AssumeRoleWithWebIdentity { + block := p.GetBlock("assume_role_with_web_identity") + if block.IsNil() { + return aws.AssumeRoleWithWebIdentity{ + Metadata: p.GetMetadata(), + Duration: types.StringDefault("", p.GetMetadata()), + Policy: types.StringDefault("", p.GetMetadata()), + RoleARN: types.StringDefault("", p.GetMetadata()), + SessionName: types.StringDefault("", p.GetMetadata()), + WebIdentityToken: types.StringDefault("", p.GetMetadata()), + WebIdentityTokenFile: types.StringDefault("", p.GetMetadata()), + } + } + + return aws.AssumeRoleWithWebIdentity{ + Metadata: block.GetMetadata(), + Duration: getStringAttrValue("duration", p), + Policy: getStringAttrValue("policy", p), + PolicyARNs: p.GetAttribute("policy_arns").AsStringValueSliceOrEmpty(), + RoleARN: getStringAttrValue("role_arn", p), + SessionName: getStringAttrValue("session_name", p), + WebIdentityToken: getStringAttrValue("web_identity_token", p), + WebIdentityTokenFile: getStringAttrValue("web_identity_token_file", p), + } +} + +func adaptEndpoints(p *terraform.Block) types.MapValue { + block := p.GetBlock("endpoints") + if block.IsNil() { + return types.MapDefault(make(map[string]string), p.GetMetadata()) + } + + values := make(map[string]string) + + for name, attr := range block.Attributes() { + values[name] = attr.AsStringValueOrDefault("", block).Value() + } + + return types.Map(values, block.GetMetadata()) +} + +func adaptDefaultTags(p *terraform.Block) aws.DefaultTags { + attr, _ := p.GetNestedAttribute("default_tags.tags") + if attr.IsNil() { + return aws.DefaultTags{} + } + + return aws.DefaultTags{ + Metadata: attr.GetMetadata(), + Tags: attr.AsMapValue(), + } +} + +func adaptIgnoreTags(p *terraform.Block) aws.IgnoreTags { + block := p.GetBlock("ignore_tags") + if block.IsNil() { + return aws.IgnoreTags{} + } + + return aws.IgnoreTags{ + Metadata: block.GetMetadata(), + Keys: block.GetAttribute("keys").AsStringValueSliceOrEmpty(), + KeyPrefixes: block.GetAttribute("key_prefixes").AsStringValueSliceOrEmpty(), + } +} + +func getStringAttrValue(name string, parent *terraform.Block) types.StringValue { + return parent.GetAttribute(name).AsStringValueOrDefault("", parent) +} diff --git a/pkg/iac/adapters/terraform/aws/provider/adapt_test.go b/pkg/iac/adapters/terraform/aws/provider/adapt_test.go new file mode 100644 index 000000000000..7189e4a31c36 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/provider/adapt_test.go @@ -0,0 +1,127 @@ +package provider + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestAdapt(t *testing.T) { + tests := []struct { + name string + source string + expected []aws.TerraformProvider + }{ + { + name: "happy", + source: ` +variable "s3_use_path_style" { + default = true +} + +provider "aws" { + version = "~> 5.0" + region = "us-east-1" + profile = "localstack" + + access_key = "fake" + secret_key = "fake" + skip_credentials_validation = true + skip_metadata_api_check = true + skip_requesting_account_id = true + s3_use_path_style = var.s3_use_path_style + + endpoints { + dynamodb = "http://localhost:4566" + s3 = "http://localhost:4566" + } + + default_tags { + tags = { + Environment = "Local" + Name = "LocalStack" + } + } +}`, + expected: []aws.TerraformProvider{ + { + Version: types.StringTest("~> 5.0"), + Region: types.StringTest("us-east-1"), + DefaultTags: aws.DefaultTags{ + Tags: types.MapTest(map[string]string{ + "Environment": "Local", + "Name": "LocalStack", + }), + }, + Endpoints: types.MapTest(map[string]string{ + "dynamodb": "http://localhost:4566", + "s3": "http://localhost:4566", + }), + Profile: types.StringTest("localstack"), + AccessKey: types.StringTest("fake"), + SecretKey: types.StringTest("fake"), + SkipCredentialsValidation: types.BoolTest(true), + SkipMetadataAPICheck: types.BoolTest(true), + SkipRequestingAccountID: types.BoolTest(true), + S3UsePathStyle: types.BoolTest(true), + MaxRetries: types.IntTest(defaultMaxRetires), + SharedConfigFiles: types.StringValueList{ + types.StringTest(defaultSharedConfigFile), + }, + SharedCredentialsFiles: types.StringValueList{ + types.StringTest(defaultSharedCredentialsFile), + }, + }, + }, + }, + { + name: "multiply provider configurations", + source: ` + +provider "aws" { + region = "us-east-1" +} + +provider "aws" { + alias = "west" + region = "us-west-2" +} +`, + expected: []aws.TerraformProvider{ + { + Region: types.StringTest("us-east-1"), + Endpoints: types.MapTest(make(map[string]string)), + MaxRetries: types.IntTest(defaultMaxRetires), + SharedConfigFiles: types.StringValueList{ + types.StringTest(defaultSharedConfigFile), + }, + SharedCredentialsFiles: types.StringValueList{ + types.StringTest(defaultSharedCredentialsFile), + }, + }, + { + Alias: types.StringTest("west"), + Region: types.StringTest("us-west-2"), + Endpoints: types.MapTest(make(map[string]string)), + MaxRetries: types.IntTest(defaultMaxRetires), + SharedConfigFiles: types.StringValueList{ + types.StringTest(defaultSharedConfigFile), + }, + SharedCredentialsFiles: types.StringValueList{ + types.StringTest(defaultSharedCredentialsFile), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.source, ".tf") + testutil.AssertDefsecEqual(t, test.expected, Adapt(modules)) + }) + } +} diff --git a/pkg/iac/adapters/terraform/aws/rds/adapt.go b/pkg/iac/adapters/terraform/aws/rds/adapt.go new file mode 100644 index 000000000000..68d302e26a99 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/rds/adapt.go @@ -0,0 +1,256 @@ +package rds + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/rds" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) rds.RDS { + return rds.RDS{ + Instances: getInstances(modules), + Clusters: getClusters(modules), + Classic: getClassic(modules), + Snapshots: getSnapshots(modules), + ParameterGroups: getParameterGroups(modules), + } +} + +func getInstances(modules terraform.Modules) (instances []rds.Instance) { + for _, resource := range modules.GetResourcesByType("aws_db_instance") { + instances = append(instances, adaptInstance(resource, modules)) + } + + return instances +} + +func getParameterGroups(modules terraform.Modules) (parametergroups []rds.ParameterGroups) { + for _, resource := range modules.GetResourcesByType("aws_db_parameter_group") { + parametergroups = append(parametergroups, adaptDBParameterGroups(resource, modules)) + } + + return parametergroups +} + +func getSnapshots(modules terraform.Modules) (snapshots []rds.Snapshots) { + for _, resource := range modules.GetResourcesByType("aws_db_snapshot") { + snapshots = append(snapshots, adaptDBSnapshots(resource, modules)) + } + + return snapshots +} + +func getClusters(modules terraform.Modules) (clusters []rds.Cluster) { + + rdsInstanceMaps := modules.GetChildResourceIDMapByType("aws_rds_cluster_instance") + for _, resource := range modules.GetResourcesByType("aws_rds_cluster") { + cluster, instanceIDs := adaptCluster(resource, modules) + for _, id := range instanceIDs { + rdsInstanceMaps.Resolve(id) + } + clusters = append(clusters, cluster) + } + + orphanResources := modules.GetResourceByIDs(rdsInstanceMaps.Orphans()...) + + if len(orphanResources) > 0 { + orphanage := rds.Cluster{ + Metadata: iacTypes.NewUnmanagedMetadata(), + BackupRetentionPeriodDays: iacTypes.IntDefault(1, iacTypes.NewUnmanagedMetadata()), + ReplicationSourceARN: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Enabled: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + KMSKeyID: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + Instances: nil, + Encryption: rds.Encryption{ + Metadata: iacTypes.NewUnmanagedMetadata(), + EncryptStorage: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + KMSKeyID: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + PublicAccess: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + Engine: iacTypes.StringUnresolvable(iacTypes.NewUnmanagedMetadata()), + LatestRestorableTime: iacTypes.TimeUnresolvable(iacTypes.NewUnmanagedMetadata()), + DeletionProtection: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + } + for _, orphan := range orphanResources { + orphanage.Instances = append(orphanage.Instances, adaptClusterInstance(orphan, modules)) + } + clusters = append(clusters, orphanage) + } + + return clusters +} + +func getClassic(modules terraform.Modules) rds.Classic { + classic := rds.Classic{ + DBSecurityGroups: nil, + } + for _, resource := range modules.GetResourcesByType("aws_db_security_group", "aws_redshift_security_group", "aws_elasticache_security_group") { + classic.DBSecurityGroups = append(classic.DBSecurityGroups, adaptClassicDBSecurityGroup(resource)) + } + return classic +} + +func adaptClusterInstance(resource *terraform.Block, modules terraform.Modules) rds.ClusterInstance { + clusterIdAttr := resource.GetAttribute("cluster_identifier") + clusterId := clusterIdAttr.AsStringValueOrDefault("", resource) + + if clusterIdAttr.IsResourceBlockReference("aws_rds_cluster") { + if referenced, err := modules.GetReferencedBlock(clusterIdAttr, resource); err == nil { + clusterId = iacTypes.String(referenced.FullName(), referenced.GetMetadata()) + } + } + + return rds.ClusterInstance{ + ClusterIdentifier: clusterId, + Instance: adaptInstance(resource, modules), + } +} + +func adaptClassicDBSecurityGroup(resource *terraform.Block) rds.DBSecurityGroup { + return rds.DBSecurityGroup{ + Metadata: resource.GetMetadata(), + } +} + +func adaptInstance(resource *terraform.Block, modules terraform.Modules) rds.Instance { + + var ReadReplicaDBInstanceIdentifiers []iacTypes.StringValue + rrdiAttr := resource.GetAttribute("replicate_source_db") + for _, rrdi := range rrdiAttr.AsStringValues() { + ReadReplicaDBInstanceIdentifiers = append(ReadReplicaDBInstanceIdentifiers, rrdi) + } + + var TagList []rds.TagList + tagres := resource.GetBlocks("tags") + for _, tagres := range tagres { + + TagList = append(TagList, rds.TagList{ + Metadata: tagres.GetMetadata(), + }) + } + + var EnabledCloudwatchLogsExports []iacTypes.StringValue + ecweAttr := resource.GetAttribute("enabled_cloudwatch_logs_exports") + for _, ecwe := range ecweAttr.AsStringValues() { + EnabledCloudwatchLogsExports = append(EnabledCloudwatchLogsExports, ecwe) + } + + replicaSource := resource.GetAttribute("replicate_source_db") + replicaSourceValue := "" + if replicaSource.IsNotNil() { + if referenced, err := modules.GetReferencedBlock(replicaSource, resource); err == nil { + replicaSourceValue = referenced.ID() + } + } + return rds.Instance{ + Metadata: resource.GetMetadata(), + BackupRetentionPeriodDays: resource.GetAttribute("backup_retention_period").AsIntValueOrDefault(0, resource), + ReplicationSourceARN: iacTypes.StringExplicit(replicaSourceValue, resource.GetMetadata()), + PerformanceInsights: adaptPerformanceInsights(resource), + Encryption: adaptEncryption(resource), + PublicAccess: resource.GetAttribute("publicly_accessible").AsBoolValueOrDefault(false, resource), + Engine: resource.GetAttribute("engine").AsStringValueOrDefault(rds.EngineAurora, resource), + IAMAuthEnabled: resource.GetAttribute("iam_database_authentication_enabled").AsBoolValueOrDefault(false, resource), + DeletionProtection: resource.GetAttribute("deletion_protection").AsBoolValueOrDefault(false, resource), + DBInstanceArn: resource.GetAttribute("arn").AsStringValueOrDefault("", resource), + StorageEncrypted: resource.GetAttribute("storage_encrypted").AsBoolValueOrDefault(true, resource), + DBInstanceIdentifier: resource.GetAttribute("identifier").AsStringValueOrDefault("", resource), + EngineVersion: resource.GetAttribute("engine_version").AsStringValueOrDefault("", resource), + AutoMinorVersionUpgrade: resource.GetAttribute("auto_minor_version_upgrade").AsBoolValueOrDefault(false, resource), + MultiAZ: resource.GetAttribute("multi_az").AsBoolValueOrDefault(false, resource), + PubliclyAccessible: resource.GetAttribute("publicly_accessible").AsBoolValueOrDefault(false, resource), + LatestRestorableTime: iacTypes.TimeUnresolvable(resource.GetMetadata()), + ReadReplicaDBInstanceIdentifiers: ReadReplicaDBInstanceIdentifiers, + TagList: TagList, + EnabledCloudwatchLogsExports: EnabledCloudwatchLogsExports, + } +} + +func adaptDBParameterGroups(resource *terraform.Block, _ terraform.Modules) rds.ParameterGroups { + + var Parameters []rds.Parameters + paramres := resource.GetBlocks("parameter") + for _, paramres := range paramres { + + Parameters = append(Parameters, rds.Parameters{ + Metadata: paramres.GetMetadata(), + ParameterName: iacTypes.StringDefault("", paramres.GetMetadata()), + ParameterValue: iacTypes.StringDefault("", paramres.GetMetadata()), + }) + } + + return rds.ParameterGroups{ + Metadata: resource.GetMetadata(), + DBParameterGroupName: resource.GetAttribute("name").AsStringValueOrDefault("", resource), + DBParameterGroupFamily: resource.GetAttribute("family").AsStringValueOrDefault("", resource), + Parameters: Parameters, + } +} + +func adaptDBSnapshots(resource *terraform.Block, _ terraform.Modules) rds.Snapshots { + + return rds.Snapshots{ + Metadata: resource.GetMetadata(), + DBSnapshotIdentifier: resource.GetAttribute("db_snapshot_identifier").AsStringValueOrDefault("", resource), + DBSnapshotArn: resource.GetAttribute("db_snapshot_arn").AsStringValueOrDefault("", resource), + Encrypted: resource.GetAttribute("encrypted").AsBoolValueOrDefault(true, resource), + KmsKeyId: resource.GetAttribute("kms_key_id").AsStringValueOrDefault("", resource), + SnapshotAttributes: nil, + } +} + +func adaptCluster(resource *terraform.Block, modules terraform.Modules) (rds.Cluster, []string) { + + clusterInstances, ids := getClusterInstances(resource, modules) + + var public bool + for _, instance := range clusterInstances { + if instance.PublicAccess.IsTrue() { + public = true + break + } + } + + return rds.Cluster{ + Metadata: resource.GetMetadata(), + BackupRetentionPeriodDays: resource.GetAttribute("backup_retention_period").AsIntValueOrDefault(1, resource), + ReplicationSourceARN: resource.GetAttribute("replication_source_identifier").AsStringValueOrDefault("", resource), + PerformanceInsights: adaptPerformanceInsights(resource), + Instances: clusterInstances, + Encryption: adaptEncryption(resource), + PublicAccess: iacTypes.Bool(public, resource.GetMetadata()), + Engine: resource.GetAttribute("engine").AsStringValueOrDefault(rds.EngineAurora, resource), + LatestRestorableTime: iacTypes.TimeUnresolvable(resource.GetMetadata()), + AvailabilityZones: resource.GetAttribute("availability_zones").AsStringValueSliceOrEmpty(), + DeletionProtection: resource.GetAttribute("deletion_protection").AsBoolValueOrDefault(false, resource), + }, ids +} + +func getClusterInstances(resource *terraform.Block, modules terraform.Modules) (clusterInstances []rds.ClusterInstance, instanceIDs []string) { + clusterInstanceResources := modules.GetReferencingResources(resource, "aws_rds_cluster_instance", "cluster_identifier") + + for _, ciResource := range clusterInstanceResources { + instanceIDs = append(instanceIDs, ciResource.ID()) + clusterInstances = append(clusterInstances, adaptClusterInstance(ciResource, modules)) + } + return clusterInstances, instanceIDs +} + +func adaptPerformanceInsights(resource *terraform.Block) rds.PerformanceInsights { + return rds.PerformanceInsights{ + Metadata: resource.GetMetadata(), + Enabled: resource.GetAttribute("performance_insights_enabled").AsBoolValueOrDefault(false, resource), + KMSKeyID: resource.GetAttribute("performance_insights_kms_key_id").AsStringValueOrDefault("", resource), + } +} + +func adaptEncryption(resource *terraform.Block) rds.Encryption { + return rds.Encryption{ + Metadata: resource.GetMetadata(), + EncryptStorage: resource.GetAttribute("storage_encrypted").AsBoolValueOrDefault(false, resource), + KMSKeyID: resource.GetAttribute("kms_key_id").AsStringValueOrDefault("", resource), + } +} diff --git a/pkg/iac/adapters/terraform/aws/rds/adapt_test.go b/pkg/iac/adapters/terraform/aws/rds/adapt_test.go new file mode 100644 index 000000000000..f17bf26cb346 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/rds/adapt_test.go @@ -0,0 +1,287 @@ +package rds + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/rds" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected rds.RDS + }{ + { + name: "defined", + terraform: ` + + resource "aws_rds_cluster" "example" { + engine = "aurora-mysql" + availability_zones = ["us-west-2a", "us-west-2b", "us-west-2c"] + backup_retention_period = 7 + kms_key_id = "kms_key_1" + storage_encrypted = true + replication_source_identifier = "arn-of-a-source-db-cluster" + deletion_protection = true + } + + resource "aws_rds_cluster_instance" "example" { + cluster_identifier = aws_rds_cluster.example.id + name = "bar" + performance_insights_enabled = true + performance_insights_kms_key_id = "performance_key_0" + kms_key_id = "kms_key_0" + storage_encrypted = true + } + + resource "aws_db_security_group" "example" { + # ... + } + + resource "aws_db_instance" "example" { + publicly_accessible = false + backup_retention_period = 5 + skip_final_snapshot = true + performance_insights_enabled = true + performance_insights_kms_key_id = "performance_key_1" + storage_encrypted = true + kms_key_id = "kms_key_2" + } +`, + expected: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.IntTest(5), + PerformanceInsights: rds.PerformanceInsights{ + Enabled: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("performance_key_1"), + }, + Encryption: rds.Encryption{ + EncryptStorage: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("kms_key_2"), + }, + Engine: iacTypes.StringTest(rds.EngineAurora), + StorageEncrypted: iacTypes.BoolTest(true), + }, + }, + Clusters: []rds.Cluster{ + { + BackupRetentionPeriodDays: iacTypes.IntTest(7), + ReplicationSourceARN: iacTypes.StringTest("arn-of-a-source-db-cluster"), + PerformanceInsights: rds.PerformanceInsights{}, + Encryption: rds.Encryption{ + EncryptStorage: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("kms_key_1"), + }, + Instances: []rds.ClusterInstance{ + { + Instance: rds.Instance{ + PerformanceInsights: rds.PerformanceInsights{ + Enabled: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("performance_key_0"), + }, + Encryption: rds.Encryption{ + EncryptStorage: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("kms_key_0"), + }, + Engine: iacTypes.StringTest(rds.EngineAurora), + StorageEncrypted: iacTypes.BoolTest(true), + }, + ClusterIdentifier: iacTypes.StringTest("aws_rds_cluster.example"), + }, + }, + Engine: iacTypes.StringTest(rds.EngineAuroraMysql), + AvailabilityZones: iacTypes.StringValueList{ + iacTypes.StringTest("us-west-2a"), + iacTypes.StringTest("us-west-2b"), + iacTypes.StringTest("us-west-2c"), + }, + DeletionProtection: iacTypes.BoolTest(true), + }, + }, + Classic: rds.Classic{ + DBSecurityGroups: []rds.DBSecurityGroup{ + {}, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptInstance(t *testing.T) { + tests := []struct { + name string + terraform string + expected rds.Instance + }{ + { + name: "instance defaults", + terraform: ` + resource "aws_db_instance" "example" { + } +`, + expected: rds.Instance{ + PerformanceInsights: rds.PerformanceInsights{}, + Encryption: rds.Encryption{}, + Engine: iacTypes.StringTest(rds.EngineAurora), + StorageEncrypted: iacTypes.BoolTest(true), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptInstance(modules.GetBlocks()[0], modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptCluster(t *testing.T) { + tests := []struct { + name string + terraform string + expected rds.Cluster + }{ + { + name: "cluster defaults", + terraform: ` + resource "aws_rds_cluster" "example" { + } +`, + expected: rds.Cluster{ + BackupRetentionPeriodDays: iacTypes.IntTest(1), + PerformanceInsights: rds.PerformanceInsights{}, + Encryption: rds.Encryption{}, + Engine: iacTypes.StringTest(rds.EngineAurora), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted, _ := adaptCluster(modules.GetBlocks()[0], modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_rds_cluster" "example" { + backup_retention_period = 7 + kms_key_id = "kms_key_1" + storage_encrypted = true + replication_source_identifier = "arn-of-a-source-db-cluster" + } + + resource "aws_rds_cluster_instance" "example" { + cluster_identifier = aws_rds_cluster.example.id + backup_retention_period = 7 + performance_insights_enabled = true + performance_insights_kms_key_id = "performance_key" + storage_encrypted = true + kms_key_id = "kms_key_0" + } + + resource "aws_db_security_group" "example" { + } + + resource "aws_db_instance" "example" { + publicly_accessible = false + backup_retention_period = 7 + performance_insights_enabled = true + performance_insights_kms_key_id = "performance_key" + storage_encrypted = true + kms_key_id = "kms_key_0" + } +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Clusters, 1) + require.Len(t, adapted.Instances, 1) + + cluster := adapted.Clusters[0] + instance := adapted.Instances[0] + classic := adapted.Classic + + assert.Equal(t, 2, cluster.Metadata.Range().GetStartLine()) + assert.Equal(t, 7, cluster.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, cluster.BackupRetentionPeriodDays.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, cluster.BackupRetentionPeriodDays.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, cluster.Encryption.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, cluster.Encryption.KMSKeyID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 5, cluster.Encryption.EncryptStorage.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, cluster.Encryption.EncryptStorage.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, cluster.ReplicationSourceARN.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, cluster.ReplicationSourceARN.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 9, cluster.Instances[0].Instance.Metadata.Range().GetStartLine()) + assert.Equal(t, 16, cluster.Instances[0].Instance.Metadata.Range().GetEndLine()) + + assert.Equal(t, 2, cluster.Instances[0].ClusterIdentifier.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, cluster.Instances[0].ClusterIdentifier.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, cluster.Instances[0].Instance.BackupRetentionPeriodDays.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, cluster.Instances[0].Instance.BackupRetentionPeriodDays.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 12, cluster.Instances[0].Instance.PerformanceInsights.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 12, cluster.Instances[0].Instance.PerformanceInsights.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, cluster.Instances[0].Instance.PerformanceInsights.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 13, cluster.Instances[0].Instance.PerformanceInsights.KMSKeyID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 14, cluster.Instances[0].Instance.Encryption.EncryptStorage.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 14, cluster.Instances[0].Instance.Encryption.EncryptStorage.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 15, cluster.Instances[0].Instance.Encryption.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 15, cluster.Instances[0].Instance.Encryption.KMSKeyID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 18, classic.DBSecurityGroups[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 19, classic.DBSecurityGroups[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 21, instance.Metadata.Range().GetStartLine()) + assert.Equal(t, 28, instance.Metadata.Range().GetEndLine()) + + assert.Equal(t, 22, instance.PublicAccess.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 22, instance.PublicAccess.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 23, instance.BackupRetentionPeriodDays.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 23, instance.BackupRetentionPeriodDays.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 24, instance.PerformanceInsights.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 24, instance.PerformanceInsights.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 25, instance.PerformanceInsights.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 25, instance.PerformanceInsights.KMSKeyID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 26, instance.Encryption.EncryptStorage.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 26, instance.Encryption.EncryptStorage.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 27, instance.Encryption.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 27, instance.Encryption.KMSKeyID.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/redshift/adapt.go b/pkg/iac/adapters/terraform/aws/redshift/adapt.go new file mode 100644 index 000000000000..37ede1a821ab --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/redshift/adapt.go @@ -0,0 +1,117 @@ +package redshift + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/redshift" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) redshift.Redshift { + return redshift.Redshift{ + Clusters: adaptClusters(modules), + SecurityGroups: adaptSecurityGroups(modules), + ClusterParameters: adaptParameters(modules), + ReservedNodes: nil, + } +} + +func adaptClusters(modules terraform.Modules) []redshift.Cluster { + var clusters []redshift.Cluster + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_redshift_cluster") { + clusters = append(clusters, adaptCluster(resource, module)) + } + } + return clusters +} + +func adaptSecurityGroups(modules terraform.Modules) []redshift.SecurityGroup { + var securityGroups []redshift.SecurityGroup + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_redshift_security_group") { + securityGroups = append(securityGroups, adaptSecurityGroup(resource)) + } + } + return securityGroups +} + +func adaptParameters(modules terraform.Modules) []redshift.ClusterParameter { + var Parameters []redshift.ClusterParameter + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_redshift_parameter_group") { + for _, r := range resource.GetBlocks("parameter") { + Parameters = append(Parameters, adaptParameter(r)) + } + } + } + return Parameters +} + +func adaptCluster(resource *terraform.Block, module *terraform.Module) redshift.Cluster { + cluster := redshift.Cluster{ + Metadata: resource.GetMetadata(), + ClusterIdentifier: resource.GetAttribute("cluster_identifier").AsStringValueOrDefault("", resource), + NodeType: resource.GetAttribute("node_type").AsStringValueOrDefault("", resource), + MasterUsername: resource.GetAttribute("master_username").AsStringValueOrDefault("", resource), + NumberOfNodes: resource.GetAttribute("number_of_nodes").AsIntValueOrDefault(1, resource), + PubliclyAccessible: resource.GetAttribute("publicly_accessible").AsBoolValueOrDefault(true, resource), + LoggingEnabled: iacTypes.Bool(false, resource.GetMetadata()), + AutomatedSnapshotRetentionPeriod: iacTypes.Int(0, resource.GetMetadata()), + AllowVersionUpgrade: resource.GetAttribute("allow_version_upgrade").AsBoolValueOrDefault(true, resource), + VpcId: iacTypes.String("", resource.GetMetadata()), + Encryption: redshift.Encryption{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + KMSKeyID: iacTypes.StringDefault("", resource.GetMetadata()), + }, + EndPoint: redshift.EndPoint{ + Metadata: resource.GetMetadata(), + Port: resource.GetAttribute("port").AsIntValueOrDefault(5439, resource), + }, + SubnetGroupName: iacTypes.StringDefault("", resource.GetMetadata()), + } + + encryptedAttr := resource.GetAttribute("encrypted") + cluster.Encryption.Enabled = encryptedAttr.AsBoolValueOrDefault(false, resource) + + if logBlock := resource.GetBlock("logging"); logBlock.IsNotNil() { + cluster.LoggingEnabled = logBlock.GetAttribute("enable").AsBoolValueOrDefault(false, logBlock) + } + + if snapBlock := resource.GetBlock("snapshot_copy"); snapBlock.IsNotNil() { + snapAttr := snapBlock.GetAttribute("retention_period") + cluster.AutomatedSnapshotRetentionPeriod = snapAttr.AsIntValueOrDefault(7, snapBlock) + } + + KMSKeyIDAttr := resource.GetAttribute("kms_key_id") + cluster.Encryption.KMSKeyID = KMSKeyIDAttr.AsStringValueOrDefault("", resource) + if KMSKeyIDAttr.IsResourceBlockReference("aws_kms_key") { + if kmsKeyBlock, err := module.GetReferencedBlock(KMSKeyIDAttr, resource); err == nil { + cluster.Encryption.KMSKeyID = iacTypes.String(kmsKeyBlock.FullName(), kmsKeyBlock.GetMetadata()) + } + } + + subnetGroupNameAttr := resource.GetAttribute("cluster_subnet_group_name") + cluster.SubnetGroupName = subnetGroupNameAttr.AsStringValueOrDefault("", resource) + + return cluster +} + +func adaptSecurityGroup(resource *terraform.Block) redshift.SecurityGroup { + descriptionAttr := resource.GetAttribute("description") + descriptionVal := descriptionAttr.AsStringValueOrDefault("Managed by Terraform", resource) + + return redshift.SecurityGroup{ + Metadata: resource.GetMetadata(), + Description: descriptionVal, + } +} + +func adaptParameter(resource *terraform.Block) redshift.ClusterParameter { + + return redshift.ClusterParameter{ + Metadata: resource.GetMetadata(), + ParameterName: resource.GetAttribute("name").AsStringValueOrDefault("", resource), + ParameterValue: resource.GetAttribute("value").AsStringValueOrDefault("", resource), + } +} diff --git a/pkg/iac/adapters/terraform/aws/redshift/adapt_test.go b/pkg/iac/adapters/terraform/aws/redshift/adapt_test.go new file mode 100644 index 000000000000..3c5520ffc7b1 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/redshift/adapt_test.go @@ -0,0 +1,207 @@ +package redshift + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/redshift" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected redshift.Redshift + }{ + { + name: "reference key id", + terraform: ` + resource "aws_kms_key" "redshift" { + enable_key_rotation = true + } + + resource "aws_redshift_cluster" "example" { + cluster_identifier = "tf-redshift-cluster" + publicly_accessible = false + number_of_nodes = 1 + allow_version_upgrade = false + port = 5440 + encrypted = true + kms_key_id = aws_kms_key.redshift.key_id + cluster_subnet_group_name = "redshift_subnet" + } + + resource "aws_redshift_security_group" "default" { + name = "redshift-sg" + description = "some description" + } +`, + expected: redshift.Redshift{ + Clusters: []redshift.Cluster{ + { + ClusterIdentifier: iacTypes.StringTest("tf-redshift-cluster"), + NumberOfNodes: iacTypes.IntTest(1), + EndPoint: redshift.EndPoint{ + Port: iacTypes.IntTest(5440), + }, + Encryption: redshift.Encryption{ + Enabled: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("aws_kms_key.redshift"), + }, + SubnetGroupName: iacTypes.StringTest("redshift_subnet"), + }, + }, + SecurityGroups: []redshift.SecurityGroup{ + { + Description: iacTypes.StringTest("some description"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptCluster(t *testing.T) { + tests := []struct { + name string + terraform string + expected redshift.Cluster + }{ + { + name: "key as string", + terraform: ` + resource "aws_redshift_cluster" "example" { + cluster_identifier = "tf-redshift-cluster" + publicly_accessible = false + number_of_nodes = 1 + allow_version_upgrade = false + port = 5440 + encrypted = true + kms_key_id = "key-id" + cluster_subnet_group_name = "redshift_subnet" + } +`, + expected: redshift.Cluster{ + ClusterIdentifier: iacTypes.StringTest("tf-redshift-cluster"), + NumberOfNodes: iacTypes.IntTest(1), + EndPoint: redshift.EndPoint{ + Port: iacTypes.IntTest(5440), + }, + Encryption: redshift.Encryption{ + Enabled: iacTypes.BoolTest(true), + KMSKeyID: iacTypes.StringTest("key-id"), + }, + SubnetGroupName: iacTypes.StringTest("redshift_subnet"), + }, + }, + { + name: "defaults", + terraform: ` + resource "aws_redshift_cluster" "example" { + } +`, + expected: redshift.Cluster{ + PubliclyAccessible: iacTypes.BoolTest(true), + NumberOfNodes: iacTypes.IntTest(1), + AllowVersionUpgrade: iacTypes.BoolTest(true), + EndPoint: redshift.EndPoint{ + Port: iacTypes.IntTest(5439), + }, + Encryption: redshift.Encryption{}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptCluster(modules.GetBlocks()[0], modules[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptSecurityGroup(t *testing.T) { + tests := []struct { + name string + terraform string + expected redshift.SecurityGroup + }{ + { + name: "defaults", + terraform: ` +resource "" "example" { +} +`, + expected: redshift.SecurityGroup{ + Description: iacTypes.StringTest("Managed by Terraform"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptSecurityGroup(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_kms_key" "redshift" { + enable_key_rotation = true + } + + resource "aws_redshift_cluster" "example" { + cluster_identifier = "tf-redshift-cluster" + encrypted = true + kms_key_id = aws_kms_key.redshift.key_id + cluster_subnet_group_name = "subnet name" + } + + resource "aws_redshift_security_group" "default" { + name = "redshift-sg" + description = "some description" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Clusters, 1) + require.Len(t, adapted.SecurityGroups, 1) + cluster := adapted.Clusters[0] + securityGroup := adapted.SecurityGroups[0] + + assert.Equal(t, 6, cluster.Metadata.Range().GetStartLine()) + assert.Equal(t, 11, cluster.Metadata.Range().GetEndLine()) + + assert.Equal(t, 8, cluster.Encryption.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 8, cluster.Encryption.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 2, cluster.Encryption.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, cluster.Encryption.KMSKeyID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 10, cluster.SubnetGroupName.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, cluster.SubnetGroupName.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, securityGroup.Metadata.Range().GetStartLine()) + assert.Equal(t, 16, securityGroup.Metadata.Range().GetEndLine()) + + assert.Equal(t, 15, securityGroup.Description.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 15, securityGroup.Description.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/s3/adapt.go b/pkg/iac/adapters/terraform/aws/s3/adapt.go new file mode 100644 index 000000000000..ef9c3052eec5 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/s3/adapt.go @@ -0,0 +1,18 @@ +package s3 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) s3.S3 { + + a := &adapter{ + modules: modules, + bucketMap: make(map[string]*s3.Bucket), + } + + return s3.S3{ + Buckets: a.adaptBuckets(), + } +} diff --git a/pkg/iac/adapters/terraform/aws/s3/adapt_test.go b/pkg/iac/adapters/terraform/aws/s3/adapt_test.go new file mode 100644 index 000000000000..2293d7643c1f --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/s3/adapt_test.go @@ -0,0 +1,454 @@ +package s3 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_PublicAccessBlock(t *testing.T) { + testCases := []struct { + desc string + source string + expectedBuckets int + hasPublicAccess bool + }{ + { + desc: "public access block is found when using the bucket name as the lookup", + source: ` +resource "aws_s3_bucket" "example" { + bucket = "bucketname" +} + +resource "aws_s3_bucket_public_access_block" "example_access_block"{ + bucket = "bucketname" +} +`, + expectedBuckets: 1, + hasPublicAccess: true, + }, + { + desc: "public access block is found when using the bucket id as the lookup", + source: ` +resource "aws_s3_bucket" "example" { + bucket = "bucketname" +} + +resource "aws_s3_bucket_public_access_block" "example_access_block"{ + bucket = aws_s3_bucket.example.id +} +`, + expectedBuckets: 1, + hasPublicAccess: true, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + + modules := tftestutil.CreateModulesFromSource(t, tC.source, ".tf") + s3Ctx := Adapt(modules) + + assert.Len(t, s3Ctx.Buckets, tC.expectedBuckets) + + for _, bucket := range s3Ctx.Buckets { + if tC.hasPublicAccess { + assert.NotNil(t, bucket.PublicAccessBlock) + } else { + assert.Nil(t, bucket.PublicAccessBlock) + } + } + + bucket := s3Ctx.Buckets[0] + assert.NotNil(t, bucket.PublicAccessBlock) + + }) + } + +} + +func Test_PublicAccessDoesNotReference(t *testing.T) { + testCases := []struct { + desc string + source string + }{ + { + desc: "just a bucket, no public access block", + source: ` +resource "aws_s3_bucket" "example" { + bucket = "bucketname" +} + `, + }, + { + desc: "bucket with unrelated public access block", + source: ` +resource "aws_s3_bucket" "example" { + bucket = "bucketname" +} + +resource "aws_s3_bucket_public_access_block" "example_access_block"{ + bucket = aws_s3_bucket.other.id +} + `, + }, + { + desc: "bucket with unrelated public access block via name", + source: ` +resource "aws_s3_bucket" "example" { + bucket = "bucketname" +} + +resource "aws_s3_bucket_public_access_block" "example_access_block"{ + bucket = "something" +} + `, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, tC.source, ".tf") + s3Ctx := Adapt(modules) + require.Len(t, s3Ctx.Buckets, 1) + assert.Nil(t, s3Ctx.Buckets[0].PublicAccessBlock) + + }) + } +} + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected s3.S3 + }{ + { + name: "basic", + terraform: ` + resource "aws_s3_bucket" "example" { + bucket = "bucket" + } + + resource "aws_s3_bucket_public_access_block" "example" { + bucket = aws_s3_bucket.example.id + + restrict_public_buckets = true + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + + } + + resource "aws_s3_bucket_acl" "example" { + bucket = aws_s3_bucket.example.id + acl = "private" + access_control_policy { + grant { + grantee { + type = "Group" + uri = "http://acs.amazonaws.com/groups/s3/LogDelivery" + } + permission = "READ_ACP" + } + } + } + + resource "aws_s3_bucket_server_side_encryption_configuration" "example" { + bucket = aws_s3_bucket.example.bucket + + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "string-key" + sse_algorithm = "aws:kms" + } + } + } + + resource "aws_s3_bucket_logging" "example" { + bucket = aws_s3_bucket.example.id + + target_bucket = aws_s3_bucket.example.id + target_prefix = "log/" + } + + resource "aws_s3_bucket_versioning" "versioning_example" { + bucket = aws_s3_bucket.example.id + versioning_configuration { + status = "Enabled" + mfa_delete = "Enabled" + } + } + + resource "aws_s3_bucket_policy" "allow_access_from_another_account" { + bucket = aws_s3_bucket.example.bucket + policy = data.aws_iam_policy_document.allow_access_from_another_account.json + } + + data "aws_iam_policy_document" "allow_access_from_another_account" { + statement { + + actions = [ + "s3:GetObject", + "s3:ListBucket", + ] + + resources = [ + "arn:aws:s3:::*", + ] + } + } + `, + expected: s3.S3{ + Buckets: []s3.Bucket{ + { + Name: iacTypes.StringTest("bucket"), + PublicAccessBlock: &s3.PublicAccessBlock{ + BlockPublicACLs: iacTypes.BoolTest(true), + BlockPublicPolicy: iacTypes.BoolTest(true), + IgnorePublicACLs: iacTypes.BoolTest(true), + RestrictPublicBuckets: iacTypes.BoolTest(true), + }, + BucketPolicies: []iam.Policy{ + { + Document: func() iam.Document { + + builder := iamgo.NewPolicyBuilder() + + sb := iamgo.NewStatementBuilder() + sb.WithEffect(iamgo.EffectAllow) + sb.WithActions([]string{"s3:GetObject", "s3:ListBucket"}) + sb.WithResources([]string{"arn:aws:s3:::*"}) + + builder.WithStatement(sb.Build()) + + return iam.Document{ + Parsed: builder.Build(), + IsOffset: true, + HasRefs: false, + } + }(), + }, + }, + Encryption: s3.Encryption{ + Enabled: iacTypes.BoolTest(true), + Algorithm: iacTypes.StringTest("aws:kms"), + KMSKeyId: iacTypes.StringTest("string-key"), + }, + Versioning: s3.Versioning{ + Enabled: iacTypes.BoolTest(true), + MFADelete: iacTypes.BoolTest(true), + }, + Logging: s3.Logging{ + Enabled: iacTypes.BoolTest(true), + TargetBucket: iacTypes.StringTest("aws_s3_bucket.example"), + }, + ACL: iacTypes.StringTest("private"), + Grants: []s3.Grant{ + { + Grantee: s3.Grantee{ + Type: iacTypes.StringTest("Group"), + URI: iacTypes.StringTest("http://acs.amazonaws.com/groups/s3/LogDelivery"), + }, + Permissions: iacTypes.StringValueList{ + iacTypes.StringTest("READ_ACP"), + }, + }, + }, + }, + }, + }, + }, + { + name: "bucket with grants", + terraform: ` +resource "aws_s3_bucket" "this" { + bucket = "test" + + grant { + type = "Group" + uri = "http://acs.amazonaws.com/groups/s3/LogDelivery" + permissions = ["FULL_CONTROL"] + } +} +`, + expected: s3.S3{ + Buckets: []s3.Bucket{ + { + Name: iacTypes.StringTest("test"), + ACL: iacTypes.StringTest("private"), + Grants: []s3.Grant{ + { + Grantee: s3.Grantee{ + Type: iacTypes.StringTest("Group"), + URI: iacTypes.StringTest("http://acs.amazonaws.com/groups/s3/LogDelivery"), + }, + Permissions: iacTypes.StringValueList{ + iacTypes.StringTest("FULL_CONTROL"), + }, + }, + }, + }, + }, + }, + }, + { + name: "non-valid SSE algorithm", + terraform: ` +resource "aws_s3_bucket" "this" { + bucket = "test" +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "this" { + bucket = aws_s3_bucket.this.id + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "" + } + } +}`, + expected: s3.S3{ + Buckets: []s3.Bucket{ + { + Name: iacTypes.StringTest("test"), + Encryption: s3.Encryption{}, + ACL: iacTypes.StringTest("private"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_s3_bucket" "example" { + bucket = "bucket" + } + + resource "aws_s3_bucket_public_access_block" "example" { + bucket = aws_s3_bucket.example.id + + restrict_public_buckets = true + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + } + + resource "aws_s3_bucket_acl" "example" { + bucket = aws_s3_bucket.example.id + acl = "private" + } + + resource "aws_s3_bucket_server_side_encryption_configuration" "example" { + bucket = aws_s3_bucket.example.bucket + + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = "string-key" + sse_algorithm = "aws:kms" + } + } + } + + resource "aws_s3_bucket_logging" "example" { + bucket = aws_s3_bucket.example.id + + target_bucket = aws_s3_bucket.example.id + target_prefix = "log/" + } + + resource "aws_s3_bucket_versioning" "versioning_example" { + bucket = aws_s3_bucket.example.id + versioning_configuration { + status = "Enabled" + } + } + + resource "aws_s3_bucket_policy" "allow_access_from_another_account" { + bucket = aws_s3_bucket.example.bucket + policy = data.aws_iam_policy_document.allow_access_from_another_account.json + } + + data "aws_iam_policy_document" "allow_access_from_another_account" { + statement { + + actions = [ + "s3:GetObject", + "s3:ListBucket", + ] + + resources = [ + "arn:aws:s3:::*", + ] + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Buckets, 1) + bucket := adapted.Buckets[0] + + assert.Equal(t, 2, bucket.Metadata.Range().GetStartLine()) + assert.Equal(t, 4, bucket.Metadata.Range().GetEndLine()) + + assert.Equal(t, 6, bucket.PublicAccessBlock.Metadata.Range().GetStartLine()) + assert.Equal(t, 13, bucket.PublicAccessBlock.Metadata.Range().GetEndLine()) + + assert.Equal(t, 9, bucket.PublicAccessBlock.RestrictPublicBuckets.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 9, bucket.PublicAccessBlock.RestrictPublicBuckets.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 10, bucket.PublicAccessBlock.BlockPublicACLs.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, bucket.PublicAccessBlock.BlockPublicACLs.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, bucket.PublicAccessBlock.BlockPublicPolicy.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, bucket.PublicAccessBlock.BlockPublicPolicy.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 12, bucket.PublicAccessBlock.IgnorePublicACLs.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 12, bucket.PublicAccessBlock.IgnorePublicACLs.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 17, bucket.ACL.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 17, bucket.ACL.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 20, bucket.Encryption.Metadata.Range().GetStartLine()) + assert.Equal(t, 29, bucket.Encryption.Metadata.Range().GetEndLine()) + + assert.Equal(t, 25, bucket.Encryption.KMSKeyId.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 25, bucket.Encryption.KMSKeyId.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 26, bucket.Encryption.Algorithm.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 26, bucket.Encryption.Algorithm.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 31, bucket.Logging.Metadata.Range().GetStartLine()) + assert.Equal(t, 36, bucket.Logging.Metadata.Range().GetEndLine()) + + assert.Equal(t, 34, bucket.Logging.TargetBucket.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 34, bucket.Logging.TargetBucket.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 38, bucket.Versioning.Metadata.Range().GetStartLine()) + assert.Equal(t, 43, bucket.Versioning.Metadata.Range().GetEndLine()) + + assert.Equal(t, 41, bucket.Versioning.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 41, bucket.Versioning.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 47, bucket.BucketPolicies[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 47, bucket.BucketPolicies[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 50, bucket.BucketPolicies[0].Document.Metadata.Range().GetStartLine()) + assert.Equal(t, 62, bucket.BucketPolicies[0].Document.Metadata.Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/s3/bucket.go b/pkg/iac/adapters/terraform/aws/s3/bucket.go new file mode 100644 index 000000000000..05a837a2b822 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/s3/bucket.go @@ -0,0 +1,340 @@ +package s3 + +import ( + "slices" + + s3types "github.com/aws/aws-sdk-go-v2/service/s3/types" + + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type adapter struct { + modules terraform.Modules + bucketMap map[string]*s3.Bucket +} + +func (a *adapter) adaptBuckets() []s3.Bucket { + for _, block := range a.modules.GetResourcesByType("aws_s3_bucket") { + bucket := &s3.Bucket{ + Metadata: block.GetMetadata(), + Name: block.GetAttribute("bucket").AsStringValueOrDefault("", block), + PublicAccessBlock: nil, + BucketPolicies: nil, + Encryption: getEncryption(block, a), + Versioning: getVersioning(block, a), + Logging: getLogging(block, a), + ACL: getBucketAcl(block, a), + Grants: getGrants(block, a), + AccelerateConfigurationStatus: getAccelerateStatus(block, a), + BucketLocation: block.GetAttribute("region").AsStringValueOrDefault("", block), + LifecycleConfiguration: getLifecycle(block, a), + Website: getWebsite(block, a), + Objects: getObject(block, a), + } + a.bucketMap[block.ID()] = bucket + } + + a.adaptBucketPolicies() + a.adaptPublicAccessBlocks() + + var buckets []s3.Bucket + for _, bucket := range a.bucketMap { + buckets = append(buckets, *bucket) + } + + return buckets +} + +func getEncryption(block *terraform.Block, a *adapter) s3.Encryption { + if sseConfgihuration := block.GetBlock("server_side_encryption_configuration"); sseConfgihuration != nil { + return newS3Encryption(block, sseConfgihuration) + } + if val, ok := applyForBucketRelatedResource(a, block, "aws_s3_bucket_server_side_encryption_configuration", func(resource *terraform.Block) s3.Encryption { + return newS3Encryption(resource, resource) + }); ok { + return val + } + return s3.Encryption{ + Metadata: block.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, block.GetMetadata()), + KMSKeyId: iacTypes.StringDefault("", block.GetMetadata()), + Algorithm: iacTypes.StringDefault("", block.GetMetadata()), + } +} + +func newS3Encryption(root, sseConfgihuration *terraform.Block) s3.Encryption { + return s3.Encryption{ + Metadata: root.GetMetadata(), + Enabled: isEncrypted(sseConfgihuration), + Algorithm: terraform.MapNestedAttribute( + sseConfgihuration, + "rule.apply_server_side_encryption_by_default.sse_algorithm", + func(attr *terraform.Attribute, parent *terraform.Block) iacTypes.StringValue { + return attr.AsStringValueOrDefault("", parent) + }, + ), + KMSKeyId: terraform.MapNestedAttribute( + sseConfgihuration, + "rule.apply_server_side_encryption_by_default.kms_master_key_id", + func(attr *terraform.Attribute, parent *terraform.Block) iacTypes.StringValue { + return attr.AsStringValueOrDefault("", parent) + }, + ), + } +} + +func getVersioning(block *terraform.Block, a *adapter) s3.Versioning { + versioning := s3.Versioning{ + Metadata: block.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, block.GetMetadata()), + MFADelete: iacTypes.BoolDefault(false, block.GetMetadata()), + } + if lockBlock := block.GetBlock("object_lock_configuration"); lockBlock != nil { + if enabled := isObjeckLockEnabled(lockBlock); enabled != nil { + versioning.Enabled = *enabled + } + } + if vBlock := block.GetBlock("versioning"); vBlock != nil { + versioning.Enabled = vBlock.GetAttribute("enabled").AsBoolValueOrDefault(true, vBlock) + versioning.MFADelete = vBlock.GetAttribute("mfa_delete").AsBoolValueOrDefault(false, vBlock) + } + + if enabled, ok := applyForBucketRelatedResource(a, block, "aws_s3_bucket_object_lock_configuration", func(resource *terraform.Block) *iacTypes.BoolValue { + // TODO: object_lock_enabled is a string + if block.GetAttribute("object_lock_enabled").IsTrue() { + return isObjeckLockEnabled(resource) + } + return nil + }); ok && enabled != nil { + versioning.Enabled = *enabled + } + + if val, ok := applyForBucketRelatedResource(a, block, "aws_s3_bucket_versioning", getVersioningFromResource); ok { + return val + } + return versioning +} + +func isObjeckLockEnabled(resource *terraform.Block) *iacTypes.BoolValue { + var val iacTypes.BoolValue + attr := resource.GetAttribute("object_lock_enabled") + switch { + case attr.IsNil(): // enabled by default + val = iacTypes.BoolDefault(true, resource.GetMetadata()) + case attr.Equals("Enabled"): + val = iacTypes.Bool(true, attr.GetMetadata()) + } + return &val +} + +// from aws_s3_bucket_versioning +func getVersioningFromResource(block *terraform.Block) s3.Versioning { + versioning := s3.Versioning{ + Metadata: block.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, block.GetMetadata()), + MFADelete: iacTypes.BoolDefault(false, block.GetMetadata()), + } + if config := block.GetBlock("versioning_configuration"); config != nil { + if status := config.GetAttribute("status"); status.IsNotNil() { + versioning.Enabled = iacTypes.Bool(status.Equals("Enabled", terraform.IgnoreCase), status.GetMetadata()) + } + if mfa := config.GetAttribute("mfa_delete"); mfa.IsNotNil() { + versioning.MFADelete = iacTypes.Bool(mfa.Equals("Enabled", terraform.IgnoreCase), mfa.GetMetadata()) + } + } + return versioning +} + +func getLogging(block *terraform.Block, a *adapter) s3.Logging { + if loggingBlock := block.GetBlock("logging"); loggingBlock.IsNotNil() { + targetBucket := loggingBlock.GetAttribute("target_bucket").AsStringValueOrDefault("", loggingBlock) + if referencedBlock, err := a.modules.GetReferencedBlock(loggingBlock.GetAttribute("target_bucket"), loggingBlock); err == nil { + targetBucket = iacTypes.String(referencedBlock.FullName(), loggingBlock.GetAttribute("target_bucket").GetMetadata()) + } + return s3.Logging{ + Metadata: loggingBlock.GetMetadata(), + Enabled: iacTypes.Bool(true, loggingBlock.GetMetadata()), + TargetBucket: targetBucket, + } + } + + if val, ok := applyForBucketRelatedResource(a, block, "aws_s3_bucket_logging", func(resource *terraform.Block) s3.Logging { + targetBucket := resource.GetAttribute("target_bucket").AsStringValueOrDefault("", resource) + if referencedBlock, err := a.modules.GetReferencedBlock(resource.GetAttribute("target_bucket"), resource); err == nil { + targetBucket = iacTypes.String(referencedBlock.FullName(), resource.GetAttribute("target_bucket").GetMetadata()) + } + return s3.Logging{ + Metadata: resource.GetMetadata(), + Enabled: hasLogging(resource), + TargetBucket: targetBucket, + } + }); ok { + return val + } + + return s3.Logging{ + Metadata: block.GetMetadata(), + Enabled: iacTypes.Bool(false, block.GetMetadata()), + TargetBucket: iacTypes.StringDefault("", block.GetMetadata()), + } +} + +func getBucketAcl(block *terraform.Block, a *adapter) iacTypes.StringValue { + aclAttr := block.GetAttribute("acl") + if aclAttr.IsString() { + return aclAttr.AsStringValueOrDefault("private", block) + } + + if val, ok := applyForBucketRelatedResource(a, block, "aws_s3_bucket_acl", func(resource *terraform.Block) iacTypes.StringValue { + return resource.GetAttribute("acl").AsStringValueOrDefault("private", resource) + }); ok { + return val + } + return iacTypes.StringDefault("private", block.GetMetadata()) +} + +func getGrants(block *terraform.Block, a *adapter) []s3.Grant { + if val, ok := applyForBucketRelatedResource(a, block, "aws_s3_bucket_acl", func(resource *terraform.Block) []s3.Grant { + var grants []s3.Grant + + if acessControlPolicy := resource.GetBlock("access_control_policy"); acessControlPolicy.IsNotNil() { + for _, grantBlock := range acessControlPolicy.GetBlocks("grant") { + grant := s3.Grant{ + Metadata: grantBlock.GetMetadata(), + Permissions: iacTypes.StringValueList{ + grantBlock.GetAttribute("permission").AsStringValueOrDefault("", grantBlock), + }, + } + + if granteeBlock := grantBlock.GetBlock("grantee"); granteeBlock.IsNotNil() { + grant.Grantee = s3.Grantee{ + Metadata: granteeBlock.GetMetadata(), + Type: granteeBlock.GetAttribute("type").AsStringValueOrDefault("", granteeBlock), + URI: granteeBlock.GetAttribute("uri").AsStringValueOrDefault("", granteeBlock), + } + } + + grants = append(grants, grant) + } + } + + return grants + + }); ok { + return val + } + + var grants []s3.Grant + for _, grantBlock := range block.GetBlocks("grant") { + grant := s3.Grant{ + Metadata: grantBlock.GetMetadata(), + Permissions: grantBlock.GetAttribute("permissions").AsStringValueSliceOrEmpty(), + Grantee: s3.Grantee{ + Metadata: grantBlock.GetMetadata(), + Type: grantBlock.GetAttribute("type").AsStringValueOrDefault("", grantBlock), + URI: grantBlock.GetAttribute("uri").AsStringValueOrDefault("", grantBlock), + }, + } + + grants = append(grants, grant) + } + + return grants +} + +func isEncrypted(sseConfgihuration *terraform.Block) iacTypes.BoolValue { + return terraform.MapNestedAttribute( + sseConfgihuration, + "rule.apply_server_side_encryption_by_default.sse_algorithm", + func(attr *terraform.Attribute, parent *terraform.Block) iacTypes.BoolValue { + if attr.IsNil() || !attr.IsString() { + return iacTypes.BoolDefault(false, parent.GetMetadata()) + } + algoVal := attr.Value().AsString() + isValidAlgo := slices.Contains(s3types.ServerSideEncryption("").Values(), s3types.ServerSideEncryption(algoVal)) + return iacTypes.Bool( + isValidAlgo, + attr.GetMetadata(), + ) + }, + ) +} + +func hasLogging(b *terraform.Block) iacTypes.BoolValue { + if loggingBlock := b.GetBlock("logging"); loggingBlock.IsNotNil() { + if targetAttr := loggingBlock.GetAttribute("target_bucket"); targetAttr.IsNotNil() && !targetAttr.IsEmpty() { + return iacTypes.Bool(true, targetAttr.GetMetadata()) + } + return iacTypes.BoolDefault(false, loggingBlock.GetMetadata()) + } + if targetBucket := b.GetAttribute("target_bucket"); targetBucket.IsNotNil() { + return iacTypes.Bool(true, targetBucket.GetMetadata()) + } + return iacTypes.BoolDefault(false, b.GetMetadata()) +} + +func getLifecycle(b *terraform.Block, a *adapter) []s3.Rules { + + var rules []s3.Rules + for _, r := range a.modules.GetReferencingResources(b, "aws_s3_bucket_lifecycle_configuration", "bucket") { + ruleblock := r.GetBlocks("rule") + for _, rule := range ruleblock { + rules = append(rules, s3.Rules{ + Metadata: rule.GetMetadata(), + Status: rule.GetAttribute("status").AsStringValueOrDefault("Enabled", rule), + }) + } + } + return rules +} + +func getWebsite(b *terraform.Block, a *adapter) (website *s3.Website) { + for _, r := range a.modules.GetReferencingResources(b, "aws_s3_bucket_website_configuration", "bucket") { + website = &s3.Website{ + Metadata: r.GetMetadata(), + } + } + return website +} + +func getObject(b *terraform.Block, a *adapter) []s3.Contents { + var object []s3.Contents + for _, r := range a.modules.GetReferencingResources(b, "aws_s3_object", "bucket") { + object = append(object, s3.Contents{ + Metadata: r.GetMetadata(), + }) + } + return object +} + +func getAccelerateStatus(b *terraform.Block, a *adapter) iacTypes.StringValue { + var status iacTypes.StringValue + for _, r := range a.modules.GetReferencingResources(b, "aws_s3_bucket_accelerate_configuration", "bucket") { + status = r.GetAttribute("status").AsStringValueOrDefault("Enabled", r) + } + return status +} + +func applyForBucketRelatedResource[T any](a *adapter, block *terraform.Block, resType string, fn func(resource *terraform.Block) T) (T, bool) { + for _, resource := range a.modules.GetResourcesByType(resType) { + bucketAttr := resource.GetAttribute("bucket") + if bucketAttr.IsNotNil() { + if bucketAttr.IsString() { + actualBucketName := block.GetAttribute("bucket").AsStringValueOrDefault("", block).Value() + if bucketAttr.Equals(block.ID()) || bucketAttr.Equals(actualBucketName) { + return fn(resource), true + } + } + if referencedBlock, err := a.modules.GetReferencedBlock(bucketAttr, resource); err == nil { + if referencedBlock.ID() == block.ID() { + return fn(resource), true + } + } + } + + } + var res T + return res, false +} diff --git a/pkg/iac/adapters/terraform/aws/s3/bucket_test.go b/pkg/iac/adapters/terraform/aws/s3/bucket_test.go new file mode 100644 index 000000000000..aeaca3d5a4c9 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/s3/bucket_test.go @@ -0,0 +1,330 @@ +package s3 + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" +) + +func Test_GetBuckets(t *testing.T) { + + source := ` +resource "aws_s3_bucket" "bucket1" { + + +} +` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + +} + +func Test_BucketGetACL(t *testing.T) { + + source := ` +resource "aws_s3_bucket" "example" { + bucket = "yournamehere" + acl = "authenticated-read" + + # ... other configuration ... +}` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.Equal(t, "authenticated-read", s3.Buckets[0].ACL.Value()) + +} + +func Test_V4BucketGetACL(t *testing.T) { + + source := ` +resource "aws_s3_bucket" "example" { + bucket = "yournamehere" +} + +resource "aws_s3_bucket_acl" "example" { + bucket = aws_s3_bucket.example.id + acl = "authenticated-read" +}` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.Equal(t, "authenticated-read", s3.Buckets[0].ACL.Value()) + +} + +func Test_BucketGetLogging(t *testing.T) { + + source := ` +resource "aws_s3_bucket" "example" { + bucket = "yournamehere" + + # ... other configuration ... + logging { + target_bucket = aws_s3_bucket.log_bucket.id + target_prefix = "log/" + } +} +` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.True(t, s3.Buckets[0].Logging.Enabled.Value()) + +} + +func Test_V4BucketGetLogging(t *testing.T) { + + source := ` +resource "aws_s3_bucket" "log_bucket" { + bucket = "example-log-bucket" + + # ... other configuration ... +} + +resource "aws_s3_bucket" "example" { + bucket = "yournamehere" + + # ... other configuration ... +} + +resource "aws_s3_bucket_logging" "example" { + bucket = aws_s3_bucket.example.id + target_bucket = aws_s3_bucket.log_bucket.id + target_prefix = "log/" +} +` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 2) + for _, bucket := range s3.Buckets { + switch bucket.Name.Value() { + case "yournamehere": + assert.True(t, bucket.Logging.Enabled.Value()) + case "example-log-bucket": + assert.False(t, bucket.Logging.Enabled.Value()) + } + } +} + +func Test_BucketGetVersioning(t *testing.T) { + source := ` +resource "aws_s3_bucket" "example" { + bucket = "yournamehere" + + # ... other configuration ... + versioning { + enabled = true + } +}` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.True(t, s3.Buckets[0].Versioning.Enabled.Value()) +} + +func Test_V4BucketGetVersioning(t *testing.T) { + source := ` +resource "aws_s3_bucket" "example" { + bucket = "yournamehere" + + # ... other configuration ... +} + +resource "aws_s3_bucket_versioning" "example" { + bucket = aws_s3_bucket.example.id + versioning_configuration { + status = "Enabled" + } +}` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.True(t, s3.Buckets[0].Versioning.Enabled.Value()) +} + +func Test_BucketGetVersioningWithLockDeprecated(t *testing.T) { + source := ` +resource "aws_s3_bucket" "example" { + bucket = "mybucket" + object_lock_configuration { + object_lock_enabled = "Enabled" + } +} +` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.True(t, s3.Buckets[0].Versioning.Enabled.Value()) + +} + +func Test_BucketGetVersioningWithLockForNewBucket(t *testing.T) { + source := ` +resource "aws_s3_bucket" "example" { + bucket = "mybucket" + object_lock_enabled = true +} + +resource "aws_s3_bucket_object_lock_configuration" "example" { + bucket = aws_s3_bucket.example.id +} +` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.True(t, s3.Buckets[0].Versioning.Enabled.Value()) + +} + +func Test_BucketGetVersioningWhenLockDisabledButVersioningEnabled(t *testing.T) { + source := ` +resource "aws_s3_bucket" "example" { + bucket = "mybucket" +} + +resource "aws_s3_bucket_object_lock_configuration" "example" { + bucket = aws_s3_bucket.example.id +} + +resource "aws_s3_bucket_versioning" "example" { + bucket = aws_s3_bucket.example.id + versioning_configuration { + status = "Enabled" + } +} +` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.True(t, s3.Buckets[0].Versioning.Enabled.Value()) + +} + +func Test_BucketGetEncryption(t *testing.T) { + + source := ` + resource "aws_s3_bucket" "example" { + bucket = "yournamehere" + + # ... other configuration ... + server_side_encryption_configuration { + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.mykey.arn + sse_algorithm = "aws:kms" + } + } + } +}` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.True(t, s3.Buckets[0].Encryption.Enabled.Value()) +} + +func Test_V4BucketGetEncryption(t *testing.T) { + + source := ` +resource "aws_s3_bucket" "example" { + bucket = "yournamehere" + + # ... other configuration ... +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "example" { + bucket = aws_s3_bucket.example.id + + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.mykey.arn + sse_algorithm = "aws:kms" + } + } +} +` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + assert.Len(t, s3.Buckets, 1) + assert.True(t, s3.Buckets[0].Encryption.Enabled.Value()) +} + +func Test_BucketWithPolicy(t *testing.T) { + + source := ` +resource "aws_s3_bucket" "bucket1" { + bucket = "lol" +} + +resource "aws_s3_bucket_policy" "allow_access_from_another_account" { + bucket = aws_s3_bucket.bucket1.id + policy = data.aws_iam_policy_document.allow_access_from_another_account.json +} + +data "aws_iam_policy_document" "allow_access_from_another_account" { + statement { + principals { + type = "AWS" + identifiers = ["123456789012"] + } + + actions = [ + "s3:GetObject", + "s3:ListBucket", + ] + + resources = [ + aws_s3_bucket.bucket1.arn, + ] + } +} + +` + modules := tftestutil.CreateModulesFromSource(t, source, ".tf") + + s3 := Adapt(modules) + + require.Len(t, s3.Buckets, 1) + require.Len(t, s3.Buckets[0].BucketPolicies, 1) + + policy := s3.Buckets[0].BucketPolicies[0] + + statements, _ := policy.Document.Parsed.Statements() + require.Len(t, statements, 1) + + principals, _ := statements[0].Principals() + actions, _ := statements[0].Actions() + + awsPrincipals, _ := principals.AWS() + require.Len(t, awsPrincipals, 1) + require.Len(t, actions, 2) + +} diff --git a/pkg/iac/adapters/terraform/aws/s3/policies.go b/pkg/iac/adapters/terraform/aws/s3/policies.go new file mode 100644 index 000000000000..3c8804957d0c --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/s3/policies.go @@ -0,0 +1,53 @@ +package s3 + +import ( + iamAdapter "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func (a *adapter) adaptBucketPolicies() { + + for _, b := range a.modules.GetResourcesByType("aws_s3_bucket_policy") { + + policyAttr := b.GetAttribute("policy") + if policyAttr.IsNil() { + continue + } + doc, err := iamAdapter.ParsePolicyFromAttr(policyAttr, b, a.modules) + if err != nil { + continue + } + + policy := iam.Policy{ + Metadata: policyAttr.GetMetadata(), + Name: iacTypes.StringDefault("", b.GetMetadata()), + Document: *doc, + Builtin: iacTypes.Bool(false, b.GetMetadata()), + } + + var bucketName string + bucketAttr := b.GetAttribute("bucket") + + if bucketAttr.IsNotNil() { + if referencedBlock, err := a.modules.GetReferencedBlock(bucketAttr, b); err == nil { + if bucket, ok := a.bucketMap[referencedBlock.ID()]; ok { + bucket.BucketPolicies = append(bucket.BucketPolicies, policy) + a.bucketMap[referencedBlock.ID()] = bucket + continue + } + } + } + + if bucketAttr.IsString() { + bucketName = bucketAttr.Value().AsString() + for id, bucket := range a.bucketMap { + if bucket.Name.EqualTo(bucketName) { + bucket.BucketPolicies = append(bucket.BucketPolicies, policy) + a.bucketMap[id] = bucket + break + } + } + } + } +} diff --git a/pkg/iac/adapters/terraform/aws/s3/public_access_block.go b/pkg/iac/adapters/terraform/aws/s3/public_access_block.go new file mode 100644 index 000000000000..eff4a71b4005 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/s3/public_access_block.go @@ -0,0 +1,41 @@ +package s3 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" +) + +func (a *adapter) adaptPublicAccessBlocks() { + + for _, b := range a.modules.GetResourcesByType("aws_s3_bucket_public_access_block") { + + pba := s3.PublicAccessBlock{ + Metadata: b.GetMetadata(), + BlockPublicACLs: b.GetAttribute("block_public_acls").AsBoolValueOrDefault(false, b), + BlockPublicPolicy: b.GetAttribute("block_public_policy").AsBoolValueOrDefault(false, b), + IgnorePublicACLs: b.GetAttribute("ignore_public_acls").AsBoolValueOrDefault(false, b), + RestrictPublicBuckets: b.GetAttribute("restrict_public_buckets").AsBoolValueOrDefault(false, b), + } + + var bucketName string + bucketAttr := b.GetAttribute("bucket") + if bucketAttr.IsNotNil() { + if referencedBlock, err := a.modules.GetReferencedBlock(bucketAttr, b); err == nil { + if bucket, ok := a.bucketMap[referencedBlock.ID()]; ok { + bucket.PublicAccessBlock = &pba + a.bucketMap[referencedBlock.ID()] = bucket + continue + } + } + } + if bucketAttr.IsString() { + bucketName = bucketAttr.Value().AsString() + for id, bucket := range a.bucketMap { + if bucketAttr.Equals(id) || bucket.Name.EqualTo(bucketName) { + bucket.PublicAccessBlock = &pba + a.bucketMap[id] = bucket + continue + } + } + } + } +} diff --git a/pkg/iac/adapters/terraform/aws/sns/adapt.go b/pkg/iac/adapters/terraform/aws/sns/adapt.go new file mode 100644 index 000000000000..761bb281a804 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/sns/adapt.go @@ -0,0 +1,38 @@ +package sns + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sns" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) sns.SNS { + return sns.SNS{ + Topics: adaptTopics(modules), + } +} + +func adaptTopics(modules terraform.Modules) []sns.Topic { + var topics []sns.Topic + for _, module := range modules { + for _, resource := range module.GetResourcesByType("aws_sns_topic") { + topics = append(topics, adaptTopic(resource)) + } + } + return topics +} + +func adaptTopic(resourceBlock *terraform.Block) sns.Topic { + return sns.Topic{ + Metadata: resourceBlock.GetMetadata(), + ARN: types.StringDefault("", resourceBlock.GetMetadata()), + Encryption: adaptEncryption(resourceBlock), + } +} + +func adaptEncryption(resourceBlock *terraform.Block) sns.Encryption { + return sns.Encryption{ + Metadata: resourceBlock.GetMetadata(), + KMSKeyID: resourceBlock.GetAttribute("kms_master_key_id").AsStringValueOrDefault("", resourceBlock), + } +} diff --git a/pkg/iac/adapters/terraform/aws/sns/adapt_test.go b/pkg/iac/adapters/terraform/aws/sns/adapt_test.go new file mode 100644 index 000000000000..b412f32f677a --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/sns/adapt_test.go @@ -0,0 +1,72 @@ +package sns + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sns" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptTopic(t *testing.T) { + tests := []struct { + name string + terraform string + expected sns.Topic + }{ + { + name: "defined", + terraform: ` + resource "aws_sns_topic" "good_example" { + kms_master_key_id = "/blah" + } +`, + expected: sns.Topic{ + Encryption: sns.Encryption{ + KMSKeyID: iacTypes.StringTest("/blah"), + }, + }, + }, + { + name: "default", + terraform: ` + resource "aws_sns_topic" "good_example" { + } +`, + expected: sns.Topic{ + Encryption: sns.Encryption{}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptTopic(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "aws_sns_topic" "good_example" { + kms_master_key_id = "/blah" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Topics, 1) + topic := adapted.Topics[0] + + assert.Equal(t, 2, topic.Metadata.Range().GetStartLine()) + assert.Equal(t, 4, topic.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, topic.Encryption.KMSKeyID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, topic.Encryption.KMSKeyID.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/aws/sqs/adapt.go b/pkg/iac/adapters/terraform/aws/sqs/adapt.go new file mode 100644 index 000000000000..1e40138b47bc --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/sqs/adapt.go @@ -0,0 +1,167 @@ +package sqs + +import ( + "github.com/google/uuid" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/aws/iam" + iamp "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sqs" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) sqs.SQS { + return sqs.SQS{ + Queues: (&adapter{ + modules: modules, + queues: make(map[string]sqs.Queue), + }).adaptQueues(), + } +} + +type adapter struct { + modules terraform.Modules + queues map[string]sqs.Queue +} + +func (a *adapter) adaptQueues() []sqs.Queue { + for _, resource := range a.modules.GetResourcesByType("aws_sqs_queue") { + a.adaptQueue(resource) + } + + for _, policyBlock := range a.modules.GetResourcesByType("aws_sqs_queue_policy") { + + policy := iamp.Policy{ + Metadata: policyBlock.GetMetadata(), + Name: iacTypes.StringDefault("", policyBlock.GetMetadata()), + Document: iamp.Document{ + Metadata: policyBlock.GetMetadata(), + }, + Builtin: iacTypes.Bool(false, policyBlock.GetMetadata()), + } + if attr := policyBlock.GetAttribute("policy"); attr.IsString() { + dataBlock, err := a.modules.GetBlockById(attr.Value().AsString()) + if err != nil { + parsed, err := iamgo.ParseString(attr.Value().AsString()) + if err != nil { + continue + } + policy.Document.Parsed = *parsed + policy.Document.Metadata = attr.GetMetadata() + } else if dataBlock.Type() == "data" && dataBlock.TypeLabel() == "aws_iam_policy_document" { // nolint: goconst + if doc, err := iam.ConvertTerraformDocument(a.modules, dataBlock); err == nil { + policy.Document.Parsed = doc.Document + policy.Document.Metadata = doc.Source.GetMetadata() + policy.Document.IsOffset = true + } + } + } else if refBlock, err := a.modules.GetReferencedBlock(attr, policyBlock); err == nil { + if refBlock.Type() == "data" && refBlock.TypeLabel() == "aws_iam_policy_document" { // nolint: goconst + if doc, err := iam.ConvertTerraformDocument(a.modules, refBlock); err == nil { + policy.Document.Parsed = doc.Document + policy.Document.Metadata = doc.Source.GetMetadata() + } + } + } + + if urlAttr := policyBlock.GetAttribute("queue_url"); urlAttr.IsNotNil() { + if refBlock, err := a.modules.GetReferencedBlock(urlAttr, policyBlock); err == nil { + if queue, ok := a.queues[refBlock.ID()]; ok { + queue.Policies = append(queue.Policies, policy) + a.queues[refBlock.ID()] = queue + continue + } + } + } + + a.queues[uuid.NewString()] = sqs.Queue{ + Metadata: iacTypes.NewUnmanagedMetadata(), + QueueURL: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + Encryption: sqs.Encryption{ + Metadata: iacTypes.NewUnmanagedMetadata(), + ManagedEncryption: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + KMSKeyID: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + Policies: []iamp.Policy{policy}, + } + } + + var queues []sqs.Queue + for _, queue := range a.queues { + queues = append(queues, queue) + } + return queues +} + +func (a *adapter) adaptQueue(resource *terraform.Block) { + + kmsKeyIdAttr := resource.GetAttribute("kms_master_key_id") + kmsKeyIdVal := kmsKeyIdAttr.AsStringValueOrDefault("", resource) + managedEncryption := resource.GetAttribute("sqs_managed_sse_enabled") + + var policies []iamp.Policy + if attr := resource.GetAttribute("policy"); attr.IsString() { + + dataBlock, err := a.modules.GetBlockById(attr.Value().AsString()) + if err != nil { + policy := iamp.Policy{ + Metadata: attr.GetMetadata(), + Name: iacTypes.StringDefault("", attr.GetMetadata()), + Document: iamp.Document{ + Metadata: attr.GetMetadata(), + }, + Builtin: iacTypes.Bool(false, attr.GetMetadata()), + } + parsed, err := iamgo.ParseString(attr.Value().AsString()) + if err == nil { + policy.Document.Parsed = *parsed + policy.Document.Metadata = attr.GetMetadata() + policy.Metadata = attr.GetMetadata() + policies = append(policies, policy) + } + } else if dataBlock.Type() == "data" && dataBlock.TypeLabel() == "aws_iam_policy_document" { + if doc, err := iam.ConvertTerraformDocument(a.modules, dataBlock); err == nil { + policy := iamp.Policy{ + Metadata: attr.GetMetadata(), + Name: iacTypes.StringDefault("", attr.GetMetadata()), + Document: iamp.Document{ + Metadata: doc.Source.GetMetadata(), + Parsed: doc.Document, + IsOffset: true, + HasRefs: false, + }, + Builtin: iacTypes.Bool(false, attr.GetMetadata()), + } + policies = append(policies, policy) + } + } + + } else if refBlock, err := a.modules.GetReferencedBlock(attr, resource); err == nil { + if refBlock.Type() == "data" && refBlock.TypeLabel() == "aws_iam_policy_document" { + if doc, err := iam.ConvertTerraformDocument(a.modules, refBlock); err == nil { + policy := iamp.Policy{ + Metadata: doc.Source.GetMetadata(), + Name: iacTypes.StringDefault("", doc.Source.GetMetadata()), + Document: iamp.Document{ + Metadata: doc.Source.GetMetadata(), + Parsed: doc.Document, + }, + Builtin: iacTypes.Bool(false, refBlock.GetMetadata()), + } + policies = append(policies, policy) + } + } + } + + a.queues[resource.ID()] = sqs.Queue{ + Metadata: resource.GetMetadata(), + QueueURL: iacTypes.StringDefault("", resource.GetMetadata()), + Encryption: sqs.Encryption{ + Metadata: resource.GetMetadata(), + ManagedEncryption: managedEncryption.AsBoolValueOrDefault(false, resource), + KMSKeyID: kmsKeyIdVal, + }, + Policies: policies, + } +} diff --git a/pkg/iac/adapters/terraform/aws/sqs/adapt_test.go b/pkg/iac/adapters/terraform/aws/sqs/adapt_test.go new file mode 100644 index 000000000000..4b2337e571b1 --- /dev/null +++ b/pkg/iac/adapters/terraform/aws/sqs/adapt_test.go @@ -0,0 +1,123 @@ +package sqs + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/iamgo" + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sqs" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected sqs.SQS + }{ + { + name: "np kms key", + terraform: ` + resource "aws_sqs_queue" "good_example" { + + policy = <= azurerm 2.97.0 + if block := resource.GetBlock("oms_agent"); block.IsNotNil() { + cluster.AddonProfile.OMSAgent = container.OMSAgent{ + Metadata: block.GetMetadata(), + Enabled: iacTypes.Bool(true, block.GetMetadata()), + } + } + + // azurerm >= 3.0.0 - new syntax for azure policy + if attr := resource.GetAttribute("azure_policy_enabled"); attr.IsNotNil() { + cluster.AddonProfile.AzurePolicy = container.AzurePolicy{ + Metadata: attr.GetMetadata(), + Enabled: attr.AsBoolValueOrDefault(false, resource), + } + } + + // azurerm < 2.99.0 + if rbacBlock := resource.GetBlock("role_based_access_control"); rbacBlock.IsNotNil() { + rbEnabledAttr := rbacBlock.GetAttribute("enabled") + cluster.RoleBasedAccessControl.Metadata = rbacBlock.GetMetadata() + cluster.RoleBasedAccessControl.Enabled = rbEnabledAttr.AsBoolValueOrDefault(false, rbacBlock) + } + + if rbacEnabledAttr := resource.GetAttribute("role_based_access_control_enabled"); rbacEnabledAttr.IsNotNil() { + // azurerm >= 2.99.0 + cluster.RoleBasedAccessControl.Metadata = rbacEnabledAttr.GetMetadata() + cluster.RoleBasedAccessControl.Enabled = rbacEnabledAttr.AsBoolValueOrDefault(false, resource) + } + + if block := resource.GetBlock("azure_active_directory_role_based_access_control"); block.IsNotNil() { + enabledAttr := block.GetAttribute("azure_rbac_enabled") + if enabledAttr.IsNotNil() { + if !cluster.RoleBasedAccessControl.Enabled.IsTrue() { + cluster.RoleBasedAccessControl.Metadata = block.GetMetadata() + cluster.RoleBasedAccessControl.Enabled = enabledAttr.AsBoolValueOrDefault(false, block) + } + } + } + + if diskEncryptionSetIDAttr := resource.GetAttribute("disk_encryption_set_id"); diskEncryptionSetIDAttr.IsNotNil() { + cluster.DiskEncryptionSetID = diskEncryptionSetIDAttr.AsStringValueOrDefault("", resource) + } + + cluster.AgentPools = adaptAgentPools(resource) + + return cluster +} + +func adaptAgentPools(resource *terraform.Block) []container.AgentPool { + var pools []container.AgentPool + + if defaultNodePoolBlock := resource.GetBlock("default_node_pool"); defaultNodePoolBlock.IsNotNil() { + pools = append(pools, adaptAgentPool(defaultNodePoolBlock)) + } + + return pools +} + +func adaptAgentPool(block *terraform.Block) container.AgentPool { + return container.AgentPool{ + Metadata: block.GetMetadata(), + DiskEncryptionSetID: block.GetAttribute("disk_encryption_set_id").AsStringValueOrDefault("", block), + NodeType: block.GetAttribute("type").AsStringValueOrDefault("VirtualMachineScaleSets", block), + } +} diff --git a/pkg/iac/adapters/terraform/azure/container/adapt_test.go b/pkg/iac/adapters/terraform/azure/container/adapt_test.go new file mode 100644 index 000000000000..b4f68e052f4c --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/container/adapt_test.go @@ -0,0 +1,277 @@ +package container + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/container" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptCluster(t *testing.T) { + tests := []struct { + name string + terraform string + expected container.KubernetesCluster + }{ + { + name: "defined", + terraform: ` + resource "azurerm_kubernetes_cluster" "example" { + private_cluster_enabled = true + + network_profile { + network_policy = "calico" + } + + api_server_access_profile { + + authorized_ip_ranges = [ + "1.2.3.4/32" + ] + + } + + addon_profile { + oms_agent { + enabled = true + } + } + + role_based_access_control { + enabled = true + } + } +`, + expected: container.KubernetesCluster{ + NetworkProfile: container.NetworkProfile{ + NetworkPolicy: iacTypes.StringTest("calico"), + }, + EnablePrivateCluster: iacTypes.BoolTest(true), + APIServerAuthorizedIPRanges: []iacTypes.StringValue{ + iacTypes.StringTest("1.2.3.4/32"), + }, + AddonProfile: container.AddonProfile{ + OMSAgent: container.OMSAgent{ + Enabled: iacTypes.BoolTest(true), + }, + }, + RoleBasedAccessControl: container.RoleBasedAccessControl{ + Enabled: iacTypes.BoolTest(true), + }, + }, + }, + { + name: "rbac with a new syntax", + terraform: ` + resource "azurerm_kubernetes_cluster" "example" { + role_based_access_control_enabled = true + } +`, + expected: container.KubernetesCluster{ + AddonProfile: container.AddonProfile{}, + RoleBasedAccessControl: container.RoleBasedAccessControl{ + Enabled: iacTypes.BoolTest(true), + }, + }, + }, + { + name: "defaults", + terraform: ` + resource "azurerm_kubernetes_cluster" "example" { + } +`, + expected: container.KubernetesCluster{}, + }, + { + name: "rbac off with k8s rbac on", + terraform: ` +resource "azurerm_kubernetes_cluster" "misreporting_example" { + role_based_access_control_enabled = true # Enable k8s RBAC + azure_active_directory_role_based_access_control { + managed = true # Enable AKS-managed Azure AAD integration + azure_rbac_enabled = false # Explicitly disable Azure RBAC for Kubernetes Authorization + } + } +`, + expected: container.KubernetesCluster{ + RoleBasedAccessControl: container.RoleBasedAccessControl{ + Enabled: iacTypes.BoolTest(true), + }, + }, + }, + { + name: "azure policy with new syntax", + terraform: ` + resource "azurerm_kubernetes_cluster" "example" { + azure_policy_enabled = true + } +`, + expected: container.KubernetesCluster{ + AddonProfile: container.AddonProfile{ + AzurePolicy: container.AzurePolicy{ + Enabled: iacTypes.BoolTest(true), + }, + }, + }, + }, + { + name: "azure policy with legacy syntax", + terraform: ` + resource "azurerm_kubernetes_cluster" "example" { + addon_profile { + azure_policy { + enabled = true + } + } + } +`, + expected: container.KubernetesCluster{ + AddonProfile: container.AddonProfile{ + AzurePolicy: container.AzurePolicy{ + Enabled: iacTypes.BoolTest(true), + }, + }, + }, + }, + { + name: "disk encryption set defined", + terraform: ` + resource "azurerm_kubernetes_cluster" "example" { + disk_encryption_set_id = "test-id" + } +`, + expected: container.KubernetesCluster{ + DiskEncryptionSetID: iacTypes.StringTest("test-id"), + }, + }, + { + name: "with default_node_pool", + terraform: ` + resource "azurerm_kubernetes_cluster" "example" { + default_node_pool { + name = "default" + node_count = 1 + vm_size = "Standard_DS2_v2" + type = "VirtualMachineScaleSets" + disk_encryption_set_id = "test-id" + } + } +`, + expected: container.KubernetesCluster{ + AgentPools: []container.AgentPool{ + { + DiskEncryptionSetID: iacTypes.StringTest("test-id"), + NodeType: iacTypes.StringTest("VirtualMachineScaleSets"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptCluster(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_kubernetes_cluster" "example" { + private_cluster_enabled = true + + network_profile { + network_policy = "calico" + } + + api_server_access_profile { + + authorized_ip_ranges = [ + "1.2.3.4/32" + ] + + } + + addon_profile { + oms_agent { + enabled = true + } + } + + role_based_access_control { + enabled = true + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.KubernetesClusters, 1) + cluster := adapted.KubernetesClusters[0] + + assert.Equal(t, 3, cluster.EnablePrivateCluster.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, cluster.EnablePrivateCluster.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 5, cluster.NetworkProfile.Metadata.Range().GetStartLine()) + assert.Equal(t, 7, cluster.NetworkProfile.Metadata.Range().GetEndLine()) + + assert.Equal(t, 6, cluster.NetworkProfile.NetworkPolicy.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, cluster.NetworkProfile.NetworkPolicy.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, cluster.APIServerAuthorizedIPRanges[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 13, cluster.APIServerAuthorizedIPRanges[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 17, cluster.AddonProfile.Metadata.Range().GetStartLine()) + assert.Equal(t, 21, cluster.AddonProfile.Metadata.Range().GetEndLine()) + + assert.Equal(t, 18, cluster.AddonProfile.OMSAgent.Metadata.Range().GetStartLine()) + assert.Equal(t, 20, cluster.AddonProfile.OMSAgent.Metadata.Range().GetEndLine()) + + assert.Equal(t, 19, cluster.AddonProfile.OMSAgent.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 19, cluster.AddonProfile.OMSAgent.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 23, cluster.RoleBasedAccessControl.Metadata.Range().GetStartLine()) + assert.Equal(t, 25, cluster.RoleBasedAccessControl.Metadata.Range().GetEndLine()) + + assert.Equal(t, 24, cluster.RoleBasedAccessControl.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 24, cluster.RoleBasedAccessControl.Enabled.GetMetadata().Range().GetEndLine()) +} + +func TestWithLocals(t *testing.T) { + src := ` + variable "ip_whitelist" { + description = "IP Ranges with allowed access." + type = list(string) + default = ["1.2.3.4"] +} + +locals { + ip_whitelist = concat(var.ip_whitelist, split(",", data.azurerm_public_ip.build_agents.ip_address)) +} + +resource "azurerm_kubernetes_cluster" "aks" { + # not working + api_server_access_profile { + authorized_ip_ranges = local.ip_whitelist + } + # working + api_server_access_profile { + authorized_ip_ranges = concat(var.ip_whitelist, split(",", data.azurerm_public_ip.example.ip_address)) + } +}` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.KubernetesClusters, 1) + cluster := adapted.KubernetesClusters[0] + require.Len(t, cluster.APIServerAuthorizedIPRanges, 1) + assert.False(t, cluster.APIServerAuthorizedIPRanges[0].GetMetadata().IsResolvable()) +} diff --git a/pkg/iac/adapters/terraform/azure/cosmosdb/adapt.go b/pkg/iac/adapters/terraform/azure/cosmosdb/adapt.go new file mode 100644 index 000000000000..c2997e5ec130 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/cosmosdb/adapt.go @@ -0,0 +1,46 @@ +package cosmosdb + +import ( + "github.com/zclconf/go-cty/cty" + + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/cosmosdb" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) cosmosdb.CosmosDB { + return cosmosdb.CosmosDB{ + Accounts: adaptCosmosDBAccounts(modules), + } +} + +func adaptCosmosDBAccounts(modules terraform.Modules) []cosmosdb.Account { + var cosmosDBAccounts []cosmosdb.Account + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_cosmosdb_account") { + cosmosDBAccounts = append(cosmosDBAccounts, adaptCosmosDBAccount(resource)) + } + } + return cosmosDBAccounts +} + +func adaptCosmosDBAccount(resource *terraform.Block) cosmosdb.Account { + // ip_range_filter is a list of strings in Terraform + ipRangeFilterAttr := resource.GetAttribute("ip_range_filter") + var ipRangeFilterVal []iacTypes.StringValue + if ipRangeFilterAttr.IsNil() { + ipRangeFilterVal = []iacTypes.StringValue{} + } else { + switch ipRangeFilterAttr.Type() { + case cty.String: + ipRangeFilterVal = []iacTypes.StringValue{ipRangeFilterAttr.AsStringValueOrDefault("", resource)} + default: + ipRangeFilterVal = ipRangeFilterAttr.AsStringValues() + } + } + + return cosmosdb.Account{ + Metadata: resource.GetMetadata(), + IPRangeFilter: ipRangeFilterVal, + } +} diff --git a/pkg/iac/adapters/terraform/azure/cosmosdb/adapt_test.go b/pkg/iac/adapters/terraform/azure/cosmosdb/adapt_test.go new file mode 100644 index 000000000000..5ee5ae72f53c --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/cosmosdb/adapt_test.go @@ -0,0 +1,227 @@ +package cosmosdb + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/cosmosdb" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptCosmosDBAccount(t *testing.T) { + tests := []struct { + name string + terraform string + expected cosmosdb.Account + }{ + { + name: "default values", + terraform: ` +resource "azurerm_cosmosdb_account" "example" { +} +`, + expected: cosmosdb.Account{ + IPRangeFilter: []iacTypes.StringValue{}, + }, + }, + { + name: "with ip_range_filter single value", + terraform: ` +resource "azurerm_cosmosdb_account" "example" { + ip_range_filter = ["10.0.0.0/16"] +} +`, + expected: cosmosdb.Account{ + IPRangeFilter: []iacTypes.StringValue{ + iacTypes.StringTest("10.0.0.0/16"), + }, + }, + }, + { + name: "with ip_range_filter multiple values", + terraform: ` +resource "azurerm_cosmosdb_account" "example" { + ip_range_filter = ["10.0.0.0/16", "192.168.1.0/24", "172.16.0.0/12"] +} +`, + expected: cosmosdb.Account{ + IPRangeFilter: []iacTypes.StringValue{ + iacTypes.StringTest("10.0.0.0/16"), + iacTypes.StringTest("192.168.1.0/24"), + iacTypes.StringTest("172.16.0.0/12"), + }, + }, + }, + { + name: "with ip_range_filter multiple values", + terraform: ` +resource "azurerm_cosmosdb_account" "example" { + ip_range_filter = ["10.0.0.0/8", "172.16.0.0/12"] +} +`, + expected: cosmosdb.Account{ + IPRangeFilter: []iacTypes.StringValue{ + iacTypes.StringTest("10.0.0.0/8"), + iacTypes.StringTest("172.16.0.0/12"), + }, + }, + }, + { + name: "empty ip_range_filter", + terraform: ` +resource "azurerm_cosmosdb_account" "example" { + ip_range_filter = [] +} +`, + expected: cosmosdb.Account{ + IPRangeFilter: nil, // AsStringValues() returns nil for empty lists + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptCosmosDBAccount(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptCosmosDBAccounts(t *testing.T) { + tests := []struct { + name string + terraform string + expected []cosmosdb.Account + }{ + { + name: "single account", + terraform: ` +resource "azurerm_cosmosdb_account" "example1" { +} +`, + expected: []cosmosdb.Account{ + { + IPRangeFilter: []iacTypes.StringValue{}, + }, + }, + }, + { + name: "multiple accounts", + terraform: ` +resource "azurerm_cosmosdb_account" "example1" { + ip_range_filter = ["10.0.0.0/16"] +} + +resource "azurerm_cosmosdb_account" "example2" { + ip_range_filter = ["192.168.0.0/16"] +} +`, + expected: []cosmosdb.Account{ + { + IPRangeFilter: []iacTypes.StringValue{ + iacTypes.StringTest("10.0.0.0/16"), + }, + }, + { + IPRangeFilter: []iacTypes.StringValue{ + iacTypes.StringTest("192.168.0.0/16"), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptCosmosDBAccounts(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected cosmosdb.CosmosDB + }{ + { + name: "basic", + terraform: ` +resource "azurerm_cosmosdb_account" "example" { + ip_range_filter = ["10.0.0.0/16"] +} +`, + expected: cosmosdb.CosmosDB{ + Accounts: []cosmosdb.Account{ + { + IPRangeFilter: []iacTypes.StringValue{ + iacTypes.StringTest("10.0.0.0/16"), + }, + }, + }, + }, + }, + { + name: "multiple accounts", + terraform: ` +resource "azurerm_cosmosdb_account" "example1" { +} + +resource "azurerm_cosmosdb_account" "example2" { + ip_range_filter = ["192.168.0.0/16"] +} +`, + expected: cosmosdb.CosmosDB{ + Accounts: []cosmosdb.Account{ + { + IPRangeFilter: []iacTypes.StringValue{}, + }, + { + IPRangeFilter: []iacTypes.StringValue{ + iacTypes.StringTest("192.168.0.0/16"), + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` +resource "azurerm_cosmosdb_account" "example" { + ip_range_filter = ["10.0.0.0/16", "192.168.1.0/24"] +} +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Accounts, 1) + + account := adapted.Accounts[0] + + assert.Equal(t, 2, account.Metadata.Range().GetStartLine()) + assert.Equal(t, 4, account.Metadata.Range().GetEndLine()) + + require.Len(t, account.IPRangeFilter, 2) + assert.Equal(t, 3, account.IPRangeFilter[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, account.IPRangeFilter[0].GetMetadata().Range().GetEndLine()) + assert.Equal(t, 3, account.IPRangeFilter[1].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, account.IPRangeFilter[1].GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/azure/database/adapt.go b/pkg/iac/adapters/terraform/azure/database/adapt.go new file mode 100644 index 000000000000..5d154be10d2c --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/database/adapt.go @@ -0,0 +1,15 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) database.Database { + return database.Database{ + MSSQLServers: adaptMSSQLServers(modules), + MariaDBServers: adaptMariaDBServers(modules), + MySQLServers: adaptMySQLServers(modules), + PostgreSQLServers: adaptPostgreSQLServers(modules), + } +} diff --git a/pkg/iac/adapters/terraform/azure/database/adapt_test.go b/pkg/iac/adapters/terraform/azure/database/adapt_test.go new file mode 100644 index 000000000000..d40f38284aab --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/database/adapt_test.go @@ -0,0 +1,713 @@ +package database + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected database.Database + }{ + { + name: "postgresql", + terraform: ` + resource "azurerm_postgresql_server" "example" { + name = "example" + + public_network_access_enabled = true + ssl_enforcement_enabled = true + ssl_minimal_tls_version_enforced = "TLS1_2" + } + + resource "azurerm_postgresql_configuration" "example" { + name = "log_connections" + resource_group_name = azurerm_resource_group.example.name + server_name = azurerm_postgresql_server.example.name + value = "on" + } + + resource "azurerm_postgresql_configuration" "example" { + name = "log_checkpoints" + resource_group_name = azurerm_resource_group.example.name + server_name = azurerm_postgresql_server.example.name + value = "on" + } + + resource "azurerm_postgresql_configuration" "example" { + name = "connection_throttling" + resource_group_name = azurerm_resource_group.example.name + server_name = azurerm_postgresql_server.example.name + value = "on" + } + + resource "azurerm_postgresql_firewall_rule" "example" { + name = "office" + resource_group_name = azurerm_resource_group.example.name + server_name = azurerm_postgresql_server.example.name + start_ip_address = "40.112.8.12" + end_ip_address = "40.112.8.12" + } +`, + expected: database.Database{ + PostgreSQLServers: []database.PostgreSQLServer{ + { + Server: database.Server{ + EnableSSLEnforcement: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest("TLS1_2"), + EnablePublicNetworkAccess: iacTypes.BoolTest(true), + FirewallRules: []database.FirewallRule{ + { + StartIP: iacTypes.StringTest("40.112.8.12"), + EndIP: iacTypes.StringTest("40.112.8.12"), + }, + }, + }, + Config: database.PostgresSQLConfig{ + LogConnections: iacTypes.BoolTest(true), + LogCheckpoints: iacTypes.BoolTest(true), + ConnectionThrottling: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + { + name: "postgresql with geo redundant backup and threat detection", + terraform: ` + resource "azurerm_postgresql_server" "example" { + name = "example" + + public_network_access_enabled = true + ssl_enforcement_enabled = true + ssl_minimal_tls_version_enforced = "TLS1_2" + geo_redundant_backup_enabled = true + + threat_detection_policy { + enabled = true + } + } + `, + expected: database.Database{ + PostgreSQLServers: []database.PostgreSQLServer{ + { + Server: database.Server{ + EnableSSLEnforcement: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest("TLS1_2"), + EnablePublicNetworkAccess: iacTypes.BoolTest(true), + }, + GeoRedundantBackupEnabled: iacTypes.BoolTest(true), + ThreatDetectionPolicy: database.ThreatDetectionPolicy{ + Enabled: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + { + name: "postgresql with log disconnections", + terraform: ` + resource "azurerm_postgresql_server" "example" { + name = "example" + + public_network_access_enabled = true + ssl_enforcement_enabled = true + ssl_minimal_tls_version_enforced = "TLS1_2" + } + + resource "azurerm_postgresql_configuration" "example" { + name = "log_disconnections" + resource_group_name = azurerm_resource_group.example.name + server_name = azurerm_postgresql_server.example.name + value = "on" + } + `, + expected: database.Database{ + PostgreSQLServers: []database.PostgreSQLServer{ + { + Server: database.Server{ + EnableSSLEnforcement: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest("TLS1_2"), + EnablePublicNetworkAccess: iacTypes.BoolTest(true), + }, + Config: database.PostgresSQLConfig{ + LogDisconnections: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + { + name: "mariadb", + terraform: ` + resource "azurerm_mariadb_server" "example" { + name = "example-mariadb-server" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + + public_network_access_enabled = false + ssl_enforcement_enabled = true + } + + resource "azurerm_mariadb_firewall_rule" "example" { + name = "test-rule" + server_name = azurerm_mariadb_server.example.name + start_ip_address = "40.112.0.0" + end_ip_address = "40.112.255.255" + } +`, + expected: database.Database{ + MariaDBServers: []database.MariaDBServer{ + { + Server: database.Server{ + EnableSSLEnforcement: iacTypes.BoolTest(true), + FirewallRules: []database.FirewallRule{ + { + StartIP: iacTypes.StringTest("40.112.0.0"), + EndIP: iacTypes.StringTest("40.112.255.255"), + }, + }, + }, + }, + }, + }, + }, + { + name: "mysql", + terraform: ` + resource "azurerm_mysql_server" "example" { + public_network_access_enabled = true + ssl_enforcement_enabled = true + ssl_minimal_tls_version_enforced = "TLS1_2" + } + + resource "azurerm_mysql_firewall_rule" "example" { + server_name = azurerm_mysql_server.example.name + start_ip_address = "40.112.8.12" + end_ip_address = "40.112.8.12" + } + `, + expected: database.Database{ + MySQLServers: []database.MySQLServer{ + { + Server: database.Server{ + EnableSSLEnforcement: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest("TLS1_2"), + EnablePublicNetworkAccess: iacTypes.BoolTest(true), + FirewallRules: []database.FirewallRule{ + { + StartIP: iacTypes.StringTest("40.112.8.12"), + EndIP: iacTypes.StringTest("40.112.8.12"), + }, + }, + }, + }, + }, + }, + }, + { + name: "postgresql flexible server", + terraform: ` + resource "azurerm_postgresql_flexible_server" "example" { + name = "example-flexible" + + public_network_access_enabled = true + } + + resource "azurerm_postgresql_flexible_server_configuration" "require_secure_transport" { + name = "require_secure_transport" + server_id = azurerm_postgresql_flexible_server.example.id + value = "ON" + } + + resource "azurerm_postgresql_flexible_server_configuration" "tls_version" { + name = "tls_version" + server_id = azurerm_postgresql_flexible_server.example.id + value = "TLS1_2" + } + + resource "azurerm_postgresql_flexible_server_configuration" "log_connections" { + name = "log_connections" + server_id = azurerm_postgresql_flexible_server.example.id + value = "on" + } + + resource "azurerm_postgresql_flexible_server_configuration" "log_checkpoints" { + name = "log_checkpoints" + server_id = azurerm_postgresql_flexible_server.example.id + value = "on" + } + + resource "azurerm_postgresql_flexible_server_firewall_rule" "example" { + name = "office" + server_id = azurerm_postgresql_flexible_server.example.id + start_ip_address = "40.112.8.12" + end_ip_address = "40.112.8.12" + } +`, + expected: database.Database{ + PostgreSQLServers: []database.PostgreSQLServer{ + { + Server: database.Server{ + EnableSSLEnforcement: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest("TLS1_2"), + EnablePublicNetworkAccess: iacTypes.BoolTest(true), + FirewallRules: []database.FirewallRule{ + { + StartIP: iacTypes.StringTest("40.112.8.12"), + EndIP: iacTypes.StringTest("40.112.8.12"), + }, + }, + }, + Config: database.PostgresSQLConfig{ + LogConnections: iacTypes.BoolTest(true), + LogCheckpoints: iacTypes.BoolTest(true), + }, + // Threat Detection is not configurable via Terraform for PostgreSQL Flexible Server + // It can only be configured via Azure CLI, so it's marked as unmanaged + ThreatDetectionPolicy: database.ThreatDetectionPolicy{}, + }, + }, + }, + }, + { + name: "postgresql flexible server with configuration resources", + terraform: ` + resource "azurerm_postgresql_flexible_server" "example" { + name = "example-flexible" + + public_network_access_enabled = true + } + + resource "azurerm_postgresql_flexible_server_firewall_rule" "example" { + name = "office" + server_id = azurerm_postgresql_flexible_server.example.id + start_ip_address = "40.112.8.12" + end_ip_address = "40.112.8.12" + } + + resource "azurerm_postgresql_flexible_server_configuration" "require_secure_transport" { + name = "require_secure_transport" + server_id = azurerm_postgresql_flexible_server.example.id + value = "ON" + } + + resource "azurerm_postgresql_flexible_server_configuration" "tls_version" { + name = "tls_version" + server_id = azurerm_postgresql_flexible_server.example.id + value = "TLS1_2" + } + `, + expected: database.Database{ + PostgreSQLServers: []database.PostgreSQLServer{ + { + Server: database.Server{ + EnableSSLEnforcement: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest("TLS1_2"), + EnablePublicNetworkAccess: iacTypes.BoolTest(true), + FirewallRules: []database.FirewallRule{ + { + StartIP: iacTypes.StringTest("40.112.8.12"), + EndIP: iacTypes.StringTest("40.112.8.12"), + }, + }, + }, + Config: database.PostgresSQLConfig{}, + // Threat Detection is not configurable via Terraform for PostgreSQL Flexible Server + // It can only be configured via Azure CLI, so it's marked as unmanaged + ThreatDetectionPolicy: database.ThreatDetectionPolicy{}, + }, + }, + }, + }, + { + name: "mysql flexible server", + terraform: ` + resource "azurerm_mysql_flexible_server" "example" { + name = "example-flexible" + + public_network_access_enabled = true + } + + resource "azurerm_mysql_flexible_server_firewall_rule" "example" { + name = "office" + server_id = azurerm_mysql_flexible_server.example.id + start_ip_address = "40.112.8.12" + end_ip_address = "40.112.8.12" + } + + resource "azurerm_mysql_flexible_server_configuration" "require_secure_transport" { + name = "require_secure_transport" + server_id = azurerm_mysql_flexible_server.example.id + value = "ON" + } + + resource "azurerm_mysql_flexible_server_configuration" "tls_version" { + name = "tls_version" + server_id = azurerm_mysql_flexible_server.example.id + value = "TLS1_2" + } + + resource "azurerm_mysql_flexible_server_configuration" "interactive_timeout" { + name = "interactive_timeout" + server_id = azurerm_mysql_flexible_server.example.id + value = "600" + } + `, + expected: database.Database{ + MySQLServers: []database.MySQLServer{ + { + Server: database.Server{ + EnableSSLEnforcement: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest("TLS1_2"), + EnablePublicNetworkAccess: iacTypes.BoolTest(true), + FirewallRules: []database.FirewallRule{ + { + StartIP: iacTypes.StringTest("40.112.8.12"), + EndIP: iacTypes.StringTest("40.112.8.12"), + }, + }, + }, + }, + }, + }, + }, + { + name: "mysql flexible server with configuration resources", + terraform: ` + resource "azurerm_mysql_flexible_server" "example" { + name = "example-flexible" + + public_network_access_enabled = true + } + + resource "azurerm_mysql_flexible_server_firewall_rule" "example" { + name = "office" + server_id = azurerm_mysql_flexible_server.example.id + start_ip_address = "40.112.8.12" + end_ip_address = "40.112.8.12" + } + + resource "azurerm_mysql_flexible_server_configuration" "require_secure_transport" { + name = "require_secure_transport" + server_id = azurerm_mysql_flexible_server.example.id + value = "ON" + } + + resource "azurerm_mysql_flexible_server_configuration" "tls_version" { + name = "tls_version" + server_id = azurerm_mysql_flexible_server.example.id + value = "TLS1_2" + } + `, + expected: database.Database{ + MySQLServers: []database.MySQLServer{ + { + Server: database.Server{ + EnableSSLEnforcement: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest("TLS1_2"), + EnablePublicNetworkAccess: iacTypes.BoolTest(true), + FirewallRules: []database.FirewallRule{ + { + StartIP: iacTypes.StringTest("40.112.8.12"), + EndIP: iacTypes.StringTest("40.112.8.12"), + }, + }, + }, + }, + }, + }, + }, + { + name: "ms sql", + terraform: ` + resource "azurerm_mssql_server" "example" { + name = "mssqlserver" + minimum_tls_version = "1.2" + public_network_access_enabled = false + } + + resource "azurerm_mssql_firewall_rule" "example" { + name = "FirewallRule1" + server_id = azurerm_mssql_server.example.id + start_ip_address = "10.0.17.62" + end_ip_address = "10.0.17.62" + } + + resource "azurerm_mssql_server_security_alert_policy" "example" { + resource_group_name = azurerm_resource_group.example.name + server_name = azurerm_mssql_server.example.name + disabled_alerts = [ + "Sql_Injection", + "Data_Exfiltration" + ] + email_account_admins = true + email_addresses = [ + "example@example.com" + ] + } + + resource "azurerm_mssql_server_extended_auditing_policy" "example" { + server_id = azurerm_mssql_server.example.id + retention_in_days = 6 + } + `, + expected: database.Database{ + MSSQLServers: []database.MSSQLServer{ + { + Server: database.Server{ + MinimumTLSVersion: iacTypes.StringTest("1.2"), + FirewallRules: []database.FirewallRule{ + { + StartIP: iacTypes.StringTest("10.0.17.62"), + EndIP: iacTypes.StringTest("10.0.17.62"), + }, + }, + }, + ExtendedAuditingPolicies: []database.ExtendedAuditingPolicy{ + { + RetentionInDays: iacTypes.IntTest(6), + }, + }, + SecurityAlertPolicies: []database.SecurityAlertPolicy{ + { + EmailAddresses: []iacTypes.StringValue{ + iacTypes.StringTest("example@example.com"), + }, + DisabledAlerts: []iacTypes.StringValue{ + iacTypes.StringTest("Sql_Injection"), + iacTypes.StringTest("Data_Exfiltration"), + }, + EmailAccountAdmins: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_postgresql_server" "example" { + public_network_access_enabled = true + ssl_enforcement_enabled = true + ssl_minimal_tls_version_enforced = "TLS1_2" + } + + resource "azurerm_postgresql_configuration" "example" { + name = "log_connections" + server_name = azurerm_postgresql_server.example.name + value = "on" + } + + resource "azurerm_postgresql_configuration" "example" { + name = "log_checkpoints" + server_name = azurerm_postgresql_server.example.name + value = "on" + } + + resource "azurerm_postgresql_configuration" "example" { + name = "connection_throttling" + server_name = azurerm_postgresql_server.example.name + value = "on" + } + + resource "azurerm_postgresql_firewall_rule" "example" { + name = "office" + server_name = azurerm_postgresql_server.example.name + start_ip_address = "40.112.8.12" + end_ip_address = "40.112.8.12" + } + + resource "azurerm_mariadb_server" "example" { + public_network_access_enabled = false + ssl_enforcement_enabled = true + } + + resource "azurerm_mariadb_firewall_rule" "example" { + name = "test-rule" + server_name = azurerm_mariadb_server.example.name + start_ip_address = "40.112.0.0" + end_ip_address = "40.112.255.255" + } + + resource "azurerm_mysql_server" "example" { + public_network_access_enabled = true + ssl_enforcement_enabled = true + ssl_minimal_tls_version_enforced = "TLS1_2" + } + + resource "azurerm_mysql_firewall_rule" "example" { + server_name = azurerm_mysql_server.example.name + start_ip_address = "40.112.8.12" + end_ip_address = "40.112.8.12" + } + + resource "azurerm_mssql_server" "example" { + name = "mssqlserver" + public_network_access_enabled = false + minimum_tls_version = "1.2" + } + + resource "azurerm_mssql_firewall_rule" "example" { + name = "FirewallRule1" + server_id = azurerm_mssql_server.example.id + start_ip_address = "10.0.17.62" + end_ip_address = "10.0.17.62" + } + + resource "azurerm_mssql_server_security_alert_policy" "example" { + server_name = azurerm_mssql_server.example.name + disabled_alerts = [ + "Sql_Injection", + "Data_Exfiltration" + ] + email_account_admins = true + email_addresses = [ + "example@example.com" + ] + } + + resource "azurerm_mssql_server_extended_auditing_policy" "example" { + server_id = azurerm_mssql_server.example.id + retention_in_days = 6 + } + ` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.PostgreSQLServers, 1) + require.Len(t, adapted.MariaDBServers, 1) + require.Len(t, adapted.MySQLServers, 1) + require.Len(t, adapted.MSSQLServers, 1) + + postgres := adapted.PostgreSQLServers[0] + mariadb := adapted.MariaDBServers[0] + mysql := adapted.MySQLServers[0] + mssql := adapted.MSSQLServers[0] + + assert.Equal(t, 2, postgres.Metadata.Range().GetStartLine()) + assert.Equal(t, 6, postgres.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, postgres.EnablePublicNetworkAccess.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, postgres.EnablePublicNetworkAccess.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, postgres.EnableSSLEnforcement.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, postgres.EnableSSLEnforcement.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 5, postgres.MinimumTLSVersion.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, postgres.MinimumTLSVersion.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, postgres.Config.LogConnections.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, postgres.Config.LogConnections.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 17, postgres.Config.LogCheckpoints.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 17, postgres.Config.LogCheckpoints.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 23, postgres.Config.ConnectionThrottling.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 23, postgres.Config.ConnectionThrottling.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 26, postgres.FirewallRules[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 31, postgres.FirewallRules[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 29, postgres.FirewallRules[0].StartIP.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 29, postgres.FirewallRules[0].StartIP.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 30, postgres.FirewallRules[0].EndIP.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 30, postgres.FirewallRules[0].EndIP.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 33, mariadb.Metadata.Range().GetStartLine()) + assert.Equal(t, 36, mariadb.Metadata.Range().GetEndLine()) + + assert.Equal(t, 34, mariadb.EnablePublicNetworkAccess.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 34, mariadb.EnablePublicNetworkAccess.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 35, mariadb.EnableSSLEnforcement.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 35, mariadb.EnableSSLEnforcement.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 38, mariadb.FirewallRules[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 43, mariadb.FirewallRules[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 41, mariadb.FirewallRules[0].StartIP.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 41, mariadb.FirewallRules[0].StartIP.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 42, mariadb.FirewallRules[0].EndIP.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 42, mariadb.FirewallRules[0].EndIP.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 45, mysql.Metadata.Range().GetStartLine()) + assert.Equal(t, 49, mysql.Metadata.Range().GetEndLine()) + + assert.Equal(t, 46, mysql.EnablePublicNetworkAccess.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 46, mysql.EnablePublicNetworkAccess.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 47, mysql.EnableSSLEnforcement.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 47, mysql.EnableSSLEnforcement.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 48, mysql.MinimumTLSVersion.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 48, mysql.MinimumTLSVersion.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 51, mysql.FirewallRules[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 55, mysql.FirewallRules[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 53, mysql.FirewallRules[0].StartIP.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 53, mysql.FirewallRules[0].StartIP.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 54, mysql.FirewallRules[0].EndIP.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 54, mysql.FirewallRules[0].EndIP.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 57, mssql.Metadata.Range().GetStartLine()) + assert.Equal(t, 61, mssql.Metadata.Range().GetEndLine()) + + assert.Equal(t, 59, mssql.EnablePublicNetworkAccess.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 59, mssql.EnablePublicNetworkAccess.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 60, mssql.MinimumTLSVersion.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 60, mssql.MinimumTLSVersion.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 63, mssql.FirewallRules[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 68, mssql.FirewallRules[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 66, mssql.FirewallRules[0].StartIP.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 66, mssql.FirewallRules[0].StartIP.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 67, mssql.FirewallRules[0].EndIP.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 67, mssql.FirewallRules[0].EndIP.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 70, mssql.SecurityAlertPolicies[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 80, mssql.SecurityAlertPolicies[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 72, mssql.SecurityAlertPolicies[0].DisabledAlerts[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 75, mssql.SecurityAlertPolicies[0].DisabledAlerts[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 76, mssql.SecurityAlertPolicies[0].EmailAccountAdmins.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 76, mssql.SecurityAlertPolicies[0].EmailAccountAdmins.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 77, mssql.SecurityAlertPolicies[0].EmailAddresses[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 79, mssql.SecurityAlertPolicies[0].EmailAddresses[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 82, mssql.ExtendedAuditingPolicies[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 85, mssql.ExtendedAuditingPolicies[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 84, mssql.ExtendedAuditingPolicies[0].RetentionInDays.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 84, mssql.ExtendedAuditingPolicies[0].RetentionInDays.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/azure/database/common.go b/pkg/iac/adapters/terraform/azure/database/common.go new file mode 100644 index 000000000000..d63482e12f22 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/database/common.go @@ -0,0 +1,47 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +// serverParameters represents server configuration parameters that are common +// to both MySQL and PostgreSQL flexible servers in Azure. +type serverParameters struct { + requireSecureTransport iacTypes.BoolValue + tlsVersion iacTypes.StringValue +} + +// parseServerParameters parses a list of server configurations to extract +// server parameters for MySQL and PostgreSQL flexible servers. +func parseServerParameters(configs []*terraform.Block, resourceMetadata iacTypes.Metadata) serverParameters { + // https://learn.microsoft.com/en-us/azure/mysql/flexible-server/overview#enterprise-grade-security-compliance-and-privacy + params := serverParameters{ + requireSecureTransport: iacTypes.BoolDefault(true, resourceMetadata), + tlsVersion: iacTypes.StringDefault("TLS1.2", resourceMetadata), + } + + for _, config := range configs { + nameAttr := config.GetAttribute("name") + valAttr := config.GetAttribute("value") + switch { + case nameAttr.Equals("require_secure_transport"): + params.requireSecureTransport, _ = iacTypes.BoolFromCtyValue(valAttr.Value(), valAttr.GetMetadata()) + case nameAttr.Equals("tls_version"): + params.tlsVersion = valAttr.AsStringValueOrDefault("TLS1_2", config) + } + } + + return params +} + +func adaptFirewallRule(resource *terraform.Block) database.FirewallRule { + return database.FirewallRule{ + Metadata: resource.GetMetadata(), + StartIP: resource.GetAttribute("start_ip_address"). + AsStringValueOrDefault("", resource), + EndIP: resource.GetAttribute("end_ip_address"). + AsStringValueOrDefault("", resource), + } +} diff --git a/pkg/iac/adapters/terraform/azure/database/mariadb.go b/pkg/iac/adapters/terraform/azure/database/mariadb.go new file mode 100644 index 000000000000..bdf813e18fa4 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/database/mariadb.go @@ -0,0 +1,39 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptMariaDBServers(modules terraform.Modules) []database.MariaDBServer { + var mariaDBServers []database.MariaDBServer + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_mariadb_server") { + mariaDBServers = append(mariaDBServers, adaptMariaDBServer(resource, module)) + } + } + + return mariaDBServers +} + +func adaptMariaDBServer(resource *terraform.Block, module *terraform.Module) database.MariaDBServer { + var firewallRules []database.FirewallRule + firewallRuleBlocks := module.GetReferencingResources(resource, "azurerm_mariadb_firewall_rule", "server_name") + for _, firewallBlock := range firewallRuleBlocks { + firewallRules = append(firewallRules, adaptFirewallRule(firewallBlock)) + } + + return database.MariaDBServer{ + Metadata: resource.GetMetadata(), + Server: database.Server{ + Metadata: resource.GetMetadata(), + EnableSSLEnforcement: resource.GetAttribute("ssl_enforcement_enabled"). + AsBoolValueOrDefault(false, resource), + MinimumTLSVersion: iacTypes.StringDefault("", resource.GetMetadata()), + EnablePublicNetworkAccess: resource.GetAttribute("public_network_access_enabled"). + AsBoolValueOrDefault(true, resource), + FirewallRules: firewallRules, + }, + } +} diff --git a/pkg/iac/adapters/terraform/azure/database/mssql.go b/pkg/iac/adapters/terraform/azure/database/mssql.go new file mode 100644 index 000000000000..eee4fba58afd --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/database/mssql.go @@ -0,0 +1,118 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptMSSQLServers(modules terraform.Modules) []database.MSSQLServer { + var mssqlServers []database.MSSQLServer + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_sql_server", "azurerm_mssql_server") { + mssqlServers = append(mssqlServers, adaptMSSQLServer(resource, module)) + } + } + return mssqlServers +} + +func adaptMSSQLServer(resource *terraform.Block, module *terraform.Module) database.MSSQLServer { + minTLSVersionVal := iacTypes.StringDefault("", resource.GetMetadata()) + publicAccessVal := iacTypes.BoolDefault(true, resource.GetMetadata()) + enableSSLEnforcementVal := iacTypes.BoolDefault(false, resource.GetMetadata()) + + if resource.TypeLabel() == "azurerm_mssql_server" { + minTLSVersionAttr := resource.GetAttribute("minimum_tls_version") + minTLSVersionVal = minTLSVersionAttr.AsStringValueOrDefault("1.2", resource) + publicAccessAttr := resource.GetAttribute("public_network_access_enabled") + publicAccessVal = publicAccessAttr.AsBoolValueOrDefault(true, resource) + } + + var alertPolicies []database.SecurityAlertPolicy + alertPolicyBlocks := module.GetReferencingResources(resource, "azurerm_mssql_server_security_alert_policy", "server_name") + for _, alertBlock := range alertPolicyBlocks { + alertPolicies = append(alertPolicies, adaptMSSQLSecurityAlertPolicy(alertBlock)) + } + + auditingPoliciesBlocks := module.GetReferencingResources(resource, "azurerm_mssql_server_extended_auditing_policy", "server_id") + auditingPoliciesBlocks = append(auditingPoliciesBlocks, resource.GetBlocks("extended_auditing_policy")...) + + databasesRes := module.GetReferencingResources(resource, "azurerm_mssql_database", "server_id") + for _, databaseRes := range databasesRes { + dbAuditingBlocks := module.GetReferencingResources(databaseRes, "azurerm_mssql_database_extended_auditing_policy", "database_id") + auditingPoliciesBlocks = append(auditingPoliciesBlocks, dbAuditingBlocks...) + } + + var auditingPolicies []database.ExtendedAuditingPolicy + for _, auditBlock := range auditingPoliciesBlocks { + auditingPolicies = append(auditingPolicies, adaptMSSQLExtendedAuditingPolicy(auditBlock)) + } + + var firewallRules []database.FirewallRule + firewallRuleBlocks := module.GetReferencingResources(resource, "azurerm_sql_firewall_rule", "server_name") + firewallRuleBlocks = append(firewallRuleBlocks, module.GetReferencingResources(resource, "azurerm_mssql_firewall_rule", "server_id")...) + for _, firewallBlock := range firewallRuleBlocks { + firewallRules = append(firewallRules, adaptFirewallRule(firewallBlock)) + } + + // Support for deprecated azuread_administrator block (backward compatibility) + var adAdmins []database.ActiveDirectoryAdministrator + azureadAdminBlock := resource.GetBlock("azuread_administrator") + if azureadAdminBlock.IsNotNil() { + adAdmins = append(adAdmins, adaptAzureADAdministratorBlock(azureadAdminBlock)) + } + + // Support for azurerm_sql_active_directory_administrator resource (preferred method) + adAdminBlocks := module.GetReferencingResources(resource, "azurerm_sql_active_directory_administrator", "server_name") + for _, adAdminBlock := range adAdminBlocks { + adAdmins = append(adAdmins, adaptActiveDirectoryAdministrator(adAdminBlock)) + } + + return database.MSSQLServer{ + Metadata: resource.GetMetadata(), + Server: database.Server{ + Metadata: resource.GetMetadata(), + EnableSSLEnforcement: enableSSLEnforcementVal, + MinimumTLSVersion: minTLSVersionVal, + EnablePublicNetworkAccess: publicAccessVal, + FirewallRules: firewallRules, + }, + ExtendedAuditingPolicies: auditingPolicies, + SecurityAlertPolicies: alertPolicies, + AdministratorLogin: resource.GetAttribute("administrator_login").AsStringValueOrDefault("", resource), + ActiveDirectoryAdministrators: adAdmins, + } +} + +func adaptMSSQLSecurityAlertPolicy(resource *terraform.Block) database.SecurityAlertPolicy { + return database.SecurityAlertPolicy{ + Metadata: resource.GetMetadata(), + EmailAddresses: resource.GetAttribute("email_addresses").AsStringValues(), + DisabledAlerts: resource.GetAttribute("disabled_alerts").AsStringValues(), + EmailAccountAdmins: resource.GetAttribute("email_account_admins"). + AsBoolValueOrDefault(false, resource), + } +} + +func adaptMSSQLExtendedAuditingPolicy(resource *terraform.Block) database.ExtendedAuditingPolicy { + return database.ExtendedAuditingPolicy{ + Metadata: resource.GetMetadata(), + RetentionInDays: resource.GetAttribute("retention_in_days").AsIntValueOrDefault(0, resource), + } +} + +func adaptActiveDirectoryAdministrator(resource *terraform.Block) database.ActiveDirectoryAdministrator { + return database.ActiveDirectoryAdministrator{ + Metadata: resource.GetMetadata(), + Login: resource.GetAttribute("login").AsStringValueOrDefault("", resource), + } +} + +func adaptAzureADAdministratorBlock(block *terraform.Block) database.ActiveDirectoryAdministrator { + return database.ActiveDirectoryAdministrator{ + Metadata: block.GetMetadata(), + // The azuread_administrator block uses login_username attribute + Login: block.GetFirstAttributeOf("login_username", "login"). + AsStringValueOrDefault("", block), + } +} diff --git a/pkg/iac/adapters/terraform/azure/database/mysql.go b/pkg/iac/adapters/terraform/azure/database/mysql.go new file mode 100644 index 000000000000..21e79750b14c --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/database/mysql.go @@ -0,0 +1,72 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func adaptMySQLServers(modules terraform.Modules) []database.MySQLServer { + var mySQLServers []database.MySQLServer + for _, module := range modules { + // Support legacy azurerm_mysql_server + for _, resource := range module.GetResourcesByType("azurerm_mysql_server") { + mySQLServers = append(mySQLServers, adaptMySQLServer(resource, module)) + } + // Support new azurerm_mysql_flexible_server + for _, resource := range module.GetResourcesByType("azurerm_mysql_flexible_server") { + mySQLServers = append(mySQLServers, adaptMySQLFlexibleServer(resource, module)) + } + } + return mySQLServers +} + +func adaptMySQLServer(resource *terraform.Block, module *terraform.Module) database.MySQLServer { + var firewallRules []database.FirewallRule + firewallRuleBlocks := module.GetReferencingResources(resource, "azurerm_mysql_firewall_rule", "server_name") + for _, firewallBlock := range firewallRuleBlocks { + firewallRules = append(firewallRules, adaptFirewallRule(firewallBlock)) + } + + return database.MySQLServer{ + Metadata: resource.GetMetadata(), + Server: database.Server{ + Metadata: resource.GetMetadata(), + EnableSSLEnforcement: resource.GetAttribute("ssl_enforcement_enabled"). + AsBoolValueOrDefault(false, resource), + MinimumTLSVersion: resource.GetAttribute("ssl_minimal_tls_version_enforced"). + AsStringValueOrDefault("TLS1_2", resource), + EnablePublicNetworkAccess: resource.GetAttribute("public_network_access_enabled"). + AsBoolValueOrDefault(true, resource), + FirewallRules: firewallRules, + }, + } +} + +func adaptMySQLFlexibleServer(resource *terraform.Block, module *terraform.Module) database.MySQLServer { + var firewallRules []database.FirewallRule + + // Flexible server firewall rules use server_id instead of server_name + firewallRuleBlocks := module.GetReferencingResources(resource, "azurerm_mysql_flexible_server_firewall_rule", "server_id") + for _, firewallBlock := range firewallRuleBlocks { + firewallRules = append(firewallRules, adaptFirewallRule(firewallBlock)) + } + + // MySQL Flexible Server configurations (new standalone resource) + // TLS settings are configured through azurerm_mysql_flexible_server_configuration resources + // Each configuration resource manages a single parameter specified in the name attribute + // By default, the server enforces secure connections using TLS 1.2 + configBlocks := module.GetReferencingResources(resource, "azurerm_mysql_flexible_server_configuration", "server_id") + params := parseServerParameters(configBlocks, resource.GetMetadata()) + + return database.MySQLServer{ + Metadata: resource.GetMetadata(), + Server: database.Server{ + Metadata: resource.GetMetadata(), + EnableSSLEnforcement: params.requireSecureTransport, + MinimumTLSVersion: params.tlsVersion, + EnablePublicNetworkAccess: resource.GetAttribute("public_network_access_enabled"). + AsBoolValueOrDefault(true, resource), + FirewallRules: firewallRules, + }, + } +} diff --git a/pkg/iac/adapters/terraform/azure/database/postgresql.go b/pkg/iac/adapters/terraform/azure/database/postgresql.go new file mode 100644 index 000000000000..556309ff40f1 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/database/postgresql.go @@ -0,0 +1,141 @@ +package database + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptPostgreSQLServers(modules terraform.Modules) []database.PostgreSQLServer { + var postgreSQLServers []database.PostgreSQLServer + for _, module := range modules { + // Support legacy azurerm_postgresql_server + for _, resource := range module.GetResourcesByType("azurerm_postgresql_server") { + postgreSQLServers = append(postgreSQLServers, adaptPostgreSQLServer(resource, module)) + } + // Support new azurerm_postgresql_flexible_server + for _, resource := range module.GetResourcesByType("azurerm_postgresql_flexible_server") { + postgreSQLServers = append(postgreSQLServers, adaptPostgreSQLFlexibleServer(resource, module)) + } + } + + return postgreSQLServers +} + +func adaptPostgreSQLServer(resource *terraform.Block, module *terraform.Module) database.PostgreSQLServer { + var firewallRules []database.FirewallRule + firewallRuleBlocks := module.GetReferencingResources(resource, "azurerm_postgresql_firewall_rule", "server_name") + for _, firewallBlock := range firewallRuleBlocks { + firewallRules = append(firewallRules, adaptFirewallRule(firewallBlock)) + } + + configs := module.GetReferencingResources(resource, "azurerm_postgresql_configuration", "server_name") + config := adaptPostgreSQLConfig(resource, configs) + return database.PostgreSQLServer{ + Metadata: resource.GetMetadata(), + Server: database.Server{ + Metadata: resource.GetMetadata(), + EnableSSLEnforcement: resource.GetAttribute("ssl_enforcement_enabled"). + AsBoolValueOrDefault(false, resource), + MinimumTLSVersion: resource.GetAttribute("ssl_minimal_tls_version_enforced"). + AsStringValueOrDefault("TLS1_2", resource), + EnablePublicNetworkAccess: resource.GetAttribute("public_network_access_enabled"). + AsBoolValueOrDefault(true, resource), + FirewallRules: firewallRules, + }, + Config: config, + GeoRedundantBackupEnabled: resource.GetAttribute("geo_redundant_backup_enabled"). + AsBoolValueOrDefault(false, resource), + ThreatDetectionPolicy: adaptThreatDetectionPolicy(resource, resource.GetMetadata()), + } +} + +func adaptPostgreSQLFlexibleServer(resource *terraform.Block, module *terraform.Module) database.PostgreSQLServer { + var firewallRules []database.FirewallRule + + firewallRuleBlocks := module.GetReferencingResources(resource, "azurerm_postgresql_flexible_server_firewall_rule", "server_id") + for _, firewallBlock := range firewallRuleBlocks { + firewallRules = append(firewallRules, adaptFirewallRule(firewallBlock)) + } + + // PostgreSQL Flexible Server configurations (new standalone resource) + // TLS settings are configured through azurerm_postgresql_flexible_server_configuration resources + // Each configuration resource manages a single parameter specified in the name attribute + // By default, the server enforces secure connections using TLS 1.2 + // Flexible server configurations use server_id instead of server_name + configBlocks := module.GetReferencingResources(resource, "azurerm_postgresql_flexible_server_configuration", "server_id") + config := adaptPostgreSQLConfig(resource, configBlocks) + params := parseServerParameters(configBlocks, resource.GetMetadata()) + + return database.PostgreSQLServer{ + Metadata: resource.GetMetadata(), + Server: database.Server{ + Metadata: resource.GetMetadata(), + EnableSSLEnforcement: params.requireSecureTransport, + MinimumTLSVersion: params.tlsVersion, + EnablePublicNetworkAccess: resource.GetAttribute("public_network_access_enabled"). + AsBoolValueOrDefault(true, resource), + FirewallRules: firewallRules, + }, + Config: config, + GeoRedundantBackupEnabled: resource.GetAttribute("geo_redundant_backup_enabled"). + AsBoolValueOrDefault(false, resource), + + // Threat Detection is not configurable via Terraform for PostgreSQL Flexible Server + // It can only be configured via Azure CLI, so we mark it as unmanaged to avoid false positives + ThreatDetectionPolicy: database.ThreatDetectionPolicy{ + Metadata: iacTypes.NewUnmanagedMetadata(), + }, + } +} + +func adaptPostgreSQLConfig(resource *terraform.Block, configBlocks []*terraform.Block) database.PostgresSQLConfig { + var defaultMetadata iacTypes.Metadata + if resource != nil { + defaultMetadata = resource.GetMetadata() + } else { + defaultMetadata = iacTypes.NewUnmanagedMetadata() + } + + config := database.PostgresSQLConfig{ + Metadata: defaultMetadata, + LogCheckpoints: iacTypes.BoolDefault(false, defaultMetadata), + ConnectionThrottling: iacTypes.BoolDefault(false, defaultMetadata), + LogConnections: iacTypes.BoolDefault(false, defaultMetadata), + LogDisconnections: iacTypes.BoolDefault(false, defaultMetadata), + } + + for _, configBlock := range configBlocks { + + nameAttr := configBlock.GetAttribute("name") + valAttr := configBlock.GetAttribute("value") + + switch { + case nameAttr.Equals("log_checkpoints"): + config.LogCheckpoints = iacTypes.Bool(valAttr.Equals("on"), valAttr.GetMetadata()) + case nameAttr.Equals("connection_throttling"): + config.ConnectionThrottling = iacTypes.Bool(valAttr.Equals("on"), valAttr.GetMetadata()) + case nameAttr.Equals("log_connections"): + config.LogConnections = iacTypes.Bool(valAttr.Equals("on"), valAttr.GetMetadata()) + case nameAttr.Equals("log_disconnections"): + config.LogDisconnections = iacTypes.Bool(valAttr.Equals("on"), valAttr.GetMetadata()) + } + } + + return config +} + +func adaptThreatDetectionPolicy(resource *terraform.Block, defaultMetadata iacTypes.Metadata) database.ThreatDetectionPolicy { + block := resource.GetBlock("threat_detection_policy") + if block.IsNil() { + return database.ThreatDetectionPolicy{ + Metadata: defaultMetadata, + Enabled: iacTypes.BoolDefault(false, defaultMetadata), + } + } + + return database.ThreatDetectionPolicy{ + Metadata: block.GetMetadata(), + Enabled: block.GetAttribute("enabled").AsBoolValueOrDefault(false, block), + } +} diff --git a/pkg/iac/adapters/terraform/azure/datafactory/adapt.go b/pkg/iac/adapters/terraform/azure/datafactory/adapt.go new file mode 100644 index 000000000000..2dfc108ee3e9 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/datafactory/adapt.go @@ -0,0 +1,33 @@ +package datafactory + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datafactory" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) datafactory.DataFactory { + return datafactory.DataFactory{ + DataFactories: adaptFactories(modules), + } +} + +func adaptFactories(modules terraform.Modules) []datafactory.Factory { + var factories []datafactory.Factory + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_data_factory") { + factories = append(factories, adaptFactory(resource)) + } + } + return factories +} + +func adaptFactory(resource *terraform.Block) datafactory.Factory { + enablePublicNetworkAttr := resource.GetAttribute("public_network_enabled") + enablePublicNetworkVal := enablePublicNetworkAttr.AsBoolValueOrDefault(true, resource) + + return datafactory.Factory{ + Metadata: resource.GetMetadata(), + EnablePublicNetwork: enablePublicNetworkVal, + } +} diff --git a/pkg/iac/adapters/terraform/azure/datafactory/adapt_test.go b/pkg/iac/adapters/terraform/azure/datafactory/adapt_test.go new file mode 100644 index 000000000000..a8aa6c3d5184 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/datafactory/adapt_test.go @@ -0,0 +1,73 @@ +package datafactory + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datafactory" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptFactory(t *testing.T) { + tests := []struct { + name string + terraform string + expected datafactory.Factory + }{ + { + name: "defined", + terraform: ` + resource "azurerm_data_factory" "example" { + name = "example" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + public_network_enabled = false + } +`, + expected: datafactory.Factory{}, + }, + { + name: "default", + terraform: ` + resource "azurerm_data_factory" "example" { + name = "example" + } +`, + expected: datafactory.Factory{ + EnablePublicNetwork: iacTypes.BoolTest(true), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptFactory(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_data_factory" "example" { + name = "example" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + public_network_enabled = false + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.DataFactories, 1) + dataFactory := adapted.DataFactories[0] + + assert.Equal(t, 6, dataFactory.EnablePublicNetwork.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, dataFactory.EnablePublicNetwork.GetMetadata().Range().GetEndLine()) + +} diff --git a/pkg/iac/adapters/terraform/azure/datalake/adapt.go b/pkg/iac/adapters/terraform/azure/datalake/adapt.go new file mode 100644 index 000000000000..3f81d8925fd8 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/datalake/adapt.go @@ -0,0 +1,38 @@ +package datalake + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datalake" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) datalake.DataLake { + return datalake.DataLake{ + Stores: adaptStores(modules), + } +} + +func adaptStores(modules terraform.Modules) []datalake.Store { + var stores []datalake.Store + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_data_lake_store") { + stores = append(stores, adaptStore(resource)) + } + } + return stores +} + +func adaptStore(resource *terraform.Block) datalake.Store { + store := datalake.Store{ + Metadata: resource.GetMetadata(), + EnableEncryption: types.BoolDefault(true, resource.GetMetadata()), + } + encryptionStateAttr := resource.GetAttribute("encryption_state") + if encryptionStateAttr.Equals("Disabled") { + store.EnableEncryption = types.Bool(false, encryptionStateAttr.GetMetadata()) + } else if encryptionStateAttr.Equals("Enabled") { + store.EnableEncryption = types.Bool(true, encryptionStateAttr.GetMetadata()) + } + return store +} diff --git a/pkg/iac/adapters/terraform/azure/datalake/adapt_test.go b/pkg/iac/adapters/terraform/azure/datalake/adapt_test.go new file mode 100644 index 000000000000..c88fdd4c1e35 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/datalake/adapt_test.go @@ -0,0 +1,76 @@ +package datalake + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datalake" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptStore(t *testing.T) { + tests := []struct { + name string + terraform string + expected datalake.Store + }{ + { + name: "enabled", + terraform: ` + resource "azurerm_data_lake_store" "good_example" { + encryption_state = "Enabled" + } +`, + expected: datalake.Store{ + EnableEncryption: iacTypes.BoolTest(true), + }, + }, + { + name: "disabled", + terraform: ` + resource "azurerm_data_lake_store" "good_example" { + encryption_state = "Disabled" + } +`, + expected: datalake.Store{}, + }, + { + name: "enabled by default", + terraform: ` + resource "azurerm_data_lake_store" "good_example" { + } +`, + expected: datalake.Store{ + EnableEncryption: iacTypes.BoolTest(true), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptStore(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_data_lake_store" "good_example" { + encryption_state = "Disabled" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Stores, 1) + store := adapted.Stores[0] + + assert.Equal(t, 3, store.EnableEncryption.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, store.EnableEncryption.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/azure/keyvault/adapt.go b/pkg/iac/adapters/terraform/azure/keyvault/adapt.go new file mode 100644 index 000000000000..87461bfbeedc --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/keyvault/adapt.go @@ -0,0 +1,157 @@ +package keyvault + +import ( + "time" + + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/keyvault" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) keyvault.KeyVault { + adapter := adapter{ + vaultSecretIDs: modules.GetChildResourceIDMapByType("azurerm_key_vault_secret"), + vaultKeyIDs: modules.GetChildResourceIDMapByType("azurerm_key_vault_key"), + } + + return keyvault.KeyVault{ + Vaults: adapter.adaptVaults(modules), + } +} + +type adapter struct { + vaultSecretIDs terraform.ResourceIDResolutions + vaultKeyIDs terraform.ResourceIDResolutions +} + +func (a *adapter) adaptVaults(modules terraform.Modules) []keyvault.Vault { + + var vaults []keyvault.Vault + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_key_vault") { + vaults = append(vaults, a.adaptVault(resource, module)) + + } + } + + orphanResources := modules.GetResourceByIDs(a.vaultSecretIDs.Orphans()...) + + if len(orphanResources) > 0 { + orphanage := keyvault.Vault{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Secrets: nil, + Keys: nil, + EnablePurgeProtection: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + SoftDeleteRetentionDays: iacTypes.IntDefault(0, iacTypes.NewUnmanagedMetadata()), + NetworkACLs: keyvault.NetworkACLs{ + Metadata: iacTypes.NewUnmanagedMetadata(), + DefaultAction: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + } + for _, secretResource := range orphanResources { + orphanage.Secrets = append(orphanage.Secrets, adaptSecret(secretResource)) + } + vaults = append(vaults, orphanage) + } + + orphanResources = modules.GetResourceByIDs(a.vaultKeyIDs.Orphans()...) + + if len(orphanResources) > 0 { + orphanage := keyvault.Vault{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Secrets: nil, + Keys: nil, + EnablePurgeProtection: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + SoftDeleteRetentionDays: iacTypes.IntDefault(0, iacTypes.NewUnmanagedMetadata()), + NetworkACLs: keyvault.NetworkACLs{ + Metadata: iacTypes.NewUnmanagedMetadata(), + DefaultAction: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + } + for _, secretResource := range orphanResources { + orphanage.Keys = append(orphanage.Keys, adaptKey(secretResource)) + } + vaults = append(vaults, orphanage) + } + + return vaults +} + +func (a *adapter) adaptVault(resource *terraform.Block, module *terraform.Module) keyvault.Vault { + var keys []keyvault.Key + var secrets []keyvault.Secret + + defaultActionVal := iacTypes.StringDefault("", resource.GetMetadata()) + + secretBlocks := module.GetReferencingResources(resource, "azurerm_key_vault_secret", "key_vault_id") + for _, secretBlock := range secretBlocks { + a.vaultSecretIDs.Resolve(secretBlock.ID()) + secrets = append(secrets, adaptSecret(secretBlock)) + } + + keyBlocks := module.GetReferencingResources(resource, "azurerm_key_vault_key", "key_vault_id") + for _, keyBlock := range keyBlocks { + a.vaultKeyIDs.Resolve(keyBlock.ID()) + keys = append(keys, adaptKey(keyBlock)) + } + + purgeProtectionAttr := resource.GetAttribute("purge_protection_enabled") + purgeProtectionVal := purgeProtectionAttr.AsBoolValueOrDefault(false, resource) + + softDeleteRetentionDaysAttr := resource.GetAttribute("soft_delete_retention_days") + softDeleteRetentionDaysVal := softDeleteRetentionDaysAttr.AsIntValueOrDefault(0, resource) + + aclMetadata := iacTypes.NewUnmanagedMetadata() + if aclBlock := resource.GetBlock("network_acls"); aclBlock.IsNotNil() { + aclMetadata = aclBlock.GetMetadata() + defaultActionAttr := aclBlock.GetAttribute("default_action") + defaultActionVal = defaultActionAttr.AsStringValueOrDefault("", resource.GetBlock("network_acls")) + } + + return keyvault.Vault{ + Metadata: resource.GetMetadata(), + Secrets: secrets, + Keys: keys, + EnablePurgeProtection: purgeProtectionVal, + SoftDeleteRetentionDays: softDeleteRetentionDaysVal, + NetworkACLs: keyvault.NetworkACLs{ + Metadata: aclMetadata, + DefaultAction: defaultActionVal, + }, + } +} + +func adaptSecret(resource *terraform.Block) keyvault.Secret { + contentTypeAttr := resource.GetAttribute("content_type") + contentTypeVal := contentTypeAttr.AsStringValueOrDefault("", resource) + + return keyvault.Secret{ + Metadata: resource.GetMetadata(), + ContentType: contentTypeVal, + ExpiryDate: resolveExpiryDate(resource), + } +} + +func adaptKey(resource *terraform.Block) keyvault.Key { + + return keyvault.Key{ + Metadata: resource.GetMetadata(), + ExpiryDate: resolveExpiryDate(resource), + } +} + +func resolveExpiryDate(resource *terraform.Block) iacTypes.TimeValue { + expiryDateAttr := resource.GetAttribute("expiration_date") + expiryDateVal := iacTypes.TimeDefault(time.Time{}, resource.GetMetadata()) + + if expiryDateAttr.IsString() { + expiryDateString := expiryDateAttr.Value().AsString() + if expiryDate, err := time.Parse(time.RFC3339, expiryDateString); err == nil { + expiryDateVal = iacTypes.Time(expiryDate, expiryDateAttr.GetMetadata()) + } + } else if expiryDateAttr.IsNotNil() { + expiryDateVal = iacTypes.TimeUnresolvable(expiryDateAttr.GetMetadata()) + } + + return expiryDateVal +} diff --git a/pkg/iac/adapters/terraform/azure/keyvault/adapt_test.go b/pkg/iac/adapters/terraform/azure/keyvault/adapt_test.go new file mode 100644 index 000000000000..caad3dd210f4 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/keyvault/adapt_test.go @@ -0,0 +1,253 @@ +package keyvault + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/keyvault" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected keyvault.KeyVault + }{ + { + name: "defined", + terraform: ` + resource "azurerm_key_vault" "example" { + name = "examplekeyvault" + enabled_for_disk_encryption = true + soft_delete_retention_days = 7 + purge_protection_enabled = true + + network_acls { + bypass = "AzureServices" + default_action = "Deny" + } + } +`, + expected: keyvault.KeyVault{ + Vaults: []keyvault.Vault{ + { + EnablePurgeProtection: iacTypes.BoolTest(true), + SoftDeleteRetentionDays: iacTypes.IntTest(7), + NetworkACLs: keyvault.NetworkACLs{ + DefaultAction: iacTypes.StringTest("Deny"), + }, + }, + }, + }, + }, + { + name: "defaults", + terraform: ` + resource "azurerm_key_vault" "example" { + } +`, + expected: keyvault.KeyVault{ + Vaults: []keyvault.Vault{ + { + NetworkACLs: keyvault.NetworkACLs{}, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptSecret(t *testing.T) { + tests := []struct { + name string + terraform string + expected keyvault.Secret + }{ + { + name: "defaults", + terraform: ` + resource "azurerm_key_vault_secret" "example" { + } +`, + expected: keyvault.Secret{ + ExpiryDate: iacTypes.TimeTest(time.Time{}), + }, + }, + { + name: "defined", + terraform: ` + resource "azurerm_key_vault_secret" "example" { + content_type = "password" + expiration_date = "1982-12-31T00:00:00Z" + } +`, + expected: keyvault.Secret{ + ContentType: iacTypes.StringTest("password"), + ExpiryDate: iacTypes.TimeTest(func(timeVal string) time.Time { + parsed, _ := time.Parse(time.RFC3339, timeVal) + return parsed + }("1982-12-31T00:00:00Z"))}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptSecret(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptKey(t *testing.T) { + tests := []struct { + name string + terraform string + expected keyvault.Key + }{ + { + name: "defined", + terraform: ` + resource "azurerm_key_vault_key" "example" { + name = "generated-certificate" + expiration_date = "1982-12-31T00:00:00Z" + } +`, + expected: keyvault.Key{ + ExpiryDate: iacTypes.TimeTest(func(timeVal string) time.Time { + parsed, _ := time.Parse(time.RFC3339, timeVal) + return parsed + }("1982-12-31T00:00:00Z")), + }, + }, + { + name: "defaults", + terraform: ` + resource "azurerm_key_vault_key" "example" { + } +`, + expected: keyvault.Key{ + ExpiryDate: iacTypes.TimeTest(time.Time{}), + }, + }, + { + name: "expiration date refers to the resource", + terraform: ` +terraform { + required_version = ">=1.3.0" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">=3.0.0" + } + time = { + source = "hashicorp/time" + version = ">=0.9.0" + } + } +} + +resource "azurerm_key_vault" "this" { + name = "keyvault" + location = "us-west" + resource_group_name = "resource-group" + tenant_id = "tenant-id" + sku_name = "Standard" +} + +resource "time_offset" "expiry" { + offset_years = 1 + base_rfc3339 = "YYYY-MM-DDTHH:MM:SSZ" +} + +resource "azurerm_key_vault_key" "this" { + name = "key" + key_vault_id = azurerm_key_vault.this.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] + expiration_date = time_offset.expiry.rfc3339 +} +`, + expected: keyvault.Key{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptKey(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_key_vault" "example" { + name = "examplekeyvault" + enabled_for_disk_encryption = true + soft_delete_retention_days = 7 + purge_protection_enabled = true + + network_acls { + bypass = "AzureServices" + default_action = "Deny" + } + } + + resource "azurerm_key_vault_key" "example" { + key_vault_id = azurerm_key_vault.example.id + name = "generated-certificate" + expiration_date = "1982-12-31T00:00:00Z" + } + + resource "azurerm_key_vault_secret" "example" { + key_vault_id = azurerm_key_vault.example.id + content_type = "password" + expiration_date = "1982-12-31T00:00:00Z" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Vaults, 1) + require.Len(t, adapted.Vaults[0].Keys, 1) + require.Len(t, adapted.Vaults[0].Secrets, 1) + + vault := adapted.Vaults[0] + key := vault.Keys[0] + secret := vault.Secrets[0] + + assert.Equal(t, 5, vault.SoftDeleteRetentionDays.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, vault.SoftDeleteRetentionDays.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, vault.EnablePurgeProtection.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, vault.EnablePurgeProtection.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 10, vault.NetworkACLs.DefaultAction.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, vault.NetworkACLs.DefaultAction.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 17, key.ExpiryDate.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 17, key.ExpiryDate.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 22, secret.ContentType.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 22, secret.ContentType.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 23, secret.ExpiryDate.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 23, secret.ExpiryDate.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/azure/monitor/adapt.go b/pkg/iac/adapters/terraform/azure/monitor/adapt.go new file mode 100644 index 000000000000..a622bcdeaf41 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/monitor/adapt.go @@ -0,0 +1,56 @@ +package monitor + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/monitor" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) monitor.Monitor { + return monitor.Monitor{ + LogProfiles: adaptLogProfiles(modules), + } +} + +func adaptLogProfiles(modules terraform.Modules) []monitor.LogProfile { + var logProfiles []monitor.LogProfile + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_monitor_log_profile") { + logProfiles = append(logProfiles, adaptLogProfile(resource)) + } + } + return logProfiles +} + +func adaptLogProfile(resource *terraform.Block) monitor.LogProfile { + + logProfile := monitor.LogProfile{ + Metadata: resource.GetMetadata(), + RetentionPolicy: monitor.RetentionPolicy{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + Days: iacTypes.IntDefault(0, resource.GetMetadata()), + }, + Categories: nil, + Locations: nil, + } + + if retentionPolicyBlock := resource.GetBlock("retention_policy"); retentionPolicyBlock.IsNotNil() { + logProfile.RetentionPolicy.Metadata = retentionPolicyBlock.GetMetadata() + enabledAttr := retentionPolicyBlock.GetAttribute("enabled") + logProfile.RetentionPolicy.Enabled = enabledAttr.AsBoolValueOrDefault(false, resource) + daysAttr := retentionPolicyBlock.GetAttribute("days") + logProfile.RetentionPolicy.Days = daysAttr.AsIntValueOrDefault(0, resource) + } + + if categoriesAttr := resource.GetAttribute("categories"); categoriesAttr.IsNotNil() { + logProfile.Categories = categoriesAttr.AsStringValues() + } + + if locationsAttr := resource.GetAttribute("locations"); locationsAttr.IsNotNil() { + logProfile.Locations = locationsAttr.AsStringValues() + } + + return logProfile +} diff --git a/pkg/iac/adapters/terraform/azure/monitor/adapt_test.go b/pkg/iac/adapters/terraform/azure/monitor/adapt_test.go new file mode 100644 index 000000000000..e358a6d00b5c --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/monitor/adapt_test.go @@ -0,0 +1,119 @@ +package monitor + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/monitor" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptLogProfile(t *testing.T) { + tests := []struct { + name string + terraform string + expected monitor.LogProfile + }{ + { + name: "defined", + terraform: ` + resource "azurerm_monitor_log_profile" "example" { + categories = [ + "Action", + "Delete", + "Write", + ] + + retention_policy { + enabled = true + days = 365 + } + + locations = [ + "eastus", + "eastus2", + "southcentralus" + ] + } +`, + expected: monitor.LogProfile{ + Categories: []iacTypes.StringValue{ + iacTypes.StringTest("Action"), + iacTypes.StringTest("Delete"), + iacTypes.StringTest("Write"), + }, + RetentionPolicy: monitor.RetentionPolicy{ + Enabled: iacTypes.BoolTest(true), + Days: iacTypes.IntTest(365), + }, + Locations: []iacTypes.StringValue{ + iacTypes.StringTest("eastus"), + iacTypes.StringTest("eastus2"), + iacTypes.StringTest("southcentralus"), + }, + }, + }, + { + name: "default", + terraform: ` + resource "azurerm_monitor_log_profile" "example" { + } +`, + expected: monitor.LogProfile{ + RetentionPolicy: monitor.RetentionPolicy{}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptLogProfile(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_monitor_log_profile" "example" { + categories = [ + "Action", + "Delete", + "Write", + ] + + retention_policy { + enabled = true + days = 365 + } + + locations = [ + "eastus", + "eastus2", + "southcentralus" + ] + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.LogProfiles, 1) + logProfile := adapted.LogProfiles[0] + + assert.Equal(t, 3, logProfile.Categories[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, logProfile.Categories[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 10, logProfile.RetentionPolicy.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, logProfile.RetentionPolicy.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, logProfile.RetentionPolicy.Days.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, logProfile.RetentionPolicy.Days.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 14, logProfile.Locations[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 18, logProfile.Locations[0].GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/azure/network/adapt.go b/pkg/iac/adapters/terraform/azure/network/adapt.go new file mode 100644 index 000000000000..a80ae1e52f77 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/network/adapt.go @@ -0,0 +1,164 @@ +package network + +import ( + "github.com/google/uuid" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/common" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func parsePortRange(input string, meta iacTypes.Metadata) common.PortRange { + return common.ParsePortRange(input, meta, common.WithWildcard()) +} + +func Adapt(modules terraform.Modules) network.Network { + return network.Network{ + SecurityGroups: (&adapter{ + modules: modules, + groups: make(map[string]network.SecurityGroup), + }).adaptSecurityGroups(), + NetworkWatcherFlowLogs: adaptWatcherLogs(modules), + NetworkInterfaces: adaptNetworkInterfaces(modules), + } +} + +type adapter struct { + modules terraform.Modules + groups map[string]network.SecurityGroup +} + +func (a *adapter) adaptSecurityGroups() []network.SecurityGroup { + + for _, module := range a.modules { + for _, resource := range module.GetResourcesByType("azurerm_network_security_group") { + a.adaptSecurityGroup(resource) + } + } + + for _, ruleBlock := range a.modules.GetResourcesByType("azurerm_network_security_rule") { + rule := AdaptSGRule(ruleBlock) + + groupAttr := ruleBlock.GetAttribute("network_security_group_name") + if groupAttr.IsNotNil() { + if referencedBlock, err := a.modules.GetReferencedBlock(groupAttr, ruleBlock); err == nil { + if group, ok := a.groups[referencedBlock.ID()]; ok { + group.Rules = append(group.Rules, rule) + a.groups[referencedBlock.ID()] = group + continue + } + } + + } + + a.groups[uuid.NewString()] = network.SecurityGroup{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Rules: []network.SecurityGroupRule{rule}, + } + } + + var securityGroups []network.SecurityGroup + for _, group := range a.groups { + securityGroups = append(securityGroups, group) + } + + return securityGroups +} + +func adaptWatcherLogs(modules terraform.Modules) []network.NetworkWatcherFlowLog { + var watcherLogs []network.NetworkWatcherFlowLog + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_network_watcher_flow_log") { + watcherLogs = append(watcherLogs, adaptWatcherLog(resource)) + } + } + return watcherLogs +} + +func (a *adapter) adaptSecurityGroup(resource *terraform.Block) { + var rules []network.SecurityGroupRule + for _, ruleBlock := range resource.GetBlocks("security_rule") { + rules = append(rules, AdaptSGRule(ruleBlock)) + } + a.groups[resource.ID()] = network.SecurityGroup{ + Metadata: resource.GetMetadata(), + Rules: rules, + } +} + +func adaptWatcherLog(resource *terraform.Block) network.NetworkWatcherFlowLog { + flowLog := network.NetworkWatcherFlowLog{ + Metadata: resource.GetMetadata(), + Enabled: resource.GetAttribute("enabled").AsBoolValueOrDefault(false, resource), + RetentionPolicy: network.RetentionPolicy{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + Days: iacTypes.IntDefault(0, resource.GetMetadata()), + }, + } + + if retentionPolicyBlock := resource.GetBlock("retention_policy"); retentionPolicyBlock.IsNotNil() { + flowLog.RetentionPolicy = network.RetentionPolicy{ + Metadata: retentionPolicyBlock.GetMetadata(), + Enabled: retentionPolicyBlock.GetAttribute("enabled"). + AsBoolValueOrDefault(false, retentionPolicyBlock), + Days: retentionPolicyBlock.GetAttribute("days"). + AsIntValueOrDefault(0, retentionPolicyBlock), + } + } + return flowLog +} + +func adaptNetworkInterfaces(modules terraform.Modules) []network.NetworkInterface { + var networkInterfaces []network.NetworkInterface + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_network_interface") { + networkInterfaces = append(networkInterfaces, AdaptNetworkInterface(resource, modules)) + } + } + + return networkInterfaces +} + +func AdaptNetworkInterface(resource *terraform.Block, modules terraform.Modules) network.NetworkInterface { + ni := network.NetworkInterface{ + Metadata: resource.GetMetadata(), + // Support both ip_forwarding_enabled (new) and enable_ip_forwarding (old) attributes + EnableIPForwarding: resource.GetFirstAttributeOf("ip_forwarding_enabled", "enable_ip_forwarding"). + AsBoolValueOrDefault(false, resource), + HasPublicIP: iacTypes.BoolDefault(false, resource.GetMetadata()), + PublicIPAddress: iacTypes.StringDefault("", resource.GetMetadata()), + SubnetID: iacTypes.StringDefault("", resource.GetMetadata()), + } + + if nsgAttr := resource.GetAttribute("network_security_group_id"); nsgAttr.IsNotNil() { + if referencedNSG, err := modules.GetReferencedBlock(nsgAttr, resource); err == nil { + ni.SecurityGroups = []network.SecurityGroup{adaptSecurityGroupFromBlock(referencedNSG)} + } + } + + ipConfigs := resource.GetBlocks("ip_configuration") + ni.IPConfigurations = make([]network.IPConfiguration, 0, len(ipConfigs)) + for _, ipConfig := range ipConfigs { + ni.IPConfigurations = append(ni.IPConfigurations, network.IPConfiguration{ + Metadata: ipConfig.GetMetadata(), + PublicIPAddress: ipConfig.GetAttribute("public_ip_address_id").AsStringValueOrDefault("", ipConfig), + SubnetID: ipConfig.GetAttribute("subnet_id").AsStringValueOrDefault("", ipConfig), + Primary: ipConfig.GetAttribute("primary").AsBoolValueOrDefault(false, ipConfig), + }) + } + + ni.Setup() + return ni +} + +func adaptSecurityGroupFromBlock(resource *terraform.Block) network.SecurityGroup { + return network.SecurityGroup{ + Metadata: resource.GetMetadata(), + Rules: xslices.Map(resource.GetBlocks("security_rule"), AdaptSGRule), + } +} diff --git a/pkg/iac/adapters/terraform/azure/network/adapt_test.go b/pkg/iac/adapters/terraform/azure/network/adapt_test.go new file mode 100644 index 000000000000..cf57dce351ed --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/network/adapt_test.go @@ -0,0 +1,288 @@ +package network + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/common" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected network.Network + }{ + { + name: "defined", + terraform: ` + resource "azurerm_network_security_rule" "example" { + name = "example_security_rule" + network_security_group_name = azurerm_network_security_group.example.name + direction = "Inbound" + access = "Allow" + protocol = "TCP" + source_port_range = "*" + destination_port_ranges = ["3389"] + source_address_prefix = "4.53.160.75" + destination_address_prefix = "*" + } + + resource "azurerm_network_security_group" "example" { + name = "tf-appsecuritygroup" + } + + resource "azurerm_network_watcher_flow_log" "example" { + resource_group_name = azurerm_resource_group.example.name + name = "example-log" + enabled = true + + retention_policy { + enabled = true + days = 7 + } + } +`, + expected: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Rules: []network.SecurityGroupRule{ + { + Allow: iacTypes.BoolTest(true), + SourceAddresses: []iacTypes.StringValue{ + iacTypes.StringTest("4.53.160.75"), + }, + DestinationAddresses: []iacTypes.StringValue{ + iacTypes.StringTest("*"), + }, + SourcePorts: []common.PortRange{ + { + End: iacTypes.IntTest(65535), + }, + }, + DestinationPorts: []common.PortRange{ + { + Start: iacTypes.IntTest(3389), + End: iacTypes.IntTest(3389), + }, + }, + Protocol: iacTypes.StringTest("TCP"), + }, + }, + }, + }, + NetworkWatcherFlowLogs: []network.NetworkWatcherFlowLog{ + { + Enabled: iacTypes.BoolTest(true), + RetentionPolicy: network.RetentionPolicy{ + Enabled: iacTypes.BoolTest(true), + Days: iacTypes.IntTest(7), + }, + }, + }, + }, + }, + { + name: "defaults", + terraform: `resource "azurerm_network_security_group" "example" { + name = "tf-appsecuritygroup" + security_rule {} +} +`, + expected: network.Network{ + SecurityGroups: []network.SecurityGroup{ + { + Rules: []network.SecurityGroupRule{ + { + Allow: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + }, + { + name: "network interface", + terraform: `resource "azurerm_network_interface" "example" { + name = "example-nic" + location = "eastus" + resource_group_name = "example-rg" + + ip_configuration { + name = "primary-ip" + primary = true + subnet_id = "subnet-primary-id" + public_ip_address_id = "public-ip-primary-id" + } + + ip_configuration { + name = "secondary-ip" + subnet_id = "subnet-secondary-id" + } +} +`, + expected: network.Network{ + NetworkInterfaces: []network.NetworkInterface{ + { + // legacy fields filled from primary + SubnetID: iacTypes.StringTest("subnet-primary-id"), + HasPublicIP: iacTypes.BoolTest(true), + PublicIPAddress: iacTypes.StringTest("public-ip-primary-id"), + + IPConfigurations: []network.IPConfiguration{ + { + SubnetID: iacTypes.StringTest("subnet-primary-id"), + Primary: iacTypes.BoolTest(true), + HasPublicIP: iacTypes.BoolTest(true), + PublicIPAddress: iacTypes.StringTest("public-ip-primary-id"), + }, + { + SubnetID: iacTypes.StringTest("subnet-secondary-id"), + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptWatcherLog(t *testing.T) { + tests := []struct { + name string + terraform string + expected network.NetworkWatcherFlowLog + }{ + { + name: "defined", + terraform: ` + resource "azurerm_network_watcher_flow_log" "watcher" { + enabled = true + retention_policy { + enabled = true + days = 90 + } + } +`, + expected: network.NetworkWatcherFlowLog{ + Enabled: iacTypes.BoolTest(true), + RetentionPolicy: network.RetentionPolicy{ + Enabled: iacTypes.BoolTest(true), + Days: iacTypes.IntTest(90), + }, + }, + }, + { + name: "defaults", + terraform: ` + resource "azurerm_network_watcher_flow_log" "watcher" { + retention_policy { + } + } +`, + expected: network.NetworkWatcherFlowLog{ + RetentionPolicy: network.RetentionPolicy{}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptWatcherLog(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_network_security_group" "example" { + name = "tf-appsecuritygroup" + } + + resource "azurerm_network_security_rule" "example" { + name = "example_security_rule" + network_security_group_name = azurerm_network_security_group.example.name + direction = "Inbound" + access = "Allow" + protocol = "TCP" + source_port_range = "*" + destination_port_ranges = ["3389"] + source_address_prefix = "4.53.160.75" + destination_address_prefix = "*" + } + + resource "azurerm_network_watcher_flow_log" "example" { + resource_group_name = azurerm_resource_group.example.name + name = "example-log" + + retention_policy { + enabled = true + days = 7 + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.SecurityGroups, 1) + require.Len(t, adapted.NetworkWatcherFlowLogs, 1) + + securityGroup := adapted.SecurityGroups[0] + rule := securityGroup.Rules[0] + watcher := adapted.NetworkWatcherFlowLogs[0] + + assert.Equal(t, 2, securityGroup.Metadata.Range().GetStartLine()) + assert.Equal(t, 4, securityGroup.Metadata.Range().GetEndLine()) + + assert.Equal(t, 6, rule.Metadata.Range().GetStartLine()) + assert.Equal(t, 16, rule.Metadata.Range().GetEndLine()) + + assert.Equal(t, 9, rule.Outbound.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 9, rule.Outbound.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 10, rule.Allow.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, rule.Allow.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, rule.Protocol.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, rule.Protocol.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 12, rule.SourcePorts[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 12, rule.SourcePorts[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 13, rule.DestinationPorts[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 13, rule.DestinationPorts[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 14, rule.SourceAddresses[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 14, rule.SourceAddresses[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 15, rule.DestinationAddresses[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 15, rule.DestinationAddresses[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 18, watcher.Metadata.Range().GetStartLine()) + assert.Equal(t, 26, watcher.Metadata.Range().GetEndLine()) + + assert.Equal(t, 22, watcher.RetentionPolicy.Metadata.Range().GetStartLine()) + assert.Equal(t, 25, watcher.RetentionPolicy.Metadata.Range().GetEndLine()) + + assert.Equal(t, 23, watcher.RetentionPolicy.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 23, watcher.RetentionPolicy.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 24, watcher.RetentionPolicy.Days.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 24, watcher.RetentionPolicy.Days.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/azure/network/common.go b/pkg/iac/adapters/terraform/azure/network/common.go new file mode 100644 index 000000000000..13d1f23be89f --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/network/common.go @@ -0,0 +1,101 @@ +package network + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/common" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func AdaptSGRule(ruleBlock *terraform.Block) network.SecurityGroupRule { + + rule := network.SecurityGroupRule{ + Metadata: ruleBlock.GetMetadata(), + Outbound: iacTypes.BoolDefault(false, ruleBlock.GetMetadata()), + Allow: iacTypes.BoolDefault(true, ruleBlock.GetMetadata()), + SourceAddresses: nil, + SourcePorts: nil, + DestinationAddresses: nil, + DestinationPorts: nil, + Protocol: ruleBlock.GetAttribute("protocol").AsStringValueOrDefault("", ruleBlock), + } + + accessAttr := ruleBlock.GetAttribute("access") + if accessAttr.Equals("Allow") { + rule.Allow = iacTypes.Bool(true, accessAttr.GetMetadata()) + } else if accessAttr.Equals("Deny") { + rule.Allow = iacTypes.Bool(false, accessAttr.GetMetadata()) + } + + directionAttr := ruleBlock.GetAttribute("direction") + if directionAttr.Equals("Inbound") { + rule.Outbound = iacTypes.Bool(false, directionAttr.GetMetadata()) + } else if directionAttr.Equals("Outbound") { + rule.Outbound = iacTypes.Bool(true, directionAttr.GetMetadata()) + } + + adaptSource(ruleBlock, &rule) + adaptDestination(ruleBlock, &rule) + + return rule +} + +func adaptSource(ruleBlock *terraform.Block, rule *network.SecurityGroupRule) { + if sourceAddressAttr := ruleBlock.GetAttribute("source_address_prefix"); sourceAddressAttr.IsString() { + rule.SourceAddresses = append(rule.SourceAddresses, sourceAddressAttr.AsStringValueOrDefault("", ruleBlock)) + } else if sourceAddressPrefixesAttr := ruleBlock.GetAttribute("source_address_prefixes"); sourceAddressPrefixesAttr.IsNotNil() { + rule.SourceAddresses = append(rule.SourceAddresses, sourceAddressPrefixesAttr.AsStringValues()...) + } + + if sourcePortRangesAttr := ruleBlock.GetAttribute("source_port_ranges"); sourcePortRangesAttr.IsNotNil() { + ports := sourcePortRangesAttr.AsStringValues() + for _, value := range ports { + rng := parsePortRange(value.Value(), value.GetMetadata()) + if rng.Valid() { + rule.SourcePorts = append(rule.SourcePorts, rng) + } + } + } else if sourcePortRangeAttr := ruleBlock.GetAttribute("source_port_range"); sourcePortRangeAttr.IsString() { + rng := parsePortRange(sourcePortRangeAttr.Value().AsString(), sourcePortRangeAttr.GetMetadata()) + if rng.Valid() { + rule.SourcePorts = append(rule.SourcePorts, rng) + } + } else if sourcePortRangeAttr := ruleBlock.GetAttribute("source_port_range"); sourcePortRangeAttr.IsNumber() { + f := sourcePortRangeAttr.AsNumber() + rule.SourcePorts = append(rule.SourcePorts, common.PortRange{ + Metadata: sourcePortRangeAttr.GetMetadata(), + Start: iacTypes.Int(int(f), sourcePortRangeAttr.GetMetadata()), + End: iacTypes.Int(int(f), sourcePortRangeAttr.GetMetadata()), + }) + } +} + +func adaptDestination(ruleBlock *terraform.Block, rule *network.SecurityGroupRule) { + if destAddressAttr := ruleBlock.GetAttribute("destination_address_prefix"); destAddressAttr.IsString() { + rule.DestinationAddresses = append(rule.DestinationAddresses, destAddressAttr.AsStringValueOrDefault("", ruleBlock)) + } else if destAddressPrefixesAttr := ruleBlock.GetAttribute("destination_address_prefixes"); destAddressPrefixesAttr.IsNotNil() { + rule.DestinationAddresses = append(rule.DestinationAddresses, destAddressPrefixesAttr.AsStringValues()...) + } + + if destPortRangesAttr := ruleBlock.GetAttribute("destination_port_ranges"); destPortRangesAttr.IsNotNil() { + ports := destPortRangesAttr.AsStringValues() + for _, value := range ports { + rng := parsePortRange(value.Value(), destPortRangesAttr.GetMetadata()) + if rng.Valid() { + rule.DestinationPorts = append(rule.DestinationPorts, rng) + } + } + } else if destPortRangeAttr := ruleBlock.GetAttribute("destination_port_range"); destPortRangeAttr.IsString() { + rng := parsePortRange(destPortRangeAttr.Value().AsString(), destPortRangeAttr.GetMetadata()) + if rng.Valid() { + rule.DestinationPorts = append(rule.DestinationPorts, rng) + } + } else if destPortRangeAttr := ruleBlock.GetAttribute("destination_port_range"); destPortRangeAttr.IsNumber() { + f := destPortRangeAttr.AsNumber() + rule.DestinationPorts = append(rule.DestinationPorts, common.PortRange{ + Metadata: destPortRangeAttr.GetMetadata(), + Start: iacTypes.Int(int(f), destPortRangeAttr.GetMetadata()), + End: iacTypes.Int(int(f), destPortRangeAttr.GetMetadata()), + }) + } +} diff --git a/pkg/iac/adapters/terraform/azure/securitycenter/adapt.go b/pkg/iac/adapters/terraform/azure/securitycenter/adapt.go new file mode 100644 index 000000000000..be239ddcf937 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/securitycenter/adapt.go @@ -0,0 +1,71 @@ +package securitycenter + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/securitycenter" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) securitycenter.SecurityCenter { + return securitycenter.SecurityCenter{ + Contacts: adaptContacts(modules), + Subscriptions: adaptSubscriptions(modules), + } +} + +func adaptContacts(modules terraform.Modules) []securitycenter.Contact { + var contacts []securitycenter.Contact + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_security_center_contact") { + contacts = append(contacts, adaptContact(resource)) + } + } + return contacts +} + +func adaptSubscriptions(modules terraform.Modules) []securitycenter.SubscriptionPricing { + var subscriptions []securitycenter.SubscriptionPricing + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_security_center_subscription_pricing") { + subscriptions = append(subscriptions, adaptSubscription(resource)) + } + } + return subscriptions +} + +func adaptContact(resource *terraform.Block) securitycenter.Contact { + enableAlertNotifAttr := resource.GetAttribute("alert_notifications") + enableAlertNotifVal := enableAlertNotifAttr.AsBoolValueOrDefault(false, resource) + + // TODO: add support for the new format https://github.com/hashicorp/terraform-provider-azurerm/issues/30797 + alertsToAdminsAttr := resource.GetAttribute("alerts_to_admins") + alertsToAdminsVal := alertsToAdminsAttr.AsBoolValueOrDefault(false, resource) + + emailAttr := resource.GetAttribute("email") + emailVal := emailAttr.AsStringValueOrDefault("", resource) + + phoneAttr := resource.GetAttribute("phone") + phoneVal := phoneAttr.AsStringValueOrDefault("", resource) + + return securitycenter.Contact{ + Metadata: resource.GetMetadata(), + EnableAlertNotifications: enableAlertNotifVal, + EnableAlertsToAdmins: alertsToAdminsVal, + Email: emailVal, + Phone: phoneVal, + IsEnabled: iacTypes.BoolValue{}, // Not supported in Terraform provider yet + MinimalSeverity: iacTypes.StringValue{}, // Not supported in Terraform provider yet + } +} + +func adaptSubscription(resource *terraform.Block) securitycenter.SubscriptionPricing { + tierAttr := resource.GetAttribute("tier") + tierVal := tierAttr.AsStringValueOrDefault("Free", resource) + + return securitycenter.SubscriptionPricing{ + Metadata: resource.GetMetadata(), + Tier: tierVal, + } +} diff --git a/pkg/iac/adapters/terraform/azure/securitycenter/adapt_test.go b/pkg/iac/adapters/terraform/azure/securitycenter/adapt_test.go new file mode 100644 index 000000000000..9244bea9ac70 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/securitycenter/adapt_test.go @@ -0,0 +1,144 @@ +package securitycenter + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/securitycenter" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptContact(t *testing.T) { + tests := []struct { + name string + terraform string + expected securitycenter.Contact + }{ + { + name: "defined", + terraform: ` + resource "azurerm_security_center_contact" "example" { + email = "contact@example.com" + phone = "+1-555-555-5555" + alert_notifications = true + alerts_to_admins = true + } +`, + expected: securitycenter.Contact{ + EnableAlertNotifications: iacTypes.BoolTest(true), + EnableAlertsToAdmins: iacTypes.BoolTest(true), + Email: iacTypes.StringTest("contact@example.com"), + Phone: iacTypes.StringTest("+1-555-555-5555"), + IsEnabled: iacTypes.BoolValue{}, + MinimalSeverity: iacTypes.StringValue{}, + }, + }, + { + name: "defaults", + terraform: ` + resource "azurerm_security_center_contact" "example" { + } +`, + expected: securitycenter.Contact{ + IsEnabled: iacTypes.BoolValue{}, + MinimalSeverity: iacTypes.StringValue{}, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptContact(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptSubscription(t *testing.T) { + tests := []struct { + name string + terraform string + expected securitycenter.SubscriptionPricing + }{ + { + name: "free tier", + terraform: ` + resource "azurerm_security_center_subscription_pricing" "example" { + tier = "Free" + }`, + expected: securitycenter.SubscriptionPricing{ + Tier: iacTypes.StringTest("Free"), + }, + }, + { + name: "default - free tier", + terraform: ` + resource "azurerm_security_center_subscription_pricing" "example" { + }`, + expected: securitycenter.SubscriptionPricing{ + Tier: iacTypes.StringTest("Free"), + }, + }, + { + name: "standard tier", + terraform: ` + resource "azurerm_security_center_subscription_pricing" "example" { + tier = "Standard" + }`, + expected: securitycenter.SubscriptionPricing{ + Tier: iacTypes.StringTest("Standard"), + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptSubscription(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_security_center_contact" "example" { + email = "contact@example.com" + phone = "+1-555-555-5555" + alert_notifications = true + alerts_to_admins = true + } + + resource "azurerm_security_center_subscription_pricing" "example" { + tier = "Standard" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Contacts, 1) + require.Len(t, adapted.Subscriptions, 1) + + contact := adapted.Contacts[0] + sub := adapted.Subscriptions[0] + + assert.Equal(t, 3, contact.Email.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, contact.Email.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, contact.Phone.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, contact.Phone.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 5, contact.EnableAlertNotifications.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, contact.EnableAlertNotifications.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, contact.EnableAlertsToAdmins.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, contact.EnableAlertsToAdmins.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 10, sub.Tier.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, sub.Tier.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/azure/storage/adapt.go b/pkg/iac/adapters/terraform/azure/storage/adapt.go new file mode 100644 index 000000000000..aa3e790cb965 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/storage/adapt.go @@ -0,0 +1,290 @@ +package storage + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/storage" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +const minimumTlsVersionOneTwo = "TLS1_2" + +func Adapt(modules terraform.Modules) storage.Storage { + accounts, containers, networkRules := adaptAccounts(modules) + + orphanAccount := storage.Account{ + Metadata: iacTypes.NewUnmanagedMetadata(), + NetworkRules: adaptOrphanNetworkRules(modules, networkRules), + EnforceHTTPS: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + Containers: adaptOrphanContainers(modules, containers), + QueueProperties: storage.QueueProperties{ + Metadata: iacTypes.NewUnmanagedMetadata(), + EnableLogging: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + }, + MinimumTLSVersion: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + BlobProperties: storage.BlobProperties{ + Metadata: iacTypes.NewUnmanagedMetadata(), + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Days: iacTypes.IntDefault(7, iacTypes.NewUnmanagedMetadata()), + }, + }, + AccountReplicationType: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + InfrastructureEncryptionEnabled: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + CustomerManagedKey: storage.CustomerManagedKey{ + Metadata: iacTypes.NewUnmanagedMetadata(), + KeyVaultKeyId: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + UserAssignedIdentityId: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + } + + accounts = append(accounts, orphanAccount) + + return storage.Storage{ + Accounts: accounts, + } +} + +func adaptOrphanContainers(modules terraform.Modules, containers []string) (orphans []storage.Container) { + accountedFor := make(map[string]bool) + for _, container := range containers { + accountedFor[container] = true + } + for _, module := range modules { + for _, containerResource := range module.GetResourcesByType("azurerm_storage_container") { + if _, ok := accountedFor[containerResource.ID()]; ok { + continue + } + orphans = append(orphans, adaptContainer(containerResource)) + } + } + + return orphans +} + +func adaptOrphanNetworkRules(modules terraform.Modules, networkRules []string) (orphans []storage.NetworkRule) { + accountedFor := make(map[string]bool) + for _, networkRule := range networkRules { + accountedFor[networkRule] = true + } + + for _, module := range modules { + for _, networkRuleResource := range module.GetResourcesByType("azurerm_storage_account_network_rules") { + if _, ok := accountedFor[networkRuleResource.ID()]; ok { + continue + } + + orphans = append(orphans, adaptNetworkRule(networkRuleResource)) + } + } + + return orphans +} + +func adaptAccounts(modules terraform.Modules) ([]storage.Account, []string, []string) { + var accounts []storage.Account + var accountedForContainers []string + var accountedForNetworkRules []string + + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_storage_account") { + account := adaptAccount(resource) + containerResource := module.GetReferencingResources(resource, "azurerm_storage_container", "storage_account_id") + + if len(containerResource) == 0 { + // If no referencing container resources are found, check for any containers that reference the account by Name instead of ID (older versions of the provider did this) + containerResource = module.GetReferencingResources(resource, "azurerm_storage_container", "storage_account_name") + } + for _, containerBlock := range containerResource { + accountedForContainers = append(accountedForContainers, containerBlock.ID()) + account.Containers = append(account.Containers, adaptContainer(containerBlock)) + } + networkRulesResource := module.GetReferencingResources(resource, "azurerm_storage_account_network_rules", "storage_account_id") + + if len(networkRulesResource) == 0 { + // If no referencing network rules resources are found, check for any that reference the account by Name instead of ID (older versions of the provider did this) + networkRulesResource = module.GetReferencingResources(resource, "azurerm_storage_account_network_rules", "storage_account_name") + } + for _, networkRuleBlock := range networkRulesResource { + accountedForNetworkRules = append(accountedForNetworkRules, networkRuleBlock.ID()) + account.NetworkRules = append(account.NetworkRules, adaptNetworkRule(networkRuleBlock)) + } + + queueResource := module.GetReferencingResources(resource, "azurerm_storage_queue", "storage_account_id") + + if len(queueResource) == 0 { + // If no referencing queue resources are found, check for any that reference the account by Name instead of ID (older versions of the provider did this) + queueResource = module.GetReferencingResources(resource, "azurerm_storage_queue", "storage_account_name") + } + + for _, queueBlock := range queueResource { + queue := storage.Queue{ + Metadata: queueBlock.GetMetadata(), + Name: queueBlock.GetAttribute("name").AsStringValueOrDefault("", queueBlock), + } + account.Queues = append(account.Queues, queue) + } + // Adapt customer managed key resource + // Only use the resource if the block wasn't already set (they are mutually exclusive in Terraform) + if account.CustomerManagedKey.KeyVaultKeyId.IsEmpty() { + customerManagedKeyResources := module.GetReferencingResources(resource, "azurerm_storage_account_customer_managed_key", "storage_account_id") + for _, cmkResource := range customerManagedKeyResources { + keyVaultKeyIdAttr := cmkResource.GetAttribute("key_vault_key_id") + if keyVaultKeyIdAttr.IsNotNil() { + account.CustomerManagedKey.KeyVaultKeyId = keyVaultKeyIdAttr.AsStringValueOrDefault("", cmkResource) + account.CustomerManagedKey.Metadata = cmkResource.GetMetadata() + } else { + // If key_vault_key_id is not directly set, try to construct from key_vault_id and key_name + keyVaultIdAttr := cmkResource.GetAttribute("key_vault_id") + keyNameAttr := cmkResource.GetAttribute("key_name") + if keyVaultIdAttr.IsNotNil() && keyNameAttr.IsNotNil() { + keyVaultId := keyVaultIdAttr.AsStringValueOrDefault("", cmkResource) + keyName := keyNameAttr.AsStringValueOrDefault("", cmkResource) + if !keyVaultId.IsEmpty() && !keyName.IsEmpty() { + // Construct the full key ID format: https://{keyVaultId}/keys/{keyName} + keyId := keyVaultId.Value() + "/keys/" + keyName.Value() + account.CustomerManagedKey.KeyVaultKeyId = iacTypes.String(keyId, cmkResource.GetMetadata()) + account.CustomerManagedKey.Metadata = cmkResource.GetMetadata() + } + } + } + userAssignedIdentityIdAttr := cmkResource.GetAttribute("user_assigned_identity_id") + if userAssignedIdentityIdAttr.IsNotNil() { + account.CustomerManagedKey.UserAssignedIdentityId = userAssignedIdentityIdAttr.AsStringValueOrDefault("", cmkResource) + } + break // Only process the first matching resource + } + } + accounts = append(accounts, account) + } + } + + return accounts, accountedForContainers, accountedForNetworkRules +} + +func adaptAccount(resource *terraform.Block) storage.Account { + account := storage.Account{ + Metadata: resource.GetMetadata(), + NetworkRules: nil, + EnforceHTTPS: iacTypes.BoolDefault(true, resource.GetMetadata()), + Containers: nil, + QueueProperties: storage.QueueProperties{ + Metadata: resource.GetMetadata(), + EnableLogging: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + MinimumTLSVersion: iacTypes.StringDefault(minimumTlsVersionOneTwo, resource.GetMetadata()), + PublicNetworkAccess: resource.GetAttribute("public_network_access_enabled").AsBoolValueOrDefault(true, resource), + BlobProperties: storage.BlobProperties{ + Metadata: resource.GetMetadata(), + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Metadata: resource.GetMetadata(), + Days: iacTypes.IntDefault(7, resource.GetMetadata()), + }, + }, + AccountReplicationType: resource.GetAttribute("account_replication_type").AsStringValueOrDefault("", resource), + InfrastructureEncryptionEnabled: resource.GetAttribute("infrastructure_encryption_enabled").AsBoolValueOrDefault(false, resource), + CustomerManagedKey: storage.CustomerManagedKey{ + Metadata: resource.GetMetadata(), + KeyVaultKeyId: iacTypes.StringDefault("", resource.GetMetadata()), + UserAssignedIdentityId: iacTypes.StringDefault("", resource.GetMetadata()), + }, + } + + networkRulesBlocks := resource.GetBlocks("network_rules") + for _, networkBlock := range networkRulesBlocks { + account.NetworkRules = append(account.NetworkRules, adaptNetworkRule(networkBlock)) + } + + account.EnforceHTTPS = resource.GetFirstAttributeOf( + "enable_https_traffic_only", + "https_traffic_only_enabled", // provider above version 4 + ).AsBoolValueOrDefault(true, resource) + + // Adapt blob properties + blobPropertiesBlock := resource.GetBlock("blob_properties") + if blobPropertiesBlock.IsNotNil() { + account.BlobProperties.Metadata = blobPropertiesBlock.GetMetadata() + deleteRetentionPolicyBlock := blobPropertiesBlock.GetBlock("delete_retention_policy") + if deleteRetentionPolicyBlock.IsNotNil() { + account.BlobProperties.DeleteRetentionPolicy.Metadata = deleteRetentionPolicyBlock.GetMetadata() + daysAttr := deleteRetentionPolicyBlock.GetAttribute("days") + if daysAttr.IsNotNil() { + account.BlobProperties.DeleteRetentionPolicy.Days = daysAttr.AsIntValueOrDefault(7, deleteRetentionPolicyBlock) + } + } + } + + // Adapt customer managed key + customerManagedKeyBlock := resource.GetBlock("customer_managed_key") + if customerManagedKeyBlock.IsNotNil() { + account.CustomerManagedKey.Metadata = customerManagedKeyBlock.GetMetadata() + keyVaultKeyIdAttr := customerManagedKeyBlock.GetAttribute("key_vault_key_id") + if keyVaultKeyIdAttr.IsNotNil() { + account.CustomerManagedKey.KeyVaultKeyId = keyVaultKeyIdAttr.AsStringValueOrDefault("", customerManagedKeyBlock) + } + userAssignedIdentityIdAttr := customerManagedKeyBlock.GetAttribute("user_assigned_identity_id") + if userAssignedIdentityIdAttr.IsNotNil() { + account.CustomerManagedKey.UserAssignedIdentityId = userAssignedIdentityIdAttr.AsStringValueOrDefault("", customerManagedKeyBlock) + } + } + + // Adapt queue properties + queuePropertiesBlock := resource.GetBlock("queue_properties") + if queuePropertiesBlock.IsNotNil() { + account.QueueProperties.Metadata = queuePropertiesBlock.GetMetadata() + loggingBlock := queuePropertiesBlock.GetBlock("logging") + if loggingBlock.IsNotNil() { + account.QueueProperties.EnableLogging = iacTypes.Bool(true, loggingBlock.GetMetadata()) + account.QueueProperties.Logging = storage.QueueLogging{ + Metadata: loggingBlock.GetMetadata(), + Delete: loggingBlock.GetAttribute("delete").AsBoolValueOrDefault(false, loggingBlock), + Read: loggingBlock.GetAttribute("read").AsBoolValueOrDefault(false, loggingBlock), + Write: loggingBlock.GetAttribute("write").AsBoolValueOrDefault(false, loggingBlock), + Version: loggingBlock.GetAttribute("version").AsStringValueOrDefault("", loggingBlock), + RetentionPolicyDays: loggingBlock.GetAttribute("retention_policy_days").AsIntValueOrDefault(0, loggingBlock), + } + } + } + + minTLSVersionAttr := resource.GetAttribute("min_tls_version") + account.MinimumTLSVersion = minTLSVersionAttr.AsStringValueOrDefault(minimumTlsVersionOneTwo, resource) + return account +} + +func adaptContainer(resource *terraform.Block) storage.Container { + accessTypeAttr := resource.GetAttribute("container_access_type") + publicAccess := iacTypes.StringDefault(storage.PublicAccessOff, resource.GetMetadata()) + + if accessTypeAttr.Equals("blob") { + publicAccess = iacTypes.String(storage.PublicAccessBlob, accessTypeAttr.GetMetadata()) + } else if accessTypeAttr.Equals("container") { + publicAccess = iacTypes.String(storage.PublicAccessContainer, accessTypeAttr.GetMetadata()) + } + + return storage.Container{ + Metadata: resource.GetMetadata(), + PublicAccess: publicAccess, + } +} + +func adaptNetworkRule(resource *terraform.Block) storage.NetworkRule { + var allowByDefault iacTypes.BoolValue + var bypass []iacTypes.StringValue + + defaultActionAttr := resource.GetAttribute("default_action") + + if defaultActionAttr.IsNotNil() { + allowByDefault = iacTypes.Bool(defaultActionAttr.Equals("Allow", terraform.IgnoreCase), defaultActionAttr.GetMetadata()) + } else { + allowByDefault = iacTypes.BoolDefault(false, resource.GetMetadata()) + } + + if bypassAttr := resource.GetAttribute("bypass"); bypassAttr.IsNotNil() { + bypass = bypassAttr.AsStringValues() + } + + return storage.NetworkRule{ + Metadata: resource.GetMetadata(), + Bypass: bypass, + AllowByDefault: allowByDefault, + } +} diff --git a/pkg/iac/adapters/terraform/azure/storage/adapt_test.go b/pkg/iac/adapters/terraform/azure/storage/adapt_test.go new file mode 100644 index 000000000000..8e110ad6a329 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/storage/adapt_test.go @@ -0,0 +1,370 @@ +package storage + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/storage" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected storage.Storage + }{ + { + name: "default", + terraform: `resource "azurerm_storage_account" "example" {}`, + expected: storage.Storage{ + Accounts: []storage.Account{ + { + PublicNetworkAccess: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest(minimumTlsVersionOneTwo), + EnforceHTTPS: iacTypes.BoolTest(true), + BlobProperties: storage.BlobProperties{ + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Days: iacTypes.IntTest(7), + }, + }, + CustomerManagedKey: storage.CustomerManagedKey{}, + }, + { + BlobProperties: storage.BlobProperties{ + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Days: iacTypes.IntTest(7), + }, + }, + }, + }, + }, + }, + { + name: "defined", + terraform: ` + resource "azurerm_resource_group" "example" { + name = "example" + } + + resource "azurerm_storage_account" "example" { + name = "storageaccountname" + resource_group_name = azurerm_resource_group.example.name + + network_rules { + default_action = "Deny" + bypass = ["Metrics", "AzureServices"] + } + + enable_https_traffic_only = true + queue_properties { + logging { + delete = true + read = true + write = true + version = "1.0" + retention_policy_days = 10 + } + } + min_tls_version = "TLS1_2" + public_network_access_enabled = false + } + + resource "azurerm_storage_account_network_rules" "test" { + resource_group_name = azurerm_resource_group.example.name + storage_account_name = azurerm_storage_account.example.name + + default_action = "Allow" + bypass = ["Metrics"] + } + + resource "azurerm_storage_container" "example" { + storage_account_name = azurerm_storage_account.example.name + resource_group_name = azurerm_resource_group.example.name + container_access_type = "blob" + } +`, + expected: storage.Storage{ + Accounts: []storage.Account{ + + { + EnforceHTTPS: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest("TLS1_2"), + NetworkRules: []storage.NetworkRule{ + { + Bypass: []iacTypes.StringValue{ + iacTypes.StringTest("Metrics"), + iacTypes.StringTest("AzureServices"), + }, + }, + { + Bypass: []iacTypes.StringValue{ + iacTypes.StringTest("Metrics"), + }, + AllowByDefault: iacTypes.BoolTest(true), + }, + }, + QueueProperties: storage.QueueProperties{ + EnableLogging: iacTypes.BoolTest(true), + Logging: storage.QueueLogging{ + Delete: iacTypes.BoolTest(true), + Read: iacTypes.BoolTest(true), + Write: iacTypes.BoolTest(true), + Version: iacTypes.StringTest("1.0"), + RetentionPolicyDays: iacTypes.IntTest(10), + }, + }, + BlobProperties: storage.BlobProperties{ + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Days: iacTypes.IntTest(7), + }, + }, + CustomerManagedKey: storage.CustomerManagedKey{}, + Containers: []storage.Container{ + { + PublicAccess: iacTypes.StringTest("blob"), + }, + }, + }, + { + Metadata: iacTypes.NewUnmanagedMetadata(), + EnforceHTTPS: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + QueueProperties: storage.QueueProperties{ + Metadata: iacTypes.NewUnmanagedMetadata(), + EnableLogging: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + }, + MinimumTLSVersion: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + AccountReplicationType: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + InfrastructureEncryptionEnabled: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + BlobProperties: storage.BlobProperties{ + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Days: iacTypes.IntDefault(7, iacTypes.NewUnmanagedMetadata()), + }, + }, + CustomerManagedKey: storage.CustomerManagedKey{ + KeyVaultKeyId: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + UserAssignedIdentityId: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + }, + }, + }, + }, + { + name: "references via storage_account_id", + terraform: ` + resource "azurerm_resource_group" "example" { + name = "example" + } + + resource "azurerm_storage_account" "example" { + name = "storageaccountname" + resource_group_name = azurerm_resource_group.example.name + } + + resource "azurerm_storage_account_network_rules" "example" { + storage_account_id = azurerm_storage_account.example.id + default_action = "Deny" + } + + resource "azurerm_storage_container" "example" { + storage_account_id = azurerm_storage_account.example.id + container_access_type = "blob" + } + + resource "azurerm_storage_queue" "example" { + storage_account_id = azurerm_storage_account.example.id + name = "queue1" + } +`, + expected: storage.Storage{ + Accounts: []storage.Account{ + { + EnforceHTTPS: iacTypes.BoolTest(true), + MinimumTLSVersion: iacTypes.StringTest(minimumTlsVersionOneTwo), + PublicNetworkAccess: iacTypes.BoolTest(true), + BlobProperties: storage.BlobProperties{ + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Days: iacTypes.IntTest(7), + }, + }, + NetworkRules: []storage.NetworkRule{ + { + AllowByDefault: iacTypes.BoolTest(false), + }, + }, + Containers: []storage.Container{ + { + PublicAccess: iacTypes.StringTest("blob"), + }, + }, + Queues: []storage.Queue{ + { + Name: iacTypes.StringTest("queue1"), + }, + }, + }, + // orphan account holder + { + BlobProperties: storage.BlobProperties{ + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Days: iacTypes.IntTest(7), + }, + }, + }, + }, + }, + }, + { + name: "orphans", + terraform: ` + resource "azurerm_storage_account_network_rules" "test" { + default_action = "Allow" + bypass = ["Metrics"] + } + + resource "azurerm_storage_container" "example" { + container_access_type = "blob" + } +`, + expected: storage.Storage{ + Accounts: []storage.Account{ + { + Metadata: iacTypes.NewUnmanagedMetadata(), + EnforceHTTPS: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + NetworkRules: []storage.NetworkRule{ + { + Bypass: []iacTypes.StringValue{ + iacTypes.StringTest("Metrics"), + }, + AllowByDefault: iacTypes.BoolTest(true), + }, + }, + QueueProperties: storage.QueueProperties{ + Metadata: iacTypes.NewUnmanagedMetadata(), + EnableLogging: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + }, + MinimumTLSVersion: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + AccountReplicationType: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + InfrastructureEncryptionEnabled: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + BlobProperties: storage.BlobProperties{ + DeleteRetentionPolicy: storage.DeleteRetentionPolicy{ + Days: iacTypes.IntDefault(7, iacTypes.NewUnmanagedMetadata()), + }, + }, + CustomerManagedKey: storage.CustomerManagedKey{ + KeyVaultKeyId: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + UserAssignedIdentityId: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + Containers: []storage.Container{ + { + PublicAccess: iacTypes.StringTest("blob"), + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_resource_group" "example" { + name = "example" + location = "West Europe" + } + + resource "azurerm_storage_account" "example" { + resource_group_name = azurerm_resource_group.example.name + + enable_https_traffic_only = true + min_tls_version = "TLS1_2" + + queue_properties { + logging { + delete = true + read = true + write = true + version = "1.0" + retention_policy_days = 10 + } + } + + network_rules { + default_action = "Deny" + bypass = ["Metrics", "AzureServices"] + } + } + + resource "azurerm_storage_account_network_rules" "test" { + resource_group_name = azurerm_resource_group.example.name + storage_account_name = azurerm_storage_account.example.name + + default_action = "Allow" + bypass = ["Metrics"] + } + + resource "azurerm_storage_container" "example" { + storage_account_name = azurerm_storage_account.example.name + resource_group_name = azurerm_resource_group.example.name + container_access_type = "blob" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Accounts, 2) //+orphans holder + account := adapted.Accounts[0] + + assert.Equal(t, 7, account.Metadata.Range().GetStartLine()) + assert.Equal(t, 27, account.Metadata.Range().GetEndLine()) + + assert.Equal(t, 10, account.EnforceHTTPS.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, account.EnforceHTTPS.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, account.MinimumTLSVersion.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, account.MinimumTLSVersion.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, account.QueueProperties.Metadata.Range().GetStartLine()) + assert.Equal(t, 21, account.QueueProperties.Metadata.Range().GetEndLine()) + + assert.Equal(t, 14, account.QueueProperties.EnableLogging.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 20, account.QueueProperties.EnableLogging.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 23, account.NetworkRules[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 26, account.NetworkRules[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 24, account.NetworkRules[0].AllowByDefault.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 24, account.NetworkRules[0].AllowByDefault.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 25, account.NetworkRules[0].Bypass[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 25, account.NetworkRules[0].Bypass[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 29, account.NetworkRules[1].Metadata.Range().GetStartLine()) + assert.Equal(t, 35, account.NetworkRules[1].Metadata.Range().GetEndLine()) + + assert.Equal(t, 33, account.NetworkRules[1].AllowByDefault.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 33, account.NetworkRules[1].AllowByDefault.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 34, account.NetworkRules[1].Bypass[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 34, account.NetworkRules[1].Bypass[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 37, account.Containers[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 41, account.Containers[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 40, account.Containers[0].PublicAccess.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 40, account.Containers[0].PublicAccess.GetMetadata().Range().GetEndLine()) + +} diff --git a/pkg/iac/adapters/terraform/azure/synapse/adapt.go b/pkg/iac/adapters/terraform/azure/synapse/adapt.go new file mode 100644 index 000000000000..37efcb596a9d --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/synapse/adapt.go @@ -0,0 +1,32 @@ +package synapse + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/synapse" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) synapse.Synapse { + return synapse.Synapse{ + Workspaces: adaptWorkspaces(modules), + } +} + +func adaptWorkspaces(modules terraform.Modules) []synapse.Workspace { + var workspaces []synapse.Workspace + for _, module := range modules { + for _, resource := range module.GetResourcesByType("azurerm_synapse_workspace") { + workspaces = append(workspaces, adaptWorkspace(resource)) + } + } + return workspaces +} + +func adaptWorkspace(resource *terraform.Block) synapse.Workspace { + enableManagedVNAttr := resource.GetAttribute("managed_virtual_network_enabled") + enableManagedVNVal := enableManagedVNAttr.AsBoolValueOrDefault(false, resource) + + return synapse.Workspace{ + Metadata: resource.GetMetadata(), + EnableManagedVirtualNetwork: enableManagedVNVal, + } +} diff --git a/pkg/iac/adapters/terraform/azure/synapse/adapt_test.go b/pkg/iac/adapters/terraform/azure/synapse/adapt_test.go new file mode 100644 index 000000000000..17291a4ebb90 --- /dev/null +++ b/pkg/iac/adapters/terraform/azure/synapse/adapt_test.go @@ -0,0 +1,74 @@ +package synapse + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/synapse" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptWorkspace(t *testing.T) { + tests := []struct { + name string + terraform string + expected synapse.Workspace + }{ + { + name: "enabled", + terraform: ` + resource "azurerm_synapse_workspace" "example" { + managed_virtual_network_enabled = true + } +`, + expected: synapse.Workspace{ + EnableManagedVirtualNetwork: iacTypes.BoolTest(true), + }, + }, + { + name: "disabled", + terraform: ` + resource "azurerm_synapse_workspace" "example" { + managed_virtual_network_enabled = false + } +`, + expected: synapse.Workspace{}, + }, + { + name: "default", + terraform: ` + resource "azurerm_synapse_workspace" "example" { + } +`, + expected: synapse.Workspace{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptWorkspace(modules.GetBlocks()[0]) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "azurerm_synapse_workspace" "example" { + managed_virtual_network_enabled = true + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Workspaces, 1) + workspace := adapted.Workspaces[0] + + assert.Equal(t, 3, workspace.EnableManagedVirtualNetwork.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, workspace.EnableManagedVirtualNetwork.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/cloudstack/adapt.go b/pkg/iac/adapters/terraform/cloudstack/adapt.go new file mode 100644 index 000000000000..d19a05bdf3a4 --- /dev/null +++ b/pkg/iac/adapters/terraform/cloudstack/adapt.go @@ -0,0 +1,13 @@ +package cloudstack + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/cloudstack/compute" + "github.com/aquasecurity/trivy/pkg/iac/providers/cloudstack" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) cloudstack.CloudStack { + return cloudstack.CloudStack{ + Compute: compute.Adapt(modules), + } +} diff --git a/pkg/iac/adapters/terraform/cloudstack/compute/adapt.go b/pkg/iac/adapters/terraform/cloudstack/compute/adapt.go new file mode 100644 index 000000000000..4908f0325158 --- /dev/null +++ b/pkg/iac/adapters/terraform/cloudstack/compute/adapt.go @@ -0,0 +1,47 @@ +package compute + +import ( + "encoding/base64" + + "github.com/aquasecurity/trivy/pkg/iac/providers/cloudstack/compute" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) compute.Compute { + return compute.Compute{ + Instances: adaptInstances(modules), + } +} + +func adaptInstances(modules terraform.Modules) []compute.Instance { + var instances []compute.Instance + for _, module := range modules { + for _, resource := range module.GetResourcesByType("cloudstack_instance") { + instances = append(instances, adaptInstance(resource)) + } + } + return instances +} + +func adaptInstance(resource *terraform.Block) compute.Instance { + userDataAttr := resource.GetAttribute("user_data") + var encoded []byte + var err error + + if userDataAttr.IsNotNil() && userDataAttr.IsString() { + encoded, err = base64.StdEncoding.DecodeString(userDataAttr.Value().AsString()) + if err != nil { + encoded = []byte(userDataAttr.Value().AsString()) + } + return compute.Instance{ + Metadata: resource.GetMetadata(), + UserData: types.String(string(encoded), userDataAttr.GetMetadata()), + } + } + + return compute.Instance{ + Metadata: resource.GetMetadata(), + UserData: types.StringDefault("", resource.GetMetadata()), + } +} diff --git a/pkg/iac/adapters/terraform/cloudstack/compute/adapt_test.go b/pkg/iac/adapters/terraform/cloudstack/compute/adapt_test.go new file mode 100644 index 000000000000..022997cbc0a8 --- /dev/null +++ b/pkg/iac/adapters/terraform/cloudstack/compute/adapt_test.go @@ -0,0 +1,84 @@ +package compute + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/cloudstack/compute" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptInstance(t *testing.T) { + tests := []struct { + name string + terraform string + expected compute.Instance + }{ + { + name: "sensitive user data", + terraform: ` + resource "cloudstack_instance" "web" { + name = "server-1" + user_data = < 0 { + cluster.NodeConfig = cluster.NodePools[0].NodeConfig + a.clusterMap[id] = cluster + } + } + + var clusters []gke.Cluster + for _, cluster := range a.clusterMap { + clusters = append(clusters, cluster) + } + return clusters +} + +func (a *adapter) adaptCluster(resource *terraform.Block) { + + cluster := gke.Cluster{ + Metadata: resource.GetMetadata(), + NodePools: nil, + IPAllocationPolicy: gke.IPAllocationPolicy{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + MasterAuthorizedNetworks: gke.MasterAuthorizedNetworks{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + CIDRs: []iacTypes.StringValue{}, + }, + NetworkPolicy: gke.NetworkPolicy{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + DatapathProvider: resource.GetAttribute("datapath_provider"). + AsStringValueOrDefault("DATAPATH_PROVIDER_UNSPECIFIED", resource), + PrivateCluster: gke.PrivateCluster{ + Metadata: resource.GetMetadata(), + EnablePrivateNodes: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + LoggingService: iacTypes.StringDefault("logging.googleapis.com/kubernetes", resource.GetMetadata()), + MonitoringService: iacTypes.StringDefault("monitoring.googleapis.com/kubernetes", resource.GetMetadata()), + MasterAuth: gke.MasterAuth{ + Metadata: resource.GetMetadata(), + ClientCertificate: gke.ClientCertificate{ + Metadata: resource.GetMetadata(), + IssueCertificate: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + Username: iacTypes.StringDefault("", resource.GetMetadata()), + Password: iacTypes.StringDefault("", resource.GetMetadata()), + }, + NodeConfig: gke.NodeConfig{ + Metadata: resource.GetMetadata(), + ImageType: iacTypes.StringDefault("", resource.GetMetadata()), + WorkloadMetadataConfig: gke.WorkloadMetadataConfig{ + Metadata: resource.GetMetadata(), + NodeMetadata: iacTypes.StringDefault("", resource.GetMetadata()), + }, + ServiceAccount: iacTypes.StringDefault("", resource.GetMetadata()), + EnableLegacyEndpoints: iacTypes.BoolDefault(true, resource.GetMetadata()), + }, + EnableShieldedNodes: iacTypes.BoolDefault(true, resource.GetMetadata()), + EnableLegacyABAC: iacTypes.BoolDefault(false, resource.GetMetadata()), + ResourceLabels: iacTypes.MapDefault(make(map[string]string), resource.GetMetadata()), + RemoveDefaultNodePool: iacTypes.BoolDefault(false, resource.GetMetadata()), + EnableAutpilot: iacTypes.BoolDefault(false, resource.GetMetadata()), + } + + if allocBlock := resource.GetBlock("ip_allocation_policy"); allocBlock.IsNotNil() { + cluster.IPAllocationPolicy.Metadata = allocBlock.GetMetadata() + cluster.IPAllocationPolicy.Enabled = iacTypes.Bool(true, allocBlock.GetMetadata()) + } + + if blocks := resource.GetBlocks("master_authorized_networks_config"); len(blocks) > 0 { + cluster.MasterAuthorizedNetworks = adaptMasterAuthNetworksAsBlocks(blocks) + } + + if policyBlock := resource.GetBlock("network_policy"); policyBlock.IsNotNil() { + enabledAttr := policyBlock.GetAttribute("enabled") + cluster.NetworkPolicy.Metadata = policyBlock.GetMetadata() + cluster.NetworkPolicy.Enabled = enabledAttr.AsBoolValueOrDefault(false, policyBlock) + } + + if privBlock := resource.GetBlock("private_cluster_config"); privBlock.IsNotNil() { + privateNodesEnabledAttr := privBlock.GetAttribute("enable_private_nodes") + cluster.PrivateCluster.Metadata = privBlock.GetMetadata() + cluster.PrivateCluster.EnablePrivateNodes = privateNodesEnabledAttr.AsBoolValueOrDefault(false, privBlock) + } + + loggingAttr := resource.GetAttribute("logging_service") + cluster.LoggingService = loggingAttr.AsStringValueOrDefault("logging.googleapis.com/kubernetes", resource) + monitoringServiceAttr := resource.GetAttribute("monitoring_service") + cluster.MonitoringService = monitoringServiceAttr.AsStringValueOrDefault("monitoring.googleapis.com/kubernetes", resource) + + if masterBlock := resource.GetBlock("master_auth"); masterBlock.IsNotNil() { + cluster.MasterAuth = adaptMasterAuth(masterBlock) + } + + if configBlock := resource.GetBlock("node_config"); configBlock.IsNotNil() { + cluster.NodeConfig = adaptNodeConfig(configBlock) + } + + if autoScalingBlock := resource.GetBlock("cluster_autoscaling"); autoScalingBlock.IsNotNil() { + cluster.AutoScaling = gke.AutoScaling{ + Metadata: autoScalingBlock.GetMetadata(), + Enabled: autoScalingBlock.GetAttribute("enabled").AsBoolValueOrDefault(false, autoScalingBlock), + } + + if b := autoScalingBlock.GetBlock("auto_provisioning_defaults"); b.IsNotNil() { + cluster.AutoScaling.AutoProvisioningDefaults = gke.AutoProvisioningDefaults{ + Metadata: b.GetMetadata(), + ServiceAccount: b.GetAttribute("service_account").AsStringValueOrDefault("", b), + Management: adaptManagement(b), + ImageType: b.GetAttribute("image_type").AsStringValueOrDefault("", b), + } + } + } + cluster.EnableShieldedNodes = resource.GetAttribute("enable_shielded_nodes").AsBoolValueOrDefault(true, resource) + + enableLegacyABACAttr := resource.GetAttribute("enable_legacy_abac") + cluster.EnableLegacyABAC = enableLegacyABACAttr.AsBoolValueOrDefault(false, resource) + + cluster.EnableAutpilot = resource.GetAttribute("enable_autopilot").AsBoolValueOrDefault(false, resource) + + resourceLabelsAttr := resource.GetAttribute("resource_labels") + if resourceLabelsAttr.IsNotNil() { + cluster.ResourceLabels = resourceLabelsAttr.AsMapValue() + } + + cluster.RemoveDefaultNodePool = resource.GetAttribute("remove_default_node_pool").AsBoolValueOrDefault(false, resource) + + a.clusterMap[resource.ID()] = cluster +} + +func adaptManagement(parent *terraform.Block) gke.Management { + b := parent.GetBlock("management") + if b.IsNil() { + return gke.Management{ + Metadata: parent.GetMetadata(), + EnableAutoRepair: iacTypes.BoolDefault(false, parent.GetMetadata()), + EnableAutoUpgrade: iacTypes.BoolDefault(false, parent.GetMetadata()), + } + } + + return gke.Management{ + Metadata: b.GetMetadata(), + EnableAutoRepair: b.GetAttribute("auto_repair").AsBoolValueOrDefault(false, b), + EnableAutoUpgrade: b.GetAttribute("auto_upgrade").AsBoolValueOrDefault(false, b), + } +} + +func (a *adapter) adaptNodePools() { + for _, nodePoolBlock := range a.modules.GetResourcesByType("google_container_node_pool") { + a.adaptNodePool(nodePoolBlock) + } +} + +func (a *adapter) adaptNodePool(resource *terraform.Block) { + nodeConfig := gke.NodeConfig{ + Metadata: resource.GetMetadata(), + ImageType: iacTypes.StringDefault("", resource.GetMetadata()), + WorkloadMetadataConfig: gke.WorkloadMetadataConfig{ + Metadata: resource.GetMetadata(), + NodeMetadata: iacTypes.StringDefault("", resource.GetMetadata()), + }, + ServiceAccount: iacTypes.StringDefault("", resource.GetMetadata()), + EnableLegacyEndpoints: iacTypes.BoolDefault(true, resource.GetMetadata()), + } + + if nodeConfigBlock := resource.GetBlock("node_config"); nodeConfigBlock.IsNotNil() { + nodeConfig = adaptNodeConfig(nodeConfigBlock) + } + + nodePool := gke.NodePool{ + Metadata: resource.GetMetadata(), + Management: adaptManagement(resource), + NodeConfig: nodeConfig, + } + + clusterAttr := resource.GetAttribute("cluster") + if referencedCluster, err := a.modules.GetReferencedBlock(clusterAttr, resource); err == nil { + if referencedCluster.TypeLabel() == "google_container_cluster" { + if cluster, ok := a.clusterMap[referencedCluster.ID()]; ok { + cluster.NodePools = append(cluster.NodePools, nodePool) + a.clusterMap[referencedCluster.ID()] = cluster + return + } + } + } + + // we didn't find a cluster to put the nodepool in, so create a placeholder + a.clusterMap[uuid.NewString()] = gke.Cluster{ + Metadata: iacTypes.NewUnmanagedMetadata(), + NodePools: []gke.NodePool{nodePool}, + IPAllocationPolicy: gke.IPAllocationPolicy{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Enabled: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + }, + MasterAuthorizedNetworks: gke.MasterAuthorizedNetworks{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Enabled: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + CIDRs: nil, + }, + NetworkPolicy: gke.NetworkPolicy{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Enabled: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + }, + PrivateCluster: gke.PrivateCluster{ + Metadata: iacTypes.NewUnmanagedMetadata(), + EnablePrivateNodes: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + }, + LoggingService: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + MonitoringService: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + MasterAuth: gke.MasterAuth{ + Metadata: iacTypes.NewUnmanagedMetadata(), + ClientCertificate: gke.ClientCertificate{ + Metadata: iacTypes.NewUnmanagedMetadata(), + IssueCertificate: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + }, + Username: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + Password: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + NodeConfig: gke.NodeConfig{ + Metadata: iacTypes.NewUnmanagedMetadata(), + ImageType: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + WorkloadMetadataConfig: gke.WorkloadMetadataConfig{ + Metadata: iacTypes.NewUnmanagedMetadata(), + NodeMetadata: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + ServiceAccount: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + EnableLegacyEndpoints: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + }, + EnableShieldedNodes: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + EnableLegacyABAC: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + ResourceLabels: iacTypes.MapDefault(nil, iacTypes.NewUnmanagedMetadata()), + RemoveDefaultNodePool: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + EnableAutpilot: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + } +} + +func adaptNodeConfig(resource *terraform.Block) gke.NodeConfig { + + config := gke.NodeConfig{ + Metadata: resource.GetMetadata(), + ImageType: resource.GetAttribute("image_type").AsStringValueOrDefault("", resource), + WorkloadMetadataConfig: gke.WorkloadMetadataConfig{ + Metadata: resource.GetMetadata(), + NodeMetadata: iacTypes.StringDefault("UNSPECIFIED", resource.GetMetadata()), + }, + ServiceAccount: resource.GetAttribute("service_account").AsStringValueOrDefault("", resource), + EnableLegacyEndpoints: iacTypes.BoolDefault(true, resource.GetMetadata()), + } + + if metadata := resource.GetAttribute("metadata"); metadata.IsNotNil() { + disableLegacy := metadata.MapValue("disable-legacy-endpoints") + if val, ok := iacTypes.BoolFromCtyValue(disableLegacy, metadata.GetMetadata()); ok { + config.EnableLegacyEndpoints = val.Invert() + } + } + + workloadBlock := resource.GetBlock("workload_metadata_config") + if workloadBlock.IsNotNil() { + config.WorkloadMetadataConfig.Metadata = workloadBlock.GetMetadata() + modeAttr := workloadBlock.GetAttribute("node_metadata") + if modeAttr.IsNil() { + modeAttr = workloadBlock.GetAttribute("mode") // try newest version + } + config.WorkloadMetadataConfig.NodeMetadata = modeAttr.AsStringValueOrDefault("UNSPECIFIED", workloadBlock) + } + + return config +} + +func adaptMasterAuth(resource *terraform.Block) gke.MasterAuth { + clientCert := gke.ClientCertificate{ + Metadata: resource.GetMetadata(), + IssueCertificate: iacTypes.BoolDefault(false, resource.GetMetadata()), + } + + if certConfigBlock := resource.GetBlock("client_certificate_config"); certConfigBlock.IsNotNil() { + clientCertAttr := certConfigBlock.GetAttribute("issue_client_certificate") + clientCert.IssueCertificate = clientCertAttr.AsBoolValueOrDefault(false, certConfigBlock) + clientCert.Metadata = certConfigBlock.GetMetadata() + } + + username := resource.GetAttribute("username").AsStringValueOrDefault("", resource) + password := resource.GetAttribute("password").AsStringValueOrDefault("", resource) + + return gke.MasterAuth{ + Metadata: resource.GetMetadata(), + ClientCertificate: clientCert, + Username: username, + Password: password, + } +} + +func adaptMasterAuthNetworksAsBlocks(blocks terraform.Blocks) gke.MasterAuthorizedNetworks { + var cidrs []iacTypes.StringValue + for _, block := range blocks { + for _, cidrBlock := range block.GetBlocks("cidr_blocks") { + if cidrAttr := cidrBlock.GetAttribute("cidr_block"); cidrAttr.IsNotNil() { + cidrs = append(cidrs, cidrAttr.AsStringValues()...) + } + } + } + enabled := iacTypes.Bool(true, blocks[0].GetMetadata()) + return gke.MasterAuthorizedNetworks{ + Metadata: blocks[0].GetMetadata(), + Enabled: enabled, + CIDRs: cidrs, + } +} diff --git a/pkg/iac/adapters/terraform/google/gke/adapt_test.go b/pkg/iac/adapters/terraform/google/gke/adapt_test.go new file mode 100644 index 000000000000..b6222d665d88 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/gke/adapt_test.go @@ -0,0 +1,396 @@ +package gke + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/gke" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected gke.GKE + }{ + { + name: "separately defined pool", + terraform: ` +resource "google_service_account" "default" { + account_id = "service-account-id" + display_name = "Service Account" +} + +resource "google_container_cluster" "example" { + name = "my-gke-cluster" + + node_config { + metadata = { + disable-legacy-endpoints = true + } + } + + pod_security_policy_config { + enabled = "true" + } + + enable_legacy_abac = "true" + enable_shielded_nodes = "true" + + remove_default_node_pool = true + initial_node_count = 1 + monitoring_service = "monitoring.googleapis.com/kubernetes" + logging_service = "logging.googleapis.com/kubernetes" + + master_auth { + client_certificate_config { + issue_client_certificate = true + } + } + + master_authorized_networks_config { + cidr_blocks { + cidr_block = "10.10.128.0/24" + display_name = "internal" + } + } + + resource_labels = { + "env" = "staging" + } + + private_cluster_config { + enable_private_nodes = true + } + + network_policy { + enabled = true + } + + ip_allocation_policy {} + + enable_autopilot = true + + datapath_provider = "ADVANCED_DATAPATH" + + cluster_autoscaling { + enabled = true + auto_provisioning_defaults { + service_account = "test" + image_type = "COS_CONTAINERD" + management { + auto_repair = true + auto_upgrade = true + } + } + } +} + +resource "google_container_node_pool" "primary_preemptible_nodes" { + cluster = google_container_cluster.example.name + node_count = 1 + + node_config { + service_account = google_service_account.default.email + metadata = { + disable-legacy-endpoints = true + } + image_type = "COS_CONTAINERD" + workload_metadata_config { + mode = "GCE_METADATA" + } + } + management { + auto_repair = true + auto_upgrade = true + } +} +`, + expected: gke.GKE{ + Clusters: []gke.Cluster{ + { + NodeConfig: gke.NodeConfig{ + ImageType: iacTypes.StringTest("COS_CONTAINERD"), + WorkloadMetadataConfig: gke.WorkloadMetadataConfig{ + NodeMetadata: iacTypes.StringTest("GCE_METADATA"), + }, + }, + AutoScaling: gke.AutoScaling{ + Enabled: iacTypes.BoolTest(true), + AutoProvisioningDefaults: gke.AutoProvisioningDefaults{ + ImageType: iacTypes.StringTest("COS_CONTAINERD"), + ServiceAccount: iacTypes.StringTest("test"), + Management: gke.Management{ + EnableAutoRepair: iacTypes.BoolTest(true), + EnableAutoUpgrade: iacTypes.BoolTest(true), + }, + }, + }, + NodePools: []gke.NodePool{ + { + Management: gke.Management{ + EnableAutoRepair: iacTypes.BoolTest(true), + EnableAutoUpgrade: iacTypes.BoolTest(true), + }, + NodeConfig: gke.NodeConfig{ + ImageType: iacTypes.StringTest("COS_CONTAINERD"), + WorkloadMetadataConfig: gke.WorkloadMetadataConfig{ + NodeMetadata: iacTypes.StringTest("GCE_METADATA"), + }, + }, + }, + }, + IPAllocationPolicy: gke.IPAllocationPolicy{ + Enabled: iacTypes.BoolTest(true), + }, + MasterAuthorizedNetworks: gke.MasterAuthorizedNetworks{ + Enabled: iacTypes.BoolTest(true), + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest("10.10.128.0/24"), + }, + }, + NetworkPolicy: gke.NetworkPolicy{ + Enabled: iacTypes.BoolTest(true), + }, + DatapathProvider: iacTypes.StringTest("ADVANCED_DATAPATH"), + PrivateCluster: gke.PrivateCluster{ + EnablePrivateNodes: iacTypes.BoolTest(true), + }, + LoggingService: iacTypes.StringTest("logging.googleapis.com/kubernetes"), + MonitoringService: iacTypes.StringTest("monitoring.googleapis.com/kubernetes"), + MasterAuth: gke.MasterAuth{ + ClientCertificate: gke.ClientCertificate{ + IssueCertificate: iacTypes.BoolTest(true), + }, + }, + EnableShieldedNodes: iacTypes.BoolTest(true), + EnableLegacyABAC: iacTypes.BoolTest(true), + ResourceLabels: iacTypes.MapTest(map[string]string{ + "env": "staging", + }), + RemoveDefaultNodePool: iacTypes.BoolTest(true), + EnableAutpilot: iacTypes.BoolTest(true), + }, + }, + }, + }, + { + name: "default node pool", + terraform: ` +resource "google_container_cluster" "example" { + node_config { + service_account = "service-account" + metadata = { + disable-legacy-endpoints = "true" + } + image_type = "COS" + workload_metadata_config { + mode = "GCE_METADATA" + } + } +} +`, + expected: gke.GKE{ + Clusters: []gke.Cluster{ + { + NodeConfig: gke.NodeConfig{ + ImageType: iacTypes.StringTest("COS"), + WorkloadMetadataConfig: gke.WorkloadMetadataConfig{ + NodeMetadata: iacTypes.StringTest("GCE_METADATA"), + }, + ServiceAccount: iacTypes.StringTest("service-account"), + }, + + IPAllocationPolicy: gke.IPAllocationPolicy{}, + MasterAuthorizedNetworks: gke.MasterAuthorizedNetworks{ + CIDRs: []iacTypes.StringValue{}, + }, + NetworkPolicy: gke.NetworkPolicy{}, + DatapathProvider: iacTypes.StringTest("DATAPATH_PROVIDER_UNSPECIFIED"), + PrivateCluster: gke.PrivateCluster{}, + LoggingService: iacTypes.StringTest("logging.googleapis.com/kubernetes"), + MonitoringService: iacTypes.StringTest("monitoring.googleapis.com/kubernetes"), + MasterAuth: gke.MasterAuth{ + ClientCertificate: gke.ClientCertificate{}, + }, + EnableShieldedNodes: iacTypes.BoolTest(true), + ResourceLabels: iacTypes.MapTest(make(map[string]string)), + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` +resource "google_container_cluster" "example" { + + node_config { + metadata = { + disable-legacy-endpoints = true + } + } + pod_security_policy_config { + enabled = "true" + } + + enable_legacy_abac = "true" + enable_shielded_nodes = "true" + + remove_default_node_pool = true + monitoring_service = "monitoring.googleapis.com/kubernetes" + logging_service = "logging.googleapis.com/kubernetes" + + master_auth { + client_certificate_config { + issue_client_certificate = true + } + } + + master_authorized_networks_config { + cidr_blocks { + cidr_block = "10.10.128.0/24" + } + } + + resource_labels = { + "env" = "staging" + } + + private_cluster_config { + enable_private_nodes = true + } + + network_policy { + enabled = true + } + ip_allocation_policy {} +} + +resource "google_container_node_pool" "primary_preemptible_nodes" { + cluster = google_container_cluster.example.name + + node_config { + metadata = { + disable-legacy-endpoints = true + } + service_account = google_service_account.default.email + image_type = "COS_CONTAINERD" + + workload_metadata_config { + mode = "GCE_METADATA" + } + } + management { + auto_repair = true + auto_upgrade = true + } +} +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Clusters, 1) + cluster := adapted.Clusters[0] + nodePool := cluster.NodePools[0] + + assert.Equal(t, 2, cluster.Metadata.Range().GetStartLine()) + assert.Equal(t, 44, cluster.Metadata.Range().GetEndLine()) + + assert.Equal(t, 49, cluster.NodeConfig.Metadata.Range().GetStartLine()) + assert.Equal(t, 59, cluster.NodeConfig.Metadata.Range().GetEndLine()) + + assert.Equal(t, 50, cluster.NodeConfig.EnableLegacyEndpoints.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 52, cluster.NodeConfig.EnableLegacyEndpoints.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, cluster.EnableLegacyABAC.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 13, cluster.EnableLegacyABAC.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 14, cluster.EnableShieldedNodes.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 14, cluster.EnableShieldedNodes.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 16, cluster.RemoveDefaultNodePool.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 16, cluster.RemoveDefaultNodePool.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 17, cluster.MonitoringService.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 17, cluster.MonitoringService.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 18, cluster.LoggingService.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 18, cluster.LoggingService.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 20, cluster.MasterAuth.Metadata.Range().GetStartLine()) + assert.Equal(t, 24, cluster.MasterAuth.Metadata.Range().GetEndLine()) + + assert.Equal(t, 21, cluster.MasterAuth.ClientCertificate.Metadata.Range().GetStartLine()) + assert.Equal(t, 23, cluster.MasterAuth.ClientCertificate.Metadata.Range().GetEndLine()) + + assert.Equal(t, 22, cluster.MasterAuth.ClientCertificate.IssueCertificate.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 22, cluster.MasterAuth.ClientCertificate.IssueCertificate.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 26, cluster.MasterAuthorizedNetworks.Metadata.Range().GetStartLine()) + assert.Equal(t, 30, cluster.MasterAuthorizedNetworks.Metadata.Range().GetEndLine()) + + assert.Equal(t, 28, cluster.MasterAuthorizedNetworks.CIDRs[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 28, cluster.MasterAuthorizedNetworks.CIDRs[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 32, cluster.ResourceLabels.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 34, cluster.ResourceLabels.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 36, cluster.PrivateCluster.Metadata.Range().GetStartLine()) + assert.Equal(t, 38, cluster.PrivateCluster.Metadata.Range().GetEndLine()) + + assert.Equal(t, 37, cluster.PrivateCluster.EnablePrivateNodes.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 37, cluster.PrivateCluster.EnablePrivateNodes.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 40, cluster.NetworkPolicy.Metadata.Range().GetStartLine()) + assert.Equal(t, 42, cluster.NetworkPolicy.Metadata.Range().GetEndLine()) + + assert.Equal(t, 41, cluster.NetworkPolicy.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 41, cluster.NetworkPolicy.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 43, cluster.IPAllocationPolicy.Metadata.Range().GetStartLine()) + assert.Equal(t, 43, cluster.IPAllocationPolicy.Metadata.Range().GetEndLine()) + + assert.Equal(t, 46, nodePool.Metadata.Range().GetStartLine()) + assert.Equal(t, 64, nodePool.Metadata.Range().GetEndLine()) + + assert.Equal(t, 49, nodePool.NodeConfig.Metadata.Range().GetStartLine()) + assert.Equal(t, 59, nodePool.NodeConfig.Metadata.Range().GetEndLine()) + + assert.Equal(t, 53, nodePool.NodeConfig.ServiceAccount.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 53, nodePool.NodeConfig.ServiceAccount.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 54, nodePool.NodeConfig.ImageType.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 54, nodePool.NodeConfig.ImageType.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 56, nodePool.NodeConfig.WorkloadMetadataConfig.Metadata.Range().GetStartLine()) + assert.Equal(t, 58, nodePool.NodeConfig.WorkloadMetadataConfig.Metadata.Range().GetEndLine()) + + assert.Equal(t, 57, nodePool.NodeConfig.WorkloadMetadataConfig.NodeMetadata.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 57, nodePool.NodeConfig.WorkloadMetadataConfig.NodeMetadata.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 60, nodePool.Management.Metadata.Range().GetStartLine()) + assert.Equal(t, 63, nodePool.Management.Metadata.Range().GetEndLine()) + + assert.Equal(t, 61, nodePool.Management.EnableAutoRepair.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 61, nodePool.Management.EnableAutoRepair.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 62, nodePool.Management.EnableAutoUpgrade.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 62, nodePool.Management.EnableAutoUpgrade.GetMetadata().Range().GetEndLine()) + +} diff --git a/pkg/iac/adapters/terraform/google/iam/adapt.go b/pkg/iac/adapters/terraform/google/iam/adapt.go new file mode 100644 index 000000000000..1dcc8a4cec68 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/iam/adapt.go @@ -0,0 +1,59 @@ +package iam + +import ( + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) iam.IAM { + return (&adapter{ + orgs: make(map[string]*iam.Organization), + projects: make(map[string]*iam.Project), + projectsByID: make(map[string]string), // projectID -> blockID + folders: make(map[string]*iam.Folder), + modules: modules, + }).Adapt() +} + +type adapter struct { + modules terraform.Modules + orgs map[string]*iam.Organization + folders map[string]*iam.Folder + projects map[string]*iam.Project + projectsByID map[string]string + workloadIdentityPoolProviders []iam.WorkloadIdentityPoolProvider +} + +func (a *adapter) Adapt() iam.IAM { + a.adaptOrganizationIAM() + a.adaptFolderIAM() + a.adaptProjectIAM() + a.adaptWorkloadIdentityPoolProviders() + return a.buildIAMOutput() +} + +func (a *adapter) buildIAMOutput() iam.IAM { + return iam.IAM{ + Organizations: fromPtrSlice(lo.Values(a.orgs)), + Folders: fromPtrSlice(lo.Values(a.folders)), + Projects: fromPtrSlice(lo.Values(a.projects)), + WorkloadIdentityPoolProviders: a.workloadIdentityPoolProviders, + } +} + +func fromPtrSlice[T any](collection []*T) []T { + if len(collection) == 0 { + return nil + } + + result := make([]T, 0, len(collection)) + for _, item := range collection { + if item == nil { + continue + } + result = append(result, *item) + } + return result +} diff --git a/pkg/iac/adapters/terraform/google/iam/adapt_test.go b/pkg/iac/adapters/terraform/google/iam/adapt_test.go new file mode 100644 index 000000000000..638ef4b3b97b --- /dev/null +++ b/pkg/iac/adapters/terraform/google/iam/adapt_test.go @@ -0,0 +1,514 @@ +package iam + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected iam.IAM + }{ + { + name: "basic", + terraform: ` +data "google_organization" "org" { + domain = "example.com" +} + +resource "google_project" "my_project" { + name = "My Project" + project_id = "your-project-id" + org_id = data.google_organization.org.org_id + auto_create_network = true +} + +resource "google_folder" "department1" { + display_name = "Department 1" + parent = data.google_organization.org.org_id +} + +resource "google_folder_iam_member" "admin" { + folder = google_folder.department1.name + role = "roles/editor" + member = "user:alice@gmail.com" +} + +resource "google_folder_iam_binding" "folder-123" { + folder = google_folder.department1.name + role = "roles/nothing" + members = [ + "user:not-alice@gmail.com", + ] +} + +resource "google_organization_iam_member" "org-123" { + org_id = data.google_organization.org.org_id + role = "roles/whatever" + member = "user:member@gmail.com" +} + +resource "google_organization_iam_binding" "binding" { + org_id = data.google_organization.org.org_id + role = "roles/browser" + + members = [ + "user:member_2@gmail.com", + ] +} + +resource "google_iam_workload_identity_pool_provider" "example" { + workload_identity_pool_id = "example-pool" + workload_identity_pool_provider_id = "example-provider" + attribute_condition = "assertion.repository_owner=='your-github-organization'" +} +`, + expected: iam.IAM{ + Projects: []iam.Project{ + { + AutoCreateNetwork: iacTypes.BoolTest(true), + }, + }, + Folders: []iam.Folder{ + { + Members: []iam.Member{ + { + Member: iacTypes.StringTest("user:alice@gmail.com"), + Role: iacTypes.StringTest("roles/editor"), + }, + }, + Bindings: []iam.Binding{ + { + Members: []iacTypes.StringValue{ + iacTypes.StringTest("user:not-alice@gmail.com"), + }, + Role: iacTypes.StringTest("roles/nothing"), + }, + }, + }, + }, + Organizations: []iam.Organization{ + { + Members: []iam.Member{ + { + Member: iacTypes.StringTest("user:member@gmail.com"), + Role: iacTypes.StringTest("roles/whatever"), + }, + }, + Bindings: []iam.Binding{ + { + Members: []iacTypes.StringValue{ + iacTypes.StringTest("user:member_2@gmail.com")}, + Role: iacTypes.StringTest("roles/browser"), + }, + }, + }, + }, + WorkloadIdentityPoolProviders: []iam.WorkloadIdentityPoolProvider{ + { + + WorkloadIdentityPoolId: iacTypes.StringTest("example-pool"), + WorkloadIdentityPoolProviderId: iacTypes.StringTest("example-provider"), + AttributeCondition: iacTypes.StringTest("assertion.repository_owner=='your-github-organization'"), + }, + }, + }, + }, + { + name: "iam policies", + terraform: ` +resource "google_folder" "test" { + display_name = "Department 1" +} + +resource "google_folder_iam_policy" "folder" { + folder = google_folder.test.folder_id + policy_data = data.google_iam_policy.folder_admin.policy_data +} + +data "google_iam_policy" "folder_admin" { + binding { + role = "roles/editor" + + members = [ + "user:jane@example.com", + ] + } +} + +data "google_organization" "test" { + domain = "example.com" +} + +resource "google_organization_iam_policy" "organization" { + org_id = data.google_organization.test.name + policy_data = data.google_iam_policy.org_admin.policy_data +} + +data "google_iam_policy" "org_admin" { + binding { + role = "roles/editor" + + members = [ + "user:jane2@example.com", + ] + } +} + +resource "google_project" "test" { + name = "My Project2" +} + +resource "google_project_iam_policy" "project" { + project = google_project.test.id + policy_data = data.google_iam_policy.project_admin.policy_data +} + +data "google_iam_policy" "project_admin" { + binding { + role = "roles/editor" + + members = [ + "user:jane3@example.com", + ] + } +} +`, + expected: iam.IAM{ + Folders: []iam.Folder{ + { + Bindings: []iam.Binding{ + { + Role: iacTypes.StringTest("roles/editor"), + Members: []iacTypes.StringValue{ + iacTypes.StringTest("user:jane@example.com"), + }, + }, + }, + }, + }, + Organizations: []iam.Organization{ + { + Bindings: []iam.Binding{ + { + Role: iacTypes.StringTest("roles/editor"), + Members: []iacTypes.StringValue{ + iacTypes.StringTest("user:jane2@example.com"), + }, + }, + }, + }, + }, + Projects: []iam.Project{ + { + AutoCreateNetwork: iacTypes.BoolTest(true), + Bindings: []iam.Binding{ + { + Role: iacTypes.StringTest("roles/editor"), + Members: []iacTypes.StringValue{ + iacTypes.StringTest("user:jane3@example.com"), + }, + }, + }, + }, + }, + }, + }, + { + name: "google_project_iam ref by value", + terraform: ` +resource "google_project" "my_project" { + name = "My Project" + project_id = "your-project-id" + org_id = "1234567" +} + + +resource "google_project_iam_member" "project" { + project = "your-project-id" + role = "roles/editor" + member = "user:jane@example.com" +} +`, + expected: iam.IAM{ + Projects: []iam.Project{ + { + AutoCreateNetwork: iacTypes.BoolTest(true), + Members: []iam.Member{ + { + Role: iacTypes.StringTest("roles/editor"), + Member: iacTypes.StringTest("user:jane@example.com"), + }, + }, + }, + }, + }, + }, + { + name: "only google_project_iam", + terraform: ` +resource "google_project_iam_member" "project" { + project = "your-project-id" + role = "roles/editor" + member = "user:jane@example.com" +} +`, + expected: iam.IAM{ + Projects: []iam.Project{ + { + Members: []iam.Member{ + { + Role: iacTypes.StringTest("roles/editor"), + Member: iacTypes.StringTest("user:jane@example.com"), + }, + }, + }, + }, + }, + }, + { + name: "audit configs", + terraform: ` +data "google_organization" "org" { + domain = "example.com" +} + +resource "google_project" "test" { + name = "Test project" + project_id = "test" + org_id = data.google_organization.org.org_id + auto_create_network = false +} + +resource "google_folder" "test" { + display_name = "Test folder" + parent = data.google_organization.org.org_id +} + +resource "google_project_iam_audit_config" "project_audit" { + project = google_project.test.project_id + service = "allServices" + + audit_log_config { + log_type = "ADMIN_READ" + } + + audit_log_config { + log_type = "DATA_WRITE" + exempted_members = [ + "user:alice@example.com", + "serviceAccount:test@project.iam.gserviceaccount.com" + ] + } +} + +resource "google_organization_iam_audit_config" "org_audit" { + org_id = data.google_organization.org.org_id + service = "storage.googleapis.com" + + audit_log_config { + log_type = "DATA_READ" + exempted_members = ["user:bob@example.com"] + } +} + +resource "google_folder_iam_audit_config" "folder_audit" { + folder = google_folder.test.name + service = "compute.googleapis.com" + + audit_log_config { + log_type = "ADMIN_READ" + } +} +`, + expected: iam.IAM{ + Projects: []iam.Project{ + { + AuditConfigs: []iam.AuditConfig{ + { + Service: iacTypes.StringTest("allServices"), + AuditLogConfigs: []iam.AuditLogConfig{ + { + LogType: iacTypes.StringTest("ADMIN_READ"), + }, + { + LogType: iacTypes.StringTest("DATA_WRITE"), + ExemptedMembers: []iacTypes.StringValue{ + iacTypes.StringTest("user:alice@example.com"), + iacTypes.StringTest("serviceAccount:test@project.iam.gserviceaccount.com"), + }, + }, + }, + }, + }, + }, + }, + Organizations: []iam.Organization{ + { + AuditConfigs: []iam.AuditConfig{ + { + Service: iacTypes.StringTest("storage.googleapis.com"), + AuditLogConfigs: []iam.AuditLogConfig{ + { + LogType: iacTypes.StringTest("DATA_READ"), + ExemptedMembers: []iacTypes.StringValue{ + iacTypes.StringTest("user:bob@example.com"), + }, + }, + }, + }, + }, + }, + }, + Folders: []iam.Folder{ + { + AuditConfigs: []iam.AuditConfig{ + { + Service: iacTypes.StringTest("compute.googleapis.com"), + AuditLogConfigs: []iam.AuditLogConfig{ + { + LogType: iacTypes.StringTest("ADMIN_READ"), + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + data "google_organization" "org" { + domain = "example.com" + } + + resource "google_project" "my_project" { + name = "My Project" + project_id = "your-project-id" + org_id = data.google_organization.org.id + auto_create_network = true + } + + resource "google_folder" "department1" { + display_name = "Department 1" + parent = data.google_organization.org.id + } + + resource "google_folder_iam_binding" "folder-123" { + folder = google_folder.department1.name + role = "roles/nothing" + members = [ + "user:not-alice@gmail.com", + ] + } + + resource "google_folder_iam_member" "admin" { + folder = google_folder.department1.name + role = "roles/editor" + member = "user:alice@gmail.com" + } + + resource "google_organization_iam_member" "org-123" { + org_id = data.google_organization.org.id + role = "roles/whatever" + member = "user:member@gmail.com" + } + + resource "google_organization_iam_binding" "binding" { + org_id = data.google_organization.org.id + role = "roles/browser" + + members = [ + "user:member_2@gmail.com", + ] + } + + resource "google_iam_workload_identity_pool_provider" "example" { + workload_identity_pool_id = "example-pool" + workload_identity_pool_provider_id = "example-provider" + attribute_condition = "assertion.repository_owner=='your-github-organization'" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Organizations, 1) + require.Len(t, adapted.Projects, 1) + require.Len(t, adapted.Folders, 1) + require.Len(t, adapted.Organizations[0].Bindings, 1) + require.Len(t, adapted.Organizations[0].Members, 1) + require.Len(t, adapted.WorkloadIdentityPoolProviders, 1) + + project := adapted.Projects[0] + folder := adapted.Folders[0] + binding := adapted.Organizations[0].Bindings[0] + member := adapted.Organizations[0].Members[0] + pool := adapted.WorkloadIdentityPoolProviders[0] + + assert.Equal(t, 6, project.Metadata.Range().GetStartLine()) + assert.Equal(t, 11, project.Metadata.Range().GetEndLine()) + + assert.Equal(t, 10, project.AutoCreateNetwork.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, project.AutoCreateNetwork.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, folder.Metadata.Range().GetStartLine()) + assert.Equal(t, 16, folder.Metadata.Range().GetEndLine()) + + assert.Equal(t, 18, folder.Bindings[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 24, folder.Bindings[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 20, folder.Bindings[0].Role.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 20, folder.Bindings[0].Role.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 21, folder.Bindings[0].Members[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 23, folder.Bindings[0].Members[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 26, folder.Members[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 30, folder.Members[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 29, folder.Members[0].Member.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 29, folder.Members[0].Member.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 28, folder.Members[0].Role.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 28, folder.Members[0].Role.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 32, member.Metadata.Range().GetStartLine()) + assert.Equal(t, 36, member.Metadata.Range().GetEndLine()) + + assert.Equal(t, 34, member.Role.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 34, member.Role.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 35, member.Member.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 35, member.Member.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 38, binding.Metadata.Range().GetStartLine()) + assert.Equal(t, 45, binding.Metadata.Range().GetEndLine()) + + assert.Equal(t, 40, binding.Role.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 40, binding.Role.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 42, binding.Members[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 44, binding.Members[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 51, pool.Metadata.Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/google/iam/convert.go b/pkg/iac/adapters/terraform/google/iam/convert.go new file mode 100644 index 000000000000..0bf31b41b223 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/iam/convert.go @@ -0,0 +1,26 @@ +package iam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func ParsePolicyBlock(block *terraform.Block) []iam.Binding { + var bindings []iam.Binding + for _, bindingBlock := range block.GetBlocks("binding") { + binding := iam.Binding{ + Metadata: bindingBlock.GetMetadata(), + Members: nil, + Role: bindingBlock.GetAttribute("role").AsStringValueOrDefault("", bindingBlock), + IncludesDefaultServiceAccount: iacTypes.BoolDefault(false, bindingBlock.GetMetadata()), + } + membersAttr := bindingBlock.GetAttribute("members") + members := membersAttr.AsStringValues().AsStrings() + for _, member := range members { + binding.Members = append(binding.Members, iacTypes.String(member, membersAttr.GetMetadata())) + } + bindings = append(bindings, binding) + } + return bindings +} diff --git a/pkg/iac/adapters/terraform/google/iam/folder_iam.go b/pkg/iac/adapters/terraform/google/iam/folder_iam.go new file mode 100644 index 000000000000..cd486c5fd459 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/iam/folder_iam.go @@ -0,0 +1,115 @@ +package iam + +import ( + "github.com/google/uuid" + + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +// see https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_folder_iam + +func (a *adapter) adaptFolderIAM() { + a.adaptFolders() + a.adaptFolderMembers() + a.adaptFolderBindings() + a.adaptFolderAuditConfigs() +} + +const googleFolder = "google_folder" + +func (a *adapter) adaptFolders() { + for _, folderBlock := range a.modules.GetResourcesByType(googleFolder) { + a.folders[folderBlock.ID()] = &iam.Folder{ + Metadata: folderBlock.GetMetadata(), + } + } +} + +func (a *adapter) adaptFolderMembers() { + for _, iamBlock := range a.modules.GetResourcesByType("google_folder_iam_member") { + member := a.adaptMember(iamBlock) + + if folder := a.findFolder(iamBlock); folder != nil { + folder.Members = append(folder.Members, member) + } else { + // we didn't find the folder - add an unmanaged one + a.folders[uuid.NewString()] = &iam.Folder{ + Metadata: types.NewUnmanagedMetadata(), + Members: []iam.Member{member}, + } + } + } +} + +func (a *adapter) adaptFolderBindings() { + + for _, iamBlock := range a.modules.GetResourcesByType("google_folder_iam_policy") { + + policyAttr := iamBlock.GetAttribute("policy_data") + if policyAttr.IsNil() { + continue + } + + policyBlock, err := a.modules.GetReferencedBlock(policyAttr, iamBlock) + if err != nil { + continue + } + + bindings := ParsePolicyBlock(policyBlock) + + if folder := a.findFolder(iamBlock); folder != nil { + folder.Bindings = append(folder.Bindings, bindings...) + } else { + // we didn't find the folder - add an unmanaged one + a.folders[uuid.NewString()] = &iam.Folder{ + Metadata: types.NewUnmanagedMetadata(), + Bindings: bindings, + } + } + } + + for _, iamBlock := range a.modules.GetResourcesByType("google_folder_iam_binding") { + binding := a.adaptBinding(iamBlock) + + if folder := a.findFolder(iamBlock); folder != nil { + folder.Bindings = append(folder.Bindings, binding) + } else { + // we didn't find the folder - add an unmanaged one + a.folders[uuid.NewString()] = &iam.Folder{ + Metadata: types.NewUnmanagedMetadata(), + Bindings: []iam.Binding{binding}, + } + } + } +} + +func (a *adapter) findFolder(iamBlock *terraform.Block) *iam.Folder { + folderAttr := iamBlock.GetAttribute("folder") + refBlock, err := a.modules.GetReferencedBlock(folderAttr, iamBlock) + if err != nil { + return nil + } + + if folder, exists := a.folders[refBlock.ID()]; exists { + return folder + } + + return nil +} + +func (a *adapter) adaptFolderAuditConfigs() { + for _, iamBlock := range a.modules.GetResourcesByType("google_folder_iam_audit_config") { + auditConfig := AdaptAuditConfig(iamBlock) + if folder := a.findFolder(iamBlock); folder != nil { + folder.AuditConfigs = append(folder.AuditConfigs, auditConfig) + } else { + // we didn't find the folder - add an unmanaged one + a.folders[uuid.NewString()] = &iam.Folder{ + Metadata: types.NewUnmanagedMetadata(), + AuditConfigs: []iam.AuditConfig{auditConfig}, + } + } + } +} diff --git a/pkg/iac/adapters/terraform/google/iam/org_iam.go b/pkg/iac/adapters/terraform/google/iam/org_iam.go new file mode 100644 index 000000000000..cfc6b124fb04 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/iam/org_iam.go @@ -0,0 +1,118 @@ +package iam + +import ( + "github.com/google/uuid" + + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +// see https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_organization_iam + +func (a *adapter) adaptOrganizationIAM() { + a.adaptOrganizations() + a.adaptOrganizationMembers() + a.adaptOrganizationBindings() + a.adaptOrganizationAuditConfigs() +} + +func (a *adapter) adaptOrganizations() { + for _, orgBlock := range a.modules.GetDatasByType("google_organization") { + a.orgs[orgBlock.ID()] = &iam.Organization{ + Metadata: orgBlock.GetMetadata(), + } + } +} + +func (a *adapter) adaptOrganizationMembers() { + for _, iamBlock := range a.modules.GetResourcesByType("google_organization_iam_member") { + + member := a.adaptMember(iamBlock) + + if org := a.findOrganization(iamBlock); org != nil { + org.Members = append(org.Members, member) + } else { + // we didn't find the org - add an unmanaged one + a.orgs[uuid.NewString()] = &iam.Organization{ + Metadata: types.NewUnmanagedMetadata(), + Members: []iam.Member{member}, + } + } + } +} + +func (a *adapter) adaptOrganizationBindings() { + + for _, iamBlock := range a.modules.GetResourcesByType("google_organization_iam_policy") { + + policyAttr := iamBlock.GetAttribute("policy_data") + if policyAttr.IsNil() { + continue + } + + policyBlock, err := a.modules.GetReferencedBlock(policyAttr, iamBlock) + if err != nil { + continue + } + + bindings := ParsePolicyBlock(policyBlock) + + if org := a.findOrganization(iamBlock); org != nil { + org.Bindings = append(org.Bindings, bindings...) + } else { + // we didn't find the org - add an unmanaged one + a.orgs[uuid.NewString()] = &iam.Organization{ + Metadata: types.NewUnmanagedMetadata(), + Bindings: bindings, + } + } + } + + for _, iamBlock := range a.modules.GetResourcesByType("google_organization_iam_binding") { + + binding := a.adaptBinding(iamBlock) + + if org := a.findOrganization(iamBlock); org != nil { + org.Bindings = append(org.Bindings, binding) + } else { + // we didn't find the org - add an unmanaged one + a.orgs[uuid.NewString()] = &iam.Organization{ + Metadata: types.NewUnmanagedMetadata(), + Bindings: []iam.Binding{binding}, + } + } + } +} + +func (a *adapter) findOrganization(iamBlock *terraform.Block) *iam.Organization { + orgAttr := iamBlock.GetAttribute("organization") + if orgAttr.IsNil() { + orgAttr = iamBlock.GetAttribute("org_id") + } + refBlock, err := a.modules.GetReferencedBlock(orgAttr, iamBlock) + if err != nil { + return nil + } + + if org, exists := a.orgs[refBlock.ID()]; exists { + return org + } + + return nil +} + +func (a *adapter) adaptOrganizationAuditConfigs() { + for _, iamBlock := range a.modules.GetResourcesByType("google_organization_iam_audit_config") { + auditConfig := AdaptAuditConfig(iamBlock) + if org := a.findOrganization(iamBlock); org != nil { + org.AuditConfigs = append(org.AuditConfigs, auditConfig) + } else { + // we didn't find the org - add an unmanaged one + a.orgs[uuid.NewString()] = &iam.Organization{ + Metadata: types.NewUnmanagedMetadata(), + AuditConfigs: []iam.AuditConfig{auditConfig}, + } + } + } +} diff --git a/pkg/iac/adapters/terraform/google/iam/project_iam.go b/pkg/iac/adapters/terraform/google/iam/project_iam.go new file mode 100644 index 000000000000..eb8b69ec92a2 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/iam/project_iam.go @@ -0,0 +1,257 @@ +package iam + +import ( + "strings" + + "github.com/google/uuid" + + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +// see https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/google_project_iam + +const GoogleProject = "google_project" + +func (a *adapter) adaptProjectIAM() { + a.adaptProjects() + a.adaptProjectMembers() + a.adaptProjectBindings() + a.adaptProjectAuditConfigs() +} + +func (a *adapter) adaptProjects() { + for _, projectBlock := range a.modules.GetResourcesByType(GoogleProject) { + idAttr := projectBlock.GetAttribute("project_id") + if idAttr.IsString() { + a.projectsByID[idAttr.Value().AsString()] = projectBlock.ID() + } + + a.projects[projectBlock.ID()] = &iam.Project{ + Metadata: projectBlock.GetMetadata().Root(), + AutoCreateNetwork: projectBlock.GetAttribute("auto_create_network").AsBoolValueOrDefault(true, projectBlock), + } + } +} + +func (a *adapter) adaptMember(iamBlock *terraform.Block) iam.Member { + return AdaptMember(iamBlock, a.modules) +} + +func AdaptMember(iamBlock *terraform.Block, modules terraform.Modules) iam.Member { + member := iam.Member{ + Metadata: iamBlock.GetMetadata(), + Member: iamBlock.GetAttribute("member").AsStringValueOrDefault("", iamBlock), + Role: iamBlock.GetAttribute("role").AsStringValueOrDefault("", iamBlock), + DefaultServiceAccount: iacTypes.BoolDefault(false, iamBlock.GetMetadata()), + } + + memberAttr := iamBlock.GetAttribute("member") + if referencedBlock, err := modules.GetReferencedBlock(memberAttr, iamBlock); err == nil { + if strings.HasSuffix(referencedBlock.TypeLabel(), "_default_service_account") { + member.DefaultServiceAccount = iacTypes.Bool(true, memberAttr.GetMetadata()) + } + } + + return member +} + +// TODO(nikita): add new resources +var projectMemberResources = []string{ + "google_project_iam_member", + "google_cloud_run_service_iam_member", + "google_compute_instance_iam_member", + "google_compute_subnetwork_iam_member", + "google_data_catalog_entry_group_iam_member", + "google_pubsub_subscription_iam_member", + "google_pubsub_topic_iam_member", + "google_sourcerepo_repository_iam_member", + "google_spanner_database_iam_member", + "google_spanner_instance_iam_member", + "google_storage_bucket_iam_member", +} + +func (a *adapter) adaptProjectMembers() { + + for _, memberType := range projectMemberResources { + for _, iamBlock := range a.modules.GetResourcesByType(memberType) { + + member := a.adaptMember(iamBlock) + + if project := a.findProject(iamBlock); project != nil { + project.Members = append(project.Members, member) + } else { + // we didn't find the folder - add an unmanaged one + a.projects[uuid.NewString()] = &iam.Project{ + Metadata: iacTypes.NewUnmanagedMetadata(), + AutoCreateNetwork: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + Members: []iam.Member{member}, + } + } + } + } +} + +func (a *adapter) adaptBinding(iamBlock *terraform.Block) iam.Binding { + return AdaptBinding(iamBlock, a.modules) +} + +func AdaptBinding(iamBlock *terraform.Block, modules terraform.Modules) iam.Binding { + binding := iam.Binding{ + Metadata: iamBlock.GetMetadata(), + Members: nil, + Role: iamBlock.GetAttribute("role").AsStringValueOrDefault("", iamBlock), + IncludesDefaultServiceAccount: iacTypes.BoolDefault(false, iamBlock.GetMetadata()), + } + membersAttr := iamBlock.GetAttribute("members") + members := membersAttr.AsStringValues().AsStrings() + for _, member := range members { + binding.Members = append(binding.Members, iacTypes.String(member, membersAttr.GetMetadata())) + } + if referencedBlock, err := modules.GetReferencedBlock(membersAttr, iamBlock); err == nil { + if strings.HasSuffix(referencedBlock.TypeLabel(), "_default_service_account") { + binding.IncludesDefaultServiceAccount = iacTypes.Bool(true, membersAttr.GetMetadata()) + } + } + return binding +} + +// TODO(nikita): add new resources +var projectBindingResources = []string{ + "google_project_iam_binding", + "google_cloud_run_service_iam_binding", + "google_compute_instance_iam_binding", + "google_compute_subnetwork_iam_binding", + "google_data_catalog_entry_group_iam_binding", + "google_pubsub_subscription_iam_binding", + "google_pubsub_topic_iam_binding", + "google_sourcerepo_repository_iam_binding", + "google_spanner_database_iam_binding", + "google_spanner_instance_iam_binding", + "google_storage_bucket_iam_binding", +} + +func (a *adapter) adaptProjectDataBindings() { + for _, iamBlock := range a.modules.GetResourcesByType("google_project_iam_policy") { + + policyAttr := iamBlock.GetAttribute("policy_data") + if policyAttr.IsNil() { + continue + } + + policyBlock, err := a.modules.GetReferencedBlock(policyAttr, iamBlock) + if err != nil { + continue + } + + bindings := ParsePolicyBlock(policyBlock) + + if project := a.findProject(iamBlock); project != nil { + project.Bindings = append(project.Bindings, bindings...) + } else { + // we didn't find the folder - add an unmanaged one + a.projects[uuid.NewString()] = &iam.Project{ + Metadata: iacTypes.NewUnmanagedMetadata(), + AutoCreateNetwork: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + Bindings: bindings, + } + } + } + +} + +func (a *adapter) adaptProjectBindings() { + + a.adaptProjectDataBindings() + + for _, bindingType := range projectBindingResources { + for _, iamBlock := range a.modules.GetResourcesByType(bindingType) { + + binding := a.adaptBinding(iamBlock) + + if project := a.findProject(iamBlock); project != nil { + project.Bindings = append(project.Bindings, binding) + } else { + // we didn't find the folder - add an unmanaged one + a.projects[uuid.NewString()] = &iam.Project{ + Metadata: iacTypes.NewUnmanagedMetadata(), + AutoCreateNetwork: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + Bindings: []iam.Binding{binding}, + } + } + } + } +} + +func (a *adapter) resolveProjectBlockID(projectAttr *terraform.Attribute, iamBlock *terraform.Block) string { + + if projectAttr.IsString() { + projectID := projectAttr.Value().AsString() + if blockID, exists := a.projectsByID[projectID]; exists { + return blockID + } + } + + refBlock, err := a.modules.GetReferencedBlock(projectAttr, iamBlock) + if err != nil { + return "" + } + return refBlock.ID() +} + +func (a *adapter) findProject(iamBlock *terraform.Block) *iam.Project { + projectAttr := iamBlock.GetAttribute("project") + blockID := a.resolveProjectBlockID(projectAttr, iamBlock) + if blockID == "" { + return nil + } + + if project, exists := a.projects[blockID]; exists { + return project + } + + return nil +} + +func (a *adapter) adaptProjectAuditConfigs() { + for _, iamBlock := range a.modules.GetResourcesByType("google_project_iam_audit_config") { + auditConfig := AdaptAuditConfig(iamBlock) + if project := a.findProject(iamBlock); project != nil { + project.AuditConfigs = append(project.AuditConfigs, auditConfig) + } else { + // we didn't find the project - add an unmanaged one + a.projects[uuid.NewString()] = &iam.Project{ + Metadata: iacTypes.NewUnmanagedMetadata(), + AutoCreateNetwork: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + AuditConfigs: []iam.AuditConfig{auditConfig}, + } + } + } +} + +func AdaptAuditConfig(block *terraform.Block) iam.AuditConfig { + auditConfig := iam.AuditConfig{ + Metadata: block.GetMetadata(), + Service: block.GetAttribute("service").AsStringValueOrDefault("", block), + } + + for _, logConfigBlock := range block.GetBlocks("audit_log_config") { + logConfig := iam.AuditLogConfig{ + Metadata: logConfigBlock.GetMetadata(), + LogType: logConfigBlock.GetAttribute("log_type").AsStringValueOrDefault("", logConfigBlock), + } + + // Parse exempted_members array + if exemptedAttr := logConfigBlock.GetAttribute("exempted_members"); !exemptedAttr.IsNil() { + for _, member := range exemptedAttr.AsStringValues().AsStrings() { + logConfig.ExemptedMembers = append(logConfig.ExemptedMembers, + iacTypes.String(member, exemptedAttr.GetMetadata())) + } + } + + auditConfig.AuditLogConfigs = append(auditConfig.AuditLogConfigs, logConfig) + } + + return auditConfig +} diff --git a/pkg/iac/adapters/terraform/google/iam/workload_identity_pool_providers.go b/pkg/iac/adapters/terraform/google/iam/workload_identity_pool_providers.go new file mode 100644 index 000000000000..242c3a09b907 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/iam/workload_identity_pool_providers.go @@ -0,0 +1,18 @@ +package iam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" +) + +// See https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_workload_identity_pool_provider + +func (a *adapter) adaptWorkloadIdentityPoolProviders() { + for _, resource := range a.modules.GetResourcesByType("google_iam_workload_identity_pool_provider") { + a.workloadIdentityPoolProviders = append(a.workloadIdentityPoolProviders, iam.WorkloadIdentityPoolProvider{ + Metadata: resource.GetMetadata(), + WorkloadIdentityPoolId: resource.GetAttribute("workload_identity_pool_id").AsStringValueOrDefault("", resource), + WorkloadIdentityPoolProviderId: resource.GetAttribute("workload_identity_pool_provider_id").AsStringValueOrDefault("", resource), + AttributeCondition: resource.GetAttribute("attribute_condition").AsStringValueOrDefault("", resource), + }) + } +} diff --git a/pkg/iac/adapters/terraform/google/kms/adapt.go b/pkg/iac/adapters/terraform/google/kms/adapt.go new file mode 100644 index 000000000000..84794880f9fe --- /dev/null +++ b/pkg/iac/adapters/terraform/google/kms/adapt.go @@ -0,0 +1,61 @@ +package kms + +import ( + "strconv" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/providers/google/kms" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) kms.KMS { + return kms.KMS{ + KeyRings: adaptKeyRings(modules), + } +} + +func adaptKeyRings(modules terraform.Modules) []kms.KeyRing { + var keyRings []kms.KeyRing + for _, module := range modules { + for _, resource := range module.GetResourcesByType("google_kms_key_ring") { + var keys []kms.Key + + keyBlocks := module.GetReferencingResources(resource, "google_kms_crypto_key", "key_ring") + for _, keyBlock := range keyBlocks { + keys = append(keys, adaptKey(keyBlock)) + } + keyRings = append(keyRings, kms.KeyRing{ + Metadata: resource.GetMetadata(), + Keys: keys, + }) + } + } + return keyRings +} + +func adaptKey(resource *terraform.Block) kms.Key { + + key := kms.Key{ + Metadata: resource.GetMetadata(), + RotationPeriodSeconds: types.IntDefault(-1, resource.GetMetadata()), + } + + rotationPeriodAttr := resource.GetAttribute("rotation_period") + if !rotationPeriodAttr.IsString() { + return key + } + rotationStr := rotationPeriodAttr.Value().AsString() + secondsStr, ok := strings.CutSuffix(rotationStr, "s") + if !ok { + return key + } + + seconds, err := strconv.Atoi(secondsStr) + if err != nil { + return key + } + + key.RotationPeriodSeconds = types.Int(seconds, rotationPeriodAttr.GetMetadata()) + return key +} diff --git a/pkg/iac/adapters/terraform/google/kms/adapt_test.go b/pkg/iac/adapters/terraform/google/kms/adapt_test.go new file mode 100644 index 000000000000..a6b61e0c2bf7 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/kms/adapt_test.go @@ -0,0 +1,141 @@ +package kms + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/kms" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptKeyRings(t *testing.T) { + tests := []struct { + name string + terraform string + expected []kms.KeyRing + }{ + { + name: "configured", + terraform: ` + resource "google_kms_key_ring" "keyring" { + name = "keyring-example" + } + + resource "google_kms_crypto_key" "example-key" { + name = "crypto-key-example" + key_ring = google_kms_key_ring.keyring.id + rotation_period = "7776000s" + } +`, + expected: []kms.KeyRing{ + { + Keys: []kms.Key{ + { + RotationPeriodSeconds: iacTypes.IntTest(7776000), + }, + }, + }, + }, + }, + { + name: "no keys", + terraform: ` + resource "google_kms_key_ring" "keyring" { + name = "keyring-example" + } + +`, + expected: []kms.KeyRing{ + {}, + }, + }, + { + name: "default rotation period", + terraform: ` + resource "google_kms_key_ring" "keyring" { + name = "keyring-example" + } + + resource "google_kms_crypto_key" "example-key" { + name = "crypto-key-example" + key_ring = google_kms_key_ring.keyring.id + } +`, + expected: []kms.KeyRing{ + { + Keys: []kms.Key{ + { + RotationPeriodSeconds: iacTypes.IntTest(-1), + }, + }, + }, + }, + }, + { + name: "invalid rotation period", + terraform: ` + resource "google_kms_key_ring" "keyring" { + name = "keyring-example" +} + +resource "google_kms_crypto_key" "example-key" { + name = "crypto-key-example" + key_ring = google_kms_key_ring.keyring.id + rotation_period = "" +} +`, + expected: []kms.KeyRing{ + { + Keys: []kms.Key{ + { + RotationPeriodSeconds: iacTypes.IntTest(-1), + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptKeyRings(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "google_kms_key_ring" "keyring" { + name = "keyring-example" + } + + resource "google_kms_crypto_key" "example-key" { + name = "crypto-key-example" + key_ring = google_kms_key_ring.keyring.id + rotation_period = "7776000s" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.KeyRings, 1) + require.Len(t, adapted.KeyRings[0].Keys, 1) + + key := adapted.KeyRings[0].Keys[0] + + assert.Equal(t, 2, adapted.KeyRings[0].Metadata.Range().GetStartLine()) + assert.Equal(t, 4, adapted.KeyRings[0].Metadata.Range().GetEndLine()) + + assert.Equal(t, 6, key.Metadata.Range().GetStartLine()) + assert.Equal(t, 10, key.Metadata.Range().GetEndLine()) + + assert.Equal(t, 9, key.RotationPeriodSeconds.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 9, key.RotationPeriodSeconds.GetMetadata().Range().GetEndLine()) + +} diff --git a/pkg/iac/adapters/terraform/google/sql/adapt.go b/pkg/iac/adapters/terraform/google/sql/adapt.go new file mode 100644 index 000000000000..2204db92aaaf --- /dev/null +++ b/pkg/iac/adapters/terraform/google/sql/adapt.go @@ -0,0 +1,150 @@ +package sql + +import ( + "strconv" + + "github.com/aquasecurity/trivy/pkg/iac/providers/google/sql" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) sql.SQL { + return sql.SQL{ + Instances: adaptInstances(modules), + } +} + +func adaptInstances(modules terraform.Modules) []sql.DatabaseInstance { + var instances []sql.DatabaseInstance + for _, module := range modules { + for _, resource := range module.GetResourcesByType("google_sql_database_instance") { + instances = append(instances, adaptInstance(resource)) + } + } + return instances +} + +func adaptInstance(resource *terraform.Block) sql.DatabaseInstance { + + instance := sql.DatabaseInstance{ + Metadata: resource.GetMetadata(), + DatabaseVersion: resource.GetAttribute("database_version").AsStringValueOrDefault("", resource), + IsReplica: iacTypes.BoolDefault(false, resource.GetMetadata()), + Settings: sql.Settings{ + Metadata: resource.GetMetadata(), + Flags: sql.Flags{ + Metadata: resource.GetMetadata(), + LogTempFileSize: iacTypes.IntDefault(-1, resource.GetMetadata()), + LocalInFile: iacTypes.BoolDefault(false, resource.GetMetadata()), + ContainedDatabaseAuthentication: iacTypes.BoolDefault(true, resource.GetMetadata()), + CrossDBOwnershipChaining: iacTypes.BoolDefault(true, resource.GetMetadata()), + LogCheckpoints: iacTypes.BoolDefault(false, resource.GetMetadata()), + LogConnections: iacTypes.BoolDefault(false, resource.GetMetadata()), + LogDisconnections: iacTypes.BoolDefault(false, resource.GetMetadata()), + LogLockWaits: iacTypes.BoolDefault(false, resource.GetMetadata()), + LogMinMessages: iacTypes.StringDefault("", resource.GetMetadata()), + LogMinDurationStatement: iacTypes.IntDefault(-1, resource.GetMetadata()), + }, + Backups: sql.Backups{ + Metadata: resource.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resource.GetMetadata()), + }, + IPConfiguration: sql.IPConfiguration{ + Metadata: resource.GetMetadata(), + RequireTLS: iacTypes.BoolDefault(false, resource.GetMetadata()), + SSLMode: iacTypes.String("", resource.GetMetadata()), + EnableIPv4: iacTypes.BoolDefault(true, resource.GetMetadata()), + AuthorizedNetworks: nil, + }, + }, + } + + if attr := resource.GetAttribute("master_instance_name"); attr.IsNotNil() { + instance.IsReplica = iacTypes.Bool(true, attr.GetMetadata()) + } + + if settingsBlock := resource.GetBlock("settings"); settingsBlock.IsNotNil() { + instance.Settings.Metadata = settingsBlock.GetMetadata() + if blocks := settingsBlock.GetBlocks("database_flags"); len(blocks) > 0 { + adaptFlags(blocks, &instance.Settings.Flags) + } + if backupBlock := settingsBlock.GetBlock("backup_configuration"); backupBlock.IsNotNil() { + instance.Settings.Backups.Metadata = backupBlock.GetMetadata() + backupConfigEnabledAttr := backupBlock.GetAttribute("enabled") + instance.Settings.Backups.Enabled = backupConfigEnabledAttr.AsBoolValueOrDefault(false, backupBlock) + } + if ipConfBlock := settingsBlock.GetBlock("ip_configuration"); ipConfBlock.IsNotNil() { + instance.Settings.IPConfiguration = adaptIPConfig(ipConfBlock) + } + } + return instance +} + +// nolint +func adaptFlags(resources terraform.Blocks, flags *sql.Flags) { + for _, resource := range resources { + + nameAttr := resource.GetAttribute("name") + valueAttr := resource.GetAttribute("value") + + if !nameAttr.IsString() || valueAttr.IsNil() { + continue + } + + switch nameAttr.Value().AsString() { + case "log_temp_files": + if logTempInt, err := strconv.Atoi(valueAttr.Value().AsString()); err == nil { + flags.LogTempFileSize = iacTypes.Int(logTempInt, nameAttr.GetMetadata()) + } + case "log_min_messages": + flags.LogMinMessages = valueAttr.AsStringValueOrDefault("", resource) + case "log_min_duration_statement": + if logMinDS, err := strconv.Atoi(valueAttr.Value().AsString()); err == nil { + flags.LogMinDurationStatement = iacTypes.Int(logMinDS, nameAttr.GetMetadata()) + } + case "local_infile": + flags.LocalInFile = iacTypes.Bool(valueAttr.Equals("on"), valueAttr.GetMetadata()) + case "log_checkpoints": + flags.LogCheckpoints = iacTypes.Bool(valueAttr.Equals("on"), valueAttr.GetMetadata()) + case "log_connections": + flags.LogConnections = iacTypes.Bool(valueAttr.Equals("on"), valueAttr.GetMetadata()) + case "log_disconnections": + flags.LogDisconnections = iacTypes.Bool(valueAttr.Equals("on"), valueAttr.GetMetadata()) + case "log_lock_waits": + flags.LogLockWaits = iacTypes.Bool(valueAttr.Equals("on"), valueAttr.GetMetadata()) + case "contained database authentication": + flags.ContainedDatabaseAuthentication = iacTypes.Bool(valueAttr.Equals("on"), valueAttr.GetMetadata()) + case "cross db ownership chaining": + flags.CrossDBOwnershipChaining = iacTypes.Bool(valueAttr.Equals("on"), valueAttr.GetMetadata()) + } + } +} + +func adaptIPConfig(resource *terraform.Block) sql.IPConfiguration { + var authorizedNetworks []struct { + Name iacTypes.StringValue + CIDR iacTypes.StringValue + } + + authNetworksBlocks := resource.GetBlocks("authorized_networks") + for _, authBlock := range authNetworksBlocks { + nameVal := authBlock.GetAttribute("name").AsStringValueOrDefault("", authBlock) + cidrVal := authBlock.GetAttribute("value").AsStringValueOrDefault("", authBlock) + + authorizedNetworks = append(authorizedNetworks, struct { + Name iacTypes.StringValue + CIDR iacTypes.StringValue + }{ + Name: nameVal, + CIDR: cidrVal, + }) + } + + return sql.IPConfiguration{ + Metadata: resource.GetMetadata(), + RequireTLS: resource.GetAttribute("require_ssl").AsBoolValueOrDefault(false, resource), + SSLMode: resource.GetAttribute("ssl_mode").AsStringValueOrDefault("", resource), + EnableIPv4: resource.GetAttribute("ipv4_enabled").AsBoolValueOrDefault(true, resource), + AuthorizedNetworks: authorizedNetworks, + } +} diff --git a/pkg/iac/adapters/terraform/google/sql/adapt_test.go b/pkg/iac/adapters/terraform/google/sql/adapt_test.go new file mode 100644 index 000000000000..80512d91d293 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/sql/adapt_test.go @@ -0,0 +1,287 @@ +package sql + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/sql" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected sql.SQL + }{ + { + name: "default flags", + terraform: ` + resource "google_sql_database_instance" "db" { + database_version = "POSTGRES_12" + settings { + backup_configuration { + enabled = true + } + ip_configuration { + ipv4_enabled = false + authorized_networks { + value = "108.12.12.0/24" + name = "internal" + } + require_ssl = true + ssl_mode = "TRUSTED_CLIENT_CERTIFICATE_REQUIRED" + } + } + } +`, + expected: sql.SQL{ + Instances: []sql.DatabaseInstance{ + { + DatabaseVersion: iacTypes.StringTest("POSTGRES_12"), + Settings: sql.Settings{ + Backups: sql.Backups{ + Enabled: iacTypes.BoolTest(true), + }, + Flags: sql.Flags{ + LogMinDurationStatement: iacTypes.IntTest(-1), + ContainedDatabaseAuthentication: iacTypes.BoolTest(true), + CrossDBOwnershipChaining: iacTypes.BoolTest(true), + LogTempFileSize: iacTypes.IntTest(-1), + }, + IPConfiguration: sql.IPConfiguration{ + RequireTLS: iacTypes.BoolTest(true), + SSLMode: iacTypes.StringTest("TRUSTED_CLIENT_CERTIFICATE_REQUIRED"), + AuthorizedNetworks: []struct { + Name iacTypes.StringValue + CIDR iacTypes.StringValue + }{ + { + Name: iacTypes.StringTest("internal"), + CIDR: iacTypes.StringTest("108.12.12.0/24"), + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "wrong ip_configuration", + terraform: ` + resource "google_sql_database_instance" "test" { + settings { + ip_configuration = [] + } + } +`, + expected: sql.SQL{ + Instances: []sql.DatabaseInstance{ + { + + Settings: sql.Settings{ + Flags: sql.Flags{ + ContainedDatabaseAuthentication: iacTypes.BoolTest(true), + CrossDBOwnershipChaining: iacTypes.BoolTest(true), + LogMinDurationStatement: iacTypes.IntTest(-1), + LogTempFileSize: iacTypes.IntTest(-1), + }, + IPConfiguration: sql.IPConfiguration{ + EnableIPv4: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func Test_adaptInstances(t *testing.T) { + tests := []struct { + name string + terraform string + expected []sql.DatabaseInstance + }{ + { + name: "all flags", + terraform: ` +resource "google_sql_database_instance" "backup_source_instance" { + name = "test-instance" + database_version = "POSTGRES_11" + + project = "test-project" + region = "europe-west6" + deletion_protection = false + settings { + tier = "db-f1-micro" + backup_configuration { + enabled = true + } + ip_configuration { + ipv4_enabled = false + private_network = "test-network" + require_ssl = true + } + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + } +} + `, + expected: []sql.DatabaseInstance{ + { + DatabaseVersion: iacTypes.StringTest("POSTGRES_11"), + Settings: sql.Settings{ + Backups: sql.Backups{ + Enabled: iacTypes.BoolTest(true), + }, + Flags: sql.Flags{ + LogConnections: iacTypes.BoolTest(true), + LogCheckpoints: iacTypes.BoolTest(true), + LogDisconnections: iacTypes.BoolTest(true), + LogLockWaits: iacTypes.BoolTest(true), + ContainedDatabaseAuthentication: iacTypes.BoolTest(true), + CrossDBOwnershipChaining: iacTypes.BoolTest(true), + LogMinDurationStatement: iacTypes.IntTest(-1), + }, + IPConfiguration: sql.IPConfiguration{ + RequireTLS: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptInstances(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "google_sql_database_instance" "backup_source_instance" { + name = "test-instance" + database_version = "POSTGRES_11" + + settings { + backup_configuration { + enabled = true + } + + ip_configuration { + ipv4_enabled = false + require_ssl = true + authorized_networks { + name = "internal" + value = "108.12.12.0/24" + } + } + + database_flags { + name = "log_connections" + value = "on" + } + database_flags { + name = "log_temp_files" + value = "0" + } + database_flags { + name = "log_checkpoints" + value = "on" + } + database_flags { + name = "log_disconnections" + value = "on" + } + database_flags { + name = "log_lock_waits" + value = "on" + } + } + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Instances, 1) + instance := adapted.Instances[0] + + assert.Equal(t, 2, instance.Metadata.Range().GetStartLine()) + assert.Equal(t, 41, instance.Metadata.Range().GetEndLine()) + + assert.Equal(t, 4, instance.DatabaseVersion.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, instance.DatabaseVersion.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, instance.Settings.Metadata.Range().GetStartLine()) + assert.Equal(t, 40, instance.Settings.Metadata.Range().GetEndLine()) + + assert.Equal(t, 7, instance.Settings.Backups.Metadata.Range().GetStartLine()) + assert.Equal(t, 9, instance.Settings.Backups.Metadata.Range().GetEndLine()) + + assert.Equal(t, 8, instance.Settings.Backups.Enabled.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 8, instance.Settings.Backups.Enabled.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, instance.Settings.IPConfiguration.Metadata.Range().GetStartLine()) + assert.Equal(t, 18, instance.Settings.IPConfiguration.Metadata.Range().GetEndLine()) + + assert.Equal(t, 12, instance.Settings.IPConfiguration.EnableIPv4.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 12, instance.Settings.IPConfiguration.EnableIPv4.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, instance.Settings.IPConfiguration.RequireTLS.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 13, instance.Settings.IPConfiguration.RequireTLS.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 15, instance.Settings.IPConfiguration.AuthorizedNetworks[0].Name.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 15, instance.Settings.IPConfiguration.AuthorizedNetworks[0].Name.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 16, instance.Settings.IPConfiguration.AuthorizedNetworks[0].CIDR.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 16, instance.Settings.IPConfiguration.AuthorizedNetworks[0].CIDR.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 22, instance.Settings.Flags.LogConnections.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 22, instance.Settings.Flags.LogConnections.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 25, instance.Settings.Flags.LogTempFileSize.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 25, instance.Settings.Flags.LogTempFileSize.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 34, instance.Settings.Flags.LogDisconnections.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 34, instance.Settings.Flags.LogDisconnections.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 38, instance.Settings.Flags.LogLockWaits.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 38, instance.Settings.Flags.LogLockWaits.GetMetadata().Range().GetEndLine()) + +} diff --git a/pkg/iac/adapters/terraform/google/storage/adapt.go b/pkg/iac/adapters/terraform/google/storage/adapt.go new file mode 100644 index 000000000000..aeeaebcb6689 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/storage/adapt.go @@ -0,0 +1,162 @@ +package storage + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/google/storage" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Adapt(modules terraform.Modules) storage.Storage { + return storage.Storage{ + Buckets: (&adapter{modules: modules}).adaptBuckets(), + } +} + +type adapter struct { + modules terraform.Modules + bindings []parentedBinding + members []parentedMember + bindingMap terraform.ResourceIDResolutions + memberMap terraform.ResourceIDResolutions +} + +func (a *adapter) adaptBuckets() []storage.Bucket { + + a.bindingMap = a.modules.GetChildResourceIDMapByType("google_storage_bucket_iam_binding", "google_storage_bucket_iam_policy") + a.memberMap = a.modules.GetChildResourceIDMapByType("google_storage_bucket_iam_member") + + a.adaptMembers() + a.adaptBindings() + + var buckets []storage.Bucket + for _, module := range a.modules { + for _, resource := range module.GetResourcesByType(GoogleStorageBucket) { + buckets = append(buckets, a.adaptBucketResource(resource)) + } + } + + orphanage := storage.Bucket{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Name: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + Location: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + EnableUniformBucketLevelAccess: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + Members: nil, + Bindings: nil, + Encryption: storage.BucketEncryption{ + Metadata: iacTypes.NewUnmanagedMetadata(), + DefaultKMSKeyName: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + Logging: storage.BucketLogging{ + Metadata: iacTypes.NewUnmanagedMetadata(), + LogBucket: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + LogObjectPrefix: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + }, + Versioning: storage.BucketVersioning{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Enabled: iacTypes.BoolDefault(false, iacTypes.NewUnmanagedMetadata()), + }, + } + for _, orphanedBindingID := range a.bindingMap.Orphans() { + for _, binding := range a.bindings { + if binding.blockID == orphanedBindingID { + orphanage.Bindings = append(orphanage.Bindings, binding.bindings...) + break + } + } + } + for _, orphanedMemberID := range a.memberMap.Orphans() { + for _, member := range a.members { + if member.blockID == orphanedMemberID { + orphanage.Members = append(orphanage.Members, member.member) + break + } + } + } + if len(orphanage.Bindings) > 0 || len(orphanage.Members) > 0 { + buckets = append(buckets, orphanage) + } + + return buckets +} + +func (a *adapter) adaptBucketResource(resourceBlock *terraform.Block) storage.Bucket { + + nameAttr := resourceBlock.GetAttribute("name") + nameValue := nameAttr.AsStringValueOrDefault("", resourceBlock) + + locationAttr := resourceBlock.GetAttribute("location") + locationValue := locationAttr.AsStringValueOrDefault("", resourceBlock) + + // See https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/storage_bucket#uniform_bucket_level_access + ublaAttr := resourceBlock.GetAttribute("uniform_bucket_level_access") + ublaValue := ublaAttr.AsBoolValueOrDefault(false, resourceBlock) + + bucket := storage.Bucket{ + Metadata: resourceBlock.GetMetadata(), + Name: nameValue, + Location: locationValue, + EnableUniformBucketLevelAccess: ublaValue, + Members: nil, + Bindings: nil, + Encryption: storage.BucketEncryption{ + Metadata: resourceBlock.GetMetadata(), + DefaultKMSKeyName: iacTypes.StringDefault("", resourceBlock.GetMetadata()), + }, + Logging: storage.BucketLogging{ + Metadata: resourceBlock.GetMetadata(), + LogBucket: iacTypes.StringDefault("", resourceBlock.GetMetadata()), + LogObjectPrefix: iacTypes.StringDefault("", resourceBlock.GetMetadata()), + }, + Versioning: storage.BucketVersioning{ + Metadata: resourceBlock.GetMetadata(), + Enabled: iacTypes.BoolDefault(false, resourceBlock.GetMetadata()), + }, + } + + if encBlock := resourceBlock.GetBlock("encryption"); encBlock.IsNotNil() { + bucket.Encryption.Metadata = encBlock.GetMetadata() + kmsKeyNameAttr := encBlock.GetAttribute("default_kms_key_name") + bucket.Encryption.DefaultKMSKeyName = kmsKeyNameAttr.AsStringValueOrDefault("", encBlock) + } + + if logBlock := resourceBlock.GetBlock("logging"); logBlock.IsNotNil() { + bucket.Logging.Metadata = logBlock.GetMetadata() + bucket.Logging.LogBucket = logBlock.GetAttribute("log_bucket").AsStringValueOrDefault("", logBlock) + bucket.Logging.LogObjectPrefix = logBlock.GetAttribute("log_object_prefix").AsStringValueOrDefault("", logBlock) + } + + if versioningBlock := resourceBlock.GetBlock("versioning"); versioningBlock.IsNotNil() { + bucket.Versioning.Metadata = versioningBlock.GetMetadata() + bucket.Versioning.Enabled = versioningBlock.GetAttribute("enabled").AsBoolValueOrDefault(false, versioningBlock) + } + + var name string + if nameAttr.IsString() { + name = nameAttr.Value().AsString() + } + + for _, member := range a.members { + if member.bucketBlockID == resourceBlock.ID() { + bucket.Members = append(bucket.Members, member.member) + a.memberMap.Resolve(member.blockID) + continue + } + if name != "" && name == member.bucketID { + bucket.Members = append(bucket.Members, member.member) + a.memberMap.Resolve(member.blockID) + } + } + for _, binding := range a.bindings { + if binding.bucketBlockID == resourceBlock.ID() { + bucket.Bindings = append(bucket.Bindings, binding.bindings...) + a.bindingMap.Resolve(binding.blockID) + continue + } + if name != "" && name == binding.bucketID { + bucket.Bindings = append(bucket.Bindings, binding.bindings...) + a.bindingMap.Resolve(binding.blockID) + } + } + + return bucket +} diff --git a/pkg/iac/adapters/terraform/google/storage/adapt_test.go b/pkg/iac/adapters/terraform/google/storage/adapt_test.go new file mode 100644 index 000000000000..fb2c05f0cf31 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/storage/adapt_test.go @@ -0,0 +1,237 @@ +package storage + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/storage" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Adapt(t *testing.T) { + tests := []struct { + name string + terraform string + expected storage.Storage + }{ + { + name: "defined", + terraform: ` + resource "google_storage_bucket" "static-site" { + name = "image-store.com" + location = "EU" + uniform_bucket_level_access = true + + encryption { + default_kms_key_name = "default-kms-key-name" + } + } + + resource "google_storage_bucket_iam_binding" "binding" { + bucket = google_storage_bucket.static-site.name + role = "roles/storage.admin #1" + members = [ + "group:test@example.com", + ] + } + + resource "google_storage_bucket_iam_member" "example" { + member = "serviceAccount:test@example.com" + bucket = google_storage_bucket.static-site.name + role = "roles/storage.admin #2" + }`, + expected: storage.Storage{ + Buckets: []storage.Bucket{ + { + Name: iacTypes.StringTest("image-store.com"), + Location: iacTypes.StringTest("EU"), + EnableUniformBucketLevelAccess: iacTypes.BoolTest(true), + Bindings: []iam.Binding{ + { + Members: []iacTypes.StringValue{ + iacTypes.StringTest("group:test@example.com"), + }, + Role: iacTypes.StringTest("roles/storage.admin #1"), + }, + }, + Members: []iam.Member{ + { + Member: iacTypes.StringTest("serviceAccount:test@example.com"), + Role: iacTypes.StringTest("roles/storage.admin #2"), + }, + }, + Encryption: storage.BucketEncryption{ + DefaultKMSKeyName: iacTypes.StringTest("default-kms-key-name"), + }, + Logging: storage.BucketLogging{}, + Versioning: storage.BucketVersioning{}, + }, + }, + }, + }, + { + name: "defaults", + terraform: ` + resource "google_storage_bucket" "static-site" { + } + + resource "google_storage_bucket_iam_binding" "binding" { + bucket = google_storage_bucket.static-site.name + } + + resource "google_storage_bucket_iam_member" "example" { + bucket = google_storage_bucket.static-site.name + }`, + expected: storage.Storage{ + Buckets: []storage.Bucket{ + { + Bindings: []iam.Binding{ + {}, + }, + Members: []iam.Member{ + {}, + }, + Encryption: storage.BucketEncryption{}, + Logging: storage.BucketLogging{}, + Versioning: storage.BucketVersioning{}, + }, + }, + }, + }, + { + name: "with logging and versioning", + terraform: ` + resource "google_storage_bucket" "example" { + name = "example-bucket" + location = "US" + + logging { + log_bucket = "access-logs-bucket" + } + + versioning { + enabled = true + } + }`, + expected: storage.Storage{ + Buckets: []storage.Bucket{ + { + Name: iacTypes.StringTest("example-bucket"), + Location: iacTypes.StringTest("US"), + Encryption: storage.BucketEncryption{}, + Logging: storage.BucketLogging{ + LogBucket: iacTypes.StringTest("access-logs-bucket"), + }, + Versioning: storage.BucketVersioning{ + Enabled: iacTypes.BoolTest(true), + }, + }, + }, + }, + }, + { + name: "with logging including log object prefix", + terraform: ` + resource "google_storage_bucket" "example" { + name = "example-bucket" + location = "US" + + logging { + log_bucket = "access-logs-bucket" + log_object_prefix = "access-logs/" + } + }`, + expected: storage.Storage{ + Buckets: []storage.Bucket{ + { + Name: iacTypes.StringTest("example-bucket"), + Location: iacTypes.StringTest("US"), + Encryption: storage.BucketEncryption{}, + Logging: storage.BucketLogging{ + LogBucket: iacTypes.StringTest("access-logs-bucket"), + LogObjectPrefix: iacTypes.StringTest("access-logs/"), + }, + Versioning: storage.BucketVersioning{}, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := Adapt(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} + +func TestLines(t *testing.T) { + src := ` + resource "google_storage_bucket" "static-site" { + name = "image-store.com" + location = "EU" + uniform_bucket_level_access = true + } + + resource "google_storage_bucket_iam_binding" "binding" { + bucket = google_storage_bucket.static-site.name + role = "roles/storage.admin #1" + members = [ + "group:test@example.com", + ] + } + + resource "google_storage_bucket_iam_member" "example" { + member = "serviceAccount:test@example.com" + bucket = google_storage_bucket.static-site.name + role = "roles/storage.admin #2" + }` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Buckets, 1) + require.Len(t, adapted.Buckets[0].Bindings, 1) + require.Len(t, adapted.Buckets[0].Members, 1) + + bucket := adapted.Buckets[0] + binding := adapted.Buckets[0].Bindings[0] + member := adapted.Buckets[0].Members[0] + + assert.Equal(t, 2, bucket.Metadata.Range().GetStartLine()) + assert.Equal(t, 6, bucket.Metadata.Range().GetEndLine()) + + assert.Equal(t, 3, bucket.Name.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, bucket.Name.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, bucket.Location.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, bucket.Location.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 5, bucket.EnableUniformBucketLevelAccess.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, bucket.EnableUniformBucketLevelAccess.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 8, binding.Metadata.Range().GetStartLine()) + assert.Equal(t, 14, binding.Metadata.Range().GetEndLine()) + + assert.Equal(t, 10, binding.Role.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, binding.Role.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, binding.Members[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 13, binding.Members[0].GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 16, member.Metadata.Range().GetStartLine()) + assert.Equal(t, 20, member.Metadata.Range().GetEndLine()) + + assert.Equal(t, 17, member.Member.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 17, member.Member.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 19, member.Role.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 19, member.Role.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/google/storage/iam.go b/pkg/iac/adapters/terraform/google/storage/iam.go new file mode 100644 index 000000000000..6f6cdd000c94 --- /dev/null +++ b/pkg/iac/adapters/terraform/google/storage/iam.go @@ -0,0 +1,98 @@ +package storage + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/google/iam" + iamTypes "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" +) + +type parentedBinding struct { + blockID string + bucketID string + bucketBlockID string + bindings []iamTypes.Binding +} + +const GoogleStorageBucket = "google_storage_bucket" + +type parentedMember struct { + blockID string + bucketID string + bucketBlockID string + member iamTypes.Member +} + +func (a *adapter) adaptBindings() { + + for _, iamBlock := range a.modules.GetResourcesByType("google_storage_bucket_iam_policy") { + var parented parentedBinding + parented.blockID = iamBlock.ID() + + bucketAttr := iamBlock.GetAttribute("bucket") + if bucketAttr.IsString() { + parented.bucketID = bucketAttr.Value().AsString() + } + + if refBlock, err := a.modules.GetReferencedBlock(bucketAttr, iamBlock); err == nil { + if refBlock.TypeLabel() == GoogleStorageBucket { + parented.bucketBlockID = refBlock.ID() + } + } + + policyAttr := iamBlock.GetAttribute("policy_data") + if policyAttr.IsNil() { + continue + } + + policyBlock, err := a.modules.GetReferencedBlock(policyAttr, iamBlock) + if err != nil { + continue + } + + parented.bindings = iam.ParsePolicyBlock(policyBlock) + a.bindings = append(a.bindings, parented) + } + + for _, iamBlock := range a.modules.GetResourcesByType("google_storage_bucket_iam_binding") { + + var parented parentedBinding + parented.blockID = iamBlock.ID() + parented.bindings = []iamTypes.Binding{iam.AdaptBinding(iamBlock, a.modules)} + + bucketAttr := iamBlock.GetAttribute("bucket") + if bucketAttr.IsString() { + parented.bucketID = bucketAttr.Value().AsString() + } + + if refBlock, err := a.modules.GetReferencedBlock(bucketAttr, iamBlock); err == nil { + if refBlock.TypeLabel() == GoogleStorageBucket { + parented.bucketBlockID = refBlock.ID() + } + } + + a.bindings = append(a.bindings, parented) + } +} + +func (a *adapter) adaptMembers() { + + for _, iamBlock := range a.modules.GetResourcesByType("google_storage_bucket_iam_member") { + + var parented parentedMember + parented.blockID = iamBlock.ID() + parented.member = iam.AdaptMember(iamBlock, a.modules) + + bucketAttr := iamBlock.GetAttribute("bucket") + if bucketAttr.IsString() { + parented.bucketID = bucketAttr.Value().AsString() + } + + if refBlock, err := a.modules.GetReferencedBlock(bucketAttr, iamBlock); err == nil { + if refBlock.TypeLabel() == GoogleStorageBucket { + parented.bucketBlockID = refBlock.ID() + } + } + + a.members = append(a.members, parented) + } + +} diff --git a/pkg/iac/adapters/terraform/kubernetes/adapt.go b/pkg/iac/adapters/terraform/kubernetes/adapt.go new file mode 100644 index 000000000000..52eed9db4052 --- /dev/null +++ b/pkg/iac/adapters/terraform/kubernetes/adapt.go @@ -0,0 +1,123 @@ +package kubernetes + +import ( + "regexp" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/providers/kubernetes" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +var versionRegex = regexp.MustCompile(`^v\d+(beta\d+)?$`) + +func Adapt(modules terraform.Modules) kubernetes.Kubernetes { + return kubernetes.Kubernetes{ + NetworkPolicies: adaptNetworkPolicies(modules), + } +} + +func adaptNetworkPolicies(modules terraform.Modules) []kubernetes.NetworkPolicy { + var networkPolicies []kubernetes.NetworkPolicy + for _, module := range modules { + for _, resource := range getBlocksIgnoreVersion(module, "resource", "kubernetes_network_policy") { + networkPolicies = append(networkPolicies, adaptNetworkPolicy(resource)) + } + } + return networkPolicies +} + +func adaptNetworkPolicy(resourceBlock *terraform.Block) kubernetes.NetworkPolicy { + + policy := kubernetes.NetworkPolicy{ + Metadata: resourceBlock.GetMetadata(), + Spec: kubernetes.NetworkPolicySpec{ + Metadata: resourceBlock.GetMetadata(), + Egress: kubernetes.Egress{ + Metadata: resourceBlock.GetMetadata(), + Ports: nil, + DestinationCIDRs: nil, + }, + Ingress: kubernetes.Ingress{ + Metadata: resourceBlock.GetMetadata(), + Ports: nil, + SourceCIDRs: nil, + }, + }, + } + + if specBlock := resourceBlock.GetBlock("spec"); specBlock.IsNotNil() { + if egressBlock := specBlock.GetBlock("egress"); egressBlock.IsNotNil() { + policy.Spec.Egress.Metadata = egressBlock.GetMetadata() + for _, port := range egressBlock.GetBlocks("ports") { + numberAttr := port.GetAttribute("number") + numberVal := numberAttr.AsStringValueOrDefault("", port) + + protocolAttr := port.GetAttribute("protocol") + protocolVal := protocolAttr.AsStringValueOrDefault("", port) + + policy.Spec.Egress.Ports = append(policy.Spec.Egress.Ports, kubernetes.Port{ + Metadata: port.GetMetadata(), + Number: numberVal, + Protocol: protocolVal, + }) + } + + for _, to := range egressBlock.GetBlocks("to") { + cidrAtrr := to.GetBlock("ip_block").GetAttribute("cidr") + cidrVal := cidrAtrr.AsStringValueOrDefault("", to) + + policy.Spec.Egress.DestinationCIDRs = append(policy.Spec.Egress.DestinationCIDRs, cidrVal) + } + } + + if ingressBlock := specBlock.GetBlock("ingress"); ingressBlock.IsNotNil() { + policy.Spec.Ingress.Metadata = ingressBlock.GetMetadata() + for _, port := range ingressBlock.GetBlocks("ports") { + numberAttr := port.GetAttribute("number") + numberVal := numberAttr.AsStringValueOrDefault("", port) + + protocolAttr := port.GetAttribute("protocol") + protocolVal := protocolAttr.AsStringValueOrDefault("", port) + + policy.Spec.Ingress.Ports = append(policy.Spec.Ingress.Ports, kubernetes.Port{ + Metadata: port.GetMetadata(), + Number: numberVal, + Protocol: protocolVal, + }) + } + + for _, from := range ingressBlock.GetBlocks("from") { + cidrAtrr := from.GetBlock("ip_block").GetAttribute("cidr") + cidrVal := cidrAtrr.AsStringValueOrDefault("", from) + + policy.Spec.Ingress.SourceCIDRs = append(policy.Spec.Ingress.SourceCIDRs, cidrVal) + } + } + } + + return policy +} + +// https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/guides/versioned-resources +func getBlocksIgnoreVersion(module *terraform.Module, blockType, resourceType string) terraform.Blocks { + var res terraform.Blocks + for _, block := range module.GetBlocks().OfType(blockType) { + if isMatchingTypeLabel(block.TypeLabel(), resourceType) { + res = append(res, block) + } + } + return res +} + +func isMatchingTypeLabel(typeLabel, resourceType string) bool { + if typeLabel == resourceType { + return true + } + + versionPart, found := strings.CutPrefix(typeLabel, resourceType+"_") + if !found { + return false + } + + return versionRegex.MatchString(versionPart) +} diff --git a/pkg/iac/adapters/terraform/kubernetes/adapt_test.go b/pkg/iac/adapters/terraform/kubernetes/adapt_test.go new file mode 100644 index 000000000000..eea390bd2e01 --- /dev/null +++ b/pkg/iac/adapters/terraform/kubernetes/adapt_test.go @@ -0,0 +1,60 @@ +package kubernetes + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsMatchingTypeLabel(t *testing.T) { + tests := []struct { + name string + typeLabel string + resourceType string + expected bool + }{ + { + name: "without version", + typeLabel: "kubernetes_network_policy", + resourceType: "kubernetes_network_policy", + expected: true, + }, + { + name: "v1", + typeLabel: "kubernetes_network_policy_v1", + resourceType: "kubernetes_network_policy", + expected: true, + }, + { + name: "beta version", + typeLabel: "kubernetes_horizontal_pod_autoscaler_v2beta2", + resourceType: "kubernetes_horizontal_pod_autoscaler", + expected: true, + }, + { + name: "another type of resource", + typeLabel: "kubernetes_network_policy", + resourceType: "kubernetes_horizontal_pod_autoscaler", + expected: false, + }, + { + name: "similar resource type", + typeLabel: "kubernetes_network_policy_test_v1", + resourceType: "kubernetes_network_policy", + expected: false, + }, + { + name: "empty resource type", + typeLabel: "kubernetes_network_policy_test_v1", + resourceType: "", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := isMatchingTypeLabel(tt.typeLabel, tt.resourceType) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/computing/adapt.go b/pkg/iac/adapters/terraform/nifcloud/computing/adapt.go new file mode 100644 index 000000000000..399f492ba4e7 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/computing/adapt.go @@ -0,0 +1,16 @@ +package computing + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/computing" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) computing.Computing { + + sgAdapter := sgAdapter{sgRuleIDs: modules.GetChildResourceIDMapByType("nifcloud_security_group_rule")} + + return computing.Computing{ + SecurityGroups: sgAdapter.adaptSecurityGroups(modules), + Instances: adaptInstances(modules), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/computing/adapt_test.go b/pkg/iac/adapters/terraform/nifcloud/computing/adapt_test.go new file mode 100644 index 000000000000..88af58c58674 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/computing/adapt_test.go @@ -0,0 +1,61 @@ +package computing + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" +) + +func TestLines(t *testing.T) { + src := ` +resource "nifcloud_instance" "example" { + security_group = nifcloud_security_group.example.group_name + + network_interface { + network_id = "net-COMMON_PRIVATE" + } +} + +resource "nifcloud_security_group" "example" { + group_name = "example" + description = "memo" +} + +resource "nifcloud_security_group_rule" "example" { + type = "IN" + security_group_names = [nifcloud_security_group.example.group_name] + from_port = 22 + to_port = 22 + protocol = "TCP" + description = "memo" + cidr_ip = "1.2.3.4/32" +} +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Instances, 1) + require.Len(t, adapted.SecurityGroups, 1) + + instance := adapted.Instances[0] + sg := adapted.SecurityGroups[0] + + assert.Equal(t, 3, instance.SecurityGroup.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, instance.SecurityGroup.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, instance.NetworkInterfaces[0].NetworkID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, instance.NetworkInterfaces[0].NetworkID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 12, sg.Description.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 12, sg.Description.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 21, sg.IngressRules[0].Description.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 21, sg.IngressRules[0].Description.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 22, sg.IngressRules[0].CIDR.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 22, sg.IngressRules[0].CIDR.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/nifcloud/computing/instance.go b/pkg/iac/adapters/terraform/nifcloud/computing/instance.go new file mode 100644 index 000000000000..ebbce94439dd --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/computing/instance.go @@ -0,0 +1,35 @@ +package computing + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/computing" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func adaptInstances(modules terraform.Modules) []computing.Instance { + var instances []computing.Instance + + for _, resource := range modules.GetResourcesByType("nifcloud_instance") { + instances = append(instances, adaptInstance(resource)) + } + return instances +} + +func adaptInstance(resource *terraform.Block) computing.Instance { + var networkInterfaces []computing.NetworkInterface + networkInterfaceBlocks := resource.GetBlocks("network_interface") + for _, networkInterfaceBlock := range networkInterfaceBlocks { + networkInterfaces = append( + networkInterfaces, + computing.NetworkInterface{ + Metadata: networkInterfaceBlock.GetMetadata(), + NetworkID: networkInterfaceBlock.GetAttribute("network_id").AsStringValueOrDefault("", resource), + }, + ) + } + + return computing.Instance{ + Metadata: resource.GetMetadata(), + SecurityGroup: resource.GetAttribute("security_group").AsStringValueOrDefault("", resource), + NetworkInterfaces: networkInterfaces, + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/computing/instance_test.go b/pkg/iac/adapters/terraform/nifcloud/computing/instance_test.go new file mode 100644 index 000000000000..00fdc371fb06 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/computing/instance_test.go @@ -0,0 +1,61 @@ +package computing + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/computing" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptInstances(t *testing.T) { + tests := []struct { + name string + terraform string + expected []computing.Instance + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_instance" "my_example" { + security_group = "example-security-group" + network_interface { + network_id = "net-COMMON_PRIVATE" + } + } +`, + expected: []computing.Instance{{ + SecurityGroup: iacTypes.StringTest("example-security-group"), + NetworkInterfaces: []computing.NetworkInterface{ + { + NetworkID: iacTypes.StringTest("net-COMMON_PRIVATE"), + }, + }, + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_instance" "my_example" { + network_interface { + } + } +`, + + expected: []computing.Instance{{ + NetworkInterfaces: []computing.NetworkInterface{ + {}, + }, + }}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptInstances(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/computing/security_group.go b/pkg/iac/adapters/terraform/nifcloud/computing/security_group.go new file mode 100644 index 000000000000..66c0ec51a73b --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/computing/security_group.go @@ -0,0 +1,76 @@ +package computing + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/computing" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type sgAdapter struct { + sgRuleIDs terraform.ResourceIDResolutions +} + +func (a *sgAdapter) adaptSecurityGroups(modules terraform.Modules) []computing.SecurityGroup { + var securityGroups []computing.SecurityGroup + for _, resource := range modules.GetResourcesByType("nifcloud_security_group") { + securityGroups = append(securityGroups, a.adaptSecurityGroup(resource, modules)) + } + orphanResources := modules.GetResourceByIDs(a.sgRuleIDs.Orphans()...) + if len(orphanResources) > 0 { + orphanage := computing.SecurityGroup{ + Metadata: iacTypes.NewUnmanagedMetadata(), + Description: iacTypes.StringDefault("", iacTypes.NewUnmanagedMetadata()), + IngressRules: nil, + } + for _, sgRule := range orphanResources { + if sgRule.GetAttribute("type").Equals("IN") { + orphanage.IngressRules = append(orphanage.IngressRules, adaptSGRule(sgRule, modules)) + } + if sgRule.GetAttribute("type").Equals("OUT") { + orphanage.EgressRules = append(orphanage.EgressRules, adaptSGRule(sgRule, modules)) + } + } + securityGroups = append(securityGroups, orphanage) + } + + return securityGroups +} + +func (a *sgAdapter) adaptSecurityGroup(resource *terraform.Block, module terraform.Modules) computing.SecurityGroup { + var ingressRules, egressRules []computing.SecurityGroupRule + + descriptionAttr := resource.GetAttribute("description") + descriptionVal := descriptionAttr.AsStringValueOrDefault("", resource) + + rulesBlocks := module.GetReferencingResources(resource, "nifcloud_security_group_rule", "security_group_names") + for _, ruleBlock := range rulesBlocks { + a.sgRuleIDs.Resolve(ruleBlock.ID()) + if ruleBlock.GetAttribute("type").Equals("IN") { + ingressRules = append(ingressRules, adaptSGRule(ruleBlock, module)) + } + if ruleBlock.GetAttribute("type").Equals("OUT") { + egressRules = append(egressRules, adaptSGRule(ruleBlock, module)) + } + } + + return computing.SecurityGroup{ + Metadata: resource.GetMetadata(), + Description: descriptionVal, + IngressRules: ingressRules, + EgressRules: egressRules, + } +} + +func adaptSGRule(resource *terraform.Block, _ terraform.Modules) computing.SecurityGroupRule { + ruleDescAttr := resource.GetAttribute("description") + ruleDescVal := ruleDescAttr.AsStringValueOrDefault("", resource) + + cidrAttr := resource.GetAttribute("cidr_ip") + cidrVal := cidrAttr.AsStringValueOrDefault("", resource) + + return computing.SecurityGroupRule{ + Metadata: resource.GetMetadata(), + Description: ruleDescVal, + CIDR: cidrVal, + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/computing/security_group_test.go b/pkg/iac/adapters/terraform/nifcloud/computing/security_group_test.go new file mode 100644 index 000000000000..f1a9344b176a --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/computing/security_group_test.go @@ -0,0 +1,75 @@ +package computing + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/computing" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptSecurityGroups(t *testing.T) { + tests := []struct { + name string + terraform string + expected []computing.SecurityGroup + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_security_group" "example" { + group_name = "example" + description = "memo" + } + + resource "nifcloud_security_group_rule" "example" { + type = "IN" + security_group_names = [nifcloud_security_group.example.group_name] + from_port = 22 + to_port = 22 + protocol = "TCP" + description = "memo" + cidr_ip = "1.2.3.4/32" + } +`, + expected: []computing.SecurityGroup{{ + Description: iacTypes.StringTest("memo"), + IngressRules: []computing.SecurityGroupRule{ + { + CIDR: iacTypes.StringTest("1.2.3.4/32"), + Description: iacTypes.StringTest("memo"), + }, + }, + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_security_group" "example" { + } + + resource "nifcloud_security_group_rule" "example" { + type = "IN" + security_group_names = [nifcloud_security_group.example.group_name] + } + +`, + + expected: []computing.SecurityGroup{{ + IngressRules: []computing.SecurityGroupRule{ + {}, + }, + }}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + sgAdapter := sgAdapter{sgRuleIDs: modules.GetChildResourceIDMapByType("nifcloud_security_group_rule")} + adapted := sgAdapter.adaptSecurityGroups(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/dns/adapt.go b/pkg/iac/adapters/terraform/nifcloud/dns/adapt.go new file mode 100644 index 000000000000..c1118e98ef23 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/dns/adapt.go @@ -0,0 +1,12 @@ +package dns + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/dns" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) dns.DNS { + return dns.DNS{ + Records: adaptRecords(modules), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/dns/adapt_test.go b/pkg/iac/adapters/terraform/nifcloud/dns/adapt_test.go new file mode 100644 index 000000000000..b3c0f535667b --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/dns/adapt_test.go @@ -0,0 +1,32 @@ +package dns + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" +) + +func TestLines(t *testing.T) { + src := ` +resource "nifcloud_dns_record" "example" { + type = "A" + record = "example-record" +} +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.Records, 1) + + record := adapted.Records[0] + + assert.Equal(t, 3, record.Type.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, record.Type.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, record.Record.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, record.Record.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/nifcloud/dns/record.go b/pkg/iac/adapters/terraform/nifcloud/dns/record.go new file mode 100644 index 000000000000..84e61889cac0 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/dns/record.go @@ -0,0 +1,23 @@ +package dns + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/dns" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func adaptRecords(modules terraform.Modules) []dns.Record { + var records []dns.Record + + for _, resource := range modules.GetResourcesByType("nifcloud_dns_record") { + records = append(records, adaptRecord(resource)) + } + return records +} + +func adaptRecord(resource *terraform.Block) dns.Record { + return dns.Record{ + Metadata: resource.GetMetadata(), + Record: resource.GetAttribute("record").AsStringValueOrDefault("", resource), + Type: resource.GetAttribute("type").AsStringValueOrDefault("", resource), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/dns/record_test.go b/pkg/iac/adapters/terraform/nifcloud/dns/record_test.go new file mode 100644 index 000000000000..18b527d393e6 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/dns/record_test.go @@ -0,0 +1,49 @@ +package dns + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/dns" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptRecords(t *testing.T) { + tests := []struct { + name string + terraform string + expected []dns.Record + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_dns_record" "example" { + type = "A" + record = "example-record" + } +`, + expected: []dns.Record{{ + Type: iacTypes.StringTest("A"), + Record: iacTypes.StringTest("example-record"), + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_dns_record" "example" { + } +`, + + expected: []dns.Record{{}}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptRecords(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/nas/adapt.go b/pkg/iac/adapters/terraform/nifcloud/nas/adapt.go new file mode 100644 index 000000000000..c1b60fc551e2 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/nas/adapt.go @@ -0,0 +1,13 @@ +package nas + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/nas" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) nas.NAS { + return nas.NAS{ + NASSecurityGroups: adaptNASSecurityGroups(modules), + NASInstances: adaptNASInstances(modules), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/nas/adapt_test.go b/pkg/iac/adapters/terraform/nifcloud/nas/adapt_test.go new file mode 100644 index 000000000000..bbd18e71a7d2 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/nas/adapt_test.go @@ -0,0 +1,44 @@ +package nas + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" +) + +func TestLines(t *testing.T) { + src := ` +resource "nifcloud_nas_instance" "example" { + network_id = "example-network" +} + +resource "nifcloud_nas_security_group" "example" { + description = "memo" + + rule { + cidr_ip = "0.0.0.0/0" + } +} +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.NASInstances, 1) + require.Len(t, adapted.NASSecurityGroups, 1) + + nasInstance := adapted.NASInstances[0] + nasSecurityGroup := adapted.NASSecurityGroups[0] + + assert.Equal(t, 3, nasInstance.NetworkID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, nasInstance.NetworkID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 7, nasSecurityGroup.Description.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, nasSecurityGroup.Description.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 10, nasSecurityGroup.CIDRs[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, nasSecurityGroup.CIDRs[0].GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/nifcloud/nas/nas_instance.go b/pkg/iac/adapters/terraform/nifcloud/nas/nas_instance.go new file mode 100644 index 000000000000..e04024f0c4dd --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/nas/nas_instance.go @@ -0,0 +1,22 @@ +package nas + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/nas" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func adaptNASInstances(modules terraform.Modules) []nas.NASInstance { + var nasInstances []nas.NASInstance + + for _, resource := range modules.GetResourcesByType("nifcloud_nas_instance") { + nasInstances = append(nasInstances, adaptNASInstance(resource)) + } + return nasInstances +} + +func adaptNASInstance(resource *terraform.Block) nas.NASInstance { + return nas.NASInstance{ + Metadata: resource.GetMetadata(), + NetworkID: resource.GetAttribute("network_id").AsStringValueOrDefault("net-COMMON_PRIVATE", resource), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/nas/nas_instance_test.go b/pkg/iac/adapters/terraform/nifcloud/nas/nas_instance_test.go new file mode 100644 index 000000000000..b515b80f3aaa --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/nas/nas_instance_test.go @@ -0,0 +1,49 @@ +package nas + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/nas" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptNASInstances(t *testing.T) { + tests := []struct { + name string + terraform string + expected []nas.NASInstance + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_nas_instance" "example" { + network_id = "example-network" + } +`, + expected: []nas.NASInstance{{ + NetworkID: iacTypes.StringTest("example-network"), + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_nas_instance" "example" { + } +`, + + expected: []nas.NASInstance{{ + NetworkID: iacTypes.StringTest("net-COMMON_PRIVATE"), + }}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptNASInstances(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/nas/nas_security_group.go b/pkg/iac/adapters/terraform/nifcloud/nas/nas_security_group.go new file mode 100644 index 000000000000..468cbddce309 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/nas/nas_security_group.go @@ -0,0 +1,30 @@ +package nas + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/nas" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptNASSecurityGroups(modules terraform.Modules) []nas.NASSecurityGroup { + var nasSecurityGroups []nas.NASSecurityGroup + + for _, resource := range modules.GetResourcesByType("nifcloud_nas_security_group") { + nasSecurityGroups = append(nasSecurityGroups, adaptNASSecurityGroup(resource)) + } + return nasSecurityGroups +} + +func adaptNASSecurityGroup(resource *terraform.Block) nas.NASSecurityGroup { + var cidrs []iacTypes.StringValue + + for _, rule := range resource.GetBlocks("rule") { + cidrs = append(cidrs, rule.GetAttribute("cidr_ip").AsStringValueOrDefault("", resource)) + } + + return nas.NASSecurityGroup{ + Metadata: resource.GetMetadata(), + Description: resource.GetAttribute("description").AsStringValueOrDefault("", resource), + CIDRs: cidrs, + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/nas/nas_security_group_test.go b/pkg/iac/adapters/terraform/nifcloud/nas/nas_security_group_test.go new file mode 100644 index 000000000000..394ceb4f2add --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/nas/nas_security_group_test.go @@ -0,0 +1,60 @@ +package nas + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/nas" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptNASSecurityGroups(t *testing.T) { + tests := []struct { + name string + terraform string + expected []nas.NASSecurityGroup + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_nas_security_group" "example" { + description = "memo" + + rule { + cidr_ip = "0.0.0.0/0" + } + } +`, + expected: []nas.NASSecurityGroup{{ + Description: iacTypes.StringTest("memo"), + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest("0.0.0.0/0"), + }, + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_nas_security_group" "example" { + rule { + } + } +`, + + expected: []nas.NASSecurityGroup{{ + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest(""), + }, + }}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptNASSecurityGroups(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/adapt.go b/pkg/iac/adapters/terraform/nifcloud/network/adapt.go new file mode 100644 index 000000000000..b3006abcd1d8 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/adapt.go @@ -0,0 +1,16 @@ +package network + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) network.Network { + + return network.Network{ + ElasticLoadBalancers: adaptElasticLoadBalancers(modules), + LoadBalancers: adaptLoadBalancers(modules), + Routers: adaptRouters(modules), + VpnGateways: adaptVpnGateways(modules), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/adapt_test.go b/pkg/iac/adapters/terraform/nifcloud/network/adapt_test.go new file mode 100644 index 000000000000..44af9f204343 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/adapt_test.go @@ -0,0 +1,83 @@ +package network + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" +) + +func TestLines(t *testing.T) { + src := ` +resource "nifcloud_elb" "example" { + protocol = "HTTP" + + network_interface { + network_id = "net-COMMON_PRIVATE" + is_vip_network = false + } +} + +resource "nifcloud_load_balancer" "example" { + ssl_policy_id = "example-ssl-policy-id" + load_balancer_port = 8080 +} + +resource "nifcloud_router" "example" { + security_group = nifcloud_security_group.example.group_name + + network_interface { + network_id = "net-COMMON_PRIVATE" + } +} + +resource "nifcloud_security_group" "example" { + group_name = "example" + description = "memo" +} + +resource "nifcloud_vpn_gateway" "example" { + security_group = nifcloud_security_group.example.group_name +} +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.ElasticLoadBalancers, 1) + require.Len(t, adapted.LoadBalancers, 1) + require.Len(t, adapted.Routers, 1) + require.Len(t, adapted.VpnGateways, 1) + + elb := adapted.ElasticLoadBalancers[0] + lb := adapted.LoadBalancers[0] + router := adapted.Routers[0] + vpngw := adapted.VpnGateways[0] + + assert.Equal(t, 3, elb.Listeners[0].Protocol.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, elb.Listeners[0].Protocol.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, elb.NetworkInterfaces[0].NetworkID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, elb.NetworkInterfaces[0].NetworkID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 7, elb.NetworkInterfaces[0].IsVipNetwork.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, elb.NetworkInterfaces[0].IsVipNetwork.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 12, lb.Listeners[0].TLSPolicy.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 12, lb.Listeners[0].TLSPolicy.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 13, lb.Listeners[0].Protocol.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 13, lb.Listeners[0].Protocol.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 17, router.SecurityGroup.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 17, router.SecurityGroup.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 20, router.NetworkInterfaces[0].NetworkID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 20, router.NetworkInterfaces[0].NetworkID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 30, vpngw.SecurityGroup.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 30, vpngw.SecurityGroup.GetMetadata().Range().GetEndLine()) + +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/elastic_load_balancer.go b/pkg/iac/adapters/terraform/nifcloud/network/elastic_load_balancer.go new file mode 100644 index 000000000000..89478aadea6e --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/elastic_load_balancer.go @@ -0,0 +1,50 @@ +package network + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func adaptElasticLoadBalancers(modules terraform.Modules) []network.ElasticLoadBalancer { + var elasticLoadBalancers []network.ElasticLoadBalancer + + for _, resource := range modules.GetResourcesByType("nifcloud_elb") { + elasticLoadBalancers = append(elasticLoadBalancers, adaptElasticLoadBalancer(resource, modules)) + } + return elasticLoadBalancers +} + +func adaptElasticLoadBalancer(resource *terraform.Block, modules terraform.Modules) network.ElasticLoadBalancer { + var listeners []network.ElasticLoadBalancerListener + var networkInterfaces []network.NetworkInterface + + networkInterfaceBlocks := resource.GetBlocks("network_interface") + for _, networkInterfaceBlock := range networkInterfaceBlocks { + networkInterfaces = append( + networkInterfaces, + network.NetworkInterface{ + Metadata: networkInterfaceBlock.GetMetadata(), + NetworkID: networkInterfaceBlock.GetAttribute("network_id").AsStringValueOrDefault("", resource), + IsVipNetwork: networkInterfaceBlock.GetAttribute("is_vip_network").AsBoolValueOrDefault(true, resource), + }, + ) + } + + listeners = append(listeners, adaptElasticLoadBalancerListener(resource)) + for _, listenerBlock := range modules.GetReferencingResources(resource, "nifcloud_elb_listener", "elb_id") { + listeners = append(listeners, adaptElasticLoadBalancerListener(listenerBlock)) + } + + return network.ElasticLoadBalancer{ + Metadata: resource.GetMetadata(), + NetworkInterfaces: networkInterfaces, + Listeners: listeners, + } +} + +func adaptElasticLoadBalancerListener(resource *terraform.Block) network.ElasticLoadBalancerListener { + return network.ElasticLoadBalancerListener{ + Metadata: resource.GetMetadata(), + Protocol: resource.GetAttribute("protocol").AsStringValueOrDefault("", resource), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/elastic_load_balancer_test.go b/pkg/iac/adapters/terraform/nifcloud/network/elastic_load_balancer_test.go new file mode 100644 index 000000000000..10d7cf9e1e6e --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/elastic_load_balancer_test.go @@ -0,0 +1,78 @@ +package network + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptElasticLoadBalancers(t *testing.T) { + tests := []struct { + name string + terraform string + expected []network.ElasticLoadBalancer + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_elb" "example" { + protocol = "HTTP" + + network_interface { + network_id = "net-COMMON_PRIVATE" + is_vip_network = false + } + } + + resource "nifcloud_elb_listener" "example" { + elb_id = nifcloud_elb.example.id + protocol = "HTTPS" + } +`, + expected: []network.ElasticLoadBalancer{{ + NetworkInterfaces: []network.NetworkInterface{ + { + NetworkID: iacTypes.StringTest("net-COMMON_PRIVATE"), + }, + }, + Listeners: []network.ElasticLoadBalancerListener{ + { + Protocol: iacTypes.StringTest("HTTP"), + }, + { + Protocol: iacTypes.StringTest("HTTPS"), + }, + }, + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_elb" "example" { + network_interface { + } + } +`, + + expected: []network.ElasticLoadBalancer{{ + NetworkInterfaces: []network.NetworkInterface{ + { + IsVipNetwork: iacTypes.BoolTest(true), + }, + }, + Listeners: []network.ElasticLoadBalancerListener{{}}, + }}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptElasticLoadBalancers(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/load_balancer.go b/pkg/iac/adapters/terraform/nifcloud/network/load_balancer.go new file mode 100644 index 000000000000..d137f799756d --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/load_balancer.go @@ -0,0 +1,67 @@ +package network + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptLoadBalancers(modules terraform.Modules) []network.LoadBalancer { + var loadBalancers []network.LoadBalancer + + for _, resource := range modules.GetResourcesByType("nifcloud_load_balancer") { + loadBalancers = append(loadBalancers, adaptLoadBalancer(resource, modules)) + } + + return loadBalancers +} + +func adaptLoadBalancer(resource *terraform.Block, modules terraform.Modules) network.LoadBalancer { + var listeners []network.LoadBalancerListener + + listeners = append(listeners, adaptListener(resource)) + for _, listenerBlock := range modules.GetReferencingResources(resource, "nifcloud_load_balancer_listener", "load_balancer_name") { + listeners = append(listeners, adaptListener(listenerBlock)) + } + + return network.LoadBalancer{ + Metadata: resource.GetMetadata(), + Listeners: listeners, + } +} + +func adaptListener(resource *terraform.Block) network.LoadBalancerListener { + protocolVal := iacTypes.String("", resource.GetMetadata()) + policyVal := iacTypes.String("", resource.GetMetadata()) + + portAttr := resource.GetAttribute("load_balancer_port") + if portAttr.IsNotNil() && portAttr.IsNumber() { + port := portAttr.AsNumber() + switch port { + case 21: + protocolVal = iacTypes.String("FTP", portAttr.GetMetadata()) + case 80: + protocolVal = iacTypes.String("HTTP", portAttr.GetMetadata()) + case 443: + protocolVal = iacTypes.String("HTTPS", portAttr.GetMetadata()) + default: + protocolVal = iacTypes.String("custom", portAttr.GetMetadata()) + } + } + + policyIDAttr := resource.GetAttribute("ssl_policy_id") + if policyIDAttr.IsNotNil() && policyIDAttr.IsString() { + policyVal = policyIDAttr.AsStringValueOrDefault("", resource) + } + + policyNameAttr := resource.GetAttribute("ssl_policy_name") + if policyNameAttr.IsNotNil() && policyNameAttr.IsString() { + policyVal = policyNameAttr.AsStringValueOrDefault("", resource) + } + + return network.LoadBalancerListener{ + Metadata: resource.GetMetadata(), + Protocol: protocolVal, + TLSPolicy: policyVal, + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/load_balancer_test.go b/pkg/iac/adapters/terraform/nifcloud/network/load_balancer_test.go new file mode 100644 index 000000000000..77615a807543 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/load_balancer_test.go @@ -0,0 +1,67 @@ +package network + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptLoadBalancers(t *testing.T) { + tests := []struct { + name string + terraform string + expected []network.LoadBalancer + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_load_balancer" "example" { + load_balancer_name = "example" + load_balancer_port = 80 + ssl_policy_id = "example-ssl-policy-id" + } + + resource "nifcloud_load_balancer_listener" "example" { + load_balancer_name = nifcloud_load_balancer.example.load_balancer_name + load_balancer_port = 443 + ssl_policy_name = "example-ssl-policy-name" + } + +`, + expected: []network.LoadBalancer{{ + Listeners: []network.LoadBalancerListener{ + { + TLSPolicy: iacTypes.StringTest("example-ssl-policy-id"), + Protocol: iacTypes.StringTest("HTTP"), + }, + { + TLSPolicy: iacTypes.StringTest("example-ssl-policy-name"), + Protocol: iacTypes.StringTest("HTTPS"), + }, + }, + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_load_balancer" "example" { + } +`, + + expected: []network.LoadBalancer{{ + Listeners: []network.LoadBalancerListener{{}}, + }}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptLoadBalancers(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/router.go b/pkg/iac/adapters/terraform/nifcloud/network/router.go new file mode 100644 index 000000000000..d75595279d7d --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/router.go @@ -0,0 +1,37 @@ +package network + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptRouters(modules terraform.Modules) []network.Router { + var routers []network.Router + + for _, resource := range modules.GetResourcesByType("nifcloud_router") { + routers = append(routers, adaptRouter(resource)) + } + return routers +} + +func adaptRouter(resource *terraform.Block) network.Router { + var networkInterfaces []network.NetworkInterface + networkInterfaceBlocks := resource.GetBlocks("network_interface") + for _, networkInterfaceBlock := range networkInterfaceBlocks { + networkInterfaces = append( + networkInterfaces, + network.NetworkInterface{ + Metadata: networkInterfaceBlock.GetMetadata(), + NetworkID: networkInterfaceBlock.GetAttribute("network_id").AsStringValueOrDefault("", resource), + IsVipNetwork: types.Bool(false, networkInterfaceBlock.GetMetadata()), + }, + ) + } + + return network.Router{ + Metadata: resource.GetMetadata(), + SecurityGroup: resource.GetAttribute("security_group").AsStringValueOrDefault("", resource), + NetworkInterfaces: networkInterfaces, + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/router_test.go b/pkg/iac/adapters/terraform/nifcloud/network/router_test.go new file mode 100644 index 000000000000..3ec5abeb2d84 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/router_test.go @@ -0,0 +1,61 @@ +package network + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptRouters(t *testing.T) { + tests := []struct { + name string + terraform string + expected []network.Router + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_router" "example" { + security_group = "example-security-group" + network_interface { + network_id = "net-COMMON_PRIVATE" + } + } +`, + expected: []network.Router{{ + SecurityGroup: iacTypes.StringTest("example-security-group"), + NetworkInterfaces: []network.NetworkInterface{ + { + NetworkID: iacTypes.StringTest("net-COMMON_PRIVATE"), + }, + }, + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_router" "example" { + network_interface { + } + } +`, + + expected: []network.Router{{ + NetworkInterfaces: []network.NetworkInterface{ + {}, + }, + }}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptRouters(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/vpn_gateway.go b/pkg/iac/adapters/terraform/nifcloud/network/vpn_gateway.go new file mode 100644 index 000000000000..dcb6812a1ef7 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/vpn_gateway.go @@ -0,0 +1,22 @@ +package network + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func adaptVpnGateways(modules terraform.Modules) []network.VpnGateway { + var vpnGateways []network.VpnGateway + + for _, resource := range modules.GetResourcesByType("nifcloud_vpn_gateway") { + vpnGateways = append(vpnGateways, adaptVpnGateway(resource)) + } + return vpnGateways +} + +func adaptVpnGateway(resource *terraform.Block) network.VpnGateway { + return network.VpnGateway{ + Metadata: resource.GetMetadata(), + SecurityGroup: resource.GetAttribute("security_group").AsStringValueOrDefault("", resource), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/network/vpn_gateway_test.go b/pkg/iac/adapters/terraform/nifcloud/network/vpn_gateway_test.go new file mode 100644 index 000000000000..66fb1cf1c3b2 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/network/vpn_gateway_test.go @@ -0,0 +1,47 @@ +package network + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptVpnGateways(t *testing.T) { + tests := []struct { + name string + terraform string + expected []network.VpnGateway + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_vpn_gateway" "example" { + security_group = "example-security-group" + } +`, + expected: []network.VpnGateway{{ + SecurityGroup: iacTypes.StringTest("example-security-group"), + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_vpn_gateway" "example" { + } +`, + + expected: []network.VpnGateway{{}}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptVpnGateways(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/nifcloud.go b/pkg/iac/adapters/terraform/nifcloud/nifcloud.go new file mode 100644 index 000000000000..5f17fe6b235c --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/nifcloud.go @@ -0,0 +1,23 @@ +package nifcloud + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/nifcloud/computing" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/nifcloud/dns" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/nifcloud/nas" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/nifcloud/network" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/nifcloud/rdb" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/nifcloud/sslcertificate" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) nifcloud.Nifcloud { + return nifcloud.Nifcloud{ + Computing: computing.Adapt(modules), + DNS: dns.Adapt(modules), + NAS: nas.Adapt(modules), + Network: network.Adapt(modules), + RDB: rdb.Adapt(modules), + SSLCertificate: sslcertificate.Adapt(modules), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/rdb/adapt.go b/pkg/iac/adapters/terraform/nifcloud/rdb/adapt.go new file mode 100644 index 000000000000..fd35b2236187 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/rdb/adapt.go @@ -0,0 +1,13 @@ +package rdb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/rdb" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) rdb.RDB { + return rdb.RDB{ + DBSecurityGroups: adaptDBSecurityGroups(modules), + DBInstances: adaptDBInstances(modules), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/rdb/adapt_test.go b/pkg/iac/adapters/terraform/nifcloud/rdb/adapt_test.go new file mode 100644 index 000000000000..4aeb48ce5b53 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/rdb/adapt_test.go @@ -0,0 +1,60 @@ +package rdb + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" +) + +func TestLines(t *testing.T) { + src := ` +resource "nifcloud_db_instance" "example" { + publicly_accessible = false + engine = "MySQL" + engine_version = "5.7.15" + backup_retention_period = 2 + network_id = "example-network" +} + +resource "nifcloud_db_security_group" "example" { + description = "memo" + + rule { + cidr_ip = "0.0.0.0/0" + } +} +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.DBInstances, 1) + require.Len(t, adapted.DBSecurityGroups, 1) + + dbInstance := adapted.DBInstances[0] + dbSecurityGroup := adapted.DBSecurityGroups[0] + + assert.Equal(t, 3, dbInstance.PublicAccess.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, dbInstance.PublicAccess.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 4, dbInstance.Engine.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 4, dbInstance.Engine.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 5, dbInstance.EngineVersion.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 5, dbInstance.EngineVersion.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 6, dbInstance.BackupRetentionPeriodDays.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 6, dbInstance.BackupRetentionPeriodDays.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 7, dbInstance.NetworkID.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 7, dbInstance.NetworkID.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 11, dbSecurityGroup.Description.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, dbSecurityGroup.Description.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, 14, dbSecurityGroup.CIDRs[0].GetMetadata().Range().GetStartLine()) + assert.Equal(t, 14, dbSecurityGroup.CIDRs[0].GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/nifcloud/rdb/db_instance.go b/pkg/iac/adapters/terraform/nifcloud/rdb/db_instance.go new file mode 100644 index 000000000000..69757e41e9a2 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/rdb/db_instance.go @@ -0,0 +1,26 @@ +package rdb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/rdb" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func adaptDBInstances(modules terraform.Modules) []rdb.DBInstance { + var dbInstances []rdb.DBInstance + + for _, resource := range modules.GetResourcesByType("nifcloud_db_instance") { + dbInstances = append(dbInstances, adaptDBInstance(resource)) + } + return dbInstances +} + +func adaptDBInstance(resource *terraform.Block) rdb.DBInstance { + return rdb.DBInstance{ + Metadata: resource.GetMetadata(), + BackupRetentionPeriodDays: resource.GetAttribute("backup_retention_period").AsIntValueOrDefault(0, resource), + Engine: resource.GetAttribute("engine").AsStringValueOrDefault("", resource), + EngineVersion: resource.GetAttribute("engine_version").AsStringValueOrDefault("", resource), + NetworkID: resource.GetAttribute("network_id").AsStringValueOrDefault("net-COMMON_PRIVATE", resource), + PublicAccess: resource.GetAttribute("publicly_accessible").AsBoolValueOrDefault(true, resource), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/rdb/db_instance_test.go b/pkg/iac/adapters/terraform/nifcloud/rdb/db_instance_test.go new file mode 100644 index 000000000000..2913a14d96b7 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/rdb/db_instance_test.go @@ -0,0 +1,57 @@ +package rdb + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/rdb" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptDBInstances(t *testing.T) { + tests := []struct { + name string + terraform string + expected []rdb.DBInstance + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_db_instance" "example" { + backup_retention_period = 2 + engine = "MySQL" + engine_version = "5.7.15" + publicly_accessible = false + network_id = "example-network" + } +`, + expected: []rdb.DBInstance{{ + BackupRetentionPeriodDays: iacTypes.IntTest(2), + Engine: iacTypes.StringTest("MySQL"), + EngineVersion: iacTypes.StringTest("5.7.15"), + NetworkID: iacTypes.StringTest("example-network"), + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_db_instance" "example" { + } +`, + + expected: []rdb.DBInstance{{ + NetworkID: iacTypes.StringTest("net-COMMON_PRIVATE"), + PublicAccess: iacTypes.BoolTest(true), + }}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptDBInstances(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/rdb/db_security_group.go b/pkg/iac/adapters/terraform/nifcloud/rdb/db_security_group.go new file mode 100644 index 000000000000..e78d95ea0e3f --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/rdb/db_security_group.go @@ -0,0 +1,30 @@ +package rdb + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/rdb" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptDBSecurityGroups(modules terraform.Modules) []rdb.DBSecurityGroup { + var dbSecurityGroups []rdb.DBSecurityGroup + + for _, resource := range modules.GetResourcesByType("nifcloud_db_security_group") { + dbSecurityGroups = append(dbSecurityGroups, adaptDBSecurityGroup(resource)) + } + return dbSecurityGroups +} + +func adaptDBSecurityGroup(resource *terraform.Block) rdb.DBSecurityGroup { + var cidrs []iacTypes.StringValue + + for _, rule := range resource.GetBlocks("rule") { + cidrs = append(cidrs, rule.GetAttribute("cidr_ip").AsStringValueOrDefault("", resource)) + } + + return rdb.DBSecurityGroup{ + Metadata: resource.GetMetadata(), + Description: resource.GetAttribute("description").AsStringValueOrDefault("", resource), + CIDRs: cidrs, + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/rdb/db_security_group_test.go b/pkg/iac/adapters/terraform/nifcloud/rdb/db_security_group_test.go new file mode 100644 index 000000000000..6292d1a99d44 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/rdb/db_security_group_test.go @@ -0,0 +1,60 @@ +package rdb + +import ( + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/rdb" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_adaptDBSecurityGroups(t *testing.T) { + tests := []struct { + name string + terraform string + expected []rdb.DBSecurityGroup + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_db_security_group" "example" { + description = "memo" + + rule { + cidr_ip = "0.0.0.0/0" + } + } +`, + expected: []rdb.DBSecurityGroup{{ + Description: iacTypes.StringTest("memo"), + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest("0.0.0.0/0"), + }, + }}, + }, + { + name: "defaults", + terraform: ` + resource "nifcloud_db_security_group" "example" { + rule { + } + } +`, + + expected: []rdb.DBSecurityGroup{{ + CIDRs: []iacTypes.StringValue{ + iacTypes.StringTest(""), + }, + }}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := tftestutil.CreateModulesFromSource(t, test.terraform, ".tf") + adapted := adaptDBSecurityGroups(modules) + testutil.AssertDefsecEqual(t, test.expected, adapted) + }) + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/sslcertificate/adapt.go b/pkg/iac/adapters/terraform/nifcloud/sslcertificate/adapt.go new file mode 100644 index 000000000000..1f1391397a12 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/sslcertificate/adapt.go @@ -0,0 +1,12 @@ +package sslcertificate + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/sslcertificate" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Adapt(modules terraform.Modules) sslcertificate.SSLCertificate { + return sslcertificate.SSLCertificate{ + ServerCertificates: adaptServerCertificates(modules), + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/sslcertificate/adapt_test.go b/pkg/iac/adapters/terraform/nifcloud/sslcertificate/adapt_test.go new file mode 100644 index 000000000000..0f045f1c2c52 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/sslcertificate/adapt_test.go @@ -0,0 +1,28 @@ +package sslcertificate + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" +) + +func TestLines(t *testing.T) { + src := ` +resource "nifcloud_ssl_certificate" "example" { + certificate = "generated-certificate" +} +` + + modules := tftestutil.CreateModulesFromSource(t, src, ".tf") + adapted := Adapt(modules) + + require.Len(t, adapted.ServerCertificates, 1) + + serverCertificate := adapted.ServerCertificates[0] + + assert.Equal(t, 3, serverCertificate.Expiration.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 3, serverCertificate.Expiration.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/adapters/terraform/nifcloud/sslcertificate/server_certificate.go b/pkg/iac/adapters/terraform/nifcloud/sslcertificate/server_certificate.go new file mode 100644 index 000000000000..7c08d0d78ca3 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/sslcertificate/server_certificate.go @@ -0,0 +1,41 @@ +package sslcertificate + +import ( + "crypto/x509" + "encoding/pem" + + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/sslcertificate" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func adaptServerCertificates(modules terraform.Modules) []sslcertificate.ServerCertificate { + var serverCertificates []sslcertificate.ServerCertificate + + for _, resource := range modules.GetResourcesByType("nifcloud_ssl_certificate") { + serverCertificates = append(serverCertificates, adaptServerCertificate(resource)) + } + return serverCertificates +} + +func adaptServerCertificate(resource *terraform.Block) sslcertificate.ServerCertificate { + certificateAttr := resource.GetAttribute("certificate") + expiryDateVal := iacTypes.TimeUnresolvable(resource.GetMetadata()) + + if certificateAttr.IsNotNil() { + expiryDateVal = iacTypes.TimeUnresolvable(certificateAttr.GetMetadata()) + if certificateAttr.IsString() { + certificateString := certificateAttr.Value().AsString() + if block, _ := pem.Decode([]byte(certificateString)); block != nil { + if cert, err := x509.ParseCertificate(block.Bytes); err == nil { + expiryDateVal = iacTypes.Time(cert.NotAfter, certificateAttr.GetMetadata()) + } + } + } + } + + return sslcertificate.ServerCertificate{ + Metadata: resource.GetMetadata(), + Expiration: expiryDateVal, + } +} diff --git a/pkg/iac/adapters/terraform/nifcloud/sslcertificate/server_certificate_test.go b/pkg/iac/adapters/terraform/nifcloud/sslcertificate/server_certificate_test.go new file mode 100644 index 000000000000..d8a294c80885 --- /dev/null +++ b/pkg/iac/adapters/terraform/nifcloud/sslcertificate/server_certificate_test.go @@ -0,0 +1,68 @@ +package sslcertificate + +import ( + "testing" + "time" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform/tftestutil" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/sslcertificate" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +const certificate = ` +-----BEGIN CERTIFICATE----- +MIIB0zCCAX2gAwIBAgIJAI/M7BYjwB+uMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQwHhcNMTIwOTEyMjE1MjAyWhcNMTUwOTEyMjE1MjAyWjBF +MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 +ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBANLJ +hPHhITqQbPklG3ibCVxwGMRfp/v4XqhfdQHdcVfHap6NQ5Wok/4xIA+ui35/MmNa +rtNuC+BdZ1tMuVCPFZcCAwEAAaNQME4wHQYDVR0OBBYEFJvKs8RfJaXTH08W+SGv +zQyKn0H8MB8GA1UdIwQYMBaAFJvKs8RfJaXTH08W+SGvzQyKn0H8MAwGA1UdEwQF +MAMBAf8wDQYJKoZIhvcNAQEFBQADQQBJlffJHybjDGxRMqaRmDhX0+6v02TUKZsW +r5QuVbpQhH6u+0UgcW0jp9QwpxoPTLTWGXEWBBBurxFwiCBhkQ+V +-----END CERTIFICATE----- +` + +func Test_adaptServerCertificates(t *testing.T) { + tests := []struct { + name string + terraform string + expected []sslcertificate.ServerCertificate + }{ + { + name: "configured", + terraform: ` + resource "nifcloud_ssl_certificate" "example" { + certificate = < 0 + case map[string]any: + hasResources = len(resources) > 0 + } + } + + return len(sniff.Parameters) > 0 || hasResources +} + +func detectDockerfile(name string, _ io.ReadSeeker) bool { + requiredFiles := []string{"Dockerfile", "Containerfile"} + for _, requiredFile := range requiredFiles { + base := filepath.Base(name) + ext := filepath.Ext(base) + if strings.TrimSuffix(base, ext) == requiredFile { + return true + } + if strings.EqualFold(ext, "."+requiredFile) { + return true + } + } + return false +} + +func detectKubernetes(name string, r io.ReadSeeker) bool { + if !IsType(name, r, FileTypeYAML) && !IsType(name, r, FileTypeJSON) { + return false + } + if resetReader(r) == nil { + return false + } + + expectedProperties := []string{"apiVersion", "kind", "metadata"} + + if IsType(name, r, FileTypeJSON) { + if resetReader(r) == nil { + return false + } + + var result map[string]any + if err := json.NewDecoder(r).Decode(&result); err != nil { + return false + } + + for _, expected := range expectedProperties { + if _, ok := result[expected]; !ok { + return false + } + } + return true + } + + // at this point, we need to inspect bytes + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err != nil { + return false + } + data := buf.Bytes() + + marker := []byte("\n---\n") + altMarker := []byte("\r\n---\r\n") + if bytes.Contains(data, altMarker) { + marker = altMarker + } + + for partial := range bytes.SplitSeq(data, marker) { + var result map[string]any + if err := yaml.Unmarshal(partial, &result); err != nil { + continue + } + match := true + for _, expected := range expectedProperties { + if _, ok := result[expected]; !ok { + match = false + break + } + } + if match { + return true + } + } + + return false +} + +func detectAnsible(name string, _ io.ReadSeeker) bool { + return filepath.Base(name) == "ansible.cfg" || + slices.Contains([]string{"", ".yml", ".yaml", ".json", ".ini"}, filepath.Ext(name)) +} + +func IsTerraformFile(path string) bool { + if strings.HasSuffix(path, filepath.ToSlash(".terraform/modules/modules.json")) { + return true + } + + for _, ext := range []string{".tf", ".tf.json", ".tfvars", ".tofu", ".tofu.json"} { + if strings.HasSuffix(path, ext) { + return true + } + } + + return false +} + +func IsType(name string, r io.ReadSeeker, t FileType) bool { + r = ensureSeeker(r) + f, ok := matchers[t] + if !ok { + return false + } + return f(name, r) +} + +func GetTypes(name string, r io.ReadSeeker) []FileType { + var matched []FileType + r = ensureSeeker(r) + for check, f := range matchers { + if f(name, r) { + matched = append(matched, check) + } + resetReader(r) + } + return matched +} + +func ensureSeeker(r io.Reader) io.ReadSeeker { + if r == nil { + return nil + } + if seeker, ok := r.(io.ReadSeeker); ok { + return seeker + } + + var buf bytes.Buffer + if _, err := io.Copy(&buf, r); err == nil { + return bytes.NewReader(buf.Bytes()) + } + + return nil +} + +func resetReader(r io.Reader) io.ReadSeeker { + if r == nil { + return nil + } + if seeker, ok := r.(io.ReadSeeker); ok { + _, _ = seeker.Seek(0, 0) + return seeker + } + return ensureSeeker(r) +} + +func isJSON(name string) bool { + ext := filepath.Ext(name) + return strings.EqualFold(ext, ".json") +} +func isYAML(name string) bool { + ext := filepath.Ext(name) + return strings.EqualFold(ext, ".yaml") || strings.EqualFold(ext, ".yml") +} + +func IsFileMatchesSchemas(schemas map[string]*gojsonschema.Schema, typ FileType, name string, r io.ReadSeeker) bool { + defer resetReader(r) + + var l gojsonschema.JSONLoader + switch { + case typ == FileTypeJSON && isJSON(name): + b, err := io.ReadAll(r) + if err != nil { + return false + } + l = gojsonschema.NewBytesLoader(b) + case typ == FileTypeYAML && isYAML(name): + var content any + if err := yaml.NewDecoder(r).Decode(&content); err != nil { + return false + } + l = gojsonschema.NewGoLoader(content) + default: + return false + } + + for schemaPath, schema := range schemas { + if res, err := schema.Validate(l); err == nil && res.Valid() { + log.Debug("File matched schema", log.FilePath(name), log.String("schema_path", schemaPath)) + return true + } + } + return false +} diff --git a/pkg/iac/detection/detect_test.go b/pkg/iac/detection/detect_test.go new file mode 100644 index 000000000000..17b1fe4739b6 --- /dev/null +++ b/pkg/iac/detection/detect_test.go @@ -0,0 +1,767 @@ +package detection + +import ( + "bytes" + "fmt" + "io" + "os" + "slices" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/xeipuuv/gojsonschema" +) + +func Test_Detection(t *testing.T) { + tests := []struct { + name string + path string + r io.ReadSeeker + expected []FileType + }{ + { + name: "text file, no reader", + path: "something.txt", + expected: nil, + }, + { + name: "text file, with reader", + path: "something.txt", + r: strings.NewReader("some file content"), + expected: nil, + }, + { + name: "terraform, no reader", + path: "main.tf", + expected: []FileType{ + FileTypeTerraform, + }, + }, + { + name: "terraform, with reader", + path: "main.tf", + r: strings.NewReader("some file content"), + expected: []FileType{ + FileTypeTerraform, + }, + }, + { + name: "terraform json, no reader", + path: "main.tf.json", + expected: []FileType{ + FileTypeTerraform, + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "terraform json, with reader", + path: "main.tf.json", + r: strings.NewReader(` +{ + "variable": { + "example": { + "default": "hello" + } + } +} +`), + expected: []FileType{ + FileTypeTerraform, + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "terraform vars, no reader", + path: "main.tfvars", + expected: []FileType{ + FileTypeTerraform, + }, + }, + { + name: "terraform vars, with reader", + path: "main.tfvars", + r: strings.NewReader("some_var = \"some value\""), + expected: []FileType{ + FileTypeTerraform, + }, + }, + { + name: "tofu, no reader", + path: "main.tofu", + expected: []FileType{ + FileTypeTerraform, + }, + }, + { + name: "tofu, with reader", + path: "main.tofu", + r: strings.NewReader("some file content"), + expected: []FileType{ + FileTypeTerraform, + }, + }, + { + name: "tofu json, no reader", + path: "main.tofu.json", + expected: []FileType{ + FileTypeTerraform, + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "tofu json, with reader", + path: "main.tofu.json", + r: strings.NewReader(` +{ + "variable": { + "example": { + "default": "hello" + } + } +} +`), + expected: []FileType{ + FileTypeTerraform, + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "cloudformation, no reader", + path: "main.yaml", + expected: []FileType{ + FileTypeYAML, + FileTypeHelm, + FileTypeAnsible, + }, + }, + { + name: "terraform plan, with reader", + path: "plan.json", + r: strings.NewReader(`{ + "format_version": "0.2", + "terraform_version": "1.0.3", + "variables": { + "bucket_name": { + "value": "tfsec-plan-testing" + } + }, + "planned_values": {}, + "resource_changes": [], + "prior_state": {}, + "configuration": {} + }`), + expected: []FileType{ + FileTypeTerraformPlanJSON, + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "cloudformation, with reader", + path: "main.yaml", + r: strings.NewReader(`--- +AWSTemplateFormatVersion: 2010-09-09 + +Description: CodePipeline for continuous integration and continuous deployment + +Parameters: + RepositoryName: + Type: String + Description: Name of the CodeCommit repository + BuildDockerImage: + Type: String + Default: aws/codebuild/ubuntu-base:14.04 + Description: Docker image to use for the build phase + DeployDockerImage: + Type: String + Default: aws/codebuild/ubuntu-base:14.04 + Description: Docker image to use for the deployment phase + +Resources: + PipelineS3Bucket: + Type: AWS::S3::Bucket +`), + expected: []FileType{ + FileTypeCloudFormation, + FileTypeYAML, + FileTypeHelm, + FileTypeAnsible, + }, + }, + { + name: "JSON with Resources, not cloudformation", + path: "whatever.json", + r: strings.NewReader(`{ + "Resources": ["something"] +}`), + expected: []FileType{ + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "Dockerfile, no reader", + path: "Dockerfile", + r: nil, + expected: []FileType{ + FileTypeDockerfile, + FileTypeAnsible, + }, + }, + { + name: "Containerfile, no reader", + path: "Containerfile", + r: nil, + expected: []FileType{ + FileTypeDockerfile, + FileTypeAnsible, + }, + }, + { + name: "Dockerfile, reader", + path: "Dockerfile", + r: strings.NewReader("FROM ubuntu\n"), + expected: []FileType{ + FileTypeDockerfile, + FileTypeAnsible, + }, + }, + { + name: "Dockerfile extension", + path: "lol.Dockerfile", + r: nil, + expected: []FileType{ + FileTypeDockerfile, + }, + }, + { + name: "kubernetes, no reader", + path: "k8s.yml", + r: nil, + expected: []FileType{ + FileTypeYAML, + FileTypeHelm, + FileTypeAnsible, + }, + }, + { + name: "kubernetes, reader", + path: "k8s.yml", + r: strings.NewReader(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80`), + expected: []FileType{ + FileTypeKubernetes, + FileTypeYAML, + FileTypeHelm, + FileTypeAnsible, + }, + }, + { + name: "kubernetes, reader, JSON", + path: "k8s.json", + r: strings.NewReader(`{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "nginx-deployment", + "labels": { + "app": "nginx" + } + }, + "spec": { + "replicas": 3, + "selector": { + "matchLabels": { + "app": "nginx" + } + }, + "template": { + "metadata": { + "labels": { + "app": "nginx" + } + }, + "spec": { + "containers": [ + { + "name": "nginx", + "image": "nginx:1.14.2", + "ports": [ + { + "containerPort": 80 + } + ] + } + ] + } + } + } +}`), + expected: []FileType{ + FileTypeKubernetes, + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "YAML, no reader", + path: "file.yaml", + r: nil, + expected: []FileType{ + FileTypeYAML, + FileTypeHelm, + FileTypeAnsible, + }, + }, + { + name: "YML, no reader", + path: "file.yml", + r: nil, + expected: []FileType{ + FileTypeYAML, + FileTypeHelm, + FileTypeAnsible, + }, + }, + { + name: "YML uppercase", + path: "file.YML", + r: nil, + expected: []FileType{ + FileTypeYAML, + FileTypeHelm, + }, + }, + { + name: "TOML, no reader", + path: "file.toml", + r: nil, + expected: []FileType{ + FileTypeTOML, + }, + }, + { + name: "JSON, no reader", + path: "file.json", + r: nil, + expected: []FileType{ + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "kubernetes, configmap", + path: "k8s.yml", + r: strings.NewReader(`apiVersion: v1 +kind: ConfigMap +metadata: + name: test + namespace: default +data: + AWS_ACCESS_KEY_ID: "XXX" + AWS_SECRET_ACCESS_KEY: "XXX"`), + expected: []FileType{ + FileTypeKubernetes, + FileTypeYAML, + FileTypeHelm, + FileTypeAnsible, + }, + }, + { + name: "kubernetes, clusterRole", + path: "k8s.yml", + r: strings.NewReader(`apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + annotations: + rbac.authorization.kubernetes.io/autoupdate: "true" + labels: + kubernetes.io/bootstrapping: rbac-defaults + rbac.authorization.k8s.io/aggregate-to-edit: "true" + name: view +rules: +- apiGroups: + - networking.k8s.io + resources: + - ingresses + - ingresses/status + - networkpolicies + verbs: + - get + - list + - watch`), + expected: []FileType{ + FileTypeKubernetes, + FileTypeYAML, + FileTypeHelm, + FileTypeAnsible, + }, + }, + { + name: "kubernetes, multi-doc with non-map doc first", + path: "k8s.yaml", + r: strings.NewReader(`--- +- this is a list, not a map +--- +apiVersion: v1 +kind: Service +metadata: + name: valid-service +`), + expected: []FileType{ + FileTypeKubernetes, + FileTypeYAML, + FileTypeHelm, + FileTypeAnsible, + }, + }, + { + name: "Azure ARM template with resources", + path: "test.json", + r: strings.NewReader(` +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-09-01", + "name": "{provide-unique-name}", + "location": "eastus", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "supportsHttpsTrafficOnly": true + } + } + ] +} +`), + expected: []FileType{ + FileTypeJSON, + FileTypeAzureARM, + FileTypeAnsible, + }, + }, + { + name: "Azure ARM resources defined as an object", + path: "test.json", + r: strings.NewReader(`{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "resources": { + "myacc": { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-06-01", + "name": "my-acc-test", + "location": "location", + "kind": "Storage" + } + } +}`), + expected: []FileType{ + FileTypeJSON, + FileTypeCloudFormation, + FileTypeAzureARM, + FileTypeAnsible, + }, + }, + { + name: "Azure ARM template with parameters", + path: "test.json", + r: strings.NewReader(` +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "storageName": { + "type": "string", + "minLength": 3, + "maxLength": 24 + } + } +} +`), + expected: []FileType{ + FileTypeJSON, + FileTypeAzureARM, + FileTypeAnsible, + }, + }, + { + name: "empty Azure ARM template", + path: "test.json", + r: strings.NewReader(` +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] +} +`), + expected: []FileType{ + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "CreateUiDefinition", + path: "CreateUiDefinition.json", + r: strings.NewReader(`{ + "$schema": "https://schema.management.azure.com/schemas/0.1.2-preview/CreateUIDefinition.MultiVm.json#", + "handler": "Microsoft.Azure.CreateUIDef", + "version": "0.1.2-preview", + "parameters": { + "config": { + "isWizard": false, + "basics": {} + }, + "basics": [], + "steps": [], + "outputs": {}, + "resourceTypes": [] + } +}`), + expected: []FileType{ + FileTypeJSON, + FileTypeAnsible, + }, + }, + { + name: "without extension", + path: "something", + expected: []FileType{ + FileTypeAnsible, + }, + }, + { + name: "Ansible inventory INI file", + path: "something.ini", + expected: []FileType{ + FileTypeAnsible, + }, + }, + { + name: "Ansible config file", + path: "ansible.cfg", + expected: []FileType{ + FileTypeAnsible, + }, + }, + { + name: " not Ansible config file", + path: "something.cfg", + expected: []FileType{}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Run("GetTypes", func(t *testing.T) { + actualDetections := GetTypes(test.path, test.r) + assert.Len(t, actualDetections, len(test.expected)) + for _, expected := range test.expected { + resetReader(test.r) + found := slices.Contains(actualDetections, expected) + assert.True(t, found, "%s should be detected", expected) + } + }) + for _, expected := range test.expected { + resetReader(test.r) + t.Run(fmt.Sprintf("IsType_%s", expected), func(t *testing.T) { + assert.True(t, IsType(test.path, test.r, expected)) + }) + } + t.Run("IsType_invalid", func(t *testing.T) { + resetReader(test.r) + assert.False(t, IsType(test.path, test.r, "invalid")) + }) + }) + } +} + +func BenchmarkIsType_SmallFile(b *testing.B) { + data, err := os.ReadFile(fmt.Sprintf("./testdata/%s", "small.file")) + require.NoError(b, err) + + b.ReportAllocs() + + for b.Loop() { + _ = IsType(fmt.Sprintf("./testdata/%s", "small.file"), bytes.NewReader(data), FileTypeAzureARM) + } +} + +func BenchmarkIsType_BigFile(b *testing.B) { + data, err := os.ReadFile(fmt.Sprintf("./testdata/%s", "big.file")) + require.NoError(b, err) + + b.ReportAllocs() + + for b.Loop() { + _ = IsType(fmt.Sprintf("./testdata/%s", "big.file"), bytes.NewReader(data), FileTypeAzureARM) + } +} + +func Test_IsFileMatchesSchemas(t *testing.T) { + + schema := `{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "service": { "type": "string" } + }, + "required": ["service"] +}` + + schema2 := `{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "provider": { "type": "string" } + }, + "required": ["provider"] + }` + + type args struct { + schemas []string + fileType FileType + fileName string + fileContent string + } + tests := []struct { + name string + args args + matches bool + }{ + { + name: "json file matches", + args: args{ + schemas: []string{schema}, + fileType: FileTypeJSON, + fileName: "test.json", + fileContent: `{ + "service": "test" +}`, + }, + matches: true, + }, + { + name: "json file dost not matches", + args: args{ + schemas: []string{schema}, + fileType: FileTypeJSON, + fileName: "test.json", + fileContent: `{ + "somefield": "test", +}`, + }, + matches: false, + }, + { + name: "json file matches, but file type is yaml", + args: args{ + schemas: []string{schema}, + fileType: FileTypeYAML, + fileName: "test.json", + fileContent: `{ + "service": "test" +}`, + }, + matches: false, + }, + { + name: "broken json file", + args: args{ + schemas: []string{schema}, + fileType: FileTypeJSON, + fileName: "test.json", + fileContent: `{ + "service": "test",, +}`, + }, + matches: false, + }, + { + name: "yaml file matches", + args: args{ + schemas: []string{schema}, + fileType: FileTypeYAML, + fileName: "test.yml", + fileContent: `service: test`, + }, + matches: true, + }, + { + name: "yaml file does not matches", + args: args{ + schemas: []string{schema}, + fileType: FileTypeYAML, + fileName: "test.yaml", + fileContent: `somefield: test`, + }, + matches: false, + }, + { + name: "broken yaml file", + args: args{ + schemas: []string{schema}, + fileType: FileTypeYAML, + fileName: "test.yaml", + fileContent: `text foobar +number: 2`, + }, + matches: false, + }, + { + name: "multiple schemas", + args: args{ + schemas: []string{schema, schema2}, + fileType: FileTypeYAML, + fileName: "test.yaml", + fileContent: `provider: test`, + }, + matches: true, + }, + } + for _, tt := range tests { + schemas := make(map[string]*gojsonschema.Schema) + for i, content := range tt.args.schemas { + l := gojsonschema.NewStringLoader(content) + s, err := gojsonschema.NewSchema(l) + require.NoError(t, err) + schemas[fmt.Sprintf("schema-%d.json", i)] = s + } + rs := strings.NewReader(tt.args.fileContent) + got := IsFileMatchesSchemas(schemas, tt.args.fileType, tt.args.fileName, rs) + assert.Equal(t, tt.matches, got) + } +} diff --git a/pkg/iac/detection/peek.go b/pkg/iac/detection/peek.go new file mode 100644 index 000000000000..0e76115d9bd8 --- /dev/null +++ b/pkg/iac/detection/peek.go @@ -0,0 +1,53 @@ +package detection + +import ( + "archive/tar" + "compress/gzip" + "errors" + "io" + "strings" +) + +func IsHelmChartArchive(path string, file io.Reader) bool { + + if !IsArchive(path) { + return false + } + + var err error + var fr = file + + if IsZip(path) { + if fr, err = gzip.NewReader(file); err != nil { + return false + } + } + tr := tar.NewReader(fr) + + if tr == nil { + return false + } + + for { + header, err := tr.Next() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return false + } + + if header.Typeflag == tar.TypeReg && strings.HasSuffix(header.Name, "Chart.yaml") { + return true + } + } + return false +} + +func IsArchive(path string) bool { + return strings.HasSuffix(path, ".tar") || IsZip(path) +} + +func IsZip(path string) bool { + return strings.HasSuffix(path, ".tgz") || strings.HasSuffix(path, ".tar.gz") +} diff --git a/pkg/iac/detection/testdata/big.file b/pkg/iac/detection/testdata/big.file new file mode 100644 index 000000000000..e7f3c2d40ecc Binary files /dev/null and b/pkg/iac/detection/testdata/big.file differ diff --git a/pkg/iac/detection/testdata/small.file b/pkg/iac/detection/testdata/small.file new file mode 100644 index 000000000000..d8ae428a4800 --- /dev/null +++ b/pkg/iac/detection/testdata/small.file @@ -0,0 +1,3 @@ +{ + "content": "foo bar baz" +} \ No newline at end of file diff --git a/pkg/iac/framework/frameworks.go b/pkg/iac/framework/frameworks.go new file mode 100644 index 000000000000..82f43947d568 --- /dev/null +++ b/pkg/iac/framework/frameworks.go @@ -0,0 +1,11 @@ +package framework + +type Framework string + +const ( + Default Framework = "default" + Experimental Framework = "experimental" + CIS_AWS_1_2 Framework = "cis-aws-1.2" + CIS_AWS_1_4 Framework = "cis-aws-1.4" + ALL Framework = "all" +) diff --git a/pkg/iac/ignore/parse.go b/pkg/iac/ignore/parse.go new file mode 100644 index 000000000000..8a7a940c6b5c --- /dev/null +++ b/pkg/iac/ignore/parse.go @@ -0,0 +1,181 @@ +package ignore + +import ( + "errors" + "strings" + "time" + + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +// RuleSectionParser defines the interface for parsing ignore rules. +type RuleSectionParser interface { + Key() string + Parse(string) bool + Param() any +} + +// Parse parses the configuration file and returns the Rules +func Parse(src, path, sourcePrefix string, parsers ...RuleSectionParser) Rules { + var rules Rules + for i, line := range strings.Split(src, "\n") { + line = strings.TrimSpace(line) + rng := types.NewRange(path, i+1, i+1, sourcePrefix, nil) + lineIgnores := parseLine(line, rng, parsers) + for _, lineIgnore := range lineIgnores { + rules = append(rules, lineIgnore) + } + } + + rules.shift() + + return rules +} + +func parseLine(line string, rng types.Range, parsers []RuleSectionParser) []Rule { + var rules []Rule + + parts := strings.Split(strings.TrimSpace(line), " ") + parts = lo.FilterMap(parts, func(part string, _ int) (string, bool) { + part = strings.TrimSpace(part) + part = strings.TrimLeftFunc(part, func(r rune) bool { + return r == '#' || r == '/' || r == '*' + }) + + return part, part != "" + }) + + for i, part := range parts { + part, exists := hasIgnoreRulePrefix(part) + if !exists { + continue + } + + sections, err := parseRuleSections(part, rng, parsers) + if err != nil { + log.Debug("Failed to parse rule", log.String("range", rng.String()), log.Err(err)) + continue + } + + rule := Rule{ + rng: rng, + isStartLine: i == 0 || (len(rules) > 0 && rules[0].isStartLine), + sections: sections, + } + + rules = append(rules, rule) + } + + return rules +} + +func hasIgnoreRulePrefix(s string) (string, bool) { + for _, prefix := range []string{ + "tfsec:", + "trivy:", + } { + if after, found := strings.CutPrefix(s, prefix); found { + return after, true + } + } + + return "", false +} + +func parseRuleSections(input string, rng types.Range, parsers []RuleSectionParser) (map[string]any, error) { + sections := make(map[string]any) + + parsers = append(parsers, &expiryDateParser{ + rng: rng, + }) + + segments := strings.Split(input, ":") + + for i := 0; i < len(segments)-1; i += 2 { + key := segments[i] + val := segments[i+1] + if key == "ignore" { + // special case, because id and parameters are in the same section + idParser := &checkIDParser{ + StringMatchParser{SectionKey: "id"}, + } + if idParser.Parse(val) { + sections[idParser.Key()] = idParser.Param() + } + } + + for _, parser := range parsers { + if parser.Key() != key { + continue + } + + if parser.Parse(val) { + sections[parser.Key()] = parser.Param() + } + } + } + + if _, exists := sections["id"]; !exists { + return nil, errors.New("rule section with the `ignore` key is required") + } + + return sections, nil +} + +type StringMatchParser struct { + SectionKey string + param string +} + +func (s *StringMatchParser) Key() string { + return s.SectionKey +} + +func (s *StringMatchParser) Parse(str string) bool { + s.param = str + return str != "" +} + +func (s *StringMatchParser) Param() any { + return s.param +} + +type checkIDParser struct { + StringMatchParser +} + +func (s *checkIDParser) Parse(str string) bool { + if idx := strings.Index(str, "["); idx != -1 { + str = str[:idx] + } + return s.StringMatchParser.Parse(str) +} + +type expiryDateParser struct { + rng types.Range + expiry time.Time +} + +func (s *expiryDateParser) Key() string { + return "exp" +} + +func (s *expiryDateParser) Parse(str string) bool { + parsed, err := time.Parse("2006-01-02", str) + if err != nil { + log.Debug("Incorrect time to ignore is specified", log.String("time", str)) + parsed = time.Time{} + } else if time.Now().After(parsed) { + log.Debug("Ignore rule time has expired for location", log.String("range", s.rng.String())) + } + + s.expiry = parsed + return true +} + +func (s *expiryDateParser) Param() any { + return s.expiry +} diff --git a/pkg/iac/ignore/rule.go b/pkg/iac/ignore/rule.go new file mode 100644 index 000000000000..86e24e8dd120 --- /dev/null +++ b/pkg/iac/ignore/rule.go @@ -0,0 +1,137 @@ +package ignore + +import ( + "regexp" + "slices" + "strings" + "time" + + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +// Ignorer represents a function that checks if the rule should be ignored. +type Ignorer func(resultMeta types.Metadata, ignoredParam any) bool + +type Rules []Rule + +// Ignore checks if the rule should be ignored based on provided metadata, IDs, and ignorer functions. +func (r Rules) Ignore(m types.Metadata, ids []string, ignorers map[string]Ignorer) bool { + return slices.ContainsFunc(r, func(r Rule) bool { + return r.ignore(m, ids, ignorers) + }) +} + +func (r Rules) shift() { + var ( + currentRange *types.Range + offset int + ) + + for i := len(r) - 1; i > 0; i-- { + currentRule, prevRule := r[i], r[i-1] + + if !prevRule.isStartLine { + continue + } + + if currentRange == nil { + currentRange = ¤tRule.rng + } + if prevRule.rng.GetStartLine()+1+offset == currentRule.rng.GetStartLine() { + r[i-1].rng = *currentRange + offset++ + } else { + currentRange = nil + offset = 0 + } + } +} + +// Rule represents a rule for ignoring vulnerabilities. +type Rule struct { + rng types.Range + isStartLine bool + sections map[string]any +} + +func (r Rule) ignore(m types.Metadata, ids []string, ignorers map[string]Ignorer) bool { + matchMeta, ok := r.matchRange(&m) + if !ok { + return false + } + + ignorers = lo.Assign(defaultIgnorers(ids), ignorers) + + for ignoreID, ignore := range ignorers { + if param, exists := r.sections[ignoreID]; exists { + if !ignore(*matchMeta, param) { + return false + } + } + } + + return true +} + +func (r Rule) matchRange(m *types.Metadata) (*types.Metadata, bool) { + metaHierarchy := m + for metaHierarchy != nil { + if r.rng.GetFilename() != metaHierarchy.Range().GetFilename() { + metaHierarchy = metaHierarchy.Parent() + continue + } + if metaHierarchy.Range().GetStartLine() == r.rng.GetStartLine()+1 || + metaHierarchy.Range().GetStartLine() == r.rng.GetStartLine() { + return metaHierarchy, true + } + metaHierarchy = metaHierarchy.Parent() + } + + return nil, false +} + +func defaultIgnorers(ids []string) map[string]Ignorer { + return map[string]Ignorer{ + "id": func(_ types.Metadata, param any) bool { + id, ok := param.(string) + if !ok { + return false + } + if id == "*" || len(ids) == 0 { + return true + } + + return slices.ContainsFunc(ids, func(s string) bool { + return MatchPattern(s, id) + }) + }, + "exp": func(_ types.Metadata, param any) bool { + expiry, ok := param.(time.Time) + return ok && time.Now().Before(expiry) + }, + } +} + +// MatchPattern checks if the pattern string matches the input pattern. +// The wildcard '*' in the pattern matches any sequence of characters. +func MatchPattern(input, pattern string) bool { + matched, err := regexp.MatchString(regexpFromPattern(pattern), input) + return err == nil && matched +} + +func regexpFromPattern(pattern string) string { + parts := strings.Split(pattern, "*") + if len(parts) == 1 { + return "^" + pattern + "$" + } + var sb strings.Builder + for i, literal := range parts { + if i > 0 { + sb.WriteString(".*") + } + sb.WriteString(regexp.QuoteMeta(literal)) + } + return "^" + sb.String() + "$" +} diff --git a/pkg/iac/ignore/rule_test.go b/pkg/iac/ignore/rule_test.go new file mode 100644 index 000000000000..d2fbe8aee515 --- /dev/null +++ b/pkg/iac/ignore/rule_test.go @@ -0,0 +1,361 @@ +package ignore_test + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/iac/ignore" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func metadataWithLine(path string, line int) types.Metadata { + return types.NewMetadata(types.NewRange(path, line, line, "", nil), "") +} + +func TestRules_Ignore(t *testing.T) { + + const filename = "test" + + type args struct { + metadata types.Metadata + ids []string + } + + tests := []struct { + name string + src string + args args + shouldIgnore bool + }{ + { + name: "no ignore", + src: `#test`, + shouldIgnore: false, + }, + { + name: "one ignore rule", + src: `#trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "blank line between rule and finding", + src: `#trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 3), + ids: []string{"rule-1"}, + }, + shouldIgnore: false, + }, + { + name: "blank line between rules", + src: `#trivy:ignore:rule-1 + +#trivy:ignore:rule-2 +`, + args: args{ + metadata: metadataWithLine(filename, 4), + ids: []string{"rule-1"}, + }, + shouldIgnore: false, + }, + { + name: "rule and a finding on the same line", + src: `#trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 1), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "rule and a finding on the same line", + src: `test #trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 1), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "multiple rules on one line", + src: `test #trivy:ignore:rule-1 #trivy:ignore:rule-2`, + args: args{ + metadata: metadataWithLine(filename, 1), + ids: []string{"rule-2"}, + }, + shouldIgnore: true, + }, + { + name: "rule and find from different files", + src: `test #trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine("another-file", 1), + ids: []string{"rule-2"}, + }, + shouldIgnore: false, + }, + { + name: "multiple ignore rule", + src: `#trivy:ignore:rule-1 +#trivy:ignore:rule-2 +`, + args: args{ + metadata: metadataWithLine(filename, 3), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "ignore section with params", + src: `#trivy:ignore:rule-1[param1=1]`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "id's don't match", + src: `#trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-2"}, + }, + shouldIgnore: false, + }, + { + name: "without ignore section", + src: `#trivy:exp:2022-01-01`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-2"}, + }, + shouldIgnore: false, + }, + { + name: "non valid ignore section", + src: `#trivy:ignore`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-2"}, + }, + shouldIgnore: false, + }, + { + name: "ignore rule with expiry date passed", + src: `#trivy:ignore:rule-1:exp:2022-01-01`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: false, + }, + { + name: "ignore rule with expiry date not passed", + src: fmt.Sprintf(`#trivy:ignore:rule-1:exp:%d-01-01`, time.Now().Year()+1), + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "ignore rule with invalid expiry date", + src: `#trivy:ignore:rule-1:exp:2026-99-01`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: false, + }, + { + name: "with valid wildcard", + src: `#trivy:ignore:rule-*`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "with non-valid wildcard", + src: `#trivy:ignore:rule-1-*d`, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1-abc"}, + }, + shouldIgnore: false, + }, + { + name: "multiple ignore rules on the same line", + src: `test #trivy:ignore:rule-1 +test #trivy:ignore:rule-2 + `, + args: args{ + metadata: metadataWithLine(filename, 1), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "multiple ignore rules on the same line", + src: `# trivy:ignore:rule-1 +# trivy:ignore:rule-2 +test #trivy:ignore:rule-3 +`, + args: args{ + metadata: metadataWithLine(filename, 3), + ids: []string{"rule-1"}, + }, + shouldIgnore: true, + }, + { + name: "multiple ignore rules on the same line", + src: `# trivy:ignore:rule-1 # trivy:ignore:rule-2 +# trivy:ignore:rule-3 +test #trivy:ignore:rule-4 +`, + args: args{ + metadata: metadataWithLine(filename, 3), + ids: []string{"rule-2"}, + }, + shouldIgnore: true, + }, + { + name: "multiple ids", + src: `# trivy:ignore:rule-1`, + args: args{ + metadata: metadataWithLine(filename, 1), + ids: []string{"rule-1", "rule-2"}, + }, + shouldIgnore: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rules := ignore.Parse(tt.src, "", filename) + got := rules.Ignore(tt.args.metadata, tt.args.ids, nil) + assert.Equal(t, tt.shouldIgnore, got) + }) + } +} + +func TestRules_IgnoreWithCustomIgnorer(t *testing.T) { + const filename = "test" + + type args struct { + metadata types.Metadata + ids []string + ignorers map[string]ignore.Ignorer + } + + tests := []struct { + name string + src string + parser ignore.RuleSectionParser + args args + shouldIgnore bool + }{ + { + name: "happy", + src: `#trivy:ignore:rule-1:ws:dev`, + parser: &ignore.StringMatchParser{ + SectionKey: "ws", + }, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + ignorers: map[string]ignore.Ignorer{ + "ws": func(_ types.Metadata, param any) bool { + ws, ok := param.(string) + if !ok { + return false + } + return ws == "dev" + }, + }, + }, + shouldIgnore: true, + }, + { + name: "with wildcard", + src: `#trivy:ignore:rule-1:ws:dev-*`, + parser: &ignore.StringMatchParser{ + SectionKey: "ws", + }, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + ignorers: map[string]ignore.Ignorer{ + "ws": func(_ types.Metadata, param any) bool { + ws, ok := param.(string) + if !ok { + return false + } + return ignore.MatchPattern("dev-stage1", ws) + }, + }, + }, + shouldIgnore: true, + }, + { + name: "bad", + src: `#trivy:ignore:rule-1:ws:prod`, + parser: &ignore.StringMatchParser{ + SectionKey: "ws", + }, + args: args{ + metadata: metadataWithLine(filename, 2), + ids: []string{"rule-1"}, + ignorers: map[string]ignore.Ignorer{ + "ws": func(_ types.Metadata, param any) bool { + ws, ok := param.(string) + if !ok { + return false + } + return ws == "dev" + }, + }, + }, + shouldIgnore: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rules := ignore.Parse(tt.src, filename, "", tt.parser) + got := rules.Ignore(tt.args.metadata, tt.args.ids, tt.args.ignorers) + assert.Equal(t, tt.shouldIgnore, got) + }) + } +} + +func TestMatchPattern(t *testing.T) { + tests := []struct { + input string + pattern string + expected bool + }{ + {"foo-test-bar", "*-test-*", true}, + {"foo-test-bar", "*-example-*", false}, + {"test", "*test", true}, + {"example", "test", false}, + {"example-test", "*-test*", true}, + {"example-test", "*example-*", true}, + } + + for _, tc := range tests { + t.Run(tc.input+":"+tc.pattern, func(t *testing.T) { + got := ignore.MatchPattern(tc.input, tc.pattern) + assert.Equal(t, tc.expected, got) + }) + } +} diff --git a/pkg/iac/providers/aws/accessanalyzer/aa.go b/pkg/iac/providers/aws/accessanalyzer/aa.go new file mode 100644 index 000000000000..66be31561d04 --- /dev/null +++ b/pkg/iac/providers/aws/accessanalyzer/aa.go @@ -0,0 +1,19 @@ +package accessanalyzer + +import "github.com/aquasecurity/trivy/pkg/iac/types" + +type AccessAnalyzer struct { + Analyzers []Analyzer +} + +type Analyzer struct { + Metadata types.Metadata + ARN types.StringValue + Name types.StringValue + Active types.BoolValue + Findings []Findings +} + +type Findings struct { + Metadata types.Metadata +} diff --git a/pkg/iac/providers/aws/apigateway/ag.go b/pkg/iac/providers/aws/apigateway/ag.go new file mode 100644 index 000000000000..d87a9b364ee8 --- /dev/null +++ b/pkg/iac/providers/aws/apigateway/ag.go @@ -0,0 +1,11 @@ +package apigateway + +import ( + v1 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v1" + v2 "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway/v2" +) + +type APIGateway struct { + V1 v1.APIGateway + V2 v2.APIGateway +} diff --git a/pkg/iac/providers/aws/apigateway/v1/apigateway.go b/pkg/iac/providers/aws/apigateway/v1/apigateway.go new file mode 100755 index 000000000000..626d91d513b7 --- /dev/null +++ b/pkg/iac/providers/aws/apigateway/v1/apigateway.go @@ -0,0 +1,62 @@ +package v1 + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type APIGateway struct { + APIs []API + DomainNames []DomainName +} + +type API struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Stages []Stage + Resources []Resource +} + +type Stage struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + AccessLogging AccessLogging + XRayTracingEnabled iacTypes.BoolValue + RESTMethodSettings []RESTMethodSettings +} + +type Resource struct { + Metadata iacTypes.Metadata + Methods []Method +} + +type AccessLogging struct { + Metadata iacTypes.Metadata + CloudwatchLogGroupARN iacTypes.StringValue +} + +type RESTMethodSettings struct { + Metadata iacTypes.Metadata + Method iacTypes.StringValue + CacheDataEncrypted iacTypes.BoolValue + CacheEnabled iacTypes.BoolValue +} + +const ( + AuthorizationNone = "NONE" + AuthorizationCustom = "CUSTOM" + AuthorizationIAM = "AWS_IAM" + AuthorizationCognitoUserPools = "COGNITO_USER_POOLS" +) + +type Method struct { + Metadata iacTypes.Metadata + HTTPMethod iacTypes.StringValue + AuthorizationType iacTypes.StringValue + APIKeyRequired iacTypes.BoolValue +} + +type DomainName struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + SecurityPolicy iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/apigateway/v2/apigateway.go b/pkg/iac/providers/aws/apigateway/v2/apigateway.go new file mode 100755 index 000000000000..5a87e8ffbca7 --- /dev/null +++ b/pkg/iac/providers/aws/apigateway/v2/apigateway.go @@ -0,0 +1,41 @@ +package v2 + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type APIGateway struct { + APIs []API + DomainNames []DomainName +} + +const ( + ProtocolTypeUnknown string = "" + ProtocolTypeREST string = "REST" + ProtocolTypeHTTP string = "HTTP" + ProtocolTypeWebsocket string = "WEBSOCKET" +) + +type API struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + ProtocolType iacTypes.StringValue + Stages []Stage +} + +type Stage struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + AccessLogging AccessLogging +} + +type AccessLogging struct { + Metadata iacTypes.Metadata + CloudwatchLogGroupARN iacTypes.StringValue +} + +type DomainName struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + SecurityPolicy iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/athena/athena.go b/pkg/iac/providers/aws/athena/athena.go new file mode 100755 index 000000000000..537a4b63d6b0 --- /dev/null +++ b/pkg/iac/providers/aws/athena/athena.go @@ -0,0 +1,35 @@ +package athena + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Athena struct { + Databases []Database + Workgroups []Workgroup +} + +type Database struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Encryption EncryptionConfiguration +} + +type Workgroup struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Encryption EncryptionConfiguration + EnforceConfiguration iacTypes.BoolValue +} + +const ( + EncryptionTypeNone = "" + EncryptionTypeSSES3 = "SSE_S3" + EncryptionTypeSSEKMS = "SSE_KMS" + EncryptionTypeCSEKMS = "CSE_KMS" +) + +type EncryptionConfiguration struct { + Metadata iacTypes.Metadata + Type iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/aws.go b/pkg/iac/providers/aws/aws.go new file mode 100755 index 000000000000..15c80a84afa7 --- /dev/null +++ b/pkg/iac/providers/aws/aws.go @@ -0,0 +1,80 @@ +package aws + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/accessanalyzer" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/apigateway" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/athena" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudfront" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudtrail" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/cloudwatch" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/codebuild" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/config" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/documentdb" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/dynamodb" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecr" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ecs" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/efs" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/eks" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticache" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elasticsearch" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/elb" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/emr" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/kinesis" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/kms" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/lambda" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/mq" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/msk" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/neptune" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/rds" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/redshift" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sam" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sns" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/sqs" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ssm" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/workspaces" +) + +type AWS struct { + Meta Meta + AccessAnalyzer accessanalyzer.AccessAnalyzer + APIGateway apigateway.APIGateway + Athena athena.Athena + Cloudfront cloudfront.Cloudfront + CloudTrail cloudtrail.CloudTrail + CloudWatch cloudwatch.CloudWatch + CodeBuild codebuild.CodeBuild + Config config.Config + DocumentDB documentdb.DocumentDB + DynamoDB dynamodb.DynamoDB + EC2 ec2.EC2 + ECR ecr.ECR + ECS ecs.ECS + EFS efs.EFS + EKS eks.EKS + ElastiCache elasticache.ElastiCache + Elasticsearch elasticsearch.Elasticsearch + ELB elb.ELB + EMR emr.EMR + IAM iam.IAM + Kinesis kinesis.Kinesis + KMS kms.KMS + Lambda lambda.Lambda + MQ mq.MQ + MSK msk.MSK + Neptune neptune.Neptune + RDS rds.RDS + Redshift redshift.Redshift + SAM sam.SAM + S3 s3.S3 + SNS sns.SNS + SQS sqs.SQS + SSM ssm.SSM + WorkSpaces workspaces.WorkSpaces +} + +type Meta struct { + TFProviders []TerraformProvider +} diff --git a/pkg/iac/providers/aws/cloudfront/cloudfront.go b/pkg/iac/providers/aws/cloudfront/cloudfront.go new file mode 100755 index 000000000000..0c4914e4c3bb --- /dev/null +++ b/pkg/iac/providers/aws/cloudfront/cloudfront.go @@ -0,0 +1,45 @@ +package cloudfront + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Cloudfront struct { + Distributions []Distribution +} + +type Distribution struct { + Metadata iacTypes.Metadata + WAFID iacTypes.StringValue + Logging Logging + DefaultCacheBehaviour CacheBehaviour + OrdererCacheBehaviours []CacheBehaviour + ViewerCertificate ViewerCertificate +} + +type Logging struct { + Metadata iacTypes.Metadata + Bucket iacTypes.StringValue +} + +type CacheBehaviour struct { + Metadata iacTypes.Metadata + ViewerProtocolPolicy iacTypes.StringValue +} + +const ( + ViewerPolicyProtocolAllowAll = "allow-all" + ViewerPolicyProtocolHTTPSOnly = "https-only" + ViewerPolicyProtocolRedirectToHTTPS = "redirect-to-https" +) + +const ( + ProtocolVersionTLS1_2 = "TLSv1.2_2021" +) + +type ViewerCertificate struct { + Metadata iacTypes.Metadata + CloudfrontDefaultCertificate iacTypes.BoolValue + SSLSupportMethod iacTypes.StringValue + MinimumProtocolVersion iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/cloudtrail/cloudtrail.go b/pkg/iac/providers/aws/cloudtrail/cloudtrail.go new file mode 100755 index 000000000000..edb28c826c9d --- /dev/null +++ b/pkg/iac/providers/aws/cloudtrail/cloudtrail.go @@ -0,0 +1,33 @@ +package cloudtrail + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type CloudTrail struct { + Trails []Trail +} + +type Trail struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + EnableLogFileValidation iacTypes.BoolValue + IsMultiRegion iacTypes.BoolValue + KMSKeyID iacTypes.StringValue + CloudWatchLogsLogGroupArn iacTypes.StringValue + IsLogging iacTypes.BoolValue + BucketName iacTypes.StringValue + EventSelectors []EventSelector +} + +type EventSelector struct { + Metadata iacTypes.Metadata + DataResources []DataResource + ReadWriteType iacTypes.StringValue // ReadOnly, WriteOnly, All. Default value is All for TF. +} + +type DataResource struct { + Metadata iacTypes.Metadata + Type iacTypes.StringValue // You can specify only the following value: "AWS::S3::Object", "AWS::Lambda::Function" and "AWS::DynamoDB::Table". + Values []iacTypes.StringValue // List of ARNs/partial ARNs - e.g. arn:aws:s3:::/ for all objects in a bucket, arn:aws:s3:::/key for specific objects +} diff --git a/pkg/iac/providers/aws/cloudwatch/cloudwatch.go b/pkg/iac/providers/aws/cloudwatch/cloudwatch.go new file mode 100755 index 000000000000..182d3c8a9dd4 --- /dev/null +++ b/pkg/iac/providers/aws/cloudwatch/cloudwatch.go @@ -0,0 +1,45 @@ +package cloudwatch + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type CloudWatch struct { + LogGroups []LogGroup + Alarms []Alarm +} + +type Alarm struct { + Metadata iacTypes.Metadata + AlarmName iacTypes.StringValue + MetricName iacTypes.StringValue + Dimensions []AlarmDimension + Metrics []MetricDataQuery +} + +type AlarmDimension struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Value iacTypes.StringValue +} + +type MetricFilter struct { + Metadata iacTypes.Metadata + FilterName iacTypes.StringValue + FilterPattern iacTypes.StringValue +} + +type MetricDataQuery struct { + Metadata iacTypes.Metadata + Expression iacTypes.StringValue + ID iacTypes.StringValue +} + +type LogGroup struct { + Metadata iacTypes.Metadata + Arn iacTypes.StringValue + Name iacTypes.StringValue + KMSKeyID iacTypes.StringValue + RetentionInDays iacTypes.IntValue + MetricFilters []MetricFilter +} diff --git a/pkg/iac/providers/aws/codebuild/codebuild.go b/pkg/iac/providers/aws/codebuild/codebuild.go new file mode 100755 index 000000000000..34115ce40cd5 --- /dev/null +++ b/pkg/iac/providers/aws/codebuild/codebuild.go @@ -0,0 +1,20 @@ +package codebuild + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type CodeBuild struct { + Projects []Project +} + +type Project struct { + Metadata iacTypes.Metadata + ArtifactSettings ArtifactSettings + SecondaryArtifactSettings []ArtifactSettings +} + +type ArtifactSettings struct { + Metadata iacTypes.Metadata + EncryptionEnabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/config/config.go b/pkg/iac/providers/aws/config/config.go new file mode 100755 index 000000000000..ef18539f3bfa --- /dev/null +++ b/pkg/iac/providers/aws/config/config.go @@ -0,0 +1,14 @@ +package config + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Config struct { + ConfigurationAggregrator ConfigurationAggregrator +} + +type ConfigurationAggregrator struct { + Metadata iacTypes.Metadata + SourceAllRegions iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/documentdb/documentdb.go b/pkg/iac/providers/aws/documentdb/documentdb.go new file mode 100755 index 000000000000..c1bdc0a73854 --- /dev/null +++ b/pkg/iac/providers/aws/documentdb/documentdb.go @@ -0,0 +1,29 @@ +package documentdb + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type DocumentDB struct { + Clusters []Cluster +} + +const ( + LogExportAudit = "audit" + LogExportProfiler = "profiler" +) + +type Cluster struct { + Metadata iacTypes.Metadata + Identifier iacTypes.StringValue + EnabledLogExports []iacTypes.StringValue + BackupRetentionPeriod iacTypes.IntValue + Instances []Instance + StorageEncrypted iacTypes.BoolValue + KMSKeyID iacTypes.StringValue +} + +type Instance struct { + Metadata iacTypes.Metadata + KMSKeyID iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/dynamodb/dynamodb.go b/pkg/iac/providers/aws/dynamodb/dynamodb.go new file mode 100755 index 000000000000..eef0d5d532c0 --- /dev/null +++ b/pkg/iac/providers/aws/dynamodb/dynamodb.go @@ -0,0 +1,30 @@ +package dynamodb + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type DynamoDB struct { + DAXClusters []DAXCluster + Tables []Table +} + +type DAXCluster struct { + Metadata iacTypes.Metadata + ServerSideEncryption ServerSideEncryption + PointInTimeRecovery iacTypes.BoolValue +} + +type Table struct { + Metadata iacTypes.Metadata + ServerSideEncryption ServerSideEncryption + PointInTimeRecovery iacTypes.BoolValue +} + +type ServerSideEncryption struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + KMSKeyID iacTypes.StringValue +} + +const DefaultKMSKeyID = "alias/aws/dynamodb" diff --git a/pkg/iac/providers/aws/ec2/ami.go b/pkg/iac/providers/aws/ec2/ami.go new file mode 100644 index 000000000000..1d281b72932d --- /dev/null +++ b/pkg/iac/providers/aws/ec2/ami.go @@ -0,0 +1,8 @@ +package ec2 + +import iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + +type RequestedAMI struct { + Metadata iacTypes.Metadata + Owners iacTypes.StringValueList +} diff --git a/pkg/iac/providers/aws/ec2/ec2.go b/pkg/iac/providers/aws/ec2/ec2.go new file mode 100755 index 000000000000..37b68ed7273c --- /dev/null +++ b/pkg/iac/providers/aws/ec2/ec2.go @@ -0,0 +1,13 @@ +package ec2 + +type EC2 struct { + Instances []Instance + LaunchConfigurations []LaunchConfiguration + LaunchTemplates []LaunchTemplate + VPCs []VPC + SecurityGroups []SecurityGroup + NetworkACLs []NetworkACL + Subnets []Subnet + Volumes []Volume + RequestedAMIs []RequestedAMI +} diff --git a/pkg/iac/providers/aws/ec2/instance.go b/pkg/iac/providers/aws/ec2/instance.go new file mode 100644 index 000000000000..dd50f016458a --- /dev/null +++ b/pkg/iac/providers/aws/ec2/instance.go @@ -0,0 +1,40 @@ +package ec2 + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Instance struct { + Metadata iacTypes.Metadata + MetadataOptions MetadataOptions + UserData iacTypes.StringValue + SecurityGroups []SecurityGroup + RootBlockDevice *BlockDevice + EBSBlockDevices []*BlockDevice +} + +type BlockDevice struct { + Metadata iacTypes.Metadata + Encrypted iacTypes.BoolValue +} + +type MetadataOptions struct { + Metadata iacTypes.Metadata + HttpTokens iacTypes.StringValue + HttpEndpoint iacTypes.StringValue +} + +func NewInstance(metadata iacTypes.Metadata) *Instance { + return &Instance{ + Metadata: metadata, + MetadataOptions: MetadataOptions{ + Metadata: metadata, + HttpTokens: iacTypes.StringDefault("optional", metadata), + HttpEndpoint: iacTypes.StringDefault("enabled", metadata), + }, + UserData: iacTypes.StringDefault("", metadata), + SecurityGroups: []SecurityGroup{}, + RootBlockDevice: nil, + EBSBlockDevices: nil, + } +} diff --git a/pkg/iac/providers/aws/ec2/launch.go b/pkg/iac/providers/aws/ec2/launch.go new file mode 100644 index 000000000000..77fa12f8305a --- /dev/null +++ b/pkg/iac/providers/aws/ec2/launch.go @@ -0,0 +1,21 @@ +package ec2 + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type LaunchConfiguration struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + AssociatePublicIP iacTypes.BoolValue + RootBlockDevice *BlockDevice + EBSBlockDevices []*BlockDevice + MetadataOptions MetadataOptions + UserData iacTypes.StringValue +} + +type LaunchTemplate struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Instance +} diff --git a/pkg/iac/providers/aws/ec2/subnet.go b/pkg/iac/providers/aws/ec2/subnet.go new file mode 100644 index 000000000000..18f605011e07 --- /dev/null +++ b/pkg/iac/providers/aws/ec2/subnet.go @@ -0,0 +1,10 @@ +package ec2 + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Subnet struct { + Metadata iacTypes.Metadata + MapPublicIpOnLaunch iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/ec2/volume.go b/pkg/iac/providers/aws/ec2/volume.go new file mode 100644 index 000000000000..3258a3ee6c5a --- /dev/null +++ b/pkg/iac/providers/aws/ec2/volume.go @@ -0,0 +1,16 @@ +package ec2 + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Volume struct { + Metadata iacTypes.Metadata + Encryption Encryption +} + +type Encryption struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + KMSKeyID iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/ec2/vpc.go b/pkg/iac/providers/aws/ec2/vpc.go new file mode 100644 index 000000000000..d6bff4fe7025 --- /dev/null +++ b/pkg/iac/providers/aws/ec2/vpc.go @@ -0,0 +1,57 @@ +package ec2 + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type NetworkACL struct { + Metadata iacTypes.Metadata + Rules []NetworkACLRule + IsDefaultRule iacTypes.BoolValue +} + +type SecurityGroup struct { + Metadata iacTypes.Metadata + IsDefault iacTypes.BoolValue + Description iacTypes.StringValue + IngressRules []SecurityGroupRule + EgressRules []SecurityGroupRule + VPCID iacTypes.StringValue +} + +type SecurityGroupRule struct { + Metadata iacTypes.Metadata + Description iacTypes.StringValue + CIDRs []iacTypes.StringValue + Protocol iacTypes.StringValue + FromPort iacTypes.IntValue + ToPort iacTypes.IntValue +} + +type VPC struct { + Metadata iacTypes.Metadata + ID iacTypes.StringValue + IsDefault iacTypes.BoolValue + SecurityGroups []SecurityGroup + FlowLogsEnabled iacTypes.BoolValue +} + +const ( + TypeIngress = "ingress" + TypeEgress = "egress" +) + +const ( + ActionAllow = "allow" + ActionDeny = "deny" +) + +type NetworkACLRule struct { + Metadata iacTypes.Metadata + Type iacTypes.StringValue + Action iacTypes.StringValue + Protocol iacTypes.StringValue + CIDRs []iacTypes.StringValue + FromPort iacTypes.IntValue + ToPort iacTypes.IntValue +} diff --git a/pkg/iac/providers/aws/ecr/ecr.go b/pkg/iac/providers/aws/ecr/ecr.go new file mode 100755 index 000000000000..053b7f13dace --- /dev/null +++ b/pkg/iac/providers/aws/ecr/ecr.go @@ -0,0 +1,34 @@ +package ecr + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type ECR struct { + Repositories []Repository +} + +type Repository struct { + Metadata iacTypes.Metadata + ImageScanning ImageScanning + ImageTagsImmutable iacTypes.BoolValue + Policies []iam.Policy + Encryption Encryption +} + +type ImageScanning struct { + Metadata iacTypes.Metadata + ScanOnPush iacTypes.BoolValue +} + +const ( + EncryptionTypeKMS = "KMS" + EncryptionTypeAES256 = "AES256" +) + +type Encryption struct { + Metadata iacTypes.Metadata + Type iacTypes.StringValue + KMSKeyID iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/ecs/ecs.go b/pkg/iac/providers/aws/ecs/ecs.go new file mode 100755 index 000000000000..661d2233b2da --- /dev/null +++ b/pkg/iac/providers/aws/ecs/ecs.go @@ -0,0 +1,124 @@ +package ecs + +import ( + "encoding/json" + + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type ECS struct { + Clusters []Cluster + TaskDefinitions []TaskDefinition +} + +type Cluster struct { + Metadata iacTypes.Metadata + Settings ClusterSettings +} + +type ClusterSettings struct { + Metadata iacTypes.Metadata + ContainerInsightsEnabled iacTypes.BoolValue +} + +type TaskDefinition struct { + Metadata iacTypes.Metadata + Volumes []Volume + ContainerDefinitions []ContainerDefinition +} + +func CreateDefinitionsFromString(metadata iacTypes.Metadata, str string) ([]ContainerDefinition, error) { + var containerDefinitionsJSON []containerDefinitionJSON + if err := json.Unmarshal([]byte(str), &containerDefinitionsJSON); err != nil { + return nil, err + } + var definitions []ContainerDefinition + for _, j := range containerDefinitionsJSON { + definitions = append(definitions, j.convert(metadata)) + } + return definitions, nil +} + +// see https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html +type containerDefinitionJSON struct { + Name string `json:"name"` + Image string `json:"image"` + CPU string `json:"cpu"` + Memory string `json:"memory"` + Essential bool `json:"essential"` + PortMappings []portMappingJSON `json:"portMappings"` + EnvVars []envVarJSON `json:"environment"` + Privileged bool `json:"privileged"` +} + +type envVarJSON struct { + Name string `json:"name"` + Value string `json:"value"` +} + +type portMappingJSON struct { + ContainerPort int `json:"containerPort"` + HostPort int `json:"hostPort"` +} + +func (j containerDefinitionJSON) convert(metadata iacTypes.Metadata) ContainerDefinition { + var mappings []PortMapping + for _, jMapping := range j.PortMappings { + mappings = append(mappings, PortMapping{ + ContainerPort: iacTypes.Int(jMapping.ContainerPort, metadata), + HostPort: iacTypes.Int(jMapping.HostPort, metadata), + }) + } + + var envVars []EnvVar + for _, env := range j.EnvVars { + envVars = append(envVars, EnvVar{ + Name: iacTypes.String(env.Name, metadata), + Value: iacTypes.String(env.Value, metadata), + }) + } + + return ContainerDefinition{ + Metadata: metadata, + Name: iacTypes.String(j.Name, metadata), + Image: iacTypes.String(j.Image, metadata), + CPU: iacTypes.String(j.CPU, metadata), + Memory: iacTypes.String(j.Memory, metadata), + Essential: iacTypes.Bool(j.Essential, metadata), + PortMappings: mappings, + Environment: envVars, + Privileged: iacTypes.Bool(j.Privileged, metadata), + } +} + +type ContainerDefinition struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Image iacTypes.StringValue + CPU iacTypes.StringValue + Memory iacTypes.StringValue + Essential iacTypes.BoolValue + PortMappings []PortMapping + Environment []EnvVar + Privileged iacTypes.BoolValue +} + +type EnvVar struct { + Name iacTypes.StringValue + Value iacTypes.StringValue +} + +type PortMapping struct { + ContainerPort iacTypes.IntValue + HostPort iacTypes.IntValue +} + +type Volume struct { + Metadata iacTypes.Metadata + EFSVolumeConfiguration EFSVolumeConfiguration +} + +type EFSVolumeConfiguration struct { + Metadata iacTypes.Metadata + TransitEncryptionEnabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/efs/efs.go b/pkg/iac/providers/aws/efs/efs.go new file mode 100755 index 000000000000..4e9765b7d500 --- /dev/null +++ b/pkg/iac/providers/aws/efs/efs.go @@ -0,0 +1,14 @@ +package efs + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type EFS struct { + FileSystems []FileSystem +} + +type FileSystem struct { + Metadata iacTypes.Metadata + Encrypted iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/eks/eks.go b/pkg/iac/providers/aws/eks/eks.go new file mode 100755 index 000000000000..9eab8b563f65 --- /dev/null +++ b/pkg/iac/providers/aws/eks/eks.go @@ -0,0 +1,32 @@ +package eks + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type EKS struct { + Clusters []Cluster +} + +type Cluster struct { + Metadata iacTypes.Metadata + Logging Logging + Encryption Encryption + PublicAccessEnabled iacTypes.BoolValue + PublicAccessCIDRs []iacTypes.StringValue +} + +type Logging struct { + Metadata iacTypes.Metadata + API iacTypes.BoolValue + Audit iacTypes.BoolValue + Authenticator iacTypes.BoolValue + ControllerManager iacTypes.BoolValue + Scheduler iacTypes.BoolValue +} + +type Encryption struct { + Metadata iacTypes.Metadata + Secrets iacTypes.BoolValue + KMSKeyID iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/elasticache/elasticache.go b/pkg/iac/providers/aws/elasticache/elasticache.go new file mode 100755 index 000000000000..4069601b3193 --- /dev/null +++ b/pkg/iac/providers/aws/elasticache/elasticache.go @@ -0,0 +1,29 @@ +package elasticache + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type ElastiCache struct { + Clusters []Cluster + ReplicationGroups []ReplicationGroup + SecurityGroups []SecurityGroup +} + +type Cluster struct { + Metadata iacTypes.Metadata + Engine iacTypes.StringValue + NodeType iacTypes.StringValue + SnapshotRetentionLimit iacTypes.IntValue // days +} + +type ReplicationGroup struct { + Metadata iacTypes.Metadata + TransitEncryptionEnabled iacTypes.BoolValue + AtRestEncryptionEnabled iacTypes.BoolValue +} + +type SecurityGroup struct { + Metadata iacTypes.Metadata + Description iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/elasticsearch/elasticsearch.go b/pkg/iac/providers/aws/elasticsearch/elasticsearch.go new file mode 100755 index 000000000000..4f3cb0583681 --- /dev/null +++ b/pkg/iac/providers/aws/elasticsearch/elasticsearch.go @@ -0,0 +1,53 @@ +package elasticsearch + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Elasticsearch struct { + Domains []Domain +} + +type Domain struct { + Metadata iacTypes.Metadata + DomainName iacTypes.StringValue + AccessPolicies iacTypes.StringValue + DedicatedMasterEnabled iacTypes.BoolValue + VpcId iacTypes.StringValue + LogPublishing LogPublishing + TransitEncryption TransitEncryption + AtRestEncryption AtRestEncryption + ServiceSoftwareOptions ServiceSoftwareOptions + Endpoint Endpoint +} + +type ServiceSoftwareOptions struct { + Metadata iacTypes.Metadata + CurrentVersion iacTypes.StringValue + NewVersion iacTypes.StringValue + UpdateAvailable iacTypes.BoolValue + UpdateStatus iacTypes.StringValue +} + +type Endpoint struct { + Metadata iacTypes.Metadata + EnforceHTTPS iacTypes.BoolValue + TLSPolicy iacTypes.StringValue +} + +type LogPublishing struct { + Metadata iacTypes.Metadata + AuditEnabled iacTypes.BoolValue + CloudWatchLogGroupArn iacTypes.StringValue +} + +type TransitEncryption struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type AtRestEncryption struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + KmsKeyId iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/elb/elb.go b/pkg/iac/providers/aws/elb/elb.go new file mode 100755 index 000000000000..04803fd26380 --- /dev/null +++ b/pkg/iac/providers/aws/elb/elb.go @@ -0,0 +1,36 @@ +package elb + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type ELB struct { + LoadBalancers []LoadBalancer +} + +const ( + TypeApplication = "application" + TypeGateway = "gateway" + TypeNetwork = "network" + TypeClassic = "classic" +) + +type LoadBalancer struct { + Metadata iacTypes.Metadata + Type iacTypes.StringValue + DropInvalidHeaderFields iacTypes.BoolValue + Internal iacTypes.BoolValue + Listeners []Listener +} + +type Listener struct { + Metadata iacTypes.Metadata + Protocol iacTypes.StringValue + TLSPolicy iacTypes.StringValue + DefaultActions []Action +} + +type Action struct { + Metadata iacTypes.Metadata + Type iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/emr/emr.go b/pkg/iac/providers/aws/emr/emr.go new file mode 100644 index 000000000000..1d00618a0345 --- /dev/null +++ b/pkg/iac/providers/aws/emr/emr.go @@ -0,0 +1,28 @@ +package emr + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type EMR struct { + Clusters []Cluster + SecurityConfiguration []SecurityConfiguration +} + +type Cluster struct { + Metadata iacTypes.Metadata + Settings ClusterSettings +} + +type ClusterSettings struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + ReleaseLabel iacTypes.StringValue + ServiceRole iacTypes.StringValue +} + +type SecurityConfiguration struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Configuration iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/iam/iam.go b/pkg/iac/providers/aws/iam/iam.go new file mode 100644 index 000000000000..216c236a9b9a --- /dev/null +++ b/pkg/iac/providers/aws/iam/iam.go @@ -0,0 +1,113 @@ +package iam + +import ( + "github.com/aquasecurity/iamgo" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type IAM struct { + PasswordPolicy PasswordPolicy + Policies []Policy + Groups []Group + Users []User + Roles []Role + ServerCertificates []ServerCertificate +} + +type ServerCertificate struct { + Metadata iacTypes.Metadata + Expiration iacTypes.TimeValue +} + +type Policy struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Document Document + Builtin iacTypes.BoolValue +} + +type Document struct { + Metadata iacTypes.Metadata + Parsed iamgo.Document + IsOffset bool + HasRefs bool +} + +func (d Document) ToRego() any { + m := d.Metadata + doc, _ := d.Parsed.MarshalJSON() + input := map[string]any{ + "filepath": m.Range().GetFilename(), + "startline": m.Range().GetStartLine(), + "endline": m.Range().GetEndLine(), + "managed": m.IsManaged(), + "explicit": m.IsExplicit(), + "value": string(doc), + "sourceprefix": m.Range().GetSourcePrefix(), + "fskey": iacTypes.CreateFSKey(m.Range().GetFS()), + "resource": m.Reference(), + } + + if m.Parent() != nil { + input["parent"] = m.Parent().ToRego() + } + + return input +} + +type Group struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Policies []Policy +} + +type User struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Policies []Policy + AccessKeys []AccessKey + MFADevices []MFADevice + LastAccess iacTypes.TimeValue +} + +type MFADevice struct { + Metadata iacTypes.Metadata + IsVirtual iacTypes.BoolValue +} + +type AccessKey struct { + Metadata iacTypes.Metadata + AccessKeyId iacTypes.StringValue + Active iacTypes.BoolValue + CreationDate iacTypes.TimeValue + LastAccess iacTypes.TimeValue +} + +type Role struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Policies []Policy +} + +func (d Document) MetadataFromIamGo(r ...iamgo.Range) iacTypes.Metadata { + m := d.Metadata + if d.HasRefs { + return m + } + newRange := m.Range() + var start int + if !d.IsOffset { + start = newRange.GetStartLine() + } + for _, rng := range r { + newRange := iacTypes.NewRange( + newRange.GetLocalFilename(), + start+rng.StartLine, + start+rng.EndLine, + newRange.GetSourcePrefix(), + newRange.GetFS(), + ) + m = iacTypes.NewMetadata(newRange, m.Reference()).WithParent(m) + } + return m +} diff --git a/pkg/iac/providers/aws/iam/passwords.go b/pkg/iac/providers/aws/iam/passwords.go new file mode 100755 index 000000000000..09632ba029e1 --- /dev/null +++ b/pkg/iac/providers/aws/iam/passwords.go @@ -0,0 +1,16 @@ +package iam + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type PasswordPolicy struct { + Metadata iacTypes.Metadata + ReusePreventionCount iacTypes.IntValue + RequireLowercase iacTypes.BoolValue + RequireUppercase iacTypes.BoolValue + RequireNumbers iacTypes.BoolValue + RequireSymbols iacTypes.BoolValue + MaxAgeDays iacTypes.IntValue + MinimumLength iacTypes.IntValue +} diff --git a/pkg/iac/providers/aws/kinesis/kinesis.go b/pkg/iac/providers/aws/kinesis/kinesis.go new file mode 100755 index 000000000000..53e6c9d75f02 --- /dev/null +++ b/pkg/iac/providers/aws/kinesis/kinesis.go @@ -0,0 +1,24 @@ +package kinesis + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Kinesis struct { + Streams []Stream +} + +type Stream struct { + Metadata iacTypes.Metadata + Encryption Encryption +} + +const ( + EncryptionTypeKMS = "KMS" +) + +type Encryption struct { + Metadata iacTypes.Metadata + Type iacTypes.StringValue + KMSKeyID iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/kms/kms.go b/pkg/iac/providers/aws/kms/kms.go new file mode 100755 index 000000000000..a0ef986fd086 --- /dev/null +++ b/pkg/iac/providers/aws/kms/kms.go @@ -0,0 +1,19 @@ +package kms + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type KMS struct { + Keys []Key +} + +const ( + KeyUsageSignAndVerify = "SIGN_VERIFY" +) + +type Key struct { + Metadata iacTypes.Metadata + Usage iacTypes.StringValue + RotationEnabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/lambda/lambda.go b/pkg/iac/providers/aws/lambda/lambda.go new file mode 100755 index 000000000000..1d4bb483747d --- /dev/null +++ b/pkg/iac/providers/aws/lambda/lambda.go @@ -0,0 +1,31 @@ +package lambda + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Lambda struct { + Functions []Function +} + +type Function struct { + Metadata iacTypes.Metadata + Tracing Tracing + Permissions []Permission +} + +const ( + TracingModePassThrough = "PassThrough" + TracingModeActive = "Active" +) + +type Tracing struct { + Metadata iacTypes.Metadata + Mode iacTypes.StringValue +} + +type Permission struct { + Metadata iacTypes.Metadata + Principal iacTypes.StringValue + SourceARN iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/mq/mq.go b/pkg/iac/providers/aws/mq/mq.go new file mode 100755 index 000000000000..a0e383de6dc4 --- /dev/null +++ b/pkg/iac/providers/aws/mq/mq.go @@ -0,0 +1,21 @@ +package mq + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type MQ struct { + Brokers []Broker +} + +type Broker struct { + Metadata iacTypes.Metadata + PublicAccess iacTypes.BoolValue + Logging Logging +} + +type Logging struct { + Metadata iacTypes.Metadata + General iacTypes.BoolValue + Audit iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/msk/msk.go b/pkg/iac/providers/aws/msk/msk.go new file mode 100755 index 000000000000..25d398167ef3 --- /dev/null +++ b/pkg/iac/providers/aws/msk/msk.go @@ -0,0 +1,60 @@ +package msk + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type MSK struct { + Clusters []Cluster +} + +type Cluster struct { + Metadata iacTypes.Metadata + EncryptionInTransit EncryptionInTransit + EncryptionAtRest EncryptionAtRest + Logging Logging +} + +const ( + ClientBrokerEncryptionTLS = "TLS" + ClientBrokerEncryptionPlaintext = "PLAINTEXT" + ClientBrokerEncryptionTLSOrPlaintext = "TLS_PLAINTEXT" +) + +type EncryptionInTransit struct { + Metadata iacTypes.Metadata + ClientBroker iacTypes.StringValue +} + +type EncryptionAtRest struct { + Metadata iacTypes.Metadata + KMSKeyARN iacTypes.StringValue + Enabled iacTypes.BoolValue +} + +type Logging struct { + Metadata iacTypes.Metadata + Broker BrokerLogging +} + +type BrokerLogging struct { + Metadata iacTypes.Metadata + S3 S3Logging + Cloudwatch CloudwatchLogging + Firehose FirehoseLogging +} + +type S3Logging struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type CloudwatchLogging struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type FirehoseLogging struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/neptune/neptune.go b/pkg/iac/providers/aws/neptune/neptune.go new file mode 100755 index 000000000000..15966afe4d47 --- /dev/null +++ b/pkg/iac/providers/aws/neptune/neptune.go @@ -0,0 +1,21 @@ +package neptune + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Neptune struct { + Clusters []Cluster +} + +type Cluster struct { + Metadata iacTypes.Metadata + Logging Logging + StorageEncrypted iacTypes.BoolValue + KMSKeyID iacTypes.StringValue +} + +type Logging struct { + Metadata iacTypes.Metadata + Audit iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/provider.go b/pkg/iac/providers/aws/provider.go new file mode 100644 index 000000000000..475d1bd0691e --- /dev/null +++ b/pkg/iac/providers/aws/provider.go @@ -0,0 +1,77 @@ +package aws + +import "github.com/aquasecurity/trivy/pkg/iac/types" + +type TerraformProvider struct { + Metadata types.Metadata + // generic fields + Alias types.StringValue + Version types.StringValue + + // provider specific fields + AccessKey types.StringValue + AllowedAccountsIDs types.StringValueList + AssumeRole AssumeRole + AssumeRoleWithWebIdentity AssumeRoleWithWebIdentity + CustomCABundle types.StringValue + DefaultTags DefaultTags + EC2MetadataServiceEndpoint types.StringValue + EC2MetadataServiceEndpointMode types.StringValue + Endpoints types.MapValue + ForbiddenAccountIDs types.StringValueList + HttpProxy types.StringValue + IgnoreTags IgnoreTags + Insecure types.BoolValue + MaxRetries types.IntValue + Profile types.StringValue + Region types.StringValue + RetryMode types.StringValue + S3UsePathStyle types.BoolValue + S3USEast1RegionalEndpoint types.StringValue + SecretKey types.StringValue + SharedConfigFiles types.StringValueList + SharedCredentialsFiles types.StringValueList + SkipCredentialsValidation types.BoolValue + SkipMetadataAPICheck types.BoolValue + SkipRegionValidation types.BoolValue + SkipRequestingAccountID types.BoolValue + STSRegion types.StringValue + Token types.StringValue + UseDualstackEndpoint types.BoolValue + UseFIPSEndpoint types.BoolValue +} + +type AssumeRole struct { + Metadata types.Metadata + Duration types.StringValue + ExternalID types.StringValue + Policy types.StringValue + PolicyARNs types.StringValueList + RoleARN types.StringValue + SessionName types.StringValue + SourceIdentity types.StringValue + Tags types.MapValue + TransitiveTagKeys types.StringValueList +} + +type AssumeRoleWithWebIdentity struct { + Metadata types.Metadata + Duration types.StringValue + Policy types.StringValue + PolicyARNs types.StringValueList + RoleARN types.StringValue + SessionName types.StringValue + WebIdentityToken types.StringValue + WebIdentityTokenFile types.StringValue +} + +type IgnoreTags struct { + Metadata types.Metadata + Keys types.StringValueList + KeyPrefixes types.StringValueList +} + +type DefaultTags struct { + Metadata types.Metadata + Tags types.MapValue +} diff --git a/pkg/iac/providers/aws/rds/classic.go b/pkg/iac/providers/aws/rds/classic.go new file mode 100755 index 000000000000..41be09b743ef --- /dev/null +++ b/pkg/iac/providers/aws/rds/classic.go @@ -0,0 +1,13 @@ +package rds + +import ( + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Classic struct { + DBSecurityGroups []DBSecurityGroup +} + +type DBSecurityGroup struct { + Metadata types.Metadata +} diff --git a/pkg/iac/providers/aws/rds/rds.go b/pkg/iac/providers/aws/rds/rds.go new file mode 100755 index 000000000000..d5a55b188e63 --- /dev/null +++ b/pkg/iac/providers/aws/rds/rds.go @@ -0,0 +1,127 @@ +package rds + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type RDS struct { + Instances []Instance + Clusters []Cluster + Classic Classic + Snapshots []Snapshots + ParameterGroups []ParameterGroups +} + +type Instance struct { + Metadata iacTypes.Metadata + BackupRetentionPeriodDays iacTypes.IntValue + ReplicationSourceARN iacTypes.StringValue + PerformanceInsights PerformanceInsights + Encryption Encryption + PublicAccess iacTypes.BoolValue + Engine iacTypes.StringValue + IAMAuthEnabled iacTypes.BoolValue + DeletionProtection iacTypes.BoolValue + DBInstanceArn iacTypes.StringValue + StorageEncrypted iacTypes.BoolValue + DBInstanceIdentifier iacTypes.StringValue + DBParameterGroups []DBParameterGroupsList + TagList []TagList + EnabledCloudwatchLogsExports []iacTypes.StringValue + EngineVersion iacTypes.StringValue + AutoMinorVersionUpgrade iacTypes.BoolValue + MultiAZ iacTypes.BoolValue + PubliclyAccessible iacTypes.BoolValue + LatestRestorableTime iacTypes.TimeValue + ReadReplicaDBInstanceIdentifiers []iacTypes.StringValue +} + +type Cluster struct { + Metadata iacTypes.Metadata + BackupRetentionPeriodDays iacTypes.IntValue + ReplicationSourceARN iacTypes.StringValue + PerformanceInsights PerformanceInsights + Instances []ClusterInstance + Encryption Encryption + PublicAccess iacTypes.BoolValue + Engine iacTypes.StringValue + LatestRestorableTime iacTypes.TimeValue + AvailabilityZones []iacTypes.StringValue + DeletionProtection iacTypes.BoolValue + SkipFinalSnapshot iacTypes.BoolValue +} + +type Snapshots struct { + Metadata iacTypes.Metadata + DBSnapshotIdentifier iacTypes.StringValue + DBSnapshotArn iacTypes.StringValue + Encrypted iacTypes.BoolValue + KmsKeyId iacTypes.StringValue + SnapshotAttributes []DBSnapshotAttributes +} + +type Parameters struct { + Metadata iacTypes.Metadata + ParameterName iacTypes.StringValue + ParameterValue iacTypes.StringValue +} + +type ParameterGroups struct { + Metadata iacTypes.Metadata + DBParameterGroupName iacTypes.StringValue + DBParameterGroupFamily iacTypes.StringValue + Parameters []Parameters +} + +type DBSnapshotAttributes struct { + Metadata iacTypes.Metadata + AttributeValues []iacTypes.StringValue +} + +const ( + EngineAurora = "aurora" + EngineAuroraMysql = "aurora-mysql" + EngineAuroraPostgresql = "aurora-postgresql" + EngineMySQL = "mysql" + EnginePostgres = "postgres" + EngineCustomOracleEE = "custom-oracle-ee" + EngineOracleEE = "oracle-ee" + EngineOracleEECDB = "oracle-ee-cdb" + EngineOracleSE2 = "oracle-se2" + EngineOracleSE2CDB = "oracle-se2-cdb" + EngineSQLServerEE = "sqlserver-ee" + EngineSQLServerSE = "sqlserver-se" + EngineSQLServerEX = "sqlserver-ex" + EngineSQLServerWEB = "sqlserver-web" + EngineMariaDB = "mariadb" + EngineCustomSQLServerEE = "custom-sqlserver-ee" + EngineCustomSQLServerSE = "custom-sqlserver-se" + EngineCustomSQLServerWEB = "custom-sqlserver-web" +) + +type Encryption struct { + Metadata iacTypes.Metadata + EncryptStorage iacTypes.BoolValue + KMSKeyID iacTypes.StringValue +} + +type ClusterInstance struct { + Instance + ClusterIdentifier iacTypes.StringValue +} + +type PerformanceInsights struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + KMSKeyID iacTypes.StringValue +} + +type DBParameterGroupsList struct { + Metadata iacTypes.Metadata + DBParameterGroupName iacTypes.StringValue + KMSKeyID iacTypes.StringValue +} + +type TagList struct { + Metadata iacTypes.Metadata +} diff --git a/pkg/iac/providers/aws/redshift/redshift.go b/pkg/iac/providers/aws/redshift/redshift.go new file mode 100755 index 000000000000..cdafaa290525 --- /dev/null +++ b/pkg/iac/providers/aws/redshift/redshift.go @@ -0,0 +1,55 @@ +package redshift + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Redshift struct { + Clusters []Cluster + ReservedNodes []ReservedNode + ClusterParameters []ClusterParameter + SecurityGroups []SecurityGroup +} + +type SecurityGroup struct { + Metadata iacTypes.Metadata + Description iacTypes.StringValue +} + +type ReservedNode struct { + Metadata iacTypes.Metadata + NodeType iacTypes.StringValue +} + +type ClusterParameter struct { + Metadata iacTypes.Metadata + ParameterName iacTypes.StringValue + ParameterValue iacTypes.StringValue +} + +type Cluster struct { + Metadata iacTypes.Metadata + ClusterIdentifier iacTypes.StringValue + NodeType iacTypes.StringValue + VpcId iacTypes.StringValue + NumberOfNodes iacTypes.IntValue + PubliclyAccessible iacTypes.BoolValue + AllowVersionUpgrade iacTypes.BoolValue + MasterUsername iacTypes.StringValue + AutomatedSnapshotRetentionPeriod iacTypes.IntValue + LoggingEnabled iacTypes.BoolValue + EndPoint EndPoint + Encryption Encryption + SubnetGroupName iacTypes.StringValue +} + +type EndPoint struct { + Metadata iacTypes.Metadata + Port iacTypes.IntValue +} + +type Encryption struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + KMSKeyID iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/s3/bucket.go b/pkg/iac/providers/aws/s3/bucket.go new file mode 100755 index 000000000000..6f723c29ab6d --- /dev/null +++ b/pkg/iac/providers/aws/s3/bucket.go @@ -0,0 +1,85 @@ +package s3 + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Bucket struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + PublicAccessBlock *PublicAccessBlock + BucketPolicies []iam.Policy + Encryption Encryption + Versioning Versioning + Logging Logging + ACL iacTypes.StringValue + Grants []Grant + BucketLocation iacTypes.StringValue + AccelerateConfigurationStatus iacTypes.StringValue + LifecycleConfiguration []Rules + Objects []Contents + Website *Website +} + +type PublicAccessBlock struct { + Metadata iacTypes.Metadata + BlockPublicACLs iacTypes.BoolValue + BlockPublicPolicy iacTypes.BoolValue + IgnorePublicACLs iacTypes.BoolValue + RestrictPublicBuckets iacTypes.BoolValue +} + +func NewPublicAccessBlock(metadata iacTypes.Metadata) PublicAccessBlock { + return PublicAccessBlock{ + Metadata: metadata, + BlockPublicPolicy: iacTypes.BoolDefault(false, metadata), + BlockPublicACLs: iacTypes.BoolDefault(false, metadata), + IgnorePublicACLs: iacTypes.BoolDefault(false, metadata), + RestrictPublicBuckets: iacTypes.BoolDefault(false, metadata), + } +} + +type Logging struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + TargetBucket iacTypes.StringValue +} + +type Versioning struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + MFADelete iacTypes.BoolValue +} + +type Encryption struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + Algorithm iacTypes.StringValue + KMSKeyId iacTypes.StringValue +} + +type Rules struct { + Metadata iacTypes.Metadata + Status iacTypes.StringValue +} + +type Contents struct { + Metadata iacTypes.Metadata +} + +type Website struct { + Metadata iacTypes.Metadata +} + +type Grant struct { + Metadata iacTypes.Metadata + Grantee Grantee + Permissions iacTypes.StringValueList +} + +type Grantee struct { + Metadata iacTypes.Metadata + URI iacTypes.StringValue + Type iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/s3/s3.go b/pkg/iac/providers/aws/s3/s3.go new file mode 100755 index 000000000000..230269a9e660 --- /dev/null +++ b/pkg/iac/providers/aws/s3/s3.go @@ -0,0 +1,5 @@ +package s3 + +type S3 struct { + Buckets []Bucket +} diff --git a/pkg/iac/providers/aws/sam/api.go b/pkg/iac/providers/aws/sam/api.go new file mode 100644 index 000000000000..ba716f3e4d92 --- /dev/null +++ b/pkg/iac/providers/aws/sam/api.go @@ -0,0 +1,38 @@ +package sam + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type API struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + TracingEnabled iacTypes.BoolValue + DomainConfiguration DomainConfiguration + AccessLogging AccessLogging + RESTMethodSettings RESTMethodSettings +} + +type ApiAuth struct { + Metadata iacTypes.Metadata + ApiKeyRequired iacTypes.BoolValue +} + +type AccessLogging struct { + Metadata iacTypes.Metadata + CloudwatchLogGroupARN iacTypes.StringValue +} + +type DomainConfiguration struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + SecurityPolicy iacTypes.StringValue +} + +type RESTMethodSettings struct { + Metadata iacTypes.Metadata + CacheDataEncrypted iacTypes.BoolValue + LoggingEnabled iacTypes.BoolValue + DataTraceEnabled iacTypes.BoolValue + MetricsEnabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/sam/application.go b/pkg/iac/providers/aws/sam/application.go new file mode 100644 index 000000000000..99fb7b7cac5b --- /dev/null +++ b/pkg/iac/providers/aws/sam/application.go @@ -0,0 +1,17 @@ +package sam + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Application struct { + Metadata iacTypes.Metadata + LocationPath iacTypes.StringValue + Location Location +} + +type Location struct { + Metadata iacTypes.Metadata + ApplicationID iacTypes.StringValue + SemanticVersion iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/sam/function.go b/pkg/iac/providers/aws/sam/function.go new file mode 100644 index 000000000000..5fb47afd2b66 --- /dev/null +++ b/pkg/iac/providers/aws/sam/function.go @@ -0,0 +1,25 @@ +package sam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Function struct { + Metadata iacTypes.Metadata + FunctionName iacTypes.StringValue + Tracing iacTypes.StringValue + ManagedPolicies []iacTypes.StringValue + Policies []iam.Policy +} + +const ( + TracingModePassThrough = "PassThrough" + TracingModeActive = "Active" +) + +type Permission struct { + Metadata iacTypes.Metadata + Principal iacTypes.StringValue + SourceARN iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/sam/http_api.go b/pkg/iac/providers/aws/sam/http_api.go new file mode 100644 index 000000000000..eb1b488bc1bb --- /dev/null +++ b/pkg/iac/providers/aws/sam/http_api.go @@ -0,0 +1,20 @@ +package sam + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type HttpAPI struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + AccessLogging AccessLogging + DefaultRouteSettings RouteSettings + DomainConfiguration DomainConfiguration +} + +type RouteSettings struct { + Metadata iacTypes.Metadata + LoggingEnabled iacTypes.BoolValue + DataTraceEnabled iacTypes.BoolValue + DetailedMetricsEnabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/sam/sam.go b/pkg/iac/providers/aws/sam/sam.go new file mode 100644 index 000000000000..ed75777d8053 --- /dev/null +++ b/pkg/iac/providers/aws/sam/sam.go @@ -0,0 +1,10 @@ +package sam + +type SAM struct { + APIs []API + Applications []Application + Functions []Function + HttpAPIs []HttpAPI + SimpleTables []SimpleTable + StateMachines []StateMachine +} diff --git a/pkg/iac/providers/aws/sam/state_machine.go b/pkg/iac/providers/aws/sam/state_machine.go new file mode 100644 index 000000000000..caee979436de --- /dev/null +++ b/pkg/iac/providers/aws/sam/state_machine.go @@ -0,0 +1,25 @@ +package sam + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type StateMachine struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + LoggingConfiguration LoggingConfiguration + ManagedPolicies []iacTypes.StringValue + Policies []iam.Policy + Tracing TracingConfiguration +} + +type LoggingConfiguration struct { + Metadata iacTypes.Metadata + LoggingEnabled iacTypes.BoolValue +} + +type TracingConfiguration struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/sam/table.go b/pkg/iac/providers/aws/sam/table.go new file mode 100644 index 000000000000..33179771ee66 --- /dev/null +++ b/pkg/iac/providers/aws/sam/table.go @@ -0,0 +1,18 @@ +package sam + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SimpleTable struct { + Metadata iacTypes.Metadata + TableName iacTypes.StringValue + SSESpecification SSESpecification +} + +type SSESpecification struct { + Metadata iacTypes.Metadata + + Enabled iacTypes.BoolValue + KMSMasterKeyID iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/sns/sns.go b/pkg/iac/providers/aws/sns/sns.go new file mode 100755 index 000000000000..d502968fae08 --- /dev/null +++ b/pkg/iac/providers/aws/sns/sns.go @@ -0,0 +1,20 @@ +package sns + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SNS struct { + Topics []Topic +} + +type Topic struct { + Metadata iacTypes.Metadata + ARN iacTypes.StringValue + Encryption Encryption +} + +type Encryption struct { + Metadata iacTypes.Metadata + KMSKeyID iacTypes.StringValue +} diff --git a/pkg/iac/providers/aws/sqs/sqs.go b/pkg/iac/providers/aws/sqs/sqs.go new file mode 100755 index 000000000000..edafcb888a22 --- /dev/null +++ b/pkg/iac/providers/aws/sqs/sqs.go @@ -0,0 +1,23 @@ +package sqs + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SQS struct { + Queues []Queue +} + +type Queue struct { + Metadata iacTypes.Metadata + QueueURL iacTypes.StringValue + Encryption Encryption + Policies []iam.Policy +} + +type Encryption struct { + Metadata iacTypes.Metadata + KMSKeyID iacTypes.StringValue + ManagedEncryption iacTypes.BoolValue +} diff --git a/pkg/iac/providers/aws/ssm/ssm.go b/pkg/iac/providers/aws/ssm/ssm.go new file mode 100755 index 000000000000..725e099f2b8d --- /dev/null +++ b/pkg/iac/providers/aws/ssm/ssm.go @@ -0,0 +1,16 @@ +package ssm + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SSM struct { + Secrets []Secret +} + +type Secret struct { + Metadata iacTypes.Metadata + KMSKeyID iacTypes.StringValue +} + +const DefaultKMSKeyID = "alias/aws/secretsmanager" diff --git a/pkg/iac/providers/aws/workspaces/workspaces.go b/pkg/iac/providers/aws/workspaces/workspaces.go new file mode 100755 index 000000000000..beaf56eef645 --- /dev/null +++ b/pkg/iac/providers/aws/workspaces/workspaces.go @@ -0,0 +1,25 @@ +package workspaces + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type WorkSpaces struct { + WorkSpaces []WorkSpace +} + +type WorkSpace struct { + Metadata iacTypes.Metadata + RootVolume Volume + UserVolume Volume +} + +type Volume struct { + Metadata iacTypes.Metadata + Encryption Encryption +} + +type Encryption struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/azure/appservice/appservice.go b/pkg/iac/providers/azure/appservice/appservice.go new file mode 100755 index 000000000000..f597b126b72e --- /dev/null +++ b/pkg/iac/providers/azure/appservice/appservice.go @@ -0,0 +1,44 @@ +package appservice + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type AppService struct { + Services []Service + FunctionApps []FunctionApp +} + +type Identity struct { + Metadata iacTypes.Metadata + Type iacTypes.StringValue +} + +type Authentication struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type Service struct { + Metadata iacTypes.Metadata + Resource iacTypes.StringValue + EnableClientCert iacTypes.BoolValue + HTTPSOnly iacTypes.BoolValue + Identity Identity + Authentication Authentication + Site Site +} + +type Site struct { + Metadata iacTypes.Metadata + EnableHTTP2 iacTypes.BoolValue + MinimumTLSVersion iacTypes.StringValue + PHPVersion iacTypes.StringValue + PythonVersion iacTypes.StringValue + FTPSState iacTypes.StringValue +} + +type FunctionApp struct { + Metadata iacTypes.Metadata + HTTPSOnly iacTypes.BoolValue +} diff --git a/pkg/iac/providers/azure/authorization/authorization.go b/pkg/iac/providers/azure/authorization/authorization.go new file mode 100755 index 000000000000..8211a090e46a --- /dev/null +++ b/pkg/iac/providers/azure/authorization/authorization.go @@ -0,0 +1,29 @@ +package authorization + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Authorization struct { + RoleDefinitions []RoleDefinition + RoleAssignments []RoleAssignment +} + +type RoleDefinition struct { + Metadata iacTypes.Metadata + Permissions []Permission + AssignableScopes []iacTypes.StringValue +} + +type RoleAssignment struct { + Metadata iacTypes.Metadata + RoleDefinitionId iacTypes.StringValue + RoleDefinitionName iacTypes.StringValue + PrincipalId iacTypes.StringValue + PrincipalType iacTypes.StringValue +} + +type Permission struct { + Metadata iacTypes.Metadata + Actions []iacTypes.StringValue +} diff --git a/pkg/iac/providers/azure/azure.go b/pkg/iac/providers/azure/azure.go new file mode 100755 index 000000000000..dc89e6d62e3a --- /dev/null +++ b/pkg/iac/providers/azure/azure.go @@ -0,0 +1,35 @@ +package azure + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/appservice" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/authorization" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/compute" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/container" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/cosmosdb" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/database" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datafactory" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/datalake" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/keyvault" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/monitor" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/securitycenter" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/storage" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/synapse" +) + +type Azure struct { + AppService appservice.AppService + Authorization authorization.Authorization + Compute compute.Compute + Container container.Container + CosmosDB cosmosdb.CosmosDB + Database database.Database + DataFactory datafactory.DataFactory + DataLake datalake.DataLake + KeyVault keyvault.KeyVault + Monitor monitor.Monitor + Network network.Network + SecurityCenter securitycenter.SecurityCenter + Storage storage.Storage + Synapse synapse.Synapse +} diff --git a/pkg/iac/providers/azure/compute/compute.go b/pkg/iac/providers/azure/compute/compute.go new file mode 100755 index 000000000000..98189913d2bf --- /dev/null +++ b/pkg/iac/providers/azure/compute/compute.go @@ -0,0 +1,44 @@ +package compute + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/azure/network" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Compute struct { + LinuxVirtualMachines []LinuxVirtualMachine + WindowsVirtualMachines []WindowsVirtualMachine + ManagedDisks []ManagedDisk +} + +type VirtualMachine struct { + Metadata iacTypes.Metadata + CustomData iacTypes.StringValue // NOT base64 encoded + NetworkInterfaces []network.NetworkInterface +} + +type LinuxVirtualMachine struct { + Metadata iacTypes.Metadata + VirtualMachine + OSProfileLinuxConfig OSProfileLinuxConfig +} + +type WindowsVirtualMachine struct { + Metadata iacTypes.Metadata + VirtualMachine +} + +type OSProfileLinuxConfig struct { + Metadata iacTypes.Metadata + DisablePasswordAuthentication iacTypes.BoolValue +} + +type ManagedDisk struct { + Metadata iacTypes.Metadata + Encryption Encryption +} + +type Encryption struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/azure/container/container.go b/pkg/iac/providers/azure/container/container.go new file mode 100755 index 000000000000..21a35e29dfef --- /dev/null +++ b/pkg/iac/providers/azure/container/container.go @@ -0,0 +1,52 @@ +package container + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Container struct { + KubernetesClusters []KubernetesCluster +} + +type KubernetesCluster struct { + Metadata iacTypes.Metadata + NetworkProfile NetworkProfile + EnablePrivateCluster iacTypes.BoolValue + APIServerAuthorizedIPRanges []iacTypes.StringValue + AddonProfile AddonProfile + RoleBasedAccessControl RoleBasedAccessControl + DiskEncryptionSetID iacTypes.StringValue + AgentPools []AgentPool +} + +type RoleBasedAccessControl struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type AddonProfile struct { + Metadata iacTypes.Metadata + OMSAgent OMSAgent + AzurePolicy AzurePolicy +} + +type OMSAgent struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type AzurePolicy struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type NetworkProfile struct { + Metadata iacTypes.Metadata + NetworkPolicy iacTypes.StringValue // "", "calico", "azure" +} + +type AgentPool struct { + Metadata iacTypes.Metadata + DiskEncryptionSetID iacTypes.StringValue + NodeType iacTypes.StringValue // "VirtualMachineScaleSets" or others +} diff --git a/pkg/iac/providers/azure/cosmosdb/cosmosdb.go b/pkg/iac/providers/azure/cosmosdb/cosmosdb.go new file mode 100644 index 000000000000..aaba8c5a6fd9 --- /dev/null +++ b/pkg/iac/providers/azure/cosmosdb/cosmosdb.go @@ -0,0 +1,12 @@ +package cosmosdb + +import iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + +type CosmosDB struct { + Accounts []Account +} + +type Account struct { + Metadata iacTypes.Metadata + IPRangeFilter []iacTypes.StringValue +} diff --git a/pkg/iac/providers/azure/database/database.go b/pkg/iac/providers/azure/database/database.go new file mode 100755 index 000000000000..b3a705a3b50c --- /dev/null +++ b/pkg/iac/providers/azure/database/database.go @@ -0,0 +1,83 @@ +package database + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Database struct { + MSSQLServers []MSSQLServer + MariaDBServers []MariaDBServer + MySQLServers []MySQLServer + PostgreSQLServers []PostgreSQLServer +} + +type MariaDBServer struct { + Metadata iacTypes.Metadata + Server +} + +type MySQLServer struct { + Metadata iacTypes.Metadata + Server +} + +type PostgreSQLServer struct { + Metadata iacTypes.Metadata + Server + Config PostgresSQLConfig + GeoRedundantBackupEnabled iacTypes.BoolValue + ThreatDetectionPolicy ThreatDetectionPolicy +} + +type PostgresSQLConfig struct { + Metadata iacTypes.Metadata + LogCheckpoints iacTypes.BoolValue + ConnectionThrottling iacTypes.BoolValue + LogConnections iacTypes.BoolValue + LogDisconnections iacTypes.BoolValue +} + +type Server struct { + Metadata iacTypes.Metadata + EnableSSLEnforcement iacTypes.BoolValue + MinimumTLSVersion iacTypes.StringValue + EnablePublicNetworkAccess iacTypes.BoolValue + FirewallRules []FirewallRule +} + +type MSSQLServer struct { + Metadata iacTypes.Metadata + Server + ExtendedAuditingPolicies []ExtendedAuditingPolicy + SecurityAlertPolicies []SecurityAlertPolicy + AdministratorLogin iacTypes.StringValue + ActiveDirectoryAdministrators []ActiveDirectoryAdministrator +} + +type SecurityAlertPolicy struct { + Metadata iacTypes.Metadata + EmailAddresses []iacTypes.StringValue + DisabledAlerts []iacTypes.StringValue + EmailAccountAdmins iacTypes.BoolValue +} + +type ExtendedAuditingPolicy struct { + Metadata iacTypes.Metadata + RetentionInDays iacTypes.IntValue +} + +type FirewallRule struct { + Metadata iacTypes.Metadata + StartIP iacTypes.StringValue + EndIP iacTypes.StringValue +} + +type ThreatDetectionPolicy struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type ActiveDirectoryAdministrator struct { + Metadata iacTypes.Metadata + Login iacTypes.StringValue +} diff --git a/pkg/iac/providers/azure/datafactory/datafactory.go b/pkg/iac/providers/azure/datafactory/datafactory.go new file mode 100755 index 000000000000..c8f0e91d010b --- /dev/null +++ b/pkg/iac/providers/azure/datafactory/datafactory.go @@ -0,0 +1,14 @@ +package datafactory + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type DataFactory struct { + DataFactories []Factory +} + +type Factory struct { + Metadata iacTypes.Metadata + EnablePublicNetwork iacTypes.BoolValue +} diff --git a/pkg/iac/providers/azure/datalake/datalake.go b/pkg/iac/providers/azure/datalake/datalake.go new file mode 100755 index 000000000000..a36f9a12c94a --- /dev/null +++ b/pkg/iac/providers/azure/datalake/datalake.go @@ -0,0 +1,14 @@ +package datalake + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type DataLake struct { + Stores []Store +} + +type Store struct { + Metadata iacTypes.Metadata + EnableEncryption iacTypes.BoolValue +} diff --git a/pkg/iac/providers/azure/keyvault/keyvault.go b/pkg/iac/providers/azure/keyvault/keyvault.go new file mode 100755 index 000000000000..8c263fa413f2 --- /dev/null +++ b/pkg/iac/providers/azure/keyvault/keyvault.go @@ -0,0 +1,34 @@ +package keyvault + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type KeyVault struct { + Vaults []Vault +} + +type Vault struct { + Metadata iacTypes.Metadata + Secrets []Secret + Keys []Key + EnablePurgeProtection iacTypes.BoolValue + SoftDeleteRetentionDays iacTypes.IntValue + NetworkACLs NetworkACLs +} + +type NetworkACLs struct { + Metadata iacTypes.Metadata + DefaultAction iacTypes.StringValue +} + +type Key struct { + Metadata iacTypes.Metadata + ExpiryDate iacTypes.TimeValue +} + +type Secret struct { + Metadata iacTypes.Metadata + ContentType iacTypes.StringValue + ExpiryDate iacTypes.TimeValue +} diff --git a/pkg/iac/providers/azure/monitor/monitor.go b/pkg/iac/providers/azure/monitor/monitor.go new file mode 100755 index 000000000000..e8ba51e40338 --- /dev/null +++ b/pkg/iac/providers/azure/monitor/monitor.go @@ -0,0 +1,22 @@ +package monitor + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Monitor struct { + LogProfiles []LogProfile +} + +type LogProfile struct { + Metadata iacTypes.Metadata + RetentionPolicy RetentionPolicy + Categories []iacTypes.StringValue + Locations []iacTypes.StringValue +} + +type RetentionPolicy struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + Days iacTypes.IntValue +} diff --git a/pkg/iac/providers/azure/network/network.go b/pkg/iac/providers/azure/network/network.go new file mode 100755 index 000000000000..34f22eef5544 --- /dev/null +++ b/pkg/iac/providers/azure/network/network.go @@ -0,0 +1,96 @@ +package network + +import ( + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/common" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Network struct { + SecurityGroups []SecurityGroup + NetworkWatcherFlowLogs []NetworkWatcherFlowLog + NetworkInterfaces []NetworkInterface +} + +type SecurityGroup struct { + Metadata iacTypes.Metadata + Rules []SecurityGroupRule +} + +type SecurityGroupRule struct { + Metadata iacTypes.Metadata + Outbound iacTypes.BoolValue + Allow iacTypes.BoolValue + SourceAddresses []iacTypes.StringValue + SourcePorts []common.PortRange + DestinationAddresses []iacTypes.StringValue + DestinationPorts []common.PortRange + Protocol iacTypes.StringValue +} + +type NetworkWatcherFlowLog struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + RetentionPolicy RetentionPolicy +} + +type RetentionPolicy struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + Days iacTypes.IntValue +} + +type NetworkInterface struct { + Metadata iacTypes.Metadata + EnableIPForwarding iacTypes.BoolValue + SecurityGroups []SecurityGroup + + // Backward compatibility fields. + // These fields represent the primary IP configuration + // (or the first one if 'primary' was not explicitly set). + SubnetID iacTypes.StringValue + HasPublicIP iacTypes.BoolValue + PublicIPAddress iacTypes.StringValue + + IPConfigurations []IPConfiguration +} + +func (ni *NetworkInterface) Setup() { + for i := range ni.IPConfigurations { + c := &ni.IPConfigurations[i] + publicIP := c.PublicIPAddress + c.HasPublicIP = lo.Ternary( + publicIP.GetMetadata().IsResolvable(), + iacTypes.Bool(publicIP.IsNotEmpty(), publicIP.GetMetadata()), + iacTypes.BoolUnresolvable(c.Metadata), + ) + } + + if primaryIpConfig, exists := ni.findPrimaryIpConfig(); exists { + ni.SubnetID = primaryIpConfig.SubnetID + ni.PublicIPAddress = primaryIpConfig.PublicIPAddress + ni.HasPublicIP = primaryIpConfig.HasPublicIP + } +} + +func (ni *NetworkInterface) findPrimaryIpConfig() (IPConfiguration, bool) { + for _, c := range ni.IPConfigurations { + if c.Primary.Value() { + return c, true + } + } + + if len(ni.IPConfigurations) > 0 { + return ni.IPConfigurations[0], true + } + return IPConfiguration{}, false +} + +type IPConfiguration struct { + Metadata iacTypes.Metadata + SubnetID iacTypes.StringValue + Primary iacTypes.BoolValue + HasPublicIP iacTypes.BoolValue + PublicIPAddress iacTypes.StringValue +} diff --git a/pkg/iac/providers/azure/securitycenter/securitycenter.go b/pkg/iac/providers/azure/securitycenter/securitycenter.go new file mode 100755 index 000000000000..29ca048cfa72 --- /dev/null +++ b/pkg/iac/providers/azure/securitycenter/securitycenter.go @@ -0,0 +1,30 @@ +package securitycenter + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SecurityCenter struct { + Contacts []Contact + Subscriptions []SubscriptionPricing +} + +type Contact struct { + Metadata iacTypes.Metadata + EnableAlertNotifications iacTypes.BoolValue + EnableAlertsToAdmins iacTypes.BoolValue + Email iacTypes.StringValue + Phone iacTypes.StringValue + IsEnabled iacTypes.BoolValue + MinimalSeverity iacTypes.StringValue +} + +const ( + TierFree = "Free" + TierStandard = "Standard" +) + +type SubscriptionPricing struct { + Metadata iacTypes.Metadata + Tier iacTypes.StringValue +} diff --git a/pkg/iac/providers/azure/storage/storage.go b/pkg/iac/providers/azure/storage/storage.go new file mode 100755 index 000000000000..b6d3c440367b --- /dev/null +++ b/pkg/iac/providers/azure/storage/storage.go @@ -0,0 +1,77 @@ +package storage + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Storage struct { + Accounts []Account +} + +type Account struct { + Metadata iacTypes.Metadata + NetworkRules []NetworkRule + EnforceHTTPS iacTypes.BoolValue + Containers []Container + QueueProperties QueueProperties + MinimumTLSVersion iacTypes.StringValue + Queues []Queue + PublicNetworkAccess iacTypes.BoolValue + BlobProperties BlobProperties + AccountReplicationType iacTypes.StringValue + InfrastructureEncryptionEnabled iacTypes.BoolValue + CustomerManagedKey CustomerManagedKey +} + +type Queue struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue +} + +type QueueProperties struct { + Metadata iacTypes.Metadata + EnableLogging iacTypes.BoolValue + Logging QueueLogging +} + +type QueueLogging struct { + Metadata iacTypes.Metadata + Delete iacTypes.BoolValue + Read iacTypes.BoolValue + Write iacTypes.BoolValue + Version iacTypes.StringValue + RetentionPolicyDays iacTypes.IntValue +} + +type NetworkRule struct { + Metadata iacTypes.Metadata + Bypass []iacTypes.StringValue + AllowByDefault iacTypes.BoolValue +} + +const ( + PublicAccessOff = "off" + PublicAccessBlob = "blob" + PublicAccessContainer = "container" +) + +type Container struct { + Metadata iacTypes.Metadata + PublicAccess iacTypes.StringValue +} + +type BlobProperties struct { + Metadata iacTypes.Metadata + DeleteRetentionPolicy DeleteRetentionPolicy +} + +type DeleteRetentionPolicy struct { + Metadata iacTypes.Metadata + Days iacTypes.IntValue +} + +type CustomerManagedKey struct { + Metadata iacTypes.Metadata + KeyVaultKeyId iacTypes.StringValue + UserAssignedIdentityId iacTypes.StringValue +} diff --git a/pkg/iac/providers/azure/synapse/synapse.go b/pkg/iac/providers/azure/synapse/synapse.go new file mode 100755 index 000000000000..dc72175d7106 --- /dev/null +++ b/pkg/iac/providers/azure/synapse/synapse.go @@ -0,0 +1,14 @@ +package synapse + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Synapse struct { + Workspaces []Workspace +} + +type Workspace struct { + Metadata iacTypes.Metadata + EnableManagedVirtualNetwork iacTypes.BoolValue +} diff --git a/pkg/iac/providers/cloudstack/cloudstack.go b/pkg/iac/providers/cloudstack/cloudstack.go new file mode 100755 index 000000000000..d514eae513a3 --- /dev/null +++ b/pkg/iac/providers/cloudstack/cloudstack.go @@ -0,0 +1,9 @@ +package cloudstack + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/cloudstack/compute" +) + +type CloudStack struct { + Compute compute.Compute +} diff --git a/pkg/iac/providers/cloudstack/compute/compute.go b/pkg/iac/providers/cloudstack/compute/compute.go new file mode 100755 index 000000000000..cbfa55ba63d4 --- /dev/null +++ b/pkg/iac/providers/cloudstack/compute/compute.go @@ -0,0 +1,14 @@ +package compute + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Compute struct { + Instances []Instance +} + +type Instance struct { + Metadata iacTypes.Metadata + UserData iacTypes.StringValue // not b64 encoded pls +} diff --git a/pkg/iac/providers/digitalocean/compute/compute.go b/pkg/iac/providers/digitalocean/compute/compute.go new file mode 100755 index 000000000000..a0b0fe24761a --- /dev/null +++ b/pkg/iac/providers/digitalocean/compute/compute.go @@ -0,0 +1,50 @@ +package compute + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Compute struct { + Firewalls []Firewall + LoadBalancers []LoadBalancer + Droplets []Droplet + KubernetesClusters []KubernetesCluster +} + +type Firewall struct { + Metadata iacTypes.Metadata + OutboundRules []OutboundFirewallRule + InboundRules []InboundFirewallRule +} + +type KubernetesCluster struct { + Metadata iacTypes.Metadata + SurgeUpgrade iacTypes.BoolValue + AutoUpgrade iacTypes.BoolValue +} + +type LoadBalancer struct { + Metadata iacTypes.Metadata + ForwardingRules []ForwardingRule + RedirectHttpToHttps iacTypes.BoolValue +} + +type ForwardingRule struct { + Metadata iacTypes.Metadata + EntryProtocol iacTypes.StringValue +} + +type OutboundFirewallRule struct { + Metadata iacTypes.Metadata + DestinationAddresses []iacTypes.StringValue +} + +type InboundFirewallRule struct { + Metadata iacTypes.Metadata + SourceAddresses []iacTypes.StringValue +} + +type Droplet struct { + Metadata iacTypes.Metadata + SSHKeys []iacTypes.StringValue +} diff --git a/pkg/iac/providers/digitalocean/digitalocean.go b/pkg/iac/providers/digitalocean/digitalocean.go new file mode 100755 index 000000000000..d56240646279 --- /dev/null +++ b/pkg/iac/providers/digitalocean/digitalocean.go @@ -0,0 +1,11 @@ +package digitalocean + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/digitalocean/compute" + "github.com/aquasecurity/trivy/pkg/iac/providers/digitalocean/spaces" +) + +type DigitalOcean struct { + Compute compute.Compute + Spaces spaces.Spaces +} diff --git a/pkg/iac/providers/digitalocean/spaces/spaces.go b/pkg/iac/providers/digitalocean/spaces/spaces.go new file mode 100755 index 000000000000..2936e469da12 --- /dev/null +++ b/pkg/iac/providers/digitalocean/spaces/spaces.go @@ -0,0 +1,28 @@ +package spaces + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Spaces struct { + Buckets []Bucket +} + +type Bucket struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Objects []Object + ACL iacTypes.StringValue + ForceDestroy iacTypes.BoolValue + Versioning Versioning +} + +type Versioning struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type Object struct { + Metadata iacTypes.Metadata + ACL iacTypes.StringValue +} diff --git a/pkg/iac/providers/dockerfile/dockerfile.go b/pkg/iac/providers/dockerfile/dockerfile.go new file mode 100644 index 000000000000..84b10c42fb7e --- /dev/null +++ b/pkg/iac/providers/dockerfile/dockerfile.go @@ -0,0 +1,61 @@ +package dockerfile + +import ( + "reflect" + + "github.com/aquasecurity/trivy/pkg/iac/rego/convert" +) + +// NOTE: we are currently preserving mixed case json here for backward compatibility + +// Dockerfile represents a parsed Dockerfile +type Dockerfile struct { + Stages []Stage +} + +type Stage struct { + Name string + Commands []Command +} + +func (d Dockerfile) ToRego() any { + return map[string]any{ + "Stages": convert.SliceToRego(reflect.ValueOf(d.Stages)), + } +} + +func (s Stage) ToRego() any { + return map[string]any{ + "Name": s.Name, + "Commands": convert.SliceToRego(reflect.ValueOf(s.Commands)), + } +} + +// Command is the struct for each dockerfile command +type Command struct { + Cmd string + SubCmd string + Flags []string + Value []string + Original string + JSON bool + Stage int + Path string + StartLine int + EndLine int +} + +func (c Command) ToRego() any { + return map[string]any{ + "Cmd": c.Cmd, + "SubCmd": c.SubCmd, + "Flags": c.Flags, + "Value": c.Value, + "Original": c.Original, + "JSON": c.JSON, + "Stage": c.Stage, + "Path": c.Path, + "StartLine": c.StartLine, + "EndLine": c.EndLine, + } +} diff --git a/pkg/iac/providers/github/actions.go b/pkg/iac/providers/github/actions.go new file mode 100644 index 000000000000..0b28269e0c80 --- /dev/null +++ b/pkg/iac/providers/github/actions.go @@ -0,0 +1,19 @@ +package github + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Action struct { + Metadata iacTypes.Metadata + EnvironmentSecrets []EnvironmentSecret +} + +type EnvironmentSecret struct { + Metadata iacTypes.Metadata + Repository iacTypes.StringValue + Environment iacTypes.StringValue + SecretName iacTypes.StringValue + PlainTextValue iacTypes.StringValue + EncryptedValue iacTypes.StringValue +} diff --git a/pkg/iac/providers/github/branch_protections.go b/pkg/iac/providers/github/branch_protections.go new file mode 100755 index 000000000000..6288fa55dec3 --- /dev/null +++ b/pkg/iac/providers/github/branch_protections.go @@ -0,0 +1,10 @@ +package github + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type BranchProtection struct { + Metadata iacTypes.Metadata + RequireSignedCommits iacTypes.BoolValue +} diff --git a/pkg/iac/providers/github/github.go b/pkg/iac/providers/github/github.go new file mode 100755 index 000000000000..449f94cecc30 --- /dev/null +++ b/pkg/iac/providers/github/github.go @@ -0,0 +1,7 @@ +package github + +type GitHub struct { + Repositories []Repository + EnvironmentSecrets []EnvironmentSecret + BranchProtections []BranchProtection +} diff --git a/pkg/iac/providers/github/repositories.go b/pkg/iac/providers/github/repositories.go new file mode 100755 index 000000000000..1917f7721df7 --- /dev/null +++ b/pkg/iac/providers/github/repositories.go @@ -0,0 +1,12 @@ +package github + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Repository struct { + Metadata iacTypes.Metadata + Public iacTypes.BoolValue + VulnerabilityAlerts iacTypes.BoolValue + Archived iacTypes.BoolValue +} diff --git a/pkg/iac/providers/google/bigquery/bigquery.go b/pkg/iac/providers/google/bigquery/bigquery.go new file mode 100755 index 000000000000..9a9bfb054dde --- /dev/null +++ b/pkg/iac/providers/google/bigquery/bigquery.go @@ -0,0 +1,26 @@ +package bigquery + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type BigQuery struct { + Datasets []Dataset +} + +type Dataset struct { + Metadata iacTypes.Metadata + ID iacTypes.StringValue + AccessGrants []AccessGrant +} + +const ( + SpecialGroupAllAuthenticatedUsers = "allAuthenticatedUsers" +) + +type AccessGrant struct { + Metadata iacTypes.Metadata + Role iacTypes.StringValue + Domain iacTypes.StringValue + SpecialGroup iacTypes.StringValue +} diff --git a/pkg/iac/providers/google/compute/compute.go b/pkg/iac/providers/google/compute/compute.go new file mode 100755 index 000000000000..ffa9db257bad --- /dev/null +++ b/pkg/iac/providers/google/compute/compute.go @@ -0,0 +1,9 @@ +package compute + +type Compute struct { + Disks []Disk + Networks []Network + SSLPolicies []SSLPolicy + ProjectMetadata ProjectMetadata + Instances []Instance +} diff --git a/pkg/iac/providers/google/compute/disk.go b/pkg/iac/providers/google/compute/disk.go new file mode 100755 index 000000000000..b636cb135651 --- /dev/null +++ b/pkg/iac/providers/google/compute/disk.go @@ -0,0 +1,17 @@ +package compute + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Disk struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Encryption DiskEncryption +} + +type DiskEncryption struct { + Metadata iacTypes.Metadata + RawKey iacTypes.BytesValue + KMSKeyLink iacTypes.StringValue +} diff --git a/pkg/iac/providers/google/compute/firewall.go b/pkg/iac/providers/google/compute/firewall.go new file mode 100755 index 000000000000..40b83512a258 --- /dev/null +++ b/pkg/iac/providers/google/compute/firewall.go @@ -0,0 +1,35 @@ +package compute + +import ( + "github.com/aquasecurity/trivy/pkg/iac/adapters/common" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Firewall struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + IngressRules []IngressRule + EgressRules []EgressRule + SourceTags []iacTypes.StringValue + TargetTags []iacTypes.StringValue +} + +type FirewallRule struct { + Metadata iacTypes.Metadata + Enforced iacTypes.BoolValue + IsAllow iacTypes.BoolValue + Protocol iacTypes.StringValue + Ports []common.PortRange +} + +type IngressRule struct { + Metadata iacTypes.Metadata + FirewallRule + SourceRanges []iacTypes.StringValue +} + +type EgressRule struct { + Metadata iacTypes.Metadata + FirewallRule + DestinationRanges []iacTypes.StringValue +} diff --git a/pkg/iac/providers/google/compute/instance.go b/pkg/iac/providers/google/compute/instance.go new file mode 100755 index 000000000000..d5189945f3b6 --- /dev/null +++ b/pkg/iac/providers/google/compute/instance.go @@ -0,0 +1,41 @@ +package compute + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Instance struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + NetworkInterfaces []NetworkInterface + ShieldedVM ShieldedVMConfig + ServiceAccount ServiceAccount + CanIPForward iacTypes.BoolValue + OSLoginEnabled iacTypes.BoolValue + EnableProjectSSHKeyBlocking iacTypes.BoolValue + EnableSerialPort iacTypes.BoolValue + BootDisks []Disk + AttachedDisks []Disk +} + +type ServiceAccount struct { + Metadata iacTypes.Metadata + Email iacTypes.StringValue + IsDefault iacTypes.BoolValue + Scopes []iacTypes.StringValue +} + +type NetworkInterface struct { + Metadata iacTypes.Metadata + Network *Network + SubNetwork *SubNetwork + HasPublicIP iacTypes.BoolValue + NATIP iacTypes.StringValue +} + +type ShieldedVMConfig struct { + Metadata iacTypes.Metadata + SecureBootEnabled iacTypes.BoolValue + IntegrityMonitoringEnabled iacTypes.BoolValue + VTPMEnabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/google/compute/metadata.go b/pkg/iac/providers/google/compute/metadata.go new file mode 100755 index 000000000000..65d3b5968f4f --- /dev/null +++ b/pkg/iac/providers/google/compute/metadata.go @@ -0,0 +1,16 @@ +package compute + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type MetadataFlags struct { + EnableOSLogin iacTypes.BoolValue + BlockProjectSSHKeys iacTypes.BoolValue + EnableSerialPort iacTypes.BoolValue +} + +type ProjectMetadata struct { + Metadata iacTypes.Metadata + EnableOSLogin iacTypes.BoolValue +} diff --git a/pkg/iac/providers/google/compute/network.go b/pkg/iac/providers/google/compute/network.go new file mode 100755 index 000000000000..c8e7d0b3d56c --- /dev/null +++ b/pkg/iac/providers/google/compute/network.go @@ -0,0 +1,11 @@ +package compute + +import ( + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Network struct { + Metadata types.Metadata + Firewall *Firewall + Subnetworks []SubNetwork +} diff --git a/pkg/iac/providers/google/compute/ssl_policy.go b/pkg/iac/providers/google/compute/ssl_policy.go new file mode 100755 index 000000000000..38ee07c047f4 --- /dev/null +++ b/pkg/iac/providers/google/compute/ssl_policy.go @@ -0,0 +1,12 @@ +package compute + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SSLPolicy struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Profile iacTypes.StringValue + MinimumTLSVersion iacTypes.StringValue +} diff --git a/pkg/iac/providers/google/compute/subnetwork.go b/pkg/iac/providers/google/compute/subnetwork.go new file mode 100755 index 000000000000..4eac339c7443 --- /dev/null +++ b/pkg/iac/providers/google/compute/subnetwork.go @@ -0,0 +1,13 @@ +package compute + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SubNetwork struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Purpose iacTypes.StringValue + EnableFlowLogs iacTypes.BoolValue + PrivateIPGoogleAccess iacTypes.BoolValue +} diff --git a/pkg/iac/providers/google/dns/dns.go b/pkg/iac/providers/google/dns/dns.go new file mode 100755 index 000000000000..3ce557f2b17a --- /dev/null +++ b/pkg/iac/providers/google/dns/dns.go @@ -0,0 +1,27 @@ +package dns + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type DNS struct { + ManagedZones []ManagedZone +} + +type ManagedZone struct { + Metadata iacTypes.Metadata + DNSSec DNSSec + Visibility iacTypes.StringValue +} + +type DNSSec struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + DefaultKeySpecs []KeySpecs +} + +type KeySpecs struct { + Metadata iacTypes.Metadata + Algorithm iacTypes.StringValue + KeyType iacTypes.StringValue +} diff --git a/pkg/iac/providers/google/gke/gke.go b/pkg/iac/providers/google/gke/gke.go new file mode 100755 index 000000000000..6b2431adef75 --- /dev/null +++ b/pkg/iac/providers/google/gke/gke.go @@ -0,0 +1,100 @@ +package gke + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type GKE struct { + Clusters []Cluster +} + +type Cluster struct { + Metadata iacTypes.Metadata + NodePools []NodePool + IPAllocationPolicy IPAllocationPolicy + MasterAuthorizedNetworks MasterAuthorizedNetworks + NetworkPolicy NetworkPolicy + PrivateCluster PrivateCluster + LoggingService iacTypes.StringValue + MonitoringService iacTypes.StringValue + MasterAuth MasterAuth + NodeConfig NodeConfig + AutoScaling AutoScaling + EnableShieldedNodes iacTypes.BoolValue + EnableLegacyABAC iacTypes.BoolValue + ResourceLabels iacTypes.MapValue + RemoveDefaultNodePool iacTypes.BoolValue + EnableAutpilot iacTypes.BoolValue + DatapathProvider iacTypes.StringValue +} + +type NodeConfig struct { + Metadata iacTypes.Metadata + ImageType iacTypes.StringValue + WorkloadMetadataConfig WorkloadMetadataConfig + ServiceAccount iacTypes.StringValue + EnableLegacyEndpoints iacTypes.BoolValue +} + +type AutoScaling struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + AutoProvisioningDefaults AutoProvisioningDefaults +} + +type AutoProvisioningDefaults struct { + Metadata iacTypes.Metadata + ImageType iacTypes.StringValue + ServiceAccount iacTypes.StringValue + Management Management +} + +type WorkloadMetadataConfig struct { + Metadata iacTypes.Metadata + NodeMetadata iacTypes.StringValue +} + +type MasterAuth struct { + Metadata iacTypes.Metadata + ClientCertificate ClientCertificate + Username iacTypes.StringValue + Password iacTypes.StringValue +} + +type ClientCertificate struct { + Metadata iacTypes.Metadata + IssueCertificate iacTypes.BoolValue +} + +type PrivateCluster struct { + Metadata iacTypes.Metadata + EnablePrivateNodes iacTypes.BoolValue +} + +type NetworkPolicy struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type MasterAuthorizedNetworks struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue + CIDRs []iacTypes.StringValue +} + +type IPAllocationPolicy struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type NodePool struct { + Metadata iacTypes.Metadata + Management Management + NodeConfig NodeConfig +} + +type Management struct { + Metadata iacTypes.Metadata + EnableAutoRepair iacTypes.BoolValue + EnableAutoUpgrade iacTypes.BoolValue +} diff --git a/pkg/iac/providers/google/google.go b/pkg/iac/providers/google/google.go new file mode 100755 index 000000000000..4616c9f0089d --- /dev/null +++ b/pkg/iac/providers/google/google.go @@ -0,0 +1,23 @@ +package google + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/google/bigquery" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/compute" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/dns" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/gke" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/kms" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/sql" + "github.com/aquasecurity/trivy/pkg/iac/providers/google/storage" +) + +type Google struct { + BigQuery bigquery.BigQuery + Compute compute.Compute + DNS dns.DNS + GKE gke.GKE + KMS kms.KMS + IAM iam.IAM + SQL sql.SQL + Storage storage.Storage +} diff --git a/pkg/iac/providers/google/iam/iam.go b/pkg/iac/providers/google/iam/iam.go new file mode 100755 index 000000000000..8b2e93886f65 --- /dev/null +++ b/pkg/iac/providers/google/iam/iam.go @@ -0,0 +1,67 @@ +package iam + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type IAM struct { + Organizations []Organization + WorkloadIdentityPoolProviders []WorkloadIdentityPoolProvider + Projects []Project + Folders []Folder +} + +type Organization struct { + Metadata iacTypes.Metadata + Members []Member + Bindings []Binding + AuditConfigs []AuditConfig +} + +type Folder struct { + Metadata iacTypes.Metadata + Members []Member + Bindings []Binding + AuditConfigs []AuditConfig +} + +type Project struct { + Metadata iacTypes.Metadata + AutoCreateNetwork iacTypes.BoolValue + Members []Member + Bindings []Binding + AuditConfigs []AuditConfig +} + +type Binding struct { + Metadata iacTypes.Metadata + Members []iacTypes.StringValue + Role iacTypes.StringValue + IncludesDefaultServiceAccount iacTypes.BoolValue +} + +type Member struct { + Metadata iacTypes.Metadata + Member iacTypes.StringValue + Role iacTypes.StringValue + DefaultServiceAccount iacTypes.BoolValue +} + +type AuditConfig struct { + Metadata iacTypes.Metadata + Service iacTypes.StringValue + AuditLogConfigs []AuditLogConfig +} + +type AuditLogConfig struct { + Metadata iacTypes.Metadata + LogType iacTypes.StringValue + ExemptedMembers []iacTypes.StringValue +} + +type WorkloadIdentityPoolProvider struct { + Metadata iacTypes.Metadata + WorkloadIdentityPoolId iacTypes.StringValue + WorkloadIdentityPoolProviderId iacTypes.StringValue + AttributeCondition iacTypes.StringValue +} diff --git a/pkg/iac/providers/google/kms/kms.go b/pkg/iac/providers/google/kms/kms.go new file mode 100755 index 000000000000..4247db119e13 --- /dev/null +++ b/pkg/iac/providers/google/kms/kms.go @@ -0,0 +1,19 @@ +package kms + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type KMS struct { + KeyRings []KeyRing +} + +type KeyRing struct { + Metadata iacTypes.Metadata + Keys []Key +} + +type Key struct { + Metadata iacTypes.Metadata + RotationPeriodSeconds iacTypes.IntValue +} diff --git a/pkg/iac/providers/google/sql/sql.go b/pkg/iac/providers/google/sql/sql.go new file mode 100755 index 000000000000..e3cc7061651d --- /dev/null +++ b/pkg/iac/providers/google/sql/sql.go @@ -0,0 +1,73 @@ +package sql + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SQL struct { + Instances []DatabaseInstance +} + +const ( + DatabaseFamilyMySQL = "MYSQL" + DatabaseFamilyPostgres = "POSTGRES" + DatabaseFamilySQLServer = "SQLSERVER" +) + +const ( + DatabaseVersionMySQL_5_6 = "MYSQL_5_6" + DatabaseVersionMySQL_5_7 = "MYSQL_5_7" + DatabaseVersionMySQL_8_0 = "MYSQL_8_0" + DatabaseVersionPostgres_9_6 = "POSTGRES_9_6" + DatabaseVersionPostgres_10 = "POSTGRES_10" + DatabaseVersionPostgres_11 = "POSTGRES_11" + DatabaseVersionPostgres_12 = "POSTGRES_12" + DatabaseVersionPostgres_13 = "POSTGRES_13" + DatabaseVersionSQLServer_2017_STANDARD = "SQLSERVER_2017_STANDARD" + DatabaseVersionSQLServer_2017_ENTERPRISE = "SQLSERVER_2017_ENTERPRISE" + DatabaseVersionSQLServer_2017_EXPRESS = "SQLSERVER_2017_EXPRESS" + DatabaseVersionSQLServer_2017_WEB = "SQLSERVER_2017_WEB" +) + +type DatabaseInstance struct { + Metadata iacTypes.Metadata + DatabaseVersion iacTypes.StringValue + Settings Settings + IsReplica iacTypes.BoolValue +} + +type Settings struct { + Metadata iacTypes.Metadata + Flags Flags + Backups Backups + IPConfiguration IPConfiguration +} +type Flags struct { + Metadata iacTypes.Metadata + LogTempFileSize iacTypes.IntValue + LocalInFile iacTypes.BoolValue + ContainedDatabaseAuthentication iacTypes.BoolValue + CrossDBOwnershipChaining iacTypes.BoolValue + LogCheckpoints iacTypes.BoolValue + LogConnections iacTypes.BoolValue + LogDisconnections iacTypes.BoolValue + LogLockWaits iacTypes.BoolValue + LogMinMessages iacTypes.StringValue // FATAL, PANIC, LOG, ERROR, WARN + LogMinDurationStatement iacTypes.IntValue +} + +type Backups struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} + +type IPConfiguration struct { + Metadata iacTypes.Metadata + RequireTLS iacTypes.BoolValue + SSLMode iacTypes.StringValue + EnableIPv4 iacTypes.BoolValue + AuthorizedNetworks []struct { + Name iacTypes.StringValue + CIDR iacTypes.StringValue + } +} diff --git a/pkg/iac/providers/google/storage/storage.go b/pkg/iac/providers/google/storage/storage.go new file mode 100755 index 000000000000..c02ca7fccc51 --- /dev/null +++ b/pkg/iac/providers/google/storage/storage.go @@ -0,0 +1,38 @@ +package storage + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/google/iam" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Storage struct { + Buckets []Bucket +} + +type Bucket struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Location iacTypes.StringValue + EnableUniformBucketLevelAccess iacTypes.BoolValue + Members []iam.Member + Bindings []iam.Binding + Encryption BucketEncryption + Logging BucketLogging + Versioning BucketVersioning +} + +type BucketEncryption struct { + Metadata iacTypes.Metadata + DefaultKMSKeyName iacTypes.StringValue +} + +type BucketLogging struct { + Metadata iacTypes.Metadata + LogBucket iacTypes.StringValue + LogObjectPrefix iacTypes.StringValue +} + +type BucketVersioning struct { + Metadata iacTypes.Metadata + Enabled iacTypes.BoolValue +} diff --git a/pkg/iac/providers/kubernetes/kubernetes.go b/pkg/iac/providers/kubernetes/kubernetes.go new file mode 100755 index 000000000000..cf71291161a1 --- /dev/null +++ b/pkg/iac/providers/kubernetes/kubernetes.go @@ -0,0 +1,38 @@ +package kubernetes + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Kubernetes struct { + NetworkPolicies []NetworkPolicy +} + +type NetworkPolicy struct { + Metadata iacTypes.Metadata + Spec NetworkPolicySpec +} + +type NetworkPolicySpec struct { + Metadata iacTypes.Metadata + Egress Egress + Ingress Ingress +} + +type Egress struct { + Metadata iacTypes.Metadata + Ports []Port + DestinationCIDRs []iacTypes.StringValue +} + +type Ingress struct { + Metadata iacTypes.Metadata + Ports []Port + SourceCIDRs []iacTypes.StringValue +} + +type Port struct { + Metadata iacTypes.Metadata + Number iacTypes.StringValue // e.g. "http" or "80" + Protocol iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/computing/computing.go b/pkg/iac/providers/nifcloud/computing/computing.go new file mode 100755 index 000000000000..aaef2361bf98 --- /dev/null +++ b/pkg/iac/providers/nifcloud/computing/computing.go @@ -0,0 +1,6 @@ +package computing + +type Computing struct { + SecurityGroups []SecurityGroup + Instances []Instance +} diff --git a/pkg/iac/providers/nifcloud/computing/instance.go b/pkg/iac/providers/nifcloud/computing/instance.go new file mode 100644 index 000000000000..2b729920f37f --- /dev/null +++ b/pkg/iac/providers/nifcloud/computing/instance.go @@ -0,0 +1,16 @@ +package computing + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Instance struct { + Metadata iacTypes.Metadata + SecurityGroup iacTypes.StringValue + NetworkInterfaces []NetworkInterface +} + +type NetworkInterface struct { + Metadata iacTypes.Metadata + NetworkID iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/computing/security_group.go b/pkg/iac/providers/nifcloud/computing/security_group.go new file mode 100644 index 000000000000..f707d8a2825a --- /dev/null +++ b/pkg/iac/providers/nifcloud/computing/security_group.go @@ -0,0 +1,18 @@ +package computing + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SecurityGroup struct { + Metadata iacTypes.Metadata + Description iacTypes.StringValue + IngressRules []SecurityGroupRule + EgressRules []SecurityGroupRule +} + +type SecurityGroupRule struct { + Metadata iacTypes.Metadata + Description iacTypes.StringValue + CIDR iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/dns/dns.go b/pkg/iac/providers/nifcloud/dns/dns.go new file mode 100755 index 000000000000..7351506d7f6f --- /dev/null +++ b/pkg/iac/providers/nifcloud/dns/dns.go @@ -0,0 +1,5 @@ +package dns + +type DNS struct { + Records []Record +} diff --git a/pkg/iac/providers/nifcloud/dns/record.go b/pkg/iac/providers/nifcloud/dns/record.go new file mode 100644 index 000000000000..44275dfb27d0 --- /dev/null +++ b/pkg/iac/providers/nifcloud/dns/record.go @@ -0,0 +1,13 @@ +package dns + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +const ZoneRegistrationAuthTxt = "nifty-dns-verify=" + +type Record struct { + Metadata iacTypes.Metadata + Type iacTypes.StringValue + Record iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/nas/nas.go b/pkg/iac/providers/nifcloud/nas/nas.go new file mode 100755 index 000000000000..e73a9c9efd70 --- /dev/null +++ b/pkg/iac/providers/nifcloud/nas/nas.go @@ -0,0 +1,6 @@ +package nas + +type NAS struct { + NASSecurityGroups []NASSecurityGroup + NASInstances []NASInstance +} diff --git a/pkg/iac/providers/nifcloud/nas/nas_instance.go b/pkg/iac/providers/nifcloud/nas/nas_instance.go new file mode 100644 index 000000000000..90567629ba09 --- /dev/null +++ b/pkg/iac/providers/nifcloud/nas/nas_instance.go @@ -0,0 +1,10 @@ +package nas + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type NASInstance struct { + Metadata iacTypes.Metadata + NetworkID iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/nas/nas_security_group.go b/pkg/iac/providers/nifcloud/nas/nas_security_group.go new file mode 100644 index 000000000000..f5351f7ab150 --- /dev/null +++ b/pkg/iac/providers/nifcloud/nas/nas_security_group.go @@ -0,0 +1,11 @@ +package nas + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type NASSecurityGroup struct { + Metadata iacTypes.Metadata + Description iacTypes.StringValue + CIDRs []iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/network/elastic_load_balancer.go b/pkg/iac/providers/nifcloud/network/elastic_load_balancer.go new file mode 100644 index 000000000000..82d977fcf8fa --- /dev/null +++ b/pkg/iac/providers/nifcloud/network/elastic_load_balancer.go @@ -0,0 +1,16 @@ +package network + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type ElasticLoadBalancer struct { + Metadata iacTypes.Metadata + NetworkInterfaces []NetworkInterface + Listeners []ElasticLoadBalancerListener +} + +type ElasticLoadBalancerListener struct { + Metadata iacTypes.Metadata + Protocol iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/network/load_balancer.go b/pkg/iac/providers/nifcloud/network/load_balancer.go new file mode 100644 index 000000000000..860a50614f8a --- /dev/null +++ b/pkg/iac/providers/nifcloud/network/load_balancer.go @@ -0,0 +1,16 @@ +package network + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type LoadBalancer struct { + Metadata iacTypes.Metadata + Listeners []LoadBalancerListener +} + +type LoadBalancerListener struct { + Metadata iacTypes.Metadata + Protocol iacTypes.StringValue + TLSPolicy iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/network/network.go b/pkg/iac/providers/nifcloud/network/network.go new file mode 100755 index 000000000000..dc337e0a2470 --- /dev/null +++ b/pkg/iac/providers/nifcloud/network/network.go @@ -0,0 +1,16 @@ +package network + +import iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + +type Network struct { + ElasticLoadBalancers []ElasticLoadBalancer + LoadBalancers []LoadBalancer + Routers []Router + VpnGateways []VpnGateway +} + +type NetworkInterface struct { + Metadata iacTypes.Metadata + NetworkID iacTypes.StringValue + IsVipNetwork iacTypes.BoolValue +} diff --git a/pkg/iac/providers/nifcloud/network/router.go b/pkg/iac/providers/nifcloud/network/router.go new file mode 100644 index 000000000000..c26aa9115729 --- /dev/null +++ b/pkg/iac/providers/nifcloud/network/router.go @@ -0,0 +1,11 @@ +package network + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Router struct { + Metadata iacTypes.Metadata + SecurityGroup iacTypes.StringValue + NetworkInterfaces []NetworkInterface +} diff --git a/pkg/iac/providers/nifcloud/network/vpn_gateway.go b/pkg/iac/providers/nifcloud/network/vpn_gateway.go new file mode 100644 index 000000000000..54ff74aab3d4 --- /dev/null +++ b/pkg/iac/providers/nifcloud/network/vpn_gateway.go @@ -0,0 +1,10 @@ +package network + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type VpnGateway struct { + Metadata iacTypes.Metadata + SecurityGroup iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/nifcloud.go b/pkg/iac/providers/nifcloud/nifcloud.go new file mode 100755 index 000000000000..4126407925c6 --- /dev/null +++ b/pkg/iac/providers/nifcloud/nifcloud.go @@ -0,0 +1,19 @@ +package nifcloud + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/computing" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/dns" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/nas" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/network" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/rdb" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud/sslcertificate" +) + +type Nifcloud struct { + Computing computing.Computing + DNS dns.DNS + NAS nas.NAS + Network network.Network + RDB rdb.RDB + SSLCertificate sslcertificate.SSLCertificate +} diff --git a/pkg/iac/providers/nifcloud/rdb/db_instance.go b/pkg/iac/providers/nifcloud/rdb/db_instance.go new file mode 100644 index 000000000000..be888d09210e --- /dev/null +++ b/pkg/iac/providers/nifcloud/rdb/db_instance.go @@ -0,0 +1,14 @@ +package rdb + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type DBInstance struct { + Metadata iacTypes.Metadata + BackupRetentionPeriodDays iacTypes.IntValue + Engine iacTypes.StringValue + EngineVersion iacTypes.StringValue + NetworkID iacTypes.StringValue + PublicAccess iacTypes.BoolValue +} diff --git a/pkg/iac/providers/nifcloud/rdb/db_security_group.go b/pkg/iac/providers/nifcloud/rdb/db_security_group.go new file mode 100644 index 000000000000..3c9e350e5cad --- /dev/null +++ b/pkg/iac/providers/nifcloud/rdb/db_security_group.go @@ -0,0 +1,11 @@ +package rdb + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type DBSecurityGroup struct { + Metadata iacTypes.Metadata + Description iacTypes.StringValue + CIDRs []iacTypes.StringValue +} diff --git a/pkg/iac/providers/nifcloud/rdb/rdb.go b/pkg/iac/providers/nifcloud/rdb/rdb.go new file mode 100755 index 000000000000..4aea31980708 --- /dev/null +++ b/pkg/iac/providers/nifcloud/rdb/rdb.go @@ -0,0 +1,6 @@ +package rdb + +type RDB struct { + DBSecurityGroups []DBSecurityGroup + DBInstances []DBInstance +} diff --git a/pkg/iac/providers/nifcloud/sslcertificate/server_certificate.go b/pkg/iac/providers/nifcloud/sslcertificate/server_certificate.go new file mode 100644 index 000000000000..86dc479851a3 --- /dev/null +++ b/pkg/iac/providers/nifcloud/sslcertificate/server_certificate.go @@ -0,0 +1,10 @@ +package sslcertificate + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type ServerCertificate struct { + Metadata iacTypes.Metadata + Expiration iacTypes.TimeValue +} diff --git a/pkg/iac/providers/nifcloud/sslcertificate/ssl_certificate.go b/pkg/iac/providers/nifcloud/sslcertificate/ssl_certificate.go new file mode 100755 index 000000000000..7ab46d870b16 --- /dev/null +++ b/pkg/iac/providers/nifcloud/sslcertificate/ssl_certificate.go @@ -0,0 +1,5 @@ +package sslcertificate + +type SSLCertificate struct { + ServerCertificates []ServerCertificate +} diff --git a/pkg/iac/providers/openstack/networking.go b/pkg/iac/providers/openstack/networking.go new file mode 100644 index 000000000000..944bc3fe9253 --- /dev/null +++ b/pkg/iac/providers/openstack/networking.go @@ -0,0 +1,27 @@ +package openstack + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Networking struct { + SecurityGroups []SecurityGroup +} + +type SecurityGroup struct { + Metadata iacTypes.Metadata + Name iacTypes.StringValue + Description iacTypes.StringValue + Rules []SecurityGroupRule +} + +// SecurityGroupRule describes https://registry.terraform.io/providers/terraform-provider-openstack/openstack/latest/docs/resources/networking_secgroup_rule_v2 +type SecurityGroupRule struct { + Metadata iacTypes.Metadata + IsIngress iacTypes.BoolValue + EtherType iacTypes.IntValue // 4 or 6 for ipv4/ipv6 + Protocol iacTypes.StringValue // e.g. tcp + PortMin iacTypes.IntValue + PortMax iacTypes.IntValue + CIDR iacTypes.StringValue +} diff --git a/pkg/iac/providers/openstack/openstack.go b/pkg/iac/providers/openstack/openstack.go new file mode 100755 index 000000000000..04a23f28fcee --- /dev/null +++ b/pkg/iac/providers/openstack/openstack.go @@ -0,0 +1,34 @@ +package openstack + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type OpenStack struct { + Compute Compute + Networking Networking +} + +type Compute struct { + Instances []Instance + Firewall Firewall +} + +type Firewall struct { + AllowRules []FirewallRule + DenyRules []FirewallRule +} + +type FirewallRule struct { + Metadata iacTypes.Metadata + Source iacTypes.StringValue + Destination iacTypes.StringValue + SourcePort iacTypes.StringValue + DestinationPort iacTypes.StringValue + Enabled iacTypes.BoolValue +} + +type Instance struct { + Metadata iacTypes.Metadata + AdminPassword iacTypes.StringValue +} diff --git a/pkg/iac/providers/oracle/oracle.go b/pkg/iac/providers/oracle/oracle.go new file mode 100755 index 000000000000..6d6a3ecbdfe5 --- /dev/null +++ b/pkg/iac/providers/oracle/oracle.go @@ -0,0 +1,18 @@ +package oracle + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Oracle struct { + Compute Compute +} + +type Compute struct { + AddressReservations []AddressReservation +} + +type AddressReservation struct { + Metadata iacTypes.Metadata + Pool iacTypes.StringValue // e.g. public-pool +} diff --git a/pkg/iac/providers/provider.go b/pkg/iac/providers/provider.go new file mode 100755 index 000000000000..46dbf19ec43c --- /dev/null +++ b/pkg/iac/providers/provider.go @@ -0,0 +1,58 @@ +package providers + +import ( + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// Provider is the provider that the check applies to +type Provider string + +const ( + UnknownProvider Provider = "" + AWSProvider Provider = "aws" + AzureProvider Provider = "azure" + CustomProvider Provider = "custom" + DigitalOceanProvider Provider = "digitalocean" + GeneralProvider Provider = "general" + GitHubProvider Provider = "github" + GoogleProvider Provider = "google" + KubernetesProvider Provider = "kubernetes" + OracleProvider Provider = "oracle" + OpenStackProvider Provider = "openstack" + NifcloudProvider Provider = "nifcloud" + CloudStackProvider Provider = "cloudstack" +) + +func AllProviders() []Provider { + return []Provider{ + AWSProvider, AzureProvider, DigitalOceanProvider, GitHubProvider, GoogleProvider, + KubernetesProvider, OracleProvider, OpenStackProvider, NifcloudProvider, CloudStackProvider, + } +} + +func RuleProviderToString(provider Provider) string { + return strings.ToUpper(string(provider)) +} + +func (p Provider) DisplayName() string { + switch p { + case "aws": + return strings.ToUpper(string(p)) + case "digitalocean": + return "Digital Ocean" + case "github": + return "GitHub" + case "openstack": + return "OpenStack" + case "cloudstack": + return "Cloudstack" + default: + return cases.Title(language.English).String(strings.ToLower(string(p))) + } +} +func (p Provider) ConstName() string { + return strings.ReplaceAll(p.DisplayName(), " ", "") +} diff --git a/pkg/iac/rego/build.go b/pkg/iac/rego/build.go new file mode 100644 index 000000000000..643635d84a7a --- /dev/null +++ b/pkg/iac/rego/build.go @@ -0,0 +1,99 @@ +package rego + +import ( + "fmt" + "io/fs" + "path/filepath" + "strings" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/util" + + "github.com/aquasecurity/trivy/pkg/iac/rego/schemas" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func BuildSchemaSetFromPolicies(policies map[string]*ast.Module, paths []string, fsys fs.FS, customSchemas map[string][]byte) (*ast.SchemaSet, error) { + schemaSet := ast.NewSchemaSet() + schemaSet.Put(ast.MustParseRef("schema.input"), make(map[string]any)) // for backwards compat only + + for _, policy := range policies { + for _, annotation := range policy.Annotations { + for _, ss := range annotation.Schemas { + schemaName, err := ss.Schema.Ptr() + if err != nil || schemaName == "input" { // for backwards compat only + continue + } + + if schemaSet.Get(ss.Schema) != nil { + continue + } + + var schema []byte + if s, ok := schemas.SchemaMap[types.Source(schemaName)]; ok { + schema = []byte(s) + } else if s, ok := customSchemas[schemaName]; ok { + schema = s + } else { + b, err := findSchemaInFS(paths, fsys, schemaName) + if err != nil { + return nil, err + } + + if b == nil { + return nil, fmt.Errorf("could not find schema %q", schemaName) + } + + schema = b + } + + var rawSchema any + if err := util.UnmarshalJSON(schema, &rawSchema); err != nil { + return schemaSet, fmt.Errorf("could not parse schema %q: %w", schemaName, err) + } + schemaSet.Put(ss.Schema, rawSchema) + } + } + } + + return schemaSet, nil +} + +// findSchemaInFS tries to find the schema anywhere in the specified FS +func findSchemaInFS(paths []string, srcFS fs.FS, schemaName string) ([]byte, error) { + var schema []byte + for _, path := range paths { + if err := fs.WalkDir(srcFS, sanitisePath(path), func(path string, info fs.DirEntry, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + if !IsJSONFile(info.Name()) { + return nil + } + if info.Name() == schemaName+".json" { + schema, err = fs.ReadFile(srcFS, filepath.ToSlash(path)) + if err != nil { + return err + } + return nil + } + return nil + }); err != nil { + return nil, err + } + } + return schema, nil +} + +func IsJSONFile(name string) bool { + return strings.HasSuffix(name, ".json") +} + +func sanitisePath(path string) string { + vol := filepath.VolumeName(path) + path = strings.TrimPrefix(path, vol) + return strings.TrimPrefix(strings.TrimPrefix(filepath.ToSlash(path), "./"), "/") +} diff --git a/pkg/iac/rego/convert/anonymous.go b/pkg/iac/rego/convert/anonymous.go new file mode 100644 index 000000000000..069ea0d06d88 --- /dev/null +++ b/pkg/iac/rego/convert/anonymous.go @@ -0,0 +1,47 @@ +package convert + +import ( + "reflect" +) + +var converterInterface = reflect.TypeOf((*Converter)(nil)).Elem() + +func anonymousToRego(inputValue reflect.Value) any { + + if inputValue.IsZero() { + return nil + } + + for inputValue.Type().Kind() == reflect.Interface { + if inputValue.IsNil() { + return nil + } + inputValue = inputValue.Elem() + } + + if inputValue.Type().Implements(converterInterface) { + returns := inputValue.MethodByName("ToRego").Call(nil) + return returns[0].Interface() + } + + for inputValue.Type().Kind() == reflect.Ptr { + if inputValue.IsNil() { + return nil + } + inputValue = inputValue.Elem() + } + + if inputValue.Type().Implements(converterInterface) { + returns := inputValue.MethodByName("ToRego").Call(nil) + return returns[0].Interface() + } + + switch kind := inputValue.Type().Kind(); kind { + case reflect.Struct: + return StructToRego(inputValue) + case reflect.Slice: + return SliceToRego(inputValue) + } + + return nil +} diff --git a/pkg/iac/rego/convert/converter.go b/pkg/iac/rego/convert/converter.go new file mode 100644 index 000000000000..880050c0eb6a --- /dev/null +++ b/pkg/iac/rego/convert/converter.go @@ -0,0 +1,5 @@ +package convert + +type Converter interface { + ToRego() any +} diff --git a/pkg/iac/rego/convert/slice.go b/pkg/iac/rego/convert/slice.go new file mode 100644 index 000000000000..02c7dcff92a0 --- /dev/null +++ b/pkg/iac/rego/convert/slice.go @@ -0,0 +1,32 @@ +package convert + +import ( + "reflect" +) + +func SliceToRego(inputValue reflect.Value) []any { + + // make sure we have a struct literal + for inputValue.Type().Kind() == reflect.Ptr { + if inputValue.IsNil() { + return nil + } + inputValue = inputValue.Elem() + } + if inputValue.Type().Kind() != reflect.Slice { + panic("not a slice") + } + + output := make([]any, inputValue.Len()) + + for i := 0; i < inputValue.Len(); i++ { + val := inputValue.Index(i) + if val.Type().Kind() == reflect.Ptr && val.IsZero() { + output[i] = nil + continue + } + output[i] = anonymousToRego(val) + } + + return output +} diff --git a/pkg/iac/rego/convert/slice_test.go b/pkg/iac/rego/convert/slice_test.go new file mode 100644 index 000000000000..7357692573a8 --- /dev/null +++ b/pkg/iac/rego/convert/slice_test.go @@ -0,0 +1,59 @@ +package convert + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_SliceConversion(t *testing.T) { + input := []struct { + X string + Y int + Z struct { + A float64 + } + }{ + {}, + } + input[0].Z.A = 123 + converted := SliceToRego(reflect.ValueOf(input)) + assert.Equal(t, []any{map[string]any{"z": make(map[string]any)}}, converted) +} + +func Test_SliceTypesConversion(t *testing.T) { + input := []types.StringValue{ + types.StringTest("test1"), + types.StringTest("test2"), + } + converted := SliceToRego(reflect.ValueOf(input)) + assert.Equal(t, []any{ + map[string]any{ + "value": "test1", + "filepath": "test.test", + "startline": 123, + "endline": 123, + "sourceprefix": "", + "managed": true, + "unresolvable": false, + "explicit": false, + "fskey": "", + "resource": "", + }, + map[string]any{ + "value": "test2", + "filepath": "test.test", + "startline": 123, + "endline": 123, + "sourceprefix": "", + "managed": true, + "unresolvable": false, + "explicit": false, + "fskey": "", + "resource": "", + }, + }, converted) +} diff --git a/pkg/iac/rego/convert/struct.go b/pkg/iac/rego/convert/struct.go new file mode 100644 index 000000000000..306f4d85a464 --- /dev/null +++ b/pkg/iac/rego/convert/struct.go @@ -0,0 +1,69 @@ +package convert + +import ( + "reflect" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type metadataProvider interface { + GetMetadata() types.Metadata +} + +var metadataInterface = reflect.TypeOf((*metadataProvider)(nil)).Elem() + +func StructToRego(inputValue reflect.Value) map[string]any { + + // make sure we have a struct literal + for inputValue.Type().Kind() == reflect.Ptr || inputValue.Type().Kind() == reflect.Interface { + if inputValue.IsNil() { + return nil + } + inputValue = inputValue.Elem() + } + if inputValue.Type().Kind() != reflect.Struct { + panic("not a struct") + } + + output := make(map[string]any, inputValue.NumField()) + + for i := 0; i < inputValue.NumField(); i++ { + field := inputValue.Field(i) + typ := inputValue.Type().Field(i) + name := typ.Name + + if !typ.IsExported() || field.Interface() == nil { + continue + } + + if _, ok := field.Interface().(types.Metadata); ok && name == "Metadata" { + continue + } + + val := anonymousToRego(reflect.ValueOf(field.Interface())) + + if val == nil { + continue + } + + output[strings.ToLower(name)] = val + } + + if inputValue.Type().Implements(metadataInterface) { + returns := inputValue.MethodByName("GetMetadata").Call(nil) + if metadata, ok := returns[0].Interface().(types.Metadata); ok { + output["__defsec_metadata"] = metadata.ToRego() + } + } else { + metaVal := inputValue.FieldByName("Metadata") + if metaVal.Kind() == reflect.Struct { + if meta, ok := metaVal.Interface().(types.Metadata); ok { + output["__defsec_metadata"] = meta.ToRego() + } + } + + } + + return output +} diff --git a/pkg/iac/rego/convert/struct_test.go b/pkg/iac/rego/convert/struct_test.go new file mode 100644 index 000000000000..0970b00c26e6 --- /dev/null +++ b/pkg/iac/rego/convert/struct_test.go @@ -0,0 +1,60 @@ +package convert + +import ( + "reflect" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_StructConversion(t *testing.T) { + tests := []struct { + name string + inp any + expected any + }{ + { + name: "struct with nested struct", + inp: struct { + X string + Y int + Z struct { + A float64 + } + }{ + X: "test", + Z: struct { + A float64 + }{ + A: 123, + }, + }, + expected: map[string]any{"z": make(map[string]any)}, + }, + { + name: "struct with metadata", + inp: struct { + X string + Metadata types.Metadata + }{ + X: "test", + Metadata: types.NewTestMetadata(), + }, + expected: map[string]any{ + "__defsec_metadata": func() any { + meta := types.NewTestMetadata().GetMetadata() + return meta.ToRego() + }(), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + converted := StructToRego(reflect.ValueOf(tt.inp)) + assert.Equal(t, tt.expected, converted) + }) + } +} diff --git a/pkg/iac/rego/custom.go b/pkg/iac/rego/custom.go new file mode 100644 index 000000000000..283cd3b07d52 --- /dev/null +++ b/pkg/iac/rego/custom.go @@ -0,0 +1,114 @@ +package rego + +import ( + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/rego" + "github.com/open-policy-agent/opa/v1/types" + + checksrego "github.com/aquasecurity/trivy-checks/pkg/rego" +) + +func init() { + + checksrego.RegisterBuiltins() + + rego.RegisterBuiltin2(®o.Function{ + Name: "result.new", + Decl: types.NewFunction(types.Args(types.S, types.A), types.A), + }, + createResult, + ) + + rego.RegisterBuiltin1(®o.Function{ + Name: "isManaged", + Decl: types.NewFunction(types.Args(types.A), types.B), + }, + func(c rego.BuiltinContext, resource *ast.Term) (*ast.Term, error) { + metadata, err := createResult(c, ast.StringTerm(""), resource) + if err != nil { + return nil, err + } + return metadata.Get(ast.StringTerm("managed")), nil + }, + ) +} + +func createResult(ctx rego.BuiltinContext, msg, cause *ast.Term) (*ast.Term, error) { + + metadata := map[string]*ast.Term{ + "startline": ast.IntNumberTerm(0), + "endline": ast.IntNumberTerm(0), + "sourceprefix": ast.StringTerm(""), + "filepath": ast.StringTerm(""), + "explicit": ast.BooleanTerm(false), + "managed": ast.BooleanTerm(true), + "fskey": ast.StringTerm(""), + "resource": ast.StringTerm(""), + "parent": ast.NullTerm(), + } + if msg != nil { + metadata["msg"] = msg + } + + // universal + input := cause.Get(ast.StringTerm("__defsec_metadata")) + if input == nil { + // docker + input = cause + } + metadata = updateMetadata(metadata, input) + + if term := input.Get(ast.StringTerm("parent")); term != nil { + var err error + metadata["parent"], err = createResult(ctx, nil, term) + if err != nil { + return nil, err + } + } + + var values [][2]*ast.Term + for key, val := range metadata { + values = append(values, [2]*ast.Term{ + ast.StringTerm(key), + val, + }) + } + return ast.ObjectTerm(values...), nil +} + +func updateMetadata(metadata map[string]*ast.Term, input *ast.Term) map[string]*ast.Term { + if term := input.Get(ast.StringTerm("startline")); term != nil { + metadata["startline"] = term + } + if term := input.Get(ast.StringTerm("StartLine")); term != nil { + metadata["startline"] = term + } + if term := input.Get(ast.StringTerm("endline")); term != nil { + metadata["endline"] = term + } + if term := input.Get(ast.StringTerm("EndLine")); term != nil { + metadata["endline"] = term + } + if term := input.Get(ast.StringTerm("filepath")); term != nil { + metadata["filepath"] = term + } + if term := input.Get(ast.StringTerm("sourceprefix")); term != nil { + metadata["sourceprefix"] = term + } + if term := input.Get(ast.StringTerm("Path")); term != nil { + metadata["filepath"] = term + } + if term := input.Get(ast.StringTerm("explicit")); term != nil { + metadata["explicit"] = term + } + if term := input.Get(ast.StringTerm("managed")); term != nil { + metadata["managed"] = term + } + if term := input.Get(ast.StringTerm("fskey")); term != nil { + metadata["fskey"] = term + } + if term := input.Get(ast.StringTerm("resource")); term != nil { + metadata["resource"] = term + } + return metadata +} diff --git a/pkg/iac/rego/embed.go b/pkg/iac/rego/embed.go new file mode 100644 index 000000000000..d702b9a1e835 --- /dev/null +++ b/pkg/iac/rego/embed.go @@ -0,0 +1,116 @@ +package rego + +import ( + "context" + "fmt" + "io/fs" + "maps" + "path/filepath" + "strings" + "sync" + + "github.com/open-policy-agent/opa/v1/ast" + + checks "github.com/aquasecurity/trivy-checks" + "github.com/aquasecurity/trivy/pkg/iac/rules" + "github.com/aquasecurity/trivy/pkg/log" +) + +var LoadAndRegister = sync.OnceFunc(func() { + modules, err := LoadEmbeddedPolicies() + if err != nil { + // we should panic as the policies were not embedded properly + panic(err) + } + loadedLibs, err := LoadEmbeddedLibraries() + if err != nil { + panic(err) + } + maps.Copy(modules, loadedLibs) + + RegisterRegoRules(modules) +}) + +func RegisterRegoRules(modules map[string]*ast.Module) { + ctx := context.TODO() + + schemaSet, _ := BuildSchemaSetFromPolicies(modules, nil, nil, make(map[string][]byte)) + + compiler := ast.NewCompiler(). + WithSchemas(schemaSet). + WithCapabilities(nil). + WithUseTypeCheckAnnotations(true) + + compiler.SetErrorLimit(DefaultAllowedRegoErrors) + compiler.Compile(modules) + if compiler.Failed() { + // we should panic as the embedded rego policies are syntactically incorrect... + panic(compiler.Errors) + } + + retriever := NewMetadataRetriever(compiler) + + for _, module := range modules { + metadata, err := retriever.RetrieveMetadata(ctx, module) + if err != nil { + log.Warn("Failed to retrieve metadata", log.String("package", module.Package.String()), log.Err(err)) + continue + } + + if metadata.ID == "" { + if !metadata.Library { + log.Warn("Check ID is empty", log.FilePath(module.Package.Location.File)) + } + continue + } + + rules.Register(metadata.ToRule()) + } +} + +func LoadEmbeddedPolicies() (map[string]*ast.Module, error) { + return LoadPoliciesFromDirs(checks.EmbeddedPolicyFileSystem, ".") +} + +func LoadEmbeddedLibraries() (map[string]*ast.Module, error) { + return LoadPoliciesFromDirs(checks.EmbeddedLibraryFileSystem, ".") +} + +func LoadPoliciesFromDirs(target fs.FS, paths ...string) (map[string]*ast.Module, error) { + modules := make(map[string]*ast.Module) + for _, path := range paths { + if err := fs.WalkDir(target, sanitisePath(path), func(path string, info fs.DirEntry, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + + if isOptionalChecks(path) { + return fs.SkipDir + } + + if !IsRegoFile(info.Name()) || IsDotFile(info.Name()) { + return nil + } + data, err := fs.ReadFile(target, filepath.ToSlash(path)) + if err != nil { + return err + } + module, err := ParseRegoModule(path, string(data)) + if err != nil { + return fmt.Errorf("failed to parse Rego module: %w", err) + } + modules[path] = module + return nil + }); err != nil { + return nil, err + } + } + return modules, nil +} + +func isOptionalChecks(path string) bool { + return strings.HasSuffix(filepath.Dir(filepath.ToSlash(path)), filepath.Join("advanced", "optional")) +} diff --git a/pkg/iac/rego/embed_test.go b/pkg/iac/rego/embed_test.go new file mode 100644 index 000000000000..c28c873ff55a --- /dev/null +++ b/pkg/iac/rego/embed_test.go @@ -0,0 +1,219 @@ +package rego_test + +import ( + "testing" + "testing/fstest" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + checks "github.com/aquasecurity/trivy-checks" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/rules" + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +func Test_EmbeddedLoading(t *testing.T) { + rego.LoadAndRegister() + + frameworkRules := rules.GetRegistered() + var found bool + for _, rule := range frameworkRules { + if rule.GetRule().RegoPackage != "" { + found = true + } + } + assert.True(t, found, "no embedded rego policies were registered as rules") +} + +func Test_RegisterRegoRules(t *testing.T) { + var testCases = []struct { + name string + inputPolicy string + expectedError bool + }{ + { + name: "happy path old single schema", + inputPolicy: `# METADATA +# title: "dummy title" +# description: "some description" +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# input: +# selector: +# - type: dockerfile +package builtin.dockerfile.DS1234 +deny[res]{ + res := true +}`, + }, + { + name: "happy path new builtin single schema", + inputPolicy: `# METADATA +# title: "dummy title" +# description: "some description" +# scope: package +# schemas: +# - input: schema["dockerfile"] +# custom: +# input: +# selector: +# - type: dockerfile +package builtin.dockerfile.DS1234 +deny[res]{ + res := true +}`, + }, + { + name: "happy path new multiple schemas", + inputPolicy: `# METADATA +# title: "dummy title" +# description: "some description" +# scope: package +# schemas: +# - input: schema["dockerfile"] +# - input: schema["kubernetes"] +# custom: +# input: +# selector: +# - type: dockerfile +package builtin.dockerfile.DS1234 +deny[res]{ + res := true +}`, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + policies, err := rego.LoadPoliciesFromDirs(checks.EmbeddedLibraryFileSystem, ".") + require.NoError(t, err) + newRule, err := rego.ParseRegoModule("/rules/newrule.rego", tc.inputPolicy) + require.NoError(t, err) + + policies["/rules/newrule.rego"] = newRule + switch { + case tc.expectedError: + assert.Panics(t, func() { + rego.RegisterRegoRules(policies) + }, tc.name) + default: + rego.RegisterRegoRules(policies) + } + }) + } +} + +func Test_RegisterDeprecatedRule(t *testing.T) { + var testCases = []struct { + name string + id string + inputPolicy string + expected scan.Rule + }{ + { + name: "deprecated check", + id: "DEP-0001", + inputPolicy: `# METADATA +# title: "deprecated check" +# description: "some description" +# scope: package +# schemas: +# - input: schema["dockerfile"] +# custom: +# id: DEP-0001 +# input: +# selector: +# - type: dockerfile +# deprecated: true +package builtin.dockerfile.DS1234 +deny[res]{ + res := true +}`, + expected: scan.Rule{ + Deprecated: true, + }, + }, + { + name: "not a deprecated check", + id: "NOTDEP-0001", + inputPolicy: `# METADATA +# title: "not a deprecated check" +# description: "some description" +# scope: package +# schemas: +# - input: schema["dockerfile"] +# custom: +# id: NOTDEP-0001 +# input: +# selector: +# - type: dockerfile +package builtin.dockerfile.DS1234 +deny[res]{ + res := true +}`, + expected: scan.Rule{ + Deprecated: false, + }, + }, + { + name: "invalid deprecation value", + id: "BADDEP-0001", + inputPolicy: `# METADATA +# title: "badly deprecated check" +# description: "some description" +# scope: package +# schemas: +# - input: schema["dockerfile"] +# custom: +# id: BADDEP-0001 +# input: +# selector: +# - type: dockerfile +# deprecated: "this is bad, deprecation is a bool value not a string" +package builtin.dockerfile.DS1234 +deny[res]{ + res := true +}`, + expected: scan.Rule{ + Deprecated: false, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + policies := make(map[string]*ast.Module) + newRule, err := rego.ParseRegoModule("/rules/newrule.rego", tc.inputPolicy) + require.NoError(t, err) + + policies["/rules/newrule.rego"] = newRule + assert.NotPanics(t, func() { + rego.RegisterRegoRules(policies) + }) + + for _, rule := range rules.GetRegistered() { + if rule.ID == tc.id { + assert.Equal(t, tc.expected.Deprecated, rule.GetRule().Deprecated, tc.name) + } + } + }) + } +} + +func TestLoadPoliciesFromDirs(t *testing.T) { + fsys := fstest.MapFS{ + "check.rego": &fstest.MapFile{Data: []byte(`package user.foo`)}, + ".check.rego": &fstest.MapFile{Data: []byte(`package user.foo`)}, + "check_test.rego": &fstest.MapFile{Data: []byte(`package user.foo_test`)}, + "test.yaml": &fstest.MapFile{Data: []byte(`foo: bar`)}, + "checks/test.rego": &fstest.MapFile{Data: []byte(`package user.checks.foo`)}, + } + + modules, err := rego.LoadPoliciesFromDirs(fsys, ".") + require.NoError(t, err) + assert.Len(t, modules, 2) +} diff --git a/pkg/iac/rego/filter.go b/pkg/iac/rego/filter.go new file mode 100644 index 000000000000..b7d6a8eda136 --- /dev/null +++ b/pkg/iac/rego/filter.go @@ -0,0 +1,85 @@ +package rego + +import ( + "github.com/open-policy-agent/opa/v1/ast" + + "github.com/aquasecurity/go-version/pkg/semver" + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/log" +) + +type RegoModuleFilter func(module *ast.Module, metadata *StaticMetadata) bool + +// TrivyVersionFilter returns a filter that allows only those modules, +// that are compatible with the given version of Trivy. +func TrivyVersionFilter(trivyVer string) RegoModuleFilter { + if trivyVer == "dev" { + return func(_ *ast.Module, _ *StaticMetadata) bool { + return true + } + } + + tv, tverr := semver.Parse(trivyVer) + if tverr != nil { + log.Warn( + "Failed to parse Trivy version - cannot confirm if all modules will work with current version", + log.Prefix("rego"), + log.String("trivy_version", trivyVer), + log.Err(tverr), + ) + return func(_ *ast.Module, _ *StaticMetadata) bool { + return true + } + } + return func(module *ast.Module, metadata *StaticMetadata) bool { + return isMinimumVersionSupported(metadata, module, tv) + } +} + +func isMinimumVersionSupported(metadata *StaticMetadata, module *ast.Module, tv semver.Version) bool { + // to ensure compatibility with old modules without minimum trivy version + if metadata.MinimumTrivyVersion == "" { + return true + } + + mmsv, err := semver.Parse(metadata.MinimumTrivyVersion) + if err != nil { + log.Warn( + "Failed to parse minimum trivy version - skipping as cannot confirm if module will work with current version", + log.Prefix("rego"), + log.FilePath(module.Package.Location.File), + log.Err(err), + ) + return false + } + + if tv.LessThan(mmsv) { + log.Warn( + "Module will be skipped as current version of Trivy is older than minimum trivy version required - please update Trivy to use this module", + log.Prefix("rego"), + log.FilePath(module.Package.Location.File), + log.String("minimum_trivy_version", metadata.MinimumTrivyVersion), + ) + return false + } + return true +} + +// FrameworksFilter returns a filter that allows only modules +// associated with the specified frameworks. +func FrameworksFilter(frameworks []framework.Framework) RegoModuleFilter { + return func(_ *ast.Module, metadata *StaticMetadata) bool { + return metadata.matchAnyFramework(frameworks) + } +} + +// IncludeDeprecatedFilter returns a filter that allows deprecated modules +// if the include flag is true. +func IncludeDeprecatedFilter(include bool) RegoModuleFilter { + return func(_ *ast.Module, metadata *StaticMetadata) bool { + if metadata.Deprecated && !include { + return false + } + return true + } +} diff --git a/pkg/iac/rego/filter_test.go b/pkg/iac/rego/filter_test.go new file mode 100644 index 000000000000..054cc7325611 --- /dev/null +++ b/pkg/iac/rego/filter_test.go @@ -0,0 +1,137 @@ +package rego_test + +import ( + "testing" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/rego" +) + +func TestTrivyVersionFilter(t *testing.T) { + module := &ast.Module{Package: &ast.Package{Location: ast.NewLocation(nil, "/test.rego", 1, 1)}} + + tests := []struct { + name string + trivyVer string + minVersion string + expected bool + }{ + {"no minimum version", "0.1.0", "", true}, + {"compatible version", "0.20.0", "0.19.0", true}, + {"incompatible version", "0.18.0", "0.19.0", false}, + {"invalid min version", "0.20.0", "invalid", false}, + {"invalid trivy version", "invalid", "0.19.0", true}, + {"empty trivy version", "", "0.19.0", true}, + {"dev trivy version", "dev", "0.19.0", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + filter := rego.TrivyVersionFilter(tt.trivyVer) + metadata := ®o.StaticMetadata{ + MinimumTrivyVersion: tt.minVersion, + } + result := filter(module, metadata) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestFrameworksFilter(t *testing.T) { + module := &ast.Module{} + + tests := []struct { + name string + moduleFw map[framework.Framework][]string + filterFw []framework.Framework + expected bool + }{ + { + name: "match single", + moduleFw: map[framework.Framework][]string{ + framework.CIS_AWS_1_2: {"2.5"}, + }, + filterFw: []framework.Framework{framework.CIS_AWS_1_2}, + expected: true, + }, + { + name: "match one of many", + moduleFw: map[framework.Framework][]string{ + framework.CIS_AWS_1_2: {"2.5"}, + framework.CIS_AWS_1_4: {"4.5"}, + }, + filterFw: []framework.Framework{framework.CIS_AWS_1_2}, + expected: true, + }, + { + name: "no match", + moduleFw: map[framework.Framework][]string{ + framework.CIS_AWS_1_2: {"2.5"}, + }, + filterFw: []framework.Framework{"PCI"}, + expected: false, + }, + { + name: "empty filter", + moduleFw: map[framework.Framework][]string{ + framework.CIS_AWS_1_2: {"2.5"}, + }, + filterFw: nil, + expected: false, + }, + { + name: "has default framework and empty filter", + moduleFw: map[framework.Framework][]string{ + framework.Default: {}, + framework.CIS_AWS_1_2: {"2.5"}, + }, + filterFw: nil, + expected: true, + }, + { + name: "empty module frameworks", + moduleFw: nil, + filterFw: []framework.Framework{framework.Default}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + metadata := ®o.StaticMetadata{ + Frameworks: tt.moduleFw, + } + filter := rego.FrameworksFilter(tt.filterFw) + result := filter(module, metadata) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIncludeDeprecatedFilter(t *testing.T) { + module := &ast.Module{} + + tests := []struct { + name string + include bool + deprecated bool + expected bool + }{ + {"include false, not deprecated", false, false, true}, + {"include false, deprecated", false, true, false}, + {"include true, deprecated", true, true, true}, + {"include true, not deprecated", true, false, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + metadata := ®o.StaticMetadata{Deprecated: tt.deprecated} + filter := rego.IncludeDeprecatedFilter(tt.include) + result := filter(module, metadata) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/iac/rego/load.go b/pkg/iac/rego/load.go new file mode 100644 index 000000000000..ae0b712e063c --- /dev/null +++ b/pkg/iac/rego/load.go @@ -0,0 +1,364 @@ +package rego + +import ( + "context" + "fmt" + "io" + "io/fs" + "maps" + "slices" + "strings" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/bundle" + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/version/doc" +) + +var builtinNamespaces = set.New("builtin", "defsec", "appshield") + +func BuiltinNamespaces() []string { + return builtinNamespaces.Items() +} + +func IsBuiltinNamespace(namespace string) bool { + return lo.ContainsBy(BuiltinNamespaces(), func(ns string) bool { + return strings.HasPrefix(namespace, ns+".") + }) +} + +func getModuleNamespace(module *ast.Module) string { + return strings.TrimPrefix(module.Package.Path.String(), "data.") +} + +func IsRegoFile(name string) bool { + return strings.HasSuffix(name, bundle.RegoExt) && !strings.HasSuffix(name, "_test"+bundle.RegoExt) +} + +func IsDotFile(name string) bool { + return strings.HasPrefix(name, ".") +} + +func (s *Scanner) loadPoliciesFromReaders(readers []io.Reader) (map[string]*ast.Module, error) { + modules := make(map[string]*ast.Module) + for i, r := range readers { + moduleName := fmt.Sprintf("reader_%d", i) + data, err := io.ReadAll(r) + if err != nil { + return nil, err + } + module, err := ParseRegoModule(moduleName, string(data)) + if err != nil { + return nil, err + } + modules[moduleName] = module + } + return modules, nil +} + +func (s *Scanner) loadEmbedded() error { + loaded, err := LoadEmbeddedLibraries() + if err != nil { + return fmt.Errorf("failed to load embedded rego libraries: %w", err) + } + s.embeddedLibs = loaded + s.logger.Debug("Embedded libraries are loaded", log.Int("count", len(loaded))) + + loaded, err = LoadEmbeddedPolicies() + if err != nil { + return fmt.Errorf("failed to load embedded rego checks: %w", err) + } + s.embeddedChecks = loaded + s.logger.Debug("Embedded checks are loaded", log.Int("count", len(loaded))) + + return nil +} + +func (s *Scanner) LoadPolicies(srcFS fs.FS) error { + + if s.policies == nil { + s.policies = make(map[string]*ast.Module) + } + + if s.policyFS != nil { + s.logger.Debug("Overriding filesystem for checks") + srcFS = s.policyFS + } + + if err := s.loadEmbedded(); err != nil { + return err + } + + if s.includeEmbeddedPolicies { + s.policies = lo.Assign(s.policies, s.embeddedChecks) + } + + if s.includeEmbeddedLibraries { + s.policies = lo.Assign(s.policies, s.embeddedLibs) + } + + var err error + if len(s.policyDirs) > 0 { + loaded, err := LoadPoliciesFromDirs(srcFS, s.policyDirs...) + if err != nil { + return fmt.Errorf("failed to load rego checks from %s: %w", s.policyDirs, err) + } + maps.Copy(s.policies, loaded) + s.logger.Debug("Checks from disk are loaded", log.Int("count", len(loaded))) + } + + if len(s.policyReaders) > 0 { + loaded, err := s.loadPoliciesFromReaders(s.policyReaders) + if err != nil { + return fmt.Errorf("failed to load rego checks from reader(s): %w", err) + } + maps.Copy(s.policies, loaded) + s.logger.Debug("Checks from readers are loaded", log.Int("count", len(loaded))) + } + + // gather namespaces + uniq := set.New[string]() + for _, module := range s.policies { + namespace := getModuleNamespace(module) + uniq.Append(namespace) + } + namespaces := uniq.Items() + + dataFS := srcFS + if s.dataFS != nil { + s.logger.Debug("Overriding filesystem for data") + dataFS = s.dataFS + } + store, err := initStore(dataFS, s.dataDirs, namespaces) + if err != nil { + return fmt.Errorf("unable to load data: %w", err) + } + s.store = store + + return s.compilePolicies(srcFS, s.policyDirs) +} + +func (s *Scanner) fallbackChecks(compiler *ast.Compiler) { + + var excludedFiles []string + + for _, e := range compiler.Errors { + if e.Location == nil { + continue + } + + loc := e.Location.File + + if lo.Contains(excludedFiles, loc) { + continue + } + + badPolicy, exists := s.policies[loc] + if !exists || badPolicy == nil { + continue + } + + if !IsBuiltinNamespace(getModuleNamespace(badPolicy)) { + continue + } + + s.logger.Debug( + "Unable to parse check. This can be due to lack of support in this version. Trying to fallback to embedded check", + log.FilePath(loc), + log.Err(e), + ) + + embedded := s.findMatchedEmbeddedCheck(badPolicy) + if embedded == nil { + s.logger.Debug("Failed to find embedded check, skipping", log.FilePath(loc)) + continue + } + + s.logger.Debug("Found embedded check", log.FilePath(embedded.Package.Location.File)) + delete(s.policies, loc) // remove bad check + s.policies[embedded.Package.Location.File] = embedded + delete(s.embeddedChecks, embedded.Package.Location.File) // avoid infinite loop if embedded check contains ref error + excludedFiles = append(excludedFiles, e.Location.File) + } + + compiler.Errors = lo.Filter(compiler.Errors, func(e *ast.Error, _ int) bool { + return e.Location == nil || !lo.Contains(excludedFiles, e.Location.File) + }) +} + +func (s *Scanner) findMatchedEmbeddedCheck(badPolicy *ast.Module) *ast.Module { + for _, embeddedCheck := range s.embeddedChecks { + if embeddedCheck.Package.Path.String() == badPolicy.Package.Path.String() { + return embeddedCheck + } + } + + badPolicyMeta, err := MetadataFromAnnotations(badPolicy) + if err != nil || badPolicyMeta == nil { + return nil + } + + for _, embeddedCheck := range s.embeddedChecks { + meta, err := MetadataFromAnnotations(embeddedCheck) + if err != nil || meta == nil { + continue + } + if (badPolicyMeta.AVDID != "" && badPolicyMeta.AVDID == meta.AVDID) || + (badPolicyMeta.ID != "" && badPolicyMeta.ID != "N/A" && badPolicyMeta.ID == meta.ID) { + return embeddedCheck + } + } + return nil +} + +func (s *Scanner) prunePoliciesWithError(compiler *ast.Compiler) error { + // Filter out the compiler-internal "error limit reached" marker; it's not a real error. + errs := lo.Filter(compiler.Errors, func(e *ast.Error, _ int) bool { + return e.Message != "error limit reached" + }) + + // Check against the scanner's maximum allowed errors. + if len(errs) > s.maxAllowedErrors { + s.logger.Error("Error(s) occurred while loading checks") + return errs + } + + for _, e := range errs { + if e.Location == nil { + continue + } + s.logger.Error( + "Error occurred while parsing", + log.FilePath(e.Location.File), log.Err(e), + ) + delete(s.policies, e.Location.File) + } + return nil +} + +func (s *Scanner) compilePolicies(srcFS fs.FS, paths []string) error { + for path, module := range s.policies { + s.handleModulesMetadata(path, module) + } + + schemaSet, err := BuildSchemaSetFromPolicies(s.policies, paths, srcFS, s.customSchemas) + if err != nil { + return err + } + + compiler := ast.NewCompiler(). + WithUseTypeCheckAnnotations(true). + WithCapabilities(ast.CapabilitiesForThisVersion()). + WithSchemas(schemaSet) + + compiler.SetErrorLimit(s.maxAllowedErrors + 1) + compiler.Compile(s.policies) + if compiler.Failed() { + s.fallbackChecks(compiler) + if err := s.prunePoliciesWithError(compiler); err != nil { + return err + } + return s.compilePolicies(srcFS, paths) + } + + s.retriever = NewMetadataRetriever(compiler) + + if err := s.filterModules(); err != nil { + return fmt.Errorf("filter modules: %w", err) + } + s.compiler = compiler + return nil +} + +func (s *Scanner) handleModulesMetadata(path string, module *ast.Module) { + if moduleHasLegacyInputFormat(module) { + s.logger.Warn( + "Module has legacy input format - please update to use annotations", + log.FilePath(module.Package.Location.File), + log.String("details", doc.URL("guide/scanner/misconfiguration/custom", "input")), + ) + } + + if moduleHasLegacyMetadataFormat(module) { + s.logger.Warn( + "Module has legacy metadata format - please update to use annotations", + log.FilePath(module.Package.Location.File), + log.String("details", doc.URL("guide/scanner/misconfiguration/custom", "metadata")), + ) + return + } + + metadata, err := MetadataFromAnnotations(module) + if err != nil { + s.logger.Error( + "Failed to retrieve metadata from annotations", + log.FilePath(module.Package.Location.File), + log.Err(err), + ) + return + } + + s.moduleMetadata[path] = metadata +} + +// moduleHasLegacyMetadataFormat checks if the module has a legacy metadata format. +// Returns true if the metadata is represented as a “__rego_metadata__” rule, +// which was used before annotations were introduced. +func moduleHasLegacyMetadataFormat(module *ast.Module) bool { + return slices.ContainsFunc(module.Rules, func(rule *ast.Rule) bool { + return rule.Head.Name.Equal(ast.Var("__rego_metadata__")) + }) +} + +// moduleHasLegacyInputFormat checks if the module has a legacy input format. +// Returns true if the input is represented as a “__rego_input__” rule, +// which was used before annotations were introduced. +func moduleHasLegacyInputFormat(module *ast.Module) bool { + return slices.ContainsFunc(module.Rules, func(rule *ast.Rule) bool { + return rule.Head.Name.Equal(ast.Var("__rego_input__")) + }) +} + +// filterModules filters the Rego modules based on metadata. +func (s *Scanner) filterModules() error { + filtered := make(map[string]*ast.Module) + + for name, module := range s.policies { + metadata, err := s.metadataForModule(context.Background(), name, module, nil) + if err != nil { + return fmt.Errorf("retrieve metadata for module %s: %w", name, err) + } + if metadata == nil { + continue + } + + if !lo.EveryBy(s.moduleFilters, func(filter RegoModuleFilter) bool { + return filter(module, metadata) + }) { + continue + } + + if len(metadata.InputOptions.Selectors) == 0 && !metadata.Library { + s.logger.Warn( + "Module has no input selectors - it will be loaded for all inputs", + log.FilePath(module.Package.Location.File), + log.String("module", name), + ) + } + + filtered[name] = module + } + + s.policies = filtered + return nil +} + +func ParseRegoModule(name, input string) (*ast.Module, error) { + return ast.ParseModuleWithOpts(name, input, ast.ParserOptions{ + ProcessAnnotation: true, + RegoVersion: ast.RegoV0, + }) +} diff --git a/pkg/iac/rego/load_test.go b/pkg/iac/rego/load_test.go new file mode 100644 index 000000000000..89ad3c6b786c --- /dev/null +++ b/pkg/iac/rego/load_test.go @@ -0,0 +1,389 @@ +package rego_test + +import ( + "bytes" + "embed" + "fmt" + "log/slog" + "strings" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + checks "github.com/aquasecurity/trivy-checks" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +//go:embed all:testdata/policies +var testEmbedFS embed.FS + +//go:embed testdata/embedded +var embeddedChecksFS embed.FS + +func Test_RegoScanning_WithSomeInvalidPolicies(t *testing.T) { + t.Run("allow no errors", func(t *testing.T) { + var debugBuf bytes.Buffer + slog.SetDefault(log.New(log.NewHandler(&debugBuf, nil))) + scanner := rego.NewScanner( + rego.WithMaxAllowedErrors(0), + rego.WithPolicyDirs("."), + ) + + err := scanner.LoadPolicies(testEmbedFS) + require.ErrorContains(t, err, `want (one of): ["Cmd" "EndLine" "Flags" "JSON" "Original" "Path" "Stage" "StartLine" "SubCmd" "Value"]`) + assert.Contains(t, debugBuf.String(), "Error(s) occurred while loading checks") + }) + + t.Run("allow up to max 1 error", func(t *testing.T) { + var debugBuf bytes.Buffer + slog.SetDefault(log.New(log.NewHandler(&debugBuf, nil))) + scanner := rego.NewScanner( + rego.WithMaxAllowedErrors(1), + rego.WithPolicyDirs("."), + ) + + err := scanner.LoadPolicies(testEmbedFS) + require.NoError(t, err) + + assert.Contains(t, debugBuf.String(), "Error occurred while parsing\tfile_path=\"testdata/policies/invalid.rego\" err=\"testdata/policies/invalid.rego:7") + }) + + t.Run("schema does not exist", func(t *testing.T) { + check := `# METADATA +# schemas: +# - input: schema["fooschema"] +package mypackage + +deny { + input.evil == "foo bar" +}` + scanner := rego.NewScanner( + rego.WithPolicyDirs("."), + rego.WithPolicyReader(strings.NewReader(check)), + ) + + err := scanner.LoadPolicies(fstest.MapFS{}) + assert.ErrorContains(t, err, "could not find schema \"fooschema\"") + }) + + t.Run("schema is invalid", func(t *testing.T) { + check := `# METADATA +# schemas: +# - input: schema["fooschema"] +package mypackage + +deny { + input.evil == "foo bar" +}` + scanner := rego.NewScanner( + rego.WithPolicyDirs("."), + rego.WithPolicyReader(strings.NewReader(check)), + ) + + fsys := fstest.MapFS{ + "schemas/fooschema.json": &fstest.MapFile{ + Data: []byte("bad json"), + }, + } + + err := scanner.LoadPolicies(fsys) + assert.ErrorContains(t, err, "could not parse schema \"fooschema\"") + }) + + t.Run("schema is not specified", func(t *testing.T) { + check := `package mypackage + +deny { + input.evil == "foo bar" +}` + scanner := rego.NewScanner( + rego.WithPolicyDirs("."), + rego.WithPolicyReader(strings.NewReader(check)), + ) + err := scanner.LoadPolicies(fstest.MapFS{}) + require.NoError(t, err) + }) +} + +func Test_FallbackToEmbedded(t *testing.T) { + tests := []struct { + name string + files map[string]*fstest.MapFile + expectedErr string + }{ + { + name: "match by namespace", + files: map[string]*fstest.MapFile{ + "policies/my-check2.rego": { + Data: []byte(`# METADATA +# schemas: +# - input: schema["fooschema"] + +package builtin.test + +deny { + input.evil == "foo bar" +}`, + ), + }, + }, + }, + { + name: "match by check ID", + files: map[string]*fstest.MapFile{ + "policies/my-check2.rego": { + Data: []byte(`# METADATA +# schemas: +# - input: schema["fooschema"] +# custom: +# id: test-001 +package builtin.test2 + +deny { + input.evil == "foo bar" +}`, + ), + }, + }, + }, + { + name: "match by check AVDID", + files: map[string]*fstest.MapFile{ + "policies/my-check2.rego": { + Data: []byte(`# METADATA +# schemas: +# - input: schema["fooschema"] +# custom: +# avd_id: avd-test-001 +package builtin.test2 + +deny { + input.evil == "foo bar" +}`, + ), + }, + }, + }, + { + name: "bad embedded check", + files: map[string]*fstest.MapFile{ + "policies/my-check2.rego": { + Data: []byte(`# METADATA +# schemas: +# - input: schema["fooschema"] +package builtin.bad.test + +deny { + input.evil == "foo bar" +}`, + ), + }, + }, + expectedErr: "testdata/embedded/bad-check.rego:8: rego_type_error: undefined ref", + }, + { + name: "with non existent function", + files: map[string]*fstest.MapFile{ + "policies/my-check2.rego": { + Data: []byte(`# METADATA +# schemas: +# - input: schema["fooschema"] +package builtin.test + +deny { + input.foo == fn.is_foo("foo") +}`, + ), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scanner := rego.NewScanner( + rego.WithMaxAllowedErrors(0), + rego.WithEmbeddedPolicies(false), + rego.WithPolicyDirs("."), + ) + + tt.files["schemas/fooschema.json"] = &fstest.MapFile{ + Data: []byte(`{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + }`), + } + + originalFS := checks.EmbeddedPolicyFileSystem + checks.EmbeddedPolicyFileSystem = embeddedChecksFS + t.Cleanup(func() { + checks.EmbeddedPolicyFileSystem = originalFS + }) + err := scanner.LoadPolicies(fstest.MapFS(tt.files)) + + if tt.expectedErr != "" { + assert.ErrorContains(t, err, tt.expectedErr) + } else { + require.NoError(t, err) + } + }) + } +} + +func Test_FallbackErrorWithoutLocation(t *testing.T) { + fsys := fstest.MapFS{ + "schemas/fooschema.json": { + Data: []byte(`{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } + }`), + }, + } + + for i := range rego.DefaultAllowedRegoErrors + 1 { + src := `# METADATA +# schemas: +# - input: schema["fooschema"] +package builtin.test%d + +deny { + input.evil == "foo bar" +}` + fsys[fmt.Sprintf("policies/my-check%d.rego", i)] = &fstest.MapFile{ + Data: fmt.Appendf(nil, src, i), + } + } + + scanner := rego.NewScanner( + rego.WithEmbeddedPolicies(false), + rego.WithPolicyDirs("."), + ) + err := scanner.LoadPolicies(fsys) + require.Error(t, err) +} + +func TestFallback_CheckWithoutAnnotation(t *testing.T) { + fsys := fstest.MapFS{ + "check.rego": &fstest.MapFile{Data: []byte(`package builtin.test +import data.some_func +deny := some_func(input) +`)}, + } + scanner := rego.NewScanner( + rego.WithPolicyDirs("."), + rego.WithEmbeddedLibraries(false), + rego.WithPolicyFilesystem(fsys), + ) + err := scanner.LoadPolicies(nil) + require.NoError(t, err) +} + +func TestIsMinimumTrivyVersion(t *testing.T) { + testCases := []struct { + name string + trivyVersion string + MinimumTrivyVersion string + expectedResults int + expectedErr string + }{ + { + name: "trivy version is newer than the check version", + trivyVersion: "1.2.3", + MinimumTrivyVersion: "1.2.0", + expectedResults: 1, + }, + { + name: "trivy version is older than the check version", + trivyVersion: "1.2.0", + MinimumTrivyVersion: "1.2.3", + expectedResults: 0, + }, + { + name: "trivy version is equal to the check version", + trivyVersion: "1.2.3", + MinimumTrivyVersion: "1.2.3", + expectedResults: 1, + }, + { + name: "check version is not a valid semver", + trivyVersion: "1.2.3", + MinimumTrivyVersion: "invalid", + expectedResults: 0, + }, + { + name: "trivy version is not a valid semver", // if we cannot parse the version, we fail open to allow the check to run + trivyVersion: "invalid", + MinimumTrivyVersion: "1.2.3", + expectedResults: 1, + }, + { + name: "check version is not set", + trivyVersion: "1.2.3", + MinimumTrivyVersion: "", + expectedResults: 1, + }, + { + name: "trivy version is dev", + trivyVersion: "dev", + MinimumTrivyVersion: "1.2.3", + expectedResults: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + fsys := fstest.MapFS{ + "check.rego": &fstest.MapFile{Data: fmt.Appendf(nil, `# METADATA +# title: "dummy title" +# description: "some description" +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# minimum_trivy_version: "%s" +package builtin.foo.ABC123 +deny { + input.evil +}`, tc.MinimumTrivyVersion)}, + } + scanner := rego.NewScanner( + rego.WithPolicyDirs("."), + rego.WithEmbeddedLibraries(false), + rego.WithEmbeddedPolicies(false), + rego.WithPolicyFilesystem(fsys), + rego.WithTrivyVersion(tc.trivyVersion), + ) + err := scanner.LoadPolicies(nil) + require.NoError(t, err, tc.name) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/check.rego", + Contents: map[string]any{ + "evil": true, + }, + FS: fsys, + }) + + if tc.expectedErr != "" { + assert.ErrorContains(t, err, tc.expectedErr, tc.name) + } else { + require.NoError(t, err) + require.Len(t, results, tc.expectedResults, tc.name) + } + }) + } +} diff --git a/pkg/iac/rego/metadata.go b/pkg/iac/rego/metadata.go new file mode 100644 index 000000000000..049771bca1ef --- /dev/null +++ b/pkg/iac/rego/metadata.go @@ -0,0 +1,478 @@ +package rego + +import ( + "cmp" + "context" + "errors" + "fmt" + "slices" + "strings" + + "github.com/go-viper/mapstructure/v2" + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/rego" + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/providers" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/severity" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +const annotationScopePackage = "package" + +type StaticMetadata struct { + Deprecated bool + ID string + // Deprecated: Use the ID field instead. + AVDID string + Title string + LongID string + ShortCode string + Aliases []string + Description string + Severity string + RecommendedActions string + PrimaryURL string + References []string + InputOptions InputOptions + Package string + Frameworks map[framework.Framework][]string + Provider string + Service string + Library bool + CloudFormation *scan.EngineMetadata + Terraform *scan.EngineMetadata + Examples string + MinimumTrivyVersion string +} + +func NewStaticMetadata(pkgPath string, inputOpt InputOptions) *StaticMetadata { + return &StaticMetadata{ + ID: "N/A", + Title: "N/A", + Severity: "UNKNOWN", + Description: fmt.Sprintf("Rego module: %s", pkgPath), + Package: pkgPath, + InputOptions: inputOpt, + Frameworks: map[framework.Framework][]string{ + framework.Default: {}, + }, + } +} + +func (sm *StaticMetadata) populate(meta map[string]any) error { + if sm.Frameworks == nil { + sm.Frameworks = make(map[framework.Framework][]string) + } + + upd := func(field *string, key string) { + if raw, ok := meta[key]; ok { + *field = fmt.Sprintf("%s", raw) + } + } + + upd(&sm.ID, "id") + upd(&sm.AVDID, "avd_id") + upd(&sm.Title, "title") + upd(&sm.LongID, "long_id") + upd(&sm.ShortCode, "short_code") + upd(&sm.Description, "description") + upd(&sm.Service, "service") + upd(&sm.Provider, "provider") + upd(&sm.RecommendedActions, "recommended_actions") + upd(&sm.RecommendedActions, "recommended_action") + upd(&sm.Examples, "examples") + upd(&sm.MinimumTrivyVersion, "minimum_trivy_version") + + if raw, ok := meta["deprecated"]; ok { + if dep, ok := raw.(bool); ok { + sm.Deprecated = dep + } + } + + if raw, ok := meta["minimum_trivy_version"]; ok { + sm.MinimumTrivyVersion = raw.(string) + } + + if raw, ok := meta["severity"]; ok { + sm.Severity = strings.ToUpper(fmt.Sprintf("%s", raw)) + } + + if raw, ok := meta["library"]; ok { + if lib, ok := raw.(bool); ok { + sm.Library = lib + } + } + + if raw, ok := meta["url"]; ok { + sm.References = append(sm.References, fmt.Sprintf("%s", raw)) + } + + if raw, ok := meta["related_resources"]; ok { + switch relatedResources := raw.(type) { + case []map[string]any: + for _, relatedResource := range relatedResources { + if raw, ok := relatedResource["ref"]; ok { + sm.References = append(sm.References, fmt.Sprintf("%s", raw)) + } + } + case []string: + sm.References = append(sm.References, relatedResources...) + } + } + + if err := sm.updateFrameworks(meta); err != nil { + return fmt.Errorf("failed to update frameworks: %w", err) + } + sm.updateAliases(meta) + + var err error + if sm.CloudFormation, err = NewEngineMetadata("cloud_formation", meta); err != nil { + return err + } + + if sm.Terraform, err = NewEngineMetadata("terraform", meta); err != nil { + return err + } + + return nil +} + +func (sm *StaticMetadata) updateFrameworks(meta map[string]any) error { + raw, ok := meta["frameworks"] + if !ok { + return nil + } + + frameworks, ok := raw.(map[string]any) + if !ok { + return fmt.Errorf("frameworks metadata is not an object, got %T", raw) + } + + if len(frameworks) > 0 { + sm.Frameworks = make(map[framework.Framework][]string) + } + + for fw, rawIDs := range frameworks { + ids, ok := rawIDs.([]any) + if !ok { + return fmt.Errorf("framework ids is not an array, got %T", rawIDs) + } + fr := framework.Framework(fw) + for _, id := range ids { + if str, ok := id.(string); ok { + sm.Frameworks[fr] = append(sm.Frameworks[fr], str) + } else { + sm.Frameworks[fr] = []string{} + } + } + } + return nil +} + +func (sm *StaticMetadata) updateAliases(meta map[string]any) { + if raw, ok := meta["aliases"]; ok { + if aliases, ok := raw.([]any); ok { + for _, a := range aliases { + sm.Aliases = append(sm.Aliases, fmt.Sprintf("%s", a)) + } + } + } +} + +func (sm *StaticMetadata) matchAnyFramework(frameworks []framework.Framework) bool { + if len(frameworks) == 0 { + frameworks = []framework.Framework{framework.Default} + } + + return slices.ContainsFunc(frameworks, func(fw framework.Framework) bool { + _, exists := sm.Frameworks[fw] + return exists + }) +} + +func (sm *StaticMetadata) FromAnnotations(annotations *ast.Annotations) error { + sm.Title = annotations.Title + sm.Description = annotations.Description + for _, resource := range annotations.RelatedResources { + if !resource.Ref.IsAbs() { + continue + } + sm.References = append(sm.References, resource.Ref.String()) + } + + if annotations.Custom == nil { + annotations.Custom = make(map[string]any) + } + + if err := sm.populate(annotations.Custom); err != nil { + return err + } + + if len(annotations.RelatedResources) > 0 { + sm.PrimaryURL = annotations.RelatedResources[0].Ref.String() + } + return nil +} + +func NewEngineMetadata(schema string, meta map[string]any) (*scan.EngineMetadata, error) { + var sMap map[string]any + if raw, ok := meta[schema]; ok { + sMap, ok = raw.(map[string]any) + if !ok { + return nil, fmt.Errorf("failed to parse %s metadata: not an object", schema) + } + } + + var em scan.EngineMetadata + if val, ok := sMap["good_examples"].(string); ok { + em.GoodExamples = []string{val} + } + if val, ok := sMap["bad_examples"].(string); ok { + em.BadExamples = []string{val} + } + switch links := sMap["links"].(type) { + case string: + em.Links = []string{links} + case []any: + for _, v := range links { + if str, ok := v.(string); ok { + em.Links = append(em.Links, str) + } + } + } + if val, ok := sMap["remediation_markdown"].(string); ok { + em.RemediationMarkdown = val + } + + return &em, nil +} + +type InputOptions struct { + Selectors []Selector +} + +type Selector struct { + Type string + Subtypes []SubType +} + +type SubType struct { + Group string + Version string + Kind string + Namespace string + Service string // only for cloud + Provider string // only for cloud +} + +func (sm *StaticMetadata) ToRule() scan.Rule { + + provider := "generic" + if sm.Provider != "" { + provider = sm.Provider + } else if len(sm.InputOptions.Selectors) > 0 { + provider = sm.InputOptions.Selectors[0].Type + } + + return scan.Rule{ + Deprecated: sm.Deprecated, + ID: sm.ID, + AVDID: sm.AVDID, + Aliases: sm.Aliases, + LongID: sm.LongID, + ShortCode: sm.ShortCode, + Summary: sm.Title, + Explanation: sm.Description, + Impact: "", + Resolution: sm.RecommendedActions, + Provider: providers.Provider(provider), + Service: cmp.Or(sm.Service, "general"), + Links: sm.References, + Severity: severity.Severity(sm.Severity), + RegoPackage: sm.Package, + Frameworks: sm.Frameworks, + CloudFormation: sm.CloudFormation, + Terraform: sm.Terraform, + Examples: sm.Examples, + MinimumTrivyVersion: sm.MinimumTrivyVersion, + } +} + +func MetadataFromAnnotations(module *ast.Module) (*StaticMetadata, error) { + if annotations := findPackageAnnotations(module); annotations != nil { + input, err := inputFromAnnotations(annotations) + if err != nil { + return nil, fmt.Errorf("retrieve input from annotations: %w", err) + } + metadata := NewStaticMetadata(module.Package.Path.String(), input) + if err := metadata.FromAnnotations(annotations); err != nil { + return nil, err + } + return metadata, nil + } + return nil, nil +} + +func inputFromAnnotations(annotations *ast.Annotations) (InputOptions, error) { + if annotations == nil || annotations.Custom == nil { + return InputOptions{}, nil + } + input, ok := annotations.Custom["input"] + if !ok { + return InputOptions{}, nil + } + + rawInput, ok := input.(map[string]any) + if !ok { + return InputOptions{}, fmt.Errorf("input is not an object, got %T", input) + } + return parseMetadataInput(rawInput) +} + +type MetadataRetriever struct { + compiler *ast.Compiler +} + +func NewMetadataRetriever(compiler *ast.Compiler) *MetadataRetriever { + return &MetadataRetriever{ + compiler: compiler, + } +} + +func (m *MetadataRetriever) RetrieveMetadata(ctx context.Context, module *ast.Module, contents ...any) (*StaticMetadata, error) { + // read metadata from official rego annotations if possible + if metadata, err := MetadataFromAnnotations(module); err != nil { + return nil, fmt.Errorf("retrieve metadata from annotations: %w", err) + } else if metadata != nil { + return metadata, nil + } + + input, err := m.inputFromRule(ctx, module) + if err != nil { + return nil, fmt.Errorf("retrieve input from rule: %w", err) + } + metadata := NewStaticMetadata(module.Package.Path.String(), input) + + // otherwise, try to read metadata from the rego module itself + // - we used to do this before annotations were a thing + options := []func(*rego.Rego){ + rego.Query(ruleQuery(module, "__rego_metadata__")), + rego.Compiler(m.compiler), + rego.Capabilities(nil), + } + // support dynamic metadata fields + for _, in := range contents { + options = append(options, rego.Input(in)) + } + + set, err := rego.New(options...).Eval(ctx) + if err != nil { + return nil, err + } + + // no metadata supplied + if set == nil { + return metadata, nil + } + + if len(set) != 1 { + return nil, errors.New("failed to parse metadata: unexpected set length") + } + if len(set[0].Expressions) != 1 { + return nil, errors.New("failed to parse metadata: unexpected expression length") + } + expression := set[0].Expressions[0] + meta, ok := expression.Value.(map[string]any) + if !ok { + return nil, errors.New("failed to parse metadata: not an object") + } + + if err := metadata.populate(meta); err != nil { + return nil, err + } + + return metadata, nil +} + +func (m *MetadataRetriever) inputFromRule(ctx context.Context, module *ast.Module) (InputOptions, error) { + instance := rego.New( + rego.Query(ruleQuery(module, "__rego_input__")), + rego.Compiler(m.compiler), + rego.Capabilities(nil), + ) + resultSet, err := instance.Eval(ctx) + if err != nil { + return InputOptions{}, fmt.Errorf("evaluate input: %w", err) + } + + if len(resultSet) != 1 || len(resultSet[0].Expressions) != 1 { + return InputOptions{}, nil + } + + expression := resultSet[0].Expressions[0] + metadata, ok := expression.Value.(map[string]any) + if !ok { + return InputOptions{}, fmt.Errorf("result is not an object, got %T", expression.Value) + } + return parseMetadataInput(metadata) +} + +func parseMetadataInput(input map[string]any) (InputOptions, error) { + raw, ok := input["selector"] + if !ok { + return InputOptions{}, nil + } + + each, ok := raw.([]any) + if !ok { + return InputOptions{}, fmt.Errorf("selector is not an array, got %T", raw) + } + + var selectors []Selector + for _, rawSelector := range each { + if selectorMap, ok := rawSelector.(map[string]any); ok { + selector, err := parseSelectorItem(selectorMap) + if err != nil { + return InputOptions{}, fmt.Errorf("parse selector item %v: %w", selectorMap, err) + } + selectors = append(selectors, selector) + } + } + return InputOptions{Selectors: selectors}, nil +} + +func parseSelectorItem(raw map[string]any) (Selector, error) { + var selector Selector + if rawType, ok := raw["type"]; ok { + selector.Type = fmt.Sprintf("%s", rawType) + // handle backward compatibility for "defsec" source type which is now "cloud" + if selector.Type == string(iacTypes.SourceDefsec) { + selector.Type = string(iacTypes.SourceCloud) + } + } + if subType, ok := raw["subtypes"].([]any); ok { + for _, subT := range subType { + if st, ok := subT.(map[string]any); ok { + var s SubType + if err := mapstructure.Decode(st, &s); err != nil { + return Selector{}, fmt.Errorf("decode subtype: %w", err) + } + selector.Subtypes = append(selector.Subtypes, s) + } + } + } + return selector, nil +} + +func ruleQuery(module *ast.Module, rule string) string { + return module.Package.Path.String() + "." + rule +} + +func findPackageAnnotations(module *ast.Module) *ast.Annotations { + return lo.FindOrElse(module.Annotations, nil, func(a *ast.Annotations) bool { + return a.Scope == annotationScopePackage + }) +} diff --git a/pkg/iac/rego/metadata_test.go b/pkg/iac/rego/metadata_test.go new file mode 100644 index 000000000000..225e1be81e97 --- /dev/null +++ b/pkg/iac/rego/metadata_test.go @@ -0,0 +1,333 @@ +package rego + +import ( + "testing" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +func Test_UpdateStaticMetadata(t *testing.T) { + t.Run("happy", func(t *testing.T) { + sm := StaticMetadata{ + ID: "i", + AVDID: "a", + Title: "t", + ShortCode: "sc", + Aliases: []string{"a", "b", "c"}, + Description: "d", + Severity: "s", + RecommendedActions: "ra", + PrimaryURL: "pu", + References: []string{"r"}, + Package: "pkg", + Provider: "pr", + Service: "srvc", + Library: false, + MinimumTrivyVersion: "v1.2.3", + } + + require.NoError(t, sm.populate( + map[string]any{ + "id": "i_n", + "avd_id": "a_n", + "title": "t_n", + "short_code": "sc_n", + "aliases": []any{"a_n", "b_n", "c_n"}, + "description": "d_n", + "service": "srvc_n", + "provider": "pr_n", + "recommended_actions": "ra_n", + "severity": "s_n", + "library": true, + "url": "r_n", + "frameworks": map[string]any{ + "all": []any{"aa"}, + }, + "minimum_trivy_version": "v1.2.3", + }, + )) + + expected := StaticMetadata{ + ID: "i_n", + AVDID: "a_n", + Title: "t_n", + ShortCode: "sc_n", + Aliases: []string{"a", "b", "c", "a_n", "b_n", "c_n"}, + Description: "d_n", + Severity: "S_N", + RecommendedActions: "ra_n", + PrimaryURL: "pu", + MinimumTrivyVersion: "v1.2.3", + References: []string{"r", "r_n"}, + Package: "pkg", + Provider: "pr_n", + Service: "srvc_n", + Library: true, + Frameworks: map[framework.Framework][]string{ + framework.ALL: {"aa"}, + }, + CloudFormation: &scan.EngineMetadata{}, + Terraform: &scan.EngineMetadata{}, + } + + assert.Equal(t, expected, sm) + }) + + t.Run("related resources are a map", func(t *testing.T) { + sm := StaticMetadata{ + References: []string{"r"}, + } + require.NoError(t, sm.populate(map[string]any{ + "related_resources": []map[string]any{ + { + "ref": "r1_n", + }, + { + "ref": "r2_n", + }, + }, + })) + + expected := StaticMetadata{ + References: []string{"r", "r1_n", "r2_n"}, + CloudFormation: &scan.EngineMetadata{}, + Terraform: &scan.EngineMetadata{}, + Frameworks: make(map[framework.Framework][]string), + } + + assert.Equal(t, expected, sm) + }) + + t.Run("related resources are a string", func(t *testing.T) { + sm := StaticMetadata{ + References: []string{"r"}, + } + require.NoError(t, sm.populate(map[string]any{ + "related_resources": []string{"r1_n", "r2_n"}, + })) + + expected := StaticMetadata{ + References: []string{"r", "r1_n", "r2_n"}, + CloudFormation: &scan.EngineMetadata{}, + Terraform: &scan.EngineMetadata{}, + Frameworks: make(map[framework.Framework][]string), + } + + assert.Equal(t, expected, sm) + }) + + t.Run("check is deprecated", func(t *testing.T) { + sm := StaticMetadata{ + Deprecated: false, + } + require.NoError(t, sm.populate(map[string]any{ + "deprecated": true, + })) + + expected := StaticMetadata{ + Deprecated: true, + CloudFormation: &scan.EngineMetadata{}, + Terraform: &scan.EngineMetadata{}, + Frameworks: make(map[framework.Framework][]string), + } + + assert.Equal(t, expected, sm) + }) + + t.Run("frameworks is not initialized", func(t *testing.T) { + sm := StaticMetadata{} + err := sm.populate(map[string]any{ + "frameworks": map[string]any{"all": []any{"a", "b", "c"}}, + }) + require.NoError(t, err) + }) +} + +func Test_NewEngineMetadata(t *testing.T) { + inputSchema := map[string]any{ + "terraform": map[string]any{ + "good_examples": `resource "aws_cloudtrail" "good_example" { + is_multi_region_trail = true + + event_selector { + read_write_type = "All" + include_management_events = true + + data_resource { + type = "AWS::S3::Object" + values = ["${data.aws_s3_bucket.important-bucket.arn}/"] + } + } + }`, + + "links": "https://avd.aquasec.com/avd/183", + }, + "cloud_formation": map[string]any{ + "good_examples": `--- +Resources: + GoodExample: + Type: AWS::CloudTrail::Trail + Properties: + IsLogging: true + IsMultiRegionTrail: true + S3BucketName: "CloudtrailBucket" + S3KeyPrefix: "/trailing" + TrailName: "Cloudtrail"`, + "links": []any{"https://avd.aquasec.com/avd/183"}, + }, + } + + var testCases = []struct { + schema string + want *scan.EngineMetadata + }{ + { + schema: "terraform", + want: &scan.EngineMetadata{ + GoodExamples: []string{ + `resource "aws_cloudtrail" "good_example" { + is_multi_region_trail = true + + event_selector { + read_write_type = "All" + include_management_events = true + + data_resource { + type = "AWS::S3::Object" + values = ["${data.aws_s3_bucket.important-bucket.arn}/"] + } + } + }`, + }, + Links: []string{"https://avd.aquasec.com/avd/183"}, + }, + }, + { + schema: "cloud_formation", + want: &scan.EngineMetadata{ + GoodExamples: []string{ + `--- +Resources: + GoodExample: + Type: AWS::CloudTrail::Trail + Properties: + IsLogging: true + IsMultiRegionTrail: true + S3BucketName: "CloudtrailBucket" + S3KeyPrefix: "/trailing" + TrailName: "Cloudtrail"`, + }, + Links: []string{"https://avd.aquasec.com/avd/183"}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.schema, func(t *testing.T) { + em, err := NewEngineMetadata(tc.schema, inputSchema) + require.NoError(t, err) + assert.Equal(t, tc.want, em) + }) + } +} + +func TestMetadataFromAnnotations(t *testing.T) { + tests := []struct { + name string + input string + expected *StaticMetadata + wantErr string + }{ + { + name: "happy", + input: `# METADATA +# title: test +# custom: +# id: test-001 +# avd_id: avd-test-001 +# severity: LOW +# minimum_trivy_version: 1.2.3 +# input: +# selector: +# - type: yaml +package user.test +`, + expected: &StaticMetadata{ + Title: "test", + ID: "test-001", + // Although this field is deprecated, we still test its parsing + // from metadata to ensure backward compatibility. + AVDID: "avd-test-001", + Severity: "LOW", + InputOptions: InputOptions{ + Selectors: []Selector{ + { + Type: "yaml", + }, + }, + }, + MinimumTrivyVersion: "1.2.3", + Package: "data.user.test", + Frameworks: map[framework.Framework][]string{ + "default": {}, + }, + Terraform: &scan.EngineMetadata{}, + CloudFormation: &scan.EngineMetadata{}, + }, + }, + { + name: "without custom", + input: `# METADATA +# title: test +package user.test +`, + expected: &StaticMetadata{ + Title: "test", + ID: "N/A", + Severity: "UNKNOWN", + Package: "data.user.test", + Frameworks: map[framework.Framework][]string{ + "default": {}, + }, + Terraform: &scan.EngineMetadata{}, + CloudFormation: &scan.EngineMetadata{}, + }, + }, + { + name: "without annotations", + input: `package user.test`, + expected: nil, + }, + { + name: "invalid input field", + input: `# METADATA +# custom: +# input: bad +package user.test`, + wantErr: "input is not an object", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + module, err := ast.ParseModuleWithOpts("test.rego", tt.input, ast.ParserOptions{ + ProcessAnnotation: true, + }) + require.NoError(t, err) + + metadata, err := MetadataFromAnnotations(module) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + assert.Equal(t, tt.expected, metadata) + }) + } +} diff --git a/pkg/iac/rego/options.go b/pkg/iac/rego/options.go new file mode 100644 index 000000000000..910ee96b9987 --- /dev/null +++ b/pkg/iac/rego/options.go @@ -0,0 +1,131 @@ +package rego + +import ( + "io" + "io/fs" + + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" +) + +func WithPolicyReader(readers ...io.Reader) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.policyReaders = readers + } + } +} + +func WithEmbeddedPolicies(include bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.includeEmbeddedPolicies = include + } + } +} + +func WithEmbeddedLibraries(include bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.includeEmbeddedLibraries = include + } + } +} + +// WithTrace specifies an io.Writer for trace logs (mainly rego tracing) - if not set, they are discarded +func WithTrace(w io.Writer) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.traceWriter = w + } + } +} + +func WithPerResultTracing(enabled bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.tracePerResult = enabled + } + } +} + +func WithPolicyDirs(paths ...string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.policyDirs = paths + } + } +} + +func WithDataDirs(paths ...string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.dataDirs = paths + } + } +} + +// WithPolicyNamespaces - namespaces which indicate rego policies containing enforced rules +func WithPolicyNamespaces(namespaces ...string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.ruleNamespaces.Append(namespaces...) + } + } +} + +func WithPolicyFilesystem(fsys fs.FS) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.policyFS = fsys + } + } +} + +func WithDataFilesystem(fsys fs.FS) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.dataFS = fsys + } + } +} + +func WithMaxAllowedErrors(limit int) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.maxAllowedErrors = limit + } + } +} + +func WithCustomSchemas(schemas map[string][]byte) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.customSchemas = schemas + } + } +} + +func WithIncludeDeprecatedChecks(include bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.includeDeprecatedChecks = include + } + } +} + +func WithFrameworks(frameworks ...framework.Framework) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.frameworks = frameworks + } + } +} + +func WithTrivyVersion(version string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.moduleFilters = append(ss.moduleFilters, TrivyVersionFilter(version)) + } + } +} diff --git a/pkg/iac/rego/provider.go b/pkg/iac/rego/provider.go new file mode 100644 index 000000000000..a49e700717c0 --- /dev/null +++ b/pkg/iac/rego/provider.go @@ -0,0 +1,43 @@ +package rego + +import ( + "fmt" + "io/fs" + "sync" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" +) + +func WithRegoScanner(rs *Scanner) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*RegoScannerProvider); ok { + ss.regoScanner = rs + } + } +} + +type RegoScannerProvider struct { + mu sync.Mutex + regoScanner *Scanner +} + +func NewRegoScannerProvider(opts ...options.ScannerOption) *RegoScannerProvider { + s := &RegoScannerProvider{} + for _, o := range opts { + o(s) + } + return s +} + +func (s *RegoScannerProvider) InitRegoScanner(fsys fs.FS, opts []options.ScannerOption) (*Scanner, error) { + s.mu.Lock() + defer s.mu.Unlock() + if s.regoScanner != nil { + return s.regoScanner, nil + } + s.regoScanner = NewScanner(opts...) + if err := s.regoScanner.LoadPolicies(fsys); err != nil { + return nil, fmt.Errorf("load checks: %w", err) + } + return s.regoScanner, nil +} diff --git a/pkg/iac/rego/result.go b/pkg/iac/rego/result.go new file mode 100644 index 000000000000..962f259890b5 --- /dev/null +++ b/pkg/iac/rego/result.go @@ -0,0 +1,167 @@ +package rego + +import ( + "fmt" + "io/fs" + "strconv" + + "github.com/open-policy-agent/opa/v1/rego" + + "github.com/aquasecurity/trivy/pkg/iac/scan" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type regoResult struct { + Filepath string + Resource string + StartLine int + EndLine int + SourcePrefix string + Message string + Explicit bool + Managed bool + FSKey string + FS fs.FS + Parent *regoResult +} + +func (r regoResult) GetMetadata() iacTypes.Metadata { + var m iacTypes.Metadata + if !r.Managed { + m = iacTypes.NewUnmanagedMetadata() + } else { + rng := iacTypes.NewRangeWithFSKey(r.Filepath, r.StartLine, r.EndLine, r.SourcePrefix, r.FSKey, r.FS) + if r.Explicit { + m = iacTypes.NewExplicitMetadata(rng, r.Resource) + } else { + m = iacTypes.NewMetadata(rng, r.Resource) + } + } + if r.Parent != nil { + return m.WithParent(r.Parent.GetMetadata()) + } + return m +} + +func (r regoResult) GetRawValue() any { + return nil +} + +func parseResult(raw any) *regoResult { + var result regoResult + result.Managed = true + switch val := raw.(type) { + case []any: + var msg string + for _, item := range val { + switch raw := item.(type) { + case map[string]any: + result = parseCause(raw) + case string: + msg = raw + } + } + result.Message = msg + case string: + result.Message = val + case map[string]any: + result = parseCause(val) + default: + result.Message = "Rego check resulted in DENY" + } + return &result +} + +func parseCause(cause map[string]any) regoResult { + var result regoResult + result.Managed = true + if msg, ok := cause["msg"]; ok { + result.Message = fmt.Sprintf("%s", msg) + } + if filepath, ok := cause["filepath"]; ok { + result.Filepath = fmt.Sprintf("%s", filepath) + } + if msg, ok := cause["fskey"]; ok { + result.FSKey = fmt.Sprintf("%s", msg) + } + if msg, ok := cause["resource"]; ok { + result.Resource = fmt.Sprintf("%s", msg) + } + if start, ok := cause["startline"]; ok { + result.StartLine = parseLineNumber(start) + } + if end, ok := cause["endline"]; ok { + result.EndLine = parseLineNumber(end) + } + if prefix, ok := cause["sourceprefix"]; ok { + result.SourcePrefix = fmt.Sprintf("%s", prefix) + } + if explicit, ok := cause["explicit"]; ok { + if set, ok := explicit.(bool); ok { + result.Explicit = set + } + } + if managed, ok := cause["managed"]; ok { + if set, ok := managed.(bool); ok { + result.Managed = set + } + } + if parent, ok := cause["parent"]; ok { + if m, ok := parent.(map[string]any); ok { + parentResult := parseCause(m) + result.Parent = &parentResult + } + } + return result +} + +func parseLineNumber(raw any) int { + str := fmt.Sprintf("%s", raw) + n, _ := strconv.Atoi(str) + return n +} + +func (s *Scanner) convertResults(resultSet rego.ResultSet, input Input, namespace, rule string, traces []string) scan.Results { + var results scan.Results + + offset := 0 + if input.Contents != nil { + if xx, ok := input.Contents.(map[string]any); ok { + if md, ok := xx["__defsec_metadata"]; ok { + if md2, ok := md.(map[string]any); ok { + if sl, ok := md2["offset"]; ok { + offset, _ = sl.(int) + } + } + } + } + } + for _, result := range resultSet { + for _, expression := range result.Expressions { + values, ok := expression.Value.([]any) + if !ok { + values = []any{expression.Value} + } + + for _, value := range values { + regoResult := parseResult(value) + regoResult.FS = input.FS + if regoResult.Filepath == "" && input.Path != "" { + regoResult.Filepath = input.Path + } + if regoResult.Message == "" { + regoResult.Message = fmt.Sprintf("Rego check rule: %s.%s", namespace, rule) + } + regoResult.StartLine += offset + regoResult.EndLine += offset + results.AddRego(regoResult.Message, namespace, rule, traces, regoResult) + } + } + } + return results +} + +func (s *Scanner) embellishResultsWithRuleMetadata(results scan.Results, metadata StaticMetadata) scan.Results { + results.SetRule(metadata.ToRule()) + return results +} diff --git a/pkg/iac/rego/result_test.go b/pkg/iac/rego/result_test.go new file mode 100644 index 000000000000..fe1b1df846aa --- /dev/null +++ b/pkg/iac/rego/result_test.go @@ -0,0 +1,104 @@ +package rego + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_parseResult(t *testing.T) { + var testCases = []struct { + name string + input any + want regoResult + }{ + { + name: "unknown", + input: nil, + want: regoResult{ + Managed: true, + Message: "Rego check resulted in DENY", + }, + }, + { + name: "string", + input: "message", + want: regoResult{ + Managed: true, + Message: "message", + }, + }, + { + name: "strings", + input: []any{"message"}, + want: regoResult{ + Managed: true, + Message: "message", + }, + }, + { + name: "maps", + input: []any{ + "message", + map[string]any{ + "filepath": "a.out", + }, + }, + want: regoResult{ + Managed: true, + Message: "message", + Filepath: "a.out", + }, + }, + { + name: "map", + input: map[string]any{ + "msg": "message", + "filepath": "a.out", + "fskey": "abcd", + "resource": "resource", + "startline": "123", + "endline": "456", + "sourceprefix": "git", + "explicit": true, + "managed": true, + }, + want: regoResult{ + Message: "message", + Filepath: "a.out", + Resource: "resource", + StartLine: 123, + EndLine: 456, + SourcePrefix: "git", + FSKey: "abcd", + Explicit: true, + Managed: true, + }, + }, + { + name: "parent", + input: map[string]any{ + "msg": "child", + "parent": map[string]any{ + "msg": "parent", + }, + }, + want: regoResult{ + Message: "child", + Managed: true, + Parent: ®oResult{ + Message: "parent", + Managed: true, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + have := parseResult(tc.input) + assert.NotNil(t, have) + assert.Equal(t, tc.want, *have) + }) + } +} diff --git a/pkg/iac/rego/runtime.go b/pkg/iac/rego/runtime.go new file mode 100644 index 000000000000..2086bee8d755 --- /dev/null +++ b/pkg/iac/rego/runtime.go @@ -0,0 +1,28 @@ +package rego + +import ( + "os" + "strings" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/version" +) + +func addRuntimeValues() *ast.Term { + env := ast.NewObject() + for _, pair := range os.Environ() { + parts := strings.SplitN(pair, "=", 2) + if len(parts) == 1 { + env.Insert(ast.StringTerm(parts[0]), ast.NullTerm()) + } else if len(parts) > 1 { + env.Insert(ast.StringTerm(parts[0]), ast.StringTerm(parts[1])) + } + } + + obj := ast.NewObject() + obj.Insert(ast.StringTerm("env"), ast.NewTerm(env)) + obj.Insert(ast.StringTerm("version"), ast.StringTerm(version.Version)) + obj.Insert(ast.StringTerm("commit"), ast.StringTerm(version.Vcs)) + + return ast.NewTerm(obj) +} diff --git a/pkg/iac/rego/scanner.go b/pkg/iac/rego/scanner.go new file mode 100644 index 000000000000..9fc3ae80d97b --- /dev/null +++ b/pkg/iac/rego/scanner.go @@ -0,0 +1,386 @@ +package rego + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/fs" + "slices" + "strings" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/rego" + "github.com/open-policy-agent/opa/v1/storage" + "github.com/open-policy-agent/opa/v1/util" + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/providers" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +// DefaultAllowedRegoErrors defines the default number of Rego compile errors +// that the scanner will tolerate. This value is set explicitly instead of +// relying on OPA's ast.CompileErrorLimitDefault to avoid depending on upstream changes. +const DefaultAllowedRegoErrors = 10 + +var ( + checkTypesWithSubtype = set.New(types.SourceCloud, types.SourceDefsec, types.SourceKubernetes) + + supportedProviders = makeSupportedProviders() +) + +func makeSupportedProviders() set.Set[string] { + m := set.New[string]() + for _, p := range providers.AllProviders() { + m.Append(string(p)) + } + m.Append("kind") // kubernetes + return m +} + +var _ options.ConfigurableScanner = (*Scanner)(nil) + +type Scanner struct { + ruleNamespaces set.Set[string] + policies map[string]*ast.Module + moduleMetadata map[string]*StaticMetadata + store storage.Store + runtimeValues *ast.Term + compiler *ast.Compiler + + // maxAllowedErrors defines how many Rego compile errors + // the scanner can tolerate. If more errors occur, an error is returned. + maxAllowedErrors int + logger *log.Logger + traceWriter io.Writer + tracePerResult bool + retriever *MetadataRetriever + policyFS fs.FS + policyDirs []string + policyReaders []io.Reader + dataFS fs.FS + dataDirs []string + frameworks []framework.Framework + includeDeprecatedChecks bool + includeEmbeddedPolicies bool + includeEmbeddedLibraries bool + + moduleFilters []RegoModuleFilter + + embeddedLibs map[string]*ast.Module + embeddedChecks map[string]*ast.Module + customSchemas map[string][]byte +} + +func (s *Scanner) trace(heading string, input any) { + if s.traceWriter == nil { + return + } + data, err := json.MarshalIndent(input, "", " ") + if err != nil { + return + } + _, _ = fmt.Fprintf(s.traceWriter, "REGO %[1]s:\n%s\nEND REGO %[1]s\n\n", heading, string(data)) +} + +type DynamicMetadata struct { + Warning bool + Filepath string + Message string + StartLine int + EndLine int +} + +func NewScanner(opts ...options.ScannerOption) *Scanner { + LoadAndRegister() + + s := &Scanner{ + maxAllowedErrors: DefaultAllowedRegoErrors, + ruleNamespaces: builtinNamespaces.Clone(), + runtimeValues: addRuntimeValues(), + logger: log.WithPrefix("rego"), + customSchemas: make(map[string][]byte), + moduleMetadata: make(map[string]*StaticMetadata), + } + + for _, opt := range opts { + opt(s) + } + + s.moduleFilters = append( + s.moduleFilters, + FrameworksFilter(s.frameworks), + IncludeDeprecatedFilter(s.includeDeprecatedChecks), + ) + + return s +} + +func (s *Scanner) runQuery(ctx context.Context, query string, input ast.Value, disableTracing bool) (rego.ResultSet, []string, error) { + + trace := (s.traceWriter != nil || s.tracePerResult) && !disableTracing + + regoOptions := []func(*rego.Rego){ + rego.Query(query), + rego.Compiler(s.compiler), + rego.Store(s.store), + rego.Runtime(s.runtimeValues), + rego.Trace(trace), + } + + if input != nil { + regoOptions = append(regoOptions, rego.ParsedInput(input)) + } + + instance := rego.New(regoOptions...) + resultSet, err := instance.Eval(ctx) + if err != nil { + return nil, nil, err + } + + // we also build a slice of trace lines for per-result tracing - primarily for fanal/trivy + var traces []string + + if trace { + if s.traceWriter != nil { + rego.PrintTrace(s.traceWriter, instance) + } + if s.tracePerResult { + traceBuffer := bytes.NewBuffer([]byte{}) + rego.PrintTrace(traceBuffer, instance) + traces = strings.Split(traceBuffer.String(), "\n") + } + } + return resultSet, traces, nil +} + +type Input struct { + Path string `json:"path"` + FS fs.FS `json:"-"` + Contents any `json:"contents"` + + // parsed is the parsed input value for the rego query + parsed ast.Value +} + +func GetInputsContents(inputs []Input) []any { + results := make([]any, len(inputs)) + for i, c := range inputs { + results[i] = c.Contents + } + return results +} + +func (s *Scanner) ScanInput(ctx context.Context, sourceType types.Source, inputs ...Input) (scan.Results, error) { + + s.logger.Debug("Scanning inputs", "count", len(inputs)) + + if len(inputs) == 0 { + return nil, nil + } + + inputs = lo.FilterMap(inputs, func(input Input, _ int) (Input, bool) { + s.trace("INPUT", input) + parsed, err := parseRawInput(input.Contents) + if err != nil { + s.logger.Error("Failed to parse input", log.FilePath(input.Path), log.Err(err)) + return input, false + } + input.parsed = parsed + return input, true + }) + + var results scan.Results + + for path, module := range s.policies { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + namespace := getModuleNamespace(module) + topLevel := strings.Split(namespace, ".")[0] + if !s.ruleNamespaces.Contains(topLevel) { + continue + } + + staticMeta, err := s.metadataForModule(ctx, path, module, inputs) + if err != nil { + s.logger.Error( + "Error occurred while retrieving metadata from check", + log.FilePath(module.Package.Location.File), + log.Err(err), + ) + continue + } + + // skip if check isn't relevant to what is being scanned + if !isPolicyApplicable(sourceType, staticMeta, inputs...) { + continue + } + + usedRules := set.New[string]() + + // all rules + for _, rule := range module.Rules { + ruleName := rule.Head.Name.String() + if usedRules.Contains(ruleName) { + continue + } + usedRules.Append(ruleName) + if isEnforcedRule(ruleName) { + ruleResults, err := s.applyRule(ctx, namespace, ruleName, inputs) + if err != nil { + s.logger.Error( + "Error occurred while applying rule from check", + log.String("rule", ruleName), + log.FilePath(module.Package.Location.File), + log.Err(err), + ) + continue + } + results = append(results, s.embellishResultsWithRuleMetadata(ruleResults, *staticMeta)...) + } + } + + } + + return results, nil +} + +func (s *Scanner) metadataForModule( + ctx context.Context, path string, module *ast.Module, inputs []Input, +) (*StaticMetadata, error) { + if metadata, exists := s.moduleMetadata[path]; exists { + return metadata, nil + } + + metadata, err := s.retriever.RetrieveMetadata(ctx, module, GetInputsContents(inputs)...) + if err != nil { + return nil, err + } + return metadata, nil +} + +func isPolicyWithSubtype(sourceType types.Source) bool { + return checkTypesWithSubtype.Contains(sourceType) +} + +func checkSubtype(ii map[string]any, provider string, subTypes []SubType) bool { + if len(subTypes) == 0 { + return true + } + + for _, st := range subTypes { + switch services := ii[provider].(type) { + case map[string]any: + if st.Provider != provider { + continue + } + if _, exists := services[st.Service]; exists { + return true + } + case string: // k8s - logic can be improved + if strings.EqualFold(services, st.Group) || + strings.EqualFold(services, st.Version) || + strings.EqualFold(services, st.Kind) { + return true + } + } + } + return false +} + +var sourcesWithExplicitSelectors = []types.Source{ + // apply terrafrom-specific checks only if selectors exist + types.SourceTerraformRaw, +} + +func isPolicyApplicable(sourceType types.Source, staticMetadata *StaticMetadata, inputs ...Input) bool { + if len(staticMetadata.InputOptions.Selectors) == 0 && + slices.Contains(sourcesWithExplicitSelectors, sourceType) { + return false + } + + if len(staticMetadata.InputOptions.Selectors) == 0 { // check always applies if no selectors + return true + } + + for _, selector := range staticMetadata.InputOptions.Selectors { + if selector.Type != string(sourceType) { + return false + } + } + + if !isPolicyWithSubtype(sourceType) { + return true + } + + for _, input := range inputs { + if ii, ok := input.Contents.(map[string]any); ok { + for provider := range ii { + if !supportedProviders.Contains(provider) { + continue + } + + // check metadata for subtype + for _, s := range staticMetadata.InputOptions.Selectors { + if checkSubtype(ii, provider, s.Subtypes) { + return true + } + } + } + } + } + return false +} + +func parseRawInput(input any) (ast.Value, error) { + if err := util.RoundTrip(&input); err != nil { + return nil, err + } + + return ast.InterfaceToValue(input) +} + +func (s *Scanner) applyRule(ctx context.Context, namespace, rule string, inputs []Input) (scan.Results, error) { + var results scan.Results + qualified := fmt.Sprintf("data.%s.%s", namespace, rule) + for _, input := range inputs { + + resultSet, traces, err := s.runQuery(ctx, qualified, input.parsed, false) + if err != nil { + return nil, err + } + s.trace("RESULTSET", resultSet) + ruleResults := s.convertResults(resultSet, input, namespace, rule, traces) + if len(ruleResults) == 0 { // It passed because we didn't find anything wrong (NOT because it didn't exist) + var result regoResult + result.FS = input.FS + result.Filepath = input.Path + result.Managed = true + results.AddPassedRego(namespace, rule, traces, result) + continue + } + results = append(results, ruleResults...) + } + + return results, nil +} + +// severity is now set with metadata, so deny/warn/violation now behave the same way +func isEnforcedRule(name string) bool { + switch { + case name == "deny", strings.HasPrefix(name, "deny_"): + return true + } + return false +} diff --git a/pkg/iac/rego/scanner_test.go b/pkg/iac/rego/scanner_test.go new file mode 100644 index 000000000000..4446be82d6f2 --- /dev/null +++ b/pkg/iac/rego/scanner_test.go @@ -0,0 +1,1078 @@ +package rego_test + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/severity" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_RegoScanning_Deny(t *testing.T) { + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": ` +# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test + +deny { + input.evil +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + FS: srcFS, + }) + require.NoError(t, err) + + require.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) + + assert.Equal(t, "/evil.lol", results.GetFailed()[0].Metadata().Range().GetFilename()) +} + +func Test_RegoScanning_AbsolutePolicyPath_Deny(t *testing.T) { + + tmp := t.TempDir() + require.NoError(t, os.Mkdir(filepath.Join(tmp, "policies"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(tmp, "policies", "test.rego"), []byte(`# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing +package defsec.test + +deny { + input.evil +}`), 0o600)) + + srcFS := os.DirFS(tmp) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + FS: srcFS, + }) + require.NoError(t, err) + + require.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) + + assert.Equal(t, "/evil.lol", results.GetFailed()[0].Metadata().Range().GetFilename()) +} + +func Test_RegoScanning_Allow(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test + +deny { + input.evil +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": false, + }, + }) + require.NoError(t, err) + + assert.Empty(t, results.GetFailed()) + require.Len(t, results.GetPassed(), 1) + assert.Empty(t, results.GetIgnored()) + + assert.Equal(t, "/evil.lol", results.GetPassed()[0].Metadata().Range().GetFilename()) +} + +func Test_RegoScanning_WithRuntimeValues(t *testing.T) { + + t.Setenv("DEFSEC_RUNTIME_VAL", "AOK") + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test + +deny_evil { + output := opa.runtime() + output.env.DEFSEC_RUNTIME_VAL == "AOK" +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + assert.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) +} + +func Test_RegoScanning_WithDenyMessage(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test + +deny[msg] { + input.evil + msg := "oh no" +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + require.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) + + assert.Equal(t, "oh no", results.GetFailed()[0].Description()) + assert.Equal(t, "/evil.lol", results.GetFailed()[0].Metadata().Range().GetFilename()) +} + +func Test_RegoScanning_WithDenyMetadata_ImpliedPath(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": ` +# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test + +deny[res] { + input.evil + res := { + "msg": "oh no", + "startline": 123, + "endline": 456, + } +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + require.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) + + assert.Equal(t, "oh no", results.GetFailed()[0].Description()) + assert.Equal(t, "/evil.lol", results.GetFailed()[0].Metadata().Range().GetFilename()) + assert.Equal(t, 123, results.GetFailed()[0].Metadata().Range().GetStartLine()) + assert.Equal(t, 456, results.GetFailed()[0].Metadata().Range().GetEndLine()) + +} + +func Test_RegoScanning_WithDenyMetadata_PersistedPath(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": ` +# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test + +deny[res] { + input.evil + res := { + "msg": "oh no", + "startline": 123, + "endline": 456, + "filepath": "/blah.txt", + } +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + require.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) + + assert.Equal(t, "oh no", results.GetFailed()[0].Description()) + assert.Equal(t, "/blah.txt", results.GetFailed()[0].Metadata().Range().GetFilename()) + assert.Equal(t, 123, results.GetFailed()[0].Metadata().Range().GetStartLine()) + assert.Equal(t, 456, results.GetFailed()[0].Metadata().Range().GetEndLine()) + +} + +func Test_RegoScanning_WithStaticMetadata(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": ` +package defsec.test + +__rego_metadata__ := { + "id": "AA001", + "title": "This is a title", + "short_code": "short-code", + "severity": "LOW", + "type": "Dockerfile Security Check", + "description": "This is a description", + "recommended_actions": "This is a recommendation", + "url": "https://google.com", +} + +deny[res] { + input.evil + res := { + "msg": "oh no", + "startline": 123, + "endline": 456, + "filepath": "/blah.txt", + } +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + require.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) + + failure := results.GetFailed()[0] + + assert.Equal(t, "oh no", failure.Description()) + assert.Equal(t, "/blah.txt", failure.Metadata().Range().GetFilename()) + assert.Equal(t, 123, failure.Metadata().Range().GetStartLine()) + assert.Equal(t, 456, failure.Metadata().Range().GetEndLine()) + assert.Equal(t, "AA001", failure.Rule().ID) + assert.Equal(t, "This is a title", failure.Rule().Summary) + assert.Equal(t, severity.Low, failure.Rule().Severity) + assert.Equal(t, "This is a recommendation", failure.Rule().Resolution) + assert.Equal(t, "https://google.com", failure.Rule().Links[0]) + +} + +func Test_RegoScanning_WithMatchingInputSelector(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test + +__rego_input__ := { + "selector": [{"type": "json"}], +} + +deny { + input.evil +} + +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + assert.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) +} + +func Test_RegoScanning_WithNonMatchingInputSelector(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": ` +package defsec.test + +__rego_input__ := { + "selector": [{"type": "testing"}], +} + +deny { + input.evil +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + assert.Empty(t, results.GetFailed()) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) +} + +func Test_RegoScanning_NoTracingByDefault(t *testing.T) { + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test + +deny { + input.evil +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + assert.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) + + assert.Empty(t, results.GetFailed()[0].Traces()) +} + +func Test_RegoScanning_GlobalTracingEnabled(t *testing.T) { + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test + +deny { + input.evil +} +`, + }) + + traceBuffer := bytes.NewBuffer([]byte{}) + + scanner := rego.NewScanner( + rego.WithTrace(traceBuffer), + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + assert.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) + + assert.Empty(t, results.GetFailed()[0].Traces()) + assert.NotEmpty(t, traceBuffer.Bytes()) +} + +func Test_RegoScanning_PerResultTracingEnabled(t *testing.T) { + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing +package defsec.test + +deny { + input.evil +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPerResultTracing(true), + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "evil": true, + }, + }) + require.NoError(t, err) + + assert.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) + + assert.NotEmpty(t, results.GetFailed()[0].Traces()) +} + +func Test_dynamicMetadata(t *testing.T) { + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": ` +package defsec.test + +__rego_metadata__ := { + "title" : sprintf("i am %s",[input.text]) +} + +deny { + input.text +} + +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "text": "dynamic", + }, + }) + require.NoError(t, err) + assert.Equal(t, "i am dynamic", results[0].Rule().Summary) +} + +func Test_staticMetadata(t *testing.T) { + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": ` +package defsec.test + +__rego_metadata__ := { + "title" : "i am static" +} + +deny { + input.text +} + +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "text": "test", + }, + }) + require.NoError(t, err) + assert.Equal(t, "i am static", results[0].Rule().Summary) +} + +func Test_annotationMetadata(t *testing.T) { + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: i am a title +# description: i am a description +# related_resources: +# - https://google.com +# custom: +# id: EG123 +# severity: LOW +# recommended_action: have a cup of tea +package defsec.test + +deny { + input.text +} + +`, + "policies/test2.rego": `# METADATA +# title: i am another title +package defsec.test2 + +deny { + input.blah +} + +`, + }) + + scanner := rego.NewScanner( + rego.WithPerResultTracing(true), + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "text": "test", + }, + }) + require.NoError(t, err) + require.Len(t, results.GetFailed(), 1) + failure := results.GetFailed()[0].Rule() + assert.Equal(t, "i am a title", failure.Summary) + assert.Equal(t, "i am a description", failure.Explanation) + require.Len(t, failure.Links, 1) + assert.Equal(t, "https://google.com", failure.Links[0]) + assert.Equal(t, severity.Low, failure.Severity) + assert.Equal(t, "have a cup of tea", failure.Resolution) +} + +func Test_RegoScanning_WithInvalidInputSchema(t *testing.T) { + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# schemas: +# - input: schema["input"] +package defsec.test + +deny { + input.evil == "lol" +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) +} + +func Test_RegoScanning_WithValidInputSchema(t *testing.T) { + + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# schemas: +# - input: schema["input"] +package defsec.test + +deny { + input.Stages[0].Commands[0].Cmd == "lol" +} +`, + }) + + scanner := rego.NewScanner( + rego.WithPolicyDirs("policies"), + ) + require.NoError(t, scanner.LoadPolicies(srcFS)) +} + +func Test_RegoScanning_WithFilepathToSchema(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# schemas: +# - input: schema["dockerfile"] +package defsec.test + +deny { + input.evil == "lol" +} +`, + }) + + scanner := rego.NewScanner( + rego.WithMaxAllowedErrors(0), + rego.WithPolicyDirs("policies"), + ) + + assert.ErrorContains( + t, + scanner.LoadPolicies(srcFS), + "undefined ref: input.evil", + ) +} + +func Test_RegoScanning_CustomData(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test +import data.settings.DS123.foo_bar_baz + +deny { + not foo_bar_baz +} +`, + }) + + dataFS := testutil.CreateFS(map[string]string{ + "data/data.json": `{ + "settings": { + "DS123":{ + "foo_bar_baz":false + } + } +}`, + "data/junk.txt": "this file should be ignored", + }) + + scanner := rego.NewScanner( + rego.WithDataFilesystem(dataFS), + rego.WithDataDirs("."), + rego.WithPolicyDirs("policies"), + ) + + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{}) + require.NoError(t, err) + + assert.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) +} + +func Test_RegoScanning_InvalidFS(t *testing.T) { + srcFS := testutil.CreateFS(map[string]string{ + "policies/test.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] +# custom: +# id: BAR-0001 +# provider: custom +# service: custom +# severity: LOW +# short_code: custom-policy +# recommended_action: Custom policy for testing + +package defsec.test +import data.settings.DS123.foo_bar_baz + +deny { + not foo_bar_baz +} +`, + }) + + dataFS := testutil.CreateFS(map[string]string{ + "data/data.json": `{ + "settings": { + "DS123":{ + "foo_bar_baz":false + } + } +}`, + "data/junk.txt": "this file should be ignored", + }) + + scanner := rego.NewScanner( + rego.WithDataFilesystem(dataFS), + rego.WithDataDirs("X://"), + rego.WithPolicyDirs("policies"), + ) + + require.NoError(t, scanner.LoadPolicies(srcFS)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{}) + require.NoError(t, err) + + assert.Len(t, results.GetFailed(), 1) + assert.Empty(t, results.GetPassed()) + assert.Empty(t, results.GetIgnored()) +} + +func Test_NoErrorsWhenUsingBadRegoCheck(t *testing.T) { + + // this check cause eval_conflict_error + // https://www.openpolicyagent.org/docs/latest/policy-language/#functions + fsys := fstest.MapFS{ + "checks/bad.rego": { + Data: []byte(`package defsec.test + +p(x) = y { + y := x[_] +} + +deny { + p([1, 2, 3]) +} +`), + }, + } + + scanner := rego.NewScanner( + rego.WithPolicyDirs("checks"), + ) + require.NoError(t, scanner.LoadPolicies(fsys)) + _, err := scanner.ScanInput(t.Context(), types.SourceYAML, rego.Input{}) + require.NoError(t, err) +} + +func Test_RegoScanning_WithDeprecatedCheck(t *testing.T) { + + check := `# METADATA +# title: i am a deprecated check +# description: i am a description +# related_resources: +# - https://google.com +# custom: +# id: EG123 +# severity: LOW +# recommended_action: have a cup of tea +# deprecated: %v +package defsec.test + +deny { + input.text +}` + + var testCases = []struct { + name string + policy string + opts []options.ScannerOption + expectedResults int + }{ + { + name: "deprecated check is skipped by default", + policy: fmt.Sprintf(check, true), + expectedResults: 0, + }, + { + name: "non-deprecated check is executed", + policy: fmt.Sprintf(check, false), + expectedResults: 1, + }, + { + name: "deprecated check is executed when includeDeprecatedChecks is true", + policy: fmt.Sprintf(check, true), + opts: []options.ScannerOption{rego.WithIncludeDeprecatedChecks(true)}, + expectedResults: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + opts := append(tc.opts, rego.WithPolicyReader(strings.NewReader(tc.policy))) + scanner := rego.NewScanner(opts...) + require.NoError(t, scanner.LoadPolicies(nil)) + + results, err := scanner.ScanInput(t.Context(), types.SourceJSON, rego.Input{ + Path: "/evil.lol", + Contents: map[string]any{ + "text": "test", + }, + }) + require.NoError(t, err) + require.Len(t, results, tc.expectedResults, tc.name) + }) + } +} + +func Test_RegoScanner_WithCustomSchemas(t *testing.T) { + + schema := `{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "service": { "type": "string" } + }, + "required": ["service"] +}` + + tests := []struct { + name string + check string + expectedResults int + }{ + { + name: "happy path", + check: `# METADATA +# title: test check +# schemas: +# - input: schema["test"] +package user.test + +deny { + input.service == "test" +} +`, + expectedResults: 1, + }, + { + name: "sad path", + check: `# METADATA +# title: test check +# schemas: +# - input: schema["test"] +package user.test + +deny { + input.other == "test" +} +`, + expectedResults: 0, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + scanner := rego.NewScanner( + rego.WithCustomSchemas(map[string][]byte{ + "test": []byte(schema), + }), + rego.WithPolicyNamespaces("user"), + rego.WithPolicyReader(strings.NewReader(tc.check)), + ) + + require.NoError(t, scanner.LoadPolicies(nil)) + + results, err := scanner.ScanInput(t.Context(), types.SourceYAML, rego.Input{ + Path: "test.yaml", + Contents: map[string]any{"service": "test"}, + }) + require.NoError(t, err) + require.Len(t, results, tc.expectedResults, tc.name) + }) + } +} diff --git a/pkg/iac/rego/schemas/00_schema.go b/pkg/iac/rego/schemas/00_schema.go new file mode 100644 index 000000000000..f98c5f8bda1e --- /dev/null +++ b/pkg/iac/rego/schemas/00_schema.go @@ -0,0 +1,25 @@ +package schemas + +import _ "embed" + +type Schema string + +var ( + None Schema = "" + Anything Schema = `{}` + + //go:embed dockerfile.json + Dockerfile Schema + + //go:embed kubernetes.json + Kubernetes Schema + + //go:embed rbac.json + RBAC Schema + + //go:embed cloud.json + Cloud Schema + + //go:embed terraform-raw.json + TerraformRaw Schema +) diff --git a/pkg/iac/rego/schemas/builder.go b/pkg/iac/rego/schemas/builder.go new file mode 100644 index 000000000000..38f3c0c13594 --- /dev/null +++ b/pkg/iac/rego/schemas/builder.go @@ -0,0 +1,275 @@ +package schemas + +import ( + "errors" + "fmt" + "reflect" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/rego/convert" + "github.com/aquasecurity/trivy/pkg/iac/state" +) + +type RawSchema struct { + Type string `json:"type"` // object + Properties map[string]Property `json:"properties,omitempty"` + Defs map[string]*Property `json:"definitions,omitempty"` +} + +type Property struct { + Type string `json:"type,omitempty"` + Ref string `json:"$ref,omitempty"` + Properties map[string]Property `json:"properties,omitempty"` + Items *Property `json:"items,omitempty"` +} + +type builder struct { + schema RawSchema +} + +func Build() (*RawSchema, error) { + + b := newBuilder() + + inputValue := reflect.ValueOf(state.State{}) + + err := b.fromInput(inputValue) + if err != nil { + return nil, err + } + + return &b.schema, nil +} + +func newBuilder() *builder { + return &builder{ + schema: RawSchema{ + Properties: nil, + Defs: nil, + }, + } +} + +func (b *builder) fromInput(inputValue reflect.Value) error { + + prop, err := b.readProperty("", nil, inputValue.Type(), 0) + if err != nil { + return err + } + if prop == nil { + return errors.New("property is nil") + } + b.schema.Properties = prop.Properties + b.schema.Type = prop.Type + return nil +} + +func refName(name string, parent, t reflect.Type) string { + if t.Name() == "" { // inline struct + return sanitize(parent.PkgPath() + "." + parent.Name() + "." + name) + } + return sanitize(t.PkgPath() + "." + t.Name()) +} + +func sanitize(s string) string { + return strings.ReplaceAll(s, "/", ".") +} + +func (b *builder) readProperty(name string, parent, inputType reflect.Type, indent int) (*Property, error) { + if inputType.Kind() == reflect.Ptr { + inputType = inputType.Elem() + } + + switch inputType.String() { + case "types.Range", "types.Reference": + return nil, nil + } + + if b.schema.Defs != nil { + _, ok := b.schema.Defs[refName(name, parent, inputType)] + if ok { + return &Property{ + Type: "object", + Ref: "#/definitions/" + refName(name, parent, inputType), + }, nil + } + } + + fmt.Println(strings.Repeat(" ", indent) + name) + + switch kind := inputType.Kind(); kind { + case reflect.Struct: + return b.readStruct(name, parent, inputType, indent) + case reflect.Slice: + return b.readSlice(name, parent, inputType, indent) + case reflect.String: + return &Property{ + Type: "string", + }, nil + case reflect.Int: + return &Property{ + Type: "integer", + }, nil + case reflect.Bool: + return &Property{ + Type: "boolean", + }, nil + case reflect.Float32, reflect.Float64: + return &Property{ + Type: "number", + }, nil + } + + switch inputType.Name() { + case "BoolValue": + return &Property{ + Type: "object", + Properties: map[string]Property{ + "value": { + Type: "boolean", + }, + }, + }, nil + case "IntValue": + return &Property{ + Type: "object", + Properties: map[string]Property{ + "value": { + Type: "integer", + }, + }, + }, nil + case "StringValue", "TimeValue", "BytesValue": + return &Property{ + Type: "object", + Properties: map[string]Property{ + "value": { + Type: "string", + }, + }, + }, nil + case "MapValue": + return &Property{ + Type: "object", + Properties: map[string]Property{ + "value": { + Type: "object", + }, + }, + }, nil + + } + + fmt.Printf("WARNING: unsupported type: %s (%s)\n", inputType.Name(), inputType) + return nil, nil +} + +var converterInterface = reflect.TypeOf((*convert.Converter)(nil)).Elem() + +func (b *builder) readStruct(name string, parent, inputType reflect.Type, indent int) (*Property, error) { + + if b.schema.Defs == nil { + b.schema.Defs = make(map[string]*Property) + } + + def := &Property{ + Type: "object", + Properties: make(map[string]Property), + } + + if parent != nil { + b.schema.Defs[refName(name, parent, inputType)] = def + } + + if inputType.Implements(converterInterface) || + inputType.String() == "types.Metadata" { + if inputType.Kind() == reflect.Ptr { + inputType = inputType.Elem() + } + returns := reflect.New(inputType).MethodByName("ToRego").Call(nil) + if err := b.readRego(def, name, parent, returns[0].Type(), returns[0].Interface(), indent); err != nil { + return nil, err + } + } else { + + for i := 0; i < inputType.NumField(); i++ { + + field := inputType.Field(i) + prop, err := b.readProperty(field.Name, inputType, field.Type, indent+1) + if err != nil { + return nil, err + } + if prop == nil { + continue + } + key := strings.ToLower(field.Name) + + // metadata exported as "__defsec_metadata" + if key == "metadata" { + key = "__defsec_metadata" + } + + def.Properties[key] = *prop + } + } + + if parent == nil { + return def, nil + } + + return &Property{ + Type: "object", + Ref: "#/definitions/" + refName(name, parent, inputType), + }, nil +} + +func (b *builder) readSlice(name string, parent, inputType reflect.Type, indent int) (*Property, error) { + + items, err := b.readProperty(name, parent, inputType.Elem(), indent+1) + if err != nil { + return nil, err + } + + prop := &Property{ + Type: "array", + Items: items, + } + return prop, nil +} + +func (b *builder) readRego(def *Property, name string, parent, typ reflect.Type, raw any, indent int) error { + + switch cast := raw.(type) { + case map[string]any: + def.Type = "object" + for k, v := range cast { + child := &Property{ + Properties: make(map[string]Property), + } + if err := b.readRego(child, k, reflect.TypeOf(raw), reflect.TypeOf(v), v, indent+1); err != nil { + return err + } + def.Properties[k] = *child + } + case map[string]string: + def.Type = "object" + for k, v := range cast { + child := &Property{ + Properties: make(map[string]Property), + } + if err := b.readRego(child, k, reflect.TypeOf(raw), reflect.TypeOf(v), v, indent+1); err != nil { + return err + } + def.Properties[k] = *child + } + default: + prop, err := b.readProperty(name, parent, typ, indent) + if err != nil { + return err + } + *def = *prop + } + + return nil + +} diff --git a/pkg/iac/rego/schemas/cloud.json b/pkg/iac/rego/schemas/cloud.json new file mode 100644 index 000000000000..6d9e363c3129 --- /dev/null +++ b/pkg/iac/rego/schemas/cloud.json @@ -0,0 +1,8636 @@ +{ + "type": "object", + "properties": { + "aws": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.AWS" + }, + "azure": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.Azure" + }, + "cloudstack": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.cloudstack.CloudStack" + }, + "digitalocean": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.DigitalOcean" + }, + "github": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.github.GitHub" + }, + "google": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.Google" + }, + "kubernetes": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.Kubernetes" + }, + "nifcloud": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.Nifcloud" + }, + "openstack": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.openstack.OpenStack" + }, + "oracle": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.oracle.Oracle" + } + }, + "definitions": { + "github.com.aquasecurity.trivy.pkg.iac.adapters.common.PortRange": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "end": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "start": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.AWS": { + "type": "object", + "properties": { + "accessanalyzer": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.accessanalyzer.AccessAnalyzer" + }, + "apigateway": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.APIGateway" + }, + "athena": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.athena.Athena" + }, + "cloudfront": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.Cloudfront" + }, + "cloudtrail": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudtrail.CloudTrail" + }, + "cloudwatch": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.CloudWatch" + }, + "codebuild": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.codebuild.CodeBuild" + }, + "config": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.config.Config" + }, + "documentdb": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.documentdb.DocumentDB" + }, + "dynamodb": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.dynamodb.DynamoDB" + }, + "ec2": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.EC2" + }, + "ecr": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecr.ECR" + }, + "ecs": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.ECS" + }, + "efs": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.efs.EFS" + }, + "eks": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.eks.EKS" + }, + "elasticache": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticache.ElastiCache" + }, + "elasticsearch": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.Elasticsearch" + }, + "elb": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elb.ELB" + }, + "emr": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.emr.EMR" + }, + "iam": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.IAM" + }, + "kinesis": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.kinesis.Kinesis" + }, + "kms": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.kms.KMS" + }, + "lambda": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.lambda.Lambda" + }, + "meta": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.Meta" + }, + "mq": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.mq.MQ" + }, + "msk": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.MSK" + }, + "neptune": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.neptune.Neptune" + }, + "rds": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.RDS" + }, + "redshift": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.Redshift" + }, + "s3": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.S3" + }, + "sam": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.SAM" + }, + "sns": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sns.SNS" + }, + "sqs": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sqs.SQS" + }, + "ssm": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ssm.SSM" + }, + "workspaces": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.workspaces.WorkSpaces" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.AssumeRole": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "duration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "externalid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "policy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "policyarns": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "rolearn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "sessionname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "sourceidentity": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "tags": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.MapValue" + }, + "transitivetagkeys": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.AssumeRoleWithWebIdentity": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "duration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "policy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "policyarns": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "rolearn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "sessionname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "webidentitytoken": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "webidentitytokenfile": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.DefaultTags": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "tags": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.MapValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.IgnoreTags": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "keyprefixes": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "keys": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.Meta": { + "type": "object", + "properties": { + "tfproviders": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.TerraformProvider" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.TerraformProvider": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accesskey": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "alias": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "allowedaccountsids": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "assumerole": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.AssumeRole" + }, + "assumerolewithwebidentity": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.AssumeRoleWithWebIdentity" + }, + "customcabundle": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "defaulttags": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.DefaultTags" + }, + "ec2metadataserviceendpoint": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "ec2metadataserviceendpointmode": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "endpoints": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.MapValue" + }, + "forbiddenaccountids": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "httpproxy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "ignoretags": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.IgnoreTags" + }, + "insecure": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "maxretries": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "profile": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "region": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "retrymode": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "s3useast1regionalendpoint": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "s3usepathstyle": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "secretkey": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "sharedconfigfiles": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "sharedcredentialsfiles": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "skipcredentialsvalidation": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "skipmetadataapicheck": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "skipregionvalidation": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "skiprequestingaccountid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "stsregion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "token": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "usedualstackendpoint": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "usefipsendpoint": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "version": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.accessanalyzer.AccessAnalyzer": { + "type": "object", + "properties": { + "analyzers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.accessanalyzer.Analyzer" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.accessanalyzer.Analyzer": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "active": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "arn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "findings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.accessanalyzer.Findings" + } + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.accessanalyzer.Findings": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.APIGateway": { + "type": "object", + "properties": { + "v1": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.APIGateway" + }, + "v2": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.APIGateway" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.API": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "resources": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.Resource" + } + }, + "stages": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.Stage" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.APIGateway": { + "type": "object", + "properties": { + "apis": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.API" + } + }, + "domainnames": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.DomainName" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.AccessLogging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cloudwatchloggrouparn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.DomainName": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "securitypolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.Method": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "apikeyrequired": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "authorizationtype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "httpmethod": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.RESTMethodSettings": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cachedataencrypted": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "cacheenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "method": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.Resource": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "methods": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.Method" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.Stage": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accesslogging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.AccessLogging" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "restmethodsettings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v1.RESTMethodSettings" + } + }, + "xraytracingenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.API": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "protocoltype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "stages": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.Stage" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.APIGateway": { + "type": "object", + "properties": { + "apis": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.API" + } + }, + "domainnames": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.DomainName" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.AccessLogging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cloudwatchloggrouparn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.DomainName": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "securitypolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.Stage": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accesslogging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.apigateway.v2.AccessLogging" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.athena.Athena": { + "type": "object", + "properties": { + "databases": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.athena.Database" + } + }, + "workgroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.athena.Workgroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.athena.Database": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.athena.EncryptionConfiguration" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.athena.EncryptionConfiguration": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.athena.Workgroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.athena.EncryptionConfiguration" + }, + "enforceconfiguration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.CacheBehaviour": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "viewerprotocolpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.Cloudfront": { + "type": "object", + "properties": { + "distributions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.Distribution" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.Distribution": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "defaultcachebehaviour": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.CacheBehaviour" + }, + "logging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.Logging" + }, + "orderercachebehaviours": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.CacheBehaviour" + } + }, + "viewercertificate": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.ViewerCertificate" + }, + "wafid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.Logging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "bucket": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudfront.ViewerCertificate": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cloudfrontdefaultcertificate": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "minimumprotocolversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "sslsupportmethod": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudtrail.CloudTrail": { + "type": "object", + "properties": { + "trails": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudtrail.Trail" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudtrail.DataResource": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "values": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudtrail.EventSelector": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "dataresources": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudtrail.DataResource" + } + }, + "readwritetype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudtrail.Trail": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "bucketname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "cloudwatchlogsloggrouparn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "enablelogfilevalidation": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "eventselectors": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudtrail.EventSelector" + } + }, + "islogging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "ismultiregion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.Alarm": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "alarmname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "dimensions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.AlarmDimension" + } + }, + "metricname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "metrics": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.MetricDataQuery" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.AlarmDimension": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "value": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.CloudWatch": { + "type": "object", + "properties": { + "alarms": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.Alarm" + } + }, + "loggroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.LogGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.LogGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "arn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "metricfilters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.MetricFilter" + } + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "retentionindays": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.MetricDataQuery": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "expression": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "id": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.cloudwatch.MetricFilter": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "filtername": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "filterpattern": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.codebuild.ArtifactSettings": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryptionenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.codebuild.CodeBuild": { + "type": "object", + "properties": { + "projects": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.codebuild.Project" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.codebuild.Project": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "artifactsettings": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.codebuild.ArtifactSettings" + }, + "secondaryartifactsettings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.codebuild.ArtifactSettings" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.config.Config": { + "type": "object", + "properties": { + "configurationaggregrator": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.config.ConfigurationAggregrator" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.config.ConfigurationAggregrator": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "sourceallregions": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.documentdb.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "backupretentionperiod": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "enabledlogexports": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "identifier": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "instances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.documentdb.Instance" + } + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "storageencrypted": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.documentdb.DocumentDB": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.documentdb.Cluster" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.documentdb.Instance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.dynamodb.DAXCluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "pointintimerecovery": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "serversideencryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.dynamodb.ServerSideEncryption" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.dynamodb.DynamoDB": { + "type": "object", + "properties": { + "daxclusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.dynamodb.DAXCluster" + } + }, + "tables": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.dynamodb.Table" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.dynamodb.ServerSideEncryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.dynamodb.Table": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "pointintimerecovery": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "serversideencryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.dynamodb.ServerSideEncryption" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.BlockDevice": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encrypted": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.EC2": { + "type": "object", + "properties": { + "instances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.Instance" + } + }, + "launchconfigurations": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.LaunchConfiguration" + } + }, + "launchtemplates": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.LaunchTemplate" + } + }, + "networkacls": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.NetworkACL" + } + }, + "requestedamis": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.RequestedAMI" + } + }, + "securitygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.SecurityGroup" + } + }, + "subnets": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.Subnet" + } + }, + "volumes": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.Volume" + } + }, + "vpcs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.VPC" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.Instance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "ebsblockdevices": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.BlockDevice" + } + }, + "metadataoptions": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.MetadataOptions" + }, + "rootblockdevice": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.BlockDevice" + }, + "securitygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.SecurityGroup" + } + }, + "userdata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.LaunchConfiguration": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "associatepublicip": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "ebsblockdevices": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.BlockDevice" + } + }, + "metadataoptions": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.MetadataOptions" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "rootblockdevice": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.BlockDevice" + }, + "userdata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.LaunchTemplate": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "instance": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.Instance" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.MetadataOptions": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "httpendpoint": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "httptokens": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.NetworkACL": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "isdefaultrule": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.NetworkACLRule" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.NetworkACLRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "action": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "cidrs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "fromport": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "protocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "toport": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.RequestedAMI": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "owners": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.SecurityGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "description": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "egressrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.SecurityGroupRule" + } + }, + "ingressrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.SecurityGroupRule" + } + }, + "isdefault": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "vpcid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.SecurityGroupRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cidrs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "description": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "fromport": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "protocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "toport": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.Subnet": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "mappubliciponlaunch": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.VPC": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "flowlogsenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "id": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "isdefault": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "securitygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.SecurityGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.Volume": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ec2.Encryption" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecr.ECR": { + "type": "object", + "properties": { + "repositories": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecr.Repository" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecr.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecr.ImageScanning": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "scanonpush": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecr.Repository": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecr.Encryption" + }, + "imagescanning": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecr.ImageScanning" + }, + "imagetagsimmutable": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "policies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "settings": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.ClusterSettings" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.ClusterSettings": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "containerinsightsenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.ContainerDefinition": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cpu": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "environment": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.EnvVar" + } + }, + "essential": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "image": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "memory": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "portmappings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.PortMapping" + } + }, + "privileged": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.ECS": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.Cluster" + } + }, + "taskdefinitions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.TaskDefinition" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.EFSVolumeConfiguration": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "transitencryptionenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.EnvVar": { + "type": "object", + "properties": { + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "value": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.PortMapping": { + "type": "object", + "properties": { + "containerport": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "hostport": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.TaskDefinition": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "containerdefinitions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.ContainerDefinition" + } + }, + "volumes": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.Volume" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.Volume": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "efsvolumeconfiguration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ecs.EFSVolumeConfiguration" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.efs.EFS": { + "type": "object", + "properties": { + "filesystems": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.efs.FileSystem" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.efs.FileSystem": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encrypted": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.eks.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.eks.Encryption" + }, + "logging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.eks.Logging" + }, + "publicaccesscidrs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "publicaccessenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.eks.EKS": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.eks.Cluster" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.eks.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "secrets": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.eks.Logging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "api": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "audit": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "authenticator": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "controllermanager": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "scheduler": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticache.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "engine": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "nodetype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "snapshotretentionlimit": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticache.ElastiCache": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticache.Cluster" + } + }, + "replicationgroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticache.ReplicationGroup" + } + }, + "securitygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticache.SecurityGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticache.ReplicationGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "atrestencryptionenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "transitencryptionenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticache.SecurityGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "description": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.AtRestEncryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.Domain": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accesspolicies": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "atrestencryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.AtRestEncryption" + }, + "dedicatedmasterenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "domainname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "endpoint": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.Endpoint" + }, + "logpublishing": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.LogPublishing" + }, + "servicesoftwareoptions": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.ServiceSoftwareOptions" + }, + "transitencryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.TransitEncryption" + }, + "vpcid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.Elasticsearch": { + "type": "object", + "properties": { + "domains": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.Domain" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.Endpoint": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enforcehttps": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "tlspolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.LogPublishing": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "auditenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "cloudwatchloggrouparn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.ServiceSoftwareOptions": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "currentversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "newversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "updateavailable": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "updatestatus": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elasticsearch.TransitEncryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elb.Action": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elb.ELB": { + "type": "object", + "properties": { + "loadbalancers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elb.LoadBalancer" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elb.Listener": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "defaultactions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elb.Action" + } + }, + "protocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "tlspolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.elb.LoadBalancer": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "dropinvalidheaderfields": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "internal": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "listeners": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.elb.Listener" + } + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.emr.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "settings": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.emr.ClusterSettings" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.emr.ClusterSettings": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "releaselabel": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "servicerole": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.emr.EMR": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.emr.Cluster" + } + }, + "securityconfiguration": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.emr.SecurityConfiguration" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.emr.SecurityConfiguration": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "configuration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.AccessKey": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accesskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "active": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "creationdate": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.TimeValue" + }, + "lastaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.TimeValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Document": { + "type": "object", + "properties": { + "endline": { + "type": "integer" + }, + "explicit": { + "type": "boolean" + }, + "filepath": { + "type": "string" + }, + "fskey": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "sourceprefix": { + "type": "string" + }, + "startline": { + "type": "integer" + }, + "value": { + "type": "string" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Group": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "policies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.IAM": { + "type": "object", + "properties": { + "groups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Group" + } + }, + "passwordpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.PasswordPolicy" + }, + "policies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy" + } + }, + "roles": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Role" + } + }, + "servercertificates": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.ServerCertificate" + } + }, + "users": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.User" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.MFADevice": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "isvirtual": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.PasswordPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "maxagedays": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "minimumlength": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "requirelowercase": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "requirenumbers": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "requiresymbols": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "requireuppercase": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "reusepreventioncount": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "builtin": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "document": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Document" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Role": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "policies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.ServerCertificate": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "expiration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.TimeValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.User": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accesskeys": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.AccessKey" + } + }, + "lastaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.TimeValue" + }, + "mfadevices": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.MFADevice" + } + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "policies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.kinesis.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.kinesis.Kinesis": { + "type": "object", + "properties": { + "streams": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.kinesis.Stream" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.kinesis.Stream": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.kinesis.Encryption" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.kms.KMS": { + "type": "object", + "properties": { + "keys": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.kms.Key" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.kms.Key": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "rotationenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "usage": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.lambda.Function": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "permissions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.lambda.Permission" + } + }, + "tracing": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.lambda.Tracing" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.lambda.Lambda": { + "type": "object", + "properties": { + "functions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.lambda.Function" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.lambda.Permission": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "principal": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "sourcearn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.lambda.Tracing": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "mode": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.mq.Broker": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "logging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.mq.Logging" + }, + "publicaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.mq.Logging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "audit": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "general": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.mq.MQ": { + "type": "object", + "properties": { + "brokers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.mq.Broker" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.BrokerLogging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cloudwatch": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.CloudwatchLogging" + }, + "firehose": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.FirehoseLogging" + }, + "s3": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.S3Logging" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.CloudwatchLogging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryptionatrest": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.EncryptionAtRest" + }, + "encryptionintransit": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.EncryptionInTransit" + }, + "logging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.Logging" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.EncryptionAtRest": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyarn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.EncryptionInTransit": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "clientbroker": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.FirehoseLogging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.Logging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "broker": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.BrokerLogging" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.MSK": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.Cluster" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.msk.S3Logging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.neptune.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "logging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.neptune.Logging" + }, + "storageencrypted": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.neptune.Logging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "audit": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.neptune.Neptune": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.neptune.Cluster" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Classic": { + "type": "object", + "properties": { + "dbsecuritygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.DBSecurityGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "availabilityzones": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "backupretentionperioddays": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "deletionprotection": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Encryption" + }, + "engine": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "instances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.ClusterInstance" + } + }, + "latestrestorabletime": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.TimeValue" + }, + "performanceinsights": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.PerformanceInsights" + }, + "publicaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "replicationsourcearn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "skipfinalsnapshot": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.ClusterInstance": { + "type": "object", + "properties": { + "clusteridentifier": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "instance": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Instance" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.DBParameterGroupsList": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "dbparametergroupname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.DBSecurityGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.DBSnapshotAttributes": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "attributevalues": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryptstorage": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Instance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "autominorversionupgrade": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "backupretentionperioddays": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "dbinstancearn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "dbinstanceidentifier": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "dbparametergroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.DBParameterGroupsList" + } + }, + "deletionprotection": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "enabledcloudwatchlogsexports": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Encryption" + }, + "engine": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "engineversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "iamauthenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "latestrestorabletime": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.TimeValue" + }, + "multiaz": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "performanceinsights": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.PerformanceInsights" + }, + "publicaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "publiclyaccessible": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "readreplicadbinstanceidentifiers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "replicationsourcearn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "storageencrypted": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "taglist": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.TagList" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.ParameterGroups": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "dbparametergroupfamily": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "dbparametergroupname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "parameters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Parameters" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Parameters": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "parametername": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "parametervalue": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.PerformanceInsights": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.RDS": { + "type": "object", + "properties": { + "classic": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Classic" + }, + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Cluster" + } + }, + "instances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Instance" + } + }, + "parametergroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.ParameterGroups" + } + }, + "snapshots": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Snapshots" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.Snapshots": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "dbsnapshotarn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "dbsnapshotidentifier": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "encrypted": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "snapshotattributes": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.DBSnapshotAttributes" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.rds.TagList": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "allowversionupgrade": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "automatedsnapshotretentionperiod": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "clusteridentifier": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.Encryption" + }, + "endpoint": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.EndPoint" + }, + "loggingenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "masterusername": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "nodetype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "numberofnodes": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "publiclyaccessible": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "subnetgroupname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "vpcid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.ClusterParameter": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "parametername": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "parametervalue": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.EndPoint": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "port": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.Redshift": { + "type": "object", + "properties": { + "clusterparameters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.ClusterParameter" + } + }, + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.Cluster" + } + }, + "reservednodes": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.ReservedNode" + } + }, + "securitygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.SecurityGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.ReservedNode": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "nodetype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.redshift.SecurityGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "description": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Bucket": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accelerateconfigurationstatus": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "acl": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "bucketlocation": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "bucketpolicies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy" + } + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Encryption" + }, + "grants": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Grant" + } + }, + "lifecycleconfiguration": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Rules" + } + }, + "logging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Logging" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "objects": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Contents" + } + }, + "publicaccessblock": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.PublicAccessBlock" + }, + "versioning": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Versioning" + }, + "website": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Website" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Contents": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "algorithm": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Grant": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "grantee": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Grantee" + }, + "permissions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Grantee": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "uri": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Logging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "targetbucket": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.PublicAccessBlock": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "blockpublicacls": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "blockpublicpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "ignorepublicacls": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "restrictpublicbuckets": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Rules": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "status": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.S3": { + "type": "object", + "properties": { + "buckets": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Bucket" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Versioning": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "mfadelete": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.s3.Website": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.API": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accesslogging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.AccessLogging" + }, + "domainconfiguration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.DomainConfiguration" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "restmethodsettings": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.RESTMethodSettings" + }, + "tracingenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.AccessLogging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cloudwatchloggrouparn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.Application": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "location": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.Location" + }, + "locationpath": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.DomainConfiguration": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "securitypolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.Function": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "functionname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "managedpolicies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "policies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy" + } + }, + "tracing": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.HttpAPI": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accesslogging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.AccessLogging" + }, + "defaultroutesettings": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.RouteSettings" + }, + "domainconfiguration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.DomainConfiguration" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.Location": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "applicationid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "semanticversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.LoggingConfiguration": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "loggingenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.RESTMethodSettings": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cachedataencrypted": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "datatraceenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "loggingenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "metricsenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.RouteSettings": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "datatraceenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "detailedmetricsenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "loggingenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.SAM": { + "type": "object", + "properties": { + "apis": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.API" + } + }, + "applications": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.Application" + } + }, + "functions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.Function" + } + }, + "httpapis": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.HttpAPI" + } + }, + "simpletables": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.SimpleTable" + } + }, + "statemachines": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.StateMachine" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.SSESpecification": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "kmsmasterkeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.SimpleTable": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "ssespecification": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.SSESpecification" + }, + "tablename": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.StateMachine": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "loggingconfiguration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.LoggingConfiguration" + }, + "managedpolicies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "policies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy" + } + }, + "tracing": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.TracingConfiguration" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sam.TracingConfiguration": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sns.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sns.SNS": { + "type": "object", + "properties": { + "topics": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sns.Topic" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sns.Topic": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "arn": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sns.Encryption" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sqs.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "managedencryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sqs.Queue": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sqs.Encryption" + }, + "policies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.iam.Policy" + } + }, + "queueurl": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.sqs.SQS": { + "type": "object", + "properties": { + "queues": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.sqs.Queue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ssm.SSM": { + "type": "object", + "properties": { + "secrets": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.ssm.Secret" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.ssm.Secret": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "kmskeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.workspaces.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.workspaces.Volume": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.workspaces.Encryption" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.workspaces.WorkSpace": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "rootvolume": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.workspaces.Volume" + }, + "uservolume": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.workspaces.Volume" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.aws.workspaces.WorkSpaces": { + "type": "object", + "properties": { + "workspaces": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.aws.workspaces.WorkSpace" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.Azure": { + "type": "object", + "properties": { + "appservice": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.AppService" + }, + "authorization": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.authorization.Authorization" + }, + "compute": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.Compute" + }, + "container": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.Container" + }, + "cosmosdb": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.cosmosdb.CosmosDB" + }, + "database": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.Database" + }, + "datafactory": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.datafactory.DataFactory" + }, + "datalake": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.datalake.DataLake" + }, + "keyvault": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.KeyVault" + }, + "monitor": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.monitor.Monitor" + }, + "network": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.Network" + }, + "securitycenter": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.securitycenter.SecurityCenter" + }, + "storage": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.Storage" + }, + "synapse": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.synapse.Synapse" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.AppService": { + "type": "object", + "properties": { + "functionapps": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.FunctionApp" + } + }, + "services": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.Service" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.Authentication": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.FunctionApp": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "httpsonly": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.Identity": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.Service": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "authentication": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.Authentication" + }, + "enableclientcert": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "httpsonly": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "identity": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.Identity" + }, + "platform": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "site": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.Site" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.appservice.Site": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enablehttp2": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "ftpsstate": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "minimumtlsversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "phpversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "pythonversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.authorization.Authorization": { + "type": "object", + "properties": { + "roleassignments": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.authorization.RoleAssignment" + } + }, + "roledefinitions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.authorization.RoleDefinition" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.authorization.Permission": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "actions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.authorization.RoleAssignment": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "principalid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "principaltype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "roledefinitionid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "roledefinitionname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.authorization.RoleDefinition": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "assignablescopes": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "permissions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.authorization.Permission" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.Compute": { + "type": "object", + "properties": { + "linuxvirtualmachines": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.LinuxVirtualMachine" + } + }, + "manageddisks": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.ManagedDisk" + } + }, + "windowsvirtualmachines": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.WindowsVirtualMachine" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.Encryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.LinuxVirtualMachine": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "osprofilelinuxconfig": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.OSProfileLinuxConfig" + }, + "virtualmachine": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.VirtualMachine" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.ManagedDisk": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.Encryption" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.OSProfileLinuxConfig": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "disablepasswordauthentication": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.VirtualMachine": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "customdata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "networkinterfaces": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.NetworkInterface" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.WindowsVirtualMachine": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "virtualmachine": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.compute.VirtualMachine" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.AddonProfile": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "azurepolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.AzurePolicy" + }, + "omsagent": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.OMSAgent" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.AgentPool": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "diskencryptionsetid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "nodetype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.AzurePolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.Container": { + "type": "object", + "properties": { + "kubernetesclusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.KubernetesCluster" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.KubernetesCluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "addonprofile": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.AddonProfile" + }, + "agentpools": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.AgentPool" + } + }, + "apiserverauthorizedipranges": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "diskencryptionsetid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "enableprivatecluster": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "networkprofile": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.NetworkProfile" + }, + "rolebasedaccesscontrol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.RoleBasedAccessControl" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.NetworkProfile": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "networkpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.OMSAgent": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.container.RoleBasedAccessControl": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.cosmosdb.Account": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "iprangefilter": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.cosmosdb.CosmosDB": { + "type": "object", + "properties": { + "accounts": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.cosmosdb.Account" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.ActiveDirectoryAdministrator": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "login": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.Database": { + "type": "object", + "properties": { + "mariadbservers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.MariaDBServer" + } + }, + "mssqlservers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.MSSQLServer" + } + }, + "mysqlservers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.MySQLServer" + } + }, + "postgresqlservers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.PostgreSQLServer" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.ExtendedAuditingPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "retentionindays": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.FirewallRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "endip": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "startip": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.MSSQLServer": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "activedirectoryadministrators": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.ActiveDirectoryAdministrator" + } + }, + "administratorlogin": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "extendedauditingpolicies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.ExtendedAuditingPolicy" + } + }, + "securityalertpolicies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.SecurityAlertPolicy" + } + }, + "server": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.Server" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.MariaDBServer": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "server": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.Server" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.MySQLServer": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "server": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.Server" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.PostgreSQLServer": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "config": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.PostgresSQLConfig" + }, + "georedundantbackupenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "server": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.Server" + }, + "threatdetectionpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.ThreatDetectionPolicy" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.PostgresSQLConfig": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "connectionthrottling": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "logcheckpoints": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "logconnections": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "logdisconnections": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.SecurityAlertPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "disabledalerts": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "emailaccountadmins": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "emailaddresses": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.Server": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enablepublicnetworkaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "enablesslenforcement": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "firewallrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.FirewallRule" + } + }, + "minimumtlsversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.database.ThreatDetectionPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.datafactory.DataFactory": { + "type": "object", + "properties": { + "datafactories": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.datafactory.Factory" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.datafactory.Factory": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enablepublicnetwork": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.datalake.DataLake": { + "type": "object", + "properties": { + "stores": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.datalake.Store" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.datalake.Store": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enableencryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.Key": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "expirydate": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.TimeValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.KeyVault": { + "type": "object", + "properties": { + "vaults": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.Vault" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.NetworkACLs": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "defaultaction": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.Secret": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "contenttype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "expirydate": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.TimeValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.Vault": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enablepurgeprotection": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "keys": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.Key" + } + }, + "networkacls": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.NetworkACLs" + }, + "secrets": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.keyvault.Secret" + } + }, + "softdeleteretentiondays": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.monitor.LogProfile": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "categories": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "locations": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "retentionpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.monitor.RetentionPolicy" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.monitor.Monitor": { + "type": "object", + "properties": { + "logprofiles": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.monitor.LogProfile" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.monitor.RetentionPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "days": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.IPConfiguration": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "haspublicip": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "primary": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "publicipaddress": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "subnetid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.Network": { + "type": "object", + "properties": { + "networkinterfaces": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.NetworkInterface" + } + }, + "networkwatcherflowlogs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.NetworkWatcherFlowLog" + } + }, + "securitygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.SecurityGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.NetworkInterface": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enableipforwarding": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "haspublicip": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "ipconfigurations": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.IPConfiguration" + } + }, + "publicipaddress": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "securitygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.SecurityGroup" + } + }, + "subnetid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.NetworkWatcherFlowLog": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "retentionpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.RetentionPolicy" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.RetentionPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "days": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.SecurityGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.SecurityGroupRule" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.network.SecurityGroupRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "allow": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "destinationaddresses": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "destinationports": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.adapters.common.PortRange" + } + }, + "outbound": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "protocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "sourceaddresses": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "sourceports": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.adapters.common.PortRange" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.securitycenter.Contact": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "email": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "enablealertnotifications": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "enablealertstoadmins": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "isenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "minimalseverity": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "phone": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.securitycenter.SecurityCenter": { + "type": "object", + "properties": { + "contacts": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.securitycenter.Contact" + } + }, + "subscriptions": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.securitycenter.SubscriptionPricing" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.securitycenter.SubscriptionPricing": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "tier": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.Account": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accountreplicationtype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "blobproperties": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.BlobProperties" + }, + "containers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.Container" + } + }, + "customermanagedkey": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.CustomerManagedKey" + }, + "enforcehttps": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "infrastructureencryptionenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "minimumtlsversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "networkrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.NetworkRule" + } + }, + "publicnetworkaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "queueproperties": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.QueueProperties" + }, + "queues": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.Queue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.BlobProperties": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "deleteretentionpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.DeleteRetentionPolicy" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.Container": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "publicaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.CustomerManagedKey": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "keyvaultkeyid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "userassignedidentityid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.DeleteRetentionPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "days": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.NetworkRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "allowbydefault": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "bypass": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.Queue": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.QueueLogging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "delete": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "read": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "retentionpolicydays": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "version": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "write": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.QueueProperties": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enablelogging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "logging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.QueueLogging" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.Storage": { + "type": "object", + "properties": { + "accounts": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.storage.Account" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.synapse.Synapse": { + "type": "object", + "properties": { + "workspaces": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.azure.synapse.Workspace" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.azure.synapse.Workspace": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enablemanagedvirtualnetwork": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.cloudstack.CloudStack": { + "type": "object", + "properties": { + "compute": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.cloudstack.compute.Compute" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.cloudstack.compute.Compute": { + "type": "object", + "properties": { + "instances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.cloudstack.compute.Instance" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.cloudstack.compute.Instance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "userdata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.DigitalOcean": { + "type": "object", + "properties": { + "compute": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.Compute" + }, + "spaces": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.spaces.Spaces" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.Compute": { + "type": "object", + "properties": { + "droplets": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.Droplet" + } + }, + "firewalls": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.Firewall" + } + }, + "kubernetesclusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.KubernetesCluster" + } + }, + "loadbalancers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.LoadBalancer" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.Droplet": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "sshkeys": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.Firewall": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "inboundrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.InboundFirewallRule" + } + }, + "outboundrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.OutboundFirewallRule" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.ForwardingRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "entryprotocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.InboundFirewallRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "sourceaddresses": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.KubernetesCluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "autoupgrade": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "surgeupgrade": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.LoadBalancer": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "forwardingrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.ForwardingRule" + } + }, + "redirecthttptohttps": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.compute.OutboundFirewallRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "destinationaddresses": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.spaces.Bucket": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "acl": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "forcedestroy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "objects": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.spaces.Object" + } + }, + "versioning": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.spaces.Versioning" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.spaces.Object": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "acl": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.spaces.Spaces": { + "type": "object", + "properties": { + "buckets": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.spaces.Bucket" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.digitalocean.spaces.Versioning": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.github.BranchProtection": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "requiresignedcommits": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.github.EnvironmentSecret": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryptedvalue": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "environment": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "plaintextvalue": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "repository": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "secretname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.github.GitHub": { + "type": "object", + "properties": { + "branchprotections": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.github.BranchProtection" + } + }, + "environmentsecrets": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.github.EnvironmentSecret" + } + }, + "repositories": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.github.Repository" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.github.Repository": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "archived": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "public": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "vulnerabilityalerts": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.Google": { + "type": "object", + "properties": { + "bigquery": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.bigquery.BigQuery" + }, + "compute": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Compute" + }, + "dns": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.dns.DNS" + }, + "gke": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.GKE" + }, + "iam": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.IAM" + }, + "kms": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.kms.KMS" + }, + "sql": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.SQL" + }, + "storage": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.Storage" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.bigquery.AccessGrant": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "domain": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "role": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "specialgroup": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.bigquery.BigQuery": { + "type": "object", + "properties": { + "datasets": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.bigquery.Dataset" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.bigquery.Dataset": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "accessgrants": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.bigquery.AccessGrant" + } + }, + "id": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Compute": { + "type": "object", + "properties": { + "disks": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Disk" + } + }, + "instances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Instance" + } + }, + "networks": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Network" + } + }, + "projectmetadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.ProjectMetadata" + }, + "sslpolicies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.SSLPolicy" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Disk": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.DiskEncryption" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.DiskEncryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "kmskeylink": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "rawkey": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BytesValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.EgressRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "destinationranges": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "firewallrule": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.FirewallRule" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Firewall": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "egressrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.EgressRule" + } + }, + "ingressrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.IngressRule" + } + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "sourcetags": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "targettags": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.FirewallRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enforced": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "isallow": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.adapters.common.PortRange" + } + }, + "protocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.IngressRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "firewallrule": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.FirewallRule" + }, + "sourceranges": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Instance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "attacheddisks": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Disk" + } + }, + "bootdisks": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Disk" + } + }, + "canipforward": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "enableprojectsshkeyblocking": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "enableserialport": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "networkinterfaces": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.NetworkInterface" + } + }, + "osloginenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "serviceaccount": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.ServiceAccount" + }, + "shieldedvm": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.ShieldedVMConfig" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Network": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "firewall": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Firewall" + }, + "subnetworks": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.SubNetwork" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.NetworkInterface": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "haspublicip": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "natip": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "network": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.Network" + }, + "subnetwork": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.SubNetwork" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.ProjectMetadata": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enableoslogin": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.SSLPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "minimumtlsversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "profile": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.ServiceAccount": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "email": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "isdefault": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "scopes": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.ShieldedVMConfig": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "integritymonitoringenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "securebootenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "vtpmenabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.compute.SubNetwork": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enableflowlogs": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "privateipgoogleaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "purpose": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.dns.DNS": { + "type": "object", + "properties": { + "managedzones": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.dns.ManagedZone" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.dns.DNSSec": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "defaultkeyspecs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.dns.KeySpecs" + } + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.dns.KeySpecs": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "algorithm": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "keytype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.dns.ManagedZone": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "dnssec": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.dns.DNSSec" + }, + "visibility": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.AutoProvisioningDefaults": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "imagetype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "management": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.Management" + }, + "serviceaccount": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.AutoScaling": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "autoprovisioningdefaults": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.AutoProvisioningDefaults" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.ClientCertificate": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "issuecertificate": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.Cluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "autoscaling": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.AutoScaling" + }, + "datapathprovider": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "enableautpilot": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "enablelegacyabac": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "enableshieldednodes": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "ipallocationpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.IPAllocationPolicy" + }, + "loggingservice": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "masterauth": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.MasterAuth" + }, + "masterauthorizednetworks": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.MasterAuthorizedNetworks" + }, + "monitoringservice": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "networkpolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.NetworkPolicy" + }, + "nodeconfig": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.NodeConfig" + }, + "nodepools": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.NodePool" + } + }, + "privatecluster": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.PrivateCluster" + }, + "removedefaultnodepool": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "resourcelabels": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.MapValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.GKE": { + "type": "object", + "properties": { + "clusters": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.Cluster" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.IPAllocationPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.Management": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enableautorepair": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "enableautoupgrade": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.MasterAuth": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "clientcertificate": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.ClientCertificate" + }, + "password": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "username": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.MasterAuthorizedNetworks": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cidrs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.NetworkPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.NodeConfig": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enablelegacyendpoints": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "imagetype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "serviceaccount": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "workloadmetadataconfig": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.WorkloadMetadataConfig" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.NodePool": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "management": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.Management" + }, + "nodeconfig": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.NodeConfig" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.PrivateCluster": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enableprivatenodes": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.gke.WorkloadMetadataConfig": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "nodemetadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.AuditConfig": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "auditlogconfigs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.AuditLogConfig" + } + }, + "service": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.AuditLogConfig": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "exemptedmembers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "logtype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Binding": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "includesdefaultserviceaccount": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "members": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "role": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Folder": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "auditconfigs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.AuditConfig" + } + }, + "bindings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Binding" + } + }, + "members": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Member" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.IAM": { + "type": "object", + "properties": { + "folders": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Folder" + } + }, + "organizations": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Organization" + } + }, + "projects": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Project" + } + }, + "workloadidentitypoolproviders": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.WorkloadIdentityPoolProvider" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Member": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "defaultserviceaccount": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "member": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "role": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Organization": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "auditconfigs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.AuditConfig" + } + }, + "bindings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Binding" + } + }, + "members": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Member" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Project": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "auditconfigs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.AuditConfig" + } + }, + "autocreatenetwork": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "bindings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Binding" + } + }, + "members": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Member" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.WorkloadIdentityPoolProvider": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "attributecondition": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "workloadidentitypoolid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "workloadidentitypoolproviderid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.kms.KMS": { + "type": "object", + "properties": { + "keyrings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.kms.KeyRing" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.kms.Key": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "rotationperiodseconds": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.kms.KeyRing": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "keys": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.kms.Key" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.Backups": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.DatabaseInstance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "databaseversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "isreplica": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "settings": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.Settings" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.Flags": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "containeddatabaseauthentication": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "crossdbownershipchaining": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "localinfile": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "logcheckpoints": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "logconnections": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "logdisconnections": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "loglockwaits": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "logmindurationstatement": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "logminmessages": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "logtempfilesize": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.IPConfiguration": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "authorizednetworks": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.IPConfiguration.AuthorizedNetworks" + } + }, + "enableipv4": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "requiretls": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "sslmode": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.IPConfiguration.AuthorizedNetworks": { + "type": "object", + "properties": { + "cidr": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.SQL": { + "type": "object", + "properties": { + "instances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.DatabaseInstance" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.Settings": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "backups": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.Backups" + }, + "flags": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.Flags" + }, + "ipconfiguration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.sql.IPConfiguration" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.Bucket": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "bindings": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Binding" + } + }, + "enableuniformbucketlevelaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "encryption": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.BucketEncryption" + }, + "location": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "logging": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.BucketLogging" + }, + "members": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.iam.Member" + } + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "versioning": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.BucketVersioning" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.BucketEncryption": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "defaultkmskeyname": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.BucketLogging": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "logbucket": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "logobjectprefix": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.BucketVersioning": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.Storage": { + "type": "object", + "properties": { + "buckets": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.google.storage.Bucket" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.Egress": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "destinationcidrs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.Port" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.Ingress": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "ports": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.Port" + } + }, + "sourcecidrs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.Kubernetes": { + "type": "object", + "properties": { + "networkpolicies": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.NetworkPolicy" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.NetworkPolicy": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "spec": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.NetworkPolicySpec" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.NetworkPolicySpec": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "egress": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.Egress" + }, + "ingress": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.Ingress" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.kubernetes.Port": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "number": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "protocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.Nifcloud": { + "type": "object", + "properties": { + "computing": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.Computing" + }, + "dns": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.dns.DNS" + }, + "nas": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.nas.NAS" + }, + "network": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.Network" + }, + "rdb": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.rdb.RDB" + }, + "sslcertificate": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.sslcertificate.SSLCertificate" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.Computing": { + "type": "object", + "properties": { + "instances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.Instance" + } + }, + "securitygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.SecurityGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.Instance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "networkinterfaces": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.NetworkInterface" + } + }, + "securitygroup": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.NetworkInterface": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "networkid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.SecurityGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "description": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "egressrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.SecurityGroupRule" + } + }, + "ingressrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.SecurityGroupRule" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.computing.SecurityGroupRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cidr": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "description": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.dns.DNS": { + "type": "object", + "properties": { + "records": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.dns.Record" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.dns.Record": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "record": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "type": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.nas.NAS": { + "type": "object", + "properties": { + "nasinstances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.nas.NASInstance" + } + }, + "nassecuritygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.nas.NASSecurityGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.nas.NASInstance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "networkid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.nas.NASSecurityGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cidrs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "description": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.ElasticLoadBalancer": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "listeners": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.ElasticLoadBalancerListener" + } + }, + "networkinterfaces": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.NetworkInterface" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.ElasticLoadBalancerListener": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "protocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.LoadBalancer": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "listeners": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.LoadBalancerListener" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.LoadBalancerListener": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "protocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "tlspolicy": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.Network": { + "type": "object", + "properties": { + "elasticloadbalancers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.ElasticLoadBalancer" + } + }, + "loadbalancers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.LoadBalancer" + } + }, + "routers": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.Router" + } + }, + "vpngateways": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.VpnGateway" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.NetworkInterface": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "isvipnetwork": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "networkid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.Router": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "networkinterfaces": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.NetworkInterface" + } + }, + "securitygroup": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.network.VpnGateway": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "securitygroup": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.rdb.DBInstance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "backupretentionperioddays": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "engine": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "engineversion": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "networkid": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "publicaccess": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.rdb.DBSecurityGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cidrs": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + }, + "description": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.rdb.RDB": { + "type": "object", + "properties": { + "dbinstances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.rdb.DBInstance" + } + }, + "dbsecuritygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.rdb.DBSecurityGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.sslcertificate.SSLCertificate": { + "type": "object", + "properties": { + "servercertificates": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.sslcertificate.ServerCertificate" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.nifcloud.sslcertificate.ServerCertificate": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "expiration": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.TimeValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.openstack.Compute": { + "type": "object", + "properties": { + "firewall": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.openstack.Firewall" + }, + "instances": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.openstack.Instance" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.openstack.Firewall": { + "type": "object", + "properties": { + "allowrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.openstack.FirewallRule" + } + }, + "denyrules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.openstack.FirewallRule" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.openstack.FirewallRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "destination": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "destinationport": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "enabled": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "source": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "sourceport": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.openstack.Instance": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "adminpassword": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.openstack.Networking": { + "type": "object", + "properties": { + "securitygroups": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.openstack.SecurityGroup" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.openstack.OpenStack": { + "type": "object", + "properties": { + "compute": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.openstack.Compute" + }, + "networking": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.openstack.Networking" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.openstack.SecurityGroup": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "description": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "name": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.openstack.SecurityGroupRule" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.openstack.SecurityGroupRule": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "cidr": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + }, + "ethertype": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "isingress": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.BoolValue" + }, + "portmax": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "portmin": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.IntValue" + }, + "protocol": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.oracle.AddressReservation": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.Metadata" + }, + "pool": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.types.StringValue" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.oracle.Compute": { + "type": "object", + "properties": { + "addressreservations": { + "type": "array", + "items": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.oracle.AddressReservation" + } + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.providers.oracle.Oracle": { + "type": "object", + "properties": { + "compute": { + "type": "object", + "$ref": "#/definitions/github.com.aquasecurity.trivy.pkg.iac.providers.oracle.Compute" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.types.BoolValue": { + "type": "object", + "properties": { + "endline": { + "type": "integer" + }, + "explicit": { + "type": "boolean" + }, + "filepath": { + "type": "string" + }, + "fskey": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "sourceprefix": { + "type": "string" + }, + "startline": { + "type": "integer" + }, + "unresolvable": { + "type": "boolean" + }, + "value": { + "type": "boolean" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.types.BytesValue": { + "type": "object", + "properties": { + "endline": { + "type": "integer" + }, + "explicit": { + "type": "boolean" + }, + "filepath": { + "type": "string" + }, + "fskey": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "sourceprefix": { + "type": "string" + }, + "startline": { + "type": "integer" + }, + "unresolvable": { + "type": "boolean" + }, + "value": { + "type": "string" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.types.IntValue": { + "type": "object", + "properties": { + "endline": { + "type": "integer" + }, + "explicit": { + "type": "boolean" + }, + "filepath": { + "type": "string" + }, + "fskey": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "sourceprefix": { + "type": "string" + }, + "startline": { + "type": "integer" + }, + "unresolvable": { + "type": "boolean" + }, + "value": { + "type": "integer" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.types.MapValue": { + "type": "object", + "properties": { + "endline": { + "type": "integer" + }, + "explicit": { + "type": "boolean" + }, + "filepath": { + "type": "string" + }, + "fskey": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "sourceprefix": { + "type": "string" + }, + "startline": { + "type": "integer" + }, + "unresolvable": { + "type": "boolean" + }, + "value": { + "type": "object" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.types.Metadata": { + "type": "object", + "properties": { + "endline": { + "type": "integer" + }, + "explicit": { + "type": "boolean" + }, + "filepath": { + "type": "string" + }, + "fskey": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "sourceprefix": { + "type": "string" + }, + "startline": { + "type": "integer" + }, + "unresolvable": { + "type": "boolean" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.types.StringValue": { + "type": "object", + "properties": { + "endline": { + "type": "integer" + }, + "explicit": { + "type": "boolean" + }, + "filepath": { + "type": "string" + }, + "fskey": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "sourceprefix": { + "type": "string" + }, + "startline": { + "type": "integer" + }, + "unresolvable": { + "type": "boolean" + }, + "value": { + "type": "string" + } + } + }, + "github.com.aquasecurity.trivy.pkg.iac.types.TimeValue": { + "type": "object", + "properties": { + "endline": { + "type": "integer" + }, + "explicit": { + "type": "boolean" + }, + "filepath": { + "type": "string" + }, + "fskey": { + "type": "string" + }, + "managed": { + "type": "boolean" + }, + "resource": { + "type": "string" + }, + "sourceprefix": { + "type": "string" + }, + "startline": { + "type": "integer" + }, + "unresolvable": { + "type": "boolean" + }, + "value": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/pkg/iac/rego/schemas/dockerfile.json b/pkg/iac/rego/schemas/dockerfile.json new file mode 100644 index 000000000000..0805b47f702d --- /dev/null +++ b/pkg/iac/rego/schemas/dockerfile.json @@ -0,0 +1,70 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/dockerfile.json", + "type": "object", + "properties": { + "Stages": { + "type": "array", + "items": { + "$ref": "#/$defs/stage" + } + } + }, + "$defs": { + "stage": { + "type": "object", + "properties": { + "Name": { + "type": "string" + }, + "Commands": { + "type": "array", + "items": { + "$ref": "#/$defs/command" + } + } + } + }, + "command": { + "type": "object", + "properties": { + "Flags": { + "type": "array", + "items": { + "type": "string" + } + }, + "Value": { + "type": "array", + "items": { + "type": "string" + } + }, + "Cmd": { + "type": "string" + }, + "SubCmd": { + "type": "string" + }, + "Original": { + "type": "string" + }, + "Path": { + "type": "string" + }, + "JSON": { + "type": "boolean" + }, + "Stage": { + "type": "integer" + }, + "StartLine": { + "type": "integer" + }, + "EndLine": { + "type": "integer" + } + } + } + } +} \ No newline at end of file diff --git a/pkg/iac/rego/schemas/kubernetes.json b/pkg/iac/rego/schemas/kubernetes.json new file mode 100644 index 000000000000..27de806d8bb5 --- /dev/null +++ b/pkg/iac/rego/schemas/kubernetes.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/kubernetes.json", + "type": "object", + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "spec": { + "type": "object" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "apiGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "resourceNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "verbs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/pkg/iac/rego/schemas/rbac.json b/pkg/iac/rego/schemas/rbac.json new file mode 100644 index 000000000000..0bbc356297af --- /dev/null +++ b/pkg/iac/rego/schemas/rbac.json @@ -0,0 +1,51 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/rbac.json", + "type": "object", + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "spec": { + "type": "object" + }, + "rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "apiGroups": { + "type": "array", + "items": { + "type": "string" + } + }, + "resources": { + "type": "array", + "items": { + "type": "string" + } + }, + "resourceNames": { + "type": "array", + "items": { + "type": "string" + } + }, + "verbs": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/pkg/iac/rego/schemas/schemas.go b/pkg/iac/rego/schemas/schemas.go new file mode 100644 index 000000000000..1b3fbbd24ac2 --- /dev/null +++ b/pkg/iac/rego/schemas/schemas.go @@ -0,0 +1,17 @@ +package schemas + +import ( + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +var SchemaMap = map[types.Source]Schema{ + types.SourceDefsec: Cloud, + types.SourceCloud: Cloud, + types.SourceKubernetes: Kubernetes, + types.SourceRbac: Kubernetes, + types.SourceDockerfile: Dockerfile, + types.SourceTOML: Anything, + types.SourceYAML: Anything, + types.SourceJSON: Anything, + types.SourceTerraformRaw: TerraformRaw, +} diff --git a/pkg/iac/rego/schemas/terraform-raw.json b/pkg/iac/rego/schemas/terraform-raw.json new file mode 100644 index 000000000000..1577f3bf3ed9 --- /dev/null +++ b/pkg/iac/rego/schemas/terraform-raw.json @@ -0,0 +1,137 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/aquasecurity/trivy/blob/main/pkg/iac/rego/schemas/terraform.json", + "type": "object", + "properties": { + "modules": { + "type": "array", + "items": { + "$ref": "#/$defs/Module" + }, + "description": "List of Terraform modules present in the configuration." + } + }, + "$defs": { + "Module": { + "type": "object", + "properties": { + "root_path": { + "type": "string", + "description": "The Terraform root directory of the project." + }, + "module_path": { + "type": "string", + "description": "Path to the current module. For remote modules, this is the path relative to the module's code directory." + }, + "blocks": { + "type": "array", + "items": { + "$ref": "#/$defs/Block" + }, + "description": "List of blocks (e.g., resource, data, variable) within the module." + } + } + }, + "Block": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/$defs/Metadata", + "description": "Metadata related to the block." + }, + "kind": { + "type": "string", + "description": "Kind of the block (e.g., resource, data, module)." + }, + "type": { + "type": "string", + "description": "Type of the block (e.g., aws_s3_bucket for a resource). For blocks that can only have 1 label (such as module or variable) this attribute will be empty." + }, + "name": { + "type": "string", + "description": "Name of the block defined by the user." + }, + "attributes": { + "type": "object", + "patternProperties": { + ".*": { + "$ref": "#/$defs/Attribute" + } + }, + "description": "Key-value attributes associated with the block." + } + } + }, + "Attribute": { + "type": "object", + "properties": { + "__defsec_metadata": { + "type": "object", + "$ref": "#/$defs/Metadata", + "description": "Metadata related to the attribute." + }, + "name": { + "type": "string", + "description": "Name of the attribute." + }, + "known": { + "type": "boolean", + "description": "Indicates whether the value of the attribute is known during analysis." + }, + "value": { + "description": "The actual value of the attribute. If unknown, then null. Can be a primitive, object, or array.", + "oneOf": [ + { "type": "null" }, + { "type": "string" }, + { "type": "number" }, + { "type": "boolean" }, + { "type": "object" }, + { "type": "array" } + ] + } + } + }, + "Metadata": { + "type": "object", + "properties": { + "filepath": { + "type": "string", + "description": "Path to the source file where the object is defined relative to the module's file system." + }, + "startline": { + "type": "number", + "description": "Line number where the object starts in the source file." + }, + "endline": { + "type": "number", + "description": "Line number where the object ends in the source file." + }, + "sourceprefix": { + "type": "string", + "description": "Module source. E.g. interface terraform-aws-modules/s3-bucket/aws" + }, + "managed": { + "type": "boolean", + "description": "Indicates whether the object is controlled by this source. Not relevant for Terraform." + }, + "explicit": { + "type": "boolean", + "description": "True if the object is explicitly defined by the user." + }, + "unresolvable": { + "type": "boolean", + "description": "True if the value cannot be determined statically." + }, + "fskey": { + "type": "string", + "description": "Internal filesystem key for uniquely identifying the object." + }, + "resource": { + "type": "string", + "description": "Fully qualified resource name if applicable. E.g. aws_s3_bucket.test[0]" + } + } + } + } +} diff --git a/pkg/iac/rego/store.go b/pkg/iac/rego/store.go new file mode 100644 index 000000000000..0e324705ffb8 --- /dev/null +++ b/pkg/iac/rego/store.go @@ -0,0 +1,59 @@ +package rego + +import ( + "fmt" + "io/fs" + "path" + + "github.com/open-policy-agent/opa/v1/loader" + "github.com/open-policy-agent/opa/v1/storage" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +var dataFileExtensions = set.NewCaseInsensitive(".yaml", ".yml", ".json") + +// initialize a store populated with OPA data files found in dataPaths +func initStore(dataFS fs.FS, dataPaths, namespaces []string) (storage.Store, error) { + dataFiles := set.New[string]() + + // The virtual file system uses a slash ('/') as a path separator, + // but OPA uses the filepath package, which is OS-dependent. + // Therefore, we need to collect all the paths ourselves and pass them to OPA. + for _, root := range dataPaths { + if err := fs.WalkDir(dataFS, root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + + if isDataFile(path) { + dataFiles.Append(path) + } + return nil + }); err != nil { + log.Error("Failed to collect data file paths", log.String("root", root), log.Err(err)) + } + } + + documents, err := loader.NewFileLoader().WithFS(dataFS).All(dataFiles.Items()) + if err != nil { + return nil, fmt.Errorf("load documents: %w", err) + } + + // pass all namespaces so that rego rule can refer to namespaces as data.namespaces + documents.Documents["namespaces"] = namespaces + + store, err := documents.Store() + if err != nil { + return nil, fmt.Errorf("get documents store: %w", err) + } + return store, nil +} + +func isDataFile(filePath string) bool { + return dataFileExtensions.Contains(path.Ext(filePath)) +} diff --git a/pkg/iac/rego/store_test.go b/pkg/iac/rego/store_test.go new file mode 100644 index 000000000000..d78432419630 --- /dev/null +++ b/pkg/iac/rego/store_test.go @@ -0,0 +1,36 @@ +package rego + +import ( + "encoding/json" + "testing" + "testing/fstest" + + "github.com/open-policy-agent/opa/v1/storage" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInitStore(t *testing.T) { + fsys := fstest.MapFS{ + "test1.yml": &fstest.MapFile{Data: []byte("foo: 1")}, + "test2.yaml": &fstest.MapFile{Data: []byte("bar: 2")}, + "test3.json": &fstest.MapFile{Data: []byte(`{"baz": 3}`)}, + "dir/test4.yaml": &fstest.MapFile{Data: []byte("qux: 4")}, + } + store, err := initStore(fsys, []string{"."}, []string{"builtin.aws.test", "user.test"}) + require.NoError(t, err) + + tx, err := store.NewTransaction(t.Context()) + require.NoError(t, err) + doc, err := store.Read(t.Context(), tx, storage.MustParsePath("/")) + require.NoError(t, err) + + expected := map[string]any{ + "foo": json.Number("1"), + "bar": json.Number("2"), + "baz": json.Number("3"), + "qux": json.Number("4"), + "namespaces": []any{"builtin.aws.test", "user.test"}, + } + assert.Equal(t, expected, doc) +} diff --git a/pkg/iac/rego/testdata/embedded/bad-check.rego b/pkg/iac/rego/testdata/embedded/bad-check.rego new file mode 100644 index 000000000000..f30348989af4 --- /dev/null +++ b/pkg/iac/rego/testdata/embedded/bad-check.rego @@ -0,0 +1,9 @@ +# METADATA +# schemas: +# - input: schema["fooschema"] + +package builtin.bad.test + +deny { + input.evil == "foo bar" +} \ No newline at end of file diff --git a/pkg/iac/rego/testdata/embedded/my-check1.rego b/pkg/iac/rego/testdata/embedded/my-check1.rego new file mode 100644 index 000000000000..2bb194925fbb --- /dev/null +++ b/pkg/iac/rego/testdata/embedded/my-check1.rego @@ -0,0 +1,11 @@ +# METADATA +# schemas: +# - input: schema["fooschema"] +# custom: +# id: test-001 +# avd_id: avd-test-001 +package builtin.test + +deny { + input.foo == "foo bar" +} \ No newline at end of file diff --git a/pkg/iac/rego/testdata/policies/._sysfile.rego b/pkg/iac/rego/testdata/policies/._sysfile.rego new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/iac/rego/testdata/policies/invalid.rego b/pkg/iac/rego/testdata/policies/invalid.rego new file mode 100644 index 000000000000..6a4afdc0fcd0 --- /dev/null +++ b/pkg/iac/rego/testdata/policies/invalid.rego @@ -0,0 +1,8 @@ +# METADATA +# schemas: +# - input: schema["dockerfile"] +package defsec.test_invalid + +deny { + input.Stages[0].Commands[0].FooBarNothingBurger == "lol" +} diff --git a/pkg/iac/rego/testdata/policies/valid.rego b/pkg/iac/rego/testdata/policies/valid.rego new file mode 100644 index 000000000000..9af18e24b0a3 --- /dev/null +++ b/pkg/iac/rego/testdata/policies/valid.rego @@ -0,0 +1,8 @@ +# METADATA +# schemas: +# - input: schema["dockerfile"] +package defsec.test_valid + +deny { + input.Stages[0].Commands[0].Cmd == "lol" +} diff --git a/pkg/iac/rules/providers.go b/pkg/iac/rules/providers.go new file mode 100644 index 000000000000..08dca2903734 --- /dev/null +++ b/pkg/iac/rules/providers.go @@ -0,0 +1,169 @@ +package rules + +import ( + "encoding/json" + "strings" +) + +type Provider struct { + Name string `json:"name"` + Services []Service `json:"services"` +} + +type Service struct { + Name string `json:"name"` + Checks []Check `json:"checks"` +} + +type Check struct { + Name string `json:"name"` + Description string `json:"description"` +} + +func GetProvidersHierarchy() (providers map[string]map[string][]string) { + + registeredRules := GetRegistered() + + provs := make(map[string]map[string][]string) + + for _, rule := range registeredRules { + + cNames := make(map[string]bool) + pName := strings.ToLower(rule.GetRule().Provider.DisplayName()) + sName := strings.ToLower(rule.GetRule().Service) + cName := rule.GetRule().ID + + if _, ok := provs[pName]; !ok { + provs[pName] = make(map[string][]string) + } + + if _, ok := provs[pName][sName]; !ok { + provs[pName][sName] = make([]string, 0) + } + + if _, ok := cNames[cName]; !ok { + cNames[cName] = true + provs[pName][sName] = append(provs[pName][sName], cName) + } + } + + return provs +} + +func GetProviders() (providers []Provider) { + + registeredRules := GetRegistered() + + provs := make(map[string]map[string][]Check) + + for _, rule := range registeredRules { + + pName := strings.ToLower(rule.GetRule().Provider.DisplayName()) + sName := strings.ToLower(rule.GetRule().Service) + cName := rule.GetRule().ID + desc := rule.GetRule().Summary + + if _, ok := provs[pName]; !ok { + provs[pName] = make(map[string][]Check) + } + + if _, ok := provs[pName][sName]; !ok { + provs[pName][sName] = []Check{} + } + + provs[pName][sName] = append(provs[pName][sName], Check{ + Name: cName, + Description: desc, + }) + } + + for providerName, providerServices := range provs { + var services []Service + for serviceName, checks := range providerServices { + services = append(services, Service{ + Name: serviceName, + Checks: checks, + }) + } + + providers = append(providers, Provider{ + Name: providerName, + Services: services, + }) + } + + return providers +} + +func GetProvidersAsJson() ([]byte, error) { + + providers := GetProviders() + + return json.MarshalIndent(providers, "", " ") +} + +func GetProviderNames() []string { + + registeredRules := GetRegistered() + + providers := make(map[string]bool) + + for _, rule := range registeredRules { + + if _, ok := providers[rule.GetRule().Provider.DisplayName()]; !ok { + providers[rule.GetRule().Provider.DisplayName()] = true + } + + } + + var uniqueProviders []string + for p := range providers { + uniqueProviders = append(uniqueProviders, p) + } + + return uniqueProviders + +} + +func GetProviderServiceNames(providerName string) []string { + + registeredRules := GetRegistered() + + services := make(map[string]bool) + + for _, rule := range registeredRules { + + if !strings.EqualFold(providerName, rule.GetRule().Provider.DisplayName()) { + continue + } + + if _, ok := services[rule.GetRule().Service]; !ok { + services[rule.GetRule().Service] = true + } + + } + var uniqueServices []string + for p := range services { + uniqueServices = append(uniqueServices, p) + } + + return uniqueServices +} + +func GetProviderServiceCheckNames(providerName, serviceName string) []string { + + registeredRules := GetRegistered() + + var checks []string + + for _, rule := range registeredRules { + + if !strings.EqualFold(providerName, rule.GetRule().Provider.DisplayName()) || + !strings.EqualFold(serviceName, rule.GetRule().Service) { + continue + } + + checks = append(checks, rule.GetRule().ID) + } + return checks +} diff --git a/pkg/iac/rules/register.go b/pkg/iac/rules/register.go new file mode 100755 index 000000000000..a8a34bd45d62 --- /dev/null +++ b/pkg/iac/rules/register.go @@ -0,0 +1,139 @@ +package rules + +import ( + "sync" + + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy-checks/pkg/compliance" + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/scan" + dftypes "github.com/aquasecurity/trivy/pkg/iac/types" + ruleTypes "github.com/aquasecurity/trivy/pkg/iac/types/rules" + "github.com/aquasecurity/trivy/pkg/set" +) + +type registry struct { + sync.RWMutex + index int + frameworks map[framework.Framework][]ruleTypes.RegisteredRule +} + +var coreRegistry = registry{ + frameworks: make(map[framework.Framework][]ruleTypes.RegisteredRule), +} + +func Reset() { + coreRegistry.Reset() +} + +func Register(rule scan.Rule) ruleTypes.RegisteredRule { + return coreRegistry.register(rule) +} + +func Deregister(rule ruleTypes.RegisteredRule) { + coreRegistry.deregister(rule) +} + +func (r *registry) register(rule scan.Rule) ruleTypes.RegisteredRule { + r.Lock() + defer r.Unlock() + if len(rule.Frameworks) == 0 { + rule.Frameworks = map[framework.Framework][]string{framework.Default: nil} + } + registeredRule := ruleTypes.RegisteredRule{ + Number: r.index, + Rule: rule, + } + r.index++ + for fw := range rule.Frameworks { + r.frameworks[fw] = append(r.frameworks[fw], registeredRule) + } + + r.frameworks[framework.ALL] = append(r.frameworks[framework.ALL], registeredRule) + + return registeredRule +} + +func (r *registry) deregister(rule ruleTypes.RegisteredRule) { + r.Lock() + defer r.Unlock() + for fw := range r.frameworks { + for i, registered := range r.frameworks[fw] { + if registered.Number == rule.Number { + r.frameworks[fw] = append(r.frameworks[fw][:i], r.frameworks[fw][i+1:]...) + break + } + } + } +} + +func (r *registry) getFrameworkRules(fw ...framework.Framework) []ruleTypes.RegisteredRule { + r.RLock() + defer r.RUnlock() + var registered []ruleTypes.RegisteredRule + if len(fw) == 0 { + fw = []framework.Framework{framework.Default} + } + unique := set.New[int]() + for _, f := range fw { + for _, rule := range r.frameworks[f] { + if unique.Contains(rule.Number) { + continue + } + registered = append(registered, rule) + unique.Append(rule.Number) + } + } + return registered +} + +func (r *registry) getSpecRules(spec string) []ruleTypes.RegisteredRule { + r.RLock() + defer r.RUnlock() + var specRules []ruleTypes.RegisteredRule + + var complianceSpec dftypes.ComplianceSpec + specContent := compliance.GetSpec(spec) + if err := yaml.Unmarshal([]byte(specContent), &complianceSpec); err != nil { + return nil + } + + checkIDs := set.New[string]() + for _, csRule := range complianceSpec.Spec.Controls { + for _, c := range csRule.Checks { + checkIDs.Append(c.ID) + } + } + + registered := r.getFrameworkRules(framework.ALL) + for _, rule := range registered { + if checkIDs.Contains(rule.AVDID) || checkIDs.Contains(rule.ID) { + specRules = append(specRules, rule) + } + } + + return specRules +} + +func (r *registry) Reset() { + r.Lock() + defer r.Unlock() + r.frameworks = make(map[framework.Framework][]ruleTypes.RegisteredRule) +} + +func GetFrameworkRules(fw ...framework.Framework) []ruleTypes.RegisteredRule { + return coreRegistry.getFrameworkRules(fw...) +} + +func GetSpecRules(spec string) []ruleTypes.RegisteredRule { + if spec != "" { + return coreRegistry.getSpecRules(spec) + } + + return GetFrameworkRules() +} + +func GetRegistered(fw ...framework.Framework) []ruleTypes.RegisteredRule { + return GetFrameworkRules(fw...) +} diff --git a/pkg/iac/rules/register_test.go b/pkg/iac/rules/register_test.go new file mode 100644 index 000000000000..9c6ca00985c9 --- /dev/null +++ b/pkg/iac/rules/register_test.go @@ -0,0 +1,140 @@ +package rules + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +func Test_Reset(t *testing.T) { + Reset() + rule := scan.Rule{} + _ = Register(rule) + assert.Len(t, GetFrameworkRules(), 1) + Reset() + assert.Empty(t, GetFrameworkRules()) +} + +func Test_Registration(t *testing.T) { + var tests = []struct { + name string + registeredFrameworks map[framework.Framework][]string + inputFrameworks []framework.Framework + expected bool + }{ + { + name: "rule without framework specified should be returned when no frameworks are requested", + expected: true, + }, + { + name: "rule without framework specified should not be returned when a specific framework is requested", + inputFrameworks: []framework.Framework{framework.CIS_AWS_1_2}, + expected: false, + }, + { + name: "rule without framework specified should be returned when the default framework is requested", + inputFrameworks: []framework.Framework{framework.Default}, + expected: true, + }, + { + name: "rule with default framework specified should be returned when the default framework is requested", + registeredFrameworks: map[framework.Framework][]string{framework.Default: {"1.1"}}, + inputFrameworks: []framework.Framework{framework.Default}, + expected: true, + }, + { + name: "rule with default framework specified should not be returned when a specific framework is requested", + registeredFrameworks: map[framework.Framework][]string{framework.Default: {"1.1"}}, + inputFrameworks: []framework.Framework{framework.CIS_AWS_1_2}, + expected: false, + }, + { + name: "rule with specific framework specified should not be returned when a default framework is requested", + registeredFrameworks: map[framework.Framework][]string{framework.CIS_AWS_1_2: {"1.1"}}, + inputFrameworks: []framework.Framework{framework.Default}, + expected: false, + }, + { + name: "rule with specific framework specified should be returned when the specific framework is requested", + registeredFrameworks: map[framework.Framework][]string{framework.CIS_AWS_1_2: {"1.1"}}, + inputFrameworks: []framework.Framework{framework.CIS_AWS_1_2}, + expected: true, + }, + { + name: "rule with multiple frameworks specified should be returned when the specific framework is requested", + registeredFrameworks: map[framework.Framework][]string{framework.CIS_AWS_1_2: {"1.1"}, "blah": {"1.2"}}, + inputFrameworks: []framework.Framework{framework.CIS_AWS_1_2}, + expected: true, + }, + { + name: "rule with multiple frameworks specified should be returned only once when multiple matching frameworks are requested", + registeredFrameworks: map[framework.Framework][]string{framework.CIS_AWS_1_2: {"1.1"}, "blah": {"1.2"}, "something": {"1.3"}}, + inputFrameworks: []framework.Framework{framework.CIS_AWS_1_2, "blah", "other"}, + expected: true, + }, + } + + for i, test := range tests { + t.Run(test.name, func(t *testing.T) { + Reset() + rule := scan.Rule{ + AVDID: fmt.Sprintf("%d-%s", i, test.name), + Frameworks: test.registeredFrameworks, + } + _ = Register(rule) + var found bool + for _, matchedRule := range GetFrameworkRules(test.inputFrameworks...) { + if matchedRule.GetRule().AVDID == rule.AVDID { + assert.False(t, found, "rule should not be returned more than once") + found = true + } + } + assert.Equal(t, test.expected, found, "rule should be returned if it matches any of the input frameworks") + }) + } +} + +func Test_Deregistration(t *testing.T) { + Reset() + registrationA := Register(scan.Rule{ + AVDID: "A", + }) + registrationB := Register(scan.Rule{ + AVDID: "B", + }) + assert.Len(t, GetFrameworkRules(), 2) + Deregister(registrationA) + actual := GetFrameworkRules() + require.Len(t, actual, 1) + assert.Equal(t, "B", actual[0].GetRule().AVDID) + Deregister(registrationB) + assert.Empty(t, GetFrameworkRules()) +} + +func Test_DeregistrationMultipleFrameworks(t *testing.T) { + Reset() + registrationA := Register(scan.Rule{ + AVDID: "A", + }) + registrationB := Register(scan.Rule{ + AVDID: "B", + Frameworks: map[framework.Framework][]string{ + "a": nil, + "b": nil, + "c": nil, + framework.Default: nil, + }, + }) + assert.Len(t, GetFrameworkRules(), 2) + Deregister(registrationA) + actual := GetFrameworkRules() + require.Len(t, actual, 1) + assert.Equal(t, "B", actual[0].GetRule().AVDID) + Deregister(registrationB) + assert.Empty(t, GetFrameworkRules()) +} diff --git a/pkg/iac/scan/code.go b/pkg/iac/scan/code.go new file mode 100644 index 000000000000..e50cc5920f67 --- /dev/null +++ b/pkg/iac/scan/code.go @@ -0,0 +1,320 @@ +package scan + +import ( + "bufio" + "errors" + "fmt" + "io/fs" + "path/filepath" + "strings" + + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Code struct { + Lines []Line +} + +func (c *Code) truncateLines(maxLines int) { + previouslyTruncated := maxLines-1 > 0 && c.Lines[maxLines-2].Truncated + if maxLines-1 > 0 && c.Lines[maxLines-1].LastCause { + c.Lines[maxLines-2].LastCause = true + } + c.Lines[maxLines-1] = Line{ + Truncated: true, + Number: c.Lines[maxLines-1].Number, + } + if previouslyTruncated { + c.Lines = c.Lines[:maxLines-1] + } else { + c.Lines = c.Lines[:maxLines] + } +} + +func (c *Code) markFirstAndLastCauses() { + var isFirst bool + var isLast bool + + for i, line := range c.Lines { + if line.IsCause && !isFirst { + c.Lines[i].FirstCause = true + isFirst = true + } + + if isFirst && !line.IsCause && i > 0 { + c.Lines[i-1].LastCause = true + isLast = true + break + } + } + + if !isLast && len(c.Lines) > 0 { + c.Lines[len(c.Lines)-1].LastCause = true + } +} + +type Line struct { + Number int `json:"Number"` + Content string `json:"Content"` + IsCause bool `json:"IsCause"` + Annotation string `json:"Annotation"` + Truncated bool `json:"Truncated"` + Highlighted string `json:"Highlighted,omitempty"` + FirstCause bool `json:"FirstCause"` + LastCause bool `json:"LastCause"` +} + +func (c *Code) IsCauseMultiline() bool { + var count int + for _, line := range c.Lines { + if line.IsCause { + count++ + if count > 1 { + return true + } + } + } + return false +} + +const ( + DarkTheme = "solarized-dark256" + LightTheme = "github" +) + +type codeSettings struct { + theme string + allowTruncation bool + maxLines int + includeHighlighted bool +} + +var defaultCodeSettings = codeSettings{ + theme: DarkTheme, + allowTruncation: true, + maxLines: 10, + includeHighlighted: true, +} + +type CodeOption func(*codeSettings) + +func OptionCodeWithTheme(theme string) CodeOption { + return func(s *codeSettings) { + s.theme = theme + } +} + +func OptionCodeWithDarkTheme() CodeOption { + return func(s *codeSettings) { + s.theme = DarkTheme + } +} + +func OptionCodeWithLightTheme() CodeOption { + return func(s *codeSettings) { + s.theme = LightTheme + } +} + +func OptionCodeWithTruncation(truncate bool) CodeOption { + return func(s *codeSettings) { + s.allowTruncation = truncate + } +} + +func OptionCodeWithMaxLines(lines int) CodeOption { + return func(s *codeSettings) { + s.maxLines = lines + } +} + +func OptionCodeWithHighlighted(include bool) CodeOption { + return func(s *codeSettings) { + s.includeHighlighted = include + } +} + +func (r *Result) GetCode(opts ...CodeOption) (*Code, error) { + settings := defaultCodeSettings + for _, opt := range opts { + opt(&settings) + } + + fsys := r.Metadata().Range().GetFS() + if fsys == nil { + return nil, errors.New("code unavailable: result was not mapped to a known filesystem") + } + + innerRange := r.metadata.Range() + if err := innerRange.Validate(); err != nil { + return nil, err + } + + if innerRange.GetStartLine() == 0 { + return nil, fmt.Errorf("inner range has invalid start line: %s", innerRange.String()) + } + + outerRange := r.getOuterRange() + if err := outerRange.Validate(); err != nil { + return nil, err + } + + filePath := strings.TrimPrefix(filepath.ToSlash(r.fsPath), "/") + rawLines, err := readLinesFromFile(fsys, filePath, outerRange.GetStartLine(), outerRange.GetEndLine()) + if err != nil { + return nil, err + } + + if outerRange.GetEndLine()-outerRange.GetStartLine() > len(rawLines) { + return nil, fmt.Errorf("invalid outer range: %s", outerRange.String()) + } + + highlightedLines := r.getHighlightedLines(outerRange, innerRange, rawLines, settings) + + var code Code + + shrink := settings.allowTruncation && outerRange.LineCount() > (innerRange.LineCount()+10) + + if shrink { + code.Lines = r.getTruncatedLines(outerRange, innerRange, rawLines, highlightedLines) + } else { + code.Lines = r.getAllLines(outerRange, innerRange, rawLines, highlightedLines) + } + + if settings.allowTruncation && len(code.Lines) > settings.maxLines && settings.maxLines > 0 { + code.truncateLines(settings.maxLines) + } + + code.markFirstAndLastCauses() + + return &code, nil +} + +func (r *Result) getHighlightedLines(outerRange, innerRange iacTypes.Range, rawLines []string, settings codeSettings) []string { + + highlightedLines := make([]string, len(rawLines)) + if !settings.includeHighlighted { + return highlightedLines + } + + content := strings.Join(rawLines, "\n") + fsKey := iacTypes.CreateFSKey(innerRange.GetFS()) + highlightedLines = highlight(fsKey, innerRange.GetLocalFilename(), + outerRange.GetStartLine(), outerRange.GetEndLine(), content, settings.theme) + + if len(highlightedLines) < len(rawLines) { + return rawLines + } + + return highlightedLines +} + +func (r *Result) getOuterRange() iacTypes.Range { + outer := r.Metadata().Range() + for parent := r.Metadata().Parent(); parent != nil && + parent.Range().GetFilename() == outer.GetFilename() && + parent.Range().GetStartLine() > 0; parent = parent.Parent() { + outer = parent.Range() + } + return outer +} + +func (r *Result) getTruncatedLines(outerRange, innerRange iacTypes.Range, rawLines, highlightedLines []string) []Line { + var lines []Line + + if outerRange.GetStartLine() < innerRange.GetStartLine() { + lines = append(lines, Line{ + Content: rawLines[0], + Highlighted: highlightedLines[0], + Number: outerRange.GetStartLine(), + }) + if outerRange.GetStartLine()+1 < innerRange.GetStartLine() { + lines = append(lines, Line{ + Truncated: true, + Number: outerRange.GetStartLine() + 1, + }) + } + } + + for lineNo := innerRange.GetStartLine() - outerRange.GetStartLine(); lineNo <= innerRange.GetEndLine()-outerRange.GetStartLine(); lineNo++ { + if lineNo >= len(rawLines) || lineNo >= len(highlightedLines) { + break + } + + line := Line{ + Number: lineNo + outerRange.GetStartLine(), + Content: strings.TrimSuffix(rawLines[lineNo], "\r"), + Highlighted: strings.TrimSuffix(highlightedLines[lineNo], "\r"), + IsCause: true, + } + + if r.Annotation() != "" && lineNo == innerRange.GetStartLine()-outerRange.GetStartLine()-1 { + line.Annotation = r.Annotation() + } + + lines = append(lines, line) + } + + if outerRange.GetEndLine() > innerRange.GetEndLine() { + if outerRange.GetEndLine() > innerRange.GetEndLine()+1 { + lines = append(lines, Line{ + Truncated: true, + Number: outerRange.GetEndLine() - 1, + }) + } + lines = append(lines, Line{ + Content: rawLines[outerRange.GetEndLine()-outerRange.GetStartLine()], + Highlighted: highlightedLines[outerRange.GetEndLine()-outerRange.GetStartLine()], + Number: outerRange.GetEndLine(), + }) + } + + return lines +} + +func (r *Result) getAllLines(outerRange, innerRange iacTypes.Range, rawLines, highlightedLines []string) []Line { + lines := make([]Line, 0, outerRange.GetEndLine()-outerRange.GetStartLine()+1) + + for lineNo := 0; lineNo <= outerRange.GetEndLine()-outerRange.GetStartLine(); lineNo++ { + line := Line{ + Number: lineNo + outerRange.GetStartLine(), + Content: strings.TrimSuffix(rawLines[lineNo], "\r"), + Highlighted: strings.TrimSuffix(highlightedLines[lineNo], "\r"), + IsCause: lineNo >= innerRange.GetStartLine()-outerRange.GetStartLine() && + lineNo <= innerRange.GetEndLine()-outerRange.GetStartLine(), + } + + if r.Annotation() != "" && lineNo == innerRange.GetStartLine()-outerRange.GetStartLine()-1 { + line.Annotation = r.Annotation() + } + + lines = append(lines, line) + } + + return lines +} + +func readLinesFromFile(fsys fs.FS, path string, from, to int) ([]string, error) { + slashedPath := strings.TrimPrefix(filepath.ToSlash(path), "/") + + file, err := fsys.Open(slashedPath) + if err != nil { + return nil, fmt.Errorf("failed to read file from result filesystem: %w", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + rawLines := make([]string, 0, to-from+1) + + for lineNum := 0; scanner.Scan() && lineNum < to; lineNum++ { + if lineNum >= from-1 { + rawLines = append(rawLines, scanner.Text()) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to scan file: %w", err) + } + + return rawLines, nil +} diff --git a/pkg/iac/scan/code_test.go b/pkg/iac/scan/code_test.go new file mode 100644 index 000000000000..559f1d9dfe69 --- /dev/null +++ b/pkg/iac/scan/code_test.go @@ -0,0 +1,402 @@ +package scan + +import ( + "strings" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func TestResult_GetCode(t *testing.T) { + const line = "If you can do a half-assed job of anything, you're a one-eyed man in a kingdom of the blind." + tests := []struct { + name string + source string + filename string + startInnerLine int + endInnerLine int + startOuterLine int + endOuterLine int + expected []Line + options []CodeOption + wantErr bool + }{ + { + name: "basic w/ defaults", + source: `1 +2 +3 +4`, + filename: "test.txt", + startInnerLine: 2, + endInnerLine: 3, + expected: []Line{ + { + Number: 2, + Content: "2", + IsCause: true, + Highlighted: "2", + FirstCause: true, + LastCause: false, + }, + { + Number: 3, + Content: "3", + IsCause: true, + Highlighted: "3", + FirstCause: false, + LastCause: true, + }, + }, + }, + { + name: "nested ranges", + source: `resource "aws_s3_bucket" "something" { + bucket = "something" +}`, + filename: "main.tf", + startInnerLine: 2, + endInnerLine: 2, + startOuterLine: 1, + endOuterLine: 3, + options: []CodeOption{OptionCodeWithHighlighted(false)}, + expected: []Line{ + { + Number: 1, + Content: `resource "aws_s3_bucket" "something" {`, + }, + { + Number: 2, + Content: ` bucket = "something"`, + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + Content: "}", + }, + }, + }, + { + name: "bad filename", + source: `1 +2 +3 +4`, + filename: "", + startInnerLine: 2, + endInnerLine: 3, + wantErr: true, + }, + { + name: "no line numbers", + source: `1 +2 +3 +4`, + filename: "test.txt", + startInnerLine: 0, + endInnerLine: 0, + wantErr: true, + }, + { + name: "negative line numbers", + source: `1 +2 +3 +4`, + filename: "test.txt", + startInnerLine: -2, + endInnerLine: -1, + wantErr: true, + }, + { + name: "invalid line numbers", + source: `1 +2 +3 +4`, + filename: "test.txt", + startInnerLine: 5, + endInnerLine: 6, + wantErr: true, + }, + { + name: "syntax highlighting", + source: `FROM ubuntu`, + filename: "Dockerfile", + startInnerLine: 1, + endInnerLine: 1, + expected: []Line{ + { + Number: 1, + Content: "FROM ubuntu", + IsCause: true, + Highlighted: "\x1b[38;5;64mFROM\x1b[0m\x1b[38;5;37m ubuntu\x1b[0m", + FirstCause: true, + LastCause: true, + }, + }, + }, + { + name: "truncation", + source: strings.Repeat(line+"\n", 100), + filename: "longfile.txt", + startInnerLine: 1, + endInnerLine: 100, + expected: []Line{ + { + Number: 1, + Content: line, + IsCause: true, + Highlighted: line, + FirstCause: true, + LastCause: false, + }, + { + Number: 2, + Content: line, + IsCause: true, + Highlighted: line, + FirstCause: false, + LastCause: false, + }, + { + Number: 3, + Content: line, + IsCause: true, + Highlighted: line, + FirstCause: false, + LastCause: false, + }, + { + Number: 4, + Content: line, + IsCause: true, + Highlighted: line, + FirstCause: false, + LastCause: false, + }, + { + Number: 5, + Content: line, + IsCause: true, + Highlighted: line, + FirstCause: false, + LastCause: false, + }, + { + Number: 6, + Content: line, + IsCause: true, + Highlighted: line, + FirstCause: false, + LastCause: false, + }, + { + Number: 7, + Content: line, + IsCause: true, + Highlighted: line, + FirstCause: false, + LastCause: false, + }, + { + Number: 8, + Content: line, + IsCause: true, + Highlighted: line, + FirstCause: false, + LastCause: false, + }, + { + Number: 9, + Content: line, + IsCause: true, + Highlighted: line, + FirstCause: false, + LastCause: true, + }, + { + Number: 10, + Truncated: true, + }, + }, + }, + { + name: "invalid inner range", + source: `Test`, + filename: "test.txt", + startInnerLine: 0, + endInnerLine: 0, + wantErr: true, + }, + { + name: "invalid outer range", + source: `Test`, + filename: "test.txt", + startInnerLine: 10, + endInnerLine: 12, + startOuterLine: 5, + endOuterLine: 3, + wantErr: true, + }, + { + name: "truncate with outer range", + source: strings.Repeat(line+"\n", 100), + filename: "longfile.txt", + startOuterLine: 1, + endOuterLine: 100, + startInnerLine: 10, + endInnerLine: 12, + options: []CodeOption{OptionCodeWithTruncation(true)}, + expected: []Line{ + { + Number: 1, + Content: line, + Highlighted: line, + }, + { + Number: 2, + Truncated: true, + }, + { + Number: 10, + Content: line, + IsCause: true, + FirstCause: true, + Highlighted: line, + }, + { + Number: 11, + Content: line, + IsCause: true, + Highlighted: line, + }, + { + Number: 12, + Content: line, + IsCause: true, + LastCause: true, + Highlighted: line, + }, + { + Number: 99, + Truncated: true, + }, + { + Number: 100, + Content: line, + Highlighted: line, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fsys := fstest.MapFS{ + test.filename: &fstest.MapFile{ + Data: []byte(test.source), + }, + } + + meta := iacTypes.NewMetadata( + iacTypes.NewRange(test.filename, test.startInnerLine, test.endInnerLine, "", fsys), + "", + ) + if test.startOuterLine > 0 { + meta = meta.WithParent(iacTypes.NewMetadata( + iacTypes.NewRange(test.filename, test.startOuterLine, test.endOuterLine, "", fsys), + "", + )) + } + result := &Result{ + metadata: meta, + fsPath: test.filename, + } + code, err := result.GetCode(test.options...) + if test.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, test.expected, code.Lines) + }) + } + +} + +func TestCode_IsCauseMultiline(t *testing.T) { + + tests := []struct { + name string + code Code + expected bool + }{ + { + name: "no cause", + code: Code{ + Lines: []Line{ + { + Number: 1, + Content: "Test", + Highlighted: "Test", + }, + }, + }, + expected: false, + }, + { + name: "one cause", + code: Code{ + Lines: []Line{ + { + Number: 1, + Content: "Test", + IsCause: true, + Highlighted: "Test", + }, + }, + }, + expected: false, + }, + { + name: "multiple causes", + code: Code{ + Lines: []Line{ + { + Number: 1, + Content: "Test", + IsCause: true, + Highlighted: "Test", + }, + { + Number: 2, + Content: "Test", + IsCause: true, + Highlighted: "Test", + }, + { + Number: 3, + Content: "Test", + IsCause: true, + Highlighted: "Test", + }, + }, + }, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.code.IsCauseMultiline()) + }) + } +} diff --git a/pkg/iac/scan/flat.go b/pkg/iac/scan/flat.go new file mode 100755 index 000000000000..f0b52c600355 --- /dev/null +++ b/pkg/iac/scan/flat.go @@ -0,0 +1,78 @@ +package scan + +import ( + "github.com/aquasecurity/trivy/pkg/iac/providers" + "github.com/aquasecurity/trivy/pkg/iac/severity" +) + +// TODO: This struct is not currently serialized to JSON, +// so JSON tags may be removed if unused. +type FlatResult struct { + // TODO: The following fields are currently unused: + // nolint: gocritic + // Deprecated, RuleID, LongID, RuleSummary, Impact, RangeAnnotation + Deprecated bool `json:"deprecated,omitempty"` + RuleID string `json:"rule_id"` + LongID string `json:"long_id"` + RuleSummary string `json:"rule_description"` + RuleProvider providers.Provider `json:"rule_provider"` + RuleService string `json:"rule_service"` + Impact string `json:"impact"` + Resolution string `json:"resolution"` + Links []string `json:"links"` + Description string `json:"description"` + RangeAnnotation string `json:"-"` + Severity severity.Severity `json:"severity"` + Status Status `json:"status"` + Resource string `json:"resource"` + Occurrences []Occurrence `json:"occurrences,omitempty"` + Location FlatRange `json:"location"` + RenderedCause RenderedCause `json:"rendered_cause"` +} + +type FlatRange struct { + Filename string `json:"filename"` + StartLine int `json:"start_line"` + EndLine int `json:"end_line"` +} + +func (r Results) Flatten() []FlatResult { + results := make([]FlatResult, 0, len(r)) + for _, original := range r { + results = append(results, original.Flatten()) + } + return results +} + +func (r *Result) Flatten() FlatResult { + rng := r.metadata.Range() + + resMetadata := r.metadata + + for resMetadata.Parent() != nil { + resMetadata = *resMetadata.Parent() + } + + return FlatResult{ + Deprecated: r.rule.Deprecated, + LongID: r.Rule().CanonicalID(), + RuleSummary: r.rule.Summary, + RuleProvider: r.rule.Provider, + RuleService: r.rule.Service, + Impact: r.rule.Impact, + Resolution: r.rule.Resolution, + Links: r.rule.Links, + Description: r.Description(), + RangeAnnotation: r.Annotation(), + Severity: r.rule.Severity, + Status: r.status, + Resource: resMetadata.Reference(), + Occurrences: r.Occurrences(), + Location: FlatRange{ + Filename: rng.GetFilename(), + StartLine: rng.GetStartLine(), + EndLine: rng.GetEndLine(), + }, + RenderedCause: r.renderedCause, + } +} diff --git a/pkg/iac/scan/highlighting.go b/pkg/iac/scan/highlighting.go new file mode 100644 index 000000000000..dcc685bb005c --- /dev/null +++ b/pkg/iac/scan/highlighting.go @@ -0,0 +1,129 @@ +package scan + +import ( + "bytes" + "fmt" + "strings" + "sync" + + "github.com/alecthomas/chroma" + "github.com/alecthomas/chroma/formatters" + "github.com/alecthomas/chroma/lexers" + "github.com/alecthomas/chroma/styles" +) + +type cache struct { + sync.RWMutex + data map[string][]string +} + +func (c *cache) Get(key string) ([]string, bool) { + c.RLock() + defer c.RUnlock() + data, ok := c.data[key] + return data, ok +} + +func (c *cache) Set(key string, data []string) { + c.Lock() + defer c.Unlock() + c.data[key] = data +} + +var globalCache = &cache{ + data: make(map[string][]string), +} + +func highlight(fsKey, filename string, startLine, endLine int, input, theme string) []string { + + key := fmt.Sprintf("%s|%s|%d-%d", fsKey, filename, startLine, endLine) + if lines, ok := globalCache.Get(key); ok { + return lines + } + + highlighted, ok := Highlight(filename, input, theme) + if !ok { + return nil + } + + lines := strings.Split(highlighted, "\n") + globalCache.Set(key, lines) + return lines +} + +func Highlight(filename, input, theme string) (string, bool) { + style := styles.Get(theme) + if style == nil { + style = styles.Fallback + } + formatter := formatters.Get("terminal256") + if formatter == nil { + formatter = formatters.Fallback + } + + lexer := lexers.Match(filename) + if lexer == nil { + lexer = lexers.Fallback + } + lexer = chroma.Coalesce(lexer) + + iterator, err := lexer.Tokenise(nil, input) + if err != nil { + return "", false + } + + var buffer bytes.Buffer + if err := formatter.Format(&buffer, style, iterator); err != nil { + return "", false + } + + return string(shiftANSIOverLineEndings(buffer.Bytes())), true +} + +func shiftANSIOverLineEndings(input []byte) []byte { + var output []byte + prev := byte(0) + inCSI := false + csiShouldCarry := false + var csi []byte + var skipOutput bool + for _, r := range input { + skipOutput = false + if !inCSI { + switch { + case r == '\n': + if csiShouldCarry && len(csi) > 0 { + skipOutput = true + output = append(output, '\n') + output = append(output, csi...) + csi = nil + csiShouldCarry = false + } + case r == '[' && prev == 0x1b: + inCSI = true + csi = append(csi, 0x1b, '[') + output = output[:len(output)-1] + skipOutput = true + default: + csiShouldCarry = false + if len(csi) > 0 { + output = append(output, csi...) + csi = nil + } + } + } else { + csi = append(csi, r) + skipOutput = true + if r >= 0x40 && r <= 0x7E { + csiShouldCarry = true + inCSI = false + } + } + if !skipOutput { + output = append(output, r) + } + prev = r + } + + return append(output, csi...) +} diff --git a/pkg/iac/scan/result.go b/pkg/iac/scan/result.go new file mode 100644 index 000000000000..26d0080e741f --- /dev/null +++ b/pkg/iac/scan/result.go @@ -0,0 +1,388 @@ +package scan + +import ( + "fmt" + "io/fs" + "path/filepath" + "reflect" + "strconv" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/ignore" + "github.com/aquasecurity/trivy/pkg/iac/severity" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Status uint8 + +const ( + StatusFailed Status = iota + StatusPassed + StatusIgnored +) + +type Result struct { + rule Rule + description string + annotation string + status Status + metadata iacTypes.Metadata + severityOverride *severity.Severity + regoNamespace string + regoRule string + traces []string + fsPath string + renderedCause RenderedCause +} + +func (r Result) RegoNamespace() string { + return r.regoNamespace +} + +func (r Result) RegoRule() string { + return r.regoRule +} + +func (r Result) Severity() severity.Severity { + if r.severityOverride != nil { + return *r.severityOverride + } + return r.Rule().Severity +} + +func (r *Result) OverrideSeverity(s severity.Severity) { + r.severityOverride = &s +} + +func (r *Result) OverrideDescription(description string) { + r.description = description +} + +func (r *Result) OverrideMetadata(metadata iacTypes.Metadata) { + r.metadata = metadata +} + +func (r *Result) OverrideStatus(status Status) { + r.status = status +} + +func (r *Result) OverrideAnnotation(annotation string) { + r.annotation = annotation +} + +func (r *Result) SetRule(ru Rule) { + r.rule = ru +} + +func (r Result) Status() Status { + return r.status +} + +func (r Result) Rule() Rule { + return r.rule +} + +func (r Result) Description() string { + return r.description +} + +func (r Result) Annotation() string { + return r.annotation +} + +func (r Result) Metadata() iacTypes.Metadata { + return r.metadata +} + +func (r Result) Range() iacTypes.Range { + return r.metadata.Range() +} + +func (r Result) Traces() []string { + return r.traces +} + +type RenderedCause struct { + Raw string +} + +func (r *Result) WithRenderedCause(cause RenderedCause) { + r.renderedCause = cause +} + +func (r *Result) AbsolutePath(fsRoot string, metadata iacTypes.Metadata) string { + if strings.HasSuffix(fsRoot, ":") { + fsRoot += "/" + } + + if metadata.IsUnmanaged() { + return "" + } + rng := metadata.Range() + if rng.GetSourcePrefix() != "" && !strings.HasPrefix(rng.GetSourcePrefix(), ".") { + return rng.GetFilename() + } + return filepath.Join(fsRoot, rng.GetLocalFilename()) +} + +func (r *Result) RelativePathTo(fsRoot, to string, metadata iacTypes.Metadata) string { + + absolute := r.AbsolutePath(fsRoot, metadata) + + if strings.HasSuffix(fsRoot, ":") { + fsRoot += "/" + } + + if metadata.IsUnmanaged() { + return absolute + } + rng := metadata.Range() + if rng.GetSourcePrefix() != "" && !strings.HasPrefix(rng.GetSourcePrefix(), ".") { + return absolute + } + if !strings.HasPrefix(rng.GetLocalFilename(), strings.TrimSuffix(fsRoot, "/")) { + return absolute + } + relative, err := filepath.Rel(to, rng.GetLocalFilename()) + if err != nil { + return absolute + } + return relative +} + +type Results []Result + +type MetadataProvider interface { + GetMetadata() iacTypes.Metadata + GetRawValue() any +} + +func (r *Results) GetPassed() Results { + return r.filterStatus(StatusPassed) +} + +func (r *Results) GetIgnored() Results { + return r.filterStatus(StatusIgnored) +} + +func (r *Results) GetFailed() Results { + return r.filterStatus(StatusFailed) +} + +func (r *Results) filterStatus(status Status) Results { + var filtered Results + if r == nil { + return filtered + } + for _, res := range *r { + if res.Status() == status { + filtered = append(filtered, res) + } + } + return filtered +} + +func (r *Results) Add(description string, source any) { + result := Result{ + description: description, + } + result.metadata = getMetadataFromSource(source) + if result.metadata.IsExplicit() { + result.annotation = getAnnotation(source) + } + rnge := result.metadata.Range() + result.fsPath = rnge.GetLocalFilename() + *r = append(*r, result) +} + +func (r *Results) AddRego(description, namespace, rule string, traces []string, source MetadataProvider) { + result := Result{ + description: description, + regoNamespace: namespace, + regoRule: rule, + traces: traces, + } + result.metadata = getMetadataFromSource(source) + if result.metadata.IsExplicit() { + result.annotation = getAnnotation(source) + } + rnge := result.metadata.Range() + result.fsPath = rnge.GetLocalFilename() + *r = append(*r, result) +} + +func (r *Results) AddPassed(source any, descriptions ...string) { + res := Result{ + description: strings.Join(descriptions, " "), + status: StatusPassed, + } + res.metadata = getMetadataFromSource(source) + rnge := res.metadata.Range() + res.fsPath = rnge.GetLocalFilename() + *r = append(*r, res) +} + +func getMetadataFromSource(source any) iacTypes.Metadata { + if provider, ok := source.(MetadataProvider); ok { + return provider.GetMetadata() + } + + metaValue := reflect.ValueOf(source) + if metaValue.Kind() == reflect.Ptr { + metaValue = metaValue.Elem() + } + metaVal := metaValue.FieldByName("Metadata") + return metaVal.Interface().(iacTypes.Metadata) +} + +func getAnnotation(source any) string { + if provider, ok := source.(MetadataProvider); ok { + return rawToString(provider.GetRawValue()) + } + return "" +} + +func (r *Results) AddPassedRego(namespace, rule string, traces []string, source any) { + res := Result{ + status: StatusPassed, + regoNamespace: namespace, + regoRule: rule, + traces: traces, + } + res.metadata = getMetadataFromSource(source) + rnge := res.metadata.Range() + res.fsPath = rnge.GetLocalFilename() + *r = append(*r, res) +} + +func (r *Results) AddIgnored(source any, descriptions ...string) { + res := Result{ + description: strings.Join(descriptions, " "), + status: StatusIgnored, + } + res.metadata = getMetadataFromSource(source) + rnge := res.metadata.Range() + res.fsPath = rnge.GetLocalFilename() + *r = append(*r, res) +} + +func (r *Results) Ignore(ignoreRules ignore.Rules, ignores map[string]ignore.Ignorer) { + for i, result := range *r { + allIDs := []string{ + result.Rule().ID, + strings.ToLower(result.Rule().ID), + result.Rule().CanonicalID(), + result.Rule().AVDID, + strings.ToLower(result.Rule().AVDID), + result.Rule().ShortCode, + } + allIDs = append(allIDs, result.Rule().Aliases...) + + if ignoreRules.Ignore(result.Metadata(), allIDs, ignores) { + (*r)[i].OverrideStatus(StatusIgnored) + } + } +} + +func (r *Results) SetRule(rule Rule) { + for i := range *r { + (*r)[i].rule = rule + } +} + +func (r *Results) SetSourceAndFilesystem(source string, f fs.FS, logicalSource bool) { + for i := range *r { + m := (*r)[i].Metadata() + if m.IsUnmanaged() { + continue + } + rng := m.Range() + + newrng := iacTypes.NewRange(rng.GetLocalFilename(), rng.GetStartLine(), rng.GetEndLine(), source, f) + if logicalSource { + newrng = iacTypes.NewRangeWithLogicalSource(rng.GetLocalFilename(), rng.GetStartLine(), rng.GetEndLine(), + source, f) + } + parent := m.Parent() + switch { + case m.IsExplicit(): + m = iacTypes.NewExplicitMetadata(newrng, m.Reference()) + default: + m = iacTypes.NewMetadata(newrng, m.Reference()) + } + if parent != nil { + m.SetParentPtr(parent) + } + (*r)[i].OverrideMetadata(m) + } +} + +func rawToString(raw any) string { + if raw == nil { + return "" + } + switch t := raw.(type) { + case int: + return strconv.Itoa(t) + case bool: + return strconv.FormatBool(t) + case float64: + return fmt.Sprintf("%f", t) + case string: + return fmt.Sprintf("%q", t) + case []string: + var items []string + for _, item := range t { + items = append(items, rawToString(item)) + } + return fmt.Sprintf("[%s]", strings.Join(items, ", ")) + case []int: + var items []string + for _, item := range t { + items = append(items, rawToString(item)) + } + return fmt.Sprintf("[%s]", strings.Join(items, ", ")) + case []float64: + var items []string + for _, item := range t { + items = append(items, rawToString(item)) + } + return fmt.Sprintf("[%s]", strings.Join(items, ", ")) + case []bool: + var items []string + for _, item := range t { + items = append(items, rawToString(item)) + } + return fmt.Sprintf("[%s]", strings.Join(items, ", ")) + default: + return "?" + } +} + +type Occurrence struct { + Resource string `json:"resource"` + Filename string `json:"filename"` + StartLine int `json:"start_line"` + EndLine int `json:"end_line"` +} + +func (r *Result) Occurrences() []Occurrence { + var occurrences []Occurrence + + mod := &r.metadata + + for { + mod = mod.Parent() + if mod == nil { + break + } + parentRange := mod.Range() + occurrences = append(occurrences, Occurrence{ + Resource: mod.Reference(), + Filename: parentRange.GetFilename(), + StartLine: parentRange.GetStartLine(), + EndLine: parentRange.GetEndLine(), + }) + } + return occurrences +} diff --git a/pkg/iac/scan/result_test.go b/pkg/iac/scan/result_test.go new file mode 100644 index 000000000000..67da0d61be86 --- /dev/null +++ b/pkg/iac/scan/result_test.go @@ -0,0 +1,57 @@ +package scan_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Occurrences(t *testing.T) { + tests := []struct { + name string + factory func() *scan.Result + expected []scan.Occurrence + }{ + { + name: "happy", + factory: func() *scan.Result { + r := scan.Result{} + causeResourceMeta := types.NewMetadata(types.NewRange( + "main.tf", 1, 13, "", nil, + ), "module.aws-security-groups[\"db1\"]") + + parentMeta := types.NewMetadata(types.NewRange( + "terraform-aws-modules/security-group/aws/main.tf", 191, 227, "", nil, + ), "aws_security_group_rule.ingress_with_cidr_blocks[0]").WithParent(causeResourceMeta) + + r.OverrideMetadata(types.NewMetadata(types.NewRange( + "terraform-aws-modules/security-group/aws/main.tf", 197, 204, "", nil, + ), "aws_security_group_rule.ingress_with_cidr_blocks").WithParent(parentMeta)) + return &r + }, + expected: []scan.Occurrence{ + { + Resource: "aws_security_group_rule.ingress_with_cidr_blocks[0]", + Filename: "terraform-aws-modules/security-group/aws/main.tf", + StartLine: 191, + EndLine: 227, + }, + { + Resource: "module.aws-security-groups[\"db1\"]", + Filename: "main.tf", + StartLine: 1, + EndLine: 13, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.factory().Occurrences()) + }) + } +} diff --git a/pkg/iac/scan/rule.go b/pkg/iac/scan/rule.go new file mode 100755 index 000000000000..4e823675c68a --- /dev/null +++ b/pkg/iac/scan/rule.go @@ -0,0 +1,162 @@ +package scan + +import ( + "fmt" + "regexp" + "slices" + "strings" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/providers" + "github.com/aquasecurity/trivy/pkg/iac/severity" + "github.com/aquasecurity/trivy/pkg/iac/state" + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +type CheckFunc func(s *state.State) (results Results) + +type EngineMetadata struct { + GoodExamples []string `json:"good_examples,omitempty"` + BadExamples []string `json:"bad_examples,omitempty"` + RemediationMarkdown string `json:"remediation_markdown,omitempty"` + Links []string `json:"links,omitempty"` +} + +type CustomChecks struct { + Terraform *TerraformCustomCheck +} + +type TerraformCustomCheck struct { + RequiredTypes []string + RequiredLabels []string + RequiredSources []string + Check func(*terraform.Block, *terraform.Module) Results +} + +type Rule struct { + ID string `json:"id"` + Deprecated bool `json:"deprecated"` + // Deprecated: Use the ID field instead. + AVDID string `json:"avd_id"` + Aliases []string `json:"aliases"` + LongID string `json:"long_id"` + ShortCode string `json:"short_code"` + Summary string `json:"summary"` + Explanation string `json:"explanation"` + Impact string `json:"impact"` + Resolution string `json:"resolution"` + Provider providers.Provider `json:"provider"` + Service string `json:"service"` + Links []string `json:"links"` + Severity severity.Severity `json:"severity"` + Terraform *EngineMetadata `json:"terraform,omitempty"` + CloudFormation *EngineMetadata `json:"cloud_formation,omitempty"` + Examples string `json:"-"` + CustomChecks CustomChecks `json:"-"` + RegoPackage string `json:"-"` + Frameworks map[framework.Framework][]string `json:"frameworks"` + Check CheckFunc `json:"-"` + MinimumTrivyVersion string `json:"minimum_trivy_version"` +} + +func (r Rule) IsDeprecated() bool { + return r.Deprecated +} + +func (r Rule) HasID(id string) bool { + if r.ID == id || r.AVDID == id || r.LongID == id || r.CanonicalID() == id { + return true + } + return slices.Contains(r.Aliases, id) +} + +func (r Rule) CanonicalID() string { + if r.LongID != "" { + return r.LongID + } + return strings.ToLower(fmt.Sprintf("%s-%s-%s", r.Provider, r.Service, r.ShortCode)) +} + +func (r Rule) ServiceDisplayName() string { + return nicify(r.Service) +} + +func (r Rule) ShortCodeDisplayName() string { + return nicify(r.ShortCode) +} + +var acronyms = []string{ + "acl", + "alb", + "api", + "arn", + "aws", + "cidr", + "db", + "dns", + "ebs", + "ec2", + "ecr", + "ecs", + "efs", + "eks", + "elb", + "gke", + "http", + "http2", + "https", + "iam", + "im", + "imds", + "ip", + "ips", + "kms", + "lb", + "md5", + "mfa", + "mq", + "msk", + "rbac", + "rdp", + "rds", + "rsa", + "sam", + "sgr", + "sha1", + "sha256", + "sns", + "sql", + "sqs", + "ssh", + "ssm", + "tls", + "ubla", + "vm", + "vpc", + "vtpm", + "waf", +} + +var specials = map[string]string{ + "dynamodb": "DynamoDB", + "documentdb": "DocumentDB", + "mysql": "MySQL", + "postgresql": "PostgreSQL", + "acls": "ACLs", + "ips": "IPs", + "bigquery": "BigQuery", +} + +func nicify(input string) string { + input = strings.ToLower(input) + for replace, with := range specials { + input = regexp.MustCompile(fmt.Sprintf("\\b%s\\b", replace)).ReplaceAllString(input, with) + } + for _, acronym := range acronyms { + input = regexp.MustCompile(fmt.Sprintf("\\b%s\\b", acronym)).ReplaceAllString(input, strings.ToUpper(acronym)) + } + return cases.Title(language.English).String(strings.ReplaceAll(input, "-", " ")) +} diff --git a/pkg/iac/scanners/ansible/fsutils/fsutils.go b/pkg/iac/scanners/ansible/fsutils/fsutils.go new file mode 100644 index 000000000000..9dfe523c8c55 --- /dev/null +++ b/pkg/iac/scanners/ansible/fsutils/fsutils.go @@ -0,0 +1,172 @@ +package fsutils + +import ( + "errors" + "io/fs" + "os" + "path" + "path/filepath" + "sort" +) + +// FileSource represents a file together with the filesystem it belongs to. +// +// It abstracts over virtual filesystems and real disk paths, allowing +// consistent access to files whether they reside in a virtual FS or on disk. +type FileSource struct { + // FS is the filesystem used to access the file. + // It is ignored if Path is absolute. + FS fs.FS + + // Path is the relative or absolute path to the file in Unix-like format. + // If Path is relative, FS is used; if absolute, the file is accessed directly on disk. + Path string +} + +func NewFileSource(fsys fs.FS, p string) FileSource { + if filepath.IsAbs(p) { + return FileSource{ + FS: nil, + Path: filepath.ToSlash(p), + } + } + return FileSource{ + FS: fsys, + Path: path.Clean(p), + } +} + +func (f FileSource) String() string { + if f.FS != nil { + return f.Path + } + return f.osPath() +} + +func (f FileSource) osPath() string { + return filepath.FromSlash(f.Path) +} + +// FSAndRelPath returns the fs.FS and relative path to use for opening the file. +// If the FileSource has an embedded FS, it is used as-is. +// For absolute paths without FS, it returns an os.DirFS rooted at the parent directory +// and the file name as the relative path. +func (f FileSource) FSAndRelPath() (fs.FS, string) { + if f.FS != nil { + return f.FS, f.Path + } + + absPath := filepath.FromSlash(f.Path) + return os.DirFS(filepath.Dir(absPath)), filepath.Base(absPath) +} + +func (f FileSource) Join(elem ...string) FileSource { + for i, e := range elem { + elem[i] = filepath.ToSlash(e) + } + return FileSource{ + FS: f.FS, + Path: path.Join(append([]string{f.Path}, elem...)...), + } +} + +func (f FileSource) Stat() (fs.FileInfo, error) { + if f.FS != nil { + return fs.Stat(f.FS, f.Path) + } + return os.Stat(f.osPath()) +} + +func (f FileSource) Exists() (bool, error) { + _, err := f.Stat() + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return false, nil + } + return false, err + } + return true, nil +} + +func (f FileSource) ReadFile() ([]byte, error) { + if f.FS != nil { + return fs.ReadFile(f.FS, f.Path) + } + return os.ReadFile(f.osPath()) +} + +func (f FileSource) Open() (fs.File, error) { + if f.FS != nil { + return f.FS.Open(f.Path) + } + return os.Open(f.osPath()) +} + +func (f FileSource) Dir() FileSource { + return FileSource{ + FS: f.FS, + Path: path.Dir(f.Path), + } +} + +func (f FileSource) ReadDir() ([]fs.DirEntry, error) { + if f.FS != nil { + return fs.ReadDir(f.FS, f.Path) + } + return os.ReadDir(f.osPath()) +} + +func (f FileSource) walkDir(fn fs.WalkDirFunc) error { + if f.FS != nil { + return fs.WalkDir(f.FS, f.Path, fn) + } + + return filepath.WalkDir(f.osPath(), fn) +} + +func (f FileSource) WalkDirFS(fn func(FileSource, fs.DirEntry) error) error { + walkFn := func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + return fn(FileSource{FS: f.FS, Path: filepath.ToSlash(path)}, d) + } + return f.walkDir(walkFn) +} + +func WalkDirsFirstAlpha(root FileSource, fn func(FileSource, fs.DirEntry) error) error { + var walk func(fileSrc FileSource) error + walk = func(fileSrc FileSource) error { + entries, err := fileSrc.ReadDir() + if err != nil { + return err + } + + SortDirsFirstAlpha(entries) + + for _, entry := range entries { + entrySrc := fileSrc.Join(entry.Name()) + if err := fn(entrySrc, entry); err != nil { + return err + } + + if entry.IsDir() { + if err := walk(entrySrc); err != nil { + return err + } + } + } + return nil + } + + return walk(root) +} + +func SortDirsFirstAlpha(entries []fs.DirEntry) { + sort.Slice(entries, func(i, j int) bool { + if entries[i].IsDir() != entries[j].IsDir() { + return entries[i].IsDir() + } + return entries[i].Name() < entries[j].Name() + }) +} diff --git a/pkg/iac/scanners/ansible/inventory/ini.go b/pkg/iac/scanners/ansible/inventory/ini.go new file mode 100644 index 000000000000..bea8b44d6ee9 --- /dev/null +++ b/pkg/iac/scanners/ansible/inventory/ini.go @@ -0,0 +1,174 @@ +package inventory + +import ( + "bufio" + "bytes" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + "github.com/aquasecurity/trivy/pkg/set" +) + +const ( + sectionHosts = iota + sectionVars + sectionChildren +) + +func ParseINI(data []byte) (*Inventory, error) { + inv := newInventory() + + currentGroup := "ungrouped" + sectionType := sectionHosts + + scanner := bufio.NewScanner(bytes.NewReader(data)) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + line = removeComment(line) + if line == "" { + continue + } + + // handle section + if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { + + sectionName := line[1 : len(line)-1] + parts := strings.SplitN(sectionName, ":", 2) + currentGroup = parts[0] + sectionType = sectionHosts + + // group related sections with :modifiers + if len(parts) == 2 { + switch parts[1] { + case "vars": + sectionType = sectionVars + case "children": + sectionType = sectionChildren + } + } + continue + } + + switch sectionType { + case sectionHosts: + fields := splitFields(line) + if len(fields) == 0 { + // skip empty line + continue + } + + hostName := fields[0] + plainHostVars := make(vars.PlainVars) + + for _, f := range fields[1:] { + kv := strings.SplitN(f, "=", 2) + if len(kv) == 2 { + plainHostVars[kv[0]] = kv[1] + } + } + + hostVars := vars.NewVars(plainHostVars, vars.InvFileHostPriority) + inv.addHost(hostName, newHost(hostVars, set.New(currentGroup))) + case sectionVars: + kv := strings.SplitN(line, "=", 2) + key := strings.TrimSpace(kv[0]) + var val string + if len(kv) == 2 { + val = strings.TrimSpace(kv[1]) + } + plainGroupVars := vars.PlainVars{key: val} + groupVars := vars.NewVars(plainGroupVars, vars.InvFileGroupPriority) + inv.addGroup(currentGroup, newGroup(groupVars, set.New[string]())) + case sectionChildren: + inv.addGroup(line, newGroup(make(vars.Vars), set.New(currentGroup))) + } + } + + if err := scanner.Err(); err != nil { + return nil, xerrors.Errorf("file scan: %w", err) + } + + inv.initDefaultGroups() + return inv, nil +} + +func removeComment(line string) string { + var ( + inQuotes bool + quoteChar rune + ) + + for i, r := range line { + switch r { + case '"', '\'': + if inQuotes { + if r == quoteChar { + inQuotes = false + } + } else { + inQuotes = true + quoteChar = r + } + case '#', ';': + if !inQuotes { + return strings.TrimSpace(line[:i]) + } + } + } + return strings.TrimSpace(line) +} + +func splitFields(input string) []string { + var ( + inQuotes bool + quoteChar rune + escape bool + field strings.Builder + fields []string + ) + + flush := func() { + if field.Len() > 0 { + fields = append(fields, field.String()) + field.Reset() + } + } + + for _, r := range input { + if escape { + field.WriteRune(r) + escape = false + continue + } + + switch r { + case '\\': + escape = true + case '"', '\'': + if !inQuotes { + inQuotes = true + quoteChar = r + continue + } + + if r == quoteChar { + inQuotes = false + continue + } + field.WriteRune(r) + case ' ', '\t': + if inQuotes { + field.WriteRune(r) + } else { + flush() + } + default: + field.WriteRune(r) + } + } + flush() + + return fields +} diff --git a/pkg/iac/scanners/ansible/inventory/init_test.go b/pkg/iac/scanners/ansible/inventory/init_test.go new file mode 100644 index 000000000000..215a8cdca88e --- /dev/null +++ b/pkg/iac/scanners/ansible/inventory/init_test.go @@ -0,0 +1,46 @@ +package inventory + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRemoveComment(t *testing.T) { + tests := []struct { + in string + expected string + }{ + {"foo # bar", "foo"}, + {"foo ; bar", "foo"}, + {`foo "# not comment" bar`, `foo "# not comment" bar`}, + {"foo", "foo"}, + {" foo ", "foo"}, + {"", ""}, + } + + for _, tt := range tests { + got := removeComment(tt.in) + assert.Equal(t, tt.expected, got) + } +} + +func TestSplitFields(t *testing.T) { + tests := []struct { + in string + expected []string + }{ + {"foo bar baz", []string{"foo", "bar", "baz"}}, + {"foo bar\tbaz", []string{"foo", "bar", "baz"}}, + {`foo "bar baz" qux`, []string{"foo", "bar baz", "qux"}}, + {`foo 'bar baz'`, []string{"foo", "bar baz"}}, + {`foo bar\ baz`, []string{"foo", "bar baz"}}, + {`"foo"`, []string{"foo"}}, + {"", []string{}}, + } + + for _, tt := range tests { + got := splitFields(tt.in) + assert.ElementsMatch(t, tt.expected, got) + } +} diff --git a/pkg/iac/scanners/ansible/inventory/inventory.go b/pkg/iac/scanners/ansible/inventory/inventory.go new file mode 100644 index 000000000000..fed89e8cdea9 --- /dev/null +++ b/pkg/iac/scanners/ansible/inventory/inventory.go @@ -0,0 +1,238 @@ +package inventory + +import ( + "fmt" + "maps" + "slices" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +type Host struct { + Vars vars.Vars + Groups set.Set[string] +} + +func newHost(vars vars.Vars, groups set.Set[string]) *Host { + return &Host{Vars: vars, Groups: groups} +} + +func (h *Host) merge(other *Host) { + h.Vars = vars.MergeVars(h.Vars, other.Vars) + h.Groups = h.Groups.Union(other.Groups) +} + +type Group struct { + Vars vars.Vars + Parents set.Set[string] +} + +func newGroup(vars vars.Vars, parents set.Set[string]) *Group { + return &Group{Vars: vars, Parents: parents} +} + +func (g *Group) merge(other *Group) { + g.Vars = vars.MergeVars(g.Vars, other.Vars) + g.Parents = g.Parents.Union(other.Parents) +} + +type Inventory struct { + hosts map[string]*Host + groups map[string]*Group + + externalVars LoadedVars +} + +func newInventory() *Inventory { + return &Inventory{ + hosts: make(map[string]*Host), + groups: make(map[string]*Group), + externalVars: make(LoadedVars), + } +} + +func (inv *Inventory) addHost(name string, newHost *Host) { + if inv.hosts == nil { + inv.hosts = make(map[string]*Host) + } + + if h, ok := inv.hosts[name]; ok { + h.merge(newHost) + } else { + // Add new host + inv.hosts[name] = newHost + } +} + +func (inv *Inventory) addGroup(name string, newGroup *Group) { + if inv.groups == nil { + inv.groups = make(map[string]*Group) + } + + if g, exists := inv.groups[name]; exists { + g.merge(newGroup) + } else { + inv.groups[name] = newGroup + } +} + +// ResolveVars evaluates the effective variables for the given host, +// merging values from the host itself, its groups, and parent groups, +// according to Ansible variable precedence rules. +// https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_variables.html#understanding-variable-precedence +// TODO: Add support for "ansible_group_priority" +// See https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html#how-variables-are-merged +func (inv *Inventory) ResolveVars(hostName string, playbookVars LoadedVars) vars.Vars { + effective := make(vars.Vars) + + host, ok := inv.hosts[hostName] + if !ok { + log.WithPrefix("ansible").Debug("ResolveVars: host not found in inventory", + log.String("host", hostName)) + return nil + } + + groupsOrder, err := inv.groupTraversalOrder(hostName) + if err != nil { + log.WithPrefix("ansible").Debug("ResolveVars: failed to get group traversal order for host", + log.String("host", hostName), log.Err(err)) + return nil + } + + // Resolve internal group vars + for _, groupName := range groupsOrder { + if g, ok := inv.groups[groupName]; ok { + maps.Copy(effective, g.Vars) + } + } + + // Resolve extenral group_vars/all + mergeScopeVars(effective, inv.externalVars, ScopeGroupAll, "all") + // Resolve playbook group_vars/all + mergeScopeVars(effective, playbookVars, ScopeGroupAll, "all") + // Resolve external group_vars/* + mergeScopeVars(effective, inv.externalVars, ScopeGroupSpecific, groupsOrder...) + // Resolve playbook group_vars/* + mergeScopeVars(effective, playbookVars, ScopeGroupSpecific, groupsOrder...) + // Resolve internal host vars + maps.Copy(effective, host.Vars) + // Resolve external host_vars/* + mergeScopeVars(effective, inv.externalVars, ScopeHost, hostName) + // Resolve playbook host_vars/* + mergeScopeVars(effective, playbookVars, ScopeHost, hostName) + return effective +} + +func mergeScopeVars(effective vars.Vars, src LoadedVars, scope VarScope, keys ...string) { + s, ok := src[scope] + if !ok { + return + } + for _, key := range keys { + if v, exists := s[key]; exists { + maps.Copy(effective, v) + } + } +} + +func (inv *Inventory) groupTraversalOrder(hostName string) ([]string, error) { + visited := set.New[string]() + temp := set.New[string]() + order := make([]string, 0, len(inv.groups)) + + var visit func(string) error + visit = func(name string) error { + if temp.Contains(name) { + return fmt.Errorf("cycle detected in group hierarchy at %q", name) + } + if visited.Contains(name) { + return nil + } + + temp.Append(name) + group, ok := inv.groups[name] + if ok { + // // By default, Ansible merges groups at the same parent/child level in alphabetical order. + parents := group.Parents.Items() + slices.Sort(parents) + for _, parent := range parents { + if err := visit(parent); err != nil { + return err + } + } + } + temp.Remove(name) + visited.Append(name) + order = append(order, name) + return nil + } + + host, exists := inv.hosts[hostName] + if !exists { + return nil, fmt.Errorf("host %q not found", hostName) + } + + // By default, Ansible merges groups at the same parent/child level in alphabetical order. + sortedHostGroups := host.Groups.Items() + slices.Sort(sortedHostGroups) + for _, name := range sortedHostGroups { + if err := visit(name); err != nil { + return nil, err + } + } + + return order, nil +} + +// initDefaultGroups creates two default groups: "all" and "ungrouped". +// The "all" group contains all hosts. The "ungrouped" group contains all hosts +// that do not belong to any other group. +// See https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html#default-groups +func (inv *Inventory) initDefaultGroups() { + allGroup := newGroup(make(vars.Vars), set.New[string]()) + inv.addGroup("all", allGroup) + + ungroupedGroup := newGroup(make(vars.Vars), set.New("all")) + inv.addGroup("ungrouped", ungroupedGroup) + + for _, host := range inv.hosts { + if host.Groups.Size() == 0 { + host.Groups = set.New("ungrouped") + } + } + + for groupName, group := range inv.groups { + if groupName != "all" && group.Parents.Size() == 0 { + group.Parents = set.New("all") + } + } +} + +// applyVars applies a list of external variables to the inventory +func (inv *Inventory) applyVars(externalVars LoadedVars) { + inv.externalVars = externalVars +} + +// Merge combines several [Inventory] into one. +func (inv *Inventory) Merge(other *Inventory) { + // Merge hosts + for name, h := range other.hosts { + inv.addHost(name, h) + } + + // Merge groups + for name, g := range other.groups { + inv.addGroup(name, g) + } +} + +func newInlineInventory(hosts []string) *Inventory { + inv := &Inventory{} + for _, hostName := range hosts { + inv.addHost(hostName, newHost(make(vars.Vars), set.New[string]())) + } + inv.initDefaultGroups() + return inv +} diff --git a/pkg/iac/scanners/ansible/inventory/inventory_test.go b/pkg/iac/scanners/ansible/inventory/inventory_test.go new file mode 100644 index 000000000000..c2d76ea4c802 --- /dev/null +++ b/pkg/iac/scanners/ansible/inventory/inventory_test.go @@ -0,0 +1,301 @@ +package inventory_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/inventory" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" +) + +func groupVar(val any) vars.Variable { + return vars.NewVariable(val, vars.InvFileGroupPriority) +} + +func hostVar(val any) vars.Variable { + return vars.NewVariable(val, vars.InvFileHostPriority) +} + +func extAllGroupVar(val any) vars.Variable { + return vars.NewVariable(val, vars.InvExtAllGroupPriority) +} + +func extGroupVar(val any) vars.Variable { + return vars.NewVariable(val, vars.InvExtGroupPriority) +} + +func extHostVar(val any) vars.Variable { + return vars.NewVariable(val, vars.InvExtHostPriority) +} + +func TestInventory_ResolveVars_YAML(t *testing.T) { + src := ` +all: + vars: + location: dc1 + os: linux + +leafs1: + vars: + os: eos + role: leaf + hosts: + leaf01: + role: custom-leaf2 + tag: leafs1 + leaf02: + ansible_host: 192.0.2.110 + +leafs0: + vars: + os: nxos + hosts: + leaf01: + ansible_host: 192.0.2.100 + role: custom-leaf + +spines: + vars: + role: spine + hosts: + spine01: + ansible_host: 192.0.2.120 + spine02: + ansible_host: 192.0.2.130 + os: nxos + +network0: + vars: + environment: dev + children: + leafs0: + leafs1: + spines: + +network1: + vars: + environment: prod + children: + network0: + leafs1: + spines: + +webservers: + vars: + role: web + hosts: + webserver01: + ansible_host: 192.0.2.140 + webserver02: + ansible_host: 192.0.2.150 + +datacenter: + children: + network0: + network1: + webservers: +` + + inv, err := inventory.ParseYAML([]byte(src)) + require.NoError(t, err) + + tests := []struct { + hostName string + expected vars.Vars + }{ + { + hostName: "leaf01", + expected: vars.Vars{ + "location": groupVar("dc1"), + "tag": hostVar("leafs1"), + "os": groupVar("eos"), + "role": hostVar("custom-leaf"), + "environment": groupVar("dev"), + "ansible_host": hostVar("192.0.2.100"), + }, + }, + { + hostName: "leaf02", + expected: vars.Vars{ + "location": groupVar("dc1"), + "os": groupVar("eos"), + "role": groupVar("leaf"), + "environment": groupVar("dev"), + "ansible_host": hostVar("192.0.2.110"), + }, + }, + { + hostName: "spine01", + expected: vars.Vars{ + "location": groupVar("dc1"), + "os": groupVar("linux"), + "role": groupVar("spine"), + "environment": groupVar("dev"), + "ansible_host": hostVar("192.0.2.120"), + }, + }, + { + hostName: "spine02", + expected: vars.Vars{ + "location": groupVar("dc1"), + "os": hostVar("nxos"), + "role": groupVar("spine"), + "environment": groupVar("dev"), + "ansible_host": hostVar("192.0.2.130"), + }, + }, + { + hostName: "webserver01", + expected: vars.Vars{ + "location": groupVar("dc1"), + "os": groupVar("linux"), + "role": groupVar("web"), + "ansible_host": hostVar("192.0.2.140"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.hostName, func(t *testing.T) { + got := inv.ResolveVars(tt.hostName, make(inventory.LoadedVars)) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestInventory_ResolveVars_INI(t *testing.T) { + + src := ` +ungrouped1 ansible_host=10.0.0.99 description="standalone;# \"host" + +[ungrouped:vars] +# comment +; comment +http_port=8080 + +[all:vars] +ansible_user=global_user ; comment +timezone=UTC # comment + +[web] +web1 ansible_host=192.168.1.11 http_port=8080 +web2 ansible_host=192.168.1.12 + +[web:vars] +timezone = Europe/Stockholm +http_port=80 + +[db] +db1 ansible_host=192.168.1.21 +db2 ansible_host=192.168.1.22 db_engine=postgres + +[db:vars] +ansible_user= "db_admin" +backup_enabled=true + +[app:children] +web +db + +[app:vars] +timezone=Europe/Berlin +app_env=production + +[test] +test1 ansible_host=10.0.0.11 +test2 ansible_host=10.0.0.12 ansible_user=test_user + +[test:vars] +app_env=staging +http_port=8081 +` + + inv, err := inventory.ParseINI([]byte(src)) + require.NoError(t, err) + + tests := []struct { + hostName string + expected vars.Vars + }{ + { + hostName: "web1", + expected: vars.Vars{ + "ansible_user": groupVar("global_user"), + "timezone": groupVar("Europe/Stockholm"), + "http_port": hostVar("8080"), + "ansible_host": hostVar("192.168.1.11"), + "app_env": groupVar("production"), + }, + }, + { + hostName: "web2", + expected: vars.Vars{ + "ansible_user": groupVar("global_user"), + "timezone": groupVar("Europe/Stockholm"), + "http_port": groupVar("80"), + "ansible_host": hostVar("192.168.1.12"), + "app_env": groupVar("production"), + }, + }, + { + hostName: "db1", + expected: vars.Vars{ + "ansible_user": groupVar("\"db_admin\""), + "timezone": groupVar("Europe/Berlin"), + "backup_enabled": groupVar("true"), + "ansible_host": hostVar("192.168.1.21"), + "app_env": groupVar("production"), + }, + }, + { + hostName: "db2", + expected: vars.Vars{ + "ansible_user": groupVar("\"db_admin\""), + "timezone": groupVar("Europe/Berlin"), + "backup_enabled": groupVar("true"), + "db_engine": hostVar("postgres"), + "ansible_host": hostVar("192.168.1.22"), + "app_env": groupVar("production"), + }, + }, + { + hostName: "test1", + expected: vars.Vars{ + "ansible_user": groupVar("global_user"), + "timezone": groupVar("UTC"), + "http_port": groupVar("8081"), + "app_env": groupVar("staging"), + "ansible_host": hostVar("10.0.0.11"), + }, + }, + { + hostName: "test2", + expected: vars.Vars{ + "ansible_user": hostVar("test_user"), + "timezone": groupVar("UTC"), + "http_port": groupVar("8081"), + "app_env": groupVar("staging"), + "ansible_host": hostVar("10.0.0.12"), + }, + }, + { + hostName: "ungrouped1", + expected: vars.Vars{ + "ansible_user": groupVar("global_user"), + "timezone": groupVar("UTC"), + "description": hostVar("standalone;# \"host"), + "ansible_host": hostVar("10.0.0.99"), + "http_port": groupVar("8080"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.hostName, func(t *testing.T) { + got := inv.ResolveVars(tt.hostName, nil) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/ansible/inventory/inventory_vars.go b/pkg/iac/scanners/ansible/inventory/inventory_vars.go new file mode 100644 index 000000000000..c0e348eaf147 --- /dev/null +++ b/pkg/iac/scanners/ansible/inventory/inventory_vars.go @@ -0,0 +1,284 @@ +package inventory + +import ( + "bytes" + "encoding/json" + "io/fs" + "maps" + "path" + "path/filepath" + "slices" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +type VarScope int + +const ( + ScopeGroupAll VarScope = iota + ScopeGroupSpecific + ScopeHost +) + +func (s VarScope) String() string { + switch s { + case ScopeGroupAll: + return "group_vars/all (external)" + case ScopeGroupSpecific: + return "group_vars/* (external)" + case ScopeHost: + return "host_vars (external)" + default: + return "" + } +} + +func fileBaseName(p string) string { + return strings.TrimSuffix(filepath.Base(p), filepath.Ext(p)) +} + +func isAllGroup(path string) bool { + return fileBaseName(path) == "all" +} + +func notAllGroup(path string) bool { + return !isAllGroup(path) +} + +func InventoryVarsSources(fileSrc fsutils.FileSource) []VarsSource { + return []VarsSource{ + { + FileSrc: fileSrc.Join("group_vars"), + Scope: ScopeGroupAll, + Priority: vars.InvExtAllGroupPriority, + Match: isAllGroup, + }, + { + FileSrc: fileSrc.Join("group_vars"), + Scope: ScopeGroupSpecific, + Priority: vars.InvExtGroupPriority, + Match: notAllGroup, + }, + { + FileSrc: fileSrc.Join("host_vars"), + Scope: ScopeHost, + Priority: vars.InvExtHostPriority, + }, + } +} + +func PlaybookVarsSources(fileSrc fsutils.FileSource) []VarsSource { + return []VarsSource{ + { + FileSrc: fileSrc.Join("group_vars"), + Scope: ScopeGroupAll, + Priority: vars.PbExtAllGroupPriority, + Match: isAllGroup, + }, + { + FileSrc: fileSrc.Join("group_vars"), + Scope: ScopeGroupSpecific, + Priority: vars.PbExtGroupPriority, + Match: notAllGroup, + }, + { + FileSrc: fileSrc.Join("host_vars"), + Scope: ScopeHost, + Priority: vars.PbExtHostPriority, + }, + } +} + +type VarsSource struct { + FileSrc fsutils.FileSource + Scope VarScope // variables scope + Priority vars.VarPriority + Match func(path string) bool +} + +// LoadedVars stores all loaded variables organized by scope and key (host or group). +// The first map is by VarScope, the second by host/group name, each holding Vars. +type LoadedVars map[VarScope]map[string]vars.Vars + +func (v *LoadedVars) Merge(other LoadedVars) { + if *v == nil { + *v = make(LoadedVars) + } + for scope, objs := range other { + if (*v)[scope] == nil { + (*v)[scope] = make(map[string]vars.Vars) + } + for name, targetVars := range objs { + existing, ok := (*v)[scope][name] + if !ok { + (*v)[scope][name] = targetVars + continue + } + merged := make(vars.Vars) + maps.Copy(merged, existing) + maps.Copy(merged, targetVars) + (*v)[scope][name] = merged + } + } +} + +func LoadVars(sources []VarsSource) LoadedVars { + logger := log.WithPrefix("ansible") + allVars := make(LoadedVars) + + for _, src := range sources { + srcVars, err := LoadSourceVars(src) + if err != nil { + continue + } + + if allVars[src.Scope] == nil { + allVars[src.Scope] = make(map[string]vars.Vars) + } + + for key, v := range srcVars { + allVars[src.Scope][key] = vars.MergeVars(allVars[src.Scope][key], v) + + logger.Debug("Loaded vars from directory", + log.String("scope", src.Scope.String()), log.String("target", key)) + } + } + + return allVars +} + +func LoadSourceVars(src VarsSource) (map[string]vars.Vars, error) { + info, err := src.FileSrc.Stat() + if err != nil { + return nil, err + } + + result := make(map[string]vars.Vars) + + if info.IsDir() { + entries, err := listEntries(src.FileSrc) + if err != nil { + return nil, err + } + + fsutils.SortDirsFirstAlpha(entries) + + for _, e := range entries { + name := e.Name() + entrySrc := src.FileSrc.Join(name) + target := strings.TrimSuffix(name, path.Ext(name)) + + if src.Match != nil && !src.Match(entrySrc.Path) { + continue + } + + if e.IsDir() { + walkFn := func(fileSrc fsutils.FileSource, d fs.DirEntry) error { + if !d.IsDir() { + plain := processFile(fileSrc) + v := vars.NewVars(plain, src.Priority) + result[target] = vars.MergeVars(result[target], v) + } + return nil + } + if err := fsutils.WalkDirsFirstAlpha(entrySrc, walkFn); err != nil { + log.WithPrefix("ansible").Debug("Walk error", log.FilePath(entrySrc.Path)) + continue + } + } else { + plain := processFile(entrySrc) + v := vars.NewVars(plain, src.Priority) + result[target] = vars.MergeVars(result[target], v) + } + + } + } + + return result, nil +} + +func processFile(fileSrc fsutils.FileSource) vars.PlainVars { + if shouldSkipFile(fileSrc.Path) { + return nil + } + + v, err := readVars(fileSrc) + if err != nil { + log.WithPrefix("ansible").Debug("Failed to read vars", + log.FilePath(fileSrc.Path), log.Err(err)) + return nil + } + + return v +} + +// listEntries returns directory entries sorted alphabetically, +// with directories listed before files. +// +// At the top level, if a directory and a file share the same +// base name, the directory takes precedence and the file is ignored. +// +// For example, if "host_vars/group_vars" contains both "all/" +// and "all.yaml", "all.yaml" will be skipped in favor of the directory. +func listEntries(root fsutils.FileSource) ([]fs.DirEntry, error) { + entries, err := root.ReadDir() + if err != nil { + return nil, err + } + + dirs := set.New[string]() + for _, e := range entries { + if e.IsDir() { + dirs.Append(e.Name()) + } + } + + filtered := make([]fs.DirEntry, 0, len(entries)) + for _, e := range entries { + if !e.IsDir() { + name := strings.TrimSuffix(e.Name(), path.Ext(e.Name())) + if dirs.Contains(name) { + continue + } + } + filtered = append(filtered, e) + } + return filtered, nil +} + +func shouldSkipFile(filePath string) bool { + base := path.Base(filePath) + if strings.HasPrefix(base, ".") || strings.HasSuffix(base, "~") { + return true + } + if !slices.Contains(vars.VarFilesExtensions, filepath.Ext(base)) { + return true + } + return false +} + +func readVars(fileSrc fsutils.FileSource) (map[string]any, error) { + data, err := fileSrc.ReadFile() + if err != nil { + return nil, err + } + + var vars map[string]any + dataTrim := bytes.TrimSpace(data) + if len(dataTrim) > 0 && dataTrim[0] == '{' { + err = json.Unmarshal(dataTrim, &vars) + } else { + err = yaml.Unmarshal(dataTrim, &vars) + } + if err != nil { + return nil, err + } + + return vars, nil +} diff --git a/pkg/iac/scanners/ansible/inventory/inventory_vars_test.go b/pkg/iac/scanners/ansible/inventory/inventory_vars_test.go new file mode 100644 index 000000000000..80c9a681c9d5 --- /dev/null +++ b/pkg/iac/scanners/ansible/inventory/inventory_vars_test.go @@ -0,0 +1,71 @@ +package inventory_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/inventory" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" +) + +func TestLoader_Load(t *testing.T) { + files := map[string]string{ + "host_vars/host1.yaml": ` +var1: value1 +var2: value2 +`, + "host_vars/host2.yaml": ` +var1: value1 +var2: value2 +`, + "group_vars/group1.yaml": ` +group_var1: gvalue1 +group_var2: skipped +`, + "group_vars/group1/vars.yaml": ` +group_var1: gvalue1_1 +`, + "group_vars/group1/vars2.yaml": ` +group_var1: gvalue1_2 +`, + "group_vars/group1/first/vars2.yaml": ` +group_var1: gvalue1_0 +`, + "group_vars/all": ` +all_var: allvalue +`, + } + + fsys := testutil.CreateFS(files) + rootSrc := fsutils.NewFileSource(fsys, ".") + sources := inventory.InventoryVarsSources(rootSrc) + got := inventory.LoadVars(sources) + + expected := inventory.LoadedVars{ + inventory.ScopeHost: map[string]vars.Vars{ + "host1": { + "var1": extHostVar("value1"), + "var2": extHostVar("value2"), + }, + "host2": { + "var1": extHostVar("value1"), + "var2": extHostVar("value2"), + }, + }, + inventory.ScopeGroupAll: map[string]vars.Vars{ + "all": { + "all_var": extAllGroupVar("allvalue"), + }, + }, + inventory.ScopeGroupSpecific: map[string]vars.Vars{ + "group1": { + "group_var1": extGroupVar("gvalue1_2"), + }, + }, + } + + assert.Equal(t, expected, got) +} diff --git a/pkg/iac/scanners/ansible/inventory/load.go b/pkg/iac/scanners/ansible/inventory/load.go new file mode 100644 index 000000000000..6afb61667376 --- /dev/null +++ b/pkg/iac/scanners/ansible/inventory/load.go @@ -0,0 +1,305 @@ +package inventory + +import ( + "io/fs" + "os" + "path" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +const defaultHostsFile = "/etc/ansible/hosts" + +// InventorySource represents a source from which inventory data can be loaded. +type InventorySource interface { + isInventorySource() +} + +type InlineHostsSource struct { + // Hosts is a list of hosts provided directly in-memory instead of + // loading them from files. + Hosts []string +} + +func (InlineHostsSource) isInventorySource() {} + +type HostsDirsSource struct { + // Dirs is a list of paths to directories containing hosts files. + Dirs []fsutils.FileSource + // VarsDir is the path to the directory containing host_vars and group_vars. + VarsDir fsutils.FileSource +} + +func (HostsDirsSource) isInventorySource() {} + +type HostFileSource struct { + // File is a path to hosts file. + File fsutils.FileSource + // VarsDir is the path to the directory containing host_vars and group_vars. + VarsDir fsutils.FileSource +} + +func (HostFileSource) isInventorySource() {} + +type LoadOptions struct { + // InventoryPath is the path from the "inventory" config + // in ansible.cfg. + InventoryPath string + + // Sources are explicit inventory sources (CLI args, env vars, etc.). + Sources []string +} + +// LoadAuto resolves inventory sources from configuration, environment variables, +// and command-line flags, then loads the resulting inventory. +func LoadAuto(fsys fs.FS, opts LoadOptions) *Inventory { + sources, err := ResolveSources(fsys, opts) + if err != nil { + log.WithPrefix("ansible").Debug("Failed to resolve inventory sources", log.Err(err)) + } + + if len(sources) == 0 { + log.WithPrefix("ansible").Debug( + "No inventory sources provided, falling back to implicit host 'localhost'") + // https://docs.ansible.com/ansible/latest/inventory/implicit_localhost.html#implicit-localhost + return newInlineInventory([]string{"localhost"}) + } + + return LoadFromSources(sources) +} + +// ResolveSources resolves one or more Ansible inventory sources into a list of InventorySource. +// +// If `sources` is empty, the function falls back to defaults: +// - cfg.Inventory directory, if configured +// - "/etc/ansible/hosts" file, if no config is provided +// +// Inline host lists are detected by the presence of commas (e.g., "host1,host2"). +// +// The returned slice of InventorySource can be used to load hosts, group_vars, and host_vars +// for each inventory source. +func ResolveSources(fsys fs.FS, opts LoadOptions) ([]InventorySource, error) { + logger := log.WithPrefix("ansible") + if len(opts.Sources) == 0 { + if opts.InventoryPath != "" { + // TODO: This is comma-separated list of Ansible inventory sources + logger.Debug("Resolve inventory source from config", log.FilePath(opts.InventoryPath)) + fileSrc := fsutils.NewFileSource(fsys, opts.InventoryPath) + src, err := resolveSource(fileSrc, set.New[string]()) + if err != nil { + return nil, xerrors.Errorf("resolve source from config: %w", err) + } + return []InventorySource{src}, nil + } + return defaultInventorySources() + } + + var result []InventorySource + + seen := set.New[string]() + + for _, s := range opts.Sources { + logger.Debug("Resolve inventory source", log.String("source", s)) + fileSrc := fsutils.NewFileSource(fsys, s) + src, err := resolveSource(fileSrc, seen) + if err != nil { + return nil, err + } + result = append(result, src) + } + + return result, nil +} + +func makeHostFileSource(fileSrc fsutils.FileSource) InventorySource { + return HostFileSource{ + File: fileSrc, + VarsDir: fileSrc.Dir(), + } +} + +// defaultInventorySources returns sources from cfg or system defaults. +func defaultInventorySources() ([]InventorySource, error) { + // TODO: use ANSIBLE_INVENTORY env + if _, err := os.Stat(defaultHostsFile); err == nil { + log.WithPrefix("ansible").Debug("Use default hosts file", log.FilePath(defaultHostsFile)) + fileSrc := fsutils.NewFileSource(nil, defaultHostsFile) + return []InventorySource{makeHostFileSource(fileSrc)}, nil + } + return nil, nil +} + +// resolveSource resolves a single source path: file, dir, or dir tree. +func resolveSource(fileSrc fsutils.FileSource, seen set.Set[string]) (InventorySource, error) { + // TODO: handle inline host list, e.g. "host1,host2" + if looksLikeInlineHosts(fileSrc.Path) { + return resolveInlineHosts(fileSrc.Path) + } + + info, err := fileSrc.Stat() + if err != nil { + return nil, err + } + + if !info.IsDir() { + return makeHostFileSource(fileSrc), nil + } + + return walkInventoryDir(fileSrc, seen) +} + +// Stub: determine if path looks like inline host list +func looksLikeInlineHosts(path string) bool { + // simple heuristic: contains comma but no wildcard + return strings.Contains(path, ",") +} + +// Stub: resolve inline hosts +func resolveInlineHosts(path string) (InventorySource, error) { + hosts := strings.Split(path, ",") + if len(hosts) > 0 { + return InlineHostsSource{ + Hosts: hosts, + }, nil + } + return nil, nil +} + +// walkInventoryDir recursively walks a directory and returns all inventory dirs containing files. +func walkInventoryDir(fileSrc fsutils.FileSource, seen set.Set[string]) (InventorySource, error) { + result := HostsDirsSource{ + VarsDir: fileSrc, + } + + if err := fileSrc.WalkDirFS(func(fileSrc fsutils.FileSource, de fs.DirEntry) error { + if !de.IsDir() { + return nil + } + + // TODO: allow files with no extension or with the extensions .json, .yml, or .yaml + base := path.Base(fileSrc.Path) + if base == "group_vars" || base == "host_vars" { + // TODO: use fs.SkipDir? + return nil // skip vars directories + } + + hasFiles, err := dirHasFiles(fileSrc) + if err != nil { + log.WithPrefix("ansible").Debug("Failed to read directory", + log.FilePath(fileSrc.Path), log.Err(err)) + return nil + } + + if !hasFiles { + return nil + } + + cleanPath := path.Clean(fileSrc.Path) + if !seen.Contains(cleanPath) { + seen.Append(cleanPath) + result.Dirs = append(result.Dirs, fileSrc) + } + + return nil + }); err != nil { + return nil, xerrors.Errorf("walk dir: %w", err) + } + + return result, nil +} + +// dirHasFiles returns true if the directory contains at least one non-directory entry. +func dirHasFiles(fileSrc fsutils.FileSource) (bool, error) { + ents, err := fileSrc.ReadDir() + if err != nil { + return false, err + } + for _, e := range ents { + if !e.IsDir() { + return true, nil + } + } + return false, nil +} + +// LoadFromSources loads inventory files or directories from +// the given sources and merges them into a single Inventory. +// +// When multiple inventory sources are provided, Ansible merges +// variables in the order the sources are specified. +// See https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html#managing-inventory-variable-load-order +func LoadFromSources(sources []InventorySource) *Inventory { + logger := log.WithPrefix("ansible") + + res := newInventory() + externalVars := make(LoadedVars) + + for _, source := range sources { + + // Ansible loads host and group variable files by searching paths + // relative to the inventory source. + // See https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html#organizing-host-and-group-variables + switch src := source.(type) { + case InlineHostsSource: + logger.Debug("Processing inline hosts source", log.Any("hosts", src.Hosts)) + inv := newInlineInventory(src.Hosts) + res.Merge(inv) + case HostFileSource: + inv, err := readAndParseHosts(src.File) + if err != nil { + logger.Debug("Failed to parse hosts file", + log.FilePath(src.File.Path), log.Err(err)) + continue + } + res.Merge(inv) + logger.Debug("Loaded hosts file", log.FilePath(src.File.Path)) + + vars := LoadVars(InventoryVarsSources(src.VarsDir)) + externalVars.Merge(vars) + case HostsDirsSource: + for _, hostsDirSrc := range src.Dirs { + entries, err := hostsDirSrc.ReadDir() + if err != nil { + logger.Debug("Failed to read dir with hosts files", + log.FilePath(hostsDirSrc.Path), log.Err(err)) + continue + } + + for _, entry := range entries { + hostFileSrc := hostsDirSrc.Join(entry.Name()) + inv, err := readAndParseHosts(hostFileSrc) + if err != nil { + logger.Debug("Failed to parse hosts file", + log.FilePath(hostFileSrc.Path), log.Err(err)) + continue + } + res.Merge(inv) + logger.Debug("Loaded hosts file", log.FilePath(hostFileSrc.Path)) + } + } + vars := LoadVars(InventoryVarsSources(src.VarsDir)) + externalVars.Merge(vars) + } + } + + res.applyVars(externalVars) + return res +} + +func readAndParseHosts(fileSrc fsutils.FileSource) (*Inventory, error) { + b, err := fileSrc.ReadFile() + if err != nil { + return nil, err + } + + if inv, err := ParseYAML(b); err == nil { + return inv, nil + } + + return ParseINI(b) +} diff --git a/pkg/iac/scanners/ansible/inventory/load_test.go b/pkg/iac/scanners/ansible/inventory/load_test.go new file mode 100644 index 000000000000..bdf095b6dd25 --- /dev/null +++ b/pkg/iac/scanners/ansible/inventory/load_test.go @@ -0,0 +1,345 @@ +package inventory_test + +import ( + "io/fs" + "os" + "runtime" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/inventory" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" +) + +func TestLoadAuto(t *testing.T) { + files := map[string]string{ + + "dev/hosts": ` +group1: + hosts: + host1: + baz: 10 + host2: + bar: 15 + vars: + bar: 10 +`, + + // host vars + "dev/host_vars/host1.yaml": ` +foo: 1 +bar: 2 +`, + "dev/host_vars/host2.yaml": ` +foo: 10 +baz: 20 +`, + + // group vars + "dev/group_vars/group1.yaml": ` +common: "yes" +foo: 5 +`, + + // test inventory + "common/hosts": ` +group1: + hosts: + host1: + common: 10 +`, + + // common + "common/group_vars/group1.yaml": ` +foo: 5 +`, + + "common/host_vars/host1.yaml": ` +foo: 10 +`, + } + + fsys := testutil.CreateFS(files) + inv := inventory.LoadAuto(fsys, inventory.LoadOptions{ + Sources: []string{"dev", "common"}, + }) + + tests := []struct { + name string + hostName string + expected vars.Vars + }{ + { + hostName: "host1", + expected: vars.Vars{ + "foo": extHostVar(10), // external host_var from common + "bar": extHostVar(2), // external host_var override file group + "baz": hostVar(10), // file host + "common": hostVar(10), // host var from common + }, + }, + { + hostName: "host2", + expected: vars.Vars{ + "foo": extHostVar(10), // external host_var override external group + "bar": hostVar(15), // file host + "baz": extHostVar(20), // external host_var override file host + "common": extGroupVar("yes"), // from group_vars + }, + }, + } + + for _, tt := range tests { + t.Run(tt.hostName, func(t *testing.T) { + got := inv.ResolveVars(tt.hostName, make(inventory.LoadedVars)) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestLoadAuto_AbsolutePath(t *testing.T) { + // The process cannot access the file because it is being used by another process. + if runtime.GOOS == "windows" { + t.Skip("TODO") + } + + tmpFile, err := os.CreateTemp(t.TempDir(), "hosts-*.yml") + require.NoError(t, err) + + _, err = tmpFile.WriteString(` +group1: + hosts: + host1: + baz: 5 + vars: + bar: 20 +`) + require.NoError(t, err) + + files := map[string]string{ + + "dev/hosts": ` +group1: + hosts: + host1: + baz: 10 + host2: + bar: 15 + vars: + bar: 10 +`, + } + fsys := testutil.CreateFS(files) + + opts := inventory.LoadOptions{ + Sources: []string{"dev", tmpFile.Name()}, + } + + inv := inventory.LoadAuto(fsys, opts) + + tests := []struct { + name string + hostName string + expected vars.Vars + }{ + { + hostName: "host1", + expected: vars.Vars{ + "bar": groupVar(20), // from second file (group) + "baz": hostVar(5), // from second file (host) + }, + }, + { + hostName: "host2", + expected: vars.Vars{ + "bar": hostVar(15), // from first file (host) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.hostName, func(t *testing.T) { + got := inv.ResolveVars(tt.hostName, make(inventory.LoadedVars)) + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestLoadAuto_EmptySources(t *testing.T) { + inv := inventory.LoadAuto(fstest.MapFS{}, inventory.LoadOptions{}) + + localhostVars := vars.Vars{ + "foo": hostVar("test"), + } + got := inv.ResolveVars("localhost", inventory.LoadedVars{ + inventory.ScopeHost: map[string]vars.Vars{ + "localhost": localhostVars, + }, + }) + + assert.Equal(t, localhostVars, got) +} + +func TestLoadAuto_NonExistentSource(t *testing.T) { + opts := inventory.LoadOptions{ + InventoryPath: "nonexistent", + } + + inv := inventory.LoadAuto(fstest.MapFS{}, opts) + localhostVars := vars.Vars{ + "foo": hostVar("test"), + } + got := inv.ResolveVars("localhost", inventory.LoadedVars{ + inventory.ScopeHost: map[string]vars.Vars{ + "localhost": localhostVars, + }, + }) + + assert.Equal(t, localhostVars, got) +} + +func TestResolveInventorySources(t *testing.T) { + + files := map[string]*fstest.MapFile{ + "emptydir": { + Mode: fs.ModeDir, + }, + "hosts.yml": {}, + "inv/hosts.yml": {}, + "inv/group_vars/group1.yml": {}, + "inv/one/hosts.yml": {}, + "inv/one/host_vars/hosts1.yml": {}, + "inv/two/hosts.yml": {}, + "inv/two/group_vars/group1.yml": {}, + } + + fsys := fstest.MapFS(files) + rootSrc := fsutils.NewFileSource(fsys, ".") + + tests := []struct { + name string + opts inventory.LoadOptions + expected []inventory.InventorySource + }{ + { + name: "single file", + opts: inventory.LoadOptions{Sources: []string{"hosts.yml"}}, + expected: []inventory.InventorySource{ + inventory.HostFileSource{ + File: rootSrc.Join("hosts.yml"), + VarsDir: rootSrc, + }, + }, + }, + { + name: "single file in subdirectory", + opts: inventory.LoadOptions{Sources: []string{"inv/hosts.yml"}}, + expected: []inventory.InventorySource{ + inventory.HostFileSource{ + File: rootSrc.Join("inv", "hosts.yml"), + VarsDir: rootSrc.Join("inv"), + }, + }, + }, + { + name: "nested directories with vars", + opts: inventory.LoadOptions{Sources: []string{"inv"}}, + expected: []inventory.InventorySource{ + inventory.HostsDirsSource{ + Dirs: []fsutils.FileSource{ + rootSrc.Join("inv"), + rootSrc.Join("inv", "one"), + rootSrc.Join("inv", "two"), + }, + VarsDir: rootSrc.Join("inv"), + }, + }, + }, + { + name: "inline hosts list", + opts: inventory.LoadOptions{Sources: []string{"host1,host2"}}, + expected: []inventory.InventorySource{ + inventory.InlineHostsSource{Hosts: []string{"host1", "host2"}}, + }, + }, + { + name: "empty sources with cfg path", + opts: inventory.LoadOptions{ + InventoryPath: "hosts.yml", + }, + expected: []inventory.InventorySource{ + inventory.HostFileSource{File: rootSrc.Join("hosts.yml"), VarsDir: rootSrc}, + }, + }, + { + name: "directory without hosts files", + opts: inventory.LoadOptions{Sources: []string{"emptydir"}}, + expected: []inventory.InventorySource{ + inventory.HostsDirsSource{VarsDir: rootSrc.Join("emptydir")}, + }, + }, + { + name: "multiple sources", + opts: inventory.LoadOptions{Sources: []string{"inv/one", "inv/two"}}, + expected: []inventory.InventorySource{ + inventory.HostsDirsSource{ + Dirs: []fsutils.FileSource{rootSrc.Join("inv", "one")}, + VarsDir: rootSrc.Join("inv", "one"), + }, + inventory.HostsDirsSource{ + Dirs: []fsutils.FileSource{rootSrc.Join("inv", "two")}, + VarsDir: rootSrc.Join("inv", "two"), + }, + }, + }, + { + name: "no sources", + opts: inventory.LoadOptions{}, + expected: []inventory.InventorySource{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, err := inventory.ResolveSources(fsys, tt.opts) + require.NoError(t, err) + require.ElementsMatch(t, tt.expected, res) + }) + } +} + +func TestResolveInventorySources_AbsolutePath(t *testing.T) { + // The process cannot access the file because it is being used by another process. + if runtime.GOOS == "windows" { + t.Skip("TODO") + } + + // create a temporary inventory file + tmpFile, err := os.CreateTemp(t.TempDir(), "hosts-*.yml") + require.NoError(t, err) + + opts := inventory.LoadOptions{ + InventoryPath: tmpFile.Name(), + } + + fsys := fstest.MapFS{} + + res, err := inventory.ResolveSources(fsys, opts) + require.NoError(t, err) + + fileSrc := fsutils.NewFileSource(nil, tmpFile.Name()) + + expected := []inventory.InventorySource{ + inventory.HostFileSource{ + File: fileSrc, + VarsDir: fileSrc.Dir(), + }, + } + + require.ElementsMatch(t, expected, res) +} diff --git a/pkg/iac/scanners/ansible/inventory/yaml.go b/pkg/iac/scanners/ansible/inventory/yaml.go new file mode 100644 index 000000000000..29767dbf8b0d --- /dev/null +++ b/pkg/iac/scanners/ansible/inventory/yaml.go @@ -0,0 +1,59 @@ +package inventory + +import ( + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/orderedmap" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + "github.com/aquasecurity/trivy/pkg/set" +) + +type rawGroup struct { + Hosts map[string]vars.PlainVars `yaml:"hosts"` + Children orderedmap.OrderedMap[string, rawGroup] `yaml:"children"` + Vars vars.PlainVars `yaml:"vars"` +} + +func ParseYAML(data []byte) (*Inventory, error) { + var raw orderedmap.OrderedMap[string, rawGroup] + if err := yaml.Unmarshal(data, &raw); err != nil { + return nil, xerrors.Errorf("unmarshal inventory yaml: %w", err) + } + + inv := newInventory() + + for groupName, groupRaw := range raw.Iter() { + if err := parseGroup(groupName, groupRaw, inv, nil); err != nil { + return nil, err + } + } + + inv.initDefaultGroups() + return inv, nil +} + +// parseGroup recursively parses a rawGroup and adds it to Inventory +func parseGroup(name string, rg rawGroup, inv *Inventory, parents []string) error { + // Add group + groupVars := vars.NewVars(rg.Vars, vars.InvFileGroupPriority) + newGroup := newGroup(groupVars, set.New(parents...)) + inv.addGroup(name, newGroup) + + // Add hosts + // A host can be in multiple groups, but Ansible processes only one instance of the host at runtime. + // Ansible merges the data from multiple groups. + for hostName, plainHostVars := range rg.Hosts { + groups := set.New(append(parents, name)...) + // TODO: support for host ranges, e.g. www[01:50:2].example.com + // https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html#adding-ranges-of-hosts + hostVars := vars.NewVars(plainHostVars, vars.InvFileHostPriority) + inv.addHost(hostName, newHost(hostVars, groups)) + } + + // Recursively parse children groups + for childName, childRg := range rg.Children.Iter() { + parseGroup(childName, childRg, inv, append(parents, name)) + } + return nil +} diff --git a/pkg/iac/scanners/ansible/orderedmap/orderedmap.go b/pkg/iac/scanners/ansible/orderedmap/orderedmap.go new file mode 100644 index 000000000000..249e9a2697d0 --- /dev/null +++ b/pkg/iac/scanners/ansible/orderedmap/orderedmap.go @@ -0,0 +1,93 @@ +package orderedmap + +import ( + "iter" + "maps" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" +) + +// OrderedMap is a map that preserves insertion order of keys. +type OrderedMap[K comparable, V any] struct { + keys []K + data map[K]V +} + +func New[K comparable, V any](capacity int) *OrderedMap[K, V] { + return &OrderedMap[K, V]{ + keys: make([]K, 0, capacity), + data: make(map[K]V, capacity), + } +} + +func NewWithData[K comparable, V any](keys []K, data map[K]V) *OrderedMap[K, V] { + return &OrderedMap[K, V]{ + keys: keys, + data: data, + } +} + +// Len returns the number of entires in the map +func (m *OrderedMap[K, V]) Len() int { + return len(m.keys) +} + +func (m *OrderedMap[K, V]) Get(key K) (V, bool) { + val, ok := m.data[key] + return val, ok +} + +func (m *OrderedMap[K, V]) Set(key K, value V) { + if _, exists := m.data[key]; !exists { + m.keys = append(m.keys, key) + } + m.data[key] = value +} + +// Iter returns an iterator over the map +func (m *OrderedMap[K, V]) Iter() iter.Seq2[K, V] { + return func(yield func(K, V) bool) { + for _, k := range m.keys { + v := m.data[k] + if !yield(k, v) { + return + } + } + } +} + +func (m *OrderedMap[K, V]) AsMap() map[K]V { + return maps.Clone(m.data) +} + +func (m *OrderedMap[K, V]) UnmarshalYAML(n *yaml.Node) error { + if n.Kind != yaml.MappingNode { + return xerrors.Errorf("expected map node, got %s", n.Tag) + } + + if len(n.Content)%2 != 0 { + return xerrors.New("invalid map node content length") + } + + size := len(n.Content) / 2 + m.keys = make([]K, 0, size) + m.data = make(map[K]V, size) + + for i := 0; i < len(n.Content); i += 2 { + keyNode, valueNode := n.Content[i], n.Content[i+1] + + var key K + if err := keyNode.Decode(&key); err != nil { + return err + } + m.keys = append(m.keys, key) + + var value V + if err := valueNode.Decode(&value); err != nil { + return err + } + m.data[key] = value + } + return nil +} diff --git a/pkg/iac/scanners/ansible/orderedmap/orderedmap_test.go b/pkg/iac/scanners/ansible/orderedmap/orderedmap_test.go new file mode 100644 index 000000000000..f9c49cb2848b --- /dev/null +++ b/pkg/iac/scanners/ansible/orderedmap/orderedmap_test.go @@ -0,0 +1,57 @@ +package orderedmap_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/orderedmap" +) + +func makeSampleMap() *orderedmap.OrderedMap[string, int] { + m := orderedmap.New[string, int](3) + m.Set("a", 1) + m.Set("b", 2) + m.Set("c", 3) + return m +} + +func TestOrderedMap_UnmarshalYAML(t *testing.T) { + yamlData := ` +a: 1 +b: 2 +c: 3 +` + var om orderedmap.OrderedMap[string, int] + err := yaml.Unmarshal([]byte(yamlData), &om) + require.NoError(t, err) + + m := make(map[string]int) + var keys []string + for k, v := range om.Iter() { + m[k] = v + keys = append(keys, k) + } + + expectedMap := map[string]int{"a": 1, "b": 2, "c": 3} + assert.Equal(t, expectedMap, m) + + expectedKeys := []string{"a", "b", "c"} + assert.Equal(t, expectedKeys, keys) +} + +func TestOrderedMap_Iter(t *testing.T) { + m := makeSampleMap() + + collected := make(map[string]int) + var order []string + for k, v := range m.Iter() { + collected[k] = v + order = append(order, k) + } + + assert.Equal(t, map[string]int{"a": 1, "b": 2, "c": 3}, collected) + assert.Equal(t, []string{"a", "b", "c"}, order) +} diff --git a/pkg/iac/scanners/ansible/parser/config.go b/pkg/iac/scanners/ansible/parser/config.go new file mode 100644 index 000000000000..c1230d4fd65d --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/config.go @@ -0,0 +1,85 @@ +package parser + +import ( + "errors" + "io/fs" + "os" + "path" + "path/filepath" + + "github.com/go-ini/ini" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +type AnsibleConfig struct { + Inventory string +} + +func LoadConfig(fsys fs.FS, dir string) (AnsibleConfig, error) { + logger := log.WithPrefix("ansible") + // https://docs.ansible.com/ansible/latest/reference_appendices/config.html#the-configuration-file + cfgPaths := []struct { + path string + useOS bool + }{ + {os.Getenv("ANSIBLE_CONFIG"), true}, + {path.Join(dir, "ansible.cfg"), false}, + {filepath.Join(fsutils.HomeDir(), ".ansible.cfg"), true}, + {"/etc/ansible/ansible.cfg", true}, + } + + for _, p := range cfgPaths { + if p.path == "" { + continue + } + + var b []byte + var err error + + logger.Debug("Trying config", log.FilePath(p.path)) + + if p.useOS { + b, err = os.ReadFile(p.path) + } else { + b, err = fs.ReadFile(fsys, p.path) + } + + if errors.Is(err, fs.ErrNotExist) { + continue + } + if err != nil { + return AnsibleConfig{}, xerrors.Errorf("read file %q: %w", p.path, err) + } + + cfg, err := parseConfig(b) + if err != nil { + return AnsibleConfig{}, xerrors.Errorf("parse config %q: %w", p.path, err) + } + + logger.Debug("Loaded config", log.FilePath(p.path)) + return cfg, nil + } + + logger.Debug("No config found in search paths") + return AnsibleConfig{}, nil +} + +func parseConfig(b []byte) (AnsibleConfig, error) { + // TODO: expand vars using os.ExpandEnv() ? + f, err := ini.Load(b) + if err != nil { + return AnsibleConfig{}, xerrors.Errorf("load config file: %w", err) + } + + cfg := AnsibleConfig{} + + section := f.Section("defaults") + if k, err := section.GetKey("inventory"); err == nil { + // TODO: trim spaces ? + cfg.Inventory = k.String() + } + return cfg, nil +} diff --git a/pkg/iac/scanners/ansible/parser/discovery.go b/pkg/iac/scanners/ansible/parser/discovery.go new file mode 100644 index 000000000000..36e9e8ec16b7 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/discovery.go @@ -0,0 +1,89 @@ +package parser + +import ( + "fmt" + "io/fs" + "path" + "slices" + + "github.com/bmatcuk/doublestar/v4" +) + +// FindProjects locates Ansible project roots within fsys starting from root. +// A directory is recognized as a project root if it contains key files or directories +// like ansible.cfg, inventory, group_vars, host_vars, roles, playbooks, or YAML playbooks. +// +// Returns a slice of project root paths. +func FindProjects(fsys fs.FS, root string) ([]string, error) { + var roots []string + walkFn := func(filePath string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if !d.IsDir() { + return nil + } + + if isAnsibleProject(fsys, filePath) { + roots = append(roots, filePath) + return fs.SkipDir + } + + return nil + } + + if err := fs.WalkDir(fsys, root, walkFn); err != nil { + return nil, fmt.Errorf("walk dir: %w", err) + } + + return roots, nil +} + +func isAnsibleProject(fsys fs.FS, dir string) bool { + anchors := []string{ + "ansible.cfg", + "inventory", "group_vars", "host_vars", "roles", "playbooks", + } + + for _, name := range anchors { + if pathExists(fsys, path.Join(dir, name)) { + return true + } + } + + if entries, err := doublestar.Glob(fsys, dir+"/roles/**/{tasks,defaults,vars}"); err == nil && len(entries) > 0 { + return true + } + + if entries, err := doublestar.Glob(fsys, dir+"/*.{yml,yaml}"); err == nil && len(entries) > 0 { + for _, entry := range entries { + if isPlaybookFile(fsys, entry) { + return true + } + } + } + + return false +} + +func isPlaybookFile(fsys fs.FS, filePath string) bool { + data, err := fs.ReadFile(fsys, filePath) + if err != nil { + return false + } + + var plays []*Play + if err := decodeYAML(data, &plays); err != nil { + return false + } + + return slices.ContainsFunc(plays, func(play *Play) bool { + return play.Hosts() != "" + }) +} + +func pathExists(fsys fs.FS, filePath string) bool { + _, err := fs.Stat(fsys, filePath) + return err == nil +} diff --git a/pkg/iac/scanners/ansible/parser/discovery_test.go b/pkg/iac/scanners/ansible/parser/discovery_test.go new file mode 100644 index 000000000000..5f78659c5cfa --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/discovery_test.go @@ -0,0 +1,72 @@ +package parser_test + +import ( + "io/fs" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/parser" +) + +func TestFindProjects(t *testing.T) { + tests := []struct { + name string + fsys fs.FS + dir string + expected []string + }{ + { + name: "single project with ansible.cfg", + fsys: fstest.MapFS{ + "project1/ansible.cfg": &fstest.MapFile{Data: []byte("[defaults]\n")}, + "project1/inventory/hosts": &fstest.MapFile{Data: []byte("[all]\nlocalhost\n")}, + }, + dir: "project1", + expected: []string{"project1"}, + }, + { + name: "no projects", + fsys: fstest.MapFS{ + "random/file.txt": &fstest.MapFile{Data: []byte("hello")}, + }, + dir: ".", + expected: nil, + }, + { + name: "project detected by playbook yaml", + fsys: fstest.MapFS{ + "proj/main.yml": &fstest.MapFile{Data: []byte(`- hosts: all + tasks: + - debug: msg=hello + `)}, + }, + dir: "proj", + expected: []string{"proj"}, + }, + { + name: "nested projects", + fsys: fstest.MapFS{ + "proj1/ansible.cfg": &fstest.MapFile{Data: []byte("[defaults]\n")}, + "proj2/main.yaml": &fstest.MapFile{Data: []byte(`- hosts: all + tasks: + - debug: msg=hello +`)}, + "proj1/roles/role1/tasks/main.yml": &fstest.MapFile{Data: []byte("- debug: msg=ok")}, + }, + dir: ".", + expected: []string{"proj1", "proj2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parser.FindProjects(tt.fsys, tt.dir) + require.NoError(t, err) + + assert.ElementsMatch(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/ansible/parser/module.go b/pkg/iac/scanners/ansible/parser/module.go new file mode 100644 index 000000000000..c16b3a763f23 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/module.go @@ -0,0 +1,30 @@ +package parser + +// Module represents a logical module in a playbook or task. +// It wraps a Node and provides module-specific utility methods. +// +// All the data and metadata for the module is stored in the embedded Node. +type Module struct { + *Node + + Name string +} + +// IsFreeForm returns true if the module is a free-form Ansible module. +// In Ansible, a free-form module is called using a single scalar value +// instead of a key-value mapping. +// +// Example: +// +// # Free-form +// - command: echo "Hello" +// # IsFreeForm() -> true +// +// # Structured +// - ansible.builtin.yum: +// name: vim +// state: present +// # IsFreeForm() -> false +func (m *Module) IsFreeForm() bool { + return m.Node.IsString() +} diff --git a/pkg/iac/scanners/ansible/parser/node.go b/pkg/iac/scanners/ansible/parser/node.go new file mode 100644 index 000000000000..42bc1bde14a6 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/node.go @@ -0,0 +1,544 @@ +package parser + +import ( + "cmp" + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/go-multierror" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/orderedmap" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/set" +) + +type NodeValue interface { + MarshalYAML() (any, error) + + nodeValueMarker() +} + +type Scalar struct { + Val any +} + +func (s *Scalar) MarshalYAML() (any, error) { + if s.Val == nil { + return nil, nil + } + return s.Val, nil +} + +type Mapping struct { + Fields *orderedmap.OrderedMap[string, *Node] +} + +func (m *Mapping) MarshalYAML() (any, error) { + node := &yaml.Node{ + Kind: yaml.MappingNode, + Tag: "!!map", + Content: nil, + } + for key, child := range m.Fields.Iter() { + keyNode := &yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Value: key, + } + valYAML, err := child.MarshalYAML() + if err != nil { + return nil, err + } + valNode := &yaml.Node{} + if err := valNode.Encode(valYAML); err != nil { + return nil, err + } + + node.Content = append(node.Content, keyNode, valNode) + } + return node, nil +} + +type Sequence struct { + Items []*Node +} + +func (s *Sequence) MarshalYAML() (any, error) { + node := &yaml.Node{ + Kind: yaml.SequenceNode, + Tag: "!!seq", + Content: nil, + } + for _, item := range s.Items { + itemYAML, err := item.MarshalYAML() + if err != nil { + return nil, err + } + itemNode := &yaml.Node{} + if err := itemNode.Encode(itemYAML); err != nil { + return nil, err + } + node.Content = append(node.Content, itemNode) + } + return node, nil +} + +func (s *Scalar) nodeValueMarker() {} +func (m *Mapping) nodeValueMarker() {} +func (s *Sequence) nodeValueMarker() {} + +type Node struct { + rng Range + metadata iacTypes.Metadata + val NodeValue + + unknown bool +} + +func (n *Node) Metadata() iacTypes.Metadata { + return n.metadata +} + +func (n *Node) IsKnown() bool { + return !n.unknown +} + +func (n *Node) UnmarshalYAML(node *yaml.Node) error { + n.rng = rangeFromNode(node) + + switch node.Kind { + case yaml.ScalarNode: + switch node.Tag { + case "!!int": + v, err := strconv.Atoi(node.Value) + if err == nil { + n.val = &Scalar{Val: v} + } + case "!!float": + // TODO: handle float properly + case "!!bool": + v, err := strconv.ParseBool(node.Value) + if err == nil { + n.val = &Scalar{Val: v} + } + case StrTag, "!!string": + n.val = &Scalar{Val: node.Value} + } + return nil + case yaml.MappingNode: + n.rng.Start-- + childData, err := decodeMapNode(node) + if err != nil { + return err + } + n.val = childData + return nil + case yaml.SequenceNode: + n.rng.Start-- + childData, err := decodeSequenceNode(node) + if err != nil { + return err + } + n.val = childData + return nil + } + return nil +} + +func decodeMapNode(node *yaml.Node) (*Mapping, error) { + childMap := orderedmap.New[string, *Node](len(node.Content) / 2) + + for i := 0; i < len(node.Content); i += 2 { + keyNode, valueNode := node.Content[i], node.Content[i+1] + + childNode, err := decodeChildNode(valueNode) + if err != nil { + return nil, err + } + + childMap.Set(keyNode.Value, &childNode) + } + + return &Mapping{Fields: childMap}, nil +} + +func decodeSequenceNode(node *yaml.Node) (*Sequence, error) { + items := make([]*Node, 0, len(node.Content)) + + for _, elemNode := range node.Content { + childNode, err := decodeChildNode(elemNode) + if err != nil { + return nil, err + } + if elemNode.Kind == yaml.MappingNode { + childNode.rng.Start++ + } + items = append(items, &childNode) + } + + return &Sequence{Items: items}, nil +} + +func decodeChildNode(yNode *yaml.Node) (Node, error) { + if yNode.Kind == yaml.ScalarNode && yNode.Tag == NullTag { + return Node{ + rng: rangeFromNode(yNode), + val: nil, + }, nil + } + + var n Node + if err := yNode.Decode(&n); err != nil { + return Node{}, err + } + return n, nil +} + +func (n *Node) initMetadata(fileSrc fsutils.FileSource, parent *iacTypes.Metadata, nodePath []string) { + fsys, relPath := fileSrc.FSAndRelPath() + ref := cmp.Or(strings.Join(nodePath, "."), ".") + rng := iacTypes.NewRange(relPath, n.rng.Start, n.rng.End, "", fsys) + + n.metadata = iacTypes.NewMetadata(rng, ref) + n.metadata.SetParentPtr(parent) + + switch val := n.val.(type) { + case *Mapping: + for key, attr := range val.Fields.Iter() { + if attr == nil { + continue + } + childPath := append(nodePath, key) + attr.initMetadata(fileSrc, parent, childPath) + } + case *Sequence: + for idx, attr := range val.Items { + if attr == nil { + continue + } + childPath := append(nodePath, fmt.Sprintf("[%d]", idx)) + attr.initMetadata(fileSrc, parent, childPath) + } + } +} + +func (n *Node) Render(variables vars.Vars) (*Node, error) { + return n.render(variables, set.New[string]()) +} + +func (n *Node) render(variables vars.Vars, visited set.Set[string]) (*Node, error) { + if n == nil { + return nil, nil + } + + switch v := n.val.(type) { + case *Scalar: + if s, ok := v.Val.(string); ok { + if found := visited.Contains(s); found { + return n, fmt.Errorf("cyclic reference detected: %q", s) + } + + visited.Append(s) + rendered, err := evaluateTemplate(s, variables) + if err != nil { + n.unknown = true + return n, fmt.Errorf("node ref %q: %w", n.metadata.Reference(), err) + } + + newNode := n.withValue(&Scalar{Val: rendered}) + if strings.Contains(rendered, "{{") { + return newNode.render(variables, visited) + } + visited.Remove(s) + + return newNode, nil + } + return n, nil + case *Mapping: + var errs error + fields := orderedmap.New[string, *Node](v.Fields.Len()) + for key, val := range v.Fields.Iter() { + r, err := val.Render(variables) + if err != nil { + errs = multierror.Append(err) + } + fields.Set(key, r) + } + return n.withValue(&Mapping{Fields: fields}), errs + case *Sequence: + var errs error + items := make([]*Node, 0, len(v.Items)) + for _, val := range v.Items { + r, err := val.Render(variables) + if err != nil { + errs = multierror.Append(err) + } + items = append(items, r) + } + return n.withValue(&Sequence{Items: items}), errs + default: + return n, nil + } +} + +func (n *Node) withValue(val NodeValue) *Node { + return &Node{val: val, rng: n.rng, metadata: n.metadata} +} + +func (n *Node) MarshalYAML() (any, error) { + if n.val == nil { + return nil, nil + } + return n.val.MarshalYAML() +} + +func (n *Node) IsNil() bool { + return n == nil || n.val == nil +} + +func (n *Node) IsMap() bool { + return safeOp(n, func(nv NodeValue) bool { + _, ok := nv.(*Mapping) + return ok + }) +} + +func (n *Node) IsList() bool { + return safeOp(n, func(nv NodeValue) bool { + _, ok := nv.(*Sequence) + return ok + }) +} + +func (n *Node) IsBool() bool { + return checkScalarType[bool](n) +} + +func (n *Node) IsString() bool { + return checkScalarType[string](n) +} + +func (n *Node) ToList() []*Node { + return safeOp(n, func(nv NodeValue) []*Node { + val, ok := nv.(*Sequence) + if !ok || val == nil { + return nil + } + + return val.Items + }) +} + +func (n *Node) ToMap() map[string]*Node { + return safeOp(n, func(nv NodeValue) map[string]*Node { + val, ok := nv.(*Mapping) + if !ok || val == nil { + return make(map[string]*Node) + } + + return val.Fields.AsMap() + }) +} + +func (n *Node) NodeAt(path string) *Node { + if path == "" || !n.IsMap() { + return nil + } + + parts := strings.SplitN(path, ".", 2) + + attr, exists := n.ToMap()[parts[0]] + if !exists { + return nil + } + + if len(parts) == 1 { + return attr + } + + return attr.NodeAt(parts[1]) +} + +func (n *Node) StringValue(path string) iacTypes.StringValue { + def := iacTypes.StringDefault("", n.metadata) + if n.IsNil() { + return def + } + + if n.unknown { + return iacTypes.StringUnresolvable(n.metadata) + } + + nested := n.NodeAt(path) + val, ok := nested.AsString() + if !ok { + return iacTypes.StringUnresolvable(n.metadata) + } + return iacTypes.String(val, n.metadata) +} + +func (n *Node) BoolValue(path string) iacTypes.BoolValue { + def := iacTypes.BoolDefault(false, n.metadata) + if n.IsNil() { + return def + } + + if n.unknown { + return iacTypes.BoolUnresolvable(n.metadata) + } + + nested := n.NodeAt(path) + val, ok := nested.AsBool() + if !ok { + return iacTypes.BoolUnresolvable(n.metadata) + } + + return iacTypes.Bool(val, iacTypes.Metadata{}) +} + +func (n *Node) AsBool() (bool, bool) { + if n.IsNil() { + return false, false + } + + scalar, ok := n.val.(*Scalar) + if !ok { + return false, false + } + + switch val := scalar.Val.(type) { + case bool: + return val, true + case string: + return parseAnsibleBool(val) + case int: + switch val { + case 0: + return false, true + case 1: + return true, true + } + } + return false, false +} + +// parseAnsibleBool implements Ansible's string→bool conversion. +func parseAnsibleBool(s string) (bool, bool) { + normalized := strings.ToLower(strings.TrimSpace(s)) + + switch normalized { + case "1", "t", "true", "y", "yes", "on": + return true, true + case "0", "f", "false", "n", "no", "off": + return false, true + default: + return false, false + } +} + +func (n *Node) AsString() (string, bool) { + if !n.IsString() { + return "", false + } + + scalar, _ := n.val.(*Scalar) + val, ok := scalar.Val.(string) + return val, ok +} + +func (n *Node) Value() any { + return safeOp(n, func(nv NodeValue) any { + scalar, ok := nv.(*Scalar) + if ok { + return scalar.Val + } + return nil + }) +} + +func checkScalarType[T any](n *Node) bool { + return safeOp(n, func(nv NodeValue) bool { + scalar, ok := nv.(*Scalar) + if !ok || scalar.Val == nil { + return false + } + _, ok = scalar.Val.(T) + return ok + }) +} + +func safeOp[T any](n *Node, op func(NodeValue) T) T { + var zero T + if n.IsNil() || n.val == nil { + return zero + } + return op(n.val) +} + +func (n *Node) Subtree(r Range) *Node { + sub, _ := n.subtree(r) + return sub +} + +func (n *Node) subtree(r Range) (*Node, bool) { + if n == nil { + return nil, false + } + + if r.Covers(n.rng) { + return n, false + } + + switch val := n.val.(type) { + case *Mapping: + subFields := orderedmap.New[string, *Node](val.Fields.Len()) + for k, child := range val.Fields.Iter() { + if child == nil { + continue + } + sub, partial := child.subtree(r) + if sub == nil { + continue + } + + if partial && r.Start > child.rng.Start { + return sub, true + } + subFields.Set(k, sub) + } + if subFields.Len() > 0 { + return n.withValue(&Mapping{Fields: subFields}), true + } + return nil, false + case *Sequence: + items := make([]*Node, 0, len(val.Items)) + for _, item := range val.Items { + if item == nil { + continue + } + sub, partial := item.subtree(r) + if sub == nil { + continue + } + if partial { + // Return a new sequence containing only the partially matched element. + return n.withValue(&Sequence{Items: []*Node{sub}}), true + } + + items = append(items, sub) + } + if len(items) > 0 { + return n.withValue(&Sequence{Items: items}), true + } + return nil, false + default: + if r.Overlaps(n.rng) { + return n, false + } + return nil, false + } +} diff --git a/pkg/iac/scanners/ansible/parser/node_test.go b/pkg/iac/scanners/ansible/parser/node_test.go new file mode 100644 index 000000000000..59a6ab613716 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/node_test.go @@ -0,0 +1,293 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/orderedmap" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" +) + +func mustNodeFromYAML(t *testing.T, src string) *Node { + t.Helper() + var n Node + require.NoError(t, decodeYAML([]byte(src), &n)) + return &n +} + +func TestNode_UnmarshalYAML(t *testing.T) { + src := `name: {{ testname }} +len: 100 +keys: + - a + - 101 + - true + - null +state: + name: test + len: 200 + foo: null +` + expected := &Node{ + rng: Range{0, 11}, + val: &Mapping{ + Fields: func() *orderedmap.OrderedMap[string, *Node] { + m := orderedmap.New[string, *Node](4) + m.Set("name", &Node{rng: Range{1, 1}, val: &Scalar{Val: "{{ testname }}"}}) + m.Set("len", &Node{rng: Range{2, 2}, val: &Scalar{Val: 100}}) + m.Set("keys", &Node{ + rng: Range{3, 7}, + val: &Sequence{ + Items: []*Node{ + {rng: Range{4, 4}, val: &Scalar{Val: "a"}}, + {rng: Range{5, 5}, val: &Scalar{Val: 101}}, + {rng: Range{6, 6}, val: &Scalar{Val: true}}, + {rng: Range{7, 7}, val: nil}, + }, + }, + }) + m.Set("state", &Node{ + rng: Range{8, 11}, + val: &Mapping{ + Fields: func() *orderedmap.OrderedMap[string, *Node] { + sm := orderedmap.New[string, *Node](3) + sm.Set("name", &Node{rng: Range{9, 9}, val: &Scalar{Val: "test"}}) + sm.Set("len", &Node{rng: Range{10, 10}, val: &Scalar{Val: 200}}) + sm.Set("foo", &Node{rng: Range{11, 11}, val: nil}) + return sm + }(), + }, + }) + return m + }(), + }, + } + + n := mustNodeFromYAML(t, src) + assert.Equal(t, expected, n) +} + +func TestNode_NodeAt(t *testing.T) { + tests := []struct { + name string + src string + path string + expected any + }{ + { + name: "first level", + src: `name: mys3bucket`, + path: "name", + expected: "mys3bucket", + }, + { + name: "happy", + src: `tags: + example: tag1`, + path: "tags.example", + expected: "tag1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := mustNodeFromYAML(t, tt.src) + got := n.NodeAt(tt.path).Value() + assert.Equal(t, tt.expected, got) + }) + } +} + +func TestNode_Render(t *testing.T) { + tests := []struct { + name string + yamlSrc string + vars map[string]any + want string + wantErr bool + }{ + { + name: "simple interpolation", + yamlSrc: `"{{ b }} {{ c }}"`, + vars: map[string]any{ + "b": "hello", + "c": "world", + }, + want: "hello world\n", + wantErr: false, + }, + { + name: "chained references", + yamlSrc: `"{{ b }}"`, + vars: map[string]any{ + "b": "{{ c }}", + "c": "final", + }, + want: "final\n", + wantErr: false, + }, + { + name: "cyclic reference", + yamlSrc: `"{{ a }}"`, + vars: map[string]any{ + "a": "{{ a }}", + }, + wantErr: true, + }, + { + name: "shared variable", + yamlSrc: `"{{ x }} and {{ y }}"`, + vars: map[string]any{ + "x": "{{ shared }}", + "y": "{{ shared }}", + "shared": "value", + }, + want: "value and value\n", + wantErr: false, + }, + { + name: "undefined variable", + yamlSrc: `"{{ missing }}"`, + vars: make(map[string]any), + wantErr: true, + }, + { + name: "empty template", + yamlSrc: `""`, + vars: make(map[string]any), + want: `"" +`, + wantErr: false, + }, + { + name: "mixed literal and template", + yamlSrc: `"start {{ a }} end"`, + vars: map[string]any{ + "a": "{{ b }}", + "b": "middle", + }, + want: "start middle end\n", + wantErr: false, + }, + { + name: "sequence and mapping", + yamlSrc: ` +list: + - "{{ x }}" + - "{{ y }}" +dict: + key1: "{{ a }}" + key2: "{{ b }}" +`, + vars: map[string]any{"x": "1", "y": "2", "a": "A", "b": "B"}, + want: `list: + - "1" + - "2" +dict: + key1: A + key2: B +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n := mustNodeFromYAML(t, tt.yamlSrc) + got, err := n.Render(vars.NewVars(tt.vars, 0)) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + marshaled, err := yaml.Marshal(got) + require.NoError(t, err) + assert.Equal(t, tt.want, string(marshaled)) + }) + } +} + +func TestNode_Subtree(t *testing.T) { + src := `name: test +len: 100 +state: + foo: bar + num: 42 +elems: + - foo: 1 + baz: 2 + - bar +` + tests := []struct { + name string + query Range + expected string + }{ + { + name: "no cover", + query: Range{0, 0}, + expected: "null\n", + }, + { + name: "single top-level field", + query: Range{1, 1}, + expected: "name: test\n", + }, + { + name: "single nested field", + query: Range{5, 5}, + expected: "num: 42\n", + }, + { + name: "nested mapping", + query: Range{4, 5}, + expected: `foo: bar +num: 42 +`, + }, + { + name: "map with some fields", + query: Range{6, 7}, + expected: `elems: + - foo: 1 +`, + }, + { + name: "single nested element", + query: Range{9, 9}, + expected: `- bar +`, + }, + { + name: "nested slice", + query: Range{7, 9}, + expected: `- foo: 1 + baz: 2 +- bar +`, + }, + { + name: "nested slice elem", + query: Range{9, 9}, + expected: `- bar +`, + }, + { + name: "full range", + query: Range{1, 9}, + expected: src, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + node := mustNodeFromYAML(t, src) + subtree := node.Subtree(tt.query) + marshaled, err := yaml.Marshal(subtree) + require.NoError(t, err) + assert.Equal(t, tt.expected, string(marshaled)) + }) + } +} diff --git a/pkg/iac/scanners/ansible/parser/parser.go b/pkg/iac/scanners/ansible/parser/parser.go new file mode 100644 index 000000000000..2e20d66ec272 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/parser.go @@ -0,0 +1,761 @@ +package parser + +import ( + "cmp" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/hashicorp/go-multierror" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/inventory" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +const ( + ansibleBuiltinPrefix = "ansible.builtin." +) + +func withBuiltinPrefix(actions ...string) []string { + result := make([]string, 0, len(actions)*2) + for _, action := range actions { + result = append(result, action, ansibleBuiltinPrefix+action) + } + return result +} + +type AnsibleProject struct { + path string + + cfg AnsibleConfig + inventory *inventory.Inventory + galaxyManifest *GalaxyManifest + + tasks ResolvedTasks +} + +func (p *AnsibleProject) Path() string { + return p.path +} + +// TODO(nikita): some tasks do not contain metadata +func (p *AnsibleProject) ListTasks() ResolvedTasks { + return p.tasks +} + +type GalaxyManifest struct { + Namespace string `yaml:"namespace"` + Name string `yaml:"name"` +} + +type Option func(p *Parser) + +func WithPlaybooks(playbooks ...string) Option { + return func(p *Parser) { + p.playbooks = playbooks + } +} + +func WithInventories(inventories ...string) Option { + return func(p *Parser) { + p.inventories = inventories + } +} + +func WithExtraVars(v map[string]any) Option { + return func(p *Parser) { + p.extraVars = vars.NewVars(v, vars.ExtraVarsPriority) + } +} + +type Parser struct { + fsys fs.FS + rootSrc fsutils.FileSource + logger *log.Logger + + inventories []string + playbooks []string + extraVars vars.Vars + + project *AnsibleProject + + // resolvedTasks caches the fully expanded list of tasks for each playbook, + // keyed by the playbook's file path to avoid redundant parsing and resolution. + resolvedTasks map[string]ResolvedTasks + + // roleCache stores loaded role data keyed by role name, + // enabling reuse of roles across multiple playbooks without repeated loading. + roleCache map[string]*Role +} + +func New(fsys fs.FS, root string, opts ...Option) *Parser { + p := &Parser{ + fsys: fsys, + rootSrc: fsutils.NewFileSource(fsys, root), + logger: log.WithPrefix("ansible"), + extraVars: make(vars.Vars), + + resolvedTasks: make(map[string]ResolvedTasks), + roleCache: make(map[string]*Role), + } + + for _, opt := range opts { + opt(p) + } + + return p +} + +func ParseProject(fsys fs.FS, root string, opts ...Option) (*AnsibleProject, error) { + parser := New(fsys, root, opts...) + project, err := parser.Parse() + if err != nil { + return nil, err + } + return project, nil +} + +func (p *Parser) Parse() (*AnsibleProject, error) { + p.logger.Debug("Parse Ansible project", log.FilePath(p.rootSrc.Path)) + + err := p.initProject() + if err != nil { + return nil, err + } + + playbookSources := xslices.Map(p.playbooks, func(playbookPath string) fsutils.FileSource { + return p.rootSrc.Join(playbookPath) + }) + + if len(playbookSources) == 0 { + playbookSources, err = p.resolvePlaybooksPaths() + if err != nil { + return nil, err + } + } + + tasks, err := p.parsePlaybooks(playbookSources) + if err != nil { + return nil, err + } + + p.project.tasks = tasks + return p.project, nil +} + +func (p *Parser) initProject() error { + cfg, err := p.readAnsibleConfig() + if err != nil { + return xerrors.Errorf("read config: %w", err) + } + + inv := inventory.LoadAuto(p.fsys, inventory.LoadOptions{ + InventoryPath: cfg.Inventory, + Sources: p.inventories, + }) + + p.project = &AnsibleProject{ + path: p.rootSrc.Path, + cfg: cfg, + inventory: inv, + galaxyManifest: p.findGalaxyManifest(), + } + return nil +} + +func (p *Parser) findGalaxyManifest() *GalaxyManifest { + var manifest GalaxyManifest + if err := decodeYAMLFileWithExtension(p.rootSrc.Join("galaxy"), &manifest, yamlExtensions); err != nil { + return nil + } + + p.logger.Debug("Found Galaxy manifest", + log.String("namespace", manifest.Namespace), log.String("name", manifest.Name)) + + return &manifest +} + +func (p *Parser) parsePlaybooks(sources []fsutils.FileSource) (ResolvedTasks, error) { + playbooks := make(map[string]*Playbook) + + for _, src := range sources { + pb, err := p.loadPlaybook(src) + if err != nil { + // Skip files that are YAML but not valid playbooks. + p.logger.Debug("Skipping YAML file: not a playbook", + log.FilePath(src.Path), log.Err(err)) + continue + } + playbooks[src.Path] = pb + } + + entryPoints := findEntryPoints(playbooks) + + // TODO: Filter entrypoint playbooks by hosts and inventory. + // For each play, check its 'hosts' field against the inventory (hosts and groups). + // Include playbooks targeting at least one host from the inventory. + // Handle special cases such as 'all', 'localhost', and dynamic variables. + // Optionally, add a mode to bypass this filtering for full scans or debugging. + + // Resolve tasks from entrypoint playbooks — those that are not imported/included by others. + var allTasks ResolvedTasks + for _, playbookSrc := range entryPoints { + tasks, err := p.resolvePlaybook(nil, nil, playbookSrc, playbooks) + if err != nil { + return nil, xerrors.Errorf("resolve playbook: %w", err) + } + allTasks = append(allTasks, tasks...) + } + + return allTasks, nil +} + +func (p *Parser) loadPlaybook(f fsutils.FileSource) (*Playbook, error) { + var plays []*Play + if err := decodeYAMLFile(f, &plays); err != nil { + return nil, xerrors.Errorf("decode YAML file: %w", err) + } + + p.logger.Debug("Loaded playbook", + log.FilePath(f.Path), log.Int("plays_count", len(plays))) + return &Playbook{ + Src: f, + Plays: plays, + }, nil +} + +func findEntryPoints(playbooks map[string]*Playbook) []fsutils.FileSource { + included := set.New[string]() + + for _, pb := range playbooks { + for _, p := range pb.Plays { + if incPath, ok := p.includedPlaybook(); ok { + includedSrc := pb.resolveIncludedSrc(incPath) + included.Append(includedSrc.Path) + } + } + } + + var entryPoints []fsutils.FileSource + for path, pb := range playbooks { + if !included.Contains(path) { + entryPoints = append(entryPoints, pb.Src) + } + } + + return entryPoints +} + +// resolvePlaybook recursively expands tasks, roles, and included playbooks within the given playbook. +func (p *Parser) resolvePlaybook( + parent *iacTypes.Metadata, parentVars vars.Vars, + playbookSrc fsutils.FileSource, playbooks map[string]*Playbook, +) (ResolvedTasks, error) { + pb, exists := playbooks[playbookSrc.Path] + if !exists { + // Attempting to load a playbook outside the scan directory (may be an included playbook). + var err error + pb, err = p.loadPlaybook(playbookSrc) + if err != nil { + return nil, xerrors.Errorf("load playbook: %w", err) + } + // Caching the loading external playbook for reuse + playbooks[playbookSrc.Path] = pb + } + + if cached, exists := p.resolvedTasks[pb.Src.Path]; exists { + return cached, nil + } + + p.logger.Debug("Resolve playbook tasks", log.FilePath(pb.Src.Path)) + + // Ansible loads host and group variable files by searching paths + // relative to the playbook file. + // See https://docs.ansible.com/ansible/latest/inventory_guide/intro_inventory.html#organizing-host-and-group-variables + playbookInvVars := inventory.LoadVars(inventory.PlaybookVarsSources(playbookSrc.Dir())) + + var tasks ResolvedTasks + for _, play := range pb.Plays { + + // Initializing the metadata of the play and its nested elements + play.initMetadata(playbookSrc, parent) + + // TODO: resolve hosts by pattern: + // https://docs.ansible.com/ansible/latest/inventory_guide/intro_patterns.html#common-patterns + hosts := play.inner.Hosts + + // TODO: iterate over hosts + hostVars := p.project.inventory.ResolveVars(hosts, playbookInvVars) + playVars := vars.MergeVars(hostVars, parentVars, play.Variables()) + + for _, playTask := range play.listTasks() { + // TODO: pass parent metadata + + // TODO: Support expanding loops (e.g. 'loop', 'with_items') in tasks. + // Example: + // - name: Install multiple packages + // ansible.builtin.yum: + // name: "{{ item }}" + // state: present + // loop: + // - httpd + // - memcached + // - mariadb + // + // During expansion, the task should be duplicated for each item with `item` rendered. + + childrenTasks, err := p.expandTask(playVars, playTask) + if err != nil { + p.logger.Debug("Failed to expand playbook task", + log.String("source", playTask.metadata.Range().String()), log.Err(err)) + } + tasks = append(tasks, childrenTasks...) + } + + // https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html#using-roles-at-the-play-level + for _, roleDef := range play.roleDefinitions() { + roleTasks, err := p.resolveRoleDefinitionTasks(roleDef, play, playVars) + if err != nil { + p.logger.Debug("Failed to load role", log.String("role", roleDef.name()), log.Err(err)) + continue + } + tasks = append(tasks, roleTasks...) + } + + // https://docs.ansible.com/ansible/latest/collections/ansible/builtin/import_playbook_module.html + if incPath, ok := play.includedPlaybook(); ok { + p.logger.Debug("Resolve playbook include", + log.String("source", play.metadata.Range().String()), + log.String("include", incPath), + ) + + effectiveVars := vars.MergeVars(playVars, play.specialVars()) + renderedPath, err := evaluateTemplate(incPath, effectiveVars) + if err != nil { + p.logger.Debug("Failed to render path", + log.FilePath(incPath), log.Err(err)) + continue + } + + fullIncSrc := pb.resolveIncludedSrc(renderedPath) + includedTasks, err := p.resolvePlaybook(&play.metadata, playVars, fullIncSrc, playbooks) + if err != nil && errors.Is(err, fs.ErrNotExist) { + p.logger.Debug("Failed to load included playbook", + log.FilePath(fullIncSrc.Path), log.Err(err)) + } else { + if err != nil { + p.logger.Debug("An error occurred while resolving playbook tasks", + log.FilePath(fullIncSrc.Path), log.Err(err)) + } + p.logger.Debug("Loaded included playbook tasks", + log.FilePath(fullIncSrc.Path), log.Int("tasks_count", len(includedTasks))) + tasks = append(tasks, includedTasks...) + } + } + } + + p.logger.Debug("Resolved playbook tasks", + log.FilePath(pb.Src.Path), log.Int("tasks_count", len(tasks))) + + p.resolvedTasks[pb.Src.Path] = tasks + return tasks, nil +} + +func (p *Parser) resolveRoleDefinitionTasks( + roleDef *RoleDefinition, play *Play, playVars vars.Vars, +) (ResolvedTasks, error) { + p.logger.Debug("Resolve role at play level", + log.String("name", roleDef.name()), + log.String("source", roleDef.metadata.Range().String())) + + role, err := p.loadRole(&roleDef.metadata, play, roleDef.name()) + if err != nil { + return nil, xerrors.Errorf("load role %q: %w", roleDef.name(), err) + } + + // When using the roles option at the play level, each role ‘x’ + // looks for files named main.yml, main.yaml, or main (without extension) + // in its internal directories (tasks, defaults, vars, etc.). + + // Ignore non-existent files, as they are loaded by default and may be missing + roleDefaults, err := role.defaultVariables("main") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + p.logger.Debug("Failed to load role default variables", log.Err(err)) + } + roleVariables, err := role.fileVariables("main") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + p.logger.Debug("Failed to load role variables", log.Err(err)) + } + + roleScopeVars := vars.MergeVars( + // Role default variables have the lowest priority + roleDefaults, + playVars, + roleVariables, + ) + + roleTasks, err := role.getTasks("main") + if err != nil { + return nil, xerrors.Errorf("load role tasks: %w", err) + } + + var tasks ResolvedTasks + for _, roleTask := range roleTasks { + childrenTasks, err := p.expandTask(roleScopeVars, roleTask) + if err != nil { + p.logger.Debug("Failed to expand role tasks", log.Err(err)) + continue + } + tasks = append(tasks, childrenTasks...) + } + + p.logger.Debug("Included role loaded", + log.FilePath(role.src.Path), log.Int("tasks_count", len(tasks))) + return tasks, nil +} + +// loadRole loads a role by name. +func (p *Parser) loadRole(parent *iacTypes.Metadata, play *Play, roleName string) (*Role, error) { + cachedRole, exists := p.roleCache[roleName] + if exists { + return cachedRole, nil + } + + roleSrc, exists := p.resolveRolePath(play.src.Dir(), roleName) + if !exists || roleSrc.Path == "" { + return nil, xerrors.Errorf("role %q not found", roleName) + } + + r := &Role{ + name: roleName, + src: roleSrc, + play: play, + cachedTasks: make(map[string][]*Task), + } + r.initMetadata(roleSrc, parent) + + if err := p.loadRoleDependencies(r); err != nil { + return nil, xerrors.Errorf("load role deps: %w", err) + } + + p.roleCache[roleName] = r + + p.logger.Debug("Role found", + log.String("name", roleName), + log.String("source", parent.GetMetadata().Range().String()), + log.FilePath(roleSrc.Path)) + return r, nil +} + +func (p *Parser) loadRoleDependencies(r *Role) error { + // The meta directory is an exception: it always uses the standard + // main.yml (or main.yaml/main) file without allowing custom filenames or overrides. + metaSrc := r.src.Join("meta", "main") + + var roleMeta RoleMeta + if err := decodeYAMLFileWithExtension(metaSrc, &roleMeta, yamlExtensions); err != nil && !errors.Is(err, os.ErrNotExist) { + return xerrors.Errorf("load meta: %w", err) + } + + roleMeta.updateMetadata(metaSrc.FS, &r.metadata, metaSrc.Path) + + for _, dep := range roleMeta.dependencies() { + depRole, err := p.loadRole(&roleMeta.metadata, r.play, dep.name()) + if err != nil { + return xerrors.Errorf("load role dependency %q: %w", dep.name(), err) + } + r.directDeps = append(r.directDeps, depRole) + } + return nil +} + +// TODO: support all possible locations of the role +// https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html +func (p *Parser) resolveRolePath(playbookDirSrc fsutils.FileSource, name string) (fsutils.FileSource, bool) { + isPath := filepath.IsAbs(name) || strings.HasPrefix(name, ".") + if isPath { + if !filepath.IsAbs(name) { + return playbookDirSrc.Join(name), true + } + return fsutils.NewFileSource(nil, name), true + } + + roleName := name + baseSrc := playbookDirSrc + + parts := strings.SplitN(roleName, ".", 3) + if len(parts) == 3 { + if m := p.project.galaxyManifest; m != nil && + m.Namespace == parts[0] && m.Name == parts[1] { + roleName = parts[2] + baseSrc = p.rootSrc + + // TODO: support resolving roles from namespace.collection + // by searching in the collections/ansible_collections directory + } + } + + roleSources := []fsutils.FileSource{ + baseSrc.Join("roles", roleName), + } + + if defaultRolesPath, exists := os.LookupEnv("DEFAULT_ROLES_PATH"); exists { + rolesSrc := fsutils.NewFileSource(nil, defaultRolesPath) + roleSources = append(roleSources, rolesSrc.Join(roleName)) + } + + for _, roleSrc := range roleSources { + if exists, _ := roleSrc.Exists(); exists { + return roleSrc, true + } + } + + return fsutils.FileSource{}, false +} + +// expandTask dispatches task expansion based on task type (block, include, role). +func (p *Parser) expandTask(parentVars vars.Vars, t *Task) (ResolvedTasks, error) { + + // TODO: pass parentVars ? + effectiveVars := vars.MergeVars(parentVars, t.Variables()) + + taskSource := t.metadata.Range().String() + switch { + case t.isBlock(): + tasks, err := p.expandBlockTasks(effectiveVars, t) + return wrapIfErr(tasks, fmt.Sprintf("expand block tasks %s", taskSource), err) + case t.isTaskInclude(): + tasks, err := p.resolveTasksInclude(effectiveVars, t) + return wrapIfErr(tasks, fmt.Sprintf("resolve tasks include %s", taskSource), err) + case t.isRoleInclude(): + tasks, err := p.resolveRoleInclude(effectiveVars, t) + return wrapIfErr(tasks, fmt.Sprintf("resolve role include %s", taskSource), err) + default: + resolvedTask := p.resolveTask(t, parentVars) + // TODO: check that the task is not absent + return ResolvedTasks{resolvedTask}, nil + } +} + +// wrapIfErr adds context to err but still returns val even if err != nil. +func wrapIfErr[T any](val T, msg string, err error) (T, error) { + if err != nil { + return val, xerrors.Errorf("%s: %w", msg, err) + } + return val, nil +} + +// expandBlockTasks expands a block task into its constituent tasks. +// +// Blocks group multiple tasks under a single block in a playbook. +// See https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_blocks.html +func (p *Parser) expandBlockTasks(parentVars vars.Vars, t *Task) (ResolvedTasks, error) { + var res ResolvedTasks + var errs error + for _, task := range t.inner.Block { + children, err := p.expandTask(parentVars, task) + if err != nil { + errs = multierror.Append(errs, err) + } + res = append(res, children...) + } + + p.logger.Debug("Expanded block tasks", + log.String("source", t.metadata.Range().String()), + log.Int("tasks_count", len(res)), + ) + return res, errs +} + +func (p *Parser) resolveTask(task *Task, parentVars vars.Vars) *ResolvedTask { + return task.resolved(p.effecitveVarsForTask(task, parentVars)) +} + +func (p *Parser) effecitveVarsForTask(task *Task, parentVars vars.Vars) vars.Vars { + return vars.MergeVars(parentVars, task.Variables(), p.extraVars, specialVarsForTask(task)) +} + +func specialVarsForTask(task *Task) vars.Vars { + variables := task.play.specialVars() + if task.role != nil { + variables = vars.MergeVars(variables, task.role.specialVars()) + } + + return variables +} + +// resolveTasksInclude locates a tasks include or import file and loads its tasks. +// +// Supports Ansible modules 'include_tasks' and 'import_tasks'. +// See https://docs.ansible.com/ansible/latest/collections/ansible/builtin/include_tasks_module.html +func (p *Parser) resolveTasksInclude(parentVars vars.Vars, task *Task) (ResolvedTasks, error) { + resolvedTask := p.resolveTask(task, parentVars) + moduleKeys := withBuiltinPrefix(ModuleIncludeTasks, ModuleImportTasks) + m, err := resolvedTask.ResolveModule(moduleKeys, true) + if err != nil { + return nil, xerrors.Errorf("resolving module for keys %v: %w", moduleKeys, err) + } + + var tasksFilePath string + if m.IsFreeForm() { + tasksFilePath, _ = m.AsString() + } else { + tasksFilePath = getStringParam(m, "file") + } + + if tasksFilePath == "" { + return nil, xerrors.New("tasks file is empty") + } + + taskSrc := task.src.Dir().Join(tasksFilePath) + includedTasks, err := loadTasks(task.play, &task.metadata, taskSrc) + if err != nil { + return nil, xerrors.Errorf("load tasks from %q: %w", taskSrc.Path, err) + } + + var allTasks ResolvedTasks + + var errs error + for _, loadedTask := range includedTasks { + children, err := p.expandTask(parentVars, loadedTask) + if err != nil { + errs = multierror.Append(xerrors.Errorf("expand included task: %w", err)) + } + allTasks = append(allTasks, children...) + } + + p.logger.Debug("Included tasks loaded", + log.String("source", task.metadata.Range().String()), + log.FilePath(taskSrc.Path), + log.Int("tasks_count", len(allTasks))) + return allTasks, errs +} + +// resolveRoleInclude locates an included or imported role and loads its tasks. +// +// Supports Ansible modules 'include_role' and 'import_role'. +// See https://docs.ansible.com/ansible/latest/collections/ansible/builtin/include_role_module.html +func (p *Parser) resolveRoleInclude(parentVars vars.Vars, task *Task) (ResolvedTasks, error) { + resolvedTask := p.resolveTask(task, parentVars) + moduleKeys := withBuiltinPrefix(ModuleIncludeRole, ModuleImportRole) + m, err := resolvedTask.ResolveModule(moduleKeys, true) + if err != nil { + return nil, xerrors.Errorf("resolving module for keys %v: %w", moduleKeys, err) + } + + module := RoleIncludeModule{ + Name: getStringParam(m, "name"), + TasksFrom: cmp.Or(getStringParam(m, "tasks_from"), "main"), + VarsFrom: cmp.Or(getStringParam(m, "vars_from"), "main"), + DefaultsFrom: cmp.Or(getStringParam(m, "defaults_from"), "main"), + } + + if module.Name == "" { + return nil, xerrors.New("role name is empty") + } + + // When using include_role/import_role, custom file names or paths can be specified + // for various role components instead of the default "main". This applies to tasks, + // defaults, vars, handlers, meta, etc. + // See: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/include_role_module.html + role, err := p.loadRole(&task.metadata, task.play, module.Name) + if err != nil { + return nil, xerrors.Errorf("load included role %q: %w", module.Name, err) + } + + roleDefaults, err := role.defaultVariables(module.DefaultsFrom) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + p.logger.Debug("Failed to load role default variables", log.Err(err)) + } + roleVariables, err := role.fileVariables(module.VarsFrom) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + p.logger.Debug("Failed to load role variables", log.Err(err)) + } + + roleScopeVars := vars.MergeVars( + // Role default variables have the lowest priority + roleDefaults, + parentVars, + roleVariables, + ) + + var allTasks ResolvedTasks + + roleTasks, err := role.getTasks(module.TasksFrom) + if err != nil { + return nil, xerrors.Errorf("load tasks from %q: %w", module.TasksFrom, err) + } + + var errs error + for _, roleTask := range roleTasks { + // TODO: do not update the parent in the metadata here, as the dependency chain may be lost + // if the task is a role dependency task + // task.updateParent(t) + children, err := p.expandTask(roleScopeVars, roleTask) + if err != nil { + errs = multierror.Append(xerrors.Errorf("expand task: %w", err)) + } + allTasks = append(allTasks, children...) + } + + p.logger.Debug("Included role loaded", + log.String("source", task.metadata.Range().String()), + log.FilePath(role.src.Path), + log.Int("tasks_count", len(allTasks))) + return allTasks, errs +} + +func getStringParam(m Module, paramKey string) string { + val, _ := m.NodeAt(paramKey).AsString() + return val +} + +func (p *Parser) readAnsibleConfig() (AnsibleConfig, error) { + return LoadConfig(p.fsys, p.rootSrc.Path) +} + +func (p *Parser) resolvePlaybooksPaths() ([]fsutils.FileSource, error) { + entries, err := p.rootSrc.ReadDir() + if err != nil { + return nil, err + } + + var res []fsutils.FileSource + + for _, entry := range entries { + if isYAMLFile(entry.Name()) { + res = append(res, p.rootSrc.Join(entry.Name())) + } + } + + return res, nil +} + +func loadTasks(play *Play, parentMetadata *iacTypes.Metadata, fileSrc fsutils.FileSource) ([]*Task, error) { + var fileTasks []*Task + tasksExtensions := append(yamlExtensions, "") + if err := decodeYAMLFileWithExtension(fileSrc, &fileTasks, tasksExtensions); err != nil { + return nil, xerrors.Errorf("decode tasks file %q: %w", fileSrc.Path, err) + } + for _, task := range fileTasks { + task.init(play, fileSrc, parentMetadata) + } + return fileTasks, nil +} + +var yamlExtensions = []string{".yml", ".yaml"} + +func isYAMLFile(filePath string) bool { + ext := filepath.Ext(filePath) + return slices.Contains(yamlExtensions, ext) +} diff --git a/pkg/iac/scanners/ansible/parser/parser_test.go b/pkg/iac/scanners/ansible/parser/parser_test.go new file mode 100644 index 000000000000..5e1be0b4c682 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/parser_test.go @@ -0,0 +1,911 @@ +package parser_test + +import ( + "cmp" + "fmt" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/parser" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func TestParseProject(t *testing.T) { + fsys := os.DirFS(filepath.Join("testdata", "sample-proj")) + + project, err := parser.New(fsys, ".").Parse() + require.NoError(t, err) + + tasks := project.ListTasks() + assert.Len(t, tasks, 6) +} + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + dir string + opts []parser.Option + files map[string]string + expectedTasks []string + }{ + { + name: "tasks in play", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: localhost + pre_tasks: + - name: Pre-task + debug: + msg: test + tasks: + - name: Task + debug: + msg: test + post_tasks: + - name: Post-task + debug: + msg: test +`, + }, + expectedTasks: []string{"Pre-task", "Task", "Post-task"}, + }, + { + name: "task name with unquoted template", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: localhost + tasks: + - name: {{ Task }} + debug: + msg: test +`, + }, + expectedTasks: []string{"{{ Task }}"}, + }, + { + name: "tasks in role", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: localhost + roles: + - test +`, + "roles/test/tasks/main.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "role with dependencies", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: localhost + roles: + - test +`, + "roles/test/tasks/main.yaml": `--- +- name: Role task + debug: + msg: Test task +`, + "roles/test/meta/main.yaml": `--- +dependencies: + - role: role2 +`, + "roles/role2/tasks/main.yaml": `--- +- name: Dependent task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Dependent task", "Role task"}, + }, + { + name: "block task", + files: map[string]string{ + "playbook.yaml": `--- +- tasks: + - name: Test block + block: + - name: Test task 1 + debug: + msg: Test task + - name: Test task 2 + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task 1", "Test task 2"}, + }, + { + name: "block task includes role", + files: map[string]string{ + "playbook.yaml": `--- +- tasks: + - name: Test block + block: + - name: Test task + include_role: + name: test +`, + "roles/test/tasks/main.yaml": `--- +- name: Role task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Role task"}, + }, + { + name: "nested block includes tasks", + dir: "playbooks", + files: map[string]string{ + "playbooks/playbook.yaml": `--- +- tasks: + - name: Test block + block: + - name: nested block + block: + - include_tasks: + file: test.yaml +`, + "playbooks/test.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "include and import tasks in play", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: all + tasks: + - name: Test task + debug: + msg: Test task + + - name: Include task list in play + ansible.builtin.include_tasks: + file: test.yaml + + - name: Import task list in play + ansible.builtin.import_tasks: + file: test2.yaml +`, + "test.yaml": `--- +- name: Included task + debug: + msg: Included task +`, + "test2.yaml": `--- +- name: Imported task + debug: + msg: Imported task +`, + }, + expectedTasks: []string{"Test task", "Included task", "Imported task"}, + }, + { + name: "unresolved template in include", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: all + tasks: + - name: Test task + debug: + msg: Test task + + - include_tasks: "{{item}}" +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "include and import tasks in role", + files: map[string]string{ + "playbook.yaml": `--- +- name: Update web servers + hosts: localhost + roles: + - test +`, + "roles/test/tasks/main.yaml": `--- +- name: Test task + debug: + msg: Test task + +- name: Include task list in role + ansible.builtin.include_tasks: + file: test.yaml + +- name: Import task list in role + ansible.builtin.import_tasks: + file: test2.yaml +`, + "roles/test/tasks/test.yaml": `--- +- name: Included task + debug: + msg: Included task +`, + "roles/test/tasks/test2.yaml": `--- +- name: Imported task + debug: + msg: Imported task +`, + }, + expectedTasks: []string{"Test task", "Included task", "Imported task"}, + }, + { + name: "include role in play", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: all + tasks: + - name: Test task + include_role: + name: test + tasks_from: test +`, + "roles/test/tasks/main.yaml": `--- +- name: Main task + debug: + msg: Main task +`, + "roles/test/tasks/test.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "inline include role in play", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: all + tasks: + - {include_role: {name: test, tasks_from: test}} +`, + "roles/test/tasks/test.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "include role in play by path", + dir: "playbooks", + files: map[string]string{ + "playbooks/playbook.yaml": `--- +- hosts: all + tasks: + - {include_role: {name: ../roles/test, tasks_from: test}} +`, + "roles/test/tasks/test.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "role task from subdir", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: all + tasks: + - {include_role: {name: test, tasks_from: subdir/test.yaml}} +`, + "roles/test/tasks/subdir/test.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "import role in play", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: all + tasks: + - name: Test task + import_role: + name: test + tasks_from: test +`, + "roles/test/tasks/main.yaml": `--- +- name: Main task + debug: + msg: Main task +`, + "roles/test/tasks/test.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "include role in role", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: all + roles: + - main +`, + "roles/main/tasks/main.yaml": `--- +- name: Main task + include_role: + name: test + tasks_from: test +`, + "roles/test/tasks/test.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "import role in role", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: all + roles: + - main +`, + "roles/main/tasks/main.yaml": `--- +- name: Main task + ansible.builtin.import_role: + name: test + tasks_from: test +`, + "roles/test/tasks/test.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "include tasks is free form", + dir: "playbooks", + files: map[string]string{ + "playbooks/playbook.yaml": `--- +- hosts: all + tasks: + - include_tasks: "{{ playbook_dir |dirname }}/tasks/test.yml" +`, + "playbooks/tasks/test.yml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "include_tasks with templated path", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: all + tasks: + - include_tasks: "{{ tasks_file }}" + vars: + tasks_file: tasks/test.yml +`, + "tasks/test.yml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + { + name: "import playbook", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: localhost + tasks: + - name: Task + debug: + msg: test + +- name: Include playbook + ansible.builtin.import_playbook: other.yaml +`, + "other.yaml": `--- +- hosts: localhost + tasks: + - name: Included Task + debug: + msg: test +`, + }, + expectedTasks: []string{"Task", "Included Task"}, + }, + { + name: "with unused role", + files: map[string]string{ + "playbook.yaml": `--- +- hosts: localhost + roles: + - main +`, + "roles/main/tasks/main.yaml": `--- +- name: Main role task + debug: + msg: test +`, + "roles/unused/tasks/main.yaml": `--- +- name: Unused task + debug: + msg: test +`, + }, + expectedTasks: []string{"Main role task"}, + }, + { + name: "multiple playbooks", + files: map[string]string{ + "site.yaml": `--- +- hosts: all + tasks: + - name: Foo task + debug: + msg: Test +`, + "playbook.yaml": `--- +- hosts: all + tasks: + - name: Bar task + debug: + msg: Test +`, + }, + expectedTasks: []string{"Foo task", "Bar task"}, + }, + { + name: "included playbook outside root directory", + dir: "project", + files: map[string]string{ + "project/main.yml": ` +- name: Main play + hosts: all + import_playbook: ../common/common.yml +`, + "common/common.yml": ` +- name: Common play + hosts: all + tasks: + - name: task from common playbook + debug: null + msg: hello from common +`, + }, + expectedTasks: []string{ + "task from common playbook", + }, + }, + { + name: "role from namespace.collection", + dir: "project", + files: map[string]string{ + "project/galaxy.yaml": ` +namespace: myns +name: mycol +`, + "project/playbook.yml": ` +- name: Play with collection role + hosts: all + roles: + - myns.mycol.myrole +`, + "project/roles/myrole/tasks/main.yml": ` +- name: task from collection role + debug: + msg: hello from collection role +`, + }, + expectedTasks: []string{ + "task from collection role", + }, + }, + { + name: "with playbook sources", + dir: "project", + opts: []parser.Option{parser.WithPlaybooks("playbooks/playbook.yaml")}, + files: map[string]string{ + "project/playbooks/playbook.yaml": `--- +- hosts: localhost + roles: + - "../roles/test" +`, + "project/roles/test/tasks/main.yaml": `--- +- name: Test task + debug: + msg: Test task +`, + }, + expectedTasks: []string{"Test task"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := testutil.CreateFS(tt.files) + dir := cmp.Or(tt.dir, ".") + p := parser.New(fsys, dir, tt.opts...) + project, err := p.Parse() + require.NoError(t, err) + + tasks := project.ListTasks() + + taskNames := xslices.Map(tasks, func(task *parser.ResolvedTask) string { + return task.Name + }) + + assert.ElementsMatch(t, tt.expectedTasks, taskNames) + }) + } +} + +func TestParser_AbsolutePath(t *testing.T) { + // The process cannot access the file because it is being used by another process. + if runtime.GOOS == "windows" { + t.Skip("TODO") + } + + tmpDir := t.TempDir() + + tasksFile, err := os.CreateTemp(tmpDir, "tasks-*.yml") + require.NoError(t, err) + + _, err = tasksFile.WriteString(`- name: Test task + debug: + msg: "From task" +`) + require.NoError(t, err) + + playbookFile, err := os.CreateTemp(tmpDir, "playbook-*.yml") + require.NoError(t, err) + + playbookSrc := fmt.Sprintf(`- name: test + hosts: localhost + connection: local + tasks: + - name: test + include_tasks: "{{ playbook_dir }}/%s" +`, filepath.Base(tasksFile.Name())) + + _, err = playbookFile.WriteString(playbookSrc) + require.NoError(t, err) + + project, err := parser.New(os.DirFS(tmpDir), ".").Parse() + require.NoError(t, err) + tasks := project.ListTasks() + taskNames := xslices.Map(tasks, func(task *parser.ResolvedTask) string { + return task.Name + }) + + expected := []string{ + "Test task", + } + assert.ElementsMatch(t, expected, taskNames) +} + +func TestParse_ResolveVariables(t *testing.T) { + tests := []struct { + name string + files map[string]string + opts []parser.Option + }{ + { + name: "vars in task", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: test + tasks: + - name: create bucket + vars: + public_access: "true" + s3_bucket: + name: "{{ bucket }}" + public_access: "{{ public_access }}" +`, + }, + }, + { + name: "vars in play", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: test + public_access: "true" + tasks: + - name: create bucket + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' +`, + }, + }, + { + name: "vars in block", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: test + tasks: + - block: + - name: create bucket + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' + vars: + public_access: "true" +`, + }, + }, + // { + // name: "vars from vars_files", + // files: map[string]string{ + // "main.yaml": `--- + // - name: test + // vars_files: + // - vars.yaml + // vars: + // bucket: test + // tasks: + // - name: create bucket + // s3_bucket: + // name: '{{ bucket }}' + // public_access: '{{ public_access }}' + // `, + // "vars.yaml": `public_access: "true"`, + // }, + // }, + // { + // name: "vars from include_vars", + // files: map[string]string{ + // "main.yaml": `--- + // - name: test + // vars: + // bucket: test + // tasks: + // - include_vars: vars.yaml + // - name: create bucket + // s3_bucket: + // name: '{{ bucket }}' + // public_access: '{{ public_access }}' + // `, + // "vars.yaml": `public_access: "true"`, + // }, + // }, + // { + // name: "vars from set_fact", + // files: map[string]string{ + // "main.yaml": `--- + // - name: test + // vars: + // bucket: test + // tasks: + // - set_fact: + // public_access: "true" + // - name: create bucket + // s3_bucket: + // name: '{{ bucket }}' + // public_access: '{{ public_access }}' + // `, + // }, + // }, + { + name: "vars from included tasks", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: test + tasks: + - include_tasks: included.yaml +`, + "included.yaml": ` +- name: create bucket + vars: + public_access: "true" + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' +`, + }, + }, + { + name: "vars from imported tasks", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: test + tasks: + - import_tasks: imported.yaml +`, + "imported.yaml": ` +- name: create bucket + vars: + public_access: "true" + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' +`, + }, + }, + { + name: "vars from role defaults", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: test + roles: + - myrole +`, + "roles/myrole/defaults/main.yaml": `public_access: "true" +bucket: "from-role" +`, + "roles/myrole/tasks/main.yaml": ` +- name: create bucket + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' +`, + }, + }, + { + name: "vars from role vars", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: test + roles: + - myrole +`, + "roles/myrole/vars/main.yaml": `public_access: "true"`, + "roles/myrole/tasks/main.yaml": ` +- name: create bucket + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' +`, + }, + }, + { + name: "vars from nested role dirs", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: test + roles: + - myrole +`, + "roles/myrole/vars/main/subdir/vars.yaml": `public_access: "foo"`, + "roles/myrole/vars/main/vars.yaml": `public_access: "true"`, + "roles/myrole/tasks/main.yaml": ` +- name: create bucket + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' +`, + }, + }, + { + name: "vars for host", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: test + hosts: webservers + tasks: + - name: create bucket + vars: + public_access: "true" + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' +`, + }, + }, + { + name: "with extra vars", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: from-play + hosts: webservers + tasks: + - name: create bucket + vars: + public_access: "true" + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' +`, + }, + opts: []parser.Option{ + parser.WithExtraVars(map[string]any{ + "bucket": "test", + }), + }, + }, + { + name: "with host_vars", + files: map[string]string{ + "main.yaml": `--- +- name: test + vars: + bucket: from-play + hosts: webservers + tasks: + - name: create bucket + vars: + public_access: "true" + s3_bucket: + name: '{{ bucket }}' + public_access: '{{ public_access }}' +`, + "host_vars/webservers": ``, + }, + opts: []parser.Option{ + parser.WithExtraVars(map[string]any{ + "bucket": "test", + }), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := testutil.CreateFS(tt.files) + p := parser.New(fsys, ".", tt.opts...) + project, err := p.Parse() + require.NoError(t, err) + + tasks := project.ListTasks() + modules := tasks.GetModules("s3_bucket") + require.Len(t, modules, 1) + + m := modules[0] + assert.Equal(t, "test", m.StringValue("name").Value()) + assert.Equal(t, "true", m.StringValue("public_access").Value()) + }) + } +} diff --git a/pkg/iac/scanners/ansible/parser/play.go b/pkg/iac/scanners/ansible/parser/play.go new file mode 100644 index 000000000000..2587c36b4bdb --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/play.go @@ -0,0 +1,218 @@ +package parser + +import ( + "path/filepath" + + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +// Playbook represents a sequence of plays in an Ansible playbook. +// +// A playbook is typically loaded from a YAML file and contains a list +// of plays that are executed in order. +// +// The playbook corresponds to a YAML list, for example: +// +// - name: First play +// hosts: all +// tasks: +// +// - ... +// +// - name: Second play +// hosts: dbservers +// tasks: +// +// - ... +type Playbook struct { + Src fsutils.FileSource + Plays []*Play + Tasks []*Task +} + +func (pb *Playbook) resolveIncludedSrc(incPath string) fsutils.FileSource { + return pb.Src.Dir().Join(incPath) +} + +// Play represents a single play in an Ansible playbook. +// +// An Ansible playbook is a list of such plays, where each play defines +// settings and tasks for a specific group of hosts. +// +// Example playbook YAML: +// +// - name: My first play +// hosts: myhosts +// tasks: +// - name: Ping my hosts +// ping: +// +// This play contains a name, target hosts, and a list of tasks. +type Play struct { + inner playInner + + src fsutils.FileSource + metadata iacTypes.Metadata + rng Range + + raw map[string]any +} + +func (p *Play) Hosts() string { + return p.inner.Hosts +} + +func (p *Play) Variables() vars.Vars { + return vars.NewVars(p.inner.Vars, vars.PlayVarsPriority) +} + +type playInner struct { + Name string `yaml:"name"` + ImportPlaybook string `yaml:"import_playbook"` + Hosts string `yaml:"hosts"` + RoleDefinitions []*RoleDefinition `yaml:"roles"` + PreTasks []*Task `yaml:"pre_tasks"` + Tasks []*Task `yaml:"tasks"` + PostTasks []*Task `yaml:"post_tasks"` + Vars vars.PlainVars `yaml:"vars"` + VarFiles []string `yaml:"var_files"` +} + +func (p *Play) UnmarshalYAML(node *yaml.Node) error { + p.rng = rangeFromNode(node) + + if err := node.Decode(&p.raw); err != nil { + return err + } + + if err := node.Decode(&p.inner); err != nil { + return err + } + + for _, task := range p.listTasks() { + task.play = p + } + + return nil +} + +// includedPlaybook returns the path of an included or imported playbook within the play. +func (p *Play) includedPlaybook() (string, bool) { + for _, k := range withBuiltinPrefix("import_playbook", "include_playbook") { + val, exists := p.raw[k] + if !exists { + continue + } + + // TODO: render Jinja2 template in playbookPath before returning + // For example, if playbookPath == "{{ playbook_dir }}/common.yml" + // then use a template engine to replace {{ playbook_dir }} with actual path. + + // TODO: support collections syntax like "my_namespace.my_collection.my_playbook" + // + // Example: + // - name: Include a playbook from a collection + // ansible.builtin.import_playbook: my_namespace.my_collection.my_playbook + // + // convert this to a real file path by locating the collection directory + // and appending "my_playbook.yml" or similar. + + playbookPath, ok := val.(string) + return filepath.ToSlash(playbookPath), ok + } + + return "", false +} + +func (p *Play) roleDefinitions() []*RoleDefinition { + return p.inner.RoleDefinitions +} + +func (p *Play) initMetadata(fileSrc fsutils.FileSource, parent *iacTypes.Metadata) { + fsys, relPath := fileSrc.FSAndRelPath() + + p.src = fileSrc + p.metadata = iacTypes.NewMetadata( + iacTypes.NewRange(relPath, p.rng.Start, p.rng.End, "", fsys), + "play", + ) + p.metadata.SetParentPtr(parent) + + for _, roleDef := range p.inner.RoleDefinitions { + roleDef.initMetadata(fileSrc, &p.metadata) + } + + for _, task := range p.listTasks() { + task.init(p, fileSrc, &p.metadata) + } +} + +func (p *Play) listTasks() []*Task { + res := make([]*Task, 0, len(p.inner.PreTasks)+len(p.inner.Tasks)+len(p.inner.PostTasks)) + res = append(res, p.inner.PreTasks...) + res = append(res, p.inner.Tasks...) + res = append(res, p.inner.PostTasks...) + return res +} + +// https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html +func (p *Play) specialVars() vars.Vars { + plainVars := vars.PlainVars{ + "ansible_play_name": p.inner.Name, + "playbook_dir": p.src.Dir(), + } + return vars.NewVars(plainVars, vars.SpecialVarsPriority) +} + +// RoleDefinition represents a role reference within a play. +// +// It typically contains the role name and optional parameters +// that customize how the role is applied. +// +// Example usage in a playbook: +// +// roles: +// - common +// - role: webserver +// vars: +// http_port: 80 +type RoleDefinition struct { + inner roleDefinitionInner + + metadata iacTypes.Metadata + rng Range +} + +type roleDefinitionInner struct { + Name string `yaml:"role"` + Vars map[string]any `yaml:"vars"` +} + +func (r *RoleDefinition) UnmarshalYAML(node *yaml.Node) error { + r.rng = rangeFromNode(node) + + // a role can be a string or a dictionary + if node.Kind == yaml.ScalarNode { + r.inner.Name = node.Value + return nil + } + + return node.Decode(&r.inner) +} + +func (r *RoleDefinition) initMetadata(fileSrc fsutils.FileSource, parent *iacTypes.Metadata) { + fsys, relPath := fileSrc.FSAndRelPath() + r.metadata = iacTypes.NewMetadata( + iacTypes.NewRange(relPath, r.rng.Start, r.rng.End, "", fsys), + "role-def", + ) + r.metadata.SetParentPtr(parent) +} + +func (r *RoleDefinition) name() string { + return r.inner.Name +} diff --git a/pkg/iac/scanners/ansible/parser/range.go b/pkg/iac/scanners/ansible/parser/range.go new file mode 100644 index 000000000000..c298bbfa6c76 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/range.go @@ -0,0 +1,31 @@ +package parser + +import "gopkg.in/yaml.v3" + +type Range struct { + Start int + End int +} + +// Covers returns true if 'r' fully contains 'other'. +func (r Range) Covers(other Range) bool { + return r.Start <= other.Start && r.End >= other.End +} + +func (r Range) Overlaps(o Range) bool { + return r.Start < o.End && o.Start < r.End +} + +func rangeFromNode(node *yaml.Node) Range { + return Range{ + Start: node.Line, + End: calculateEndLine(node), + } +} + +func calculateEndLine(node *yaml.Node) int { + for node.Content != nil { + node = node.Content[len(node.Content)-1] + } + return node.Line +} diff --git a/pkg/iac/scanners/ansible/parser/role.go b/pkg/iac/scanners/ansible/parser/role.go new file mode 100644 index 000000000000..0decbb5455ee --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/role.go @@ -0,0 +1,148 @@ +package parser + +import ( + "errors" + "io/fs" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +// Role represent project role +type Role struct { + name string + src fsutils.FileSource + metadata iacTypes.Metadata + play *Play + + cachedTasks map[string][]*Task + + directDeps []*Role +} + +func (r *Role) initMetadata(fileSrc fsutils.FileSource, parent *iacTypes.Metadata) { + fsys, relPath := fileSrc.FSAndRelPath() + rng := iacTypes.NewRange(relPath, 0, 0, "", fsys) + r.metadata = iacTypes.NewMetadata(rng, "role") + r.metadata.SetParentPtr(parent) +} + +func (r *Role) getTasks(tasksFile string) ([]*Task, error) { + if cached, ok := r.cachedTasks[tasksFile]; ok { + return cached, nil + } + + var allTasks []*Task + + for _, dep := range r.directDeps { + // TODO: find out how direct dependency tasks are loaded + depTasks, err := dep.getTasks("main") + if err != nil && !errors.Is(err, fs.ErrNotExist) { + log.WithPrefix("ansible").Debug("Failed to load dependency tasks", + log.String("dependency", dep.name)) + } else if err == nil { + allTasks = append(allTasks, depTasks...) + } + } + + tasksFileSrc := r.src.Join("tasks", tasksFile) + fileTasks, err := loadTasks(r.play, &r.metadata, tasksFileSrc) + if err != nil { + return nil, err + } + + for _, roleTask := range fileTasks { + roleTask.role = r + } + allTasks = append(allTasks, fileTasks...) + log.WithPrefix("ansible").Debug("Role tasks loaded", + log.FilePath(tasksFileSrc.Path), log.Int("tasks_count", len(allTasks))) + + r.cachedTasks[tasksFile] = allTasks + return allTasks, nil +} + +func (r *Role) fileVariables(from string) (vars.Vars, error) { + return r.loadVars("vars", vars.RoleVarsPriority, from) +} + +func (r *Role) defaultVariables(from string) (vars.Vars, error) { + return r.loadVars("defaults", vars.RoleDefaultsPriority, from) +} + +func (r *Role) loadVars(scope string, priority vars.VarPriority, from string) (vars.Vars, error) { + var variables vars.Vars + + walkFn := func(fs fsutils.FileSource, de fs.DirEntry) error { + if de.IsDir() { + return nil + } + + var plainFileVars vars.PlainVars + if err := decodeYAMLFileWithExtension(fs, &plainFileVars, vars.VarFilesExtensions); err != nil { + return xerrors.Errorf("load vars: %w", err) + } + fileVars := vars.NewVars(plainFileVars, priority) + variables = vars.MergeVars(variables, fileVars) + return nil + } + + varsSrc := r.src.Join(scope, from) + + // try load from file + var plainFileVars vars.PlainVars + if err := decodeYAMLFileWithExtension(varsSrc, &plainFileVars, vars.VarFilesExtensions); err == nil { + log.WithPrefix("ansible").Debug("Loaded vars file", log.FilePath(varsSrc.Path)) + variables = vars.NewVars(plainFileVars, priority) + return variables, nil + } + + if err := fsutils.WalkDirsFirstAlpha(varsSrc, walkFn); err != nil { + return nil, xerrors.Errorf("collect variables from %q: %w", varsSrc.Path, err) + } + + log.WithPrefix("ansible").Debug("Loaded vars from directory", + log.String("scope", scope), log.FilePath(varsSrc.Path)) + return variables, nil +} + +// https://docs.ansible.com/ansible/latest/reference_appendices/special_variables.html +func (r *Role) specialVars() vars.Vars { + plainVars := vars.PlainVars{ + "role_name": r.name, + "role_path": r.src, + } + return vars.NewVars(plainVars, vars.SpecialVarsPriority) +} + +type RoleMeta struct { + metadata iacTypes.Metadata + rng Range + inner roleMetaInner +} + +func (m *RoleMeta) updateMetadata(fsys fs.FS, parent *iacTypes.Metadata, path string) { + m.metadata = iacTypes.NewMetadata( + iacTypes.NewRange(path, m.rng.Start, m.rng.End, "", fsys), + "role-metadata", + ) + m.metadata.SetParentPtr(parent) +} + +func (m RoleMeta) dependencies() []*RoleDefinition { + return m.inner.Dependencies +} + +type roleMetaInner struct { + Dependencies []*RoleDefinition `yaml:"dependencies"` +} + +func (m *RoleMeta) UnmarshalYAML(node *yaml.Node) error { + m.rng = rangeFromNode(node) + return node.Decode(&m.inner) +} diff --git a/pkg/iac/scanners/ansible/parser/task.go b/pkg/iac/scanners/ansible/parser/task.go new file mode 100644 index 000000000000..33304604afdb --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/task.go @@ -0,0 +1,240 @@ +package parser + +import ( + "errors" + + "github.com/samber/lo" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/orderedmap" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +const ( + ModuleIncludeRole = "include_role" + ModuleImportRole = "import_role" + ModuleIncludeTasks = "include_tasks" + ModuleImportTasks = "import_tasks" +) + +// RoleIncludeModule represents the "include_role" or "import_role" module +type RoleIncludeModule struct { + Name string + TasksFrom string + DefaultsFrom string + VarsFrom string +} + +// Task represents a single Ansible task. +// +// A task defines a single unit of work, which may include running a module, +// calling a role, or including other task files. +// +// Tasks can contain parameters, conditions (when), loops, and other +// Ansible constructs. +// +// Example task: +// +// - name: Install nginx +// apt: +// name: nginx +// state: present +type Task struct { + inner taskInner + raw orderedmap.OrderedMap[string, *Node] + + rng Range + src fsutils.FileSource + metadata iacTypes.Metadata + + role *Role + play *Play +} + +func (t *Task) Variables() vars.Vars { + if t.isBlock() { + return vars.NewVars(t.inner.Vars, vars.BlockVarsPriority) + } + return vars.NewVars(t.inner.Vars, vars.TaskVarsPriority) +} + +type taskInner struct { + Name string `yaml:"name"` + Block []*Task `yaml:"block"` + Vars vars.PlainVars `yaml:"vars"` +} + +func (t *Task) UnmarshalYAML(node *yaml.Node) error { + t.rng = rangeFromNode(node) + + var rawMap orderedmap.OrderedMap[string, *Node] + if err := node.Decode(&rawMap); err != nil { + return err + } + + t.raw = rawMap + return node.Decode(&t.inner) +} + +func (t *Task) isBlock() bool { + return len(t.inner.Block) > 0 +} + +func (t *Task) init(play *Play, fileSrc fsutils.FileSource, parent *iacTypes.Metadata) { + fsys, relPath := fileSrc.FSAndRelPath() + ref := lo.Ternary(t.isBlock(), "tasks-block", "tasks") + rng := iacTypes.NewRange(relPath, t.rng.Start, t.rng.End, "", fsys) + t.play = play + t.src = fileSrc + t.metadata = iacTypes.NewMetadata(rng, ref) + t.metadata.SetParentPtr(parent) + + for _, tt := range t.inner.Block { + tt.init(play, fileSrc, parent) + } + + for _, n := range t.raw.Iter() { + if n == nil { + continue + } + n.initMetadata(fileSrc, &t.metadata, nil) + } +} + +// hasModuleKey checks if the task has any of the given module keys in its raw map. +func (t *Task) hasModuleKey(keys []string) bool { + for _, module := range keys { + if _, exists := t.raw.Get(module); exists { + return true + } + } + return false +} + +// isTaskInclude returns true if the task includes or imports other tasks (task include modules). +func (t *Task) isTaskInclude() bool { + return t.hasModuleKey(withBuiltinPrefix(ModuleImportTasks, ModuleIncludeTasks)) +} + +// isRoleInclude returns true if the task includes or imports a role (role include modules). +func (t *Task) isRoleInclude() bool { + return t.hasModuleKey(withBuiltinPrefix(ModuleImportRole, ModuleIncludeRole)) +} + +func (t *Task) resolved(variables vars.Vars) *ResolvedTask { + if variables == nil { + variables = make(vars.Vars) + } + resolved := &ResolvedTask{ + Name: t.inner.Name, + Metadata: t.metadata, + Vars: variables, + Range: t.rng, + Fields: t.raw, + } + + return resolved +} + +type ResolvedTasks []*ResolvedTask + +func (t ResolvedTasks) GetModules(keys ...string) []Module { + var modules []Module + + for _, task := range t { + m, err := task.ResolveModule(keys, false) + if err != nil { + if errors.Is(err, ErrModuleNotFound) { + continue + } + log.WithPrefix("ansible").Debug("Failed to find module", log.Err(err)) + continue + } + modules = append(modules, m) + } + + return modules +} + +func (t ResolvedTasks) FilterByState(exclude ...string) ResolvedTasks { + excludeSet := set.New(exclude...) + return lo.Filter(t, func(task *ResolvedTask, _ int) bool { + state, exists := task.Fields.Get("state") + if !exists || state == nil || !state.IsKnown() { + return true + } + if v, ok := state.AsString(); ok && excludeSet.Contains(v) { + return false + } + return true + }) +} + +// ResolvedTask represents an Ansible task with all variables resolved. +// +// It holds only the essential data needed for execution and +// ensures the original Task remains unmodified. +type ResolvedTask struct { + Name string + Fields orderedmap.OrderedMap[string, *Node] + Vars vars.Vars + + Metadata iacTypes.Metadata + Range Range +} + +var ErrModuleNotFound = errors.New("module not found") + +// ResolveModule searches for the first module from given keys in task fields, +// renders its parameters using task variables, and returns the module. +// The module can be either structured (map of parameters) or free-form (string). +// Returns an error if no module is found or if rendering fails. +func (t *ResolvedTask) ResolveModule(keys []string, strict bool) (Module, error) { + for _, key := range keys { + f, exists := t.Fields.Get(key) + if !exists { + continue + } + + // TODO: cache the module? + rendered, err := f.Render(t.Vars) + if err != nil { + if strict { + return Module{}, xerrors.Errorf("render: %w", err) + } + log.WithPrefix("ansible").Debug("Failed to render module params", + log.String("source", t.Metadata.Range().String()), + log.Err(err)) + } + return Module{Node: rendered, Name: key}, nil + } + return Module{}, ErrModuleNotFound +} + +func (t *ResolvedTask) MarshalYAML() (any, error) { + out := make(map[string]any, t.Fields.Len()) + for fieldName, field := range t.Fields.Iter() { + rendered, _ := field.Render(t.Vars) + out[fieldName] = rendered.val + } + return out, nil +} + +func (t *ResolvedTask) GetFieldsByRange(r Range) map[string]*Node { + out := make(map[string]*Node) + for key, node := range t.Fields.Iter() { + if node == nil { + continue + } + sub := node.Subtree(r) + if sub != nil { + out[key], _ = sub.Render(t.Vars) + } + } + return out +} diff --git a/pkg/iac/scanners/ansible/parser/task_test.go b/pkg/iac/scanners/ansible/parser/task_test.go new file mode 100644 index 000000000000..8de27edf47c5 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/task_test.go @@ -0,0 +1,126 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func TestResolvedTask_MarshalYAML(t *testing.T) { + src := `name: "{{ name }}" +msg: "{{ msg }}" +num: "{{ num }}" +nested: + foo: "foo" +` + + var task Task + require.NoError(t, decodeYAML([]byte(src), &task)) + + plainVars := vars.PlainVars{ + "name": "test task", + "msg": "hello", + "num": 42, + } + resolved := task.resolved(vars.NewVars(plainVars, 0)) + + data, err := yaml.Marshal(resolved) + require.NoError(t, err) + + got := string(data) + wantSubstrs := []string{ + "name: test task", + "msg: hello", + `num: "42"`, + `foo: foo`, + } + for _, substr := range wantSubstrs { + assert.Contains(t, got, substr) + } +} + +func TestResolvedTasks_GetModules(t *testing.T) { + src := ` +- name: s3 bucket present + amazon.aws.s3_bucket: + name: mybucket + +- name: user absent + ansible.builtin.user: + name: old_user + +- name: debug task + ansible.builtin.debug: + msg: "Hello" +` + var tasks []*Task + require.NoError(t, decodeYAML([]byte(src), &tasks)) + + var resolved ResolvedTasks + for _, t := range tasks { + resolved = append(resolved, t.resolved(nil)) + } + + modules := resolved.GetModules("amazon.aws.s3_bucket", "ansible.builtin.user") + var names []string + for _, m := range modules { + names = append(names, m.Name) + } + + expected := []string{"amazon.aws.s3_bucket", "ansible.builtin.user"} + assert.ElementsMatch(t, expected, names) +} + +func TestResolvedTasks_FilterByState(t *testing.T) { + src := ` +- name: task1 + state: present +- name: task2 + state: absent +- name: task3 + state: present +- name: task4 + # no state +` + var tasks []*Task + require.NoError(t, decodeYAML([]byte(src), &tasks)) + + var resolved ResolvedTasks + for _, t := range tasks { + resolved = append(resolved, t.resolved(nil)) + } + filtered := resolved.FilterByState("absent") + + names := xslices.Map(filtered, func(t *ResolvedTask) string { return t.Name }) + assert.ElementsMatch(t, []string{"task1", "task3", "task4"}, names) +} + +func TestResolvedTask_GetFieldsByRange(t *testing.T) { + src := `a: valueA +b: valueB +c: valueC +d: valueD +` + + var task *Task + require.NoError(t, decodeYAML([]byte(src), &task)) + + resolved := task.resolved(nil) + r := Range{Start: 2, End: 3} + + fields := resolved.GetFieldsByRange(r) + + expected := `b: valueB +c: valueC +` + + marshaled, err := yaml.Marshal(fields) + require.NoError(t, err) + + assert.Equal(t, expected, string(marshaled)) +} diff --git a/pkg/iac/scanners/ansible/parser/template.go b/pkg/iac/scanners/ansible/parser/template.go new file mode 100644 index 000000000000..b79c2379d9cf --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/template.go @@ -0,0 +1,143 @@ +package parser + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "strings" + "time" + + "github.com/nikolalohinski/gonja/v2" + "github.com/nikolalohinski/gonja/v2/config" + "github.com/nikolalohinski/gonja/v2/exec" + "github.com/nikolalohinski/gonja/v2/loaders" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" +) + +var gonjaConfig *config.Config +var gonjaEnv *exec.Environment + +// TODO: implement support for a subset of popular Ansible filter plugins. +// https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html#filter-plugins +// Example: see dirnameFilter for how the "dirname" filter is implemented. +func init() { + gonjaConfig = gonja.DefaultConfig.Inherit() + gonjaConfig.StrictUndefined = true + + gonjaEnv = gonja.DefaultEnvironment + gonjaEnv.Filters.Register("dirname", dirnameFilter) +} + +// TODO: add support for a subset of popular Ansible lookup plugins. +// Reference: https://docs.ansible.com/ansible/latest/collections/ansible/builtin/index.html#lookup-plugins +// +// Examples: "template" lookup, "vars" lookup. +// +// Idea: register lookup plugins in the execution context as functions, +// and then use this context as a parent context +// +// ectx := exec.NewContext(map[string]any{ +// "lookup": func(args *exec.VarArgs) (any, error) { +// ... +// if args.Args[0].String() == "template" { +// // read and evaluate template +// } +// +// ... +// return nil, fmt.Errorf("unsupported lookup plugin %s", args.Args[0].String()) +// }, +// }) + +// evaluateTemplateSafe executes a Gonja template with given variables safely. +// It prevents infinite loops and recovers from panics. +// Added due to infinite loop issue: https://github.com/NikolaLohinski/gonja/issues/52 +func evaluateTemplate(input string, variables vars.Vars) (string, error) { + type result struct { + res string + err error + } + + resultCh := make(chan result, 1) + + // Run the template evaluation in a separate goroutine + // to prevent infinite loops or long-running evaluation + go func() { + // Catch any panic that may occur during template evaluation + defer func() { + if r := recover(); r != nil { + resultCh <- result{"", fmt.Errorf("template evaluation panic: %v", r)} + } + }() + + res, err := evaluateTemplateUnsafe(input, variables) + resultCh <- result{res, err} + }() + + // Wait for evaluation to finish or timeout after 2 seconds + timeout := time.Second * 2 + select { + case r := <-resultCh: + return r.res, r.err + case <-time.After(timeout): + return "", fmt.Errorf("template evaluation timeout after %s", timeout) + } +} + +// evaluateTemplate evaluates a template with given variables. +func evaluateTemplateUnsafe(input string, variables vars.Vars) (string, error) { + tpl, err := newTemplate(input) + if err != nil { + return "", xerrors.Errorf("init template: %w", err) + } + + var buf bytes.Buffer + + if err := tpl.Execute(&buf, exec.NewContext(variables.ToPlain())); err != nil { + return "", xerrors.Errorf("execute template: %w", err) + } + return buf.String(), nil +} + +// newTemplate creates a new template. This function is similar to [gonja.FromBytes], +// but applies a custom configuration. +func newTemplate(input string) (*exec.Template, error) { + rootID := fmt.Sprintf("root-%s", string(sha256.New().Sum([]byte(input)))) + + loader, err := loaders.NewFileSystemLoader("") + if err != nil { + return nil, xerrors.Errorf("create fs loader: %w", err) + } + + shiftedLoader, err := loaders.NewShiftedLoader(rootID, strings.NewReader(input), loader) + if err != nil { + return nil, xerrors.Errorf("create shifted loader: %w", err) + } + + tpl, err := exec.NewTemplate(rootID, gonjaConfig, shiftedLoader, gonjaEnv) + if err != nil { + return nil, xerrors.Errorf("create new template: %w", err) + } + + return tpl, nil +} + +func dirnameFilter(_ *exec.Evaluator, in *exec.Value, params *exec.VarArgs) *exec.Value { + if in == nil { + return exec.ValueError(errors.New("input value is nil")) + } + + if params != nil && len(params.Args) > 0 { + return exec.ValueError(errors.New("no parameters allowed")) + } + + switch val := in.Val.Interface().(type) { + case fsutils.FileSource: + return exec.AsSafeValue(val.Dir()) + default: + return exec.ValueError(fmt.Errorf("unsupported type %T", in.Val.Interface())) + } +} diff --git a/pkg/iac/scanners/ansible/parser/template_test.go b/pkg/iac/scanners/ansible/parser/template_test.go new file mode 100644 index 000000000000..2ee5856961bf --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/template_test.go @@ -0,0 +1,76 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/vars" +) + +func TestEvaluateTemplate(t *testing.T) { + tests := []struct { + name string + input string + vars vars.PlainVars + expected string + expectErr bool + }{ + { + name: "simple variable", + input: "Hello {{ name }}", + vars: vars.PlainVars{"name": "World"}, + expected: "Hello World", + }, + { + name: "arithmetic", + input: "{{ a + b }}", + vars: vars.PlainVars{"a": 2, "b": 3}, + expected: "5", + }, + { + name: "if else true", + input: "{% if flag %}Yes{% else %}No{% endif %}", + vars: vars.PlainVars{"flag": true}, + expected: "Yes", + }, + { + name: "if else false", + input: "{% if flag %}Yes{% else %}No{% endif %}", + vars: vars.PlainVars{"flag": false}, + expected: "No", + }, + { + name: "invalid template", + input: "{{ foo ", + vars: vars.PlainVars{}, + expectErr: true, + }, + { + name: "missing variable", + input: "Hello {{ name }}", + vars: vars.PlainVars{}, + expectErr: true, + }, + { + name: "multiple variables", + input: "{{ greeting }}, {{ name }}!", + vars: vars.PlainVars{"greeting": "Hi", "name": "Alice"}, + expected: "Hi, Alice!", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + variables := vars.NewVars(tt.vars, 0) + got, err := evaluateTemplate(tt.input, variables) + if tt.expectErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/ansible/parser/testdata/sample-proj/inventory/.gitkeep b/pkg/iac/scanners/ansible/parser/testdata/sample-proj/inventory/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/iac/scanners/ansible/parser/testdata/sample-proj/playbook.yaml b/pkg/iac/scanners/ansible/parser/testdata/sample-proj/playbook.yaml new file mode 100644 index 000000000000..492c12858147 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/testdata/sample-proj/playbook.yaml @@ -0,0 +1,34 @@ +--- +- name: Update web servers + hosts: webservers + remote_user: root + + tasks: + - name: Ensure apache is at the latest version + s3_bucket: + name: mys3bucket + public_access: + ignore_public_acls: true + + - name: Write the apache config file + ansible.builtin.template: + src: /srv/httpd.j2 + dest: /etc/httpd.conf + + roles: + - test + +- name: Update db servers + hosts: databases + remote_user: root + + tasks: + - name: Ensure postgresql is at the latest version + ansible.builtin.yum: + name: postgresql + state: latest + + - name: Ensure that postgresql is started + ansible.builtin.service: + name: postgresql + state: started \ No newline at end of file diff --git a/pkg/iac/scanners/ansible/parser/testdata/sample-proj/roles/test/meta/main.yaml b/pkg/iac/scanners/ansible/parser/testdata/sample-proj/roles/test/meta/main.yaml new file mode 100644 index 000000000000..aac0ddbf8706 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/testdata/sample-proj/roles/test/meta/main.yaml @@ -0,0 +1,2 @@ +dependencies: + - role: test2 \ No newline at end of file diff --git a/pkg/iac/scanners/ansible/parser/testdata/sample-proj/roles/test/tasks/main.yaml b/pkg/iac/scanners/ansible/parser/testdata/sample-proj/roles/test/tasks/main.yaml new file mode 100644 index 000000000000..2358089f9ce4 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/testdata/sample-proj/roles/test/tasks/main.yaml @@ -0,0 +1,3 @@ +- name: Role task + s3_bucket: + name: test1-bucket \ No newline at end of file diff --git a/pkg/iac/scanners/ansible/parser/testdata/sample-proj/roles/test2/tasks/main.yaml b/pkg/iac/scanners/ansible/parser/testdata/sample-proj/roles/test2/tasks/main.yaml new file mode 100644 index 000000000000..aa1e9832f91d --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/testdata/sample-proj/roles/test2/tasks/main.yaml @@ -0,0 +1,3 @@ +- name: Role2 task + s3_bucket: + name: test2-bucket \ No newline at end of file diff --git a/pkg/iac/scanners/ansible/parser/yaml.go b/pkg/iac/scanners/ansible/parser/yaml.go new file mode 100644 index 000000000000..e27767d5f713 --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/yaml.go @@ -0,0 +1,109 @@ +package parser + +import ( + "io/fs" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/fsutils" +) + +const ( + NullTag = "!!null" + StrTag = "!!str" +) + +func decodeYAMLFileWithExtension(fileSrc fsutils.FileSource, dst any, extensions []string) error { + for _, ext := range extensions { + f := fsutils.FileSource{FS: fileSrc.FS, Path: fileSrc.Path + ext} + if exists, _ := f.Exists(); exists { + return decodeYAMLFile(f, dst) + } + } + return fs.ErrNotExist +} + +func decodeYAMLFile(f fsutils.FileSource, dst any) error { + data, err := f.ReadFile() + if err != nil { + return xerrors.Errorf("read file %s: %w", f.Path, err) + } + + if err := decodeYAML(data, dst); err != nil { + return xerrors.Errorf("decode %s: %w", f.Path, err) + } + return nil +} + +func decodeYAML(data []byte, dst any) error { + var root yaml.Node + if err := yaml.Unmarshal(data, &root); err != nil { + return err + } + unwrapTemplates(&root) + return root.Decode(dst) +} + +// unwrapTemplates recursively traverses a YAML node tree and converts +// any double-mapping nodes that represent templates (like {{ key }}) +// into scalar nodes with the template string. +// +// Specifically, it detects nodes of the form: +// +// MappingNode +// Content[0]: MappingNode +// Content[0]: ScalarNode (key) +// Content[1]: ScalarNode null +// Content[1]: ScalarNode null +// +// and converts them into: +// +// ScalarNode "{{ key }}" +func unwrapTemplates(n *yaml.Node) { + walk(n, func(node *yaml.Node) bool { + if node.Kind != yaml.MappingNode || len(node.Content) != 2 { + return false + } + + innerKey := node.Content[0] + innerVal := node.Content[1] + + if innerKey.Kind == yaml.MappingNode && + len(innerKey.Content) == 2 && + innerVal.Tag == NullTag && + innerKey.Content[0].Kind == yaml.ScalarNode && + innerKey.Content[0].Tag == StrTag && + innerKey.Content[1].Tag == NullTag { + + node.Kind = yaml.ScalarNode + node.Tag = StrTag + node.Value = "{{ " + innerKey.Content[0].Value + " }}" + node.Content = nil + return true + } + + return false + }) +} + +// walk traverses a YAML node tree and calls fn on each node. +// If fn returns true, walk stops traversing that branch. +func walk(n *yaml.Node, fn func(*yaml.Node) (stop bool)) bool { + if fn(n) { + return true + } + switch n.Kind { + case yaml.DocumentNode, yaml.SequenceNode: + for _, c := range n.Content { + walk(c, fn) + } + case yaml.MappingNode: + for i := 0; i < len(n.Content); i += 2 { + walk(n.Content[i], fn) + walk(n.Content[i+1], fn) + } + case yaml.ScalarNode: + } + return false +} diff --git a/pkg/iac/scanners/ansible/parser/yaml_test.go b/pkg/iac/scanners/ansible/parser/yaml_test.go new file mode 100644 index 000000000000..4b7eb4ea580d --- /dev/null +++ b/pkg/iac/scanners/ansible/parser/yaml_test.go @@ -0,0 +1,90 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestUnwrapTemplates(t *testing.T) { + tests := []struct { + name string + src string + expected string + }{ + { + name: "single template", + src: ` +{{ name }} +`, + expected: `'{{ name }}' +`, + }, + { + name: "nested template in mapping", + src: `test: + {{ nested }} +`, + expected: `test: '{{ nested }}' +`, + }, + { + name: "sequence with templates", + src: `- {{ first }} +- value +- {{ second }} +`, + expected: `- '{{ first }}' +- value +- '{{ second }}' +`, + }, + { + name: "non-template values remain", + src: `plain: value +seq: + - 123 + - text +`, + expected: `plain: value +seq: + - 123 + - text +`, + }, + { + name: "mapping remains unchanged", + src: `simple_map: { key1: val1, key2: val2 } +nested_map: + inner: { a: 1, b: 2 } +`, + expected: `simple_map: {key1: val1, key2: val2} +nested_map: + inner: {a: 1, b: 2} +`, + }, + { + name: "template inside inline mapping", + src: `inline_map: { value: {{ name }}, static: fixed }`, + expected: `inline_map: {value: '{{ name }}', static: fixed} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var root yaml.Node + err := yaml.Unmarshal([]byte(tt.src), &root) + require.NoError(t, err) + + unwrapTemplates(&root) + + out, err := yaml.Marshal(&root) + require.NoError(t, err) + + assert.Equal(t, tt.expected, string(out)) + }) + } +} diff --git a/pkg/iac/scanners/ansible/scanner.go b/pkg/iac/scanners/ansible/scanner.go new file mode 100644 index 000000000000..1e8f65dd06e8 --- /dev/null +++ b/pkg/iac/scanners/ansible/scanner.go @@ -0,0 +1,150 @@ +package ansible + +import ( + "context" + "fmt" + "io/fs" + + "gopkg.in/yaml.v3" + + adapter "github.com/aquasecurity/trivy/pkg/iac/adapters/ansible" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible/parser" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +var ( + _ scanners.FSScanner = (*Scanner)(nil) + _ options.ConfigurableScanner = (*Scanner)(nil) +) + +type Scanner struct { + *rego.RegoScannerProvider + opts []options.ScannerOption + parserOpts []parser.Option +} + +func WithPlaybooks(playbooks []string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.parserOpts = append(ss.parserOpts, parser.WithPlaybooks(playbooks...)) + } + } +} + +func WithInventories(inventories []string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.parserOpts = append(ss.parserOpts, parser.WithInventories(inventories...)) + } + } +} + +func WithExtraVars(evars map[string]any) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if ss, ok := s.(*Scanner); ok { + ss.parserOpts = append(ss.parserOpts, parser.WithExtraVars(evars)) + } + } +} + +func New(opts ...options.ScannerOption) *Scanner { + scanner := &Scanner{ + RegoScannerProvider: rego.NewRegoScannerProvider(opts...), + opts: opts, + } + for _, opt := range opts { + opt(scanner) + } + return scanner +} + +func (s *Scanner) Name() string { + return "Ansible" +} + +func (s *Scanner) ScanFS(ctx context.Context, fsys fs.FS, dir string) (scan.Results, error) { + roots, err := parser.FindProjects(fsys, dir) + if err != nil { + return nil, fmt.Errorf("find projects: %w", err) + } + + var results scan.Results + for _, projectRoot := range roots { + log.WithPrefix("ansible").Debug("Detected ansible project", log.FilePath(projectRoot)) + project, err := parser.ParseProject(fsys, projectRoot, s.parserOpts...) + if err != nil { + return nil, fmt.Errorf("parse project: %w", err) + } + res, err := s.scanProject(ctx, fsys, project) + if err != nil { + return nil, fmt.Errorf("scan project: %w", err) + } + results = append(results, res...) + } + return results, nil +} + +func (s *Scanner) scanProject(ctx context.Context, fsys fs.FS, project *parser.AnsibleProject) (scan.Results, error) { + tasks := project.ListTasks().FilterByState("absent") + state := adapter.Adapt(tasks) + + rs, err := s.InitRegoScanner(fsys, s.opts) + if err != nil { + return nil, fmt.Errorf("init rego scanner: %w", err) + } + + results, err := rs.ScanInput(ctx, types.SourceCloud, rego.Input{ + Path: project.Path(), + FS: fsys, + Contents: state.ToRego(), + }) + if err != nil { + return nil, fmt.Errorf("rego scan: %w", err) + } + + for i, res := range results { + if res.Status() != scan.StatusFailed { + continue + } + + rendered, ok := renderCause(tasks, res.Range()) + if ok { + res.WithRenderedCause(rendered) + results[i] = res + } + } + + return results, nil +} + +func renderCause(tasks parser.ResolvedTasks, causeRng types.Range) (scan.RenderedCause, bool) { + fields := fieldsForRange(tasks, causeRng) + if fields == nil { + return scan.RenderedCause{}, false + } + + b, err := yaml.Marshal(fields) + if err != nil { + return scan.RenderedCause{}, false + } + return scan.RenderedCause{Raw: string(b)}, true +} + +func fieldsForRange(tasks parser.ResolvedTasks, causeRng types.Range) any { + for _, task := range tasks { + taskRng := task.Metadata.Range() + if taskRng.GetFilename() == causeRng.GetFilename() && taskRng.Includes(causeRng) { + queryRange := parser.Range{ + Start: causeRng.GetStartLine(), + End: causeRng.GetEndLine(), + } + return task.GetFieldsByRange(queryRange) + } + } + return nil +} diff --git a/pkg/iac/scanners/ansible/scanner_test.go b/pkg/iac/scanners/ansible/scanner_test.go new file mode 100644 index 000000000000..0298003c756a --- /dev/null +++ b/pkg/iac/scanners/ansible/scanner_test.go @@ -0,0 +1,39 @@ +package ansible + +import ( + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/rego" +) + +func TestBasicScan(t *testing.T) { + fsys := fstest.MapFS{ + "playbook.yaml": { + Data: []byte(`--- +- name: Update web servers + hosts: localhost + + tasks: + - name: Ensure apache is at the latest version + s3_bucket: + name: mys3bucket + public_access: +`), + }, + } + + scanner := New( + rego.WithEmbeddedLibraries(true), + rego.WithEmbeddedPolicies(true), + ) + + results, err := scanner.ScanFS(t.Context(), fsys, ".") + require.NoError(t, err) + + failed := results.GetFailed() + assert.NotEmpty(t, failed) +} diff --git a/pkg/iac/scanners/ansible/vars/vars.go b/pkg/iac/scanners/ansible/vars/vars.go new file mode 100644 index 000000000000..ed2012c81828 --- /dev/null +++ b/pkg/iac/scanners/ansible/vars/vars.go @@ -0,0 +1,148 @@ +package vars + +import ( + "fmt" + "maps" + + "github.com/aquasecurity/trivy/pkg/log" +) + +// VarPriority represents the priority level of a variable. +// Higher values indicate higher precedence when merging variables. +type VarPriority int + +const ( + RoleDefaultsPriority VarPriority = iota + 1 + InvFileGroupPriority + InvExtAllGroupPriority + PbExtAllGroupPriority + InvExtGroupPriority + PbExtGroupPriority + InvFileHostPriority + InvExtHostPriority + PbExtHostPriority + PlayVarsPriority + PlayVarsFilesPriority + RoleVarsPriority + BlockVarsPriority + TaskVarsPriority + ExtraVarsPriority + // Special variables cannot be set directly by the user; + // Ansible will always override them to reflect internal state. + SpecialVarsPriority +) + +func (p VarPriority) Source() string { + switch p { + case RoleDefaultsPriority: + return "role defaults" + case InvFileGroupPriority: + return "group vars (file)" + case InvExtAllGroupPriority: + return "group_vars/all (inv)" + case PbExtAllGroupPriority: + return "group_vars/all (pb)" + case InvExtGroupPriority: + return "group_vars/* (inv)" + case PbExtGroupPriority: + return "group_vars/* (pb)" + case InvFileHostPriority: + return "host vars (file)" + case InvExtHostPriority: + return "host_vars/* (inv)" + case PbExtHostPriority: + return "host_vars/* (pb)" + case PlayVarsPriority: + return "play" + case PlayVarsFilesPriority: + return "role (defaults)" + case RoleVarsPriority: + return "role" + case BlockVarsPriority: + return "block" + case TaskVarsPriority: + return "task" + case ExtraVarsPriority: + return "extra" + case SpecialVarsPriority: + return "special" + default: + return "unknown" + } +} + +var VarFilesExtensions = []string{"", ".yml", ".yaml", ".json"} + +// Variable represents a variable with its value and priority. +type Variable struct { + Value any + Priority VarPriority +} + +func NewVariable(val any, priority VarPriority) Variable { + return Variable{ + Value: val, + Priority: priority, + } +} + +// PlainVars is a simple map from variable names to their values. +type PlainVars map[string]any + +// Vars represents a set of variables as a map from string keys to Variable. +type Vars map[string]Variable + +// NewVars creates a Vars map from a plain map[string]any, assigning +// the given priority to each variable. +func NewVars(values PlainVars, priority VarPriority) Vars { + v := make(Vars, len(values)) + for k, val := range values { + v[k] = Variable{ + Value: val, + Priority: priority, + } + } + return v +} + +// ToPlain returns a plain map[string]any with only variable values, +// discarding Priority and Source information. +func (v Vars) ToPlain() map[string]any { + plain := make(map[string]any, len(v)) + for k, variable := range v { + plain[k] = variable.Value + } + return plain +} + +// Clone creates a shallow copy of Vars. +func (v Vars) Clone() Vars { + if v == nil { + return nil + } + c := make(Vars, len(v)) + maps.Copy(c, v) + return c +} + +func MergeVars(varsList ...Vars) Vars { + result := Vars{} + for _, vars := range varsList { + for k, newVar := range vars { + if existing, ok := result[k]; ok { + if newVar.Priority < existing.Priority { + log.WithPrefix("ansible").Debug( + fmt.Sprintf( + "Overwriting variable %q from %s (priority %d) with value from %s (priority %d)", + k, + existing.Priority.Source(), existing.Priority, + newVar.Priority.Source(), newVar.Priority, + ), + ) + } + } + result[k] = newVar + } + } + return result +} diff --git a/pkg/iac/scanners/azure/arm/parser/parser.go b/pkg/iac/scanners/azure/arm/parser/parser.go new file mode 100644 index 000000000000..bbe6250a9607 --- /dev/null +++ b/pkg/iac/scanners/azure/arm/parser/parser.go @@ -0,0 +1,135 @@ +package parser + +import ( + "context" + "fmt" + "io/fs" + + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure/resolver" + "github.com/aquasecurity/trivy/pkg/log" +) + +type Parser struct { + targetFS fs.FS + logger *log.Logger +} + +func New(targetFS fs.FS) *Parser { + return &Parser{ + targetFS: targetFS, + logger: log.WithPrefix("arm parser"), + } +} + +func (p *Parser) ParseFS(ctx context.Context, dir string) ([]azure.Deployment, error) { + + var deployments []azure.Deployment + + if err := fs.WalkDir(p.targetFS, dir, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + if entry.IsDir() { + return nil + } + + deployment, err := p.parseFile(path) + if err != nil { + p.logger.Error("Failed to parse file", log.FilePath(path), log.Err(err)) + return nil + } + + deployments = append(deployments, *deployment) + return nil + }); err != nil { + return nil, err + } + + return deployments, nil +} + +func (p *Parser) parseFile(path string) (*azure.Deployment, error) { + template, err := ParseTemplate(p.targetFS, path) + if err != nil { + return nil, fmt.Errorf("parse template: %w", err) + } + return p.convertTemplate(template), nil +} + +func (p *Parser) convertTemplate(template *Template) *azure.Deployment { + + deployment := azure.Deployment{ + Metadata: lo.FromPtr(template.Metadata), + TargetScope: azure.ScopeResourceGroup, // TODO: override from --resource-group? + Parameters: nil, + Variables: nil, + Resources: nil, + Outputs: nil, + } + + if r, ok := template.Metadata.Internal().(resolver.Resolver); ok { + r.SetDeployment(&deployment) + } + + // TODO: the references passed here should probably not be the name - maybe params.NAME.DefaultValue? + for name, param := range template.Parameters { + deployment.Parameters = append(deployment.Parameters, azure.Parameter{ + Variable: azure.Variable{ + Name: name, + Value: param.DefaultValue, + }, + Default: param.DefaultValue, + Decorators: nil, + }) + } + + for name, variable := range template.Variables { + deployment.Variables = append(deployment.Variables, azure.Variable{ + Name: name, + Value: variable, + }) + } + + for name, output := range template.Outputs { + deployment.Outputs = append(deployment.Outputs, azure.Output{ + Name: name, + Value: output, + }) + } + + for _, resource := range template.Resources { + deployment.Resources = append(deployment.Resources, p.convertResource(resource)) + } + + return &deployment +} + +func (p *Parser) convertResource(input Resource) azure.Resource { + + var children []azure.Resource + + for _, child := range input.Resources { + children = append(children, p.convertResource(child)) + } + + resource := azure.Resource{ + Metadata: lo.FromPtr(input.Metadata), + APIVersion: input.APIVersion, + Type: input.Type, + Kind: input.Kind, + Name: input.Name, + Location: input.Location, + Properties: input.Properties, + Resources: children, + } + + return resource +} diff --git a/pkg/iac/scanners/azure/arm/parser/parser_test.go b/pkg/iac/scanners/azure/arm/parser/parser_test.go new file mode 100644 index 000000000000..28fd31e279c3 --- /dev/null +++ b/pkg/iac/scanners/azure/arm/parser/parser_test.go @@ -0,0 +1,347 @@ +package parser + +import ( + "io/fs" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure/resolver" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func createMetadata(targetFS fs.FS, filename string, start, end int, ref string, parent *types.Metadata) types.Metadata { + child := types.NewMetadata(types.NewRange(filename, start, end, "", targetFS), ref) + if parent != nil { + child.SetParentPtr(parent) + } + return child +} + +func TestParser_Parse(t *testing.T) { + filename := "example.json" + + tests := []struct { + name string + input string + want func(fsys fs.FS) azure.Deployment + }{ + { + name: "basic param", + input: `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", // another one + "contentVersion": "1.0.0.0", + "parameters": { + "storagePrefix": { + "type": "string", + "defaultValue": "x", + "maxLength": 11, + "minLength": 3 + } + }, + "resources": [] +}`, + want: func(fsys fs.FS) azure.Deployment { + root := createMetadata(fsys, filename, 0, 0, "", nil).WithInternal(resolver.NewResolver()) + metadata := createMetadata(fsys, filename, 1, 13, "", &root) + storageMetadata := createMetadata(fsys, filename, 5, 10, "parameters.storagePrefix", &metadata) + + return azure.Deployment{ + Metadata: metadata, + TargetScope: azure.ScopeResourceGroup, + Parameters: []azure.Parameter{ + { + Variable: azure.Variable{ + Name: "storagePrefix", + Value: azure.NewValue("x", createMetadata(fsys, filename, 7, 7, "parameters.storagePrefix.defaultValue", &storageMetadata)), + }, + Default: azure.NewValue("x", createMetadata(fsys, filename, 7, 7, "parameters.storagePrefix.defaultValue", &storageMetadata)), + Decorators: nil, + }, + }, + } + }, + }, + { + name: "storageAccount", + input: `{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", // another one + "contentVersion": "1.0.0.0", + "parameters": {}, + "resources": [ +{ + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2022-05-01", + "name": "myResource", + "location": "string", + "tags": { + "tagName1": "tagValue1", + "tagName2": "tagValue2" + }, + "sku": { + "name": "string" + }, + "kind": "string", + "extendedLocation": { + "name": "string", + "type": "EdgeZone" + }, + "identity": { + "type": "string", + "userAssignedIdentities": {} + }, + "properties": { + "allowSharedKeyAccess":false, + "customDomain": { + "name": "string", + "useSubDomainName":false, + "number": 123, + "expr": "[toLower('Production')]" + }, + "networkAcls": [ + { + "bypass": "AzureServices1" + }, + { + "bypass": "AzureServices2" + } + ] + } +} +] +}`, + want: func(fsys fs.FS) azure.Deployment { + rootMetadata := createMetadata(fsys, filename, 0, 0, "", nil).WithInternal(resolver.NewResolver()) + fileMetadata := createMetadata(fsys, filename, 1, 46, "", &rootMetadata) + + resourceMetadata := createMetadata(fsys, filename, 6, 44, "resources[0]", &fileMetadata) + + propertiesMetadata := createMetadata(fsys, filename, 27, 43, "resources[0].properties", &resourceMetadata) + customDomainMetadata := createMetadata(fsys, filename, 29, 34, "resources[0].properties.customDomain", &propertiesMetadata) + networkACLListMetadata := createMetadata(fsys, filename, 35, 42, "resources[0].properties.networkAcls", &propertiesMetadata) + + networkACL0Metadata := createMetadata(fsys, filename, 36, 38, "resources[0].properties.networkAcls[0]", &networkACLListMetadata) + networkACL1Metadata := createMetadata(fsys, filename, 39, 41, "resources[0].properties.networkAcls[1]", &networkACLListMetadata) + + return azure.Deployment{ + Metadata: fileMetadata, + TargetScope: azure.ScopeResourceGroup, + Resources: []azure.Resource{ + { + Metadata: resourceMetadata, + APIVersion: azure.NewValue( + "2022-05-01", + createMetadata(fsys, filename, 8, 8, "resources[0].apiVersion", &resourceMetadata), + ), + Type: azure.NewValue( + "Microsoft.Storage/storageAccounts", + createMetadata(fsys, filename, 7, 7, "resources[0].type", &resourceMetadata), + ), + Kind: azure.NewValue( + "string", + createMetadata(fsys, filename, 18, 18, "resources[0].kind", &resourceMetadata), + ), + Name: azure.NewValue( + "myResource", + createMetadata(fsys, filename, 9, 9, "resources[0].name", &resourceMetadata), + ), + Location: azure.NewValue( + "string", + createMetadata(fsys, filename, 10, 10, "resources[0].location", &resourceMetadata), + ), + Properties: azure.NewValue( + map[string]azure.Value{ + "allowSharedKeyAccess": azure.NewValue(false, createMetadata(fsys, filename, 28, 28, "resources[0].properties.allowSharedKeyAccess", &propertiesMetadata)), + "customDomain": azure.NewValue( + map[string]azure.Value{ + "name": azure.NewValue("string", createMetadata(fsys, filename, 30, 30, "resources[0].properties.customDomain.name", &customDomainMetadata)), + "useSubDomainName": azure.NewValue(false, createMetadata(fsys, filename, 31, 31, "resources[0].properties.customDomain.useSubDomainName", &customDomainMetadata)), + "number": azure.NewValue(int64(123), createMetadata(fsys, filename, 32, 32, "resources[0].properties.customDomain.number", &customDomainMetadata)), + "expr": azure.NewExprValue("toLower('Production')", createMetadata(fsys, filename, 33, 33, "resources[0].properties.customDomain.expr", &customDomainMetadata)), + }, customDomainMetadata), + "networkAcls": azure.NewValue( + []azure.Value{ + azure.NewValue( + map[string]azure.Value{ + "bypass": azure.NewValue("AzureServices1", createMetadata(fsys, filename, 37, 37, "resources[0].properties.networkAcls[0].bypass", &networkACL0Metadata)), + }, + networkACL0Metadata, + ), + azure.NewValue( + map[string]azure.Value{ + "bypass": azure.NewValue("AzureServices2", createMetadata(fsys, filename, 40, 40, "resources[0].properties.networkAcls[1].bypass", &networkACL1Metadata)), + }, + networkACL1Metadata, + ), + }, networkACLListMetadata), + }, + propertiesMetadata, + ), + }, + }, + } + }, + }, + { + name: "resources defined as an object", + input: `{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "resources": { + "myacc": { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2025-06-01", + "name": "my-acc-test", + "location": "location", + "kind": "Storage" + } + } +}`, + want: func(fsys fs.FS) azure.Deployment { + rootMetadata := createMetadata(fsys, filename, 0, 0, "", nil).WithInternal(resolver.NewResolver()) + fileMetadata := createMetadata(fsys, filename, 1, 14, "", &rootMetadata) + resourceMetadata := createMetadata(fsys, filename, 6, 12, "resources.myacc", &fileMetadata) + return azure.Deployment{ + Metadata: fileMetadata, + TargetScope: azure.ScopeResourceGroup, + Resources: []azure.Resource{ + { + Metadata: resourceMetadata, + APIVersion: azure.NewValue( + "2025-06-01", + createMetadata(fsys, filename, 8, 8, "resources.myacc.apiVersion", &resourceMetadata), + ), + Type: azure.NewValue( + "Microsoft.Storage/storageAccounts", + createMetadata(fsys, filename, 7, 7, "resources.myacc.type", &resourceMetadata), + ), + Kind: azure.NewValue( + "Storage", + createMetadata(fsys, filename, 11, 11, "resources.myacc.kind", &resourceMetadata), + ), + Name: azure.NewValue( + "my-acc-test", + createMetadata(fsys, filename, 9, 9, "resources.myacc.name", &resourceMetadata), + ), + Location: azure.NewValue( + "location", + createMetadata(fsys, filename, 10, 10, "resources.myacc.location", &resourceMetadata), + ), + }, + }, + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := fstest.MapFS{ + filename: &fstest.MapFile{Data: []byte(tt.input)}, + } + p := New(fsys) + got, err := p.ParseFS(t.Context(), ".") + require.NoError(t, err) + + if tt.want == nil { + assert.Empty(t, got) + return + } + + require.Len(t, got, 1) + want := tt.want(fsys) + assert.Equal(t, want, got[0]) + }) + } +} + +func Test_NestedResourceParsing(t *testing.T) { + input := ` +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "environment": { + "type": "string", + "allowedValues": [ + "dev", + "test", + "prod" + ] + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Location for all resources." + } + }, + "storageAccountSkuName": { + "type": "string", + "defaultValue": "Standard_LRS" + }, + "storageAccountSkuTier": { + "type": "string", + "defaultValue": "Standard" + } + }, + "variables": { + "uniquePart": "[take(uniqueString(resourceGroup().id), 4)]", + "storageAccountName": "[concat('mystorageaccount', variables('uniquePart'), parameters('environment'))]", + "queueName": "myqueue" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "apiVersion": "2019-06-01", + "sku": { + "name": "[parameters('storageAccountSkuName')]", + "tier": "[parameters('storageAccountSkuTier')]" + }, + "kind": "StorageV2", + "properties": {}, + "resources": [ + { + "name": "[concat('default/', variables('queueName'))]", + "type": "queueServices/queues", + "apiVersion": "2019-06-01", + "dependsOn": [ + "[variables('storageAccountName')]" + ], + "properties": { + "metadata": {} + } + } + ] + } + ] +} +` + + fsys := fstest.MapFS{ + "nested.json": &fstest.MapFile{Data: []byte(input)}, + } + + p := New(fsys) + got, err := p.ParseFS(t.Context(), ".") + require.NoError(t, err) + require.Len(t, got, 1) + + deployment := got[0] + + require.Len(t, deployment.Resources, 1) + + storageAccountResource := deployment.Resources[0] + + require.Len(t, storageAccountResource.Resources, 1) + + queue := storageAccountResource.Resources[0] + + assert.Equal(t, "queueServices/queues", queue.Type.AsString()) +} diff --git a/pkg/iac/scanners/azure/arm/parser/template.go b/pkg/iac/scanners/azure/arm/parser/template.go new file mode 100644 index 000000000000..5c00f2efb350 --- /dev/null +++ b/pkg/iac/scanners/azure/arm/parser/template.go @@ -0,0 +1,204 @@ +package parser + +import ( + "bytes" + "encoding/json/jsontext" + "encoding/json/v2" + "errors" + "fmt" + "io/fs" + "iter" + "strconv" + "strings" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure/resolver" + "github.com/aquasecurity/trivy/pkg/iac/types" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type MetadataReceiver interface { + SetMetadata(*types.Metadata) +} + +type Template struct { + Metadata *types.Metadata `json:"-"` + Schema azure.Value `json:"$schema"` + ContentVersion azure.Value `json:"contentVersion"` + APIProfile azure.Value `json:"apiProfile"` + Parameters map[string]Parameter `json:"parameters"` + Variables map[string]azure.Value `json:"variables"` + Functions []Function `json:"functions"` + Resources Resources `json:"resources"` + Outputs map[string]azure.Value `json:"outputs"` +} + +// Resources is a collection of Resource items that can be represented in ARM +// templates either as an array or as an object (e.g., language v2). This custom +// unmarshaler normalizes both forms into a flat slice. +type Resources []Resource + +func (r *Resources) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + switch dec.PeekKind() { + case '[': + var arr []Resource + if err := json.UnmarshalDecode(dec, &arr); err != nil { + return err + } + *r = arr + case '{': + var m map[string]Resource + if err := json.UnmarshalDecode(dec, &m); err != nil { + return err + } + res := make([]Resource, 0, len(m)) + for _, v := range m { + res = append(res, v) + } + *r = res + default: + return errors.New("unexpected JSON token for resources") + } + return nil +} + +type Parameter struct { + Metadata *types.Metadata + Type azure.Value `json:"type"` + DefaultValue azure.Value `json:"defaultValue"` + MaxLength azure.Value `json:"maxLength"` + MinLength azure.Value `json:"minLength"` +} + +type Function struct{} + +type Resource struct { + Metadata *types.Metadata `json:"-"` + innerResource +} + +func (t *Template) SetMetadata(m *types.Metadata) { + t.Metadata = m +} + +func (r *Resource) SetMetadata(m *types.Metadata) { + r.Metadata = m +} + +func (p *Parameter) SetMetadata(m *types.Metadata) { + p.Metadata = m +} + +type innerResource struct { + APIVersion azure.Value `json:"apiVersion"` + Type azure.Value `json:"type"` + Kind azure.Value `json:"kind"` + Name azure.Value `json:"name"` + Location azure.Value `json:"location"` + Tags azure.Value `json:"tags"` + Sku azure.Value `json:"sku"` + Properties azure.Value `json:"properties"` + Resources []Resource `json:"resources"` +} + +func ParseTemplate(fsys fs.FS, path string) (*Template, error) { + data, err := fs.ReadFile(fsys, path) + if err != nil { + return nil, fmt.Errorf("read file: %w", err) + } + + lr := xjson.NewLineReader(bytes.NewReader(xjson.ToRFC8259(data))) + + mc := metadataCollector{ + path: path, + fsys: fsys, + entries: make(map[jsontext.Pointer]*types.Metadata), + } + + var template Template + if err := json.UnmarshalRead(lr, &template, json.WithUnmarshalers( + xjson.UnmarshalerWithLocation[MetadataReceiver](lr, func() xjson.DecodeHook { + return xjson.DecodeHook{ + After: mc.After, + } + }()), + )); err != nil { + return nil, fmt.Errorf("unmarshal template: %w", err) + } + mc.linkParentMetadata() + + rootMetadata := types.NewMetadata( + types.NewRange(path, 0, 0, "", fsys), + "", + ).WithInternal(resolver.NewResolver()) + template.Metadata.SetParentPtr(&rootMetadata) + return &template, nil +} + +type metadataCollector struct { + path string + fsys fs.FS + entries map[jsontext.Pointer]*types.Metadata +} + +func (c *metadataCollector) After(dec *jsontext.Decoder, obj any, loc ftypes.Location) { + ptr := dec.StackPointer() + ref := buildNodeRef(ptr.Tokens()) + rng := types.NewRange(c.path, loc.StartLine, loc.EndLine, "", c.fsys) + + md := types.NewMetadata(rng, ref) + c.entries[ptr] = &md + + if r, ok := obj.(MetadataReceiver); ok { + r.SetMetadata(&md) + } +} + +func (c *metadataCollector) linkParentMetadata() { + for path, md := range c.entries { + if md == nil || !isValidMetadata(md) { + continue + } + parentPath, ok := c.findClosestValidParent(path) + if !ok || parentPath == path { + continue + } + md.SetParentPtr(c.entries[parentPath]) + } +} + +func (c *metadataCollector) findClosestValidParent(path jsontext.Pointer) (jsontext.Pointer, bool) { + for { + path = path.Parent() + if md, ok := c.entries[path]; ok && md != nil && isValidMetadata(md) { + return path, true + } + if path == "" { + return "", false + } + } +} + +func buildNodeRef(seq iter.Seq[string]) string { + var sb strings.Builder + for el := range seq { + if _, err := strconv.Atoi(el); err == nil { + sb.WriteString("[") + sb.WriteString(el) + sb.WriteString("]") + } else { + if sb.Len() > 0 { + sb.WriteString(".") + } + sb.WriteString(el) + } + + } + return sb.String() +} + +func isValidMetadata(m *types.Metadata) bool { + rng := m.Range() + return rng.GetStartLine() != 0 && rng.GetEndLine() != 0 +} diff --git a/pkg/iac/scanners/azure/arm/parser/template_test.go b/pkg/iac/scanners/azure/arm/parser/template_test.go new file mode 100644 index 000000000000..319e81410afa --- /dev/null +++ b/pkg/iac/scanners/azure/arm/parser/template_test.go @@ -0,0 +1,51 @@ +package parser + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" +) + +func Test_JSONUnmarshal(t *testing.T) { + target, err := ParseTemplate(os.DirFS("testdata"), "example.json") + require.NoError(t, err) + + assert.Equal(t, + "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + target.Schema.AsString(), + ) + + assert.Equal(t, "1.0.0.0", target.ContentVersion.Raw()) + + require.Contains(t, target.Parameters, "storagePrefix") + prefix := target.Parameters["storagePrefix"] + /* + "type": "string", + "defaultValue": "x", + "maxLength": 11, + "minLength": 3 + */ + assert.Equal(t, "string", prefix.Type.Raw()) + assert.Equal(t, azure.KindString, prefix.Type.Kind) + assert.Equal(t, 8, prefix.Type.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 8, prefix.Type.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, "x", prefix.DefaultValue.Raw()) + assert.Equal(t, azure.KindString, prefix.DefaultValue.Kind) + assert.Equal(t, 9, prefix.DefaultValue.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 9, prefix.DefaultValue.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, int64(11), prefix.MaxLength.Raw()) + assert.Equal(t, azure.KindNumber, prefix.MaxLength.Kind) + assert.Equal(t, 10, prefix.MaxLength.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 10, prefix.MaxLength.GetMetadata().Range().GetEndLine()) + + assert.Equal(t, int64(3), prefix.MinLength.Raw()) + assert.Equal(t, azure.KindNumber, prefix.MinLength.Kind) + assert.Equal(t, 11, prefix.MinLength.GetMetadata().Range().GetStartLine()) + assert.Equal(t, 11, prefix.MinLength.GetMetadata().Range().GetEndLine()) +} diff --git a/pkg/iac/scanners/azure/arm/parser/testdata/example.json b/pkg/iac/scanners/azure/arm/parser/testdata/example.json new file mode 100644 index 000000000000..9698ed1a0583 --- /dev/null +++ b/pkg/iac/scanners/azure/arm/parser/testdata/example.json @@ -0,0 +1,15 @@ +{ + // wow this is a comment + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", // another one + // this version is great + "contentVersion": "1.0.0.0", + "parameters": { + "storagePrefix": { + "type": "string", + "defaultValue": "x", + "maxLength": 11, + "minLength": 3 + } + }, + "resources": [] +} \ No newline at end of file diff --git a/pkg/iac/scanners/azure/arm/scanner.go b/pkg/iac/scanners/azure/arm/scanner.go new file mode 100644 index 000000000000..7869569b1b46 --- /dev/null +++ b/pkg/iac/scanners/azure/arm/scanner.go @@ -0,0 +1,88 @@ +package arm + +import ( + "context" + "fmt" + "io/fs" + + "github.com/aquasecurity/trivy/pkg/iac/adapters/arm" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure/arm/parser" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +var _ scanners.FSScanner = (*Scanner)(nil) +var _ options.ConfigurableScanner = (*Scanner)(nil) + +type Scanner struct { + *rego.RegoScannerProvider + opts []options.ScannerOption + logger *log.Logger +} + +func New(opts ...options.ScannerOption) *Scanner { + scanner := &Scanner{ + RegoScannerProvider: rego.NewRegoScannerProvider(opts...), + opts: opts, + logger: log.WithPrefix("azure-arm"), + } + for _, opt := range opts { + opt(scanner) + } + return scanner +} + +func (s *Scanner) Name() string { + return "Azure ARM" +} + +func (s *Scanner) ScanFS(ctx context.Context, fsys fs.FS, dir string) (scan.Results, error) { + p := parser.New(fsys) + deployments, err := p.ParseFS(ctx, dir) + if err != nil { + return nil, err + } + + return s.scanDeployments(ctx, deployments, fsys) +} + +func (s *Scanner) scanDeployments(ctx context.Context, deployments []azure.Deployment, f fs.FS) (scan.Results, error) { + + var results scan.Results + + for _, deployment := range deployments { + + result, err := s.scanDeployment(ctx, deployment, f) + if err != nil { + return nil, err + } + results = append(results, result...) + } + + return results, nil +} + +func (s *Scanner) scanDeployment(ctx context.Context, deployment azure.Deployment, fsys fs.FS) (scan.Results, error) { + state := arm.Adapt(ctx, deployment) + + rs, err := s.InitRegoScanner(fsys, s.opts) + if err != nil { + return nil, fmt.Errorf("init rego scanner: %w", err) + } + + results, err := rs.ScanInput(ctx, types.SourceCloud, rego.Input{ + Path: deployment.Metadata.Range().GetFilename(), + FS: fsys, + Contents: state.ToRego(), + }) + if err != nil { + return nil, fmt.Errorf("rego scan error: %w", err) + } + + return results, nil +} diff --git a/pkg/iac/scanners/azure/deployment.go b/pkg/iac/scanners/azure/deployment.go new file mode 100644 index 000000000000..9932f1538db3 --- /dev/null +++ b/pkg/iac/scanners/azure/deployment.go @@ -0,0 +1,179 @@ +package azure + +import ( + "os" + + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Deployment struct { + Metadata types.Metadata + TargetScope Scope + Parameters []Parameter + Variables []Variable + Resources []Resource + Outputs []Output +} + +type Parameter struct { + Variable + Default Value + Decorators []Decorator +} + +type Variable struct { + Name string + Value Value +} + +type Output Variable + +type Resource struct { + Metadata types.Metadata + APIVersion Value + Type Value + Kind Value + Name Value + Location Value + Tags Value + Sku Value + Properties Value + Resources []Resource +} + +type PropertyBag struct { + Metadata types.Metadata + Data map[string]Value +} + +type Decorator struct { + Name string + Args []Value +} + +type Scope string + +const ( + ScopeResourceGroup Scope = "resourceGroup" +) + +func (d *Deployment) GetResourcesByType(t string) []Resource { + var resources []Resource + for _, r := range d.Resources { + if r.Type.AsString() == t { + resources = append(resources, r) + } + } + return resources +} + +func (r *Resource) GetResourcesByType(t string) []Resource { + var resources []Resource + for _, res := range r.Resources { + if res.Type.AsString() == t { + resources = append(resources, res) + } + } + return resources +} + +func (d *Deployment) GetParameter(parameterName string) any { + + for _, parameter := range d.Parameters { + if parameter.Name == parameterName { + return parameter.Value.Raw() + } + } + return nil +} + +func (d *Deployment) GetVariable(variableName string) any { + + for _, variable := range d.Variables { + if variable.Name == variableName { + return variable.Value.Raw() + } + } + return nil +} + +func (d *Deployment) GetEnvVariable(envVariableName string) any { + + if envVariable, exists := os.LookupEnv(envVariableName); exists { + return envVariable + } + return nil +} + +func (d *Deployment) GetOutput(outputName string) any { + + for _, output := range d.Outputs { + if output.Name == outputName { + return output.Value.Raw() + } + } + return nil +} + +func (d *Deployment) GetDeployment() any { + + type template struct { + Schema string `json:"$schema"` + ContentVersion string `json:"contentVersion"` + Parameters map[string]any `json:"parameters"` + Variables map[string]any `json:"variables"` + Resources []any `json:"resources"` + Outputs map[string]any `json:"outputs"` + } + + type templateLink struct { + URI string `json:"uri"` + } + + type properties struct { + TemplateLink templateLink `json:"templateLink"` + Template template `json:"template"` + TemplateHash string `json:"templateHash"` + Parameters map[string]any `json:"parameters"` + Mode string `json:"mode"` + ProvisioningState string `json:"provisioningState"` + } + + deploymentShell := struct { + Name string `json:"name"` + Properties properties `json:"properties"` + }{ + Name: "Placeholder Deployment", + Properties: properties{ + TemplateLink: templateLink{ + URI: "https://placeholder.com", + }, + Template: template{ + Schema: "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + ContentVersion: "", + Parameters: make(map[string]any), + Variables: make(map[string]any), + Resources: make([]any, 0), + Outputs: make(map[string]any), + }, + }, + } + + for _, parameter := range d.Parameters { + deploymentShell.Properties.Template.Parameters[parameter.Name] = parameter.Value.Raw() + } + + for _, variable := range d.Variables { + deploymentShell.Properties.Template.Variables[variable.Name] = variable.Value.Raw() + } + + for _, resource := range d.Resources { + deploymentShell.Properties.Template.Resources = append(deploymentShell.Properties.Template.Resources, resource) + } + + for _, output := range d.Outputs { + deploymentShell.Properties.Template.Outputs[output.Name] = output.Value.Raw() + } + + return deploymentShell +} diff --git a/pkg/iac/scanners/azure/expressions/lex.go b/pkg/iac/scanners/azure/expressions/lex.go new file mode 100644 index 000000000000..6bbb4717e745 --- /dev/null +++ b/pkg/iac/scanners/azure/expressions/lex.go @@ -0,0 +1,204 @@ +package expressions + +import ( + "bufio" + "errors" + "fmt" + "strconv" + "strings" +) + +type TokenType uint16 + +const ( + TokenName TokenType = iota + TokenOpenParen + TokenCloseParen + TokenComma + TokenDot + TokenLiteralString + TokenLiteralInteger + TokenLiteralFloat + TokenNewLine +) + +type Token struct { + Type TokenType + Data any +} + +type lexer struct { + reader *bufio.Reader +} + +func lex(expression string) ([]Token, error) { + lexer := &lexer{ + reader: bufio.NewReader(strings.NewReader(expression)), + } + return lexer.Lex() +} + +func (l *lexer) unread() { + _ = l.reader.UnreadRune() +} + +func (l *lexer) read() (rune, error) { + r, _, err := l.reader.ReadRune() + return r, err +} + +func (l *lexer) Lex() ([]Token, error) { + var tokens []Token + + for { + r, err := l.read() + if err != nil { + break + } + + switch r { + case ' ', '\t', '\r': + continue + case '\n': + tokens = append(tokens, Token{Type: TokenNewLine}) + case '(': + tokens = append(tokens, Token{Type: TokenOpenParen}) + case ')': + tokens = append(tokens, Token{Type: TokenCloseParen}) + case ',': + tokens = append(tokens, Token{Type: TokenComma}) + case '.': + tokens = append(tokens, Token{Type: TokenDot}) + case '"', '\'': + token, err := l.lexString(r) + if err != nil { + return nil, fmt.Errorf("string parse error: %w", err) + } + tokens = append(tokens, token) + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + l.unread() + token, err := l.lexNumber() + if err != nil { + return nil, fmt.Errorf("number parse error: %w", err) + } + tokens = append(tokens, token) + default: + l.unread() + tokens = append(tokens, l.lexKeyword()) + } + } + + return tokens, nil +} + +func (l *lexer) lexString(terminator rune) (Token, error) { + var sb strings.Builder + for { + r, err := l.read() + if err != nil { + break + } + if r == '\\' { + r, err := l.readEscapedChar() + if err != nil { + return Token{}, fmt.Errorf("bad escape: %w", err) + } + sb.WriteRune(r) + continue + } + if r == terminator { + break + } + sb.WriteRune(r) + } + return Token{ + Type: TokenLiteralString, + Data: sb.String(), + }, nil +} + +func (l *lexer) readEscapedChar() (rune, error) { + r, err := l.read() + if err != nil { + return 0, errors.New("unexpected EOF") + } + switch r { + case 'n': + return '\n', nil + case 'r': + return '\r', nil + case 't': + return '\t', nil + case '"', '\'': + return r, nil + default: + return 0, fmt.Errorf("'%c' is not a supported escape sequence", r) + } +} + +func (l *lexer) lexNumber() (Token, error) { + + var sb strings.Builder + var decimal bool + +LOOP: + for { + r, err := l.read() + if err != nil { + break + } + switch r { + case '.': + decimal = true + sb.WriteRune('.') + case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': + sb.WriteRune(r) + default: + l.unread() + break LOOP + } + } + + raw := sb.String() + if decimal { + fl, err := strconv.ParseFloat(raw, 64) + if err != nil { + return Token{}, err + } + return Token{ + Type: TokenLiteralFloat, + Data: fl, + }, nil + } + + i, err := strconv.ParseInt(raw, 10, 64) + if err != nil { + return Token{}, err + } + return Token{ + Type: TokenLiteralInteger, + Data: i, + }, nil +} + +func (l *lexer) lexKeyword() Token { + var sb strings.Builder +LOOP: + for { + r, err := l.read() + if err != nil { + break + } + switch { + case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '_': + sb.WriteRune(r) + default: + l.unread() + break LOOP + } + } + return Token{ + Type: TokenName, + Data: sb.String(), + } +} diff --git a/pkg/iac/scanners/azure/expressions/node.go b/pkg/iac/scanners/azure/expressions/node.go new file mode 100644 index 000000000000..8c274b500a6c --- /dev/null +++ b/pkg/iac/scanners/azure/expressions/node.go @@ -0,0 +1,75 @@ +package expressions + +import ( + functions2 "github.com/aquasecurity/trivy/pkg/iac/scanners/azure/functions" +) + +type Node interface { + Evaluate(deploymentProvider functions2.DeploymentData) any +} + +type expressionValue struct { + val any +} + +func (e expressionValue) Evaluate(deploymentProvider functions2.DeploymentData) any { + if f, ok := e.val.(expression); ok { + return f.Evaluate(deploymentProvider) + } + return e.val +} + +type expression struct { + name string + args []Node +} + +func (f expression) Evaluate(deploymentProvider functions2.DeploymentData) any { + args := make([]any, len(f.args)) + for i, arg := range f.args { + args[i] = arg.Evaluate(deploymentProvider) + } + + return functions2.Evaluate(deploymentProvider, f.name, args...) +} + +func NewExpressionTree(code string) (Node, error) { + tokens, err := lex(code) + if err != nil { + return nil, err + } + + // create a walker for the nodes + tw := newTokenWalker(tokens) + + // generate the root function + return newFunctionNode(tw), nil +} + +func newFunctionNode(tw *tokenWalker) Node { + funcNode := &expression{ + name: tw.pop().Data.(string), + } + + for tw.hasNext() { + token := tw.pop() + if token == nil { + break + } + + switch token.Type { + case TokenCloseParen: + return funcNode + case TokenName: + if tw.peek().Type == TokenOpenParen { + // this is a function, unwind 1 + tw.unPop() + funcNode.args = append(funcNode.args, newFunctionNode(tw)) + } + case TokenLiteralString, TokenLiteralInteger, TokenLiteralFloat: + funcNode.args = append(funcNode.args, expressionValue{token.Data}) + } + + } + return funcNode +} diff --git a/pkg/iac/scanners/azure/expressions/token_walker.go b/pkg/iac/scanners/azure/expressions/token_walker.go new file mode 100644 index 000000000000..d07a238d1bd9 --- /dev/null +++ b/pkg/iac/scanners/azure/expressions/token_walker.go @@ -0,0 +1,40 @@ +package expressions + +type tokenWalker struct { + tokens []Token + currentPosition int +} + +func newTokenWalker(tokens []Token) *tokenWalker { + return &tokenWalker{ + tokens: tokens, + currentPosition: 0, + } +} + +func (t *tokenWalker) peek() Token { + if t.currentPosition >= len(t.tokens) { + return Token{} + } + return t.tokens[t.currentPosition] +} + +func (t *tokenWalker) hasNext() bool { + return t.currentPosition+1 < len(t.tokens) +} + +func (t *tokenWalker) unPop() { + if t.currentPosition > 0 { + t.currentPosition-- + } +} + +func (t *tokenWalker) pop() *Token { + if !t.hasNext() { + return nil + } + + token := t.tokens[t.currentPosition] + t.currentPosition++ + return &token +} diff --git a/pkg/iac/scanners/azure/functions/add.go b/pkg/iac/scanners/azure/functions/add.go new file mode 100644 index 000000000000..21b1281574ad --- /dev/null +++ b/pkg/iac/scanners/azure/functions/add.go @@ -0,0 +1,15 @@ +package functions + +func Add(args ...any) any { + + if len(args) != 2 { + return nil + } + + if a, ok := args[0].(int); ok { + if b, ok := args[1].(int); ok { + return a + b + } + } + return nil +} diff --git a/pkg/iac/scanners/azure/functions/add_test.go b/pkg/iac/scanners/azure/functions/add_test.go new file mode 100644 index 000000000000..51ae2bab044a --- /dev/null +++ b/pkg/iac/scanners/azure/functions/add_test.go @@ -0,0 +1,38 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Add(t *testing.T) { + tests := []struct { + name string + args []any + expected int + }{ + { + name: "Add with 1 and 2", + args: []any{1, 2}, + expected: 3, + }, + { + name: "Add with 2 and 3", + args: []any{2, 3}, + expected: 5, + }, + { + name: "Add with 3 and -4", + args: []any{3, -4}, + expected: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Add(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/and.go b/pkg/iac/scanners/azure/functions/and.go new file mode 100644 index 000000000000..4b8b10b7cb31 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/and.go @@ -0,0 +1,27 @@ +package functions + +func And(args ...any) any { + + if len(args) <= 1 { + return false + } + + arg0, ok := args[0].(bool) + if !ok { + return false + } + + benchmark := arg0 + + for _, arg := range args[1:] { + arg1, ok := arg.(bool) + if !ok { + return false + } + if benchmark != arg1 { + return false + } + + } + return true +} diff --git a/pkg/iac/scanners/azure/functions/and_test.go b/pkg/iac/scanners/azure/functions/and_test.go new file mode 100644 index 000000000000..8124ee262b1f --- /dev/null +++ b/pkg/iac/scanners/azure/functions/and_test.go @@ -0,0 +1,39 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_And(t *testing.T) { + + tests := []struct { + name string + args []any + expected bool + }{ + { + name: "And with same 2 bools", + args: []any{true, true}, + expected: true, + }, + { + name: "And with same 3 bools", + args: []any{true, true, true}, + expected: true, + }, + { + name: "And with different 4 bools", + args: []any{true, true, false, true}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := And(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/array.go b/pkg/iac/scanners/azure/functions/array.go new file mode 100644 index 000000000000..813a133b30a0 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/array.go @@ -0,0 +1,29 @@ +package functions + +func Array(args ...any) any { + + if len(args) != 1 { + return "" + } + + switch ctype := args[0].(type) { + case int: + return []int{ctype} + case string: + return []string{ctype} + case map[string]any: + var result []any + for k, v := range ctype { + result = append(result, k, v) + } + return result + case any: + switch ctype := ctype.(type) { + case []string: + return ctype + case []any: + return ctype + } + } + return []any{} +} diff --git a/pkg/iac/scanners/azure/functions/array_test.go b/pkg/iac/scanners/azure/functions/array_test.go new file mode 100644 index 000000000000..f4396fc4093a --- /dev/null +++ b/pkg/iac/scanners/azure/functions/array_test.go @@ -0,0 +1,44 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Array(t *testing.T) { + test := []struct { + name string + input []any + expected any + }{ + { + name: "array from an int", + input: []any{1}, + expected: []int{1}, + }, + { + name: "array from a string", + input: []any{"hello"}, + expected: []string{"hello"}, + }, + { + name: "array from a map", + input: []any{map[string]any{"hello": "world"}}, + expected: []any{"hello", "world"}, + }, + { + name: "array from an slice", + input: []any{ + []string{"hello", "world"}, + }, + expected: []string{"hello", "world"}, + }, + } + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + actual := Array(tt.input...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/base64.go b/pkg/iac/scanners/azure/functions/base64.go new file mode 100644 index 000000000000..46b784bb0f96 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/base64.go @@ -0,0 +1,52 @@ +package functions + +import ( + "encoding/base64" + "encoding/json" +) + +func Base64(args ...any) any { + + if len(args) == 0 { + return nil + } + + input := args[0].(string) + + return base64.StdEncoding.EncodeToString([]byte(input)) +} + +func Base64ToString(args ...any) any { + if len(args) == 0 { + return nil + } + + input := args[0].(string) + + result, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return "" + } + return string(result) +} + +func Base64ToJson(args ...any) any { + + if len(args) == 0 { + return nil + } + + input := args[0].(string) + + decoded, err := base64.StdEncoding.DecodeString(input) + if err != nil { + return nil + } + + var result map[string]any + + if err := json.Unmarshal(decoded, &result); err != nil { + return nil + } + return result +} diff --git a/pkg/iac/scanners/azure/functions/base64_test.go b/pkg/iac/scanners/azure/functions/base64_test.go new file mode 100644 index 000000000000..648617ee40eb --- /dev/null +++ b/pkg/iac/scanners/azure/functions/base64_test.go @@ -0,0 +1,85 @@ +package functions + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Base64Call(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "simple base64 call", + args: []any{ + "hello, world", + }, + expected: "aGVsbG8sIHdvcmxk", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Base64(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} + +func Test_Base64ToStringCall(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "simple base64ToString call", + args: []any{ + "aGVsbG8sIHdvcmxk", + }, + expected: "hello, world", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Base64ToString(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} + +func Test_Base64ToJsonCall(t *testing.T) { + + tests := []struct { + name string + args []any + expected string + }{ + { + name: "simple base64ToJson call", + args: []any{ + "eyJoZWxsbyI6ICJ3b3JsZCJ9", + }, + expected: `{"hello":"world"}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Base64ToJson(tt.args...) + + actualContent, err := json.Marshal(actual) + require.NoError(t, err) + + assert.Equal(t, tt.expected, string(actualContent)) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/bool.go b/pkg/iac/scanners/azure/functions/bool.go new file mode 100644 index 000000000000..cb3031dba4cb --- /dev/null +++ b/pkg/iac/scanners/azure/functions/bool.go @@ -0,0 +1,20 @@ +package functions + +import "strings" + +func Bool(args ...any) any { + if len(args) != 1 { + return false + } + + switch input := args[0].(type) { + case bool: + return input + case string: + input = strings.ToLower(input) + return input == "true" || input == "1" || input == "yes" || input == "on" + case int: + return input == 1 + } + return false +} diff --git a/pkg/iac/scanners/azure/functions/bool_test.go b/pkg/iac/scanners/azure/functions/bool_test.go new file mode 100644 index 000000000000..054dd52776ad --- /dev/null +++ b/pkg/iac/scanners/azure/functions/bool_test.go @@ -0,0 +1,63 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Bool(t *testing.T) { + tests := []struct { + name string + args []any + expected bool + }{ + { + name: "Bool with true", + args: []any{true}, + expected: true, + }, + { + name: "Bool with false", + args: []any{false}, + expected: false, + }, + { + name: "Bool with 1", + args: []any{1}, + expected: true, + }, + { + name: "Bool with 0", + args: []any{0}, + expected: false, + }, + { + name: "Bool with true string", + args: []any{"true"}, + expected: true, + }, + { + name: "Bool with false string", + args: []any{"false"}, + expected: false, + }, + { + name: "Bool with 1 string", + args: []any{"1"}, + expected: true, + }, + { + name: "Bool with 0 string", + args: []any{"0"}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Bool(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/casing.go b/pkg/iac/scanners/azure/functions/casing.go new file mode 100644 index 000000000000..2df433a5ade2 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/casing.go @@ -0,0 +1,29 @@ +package functions + +import "strings" + +func ToLower(args ...any) any { + if len(args) != 1 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return "" + } + + return strings.ToLower(input) +} + +func ToUpper(args ...any) any { + if len(args) != 1 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return "" + } + + return strings.ToUpper(input) +} diff --git a/pkg/iac/scanners/azure/functions/casing_test.go b/pkg/iac/scanners/azure/functions/casing_test.go new file mode 100644 index 000000000000..d94fca7ab050 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/casing_test.go @@ -0,0 +1,71 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_ToLower(t *testing.T) { + + tests := []struct { + name string + args []any + expected string + }{ + { + name: "lowercase a string", + args: []any{ + "HELLO", + }, + expected: "hello", + }, + { + name: "lowercase a string with a non-string input", + args: []any{ + 10, + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := ToLower(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} + +func Test_ToUpper(t *testing.T) { + + tests := []struct { + name string + args []any + expected string + }{ + { + name: "uppercase a string", + args: []any{ + "hello", + }, + expected: "HELLO", + }, + { + name: "uppercase a string with a non-string input", + args: []any{ + 10, + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := ToUpper(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/coalesce.go b/pkg/iac/scanners/azure/functions/coalesce.go new file mode 100644 index 000000000000..cec7220a83ba --- /dev/null +++ b/pkg/iac/scanners/azure/functions/coalesce.go @@ -0,0 +1,10 @@ +package functions + +func Coalesce(args ...any) any { + for _, arg := range args { + if arg != nil { + return arg + } + } + return nil +} diff --git a/pkg/iac/scanners/azure/functions/coalesce_test.go b/pkg/iac/scanners/azure/functions/coalesce_test.go new file mode 100644 index 000000000000..b0d0459bde54 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/coalesce_test.go @@ -0,0 +1,56 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Coalesce(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + { + name: "coalesce with nil", + args: []any{ + nil, + }, + expected: nil, + }, + { + name: "coalesce with nil and string", + args: []any{ + nil, + "test", + }, + expected: "test", + }, + { + name: "coalesce with nil and string and int", + args: []any{ + nil, + "test", + 1, + }, + expected: "test", + }, + { + name: "coalesce with nil and nil and array", + args: []any{ + nil, + nil, + []any{"a", "b", "c"}, + }, + expected: []any{"a", "b", "c"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Coalesce(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/concat.go b/pkg/iac/scanners/azure/functions/concat.go new file mode 100644 index 000000000000..997f9ed231c4 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/concat.go @@ -0,0 +1,29 @@ +package functions + +import ( + "fmt" + "strings" +) + +func Concat(args ...any) any { + + switch args[0].(type) { + case string: + var sb strings.Builder + for _, arg := range args { + sb.WriteString(fmt.Sprintf("%v", arg)) + } + return sb.String() + case any: + var result []any + for _, arg := range args { + argArr, ok := arg.([]any) + if !ok { + continue + } + result = append(result, argArr...) + } + return result + } + return "" +} diff --git a/pkg/iac/scanners/azure/functions/concat_test.go b/pkg/iac/scanners/azure/functions/concat_test.go new file mode 100644 index 000000000000..0418058c483c --- /dev/null +++ b/pkg/iac/scanners/azure/functions/concat_test.go @@ -0,0 +1,94 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_StringConcatenation(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "simple string concatenation", + args: []any{ + "hello", + ", ", + "world", + "!", + }, + expected: "hello, world!", + }, + { + name: "string concatenation with non strings", + args: []any{ + "pi to 3 decimal places is ", + 3.142, + }, + expected: "pi to 3 decimal places is 3.142", + }, + { + name: "string concatenation with multiple primitives", + args: []any{ + "to say that ", + 3, + " is greater than ", + 5, + " would be ", + false, + }, + expected: "to say that 3 is greater than 5 would be false", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + concatenated := Concat(tt.args...) + require.Equal(t, tt.expected, concatenated) + }) + } +} + +func Test_ArrayConcatenation(t *testing.T) { + tests := []struct { + name string + args []any + expected []any + }{ + { + name: "simple array concatenation", + args: []any{ + []any{1, 2, 3}, + []any{4, 5, 6}, + }, + expected: []any{1, 2, 3, 4, 5, 6}, + }, + { + name: "array concatenation with non arrays", + args: []any{ + []any{1, 2, 3}, + 4, + }, + expected: []any{1, 2, 3}, + }, + { + name: "array concatenation with multiple primitives", + args: []any{ + []any{1, 2, 3}, + 4, + []any{5, 6, 7}, + }, + expected: []any{1, 2, 3, 5, 6, 7}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + concatenated := Concat(tt.args...) + require.Equal(t, tt.expected, concatenated) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/contains.go b/pkg/iac/scanners/azure/functions/contains.go new file mode 100644 index 000000000000..859a89a22ab3 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/contains.go @@ -0,0 +1,37 @@ +package functions + +import ( + "fmt" + "slices" + "strings" +) + +func Contains(args ...any) any { + + if len(args) != 2 { + return false + } + + container := args[0] + itemToFind := args[1] + + switch cType := container.(type) { + case string: + switch iType := itemToFind.(type) { + case string: + return strings.Contains(strings.ToLower(cType), strings.ToLower(iType)) + case int, int32, int64, uint, uint32, uint64: + return strings.Contains(strings.ToLower(cType), fmt.Sprintf("%d", iType)) + } + case []any: + return slices.Contains(cType, itemToFind) + case map[string]any: + for key := range cType { + if key == itemToFind { + return true + } + } + } + + return false +} diff --git a/pkg/iac/scanners/azure/functions/contains_test.go b/pkg/iac/scanners/azure/functions/contains_test.go new file mode 100644 index 000000000000..37fd793cdeff --- /dev/null +++ b/pkg/iac/scanners/azure/functions/contains_test.go @@ -0,0 +1,95 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Contains(t *testing.T) { + tests := []struct { + name string + args []any + expected bool + }{ + { + name: "simple true string contains", + args: []any{ + "hello, world", + "hell", + }, + expected: true, + }, + { + name: "simple false string contains", + args: []any{ + "hello, world", + "help", + }, + expected: false, + }, + { + name: "simple true string contains with case sensitivity", + args: []any{ + "hello, world", + "HELL", + }, + expected: true, + }, + { + name: "simple true string contains with number", + args: []any{ + "You're my number 1", + 1, + }, + expected: true, + }, + { + name: "true object contains key", + args: []any{ + map[string]any{ + "hello": "world", + }, + "hello", + }, + expected: true, + }, + { + name: "false object contains key", + args: []any{ + map[string]any{ + "hello": "world", + }, + "world", + }, + expected: false, + }, + { + name: "true array contains value", + args: []any{ + []any{ + "hello", "world", + }, + "hello", + }, + expected: true, + }, + { + name: "false array contains value", + args: []any{ + []any{ + "hello", "world", + }, + "help", + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doesContain := Contains(tt.args...) + require.Equal(t, tt.expected, doesContain) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/copy_index.go b/pkg/iac/scanners/azure/functions/copy_index.go new file mode 100644 index 000000000000..777d9a6237f6 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/copy_index.go @@ -0,0 +1,25 @@ +package functions + +var loopCounter = make(map[string]int) + +func CopyIndex(args ...any) any { + loopName := "default" + offset := 1 + if len(args) > 0 { + if providedLoopName, ok := args[0].(string); ok { + loopName = providedLoopName + } + } + if len(args) > 1 { + if providedOffset, ok := args[1].(int); ok { + offset = providedOffset + } + } + + if _, ok := loopCounter[loopName]; !ok { + loopCounter[loopName] = 0 + } + + loopCounter[loopName] += offset + return loopCounter[loopName] +} diff --git a/pkg/iac/scanners/azure/functions/copy_index_test.go b/pkg/iac/scanners/azure/functions/copy_index_test.go new file mode 100644 index 000000000000..c161d2596223 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/copy_index_test.go @@ -0,0 +1,52 @@ +package functions + +import "testing" + +func Test_CopyIndex(t *testing.T) { + tests := []struct { + name string + args []any + expected int + }{ + { + name: "CopyIndex with 1", + args: []any{}, + expected: 1, + }, + { + name: "CopyIndex with 2", + args: []any{}, + expected: 2, + }, + { + name: "CopyIndex with 3", + args: []any{}, + expected: 3, + }, + { + name: "CopyIndex with loopName", + args: []any{"loop1"}, + expected: 1, + }, + { + name: "CopyIndex with same lo" + + "opName", + args: []any{"loop1"}, + expected: 2, + }, + { + name: "CopyIndex with loopName", + args: []any{"loop2", 10}, + expected: 10, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CopyIndex(tt.args...) + if got != tt.expected { + t.Errorf("CopyIndex() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/create_array.go b/pkg/iac/scanners/azure/functions/create_array.go new file mode 100644 index 000000000000..ad92670e10be --- /dev/null +++ b/pkg/iac/scanners/azure/functions/create_array.go @@ -0,0 +1,11 @@ +package functions + +func CreateArray(args ...any) any { + var result []any + if len(args) == 0 { + return result + } + + result = append(result, args...) + return result +} diff --git a/pkg/iac/scanners/azure/functions/create_array_test.go b/pkg/iac/scanners/azure/functions/create_array_test.go new file mode 100644 index 000000000000..d2d6062b9bd5 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/create_array_test.go @@ -0,0 +1,68 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_CreateArray(t *testing.T) { + + tests := []struct { + name string + args []any + expected any + }{ + { + name: "create array with strings", + args: []any{ + "Hello", + "World", + }, + expected: []any{"Hello", "World"}, + }, + { + name: "create array with ints", + + args: []any{ + 1, 2, 3, + }, + expected: []any{1, 2, 3}, + }, + { + name: "create array with arrays", + args: []any{ + []any{1, 2, 3}, + []any{4, 5, 6}, + }, + expected: []any{[]any{1, 2, 3}, []any{4, 5, 6}}, + }, + { + name: "create arrau with maps", + args: []any{ + map[string]any{ + "one": "a", + }, + map[string]any{ + "two": "b", + }, + }, + expected: []any{ + map[string]any{ + "one": "a", + }, + map[string]any{ + "two": "b", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := CreateArray(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/create_object.go b/pkg/iac/scanners/azure/functions/create_object.go new file mode 100644 index 000000000000..48a0ebafb673 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/create_object.go @@ -0,0 +1,21 @@ +package functions + +func CreateObject(args ...any) any { + obj := make(map[string]any) + if len(args) == 0 { + return obj + } + + // if there aren't even pairs then return an empty object + if len(args)%2 != 0 { + return obj + } + + for i := 0; i < len(args); i += 2 { + key := args[i].(string) + value := args[i+1] + obj[key] = value + } + + return obj +} diff --git a/pkg/iac/scanners/azure/functions/create_object_test.go b/pkg/iac/scanners/azure/functions/create_object_test.go new file mode 100644 index 000000000000..10cc72d52ede --- /dev/null +++ b/pkg/iac/scanners/azure/functions/create_object_test.go @@ -0,0 +1,60 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_CreateObject(t *testing.T) { + + tests := []struct { + name string + args []any + expected any + }{ + { + name: "CreateObject with no args", + args: []any{}, + expected: make(map[string]any), + }, + { + name: "CreateObject with one arg", + args: []any{"foo", "bar"}, + expected: map[string]any{"foo": "bar"}, + }, + { + name: "CreateObject with two args", + args: []any{"foo", "bar", "baz", "qux"}, + expected: map[string]any{"foo": "bar", "baz": "qux"}, + }, + { + name: "CreateObject with three args", + args: []any{"foo", "bar", "baz", 1, "quux", true}, + expected: map[string]any{"foo": "bar", "baz": 1, "quux": true}, + }, + { + name: "CreateObject with odd number of args", + args: []any{"foo", "bar", "baz"}, + expected: make(map[string]any), + }, + { + name: "CreateObject with odd number of args", + args: []any{"foo", "bar", "baz", []string{"Hello", "World"}}, + expected: map[string]any{ + "foo": "bar", + "baz": []string{ + "Hello", "World", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := CreateObject(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/data_uri.go b/pkg/iac/scanners/azure/functions/data_uri.go new file mode 100644 index 000000000000..3b147b568f09 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/data_uri.go @@ -0,0 +1,36 @@ +package functions + +import ( + "fmt" + "strings" +) + +func DataUri(args ...any) any { + if len(args) == 0 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return "" + } + + return fmt.Sprintf("data:text/plain;charset=utf8;base64,%s", Base64(input)) +} + +func DataUriToString(args ...any) any { + if len(args) == 0 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return "" + } + parts := strings.Split(input, "base64,") + if len(parts) != 2 { + return "" + } + + return Base64ToString(parts[1]) +} diff --git a/pkg/iac/scanners/azure/functions/data_uri_test.go b/pkg/iac/scanners/azure/functions/data_uri_test.go new file mode 100644 index 000000000000..87f8a16de609 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/data_uri_test.go @@ -0,0 +1,53 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_data_uri_from_string(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "data uri from string", + args: []any{ + "Hello", + }, + expected: "data:text/plain;charset=utf8;base64,SGVsbG8=", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dataUri := DataUri(tt.args...) + require.Equal(t, tt.expected, dataUri) + }) + } +} + +func Test_string_from_data_uri(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "data uri to string", + args: []any{ + "data:;base64,SGVsbG8sIFdvcmxkIQ==", + }, + expected: "Hello, World!", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dataUri := DataUriToString(tt.args...) + require.Equal(t, tt.expected, dataUri) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/date_time_add.go b/pkg/iac/scanners/azure/functions/date_time_add.go new file mode 100644 index 000000000000..39f88ddead80 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/date_time_add.go @@ -0,0 +1,115 @@ +package functions + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "time" +) + +var pattern = regexp.MustCompile(`^P((?P\d+)Y)?((?P\d+)M)?((?P\d+)W)?((?P\d+)D)?(T((?P\d+)H)?((?P\d+)M)?((?P\d+)S)?)?$`) + +func DateTimeAdd(args ...any) any { + if len(args) < 2 { + return nil + } + + base, ok := args[0].(string) + if !ok { + return nil + } + + format := time.RFC3339 + if len(args) == 3 { + if providedFormat, ok := args[2].(string); ok { + format = convertFormat(providedFormat) + } + + } + + baseTime, err := time.Parse(format, base) + if err != nil { + return nil + } + + duration, err := parseISO8601(args[1].(string)) + if err != nil { + return nil + } + + timeDuration := duration.timeDuration() + baseTime = baseTime.Add(timeDuration) + + if ok { + return baseTime.Format(format) + } + + return baseTime.Format(time.RFC3339) +} + +type Iso8601Duration struct { + Y int + M int + W int + D int + // Time Component + TH int + TM int + TS int +} + +func parseISO8601(from string) (Iso8601Duration, error) { + var match []string + var d Iso8601Duration + + if !pattern.MatchString(from) { + return d, errors.New("could not parse duration string") + } + match = pattern.FindStringSubmatch(from) + + for i, name := range pattern.SubexpNames() { + part := match[i] + if i == 0 || name == "" || part == "" { + continue + } + + val, err := strconv.Atoi(part) + if err != nil { + return d, err + } + switch name { + case "year": + d.Y = val + case "month": + d.M = val + case "week": + d.W = val + case "day": + d.D = val + case "hour": + d.TH = val + case "minute": + d.TM = val + case "second": + d.TS = val + default: + return d, fmt.Errorf("unknown field %s", name) + } + } + + return d, nil +} + +func (d Iso8601Duration) timeDuration() time.Duration { + var dur time.Duration + dur += time.Duration(d.TH) * time.Hour + dur += time.Duration(d.TM) * time.Minute + dur += time.Duration(d.TS) * time.Second + dur += time.Duration(d.D) * 24 * time.Hour + dur += time.Duration(d.W) * 7 * 24 * time.Hour + dur += time.Duration(d.M) * 30 * 24 * time.Hour + dur += time.Duration(d.Y) * 365 * 24 * time.Hour + + return dur +} diff --git a/pkg/iac/scanners/azure/functions/date_time_epoch.go b/pkg/iac/scanners/azure/functions/date_time_epoch.go new file mode 100644 index 000000000000..5c42a4e25baa --- /dev/null +++ b/pkg/iac/scanners/azure/functions/date_time_epoch.go @@ -0,0 +1,38 @@ +package functions + +import ( + "time" + + smithyTime "github.com/aws/smithy-go/time" +) + +func DateTimeFromEpoch(args ...any) any { + if len(args) != 1 { + return nil + } + + epoch, ok := args[0].(int) + if !ok { + return nil + } + + return smithyTime.ParseEpochSeconds(float64(epoch)).Format(time.RFC3339) +} + +func DateTimeToEpoch(args ...any) any { + if len(args) != 1 { + return nil + } + + dateTime, ok := args[0].(string) + if !ok { + return nil + } + + parsed, err := time.Parse(time.RFC3339, dateTime) + if err != nil { + return nil + } + + return int(parsed.Unix()) +} diff --git a/pkg/iac/scanners/azure/functions/date_time_epoch_test.go b/pkg/iac/scanners/azure/functions/date_time_epoch_test.go new file mode 100644 index 000000000000..1df45badd7cc --- /dev/null +++ b/pkg/iac/scanners/azure/functions/date_time_epoch_test.go @@ -0,0 +1,51 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_DateTimeFromEpoch(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + { + name: "datetime from epoch", + args: []any{ + 1683040573, + }, + expected: "2023-05-02T15:16:13Z", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := DateTimeFromEpoch(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func Test_DateTimeToEpoch(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + { + name: "datetime to epoch", + args: []any{ + "2023-05-02T15:16:13Z", + }, + expected: 1683040573, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := DateTimeToEpoch(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/datetime_add_test.go b/pkg/iac/scanners/azure/functions/datetime_add_test.go new file mode 100644 index 000000000000..3334323bf09c --- /dev/null +++ b/pkg/iac/scanners/azure/functions/datetime_add_test.go @@ -0,0 +1,72 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_DateTimeAdd(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + + { + name: "datetime add 1 years", + args: []any{ + "2010-01-01T00:00:00Z", + "P1Y", + }, + expected: "2011-01-01T00:00:00Z", + }, + { + name: "datetime add 3 months", + args: []any{ + "2010-01-01T00:00:00Z", + "P3M", + }, + expected: "2010-04-01T00:00:00Z", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := DateTimeAdd(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func Test_ISO8601DurationParse(t *testing.T) { + tests := []struct { + name string + args string + expected Iso8601Duration + }{ + + { + name: "parse 1 year", + args: "P1Y", + expected: Iso8601Duration{Y: 1}, + }, + { + name: "parse 3 months", + args: "P3M", + expected: Iso8601Duration{M: 3}, + }, + { + name: "parse 12 hours", + args: "PT12H", + expected: Iso8601Duration{TH: 12}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := parseISO8601(tt.args) + require.NoError(t, err) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/deployment.go b/pkg/iac/scanners/azure/functions/deployment.go new file mode 100644 index 000000000000..caabf1f26c24 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/deployment.go @@ -0,0 +1,75 @@ +package functions + +type DeploymentData interface { + GetParameter(name string) any + GetVariable(variableName string) any + GetEnvVariable(envVariableName string) any +} + +func Deployment(_ DeploymentData, _ ...any) any { + + /* + + { + "name": "", + "properties": { + "templateLink": { + "uri": "" + }, + "template": { + "$schema": "", + "contentVersion": "", + "parameters": {}, + "variables": {}, + "resources": [], + "outputs": {} + }, + "templateHash": "", + "parameters": {}, + "mode": "", + "provisioningState": "" + } + } + + */ + + return nil +} + +func Environment(envProvider DeploymentData, args ...any) any { + if len(args) == 0 { + return nil + } + + envVarName, ok := args[0].(string) + if !ok { + return nil + } + return envProvider.GetEnvVariable(envVarName) +} + +func Variables(varProvider DeploymentData, args ...any) any { + if len(args) == 0 { + return nil + } + + varName, ok := args[0].(string) + if !ok { + return nil + } + return varProvider.GetVariable(varName) +} + +func Parameters(paramProvider DeploymentData, args ...any) any { + if len(args) == 0 { + return nil + } + + paramName, ok := args[0].(string) + if !ok { + return nil + } + + return paramProvider.GetParameter(paramName) + +} diff --git a/pkg/iac/scanners/azure/functions/div.go b/pkg/iac/scanners/azure/functions/div.go new file mode 100644 index 000000000000..2890e3f8c89c --- /dev/null +++ b/pkg/iac/scanners/azure/functions/div.go @@ -0,0 +1,15 @@ +package functions + +func Div(args ...any) any { + + if len(args) != 2 { + return nil + } + + if a, ok := args[0].(int); ok { + if b, ok := args[1].(int); ok { + return a / b + } + } + return nil +} diff --git a/pkg/iac/scanners/azure/functions/div_test.go b/pkg/iac/scanners/azure/functions/div_test.go new file mode 100644 index 000000000000..55a429908bcb --- /dev/null +++ b/pkg/iac/scanners/azure/functions/div_test.go @@ -0,0 +1,38 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Div(t *testing.T) { + tests := []struct { + name string + args []any + expected int + }{ + { + name: "Div 2 by 1", + args: []any{2, 1}, + expected: 2, + }, + { + name: "Div 4 by 2", + args: []any{4, 2}, + expected: 2, + }, + { + name: "Div 6 by 2", + args: []any{6, 2}, + expected: 3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Div(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/empty.go b/pkg/iac/scanners/azure/functions/empty.go new file mode 100644 index 000000000000..96848b9dcd5a --- /dev/null +++ b/pkg/iac/scanners/azure/functions/empty.go @@ -0,0 +1,33 @@ +package functions + +func Empty(args ...any) any { + + if len(args) != 1 { + return false + } + + container := args[0] + + switch cType := container.(type) { + case string: + return cType == "" + case map[string]any: + return len(cType) == 0 + case any: + switch iType := cType.(type) { + case []string: + return len(iType) == 0 + case []bool: + return len(iType) == 0 + case []int: + return len(iType) == 0 + case []float64: + return len(iType) == 0 + case map[string]any: + return len(iType) == 0 + } + + } + + return false +} diff --git a/pkg/iac/scanners/azure/functions/empty_test.go b/pkg/iac/scanners/azure/functions/empty_test.go new file mode 100644 index 000000000000..e325f3c80447 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/empty_test.go @@ -0,0 +1,68 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Empty(t *testing.T) { + tests := []struct { + name string + args []any + expected bool + }{ + { + name: "string is empty", + args: []any{ + "", + }, + expected: true, + }, + { + name: "string is not empty", + args: []any{ + "hello, world", + }, + expected: false, + }, + { + name: "array is empty", + args: []any{ + []string{}, + }, + expected: true, + }, + { + name: "array is not empty", + args: []any{ + []string{"Hello", "World"}, + }, + expected: false, + }, + { + name: "map is empty", + args: []any{ + make(map[string]any), + }, + expected: true, + }, + { + name: "map is not empty", + args: []any{ + map[string]any{ + "hello": "world", + }, + "world", + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + doesContain := Empty(tt.args...) + require.Equal(t, tt.expected, doesContain) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/ends_with.go b/pkg/iac/scanners/azure/functions/ends_with.go new file mode 100644 index 000000000000..1ca6b099657f --- /dev/null +++ b/pkg/iac/scanners/azure/functions/ends_with.go @@ -0,0 +1,22 @@ +package functions + +import "strings" + +func EndsWith(args ...any) any { + + if len(args) != 2 { + return false + } + + stringToSearch, ok := args[0].(string) + if !ok { + return false + } + + stringToFind, ok := args[1].(string) + if !ok { + return false + } + + return strings.HasSuffix(stringToSearch, stringToFind) +} diff --git a/pkg/iac/scanners/azure/functions/ends_with_test.go b/pkg/iac/scanners/azure/functions/ends_with_test.go new file mode 100644 index 000000000000..d8703d6c6493 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/ends_with_test.go @@ -0,0 +1,41 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_EndsWith(t *testing.T) { + + tests := []struct { + name string + args []any + expected bool + }{ + { + name: "string ends with", + args: []any{ + "Hello world!", + "world!", + }, + expected: true, + }, + { + name: "string does not end with", + args: []any{ + "Hello world!", + "world", + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := EndsWith(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/equals.go b/pkg/iac/scanners/azure/functions/equals.go new file mode 100644 index 000000000000..a52499bea9ce --- /dev/null +++ b/pkg/iac/scanners/azure/functions/equals.go @@ -0,0 +1,25 @@ +package functions + +func Equals(args ...any) any { + if len(args) != 2 { + return false + } + + slice1, ok := args[0].([]any) + if ok { + slice2, ok := args[1].([]any) + if ok { + if len(slice1) != len(slice2) { + return false + } + for i := range slice1 { + if slice1[i] != slice2[i] { + return false + } + } + return true + } + } + + return args[0] == args[1] +} diff --git a/pkg/iac/scanners/azure/functions/equals_test.go b/pkg/iac/scanners/azure/functions/equals_test.go new file mode 100644 index 000000000000..afee2c3d92a1 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/equals_test.go @@ -0,0 +1,111 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Equals(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + { + name: "equals with nil", + args: []any{ + nil, + }, + expected: false, + }, + { + name: "equals with nil and string", + args: []any{ + nil, + "test", + }, + expected: false, + }, + { + name: "equals with nil and string and int", + args: []any{ + nil, + "test", + 1, + }, + expected: false, + }, + { + name: "equals with nil and nil and array", + args: []any{ + nil, + nil, + []any{"a", "b", "c"}, + }, + expected: false, + }, + { + name: "equals with nil and nil", + args: []any{ + nil, + nil, + }, + expected: true, + }, + { + name: "equals with string and string", + args: []any{ + "test", + "test", + }, + expected: true, + }, + { + name: "equals with string and string", + args: []any{ + "test", + "test1", + }, + expected: false, + }, + { + name: "equals with int and int", + args: []any{ + 1, + 1, + }, + expected: true, + }, + { + name: "equals with int and int", + args: []any{ + 1, + 2, + }, + expected: false, + }, + { + name: "equals with array and array", + args: []any{ + []any{"a", "b", "c"}, + []any{"a", "b", "c"}, + }, + expected: true, + }, + { + name: "equals with array and array", + args: []any{ + []any{"a", "b", "c"}, + []any{"a", "b", "d"}, + }, + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Equals(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/false.go b/pkg/iac/scanners/azure/functions/false.go new file mode 100644 index 000000000000..926eda794b24 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/false.go @@ -0,0 +1,5 @@ +package functions + +func False(_ ...any) any { + return false +} diff --git a/pkg/iac/scanners/azure/functions/first.go b/pkg/iac/scanners/azure/functions/first.go new file mode 100644 index 000000000000..83bd61eba20c --- /dev/null +++ b/pkg/iac/scanners/azure/functions/first.go @@ -0,0 +1,37 @@ +package functions + +func First(args ...any) any { + if len(args) != 1 { + return "" + } + + container := args[0] + + switch cType := container.(type) { + case string: + if cType != "" { + return string(cType[0]) + } + case any: + switch iType := cType.(type) { + case []string: + if len(iType) > 0 { + return iType[0] + } + case []bool: + if len(iType) > 0 { + return iType[0] + } + case []int: + if len(iType) > 0 { + return iType[0] + } + case []float64: + if len(iType) > 0 { + return iType[0] + } + } + } + + return "" +} diff --git a/pkg/iac/scanners/azure/functions/first_test.go b/pkg/iac/scanners/azure/functions/first_test.go new file mode 100644 index 000000000000..690cd3d1f77d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/first_test.go @@ -0,0 +1,51 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_First(t *testing.T) { + test := []struct { + name string + args []any + expected any + }{ + { + name: "first in empty string", + args: []any{ + "", + }, + expected: "", + }, + { + name: "first in string", + args: []any{ + "Hello", + }, + expected: "H", + }, + { + name: "first in empty slice", + args: []any{ + []string{}, + }, + expected: "", + }, + { + name: "first in slice", + args: []any{ + []string{"Hello", "World"}, + }, + expected: "Hello", + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + actual := First(tt.args...) + require.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/float.go b/pkg/iac/scanners/azure/functions/float.go new file mode 100644 index 000000000000..d7a1acfcdfac --- /dev/null +++ b/pkg/iac/scanners/azure/functions/float.go @@ -0,0 +1,20 @@ +package functions + +import "strconv" + +func Float(args ...any) any { + if len(args) != 1 { + return 0.0 + } + if a, ok := args[0].(int); ok { + return float64(a) + } + if a, ok := args[0].(string); ok { + f, err := strconv.ParseFloat(a, 64) + if err != nil { + return 0.0 + } + return f + } + return 0.0 +} diff --git a/pkg/iac/scanners/azure/functions/float_test.go b/pkg/iac/scanners/azure/functions/float_test.go new file mode 100644 index 000000000000..63610183bcdd --- /dev/null +++ b/pkg/iac/scanners/azure/functions/float_test.go @@ -0,0 +1,36 @@ +package functions + +import "testing" + +func Test_Float(t *testing.T) { + tests := []struct { + name string + args []any + expected float64 + }{ + { + name: "Float with 1", + args: []any{1}, + expected: 1.0, + }, + { + name: "Float with 2", + args: []any{"2"}, + expected: 2.0, + }, + { + name: "Float with 3", + args: []any{"2.3"}, + expected: 2.3, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Float(tt.args...) + if got != tt.expected { + t.Errorf("Float() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/format.go b/pkg/iac/scanners/azure/functions/format.go new file mode 100644 index 000000000000..48395f374aa6 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/format.go @@ -0,0 +1,31 @@ +package functions + +import ( + "fmt" + "strings" +) + +func Format(args ...any) any { + formatter := generateFormatterString(args...) + + return fmt.Sprintf(formatter, args[1:]...) +} + +func generateFormatterString(args ...any) string { + + formatter, ok := args[0].(string) + if !ok { + return "" + } + for i, arg := range args[1:] { + switch arg.(type) { + case string: + formatter = strings.ReplaceAll(formatter, fmt.Sprintf("{%d}", i), "%s") + case int, int32, int64, uint, uint32, uint64: + formatter = strings.ReplaceAll(formatter, fmt.Sprintf("{%d}", i), "%d") + case float64, float32: + formatter = strings.ReplaceAll(formatter, fmt.Sprintf("{%d}", i), "%f") + } + } + return formatter +} diff --git a/pkg/iac/scanners/azure/functions/format_test.go b/pkg/iac/scanners/azure/functions/format_test.go new file mode 100644 index 000000000000..34a0138d1f0a --- /dev/null +++ b/pkg/iac/scanners/azure/functions/format_test.go @@ -0,0 +1,42 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_FormatCall(t *testing.T) { + + tests := []struct { + name string + args []any + expected string + }{ + { + name: "simple format call", + args: []any{ + "{0}/{1}", + "myPostgreSQLServer", + "log_checkpoints", + }, + expected: "myPostgreSQLServer/log_checkpoints", + }, + { + name: "complex format call", + args: []any{ + "{0} + {1} = {2}", + 1, 2, 3, + }, + expected: "1 + 2 = 3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Format(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/functions.go b/pkg/iac/scanners/azure/functions/functions.go new file mode 100644 index 000000000000..f24112d80953 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/functions.go @@ -0,0 +1,99 @@ +package functions + +var deploymentFuncs = map[string]func(dp DeploymentData, args ...any) any{ + "parameters": Parameters, + "deployment": Deployment, + "environment": Environment, + "variables": Variables, +} +var generalFuncs = map[string]func(...any) any{ + + "add": Add, + "and": And, + "array": Array, + "base64": Base64, + "base64ToJson": Base64ToJson, + "bool": Bool, + "coalesce": Coalesce, + "concat": Concat, + "contains": Contains, + "copyIndex": CopyIndex, + "createArray": CreateArray, + "createObject": CreateObject, + "dataUri": DataUri, + "dataUriToString": DataUriToString, + "dateTimeAdd": DateTimeAdd, + "dateTimeFromEpoch": DateTimeFromEpoch, + "dateTimeToEpoch": DateTimeToEpoch, + "div": Div, + "empty": Empty, + "endsWith": EndsWith, + "equals": Equals, + "extensionResourceId": ExtensionResourceID, + "false": False, + "float": Float, + "format": Format, + "greater": Greater, + "greaterOrEquals": GreaterOrEquals, + "guid": Guid, + "if": If, + "indexOf": IndexOf, + "int": Int, + "intersection": Intersection, + "items": Items, + "join": Join, + "lastIndexOf": LastIndexOf, + "length": Length, + "less": Less, + "lessOrEquals": LessOrEquals, + // "list": List, + "managementGroup": ManagementGroup, + "managementGroupResourceId": ManagementGroupResourceID, + "max": Max, + "min": Min, + "mod": Mod, + "mul": Mul, + "newGuid": NewGuid, + "not": Not, + "null": Null, + "or": Or, + "padLeft": PadLeft, + "pickZones": PickZones, + "range": Range, + "reference": Reference, + "replace": Replace, + "resourceGroup": ResourceGroup, + "resourceId": ResourceID, + "skip": Skip, + "split": Split, + "startsWith": StartsWith, + "string": String, + "sub": Sub, + "subscription": Subscription, + "subscriptionResourceId": SubscriptionResourceID, + "substring": SubString, + "tenant": Tenant, + "tenantResourceId": TenantResourceID, + "toLower": ToLower, + "toUpper": ToUpper, + "trim": Trim, + "true": True, + "union": Union, + "union:": Union, + "uniqueString": UniqueString, + "uri": Uri, + "utcNow": UTCNow, +} + +func Evaluate(deploymentProvider DeploymentData, name string, args ...any) any { + + if f, ok := deploymentFuncs[name]; ok { + return f(deploymentProvider, args...) + } + + if f, ok := generalFuncs[name]; ok { + return f(args...) + } + + return nil +} diff --git a/pkg/iac/scanners/azure/functions/greater.go b/pkg/iac/scanners/azure/functions/greater.go new file mode 100644 index 000000000000..458985862010 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/greater.go @@ -0,0 +1,47 @@ +package functions + +func Greater(args ...any) any { + + if len(args) != 2 { + return false + } + + switch arg0 := args[0].(type) { + case int: + arg1, ok := args[1].(int) + if ok { + return arg0 > arg1 + } + case string: + arg1, ok := args[1].(string) + if ok { + return arg0 > arg1 + } + } + + return false +} + +func GreaterOrEquals(args ...any) any { + + if len(args) != 2 { + return false + } + + switch arg0 := args[0].(type) { + case nil: + return args[1] == nil + case int: + arg1, ok := args[1].(int) + if ok { + return arg0 >= arg1 + } + case string: + arg1, ok := args[1].(string) + if ok { + return arg0 >= arg1 + } + } + + return false +} diff --git a/pkg/iac/scanners/azure/functions/greater_test.go b/pkg/iac/scanners/azure/functions/greater_test.go new file mode 100644 index 000000000000..ae5fd1c72a7f --- /dev/null +++ b/pkg/iac/scanners/azure/functions/greater_test.go @@ -0,0 +1,119 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Greater(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + + { + name: "greater with nil and string", + args: []any{ + nil, + "test", + }, + expected: false, + }, + { + name: "greater with nil and nil", + args: []any{ + nil, + nil, + }, + expected: false, + }, + { + name: "greater with string and string", + args: []any{ + "test", + "test", + }, + expected: false, + }, + { + name: "greater with string and int", + args: []any{ + "test", + 1, + }, + expected: false, + }, + { + name: "greater with int and int", + args: []any{ + 1, + 1, + }, + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Greater(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func Test_GreaterThanOrEqual(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + + { + name: "greater with nil and string", + args: []any{ + nil, + "test", + }, + expected: false, + }, + { + name: "greater with nil and nil", + args: []any{ + nil, + nil, + }, + expected: true, + }, + { + name: "greater with string and string", + args: []any{ + "test", + "test", + }, + expected: true, + }, + { + name: "greater with string and int", + args: []any{ + "test", + 1, + }, + expected: false, + }, + { + name: "greater with int and int", + args: []any{ + 1, + 1, + }, + expected: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := GreaterOrEquals(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/guid.go b/pkg/iac/scanners/azure/functions/guid.go new file mode 100644 index 000000000000..b82612b3b082 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/guid.go @@ -0,0 +1,44 @@ +package functions + +import ( + "crypto/sha256" + "strings" + + "github.com/google/uuid" +) + +func Guid(args ...any) any { + + if len(args) == 0 { + return "" + } + + hashParts := make([]string, len(args)) + for i, str := range args { + hashParts[i] = str.(string) + } + + guid, err := generateSeededGUID(hashParts...) + if err != nil { + return "" + } + + return guid.String() +} + +func generateSeededGUID(seedParts ...string) (uuid.UUID, error) { + var id uuid.UUID + + stringToHash := strings.Join(seedParts, "") + + hsha2 := sha256.Sum256([]byte(stringToHash)) + + copy(id[:], hsha2[:16]) + id[6] = (id[6] & 0x0f) | 0x40 // Version 4 + id[8] = (id[8] & 0x3f) | 0x80 // Variant is 10 + return id, nil +} + +func NewGuid(_ ...any) any { + return uuid.NewString() +} diff --git a/pkg/iac/scanners/azure/functions/guid_test.go b/pkg/iac/scanners/azure/functions/guid_test.go new file mode 100644 index 000000000000..d29eed0ebd33 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/guid_test.go @@ -0,0 +1,35 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Guid(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "guid from a string", + args: []any{ + "hello", + }, + expected: "2cf24dba-5fb0-430e-a6e8-3b2ac5b9e29e", + }, + { + name: "guid from an string", + args: []any{}, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + guid := Guid(tt.args...) + require.Equal(t, tt.expected, guid) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/if.go b/pkg/iac/scanners/azure/functions/if.go new file mode 100644 index 000000000000..03cfa752c71f --- /dev/null +++ b/pkg/iac/scanners/azure/functions/if.go @@ -0,0 +1,15 @@ +package functions + +func If(args ...any) any { + + if len(args) != 3 { + return nil + } + + if condition, ok := args[0].(bool); ok { + if condition { + return args[1] + } + } + return args[2] +} diff --git a/pkg/iac/scanners/azure/functions/if_test.go b/pkg/iac/scanners/azure/functions/if_test.go new file mode 100644 index 000000000000..b13c040efaf1 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/if_test.go @@ -0,0 +1,44 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_If(t *testing.T) { + + tests := []struct { + name string + args []any + expected any + }{ + { + name: "If with true", + args: []any{true, "true", "false"}, + expected: "true", + }, + { + name: "If with false", + args: []any{false, "true", "false"}, + expected: "false", + }, + { + name: "If with true and slice returned", + args: []any{ + true, + []any{"Hello", "World"}, + []any{"Goodbye", "World"}, + }, + expected: []any{"Hello", "World"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := If(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/index_of.go b/pkg/iac/scanners/azure/functions/index_of.go new file mode 100644 index 000000000000..25242f90fd7b --- /dev/null +++ b/pkg/iac/scanners/azure/functions/index_of.go @@ -0,0 +1,22 @@ +package functions + +import "strings" + +func IndexOf(args ...any) any { + + if len(args) != 2 { + return -1 + } + + stringToSearch, ok := args[0].(string) + if !ok { + return -1 + } + + stringToFind, ok := args[1].(string) + if !ok { + return -1 + } + + return strings.Index(stringToSearch, stringToFind) +} diff --git a/pkg/iac/scanners/azure/functions/index_of_test.go b/pkg/iac/scanners/azure/functions/index_of_test.go new file mode 100644 index 000000000000..0e5214aaa8e8 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/index_of_test.go @@ -0,0 +1,48 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_IndexOf(t *testing.T) { + + tests := []struct { + name string + args []any + expected int + }{ + { + name: "get index of string that is there", + args: []any{ + "Hello world!", + "Hell", + }, + expected: 0, + }, + { + name: "get index of string that is there as well", + args: []any{ + "Hello world!", + "world", + }, + expected: 6, + }, + { + name: "get index of string that isn't there", + args: []any{ + "Hello world!", + "planet!", + }, + expected: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := IndexOf(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/int.go b/pkg/iac/scanners/azure/functions/int.go new file mode 100644 index 000000000000..9d82077e5ab5 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/int.go @@ -0,0 +1,20 @@ +package functions + +import "strconv" + +func Int(args ...any) any { + if len(args) != 1 { + return 0 + } + if a, ok := args[0].(int); ok { + return a + } + if a, ok := args[0].(string); ok { + i, err := strconv.Atoi(a) + if err != nil { + return 0 + } + return i + } + return 0 +} diff --git a/pkg/iac/scanners/azure/functions/int_test.go b/pkg/iac/scanners/azure/functions/int_test.go new file mode 100644 index 000000000000..aa3e8cd07a13 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/int_test.go @@ -0,0 +1,36 @@ +package functions + +import "testing" + +func Test_Int(t *testing.T) { + tests := []struct { + name string + args []any + expected int + }{ + { + name: "Int with 1", + args: []any{1}, + expected: 1, + }, + { + name: "Int with 2", + args: []any{"2"}, + expected: 2, + }, + { + name: "Int with 2.3", + args: []any{"2.3"}, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Int(tt.args...) + if got != tt.expected { + t.Errorf("Int() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/intersection.go b/pkg/iac/scanners/azure/functions/intersection.go new file mode 100644 index 000000000000..d32db89d4585 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/intersection.go @@ -0,0 +1,77 @@ +package functions + +import ( + "maps" + "sort" +) + +func Intersection(args ...any) any { + + if len(args) < 2 { + return []any{} + } + + switch args[0].(type) { + case map[string]any: + return intersectionMap(args...) + case any: + return intersectionArray(args...) + } + + return []any{} +} + +func intersectionArray(args ...any) any { + var result []any + hash := make(map[any]bool) + + for _, arg := range args[0].([]any) { + hash[arg] = true + } + + for i := 1; i < len(args); i++ { + workingHash := make(map[any]bool) + argArr, ok := args[i].([]any) + if !ok { + continue + } + for _, item := range argArr { + if _, ok := hash[item]; ok { + workingHash[item] = true + } + } + hash = workingHash + } + + for k := range hash { + result = append(result, k) + } + + sort.Slice(result, func(i, j int) bool { + return result[i].(string) < result[j].(string) + }) + + return result +} + +func intersectionMap(args ...any) any { + hash := make(map[string]any) + + maps.Copy(hash, args[0].(map[string]any)) + + for i := 1; i < len(args); i++ { + workingHash := make(map[string]any) + argArr, ok := args[i].(map[string]any) + if !ok { + continue + } + for k, v := range argArr { + if ev, ok := hash[k]; ok && ev == v { + workingHash[k] = v + } + } + hash = workingHash + } + + return hash +} diff --git a/pkg/iac/scanners/azure/functions/intersection_test.go b/pkg/iac/scanners/azure/functions/intersection_test.go new file mode 100644 index 000000000000..85b6b0564e30 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/intersection_test.go @@ -0,0 +1,106 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Intersect(t *testing.T) { + + tests := []struct { + name string + args []any + expected any + }{ + { + name: "intersect two arrays", + args: []any{ + []any{"a", "b", "c"}, + []any{"b", "c", "d"}, + }, + expected: []any{"b", "c"}, + }, + { + name: "intersect three arrays", + args: []any{ + []any{"a", "b", "c", "d"}, + []any{"b", "c", "d"}, + []any{"b", "c"}, + }, + expected: []any{"b", "c"}, + }, + { + name: "intersect two arrays with one empty", + args: []any{ + []any{"a", "b", "c"}, + []any{}, + }, + expected: []any(nil), + }, + { + name: "intersect two arrays with both empty", + args: []any{ + []any{}, + []any{}, + }, + expected: []any(nil), + }, + { + name: "intersect two arrays with both nil", + args: []any{ + nil, + nil, + }, + expected: []any{}, + }, + { + name: "intersect two maps", + args: []any{ + map[string]any{ + "a": "a", + "b": "b", + "c": "c", + }, + map[string]any{ + "b": "b", + "c": "c", + "d": "d", + }, + }, + expected: map[string]any{ + "b": "b", + "c": "c", + }, + }, + { + name: "intersect three maps", + args: []any{ + map[string]any{ + "a": "a", + "b": "b", + "c": "c", + }, + map[string]any{ + "b": "b", + "c": "c", + "d": "d", + }, + map[string]any{ + "b": "b", + "d": "d", + }, + }, + expected: map[string]any{ + "b": "b", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Intersection(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/items.go b/pkg/iac/scanners/azure/functions/items.go new file mode 100644 index 000000000000..6f4e3af77f8b --- /dev/null +++ b/pkg/iac/scanners/azure/functions/items.go @@ -0,0 +1,5 @@ +package functions + +func Items(_ ...any) any { + return nil +} diff --git a/pkg/iac/scanners/azure/functions/join.go b/pkg/iac/scanners/azure/functions/join.go new file mode 100644 index 000000000000..10fd76ee9f43 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/join.go @@ -0,0 +1,22 @@ +package functions + +import "strings" + +func Join(args ...any) any { + + if len(args) != 2 { + return "" + } + + container, ok := args[0].([]string) + if !ok { + return "" + } + + separator, ok := args[1].(string) + if !ok { + return "" + } + + return strings.Join(container, separator) +} diff --git a/pkg/iac/scanners/azure/functions/join_test.go b/pkg/iac/scanners/azure/functions/join_test.go new file mode 100644 index 000000000000..614bdfa94ee5 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/join_test.go @@ -0,0 +1,39 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Join(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "join strings with no items", + args: []any{ + []string{}, + " ", + }, + expected: "", + }, + { + name: "join strings", + args: []any{ + []string{"Hello", "World"}, + " ", + }, + expected: "Hello World", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Join(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/json.go b/pkg/iac/scanners/azure/functions/json.go new file mode 100644 index 000000000000..07f7c1828ae0 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/json.go @@ -0,0 +1,20 @@ +package functions + +import "encoding/json" + +func JSON(args ...any) any { + if len(args) != 1 { + return "" + } + + value, ok := args[0].(string) + if !ok { + return "" + } + + var jsonType map[string]any + if err := json.Unmarshal([]byte(value), &jsonType); err != nil { + return "" + } + return jsonType +} diff --git a/pkg/iac/scanners/azure/functions/json_test.go b/pkg/iac/scanners/azure/functions/json_test.go new file mode 100644 index 000000000000..46f2285618cd --- /dev/null +++ b/pkg/iac/scanners/azure/functions/json_test.go @@ -0,0 +1,42 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_JSON(t *testing.T) { + + tests := []struct { + name string + args []any + expected map[string]any + }{ + { + name: "simple json string to json type", + args: []any{ + `{"hello": "world"}`, + }, + expected: map[string]any{ + "hello": "world", + }, + }, + { + name: "more complex json string to json type", + args: []any{ + `{"hello": ["world", "world2"]}`, + }, + expected: map[string]any{ + "hello": []any{"world", "world2"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := JSON(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/last.go b/pkg/iac/scanners/azure/functions/last.go new file mode 100644 index 000000000000..bbf307515d82 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/last.go @@ -0,0 +1,37 @@ +package functions + +func Last(args ...any) any { + if len(args) != 1 { + return "" + } + + container := args[0] + + switch cType := container.(type) { + case string: + if cType != "" { + return string(cType[len(cType)-1]) + } + case any: + switch iType := cType.(type) { + case []string: + if len(iType) > 0 { + return iType[len(iType)-1] + } + case []bool: + if len(iType) > 0 { + return iType[len(iType)-1] + } + case []int: + if len(iType) > 0 { + return iType[len(iType)-1] + } + case []float64: + if len(iType) > 0 { + return iType[len(iType)-1] + } + } + } + + return "" +} diff --git a/pkg/iac/scanners/azure/functions/last_index_of.go b/pkg/iac/scanners/azure/functions/last_index_of.go new file mode 100644 index 000000000000..1fa46d132c79 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/last_index_of.go @@ -0,0 +1,22 @@ +package functions + +import "strings" + +func LastIndexOf(args ...any) any { + + if len(args) != 2 { + return -1 + } + + stringToSearch, ok := args[0].(string) + if !ok { + return -1 + } + + stringToFind, ok := args[1].(string) + if !ok { + return -1 + } + + return strings.LastIndex(stringToSearch, stringToFind) +} diff --git a/pkg/iac/scanners/azure/functions/last_index_of_test.go b/pkg/iac/scanners/azure/functions/last_index_of_test.go new file mode 100644 index 000000000000..3b83a68fe744 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/last_index_of_test.go @@ -0,0 +1,48 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_LastIndexOf(t *testing.T) { + + tests := []struct { + name string + args []any + expected int + }{ + { + name: "get last index of string that is there", + args: []any{ + "Hello world!", + "l", + }, + expected: 9, + }, + { + name: "get last index of string that is there as well", + args: []any{ + "Hello world!", + "world", + }, + expected: 6, + }, + { + name: "get last index of string that isn't there", + args: []any{ + "Hello world!", + "planet!", + }, + expected: -1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := LastIndexOf(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/last_test.go b/pkg/iac/scanners/azure/functions/last_test.go new file mode 100644 index 000000000000..4073392b131d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/last_test.go @@ -0,0 +1,51 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Last(t *testing.T) { + test := []struct { + name string + args []any + expected any + }{ + { + name: "last in empty string", + args: []any{ + "", + }, + expected: "", + }, + { + name: "last in string", + args: []any{ + "Hello", + }, + expected: "o", + }, + { + name: "last in empty slice", + args: []any{ + []string{}, + }, + expected: "", + }, + { + name: "last in slice", + args: []any{ + []string{"Hello", "World"}, + }, + expected: "World", + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + actual := Last(tt.args...) + require.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/length.go b/pkg/iac/scanners/azure/functions/length.go new file mode 100644 index 000000000000..c63a00329709 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/length.go @@ -0,0 +1,29 @@ +package functions + +func Length(args ...any) any { + + if len(args) != 1 { + return 0 + } + + switch ctype := args[0].(type) { + case string: + return len(ctype) + case map[string]any: + return len(ctype) + case any: + switch iType := ctype.(type) { + case []string: + return len(iType) + case []bool: + return len(iType) + case []int: + return len(iType) + case []float64: + return len(iType) + case []any: + return len(iType) + } + } + return 0 +} diff --git a/pkg/iac/scanners/azure/functions/length_test.go b/pkg/iac/scanners/azure/functions/length_test.go new file mode 100644 index 000000000000..09a9f68a4a67 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/length_test.go @@ -0,0 +1,53 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Length(t *testing.T) { + + tests := []struct { + name string + args []any + expected int + }{ + { + name: "length of a string", + args: []any{ + "hello", + }, + expected: 5, + }, + { + name: "length of an empty string", + args: []any{ + "", + }, + expected: 0, + }, + { + name: "length of an empty slice", + args: []any{ + []string{}, + }, + expected: 0, + }, + { + name: "length of an slice with items", + args: []any{ + []string{ + "hello", "world", + }, + }, + expected: 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Length(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/less.go b/pkg/iac/scanners/azure/functions/less.go new file mode 100644 index 000000000000..5cdd7cb84c1d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/less.go @@ -0,0 +1,47 @@ +package functions + +func Less(args ...any) any { + + if len(args) != 2 { + return false + } + + switch arg0 := args[0].(type) { + case int: + arg1, ok := args[1].(int) + if ok { + return arg0 < arg1 + } + case string: + arg1, ok := args[1].(string) + if ok { + return arg0 < arg1 + } + } + + return false +} + +func LessOrEquals(args ...any) any { + + if len(args) != 2 { + return false + } + + switch arg0 := args[0].(type) { + case nil: + return args[1] == nil + case int: + arg1, ok := args[1].(int) + if ok { + return arg0 <= arg1 + } + case string: + arg1, ok := args[1].(string) + if ok { + return arg0 <= arg1 + } + } + + return false +} diff --git a/pkg/iac/scanners/azure/functions/less_test.go b/pkg/iac/scanners/azure/functions/less_test.go new file mode 100644 index 000000000000..43c7b01e63d6 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/less_test.go @@ -0,0 +1,119 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Less(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + + { + name: "less with nil and string", + args: []any{ + nil, + "test", + }, + expected: false, + }, + { + name: "less with nil and nil", + args: []any{ + nil, + nil, + }, + expected: false, + }, + { + name: "less with string and string", + args: []any{ + "test", + "test", + }, + expected: false, + }, + { + name: "less with string and int", + args: []any{ + "test", + 1, + }, + expected: false, + }, + { + name: "less with int and int", + args: []any{ + 1, + 1, + }, + expected: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Less(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} + +func Test_LessThanOrEqual(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + + { + name: "less with nil and string", + args: []any{ + nil, + "test", + }, + expected: false, + }, + { + name: "less with nil and nil", + args: []any{ + nil, + nil, + }, + expected: true, + }, + { + name: "less with string and string", + args: []any{ + "test", + "test", + }, + expected: true, + }, + { + name: "less with string and int", + args: []any{ + "test", + 1, + }, + expected: false, + }, + { + name: "less with int and int", + args: []any{ + 1, + 1, + }, + expected: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := LessOrEquals(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/max.go b/pkg/iac/scanners/azure/functions/max.go new file mode 100644 index 000000000000..e0519bc96e2f --- /dev/null +++ b/pkg/iac/scanners/azure/functions/max.go @@ -0,0 +1,32 @@ +package functions + +func Max(args ...any) any { + switch args[0].(type) { + case int: + var ints []int + for _, arg := range args { + ints = append(ints, arg.(int)) + } + return maxInt(ints) + case any: + if iType, ok := args[0].([]int); ok { + return maxInt(iType) + } + } + return 0 +} + +func maxInt(args []int) int { + if len(args) == 0 { + return 0 + } + + maxN := args[0] + + for i := 1; i < len(args); i++ { + if args[i] > maxN { + maxN = args[i] + } + } + return maxN +} diff --git a/pkg/iac/scanners/azure/functions/max_test.go b/pkg/iac/scanners/azure/functions/max_test.go new file mode 100644 index 000000000000..a64afa1e0277 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/max_test.go @@ -0,0 +1,58 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Max(t *testing.T) { + test := []struct { + name string + args []any + expected int + }{ + { + name: "max of empty slice", + args: []any{ + []int{}, + }, + expected: 0, + }, + { + name: "max of slice", + args: []any{ + []int{1, 2, 3}, + }, + expected: 3, + }, + { + name: "max of slice with negative numbers", + args: []any{ + []int{-1, -2, -3}, + }, + expected: -1, + }, + { + name: "max of slice with negative and positive numbers", + args: []any{ + []int{-1, 2, -3}, + }, + expected: 2, + }, + { + name: "max of comma separated numbers", + args: []any{ + 1, 2, 3, 4, 5, + }, + expected: 5, + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + actual := Max(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/min.go b/pkg/iac/scanners/azure/functions/min.go new file mode 100644 index 000000000000..cc857aef2b16 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/min.go @@ -0,0 +1,32 @@ +package functions + +func Min(args ...any) any { + switch args[0].(type) { + case int: + var ints []int + for _, arg := range args { + ints = append(ints, arg.(int)) + } + return minInt(ints) + case any: + if iType, ok := args[0].([]int); ok { + return minInt(iType) + } + } + return 0 +} + +func minInt(args []int) int { + if len(args) == 0 { + return 0 + } + + minN := args[0] + + for i := 1; i < len(args); i++ { + if args[i] < minN { + minN = args[i] + } + } + return minN +} diff --git a/pkg/iac/scanners/azure/functions/min_test.go b/pkg/iac/scanners/azure/functions/min_test.go new file mode 100644 index 000000000000..c6a0da68a226 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/min_test.go @@ -0,0 +1,58 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Min(t *testing.T) { + test := []struct { + name string + args []any + expected int + }{ + { + name: "min of empty slice", + args: []any{ + []int{}, + }, + expected: 0, + }, + { + name: "min of slice", + args: []any{ + []int{1, 2, 3}, + }, + expected: 1, + }, + { + name: "min of slice with negative numbers", + args: []any{ + []int{-1, -2, -3}, + }, + expected: -3, + }, + { + name: "min of slice with negative and positive numbers", + args: []any{ + []int{-1, 2, -3}, + }, + expected: -3, + }, + { + name: "min of comma separated numbers", + args: []any{ + 1, 2, 3, 4, 5, + }, + expected: 1, + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + actual := Min(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/mod.go b/pkg/iac/scanners/azure/functions/mod.go new file mode 100644 index 000000000000..8c03d4241df2 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/mod.go @@ -0,0 +1,14 @@ +package functions + +func Mod(args ...any) any { + if len(args) != 2 { + return 0 + } + + if a, ok := args[0].(int); ok { + if b, ok := args[1].(int); ok { + return a % b + } + } + return 0 +} diff --git a/pkg/iac/scanners/azure/functions/mod_test.go b/pkg/iac/scanners/azure/functions/mod_test.go new file mode 100644 index 000000000000..60a055ae1544 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/mod_test.go @@ -0,0 +1,41 @@ +package functions + +import "testing" + +func Test_Mod(t *testing.T) { + tests := []struct { + name string + args []any + expected int + }{ + { + name: "Mod with 1 and 2", + args: []any{1, 2}, + expected: 1, + }, + { + name: "Mod with 2 and 3", + args: []any{2, 3}, + expected: 2, + }, + { + name: "Mod with 3 and -4", + args: []any{3, -4}, + expected: 3, + }, + { + name: "Mod with 7 and 3", + args: []any{7, 3}, + expected: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Mod(tt.args...) + if got != tt.expected { + t.Errorf("Mod() = %v, want %v", got, tt.expected) + } + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/mul.go b/pkg/iac/scanners/azure/functions/mul.go new file mode 100644 index 000000000000..0cf8a8242c56 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/mul.go @@ -0,0 +1,15 @@ +package functions + +func Mul(args ...any) any { + + if len(args) != 2 { + return nil + } + + if a, ok := args[0].(int); ok { + if b, ok := args[1].(int); ok { + return a * b + } + } + return nil +} diff --git a/pkg/iac/scanners/azure/functions/mul_test.go b/pkg/iac/scanners/azure/functions/mul_test.go new file mode 100644 index 000000000000..77a74a4e2cf1 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/mul_test.go @@ -0,0 +1,38 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Mul(t *testing.T) { + tests := []struct { + name string + args []any + expected int + }{ + { + name: "multiply -2 by 1", + args: []any{-2, 1}, + expected: -2, + }, + { + name: "multiply 4 by 2", + args: []any{4, 2}, + expected: 8, + }, + { + name: "multiply 6 by 3", + args: []any{6, 3}, + expected: 18, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Mul(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/not.go b/pkg/iac/scanners/azure/functions/not.go new file mode 100644 index 000000000000..7b12d169fc18 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/not.go @@ -0,0 +1,13 @@ +package functions + +func Not(args ...any) any { + + if len(args) != 1 { + return false + } + + if condition, ok := args[0].(bool); ok { + return !condition + } + return false +} diff --git a/pkg/iac/scanners/azure/functions/not_test.go b/pkg/iac/scanners/azure/functions/not_test.go new file mode 100644 index 000000000000..808e3b5643dc --- /dev/null +++ b/pkg/iac/scanners/azure/functions/not_test.go @@ -0,0 +1,33 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Not(t *testing.T) { + tests := []struct { + name string + args []any + expected bool + }{ + { + name: "Not with true", + args: []any{true}, + expected: false, + }, + { + name: "Not with false", + args: []any{false}, + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Not(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/null.go b/pkg/iac/scanners/azure/functions/null.go new file mode 100644 index 000000000000..363e23ebadbf --- /dev/null +++ b/pkg/iac/scanners/azure/functions/null.go @@ -0,0 +1,5 @@ +package functions + +func Null(_ ...any) any { + return nil +} diff --git a/pkg/iac/scanners/azure/functions/null_test.go b/pkg/iac/scanners/azure/functions/null_test.go new file mode 100644 index 000000000000..3394193415fb --- /dev/null +++ b/pkg/iac/scanners/azure/functions/null_test.go @@ -0,0 +1,12 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Null(t *testing.T) { + + assert.Nil(t, Null()) +} diff --git a/pkg/iac/scanners/azure/functions/or.go b/pkg/iac/scanners/azure/functions/or.go new file mode 100644 index 000000000000..b94b706e50ae --- /dev/null +++ b/pkg/iac/scanners/azure/functions/or.go @@ -0,0 +1,20 @@ +package functions + +func Or(args ...any) any { + + if len(args) <= 1 { + return false + } + + for _, arg := range args { + arg1, ok := arg.(bool) + if !ok { + return false + } + if arg1 { + return true + } + + } + return false +} diff --git a/pkg/iac/scanners/azure/functions/or_test.go b/pkg/iac/scanners/azure/functions/or_test.go new file mode 100644 index 000000000000..83c7d29cf35d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/or_test.go @@ -0,0 +1,44 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Or(t *testing.T) { + + tests := []struct { + name string + args []any + expected bool + }{ + { + name: "And with same 2 bools", + args: []any{true, true}, + expected: true, + }, + { + name: "And with same 3 bools", + args: []any{true, true, true}, + expected: true, + }, + { + name: "And with different 4 bools", + args: []any{true, true, false, true}, + expected: true, + }, + { + name: "And with same false 4 bools", + args: []any{false, false, false, false}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Or(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/pad.go b/pkg/iac/scanners/azure/functions/pad.go new file mode 100644 index 000000000000..a95a2e626303 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/pad.go @@ -0,0 +1,32 @@ +package functions + +import "strings" + +func PadLeft(args ...any) any { + if len(args) != 3 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return "" + } + + length, ok := args[1].(int) + if !ok { + return "" + } + + pad, ok := args[2].(string) + if !ok { + return "" + } + + if len(input) >= length { + return input + } + + repeat := (length - len(input)) / len(pad) + + return strings.Repeat(pad, repeat) + input +} diff --git a/pkg/iac/scanners/azure/functions/pad_test.go b/pkg/iac/scanners/azure/functions/pad_test.go new file mode 100644 index 000000000000..aefc367f12af --- /dev/null +++ b/pkg/iac/scanners/azure/functions/pad_test.go @@ -0,0 +1,61 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_PadLeft(t *testing.T) { + + tests := []struct { + name string + args []any + expected string + }{ + { + name: "pad left with a input smaller than length", + args: []any{ + "1234", + 8, + "0", + }, + expected: "00001234", + }, + { + name: "pad left with a input larger than length", + args: []any{ + "1234", + 2, + "0", + }, + expected: "1234", + }, + { + name: "pad left with a input same as than length", + args: []any{ + "1234", + 4, + "0", + }, + expected: "1234", + }, + { + name: "pad left with larger padding character", + args: []any{ + "1234", + 8, + "00", + }, + expected: "00001234", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := PadLeft(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/parameters.go b/pkg/iac/scanners/azure/functions/parameters.go new file mode 100644 index 000000000000..b13ee3d60e4e --- /dev/null +++ b/pkg/iac/scanners/azure/functions/parameters.go @@ -0,0 +1 @@ +package functions diff --git a/pkg/iac/scanners/azure/functions/pick_zones.go b/pkg/iac/scanners/azure/functions/pick_zones.go new file mode 100644 index 000000000000..25d26205a5f0 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/pick_zones.go @@ -0,0 +1,20 @@ +package functions + +func PickZones(args ...any) any { + if len(args) < 3 { + return nil + } + numOfZones := 1 + + if len(args) > 3 { + numOfZones = min(args[3].(int), 3) + } + + var zones []int + + for i := 1; i <= numOfZones; i++ { + zones = append(zones, i) + } + + return zones +} diff --git a/pkg/iac/scanners/azure/functions/pick_zones_test.go b/pkg/iac/scanners/azure/functions/pick_zones_test.go new file mode 100644 index 000000000000..19db480f9b0d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/pick_zones_test.go @@ -0,0 +1,14 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_PickZones(t *testing.T) { + assert.Equal(t, []int{1}, PickZones("Microsoft.Compute", "virtualmachines", "eu-west-1")) + assert.Equal(t, []int{1, 2}, PickZones("Microsoft.Compute", "virtualmachines", "eu-west-1", 2)) + assert.Equal(t, []int{1, 2, 3}, PickZones("Microsoft.Compute", "virtualmachines", "eu-west-1", 3)) + assert.Equal(t, []int{1, 2, 3}, PickZones("Microsoft.Compute", "virtualmachines", "eu-west-1", 4)) +} diff --git a/pkg/iac/scanners/azure/functions/range.go b/pkg/iac/scanners/azure/functions/range.go new file mode 100644 index 000000000000..33090e276505 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/range.go @@ -0,0 +1,30 @@ +package functions + +func Range(args ...any) any { + + if len(args) != 2 { + return []any{} + } + + start, ok := args[0].(int) + if !ok { + return []int{} + } + + count, ok := args[1].(int) + if !ok { + return []int{} + } + + if count > 10000 { + count = 10000 + } + + result := make([]int, count) + + for i := 0; i < count; i++ { + result[i] = start + i + } + + return result +} diff --git a/pkg/iac/scanners/azure/functions/range_test.go b/pkg/iac/scanners/azure/functions/range_test.go new file mode 100644 index 000000000000..eb41cb9b83ec --- /dev/null +++ b/pkg/iac/scanners/azure/functions/range_test.go @@ -0,0 +1,47 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Range(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + { + name: "range for 3 from 1", + args: []any{ + 1, + 3, + }, + expected: []int{1, 2, 3}, + }, + { + name: "range with for 10 from 3", + args: []any{ + 3, + 10, + }, + expected: []int{3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + }, + { + name: "range with for 10 from -10", + args: []any{ + -10, + 10, + }, + expected: []int{-10, -9, -8, -7, -6, -5, -4, -3, -2, -1}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Range(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/reference.go b/pkg/iac/scanners/azure/functions/reference.go new file mode 100644 index 000000000000..cb768e102e0d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/reference.go @@ -0,0 +1,12 @@ +package functions + +import "fmt" + +// Reference function can't work as per Azure because it requires Azure ARM logic +// best effort is to return the resourcename with a suffix to try and make it unique +func Reference(args ...any) any { + if len(args) < 1 { + return nil + } + return fmt.Sprintf("%v-reference", args[0]) +} diff --git a/pkg/iac/scanners/azure/functions/reference_test.go b/pkg/iac/scanners/azure/functions/reference_test.go new file mode 100644 index 000000000000..c669fe98d3f0 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/reference_test.go @@ -0,0 +1,12 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Reference(t *testing.T) { + assert.Equal(t, "test-reference", Reference("test")) + assert.Equal(t, "123-reference", Reference(123)) +} diff --git a/pkg/iac/scanners/azure/functions/replace.go b/pkg/iac/scanners/azure/functions/replace.go new file mode 100644 index 000000000000..bbe6ec8f4d9d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/replace.go @@ -0,0 +1,26 @@ +package functions + +import "strings" + +func Replace(args ...any) any { + if len(args) != 3 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return "" + } + + purana, ok := args[1].(string) + if !ok { + return "" + } + + nava, ok := args[2].(string) + if !ok { + return "" + } + + return strings.ReplaceAll(input, purana, nava) +} diff --git a/pkg/iac/scanners/azure/functions/replace_test.go b/pkg/iac/scanners/azure/functions/replace_test.go new file mode 100644 index 000000000000..ed827d2a7756 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/replace_test.go @@ -0,0 +1,41 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Replace(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "replace a string", + args: []any{ + "hello", + "l", + "p", + }, + expected: "heppo", + }, + { + name: "replace a string with invalid replacement", + args: []any{ + "hello", + "q", + "p", + }, + expected: "hello", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Replace(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/resource.go b/pkg/iac/scanners/azure/functions/resource.go new file mode 100644 index 000000000000..33dffbc9e0c2 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/resource.go @@ -0,0 +1,48 @@ +package functions + +import ( + "fmt" +) + +func ResourceID(args ...any) any { + if len(args) < 2 { + return nil + } + + var resourceID string + + for _, arg := range args { + resourceID += "/" + fmt.Sprintf("%v", arg) + } + + return resourceID +} + +func ExtensionResourceID(args ...any) any { + if len(args) < 3 { + return nil + } + + var resourceID string + + for _, arg := range args { + resourceID += "/" + fmt.Sprintf("%v", arg) + } + + return resourceID +} + +func ResourceGroup(_ ...any) any { + return fmt.Sprintf(`{ +"id": "/subscriptions/%s/resourceGroups/PlaceHolderResourceGroup", +"name": "Placeholder Resource Group", +"type":"Microsoft.Resources/resourceGroups", +"location": "westus", +"managedBy": "%s", +"tags": { +}, +"properties": { + "provisioningState": "Succeeded +} +}`, subscriptionID, managingResourceID) +} diff --git a/pkg/iac/scanners/azure/functions/resource_test.go b/pkg/iac/scanners/azure/functions/resource_test.go new file mode 100644 index 000000000000..d6dac14b4184 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/resource_test.go @@ -0,0 +1,12 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_ResourceID(t *testing.T) { + assert.Equal(t, "/test1/test2", ResourceID("test1", "test2")) + assert.Equal(t, "/test1/123", ResourceID("test1", 123)) +} diff --git a/pkg/iac/scanners/azure/functions/scope.go b/pkg/iac/scanners/azure/functions/scope.go new file mode 100644 index 000000000000..5801cba7c4b0 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/scope.go @@ -0,0 +1,106 @@ +package functions + +import ( + "fmt" + + "github.com/google/uuid" +) + +var ( + tenantID = uuid.NewString() + groupID = uuid.NewString() + updaterID = uuid.NewString() + subscriptionID = uuid.NewString() + managingResourceID = uuid.NewString() +) + +func ManagementGroup(_ ...any) any { + + return fmt.Sprintf(`{ + "id": "/providers/Microsoft.Management/managementGroups/mgPlaceholder", + "name": "mgPlaceholder", + "properties": { + "details": { + "parent": { + "displayName": "Tenant Root Group", + "id": "/providers/Microsoft.Management/managementGroups/%[1]s", + "name": "%[1]s" + }, + "updatedBy": "%[2]s", + "updatedTime": "2020-07-23T21:05:52.661306Z", + "version": "1" + }, + "displayName": "Management PlaceHolder Group", + "tenantId": "%[3]s" + }, + "type": "/providers/Microsoft.Management/managementGroups" + } +`, groupID, updaterID, tenantID) +} + +func ManagementGroupResourceID(args ...any) any { + if len(args) < 2 { + return "" + } + + switch len(args) { + case 3: + return fmt.Sprintf("/providers/Microsoft.Management/managementGroups/%s/providers/%s/%s/%s", groupID, args[0], args[1], args[2]) + case 4: + return fmt.Sprintf("/providers/Microsoft.Management/managementGroups/%s/providers/%s/%s/%s", args[0], args[1], args[2], args[3]) + default: + return fmt.Sprintf("/providers/Microsoft.Management/managementGroups/%s/providers/%s/%s", groupID, args[0], args[1]) + } + +} + +func Subscription(_ ...any) any { + return fmt.Sprintf(`{ + "id": "/subscriptions/%[1]s", + "subscriptionId": "%[1]s", + "tenantId": "%[2]s", + "displayName": "Placeholder Subscription" +}`, subscriptionID, tenantID) +} + +func SubscriptionResourceID(args ...any) any { + if len(args) < 2 { + return nil + } + + switch len(args) { + + case 3: + return fmt.Sprintf("/subscriptions/%s/providers/%s/%s/%s", subscriptionID, args[0], args[1], args[2]) + case 4: + // subscription ID has been provided so use that + return fmt.Sprintf("/subscriptions/%s/providers/%s/%s/%s", args[0], args[1], args[2], args[3]) + default: + + return fmt.Sprintf("/subscriptions/%s/providers/%s/%s", subscriptionID, args[0], args[1]) + } +} + +func Tenant(_ ...any) any { + return fmt.Sprintf(`{ + "countryCode": "US", + "displayName": "Placeholder Tenant Name", + "id": "/tenants/%[1]s", + "tenantId": "%[1]s" + }`, tenantID) +} + +func TenantResourceID(args ...any) any { + if len(args) < 2 { + return nil + } + + switch len(args) { + case 3: + return fmt.Sprintf("/providers/%s/%s/%s", args[0], args[1], args[2]) + + default: + return fmt.Sprintf("/providers/%s/%s", args[0], args[1]) + } + +} diff --git a/pkg/iac/scanners/azure/functions/scope_test.go b/pkg/iac/scanners/azure/functions/scope_test.go new file mode 100644 index 000000000000..10fe7a5f6d02 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/scope_test.go @@ -0,0 +1,34 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_SubscriptionResourceID(t *testing.T) { + + tests := []struct { + name string + args []any + expected string + }{ + { + name: "scope resource id with subscription ID", + args: []any{ + "4ec875a5-41a2-4837-88cf-4266466e65ed", + "Microsoft.Authorization/roleDefinitions", + "8e3af657-a8ff-443c-a75c-2fe8c4bcb635", + "b34282f6-5e3c-4306-8741-ebd7a871d187", + }, + expected: "/subscriptions/4ec875a5-41a2-4837-88cf-4266466e65ed/providers/Microsoft.Authorization/roleDefinitions/8e3af657-a8ff-443c-a75c-2fe8c4bcb635/b34282f6-5e3c-4306-8741-ebd7a871d187", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := SubscriptionResourceID(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/skip.go b/pkg/iac/scanners/azure/functions/skip.go new file mode 100644 index 000000000000..dd99604fb680 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/skip.go @@ -0,0 +1,34 @@ +package functions + +func Skip(args ...any) any { + if len(args) != 2 { + return "" + } + + count, ok := args[1].(int) + if !ok { + return "" + } + switch input := args[0].(type) { + case string: + if count > len(input) { + return "" + } + return input[count:] + case any: + switch iType := input.(type) { + case []int: + return iType[count:] + case []string: + return iType[count:] + case []bool: + return iType[count:] + case []float64: + return iType[count:] + case []any: + return iType[count:] + } + } + + return "" +} diff --git a/pkg/iac/scanners/azure/functions/skip_test.go b/pkg/iac/scanners/azure/functions/skip_test.go new file mode 100644 index 000000000000..3a5645cbf6ee --- /dev/null +++ b/pkg/iac/scanners/azure/functions/skip_test.go @@ -0,0 +1,65 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Skip(t *testing.T) { + + tests := []struct { + name string + args []any + expected any + }{ + { + name: "skip a string", + args: []any{ + "hello", + 1, + }, + expected: "ello", + }, + { + name: "skip beyond the length a string", + args: []any{ + "hello", + 6, + }, + expected: "", + }, + { + name: "skip with a zero count on a string", + args: []any{ + "hello", + 0, + }, + expected: "hello", + }, + { + name: "skip with slice of ints", + args: []any{ + []int{1, 2, 3, 4, 5}, + 2, + }, + expected: []int{3, 4, 5}, + }, + { + name: "skip with slice of strings", + args: []any{ + []string{"hello", "world"}, + 1, + }, + expected: []string{"world"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Skip(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/split.go b/pkg/iac/scanners/azure/functions/split.go new file mode 100644 index 000000000000..598f1995211d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/split.go @@ -0,0 +1,35 @@ +package functions + +import "strings" + +func Split(args ...any) any { + if len(args) != 2 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return "" + } + + switch separator := args[1].(type) { + case string: + return strings.Split(input, separator) + case any: + if separator, ok := separator.([]string); ok { + m := make(map[rune]int) + for _, r := range separator { + r := rune(r[0]) + m[r] = 1 + } + + splitter := func(r rune) bool { + return m[r] == 1 + } + + return strings.FieldsFunc(input, splitter) + } + + } + return []string{} +} diff --git a/pkg/iac/scanners/azure/functions/split_test.go b/pkg/iac/scanners/azure/functions/split_test.go new file mode 100644 index 000000000000..e396dd2ef937 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/split_test.go @@ -0,0 +1,38 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Split(t *testing.T) { + tests := []struct { + name string + args []any + expected []string + }{ + { + name: "split a string", + args: []any{ + "hello, world", + ",", + }, + expected: []string{"hello", " world"}, + }, + { + name: "split a string with multiple separators", + args: []any{ + "one;two,three", + []string{",", ";"}, + }, + expected: []string{"one", "two", "three"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Split(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/starts_with.go b/pkg/iac/scanners/azure/functions/starts_with.go new file mode 100644 index 000000000000..3ae79bfff34b --- /dev/null +++ b/pkg/iac/scanners/azure/functions/starts_with.go @@ -0,0 +1,22 @@ +package functions + +import "strings" + +func StartsWith(args ...any) any { + + if len(args) != 2 { + return false + } + + stringToSearch, ok := args[0].(string) + if !ok { + return false + } + + stringToFind, ok := args[1].(string) + if !ok { + return false + } + + return strings.HasPrefix(stringToSearch, stringToFind) +} diff --git a/pkg/iac/scanners/azure/functions/starts_with_test.go b/pkg/iac/scanners/azure/functions/starts_with_test.go new file mode 100644 index 000000000000..400b36c1b894 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/starts_with_test.go @@ -0,0 +1,41 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_StartsWith(t *testing.T) { + + tests := []struct { + name string + args []any + expected bool + }{ + { + name: "string ends with", + args: []any{ + "Hello, world!", + "Hello,", + }, + expected: true, + }, + { + name: "string does not end with", + args: []any{ + "Hello world!", + "Hello,", + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := StartsWith(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/string.go b/pkg/iac/scanners/azure/functions/string.go new file mode 100644 index 000000000000..278f09629f9d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/string.go @@ -0,0 +1,16 @@ +package functions + +import "fmt" + +func String(args ...any) any { + if len(args) != 1 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return fmt.Sprintf("%v", args[0]) + } + + return input +} diff --git a/pkg/iac/scanners/azure/functions/string_test.go b/pkg/iac/scanners/azure/functions/string_test.go new file mode 100644 index 000000000000..2e8e35bbde88 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/string_test.go @@ -0,0 +1,44 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_String(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "string from a string", + args: []any{ + "hello", + }, + expected: "hello", + }, + { + name: "string from a bool", + args: []any{ + false, + }, + expected: "false", + }, + { + name: "string from an int", + args: []any{ + 10, + }, + expected: "10", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := String(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/sub.go b/pkg/iac/scanners/azure/functions/sub.go new file mode 100644 index 000000000000..08751ff94656 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/sub.go @@ -0,0 +1,15 @@ +package functions + +func Sub(args ...any) any { + + if len(args) != 2 { + return nil + } + + if a, ok := args[0].(int); ok { + if b, ok := args[1].(int); ok { + return a - b + } + } + return nil +} diff --git a/pkg/iac/scanners/azure/functions/sub_test.go b/pkg/iac/scanners/azure/functions/sub_test.go new file mode 100644 index 000000000000..66bd0c403758 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/sub_test.go @@ -0,0 +1,43 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Sub(t *testing.T) { + tests := []struct { + name string + args []any + expected int + }{ + { + name: "subtract 2 from 5", + args: []any{5, 2}, + expected: 3, + }, + { + name: "subtract 2 from 1", + args: []any{1, 2}, + expected: -1, + }, + { + name: "subtract 3 from 2", + args: []any{2, 3}, + expected: -1, + }, + { + name: "subtract -4 from 3", + args: []any{3, -4}, + expected: 7, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Sub(tt.args...) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/substring.go b/pkg/iac/scanners/azure/functions/substring.go new file mode 100644 index 000000000000..502ba6ac5df9 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/substring.go @@ -0,0 +1,36 @@ +package functions + +func SubString(args ...any) any { + if len(args) < 2 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return "" + } + + start, ok := args[1].(int) + if !ok { + return "" + } + + if len(args) == 2 { + args = append(args, len(input)) + } + + length, ok := args[2].(int) + if !ok { + return "" + } + + if start > len(input) { + return "" + } + + if start+length > len(input) { + return input[start:] + } + + return input[start : start+length] +} diff --git a/pkg/iac/scanners/azure/functions/substring_test.go b/pkg/iac/scanners/azure/functions/substring_test.go new file mode 100644 index 000000000000..633e21b66996 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/substring_test.go @@ -0,0 +1,49 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_SubString(t *testing.T) { + + tests := []struct { + name string + args []any + expected string + }{ + { + name: "substring a string", + args: []any{ + "hello", + 1, + 3, + }, + expected: "ell", + }, + { + name: "substring a string with no upper bound", + args: []any{ + "hello", + 1, + }, + expected: "ello", + }, + { + name: "substring a string with start higher than the length", + args: []any{ + "hello", + 10, + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := SubString(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/take.go b/pkg/iac/scanners/azure/functions/take.go new file mode 100644 index 000000000000..9e43ed2ecb9d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/take.go @@ -0,0 +1,49 @@ +package functions + +func Take(args ...any) any { + if len(args) != 2 { + return "" + } + + count, ok := args[1].(int) + if !ok { + return "" + } + switch input := args[0].(type) { + case string: + if count > len(input) { + return input + } + return input[:count] + case any: + switch iType := input.(type) { + case []int: + if count > len(iType) { + return iType + } + return iType[:count] + case []string: + if count > len(iType) { + return iType + } + return iType[:count] + case []bool: + if count > len(iType) { + return iType + } + return iType[:count] + case []float64: + if count > len(iType) { + return iType + } + return iType[:count] + case []any: + if count > len(iType) { + return iType + } + return iType[:count] + } + } + + return "" +} diff --git a/pkg/iac/scanners/azure/functions/take_test.go b/pkg/iac/scanners/azure/functions/take_test.go new file mode 100644 index 000000000000..8334a1b7e121 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/take_test.go @@ -0,0 +1,63 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Take(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + { + name: "take a string", + args: []any{ + "hello", + 2, + }, + expected: "he", + }, + { + name: "take a string with invalid count", + args: []any{ + "hello", + 10, + }, + expected: "hello", + }, + { + name: "take a string from slice", + args: []any{ + []string{"a", "b", "c"}, + 2, + }, + expected: []string{"a", "b"}, + }, + { + name: "take a string from a slice", + args: []any{ + []string{"a", "b", "c"}, + 2, + }, + expected: []string{"a", "b"}, + }, + { + name: "take a string from a slice with invalid count", + args: []any{ + []string{"a", "b", "c"}, + 10, + }, + expected: []string{"a", "b", "c"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Take(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/trim.go b/pkg/iac/scanners/azure/functions/trim.go new file mode 100644 index 000000000000..76006154fc7b --- /dev/null +++ b/pkg/iac/scanners/azure/functions/trim.go @@ -0,0 +1,16 @@ +package functions + +import "strings" + +func Trim(args ...any) any { + if len(args) != 1 { + return "" + } + + input, ok := args[0].(string) + if !ok { + return "" + } + + return strings.TrimSpace(input) +} diff --git a/pkg/iac/scanners/azure/functions/trim_test.go b/pkg/iac/scanners/azure/functions/trim_test.go new file mode 100644 index 000000000000..b77e7b2e229d --- /dev/null +++ b/pkg/iac/scanners/azure/functions/trim_test.go @@ -0,0 +1,71 @@ +package functions + +import "testing" + +func Test_Trim(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "trim a string", + args: []any{ + " hello ", + }, + expected: "hello", + }, + { + name: "trim a string with multiple spaces", + args: []any{ + " hello ", + }, + expected: "hello", + }, + { + name: "trim a string with tabs", + args: []any{ + " hello ", + }, + expected: "hello", + }, + { + name: "trim a string with new lines", + args: []any{ + ` + +hello + +`, + }, + expected: "hello", + }, + { + name: "trim a string with tabs, spaces and new lines", + args: []any{ + ` + +hello + +`, + }, + expected: "hello", + }, + { + name: "trim a string with non string input", + args: []any{ + 10, + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Trim(tt.args...) + if actual != tt.expected { + t.Errorf("Trim(%v) = %v, expected %v", tt.args, actual, tt.expected) + } + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/true.go b/pkg/iac/scanners/azure/functions/true.go new file mode 100644 index 000000000000..cc15cd5a1443 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/true.go @@ -0,0 +1,5 @@ +package functions + +func True(_ ...any) any { + return true +} diff --git a/pkg/iac/scanners/azure/functions/union.go b/pkg/iac/scanners/azure/functions/union.go new file mode 100644 index 000000000000..ba3b295dce2b --- /dev/null +++ b/pkg/iac/scanners/azure/functions/union.go @@ -0,0 +1,59 @@ +package functions + +import ( + "maps" + "sort" +) + +func Union(args ...any) any { + if len(args) == 0 { + return []any{} + } + if len(args) == 1 { + return args[0] + } + + switch args[0].(type) { + case map[string]any: + return unionMap(args...) + case any: + return unionArray(args...) + } + + return []any{} + +} + +func unionMap(args ...any) any { + result := make(map[string]any) + + for _, arg := range args { + if iType, ok := arg.(map[string]any); ok { + maps.Copy(result, iType) + } + } + + return result +} + +func unionArray(args ...any) any { + var result []any + union := make(map[any]bool) + + for _, arg := range args { + if iType, ok := arg.([]any); ok { + for _, item := range iType { + union[item] = true + } + } + } + + for k := range union { + result = append(result, k) + } + sort.Slice(result, func(i, j int) bool { + return result[i].(string) < result[j].(string) + }) + + return result +} diff --git a/pkg/iac/scanners/azure/functions/union_test.go b/pkg/iac/scanners/azure/functions/union_test.go new file mode 100644 index 000000000000..6c030f7d8b8b --- /dev/null +++ b/pkg/iac/scanners/azure/functions/union_test.go @@ -0,0 +1,110 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Union(t *testing.T) { + tests := []struct { + name string + args []any + expected any + }{ + { + name: "union single array", + args: []any{ + []any{"a", "b", "c"}, + }, + expected: []any{"a", "b", "c"}, + }, + { + name: "union two arrays", + args: []any{ + []any{"a", "b", "c"}, + []any{"b", "c", "d"}, + }, + expected: []any{"a", "b", "c", "d"}, + }, + { + name: "union two arrays", + args: []any{ + []any{"a", "b", "c"}, + []any{"b", "c", "d"}, + []any{"b", "c", "d", "e"}, + }, + expected: []any{"a", "b", "c", "d", "e"}, + }, + { + name: "union single maps", + args: []any{ + map[string]any{ + "a": "a", + "b": "b", + "c": "c", + }, + }, + expected: map[string]any{ + "a": "a", + "b": "b", + "c": "c", + }, + }, + { + name: "union two maps", + args: []any{ + map[string]any{ + "a": "a", + "b": "b", + "c": "c", + }, + map[string]any{ + "b": "b", + "c": "c", + "d": "d", + }, + }, + expected: map[string]any{ + "a": "a", + "b": "b", + "c": "c", + "d": "d", + }, + }, + { + name: "union three maps", + args: []any{ + map[string]any{ + "a": "a", + "b": "b", + "c": "c", + }, + map[string]any{ + "b": "b", + "c": "c", + "d": "d", + }, + map[string]any{ + "b": "b", + "c": "c", + "e": "e", + }, + }, + expected: map[string]any{ + "a": "a", + "b": "b", + "c": "c", + "d": "d", + "e": "e", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Union(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/unique_string.go b/pkg/iac/scanners/azure/functions/unique_string.go new file mode 100644 index 000000000000..72b5fb5aabc6 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/unique_string.go @@ -0,0 +1,21 @@ +package functions + +import ( + "crypto/sha256" + "encoding/hex" + "strings" +) + +func UniqueString(args ...any) any { + if len(args) == 0 { + return "" + } + + hashParts := make([]string, len(args)) + for i, str := range args { + hashParts[i] = str.(string) + } + + hash := sha256.New().Sum([]byte(strings.Join(hashParts, ""))) + return hex.EncodeToString(hash)[:13] +} diff --git a/pkg/iac/scanners/azure/functions/unique_string_test.go b/pkg/iac/scanners/azure/functions/unique_string_test.go new file mode 100644 index 000000000000..1cbc3ba03476 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/unique_string_test.go @@ -0,0 +1,38 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_UniqueString(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "unique string from a string", + args: []any{ + "hello", + }, + expected: "68656c6c6fe3b", + }, + { + name: "unique string from a string", + args: []any{ + "hello", + "world", + }, + expected: "68656c6c6f776", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := UniqueString(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/functions/uri.go b/pkg/iac/scanners/azure/functions/uri.go new file mode 100644 index 000000000000..f3ff499740ba --- /dev/null +++ b/pkg/iac/scanners/azure/functions/uri.go @@ -0,0 +1,29 @@ +package functions + +import ( + "net/url" + "path" +) + +func Uri(args ...any) any { + if len(args) != 2 { + return "" + } + + result, err := joinPath(args[0].(string), args[1].(string)) + if err != nil { + return "" + } + return result +} + +// Backport url.JoinPath until we're ready for Go 1.19 +func joinPath(base string, elem ...string) (string, error) { + u, err := url.Parse(base) + if err != nil { + return "", err + } + elem = append([]string{u.EscapedPath()}, elem...) + u.Path = path.Join(elem...) + return u.String(), nil +} diff --git a/pkg/iac/scanners/azure/functions/uri_test.go b/pkg/iac/scanners/azure/functions/uri_test.go new file mode 100644 index 000000000000..0577093ce4eb --- /dev/null +++ b/pkg/iac/scanners/azure/functions/uri_test.go @@ -0,0 +1,48 @@ +package functions + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_Uri(t *testing.T) { + tests := []struct { + name string + args []any + expected string + }{ + { + name: "uri from a base and relative with no trailing slash", + args: []any{ + "http://contoso.org/firstpath", + "myscript.sh", + }, + expected: "http://contoso.org/firstpath/myscript.sh", + }, + { + name: "uri from a base and relative with trailing slash", + args: []any{ + "http://contoso.org/firstpath/", + "myscript.sh", + }, + expected: "http://contoso.org/firstpath/myscript.sh", + }, + { + name: "uri from a base with trailing slash and relative with ../", + args: []any{ + "http://contoso.org/firstpath/", + "../myscript.sh", + }, + expected: "http://contoso.org/myscript.sh", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := Uri(tt.args...) + require.Equal(t, tt.expected, actual) + }) + } + +} diff --git a/pkg/iac/scanners/azure/functions/utc_now.go b/pkg/iac/scanners/azure/functions/utc_now.go new file mode 100644 index 000000000000..dcb53d9d2740 --- /dev/null +++ b/pkg/iac/scanners/azure/functions/utc_now.go @@ -0,0 +1,47 @@ +package functions + +import ( + "strings" + "time" +) + +func UTCNow(args ...any) any { + if len(args) > 1 { + return nil + } + + if len(args) == 1 { + format, ok := args[0].(string) + if ok { + goFormat := convertFormat(format) + return time.Now().UTC().Format(goFormat) + } + } + + return time.Now().UTC().Format(time.RFC3339) +} + +// don't look directly at this code +func convertFormat(format string) string { + goFormat := format + goFormat = strings.ReplaceAll(goFormat, "yyyy", "2006") + goFormat = strings.ReplaceAll(goFormat, "yy", "06") + goFormat = strings.ReplaceAll(goFormat, "MMMM", "January") + goFormat = strings.ReplaceAll(goFormat, "MMM", "Jan") + goFormat = strings.ReplaceAll(goFormat, "MM", "01") + goFormat = strings.ReplaceAll(goFormat, "M", "1") + goFormat = strings.ReplaceAll(goFormat, "dd", "02") + goFormat = strings.ReplaceAll(goFormat, "d", "2") + goFormat = strings.ReplaceAll(goFormat, "HH", "15") + goFormat = strings.ReplaceAll(goFormat, "H", "3") + goFormat = strings.ReplaceAll(goFormat, "hh", "03") + goFormat = strings.ReplaceAll(goFormat, "h", "3") + goFormat = strings.ReplaceAll(goFormat, "mm", "04") + goFormat = strings.ReplaceAll(goFormat, "m", "4") + goFormat = strings.ReplaceAll(goFormat, "ss", "05") + goFormat = strings.ReplaceAll(goFormat, "s", "5") + goFormat = strings.ReplaceAll(goFormat, "tt", "PM") + goFormat = strings.ReplaceAll(goFormat, "t", "PM") + return goFormat + +} diff --git a/pkg/iac/scanners/azure/functions/utc_now_test.go b/pkg/iac/scanners/azure/functions/utc_now_test.go new file mode 100644 index 000000000000..94263af9f9aa --- /dev/null +++ b/pkg/iac/scanners/azure/functions/utc_now_test.go @@ -0,0 +1,41 @@ +package functions + +import ( + "fmt" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_UTCNow(t *testing.T) { + + tests := []struct { + name string + args []any + expected string + }{ + { + name: "utc now day", + args: []any{ + "d", + }, + expected: strconv.Itoa(time.Now().UTC().Day()), + }, + { + name: "utc now date", + args: []any{ + "yyyy-M-d", + }, + expected: fmt.Sprintf("%d-%d-%d", time.Now().UTC().Year(), time.Now().UTC().Month(), time.Now().UTC().Day()), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := UTCNow(tt.args...) + assert.Equal(t, tt.expected, actual) + }) + } +} diff --git a/pkg/iac/scanners/azure/resolver/resolver.go b/pkg/iac/scanners/azure/resolver/resolver.go new file mode 100644 index 000000000000..3794ed20ab64 --- /dev/null +++ b/pkg/iac/scanners/azure/resolver/resolver.go @@ -0,0 +1,51 @@ +package resolver + +import ( + azure2 "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure/expressions" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Resolver interface { + ResolveExpression(expression azure2.Value) azure2.Value + SetDeployment(d *azure2.Deployment) +} + +func NewResolver() Resolver { + return &resolver{} +} + +type resolver struct { + deployment *azure2.Deployment +} + +func (r *resolver) SetDeployment(d *azure2.Deployment) { + r.deployment = d +} + +func (r *resolver) ResolveExpression(expression azure2.Value) azure2.Value { + if expression.Kind != azure2.KindExpression { + return expression + } + if r.deployment == nil { + panic("cannot resolve expression on nil deployment") + } + code := expression.AsString() + + resolved, err := r.resolveExpressionString(code, expression.GetMetadata()) + if err != nil { + expression.Kind = azure2.KindUnresolvable + return expression + } + return resolved +} + +func (r *resolver) resolveExpressionString(code string, metadata iacTypes.Metadata) (azure2.Value, error) { + et, err := expressions.NewExpressionTree(code) + if err != nil { + return azure2.NullValue, err + } + + evaluatedValue := et.Evaluate(r.deployment) + return azure2.NewValue(evaluatedValue, metadata), nil +} diff --git a/pkg/iac/scanners/azure/resolver/resolver_test.go b/pkg/iac/scanners/azure/resolver/resolver_test.go new file mode 100644 index 000000000000..9b9502c06c08 --- /dev/null +++ b/pkg/iac/scanners/azure/resolver/resolver_test.go @@ -0,0 +1,102 @@ +package resolver + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + azure2 "github.com/aquasecurity/trivy/pkg/iac/scanners/azure" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_resolveFunc(t *testing.T) { + + tests := []struct { + name string + expr string + expected string + }{ + { + name: "simple format call", + expr: "format('{0}/{1}', 'myPostgreSQLServer', 'log_checkpoints')", + expected: "myPostgreSQLServer/log_checkpoints", + }, + { + name: "simple format call with numbers", + expr: "format('{0} + {1} = {2}', 1, 2, 3)", + expected: "1 + 2 = 3", + }, + { + name: "format with nested format", + expr: "format('{0} + {1} = {2}', format('{0}', 1), 2, 3)", + expected: "1 + 2 = 3", + }, + { + name: "format with multiple nested format", + expr: "format('{0} + {1} = {2}', format('{0}', 1), 2, format('{0}', 3))", + expected: "1 + 2 = 3", + }, + { + name: "format with nested base64", + expr: "format('the base64 of \"hello, world\" is {0}', base64('hello, world'))", + expected: "the base64 of \"hello, world\" is aGVsbG8sIHdvcmxk", + }, + { + name: "dateTimeAdd with add a day", + expr: "dateTimeAdd(utcNow('yyyy-MM-dd'), 'P1D', 'yyyy-MM-dd')", + expected: time.Now().UTC().AddDate(0, 0, 1).Format("2006-01-02"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolver := resolver{} + + resolvedValue, err := resolver.resolveExpressionString(tt.expr, types.NewTestMetadata()) + require.NoError(t, err) + require.Equal(t, azure2.KindString, resolvedValue.Kind) + + require.Equal(t, tt.expected, resolvedValue.AsString()) + }) + } +} + +func Test_resolveParameter(t *testing.T) { + tests := []struct { + name string + deployment *azure2.Deployment + expr string + expected string + }{ + { + name: "format call with parameter", + deployment: &azure2.Deployment{ + Parameters: []azure2.Parameter{ + { + Variable: azure2.Variable{ + Name: "dbName", + Value: azure2.NewValue("myPostgreSQLServer", types.NewTestMetadata()), + }, + }, + }, + }, + expr: "format('{0}/{1}', parameters('dbName'), 'log_checkpoints')", + expected: "myPostgreSQLServer/log_checkpoints", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolver := resolver{ + deployment: tt.deployment, + } + + resolvedValue, err := resolver.resolveExpressionString(tt.expr, types.NewTestMetadata()) + require.NoError(t, err) + require.Equal(t, azure2.KindString, resolvedValue.Kind) + + require.Equal(t, tt.expected, resolvedValue.AsString()) + }) + } + +} diff --git a/pkg/iac/scanners/azure/value.go b/pkg/iac/scanners/azure/value.go new file mode 100644 index 000000000000..898bc00d1aa1 --- /dev/null +++ b/pkg/iac/scanners/azure/value.go @@ -0,0 +1,335 @@ +package azure + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + "maps" + "strings" + "time" + + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/set" +) + +type EvalContext struct{} + +type Kind string + +const ( + KindUnresolvable Kind = "unresolvable" + KindNull Kind = "null" + KindBoolean Kind = "boolean" + KindString Kind = "string" + KindNumber Kind = "number" + KindObject Kind = "object" + KindArray Kind = "array" + KindExpression Kind = "expression" +) + +type Value struct { + metadata *types.Metadata + rLit any + rMap map[string]Value + rArr []Value + Kind Kind + Comments []string +} + +var NullValue = Value{ + Kind: KindNull, +} + +var boolTrueValues = set.NewCaseInsensitive("true", "1", "yes", "on", "enabled") + +func NewValue(value any, metadata types.Metadata) Value { + + v := Value{ + metadata: &metadata, + } + + switch ty := value.(type) { + case []any: + v.Kind = KindArray + for _, child := range ty { + if internal, ok := child.(Value); ok { + v.rArr = append(v.rArr, internal) + } else { + v.rArr = append(v.rArr, NewValue(child, metadata)) + } + } + case []Value: + v.Kind = KindArray + v.rArr = append(v.rArr, ty...) + + case map[string]any: + v.Kind = KindObject + v.rMap = make(map[string]Value) + for key, val := range ty { + if internal, ok := val.(Value); ok { + v.rMap[key] = internal + } else { + v.rMap[key] = NewValue(val, metadata) + } + } + case map[string]Value: + v.Kind = KindObject + v.rMap = make(map[string]Value) + maps.Copy(v.rMap, ty) + case string: + v.Kind = KindString + v.rLit = ty + case int, int64, int32, float32, float64, int8, int16, uint8, uint16, uint32, uint64: + v.Kind = KindNumber + v.rLit = ty + case bool: + v.Kind = KindBoolean + v.rLit = ty + case nil: + v.Kind = KindNull + v.rLit = ty + default: + v.Kind = KindUnresolvable + v.rLit = ty + } + + return v +} + +func NewExprValue(value string, metadata types.Metadata) Value { + return Value{ + metadata: &metadata, + Kind: KindExpression, + rLit: value, + } +} + +func (v Value) GetMetadata() types.Metadata { + return lo.FromPtr(v.metadata) +} + +func (v *Value) SetMetadata(m *types.Metadata) { + v.metadata = m +} + +func (v *Value) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + switch k := dec.PeekKind(); k { + case 't', 'f': + v.Kind = KindBoolean + if err := json.UnmarshalDecode(dec, &v.rLit); err != nil { + return err + } + case '"': + var s string + if err := json.UnmarshalDecode(dec, &s); err != nil { + return err + } + if strings.HasPrefix(s, "[") && !strings.HasPrefix(s, "[[") && strings.HasSuffix(s, "]") { + v.Kind = KindExpression + v.rLit = s[1 : len(s)-1] + } else { + v.Kind = KindString + v.rLit = s + } + case '0': + v.Kind = KindNumber + if err := json.UnmarshalDecode(dec, &v.rLit); err != nil { + return err + } + if f, ok := v.rLit.(float64); ok { + if i := int64(f); float64(i) == f { + v.rLit = i + } + } + case '[': + v.Kind = KindArray + if err := json.UnmarshalDecode(dec, &v.rArr); err != nil { + return err + } + case '{': + v.Kind = KindObject + if err := json.UnmarshalDecode(dec, &v.rMap); err != nil { + return err + } + case 'n': + // TODO: UnmarshalJSONFrom is called only for the root null + return dec.SkipValue() + case 0: + return dec.SkipValue() + default: + return fmt.Errorf("unexpected token kind %q at %d", k.String(), dec.InputOffset()) + } + return nil +} + +func (v Value) AsString() string { + v.Resolve() + + if v.Kind != KindString { + return "" + } + + return v.rLit.(string) +} + +func (v Value) AsBool() bool { + v.Resolve() + if v.Kind != KindBoolean { + return false + } + return v.rLit.(bool) +} + +func (v Value) AsInt() int { + v.Resolve() + if v.Kind != KindNumber { + return 0 + } + return int(v.rLit.(int64)) +} + +func (v Value) AsFloat() float64 { + v.Resolve() + if v.Kind != KindNumber { + return 0 + } + return v.rLit.(float64) +} + +func (v Value) AsIntValue(defaultValue int, metadata types.Metadata) types.IntValue { + v.Resolve() + if v.Kind != KindNumber { + return types.Int(defaultValue, metadata) + } + return types.Int(v.AsInt(), metadata) +} + +func (v Value) AsBoolValue(defaultValue bool, metadata types.Metadata) types.BoolValue { + v.Resolve() + if v.Kind == KindString { + if boolTrueValues.Contains(v.rLit.(string)) { + return types.Bool(true, metadata) + } + } + + if v.Kind != KindBoolean { + return types.Bool(defaultValue, metadata) + } + + return types.Bool(v.rLit.(bool), v.GetMetadata()) +} + +func (v Value) EqualTo(value any) bool { + switch ty := value.(type) { + case string: + return v.AsString() == ty + default: + panic("not supported") + } +} + +func (v Value) AsStringValue(defaultValue string, metadata types.Metadata) types.StringValue { + v.Resolve() + if v.Kind != KindString { + return types.StringDefault(defaultValue, metadata) + } + return types.String(v.rLit.(string), v.GetMetadata()) +} + +func (v Value) GetMapValue(key string) Value { + v.Resolve() + if v.Kind != KindObject { + return NullValue + } + v, exists := v.rMap[key] + if !exists { + return NullValue + } + return v +} + +func (v Value) AsMap() map[string]Value { + v.Resolve() + if v.Kind != KindObject { + return nil + } + return v.rMap +} + +func (v Value) AsList() []Value { + v.Resolve() + if v.Kind != KindArray { + return nil + } + return v.rArr +} + +func (v Value) Raw() any { + switch v.Kind { + case KindArray: + // TODO: recursively build raw array + return nil + case KindObject: + // TODO: recursively build raw object + return nil + default: + return v.rLit + } +} + +func (v *Value) Resolve() { + if v.Kind != KindExpression { + return + } + // if resolver, ok := v.Metadata.Internal().(Resolver); ok { + // *v = resolver.ResolveExpression(*v) + // } +} + +func (v Value) HasKey(key string) bool { + v.Resolve() + _, ok := v.rMap[key] + return ok +} + +func (v Value) AsTimeValue(parentMeta types.Metadata) types.TimeValue { + v.Resolve() + + switch v.Kind { + case KindString: + t, err := time.Parse(time.RFC3339, v.rLit.(string)) + if err != nil { + return types.Time(time.Time{}, parentMeta) + } + return types.Time(t, v.GetMetadata()) + case KindNumber: + var tv int64 + switch vv := v.rLit.(type) { + case float64: + tv = int64(vv) + case int64: + tv = vv + } + return types.Time(time.Unix(tv, 0), v.GetMetadata()) + default: + return types.Time(time.Time{}, parentMeta) + } +} + +func (v Value) AsStringValuesList(defaultValue string) (stringValues []types.StringValue) { + v.Resolve() + if v.Kind != KindArray { + return + } + for _, item := range v.rArr { + stringValues = append(stringValues, item.AsStringValue(defaultValue, item.GetMetadata())) + } + + return stringValues +} + +func (v Value) IsNull() bool { + return v.Kind == KindNull +} diff --git a/pkg/iac/scanners/azure/value_test.go b/pkg/iac/scanners/azure/value_test.go new file mode 100644 index 000000000000..cab71cbf2afb --- /dev/null +++ b/pkg/iac/scanners/azure/value_test.go @@ -0,0 +1,52 @@ +package azure + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_ValueAsInt(t *testing.T) { + val := NewValue(int64(10), types.NewTestMetadata()) + assert.Equal(t, 10, val.AsInt()) +} + +func Test_ValueAsTime(t *testing.T) { + tests := []struct { + name string + val any + expected time.Time + }{ + { + name: "string", + val: "2023-12-15T14:45:00Z", + expected: time.Date(2023, 12, 15, 14, 45, 0, 0, time.UTC), + }, + { + name: "int", + val: int64(200), + expected: time.Unix(200, 0), + }, + { + name: "float", + val: float64(100), + expected: time.Unix(100, 0), + }, + { + name: "invalid type", + val: nil, + expected: time.Time{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + val := NewValue(tt.val, types.NewTestMetadata()) + got := val.AsTimeValue(types.NewTestMetadata()).Value() + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/cloudformation/cftypes/types.go b/pkg/iac/scanners/cloudformation/cftypes/types.go new file mode 100644 index 000000000000..6a533aae179c --- /dev/null +++ b/pkg/iac/scanners/cloudformation/cftypes/types.go @@ -0,0 +1,39 @@ +package cftypes + +import "reflect" + +type CfType string + +const ( + String CfType = "string" + Int CfType = "int" + Float64 CfType = "float64" + Bool CfType = "bool" + Map CfType = "map" + List CfType = "list" + Unknown CfType = "unknown" +) + +func TypeFromGoValue(value any) CfType { + if value == nil { + return Unknown + } + switch reflect.TypeOf(value).Kind() { + case reflect.String: + return String + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return Int + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + return Int + case reflect.Float32, reflect.Float64: + return Float64 + case reflect.Bool: + return Bool + case reflect.Map: + return Map + case reflect.Slice: + return List + default: + return Unknown + } +} diff --git a/pkg/iac/scanners/cloudformation/parser/errors.go b/pkg/iac/scanners/cloudformation/parser/errors.go new file mode 100644 index 000000000000..655f137cd271 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/errors.go @@ -0,0 +1,24 @@ +package parser + +import ( + "fmt" +) + +type InvalidContentError struct { + source string + err error +} + +func NewErrInvalidContent(source string, err error) *InvalidContentError { + return &InvalidContentError{ + source: source, + err: err, + } +} +func (e *InvalidContentError) Error() string { + return fmt.Sprintf("Invalid content in file: %s. Error: %v", e.source, e.err) +} + +func (e *InvalidContentError) Reason() error { + return e.err +} diff --git a/pkg/iac/scanners/cloudformation/parser/file_context.go b/pkg/iac/scanners/cloudformation/parser/file_context.go new file mode 100644 index 000000000000..7b0bc8318395 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/file_context.go @@ -0,0 +1,255 @@ +package parser + +import ( + "fmt" + "maps" + "regexp" + "strings" + + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/iac/ignore" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type SourceFormat string + +const ( + YamlSourceFormat SourceFormat = "yaml" + JsonSourceFormat SourceFormat = "json" + + ForEachPrefix = "Fn::ForEach::" +) + +type FileContexts []*FileContext + +type FileContext struct { + filepath string + lines []string + SourceFormat SourceFormat + Ignores ignore.Rules + Parameters map[string]*Parameter `json:"Parameters" yaml:"Parameters"` + Resources map[string]*Resource `json:"Resources" yaml:"Resources"` + Globals map[string]*Resource `json:"Globals" yaml:"Globals"` + Mappings map[string]any `json:"Mappings,omitempty" yaml:"Mappings"` + Conditions map[string]Property `json:"Conditions,omitempty" yaml:"Conditions"` +} + +func (t *FileContext) GetResourceByLogicalID(name string) *Resource { + for n, r := range t.Resources { + if name == n { + return r + } + } + return nil +} + +func (t *FileContext) GetResourcesByType(names ...string) []*Resource { + var resources []*Resource + for _, r := range t.Resources { + for _, name := range names { + if name == r.Type() { + resources = append(resources, r) + } + } + } + return resources +} + +func (t *FileContext) Metadata() iacTypes.Metadata { + rng := iacTypes.NewRange(t.filepath, 1, len(t.lines), "", nil) + + return iacTypes.NewMetadata(rng, NewCFReference("Template", rng).String()) +} + +// TODO: use map[string]string +func (t *FileContext) overrideParameters(params map[string]any) { + for key := range t.Parameters { + if val, ok := params[key]; ok { + t.Parameters[key].UpdateDefault(val) + } + } +} + +func (t *FileContext) missingParameterValues() []string { + var missing []string + for key := range t.Parameters { + if t.Parameters[key].inner.Default == nil { + missing = append(missing, key) + } + } + return missing +} + +func (t *FileContext) stripNullProperties() { + for _, resource := range t.Resources { + resource.properties = lo.OmitBy(resource.properties, func(_ string, v *Property) bool { + return v.IsNil() + }) + } +} + +func (t *FileContext) expandTransforms() error { + resources := make(map[string]*Resource, len(t.Resources)) + + for name, r := range t.Resources { + if r.raw == nil { + resources[name] = r + continue + } + + instances, err := t.expandTransform(r.raw, name) + if err != nil { + return err + } + + for logicalID, rawProp := range instances { + instance, err := newExpandedResource(r, logicalID, rawProp) + if err != nil { + return err + } + resources[logicalID] = instance + } + } + + t.Resources = resources + return nil +} + +func newExpandedResource(base *Resource, logicalID string, raw *Property) (*Resource, error) { + rawMap := raw.AsMap() + typProp, ok := rawMap["Type"] + if !ok { + return nil, fmt.Errorf("missing 'Type' in expanded resource %q", logicalID) + } + propsProp, ok := rawMap["Properties"] + if !ok { + return nil, fmt.Errorf("missing 'Properties' in expanded resource %q", logicalID) + } + + instance := base.clone() + instance.typ = typProp.AsString() + instance.properties = propsProp.AsMap() + instance.setId(logicalID) + return instance, nil +} + +func (t *FileContext) expandTransform(prop *Property, logicalName string) (map[string]*Property, error) { + if strings.HasPrefix(logicalName, "Fn::ForEach::") { + return expandForEach(prop, nil) + } + + return nil, nil +} + +func expandForEach(prop *Property, parentCtx *LoopContext) (map[string]*Property, error) { + + args := prop.AsList() + if len(args) != 3 { + return nil, fmt.Errorf("invalid Fn::ForEach: expected 3 arguments, got %d", len(args)) + } + + identifier := args[0].AsString() + coll := args[1].AsList() + templ := args[2].AsMap() + + result := make(map[string]*Property) + + for _, el := range coll { + loopCtx := parentCtx.Child(identifier, el) + + for tmplKey, templValue := range templ { + cp := templValue.clone() + + // handle nested loop + if strings.HasPrefix(tmplKey, ForEachPrefix) { + nestedResult, err := expandForEach(cp, loopCtx) + if err != nil { + return nil, err + } + maps.Copy(result, nestedResult) + continue + } + + logicalID := resolveLoopPlaceholders(tmplKey, loopCtx) + cp.setLogicalResource(logicalID) + if err := expandProperties(cp, loopCtx); err != nil { + return nil, err + } + + result[logicalID] = cp + } + } + + return result, nil +} + +var placeholderRe = regexp.MustCompile(`[$&]\{([^}]+)\}`) + +func resolveLoopPlaceholders(v string, loopCtx *LoopContext) string { + return placeholderRe.ReplaceAllStringFunc(v, func(s string) string { + id := s[2 : len(s)-1] + val, found := loopCtx.Resolve(id) + if found { + return val.AsString() + } + return s + }) +} + +func expandProperties(prop *Property, parentCtx *LoopContext) error { + prop.loopCtx = parentCtx + + switch v := prop.Value.(type) { + case string: + prop.Value = resolveLoopPlaceholders(v, parentCtx) + case map[string]*Property: + newProps := make(map[string]*Property) + for k, el := range v { + if strings.HasPrefix(k, ForEachPrefix) { + expanded, err := expandForEach(el, parentCtx) + if err != nil { + return err + } + maps.Copy(newProps, expanded) + } else { + if err := expandProperties(el, parentCtx); err != nil { + return err + } + newProps[k] = el + } + } + prop.Value = newProps + case []*Property: + for _, el := range v { + if err := expandProperties(el, parentCtx); err != nil { + return err + } + } + } + return nil +} + +type LoopContext struct { + Identifier string + Value *Property + Parent *LoopContext +} + +func (c *LoopContext) Child(identifier string, value *Property) *LoopContext { + return &LoopContext{ + Identifier: identifier, + Value: value, + Parent: c, + } +} + +func (c *LoopContext) Resolve(name string) (*Property, bool) { + if c.Identifier == name { + return c.Value, true + } + if c.Parent != nil { + return c.Parent.Resolve(name) + } + return nil, false +} diff --git a/pkg/iac/scanners/cloudformation/parser/file_context_test.go b/pkg/iac/scanners/cloudformation/parser/file_context_test.go new file mode 100644 index 000000000000..80bdfa72eb05 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/file_context_test.go @@ -0,0 +1,105 @@ +package parser + +import ( + "slices" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFileContext_OverrideParameters(t *testing.T) { + tests := []struct { + name string + ctx FileContext + arg map[string]any + expected map[string]*Parameter + }{ + { + name: "happy", + ctx: FileContext{ + Parameters: map[string]*Parameter{ + "BucketName": { + inner: parameterInner{ + Type: "String", + Default: "test", + }, + }, + "QueueName": { + inner: parameterInner{ + Type: "String", + }, + }, + }, + }, + arg: map[string]any{ + "BucketName": "test2", + "QueueName": "test", + "SomeKey": "some_value", + }, + expected: map[string]*Parameter{ + "BucketName": { + inner: parameterInner{ + Type: "String", + Default: "test2", + }, + }, + "QueueName": { + inner: parameterInner{ + Type: "String", + Default: "test", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.ctx.overrideParameters(tt.arg) + assert.Equal(t, tt.expected, tt.ctx.Parameters) + }) + } +} + +func TestFileContext_MissingParameterValues(t *testing.T) { + + tests := []struct { + name string + ctx FileContext + expected []string + }{ + { + name: "happy", + ctx: FileContext{ + Parameters: map[string]*Parameter{ + "BucketName": { + inner: parameterInner{ + Type: "String", + Default: "test", + }, + }, + "QueueName": { + inner: parameterInner{ + Type: "String", + }, + }, + "KMSKey": { + inner: parameterInner{ + Type: "String", + Default: nil, + }, + }, + }, + }, + expected: []string{"KMSKey", "QueueName"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.ctx.missingParameterValues() + slices.Sort(got) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_and.go b/pkg/iac/scanners/cloudformation/parser/fn_and.go new file mode 100644 index 000000000000..0030d0761ae5 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_and.go @@ -0,0 +1,40 @@ +package parser + +import ( + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveAnd(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::And"].AsList() + + if len(refValue) < 2 { + return abortIntrinsic(property, "Fn::And should have at least 2 values, returning original Property") + } + + results := make([]bool, len(refValue)) + for i := range refValue { + + r := false + if refValue[i].IsBool() { + r = refValue[i].AsBool() + } + + results[i] = r + } + + theSame := allSameStrings(results) + return property.deriveResolved(cftypes.Bool, theSame), true +} + +func allSameStrings(a []bool) bool { + for i := 1; i < len(a); i++ { + if a[i] != a[0] { + return false + } + } + return true +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_and_test.go b/pkg/iac/scanners/cloudformation/parser/fn_and_test.go new file mode 100644 index 000000000000..a014583d5e1a --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_and_test.go @@ -0,0 +1,132 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_resolve_and_value(t *testing.T) { + + property1 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "foo", + }, + }, + }, + }, + } + + property2 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "foo", + }, + }, + }, + }, + } + andProperty := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::And": { + Type: cftypes.List, + Value: []*Property{ + property1, + property2, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(andProperty) + require.True(t, success) + + assert.True(t, resolvedProperty.IsTrue()) +} + +func Test_resolve_and_value_not_the_same(t *testing.T) { + + property1 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "bar", + }, + }, + }, + }, + } + + property2 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "foo", + }, + }, + }, + }, + } + andProperty := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::And": { + Type: cftypes.List, + Value: []*Property{ + property1, + property2, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(andProperty) + require.True(t, success) + + assert.False(t, resolvedProperty.IsTrue()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_base64.go b/pkg/iac/scanners/cloudformation/parser/fn_base64.go new file mode 100644 index 000000000000..ad94ed08d6e8 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_base64.go @@ -0,0 +1,19 @@ +package parser + +import ( + "encoding/base64" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveBase64(property *Property) (*Property, bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::Base64"].AsString() + + retVal := base64.StdEncoding.EncodeToString([]byte(refValue)) + + return property.deriveResolved(cftypes.String, retVal), true +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_base64_test.go b/pkg/iac/scanners/cloudformation/parser/fn_base64_test.go new file mode 100644 index 000000000000..71a2ba2bdfae --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_base64_test.go @@ -0,0 +1,29 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_resolve_base64_value(t *testing.T) { + + property := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Base64": { + Type: cftypes.String, + Value: "HelloWorld", + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + + assert.Equal(t, "SGVsbG9Xb3JsZA==", resolvedProperty.AsString()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_builtin.go b/pkg/iac/scanners/cloudformation/parser/fn_builtin.go new file mode 100644 index 000000000000..7e4101ec4aef --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_builtin.go @@ -0,0 +1,65 @@ +package parser + +import ( + "errors" + "net" + + "github.com/apparentlymart/go-cidr/cidr" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func GetAzs(property *Property) (*Property, bool) { + return property.deriveResolved(cftypes.List, []*Property{ + property.deriveResolved(cftypes.String, "us-east-1a"), + property.deriveResolved(cftypes.String, "us-east-1a"), + property.deriveResolved(cftypes.String, "us-east-1a"), + }), true +} + +func GetCidr(property *Property) (*Property, bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::Cidr"] + if refValue.IsNotList() || len(refValue.AsList()) != 3 { + return abortIntrinsic(property, "Fn::Cidr expects a list of 3 attributes") + } + + listParts := refValue.AsList() + ipaddressProp := listParts[0] + ipAddress := "10.0.0.0/2" + if ipaddressProp.IsString() { + ipAddress = ipaddressProp.AsString() + } + count := listParts[1].AsInt() + bit := listParts[2].AsInt() + + ranges, err := calculateCidrs(ipAddress, count, bit, property) + if err != nil { + return abortIntrinsic(property, "Could not calculate the required ranges") + } + return property.deriveResolved(cftypes.List, ranges), true +} + +func calculateCidrs(ipaddress string, count, bit int, original *Property) ([]*Property, error) { + + var cidrProperties []*Property + + _, network, err := net.ParseCIDR(ipaddress) + if err != nil { + return nil, err + } + + for i := range count { + next, err := cidr.Subnet(network, bit, i) + if err != nil { + return nil, errors.New("failed to create cidr blocks") + } + + cidrProperties = append(cidrProperties, original.deriveResolved(cftypes.String, next.String())) + } + + return cidrProperties, nil +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_builtin_test.go b/pkg/iac/scanners/cloudformation/parser/fn_builtin_test.go new file mode 100644 index 000000000000..bbfa1cbeb32b --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_builtin_test.go @@ -0,0 +1,59 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_cidr_generator(t *testing.T) { + + original := &Property{ + ctx: nil, + name: "cidr", + comment: "", + Type: "", + Value: nil, + } + + ranges, err := calculateCidrs("10.1.0.0/16", 4, 4, original) + require.NoError(t, err) + require.Len(t, ranges, 4) + + results := make(map[int]string) + for i, property := range ranges { + value := property.AsString() + results[i] = value + } + + assert.Equal(t, "10.1.0.0/20", results[0]) + assert.Equal(t, "10.1.16.0/20", results[1]) + assert.Equal(t, "10.1.32.0/20", results[2]) + assert.Equal(t, "10.1.48.0/20", results[3]) +} + +func Test_cidr_generator_8_bits(t *testing.T) { + original := &Property{ + ctx: nil, + name: "cidr", + comment: "", + Type: "", + Value: nil, + } + + ranges, err := calculateCidrs("10.1.0.0/16", 4, 8, original) + require.NoError(t, err) + require.Len(t, ranges, 4) + + results := make(map[int]string) + for i, property := range ranges { + value := property.AsString() + results[i] = value + } + + assert.Equal(t, "10.1.0.0/24", results[0]) + assert.Equal(t, "10.1.1.0/24", results[1]) + assert.Equal(t, "10.1.2.0/24", results[2]) + assert.Equal(t, "10.1.3.0/24", results[3]) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_condition.go b/pkg/iac/scanners/cloudformation/parser/fn_condition.go new file mode 100644 index 000000000000..8d5c923936ab --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_condition.go @@ -0,0 +1,21 @@ +package parser + +func ResolveCondition(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refProp := property.AsMap()["Condition"] + if refProp.IsNotString() { + return nil, false + } + refValue := refProp.AsString() + + for k, prop := range property.ctx.Conditions { + if k == refValue { + return prop.resolveValue() + } + } + + return nil, false +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_condition_test.go b/pkg/iac/scanners/cloudformation/parser/fn_condition_test.go new file mode 100644 index 000000000000..d4150dcee654 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_condition_test.go @@ -0,0 +1,77 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_resolve_condition_value(t *testing.T) { + + fctx := new(FileContext) + fctx.Conditions = map[string]Property{ + "SomeCondition": { + ctx: fctx, + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + ctx: fctx, + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "some val", + }, + { + Type: cftypes.String, + Value: "some val", + }, + }, + }, + }, + }, + "EnableVersioning": { + ctx: fctx, + Type: cftypes.Map, + Value: map[string]*Property{ + "Condition": { + Type: cftypes.String, + Value: "SomeCondition", + }, + }, + }, + } + + property := &Property{ + ctx: fctx, + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::If": { + ctx: fctx, + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "EnableVersioning", + }, + { + Type: cftypes.String, + Value: "Enabled", + }, + { + Type: cftypes.String, + Value: "Suspended", + }, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + + assert.Equal(t, "Enabled", resolvedProperty.AsString()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_equals.go b/pkg/iac/scanners/cloudformation/parser/fn_equals.go new file mode 100644 index 000000000000..4043735849a2 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_equals.go @@ -0,0 +1,21 @@ +package parser + +import ( + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveEquals(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::Equals"].AsList() + + if len(refValue) != 2 { + return abortIntrinsic(property, "Fn::Equals should have exactly 2 values, returning original Property") + } + + propA, _ := refValue[0].resolveValue() + propB, _ := refValue[1].resolveValue() + return property.deriveResolved(cftypes.Bool, propA.EqualTo(propB.RawValue())), true +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_equals_test.go b/pkg/iac/scanners/cloudformation/parser/fn_equals_test.go new file mode 100644 index 000000000000..a83dc049d7ec --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_equals_test.go @@ -0,0 +1,136 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_resolve_equals_value(t *testing.T) { + + property := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "foo", + }, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + + assert.True(t, resolvedProperty.IsTrue()) +} + +func Test_resolve_equals_value_to_false(t *testing.T) { + + property := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "bar", + }, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + + assert.False(t, resolvedProperty.IsTrue()) +} + +func Test_resolve_equals_value_to_true_when_boolean(t *testing.T) { + + property := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.Bool, + Value: true, + }, + { + Type: cftypes.Bool, + Value: true, + }, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + assert.True(t, resolvedProperty.IsTrue()) +} + +func Test_resolve_equals_value_when_one_is_a_reference(t *testing.T) { + + property := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "staging", + }, + { + ctx: &FileContext{ + Parameters: map[string]*Parameter{ + "Environment": { + inner: parameterInner{ + Type: "string", + Default: "staging", + }, + }, + }, + }, + Type: cftypes.Map, + Value: map[string]*Property{ + "Ref": { + Type: cftypes.String, + Value: "Environment", + }, + }, + }, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + + assert.True(t, resolvedProperty.IsTrue()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_find_in_map.go b/pkg/iac/scanners/cloudformation/parser/fn_find_in_map.go new file mode 100644 index 000000000000..69441bd138ce --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_find_in_map.go @@ -0,0 +1,80 @@ +package parser + +import ( + "fmt" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveFindInMap(property *Property) (*Property, bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::FindInMap"].AsList() + + if len(refValue) < 3 || len(refValue) > 4 { + return abortIntrinsic(property, "Fn::FindInMap expects 3 or 4 arguments") + } + + if property.ctx == nil { + return abortIntrinsic(property, "property context is missing") + } + + var defaultValue any + if len(refValue) == 4 { + if m := refValue[3].AsMap(); m != nil { + if defProp, exists := m["DefaultValue"]; exists && defProp != nil { + defaultValue = defProp.RawValue() + } + } + } + + mapName := refValue[0].AsString() + topKey := refValue[1].AsString() + secKey := refValue[2].AsString() + + value, err := resolveMapping(property.ctx, mapName, topKey, secKey) + if err != nil { + if defaultValue == nil { + return abortIntrinsic(property, err.Error()) + } + value = defaultValue + } + + switch v := value.(type) { + case string: + return property.deriveResolved(cftypes.String, v), true + case []any: + elems := make([]*Property, len(v)) + for i, el := range v { + elems[i] = property.deriveResolved(cftypes.String, el) + } + return property.deriveResolved(cftypes.List, elems), true + default: + return abortIntrinsic(property, fmt.Sprintf("unsupported type in mapping: %T", v)) + } +} + +func resolveMapping(ctx *FileContext, mapName, topKey, secKey string) (any, error) { + m, ok := ctx.Mappings[mapName] + if !ok { + return nil, fmt.Errorf("map %s not found", mapName) + } + mapContents, ok := m.(map[string]any) + if !ok { + return nil, fmt.Errorf("map %s has invalid type", mapName) + } + + k, ok := mapContents[topKey] + if !ok { + return nil, fmt.Errorf("key %s not found in map %s", topKey, mapName) + } + mapValues := k.(map[string]any) + + prop, ok := mapValues[secKey] + if !ok { + return nil, fmt.Errorf("key %s not found in %s", secKey, topKey) + } + return prop, nil +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_find_in_map_test.go b/pkg/iac/scanners/cloudformation/parser/fn_find_in_map_test.go new file mode 100644 index 000000000000..4bfa6c7330ee --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_find_in_map_test.go @@ -0,0 +1,95 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func Test_FindInMap(t *testing.T) { + source := `--- +Parameters: + Environment: + Type: String + Default: dev +Mappings: + CacheNodeTypes: + production: + NodeType: cache.t2.large + CacheSecurityGroupNames: [ "sg-1", "sg-2" ] + test: + NodeType: cache.t2.small + CacheSecurityGroupNames: [ "sg-3" ] + dev: + NodeType: cache.t2.micro + CacheSecurityGroupNames: [ "sg-4" ] +Resources: + ElasticacheCluster: + Type: 'AWS::ElastiCache::CacheCluster' + Properties: + Engine: memcached + CacheNodeType: !FindInMap [ CacheNodeTypes, !Ref Environment, NodeType ] + NumCacheNodes: '1' + + ElasticacheClusterWithDefault: + Type: 'AWS::ElastiCache::CacheCluster' + Properties: + Engine: memcached + CacheNodeType: !FindInMap [ CacheNodeTypes, staging, NodeType, DefaultValue: cache.t2.medium ] + NumCacheNodes: '1' + + ElasticacheClusterList: + Type: 'AWS::ElastiCache::CacheCluster' + Properties: + Engine: memcached + CacheSecurityGroupNames: !FindInMap [ CacheNodeTypes, production, CacheSecurityGroupNames ] + NumCacheNodes: '1' +` + + ctx := createTestFileContext(t, source) + require.NotNil(t, ctx) + + cluster := ctx.GetResourceByLogicalID("ElasticacheCluster") + require.NotNil(t, cluster) + nodeTypeProp := cluster.GetStringProperty("CacheNodeType", "") + assert.Equal(t, "cache.t2.micro", nodeTypeProp.Value()) + + clusterDefault := ctx.GetResourceByLogicalID("ElasticacheClusterWithDefault") + require.NotNil(t, clusterDefault) + nodeTypePropDefault := clusterDefault.GetStringProperty("CacheNodeType", "") + assert.Equal(t, "cache.t2.medium", nodeTypePropDefault.Value()) + + clusterList := ctx.GetResourceByLogicalID("ElasticacheClusterList") + require.NotNil(t, clusterList) + sgNamesProp := clusterList.GetProperty("CacheSecurityGroupNames").AsList() + groupNames := xslices.Map(sgNamesProp, func(prop *Property) any { + return prop.AsString() + }) + assert.ElementsMatch(t, []any{"sg-1", "sg-2"}, groupNames) +} + +func Test_InferType(t *testing.T) { + source := `--- +Mappings: + ApiDB: + MultiAZ: + development: False +Resources: + ApiDB: + Type: AWS::RDS::DBInstance + Properties: + MultiAZ: !FindInMap [ApiDB, MultiAZ, development] +` + + ctx := createTestFileContext(t, source) + require.NotNil(t, ctx) + + testRes := ctx.GetResourceByLogicalID("ApiDB") + require.NotNil(t, testRes) + + nodeTypeProp := testRes.GetBoolProperty("MultiAZ") + assert.False(t, nodeTypeProp.Value()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_get_attr.go b/pkg/iac/scanners/cloudformation/parser/fn_get_attr.go new file mode 100644 index 000000000000..d3deb3fd49c9 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_get_attr.go @@ -0,0 +1,46 @@ +package parser + +import ( + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveGetAtt(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValueProp := property.AsMap()["Fn::GetAtt"] + + var refValue []string + + if refValueProp.IsString() { + refValue = strings.Split(refValueProp.AsString(), ".") + } + + if refValueProp.IsList() { + for _, p := range refValueProp.AsList() { + refValue = append(refValue, p.AsString()) + } + } + + if len(refValue) != 2 { + return abortIntrinsic(property, "Fn::GetAtt should have exactly 2 values, returning original Property") + } + + logicalId := refValue[0] + attribute := refValue[1] + + referencedResource := property.ctx.GetResourceByLogicalID(logicalId) + if referencedResource == nil || referencedResource.IsNil() { + return property.deriveResolved(cftypes.String, ""), true + } + + referencedProperty := referencedResource.GetProperty(attribute) + if referencedProperty.IsNil() { + return property.deriveResolved(cftypes.String, referencedResource.ID()), true + } + + return property.deriveResolved(referencedProperty.Type, referencedProperty.RawValue()), true +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_get_attr_test.go b/pkg/iac/scanners/cloudformation/parser/fn_get_attr_test.go new file mode 100644 index 000000000000..83e059c5c0f9 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_get_attr_test.go @@ -0,0 +1,50 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_resolve_get_attr_value(t *testing.T) { + + source := `--- +Resources: + ElasticacheSecurityGroup: + Type: 'AWS::EC2::SecurityGroup' + Properties: + GroupDescription: Elasticache Security Group + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 11211 + ToPort: 11211 + SourceSecurityGroupName: !Ref InstanceSecurityGroup + ElasticacheCluster: + Type: 'AWS::ElastiCache::CacheCluster' + Properties: + Engine: memcached + CacheNodeType: cache.t2.micro + NumCacheNodes: '1' + VpcSecurityGroupIds: + - !GetAtt + - ElasticacheSecurityGroup + - GroupId +` + ctx := createTestFileContext(t, source) + require.NotNil(t, ctx) + + testRes := ctx.GetResourceByLogicalID("ElasticacheCluster") + assert.NotNil(t, testRes) + + sgProp := testRes.GetProperty("VpcSecurityGroupIds") + require.True(t, sgProp.IsNotNil()) + require.True(t, sgProp.IsList()) + + for _, property := range sgProp.AsList() { + resolved, success := ResolveIntrinsicFunc(property) + require.True(t, success) + assert.True(t, resolved.IsNotNil()) + } + +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_if.go b/pkg/iac/scanners/cloudformation/parser/fn_if.go new file mode 100644 index 000000000000..97becbc90e21 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_if.go @@ -0,0 +1,39 @@ +package parser + +func ResolveIf(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::If"].AsList() + + if len(refValue) != 3 { + return abortIntrinsic(property, "Fn::If should have exactly 3 values, returning original Property") + } + + condition, _ := refValue[0].resolveValue() + trueState, _ := refValue[1].resolveValue() + falseState, _ := refValue[2].resolveValue() + + conditionMet := false + + con, _ := condition.resolveValue() + if con.IsBool() { + conditionMet = con.AsBool() + } else if property.ctx.Conditions != nil && + condition.IsString() { + + condition := property.ctx.Conditions[condition.AsString()] + if condition.isFunction() { + con, _ := condition.resolveValue() + if con.IsBool() { + conditionMet = con.AsBool() + } + } + } + + if conditionMet { + return trueState, true + } + return falseState, true +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_if_test.go b/pkg/iac/scanners/cloudformation/parser/fn_if_test.go new file mode 100644 index 000000000000..f4043adcb597 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_if_test.go @@ -0,0 +1,42 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_resolve_if_value(t *testing.T) { + + property := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::If": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.Bool, + Value: true, + }, + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "bar", + }, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + + assert.Equal(t, "foo", resolvedProperty.String()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_join.go b/pkg/iac/scanners/cloudformation/parser/fn_join.go new file mode 100644 index 000000000000..e1d39dc702f7 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_join.go @@ -0,0 +1,34 @@ +package parser + +import ( + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveJoin(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::Join"].AsList() + + if len(refValue) != 2 { + return abortIntrinsic(property, "Fn::Join should have exactly 2 values, returning original Property") + } + + joiner := refValue[0].AsString() + items := refValue[1].AsList() + + var itemValues []string + for _, item := range items { + resolved, success := item.resolveValue() + if success { + itemValues = append(itemValues, resolved.AsString()) + } + } + + joined := strings.Join(itemValues, joiner) + + return property.deriveResolved(cftypes.String, joined), true +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_join_test.go b/pkg/iac/scanners/cloudformation/parser/fn_join_test.go new file mode 100644 index 000000000000..74df00f4f64e --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_join_test.go @@ -0,0 +1,115 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_resolve_join_value(t *testing.T) { + + property := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Join": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "::", + }, + { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "s3", + }, + { + Type: cftypes.String, + Value: "part1", + }, + { + Type: cftypes.String, + Value: "part2", + }, + }, + }, + }, + }, + }, + } + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + + assert.Equal(t, "s3::part1::part2", resolvedProperty.AsString()) +} + +func Test_resolve_join_value_with_reference(t *testing.T) { + + property := &Property{ + ctx: &FileContext{ + Parameters: map[string]*Parameter{ + "Environment": { + inner: parameterInner{ + Type: "string", + Default: "staging", + }, + }, + }, + }, + name: "EnvironmentBucket", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Join": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "::", + }, + { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "s3", + }, + { + Type: cftypes.String, + Value: "part1", + }, + { + ctx: &FileContext{ + Parameters: map[string]*Parameter{ + "Environment": { + inner: parameterInner{ + Type: "string", + Default: "staging", + }, + }, + }, + }, + Type: cftypes.Map, + Value: map[string]*Property{ + "Ref": { + Type: cftypes.String, + Value: "Environment", + }, + }, + }, + }, + }, + }, + }, + }, + } + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + + assert.Equal(t, "s3::part1::staging", resolvedProperty.AsString()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_length.go b/pkg/iac/scanners/cloudformation/parser/fn_length.go new file mode 100644 index 000000000000..2026dd4170e9 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_length.go @@ -0,0 +1,26 @@ +package parser + +import ( + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveLength(property *Property) (*Property, bool) { + if !property.isFunction() { + return property, true + } + + val := property.AsMap()["Fn::Length"] + if val.IsList() { + return property.deriveResolved(cftypes.Int, val.Len()), true + } else if val.IsMap() { + resolved, _ := val.resolveValue() + + if resolved.IsList() { + return property.deriveResolved(cftypes.Int, resolved.Len()), true + } + return resolved, false + } + + return property, false + +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_length_test.go b/pkg/iac/scanners/cloudformation/parser/fn_length_test.go new file mode 100644 index 000000000000..252a2026ffb7 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_length_test.go @@ -0,0 +1,80 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_ResolveLength_WhenPropIsArray(t *testing.T) { + prop := &Property{ + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Length": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.Int, + Value: 1, + }, + { + Type: cftypes.String, + Value: "IntParameter", + }, + }, + }, + }, + } + resolved, ok := ResolveIntrinsicFunc(prop) + require.True(t, ok) + require.True(t, resolved.IsInt()) + require.Equal(t, 2, resolved.AsInt()) +} + +func Test_ResolveLength_WhenPropIsIntrinsicFunction(t *testing.T) { + fctx := &FileContext{ + Parameters: map[string]*Parameter{ + "SomeParameter": { + inner: parameterInner{ + Type: "string", + Default: "a|b|c|d", + }, + }, + }, + } + prop := &Property{ + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Length": { + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Split": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "|", + }, + { + ctx: fctx, + Type: cftypes.Map, + Value: map[string]*Property{ + "Ref": { + Type: cftypes.String, + Value: "SomeParameter", + }, + }, + }, + }, + }, + }, + }, + }, + } + resolved, ok := ResolveIntrinsicFunc(prop) + require.True(t, ok) + require.True(t, resolved.IsInt()) + require.Equal(t, 4, resolved.AsInt()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_not.go b/pkg/iac/scanners/cloudformation/parser/fn_not.go new file mode 100644 index 000000000000..fa76db76319b --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_not.go @@ -0,0 +1,25 @@ +package parser + +import ( + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveNot(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::Not"].AsList() + + if len(refValue) != 1 { + return abortIntrinsic(property, "Fn::No should have at only 1 values, returning original Property") + } + + funcToInvert, _ := refValue[0].resolveValue() + + if funcToInvert.IsBool() { + return property.deriveResolved(cftypes.Bool, !funcToInvert.AsBool()), true + } + + return property, false +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_not_test.go b/pkg/iac/scanners/cloudformation/parser/fn_not_test.go new file mode 100644 index 000000000000..88c673bfdec0 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_not_test.go @@ -0,0 +1,90 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_resolve_not_value(t *testing.T) { + property1 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "bar", + }, + }, + }, + }, + } + + notProperty := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Not": { + Type: cftypes.List, + Value: []*Property{ + property1, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(notProperty) + require.True(t, success) + + assert.True(t, resolvedProperty.IsTrue()) +} + +func Test_resolve_not_value_when_true(t *testing.T) { + property1 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "foo", + }, + }, + }, + }, + } + + notProperty := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Not": { + Type: cftypes.List, + Value: []*Property{ + property1, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(notProperty) + require.True(t, success) + + assert.False(t, resolvedProperty.IsTrue()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_or.go b/pkg/iac/scanners/cloudformation/parser/fn_or.go new file mode 100644 index 000000000000..da13774e7113 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_or.go @@ -0,0 +1,41 @@ +package parser + +import ( + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveOr(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::Or"].AsList() + + if len(refValue) < 2 { + return abortIntrinsic(property, "Fn::Or should have at least 2 values, returning original Property") + } + + results := make([]bool, len(refValue)) + for i := range refValue { + + r := false + if refValue[i].IsBool() { + r = refValue[i].AsBool() + } + + results[i] = r + } + + atleastOne := atleastOne(results) + return property.deriveResolved(cftypes.Bool, atleastOne), true +} + +func atleastOne(a []bool) bool { + for _, b := range a { + if b { + return true + } + } + + return false +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_or_test.go b/pkg/iac/scanners/cloudformation/parser/fn_or_test.go new file mode 100644 index 000000000000..69ede90c5f08 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_or_test.go @@ -0,0 +1,130 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_resolve_or_value(t *testing.T) { + property1 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "bar", + }, + }, + }, + }, + } + + property2 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "foo", + }, + }, + }, + }, + } + orProperty := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Or": { + Type: cftypes.List, + Value: []*Property{ + property1, + property2, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(orProperty) + require.True(t, success) + + assert.True(t, resolvedProperty.IsTrue()) +} + +func Test_resolve_or_value_when_neither_true(t *testing.T) { + property1 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "bar", + }, + }, + }, + }, + } + + property2 := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "bar", + }, + { + Type: cftypes.String, + Value: "foo", + }, + }, + }, + }, + } + orProperty := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Or": { + Type: cftypes.List, + Value: []*Property{ + property1, + property2, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(orProperty) + require.True(t, success) + + assert.False(t, resolvedProperty.IsTrue()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_ref.go b/pkg/iac/scanners/cloudformation/parser/fn_ref.go new file mode 100644 index 000000000000..8357d4a35b13 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_ref.go @@ -0,0 +1,55 @@ +package parser + +import ( + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveReference(property *Property) (*Property, bool) { + if !property.isFunction() { + return property, true + } + + refProp := property.AsMap()["Ref"] + if refProp.IsNotString() { + return property, false + } + refValue := refProp.AsString() + + if pseudo, ok := pseudoParameters[refValue]; ok { + return property.deriveResolved(pseudo.t, pseudo.val), true + } + + if property.loopCtx != nil { + v, found := property.loopCtx.Resolve(refValue) + if found { + return property.deriveResolved(v.Type, v.RawValue()), true + } + } + + if property.ctx == nil { + return property, false + } + + if param, exists := property.ctx.Parameters[refValue]; exists { + resolvedType := param.Type() + + switch param.Default().(type) { + case bool: + resolvedType = cftypes.Bool + case string: + resolvedType = cftypes.String + case int: + resolvedType = cftypes.Int + } + + resolved := property.deriveResolved(resolvedType, param.Default()) + return resolved, true + } + + if res, exists := property.ctx.Resources[refValue]; exists { + resolved := property.deriveResolved(cftypes.String, res.ID()) + return resolved, true + } + + return nil, false +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_ref_test.go b/pkg/iac/scanners/cloudformation/parser/fn_ref_test.go new file mode 100644 index 000000000000..7eca1418fb77 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_ref_test.go @@ -0,0 +1,77 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_resolve_referenced_value(t *testing.T) { + + property := &Property{ + ctx: &FileContext{ + Parameters: map[string]*Parameter{ + "BucketName": { + inner: parameterInner{ + Type: "string", + Default: "someBucketName", + }, + }, + }, + }, + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Ref": { + Type: cftypes.String, + Value: "BucketName", + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + + assert.Equal(t, "someBucketName", resolvedProperty.AsString()) +} + +func Test_property_value_correct_when_not_reference(t *testing.T) { + + property := &Property{ + name: "BucketName", + rng: types.NewRange("testfile", 1, 1, "", nil), + Type: cftypes.String, + Value: "someBucketName", + } + + // should fail when trying to resolve function that is not in fact a function + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.False(t, success) + + assert.Equal(t, "someBucketName", resolvedProperty.AsString()) +} + +func Test_resolve_ref_with_pseudo_value(t *testing.T) { + source := `--- +Resources: + TestInstance: + Type: AWS::EC2::Instance + Properties: + ImageId: "ami-79fd7eee" + KeyName: !Join [":", ["aws", !Ref AWS::Region, "key" ]] +` + ctx := createTestFileContext(t, source) + require.NotNil(t, ctx) + + testRes := ctx.GetResourceByLogicalID("TestInstance") + require.NotNil(t, testRes) + + keyNameProp := testRes.GetProperty("KeyName") + require.NotNil(t, keyNameProp) + + assert.Equal(t, "aws:eu-west-1:key", keyNameProp.AsString()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_select.go b/pkg/iac/scanners/cloudformation/parser/fn_select.go new file mode 100644 index 000000000000..195db83802c6 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_select.go @@ -0,0 +1,39 @@ +package parser + +import ( + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveSelect(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::Select"].AsList() + + if len(refValue) != 2 { + return abortIntrinsic(property, "Fn::Select should have exactly 2 values, returning original Property") + } + + index := refValue[0] + list := refValue[1] + + if index.IsNotInt() { + if !index.IsConvertableTo(cftypes.Int) { + return abortIntrinsic(property, "index on property [%s] should be an int, returning original Property", property.name) + } + index = index.ConvertTo(cftypes.Int) + } + + if list.IsNotList() { + return abortIntrinsic(property, "list on property [%s] should be a list, returning original Property", property.name) + } + + listItems := list.AsList() + + if len(listItems) <= index.AsInt() { + return nil, false + } + + return listItems[index.AsInt()], true +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_select_test.go b/pkg/iac/scanners/cloudformation/parser/fn_select_test.go new file mode 100644 index 000000000000..92b634457b2d --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_select_test.go @@ -0,0 +1,77 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_resolve_select_value(t *testing.T) { + + source := `--- +Parameters: + EngineIndex: + Type: Integer + Default: 1 +Resources: + ElasticacheCluster: + Type: 'AWS::ElastiCache::CacheCluster' + Properties: + Engine: !Select [ !Ref EngineIndex, [memcached, redis ]] + CacheNodeType: cache.t2.micro + NumCacheNodes: '1' +` + ctx := createTestFileContext(t, source) + require.NotNil(t, ctx) + + testRes := ctx.GetResourceByLogicalID("ElasticacheCluster") + assert.NotNil(t, testRes) + + engineProp := testRes.GetProperty("Engine") + require.True(t, engineProp.IsNotNil()) + require.True(t, engineProp.IsString()) + + require.Equal(t, "redis", engineProp.AsString()) +} + +func Test_SelectPseudoListParam(t *testing.T) { + src := `--- +Resources: + myASGrpOne: + Type: AWS::AutoScaling::AutoScalingGroup + Version: "2009-05-15" + Properties: + AvailabilityZones: + - "us-east-1a" + LaunchConfigurationName: + Ref: MyLaunchConfiguration + MinSize: "0" + MaxSize: "0" + NotificationConfigurations: + - TopicARN: + Fn::Select: + - "1" + - Ref: AWS::NotificationARNs + NotificationTypes: + - autoscaling:EC2_INSTANCE_LAUNCH + - autoscaling:EC2_INSTANCE_LAUNCH_ERROR + +` + + ctx := createTestFileContext(t, src) + require.NotNil(t, ctx) + + resource := ctx.GetResourceByLogicalID("myASGrpOne") + require.NotNil(t, resource) + + notification := resource.GetProperty("NotificationConfigurations") + require.True(t, notification.IsNotNil()) + require.True(t, notification.IsList()) + first := notification.AsList()[0] + require.True(t, first.IsMap()) + topic, ok := first.AsMap()["TopicARN"] + require.True(t, ok) + require.Equal(t, "notification::arn::2", topic.AsString()) + +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_split.go b/pkg/iac/scanners/cloudformation/parser/fn_split.go new file mode 100644 index 000000000000..6e534b542fb9 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_split.go @@ -0,0 +1,42 @@ +package parser + +import ( + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveSplit(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::Split"].AsList() + + if len(refValue) != 2 { + return abortIntrinsic(property, "Fn::Split should have exactly 2 values, returning original Property") + } + + delimiterProp := refValue[0] + splitProp := refValue[1] + + if !splitProp.IsString() || !delimiterProp.IsString() { + abortIntrinsic(property, "Fn::Split requires two strings as input, returning original Property") + + } + + propertyList := createPropertyList(splitProp, delimiterProp, property) + + return property.deriveResolved(cftypes.List, propertyList), true +} + +func createPropertyList(splitProp, delimiterProp, parent *Property) []*Property { + splitString := splitProp.AsString() + delimiter := delimiterProp.AsString() + + var props []*Property + for split := range strings.SplitSeq(splitString, delimiter) { + props = append(props, parent.deriveResolved(cftypes.String, split)) + } + return props +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_split_test.go b/pkg/iac/scanners/cloudformation/parser/fn_split_test.go new file mode 100644 index 000000000000..2cc79c8fbf27 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_split_test.go @@ -0,0 +1,46 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +/* + Fn::Split: ["::", "s3::bucket::to::split"] + +*/ + +func Test_resolve_split_value(t *testing.T) { + + property := &Property{ + name: "BucketName", + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Split": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "::", + }, + { + Type: cftypes.String, + Value: "s3::bucket::to::split", + }, + }, + }, + }, + } + + resolvedProperty, success := ResolveIntrinsicFunc(property) + require.True(t, success) + assert.True(t, resolvedProperty.IsNotNil()) + assert.True(t, resolvedProperty.IsList()) + listContents := resolvedProperty.AsList() + assert.Len(t, listContents, 4) + +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_sub.go b/pkg/iac/scanners/cloudformation/parser/fn_sub.go new file mode 100644 index 000000000000..8664f406f285 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_sub.go @@ -0,0 +1,71 @@ +package parser + +import ( + "fmt" + "strconv" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func ResolveSub(property *Property) (resolved *Property, success bool) { + if !property.isFunction() { + return property, true + } + + refValue := property.AsMap()["Fn::Sub"] + + if refValue.IsString() { + return resolveStringSub(refValue, property), true + } + + if refValue.IsList() { + return resolveMapSub(refValue, property) + } + + return property, false +} + +func resolveMapSub(refValue, original *Property) (*Property, bool) { + refValues := refValue.AsList() + if len(refValues) != 2 { + return abortIntrinsic(original, "Fn::Sub with list expects 2 values, returning original property") + } + + workingString := refValues[0].AsString() + components := refValues[1].AsMap() + + for k, v := range components { + replacement := "[failed to resolve]" + switch v.Type { + case cftypes.Map: + resolved, _ := ResolveIntrinsicFunc(v) + replacement = resolved.AsString() + case cftypes.String: + replacement = v.AsString() + case cftypes.Int: + replacement = strconv.Itoa(v.AsInt()) + case cftypes.Bool: + replacement = strconv.FormatBool(v.AsBool()) + case cftypes.List: + var parts []string + for _, p := range v.AsList() { + parts = append(parts, p.String()) + } + replacement = fmt.Sprintf("[%s]", strings.Join(parts, ", ")) + } + workingString = strings.ReplaceAll(workingString, fmt.Sprintf("${%s}", k), replacement) + } + + return original.deriveResolved(cftypes.String, workingString), true +} + +func resolveStringSub(refValue, original *Property) *Property { + workingString := refValue.AsString() + + for k, param := range pseudoParameters { + workingString = strings.ReplaceAll(workingString, fmt.Sprintf("${%s}", k), fmt.Sprintf("%v", param.getRawValue())) + } + + return original.deriveResolved(cftypes.String, workingString) +} diff --git a/pkg/iac/scanners/cloudformation/parser/fn_sub_test.go b/pkg/iac/scanners/cloudformation/parser/fn_sub_test.go new file mode 100644 index 000000000000..5ab98a59692b --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/fn_sub_test.go @@ -0,0 +1,103 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_resolve_sub_value(t *testing.T) { + source := `--- +Resources: + TestInstance: + Type: AWS::EC2::Instance + Properties: + ImageId: "ami-79fd7eee" + KeyName: "testkey" + UserData: + !Sub | + #!/bin/bash -xe + yum update -y aws-cfn-bootstrap + /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchConfig --configsets wordpress_install --region ${AWS::Region} + /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerGroup --region ${AWS::Region} +` + ctx := createTestFileContext(t, source) + require.NotNil(t, ctx) + + testRes := ctx.GetResourceByLogicalID("TestInstance") + require.NotNil(t, testRes) + + userDataProp := testRes.GetProperty("UserData") + require.NotNil(t, userDataProp) + + assert.Equal(t, "#!/bin/bash -xe\nyum update -y aws-cfn-bootstrap\n/opt/aws/bin/cfn-init -v --stack cfsec-test-stack --resource LaunchConfig --configsets wordpress_install --region eu-west-1\n/opt/aws/bin/cfn-signal -e $? --stack cfsec-test-stack --resource WebServerGroup --region eu-west-1\n", userDataProp.AsString()) +} + +func Test_resolve_sub_value_with_base64(t *testing.T) { + + source := `--- +Resources: + TestInstance: + Type: AWS::EC2::Instance + Properties: + ImageId: "ami-79fd7eee" + KeyName: "testkey" + UserData: + Fn::Base64: + !Sub | + #!/bin/bash -xe + yum update -y aws-cfn-bootstrap + /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchConfig --configsets wordpress_install --region ${AWS::Region} + /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerGroup --region ${AWS::Region}` + ctx := createTestFileContext(t, source) + require.NotNil(t, ctx) + + testRes := ctx.GetResourceByLogicalID("TestInstance") + require.NotNil(t, testRes) + + userDataProp := testRes.GetProperty("UserData") + require.NotNil(t, userDataProp) + + assert.Equal(t, "IyEvYmluL2Jhc2ggLXhlCnl1bSB1cGRhdGUgLXkgYXdzLWNmbi1ib290c3RyYXAKL29wdC9hd3MvYmluL2Nmbi1pbml0IC12IC0tc3RhY2sgY2ZzZWMtdGVzdC1zdGFjayAtLXJlc291cmNlIExhdW5jaENvbmZpZyAtLWNvbmZpZ3NldHMgd29yZHByZXNzX2luc3RhbGwgLS1yZWdpb24gZXUtd2VzdC0xCi9vcHQvYXdzL2Jpbi9jZm4tc2lnbmFsIC1lICQ/IC0tc3RhY2sgY2ZzZWMtdGVzdC1zdGFjayAtLXJlc291cmNlIFdlYlNlcnZlckdyb3VwIC0tcmVnaW9uIGV1LXdlc3QtMQ==", userDataProp.AsString()) +} + +func Test_resolve_sub_value_with_map(t *testing.T) { + + source := `--- +Parameters: + RootDomainName: + Type: String + Default: somedomain.com +Resources: + TestDistribution: + Type: AWS::CloudFront::Distribution + Properties: + DistributionConfig: + DefaultCacheBehavior: + TargetOriginId: target + ViewerProtocolPolicy: https-only + Enabled: true + Origins: + - DomainName: + !Sub + - www.${Domain} + - { Domain: !Ref RootDomainName } + Id: somedomain1 + + +` + ctx := createTestFileContext(t, source) + require.NotNil(t, ctx) + + testRes := ctx.GetResourceByLogicalID("TestDistribution") + require.NotNil(t, testRes) + + originsList := testRes.GetProperty("DistributionConfig.Origins") + + domainNameProp := originsList.AsList()[0].GetProperty("DomainName") + require.NotNil(t, domainNameProp) + + assert.Equal(t, "www.somedomain.com", domainNameProp.AsString()) + +} diff --git a/pkg/iac/scanners/cloudformation/parser/intrinsics.go b/pkg/iac/scanners/cloudformation/parser/intrinsics.go new file mode 100644 index 000000000000..cda2bde29e41 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/intrinsics.go @@ -0,0 +1,106 @@ +package parser + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +var intrinsicFuncs map[string]func(property *Property) (*Property, bool) + +func init() { + intrinsicFuncs = map[string]func(property *Property) (*Property, bool){ + "Ref": ResolveReference, + "Fn::Base64": ResolveBase64, + "Fn::Equals": ResolveEquals, + "Fn::Join": ResolveJoin, + "Fn::Split": ResolveSplit, + "Fn::Sub": ResolveSub, + "Fn::FindInMap": ResolveFindInMap, + "Fn::Select": ResolveSelect, + "Fn::GetAtt": ResolveGetAtt, + "Fn::GetAZs": GetAzs, + "Fn::Cidr": GetCidr, + "Fn::ImportValue": ImportPlaceholder, + "Fn::If": ResolveIf, + "Fn::And": ResolveAnd, + "Fn::Or": ResolveOr, + "Fn::Not": ResolveNot, + "Fn::Length": ResolveLength, + "Condition": ResolveCondition, + } +} + +func ImportPlaceholder(property *Property) (*Property, bool) { + property.unresolved = true + return property, false +} + +func PassthroughResolution(property *Property) (*Property, bool) { + return property, false +} + +func IsIntrinsicFunc(node *yaml.Node) bool { + if node == nil || node.Tag == "" { + return false + } + + nodeTag := strings.TrimPrefix(node.Tag, "!") + if nodeTag != "Ref" && nodeTag != "Condition" { + nodeTag = fmt.Sprintf("Fn::%s", nodeTag) + } + for tag := range intrinsicFuncs { + + if nodeTag == tag { + return true + } + } + return false +} + +func IsIntrinsic(key string) bool { + for tag := range intrinsicFuncs { + if tag == key { + return true + } + } + return false +} + +func ResolveIntrinsicFunc(property *Property) (*Property, bool) { + if property == nil { + return nil, false + } + if !property.IsMap() { + return property, false + } + + for funcName := range property.AsMap() { + if fn := intrinsicFuncs[funcName]; fn != nil { + prop, resolved := fn(property) + if prop == nil || !resolved { + return prop, false + } + + prop.inferType() + return prop, true + } + } + return property, false +} + +func getIntrinsicTag(tag string) string { + tag = strings.TrimPrefix(tag, "!") + switch tag { + case "Ref", "Contains": + return tag + default: + return fmt.Sprintf("Fn::%s", tag) + } +} + +func abortIntrinsic(property *Property, _ string, _ ...string) (*Property, bool) { + // + return property, false +} diff --git a/pkg/iac/scanners/cloudformation/parser/intrinsics_test.go b/pkg/iac/scanners/cloudformation/parser/intrinsics_test.go new file mode 100644 index 000000000000..a69e04dd0fba --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/intrinsics_test.go @@ -0,0 +1,45 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" +) + +func Test_is_intrinsic_returns_expected(t *testing.T) { + + testCases := []struct { + nodeTag string + expectedResult bool + }{ + { + nodeTag: "!Ref", + expectedResult: true, + }, + { + nodeTag: "!Join", + expectedResult: true, + }, + { + nodeTag: "!Sub", + expectedResult: true, + }, + { + nodeTag: "!Equals", + expectedResult: true, + }, + { + nodeTag: "!Equal", + expectedResult: false, + }, + } + + for _, tt := range testCases { + n := &yaml.Node{ + Tag: tt.nodeTag, + } + assert.Equal(t, tt.expectedResult, IsIntrinsicFunc(n)) + } + +} diff --git a/pkg/iac/scanners/cloudformation/parser/parameter.go b/pkg/iac/scanners/cloudformation/parser/parameter.go new file mode 100644 index 000000000000..10b1368e6c94 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/parameter.go @@ -0,0 +1,159 @@ +package parser + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "errors" + "fmt" + "io" + "maps" + "strconv" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +type Parameter struct { + // TODO: remove inner + inner parameterInner +} + +type parameterInner struct { + Type string `yaml:"Type"` + Default any `yaml:"Default"` +} + +func (p *Parameter) UnmarshalYAML(node *yaml.Node) error { + return node.Decode(&p.inner) +} + +func (p *Parameter) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + + var inner parameterInner + + if err := json.UnmarshalDecode(dec, &inner, + json.WithUnmarshalers(json.UnmarshalFromFunc(unmarshalIntFirst)), + ); err != nil { + return err + } + + p.inner = inner + return nil +} + +func unmarshalIntFirst(dec *jsontext.Decoder, v *any) error { + if dec.PeekKind() == '0' { + if jval, err := dec.ReadValue(); err != nil { + return err + } else if v1, err := strconv.ParseInt(string(jval), 10, 64); err == nil { + *v = int(v1) + } else if v1, err := strconv.ParseFloat(string(jval), 64); err == nil { + *v = v1 + } + return nil + } + return json.SkipFunc +} + +func (p *Parameter) Type() cftypes.CfType { + switch p.inner.Type { + case "Boolean": + return cftypes.Bool + case "String": + return cftypes.String + case "Integer": + return cftypes.Int + default: + return cftypes.String + } +} + +func (p *Parameter) Default() any { + return p.inner.Default +} + +func (p *Parameter) UpdateDefault(inVal any) { + passedVal := inVal.(string) + + switch p.inner.Type { + case "Boolean": + p.inner.Default, _ = strconv.ParseBool(passedVal) + case "String": + p.inner.Default = passedVal + case "Integer": + p.inner.Default, _ = strconv.Atoi(passedVal) + default: + p.inner.Default = passedVal + } +} + +type Parameters map[string]any + +func (p *Parameters) Merge(other Parameters) { + maps.Copy((*p), other) +} + +func (p *Parameters) UnmarshalJSONFrom(d *jsontext.Decoder) error { + (*p) = make(Parameters) + + switch d.PeekKind() { + case '{': + // CodePipeline like format + var params struct { + Params map[string]any `json:"Parameters"` + } + + if err := json.UnmarshalDecode(d, ¶ms); err != nil { + return err + } + + (*p) = params.Params + case '[': + // Original format + var params []string + + jval, err := d.ReadValue() + if err != nil { + return err + } + + if err := json.Unmarshal(jval, ¶ms); err == nil { + for _, param := range params { + parts := strings.Split(param, "=") + if len(parts) != 2 { + return fmt.Errorf("invalid key-value parameter: %q", param) + } + (*p)[parts[0]] = parts[1] + } + return nil + } + + // CloudFormation like format + var cfparams []struct { + ParameterKey string `json:"ParameterKey"` + ParameterValue string `json:"ParameterValue"` + } + + if err := json.Unmarshal(jval, &cfparams, json.RejectUnknownMembers(true)); err != nil { + return err + } + + for _, param := range cfparams { + (*p)[param.ParameterKey] = param.ParameterValue + } + default: + return errors.New("unsupported parameters format") + } + + return nil +} + +func ParseParameters(r io.Reader) (Parameters, error) { + var parameters Parameters + if err := json.UnmarshalRead(r, ¶meters); err != nil { + return nil, err + } + return parameters, nil +} diff --git a/pkg/iac/scanners/cloudformation/parser/parameters_test.go b/pkg/iac/scanners/cloudformation/parser/parameters_test.go new file mode 100644 index 000000000000..320ba9bc6d04 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/parameters_test.go @@ -0,0 +1,87 @@ +package parser + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParameters_UnmarshalJSON(t *testing.T) { + tests := []struct { + name string + source string + expected Parameters + wantErr bool + }{ + { + name: "original format", + source: `[ + "Key1=Value1", + "Key2=Value2" + ]`, + expected: map[string]any{ + "Key1": "Value1", + "Key2": "Value2", + }, + }, + { + name: "CloudFormation like format", + source: `[ + { + "ParameterKey": "Key1", + "ParameterValue": "Value1" + }, + { + "ParameterKey": "Key2", + "ParameterValue": "Value2" + } + ]`, + expected: map[string]any{ + "Key1": "Value1", + "Key2": "Value2", + }, + }, + { + name: "CloudFormation like format, with unknown fields", + source: `[ + { + "ParameterKey": "Key1", + "ParameterValue": "Value1" + }, + { + "ParameterKey": "Key2", + "ParameterValue": "Value2", + "UsePreviousValue": true + } + ]`, + wantErr: true, + }, + { + name: "CodePipeline like format", + source: `{ + "Parameters": { + "Key1": "Value1", + "Key2": "Value2" + } + }`, + expected: map[string]any{ + "Key1": "Value1", + "Key2": "Value2", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params, err := ParseParameters(strings.NewReader(tt.source)) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expected, params) + }) + } +} diff --git a/pkg/iac/scanners/cloudformation/parser/parser.go b/pkg/iac/scanners/cloudformation/parser/parser.go new file mode 100644 index 000000000000..af60b759c3bc --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/parser.go @@ -0,0 +1,204 @@ +package parser + +import ( + "context" + "fmt" + "io" + "io/fs" + "path" + "path/filepath" + "strings" + + "github.com/hashicorp/go-multierror" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/ignore" + "github.com/aquasecurity/trivy/pkg/log" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type Parser struct { + logger *log.Logger + parameterFiles []string + parameters map[string]any + overridedParameters Parameters + configsFS fs.FS +} + +type Option func(*Parser) + +func WithParameters(params map[string]any) Option { + return func(p *Parser) { + p.parameters = params + } +} + +func WithParameterFiles(files ...string) Option { + return func(p *Parser) { + p.parameterFiles = files + } +} + +func WithConfigsFS(fsys fs.FS) Option { + return func(p *Parser) { + p.configsFS = fsys + } +} + +func New(opts ...Option) *Parser { + p := &Parser{ + logger: log.WithPrefix("cloudformation parser"), + } + for _, option := range opts { + option(p) + } + return p +} + +func (p *Parser) ParseFS(ctx context.Context, fsys fs.FS, dir string) (FileContexts, error) { + var contexts FileContexts + if err := fs.WalkDir(fsys, filepath.ToSlash(dir), func(path string, entry fs.DirEntry, err error) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + if err != nil { + return err + } + if entry.IsDir() { + return nil + } + + c, err := p.ParseFile(ctx, fsys, path) + if err != nil { + p.logger.Error("Error parsing file", log.FilePath(path), log.Err(err)) + return nil + } + contexts = append(contexts, c) + return nil + }); err != nil { + return nil, err + } + return contexts, nil +} + +func (p *Parser) ParseFile(ctx context.Context, fsys fs.FS, filePath string) (fctx *FileContext, err error) { + defer func() { + if e := recover(); e != nil { + err = fmt.Errorf("panic during parse: %s", e) + } + }() + + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + if p.configsFS == nil { + p.configsFS = fsys + } + + if err := p.parseParams(); err != nil { + return nil, fmt.Errorf("failed to parse parameters file: %w", err) + } + + sourceFmt := YamlSourceFormat + if path.Ext(filePath) == ".json" { + sourceFmt = JsonSourceFormat + } + + f, err := fsys.Open(filePath) + if err != nil { + return nil, err + } + defer f.Close() + + content, err := io.ReadAll(f) + if err != nil { + return nil, err + } + + lines := strings.Split(string(content), "\n") + + fctx = &FileContext{ + filepath: filePath, + lines: lines, + SourceFormat: sourceFmt, + } + + switch sourceFmt { + case YamlSourceFormat: + if err := yaml.Unmarshal(content, fctx); err != nil { + return nil, NewErrInvalidContent(filePath, err) + } + fctx.Ignores = ignore.Parse(string(content), filePath, "") + case JsonSourceFormat: + if err := xjson.Unmarshal(content, fctx); err != nil { + return nil, NewErrInvalidContent(filePath, err) + } + } + + fctx.stripNullProperties() + + fctx.overrideParameters(p.overridedParameters) + + if params := fctx.missingParameterValues(); len(params) > 0 { + p.logger.Warn("Missing parameter values", log.FilePath(filePath), log.String("parameters", strings.Join(params, ", "))) + } + + fctx.lines = lines + fctx.SourceFormat = sourceFmt + fctx.filepath = filePath + + p.logger.Debug("Context loaded from source", log.FilePath(filePath)) + + // the context must be set to conditions before resources + for _, c := range fctx.Conditions { + c.setContext(fctx) + } + + for name, r := range fctx.Resources { + r.configureResource(name, fsys, filePath, fctx) + } + + if err := fctx.expandTransforms(); err != nil { + return nil, err + } + + return fctx, nil +} + +func (p *Parser) parseParams() error { + if p.overridedParameters != nil { // parameters have already been parsed + return nil + } + + params := make(Parameters) + + var errs error + for _, path := range p.parameterFiles { + f, err := p.configsFS.Open(path) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("open file: %w", err)) + continue + } + + if parameters, err := ParseParameters(f); err != nil { + errs = multierror.Append(errs, err) + } else { + params.Merge(parameters) + } + _ = f.Close() + } + + if errs != nil { + return errs + } + + params.Merge(p.parameters) + + p.overridedParameters = params + return nil +} diff --git a/pkg/iac/scanners/cloudformation/parser/parser_test.go b/pkg/iac/scanners/cloudformation/parser/parser_test.go new file mode 100644 index 000000000000..f3b410370ee1 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/parser_test.go @@ -0,0 +1,639 @@ +package parser + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" +) + +func parseFile(t *testing.T, source, name string) (FileContexts, error) { + tmp := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(tmp, name), []byte(source), 0o600)) + fs := os.DirFS(tmp) + return New().ParseFS(t.Context(), fs, ".") +} + +func Test_parse_yaml(t *testing.T) { + source := `--- +Parameters: + BucketName: + Type: String + Default: naughty + EncryptBucket: + Type: Boolean + Default: false +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: naughty + BucketEncryption: + ServerSideEncryptionConfiguration: + - BucketKeyEnabled: + Ref: EncryptBucket` + + files, err := parseFile(t, source, "cf.yaml") + require.NoError(t, err) + assert.Len(t, files, 1) + file := files[0] + + assert.Len(t, file.Resources, 1) + assert.Len(t, file.Parameters, 2) + + bucket, ok := file.Resources["S3Bucket"] + require.True(t, ok, "S3Bucket resource should be available") + assert.Equal(t, "cf.yaml", bucket.Range().GetFilename()) + assert.Equal(t, 10, bucket.Range().GetStartLine()) + assert.Equal(t, 17, bucket.Range().GetEndLine()) +} + +func Test_parse_json(t *testing.T) { + source := `{ + "Parameters": { + "BucketName": { + "Type": "String", + "Default": "naughty" + }, + "BucketKeyEnabled": { + "Type": "Boolean", + "Default": false + } + }, + "Resources": { + "S3Bucket": { + "Type": "AWS::S3::Bucket", + "properties": { + "BucketName": { + "Ref": "BucketName" + }, + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "BucketKeyEnabled": { + "Ref": "BucketKeyEnabled" + } + } + ] + } + } + } + } +} +` + + files, err := parseFile(t, source, "cf.json") + require.NoError(t, err) + assert.Len(t, files, 1) + file := files[0] + + assert.Len(t, file.Resources, 1) + assert.Len(t, file.Parameters, 2) +} + +func Test_parse_yaml_with_map_ref(t *testing.T) { + source := `--- +Parameters: + BucketName: + Type: String + Default: referencedBucket + EncryptBucket: + Type: Boolean + Default: false +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: + Ref: BucketName + BucketEncryption: + ServerSideEncryptionConfiguration: + - BucketKeyEnabled: + Ref: EncryptBucket` + + files, err := parseFile(t, source, "cf.yaml") + require.NoError(t, err) + assert.Len(t, files, 1) + file := files[0] + + assert.Len(t, file.Resources, 1) + assert.Len(t, file.Parameters, 2) + + res := file.GetResourceByLogicalID("S3Bucket") + assert.NotNil(t, res) + + refProp := res.GetProperty("BucketName") + assert.False(t, refProp.IsNil()) + assert.Equal(t, "referencedBucket", refProp.AsString()) +} + +func Test_parse_yaml_with_intrinsic_functions(t *testing.T) { + source := `--- +Parameters: + BucketName: + Type: String + Default: somebucket + EncryptBucket: + Type: Boolean + Default: false +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: !Ref BucketName + BucketEncryption: + ServerSideEncryptionConfiguration: + - BucketKeyEnabled: false +` + + files, err := parseFile(t, source, "cf.yaml") + require.NoError(t, err) + assert.Len(t, files, 1) + ctx := files[0] + + assert.Len(t, ctx.Resources, 1) + assert.Len(t, ctx.Parameters, 2) + + res := ctx.GetResourceByLogicalID("S3Bucket") + assert.NotNil(t, res) + + refProp := res.GetProperty("BucketName") + assert.False(t, refProp.IsNil()) + assert.Equal(t, "somebucket", refProp.AsString()) +} + +func createTestFileContext(t *testing.T, source string) *FileContext { + contexts, err := parseFile(t, source, "main.yaml") + require.NoError(t, err) + require.Len(t, contexts, 1) + return contexts[0] +} + +func Test_parse_yaml_use_condition_in_resource(t *testing.T) { + source := `--- +AWSTemplateFormatVersion: "2010-09-09" +Description: some description +Parameters: + ServiceName: + Type: String + Description: The service name + EnvName: + Type: String + Description: Optional environment name to prefix all resources with + Default: "" + +Conditions: + SuffixResources: !Not [!Equals [!Ref EnvName, ""]] + +Resources: + ErrorTimedOutMetricFilter: + Type: AWS::Logs::MetricFilter + Properties: + FilterPattern: '?ERROR ?error ?Error ?"timed out"' # If log contains one of these error words or timed out + LogGroupName: + !If [ + SuffixResources, + !Sub "/aws/lambda/${ServiceName}-${EnvName}", + !Sub "/aws/lambda/${ServiceName}", + ] + MetricTransformations: + - MetricName: !Sub "${ServiceName}-ErrorLogCount" + MetricNamespace: market-LogMetrics + MetricValue: 1 + DefaultValue: 0 +` + + files, err := parseFile(t, source, "cf.yaml") + require.NoError(t, err) + assert.Len(t, files, 1) + ctx := files[0] + + assert.Len(t, ctx.Parameters, 2) + assert.Len(t, ctx.Conditions, 1) + assert.Len(t, ctx.Resources, 1) + + res := ctx.GetResourceByLogicalID("ErrorTimedOutMetricFilter") + assert.NotNil(t, res) + + refProp := res.GetProperty("LogGroupName") + assert.False(t, refProp.IsNil()) + assert.Equal(t, "/aws/lambda/${ServiceName}", refProp.AsString()) +} + +func TestParse_WithParameters(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.yaml": `AWSTemplateFormatVersion: 2010-09-09 +Parameters: + KmsMasterKeyId: + Type: String +Resources: + TestQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: test-queue + KmsMasterKeyId: !Ref KmsMasterKeyId + `, + }) + + params := map[string]any{ + "KmsMasterKeyId": "some_id", + } + p := New(WithParameters(params)) + + files, err := p.ParseFS(t.Context(), fs, ".") + require.NoError(t, err) + require.Len(t, files, 1) + + file := files[0] + res := file.GetResourceByLogicalID("TestQueue") + assert.NotNil(t, res) + + kmsProp := res.GetProperty("KmsMasterKeyId") + assert.False(t, kmsProp.IsNil()) + assert.Equal(t, "some_id", kmsProp.AsString()) +} + +func TestParse_WithParameterFiles(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.yaml": `AWSTemplateFormatVersion: 2010-09-09 +Parameters: + KmsMasterKeyId: + Type: String +Resources: + TestQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: test-queue + KmsMasterKeyId: !Ref KmsMasterKeyId +`, + "params.json": `[ + { + "ParameterKey": "KmsMasterKeyId", + "ParameterValue": "some_id" + } +] + `, + }) + + p := New(WithParameterFiles("params.json")) + + files, err := p.ParseFS(t.Context(), fs, ".") + require.NoError(t, err) + require.Len(t, files, 1) + + file := files[0] + res := file.GetResourceByLogicalID("TestQueue") + assert.NotNil(t, res) + + kmsProp := res.GetProperty("KmsMasterKeyId") + assert.False(t, kmsProp.IsNil()) + assert.Equal(t, "some_id", kmsProp.AsString()) +} + +func TestParse_WithConfigFS(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "queue.yaml": `AWSTemplateFormatVersion: 2010-09-09 +Parameters: + KmsMasterKeyId: + Type: String +Resources: + TestQueue: + Type: 'AWS::SQS::Queue' + Properties: + QueueName: testqueue + KmsMasterKeyId: !Ref KmsMasterKeyId +`, + "bucket.yaml": `AWSTemplateFormatVersion: '2010-09-09' +Description: Bucket +Parameters: + BucketName: + Type: String +Resources: + S3Bucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref BucketName +`, + }) + + configFS := testutil.CreateFS(map[string]string{ + "/workdir/parameters/queue.json": `[ + { + "ParameterKey": "KmsMasterKeyId", + "ParameterValue": "some_id" + } + ] + `, + "/workdir/parameters/s3.json": `[ + { + "ParameterKey": "BucketName", + "ParameterValue": "testbucket" + } + ]`, + }) + + p := New( + WithParameterFiles("workdir/parameters/queue.json", "workdir/parameters/s3.json"), + WithConfigsFS(configFS), + ) + + files, err := p.ParseFS(t.Context(), fs, ".") + require.NoError(t, err) + require.Len(t, files, 2) + + for _, file := range files { + if strings.Contains(file.filepath, "queue") { + res := file.GetResourceByLogicalID("TestQueue") + assert.NotNil(t, res) + + kmsProp := res.GetProperty("KmsMasterKeyId") + assert.False(t, kmsProp.IsNil()) + assert.Equal(t, "some_id", kmsProp.AsString()) + } else if strings.Contains(file.filepath, "s3") { + res := file.GetResourceByLogicalID("S3Bucket") + assert.NotNil(t, res) + + bucketNameProp := res.GetProperty("BucketName") + assert.False(t, bucketNameProp.IsNil()) + assert.Equal(t, "testbucket", bucketNameProp.AsString()) + } + } +} + +func TestJsonWithNumbers(t *testing.T) { + src := ` +{ + "AWSTemplateFormatVersion": "2010-09-09", + "Parameters": { + "SomeIntParam": { + "Type": "Number", + "Default": 1 + }, + "SomeFloatParam": { + "Type": "Number", + "Default": 1.1 + } + }, + "Resources": { + "SomeResource": { + "Type": "Test::Resource", + "Properties": { + "SomeIntProp": 1, + "SomeFloatProp": 1.1 + } + } + } +} +` + + fsys := testutil.CreateFS(map[string]string{ + "main.json": src, + }) + + files, err := New().ParseFS(t.Context(), fsys, ".") + + require.NoError(t, err) + require.Len(t, files, 1) + + file := files[0] + + assert.Equal(t, 1, file.Parameters["SomeIntParam"].Default()) + assert.InEpsilon(t, 1.1, file.Parameters["SomeFloatParam"].Default(), 0.0001) + + res := file.GetResourcesByType("Test::Resource") + assert.NotNil(t, res) + assert.Len(t, res, 1) + + assert.Equal(t, 1, res[0].GetProperty("SomeIntProp").AsIntValue().Value()) + assert.Equal(t, 0, res[0].GetProperty("SomeFloatProp").AsIntValue().Value()) +} + +func TestParameterIsNull(t *testing.T) { + src := `--- +AWSTemplateFormatVersion: 2010-09-09 + +Parameters: + Email: + Type: String + +Conditions: + SubscribeEmail: !Not [!Equals [ !Ref Email, ""]] +` + + fsys := testutil.CreateFS(map[string]string{ + "main.yaml": src, + }) + + files, err := New().ParseFS(t.Context(), fsys, ".") + require.NoError(t, err) + require.Len(t, files, 1) +} + +func Test_TemplateWithNullProperty(t *testing.T) { + src := `AWSTemplateFormatVersion: "2010-09-09" +Resources: + TestBucket: + Type: "AWS::S3::Bucket" + Properties: + BucketName:` + + fsys := testutil.CreateFS(map[string]string{ + "main.yaml": src, + }) + + files, err := New().ParseFS(t.Context(), fsys, ".") + require.NoError(t, err) + require.Len(t, files, 1) + + file := files[0] + + res := file.GetResourceByLogicalID("TestBucket") + + assert.True(t, res.GetProperty("BucketName").IsNil()) +} + +func Test_TemplateWithNullNestedProperty(t *testing.T) { + src := `AWSTemplateFormatVersion: "2010-09-09" +Description: "BAD" +Resources: + TestBucket: + Type: "AWS::S3::Bucket" + Properties: + BucketName: test + PublicAccessBlockConfiguration: + BlockPublicAcls: null` + + fsys := testutil.CreateFS(map[string]string{ + "main.yaml": src, + }) + + files, err := New().ParseFS(t.Context(), fsys, ".") + require.NoError(t, err) + require.Len(t, files, 1) + + file := files[0] + + res := file.GetResourceByLogicalID("TestBucket") + + assert.True(t, res.GetProperty("PublicAccessBlockConfiguration.BlockPublicAcls").IsNil()) +} + +func Test_ExpandForEachYAML(t *testing.T) { + source := `AWSTemplateFormatVersion: 2010-09-09 +Transform: AWS::LanguageExtensions +Parameters: + TopicNamesParam: + Type: CommaDelimitedList + Default: Success,Failure +Mappings: + Success: + Properties: + DisplayName: success + FifoTopic: "true" + Failure: + Properties: + DisplayName: failure + FifoTopic: "false" +Resources: + 'Fn::ForEach::Topics': + - TopicName + - !Split [",", !Ref TopicNamesParam] + - 'SnsTopic${TopicName}': + Type: 'AWS::SNS::Topic' + Properties: + TopicName: !Sub '${TopicName}.fifo' + 'Fn::ForEach::Properties': + - PropertyName + - [DisplayName, FifoTopic] + - '${PropertyName}': + 'Fn::FindInMap': + - Ref: 'TopicName' + - Properties + - Ref: 'PropertyName' + 'Fn::ForEach::Subscriptions': + - SubName + - ['Alpha', 'Beta'] + - 'SnsSubscription${TopicName}${SubName}': + Type: 'AWS::SNS::Subscription' + Properties: + TopicArn: !Ref 'SnsTopic${TopicName}' + Protocol: email + Endpoint: !Sub '${SubName}@example.com' +` + + files, err := parseFile(t, source, "cf.yaml") + require.NoError(t, err) + file := files[0] + + assert.Len(t, file.Resources, 6) + + tests := []struct { + LogicalID string + Props map[string]any + }{ + // SnsTopic + { + "SnsTopicSuccess", + map[string]any{ + "TopicName": "Success.fifo", + "DisplayName": "success", + "FifoTopic": "true", + }, + }, + { + "SnsTopicFailure", + map[string]any{ + "TopicName": "Failure.fifo", + "DisplayName": "failure", + "FifoTopic": "false", + }, + }, + // SnsSubscription + { + "SnsSubscriptionSuccessAlpha", + map[string]any{ + "TopicArn": "SnsTopicSuccess", + "Protocol": "email", + "Endpoint": "Alpha@example.com", + }, + }, + { + "SnsSubscriptionSuccessBeta", + map[string]any{ + "TopicArn": "SnsTopicSuccess", + "Protocol": "email", + "Endpoint": "Beta@example.com", + }, + }, + { + "SnsSubscriptionFailureAlpha", + map[string]any{ + "TopicArn": "SnsTopicFailure", + "Protocol": "email", + "Endpoint": "Alpha@example.com", + }, + }, + { + "SnsSubscriptionFailureBeta", + map[string]any{ + "TopicArn": "SnsTopicFailure", + "Protocol": "email", + "Endpoint": "Beta@example.com", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.LogicalID, func(t *testing.T) { + res, ok := file.Resources[tt.LogicalID] + require.True(t, ok) + for propName, expected := range tt.Props { + prop := res.GetProperty(propName) + assert.Equal(t, expected, prop.RawValue()) + } + }) + } +} + +func Test_ExpandForEachJSON(t *testing.T) { + source := `{ + "AWSTemplateFormatVersion": "2010-09-09", + "Transform": "AWS::LanguageExtensions", + "Resources": { + "Fn::ForEach::Buckets": [ + "Suffix", + ["A", "B"], + { + "S3Bucket${Suffix}": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { "Fn::Sub": "bucket-${Suffix}" } + } + } + } + ] + } + }` + + files, err := parseFile(t, source, "cf.json") + require.NoError(t, err) + require.Len(t, files, 1) + + file := files[0] + require.Len(t, file.Resources, 2) + + b1, ok := file.Resources["S3BucketA"] + require.True(t, ok) + assert.Equal(t, "AWS::S3::Bucket", b1.Type()) + assert.Equal(t, "bucket-A", b1.GetProperty("BucketName").AsString()) + + b2, ok := file.Resources["S3BucketB"] + require.True(t, ok) + assert.Equal(t, "AWS::S3::Bucket", b2.Type()) + assert.Equal(t, "bucket-B", b2.GetProperty("BucketName").AsString()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/property.go b/pkg/iac/scanners/cloudformation/parser/property.go new file mode 100644 index 000000000000..42feede738b9 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/property.go @@ -0,0 +1,485 @@ +package parser + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + "io/fs" + "reflect" + "strconv" + "strings" + + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type EqualityOptions = int + +const ( + IgnoreCase EqualityOptions = iota +) + +type Property struct { + xjson.Location + ctx *FileContext + Type cftypes.CfType + Value any `json:"Value" yaml:"Value"` + name string + comment string + rng iacTypes.Range + parentRange iacTypes.Range + logicalId string + unresolved bool + + loopCtx *LoopContext +} + +func (p *Property) setName(name string) { + p.name = name + if p.Type == cftypes.Map { + for n, subProp := range p.AsMap() { + if subProp == nil { + continue + } + subProp.setName(n) + } + } +} + +func (p *Property) setContext(ctx *FileContext) { + p.walk(func(prop *Property) bool { + prop.ctx = ctx + return true + }) +} + +func (p *Property) setLogicalResource(id string) { + p.walk(func(prop *Property) bool { + prop.logicalId = id + return !prop.isFunction() + }) +} + +func (p *Property) setFileAndParentRange(target fs.FS, filepath string, parentRange iacTypes.Range) { + p.rng = iacTypes.NewRange(filepath, p.StartLine, p.EndLine, p.rng.GetSourcePrefix(), target) + p.parentRange = parentRange + + switch p.Type { + case cftypes.Map: + for _, subProp := range p.AsMap() { + if subProp == nil { + continue + } + subProp.setFileAndParentRange(target, filepath, p.rng) + } + case cftypes.List: + for _, subProp := range p.AsList() { + if subProp == nil { + continue + } + subProp.setFileAndParentRange(target, filepath, p.rng) + } + } +} + +func (p *Property) clone() *Property { + if p == nil { + return nil + } + + clone := &Property{ + Location: p.Location, + ctx: p.ctx, + Type: p.Type, + name: p.name, + comment: p.comment, + rng: p.rng, + parentRange: p.parentRange, + logicalId: p.logicalId, + unresolved: p.unresolved, + } + + switch v := p.Value.(type) { + case map[string]*Property: + m := make(map[string]*Property, len(v)) + for k, el := range v { + m[k] = el.clone() + } + clone.Value = m + case []*Property: + slice := make([]*Property, len(v)) + for i, el := range v { + slice[i] = el.clone() + } + clone.Value = slice + default: + clone.Value = v + } + + return clone +} + +func (p *Property) UnmarshalYAML(node *yaml.Node) error { + p.StartLine = node.Line + p.EndLine = calculateEndLine(node) + p.comment = node.LineComment + return setPropertyValueFromYaml(node, p) +} + +func (p *Property) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + var valPtr any + var nodeType cftypes.CfType + + switch k := dec.PeekKind(); k { + case 't', 'f': + valPtr = new(bool) + nodeType = cftypes.Bool + case '"': + valPtr = new(string) + nodeType = cftypes.String + case '0': + return p.parseNumericValue(dec) + case '[', 'n': + valPtr = new([]*Property) + nodeType = cftypes.List + case '{': + valPtr = new(map[string]*Property) + nodeType = cftypes.Map + case 0: + return dec.SkipValue() + default: + return fmt.Errorf("unexpected token kind %q at %d", k.String(), dec.InputOffset()) + } + + if err := json.UnmarshalDecode(dec, valPtr); err != nil { + return err + } + + p.Value = reflect.ValueOf(valPtr).Elem().Interface() + p.Type = nodeType + return nil +} + +func (p *Property) parseNumericValue(dec *jsontext.Decoder) error { + raw, err := dec.ReadValue() + if err != nil { + return err + } + strVal := string(raw) + + if v, err := strconv.ParseInt(strVal, 10, 64); err == nil { + p.Value = int(v) + p.Type = cftypes.Int + return nil + } + if v, err := strconv.ParseFloat(strVal, 64); err == nil { + p.Value = v + p.Type = cftypes.Float64 + return nil + } + return fmt.Errorf("invalid numeric value: %q", strVal) +} + +func (p *Property) Metadata() iacTypes.Metadata { + return iacTypes.NewMetadata(p.rng, p.name). + WithParent(iacTypes.NewMetadata(p.parentRange, p.logicalId)) +} + +func (p *Property) isFunction() bool { + if p == nil { + return false + } + if p.Type == cftypes.Map { + for n := range p.AsMap() { + return IsIntrinsic(n) + } + } + return false +} + +func (p *Property) RawValue() any { + return p.Value +} + +func (p *Property) AsRawStrings() ([]string, error) { + if len(p.ctx.lines) < p.rng.GetEndLine() { + return p.ctx.lines, nil + } + return p.ctx.lines[p.rng.GetStartLine()-1 : p.rng.GetEndLine()], nil +} + +func (p *Property) resolveValue() (*Property, bool) { + if !p.isFunction() || p.IsUnresolved() { + return p, true + } + + resolved, ok := ResolveIntrinsicFunc(p) + if ok { + return resolved, true + } + + p.unresolved = true + return p, false +} + +func (p *Property) GetStringProperty(path string, defaultValue ...string) iacTypes.StringValue { + defVal := "" + if len(defaultValue) > 0 { + defVal = defaultValue[0] + } + + if p.IsUnresolved() { + return iacTypes.StringUnresolvable(p.Metadata()) + } + + prop := p.GetProperty(path) + if prop.IsNotString() { + return p.StringDefault(defVal) + } + return prop.AsStringValue() +} + +func (p *Property) StringDefault(defaultValue string) iacTypes.StringValue { + return iacTypes.StringDefault(defaultValue, p.Metadata()) +} + +func (p *Property) GetBoolProperty(path string, defaultValue ...bool) iacTypes.BoolValue { + defVal := false + if len(defaultValue) > 0 { + defVal = defaultValue[0] + } + + if p.IsUnresolved() { + return iacTypes.BoolUnresolvable(p.Metadata()) + } + + prop := p.GetProperty(path) + + if prop.isFunction() { + prop, _ = prop.resolveValue() + } + + if prop.IsNotBool() { + return p.inferBool(prop, defVal) + } + return prop.AsBoolValue() +} + +func (p *Property) GetIntProperty(path string, defaultValue ...int) iacTypes.IntValue { + defVal := 0 + if len(defaultValue) > 0 { + defVal = defaultValue[0] + } + + if p.IsUnresolved() { + return iacTypes.IntUnresolvable(p.Metadata()) + } + + prop := p.GetProperty(path) + + if prop.IsNotInt() { + return p.IntDefault(defVal) + } + return prop.AsIntValue() +} + +func (p *Property) BoolDefault(defaultValue bool) iacTypes.BoolValue { + return iacTypes.BoolDefault(defaultValue, p.Metadata()) +} + +func (p *Property) IntDefault(defaultValue int) iacTypes.IntValue { + return iacTypes.IntDefault(defaultValue, p.Metadata()) +} + +func (p *Property) GetProperty(path string) *Property { + pathParts := strings.Split(path, ".") + + first := pathParts[0] + property := p + + if p.isFunction() { + property, _ = p.resolveValue() + } + + if property.IsNotMap() { + return nil + } + + for n, p := range property.AsMap() { + if n == first { + property = p + break + } + } + + if len(pathParts) == 1 || property == nil { + return property + } + + if nestedProperty := property.GetProperty(strings.Join(pathParts[1:], ".")); nestedProperty != nil { + if nestedProperty.isFunction() { + resolved, _ := nestedProperty.resolveValue() + return resolved + } + return nestedProperty + } + + return &Property{} +} + +func (p *Property) deriveResolved(propType cftypes.CfType, propValue any) *Property { + clone := p.clone() + clone.Type = propType + clone.Value = propValue + return clone +} + +func (p *Property) ParentRange() iacTypes.Range { + return p.parentRange +} + +func (p *Property) inferBool(prop *Property, defaultValue bool) iacTypes.BoolValue { + if prop.IsString() { + if prop.EqualTo("true", IgnoreCase) { + return iacTypes.Bool(true, prop.Metadata()) + } + if prop.EqualTo("yes", IgnoreCase) { + return iacTypes.Bool(true, prop.Metadata()) + } + if prop.EqualTo("1", IgnoreCase) { + return iacTypes.Bool(true, prop.Metadata()) + } + if prop.EqualTo("false", IgnoreCase) { + return iacTypes.Bool(false, prop.Metadata()) + } + if prop.EqualTo("no", IgnoreCase) { + return iacTypes.Bool(false, prop.Metadata()) + } + if prop.EqualTo("0", IgnoreCase) { + return iacTypes.Bool(false, prop.Metadata()) + } + } + + if prop.IsInt() { + if prop.EqualTo(0) { + return iacTypes.Bool(false, prop.Metadata()) + } + if prop.EqualTo(1) { + return iacTypes.Bool(true, prop.Metadata()) + } + } + + return p.BoolDefault(defaultValue) +} + +func (p *Property) String() string { + r := "" + switch p.Type { + case cftypes.String: + r = p.AsString() + case cftypes.Int: + r = strconv.Itoa(p.AsInt()) + } + return r +} + +func (p *Property) GetJsonBytes(squashList ...bool) []byte { + if p.IsNil() { + return []byte{} + } + lines, err := p.AsRawStrings() + if err != nil { + return nil + } + if p.ctx.SourceFormat == JsonSourceFormat { + return []byte(strings.Join(lines, " ")) + } + + if len(squashList) > 0 { + lines[0] = strings.Replace(lines[0], "-", " ", 1) + } + + lines = removeLeftMargin(lines) + + yamlContent := strings.Join(lines, "\n") + var body any + if err := yaml.Unmarshal([]byte(yamlContent), &body); err != nil { + return nil + } + jsonBody := convert(body) + policyJson, err := json.Marshal(jsonBody) + if err != nil { + return nil + } + return policyJson +} + +func (p *Property) GetJsonBytesAsString(squashList ...bool) string { + return string(p.GetJsonBytes(squashList...)) +} + +func removeLeftMargin(lines []string) []string { + if len(lines) == 0 { + return lines + } + prefixSpace := len(lines[0]) - len(strings.TrimLeft(lines[0], " ")) + + for i, line := range lines { + if len(line) >= prefixSpace { + lines[i] = line[prefixSpace:] + } + } + return lines +} + +func convert(input any) any { + switch x := input.(type) { + case map[any]any: + outpMap := make(map[string]any) + for k, v := range x { + outpMap[k.(string)] = convert(v) + } + return outpMap + case []any: + for i, v := range x { + x[i] = convert(v) + } + } + return input +} + +func (p *Property) inferType() { + typ := cftypes.TypeFromGoValue(p.Value) + if typ == cftypes.Unknown { + return + } + p.Type = typ +} + +func (p *Property) walk(fn func(*Property) bool) { + if fn == nil { + return + } + + if !fn(p) { + return + } + + switch v := p.Value.(type) { + case map[string]*Property: + for _, child := range v { + if child != nil { + child.walk(fn) + } + } + case []*Property: + for _, child := range v { + if child != nil { + child.walk(fn) + } + } + } +} diff --git a/pkg/iac/scanners/cloudformation/parser/property_conversion.go b/pkg/iac/scanners/cloudformation/parser/property_conversion.go new file mode 100644 index 000000000000..66a3ff8f0f23 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/property_conversion.go @@ -0,0 +1,145 @@ +package parser + +import ( + "fmt" + "strconv" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" + "github.com/aquasecurity/trivy/pkg/log" +) + +func (p *Property) IsConvertableTo(conversionType cftypes.CfType) bool { + if p.IsNil() { + return false + } + + switch conversionType { + case cftypes.Int: + return p.isConvertableToInt() + case cftypes.Bool: + return p.isConvertableToBool() + case cftypes.String: + return p.isConvertableToString() + } + return false +} + +func (p *Property) isConvertableToString() bool { + switch p.Type { + case cftypes.Map: + return false + case cftypes.List: + for _, p := range p.AsList() { + if !p.IsString() { + return false + } + } + } + return true +} + +func (p *Property) isConvertableToBool() bool { + switch p.Type { + case cftypes.String: + return p.EqualTo("true", IgnoreCase) || p.EqualTo("false", IgnoreCase) || + p.EqualTo("1", IgnoreCase) || p.EqualTo("0", IgnoreCase) + + case cftypes.Int: + return p.EqualTo(1) || p.EqualTo(0) + case cftypes.Bool: + return true + } + return false +} + +func (p *Property) isConvertableToInt() bool { + switch p.Type { + case cftypes.String: + if _, err := strconv.Atoi(p.AsString()); err == nil { + return true + } + case cftypes.Bool, cftypes.Int: + return true + } + return false +} + +func (p *Property) ConvertTo(conversionType cftypes.CfType) *Property { + if p.IsNil() { + return nil + } + + if p.Type == conversionType { + return p + } + + if !p.IsConvertableTo(conversionType) { + log.Debug("Failed to convert property", + log.String("from", string(p.Type)), + log.String("to", string(conversionType)), + log.Any("range", p.rng.String()), + ) + return p + } + switch conversionType { + case cftypes.Int: + return p.convertToInt() + case cftypes.Bool: + return p.convertToBool() + case cftypes.String: + return p.convertToString() + } + return p +} + +func (p *Property) convertToString() *Property { + switch p.Type { + case cftypes.Int: + return p.deriveResolved(cftypes.String, strconv.Itoa(p.AsInt())) + case cftypes.Bool: + return p.deriveResolved(cftypes.String, strconv.FormatBool(p.AsBool())) + case cftypes.List: + var parts []string + for _, property := range p.AsList() { + parts = append(parts, property.AsString()) + } + return p.deriveResolved(cftypes.String, fmt.Sprintf("[%s]", strings.Join(parts, ", "))) + } + return p +} + +func (p *Property) convertToBool() *Property { + switch p.Type { + case cftypes.String: + if p.EqualTo("true", IgnoreCase) || p.EqualTo("1") { + return p.deriveResolved(cftypes.Bool, true) + } + if p.EqualTo("false", IgnoreCase) || p.EqualTo("0") { + return p.deriveResolved(cftypes.Bool, false) + } + case cftypes.Int: + if p.EqualTo(1) { + return p.deriveResolved(cftypes.Bool, true) + } + if p.EqualTo(0) { + return p.deriveResolved(cftypes.Bool, false) + } + } + return p +} + +func (p *Property) convertToInt() *Property { + switch p.Type { + case cftypes.String: + if val, err := strconv.Atoi(p.AsString()); err == nil { + return p.deriveResolved(cftypes.Int, val) + } + case cftypes.Bool: + if p.IsTrue() { + return p.deriveResolved(cftypes.Int, 1) + } + return p.deriveResolved(cftypes.Int, 0) + } + return p +} diff --git a/pkg/iac/scanners/cloudformation/parser/property_helpers.go b/pkg/iac/scanners/cloudformation/parser/property_helpers.go new file mode 100644 index 000000000000..2887b68bf537 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/property_helpers.go @@ -0,0 +1,266 @@ +package parser + +import ( + "strconv" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func (p *Property) IsNil() bool { + return p == nil || p.Value == nil +} + +func (p *Property) IsNotNil() bool { + return !p.IsUnresolved() && !p.IsNil() +} + +func (p *Property) Is(t cftypes.CfType) bool { + if p.IsNil() || p.IsUnresolved() { + return false + } + if p.isFunction() { + if prop, success := p.resolveValue(); success && prop != p { + return prop.Is(t) + } + } + return p.Type == t +} + +func (p *Property) IsString() bool { + return p.Is(cftypes.String) +} + +func (p *Property) IsNotString() bool { + return !p.IsUnresolved() && !p.IsString() +} + +func (p *Property) IsInt() bool { + return p.Is(cftypes.Int) +} + +func (p *Property) IsNotInt() bool { + return !p.IsUnresolved() && !p.IsInt() +} + +func (p *Property) IsMap() bool { + if p.IsNil() || p.IsUnresolved() { + return false + } + return p.Type == cftypes.Map +} + +func (p *Property) IsNotMap() bool { + return !p.IsUnresolved() && !p.IsMap() +} + +func (p *Property) IsList() bool { + return p.Is(cftypes.List) +} + +func (p *Property) IsNotList() bool { + return !p.IsUnresolved() && !p.IsList() +} + +func (p *Property) IsBool() bool { + return p.Is(cftypes.Bool) +} + +func (p *Property) IsUnresolved() bool { + return p != nil && p.unresolved +} + +func (p *Property) IsNotBool() bool { + return !p.IsUnresolved() && !p.IsBool() +} + +func (p *Property) AsString() string { + if p.isFunction() { + if prop, success := p.resolveValue(); success && prop != p { + return prop.AsString() + } + return "" + } + if p.IsNil() { + return "" + } + if !p.IsString() { + return "" + } + + return p.Value.(string) +} + +func (p *Property) AsStringValue() iacTypes.StringValue { + if p.unresolved { + return iacTypes.StringUnresolvable(p.Metadata()) + } + return iacTypes.StringExplicit(p.AsString(), p.Metadata()) +} + +func (p *Property) AsInt() int { + if p.isFunction() { + if prop, success := p.resolveValue(); success && prop != p { + return prop.AsInt() + } + return 0 + } + if p.IsNotInt() { + if p.isConvertableToInt() { + return p.convertToInt().AsInt() + } + return 0 + } + + return p.Value.(int) +} + +func (p *Property) AsIntValue() iacTypes.IntValue { + if p.unresolved { + return iacTypes.IntUnresolvable(p.Metadata()) + } + return iacTypes.IntExplicit(p.AsInt(), p.Metadata()) +} + +func (p *Property) AsBool() bool { + if p.isFunction() { + if prop, success := p.resolveValue(); success && prop != p { + return prop.AsBool() + } + return false + } + if !p.IsBool() { + return false + } + return p.Value.(bool) +} + +func (p *Property) AsBoolValue() iacTypes.BoolValue { + if p.unresolved { + return iacTypes.BoolUnresolvable(p.Metadata()) + } + return iacTypes.Bool(p.AsBool(), p.Metadata()) +} + +func (p *Property) AsMap() map[string]*Property { + val, ok := p.Value.(map[string]*Property) + if !ok { + return nil + } + return val +} + +func (p *Property) AsList() []*Property { + if p.isFunction() { + if prop, success := p.resolveValue(); success && prop != p { + return prop.AsList() + } + return []*Property{} + } + + if list, ok := p.Value.([]*Property); ok { + return list + } + return nil +} + +func (p *Property) Len() int { + return len(p.AsList()) +} + +func (p *Property) EqualTo(checkValue any, equalityOptions ...EqualityOptions) bool { + var ignoreCase bool + for _, option := range equalityOptions { + if option == IgnoreCase { + ignoreCase = true + } + } + + switch checkerVal := checkValue.(type) { + case string: + if p.IsNil() { + return false + } + + if p.Type == cftypes.String || p.IsString() { + if ignoreCase { + return strings.EqualFold(p.AsString(), checkerVal) + } + return p.AsString() == checkerVal + } else if p.Type == cftypes.Int || p.IsInt() { + if val, err := strconv.Atoi(checkerVal); err == nil { + return p.AsInt() == val + } + } + return false + case bool: + if p.Type == cftypes.Bool || p.IsBool() { + return p.AsBool() == checkerVal + } + case int: + if p.Type == cftypes.Int || p.IsInt() { + return p.AsInt() == checkerVal + } + } + + return false + +} + +func (p *Property) IsTrue() bool { + if p.IsNil() || !p.IsBool() { + return false + } + + return p.AsBool() +} + +func (p *Property) IsEmpty() bool { + + if p.IsNil() { + return true + } + if p.IsUnresolved() { + return false + } + + switch p.Type { + case cftypes.String: + return p.AsString() == "" + case cftypes.List, cftypes.Map: + return len(p.AsList()) == 0 + default: + return false + } +} + +func (p *Property) Contains(checkVal any) bool { + if p == nil || p.IsNil() { + return false + } + + switch p.Type { + case cftypes.List: + for _, p := range p.AsList() { + if p.EqualTo(checkVal) { + return true + } + } + case cftypes.Map: + if _, ok := checkVal.(string); !ok { + return false + } + for key := range p.AsMap() { + if key == checkVal.(string) { + return true + } + } + case cftypes.String: + if _, ok := checkVal.(string); !ok { + return false + } + return strings.Contains(p.AsString(), checkVal.(string)) + } + return false +} diff --git a/pkg/iac/scanners/cloudformation/parser/property_helpers_test.go b/pkg/iac/scanners/cloudformation/parser/property_helpers_test.go new file mode 100644 index 000000000000..69e3290ad666 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/property_helpers_test.go @@ -0,0 +1,168 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_EqualTo(t *testing.T) { + tests := []struct { + name string + property *Property + checkValue any + opts []EqualityOptions + isEqual bool + }{ + { + name: "prop is nil", + property: nil, + checkValue: "some value", + isEqual: false, + }, + { + name: "compare strings", + property: &Property{ + name: "test_prop", + ctx: &FileContext{}, + rng: types.NewRange("testfile", 1, 1, "", nil), + Type: cftypes.String, + Value: "is str", + }, + checkValue: "is str", + isEqual: true, + }, + { + name: "compare strings ignoring case", + property: &Property{ + Type: cftypes.String, + Value: "is str", + }, + opts: []EqualityOptions{IgnoreCase}, + checkValue: "Is StR", + isEqual: true, + }, + { + name: "strings ate not equal", + property: &Property{ + Type: cftypes.String, + Value: "some value", + }, + checkValue: "some other value", + isEqual: false, + }, + { + name: "compare prop with a int represented by a string", + property: &Property{ + Type: cftypes.Int, + Value: 147, + }, + checkValue: "147", + isEqual: true, + }, + { + name: "compare ints", + property: &Property{ + Type: cftypes.Int, + Value: 701, + }, + checkValue: 701, + isEqual: true, + }, + { + name: "compare bools", + property: &Property{ + Type: cftypes.Bool, + Value: true, + }, + checkValue: true, + isEqual: true, + }, + { + name: "prop is string fn", + property: &Property{ + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::If": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.Bool, + Value: false, + }, + { + Type: cftypes.String, + Value: "bad", + }, + { + Type: cftypes.String, + Value: "some value", + }, + }, + }, + }, + }, + checkValue: "some value", + isEqual: true, + }, + { + name: "prop is int fn", + property: &Property{ + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::If": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.Bool, + Value: true, + }, + { + Type: cftypes.Int, + Value: 121, + }, + { + Type: cftypes.Int, + Value: -1, + }, + }, + }, + }, + }, + checkValue: 121, + isEqual: true, + }, + { + name: "prop is bool fn", + property: &Property{ + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::Equals": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.String, + Value: "foo", + }, + { + Type: cftypes.String, + Value: "foo", + }, + }, + }, + }, + }, + checkValue: true, + isEqual: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.isEqual, tt.property.EqualTo(tt.checkValue, tt.opts...)) + }) + } +} diff --git a/pkg/iac/scanners/cloudformation/parser/pseudo_parameters.go b/pkg/iac/scanners/cloudformation/parser/pseudo_parameters.go new file mode 100644 index 000000000000..243b7ca3483d --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/pseudo_parameters.go @@ -0,0 +1,44 @@ +package parser + +import ( + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +type pseudoParameter struct { + t cftypes.CfType + val any + raw any +} + +var pseudoParameters = map[string]pseudoParameter{ + "AWS::AccountId": {t: cftypes.String, val: "123456789012"}, + "AWS::NotificationARNs": { + t: cftypes.List, + val: []*Property{ + { + Type: cftypes.String, + Value: "notification::arn::1", + }, + { + Type: cftypes.String, + Value: "notification::arn::2", + }, + }, + raw: []string{"notification::arn::1", "notification::arn::2"}, + }, + "AWS::NoValue": {t: cftypes.String, val: ""}, + "AWS::Partition": {t: cftypes.String, val: "aws"}, + "AWS::Region": {t: cftypes.String, val: "eu-west-1"}, + "AWS::StackId": {t: cftypes.String, val: "arn:aws:cloudformation:eu-west-1:stack/ID"}, + "AWS::StackName": {t: cftypes.String, val: "cfsec-test-stack"}, + "AWS::URLSuffix": {t: cftypes.String, val: "amazonaws.com"}, +} + +func (p pseudoParameter) getRawValue() any { + switch p.t { + case cftypes.List: + return p.raw + default: + return p.val + } +} diff --git a/pkg/iac/scanners/cloudformation/parser/pseudo_parameters_test.go b/pkg/iac/scanners/cloudformation/parser/pseudo_parameters_test.go new file mode 100644 index 000000000000..e653b74e65ef --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/pseudo_parameters_test.go @@ -0,0 +1,36 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_Raw(t *testing.T) { + tests := []struct { + name string + key string + expected any + }{ + { + name: "parameter with a string type value", + key: "AWS::AccountId", + expected: "123456789012", + }, + { + name: "a parameter with a list type value", + key: "AWS::NotificationARNs", + expected: []string{"notification::arn::1", "notification::arn::2"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if val, ok := pseudoParameters[tt.key]; ok { + assert.Equal(t, tt.expected, val.getRawValue()) + } else { + t.Fatal("unexpected parameter key") + } + }) + } +} diff --git a/pkg/iac/scanners/cloudformation/parser/reference.go b/pkg/iac/scanners/cloudformation/parser/reference.go new file mode 100644 index 000000000000..705eef2747af --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/reference.go @@ -0,0 +1,21 @@ +package parser + +import ( + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type CFReference struct { + logicalId string + resourceRange iacTypes.Range +} + +func NewCFReference(id string, resourceRange iacTypes.Range) CFReference { + return CFReference{ + logicalId: id, + resourceRange: resourceRange, + } +} + +func (cf CFReference) String() string { + return cf.resourceRange.String() +} diff --git a/pkg/iac/scanners/cloudformation/parser/resource.go b/pkg/iac/scanners/cloudformation/parser/resource.go new file mode 100644 index 000000000000..6b93b90cd61c --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/resource.go @@ -0,0 +1,271 @@ +package parser + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + "io/fs" + "strings" + + "gopkg.in/yaml.v3" + + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type Resource struct { + xjson.Location + typ string + properties map[string]*Property + ctx *FileContext + rng iacTypes.Range + id string + comment string + + raw *Property +} + +func (r *Resource) configureResource(id string, target fs.FS, filepath string, ctx *FileContext) { + r.setId(id) + r.setFile(target, filepath) + r.setContext(ctx) +} + +func (r *Resource) setId(id string) { + r.id = id + + for n, p := range r.properties { + p.setName(n) + } +} + +func (r *Resource) setFile(target fs.FS, filepath string) { + r.rng = iacTypes.NewRange(filepath, r.StartLine, r.EndLine, r.rng.GetSourcePrefix(), target) + + for _, p := range r.properties { + p.setFileAndParentRange(target, filepath, r.rng) + } +} + +func (r *Resource) setContext(ctx *FileContext) { + r.ctx = ctx + if r.raw != nil { + r.raw.setContext(ctx) + } else { + for _, p := range r.properties { + p.setLogicalResource(r.id) + p.setContext(ctx) + } + } +} + +func (r *Resource) clone() *Resource { + clone := &Resource{ + typ: r.typ, + ctx: r.ctx, + rng: r.rng, + id: r.id, + comment: r.comment, + } + + if r.properties != nil { + clone.properties = make(map[string]*Property, len(r.properties)) + for k, p := range r.properties { + clone.properties[k] = p.clone() + } + } + + if r.raw != nil { + clone.raw = r.raw.clone() + } + + return clone +} + +type resourceInner struct { + Type string `json:"Type" yaml:"Type"` + Properties map[string]*Property `json:"Properties" yaml:"Properties"` +} + +func (r *Resource) UnmarshalYAML(node *yaml.Node) error { + r.StartLine = node.Line - 1 + r.EndLine = calculateEndLine(node) + r.comment = node.LineComment + + switch node.Kind { + case yaml.MappingNode: + var i resourceInner + if err := node.Decode(&i); err != nil { + return err + } + r.typ = i.Type + r.properties = i.Properties + return nil + case yaml.SequenceNode: + var raw Property + if err := node.Decode(&raw); err != nil { + return err + } + r.raw = &raw + return nil + default: + return fmt.Errorf("unsupported YAML node kind: %v", node.Kind) + } +} + +func (r *Resource) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + switch dec.PeekKind() { + case '{': + var i resourceInner + if err := json.UnmarshalDecode(dec, &i); err != nil { + return err + } + r.typ = i.Type + r.properties = i.Properties + case '[': + var raw Property + if err := json.UnmarshalDecode(dec, &raw); err != nil { + return err + } + r.raw = &raw + } + return nil +} + +func (r *Resource) ID() string { + return r.id +} + +func (r *Resource) Type() string { + return r.typ +} + +func (r *Resource) Range() iacTypes.Range { + return r.rng +} + +func (r *Resource) SourceFormat() SourceFormat { + return r.ctx.SourceFormat +} + +func (r *Resource) Metadata() iacTypes.Metadata { + return iacTypes.NewMetadata(r.Range(), NewCFReference(r.id, r.rng).String()) +} + +func (r *Resource) IsNil() bool { + return r.id == "" +} + +func (r *Resource) GetProperty(path string) *Property { + + pathParts := strings.Split(path, ".") + + first := pathParts[0] + property := &Property{} + + if p, exists := r.properties[first]; exists { + property = p + } + + if len(pathParts) == 1 || property.IsNil() { + if property.isFunction() { + resolved, _ := property.resolveValue() + return resolved + } + return property + } + + if nestedProperty := property.GetProperty(strings.Join(pathParts[1:], ".")); nestedProperty != nil { + return nestedProperty + } + + return &Property{} +} + +func (r *Resource) GetStringProperty(path string, defaultValue ...string) iacTypes.StringValue { + defVal := "" + if len(defaultValue) > 0 { + defVal = defaultValue[0] + } + + prop := r.GetProperty(path) + + if prop.IsNotString() { + return r.StringDefault(defVal) + } + return prop.AsStringValue() +} + +func (r *Resource) GetBoolProperty(path string, defaultValue ...bool) iacTypes.BoolValue { + defVal := false + if len(defaultValue) > 0 { + defVal = defaultValue[0] + } + + prop := r.GetProperty(path) + + if prop.IsNotBool() { + return r.inferBool(prop, defVal) + } + return prop.AsBoolValue() +} + +func (r *Resource) GetIntProperty(path string, defaultValue ...int) iacTypes.IntValue { + defVal := 0 + if len(defaultValue) > 0 { + defVal = defaultValue[0] + } + + prop := r.GetProperty(path) + + if prop.IsNotInt() { + return r.IntDefault(defVal) + } + return prop.AsIntValue() +} + +func (r *Resource) StringDefault(defaultValue string) iacTypes.StringValue { + return iacTypes.StringDefault(defaultValue, r.Metadata()) +} + +func (r *Resource) BoolDefault(defaultValue bool) iacTypes.BoolValue { + return iacTypes.BoolDefault(defaultValue, r.Metadata()) +} + +func (r *Resource) IntDefault(defaultValue int) iacTypes.IntValue { + return iacTypes.IntDefault(defaultValue, r.Metadata()) +} + +func (r *Resource) inferBool(prop *Property, defaultValue bool) iacTypes.BoolValue { + if prop.IsString() { + if prop.EqualTo("true", IgnoreCase) { + return iacTypes.Bool(true, prop.Metadata()) + } + if prop.EqualTo("yes", IgnoreCase) { + return iacTypes.Bool(true, prop.Metadata()) + } + if prop.EqualTo("1", IgnoreCase) { + return iacTypes.Bool(true, prop.Metadata()) + } + if prop.EqualTo("false", IgnoreCase) { + return iacTypes.Bool(false, prop.Metadata()) + } + if prop.EqualTo("no", IgnoreCase) { + return iacTypes.Bool(false, prop.Metadata()) + } + if prop.EqualTo("0", IgnoreCase) { + return iacTypes.Bool(false, prop.Metadata()) + } + } + + if prop.IsInt() { + if prop.EqualTo(0) { + return iacTypes.Bool(false, prop.Metadata()) + } + if prop.EqualTo(1) { + return iacTypes.Bool(true, prop.Metadata()) + } + } + + return r.BoolDefault(defaultValue) +} diff --git a/pkg/iac/scanners/cloudformation/parser/resource_test.go b/pkg/iac/scanners/cloudformation/parser/resource_test.go new file mode 100644 index 000000000000..a202b7eff700 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/resource_test.go @@ -0,0 +1,58 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" +) + +func Test_GetProperty_PropIsFunction(t *testing.T) { + resource := Resource{ + typ: "AWS::S3::Bucket", + properties: map[string]*Property{ + "BucketName": { + Type: cftypes.String, + Value: "mybucket", + }, + "VersioningConfiguration": { + Type: cftypes.Map, + Value: map[string]*Property{ + "Fn::If": { + Type: cftypes.List, + Value: []*Property{ + { + Type: cftypes.Bool, + Value: false, + }, + { + Type: cftypes.Map, + Value: map[string]*Property{ + "Status": { + Type: cftypes.String, + Value: "Enabled", + }, + }, + }, + { + Type: cftypes.Map, + Value: map[string]*Property{ + "Status": { + Type: cftypes.String, + Value: "Suspended", + }, + }, + }, + }, + }, + }, + }, + }, + } + + prop := resource.GetProperty("VersioningConfiguration.Status") + require.NotNil(t, prop) + require.True(t, prop.IsString()) + require.Equal(t, "Suspended", prop.AsString()) +} diff --git a/pkg/iac/scanners/cloudformation/parser/util.go b/pkg/iac/scanners/cloudformation/parser/util.go new file mode 100644 index 000000000000..14d60c12d4fe --- /dev/null +++ b/pkg/iac/scanners/cloudformation/parser/util.go @@ -0,0 +1,105 @@ +package parser + +import ( + "strconv" + + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/cftypes" + "github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes/parser" +) + +func setPropertyValueFromYaml(node *yaml.Node, propertyData *Property) error { + if IsIntrinsicFunc(node) { + var newContent []*yaml.Node + + newContent = append(newContent, &yaml.Node{ + Tag: "!!str", + Value: getIntrinsicTag(node.Tag), + Kind: yaml.ScalarNode, + }) + + newContent = createNode(node, newContent) + + node.Tag = string(parser.TagMap) + node.Kind = yaml.MappingNode + node.Content = newContent + } + + if node.Content == nil { + + switch node.Tag { + case "!!int": + propertyData.Type = cftypes.Int + propertyData.Value, _ = strconv.Atoi(node.Value) + case "!!bool": + propertyData.Type = cftypes.Bool + propertyData.Value, _ = strconv.ParseBool(node.Value) + case "!!float": + propertyData.Type = cftypes.Float64 + propertyData.Value, _ = strconv.ParseFloat(node.Value, 64) + case "!!str", "!!string": + propertyData.Type = cftypes.String + propertyData.Value = node.Value + } + return nil + } + + switch node.Tag { + case string(parser.TagMap): + var childData map[string]*Property + if err := node.Decode(&childData); err != nil { + return err + } + propertyData.Type = cftypes.Map + propertyData.Value = childData + return nil + case "!!seq": + var childData []*Property + if err := node.Decode(&childData); err != nil { + return err + } + propertyData.Type = cftypes.List + propertyData.Value = childData + return nil + } + + return nil +} + +func createNode(node *yaml.Node, newContent []*yaml.Node) []*yaml.Node { + if node.Content == nil { + newContent = append(newContent, &yaml.Node{ + Tag: "!!str", + Value: node.Value, + Kind: yaml.ScalarNode, + }) + } else { + + newNode := &yaml.Node{ + Content: node.Content, + Kind: node.Kind, + } + + switch node.Kind { + case yaml.SequenceNode: + newNode.Tag = "!!seq" + case yaml.MappingNode: + newNode.Tag = string(parser.TagMap) + case yaml.ScalarNode: + default: + newNode.Tag = node.Tag + } + newContent = append(newContent, newNode) + } + return newContent +} + +func calculateEndLine(node *yaml.Node) int { + if node.Content == nil { + return node.Line + } + + return calculateEndLine(node.Content[len(node.Content)-1]) + +} diff --git a/pkg/iac/scanners/cloudformation/scanner.go b/pkg/iac/scanners/cloudformation/scanner.go new file mode 100644 index 000000000000..d654476fe10d --- /dev/null +++ b/pkg/iac/scanners/cloudformation/scanner.go @@ -0,0 +1,130 @@ +package cloudformation + +import ( + "context" + "fmt" + "io/fs" + + adapter "github.com/aquasecurity/trivy/pkg/iac/adapters/cloudformation" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +func WithParameters(params map[string]any) options.ScannerOption { + return func(cs options.ConfigurableScanner) { + if s, ok := cs.(*Scanner); ok { + s.addParserOption(parser.WithParameters(params)) + } + } +} + +func WithParameterFiles(files ...string) options.ScannerOption { + return func(cs options.ConfigurableScanner) { + if s, ok := cs.(*Scanner); ok { + s.addParserOption(parser.WithParameterFiles(files...)) + } + } +} + +func WithConfigsFS(fsys fs.FS) options.ScannerOption { + return func(cs options.ConfigurableScanner) { + if s, ok := cs.(*Scanner); ok { + s.addParserOption(parser.WithConfigsFS(fsys)) + } + } +} + +var _ scanners.FSScanner = (*Scanner)(nil) +var _ options.ConfigurableScanner = (*Scanner)(nil) + +type Scanner struct { + *rego.RegoScannerProvider + logger *log.Logger + parser *parser.Parser + options []options.ScannerOption + parserOptions []parser.Option +} + +func (s *Scanner) addParserOption(opt parser.Option) { + s.parserOptions = append(s.parserOptions, opt) +} + +func (s *Scanner) Name() string { + return "CloudFormation" +} + +// New creates a new Scanner +func New(opts ...options.ScannerOption) *Scanner { + s := &Scanner{ + RegoScannerProvider: rego.NewRegoScannerProvider(opts...), + options: opts, + logger: log.WithPrefix("cloudformation scanner"), + } + for _, opt := range opts { + opt(s) + } + s.parser = parser.New(s.parserOptions...) + return s +} + +func (s *Scanner) ScanFS(ctx context.Context, fsys fs.FS, dir string) (results scan.Results, err error) { + + contexts, err := s.parser.ParseFS(ctx, fsys, dir) + if err != nil { + return nil, err + } + + if len(contexts) == 0 { + return nil, nil + } + + rs, err := s.InitRegoScanner(fsys, s.options) + if err != nil { + return nil, fmt.Errorf("init rego scanner: %w", err) + } + + for _, cfCtx := range contexts { + if cfCtx == nil { + continue + } + fileResults, err := s.scanFileContext(ctx, rs, cfCtx, fsys) + if err != nil { + return nil, err + } + results = append(results, fileResults...) + } + return results, nil +} + +func (s *Scanner) scanFileContext(ctx context.Context, regoScanner *rego.Scanner, cfCtx *parser.FileContext, fsys fs.FS) (scan.Results, error) { + state := adapter.Adapt(*cfCtx) + if state == nil { + return nil, nil + } + + results, err := regoScanner.ScanInput(ctx, types.SourceCloud, rego.Input{ + Path: cfCtx.Metadata().Range().GetFilename(), + FS: fsys, + Contents: state.ToRego(), + }) + if err != nil { + return nil, fmt.Errorf("rego scan error: %w", err) + } + + // ignore a result based on user input + results.Ignore(cfCtx.Ignores, nil) + + for _, ignored := range results.GetIgnored() { + s.logger.Info("Ignore finding", + log.String("rule", ignored.Rule().CanonicalID()), + log.String("range", ignored.Range().String()), + ) + } + + return results, nil +} diff --git a/pkg/iac/scanners/cloudformation/scanner_test.go b/pkg/iac/scanners/cloudformation/scanner_test.go new file mode 100644 index 000000000000..e5b2325923a0 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/scanner_test.go @@ -0,0 +1,225 @@ +package cloudformation + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/framework" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +func Test_BasicScan(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "/code/main.yaml": `--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: public-bucket + +`, + "/rules/rule.rego": `package builtin.dockerfile.DS006 + +__rego_metadata__ := { + "id": "DS006", + "title": "COPY '--from' referring to the current image", + "short_code": "no-self-referencing-copy-from", + "version": "v1.0.0", + "severity": "CRITICAL", + "type": "Dockerfile Security Check", + "description": "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself.", + "recommended_actions": "Change the '--from' so that it will not refer to itself", + "url": "https://docs.docker.com/develop/develop-images/multistage-build/", +} + +__rego_input__ := { + "combine": false, + "selector": [{"type": "defsec", "subtypes": [{"service": "s3", "provider": "aws"}]}], +} + +deny[res] { + res := { + "msg": "oh no", + "filepath": "code/main.yaml", + "startline": 6, + "endline": 6, + } +} + +`, + }) + + scanner := New(rego.WithPolicyDirs("rules")) + + results, err := scanner.ScanFS(t.Context(), fs, "code") + require.NoError(t, err) + + require.Len(t, results.GetFailed(), 1) + + assert.Equal(t, scan.Rule{ + ID: "DS006", + ShortCode: "no-self-referencing-copy-from", + Summary: "COPY '--from' referring to the current image", + Explanation: "COPY '--from' should not mention the current FROM alias, since it is impossible to copy from itself.", + Impact: "", + Resolution: "Change the '--from' so that it will not refer to itself", + Provider: "cloud", + Service: "general", + Links: []string{"https://docs.docker.com/develop/develop-images/multistage-build/"}, + Severity: "CRITICAL", + Terraform: &scan.EngineMetadata{}, + CloudFormation: &scan.EngineMetadata{}, + RegoPackage: "data.builtin.dockerfile.DS006", + Frameworks: map[framework.Framework][]string{ + framework.Default: {}, + }, + }, results.GetFailed()[0].Rule()) + + failure := results.GetFailed()[0] + actualCode, err := failure.GetCode() + require.NoError(t, err) + for i := range actualCode.Lines { + actualCode.Lines[i].Highlighted = "" + } + assert.Equal(t, []scan.Line{ + { + Number: 6, + Content: " BucketName: public-bucket", + IsCause: true, + FirstCause: true, + LastCause: true, + Annotation: "", + }, + }, actualCode.Lines) +} + +const bucketNameCheck = `# METADATA +# title: "test rego" +# scope: package +# schemas: +# - input: schema["cloud"] +# custom: +# id: AWS-001 +# provider: aws +# service: s3 +# severity: LOW +# input: +# selector: +# - type: cloud +# subtypes: +# - service: s3 +# provider: aws +package user.aws.aws001 + +deny[res] { + bucket := input.aws.s3.buckets[_] + bucket.name.value == "test-bucket" + res := result.new("Denied", bucket.name) +} + +deny[res] { + bucket := input.aws.s3.buckets[_] + algo := bucket.encryption.algorithm + algo.value == "AES256" + res := result.new("Denied", algo) +} +` + +func TestIgnore(t *testing.T) { + tests := []struct { + name string + src string + ignored int + }{ + { + name: "without ignore", + src: `--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket +`, + ignored: 0, + }, + { + name: "rule before resource", + src: `--- +Resources: +#trivy:ignore:AWS-001 + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket +`, + ignored: 1, + }, + { + name: "rule before property", + src: `--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: +#trivy:ignore:AWS-001 + BucketName: test-bucket +`, + ignored: 1, + }, + { + name: "rule on the same line with the property", + src: `--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket #trivy:ignore:AWS-001 +`, + ignored: 1, + }, + { + name: "rule on the same line with the nested property", + src: `--- +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: test-bucket + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 #trivy:ignore:AWS-001 +`, + ignored: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := testutil.CreateFS(map[string]string{ + "/code/main.yaml": tt.src, + }) + + scanner := New( + rego.WithEmbeddedPolicies(false), + rego.WithPolicyReader(strings.NewReader(bucketNameCheck)), + rego.WithPolicyNamespaces("user"), + ) + + results, err := scanner.ScanFS(t.Context(), fsys, "code") + require.NoError(t, err) + + if tt.ignored == 0 { + require.Len(t, results.GetFailed(), 1) + } else { + assert.Len(t, results.GetIgnored(), tt.ignored) + } + }) + } +} diff --git a/pkg/iac/scanners/cloudformation/test/cf_scanning_test.go b/pkg/iac/scanners/cloudformation/test/cf_scanning_test.go new file mode 100644 index 000000000000..b838432d962f --- /dev/null +++ b/pkg/iac/scanners/cloudformation/test/cf_scanning_test.go @@ -0,0 +1,30 @@ +package test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation" +) + +func Test_basic_cloudformation_scanning(t *testing.T) { + cfScanner := cloudformation.New(rego.WithEmbeddedPolicies(true), rego.WithEmbeddedLibraries(true)) + + results, err := cfScanner.ScanFS(t.Context(), os.DirFS("./examples/bucket"), ".") + require.NoError(t, err) + + assert.NotEmpty(t, results.GetFailed()) +} + +func Test_cloudformation_scanning_has_expected_errors(t *testing.T) { + cfScanner := cloudformation.New(rego.WithEmbeddedPolicies(true), rego.WithEmbeddedLibraries(true)) + + results, err := cfScanner.ScanFS(t.Context(), os.DirFS("./examples/bucket"), ".") + require.NoError(t, err) + + assert.NotEmpty(t, results.GetFailed()) +} diff --git a/pkg/iac/scanners/cloudformation/test/examples/bucket/bucket.yaml b/pkg/iac/scanners/cloudformation/test/examples/bucket/bucket.yaml new file mode 100644 index 000000000000..21f1c25042b0 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/test/examples/bucket/bucket.yaml @@ -0,0 +1,24 @@ +--- +AWSTemplateFormatVersion: "2010-09-09" +Description: An example Stack for a bucket +Parameters: + BucketName: + Type: String + Default: naughty-bucket + EncryptBucket: + Type: Boolean + Default: false +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: + Ref: BucketName + PublicAccessBlockConfiguration: + BlockPublicAcls: false + BlockPublicPolicy: false + IgnorePublicAcls: true + RestrictPublicBuckets: false + BucketEncryption: + ServerSideEncryptionConfiguration: + - BucketKeyEnabled: !Ref EncryptBucket diff --git a/pkg/iac/scanners/cloudformation/test/examples/ignores/bucket_with_ignores.yaml b/pkg/iac/scanners/cloudformation/test/examples/ignores/bucket_with_ignores.yaml new file mode 100644 index 000000000000..ec5e8a8d7661 --- /dev/null +++ b/pkg/iac/scanners/cloudformation/test/examples/ignores/bucket_with_ignores.yaml @@ -0,0 +1,24 @@ +--- +AWSTemplateFormatVersion: "2010-09-09" +Description: An example Stack for a bucket +Parameters: + BucketName: + Type: String + Default: naughty-bucket + EncryptBucket: + Type: Boolean + Default: false +Resources: + S3Bucket: + Type: 'AWS::S3::Bucket' + Properties: + BucketName: + Ref: BucketName + PublicAccessBlockConfiguration: + BlockPublicAcls: false + BlockPublicPolicy: false # cfsec:ignore:AVD-AWS-0087 + IgnorePublicAcls: true + RestrictPublicBuckets: false + BucketEncryption: + ServerSideEncryptionConfiguration: + - BucketKeyEnabled: !Ref EncryptBucket diff --git a/pkg/iac/scanners/cloudformation/test/examples/roles/roles.yml b/pkg/iac/scanners/cloudformation/test/examples/roles/roles.yml new file mode 100644 index 000000000000..5b927457762b --- /dev/null +++ b/pkg/iac/scanners/cloudformation/test/examples/roles/roles.yml @@ -0,0 +1,51 @@ +Resources: + LambdaAPIRole: + Type: "AWS::IAM::Role" + Properties: + RoleName: "${self:service}-${self:provider.stage}-LambdaAPI" + Policies: + - PolicyName: "${self:service}-${self:provider.stage}-lambda" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "logs:CreateLogStream" + - "logs:CreateLogGroup" + - "logs:PutLogEvents" + Resource: !Sub "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/${self:service}-${self:provider.stage}*:*" + - !If + - EnableCrossAccountSnsPublish + - PolicyName: "${self:service}-${self:provider.stage}-asngen-sns-publish" + PolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Action: + - "SNS:Publish" + Resource: + - !Sub "arn:aws:sns:${self:provider.region}:${self:provider.itopia_account_id}:${self:provider.stage}-*-PurchaseOrder.fifo" + - !Sub "arn:aws:sns:${self:provider.region}:${self:provider.itopia_account_id}:${self:provider.stage}-*-Vendor.fifo" + - !Sub "arn:aws:sns:${self:provider.region}:${self:provider.itopia_account_id}:${self:provider.stage}-*-Customer.fifo" + - !Sub "arn:aws:sns:${self:provider.region}:${self:provider.itopia_account_id}:${self:provider.stage}-*-Manufacturer.fifo" + - !Sub "arn:aws:sns:${self:provider.region}:${self:provider.itopia_account_id}:${self:provider.stage}-*-ManufacturerItem.fifo" + - !Sub "arn:aws:sns:${self:provider.region}:${self:provider.itopia_account_id}:${self:provider.stage}-*-Item.fifo" + - !Sub "arn:aws:sns:${self:provider.region}:${self:provider.itopia_account_id}:${self:provider.stage}-*-VendorItem.fifo" + - !Ref "AWS::NoValue" + AssumeRolePolicyDocument: + Version: "2012-10-17" + Statement: + - Effect: Allow + Principal: + Service: + - "lambda.amazonaws.com" + Action: + - "sts:AssumeRole" + + + + +Conditions: + EnableCrossAccountSnsPublish: !Equals + - ${env:ALLOW_SNS_PUBLISH, true} + - true diff --git a/pkg/iac/scanners/dockerfile/parser/parser.go b/pkg/iac/scanners/dockerfile/parser/parser.go new file mode 100644 index 000000000000..d3f56a903fa6 --- /dev/null +++ b/pkg/iac/scanners/dockerfile/parser/parser.go @@ -0,0 +1,190 @@ +package parser + +import ( + "context" + "io" + "slices" + "strings" + + "github.com/moby/buildkit/frontend/dockerfile/instructions" + "github.com/moby/buildkit/frontend/dockerfile/parser" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/iac/providers/dockerfile" +) + +type Parser struct { + strict bool +} + +type Option func(p *Parser) + +// WithStrict returns a Parser option that enables strict parsing mode. +// By default, the Parser runs in non-strict mode, where unknown flags are ignored. +// Calling this option ensures that unknown flags cause an error. +func WithStrict() Option { + return func(p *Parser) { + p.strict = true + } +} + +func NewParser(opts ...Option) *Parser { + p := &Parser{} + for _, opt := range opts { + opt(p) + } + return p +} + +func (p *Parser) Parse(_ context.Context, r io.Reader, path string) ([]*dockerfile.Dockerfile, error) { + parsed, err := parser.Parse(r) + if err != nil { + return nil, xerrors.Errorf("dockerfile parse error: %w", err) + } + + var ( + parsedFile dockerfile.Dockerfile + stage dockerfile.Stage + stageIndex int + ) + + fromValue := "args" + for _, child := range parsed.AST.Children { + child.Value = strings.ToLower(child.Value) + + instr, err := p.parseInstruction(child) + if err != nil { + return nil, xerrors.Errorf("parse dockerfile instruction: %w", err) + } + + if _, ok := instr.(*instructions.Stage); ok { + if len(stage.Commands) > 0 { + parsedFile.Stages = append(parsedFile.Stages, stage) + } + if fromValue != "args" { + stageIndex++ + } + fromValue = strings.TrimSpace(strings.TrimPrefix(child.Original, "FROM ")) + stage = dockerfile.Stage{ + Name: fromValue, + } + } + + cmd := dockerfile.Command{ + Cmd: child.Value, + Original: child.Original, + Flags: child.Flags, + Stage: stageIndex, + Path: path, + StartLine: child.StartLine, + EndLine: child.EndLine, + } + + // processing statement with sub-statement + // example: ONBUILD RUN foo bar + // https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#onbuild + if child.Next != nil && len(child.Next.Children) > 0 { + cmd.SubCmd = child.Next.Children[0].Value + child = child.Next.Children[0] + } + + // mark if the instruction is in exec form + // https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#exec-form + cmd.JSON = child.Attributes["json"] + + // heredoc may contain a script that will be executed in the shell, so we need to process it + // https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md#here-documents + if len(child.Heredocs) > 0 && child.Next != nil { + cmd.Original = originalFromHeredoc(child) + cmd.Value = []string{processHeredoc(child)} + } else { + for n := child.Next; n != nil; n = n.Next { + cmd.Value = append(cmd.Value, n.Value) + } + } + + stage.Commands = append(stage.Commands, cmd) + + } + if len(stage.Commands) > 0 { + parsedFile.Stages = append(parsedFile.Stages, stage) + } + + return []*dockerfile.Dockerfile{&parsedFile}, nil +} + +func (p *Parser) parseInstruction(child *parser.Node) (any, error) { + for { + instr, err := instructions.ParseInstruction(child) + if err == nil { + return instr, nil + } else if p.strict { + return nil, xerrors.Errorf("parse instruction %q: %w", child.Value, err) + } + + flagName := extractUnknownFlag(err.Error()) + if flagName == "" { + return nil, xerrors.Errorf("parse instruction %q: %w", child.Value, err) + } + + filtered := slices.DeleteFunc(child.Flags, func(flag string) bool { + return strings.HasPrefix(flag, flagName) + }) + + if len(filtered) == len(child.Flags) { + return nil, xerrors.Errorf("cannot remove unknown flag %q from flags %v", flagName, child.Flags) + } + child.Flags = filtered + } +} + +func extractUnknownFlag(errMsg string) string { + after, ok := strings.CutPrefix(errMsg, "unknown flag: ") + if !ok { + return "" + } + + flagName, _, _ := strings.Cut(after, " ") + return flagName +} + +func originalFromHeredoc(node *parser.Node) string { + var sb strings.Builder + sb.WriteString(node.Original) + sb.WriteRune('\n') + for i, heredoc := range node.Heredocs { + sb.WriteString(heredoc.Content) + sb.WriteString(heredoc.Name) + if i != len(node.Heredocs)-1 { + sb.WriteRune('\n') + } + } + + return sb.String() +} + +// heredoc processing taken from here +// https://github.com/moby/buildkit/blob/9a39e2c112b7c98353c27e64602bc08f31fe356e/frontend/dockerfile/dockerfile2llb/convert.go#L1200 +func processHeredoc(node *parser.Node) string { + if parser.MustParseHeredoc(node.Next.Value) == nil || strings.HasPrefix(node.Heredocs[0].Content, "#!") { + // more complex heredoc is passed to the shell as is + var sb strings.Builder + sb.WriteString(node.Next.Value) + for _, heredoc := range node.Heredocs { + sb.WriteRune('\n') + sb.WriteString(heredoc.Content) + sb.WriteString(heredoc.Name) + } + return sb.String() + } + + // simple heredoc and the content is run in a shell + content := node.Heredocs[0].Content + if node.Heredocs[0].Chomp { + content = parser.ChompHeredocContent(content) + } + + content = strings.ReplaceAll(content, "\r\n", "\n") + cmds := strings.Split(strings.TrimSuffix(content, "\n"), "\n") + return strings.Join(cmds, " ; ") +} diff --git a/pkg/iac/scanners/dockerfile/parser/parser_test.go b/pkg/iac/scanners/dockerfile/parser/parser_test.go new file mode 100644 index 000000000000..904cac2f6d3a --- /dev/null +++ b/pkg/iac/scanners/dockerfile/parser/parser_test.go @@ -0,0 +1,188 @@ +package parser_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/providers/dockerfile" + "github.com/aquasecurity/trivy/pkg/iac/scanners/dockerfile/parser" +) + +func Test_Parser(t *testing.T) { + input := `FROM ubuntu:18.04 +COPY . /app +RUN make /app +CMD python /app/app.py +` + p := parser.NewParser() + dfs, err := p.Parse(t.Context(), strings.NewReader(input), "Dockerfile") + require.NoError(t, err) + require.Len(t, dfs, 1) + + df := dfs[0] + assert.Len(t, df.Stages, 1) + + assert.Equal(t, "ubuntu:18.04", df.Stages[0].Name) + commands := df.Stages[0].Commands + assert.Len(t, commands, 4) + + // FROM ubuntu:18.04 + assert.Equal(t, "from", commands[0].Cmd) + assert.Equal(t, "ubuntu:18.04", commands[0].Value[0]) + assert.Equal(t, "Dockerfile", commands[0].Path) + assert.Equal(t, 1, commands[0].StartLine) + assert.Equal(t, 1, commands[0].EndLine) + + // COPY . /app + assert.Equal(t, "copy", commands[1].Cmd) + assert.Equal(t, ". /app", strings.Join(commands[1].Value, " ")) + assert.Equal(t, "Dockerfile", commands[1].Path) + assert.Equal(t, 2, commands[1].StartLine) + assert.Equal(t, 2, commands[1].EndLine) + + // RUN make /app + assert.Equal(t, "run", commands[2].Cmd) + assert.Equal(t, "make /app", commands[2].Value[0]) + assert.Equal(t, "Dockerfile", commands[2].Path) + assert.Equal(t, 3, commands[2].StartLine) + assert.Equal(t, 3, commands[2].EndLine) + + // CMD python /app/app.py + assert.Equal(t, "cmd", commands[3].Cmd) + assert.Equal(t, "python /app/app.py", commands[3].Value[0]) + assert.Equal(t, "Dockerfile", commands[3].Path) + assert.Equal(t, 4, commands[3].StartLine) + assert.Equal(t, 4, commands[3].EndLine) +} + +func TestParserUnknownFlags(t *testing.T) { + input := `FROM ubuntu:18.04 +COPY --foo --chown=1 --bar=./test . /app +RUN --baz --baz --network=host make /app +ONBUILD RUN --foo make /app +CMD python /app/app.py +` + p := parser.NewParser() + dfs, err := p.Parse(t.Context(), strings.NewReader(input), "Dockerfile") + require.NoError(t, err) + require.Len(t, dfs, 1) + + expected := &dockerfile.Dockerfile{ + Stages: []dockerfile.Stage{ + { + Name: "ubuntu:18.04", + Commands: []dockerfile.Command{ + { + Cmd: "from", + Value: []string{"ubuntu:18.04"}, + Flags: []string{}, + Original: "FROM ubuntu:18.04", + Path: "Dockerfile", + StartLine: 1, + EndLine: 1, + }, + { + Cmd: "copy", + Value: []string{".", "/app"}, + Flags: []string{"--chown=1"}, + Original: "COPY --foo --chown=1 --bar=./test . /app", + Path: "Dockerfile", + StartLine: 2, + EndLine: 2, + }, + { + Cmd: "run", + Value: []string{"make /app"}, + Flags: []string{"--network=host"}, + Original: "RUN --baz --baz --network=host make /app", + Path: "Dockerfile", + StartLine: 3, + EndLine: 3, + }, + { + Cmd: "onbuild", + SubCmd: "RUN", + Value: []string{"make /app"}, + Flags: []string{}, + Original: "ONBUILD RUN --foo make /app", + Path: "Dockerfile", + StartLine: 4, + EndLine: 4, + }, + { + Cmd: "cmd", + Value: []string{"python /app/app.py"}, + Flags: []string{}, + Original: "CMD python /app/app.py", + Path: "Dockerfile", + StartLine: 5, + EndLine: 5, + }, + }, + }, + }, + } + assert.Equal(t, expected, dfs[0]) +} + +func Test_ParseHeredocs(t *testing.T) { + tests := []struct { + name string + src string + expected string + }{ + { + name: "multi-line script", + src: `RUN < /tmp/output && echo 'done' +hello +mr +potato +EOF`, + expected: "cat < /tmp/output && echo 'done'\nhello\nmr\npotato\nEOF", + }, + { + name: "redirect to file", + src: `RUN < /etc/config.yaml +key1: value1 +key2: value2 +EOF`, + expected: "< /etc/config.yaml\nkey1: value1\nkey2: value2\nEOF", + }, + { + name: "with a shebang", + src: `RUN < 1 { + return manifestFilePathParts[1] + } + return manifestFilePathParts[0] +} diff --git a/pkg/iac/scanners/helm/parser/parser_tar.go b/pkg/iac/scanners/helm/parser/parser_tar.go new file mode 100644 index 000000000000..63da1aa97d60 --- /dev/null +++ b/pkg/iac/scanners/helm/parser/parser_tar.go @@ -0,0 +1,153 @@ +package parser + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/iac/detection" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/mapfs" +) + +var errSkipFS = errors.New("skip parse FS") + +func (p *Parser) unpackArchive(srcFS fs.FS, targetFS *mapfs.FS, archivePath string) error { + file, err := srcFS.Open(archivePath) + if err != nil { + return fmt.Errorf("failed to open tar: %w", err) + } + defer file.Close() + + var tr *tar.Reader + + if detection.IsZip(archivePath) { + zipped, err := gzip.NewReader(file) + if err != nil { + return fmt.Errorf("failed to create gzip reader: %w", err) + } + defer zipped.Close() + tr = tar.NewReader(zipped) + } else { + tr = tar.NewReader(file) + } + + checkExistedChart := true + symlinks := make(map[string]string) + + for { + header, err := tr.Next() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return fmt.Errorf("failed to get next entry: %w", err) + } + + name := filepath.ToSlash(header.Name) + + if checkExistedChart { + // Do not add archive files to FS if the chart already exists + // This can happen when the source chart is located next to an archived chart (with the `helm package` command) + // The first level folder in the archive is equal to the Chart name + if _, err := fs.Stat(srcFS, path.Clean(path.Dir(archivePath)+"/"+path.Dir(name))); err == nil { + return errSkipFS + } + checkExistedChart = false + } + + // get the individual path and extract to the current directory + targetPath := archiveEntryPath(archivePath, name) + + switch header.Typeflag { + case tar.TypeDir: + if err := targetFS.MkdirAll(targetPath, os.FileMode(header.Mode)); err != nil && !errors.Is(err, fs.ErrExist) { + return err + } + case tar.TypeReg: + data, err := io.ReadAll(tr) + if err != nil { + return fmt.Errorf("read file: %w", err) + } + + p.logger.Debug("Unpacking tar entry", log.FilePath(targetPath)) + if err := writeFile(targetFS, data, targetPath); err != nil { + return err + } + case tar.TypeSymlink: + link := filepath.ToSlash(header.Linkname) + if path.IsAbs(link) { + p.logger.Debug("Symlink is absolute, skipping", log.String("link", link)) + continue + } + + symlinks[targetPath] = path.Join(path.Dir(targetPath), link) // nolint:gosec // virtual file system is used + default: + return fmt.Errorf("header type %q is not supported", header.Typeflag) + } + } + + for target, link := range symlinks { + p.logger.Debug("Copying symlink as file/dir", + log.String("target", target), log.String("link", link)) + if err := copyPath(targetFS, link, target); err != nil { + return fmt.Errorf("copy symlink error: %w", err) + } + } + + return nil +} + +func archiveEntryPath(archivePath, name string) string { + return path.Join(path.Dir(archivePath), path.Clean(name)) +} + +func writeFile(fsys *mapfs.FS, data []byte, dst string) error { + if err := fsys.MkdirAll(path.Dir(dst), fs.ModePerm); err != nil && !errors.Is(err, fs.ErrExist) { + return fmt.Errorf("mkdir error: %w", err) + } + return fsys.WriteVirtualFile(dst, data, fs.ModePerm) +} + +func copyPath(fsys *mapfs.FS, src, dst string) error { + fi, err := fsys.Stat(src) + if err != nil { + // the file is missing, just skip it + return nil + } + if fi.IsDir() { + return copyDir(fsys, src, dst) + } + data, err := fs.ReadFile(fsys, src) + if err != nil { + return fmt.Errorf("read file: %w", err) + } + return writeFile(fsys, data, dst) +} + +func copyDir(fsys *mapfs.FS, src, dst string) error { + walkFn := func(filePath string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } + + if entry.IsDir() { + return nil + } + + target := path.Join(dst, filePath[len(src):]) + data, err := fs.ReadFile(fsys, filePath) + if err != nil { + return fmt.Errorf("read file: %w", err) + } + return writeFile(fsys, data, target) + } + + return fs.WalkDir(fsys, src, walkFn) +} diff --git a/pkg/iac/scanners/helm/parser/parser_test.go b/pkg/iac/scanners/helm/parser/parser_test.go new file mode 100644 index 000000000000..59f1d0c38631 --- /dev/null +++ b/pkg/iac/scanners/helm/parser/parser_test.go @@ -0,0 +1,81 @@ +package parser + +import ( + "os" + "path/filepath" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseFS(t *testing.T) { + t.Run("source chart is located next to an same archived chart", func(t *testing.T) { + p, err := New(".") + require.NoError(t, err) + require.NoError(t, p.ParseFS(t.Context(), os.DirFS(filepath.Join("testdata", "chart-and-archived-chart")), ".")) + + expectedFiles := []string{ + "my-chart/Chart.yaml", + "my-chart/templates/pod.yaml", + } + assert.ElementsMatch(t, expectedFiles, lo.Keys(p.filepaths)) + }) + + t.Run("archive with symlinks", func(t *testing.T) { + // mkdir -p chart && cd $_ + // touch Chart.yaml + // mkdir -p dir && cp -p Chart.yaml dir/Chart.yaml + // mkdir -p sym-to-file && ln -s ../Chart.yaml sym-to-file/Chart.yaml + // ln -s dir sym-to-dir + // mkdir rec-sym && touch rec-sym/Chart.yaml + // ln -s . ./rec-sym/a + // cd .. && tar -czvf chart.tar.gz chart && rm -rf chart + p, err := New(".") + require.NoError(t, err) + + fsys := os.DirFS(filepath.Join("testdata", "archive-with-symlinks")) + require.NoError(t, p.ParseFS(t.Context(), fsys, "chart.tar.gz")) + + expectedFiles := []string{ + "chart/Chart.yaml", + "chart/dir/Chart.yaml", + "chart/rec-sym/Chart.yaml", + "chart/rec-sym/a/Chart.yaml", + "chart/sym-to-dir/Chart.yaml", + "chart/sym-to-file/Chart.yaml", + } + assert.ElementsMatch(t, expectedFiles, lo.Keys(p.filepaths)) + }) + + t.Run("chart with multiple archived deps", func(t *testing.T) { + p, err := New(".") + require.NoError(t, err) + + fsys := os.DirFS(filepath.Join("testdata", "multiple-archived-deps")) + require.NoError(t, p.ParseFS(t.Context(), fsys, ".")) + + expectedFiles := []string{ + "Chart.yaml", + "charts/common-2.26.0.tgz", + "charts/opentelemetry-collector-0.108.0.tgz", + } + assert.ElementsMatch(t, expectedFiles, lo.Keys(p.filepaths)) + }) + + t.Run("archives are not dependencies", func(t *testing.T) { + p, err := New(".") + require.NoError(t, err) + + fsys := os.DirFS(filepath.Join("testdata", "non-deps-archives")) + require.NoError(t, p.ParseFS(t.Context(), fsys, ".")) + + expectedFiles := []string{ + "Chart.yaml", + "backup_charts/wordpress-operator/Chart.yaml", + "backup_charts/mysql-operator/Chart.yaml", + } + assert.Subset(t, lo.Keys(p.filepaths), expectedFiles) + }) +} diff --git a/pkg/iac/scanners/helm/parser/testdata/archive-with-symlinks/chart.tar.gz b/pkg/iac/scanners/helm/parser/testdata/archive-with-symlinks/chart.tar.gz new file mode 100644 index 000000000000..a3183710c17f Binary files /dev/null and b/pkg/iac/scanners/helm/parser/testdata/archive-with-symlinks/chart.tar.gz differ diff --git a/pkg/iac/scanners/helm/parser/testdata/chart-and-archived-chart/my-chart-0.1.0.tgz b/pkg/iac/scanners/helm/parser/testdata/chart-and-archived-chart/my-chart-0.1.0.tgz new file mode 100644 index 000000000000..e36b2b474f3e Binary files /dev/null and b/pkg/iac/scanners/helm/parser/testdata/chart-and-archived-chart/my-chart-0.1.0.tgz differ diff --git a/pkg/iac/scanners/helm/parser/testdata/chart-and-archived-chart/my-chart/Chart.yaml b/pkg/iac/scanners/helm/parser/testdata/chart-and-archived-chart/my-chart/Chart.yaml new file mode 100644 index 000000000000..767f748a8d59 --- /dev/null +++ b/pkg/iac/scanners/helm/parser/testdata/chart-and-archived-chart/my-chart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: my-chart +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" diff --git a/pkg/iac/scanners/helm/parser/testdata/chart-and-archived-chart/my-chart/templates/pod.yaml b/pkg/iac/scanners/helm/parser/testdata/chart-and-archived-chart/my-chart/templates/pod.yaml new file mode 100644 index 000000000000..3649247c1bb1 --- /dev/null +++ b/pkg/iac/scanners/helm/parser/testdata/chart-and-archived-chart/my-chart/templates/pod.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 3 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 80 \ No newline at end of file diff --git a/pkg/iac/scanners/helm/parser/testdata/multiple-archived-deps/Chart.yaml b/pkg/iac/scanners/helm/parser/testdata/multiple-archived-deps/Chart.yaml new file mode 100644 index 000000000000..82d6c918088f --- /dev/null +++ b/pkg/iac/scanners/helm/parser/testdata/multiple-archived-deps/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +appVersion: "1.1" +description: Test Chart +name: y-chart +version: 1.0.0 +kubeVersion: ">=1.21" + +dependencies: + - name: common + repository: https://charts.bitnami.com/bitnami + version: 2.26.0 + - name: opentelemetry-collector + version: 0.108.0 + repository: https://open-telemetry.github.io/opentelemetry-helm-charts \ No newline at end of file diff --git a/pkg/iac/scanners/helm/parser/testdata/multiple-archived-deps/charts/common-2.26.0.tgz b/pkg/iac/scanners/helm/parser/testdata/multiple-archived-deps/charts/common-2.26.0.tgz new file mode 100644 index 000000000000..43f85ba36a91 Binary files /dev/null and b/pkg/iac/scanners/helm/parser/testdata/multiple-archived-deps/charts/common-2.26.0.tgz differ diff --git a/pkg/iac/scanners/helm/parser/testdata/multiple-archived-deps/charts/opentelemetry-collector-0.108.0.tgz b/pkg/iac/scanners/helm/parser/testdata/multiple-archived-deps/charts/opentelemetry-collector-0.108.0.tgz new file mode 100644 index 000000000000..0ea88fb48247 Binary files /dev/null and b/pkg/iac/scanners/helm/parser/testdata/multiple-archived-deps/charts/opentelemetry-collector-0.108.0.tgz differ diff --git a/pkg/iac/scanners/helm/parser/testdata/non-deps-archives/Chart.yaml b/pkg/iac/scanners/helm/parser/testdata/non-deps-archives/Chart.yaml new file mode 100644 index 000000000000..65225b732935 --- /dev/null +++ b/pkg/iac/scanners/helm/parser/testdata/non-deps-archives/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +appVersion: "1.1" +description: Test Chart +name: y-chart +version: 1.0.0 +kubeVersion: ">=1.21" + +dependencies: + - name: mysql-operator + repository: https://mysql.github.io/mysql-operator/ + version: 2.2.2 + - name: wordpress-operator + version: 0.12.4 + repository: https://helm-charts.bitpoke.io \ No newline at end of file diff --git a/pkg/iac/scanners/helm/parser/testdata/non-deps-archives/backup_charts/mysql-operator-2.2.2.tgz b/pkg/iac/scanners/helm/parser/testdata/non-deps-archives/backup_charts/mysql-operator-2.2.2.tgz new file mode 100644 index 000000000000..b80fc3c0425e Binary files /dev/null and b/pkg/iac/scanners/helm/parser/testdata/non-deps-archives/backup_charts/mysql-operator-2.2.2.tgz differ diff --git a/pkg/iac/scanners/helm/parser/testdata/non-deps-archives/backup_charts/wordpress-operator-0.12.4.tgz b/pkg/iac/scanners/helm/parser/testdata/non-deps-archives/backup_charts/wordpress-operator-0.12.4.tgz new file mode 100644 index 000000000000..4086e9f1a6ba Binary files /dev/null and b/pkg/iac/scanners/helm/parser/testdata/non-deps-archives/backup_charts/wordpress-operator-0.12.4.tgz differ diff --git a/pkg/iac/scanners/helm/parser/vals.go b/pkg/iac/scanners/helm/parser/vals.go new file mode 100644 index 000000000000..7e5a343fbef1 --- /dev/null +++ b/pkg/iac/scanners/helm/parser/vals.go @@ -0,0 +1,112 @@ +package parser + +import ( + "fmt" + "io" + "maps" + "net/url" + "os" + "strings" + + "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/getter" + "helm.sh/helm/v3/pkg/strvals" +) + +type ValueOptions struct { + ValueFiles []string + StringValues []string + Values []string + FileValues []string +} + +// MergeValues merges values from files specified via -f/--values and directly +// via --set, --set-string, or --set-file, marshaling them to YAML +func (opts *ValueOptions) MergeValues() (map[string]any, error) { + base := make(map[string]any) + + // User specified a values files via -f/--values + for _, filePath := range opts.ValueFiles { + currentMap := make(map[string]any) + + bytes, err := readFile(filePath) + if err != nil { + return nil, err + } + + if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { + return nil, fmt.Errorf("failed to parse %s: %w", filePath, err) + } + // Merge with the previous map + base = mergeMaps(base, currentMap) + } + + // User specified a value via --set + for _, value := range opts.Values { + if err := strvals.ParseInto(value, base); err != nil { + return nil, fmt.Errorf("failed parsing --set data, %w", err) + } + } + + // User specified a value via --set-string + for _, value := range opts.StringValues { + if err := strvals.ParseIntoString(value, base); err != nil { + return nil, fmt.Errorf("failed parsing --set-string data %w", err) + } + } + + // User specified a value via --set-file + for _, value := range opts.FileValues { + reader := func(rs []rune) (any, error) { + bytes, err := readFile(string(rs)) + if err != nil { + return nil, err + } + return string(bytes), err + } + if err := strvals.ParseIntoFile(value, base, reader); err != nil { + return nil, fmt.Errorf("failed parsing --set-file data: %w", err) + } + } + + return base, nil +} + +func mergeMaps(a, b map[string]any) map[string]any { + out := make(map[string]any, len(a)) + maps.Copy(out, a) + for k, v := range b { + if v, ok := v.(map[string]any); ok { + if bv, ok := out[k]; ok { + if bv, ok := bv.(map[string]any); ok { + out[k] = mergeMaps(bv, v) + continue + } + } + } + out[k] = v + } + return out +} + +// readFile load a file from stdin, the local directory, or a remote file with a url. +func readFile(filePath string) ([]byte, error) { + if strings.TrimSpace(filePath) == "-" { + return io.ReadAll(os.Stdin) + } + u, _ := url.Parse(filePath) + + // FIXME: maybe someone handle other protocols like ftp. + if u.Scheme == "http" || u.Scheme == "https" { + g, err := getter.NewHTTPGetter() + if err != nil { + return nil, err + } + data, err := g.Get(filePath, getter.WithURL(filePath)) + if err != nil { + return nil, err + } + return data.Bytes(), err + } + return os.ReadFile(filePath) +} diff --git a/pkg/iac/scanners/helm/scanner.go b/pkg/iac/scanners/helm/scanner.go new file mode 100644 index 000000000000..3ae66d838eae --- /dev/null +++ b/pkg/iac/scanners/helm/scanner.go @@ -0,0 +1,158 @@ +package helm + +import ( + "context" + "fmt" + "io/fs" + "path" + "path/filepath" + "strings" + + "helm.sh/helm/v3/pkg/chartutil" + + "github.com/aquasecurity/trivy/pkg/iac/detection" + "github.com/aquasecurity/trivy/pkg/iac/ignore" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners" + "github.com/aquasecurity/trivy/pkg/iac/scanners/helm/parser" + kparser "github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes/parser" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/mapfs" +) + +var _ scanners.FSScanner = (*Scanner)(nil) +var _ options.ConfigurableScanner = (*Scanner)(nil) + +type Scanner struct { + *rego.RegoScannerProvider + logger *log.Logger + options []options.ScannerOption + parserOptions []parser.Option +} + +// New creates a new Scanner +func New(opts ...options.ScannerOption) *Scanner { + s := &Scanner{ + RegoScannerProvider: rego.NewRegoScannerProvider(opts...), + options: opts, + logger: log.WithPrefix("helm scanner"), + } + + for _, option := range opts { + option(s) + } + return s +} + +func (s *Scanner) addParserOptions(opts ...parser.Option) { + s.parserOptions = append(s.parserOptions, opts...) +} + +func (s *Scanner) Name() string { + return "Helm" +} + +func (s *Scanner) ScanFS(ctx context.Context, fsys fs.FS, dir string) (scan.Results, error) { + var results []scan.Result + if err := fs.WalkDir(fsys, dir, func(filePath string, d fs.DirEntry, err error) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + if detection.IsArchive(filePath) { + scanResults, err := s.getScanResults(ctx, filePath, fsys) + if err != nil { + return err + } + results = append(results, scanResults...) + } else if path.Base(filePath) == chartutil.ChartfileName { + if scanResults, err := s.getScanResults(ctx, filepath.Dir(filePath), fsys); err != nil { + return err + } else { + results = append(results, scanResults...) + } + return fs.SkipDir + } + + return nil + }); err != nil { + return nil, err + } + + return results, nil + +} + +func (s *Scanner) getScanResults(ctx context.Context, path string, target fs.FS) (results []scan.Result, err error) { + helmParser, err := parser.New(path, s.parserOptions...) + if err != nil { + return nil, err + } + + if err := helmParser.ParseFS(ctx, target, path); err != nil { + return nil, err + } + + chartFiles, err := helmParser.RenderedChartFiles() + if err != nil { // not valid helm, maybe some other yaml etc., abort + s.logger.Error( + "Failed to render Chart files", + log.FilePath(path), log.Err(err), + ) + return nil, nil + } + + rs, err := s.InitRegoScanner(target, s.options) + if err != nil { + return nil, fmt.Errorf("init rego scanner: %w", err) + } + + for _, file := range chartFiles { + s.logger.Debug("Processing rendered chart file", log.FilePath(file.TemplateFilePath)) + + ignoreRules := ignore.Parse(file.ManifestContent, file.TemplateFilePath, helmParser.ChartSource) + manifests, err := kparser.Parse(ctx, strings.NewReader(file.ManifestContent), file.TemplateFilePath) + if err != nil { + return nil, fmt.Errorf("unmarshal yaml: %w", err) + } + + manifestFS := mapfs.New() + if err := manifestFS.MkdirAll(filepath.Dir(file.TemplateFilePath), fs.ModePerm); err != nil { + return nil, err + } + if err := manifestFS.WriteVirtualFile(file.TemplateFilePath, []byte(file.ManifestContent), fs.ModePerm); err != nil { + return nil, err + } + + for _, manifest := range manifests { + fileResults, err := rs.ScanInput(ctx, types.SourceKubernetes, rego.Input{ + Path: file.TemplateFilePath, + Contents: manifest.ToRego(), + FS: manifestFS, + }) + if err != nil { + return nil, fmt.Errorf("scanning error: %w", err) + } + + fileResults.SetSourceAndFilesystem(helmParser.ChartSource, manifestFS, detection.IsArchive(helmParser.ChartSource)) + fileResults.Ignore(ignoreRules, nil) + + results = append(results, fileResults...) + } + + } + return results, nil +} diff --git a/pkg/iac/scanners/helm/test/mysql/.helmignore b/pkg/iac/scanners/helm/test/mysql/.helmignore new file mode 100644 index 000000000000..f0c131944441 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/.helmignore @@ -0,0 +1,21 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj diff --git a/pkg/iac/scanners/helm/test/mysql/Chart.lock b/pkg/iac/scanners/helm/test/mysql/Chart.lock new file mode 100644 index 000000000000..2a6356005c25 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/Chart.lock @@ -0,0 +1,6 @@ +dependencies: +- name: common + repository: https://charts.bitnami.com/bitnami + version: 1.11.1 +digest: sha256:a000bcd4d4cdd813c67d633b5523b4a4cd478fb95f1cae665d9b0ba5c45b40e2 +generated: "2022-02-16T22:19:57.971058445Z" diff --git a/pkg/iac/scanners/helm/test/mysql/Chart.yaml b/pkg/iac/scanners/helm/test/mysql/Chart.yaml new file mode 100644 index 000000000000..7d5f5c6ce834 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/Chart.yaml @@ -0,0 +1,28 @@ +annotations: + category: Database +apiVersion: v2 +appVersion: 8.0.28 +dependencies: +- name: common + repository: https://charts.bitnami.com/bitnami + tags: + - bitnami-common + version: 1.x.x +description: MySQL is a fast, reliable, scalable, and easy to use open source relational + database system. Designed to handle mission-critical, heavy-load production applications. +home: https://github.com/bitnami/charts/tree/master/bitnami/mysql +icon: https://bitnami.com/assets/stacks/mysql/img/mysql-stack-220x234.png +keywords: +- mysql +- database +- sql +- cluster +- high availability +maintainers: +- email: containers@bitnami.com + name: Bitnami +name: mysql +sources: +- https://github.com/bitnami/bitnami-docker-mysql +- https://mysql.com +version: 8.8.26 diff --git a/pkg/iac/scanners/helm/test/mysql/README.md b/pkg/iac/scanners/helm/test/mysql/README.md new file mode 100644 index 000000000000..df9c2794f387 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/README.md @@ -0,0 +1,491 @@ + + +# MySQL packaged by Bitnami + +MySQL is a fast, reliable, scalable, and easy-to-use open source relational database system designed to handle mission-critical, heavy-load production applications. + +[Overview of MySQL](http://www.mysql.com) + +Trademarks: This software listing is packaged by Bitnami. The respective trademarks mentioned in the offering are owned by the respective companies, and use of them does not imply any affiliation or endorsement. + +## TL;DR + +```bash +$ helm repo add bitnami https://charts.bitnami.com/bitnami +$ helm install my-release bitnami/mysql +``` + +## Introduction + +This chart bootstraps a [MySQL](https://github.com/bitnami/bitnami-docker-mysql) replication cluster deployment on a [Kubernetes](https://kubernetes.io) cluster using the [Helm](https://helm.sh) package manager. + +Bitnami charts can be used with [Kubeapps](https://kubeapps.com/) for deployment and management of Helm Charts in clusters. This Helm chart has been tested on top of [Bitnami Kubernetes Production Runtime](https://kubeprod.io/) (BKPR). Deploy BKPR to get automated TLS certificates, logging and monitoring for your applications. + +## Prerequisites + +- Kubernetes 1.19+ +- Helm 3.2.0+ +- PV provisioner support in the underlying infrastructure + +## Installing the Chart + +To install the chart with the release name `my-release`: + +```bash +$ helm repo add bitnami https://charts.bitnami.com/bitnami +$ helm install my-release bitnami/mysql +``` + +These commands deploy MySQL on the Kubernetes cluster in the default configuration. The [Parameters](#parameters) section lists the parameters that can be configured during installation. + +> **Tip**: List all releases using `helm list` + +## Uninstalling the Chart + +To uninstall/delete the `my-release` deployment: + +```bash +$ helm delete my-release +``` + +The command removes all the Kubernetes components associated with the chart and deletes the release. + +## Parameters + +### Global parameters + +| Name | Description | Value | +| ------------------------- | ----------------------------------------------- | ----- | +| `global.imageRegistry` | Global Docker image registry | `""` | +| `global.imagePullSecrets` | Global Docker registry secret names as an array | `[]` | +| `global.storageClass` | Global StorageClass for Persistent Volume(s) | `""` | + + +### Common parameters + +| Name | Description | Value | +| ------------------------ | --------------------------------------------------------------------------------------------------------- | --------------- | +| `nameOverride` | String to partially override common.names.fullname template (will maintain the release name) | `""` | +| `fullnameOverride` | String to fully override common.names.fullname template | `""` | +| `clusterDomain` | Cluster domain | `cluster.local` | +| `commonAnnotations` | Common annotations to add to all MySQL resources (sub-charts are not considered). Evaluated as a template | `{}` | +| `commonLabels` | Common labels to add to all MySQL resources (sub-charts are not considered). Evaluated as a template | `{}` | +| `extraDeploy` | Array with extra yaml to deploy with the chart. Evaluated as a template | `[]` | +| `schedulerName` | Use an alternate scheduler, e.g. "stork". | `""` | +| `diagnosticMode.enabled` | Enable diagnostic mode (all probes will be disabled and the command will be overridden) | `false` | +| `diagnosticMode.command` | Command to override all containers in the deployment | `["sleep"]` | +| `diagnosticMode.args` | Args to override all containers in the deployment | `["infinity"]` | + + +### MySQL common parameters + +| Name | Description | Value | +| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| `image.registry` | MySQL image registry | `docker.io` | +| `image.repository` | MySQL image repository | `bitnami/mysql` | +| `image.tag` | MySQL image tag (immutable tags are recommended) | `8.0.28-debian-10-r0` | +| `image.pullPolicy` | MySQL image pull policy | `IfNotPresent` | +| `image.pullSecrets` | Specify docker-registry secret names as an array | `[]` | +| `image.debug` | Specify if debug logs should be enabled | `false` | +| `architecture` | MySQL architecture (`standalone` or `replication`) | `standalone` | +| `auth.rootPassword` | Password for the `root` user. Ignored if existing secret is provided | `""` | +| `auth.database` | Name for a custom database to create | `my_database` | +| `auth.username` | Name for a custom user to create | `""` | +| `auth.password` | Password for the new user. Ignored if existing secret is provided | `""` | +| `auth.replicationUser` | MySQL replication user | `replicator` | +| `auth.replicationPassword` | MySQL replication user password. Ignored if existing secret is provided | `""` | +| `auth.existingSecret` | Use existing secret for password details. The secret has to contain the keys `mysql-root-password`, `mysql-replication-password` and `mysql-password` | `""` | +| `auth.forcePassword` | Force users to specify required passwords | `false` | +| `auth.usePasswordFiles` | Mount credentials as files instead of using an environment variable | `false` | +| `auth.customPasswordFiles` | Use custom password files when `auth.usePasswordFiles` is set to `true`. Define path for keys `root` and `user`, also define `replicator` if `architecture` is set to `replication` | `{}` | +| `initdbScripts` | Dictionary of initdb scripts | `{}` | +| `initdbScriptsConfigMap` | ConfigMap with the initdb scripts (Note: Overrides `initdbScripts`) | `""` | + + +### MySQL Primary parameters + +| Name | Description | Value | +| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------- | +| `primary.command` | Override default container command on MySQL Primary container(s) (useful when using custom images) | `[]` | +| `primary.args` | Override default container args on MySQL Primary container(s) (useful when using custom images) | `[]` | +| `primary.hostAliases` | Deployment pod host aliases | `[]` | +| `primary.configuration` | Configure MySQL Primary with a custom my.cnf file | `""` | +| `primary.existingConfigmap` | Name of existing ConfigMap with MySQL Primary configuration. | `""` | +| `primary.updateStrategy` | Update strategy type for the MySQL primary statefulset | `RollingUpdate` | +| `primary.rollingUpdatePartition` | Partition update strategy for MySQL Primary statefulset | `""` | +| `primary.podAnnotations` | Additional pod annotations for MySQL primary pods | `{}` | +| `primary.podAffinityPreset` | MySQL primary pod affinity preset. Ignored if `primary.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `primary.podAntiAffinityPreset` | MySQL primary pod anti-affinity preset. Ignored if `primary.affinity` is set. Allowed values: `soft` or `hard` | `soft` | +| `primary.nodeAffinityPreset.type` | MySQL primary node affinity preset type. Ignored if `primary.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `primary.nodeAffinityPreset.key` | MySQL primary node label key to match Ignored if `primary.affinity` is set. | `""` | +| `primary.nodeAffinityPreset.values` | MySQL primary node label values to match. Ignored if `primary.affinity` is set. | `[]` | +| `primary.affinity` | Affinity for MySQL primary pods assignment | `{}` | +| `primary.nodeSelector` | Node labels for MySQL primary pods assignment | `{}` | +| `primary.tolerations` | Tolerations for MySQL primary pods assignment | `[]` | +| `primary.podSecurityContext.enabled` | Enable security context for MySQL primary pods | `true` | +| `primary.podSecurityContext.fsGroup` | Group ID for the mounted volumes' filesystem | `1001` | +| `primary.containerSecurityContext.enabled` | MySQL primary container securityContext | `true` | +| `primary.containerSecurityContext.runAsUser` | User ID for the MySQL primary container | `1001` | +| `primary.resources.limits` | The resources limits for MySQL primary containers | `{}` | +| `primary.resources.requests` | The requested resources for MySQL primary containers | `{}` | +| `primary.livenessProbe.enabled` | Enable livenessProbe | `true` | +| `primary.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `5` | +| `primary.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `10` | +| `primary.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` | +| `primary.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `3` | +| `primary.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` | +| `primary.readinessProbe.enabled` | Enable readinessProbe | `true` | +| `primary.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `5` | +| `primary.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `10` | +| `primary.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` | +| `primary.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` | +| `primary.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` | +| `primary.startupProbe.enabled` | Enable startupProbe | `true` | +| `primary.startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `15` | +| `primary.startupProbe.periodSeconds` | Period seconds for startupProbe | `10` | +| `primary.startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` | +| `primary.startupProbe.failureThreshold` | Failure threshold for startupProbe | `10` | +| `primary.startupProbe.successThreshold` | Success threshold for startupProbe | `1` | +| `primary.customLivenessProbe` | Override default liveness probe for MySQL primary containers | `{}` | +| `primary.customReadinessProbe` | Override default readiness probe for MySQL primary containers | `{}` | +| `primary.customStartupProbe` | Override default startup probe for MySQL primary containers | `{}` | +| `primary.extraFlags` | MySQL primary additional command line flags | `""` | +| `primary.extraEnvVars` | Extra environment variables to be set on MySQL primary containers | `[]` | +| `primary.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for MySQL primary containers | `""` | +| `primary.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for MySQL primary containers | `""` | +| `primary.persistence.enabled` | Enable persistence on MySQL primary replicas using a `PersistentVolumeClaim`. If false, use emptyDir | `true` | +| `primary.persistence.existingClaim` | Name of an existing `PersistentVolumeClaim` for MySQL primary replicas | `""` | +| `primary.persistence.storageClass` | MySQL primary persistent volume storage Class | `""` | +| `primary.persistence.annotations` | MySQL primary persistent volume claim annotations | `{}` | +| `primary.persistence.accessModes` | MySQL primary persistent volume access Modes | `["ReadWriteOnce"]` | +| `primary.persistence.size` | MySQL primary persistent volume size | `8Gi` | +| `primary.persistence.selector` | Selector to match an existing Persistent Volume | `{}` | +| `primary.extraVolumes` | Optionally specify extra list of additional volumes to the MySQL Primary pod(s) | `[]` | +| `primary.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the MySQL Primary container(s) | `[]` | +| `primary.initContainers` | Add additional init containers for the MySQL Primary pod(s) | `[]` | +| `primary.sidecars` | Add additional sidecar containers for the MySQL Primary pod(s) | `[]` | +| `primary.service.type` | MySQL Primary K8s service type | `ClusterIP` | +| `primary.service.port` | MySQL Primary K8s service port | `3306` | +| `primary.service.nodePort` | MySQL Primary K8s service node port | `""` | +| `primary.service.clusterIP` | MySQL Primary K8s service clusterIP IP | `""` | +| `primary.service.loadBalancerIP` | MySQL Primary loadBalancerIP if service type is `LoadBalancer` | `""` | +| `primary.service.externalTrafficPolicy` | Enable client source IP preservation | `Cluster` | +| `primary.service.loadBalancerSourceRanges` | Addresses that are allowed when MySQL Primary service is LoadBalancer | `[]` | +| `primary.service.annotations` | Provide any additional annotations which may be required | `{}` | +| `primary.pdb.enabled` | Enable/disable a Pod Disruption Budget creation for MySQL primary pods | `false` | +| `primary.pdb.minAvailable` | Minimum number/percentage of MySQL primary pods that should remain scheduled | `1` | +| `primary.pdb.maxUnavailable` | Maximum number/percentage of MySQL primary pods that may be made unavailable | `""` | +| `primary.podLabels` | MySQL Primary pod label. If labels are same as commonLabels , this will take precedence | `{}` | + + +### MySQL Secondary parameters + +| Name | Description | Value | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- | ------------------- | +| `secondary.replicaCount` | Number of MySQL secondary replicas | `1` | +| `secondary.hostAliases` | Deployment pod host aliases | `[]` | +| `secondary.command` | Override default container command on MySQL Secondary container(s) (useful when using custom images) | `[]` | +| `secondary.args` | Override default container args on MySQL Secondary container(s) (useful when using custom images) | `[]` | +| `secondary.configuration` | Configure MySQL Secondary with a custom my.cnf file | `""` | +| `secondary.existingConfigmap` | Name of existing ConfigMap with MySQL Secondary configuration. | `""` | +| `secondary.updateStrategy` | Update strategy type for the MySQL secondary statefulset | `RollingUpdate` | +| `secondary.rollingUpdatePartition` | Partition update strategy for MySQL Secondary statefulset | `""` | +| `secondary.podAnnotations` | Additional pod annotations for MySQL secondary pods | `{}` | +| `secondary.podAffinityPreset` | MySQL secondary pod affinity preset. Ignored if `secondary.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `secondary.podAntiAffinityPreset` | MySQL secondary pod anti-affinity preset. Ignored if `secondary.affinity` is set. Allowed values: `soft` or `hard` | `soft` | +| `secondary.nodeAffinityPreset.type` | MySQL secondary node affinity preset type. Ignored if `secondary.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `secondary.nodeAffinityPreset.key` | MySQL secondary node label key to match Ignored if `secondary.affinity` is set. | `""` | +| `secondary.nodeAffinityPreset.values` | MySQL secondary node label values to match. Ignored if `secondary.affinity` is set. | `[]` | +| `secondary.affinity` | Affinity for MySQL secondary pods assignment | `{}` | +| `secondary.nodeSelector` | Node labels for MySQL secondary pods assignment | `{}` | +| `secondary.tolerations` | Tolerations for MySQL secondary pods assignment | `[]` | +| `secondary.podSecurityContext.enabled` | Enable security context for MySQL secondary pods | `true` | +| `secondary.podSecurityContext.fsGroup` | Group ID for the mounted volumes' filesystem | `1001` | +| `secondary.containerSecurityContext.enabled` | MySQL secondary container securityContext | `true` | +| `secondary.containerSecurityContext.runAsUser` | User ID for the MySQL secondary container | `1001` | +| `secondary.resources.limits` | The resources limits for MySQL secondary containers | `{}` | +| `secondary.resources.requests` | The requested resources for MySQL secondary containers | `{}` | +| `secondary.livenessProbe.enabled` | Enable livenessProbe | `true` | +| `secondary.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `5` | +| `secondary.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `10` | +| `secondary.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` | +| `secondary.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `3` | +| `secondary.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` | +| `secondary.readinessProbe.enabled` | Enable readinessProbe | `true` | +| `secondary.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `5` | +| `secondary.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `10` | +| `secondary.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` | +| `secondary.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` | +| `secondary.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` | +| `secondary.startupProbe.enabled` | Enable startupProbe | `true` | +| `secondary.startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `15` | +| `secondary.startupProbe.periodSeconds` | Period seconds for startupProbe | `10` | +| `secondary.startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` | +| `secondary.startupProbe.failureThreshold` | Failure threshold for startupProbe | `15` | +| `secondary.startupProbe.successThreshold` | Success threshold for startupProbe | `1` | +| `secondary.customLivenessProbe` | Override default liveness probe for MySQL secondary containers | `{}` | +| `secondary.customReadinessProbe` | Override default readiness probe for MySQL secondary containers | `{}` | +| `secondary.customStartupProbe` | Override default startup probe for MySQL secondary containers | `{}` | +| `secondary.extraFlags` | MySQL secondary additional command line flags | `""` | +| `secondary.extraEnvVars` | An array to add extra environment variables on MySQL secondary containers | `[]` | +| `secondary.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for MySQL secondary containers | `""` | +| `secondary.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for MySQL secondary containers | `""` | +| `secondary.persistence.enabled` | Enable persistence on MySQL secondary replicas using a `PersistentVolumeClaim` | `true` | +| `secondary.persistence.storageClass` | MySQL secondary persistent volume storage Class | `""` | +| `secondary.persistence.annotations` | MySQL secondary persistent volume claim annotations | `{}` | +| `secondary.persistence.accessModes` | MySQL secondary persistent volume access Modes | `["ReadWriteOnce"]` | +| `secondary.persistence.size` | MySQL secondary persistent volume size | `8Gi` | +| `secondary.persistence.selector` | Selector to match an existing Persistent Volume | `{}` | +| `secondary.extraVolumes` | Optionally specify extra list of additional volumes to the MySQL secondary pod(s) | `[]` | +| `secondary.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the MySQL secondary container(s) | `[]` | +| `secondary.initContainers` | Add additional init containers for the MySQL secondary pod(s) | `[]` | +| `secondary.sidecars` | Add additional sidecar containers for the MySQL secondary pod(s) | `[]` | +| `secondary.service.type` | MySQL secondary Kubernetes service type | `ClusterIP` | +| `secondary.service.port` | MySQL secondary Kubernetes service port | `3306` | +| `secondary.service.nodePort` | MySQL secondary Kubernetes service node port | `""` | +| `secondary.service.clusterIP` | MySQL secondary Kubernetes service clusterIP IP | `""` | +| `secondary.service.loadBalancerIP` | MySQL secondary loadBalancerIP if service type is `LoadBalancer` | `""` | +| `secondary.service.externalTrafficPolicy` | Enable client source IP preservation | `Cluster` | +| `secondary.service.loadBalancerSourceRanges` | Addresses that are allowed when MySQL secondary service is LoadBalancer | `[]` | +| `secondary.service.annotations` | Provide any additional annotations which may be required | `{}` | +| `secondary.pdb.enabled` | Enable/disable a Pod Disruption Budget creation for MySQL secondary pods | `false` | +| `secondary.pdb.minAvailable` | Minimum number/percentage of MySQL secondary pods that should remain scheduled | `1` | +| `secondary.pdb.maxUnavailable` | Maximum number/percentage of MySQL secondary pods that may be made unavailable | `""` | +| `secondary.podLabels` | Additional pod labels for MySQL secondary pods | `{}` | + + +### RBAC parameters + +| Name | Description | Value | +| ---------------------------- | ------------------------------------------------------ | ------- | +| `serviceAccount.create` | Enable the creation of a ServiceAccount for MySQL pods | `true` | +| `serviceAccount.name` | Name of the created ServiceAccount | `""` | +| `serviceAccount.annotations` | Annotations for MySQL Service Account | `{}` | +| `rbac.create` | Whether to create & use RBAC resources or not | `false` | + + +### Network Policy + +| Name | Description | Value | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ------- | +| `networkPolicy.enabled` | Enable creation of NetworkPolicy resources | `false` | +| `networkPolicy.allowExternal` | The Policy model to apply. | `true` | +| `networkPolicy.explicitNamespacesSelector` | A Kubernetes LabelSelector to explicitly select namespaces from which ingress traffic could be allowed to MySQL | `{}` | + + +### Volume Permissions parameters + +| Name | Description | Value | +| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------- | +| `volumePermissions.enabled` | Enable init container that changes the owner and group of the persistent volume(s) mountpoint to `runAsUser:fsGroup` | `false` | +| `volumePermissions.image.registry` | Init container volume-permissions image registry | `docker.io` | +| `volumePermissions.image.repository` | Init container volume-permissions image repository | `bitnami/bitnami-shell` | +| `volumePermissions.image.tag` | Init container volume-permissions image tag (immutable tags are recommended) | `10-debian-10-r312` | +| `volumePermissions.image.pullPolicy` | Init container volume-permissions image pull policy | `IfNotPresent` | +| `volumePermissions.image.pullSecrets` | Specify docker-registry secret names as an array | `[]` | +| `volumePermissions.resources` | Init container volume-permissions resources | `{}` | + + +### Metrics parameters + +| Name | Description | Value | +| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------- | +| `metrics.enabled` | Start a side-car prometheus exporter | `false` | +| `metrics.image.registry` | Exporter image registry | `docker.io` | +| `metrics.image.repository` | Exporter image repository | `bitnami/mysqld-exporter` | +| `metrics.image.tag` | Exporter image tag (immutable tags are recommended) | `0.13.0-debian-10-r216` | +| `metrics.image.pullPolicy` | Exporter image pull policy | `IfNotPresent` | +| `metrics.image.pullSecrets` | Specify docker-registry secret names as an array | `[]` | +| `metrics.service.type` | Kubernetes service type for MySQL Prometheus Exporter | `ClusterIP` | +| `metrics.service.port` | MySQL Prometheus Exporter service port | `9104` | +| `metrics.service.annotations` | Prometheus exporter service annotations | `{}` | +| `metrics.extraArgs.primary` | Extra args to be passed to mysqld_exporter on Primary pods | `[]` | +| `metrics.extraArgs.secondary` | Extra args to be passed to mysqld_exporter on Secondary pods | `[]` | +| `metrics.resources.limits` | The resources limits for MySQL prometheus exporter containers | `{}` | +| `metrics.resources.requests` | The requested resources for MySQL prometheus exporter containers | `{}` | +| `metrics.livenessProbe.enabled` | Enable livenessProbe | `true` | +| `metrics.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `120` | +| `metrics.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `10` | +| `metrics.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` | +| `metrics.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `3` | +| `metrics.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` | +| `metrics.readinessProbe.enabled` | Enable readinessProbe | `true` | +| `metrics.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `30` | +| `metrics.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `10` | +| `metrics.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` | +| `metrics.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` | +| `metrics.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` | +| `metrics.serviceMonitor.enabled` | Create ServiceMonitor Resource for scraping metrics using PrometheusOperator | `false` | +| `metrics.serviceMonitor.namespace` | Specify the namespace in which the serviceMonitor resource will be created | `""` | +| `metrics.serviceMonitor.interval` | Specify the interval at which metrics should be scraped | `30s` | +| `metrics.serviceMonitor.scrapeTimeout` | Specify the timeout after which the scrape is ended | `""` | +| `metrics.serviceMonitor.relabellings` | Specify Metric Relabellings to add to the scrape endpoint | `[]` | +| `metrics.serviceMonitor.honorLabels` | Specify honorLabels parameter to add the scrape endpoint | `false` | +| `metrics.serviceMonitor.additionalLabels` | Used to pass Labels that are used by the Prometheus installed in your cluster to select Service Monitors to work with | `{}` | + + +The above parameters map to the env variables defined in [bitnami/mysql](https://github.com/bitnami/bitnami-docker-mysql). For more information please refer to the [bitnami/mysql](https://github.com/bitnami/bitnami-docker-mysql) image documentation. + +Specify each parameter using the `--set key=value[,key=value]` argument to `helm install`. For example, + +```bash +$ helm install my-release \ + --set auth.rootPassword=secretpassword,auth.database=app_database \ + bitnami/mysql +``` + +The above command sets the MySQL `root` account password to `secretpassword`. Additionally it creates a database named `app_database`. + +> NOTE: Once this chart is deployed, it is not possible to change the application's access credentials, such as usernames or passwords, using Helm. To change these application credentials after deployment, delete any persistent volumes (PVs) used by the chart and re-deploy it, or use the application's built-in administrative tools if available. + +Alternatively, a YAML file that specifies the values for the parameters can be provided while installing the chart. For example, + +```bash +$ helm install my-release -f values.yaml bitnami/mysql +``` + +> **Tip**: You can use the default [values.yaml](values.yaml) + +## Configuration and installation details + +### [Rolling VS Immutable tags](https://docs.bitnami.com/containers/how-to/understand-rolling-tags-containers/) + +It is strongly recommended to use immutable tags in a production environment. This ensures your deployment does not change automatically if the same tag is updated with a different image. + +Bitnami will release a new chart updating its containers if a new version of the main container, significant changes, or critical vulnerabilities exist. + +### Use a different MySQL version + +To modify the application version used in this chart, specify a different version of the image using the `image.tag` parameter and/or a different repository using the `image.repository` parameter. Refer to the [chart documentation for more information on these parameters and how to use them with images from a private registry](https://docs.bitnami.com/kubernetes/infrastructure/mysql/configuration/change-image-version/). + +### Customize a new MySQL instance + +The [Bitnami MySQL](https://github.com/bitnami/bitnami-docker-mysql) image allows you to use your custom scripts to initialize a fresh instance. Custom scripts may be specified using the `initdbScripts` parameter. Alternatively, an external ConfigMap may be created with all the initialization scripts and the ConfigMap passed to the chart via the `initdbScriptsConfigMap` parameter. Note that this will override the `initdbScripts` parameter. + +The allowed extensions are `.sh`, `.sql` and `.sql.gz`. + +These scripts are treated differently depending on their extension. While `.sh` scripts are executed on all the nodes, `.sql` and `.sql.gz` scripts are only executed on the primary nodes. This is because `.sh` scripts support conditional tests to identify the type of node they are running on, while such tests are not supported in `.sql` or `sql.gz` files. + +Refer to the [chart documentation for more information and a usage example](http://docs.bitnami.com/kubernetes/infrastructure/mysql/configuration/customize-new-instance/). + +### Sidecars and Init Containers + +If you have a need for additional containers to run within the same pod as MySQL, you can do so via the `sidecars` config parameter. Simply define your container according to the Kubernetes container spec. + +```yaml +sidecars: + - name: your-image-name + image: your-image + imagePullPolicy: Always + ports: + - name: portname + containerPort: 1234 +``` + +Similarly, you can add extra init containers using the `initContainers` parameter. + +```yaml +initContainers: + - name: your-image-name + image: your-image + imagePullPolicy: Always + ports: + - name: portname + containerPort: 1234 +``` + +## Persistence + +The [Bitnami MySQL](https://github.com/bitnami/bitnami-docker-mysql) image stores the MySQL data and configurations at the `/bitnami/mysql` path of the container. + +The chart mounts a [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) volume at this location. The volume is created using dynamic volume provisioning by default. An existing PersistentVolumeClaim can also be defined for this purpose. + +If you encounter errors when working with persistent volumes, refer to our [troubleshooting guide for persistent volumes](https://docs.bitnami.com/kubernetes/faq/troubleshooting/troubleshooting-persistence-volumes/). + +## Network Policy + +To enable network policy for MySQL, install [a networking plugin that implements the Kubernetes NetworkPolicy spec](https://kubernetes.io/docs/tasks/administer-cluster/declare-network-policy#before-you-begin), and set `networkPolicy.enabled` to `true`. + +For Kubernetes v1.5 & v1.6, you must also turn on NetworkPolicy by setting the DefaultDeny namespace annotation. Note: this will enforce policy for _all_ pods in the namespace: + +```console +$ kubectl annotate namespace default "net.beta.kubernetes.io/network-policy={\"ingress\":{\"isolation\":\"DefaultDeny\"}}" +``` + +With NetworkPolicy enabled, traffic will be limited to just port 3306. + +For more precise policy, set `networkPolicy.allowExternal=false`. This will only allow pods with the generated client label to connect to MySQL. +This label will be displayed in the output of a successful install. + +## Pod affinity + +This chart allows you to set your custom affinity using the `XXX.affinity` parameter(s). Find more information about Pod affinity in the [Kubernetes documentation](https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity). + +As an alternative, you can use the preset configurations for pod affinity, pod anti-affinity, and node affinity available at the [bitnami/common](https://github.com/bitnami/charts/tree/master/bitnami/common#affinities) chart. To do so, set the `XXX.podAffinityPreset`, `XXX.podAntiAffinityPreset`, or `XXX.nodeAffinityPreset` parameters. + +## Troubleshooting + +Find more information about how to deal with common errors related to Bitnami's Helm charts in [this troubleshooting guide](https://docs.bitnami.com/general/how-to/troubleshoot-helm-chart-issues). + +## Upgrading + +It's necessary to set the `auth.rootPassword` parameter when upgrading for readiness/liveness probes to work properly. When you install this chart for the first time, some notes will be displayed providing the credentials you must use under the 'Administrator credentials' section. Please note down the password and run the command below to upgrade your chart: + +```bash +$ helm upgrade my-release bitnami/mysql --set auth.rootPassword=[ROOT_PASSWORD] +``` + +| Note: you need to substitute the placeholder _[ROOT_PASSWORD]_ with the value obtained in the installation notes. + +### To 8.0.0 + +- Several parameters were renamed or disappeared in favor of new ones on this major version: + - The terms *master* and *slave* have been replaced by the terms *primary* and *secondary*. Therefore, parameters prefixed with `master` or `slave` are now prefixed with `primary` or `secondary`, respectively. + - Credentials parameters are reorganized under the `auth` parameter. + - `replication.enabled` parameter is deprecated in favor of `architecture` parameter that accepts two values: `standalone` and `replication`. +- Chart labels were adapted to follow the [Helm charts standard labels](https://helm.sh/docs/chart_best_practices/labels/#standard-labels). +- This version also introduces `bitnami/common`, a [library chart](https://helm.sh/docs/topics/library_charts/#helm) as a dependency. More documentation about this new utility could be found [here](https://github.com/bitnami/charts/tree/master/bitnami/common#bitnami-common-library-chart). Please, make sure that you have updated the chart dependencies before executing any upgrade. + +Consequences: + +- Backwards compatibility is not guaranteed. To upgrade to `8.0.0`, install a new release of the MySQL chart, and migrate the data from your previous release. You have 2 alternatives to do so: + - Create a backup of the database, and restore it on the new release using tools such as [mysqldump](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html). + - Reuse the PVC used to hold the master data on your previous release. To do so, use the `primary.persistence.existingClaim` parameter. The following example assumes that the release name is `mysql`: + +```bash +$ helm install mysql bitnami/mysql --set auth.rootPassword=[ROOT_PASSWORD] --set primary.persistence.existingClaim=[EXISTING_PVC] +``` + +| Note: you need to substitute the placeholder _[EXISTING_PVC]_ with the name of the PVC used on your previous release, and _[ROOT_PASSWORD]_ with the root password used in your previous release. + +### To 7.0.0 + +[On November 13, 2020, Helm v2 support formally ended](https://github.com/helm/charts#status-of-the-project). This major version is the result of the required changes applied to the Helm Chart to be able to incorporate the different features added in Helm v3 and to be consistent with the Helm project itself regarding the Helm v2 EOL. + +[Learn more about this change and related upgrade considerations](https://docs.bitnami.com/kubernetes/infrastructure/mysql/administration/upgrade-helm3/). + +### To 3.0.0 + +Backwards compatibility is not guaranteed unless you modify the labels used on the chart's deployments. +Use the workaround below to upgrade from versions previous to 3.0.0. The following example assumes that the release name is mysql: + +```console +$ kubectl delete statefulset mysql-master --cascade=false +$ kubectl delete statefulset mysql-slave --cascade=false +``` + +## License + +Copyright © 2022 Bitnami + +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. \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/.helmignore b/pkg/iac/scanners/helm/test/mysql/charts/common/.helmignore new file mode 100644 index 000000000000..50af03172541 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/Chart.yaml b/pkg/iac/scanners/helm/test/mysql/charts/common/Chart.yaml new file mode 100644 index 000000000000..87226649a57c --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/Chart.yaml @@ -0,0 +1,23 @@ +annotations: + category: Infrastructure +apiVersion: v2 +appVersion: 1.11.1 +description: A Library Helm Chart for grouping common logic between bitnami charts. + This chart is not deployable by itself. +home: https://github.com/bitnami/charts/tree/master/bitnami/common +icon: https://bitnami.com/downloads/logos/bitnami-mark.png +keywords: +- common +- helper +- template +- function +- bitnami +maintainers: +- email: containers@bitnami.com + name: Bitnami +name: common +sources: +- https://github.com/bitnami/charts +- https://www.bitnami.com/ +type: library +version: 1.11.1 diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/README.md b/pkg/iac/scanners/helm/test/mysql/charts/common/README.md new file mode 100644 index 000000000000..da84c426d0db --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/README.md @@ -0,0 +1,345 @@ +# Bitnami Common Library Chart + +A [Helm Library Chart](https://helm.sh/docs/topics/library_charts/#helm) for grouping common logic between bitnami charts. + +## TL;DR + +```yaml +dependencies: + - name: common + version: 0.x.x + repository: https://charts.bitnami.com/bitnami +``` + +```bash +$ helm dependency update +``` + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "common.names.fullname" . }} +data: + myvalue: "Hello World" +``` + +## Introduction + +This chart provides a common template helpers which can be used to develop new charts using [Helm](https://helm.sh) package manager. + +Bitnami charts can be used with [Kubeapps](https://kubeapps.com/) for deployment and management of Helm Charts in clusters. This Helm chart has been tested on top of [Bitnami Kubernetes Production Runtime](https://kubeprod.io/) (BKPR). Deploy BKPR to get automated TLS certificates, logging and monitoring for your applications. + +## Prerequisites + +- Kubernetes 1.19+ +- Helm 3.2.0+ + +## Parameters + +The following table lists the helpers available in the library which are scoped in different sections. + +### Affinities + +| Helper identifier | Description | Expected Input | +|-------------------------------|------------------------------------------------------|------------------------------------------------| +| `common.affinities.node.soft` | Return a soft nodeAffinity definition | `dict "key" "FOO" "values" (list "BAR" "BAZ")` | +| `common.affinities.node.hard` | Return a hard nodeAffinity definition | `dict "key" "FOO" "values" (list "BAR" "BAZ")` | +| `common.affinities.pod.soft` | Return a soft podAffinity/podAntiAffinity definition | `dict "component" "FOO" "context" $` | +| `common.affinities.pod.hard` | Return a hard podAffinity/podAntiAffinity definition | `dict "component" "FOO" "context" $` | + +### Capabilities + +| Helper identifier | Description | Expected Input | +|------------------------------------------------|------------------------------------------------------------------------------------------------|-------------------| +| `common.capabilities.kubeVersion` | Return the target Kubernetes version (using client default if .Values.kubeVersion is not set). | `.` Chart context | +| `common.capabilities.cronjob.apiVersion` | Return the appropriate apiVersion for cronjob. | `.` Chart context | +| `common.capabilities.deployment.apiVersion` | Return the appropriate apiVersion for deployment. | `.` Chart context | +| `common.capabilities.statefulset.apiVersion` | Return the appropriate apiVersion for statefulset. | `.` Chart context | +| `common.capabilities.ingress.apiVersion` | Return the appropriate apiVersion for ingress. | `.` Chart context | +| `common.capabilities.rbac.apiVersion` | Return the appropriate apiVersion for RBAC resources. | `.` Chart context | +| `common.capabilities.crd.apiVersion` | Return the appropriate apiVersion for CRDs. | `.` Chart context | +| `common.capabilities.policy.apiVersion` | Return the appropriate apiVersion for podsecuritypolicy. | `.` Chart context | +| `common.capabilities.networkPolicy.apiVersion` | Return the appropriate apiVersion for networkpolicy. | `.` Chart context | +| `common.capabilities.supportsHelmVersion` | Returns true if the used Helm version is 3.3+ | `.` Chart context | + +### Errors + +| Helper identifier | Description | Expected Input | +|-----------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------| +| `common.errors.upgrade.passwords.empty` | It will ensure required passwords are given when we are upgrading a chart. If `validationErrors` is not empty it will throw an error and will stop the upgrade action. | `dict "validationErrors" (list $validationError00 $validationError01) "context" $` | + +### Images + +| Helper identifier | Description | Expected Input | +|-----------------------------|------------------------------------------------------|---------------------------------------------------------------------------------------------------------| +| `common.images.image` | Return the proper and full image name | `dict "imageRoot" .Values.path.to.the.image "global" $`, see [ImageRoot](#imageroot) for the structure. | +| `common.images.pullSecrets` | Return the proper Docker Image Registry Secret Names (deprecated: use common.images.renderPullSecrets instead) | `dict "images" (list .Values.path.to.the.image1, .Values.path.to.the.image2) "global" .Values.global` | +| `common.images.renderPullSecrets` | Return the proper Docker Image Registry Secret Names (evaluates values as templates) | `dict "images" (list .Values.path.to.the.image1, .Values.path.to.the.image2) "context" $` | + +### Ingress + +| Helper identifier | Description | Expected Input | +|-------------------------------------------|-------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `common.ingress.backend` | Generate a proper Ingress backend entry depending on the API version | `dict "serviceName" "foo" "servicePort" "bar"`, see the [Ingress deprecation notice](https://kubernetes.io/blog/2019/07/18/api-deprecations-in-1-16/) for the syntax differences | +| `common.ingress.supportsPathType` | Prints "true" if the pathType field is supported | `.` Chart context | +| `common.ingress.supportsIngressClassname` | Prints "true" if the ingressClassname field is supported | `.` Chart context | +| `common.ingress.certManagerRequest` | Prints "true" if required cert-manager annotations for TLS signed certificates are set in the Ingress annotations | `dict "annotations" .Values.path.to.the.ingress.annotations` | + +### Labels + +| Helper identifier | Description | Expected Input | +|-----------------------------|------------------------------------------------------|-------------------| +| `common.labels.standard` | Return Kubernetes standard labels | `.` Chart context | +| `common.labels.matchLabels` | Return the proper Docker Image Registry Secret Names | `.` Chart context | + +### Names + +| Helper identifier | Description | Expected Input | +|-------------------------|------------------------------------------------------------|-------------------| +| `common.names.name` | Expand the name of the chart or use `.Values.nameOverride` | `.` Chart context | +| `common.names.fullname` | Create a default fully qualified app name. | `.` Chart context | +| `common.names.chart` | Chart name plus version | `.` Chart context | + +### Secrets + +| Helper identifier | Description | Expected Input | +|---------------------------|--------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `common.secrets.name` | Generate the name of the secret. | `dict "existingSecret" .Values.path.to.the.existingSecret "defaultNameSuffix" "mySuffix" "context" $` see [ExistingSecret](#existingsecret) for the structure. | +| `common.secrets.key` | Generate secret key. | `dict "existingSecret" .Values.path.to.the.existingSecret "key" "keyName"` see [ExistingSecret](#existingsecret) for the structure. | +| `common.passwords.manage` | Generate secret password or retrieve one if already created. | `dict "secret" "secret-name" "key" "keyName" "providedValues" (list "path.to.password1" "path.to.password2") "length" 10 "strong" false "chartName" "chartName" "context" $`, length, strong and chartNAme fields are optional. | +| `common.secrets.exists` | Returns whether a previous generated secret already exists. | `dict "secret" "secret-name" "context" $` | + +### Storage + +| Helper identifier | Description | Expected Input | +|-------------------------------|---------------------------------------|---------------------------------------------------------------------------------------------------------------------| +| `common.storage.class` | Return the proper Storage Class | `dict "persistence" .Values.path.to.the.persistence "global" $`, see [Persistence](#persistence) for the structure. | + +### TplValues + +| Helper identifier | Description | Expected Input | +|---------------------------|----------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| +| `common.tplvalues.render` | Renders a value that contains template | `dict "value" .Values.path.to.the.Value "context" $`, value is the value should rendered as template, context frequently is the chart context `$` or `.` | + +### Utils + +| Helper identifier | Description | Expected Input | +|--------------------------------|------------------------------------------------------------------------------------------|------------------------------------------------------------------------| +| `common.utils.fieldToEnvVar` | Build environment variable name given a field. | `dict "field" "my-password"` | +| `common.utils.secret.getvalue` | Print instructions to get a secret value. | `dict "secret" "secret-name" "field" "secret-value-field" "context" $` | +| `common.utils.getValueFromKey` | Gets a value from `.Values` object given its key path | `dict "key" "path.to.key" "context" $` | +| `common.utils.getKeyFromList` | Returns first `.Values` key with a defined value or first of the list if all non-defined | `dict "keys" (list "path.to.key1" "path.to.key2") "context" $` | + +### Validations + +| Helper identifier | Description | Expected Input | +|--------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `common.validations.values.single.empty` | Validate a value must not be empty. | `dict "valueKey" "path.to.value" "secret" "secret.name" "field" "my-password" "subchart" "subchart" "context" $` secret, field and subchart are optional. In case they are given, the helper will generate a how to get instruction. See [ValidateValue](#validatevalue) | +| `common.validations.values.multiple.empty` | Validate a multiple values must not be empty. It returns a shared error for all the values. | `dict "required" (list $validateValueConf00 $validateValueConf01) "context" $`. See [ValidateValue](#validatevalue) | +| `common.validations.values.mariadb.passwords` | This helper will ensure required password for MariaDB are not empty. It returns a shared error for all the values. | `dict "secret" "mariadb-secret" "subchart" "true" "context" $` subchart field is optional and could be true or false it depends on where you will use mariadb chart and the helper. | +| `common.validations.values.postgresql.passwords` | This helper will ensure required password for PostgreSQL are not empty. It returns a shared error for all the values. | `dict "secret" "postgresql-secret" "subchart" "true" "context" $` subchart field is optional and could be true or false it depends on where you will use postgresql chart and the helper. | +| `common.validations.values.redis.passwords` | This helper will ensure required password for Redis™ are not empty. It returns a shared error for all the values. | `dict "secret" "redis-secret" "subchart" "true" "context" $` subchart field is optional and could be true or false it depends on where you will use redis chart and the helper. | +| `common.validations.values.cassandra.passwords` | This helper will ensure required password for Cassandra are not empty. It returns a shared error for all the values. | `dict "secret" "cassandra-secret" "subchart" "true" "context" $` subchart field is optional and could be true or false it depends on where you will use cassandra chart and the helper. | +| `common.validations.values.mongodb.passwords` | This helper will ensure required password for MongoDB® are not empty. It returns a shared error for all the values. | `dict "secret" "mongodb-secret" "subchart" "true" "context" $` subchart field is optional and could be true or false it depends on where you will use mongodb chart and the helper. | + +### Warnings + +| Helper identifier | Description | Expected Input | +|------------------------------|----------------------------------|------------------------------------------------------------| +| `common.warnings.rollingTag` | Warning about using rolling tag. | `ImageRoot` see [ImageRoot](#imageroot) for the structure. | + +## Special input schemas + +### ImageRoot + +```yaml +registry: + type: string + description: Docker registry where the image is located + example: docker.io + +repository: + type: string + description: Repository and image name + example: bitnami/nginx + +tag: + type: string + description: image tag + example: 1.16.1-debian-10-r63 + +pullPolicy: + type: string + description: Specify a imagePullPolicy. Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + +pullSecrets: + type: array + items: + type: string + description: Optionally specify an array of imagePullSecrets (evaluated as templates). + +debug: + type: boolean + description: Set to true if you would like to see extra information on logs + example: false + +## An instance would be: +# registry: docker.io +# repository: bitnami/nginx +# tag: 1.16.1-debian-10-r63 +# pullPolicy: IfNotPresent +# debug: false +``` + +### Persistence + +```yaml +enabled: + type: boolean + description: Whether enable persistence. + example: true + +storageClass: + type: string + description: Ghost data Persistent Volume Storage Class, If set to "-", storageClassName: "" which disables dynamic provisioning. + example: "-" + +accessMode: + type: string + description: Access mode for the Persistent Volume Storage. + example: ReadWriteOnce + +size: + type: string + description: Size the Persistent Volume Storage. + example: 8Gi + +path: + type: string + description: Path to be persisted. + example: /bitnami + +## An instance would be: +# enabled: true +# storageClass: "-" +# accessMode: ReadWriteOnce +# size: 8Gi +# path: /bitnami +``` + +### ExistingSecret + +```yaml +name: + type: string + description: Name of the existing secret. + example: mySecret +keyMapping: + description: Mapping between the expected key name and the name of the key in the existing secret. + type: object + +## An instance would be: +# name: mySecret +# keyMapping: +# password: myPasswordKey +``` + +#### Example of use + +When we store sensitive data for a deployment in a secret, some times we want to give to users the possibility of using theirs existing secrets. + +```yaml +# templates/secret.yaml +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "common.names.fullname" . }} + labels: + app: {{ include "common.names.fullname" . }} +type: Opaque +data: + password: {{ .Values.password | b64enc | quote }} + +# templates/dpl.yaml +--- +... + env: + - name: PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "common.secrets.name" (dict "existingSecret" .Values.existingSecret "context" $) }} + key: {{ include "common.secrets.key" (dict "existingSecret" .Values.existingSecret "key" "password") }} +... + +# values.yaml +--- +name: mySecret +keyMapping: + password: myPasswordKey +``` + +### ValidateValue + +#### NOTES.txt + +```console +{{- $validateValueConf00 := (dict "valueKey" "path.to.value00" "secret" "secretName" "field" "password-00") -}} +{{- $validateValueConf01 := (dict "valueKey" "path.to.value01" "secret" "secretName" "field" "password-01") -}} + +{{ include "common.validations.values.multiple.empty" (dict "required" (list $validateValueConf00 $validateValueConf01) "context" $) }} +``` + +If we force those values to be empty we will see some alerts + +```console +$ helm install test mychart --set path.to.value00="",path.to.value01="" + 'path.to.value00' must not be empty, please add '--set path.to.value00=$PASSWORD_00' to the command. To get the current value: + + export PASSWORD_00=$(kubectl get secret --namespace default secretName -o jsonpath="{.data.password-00}" | base64 --decode) + + 'path.to.value01' must not be empty, please add '--set path.to.value01=$PASSWORD_01' to the command. To get the current value: + + export PASSWORD_01=$(kubectl get secret --namespace default secretName -o jsonpath="{.data.password-01}" | base64 --decode) +``` + +## Upgrading + +### To 1.0.0 + +[On November 13, 2020, Helm v2 support was formally finished](https://github.com/helm/charts#status-of-the-project), this major version is the result of the required changes applied to the Helm Chart to be able to incorporate the different features added in Helm v3 and to be consistent with the Helm project itself regarding the Helm v2 EOL. + +**What changes were introduced in this major version?** + +- Previous versions of this Helm Chart use `apiVersion: v1` (installable by both Helm 2 and 3), this Helm Chart was updated to `apiVersion: v2` (installable by Helm 3 only). [Here](https://helm.sh/docs/topics/charts/#the-apiversion-field) you can find more information about the `apiVersion` field. +- Use `type: library`. [Here](https://v3.helm.sh/docs/faq/#library-chart-support) you can find more information. +- The different fields present in the *Chart.yaml* file has been ordered alphabetically in a homogeneous way for all the Bitnami Helm Charts + +**Considerations when upgrading to this version** + +- If you want to upgrade to this version from a previous one installed with Helm v3, you shouldn't face any issues +- If you want to upgrade to this version using Helm v2, this scenario is not supported as this version doesn't support Helm v2 anymore +- If you installed the previous version with Helm v2 and wants to upgrade to this version with Helm v3, please refer to the [official Helm documentation](https://helm.sh/docs/topics/v2_v3_migration/#migration-use-cases) about migrating from Helm v2 to v3 + +**Useful links** + +- https://docs.bitnami.com/tutorials/resolve-helm2-helm3-post-migration-issues/ +- https://helm.sh/docs/topics/v2_v3_migration/ +- https://helm.sh/blog/migrate-from-helm-v2-to-helm-v3/ + +## License + +Copyright © 2022 Bitnami + +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. diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_affinities.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_affinities.tpl new file mode 100644 index 000000000000..189ea403d558 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_affinities.tpl @@ -0,0 +1,102 @@ +{{/* vim: set filetype=mustache: */}} + +{{/* +Return a soft nodeAffinity definition +{{ include "common.affinities.nodes.soft" (dict "key" "FOO" "values" (list "BAR" "BAZ")) -}} +*/}} +{{- define "common.affinities.nodes.soft" -}} +preferredDuringSchedulingIgnoredDuringExecution: + - preference: + matchExpressions: + - key: {{ .key }} + operator: In + values: + {{- range .values }} + - {{ . | quote }} + {{- end }} + weight: 1 +{{- end -}} + +{{/* +Return a hard nodeAffinity definition +{{ include "common.affinities.nodes.hard" (dict "key" "FOO" "values" (list "BAR" "BAZ")) -}} +*/}} +{{- define "common.affinities.nodes.hard" -}} +requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: {{ .key }} + operator: In + values: + {{- range .values }} + - {{ . | quote }} + {{- end }} +{{- end -}} + +{{/* +Return a nodeAffinity definition +{{ include "common.affinities.nodes" (dict "type" "soft" "key" "FOO" "values" (list "BAR" "BAZ")) -}} +*/}} +{{- define "common.affinities.nodes" -}} + {{- if eq .type "soft" }} + {{- include "common.affinities.nodes.soft" . -}} + {{- else if eq .type "hard" }} + {{- include "common.affinities.nodes.hard" . -}} + {{- end -}} +{{- end -}} + +{{/* +Return a soft podAffinity/podAntiAffinity definition +{{ include "common.affinities.pods.soft" (dict "component" "FOO" "extraMatchLabels" .Values.extraMatchLabels "context" $) -}} +*/}} +{{- define "common.affinities.pods.soft" -}} +{{- $component := default "" .component -}} +{{- $extraMatchLabels := default (dict) .extraMatchLabels -}} +preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: {{- (include "common.labels.matchLabels" .context) | nindent 10 }} + {{- if not (empty $component) }} + {{ printf "app.kubernetes.io/component: %s" $component }} + {{- end }} + {{- range $key, $value := $extraMatchLabels }} + {{ $key }}: {{ $value | quote }} + {{- end }} + namespaces: + - {{ .context.Release.Namespace | quote }} + topologyKey: kubernetes.io/hostname + weight: 1 +{{- end -}} + +{{/* +Return a hard podAffinity/podAntiAffinity definition +{{ include "common.affinities.pods.hard" (dict "component" "FOO" "extraMatchLabels" .Values.extraMatchLabels "context" $) -}} +*/}} +{{- define "common.affinities.pods.hard" -}} +{{- $component := default "" .component -}} +{{- $extraMatchLabels := default (dict) .extraMatchLabels -}} +requiredDuringSchedulingIgnoredDuringExecution: + - labelSelector: + matchLabels: {{- (include "common.labels.matchLabels" .context) | nindent 8 }} + {{- if not (empty $component) }} + {{ printf "app.kubernetes.io/component: %s" $component }} + {{- end }} + {{- range $key, $value := $extraMatchLabels }} + {{ $key }}: {{ $value | quote }} + {{- end }} + namespaces: + - {{ .context.Release.Namespace | quote }} + topologyKey: kubernetes.io/hostname +{{- end -}} + +{{/* +Return a podAffinity/podAntiAffinity definition +{{ include "common.affinities.pods" (dict "type" "soft" "key" "FOO" "values" (list "BAR" "BAZ")) -}} +*/}} +{{- define "common.affinities.pods" -}} + {{- if eq .type "soft" }} + {{- include "common.affinities.pods.soft" . -}} + {{- else if eq .type "hard" }} + {{- include "common.affinities.pods.hard" . -}} + {{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_capabilities.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_capabilities.tpl new file mode 100644 index 000000000000..b94212bbe77c --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_capabilities.tpl @@ -0,0 +1,128 @@ +{{/* vim: set filetype=mustache: */}} + +{{/* +Return the target Kubernetes version +*/}} +{{- define "common.capabilities.kubeVersion" -}} +{{- if .Values.global }} + {{- if .Values.global.kubeVersion }} + {{- .Values.global.kubeVersion -}} + {{- else }} + {{- default .Capabilities.KubeVersion.Version .Values.kubeVersion -}} + {{- end -}} +{{- else }} +{{- default .Capabilities.KubeVersion.Version .Values.kubeVersion -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for poddisruptionbudget. +*/}} +{{- define "common.capabilities.policy.apiVersion" -}} +{{- if semverCompare "<1.21-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "policy/v1beta1" -}} +{{- else -}} +{{- print "policy/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for networkpolicy. +*/}} +{{- define "common.capabilities.networkPolicy.apiVersion" -}} +{{- if semverCompare "<1.7-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "extensions/v1beta1" -}} +{{- else -}} +{{- print "networking.k8s.io/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for cronjob. +*/}} +{{- define "common.capabilities.cronjob.apiVersion" -}} +{{- if semverCompare "<1.21-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "batch/v1beta1" -}} +{{- else -}} +{{- print "batch/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for deployment. +*/}} +{{- define "common.capabilities.deployment.apiVersion" -}} +{{- if semverCompare "<1.14-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "extensions/v1beta1" -}} +{{- else -}} +{{- print "apps/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for statefulset. +*/}} +{{- define "common.capabilities.statefulset.apiVersion" -}} +{{- if semverCompare "<1.14-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "apps/v1beta1" -}} +{{- else -}} +{{- print "apps/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for ingress. +*/}} +{{- define "common.capabilities.ingress.apiVersion" -}} +{{- if .Values.ingress -}} +{{- if .Values.ingress.apiVersion -}} +{{- .Values.ingress.apiVersion -}} +{{- else if semverCompare "<1.14-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "extensions/v1beta1" -}} +{{- else if semverCompare "<1.19-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "networking.k8s.io/v1beta1" -}} +{{- else -}} +{{- print "networking.k8s.io/v1" -}} +{{- end }} +{{- else if semverCompare "<1.14-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "extensions/v1beta1" -}} +{{- else if semverCompare "<1.19-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "networking.k8s.io/v1beta1" -}} +{{- else -}} +{{- print "networking.k8s.io/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for RBAC resources. +*/}} +{{- define "common.capabilities.rbac.apiVersion" -}} +{{- if semverCompare "<1.17-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "rbac.authorization.k8s.io/v1beta1" -}} +{{- else -}} +{{- print "rbac.authorization.k8s.io/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Return the appropriate apiVersion for CRDs. +*/}} +{{- define "common.capabilities.crd.apiVersion" -}} +{{- if semverCompare "<1.19-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "apiextensions.k8s.io/v1beta1" -}} +{{- else -}} +{{- print "apiextensions.k8s.io/v1" -}} +{{- end -}} +{{- end -}} + +{{/* +Returns true if the used Helm version is 3.3+. +A way to check the used Helm version was not introduced until version 3.3.0 with .Capabilities.HelmVersion, which contains an additional "{}}" structure. +This check is introduced as a regexMatch instead of {{ if .Capabilities.HelmVersion }} because checking for the key HelmVersion in <3.3 results in a "interface not found" error. +**To be removed when the catalog's minimun Helm version is 3.3** +*/}} +{{- define "common.capabilities.supportsHelmVersion" -}} +{{- if regexMatch "{(v[0-9])*[^}]*}}$" (.Capabilities | toString ) }} + {{- true -}} +{{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_errors.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_errors.tpl new file mode 100644 index 000000000000..a79cc2e322e0 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_errors.tpl @@ -0,0 +1,23 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Through error when upgrading using empty passwords values that must not be empty. + +Usage: +{{- $validationError00 := include "common.validations.values.single.empty" (dict "valueKey" "path.to.password00" "secret" "secretName" "field" "password-00") -}} +{{- $validationError01 := include "common.validations.values.single.empty" (dict "valueKey" "path.to.password01" "secret" "secretName" "field" "password-01") -}} +{{ include "common.errors.upgrade.passwords.empty" (dict "validationErrors" (list $validationError00 $validationError01) "context" $) }} + +Required password params: + - validationErrors - String - Required. List of validation strings to be return, if it is empty it won't throw error. + - context - Context - Required. Parent context. +*/}} +{{- define "common.errors.upgrade.passwords.empty" -}} + {{- $validationErrors := join "" .validationErrors -}} + {{- if and $validationErrors .context.Release.IsUpgrade -}} + {{- $errorString := "\nPASSWORDS ERROR: You must provide your current passwords when upgrading the release." -}} + {{- $errorString = print $errorString "\n Note that even after reinstallation, old credentials may be needed as they may be kept in persistent volume claims." -}} + {{- $errorString = print $errorString "\n Further information can be obtained at https://docs.bitnami.com/general/how-to/troubleshoot-helm-chart-issues/#credential-errors-while-upgrading-chart-releases" -}} + {{- $errorString = print $errorString "\n%s" -}} + {{- printf $errorString $validationErrors | fail -}} + {{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_images.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_images.tpl new file mode 100644 index 000000000000..42ffbc7227eb --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_images.tpl @@ -0,0 +1,75 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Return the proper image name +{{ include "common.images.image" ( dict "imageRoot" .Values.path.to.the.image "global" $) }} +*/}} +{{- define "common.images.image" -}} +{{- $registryName := .imageRoot.registry -}} +{{- $repositoryName := .imageRoot.repository -}} +{{- $tag := .imageRoot.tag | toString -}} +{{- if .global }} + {{- if .global.imageRegistry }} + {{- $registryName = .global.imageRegistry -}} + {{- end -}} +{{- end -}} +{{- if $registryName }} +{{- printf "%s/%s:%s" $registryName $repositoryName $tag -}} +{{- else -}} +{{- printf "%s:%s" $repositoryName $tag -}} +{{- end -}} +{{- end -}} + +{{/* +Return the proper Docker Image Registry Secret Names (deprecated: use common.images.renderPullSecrets instead) +{{ include "common.images.pullSecrets" ( dict "images" (list .Values.path.to.the.image1, .Values.path.to.the.image2) "global" .Values.global) }} +*/}} +{{- define "common.images.pullSecrets" -}} + {{- $pullSecrets := list }} + + {{- if .global }} + {{- range .global.imagePullSecrets -}} + {{- $pullSecrets = append $pullSecrets . -}} + {{- end -}} + {{- end -}} + + {{- range .images -}} + {{- range .pullSecrets -}} + {{- $pullSecrets = append $pullSecrets . -}} + {{- end -}} + {{- end -}} + + {{- if (not (empty $pullSecrets)) }} +imagePullSecrets: + {{- range $pullSecrets }} + - name: {{ . }} + {{- end }} + {{- end }} +{{- end -}} + +{{/* +Return the proper Docker Image Registry Secret Names evaluating values as templates +{{ include "common.images.renderPullSecrets" ( dict "images" (list .Values.path.to.the.image1, .Values.path.to.the.image2) "context" $) }} +*/}} +{{- define "common.images.renderPullSecrets" -}} + {{- $pullSecrets := list }} + {{- $context := .context }} + + {{- if $context.Values.global }} + {{- range $context.Values.global.imagePullSecrets -}} + {{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" $context)) -}} + {{- end -}} + {{- end -}} + + {{- range .images -}} + {{- range .pullSecrets -}} + {{- $pullSecrets = append $pullSecrets (include "common.tplvalues.render" (dict "value" . "context" $context)) -}} + {{- end -}} + {{- end -}} + + {{- if (not (empty $pullSecrets)) }} +imagePullSecrets: + {{- range $pullSecrets }} + - name: {{ . }} + {{- end }} + {{- end }} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_ingress.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_ingress.tpl new file mode 100644 index 000000000000..8caf73a61082 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_ingress.tpl @@ -0,0 +1,68 @@ +{{/* vim: set filetype=mustache: */}} + +{{/* +Generate backend entry that is compatible with all Kubernetes API versions. + +Usage: +{{ include "common.ingress.backend" (dict "serviceName" "backendName" "servicePort" "backendPort" "context" $) }} + +Params: + - serviceName - String. Name of an existing service backend + - servicePort - String/Int. Port name (or number) of the service. It will be translated to different yaml depending if it is a string or an integer. + - context - Dict - Required. The context for the template evaluation. +*/}} +{{- define "common.ingress.backend" -}} +{{- $apiVersion := (include "common.capabilities.ingress.apiVersion" .context) -}} +{{- if or (eq $apiVersion "extensions/v1beta1") (eq $apiVersion "networking.k8s.io/v1beta1") -}} +serviceName: {{ .serviceName }} +servicePort: {{ .servicePort }} +{{- else -}} +service: + name: {{ .serviceName }} + port: + {{- if typeIs "string" .servicePort }} + name: {{ .servicePort }} + {{- else if or (typeIs "int" .servicePort) (typeIs "float64" .servicePort) }} + number: {{ .servicePort | int }} + {{- end }} +{{- end -}} +{{- end -}} + +{{/* +Print "true" if the API pathType field is supported +Usage: +{{ include "common.ingress.supportsPathType" . }} +*/}} +{{- define "common.ingress.supportsPathType" -}} +{{- if (semverCompare "<1.18-0" (include "common.capabilities.kubeVersion" .)) -}} +{{- print "false" -}} +{{- else -}} +{{- print "true" -}} +{{- end -}} +{{- end -}} + +{{/* +Returns true if the ingressClassname field is supported +Usage: +{{ include "common.ingress.supportsIngressClassname" . }} +*/}} +{{- define "common.ingress.supportsIngressClassname" -}} +{{- if semverCompare "<1.18-0" (include "common.capabilities.kubeVersion" .) -}} +{{- print "false" -}} +{{- else -}} +{{- print "true" -}} +{{- end -}} +{{- end -}} + +{{/* +Return true if cert-manager required annotations for TLS signed +certificates are set in the Ingress annotations +Ref: https://cert-manager.io/docs/usage/ingress/#supported-annotations +Usage: +{{ include "common.ingress.certManagerRequest" ( dict "annotations" .Values.path.to.the.ingress.annotations ) }} +*/}} +{{- define "common.ingress.certManagerRequest" -}} +{{ if or (hasKey .annotations "cert-manager.io/cluster-issuer") (hasKey .annotations "cert-manager.io/issuer") }} + {{- true -}} +{{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_labels.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_labels.tpl new file mode 100644 index 000000000000..252066c7e2b3 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_labels.tpl @@ -0,0 +1,18 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Kubernetes standard labels +*/}} +{{- define "common.labels.standard" -}} +app.kubernetes.io/name: {{ include "common.names.name" . }} +helm.sh/chart: {{ include "common.names.chart" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{/* +Labels to use on deploy.spec.selector.matchLabels and svc.spec.selector +*/}} +{{- define "common.labels.matchLabels" -}} +app.kubernetes.io/name: {{ include "common.names.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_names.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_names.tpl new file mode 100644 index 000000000000..cf0323171f39 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_names.tpl @@ -0,0 +1,52 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define "common.names.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "common.names.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "common.names.fullname" -}} +{{- if .Values.fullnameOverride -}} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .Chart.Name .Values.nameOverride -}} +{{- if contains $name .Release.Name -}} +{{- .Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Create a default fully qualified dependency name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +Usage: +{{ include "common.names.dependency.fullname" (dict "chartName" "dependency-chart-name" "chartValues" .Values.dependency-chart "context" $) }} +*/}} +{{- define "common.names.dependency.fullname" -}} +{{- if .chartValues.fullnameOverride -}} +{{- .chartValues.fullnameOverride | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- $name := default .chartName .chartValues.nameOverride -}} +{{- if contains $name .context.Release.Name -}} +{{- .context.Release.Name | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- printf "%s-%s" .context.Release.Name $name | trunc 63 | trimSuffix "-" -}} +{{- end -}} +{{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_secrets.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_secrets.tpl new file mode 100644 index 000000000000..a1afc1195996 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_secrets.tpl @@ -0,0 +1,131 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Generate secret name. + +Usage: +{{ include "common.secrets.name" (dict "existingSecret" .Values.path.to.the.existingSecret "defaultNameSuffix" "mySuffix" "context" $) }} + +Params: + - existingSecret - ExistingSecret/String - Optional. The path to the existing secrets in the values.yaml given by the user + to be used instead of the default one. Allows for it to be of type String (just the secret name) for backwards compatibility. + +info: https://github.com/bitnami/charts/tree/master/bitnami/common#existingsecret + - defaultNameSuffix - String - Optional. It is used only if we have several secrets in the same deployment. + - context - Dict - Required. The context for the template evaluation. +*/}} +{{- define "common.secrets.name" -}} +{{- $name := (include "common.names.fullname" .context) -}} + +{{- if .defaultNameSuffix -}} +{{- $name = printf "%s-%s" $name .defaultNameSuffix | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{- with .existingSecret -}} +{{- if not (typeIs "string" .) -}} +{{- with .name -}} +{{- $name = . -}} +{{- end -}} +{{- else -}} +{{- $name = . -}} +{{- end -}} +{{- end -}} + +{{- printf "%s" $name -}} +{{- end -}} + +{{/* +Generate secret key. + +Usage: +{{ include "common.secrets.key" (dict "existingSecret" .Values.path.to.the.existingSecret "key" "keyName") }} + +Params: + - existingSecret - ExistingSecret/String - Optional. The path to the existing secrets in the values.yaml given by the user + to be used instead of the default one. Allows for it to be of type String (just the secret name) for backwards compatibility. + +info: https://github.com/bitnami/charts/tree/master/bitnami/common#existingsecret + - key - String - Required. Name of the key in the secret. +*/}} +{{- define "common.secrets.key" -}} +{{- $key := .key -}} + +{{- if .existingSecret -}} + {{- if not (typeIs "string" .existingSecret) -}} + {{- if .existingSecret.keyMapping -}} + {{- $key = index .existingSecret.keyMapping $.key -}} + {{- end -}} + {{- end }} +{{- end -}} + +{{- printf "%s" $key -}} +{{- end -}} + +{{/* +Generate secret password or retrieve one if already created. + +Usage: +{{ include "common.secrets.passwords.manage" (dict "secret" "secret-name" "key" "keyName" "providedValues" (list "path.to.password1" "path.to.password2") "length" 10 "strong" false "chartName" "chartName" "context" $) }} + +Params: + - secret - String - Required - Name of the 'Secret' resource where the password is stored. + - key - String - Required - Name of the key in the secret. + - providedValues - List - Required - The path to the validating value in the values.yaml, e.g: "mysql.password". Will pick first parameter with a defined value. + - length - int - Optional - Length of the generated random password. + - strong - Boolean - Optional - Whether to add symbols to the generated random password. + - chartName - String - Optional - Name of the chart used when said chart is deployed as a subchart. + - context - Context - Required - Parent context. +*/}} +{{- define "common.secrets.passwords.manage" -}} + +{{- $password := "" }} +{{- $subchart := "" }} +{{- $chartName := default "" .chartName }} +{{- $passwordLength := default 10 .length }} +{{- $providedPasswordKey := include "common.utils.getKeyFromList" (dict "keys" .providedValues "context" $.context) }} +{{- $providedPasswordValue := include "common.utils.getValueFromKey" (dict "key" $providedPasswordKey "context" $.context) }} +{{- $secretData := (lookup "v1" "Secret" $.context.Release.Namespace .secret).data }} +{{- if $secretData }} + {{- if hasKey $secretData .key }} + {{- $password = index $secretData .key }} + {{- else }} + {{- printf "\nPASSWORDS ERROR: The secret \"%s\" does not contain the key \"%s\"\n" .secret .key | fail -}} + {{- end -}} +{{- else if $providedPasswordValue }} + {{- $password = $providedPasswordValue | toString | b64enc | quote }} +{{- else }} + + {{- if .context.Values.enabled }} + {{- $subchart = $chartName }} + {{- end -}} + + {{- $requiredPassword := dict "valueKey" $providedPasswordKey "secret" .secret "field" .key "subchart" $subchart "context" $.context -}} + {{- $requiredPasswordError := include "common.validations.values.single.empty" $requiredPassword -}} + {{- $passwordValidationErrors := list $requiredPasswordError -}} + {{- include "common.errors.upgrade.passwords.empty" (dict "validationErrors" $passwordValidationErrors "context" $.context) -}} + + {{- if .strong }} + {{- $subStr := list (lower (randAlpha 1)) (randNumeric 1) (upper (randAlpha 1)) | join "_" }} + {{- $password = randAscii $passwordLength }} + {{- $password = regexReplaceAllLiteral "\\W" $password "@" | substr 5 $passwordLength }} + {{- $password = printf "%s%s" $subStr $password | toString | shuffle | b64enc | quote }} + {{- else }} + {{- $password = randAlphaNum $passwordLength | b64enc | quote }} + {{- end }} +{{- end -}} +{{- printf "%s" $password -}} +{{- end -}} + +{{/* +Returns whether a previous generated secret already exists + +Usage: +{{ include "common.secrets.exists" (dict "secret" "secret-name" "context" $) }} + +Params: + - secret - String - Required - Name of the 'Secret' resource where the password is stored. + - context - Context - Required - Parent context. +*/}} +{{- define "common.secrets.exists" -}} +{{- $secret := (lookup "v1" "Secret" $.context.Release.Namespace .secret) }} +{{- if $secret }} + {{- true -}} +{{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_storage.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_storage.tpl new file mode 100644 index 000000000000..60e2a844f6eb --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_storage.tpl @@ -0,0 +1,23 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Return the proper Storage Class +{{ include "common.storage.class" ( dict "persistence" .Values.path.to.the.persistence "global" $) }} +*/}} +{{- define "common.storage.class" -}} + +{{- $storageClass := .persistence.storageClass -}} +{{- if .global -}} + {{- if .global.storageClass -}} + {{- $storageClass = .global.storageClass -}} + {{- end -}} +{{- end -}} + +{{- if $storageClass -}} + {{- if (eq "-" $storageClass) -}} + {{- printf "storageClassName: \"\"" -}} + {{- else }} + {{- printf "storageClassName: %s" $storageClass -}} + {{- end -}} +{{- end -}} + +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_tplvalues.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_tplvalues.tpl new file mode 100644 index 000000000000..2db166851bb5 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_tplvalues.tpl @@ -0,0 +1,13 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Renders a value that contains template. +Usage: +{{ include "common.tplvalues.render" ( dict "value" .Values.path.to.the.Value "context" $) }} +*/}} +{{- define "common.tplvalues.render" -}} + {{- if typeIs "string" .value }} + {{- tpl .value .context }} + {{- else }} + {{- tpl (.value | toYaml) .context }} + {{- end }} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_utils.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_utils.tpl new file mode 100644 index 000000000000..ea083a249f80 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_utils.tpl @@ -0,0 +1,62 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Print instructions to get a secret value. +Usage: +{{ include "common.utils.secret.getvalue" (dict "secret" "secret-name" "field" "secret-value-field" "context" $) }} +*/}} +{{- define "common.utils.secret.getvalue" -}} +{{- $varname := include "common.utils.fieldToEnvVar" . -}} +export {{ $varname }}=$(kubectl get secret --namespace {{ .context.Release.Namespace | quote }} {{ .secret }} -o jsonpath="{.data.{{ .field }}}" | base64 --decode) +{{- end -}} + +{{/* +Build env var name given a field +Usage: +{{ include "common.utils.fieldToEnvVar" dict "field" "my-password" }} +*/}} +{{- define "common.utils.fieldToEnvVar" -}} + {{- $fieldNameSplit := splitList "-" .field -}} + {{- $upperCaseFieldNameSplit := list -}} + + {{- range $fieldNameSplit -}} + {{- $upperCaseFieldNameSplit = append $upperCaseFieldNameSplit ( upper . ) -}} + {{- end -}} + + {{ join "_" $upperCaseFieldNameSplit }} +{{- end -}} + +{{/* +Gets a value from .Values given +Usage: +{{ include "common.utils.getValueFromKey" (dict "key" "path.to.key" "context" $) }} +*/}} +{{- define "common.utils.getValueFromKey" -}} +{{- $splitKey := splitList "." .key -}} +{{- $value := "" -}} +{{- $latestObj := $.context.Values -}} +{{- range $splitKey -}} + {{- if not $latestObj -}} + {{- printf "please review the entire path of '%s' exists in values" $.key | fail -}} + {{- end -}} + {{- $value = ( index $latestObj . ) -}} + {{- $latestObj = $value -}} +{{- end -}} +{{- printf "%v" (default "" $value) -}} +{{- end -}} + +{{/* +Returns first .Values key with a defined value or first of the list if all non-defined +Usage: +{{ include "common.utils.getKeyFromList" (dict "keys" (list "path.to.key1" "path.to.key2") "context" $) }} +*/}} +{{- define "common.utils.getKeyFromList" -}} +{{- $key := first .keys -}} +{{- $reverseKeys := reverse .keys }} +{{- range $reverseKeys }} + {{- $value := include "common.utils.getValueFromKey" (dict "key" . "context" $.context ) }} + {{- if $value -}} + {{- $key = . }} + {{- end -}} +{{- end -}} +{{- printf "%s" $key -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_warnings.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_warnings.tpl new file mode 100644 index 000000000000..ae10fa41ee7d --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/_warnings.tpl @@ -0,0 +1,14 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Warning about using rolling tag. +Usage: +{{ include "common.warnings.rollingTag" .Values.path.to.the.imageRoot }} +*/}} +{{- define "common.warnings.rollingTag" -}} + +{{- if and (contains "bitnami/" .repository) (not (.tag | toString | regexFind "-r\\d+$|sha256:")) }} +WARNING: Rolling tag detected ({{ .repository }}:{{ .tag }}), please note that it is strongly recommended to avoid using rolling tags in a production environment. ++info https://docs.bitnami.com/containers/how-to/understand-rolling-tags-containers/ +{{- end }} + +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_cassandra.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_cassandra.tpl new file mode 100644 index 000000000000..ded1ae3bcad7 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_cassandra.tpl @@ -0,0 +1,72 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Validate Cassandra required passwords are not empty. + +Usage: +{{ include "common.validations.values.cassandra.passwords" (dict "secret" "secretName" "subchart" false "context" $) }} +Params: + - secret - String - Required. Name of the secret where Cassandra values are stored, e.g: "cassandra-passwords-secret" + - subchart - Boolean - Optional. Whether Cassandra is used as subchart or not. Default: false +*/}} +{{- define "common.validations.values.cassandra.passwords" -}} + {{- $existingSecret := include "common.cassandra.values.existingSecret" . -}} + {{- $enabled := include "common.cassandra.values.enabled" . -}} + {{- $dbUserPrefix := include "common.cassandra.values.key.dbUser" . -}} + {{- $valueKeyPassword := printf "%s.password" $dbUserPrefix -}} + + {{- if and (or (not $existingSecret) (eq $existingSecret "\"\"")) (eq $enabled "true") -}} + {{- $requiredPasswords := list -}} + + {{- $requiredPassword := dict "valueKey" $valueKeyPassword "secret" .secret "field" "cassandra-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredPassword -}} + + {{- include "common.validations.values.multiple.empty" (dict "required" $requiredPasswords "context" .context) -}} + + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for existingSecret. + +Usage: +{{ include "common.cassandra.values.existingSecret" (dict "context" $) }} +Params: + - subchart - Boolean - Optional. Whether Cassandra is used as subchart or not. Default: false +*/}} +{{- define "common.cassandra.values.existingSecret" -}} + {{- if .subchart -}} + {{- .context.Values.cassandra.dbUser.existingSecret | quote -}} + {{- else -}} + {{- .context.Values.dbUser.existingSecret | quote -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for enabled cassandra. + +Usage: +{{ include "common.cassandra.values.enabled" (dict "context" $) }} +*/}} +{{- define "common.cassandra.values.enabled" -}} + {{- if .subchart -}} + {{- printf "%v" .context.Values.cassandra.enabled -}} + {{- else -}} + {{- printf "%v" (not .context.Values.enabled) -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for the key dbUser + +Usage: +{{ include "common.cassandra.values.key.dbUser" (dict "subchart" "true" "context" $) }} +Params: + - subchart - Boolean - Optional. Whether Cassandra is used as subchart or not. Default: false +*/}} +{{- define "common.cassandra.values.key.dbUser" -}} + {{- if .subchart -}} + cassandra.dbUser + {{- else -}} + dbUser + {{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_mariadb.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_mariadb.tpl new file mode 100644 index 000000000000..b6906ff77b72 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_mariadb.tpl @@ -0,0 +1,103 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Validate MariaDB required passwords are not empty. + +Usage: +{{ include "common.validations.values.mariadb.passwords" (dict "secret" "secretName" "subchart" false "context" $) }} +Params: + - secret - String - Required. Name of the secret where MariaDB values are stored, e.g: "mysql-passwords-secret" + - subchart - Boolean - Optional. Whether MariaDB is used as subchart or not. Default: false +*/}} +{{- define "common.validations.values.mariadb.passwords" -}} + {{- $existingSecret := include "common.mariadb.values.auth.existingSecret" . -}} + {{- $enabled := include "common.mariadb.values.enabled" . -}} + {{- $architecture := include "common.mariadb.values.architecture" . -}} + {{- $authPrefix := include "common.mariadb.values.key.auth" . -}} + {{- $valueKeyRootPassword := printf "%s.rootPassword" $authPrefix -}} + {{- $valueKeyUsername := printf "%s.username" $authPrefix -}} + {{- $valueKeyPassword := printf "%s.password" $authPrefix -}} + {{- $valueKeyReplicationPassword := printf "%s.replicationPassword" $authPrefix -}} + + {{- if and (or (not $existingSecret) (eq $existingSecret "\"\"")) (eq $enabled "true") -}} + {{- $requiredPasswords := list -}} + + {{- $requiredRootPassword := dict "valueKey" $valueKeyRootPassword "secret" .secret "field" "mariadb-root-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredRootPassword -}} + + {{- $valueUsername := include "common.utils.getValueFromKey" (dict "key" $valueKeyUsername "context" .context) }} + {{- if not (empty $valueUsername) -}} + {{- $requiredPassword := dict "valueKey" $valueKeyPassword "secret" .secret "field" "mariadb-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredPassword -}} + {{- end -}} + + {{- if (eq $architecture "replication") -}} + {{- $requiredReplicationPassword := dict "valueKey" $valueKeyReplicationPassword "secret" .secret "field" "mariadb-replication-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredReplicationPassword -}} + {{- end -}} + + {{- include "common.validations.values.multiple.empty" (dict "required" $requiredPasswords "context" .context) -}} + + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for existingSecret. + +Usage: +{{ include "common.mariadb.values.auth.existingSecret" (dict "context" $) }} +Params: + - subchart - Boolean - Optional. Whether MariaDB is used as subchart or not. Default: false +*/}} +{{- define "common.mariadb.values.auth.existingSecret" -}} + {{- if .subchart -}} + {{- .context.Values.mariadb.auth.existingSecret | quote -}} + {{- else -}} + {{- .context.Values.auth.existingSecret | quote -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for enabled mariadb. + +Usage: +{{ include "common.mariadb.values.enabled" (dict "context" $) }} +*/}} +{{- define "common.mariadb.values.enabled" -}} + {{- if .subchart -}} + {{- printf "%v" .context.Values.mariadb.enabled -}} + {{- else -}} + {{- printf "%v" (not .context.Values.enabled) -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for architecture + +Usage: +{{ include "common.mariadb.values.architecture" (dict "subchart" "true" "context" $) }} +Params: + - subchart - Boolean - Optional. Whether MariaDB is used as subchart or not. Default: false +*/}} +{{- define "common.mariadb.values.architecture" -}} + {{- if .subchart -}} + {{- .context.Values.mariadb.architecture -}} + {{- else -}} + {{- .context.Values.architecture -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for the key auth + +Usage: +{{ include "common.mariadb.values.key.auth" (dict "subchart" "true" "context" $) }} +Params: + - subchart - Boolean - Optional. Whether MariaDB is used as subchart or not. Default: false +*/}} +{{- define "common.mariadb.values.key.auth" -}} + {{- if .subchart -}} + mariadb.auth + {{- else -}} + auth + {{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_mongodb.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_mongodb.tpl new file mode 100644 index 000000000000..a071ea4d3127 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_mongodb.tpl @@ -0,0 +1,108 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Validate MongoDB® required passwords are not empty. + +Usage: +{{ include "common.validations.values.mongodb.passwords" (dict "secret" "secretName" "subchart" false "context" $) }} +Params: + - secret - String - Required. Name of the secret where MongoDB® values are stored, e.g: "mongodb-passwords-secret" + - subchart - Boolean - Optional. Whether MongoDB® is used as subchart or not. Default: false +*/}} +{{- define "common.validations.values.mongodb.passwords" -}} + {{- $existingSecret := include "common.mongodb.values.auth.existingSecret" . -}} + {{- $enabled := include "common.mongodb.values.enabled" . -}} + {{- $authPrefix := include "common.mongodb.values.key.auth" . -}} + {{- $architecture := include "common.mongodb.values.architecture" . -}} + {{- $valueKeyRootPassword := printf "%s.rootPassword" $authPrefix -}} + {{- $valueKeyUsername := printf "%s.username" $authPrefix -}} + {{- $valueKeyDatabase := printf "%s.database" $authPrefix -}} + {{- $valueKeyPassword := printf "%s.password" $authPrefix -}} + {{- $valueKeyReplicaSetKey := printf "%s.replicaSetKey" $authPrefix -}} + {{- $valueKeyAuthEnabled := printf "%s.enabled" $authPrefix -}} + + {{- $authEnabled := include "common.utils.getValueFromKey" (dict "key" $valueKeyAuthEnabled "context" .context) -}} + + {{- if and (or (not $existingSecret) (eq $existingSecret "\"\"")) (eq $enabled "true") (eq $authEnabled "true") -}} + {{- $requiredPasswords := list -}} + + {{- $requiredRootPassword := dict "valueKey" $valueKeyRootPassword "secret" .secret "field" "mongodb-root-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredRootPassword -}} + + {{- $valueUsername := include "common.utils.getValueFromKey" (dict "key" $valueKeyUsername "context" .context) }} + {{- $valueDatabase := include "common.utils.getValueFromKey" (dict "key" $valueKeyDatabase "context" .context) }} + {{- if and $valueUsername $valueDatabase -}} + {{- $requiredPassword := dict "valueKey" $valueKeyPassword "secret" .secret "field" "mongodb-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredPassword -}} + {{- end -}} + + {{- if (eq $architecture "replicaset") -}} + {{- $requiredReplicaSetKey := dict "valueKey" $valueKeyReplicaSetKey "secret" .secret "field" "mongodb-replica-set-key" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredReplicaSetKey -}} + {{- end -}} + + {{- include "common.validations.values.multiple.empty" (dict "required" $requiredPasswords "context" .context) -}} + + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for existingSecret. + +Usage: +{{ include "common.mongodb.values.auth.existingSecret" (dict "context" $) }} +Params: + - subchart - Boolean - Optional. Whether MongoDb is used as subchart or not. Default: false +*/}} +{{- define "common.mongodb.values.auth.existingSecret" -}} + {{- if .subchart -}} + {{- .context.Values.mongodb.auth.existingSecret | quote -}} + {{- else -}} + {{- .context.Values.auth.existingSecret | quote -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for enabled mongodb. + +Usage: +{{ include "common.mongodb.values.enabled" (dict "context" $) }} +*/}} +{{- define "common.mongodb.values.enabled" -}} + {{- if .subchart -}} + {{- printf "%v" .context.Values.mongodb.enabled -}} + {{- else -}} + {{- printf "%v" (not .context.Values.enabled) -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for the key auth + +Usage: +{{ include "common.mongodb.values.key.auth" (dict "subchart" "true" "context" $) }} +Params: + - subchart - Boolean - Optional. Whether MongoDB® is used as subchart or not. Default: false +*/}} +{{- define "common.mongodb.values.key.auth" -}} + {{- if .subchart -}} + mongodb.auth + {{- else -}} + auth + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for architecture + +Usage: +{{ include "common.mongodb.values.architecture" (dict "subchart" "true" "context" $) }} +Params: + - subchart - Boolean - Optional. Whether MariaDB is used as subchart or not. Default: false +*/}} +{{- define "common.mongodb.values.architecture" -}} + {{- if .subchart -}} + {{- .context.Values.mongodb.architecture -}} + {{- else -}} + {{- .context.Values.architecture -}} + {{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_postgresql.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_postgresql.tpl new file mode 100644 index 000000000000..164ec0d01252 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_postgresql.tpl @@ -0,0 +1,129 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Validate PostgreSQL required passwords are not empty. + +Usage: +{{ include "common.validations.values.postgresql.passwords" (dict "secret" "secretName" "subchart" false "context" $) }} +Params: + - secret - String - Required. Name of the secret where postgresql values are stored, e.g: "postgresql-passwords-secret" + - subchart - Boolean - Optional. Whether postgresql is used as subchart or not. Default: false +*/}} +{{- define "common.validations.values.postgresql.passwords" -}} + {{- $existingSecret := include "common.postgresql.values.existingSecret" . -}} + {{- $enabled := include "common.postgresql.values.enabled" . -}} + {{- $valueKeyPostgresqlPassword := include "common.postgresql.values.key.postgressPassword" . -}} + {{- $valueKeyPostgresqlReplicationEnabled := include "common.postgresql.values.key.replicationPassword" . -}} + {{- if and (or (not $existingSecret) (eq $existingSecret "\"\"")) (eq $enabled "true") -}} + {{- $requiredPasswords := list -}} + {{- $requiredPostgresqlPassword := dict "valueKey" $valueKeyPostgresqlPassword "secret" .secret "field" "postgresql-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredPostgresqlPassword -}} + + {{- $enabledReplication := include "common.postgresql.values.enabled.replication" . -}} + {{- if (eq $enabledReplication "true") -}} + {{- $requiredPostgresqlReplicationPassword := dict "valueKey" $valueKeyPostgresqlReplicationEnabled "secret" .secret "field" "postgresql-replication-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredPostgresqlReplicationPassword -}} + {{- end -}} + + {{- include "common.validations.values.multiple.empty" (dict "required" $requiredPasswords "context" .context) -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to decide whether evaluate global values. + +Usage: +{{ include "common.postgresql.values.use.global" (dict "key" "key-of-global" "context" $) }} +Params: + - key - String - Required. Field to be evaluated within global, e.g: "existingSecret" +*/}} +{{- define "common.postgresql.values.use.global" -}} + {{- if .context.Values.global -}} + {{- if .context.Values.global.postgresql -}} + {{- index .context.Values.global.postgresql .key | quote -}} + {{- end -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for existingSecret. + +Usage: +{{ include "common.postgresql.values.existingSecret" (dict "context" $) }} +*/}} +{{- define "common.postgresql.values.existingSecret" -}} + {{- $globalValue := include "common.postgresql.values.use.global" (dict "key" "existingSecret" "context" .context) -}} + + {{- if .subchart -}} + {{- default (.context.Values.postgresql.existingSecret | quote) $globalValue -}} + {{- else -}} + {{- default (.context.Values.existingSecret | quote) $globalValue -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for enabled postgresql. + +Usage: +{{ include "common.postgresql.values.enabled" (dict "context" $) }} +*/}} +{{- define "common.postgresql.values.enabled" -}} + {{- if .subchart -}} + {{- printf "%v" .context.Values.postgresql.enabled -}} + {{- else -}} + {{- printf "%v" (not .context.Values.enabled) -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for the key postgressPassword. + +Usage: +{{ include "common.postgresql.values.key.postgressPassword" (dict "subchart" "true" "context" $) }} +Params: + - subchart - Boolean - Optional. Whether postgresql is used as subchart or not. Default: false +*/}} +{{- define "common.postgresql.values.key.postgressPassword" -}} + {{- $globalValue := include "common.postgresql.values.use.global" (dict "key" "postgresqlUsername" "context" .context) -}} + + {{- if not $globalValue -}} + {{- if .subchart -}} + postgresql.postgresqlPassword + {{- else -}} + postgresqlPassword + {{- end -}} + {{- else -}} + global.postgresql.postgresqlPassword + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for enabled.replication. + +Usage: +{{ include "common.postgresql.values.enabled.replication" (dict "subchart" "true" "context" $) }} +Params: + - subchart - Boolean - Optional. Whether postgresql is used as subchart or not. Default: false +*/}} +{{- define "common.postgresql.values.enabled.replication" -}} + {{- if .subchart -}} + {{- printf "%v" .context.Values.postgresql.replication.enabled -}} + {{- else -}} + {{- printf "%v" .context.Values.replication.enabled -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for the key replication.password. + +Usage: +{{ include "common.postgresql.values.key.replicationPassword" (dict "subchart" "true" "context" $) }} +Params: + - subchart - Boolean - Optional. Whether postgresql is used as subchart or not. Default: false +*/}} +{{- define "common.postgresql.values.key.replicationPassword" -}} + {{- if .subchart -}} + postgresql.replication.password + {{- else -}} + replication.password + {{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_redis.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_redis.tpl new file mode 100644 index 000000000000..5d72959b9eee --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_redis.tpl @@ -0,0 +1,76 @@ + +{{/* vim: set filetype=mustache: */}} +{{/* +Validate Redis™ required passwords are not empty. + +Usage: +{{ include "common.validations.values.redis.passwords" (dict "secret" "secretName" "subchart" false "context" $) }} +Params: + - secret - String - Required. Name of the secret where redis values are stored, e.g: "redis-passwords-secret" + - subchart - Boolean - Optional. Whether redis is used as subchart or not. Default: false +*/}} +{{- define "common.validations.values.redis.passwords" -}} + {{- $enabled := include "common.redis.values.enabled" . -}} + {{- $valueKeyPrefix := include "common.redis.values.keys.prefix" . -}} + {{- $standarizedVersion := include "common.redis.values.standarized.version" . }} + + {{- $existingSecret := ternary (printf "%s%s" $valueKeyPrefix "auth.existingSecret") (printf "%s%s" $valueKeyPrefix "existingSecret") (eq $standarizedVersion "true") }} + {{- $existingSecretValue := include "common.utils.getValueFromKey" (dict "key" $existingSecret "context" .context) }} + + {{- $valueKeyRedisPassword := ternary (printf "%s%s" $valueKeyPrefix "auth.password") (printf "%s%s" $valueKeyPrefix "password") (eq $standarizedVersion "true") }} + {{- $valueKeyRedisUseAuth := ternary (printf "%s%s" $valueKeyPrefix "auth.enabled") (printf "%s%s" $valueKeyPrefix "usePassword") (eq $standarizedVersion "true") }} + + {{- if and (or (not $existingSecret) (eq $existingSecret "\"\"")) (eq $enabled "true") -}} + {{- $requiredPasswords := list -}} + + {{- $useAuth := include "common.utils.getValueFromKey" (dict "key" $valueKeyRedisUseAuth "context" .context) -}} + {{- if eq $useAuth "true" -}} + {{- $requiredRedisPassword := dict "valueKey" $valueKeyRedisPassword "secret" .secret "field" "redis-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredRedisPassword -}} + {{- end -}} + + {{- include "common.validations.values.multiple.empty" (dict "required" $requiredPasswords "context" .context) -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right value for enabled redis. + +Usage: +{{ include "common.redis.values.enabled" (dict "context" $) }} +*/}} +{{- define "common.redis.values.enabled" -}} + {{- if .subchart -}} + {{- printf "%v" .context.Values.redis.enabled -}} + {{- else -}} + {{- printf "%v" (not .context.Values.enabled) -}} + {{- end -}} +{{- end -}} + +{{/* +Auxiliary function to get the right prefix path for the values + +Usage: +{{ include "common.redis.values.key.prefix" (dict "subchart" "true" "context" $) }} +Params: + - subchart - Boolean - Optional. Whether redis is used as subchart or not. Default: false +*/}} +{{- define "common.redis.values.keys.prefix" -}} + {{- if .subchart -}}redis.{{- else -}}{{- end -}} +{{- end -}} + +{{/* +Checks whether the redis chart's includes the standarizations (version >= 14) + +Usage: +{{ include "common.redis.values.standarized.version" (dict "context" $) }} +*/}} +{{- define "common.redis.values.standarized.version" -}} + + {{- $standarizedAuth := printf "%s%s" (include "common.redis.values.keys.prefix" .) "auth" -}} + {{- $standarizedAuthValues := include "common.utils.getValueFromKey" (dict "key" $standarizedAuth "context" .context) }} + + {{- if $standarizedAuthValues -}} + {{- true -}} + {{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_validations.tpl b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_validations.tpl new file mode 100644 index 000000000000..9a814cf40dcb --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/templates/validations/_validations.tpl @@ -0,0 +1,46 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Validate values must not be empty. + +Usage: +{{- $validateValueConf00 := (dict "valueKey" "path.to.value" "secret" "secretName" "field" "password-00") -}} +{{- $validateValueConf01 := (dict "valueKey" "path.to.value" "secret" "secretName" "field" "password-01") -}} +{{ include "common.validations.values.empty" (dict "required" (list $validateValueConf00 $validateValueConf01) "context" $) }} + +Validate value params: + - valueKey - String - Required. The path to the validating value in the values.yaml, e.g: "mysql.password" + - secret - String - Optional. Name of the secret where the validating value is generated/stored, e.g: "mysql-passwords-secret" + - field - String - Optional. Name of the field in the secret data, e.g: "mysql-password" +*/}} +{{- define "common.validations.values.multiple.empty" -}} + {{- range .required -}} + {{- include "common.validations.values.single.empty" (dict "valueKey" .valueKey "secret" .secret "field" .field "context" $.context) -}} + {{- end -}} +{{- end -}} + +{{/* +Validate a value must not be empty. + +Usage: +{{ include "common.validations.value.empty" (dict "valueKey" "mariadb.password" "secret" "secretName" "field" "my-password" "subchart" "subchart" "context" $) }} + +Validate value params: + - valueKey - String - Required. The path to the validating value in the values.yaml, e.g: "mysql.password" + - secret - String - Optional. Name of the secret where the validating value is generated/stored, e.g: "mysql-passwords-secret" + - field - String - Optional. Name of the field in the secret data, e.g: "mysql-password" + - subchart - String - Optional - Name of the subchart that the validated password is part of. +*/}} +{{- define "common.validations.values.single.empty" -}} + {{- $value := include "common.utils.getValueFromKey" (dict "key" .valueKey "context" .context) }} + {{- $subchart := ternary "" (printf "%s." .subchart) (empty .subchart) }} + + {{- if not $value -}} + {{- $varname := "my-value" -}} + {{- $getCurrentValue := "" -}} + {{- if and .secret .field -}} + {{- $varname = include "common.utils.fieldToEnvVar" . -}} + {{- $getCurrentValue = printf " To get the current value:\n\n %s\n" (include "common.utils.secret.getvalue" .) -}} + {{- end -}} + {{- printf "\n '%s' must not be empty, please add '--set %s%s=$%s' to the command.%s" .valueKey $subchart .valueKey $varname $getCurrentValue -}} + {{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/charts/common/values.yaml b/pkg/iac/scanners/helm/test/mysql/charts/common/values.yaml new file mode 100644 index 000000000000..f2df68e5e6af --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/charts/common/values.yaml @@ -0,0 +1,5 @@ +## bitnami/common +## It is required by CI/CD tools and processes. +## @skip exampleValue +## +exampleValue: common-chart diff --git a/pkg/iac/scanners/helm/test/mysql/ci/values-production-with-rbac.yaml b/pkg/iac/scanners/helm/test/mysql/ci/values-production-with-rbac.yaml new file mode 100644 index 000000000000..d3370c931113 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/ci/values-production-with-rbac.yaml @@ -0,0 +1,30 @@ +# Test values file for generating all of the yaml and check that +# the rendering is correct + +architecture: replication +auth: + usePasswordFiles: true + +primary: + extraEnvVars: + - name: TEST + value: "3" + podDisruptionBudget: + create: true + +secondary: + replicaCount: 2 + extraEnvVars: + - name: TEST + value: "2" + podDisruptionBudget: + create: true + +serviceAccount: + create: true + name: mysql-service-account +rbac: + create: true + +metrics: + enabled: true diff --git a/pkg/iac/scanners/helm/test/mysql/templates/NOTES.txt b/pkg/iac/scanners/helm/test/mysql/templates/NOTES.txt new file mode 100644 index 000000000000..1b8b6d5ea7d2 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/NOTES.txt @@ -0,0 +1,102 @@ +CHART NAME: {{ .Chart.Name }} +CHART VERSION: {{ .Chart.Version }} +APP VERSION: {{ .Chart.AppVersion }} + +** Please be patient while the chart is being deployed ** + +{{- if .Values.diagnosticMode.enabled }} +The chart has been deployed in diagnostic mode. All probes have been disabled and the command has been overwritten with: + + command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 4 }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 4 }} + +Get the list of pods by executing: + + kubectl get pods --namespace {{ .Release.Namespace }} -l app.kubernetes.io/instance={{ .Release.Name }} + +Access the pod you want to debug by executing + + kubectl exec --namespace {{ .Release.Namespace }} -ti -- bash + +In order to replicate the container startup scripts execute this command: + + /opt/bitnami/scripts/mysql/entrypoint.sh /opt/bitnami/scripts/mysql/run.sh + +{{- else }} + +Tip: + + Watch the deployment status using the command: kubectl get pods -w --namespace {{ .Release.Namespace }} + +Services: + + echo Primary: {{ include "mysql.primary.fullname" . }}.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }}:{{ .Values.primary.service.port }} +{{- if eq .Values.architecture "replication" }} + echo Secondary: {{ include "mysql.secondary.fullname" . }}.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }}:{{ .Values.secondary.service.port }} +{{- end }} + +Execute the following to get the administrator credentials: + + echo Username: root + MYSQL_ROOT_PASSWORD=$(kubectl get secret --namespace {{ .Release.Namespace }} {{ template "mysql.secretName" . }} -o jsonpath="{.data.mysql-root-password}" | base64 --decode) + +To connect to your database: + + 1. Run a pod that you can use as a client: + + kubectl run {{ include "common.names.fullname" . }}-client --rm --tty -i --restart='Never' --image {{ template "mysql.image" . }} --namespace {{ .Release.Namespace }} --command -- bash + + 2. To connect to primary service (read/write): + + mysql -h {{ include "mysql.primary.fullname" . }}.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }} -uroot -p"$MYSQL_ROOT_PASSWORD" + +{{- if eq .Values.architecture "replication" }} + + 3. To connect to secondary service (read-only): + + mysql -h {{ include "mysql.secondary.fullname" . }}.{{ .Release.Namespace }}.svc.{{ .Values.clusterDomain }} -uroot -p"$MYSQL_ROOT_PASSWORD" +{{- end }} + +{{ if and (.Values.networkPolicy.enabled) (not .Values.networkPolicy.allowExternal) }} +Note: Since NetworkPolicy is enabled, only pods with label {{ template "common.names.fullname" . }}-client=true" will be able to connect to MySQL. +{{- end }} + +{{- if .Values.metrics.enabled }} + +To access the MySQL Prometheus metrics from outside the cluster execute the following commands: + + kubectl port-forward --namespace {{ .Release.Namespace }} svc/{{ printf "%s-metrics" (include "common.names.fullname" .) }} {{ .Values.metrics.service.port }}:{{ .Values.metrics.service.port }} & + curl http://127.0.0.1:{{ .Values.metrics.service.port }}/metrics + +{{- end }} + +To upgrade this helm chart: + + 1. Obtain the password as described on the 'Administrator credentials' section and set the 'root.password' parameter as shown below: + + ROOT_PASSWORD=$(kubectl get secret --namespace {{ .Release.Namespace }} {{ include "common.names.fullname" . }} -o jsonpath="{.data.mysql-root-password}" | base64 --decode) + helm upgrade --namespace {{ .Release.Namespace }} {{ .Release.Name }} bitnami/mysql --set auth.rootPassword=$ROOT_PASSWORD + +{{ include "mysql.validateValues" . }} +{{ include "mysql.checkRollingTags" . }} +{{- if and (not .Values.auth.existingSecret) (not .Values.auth.customPasswordFiles) -}} + {{- $secretName := include "mysql.secretName" . -}} + {{- $requiredPasswords := list -}} + + {{- $requiredRootPassword := dict "valueKey" "auth.rootPassword" "secret" $secretName "field" "mysql-root-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredRootPassword -}} + + {{- if not (empty .Values.auth.username) -}} + {{- $requiredPassword := dict "valueKey" "auth.password" "secret" $secretName "field" "mysql-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredPassword -}} + {{- end -}} + + {{- if (eq .Values.architecture "replication") -}} + {{- $requiredReplicationPassword := dict "valueKey" "auth.replicationPassword" "secret" $secretName "field" "mysql-replication-password" -}} + {{- $requiredPasswords = append $requiredPasswords $requiredReplicationPassword -}} + {{- end -}} + + {{- $mysqlPasswordValidationErrors := include "common.validations.values.multiple.empty" (dict "required" $requiredPasswords "context" $) -}} + {{- include "common.errors.upgrade.passwords.empty" (dict "validationErrors" $mysqlPasswordValidationErrors "context" $) -}} +{{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/_helpers.tpl b/pkg/iac/scanners/helm/test/mysql/templates/_helpers.tpl new file mode 100644 index 000000000000..6c2bcff81398 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/_helpers.tpl @@ -0,0 +1,192 @@ +{{/* vim: set filetype=mustache: */}} + +{{- define "mysql.primary.fullname" -}} +{{- if eq .Values.architecture "replication" }} +{{- printf "%s-%s" (include "common.names.fullname" .) "primary" | trunc 63 | trimSuffix "-" -}} +{{- else -}} +{{- include "common.names.fullname" . -}} +{{- end -}} +{{- end -}} + +{{- define "mysql.secondary.fullname" -}} +{{- printf "%s-%s" (include "common.names.fullname" .) "secondary" | trunc 63 | trimSuffix "-" -}} +{{- end -}} + +{{/* +Return the proper MySQL image name +*/}} +{{- define "mysql.image" -}} +{{ include "common.images.image" (dict "imageRoot" .Values.image "global" .Values.global) }} +{{- end -}} + +{{/* +Return the proper metrics image name +*/}} +{{- define "mysql.metrics.image" -}} +{{ include "common.images.image" (dict "imageRoot" .Values.metrics.image "global" .Values.global) }} +{{- end -}} + +{{/* +Return the proper image name (for the init container volume-permissions image) +*/}} +{{- define "mysql.volumePermissions.image" -}} +{{ include "common.images.image" (dict "imageRoot" .Values.volumePermissions.image "global" .Values.global) }} +{{- end -}} + +{{/* +Return the proper Docker Image Registry Secret Names +*/}} +{{- define "mysql.imagePullSecrets" -}} +{{ include "common.images.pullSecrets" (dict "images" (list .Values.image .Values.metrics.image .Values.volumePermissions.image) "global" .Values.global) }} +{{- end -}} + +{{ template "mysql.initdbScriptsCM" . }} +{{/* +Get the initialization scripts ConfigMap name. +*/}} +{{- define "mysql.initdbScriptsCM" -}} +{{- if .Values.initdbScriptsConfigMap -}} + {{- printf "%s" .Values.initdbScriptsConfigMap -}} +{{- else -}} + {{- printf "%s-init-scripts" (include "mysql.primary.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{/* + Returns the proper service account name depending if an explicit service account name is set + in the values file. If the name is not set it will default to either mysql.fullname if serviceAccount.create + is true or default otherwise. +*/}} +{{- define "mysql.serviceAccountName" -}} + {{- if .Values.serviceAccount.create -}} + {{ default (include "common.names.fullname" .) .Values.serviceAccount.name }} + {{- else -}} + {{ default "default" .Values.serviceAccount.name }} + {{- end -}} +{{- end -}} + +{{/* +Return the configmap with the MySQL Primary configuration +*/}} +{{- define "mysql.primary.configmapName" -}} +{{- if .Values.primary.existingConfigmap -}} + {{- printf "%s" (tpl .Values.primary.existingConfigmap $) -}} +{{- else -}} + {{- printf "%s" (include "mysql.primary.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{/* +Return true if a configmap object should be created for MySQL Secondary +*/}} +{{- define "mysql.primary.createConfigmap" -}} +{{- if and .Values.primary.configuration (not .Values.primary.existingConfigmap) }} + {{- true -}} +{{- else -}} +{{- end -}} +{{- end -}} + +{{/* +Return the configmap with the MySQL Primary configuration +*/}} +{{- define "mysql.secondary.configmapName" -}} +{{- if .Values.secondary.existingConfigmap -}} + {{- printf "%s" (tpl .Values.secondary.existingConfigmap $) -}} +{{- else -}} + {{- printf "%s" (include "mysql.secondary.fullname" .) -}} +{{- end -}} +{{- end -}} + +{{/* +Return true if a configmap object should be created for MySQL Secondary +*/}} +{{- define "mysql.secondary.createConfigmap" -}} +{{- if and (eq .Values.architecture "replication") .Values.secondary.configuration (not .Values.secondary.existingConfigmap) }} + {{- true -}} +{{- else -}} +{{- end -}} +{{- end -}} + +{{/* +Return the secret with MySQL credentials +*/}} +{{- define "mysql.secretName" -}} + {{- if .Values.auth.existingSecret -}} + {{- printf "%s" .Values.auth.existingSecret -}} + {{- else -}} + {{- printf "%s" (include "common.names.fullname" .) -}} + {{- end -}} +{{- end -}} + +{{/* +Return true if a secret object should be created for MySQL +*/}} +{{- define "mysql.createSecret" -}} +{{- if and (not .Values.auth.existingSecret) (not .Values.auth.customPasswordFiles) }} + {{- true -}} +{{- end -}} +{{- end -}} + +{{/* +Returns the available value for certain key in an existing secret (if it exists), +otherwise it generates a random value. +*/}} +{{- define "getValueFromSecret" }} + {{- $len := (default 16 .Length) | int -}} + {{- $obj := (lookup "v1" "Secret" .Namespace .Name).data -}} + {{- if $obj }} + {{- index $obj .Key | b64dec -}} + {{- else -}} + {{- randAlphaNum $len -}} + {{- end -}} +{{- end }} + +{{- define "mysql.root.password" -}} + {{- if not (empty .Values.auth.rootPassword) }} + {{- .Values.auth.rootPassword }} + {{- else if (not .Values.auth.forcePassword) }} + {{- include "getValueFromSecret" (dict "Namespace" .Release.Namespace "Name" (include "common.names.fullname" .) "Length" 10 "Key" "mysql-root-password") }} + {{- else }} + {{- required "A MySQL Root Password is required!" .Values.auth.rootPassword }} + {{- end }} +{{- end -}} + +{{- define "mysql.password" -}} + {{- if and (not (empty .Values.auth.username)) (not (empty .Values.auth.password)) }} + {{- .Values.auth.password }} + {{- else if (not .Values.auth.forcePassword) }} + {{- include "getValueFromSecret" (dict "Namespace" .Release.Namespace "Name" (include "common.names.fullname" .) "Length" 10 "Key" "mysql-password") }} + {{- else }} + {{- required "A MySQL Database Password is required!" .Values.auth.password }} + {{- end }} +{{- end -}} + +{{- define "mysql.replication.password" -}} + {{- if not (empty .Values.auth.replicationPassword) }} + {{- .Values.auth.replicationPassword }} + {{- else if (not .Values.auth.forcePassword) }} + {{- include "getValueFromSecret" (dict "Namespace" .Release.Namespace "Name" (include "common.names.fullname" .) "Length" 10 "Key" "mysql-replication-password") }} + {{- else }} + {{- required "A MySQL Replication Password is required!" .Values.auth.replicationPassword }} + {{- end }} +{{- end -}} + +{{/* Check if there are rolling tags in the images */}} +{{- define "mysql.checkRollingTags" -}} +{{- include "common.warnings.rollingTag" .Values.image }} +{{- include "common.warnings.rollingTag" .Values.metrics.image }} +{{- include "common.warnings.rollingTag" .Values.volumePermissions.image }} +{{- end -}} + +{{/* +Compile all warnings into a single message, and call fail. +*/}} +{{- define "mysql.validateValues" -}} +{{- $messages := list -}} +{{- $messages := without $messages "" -}} +{{- $message := join "\n" $messages -}} + +{{- if $message -}} +{{- printf "\nVALUES VALIDATION:\n%s" $message | fail -}} +{{- end -}} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/extra-list.yaml b/pkg/iac/scanners/helm/test/mysql/templates/extra-list.yaml new file mode 100644 index 000000000000..9ac65f9e16f4 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/extra-list.yaml @@ -0,0 +1,4 @@ +{{- range .Values.extraDeploy }} +--- +{{ include "common.tplvalues.render" (dict "value" . "context" $) }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/metrics-svc.yaml b/pkg/iac/scanners/helm/test/mysql/templates/metrics-svc.yaml new file mode 100644 index 000000000000..fb0d9d761dc6 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/metrics-svc.yaml @@ -0,0 +1,29 @@ +{{- if .Values.metrics.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ printf "%s-metrics" (include "common.names.fullname" .) }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + app.kubernetes.io/component: metrics + {{- if or .Values.metrics.service.annotations .Values.commonAnnotations }} + annotations: + {{- if .Values.metrics.service.annotations }} + {{- include "common.tplvalues.render" (dict "value" .Values.metrics.service.annotations "context" $) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} + {{- end }} +spec: + type: {{ .Values.metrics.service.type }} + ports: + - port: {{ .Values.metrics.service.port }} + targetPort: metrics + protocol: TCP + name: metrics + selector: {{- include "common.labels.matchLabels" $ | nindent 4 }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/networkpolicy.yaml b/pkg/iac/scanners/helm/test/mysql/templates/networkpolicy.yaml new file mode 100644 index 000000000000..a0d1d01d4079 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/networkpolicy.yaml @@ -0,0 +1,38 @@ +{{- if .Values.networkPolicy.enabled }} +kind: NetworkPolicy +apiVersion: {{ template "common.capabilities.networkPolicy.apiVersion" . }} +metadata: + name: {{ template "common.names.fullname" . }} + labels: + {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} + namespace: {{ .Release.Namespace }} +spec: + podSelector: + matchLabels: + {{- include "common.labels.matchLabels" . | nindent 6 }} + ingress: + # Allow inbound connections + - ports: + - port: {{ .Values.primary.service.port }} + {{- if not .Values.networkPolicy.allowExternal }} + from: + - podSelector: + matchLabels: + {{ template "common.names.fullname" . }}-client: "true" + {{- if .Values.networkPolicy.explicitNamespacesSelector }} + namespaceSelector: +{{ toYaml .Values.networkPolicy.explicitNamespacesSelector | indent 12 }} + {{- end }} + - podSelector: + matchLabels: + {{- include "common.labels.matchLabels" . | nindent 14 }} + {{- end }} + {{- if .Values.metrics.enabled }} + # Allow prometheus scrapes + - ports: + - port: 9104 + {{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/primary/configmap.yaml b/pkg/iac/scanners/helm/test/mysql/templates/primary/configmap.yaml new file mode 100644 index 000000000000..540b7b9072e9 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/primary/configmap.yaml @@ -0,0 +1,18 @@ +{{- if (include "mysql.primary.createConfigmap" .) }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "mysql.primary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: primary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +data: + my.cnf: |- + {{ .Values.primary.configuration | nindent 4 }} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/primary/initialization-configmap.yaml b/pkg/iac/scanners/helm/test/mysql/templates/primary/initialization-configmap.yaml new file mode 100644 index 000000000000..83cbaea74883 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/primary/initialization-configmap.yaml @@ -0,0 +1,14 @@ +{{- if and .Values.initdbScripts (not .Values.initdbScriptsConfigMap) }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ printf "%s-init-scripts" (include "mysql.primary.fullname" .) }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: primary + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +data: +{{- include "common.tplvalues.render" (dict "value" .Values.initdbScripts "context" .) | nindent 2 }} +{{ end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/primary/pdb.yaml b/pkg/iac/scanners/helm/test/mysql/templates/primary/pdb.yaml new file mode 100644 index 000000000000..106ad5207e5a --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/primary/pdb.yaml @@ -0,0 +1,25 @@ +{{- if .Values.primary.pdb.enabled }} +apiVersion: {{ include "common.capabilities.policy.apiVersion" . }} +kind: PodDisruptionBudget +metadata: + name: {{ include "mysql.primary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: primary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- if .Values.primary.pdb.minAvailable }} + minAvailable: {{ .Values.primary.pdb.minAvailable }} + {{- end }} + {{- if .Values.primary.pdb.maxUnavailable }} + maxUnavailable: {{ .Values.primary.pdb.maxUnavailable }} + {{- end }} + selector: + matchLabels: {{ include "common.labels.matchLabels" . | nindent 6 }} + app.kubernetes.io/component: primary +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/primary/statefulset.yaml b/pkg/iac/scanners/helm/test/mysql/templates/primary/statefulset.yaml new file mode 100644 index 000000000000..6f9c99ea66d9 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/primary/statefulset.yaml @@ -0,0 +1,368 @@ +apiVersion: {{ include "common.capabilities.statefulset.apiVersion" . }} +kind: StatefulSet +metadata: + name: {{ include "mysql.primary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: primary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.primary.podLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.primary.podLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + replicas: 1 + selector: + matchLabels: {{ include "common.labels.matchLabels" . | nindent 6 }} + app.kubernetes.io/component: primary + serviceName: {{ include "mysql.primary.fullname" . }} + updateStrategy: + type: {{ .Values.primary.updateStrategy }} + {{- if (eq "Recreate" .Values.primary.updateStrategy) }} + rollingUpdate: null + {{- else if .Values.primary.rollingUpdatePartition }} + rollingUpdate: + partition: {{ .Values.primary.rollingUpdatePartition }} + {{- end }} + template: + metadata: + annotations: + {{- if (include "mysql.primary.createConfigmap" .) }} + checksum/configuration: {{ include (print $.Template.BasePath "/primary/configmap.yaml") . | sha256sum }} + {{- end }} + {{- if .Values.primary.podAnnotations }} + {{- include "common.tplvalues.render" (dict "value" .Values.primary.podAnnotations "context" $) | nindent 8 }} + {{- end }} + labels: {{- include "common.labels.standard" . | nindent 8 }} + app.kubernetes.io/component: primary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 8 }} + {{- end }} + {{- if .Values.primary.podLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.primary.podLabels "context" $ ) | nindent 8 }} + {{- end }} + spec: + {{- include "mysql.imagePullSecrets" . | nindent 6 }} + {{- if .Values.primary.hostAliases }} + hostAliases: {{- include "common.tplvalues.render" (dict "value" .Values.primary.hostAliases "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.schedulerName }} + schedulerName: {{ .Values.schedulerName | quote }} + {{- end }} + serviceAccountName: {{ template "mysql.serviceAccountName" . }} + {{- if .Values.primary.affinity }} + affinity: {{- include "common.tplvalues.render" (dict "value" .Values.primary.affinity "context" $) | nindent 8 }} + {{- else }} + affinity: + podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.primary.podAffinityPreset "component" "primary" "context" $) | nindent 10 }} + podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.primary.podAntiAffinityPreset "component" "primary" "context" $) | nindent 10 }} + nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.primary.nodeAffinityPreset.type "key" .Values.primary.nodeAffinityPreset.key "values" .Values.primary.nodeAffinityPreset.values) | nindent 10 }} + {{- end }} + {{- if .Values.primary.nodeSelector }} + nodeSelector: {{- include "common.tplvalues.render" (dict "value" .Values.primary.nodeSelector "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.primary.tolerations }} + tolerations: {{- include "common.tplvalues.render" (dict "value" .Values.primary.tolerations "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName | quote }} + {{- end }} + {{- if .Values.primary.podSecurityContext.enabled }} + securityContext: {{- omit .Values.primary.podSecurityContext "enabled" | toYaml | nindent 8 }} + {{- end }} + {{- if or .Values.primary.initContainers (and .Values.primary.podSecurityContext.enabled .Values.volumePermissions.enabled .Values.primary.persistence.enabled) }} + initContainers: + {{- if .Values.primary.initContainers }} + {{- include "common.tplvalues.render" (dict "value" .Values.primary.initContainers "context" $) | nindent 8 }} + {{- end }} + {{- if and .Values.primary.podSecurityContext.enabled .Values.volumePermissions.enabled .Values.primary.persistence.enabled }} + - name: volume-permissions + image: {{ include "mysql.volumePermissions.image" . }} + imagePullPolicy: {{ .Values.volumePermissions.image.pullPolicy | quote }} + command: + - /bin/bash + - -ec + - | + chown -R {{ .Values.primary.containerSecurityContext.runAsUser }}:{{ .Values.primary.podSecurityContext.fsGroup }} /bitnami/mysql + securityContext: + runAsUser: 0 + {{- if .Values.volumePermissions.resources }} + resources: {{- toYaml .Values.volumePermissions.resources | nindent 12 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /bitnami/mysql + {{- end }} + {{- end }} + containers: + - name: mysql + image: {{ include "mysql.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + {{- if .Values.primary.containerSecurityContext.enabled }} + securityContext: {{- omit .Values.primary.containerSecurityContext "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 12 }} + {{- else if .Values.primary.command }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.primary.command "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 12 }} + {{- else if .Values.primary.args }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.primary.args "context" $) | nindent 12 }} + {{- end }} + env: + - name: BITNAMI_DEBUG + value: {{ ternary "true" "false" (or .Values.image.debug .Values.diagnosticMode.enabled) | quote }} + {{- if .Values.auth.usePasswordFiles }} + - name: MYSQL_ROOT_PASSWORD_FILE + value: {{ default "/opt/bitnami/mysql/secrets/mysql-root-password" .Values.auth.customPasswordFiles.root }} + {{- else }} + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mysql.secretName" . }} + key: mysql-root-password + {{- end }} + {{- if not (empty .Values.auth.username) }} + - name: MYSQL_USER + value: {{ .Values.auth.username | quote }} + {{- if .Values.auth.usePasswordFiles }} + - name: MYSQL_PASSWORD_FILE + value: {{ default "/opt/bitnami/mysql/secrets/mysql-password" .Values.auth.customPasswordFiles.user }} + {{- else }} + - name: MYSQL_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mysql.secretName" . }} + key: mysql-password + {{- end }} + {{- end }} + - name: MYSQL_DATABASE + value: {{ .Values.auth.database | quote }} + {{- if eq .Values.architecture "replication" }} + - name: MYSQL_REPLICATION_MODE + value: "master" + - name: MYSQL_REPLICATION_USER + value: {{ .Values.auth.replicationUser | quote }} + {{- if .Values.auth.usePasswordFiles }} + - name: MYSQL_REPLICATION_PASSWORD_FILE + value: {{ default "/opt/bitnami/mysql/secrets/mysql-replication-password" .Values.auth.customPasswordFiles.replicator }} + {{- else }} + - name: MYSQL_REPLICATION_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mysql.secretName" . }} + key: mysql-replication-password + {{- end }} + {{- end }} + {{- if .Values.primary.extraFlags }} + - name: MYSQL_EXTRA_FLAGS + value: "{{ .Values.primary.extraFlags }}" + {{- end }} + {{- if .Values.primary.extraEnvVars }} + {{- include "common.tplvalues.render" (dict "value" .Values.primary.extraEnvVars "context" $) | nindent 12 }} + {{- end }} + {{- if or .Values.primary.extraEnvVarsCM .Values.primary.extraEnvVarsSecret }} + envFrom: + {{- if .Values.primary.extraEnvVarsCM }} + - configMapRef: + name: {{ .Values.primary.extraEnvVarsCM }} + {{- end }} + {{- if .Values.primary.extraEnvVarsSecret }} + - secretRef: + name: {{ .Values.primary.extraEnvVarsSecret }} + {{- end }} + {{- end }} + ports: + - name: mysql + containerPort: 3306 + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.primary.livenessProbe.enabled }} + livenessProbe: {{- omit .Values.primary.livenessProbe "enabled" | toYaml | nindent 12 }} + exec: + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE") + fi + mysqladmin status -uroot -p"${password_aux}" + {{- else if .Values.primary.customLivenessProbe }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.primary.customLivenessProbe "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.primary.readinessProbe.enabled }} + readinessProbe: {{- omit .Values.primary.readinessProbe "enabled" | toYaml | nindent 12 }} + exec: + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE") + fi + mysqladmin status -uroot -p"${password_aux}" + {{- else if .Values.primary.customReadinessProbe }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.primary.customReadinessProbe "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.primary.startupProbe.enabled }} + startupProbe: {{- omit .Values.primary.startupProbe "enabled" | toYaml | nindent 12 }} + exec: + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE") + fi + mysqladmin status -uroot -p"${password_aux}" + {{- else if .Values.primary.customStartupProbe }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.primary.customStartupProbe "context" $) | nindent 12 }} + {{- end }} + {{- end }} + {{- if .Values.primary.resources }} + resources: {{ toYaml .Values.primary.resources | nindent 12 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /bitnami/mysql + {{- if or .Values.initdbScriptsConfigMap .Values.initdbScripts }} + - name: custom-init-scripts + mountPath: /docker-entrypoint-initdb.d + {{- end }} + {{- if or .Values.primary.configuration .Values.primary.existingConfigmap }} + - name: config + mountPath: /opt/bitnami/mysql/conf/my.cnf + subPath: my.cnf + {{- end }} + {{- if and .Values.auth.usePasswordFiles (not .Values.auth.customPasswordFiles) }} + - name: mysql-credentials + mountPath: /opt/bitnami/mysql/secrets/ + {{- end }} + {{- if .Values.primary.extraVolumeMounts }} + {{- include "common.tplvalues.render" (dict "value" .Values.primary.extraVolumeMounts "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.metrics.enabled }} + - name: metrics + image: {{ include "mysql.metrics.image" . }} + imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} + env: + {{- if .Values.auth.usePasswordFiles }} + - name: MYSQL_ROOT_PASSWORD_FILE + value: {{ default "/opt/bitnami/mysqld-exporter/secrets/mysql-root-password" .Values.auth.customPasswordFiles.root }} + {{- else }} + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "mysql.secretName" . }} + key: mysql-root-password + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 12 }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 12 }} + {{- else }} + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE") + fi + DATA_SOURCE_NAME="root:${password_aux}@(localhost:3306)/" /bin/mysqld_exporter {{- range .Values.metrics.extraArgs.primary }} {{ . }} {{- end }} + {{- end }} + ports: + - name: metrics + containerPort: 9104 + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.metrics.livenessProbe.enabled }} + livenessProbe: {{- omit .Values.metrics.livenessProbe "enabled" | toYaml | nindent 12 }} + httpGet: + path: /metrics + port: metrics + {{- end }} + {{- if .Values.metrics.readinessProbe.enabled }} + readinessProbe: {{- omit .Values.metrics.readinessProbe "enabled" | toYaml | nindent 12 }} + httpGet: + path: /metrics + port: metrics + {{- end }} + {{- end }} + {{- if .Values.metrics.resources }} + resources: {{- toYaml .Values.metrics.resources | nindent 12 }} + {{- end }} + {{- if and .Values.auth.usePasswordFiles (not .Values.auth.customPasswordFiles) }} + volumeMounts: + - name: mysql-credentials + mountPath: /opt/bitnami/mysqld-exporter/secrets/ + {{- end }} + {{- end }} + {{- if .Values.primary.sidecars }} + {{- include "common.tplvalues.render" (dict "value" .Values.primary.sidecars "context" $) | nindent 8 }} + {{- end }} + volumes: + {{- if or .Values.primary.configuration .Values.primary.existingConfigmap }} + - name: config + configMap: + name: {{ include "mysql.primary.configmapName" . }} + {{- end }} + {{- if or .Values.initdbScriptsConfigMap .Values.initdbScripts }} + - name: custom-init-scripts + configMap: + name: {{ include "mysql.initdbScriptsCM" . }} + {{- end }} + {{- if and .Values.auth.usePasswordFiles (not .Values.auth.customPasswordFiles) }} + - name: mysql-credentials + secret: + secretName: {{ include "mysql.secretName" . }} + items: + - key: mysql-root-password + path: mysql-root-password + - key: mysql-password + path: mysql-password + {{- if eq .Values.architecture "replication" }} + - key: mysql-replication-password + path: mysql-replication-password + {{- end }} + {{- end }} + {{- if .Values.primary.extraVolumes }} + {{- include "common.tplvalues.render" (dict "value" .Values.primary.extraVolumes "context" $) | nindent 8 }} + {{- end }} + {{- if and .Values.primary.persistence.enabled .Values.primary.persistence.existingClaim }} + - name: data + persistentVolumeClaim: + claimName: {{ tpl .Values.primary.persistence.existingClaim . }} + {{- else if not .Values.primary.persistence.enabled }} + - name: data + emptyDir: {} + {{- else if and .Values.primary.persistence.enabled (not .Values.primary.persistence.existingClaim) }} + volumeClaimTemplates: + - metadata: + name: data + labels: {{ include "common.labels.matchLabels" . | nindent 10 }} + app.kubernetes.io/component: primary + {{- if .Values.primary.persistence.annotations }} + annotations: + {{- toYaml .Values.primary.persistence.annotations | nindent 10 }} + {{- end }} + spec: + accessModes: + {{- range .Values.primary.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.primary.persistence.size | quote }} + {{ include "common.storage.class" (dict "persistence" .Values.primary.persistence "global" .Values.global) }} + {{- if .Values.primary.persistence.selector }} + selector: {{- include "common.tplvalues.render" (dict "value" .Values.primary.persistence.selector "context" $) | nindent 10 }} + {{- end -}} + {{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/primary/svc-headless.yaml b/pkg/iac/scanners/helm/test/mysql/templates/primary/svc-headless.yaml new file mode 100644 index 000000000000..49e6e5798783 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/primary/svc-headless.yaml @@ -0,0 +1,24 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "mysql.primary.fullname" . }}-headless + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: primary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + annotations: + {{- if .Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + type: ClusterIP + clusterIP: None + publishNotReadyAddresses: true + ports: + - name: mysql + port: {{ .Values.primary.service.port }} + targetPort: mysql + selector: {{ include "common.labels.matchLabels" . | nindent 4 }} + app.kubernetes.io/component: primary diff --git a/pkg/iac/scanners/helm/test/mysql/templates/primary/svc.yaml b/pkg/iac/scanners/helm/test/mysql/templates/primary/svc.yaml new file mode 100644 index 000000000000..b46e6faa8149 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/primary/svc.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "mysql.primary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: primary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + annotations: + {{- if .Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.primary.service.annotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.primary.service.annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.primary.service.type }} + {{- if and (eq .Values.primary.service.type "ClusterIP") .Values.primary.service.clusterIP }} + clusterIP: {{ .Values.primary.service.clusterIP }} + {{- end }} + {{- if and .Values.primary.service.loadBalancerIP (eq .Values.primary.service.type "LoadBalancer") }} + loadBalancerIP: {{ .Values.primary.service.loadBalancerIP }} + externalTrafficPolicy: {{ .Values.primary.service.externalTrafficPolicy | quote }} + {{- end }} + {{- if and (eq .Values.primary.service.type "LoadBalancer") .Values.primary.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: {{- toYaml .Values.primary.service.loadBalancerSourceRanges | nindent 4 }} + {{- end }} + ports: + - name: mysql + port: {{ .Values.primary.service.port }} + protocol: TCP + targetPort: mysql + {{- if (and (or (eq .Values.primary.service.type "NodePort") (eq .Values.primary.service.type "LoadBalancer")) .Values.primary.service.nodePort) }} + nodePort: {{ .Values.primary.service.nodePort }} + {{- else if eq .Values.primary.service.type "ClusterIP" }} + nodePort: null + {{- end }} + selector: {{ include "common.labels.matchLabels" . | nindent 4 }} + app.kubernetes.io/component: primary diff --git a/pkg/iac/scanners/helm/test/mysql/templates/role.yaml b/pkg/iac/scanners/helm/test/mysql/templates/role.yaml new file mode 100644 index 000000000000..4cbdd5c9ff20 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/role.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.serviceAccount.create .Values.rbac.create }} +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +kind: Role +metadata: + name: {{ include "common.names.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +rules: + - apiGroups: + - "" + resources: + - endpoints + verbs: + - get +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/rolebinding.yaml b/pkg/iac/scanners/helm/test/mysql/templates/rolebinding.yaml new file mode 100644 index 000000000000..90ede32f5fc7 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/rolebinding.yaml @@ -0,0 +1,21 @@ +{{- if and .Values.serviceAccount.create .Values.rbac.create }} +kind: RoleBinding +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +metadata: + name: {{ include "common.names.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +subjects: + - kind: ServiceAccount + name: {{ include "mysql.serviceAccountName" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "common.names.fullname" . -}} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/secondary/configmap.yaml b/pkg/iac/scanners/helm/test/mysql/templates/secondary/configmap.yaml new file mode 100644 index 000000000000..682e3e19ba96 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/secondary/configmap.yaml @@ -0,0 +1,18 @@ +{{- if (include "mysql.secondary.createConfigmap" .) }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "mysql.secondary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: secondary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +data: + my.cnf: |- + {{ .Values.secondary.configuration | nindent 4 }} +{{- end -}} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/secondary/pdb.yaml b/pkg/iac/scanners/helm/test/mysql/templates/secondary/pdb.yaml new file mode 100644 index 000000000000..49c7e167c0a2 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/secondary/pdb.yaml @@ -0,0 +1,25 @@ +{{- if and (eq .Values.architecture "replication") .Values.secondary.pdb.enabled }} +apiVersion: {{ include "common.capabilities.policy.apiVersion" . }} +kind: PodDisruptionBudget +metadata: + name: {{ include "mysql.secondary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: secondary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- if .Values.secondary.pdb.minAvailable }} + minAvailable: {{ .Values.secondary.pdb.minAvailable }} + {{- end }} + {{- if .Values.secondary.pdb.maxUnavailable }} + maxUnavailable: {{ .Values.secondary.pdb.maxUnavailable }} + {{- end }} + selector: + matchLabels: {{ include "common.labels.matchLabels" . | nindent 6 }} + app.kubernetes.io/component: secondary +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/secondary/statefulset.yaml b/pkg/iac/scanners/helm/test/mysql/templates/secondary/statefulset.yaml new file mode 100644 index 000000000000..ef196ebf6df0 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/secondary/statefulset.yaml @@ -0,0 +1,338 @@ +{{- if eq .Values.architecture "replication" }} +apiVersion: {{ include "common.capabilities.statefulset.apiVersion" . }} +kind: StatefulSet +metadata: + name: {{ include "mysql.secondary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: secondary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.secondary.podLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.secondary.podLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + replicas: {{ .Values.secondary.replicaCount }} + selector: + matchLabels: {{ include "common.labels.matchLabels" . | nindent 6 }} + app.kubernetes.io/component: secondary + serviceName: {{ include "mysql.secondary.fullname" . }} + updateStrategy: + type: {{ .Values.secondary.updateStrategy }} + {{- if (eq "Recreate" .Values.secondary.updateStrategy) }} + rollingUpdate: null + {{- else if .Values.secondary.rollingUpdatePartition }} + rollingUpdate: + partition: {{ .Values.secondary.rollingUpdatePartition }} + {{- end }} + template: + metadata: + annotations: + {{- if (include "mysql.secondary.createConfigmap" .) }} + checksum/configuration: {{ include (print $.Template.BasePath "/secondary/configmap.yaml") . | sha256sum }} + {{- end }} + {{- if .Values.secondary.podAnnotations }} + {{- include "common.tplvalues.render" (dict "value" .Values.secondary.podAnnotations "context" $) | nindent 8 }} + {{- end }} + labels: {{- include "common.labels.standard" . | nindent 8 }} + app.kubernetes.io/component: secondary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 8 }} + {{- end }} + {{- if .Values.secondary.podLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.secondary.podLabels "context" $ ) | nindent 8 }} + {{- end }} + spec: + {{- include "mysql.imagePullSecrets" . | nindent 6 }} + {{- if .Values.secondary.hostAliases }} + hostAliases: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.hostAliases "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.schedulerName }} + schedulerName: {{ .Values.schedulerName | quote }} + {{- end }} + serviceAccountName: {{ include "mysql.serviceAccountName" . }} + {{- if .Values.secondary.affinity }} + affinity: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.affinity "context" $) | nindent 8 }} + {{- else }} + affinity: + podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.secondary.podAffinityPreset "component" "secondary" "context" $) | nindent 10 }} + podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.secondary.podAntiAffinityPreset "component" "secondary" "context" $) | nindent 10 }} + nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.secondary.nodeAffinityPreset.type "key" .Values.secondary.nodeAffinityPreset.key "values" .Values.secondary.nodeAffinityPreset.values) | nindent 10 }} + {{- end }} + {{- if .Values.secondary.nodeSelector }} + nodeSelector: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.nodeSelector "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.secondary.tolerations }} + tolerations: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.tolerations "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.priorityClassName }} + priorityClassName: {{ .Values.priorityClassName | quote }} + {{- end }} + {{- if .Values.secondary.podSecurityContext.enabled }} + securityContext: {{- omit .Values.secondary.podSecurityContext "enabled" | toYaml | nindent 8 }} + {{- end }} + {{- if or .Values.secondary.initContainers (and .Values.secondary.podSecurityContext.enabled .Values.volumePermissions.enabled .Values.secondary.persistence.enabled) }} + initContainers: + {{- if .Values.secondary.initContainers }} + {{- include "common.tplvalues.render" (dict "value" .Values.secondary.initContainers "context" $) | nindent 8 }} + {{- end }} + {{- if and .Values.secondary.podSecurityContext.enabled .Values.volumePermissions.enabled .Values.secondary.persistence.enabled }} + - name: volume-permissions + image: {{ include "mysql.volumePermissions.image" . }} + imagePullPolicy: {{ .Values.volumePermissions.image.pullPolicy | quote }} + command: + - /bin/bash + - -ec + - | + chown -R {{ .Values.secondary.containerSecurityContext.runAsUser }}:{{ .Values.secondary.podSecurityContext.fsGroup }} /bitnami/mysql + securityContext: + runAsUser: 0 + {{- if .Values.volumePermissions.resources }} + resources: {{- toYaml .Values.volumePermissions.resources | nindent 12 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /bitnami/mysql + {{- end }} + {{- end }} + containers: + - name: mysql + image: {{ include "mysql.image" . }} + imagePullPolicy: {{ .Values.image.pullPolicy | quote }} + {{- if .Values.secondary.containerSecurityContext.enabled }} + securityContext: {{- omit .Values.secondary.containerSecurityContext "enabled" | toYaml | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 12 }} + {{- else if .Values.secondary.command }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.command "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 12 }} + {{- else if .Values.secondary.args }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.args "context" $) | nindent 12 }} + {{- end }} + env: + - name: BITNAMI_DEBUG + value: {{ ternary "true" "false" (or .Values.image.debug .Values.diagnosticMode.enabled) | quote }} + - name: MYSQL_REPLICATION_MODE + value: "slave" + - name: MYSQL_MASTER_HOST + value: {{ include "mysql.primary.fullname" . }} + - name: MYSQL_MASTER_PORT_NUMBER + value: {{ .Values.primary.service.port | quote }} + - name: MYSQL_MASTER_ROOT_USER + value: "root" + - name: MYSQL_REPLICATION_USER + value: {{ .Values.auth.replicationUser | quote }} + {{- if .Values.auth.usePasswordFiles }} + - name: MYSQL_MASTER_ROOT_PASSWORD_FILE + value: {{ default "/opt/bitnami/mysql/secrets/mysql-root-password" .Values.auth.customPasswordFiles.root }} + - name: MYSQL_REPLICATION_PASSWORD_FILE + value: {{ default "/opt/bitnami/mysql/secrets/mysql-replication-password" .Values.auth.customPasswordFiles.replicator }} + {{- else }} + - name: MYSQL_MASTER_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mysql.secretName" . }} + key: mysql-root-password + - name: MYSQL_REPLICATION_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mysql.secretName" . }} + key: mysql-replication-password + {{- end }} + {{- if .Values.secondary.extraFlags }} + - name: MYSQL_EXTRA_FLAGS + value: "{{ .Values.secondary.extraFlags }}" + {{- end }} + {{- if .Values.secondary.extraEnvVars }} + {{- include "common.tplvalues.render" (dict "value" .Values.secondary.extraEnvVars "context" $) | nindent 12 }} + {{- end }} + {{- if or .Values.secondary.extraEnvVarsCM .Values.secondary.extraEnvVarsSecret }} + envFrom: + {{- if .Values.secondary.extraEnvVarsCM }} + - configMapRef: + name: {{ .Values.secondary.extraEnvVarsCM }} + {{- end }} + {{- if .Values.secondary.extraEnvVarsSecret }} + - secretRef: + name: {{ .Values.secondary.extraEnvVarsSecret }} + {{- end }} + {{- end }} + ports: + - name: mysql + containerPort: 3306 + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.secondary.livenessProbe.enabled }} + livenessProbe: {{- omit .Values.secondary.livenessProbe "enabled" | toYaml | nindent 12 }} + exec: + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_MASTER_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_MASTER_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_MASTER_ROOT_PASSWORD_FILE") + fi + mysqladmin status -uroot -p"${password_aux}" + {{- else if .Values.secondary.customLivenessProbe }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.customLivenessProbe "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.secondary.readinessProbe.enabled }} + readinessProbe: {{- omit .Values.secondary.readinessProbe "enabled" | toYaml | nindent 12 }} + exec: + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_MASTER_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_MASTER_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_MASTER_ROOT_PASSWORD_FILE") + fi + mysqladmin status -uroot -p"${password_aux}" + {{- else if .Values.secondary.customReadinessProbe }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.customReadinessProbe "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.secondary.startupProbe.enabled }} + startupProbe: {{- omit .Values.secondary.startupProbe "enabled" | toYaml | nindent 12 }} + exec: + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_MASTER_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_MASTER_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_MASTER_ROOT_PASSWORD_FILE") + fi + mysqladmin status -uroot -p"${password_aux}" + {{- else if .Values.secondary.customStartupProbe }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.customStartupProbe "context" $) | nindent 12 }} + {{- end }} + {{- end }} + {{- if .Values.secondary.resources }} + resources: {{ toYaml .Values.secondary.resources | nindent 12 }} + {{- end }} + volumeMounts: + - name: data + mountPath: /bitnami/mysql + {{- if or .Values.secondary.configuration .Values.secondary.existingConfigmap }} + - name: config + mountPath: /opt/bitnami/mysql/conf/my.cnf + subPath: my.cnf + {{- end }} + {{- if and .Values.auth.usePasswordFiles (not .Values.auth.customPasswordFiles) }} + - name: mysql-credentials + mountPath: /opt/bitnami/mysql/secrets/ + {{- end }} + {{- if .Values.secondary.extraVolumeMounts }} + {{- include "common.tplvalues.render" (dict "value" .Values.secondary.extraVolumeMounts "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.metrics.enabled }} + - name: metrics + image: {{ include "mysql.metrics.image" . }} + imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} + env: + {{- if .Values.auth.usePasswordFiles }} + - name: MYSQL_ROOT_PASSWORD_FILE + value: {{ default "/opt/bitnami/mysqld-exporter/secrets/mysql-root-password" .Values.auth.customPasswordFiles.root }} + {{- else }} + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ template "mysql.secretName" . }} + key: mysql-root-password + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 12 }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 12 }} + {{- else }} + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE") + fi + DATA_SOURCE_NAME="root:${password_aux}@(localhost:3306)/" /bin/mysqld_exporter {{- range .Values.metrics.extraArgs.secondary }} {{ . }} {{- end }} + {{- end }} + ports: + - name: metrics + containerPort: 9104 + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.metrics.livenessProbe.enabled }} + livenessProbe: {{- omit .Values.metrics.livenessProbe "enabled" | toYaml | nindent 12 }} + httpGet: + path: /metrics + port: metrics + {{- end }} + {{- if .Values.metrics.readinessProbe.enabled }} + readinessProbe: {{- omit .Values.metrics.readinessProbe "enabled" | toYaml | nindent 12 }} + httpGet: + path: /metrics + port: metrics + {{- end }} + {{- end }} + {{- if .Values.metrics.resources }} + resources: {{- toYaml .Values.metrics.resources | nindent 12 }} + {{- end }} + {{- if and .Values.auth.usePasswordFiles (not .Values.auth.customPasswordFiles) }} + volumeMounts: + - name: mysql-credentials + mountPath: /opt/bitnami/mysqld-exporter/secrets/ + {{- end }} + {{- end }} + {{- if .Values.secondary.sidecars }} + {{- include "common.tplvalues.render" (dict "value" .Values.secondary.sidecars "context" $) | nindent 8 }} + {{- end }} + volumes: + {{- if or .Values.secondary.configuration .Values.secondary.existingConfigmap }} + - name: config + configMap: + name: {{ include "mysql.secondary.configmapName" . }} + {{- end }} + {{- if and .Values.auth.usePasswordFiles (not .Values.auth.customPasswordFiles) }} + - name: mysql-credentials + secret: + secretName: {{ template "mysql.secretName" . }} + items: + - key: mysql-root-password + path: mysql-root-password + - key: mysql-replication-password + path: mysql-replication-password + {{- end }} + {{- if .Values.secondary.extraVolumes }} + {{- include "common.tplvalues.render" (dict "value" .Values.secondary.extraVolumes "context" $) | nindent 8 }} + {{- end }} + {{- if not .Values.secondary.persistence.enabled }} + - name: data + emptyDir: {} + {{- else }} + volumeClaimTemplates: + - metadata: + name: data + labels: {{ include "common.labels.matchLabels" . | nindent 10 }} + app.kubernetes.io/component: secondary + {{- if .Values.secondary.persistence.annotations }} + annotations: + {{- toYaml .Values.secondary.persistence.annotations | nindent 10 }} + {{- end }} + spec: + accessModes: + {{- range .Values.secondary.persistence.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.secondary.persistence.size | quote }} + {{ include "common.storage.class" (dict "persistence" .Values.secondary.persistence "global" .Values.global) }} + {{- if .Values.secondary.persistence.selector }} + selector: {{- include "common.tplvalues.render" (dict "value" .Values.secondary.persistence.selector "context" $) | nindent 10 }} + {{- end -}} + {{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/secondary/svc-headless.yaml b/pkg/iac/scanners/helm/test/mysql/templates/secondary/svc-headless.yaml new file mode 100644 index 000000000000..703d8e747b75 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/secondary/svc-headless.yaml @@ -0,0 +1,26 @@ +{{- if eq .Values.architecture "replication" }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "mysql.secondary.fullname" . }}-headless + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: secondary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + annotations: + {{- if .Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + type: ClusterIP + clusterIP: None + publishNotReadyAddresses: true + ports: + - name: mysql + port: {{ .Values.secondary.service.port }} + targetPort: mysql + selector: {{ include "common.labels.matchLabels" . | nindent 4 }} + app.kubernetes.io/component: secondary +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/secondary/svc.yaml b/pkg/iac/scanners/helm/test/mysql/templates/secondary/svc.yaml new file mode 100644 index 000000000000..74a4c6ef5fb8 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/secondary/svc.yaml @@ -0,0 +1,43 @@ +{{- if eq .Values.architecture "replication" }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "mysql.secondary.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + app.kubernetes.io/component: secondary + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + annotations: + {{- if .Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.secondary.service.annotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.secondary.service.annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + type: {{ .Values.secondary.service.type }} + {{- if and (eq .Values.secondary.service.type "ClusterIP") .Values.secondary.service.clusterIP }} + clusterIP: {{ .Values.secondary.service.clusterIP }} + {{- end }} + {{- if and .Values.secondary.service.loadBalancerIP (eq .Values.secondary.service.type "LoadBalancer") }} + loadBalancerIP: {{ .Values.secondary.service.loadBalancerIP }} + externalTrafficPolicy: {{ .Values.secondary.service.externalTrafficPolicy | quote }} + {{- end }} + {{- if and (eq .Values.secondary.service.type "LoadBalancer") .Values.secondary.service.loadBalancerSourceRanges }} + loadBalancerSourceRanges: {{- toYaml .Values.secondary.service.loadBalancerSourceRanges | nindent 4 }} + {{- end }} + ports: + - name: mysql + port: {{ .Values.secondary.service.port }} + protocol: TCP + targetPort: mysql + {{- if (and (or (eq .Values.secondary.service.type "NodePort") (eq .Values.secondary.service.type "LoadBalancer")) .Values.secondary.service.nodePort) }} + nodePort: {{ .Values.secondary.service.nodePort }} + {{- else if eq .Values.secondary.service.type "ClusterIP" }} + nodePort: null + {{- end }} + selector: {{ include "common.labels.matchLabels" . | nindent 4 }} + app.kubernetes.io/component: secondary +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/secrets.yaml b/pkg/iac/scanners/helm/test/mysql/templates/secrets.yaml new file mode 100644 index 000000000000..9412fc35a5bc --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/secrets.yaml @@ -0,0 +1,21 @@ +{{- if eq (include "mysql.createSecret" .) "true" }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "common.names.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +type: Opaque +data: + mysql-root-password: {{ include "mysql.root.password" . | b64enc | quote }} + mysql-password: {{ include "mysql.password" . | b64enc | quote }} + {{- if eq .Values.architecture "replication" }} + mysql-replication-password: {{ include "mysql.replication.password" . | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/serviceaccount.yaml b/pkg/iac/scanners/helm/test/mysql/templates/serviceaccount.yaml new file mode 100644 index 000000000000..59eb10409d91 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/serviceaccount.yaml @@ -0,0 +1,22 @@ +{{- if .Values.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "mysql.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + annotations: + {{- if .Values.serviceAccount.annotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.serviceAccount.annotations "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +{{- if (not .Values.auth.customPasswordFiles) }} +secrets: + - name: {{ template "mysql.secretName" . }} +{{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/templates/servicemonitor.yaml b/pkg/iac/scanners/helm/test/mysql/templates/servicemonitor.yaml new file mode 100644 index 000000000000..f082dd5409d6 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/templates/servicemonitor.yaml @@ -0,0 +1,42 @@ +{{- if and .Values.metrics.enabled .Values.metrics.serviceMonitor.enabled }} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ include "common.names.fullname" . }} + {{- if .Values.metrics.serviceMonitor.namespace }} + namespace: {{ .Values.metrics.serviceMonitor.namespace }} + {{- else }} + namespace: {{ .Release.Namespace }} + {{- end }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.additionalLabels }} + {{- include "common.tplvalues.render" (dict "value" .Values.metrics.serviceMonitor.additionalLabels "context" $) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + endpoints: + - port: metrics + {{- if .Values.metrics.serviceMonitor.interval }} + interval: {{ .Values.metrics.serviceMonitor.interval }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.scrapeTimeout }} + scrapeTimeout: {{ .Values.metrics.serviceMonitor.scrapeTimeout }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.honorLabels }} + honorLabels: {{ .Values.metrics.serviceMonitor.honorLabels }} + {{- end }} + {{- if .Values.metrics.serviceMonitor.relabellings }} + metricRelabelings: {{- toYaml .Values.metrics.serviceMonitor.relabellings | nindent 6 }} + {{- end }} + namespaceSelector: + matchNames: + - {{ .Release.Namespace }} + selector: + matchLabels: {{- include "common.labels.matchLabels" . | nindent 6 }} + app.kubernetes.io/component: metrics +{{- end }} diff --git a/pkg/iac/scanners/helm/test/mysql/values.schema.json b/pkg/iac/scanners/helm/test/mysql/values.schema.json new file mode 100644 index 000000000000..8021a4603600 --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/values.schema.json @@ -0,0 +1,178 @@ +{ + "$schema": "http://json-schema.org/schema#", + "type": "object", + "properties": { + "architecture": { + "type": "string", + "title": "MySQL architecture", + "form": true, + "description": "Allowed values: `standalone` or `replication`", + "enum": ["standalone", "replication"] + }, + "auth": { + "type": "object", + "title": "Authentication configuration", + "form": true, + "required": ["database", "username", "password"], + "properties": { + "rootPassword": { + "type": "string", + "title": "MySQL root password", + "description": "Defaults to a random 10-character alphanumeric string if not set" + }, + "database": { + "type": "string", + "title": "MySQL custom database name" + }, + "username": { + "type": "string", + "title": "MySQL custom username" + }, + "password": { + "type": "string", + "title": "MySQL custom password" + }, + "replicationUser": { + "type": "string", + "title": "MySQL replication username" + }, + "replicationPassword": { + "type": "string", + "title": "MySQL replication password" + } + } + }, + "primary": { + "type": "object", + "title": "Primary database configuration", + "form": true, + "properties": { + "podSecurityContext": { + "type": "object", + "title": "MySQL primary Pod security context", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "fsGroup": { + "type": "integer", + "default": 1001, + "hidden": { + "value": false, + "path": "primary/podSecurityContext/enabled" + } + } + } + }, + "containerSecurityContext": { + "type": "object", + "title": "MySQL primary container security context", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "runAsUser": { + "type": "integer", + "default": 1001, + "hidden": { + "value": false, + "path": "primary/containerSecurityContext/enabled" + } + } + } + }, + "persistence": { + "type": "object", + "title": "Enable persistence using Persistent Volume Claims", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "title": "If true, use a Persistent Volume Claim, If false, use emptyDir" + }, + "size": { + "type": "string", + "title": "Persistent Volume Size", + "form": true, + "render": "slider", + "sliderMin": 1, + "sliderUnit": "Gi", + "hidden": { + "value": false, + "path": "primary/persistence/enabled" + } + } + } + } + } + }, + "secondary": { + "type": "object", + "title": "Secondary database configuration", + "form": true, + "properties": { + "podSecurityContext": { + "type": "object", + "title": "MySQL secondary Pod security context", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "fsGroup": { + "type": "integer", + "default": 1001, + "hidden": { + "value": false, + "path": "secondary/podSecurityContext/enabled" + } + } + } + }, + "containerSecurityContext": { + "type": "object", + "title": "MySQL secondary container security context", + "properties": { + "enabled": { + "type": "boolean", + "default": false + }, + "runAsUser": { + "type": "integer", + "default": 1001, + "hidden": { + "value": false, + "path": "secondary/containerSecurityContext/enabled" + } + } + } + }, + "persistence": { + "type": "object", + "title": "Enable persistence using Persistent Volume Claims", + "properties": { + "enabled": { + "type": "boolean", + "default": true, + "title": "If true, use a Persistent Volume Claim, If false, use emptyDir" + }, + "size": { + "type": "string", + "title": "Persistent Volume Size", + "form": true, + "render": "slider", + "sliderMin": 1, + "sliderUnit": "Gi", + "hidden": { + "value": false, + "path": "secondary/persistence/enabled" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/mysql/values.yaml b/pkg/iac/scanners/helm/test/mysql/values.yaml new file mode 100644 index 000000000000..3900e865955c --- /dev/null +++ b/pkg/iac/scanners/helm/test/mysql/values.yaml @@ -0,0 +1,1020 @@ +## @section Global parameters +## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry, imagePullSecrets and storageClass + +## @param global.imageRegistry Global Docker image registry +## @param global.imagePullSecrets [array] Global Docker registry secret names as an array +## @param global.storageClass Global StorageClass for Persistent Volume(s) +## +global: + imageRegistry: "" + ## E.g. + ## imagePullSecrets: + ## - myRegistryKeySecretName + ## + imagePullSecrets: [] + storageClass: "" + +## @section Common parameters + +## @param nameOverride String to partially override common.names.fullname template (will maintain the release name) +## +nameOverride: "" +## @param fullnameOverride String to fully override common.names.fullname template +## +fullnameOverride: "" +## @param clusterDomain Cluster domain +## +clusterDomain: cluster.local +## @param commonAnnotations [object] Common annotations to add to all MySQL resources (sub-charts are not considered). Evaluated as a template +## +commonAnnotations: {} +## @param commonLabels [object] Common labels to add to all MySQL resources (sub-charts are not considered). Evaluated as a template +## +commonLabels: {} +## @param extraDeploy [array] Array with extra yaml to deploy with the chart. Evaluated as a template +## +extraDeploy: [] +## @param schedulerName Use an alternate scheduler, e.g. "stork". +## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/ +## +schedulerName: "" + +## Enable diagnostic mode in the deployment +## +diagnosticMode: + ## @param diagnosticMode.enabled Enable diagnostic mode (all probes will be disabled and the command will be overridden) + ## + enabled: false + ## @param diagnosticMode.command Command to override all containers in the deployment + ## + command: + - sleep + ## @param diagnosticMode.args Args to override all containers in the deployment + ## + args: + - infinity + +## @section MySQL common parameters + +## Bitnami MySQL image +## ref: https://hub.docker.com/r/bitnami/mysql/tags/ +## @param image.registry MySQL image registry +## @param image.repository MySQL image repository +## @param image.tag MySQL image tag (immutable tags are recommended) +## @param image.pullPolicy MySQL image pull policy +## @param image.pullSecrets [array] Specify docker-registry secret names as an array +## @param image.debug Specify if debug logs should be enabled +## +image: + registry: docker.io + repository: bitnami/mysql + tag: 8.0.28-debian-10-r23 + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: https://kubernetes.io/docs/user-guide/images/#pre-pulling-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets (secrets must be manually created in the namespace) + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## Example: + ## pullSecrets: + ## - myRegistryKeySecretName + ## + pullSecrets: [] + ## Set to true if you would like to see extra information on logs + ## It turns BASH and/or NAMI debugging in the image + ## + debug: false +## @param architecture MySQL architecture (`standalone` or `replication`) +## +architecture: standalone +## MySQL Authentication parameters +## +auth: + ## @param auth.rootPassword Password for the `root` user. Ignored if existing secret is provided + ## ref: https://github.com/bitnami/bitnami-docker-mysql#setting-the-root-password-on-first-run + ## + rootPassword: "" + ## @param auth.database Name for a custom database to create + ## ref: https://github.com/bitnami/bitnami-docker-mysql/blob/master/README.md#creating-a-database-on-first-run + ## + database: my_database + ## @param auth.username Name for a custom user to create + ## ref: https://github.com/bitnami/bitnami-docker-mysql/blob/master/README.md#creating-a-database-user-on-first-run + ## + username: "" + ## @param auth.password Password for the new user. Ignored if existing secret is provided + ## + password: "" + ## @param auth.replicationUser MySQL replication user + ## ref: https://github.com/bitnami/bitnami-docker-mysql#setting-up-a-replication-cluster + ## + replicationUser: replicator + ## @param auth.replicationPassword MySQL replication user password. Ignored if existing secret is provided + ## + replicationPassword: "" + ## @param auth.existingSecret Use existing secret for password details. The secret has to contain the keys `mysql-root-password`, `mysql-replication-password` and `mysql-password` + ## NOTE: When it's set the auth.rootPassword, auth.password, auth.replicationPassword are ignored. + ## + existingSecret: "" + ## @param auth.forcePassword Force users to specify required passwords + ## + forcePassword: false + ## @param auth.usePasswordFiles Mount credentials as files instead of using an environment variable + ## + usePasswordFiles: false + ## @param auth.customPasswordFiles [object] Use custom password files when `auth.usePasswordFiles` is set to `true`. Define path for keys `root` and `user`, also define `replicator` if `architecture` is set to `replication` + ## Example: + ## customPasswordFiles: + ## root: /vault/secrets/mysql-root + ## user: /vault/secrets/mysql-user + ## replicator: /vault/secrets/mysql-replicator + ## + customPasswordFiles: {} +## @param initdbScripts [object] Dictionary of initdb scripts +## Specify dictionary of scripts to be run at first boot +## Example: +## initdbScripts: +## my_init_script.sh: | +## #!/bin/bash +## echo "Do something." +## +initdbScripts: {} +## @param initdbScriptsConfigMap ConfigMap with the initdb scripts (Note: Overrides `initdbScripts`) +## +initdbScriptsConfigMap: "" + +## @section MySQL Primary parameters + +primary: + ## @param primary.command [array] Override default container command on MySQL Primary container(s) (useful when using custom images) + ## + command: [] + ## @param primary.args [array] Override default container args on MySQL Primary container(s) (useful when using custom images) + ## + args: [] + ## @param primary.hostAliases [array] Deployment pod host aliases + ## https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/ + ## + hostAliases: [] + ## @param primary.configuration [string] Configure MySQL Primary with a custom my.cnf file + ## ref: https://mysql.com/kb/en/mysql/configuring-mysql-with-mycnf/#example-of-configuration-file + ## + configuration: |- + [mysqld] + default_authentication_plugin=mysql_native_password + skip-name-resolve + explicit_defaults_for_timestamp + basedir=/opt/bitnami/mysql + plugin_dir=/opt/bitnami/mysql/lib/plugin + port=3306 + socket=/opt/bitnami/mysql/tmp/mysql.sock + datadir=/bitnami/mysql/data + tmpdir=/opt/bitnami/mysql/tmp + max_allowed_packet=16M + bind-address=0.0.0.0 + pid-file=/opt/bitnami/mysql/tmp/mysqld.pid + log-error=/opt/bitnami/mysql/logs/mysqld.log + character-set-server=UTF8 + collation-server=utf8_general_ci + + [client] + port=3306 + socket=/opt/bitnami/mysql/tmp/mysql.sock + default-character-set=UTF8 + plugin_dir=/opt/bitnami/mysql/lib/plugin + + [manager] + port=3306 + socket=/opt/bitnami/mysql/tmp/mysql.sock + pid-file=/opt/bitnami/mysql/tmp/mysqld.pid + ## @param primary.existingConfigmap Name of existing ConfigMap with MySQL Primary configuration. + ## NOTE: When it's set the 'configuration' parameter is ignored + ## + existingConfigmap: "" + ## @param primary.updateStrategy Update strategy type for the MySQL primary statefulset + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies + ## + updateStrategy: RollingUpdate + ## @param primary.rollingUpdatePartition Partition update strategy for MySQL Primary statefulset + ## https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#partitions + ## + rollingUpdatePartition: "" + ## @param primary.podAnnotations [object] Additional pod annotations for MySQL primary pods + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + podAnnotations: {} + ## @param primary.podAffinityPreset MySQL primary pod affinity preset. Ignored if `primary.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## + podAffinityPreset: "" + ## @param primary.podAntiAffinityPreset MySQL primary pod anti-affinity preset. Ignored if `primary.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## + podAntiAffinityPreset: soft + ## MySQL Primary node affinity preset + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity + ## + nodeAffinityPreset: + ## @param primary.nodeAffinityPreset.type MySQL primary node affinity preset type. Ignored if `primary.affinity` is set. Allowed values: `soft` or `hard` + ## + type: "" + ## @param primary.nodeAffinityPreset.key MySQL primary node label key to match Ignored if `primary.affinity` is set. + ## E.g. + ## key: "kubernetes.io/e2e-az-name" + ## + key: "" + ## @param primary.nodeAffinityPreset.values [array] MySQL primary node label values to match. Ignored if `primary.affinity` is set. + ## E.g. + ## values: + ## - e2e-az1 + ## - e2e-az2 + ## + values: [] + ## @param primary.affinity [object] Affinity for MySQL primary pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + ## Note: podAffinityPreset, podAntiAffinityPreset, and nodeAffinityPreset will be ignored when it's set + ## + affinity: {} + ## @param primary.nodeSelector [object] Node labels for MySQL primary pods assignment + ## ref: https://kubernetes.io/docs/user-guide/node-selection/ + ## + nodeSelector: {} + ## @param primary.tolerations [array] Tolerations for MySQL primary pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + ## + tolerations: [] + ## MySQL primary Pod security context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + ## @param primary.podSecurityContext.enabled Enable security context for MySQL primary pods + ## @param primary.podSecurityContext.fsGroup Group ID for the mounted volumes' filesystem + ## + podSecurityContext: + enabled: true + fsGroup: 1001 + ## MySQL primary container security context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container + ## @param primary.containerSecurityContext.enabled MySQL primary container securityContext + ## @param primary.containerSecurityContext.runAsUser User ID for the MySQL primary container + ## + containerSecurityContext: + enabled: true + runAsUser: 1001 + ## MySQL primary container's resource requests and limits + ## ref: https://kubernetes.io/docs/user-guide/compute-resources/ + ## We usually recommend not to specify default resources and to leave this as a conscious + ## choice for the user. This also increases chances charts run on environments with little + ## resources, such as Minikube. If you do want to specify resources, uncomment the following + ## lines, adjust them as necessary, and remove the curly braces after 'resources:'. + ## @param primary.resources.limits [object] The resources limits for MySQL primary containers + ## @param primary.resources.requests [object] The requested resources for MySQL primary containers + ## + resources: + ## Example: + ## limits: + ## cpu: 250m + ## memory: 256Mi + limits: {} + ## Examples: + ## requests: + ## cpu: 250m + ## memory: 256Mi + requests: {} + ## Configure extra options for liveness probe + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + ## @param primary.livenessProbe.enabled Enable livenessProbe + ## @param primary.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe + ## @param primary.livenessProbe.periodSeconds Period seconds for livenessProbe + ## @param primary.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe + ## @param primary.livenessProbe.failureThreshold Failure threshold for livenessProbe + ## @param primary.livenessProbe.successThreshold Success threshold for livenessProbe + ## + livenessProbe: + enabled: true + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + successThreshold: 1 + ## Configure extra options for readiness probe + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + ## @param primary.readinessProbe.enabled Enable readinessProbe + ## @param primary.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe + ## @param primary.readinessProbe.periodSeconds Period seconds for readinessProbe + ## @param primary.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe + ## @param primary.readinessProbe.failureThreshold Failure threshold for readinessProbe + ## @param primary.readinessProbe.successThreshold Success threshold for readinessProbe + ## + readinessProbe: + enabled: true + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + successThreshold: 1 + ## Configure extra options for startupProbe probe + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + ## @param primary.startupProbe.enabled Enable startupProbe + ## @param primary.startupProbe.initialDelaySeconds Initial delay seconds for startupProbe + ## @param primary.startupProbe.periodSeconds Period seconds for startupProbe + ## @param primary.startupProbe.timeoutSeconds Timeout seconds for startupProbe + ## @param primary.startupProbe.failureThreshold Failure threshold for startupProbe + ## @param primary.startupProbe.successThreshold Success threshold for startupProbe + ## + startupProbe: + enabled: true + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 10 + successThreshold: 1 + ## @param primary.customLivenessProbe [object] Override default liveness probe for MySQL primary containers + ## + customLivenessProbe: {} + ## @param primary.customReadinessProbe [object] Override default readiness probe for MySQL primary containers + ## + customReadinessProbe: {} + ## @param primary.customStartupProbe [object] Override default startup probe for MySQL primary containers + ## + customStartupProbe: {} + ## @param primary.extraFlags MySQL primary additional command line flags + ## Can be used to specify command line flags, for example: + ## E.g. + ## extraFlags: "--max-connect-errors=1000 --max_connections=155" + ## + extraFlags: "" + ## @param primary.extraEnvVars [array] Extra environment variables to be set on MySQL primary containers + ## E.g. + ## extraEnvVars: + ## - name: TZ + ## value: "Europe/Paris" + ## + extraEnvVars: [] + ## @param primary.extraEnvVarsCM Name of existing ConfigMap containing extra env vars for MySQL primary containers + ## + extraEnvVarsCM: "" + ## @param primary.extraEnvVarsSecret Name of existing Secret containing extra env vars for MySQL primary containers + ## + extraEnvVarsSecret: "" + ## Enable persistence using Persistent Volume Claims + ## ref: https://kubernetes.io/docs/user-guide/persistent-volumes/ + ## + persistence: + ## @param primary.persistence.enabled Enable persistence on MySQL primary replicas using a `PersistentVolumeClaim`. If false, use emptyDir + ## + enabled: true + ## @param primary.persistence.existingClaim Name of an existing `PersistentVolumeClaim` for MySQL primary replicas + ## NOTE: When it's set the rest of persistence parameters are ignored + ## + existingClaim: "" + ## @param primary.persistence.storageClass MySQL primary persistent volume storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + storageClass: "" + ## @param primary.persistence.annotations [object] MySQL primary persistent volume claim annotations + ## + annotations: {} + ## @param primary.persistence.accessModes MySQL primary persistent volume access Modes + ## + accessModes: + - ReadWriteOnce + ## @param primary.persistence.size MySQL primary persistent volume size + ## + size: 8Gi + ## @param primary.persistence.selector [object] Selector to match an existing Persistent Volume + ## selector: + ## matchLabels: + ## app: my-app + ## + selector: {} + ## @param primary.extraVolumes [array] Optionally specify extra list of additional volumes to the MySQL Primary pod(s) + ## + extraVolumes: [] + ## @param primary.extraVolumeMounts [array] Optionally specify extra list of additional volumeMounts for the MySQL Primary container(s) + ## + extraVolumeMounts: [] + ## @param primary.initContainers [array] Add additional init containers for the MySQL Primary pod(s) + ## + initContainers: [] + ## @param primary.sidecars [array] Add additional sidecar containers for the MySQL Primary pod(s) + ## + sidecars: [] + ## MySQL Primary Service parameters + ## + service: + ## @param primary.service.type MySQL Primary K8s service type + ## + type: ClusterIP + ## @param primary.service.port MySQL Primary K8s service port + ## + port: 3306 + ## @param primary.service.nodePort MySQL Primary K8s service node port + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + ## + nodePort: "" + ## @param primary.service.clusterIP MySQL Primary K8s service clusterIP IP + ## e.g: + ## clusterIP: None + ## + clusterIP: "" + ## @param primary.service.loadBalancerIP MySQL Primary loadBalancerIP if service type is `LoadBalancer` + ## Set the LoadBalancer service type to internal only + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer + ## + loadBalancerIP: "" + ## @param primary.service.externalTrafficPolicy Enable client source IP preservation + ## ref https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip + ## + externalTrafficPolicy: Cluster + ## @param primary.service.loadBalancerSourceRanges [array] Addresses that are allowed when MySQL Primary service is LoadBalancer + ## https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/#restrict-access-for-loadbalancer-service + ## E.g. + ## loadBalancerSourceRanges: + ## - 10.10.10.0/24 + ## + loadBalancerSourceRanges: [] + ## @param primary.service.annotations [object] Provide any additional annotations which may be required + ## + annotations: {} + ## MySQL primary Pod Disruption Budget configuration + ## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/ + ## + pdb: + ## @param primary.pdb.enabled Enable/disable a Pod Disruption Budget creation for MySQL primary pods + ## + enabled: false + ## @param primary.pdb.minAvailable Minimum number/percentage of MySQL primary pods that should remain scheduled + ## + minAvailable: 1 + ## @param primary.pdb.maxUnavailable Maximum number/percentage of MySQL primary pods that may be made unavailable + ## + maxUnavailable: "" + ## @param primary.podLabels [object] MySQL Primary pod label. If labels are same as commonLabels , this will take precedence + ## + podLabels: {} + +## @section MySQL Secondary parameters + +secondary: + ## @param secondary.replicaCount Number of MySQL secondary replicas + ## + replicaCount: 1 + ## @param secondary.hostAliases [array] Deployment pod host aliases + ## https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/ + ## + hostAliases: [] + ## @param secondary.command [array] Override default container command on MySQL Secondary container(s) (useful when using custom images) + ## + command: [] + ## @param secondary.args [array] Override default container args on MySQL Secondary container(s) (useful when using custom images) + ## + args: [] + ## @param secondary.configuration [string] Configure MySQL Secondary with a custom my.cnf file + ## ref: https://mysql.com/kb/en/mysql/configuring-mysql-with-mycnf/#example-of-configuration-file + ## + configuration: |- + [mysqld] + default_authentication_plugin=mysql_native_password + skip-name-resolve + explicit_defaults_for_timestamp + basedir=/opt/bitnami/mysql + port=3306 + socket=/opt/bitnami/mysql/tmp/mysql.sock + datadir=/bitnami/mysql/data + tmpdir=/opt/bitnami/mysql/tmp + max_allowed_packet=16M + bind-address=0.0.0.0 + pid-file=/opt/bitnami/mysql/tmp/mysqld.pid + log-error=/opt/bitnami/mysql/logs/mysqld.log + character-set-server=UTF8 + collation-server=utf8_general_ci + + [client] + port=3306 + socket=/opt/bitnami/mysql/tmp/mysql.sock + default-character-set=UTF8 + + [manager] + port=3306 + socket=/opt/bitnami/mysql/tmp/mysql.sock + pid-file=/opt/bitnami/mysql/tmp/mysqld.pid + ## @param secondary.existingConfigmap Name of existing ConfigMap with MySQL Secondary configuration. + ## NOTE: When it's set the 'configuration' parameter is ignored + ## + existingConfigmap: "" + ## @param secondary.updateStrategy Update strategy type for the MySQL secondary statefulset + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies + ## + updateStrategy: RollingUpdate + ## @param secondary.rollingUpdatePartition Partition update strategy for MySQL Secondary statefulset + ## https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#partitions + ## + rollingUpdatePartition: "" + ## @param secondary.podAnnotations [object] Additional pod annotations for MySQL secondary pods + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + podAnnotations: {} + ## @param secondary.podAffinityPreset MySQL secondary pod affinity preset. Ignored if `secondary.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## + podAffinityPreset: "" + ## @param secondary.podAntiAffinityPreset MySQL secondary pod anti-affinity preset. Ignored if `secondary.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## Allowed values: soft, hard + ## + podAntiAffinityPreset: soft + ## MySQL Secondary node affinity preset + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity + ## + nodeAffinityPreset: + ## @param secondary.nodeAffinityPreset.type MySQL secondary node affinity preset type. Ignored if `secondary.affinity` is set. Allowed values: `soft` or `hard` + ## + type: "" + ## @param secondary.nodeAffinityPreset.key MySQL secondary node label key to match Ignored if `secondary.affinity` is set. + ## E.g. + ## key: "kubernetes.io/e2e-az-name" + ## + key: "" + ## @param secondary.nodeAffinityPreset.values [array] MySQL secondary node label values to match. Ignored if `secondary.affinity` is set. + ## E.g. + ## values: + ## - e2e-az1 + ## - e2e-az2 + ## + values: [] + ## @param secondary.affinity [object] Affinity for MySQL secondary pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + ## Note: podAffinityPreset, podAntiAffinityPreset, and nodeAffinityPreset will be ignored when it's set + ## + affinity: {} + ## @param secondary.nodeSelector [object] Node labels for MySQL secondary pods assignment + ## ref: https://kubernetes.io/docs/user-guide/node-selection/ + ## + nodeSelector: {} + ## @param secondary.tolerations [array] Tolerations for MySQL secondary pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + ## + tolerations: [] + ## MySQL secondary Pod security context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + ## @param secondary.podSecurityContext.enabled Enable security context for MySQL secondary pods + ## @param secondary.podSecurityContext.fsGroup Group ID for the mounted volumes' filesystem + ## + podSecurityContext: + enabled: true + fsGroup: 1001 + ## MySQL secondary container security context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container + ## @param secondary.containerSecurityContext.enabled MySQL secondary container securityContext + ## @param secondary.containerSecurityContext.runAsUser User ID for the MySQL secondary container + ## + containerSecurityContext: + enabled: true + runAsUser: 1001 + ## MySQL secondary container's resource requests and limits + ## ref: https://kubernetes.io/docs/user-guide/compute-resources/ + ## We usually recommend not to specify default resources and to leave this as a conscious + ## choice for the user. This also increases chances charts run on environments with little + ## resources, such as Minikube. If you do want to specify resources, uncomment the following + ## lines, adjust them as necessary, and remove the curly braces after 'resources:'. + ## @param secondary.resources.limits [object] The resources limits for MySQL secondary containers + ## @param secondary.resources.requests [object] The requested resources for MySQL secondary containers + ## + resources: + ## Example: + ## limits: + ## cpu: 250m + ## memory: 256Mi + limits: {} + ## Examples: + ## requests: + ## cpu: 250m + ## memory: 256Mi + requests: {} + ## Configure extra options for liveness probe + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + ## @param secondary.livenessProbe.enabled Enable livenessProbe + ## @param secondary.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe + ## @param secondary.livenessProbe.periodSeconds Period seconds for livenessProbe + ## @param secondary.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe + ## @param secondary.livenessProbe.failureThreshold Failure threshold for livenessProbe + ## @param secondary.livenessProbe.successThreshold Success threshold for livenessProbe + ## + livenessProbe: + enabled: true + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + successThreshold: 1 + ## Configure extra options for readiness probe + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + ## @param secondary.readinessProbe.enabled Enable readinessProbe + ## @param secondary.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe + ## @param secondary.readinessProbe.periodSeconds Period seconds for readinessProbe + ## @param secondary.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe + ## @param secondary.readinessProbe.failureThreshold Failure threshold for readinessProbe + ## @param secondary.readinessProbe.successThreshold Success threshold for readinessProbe + ## + readinessProbe: + enabled: true + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 3 + successThreshold: 1 + ## Configure extra options for startupProbe probe + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + ## @param secondary.startupProbe.enabled Enable startupProbe + ## @param secondary.startupProbe.initialDelaySeconds Initial delay seconds for startupProbe + ## @param secondary.startupProbe.periodSeconds Period seconds for startupProbe + ## @param secondary.startupProbe.timeoutSeconds Timeout seconds for startupProbe + ## @param secondary.startupProbe.failureThreshold Failure threshold for startupProbe + ## @param secondary.startupProbe.successThreshold Success threshold for startupProbe + ## + startupProbe: + enabled: true + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 1 + failureThreshold: 15 + successThreshold: 1 + ## @param secondary.customLivenessProbe [object] Override default liveness probe for MySQL secondary containers + ## + customLivenessProbe: {} + ## @param secondary.customReadinessProbe [object] Override default readiness probe for MySQL secondary containers + ## + customReadinessProbe: {} + ## @param secondary.customStartupProbe [object] Override default startup probe for MySQL secondary containers + ## + customStartupProbe: {} + ## @param secondary.extraFlags MySQL secondary additional command line flags + ## Can be used to specify command line flags, for example: + ## E.g. + ## extraFlags: "--max-connect-errors=1000 --max_connections=155" + ## + extraFlags: "" + ## @param secondary.extraEnvVars [array] An array to add extra environment variables on MySQL secondary containers + ## E.g. + ## extraEnvVars: + ## - name: TZ + ## value: "Europe/Paris" + ## + extraEnvVars: [] + ## @param secondary.extraEnvVarsCM Name of existing ConfigMap containing extra env vars for MySQL secondary containers + ## + extraEnvVarsCM: "" + ## @param secondary.extraEnvVarsSecret Name of existing Secret containing extra env vars for MySQL secondary containers + ## + extraEnvVarsSecret: "" + ## Enable persistence using Persistent Volume Claims + ## ref: https://kubernetes.io/docs/user-guide/persistent-volumes/ + ## + persistence: + ## @param secondary.persistence.enabled Enable persistence on MySQL secondary replicas using a `PersistentVolumeClaim` + ## + enabled: true + ## @param secondary.persistence.storageClass MySQL secondary persistent volume storage Class + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + storageClass: "" + ## @param secondary.persistence.annotations [object] MySQL secondary persistent volume claim annotations + ## + annotations: {} + ## @param secondary.persistence.accessModes MySQL secondary persistent volume access Modes + ## + accessModes: + - ReadWriteOnce + ## @param secondary.persistence.size MySQL secondary persistent volume size + ## + size: 8Gi + ## @param secondary.persistence.selector [object] Selector to match an existing Persistent Volume + ## selector: + ## matchLabels: + ## app: my-app + ## + selector: {} + ## @param secondary.extraVolumes [array] Optionally specify extra list of additional volumes to the MySQL secondary pod(s) + ## + extraVolumes: [] + ## @param secondary.extraVolumeMounts [array] Optionally specify extra list of additional volumeMounts for the MySQL secondary container(s) + ## + extraVolumeMounts: [] + ## @param secondary.initContainers [array] Add additional init containers for the MySQL secondary pod(s) + ## + initContainers: [] + ## @param secondary.sidecars [array] Add additional sidecar containers for the MySQL secondary pod(s) + ## + sidecars: [] + ## MySQL Secondary Service parameters + ## + service: + ## @param secondary.service.type MySQL secondary Kubernetes service type + ## + type: ClusterIP + ## @param secondary.service.port MySQL secondary Kubernetes service port + ## + port: 3306 + ## @param secondary.service.nodePort MySQL secondary Kubernetes service node port + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport + ## + nodePort: "" + ## @param secondary.service.clusterIP MySQL secondary Kubernetes service clusterIP IP + ## e.g: + ## clusterIP: None + ## + clusterIP: "" + ## @param secondary.service.loadBalancerIP MySQL secondary loadBalancerIP if service type is `LoadBalancer` + ## Set the LoadBalancer service type to internal only + ## ref: https://kubernetes.io/docs/concepts/services-networking/service/#internal-load-balancer + ## + loadBalancerIP: "" + ## @param secondary.service.externalTrafficPolicy Enable client source IP preservation + ## ref https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/#preserving-the-client-source-ip + ## + externalTrafficPolicy: Cluster + ## @param secondary.service.loadBalancerSourceRanges [array] Addresses that are allowed when MySQL secondary service is LoadBalancer + ## https://kubernetes.io/docs/tasks/access-application-cluster/configure-cloud-provider-firewall/#restrict-access-for-loadbalancer-service + ## E.g. + ## loadBalancerSourceRanges: + ## - 10.10.10.0/24 + ## + loadBalancerSourceRanges: [] + ## @param secondary.service.annotations [object] Provide any additional annotations which may be required + ## + annotations: {} + ## MySQL secondary Pod Disruption Budget configuration + ## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb/ + ## + pdb: + ## @param secondary.pdb.enabled Enable/disable a Pod Disruption Budget creation for MySQL secondary pods + ## + enabled: false + ## @param secondary.pdb.minAvailable Minimum number/percentage of MySQL secondary pods that should remain scheduled + ## + minAvailable: 1 + ## @param secondary.pdb.maxUnavailable Maximum number/percentage of MySQL secondary pods that may be made unavailable + ## + maxUnavailable: "" + ## @param secondary.podLabels [object] Additional pod labels for MySQL secondary pods + ## + podLabels: {} + +## @section RBAC parameters + +## MySQL pods ServiceAccount +## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/ +## +serviceAccount: + ## @param serviceAccount.create Enable the creation of a ServiceAccount for MySQL pods + ## + create: true + ## @param serviceAccount.name Name of the created ServiceAccount + ## If not set and create is true, a name is generated using the mysql.fullname template + ## + name: "" + ## @param serviceAccount.annotations [object] Annotations for MySQL Service Account + ## + annotations: {} +## Role Based Access +## ref: https://kubernetes.io/docs/admin/authorization/rbac/ +## +rbac: + ## @param rbac.create Whether to create & use RBAC resources or not + ## + create: false + +## @section Network Policy + +## MySQL Nework Policy configuration +## +networkPolicy: + ## @param networkPolicy.enabled Enable creation of NetworkPolicy resources + ## + enabled: false + ## @param networkPolicy.allowExternal The Policy model to apply. + ## When set to false, only pods with the correct + ## client label will have network access to the port MySQL is listening + ## on. When true, MySQL will accept connections from any source + ## (with the correct destination port). + ## + allowExternal: true + ## @param networkPolicy.explicitNamespacesSelector [object] A Kubernetes LabelSelector to explicitly select namespaces from which ingress traffic could be allowed to MySQL + ## If explicitNamespacesSelector is missing or set to {}, only client Pods that are in the networkPolicy's namespace + ## and that match other criteria, the ones that have the good label, can reach the DB. + ## But sometimes, we want the DB to be accessible to clients from other namespaces, in this case, we can use this + ## LabelSelector to select these namespaces, note that the networkPolicy's namespace should also be explicitly added. + ## + ## Example: + ## explicitNamespacesSelector: + ## matchLabels: + ## role: frontend + ## matchExpressions: + ## - {key: role, operator: In, values: [frontend]} + ## + explicitNamespacesSelector: {} + +## @section Volume Permissions parameters + +## Init containers parameters: +## volumePermissions: Change the owner and group of the persistent volume mountpoint to runAsUser:fsGroup values from the securityContext section. +## +volumePermissions: + ## @param volumePermissions.enabled Enable init container that changes the owner and group of the persistent volume(s) mountpoint to `runAsUser:fsGroup` + ## + enabled: false + ## @param volumePermissions.image.registry Init container volume-permissions image registry + ## @param volumePermissions.image.repository Init container volume-permissions image repository + ## @param volumePermissions.image.tag Init container volume-permissions image tag (immutable tags are recommended) + ## @param volumePermissions.image.pullPolicy Init container volume-permissions image pull policy + ## @param volumePermissions.image.pullSecrets [array] Specify docker-registry secret names as an array + ## + image: + registry: docker.io + repository: bitnami/bitnami-shell + tag: 10-debian-10-r349 + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## e.g: + ## pullSecrets: + ## - myRegistryKeySecretName + ## + pullSecrets: [] + ## @param volumePermissions.resources [object] Init container volume-permissions resources + ## + resources: {} + +## @section Metrics parameters + +## Mysqld Prometheus exporter parameters +## +metrics: + ## @param metrics.enabled Start a side-car prometheus exporter + ## + enabled: false + ## @param metrics.image.registry Exporter image registry + ## @param metrics.image.repository Exporter image repository + ## @param metrics.image.tag Exporter image tag (immutable tags are recommended) + ## @param metrics.image.pullPolicy Exporter image pull policy + ## @param metrics.image.pullSecrets [array] Specify docker-registry secret names as an array + ## + image: + registry: docker.io + repository: bitnami/mysqld-exporter + tag: 0.13.0-debian-10-r256 + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## e.g: + ## pullSecrets: + ## - myRegistryKeySecretName + ## + pullSecrets: [] + ## MySQL Prometheus exporter service parameters + ## Mysqld Prometheus exporter liveness and readiness probes + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes + ## @param metrics.service.type Kubernetes service type for MySQL Prometheus Exporter + ## @param metrics.service.port MySQL Prometheus Exporter service port + ## @param metrics.service.annotations [object] Prometheus exporter service annotations + ## + service: + type: ClusterIP + port: 9104 + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "{{ .Values.metrics.service.port }}" + ## @param metrics.extraArgs.primary [array] Extra args to be passed to mysqld_exporter on Primary pods + ## @param metrics.extraArgs.secondary [array] Extra args to be passed to mysqld_exporter on Secondary pods + ## ref: https://github.com/prometheus/mysqld_exporter/ + ## E.g. + ## - --collect.auto_increment.columns + ## - --collect.binlog_size + ## - --collect.engine_innodb_status + ## - --collect.engine_tokudb_status + ## - --collect.global_status + ## - --collect.global_variables + ## - --collect.info_schema.clientstats + ## - --collect.info_schema.innodb_metrics + ## - --collect.info_schema.innodb_tablespaces + ## - --collect.info_schema.innodb_cmp + ## - --collect.info_schema.innodb_cmpmem + ## - --collect.info_schema.processlist + ## - --collect.info_schema.processlist.min_time + ## - --collect.info_schema.query_response_time + ## - --collect.info_schema.tables + ## - --collect.info_schema.tables.databases + ## - --collect.info_schema.tablestats + ## - --collect.info_schema.userstats + ## - --collect.perf_schema.eventsstatements + ## - --collect.perf_schema.eventsstatements.digest_text_limit + ## - --collect.perf_schema.eventsstatements.limit + ## - --collect.perf_schema.eventsstatements.timelimit + ## - --collect.perf_schema.eventswaits + ## - --collect.perf_schema.file_events + ## - --collect.perf_schema.file_instances + ## - --collect.perf_schema.indexiowaits + ## - --collect.perf_schema.tableiowaits + ## - --collect.perf_schema.tablelocks + ## - --collect.perf_schema.replication_group_member_stats + ## - --collect.slave_status + ## - --collect.slave_hosts + ## - --collect.heartbeat + ## - --collect.heartbeat.database + ## - --collect.heartbeat.table + ## + extraArgs: + primary: [] + secondary: [] + ## Mysqld Prometheus exporter resource requests and limits + ## ref: https://kubernetes.io/docs/user-guide/compute-resources/ + ## We usually recommend not to specify default resources and to leave this as a conscious + ## choice for the user. This also increases chances charts run on environments with little + ## resources, such as Minikube. If you do want to specify resources, uncomment the following + ## lines, adjust them as necessary, and remove the curly braces after 'resources:'. + ## @param metrics.resources.limits [object] The resources limits for MySQL prometheus exporter containers + ## @param metrics.resources.requests [object] The requested resources for MySQL prometheus exporter containers + ## + resources: + ## Example: + ## limits: + ## cpu: 100m + ## memory: 256Mi + limits: {} + ## Examples: + ## requests: + ## cpu: 100m + ## memory: 256Mi + requests: {} + ## Mysqld Prometheus exporter liveness probe + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes + ## @param metrics.livenessProbe.enabled Enable livenessProbe + ## @param metrics.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe + ## @param metrics.livenessProbe.periodSeconds Period seconds for livenessProbe + ## @param metrics.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe + ## @param metrics.livenessProbe.failureThreshold Failure threshold for livenessProbe + ## @param metrics.livenessProbe.successThreshold Success threshold for livenessProbe + ## + livenessProbe: + enabled: true + initialDelaySeconds: 120 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + ## Mysqld Prometheus exporter readiness probe + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes + ## @param metrics.readinessProbe.enabled Enable readinessProbe + ## @param metrics.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe + ## @param metrics.readinessProbe.periodSeconds Period seconds for readinessProbe + ## @param metrics.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe + ## @param metrics.readinessProbe.failureThreshold Failure threshold for readinessProbe + ## @param metrics.readinessProbe.successThreshold Success threshold for readinessProbe + ## + readinessProbe: + enabled: true + initialDelaySeconds: 30 + periodSeconds: 10 + timeoutSeconds: 1 + successThreshold: 1 + failureThreshold: 3 + ## Prometheus Service Monitor + ## ref: https://github.com/coreos/prometheus-operator + ## + serviceMonitor: + ## @param metrics.serviceMonitor.enabled Create ServiceMonitor Resource for scraping metrics using PrometheusOperator + ## + enabled: false + ## @param metrics.serviceMonitor.namespace Specify the namespace in which the serviceMonitor resource will be created + ## + namespace: "" + ## @param metrics.serviceMonitor.interval Specify the interval at which metrics should be scraped + ## + interval: 30s + ## @param metrics.serviceMonitor.scrapeTimeout Specify the timeout after which the scrape is ended + ## e.g: + ## scrapeTimeout: 30s + ## + scrapeTimeout: "" + ## @param metrics.serviceMonitor.relabellings [array] Specify Metric Relabellings to add to the scrape endpoint + ## + relabellings: [] + ## @param metrics.serviceMonitor.honorLabels Specify honorLabels parameter to add the scrape endpoint + ## + honorLabels: false + ## @param metrics.serviceMonitor.additionalLabels [object] Used to pass Labels that are used by the Prometheus installed in your cluster to select Service Monitors to work with + ## ref: https://github.com/coreos/prometheus-operator/blob/master/Documentation/api.md#prometheusspec + ## + additionalLabels: {} diff --git a/pkg/iac/scanners/helm/test/option_test.go b/pkg/iac/scanners/helm/test/option_test.go new file mode 100644 index 000000000000..42dd08dc90ce --- /dev/null +++ b/pkg/iac/scanners/helm/test/option_test.go @@ -0,0 +1,225 @@ +package test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/helm/parser" +) + +func Test_helm_parser_with_options_with_values_file(t *testing.T) { + + tests := []struct { + testName string + chartName string + valuesFile string + }{ + { + testName: "Parsing directory 'testchart'", + chartName: "testchart", + valuesFile: "values/values.yaml", + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + chartName := test.chartName + + t.Logf("Running test: %s", test.testName) + + var opts []parser.Option + + if test.valuesFile != "" { + opts = append(opts, parser.OptionWithValuesFile(test.valuesFile)) + } + + helmParser, err := parser.New(chartName, opts...) + require.NoError(t, err) + require.NoError(t, helmParser.ParseFS(t.Context(), os.DirFS(filepath.Join("testdata", chartName)), ".")) + manifests, err := helmParser.RenderedChartFiles() + require.NoError(t, err) + + assert.Len(t, manifests, 3) + + for _, manifest := range manifests { + expectedPath := filepath.Join("testdata", "expected", "options", chartName, manifest.TemplateFilePath) + + expectedContent, err := os.ReadFile(expectedPath) + require.NoError(t, err) + + cleanExpected := strings.ReplaceAll(string(expectedContent), "\r\n", "\n") + cleanActual := strings.ReplaceAll(manifest.ManifestContent, "\r\n", "\n") + + assert.Equal(t, cleanExpected, cleanActual) + } + }) + } +} + +func Test_helm_parser_with_options_with_set_value(t *testing.T) { + + tests := []struct { + testName string + chartName string + valuesFile string + values string + }{ + { + testName: "Parsing directory 'testchart'", + chartName: "testchart", + values: "securityContext.runAsUser=0", + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + chartName := test.chartName + + t.Logf("Running test: %s", test.testName) + + var opts []parser.Option + + if test.valuesFile != "" { + opts = append(opts, parser.OptionWithValuesFile(test.valuesFile)) + } + + if test.values != "" { + opts = append(opts, parser.OptionWithValues(test.values)) + } + + helmParser, err := parser.New(chartName, opts...) + require.NoError(t, err) + err = helmParser.ParseFS(t.Context(), os.DirFS(filepath.Join("testdata", chartName)), ".") + require.NoError(t, err) + manifests, err := helmParser.RenderedChartFiles() + require.NoError(t, err) + + assert.Len(t, manifests, 3) + + for _, manifest := range manifests { + expectedPath := filepath.Join("testdata", "expected", "options", chartName, manifest.TemplateFilePath) + + expectedContent, err := os.ReadFile(expectedPath) + require.NoError(t, err) + + cleanExpected := strings.ReplaceAll(string(expectedContent), "\r\n", "\n") + cleanActual := strings.ReplaceAll(manifest.ManifestContent, "\r\n", "\n") + + assert.Equal(t, cleanExpected, cleanActual) + } + }) + } +} + +func Test_helm_parser_with_options_with_api_versions(t *testing.T) { + + tests := []struct { + testName string + chartName string + apiVersions []string + }{ + { + testName: "Parsing directory 'with-api-version'", + chartName: "with-api-version", + apiVersions: []string{"policy/v1/PodDisruptionBudget"}, + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + chartName := test.chartName + + t.Logf("Running test: %s", test.testName) + + var opts []parser.Option + + if len(test.apiVersions) > 0 { + opts = append(opts, parser.OptionWithAPIVersions(test.apiVersions...)) + } + + helmParser, err := parser.New(chartName, opts...) + require.NoError(t, err) + err = helmParser.ParseFS(t.Context(), os.DirFS(filepath.Join("testdata", chartName)), ".") + require.NoError(t, err) + manifests, err := helmParser.RenderedChartFiles() + require.NoError(t, err) + + assert.Len(t, manifests, 1) + + for _, manifest := range manifests { + expectedPath := filepath.Join("testdata", "expected", "options", chartName, manifest.TemplateFilePath) + + expectedContent, err := os.ReadFile(expectedPath) + require.NoError(t, err) + + cleanExpected := strings.TrimSpace(strings.ReplaceAll(string(expectedContent), "\r\n", "\n")) + cleanActual := strings.TrimSpace(strings.ReplaceAll(manifest.ManifestContent, "\r\n", "\n")) + + assert.Equal(t, cleanExpected, cleanActual) + } + }) + } +} + +func Test_helm_parser_with_options_with_kube_versions(t *testing.T) { + + tests := []struct { + testName string + chartName string + kubeVersion string + expectedError string + }{ + { + testName: "Parsing directory 'with-kube-version'", + chartName: "with-kube-version", + kubeVersion: "1.60", + }, + { + testName: "Parsing directory 'with-kube-version' with invalid kube version", + chartName: "with-kube-version", + kubeVersion: "a.b.c", + expectedError: "could not parse \"a.b.c\" as version", + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + chartName := test.chartName + + t.Logf("Running test: %s", test.testName) + + var opts []parser.Option + + opts = append(opts, parser.OptionWithKubeVersion(test.kubeVersion)) + + helmParser, err := parser.New(chartName, opts...) + if test.expectedError != "" { + require.EqualError(t, err, test.expectedError) + return + } + require.NoError(t, err) + require.NoError(t, helmParser.ParseFS(t.Context(), os.DirFS(filepath.Join("testdata", chartName)), ".")) + manifests, err := helmParser.RenderedChartFiles() + require.NoError(t, err) + + assert.Len(t, manifests, 1) + + for _, manifest := range manifests { + expectedPath := filepath.Join("testdata", "expected", "options", chartName, manifest.TemplateFilePath) + + expectedContent, err := os.ReadFile(expectedPath) + require.NoError(t, err) + + cleanExpected := strings.TrimSpace(strings.ReplaceAll(string(expectedContent), "\r\n", "\n")) + cleanActual := strings.TrimSpace(strings.ReplaceAll(manifest.ManifestContent, "\r\n", "\n")) + + assert.Equal(t, cleanExpected, cleanActual) + } + }) + } +} diff --git a/pkg/iac/scanners/helm/test/parser_test.go b/pkg/iac/scanners/helm/test/parser_test.go new file mode 100644 index 000000000000..3d6c239b20c8 --- /dev/null +++ b/pkg/iac/scanners/helm/test/parser_test.go @@ -0,0 +1,186 @@ +package test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/detection" + "github.com/aquasecurity/trivy/pkg/iac/scanners/helm/parser" +) + +func Test_helm_parser(t *testing.T) { + + tests := []struct { + testName string + chartName string + }{ + { + testName: "Parsing directory 'testchart'", + chartName: "testchart", + }, + { + testName: "Parsing directory with tarred dependency", + chartName: "with-tarred-dep", + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + chartName := test.chartName + helmParser, err := parser.New(chartName) + require.NoError(t, err) + require.NoError(t, helmParser.ParseFS(t.Context(), os.DirFS("testdata"), chartName)) + manifests, err := helmParser.RenderedChartFiles() + require.NoError(t, err) + + assert.Len(t, manifests, 3) + + for _, manifest := range manifests { + expectedPath := filepath.Join("testdata", "expected", chartName, manifest.TemplateFilePath) + + expectedContent, err := os.ReadFile(expectedPath) + require.NoError(t, err) + + got := strings.ReplaceAll(manifest.ManifestContent, "\r\n", "\n") + assert.Equal(t, strings.ReplaceAll(string(expectedContent), "\r\n", "\n"), got) + } + }) + } +} + +func Test_helm_parser_where_name_non_string(t *testing.T) { + + tests := []struct { + testName string + chartName string + }{ + { + testName: "Scanning chart with integer for name", + chartName: "numberName", + }, + } + + for _, test := range tests { + chartName := test.chartName + + t.Logf("Running test: %s", test.testName) + + helmParser, err := parser.New(chartName) + require.NoError(t, err) + require.NoError(t, helmParser.ParseFS(t.Context(), os.DirFS(filepath.Join("testdata", chartName)), ".")) + } +} + +func Test_tar_is_chart(t *testing.T) { + + tests := []struct { + testName string + archiveFile string + isHelmChart bool + }{ + { + testName: "standard tarball", + archiveFile: "mysql-8.8.26.tar", + isHelmChart: true, + }, + { + testName: "gzip tarball with tar.gz extension", + archiveFile: "mysql-8.8.26.tar.gz", + isHelmChart: true, + }, + { + testName: "broken gzip tarball with tar.gz extension", + archiveFile: "aws-cluster-autoscaler-bad.tar.gz", + isHelmChart: true, + }, + { + testName: "gzip tarball with tgz extension", + archiveFile: "mysql-8.8.26.tgz", + isHelmChart: true, + }, + { + testName: "gzip tarball that has nothing of interest in it", + archiveFile: "nope.tgz", + isHelmChart: false, + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + testPath := filepath.Join("testdata", test.archiveFile) + file, err := os.Open(testPath) + require.NoError(t, err) + defer file.Close() + + assert.Equal(t, test.isHelmChart, detection.IsHelmChartArchive(test.archiveFile, file)) + }) + } +} + +func Test_helm_tarball_parser(t *testing.T) { + + tests := []struct { + testName string + chartName string + archiveFile string + }{ + { + testName: "standard tarball", + chartName: "mysql", + archiveFile: "mysql-8.8.26.tar", + }, + { + testName: "gzip tarball with tar.gz extension", + chartName: "mysql", + archiveFile: "mysql-8.8.26.tar.gz", + }, + { + testName: "gzip tarball with tgz extension", + chartName: "mysql", + archiveFile: "mysql-8.8.26.tgz", + }, + } + + for _, test := range tests { + + t.Logf("Running test: %s", test.testName) + + helmParser, err := parser.New(test.archiveFile) + require.NoError(t, err) + require.NoError(t, helmParser.ParseFS(t.Context(), os.DirFS("testdata"), test.archiveFile)) + + manifests, err := helmParser.RenderedChartFiles() + require.NoError(t, err) + + assert.Len(t, manifests, 6) + + oneOf := []string{ + "configmap.yaml", + "statefulset.yaml", + "svc-headless.yaml", + "svc.yaml", + "secrets.yaml", + "serviceaccount.yaml", + } + + for _, manifest := range manifests { + filename := filepath.Base(manifest.TemplateFilePath) + assert.Contains(t, oneOf, filename) + + if strings.HasSuffix(manifest.TemplateFilePath, "secrets.yaml") { + continue + } + expectedPath := filepath.Join("testdata", "expected", test.chartName, manifest.TemplateFilePath) + + expectedContent, err := os.ReadFile(expectedPath) + require.NoError(t, err) + + assert.Equal(t, strings.ReplaceAll(string(expectedContent), "\r\n", "\n"), strings.ReplaceAll(manifest.ManifestContent, "\r\n", "\n")) + } + } +} diff --git a/pkg/iac/scanners/helm/test/scanner_test.go b/pkg/iac/scanners/helm/test/scanner_test.go new file mode 100644 index 000000000000..fe4f769bd805 --- /dev/null +++ b/pkg/iac/scanners/helm/test/scanner_test.go @@ -0,0 +1,216 @@ +package test + +import ( + "os" + "path/filepath" + "strings" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners/helm" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/set" +) + +func TestScanner_ScanFS(t *testing.T) { + tests := []struct { + name string + target string + opts []options.ScannerOption + assert func(t *testing.T, results scan.Results) + }{ + { + name: "archived chart", + target: filepath.Join("testdata", "mysql-8.8.26.tar"), + assert: assertIds([]string{ + "KSV-0001", "KSV-0003", + "KSV-0011", "KSV-0012", "KSV-0014", + "KSV-0015", "KSV-0016", "KSV-0018", + "KSV-0020", "KSV-0021", "KSV-0030", + "KSV-0104", "KSV-0106", "KSV-0125", + "KSV-0004", + }), + }, + { + name: "chart in directory", + target: filepath.Join("testdata", "testchart"), + assert: func(t *testing.T, results scan.Results) { + assertIds([]string{ + "KSV-0001", "KSV-0003", + "KSV-0011", "KSV-0012", "KSV-0014", + "KSV-0015", "KSV-0016", + "KSV-0020", "KSV-0021", "KSV-0030", + "KSV-0104", "KSV-0106", + "KSV-0117", "KSV-0110", "KSV-0118", + "KSV-0004", + })(t, results) + + ignored := results.GetIgnored() + assert.Len(t, ignored, 1) + + assert.Equal(t, "KSV-0018", ignored[0].Rule().ID) + assert.Equal(t, "testchart/templates/deployment.yaml", ignored[0].Metadata().Range().GetFilename()) + }, + }, + { + // TODO: The chart name isn't actually empty + name: "scanner with missing chart name can recover", + target: filepath.Join("testdata", "aws-cluster-autoscaler-bad.tar.gz"), + assert: assertIds([]string{ + "KSV-0014", "KSV-0023", "KSV-0030", + "KSV-0104", "KSV-0003", "KSV-0018", + "KSV-0118", "KSV-0012", "KSV-0106", + "KSV-0016", "KSV-0001", "KSV-0011", + "KSV-0015", "KSV-0021", "KSV-0110", "KSV-0020", + "KSV-0004", + }), + }, + { + name: "with custom check", + target: filepath.Join("testdata", "mysql-8.8.26.tar"), + opts: []options.ScannerOption{ + rego.WithPolicyNamespaces("user"), + rego.WithPolicyReader(strings.NewReader(`package user.kubernetes.ID001 +__rego_metadata__ := { + "id": "USR-ID001", + "title": "Services not allowed", + "severity": "LOW", + "description": "Services are not allowed because of some reasons.", +} + +__rego_input__ := { + "selector": [ + {"type": "kubernetes"}, + ], +} + +deny[res] { + input.kind == "Service" + msg := sprintf("Found service '%s' but services are not allowed", [input.metadata.name]) + res := result.new(msg, input) +}`)), + }, + assert: assertIds([]string{ + "KSV-0001", "KSV-0003", + "KSV-0011", "KSV-0012", "KSV-0014", + "KSV-0015", "KSV-0016", "KSV-0018", + "KSV-0020", "KSV-0021", "KSV-0030", + "KSV-0104", "KSV-0106", "USR-ID001", + "KSV-0004", "KSV-0125", + }), + }, + { + name: "template-based name", + target: filepath.Join("testdata", "templated-name"), + opts: []options.ScannerOption{ + rego.WithEmbeddedLibraries(false), + rego.WithEmbeddedPolicies(false), + }, + }, + { + name: "failed result contains the code", + target: filepath.Join("testdata", "simmilar-templates"), + opts: []options.ScannerOption{ + rego.WithEmbeddedPolicies(false), + rego.WithEmbeddedLibraries(true), + rego.WithPolicyNamespaces("user"), + rego.WithPolicyReader(strings.NewReader(`# METADATA +# title: "Test rego" +# schemas: +# - input: schema["kubernetes"] +# custom: +# id: USR-ID001 +# severity: LOW +# input: +# selector: +# - type: kubernetes +package user.kubernetes.ID001 + +deny[res] { + input.spec.replicas == 3 + res := result.new("Replicas are not allowed", input) +}`)), + }, + assert: func(t *testing.T, results scan.Results) { + failedResults := results.GetFailed() + require.Len(t, failedResults, 1) + code, err := failedResults[0].GetCode() + require.NoError(t, err) + assert.NotNil(t, code) + }, + }, + { + name: "scan the subchart once", + target: filepath.Join("testdata", "with-subchart"), + opts: []options.ScannerOption{ + rego.WithEmbeddedPolicies(false), + rego.WithEmbeddedLibraries(true), + rego.WithPolicyNamespaces("user"), + rego.WithPolicyReader(strings.NewReader(`# METADATA +# title: "Test rego" +# schemas: +# - input: schema["kubernetes"] +# custom: +# id: USR-ID001 +# severity: LOW +# input: +# selector: +# - type: kubernetes +# subtypes: +# - kind: pod +package user.kubernetes.ID001 + +import data.lib.kubernetes + +deny[res] { + container := kubernetes.containers[_] + container.securityContext.readOnlyRootFilesystem == false + res := result.new("set 'securityContext.readOnlyRootFilesystem' to true", container) +}`)), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + opts := []options.ScannerOption{ + rego.WithEmbeddedPolicies(true), + rego.WithEmbeddedLibraries(true), + } + opts = append(opts, tt.opts...) + scanner := helm.New(opts...) + fsys := os.DirFS(filepath.Dir(tt.target)) + results, err := scanner.ScanFS(t.Context(), fsys, filepath.Base(tt.target)) + require.NoError(t, err) + + if tt.assert != nil { + tt.assert(t, results) + } + }) + } +} + +func assertIds(expected []string) func(t *testing.T, results scan.Results) { + return func(t *testing.T, results scan.Results) { + t.Helper() + + errorCodes := set.New[string]() + for _, result := range results.GetFailed() { + errorCodes.Append(result.Rule().ID) + } + assert.ElementsMatch(t, expected, errorCodes.Items()) + } +} + +func TestScaningNonHelmChartDoesNotCauseError(t *testing.T) { + fsys := fstest.MapFS{ + "testChart.yaml": &fstest.MapFile{Data: []byte(`foo: bar`)}, + } + _, err := helm.New().ScanFS(t.Context(), fsys, ".") + require.NoError(t, err) +} diff --git a/pkg/iac/scanners/helm/test/testdata/aws-cluster-autoscaler-bad.tar.gz b/pkg/iac/scanners/helm/test/testdata/aws-cluster-autoscaler-bad.tar.gz new file mode 100644 index 000000000000..a66f228c9851 Binary files /dev/null and b/pkg/iac/scanners/helm/test/testdata/aws-cluster-autoscaler-bad.tar.gz differ diff --git a/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/configmap.yaml b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/configmap.yaml new file mode 100644 index 000000000000..9ee00d2c2c0c --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/configmap.yaml @@ -0,0 +1,42 @@ +# Source: mysql/templates/primary/configmap.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: mysql + namespace: + labels: + app.kubernetes.io/name: mysql + helm.sh/chart: mysql-8.8.26 + app.kubernetes.io/instance: mysql + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: primary +data: + my.cnf: |- + + [mysqld] + default_authentication_plugin=mysql_native_password + skip-name-resolve + explicit_defaults_for_timestamp + basedir=/opt/bitnami/mysql + plugin_dir=/opt/bitnami/mysql/lib/plugin + port=3306 + socket=/opt/bitnami/mysql/tmp/mysql.sock + datadir=/bitnami/mysql/data + tmpdir=/opt/bitnami/mysql/tmp + max_allowed_packet=16M + bind-address=0.0.0.0 + pid-file=/opt/bitnami/mysql/tmp/mysqld.pid + log-error=/opt/bitnami/mysql/logs/mysqld.log + character-set-server=UTF8 + collation-server=utf8_general_ci + + [client] + port=3306 + socket=/opt/bitnami/mysql/tmp/mysql.sock + default-character-set=UTF8 + plugin_dir=/opt/bitnami/mysql/lib/plugin + + [manager] + port=3306 + socket=/opt/bitnami/mysql/tmp/mysql.sock + pid-file=/opt/bitnami/mysql/tmp/mysqld.pid \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/statefulset.yaml b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/statefulset.yaml new file mode 100644 index 000000000000..a7f5f59d831b --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/statefulset.yaml @@ -0,0 +1,147 @@ +# Source: mysql/templates/primary/statefulset.yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mysql + namespace: + labels: + app.kubernetes.io/name: mysql + helm.sh/chart: mysql-8.8.26 + app.kubernetes.io/instance: mysql + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: primary +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: mysql + app.kubernetes.io/instance: mysql + app.kubernetes.io/component: primary + serviceName: mysql + updateStrategy: + type: RollingUpdate + template: + metadata: + annotations: + checksum/configuration: 6adfba795651cd736dfa943a87e0853ce417b9fb842b57535e3b1b4e762a33fd + labels: + app.kubernetes.io/name: mysql + helm.sh/chart: mysql-8.8.26 + app.kubernetes.io/instance: mysql + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: primary + spec: + + serviceAccountName: mysql + affinity: + podAffinity: + + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: + app.kubernetes.io/name: mysql + app.kubernetes.io/instance: mysql + app.kubernetes.io/component: primary + namespaces: + - "" + topologyKey: kubernetes.io/hostname + weight: 1 + nodeAffinity: + + securityContext: + fsGroup: 1001 + containers: + - name: mysql + image: docker.io/bitnami/mysql:8.0.28-debian-10-r23 + imagePullPolicy: "IfNotPresent" + securityContext: + runAsUser: 1001 + env: + - name: BITNAMI_DEBUG + value: "false" + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: mysql + key: mysql-root-password + - name: MYSQL_DATABASE + value: "my_database" + ports: + - name: mysql + containerPort: 3306 + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + exec: + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE") + fi + mysqladmin status -uroot -p"${password_aux}" + readinessProbe: + failureThreshold: 3 + initialDelaySeconds: 5 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + exec: + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE") + fi + mysqladmin status -uroot -p"${password_aux}" + startupProbe: + failureThreshold: 10 + initialDelaySeconds: 15 + periodSeconds: 10 + successThreshold: 1 + timeoutSeconds: 1 + exec: + command: + - /bin/bash + - -ec + - | + password_aux="${MYSQL_ROOT_PASSWORD:-}" + if [[ -f "${MYSQL_ROOT_PASSWORD_FILE:-}" ]]; then + password_aux=$(cat "$MYSQL_ROOT_PASSWORD_FILE") + fi + mysqladmin status -uroot -p"${password_aux}" + resources: + limits: {} + requests: {} + volumeMounts: + - name: data + mountPath: /bitnami/mysql + - name: config + mountPath: /opt/bitnami/mysql/conf/my.cnf + subPath: my.cnf + volumes: + - name: config + configMap: + name: mysql + volumeClaimTemplates: + - metadata: + name: data + labels: + app.kubernetes.io/name: mysql + app.kubernetes.io/instance: mysql + app.kubernetes.io/component: primary + spec: + accessModes: + - "ReadWriteOnce" + resources: + requests: + storage: "8Gi" \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/svc-headless.yaml b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/svc-headless.yaml new file mode 100644 index 000000000000..9fe0f11c87ae --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/svc-headless.yaml @@ -0,0 +1,25 @@ +# Source: mysql/templates/primary/svc-headless.yaml +apiVersion: v1 +kind: Service +metadata: + name: mysql-headless + namespace: + labels: + app.kubernetes.io/name: mysql + helm.sh/chart: mysql-8.8.26 + app.kubernetes.io/instance: mysql + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: primary + annotations: +spec: + type: ClusterIP + clusterIP: None + publishNotReadyAddresses: true + ports: + - name: mysql + port: 3306 + targetPort: mysql + selector: + app.kubernetes.io/name: mysql + app.kubernetes.io/instance: mysql + app.kubernetes.io/component: primary \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/svc.yaml b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/svc.yaml new file mode 100644 index 000000000000..2bbdab8fe468 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/primary/svc.yaml @@ -0,0 +1,25 @@ +# Source: mysql/templates/primary/svc.yaml +apiVersion: v1 +kind: Service +metadata: + name: mysql + namespace: + labels: + app.kubernetes.io/name: mysql + helm.sh/chart: mysql-8.8.26 + app.kubernetes.io/instance: mysql + app.kubernetes.io/managed-by: Helm + app.kubernetes.io/component: primary + annotations: +spec: + type: ClusterIP + ports: + - name: mysql + port: 3306 + protocol: TCP + targetPort: mysql + nodePort: null + selector: + app.kubernetes.io/name: mysql + app.kubernetes.io/instance: mysql + app.kubernetes.io/component: primary \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/secrets.yaml b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/secrets.yaml new file mode 100644 index 000000000000..ffa6909e2f04 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/secrets.yaml @@ -0,0 +1,15 @@ +# Source: mysql/templates/secrets.yaml +apiVersion: v1 +kind: Secret +metadata: + name: mysql + namespace: + labels: + app.kubernetes.io/name: mysql + helm.sh/chart: mysql-8.8.26 + app.kubernetes.io/instance: mysql + app.kubernetes.io/managed-by: Helm +type: Opaque +data: + mysql-root-password: "aGZYYW1vN3V5NA==" + mysql-password: "eHR6YU9MR1VhbA==" \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/serviceaccount.yaml b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/serviceaccount.yaml new file mode 100644 index 000000000000..760b8bf731a5 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/mysql/templates/serviceaccount.yaml @@ -0,0 +1,14 @@ +# Source: mysql/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mysql + namespace: + labels: + app.kubernetes.io/name: mysql + helm.sh/chart: mysql-8.8.26 + app.kubernetes.io/instance: mysql + app.kubernetes.io/managed-by: Helm + annotations: +secrets: + - name: mysql \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/options/testchart/templates/deployment.yaml b/pkg/iac/scanners/helm/test/testdata/expected/options/testchart/templates/deployment.yaml new file mode 100644 index 000000000000..1280e673b92a --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/options/testchart/templates/deployment.yaml @@ -0,0 +1,47 @@ +# Source: testchart/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: testchart + labels: + helm.sh/chart: testchart-0.1.0 + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + template: + metadata: + labels: + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + spec: + serviceAccountName: testchart + securityContext: + {} + containers: + # trivy:ignore:KSV018 + - name: testchart + securityContext: + runAsUser: 0 + image: "nginx:1.16.0" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {} \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/options/testchart/templates/service.yaml b/pkg/iac/scanners/helm/test/testdata/expected/options/testchart/templates/service.yaml new file mode 100644 index 000000000000..6c6699f3d5dd --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/options/testchart/templates/service.yaml @@ -0,0 +1,21 @@ +# Source: testchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: testchart + labels: + helm.sh/chart: testchart-0.1.0 + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/options/testchart/templates/serviceaccount.yaml b/pkg/iac/scanners/helm/test/testdata/expected/options/testchart/templates/serviceaccount.yaml new file mode 100644 index 000000000000..6fe44a89bb3b --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/options/testchart/templates/serviceaccount.yaml @@ -0,0 +1,11 @@ +# Source: testchart/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: testchart + labels: + helm.sh/chart: testchart-0.1.0 + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/options/with-api-version/templates/pdb.yaml b/pkg/iac/scanners/helm/test/testdata/expected/options/with-api-version/templates/pdb.yaml new file mode 100644 index 000000000000..7c7ef5fd74d7 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/options/with-api-version/templates/pdb.yaml @@ -0,0 +1,17 @@ +# Source: with-api-version/templates/pdb.yaml +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: with-api-version + labels: + helm.sh/chart: with-api-version-0.1.0 + app.kubernetes.io/name: with-api-version + app.kubernetes.io/instance: with-api-version + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + selector: + matchLabels: + app.kubernetes.io/name: with-api-version + app.kubernetes.io/instance: with-api-version + maxUnavailable: 0 diff --git a/pkg/iac/scanners/helm/test/testdata/expected/options/with-kube-version/templates/pdb.yaml b/pkg/iac/scanners/helm/test/testdata/expected/options/with-kube-version/templates/pdb.yaml new file mode 100644 index 000000000000..7c7ef5fd74d7 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/options/with-kube-version/templates/pdb.yaml @@ -0,0 +1,17 @@ +# Source: with-api-version/templates/pdb.yaml +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: with-api-version + labels: + helm.sh/chart: with-api-version-0.1.0 + app.kubernetes.io/name: with-api-version + app.kubernetes.io/instance: with-api-version + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + selector: + matchLabels: + app.kubernetes.io/name: with-api-version + app.kubernetes.io/instance: with-api-version + maxUnavailable: 0 diff --git a/pkg/iac/scanners/helm/test/testdata/expected/testchart/templates/deployment.yaml b/pkg/iac/scanners/helm/test/testdata/expected/testchart/templates/deployment.yaml new file mode 100644 index 000000000000..9a2e277c106a --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/testchart/templates/deployment.yaml @@ -0,0 +1,47 @@ +# Source: testchart/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: testchart + labels: + helm.sh/chart: testchart-0.1.0 + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + template: + metadata: + labels: + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + spec: + serviceAccountName: testchart + securityContext: + {} + containers: + # trivy:ignore:KSV018 + - name: testchart + securityContext: + {} + image: "nginx:1.16.0" + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {} \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/testchart/templates/service.yaml b/pkg/iac/scanners/helm/test/testdata/expected/testchart/templates/service.yaml new file mode 100644 index 000000000000..6c6699f3d5dd --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/testchart/templates/service.yaml @@ -0,0 +1,21 @@ +# Source: testchart/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: testchart + labels: + helm.sh/chart: testchart-0.1.0 + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm +spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/testchart/templates/serviceaccount.yaml b/pkg/iac/scanners/helm/test/testdata/expected/testchart/templates/serviceaccount.yaml new file mode 100644 index 000000000000..6fe44a89bb3b --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/testchart/templates/serviceaccount.yaml @@ -0,0 +1,11 @@ +# Source: testchart/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: testchart + labels: + helm.sh/chart: testchart-0.1.0 + app.kubernetes.io/name: testchart + app.kubernetes.io/instance: testchart + app.kubernetes.io/version: "1.16.0" + app.kubernetes.io/managed-by: Helm \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/with-tarred-dep/templates/deployment.yaml b/pkg/iac/scanners/helm/test/testdata/expected/with-tarred-dep/templates/deployment.yaml new file mode 100644 index 000000000000..ed57d12a6e2b --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/with-tarred-dep/templates/deployment.yaml @@ -0,0 +1,78 @@ +# Source: with-tarred-dep/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: with-tarred-dep + labels: + app.kubernetes.io/name: with-tarred-dep + helm.sh/chart: with-tarred-dep-0.1.1 + app.kubernetes.io/instance: with-tarred-dep + app.kubernetes.io/managed-by: Helm +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: with-tarred-dep + app.kubernetes.io/instance: with-tarred-dep + template: + metadata: + labels: + app.kubernetes.io/name: with-tarred-dep + helm.sh/chart: with-tarred-dep-0.1.1 + app.kubernetes.io/instance: with-tarred-dep + app.kubernetes.io/managed-by: Helm + spec: + containers: + - name: metadata-service + env: + - name: METADATASERVICE_UPSTREAM_API_URL + value: '' + - name: METADATASERVICE_OIDC_AUDIENCE + value: "" + - name: METADATASERVICE_OIDC_ISSUER + value: "" + - name: METADATASERVICE_OIDC_JWKSURI + value: "" + - name: METADATASERVICE_OIDC_CLAIMS_ROLES + value: "" + - name: METADATASERVICE_OIDC_CLAIMS_USERNAME + value: "" + - name: METADATASERVICE_DB_URI + valueFrom: + secretKeyRef: + name: with-tarred-dep-dbconn + key: uri + image: "ghcr.io/metal-toolbox/hollow-metadataservice:v0.0.1" + imagePullPolicy: Always + volumeMounts: + - name: dbcerts + mountPath: "/dbcerts" + readOnly: true + ports: + - name: http + containerPort: 8000 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz/liveness + port: http + initialDelaySeconds: 5 + timeoutSeconds: 2 + readinessProbe: + httpGet: + path: /healthz/readiness + port: http + initialDelaySeconds: 5 + timeoutSeconds: 2 + resources: + limits: + cpu: 4 + memory: 4Gi + requests: + cpu: 4 + memory: 4Gi + volumes: + - name: dbcerts + secret: + secretName: with-tarred-dep-crdb-ca + defaultMode: 0400 \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/with-tarred-dep/templates/ingress.yaml b/pkg/iac/scanners/helm/test/testdata/expected/with-tarred-dep/templates/ingress.yaml new file mode 100644 index 000000000000..b48564477997 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/with-tarred-dep/templates/ingress.yaml @@ -0,0 +1,26 @@ +# Source: with-tarred-dep/templates/ingress.yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: with-tarred-dep + labels: + app.kubernetes.io/name: with-tarred-dep + helm.sh/chart: with-tarred-dep-0.1.1 + app.kubernetes.io/instance: with-tarred-dep + app.kubernetes.io/managed-by: Helm +spec: + rules: + - host: metadata-service.mydomain + http: + paths: + - path: /($|metadata|userdata|2009-04-04) + pathType: Prefix + backend: + service: + name: with-tarred-dep + port: + name: http +# tls: [] +# hosts: +# - hollow-metadataservice.mydomain +# secretName: hollow-metadataservice-example-tls \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/expected/with-tarred-dep/templates/service.yaml b/pkg/iac/scanners/helm/test/testdata/expected/with-tarred-dep/templates/service.yaml new file mode 100644 index 000000000000..7d86aeb5b02b --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/expected/with-tarred-dep/templates/service.yaml @@ -0,0 +1,24 @@ +# Source: with-tarred-dep/templates/service.yaml +apiVersion: v1 +kind: Service +metadata: + name: with-tarred-dep + labels: + app.kubernetes.io/name: with-tarred-dep + helm.sh/chart: with-tarred-dep-0.1.1 + app.kubernetes.io/instance: with-tarred-dep + app.kubernetes.io/managed-by: Helm +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 8000 + - name: https + port: 443 + protocol: TCP + targetPort: 8000 + selector: + app.kubernetes.io/name: with-tarred-dep + app.kubernetes.io/instance: with-tarred-dep + type: ClusterIP \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/mysql-8.8.26.tar b/pkg/iac/scanners/helm/test/testdata/mysql-8.8.26.tar new file mode 100644 index 000000000000..53cb6802de42 Binary files /dev/null and b/pkg/iac/scanners/helm/test/testdata/mysql-8.8.26.tar differ diff --git a/pkg/iac/scanners/helm/test/testdata/mysql-8.8.26.tar.gz b/pkg/iac/scanners/helm/test/testdata/mysql-8.8.26.tar.gz new file mode 100644 index 000000000000..ff8bd1ab402e Binary files /dev/null and b/pkg/iac/scanners/helm/test/testdata/mysql-8.8.26.tar.gz differ diff --git a/pkg/iac/scanners/helm/test/testdata/mysql-8.8.26.tgz b/pkg/iac/scanners/helm/test/testdata/mysql-8.8.26.tgz new file mode 100644 index 000000000000..ff8bd1ab402e Binary files /dev/null and b/pkg/iac/scanners/helm/test/testdata/mysql-8.8.26.tgz differ diff --git a/pkg/iac/scanners/helm/test/testdata/nope.tgz b/pkg/iac/scanners/helm/test/testdata/nope.tgz new file mode 100644 index 000000000000..a47332d93877 Binary files /dev/null and b/pkg/iac/scanners/helm/test/testdata/nope.tgz differ diff --git a/pkg/iac/scanners/helm/test/testdata/numberName/Chart.yaml b/pkg/iac/scanners/helm/test/testdata/numberName/Chart.yaml new file mode 100644 index 000000000000..e840fbabf456 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/numberName/Chart.yaml @@ -0,0 +1,3 @@ +apiVersion: v2 +name: 1001 +version: 1.0.0 \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/simmilar-templates/Chart.yaml b/pkg/iac/scanners/helm/test/testdata/simmilar-templates/Chart.yaml new file mode 100644 index 000000000000..e5855a786639 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/simmilar-templates/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: test-license-manager +version: 1.10.0 +type: application +appVersion: 1.10.0 +deprecated: false diff --git a/pkg/iac/scanners/helm/test/testdata/simmilar-templates/templates/deployment.yaml b/pkg/iac/scanners/helm/test/testdata/simmilar-templates/templates/deployment.yaml new file mode 100644 index 000000000000..eb2b2a343d51 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/simmilar-templates/templates/deployment.yaml @@ -0,0 +1,21 @@ +apiVersion: apps/v1 +kind: Deployment +spec: + replicas: 3 + selector: + matchLabels: + app: myapp + template: + metadata: + labels: + app: myapp + spec: + containers: + - name: myapp + image: test:latest + resources: + limits: + memory: "128Mi" + cpu: "500m" + ports: + - containerPort: 80 diff --git a/pkg/iac/scanners/helm/test/testdata/simmilar-templates/templates/manifest.yaml b/pkg/iac/scanners/helm/test/testdata/simmilar-templates/templates/manifest.yaml new file mode 100644 index 000000000000..49d9df010f82 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/simmilar-templates/templates/manifest.yaml @@ -0,0 +1,2 @@ +apiVersion: apps/v1 +kind: Deployment diff --git a/pkg/iac/scanners/helm/test/testdata/templated-name/Chart.yaml b/pkg/iac/scanners/helm/test/testdata/templated-name/Chart.yaml new file mode 100644 index 000000000000..e675643d99c9 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/templated-name/Chart.yaml @@ -0,0 +1,7 @@ +apiVersion: v2 +name: {{COMPONENT_NAME}} +version: {{COMPONENT_VERSION}} +description: A Helm chart for Kubernetes +keywords: + - kublr + - audit diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/.helmignore b/pkg/iac/scanners/helm/test/testdata/testchart/.helmignore new file mode 100644 index 000000000000..0e8a0eb36f4c --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/Chart.yaml b/pkg/iac/scanners/helm/test/testdata/testchart/Chart.yaml new file mode 100644 index 000000000000..0ffb7d074a72 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: testchart +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/templates/NOTES.txt b/pkg/iac/scanners/helm/test/testdata/testchart/templates/NOTES.txt new file mode 100644 index 000000000000..45e51670a862 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "testchart.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "testchart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "testchart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "testchart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/templates/_helpers.tpl b/pkg/iac/scanners/helm/test/testdata/testchart/templates/_helpers.tpl new file mode 100644 index 000000000000..4b0db05bf5f5 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "testchart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "testchart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "testchart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "testchart.labels" -}} +helm.sh/chart: {{ include "testchart.chart" . }} +{{ include "testchart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "testchart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "testchart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "testchart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "testchart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/templates/deployment.yaml b/pkg/iac/scanners/helm/test/testdata/testchart/templates/deployment.yaml new file mode 100644 index 000000000000..4f81b4f85eb9 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/templates/deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "testchart.fullname" . }} + labels: + {{- include "testchart.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "testchart.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "testchart.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "testchart.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + # trivy:ignore:KSV018 + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/templates/hpa.yaml b/pkg/iac/scanners/helm/test/testdata/testchart/templates/hpa.yaml new file mode 100644 index 000000000000..51734471d41d --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "testchart.fullname" . }} + labels: + {{- include "testchart.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "testchart.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/templates/ingress.yaml b/pkg/iac/scanners/helm/test/testdata/testchart/templates/ingress.yaml new file mode 100644 index 000000000000..9732d2a24a14 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "testchart.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "testchart.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/templates/service.yaml b/pkg/iac/scanners/helm/test/testdata/testchart/templates/service.yaml new file mode 100644 index 000000000000..86baf148215d --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "testchart.fullname" . }} + labels: + {{- include "testchart.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "testchart.selectorLabels" . | nindent 4 }} diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/templates/serviceaccount.yaml b/pkg/iac/scanners/helm/test/testdata/testchart/templates/serviceaccount.yaml new file mode 100644 index 000000000000..f728deb2a6bb --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "testchart.serviceAccountName" . }} + labels: + {{- include "testchart.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/templates/tests/test-connection.yaml b/pkg/iac/scanners/helm/test/testdata/testchart/templates/tests/test-connection.yaml new file mode 100644 index 000000000000..a391ef1c462f --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "testchart.fullname" . }}-test-connection" + labels: + {{- include "testchart.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "testchart.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/pkg/iac/scanners/helm/test/testdata/testchart/values.yaml b/pkg/iac/scanners/helm/test/testdata/testchart/values.yaml new file mode 100644 index 000000000000..4acdf3c931bd --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/testchart/values.yaml @@ -0,0 +1,86 @@ +# Default values for testchart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: + {} + # fsGroup: 2000 + +securityContext: + {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: + {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: + {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/pkg/iac/scanners/helm/test/testdata/with-api-version/.helmignore b/pkg/iac/scanners/helm/test/testdata/with-api-version/.helmignore new file mode 100644 index 000000000000..0e8a0eb36f4c --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-api-version/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/iac/scanners/helm/test/testdata/with-api-version/Chart.yaml b/pkg/iac/scanners/helm/test/testdata/with-api-version/Chart.yaml new file mode 100644 index 000000000000..22dab35d32f4 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-api-version/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: with-api-version +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/pkg/iac/scanners/helm/test/testdata/with-api-version/templates/_helpers.tpl b/pkg/iac/scanners/helm/test/testdata/with-api-version/templates/_helpers.tpl new file mode 100644 index 000000000000..cab726131dc5 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-api-version/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "with-api-version.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "with-api-version.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "with-api-version.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "with-api-version.labels" -}} +helm.sh/chart: {{ include "with-api-version.chart" . }} +{{ include "with-api-version.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "with-api-version.selectorLabels" -}} +app.kubernetes.io/name: {{ include "with-api-version.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "with-api-version.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "with-api-version.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/with-api-version/templates/pdb.yaml b/pkg/iac/scanners/helm/test/testdata/with-api-version/templates/pdb.yaml new file mode 100644 index 000000000000..a0a54cbc232b --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-api-version/templates/pdb.yaml @@ -0,0 +1,11 @@ +apiVersion: {{ $.Capabilities.APIVersions.Has "policy/v1/PodDisruptionBudget" | ternary "policy/v1" "policy/v1beta1" }} +kind: PodDisruptionBudget +metadata: + name: {{ include "with-api-version.fullname" . }} + labels: + {{- include "with-api-version.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "with-api-version.selectorLabels" . | nindent 6 }} + maxUnavailable: 0 diff --git a/pkg/iac/scanners/helm/test/testdata/with-api-version/values.yaml b/pkg/iac/scanners/helm/test/testdata/with-api-version/values.yaml new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/iac/scanners/helm/test/testdata/with-kube-version/.helmignore b/pkg/iac/scanners/helm/test/testdata/with-kube-version/.helmignore new file mode 100644 index 000000000000..0e8a0eb36f4c --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-kube-version/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/iac/scanners/helm/test/testdata/with-kube-version/Chart.yaml b/pkg/iac/scanners/helm/test/testdata/with-kube-version/Chart.yaml new file mode 100644 index 000000000000..99c44c125940 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-kube-version/Chart.yaml @@ -0,0 +1,26 @@ +apiVersion: v2 +name: with-api-version +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" + +kubeVersion: ">=1.60.0-0" diff --git a/pkg/iac/scanners/helm/test/testdata/with-kube-version/templates/_helpers.tpl b/pkg/iac/scanners/helm/test/testdata/with-kube-version/templates/_helpers.tpl new file mode 100644 index 000000000000..cab726131dc5 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-kube-version/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "with-api-version.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "with-api-version.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "with-api-version.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "with-api-version.labels" -}} +helm.sh/chart: {{ include "with-api-version.chart" . }} +{{ include "with-api-version.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "with-api-version.selectorLabels" -}} +app.kubernetes.io/name: {{ include "with-api-version.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "with-api-version.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "with-api-version.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/with-kube-version/templates/pdb.yaml b/pkg/iac/scanners/helm/test/testdata/with-kube-version/templates/pdb.yaml new file mode 100644 index 000000000000..0c063e06df97 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-kube-version/templates/pdb.yaml @@ -0,0 +1,11 @@ +apiVersion: policy/v1 +kind: PodDisruptionBudget +metadata: + name: {{ include "with-api-version.fullname" . }} + labels: + {{- include "with-api-version.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "with-api-version.selectorLabels" . | nindent 6 }} + maxUnavailable: 0 diff --git a/pkg/iac/scanners/helm/test/testdata/with-kube-version/values.yaml b/pkg/iac/scanners/helm/test/testdata/with-kube-version/values.yaml new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/iac/scanners/helm/test/testdata/with-subchart/Chart.yaml b/pkg/iac/scanners/helm/test/testdata/with-subchart/Chart.yaml new file mode 100644 index 000000000000..3c8c9b71ae45 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-subchart/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: test +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" diff --git a/pkg/iac/scanners/helm/test/testdata/with-subchart/charts/nginx/Chart.yaml b/pkg/iac/scanners/helm/test/testdata/with-subchart/charts/nginx/Chart.yaml new file mode 100644 index 000000000000..45cdc636218e --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-subchart/charts/nginx/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: nginx +description: A Helm chart for Kubernetes +type: application +version: 0.1.0 +appVersion: "1.16.0" diff --git a/pkg/iac/scanners/helm/test/testdata/with-subchart/charts/nginx/templates/pod.yaml b/pkg/iac/scanners/helm/test/testdata/with-subchart/charts/nginx/templates/pod.yaml new file mode 100644 index 000000000000..70b3a84a8130 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-subchart/charts/nginx/templates/pod.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: nginx +spec: + containers: + - name: nginx + image: nginx:1.14.2 + ports: + - containerPort: 8080 + securityContext: + readOnlyRootFilesystem: {{ .Values.readOnlyFs }} diff --git a/pkg/iac/scanners/helm/test/testdata/with-subchart/charts/nginx/values.yaml b/pkg/iac/scanners/helm/test/testdata/with-subchart/charts/nginx/values.yaml new file mode 100644 index 000000000000..ff3cff9db1e3 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-subchart/charts/nginx/values.yaml @@ -0,0 +1 @@ +readOnlyFs: false diff --git a/pkg/iac/scanners/helm/test/testdata/with-subchart/values.yaml b/pkg/iac/scanners/helm/test/testdata/with-subchart/values.yaml new file mode 100644 index 000000000000..1e51a8fed1da --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-subchart/values.yaml @@ -0,0 +1,2 @@ +nginx: + readOnlyFs: true diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/.helmignore b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/.helmignore new file mode 100644 index 000000000000..50af03172541 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/.helmignore @@ -0,0 +1,22 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/Chart.yaml b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/Chart.yaml new file mode 100644 index 000000000000..bd163a944cae --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: with-tarred-dep +description: Test With Tarred Dependencies +type: application +version: 0.1.1 +appVersion: "1.0" +sources: + - https://github.com/test/with-tarred-dep +dependencies: + - name: common + repository: https://charts.bitnami.com/bitnami + tags: + - bitnami-common + version: 1.16.1 diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/LICENSE b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/LICENSE new file mode 100644 index 000000000000..261eeb9e9f8b --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/charts/common-1.16.1.tgz b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/charts/common-1.16.1.tgz new file mode 100644 index 000000000000..6a2df2e15b93 Binary files /dev/null and b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/charts/common-1.16.1.tgz differ diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/renovate.json b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/renovate.json new file mode 100644 index 000000000000..a78e667b7736 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] + } \ No newline at end of file diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/.gitkeep b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/deployment.yaml b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/deployment.yaml new file mode 100644 index 000000000000..003d08eb745d --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ template "common.names.fullname" . }} + labels: {{- include "common.labels.standard" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: {{- include "common.labels.matchLabels" . | nindent 6 }} + template: + metadata: + labels: {{- include "common.labels.standard" . | nindent 8 }} + spec: + containers: + - name: metadata-service + env: + - name: METADATASERVICE_UPSTREAM_API_URL + value: '{{ .Values.upstreamAPI }}' + - name: METADATASERVICE_OIDC_AUDIENCE + value: "{{ .Values.oidc.audience }}" + - name: METADATASERVICE_OIDC_ISSUER + value: "{{ .Values.oidc.issuer }}" + - name: METADATASERVICE_OIDC_JWKSURI + value: "{{ .Values.oidc.jwksuri }}" + - name: METADATASERVICE_OIDC_CLAIMS_ROLES + value: "{{ .Values.oidc.rolesClaim }}" + - name: METADATASERVICE_OIDC_CLAIMS_USERNAME + value: "{{ .Values.oidc.userClaim }}" + - name: METADATASERVICE_DB_URI + valueFrom: + secretKeyRef: + name: {{ template "common.names.fullname" . }}-dbconn + key: uri + image: "{{ .Values.metadataservice.image.repository }}:{{ .Values.metadataservice.image.tag }}" + imagePullPolicy: Always + volumeMounts: + - name: dbcerts + mountPath: "/dbcerts" + readOnly: true + ports: + - name: http + containerPort: 8000 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz/liveness + port: http + initialDelaySeconds: 5 + timeoutSeconds: 2 + readinessProbe: + httpGet: + path: /healthz/readiness + port: http + initialDelaySeconds: 5 + timeoutSeconds: 2 + resources: +{{ toYaml .Values.resources | indent 12 }} + volumes: + - name: dbcerts + secret: + secretName: {{ template "common.names.fullname" . }}-crdb-ca + defaultMode: 0400 diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/ingress.yaml b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/ingress.yaml new file mode 100644 index 000000000000..45cd321ca9a9 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/ingress.yaml @@ -0,0 +1,36 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ template "common.names.fullname" . }} + labels: {{- include "common.labels.standard" . | nindent 4 }} +spec: + {{- if and .Values.ingress.ingressClassName (eq "true" (include "common.ingress.supportsIngressClassname" .)) }} + ingressClassName: {{ .Values.ingress.ingressClassName | quote }} + {{- end }} + rules: + {{- range .Values.ingress.hostnames }} + - host: {{ . }} + http: + paths: + - path: / + {{- if $.Values.ingress.publicPaths -}} + ( + {{- range $index,$path := $.Values.ingress.publicPaths }} + {{- if $index }}|{{ end }} + {{- $path }} + {{- end -}} + ) + {{- end }} + pathType: Prefix + backend: + service: + name: {{ template "common.names.fullname" $ }} + port: + name: http + {{- end }} +# tls: [] +# hosts: +# - hollow-metadataservice.mydomain +# secretName: hollow-metadataservice-example-tls +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/secrets-crdb-ca.yaml b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/secrets-crdb-ca.yaml new file mode 100644 index 000000000000..18c39c058dcd --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/secrets-crdb-ca.yaml @@ -0,0 +1,17 @@ +{{- if .Values.crdbCA }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "common.names.fullname" . }}-crdb-ca + namespace: {{ .Release.Namespace | quote }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +type: Opaque +data: + ca.crt: {{ .Values.crdbCA | b64enc | quote }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/secrets-dbconn.yaml b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/secrets-dbconn.yaml new file mode 100644 index 000000000000..06c93061d08c --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/secrets-dbconn.yaml @@ -0,0 +1,17 @@ +{{- if .Values.dbconnURI }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ template "common.names.fullname" . }}-dbconn + namespace: {{ .Release.Namespace | quote }} + labels: {{- include "common.labels.standard" . | nindent 4 }} + {{- if .Values.commonLabels }} + {{- include "common.tplvalues.render" ( dict "value" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- end }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +type: Opaque +data: + uri: {{ .Values.dbconnURI | b64enc | quote }} +{{- end }} diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/service.yaml b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/service.yaml new file mode 100644 index 000000000000..fdb8b82d76f8 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/templates/service.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "common.names.fullname" . }} + labels: {{- include "common.labels.standard" . | nindent 4 }} +spec: + ports: + - name: http + port: 80 + protocol: TCP + targetPort: 8000 + - name: https + port: 443 + protocol: TCP + targetPort: 8000 + selector:{{ include "common.labels.matchLabels" . | nindent 4 }} + type: ClusterIP diff --git a/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/values.yaml b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/values.yaml new file mode 100644 index 000000000000..7a86583f54e3 --- /dev/null +++ b/pkg/iac/scanners/helm/test/testdata/with-tarred-dep/values.yaml @@ -0,0 +1,30 @@ +metadataservice: + image: + repository: ghcr.io/metal-toolbox/hollow-metadataservice + tag: "v0.0.1" + +ingress: + enabled: true + hostnames: + - metadata-service.mydomain + publicPaths: + - $ + - metadata + - userdata + - '2009-04-04' + +oidc: + audience: "" + issuer: "" + jwksuri: "" + rolesClaim: "" + userClaim: "" + +replicaCount: 1 +resources: + limits: + cpu: 4 + memory: 4Gi + requests: + cpu: 4 + memory: 4Gi diff --git a/pkg/iac/scanners/helm/test/values/values.yaml b/pkg/iac/scanners/helm/test/values/values.yaml new file mode 100644 index 000000000000..6f637160ffa9 --- /dev/null +++ b/pkg/iac/scanners/helm/test/values/values.yaml @@ -0,0 +1,3 @@ +--- +securityContext: + runAsUser: 0 \ No newline at end of file diff --git a/pkg/iac/scanners/kubernetes/parser/manifest.go b/pkg/iac/scanners/kubernetes/parser/manifest.go new file mode 100644 index 000000000000..04e44e5edae3 --- /dev/null +++ b/pkg/iac/scanners/kubernetes/parser/manifest.go @@ -0,0 +1,64 @@ +package parser + +import ( + "fmt" + + "github.com/samber/lo" + "gopkg.in/yaml.v3" + + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type Manifest struct { + FilePath string + Content *ManifestNode +} + +func NewManifest(path string, root *ManifestNode) (*Manifest, error) { + if root.Type != TagMap { + return &Manifest{FilePath: path}, nil + } + + root.Walk(func(n *ManifestNode) { + n.FilePath = path + switch v := n.Value.(type) { + case []*ManifestNode: + n.Value = lo.Filter(v, func(vv *ManifestNode, _ int) bool { + return vv != nil + }) + case map[string]*ManifestNode: + n.Value = lo.OmitBy(v, func(_ string, vv *ManifestNode) bool { + return vv == nil + }) + } + }) + return &Manifest{ + FilePath: path, + Content: root, + }, nil +} + +func (m *Manifest) ToRego() any { + if m.Content == nil { + return nil + } + return m.Content.ToRego() +} + +func ManifestFromJSON(path string, data []byte) (*Manifest, error) { + var root = &ManifestNode{} + if err := xjson.Unmarshal(data, root); err != nil { + return nil, fmt.Errorf("unmarshal json: %w", err) + } + + return NewManifest(path, root) +} + +func ManifestFromYAML(path string, data []byte) (*Manifest, error) { + var root = &ManifestNode{} + if err := yaml.Unmarshal(data, root); err != nil { + return nil, fmt.Errorf("unmarshal yaml: %w", err) + } + + return NewManifest(path, root) +} diff --git a/pkg/iac/scanners/kubernetes/parser/manifest_node.go b/pkg/iac/scanners/kubernetes/parser/manifest_node.go new file mode 100644 index 000000000000..9918d981922f --- /dev/null +++ b/pkg/iac/scanners/kubernetes/parser/manifest_node.go @@ -0,0 +1,246 @@ +package parser + +import ( + "encoding/base64" + "encoding/json/jsontext" + "encoding/json/v2" + "fmt" + "reflect" + "strconv" + "time" + + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/log" + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +type TagType string + +const ( + TagBool TagType = "!!bool" + TagInt TagType = "!!int" + TagFloat TagType = "!!float" + TagStr TagType = "!!str" + TagString TagType = "!!string" + TagSlice TagType = "!!seq" + TagMap TagType = "!!map" + TagTimestamp TagType = "!!timestamp" + TagBinary TagType = "!!binary" +) + +type ManifestNode struct { + xjson.Location + Offset int + Value any + Type TagType + FilePath string +} + +func (n *ManifestNode) ToRego() any { + if n == nil { + return nil + } + switch n.Type { + case TagBool, TagInt, TagFloat, TagString, TagStr, TagBinary: + return n.Value + case TagTimestamp: + t, ok := n.Value.(time.Time) + if !ok { + return nil + } + return t.Format(time.RFC3339) + case TagSlice: + var output []any + for _, node := range n.Value.([]*ManifestNode) { + output = append(output, node.ToRego()) + } + return output + case TagMap: + output := map[string]any{ + "__defsec_metadata": n.metadata(), + } + for key, node := range n.Value.(map[string]*ManifestNode) { + output[key] = node.ToRego() + } + return output + } + return nil +} + +func (n *ManifestNode) metadata() map[string]any { + return map[string]any{ + "startline": n.StartLine, + "endline": n.EndLine, + "filepath": n.FilePath, + "offset": n.Offset, + } +} + +func (n *ManifestNode) UnmarshalYAML(node *yaml.Node) error { + n.StartLine = node.Line + n.EndLine = node.Line + n.Type = TagType(node.Tag) + + switch TagType(node.Tag) { + case TagString, TagStr: + n.Value = node.Value + case TagInt: + val, err := strconv.Atoi(node.Value) + if err != nil { + return fmt.Errorf("failed to parse int: %w", err) + } + n.Value = val + case TagFloat: + val, err := strconv.ParseFloat(node.Value, 64) + if err != nil { + return fmt.Errorf("failed to parse float: %w", err) + } + n.Value = val + case TagBool: + val, err := strconv.ParseBool(node.Value) + if err != nil { + return fmt.Errorf("failed to parse bool: %w", err) + } + n.Value = val + case TagTimestamp: + var val time.Time + if err := node.Decode(&val); err != nil { + return fmt.Errorf("failed to decode timestamp: %w", err) + } + n.Value = val + case TagBinary: + val, err := base64.StdEncoding.DecodeString(node.Value) + if err != nil { + return fmt.Errorf("failed to decode binary data: %w", err) + } + n.Value = val + case TagMap: + return n.handleMapTag(node) + case TagSlice: + return n.handleSliceTag(node) + default: + log.WithPrefix("k8s").Debug("Skipping unsupported node tag", + log.String("tag", node.Tag), + log.FilePath(n.FilePath), + log.Int("line", node.Line), + ) + } + return nil +} + +func (n *ManifestNode) handleSliceTag(node *yaml.Node) error { + nodes := make([]*ManifestNode, 0, len(node.Content)) + maxLine := node.Line + for _, contentNode := range node.Content { + newNode, err := newManifestNodeFromYaml(n.FilePath, contentNode) + if err != nil { + return err + } + maxLine = max(maxLine, newNode.EndLine) + nodes = append(nodes, newNode) + } + n.EndLine = maxLine + n.Value = nodes + return nil +} + +func (n *ManifestNode) handleMapTag(node *yaml.Node) error { + if len(node.Content)%2 != 0 { + return fmt.Errorf("invalid map node at line %d: uneven number of children", node.Line) + } + + output := make(map[string]*ManifestNode, len(node.Content)/2) + maxLine := node.Line + for i := 0; i < len(node.Content); i += 2 { + keyNode := node.Content[i] + valueNode := node.Content[i+1] + + newNode, err := newManifestNodeFromYaml(n.FilePath, valueNode) + if err != nil { + return err + } + + if newNode.Type == TagMap { + // Set StartLine to the key's line so that the map node snippet + // correctly starts at the key in the YAML file + newNode.StartLine = keyNode.Line + } + output[keyNode.Value] = newNode + + maxLine = max(maxLine, newNode.EndLine) + } + n.EndLine = maxLine + n.Value = output + return nil +} + +func newManifestNodeFromYaml(filePath string, yamlNode *yaml.Node) (*ManifestNode, error) { + newNode := &ManifestNode{ + FilePath: filePath, + } + + if yamlNode.Tag == "!!null" { + // UnmarshalYAML is not called for null nodes + newNode.StartLine = yamlNode.Line + newNode.EndLine = yamlNode.Line + return newNode, nil + } + + if err := yamlNode.Decode(newNode); err != nil { + return nil, err + } + + return newNode, nil +} + +func (n *ManifestNode) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + var valPtr any + var nodeType TagType + switch k := dec.PeekKind(); k { + case 't', 'f': + valPtr = new(bool) + nodeType = TagBool + case '"': + nodeType = TagStr + valPtr = new(string) + case '0': + nodeType = TagInt + valPtr = new(uint64) + case '[': + valPtr = new([]*ManifestNode) + nodeType = TagSlice + case '{': + valPtr = new(map[string]*ManifestNode) + nodeType = TagMap + case 'n': + // TODO: UnmarshalJSONFrom is called only for the root null + return dec.SkipValue() + case 0: + return dec.SkipValue() + default: + return fmt.Errorf("unexpected token kind %q at %d", k.String(), dec.InputOffset()) + } + + if err := json.UnmarshalDecode(dec, valPtr); err != nil { + return err + } + + n.Value = reflect.ValueOf(valPtr).Elem().Interface() + n.Type = nodeType + return nil +} + +func (n *ManifestNode) Walk(f func(n *ManifestNode)) { + f(n) + switch n.Type { + case TagSlice: + for _, node := range n.Value.([]*ManifestNode) { + node.Walk(f) + } + case TagMap: + for _, node := range n.Value.(map[string]*ManifestNode) { + node.Walk(f) + } + } +} diff --git a/pkg/iac/scanners/kubernetes/parser/manifest_test.go b/pkg/iac/scanners/kubernetes/parser/manifest_test.go new file mode 100644 index 000000000000..1756d281fabd --- /dev/null +++ b/pkg/iac/scanners/kubernetes/parser/manifest_test.go @@ -0,0 +1,213 @@ +package parser_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes/parser" +) + +func TestJsonManifestToRego(t *testing.T) { + content := `{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "hello-cpu-limit", + "foo": null + }, + "spec": { + "containers": [ + { + "command": [ + "sh", + "-c", + "echo 'Hello' && sleep 1h", + null + ], + "image": "busybox", + "name": "hello" + } + ] + } +}` + + const filePath = "pod.json" + manifest, err := parser.ManifestFromJSON(filePath, []byte(content)) + require.NoError(t, err) + + expected := map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 1, + "endline": 22, + }, + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 4, + "endline": 7, + }, + "name": "hello-cpu-limit", + }, + "spec": map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 8, + "endline": 21, + }, + "containers": []any{ + map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 10, + "endline": 19, + }, + "command": []any{ + "sh", + "-c", + "echo 'Hello' && sleep 1h", + }, + "image": "busybox", + "name": "hello", + }, + }, + }, + } + assert.Equal(t, expected, manifest.ToRego()) +} + +func TestYamlManifestToRego(t *testing.T) { + content := `apiVersion: v1 +kind: Pod +metadata: + name: hello-cpu-limit + foo: null +spec: + containers: + - command: + - sh + - -c + - echo 'Hello' && sleep 1h + - null + image: busybox + name: hello +` + + const filePath = "pod.yaml" + manifest, err := parser.ManifestFromYAML(filePath, []byte(content)) + require.NoError(t, err) + + expected := map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 1, + "endline": 14, + }, + "apiVersion": "v1", + "kind": "Pod", + "metadata": map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 3, + "endline": 5, + }, + "name": "hello-cpu-limit", + "foo": nil, // YAML null preserved + }, + "spec": map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 6, + "endline": 14, + }, + "containers": []any{ + map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 8, + "endline": 14, + }, + "command": []any{ + "sh", + "-c", + "echo 'Hello' && sleep 1h", + nil, // YAML null preserved here too + }, + "image": "busybox", + "name": "hello", + }, + }, + }, + } + assert.Equal(t, expected, manifest.ToRego()) +} + +func TestManifestToRego(t *testing.T) { + const filePath = "pod.json" + tests := []struct { + name string + src string + expected any + }{ + { + name: "timestamp tag", + src: `field: !!timestamp 2024-04-01`, + expected: map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 1, + "endline": 1, + }, + "field": "2024-04-01T00:00:00Z", + }, + }, + { + name: "binary tag", + src: `field: !!binary dGVzdA==`, + expected: map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 1, + "endline": 1, + }, + "field": []uint8{0x74, 0x65, 0x73, 0x74}, + }, + }, + { + name: "float tag", + src: `field: 1.1`, + expected: map[string]any{ + "__defsec_metadata": map[string]any{ + "filepath": filePath, + "offset": 0, + "startline": 1, + "endline": 1, + }, + "field": 1.1, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manifest, err := parser.ManifestFromYAML(filePath, []byte(tt.src)) + require.NoError(t, err) + data := manifest.ToRego() + assert.Equal(t, tt.expected, data) + }) + } +} diff --git a/pkg/iac/scanners/kubernetes/parser/parser.go b/pkg/iac/scanners/kubernetes/parser/parser.go new file mode 100644 index 000000000000..b70a667eeb0c --- /dev/null +++ b/pkg/iac/scanners/kubernetes/parser/parser.go @@ -0,0 +1,59 @@ +package parser + +import ( + "bytes" + "context" + "io" + "regexp" + "strings" +) + +func Parse(_ context.Context, r io.Reader, path string) ([]*Manifest, error) { + contents, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + if len(contents) == 0 { + return nil, nil + } + + if bytes.TrimSpace(contents)[0] == '{' { + manifest, err := ManifestFromJSON(path, contents) + if err != nil { + return nil, err + } + return []*Manifest{manifest}, nil + } + + var manifests []*Manifest + + re := regexp.MustCompile(`(?m:^---\r?\n)`) + offset := 0 + for _, partial := range re.Split(string(contents), -1) { + manifest, err := ManifestFromYAML(path, []byte(partial)) + if err != nil { + return nil, err + } + if manifest.Content != nil { + manifest.Content.Offset = offset + manifests = append(manifests, manifest) + } + + offset += countLines(partial) + } + + return manifests, nil +} + +func countLines(s string) int { + if s == "" { + return 1 + } + + count := strings.Count(s, "\n") + if s[len(s)-1] != '\n' { + count++ + } + return count +} diff --git a/pkg/iac/scanners/kubernetes/parser/parser_test.go b/pkg/iac/scanners/kubernetes/parser/parser_test.go new file mode 100644 index 000000000000..781264c124c2 --- /dev/null +++ b/pkg/iac/scanners/kubernetes/parser/parser_test.go @@ -0,0 +1,74 @@ +package parser_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes/parser" +) + +func TestParse(t *testing.T) { + const filePath = "test.yaml" + + tests := []struct { + name string + src string + expectedOffsets []int + }{ + { + name: "empty file", + src: "", + expectedOffsets: nil, + }, + { + name: "single YAML without separator", + src: ` +apiVersion: v1 +kind: Pod +`, + expectedOffsets: []int{0}, + }, + { + name: "multiple YAML documents", + src: `--- +apiVersion: v1 +kind: Pod +--- +apiVersion: v1 +kind: Service +`, + expectedOffsets: []int{1, 3}, + }, + { + name: "YAML with multiple empty blocks", + src: `--- + +--- +--- +apiVersion: v1 +kind: Pod +`, + expectedOffsets: []int{3}, + }, + { + name: "Windows line endings", + src: "---\r\napiVersion: v1\r\nkind: Pod\r\n", + expectedOffsets: []int{1}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + manifests, err := parser.Parse(t.Context(), strings.NewReader(tt.src), filePath) + require.NoError(t, err) + require.Len(t, manifests, len(tt.expectedOffsets)) + + for i, manifest := range manifests { + require.NotNil(t, manifest.Content) + require.Equal(t, tt.expectedOffsets[i], manifest.Content.Offset) + } + }) + } +} diff --git a/pkg/iac/scanners/kubernetes/scanner.go b/pkg/iac/scanners/kubernetes/scanner.go new file mode 100644 index 000000000000..30d69268f2a3 --- /dev/null +++ b/pkg/iac/scanners/kubernetes/scanner.go @@ -0,0 +1,20 @@ +package kubernetes + +import ( + "context" + "io" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/generic" + "github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes/parser" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func NewScanner(opts ...options.ScannerOption) *generic.GenericScanner[*parser.Manifest] { + p := generic.ParseFunc[*parser.Manifest](parse) + return generic.NewScanner("Kubernetes", types.SourceKubernetes, p, opts...) +} + +func parse(ctx context.Context, r io.Reader, path string) ([]*parser.Manifest, error) { + return parser.Parse(ctx, r, path) +} diff --git a/pkg/iac/scanners/kubernetes/scanner_test.go b/pkg/iac/scanners/kubernetes/scanner_test.go new file mode 100644 index 000000000000..6a65e3ee1966 --- /dev/null +++ b/pkg/iac/scanners/kubernetes/scanner_test.go @@ -0,0 +1,309 @@ +package kubernetes_test + +import ( + "io/fs" + "strings" + "testing" + "testing/fstest" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes" +) + +func Test_ScanYAML(t *testing.T) { + file := ` +apiVersion: v1 +kind: Pod +metadata: + name: hello-cpu-limit +spec: + containers: + - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] + image: busybox + name: hello +` + fsys := buildFS(map[string]string{ + "code/example.yaml": file, + "checks/rule.rego": `# METADATA +# title: test check +# custom: +# id: KSV011 +# severity: LOW +# input: +# selector: +# - type: kubernetes +package builtin.kubernetes.KSV011 + +import data.lib.kubernetes + +deny[res] { + container := kubernetes.containers[_] + res := result.new("fail", container) +} +`, + }) + + scanner := kubernetes.NewScanner( + rego.WithPolicyFilesystem(fsys), + rego.WithPolicyDirs("checks"), + rego.WithEmbeddedLibraries(true), + ) + + results, err := scanner.ScanFS(t.Context(), fsys, "code") + require.NoError(t, err) + + failed := results.GetFailed() + require.Len(t, failed, 1) + + assert.Equal(t, "KSV011", failed[0].Rule().ID) + assertLines(t, file, failed) +} + +func Test_ScanJSON(t *testing.T) { + + file := ` +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "name": "hello-cpu-limit" + }, + "spec": { + "containers": [ + { + "command": [ + "sh", + "-c", + "echo 'Hello' && sleep 1h" + ], + "image": "busybox", + "name": "hello" + } + ] + } +} +` + + fsys := buildFS(map[string]string{ + "code/example.json": file, + "checks/rule.rego": `# METADATA +# title: test check +# custom: +# id: KSV011 +# severity: LOW +# input: +# selector: +# - type: kubernetes +package builtin.kubernetes.KSV011 + +import data.lib.kubernetes + +deny[res] { + container := kubernetes.containers[_] + res := result.new("fail", container) +} +`, + }) + + scanner := kubernetes.NewScanner( + rego.WithPolicyFilesystem(fsys), + rego.WithPolicyDirs("checks"), + rego.WithEmbeddedLibraries(true), + ) + + results, err := scanner.ScanFS(t.Context(), fsys, "code") + require.NoError(t, err) + + require.Len(t, results.GetFailed(), 1) + + failed := results.GetFailed() + require.Len(t, failed, 1) + + assert.Equal(t, "KSV011", failed[0].Rule().ID) + assertLines(t, file, failed) +} + +func Test_YamlWithSeparator(t *testing.T) { + + fsys := buildFS(map[string]string{ + "check.rego": `# METADATA +# title: Custom policy +# description: Custom policy for testing +# scope: package +# schemas: +# - input: schema["input"] + +package defsec + +deny[res] { + input.kind == "Pod" + res := result.new("fail", input) +}`, + "k8s.yaml": ` +--- +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello-cpu-limit +spec: + containers: + - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] + image: busybox + name: hello +`, + }) + + scanner := kubernetes.NewScanner( + rego.WithPolicyFilesystem(fsys), + rego.WithPolicyDirs("."), + rego.WithEmbeddedLibraries(true), + ) + results, err := scanner.ScanFS(t.Context(), fsys, ".") + require.NoError(t, err) + + assert.NotEmpty(t, results.GetFailed()) +} + +func Test_YamlMultiDocument(t *testing.T) { + file := ` +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello1-cpu-limit +spec: + containers: + - command: ["sh", "-c", "echo 'Hello1' && sleep 1h"] + image: busybox + name: hello1 +--- +apiVersion: v1 +kind: Pod +metadata: + name: hello2-cpu-limit +spec: + containers: + - command: ["sh", "-c", "echo 'Hello2' && sleep 1h"] + image: busybox + name: hello2 +` + fsys := buildFS(map[string]string{ + "check.rego": `package defsec + +deny[res] { + input.kind == "Pod" + res := result.new("fail", input) +}`, + "k8s.yaml": file, + }) + + scanner := kubernetes.NewScanner( + rego.WithPolicyFilesystem(fsys), + rego.WithPolicyDirs("."), + rego.WithEmbeddedLibraries(true), + ) + + results, err := scanner.ScanFS(t.Context(), fsys, ".") + require.NoError(t, err) + + assertLines(t, file, results) +} + +func Test_CheckWithSubtype(t *testing.T) { + fsys := buildFS(map[string]string{ + "checks/pod_policy.rego": `# METADATA +# title: test check +# scope: package +# schemas: +# - input: schema["kubernetes"] +# custom: +# id: KSV001 +# severity: MEDIUM +# input: +# selector: +# - type: kubernetes +# subtypes: +# - kind: Pod +package builtin.kubernetes.KSV001 + +import data.lib.kubernetes + +deny[res] { + res := result.new("fail", input) +} +`, + "checks/namespace_policy.rego": `# METADATA +# title: test check 2 +# scope: package +# schemas: +# - input: schema["kubernetes"] +# custom: +# id: KSV002 +# severity: LOW +# input: +# selector: +# - type: kubernetes +# subtypes: +# - kind: Namespace +package builtin.kubernetes.KSV002 + +deny[res] { + res := result.new("fail", input) +} +`, + "test/KSV001/pod.yaml": `apiVersion: v1 +kind: Pod +metadata: + name: hello-cpu-limit +spec: + containers: + - command: ["sh", "-c", "echo 'Hello' && sleep 1h"] + image: busybox + name: hello + securityContext: + capabilities: + drop: + - all +`, + }) + + scanner := kubernetes.NewScanner( + rego.WithEmbeddedLibraries(true), + rego.WithPolicyDirs("checks"), + rego.WithPolicyFilesystem(fsys), + ) + results, err := scanner.ScanFS(t.Context(), fsys, "test/KSV001") + require.NoError(t, err) + + require.NoError(t, err) + require.Len(t, results.GetFailed(), 1) + + failure := results.GetFailed()[0] + + assert.Equal(t, "KSV001", failure.Rule().ID) +} + +func assertLines(t *testing.T, content string, results scan.Results) { + lines := strings.Split(content, "\n") + for _, res := range results { + actualCode, err := res.GetCode() + require.NoError(t, err) + assert.NotEmpty(t, actualCode.Lines) + for _, line := range actualCode.Lines { + assert.Greater(t, len(lines), line.Number) + assert.Equal(t, line.Content, lines[line.Number-1]) + } + } +} + +func buildFS(files map[string]string) fs.FS { + return fstest.MapFS(lo.MapValues(files, func(val string, _ string) *fstest.MapFile { + return &fstest.MapFile{Data: []byte(val)} + })) +} diff --git a/pkg/iac/scanners/options/scanner.go b/pkg/iac/scanners/options/scanner.go new file mode 100644 index 000000000000..95b7903528ae --- /dev/null +++ b/pkg/iac/scanners/options/scanner.go @@ -0,0 +1,17 @@ +package options + +type ConfigurableScanner any + +type ScannerOption func(s ConfigurableScanner) + +type RawConfigScanner interface { + SetScanRawConfig(v bool) +} + +func WithScanRawConfig(v bool) ScannerOption { + return func(s ConfigurableScanner) { + if ss, ok := s.(RawConfigScanner); ok { + ss.SetScanRawConfig(v) + } + } +} diff --git a/pkg/iac/scanners/scanner.go b/pkg/iac/scanners/scanner.go new file mode 100644 index 000000000000..0702726b119a --- /dev/null +++ b/pkg/iac/scanners/scanner.go @@ -0,0 +1,16 @@ +package scanners + +import ( + "context" + "io/fs" + + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +type FSScanner interface { + // Name provides the human-readable name of the scanner e.g. "CloudFormation" + Name() string + // ScanFS scans the given filesystem for issues, starting at the provided directory. + // Use '.' to scan an entire filesystem. + ScanFS(ctx context.Context, fs fs.FS, dir string) (scan.Results, error) +} diff --git a/pkg/iac/scanners/terraform/attribute_test.go b/pkg/iac/scanners/terraform/attribute_test.go new file mode 100644 index 000000000000..69f4a8684f44 --- /dev/null +++ b/pkg/iac/scanners/terraform/attribute_test.go @@ -0,0 +1,376 @@ +package terraform + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func Test_AttributeContains(t *testing.T) { + var tests = []struct { + name string + source string + checkAttribute string + checkValue string + expectedResult bool + ignoreCase bool + }{ + { + name: "bucket name contains Name", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" +}`, + checkAttribute: "bucket_name", + checkValue: "etNa", + expectedResult: true, + }, + { + name: "bucket acl doesn't contain private", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" + acl = "public-read" +}`, + checkAttribute: "acl", + checkValue: "private", + expectedResult: false, + }, + { + name: "tags attribute is a map with a Department key", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" + acl = "public-read" + tags = { + Department = "Finance" + } +}`, + checkAttribute: "tags", + checkValue: "Department", + expectedResult: true, + }, + { + name: "cidr_block has expected subnet", + source: ` +resource "aws_security_group" "my-security_group" { + cidr_block = ["10.0.0.0/16", "172.0.0.0/8" ] +}`, + checkAttribute: "cidr_block", + checkValue: "172.0.0.0/8", + expectedResult: true, + }, + { + name: "autoscaling group has propagated key defined 1st tag is present", + source: ` +resource "aws_autoscaling_group" "my-aws_autoscaling_group" { + tags = [ + { + "key" = "Name" + "propagate_at_launch" = "true" + "value" = "couchbase-seb-develop-dev" + }, + { + "key" = "app" + "propagate_at_launch" = "true" + "value" = "myapp" + } + ] +}`, + checkAttribute: "tags", + checkValue: "Name", + expectedResult: true, + }, + { + name: "autoscaling group has propagated key defined 2nd tag is present", + source: ` +resource "aws_autoscaling_group" "my-aws_autoscaling_group" { + tags = [ + { + "key" = "Name" + "propagate_at_launch" = "true" + "value" = "couchbase-seb-develop-dev" + }, + { + "key" = "app" + "propagate_at_launch" = "true" + "value" = "myapp" + } + ] +}`, + checkAttribute: "tags", + checkValue: "app", + expectedResult: true, + }, + { + name: "autoscaling group has propagated key defined and tag is not present", + source: ` +resource "aws_autoscaling_group" "my-aws_autoscaling_group" { + tags = [ + { + "key" = "Name" + "propagate_at_launch" = "true" + "value" = "couchbase-seb-develop-dev" + }, + { + "key" = "app" + "propagate_at_launch" = "true" + "value" = "myapp" + } + ] +}`, + checkAttribute: "tags", + checkValue: "NotThere", + expectedResult: false, + }, + { + name: "contains array of strings ignores case", + source: ` +resource "aws_security_group" "my-security_group" { + cidr_block = ["Foo", "Bar" ] +}`, + checkAttribute: "cidr_block", + checkValue: "foo", + expectedResult: true, + ignoreCase: true, + }, + { + name: "contains array of strings without ignore case", + source: ` +resource "aws_security_group" "my-security_group" { + cidr_block = ["Foo", "Bar" ] +}`, + checkAttribute: "cidr_block", + checkValue: "foo", + expectedResult: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := createModulesFromSource(t, test.source, ".tf") + for _, module := range modules { + for _, b := range module.GetBlocks() { + attr := b.GetAttribute(test.checkAttribute) + require.NotNil(t, attr) + if test.ignoreCase { + assert.Equal(t, test.expectedResult, attr.Contains(test.checkValue, terraform.IgnoreCase)) + } else { + assert.Equal(t, test.expectedResult, attr.Contains(test.checkValue)) + } + } + } + }) + } +} + +func Test_AttributeIsEmpty(t *testing.T) { + var tests = []struct { + name string + source string + checkAttribute string + checkValue []any + expectedResult bool + }{ + { + name: "bucket acl is not empty", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" + acl = "public-read" +}`, + checkAttribute: "acl", + expectedResult: false, + }, + { + name: "bucket acl is empty", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" + acl = "" +}`, + checkAttribute: "acl", + expectedResult: true, + }, + { + name: "tags is not empty", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" + acl = "public-read" + tags = { + Department = "Finance" + } +}`, + checkAttribute: "tags", + expectedResult: false, + }, + { + name: "tags is empty", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" + acl = "public-read" + tags = { + } +}`, + checkAttribute: "tags", + expectedResult: true, + }, + { + name: "cidr is not empty", + source: ` +resource "aws_security_group_rule" "example" { + type = "ingress" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + security_group_id = "sg-123456" +}`, + checkAttribute: "cidr_blocks", + expectedResult: false, + }, + { + name: "cidr is empty", + source: ` +resource "aws_security_group_rule" "example" { + type = "ingress" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = [] + security_group_id = "sg-123456" +}`, + checkAttribute: "cidr_blocks", + expectedResult: true, + }, + { + name: "from_port_is_not_empty", + source: ` +resource "aws_security_group_rule" "example" { + type = "ingress" + from_port = 0 + to_port = 65535 + protocol = "tcp" + cidr_blocks = [] + security_group_id = "sg-123456" +}`, + checkAttribute: "from_port", + expectedResult: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := createModulesFromSource(t, test.source, ".tf") + for _, module := range modules { + for _, block := range module.GetBlocks() { + attr := block.GetAttribute(test.checkAttribute) + require.NotNil(t, attr) + assert.Equal(t, test.expectedResult, attr.IsEmpty()) + } + } + }) + } +} + +func Test_AttributeIsTrue(t *testing.T) { + var tests = []struct { + name string + source string + checkAttribute string + expectedResult bool + }{ + { + name: "check attribute is true", + source: ` +resource "boolean_something" "my-something" { + value = true +}`, + checkAttribute: "value", + expectedResult: true, + }, + { + name: "check attribute as string is true", + source: ` +resource "boolean_something" "my-something" { + value = "true" +}`, + checkAttribute: "value", + expectedResult: true, + }, + { + name: "check attribute as string is false", + source: ` +resource "boolean_something" "my-something" { + value = "true" +}`, + checkAttribute: "value", + expectedResult: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := createModulesFromSource(t, test.source, ".tf") + for _, module := range modules { + for _, block := range module.GetBlocks() { + attr := block.GetAttribute(test.checkAttribute) + require.NotNil(t, attr) + assert.Equal(t, test.expectedResult, attr.IsTrue()) + } + } + }) + } +} + +func Test_AttributeIsFalse(t *testing.T) { + var tests = []struct { + name string + source string + checkAttribute string + expectedResult bool + }{ + { + name: "check attribute is false", + source: ` +resource "boolean_something" "my-something" { + value = false +}`, + checkAttribute: "value", + expectedResult: true, + }, + { + name: "check attribute as string is false", + source: ` +resource "boolean_something" "my-something" { + value = "false" +}`, + checkAttribute: "value", + expectedResult: true, + }, + { + name: "check attribute true", + source: ` +resource "boolean_something" "my-something" { + value = "true" +}`, + checkAttribute: "value", + expectedResult: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := createModulesFromSource(t, test.source, ".tf") + for _, module := range modules { + for _, block := range module.GetBlocks() { + attr := block.GetAttribute(test.checkAttribute) + require.NotNil(t, attr) + assert.Equal(t, test.expectedResult, attr.IsFalse()) + } + } + }) + } +} diff --git a/pkg/iac/scanners/terraform/block_test.go b/pkg/iac/scanners/terraform/block_test.go new file mode 100644 index 000000000000..015f4e458d7b --- /dev/null +++ b/pkg/iac/scanners/terraform/block_test.go @@ -0,0 +1,83 @@ +package terraform + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_IsPresentCheckOnBlock(t *testing.T) { + var tests = []struct { + name string + source string + expectedAttribute string + }{ + { + name: "expected attribute is present", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" +}`, + expectedAttribute: "bucket_name", + }, + { + name: "expected acl attribute is present", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" + acl = "public-read" +}`, + expectedAttribute: "acl", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := createModulesFromSource(t, test.source, ".tf") + for _, module := range modules { + for _, block := range module.GetBlocks() { + assert.NotNil(t, block.GetAttribute(test.expectedAttribute)) + } + } + }) + } +} + +func Test_IsNotPresentCheckOnBlock(t *testing.T) { + var tests = []struct { + name string + source string + expectedAttribute string + }{ + { + name: "expected attribute is not present", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" + +}`, + expectedAttribute: "acl", + }, + { + name: "expected acl attribute is not present", + source: ` +resource "aws_s3_bucket" "my-bucket" { + bucket_name = "bucketName" + acl = "public-read" + +}`, + expectedAttribute: "logging", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + modules := createModulesFromSource(t, test.source, ".tf") + for _, module := range modules { + for _, block := range module.GetBlocks() { + assert.Nil(t, block.GetAttribute(test.expectedAttribute)) + } + } + }) + } +} diff --git a/pkg/iac/scanners/terraform/count_test.go b/pkg/iac/scanners/terraform/count_test.go new file mode 100644 index 000000000000..fbedaeed5a97 --- /dev/null +++ b/pkg/iac/scanners/terraform/count_test.go @@ -0,0 +1,165 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/rego" +) + +func Test_ResourcesWithCount(t *testing.T) { + var tests = []struct { + name string + source string + expected int + }{ + { + name: "unspecified count defaults to 1", + source: ` + resource "aws_s3_bucket" "test" {} +`, + expected: 1, + }, + { + name: "count is literal 1", + source: ` + resource "aws_s3_bucket" "test" { + count = 1 + } +`, + expected: 1, + }, + { + name: "count is literal 99", + source: ` + resource "aws_s3_bucket" "test" { + count = 99 + } +`, + expected: 99, + }, + { + name: "count is literal 0", + source: ` + resource "aws_s3_bucket" "test" { + count = 0 + } +`, + expected: 0, + }, + { + name: "count is 0 from variable", + source: ` + variable "count" { + default = 0 + } + resource "aws_s3_bucket" "test" { + count = var.count + } +`, + expected: 0, + }, + { + name: "count is 1 from variable", + source: ` + variable "count" { + default = 1 + } + resource "aws_s3_bucket" "test" { + count = var.count + } +`, + expected: 1, + }, + { + name: "count is 1 from variable without default", + source: ` + variable "count" { + } + resource "aws_s3_bucket" "test" { + count = var.count + } +`, + expected: 1, + }, + { + name: "count is 0 from conditional", + source: ` + variable "enabled" { + default = false + } + resource "aws_s3_bucket" "test" { + count = var.enabled ? 1 : 0 + } +`, + expected: 0, + }, + { + name: "count is 1 from conditional", + source: ` + variable "enabled" { + default = true + } + resource "aws_s3_bucket" "test" { + count = var.enabled ? 1 : 0 + } +`, + expected: 1, + }, + { + name: "issue 962", + source: ` + resource "something" "else" { + count = 2 + ok = true + } + + resource "aws_s3_bucket" "test" { + bucket = something.else[0].ok ? "test" : "" + } +`, + expected: 0, + }, + { + name: "Test use of count.index", + source: ` +resource "aws_s3_bucket" "test" { + count = 1 + bucket = var.things[count.index]["ok"] ? "test" : "" +} + +variable "things" { + description = "A list of maps that creates a number of sg" + type = list(map(string)) + + default = [ + { + ok = true + } + ] +} + `, + expected: 0, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + results := scanHCL(t, test.source, + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + + assert.Len(t, results.GetFailed(), test.expected) + + if test.expected > 0 { + testutil.AssertRuleFound(t, "aws-s3-non-empty-bucket", results, "false negative found") + } else { + testutil.AssertRuleNotFound(t, "aws-s3-non-empty-bucket", results, "false positive found") + } + }) + } +} diff --git a/pkg/iac/scanners/terraform/deterministic_test.go b/pkg/iac/scanners/terraform/deterministic_test.go new file mode 100644 index 000000000000..b5a21fe6eae5 --- /dev/null +++ b/pkg/iac/scanners/terraform/deterministic_test.go @@ -0,0 +1,43 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/rego" +) + +func Test_DeterministicResults(t *testing.T) { + fsys := testutil.CreateFS(map[string]string{ + "first.tf": ` +resource "aws_s3_bucket" "test" { + for_each = other.thing +} + `, + "second.tf": ` +resource "other" "thing" { + for_each = local.list +} + `, + "third.tf": ` +locals { + list = { + a = 1, + b = 2, + } +} + `, + }) + + for range 100 { + results, err := scanFS(fsys, ".", + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + require.NoError(t, err) + require.Len(t, results.GetFailed(), 2) + } +} diff --git a/pkg/iac/scanners/terraform/executor/executor.go b/pkg/iac/scanners/terraform/executor/executor.go new file mode 100644 index 000000000000..0e9c393024e9 --- /dev/null +++ b/pkg/iac/scanners/terraform/executor/executor.go @@ -0,0 +1,252 @@ +package executor + +import ( + "context" + "fmt" + "strconv" + "strings" + + "github.com/hashicorp/hcl/v2/hclwrite" + "github.com/zclconf/go-cty/cty" + + adapter "github.com/aquasecurity/trivy/pkg/iac/adapters/terraform" + "github.com/aquasecurity/trivy/pkg/iac/ignore" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +// Executor scans HCL blocks by running all registered rules against them +type Executor struct { + workspaceName string + logger *log.Logger + resultsFilters []func(scan.Results) scan.Results + regoScanner *rego.Scanner + scanRawConfig bool +} + +// New creates a new Executor +func New(options ...Option) *Executor { + s := &Executor{ + logger: log.WithPrefix("terraform executor"), + } + for _, option := range options { + option(s) + } + return s +} + +func (e *Executor) Execute(ctx context.Context, modules terraform.Modules, basePath string) (scan.Results, error) { + + e.logger.Debug("Adapting modules...") + infra := adapter.Adapt(modules) + e.logger.Debug("Adapted module(s) into state data.", log.Int("count", len(modules))) + + e.logger.Debug("Scan state data") + results, err := e.regoScanner.ScanInput(ctx, types.SourceCloud, rego.Input{ + Contents: infra.ToRego(), + Path: basePath, + }) + if err != nil { + return nil, err + } + + if e.scanRawConfig { + e.logger.Debug("Scan raw Terraform data") + results2, err := e.regoScanner.ScanInput(ctx, types.SourceTerraformRaw, rego.Input{ + Contents: terraform.ExportModules(modules), + Path: basePath, + }) + if err != nil { + e.logger.Error("Failed to scan raw Terraform data", + log.FilePath(basePath), log.Err(err)) + } else { + results = append(results, results2...) + } + } + + e.logger.Debug("Finished applying checks") + + e.logger.Debug("Applying ignores...") + var ignores ignore.Rules + for _, module := range modules { + ignores = append(ignores, module.Ignores()...) + } + + ignorers := map[string]ignore.Ignorer{ + "ws": workspaceIgnorer(e.workspaceName), + "ignore": attributeIgnorer(modules), + } + + // ignore a result based on user input + results.Ignore(ignores, ignorers) + + for _, ignored := range results.GetIgnored() { + e.logger.Info("Ignore finding", + log.String("rule", ignored.Rule().CanonicalID()), + log.String("range", ignored.Range().String()), + ) + } + + results = e.filterResults(results) + + for i, res := range results { + if res.Status() != scan.StatusFailed { + continue + } + + res.WithRenderedCause(e.renderCause(modules, res.Range())) + results[i] = res + } + + return results, nil +} + +func (e *Executor) renderCause(modules terraform.Modules, causeRng types.Range) scan.RenderedCause { + tfBlock := findBlockForCause(modules, causeRng) + if tfBlock == nil { + e.logger.Debug("No matching Terraform block found", log.String("cause_range", causeRng.String())) + return scan.RenderedCause{} + } + + block := hclwrite.NewBlock(tfBlock.Type(), normalizeBlockLables(tfBlock)) + + if !writeBlock(tfBlock, block, causeRng) { + e.logger.Debug("No matching block attribute found", log.String("cause_range", causeRng.String())) + return scan.RenderedCause{} + } + + f := hclwrite.NewEmptyFile() + f.Body().AppendBlock(block) + + cause := string(hclwrite.Format(f.Bytes())) + cause = strings.TrimSuffix(cause, "\n") + return scan.RenderedCause{Raw: cause} +} + +// normalizeBlockLables removes indexes and keys from labels. +func normalizeBlockLables(block *terraform.Block) []string { + labels := block.Labels() + if block.IsExpanded() { + nameLabel := labels[len(labels)-1] + idx := strings.LastIndex(nameLabel, "[") + if idx != -1 { + labels[len(labels)-1] = nameLabel[:idx] + } + } + + return labels +} + +func writeBlock(tfBlock *terraform.Block, block *hclwrite.Block, causeRng types.Range) bool { + var found bool + + for _, attr := range tfBlock.Attributes() { + if !attr.GetMetadata().Range().Covers(causeRng) || attr.IsLiteral() { + continue + } + + value := attr.Value() + if !value.IsWhollyKnown() { + continue + } + + block.Body().SetAttributeValue(attr.Name(), value) + found = true + } + + for _, child := range tfBlock.AllBlocks() { + if child.GetMetadata().Range().Covers(causeRng) { + childBlock := hclwrite.NewBlock(child.Type(), nil) + if writeBlock(child, childBlock, causeRng) { + block.Body().AppendBlock(childBlock) + found = true + } + } + } + + return found +} + +func findBlockForCause(modules terraform.Modules, causeRng types.Range) *terraform.Block { + for _, block := range modules.GetBlocks() { + blockRng := block.GetMetadata().Range() + if blockRng.GetFilename() == causeRng.GetFilename() && blockRng.Includes(causeRng) { + return block + } + } + return nil +} + +func (e *Executor) filterResults(results scan.Results) scan.Results { + if len(e.resultsFilters) > 0 && len(results) > 0 { + before := len(results.GetIgnored()) + e.logger.Debug("Applying results filters...") + for _, filter := range e.resultsFilters { + results = filter(results) + } + e.logger.Debug("Applied results filters.", + log.Int("count", len(results.GetIgnored())-before)) + } + + return results +} + +func ignoreByParams(params map[string]string, modules terraform.Modules, m *types.Metadata) bool { + if len(params) == 0 { + return true + } + block := modules.GetBlockByIgnoreRange(m) + if block == nil { + return true + } + for key, param := range params { + val := block.GetValueByPath(key) + if val.IsNull() || !val.IsKnown() { + return false + } + switch val.Type() { + case cty.String: + if val.AsString() != param { + return false + } + case cty.Number: + bf := val.AsBigFloat() + f64, _ := bf.Float64() + comparableInt := strconv.Itoa(int(f64)) + comparableFloat := fmt.Sprintf("%f", f64) + if param != comparableInt && param != comparableFloat { + return false + } + case cty.Bool: + if strconv.FormatBool(val.True()) != param { + return false + } + default: + return false + } + } + return true +} + +func workspaceIgnorer(ws string) ignore.Ignorer { + return func(_ types.Metadata, param any) bool { + ignoredWorkspace, ok := param.(string) + if !ok { + return false + } + return ignore.MatchPattern(ws, ignoredWorkspace) + } +} + +func attributeIgnorer(modules terraform.Modules) ignore.Ignorer { + return func(resultMeta types.Metadata, param any) bool { + params, ok := param.(map[string]string) + if !ok { + return false + } + return ignoreByParams(params, modules, &resultMeta) + } +} diff --git a/pkg/iac/scanners/terraform/executor/option.go b/pkg/iac/scanners/terraform/executor/option.go new file mode 100644 index 000000000000..4459a6861d00 --- /dev/null +++ b/pkg/iac/scanners/terraform/executor/option.go @@ -0,0 +1,32 @@ +package executor + +import ( + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +type Option func(s *Executor) + +func OptionWithResultsFilter(f func(scan.Results) scan.Results) Option { + return func(s *Executor) { + s.resultsFilters = append(s.resultsFilters, f) + } +} + +func OptionWithWorkspaceName(workspaceName string) Option { + return func(s *Executor) { + s.workspaceName = workspaceName + } +} + +func OptionWithRegoScanner(s *rego.Scanner) Option { + return func(e *Executor) { + e.regoScanner = s + } +} + +func OptionWithScanRawConfig(b bool) Option { + return func(e *Executor) { + e.scanRawConfig = b + } +} diff --git a/pkg/iac/scanners/terraform/fs_test.go b/pkg/iac/scanners/terraform/fs_test.go new file mode 100644 index 000000000000..eedaf1f62480 --- /dev/null +++ b/pkg/iac/scanners/terraform/fs_test.go @@ -0,0 +1,21 @@ +package terraform + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/rego" +) + +func Test_OS_FS(t *testing.T) { + s := New( + rego.WithEmbeddedPolicies(true), + rego.WithEmbeddedLibraries(true), + ) + results, err := s.ScanFS(t.Context(), os.DirFS("testdata"), "fail") + require.NoError(t, err) + assert.NotEmpty(t, results.GetFailed()) +} diff --git a/pkg/iac/scanners/terraform/ignore_test.go b/pkg/iac/scanners/terraform/ignore_test.go new file mode 100644 index 000000000000..09ce9ed08481 --- /dev/null +++ b/pkg/iac/scanners/terraform/ignore_test.go @@ -0,0 +1,660 @@ +package terraform + +import ( + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/rego" +) + +func Test_IgnoreAll(t *testing.T) { + + var testCases = []struct { + name string + source string + assertLength int + }{ + { + name: "inline rule ignore all checks", + source: `resource "aws_s3_bucket" "test" { + bucket = "" // %s:ignore:* +}`, + assertLength: 0, + }, + { + name: "rule above block ignore all checks", + source: `// %s:ignore:* +resource "aws_s3_bucket" "test" {}`, + assertLength: 0, + }, + { + name: "rule above block with boolean parameter", + source: `// %s:ignore:*[object_lock_enabled=false] +resource "aws_s3_bucket" "test" { + object_lock_enabled = false +}`, + assertLength: 0, + }, + { + name: "rule above block with non-matching boolean parameter", + source: `// %s:ignore:*[object_lock_enabled=false] +resource "aws_s3_bucket" "test" { + object_lock_enabled = true +}`, + assertLength: 1, + }, + { + name: "rule above block with string parameter", + source: `// %s:ignore:*[acl=private] +resource "aws_s3_bucket" "test" { + acl = "private" +}`, + assertLength: 0, + }, + { + name: "rule above block with non-matching string parameter", + source: `// %s:ignore:*[acl=private] +resource "aws_s3_bucket" "test" { + acl = "public" +}`, + assertLength: 1, + }, + { + name: "rule above block with int parameter", + source: `// %s:ignore:*[some_int=123] +resource "aws_s3_bucket" "test" { + some_int = 123 +}`, + assertLength: 0, + }, + { + name: "rule above block with non-matching int parameter", + source: `// %s:ignore:*[some_int=456] +resource "aws_s3_bucket" "test" { + some_int = 123 +}`, + assertLength: 1, + }, + { + name: "stacked rules above block", + source: `// %s:ignore:* +// %s:ignore:a +// %s:ignore:b +// %s:ignore:c +// %s:ignore:d +resource "aws_s3_bucket" "test" {} +`, + assertLength: 0, + }, + { + name: "stacked rules above block without a match", + source: `#%s:ignore:* + +#%s:ignore:x +#%s:ignore:a +#%s:ignore:b +#%s:ignore:c +#%s:ignore:d +resource "aws_s3_bucket" "test" {} +`, + assertLength: 1, + }, + { + name: "stacked rules above block without spaces between '#' comments", + source: `#%s:ignore:* +#%s:ignore:a +#%s:ignore:b +#%s:ignore:c +#%s:ignore:d +resource "aws_s3_bucket" "test" {} +`, + assertLength: 0, + }, + { + name: "stacked rules above block without spaces between '//' comments", + source: `//%s:ignore:* +//%s:ignore:a +//%s:ignore:b +//%s:ignore:c +//%s:ignore:d +resource "aws_s3_bucket" "test" {} +`, + assertLength: 0, + }, + { + name: "rule above the finding", + source: `resource "aws_s3_bucket" "test" { + # %s:ignore:aws-s3-non-empty-bucket + bucket = "" +}`, + assertLength: 0, + }, + { + name: "rule with breached expiration date", + source: `resource "aws_s3_bucket" "test" { + bucket = "" # %s:ignore:aws-s3-non-empty-bucket:exp:2000-01-02 +}`, + assertLength: 1, + }, + { + name: "rule with unbreached expiration date", + source: `resource "aws_s3_bucket" "test" { + bucket = "" # %s:ignore:aws-s3-non-empty-bucket:exp:2221-01-02 +}`, + assertLength: 0, + }, + { + name: "rule with invalid expiration date", + source: `resource "aws_s3_bucket" "test" { + bucket = "" # %s:ignore:aws-s3-non-empty-bucket:exp:2221-13-02 +}`, + assertLength: 1, + }, + { + name: "rule above block with unbreached expiration date", + source: `#%s:ignore:aws-s3-non-empty-bucket:exp:2221-01-02 +resource "aws_s3_bucket" "test" {}`, + assertLength: 0, + }, + { + name: "trivy inline rule ignore all checks", + source: `resource "aws_s3_bucket" "test" { + bucket = "" // %s:ignore:* +}`, + assertLength: 0, + }, + { + name: "ignore by nested attribute", + source: `// %s:ignore:*[versioning.enabled=false] +resource "aws_s3_bucket" "test" { + versioning { + enabled = false + } +}`, + assertLength: 0, + }, + { + name: "ignore by nested attribute of another type", + source: `// %s:ignore:*[versioning.enabled=1] +resource "aws_s3_bucket" "test" { + versioning { + enabled = false + } +}`, + assertLength: 1, + }, + { + name: "ignore by non-existent nested attribute", + source: `// %s:ignore:*[versioning.target=foo] +resource "aws_s3_bucket" "test" { + versioning { + enabled = false + } +}`, + assertLength: 1, + }, + { + name: "ignore resource with `for_each` meta-argument", + source: `// %s:ignore:*[acl=public] +resource "aws_s3_bucket" "test" { + for_each = toset(["private", "public"]) + acl = each.value +}`, + assertLength: 1, + }, + { + name: "ignore by dynamic block value", + source: `// %s:ignore:*[versioning.enabled=false] +resource "aws_s3_bucket" "test" { + dynamic "versioning" { + for_each = [{}] + content { + enabled = false + } + } +}`, + assertLength: 0, + }, + { + name: "ignore by each.value", + source: `locals { + acls = toset(["private", "public"]) +} + +// %s:ignore:*[each.value=private] +resource "aws_s3_bucket" "test" { + for_each = local.acls + + acl = each.value +}`, + assertLength: 1, + }, + { + name: "ignore by nested each.value", + source: `locals { + acls = { + private = { + permission = "private" + } + public = { + permission = "public" + } + } +} + +// %s:ignore:*[each.value.permission=private] +resource "aws_s3_bucket" "test" { + for_each = local.acls + + acl = each.value.permission +}`, + assertLength: 1, + }, + { + name: "ignore resource with `count` meta-argument", + source: `// %s:ignore:*[count.index=1] +resource "aws_s3_bucket" "test" { + count = 2 +}`, + assertLength: 1, + }, + { + name: "invalid index when accessing blocks", + source: `// %s:ignore:*[ingress.99.port=9090] +// %s:ignore:*[ingress.-10.port=9090] +resource "aws_s3_bucket" "test" { + dynamic "ingress" { + for_each = [8080, 9090] + content { + port = ingress.value + } + } +}`, + assertLength: 1, + }, + { + name: "ignore by list value", + source: `#%s:ignore:*[someattr.1.Environment=dev] +resource "aws_s3_bucket" "test" { + someattr = [ + { + Environment = "prod" + }, + { + Environment = "dev" + } + ] +}`, + assertLength: 0, + }, + { + name: "ignore by list value with invalid index", + source: `#%s:ignore:*[someattr.-2.Environment=dev] +resource "aws_s3_bucket" "test" { + someattr = [ + { + Environment = "prod" + }, + { + Environment = "dev" + } + ] +}`, + assertLength: 1, + }, + { + name: "ignore by object value", + source: `#%s:ignore:*[tags.Environment=dev] +resource "aws_s3_bucket" "test" { + tags = { + Environment = "dev" + } +}`, + assertLength: 0, + }, + { + name: "ignore by object value in block", + source: `#%s:ignore:*[someblock.tags.Environment=dev] +resource "aws_s3_bucket" "test" { + someblock { + tags = { + Environment = "dev" + } + } +}`, + assertLength: 0, + }, + { + name: "ignore by list value in map", + source: ` +variable "testvar" { + type = map(list(string)) + default = { + server1 = ["web", "dev"] + server2 = ["prod"] + } +} + +#%s:ignore:*[someblock.someattr.server1.1=dev] +resource "aws_s3_bucket" "test" { + someblock { + someattr = var.testvar + } +} +`, + assertLength: 0, + }, + { + name: "ignore by alias", + source: `#%s:ignore:my-alias +resource "aws_s3_bucket" "test" {}`, + assertLength: 0, + }, + { + name: "ignore by alias with trivy prefix", + source: `#%s:ignore:my-alias +resource "aws_s3_bucket" "test" {}`, + assertLength: 0, + }, + { + name: "ignore for implied IAM resource", + source: `# %s:ignore:aws-iam-enforce-mfa +resource "aws_iam_group" "this" { + name = "group-name" +} + +resource "aws_iam_policy" "this" { + name = "test-policy" + policy = data.aws_iam_policy_document.this.json +} + + +resource "aws_iam_group_policy_attachment" "this" { + group = aws_iam_group.this.name + policy_arn = aws_iam_policy.this.arn +} + +data "aws_iam_policy_document" "this" { + statement { + sid = "PublishToCloudWatch" + actions = [ + "cloudwatch:PutMetricData", + ] + resources = ["*"] + } +}`, + assertLength: 0, + }, + { + name: "ignore marker value is unknown", + source: `#trivy:ignore:*[bucket=mybucket-bucket1] +resource "aws_s3_bucket" "test" { + bucket = "mybucket-${each.key}" +}`, + assertLength: 1, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + prefixes := []string{"tfsec", "trivy"} + for _, prefix := range prefixes { + t.Run(prefix, func(t *testing.T) { + results := scanHCL( + t, formatWithSingleValue(tc.source, prefix), + rego.WithPolicyReader( + strings.NewReader(emptyBucketCheck), + strings.NewReader(enforceGroupMfaCheck)), + rego.WithPolicyNamespaces("user"), + ) + assert.Len(t, results.GetFailed(), tc.assertLength) + }) + } + }) + } +} + +func formatWithSingleValue(format string, value any) string { + count := strings.Count(format, "%s") + + args := make([]any, count) + for i := range args { + args[i] = value + } + + return fmt.Sprintf(format, args...) +} + +func Test_IgnoreByDynamicBlockValue(t *testing.T) { + + check := `# METADATA +# custom: +# id: USER-TEST-0124 +# short_code: test +# provider: aws +# service: ec2 +package user.test124 + +import rego.v1 + +deny contains res if { + some group in input.aws.ec2.securitygroups + some rule in group.ingressrules + rule.toport.value < 1024 + res := result.new( + sprintf("Port below 1024 is not allowed, but got %s", [rule.toport.value]), + rule.toport, + ) +} +` + + tests := []struct { + name string + source string + expected int + }{ + { + name: "by dynamic value", + source: `// trivy:ignore:*[ingress.from_port=80] +resource "aws_security_group" "loadbalancer" { + name = "test" + + dynamic "ingress" { + for_each = [80, 443] + content { + from_port = ingress.value + to_port = ingress.value + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + } +} +`, + expected: 0, + }, + { + name: "access by index", + source: `// trivy:ignore:*[ingress.0.from_port=80] +resource "aws_security_group" "loadbalancer" { + name = "test" + + dynamic "ingress" { + for_each = [80, 443] + content { + from_port = ingress.value + to_port = ingress.value + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + } +} +`, + expected: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results := scanHCL(t, tt.source, + rego.WithPolicyReader(strings.NewReader(check)), + rego.WithPolicyNamespaces("user")) + require.Len(t, results.GetFailed(), tt.expected) + }) + } +} + +func Test_IgnoreByWorkspace(t *testing.T) { + + tests := []struct { + name string + src string + expectedFailed int + }{ + { + name: "with expiry and workspace", + src: `# tfsec:ignore:aws-s3-non-empty-bucket:exp:2221-01-02:ws:testworkspace +resource "aws_s3_bucket" "test" {}`, + expectedFailed: 0, + }, + { + name: "bad workspace", + src: `# tfsec:ignore:aws-s3-non-empty-bucket:exp:2221-01-02:ws:otherworkspace +resource "aws_s3_bucket" "test" {}`, + expectedFailed: 1, + }, + { + name: "with expiry and workspace, trivy prefix", + src: `# trivy:ignore:aws-s3-non-empty-bucket:exp:2221-01-02:ws:testworkspace +resource "aws_s3_bucket" "test" {}`, + expectedFailed: 0, + }, + { + name: "bad workspace, trivy prefix", + src: `# trivy:ignore:aws-s3-non-empty-bucket:exp:2221-01-02:ws:otherworkspace +resource "aws_s3_bucket" "test" {}`, + expectedFailed: 1, + }, + { + name: "workspace with wildcard", + src: `# tfsec:ignore:*:ws:test* +resource "aws_s3_bucket" "test" {}`, + expectedFailed: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + results := scanHCL(t, tt.src, + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ScannerWithWorkspaceName("testworkspace"), + ) + assert.Len(t, results.GetFailed(), tt.expectedFailed) + }) + } +} + +func Test_IgnoreByAVDID(t *testing.T) { + check := `# METADATA +# custom: +# id: TEST-0123 +# avd_id: AVD-TEST-0123 +# short_code: non-empty-bucket +# provider: aws +# service: s3 +package user.test123 + +import rego.v1 + + +deny contains res if { + some bucket in input.aws.s3.buckets + bucket.name.value == "" + res := result.new("The bucket name cannot be empty.", bucket.name) +}` + + src := ` +# trivy:ignore:avd-test-0123 +resource "aws_s3_bucket" "test" {} +` + + results := scanHCL(t, src, + rego.WithPolicyReader(strings.NewReader(check)), + rego.WithPolicyNamespaces("user"), + ) + testutil.AssertRuleNotFailed(t, "aws-s3-non-empty-bucket", results, "") +} + +func Test_IgnoreInlineByAllIDs(t *testing.T) { + testCases := []struct { + input string + }{ + { + input: `resource "aws_s3_bucket" "test" { + bucket = "" # tfsec:ignore:%s +} + `, + }, + { + input: `resource "aws_s3_bucket" "test" { + bucket = "" # trivy:ignore:%s +} + `, + }, + } + + for _, tc := range testCases { + ids := []string{ + "USER-TEST-0123", strings.ToLower("user-test-0123"), + "non-empty-bucket", "aws-s3-non-empty-bucket", + } + + for _, id := range ids { + t.Run(id, func(t *testing.T) { + results := scanHCL(t, fmt.Sprintf(tc.input, id), + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + testutil.AssertRuleNotFailed(t, "aws-s3-non-empty-bucket", results, "") + }) + } + } +} + +func TestIgnoreRemoteTerraformResource(t *testing.T) { + + fsys := testutil.CreateFS(map[string]string{ + "main.tf": `module "bucket" { + source = "git::https://github.com/test/bucket" +}`, + ".terraform/modules/modules.json": `{ + "Modules": [ + { "Key": "", "Source": "", "Dir": "." }, + { + "Key": "bucket", + "Source": "git::https://github.com/test/bucket", + "Dir": ".terraform/modules/bucket" + } + ] +} +`, + ".terraform/modules/bucket/main.tf": ` +# trivy:ignore:user-test-0123 +resource "aws_s3_bucket" "test" { + bucket = "" +} +`, + }) + + results, err := scanFS(fsys, ".", + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + require.NoError(t, err) + testutil.AssertRuleNotFailed(t, "aws-s3-non-empty-bucket", results, "") +} diff --git a/pkg/iac/scanners/terraform/json_test.go b/pkg/iac/scanners/terraform/json_test.go new file mode 100644 index 000000000000..aad2d9ac8c32 --- /dev/null +++ b/pkg/iac/scanners/terraform/json_test.go @@ -0,0 +1,73 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/rego" +) + +func TestScanningJSON(t *testing.T) { + + var tests = []struct { + name string + source string + expected bool + }{ + { + name: "check results are picked up in tf json configs", + source: ` + { + "provider": { + "aws": { + "profile": null, + "region": "eu-west-1" + } + }, + "resource": { + "aws_s3_bucket": { + "test": { + "bucket": "" + } + } + } + }`, + expected: true, + }, + { + name: "check attributes are checked in tf json configs", + source: ` + { + "provider": { + "aws": { + "profile": null, + "region": "eu-west-1" + } + }, + "resource": { + "aws_s3_bucket": { + "test": { + "bucket": "test" + } + } + } + }`, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + results := scanJSON(t, test.source, + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + if test.expected { + testutil.AssertRuleFound(t, "aws-s3-non-empty-bucket", results, "false negative found") + } else { + testutil.AssertRuleNotFound(t, "aws-s3-non-empty-bucket", results, "false positive found") + } + }) + } +} diff --git a/pkg/iac/scanners/terraform/module_test.go b/pkg/iac/scanners/terraform/module_test.go new file mode 100644 index 000000000000..71222e019035 --- /dev/null +++ b/pkg/iac/scanners/terraform/module_test.go @@ -0,0 +1,380 @@ +package terraform + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/rego" +) + +func Test_Modules(t *testing.T) { + tests := []struct { + name string + files map[string]string + expected bool + }{ + { + // IMPORTANT: if this test is failing, you probably need to set the version of go-cty in go.mod to the same version that hcl uses. + name: "go-cty compatibility issue", + files: map[string]string{ + "/project/main.tf": ` +data "aws_vpc" "default" { + default = true +} + +module "test" { + source = "../modules/problem/" + cidr_block = data.aws_vpc.default.cidr_block +}`, + "/modules/problem/main.tf": `variable "cidr_block" {} + +variable "open" { + default = false +} + +resource "aws_security_group" "this" { + name = "Test" + + ingress { + description = "HTTPs" + from_port = 443 + to_port = 443 + protocol = "tcp" + self = ! var.open + cidr_blocks = var.open ? [var.cidr_block] : null + } +} + +resource "aws_s3_bucket" "test" {}`, + }, + expected: true, + }, + { + name: "misconfig in sibling directory module", + files: map[string]string{ + "/project/main.tf": ` +module "something" { + source = "../modules/problem" +} +`, + "modules/problem/main.tf": ` +resource "aws_s3_bucket" "test" {}`, + }, + expected: true, + }, + { + name: "ignore misconfig in module", + files: map[string]string{ + "/project/main.tf": ` +#tfsec:ignore:aws-s3-non-empty-bucket +module "something" { + source = "../modules/problem" +} +`, + "modules/problem/main.tf": ` +resource "aws_s3_bucket" "test" {} +`, + }, + expected: false, + }, + { + name: "misconfig in subdirectory module", + files: map[string]string{ + "project/main.tf": ` +module "something" { + source = "./modules/problem" +} +`, + "project/modules/problem/main.tf": ` +resource "aws_s3_bucket" "test" {} +`, + }, + expected: true, + }, + { + name: "misconfig in parent directory module", + files: map[string]string{ + "project/main.tf": ` +module "something" { + source = "../problem" +} +`, + "problem/main.tf": ` +resource "aws_s3_bucket" "test" {} +`}, + expected: true, + }, + { + name: "misconfig in reused module", + files: map[string]string{ + "project/main.tf": ` +module "something_good" { + source = "../modules/problem" + bucket = "test" +} + +module "something_bad" { + source = "../modules/problem" + bucket = "" +} +`, + "modules/problem/main.tf": ` +variable "bucket" {} + +resource "aws_s3_bucket" "test" { + bucket = var.bucket +} +`}, + expected: true, + }, + { + name: "misconfig in nested module", + files: map[string]string{ + "project/main.tf": ` +module "something" { + source = "../modules/a" +} +`, + "modules/a/main.tf": ` + module "something" { + source = "../../modules/b" +} +`, + "modules/b/main.tf": ` +module "something" { + source = "../c" +} +`, + "modules/c/main.tf": `resource "aws_s3_bucket" "test" {}`, + }, + expected: true, + }, + { + name: "misconfig in reused nested module", + files: map[string]string{ + "project/main.tf": ` +module "something" { + source = "../modules/a" + bucket = "test" +} + +module "something-bad" { + source = "../modules/a" + bucket = "" +} +`, + "modules/a/main.tf": ` +variable "bucket" {} + +module "something" { + source = "../../modules/b" + bucket = var.bucket +} +`, + "modules/b/main.tf": ` +variable "bucket" {} + +module "something" { + source = "../c" + bucket = var.bad +} +`, + "modules/c/main.tf": ` +variable "bucket" {} + +resource "aws_s3_bucket" "test" { + bucket = var.bucket +} +`, + }, + expected: true, + }, + { + name: "misconfig in terraform cached module", + files: map[string]string{ + "project/main.tf": ` +module "something" { + source = "../modules/somewhere" + bucket = "test" +} +`, + "modules/somewhere/main.tf": ` +module "something_nested" { + count = 1 + source = "github.com/some/module.git" + bucket = "" +} + +variable "bucket" { + default = "" +}`, + "project/.terraform/modules/something.something_nested/main.tf": ` +variable "bucket" {} + +resource "aws_s3_bucket" "test" { + bucket = var.bucket +} +`, + "project/.terraform/modules/modules.json": ` + {"Modules":[ + {"Key":"something","Source":"../modules/somewhere","Version":"2.35.0","Dir":"../modules/somewhere"}, + {"Key":"something.something_nested","Source":"git::https://github.com/some/module.git","Version":"2.35.0","Dir":".terraform/modules/something.something_nested"} + ]} +`, + }, + expected: true, + }, + { + name: "misconfig in reused terraform cached module", + files: map[string]string{ + "project/main.tf": ` +module "something" { + source = "/nowhere" + bucket = "" +} + +module "something2" { + source = "/nowhere" + bucket = "" +} +`, + "project/.terraform/modules/a/main.tf": ` +variable "bucket" {} + +resource "aws_s3_bucket" "test" { + bucket = var.bucket +} +`, + "project/.terraform/modules/modules.json": ` + {"Modules":[{"Key":"something","Source":"/nowhere","Version":"2.35.0","Dir":".terraform/modules/a"},{"Key":"something2","Source":"/nowhere","Version":"2.35.0","Dir":".terraform/modules/a"}]} +`, + }, + expected: true, + }, + { + name: "misconfig in nested modules with duplicate module names and paths", + files: map[string]string{ + "project/main.tf": ` +module "something" { + source = "../modules/a" + s3_bucket_count = 0 +} + +module "something-bad" { + source = "../modules/a" + s3_bucket_count = 1 +} +`, + "modules/a/main.tf": ` +variable "s3_bucket_count" { + default = 0 +} +module "something" { + source = "../b" + s3_bucket_count = var.s3_bucket_count +} +`, + "modules/b/main.tf": ` +variable "s3_bucket_count" { + default = 0 +} +module "something" { + source = "../c" + s3_bucket_count = var.s3_bucket_count +} +`, + "modules/c/main.tf": ` +variable "s3_bucket_count" { + default = 0 +} + +resource "aws_s3_bucket" "test" { + count = var.s3_bucket_count +} +`, + }, + expected: true, + }, + { + name: "misconfigured attribute referencing to dynamic variable", + files: map[string]string{ + "project/main.tf": ` +resource "something" "this" { + dynamic "blah" { + for_each = ["a"] + content { + bucket = "" + } + } +} + +resource "aws_s3_bucket" "test" { + secure = something.this.blah[0].bucket +} +`}, + expected: true, + }, + { + name: "attribute referencing to dynamic variable without index", + files: map[string]string{ + "project/main.tf": ` +resource "something" "else" { + dynamic "blah" { + for_each = toset(["test"]) + content { + bucket = blah.value + } + } +} + +resource "aws_s3_bucket" "test" { + bucket = something.else.blah.bucket +}`}, + expected: false, + }, + { + name: "references passed to nested module", + files: map[string]string{ + "project/main.tf": ` + +resource "some_resource" "this" { + name = "test" +} + +module "something" { + source = "../modules/a" + bucket = some_resource.this.name +} +`, + "modules/a/main.tf": ` +variable "bucket" { + type = string +} + +resource "aws_s3_bucket" "test" { + bucket = var.bucket +} +`}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := testutil.CreateFS(tt.files) + results, err := scanFS(fsys, "project", + rego.WithPolicyReader(strings.NewReader(emptyBucketCheck)), + rego.WithPolicyNamespaces("user"), + ) + require.NoError(t, err) + if tt.expected { + testutil.AssertRuleFound(t, "aws-s3-non-empty-bucket", results, "") + } else { + testutil.AssertRuleNotFailed(t, "aws-s3-non-empty-bucket", results, "") + } + }) + } +} diff --git a/pkg/iac/scanners/terraform/options.go b/pkg/iac/scanners/terraform/options.go new file mode 100644 index 000000000000..7547a19a8382 --- /dev/null +++ b/pkg/iac/scanners/terraform/options.go @@ -0,0 +1,110 @@ +package terraform + +import ( + "io/fs" + "strings" + + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/executor" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser" +) + +type ConfigurableTerraformScanner interface { + options.ConfigurableScanner + SetForceAllDirs(bool) + AddExecutorOptions(options ...executor.Option) + AddParserOptions(options ...parser.Option) +} + +func ScannerWithTFVarsPaths(paths ...string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddParserOptions(parser.OptionWithTFVarsPaths(paths...)) + } + } +} + +func ScannerWithWorkspaceName(name string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddParserOptions(parser.OptionWithWorkspaceName(name)) + tf.AddExecutorOptions(executor.OptionWithWorkspaceName(name)) + } + } +} + +func ScannerWithAllDirectories(all bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.SetForceAllDirs(all) + } + } +} + +func ScannerWithSkipDownloaded(skip bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if !skip { + return + } + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddExecutorOptions(executor.OptionWithResultsFilter(func(results scan.Results) scan.Results { + for i, result := range results { + prefix := result.Range().GetSourcePrefix() + if prefix != "" && !strings.HasPrefix(prefix, ".") { + results[i].OverrideStatus(scan.StatusIgnored) + } + } + return results + })) + } + } +} + +func ScannerWithDownloadsAllowed(allowed bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddParserOptions(parser.OptionWithDownloads(allowed)) + } + } +} + +func ScannerWithSkipCachedModules(b bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddParserOptions(parser.OptionWithSkipCachedModules(b)) + } + } +} + +func ScannerWithConfigsFileSystem(fsys fs.FS) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddParserOptions(parser.OptionWithConfigsFS(fsys)) + } + } +} + +func ScannerWithSkipFiles(files []string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddParserOptions(parser.OptionWithSkipFiles(files)) + } + } +} + +func ScannerWithSkipDirs(dirs []string) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddParserOptions(parser.OptionWithSkipDirs(dirs)) + } + } +} + +func ScannerWithStopOnHCLError(stop bool) options.ScannerOption { + return func(s options.ConfigurableScanner) { + if tf, ok := s.(ConfigurableTerraformScanner); ok { + tf.AddParserOptions(parser.OptionStopOnHCLError(stop)) + } + } +} diff --git a/pkg/iac/scanners/terraform/parser/ctylist.go b/pkg/iac/scanners/terraform/parser/ctylist.go new file mode 100644 index 000000000000..13132824d9f7 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/ctylist.go @@ -0,0 +1,34 @@ +package parser + +import "github.com/zclconf/go-cty/cty" + +// insertTupleElement inserts a value into a tuple at the specified index. +// If the idx is outside the bounds of the list, it grows the tuple to +// the new size, and fills in `cty.NilVal` for the missing elements. +// +// This function will not panic. If the list value is not a list, it will +// be replaced with an empty list. +func insertTupleElement(list cty.Value, idx int, val cty.Value) cty.Value { + if list.IsNull() || !list.Type().IsTupleType() { + // better than a panic + list = cty.EmptyTupleVal + } + + if idx < 0 { + // Nothing to do? + return list + } + + // Create a new list of the correct length, copying in the old list + // values for matching indices. + newList := make([]cty.Value, max(idx+1, list.LengthInt())) + for it := list.ElementIterator(); it.Next(); { + key, elem := it.Element() + elemIdx, _ := key.AsBigFloat().Int64() + newList[elemIdx] = elem + } + // Insert the new value. + newList[idx] = val + + return cty.TupleVal(newList) +} diff --git a/pkg/iac/scanners/terraform/parser/ctylist_test.go b/pkg/iac/scanners/terraform/parser/ctylist_test.go new file mode 100644 index 000000000000..70be3db75226 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/ctylist_test.go @@ -0,0 +1,102 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" +) + +func Test_insertTupleElement(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + start cty.Value + index int + value cty.Value + want cty.Value + }{ + { + name: "empty", + start: cty.Value{}, + index: 0, + value: cty.NilVal, + want: cty.TupleVal([]cty.Value{cty.NilVal}), + }, + { + name: "empty to length", + start: cty.Value{}, + index: 2, + value: cty.NilVal, + want: cty.TupleVal([]cty.Value{cty.NilVal, cty.NilVal, cty.NilVal}), + }, + { + name: "insert to empty", + start: cty.EmptyTupleVal, + index: 1, + value: cty.NumberIntVal(5), + want: cty.TupleVal([]cty.Value{cty.NilVal, cty.NumberIntVal(5)}), + }, + { + name: "insert to existing", + start: cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2), cty.NumberIntVal(3)}), + index: 1, + value: cty.NumberIntVal(5), + want: cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(5), cty.NumberIntVal(3)}), + }, + { + name: "insert to existing, extends", + start: cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.NumberIntVal(2), cty.NumberIntVal(3)}), + index: 4, + value: cty.NumberIntVal(5), + want: cty.TupleVal([]cty.Value{ + cty.NumberIntVal(1), cty.NumberIntVal(2), + cty.NumberIntVal(3), cty.NilVal, + cty.NumberIntVal(5), + }), + }, + { + name: "mixed list", + start: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.NumberIntVal(2), cty.NumberIntVal(3)}), + index: 1, + value: cty.BoolVal(true), + want: cty.TupleVal([]cty.Value{ + cty.StringVal("a"), cty.BoolVal(true), cty.NumberIntVal(3), + }), + }, + { + name: "replace end", + start: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.NumberIntVal(2), cty.NumberIntVal(3)}), + index: 2, + value: cty.StringVal("end"), + want: cty.TupleVal([]cty.Value{ + cty.StringVal("a"), cty.NumberIntVal(2), cty.StringVal("end"), + }), + }, + + // Some bad arguments + { + name: "negative index", + start: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.NumberIntVal(2), cty.NumberIntVal(3)}), + index: -1, + value: cty.BoolVal(true), + want: cty.TupleVal([]cty.Value{cty.StringVal("a"), cty.NumberIntVal(2), cty.NumberIntVal(3)}), + }, + { + name: "non-list", + start: cty.BoolVal(true), + index: 1, + value: cty.BoolVal(true), + want: cty.TupleVal([]cty.Value{cty.NilVal, cty.BoolVal(true)}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tt.want, insertTupleElement(tt.start, tt.index, tt.value)) + }) + } +} diff --git a/pkg/iac/scanners/terraform/parser/evaluator.go b/pkg/iac/scanners/terraform/parser/evaluator.go new file mode 100644 index 000000000000..31152cef3637 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/evaluator.go @@ -0,0 +1,700 @@ +package parser + +import ( + "context" + "errors" + "io/fs" + "maps" + "reflect" + "slices" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/samber/lo" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + + "github.com/aquasecurity/trivy/pkg/iac/ignore" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + tfcontext "github.com/aquasecurity/trivy/pkg/iac/terraform/context" + "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +const ( + maxContextIterations = 32 +) + +type evaluator struct { + logger *log.Logger + filesystem fs.FS + ctx *tfcontext.Context + blocks terraform.Blocks + inputVars map[string]cty.Value + moduleMetadata *ModulesMetadata + projectRootPath string // root of the current scan + modulePath string + moduleName string + ignores ignore.Rules + parentParser *Parser + allowDownloads bool + skipCachedModules bool +} + +func newEvaluator( + target fs.FS, + parentParser *Parser, + projectRootPath string, + modulePath string, + workingDir string, + moduleName string, + blocks terraform.Blocks, + inputVars map[string]cty.Value, + moduleMetadata *ModulesMetadata, + workspace string, + ignores ignore.Rules, + logger *log.Logger, + allowDownloads bool, + skipCachedModules bool, +) *evaluator { + + // create a context to store variables and make functions available + ctx := tfcontext.NewContext(&hcl.EvalContext{ + Functions: Functions(target, modulePath), + }, nil) + + // these variables are made available by terraform to each module + ctx.SetByDot(cty.StringVal(workspace), "terraform.workspace") + ctx.SetByDot(cty.StringVal(projectRootPath), "path.root") + ctx.SetByDot(cty.StringVal(modulePath), "path.module") + ctx.SetByDot(cty.StringVal(workingDir), "path.cwd") + + // each block gets its own scope to define variables in + for _, b := range blocks { + b.OverrideContext(ctx.NewChild()) + } + + return &evaluator{ + filesystem: target, + parentParser: parentParser, + modulePath: modulePath, + moduleName: moduleName, + projectRootPath: projectRootPath, + ctx: ctx, + blocks: blocks, + inputVars: inputVars, + moduleMetadata: moduleMetadata, + ignores: ignores, + logger: logger, + allowDownloads: allowDownloads, + skipCachedModules: skipCachedModules, + } +} + +func (e *evaluator) evaluateStep() { + + e.ctx.Set(e.getValuesByBlockType("variable"), "var") + e.ctx.Set(e.getValuesByBlockType("locals"), "local") + e.ctx.Set(e.getValuesByBlockType("provider"), "provider") + + for typ, resource := range e.getResources() { + e.ctx.Set(resource, typ) + } + + e.ctx.Set(e.getValuesByBlockType("data"), "data") + e.ctx.Set(e.getValuesByBlockType("output"), "output") + e.ctx.Set(e.getValuesByBlockType("module"), "module") +} + +// exportOutputs is used to export module outputs to the parent module +func (e *evaluator) exportOutputs() cty.Value { + data := make(map[string]cty.Value) + for _, block := range e.blocks.OfType("output") { + attr := block.GetAttribute("value") + if attr.IsNil() { + continue + } + data[block.Label()] = attr.Value() + e.logger.Debug( + "Added module output", + log.String("block", block.Label()), + log.String("value", attr.Value().GoString()), + ) + } + return cty.ObjectVal(data) +} + +func (e *evaluator) EvaluateAll(ctx context.Context) (terraform.Modules, map[string]fs.FS) { + + e.logger.Debug("Starting module evaluation...", log.String("path", e.modulePath)) + + fsKey := types.CreateFSKey(e.filesystem) + fsMap := map[string]fs.FS{ + fsKey: e.filesystem, + } + + e.evaluateSteps() + + // expand out resources and modules via count, for-each and dynamic + // (not a typo, we do this twice so every order is processed) + // TODO: using a module in for_each or count does not work, + // because the child module is evaluated later + e.blocks = e.expandBlocks(e.blocks) + e.blocks = e.expandBlocks(e.blocks) + + // Re-evaluate locals after all expansions to ensure computed locals + // that depend on expanded resources get the correct values + e.ctx.Replace(e.getValuesByBlockType("locals"), "local") + + // Final expansion round to handle any previously deferred for_each blocks + // that now have correct local values + e.blocks = e.expandBlockForEaches(e.blocks) + + // rootModule is initialized here, but not fully evaluated until all submodules are evaluated. + // Initializing it up front to keep the module hierarchy of parents correct. + rootModule := terraform.NewModule(e.projectRootPath, e.modulePath, e.blocks, e.ignores) + submodules := e.evaluateSubmodules(ctx, rootModule, fsMap) + + e.logger.Debug("Starting post-submodules evaluation...") + e.evaluateSteps() + + e.logger.Debug("Module evaluation complete.") + return append(terraform.Modules{rootModule}, submodules...), fsMap +} + +func (e *evaluator) evaluateSubmodules(ctx context.Context, parent *terraform.Module, fsMap map[string]fs.FS) terraform.Modules { + submodules := e.loadSubmodules(ctx) + + if len(submodules) == 0 { + return nil + } + + e.logger.Debug("Starting submodules evaluation...") + + for i := range maxContextIterations { + changed := false + for _, sm := range submodules { + changed = changed || e.evaluateSubmodule(ctx, sm) + } + if !changed { + e.logger.Debug("All submodules are evaluated", log.Int("loop", i)) + break + } + } + + e.logger.Debug("Starting post-submodule evaluation...") + e.evaluateSteps() + + var modules terraform.Modules + for _, sm := range submodules { + // Assign the parent placeholder to any submodules without a parent. Any modules + // with a parent already have their correct parent placeholder assigned. + for _, submod := range sm.modules { + if submod.Parent() == nil { + submod.SetParent(parent) + } + } + + modules = append(modules, sm.modules...) + maps.Copy(fsMap, sm.fsMap) + } + + e.logger.Debug("Finished processing submodule(s).", log.Int("count", len(modules))) + return modules +} + +type submodule struct { + definition *ModuleDefinition + eval *evaluator + modules terraform.Modules + lastState map[string]cty.Value + fsMap map[string]fs.FS +} + +func (e *evaluator) loadSubmodules(ctx context.Context) []*submodule { + var submodules []*submodule + + for _, definition := range e.loadModules(ctx) { + eval, err := definition.Parser.Load(ctx) + if errors.Is(err, ErrNoFiles) { + continue + } else if err != nil { + e.logger.Error("Failed to load submodule", log.String("name", definition.Name), log.Err(err)) + continue + } + + submodules = append(submodules, &submodule{ + definition: definition, + eval: eval, + fsMap: make(map[string]fs.FS), + }) + } + + return submodules +} + +func (e *evaluator) evaluateSubmodule(ctx context.Context, sm *submodule) bool { + inputVars := sm.definition.inputVars() + if len(sm.modules) > 0 { + if reflect.DeepEqual(inputVars, sm.lastState) { + e.logger.Debug("Submodule inputs unchanged", log.String("name", sm.definition.Name)) + return false + } + } + + e.logger.Debug("Evaluating submodule", log.String("name", sm.definition.Name)) + sm.eval.inputVars = inputVars + sm.modules, sm.fsMap = sm.eval.EvaluateAll(ctx) + outputs := sm.eval.exportOutputs() + + valueMap := e.ctx.Get("module").AsValueMap() + if valueMap == nil { + valueMap = make(map[string]cty.Value) + } + + // lastState needs to be captured after applying outputs – so that they + // don't get treated as changes – but before running post-submodule + // evaluation, so that changes from that can trigger re-evaluations of + // the submodule if/when they feed back into inputs. + ref := sm.definition.Definition.Reference() + e.ctx.Set(blockInstanceValues(sm.definition.Definition, valueMap, outputs), "module", ref.NameLabel()) + e.ctx.Set(outputs, "module", sm.definition.Name) + sm.lastState = sm.definition.inputVars() + e.evaluateSteps() + return true +} + +func (e *evaluator) evaluateSteps() { + var lastContext hcl.EvalContext + for i := range maxContextIterations { + + e.logger.Debug("Starting iteration", log.Int("iteration", i)) + e.evaluateStep() + + // if ctx matches the last evaluation, we can bail, nothing left to resolve + if i > 0 && reflect.DeepEqual(lastContext.Variables, e.ctx.Inner().Variables) { + e.logger.Debug("Context unchanged", log.Int("iteration", i)) + break + } + if len(e.ctx.Inner().Variables) != len(lastContext.Variables) { + lastContext.Variables = make(map[string]cty.Value, len(e.ctx.Inner().Variables)) + } + maps.Copy(lastContext.Variables, e.ctx.Inner().Variables) + } +} + +func (e *evaluator) expandBlocks(blocks terraform.Blocks) terraform.Blocks { + return e.expandDynamicBlocks(e.expandBlockForEaches(e.expandBlockCounts(blocks))...) +} + +func (e *evaluator) expandDynamicBlocks(blocks ...*terraform.Block) terraform.Blocks { + for _, b := range blocks { + if err := b.ExpandBlock(); err != nil { + e.logger.Debug(`Failed to expand dynamic block.`, + log.String("block", b.FullName()), log.Err(err)) + } + } + return blocks +} + +func isBlockSupportsForEachMetaArgument(block *terraform.Block) bool { + return slices.Contains([]string{ + "module", + "resource", + "data", + }, block.Type()) +} + +func (e *evaluator) expandBlockForEaches(blocks terraform.Blocks) terraform.Blocks { + + var forEachFiltered terraform.Blocks + + for _, block := range blocks { + + forEachAttr := block.GetAttribute("for_each") + + if forEachAttr.IsNil() || block.IsExpanded() || !isBlockSupportsForEachMetaArgument(block) || e.shouldDeferForEachExpansion(forEachAttr) { + forEachFiltered = append(forEachFiltered, block) + continue + } + + forEachVal := forEachAttr.Value() + + if forEachVal.IsNull() || !forEachVal.IsKnown() || !forEachAttr.IsIterable() { + e.logger.Debug(`Failed to expand block. Invalid "for-each" argument. Must be known and iterable.`, + log.String("block", block.FullName()), + log.String("value", forEachVal.GoString()), + ) + continue + } + + clones := make(map[string]cty.Value) + _ = forEachAttr.Each(func(key cty.Value, val cty.Value) { + + if val.IsNull() { + return + } + + // instances are identified by a map key (or set member) from the value provided to for_each + idx, err := convert.Convert(key, cty.String) + if err != nil { + e.logger.Debug( + `Failed to expand block. Invalid "for-each" argument: map key (or set value) is not a string`, + log.String("block", block.FullName()), + log.String("key", key.GoString()), + log.String("value", val.GoString()), + log.Err(err), + ) + return + } + + // if the argument is a collection but not a map, then the resource identifier + // is the value of the collection. The exception is the use of for-each inside a dynamic block, + // because in this case the collection element may not be a primitive value. + if (forEachVal.Type().IsCollectionType() || forEachVal.Type().IsTupleType()) && + !forEachVal.Type().IsMapType() { + stringVal, err := convert.Convert(val, cty.String) + if err != nil { + e.logger.Debug( + "Failed to expand block. Invalid 'for-each' argument: value is not a string", + log.String("block", block.FullName()), + log.String("key", idx.AsString()), + log.String("value", val.GoString()), + log.Err(err), + ) + return + } + idx = stringVal + } + + clone := block.Clone(idx) + ctx := clone.Context() + e.copyVariables(block, clone) + + eachObj := cty.ObjectVal(map[string]cty.Value{ + "key": idx, + "value": val, + }) + + ctx.Set(eachObj, "each") + ctx.Set(eachObj, block.TypeLabel()) + forEachFiltered = append(forEachFiltered, clone) + clones[idx.AsString()] = clone.Values() + }) + + metadata := block.GetMetadata() + if len(clones) == 0 { + e.ctx.SetByDot(cty.EmptyTupleVal, metadata.Reference()) + } else { + // The for-each meta-argument creates multiple instances of the resource that are stored in the map. + // So we must replace the old resource with a map with the attributes of the resource. + e.ctx.Replace(cty.ObjectVal(clones), metadata.Reference()) + } + e.logger.Debug("Expanded block into clones via 'for_each' attribute.", + log.String("block", block.FullName()), + log.Int("clones", len(clones)), + ) + } + + return forEachFiltered +} + +func isBlockSupportsCountMetaArgument(block *terraform.Block) bool { + return slices.Contains([]string{ + "module", + "resource", + "data", + }, block.Type()) +} + +func (e *evaluator) expandBlockCounts(blocks terraform.Blocks) terraform.Blocks { + var countFiltered terraform.Blocks + for _, block := range blocks { + countAttr := block.GetAttribute("count") + if countAttr.IsNil() || block.IsExpanded() || !isBlockSupportsCountMetaArgument(block) { + countFiltered = append(countFiltered, block) + continue + } + count := 1 + countAttrVal := countAttr.Value() + if !countAttrVal.IsNull() && countAttrVal.IsKnown() && countAttrVal.Type() == cty.Number { + count = int(countAttr.AsNumber()) + } + + var clones []cty.Value + for i := 0; i < count; i++ { + clone := block.Clone(cty.NumberIntVal(int64(i))) + clones = append(clones, clone.Values()) + countFiltered = append(countFiltered, clone) + metadata := clone.GetMetadata() + e.ctx.SetByDot(clone.Values(), metadata.Reference()) + } + metadata := block.GetMetadata() + if len(clones) == 0 { + e.ctx.SetByDot(cty.EmptyTupleVal, metadata.Reference()) + } else { + e.ctx.SetByDot(cty.TupleVal(clones), metadata.Reference()) + } + e.logger.Debug( + "Expanded block into clones via 'count' attribute.", + log.String("block", block.FullName()), + log.Int("clones", len(clones)), + ) + } + + return countFiltered +} + +func (e *evaluator) copyVariables(from, to *terraform.Block) { + + var fromBase string + var fromRel string + var toRel string + + switch from.Type() { + case "resource": + fromBase = from.TypeLabel() + fromRel = from.NameLabel() + toRel = to.NameLabel() + case "module": + fromBase = from.Type() + fromRel = from.TypeLabel() + toRel = to.TypeLabel() + default: + return + } + + rootCtx := e.ctx.Root() + srcValue := rootCtx.Get(fromBase, fromRel) + if srcValue == cty.NilVal { + return + } + rootCtx.Set(srcValue, fromBase, toRel) +} + +func (e *evaluator) evaluateVariable(b *terraform.Block) (cty.Value, error) { + if b.Label() == "" { + return cty.NilVal, errors.New("empty label - cannot resolve") + } + + attributes := b.Attributes() + + var valType cty.Type + var defaults *typeexpr.Defaults + if typeAttr, exists := attributes["type"]; exists { + ty, def, err := typeAttr.DecodeVarType() + if err != nil { + return cty.NilVal, err + } + valType = ty + defaults = def + } + + var val cty.Value + + if override, exists := e.inputVars[b.Label()]; exists && override.Type() != cty.NilType { + val = override + } else if def, exists := attributes["default"]; exists { + val = def.NullableValue() + } else { + return cty.NilVal, errors.New("no value found") + } + + if valType != cty.NilType { + if defaults != nil { + val = defaults.Apply(val) + } + + typedVal, err := convert.Convert(val, valType) + if err != nil { + return cty.NilVal, err + } + return typedVal, nil + } + + return val, nil + +} + +func (e *evaluator) evaluateOutput(b *terraform.Block) (cty.Value, error) { + if b.Label() == "" { + return cty.NilVal, errors.New("empty label - cannot resolve") + } + + attribute := b.GetAttribute("value") + if attribute.IsNil() { + return cty.NilVal, errors.New("cannot resolve output with no attributes") + } + return attribute.Value(), nil +} + +// returns true if all evaluations were successful +func (e *evaluator) getValuesByBlockType(blockType string) cty.Value { + + blocksOfType := e.blocks.OfType(blockType) + values := make(map[string]cty.Value) + + for _, b := range blocksOfType { + switch b.Type() { + case "variable": // variables are special in that their value comes from the "default" attribute + val, err := e.evaluateVariable(b) + if err != nil { + continue + } + values[b.Label()] = val + case "output": + val, err := e.evaluateOutput(b) + if err != nil { + continue + } + values[b.Label()] = val + case "locals", "moved", "import": + localValues := b.Values().AsValueMap() + maps.Copy(values, localValues) + case "provider", "module", "check": + if b.Label() == "" { + continue + } + values[b.Label()] = b.Values() + case "data": + if len(b.Labels()) < 2 { + continue + } + + // Data blocks should all be loaded into the top level 'values' + // object. The hierarchy of the map is: + // values = map[]map[] = + // Block -> Block's attributes as a cty.Object + // Tuple(Block) -> Instances of the block + // Object(Block) -> Field values are instances of the block + ref := b.Reference() + typeValues, ok := values[ref.TypeLabel()] + if !ok { + typeValues = cty.ObjectVal(make(map[string]cty.Value)) + values[ref.TypeLabel()] = typeValues + } + + valueMap := typeValues.AsValueMap() + if valueMap == nil { + valueMap = make(map[string]cty.Value) + } + valueMap[ref.NameLabel()] = blockInstanceValues(b, valueMap, b.Values()) + + // Update the map of all blocks with the same type. + values[ref.TypeLabel()] = cty.ObjectVal(valueMap) + } + } + + return cty.ObjectVal(values) +} + +func (e *evaluator) getResources() map[string]cty.Value { + values := make(map[string]map[string]cty.Value) + + for _, b := range e.blocks { + if b.Type() != "resource" || len(b.Labels()) < 2 { + continue + } + + ref := b.Reference() + typeValues, exists := values[ref.TypeLabel()] + if !exists { + typeValues = make(map[string]cty.Value) + values[ref.TypeLabel()] = typeValues + } + + instanceVal := blockInstanceValues(b, typeValues, b.Values()) + typeValues[ref.NameLabel()] = instanceVal + } + + result := lo.MapValues(values, func(v map[string]cty.Value, _ string) cty.Value { + return cty.ObjectVal(v) + }) + + return result +} + +// blockInstanceValues returns a cty.Value containing the values of the block instances. +// If the count argument is used, a tuple is returned where the index corresponds to the argument index. +// If the for_each argument is used, an object is returned where the key corresponds to the argument key. +// In other cases, the values of the block itself are returned. +func blockInstanceValues(b *terraform.Block, typeValues map[string]cty.Value, values cty.Value) cty.Value { + ref := b.Reference() + key := ref.RawKey() + + switch { + case key.Type().Equals(cty.Number) && b.GetAttribute("count") != nil: + idx, _ := key.AsBigFloat().Int64() + return insertTupleElement(typeValues[ref.NameLabel()], int(idx), values) + case isForEachKey(key) && b.GetAttribute("for_each") != nil: + keyStr := ref.Key() + + instancesVal, exists := typeValues[ref.NameLabel()] + if !exists || !instancesVal.CanIterateElements() { + instancesVal = cty.EmptyObjectVal + } + + instances := instancesVal.AsValueMap() + if instances == nil { + instances = make(map[string]cty.Value) + } + + instances[keyStr] = values + return cty.ObjectVal(instances) + default: + return values + } +} + +func isForEachKey(key cty.Value) bool { + return key.Type().Equals(cty.Number) || key.Type().Equals(cty.String) +} + +func (e *evaluator) shouldDeferForEachExpansion(forEachAttr *terraform.Attribute) bool { + // Check if the for_each references a local value + for _, ref := range forEachAttr.AllReferences() { + if ref.BlockType().Name() == "locals" { + // Get the local value from context + localName := ref.NameLabel() + if localVal := e.ctx.Get("local", localName); localVal != cty.NilVal && localVal.IsKnown() { + // Check if this local contains unresolved dynamic values or looks like + // it was computed from a single resource object instead of an expanded collection + if e.localHasDynamicValues(localVal) { + return true + } + } + } + } + return false +} + +func (e *evaluator) localHasDynamicValues(localVal cty.Value) bool { + if !localVal.Type().IsObjectType() { + return false + } + + valueMap := localVal.AsValueMap() + if valueMap == nil { + return false + } + + // Check if the local contains DynamicVal which indicates unresolved references + for _, val := range valueMap { + if val.Type() == cty.DynamicPseudoType { + return true + } + + // If it's an object, check recursively for dynamic values + if val.Type().IsObjectType() { + if subMap := val.AsValueMap(); subMap != nil { + for _, subVal := range subMap { + if subVal.Type() == cty.DynamicPseudoType { + return true + } + } + } + } + } + + return false +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/cidr.go b/pkg/iac/scanners/terraform/parser/funcs/cidr.go new file mode 100644 index 000000000000..23b1c7be0d45 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/cidr.go @@ -0,0 +1,195 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "fmt" + "math/big" + + "github.com/apparentlymart/go-cidr/cidr" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/gocty" +) + +// CidrHostFunc constructs a function that calculates a full host IP address +// within a given IP network address prefix. +var CidrHostFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "prefix", + Type: cty.String, + }, + { + Name: "hostnum", + Type: cty.Number, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var hostNum *big.Int + if err := gocty.FromCtyValue(args[1], &hostNum); err != nil { + return cty.UnknownVal(cty.String), err + } + _, network, err := ParseCIDR(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) + } + + ip, err := cidr.HostBig(network, hostNum) + if err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.StringVal(ip.String()), nil + }, +}) + +// CidrNetmaskFunc constructs a function that converts an IPv4 address prefix given +// in CIDR notation into a subnet mask address. +var CidrNetmaskFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "prefix", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + _, network, err := ParseCIDR(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) + } + + if network.IP.To4() == nil { + return cty.UnknownVal(cty.String), fmt.Errorf("IPv6 addresses cannot have a netmask: %s", args[0].AsString()) + } + + return cty.StringVal(IP(network.Mask).String()), nil + }, +}) + +// CidrSubnetFunc constructs a function that calculates a subnet address within +// a given IP network address prefix. +var CidrSubnetFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "prefix", + Type: cty.String, + }, + { + Name: "newbits", + Type: cty.Number, + }, + { + Name: "netnum", + Type: cty.Number, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var newbits int + if err := gocty.FromCtyValue(args[1], &newbits); err != nil { + return cty.UnknownVal(cty.String), err + } + var netnum *big.Int + if err := gocty.FromCtyValue(args[2], &netnum); err != nil { + return cty.UnknownVal(cty.String), err + } + + _, network, err := ParseCIDR(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("invalid CIDR expression: %s", err) + } + + newNetwork, err := cidr.SubnetBig(network, newbits, netnum) + if err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.StringVal(newNetwork.String()), nil + }, +}) + +// CidrSubnetsFunc is similar to CidrSubnetFunc but calculates many consecutive +// subnet addresses at once, rather than just a single subnet extension. +var CidrSubnetsFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "prefix", + Type: cty.String, + }, + }, + VarParam: &function.Parameter{ + Name: "newbits", + Type: cty.Number, + }, + Type: function.StaticReturnType(cty.List(cty.String)), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + _, network, err := ParseCIDR(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid CIDR expression: %s", err) + } + startPrefixLen, _ := network.Mask.Size() + + prefixLengthArgs := args[1:] + if len(prefixLengthArgs) == 0 { + return cty.ListValEmpty(cty.String), nil + } + + var firstLength int + if err := gocty.FromCtyValue(prefixLengthArgs[0], &firstLength); err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(1, err) + } + firstLength += startPrefixLen + + retVals := make([]cty.Value, len(prefixLengthArgs)) + + current, _ := cidr.PreviousSubnet(network, firstLength) + for i, lengthArg := range prefixLengthArgs { + var length int + if err := gocty.FromCtyValue(lengthArg, &length); err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(i+1, err) + } + + if length < 1 { + return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "must extend prefix by at least one bit") + } + // For portability with 32-bit systems where the subnet number + // will be a 32-bit int, we only allow extension of 32 bits in + // one call even if we're running on a 64-bit machine. + // (Of course, this is significant only for IPv6.) + if length > 32 { + return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "may not extend prefix by more than 32 bits") + } + length += startPrefixLen + if length > (len(network.IP) * 8) { + protocol := "IP" + switch len(network.IP) * 8 { + case 32: + protocol = "IPv4" + case 128: + protocol = "IPv6" + } + return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "would extend prefix to %d bits, which is too long for an %s address", length, protocol) + } + + next, rollover := cidr.NextSubnet(current, length) + if rollover || !network.Contains(next.IP) { + // If we run out of suffix bits in the base CIDR prefix then + // NextSubnet will start incrementing the prefix bits, which + // we don't allow because it would then allocate addresses + // outside of the caller's given prefix. + return cty.UnknownVal(cty.String), function.NewArgErrorf(i+1, "not enough remaining address space for a subnet with a prefix of %d bits after %s", length, current.String()) + } + + current = next + retVals[i] = cty.StringVal(current.String()) + } + + return cty.ListVal(retVals), nil + }, +}) diff --git a/pkg/iac/scanners/terraform/parser/funcs/collection.go b/pkg/iac/scanners/terraform/parser/funcs/collection.go new file mode 100644 index 000000000000..687b39b209f1 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/collection.go @@ -0,0 +1,652 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "errors" + "fmt" + "math/big" + "sort" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/function/stdlib" + "github.com/zclconf/go-cty/cty/gocty" +) + +var LengthFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "value", + Type: cty.DynamicPseudoType, + AllowDynamicType: true, + AllowUnknown: true, + AllowMarked: true, + }, + }, + Type: func(args []cty.Value) (cty.Type, error) { + collTy := args[0].Type() + switch { + case collTy == cty.String || collTy.IsTupleType() || collTy.IsObjectType() || collTy.IsListType() || collTy.IsMapType() || collTy.IsSetType() || collTy == cty.DynamicPseudoType: + return cty.Number, nil + default: + return cty.Number, errors.New("argument must be a string, a collection type, or a structural type") + } + }, + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + coll := args[0] + collTy := args[0].Type() + marks := coll.Marks() + switch { + case collTy == cty.DynamicPseudoType: + return cty.UnknownVal(cty.Number).WithMarks(marks), nil + case collTy.IsTupleType(): + l := len(collTy.TupleElementTypes()) + return cty.NumberIntVal(int64(l)).WithMarks(marks), nil + case collTy.IsObjectType(): + l := len(collTy.AttributeTypes()) + return cty.NumberIntVal(int64(l)).WithMarks(marks), nil + case collTy == cty.String: + // We'll delegate to the cty stdlib strlen function here, because + // it deals with all of the complexities of tokenizing unicode + // grapheme clusters. + return stdlib.Strlen(coll) + case collTy.IsListType() || collTy.IsSetType() || collTy.IsMapType(): + return coll.Length(), nil + default: + // Should never happen, because of the checks in our Type func above + return cty.UnknownVal(cty.Number), errors.New("impossible value type for length(...)") + } + }, +}) + +// AllTrueFunc constructs a function that returns true if all elements of the +// list are true. If the list is empty, return true. +var AllTrueFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: cty.List(cty.Bool), + }, + }, + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + result := cty.True + for it := args[0].ElementIterator(); it.Next(); { + _, v := it.Element() + if !v.IsKnown() { + return cty.UnknownVal(cty.Bool), nil + } + if v.IsNull() { + return cty.False, nil + } + result = result.And(v) + if result.False() { + return cty.False, nil + } + } + return result, nil + }, +}) + +// AnyTrueFunc constructs a function that returns true if any element of the +// list is true. If the list is empty, return false. +var AnyTrueFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: cty.List(cty.Bool), + }, + }, + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + result := cty.False + var hasUnknown bool + for it := args[0].ElementIterator(); it.Next(); { + _, v := it.Element() + if !v.IsKnown() { + hasUnknown = true + continue + } + if v.IsNull() { + continue + } + result = result.Or(v) + if result.True() { + return cty.True, nil + } + } + if hasUnknown { + return cty.UnknownVal(cty.Bool), nil + } + return result, nil + }, +}) + +// CoalesceFunc constructs a function that takes any number of arguments and +// returns the first one that isn't empty. This function was copied from go-cty +// stdlib and modified so that it returns the first *non-empty* non-null element +// from a sequence, instead of merely the first non-null. +var CoalesceFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + VarParam: &function.Parameter{ + Name: "vals", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowDynamicType: true, + AllowNull: true, + }, + Type: func(args []cty.Value) (ret cty.Type, err error) { + argTypes := make([]cty.Type, len(args)) + for i, val := range args { + argTypes[i] = val.Type() + } + retType, _ := convert.UnifyUnsafe(argTypes) + if retType == cty.NilType { + return cty.NilType, errors.New("all arguments must have the same type") + } + return retType, nil + }, + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + for _, argVal := range args { + // We already know this will succeed because of the checks in our Type func above + argVal, _ = convert.Convert(argVal, retType) + if !argVal.IsKnown() { + return cty.UnknownVal(retType), nil + } + if argVal.IsNull() { + continue + } + if retType == cty.String && argVal.RawEquals(cty.StringVal("")) { + continue + } + + return argVal, nil + } + return cty.NilVal, errors.New("no non-null, non-empty-string arguments") + }, +}) + +// IndexFunc constructs a function that finds the element index for a given value in a list. +var IndexFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: cty.DynamicPseudoType, + }, + { + Name: "value", + Type: cty.DynamicPseudoType, + }, + }, + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + if !(args[0].Type().IsListType() || args[0].Type().IsTupleType()) { + return cty.NilVal, errors.New("argument must be a list or tuple") + } + + if !args[0].IsKnown() { + return cty.UnknownVal(cty.Number), nil + } + + if args[0].LengthInt() == 0 { // Easy path + return cty.NilVal, errors.New("cannot search an empty list") + } + + for it := args[0].ElementIterator(); it.Next(); { + i, v := it.Element() + eq, err := stdlib.Equal(v, args[1]) + if err != nil { + return cty.NilVal, err + } + if !eq.IsKnown() { + return cty.UnknownVal(cty.Number), nil + } + if eq.True() { + return i, nil + } + } + return cty.NilVal, errors.New("item not found") + + }, +}) + +// LookupFunc constructs a function that performs dynamic lookups of map types. +var LookupFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "inputMap", + Type: cty.DynamicPseudoType, + AllowMarked: true, + }, + { + Name: "key", + Type: cty.String, + AllowMarked: true, + }, + }, + VarParam: &function.Parameter{ + Name: "default", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowDynamicType: true, + AllowNull: true, + AllowMarked: true, + }, + Type: func(args []cty.Value) (ret cty.Type, err error) { + if len(args) < 1 || len(args) > 3 { + return cty.NilType, fmt.Errorf("lookup() takes two or three arguments, got %d", len(args)) + } + + ty := args[0].Type() + + switch { + case ty.IsObjectType(): + if !args[1].IsKnown() { + return cty.DynamicPseudoType, nil + } + + keyVal, _ := args[1].Unmark() + key := keyVal.AsString() + if ty.HasAttribute(key) { + return args[0].GetAttr(key).Type(), nil + } else if len(args) == 3 { + // if the key isn't found but a default is provided, + // return the default type + return args[2].Type(), nil + } + return cty.DynamicPseudoType, function.NewArgErrorf(0, "the given object has no attribute %q", key) + case ty.IsMapType(): + if len(args) == 3 { + _, err = convert.Convert(args[2], ty.ElementType()) + if err != nil { + return cty.NilType, function.NewArgErrorf(2, "the default value must have the same type as the map elements") + } + } + return ty.ElementType(), nil + default: + return cty.NilType, function.NewArgErrorf(0, "lookup() requires a map as the first argument") + } + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var defaultVal cty.Value + defaultValueSet := false + + if len(args) == 3 { + // intentionally leave default value marked + defaultVal = args[2] + defaultValueSet = true + } + + // keep track of marks from the collection and key + var markses []cty.ValueMarks + + // unmark collection, retain marks to reapply later + mapVar, mapMarks := args[0].Unmark() + markses = append(markses, mapMarks) + + // include marks on the key in the result + keyVal, keyMarks := args[1].Unmark() + if len(keyMarks) > 0 { + markses = append(markses, keyMarks) + } + lookupKey := keyVal.AsString() + + if !mapVar.IsKnown() { + return cty.UnknownVal(retType).WithMarks(markses...), nil + } + + if mapVar.Type().IsObjectType() { + if mapVar.Type().HasAttribute(lookupKey) { + return mapVar.GetAttr(lookupKey).WithMarks(markses...), nil + } + } else if mapVar.HasIndex(cty.StringVal(lookupKey)) == cty.True { + return mapVar.Index(cty.StringVal(lookupKey)).WithMarks(markses...), nil + } + + if defaultValueSet { + defaultVal, err = convert.Convert(defaultVal, retType) + if err != nil { + return cty.NilVal, err + } + return defaultVal.WithMarks(markses...), nil + } + + return cty.UnknownVal(cty.DynamicPseudoType), fmt.Errorf( + "lookup failed to find key %s", redactIfSensitive(lookupKey, keyMarks)) + }, +}) + +// MatchkeysFunc constructs a function that constructs a new list by taking a +// subset of elements from one list whose indexes match the corresponding +// indexes of values in another list. +var MatchkeysFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "values", + Type: cty.List(cty.DynamicPseudoType), + }, + { + Name: "keys", + Type: cty.List(cty.DynamicPseudoType), + }, + { + Name: "searchset", + Type: cty.List(cty.DynamicPseudoType), + }, + }, + Type: func(args []cty.Value) (cty.Type, error) { + ty, _ := convert.UnifyUnsafe([]cty.Type{args[1].Type(), args[2].Type()}) + if ty == cty.NilType { + return cty.NilType, errors.New("keys and searchset must be of the same type") + } + + // the return type is based on args[0] (values) + return args[0].Type(), nil + }, + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + if !args[0].IsKnown() { + return cty.UnknownVal(cty.List(retType.ElementType())), nil + } + + if args[0].LengthInt() != args[1].LengthInt() { + return cty.ListValEmpty(retType.ElementType()), errors.New("length of keys and values should be equal") + } + + output := make([]cty.Value, 0) + values := args[0] + + // Keys and searchset must be the same type. + // We can skip error checking here because we've already verified that + // they can be unified in the Type function + ty, _ := convert.UnifyUnsafe([]cty.Type{args[1].Type(), args[2].Type()}) + keys, _ := convert.Convert(args[1], ty) + searchset, _ := convert.Convert(args[2], ty) + + // if searchset is empty, return an empty list. + if searchset.LengthInt() == 0 { + return cty.ListValEmpty(retType.ElementType()), nil + } + + if !values.IsWhollyKnown() || !keys.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } + + i := 0 + for it := keys.ElementIterator(); it.Next(); { + _, key := it.Element() + for iter := searchset.ElementIterator(); iter.Next(); { + _, search := iter.Element() + eq, err := stdlib.Equal(key, search) + if err != nil { + return cty.NilVal, err + } + if !eq.IsKnown() { + return cty.ListValEmpty(retType.ElementType()), nil + } + if eq.True() { + v := values.Index(cty.NumberIntVal(int64(i))) + output = append(output, v) + break + } + } + i++ + } + + // if we haven't matched any key, then output is an empty list. + if len(output) == 0 { + return cty.ListValEmpty(retType.ElementType()), nil + } + return cty.ListVal(output), nil + }, +}) + +// OneFunc returns either the first element of a one-element list, or null +// if given a zero-element list. +var OneFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: cty.DynamicPseudoType, + }, + }, + Type: func(args []cty.Value) (cty.Type, error) { + ty := args[0].Type() + switch { + case ty.IsListType() || ty.IsSetType(): + return ty.ElementType(), nil + case ty.IsTupleType(): + etys := ty.TupleElementTypes() + switch len(etys) { + case 0: + // No specific type information, so we'll ultimately return + // a null value of unknown type. + return cty.DynamicPseudoType, nil + case 1: + return etys[0], nil + } + } + return cty.NilType, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements") + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + val := args[0] + ty := val.Type() + + // Our parameter spec above doesn't set AllowUnknown or AllowNull, + // so we can assume our top-level collection is both known and non-null + // in here. + + switch { + case ty.IsListType() || ty.IsSetType(): + lenVal := val.Length() + if !lenVal.IsKnown() { + return cty.UnknownVal(retType), nil + } + var l int + err := gocty.FromCtyValue(lenVal, &l) + if err != nil { + // It would be very strange to get here, because that would + // suggest that the length is either not a number or isn't + // an integer, which would suggest a bug in cty. + return cty.NilVal, fmt.Errorf("invalid collection length: %s", err) + } + switch l { + case 0: + return cty.NullVal(retType), nil + case 1: + var ret cty.Value + // We'll use an iterator here because that works for both lists + // and sets, whereas indexing directly would only work for lists. + // Since we've just checked the length, we should only actually + // run this loop body once. + for it := val.ElementIterator(); it.Next(); { + _, ret = it.Element() + } + return ret, nil + } + case ty.IsTupleType(): + etys := ty.TupleElementTypes() + switch len(etys) { + case 0: + return cty.NullVal(retType), nil + case 1: + ret := val.Index(cty.NumberIntVal(0)) + return ret, nil + } + } + return cty.NilVal, function.NewArgErrorf(0, "must be a list, set, or tuple value with either zero or one elements") + }, +}) + +// SumFunc constructs a function that returns the sum of all +// numbers provided in a list +var SumFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "list", + Type: cty.DynamicPseudoType, + }, + }, + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + + if !args[0].CanIterateElements() { + return cty.NilVal, function.NewArgErrorf(0, "cannot sum noniterable") + } + + if args[0].LengthInt() == 0 { // Easy path + return cty.NilVal, function.NewArgErrorf(0, "cannot sum an empty list") + } + + arg := args[0].AsValueSlice() + ty := args[0].Type() + + if !ty.IsListType() && !ty.IsSetType() && !ty.IsTupleType() { + return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple. Received %s", ty.FriendlyName()) + } + + if !args[0].IsWhollyKnown() { + return cty.UnknownVal(cty.Number), nil + } + + // big.Float.Add can panic if the input values are opposing infinities, + // so we must catch that here in order to remain within + // the cty Function abstraction. + defer func() { + if r := recover(); r != nil { + if _, ok := r.(big.ErrNaN); ok { + ret = cty.NilVal + err = errors.New("can't compute sum of opposing infinities") + } else { + // not a panic we recognize + panic(r) + } + } + }() + + s := arg[0] + if s.IsNull() { + return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values") + } + s, err = convert.Convert(s, cty.Number) + if err != nil { + return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values") + } + for _, v := range arg[1:] { + if v.IsNull() { + return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values") + } + v, err = convert.Convert(v, cty.Number) + if err != nil { + return cty.NilVal, function.NewArgErrorf(0, "argument must be list, set, or tuple of number values") + } + s = s.Add(v) + } + + return s, nil + }, +}) + +// TransposeFunc constructs a function that takes a map of lists of strings and +// swaps the keys and values to produce a new map of lists of strings. +var TransposeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "values", + Type: cty.Map(cty.List(cty.String)), + }, + }, + Type: function.StaticReturnType(cty.Map(cty.List(cty.String))), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + inputMap := args[0] + if !inputMap.IsWhollyKnown() { + return cty.UnknownVal(retType), nil + } + + outputMap := make(map[string]cty.Value) + tmpMap := make(map[string][]string) + + for it := inputMap.ElementIterator(); it.Next(); { + inKey, inVal := it.Element() + for iter := inVal.ElementIterator(); iter.Next(); { + _, val := iter.Element() + if !val.Type().Equals(cty.String) { + return cty.MapValEmpty(cty.List(cty.String)), errors.New("input must be a map of lists of strings") + } + + outKey := val.AsString() + if _, ok := tmpMap[outKey]; !ok { + tmpMap[outKey] = make([]string, 0) + } + outVal := tmpMap[outKey] + outVal = append(outVal, inKey.AsString()) + sort.Strings(outVal) + tmpMap[outKey] = outVal + } + } + + for outKey, outVal := range tmpMap { + values := make([]cty.Value, 0) + for _, v := range outVal { + values = append(values, cty.StringVal(v)) + } + outputMap[outKey] = cty.ListVal(values) + } + + if len(outputMap) == 0 { + return cty.MapValEmpty(cty.List(cty.String)), nil + } + + return cty.MapVal(outputMap), nil + }, +}) + +// ListFunc constructs a function that takes an arbitrary number of arguments +// and returns a list containing those values in the same order. +// +// This function is deprecated in Terraform v0.12 +var ListFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + VarParam: &function.Parameter{ + Name: "vals", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowDynamicType: true, + AllowNull: true, + }, + Type: func(args []cty.Value) (ret cty.Type, err error) { + return cty.DynamicPseudoType, errors.New("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list") + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + return cty.DynamicVal, errors.New("the \"list\" function was deprecated in Terraform v0.12 and is no longer available; use tolist([ ... ]) syntax to write a literal list") + }, +}) + +// MapFunc constructs a function that takes an even number of arguments and +// returns a map whose elements are constructed from consecutive pairs of arguments. +// +// This function is deprecated in Terraform v0.12 +var MapFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + VarParam: &function.Parameter{ + Name: "vals", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowDynamicType: true, + AllowNull: true, + }, + Type: func(args []cty.Value) (ret cty.Type, err error) { + return cty.DynamicPseudoType, errors.New("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map") + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + return cty.DynamicVal, errors.New("the \"map\" function was deprecated in Terraform v0.12 and is no longer available; use tomap({ ... }) syntax to write a literal map") + }, +}) diff --git a/pkg/iac/scanners/terraform/parser/funcs/conversion.go b/pkg/iac/scanners/terraform/parser/funcs/conversion.go new file mode 100644 index 000000000000..18a1310e69f7 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/conversion.go @@ -0,0 +1,96 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "strconv" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/zclconf/go-cty/cty/function" +) + +// MakeToFunc constructs a "to..." function, like "tostring", which converts +// its argument to a specific type or type kind. +// +// The given type wantTy can be any type constraint that cty's "convert" package +// would accept. In particular, this means that you can pass +// cty.List(cty.DynamicPseudoType) to mean "list of any single type", which +// will then cause cty to attempt to unify all of the element types when given +// a tuple. +func MakeToFunc(wantTy cty.Type) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "v", + // We use DynamicPseudoType rather than wantTy here so that + // all values will pass through the function API verbatim and + // we can handle the conversion logic within the Type and + // Impl functions. This allows us to customize the error + // messages to be more appropriate for an explicit type + // conversion, whereas the cty function system produces + // messages aimed at _implicit_ type conversions. + Type: cty.DynamicPseudoType, + AllowNull: true, + AllowMarked: true, + AllowDynamicType: true, + }, + }, + Type: func(args []cty.Value) (cty.Type, error) { + gotTy := args[0].Type() + if gotTy.Equals(wantTy) { + return wantTy, nil + } + conv := convert.GetConversionUnsafe(args[0].Type(), wantTy) + if conv == nil { + // We'll use some specialized errors for some trickier cases, + // but most we can handle in a simple way. + switch { + case gotTy.IsTupleType() && wantTy.IsTupleType(): + return cty.NilType, function.NewArgErrorf(0, "incompatible tuple type for conversion: %s", convert.MismatchMessage(gotTy, wantTy)) + case gotTy.IsObjectType() && wantTy.IsObjectType(): + return cty.NilType, function.NewArgErrorf(0, "incompatible object type for conversion: %s", convert.MismatchMessage(gotTy, wantTy)) + default: + return cty.NilType, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) + } + } + // If a conversion is available then everything is fine. + return wantTy, nil + }, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + // We didn't set "AllowUnknown" on our argument, so it is guaranteed + // to be known here but may still be null. + ret, err := convert.Convert(args[0], retType) + if err != nil { + val, _ := args[0].UnmarkDeep() + // Because we used GetConversionUnsafe above, conversion can + // still potentially fail in here. For example, if the user + // asks to convert the string "a" to bool then we'll + // optimistically permit it during type checking but fail here + // once we note that the value isn't either "true" or "false". + gotTy := val.Type() + switch { + case Contains(args[0], MarkedSensitive): + // Generic message so we won't inadvertently disclose + // information about sensitive values. + return cty.NilVal, function.NewArgErrorf(0, "cannot convert this sensitive %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) + + case gotTy == cty.String && wantTy == cty.Bool: + what := "string" + if !val.IsNull() { + what = strconv.Quote(val.AsString()) + } + return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to bool; only the strings "true" or "false" are allowed`, what) + case gotTy == cty.String && wantTy == cty.Number: + what := "string" + if !val.IsNull() { + what = strconv.Quote(val.AsString()) + } + return cty.NilVal, function.NewArgErrorf(0, `cannot convert %s to number; given string must be a decimal representation of a number`, what) + default: + return cty.NilVal, function.NewArgErrorf(0, "cannot convert %s to %s", gotTy.FriendlyName(), wantTy.FriendlyNameForConstraint()) + } + } + return ret, nil + }, + }) +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/crypto.go b/pkg/iac/scanners/terraform/parser/funcs/crypto.go new file mode 100644 index 000000000000..a7fe0b654d4e --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/crypto.go @@ -0,0 +1,271 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "crypto/md5" // nolint: gosec + "crypto/rsa" + "crypto/sha1" // nolint: gosec + "crypto/sha256" + "crypto/sha512" + "encoding/asn1" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "hash" + "io" + "io/fs" + "strings" + + uuidv5 "github.com/google/uuid" + uuid "github.com/hashicorp/go-uuid" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/gocty" + "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/ssh" +) + +var UUIDFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + result, err := uuid.GenerateUUID() + if err != nil { + return cty.UnknownVal(cty.String), err + } + return cty.StringVal(result), nil + }, +}) + +var UUIDV5Func = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "namespace", + Type: cty.String, + }, + { + Name: "name", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var namespace uuidv5.UUID + switch { + case args[0].AsString() == "dns": + namespace = uuidv5.NameSpaceDNS + case args[0].AsString() == "url": + namespace = uuidv5.NameSpaceURL + case args[0].AsString() == "oid": + namespace = uuidv5.NameSpaceOID + case args[0].AsString() == "x500": + namespace = uuidv5.NameSpaceX500 + default: + if namespace, err = uuidv5.Parse(args[0].AsString()); err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("uuidv5() doesn't support namespace %s (%v)", args[0].AsString(), err) + } + } + val := args[1].AsString() + return cty.StringVal(uuidv5.NewSHA1(namespace, []byte(val)).String()), nil + }, +}) + +// Base64Sha256Func constructs a function that computes the SHA256 hash of a given string +// and encodes it with Base64. +var Base64Sha256Func = makeStringHashFunction(sha256.New, base64.StdEncoding.EncodeToString) + +// MakeFileBase64Sha256Func constructs a function that is like Base64Sha256Func but reads the +// contents of a file rather than hashing a given literal string. +func MakeFileBase64Sha256Func(target fs.FS, baseDir string) function.Function { + return makeFileHashFunction(target, baseDir, sha256.New, base64.StdEncoding.EncodeToString) +} + +// Base64Sha512Func constructs a function that computes the SHA256 hash of a given string +// and encodes it with Base64. +var Base64Sha512Func = makeStringHashFunction(sha512.New, base64.StdEncoding.EncodeToString) + +// MakeFileBase64Sha512Func constructs a function that is like Base64Sha512Func but reads the +// contents of a file rather than hashing a given literal string. +func MakeFileBase64Sha512Func(target fs.FS, baseDir string) function.Function { + return makeFileHashFunction(target, baseDir, sha512.New, base64.StdEncoding.EncodeToString) +} + +// BcryptFunc constructs a function that computes a hash of the given string using the Blowfish cipher. +var BcryptFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + VarParam: &function.Parameter{ + Name: "cost", + Type: cty.Number, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + defaultCost := 10 + + if len(args) > 1 { + var val int + if err := gocty.FromCtyValue(args[1], &val); err != nil { + return cty.UnknownVal(cty.String), err + } + defaultCost = val + } + + if len(args) > 2 { + return cty.UnknownVal(cty.String), errors.New("bcrypt() takes no more than two arguments") + } + + input := args[0].AsString() + out, err := bcrypt.GenerateFromPassword([]byte(input), defaultCost) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("error occurred generating password %s", err.Error()) + } + + return cty.StringVal(string(out)), nil + }, +}) + +// Md5Func constructs a function that computes the MD5 hash of a given string and encodes it with hexadecimal digits. +var Md5Func = makeStringHashFunction(md5.New, hex.EncodeToString) + +// MakeFileMd5Func constructs a function that is like Md5Func but reads the +// contents of a file rather than hashing a given literal string. +func MakeFileMd5Func(target fs.FS, baseDir string) function.Function { + return makeFileHashFunction(target, baseDir, md5.New, hex.EncodeToString) +} + +// RsaDecryptFunc constructs a function that decrypts an RSA-encrypted ciphertext. +var RsaDecryptFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "ciphertext", + Type: cty.String, + }, + { + Name: "privatekey", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + s := args[0].AsString() + key := args[1].AsString() + + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "failed to decode input %q: cipher text must be base64-encoded", s) + } + + rawKey, err := ssh.ParseRawPrivateKey([]byte(key)) + if err != nil { + var errStr string + switch e := err.(type) { + case asn1.SyntaxError: + errStr = strings.ReplaceAll(e.Error(), "asn1: syntax error", "invalid ASN1 data in the given private key") + case asn1.StructuralError: + errStr = strings.ReplaceAll(e.Error(), "asn1: structure error", "invalid ASN1 data in the given private key") + default: + errStr = fmt.Sprintf("invalid private key: %s", e) + } + return cty.UnknownVal(cty.String), function.NewArgError(1, errors.New(errStr)) + } + privateKey, ok := rawKey.(*rsa.PrivateKey) + if !ok { + return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "invalid private key type %t", rawKey) + } + + out, err := rsa.DecryptPKCS1v15(nil, privateKey, b) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to decrypt: %s", err) + } + + return cty.StringVal(string(out)), nil + }, +}) + +// Sha1Func constructs a function that computes the SHA1 hash of a given string +// and encodes it with hexadecimal digits. +var Sha1Func = makeStringHashFunction(sha1.New, hex.EncodeToString) + +// MakeFileSha1Func constructs a function that is like Sha1Func but reads the +// contents of a file rather than hashing a given literal string. +func MakeFileSha1Func(target fs.FS, baseDir string) function.Function { + return makeFileHashFunction(target, baseDir, sha1.New, hex.EncodeToString) +} + +// Sha256Func constructs a function that computes the SHA256 hash of a given string +// and encodes it with hexadecimal digits. +var Sha256Func = makeStringHashFunction(sha256.New, hex.EncodeToString) + +// MakeFileSha256Func constructs a function that is like Sha256Func but reads the +// contents of a file rather than hashing a given literal string. +func MakeFileSha256Func(target fs.FS, baseDir string) function.Function { + return makeFileHashFunction(target, baseDir, sha256.New, hex.EncodeToString) +} + +// Sha512Func constructs a function that computes the SHA512 hash of a given string +// and encodes it with hexadecimal digits. +var Sha512Func = makeStringHashFunction(sha512.New, hex.EncodeToString) + +// MakeFileSha512Func constructs a function that is like Sha512Func but reads the +// contents of a file rather than hashing a given literal string. +func MakeFileSha512Func(target fs.FS, baseDir string) function.Function { + return makeFileHashFunction(target, baseDir, sha512.New, hex.EncodeToString) +} + +func makeStringHashFunction(hf func() hash.Hash, enc func([]byte) string) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + s := args[0].AsString() + h := hf() + h.Write([]byte(s)) + rv := enc(h.Sum(nil)) + return cty.StringVal(rv), nil + }, + }) +} + +func makeFileHashFunction(target fs.FS, baseDir string, hf func() hash.Hash, enc func([]byte) string) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + path := args[0].AsString() + f, err := openFile(target, baseDir, path) + if err != nil { + return cty.UnknownVal(cty.String), err + } + defer f.Close() + + h := hf() + _, err = io.Copy(h, f) + if err != nil { + return cty.UnknownVal(cty.String), err + } + rv := enc(h.Sum(nil)) + return cty.StringVal(rv), nil + }, + }) +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/datetime.go b/pkg/iac/scanners/terraform/parser/funcs/datetime.go new file mode 100644 index 000000000000..12d73d58ba1c --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/datetime.go @@ -0,0 +1,159 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "fmt" + "time" + + "golang.org/x/xerrors" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// TimestampFunc constructs a function that returns a string representation of the current date and time. +var TimestampFunc = function.New(&function.Spec{ + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(time.Now().UTC().Format(time.RFC3339)), nil + }, +}) + +// MakeStaticTimestampFunc constructs a function that returns a string +// representation of the date and time specified by the provided argument. +func MakeStaticTimestampFunc(static time.Time) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{}, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(static.Format(time.RFC3339)), nil + }, + }) +} + +// TimeAddFunc constructs a function that adds a duration to a timestamp, returning a new timestamp. +var TimeAddFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "timestamp", + Type: cty.String, + }, + { + Name: "duration", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + ts, err := parseTimestamp(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), err + } + duration, err := time.ParseDuration(args[1].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.StringVal(ts.Add(duration).Format(time.RFC3339)), nil + }, +}) + +// TimeCmpFunc is a function that compares two timestamps. +var TimeCmpFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "timestamp_a", + Type: cty.String, + }, + { + Name: "timestamp_b", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + tsA, err := parseTimestamp(args[0].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(0, err) + } + tsB, err := parseTimestamp(args[1].AsString()) + if err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(1, err) + } + + switch { + case tsA.Equal(tsB): + return cty.NumberIntVal(0), nil + case tsA.Before(tsB): + return cty.NumberIntVal(-1), nil + default: + // By elimintation, tsA must be after tsB. + return cty.NumberIntVal(1), nil + } + }, +}) + +func parseTimestamp(ts string) (time.Time, error) { + t, err := time.Parse(time.RFC3339, ts) + if err != nil { + switch err := err.(type) { + case *time.ParseError: + // If err is a time.ParseError then its string representation is not + // appropriate since it relies on details of Go's strange date format + // representation, which a caller of our functions is not expected + // to be familiar with. + // + // Therefore we do some light transformation to get a more suitable + // error that should make more sense to our callers. These are + // still not awesome error messages, but at least they refer to + // the timestamp portions by name rather than by Go's example + // values. + if err.LayoutElem == "" && err.ValueElem == "" && err.Message != "" { + // For some reason err.Message is populated with a ": " prefix + // by the time package. + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp%s", err.Message) + } + var what string + switch err.LayoutElem { + case "2006": + what = "year" + case "01": + what = "month" + case "02": + what = "day of month" + case "15": + what = "hour" + case "04": + what = "minute" + case "05": + what = "second" + case "Z07:00": + what = "UTC offset" + case "T": + return time.Time{}, xerrors.New("not a valid RFC3339 timestamp: missing required time introducer 'T'") + case ":", "-": + if err.ValueElem == "" { + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string where %q is expected", err.LayoutElem) + } else { + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: found %q where %q is expected", err.ValueElem, err.LayoutElem) + } + default: + // Should never get here, because time.RFC3339 includes only the + // above portions, but since that might change in future we'll + // be robust here. + what = "timestamp segment" + } + if err.ValueElem == "" { + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: end of string before %s", what) + } else { + return time.Time{}, fmt.Errorf("not a valid RFC3339 timestamp: cannot use %q as %s", err.ValueElem, what) + } + } + return time.Time{}, err + } + return t, nil +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/encoding.go b/pkg/iac/scanners/terraform/parser/funcs/encoding.go new file mode 100644 index 000000000000..75876dfd33f7 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/encoding.go @@ -0,0 +1,193 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "bytes" + "compress/gzip" + "encoding/base64" + "errors" + "fmt" + "log" + "net/url" + "unicode/utf8" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + "golang.org/x/text/encoding/ianaindex" +) + +// Base64DecodeFunc constructs a function that decodes a string containing a base64 sequence. +var Base64DecodeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + AllowMarked: true, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + str, strMarks := args[0].Unmark() + s := str.AsString() + sDec, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to decode base64 data %s", redactIfSensitive(s, strMarks)) + } + if !utf8.Valid([]byte(sDec)) { + log.Printf("[DEBUG] the result of decoding the provided string is not valid UTF-8: %s", redactIfSensitive(sDec, strMarks)) + return cty.UnknownVal(cty.String), errors.New("the result of decoding the provided string is not valid UTF-8") + } + return cty.StringVal(string(sDec)).WithMarks(strMarks), nil + }, +}) + +// Base64EncodeFunc constructs a function that encodes a string to a base64 sequence. +var Base64EncodeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(base64.StdEncoding.EncodeToString([]byte(args[0].AsString()))), nil + }, +}) + +// TextEncodeBase64Func constructs a function that encodes a string to a target encoding and then to a base64 sequence. +var TextEncodeBase64Func = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "string", + Type: cty.String, + }, + { + Name: "encoding", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + encoding, err := ianaindex.IANA.Encoding(args[1].AsString()) + if err != nil || encoding == nil { + return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias in this Terraform version", args[1].AsString()) + } + + encName, err := ianaindex.IANA.Name(encoding) + if err != nil { // would be weird, since we just read this encoding out + encName = args[1].AsString() + } + + encoder := encoding.NewEncoder() + encodedInput, err := encoder.Bytes([]byte(args[0].AsString())) + if err != nil { + // The string representations of "err" disclose implementation + // details of the underlying library, and the main error we might + // like to return a special message for is unexported as + // golang.org/x/text/encoding/internal.RepertoireError, so this + // is just a generic error message for now. + // + // We also don't include the string itself in the message because + // it can typically be very large, contain newline characters, + // etc. + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains characters that cannot be represented in %s", encName) + } + + return cty.StringVal(base64.StdEncoding.EncodeToString(encodedInput)), nil + }, +}) + +// TextDecodeBase64Func constructs a function that decodes a base64 sequence to a target encoding. +var TextDecodeBase64Func = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "source", + Type: cty.String, + }, + { + Name: "encoding", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + encoding, err := ianaindex.IANA.Encoding(args[1].AsString()) + if err != nil || encoding == nil { + return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "%q is not a supported IANA encoding name or alias in this Terraform version", args[1].AsString()) + } + + encName, err := ianaindex.IANA.Name(encoding) + if err != nil { // would be weird, since we just read this encoding out + encName = args[1].AsString() + } + + s := args[0].AsString() + sDec, err := base64.StdEncoding.DecodeString(s) + if err != nil { + switch err := err.(type) { + case base64.CorruptInputError: + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given value is has an invalid base64 symbol at offset %d", int(err)) + default: + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "invalid source string: %w", err) + } + + } + + decoder := encoding.NewDecoder() + decoded, err := decoder.Bytes(sDec) + if err != nil || bytes.ContainsRune(decoded, '�') { + return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "the given string contains symbols that are not defined for %s", encName) + } + + return cty.StringVal(string(decoded)), nil + }, +}) + +// Base64GzipFunc constructs a function that compresses a string with gzip and then encodes the result in +// Base64 encoding. +var Base64GzipFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + s := args[0].AsString() + + var b bytes.Buffer + gz := gzip.NewWriter(&b) + if _, err := gz.Write([]byte(s)); err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to write gzip raw data: %w", err) + } + if err := gz.Flush(); err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to flush gzip writer: %w", err) + } + if err := gz.Close(); err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("failed to close gzip writer: %w", err) + } + return cty.StringVal(base64.StdEncoding.EncodeToString(b.Bytes())), nil + }, +}) + +// URLEncodeFunc constructs a function that applies URL encoding to a given string. +var URLEncodeFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(url.QueryEscape(args[0].AsString())), nil + }, +}) diff --git a/pkg/iac/scanners/terraform/parser/funcs/filesystem.go b/pkg/iac/scanners/terraform/parser/funcs/filesystem.go new file mode 100644 index 000000000000..b338c3ef505a --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/filesystem.go @@ -0,0 +1,406 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "encoding/base64" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "unicode/utf8" + + "github.com/bmatcuk/doublestar/v4" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/mitchellh/go-homedir" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// MakeFileFunc constructs a function that takes a file path and returns the +// contents of that file, either directly as a string (where valid UTF-8 is +// required) or as a string containing base64 bytes. +func MakeFileFunc(target fs.FS, baseDir string, encBase64 bool) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + path := args[0].AsString() + src, err := readFileBytes(target, baseDir, path) + if err != nil { + err = function.NewArgError(0, err) + return cty.UnknownVal(cty.String), err + } + + switch { + case encBase64: + enc := base64.StdEncoding.EncodeToString(src) + return cty.StringVal(enc), nil + default: + if !utf8.Valid(src) { + return cty.UnknownVal(cty.String), fmt.Errorf("contents of %s are not valid UTF-8; use the filebase64 function to obtain the Base64 encoded contents or the other file functions (e.g. filemd5, filesha256) to obtain file hashing results instead", path) + } + return cty.StringVal(string(src)), nil + } + }, + }) +} + +// MakeTemplateFileFunc constructs a function that takes a file path and +// an arbitrary object of named values and attempts to render the referenced +// file as a template using HCL template syntax. +// +// The template itself may recursively call other functions so a callback +// must be provided to get access to those functions. The template cannot, +// however, access any variables defined in the scope: it is restricted only to +// those variables provided in the second function argument, to ensure that all +// dependencies on other graph nodes can be seen before executing this function. +// +// As a special exception, a referenced template file may not recursively call +// the templatefile function, since that would risk the same file being +// included into itself indefinitely. +func MakeTemplateFileFunc(target fs.FS, baseDir string, funcsCb func() map[string]function.Function) function.Function { + + params := []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + { + Name: "vars", + Type: cty.DynamicPseudoType, + }, + } + + loadTmpl := func(fn string) (hcl.Expression, error) { + // We re-use File here to ensure the same filename interpretation + // as it does, along with its other safety checks. + tmplVal, err := File(target, baseDir, cty.StringVal(fn)) + if err != nil { + return nil, err + } + + expr, diags := hclsyntax.ParseTemplate([]byte(tmplVal.AsString()), fn, hcl.Pos{Line: 1, Column: 1}) + if diags.HasErrors() { + return nil, diags + } + + return expr, nil + } + + renderTmpl := func(expr hcl.Expression, varsVal cty.Value) (cty.Value, error) { + if varsTy := varsVal.Type(); !(varsTy.IsMapType() || varsTy.IsObjectType()) { + return cty.DynamicVal, function.NewArgErrorf(1, "invalid vars value: must be a map") // or an object, but we don't strongly distinguish these most of the time + } + + ctx := &hcl.EvalContext{ + Variables: varsVal.AsValueMap(), + } + + // We require all of the variables to be valid HCL identifiers, because + // otherwise there would be no way to refer to them in the template + // anyway. Rejecting this here gives better feedback to the user + // than a syntax error somewhere in the template itself. + for n := range ctx.Variables { + if !hclsyntax.ValidIdentifier(n) { + // This error message intentionally doesn't describe _all_ of + // the different permutations that are technically valid as an + // HCL identifier, but rather focuses on what we might + // consider to be an "idiomatic" variable name. + return cty.DynamicVal, function.NewArgErrorf(1, "invalid template variable name %q: must start with a letter, followed by zero or more letters, digits, and underscores", n) + } + } + + // We'll pre-check references in the template here so we can give a + // more specialized error message than HCL would by default, so it's + // clearer that this problem is coming from a templatefile call. + for _, traversal := range expr.Variables() { + root := traversal.RootName() + if _, ok := ctx.Variables[root]; !ok { + return cty.DynamicVal, function.NewArgErrorf(1, "vars map does not contain key %q, referenced at %s", root, traversal[0].SourceRange()) + } + } + + givenFuncs := funcsCb() // this callback indirection is to avoid chicken/egg problems + funcs := make(map[string]function.Function, len(givenFuncs)) + for name, fn := range givenFuncs { + if name == "templatefile" { + // We stub this one out to prevent recursive calls. + funcs[name] = function.New(&function.Spec{ + Params: params, + Type: func(args []cty.Value) (cty.Type, error) { + return cty.NilType, errors.New("cannot recursively call templatefile from inside templatefile call") + }, + }) + continue + } + funcs[name] = fn + } + ctx.Functions = funcs + + val, diags := expr.Value(ctx) + if diags.HasErrors() { + return cty.DynamicVal, diags + } + return val, nil + } + + return function.New(&function.Spec{ + Params: params, + Type: func(args []cty.Value) (cty.Type, error) { + if !(args[0].IsKnown() && args[1].IsKnown()) { + return cty.DynamicPseudoType, nil + } + + // We'll render our template now to see what result type it produces. + // A template consisting only of a single interpolation an potentially + // return any type. + expr, err := loadTmpl(args[0].AsString()) + if err != nil { + return cty.DynamicPseudoType, err + } + + // This is safe even if args[1] contains unknowns because the HCL + // template renderer itself knows how to short-circuit those. + val, err := renderTmpl(expr, args[1]) + return val.Type(), err + }, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + expr, err := loadTmpl(args[0].AsString()) + if err != nil { + return cty.DynamicVal, err + } + return renderTmpl(expr, args[1]) + }, + }) + +} + +// MakeFileExistsFunc constructs a function that takes a path +// and determines whether a file exists at that path +func MakeFileExistsFunc(target fs.FS, baseDir string) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + path := args[0].AsString() + path, err := homedir.Expand(path) + if err != nil { + return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to expand ~: %s", err) + } + + if !filepath.IsAbs(path) { + path = filepath.Join(baseDir, path) + } + + // Trivy uses a virtual file system + path = filepath.ToSlash(path) + + fi, err := fs.Stat(target, path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return cty.False, nil + } + return cty.UnknownVal(cty.Bool), fmt.Errorf("failed to stat %s", path) + } + + if fi.Mode().IsRegular() { + return cty.True, nil + } + + return cty.False, fmt.Errorf("%s is not a regular file, but %q", + path, fi.Mode().String()) + }, + }) +} + +// MakeFileSetFunc constructs a function that takes a glob pattern +// and enumerates a file set from that pattern +func MakeFileSetFunc(target fs.FS, baseDir string) function.Function { + return function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + { + Name: "pattern", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Set(cty.String)), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + path := args[0].AsString() + pattern := args[1].AsString() + + if !filepath.IsAbs(path) { + path = filepath.Join(baseDir, path) + } + + // Join the path to the glob pattern, and ensure both path and pattern + // agree on path separators, so the globbing works as expected. + pattern = filepath.ToSlash(filepath.Join(path, pattern)) + path = filepath.ToSlash(path) + + matches, err := doublestar.Glob(target, pattern) + if err != nil { + return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to glob pattern (%s): %s", pattern, err) + } + + var matchVals []cty.Value + for _, match := range matches { + fi, err := fs.Stat(target, match) + + if err != nil { + return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to stat (%s): %s", match, err) + } + + if !fi.Mode().IsRegular() { + continue + } + + // Remove the path and file separator from matches. + match, err = filepath.Rel(path, match) + + if err != nil { + return cty.UnknownVal(cty.Set(cty.String)), fmt.Errorf("failed to trim path of match (%s): %s", match, err) + } + + // Replace any remaining file separators with forward slash (/) + // separators for cross-system compatibility. + match = filepath.ToSlash(match) + + matchVals = append(matchVals, cty.StringVal(match)) + } + + if len(matchVals) == 0 { + return cty.SetValEmpty(cty.String), nil + } + + return cty.SetVal(matchVals), nil + }, + }) +} + +// BasenameFunc constructs a function that takes a string containing a filesystem path +// and removes all except the last portion from it. +var BasenameFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(filepath.Base(args[0].AsString())), nil + }, +}) + +// DirnameFunc constructs a function that takes a string containing a filesystem path +// and removes the last portion from it. +var DirnameFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + return cty.StringVal(filepath.Dir(args[0].AsString())), nil + }, +}) + +// AbsPathFunc constructs a function that converts a filesystem path to an absolute path +var AbsPathFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + absPath, err := filepath.Abs(args[0].AsString()) + return cty.StringVal(filepath.ToSlash(absPath)), err + }, +}) + +// PathExpandFunc constructs a function that expands a leading ~ character to the current user's home directory. +var PathExpandFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "path", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + + homePath, err := homedir.Expand(args[0].AsString()) + return cty.StringVal(homePath), err + }, +}) + +func openFile(target fs.FS, baseDir, path string) (fs.File, error) { + path, err := homedir.Expand(path) + if err != nil { + return nil, fmt.Errorf("failed to expand ~: %s", err) + } + + if !filepath.IsAbs(path) { + path = filepath.Join(baseDir, path) + } + + // Trivy uses a virtual file system + path = filepath.ToSlash(path) + + if target == nil { + return nil, fmt.Errorf("open file %q, filesystem is nil", path) + } + + return target.Open(path) +} + +func readFileBytes(target fs.FS, baseDir, path string) ([]byte, error) { + f, err := openFile(target, baseDir, path) + if err != nil { + if os.IsNotExist(err) { + // An extra Terraform-specific hint for this situation + return nil, fmt.Errorf("no file exists at %s; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource", path) + } + return nil, err + } + + src, err := io.ReadAll(f) + if err != nil { + return nil, fmt.Errorf("failed to read %s", path) + } + + return src, nil +} + +// File reads the contents of the file at the given path. +// +// The file must contain valid UTF-8 bytes, or this function will return an error. +// +// The underlying function implementation works relative to a particular base +// directory, so this wrapper takes a base directory string and uses it to +// construct the underlying function before calling it. +func File(target fs.FS, baseDir string, path cty.Value) (cty.Value, error) { + fn := MakeFileFunc(target, baseDir, false) + return fn.Call([]cty.Value{path}) +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/ip.go b/pkg/iac/scanners/terraform/parser/funcs/ip.go new file mode 100644 index 000000000000..6bdd0feb00a7 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/ip.go @@ -0,0 +1,261 @@ +// Copied from github.com/hashicorp/terraform/internal/ipaddr +package funcs + +import ( + stdnet "net" +) + +// Bigger than we need, not too big to worry about overflow +const bigVal = 0xFFFFFF + +// Decimal to integer. +// Returns number, characters consumed, success. +func dtoi(s string) (n int, i int, ok bool) { + n = 0 + for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { + n = n*10 + int(s[i]-'0') + if n >= bigVal { + return bigVal, i, false + } + } + if i == 0 { + return 0, 0, false + } + return n, i, true +} + +// Hexadecimal to integer. +// Returns number, characters consumed, success. +func xtoi(s string) (n int, i int, ok bool) { + n = 0 + for i = 0; i < len(s); i++ { + if '0' <= s[i] && s[i] <= '9' { + n *= 16 + n += int(s[i] - '0') + } else if 'a' <= s[i] && s[i] <= 'f' { + n *= 16 + n += int(s[i]-'a') + 10 + } else if 'A' <= s[i] && s[i] <= 'F' { + n *= 16 + n += int(s[i]-'A') + 10 + } else { + break + } + if n >= bigVal { + return 0, i, false + } + } + if i == 0 { + return 0, i, false + } + return n, i, true +} + +// +// Lean on the standard net lib as much as possible. +// + +type IP = stdnet.IP +type IPNet = stdnet.IPNet +type ParseError = stdnet.ParseError + +const IPv4len = stdnet.IPv4len +const IPv6len = stdnet.IPv6len + +var CIDRMask = stdnet.CIDRMask +var IPv4 = stdnet.IPv4 + +// Parse IPv4 address (d.d.d.d). +func parseIPv4(s string) IP { + var p [IPv4len]byte + for i := range IPv4len { + if len(s) == 0 { + // Missing octets. + return nil + } + if i > 0 { + if s[0] != '.' { + return nil + } + s = s[1:] + } + n, c, ok := dtoi(s) + if !ok || n > 0xFF { + return nil + } + // + // NOTE: This correct check was added for go-1.17, but is a + // backwards-incompatible change for Terraform users, who might have + // already written modules with leading zeroes. + // + // if c > 1 && s[0] == '0' { + // // Reject non-zero components with leading zeroes. + // return nil + //} + s = s[c:] + p[i] = byte(n) + } + if len(s) != 0 { + return nil + } + return IPv4(p[0], p[1], p[2], p[3]) +} + +// parseIPv6 parses s as a literal IPv6 address described in RFC 4291 +// and RFC 5952. +func parseIPv6(s string) (ip IP) { + ip = make(IP, IPv6len) + ellipsis := -1 // position of ellipsis in ip + + // Might have leading ellipsis + if len(s) >= 2 && s[0] == ':' && s[1] == ':' { + ellipsis = 0 + s = s[2:] + // Might be only ellipsis + if len(s) == 0 { + return ip + } + } + + // Loop, parsing hex numbers followed by colon. + i := 0 + for i < IPv6len { + // Hex number. + n, c, ok := xtoi(s) + if !ok || n > 0xFFFF { + return nil + } + + // If followed by dot, might be in trailing IPv4. + if c < len(s) && s[c] == '.' { + if ellipsis < 0 && i != IPv6len-IPv4len { + // Not the right place. + return nil + } + if i+IPv4len > IPv6len { + // Not enough room. + return nil + } + ip4 := parseIPv4(s) + if ip4 == nil { + return nil + } + ip[i] = ip4[12] + ip[i+1] = ip4[13] + ip[i+2] = ip4[14] + ip[i+3] = ip4[15] + s = "" + i += IPv4len + break + } + + // Save this 16-bit chunk. + ip[i] = byte(n >> 8) + ip[i+1] = byte(n) + i += 2 + + // Stop at end of string. + s = s[c:] + if len(s) == 0 { + break + } + + // Otherwise must be followed by colon and more. + if s[0] != ':' || len(s) == 1 { + return nil + } + s = s[1:] + + // Look for ellipsis. + if s[0] == ':' { + if ellipsis >= 0 { // already have one + return nil + } + ellipsis = i + s = s[1:] + if len(s) == 0 { // can be at end + break + } + } + } + + // Must have used entire string. + if len(s) != 0 { + return nil + } + + // If didn't parse enough, expand ellipsis. + if i < IPv6len { + if ellipsis < 0 { + return nil + } + n := IPv6len - i + for j := i - 1; j >= ellipsis; j-- { + ip[j+n] = ip[j] + } + for j := ellipsis + n - 1; j >= ellipsis; j-- { + ip[j] = 0 + } + } else if ellipsis >= 0 { + // Ellipsis must represent at least one 0 group. + return nil + } + return ip +} + +// ParseIP parses s as an IP address, returning the result. +// The string s can be in IPv4 dotted decimal ("192.0.2.1"), IPv6 +// ("2001:db8::68"), or IPv4-mapped IPv6 ("::ffff:192.0.2.1") form. +// If s is not a valid textual representation of an IP address, +// ParseIP returns nil. +func ParseIP(s string) IP { + for i := 0; i < len(s); i++ { + switch s[i] { + case '.': + return parseIPv4(s) + case ':': + return parseIPv6(s) + } + } + return nil +} + +// ParseCIDR parses s as a CIDR notation IP address and prefix length, +// like "192.0.2.0/24" or "2001:db8::/32", as defined in +// RFC 4632 and RFC 4291. +// +// It returns the IP address and the network implied by the IP and +// prefix length. +// For example, ParseCIDR("192.0.2.1/24") returns the IP address +// 192.0.2.1 and the network 192.0.2.0/24. +func ParseCIDR(s string) (IP, *IPNet, error) { + i := indexByteString(s, '/') + if i < 0 { + return nil, nil, &ParseError{Type: "CIDR address", Text: s} + } + addr, mask := s[:i], s[i+1:] + iplen := IPv4len + ip := parseIPv4(addr) + if ip == nil { + iplen = IPv6len + ip = parseIPv6(addr) + } + n, i, ok := dtoi(mask) + if ip == nil || !ok || i != len(mask) || n < 0 || n > 8*iplen { + return nil, nil, &ParseError{Type: "CIDR address", Text: s} + } + m := CIDRMask(n, 8*iplen) + return ip, &IPNet{IP: ip.Mask(m), Mask: m}, nil +} + +// This is copied from go/src/internal/bytealg, which includes versions +// optimized for various platforms. Those optimizations are elided here so we +// don't have to maintain them. +func indexByteString(s string, c byte) int { + for i := 0; i < len(s); i++ { + if s[i] == c { + return i + } + } + return -1 +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/marks.go b/pkg/iac/scanners/terraform/parser/funcs/marks.go new file mode 100644 index 000000000000..abbc397f1e08 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/marks.go @@ -0,0 +1,44 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/marks +package funcs + +import ( + "github.com/zclconf/go-cty/cty" + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// valueMarks allow creating strictly typed values for use as cty.Value marks. +// The variable name for new values should be the title-cased format of the +// value to better match the GoString output for debugging. +type valueMark string + +func (m valueMark) GoString() string { + return "marks." + cases.Title(language.English).String(string(m)) +} + +// Has returns true if and only if the cty.Value has the given mark. +func Has(val cty.Value, mark valueMark) bool { + return val.HasMark(mark) +} + +// Contains returns true if the cty.Value or any any value within it contains +// the given mark. +func Contains(val cty.Value, mark valueMark) bool { + ret := false + _ = cty.Walk(val, func(_ cty.Path, v cty.Value) (bool, error) { + if v.HasMark(mark) { + ret = true + return false, nil + } + return true, nil + }) + return ret +} + +// MarkedSensitive indicates that this value is marked as sensitive in the context of +// Terraform. +const MarkedSensitive = valueMark("sensitive") + +// MarkedRaw is used to indicate to the repl that the value should be written without +// any formatting. +const MarkedRaw = valueMark("raw") diff --git a/pkg/iac/scanners/terraform/parser/funcs/number.go b/pkg/iac/scanners/terraform/parser/funcs/number.go new file mode 100644 index 000000000000..60ebd660bf18 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/number.go @@ -0,0 +1,157 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "math" + "math/big" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/gocty" +) + +// LogFunc constructs a function that returns the logarithm of a given number in a given base. +var LogFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "num", + Type: cty.Number, + }, + { + Name: "base", + Type: cty.Number, + }, + }, + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var num float64 + if err := gocty.FromCtyValue(args[0], &num); err != nil { + return cty.UnknownVal(cty.String), err + } + + var base float64 + if err := gocty.FromCtyValue(args[1], &base); err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.NumberFloatVal(math.Log(num) / math.Log(base)), nil + }, +}) + +// PowFunc constructs a function that returns the logarithm of a given number in a given base. +var PowFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "num", + Type: cty.Number, + }, + { + Name: "power", + Type: cty.Number, + }, + }, + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var num float64 + if err := gocty.FromCtyValue(args[0], &num); err != nil { + return cty.UnknownVal(cty.String), err + } + + var power float64 + if err := gocty.FromCtyValue(args[1], &power); err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.NumberFloatVal(math.Pow(num, power)), nil + }, +}) + +// SignumFunc constructs a function that returns the closest whole number greater +// than or equal to the given value. +var SignumFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "num", + Type: cty.Number, + }, + }, + Type: function.StaticReturnType(cty.Number), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + var num int + if err := gocty.FromCtyValue(args[0], &num); err != nil { + return cty.UnknownVal(cty.String), err + } + switch { + case num < 0: + return cty.NumberIntVal(-1), nil + case num > 0: + return cty.NumberIntVal(+1), nil + default: + return cty.NumberIntVal(0), nil + } + }, +}) + +// ParseIntFunc constructs a function that parses a string argument and returns an integer of the specified base. +var ParseIntFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "number", + Type: cty.DynamicPseudoType, + AllowMarked: true, + }, + { + Name: "base", + Type: cty.Number, + AllowMarked: true, + }, + }, + + Type: func(args []cty.Value) (cty.Type, error) { + if !args[0].Type().Equals(cty.String) { + return cty.Number, function.NewArgErrorf(0, "first argument must be a string, not %s", args[0].Type().FriendlyName()) + } + return cty.Number, nil + }, + RefineResult: refineNotNull, + + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + var numstr string + var base int + var err error + + numArg, numMarks := args[0].Unmark() + if err = gocty.FromCtyValue(numArg, &numstr); err != nil { + return cty.UnknownVal(cty.String), function.NewArgError(0, err) + } + + baseArg, baseMarks := args[1].Unmark() + if err = gocty.FromCtyValue(baseArg, &base); err != nil { + return cty.UnknownVal(cty.Number), function.NewArgError(1, err) + } + + if base < 2 || base > 62 { + return cty.UnknownVal(cty.Number), function.NewArgErrorf( + 1, + "base must be a whole number between 2 and 62 inclusive", + ) + } + + num, ok := (&big.Int{}).SetString(numstr, base) + if !ok { + return cty.UnknownVal(cty.Number), function.NewArgErrorf( + 0, + "cannot parse %s as a base %s integer", + redactIfSensitive(numstr, numMarks), + redactIfSensitive(base, baseMarks), + ) + } + + parsedNum := cty.NumberVal((&big.Float{}).SetInt(num)).WithMarks(numMarks, baseMarks) + + return parsedNum, nil + }, +}) diff --git a/pkg/iac/scanners/terraform/parser/funcs/redact.go b/pkg/iac/scanners/terraform/parser/funcs/redact.go new file mode 100644 index 000000000000..9f1e8d3551f6 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/redact.go @@ -0,0 +1,20 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "fmt" + + "github.com/zclconf/go-cty/cty" +) + +func redactIfSensitive(value any, markses ...cty.ValueMarks) string { + if Has(cty.DynamicVal.WithMarks(markses...), MarkedSensitive) { + return "(sensitive value)" + } + switch v := value.(type) { + case string: + return fmt.Sprintf("%q", v) + default: + return fmt.Sprintf("%v", v) + } +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/refinements.go b/pkg/iac/scanners/terraform/parser/funcs/refinements.go new file mode 100644 index 000000000000..de9cb08b1604 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/refinements.go @@ -0,0 +1,10 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "github.com/zclconf/go-cty/cty" +) + +func refineNotNull(b *cty.RefinementBuilder) *cty.RefinementBuilder { + return b.NotNull() +} diff --git a/pkg/iac/scanners/terraform/parser/funcs/sensitive.go b/pkg/iac/scanners/terraform/parser/funcs/sensitive.go new file mode 100644 index 000000000000..3566a678fc9b --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/sensitive.go @@ -0,0 +1,74 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// SensitiveFunc returns a value identical to its argument except that +// Terraform will consider it to be sensitive. +var SensitiveFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowNull: true, + AllowMarked: true, + AllowDynamicType: true, + }, + }, + Type: func(args []cty.Value) (cty.Type, error) { + // This function only affects the value's marks, so the result + // type is always the same as the argument type. + return args[0].Type(), nil + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + val, _ := args[0].Unmark() + return val.Mark(MarkedSensitive), nil + }, +}) + +// NonsensitiveFunc takes a sensitive value and returns the same value without +// the sensitive marking, effectively exposing the value. +var NonsensitiveFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowNull: true, + AllowMarked: true, + AllowDynamicType: true, + }, + }, + Type: func(args []cty.Value) (cty.Type, error) { + // This function only affects the value's marks, so the result + // type is always the same as the argument type. + return args[0].Type(), nil + }, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + v, marks := args[0].Unmark() + delete(marks, MarkedSensitive) // remove the sensitive marking + return v.WithMarks(marks), nil + }, +}) + +var IssensitiveFunc = function.New(&function.Spec{ + Params: []function.Parameter{{ + Name: "value", + Type: cty.DynamicPseudoType, + AllowUnknown: true, + AllowNull: true, + AllowMarked: true, + AllowDynamicType: true, + }}, + Type: func(args []cty.Value) (cty.Type, error) { + return cty.Bool, nil + }, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + s := args[0].HasMark(MarkedSensitive) + return cty.BoolVal(s), nil + }, +}) diff --git a/pkg/iac/scanners/terraform/parser/funcs/string.go b/pkg/iac/scanners/terraform/parser/funcs/string.go new file mode 100644 index 000000000000..f859c7af7a31 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/funcs/string.go @@ -0,0 +1,152 @@ +// Copied from github.com/hashicorp/terraform/internal/lang/funcs +package funcs + +import ( + "regexp" + "strings" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" +) + +// StartsWithFunc constructs a function that checks if a string starts with +// a specific prefix using strings.HasPrefix +var StartsWithFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + AllowUnknown: true, + }, + { + Name: "prefix", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + prefix := args[1].AsString() + + if !args[0].IsKnown() { + // If the unknown value has a known prefix then we might be + // able to still produce a known result. + if prefix == "" { + // The empty string is a prefix of any string. + return cty.True, nil + } + if knownPrefix := args[0].Range().StringPrefix(); knownPrefix != "" { + if strings.HasPrefix(knownPrefix, prefix) { + return cty.True, nil + } + if len(knownPrefix) >= len(prefix) { + // If the prefix we're testing is no longer than the known + // prefix and it didn't match then the full string with + // that same prefix can't match either. + return cty.False, nil + } + } + return cty.UnknownVal(cty.Bool), nil + } + + str := args[0].AsString() + + if strings.HasPrefix(str, prefix) { + return cty.True, nil + } + + return cty.False, nil + }, +}) + +// EndsWithFunc constructs a function that checks if a string ends with +// a specific suffix using strings.HasSuffix +var EndsWithFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + { + Name: "suffix", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { + str := args[0].AsString() + suffix := args[1].AsString() + + if strings.HasSuffix(str, suffix) { + return cty.True, nil + } + + return cty.False, nil + }, +}) + +// ReplaceFunc constructs a function that searches a given string for another +// given substring, and replaces each occurrence with a given replacement string. +var ReplaceFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + { + Name: "substr", + Type: cty.String, + }, + { + Name: "replace", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + RefineResult: refineNotNull, + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + str := args[0].AsString() + substr := args[1].AsString() + replace := args[2].AsString() + + // We search/replace using a regexp if the string is surrounded + // in forward slashes. + if len(substr) > 1 && substr[0] == '/' && substr[len(substr)-1] == '/' { + re, err := regexp.Compile(substr[1 : len(substr)-1]) + if err != nil { + return cty.UnknownVal(cty.String), err + } + + return cty.StringVal(re.ReplaceAllString(str, replace)), nil + } + + return cty.StringVal(strings.Replace(str, substr, replace, -1)), nil + }, +}) + +// StrContainsFunc searches a given string for another given substring, +// if found the function returns true, otherwise returns false. +var StrContainsFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + { + Name: "substr", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.Bool), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + str := args[0].AsString() + substr := args[1].AsString() + + if strings.Contains(str, substr) { + return cty.True, nil + } + + return cty.False, nil + }, +}) diff --git a/pkg/iac/scanners/terraform/parser/functions.go b/pkg/iac/scanners/terraform/parser/functions.go new file mode 100644 index 000000000000..6f6406d8ed19 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/functions.go @@ -0,0 +1,137 @@ +package parser + +import ( + "io/fs" + + "github.com/hashicorp/hcl/v2/ext/tryfunc" + ctyyaml "github.com/zclconf/go-cty-yaml" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/function/stdlib" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser/funcs" +) + +// Functions returns the set of functions that should be used to when evaluating +// expressions in the receiving scope. +func Functions(target fs.FS, baseDir string) map[string]function.Function { + return map[string]function.Function{ + "abs": stdlib.AbsoluteFunc, + "abspath": funcs.AbsPathFunc, + "alltrue": funcs.AllTrueFunc, + "anytrue": funcs.AnyTrueFunc, + "basename": funcs.BasenameFunc, + "base64decode": funcs.Base64DecodeFunc, + "base64encode": funcs.Base64EncodeFunc, + "base64gzip": funcs.Base64GzipFunc, + "base64sha256": funcs.Base64Sha256Func, + "base64sha512": funcs.Base64Sha512Func, + "bcrypt": funcs.BcryptFunc, + "can": tryfunc.CanFunc, + "ceil": stdlib.CeilFunc, + "chomp": stdlib.ChompFunc, + "cidrhost": funcs.CidrHostFunc, + "cidrnetmask": funcs.CidrNetmaskFunc, + "cidrsubnet": funcs.CidrSubnetFunc, + "cidrsubnets": funcs.CidrSubnetsFunc, + "coalesce": funcs.CoalesceFunc, + "coalescelist": stdlib.CoalesceListFunc, + "compact": stdlib.CompactFunc, + "concat": stdlib.ConcatFunc, + "contains": stdlib.ContainsFunc, + "csvdecode": stdlib.CSVDecodeFunc, + "dirname": funcs.DirnameFunc, + "distinct": stdlib.DistinctFunc, + "element": stdlib.ElementFunc, + "endswith": funcs.EndsWithFunc, + "chunklist": stdlib.ChunklistFunc, + "file": funcs.MakeFileFunc(target, baseDir, false), + "fileexists": funcs.MakeFileExistsFunc(target, baseDir), + "fileset": funcs.MakeFileSetFunc(target, baseDir), + "filebase64": funcs.MakeFileFunc(target, baseDir, true), + "filebase64sha256": funcs.MakeFileBase64Sha256Func(target, baseDir), + "filebase64sha512": funcs.MakeFileBase64Sha512Func(target, baseDir), + "filemd5": funcs.MakeFileMd5Func(target, baseDir), + "filesha1": funcs.MakeFileSha1Func(target, baseDir), + "filesha256": funcs.MakeFileSha256Func(target, baseDir), + "filesha512": funcs.MakeFileSha512Func(target, baseDir), + "flatten": stdlib.FlattenFunc, + "floor": stdlib.FloorFunc, + "format": stdlib.FormatFunc, + "formatdate": stdlib.FormatDateFunc, + "formatlist": stdlib.FormatListFunc, + "indent": stdlib.IndentFunc, + "index": funcs.IndexFunc, // stdlib.IndexFunc is not compatible + "join": stdlib.JoinFunc, + "jsondecode": stdlib.JSONDecodeFunc, + "jsonencode": stdlib.JSONEncodeFunc, + "keys": stdlib.KeysFunc, + "length": funcs.LengthFunc, + "list": funcs.ListFunc, + "log": stdlib.LogFunc, + "lookup": funcs.LookupFunc, + "lower": stdlib.LowerFunc, + "map": funcs.MapFunc, + "matchkeys": funcs.MatchkeysFunc, + "max": stdlib.MaxFunc, + "md5": funcs.Md5Func, + "merge": stdlib.MergeFunc, + "min": stdlib.MinFunc, + "one": funcs.OneFunc, + "parseint": stdlib.ParseIntFunc, + "pathexpand": funcs.PathExpandFunc, + "pow": stdlib.PowFunc, + "range": stdlib.RangeFunc, + "regex": stdlib.RegexFunc, + "regexall": stdlib.RegexAllFunc, + "replace": funcs.ReplaceFunc, + "reverse": stdlib.ReverseListFunc, + "rsadecrypt": funcs.RsaDecryptFunc, + "sensitive": funcs.SensitiveFunc, + "nonsensitive": funcs.NonsensitiveFunc, + "issensitive": funcs.IssensitiveFunc, + "setintersection": stdlib.SetIntersectionFunc, + "setproduct": stdlib.SetProductFunc, + "setsubtract": stdlib.SetSubtractFunc, + "setunion": stdlib.SetUnionFunc, + "sha1": funcs.Sha1Func, + "sha256": funcs.Sha256Func, + "sha512": funcs.Sha512Func, + "signum": stdlib.SignumFunc, + "slice": stdlib.SliceFunc, + "sort": stdlib.SortFunc, + "split": stdlib.SplitFunc, + "startswith": funcs.StartsWithFunc, + "strcontains": funcs.StrContainsFunc, + "strrev": stdlib.ReverseFunc, + "substr": stdlib.SubstrFunc, + "sum": funcs.SumFunc, + "textdecodebase64": funcs.TextDecodeBase64Func, + "textencodebase64": funcs.TextEncodeBase64Func, + "timestamp": funcs.TimestampFunc, + "timeadd": stdlib.TimeAddFunc, + "timecmp": funcs.TimeCmpFunc, + "title": stdlib.TitleFunc, + "tostring": funcs.MakeToFunc(cty.String), + "tonumber": funcs.MakeToFunc(cty.Number), + "tobool": funcs.MakeToFunc(cty.Bool), + "toset": funcs.MakeToFunc(cty.Set(cty.DynamicPseudoType)), + "tolist": funcs.MakeToFunc(cty.List(cty.DynamicPseudoType)), + "tomap": funcs.MakeToFunc(cty.Map(cty.DynamicPseudoType)), + "transpose": funcs.TransposeFunc, + "trim": stdlib.TrimFunc, + "trimprefix": stdlib.TrimPrefixFunc, + "trimspace": stdlib.TrimSpaceFunc, + "trimsuffix": stdlib.TrimSuffixFunc, + "try": tryfunc.TryFunc, + "upper": stdlib.UpperFunc, + "urlencode": funcs.URLEncodeFunc, + "uuid": funcs.UUIDFunc, + "uuidv5": funcs.UUIDV5Func, + "values": stdlib.ValuesFunc, + "yamldecode": ctyyaml.YAMLDecodeFunc, + "yamlencode": ctyyaml.YAMLEncodeFunc, + "zipmap": stdlib.ZipmapFunc, + } + +} diff --git a/pkg/iac/scanners/terraform/parser/load_module.go b/pkg/iac/scanners/terraform/parser/load_module.go new file mode 100644 index 000000000000..9797792040b7 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/load_module.go @@ -0,0 +1,172 @@ +package parser + +import ( + "context" + "errors" + "fmt" + "io/fs" + "path" + "strings" + + "github.com/zclconf/go-cty/cty" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser/resolvers" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/log" +) + +type ModuleDefinition struct { + Name string + Path string + FileSystem fs.FS + Definition *terraform.Block + Parser *Parser + External bool +} + +func (d *ModuleDefinition) inputVars() map[string]cty.Value { + inputs := d.Definition.NullableValues().AsValueMap() + if inputs == nil { + return make(map[string]cty.Value) + } + return inputs +} + +// loadModules reads all module blocks and loads them +func (e *evaluator) loadModules(ctx context.Context) []*ModuleDefinition { + var moduleDefinitions []*ModuleDefinition + + expanded := e.expandBlocks(e.blocks.OfType("module")) + + for _, moduleBlock := range expanded { + if moduleBlock.Label() == "" { + continue + } + moduleDefinition, err := e.loadModule(ctx, moduleBlock) + if err != nil { + rng := moduleBlock.GetMetadata().Range() + e.logger.Error("Failed to load module. Maybe try 'terraform init'?", + log.String("loc", rng.String()), log.Err(err)) + continue + } + + e.logger.Debug("Loaded module", log.String("name", moduleDefinition.Name), log.FilePath(moduleDefinition.Path)) + moduleDefinitions = append(moduleDefinitions, moduleDefinition) + } + + return moduleDefinitions +} + +// takes in a module "x" {} block and loads resources etc. into e.moduleBlocks - additionally returns variables to add to ["module.x.*"] variables +func (e *evaluator) loadModule(ctx context.Context, b *terraform.Block) (*ModuleDefinition, error) { + if b.Label() == "" { + return nil, errors.New("module without label") + } + + sourceVal := b.GetAttribute("source").Value() + if !sourceVal.IsKnown() || sourceVal.Type() != cty.String { + return nil, errors.New("source is not a string") + } + + source := sourceVal.AsString() + if source == "" { + return nil, errors.New("source is empty") + } + + if e.moduleMetadata != nil { + // if we have module metadata we can parse all the modules as they'll be cached locally! + def, err := e.loadModuleFromTerraformCache(ctx, b, source) + if err == nil { + e.logger.Debug("Using module from Terraform cache .terraform/modules", + log.String("source", source)) + return def, nil + } + + e.logger.Debug("Failed to load module from .terraform cache", + log.String("module_source", source), + log.Err(err), + ) + } + + // we don't have the module installed via 'terraform init' so we need to grab it... + return e.loadExternalModule(ctx, b, source) +} + +func (e *evaluator) loadModuleFromTerraformCache(ctx context.Context, b *terraform.Block, source string) (*ModuleDefinition, error) { + var modulePath string + moduleKey := b.ModuleKey() + for _, module := range e.moduleMetadata.Modules { + if module.Key == moduleKey { + modulePath = path.Clean(path.Join(e.projectRootPath, module.Dir)) + break + } + } + if modulePath == "" { + return nil, fmt.Errorf("resolve module with key %q from .terraform/modules", moduleKey) + } + + if strings.HasPrefix(source, ".") { + source = "" + } + + e.logger.Debug("Module resolved using modules.json", + log.String("block", b.FullName()), + log.String("source", source), + log.String("modulePath", modulePath), + ) + moduleParser := e.parentParser.newModuleParser(e.filesystem, source, modulePath, b.Label(), b) + if err := moduleParser.ParseFS(ctx, modulePath); err != nil { + return nil, fmt.Errorf("parse module filesystem: %w", err) + } + + return &ModuleDefinition{ + Name: b.Label(), + Path: modulePath, + Definition: b, + Parser: moduleParser, + FileSystem: e.filesystem, + }, nil +} + +func (e *evaluator) loadExternalModule(ctx context.Context, b *terraform.Block, source string) (*ModuleDefinition, error) { + + e.logger.Debug("Locating non-initialized module", log.String("source", source)) + + version := b.GetAttribute("version").AsStringValueOrDefault("", b).Value() + opt := resolvers.Options{ + Source: source, + OriginalSource: source, + Version: version, + OriginalVersion: version, + WorkingDir: e.projectRootPath, + Name: b.FullName(), + ModulePath: e.modulePath, + Logger: e.logger.With(log.Prefix("module resolver")), + AllowDownloads: e.allowDownloads, + SkipCache: e.skipCachedModules, + } + + filesystem, prefix, downloadPath, err := resolveModule(ctx, e.filesystem, opt) + if err != nil { + return nil, err + } + prefix = path.Join(e.parentParser.moduleSource, prefix) + e.logger.Debug("Module resolved", + log.String("block", b.FullName()), + log.String("source", source), + log.String("prefix", prefix), + log.FilePath(downloadPath), + ) + moduleParser := e.parentParser.newModuleParser(filesystem, prefix, downloadPath, b.Label(), b) + if err := moduleParser.ParseFS(ctx, downloadPath); err != nil { + return nil, err + } + return &ModuleDefinition{ + Name: b.Label(), + Path: downloadPath, + Definition: b, + Parser: moduleParser, + FileSystem: filesystem, + External: true, + }, nil +} diff --git a/pkg/iac/scanners/terraform/parser/load_module_metadata.go b/pkg/iac/scanners/terraform/parser/load_module_metadata.go new file mode 100644 index 000000000000..13259a6055f1 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/load_module_metadata.go @@ -0,0 +1,37 @@ +package parser + +import ( + "encoding/json" + "io/fs" + "path" +) + +const ManifestSnapshotFile = ".terraform/modules/modules.json" + +type ModulesMetadata struct { + Modules []ModuleMetadata `json:"Modules"` +} + +type ModuleMetadata struct { + Key string `json:"Key"` + Source string `json:"Source"` + Version string `json:"Version"` + Dir string `json:"Dir"` +} + +func loadModuleMetadata(target fs.FS, fullPath string) (*ModulesMetadata, string, error) { + metadataPath := path.Join(fullPath, ManifestSnapshotFile) + + f, err := target.Open(metadataPath) + if err != nil { + return nil, metadataPath, err + } + defer f.Close() + + var metadata ModulesMetadata + if err := json.NewDecoder(f).Decode(&metadata); err != nil { + return nil, metadataPath, err + } + + return &metadata, metadataPath, nil +} diff --git a/pkg/iac/scanners/terraform/parser/load_vars.go b/pkg/iac/scanners/terraform/parser/load_vars.go new file mode 100644 index 000000000000..622c9df16250 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/load_vars.go @@ -0,0 +1,82 @@ +package parser + +import ( + "fmt" + "io/fs" + "maps" + "os" + "path/filepath" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + hcljson "github.com/hashicorp/hcl/v2/json" + "github.com/zclconf/go-cty/cty" +) + +func loadTFVars(srcFS fs.FS, filenames []string) (map[string]cty.Value, error) { + combinedVars := make(map[string]cty.Value) + + for _, env := range os.Environ() { + split := strings.Split(env, "=") + key := split[0] + if !strings.HasPrefix(key, "TF_VAR_") { + continue + } + key = strings.TrimPrefix(key, "TF_VAR_") + var val string + if len(split) > 1 { + val = split[1] + } + combinedVars[key] = cty.StringVal(val) + } + + for _, filename := range filenames { + vars, err := loadTFVarsFile(srcFS, filename) + if err != nil { + return nil, fmt.Errorf("failed to load tfvars from %s: %w", filename, err) + } + maps.Copy(combinedVars, vars) + } + + return combinedVars, nil +} + +func loadTFVarsFile(srcFS fs.FS, filename string) (map[string]cty.Value, error) { + inputVars := make(map[string]cty.Value) + if filename == "" { + return inputVars, nil + } + + src, err := fs.ReadFile(srcFS, filepath.ToSlash(filename)) + if err != nil { + return nil, err + } + + var attrs hcl.Attributes + if strings.HasSuffix(filename, ".json") { + variableFile, err := hcljson.Parse(src, filename) + if err != nil { + return nil, err + } + attrs, err = variableFile.Body.JustAttributes() + if err != nil { + return nil, err + } + } else { + variableFile, err := hclsyntax.ParseConfig(src, filename, hcl.Pos{Line: 1, Column: 1}) + if err != nil { + return nil, err + } + attrs, err = variableFile.Body.JustAttributes() + if err != nil { + return nil, err + } + } + + for _, attr := range attrs { + inputVars[attr.Name], _ = attr.Expr.Value(&hcl.EvalContext{}) + } + + return inputVars, nil +} diff --git a/pkg/iac/scanners/terraform/parser/load_vars_test.go b/pkg/iac/scanners/terraform/parser/load_vars_test.go new file mode 100644 index 000000000000..09f256f2616f --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/load_vars_test.go @@ -0,0 +1,45 @@ +package parser + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" + + "github.com/aquasecurity/trivy/internal/testutil" +) + +func Test_TFVarsFile(t *testing.T) { + t.Run("tfvars file", func(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "test.tfvars": `instance_type = "t2.large"`, + }) + + vars, err := loadTFVars(fs, []string{"test.tfvars"}) + require.NoError(t, err) + assert.Equal(t, "t2.large", vars["instance_type"].AsString()) + }) + + t.Run("tfvars json file", func(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "test.tfvars.json": `{ + "variable": { + "foo": { + "default": "bar" + }, + "baz": "qux" + }, + "foo2": true, + "foo3": 3 +}`, + }) + + vars, err := loadTFVars(fs, []string{"test.tfvars.json"}) + require.NoError(t, err) + assert.Equal(t, "bar", vars["variable"].GetAttr("foo").GetAttr("default").AsString()) + assert.Equal(t, "qux", vars["variable"].GetAttr("baz").AsString()) + assert.True(t, vars["foo2"].True()) + assert.True(t, vars["foo3"].Equals(cty.NumberIntVal(3)).True()) + }) +} diff --git a/pkg/iac/scanners/terraform/parser/module_retrieval.go b/pkg/iac/scanners/terraform/parser/module_retrieval.go new file mode 100644 index 000000000000..cc374b68eeb7 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/module_retrieval.go @@ -0,0 +1,35 @@ +package parser + +import ( + "context" + "fmt" + "io/fs" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser/resolvers" + "github.com/aquasecurity/trivy/pkg/log" +) + +type ModuleResolver interface { + Resolve(context.Context, fs.FS, resolvers.Options) (filesystem fs.FS, prefix string, downloadPath string, applies bool, err error) +} + +var defaultResolvers = []ModuleResolver{ + resolvers.Local, + resolvers.Cache, + resolvers.Remote, + resolvers.Registry, +} + +func resolveModule(ctx context.Context, current fs.FS, opt resolvers.Options) (filesystem fs.FS, sourcePrefix, downloadPath string, err error) { + opt.Logger.Debug("Resolving module", + log.String("name", opt.Name), log.String("source", opt.Source)) + for _, resolver := range defaultResolvers { + if filesystem, prefix, path, applies, err := resolver.Resolve(ctx, current, opt); err != nil { + return nil, "", "", err + } else if applies { + opt.Logger.Debug("Module resolved", log.FilePath(path)) + return filesystem, prefix, path, nil + } + } + return nil, "", "", fmt.Errorf("failed to resolve module '%s' with source: %s", opt.Name, opt.Source) +} diff --git a/pkg/iac/scanners/terraform/parser/modules.go b/pkg/iac/scanners/terraform/parser/modules.go new file mode 100644 index 000000000000..39cf76cb4c7e --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/modules.go @@ -0,0 +1,82 @@ +package parser + +import ( + "context" + "path" + "sort" + "strings" + + "github.com/samber/lo" + "github.com/zclconf/go-cty/cty" + + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +// FindRootModules takes a list of module paths and identifies the root local modules. +// It builds a graph based on the module dependencies and determines the modules that have no incoming dependencies, +// considering them as root modules. +func (p *Parser) FindRootModules(ctx context.Context, dirs []string) ([]string, error) { + // skip cached terraform modules as they cannot be root modules + dirs = lo.Filter(dirs, func(dir string, _ int) bool { + return !strings.Contains(dir, ".terraform/modules/") + }) + for _, dir := range dirs { + if err := p.ParseFS(ctx, dir); err != nil { + return nil, err + } + } + + blocks, _, err := p.readBlocks(p.files) + if err != nil { + return nil, err + } + + g := buildGraph(blocks, dirs) + rootModules := g.rootModules() + sort.Strings(rootModules) + return rootModules, nil +} + +type modulesGraph map[string][]string + +func buildGraph(blocks terraform.Blocks, paths []string) modulesGraph { + moduleBlocks := blocks.OfType("module") + + graph := lo.SliceToMap(paths, func(p string) (string, []string) { + return p, nil + }) + + for _, block := range moduleBlocks { + sourceVal := block.GetAttribute("source").Value() + if !sourceVal.IsKnown() || sourceVal.Type() != cty.String { + continue + } + + source := sourceVal.AsString() + if strings.HasPrefix(source, ".") { + filename := block.GetMetadata().Range().GetFilename() + dir := path.Dir(filename) + graph[dir] = append(graph[dir], path.Join(dir, source)) + } + } + + return graph +} + +func (g modulesGraph) rootModules() []string { + incomingEdges := make(map[string]int) + for _, neighbors := range g { + for _, neighbor := range neighbors { + incomingEdges[neighbor]++ + } + } + + var roots []string + for module := range g { + if incomingEdges[module] == 0 { + roots = append(roots, module) + } + } + + return roots +} diff --git a/pkg/iac/scanners/terraform/parser/modules_test.go b/pkg/iac/scanners/terraform/parser/modules_test.go new file mode 100644 index 000000000000..2bfe9916183d --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/modules_test.go @@ -0,0 +1,84 @@ +package parser + +import ( + "path" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func TestFindRootModules(t *testing.T) { + tests := []struct { + name string + files map[string]string + expected []string + }{ + { + name: "multiple root modules", + files: map[string]string{ + "code/main.tf": ` +module "this" { + count = 0 + source = "./modules/s3" +}`, + "code/modules/s3/main.tf": ` +module "this" { + source = "./modules/logging" +} +resource "aws_s3_bucket" "this" { + bucket = "test" +}`, + "code/modules/s3/modules/logging/main.tf": ` +resource "aws_s3_bucket" "this" { + bucket = "test1" +}`, + "code/example/main.tf": ` +module "this" { + source = "../modules/s3" +}`, + }, + expected: []string{ + "code", + "code/example", + }, + }, + { + name: "without module block", + files: map[string]string{ + "code/infra1/main.tf": `resource "test" "this" {}`, + "code/infra2/main.tf": `resource "test" "this" {}`, + }, + expected: []string{ + "code/infra1", + "code/infra2", + }, + }, + { + name: "unknown module source", + files: map[string]string{ + "main.tf": `module "test" { + source = "${path.module}/modules/foo" +}`, + }, + expected: []string{"."}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := testutil.CreateFS(tt.files) + parser := New(fsys, "", OptionStopOnHCLError(true)) + + modules := xslices.Map(lo.Keys(tt.files), path.Dir) + + got, err := parser.FindRootModules(t.Context(), modules) + require.NoError(t, err) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/terraform/parser/option.go b/pkg/iac/scanners/terraform/parser/option.go new file mode 100644 index 000000000000..86cf69cc3778 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/option.go @@ -0,0 +1,77 @@ +package parser + +import ( + "io/fs" + + "github.com/zclconf/go-cty/cty" + + "github.com/aquasecurity/trivy/pkg/log" +) + +type Option func(p *Parser) + +func OptionWithTFVarsPaths(paths ...string) Option { + return func(p *Parser) { + p.tfvarsPaths = paths + } +} + +func OptionStopOnHCLError(stop bool) Option { + return func(p *Parser) { + p.stopOnHCLError = stop + } +} + +func OptionWithLogger(log *log.Logger) Option { + return func(p *Parser) { + p.logger = log + } +} + +func OptionWithWorkingDirectoryPath(cwd string) Option { + return func(p *Parser) { + p.cwd = cwd + } +} + +func OptionsWithTfVars(vars map[string]cty.Value) Option { + return func(p *Parser) { + p.tfvars = vars + } +} + +func OptionWithWorkspaceName(workspaceName string) Option { + return func(p *Parser) { + p.workspaceName = workspaceName + } +} + +func OptionWithDownloads(allowed bool) Option { + return func(p *Parser) { + p.allowDownloads = allowed + } +} + +func OptionWithSkipCachedModules(b bool) Option { + return func(p *Parser) { + p.skipCachedModules = b + } +} + +func OptionWithConfigsFS(fsys fs.FS) Option { + return func(p *Parser) { + p.configsFS = fsys + } +} + +func OptionWithSkipFiles(files []string) Option { + return func(p *Parser) { + p.skipPaths = files + } +} + +func OptionWithSkipDirs(dirs []string) Option { + return func(p *Parser) { + p.skipPaths = dirs + } +} diff --git a/pkg/iac/scanners/terraform/parser/parser.go b/pkg/iac/scanners/terraform/parser/parser.go new file mode 100644 index 000000000000..f303253bb424 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/parser.go @@ -0,0 +1,473 @@ +package parser + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "io/fs" + "log/slog" + "os" + "path" + "path/filepath" + "sort" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclparse" + "github.com/zclconf/go-cty/cty" + + "github.com/aquasecurity/trivy/pkg/fanal/utils" + "github.com/aquasecurity/trivy/pkg/iac/ignore" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + tfcontext "github.com/aquasecurity/trivy/pkg/iac/terraform/context" + "github.com/aquasecurity/trivy/pkg/log" +) + +type sourceFile struct { + file *hcl.File + path string +} + +// Parser is a tool for parsing terraform templates at a given file system location +type Parser struct { + projectRoot string + moduleName string + modulePath string + moduleSource string + moduleFS fs.FS + moduleBlock *terraform.Block + files []sourceFile + tfvarsPaths []string + tfvars map[string]cty.Value + stopOnHCLError bool + workspaceName string + underlying *hclparse.Parser + children []*Parser + options []Option + logger *log.Logger + allowDownloads bool + skipCachedModules bool + fsMap map[string]fs.FS + configsFS fs.FS + skipPaths []string + // cwd is optional, if left to empty string, 'os.Getwd' + // will be used for populating 'path.cwd' in terraform. + cwd string +} + +// New creates a new Parser +func New(moduleFS fs.FS, moduleSource string, opts ...Option) *Parser { + p := &Parser{ + workspaceName: "default", + underlying: hclparse.NewParser(), + options: opts, + moduleName: "root", + allowDownloads: true, + moduleFS: moduleFS, + moduleSource: moduleSource, + configsFS: moduleFS, + logger: slog.Default(), + tfvars: make(map[string]cty.Value), + } + + for _, option := range opts { + option(p) + } + + // Scope the logger to the parser + p.logger = p.logger.With(log.Prefix("terraform parser")).With("module", "root") + + return p +} + +func (p *Parser) newModuleParser(moduleFS fs.FS, moduleSource, modulePath, moduleName string, moduleBlock *terraform.Block) *Parser { + mp := New(moduleFS, moduleSource) + mp.modulePath = modulePath + mp.moduleBlock = moduleBlock + mp.moduleName = moduleName + mp.logger = p.logger + mp.projectRoot = p.projectRoot + mp.skipPaths = p.skipPaths + mp.options = p.options + p.children = append(p.children, mp) + for _, option := range p.options { + option(mp) + } + + // The options above can reset the logger, so set the logging prefix after the + // options. + mp.logger = mp.logger.With(log.Prefix("terraform parser")).With("module", moduleName) + return mp +} + +func (p *Parser) Files() map[string]*hcl.File { + return p.underlying.Files() +} + +func (p *Parser) ParseFile(_ context.Context, fullPath string) error { + + isJSON := strings.HasSuffix(fullPath, ".tf.json") || strings.HasSuffix(fullPath, ".tofu.json") + isHCL := strings.HasSuffix(fullPath, ".tf") || strings.HasSuffix(fullPath, ".tofu") + if !isJSON && !isHCL { + return nil + } + + p.logger.Debug("Parsing", log.FilePath(fullPath)) + f, err := p.moduleFS.Open(filepath.ToSlash(fullPath)) + if err != nil { + return err + } + defer func() { _ = f.Close() }() + + data, err := io.ReadAll(f) + if err != nil { + return err + } + + if dir := path.Dir(fullPath); p.projectRoot == "" { + p.logger.Debug("Setting project/module root", log.FilePath(dir)) + p.projectRoot = dir + p.modulePath = dir + } + + var file *hcl.File + var diag hcl.Diagnostics + + if isHCL { + file, diag = p.underlying.ParseHCL(data, fullPath) + } else { + file, diag = p.underlying.ParseJSON(data, fullPath) + } + if diag != nil && diag.HasErrors() { + return diag + } + p.files = append(p.files, sourceFile{ + file: file, + path: fullPath, + }) + + p.logger.Debug("Added file", log.FilePath(fullPath)) + return nil +} + +// ParseFS parses a root module, where it exists at the root of the provided filesystem +func (p *Parser) ParseFS(ctx context.Context, dir string) error { + if p.moduleFS == nil { + return errors.New("module filesystem is nil, nothing to parse") + } + dir = path.Clean(dir) + + if p.projectRoot == "" { + p.logger.Debug("Setting project/module root", log.FilePath(dir)) + p.projectRoot = dir + p.modulePath = dir + } + + slashed := filepath.ToSlash(dir) + p.logger.Debug("Parsing FS", log.FilePath(slashed)) + fileInfos, err := fs.ReadDir(p.moduleFS, slashed) + if err != nil { + return fmt.Errorf("read dir: %w", err) + } + + var paths []string + for _, info := range fileInfos { + realPath := path.Join(dir, info.Name()) + if info.IsDir() { + continue + } + if utils.SkipPath(realPath, utils.CleanSkipPaths(p.skipPaths)) { + p.logger.Debug("Skipping path based on input glob", log.FilePath(realPath), log.Any("glob", p.skipPaths)) + continue + } + paths = append(paths, realPath) + } + sort.Strings(paths) + for _, path := range paths { + var err error + if err = p.ParseFile(ctx, path); err == nil { + continue + } + + if p.stopOnHCLError { + return err + } + var diags hcl.Diagnostics + if errors.As(err, &diags) { + errc := p.showParseErrors(p.moduleFS, path, diags) + if errc == nil { + continue + } + p.logger.Error("Failed to get the causes of the parsing error", log.Err(errc)) + } + p.logger.Error("Error parsing file", log.FilePath(path), log.Err(err)) + continue + } + + return nil +} + +func (p *Parser) showParseErrors(fsys fs.FS, filePath string, diags hcl.Diagnostics) error { + file, err := fsys.Open(filePath) + if err != nil { + return fmt.Errorf("failed to read file: %w", err) + } + defer file.Close() + + for _, diag := range diags { + if subj := diag.Subject; subj != nil { + lines, err := readLinesFromFile(file, subj.Start.Line, subj.End.Line) + if err != nil { + return err + } + + cause := strings.Join(lines, "\n") + p.logger.Error("Error parsing file", log.FilePath(filePath), + log.String("cause", cause), log.Err(diag)) + } + } + + return nil +} + +func readLinesFromFile(f io.Reader, from, to int) ([]string, error) { + scanner := bufio.NewScanner(f) + rawLines := make([]string, 0, to-from+1) + + for lineNum := 0; scanner.Scan() && lineNum < to; lineNum++ { + if lineNum >= from-1 { + rawLines = append(rawLines, scanner.Text()) + } + } + + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("failed to scan file: %w", err) + } + + return rawLines, nil +} + +var ErrNoFiles = errors.New("no files found") + +func (p *Parser) Load(_ context.Context) (*evaluator, error) { + p.logger.Debug("Loading module", log.String("module", p.moduleName)) + + if len(p.files) == 0 { + p.logger.Info("No files found, nothing to do.") + return nil, ErrNoFiles + } + + blocks, ignores, err := p.readBlocks(p.files) + if err != nil { + return nil, err + } + p.logger.Debug("Read block(s) and ignore(s)", + log.Int("blocks", len(blocks)), log.Int("ignores", len(ignores))) + + var inputVars map[string]cty.Value + + switch { + case p.moduleBlock != nil: + inputVars = p.moduleBlock.Values().AsValueMap() + p.logger.Debug("Added input variables from module definition", + log.Int("count", len(inputVars))) + case len(p.tfvars) > 0: + inputVars = p.tfvars + p.logger.Debug("Added input variables from tfvars.", log.Int("count", len(inputVars))) + default: + inputVars, err = loadTFVars(p.configsFS, p.tfvarsPaths) + if err != nil { + return nil, err + } + p.logger.Debug("Added input variables from tfvars", log.Int("count", len(inputVars))) + p.setFallbackValuesForMissingVars(inputVars, blocks) + } + + modulesMetadata, metadataPath, err := loadModuleMetadata(p.moduleFS, p.projectRoot) + + if err != nil && !errors.Is(err, os.ErrNotExist) { + p.logger.Error("Error loading module metadata", log.Err(err)) + } else if err == nil { + p.logger.Debug("Loaded module metadata for modules", + log.FilePath(metadataPath), + log.Int("count", len(modulesMetadata.Modules)), + ) + } + + var workingDir string + if p.cwd != "" { + workingDir = p.cwd + } else { + workingDir, err = os.Getwd() + if err != nil { + return nil, err + } + } + + p.logger.Debug("Working directory for module evaluation", log.FilePath(workingDir)) + return newEvaluator( + p.moduleFS, + p, + p.projectRoot, + p.modulePath, + workingDir, + p.moduleName, + blocks, + inputVars, + modulesMetadata, + p.workspaceName, + ignores, + p.logger.With(log.Prefix("terraform evaluator")), + p.allowDownloads, + p.skipCachedModules, + ), nil +} + +func missingVariableValues(blocks terraform.Blocks, inputVars map[string]cty.Value) []*terraform.Block { + var missing []*terraform.Block + for _, varBlock := range blocks.OfType("variable") { + if varBlock.GetAttribute("default") == nil { + if _, ok := inputVars[varBlock.TypeLabel()]; !ok { + missing = append(missing, varBlock) + } + } + } + + return missing +} + +// Set fallback values for missing variables, to allow expressions using them to be +// still be possibly evaluated to a value different than null. +func (p *Parser) setFallbackValuesForMissingVars(inputVars map[string]cty.Value, blocks []*terraform.Block) { + varBlocks := missingVariableValues(blocks, inputVars) + if len(varBlocks) == 0 { + return + } + + missingVars := make([]string, 0, len(varBlocks)) + for _, block := range varBlocks { + varType := inputVariableType(block) + if varType != cty.NilType { + inputVars[block.TypeLabel()] = cty.UnknownVal(varType) + } else { + inputVars[block.TypeLabel()] = cty.DynamicVal + } + missingVars = append(missingVars, block.TypeLabel()) + } + + p.logger.Warn( + "Variable values were not found in the environment or variable files. Evaluating may not work correctly.", + log.String("variables", strings.Join(missingVars, ", ")), + ) +} + +func inputVariableType(b *terraform.Block) cty.Type { + typeAttr, exists := b.Attributes()["type"] + if !exists { + return cty.NilType + } + ty, _, err := typeAttr.DecodeVarType() + if err != nil { + return cty.NilType + } + return ty +} + +func (p *Parser) EvaluateAll(ctx context.Context) (terraform.Modules, error) { + e, err := p.Load(ctx) + if errors.Is(err, ErrNoFiles) { + return nil, nil + } else if err != nil { + return nil, err + } + + modules, fsMap := e.EvaluateAll(ctx) + p.logger.Debug("Finished parsing module") + p.fsMap = fsMap + return modules, nil +} + +func (p *Parser) GetFilesystemMap() map[string]fs.FS { + if p.fsMap == nil { + return make(map[string]fs.FS) + } + return p.fsMap +} + +func (p *Parser) readBlocks(files []sourceFile) (terraform.Blocks, ignore.Rules, error) { + var blocks terraform.Blocks + var ignores ignore.Rules + moduleCtx := tfcontext.NewContext(&hcl.EvalContext{}, nil) + for _, file := range files { + fileBlocks, err := loadBlocksFromFile(file) + if err != nil { + if p.stopOnHCLError { + return nil, nil, err + } + p.logger.Error("Encountered HCL parse error", log.FilePath(file.path), log.Err(err)) + continue + } + for _, fileBlock := range fileBlocks { + blocks = append(blocks, terraform.NewBlock(fileBlock, moduleCtx, p.moduleBlock, nil, p.moduleSource, p.moduleFS)) + } + fileIgnores := ignore.Parse( + string(file.file.Bytes), + file.path, + p.moduleSource, + &ignore.StringMatchParser{ + SectionKey: "ws", + }, + ¶mParser{}, + ) + ignores = append(ignores, fileIgnores...) + } + + sortBlocksByHierarchy(blocks) + return blocks, ignores, nil +} + +func loadBlocksFromFile(file sourceFile) (hcl.Blocks, error) { + contents, diagnostics := file.file.Body.Content(terraform.Schema) + if diagnostics != nil && diagnostics.HasErrors() { + return nil, diagnostics + } + if contents == nil { + return nil, nil + } + return contents.Blocks, nil +} + +type paramParser struct { + params map[string]string +} + +func (s *paramParser) Key() string { + return "ignore" +} + +func (s *paramParser) Parse(str string) bool { + s.params = make(map[string]string) + + idx := strings.Index(str, "[") + if idx == -1 { + return false + } + + str = str[idx+1:] + + paramStr := strings.TrimSuffix(str, "]") + for pair := range strings.SplitSeq(paramStr, ",") { + parts := strings.Split(pair, "=") + if len(parts) != 2 { + continue + } + s.params[parts[0]] = parts[1] + } + return true +} + +func (s *paramParser) Param() any { + return s.params +} diff --git a/pkg/iac/scanners/terraform/parser/parser_test.go b/pkg/iac/scanners/terraform/parser/parser_test.go new file mode 100644 index 000000000000..e2947cc2675a --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/parser_test.go @@ -0,0 +1,2926 @@ +package parser + +import ( + "bytes" + "io/fs" + "log/slog" + "os" + "path/filepath" + "slices" + "sort" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +func Test_BasicParsing(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "test.tf": ` + +locals { + proxy = var.cats_mother +} + +variable "cats_mother" { + default = "boots" +} + +provider "cats" { + +} + +moved { + +} + +action "aws_lambda_invoke" "example" { + config { + function_name = "123456789012:function:my-function:1" + payload = jsonencode({ + key1 = "value1" + key2 = "value2" + }) + } +} + + +import { + to = cats_cat.mittens + id = "mittens" +} + +resource "cats_cat" "mittens" { + name = "mittens" + special = true +} + +resource "cats_kitten" "the-great-destroyer" { + name = "the great destroyer" + parent = cats_cat.mittens.name +} + +data "cats_cat" "the-cats-mother" { + name = local.proxy +} + +check "cats_mittens_is_special" { + data "cats_cat" "mittens" { + name = "mittens" + } + + assert { + condition = data.cats_cat.mittens.special == true + error_message = "${data.cats_cat.mittens.name} must be special" + } +} + +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + blocks := modules[0].GetBlocks() + + // variable + variables := blocks.OfType("variable") + require.Len(t, variables, 1) + assert.Equal(t, "variable", variables[0].Type()) + require.Len(t, variables[0].Labels(), 1) + assert.Equal(t, "cats_mother", variables[0].TypeLabel()) + defaultVal := variables[0].GetAttribute("default") + require.NotNil(t, defaultVal) + assert.Equal(t, cty.String, defaultVal.Value().Type()) + assert.Equal(t, "boots", defaultVal.Value().AsString()) + + // provider + providerBlocks := blocks.OfType("provider") + require.Len(t, providerBlocks, 1) + assert.Equal(t, "provider", providerBlocks[0].Type()) + require.Len(t, providerBlocks[0].Labels(), 1) + assert.Equal(t, "cats", providerBlocks[0].TypeLabel()) + + // resources + resourceBlocks := blocks.OfType("resource") + + sort.Slice(resourceBlocks, func(i, j int) bool { + return resourceBlocks[i].TypeLabel() < resourceBlocks[j].TypeLabel() + }) + + require.Len(t, resourceBlocks, 2) + require.Len(t, resourceBlocks[0].Labels(), 2) + + assert.Equal(t, "resource", resourceBlocks[0].Type()) + assert.Equal(t, "cats_cat", resourceBlocks[0].TypeLabel()) + assert.Equal(t, "mittens", resourceBlocks[0].NameLabel()) + + assert.Equal(t, "mittens", resourceBlocks[0].GetAttribute("name").Value().AsString()) + assert.True(t, resourceBlocks[0].GetAttribute("special").Value().True()) + + assert.Equal(t, "resource", resourceBlocks[1].Type()) + assert.Equal(t, "cats_kitten", resourceBlocks[1].TypeLabel()) + assert.Equal(t, "the great destroyer", resourceBlocks[1].GetAttribute("name").Value().AsString()) + assert.Equal(t, "mittens", resourceBlocks[1].GetAttribute("parent").Value().AsString()) + + // import + importBlocks := blocks.OfType("import") + + assert.Equal(t, "import", importBlocks[0].Type()) + require.NotNil(t, importBlocks[0].GetAttribute("to")) + assert.Equal(t, "mittens", importBlocks[0].GetAttribute("id").Value().AsString()) + + // data + dataBlocks := blocks.OfType("data") + require.Len(t, dataBlocks, 1) + require.Len(t, dataBlocks[0].Labels(), 2) + + assert.Equal(t, "data", dataBlocks[0].Type()) + assert.Equal(t, "cats_cat", dataBlocks[0].TypeLabel()) + assert.Equal(t, "the-cats-mother", dataBlocks[0].NameLabel()) + + assert.Equal(t, "boots", dataBlocks[0].GetAttribute("name").Value().AsString()) + + // check + checkBlocks := blocks.OfType("check") + require.Len(t, checkBlocks, 1) + require.Len(t, checkBlocks[0].Labels(), 1) + + assert.Equal(t, "check", checkBlocks[0].Type()) + assert.Equal(t, "cats_mittens_is_special", checkBlocks[0].TypeLabel()) + + require.NotNil(t, checkBlocks[0].GetBlock("data")) + require.NotNil(t, checkBlocks[0].GetBlock("assert")) +} + +func Test_Modules(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` +module "my-mod" { + source = "../module" + input = "ok" +} + +output "result" { + value = module.my-mod.mod_result +} +`, + "module/module.tf": ` +variable "input" { + default = "?" +} + +output "mod_result" { + value = var.input +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + require.Len(t, modules, 2) + rootModule := modules[0] + childModule := modules[1] + + moduleBlocks := rootModule.GetBlocks().OfType("module") + require.Len(t, moduleBlocks, 1) + + assert.Equal(t, "module", moduleBlocks[0].Type()) + assert.Equal(t, "module.my-mod", moduleBlocks[0].FullName()) + inputAttr := moduleBlocks[0].GetAttribute("input") + require.NotNil(t, inputAttr) + require.Equal(t, cty.String, inputAttr.Value().Type()) + assert.Equal(t, "ok", inputAttr.Value().AsString()) + + rootOutputs := rootModule.GetBlocks().OfType("output") + require.Len(t, rootOutputs, 1) + assert.Equal(t, "output.result", rootOutputs[0].FullName()) + valAttr := rootOutputs[0].GetAttribute("value") + require.NotNil(t, valAttr) + require.Equal(t, cty.String, valAttr.Type()) + assert.Equal(t, "ok", valAttr.Value().AsString()) + + childOutputs := childModule.GetBlocks().OfType("output") + require.Len(t, childOutputs, 1) + assert.Equal(t, "module.my-mod.output.mod_result", childOutputs[0].FullName()) + childValAttr := childOutputs[0].GetAttribute("value") + require.NotNil(t, childValAttr) + require.Equal(t, cty.String, childValAttr.Type()) + assert.Equal(t, "ok", childValAttr.Value().AsString()) + +} + +func Test_NestedParentModule(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` +module "my-mod" { + source = "../." + input = "ok" +} + +output "result" { + value = module.my-mod.mod_result +} +`, + "root.tf": ` +variable "input" { + default = "?" +} + +output "mod_result" { + value = var.input +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 2) + rootModule := modules[0] + childModule := modules[1] + + moduleBlocks := rootModule.GetBlocks().OfType("module") + require.Len(t, moduleBlocks, 1) + + assert.Equal(t, "module", moduleBlocks[0].Type()) + assert.Equal(t, "module.my-mod", moduleBlocks[0].FullName()) + inputAttr := moduleBlocks[0].GetAttribute("input") + require.NotNil(t, inputAttr) + require.Equal(t, cty.String, inputAttr.Value().Type()) + assert.Equal(t, "ok", inputAttr.Value().AsString()) + + rootOutputs := rootModule.GetBlocks().OfType("output") + require.Len(t, rootOutputs, 1) + assert.Equal(t, "output.result", rootOutputs[0].FullName()) + valAttr := rootOutputs[0].GetAttribute("value") + require.NotNil(t, valAttr) + require.Equal(t, cty.String, valAttr.Type()) + assert.Equal(t, "ok", valAttr.Value().AsString()) + + childOutputs := childModule.GetBlocks().OfType("output") + require.Len(t, childOutputs, 1) + assert.Equal(t, "module.my-mod.output.mod_result", childOutputs[0].FullName()) + childValAttr := childOutputs[0].GetAttribute("value") + require.NotNil(t, childValAttr) + require.Equal(t, cty.String, childValAttr.Type()) + assert.Equal(t, "ok", childValAttr.Value().AsString()) +} + +func Test_UndefinedModuleOutputReference(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` +resource "something" "blah" { + value = module.x.y +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("something") + require.Len(t, blocks, 1) + block := blocks[0] + + attr := block.GetAttribute("value") + require.NotNil(t, attr) + + assert.False(t, attr.IsResolvable()) +} + +func Test_UndefinedModuleOutputReferenceInSlice(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` +resource "something" "blah" { + value = ["first", module.x.y, "last"] +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("something") + require.Len(t, blocks, 1) + block := blocks[0] + + attr := block.GetAttribute("value") + require.NotNil(t, attr) + + assert.True(t, attr.IsResolvable()) + + values := attr.AsStringValueSliceOrEmpty() + require.Len(t, values, 3) + + assert.Equal(t, "first", values[0].Value()) + assert.True(t, values[0].GetMetadata().IsResolvable()) + + assert.False(t, values[1].GetMetadata().IsResolvable()) + + assert.Equal(t, "last", values[2].Value()) + assert.True(t, values[2].GetMetadata().IsResolvable()) +} + +func Test_TemplatedSliceValue(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` + +variable "x" { + default = "hello" +} + +resource "something" "blah" { + value = ["first", "${var.x}-${var.x}", "last"] +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("something") + require.Len(t, blocks, 1) + block := blocks[0] + + attr := block.GetAttribute("value") + require.NotNil(t, attr) + + assert.True(t, attr.IsResolvable()) + + values := attr.AsStringValueSliceOrEmpty() + require.Len(t, values, 3) + + assert.Equal(t, "first", values[0].Value()) + assert.True(t, values[0].GetMetadata().IsResolvable()) + + assert.Equal(t, "hello-hello", values[1].Value()) + assert.True(t, values[1].GetMetadata().IsResolvable()) + + assert.Equal(t, "last", values[2].Value()) + assert.True(t, values[2].GetMetadata().IsResolvable()) +} + +func Test_SliceOfVars(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` + +variable "x" { + default = "1" +} + +variable "y" { + default = "2" +} + +resource "something" "blah" { + value = [var.x, var.y] +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("something") + require.Len(t, blocks, 1) + block := blocks[0] + + attr := block.GetAttribute("value") + require.NotNil(t, attr) + + assert.True(t, attr.IsResolvable()) + + values := attr.AsStringValueSliceOrEmpty() + require.Len(t, values, 2) + + assert.Equal(t, "1", values[0].Value()) + assert.True(t, values[0].GetMetadata().IsResolvable()) + + assert.Equal(t, "2", values[1].Value()) + assert.True(t, values[1].GetMetadata().IsResolvable()) +} + +func Test_VarSlice(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` + +variable "x" { + default = ["a", "b", "c"] +} + +resource "something" "blah" { + value = var.x +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("something") + require.Len(t, blocks, 1) + block := blocks[0] + + attr := block.GetAttribute("value") + require.NotNil(t, attr) + + assert.True(t, attr.IsResolvable()) + + values := attr.AsStringValueSliceOrEmpty() + require.Len(t, values, 3) + + assert.Equal(t, "a", values[0].Value()) + assert.True(t, values[0].GetMetadata().IsResolvable()) + + assert.Equal(t, "b", values[1].Value()) + assert.True(t, values[1].GetMetadata().IsResolvable()) + + assert.Equal(t, "c", values[2].Value()) + assert.True(t, values[2].GetMetadata().IsResolvable()) +} + +func Test_LocalSliceNested(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` + +variable "x" { + default = "a" +} + +locals { + y = [var.x, "b", "c"] +} + +resource "something" "blah" { + value = local.y +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("something") + require.Len(t, blocks, 1) + block := blocks[0] + + attr := block.GetAttribute("value") + require.NotNil(t, attr) + + assert.True(t, attr.IsResolvable()) + + values := attr.AsStringValueSliceOrEmpty() + require.Len(t, values, 3) + + assert.Equal(t, "a", values[0].Value()) + assert.True(t, values[0].GetMetadata().IsResolvable()) + + assert.Equal(t, "b", values[1].Value()) + assert.True(t, values[1].GetMetadata().IsResolvable()) + + assert.Equal(t, "c", values[2].Value()) + assert.True(t, values[2].GetMetadata().IsResolvable()) +} + +func Test_FunctionCall(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` + +variable "x" { + default = ["a", "b"] +} + +resource "something" "blah" { + value = concat(var.x, ["c"]) +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + require.Len(t, modules, 1) + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("something") + require.Len(t, blocks, 1) + block := blocks[0] + + attr := block.GetAttribute("value") + require.NotNil(t, attr) + + assert.True(t, attr.IsResolvable()) + + values := attr.AsStringValueSliceOrEmpty() + require.Len(t, values, 3) + + assert.Equal(t, "a", values[0].Value()) + assert.True(t, values[0].GetMetadata().IsResolvable()) + + assert.Equal(t, "b", values[1].Value()) + assert.True(t, values[1].GetMetadata().IsResolvable()) + + assert.Equal(t, "c", values[2].Value()) + assert.True(t, values[2].GetMetadata().IsResolvable()) +} + +func Test_NullDefaultValueForVar(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "test.tf": ` +variable "bucket_name" { + type = string + default = null +} + +resource "aws_s3_bucket" "default" { + bucket = var.bucket_name != null ? var.bucket_name : "default" +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("aws_s3_bucket") + require.Len(t, blocks, 1) + block := blocks[0] + + attr := block.GetAttribute("bucket") + require.NotNil(t, attr) + assert.Equal(t, "default", attr.Value().AsString()) +} + +func Test_MultipleInstancesOfSameResource(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "test.tf": ` + +resource "aws_kms_key" "key1" { + description = "Key #1" + enable_key_rotation = true +} + +resource "aws_kms_key" "key2" { + description = "Key #2" + enable_key_rotation = true +} + +resource "aws_s3_bucket" "this" { + bucket = "test" + } + + +resource "aws_s3_bucket_server_side_encryption_configuration" "this1" { + bucket = aws_s3_bucket.this.id + + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.key1.arn + sse_algorithm = "aws:kms" + } + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "this2" { + bucket = aws_s3_bucket.this.id + + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.key2.arn + sse_algorithm = "aws:kms" + } + } +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("aws_s3_bucket_server_side_encryption_configuration") + assert.Len(t, blocks, 2) + + for _, block := range blocks { + attr, parent := block.GetNestedAttribute("rule.apply_server_side_encryption_by_default.kms_master_key_id") + assert.Equal(t, "apply_server_side_encryption_by_default", parent.Type()) + assert.NotNil(t, attr) + assert.NotEmpty(t, attr.Value().AsString()) + } +} + +func Test_IfConfigFsIsNotSet_ThenUseModuleFsForVars(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": ` +variable "bucket_name" { + type = string +} +resource "aws_s3_bucket" "main" { + bucket = var.bucket_name +} +`, + "main.tfvars": `bucket_name = "test_bucket"`, + }) + parser := New(fs, "", + OptionStopOnHCLError(true), + OptionWithTFVarsPaths("main.tfvars"), + ) + + require.NoError(t, parser.ParseFS(t.Context(), ".")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + blocks := rootModule.GetResourcesByType("aws_s3_bucket") + require.Len(t, blocks, 1) + + block := blocks[0] + + assert.Equal(t, "test_bucket", block.GetAttribute("bucket").AsStringValueOrDefault("", block).Value()) +} + +func Test_ForEachRefToLocals(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": ` +locals { + buckets = toset([ + "foo", + "bar", + ]) +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.key +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("aws_s3_bucket") + assert.Len(t, blocks, 2) + + for _, block := range blocks { + attr := block.GetAttribute("bucket") + require.NotNil(t, attr) + assert.Contains(t, []string{"foo", "bar"}, attr.AsStringValueOrDefault("", block).Value()) + } +} + +func Test_ForEachRefToVariableWithDefault(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": ` +variable "buckets" { + type = set(string) + default = ["foo", "bar"] +} + +resource "aws_s3_bucket" "this" { + for_each = var.buckets + bucket = each.key +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("aws_s3_bucket") + assert.Len(t, blocks, 2) + + for _, block := range blocks { + attr := block.GetAttribute("bucket") + require.NotNil(t, attr) + assert.Contains(t, []string{"foo", "bar"}, attr.AsStringValueOrDefault("", block).Value()) + } +} + +func Test_ForEachRefToVariableFromFile(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": ` +variable "policy_rules" { + type = object({ + secure_tags = optional(map(object({ + session_matcher = optional(string) + priority = number + enabled = optional(bool, true) + })), {}) + }) +} + +resource "google_network_security_gateway_security_policy_rule" "secure_tag_rules" { + for_each = var.policy_rules.secure_tags + provider = google-beta + project = "test" + name = each.key + enabled = each.value.enabled + priority = each.value.priority + session_matcher = each.value.session_matcher +} +`, + "main.tfvars": ` +policy_rules = { + secure_tags = { + secure-tag-1 = { + session_matcher = "host() != 'google.com'" + priority = 1001 + } + } +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true), OptionWithTFVarsPaths("main.tfvars")) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("google_network_security_gateway_security_policy_rule") + assert.Len(t, blocks, 1) + + block := blocks[0] + + assert.Equal(t, "secure-tag-1", block.GetAttribute("name").AsStringValueOrDefault("", block).Value()) + assert.True(t, block.GetAttribute("enabled").AsBoolValueOrDefault(false, block).Value()) + assert.Equal(t, "host() != 'google.com'", block.GetAttribute("session_matcher").AsStringValueOrDefault("", block).Value()) + assert.Equal(t, 1001, block.GetAttribute("priority").AsIntValueOrDefault(0, block).Value()) +} + +func Test_ForEachRefersToMapThatContainsSameStringValues(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": `locals { + buckets = { + bucket1 = "test1" + bucket2 = "test1" + } +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.key +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + bucketBlocks := modules.GetResourcesByType("aws_s3_bucket") + assert.Len(t, bucketBlocks, 2) + + var labels []string + + for _, b := range bucketBlocks { + labels = append(labels, b.Label()) + } + + expectedLabels := []string{ + `aws_s3_bucket.this["bucket1"]`, + `aws_s3_bucket.this["bucket2"]`, + } + assert.Equal(t, expectedLabels, labels) +} + +func TestDataSourceWithCountMetaArgument(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": ` +data "http" "example" { + count = 2 +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + + httpDataSources := rootModule.GetDatasByType("http") + assert.Len(t, httpDataSources, 2) + + var labels []string + for _, b := range httpDataSources { + labels = append(labels, b.Label()) + } + + expectedLabels := []string{ + `http.example[0]`, + `http.example[1]`, + } + assert.Equal(t, expectedLabels, labels) +} + +func TestDataSourceWithForEachMetaArgument(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": ` +locals { + ports = ["80", "8080"] +} +data "http" "example" { + for_each = toset(local.ports) + url = "localhost:${each.key}" +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + + httpDataSources := rootModule.GetDatasByType("http") + assert.Len(t, httpDataSources, 2) +} + +func TestForEach(t *testing.T) { + tests := []struct { + name string + src string + expectedBucketName string + expectedNameLabel string + }{ + { + name: "arg is set and ref to each.key", + src: `locals { + buckets = ["bucket1"] +} + +resource "aws_s3_bucket" "this" { + for_each = toset(local.buckets) + bucket = each.key +}`, + expectedBucketName: "bucket1", + expectedNameLabel: `this["bucket1"]`, + }, + { + name: "arg is set and ref to each.value", + src: `locals { + buckets = ["bucket1"] +} + +resource "aws_s3_bucket" "this" { + for_each = toset(local.buckets) + bucket = each.value +}`, + expectedBucketName: "bucket1", + expectedNameLabel: `this["bucket1"]`, + }, + { + name: "arg is map and ref to each.key", + src: `locals { + buckets = { + bucket1key = "bucket1value" + } +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.key +}`, + expectedBucketName: "bucket1key", + expectedNameLabel: `this["bucket1key"]`, + }, + { + name: "arg is map and ref to each.value", + src: `locals { + buckets = { + bucket1key = "bucket1value" + } +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.value +}`, + expectedBucketName: "bucket1value", + expectedNameLabel: `this["bucket1key"]`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + modules := parse(t, map[string]string{ + "main.tf": tt.src, + }) + require.Len(t, modules, 1) + + buckets := modules.GetResourcesByType("aws_s3_bucket") + assert.Len(t, buckets, 1) + + bucket := buckets[0] + bucketName := bucket.GetAttribute("bucket").Value().AsString() + assert.Equal(t, tt.expectedBucketName, bucketName) + + assert.Equal(t, tt.expectedNameLabel, bucket.NameLabel()) + }) + } + +} + +func TestForEachCountExpanded(t *testing.T) { + + tests := []struct { + name string + source string + expectedCount int + }{ + { + name: "arg is list of strings", + source: `locals { + buckets = ["bucket1", "bucket2"] +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.key +}`, + expectedCount: 2, + }, + { + name: "arg is empty list", + source: `locals { + buckets = [] +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.value +}`, + expectedCount: 0, + }, + { + name: "arg is empty set", + source: `locals { + buckets = toset([]) +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.key +}`, + expectedCount: 0, + }, + { + name: "argument set with the same values", + source: `locals { + buckets = ["true", "true"] +} + +resource "aws_s3_bucket" "this" { + for_each = toset(local.buckets) + bucket = each.key +}`, + expectedCount: 1, + }, + { + name: "arg is non-valid set", + source: `locals { + buckets = [{ + bucket1key = "bucket1value" + }] +} + +resource "aws_s3_bucket" "this" { + for_each = toset(local.buckets) + bucket = each.value +}`, + expectedCount: 0, + }, + { + name: "arg is set of strings", + source: `locals { + buckets = ["bucket1", "bucket2"] +} + +resource "aws_s3_bucket" "this" { + for_each = toset(local.buckets) + bucket = each.key +}`, + expectedCount: 2, + }, + { + name: "arg is map", + source: `locals { + buckets = { + 1 = {} + 2 = {} + } +} + +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.key +}`, + expectedCount: 2, + }, + { + name: "arg is empty map", + source: `locals { + buckets = {} +} +resource "aws_s3_bucket" "this" { + for_each = local.buckets + bucket = each.value +} + `, + expectedCount: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + modules := parse(t, map[string]string{ + "main.tf": tt.source, + }) + assert.Len(t, modules, 1) + + bucketBlocks := modules.GetResourcesByType("aws_s3_bucket") + assert.Len(t, bucketBlocks, tt.expectedCount) + }) + } +} + +func TestForEachRefToResource(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": ` + locals { + vpcs = { + "test1" = { + cidr_block = "192.168.0.0/28" + } + "test2" = { + cidr_block = "192.168.1.0/28" + } + } +} + +resource "aws_vpc" "example" { + for_each = local.vpcs + cidr_block = each.value.cidr_block +} + +resource "aws_internet_gateway" "example" { + for_each = aws_vpc.example + vpc_id = each.key +} +`, + }) + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + + blocks := modules.GetResourcesByType("aws_internet_gateway") + require.Len(t, blocks, 2) + + var vpcIds []string + for _, b := range blocks { + vpcIds = append(vpcIds, b.GetAttribute("vpc_id").Value().AsString()) + } + + expectedVpcIds := []string{"test1", "test2"} + assert.Equal(t, expectedVpcIds, vpcIds) +} + +func TestArnAttributeOfBucketIsCorrect(t *testing.T) { + + t.Run("the bucket doesn't have a name", func(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": `resource "aws_s3_bucket" "this" {}`, + }) + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + + blocks := modules.GetResourcesByType("aws_s3_bucket") + assert.Len(t, blocks, 1) + + bucket := blocks[0] + + values := bucket.Values() + arnVal := values.GetAttr("arn") + assert.True(t, arnVal.Type().Equals(cty.String)) + + id := values.GetAttr("id").AsString() + + arn := arnVal.AsString() + assert.Equal(t, "arn:aws:s3:::"+id, arn) + }) + + t.Run("the bucket has a name", func(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": `resource "aws_s3_bucket" "this" { + bucket = "test" +} + +resource "aws_iam_role" "this" { + name = "test_role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Sid = "" + Principal = { + Service = "s3.amazonaws.com" + } + }, + ] + }) +} + +resource "aws_iam_role_policy" "this" { + name = "test_policy" + role = aws_iam_role.this.id + policy = data.aws_iam_policy_document.this.json +} + +data "aws_iam_policy_document" "this" { + statement { + effect = "Allow" + actions = [ + "s3:GetObject" + ] + resources = ["${aws_s3_bucket.this.arn}/*"] + } +}`, + }) + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + + blocks := modules[0].GetDatasByType("aws_iam_policy_document") + assert.Len(t, blocks, 1) + + policyDoc := blocks[0] + + statement := policyDoc.GetBlock("statement") + resources := statement.GetAttribute("resources").AsStringValueSliceOrEmpty() + + assert.Len(t, resources, 1) + assert.True(t, resources[0].EqualTo("arn:aws:s3:::test/*")) + }) +} + +func TestForEachWithObjectsOfDifferentTypes(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": `module "backups" { + bucket_name = each.key + client = each.value.client + path_writers = each.value.path_writers + + for_each = { + "bucket1" = { + client = "client1" + path_writers = ["writer1"] // tuple with string + }, + "bucket2" = { + client = "client2" + path_writers = [] // empty tuple + } + } +} +`, + }) + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) +} + +func TestCountMetaArgument(t *testing.T) { + tests := []struct { + name string + src string + expected int + }{ + { + name: "zero resources", + src: `resource "test" "this" { + count = 0 +}`, + expected: 0, + }, + { + name: "several resources", + src: `resource "test" "this" { + count = 2 +}`, + expected: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := testutil.CreateFS(map[string]string{ + "main.tf": tt.src, + }) + parser := New(fsys, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + resources := modules.GetResourcesByType("test") + assert.Len(t, resources, tt.expected) + }) + } +} + +func TestCountMetaArgumentInModule(t *testing.T) { + tests := []struct { + name string + files map[string]string + expectedCountModules int + expectedCountResources int + }{ + { + name: "zero modules", + files: map[string]string{ + "main.tf": `module "this" { + count = 0 + source = "./modules/test" +}`, + "modules/test/main.tf": `resource "test" "this" {}`, + }, + expectedCountModules: 1, + expectedCountResources: 0, + }, + { + name: "several modules", + files: map[string]string{ + "main.tf": `module "this" { + count = 2 + source = "./modules/test" +}`, + "modules/test/main.tf": `resource "test" "this" {}`, + }, + expectedCountModules: 3, + expectedCountResources: 2, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := testutil.CreateFS(tt.files) + parser := New(fsys, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + assert.Len(t, modules, tt.expectedCountModules) + + resources := modules.GetResourcesByType("test") + assert.Len(t, resources, tt.expectedCountResources) + }) + } +} + +func TestDynamicBlocks(t *testing.T) { + tests := []struct { + name string + src string + expected []any + }{ + { + name: "for-each use tuple of int", + src: `resource "test_resource" "test" { + dynamic "foo" { + for_each = [80, 443] + content { + bar = foo.value + } + } +}`, + expected: []any{float64(80), float64(443)}, + }, + { + name: "for-each use list of int", + src: `resource "test_resource" "test" { + dynamic "foo" { + for_each = tolist([80, 443]) + content { + bar = foo.value + } + } +}`, + expected: []any{float64(80), float64(443)}, + }, + { + name: "for-each use set of int", + src: `resource "test_resource" "test" { + dynamic "foo" { + for_each = toset([80, 443]) + content { + bar = foo.value + } + } +}`, + expected: []any{float64(80), float64(443)}, + }, + { + name: "for-each use list of bool", + src: `resource "test_resource" "test" { + dynamic "foo" { + for_each = tolist([true]) + content { + bar = foo.value + } + } +}`, + expected: []any{true}, + }, + { + name: "empty for-each", + src: `resource "test_resource" "test" { + dynamic "foo" { + for_each = [] + content {} + } +}`, + expected: []any{}, + }, + { + name: "for-each use tuple of objects", + src: `variable "test_var" { + type = list(object({ enabled = bool })) + default = [{ enabled = true }] +} + +resource "test_resource" "test" { + dynamic "foo" { + for_each = var.test_var + + content { + bar = foo.value.enabled + } + } +}`, + expected: []any{true}, + }, + { + name: "attribute ref to object key", + src: `variable "some_var" { + type = map( + object({ + tag = string + }) + ) + default = { + ssh = { "tag" = "login" } + http = { "tag" = "proxy" } + https = { "tag" = "proxy" } + } +} + +resource "test_resource" "test" { + dynamic "foo" { + for_each = { for name, values in var.some_var : name => values } + content { + bar = foo.key + } + } +}`, + expected: []any{"ssh", "http", "https"}, + }, + { + name: "attribute ref to object value", + src: `variable "some_var" { + type = map( + object({ + tag = string + }) + ) + default = { + ssh = { "tag" = "login" } + http = { "tag" = "proxy" } + https = { "tag" = "proxy" } + } +} + +resource "test_resource" "test" { + dynamic "foo" { + for_each = { for name, values in var.some_var : name => values } + content { + bar = foo.value.tag + } + } +}`, + expected: []any{"login", "proxy", "proxy"}, + }, + { + name: "attribute ref to map key", + src: `variable "some_var" { + type = map + default = { + ssh = { "tag" = "login" } + http = { "tag" = "proxy" } + https = { "tag" = "proxy" } + } +} + +resource "test_resource" "test" { + dynamic "foo" { + for_each = var.some_var + content { + bar = foo.key + } + } +}`, + expected: []any{"ssh", "http", "https"}, + }, + { + name: "attribute ref to map value", + src: `variable "some_var" { + type = map + default = { + ssh = { "tag" = "login" } + http = { "tag" = "proxy" } + https = { "tag" = "proxy" } + } +} + +resource "test_resource" "test" { + dynamic "foo" { + for_each = var.some_var + content { + bar = foo.value.tag + } + } +}`, + expected: []any{"login", "proxy", "proxy"}, + }, + { + name: "dynamic block with iterator", + src: `resource "test_resource" "test" { + dynamic "foo" { + for_each = ["foo", "bar"] + iterator = some_iterator + content { + bar = some_iterator.value + } + } +}`, + expected: []any{"foo", "bar"}, + }, + { + name: "iterator and parent block with same name", + src: `resource "test_resource" "test" { + dynamic "foo" { + for_each = ["foo", "bar"] + iterator = foo + content { + bar = foo.value + } + } +}`, + expected: []any{"foo", "bar"}, + }, + { + name: "for-each use null value", + src: `resource "test_resource" "test" { + dynamic "foo" { + for_each = null + content { + bar = foo.value + } + } +}`, + expected: []any{}, + }, + { + name: "no for-each attribute", + src: `resource "test_resource" "test" { + dynamic "foo" { + content { + bar = foo.value + } + } +}`, + expected: []any{}, + }, + { + name: "unknown for-each", + src: `resource "test_resource" "test" { + dynamic "foo" { + for_each = lookup(foo, "") ? [] : [] + } +}`, + expected: []any{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + modules := parse(t, map[string]string{ + "main.tf": tt.src, + }) + require.Len(t, modules, 1) + + resource := modules.GetResourcesByType("test_resource") + require.Len(t, resource, 1) + blocks := resource[0].GetBlocks("foo") + + var vals []any + for _, attr := range blocks { + vals = append(vals, attr.GetAttribute("bar").GetRawValue()) + } + + assert.ElementsMatch(t, tt.expected, vals) + }) + } +} + +func TestNestedDynamicBlock(t *testing.T) { + modules := parse(t, map[string]string{ + "main.tf": `resource "test_resource" "test" { + dynamic "foo" { + for_each = ["1", "1"] + content { + dynamic "bar" { + for_each = [true, true] + content { + baz = foo.value + qux = bar.value + } + } + } + } +}`, + }) + require.Len(t, modules, 1) + + testResources := modules.GetResourcesByType("test_resource") + assert.Len(t, testResources, 1) + blocks := testResources[0].GetBlocks("foo") + assert.Len(t, blocks, 2) + + var nested []*terraform.Block + for _, block := range blocks { + nested = append(nested, block.GetBlocks("bar")...) + for _, b := range nested { + assert.Equal(t, "1", b.GetAttribute("baz").GetRawValue()) + assert.Equal(t, true, b.GetAttribute("qux").GetRawValue()) + } + } + assert.Len(t, nested, 4) +} + +func parse(t *testing.T, files map[string]string, opts ...Option) terraform.Modules { + fs := testutil.CreateFS(files) + opts = append(opts, OptionStopOnHCLError(true)) + parser := New(fs, "", opts...) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + return modules +} + +func TestModuleRefersToOutputOfAnotherModule(t *testing.T) { + files := map[string]string{ + "main.tf": ` +module "module2" { + source = "./modules/foo" +} + +module "module1" { + source = "./modules/bar" + test_var = module.module2.test_out +} +`, + "modules/foo/main.tf": ` +output "test_out" { + value = "test_value" +} +`, + "modules/bar/main.tf": ` +variable "test_var" {} + +resource "test_resource" "this" { + dynamic "dynamic_block" { + for_each = [var.test_var] + content { + some_attr = dynamic_block.value + } + } +} +`, + } + + modules := parse(t, files) + require.Len(t, modules, 3) + + resources := modules.GetResourcesByType("test_resource") + require.Len(t, resources, 1) + + attr, _ := resources[0].GetNestedAttribute("dynamic_block.some_attr") + require.NotNil(t, attr) + + assert.Equal(t, "test_value", attr.GetRawValue()) +} + +func TestPopulateContextWithBlockInstances(t *testing.T) { + + tests := []struct { + name string + blockType string + files map[string]string + }{ + { + name: "data blocks with count", + blockType: "data", + files: map[string]string{ + "main.tf": `data "d" "foo" { + count = 1 + value = "Index ${count.index}" +} + +data "b" "foo" { + count = 1 + value = data.d.foo[0].value +} + +data "c" "foo" { + count = 1 + value = data.b.foo[0].value +}`, + }, + }, + { + name: "resource blocks with count", + blockType: "resource", + files: map[string]string{ + "main.tf": `resource "d" "foo" { + count = 1 + value = "Index ${count.index}" +} + +resource "b" "foo" { + count = 1 + value = d.foo[0].value +} + +resource "c" "foo" { + count = 1 + value = b.foo[0].value +}`, + }, + }, + { + name: "module block with count", + blockType: "data", + files: map[string]string{ + "main.tf": `module "a" { + source = "./modules/a" + count = 2 + inp = "Index ${count.index}" +} + +data "b" "foo" { + count = 1 + value = module.a[0].value +} + +data "c" "foo" { + count = 1 + value = data.b.foo[0].value +}`, + "modules/a/main.tf": `variable "inp" {} +output "value" { + value = var.inp +}`, + }, + }, + { + name: "data blocks with for_each", + blockType: "data", + files: map[string]string{ + "main.tf": `data "d" "foo" { + for_each = toset([0]) + value = "Index ${each.key}" +} + +data "b" "foo" { + for_each = data.d.foo + value = each.value.value +} + +data "c" "foo" { + for_each = data.b.foo + value = each.value.value +}`, + }, + }, + { + name: "resource blocks with for_each", + blockType: "resource", + files: map[string]string{ + "main.tf": `resource "d" "foo" { + for_each = toset([0]) + value = "Index ${each.key}" +} + +resource "b" "foo" { + for_each = d.foo + value = each.value.value +} + +resource "c" "foo" { + for_each = b.foo + value = each.value.value +}`, + }, + }, + { + name: "module block with for_each", + blockType: "data", + files: map[string]string{ + "main.tf": `module "a" { + for_each = toset([0]) + source = "./modules/a" + inp = "Index ${each.key}" +} + +data "b" "foo" { + value = module.a["0"].value +}`, + "modules/a/main.tf": `variable "inp" {} +output "value" { + value = var.inp +}`, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + modules := parse(t, tt.files) + require.GreaterOrEqual(t, len(modules), 1) + for _, b := range modules.GetBlocks().OfType(tt.blockType) { + attr := b.GetAttribute("value") + assert.Equal(t, "Index 0", attr.Value().AsString()) + } + }) + } +} + +// TestNestedModulesOptions ensures parser options are carried to the nested +// submodule evaluators. +// The test will include an invalid module that will fail to download +// if it is attempted. +func TestNestedModulesOptions(t *testing.T) { + // reset the previous default logger + prevLog := slog.Default() + defer slog.SetDefault(prevLog) + var buf bytes.Buffer + slog.SetDefault(slog.New(log.NewHandler(&buf, nil))) + + // Folder structure + // ./ + // ├── main.tf + // └── modules + // ├── city + // │ └── main.tf + // ├── queens + // │ └── main.tf + // └── brooklyn + // └── main.tf + // + // Modules referenced + // main -> city ├─> brooklyn + // └─> queens + files := map[string]string{ + "main.tf": ` +module "city" { + source = "./modules/city" +} + +resource "city" "neighborhoods" { + names = module.city.neighborhoods +} +`, + "modules/city/main.tf": ` +module "brooklyn" { + source = "./brooklyn" +} + +module "queens" { + source = "./queens" +} + +output "neighborhoods" { + value = [module.brooklyn.name, module.queens.name] +} +`, + "modules/city/brooklyn/main.tf": ` +output "name" { + value = "Brooklyn" +} +`, + "modules/city/queens/main.tf": ` +output "name" { + value = "Queens" +} + +module "invalid" { + source = "https://example.invaliddomain" +} +`, + } + + // Using the OptionWithDownloads(false) option will prevent the invalid + // module from being downloaded. If the log exists "failed to download" + // then the submodule evaluator attempted to download, which was disallowed. + modules := parse(t, files, OptionWithDownloads(false)) + require.Len(t, modules, 4) + + resources := modules.GetResourcesByType("city") + require.Len(t, resources, 1) + + for _, res := range resources { + attr, _ := res.GetNestedAttribute("names") + require.NotNil(t, attr, res.FullName()) + assert.Equal(t, []string{"Brooklyn", "Queens"}, attr.GetRawValue()) + } + + require.NotContains(t, buf.String(), "failed to download") + + // Verify module parents are set correctly. + expectedParents := map[string]string{ + ".": "", + "modules/city": ".", + "modules/city/brooklyn": "modules/city", + "modules/city/queens": "modules/city", + } + + for _, mod := range modules { + expected, exists := expectedParents[mod.ModulePath()] + require.Truef(t, exists, "module %s does not exist in assertion", mod.ModulePath()) + if expected == "" { + require.Nil(t, mod.Parent()) + } else { + require.Equal(t, expected, mod.Parent().ModulePath(), "parent of module %q", mod.ModulePath()) + } + } +} + +// TestModuleParents sets up a nested module structure and verifies the +// parent-child relationships are correctly set. +func TestModuleParents(t *testing.T) { + // The setup is a list of continents, some countries, some cities, etc. + dirfs := os.DirFS("./testdata/nested") + parser := New(dirfs, "", + OptionStopOnHCLError(true), + OptionWithDownloads(false), + ) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + // modules only have 'parent'. They do not have children, so create + // a structure that allows traversal from the root to the leafs. + modChildren := make(map[*terraform.Module][]*terraform.Module) + // Keep track of every module that exists + modSet := set.New[*terraform.Module]() + var root *terraform.Module + for _, mod := range modules { + modChildren[mod] = make([]*terraform.Module, 0) + modSet.Append(mod) + + if mod.Parent() == nil { + // Only 1 root should exist + require.Nil(t, root, "root module already set") + root = mod + } + modChildren[mod.Parent()] = append(modChildren[mod.Parent()], mod) + } + + type node struct { + prefix string + modulePath string + children []node + } + + // expectedTree is the full module tree structure. + expectedTree := node{ + modulePath: ".", + children: []node{ + { + modulePath: "north-america", + children: []node{ + { + modulePath: "north-america/united-states", + children: []node{ + {modulePath: "north-america/united-states/springfield", prefix: "illinois-"}, + {modulePath: "north-america/united-states/springfield", prefix: "idaho-"}, + {modulePath: "north-america/united-states/new-york", children: []node{ + {modulePath: "north-america/united-states/new-york/new-york-city"}, + }}, + }, + }, + { + modulePath: "north-america/canada", + children: []node{ + {modulePath: "north-america/canada/springfield", prefix: "ontario-"}, + }, + }, + }, + }, + }, + } + + var assertChild func(t *testing.T, n node, mod *terraform.Module) + assertChild = func(t *testing.T, n node, mod *terraform.Module) { + modSet.Remove(mod) + children := modChildren[mod] + + t.Run(n.modulePath, func(t *testing.T) { + if !assert.Len(t, children, len(n.children), "modChildren count for %s", n.modulePath) { + return + } + for _, child := range children { + // Find the child module that we are expecting. + idx := slices.IndexFunc(n.children, func(node node) bool { + outputBlocks := child.GetBlocks().OfType("output") + outIdx := slices.IndexFunc(outputBlocks, func(outputBlock *terraform.Block) bool { + return outputBlock.Labels()[0] == "name" + }) + if outIdx == -1 { + return false + } + + output := outputBlocks[outIdx] + outVal := output.GetAttribute("value").Value() + if !outVal.Type().Equals(cty.String) { + return false + } + + modName := filepath.Base(node.modulePath) + if outVal.AsString() != node.prefix+modName { + return false + } + + return node.modulePath == child.ModulePath() + }) + if !assert.NotEqualf(t, -1, idx, "module prefix=%s path=%s not found in %s", n.prefix, child.ModulePath(), n.modulePath) { + continue + } + + assertChild(t, n.children[idx], child) + } + }) + + } + + assertChild(t, expectedTree, root) + // If any module was not asserted, the test will fail. This ensures the + // entire module tree is checked. + require.Equal(t, 0, modSet.Size(), "all modules asserted") +} + +func TestCyclicModules(t *testing.T) { + files := map[string]string{ + "main.tf": ` +module "module2" { + source = "./modules/foo" + test_var = passthru.handover.from_1 +} + +// Demonstrates need for evaluateSteps between submodule evaluations. +resource "passthru" "handover" { + from_1 = module.module1.test_out + from_2 = module.module2.test_out +} + +module "module1" { + source = "./modules/bar" + test_var = passthru.handover.from_2 +} +`, + "modules/foo/main.tf": ` +variable "test_var" {} + +resource "test_resource" "this" { + dynamic "dynamic_block" { + for_each = [var.test_var] + content { + some_attr = dynamic_block.value + } + } +} + +output "test_out" { + value = "test_value" +} +`, + "modules/bar/main.tf": ` +variable "test_var" {} + +resource "test_resource" "this" { + dynamic "dynamic_block" { + for_each = [var.test_var] + content { + some_attr = dynamic_block.value + } + } +} + +output "test_out" { + value = test_resource.this.dynamic_block.some_attr +} +`, + } + + modules := parse(t, files) + require.Len(t, modules, 3) + + resources := modules.GetResourcesByType("test_resource") + require.Len(t, resources, 2) + + for _, res := range resources { + attr, _ := res.GetNestedAttribute("dynamic_block.some_attr") + require.NotNil(t, attr, res.FullName()) + assert.Equal(t, "test_value", attr.GetRawValue()) + } +} + +func TestExtractSetValue(t *testing.T) { + files := map[string]string{ + "main.tf": ` +resource "test" "set-value" { + value = toset(["x", "y", "x"]) +} +`, + } + + resources := parse(t, files).GetResourcesByType("test") + require.Len(t, resources, 1) + attr := resources[0].GetAttribute("value") + require.NotNil(t, attr) + assert.Equal(t, []string{"x", "y"}, attr.GetRawValue()) +} + +func TestFunc_fileset(t *testing.T) { + files := map[string]string{ + "main.tf": ` +resource "test" "fileset-func" { + value = fileset(path.module, "**/*.py") +} +`, + "a.py": ``, + "path/b.py": ``, + } + + resources := parse(t, files).GetResourcesByType("test") + require.Len(t, resources, 1) + attr := resources[0].GetAttribute("value") + require.NotNil(t, attr) + assert.Equal(t, []string{"a.py", "path/b.py"}, attr.GetRawValue()) +} + +func TestExprWithMissingVar(t *testing.T) { + files := map[string]string{ + "main.tf": ` +variable "v" { + type = string +} + +resource "test" "values" { + s = "foo-${var.v}" + l1 = ["foo", var.v] + l2 = concat(["foo"], [var.v]) + d1 = {foo = var.v} + d2 = merge({"foo": "bar"}, {"baz": var.v}) +} +`, + } + + resources := parse(t, files).GetResourcesByType("test") + require.Len(t, resources, 1) + + s_attr := resources[0].GetAttribute("s") + require.NotNil(t, s_attr) + assert.Equal(t, "foo-", s_attr.Value().Range().StringPrefix()) + + for _, name := range []string{"l1", "l2", "d1", "d2"} { + attr := resources[0].GetAttribute(name) + require.NotNil(t, attr) + } +} + +func TestVarTypeShortcut(t *testing.T) { + files := map[string]string{ + "main.tf": ` +variable "magic_list" { + type = list + default = ["x", "y"] +} + +variable "magic_map" { + type = map + default = {a = 1, b = 2} +} + +resource "test" "values" { + l = var.magic_list + m = var.magic_map +} +`, + } + + resources := parse(t, files).GetResourcesByType("test") + require.Len(t, resources, 1) + + list_attr := resources[0].GetAttribute("l") + require.NotNil(t, list_attr) + assert.Equal(t, []string{"x", "y"}, list_attr.GetRawValue()) + + map_attr := resources[0].GetAttribute("m") + require.NotNil(t, map_attr) + assert.True(t, map_attr.Value().RawEquals(cty.MapVal(map[string]cty.Value{ + "a": cty.NumberIntVal(1), "b": cty.NumberIntVal(2), + }))) +} + +func Test_LoadLocalCachedModule(t *testing.T) { + fsys := os.DirFS(filepath.Join("testdata", "cached-modules")) + + parser := New( + fsys, "", + OptionStopOnHCLError(true), + OptionWithDownloads(false), + ) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + assert.Len(t, modules, 2) + + buckets := modules.GetResourcesByType("aws_s3_bucket") + assert.Len(t, buckets, 1) + + assert.Equal(t, "my-private-module/s3-bucket/aws/.terraform/modules/s3-bucket/main.tf", buckets[0].GetMetadata().Range().GetFilename()) + + bucketName := buckets[0].GetAttribute("bucket").Value().AsString() + assert.Equal(t, "my-s3-bucket", bucketName) +} + +func TestTFVarsFileDoesNotExist(t *testing.T) { + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{ + Data: []byte(``), + }, + } + + parser := New( + fsys, "", + OptionStopOnHCLError(true), + OptionWithDownloads(false), + OptionWithTFVarsPaths("main.tfvars"), + ) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + _, err := parser.EvaluateAll(t.Context()) + assert.ErrorContains(t, err, "file does not exist") +} + +func Test_OptionsWithTfVars(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "main.tf": `resource "test" "this" { + foo = var.foo +} +variable "foo" {} +`}) + + parser := New(fs, "", OptionsWithTfVars( + map[string]cty.Value{ + "foo": cty.StringVal("bar"), + }, + )) + + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + assert.Len(t, modules, 1) + + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("test") + assert.Len(t, blocks, 1) + assert.Equal(t, "bar", blocks[0].GetAttribute("foo").Value().AsString()) +} + +func Test_AWSRegionNameDefined(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "code/test.tf": ` +data "aws_region" "current" {} + +data "aws_region" "other" { + name = "us-east-2" +} + +resource "something" "blah" { + r1 = data.aws_region.current.name + r2 = data.aws_region.other.name +} +`, + }) + + parser := New(fs, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), "code")) + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + rootModule := modules[0] + + blocks := rootModule.GetResourcesByType("something") + require.Len(t, blocks, 1) + block := blocks[0] + + r1 := block.GetAttribute("r1") + require.NotNil(t, r1) + assert.True(t, r1.IsResolvable()) + assert.Equal(t, "current-region", r1.Value().AsString()) + + r2 := block.GetAttribute("r2") + require.NotNil(t, r2) + assert.True(t, r2.IsResolvable()) + assert.Equal(t, "us-east-2", r2.Value().AsString()) +} + +func TestLogAboutMissingVariableValues(t *testing.T) { + var buf bytes.Buffer + slog.SetDefault(slog.New(log.NewHandler(&buf, nil))) + + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{ + Data: []byte(` +variable "foo" {} + +variable "bar" { + default = "bar" +} + +variable "baz" {} +`), + }, + "main.tfvars": &fstest.MapFile{ + Data: []byte(`baz = "baz"`), + }, + } + + parser := New( + fsys, "", + OptionStopOnHCLError(true), + OptionWithTFVarsPaths("main.tfvars"), + ) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + _, err := parser.Load(t.Context()) + require.NoError(t, err) + + assert.Contains(t, buf.String(), "Variable values were not found in the environment or variable files.") + assert.Contains(t, buf.String(), "variables=\"foo\"") +} + +func TestLoadChildModulesFromLocalCache(t *testing.T) { + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{Data: []byte(`module "level_1" { + source = "./modules/level_1" +}`)}, + "modules/level_1/main.tf": &fstest.MapFile{Data: []byte(`module "level_2" { + source = "../level_2" +}`)}, + "modules/level_2/main.tf": &fstest.MapFile{Data: []byte(`module "level_3" { + count = 2 + source = "../level_3" +}`)}, + "modules/level_3/main.tf": &fstest.MapFile{Data: []byte(`resource "foo" "bar" {}`)}, + ".terraform/modules/modules.json": &fstest.MapFile{Data: []byte(`{ + "Modules": [ + { "Key": "", "Source": "", "Dir": "." }, + { + "Key": "level_1", + "Source": "./modules/level_1", + "Dir": "modules/level_1" + }, + { + "Key": "level_1.level_2", + "Source": "../level_2", + "Dir": "modules/level_2" + }, + { + "Key": "level_1.level_2.level_3", + "Source": "../level_3", + "Dir": "modules/level_3" + } + ] +}`)}, + } + + var buf bytes.Buffer + logger := slog.New(log.NewHandler(&buf, &log.Options{Level: log.LevelDebug})) + + parser := New( + fsys, "", + OptionStopOnHCLError(true), + OptionWithLogger(logger), + ) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + assert.Len(t, modules, 5) + + assert.Contains(t, buf.String(), "Using module from Terraform cache .terraform/modules\tmodule=\"root\" source=\"./modules/level_1\"") + assert.Contains(t, buf.String(), "Using module from Terraform cache .terraform/modules\tmodule=\"level_1\" source=\"../level_2\"") + assert.Contains(t, buf.String(), "Using module from Terraform cache .terraform/modules\tmodule=\"level_2\" source=\"../level_3\"") + assert.Contains(t, buf.String(), "Using module from Terraform cache .terraform/modules\tmodule=\"level_2\" source=\"../level_3\"") +} + +func TestNilParser(t *testing.T) { + parser := New( + nil, "", + ) + err := parser.ParseFS(t.Context(), ".") + require.Error(t, err) +} + +func TestLogParseErrors(t *testing.T) { + var buf bytes.Buffer + slog.SetDefault(slog.New(log.NewHandler(&buf, nil))) + + src := `resource "aws-s3-bucket" "name" { + bucket = < +}` + + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{ + Data: []byte(src), + }, + } + + parser := New(fsys, "") + err := parser.ParseFS(t.Context(), ".") + require.NoError(t, err) + + assert.Contains(t, buf.String(), `cause=" bucket = <"`) +} + +func Test_PassingNullToChildModule_DoesNotEraseType(t *testing.T) { + tests := []struct { + name string + fsys fs.FS + }{ + { + name: "typed variable", + fsys: fstest.MapFS{ + "main.tf": &fstest.MapFile{Data: []byte(`module "test" { + source = "./modules/test" + test_var = null +}`)}, + "modules/test/main.tf": &fstest.MapFile{Data: []byte(`variable "test_var" { + type = number +} + +resource "foo" "this" { + bar = var.test_var != null ? 1 : 2 +}`)}, + }, + }, + { + name: "typed variable with default", + fsys: fstest.MapFS{ + "main.tf": &fstest.MapFile{Data: []byte(`module "test" { + source = "./modules/test" + test_var = null +}`)}, + "modules/test/main.tf": &fstest.MapFile{Data: []byte(`variable "test_var" { + type = number + default = null +} + +resource "foo" "this" { + bar = var.test_var != null ? 1 : 2 +}`)}, + }, + }, + { + name: "empty variable", + fsys: fstest.MapFS{ + "main.tf": &fstest.MapFile{Data: []byte(`module "test" { + source = "./modules/test" + test_var = null +}`)}, + "modules/test/main.tf": &fstest.MapFile{Data: []byte(`variable "test_var" {} + +resource "foo" "this" { + bar = var.test_var != null ? 1 : 2 +}`)}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parser := New( + tt.fsys, "", + OptionStopOnHCLError(true), + ) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + _, err := parser.Load(t.Context()) + require.NoError(t, err) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + res := modules.GetResourcesByType("foo")[0] + attr := res.GetAttribute("bar") + val, _ := attr.Value().AsBigFloat().Int64() + assert.Equal(t, int64(2), val) + }) + } +} + +func TestAttrRefToNullVariable(t *testing.T) { + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{Data: []byte(`variable "name" { + type = string + default = null +} + +resource "aws_s3_bucket" "example" { + bucket = var.name +}`)}, + } + + parser := New(fsys, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + _, err := parser.Load(t.Context()) + require.NoError(t, err) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + val := modules.GetResourcesByType("aws_s3_bucket")[0].GetAttribute("bucket").GetRawValue() + assert.Nil(t, val) +} + +func Test_AttrIsRefToOtherBlock(t *testing.T) { + fsys := testutil.CreateFS(map[string]string{ + "main.tf": `locals { + baz_idx = 0 +} + +resource "foo" "bar" { + attr = baz.qux[local.baz_idx].attr +} + +resource "baz" "qux" { + count = 1 + attr = "test" +} +`, + }) + + parser := New(fsys, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + require.Len(t, modules, 1) + root := modules[0] + foo := root.GetResourcesByType("foo")[0] + fooAttr := foo.GetAttribute("attr") + b, err := root.GetReferencedBlock(fooAttr, foo) + require.NoError(t, err) + require.NotNil(t, b) +} + +func TestConfigWithEphemeralBlock(t *testing.T) { + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{Data: []byte(`ephemeral "random_password" "password" { + length = 16 +}`)}, + } + + parser := New(fsys, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + _, err := parser.Load(t.Context()) + require.NoError(t, err) +} + +func TestConvertObjectWithUnknownAndNullValuesToMap(t *testing.T) { + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{Data: []byte(`module "foo" { + source = "./modules/foo" +} + +locals { + known = "test" +} + +module "bar" { + source = "./modules/bar" + outputs = { + "key1" : { "Value" : module.foo.test }, + "key2" : { "Value" : local.known }, + "key3" : { "Value" : local.unknown }, + } +}`)}, + "modules/foo/main.tf": &fstest.MapFile{Data: []byte(`output "test" { + value = ref_to_unknown +}`)}, + "modules/bar/main.tf": &fstest.MapFile{Data: []byte(`variable "outputs" { + type = map(any) +}`)}, + } + + parser := New(fsys, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + _, err := parser.Load(t.Context()) + require.NoError(t, err) + + _, err = parser.EvaluateAll(t.Context()) + require.NoError(t, err) +} + +func TestAttributeWithMissingVarIsUnresolvable(t *testing.T) { + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{Data: []byte(`variable "inp" { + type = string +} + +resource "foo" "bar" { + attr = "${var.inp}-test" +} +`)}, + } + + parser := New(fsys, "", OptionStopOnHCLError(true)) + require.NoError(t, parser.ParseFS(t.Context(), ".")) + + _, err := parser.Load(t.Context()) + require.NoError(t, err) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + require.Len(t, modules, 1) + foo := modules[0].GetResourcesByType("foo")[0] + attr := foo.GetAttribute("attr") + assert.False(t, attr.IsResolvable()) +} + +// TestInstancedLogger checks if any global logs are generated by the terraform +// parser + evaluation. All logs should be attached to the instanced logger. This +// test does not run all error cases, so it will not capture all log output. +func TestInstancedLogger(t *testing.T) { + // reset global logger + prevLog := slog.Default() + defer slog.SetDefault(prevLog) + + // capture logs to the global logger + var buf bytes.Buffer + + slog.SetDefault(slog.New(log.NewHandler(&buf, &log.Options{ + Level: slog.LevelDebug, + }))) + + // create a new logger for the parser + var instance bytes.Buffer + logger := slog.New(log.NewHandler(&instance, &log.Options{ + Level: slog.LevelDebug, + })) + + opts := []Option{ + OptionWithLogger(logger), + OptionStopOnHCLError(false), + OptionWithDownloads(false), + } + // Run some scenarios that will trigger logs + t.Run("ParserLogs", func(t *testing.T) { + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{ + Data: []byte(` + bare words + `)}, + } + + parser := New(fsys, "", opts...) + + // No error is returned, but some parser logs are expected + err := parser.ParseFS(t.Context(), ".") + require.NoError(t, err) + require.NotEmpty(t, instance.String()) + instance.Reset() + }) + + t.Run("Evaluator", func(t *testing.T) { + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{ + Data: []byte(` + locals { + phrase = "hello world" + } + + output "phrase" { + value = locals.phrase + } + `)}, + } + + parser := New(fsys, "", opts...) + + err := parser.ParseFS(t.Context(), ".") + require.NoError(t, err) + + _, err = parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + require.NotEmpty(t, instance.String()) + instance.Reset() + }) + + t.Run("ModuleFetching", func(t *testing.T) { + fsys := testutil.CreateFS(map[string]string{ + "main.tf": ` + module "invalid" { + source = "totally.invalid" + } + + module "echo" { + source = "./echo" + input = "hello" + } + + locals { + foo = module.echo.value + } + `, + "echo/main.tf": ` + variable "input" { + type = string + } + output "value" { + value = var.input + } + `, + }) + + parser := New(fsys, "", opts...) + + err := parser.ParseFS(t.Context(), ".") + require.NoError(t, err) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + require.NotEmpty(t, instance.String()) + require.Len(t, modules, 2) + instance.Reset() + }) + + //nolint:testifylint // linter wants `emptyf`, but the output is not legible + if !assert.Emptyf(t, buf.Bytes(), "logs detected in global logger, all logs should be using the instanced logger") { + t.Log(buf.String()) // Helpful for debugging + } +} + +func TestProvidedWorkingDirectory(t *testing.T) { + const fakeCwd = "/some/path" + fsys := testutil.CreateFS(map[string]string{ + "main.tf": ` + resource "foo" "bar" { + cwd = path.cwd + } + `, + }) + + parser := New(fsys, "", OptionWithWorkingDirectoryPath(fakeCwd)) + err := parser.ParseFS(t.Context(), ".") + require.NoError(t, err) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + require.Len(t, modules, 1) + foo := modules[0].GetResourcesByType("foo")[0] + attr := foo.GetAttribute("cwd") + require.Equal(t, fakeCwd, attr.Value().AsString()) +} + +func Test_ResolveRemoteSubmoduleFromTerraformCache(t *testing.T) { + fsys := fstest.MapFS{ + "main.tf": &fstest.MapFile{Data: []byte(`module "aws_bucket" { + source = "github.com/foo/bar.git//aws/modules/bucket?ref=v0.0.1" +} + +locals { + test = module.aws_bucket.out +} +`)}, + ".terraform/modules/modules.json": &fstest.MapFile{Data: []byte(`{ + "Modules": [ + { "Key": "", "Source": "", "Dir": "." }, + { + "Key": "aws_bucket", + "Source": "git::https://github.com/foo/bar.git//aws/modules/bucket?ref=v0.0.1", + "Dir": ".terraform/modules/aws_bucket/aws/modules/bucket" + } + ] +}`)}, + ".terraform/modules/aws_bucket/aws/modules/bucket/main.tf": &fstest.MapFile{Data: []byte(`output "out" { + value = "some_value" +}`)}, + } + + parser := New(fsys, "", + OptionWithSkipCachedModules(true), + OptionWithDownloads(false), + OptionStopOnHCLError(true), + ) + err := parser.ParseFS(t.Context(), ".") + require.NoError(t, err) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + require.Len(t, modules, 2) + bucket := modules[0].GetBlocks().OfType("locals")[0] + attr := bucket.GetAttribute("test") + require.Equal(t, "some_value", attr.Value().AsString()) +} + +func Test_UnknownModuleSource(t *testing.T) { + files := map[string]string{ + `main.tf`: ` +module "test" { + source = "${foo}/modules/foo" +} +`, + } + + fsys := testutil.CreateFS(files) + parser := New(fsys, "", + OptionWithSkipCachedModules(true), + OptionStopOnHCLError(true), + ) + err := parser.ParseFS(t.Context(), ".") + require.NoError(t, err) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + require.Len(t, modules, 1) +} + +func Test_MarkedValues(t *testing.T) { + + tests := []struct { + name string + src string + }{ + { + name: "marked object", + src: `resource "foo" "bar" { + test = sensitive({}) +}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + files := map[string]string{ + `main.tf`: tt.src, + } + + fsys := testutil.CreateFS(files) + parser := New(fsys, "", + OptionWithSkipCachedModules(true), + OptionStopOnHCLError(true), + ) + err := parser.ParseFS(t.Context(), ".") + require.NoError(t, err) + + modules, err := parser.EvaluateAll(t.Context()) + require.NoError(t, err) + + require.Len(t, modules, 1) + }) + } +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/cache.go b/pkg/iac/scanners/terraform/parser/resolvers/cache.go new file mode 100644 index 000000000000..5e7641af3333 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/cache.go @@ -0,0 +1,75 @@ +package resolvers + +import ( + "context" + "crypto/md5" // #nosec + "encoding/hex" + "errors" + "io/fs" + "os" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/log" +) + +type cacheResolver struct{} + +var Cache = &cacheResolver{} + +const tempDirName = ".aqua" + +var defaultCacheDir = filepath.Join(os.TempDir(), tempDirName, "cache") + +func locateCacheFS(cacheDir string) (fs.FS, error) { + dir, err := locateCacheDir(cacheDir) + if err != nil { + return nil, err + } + return os.DirFS(dir), nil +} + +func locateCacheDir(cacheDir string) (string, error) { + if cacheDir == "" { + cacheDir = defaultCacheDir + } + + if err := os.MkdirAll(cacheDir, 0o750); err != nil { + return "", err + } + if !isWritable(cacheDir) { + return "", errors.New("cache directory is not writable") + } + return cacheDir, nil +} + +func (r *cacheResolver) Resolve(_ context.Context, _ fs.FS, opt Options) (filesystem fs.FS, prefix, downloadPath string, applies bool, err error) { + if opt.SkipCache { + opt.Logger.Debug("Module caching is disabled") + return nil, "", "", false, nil + } + cacheFS, err := locateCacheFS(opt.CacheDir) + if err != nil { + opt.Logger.Debug("No cache filesystem is available on this machine.", log.Err(err)) + return nil, "", "", false, nil + } + + src, subdir := splitPackageSubdirRaw(opt.Source) + key := cacheKey(src, opt.Version) + + opt.Logger.Debug("Trying to resolve module via cache", log.String("key", key)) + if info, err := fs.Stat(cacheFS, filepath.ToSlash(key)); err == nil && info.IsDir() { + opt.Logger.Debug("Module resolved from cache", log.String("key", key)) + cacheDir, err := locateCacheDir(opt.CacheDir) + if err != nil { + return nil, "", "", true, err + } + + return os.DirFS(filepath.Join(cacheDir, key)), opt.OriginalSource, subdir, true, nil + } + return nil, "", "", false, nil +} + +func cacheKey(source, version string) string { + hash := md5.Sum([]byte(source + ":" + version)) // #nosec + return hex.EncodeToString(hash[:]) +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/cache_integration_test.go b/pkg/iac/scanners/terraform/parser/resolvers/cache_integration_test.go new file mode 100644 index 000000000000..70ac9dad816a --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/cache_integration_test.go @@ -0,0 +1,168 @@ +//go:build unix + +package resolvers_test + +import ( + "context" + "io/fs" + "net/http" + "net/http/httptest" + "path" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/gittest" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser/resolvers" + "github.com/aquasecurity/trivy/pkg/log" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" +) + +type moduleResolver interface { + Resolve(context.Context, fs.FS, resolvers.Options) (fs.FS, string, string, bool, error) +} + +func testOptions(t *testing.T, source string) resolvers.Options { + return resolvers.Options{ + Source: source, + OriginalSource: source, + Version: "", + OriginalVersion: "", + AllowDownloads: true, + CacheDir: t.TempDir(), + Logger: log.WithPrefix("test"), + } +} + +func newRegistry(repoURL string) *httptest.Server { + mux := http.NewServeMux() + mux.HandleFunc("/v1/modules/terraform-aws-modules/s3-bucket/aws/download", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("X-Terraform-Get", repoURL) + w.WriteHeader(http.StatusNoContent) + }) + + return httptest.NewTLSServer(mux) +} + +func buildGitSource(repoURL string) string { return "git::" + repoURL } + +func TestResolveModuleFromCache(t *testing.T) { + + repo := "terraform-aws-s3-bucket" + gs := gittest.NewServer(t, repo, "testdata/terraform-aws-s3-bucket", gittest.Options{}) + defer gs.Close() + + repoURL := gs.URL + "/" + repo + ".git" + + registry := newRegistry(buildGitSource(repoURL)) + defer registry.Close() + + registryAddress := strings.TrimPrefix(registry.URL, "https://") + + tests := []struct { + name string + opts resolvers.Options + firstResolver moduleResolver + expectedSubdir string + expectedString string + }{ + { + name: "registry", + opts: resolvers.Options{ + Source: registryAddress + "/terraform-aws-modules/s3-bucket/aws", + Client: &http.Client{ + Transport: xhttp.NewTransport(xhttp.Options{Insecure: true}).Build(), + }, + }, + firstResolver: resolvers.Registry, + expectedSubdir: ".", + expectedString: "# AWS S3 bucket Terraform module", + }, + { + name: "registry with subdir", + opts: resolvers.Options{ + Source: registryAddress + "/terraform-aws-modules/s3-bucket/aws//modules/object", + Client: &http.Client{ + Transport: xhttp.NewTransport(xhttp.Options{Insecure: true}).Build(), + }, + }, + firstResolver: resolvers.Registry, + expectedSubdir: "modules/object", + expectedString: "# S3 bucket object", + }, + { + name: "remote", + opts: resolvers.Options{ + Source: buildGitSource(repoURL), + }, + firstResolver: resolvers.Remote, + expectedSubdir: ".", + expectedString: "# AWS S3 bucket Terraform module", + }, + { + name: "remote with subdir", + opts: resolvers.Options{ + Source: buildGitSource(repoURL) + "//modules/object", + }, + firstResolver: resolvers.Remote, + expectedSubdir: "modules/object", + expectedString: "# S3 bucket object", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + tt.opts.OriginalSource = tt.opts.Source + tt.opts.AllowDownloads = true + tt.opts.CacheDir = t.TempDir() + tt.opts.Logger = log.WithPrefix("test") + + fsys, _, dir, _, err := tt.firstResolver.Resolve(t.Context(), nil, tt.opts) + require.NoError(t, err) + assert.Equal(t, tt.expectedSubdir, dir) + + b, err := fs.ReadFile(fsys, path.Join(dir, "README.md")) + require.NoError(t, err) + assert.Equal(t, tt.expectedString, string(b)) + + _, _, dir, _, err = resolvers.Cache.Resolve(t.Context(), fsys, tt.opts) + require.NoError(t, err) + assert.Equal(t, tt.expectedSubdir, dir) + + b, err = fs.ReadFile(fsys, path.Join(dir, "README.md")) + require.NoError(t, err) + assert.Equal(t, tt.expectedString, string(b)) + }) + } +} + +func TestResolveModuleFromCacheWithDifferentSubdir(t *testing.T) { + repo := "terraform-aws-s3-bucket" + gs := gittest.NewServer(t, repo, "testdata/terraform-aws-s3-bucket", gittest.Options{}) + defer gs.Close() + + repoURL := gs.URL + "/" + repo + ".git" + + fsys, _, dir, _, err := resolvers.Remote.Resolve( + t.Context(), nil, + testOptions(t, "git::"+repoURL+"//modules/object"), + ) + require.NoError(t, err) + + b, err := fs.ReadFile(fsys, path.Join(dir, "README.md")) + require.NoError(t, err) + assert.Equal(t, "# S3 bucket object", string(b)) + + fsys, _, dir, _, err = resolvers.Remote.Resolve( + t.Context(), nil, + testOptions(t, "git::"+repoURL+"//modules/notification"), + ) + require.NoError(t, err) + + b, err = fs.ReadFile(fsys, path.Join(dir, "README.md")) + require.NoError(t, err) + assert.Equal(t, "# S3 bucket notification", string(b)) +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/local.go b/pkg/iac/scanners/terraform/parser/resolvers/local.go new file mode 100644 index 000000000000..a11a294edcfb --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/local.go @@ -0,0 +1,33 @@ +package resolvers + +import ( + "context" + "io/fs" + "path" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/log" +) + +type localResolver struct{} + +var Local = &localResolver{} + +func (r *localResolver) Resolve(_ context.Context, target fs.FS, opt Options) (filesystem fs.FS, prefix, downloadPath string, applies bool, err error) { + if !opt.hasPrefix(".", "..") { + return nil, "", "", false, nil + } + joined := path.Clean(path.Join(opt.ModulePath, opt.Source)) + if _, err := fs.Stat(target, filepath.ToSlash(joined)); err == nil { + opt.Logger.Debug("Module resolved locally", + log.String("name", opt.Name), log.FilePath(joined), + ) + return target, "", joined, true, nil + } + + clean := path.Clean(opt.Source) + opt.Logger.Debug("Module resolved locally", + log.String("name", opt.Name), log.FilePath(clean), + ) + return target, "", clean, true, nil +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/options.go b/pkg/iac/scanners/terraform/parser/resolvers/options.go new file mode 100644 index 000000000000..937f89709dc3 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/options.go @@ -0,0 +1,27 @@ +package resolvers + +import ( + "net/http" + "strings" + + "github.com/aquasecurity/trivy/pkg/log" +) + +type Options struct { + Source, OriginalSource, Version, OriginalVersion, WorkingDir, Name, ModulePath string + Logger *log.Logger + AllowDownloads bool + SkipCache bool + RelativePath string + CacheDir string + Client *http.Client +} + +func (o *Options) hasPrefix(prefixes ...string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(o.Source, prefix) { + return true + } + } + return false +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/registry.go b/pkg/iac/scanners/terraform/parser/resolvers/registry.go new file mode 100644 index 000000000000..56e7fcb8c867 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/registry.go @@ -0,0 +1,222 @@ +package resolvers + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/fs" + "net/http" + "os" + "sort" + "strings" + "time" + + "golang.org/x/net/idna" + + "github.com/aquasecurity/go-version/pkg/version" + "github.com/aquasecurity/trivy/pkg/log" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" +) + +type registryResolver struct { + client *http.Client +} + +var Registry = ®istryResolver{ + // give it a maximum 5 seconds to resolve the module + client: xhttp.Client(xhttp.WithTimeout(5 * time.Second)), +} + +type moduleVersions struct { + Modules []moduleProviderVersions `json:"modules"` +} + +type moduleProviderVersions struct { + Versions []moduleVersion `json:"versions"` +} + +type moduleVersion struct { + Version string `json:"version"` +} + +const registryHostname = "registry.terraform.io" + +// nolint +func (r *registryResolver) Resolve(ctx context.Context, target fs.FS, opt Options) (filesystem fs.FS, prefix string, downloadPath string, applies bool, err error) { + + client := r.client + if opt.Client != nil { + client = opt.Client + } + + if !opt.AllowDownloads { + return + } + + inputVersion := opt.Version + source, _ := splitPackageSubdirRaw(opt.OriginalSource) + parts := strings.Split(source, "/") + if len(parts) < 3 || len(parts) > 4 { + return + } + + hostname := registryHostname + var token string + if len(parts) == 4 { + hostname = parts[0] + parts = parts[1:] + + token, err = getPrivateRegistryTokenFromEnvVars(hostname) + if err == nil { + opt.Logger.Debug("Found a token for the registry", log.String("hostname", hostname)) + } else { + opt.Logger.Error( + "Failed to find a token for the registry", + log.String("hostname", hostname), log.Err(err)) + } + } + + moduleName := strings.Join(parts, "/") + + if opt.Version != "" { + versionUrl := fmt.Sprintf("https://%s/v1/modules/%s/versions", hostname, moduleName) + opt.Logger.Debug("Requesting module versions from registry using", + log.String("url", versionUrl)) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, versionUrl, nil) + if err != nil { + return nil, "", "", true, err + } + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + resp, err := client.Do(req) + if err != nil { + return nil, "", "", true, err + } + defer func() { _ = resp.Body.Close() }() + if resp.StatusCode != http.StatusOK { + return nil, "", "", true, fmt.Errorf("unexpected status code for versions endpoint: %d", resp.StatusCode) + } + var availableVersions moduleVersions + if err := json.NewDecoder(resp.Body).Decode(&availableVersions); err != nil { + return nil, "", "", true, err + } + + opt.Version, err = resolveVersion(inputVersion, availableVersions) + if err != nil { + return nil, "", "", true, err + } + opt.Logger.Debug("Found module version", + log.String("version", opt.Version), log.String("constraint", inputVersion)) + } + + var url string + if opt.Version == "" { + url = fmt.Sprintf("https://%s/v1/modules/%s/download", hostname, moduleName) + } else { + url = fmt.Sprintf("https://%s/v1/modules/%s/%s/download", hostname, moduleName, opt.Version) + } + + opt.Logger.Debug("Requesting module source from registry", log.String("url", url)) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return nil, "", "", true, err + } + if token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + if opt.Version != "" { + req.Header.Set("X-Terraform-Version", opt.Version) + } + + resp, err := client.Do(req) + if err != nil { + return nil, "", "", true, err + } + defer func() { _ = resp.Body.Close() }() + + // OpenTofu may return 200 with body + switch resp.StatusCode { + case http.StatusOK: + // https://opentofu.org/docs/internals/module-registry-protocol/#sample-response-1 + var downloadResponse struct { + Location string `json:"location"` + } + if err := json.NewDecoder(resp.Body).Decode(&downloadResponse); err != nil { + return nil, "", "", true, fmt.Errorf("failed to decode download response: %w", err) + } + + opt.Source = downloadResponse.Location + case http.StatusNoContent: + opt.Source = resp.Header.Get("X-Terraform-Get") + default: + return nil, "", "", true, fmt.Errorf("unexpected status code: %d", resp.StatusCode) + } + + if opt.Source == "" { + return nil, "", "", true, fmt.Errorf("no source was found for the registry at %s", hostname) + } + + opt.Logger.Debug("Module resolved via registry to new source", + log.String("source", opt.Source), log.String("name", moduleName)) + + filesystem, prefix, downloadPath, _, err = Remote.Resolve(ctx, target, opt) + if err != nil { + return nil, "", "", true, err + } + + return filesystem, prefix, downloadPath, true, nil +} + +func getPrivateRegistryTokenFromEnvVars(hostname string) (string, error) { + token := "" + asciiHostname, err := idna.ToASCII(hostname) + if err != nil { + return "", fmt.Errorf("could not convert hostname %s to a punycode encoded ASCII string so cannot find token for this registry", hostname) + } + + envVar := fmt.Sprintf("TF_TOKEN_%s", strings.ReplaceAll(asciiHostname, ".", "_")) + token = os.Getenv(envVar) + + // Dashes in the hostname can optionally be converted to double underscores + if token == "" { + envVar = strings.ReplaceAll(envVar, "-", "__") + token = os.Getenv(envVar) + } + + if token == "" { + return "", fmt.Errorf("no token was found for the registry at %s", hostname) + } + return token, nil +} + +func resolveVersion(input string, versions moduleVersions) (string, error) { + if len(versions.Modules) != 1 { + return "", fmt.Errorf("1 module expected, found %d", len(versions.Modules)) + } + if len(versions.Modules[0].Versions) == 0 { + return "", errors.New("no available versions for module") + } + + constraints, err := version.NewConstraints(input) + if err != nil { + return "", err + } + var realVersions version.Collection + for _, rawVersion := range versions.Modules[0].Versions { + realVersion, err := version.Parse(rawVersion.Version) + if err != nil { + continue + } + realVersions = append(realVersions, realVersion) + } + sort.Sort(sort.Reverse(realVersions)) + for _, realVersion := range realVersions { + if constraints.Check(realVersion) { + return realVersion.String(), nil + } + } + return "", fmt.Errorf("no available versions for module constraint '%s'", input) +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/registry_test.go b/pkg/iac/scanners/terraform/parser/resolvers/registry_test.go new file mode 100644 index 000000000000..4618cb2d7017 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/registry_test.go @@ -0,0 +1,131 @@ +package resolvers + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func Test_getPrivateRegistryTokenFromEnvVars_ErrorsWithNoEnvVarSet(t *testing.T) { + token, err := getPrivateRegistryTokenFromEnvVars("registry.example.com") + assert.Empty(t, token) + assert.Equal(t, "no token was found for the registry at registry.example.com", err.Error()) +} + +func Test_getPrivateRegistryTokenFromEnvVars_ConvertsSiteNameToEnvVar(t *testing.T) { + tests := []struct { + name string + siteName string + tokenName string + }{ + { + name: "returns string when simple env var set", + siteName: "registry.example.com", + tokenName: "TF_TOKEN_registry_example_com", + }, + { + name: "allows dashes in hostname to be dashes", + siteName: "my-registry.example.com", + tokenName: "TF_TOKEN_my-registry_example_com", + }, + { + name: "allows dashes in hostname to be double underscores", + siteName: "my-registry.example.com", + tokenName: "TF_TOKEN_my__registry_example_com", + }, + { + name: "handles utf8 to punycode correctly", + siteName: "例えば.com", + tokenName: "TF_TOKEN_xn--r8j3dr99h_com", + }, + { + name: "handles punycode with dash to underscore conversion", + siteName: "café.fr", + tokenName: "TF_TOKEN_xn____caf__dma_fr", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(tt.tokenName, "abcd") + token, err := getPrivateRegistryTokenFromEnvVars(tt.siteName) + assert.Equal(t, "abcd", token) + require.NoError(t, err) + }) + } +} + +func Test_resolveVersion(t *testing.T) { + makeModuleVersions := func(versions ...string) moduleVersions { + return moduleVersions{ + Modules: []moduleProviderVersions{ + {Versions: xslices.Map(versions, func(v string) moduleVersion { + return moduleVersion{Version: v} + })}, + }, + } + } + + tests := []struct { + name string + input string + versions moduleVersions + want string + wantErr string + }{ + { + name: "pessimistic constraint ~> 3.1", + input: "~> 3.1", + versions: makeModuleVersions("3.0.0", "3.1.0", "3.2.0", "4.0.0"), + want: "3.2.0", + }, + { + name: "exact version = 3.1.0", + input: "= 3.1.0", + versions: makeModuleVersions("3.0.0", "3.1.0", "3.1.1"), + want: "3.1.0", + }, + { + name: "empty constraint returns error", + input: "", + versions: makeModuleVersions("1.0.0", "2.0.0", "3.0.0"), + wantErr: "improper constraint", + }, + { + name: "invalid constraint", + input: ">> 3.0", + versions: makeModuleVersions("3.0.0", "3.1.0"), + wantErr: "improper constraint", + }, + { + name: "no modules", + input: "~> 1.0", + versions: moduleVersions{}, + wantErr: "1 module expected, found 0", + }, + { + name: "empty version list", + input: "~> 1.0", + versions: moduleVersions{ + Modules: []moduleProviderVersions{{Versions: nil}}, + }, + wantErr: "no available versions for module", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := resolveVersion(tt.input, tt.versions) + if tt.wantErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantErr) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, got) + } + }) + } +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/remote.go b/pkg/iac/scanners/terraform/parser/resolvers/remote.go new file mode 100644 index 000000000000..bd4586a2e550 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/remote.go @@ -0,0 +1,104 @@ +package resolvers + +import ( + "context" + "fmt" + "io/fs" + "os" + "path/filepath" + "sync/atomic" + + "github.com/hashicorp/go-getter" + + "github.com/aquasecurity/trivy/pkg/log" +) + +type remoteResolver struct { + count atomic.Int32 +} + +var Remote = &remoteResolver{} + +func (r *remoteResolver) incrementCount(o Options) { + + r.count.Add(1) + o.Logger.Debug("Incrementing the download counter", log.Int("count", int(r.count.Load()))) +} + +func (r *remoteResolver) GetDownloadCount() int { + return int(r.count.Load()) +} + +func (r *remoteResolver) Resolve(ctx context.Context, _ fs.FS, opt Options) (filesystem fs.FS, prefix, downloadPath string, applies bool, err error) { + if !opt.hasPrefix("github.com/", "bitbucket.org/", "s3:", "git@", "git:", "hg:", "https:", "gcs:") { + return nil, "", "", false, nil + } + + if !opt.AllowDownloads { + return nil, "", "", false, nil + } + + origSrc, subdir := splitPackageSubdirRaw(opt.OriginalSource) + key := cacheKey(origSrc, opt.OriginalVersion) + opt.Logger.Debug("Caching module", log.String("key", key)) + + baseCacheDir, err := locateCacheDir(opt.CacheDir) + if err != nil { + return nil, "", "", true, fmt.Errorf("failed to locate cache directory: %w", err) + } + + cacheDir := filepath.Join(baseCacheDir, key) + + src, _ := splitPackageSubdirRaw(opt.Source) + + opt.Source = src + if err := r.download(ctx, opt, cacheDir); err != nil { + return nil, "", "", true, err + } + + r.incrementCount(opt) + opt.Logger.Debug("Successfully resolve module via remote download", + log.String("name", opt.Name), + log.String("source", opt.OriginalSource), + ) + return os.DirFS(cacheDir), opt.OriginalSource, subdir, true, nil +} + +func (r *remoteResolver) download(ctx context.Context, opt Options, dst string) error { + _ = os.RemoveAll(dst) + if err := os.MkdirAll(filepath.Dir(dst), 0o750); err != nil { + return err + } + + // Overwrite the file getter so that a file will be copied + getter.Getters["file"] = &getter.FileGetter{Copy: true} + + opt.Logger.Debug("Downloading module", log.String("source", opt.Source)) + + // Build the client + client := &getter.Client{ + Ctx: ctx, + Src: opt.Source, + Dst: dst, + Pwd: opt.WorkingDir, + Getters: getter.Getters, + Mode: getter.ClientModeAny, + } + + terminalPrompt := os.Getenv("GIT_TERMINAL_PROMPT") + if err := os.Setenv("GIT_TERMINAL_PROMPT", "0"); err != nil { + opt.Logger.Error("Failed to set env", log.String("name", "GIT_TERMINAL_PROMPT"), log.Err(err)) + } else { + defer os.Setenv("GIT_TERMINAL_PROMPT", terminalPrompt) + } + + if err := client.Get(); err != nil { + return fmt.Errorf("failed to download: %w", err) + } + + return nil +} + +func (r *remoteResolver) GetSourcePrefix(source string) string { + return source +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/source.go b/pkg/iac/scanners/terraform/parser/resolvers/source.go new file mode 100644 index 000000000000..ee5662d6c7d0 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/source.go @@ -0,0 +1,37 @@ +package resolvers + +import "strings" + +func splitPackageSubdirRaw(src string) (string, string) { + stop := len(src) + if idx := strings.Index(src, "?"); idx > -1 { + stop = idx + } + + // Calculate an offset to avoid accidentally marking the scheme + // as the dir. + var offset int + if idx := strings.Index(src[:stop], "://"); idx > -1 { + offset = idx + 3 + } + + // First see if we even have an explicit subdir + idx := strings.Index(src[offset:stop], "//") + if idx == -1 { + return src, "." + } + + idx += offset + subdir := src[idx+2:] + src = src[:idx] + + // Next, check if we have query parameters and push them onto the + // URL. + if idx = strings.Index(subdir, "?"); idx > -1 { + query := subdir[idx:] + subdir = subdir[:idx] + src += query + } + + return src, subdir +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/source_test.go b/pkg/iac/scanners/terraform/parser/resolvers/source_test.go new file mode 100644 index 000000000000..46df7db700dc --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/source_test.go @@ -0,0 +1,50 @@ +package resolvers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSplitPackageSubdirRaw(t *testing.T) { + + tests := []struct { + name string + source string + expectedPkg string + expectedSubdir string + }{ + { + name: "address with scheme and query string", + source: "git::https://github.com/aquasecurity/terraform-modules.git//modules/ecs-service?ref=v0.1.0", + expectedPkg: "git::https://github.com/aquasecurity/terraform-modules.git?ref=v0.1.0", + expectedSubdir: "modules/ecs-service", + }, + { + name: "address with scheme", + source: "git::https://github.com/aquasecurity/terraform-modules.git//modules/ecs-service", + expectedPkg: "git::https://github.com/aquasecurity/terraform-modules.git", + expectedSubdir: "modules/ecs-service", + }, + { + name: "registry address", + source: "hashicorp/consul/aws//modules/consul-cluster", + expectedPkg: "hashicorp/consul/aws", + expectedSubdir: "modules/consul-cluster", + }, + { + name: "without subdir", + source: `hashicorp/consul/aws`, + expectedPkg: `hashicorp/consul/aws`, + expectedSubdir: ".", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + pkgAddr, subdir := splitPackageSubdirRaw(test.source) + assert.Equal(t, test.expectedPkg, pkgAddr) + assert.Equal(t, test.expectedSubdir, subdir) + }) + } +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/testdata/terraform-aws-s3-bucket/README.md b/pkg/iac/scanners/terraform/parser/resolvers/testdata/terraform-aws-s3-bucket/README.md new file mode 100644 index 000000000000..26e6186c9cbe --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/testdata/terraform-aws-s3-bucket/README.md @@ -0,0 +1 @@ +# AWS S3 bucket Terraform module \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/resolvers/testdata/terraform-aws-s3-bucket/modules/notification/README.md b/pkg/iac/scanners/terraform/parser/resolvers/testdata/terraform-aws-s3-bucket/modules/notification/README.md new file mode 100644 index 000000000000..9f2f884a9586 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/testdata/terraform-aws-s3-bucket/modules/notification/README.md @@ -0,0 +1 @@ +# S3 bucket notification \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/resolvers/testdata/terraform-aws-s3-bucket/modules/object/README.md b/pkg/iac/scanners/terraform/parser/resolvers/testdata/terraform-aws-s3-bucket/modules/object/README.md new file mode 100644 index 000000000000..a016e2bb23cb --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/testdata/terraform-aws-s3-bucket/modules/object/README.md @@ -0,0 +1 @@ +# S3 bucket object \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/resolvers/writable.go b/pkg/iac/scanners/terraform/parser/resolvers/writable.go new file mode 100644 index 000000000000..84f471f779c2 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/writable.go @@ -0,0 +1,36 @@ +//go:build !windows +// +build !windows + +package resolvers + +import ( + "os" + "syscall" +) + +func isWritable(path string) bool { + info, err := os.Stat(path) + if err != nil { + return false + } + + if !info.IsDir() { + return false + } + + // Check if the user bit is enabled in file permission + if info.Mode().Perm()&(1<<(uint(7))) == 0 { + return false + } + + var stat syscall.Stat_t + if err = syscall.Stat(path, &stat); err != nil { + return false + } + + if uint32(os.Geteuid()) != stat.Uid { + return false + } + + return true +} diff --git a/pkg/iac/scanners/terraform/parser/resolvers/writable_windows.go b/pkg/iac/scanners/terraform/parser/resolvers/writable_windows.go new file mode 100644 index 000000000000..69cb3c7169b1 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/resolvers/writable_windows.go @@ -0,0 +1,24 @@ +package resolvers + +import ( + "os" +) + +func isWritable(path string) bool { + + info, err := os.Stat(path) + if err != nil { + return false + } + + if !info.IsDir() { + return false + } + + // Check if the user bit is enabled in file permission + if info.Mode().Perm()&(1<<(uint(7))) == 0 { + return false + } + + return true +} diff --git a/pkg/iac/scanners/terraform/parser/sort.go b/pkg/iac/scanners/terraform/parser/sort.go new file mode 100644 index 000000000000..28fc79b1990c --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/sort.go @@ -0,0 +1,58 @@ +package parser + +import ( + "sort" + + "github.com/aquasecurity/trivy/pkg/iac/terraform" +) + +func sortBlocksByHierarchy(blocks terraform.Blocks) { + c := &counter{ + cache: make(map[string]int), + } + sort.Slice(blocks, func(i, j int) bool { + a := blocks[i] + b := blocks[j] + iDepth, jDepth := c.countBlockRecursion(a, blocks, 0), c.countBlockRecursion(b, blocks, 0) + switch { + case iDepth < jDepth: + return true + case iDepth > jDepth: + return false + default: + return blocks[i].FullName() < blocks[j].FullName() + } + }) +} + +type counter struct { + cache map[string]int +} + +func (c *counter) countBlockRecursion(block *terraform.Block, blocks terraform.Blocks, count int) int { + metadata := block.GetMetadata() + if cached, ok := c.cache[metadata.Reference()]; ok { + return cached + } + var maxCount int + var hasRecursion bool + for _, attrName := range []string{"for_each", "count"} { + if attr := block.GetAttribute(attrName); attr.IsNotNil() { + hasRecursion = true + for _, other := range blocks { + if attr.ReferencesBlock(other) { + depth := c.countBlockRecursion(other, blocks, count) + if depth > maxCount { + maxCount = depth + } + } + } + } + } + if hasRecursion { + maxCount++ + } + result := maxCount + count + c.cache[metadata.Reference()] = result + return result +} diff --git a/pkg/iac/scanners/terraform/parser/testdata/cached-modules/.terraform/modules/modules.json b/pkg/iac/scanners/terraform/parser/testdata/cached-modules/.terraform/modules/modules.json new file mode 100644 index 000000000000..76be610c1339 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/cached-modules/.terraform/modules/modules.json @@ -0,0 +1 @@ +{"Modules":[{"Key":"","Source":"","Dir":"."},{"Key":"s3-bucket","Source":"registry.myregistry.org/my-private-module/s3-bucket/aws","Version":"1.0.0","Dir":".terraform/modules/s3-bucket"}]} \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/testdata/cached-modules/.terraform/modules/s3-bucket/main.tf b/pkg/iac/scanners/terraform/parser/testdata/cached-modules/.terraform/modules/s3-bucket/main.tf new file mode 100644 index 000000000000..68506b9930b8 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/cached-modules/.terraform/modules/s3-bucket/main.tf @@ -0,0 +1,7 @@ +variable "bucket" { + type = string +} + +resource "aws_s3_bucket" "this" { + bucket = var.bucket +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/testdata/cached-modules/main.tf b/pkg/iac/scanners/terraform/parser/testdata/cached-modules/main.tf new file mode 100644 index 000000000000..231058b26e01 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/cached-modules/main.tf @@ -0,0 +1,5 @@ +module "s3-bucket" { + source = "my-private-module/s3-bucket/aws" + version = "1.0.0" + bucket = "my-s3-bucket" +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/testdata/nested/main.tf b/pkg/iac/scanners/terraform/parser/testdata/nested/main.tf new file mode 100644 index 000000000000..1012ab57bfd7 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/nested/main.tf @@ -0,0 +1,9 @@ +module "north-america" { + source = "./north-america" +} + +output "all" { + value = [ + module.north-america, + ] +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/canada/canada.tf b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/canada/canada.tf new file mode 100644 index 000000000000..bb27996a0187 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/canada/canada.tf @@ -0,0 +1,17 @@ +variable "prefix" { + type = string + default = "" +} + +output "name" { + value = "${var.prefix}canada" +} + +module "ontario-springfield" { + source = "./springfield" + prefix = "ontario-" +} + +output "ontario-springfield" { + value = module.ontario-springfield.name +} diff --git a/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/canada/springfield/springfield.tf b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/canada/springfield/springfield.tf new file mode 100644 index 000000000000..e8768e67e2a8 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/canada/springfield/springfield.tf @@ -0,0 +1,8 @@ +variable "prefix" { + type = string + default = "" +} + +output "name" { + value = "${var.prefix}springfield" +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/north-america.tf b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/north-america.tf new file mode 100644 index 000000000000..26b53f5f9d8c --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/north-america.tf @@ -0,0 +1,22 @@ +variable "prefix" { + type = string + default = "" +} + +output "name" { + value = "${var.prefix}north-america" +} + +module "canada" { + source = "./canada" + prefix = "" +} + +module "united-states" { + source = "./united-states" + prefix = "" +} + +output "united-states" { + value = module.united-states.name +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/new-york/new-york-city/new-york-city.tf b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/new-york/new-york-city/new-york-city.tf new file mode 100644 index 000000000000..e0a024179706 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/new-york/new-york-city/new-york-city.tf @@ -0,0 +1,8 @@ +variable "prefix" { + type = string + default = "" +} + +output "name" { + value = "${var.prefix}new-york-city" +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/new-york/new-york.tf b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/new-york/new-york.tf new file mode 100644 index 000000000000..fc87248b5756 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/new-york/new-york.tf @@ -0,0 +1,17 @@ +variable "prefix" { + type = string + default = "" +} + +output "name" { + value = "${var.prefix}new-york" +} + +module "new-york-city" { + source = "./new-york-city" + prefix = "" +} + +output "new-york-city" { + value = module.new-york-city.name +} diff --git a/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/springfield/springfield.tf b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/springfield/springfield.tf new file mode 100644 index 000000000000..e8768e67e2a8 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/springfield/springfield.tf @@ -0,0 +1,8 @@ +variable "prefix" { + type = string + default = "" +} + +output "name" { + value = "${var.prefix}springfield" +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/united-states.tf b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/united-states.tf new file mode 100644 index 000000000000..c6fff86f38bb --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/nested/north-america/united-states/united-states.tf @@ -0,0 +1,38 @@ +variable "prefix" { + type = string + default = "" +} + +output "name" { + value = "${var.prefix}united-states" +} + +// Same module twice, with different variables +module "illinois-springfield" { + source = "./springfield" + prefix = "illinois-" +} + +output "illinois-springfield" { + value = module.illinois-springfield.name +} + +module "idaho-springfield" { + source = "./springfield" + prefix = "idaho-" +} + +output "idaho-springfield" { + value = module.idaho-springfield.name +} + +module "new-york" { + source = "./new-york" + prefix = "" +} + +output "new-york" { + value = module.new-york.name +} + + diff --git a/pkg/iac/scanners/terraform/parser/testdata/tfvars/terraform.tfvars b/pkg/iac/scanners/terraform/parser/testdata/tfvars/terraform.tfvars new file mode 100644 index 000000000000..23fee69e2bb1 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/tfvars/terraform.tfvars @@ -0,0 +1 @@ +instance_type = "t2.large" \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/parser/testdata/tfvars/terraform.tfvars.json b/pkg/iac/scanners/terraform/parser/testdata/tfvars/terraform.tfvars.json new file mode 100644 index 000000000000..bde0e75763b1 --- /dev/null +++ b/pkg/iac/scanners/terraform/parser/testdata/tfvars/terraform.tfvars.json @@ -0,0 +1,10 @@ +{ + "variable": { + "foo": { + "default": "bar" + }, + "baz": "qux" + }, + "foo2": true, + "foo3": 3 +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraform/scanner.go b/pkg/iac/scanners/terraform/scanner.go new file mode 100644 index 000000000000..53bcd9ab61cb --- /dev/null +++ b/pkg/iac/scanners/terraform/scanner.go @@ -0,0 +1,238 @@ +package terraform + +import ( + "context" + "fmt" + "io/fs" + "path" + "path/filepath" + "sort" + "strings" + "sync" + + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/executor" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser" + "github.com/aquasecurity/trivy/pkg/iac/terraform" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +var _ scanners.FSScanner = (*Scanner)(nil) +var _ options.ConfigurableScanner = (*Scanner)(nil) +var _ options.RawConfigScanner = (*Scanner)(nil) +var _ ConfigurableTerraformScanner = (*Scanner)(nil) + +type Scanner struct { + *rego.RegoScannerProvider + logger *log.Logger + options []options.ScannerOption + parserOpt []parser.Option + executorOpt []executor.Option + dirs set.Set[string] + forceAllDirs bool + execLock sync.RWMutex +} + +func (s *Scanner) Name() string { + return "Terraform" +} + +func (s *Scanner) SetForceAllDirs(b bool) { + s.forceAllDirs = b +} + +func (s *Scanner) SetScanRawConfig(b bool) { + s.AddExecutorOptions(executor.OptionWithScanRawConfig(b)) +} + +func (s *Scanner) AddParserOptions(opts ...parser.Option) { + s.parserOpt = append(s.parserOpt, opts...) +} + +func (s *Scanner) AddExecutorOptions(opts ...executor.Option) { + s.executorOpt = append(s.executorOpt, opts...) +} + +func New(opts ...options.ScannerOption) *Scanner { + s := &Scanner{ + RegoScannerProvider: rego.NewRegoScannerProvider(opts...), + dirs: set.New[string](), + options: opts, + logger: log.WithPrefix("terraform scanner"), + } + for _, opt := range opts { + opt(s) + } + return s +} + +// terraformRootModule represents the module to be used as the root module for Terraform deployment. +type terraformRootModule struct { + rootPath string + childs terraform.Modules + fsMap map[string]fs.FS +} + +func (s *Scanner) ScanFS(ctx context.Context, target fs.FS, dir string) (scan.Results, error) { + + s.logger.Debug("Scanning directory", log.FilePath(dir)) + + // find directories which directly contain tf files + modulePaths := s.findModules(target, dir, dir) + sort.Strings(modulePaths) + + if len(modulePaths) == 0 { + s.logger.Info("No modules found, skipping directory", log.FilePath(dir)) + return nil, nil + } + + rs, err := s.InitRegoScanner(target, s.options) + if err != nil { + return nil, fmt.Errorf("init rego scanner: %w", err) + } + + s.execLock.Lock() + s.executorOpt = append(s.executorOpt, executor.OptionWithRegoScanner(rs)) + s.execLock.Unlock() + + var allResults scan.Results + + p := parser.New(target, "", s.parserOpt...) + rootDirs, err := p.FindRootModules(ctx, modulePaths) + if err != nil { + return nil, fmt.Errorf("failed to find root modules: %w", err) + } + + rootModules := make([]terraformRootModule, 0, len(rootDirs)) + + // parse all root module directories + for _, dir := range rootDirs { + + s.logger.Info("Scanning root module", log.FilePath(dir)) + + p := parser.New(target, "", s.parserOpt...) + + if err := p.ParseFS(ctx, dir); err != nil { + return nil, err + } + + modules, err := p.EvaluateAll(ctx) + if err != nil { + return nil, err + } + + rootModules = append(rootModules, terraformRootModule{ + rootPath: dir, + childs: modules, + fsMap: p.GetFilesystemMap(), + }) + } + + for _, module := range rootModules { + s.execLock.RLock() + e := executor.New(s.executorOpt...) + s.execLock.RUnlock() + results, err := e.Execute(ctx, module.childs, module.rootPath) + if err != nil { + return nil, err + } + + for i, result := range results { + if result.Metadata().Range().GetFS() != nil { + continue + } + key := result.Metadata().Range().GetFSKey() + if key == "" { + continue + } + if filesystem, ok := module.fsMap[key]; ok { + override := scan.Results{ + result, + } + override.SetSourceAndFilesystem(result.Range().GetSourcePrefix(), filesystem, false) + results[i] = override[0] + } + } + + allResults = append(allResults, results...) + } + + return allResults, nil +} + +func (s *Scanner) removeNestedDirs(dirs []string) []string { + if s.forceAllDirs { + return dirs + } + var clean []string + for _, dirA := range dirs { + dirOK := true + for _, dirB := range dirs { + if dirA == dirB { + continue + } + if str, err := filepath.Rel(dirB, dirA); err == nil && !strings.HasPrefix(str, "..") { + dirOK = false + break + } + } + if dirOK { + clean = append(clean, dirA) + } + } + return clean +} + +func (s *Scanner) findModules(target fs.FS, scanDir string, dirs ...string) []string { + + var roots []string + var others []string + + for _, dir := range dirs { + if s.isRootModule(target, dir) { + roots = append(roots, dir) + if !s.forceAllDirs { + continue + } + } + + // if this isn't a root module, look at directories inside it + files, err := fs.ReadDir(target, filepath.ToSlash(dir)) + if err != nil { + continue + } + for _, file := range files { + if file.IsDir() { + others = append(others, path.Join(dir, file.Name())) + } + } + } + + if (len(roots) == 0 || s.forceAllDirs) && len(others) > 0 { + roots = append(roots, s.findModules(target, scanDir, others...)...) + } + + return s.removeNestedDirs(roots) +} + +func (s *Scanner) isRootModule(target fs.FS, dir string) bool { + files, err := fs.ReadDir(target, filepath.ToSlash(dir)) + if err != nil { + s.logger.Error("Failed to read dir", log.FilePath(dir), log.Err(err)) + return false + } + suffixes := []string{".tf", ".tf.json", ".tofu", ".tofu.json"} + + for _, file := range files { + for _, suf := range suffixes { + if strings.HasSuffix(file.Name(), suf) { + return true + } + } + } + return false +} diff --git a/pkg/iac/scanners/terraform/scanner_test.go b/pkg/iac/scanners/terraform/scanner_test.go new file mode 100644 index 000000000000..ed84fd3c9170 --- /dev/null +++ b/pkg/iac/scanners/terraform/scanner_test.go @@ -0,0 +1,1272 @@ +package terraform + +import ( + "fmt" + "strconv" + "strings" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" +) + +func Test_OptionWithPolicyDirs(t *testing.T) { + + fsys := testutil.CreateFS(map[string]string{ + "/code/main.tf": `resource "aws_s3_bucket" "my-bucket" {}`, + "/rules/test.rego": emptyBucketCheck, + }) + + results, err := scanFS(fsys, "code", + rego.WithPolicyFilesystem(fsys), + rego.WithPolicyDirs("rules"), + rego.WithPolicyNamespaces("user"), + ) + require.NoError(t, err) + require.Len(t, results.GetFailed(), 1) + + failure := results.GetFailed()[0] + assert.Equal(t, "USER-TEST-0123", failure.Rule().ID) + + actualCode, err := failure.GetCode() + require.NoError(t, err) + for i := range actualCode.Lines { + actualCode.Lines[i].Highlighted = "" + } + assert.Equal(t, []scan.Line{ + { + Number: 1, + Content: "resource \"aws_s3_bucket\" \"my-bucket\" {}", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, actualCode.Lines) + +} + +func Test_OptionWithPolicyNamespaces(t *testing.T) { + + tests := []struct { + includedNamespaces []string + policyNamespace string + wantFailure bool + }{ + { + includedNamespaces: nil, + policyNamespace: "blah", + wantFailure: false, + }, + { + includedNamespaces: nil, + policyNamespace: "appshield.something", + wantFailure: true, + }, + { + includedNamespaces: nil, + policyNamespace: "defsec.blah", + wantFailure: true, + }, + { + includedNamespaces: []string{"user"}, + policyNamespace: "users", + wantFailure: false, + }, + { + includedNamespaces: []string{"users"}, + policyNamespace: "something.users", + wantFailure: false, + }, + { + includedNamespaces: []string{"users"}, + policyNamespace: "users", + wantFailure: true, + }, + { + includedNamespaces: []string{"users"}, + policyNamespace: "users.my_rule", + wantFailure: true, + }, + { + includedNamespaces: []string{ + "a", + "users", + "b", + }, + policyNamespace: "users", + wantFailure: true, + }, + { + includedNamespaces: []string{"user"}, + policyNamespace: "defsec", + wantFailure: true, + }, + } + + for i, test := range tests { + + t.Run(strconv.Itoa(i), func(t *testing.T) { + + fs := testutil.CreateFS(map[string]string{ + "/code/main.tf": ` +resource "aws_s3_bucket" "my-bucket" { + bucket = "evil" +} +`, + "/rules/test.rego": fmt.Sprintf(` +# METADATA +# custom: +# input: +# selector: +# - type: cloud +# subtypes: +# - service: s3 +# provider: aws +package %s + +deny[cause] { +bucket := input.aws.s3.buckets[_] +bucket.name.value == "evil" +cause := bucket.name +} + + `, test.policyNamespace), + }) + + scanner := New( + rego.WithPolicyDirs("rules"), + rego.WithPolicyNamespaces(test.includedNamespaces...), + ) + + results, err := scanner.ScanFS(t.Context(), fs, "code") + require.NoError(t, err) + + var found bool + for _, result := range results.GetFailed() { + if result.RegoNamespace() == test.policyNamespace && result.RegoRule() == "deny" { + found = true + break + } + } + assert.Equal(t, test.wantFailure, found) + }) + } + +} + +func Test_IAMPolicyRego(t *testing.T) { + fs := testutil.CreateFS(map[string]string{ + "/code/main.tf": ` +resource "aws_sqs_queue_policy" "bad_example" { + queue_url = aws_sqs_queue.q.id + + policy = < tfplan.Plan.VariablesEntry + 0, // 1: tfplan.Plan.VariablesEntry.value:type_name -> tfplan.DynamicValue + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_init() } +func file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_init() { + if File_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DynamicValue); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Plan); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_goTypes, + DependencyIndexes: file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_depIdxs, + MessageInfos: file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_msgTypes, + }.Build() + File_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto = out.File + file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_rawDesc = nil + file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_goTypes = nil + file_pkg_iac_scanners_terraformplan_snapshot_planproto_planfile_proto_depIdxs = nil +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/planproto/planfile.proto b/pkg/iac/scanners/terraformplan/snapshot/planproto/planfile.proto new file mode 100644 index 000000000000..688be55be7aa --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/planproto/planfile.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; +package tfplan; + +option go_package = "github.com/aquasecurity/trivy/iac/scanners/terraformplan/snapshot/planproto"; + +message DynamicValue { + bytes msgpack = 1; +} + +message Plan { + map variables = 2; +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/scanner.go b/pkg/iac/scanners/terraformplan/snapshot/scanner.go new file mode 100644 index 000000000000..584a769201c4 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/scanner.go @@ -0,0 +1,84 @@ +package snapshot + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + tfscanner "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform" + tfparser "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser" +) + +type Scanner struct { + inner *tfscanner.Scanner +} + +func (s *Scanner) Name() string { + return "Terraform Plan Snapshot" +} + +func New(opts ...options.ScannerOption) *Scanner { + scanner := &Scanner{ + inner: tfscanner.New( + append(opts, options.WithScanRawConfig(false))..., + ), + } + + return scanner +} + +func (s *Scanner) ScanFS(ctx context.Context, fsys fs.FS, dir string) (scan.Results, error) { + var results scan.Results + walkFn := func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + res, err := s.ScanFile(ctx, fsys, path) + if errors.Is(err, errNoTerraformPlan) { + return nil + } else if err != nil { + return err + } + results = append(results, res...) + return nil + } + if err := fs.WalkDir(fsys, dir, walkFn); err != nil { + return nil, err + } + return results, nil +} + +func (s *Scanner) ScanFile(ctx context.Context, fsys fs.FS, filepath string) (scan.Results, error) { + file, err := fsys.Open(filepath) + if err != nil { + return nil, err + } + defer file.Close() + return s.scan(ctx, file) +} + +func (s *Scanner) scan(ctx context.Context, reader io.Reader) (scan.Results, error) { + snap, err := parseSnapshot(reader) + if err != nil { + return nil, err + } + fsys, err := snap.toFS() + if err != nil { + return nil, fmt.Errorf("failed to convert snapshot to FS: %w", err) + } + + s.inner.AddParserOptions( + tfparser.OptionsWithTfVars(snap.inputVariables), + tfparser.OptionWithDownloads(false), + ) + return s.inner.ScanFS(ctx, fsys, ".") +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/scanner_test.go b/pkg/iac/scanners/terraformplan/snapshot/scanner_test.go new file mode 100644 index 000000000000..494b04f72375 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/scanner_test.go @@ -0,0 +1,74 @@ +package snapshot + +import ( + "os" + "path" + "sort" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + tfscanner "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func Test_ScanFS(t *testing.T) { + t.Parallel() + + tests := []struct { + dir string + expectedDir string + expectedIDs []string + }{ + { + dir: "just-resource", + expectedIDs: []string{"ID001"}, + }, + { + dir: "with-local-module", + expectedIDs: []string{"ID001"}, + }, + { + dir: "with-remote-module", + expectedIDs: []string{"ID001"}, + }, + { + dir: "with-var", + expectedIDs: []string{"ID001"}, + }, + } + + for _, tc := range tests { + t.Run(tc.dir, func(t *testing.T) { + fs := os.DirFS("testdata") + + scanner := New( + rego.WithPolicyDirs(path.Join(tc.dir, "checks")), + rego.WithPolicyFilesystem(fs), + rego.WithPolicyNamespaces("user"), + rego.WithEmbeddedLibraries(false), + rego.WithEmbeddedPolicies(false), + rego.WithMaxAllowedErrors(0), + tfscanner.ScannerWithSkipCachedModules(true), + ) + + results, err := scanner.ScanFS(t.Context(), fs, path.Join(tc.dir, "tfplan")) + require.NoError(t, err) + require.Len(t, results, 1) + + failed := results.GetFailed() + + assert.Len(t, failed, len(tc.expectedIDs)) + + ids := xslices.Map(failed, func(res scan.Result) string { + return res.Rule().ID + }) + sort.Strings(ids) + + assert.Equal(t, tc.expectedIDs, ids) + }) + } +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/snapshot.go b/pkg/iac/scanners/terraformplan/snapshot/snapshot.go new file mode 100644 index 000000000000..b71e35a06498 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/snapshot.go @@ -0,0 +1,230 @@ +package snapshot + +import ( + "archive/zip" + "encoding/json" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path" + "slices" + "strings" + + "github.com/zclconf/go-cty/cty" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform/parser" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/mapfs" + iox "github.com/aquasecurity/trivy/pkg/x/io" +) + +const ( + configSnapshotPrefix = "tfconfig/" + configSnapshotManifestFile = configSnapshotPrefix + "modules.json" + configSnapshotModulePrefix = configSnapshotPrefix + "m-" + + tfplanFilename = "tfplan" +) + +type ( + configSnapshotModuleManifest []parser.ModuleMetadata +) + +var errNoTerraformPlan = errors.New("no terraform plan file") + +func IsPlanSnapshot(r io.Reader) bool { + zr, err := readSnapshot(r) + if err != nil { + return false + } + return containsTfplanFile(zr) +} + +func readSnapshot(r io.Reader) (*zip.Reader, error) { + if r == nil { + return nil, errors.New("reader is nil") + } + + rsa, size, err := iox.NewReadSeekerAtWithSize(r) + if err != nil { + return nil, err + } + + zr, err := zip.NewReader(rsa, size) + if err != nil { + return nil, err + } + + return zr, nil +} + +func parseSnapshot(r io.Reader) (*snapshot, error) { + zr, err := readSnapshot(r) + if err != nil { + return nil, err + } + + if !containsTfplanFile(zr) { + return nil, errNoTerraformPlan + } + + snap := &snapshot{ + modules: make(map[string]*snapshotModule), + inputVariables: make(map[string]cty.Value), + } + + for _, file := range zr.File { + switch { + case file.Name == configSnapshotManifestFile: + var err error + snap.moduleManifest, err = readModuleManifest(file) + if err != nil { + return nil, err + } + case strings.HasPrefix(file.Name, configSnapshotModulePrefix): + if err := snap.addFile(file); err != nil { + return nil, err + } + case file.Name == tfplanFilename: + r, err := file.Open() + if err != nil { + return nil, fmt.Errorf("failed to open plan: %w", err) + } + + plan, err := readTfPlan(r) + if err != nil { + _ = r.Close() + return nil, fmt.Errorf("failed to read tfplan: %w", err) + } + _ = r.Close() + + snap.inputVariables, err = plan.inputVariables() + if err != nil { + return nil, err + } + } + } + + for _, record := range snap.moduleManifest { + modSnap := snap.getOrCreateModuleSnapshot(record.Key) + modSnap.dir = record.Dir + } + + return snap, nil +} + +func containsTfplanFile(zr *zip.Reader) bool { + return slices.ContainsFunc(zr.File, func(f *zip.File) bool { + return f.Name == tfplanFilename + }) +} + +func readModuleManifest(f *zip.File) (configSnapshotModuleManifest, error) { + r, err := f.Open() + if err != nil { + return nil, fmt.Errorf("failed to open module manifest: %s", r) + } + defer r.Close() + + var manifest configSnapshotModuleManifest + if err := json.NewDecoder(r).Decode(&manifest); err != nil { + return nil, fmt.Errorf("failed to read module manifest: %s", f.Name) + } + return manifest, nil +} + +type ( + snapshotModule struct { + // dir is the path, relative to the root directory given when the + // snapshot was created, where the module appears in the snapshot's + // virtual filesystem. + dir string + + // files is a map from each configuration file filename for the + // module to a raw byte representation of the source file contents. + files map[string][]byte + } + + snapshot struct { + moduleManifest configSnapshotModuleManifest + modules map[string]*snapshotModule + inputVariables map[string]cty.Value + } +) + +func (s *snapshot) addFile(file *zip.File) error { + relName := file.Name[len(configSnapshotModulePrefix):] + moduleKey, fileName := path.Split(relName) + if moduleKey == "" { + return nil + } + moduleKey = moduleKey[:len(moduleKey)-1] + + r, err := file.Open() + if err != nil { + return fmt.Errorf("failed to open snapshot of %s from module %q: %s", fileName, moduleKey, err) + } + defer r.Close() + + fileSrc, err := io.ReadAll(r) + if err != nil { + return fmt.Errorf("failed to read snapshot of %s from module %q: %s", fileName, moduleKey, err) + } + + modSnap := s.getOrCreateModuleSnapshot(moduleKey) + modSnap.files[fileName] = fileSrc + return nil +} + +func (s *snapshot) getOrCreateModuleSnapshot(key string) *snapshotModule { + modSnap, exists := s.modules[key] + if !exists { + modSnap = &snapshotModule{ + files: make(map[string][]byte), + } + s.modules[key] = modSnap + } + return modSnap +} + +func (s *snapshot) toFS() (fs.FS, error) { + fsys := mapfs.New() + + if err := s.writeManifest(fsys); err != nil { + log.WithPrefix(log.PrefixMisconfiguration).Error("Failed to write manifest file", log.Err(err)) + } + + for _, module := range s.modules { + if err := fsys.MkdirAll(module.dir, fs.ModePerm); err != nil && !errors.Is(err, os.ErrExist) { + return nil, err + } + for filename, file := range module.files { + filePath := filename + if module.dir != "" { + filePath = path.Join(module.dir, filename) + } + if err := fsys.WriteVirtualFile(filePath, file, fs.ModePerm); err != nil { + return nil, fmt.Errorf("failed to add file: %w", err) + } + } + } + return fsys, nil +} + +func (s *snapshot) writeManifest(fsys *mapfs.FS) error { + if err := fsys.MkdirAll(path.Dir(parser.ManifestSnapshotFile), fs.ModePerm); err != nil { + return fmt.Errorf("create manifest directory: %w", err) + } + + b, err := json.Marshal(parser.ModulesMetadata{Modules: s.moduleManifest}) + if err != nil { + return fmt.Errorf("marshal manifest snapshot: %w", err) + } + + if err := fsys.WriteVirtualFile(parser.ManifestSnapshotFile, b, fs.ModePerm); err != nil { + return fmt.Errorf("write manifest snapshot: %w", err) + } + return nil +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/snapshot_test.go b/pkg/iac/scanners/terraformplan/snapshot/snapshot_test.go new file mode 100644 index 000000000000..784b9a10c864 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/snapshot_test.go @@ -0,0 +1,146 @@ +package snapshot + +import ( + "archive/zip" + "bytes" + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" +) + +func TestReadSnapshot(t *testing.T) { + tests := []struct { + name string + dir string + expectedFiles []string + }{ + { + name: "just resource", + dir: "just-resource", + expectedFiles: []string{ + "main.tf", + "terraform.tf", + ".terraform/modules/modules.json", + }, + }, + { + name: "with local module", + dir: "with-local-module", + expectedFiles: []string{ + "main.tf", + "terraform.tf", + "modules/ec2/main.tf", + ".terraform/modules/modules.json", + }, + }, + { + name: "with nested modules", + dir: "nested-modules", + expectedFiles: []string{ + "main.tf", + "terraform.tf", + "modules/s3/main.tf", + "modules/s3/modules/logging/main.tf", + ".terraform/modules/modules.json", + }, + }, + { + name: "with remote module", + dir: "with-remote-module", + expectedFiles: []string{ + "main.tf", + "terraform.tf", + ".terraform/modules/modules.json", + ".terraform/modules/s3_bucket/main.tf", + ".terraform/modules/s3_bucket/outputs.tf", + ".terraform/modules/s3_bucket/variables.tf", + ".terraform/modules/s3_bucket/versions.tf", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(filepath.Join("testdata", tt.dir, "tfplan")) + require.NoError(t, err) + defer f.Close() + + snapshot, err := parseSnapshot(f) + require.NoError(t, err) + require.NotNil(t, snapshot) + + fsys, err := snapshot.toFS() + require.NoError(t, err) + + files, err := getAllfiles(fsys) + require.NoError(t, err) + + assert.ElementsMatch(t, tt.expectedFiles, files) + }) + } +} + +func getAllfiles(fsys fs.FS) ([]string, error) { + var files []string + walkFn := func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + files = append(files, filepath.ToSlash(path)) + return nil + } + if err := fs.WalkDir(fsys, ".", walkFn); err != nil { + return nil, err + } + return files, nil +} + +func TestIsPlanSnapshot(t *testing.T) { + t.Run("TF plan", func(t *testing.T) { + f, err := os.Open(filepath.Join("testdata", "just-resource", "tfplan")) + require.NoError(t, err) + defer f.Close() + + got := IsPlanSnapshot(f) + assert.True(t, got) + }) + + t.Run("just a zip file", func(t *testing.T) { + var b bytes.Buffer + zw := zip.NewWriter(&b) + defer zw.Close() + + w, err := zw.Create("test.txt") + require.NoError(t, err) + + w.Write([]byte("test")) + + got := IsPlanSnapshot(&b) + assert.False(t, got) + }) +} + +func TestPlanWithVariables(t *testing.T) { + f, err := os.Open(filepath.Join("testdata", "with-var", "tfplan")) + require.NoError(t, err) + defer f.Close() + + snapshot, err := parseSnapshot(f) + require.NoError(t, err) + require.NotNil(t, snapshot) + + expectedVars := map[string]cty.Value{ + "bucket_name": cty.StringVal("test-bucket"), + } + assert.Equal(t, expectedVars, snapshot.inputVariables) +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/checks/s3-bucket-name.rego b/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/checks/s3-bucket-name.rego new file mode 100644 index 000000000000..87f3dc2ca820 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/checks/s3-bucket-name.rego @@ -0,0 +1,21 @@ +# METADATA +# title: Test rego +# description: A bucket named "test-bucket" is not allowed +# schemas: +# - input: schema["cloud"] +# custom: +# id: ID001 +# severity: LOW +# input: +# selector: +# - type: cloud +# subtypes: +# - service: s3 +# provider: aws +package user.aws.ID001 + +deny[res] { + bucket := input.aws.s3.buckets[_] + bucket.name.value == "test-bucket" + res := result.new("Bucket not allowed", bucket.name) +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/main.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/main.tf new file mode 100644 index 000000000000..76b7c4788d76 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/main.tf @@ -0,0 +1,3 @@ +resource "aws_s3_bucket" "this" { + bucket = "test-bucket" +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/terraform.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/terraform.tf new file mode 100644 index 000000000000..ac2d39c811e5 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/terraform.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "aws" + version = "5.35.0" + } + } +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/tfplan b/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/tfplan new file mode 100644 index 000000000000..7556492a0dff Binary files /dev/null and b/pkg/iac/scanners/terraformplan/snapshot/testdata/just-resource/tfplan differ diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/main.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/main.tf new file mode 100644 index 000000000000..a474edf3ea6e --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/main.tf @@ -0,0 +1,3 @@ +module "s3_bucket" { + source = "./modules/s3" +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/modules/s3/main.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/modules/s3/main.tf new file mode 100644 index 000000000000..d847578a1a00 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/modules/s3/main.tf @@ -0,0 +1,8 @@ +resource "aws_s3_bucket" "this" { + +} + +module "s3_log" { + source = "./modules/logging" + bucket = aws_s3_bucket.this.id +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/modules/s3/modules/logging/main.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/modules/s3/modules/logging/main.tf new file mode 100644 index 000000000000..b9cb54f04a5b --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/modules/s3/modules/logging/main.tf @@ -0,0 +1,10 @@ +resource "aws_s3_bucket_versioning" "this" { + bucket = var.bucket + versioning_configuration { + status = "Enabled" + } +} + +variable "bucket" { + type = string +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/terraform.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/terraform.tf new file mode 100644 index 000000000000..ac2d39c811e5 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/terraform.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "aws" + version = "5.35.0" + } + } +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/tfplan b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/tfplan new file mode 100644 index 000000000000..148507541588 Binary files /dev/null and b/pkg/iac/scanners/terraformplan/snapshot/testdata/nested-modules/tfplan differ diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/checks/ec2-userdata.rego b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/checks/ec2-userdata.rego new file mode 100644 index 000000000000..83b5075fd8e6 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/checks/ec2-userdata.rego @@ -0,0 +1,21 @@ +# METADATA +# title: Test rego 2 +# description: Instances with userdata are not allowed +# schemas: +# - input: schema["cloud"] +# custom: +# id: ID001 +# severity: MEDIUM +# input: +# selector: +# - type: cloud +# subtypes: +# - service: ec2 +# provider: aws +package user.aws.ID001 + +deny[res] { + instance := input.aws.ec2.instances[_] + instance.userdata.value != "" + res := result.new("Instances with userdata are not allowed", instance.userdata) +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/main.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/main.tf new file mode 100644 index 000000000000..9755bb5807a5 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/main.tf @@ -0,0 +1,5 @@ +module "ec2_instance" { + source = "./modules/ec2" + instance_type = "t3.micro" + user_data = "test" +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/modules/ec2/main.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/modules/ec2/main.tf new file mode 100644 index 000000000000..488bb6200e29 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/modules/ec2/main.tf @@ -0,0 +1,34 @@ +data "aws_ami" "this" { + most_recent = true + owners = ["amazon"] + filter { + name = "architecture" + values = ["arm64"] + } + filter { + name = "name" + values = ["al2023-ami-2023*"] + } +} + +resource "aws_instance" "this" { + ami = data.aws_ami.this.id + instance_market_options { + spot_options { + max_price = 0.0031 + } + } + instance_type = var.instance_type + tags = { + Name = "test-spot" + } + user_data = var.user_data +} + +variable "instance_type" { + type = string +} + +variable "user_data" { + type = string +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/terraform.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/terraform.tf new file mode 100644 index 000000000000..ac2d39c811e5 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/terraform.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "aws" + version = "5.35.0" + } + } +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/tfplan b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/tfplan new file mode 100644 index 000000000000..ecd1456588fb Binary files /dev/null and b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-local-module/tfplan differ diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/checks/s3-bucket-name.rego b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/checks/s3-bucket-name.rego new file mode 100644 index 000000000000..87f3dc2ca820 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/checks/s3-bucket-name.rego @@ -0,0 +1,21 @@ +# METADATA +# title: Test rego +# description: A bucket named "test-bucket" is not allowed +# schemas: +# - input: schema["cloud"] +# custom: +# id: ID001 +# severity: LOW +# input: +# selector: +# - type: cloud +# subtypes: +# - service: s3 +# provider: aws +package user.aws.ID001 + +deny[res] { + bucket := input.aws.s3.buckets[_] + bucket.name.value == "test-bucket" + res := result.new("Bucket not allowed", bucket.name) +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/main.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/main.tf new file mode 100644 index 000000000000..de5a5fb6f7dc --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/main.tf @@ -0,0 +1,6 @@ +module "s3_bucket" { + source = "terraform-aws-modules/s3-bucket/aws" + version = "4.1.0" + + bucket = "test-bucket" +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/terraform.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/terraform.tf new file mode 100644 index 000000000000..ac2d39c811e5 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/terraform.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + aws = { + source = "aws" + version = "5.35.0" + } + } +} diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/tfplan b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/tfplan new file mode 100644 index 000000000000..872ad20646cb Binary files /dev/null and b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-remote-module/tfplan differ diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-var/checks/s3-bucket-name.rego b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-var/checks/s3-bucket-name.rego new file mode 100644 index 000000000000..87f3dc2ca820 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-var/checks/s3-bucket-name.rego @@ -0,0 +1,21 @@ +# METADATA +# title: Test rego +# description: A bucket named "test-bucket" is not allowed +# schemas: +# - input: schema["cloud"] +# custom: +# id: ID001 +# severity: LOW +# input: +# selector: +# - type: cloud +# subtypes: +# - service: s3 +# provider: aws +package user.aws.ID001 + +deny[res] { + bucket := input.aws.s3.buckets[_] + bucket.name.value == "test-bucket" + res := result.new("Bucket not allowed", bucket.name) +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-var/main.tf b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-var/main.tf new file mode 100644 index 000000000000..a6fc475bfb3e --- /dev/null +++ b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-var/main.tf @@ -0,0 +1,5 @@ +variable "bucket_name" {} + +resource "aws_s3_bucket" "this" { + bucket = var.bucket_name +} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/snapshot/testdata/with-var/tfplan b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-var/tfplan new file mode 100644 index 000000000000..78be033f53d4 Binary files /dev/null and b/pkg/iac/scanners/terraformplan/snapshot/testdata/with-var/tfplan differ diff --git a/pkg/iac/scanners/terraformplan/tfjson/parser/block.go b/pkg/iac/scanners/terraformplan/tfjson/parser/block.go new file mode 100644 index 000000000000..f3c60e8d7cc1 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/parser/block.go @@ -0,0 +1,214 @@ +package parser + +import ( + "fmt" + "io" + "sort" + "strconv" + "strings" +) + +type PlanReference struct { + Value string +} + +type PlanBlock struct { + Type string + Name string + BlockType string + Blocks []*PlanBlock + Attributes map[string]any +} + +func (pb *PlanBlock) GetOrCreateBlock(typ string) *PlanBlock { + for _, cb := range pb.Blocks { + if cb.BlockType == typ { + return cb + } + } + newChildBlock := &PlanBlock{ + BlockType: typ, + Attributes: make(map[string]any), + } + pb.Blocks = append(pb.Blocks, newChildBlock) + return newChildBlock +} + +func (pb *PlanBlock) toHCL(w io.Writer) { + r := &hclRenderer{ + w: w, + indent: "", + } + r.renderBlock(pb) +} + +type hclRenderer struct { + w io.Writer + indent string +} + +func (r *hclRenderer) write(s string) { + fmt.Fprint(r.w, s) +} + +func (r *hclRenderer) writeln(s string) { + fmt.Fprintln(r.w, s) +} + +func (r *hclRenderer) writef(format string, args ...any) { + fmt.Fprintf(r.w, format, args...) +} + +func (r *hclRenderer) incIndent() { + r.indent += " " +} + +func (r *hclRenderer) decIndent() { + if len(r.indent) >= 2 { + r.indent = r.indent[:len(r.indent)-2] + } +} + +func (r *hclRenderer) renderBlock(b *PlanBlock) { + r.write(r.indent) + if b.BlockType != "" && b.Type != "" && b.Name != "" { + // top-level block + r.writef("%s \"%s\" \"%s\" {\n", b.BlockType, b.Type, b.Name) + } else { + // child block + r.writef("%s {\n", b.BlockType) + } + + keys := make([]string, 0, len(b.Attributes)) + for k := range b.Attributes { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, key := range keys { + value := b.Attributes[key] + if value == nil { + continue + } + r.renderAttributeLine(key, value) + } + + sort.Slice(b.Blocks, func(i, j int) bool { + return b.Blocks[i].BlockType < b.Blocks[j].BlockType + }) + + r.incIndent() + for _, child := range b.Blocks { + r.renderBlock(child) + } + r.decIndent() + r.writeln(r.indent + "}") +} + +func (r *hclRenderer) renderAttributeLine(key string, val any) { + r.write(fmt.Sprintf("%s%s = ", r.indent+" ", key)) + r.renderAttributeValue(val) + r.writeln("") +} + +func (r *hclRenderer) renderAttributeValue(val any) { + switch t := val.(type) { + case map[string]any: + r.renderMap(t) + case []any: + r.renderSlice(t) + default: + renderPrimitive(r.w, val) + } +} + +func renderPrimitive(w io.Writer, val any) { + switch t := val.(type) { + case PlanReference: + fmt.Fprint(w, t.Value) + case string: + fmt.Fprint(w, parseStringPrimitive(t)) + default: + fmt.Fprintf(w, "%#v", t) + } +} + +func (r *hclRenderer) renderSlice(vals []any) { + if len(vals) == 0 { + r.write("[]") + return + } + + r.writeln("[") + + r.incIndent() + defer r.decIndent() + + for _, v := range vals { + r.write(r.indent + " ") + renderPrimitive(r.w, v) + r.writeln(",") + } + r.write(r.indent + "]") +} + +func (r *hclRenderer) renderMap(val map[string]any) { + if len(val) == 0 { + r.write("{}") + return + } + + keys := make([]string, 0, len(val)) + for k := range val { + keys = append(keys, k) + } + sort.Strings(keys) + + r.writeln("{") + r.incIndent() + defer r.decIndent() + + for _, k := range keys { + v := val[k] + if v == nil { + continue + } + + r.renderAttributeLine(strconv.Quote(k), v) + } + r.write(r.indent + "}") +} + +func parseStringPrimitive(input string) string { + input = escapeSpecialSequences(input) + if strings.Contains(input, "\n") { + return fmt.Sprintf("< 0 && rune(input[i-1]) == r { + continue + } + + sb.WriteRune(r) + } else { + sb.WriteRune(r) + } + } + return sb.String() +} diff --git a/pkg/iac/scanners/terraformplan/tfjson/parser/block_test.go b/pkg/iac/scanners/terraformplan/tfjson/parser/block_test.go new file mode 100644 index 000000000000..d4197ee7e749 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/parser/block_test.go @@ -0,0 +1,153 @@ +package parser + +import ( + "strings" + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_EscapeSpecialSequences(t *testing.T) { + tests := []struct { + name string + inp string + expected string + }{ + { + name: "without special sequences", + inp: `"hello world\\"`, + expected: `"hello world\\"`, + }, + { + name: "interpolation", + inp: `"Hello, ${var.name}!"`, + expected: `"Hello, $${var.name}!"`, + }, + { + name: "directive", + inp: `"Hello, %{ if true }foo%{ else }bar%{ endif }!"`, + expected: `"Hello, %%{ if true }foo%%{ else }bar%%{ endif }!"`, + }, + { + name: "interpolation already escaped", + inp: `"Hello, $${var.name}!"`, + expected: `"Hello, $${var.name}!"`, + }, + { + name: "start with special character", + inp: `${var.name}!"`, + expected: `$${var.name}!"`, + }, + { + name: "grok pattern", + inp: "# Grok Pattern Template\ngrok_pattern = \"%{TIMESTAMP_ISO8601:time} \\\\[%{NUMBER:pid}\\\\] %{GREEDYDATA:message}\"", + expected: "# Grok Pattern Template\ngrok_pattern = \"%%{TIMESTAMP_ISO8601:time} \\\\[%%{NUMBER:pid}\\\\] %%{GREEDYDATA:message}\"", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := escapeSpecialSequences(tt.inp) + assert.Equal(t, tt.expected, got) + + // We make sure that the characters are properly escaped + _, diag := hclsyntax.ParseTemplate([]byte(got), "", hcl.InitialPos) + if diag.HasErrors() { + require.NoError(t, diag) + } + }) + } +} + +func Test_ToHcl(t *testing.T) { + tests := []struct { + name string + block PlanBlock + expected string + }{ + { + name: "empty block", + block: PlanBlock{ + BlockType: "resource", + Type: "aws_s3_bucket", + Name: "empty", + }, + expected: `resource "aws_s3_bucket" "empty" { +} +`, + }, + { + name: "simple attributes", + block: PlanBlock{ + BlockType: "resource", + Type: "aws_instance", + Name: "example", + Attributes: map[string]any{ + "num": 3, + "boolean": true, + "str": "test", + }, + }, + expected: `resource "aws_instance" "example" { + boolean = true + num = 3 + str = "test" +} +`, + }, + { + name: "nested blocks", + block: PlanBlock{ + BlockType: "resource", + Type: "aws_security_group", + Name: "sg", + Blocks: []*PlanBlock{ + { + BlockType: "ingress", + Attributes: map[string]any{ + "from_port": 80, + "to_port": 80, + }, + }, + }, + }, + expected: `resource "aws_security_group" "sg" { + ingress { + from_port = 80 + to_port = 80 + } +} +`, + }, + { + name: "map key doesn't valid identifier", + block: PlanBlock{ + BlockType: "resource", + Type: "aws_s3_bucket", + Name: "name", + Attributes: map[string]any{ + "tags": map[string]any{ + "foo: /b\"ar": "baz", + }, + }, + }, + expected: `resource "aws_s3_bucket" "name" { + tags = { + "foo: /b\"ar" = "baz" + } +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var sb strings.Builder + tt.block.toHCL(&sb) + assert.Equal(t, tt.expected, sb.String()) + }) + } +} diff --git a/pkg/iac/scanners/terraformplan/tfjson/parser/parser.go b/pkg/iac/scanners/terraformplan/tfjson/parser/parser.go new file mode 100644 index 000000000000..418fed8a3c9a --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/parser/parser.go @@ -0,0 +1,320 @@ +package parser + +import ( + "crypto/md5" //#nosec + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/fs" + "os" + "strings" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/mapfs" +) + +const TerraformMainFile = "main.tf" + +type Parser struct { + logger *log.Logger +} + +func New() *Parser { + return &Parser{ + logger: log.WithPrefix("tfjson parser"), + } +} + +func (p *Parser) Parse(reader io.Reader) (*PlanFile, error) { + var planFile PlanFile + + if err := json.NewDecoder(reader).Decode(&planFile); err != nil { + return nil, err + } + + return &planFile, nil + +} + +func (p *PlanFile) ToFS() (fs.FS, error) { + resources, err := buildPlanBlocks(p.PlannedValues.RootModule, p.ResourceChanges, p.Configuration.RootModule) + if err != nil { + return nil, err + } + + var sb strings.Builder + for i, r := range resources { + if i > 0 { + sb.WriteByte('\n') + } + r.toHCL(&sb) + } + + content := sb.String() + fsys := mapfs.New() + if err := fsys.WriteVirtualFile(TerraformMainFile, []byte(content), os.ModePerm); err != nil { + return nil, err + } + return fsys, nil +} + +func buildPlanBlocks(module Module, resourceChanges []ResourceChange, configuration ConfigurationModule) ([]*PlanBlock, error) { + var resources []*PlanBlock + for _, r := range module.Resources { + resourceExprs := getConfiguration(r.Address, module.Address, configuration) + schema := schemaForBlock(r, resourceExprs) + changes := getValues(r.Address, resourceChanges) + resource := decodeBlock(schema, changes.After) + // fill top-level block fields + resource.BlockType = r.BlockType() + resource.Type = r.Type + resource.Name = moduleResourceName(r.Address, r.Type, r.Name) + resources = append(resources, resource) + } + + for _, m := range module.ChildModules { + // The module name is always the last part of the address + parts := strings.Split(m.Address, ".") + moduleName := parts[len(parts)-1] + moduleCall := configuration.ModuleCalls[moduleName] + pb, err := buildPlanBlocks(m, resourceChanges, moduleCall.Module) + if err != nil { + return nil, err + } + resources = append(resources, pb...) + } + + return resources, nil +} + +func decodeBlock(schema BlockSchema, rawBlock map[string]any) *PlanBlock { + block := &PlanBlock{ + Attributes: make(map[string]any), + } + + for key, child := range rawBlock { + decodeChildBlock(block, key, child, schema[key]) + } + + populateReferences(schema, block) + return block +} + +func decodeChildBlock(block *PlanBlock, key string, child any, schema *SchemaNode) { + switch { + case schema == nil: + appendBlockOrAttribute(block, key, child) + case schema.Type == AttributeNode: + appendBlockOrAttribute(block, key, decodeAttribute(schema, child)) + case schema.Type == BlockNode: + nestedBlocks := decodeNestedBlocks(schema, key, normalizeToSlice(child)) + block.Blocks = append(block.Blocks, nestedBlocks...) + } +} + +func normalizeToSlice(v any) []any { + if s, ok := v.([]any); ok { + return s + } + return []any{v} +} + +func appendBlockOrAttribute(block *PlanBlock, name string, value any) { + if s, ok := value.([]any); ok && len(s) > 0 { + if m, ok := s[0].(map[string]any); ok { + block.Blocks = append(block.Blocks, &PlanBlock{ + BlockType: name, + Attributes: m, + }) + return + } + } + block.Attributes[name] = value +} + +func populateReferences(schema BlockSchema, block *PlanBlock) { + for k, childNodeSchema := range schema { + switch childNodeSchema.Type { + case BlockNode: + cb := block.GetOrCreateBlock(k) + for _, childrenBlockChema := range childNodeSchema.Children { + populateReferences(childrenBlockChema, cb) + } + case AttributeNode: + if ref, ok := childNodeSchema.Value.(PlanReference); ok { + if _, exists := block.Attributes[k]; !exists { + block.Attributes[k] = ref + } + } + } + } +} + +func decodeNestedBlocks(schema *SchemaNode, key string, v []any) []*PlanBlock { + nestedBlocks := make([]*PlanBlock, 0, len(v)) + for i, el := range v { + m, ok := el.(map[string]any) + if !ok { + continue + } + nestedBlockSchema := make(BlockSchema) + if i < len(schema.Children) { + nestedBlockSchema = schema.Children[i] + } + nestedBlock := decodeBlock(nestedBlockSchema, m) + nestedBlock.BlockType = key + nestedBlocks = append(nestedBlocks, nestedBlock) + } + return nestedBlocks +} + +func decodeAttribute(schema *SchemaNode, rawAttr any) any { + if schema.Value == nil { + return rawAttr + } + + // For attributes of type object or map, the schema does not include field names and looks like: + // "list_attr": { "references": ["local.foo"] }, + // Therefore, we cannot determine which specific fields are unknown + // and we can ignore references from expressions. + return rawAttr +} + +func unpackConfigurationValue(val map[string]any, r Resource) any { + for k, v := range val { + switch k { + case "references": + s, ok := v.([]any) + if !ok || len(s) == 0 { + return PlanReference{} + } + + ref, ok := s[0].(string) + if !ok { + return PlanReference{} + } + return parseAttributeReference(r.Address, r.Type, ref) + case "constant_value": + return v + } + } + + return nil +} + +// parseAttributeReference parses an attribute reference string and returns +// a PlanReference. The reference may point to another resource and is adjusted +// according to Terraform address rules. +func parseAttributeReference(rAddress, rType, reference string) PlanReference { + parts := strings.Split(reference, ".") + nameIdx := 1 + if parts[0] == "data" { + nameIdx = 2 + } + parts[nameIdx] = moduleResourceName(rAddress, rType, parts[nameIdx]) + reference = strings.Join(parts, ".") + return PlanReference{Value: reference} +} + +// moduleResourceName returns the resource name with a module hash if the resource +// is inside a Terraform module. Otherwise, it returns the original name. +func moduleResourceName(rAddress, rType, name string) string { + if !strings.HasPrefix(rAddress, "module.") { + return name + } + + parts := strings.Split(rAddress, fmt.Sprintf(".%s.", rType)) + moduleAddress := strings.TrimSuffix(parts[0], ".data") + hash := md5.Sum([]byte(moduleAddress)) // #nosec + return fmt.Sprintf("%s_%s", name, hex.EncodeToString(hash[:])) +} + +func getConfiguration(address, moduleAddress string, configuration ConfigurationModule) ResourceExpressions { + resourceAddress, _ := strings.CutPrefix(address, moduleAddress) + for _, resource := range configuration.Resources { + if resource.Address == resourceAddress { + return resource.Expressions + } + } + + return make(ResourceExpressions) +} + +func getValues(address string, resourceChange []ResourceChange) *ResourceChange { + for _, r := range resourceChange { + if r.Address == address { + return &r + } + } + return nil +} + +type NodeType = int + +const ( + AttributeNode NodeType = iota + BlockNode +) + +type BlockSchema = map[string]*SchemaNode + +// SchemaNode represents a node in the Terraform resource schema. +type SchemaNode struct { + // Type specifies whether the node is a Block or an Attribute. + Type NodeType + + // Only used for Block nodes, contains nested BlockSchemas. + Children []BlockSchema + + // Only used for Attribute nodes. + // Can be nil, a raw Go value (number, bool, string), or a PlanReference + // if the attribute refers to another resource. + Value any +} + +func schemaForBlock(r Resource, expressions ResourceExpressions) BlockSchema { + schema := make(map[string]*SchemaNode) + for n, expr := range expressions { + nodeSchema := schemaForExpression(r, expr) + if nodeSchema != nil { + schema[n] = nodeSchema + } + } + return schema +} + +func schemaForExpression(r Resource, expr any) *SchemaNode { + switch v := expr.(type) { + case map[string]any: + // https://developer.hashicorp.com/terraform/internals/json-format#expression-representation + attrKeys := []string{"constant_value", "references"} + for _, k := range attrKeys { + if _, exists := v[k]; exists { + attrVal := unpackConfigurationValue(v, r) + return &SchemaNode{ + Type: AttributeNode, + Value: attrVal, + } + } + } + // https://developer.hashicorp.com/terraform/internals/json-format#block-expressions-representation + return &SchemaNode{ + Type: BlockNode, + Children: []BlockSchema{schemaForBlock(r, v)}, + } + case []any: + children := make([]BlockSchema, 0, len(v)) + for _, el := range v { + if m, ok := el.(map[string]any); ok { + children = append(children, schemaForBlock(r, m)) + } + } + return &SchemaNode{ + Type: BlockNode, + Children: children, + } + } + + return nil +} diff --git a/pkg/iac/scanners/terraformplan/tfjson/parser/parser_test.go b/pkg/iac/scanners/terraformplan/tfjson/parser/parser_test.go new file mode 100644 index 000000000000..6b915c3cb931 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/parser/parser_test.go @@ -0,0 +1,110 @@ +package parser_test + +import ( + "bytes" + "io/fs" + "os" + "testing" + + "github.com/hashicorp/hcl/v2/hclparse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraformplan/tfjson/parser" +) + +func TestParser_Parse(t *testing.T) { + tests := []struct { + name string + path string + expected string + }{ + { + name: "simple case", + path: "../testdata/plan.json", + expected: `resource "aws_s3_bucket" "planbucket" { + bucket = "tfsec-plan-testing" + force_destroy = false + logging { + target_bucket = "arn:aws:s3:::iac-tfsec-dev" + } + versioning { + enabled = true + mfa_delete = false + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "example" { + bucket = aws_s3_bucket.planbucket.id + rule { + bucket_key_enabled = true + apply_server_side_encryption_by_default { + kms_master_key_id = "" + sse_algorithm = "AES256" + } + } +} + +resource "aws_security_group" "sg" { + description = "Managed by Terraform" + name = "sg" + revoke_rules_on_delete = false + tags = { + "Name" = "blah" + } + tags_all = { + "Name" = "blah" + } + ingress { + cidr_blocks = [ + "0.0.0.0/0", + ] + description = "" + from_port = 80 + ipv6_cidr_blocks = [] + prefix_list_ids = [] + protocol = "tcp" + security_groups = [] + self = false + to_port = 80 + } +} +`, + }, + { + name: "with module call", + path: "../testdata/module_call.json", + expected: `resource "aws_s3_bucket" "main_c3bbddd8ef830639c0419b6b59b1fd80" { + bucket = "test" + force_destroy = false + region = "eu-west-1" +} +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := os.ReadFile(tt.path) + require.NoError(t, err) + + planFile, err := parser.New().Parse(bytes.NewReader(data)) + require.NoError(t, err) + assert.NotNil(t, planFile) + + fsys, err := planFile.ToFS() + require.NoError(t, err) + assert.NotNil(t, fsys) + + b, err := fs.ReadFile(fsys, parser.TerraformMainFile) + require.NoError(t, err) + + assert.Equal(t, tt.expected, string(b)) + + _, diags := hclparse.NewParser().ParseHCL(b, "main.tf") + if diags.HasErrors() { + assert.NoError(t, diags) + } + }) + } +} diff --git a/pkg/iac/scanners/terraformplan/tfjson/parser/plan_file.go b/pkg/iac/scanners/terraformplan/tfjson/parser/plan_file.go new file mode 100644 index 000000000000..7f1f4932f76c --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/parser/plan_file.go @@ -0,0 +1,62 @@ +package parser + +type Resource struct { + Address string `json:"address"` + ModuleAddress string `json:"module_address"` + Mode string `json:"mode"` + Type string `json:"type"` + Name string `json:"name"` +} + +func (r Resource) BlockType() string { + if r.Mode == "managed" { + return "resource" + } + return r.Mode +} + +type ResourceChange struct { + Resource + Change `json:"change"` +} + +type ResourceExpressions map[string]any + +type ConfigurationResource struct { + Resource + Expressions ResourceExpressions `json:"expressions"` +} + +type Change struct { + After map[string]any `json:"after"` +} + +type Module struct { + Resources []Resource `json:"resources"` + ChildModules []Module `json:"child_modules"` + // Omitted if the instance is in the root module. + Address string `json:"address"` +} + +type ConfigurationModule struct { + Resources []ConfigurationResource `json:"resources"` + ModuleCalls map[string]CallModule `json:"module_calls"` +} + +type CallModule struct { + Module ConfigurationModule `json:"module"` +} + +type PlannedValues struct { + RootModule Module `json:"root_module"` +} + +type Configuration struct { + RootModule ConfigurationModule `json:"root_module"` +} + +type PlanFile struct { + PlannedValues PlannedValues `json:"planned_values"` + ResourceChanges []ResourceChange `json:"resource_changes"` + Configuration Configuration `json:"configuration"` +} diff --git a/pkg/iac/scanners/terraformplan/tfjson/scanner.go b/pkg/iac/scanners/terraformplan/tfjson/scanner.go new file mode 100644 index 000000000000..1504fb3bffc5 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/scanner.go @@ -0,0 +1,92 @@ +package tfjson + +import ( + "context" + "fmt" + "io" + "io/fs" + + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraformplan/tfjson/parser" + "github.com/aquasecurity/trivy/pkg/log" +) + +type Scanner struct { + inner *terraform.Scanner + parser *parser.Parser + logger *log.Logger + options []options.ScannerOption +} + +func (s *Scanner) Name() string { + return "Terraform Plan JSON" +} + +func (s *Scanner) ScanFS(ctx context.Context, fsys fs.FS, dir string) (scan.Results, error) { + + var results scan.Results + + walkFn := func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return nil + } + + res, err := s.ScanFile(ctx, path, fsys) + if err != nil { + return fmt.Errorf("failed to scan %s: %w", path, err) + } + + results = append(results, res...) + return nil + } + + if err := fs.WalkDir(fsys, dir, walkFn); err != nil { + return nil, err + } + + return results, nil +} + +func New(opts ...options.ScannerOption) *Scanner { + scanner := &Scanner{ + inner: terraform.New( + append(opts, options.WithScanRawConfig(false))..., + ), + parser: parser.New(), + logger: log.WithPrefix("tfjson scanner"), + options: opts, + } + + return scanner +} + +func (s *Scanner) ScanFile(ctx context.Context, filepath string, fsys fs.FS) (scan.Results, error) { + + s.logger.Debug("Scanning file", log.FilePath(filepath)) + file, err := fsys.Open(filepath) + if err != nil { + return nil, err + } + defer file.Close() + return s.Scan(ctx, file) +} + +func (s *Scanner) Scan(ctx context.Context, reader io.Reader) (scan.Results, error) { + planFile, err := s.parser.Parse(reader) + if err != nil { + return nil, err + } + + planFS, err := planFile.ToFS() + if err != nil { + return nil, fmt.Errorf("failed to convert plan to FS: %w", err) + } + + return s.inner.ScanFS(ctx, planFS, ".") +} diff --git a/pkg/iac/scanners/terraformplan/tfjson/scanner_test.go b/pkg/iac/scanners/terraformplan/tfjson/scanner_test.go new file mode 100644 index 000000000000..9aa6ce2fe793 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/scanner_test.go @@ -0,0 +1,129 @@ +package tfjson_test + +import ( + "os" + "strings" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraformplan/tfjson" +) + +const defaultCheck = `package defsec.abcdefg + +__rego_metadata__ := { + "id": "TEST-0123", + "title": "Buckets should not be evil", + "short_code": "no-evil-buckets", + "severity": "CRITICAL", + "type": "DefSec Security Check", + "description": "You should not allow buckets to be evil", + "recommended_actions": "Use a good bucket instead", + "url": "https://google.com/search?q=is+my+bucket+evil", +} + +__rego_input__ := { + "combine": false, + "selector": [{"type": "cloud", "subtypes": [{"service": "s3", "provider": "aws"}]}], +} + +deny[cause] { + bucket := input.aws.s3.buckets[_] + bucket.name.value == "tfsec-plan-testing" + cause := bucket.name +}` + +func TestScanner_ScanFS(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + input string + options []options.ScannerOption + expected []string + }{ + { + name: "use builtin checks", + input: "testdata/plan.json", + options: []options.ScannerOption{ + rego.WithEmbeddedPolicies(true), + rego.WithEmbeddedLibraries(true), + }, + expected: []string{ + "AWS-0093", + "AWS-0086", + "AWS-0132", + "AWS-0094", + "AWS-0087", + "AWS-0091", + "AWS-0099", + "AWS-0124", + }, + }, + { + name: "with user namespace", + input: "testdata/plan.json", + options: []options.ScannerOption{ + rego.WithPolicyReader(strings.NewReader(defaultCheck)), + rego.WithPolicyNamespaces("user"), + }, + expected: []string{"TEST-0123"}, + }, + { + name: "with templated plan json", + input: "testdata/plan_with_template.json", + options: []options.ScannerOption{ + rego.WithPolicyReader(strings.NewReader(`# METADATA +# schemas: +# - input: schema["cloud"] +# custom: +# id: TEST-0123 +package user.foobar.ABC001 + +deny[cause] { + bucket := input.aws.s3.buckets[_] + bucket.name.value == "${template-name-is-$evil}" + cause := bucket.name +}`)), + rego.WithPolicyNamespaces("user"), + }, + expected: []string{"TEST-0123"}, + }, + { + name: "plan with arbitrary name", + input: "testdata/arbitrary_name.json", + options: []options.ScannerOption{ + rego.WithPolicyReader(strings.NewReader(defaultCheck)), + rego.WithPolicyNamespaces("user"), + }, + expected: []string{"TEST-0123"}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + b, err := os.ReadFile(tc.input) + require.NoError(t, err) + + fsys := fstest.MapFS{ + "main.tfplan.json": {Data: b}, + } + + scanner := tfjson.New(tc.options...) + results, err := scanner.ScanFS(t.Context(), fsys, ".") + require.NoError(t, err) + + var got []string + for _, r := range results.GetFailed() { + got = append(got, r.Rule().ID) + } + + assert.ElementsMatch(t, tc.expected, got) + }) + } +} diff --git a/pkg/iac/scanners/terraformplan/tfjson/testdata/arbitrary_name.json b/pkg/iac/scanners/terraformplan/tfjson/testdata/arbitrary_name.json new file mode 100644 index 000000000000..e0109b0325a8 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/testdata/arbitrary_name.json @@ -0,0 +1 @@ +{"format_version":"1.2","terraform_version":"1.8.1","planned_values":{"root_module":{"resources":[{"address":"aws_s3_bucket.this","mode":"managed","type":"aws_s3_bucket","name":"this","provider_name":"registry.opentofu.org/hashicorp/aws","schema_version":0,"values":{"bucket":"tfsec-plan-testing","force_destroy":false,"tags":null,"timeouts":null},"sensitive_values":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[],"website":[]}}]}},"resource_drift":[{"address":"aws_security_group.this","mode":"managed","type":"aws_security_group","name":"this","provider_name":"registry.opentofu.org/hashicorp/aws","change":{"actions":["delete"],"before":{"arn":"arn:aws:ec2:us-west-1:145167242158:security-group/sg-0f3ac859864629452","description":"Managed by Terraform","egress":[{"cidr_blocks":[],"description":"","from_port":0,"ipv6_cidr_blocks":[],"prefix_list_ids":[],"protocol":"-1","security_groups":[],"self":false,"to_port":0}],"id":"sg-0f3ac859864629452","ingress":[],"name":"test_sg","name_prefix":"","owner_id":"145167242158","revoke_rules_on_delete":false,"tags":null,"tags_all":{},"timeouts":null,"vpc_id":"vpc-06165c55f5a70fdfa"},"after":null,"after_unknown":{},"before_sensitive":{"egress":[{"cidr_blocks":[],"ipv6_cidr_blocks":[],"prefix_list_ids":[],"security_groups":[]}],"ingress":[],"tags_all":{}},"after_sensitive":false}}],"resource_changes":[{"address":"aws_s3_bucket.this","mode":"managed","type":"aws_s3_bucket","name":"this","provider_name":"registry.opentofu.org/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"bucket":"tfsec-plan-testing","force_destroy":false,"tags":null,"timeouts":null},"after_unknown":{"acceleration_status":true,"acl":true,"arn":true,"bucket_domain_name":true,"bucket_prefix":true,"bucket_regional_domain_name":true,"cors_rule":true,"grant":true,"hosted_zone_id":true,"id":true,"lifecycle_rule":true,"logging":true,"object_lock_configuration":true,"object_lock_enabled":true,"policy":true,"region":true,"replication_configuration":true,"request_payer":true,"server_side_encryption_configuration":true,"tags_all":true,"versioning":true,"website":true,"website_domain":true,"website_endpoint":true},"before_sensitive":false,"after_sensitive":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[],"website":[]}}}],"configuration":{"provider_config":{"aws":{"name":"aws","full_name":"registry.opentofu.org/hashicorp/aws"}},"root_module":{"resources":[{"address":"aws_s3_bucket.this","mode":"managed","type":"aws_s3_bucket","name":"this","provider_config_key":"aws","expressions":{"bucket":{"constant_value":"tfsec-plan-testing"}},"schema_version":0}]}},"timestamp":"2024-08-28T04:27:38Z","errored":false} diff --git a/pkg/iac/scanners/terraformplan/tfjson/testdata/module_call.json b/pkg/iac/scanners/terraformplan/tfjson/testdata/module_call.json new file mode 100644 index 000000000000..76cd90f475d9 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/testdata/module_call.json @@ -0,0 +1 @@ +{"format_version":"1.2","terraform_version":"1.14.2","planned_values":{"root_module":{"child_modules":[{"resources":[{"address":"module.s3_bucket.aws_s3_bucket.main","mode":"managed","type":"aws_s3_bucket","name":"main","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"bucket":"test","force_destroy":false,"region":"eu-west-1","tags":null,"timeouts":null},"sensitive_values":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[],"website":[]},"identity_schema_version":0,"identity":{"account_id":null,"bucket":null,"region":null}}],"address":"module.s3_bucket"}]}},"resource_changes":[{"address":"module.s3_bucket.aws_s3_bucket.main","module_address":"module.s3_bucket","mode":"managed","type":"aws_s3_bucket","name":"main","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"bucket":"test","force_destroy":false,"region":"eu-west-1","tags":null,"timeouts":null},"after_unknown":{"acceleration_status":true,"acl":true,"arn":true,"bucket_domain_name":true,"bucket_prefix":true,"bucket_region":true,"bucket_regional_domain_name":true,"cors_rule":true,"grant":true,"hosted_zone_id":true,"id":true,"lifecycle_rule":true,"logging":true,"object_lock_configuration":true,"object_lock_enabled":true,"policy":true,"replication_configuration":true,"request_payer":true,"server_side_encryption_configuration":true,"tags_all":true,"versioning":true,"website":true,"website_domain":true,"website_endpoint":true},"before_sensitive":false,"after_sensitive":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[],"website":[]},"after_identity":{"account_id":null,"bucket":null,"region":null}}}],"configuration":{"provider_config":{"aws":{"name":"aws","full_name":"registry.terraform.io/hashicorp/aws","expressions":{"region":{"constant_value":"eu-west-1"}}}},"root_module":{"module_calls":{"s3_bucket":{"source":"./modules/bucket","expressions":{"bucket":{"constant_value":"test"}},"module":{"resources":[{"address":"aws_s3_bucket.main","mode":"managed","type":"aws_s3_bucket","name":"main","provider_config_key":"aws","expressions":{"bucket":{"references":["var.bucket"]}},"schema_version":0}],"variables":{"bucket":{}}}}}}},"timestamp":"2025-12-26T05:42:43Z","applyable":true,"complete":true,"errored":false} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/tfjson/testdata/plan.json b/pkg/iac/scanners/terraformplan/tfjson/testdata/plan.json new file mode 100644 index 000000000000..c3eaf7bda8e7 --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/testdata/plan.json @@ -0,0 +1 @@ +{"format_version":"0.2","terraform_version":"1.0.3","variables":{"bucket_name":{"value":"tfsec-plan-testing"}},"planned_values":{"root_module":{"resources":[{"address":"aws_s3_bucket.planbucket","mode":"managed","type":"aws_s3_bucket","name":"planbucket","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"bucket":"tfsec-plan-testing","bucket_prefix":null,"force_destroy":false,"logging":[{"target_bucket":"arn:aws:s3:::iac-tfsec-dev","target_prefix":null}],"tags":null,"versioning":[{"enabled":true,"mfa_delete":false}]},"sensitive_values":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[{}],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[{}],"website":[]}},{"address":"aws_s3_bucket_server_side_encryption_configuration.example","mode":"managed","type":"aws_s3_bucket_server_side_encryption_configuration","name":"example","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"expected_bucket_owner":null,"rule":[{"apply_server_side_encryption_by_default":[{"kms_master_key_id":"","sse_algorithm":"AES256"}],"bucket_key_enabled":true}]},"sensitive_values":{"rule":[{"apply_server_side_encryption_by_default":[{}]}]}},{"address":"aws_security_group.sg","mode":"managed","type":"aws_security_group","name":"sg","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":1,"values":{"description":"Managed by Terraform","ingress":[{"cidr_blocks":["0.0.0.0/0"],"description":"","from_port":80,"ipv6_cidr_blocks":[],"prefix_list_ids":[],"protocol":"tcp","security_groups":[],"self":false,"to_port":80}],"name":"sg","revoke_rules_on_delete":false,"tags":{"Name":"blah"},"tags_all":{"Name":"blah"},"timeouts":null},"sensitive_values":{"egress":[],"ingress":[{"cidr_blocks":[false],"ipv6_cidr_blocks":[],"prefix_list_ids":[],"security_groups":[]}],"tags":{},"tags_all":{}}}]}},"resource_changes":[{"address":"aws_s3_bucket.planbucket","mode":"managed","type":"aws_s3_bucket","name":"planbucket","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"bucket":"tfsec-plan-testing","bucket_prefix":null,"force_destroy":false,"logging":[{"target_bucket":"arn:aws:s3:::iac-tfsec-dev","target_prefix":null}],"tags":null,"versioning":[{"enabled":true,"mfa_delete":false}]},"after_unknown":{"acceleration_status":true,"acl":true,"arn":true,"bucket_domain_name":true,"bucket_regional_domain_name":true,"cors_rule":true,"grant":true,"hosted_zone_id":true,"id":true,"lifecycle_rule":true,"logging":[{}],"object_lock_configuration":true,"object_lock_enabled":true,"policy":true,"region":true,"replication_configuration":true,"request_payer":true,"server_side_encryption_configuration":true,"tags_all":true,"versioning":[{}],"website":true,"website_domain":true,"website_endpoint":true},"before_sensitive":false,"after_sensitive":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[{}],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[{}],"website":[]}}},{"address":"aws_s3_bucket_server_side_encryption_configuration.example","mode":"managed","type":"aws_s3_bucket_server_side_encryption_configuration","name":"example","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"expected_bucket_owner":null,"rule":[{"apply_server_side_encryption_by_default":[{"kms_master_key_id":"","sse_algorithm":"AES256"}],"bucket_key_enabled":true}]},"after_unknown":{"bucket":true,"id":true,"rule":[{"apply_server_side_encryption_by_default":[{}]}]},"before_sensitive":false,"after_sensitive":{"rule":[{"apply_server_side_encryption_by_default":[{}]}]}}},{"address":"aws_security_group.sg","mode":"managed","type":"aws_security_group","name":"sg","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"description":"Managed by Terraform","ingress":[{"cidr_blocks":["0.0.0.0/0"],"description":"","from_port":80,"ipv6_cidr_blocks":[],"prefix_list_ids":[],"protocol":"tcp","security_groups":[],"self":false,"to_port":80}],"name":"sg","revoke_rules_on_delete":false,"tags":{"Name":"blah"},"tags_all":{"Name":"blah"},"timeouts":null},"after_unknown":{"arn":true,"egress":true,"id":true,"ingress":[{"cidr_blocks":[false],"ipv6_cidr_blocks":[],"prefix_list_ids":[],"security_groups":[]}],"name_prefix":true,"owner_id":true,"tags":{},"tags_all":{},"vpc_id":true},"before_sensitive":false,"after_sensitive":{"egress":[],"ingress":[{"cidr_blocks":[false],"ipv6_cidr_blocks":[],"prefix_list_ids":[],"security_groups":[]}],"tags":{},"tags_all":{}}}}],"prior_state":{"format_version":"0.2","terraform_version":"1.0.3","values":{"root_module":{"resources":[{"address":"data.aws_s3_bucket.logging_bucket","mode":"data","type":"aws_s3_bucket","name":"logging_bucket","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"arn":"arn:aws:s3:::iac-tfsec-dev","bucket":"iac-tfsec-dev","bucket_domain_name":"iac-tfsec-dev.s3.amazonaws.com","bucket_regional_domain_name":"iac-tfsec-dev.s3.amazonaws.com","hosted_zone_id":"Z3AQBSTGFYJSTF","id":"iac-tfsec-dev","region":"us-east-1","website_domain":null,"website_endpoint":null},"sensitive_values":{}}]}}},"configuration":{"provider_config":{"aws":{"name":"aws"}},"root_module":{"resources":[{"address":"aws_s3_bucket.planbucket","mode":"managed","type":"aws_s3_bucket","name":"planbucket","provider_config_key":"aws","expressions":{"bucket":{"references":["var.bucket_name"]},"logging":[{"target_bucket":{"references":["data.aws_s3_bucket.logging_bucket.arn","data.aws_s3_bucket.logging_bucket"]}}],"versioning":[{"enabled":{"constant_value":true}}]},"schema_version":0},{"address":"aws_s3_bucket_server_side_encryption_configuration.example","mode":"managed","type":"aws_s3_bucket_server_side_encryption_configuration","name":"example","provider_config_key":"aws","expressions":{"bucket":{"references":["aws_s3_bucket.planbucket.id","aws_s3_bucket.planbucket"]},"rule":[{"apply_server_side_encryption_by_default":[{"sse_algorithm":{"constant_value":"AES256"}}],"bucket_key_enabled":{"constant_value":true}}]},"schema_version":0},{"address":"aws_security_group.sg","mode":"managed","type":"aws_security_group","name":"sg","provider_config_key":"aws","expressions":{"name":{"constant_value":"sg"},"tags":{"constant_value":{"Name":"blah"}}},"schema_version":1},{"address":"data.aws_s3_bucket.logging_bucket","mode":"data","type":"aws_s3_bucket","name":"logging_bucket","provider_config_key":"aws","expressions":{"bucket":{"constant_value":"iac-tfsec-dev"}},"schema_version":0}],"variables":{"bucket_name":{"default":"tfsec-plan-testing"}}}}} \ No newline at end of file diff --git a/pkg/iac/scanners/terraformplan/tfjson/testdata/plan_with_template.json b/pkg/iac/scanners/terraformplan/tfjson/testdata/plan_with_template.json new file mode 100644 index 000000000000..2ae6e5c8d7ed --- /dev/null +++ b/pkg/iac/scanners/terraformplan/tfjson/testdata/plan_with_template.json @@ -0,0 +1,480 @@ +{ + "format_version": "0.2", + "terraform_version": "1.0.3", + "variables": { + "bucket_name": { + "value": "${template-name-is-$evil}" + } + }, + "planned_values": { + "root_module": { + "resources": [ + { + "address": "aws_s3_bucket.planbucket", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "planbucket", + "provider_name": "registry.terraform.io/hashicorp/aws", + "schema_version": 0, + "values": { + "bucket": "${template-name-is-$evil}", + "bucket_prefix": null, + "force_destroy": false, + "logging": [ + { + "target_bucket": "arn:aws:s3:::iac-tfsec-dev", + "target_prefix": null + } + ], + "tags": null, + "versioning": [ + { + "enabled": true, + "mfa_delete": false + } + ] + }, + "sensitive_values": { + "cors_rule": [], + "grant": [], + "lifecycle_rule": [], + "logging": [ + {} + ], + "object_lock_configuration": [], + "replication_configuration": [], + "server_side_encryption_configuration": [], + "tags_all": {}, + "versioning": [ + {} + ], + "website": [] + } + }, + { + "address": "aws_s3_bucket_server_side_encryption_configuration.example", + "mode": "managed", + "type": "aws_s3_bucket_server_side_encryption_configuration", + "name": "example", + "provider_name": "registry.terraform.io/hashicorp/aws", + "schema_version": 0, + "values": { + "expected_bucket_owner": null, + "rule": [ + { + "apply_server_side_encryption_by_default": [ + { + "kms_master_key_id": "", + "sse_algorithm": "AES256" + } + ], + "bucket_key_enabled": true + } + ] + }, + "sensitive_values": { + "rule": [ + { + "apply_server_side_encryption_by_default": [ + {} + ] + } + ] + } + }, + { + "address": "aws_security_group.sg", + "mode": "managed", + "type": "aws_security_group", + "name": "sg", + "provider_name": "registry.terraform.io/hashicorp/aws", + "schema_version": 1, + "values": { + "description": "Managed by Terraform", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "", + "from_port": 80, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 80 + } + ], + "name": "sg", + "revoke_rules_on_delete": false, + "tags": { + "Name": "blah" + }, + "tags_all": { + "Name": "blah" + }, + "timeouts": null + }, + "sensitive_values": { + "egress": [], + "ingress": [ + { + "cidr_blocks": [ + false + ], + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "security_groups": [] + } + ], + "tags": {}, + "tags_all": {} + } + } + ] + } + }, + "resource_changes": [ + { + "address": "aws_s3_bucket.planbucket", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "planbucket", + "provider_name": "registry.terraform.io/hashicorp/aws", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "bucket": "${template-name-is-$evil}", + "bucket_prefix": null, + "force_destroy": false, + "logging": [ + { + "target_bucket": "arn:aws:s3:::iac-tfsec-dev", + "target_prefix": null + } + ], + "tags": null, + "versioning": [ + { + "enabled": true, + "mfa_delete": false + } + ] + }, + "after_unknown": { + "acceleration_status": true, + "acl": true, + "arn": true, + "bucket_domain_name": true, + "bucket_regional_domain_name": true, + "cors_rule": true, + "grant": true, + "hosted_zone_id": true, + "id": true, + "lifecycle_rule": true, + "logging": [ + {} + ], + "object_lock_configuration": true, + "object_lock_enabled": true, + "policy": true, + "region": true, + "replication_configuration": true, + "request_payer": true, + "server_side_encryption_configuration": true, + "tags_all": true, + "versioning": [ + {} + ], + "website": true, + "website_domain": true, + "website_endpoint": true + }, + "before_sensitive": false, + "after_sensitive": { + "cors_rule": [], + "grant": [], + "lifecycle_rule": [], + "logging": [ + {} + ], + "object_lock_configuration": [], + "replication_configuration": [], + "server_side_encryption_configuration": [], + "tags_all": {}, + "versioning": [ + {} + ], + "website": [] + } + } + }, + { + "address": "aws_s3_bucket_server_side_encryption_configuration.example", + "mode": "managed", + "type": "aws_s3_bucket_server_side_encryption_configuration", + "name": "example", + "provider_name": "registry.terraform.io/hashicorp/aws", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "expected_bucket_owner": null, + "rule": [ + { + "apply_server_side_encryption_by_default": [ + { + "kms_master_key_id": "", + "sse_algorithm": "AES256" + } + ], + "bucket_key_enabled": true + } + ] + }, + "after_unknown": { + "bucket": true, + "id": true, + "rule": [ + { + "apply_server_side_encryption_by_default": [ + {} + ] + } + ] + }, + "before_sensitive": false, + "after_sensitive": { + "rule": [ + { + "apply_server_side_encryption_by_default": [ + {} + ] + } + ] + } + } + }, + { + "address": "aws_security_group.sg", + "mode": "managed", + "type": "aws_security_group", + "name": "sg", + "provider_name": "registry.terraform.io/hashicorp/aws", + "change": { + "actions": [ + "create" + ], + "before": null, + "after": { + "description": "Managed by Terraform", + "ingress": [ + { + "cidr_blocks": [ + "0.0.0.0/0" + ], + "description": "", + "from_port": 80, + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "protocol": "tcp", + "security_groups": [], + "self": false, + "to_port": 80 + } + ], + "name": "sg", + "revoke_rules_on_delete": false, + "tags": { + "Name": "blah" + }, + "tags_all": { + "Name": "blah" + }, + "timeouts": null + }, + "after_unknown": { + "arn": true, + "egress": true, + "id": true, + "ingress": [ + { + "cidr_blocks": [ + false + ], + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "security_groups": [] + } + ], + "name_prefix": true, + "owner_id": true, + "tags": {}, + "tags_all": {}, + "vpc_id": true + }, + "before_sensitive": false, + "after_sensitive": { + "egress": [], + "ingress": [ + { + "cidr_blocks": [ + false + ], + "ipv6_cidr_blocks": [], + "prefix_list_ids": [], + "security_groups": [] + } + ], + "tags": {}, + "tags_all": {} + } + } + } + ], + "prior_state": { + "format_version": "0.2", + "terraform_version": "1.0.3", + "values": { + "root_module": { + "resources": [ + { + "address": "data.aws_s3_bucket.logging_bucket", + "mode": "data", + "type": "aws_s3_bucket", + "name": "logging_bucket", + "provider_name": "registry.terraform.io/hashicorp/aws", + "schema_version": 0, + "values": { + "arn": "arn:aws:s3:::iac-tfsec-dev", + "bucket": "iac-tfsec-dev", + "bucket_domain_name": "iac-tfsec-dev.s3.amazonaws.com", + "bucket_regional_domain_name": "iac-tfsec-dev.s3.amazonaws.com", + "hosted_zone_id": "Z3AQBSTGFYJSTF", + "id": "iac-tfsec-dev", + "region": "us-east-1", + "website_domain": null, + "website_endpoint": null + }, + "sensitive_values": {} + } + ] + } + } + }, + "configuration": { + "provider_config": { + "aws": { + "name": "aws" + } + }, + "root_module": { + "resources": [ + { + "address": "aws_s3_bucket.planbucket", + "mode": "managed", + "type": "aws_s3_bucket", + "name": "planbucket", + "provider_config_key": "aws", + "expressions": { + "bucket": { + "references": [ + "var.bucket_name" + ] + }, + "logging": [ + { + "target_bucket": { + "references": [ + "data.aws_s3_bucket.logging_bucket.arn", + "data.aws_s3_bucket.logging_bucket" + ] + } + } + ], + "versioning": [ + { + "enabled": { + "constant_value": true + } + } + ] + }, + "schema_version": 0 + }, + { + "address": "aws_s3_bucket_server_side_encryption_configuration.example", + "mode": "managed", + "type": "aws_s3_bucket_server_side_encryption_configuration", + "name": "example", + "provider_config_key": "aws", + "expressions": { + "bucket": { + "references": [ + "aws_s3_bucket.planbucket.id", + "aws_s3_bucket.planbucket" + ] + }, + "rule": [ + { + "apply_server_side_encryption_by_default": [ + { + "sse_algorithm": { + "constant_value": "AES256" + } + } + ], + "bucket_key_enabled": { + "constant_value": true + } + } + ] + }, + "schema_version": 0 + }, + { + "address": "aws_security_group.sg", + "mode": "managed", + "type": "aws_security_group", + "name": "sg", + "provider_config_key": "aws", + "expressions": { + "name": { + "constant_value": "sg" + }, + "tags": { + "constant_value": { + "Name": "blah" + } + } + }, + "schema_version": 1 + }, + { + "address": "data.aws_s3_bucket.logging_bucket", + "mode": "data", + "type": "aws_s3_bucket", + "name": "logging_bucket", + "provider_config_key": "aws", + "expressions": { + "bucket": { + "constant_value": "iac-tfsec-dev" + } + }, + "schema_version": 0 + } + ], + "variables": { + "bucket_name": { + "default": "${template-name-is-$evil}" + } + } + } + } +} \ No newline at end of file diff --git a/pkg/iac/severity/severity.go b/pkg/iac/severity/severity.go new file mode 100755 index 000000000000..69d9b63eea42 --- /dev/null +++ b/pkg/iac/severity/severity.go @@ -0,0 +1,44 @@ +package severity + +import ( + "slices" + "strings" +) + +type Severity string + +const ( + None Severity = "" + Critical Severity = "CRITICAL" + High Severity = "HIGH" + Medium Severity = "MEDIUM" + Low Severity = "LOW" +) + +var ValidSeverity = []Severity{ + Critical, High, Medium, Low, +} + +func (s *Severity) IsValid() bool { + return slices.Contains(ValidSeverity, *s) +} + +func (s *Severity) Valid() []Severity { + return ValidSeverity +} + +func StringToSeverity(sev string) Severity { + s := strings.ToUpper(sev) + switch s { + case "CRITICAL", "HIGH", "MEDIUM", "LOW": + return Severity(s) + case "ERROR": + return High + case "WARNING": + return Medium + case "INFO": + return Low + default: + return None + } +} diff --git a/pkg/iac/state/merge.go b/pkg/iac/state/merge.go new file mode 100644 index 000000000000..16e1275813a8 --- /dev/null +++ b/pkg/iac/state/merge.go @@ -0,0 +1,43 @@ +package state + +import ( + "reflect" +) + +// Merge merges the states of the services that have been scanned into a single state. +// if a service has data on both a and b, the service data from b will be preferred. +func (a *State) Merge(b *State) (*State, error) { + var output State + + aVal := reflect.ValueOf(a).Elem() + bVal := reflect.ValueOf(b).Elem() + outputVal := reflect.ValueOf(&output).Elem() + + stateType := reflect.ValueOf(a).Elem().Type() + for i := 0; i < stateType.NumField(); i++ { + field := stateType.Field(i) + if !field.IsExported() { + continue + } + if field.Type.Kind() != reflect.Struct { + continue + } + for j := 0; j < field.Type.NumField(); j++ { + serviceField := field.Type.Field(j) + if !serviceField.IsExported() { + continue + } + if serviceField.Type.Kind() != reflect.Struct { + continue + } + if !bVal.Field(i).Field(j).IsZero() { + outputVal.Field(i).Field(j).Set(bVal.Field(i).Field(j)) + } else { + outputVal.Field(i).Field(j).Set(aVal.Field(i).Field(j)) + } + } + } + + normalised := outputVal.Interface().(State) + return &normalised, nil +} diff --git a/pkg/iac/state/merge_test.go b/pkg/iac/state/merge_test.go new file mode 100644 index 000000000000..baf3959f3890 --- /dev/null +++ b/pkg/iac/state/merge_test.go @@ -0,0 +1,340 @@ +package state + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/iac/providers/aws" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/ec2" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/rds" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_Merging(t *testing.T) { + tests := []struct { + name string + a, b, expected State + }{ + { + name: "both empty", + }, + { + name: "a empty, b has a service", + b: State{ + AWS: aws.AWS{ + RDS: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.Int(1, iacTypes.Metadata{}), + ReplicationSourceARN: iacTypes.String("arn:whatever", iacTypes.Metadata{}), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.Metadata{}, + Enabled: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + Encryption: rds.Encryption{ + Metadata: iacTypes.Metadata{}, + EncryptStorage: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + PublicAccess: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + expected: State{ + AWS: aws.AWS{ + RDS: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.Int(1, iacTypes.Metadata{}), + ReplicationSourceARN: iacTypes.String("arn:whatever", iacTypes.Metadata{}), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.Metadata{}, + Enabled: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + Encryption: rds.Encryption{ + Metadata: iacTypes.Metadata{}, + EncryptStorage: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + PublicAccess: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + }, + { + name: "b empty, a has a service", + a: State{ + AWS: aws.AWS{ + RDS: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.Int(1, iacTypes.Metadata{}), + ReplicationSourceARN: iacTypes.String("arn:whatever", iacTypes.Metadata{}), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.Metadata{}, + Enabled: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + Encryption: rds.Encryption{ + Metadata: iacTypes.Metadata{}, + EncryptStorage: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + PublicAccess: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + expected: State{ + AWS: aws.AWS{ + RDS: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.Int(1, iacTypes.Metadata{}), + ReplicationSourceARN: iacTypes.String("arn:whatever", iacTypes.Metadata{}), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.Metadata{}, + Enabled: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + Encryption: rds.Encryption{ + Metadata: iacTypes.Metadata{}, + EncryptStorage: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + PublicAccess: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + }, + { + name: "both have differing versions of same service", + a: State{ + AWS: aws.AWS{ + RDS: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.Int(1, iacTypes.Metadata{}), + ReplicationSourceARN: iacTypes.String("arn:whatever", iacTypes.Metadata{}), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.Metadata{}, + Enabled: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + Encryption: rds.Encryption{ + Metadata: iacTypes.Metadata{}, + EncryptStorage: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + PublicAccess: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + b: State{ + AWS: aws.AWS{ + RDS: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.Int(1, iacTypes.Metadata{}), + ReplicationSourceARN: iacTypes.String("arn:whatever:B", iacTypes.Metadata{}), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.Metadata{}, + Enabled: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere:B", iacTypes.Metadata{}), + }, + Encryption: rds.Encryption{ + Metadata: iacTypes.Metadata{}, + EncryptStorage: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere:B", iacTypes.Metadata{}), + }, + PublicAccess: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + expected: State{ + AWS: aws.AWS{ + RDS: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.Int(1, iacTypes.Metadata{}), + ReplicationSourceARN: iacTypes.String("arn:whatever:B", iacTypes.Metadata{}), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.Metadata{}, + Enabled: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere:B", iacTypes.Metadata{}), + }, + Encryption: rds.Encryption{ + Metadata: iacTypes.Metadata{}, + EncryptStorage: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere:B", iacTypes.Metadata{}), + }, + PublicAccess: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + }, + { + name: "each has a different service", + a: State{ + AWS: aws.AWS{ + RDS: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.Int(1, iacTypes.Metadata{}), + ReplicationSourceARN: iacTypes.String("arn:whatever", iacTypes.Metadata{}), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.Metadata{}, + Enabled: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + Encryption: rds.Encryption{ + Metadata: iacTypes.Metadata{}, + EncryptStorage: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + PublicAccess: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + b: State{ + AWS: aws.AWS{ + EC2: ec2.EC2{ + Instances: []ec2.Instance{ + { + Metadata: iacTypes.Metadata{}, + MetadataOptions: ec2.MetadataOptions{ + Metadata: iacTypes.Metadata{}, + HttpTokens: iacTypes.String("something", iacTypes.Metadata{}), + HttpEndpoint: iacTypes.String("something", iacTypes.Metadata{}), + }, + UserData: iacTypes.String("something", iacTypes.Metadata{}), + SecurityGroups: []ec2.SecurityGroup{ + { + Metadata: iacTypes.Metadata{}, + IsDefault: iacTypes.Bool(true, iacTypes.Metadata{}), + Description: iacTypes.String("something", iacTypes.Metadata{}), + IngressRules: []ec2.SecurityGroupRule{ + { + Metadata: iacTypes.Metadata{}, + Description: iacTypes.String("something", iacTypes.Metadata{}), + CIDRs: []iacTypes.StringValue{ + iacTypes.String("something", iacTypes.Metadata{}), + }, + }, + }, + EgressRules: nil, + VPCID: iacTypes.String("something", iacTypes.Metadata{}), + }, + }, + RootBlockDevice: &ec2.BlockDevice{ + Metadata: iacTypes.Metadata{}, + Encrypted: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + EBSBlockDevices: []*ec2.BlockDevice{ + { + Metadata: iacTypes.Metadata{}, + Encrypted: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + }, + }, + expected: State{ + AWS: aws.AWS{ + EC2: ec2.EC2{ + Instances: []ec2.Instance{ + { + Metadata: iacTypes.Metadata{}, + MetadataOptions: ec2.MetadataOptions{ + Metadata: iacTypes.Metadata{}, + HttpTokens: iacTypes.String("something", iacTypes.Metadata{}), + HttpEndpoint: iacTypes.String("something", iacTypes.Metadata{}), + }, + UserData: iacTypes.String("something", iacTypes.Metadata{}), + SecurityGroups: []ec2.SecurityGroup{ + { + Metadata: iacTypes.Metadata{}, + IsDefault: iacTypes.Bool(true, iacTypes.Metadata{}), + Description: iacTypes.String("something", iacTypes.Metadata{}), + IngressRules: []ec2.SecurityGroupRule{ + { + Metadata: iacTypes.Metadata{}, + Description: iacTypes.String("something", iacTypes.Metadata{}), + CIDRs: []iacTypes.StringValue{ + iacTypes.String("something", iacTypes.Metadata{}), + }, + }, + }, + EgressRules: nil, + VPCID: iacTypes.String("something", iacTypes.Metadata{}), + }, + }, + RootBlockDevice: &ec2.BlockDevice{ + Metadata: iacTypes.Metadata{}, + Encrypted: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + EBSBlockDevices: []*ec2.BlockDevice{ + { + Metadata: iacTypes.Metadata{}, + Encrypted: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + RDS: rds.RDS{ + Instances: []rds.Instance{ + { + BackupRetentionPeriodDays: iacTypes.Int(1, iacTypes.Metadata{}), + ReplicationSourceARN: iacTypes.String("arn:whatever", iacTypes.Metadata{}), + PerformanceInsights: rds.PerformanceInsights{ + Metadata: iacTypes.Metadata{}, + Enabled: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + Encryption: rds.Encryption{ + Metadata: iacTypes.Metadata{}, + EncryptStorage: iacTypes.Bool(true, iacTypes.Metadata{}), + KMSKeyID: iacTypes.String("keyidhere", iacTypes.Metadata{}), + }, + PublicAccess: iacTypes.Bool(true, iacTypes.Metadata{}), + }, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + state := test.b + actual, err := test.a.Merge(&state) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, test.expected, *actual) + }) + } + +} diff --git a/pkg/iac/state/state.go b/pkg/iac/state/state.go new file mode 100755 index 000000000000..bf04db71aa54 --- /dev/null +++ b/pkg/iac/state/state.go @@ -0,0 +1,34 @@ +package state + +import ( + "reflect" + + "github.com/aquasecurity/trivy/pkg/iac/providers/aws" + "github.com/aquasecurity/trivy/pkg/iac/providers/azure" + "github.com/aquasecurity/trivy/pkg/iac/providers/cloudstack" + "github.com/aquasecurity/trivy/pkg/iac/providers/digitalocean" + "github.com/aquasecurity/trivy/pkg/iac/providers/github" + "github.com/aquasecurity/trivy/pkg/iac/providers/google" + "github.com/aquasecurity/trivy/pkg/iac/providers/kubernetes" + "github.com/aquasecurity/trivy/pkg/iac/providers/nifcloud" + "github.com/aquasecurity/trivy/pkg/iac/providers/openstack" + "github.com/aquasecurity/trivy/pkg/iac/providers/oracle" + "github.com/aquasecurity/trivy/pkg/iac/rego/convert" +) + +type State struct { + AWS aws.AWS + Azure azure.Azure + CloudStack cloudstack.CloudStack + DigitalOcean digitalocean.DigitalOcean + GitHub github.GitHub + Google google.Google + Kubernetes kubernetes.Kubernetes + OpenStack openstack.OpenStack + Oracle oracle.Oracle + Nifcloud nifcloud.Nifcloud +} + +func (a *State) ToRego() any { + return convert.StructToRego(reflect.ValueOf(a)) +} diff --git a/pkg/iac/state/state_test.go b/pkg/iac/state/state_test.go new file mode 100644 index 000000000000..46de9b506d4b --- /dev/null +++ b/pkg/iac/state/state_test.go @@ -0,0 +1,96 @@ +package state + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/providers/aws" + "github.com/aquasecurity/trivy/pkg/iac/providers/aws/s3" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_RegoConversion(t *testing.T) { + s := State{ + AWS: aws.AWS{ + S3: s3.S3{ + Buckets: []s3.Bucket{ + { + Metadata: iacTypes.NewMetadata( + iacTypes.NewRange("main.tf", 2, 4, "", nil), + "aws_s3_bucket.example", + ), + Name: iacTypes.String("my-bucket", iacTypes.NewMetadata( + iacTypes.NewRange("main.tf", 3, 3, "", nil), + "aws_s3_bucket.example.bucket", + )), + }, + }, + }, + }, + } + converted := s.ToRego() + assert.Equal(t, map[string]any{ + "aws": map[string]any{ + "s3": map[string]any{ + "buckets": []any{ + map[string]any{ + "__defsec_metadata": map[string]any{ + "resource": "aws_s3_bucket.example", + "sourceprefix": "", + "filepath": "main.tf", + "startline": 2, + "endline": 4, + "managed": true, + "unresolvable": false, + "explicit": false, + "fskey": "", + }, + "name": map[string]any{ + "resource": "aws_s3_bucket.example.bucket", + "sourceprefix": "", + "filepath": "main.tf", + "startline": 3, + "endline": 3, + "value": "my-bucket", + "managed": true, + "unresolvable": false, + "explicit": false, + "fskey": "", + }, + }, + }, + }, + }, + }, converted) +} + +func Test_JSONPersistenceOfData(t *testing.T) { + s := State{ + AWS: aws.AWS{ + S3: s3.S3{ + Buckets: []s3.Bucket{ + { + Metadata: iacTypes.NewMetadata( + iacTypes.NewRange("main.tf", 2, 4, "", nil), + "aws_s3_bucket.example", + ), + Name: iacTypes.String("my-bucket", iacTypes.NewMetadata( + iacTypes.NewRange("main.tf", 3, 3, "", nil), + "aws_s3_bucket.example.bucket", + )), + }, + }, + }, + }, + } + data, err := json.Marshal(s) + require.NoError(t, err) + + var restored State + require.NoError(t, json.Unmarshal(data, &restored)) + + assert.Equal(t, s, restored) +} diff --git a/pkg/iac/terraform/attribute.go b/pkg/iac/terraform/attribute.go new file mode 100644 index 000000000000..0256aaa64588 --- /dev/null +++ b/pkg/iac/terraform/attribute.go @@ -0,0 +1,949 @@ +package terraform + +import ( + "fmt" + "io/fs" + "reflect" + "slices" + "strconv" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/ext/typeexpr" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/convert" + "github.com/zclconf/go-cty/cty/gocty" + + "github.com/aquasecurity/trivy/pkg/iac/terraform/context" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/log" +) + +type Attribute struct { + hclAttribute *hcl.Attribute + module string + ctx *context.Context + metadata iacTypes.Metadata + reference Reference +} + +func (a *Attribute) DecodeVarType() (cty.Type, *typeexpr.Defaults, error) { + // Special-case the shortcuts for list(any) and map(any) which aren't hcl. + switch hcl.ExprAsKeyword(a.hclAttribute.Expr) { + case "list": + return cty.List(cty.DynamicPseudoType), nil, nil + case "map": + return cty.Map(cty.DynamicPseudoType), nil, nil + } + + t, def, diag := typeexpr.TypeConstraintWithDefaults(a.hclAttribute.Expr) + if diag.HasErrors() { + return cty.NilType, nil, diag + } + return t, def, nil +} + +func NewAttribute(attr *hcl.Attribute, ctx *context.Context, module string, parent iacTypes.Metadata, parentRef Reference, moduleSource string, moduleFS fs.FS) *Attribute { + rng := iacTypes.NewRange( + attr.Range.Filename, + attr.Range.Start.Line, + attr.Range.End.Line, + moduleSource, + moduleFS, + ) + reference := extendReference(parentRef, attr.Name) + metadata := iacTypes.NewMetadata(rng, reference.String()) + return &Attribute{ + hclAttribute: attr, + ctx: ctx, + module: module, + metadata: metadata.WithParent(parent), + reference: reference, + } +} + +func (a *Attribute) HCLAttribute() *hcl.Attribute { + return a.hclAttribute +} + +func (a *Attribute) GetMetadata() iacTypes.Metadata { + return a.metadata +} + +func (a *Attribute) GetRawValue() any { + return safeOp(a, func(v cty.Value) any { + switch typ := v.Type(); typ { + case cty.String: + return v.AsString() + case cty.Bool: + return v.True() + case cty.Number: + float, _ := v.AsBigFloat().Float64() + return float + default: + switch { + case typ.IsTupleType(), typ.IsListType(), typ.IsSetType(): + values := a.Value().AsValueSlice() + if len(values) == 0 { + return []string{} + } + switch values[0].Type() { + case cty.String: + var output []string + for _, value := range values { + output = append(output, value.AsString()) + } + return output + case cty.Number: + var output []float64 + for _, value := range values { + bf := value.AsBigFloat() + f, _ := bf.Float64() + output = append(output, f) + } + return output + case cty.Bool: + var output []bool + for _, value := range values { + output = append(output, value.True()) + } + return output + } + } + } + return nil + }) +} + +func (a *Attribute) AsBytesValueOrDefault(defaultValue []byte, parent *Block) iacTypes.BytesValue { + if a.IsNil() { + return iacTypes.BytesDefault(defaultValue, parent.GetMetadata()) + } + if !a.IsResolvable() || !a.IsString() { + return iacTypes.BytesUnresolvable(a.GetMetadata()) + } + return iacTypes.BytesExplicit( + []byte(a.Value().AsString()), + a.GetMetadata(), + ) +} + +func (a *Attribute) AsStringValueOrDefault(defaultValue string, parent *Block) iacTypes.StringValue { + if a.IsNil() { + return iacTypes.StringDefault(defaultValue, parent.GetMetadata()) + } + if !a.IsResolvable() || !a.IsString() { + return iacTypes.StringUnresolvable(a.GetMetadata()) + } + return iacTypes.StringExplicit( + a.Value().AsString(), + a.GetMetadata(), + ) +} + +func (a *Attribute) AsStringValueSliceOrEmpty() (stringValues []iacTypes.StringValue) { + if a.IsNil() { + return stringValues + } + return a.AsStringValues() +} + +func (a *Attribute) AsStringValuesOrDefault(parent *Block, defaults ...string) []iacTypes.StringValue { + if a.IsNil() { + res := make(iacTypes.StringValueList, 0, len(defaults)) + for _, def := range defaults { + res = append(res, iacTypes.StringDefault(def, parent.GetMetadata())) + } + return res + } + return a.AsStringValues() +} + +func (a *Attribute) AsBoolValueOrDefault(defaultValue bool, parent *Block) iacTypes.BoolValue { + if a.IsNil() { + return iacTypes.BoolDefault(defaultValue, parent.GetMetadata()) + } + if !a.IsResolvable() || !a.IsBool() { + return iacTypes.BoolUnresolvable(a.GetMetadata()) + } + return iacTypes.BoolExplicit( + a.IsTrue(), + a.GetMetadata(), + ) +} + +func (a *Attribute) AsIntValueOrDefault(defaultValue int, parent *Block) iacTypes.IntValue { + if a.IsNil() { + return iacTypes.IntDefault(defaultValue, parent.GetMetadata()) + } + if !a.IsResolvable() || !a.IsNumber() { + return iacTypes.IntUnresolvable(a.GetMetadata()) + } + flt := a.AsNumber() + return iacTypes.IntExplicit( + int(flt), + a.GetMetadata(), + ) +} + +func (a *Attribute) IsLiteral() bool { + if a == nil { + return false + } + return len(a.hclAttribute.Expr.Variables()) == 0 +} + +func (a *Attribute) IsResolvable() bool { + if a == nil { + return false + } + return a.Value() != cty.NilVal && a.Value().IsKnown() +} + +func (a *Attribute) Type() cty.Type { + if a == nil { + return cty.NilType + } + return a.Value().Type() +} + +func (a *Attribute) IsIterable() bool { + if a == nil { + return false + } + + ty := a.Value().Type() + return ty.IsListType() || ty.IsObjectType() || ty.IsMapType() || + ty.IsSetType() || ty.IsTupleType() +} + +func (a *Attribute) Each(f func(key cty.Value, val cty.Value)) error { + if a == nil { + return nil + } + var outerErr error + defer func() { + if err := recover(); err != nil { + outerErr = fmt.Errorf("go-cty bug detected - cannot call ForEachElement: %s", err) + } + }() + val := a.Value() + val.ForEachElement(func(key cty.Value, val cty.Value) (stop bool) { + f(key, val) + return false + }) + return outerErr +} + +func (a *Attribute) IsString() bool { + return safeOp(a, func(v cty.Value) bool { + return v.Type() == cty.String + }) +} + +func (a *Attribute) IsMapOrObject() bool { + return safeOp(a, func(v cty.Value) bool { + return v.Type().IsObjectType() || v.Type().IsMapType() + }) +} + +func (a *Attribute) IsNumber() bool { + return safeOp(a, func(v cty.Value) bool { + switch v.Type() { + case cty.Number: + return true + case cty.String: + _, err := strconv.ParseFloat(v.AsString(), 64) + return err == nil + default: + return false + } + }) +} + +func (a *Attribute) IsBool() bool { + return safeOp(a, func(v cty.Value) bool { + switch v.Type() { + case cty.Bool, cty.Number: + return true + case cty.String: + val := v.AsString() + val = strings.Trim(val, "\"") + return strings.EqualFold(val, "false") || strings.EqualFold(val, "true") + default: + return false + } + }) +} + +func (a *Attribute) Value() (ctyVal cty.Value) { + if a == nil { + return cty.NilVal + } + defer func() { + if err := recover(); err != nil { + ctyVal = cty.NilVal + } + }() + ctyVal, _ = a.hclAttribute.Expr.Value(a.ctx.Inner()) + if ctyVal.IsNull() { + return cty.DynamicVal + } + return ctyVal +} + +// Allows a null value for a variable https://developer.hashicorp.com/terraform/language/expressions/types#null +func (a *Attribute) NullableValue() (ctyVal cty.Value) { + if a == nil { + return cty.NilVal + } + defer func() { + if err := recover(); err != nil { + ctyVal = cty.NilVal + } + }() + ctyVal, _ = a.hclAttribute.Expr.Value(a.ctx.Inner()) + if !ctyVal.IsKnown() || ctyVal.IsNull() { + return cty.NullVal(cty.DynamicPseudoType) + } + return ctyVal +} + +func (a *Attribute) Name() string { + if a == nil { + return "" + } + return a.hclAttribute.Name +} + +func (a *Attribute) AsStringValues() iacTypes.StringValueList { + if a == nil { + return nil + } + return a.getStringValues(a.hclAttribute.Expr, a.ctx.Inner()) +} + +// nolint +func (a *Attribute) getStringValues(expr hcl.Expression, ctx *hcl.EvalContext) (results []iacTypes.StringValue) { + + defer func() { + if err := recover(); err != nil { + results = []iacTypes.StringValue{iacTypes.StringUnresolvable(a.metadata)} + } + }() + + switch t := expr.(type) { + case *hclsyntax.TupleConsExpr: + for _, expr := range t.Exprs { + val, err := expr.Value(a.ctx.Inner()) + if err != nil { + results = append(results, iacTypes.StringUnresolvable(a.metadata)) + continue + } + results = append(results, a.valueToString(val)) + } + case *hclsyntax.FunctionCallExpr, *hclsyntax.ConditionalExpr: + subVal, err := t.Value(ctx) + if err != nil { + return append(results, iacTypes.StringUnresolvable(a.metadata)) + } + return a.valueToStrings(subVal) + case *hclsyntax.LiteralValueExpr: + return a.valueToStrings(t.Val) + case *hclsyntax.TemplateExpr: + // walk the parts of the expression to ensure that it has a literal value + for _, p := range t.Parts { + val, err := p.Value(a.ctx.Inner()) + if err != nil { + results = append(results, iacTypes.StringUnresolvable(a.metadata)) + continue + } + value := a.valueToString(val) + results = append(results, value) + } + case *hclsyntax.ScopeTraversalExpr: + // handle the case for referencing a data + if len(t.Variables()) > 0 { + if t.Variables()[0].RootName() == "data" { + // we can't resolve data lookups at this time, so make unresolvable + return append(results, iacTypes.StringUnresolvable(a.metadata)) + } + } + subVal, err := t.Value(ctx) + if err != nil { + return append(results, iacTypes.StringUnresolvable(a.metadata)) + } + return a.valueToStrings(subVal) + default: + val, err := t.Value(a.ctx.Inner()) + if err != nil { + return append(results, iacTypes.StringUnresolvable(a.metadata)) + } + results = a.valueToStrings(val) + } + return results +} + +func (a *Attribute) valueToStrings(value cty.Value) (results []iacTypes.StringValue) { + defer func() { + if err := recover(); err != nil { + results = []iacTypes.StringValue{iacTypes.StringUnresolvable(a.metadata)} + } + }() + if value.IsNull() || !value.IsKnown() { + return []iacTypes.StringValue{iacTypes.StringUnresolvable(a.metadata)} + } + + if value.Type().IsListType() || value.Type().IsTupleType() || value.Type().IsSetType() { + for _, val := range value.AsValueSlice() { + results = append(results, a.valueToString(val)) + } + } + return results +} + +func (a *Attribute) valueToString(value cty.Value) (result iacTypes.StringValue) { + defer func() { + if err := recover(); err != nil { + result = iacTypes.StringUnresolvable(a.metadata) + } + }() + + result = iacTypes.StringUnresolvable(a.metadata) + + if value.IsNull() || !value.IsKnown() { + return result + } + + switch value.Type() { + case cty.String: + return iacTypes.String(value.AsString(), a.metadata) + default: + return result + } +} + +func (a *Attribute) listContains(stringToLookFor string, ignoreCase bool) bool { + return safeOp(a, func(v cty.Value) bool { + if !v.Type().IsListType() && !v.Type().IsTupleType() { + return false + } + + elems := v.AsValueSlice() + for _, el := range elems { + if el.IsNull() || !el.IsKnown() { + // there is nothing we can do with this value + continue + } + stringToTest := el + if el.Type().IsObjectType() || el.Type().IsMapType() { + valueMap := el.AsValueMap() + stringToTest = valueMap["key"] + } + if el.Type().HasDynamicTypes() { + return slices.Contains(a.extractListValues(), stringToLookFor) + } + if ignoreCase { + return strings.EqualFold(stringToTest.AsString(), stringToLookFor) + } + if stringToTest.AsString() == stringToLookFor { + return true + } + } + + return false + }) +} + +func (a *Attribute) extractListValues() []string { + var values []string + if a.hclAttribute == nil || a.hclAttribute.Expr == nil || a.hclAttribute.Expr.Variables() == nil { + return values + } + for _, v := range a.hclAttribute.Expr.Variables() { + values = append(values, v.RootName()) + } + return values +} + +func (a *Attribute) mapContains(checkValue any) bool { + return safeOp(a, func(v cty.Value) bool { + if !v.Type().IsObjectType() && !v.Type().IsMapType() { + return false + } + valueMap := v.AsValueMap() + switch t := checkValue.(type) { + case map[any]any: + for k, v := range t { + for key, value := range valueMap { + rawValue := getRawValue(value) + if key == k && evaluate(v, rawValue) { + return true + } + } + } + return false + case map[string]any: + for k, v := range t { + for key, value := range valueMap { + rawValue := getRawValue(value) + if key == k && evaluate(v, rawValue) { + return true + } + } + } + return false + default: + for key := range valueMap { + if key == checkValue { + return true + } + } + return false + } + }) +} + +func (a *Attribute) Contains(checkValue any, equalityOptions ...EqualityOption) bool { + return safeOp(a, func(v cty.Value) bool { + ignoreCase := slices.Contains(equalityOptions, IgnoreCase) + + if a.IsMapOrObject() { + return a.mapContains(checkValue) + } + + stringToLookFor := fmt.Sprintf("%v", checkValue) + + if v.Type().IsListType() || v.Type().IsTupleType() { + return a.listContains(stringToLookFor, ignoreCase) + } + + if ignoreCase { + return containsIgnoreCase(v.AsString(), stringToLookFor) + } + + return strings.Contains(v.AsString(), stringToLookFor) + }) +} + +func containsIgnoreCase(left, substring string) bool { + return strings.Contains(strings.ToLower(left), strings.ToLower(substring)) +} + +type EqualityOption int + +const ( + IgnoreCase EqualityOption = iota +) + +func (a *Attribute) Equals(checkValue any, equalityOptions ...EqualityOption) bool { + return safeOp(a, func(v cty.Value) bool { + switch v.Type() { + case cty.String: + if slices.Contains(equalityOptions, IgnoreCase) { + return strings.EqualFold(strings.ToLower(v.AsString()), strings.ToLower(fmt.Sprintf("%v", checkValue))) + } + return strings.EqualFold(v.AsString(), fmt.Sprintf("%v", checkValue)) + case cty.Number: + checkNumber, err := gocty.ToCtyValue(checkValue, cty.Number) + if err != nil { + return false + } + return a.Value().RawEquals(checkNumber) + case cty.Bool: + return v.True() == checkValue + default: + return false + } + }) +} + +func (a *Attribute) IsTrue() bool { + return safeOp(a, func(v cty.Value) bool { + switch v.Type() { + case cty.Bool: + return v.True() + case cty.String: + val := v.AsString() + val = strings.Trim(val, "\"") + return strings.EqualFold(val, "true") + case cty.Number: + bf := v.AsBigFloat() + f, _ := bf.Float64() + return f > 0 + default: + return false + } + }) +} + +func (a *Attribute) IsFalse() bool { + return safeOp(a, func(v cty.Value) bool { + switch v.Type() { + case cty.Bool: + return v.False() + case cty.String: + val := v.AsString() + val = strings.Trim(val, "\"") + return strings.EqualFold(val, "false") + case cty.Number: + bf := v.AsBigFloat() + f, _ := bf.Float64() + return f == 0 + default: + return false + } + }) +} + +func (a *Attribute) IsEmpty() bool { + if a == nil { + return false + } + val := a.Value() + ty := val.Type() + if ty.IsTupleType() || ty.IsObjectType() { + return val.LengthInt() == 0 + } + + if val.IsNull() { + return a.isNullAttributeEmpty() + } + + if !val.IsKnown() { + return false + } + + switch { + case ty == cty.String: + return val.AsString() == "" + case ty == cty.Number: + // a number can't ever be empty + return false + case ty.IsListType(), ty.IsSetType(), ty.IsMapType(): + return val.LengthInt() == 0 + } + + return true +} + +func (a *Attribute) isNullAttributeEmpty() bool { + if a == nil { + return false + } + switch t := a.hclAttribute.Expr.(type) { + case *hclsyntax.FunctionCallExpr, *hclsyntax.ScopeTraversalExpr, + *hclsyntax.ConditionalExpr, *hclsyntax.LiteralValueExpr: + return false + case *hclsyntax.TemplateExpr: + // walk the parts of the expression to ensure that it has a literal value + for _, p := range t.Parts { + switch pt := p.(type) { + case *hclsyntax.LiteralValueExpr: + if pt != nil && !pt.Val.IsNull() { + return false + } + case *hclsyntax.ScopeTraversalExpr: + return false + } + } + } + return true +} + +func (a *Attribute) MapValue(mapKey string) cty.Value { + return safeOp(a, func(v cty.Value) cty.Value { + if !v.Type().IsObjectType() && !v.Type().IsMapType() { + return cty.NilVal + } + m := v.AsValueMap() + if m == nil { + return cty.NilVal + } + return m[mapKey] + }) +} + +func (a *Attribute) AsMapValue() iacTypes.MapValue { + return safeOp(a, func(v cty.Value) iacTypes.MapValue { + if !a.IsMapOrObject() { + return iacTypes.MapValue{} + } + + values := make(map[string]string) + v.ForEachElement(func(key cty.Value, val cty.Value) (stop bool) { + if key.Type() == cty.String && key.IsKnown() && + val.Type() == cty.String && val.IsKnown() { + values[key.AsString()] = val.AsString() + } + return false + }) + return iacTypes.Map(values, a.GetMetadata()) + }) +} + +func createDotReferenceFromTraversal(parentRef string, traversals ...hcl.Traversal) (*Reference, error) { + var refParts []string + var key cty.Value + for _, x := range traversals { + for _, p := range x { + switch part := p.(type) { + case hcl.TraverseRoot: + refParts = append(refParts, part.Name) + case hcl.TraverseAttr: + refParts = append(refParts, part.Name) + case hcl.TraverseIndex: + key = part.Key + } + } + } + ref, err := newReference(refParts, parentRef) + if err != nil { + return nil, err + } + ref.SetKey(key) + return ref, nil +} + +func (a *Attribute) ReferencesBlock(b *Block) bool { + if a == nil { + return false + } + for _, ref := range a.AllReferences() { + if ref.RefersTo(b.reference) { + return true + } + } + return false +} + +func (a *Attribute) AllReferences() []*Reference { + if a == nil { + return nil + } + return a.referencesFromExpression(a.hclAttribute.Expr) +} + +func (a *Attribute) referencesFromExpression(expr hcl.Expression) []*Reference { + if reflect.TypeOf(expr).String() == "*json.expression" { + if ref, err := createDotReferenceFromTraversal(a.module, expr.Variables()...); err == nil { + return []*Reference{ref} + } + return nil + } + + vars := expr.Variables() + refs := make([]*Reference, 0, len(vars)) + for _, v := range vars { + ref, err := createDotReferenceFromTraversal(a.module, v) + if err != nil { + continue + } + + if relExpr, ok := expr.(*hclsyntax.RelativeTraversalExpr); ok { + if idxExpr, ok := relExpr.Source.(*hclsyntax.IndexExpr); ok { + key, _ := idxExpr.Key.Value(a.ctx.Inner()) + ref.SetKey(key) + } + } + refs = append(refs, ref) + } + return refs +} + +func (a *Attribute) IsResourceBlockReference(resourceType string) bool { + if a == nil { + return false + } + if t, ok := a.hclAttribute.Expr.(*hclsyntax.ScopeTraversalExpr); ok { + split := t.Traversal.SimpleSplit() + return split.Abs.RootName() == resourceType + } + return false +} + +func (a *Attribute) References(r Reference) bool { + if a == nil { + return false + } + for _, ref := range a.AllReferences() { + if ref.RefersTo(r) { + return true + } + } + return false +} + +func getRawValue(value cty.Value) any { + if value.IsNull() || !value.IsKnown() { + return value + } + + switch value.Type() { + case cty.String: + return value.AsString() + case cty.Number: + return value.AsBigFloat() + case cty.Bool: + return value.True() + default: + return value + } +} + +func (a *Attribute) IsNil() bool { + return a == nil +} + +func (a *Attribute) IsNotNil() bool { + return !a.IsNil() +} + +func (a *Attribute) AsNumber() float64 { + return safeOp(a, func(v cty.Value) float64 { + switch v.Type() { + case cty.Number: + v, _ := v.AsBigFloat().Float64() + return v + case cty.String: + v, _ := strconv.ParseFloat(v.AsString(), 64) + return v + default: + return 0 + } + }) +} + +func safeOp[T any](a *Attribute, fn func(cty.Value) T) T { + var res T + if a == nil { + return res + } + + val := a.Value() + if val.IsNull() || !val.IsKnown() { + return res + } + + unmarked, _ := val.UnmarkDeep() + + return fn(unmarked) +} + +// RewriteExpr applies the given function `transform` to the expression of the attribute, +// recursively traversing and transforming it. +func (a *Attribute) RewriteExpr(transform func(hclsyntax.Expression) hclsyntax.Expression) { + if a == nil || a.hclAttribute == nil { + return + } + expr, ok := a.hclAttribute.Expr.(hclsyntax.Expression) + if !ok { + return + } + a.hclAttribute.Expr = RewriteExpr(expr, transform) +} + +// nolint: gocyclo +// RewriteExpr recursively rewrites an HCL expression tree in-place, +// applying the provided transformation function `transform` to each node. +func RewriteExpr( + expr hclsyntax.Expression, + transform func(hclsyntax.Expression) hclsyntax.Expression, +) hclsyntax.Expression { + if expr == nil { + return nil + } + switch e := expr.(type) { + case *hclsyntax.LiteralValueExpr: + case *hclsyntax.TemplateExpr: + for i, p := range e.Parts { + e.Parts[i] = RewriteExpr(p, transform) + } + case *hclsyntax.TemplateWrapExpr: + e.Wrapped = RewriteExpr(e.Wrapped, transform) + case *hclsyntax.BinaryOpExpr: + e.LHS = RewriteExpr(e.LHS, transform) + e.RHS = RewriteExpr(e.RHS, transform) + case *hclsyntax.UnaryOpExpr: + e.Val = RewriteExpr(e.Val, transform) + case *hclsyntax.TupleConsExpr: + for i, elem := range e.Exprs { + e.Exprs[i] = RewriteExpr(elem, transform) + } + case *hclsyntax.ParenthesesExpr: + e.Expression = RewriteExpr(e.Expression, transform) + case *hclsyntax.ObjectConsExpr: + for i, item := range e.Items { + e.Items[i].KeyExpr = RewriteExpr(item.KeyExpr, transform) + e.Items[i].ValueExpr = RewriteExpr(item.ValueExpr, transform) + } + case *hclsyntax.ObjectConsKeyExpr: + e.Wrapped = RewriteExpr(e.Wrapped, transform) + case *hclsyntax.ScopeTraversalExpr: + case *hclsyntax.RelativeTraversalExpr: + e.Source = RewriteExpr(e.Source, transform) + case *hclsyntax.ConditionalExpr: + e.Condition = RewriteExpr(e.Condition, transform) + e.TrueResult = RewriteExpr(e.TrueResult, transform) + e.FalseResult = RewriteExpr(e.FalseResult, transform) + case *hclsyntax.FunctionCallExpr: + for i, arg := range e.Args { + e.Args[i] = RewriteExpr(arg, transform) + } + case *hclsyntax.IndexExpr: + e.Collection = RewriteExpr(e.Collection, transform) + e.Key = RewriteExpr(e.Key, transform) + case *hclsyntax.ForExpr: + e.CollExpr = RewriteExpr(e.CollExpr, transform) + e.KeyExpr = RewriteExpr(e.KeyExpr, transform) + e.ValExpr = RewriteExpr(e.ValExpr, transform) + e.CondExpr = RewriteExpr(e.CondExpr, transform) + case *hclsyntax.SplatExpr: + e.Source = RewriteExpr(e.Source, transform) + case *hclsyntax.AnonSymbolExpr: + default: + log.Debug( + "RewriteExpr encountered an unhandled expression type", + log.Prefix(log.PrefixMisconfiguration), + log.String("expr_type", fmt.Sprintf("%T", expr)), + ) + } + return transform(expr) +} + +// UnknownValuePrefix is a placeholder string used to represent parts of a +// template expression that cannot be fully evaluated due to unknown values. +const UnknownValuePrefix = "__UNRESOLVED__" + +// PartialTemplateExpr is a wrapper around hclsyntax.TemplateExpr that +// replaces unknown or unevaluated parts with placeholder strings during evaluation. +type PartialTemplateExpr struct { + *hclsyntax.TemplateExpr +} + +func (e *PartialTemplateExpr) Value(ctx *hcl.EvalContext) (cty.Value, hcl.Diagnostics) { + parts := make([]hclsyntax.Expression, len(e.Parts)) + for i, part := range e.Parts { + partVal, diags := part.Value(ctx) + if diags.HasErrors() || partVal.IsNull() || !partVal.IsKnown() { + parts[i] = &hclsyntax.LiteralValueExpr{ + Val: cty.StringVal(UnknownValuePrefix), + SrcRange: part.Range(), + } + } else if _, err := convert.Convert(partVal, cty.String); err != nil { + parts[i] = &hclsyntax.LiteralValueExpr{ + Val: cty.StringVal(UnknownValuePrefix), + SrcRange: part.Range(), + } + } else { + parts[i] = part + } + } + newTemplate := &hclsyntax.TemplateExpr{ + Parts: parts, + SrcRange: e.SrcRange, + } + + return newTemplate.Value(ctx) +} diff --git a/pkg/iac/terraform/attribute_test.go b/pkg/iac/terraform/attribute_test.go new file mode 100644 index 000000000000..5cbf976e702d --- /dev/null +++ b/pkg/iac/terraform/attribute_test.go @@ -0,0 +1,132 @@ +package terraform + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/hcl/v2/json" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/iac/terraform/context" + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +func Test_AllReferences(t *testing.T) { + cases := []struct { + input string + refs []string + }{ + { + input: "42", // literal + refs: []string{}, + }, + { + input: "5 == 5", // comparison + refs: []string{}, + }, + { + input: "var.foo", + refs: []string{"variable.foo"}, + }, + { + input: "resource.foo.bar[local.idx].name", + refs: []string{"foo.bar", "locals.idx"}, + }, + { + input: "resource.foo.bar[0].name", + refs: []string{"foo.bar[0].name"}, + }, + { + input: "resource.aws_instance.id", + refs: []string{"aws_instance.id"}, + }, + { + input: "data.aws_ami.ubuntu.most_recent", + refs: []string{"data.aws_ami.ubuntu.most_recent"}, + }, + { + input: "5 == 5 ? var.foo : data.aws_ami.ubuntu.most_recent", // conditional + refs: []string{"variable.foo", "data.aws_ami.ubuntu.most_recent"}, + }, + { + input: `{x = 1, y = data.aws_ami.ubuntu.most_recent}`, + refs: []string{"data.aws_ami.ubuntu.most_recent"}, + }, + { + input: `{foo = 1 == 1 ? var.bar : data.aws_ami.ubuntu.most_recent}`, + refs: []string{"variable.bar", "data.aws_ami.ubuntu.most_recent"}, + }, + { + input: `[var.foo, var.bar]`, + refs: []string{"variable.foo", "variable.bar"}, + }, + { + // Expression in the key + input: `{(local.foo): local.bar}`, + refs: []string{"locals.foo", "locals.bar"}, + }, + } + + for _, test := range cases { + t.Run(test.input, func(t *testing.T) { + ctx := context.NewContext(&hcl.EvalContext{}, nil) + + exp, diag := hclsyntax.ParseExpression([]byte(test.input), "", hcl.Pos{Line: 1, Column: 1}) + if diag.HasErrors() { + require.NoError(t, diag) + } + + a := NewAttribute(&hcl.Attribute{ + Name: "test", + Expr: exp, + Range: hcl.Range{}, + NameRange: hcl.Range{}, + }, ctx, "", types.Metadata{}, Reference{}, "", nil) + + refs := a.AllReferences() + humanRefs := make([]string, 0, len(refs)) + for _, ref := range refs { + humanRefs = append(humanRefs, ref.HumanReadable()) + } + + require.ElementsMatch(t, test.refs, humanRefs) + }) + } +} + +func Test_AllReferences_JSON(t *testing.T) { + tests := []struct { + src string + expected []string + }{ + { + src: `"hello ${noun}"`, + expected: []string{"noun"}, + }, + } + + for _, tt := range tests { + t.Run(tt.src, func(t *testing.T) { + expr, diag := json.ParseExpression([]byte(tt.src), "") + if diag.HasErrors() { + require.NoError(t, diag) + } + + attr := NewAttribute(&hcl.Attribute{ + Name: "test", + Expr: expr, + Range: hcl.Range{}, + NameRange: hcl.Range{}, + }, context.NewContext(&hcl.EvalContext{}, nil), "", types.Metadata{}, Reference{}, "", nil) + + refs := attr.AllReferences() + humanRefs := make([]string, 0, len(refs)) + for _, ref := range refs { + humanRefs = append(humanRefs, ref.HumanReadable()) + } + + require.ElementsMatch(t, tt.expected, humanRefs) + }) + } +} diff --git a/pkg/iac/terraform/block.go b/pkg/iac/terraform/block.go new file mode 100644 index 000000000000..33dfd391a540 --- /dev/null +++ b/pkg/iac/terraform/block.go @@ -0,0 +1,701 @@ +package terraform + +import ( + "errors" + "fmt" + "io/fs" + "strconv" + "strings" + + "github.com/google/uuid" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" + + "github.com/aquasecurity/trivy/pkg/iac/terraform/context" + iacTypes "github.com/aquasecurity/trivy/pkg/iac/types" + "github.com/aquasecurity/trivy/pkg/set" +) + +type Block struct { + id string + hclBlock *hcl.Block + context *context.Context + moduleBlock *Block + parentBlock *Block + expanded bool + cloneIndex int + childBlocks []*Block + attributes []*Attribute + metadata iacTypes.Metadata + moduleSource string + moduleFS fs.FS + reference Reference +} + +func NewBlock(hclBlock *hcl.Block, ctx *context.Context, moduleBlock *Block, parentBlock *Block, moduleSource string, + moduleFS fs.FS, index ...cty.Value, +) *Block { + if ctx == nil { + ctx = context.NewContext(&hcl.EvalContext{}, nil) + } + + var r hcl.Range + switch body := hclBlock.Body.(type) { + case *hclsyntax.Body: + r = body.SrcRange + default: + r = hclBlock.DefRange + r.End = hclBlock.Body.MissingItemRange().End + } + moduleName := "root" + if moduleBlock != nil { + moduleName = moduleBlock.FullName() + } + rng := iacTypes.NewRange( + r.Filename, + r.Start.Line, + r.End.Line, + moduleSource, + moduleFS, + ) + + var parts []string + // if there are no labels then use the block type + // this is for the case where "special" keywords like "resource" are used + // as normal block names in top level blocks - see issue tfsec#1528 for an example + if hclBlock.Type != "resource" || len(hclBlock.Labels) == 0 { + parts = append(parts, hclBlock.Type) + } + parts = append(parts, hclBlock.Labels...) + + var parent string + if moduleBlock != nil { + parent = moduleBlock.FullName() + } + ref, _ := newReference(parts, parent) + if len(index) > 0 { + key := index[0] + ref.SetKey(key) + } + + metadata := iacTypes.NewMetadata(rng, ref.String()) + + if parentBlock != nil { + metadata = metadata.WithParent(parentBlock.metadata) + } else if moduleBlock != nil { + metadata = metadata.WithParent(moduleBlock.GetMetadata()) + } + + b := Block{ + id: uuid.NewString(), + context: ctx, + hclBlock: hclBlock, + moduleBlock: moduleBlock, + moduleSource: moduleSource, + moduleFS: moduleFS, + parentBlock: parentBlock, + metadata: metadata, + reference: *ref, + } + + var children Blocks + switch body := hclBlock.Body.(type) { + case *hclsyntax.Body: + for _, b2 := range body.Blocks { + children = append(children, NewBlock(b2.AsHCLBlock(), ctx, moduleBlock, &b, moduleSource, moduleFS)) + } + default: + content, _, diag := hclBlock.Body.PartialContent(Schema) + if diag == nil { + for _, hb := range content.Blocks { + children = append(children, NewBlock(hb, ctx, moduleBlock, &b, moduleSource, moduleFS)) + } + } + } + + b.childBlocks = children + + for _, attr := range b.createAttributes() { + b.attributes = append(b.attributes, NewAttribute(attr, ctx, moduleName, metadata, *ref, moduleSource, moduleFS)) + } + + return &b +} + +func (b *Block) HCLBlock() *hcl.Block { + return b.hclBlock +} + +func (b *Block) ID() string { + return b.id +} + +func (b *Block) Reference() Reference { + return b.reference +} + +func (b *Block) GetMetadata() iacTypes.Metadata { + return b.metadata +} + +func (b *Block) GetRawValue() any { + return nil +} + +func (b *Block) injectBlock(block *Block) { + for attrName, attr := range block.Attributes() { + path := fmt.Sprintf("%s.%s.%s", b.reference.String(), block.hclBlock.Type, attrName) + b.context.Root().SetByDot(attr.Value(), path) + } + b.childBlocks = append(b.childBlocks, block) +} + +func (b *Block) markExpanded() { + b.expanded = true +} + +func (b *Block) IsExpanded() bool { + return b.expanded +} + +func (b *Block) inherit(ctx *context.Context, index ...cty.Value) *Block { + return NewBlock(b.copyBlock(), ctx, b.moduleBlock, b.parentBlock, b.moduleSource, b.moduleFS, index...) +} + +func (b *Block) copyBlock() *hcl.Block { + hclBlock := *b.hclBlock + return &hclBlock +} + +func (b *Block) childContext() *context.Context { + if b.context == nil { + return context.NewContext(&hcl.EvalContext{}, nil) + } + return b.context.NewChild() +} + +func (b *Block) Clone(index cty.Value) *Block { + childCtx := b.childContext() + clone := b.inherit(childCtx, index) + + if len(clone.hclBlock.Labels) > 0 { + position := len(clone.hclBlock.Labels) - 1 + labels := make([]string, len(clone.hclBlock.Labels)) + for i := range labels { + labels[i] = clone.hclBlock.Labels[i] + } + if index.IsKnown() && !index.IsNull() { + switch index.Type() { + case cty.Number: + f, _ := index.AsBigFloat().Float64() + labels[position] = fmt.Sprintf("%s[%d]", clone.hclBlock.Labels[position], int(f)) + case cty.String: + labels[position] = fmt.Sprintf("%s[%q]", clone.hclBlock.Labels[position], index.AsString()) + default: + labels[position] = fmt.Sprintf("%s[%#v]", clone.hclBlock.Labels[position], index) + } + } else { + labels[position] = fmt.Sprintf("%s[%d]", clone.hclBlock.Labels[position], b.cloneIndex) + } + clone.hclBlock.Labels = labels + } + indexVal, _ := gocty.ToCtyValue(index, cty.Number) + clone.context.SetByDot(indexVal, "count.index") + clone.markExpanded() + b.cloneIndex++ + return clone +} + +func (b *Block) Context() *context.Context { + return b.context +} + +func (b *Block) OverrideContext(ctx *context.Context) { + b.context = ctx + for _, block := range b.childBlocks { + block.OverrideContext(ctx.NewChild()) + } + for _, attr := range b.attributes { + attr.ctx = ctx + } +} + +func (b *Block) Type() string { + return b.hclBlock.Type +} + +func (b *Block) Labels() []string { + return b.hclBlock.Labels +} + +func (b *Block) GetFirstMatchingBlock(names ...string) *Block { + var returnBlock *Block + for _, name := range names { + childBlock := b.GetBlock(name) + if childBlock.IsNotNil() { + return childBlock + } + } + return returnBlock +} + +func (b *Block) createAttributes() hcl.Attributes { + switch body := b.hclBlock.Body.(type) { + case *hclsyntax.Body: + attributes := make(hcl.Attributes) + for _, a := range body.Attributes { + attributes[a.Name] = a.AsHCLAttribute() + } + return attributes + default: + _, body, diag := b.hclBlock.Body.PartialContent(Schema) + if diag != nil { + return nil + } + attrs, diag := body.JustAttributes() + if diag != nil { + return nil + } + return attrs + } +} + +func (b *Block) GetBlock(name string) *Block { + var returnBlock *Block + if b == nil || b.hclBlock == nil { + return returnBlock + } + for _, child := range b.childBlocks { + if child.Type() == name { + return child + } + } + return returnBlock +} + +func (b *Block) AllBlocks() Blocks { + if b == nil || b.hclBlock == nil { + return nil + } + return b.childBlocks +} + +func (b *Block) GetBlocks(name string) Blocks { + if b == nil || b.hclBlock == nil { + return nil + } + var results []*Block + for _, child := range b.childBlocks { + if child.Type() == name { + results = append(results, child) + } + } + return results +} + +func (b *Block) GetAttributes() []*Attribute { + if b == nil { + return nil + } + return b.attributes +} + +func (b *Block) GetAttribute(name string) *Attribute { + return b.GetFirstAttributeOf(name) +} + +func (b *Block) GetFirstAttributeOf(names ...string) *Attribute { + if b == nil || b.hclBlock == nil || len(names) == 0 { + return nil + } + + nameSet := set.New(names...) + + for _, attr := range b.attributes { + if ok := nameSet.Contains(attr.Name()); ok { + return attr + } + } + return nil +} + +// GetValueByPath returns the value of the attribute located at the given path. +// Supports special paths like "count.index," "each.key," and "each.value." +// The path may contain indices, keys and dots (used as separators). +func (b *Block) GetValueByPath(path string) cty.Value { + if path == "count.index" || path == "each.key" || path == "each.value" { + return b.Context().GetByDot(path) + } + + if restPath, ok := strings.CutPrefix(path, "each.value."); ok { + if restPath == "" { + return cty.NilVal + } + + val := b.Context().GetByDot("each.value") + res, err := getValueByPath(val, strings.Split(restPath, ".")) + if err != nil { + return cty.NilVal + } + return res + } + + attr, restPath := b.getAttributeByPath(path) + + if attr == nil { + return cty.NilVal + } + + if !attr.IsIterable() || len(restPath) == 0 { + return attr.Value() + } + + res, err := getValueByPath(attr.Value(), restPath) + if err != nil { + return cty.NilVal + } + return res +} + +func (b *Block) getAttributeByPath(path string) (*Attribute, []string) { + steps := strings.Split(path, ".") + + if len(steps) == 1 { + return b.GetAttribute(steps[0]), nil + } + + var ( + attribute *Attribute + stepIndex int + ) + + for currentBlock := b; currentBlock != nil && stepIndex < len(steps); { + blocks := currentBlock.GetBlocks(steps[stepIndex]) + var nextBlock *Block + if !hasIndex(steps, stepIndex+1) && len(blocks) > 0 { + // if index is not provided then return the first block for backwards compatibility + nextBlock = blocks[0] + } else if len(blocks) > 1 && stepIndex < len(steps)-2 { + // handling the case when there are multiple blocks with the same name, + // e.g. when using a `dynamic` block + indexVal, err := strconv.Atoi(steps[stepIndex+1]) + if err == nil && indexVal >= 0 && indexVal < len(blocks) { + nextBlock = blocks[indexVal] + stepIndex++ + } + } + + if nextBlock == nil { + attribute = currentBlock.GetAttribute(steps[stepIndex]) + } + + currentBlock = nextBlock + stepIndex++ + } + + return attribute, steps[stepIndex:] +} + +func hasIndex(steps []string, idx int) bool { + if idx < 0 || idx >= len(steps) { + return false + } + _, err := strconv.Atoi(steps[idx]) + return err == nil +} + +func getValueByPath(val cty.Value, path []string) (cty.Value, error) { + var err error + for _, step := range path { + switch valType := val.Type(); { + case valType.IsMapType(): + val, err = cty.IndexStringPath(step).Apply(val) + case valType.IsObjectType(): + val, err = cty.GetAttrPath(step).Apply(val) + case valType.IsListType() || valType.IsTupleType(): + var idx int + idx, err = strconv.Atoi(step) + if err != nil { + return cty.NilVal, fmt.Errorf("index %q is not a number", step) + } + val, err = cty.IndexIntPath(idx).Apply(val) + default: + return cty.NilVal, fmt.Errorf( + "unexpected value type %s for path step %q", + valType.FriendlyName(), step, + ) + } + if err != nil { + return cty.NilVal, err + } + } + return val, nil +} + +func (b *Block) GetNestedAttribute(name string) (*Attribute, *Block) { + parts := strings.Split(name, ".") + blocks := parts[:len(parts)-1] + attrName := parts[len(parts)-1] + + working := b + for _, subBlock := range blocks { + checkBlock := working.GetBlock(subBlock) + if checkBlock == nil { + return nil, working + } + working = checkBlock + } + + if working != nil { + if attr := working.GetAttribute(attrName); attr != nil { + return attr, working + } + } + + return nil, b +} + +func MapNestedAttribute[T any](block *Block, path string, f func(attr *Attribute, parent *Block) T) T { + return f(block.GetNestedAttribute(path)) +} + +// LocalName is the name relative to the current module +func (b *Block) LocalName() string { + return b.reference.String() +} + +func (b *Block) FullLocalName() string { + if b.parentBlock != nil { + return fmt.Sprintf( + "%s.%s", + b.parentBlock.FullLocalName(), + b.LocalName(), + ) + } + return b.LocalName() +} + +func (b *Block) FullName() string { + if b.moduleBlock != nil { + return fmt.Sprintf( + "%s.%s", + b.moduleBlock.FullName(), + b.LocalName(), + ) + } + + return b.LocalName() +} + +func (b *Block) ModuleBlock() *Block { + return b.moduleBlock +} + +func (b *Block) ModuleKey() string { + name := b.Reference().NameLabel() + if b.moduleBlock == nil { + return name + } + return fmt.Sprintf("%s.%s", b.moduleBlock.ModuleKey(), name) +} + +func (b *Block) UniqueName() string { + if b.moduleBlock != nil { + return fmt.Sprintf("%s:%s:%s", b.FullName(), b.metadata.Range().GetFilename(), b.moduleBlock.UniqueName()) + } + return fmt.Sprintf("%s:%s", b.FullName(), b.metadata.Range().GetFilename()) +} + +func (b *Block) TypeLabel() string { + if len(b.Labels()) > 0 { + return b.Labels()[0] + } + return "" +} + +func (b *Block) NameLabel() string { + if len(b.Labels()) > 1 { + return b.Labels()[1] + } + return "" +} + +func (b *Block) InModule() bool { + if b == nil { + return false + } + return b.moduleBlock != nil +} + +func (b *Block) Label() string { + return strings.Join(b.hclBlock.Labels, ".") +} + +func (b *Block) IsResourceType(resourceType string) bool { + return b.TypeLabel() == resourceType +} + +func (b *Block) IsEmpty() bool { + return len(b.AllBlocks()) == 0 && len(b.GetAttributes()) == 0 +} + +func (b *Block) Attributes() map[string]*Attribute { + attributes := make(map[string]*Attribute) + for _, attr := range b.GetAttributes() { + attributes[attr.Name()] = attr + } + return attributes +} + +func (b *Block) NullableValues() cty.Value { + return b.values(true) +} + +func (b *Block) Values() cty.Value { + return b.values(false) +} + +func (b *Block) values(allowNull bool) cty.Value { + values := createPresetValues(b) + for _, attribute := range b.GetAttributes() { + if attribute.Name() == "for_each" { + continue + } + if allowNull { + values[attribute.Name()] = attribute.NullableValue() + } else { + values[attribute.Name()] = attribute.Value() + } + } + return cty.ObjectVal(postProcessValues(b, values)) +} + +func (b *Block) IsNil() bool { + return b == nil +} + +func (b *Block) IsNotNil() bool { + return !b.IsNil() +} + +func (b *Block) ExpandBlock() error { + var ( + expanded []*Block + errs error + ) + + for _, child := range b.childBlocks { + if child.Type() == "dynamic" { + blocks, err := child.expandDynamic() + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("block %q: %w", child.TypeLabel(), err)) + continue + } + expanded = append(expanded, blocks...) + } + } + + for _, block := range expanded { + b.injectBlock(block) + } + + return errs +} + +func (b *Block) expandDynamic() ([]*Block, error) { + if b.IsExpanded() || b.Type() != "dynamic" { + return nil, nil + } + + realBlockType := b.TypeLabel() + if realBlockType == "" { + return nil, errors.New("dynamic block must have 1 label") + } + + forEachVal, err := b.validateForEach() + if err != nil { + return nil, fmt.Errorf("invalid for-each in %s block: %w", b.FullLocalName(), err) + } + + if !forEachVal.IsKnown() { + return nil, errors.New("for-each must be known") + } + + var ( + expanded []*Block + errs error + ) + + forEachVal.ForEachElement(func(key, val cty.Value) (stop bool) { + if val.IsNull() || !val.IsKnown() { + return + } + + iteratorName, err := b.iteratorName(realBlockType) + if err != nil { + errs = multierror.Append(errs, err) + return + } + + forEachCtx := b.childContext() + obj := cty.ObjectVal(map[string]cty.Value{ + "key": key, + "value": val, + }) + forEachCtx.Set(obj, iteratorName) + + if content := b.GetBlock("content"); content != nil { + inherited := content.inherit(forEachCtx) + inherited.hclBlock.Labels = []string{} + inherited.hclBlock.Type = realBlockType + if err := inherited.ExpandBlock(); err != nil { + errs = multierror.Append(errs, err) + return + } + expanded = append(expanded, inherited) + } + return + }) + + if len(expanded) > 0 { + b.markExpanded() + } + + return expanded, errs +} + +func (b *Block) validateForEach() (cty.Value, error) { + forEachAttr := b.GetAttribute("for_each") + if forEachAttr == nil { + return cty.NilVal, errors.New("for_each attribute required") + } + + forEachVal := forEachAttr.Value() + + if !forEachVal.CanIterateElements() { + return cty.NilVal, fmt.Errorf("cannot use a %s value in for_each. An iterable collection is required", forEachVal.GoString()) + } + + return forEachVal, nil +} + +func (b *Block) iteratorName(blockType string) (string, error) { + iteratorAttr := b.GetAttribute("iterator") + if iteratorAttr == nil { + return blockType, nil + } + + traversal, diags := hcl.AbsTraversalForExpr(iteratorAttr.hclAttribute.Expr) + if diags.HasErrors() { + return "", diags + } + + if len(traversal) != 1 { + return "", errors.New("dynamic iterator must be a single variable name") + } + + return traversal.RootName(), nil +} diff --git a/pkg/iac/terraform/blocks.go b/pkg/iac/terraform/blocks.go new file mode 100644 index 000000000000..311e83583d26 --- /dev/null +++ b/pkg/iac/terraform/blocks.go @@ -0,0 +1,22 @@ +package terraform + +type Blocks []*Block + +func (blocks Blocks) OfType(t string) Blocks { + var results []*Block + for _, block := range blocks { + if block.Type() == t { + results = append(results, block) + } + } + return results +} + +func (blocks Blocks) WithID(id string) *Block { + for _, block := range blocks { + if block.ID() == id { + return block + } + } + return nil +} diff --git a/pkg/iac/terraform/context/context.go b/pkg/iac/terraform/context/context.go new file mode 100644 index 000000000000..22ef4d8ef656 --- /dev/null +++ b/pkg/iac/terraform/context/context.go @@ -0,0 +1,151 @@ +package context + +import ( + "maps" + "strings" + + "github.com/hashicorp/hcl/v2" + "github.com/zclconf/go-cty/cty" +) + +type Context struct { + ctx *hcl.EvalContext + parent *Context +} + +func NewContext(ctx *hcl.EvalContext, parent *Context) *Context { + if ctx.Variables == nil { + ctx.Variables = make(map[string]cty.Value) + } + return &Context{ + ctx: ctx, + parent: parent, + } +} + +func (c *Context) NewChild() *Context { + return NewContext(c.ctx.NewChild(), c) +} + +func (c *Context) Parent() *Context { + return c.parent +} + +func (c *Context) Inner() *hcl.EvalContext { + return c.ctx +} + +func (c *Context) Root() *Context { + root := c + for root.Parent() != nil { + root = root.Parent() + } + return root +} + +func (c *Context) Get(parts ...string) cty.Value { + if len(parts) == 0 { + return cty.NilVal + } + + curr := c.ctx.Variables[parts[0]] + if len(parts) == 1 { + return curr + } + + for i, part := range parts[1:] { + if !curr.Type().HasAttribute(part) { + return cty.NilVal + } + + attr := curr.GetAttr(part) + + if i == len(parts)-2 { // iteration from the first element + return attr + } + + if !attr.IsKnown() || !attr.Type().IsObjectType() { + return cty.NilVal + } + curr = attr + } + + return cty.NilVal +} + +func (c *Context) GetByDot(path string) cty.Value { + return c.Get(strings.Split(path, ".")...) +} + +func (c *Context) SetByDot(val cty.Value, path string) { + c.Set(val, strings.Split(path, ".")...) +} + +func (c *Context) Set(val cty.Value, parts ...string) { + if len(parts) == 0 { + return + } + + v := mergeVars(c.ctx.Variables[parts[0]], parts[1:], val) + c.ctx.Variables[parts[0]] = v +} + +func (c *Context) Replace(val cty.Value, path string) { + parts := strings.Split(path, ".") + if len(parts) == 0 { + return + } + + delete(c.ctx.Variables, parts[0]) + c.Set(val, parts...) +} + +func mergeVars(src cty.Value, parts []string, value cty.Value) cty.Value { + + if len(parts) == 0 { + if isNotEmptyObject(src) && isNotEmptyObject(value) { + return mergeObjects(src, value) + } + return value + } + + data := make(map[string]cty.Value) + if isNotEmptyObject(src) { + data = src.AsValueMap() + if attr, ok := data[parts[0]]; ok { + src = attr + } else { + src = cty.EmptyObjectVal + } + } + + data[parts[0]] = mergeVars(src, parts[1:], value) + + return cty.ObjectVal(data) +} + +func mergeObjects(a, b cty.Value) cty.Value { + output := make(map[string]cty.Value) + + maps.Copy(output, a.AsValueMap()) + b.ForEachElement(func(key, val cty.Value) (stop bool) { + k := key.AsString() + old := output[k] + if old.IsKnown() && isNotEmptyObject(old) && isNotEmptyObject(val) { + output[k] = mergeObjects(old, val) + } else { + output[k] = val + } + return false + }) + return cty.ObjectVal(output) +} + +func isNotEmptyObject(val cty.Value) bool { + if val.IsNull() || !val.IsKnown() || !val.Type().IsObjectType() { + return false + } + + unmarked, _ := val.Unmark() + return unmarked.LengthInt() > 0 +} diff --git a/pkg/iac/terraform/context/context_test.go b/pkg/iac/terraform/context/context_test.go new file mode 100644 index 000000000000..f44a2f44db3c --- /dev/null +++ b/pkg/iac/terraform/context/context_test.go @@ -0,0 +1,275 @@ +package context + +import ( + "testing" + + "github.com/hashicorp/hcl/v2" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" +) + +func Test_ContextVariables(t *testing.T) { + underlying := &hcl.EvalContext{} + ctx := NewContext(underlying, nil) + + val, err := gocty.ToCtyValue("hello", cty.String) + if err != nil { + t.Fatal(err) + } + + ctx.Set(val, "my", "value") + value := underlying.Variables["my"].AsValueMap()["value"] + assert.Equal(t, "hello", value.AsString()) + +} + +func Test_ContextVariablesPreservation(t *testing.T) { + + underlying := &hcl.EvalContext{} + underlying.Variables = make(map[string]cty.Value) + underlying.Variables["x"], _ = gocty.ToCtyValue("does it work?", cty.String) + str, _ := gocty.ToCtyValue("something", cty.String) + underlying.Variables["my"] = cty.ObjectVal(map[string]cty.Value{ + "other": str, + "obj": cty.ObjectVal(map[string]cty.Value{ + "another": str, + }), + }) + ctx := NewContext(underlying, nil) + + val, err := gocty.ToCtyValue("hello", cty.String) + if err != nil { + t.Fatal(err) + } + + ctx.Set(val, "my", "value") + assert.Equal(t, "hello", underlying.Variables["my"].AsValueMap()["value"].AsString()) + assert.Equal(t, "something", underlying.Variables["my"].AsValueMap()["other"].AsString()) + assert.Equal(t, "something", underlying.Variables["my"].AsValueMap()["obj"].AsValueMap()["another"].AsString()) + assert.Equal(t, "does it work?", underlying.Variables["x"].AsString()) + +} + +func Test_SetWithMerge(t *testing.T) { + hctx := hcl.EvalContext{ + Variables: map[string]cty.Value{ + "my": cty.ObjectVal(map[string]cty.Value{ + "someValue": cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("test"), + "bar": cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("test"), + }), + }), + }), + }, + } + + ctx := NewContext(&hctx, nil) + + val := cty.ObjectVal(map[string]cty.Value{ + "foo2": cty.StringVal("test2"), + "bar": cty.ObjectVal(map[string]cty.Value{ + "foo2": cty.StringVal("test2"), + }), + }) + + ctx.Set(val, "my", "someValue") + got := ctx.Get("my", "someValue") + expected := cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("test"), + "foo2": cty.StringVal("test2"), + "bar": cty.ObjectVal(map[string]cty.Value{ + "foo": cty.StringVal("test"), + "foo2": cty.StringVal("test2"), + }), + }) + + assert.Equal(t, expected, got) +} + +func Test_ContextVariablesPreservationByDot(t *testing.T) { + + underlying := &hcl.EvalContext{} + underlying.Variables = make(map[string]cty.Value) + underlying.Variables["x"], _ = gocty.ToCtyValue("does it work?", cty.String) + str, _ := gocty.ToCtyValue("something", cty.String) + underlying.Variables["my"] = cty.ObjectVal(map[string]cty.Value{ + "other": str, + "obj": cty.ObjectVal(map[string]cty.Value{ + "another": str, + }), + }) + ctx := NewContext(underlying, nil) + + val, err := gocty.ToCtyValue("hello", cty.String) + if err != nil { + t.Fatal(err) + } + + ctx.SetByDot(val, "my.something.value") + assert.Equal(t, "hello", underlying.Variables["my"].AsValueMap()["something"].AsValueMap()["value"].AsString()) + assert.Equal(t, "something", underlying.Variables["my"].AsValueMap()["other"].AsString()) + assert.Equal(t, "something", underlying.Variables["my"].AsValueMap()["obj"].AsValueMap()["another"].AsString()) + assert.Equal(t, "does it work?", underlying.Variables["x"].AsString()) +} + +func Test_ContextSetThenImmediateGet(t *testing.T) { + + underlying := &hcl.EvalContext{} + + ctx := NewContext(underlying, nil) + + ctx.Set(cty.ObjectVal(map[string]cty.Value{ + "mod_result": cty.StringVal("ok"), + }), "module", "modulename") + + val := ctx.Get("module", "modulename", "mod_result") + assert.Equal(t, "ok", val.AsString()) +} + +func Test_ContextSetThenImmediateGetWithChild(t *testing.T) { + + underlying := &hcl.EvalContext{} + + ctx := NewContext(underlying, nil) + + childCtx := ctx.NewChild() + + childCtx.Root().Set(cty.ObjectVal(map[string]cty.Value{ + "mod_result": cty.StringVal("ok"), + }), "module", "modulename") + + val := ctx.Get("module", "modulename", "mod_result") + assert.Equal(t, "ok", val.AsString()) +} + +func Test_MergeObjects(t *testing.T) { + + tests := []struct { + name string + oldVal cty.Value + newVal cty.Value + expected cty.Value + }{ + { + name: "happy", + oldVal: cty.ObjectVal(map[string]cty.Value{ + "this": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("some_id"), + "arn": cty.StringVal("some_arn"), + }), + }), + newVal: cty.ObjectVal(map[string]cty.Value{ + "this": cty.ObjectVal(map[string]cty.Value{ + "arn": cty.StringVal("some_new_arn"), + "bucket": cty.StringVal("test"), + }), + }), + expected: cty.ObjectVal(map[string]cty.Value{ + "this": cty.ObjectVal(map[string]cty.Value{ + "id": cty.StringVal("some_id"), + "arn": cty.StringVal("some_new_arn"), + "bucket": cty.StringVal("test"), + }), + }), + }, + { + name: "old value is empty", + oldVal: cty.EmptyObjectVal, + newVal: cty.ObjectVal(map[string]cty.Value{ + "this": cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + }), + }), + expected: cty.ObjectVal(map[string]cty.Value{ + "this": cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + }), + }), + }, + { + name: "new value is empty", + oldVal: cty.ObjectVal(map[string]cty.Value{ + "this": cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + }), + }), + newVal: cty.EmptyObjectVal, + expected: cty.ObjectVal(map[string]cty.Value{ + "this": cty.ObjectVal(map[string]cty.Value{ + "bucket": cty.StringVal("test"), + }), + }), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, mergeObjects(tt.oldVal, tt.newVal)) + }) + } + +} + +func Test_IsNotEmptyObject(t *testing.T) { + tests := []struct { + name string + val cty.Value + expected bool + }{ + { + name: "happy", + val: cty.ObjectVal(map[string]cty.Value{ + "field": cty.NilVal, + }), + expected: true, + }, + { + name: "empty object", + val: cty.EmptyObjectVal, + expected: false, + }, + { + name: "nil value", + val: cty.NilVal, + expected: false, + }, + { + name: "dynamic value", + val: cty.DynamicVal, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, isNotEmptyObject(tt.val)) + }) + } +} + +func TestReplace(t *testing.T) { + t.Run("replacement of an existing value", func(t *testing.T) { + underlying := &hcl.EvalContext{} + ctx := NewContext(underlying, nil) + ctx.SetByDot(cty.StringVal("some-value"), "my.value") + require.NotEqual(t, cty.NilVal, ctx.GetByDot("my.value")) + ctx.Replace(cty.NumberIntVal(-1), "my.value") + assert.Equal(t, cty.NumberIntVal(-1), ctx.GetByDot("my.value")) + }) + + t.Run("replacement of a non-existing value", func(t *testing.T) { + underlying := &hcl.EvalContext{} + ctx := NewContext(underlying, nil) + ctx.Replace(cty.NumberIntVal(-1), "my.value") + assert.Equal(t, cty.NumberIntVal(-1), ctx.GetByDot("my.value")) + }) + + t.Run("empty path", func(_ *testing.T) { + underlying := &hcl.EvalContext{} + ctx := NewContext(underlying, nil) + ctx.Replace(cty.NumberIntVal(-1), "") + }) +} diff --git a/pkg/iac/terraform/export.go b/pkg/iac/terraform/export.go new file mode 100644 index 000000000000..acb44002aa90 --- /dev/null +++ b/pkg/iac/terraform/export.go @@ -0,0 +1,106 @@ +package terraform + +import ( + "encoding/json" + + "github.com/samber/lo" + "github.com/zclconf/go-cty/cty" + ctyjson "github.com/zclconf/go-cty/cty/json" + + "github.com/aquasecurity/trivy/pkg/log" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func ExportModules(modules Modules) TerraformConfigExport { + return TerraformConfigExport{ + Modules: xslices.Map(modules, (*Module).ToModuleExport), + } +} + +// TODO(nikpivkin): export directly to OPA values +type TerraformConfigExport struct { + Modules []ModuleExport `json:"modules"` +} + +type ModuleExport struct { + RootPath string `json:"root_path"` + ModulePath string `json:"module_path"` + ParentPath string `json:"parent_path"` + Blocks []BlockExport `json:"blocks"` +} + +type BlockExport struct { + Metadata any `json:"__defsec_metadata"` + Kind string `json:"kind"` + Type string `json:"type"` + Name string `json:"name"` + Attributes map[string]AttributeExport `json:"attributes"` +} + +type AttributeExport struct { + Metadata any `json:"__defsec_metadata"` + Name string `json:"name"` + Value json.RawMessage `json:"value"` + Known bool `json:"known"` +} + +func (c *Module) ToModuleExport() ModuleExport { + var parentPath string + if parentModule := c.Parent(); parentModule != nil { + parentPath = parentModule.ModulePath() + } + return ModuleExport{ + RootPath: c.RootPath(), + ModulePath: c.ModulePath(), + ParentPath: parentPath, + Blocks: xslices.Map(c.blocks, (*Block).ToBlockExport), + } +} + +func (b *Block) ToBlockExport() BlockExport { + typeLabel := b.TypeLabel() + nameLabel := b.NameLabel() + + if len(b.Labels()) == 1 { + nameLabel = typeLabel + typeLabel = "" + } + + return BlockExport{ + Metadata: b.metadata.ToRego(), + Kind: b.Type(), + Type: typeLabel, + Name: nameLabel, + Attributes: lo.SliceToMap( + b.attributes, func(a *Attribute) (string, AttributeExport) { + return a.Name(), a.ToAttributeExport() + }, + ), + } +} + +func (a *Attribute) ToAttributeExport() AttributeExport { + value, known := ExportCtyValueToJSON(a.Value()) + return AttributeExport{ + Metadata: a.metadata.ToRego(), + Name: a.Name(), + Known: known, + Value: value, + } +} + +func ExportCtyValueToJSON(v cty.Value) (json.RawMessage, bool) { + if v.IsNull() || !v.IsKnown() { + return json.RawMessage("null"), false + } + + ty := v.Type() + bytes, err := ctyjson.Marshal(v, ty) + if err != nil { + log.WithPrefix("terraform").Debug("Failed to marshal cty value", + log.String("value", v.GoString()), log.Err(err)) + return json.RawMessage("null"), false + } + + return json.RawMessage(bytes), true +} diff --git a/pkg/iac/terraform/module.go b/pkg/iac/terraform/module.go new file mode 100644 index 000000000000..48771fb2c195 --- /dev/null +++ b/pkg/iac/terraform/module.go @@ -0,0 +1,166 @@ +package terraform + +import ( + "fmt" + + "github.com/aquasecurity/trivy/pkg/iac/ignore" +) + +type Module struct { + blocks Blocks + blockMap map[string]Blocks + rootPath string + modulePath string + ignores ignore.Rules + parent *Module +} + +func NewModule(rootPath, modulePath string, blocks Blocks, ignores ignore.Rules) *Module { + + blockMap := make(map[string]Blocks) + + for _, b := range blocks { + if b.NameLabel() != "" { + blockMap[b.TypeLabel()] = append(blockMap[b.TypeLabel()], b) + } + } + + return &Module{ + blocks: blocks, + ignores: ignores, + blockMap: blockMap, + rootPath: rootPath, + modulePath: modulePath, + } +} + +func (c *Module) SetParent(parent *Module) { + c.parent = parent +} + +func (c *Module) RootPath() string { + return c.rootPath +} + +func (c *Module) ModulePath() string { + return c.modulePath +} + +func (c *Module) Parent() *Module { + return c.parent +} + +func (c *Module) Ignores() ignore.Rules { + return c.ignores +} + +func (c *Module) GetBlocks() Blocks { + return c.blocks +} + +func (c *Module) GetBlocksByTypeLabel(typeLabel string) Blocks { + return c.blockMap[typeLabel] +} + +func (c *Module) getBlocksByType(blockType string, labels ...string) Blocks { + if blockType == "module" { + return c.getModuleBlocks() + } + var results Blocks + for _, label := range labels { + for _, block := range c.blockMap[label] { + if block.Type() == blockType { + results = append(results, block) + } + } + } + return results +} + +func (c *Module) getModuleBlocks() Blocks { + var results Blocks + for _, block := range c.blocks { + if block.Type() == "module" { + results = append(results, block) + } + } + return results +} + +func (c *Module) GetResourcesByType(labels ...string) Blocks { + return c.getBlocksByType("resource", labels...) +} + +func (c *Module) GetResourcesByIDs(ids ...string) Blocks { + var blocks Blocks + + for _, id := range ids { + if block := c.blocks.WithID(id); block != nil { + blocks = append(blocks, block) + } + } + return blocks +} + +func (c *Module) GetDatasByType(label string) Blocks { + return c.getBlocksByType("data", label) +} + +func (c *Module) GetReferencedBlock(referringAttr *Attribute, parentBlock *Block) (*Block, error) { + for _, ref := range referringAttr.AllReferences() { + if ref.TypeLabel() == "each" { + if forEachAttr := parentBlock.GetAttribute("for_each"); forEachAttr.IsNotNil() { + if b, err := c.GetReferencedBlock(forEachAttr, parentBlock); err == nil { + return b, nil + } + } + } + for _, block := range c.blocks { + if ref.RefersTo(block.reference) { + return block, nil + } + kref := *ref + kref.SetKey(parentBlock.reference.RawKey()) + if kref.RefersTo(block.reference) { + return block, nil + } + } + } + return nil, fmt.Errorf("no referenced block found in '%s'", referringAttr.Name()) +} + +func (c *Module) GetBlockByID(id string) (*Block, error) { + found := c.blocks.WithID(id) + if found == nil { + return nil, fmt.Errorf("no block found with id '%s'", id) + } + return found, nil +} + +func (c *Module) GetReferencingResources(originalBlock *Block, referencingLabel, referencingAttributeName string) Blocks { + return c.GetReferencingBlocks(originalBlock, "resource", referencingLabel, referencingAttributeName) +} + +func (c *Module) GetReferencingBlocks(originalBlock *Block, referencingType, referencingLabel, referencingAttributeName string) Blocks { + blocks := c.getBlocksByType(referencingType, referencingLabel) + var results Blocks + for _, block := range blocks { + attr := block.GetAttribute(referencingAttributeName) + if attr == nil { + continue + } + if attr.References(originalBlock.reference) { + results = append(results, block) + } else { + for _, ref := range attr.AllReferences() { + if ref.TypeLabel() == "each" { + fe := block.GetAttribute("for_each") + if fe.References(originalBlock.reference) { + results = append(results, block) + } + } + } + } + } + return results +} diff --git a/pkg/iac/terraform/modules.go b/pkg/iac/terraform/modules.go new file mode 100644 index 000000000000..24c67e6f161b --- /dev/null +++ b/pkg/iac/terraform/modules.go @@ -0,0 +1,117 @@ +package terraform + +import ( + "errors" + + "github.com/aquasecurity/trivy/pkg/iac/types" +) + +type Modules []*Module + +type ResourceIDResolutions map[string]bool + +func (r ResourceIDResolutions) Resolve(id string) { + r[id] = true +} + +func (r ResourceIDResolutions) Orphans() (orphanIDs []string) { + for id, resolved := range r { + if !resolved { + orphanIDs = append(orphanIDs, id) + } + } + return orphanIDs +} + +func (m Modules) GetDatasByType(typeLabel string) Blocks { + var blocks Blocks + for _, module := range m { + blocks = append(blocks, module.GetDatasByType(typeLabel)...) + } + + return blocks +} + +func (m Modules) GetResourcesByType(typeLabel ...string) Blocks { + var blocks Blocks + for _, module := range m { + blocks = append(blocks, module.GetResourcesByType(typeLabel...)...) + } + + return blocks +} + +func (m Modules) GetChildResourceIDMapByType(typeLabels ...string) ResourceIDResolutions { + blocks := m.GetResourcesByType(typeLabels...) + + idMap := make(map[string]bool) + for _, block := range blocks { + idMap[block.ID()] = false + } + + return idMap +} + +func (m Modules) GetReferencedBlock(referringAttr *Attribute, parentBlock *Block) (*Block, error) { + var bestMatch *Block + for _, module := range m { + b, err := module.GetReferencedBlock(referringAttr, parentBlock) + if err == nil { + if bestMatch == nil || b.moduleBlock == parentBlock.moduleBlock { + bestMatch = b + } + } + } + if bestMatch != nil { + return bestMatch, nil + } + return nil, errors.New("block not found") +} + +func (m Modules) GetReferencingResources(originalBlock *Block, referencingLabel, referencingAttributeName string) Blocks { + var blocks Blocks + for _, module := range m { + blocks = append(blocks, module.GetReferencingResources(originalBlock, referencingLabel, referencingAttributeName)...) + } + + return blocks +} + +func (m Modules) GetBlocks() Blocks { + var blocks Blocks + for _, module := range m { + blocks = append(blocks, module.GetBlocks()...) + } + return blocks +} + +func (m Modules) GetBlockById(id string) (*Block, error) { + for _, module := range m { + if found := module.blocks.WithID(id); found != nil { + return found, nil + } + + } + return nil, errors.New("block not found") +} + +func (m Modules) GetResourceByIDs(id ...string) Blocks { + var blocks Blocks + for _, module := range m { + blocks = append(blocks, module.GetResourcesByIDs(id...)...) + } + + return blocks +} + +func (m Modules) GetBlockByIgnoreRange(blockMetadata *types.Metadata) *Block { + for _, module := range m { + for _, block := range module.GetBlocks() { + metadata := block.GetMetadata() + if blockMetadata.Reference() == metadata.Reference() { + return block + } + } + } + return nil +} diff --git a/pkg/iac/terraform/presets.go b/pkg/iac/terraform/presets.go new file mode 100644 index 000000000000..6999e0cef7ec --- /dev/null +++ b/pkg/iac/terraform/presets.go @@ -0,0 +1,74 @@ +package terraform + +import ( + "fmt" + "math/rand/v2" + "strings" + + "github.com/google/uuid" + "github.com/zclconf/go-cty/cty" +) + +var resourceRandomAttributes = map[string][]string{ + // If the user leaves the name blank, Terraform will automatically generate a unique name + "aws_launch_template": {"name"}, + "random_id": {"hex", "dec", "b64_url", "b64_std"}, + "random_password": {"result", "bcrypt_hash"}, + "random_string": {"result"}, + "random_bytes": {"base64", "hex"}, + "random_uuid": {"result"}, +} + +func createPresetValues(b *Block) map[string]cty.Value { + presets := make(map[string]cty.Value) + + // here we set up common "id" values that are set by the provider - this ensures all blocks have a default + // referencable id/arn. this isn't perfect, but the only way to link blocks in certain circumstances. + presets["id"] = cty.StringVal(b.ID()) + + if strings.HasPrefix(b.TypeLabel(), "aws_") { + presets["arn"] = cty.StringVal(b.ID()) + } + + switch b.TypeLabel() { + // workaround for weird iam feature + case "aws_iam_policy_document": + presets["json"] = cty.StringVal(b.ID()) + // allow referencing the current region name + case "aws_region": + presets["name"] = cty.StringVal("current-region") + case "random_integer": + //nolint:gosec + presets["result"] = cty.NumberIntVal(rand.Int64()) + } + + if attrs, exists := resourceRandomAttributes[b.TypeLabel()]; exists { + for _, attr := range attrs { + presets[attr] = cty.StringVal(uuid.New().String()) + } + } + + return presets +} + +func postProcessValues(b *Block, input map[string]cty.Value) map[string]cty.Value { + + // alias id to "bucket" (bucket name) for s3 bucket resources + if strings.HasPrefix(b.TypeLabel(), "aws_s3_bucket") { + if bucket, ok := input["bucket"]; ok { + input["id"] = bucket + } else { + input["bucket"] = cty.StringVal(b.ID()) + } + } + + if b.TypeLabel() == "aws_s3_bucket" { + var bucketName string + if bucket := input["bucket"]; bucket.Type().Equals(cty.String) && bucket.IsKnown() { + bucketName = bucket.AsString() + } + input["arn"] = cty.StringVal(fmt.Sprintf("arn:aws:s3:::%s", bucketName)) + } + + return input +} diff --git a/pkg/iac/terraform/reference.go b/pkg/iac/terraform/reference.go new file mode 100644 index 000000000000..de601d7433a0 --- /dev/null +++ b/pkg/iac/terraform/reference.go @@ -0,0 +1,178 @@ +package terraform + +import ( + "errors" + "fmt" + + "github.com/zclconf/go-cty/cty" +) + +type Reference struct { + blockType Type + typeLabel string + nameLabel string + remainder []string + key cty.Value + parent string +} + +func extendReference(ref Reference, name string) Reference { + child := ref + child.remainder = make([]string, len(ref.remainder)) + if len(ref.remainder) > 0 { + copy(child.remainder, ref.remainder) + } + child.remainder = append(child.remainder, name) + return child +} + +func newReference(parts []string, parentKey string) (*Reference, error) { + + var ref Reference + + if len(parts) == 0 { + return nil, errors.New("cannot create empty reference") + } + + blockType, err := TypeFromRefName(parts[0]) + if err != nil { + blockType = &TypeResource + } + + ref.blockType = *blockType + + if ref.blockType.removeTypeInReference && parts[0] != blockType.name { + ref.typeLabel = parts[0] + if len(parts) > 1 { + ref.nameLabel = parts[1] + } + } else if len(parts) > 1 { + ref.typeLabel = parts[1] + if len(parts) > 2 { + ref.nameLabel = parts[2] + } else { + ref.nameLabel = ref.typeLabel + ref.typeLabel = "" + } + } + if len(parts) > 3 { + ref.remainder = parts[3:] + } + + if parentKey != "root" { + ref.parent = parentKey + } + + return &ref, nil +} + +func (r Reference) BlockType() Type { + return r.blockType +} + +func (r Reference) TypeLabel() string { + return r.typeLabel +} + +func (r Reference) NameLabel() string { + return r.nameLabel +} + +func (r Reference) HumanReadable() string { + if r.parent == "" { + return r.String() + } + return fmt.Sprintf("%s:%s", r.parent, r.String()) +} + +func (r Reference) LogicalID() string { + return r.String() +} + +func (r Reference) String() string { + + base := r.typeLabel + if r.nameLabel != "" { + base = fmt.Sprintf("%s.%s", base, r.nameLabel) + } + + if !r.blockType.removeTypeInReference { + base = r.blockType.Name() + if r.typeLabel != "" { + base += "." + r.typeLabel + } + if r.nameLabel != "" { + base += "." + r.nameLabel + } + } + + base += r.KeyBracketed() + + for _, rem := range r.remainder { + base += "." + rem + } + + return base +} + +func (r Reference) RefersTo(other Reference) bool { + + if r.BlockType() != other.BlockType() { + return false + } + if r.TypeLabel() != other.TypeLabel() { + return false + } + if r.NameLabel() != other.NameLabel() { + return false + } + if (r.Key() != "" || other.Key() != "") && r.Key() != other.Key() { + return false + } + return true +} + +func (r *Reference) SetKey(key cty.Value) { + if key.IsNull() || !key.IsKnown() { + return + } + r.key = key +} + +func (r Reference) KeyBracketed() string { + switch v := key(r).(type) { + case int: + return fmt.Sprintf("[%d]", v) + case string: + if v == "" { + return "" + } + return fmt.Sprintf("[%q]", v) + default: + return "" + } +} + +func (r Reference) RawKey() cty.Value { + return r.key +} + +func (r Reference) Key() string { + return fmt.Sprintf("%v", key(r)) +} + +func key(r Reference) any { + if r.key.IsNull() || !r.key.IsKnown() { + return "" + } + switch r.key.Type() { + case cty.Number: + f := r.key.AsBigFloat() + f64, _ := f.Float64() + return int(f64) + case cty.String: + return r.key.AsString() + default: + return "" + } +} diff --git a/pkg/iac/terraform/reference_test.go b/pkg/iac/terraform/reference_test.go new file mode 100644 index 000000000000..6ad5bc782655 --- /dev/null +++ b/pkg/iac/terraform/reference_test.go @@ -0,0 +1,171 @@ +package terraform + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" +) + +func Test_ReferenceParsing(t *testing.T) { + cases := []struct { + input []string + expected string + }{ + { + input: []string{"module", "my-mod"}, + expected: "module.my-mod", + }, + { + input: []string{"aws_s3_bucket", "test"}, + expected: "aws_s3_bucket.test", + }, + { + input: []string{"resource", "aws_s3_bucket", "test"}, + expected: "aws_s3_bucket.test", + }, + { + input: []string{"module", "my-mod"}, + expected: "module.my-mod", + }, + { + input: []string{"data", "aws_iam_policy_document", "s3_policy"}, + expected: "data.aws_iam_policy_document.s3_policy", + }, + { + input: []string{"provider", "aws"}, + expected: "provider.aws", + }, + { + input: []string{"output", "something"}, + expected: "output.something", + }, + } + + for _, test := range cases { + t.Run(test.expected, func(t *testing.T) { + ref, err := newReference(test.input, "") + require.NoError(t, err) + assert.Equal(t, test.expected, ref.String()) + }) + } +} + +func Test_SetKey(t *testing.T) { + tests := []struct { + name string + key cty.Value + want cty.Value + }{ + { + name: "happy", + key: cty.StringVal("str"), + want: cty.StringVal("str"), + }, + { + name: "null key", + key: cty.NullVal(cty.String), + want: cty.Value{}, + }, + { + name: "unknown key", + key: cty.UnknownVal(cty.String), + want: cty.Value{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { + ref, err := newReference([]string{"resource", "test"}, "") + require.NoError(t, err) + + ref.SetKey(tt.key) + + assert.Equal(t, tt.want, ref.RawKey()) + }) + }) + } +} + +func Test_Key(t *testing.T) { + + tests := []struct { + name string + key cty.Value + want string + }{ + { + name: "empty key", + want: "", + }, + { + name: "str key", + key: cty.StringVal("some_value"), + want: "some_value", + }, + { + name: "number key", + key: cty.NumberIntVal(122), + want: "122", + }, + { + name: "bool key", + key: cty.BoolVal(true), + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Run(tt.name, func(t *testing.T) { + ref, err := newReference([]string{"resource", "test"}, "") + require.NoError(t, err) + + ref.SetKey(tt.key) + + assert.Equal(t, tt.want, ref.Key()) + }) + }) + } +} + +func Test_KeyBracketed(t *testing.T) { + tests := []struct { + name string + key cty.Value + want string + }{ + { + name: "empty key", + want: "", + }, + { + name: "str key", + key: cty.StringVal("some_value"), + want: "[\"some_value\"]", + }, + { + name: "number key", + key: cty.NumberIntVal(122), + want: "[122]", + }, + { + name: "bool key", + key: cty.BoolVal(true), + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ref, err := newReference([]string{"resource", "test"}, "") + require.NoError(t, err) + + ref.SetKey(tt.key) + + assert.Equal(t, tt.want, ref.KeyBracketed()) + }) + } +} diff --git a/pkg/iac/terraform/schema.go b/pkg/iac/terraform/schema.go new file mode 100644 index 000000000000..dcedda7c0002 --- /dev/null +++ b/pkg/iac/terraform/schema.go @@ -0,0 +1,63 @@ +package terraform + +import "github.com/hashicorp/hcl/v2" + +// Schema is regularly lifted from terraform source to ensure compatibility +var Schema = &hcl.BodySchema{ + Blocks: []hcl.BlockHeaderSchema{ + { + Type: "terraform", + }, + { + Type: "required_providers", + }, + { + Type: "provider", + LabelNames: []string{"name"}, + }, + { + Type: "variable", + LabelNames: []string{"name"}, + }, + { + Type: "locals", + }, + { + Type: "output", + LabelNames: []string{"name"}, + }, + { + Type: "module", + LabelNames: []string{"name"}, + }, + { + Type: "check", + LabelNames: []string{"name"}, + }, + { + Type: "resource", + LabelNames: []string{"type", "name"}, + }, + { + Type: "data", + LabelNames: []string{"type", "name"}, + }, + { + Type: "ephemeral", + LabelNames: []string{"type", "name"}, + }, + { + Type: "action", + LabelNames: []string{"type", "name"}, + }, + { + Type: "moved", + }, + { + Type: "import", + }, + { + Type: "removed", + }, + }, +} diff --git a/pkg/iac/terraform/type.go b/pkg/iac/terraform/type.go new file mode 100644 index 000000000000..5a91fee61a5f --- /dev/null +++ b/pkg/iac/terraform/type.go @@ -0,0 +1,108 @@ +package terraform + +import "errors" + +type Type struct { + name string + refName string + removeTypeInReference bool +} + +func (t Type) Name() string { + return t.name +} + +func (t Type) ShortName() string { + if t.refName != "" { + return t.refName + } + return t.name +} + +var TypeCheck = Type{ + name: "check", +} + +var TypeData = Type{ + name: "data", +} + +var TypeResource = Type{ + name: "resource", + removeTypeInReference: true, +} + +var TypeVariable = Type{ + name: "variable", + refName: "var", +} + +var TypeImport = Type{ + name: "import", +} + +var TypeLocal = Type{ + name: "locals", + refName: "local", +} + +var TypeMoved = Type{ + name: "moved", +} + +var TypeProvider = Type{ + name: "provider", +} + +var TypeOutput = Type{ + name: "output", +} + +var TypeModule = Type{ + name: "module", +} + +var TypeTerraform = Type{ + name: "terraform", +} + +var ValidTypes = []Type{ + TypeCheck, + TypeData, + TypeImport, + TypeLocal, + TypeModule, + TypeMoved, + TypeOutput, + TypeProvider, + TypeResource, + TypeTerraform, + TypeVariable, +} + +func IsValidType(name string) bool { + for _, valid := range ValidTypes { + if valid.name == name { + return true + } + } + return false +} + +func IsValidBlockReference(name string) bool { + for _, valid := range ValidTypes { + if valid.refName == name { + return true + } + } + return false +} + +func TypeFromRefName(name string) (*Type, error) { + for _, valid := range ValidTypes { + if valid.refName == name || (valid.refName == "" && valid.name == name) { + return &valid, nil + } + } + return nil, errors.New("block type not found") +} diff --git a/pkg/iac/terraform/value_functions.go b/pkg/iac/terraform/value_functions.go new file mode 100644 index 000000000000..e6406493f5c0 --- /dev/null +++ b/pkg/iac/terraform/value_functions.go @@ -0,0 +1,80 @@ +package terraform + +import ( + "fmt" + "regexp" + "slices" + + "github.com/zclconf/go-cty/cty" + "github.com/zclconf/go-cty/cty/gocty" +) + +const ( + functionNameKey = "action" + valueNameKey = "value" +) + +var functions = map[string]func(any, any) bool{ + "isAny": isAny, + "isNone": isNone, + "regexMatches": regexMatches, +} + +func evaluate(criteriaValue, testValue any) bool { + switch t := criteriaValue.(type) { + case map[any]any: + if t[functionNameKey] != nil { + return executeFunction(t[functionNameKey].(string), t[valueNameKey], testValue) + } + case map[string]any: + if t[functionNameKey] != nil { + return executeFunction(t[functionNameKey].(string), t[valueNameKey], testValue) + } + default: + return t == testValue + } + return false +} + +func executeFunction(functionName string, criteriaValues, testValue any) bool { + if functions[functionName] != nil { + return functions[functionName](criteriaValues, testValue) + } + return false +} + +func isAny(criteriaValues, testValue any) bool { + switch t := criteriaValues.(type) { + case []any: + return slices.Contains(t, testValue) + case []string: + return slices.Contains(t, testValue.(string)) + } + return false +} + +func isNone(criteriaValues, testValue any) bool { + return !isAny(criteriaValues, testValue) +} + +func regexMatches(criteriaValue, testValue any) bool { + var patternVal string + switch t := criteriaValue.(type) { + case string: + patternVal = fmt.Sprintf("%v", criteriaValue) + case cty.Value: + if err := gocty.FromCtyValue(t, &patternVal); err != nil { + return false + } + default: + return false + } + + re, err := regexp.Compile(patternVal) + if err != nil { + return false + } + + match := re.MatchString(fmt.Sprintf("%v", testValue)) + return match +} diff --git a/pkg/iac/types/base_value.go b/pkg/iac/types/base_value.go new file mode 100644 index 000000000000..53628cd20174 --- /dev/null +++ b/pkg/iac/types/base_value.go @@ -0,0 +1,79 @@ +package types + +import ( + "encoding/json" +) + +type BaseValue[T any] struct { + metadata Metadata + value T +} + +func defaultValue[T any](value T, m Metadata) BaseValue[T] { + m.isDefault = true + return newValue(value, m) +} + +func unresolvableValue[T any](m Metadata) BaseValue[T] { + m.isUnresolvable = true + var zero T + return newValue(zero, m) +} + +func explicitValue[T any](value T, m Metadata) BaseValue[T] { + m.isExplicit = true + return newValue(value, m) +} + +func testValue[T any](value T) BaseValue[T] { + return newValue(value, NewTestMetadata()) +} + +func newValue[T any](val T, metadata Metadata) BaseValue[T] { + return BaseValue[T]{ + metadata: metadata, + value: val, + } +} + +func (v BaseValue[T]) GetMetadata() Metadata { + return v.metadata +} + +func (v BaseValue[T]) Value() T { + return v.value +} + +func (v BaseValue[T]) GetRawValue() any { + return v.value +} + +func (v BaseValue[T]) ToRego() any { + m := v.metadata.ToRego().(map[string]any) + m["value"] = v.value + return m +} + +type encodedValue[T any] struct { + Value T `json:"value"` + Metadata Metadata `json:"metadata"` +} + +func (v BaseValue[T]) MarshalJSON() ([]byte, error) { + ev := encodedValue[T]{ + Value: v.value, + Metadata: v.metadata, + } + return json.Marshal(ev) +} + +func (v *BaseValue[T]) UnmarshalJSON(data []byte) error { + var ev encodedValue[T] + if err := json.Unmarshal(data, &ev); err != nil { + return err + } + + v.value = ev.Value + v.metadata = ev.Metadata + return nil +} diff --git a/pkg/iac/types/bool.go b/pkg/iac/types/bool.go new file mode 100755 index 000000000000..d9f4d72ebd8d --- /dev/null +++ b/pkg/iac/types/bool.go @@ -0,0 +1,89 @@ +package types + +import ( + "strings" + + "github.com/zclconf/go-cty/cty" +) + +type BoolValue struct { + BaseValue[bool] +} + +func Bool(value bool, m Metadata) BoolValue { + return BoolValue{newValue(value, m)} +} + +func BoolDefault(value bool, m Metadata) BoolValue { + return BoolValue{defaultValue(value, m)} +} + +func BoolUnresolvable(m Metadata) BoolValue { + return BoolValue{unresolvableValue[bool](m)} +} + +func BoolExplicit(value bool, m Metadata) BoolValue { + return BoolValue{explicitValue(value, m)} +} + +func BoolTest(value bool) BoolValue { + return BoolValue{testValue(value)} +} + +func (b BoolValue) IsTrue() bool { + if b.metadata.isUnresolvable { + return false + } + return b.Value() +} + +func (b BoolValue) IsFalse() bool { + if b.metadata.isUnresolvable { + return false + } + return !b.Value() +} + +func (b BoolValue) Invert() BoolValue { + return Bool(!b.value, b.metadata) +} + +// BoolFromCtyValue converts a cty.Value to iacTypes.BoolValue. +// Returns the BoolValue and true if conversion to bool succeeded. +func BoolFromCtyValue(val cty.Value, metadata Metadata) (BoolValue, bool) { + if val.IsNull() || !val.IsKnown() { + return BoolUnresolvable(metadata), false + } + + unmarked, _ := val.Unmark() + v, ok := ctyToBool(unmarked) + if !ok { + return BoolUnresolvable(metadata), false + } + + return BoolExplicit(v, metadata), true +} + +func ctyToBool(val cty.Value) (bool, bool) { + switch val.Type() { + case cty.Bool: + return val.True(), true + case cty.String: + switch strings.ToLower(val.AsString()) { + case "true", "yes", "y", "1", "on": + return true, true + case "false", "no", "n", "0", "off": + return false, true + } + case cty.Number: + v, _ := val.AsBigFloat().Int64() + switch v { + case 1: + return true, true + case 0: + return false, true + } + } + + return false, false +} diff --git a/pkg/iac/types/bool_test.go b/pkg/iac/types/bool_test.go new file mode 100644 index 000000000000..942fa6511f9e --- /dev/null +++ b/pkg/iac/types/bool_test.go @@ -0,0 +1,96 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zclconf/go-cty/cty" +) + +var fakeMetadata = NewMetadata(NewRange("main.tf", 123, 123, "", nil), "") + +func Test_BoolValueIsTrue(t *testing.T) { + testCases := []struct { + desc string + value bool + expected bool + }{ + { + desc: "returns true when isTrue", + value: true, + expected: true, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + + val := Bool(tC.value, fakeMetadata) + + assert.Equal(t, tC.expected, val.IsTrue()) + }) + } +} + +func Test_BoolJSON(t *testing.T) { + val := Bool(true, NewMetadata(NewRange("main.tf", 123, 123, "", nil), "")) + data, err := json.Marshal(val) + require.NoError(t, err) + + var restored BoolValue + err = json.Unmarshal(data, &restored) + require.NoError(t, err) + + assert.Equal(t, val, restored) +} + +func TestGetBoolFromValue(t *testing.T) { + metadata := NewTestMetadata() + + tests := []struct { + name string + ctyVal cty.Value + expected bool + ok bool + }{ + // Bool + {"bool true", cty.BoolVal(true), true, true}, + {"bool false", cty.BoolVal(false), false, true}, + + // Strings (true) + {"string 'true'", cty.StringVal("true"), true, true}, + {"string 'TRUE'", cty.StringVal("TRUE"), true, true}, + {"string 'yes'", cty.StringVal("yes"), true, true}, + {"string '1'", cty.StringVal("1"), true, true}, + + // Strings (false) + {"string 'false'", cty.StringVal("false"), false, true}, + {"string 'NO'", cty.StringVal("NO"), false, true}, + {"string '0'", cty.StringVal("0"), false, true}, + + // Numbers + {"number 1", cty.NumberIntVal(1), true, true}, + {"number 0", cty.NumberIntVal(0), false, true}, + {"number 42 (invalid)", cty.NumberIntVal(42), false, false}, + + // Null / Unknown + {"null", cty.NullVal(cty.Bool), false, false}, + {"unknown", cty.UnknownVal(cty.Bool), false, false}, + + // Invalid string + {"string 'maybe'", cty.StringVal("maybe"), false, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := BoolFromCtyValue(tt.ctyVal, metadata) + assert.Equal(t, tt.ok, ok) + if ok { + assert.Equal(t, tt.expected, got.Value()) + } else { + assert.False(t, got.Value()) + } + }) + } +} diff --git a/pkg/iac/types/bytes.go b/pkg/iac/types/bytes.go new file mode 100755 index 000000000000..8697bc44a5e7 --- /dev/null +++ b/pkg/iac/types/bytes.go @@ -0,0 +1,35 @@ +package types + +type BytesValue struct { + BaseValue[[]byte] +} + +func Bytes(value []byte, m Metadata) BytesValue { + return BytesValue{newValue(value, m)} +} + +func BytesDefault(value []byte, m Metadata) BytesValue { + return BytesValue{defaultValue(value, m)} +} + +func BytesExplicit(value []byte, m Metadata) BytesValue { + return BytesValue{explicitValue(value, m)} +} + +func BytesUnresolvable(m Metadata) BytesValue { + return BytesValue{unresolvableValue[[]byte](m)} +} + +func BytesTest(value []byte) BytesValue { + return BytesValue{testValue(value)} +} + +func (v BytesValue) ToRego() any { + m := v.metadata.ToRego().(map[string]any) + m["value"] = string(v.value) + return m +} + +func (v BytesValue) Len() int { + return len(v.value) +} diff --git a/pkg/iac/types/bytes_test.go b/pkg/iac/types/bytes_test.go new file mode 100644 index 000000000000..709f187b1add --- /dev/null +++ b/pkg/iac/types/bytes_test.go @@ -0,0 +1,21 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_BytesJSON(t *testing.T) { + val := Bytes([]byte{0xff, 0x88, 0x00}, NewMetadata(NewRange("main.tf", 123, 123, "", nil), "")) + data, err := json.Marshal(val) + require.NoError(t, err) + + var restored BytesValue + err = json.Unmarshal(data, &restored) + require.NoError(t, err) + + assert.Equal(t, val, restored) +} diff --git a/pkg/iac/types/compliance.go b/pkg/iac/types/compliance.go new file mode 100644 index 000000000000..5bb0fa346975 --- /dev/null +++ b/pkg/iac/types/compliance.go @@ -0,0 +1,39 @@ +package types + +type Severity string +type ControlStatus string + +// SpecCheck represent the scanner who perform the control check +type SpecCheck struct { + ID string `yaml:"id"` +} +type Command struct { + ID string `yaml:"id"` +} + +// ComplianceSpec represent the compliance specification +type ComplianceSpec struct { + Spec Spec `yaml:"spec"` +} + +type Spec struct { + ID string `yaml:"id"` + Title string `yaml:"title"` + Description string `yaml:"description"` + Version string `yaml:"version"` + Platform string `yaml:"platform"` + Type string `yaml:"type"` + RelatedResources []string `yaml:"relatedResources,omitempty"` + Controls []Control `yaml:"controls"` +} + +// Control represent the cps controls data and mapping checks +type Control struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + Description string `yaml:"description,omitempty"` + Checks []SpecCheck `yaml:"checks,omitempty"` + Commands []Command `yaml:"commands,omitempty"` + Severity Severity `yaml:"severity"` + DefaultStatus ControlStatus `yaml:"defaultStatus,omitempty"` +} diff --git a/pkg/iac/types/fskey.go b/pkg/iac/types/fskey.go new file mode 100644 index 000000000000..35db93d2af70 --- /dev/null +++ b/pkg/iac/types/fskey.go @@ -0,0 +1,14 @@ +package types + +import ( + "crypto/sha256" + "fmt" + "io/fs" +) + +func CreateFSKey(filesystem fs.FS) string { + if filesystem == nil { + return "" + } + return fmt.Sprintf("%x", sha256.Sum256(fmt.Appendf(nil, "%s%#[1]v", filesystem))) +} diff --git a/pkg/iac/types/fskey_test.go b/pkg/iac/types/fskey_test.go new file mode 100644 index 000000000000..a9288b58bc40 --- /dev/null +++ b/pkg/iac/types/fskey_test.go @@ -0,0 +1,41 @@ +package types + +import ( + "io/fs" + "os" + "testing" + "testing/fstest" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/mapfs" + "github.com/aquasecurity/trivy/pkg/set" +) + +func Test_FSKey(t *testing.T) { + + systems := []fs.FS{ + os.DirFS("."), + os.DirFS(".."), + fstest.MapFS{}, + mapfs.New(), + mapfs.New(), + } + + keys := set.New[string]() + + t.Run("uniqueness", func(t *testing.T) { + for _, system := range systems { + key := CreateFSKey(system) + assert.False(t, keys.Contains(key), "filesystem keys should be unique") + keys.Append(key) + } + }) + + t.Run("reproducible", func(t *testing.T) { + for _, system := range systems { + key := CreateFSKey(system) + assert.True(t, keys.Contains(key), "filesystem keys should be reproducible") + } + }) +} diff --git a/pkg/iac/types/int.go b/pkg/iac/types/int.go new file mode 100755 index 000000000000..ae6ba7461359 --- /dev/null +++ b/pkg/iac/types/int.go @@ -0,0 +1,46 @@ +package types + +type IntValue struct { + BaseValue[int] +} + +func Int(value int, m Metadata) IntValue { + return IntValue{newValue(value, m)} +} + +func IntDefault(value int, m Metadata) IntValue { + return IntValue{defaultValue(value, m)} +} + +func IntUnresolvable(m Metadata) IntValue { + return IntValue{unresolvableValue[int](m)} +} + +func IntExplicit(value int, m Metadata) IntValue { + return IntValue{newValue(value, m)} +} + +func IntTest(value int) IntValue { + return IntValue{testValue(value)} +} + +func (b IntValue) EqualTo(i int) bool { + if b.metadata.isUnresolvable { + return false + } + return b.value == i +} + +func (b IntValue) LessThan(i int) bool { + if b.metadata.isUnresolvable { + return false + } + return b.value < i +} + +func (b IntValue) GreaterThan(i int) bool { + if b.metadata.isUnresolvable { + return false + } + return b.value > i +} diff --git a/pkg/iac/types/int_test.go b/pkg/iac/types/int_test.go new file mode 100644 index 000000000000..83e2d65cf0b0 --- /dev/null +++ b/pkg/iac/types/int_test.go @@ -0,0 +1,21 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_IntJSON(t *testing.T) { + val := Int(0x66, NewMetadata(NewRange("main.tf", 123, 123, "", nil), "")) + data, err := json.Marshal(val) + require.NoError(t, err) + + var restored IntValue + err = json.Unmarshal(data, &restored) + require.NoError(t, err) + + assert.Equal(t, val, restored) +} diff --git a/pkg/iac/types/map.go b/pkg/iac/types/map.go new file mode 100755 index 000000000000..5055c7d4dee2 --- /dev/null +++ b/pkg/iac/types/map.go @@ -0,0 +1,41 @@ +package types + +type MapValue struct { + BaseValue[map[string]string] +} + +func Map(value map[string]string, m Metadata) MapValue { + return MapValue{newValue(value, m)} +} + +func MapDefault(value map[string]string, m Metadata) MapValue { + return MapValue{defaultValue(value, m)} +} + +func MapExplicit(value map[string]string, m Metadata) MapValue { + return MapValue{explicitValue(value, m)} +} + +func MapTest(value map[string]string) MapValue { + return MapValue{testValue(value)} +} + +func (b MapValue) Value() map[string]string { + return b.value +} + +func (b MapValue) GetRawValue() any { + return b.value +} + +func (b MapValue) Len() int { + return len(b.value) +} + +func (b MapValue) HasKey(key string) bool { + if b.value == nil { + return false + } + _, ok := b.value[key] + return ok +} diff --git a/pkg/iac/types/map_test.go b/pkg/iac/types/map_test.go new file mode 100644 index 000000000000..1dba9504b30c --- /dev/null +++ b/pkg/iac/types/map_test.go @@ -0,0 +1,25 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_MapJSON(t *testing.T) { + val := Map(map[string]string{ + "yeah": "it", + "seems": "to", + "work": "fine", + }, NewMetadata(NewRange("main.tf", 123, 123, "", nil), "")) + data, err := json.Marshal(val) + require.NoError(t, err) + + var restored MapValue + err = json.Unmarshal(data, &restored) + require.NoError(t, err) + + assert.Equal(t, val, restored) +} diff --git a/pkg/iac/types/metadata.go b/pkg/iac/types/metadata.go new file mode 100755 index 000000000000..a7b6c0dc03b3 --- /dev/null +++ b/pkg/iac/types/metadata.go @@ -0,0 +1,215 @@ +package types + +import ( + "encoding/json" + "fmt" + "strings" +) + +type Metadata struct { + rnge Range + ref string + isManaged bool + isDefault bool + isExplicit bool + isUnresolvable bool + parent *Metadata + internal any +} + +func (m Metadata) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "range": m.rnge, + "ref": m.ref, + "managed": m.isManaged, + "default": m.isDefault, + "explicit": m.isExplicit, + "unresolvable": m.isUnresolvable, + "parent": m.parent, + }) +} + +func (m *Metadata) UnmarshalJSON(data []byte) error { + var keys map[string]any + if err := json.Unmarshal(data, &keys); err != nil { + return err + } + if keys["range"] != nil { + raw, err := json.Marshal(keys["range"]) + if err != nil { + return err + } + var r Range + if err := json.Unmarshal(raw, &r); err != nil { + return err + } + m.rnge = r + } + if keys["ref"] != nil { + m.ref = keys["ref"].(string) + } + if keys["managed"] != nil { + m.isManaged = keys["managed"].(bool) + } + if keys["default"] != nil { + m.isDefault = keys["default"].(bool) + } + if keys["explicit"] != nil { + m.isExplicit = keys["explicit"].(bool) + } + if keys["unresolvable"] != nil { + m.isUnresolvable = keys["unresolvable"].(bool) + } + if keys["parent"] != nil { + if _, ok := keys["parent"].(map[string]any); ok { + raw, err := json.Marshal(keys["parent"]) + if err != nil { + return err + } + var parent Metadata + if err := json.Unmarshal(raw, &parent); err != nil { + return err + } + m.parent = &parent + } + } + return nil +} + +func (m *Metadata) ToRego() any { + input := map[string]any{ + "filepath": m.Range().GetLocalFilename(), + "startline": m.Range().GetStartLine(), + "endline": m.Range().GetEndLine(), + "sourceprefix": m.Range().GetSourcePrefix(), + "managed": m.isManaged, + "explicit": m.isExplicit, + "unresolvable": m.isUnresolvable, + "fskey": CreateFSKey(m.Range().GetFS()), + "resource": m.Reference(), + } + if m.parent != nil { + input["parent"] = m.parent.ToRego() + } + return input +} + +func NewMetadata(r Range, ref string) Metadata { + return Metadata{ + rnge: r, + ref: ref, + isManaged: true, + } +} + +func NewUnresolvableMetadata(r Range, ref string) Metadata { + unres := NewMetadata(r, ref) + unres.isUnresolvable = true + return unres +} + +func NewExplicitMetadata(r Range, ref string) Metadata { + m := NewMetadata(r, ref) + m.isExplicit = true + return m +} + +func (m Metadata) WithParent(p Metadata) Metadata { + m.parent = &p + return m +} + +func (m *Metadata) SetParentPtr(p *Metadata) { + m.parent = p +} + +func (m Metadata) Parent() *Metadata { + return m.parent +} + +func (m Metadata) Root() Metadata { + meta := &m + for meta.Parent() != nil { + meta = meta.Parent() + } + return *meta +} + +func (m Metadata) WithInternal(internal any) Metadata { + m.internal = internal + return m +} + +func (m Metadata) Internal() any { + return m.internal +} + +func (m Metadata) IsMultiLine() bool { + return m.rnge.GetStartLine() < m.rnge.GetEndLine() +} + +func NewUnmanagedMetadata() Metadata { + m := NewMetadata(NewRange("", 0, 0, "", nil), "") + m.isManaged = false + return m +} + +func NewTestMetadata() Metadata { + return NewMetadata(NewRange("test.test", 123, 123, "", nil), "") +} + +func NewApiMetadata(provider string, parts ...string) Metadata { + return NewMetadata(NewRange(fmt.Sprintf("/%s/%s", provider, strings.Join(parts, "/")), 0, 0, "", nil), "") +} + +func NewRemoteMetadata(id string) Metadata { + return NewMetadata(NewRange(id, 0, 0, "remote", nil), id) +} + +func (m Metadata) IsDefault() bool { + return m.isDefault +} + +func (m Metadata) IsResolvable() bool { + return !m.isUnresolvable +} + +func (m Metadata) IsExplicit() bool { + return m.isExplicit +} + +func (m Metadata) String() string { + return m.ref +} + +func (m Metadata) Reference() string { + return m.ref +} + +func (m Metadata) Range() Range { + return m.rnge +} + +func (m Metadata) IsManaged() bool { + return m.isManaged +} + +func (m Metadata) IsUnmanaged() bool { + return !m.isManaged +} + +func (m Metadata) GetMetadata() Metadata { + return m +} + +func (m Metadata) GetRawValue() any { + return nil +} + +func (m *Metadata) SetReference(ref string) { + m.ref = ref +} + +func (m *Metadata) SetRange(r Range) { + m.rnge = r +} diff --git a/pkg/iac/types/metadata_test.go b/pkg/iac/types/metadata_test.go new file mode 100644 index 000000000000..f7d46bbf9dbd --- /dev/null +++ b/pkg/iac/types/metadata_test.go @@ -0,0 +1,38 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_MetadataToRego(t *testing.T) { + m1 := NewTestMetadata() + m1.isUnresolvable = true + expected := map[string]any{ + "endline": 123, + "explicit": false, + "filepath": "test.test", + "fskey": "", + "managed": true, + "unresolvable": true, + "resource": "", + "sourceprefix": "", + "startline": 123, + } + assert.Equal(t, expected, m1.ToRego()) + m2 := NewTestMetadata() + m1.SetParentPtr(&m2) + expected["parent"] = map[string]any{ + "endline": 123, + "explicit": false, + "filepath": "test.test", + "fskey": "", + "managed": true, + "unresolvable": false, + "resource": "", + "sourceprefix": "", + "startline": 123, + } + assert.Equal(t, expected, m1.ToRego()) +} diff --git a/pkg/iac/types/range.go b/pkg/iac/types/range.go new file mode 100755 index 000000000000..a013e3f8b2e5 --- /dev/null +++ b/pkg/iac/types/range.go @@ -0,0 +1,151 @@ +package types + +import ( + "encoding/json" + "fmt" + "io/fs" + "path" +) + +func NewRange(filename string, startLine, endLine int, sourcePrefix string, srcFS fs.FS) Range { + return newRange(filename, startLine, endLine, sourcePrefix, CreateFSKey(srcFS), srcFS, false) +} + +func NewRangeWithLogicalSource(filename string, startLine, endLine int, sourcePrefix string, srcFS fs.FS) Range { + return newRange(filename, startLine, endLine, sourcePrefix, CreateFSKey(srcFS), srcFS, true) +} + +func NewRangeWithFSKey(filename string, startLine, endLine int, sourcePrefix, fsKey string, fsys fs.FS) Range { + return newRange(filename, startLine, endLine, sourcePrefix, fsKey, fsys, false) +} + +func newRange(filename string, startLine, endLine int, sourcePrefix, fsKey string, fsys fs.FS, isLogical bool) Range { + return Range{ + filename: filename, + startLine: startLine, + endLine: endLine, + fs: fsys, + fsKey: fsKey, + sourcePrefix: sourcePrefix, + isLogicalSource: isLogical, + } +} + +type Range struct { + filename string + startLine int + endLine int + sourcePrefix string + isLogicalSource bool + fs fs.FS + fsKey string +} + +func (r Range) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]any{ + "filename": r.filename, + "startLine": r.startLine, + "endLine": r.endLine, + "sourcePrefix": r.sourcePrefix, + "fsKey": r.fsKey, + "isLogicalSource": r.isLogicalSource, + }) +} + +func (r *Range) UnmarshalJSON(data []byte) error { + var keys map[string]any + if err := json.Unmarshal(data, &keys); err != nil { + return err + } + if keys["filename"] != nil { + r.filename = keys["filename"].(string) + } + if keys["startLine"] != nil { + r.startLine = int(keys["startLine"].(float64)) + } + if keys["endLine"] != nil { + r.endLine = int(keys["endLine"].(float64)) + } + if keys["sourcePrefix"] != nil { + r.sourcePrefix = keys["sourcePrefix"].(string) + } + if keys["fsKey"] != nil { + r.fsKey = keys["fsKey"].(string) + } + if keys["isLogicalSource"] != nil { + r.isLogicalSource = keys["isLogicalSource"].(bool) + } + return nil +} + +func (r Range) GetFSKey() string { + return r.fsKey +} + +func (r Range) LineCount() int { + if r.endLine == 0 { + return 0 + } + return (r.endLine - r.startLine) + 1 +} + +func (r Range) GetFilename() string { + if r.sourcePrefix == "" { + return r.filename + } + if r.isLogicalSource { + return fmt.Sprintf("%s:%s", r.sourcePrefix, r.filename) + } + return path.Join(r.sourcePrefix, r.filename) +} + +func (r Range) GetLocalFilename() string { + return r.filename +} + +func (r Range) GetStartLine() int { + return r.startLine +} + +func (r Range) GetEndLine() int { + return r.endLine +} + +func (r Range) IsMultiLine() bool { + return r.startLine < r.endLine +} + +func (r Range) String() string { + if r.startLine != r.endLine { + return fmt.Sprintf("%s:%d-%d", r.GetFilename(), r.startLine, r.endLine) + } + if r.startLine == 0 && r.endLine == 0 { + return r.GetFilename() + } + return fmt.Sprintf("%s:%d", r.GetFilename(), r.startLine) +} + +func (r Range) GetFS() fs.FS { + return r.fs +} + +func (r Range) GetSourcePrefix() string { + return r.sourcePrefix +} + +func (r Range) Validate() error { + if r.startLine < 0 || r.endLine < 0 || r.startLine > r.endLine { + return fmt.Errorf("invalid range: %s", r.String()) + } + return nil +} + +// Includes returns true if 'other' is strictly inside 'r'. +func (r Range) Includes(other Range) bool { + return r.startLine < other.startLine && r.endLine > other.endLine +} + +// Covers returns true if 'r' fully contains 'other'. +func (r Range) Covers(other Range) bool { + return r.startLine <= other.startLine && r.endLine >= other.endLine +} diff --git a/pkg/iac/types/rules/rule.go b/pkg/iac/types/rules/rule.go new file mode 100644 index 000000000000..b86a4ad6d11f --- /dev/null +++ b/pkg/iac/types/rules/rule.go @@ -0,0 +1,18 @@ +package rules + +import ( + "github.com/aquasecurity/trivy/pkg/iac/scan" +) + +type RegisteredRule struct { + scan.Rule + Number int +} + +func (r *RegisteredRule) GetRule() scan.Rule { + return r.Rule +} + +func (r *RegisteredRule) AddLink(link string) { + r.Rule.Links = append([]string{link}, r.Rule.Links...) +} diff --git a/pkg/iac/types/sources.go b/pkg/iac/types/sources.go new file mode 100644 index 000000000000..6b7a612ec200 --- /dev/null +++ b/pkg/iac/types/sources.go @@ -0,0 +1,17 @@ +package types + +type Source string + +const ( + SourceDockerfile Source = "dockerfile" + SourceKubernetes Source = "kubernetes" + // Deprecated: use "kubernetes" instead + SourceRbac Source = "rbac" + // Deprecated: use "cloud" instead + SourceDefsec Source = "defsec" + SourceCloud Source = "cloud" + SourceYAML Source = "yaml" + SourceJSON Source = "json" + SourceTOML Source = "toml" + SourceTerraformRaw Source = "terraform-raw" +) diff --git a/pkg/iac/types/string.go b/pkg/iac/types/string.go new file mode 100755 index 000000000000..336f9674c2cc --- /dev/null +++ b/pkg/iac/types/string.go @@ -0,0 +1,98 @@ +package types + +import ( + "slices" + "strings" +) + +type StringValue struct { + BaseValue[string] +} + +func String(value string, m Metadata) StringValue { + return StringValue{newValue(value, m)} +} + +func StringDefault(value string, m Metadata) StringValue { + return StringValue{defaultValue(value, m)} +} + +func StringUnresolvable(m Metadata) StringValue { + return StringValue{unresolvableValue[string](m)} +} + +func StringExplicit(value string, m Metadata) StringValue { + return StringValue{explicitValue(value, m)} +} + +func StringTest(value string) StringValue { + return String(value, NewTestMetadata()) +} + +func (s StringValue) IsOneOf(values ...string) bool { + if s.metadata.isUnresolvable { + return false + } + return slices.Contains(values, s.value) +} + +func (s StringValue) IsEmpty() bool { + if s.metadata.isUnresolvable { + return false + } + return s.value == "" +} + +func (s StringValue) IsNotEmpty() bool { + if s.metadata.isUnresolvable { + return false + } + return s.value != "" +} + +func (s StringValue) EqualTo(value string) bool { + if s.metadata.isUnresolvable { + return false + } + + return s.value == value +} + +func (s StringValue) NotEqualTo(value string) bool { + if s.metadata.isUnresolvable { + return false + } + + return s.value != value +} + +func (s StringValue) StartsWith(prefix string) bool { + if s.metadata.isUnresolvable { + return false + } + + return strings.HasPrefix(s.value, prefix) +} + +func (s StringValue) EndsWith(suffix string) bool { + if s.metadata.isUnresolvable { + return false + } + return strings.HasSuffix(s.value, suffix) +} + +func (s StringValue) Contains(value string) bool { + if s.metadata.isUnresolvable { + return false + } + return strings.Contains(s.value, value) +} + +type StringValueList []StringValue + +func (l StringValueList) AsStrings() (output []string) { + for _, item := range l { + output = append(output, item.Value()) + } + return output +} diff --git a/pkg/iac/types/string_test.go b/pkg/iac/types/string_test.go new file mode 100644 index 000000000000..39f50e00299e --- /dev/null +++ b/pkg/iac/types/string_test.go @@ -0,0 +1,48 @@ +package types + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_StringValueStartsWith(t *testing.T) { + testCases := []struct { + desc string + input string + prefix string + expected bool + }{ + { + desc: "return true when starts with", + input: "something", + prefix: "some", + expected: true, + }, + { + desc: "return false when does not start with", + input: "something", + prefix: "nothing", + expected: false, + }, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + assert.Equal(t, tc.expected, String(tc.input, fakeMetadata).StartsWith(tc.prefix)) + }) + } +} + +func Test_StringJSON(t *testing.T) { + val := String("hello world", NewMetadata(NewRange("main.tf", 123, 123, "", nil), "")) + data, err := json.Marshal(val) + require.NoError(t, err) + + var restored StringValue + err = json.Unmarshal(data, &restored) + require.NoError(t, err) + + assert.Equal(t, val, restored) +} diff --git a/pkg/iac/types/time.go b/pkg/iac/types/time.go new file mode 100755 index 000000000000..351408484d65 --- /dev/null +++ b/pkg/iac/types/time.go @@ -0,0 +1,92 @@ +package types + +import ( + "encoding/json" + "time" +) + +type TimeValue struct { + BaseValue[RFC3339Time] +} + +type RFC3339Time struct { + time.Time +} + +func (t RFC3339Time) MarshalJSON() ([]byte, error) { + return json.Marshal(t.Format(time.RFC3339)) +} + +func (t *RFC3339Time) UnmarshalJSON(data []byte) error { + if string(data) == "null" { + t.Time = time.Time{} + return nil + } + + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + ti, err := time.Parse(time.RFC3339, s) + if err != nil { + return err + } + t.Time = ti + return nil +} + +func Time(value time.Time, m Metadata) TimeValue { + return TimeValue{newValue(RFC3339Time{value}, m)} +} + +func TimeDefault(value time.Time, m Metadata) TimeValue { + return TimeValue{defaultValue(RFC3339Time{value}, m)} +} + +func TimeExplicit(value time.Time, m Metadata) TimeValue { + return TimeValue{explicitValue(RFC3339Time{value}, m)} +} + +func TimeUnresolvable(m Metadata) TimeValue { + return TimeValue{unresolvableValue[RFC3339Time](m)} +} + +func TimeTest(value time.Time) TimeValue { + return TimeValue{testValue(RFC3339Time{value})} +} + +func (t TimeValue) Value() time.Time { + return t.value.Time +} + +func (t TimeValue) GetRawValue() any { + return t.value +} + +func (t TimeValue) ToRego() any { + m := t.metadata.ToRego().(map[string]any) + m["value"] = t.value.Format(time.RFC3339) + return m +} + +func (t TimeValue) IsNever() bool { + if t.GetMetadata().isUnresolvable { + return false + } + return t.value.IsZero() +} + +func (t TimeValue) Before(i time.Time) bool { + if t.metadata.isUnresolvable { + return false + } + return t.value.Before(i) +} + +func (t TimeValue) After(i time.Time) bool { + if t.metadata.isUnresolvable { + return false + } + return t.value.After(i) +} diff --git a/pkg/iac/types/time_test.go b/pkg/iac/types/time_test.go new file mode 100644 index 000000000000..5d38b0dfb570 --- /dev/null +++ b/pkg/iac/types/time_test.go @@ -0,0 +1,23 @@ +package types + +import ( + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_TimeJSON(t *testing.T) { + val := Time(time.Now(), NewMetadata(NewRange("main.tf", 123, 123, "", nil), "")) + data, err := json.Marshal(val) + require.NoError(t, err) + + var restored TimeValue + err = json.Unmarshal(data, &restored) + require.NoError(t, err) + + assert.Equal(t, val.value.Format(time.RFC3339), restored.Value().Format(time.RFC3339)) + assert.Equal(t, val.metadata, restored.metadata) +} diff --git a/pkg/javadb/client.go b/pkg/javadb/client.go new file mode 100644 index 000000000000..76ad94c8688d --- /dev/null +++ b/pkg/javadb/client.go @@ -0,0 +1,223 @@ +package javadb + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sort" + "sync" + "time" + + "github.com/google/go-containerregistry/pkg/name" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-java-db/pkg/db" + "github.com/aquasecurity/trivy-java-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/dependency/parser/java/jar" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/oci" +) + +const ( + SchemaVersion = db.SchemaVersion + mediaType = "application/vnd.aquasec.trivy.javadb.layer.v1.tar+gzip" +) + +var ( + // GitHub Container Registry + DefaultGHCRRepository = fmt.Sprintf("%s:%d", "ghcr.io/aquasecurity/trivy-java-db", SchemaVersion) + + // GCR mirrors + DefaultGCRRepository = fmt.Sprintf("%s:%d", "mirror.gcr.io/aquasec/trivy-java-db", SchemaVersion) +) + +var updater *Updater + +type Updater struct { + repos []name.Reference + dbDir string + skip bool + quiet bool + registryOption ftypes.RegistryOptions + once sync.Once // we need to update java-db once per run +} + +func (u *Updater) Update() error { + ctx := log.WithContextPrefix(context.Background(), log.PrefixJavaDB) + metac := db.NewMetadata(u.dbDir) + + meta, err := metac.Get() + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + return xerrors.Errorf("Java DB metadata error: %w", err) + } else if u.skip { + log.ErrorContext(ctx, "The first run cannot skip downloading Java DB") + return xerrors.New("'--skip-java-db-update' cannot be specified on the first run") + } + } + + if (meta.Version != SchemaVersion || !u.isNewDB(ctx, meta)) && !u.skip { + // Download DB + // TODO: support remote options + if err := u.downloadDB(ctx); err != nil { + return xerrors.Errorf("OCI artifact error: %w", err) + } + + // Parse the newly downloaded metadata.json + meta, err = metac.Get() + if err != nil { + return xerrors.Errorf("Java DB metadata error: %w", err) + } + + // Update DownloadedAt + meta.DownloadedAt = time.Now().UTC() + if err = metac.Update(meta); err != nil { + return xerrors.Errorf("Java DB metadata update error: %w", err) + } + log.InfoContext(ctx, "Java DB is cached for 3 days. If you want to update the database more frequently, "+ + `"trivy clean --java-db" command clears the DB cache.`) + } + + return nil +} + +func (u *Updater) isNewDB(ctx context.Context, meta db.Metadata) bool { + now := time.Now().UTC() + if now.Before(meta.NextUpdate) { + log.DebugContext(ctx, "Java DB update was skipped because the local Java DB is the latest") + return true + } + + if now.Before(meta.DownloadedAt.Add(time.Hour * 24)) { // 1 day + log.DebugContext(ctx, "Java DB update was skipped because the local Java DB was downloaded during the last day") + return true + } + return false +} + +func (u *Updater) downloadDB(ctx context.Context) error { + log.InfoContext(ctx, "Downloading Java DB...") + + artifacts := oci.NewArtifacts(u.repos, u.registryOption) + downloadOpt := oci.DownloadOption{ + MediaType: mediaType, + Quiet: u.quiet, + } + if err := artifacts.Download(ctx, u.dbDir, downloadOpt); err != nil { + return xerrors.Errorf("failed to download Java DB: %w", err) + } + + return nil +} + +func Init(cacheDir string, javaDBRepositories []name.Reference, skip, quiet bool, registryOption ftypes.RegistryOptions) { + updater = &Updater{ + repos: javaDBRepositories, + dbDir: dbDir(cacheDir), + skip: skip, + quiet: quiet, + registryOption: registryOption, + } +} + +func Update() error { + if updater == nil { + return xerrors.New("Java DB client not initialized") + } + + var err error + updater.once.Do(func() { + err = updater.Update() + }) + return err +} + +func Clear(_ context.Context, cacheDir string) error { + return os.RemoveAll(dbDir(cacheDir)) +} + +func dbDir(cacheDir string) string { + return filepath.Join(cacheDir, "java-db") +} + +type DB struct { + driver db.DB +} + +func NewClient() (*DB, error) { + if err := Update(); err != nil { + return nil, xerrors.Errorf("Java DB update failed: %s", err) + } + + dbc, err := db.New(updater.dbDir) + if err != nil { + return nil, xerrors.Errorf("Java DB open error: %w", err) + } + + return &DB{driver: dbc}, nil +} + +func (d *DB) Exists(groupID, artifactID string) (bool, error) { + index, err := d.driver.SelectIndexByArtifactIDAndGroupID(artifactID, groupID) + if err != nil { + return false, err + } + return index.ArtifactID != "", nil +} + +func (d *DB) SearchBySHA1(sha1 string) (jar.Properties, error) { + index, err := d.driver.SelectIndexBySha1(sha1) + if err != nil { + return jar.Properties{}, xerrors.Errorf("select error: %w", err) + } else if index.ArtifactID == "" { + return jar.Properties{}, xerrors.Errorf("digest %s: %w", sha1, jar.ArtifactNotFoundErr) + } + return jar.Properties{ + GroupID: index.GroupID, + ArtifactID: index.ArtifactID, + Version: index.Version, + }, nil +} + +func (d *DB) SearchByArtifactID(artifactID, version string) (string, error) { + indexes, err := d.driver.SelectIndexesByArtifactIDAndFileType(artifactID, version, types.JarType) + if err != nil { + return "", xerrors.Errorf("select error: %w", err) + } else if len(indexes) == 0 { + return "", xerrors.Errorf("artifactID %s: %w", artifactID, jar.ArtifactNotFoundErr) + } + sort.Slice(indexes, func(i, j int) bool { + return indexes[i].GroupID < indexes[j].GroupID + }) + + // Some artifacts might have the same artifactId. + // e.g. "javax.servlet:jstl" and "jstl:jstl" + groupIDs := make(map[string]int) + for _, index := range indexes { + if i, ok := groupIDs[index.GroupID]; ok { + groupIDs[index.GroupID] = i + 1 + continue + } + groupIDs[index.GroupID] = 1 + } + maxCount := 0 + var groupID string + for k, v := range groupIDs { + if v > maxCount { + maxCount = v + groupID = k + } + } + + return groupID, nil +} + +func (d *DB) Close() error { + if d == nil { + return nil + } + return d.driver.Close() +} diff --git a/pkg/k8s/commands/cluster.go b/pkg/k8s/commands/cluster.go new file mode 100644 index 000000000000..268a4ef47e14 --- /dev/null +++ b/pkg/k8s/commands/cluster.go @@ -0,0 +1,103 @@ +package commands + +import ( + "context" + + "golang.org/x/xerrors" + + trivy_checks "github.com/aquasecurity/trivy-checks" + k8sArtifacts "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" + "github.com/aquasecurity/trivy-kubernetes/pkg/k8s" + "github.com/aquasecurity/trivy-kubernetes/pkg/trivyk8s" + "github.com/aquasecurity/trivy/pkg/commands/operation" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/policy" + "github.com/aquasecurity/trivy/pkg/types" +) + +// clusterRun runs scan on kubernetes cluster +func clusterRun(ctx context.Context, opts flag.Options, cluster k8s.Cluster) error { + if err := validateReportArguments(opts); err != nil { + return err + } + var artifacts []*k8sArtifacts.Artifact + var err error + switch opts.Format { + case types.FormatCycloneDX: + artifacts, err = trivyk8s.New(cluster).ListClusterBomInfo(ctx) + if err != nil { + return xerrors.Errorf("get k8s artifacts with node info error: %w", err) + } + case types.FormatJSON, types.FormatTable: + k8sOpts := []trivyk8s.K8sOption{ + trivyk8s.WithExcludeNamespaces(opts.ExcludeNamespaces), + trivyk8s.WithIncludeNamespaces(opts.IncludeNamespaces), + trivyk8s.WithExcludeKinds(opts.ExcludeKinds), + trivyk8s.WithIncludeKinds(opts.IncludeKinds), + trivyk8s.WithExcludeOwned(opts.ExcludeOwned), + } + if opts.Scanners.AnyEnabled(types.MisconfigScanner) && !opts.DisableNodeCollector { + artifacts, err = trivyk8s.New(cluster, k8sOpts...).ListArtifactAndNodeInfo(ctx, nodeCollectorOptions(ctx, opts)...) + if err != nil { + return xerrors.Errorf("get k8s artifacts with node info error: %w", err) + } + } else { + artifacts, err = trivyk8s.New(cluster, k8sOpts...).ListArtifacts(ctx) + if err != nil { + return xerrors.Errorf("get k8s artifacts error: %w", err) + } + } + default: + return xerrors.Errorf(`unknown format %q. Use "json" or "table" or "cyclonedx"`, opts.Format) + } + + if !opts.DisableNodeCollector && !opts.Quiet { + log.InfoContext(ctx, "Node scanning is enabled") + log.InfoContext(ctx, "If you want to disable Node scanning via an in-cluster Job, please try '--disable-node-collector' to disable the Node-Collector job.") + } + runner := newRunner(opts, cluster.GetCurrentContext()) + return runner.run(ctx, artifacts) +} + +func nodeCollectorOptions(ctx context.Context, opts flag.Options) []trivyk8s.NodeCollectorOption { + nodeCollectorOptions := []trivyk8s.NodeCollectorOption{ + trivyk8s.WithScanJobNamespace(opts.NodeCollectorNamespace), + trivyk8s.WithIgnoreLabels(opts.ExcludeNodes), + trivyk8s.WithScanJobImageRef(opts.NodeCollectorImageRef), + trivyk8s.WithTolerations(opts.Tolerations), + } + + ctx = log.WithContextPrefix(ctx, log.PrefixMisconfiguration) + c, _ := policy.NewClient(opts.CacheDir, opts.Quiet, opts.MisconfOptions.ChecksBundleRepository) + contentPath, err := operation.InitBuiltinChecks(ctx, c, opts.SkipCheckUpdate, opts.RegistryOpts()) + if err != nil { + log.Error("Falling back to embedded checks", log.Err(err)) + nodeCollectorOptions = append(nodeCollectorOptions, + []trivyk8s.NodeCollectorOption{ + trivyk8s.WithEmbeddedCommandFileSystem(trivy_checks.EmbeddedK8sCommandsFileSystem), + trivyk8s.WithEmbeddedNodeConfigFilesystem(trivy_checks.EmbeddedConfigCommandsFileSystem), + }...) + } + + complianceCommandsIDs := getComplianceCommands(opts) + nodeCollectorOptions = append(nodeCollectorOptions, []trivyk8s.NodeCollectorOption{ + trivyk8s.WithCommandPaths([]string{contentPath}), + trivyk8s.WithSpecCommandIds(complianceCommandsIDs), + }...) + return nodeCollectorOptions +} + +func getComplianceCommands(opts flag.Options) []string { + var commands []string + if opts.Compliance.Spec.ID != "" { + for _, control := range opts.Compliance.Spec.Controls { + for _, command := range control.Commands { + if command.ID != "" { + commands = append(commands, command.ID) + } + } + } + } + return commands +} diff --git a/pkg/k8s/commands/run.go b/pkg/k8s/commands/run.go new file mode 100644 index 000000000000..15f2e6841971 --- /dev/null +++ b/pkg/k8s/commands/run.go @@ -0,0 +1,143 @@ +package commands + +import ( + "context" + "errors" + + "github.com/spf13/viper" + "golang.org/x/xerrors" + + k8sArtifacts "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" + "github.com/aquasecurity/trivy-kubernetes/pkg/k8s" + cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" + "github.com/aquasecurity/trivy/pkg/commands/operation" + cr "github.com/aquasecurity/trivy/pkg/compliance/report" + "github.com/aquasecurity/trivy/pkg/flag" + k8sRep "github.com/aquasecurity/trivy/pkg/k8s" + "github.com/aquasecurity/trivy/pkg/k8s/report" + "github.com/aquasecurity/trivy/pkg/k8s/scanner" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Run runs a k8s scan +func Run(ctx context.Context, args []string, opts flag.Options) error { + clusterOptions := []k8s.ClusterOption{ + k8s.WithKubeConfig(opts.K8sOptions.KubeConfig), + k8s.WithBurst(opts.K8sOptions.Burst), + k8s.WithQPS(opts.K8sOptions.QPS), + } + if len(args) > 0 { + clusterOptions = append(clusterOptions, k8s.WithContext(args[0])) + } + cluster, err := k8s.GetCluster(clusterOptions...) + if err != nil { + return xerrors.Errorf("failed getting k8s cluster: %w", err) + } + ctx, cancel := context.WithTimeout(ctx, opts.Timeout) + defer cancel() + opts.K8sVersion = cluster.GetClusterVersion() + return clusterRun(ctx, opts, cluster) +} + +type runner struct { + flagOpts flag.Options + cluster string +} + +func newRunner(flagOpts flag.Options, cluster string) *runner { + return &runner{ + flagOpts, + cluster, + } +} + +func (r *runner) run(ctx context.Context, artifacts []*k8sArtifacts.Artifact) error { + runner, err := cmd.NewRunner(ctx, r.flagOpts, cmd.TargetK8s) + if err != nil { + if errors.Is(err, cmd.SkipScan) { + return nil + } + return xerrors.Errorf("init error: %w", err) + } + defer func() { + if err := runner.Close(ctx); err != nil { + log.ErrorContext(ctx, "failed to close runner: %s", err) + } + }() + + s := scanner.NewScanner(r.cluster, runner, r.flagOpts) + + // set scanners types by spec + if r.flagOpts.Compliance.Spec.ID != "" { + scanners, err := r.flagOpts.Compliance.Scanners() + if err != nil { + return xerrors.Errorf("scanner error: %w", err) + } + r.flagOpts.ScanOptions.Scanners = scanners + } + var rpt report.Report + log.Info("Scanning K8s...", log.String("K8s", r.cluster)) + rpt, err = s.Scan(ctx, artifacts) + if err != nil { + return xerrors.Errorf("k8s scan error: %w", err) + } + + output, cleanup, err := r.flagOpts.OutputWriter(ctx) + if err != nil { + return xerrors.Errorf("failed to create output file: %w", err) + } + defer cleanup() + + if r.flagOpts.Compliance.Spec.ID != "" { + var scanResults []types.Results + for _, rss := range rpt.Resources { + scanResults = append(scanResults, rss.Results) + } + complianceReport, err := cr.BuildComplianceReport(scanResults, r.flagOpts.Compliance) + if err != nil { + return xerrors.Errorf("compliance report build error: %w", err) + } + return cr.Write(ctx, complianceReport, cr.Option{ + Format: r.flagOpts.Format, + Report: r.flagOpts.ReportFormat, + Output: output, + }) + } + + if err := k8sRep.Write(ctx, rpt, report.Option{ + Format: r.flagOpts.Format, + Report: r.flagOpts.ReportFormat, + Output: output, + Severities: r.flagOpts.Severities, + Scanners: r.flagOpts.ScanOptions.Scanners, + APIVersion: r.flagOpts.AppVersion, + }); err != nil { + return xerrors.Errorf("unable to write results: %w", err) + } + + return operation.Exit(r.flagOpts, rpt.Failed(), types.Metadata{}) +} + +// Full-cluster scanning with '--format table' without explicit '--report all' is not allowed so that it won't mess up user's terminal. +// To show all the results, user needs to specify "--report all" explicitly +// even though the default value of "--report" is "all". +// +// e.g. +// $ trivy k8s --report all +// +// Or they can use "--format json" with implicit "--report all". +// +// e.g. $ trivy k8s --format json // All the results are shown in JSON +func validateReportArguments(opts flag.Options) error { + if opts.ReportFormat == "all" && + !viper.IsSet("report") && + opts.Format == "table" { + + m := "All the results in the table format can mess up your terminal. Use \"--report all\" to tell Trivy to output it to your terminal anyway, or consider \"--report summary\" to show the summary output." + + return xerrors.New(m) + } + + return nil +} diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go new file mode 100644 index 000000000000..acc3e35c378a --- /dev/null +++ b/pkg/k8s/k8s.go @@ -0,0 +1,47 @@ +package k8s + +import ( + "context" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/scan/langpkg" + "github.com/aquasecurity/trivy/pkg/scan/local" + "github.com/aquasecurity/trivy/pkg/scan/ospkg" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vulnerability" +) + +// ScanKubernetes implements the scanner +type ScanKubernetes struct { + localScanner local.Service +} + +// NewScanKubernetes is the factory method for scanner +func NewScanKubernetes(s local.Service) *ScanKubernetes { + return &ScanKubernetes{localScanner: s} +} + +// NewKubernetesScanner is the factory method for scanner +func NewKubernetesScanner() *ScanKubernetes { + return initializeScanK8s(nil) +} + +// initializeScanK8s creates a new Kubernetes scanner with the provided cache. +// If cache is nil, it will create the scanner without cache dependency. +func initializeScanK8s(localArtifactCache cache.LocalArtifactCache) *ScanKubernetes { + applier := applier.NewApplier(localArtifactCache) + osScanner := ospkg.NewScanner() + langScanner := langpkg.NewScanner() + vulnClient := vulnerability.NewClient(db.Config{}) + + localService := local.NewService(applier, osScanner, langScanner, vulnClient) + return NewScanKubernetes(localService) +} + +// Scan scans k8s core components and return it findings +func (sk ScanKubernetes) Scan(ctx context.Context, target types.ScanTarget, options types.ScanOptions) (types.Results, ftypes.OS, error) { + return sk.localScanner.ScanTarget(ctx, target, options) +} diff --git a/pkg/k8s/report/cyclonedx.go b/pkg/k8s/report/cyclonedx.go new file mode 100644 index 000000000000..1d87769c29b2 --- /dev/null +++ b/pkg/k8s/report/cyclonedx.go @@ -0,0 +1,37 @@ +package report + +import ( + "context" + "io" + + cdx "github.com/CycloneDX/cyclonedx-go" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" +) + +// CycloneDXWriter implements types.Writer +type CycloneDXWriter struct { + encoder cdx.BOMEncoder + marshaler cyclonedx.Marshaler +} + +// NewCycloneDXWriter constract new CycloneDXWriter +func NewCycloneDXWriter(output io.Writer, format cdx.BOMFileFormat, appVersion string) CycloneDXWriter { + encoder := cdx.NewBOMEncoder(output, format) + encoder.SetPretty(true) + encoder.SetEscapeHTML(false) + return CycloneDXWriter{ + encoder: encoder, + marshaler: cyclonedx.NewMarshaler(appVersion), + } +} + +func (w CycloneDXWriter) Write(ctx context.Context, component *core.BOM) error { + bom, err := w.marshaler.Marshal(ctx, component) + if err != nil { + return xerrors.Errorf("CycloneDX marshal error: %w", err) + } + return w.encoder.Encode(bom) +} diff --git a/pkg/k8s/report/json.go b/pkg/k8s/report/json.go new file mode 100644 index 000000000000..12420c0704d5 --- /dev/null +++ b/pkg/k8s/report/json.go @@ -0,0 +1,40 @@ +package report + +import ( + "encoding/json" + "fmt" + "io" + + "golang.org/x/xerrors" +) + +type JSONWriter struct { + Output io.Writer + Report string +} + +// Write writes the results in JSON format +func (jw JSONWriter) Write(report Report) error { + var output []byte + var err error + + switch jw.Report { + case AllReport: + output, err = json.MarshalIndent(report, "", " ") + if err != nil { + return xerrors.Errorf("failed to write json: %w", err) + } + case SummaryReport: + output, err = json.MarshalIndent(report.consolidate(), "", " ") + if err != nil { + return xerrors.Errorf("failed to write json: %w", err) + } + default: + return xerrors.Errorf(`report %q not supported. Use "summary" or "all"`, jw.Report) + } + if _, err = fmt.Fprintln(jw.Output, string(output)); err != nil { + return xerrors.Errorf("failed to write json: %w", err) + } + + return nil +} diff --git a/pkg/k8s/report/report.go b/pkg/k8s/report/report.go new file mode 100644 index 000000000000..1f1c1ec50d93 --- /dev/null +++ b/pkg/k8s/report/report.go @@ -0,0 +1,300 @@ +package report + +import ( + "errors" + "fmt" + "io" + "slices" + "strings" + + "github.com/samber/lo" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + AllReport = "all" + SummaryReport = "summary" + + workloadComponent = "workload" + infraComponent = "infra" + infraNamespace = "kube-system" +) + +type Option struct { + Format types.Format + Report string + Output io.Writer + Severities []dbTypes.Severity + ColumnHeading []string + Scanners types.Scanners + APIVersion string +} + +// Report represents a kubernetes scan report +type Report struct { + SchemaVersion int `json:",omitempty"` + ClusterName string + Resources []Resource `json:",omitempty"` + BOM *core.BOM `json:"-"` + name string +} + +// ConsolidatedReport represents a kubernetes scan report with consolidated findings +type ConsolidatedReport struct { + SchemaVersion int `json:",omitempty"` + ClusterName string + Findings []Resource `json:",omitempty"` +} + +// Resource represents a kubernetes resource report +type Resource struct { + Namespace string `json:",omitempty"` + Kind string + Name string + Metadata []types.Metadata `json:",omitempty"` + Results types.Results `json:",omitempty"` + Error string `json:",omitempty"` + + // original report + Report types.Report `json:"-"` +} + +func (r Resource) fullname() string { + return strings.ToLower(fmt.Sprintf("%s/%s/%s", r.Namespace, r.Kind, r.Name)) +} + +// Failed returns whether the k8s report includes any vulnerabilities or misconfigurations +func (r Report) Failed() bool { + for _, v := range r.Resources { + if v.Results.Failed() { + return true + } + } + return false +} + +func (r Report) consolidate() ConsolidatedReport { + consolidated := ConsolidatedReport{ + SchemaVersion: r.SchemaVersion, + ClusterName: r.ClusterName, + } + + index := make(map[string]Resource) + var vulnerabilities []Resource + for _, m := range r.Resources { + if vulnerabilitiesOrSecretResource(m) { + vulnerabilities = append(vulnerabilities, m) + } + if misconfigsResource(m) { + res, ok := index[m.fullname()] + index[m.fullname()] = m + if ok { + index[m.fullname()].Results[0].Misconfigurations = append(index[m.fullname()].Results[0].Misconfigurations, res.Results[0].Misconfigurations...) + } + } + } + + for _, v := range vulnerabilities { + key := v.fullname() + + if res, ok := index[key]; ok { + // Combine metadata + metadata := lo.UniqBy(append(res.Metadata, v.Metadata...), func(x types.Metadata) string { + return x.ImageID + }) + index[key] = Resource{ + Namespace: res.Namespace, + Kind: res.Kind, + Name: res.Name, + Metadata: metadata, + Results: append(res.Results, v.Results...), + Error: res.Error, + } + + continue + } + + index[key] = v + } + + consolidated.Findings = lo.Values(index) + + return consolidated +} + +// Writer defines the result write operation +type Writer interface { + Write(Report) error +} + +type reports struct { + Report Report + Columns []string +} + +// SeparateMisconfigReports returns 3 reports based on scanners and components flags, +// - misconfiguration report +// - rbac report +// - infra checks report +func SeparateMisconfigReports(k8sReport Report, scanners types.Scanners) []reports { + + var workloadMisconfig, infraMisconfig, rbacAssessment, workloadVulnerabilities, infraVulnerabilities, workloadResource []Resource + for _, resource := range k8sReport.Resources { + switch { + case vulnerabilitiesOrSecretResource(resource) && !infraResource(resource): + if resource.Namespace == infraNamespace || nodeInfoResource(resource) { + infraVulnerabilities = append(infraVulnerabilities, nodeKind(resource)) + } else { + workloadVulnerabilities = append(workloadVulnerabilities, resource) + } + case scanners.Enabled(types.RBACScanner) && rbacResource(resource): + rbacAssessment = append(rbacAssessment, resource) + case infraResource(resource): + infraMisconfig = append(infraMisconfig, nodeKind(resource)) + case scanners.Enabled(types.MisconfigScanner) && + !rbacResource(resource): + workloadMisconfig = append(workloadMisconfig, resource) + } + } + + var r []reports + workloadResource = append(workloadResource, workloadVulnerabilities...) + workloadResource = append(workloadResource, workloadMisconfig...) + if shouldAddToReport(scanners) { + workloadReport := Report{ + SchemaVersion: 0, + ClusterName: k8sReport.ClusterName, + Resources: workloadResource, + name: "Workload Assessment", + } + r = append(r, reports{ + Report: workloadReport, + Columns: WorkloadColumns(), + }) + + } + infraMisconfig = append(infraMisconfig, infraVulnerabilities...) + if shouldAddToReport(scanners) { + r = append(r, reports{ + Report: Report{ + SchemaVersion: 0, + ClusterName: k8sReport.ClusterName, + Resources: infraMisconfig, + name: "Infra Assessment", + }, + Columns: InfraColumns(), + }) + } + + if scanners.Enabled(types.RBACScanner) { + r = append(r, reports{ + Report: Report{ + SchemaVersion: 0, + ClusterName: k8sReport.ClusterName, + Resources: rbacAssessment, + name: "RBAC Assessment", + }, + Columns: RoleColumns(), + }) + } + + return r +} + +func rbacResource(misConfig Resource) bool { + return slices.Contains([]string{ + "Role", + "RoleBinding", + "ClusterRole", + "ClusterRoleBinding", + }, misConfig.Kind) +} + +func infraResource(misConfig Resource) bool { + return !rbacResource(misConfig) && (misConfig.Namespace == infraNamespace) || nodeInfoResource(misConfig) +} + +func CreateResource(artifact *artifacts.Artifact, report types.Report, err error) Resource { + r := createK8sResource(artifact, report.Results) + + r.Metadata = []types.Metadata{report.Metadata} + r.Report = report + // if there was any error during the scan + if err != nil { + r.Error = err.Error() + } + + return r +} + +func nodeInfoResource(nodeInfo Resource) bool { + return nodeInfo.Kind == "NodeInfo" || nodeInfo.Kind == "NodeComponents" +} + +func createK8sResource(artifact *artifacts.Artifact, scanResults types.Results) Resource { + results := make([]types.Result, 0, len(scanResults)) + // fix target name + for _, result := range scanResults { + // if resource is a kubernetes file fix the target name, + // to avoid showing the temp file that was removed. + if result.Type == ftypes.Kubernetes { + result.Target = fmt.Sprintf("%s/%s", artifact.Kind, artifact.Name) + } + results = append(results, result) + } + + r := Resource{ + Namespace: artifact.Namespace, + Kind: artifact.Kind, + Name: artifact.Name, + Metadata: []types.Metadata{}, + Results: results, + Report: types.Report{ + Results: results, + ArtifactName: artifact.Name, + }, + } + + return r +} + +func (r Report) PrintErrors() { + for _, resource := range r.Resources { + if resource.Error != "" { + log.Error("Error during vulnerabilities or misconfiguration scan", log.Err(errors.New(resource.Error))) + } + } +} + +func shouldAddToReport(scanners types.Scanners) bool { + return scanners.AnyEnabled( + types.MisconfigScanner, + types.VulnerabilityScanner, + types.SecretScanner) +} + +func vulnerabilitiesOrSecretResource(resource Resource) bool { + for _, result := range resource.Results { + if len(result.Vulnerabilities) > 0 || len(resource.Results[0].Secrets) > 0 { + return true + } + } + return false +} + +func misconfigsResource(resource Resource) bool { + return len(resource.Results) > 0 && len(resource.Results[0].Misconfigurations) > 0 +} + +func nodeKind(resource Resource) Resource { + if nodeInfoResource(resource) { + resource.Kind = "Node" + } + return resource +} diff --git a/pkg/k8s/report/report_test.go b/pkg/k8s/report/report_test.go new file mode 100644 index 000000000000..0d4cd16d3fbc --- /dev/null +++ b/pkg/k8s/report/report_test.go @@ -0,0 +1,841 @@ +package report + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + deployOrionWithMisconfigs = Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "orion", + Metadata: []types.Metadata{ + { + ImageID: "123", + RepoTags: []string{ + "alpine:3.14", + }, + RepoDigests: []string{ + "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + }, + }, + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "ID100", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "ID101", + Status: types.MisconfStatusFailure, + Severity: "MEDIUM", + }, + { + ID: "ID102", + Status: types.MisconfStatusFailure, + Severity: "HIGH", + }, + { + ID: "ID103", + Status: types.MisconfStatusFailure, + Severity: "CRITICAL", + }, + { + ID: "ID104", + Status: types.MisconfStatusFailure, + Severity: "UNKNOWN", + }, + { + ID: "ID105", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "ID106", + Status: types.MisconfStatusFailure, + Severity: "HIGH", + }, + }, + }, + }, + } + + deployOrionWithVulns = Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "orion", + Metadata: []types.Metadata{ + { + ImageID: "123", + RepoTags: []string{ + "alpine:3.14", + }, + RepoDigests: []string{ + "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + }, + }, + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-1111", + Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}, + }, + { + VulnerabilityID: "CVE-2022-2222", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + { + VulnerabilityID: "CVE-2022-3333", + Vulnerability: dbTypes.Vulnerability{Severity: "HIGH"}, + }, + { + VulnerabilityID: "CVE-2022-4444", + Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}, + }, + { + VulnerabilityID: "CVE-2022-5555", + Vulnerability: dbTypes.Vulnerability{Severity: "UNKNOWN"}, + }, + { + VulnerabilityID: "CVE-2022-6666", + Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}, + }, + { + VulnerabilityID: "CVE-2022-7777", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + }, + }, + }, + } + deployOrionWithThirdVulns = Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "orion", + Metadata: []types.Metadata{ + { + ImageID: "123", + RepoTags: []string{ + "alpine:3.14", + }, + RepoDigests: []string{ + "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + }, + }, + Results: types.Results{ + {}, + {}, + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-1111", + Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}, + }, + { + VulnerabilityID: "CVE-2022-2222", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + { + VulnerabilityID: "CVE-2022-3333", + Vulnerability: dbTypes.Vulnerability{Severity: "HIGH"}, + }, + { + VulnerabilityID: "CVE-2022-4444", + Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}, + }, + { + VulnerabilityID: "CVE-2022-5555", + Vulnerability: dbTypes.Vulnerability{Severity: "UNKNOWN"}, + }, + { + VulnerabilityID: "CVE-2022-6666", + Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}, + }, + { + VulnerabilityID: "CVE-2022-7777", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + }, + }, + }, + } + + orionDeployWithAnotherMisconfig = Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "orion", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "ID201", + Status: types.MisconfStatusFailure, + Severity: "HIGH", + }, + }, + }, + }, + } + + image1WithVulns = Resource{ + Namespace: "default", + Kind: "Pod", + Name: "multi-image-pod", + Metadata: []types.Metadata{ + { + ImageID: "image1", + RepoTags: []string{ + "alpine:3.14", + }, + RepoDigests: []string{ + "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + }, + }, + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-1111", + Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}, + }, + }, + }, + }, + } + + image2WithVulns = Resource{ + Namespace: "default", + Kind: "Pod", + Name: "multi-image-pod", + Metadata: []types.Metadata{ + { + ImageID: "image2", + RepoTags: []string{ + "alpine:3.17.3", + }, + RepoDigests: []string{ + "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + }, + }, + }, + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-2222", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + }, + }, + }, + } + + multiImagePodWithVulns = Resource{ + Namespace: "default", + Kind: "Pod", + Name: "multi-image-pod", + Metadata: []types.Metadata{ + { + ImageID: "image1", + RepoTags: []string{ + "alpine:3.14", + }, + RepoDigests: []string{ + "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + }, + { + ImageID: "image2", + RepoTags: []string{ + "alpine:3.17.3", + }, + RepoDigests: []string{ + "alpine@sha256:124c7d2707904eea7431fffe91522a01e5a861a624ee31d03372cc1d138a3126", + }, + }, + }, + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-1111", + Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}, + }, + }, + }, + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-2222", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + }, + }, + }, + } + + deployOrionWithBothVulnsAndMisconfigs = Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "orion", + Metadata: []types.Metadata{ + { + ImageID: "123", + RepoTags: []string{ + "alpine:3.14", + }, + RepoDigests: []string{ + "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + }, + }, + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "ID100", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "ID101", + Status: types.MisconfStatusFailure, + Severity: "MEDIUM", + }, + { + ID: "ID102", + Status: types.MisconfStatusFailure, + Severity: "HIGH", + }, + { + ID: "ID103", + Status: types.MisconfStatusFailure, + Severity: "CRITICAL", + }, + { + ID: "ID104", + Status: types.MisconfStatusFailure, + Severity: "UNKNOWN", + }, + { + ID: "ID105", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "ID106", + Status: types.MisconfStatusFailure, + Severity: "HIGH", + }, + }, + }, + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-1111", + Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}, + }, + { + VulnerabilityID: "CVE-2022-2222", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + { + VulnerabilityID: "CVE-2022-3333", + Vulnerability: dbTypes.Vulnerability{Severity: "HIGH"}, + }, + { + VulnerabilityID: "CVE-2022-4444", + Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}, + }, + { + VulnerabilityID: "CVE-2022-5555", + Vulnerability: dbTypes.Vulnerability{Severity: "UNKNOWN"}, + }, + { + VulnerabilityID: "CVE-2022-6666", + Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}, + }, + { + VulnerabilityID: "CVE-2022-7777", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + }, + }, + }, + } + + cronjobHelloWithVulns = Resource{ + Namespace: "default", + Kind: "Cronjob", + Name: "hello", + Metadata: []types.Metadata{ + { + ImageID: "123", + RepoTags: []string{ + "alpine:3.14", + }, + RepoDigests: []string{ + "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + }, + }, + Results: types.Results{ + {Vulnerabilities: []types.DetectedVulnerability{{VulnerabilityID: "CVE-2020-9999"}}}, + }, + } + + podPrometheusWithMisconfigs = Resource{ + Namespace: "default", + Kind: "Pod", + Name: "prometheus", + Metadata: []types.Metadata{ + { + ImageID: "123", + RepoTags: []string{ + "alpine:3.14", + }, + RepoDigests: []string{ + "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + }, + }, + Results: types.Results{ + {Misconfigurations: []types.DetectedMisconfiguration{{ID: "ID100"}}}, + }, + } + + roleWithMisconfig = Resource{ + Namespace: "default", + Kind: "Role", + Name: "system::leader-locking-kube-controller-manager", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "ID100", + Status: types.MisconfStatusFailure, + Severity: "MEDIUM", + }, + }, + }, + }, + } + + deployLuaWithSecrets = Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "lua", + Results: types.Results{ + { + Secrets: []types.DetectedSecret{ + { + RuleID: "secret1", + Severity: "CRITICAL", + }, + { + RuleID: "secret2", + Severity: "MEDIUM", + }, + }, + }, + }, + } + + apiseverPodWithMisconfigAndInfra = Resource{ + Namespace: "kube-system", + Kind: "Pod", + Name: "kube-apiserver", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "KSV-ID100", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "KSV-ID101", + Status: types.MisconfStatusFailure, + Severity: "MEDIUM", + }, + { + ID: "KSV-ID102", + Status: types.MisconfStatusFailure, + Severity: "HIGH", + }, + + { + ID: "KCV-ID100", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "KCV-ID101", + Status: types.MisconfStatusFailure, + Severity: "MEDIUM", + }, + }, + }, + }, + } +) + +func TestReport_consolidate(t *testing.T) { + concatenatedResource := orionDeployWithAnotherMisconfig + concatenatedResource.Results[0].Misconfigurations = append(concatenatedResource.Results[0].Misconfigurations, + deployOrionWithMisconfigs.Results[0].Misconfigurations...) + + tests := []struct { + name string + report Report + expectedFindings map[string]Resource + }{ + { + name: "report with both misconfigs and vulnerabilities", + report: Report{ + Resources: []Resource{ + deployOrionWithVulns, + cronjobHelloWithVulns, + deployOrionWithMisconfigs, + podPrometheusWithMisconfigs, + }, + }, + expectedFindings: map[string]Resource{ + "default/deploy/orion": deployOrionWithBothVulnsAndMisconfigs, + "default/cronjob/hello": cronjobHelloWithVulns, + "default/pod/prometheus": podPrometheusWithMisconfigs, + }, + }, + { + name: "report with only misconfigurations", + report: Report{ + Resources: []Resource{ + deployOrionWithMisconfigs, + podPrometheusWithMisconfigs, + }, + }, + expectedFindings: map[string]Resource{ + "default/deploy/orion": deployOrionWithMisconfigs, + "default/pod/prometheus": podPrometheusWithMisconfigs, + }, + }, + { + name: "report with only vulnerabilities", + report: Report{ + Resources: []Resource{ + deployOrionWithVulns, + cronjobHelloWithVulns, + }, + }, + expectedFindings: map[string]Resource{ + "default/deploy/orion": deployOrionWithVulns, + "default/cronjob/hello": cronjobHelloWithVulns, + }, + }, + { + name: "report with vulnerabilities in the third result", + report: Report{ + Resources: []Resource{ + deployOrionWithThirdVulns, + }, + }, + expectedFindings: map[string]Resource{ + "default/deploy/orion": deployOrionWithThirdVulns, + }, + }, + { + name: "report with misconfigs in image and pod", + report: Report{ + Resources: []Resource{ + deployOrionWithMisconfigs, + orionDeployWithAnotherMisconfig, + }, + }, + expectedFindings: map[string]Resource{ + "default/deploy/orion": concatenatedResource, + }, + }, + { + name: "report with multi image pod containing vulnerabilities", + report: Report{ + Resources: []Resource{ + image1WithVulns, + image2WithVulns, + }, + }, + expectedFindings: map[string]Resource{ + "default/pod/multi-image-pod": multiImagePodWithVulns, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + consolidateReport := tt.report.consolidate() + + if len(consolidateReport.Findings) != len(tt.expectedFindings) { + t.Errorf("expected %d findings, got %d", len(tt.expectedFindings), len(consolidateReport.Findings)) + } + + for _, f := range consolidateReport.Findings { + key := f.fullname() + + expected, found := tt.expectedFindings[key] + if !found { + t.Errorf("key not found: %s", key) + } + + assert.Equal(t, expected, f) + } + }) + } +} + +func TestResource_fullname(t *testing.T) { + tests := []struct { + expected string + resource Resource + }{ + { + "default/deploy/orion", + deployOrionWithBothVulnsAndMisconfigs, + }, + { + "default/deploy/orion", + deployOrionWithMisconfigs, + }, + { + "default/cronjob/hello", + cronjobHelloWithVulns, + }, + { + "default/pod/prometheus", + podPrometheusWithMisconfigs, + }, + } + + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.resource.fullname()) + }) + } +} + +func TestResourceFailed(t *testing.T) { + tests := []struct { + name string + report Report + expected bool + }{ + { + name: "report with both misconfigs and vulnerabilities", + report: Report{ + Resources: []Resource{ + deployOrionWithVulns, + cronjobHelloWithVulns, + deployOrionWithMisconfigs, + podPrometheusWithMisconfigs, + }, + }, + expected: true, + }, + { + name: "report with only misconfigurations", + report: Report{ + Resources: []Resource{ + deployOrionWithMisconfigs, + podPrometheusWithMisconfigs, + }, + }, + expected: true, + }, + { + name: "report with only vulnerabilities", + report: Report{ + Resources: []Resource{ + deployOrionWithVulns, + cronjobHelloWithVulns, + }, + }, + expected: true, + }, + { + name: "report without vulnerabilities and misconfigurations", + report: Report{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, tt.report.Failed()) + }) + } +} + +func Test_rbacResource(t *testing.T) { + tests := []struct { + name string + misConfig Resource + want bool + }{ + { + name: "rbac Role resources", + misConfig: Resource{Kind: "Role"}, + want: true, + }, + { + name: "rbac ClusterRole resources", + misConfig: Resource{Kind: "ClusterRole"}, + want: true, + }, + { + name: "rbac RoleBinding resources", + misConfig: Resource{Kind: "RoleBinding"}, + want: true, + }, + { + name: "rbac ClusterRoleBinding resources", + misConfig: Resource{Kind: "ClusterRoleBinding"}, + want: true, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := rbacResource(test.misConfig) + assert.Equal(t, test.want, got) + }) + } +} + +func Test_separateMisconfigReports(t *testing.T) { + k8sReport := Report{ + Resources: []Resource{ + {Kind: "Role"}, + {Kind: "Deployment"}, + {Kind: "StatefulSet"}, + { + Kind: "Pod", + Namespace: "kube-system", + Results: []types.Result{ + {Misconfigurations: []types.DetectedMisconfiguration{{ID: "KCV-0001"}}}, + {Misconfigurations: []types.DetectedMisconfiguration{{ID: "KSV-0001"}}}, + }, + }, + }, + } + + tests := []struct { + name string + k8sReport Report + scanners types.Scanners + expectedReports []Report + }{ + { + name: "Config, Rbac, and Infra Reports", + k8sReport: k8sReport, + scanners: types.Scanners{ + types.MisconfigScanner, + types.RBACScanner, + }, + expectedReports: []Report{ + // the order matter for the test + { + Resources: []Resource{ + {Kind: "Deployment"}, + {Kind: "StatefulSet"}, + }, + }, + {Resources: []Resource{{Kind: "Pod"}}}, + {Resources: []Resource{{Kind: "Role"}}}, + }, + }, + { + name: "Config and Infra for the same resource", + k8sReport: k8sReport, + scanners: types.Scanners{types.MisconfigScanner}, + expectedReports: []Report{ + // the order matter for the test + { + Resources: []Resource{ + {Kind: "Deployment"}, + {Kind: "StatefulSet"}, + }, + }, + {Resources: []Resource{{Kind: "Pod"}}}, + }, + }, + { + name: "Role Report Only", + k8sReport: k8sReport, + scanners: types.Scanners{types.RBACScanner}, + expectedReports: []Report{ + {Resources: []Resource{{Kind: "Role"}}}, + }, + }, + { + name: "Config Report Only", + k8sReport: k8sReport, + scanners: types.Scanners{types.MisconfigScanner}, + expectedReports: []Report{ + { + Resources: []Resource{ + {Kind: "Deployment"}, + {Kind: "StatefulSet"}, + }, + }, + { + Resources: []Resource{ + {Kind: "Pod"}, + }, + }, + }, + }, + { + name: "Infra Report Only", + k8sReport: k8sReport, + scanners: types.Scanners{types.MisconfigScanner}, + expectedReports: []Report{ + { + Resources: []Resource{ + {Kind: "Deployment"}, + {Kind: "StatefulSet"}, + }, + }, + { + Resources: []Resource{ + {Kind: "Pod"}, + }, + }, + }, + }, + + // TODO: add vuln only + // TODO: add secret only + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + reports := SeparateMisconfigReports(tt.k8sReport, tt.scanners) + assert.Len(t, reports, len(tt.expectedReports)) + + for i := range reports { + assert.Len(t, reports[i].Report.Resources, len(tt.expectedReports[i].Resources)) + for j, m := range tt.expectedReports[i].Resources { + assert.Equal(t, m.Kind, reports[i].Report.Resources[j].Kind) + } + } + }) + } +} diff --git a/pkg/k8s/report/summary.go b/pkg/k8s/report/summary.go new file mode 100644 index 000000000000..3f9b0659632b --- /dev/null +++ b/pkg/k8s/report/summary.go @@ -0,0 +1,213 @@ +package report + +import ( + "fmt" + "io" + "slices" + "sort" + "strconv" + "strings" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/table" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + pkgReport "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +type SummaryWriter struct { + Output io.Writer + Severities []string + SeverityHeadings []string + ColumnsHeading []string +} + +func NewSummaryWriter(output io.Writer, requiredSevs []dbTypes.Severity, columnHeading []string) SummaryWriter { + var severities []string + var severityHeadings []string + severities, severityHeadings = getRequiredSeverities(requiredSevs) + return SummaryWriter{ + Output: output, + Severities: severities, + SeverityHeadings: severityHeadings, + ColumnsHeading: columnHeading, + } +} + +func ColumnHeading(scanners types.Scanners, availableColumns []string) []string { + columns := []string{ + NamespaceColumn, + ResourceColumn, + } + securityOptions := make(map[string]any, 0) + // maintain column order (vuln,config,secret) + for _, check := range scanners { + switch check { + case types.VulnerabilityScanner: + securityOptions[VulnerabilitiesColumn] = nil + case types.MisconfigScanner: + securityOptions[MisconfigurationsColumn] = nil + case types.SecretScanner: + securityOptions[SecretsColumn] = nil + case types.RBACScanner: + securityOptions[RbacAssessmentColumn] = nil + } + } + for _, col := range availableColumns { + if _, ok := securityOptions[col]; ok { + columns = append(columns, col) + } + } + return columns +} + +// Write writes the results in a summarized table format +func (s SummaryWriter) Write(report Report) error { + // no report column to print + if len(s.ColumnsHeading) == 2 { + return nil + } + consolidated := report.consolidate() + + if _, err := fmt.Fprintln(s.Output); err != nil { + return xerrors.Errorf("failed to write summary report: %w", err) + } + + if _, err := fmt.Fprintln(s.Output, report.name); err != nil { + return xerrors.Errorf("failed to write summary report title: %w", err) + } + + t := table.New(s.Output) + t.SetRowLines(false) + configureHeader(s, t, s.ColumnsHeading) + + sort.Slice(consolidated.Findings, func(i, j int) bool { + return consolidated.Findings[i].Namespace > consolidated.Findings[j].Namespace + }) + + for _, finding := range consolidated.Findings { + if !finding.Results.Failed() { + continue + } + vCount, mCount, sCount := accumulateSeverityCounts(finding) + name := fmt.Sprintf("%s/%s", finding.Kind, finding.Name) + rowParts := []string{ + finding.Namespace, + name, + } + + if slices.Contains(s.ColumnsHeading, VulnerabilitiesColumn) { + rowParts = append(rowParts, s.generateSummary(vCount)...) + } + + if slices.Contains(s.ColumnsHeading, MisconfigurationsColumn) || + slices.Contains(s.ColumnsHeading, RbacAssessmentColumn) { + rowParts = append(rowParts, s.generateSummary(mCount)...) + } + + if slices.Contains(s.ColumnsHeading, SecretsColumn) { + rowParts = append(rowParts, s.generateSummary(sCount)...) + } + + t.AddRow(rowParts...) + } + + t.Render() + + keyParts := []string{"Severities:"} + for _, s := range s.Severities { + keyParts = append(keyParts, fmt.Sprintf("%s=%s", s[:1], pkgReport.ColorizeSeverity(s, s))) + } + + _, _ = fmt.Fprintln(s.Output, strings.Join(keyParts, " ")) + _, _ = fmt.Fprintln(s.Output) + return nil +} + +func (s SummaryWriter) generateSummary(sevCount map[string]int) []string { + var parts []string + + for _, sev := range s.Severities { + if count, ok := sevCount[sev]; ok { + parts = append(parts, pkgReport.ColorizeSeverity(strconv.Itoa(count), sev)) + } else { + parts = append(parts, " ") + } + } + + return parts +} + +func getRequiredSeverities(requiredSevs []dbTypes.Severity) ([]string, []string) { + requiredSevOrder := []dbTypes.Severity{ + dbTypes.SeverityCritical, + dbTypes.SeverityHigh, + dbTypes.SeverityMedium, + dbTypes.SeverityLow, + dbTypes.SeverityUnknown, + } + var severities []string + var severityHeadings []string + for _, sev := range requiredSevOrder { + for _, p := range requiredSevs { + if p == sev { + severities = append(severities, sev.String()) + severityHeadings = append(severityHeadings, strings.ToUpper(sev.String()[:1])) + continue + } + } + } + return severities, severityHeadings +} + +func accumulateSeverityCounts(finding Resource) (map[string]int, map[string]int, map[string]int) { + vCount := make(map[string]int) + mCount := make(map[string]int) + sCount := make(map[string]int) + for _, r := range finding.Results { + for _, rv := range r.Vulnerabilities { + vCount[rv.Severity]++ + } + for _, rv := range r.Misconfigurations { + if rv.Status == types.MisconfStatusPassed { + continue + } + mCount[rv.Severity]++ + } + for _, rv := range r.Secrets { + sCount[rv.Severity]++ + } + } + return vCount, mCount, sCount +} + +func configureHeader(s SummaryWriter, t *table.Table, columnHeading []string) { + sevCount := len(s.Severities) + if len(columnHeading) > 2 { + headerRow := []string{ + columnHeading[0], + columnHeading[1], + } + // vulnerabilities headings + count := len(columnHeading) - len(headerRow) + colSpan := []int{ + 1, + 1, + } + headerAlignment := []table.Alignment{ + table.AlignLeft, + table.AlignLeft, + } + for range count { + headerRow = append(headerRow, s.SeverityHeadings...) + colSpan = append(colSpan, sevCount) + headerAlignment = append(headerAlignment, table.AlignCenter) + } + t.SetHeaders(columnHeading...) + t.AddHeaders(headerRow...) + t.SetAlignment(headerAlignment...) + t.SetAutoMergeHeaders(true) + t.SetHeaderColSpans(0, colSpan...) + } +} diff --git a/pkg/k8s/report/summary_test.go b/pkg/k8s/report/summary_test.go new file mode 100644 index 000000000000..dd1b6985e159 --- /dev/null +++ b/pkg/k8s/report/summary_test.go @@ -0,0 +1,100 @@ +package report + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestReport_ColumnHeading(t *testing.T) { + allScanners := types.Scanners{ + types.VulnerabilityScanner, + types.MisconfigScanner, + types.SecretScanner, + types.RBACScanner, + } + + tests := []struct { + name string + scanners types.Scanners + availableColumns []string + want []string + }{ + { + name: "filter workload columns", + scanners: allScanners, + availableColumns: WorkloadColumns(), + want: []string{ + NamespaceColumn, + ResourceColumn, + VulnerabilitiesColumn, + MisconfigurationsColumn, + SecretsColumn, + }, + }, + { + name: "filter rbac columns", + scanners: allScanners, + availableColumns: RoleColumns(), + want: []string{ + NamespaceColumn, + ResourceColumn, + RbacAssessmentColumn, + }, + }, + { + name: "filter infra columns", + scanners: allScanners, + availableColumns: InfraColumns(), + want: []string{ + NamespaceColumn, + ResourceColumn, + VulnerabilitiesColumn, + MisconfigurationsColumn, + SecretsColumn, + }, + }, + { + name: "config column only", + scanners: types.Scanners{types.MisconfigScanner}, + availableColumns: WorkloadColumns(), + want: []string{ + NamespaceColumn, + ResourceColumn, + MisconfigurationsColumn, + }, + }, + { + name: "secret column only", + scanners: types.Scanners{types.SecretScanner}, + availableColumns: WorkloadColumns(), + want: []string{ + NamespaceColumn, + ResourceColumn, + SecretsColumn, + }, + }, + { + name: "vuln column only", + scanners: types.Scanners{types.VulnerabilityScanner}, + availableColumns: WorkloadColumns(), + want: []string{ + NamespaceColumn, + ResourceColumn, + VulnerabilitiesColumn, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + column := ColumnHeading(tt.scanners, tt.availableColumns) + if !assert.Equal(t, tt.want, column) { + t.Error(fmt.Errorf("TestReport_ColumnHeading want %v got %v", tt.want, column)) + } + }) + } +} diff --git a/pkg/k8s/report/table.go b/pkg/k8s/report/table.go new file mode 100644 index 000000000000..d7b1a5509fcf --- /dev/null +++ b/pkg/k8s/report/table.go @@ -0,0 +1,93 @@ +package report + +import ( + "context" + "fmt" + "io" + "strings" + + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + pkgReport "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +type TableWriter struct { + Report string + Output io.Writer + Severities []dbTypes.Severity + ColumnHeading []string + + TableMode []types.TableMode // For `--report all` only +} + +const ( + NamespaceColumn = "Namespace" + ResourceColumn = "Resource" + VulnerabilitiesColumn = "Vulnerabilities" + MisconfigurationsColumn = "Misconfigurations" + SecretsColumn = "Secrets" + RbacAssessmentColumn = "RBAC Assessment" +) + +func WorkloadColumns() []string { + return []string{ + VulnerabilitiesColumn, + MisconfigurationsColumn, + SecretsColumn, + } +} + +func RoleColumns() []string { + return []string{RbacAssessmentColumn} +} + +func InfraColumns() []string { + return []string{ + VulnerabilitiesColumn, + MisconfigurationsColumn, + SecretsColumn, + } +} + +func (tw TableWriter) Write(ctx context.Context, report Report) error { + switch tw.Report { + case AllReport: + t := pkgReport.NewWriter(pkgReport.Options{ + Output: tw.Output, + Severities: tw.Severities, + // k8s has its own summary report, so we only need to show the detailed tables here + TableModes: []types.TableMode{ + types.Detailed, + }, + }) + for i, r := range report.Resources { + if r.Report.Results.Failed() { + updateTargetContext(&report.Resources[i]) + err := t.Write(ctx, r.Report) + if err != nil { + return err + } + } + } + case SummaryReport: + writer := NewSummaryWriter(tw.Output, tw.Severities, tw.ColumnHeading) + return writer.Write(report) + default: + return xerrors.Errorf(`report %q not supported. Use "summary" or "all"`, tw.Report) + } + + return nil +} + +// updateTargetContext add context namespace, kind and name to the target +func updateTargetContext(r *Resource) { + targetName := fmt.Sprintf("namespace: %s, %s: %s", r.Namespace, strings.ToLower(r.Kind), r.Name) + if r.Kind == "NodeComponents" || r.Kind == "NodeInfo" { + targetName = fmt.Sprintf("node: %s", r.Name) + } + for i := range r.Report.Results { + r.Report.Results[i].Target = targetName + } +} diff --git a/pkg/k8s/scanner/io.go b/pkg/k8s/scanner/io.go new file mode 100644 index 000000000000..1aeaec9a4540 --- /dev/null +++ b/pkg/k8s/scanner/io.go @@ -0,0 +1,80 @@ +package scanner + +import ( + "fmt" + "os" + "path/filepath" + "regexp" + "runtime" + + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" + "github.com/aquasecurity/trivy/pkg/log" + xos "github.com/aquasecurity/trivy/pkg/x/os" +) + +var r = regexp.MustCompile("[\\\\/:*?<>]") + +func generateTempFileByArtifact(artifact *artifacts.Artifact, tempDir string) (string, error) { + filename := fmt.Sprintf("%s-%s-%s-*.yaml", artifact.Namespace, artifact.Kind, artifact.Name) + if runtime.GOOS == "windows" { + // removes characters not permitted in file/directory names on Windows + filename = filenameWindowsFriendly(filename) + } + file, err := xos.CreateTemp(tempDir, filename) + if err != nil { + return "", xerrors.Errorf("failed to create temporary file: %w", err) + } + shouldRemove := false + defer func() { + if err := file.Close(); err != nil { + log.Error("Failed to close temp file", log.FilePath(file.Name()), log.Err(err)) + } + if shouldRemove { + removeFile(file.Name()) + } + }() + if err := yaml.NewEncoder(file).Encode(artifact.RawResource); err != nil { + shouldRemove = true + return "", xerrors.Errorf("failed to encode artifact: %w", err) + } + return filepath.Base(file.Name()), nil +} + +// generateTempDir creates a directory with yaml files generated from kubernetes artifacts +// returns a directory name, a map for mapping a temp target file to k8s artifact and error +func generateTempDir(arts []*artifacts.Artifact) (string, map[string]*artifacts.Artifact, error) { + tempDir, err := xos.MkdirTemp("", "k8s-scan-") + if err != nil { + return "", nil, xerrors.Errorf("failed to create temp directory: %w", err) + } + + m := make(map[string]*artifacts.Artifact) + for _, artifact := range arts { + filename, err := generateTempFileByArtifact(artifact, tempDir) + if err != nil { + log.Error("Failed to create temp file", log.FilePath(filename), log.Err(err)) + continue + } + m[filename] = artifact + } + return tempDir, m, nil +} + +func removeDir(dirname string) { + if err := os.RemoveAll(dirname); err != nil { + log.Error("Failed to remove temp directory", log.FilePath(dirname), log.Err(err)) + } +} + +func removeFile(filename string) { + if err := os.Remove(filename); err != nil { + log.Error("Failed to remove temp file", log.FilePath(filename), log.Err(err)) + } +} + +func filenameWindowsFriendly(name string) string { + return r.ReplaceAllString(name, "_") +} diff --git a/pkg/k8s/scanner/io_test.go b/pkg/k8s/scanner/io_test.go new file mode 100644 index 000000000000..9e256b39f2b3 --- /dev/null +++ b/pkg/k8s/scanner/io_test.go @@ -0,0 +1,39 @@ +package scanner + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_FilenameWindowsFriendly(t *testing.T) { + + tests := []struct { + name string + fileName string + want string + }{ + { + name: "name with invalid char - colon", + fileName: `kube-system-Role-system:controller:bootstrap-signer-2934213283.yaml`, + want: `kube-system-Role-system_controller_bootstrap-signer-2934213283.yaml`, + }, + { + name: "name with no invalid chars", + fileName: `kube-system-Role-system-controller-bootstrap-signer-2934213283.yaml`, + want: `kube-system-Role-system-controller-bootstrap-signer-2934213283.yaml`, + }, + { + name: "name with no invalid - slash", + fileName: "-ClusterRoleBinding-system\\basic-user-725844313.yaml", + want: `-ClusterRoleBinding-system_basic-user-725844313.yaml`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got := filenameWindowsFriendly(test.fileName) + assert.Equal(t, test.want, got) + }) + } +} diff --git a/pkg/k8s/scanner/scanner.go b/pkg/k8s/scanner/scanner.go new file mode 100644 index 000000000000..1d6ebddd3934 --- /dev/null +++ b/pkg/k8s/scanner/scanner.go @@ -0,0 +1,665 @@ +package scanner + +import ( + "bytes" + "context" + "fmt" + "sort" + "strings" + + ms "github.com/go-viper/mapstructure/v2" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/go-version/pkg/version" + "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" + "github.com/aquasecurity/trivy-kubernetes/pkg/bom" + "github.com/aquasecurity/trivy/pkg/cache" + cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" + "github.com/aquasecurity/trivy/pkg/digest" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/k8s" + "github.com/aquasecurity/trivy/pkg/k8s/report" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/parallel" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" + "github.com/aquasecurity/trivy/pkg/scan/local" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + k8sCoreComponentNamespace = cyclonedx.Namespace + "resource:" + k8sComponentType = "Type" + k8sComponentName = "Name" + k8sComponentNode = "node" +) + +type Scanner struct { + cluster string + runner cmd.Runner + opts flag.Options +} + +func NewScanner(cluster string, runner cmd.Runner, opts flag.Options) *Scanner { + return &Scanner{ + cluster, + runner, + opts, + } +} + +func (s *Scanner) Scan(ctx context.Context, artifactsData []*artifacts.Artifact) (report.Report, error) { + // disable logs before scanning + log.InitLogger(s.opts.Debug, true) + + // enable log, this is done in a defer function, + // to enable logs even when the function returns earlier + // due to an error + defer log.InitLogger(s.opts.Debug, false) + + if s.opts.Format == types.FormatCycloneDX { + kbom, err := s.clusterInfoToReportResources(artifactsData) + if err != nil { + return report.Report{}, err + } + return report.Report{ + SchemaVersion: 0, + BOM: kbom, + }, nil + } + var resourceArtifacts []*artifacts.Artifact + var k8sCoreArtifacts []*artifacts.Artifact + for _, artifact := range artifactsData { + if strings.HasSuffix(artifact.Kind, "Components") || strings.HasSuffix(artifact.Kind, "Cluster") { + k8sCoreArtifacts = append(k8sCoreArtifacts, artifact) + continue + } + resourceArtifacts = append(resourceArtifacts, artifact) + } + + var resources []report.Resource + + // scans kubernetes artifacts as a scope of yaml files + if local.ShouldScanMisconfigOrRbac(s.opts.Scanners) { + misconfigs, err := s.scanMisconfigs(ctx, resourceArtifacts) + if err != nil { + return report.Report{}, xerrors.Errorf("scanning misconfigurations error: %w", err) + } + resources = append(resources, misconfigs...) + } + + // scan images from kubernetes cluster in parallel + if s.opts.Scanners.AnyEnabled(types.VulnerabilityScanner, types.SecretScanner) && !s.opts.SkipImages { + onItem := func(ctx context.Context, artifact *artifacts.Artifact) ([]report.Resource, error) { + opts := s.opts + opts.Credentials = make([]ftypes.Credential, len(s.opts.Credentials)) + copy(opts.Credentials, s.opts.Credentials) + // add image private registry credential auto detected from workload imagePullsecret / serviceAccount + if len(artifact.Credentials) > 0 { + for _, cred := range artifact.Credentials { + opts.RegistryOptions.Credentials = append(opts.RegistryOptions.Credentials, + ftypes.Credential{ + Username: cred.Username, + Password: cred.Password, + }, + ) + } + } + vulns, err := s.scanVulns(ctx, artifact, opts) + if err != nil { + return nil, xerrors.Errorf("scanning vulnerabilities error: %w", err) + } + return vulns, nil + } + + onResult := func(result []report.Resource) error { + resources = append(resources, result...) + return nil + } + workers := s.opts.Parallel + if s.opts.CacheBackend == string(cache.TypeFS) { + // To avoid lock contention in bbolt, we limit the number of workers to 1 when using FS cache. + workers = 1 + } + + p := parallel.NewPipeline(workers, !s.opts.Quiet, resourceArtifacts, onItem, onResult) + if err := p.Do(ctx); err != nil { + return report.Report{}, err + } + } + + if s.opts.Scanners.AnyEnabled(types.VulnerabilityScanner) { + k8sResource, err := s.scanK8sVulns(ctx, k8sCoreArtifacts) + if err != nil { + return report.Report{}, err + } + resources = append(resources, k8sResource...) + } + return report.Report{ + SchemaVersion: 0, + ClusterName: s.cluster, + Resources: resources, + }, nil + +} + +func (s *Scanner) scanVulns(ctx context.Context, artifact *artifacts.Artifact, opts flag.Options) ([]report.Resource, error) { + resources := make([]report.Resource, 0, len(artifact.Images)) + + for _, image := range artifact.Images { + + opts.Target = image + + imageReport, err := s.runner.ScanImage(ctx, opts) + + if err != nil { + resources = append(resources, report.CreateResource(artifact, imageReport, err)) + continue + } + + resource, err := s.filter(ctx, imageReport, artifact) + if err != nil { + return nil, xerrors.Errorf("filter error: %w", err) + } + + resources = append(resources, resource) + } + + return resources, nil +} + +func (s *Scanner) scanMisconfigs(ctx context.Context, k8sArtifacts []*artifacts.Artifact) ([]report.Resource, error) { + dir, artifactsByFilename, err := generateTempDir(k8sArtifacts) + if err != nil { + return nil, xerrors.Errorf("failed to generate temp dir: %w", err) + } + + s.opts.Target = dir + origCacheBackend := s.opts.CacheBackend + // Using an in-memory cache is safe since scanning k8s is not supported in client/server mode, + // and the cache created during file system scanning is removed after each scan. + s.opts.CacheBackend = string(cache.TypeMemory) + + defer func() { + // remove config files + removeDir(dir) + // restore cache backend + s.opts.CacheBackend = origCacheBackend + }() + + configReport, err := s.runner.ScanFilesystem(ctx, s.opts) + + if err != nil { + return nil, xerrors.Errorf("failed to scan filesystem: %w", err) + } + resources := make([]report.Resource, 0, len(k8sArtifacts)) + + for _, res := range configReport.Results { + artifact := artifactsByFilename[res.Target] + + singleReport := types.Report{ + SchemaVersion: configReport.SchemaVersion, + CreatedAt: configReport.CreatedAt, + ArtifactName: res.Target, + ArtifactType: configReport.ArtifactType, + Metadata: configReport.Metadata, + Results: types.Results{res}, + } + + resource, err := s.filter(ctx, singleReport, artifact) + if err != nil { + resource = report.CreateResource(artifact, singleReport, err) + } + resources = append(resources, resource) + } + + return resources, nil +} + +func (s *Scanner) filter(ctx context.Context, r types.Report, artifact *artifacts.Artifact) (report.Resource, error) { + var err error + r, err = s.runner.Filter(ctx, s.opts, r) + if err != nil { + return report.Resource{}, xerrors.Errorf("filter error: %w", err) + } + return report.CreateResource(artifact, r, nil), nil +} + +const ( + oci = "oci" + kubelet = "k8s.io/kubelet" + controlPlaneComponents = "ControlPlaneComponents" + clusterInfo = "Cluster" + nodeComponents = "NodeComponents" + nodeCoreComponents = "node-core-components" +) + +func (s *Scanner) scanK8sVulns(ctx context.Context, artifactsData []*artifacts.Artifact) ([]report.Resource, error) { + var resources []report.Resource + + // Find the first node name to identify cluster type + nodeName := s.findNodeName(artifactsData) + + k8sScanner := k8s.NewKubernetesScanner() + scanOptions := types.ScanOptions{ + Scanners: s.opts.Scanners, + PkgTypes: s.opts.PkgTypes, + PkgRelationships: s.opts.PackageOptions.PkgRelationships, + VulnSeveritySources: s.opts.VulnSeveritySources, + } + for _, artifact := range artifactsData { + switch artifact.Kind { + case controlPlaneComponents: + var comp bom.Component + err := ms.Decode(artifact.RawResource, &comp) + if err != nil { + return nil, err + } + cpcVersion := unifiedVersion(comp.Version) + + lang := k8sNamespace(cpcVersion, nodeName) + results, _, err := k8sScanner.Scan(ctx, types.ScanTarget{ + Applications: []ftypes.Application{ + { + Type: ftypes.LangType(lang), + FilePath: artifact.Name, + Packages: []ftypes.Package{ + { + Name: comp.Name, + Version: cpcVersion, + }, + }, + }, + }, + }, scanOptions) + if err != nil { + return nil, err + } + if results != nil { + resource, err := s.filter(ctx, types.Report{ + Results: results, + ArtifactName: artifact.Name, + }, artifact) + if err != nil { + return nil, err + } + resources = append(resources, resource) + } + case nodeComponents: + var nf bom.NodeInfo + err := ms.Decode(artifact.RawResource, &nf) + if err != nil { + return nil, err + } + kubeletVersion := unifiedVersion(nf.KubeletVersion) + lang := k8sNamespace(kubeletVersion, nodeName) + runtimeName, runtimeVersion := runtimeNameVersion(nf.ContainerRuntimeVersion) + results, _, err := k8sScanner.Scan(ctx, types.ScanTarget{ + Applications: []ftypes.Application{ + { + Type: ftypes.LangType(lang), + FilePath: artifact.Name, + Packages: []ftypes.Package{ + { + Name: kubelet, + Version: kubeletVersion, + }, + }, + }, + { + Type: ftypes.GoBinary, + FilePath: artifact.Name, + Packages: []ftypes.Package{ + { + Name: runtimeName, + Version: runtimeVersion, + }, + }, + }, + }, + }, scanOptions) + if err != nil { + return nil, err + } + if results != nil { + resource, err := s.filter(ctx, types.Report{ + Results: results, + ArtifactName: artifact.Name, + }, artifact) + if err != nil { + return nil, err + } + resources = append(resources, resource) + } + case clusterInfo: + var cf bom.ClusterInfo + err := ms.Decode(artifact.RawResource, &cf) + if err != nil { + return nil, err + } + lang := k8sNamespace(cf.Version, nodeName) + + results, _, err := k8sScanner.Scan(ctx, types.ScanTarget{ + Applications: []ftypes.Application{ + { + Type: ftypes.LangType(lang), + FilePath: artifact.Name, + Packages: []ftypes.Package{ + { + Name: cf.Name, + Version: cf.Version, + }, + }, + }, + }, + }, scanOptions) + if err != nil { + return nil, err + } + if results != nil { + resource, err := s.filter(ctx, types.Report{ + Results: results, + ArtifactName: artifact.Name, + }, artifact) + if err != nil { + return nil, err + } + resources = append(resources, resource) + } + } + } + return resources, nil +} + +func (*Scanner) findNodeName(allArtifact []*artifacts.Artifact) string { + for _, artifact := range allArtifact { + if artifact.Kind != nodeComponents { + continue + } + return artifact.Name + } + return "" +} + +func (s *Scanner) clusterInfoToReportResources(allArtifact []*artifacts.Artifact) (*core.BOM, error) { + var rootComponent *core.Component + var coreComponents []*core.Component + + // Find the first node name to identify cluster type + nodeName := s.findNodeName(allArtifact) + + kbom := core.NewBOM(core.Options{ + GenerateBOMRef: true, + }) + for _, artifact := range allArtifact { + switch artifact.Kind { + case controlPlaneComponents: + var comp bom.Component + if err := ms.Decode(artifact.RawResource, &comp); err != nil { + return nil, err + } + cVersion := unifiedVersion(comp.Version) + + controlPlane := &core.Component{ + Name: comp.Name, + Version: cVersion, + Type: core.TypeApplication, + Properties: toProperties(comp.Properties, k8sCoreComponentNamespace), + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: generatePURL(comp.Name, cVersion, nodeName), + }, + } + coreComponents = append(coreComponents, controlPlane) + + for _, c := range comp.Containers { + name := fmt.Sprintf("%s/%s", c.Registry, c.Repository) + cDigest := c.Digest + if !strings.Contains(c.Digest, string(digest.SHA256)) { + cDigest = fmt.Sprintf("%s:%s", string(digest.SHA256), cDigest) + } + ver := unifiedVersion(c.Version) + + imagePURL, err := purl.New(purl.TypeOCI, types.Metadata{ + RepoDigests: []string{ + fmt.Sprintf("%s@%s", name, cDigest), + }, + }, ftypes.Package{}) + if err != nil { + return nil, xerrors.Errorf("failed to create PURL: %w", err) + } + + imageComponent := &core.Component{ + Type: core.TypeContainerImage, + Name: name, + Version: cDigest, + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: imagePURL.Unwrap(), + }, + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: fmt.Sprintf("%s:%s", name, ver), + }, + { + Name: core.PropertyPkgType, + Value: oci, + }, + }, + } + kbom.AddRelationship(controlPlane, imageComponent, core.RelationshipDependsOn) + } + case nodeComponents: + var nf bom.NodeInfo + err := ms.Decode(artifact.RawResource, &nf) + if err != nil { + return nil, err + } + coreComponents = append(coreComponents, s.nodeComponent(kbom, nf)) + case clusterInfo: + var cf bom.ClusterInfo + if err := ms.Decode(artifact.RawResource, &cf); err != nil { + return nil, err + } + cVersion := unifiedVersion(cf.Version) + + rootComponent = &core.Component{ + Type: core.TypePlatform, + Name: cf.Name, + Version: cVersion, + Properties: toProperties(cf.Properties, k8sCoreComponentNamespace), + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: generatePURL(cf.Name, cVersion, nodeName), + }, + Root: true, + } + kbom.AddComponent(rootComponent) + default: + return nil, fmt.Errorf("resource kind %s is not supported", artifact.Kind) + } + } + + for _, c := range coreComponents { + kbom.AddRelationship(rootComponent, c, core.RelationshipContains) + } + + return kbom, nil +} + +func (s *Scanner) nodeComponent(b *core.BOM, nf bom.NodeInfo) *core.Component { + osName, osVersion := osNameVersion(nf.OsImage) + runtimeName, runtimeVersion := runtimeNameVersion(nf.ContainerRuntimeVersion) + kubeletVersion := unifiedVersion(nf.KubeletVersion) + properties := toProperties(nf.Properties, "") + properties = append(properties, toProperties(map[string]string{ + k8sComponentType: k8sComponentNode, + k8sComponentName: nf.NodeName, + }, k8sCoreComponentNamespace)...) + + nodeComponent := &core.Component{ + Type: core.TypePlatform, + Name: nf.NodeName, + Properties: properties, + } + + osComponent := &core.Component{ + Type: core.TypeOS, + Name: osName, + Version: osVersion, + Properties: []core.Property{ + { + Name: "Class", + Value: string(types.ClassOSPkg), + }, + { + Name: "Type", + Value: osName, + }, + }, + } + b.AddRelationship(nodeComponent, osComponent, core.RelationshipContains) + + appComponent := &core.Component{ + Type: core.TypeApplication, + Name: nodeCoreComponents, + } + b.AddRelationship(nodeComponent, appComponent, core.RelationshipContains) + + kubeletComponent := &core.Component{ + Type: core.TypeApplication, + Name: kubelet, + Version: kubeletVersion, + Properties: []core.Property{ + { + Name: k8sComponentType, + Value: k8sComponentNode, + Namespace: k8sCoreComponentNamespace, + }, + { + Name: k8sComponentName, + Value: kubelet, + Namespace: k8sCoreComponentNamespace, + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: generatePURL(kubelet, kubeletVersion, nf.NodeName), + }, + } + b.AddRelationship(appComponent, kubeletComponent, core.RelationshipContains) + + runtimeComponent := &core.Component{ + Type: core.TypeApplication, + Name: runtimeName, + Version: runtimeVersion, + Properties: []core.Property{ + { + Name: k8sComponentType, + Value: k8sComponentNode, + Namespace: k8sCoreComponentNamespace, + }, + { + Name: k8sComponentName, + Value: runtimeName, + Namespace: k8sCoreComponentNamespace, + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: packageurl.NewPackageURL(packageurl.TypeGolang, "", runtimeName, runtimeVersion, packageurl.Qualifiers{}, ""), + }, + } + b.AddRelationship(appComponent, runtimeComponent, core.RelationshipContains) + + return nodeComponent +} + +func unifiedVersion(ver string) string { + if strings.HasPrefix(ver, "v") || ver == "" { + return ver + } + return "v" + ver +} + +func osNameVersion(name string) (string, string) { + var buffer bytes.Buffer + var v string + var err error + parts := strings.SplitSeq(name, " ") + for p := range parts { + _, err = version.Parse(p) + if err != nil { + buffer.WriteString(p + " ") + continue + } + v = p + break + } + return strings.ToLower(strings.TrimSpace(buffer.String())), v +} + +func runtimeNameVersion(name string) (string, string) { + runtime, ver, ok := strings.Cut(name, "://") + if !ok { + return "", "" + } + + switch runtime { + case "cri-o": + name = "github.com/cri-o/cri-o" + case "containerd": + name = "github.com/containerd/containerd" + case "cri-dockerd": + name = "github.com/Mirantis/cri-dockerd" + } + return name, unifiedVersion(ver) +} + +func toProperties(props map[string]string, namespace string) []core.Property { + properties := lo.MapToSlice(props, func(k, v string) core.Property { + return core.Property{ + Name: k, + Value: v, + Namespace: namespace, + } + }) + if len(properties) == 0 { + return nil + } + sort.Slice(properties, func(i, j int) bool { + return properties[i].Name < properties[j].Name + }) + return properties +} + +func generatePURL(name, ver, nodeName string) *packageurl.PackageURL { + var namespace string + // Identify k8s distribution. An empty namespace means upstream. + if namespace = k8sNamespace(ver, nodeName); namespace == "" { + return nil + } else if namespace == "kubernetes" { + namespace = "" + } + + return packageurl.NewPackageURL(purl.TypeK8s, namespace, name, ver, nil, "") +} + +func k8sNamespace(ver, nodeName string) string { + namespace := "kubernetes" + switch { + case strings.Contains(ver, "eks"): + namespace = purl.NamespaceEKS + case strings.Contains(ver, "gke"): + namespace = purl.NamespaceGKE + case strings.Contains(ver, "hotfix"): + if !strings.Contains(nodeName, "aks") { + // Unknown k8s distribution + return "" + } + namespace = purl.NamespaceAKS + case strings.Contains(nodeName, "ocp"): + namespace = purl.NamespaceOCP + } + return namespace +} diff --git a/pkg/k8s/scanner/scanner_test.go b/pkg/k8s/scanner/scanner_test.go new file mode 100644 index 000000000000..9e49b68ef827 --- /dev/null +++ b/pkg/k8s/scanner/scanner_test.go @@ -0,0 +1,551 @@ +package scanner + +import ( + "sort" + "testing" + + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-kubernetes/pkg/artifacts" + cmd "github.com/aquasecurity/trivy/pkg/commands/artifact" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/uuid" +) + +func TestScanner_Scan(t *testing.T) { + flagOpts := flag.Options{ReportOptions: flag.ReportOptions{Format: "cyclonedx"}} + tests := []struct { + name string + clusterName string + artifacts []*artifacts.Artifact + wantComponents []*core.Component + }{ + { + name: "test cluster info with resources", + clusterName: "test-cluster", + artifacts: []*artifacts.Artifact{ + { + Namespace: "kube-system", + Kind: "Cluster", + Name: "k8s.io/kubernetes", + RawResource: map[string]any{ + "name": "k8s.io/kubernetes", + "version": "1.21.1", + "type": "ClusterInfo", + "Properties": map[string]string{ + "Name": "kind-kind", + "Type": "cluster", + }, + }, + }, + { + Namespace: "kube-system", + Kind: "ControlPlaneComponents", + Name: "k8s.io/apiserver", + RawResource: map[string]any{ + "Containers": []any{ + map[string]any{ + "Digest": "18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f", + "ID": "kube-apiserver:v1.21.1", + "Registry": "k8s.gcr.io", + "Repository": "kube-apiserver", + "Version": "v1.21.1", + }, + }, + "Name": "k8s.io/apiserver", + "Version": "1.21.1", + }, + }, + { + Kind: "NodeComponents", + Name: "kind-control-plane", + RawResource: map[string]any{ + "ContainerRuntimeVersion": "containerd://1.5.2", + "Hostname": "kind-control-plane", + "KubeProxyVersion": "6.2.13-300.fc38.aarch64", + "KubeletVersion": "v1.21.1", + "NodeName": "kind-control-plane", + "OsImage": "Ubuntu 21.04", + "Properties": map[string]string{ + "Architecture": "arm64", + "HostName": "kind-control-plane", + "KernelVersion": "6.2.15-300.fc38.aarch64", + "NodeRole": "master", + "OperatingSystem": "linux", + }, + }, + }, + }, + wantComponents: []*core.Component{ + { + Type: core.TypeApplication, + Name: "github.com/containerd/containerd", + Version: "v1.5.2", + Properties: []core.Property{ + { + Name: k8sComponentName, + Value: "github.com/containerd/containerd", + Namespace: k8sCoreComponentNamespace, + }, + { + Name: k8sComponentType, + Value: "node", + Namespace: k8sCoreComponentNamespace, + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: "golang", + Name: "github.com/containerd/containerd", + Version: "v1.5.2", + Qualifiers: packageurl.Qualifiers{}, + }, + BOMRef: "pkg:golang/github.com%2Fcontainerd%2Fcontainerd@v1.5.2", + }, + }, + { + Type: core.TypeApplication, + Name: "k8s.io/apiserver", + Version: "v1.21.1", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: purl.TypeK8s, + Name: "k8s.io/apiserver", + Version: "v1.21.1", + }, + BOMRef: "pkg:k8s/k8s.io%2Fapiserver@v1.21.1", + }, + }, + { + Type: core.TypeApplication, + Name: "k8s.io/kubelet", + Version: "v1.21.1", + Properties: []core.Property{ + { + Name: k8sComponentName, + Value: "k8s.io/kubelet", + Namespace: k8sCoreComponentNamespace, + }, + { + Name: k8sComponentType, + Value: "node", + Namespace: k8sCoreComponentNamespace, + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: "k8s", + Name: "k8s.io/kubelet", + Version: "v1.21.1", + }, + BOMRef: "pkg:k8s/k8s.io%2Fkubelet@v1.21.1", + }, + }, + { + Type: core.TypeApplication, + Name: "node-core-components", + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000006", + }, + }, + { + Type: core.TypeContainerImage, + Name: "k8s.gcr.io/kube-apiserver", + Version: "sha256:18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: "oci", + Name: "kube-apiserver", + Version: "sha256:18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f", + Qualifiers: packageurl.Qualifiers{ + { + Key: "repository_url", + Value: "k8s.gcr.io/kube-apiserver", + }, + }, + }, + BOMRef: "pkg:oci/kube-apiserver@sha256%3A18e61c783b41758dd391ab901366ec3546b26fae00eef7e223d1f94da808e02f?repository_url=k8s.gcr.io%2Fkube-apiserver", + }, + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "k8s.gcr.io/kube-apiserver:v1.21.1", + }, + { + Name: core.PropertyPkgType, + Value: "oci", + }, + }, + }, + { + Type: core.TypeOS, + Name: "ubuntu", + Version: "21.04", + Properties: []core.Property{ + { + Name: "Class", + Value: "os-pkgs", + Namespace: "", + }, + { + Name: "Type", + Value: "ubuntu", + Namespace: "", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000005", + }, + }, + { + Type: core.TypePlatform, + Root: true, + Name: "k8s.io/kubernetes", + Version: "v1.21.1", + Properties: []core.Property{ + { + Name: "Name", + Value: "kind-kind", + Namespace: k8sCoreComponentNamespace, + }, + { + Name: "Type", + Value: "cluster", + Namespace: k8sCoreComponentNamespace, + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: purl.TypeK8s, + Name: "k8s.io/kubernetes", + Version: "v1.21.1", + }, + BOMRef: "pkg:k8s/k8s.io%2Fkubernetes@v1.21.1", + }, + }, + { + Type: core.TypePlatform, + Name: "kind-control-plane", + Properties: []core.Property{ + { + Name: "Architecture", + Value: "arm64", + }, + { + Name: "HostName", + Value: "kind-control-plane", + }, + { + Name: "KernelVersion", + Value: "6.2.15-300.fc38.aarch64", + }, + { + Name: k8sComponentName, + Value: "kind-control-plane", + Namespace: k8sCoreComponentNamespace, + }, + { + Name: "NodeRole", + Value: "master", + }, + { + Name: "OperatingSystem", + Value: "linux", + }, + { + Name: k8sComponentType, + Value: "node", + Namespace: k8sCoreComponentNamespace, + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000004", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := t.Context() + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + runner, err := cmd.NewRunner(ctx, flagOpts, cmd.TargetK8s) + require.NoError(t, err) + + scanner := NewScanner(tt.clusterName, runner, flagOpts) + got, err := scanner.Scan(ctx, tt.artifacts) + require.NoError(t, err) + + gotComponents := lo.Values(got.BOM.Components()) + require.Len(t, gotComponents, len(tt.wantComponents)) + + sort.Slice(gotComponents, func(i, j int) bool { + switch { + case gotComponents[i].Type != gotComponents[j].Type: + return gotComponents[i].Type < gotComponents[j].Type + case gotComponents[i].Name != gotComponents[j].Name: + return gotComponents[i].Name < gotComponents[j].Name + default: + return gotComponents[i].Version < gotComponents[j].Version + } + }) + for i, want := range tt.wantComponents { + assert.EqualExportedValues(t, *want, *gotComponents[i], want.Name) + } + }) + } +} + +func TestTestOsNameVersion(t *testing.T) { + tests := []struct { + name string + nameVersion string + compName string + compVersion string + }{ + + { + name: "valid version", + nameVersion: "ubuntu 20.04", + compName: "ubuntu", + compVersion: "20.04", + }, + { + name: "valid sem version", + nameVersion: "ubuntu 20.04.1", + compName: "ubuntu", + compVersion: "20.04.1", + }, + { + name: "non valid version", + nameVersion: "ubuntu", + compName: "ubuntu", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + name, version := osNameVersion(tt.nameVersion) + assert.Equal(t, name, tt.compName) + assert.Equal(t, version, tt.compVersion) + }) + } +} + +func TestGeneratePURL(t *testing.T) { + tests := []struct { + name string + compName string + compVersion string + nodeName string + want string + }{ + { + name: "native k8s component", + compName: "k8s.io/kubelet", + compVersion: "1.24.10", + nodeName: "kind-kind", + want: "pkg:k8s/k8s.io%2Fkubelet@1.24.10", + }, + + { + name: "GKE", + compName: "k8s.io/kubelet", + compVersion: "1.24.10-gke.2300", + nodeName: "gke-gke1796-default-pool-768cb718-sk1d", + want: "pkg:k8s/gke/k8s.io%2Fkubelet@1.24.10-gke.2300", + }, + { + name: "AKS", + compName: "k8s.io/kubelet", + compVersion: "1.24.10-hotfix.20221110", + nodeName: "aks-default-23814474-vmss000000", + want: "pkg:k8s/aks/k8s.io%2Fkubelet@1.24.10-hotfix.20221110", + }, + { + name: "EKS", + compName: "k8s.io/kubelet", + compVersion: "1.23.17-eks-8ccc7ba", + nodeName: "eks-vmss000000", + want: "pkg:k8s/eks/k8s.io%2Fkubelet@1.23.17-eks-8ccc7ba", + }, + { + name: "Rancher", + compName: "k8s.io/kubelet", + compVersion: "1.24.11+rke2r1", + nodeName: "ip-10-0-5-23", + want: "pkg:k8s/k8s.io%2Fkubelet@1.24.11%2Brke2r1", + }, + { + name: "OCP", + compName: "k8s.io/kubelet", + compVersion: "1.26.7+c7ee51f", + nodeName: "ocp413vpool14000-p8vnm-master-2", + want: "pkg:k8s/ocp/k8s.io%2Fkubelet@1.26.7%2Bc7ee51f", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := generatePURL(tt.compName, tt.compVersion, tt.nodeName) + assert.Equal(t, tt.want, got.String()) + }) + } +} + +func TestK8sNamespace(t *testing.T) { + tests := []struct { + name string + compVersion string + nodeName string + want string + }{ + { + name: "native k8s component", + compVersion: "1.24.10", + nodeName: "kind-kind", + want: "kubernetes", + }, + + { + name: "GKE", + compVersion: "1.24.10-gke.2300", + nodeName: "gke-gke1796-default-pool-768cb718-sk1d", + want: "gke", + }, + { + name: "AKS", + compVersion: "1.24.10-hotfix.20221110", + nodeName: "aks-default-23814474-vmss000000", + want: "aks", + }, + { + name: "EKS", + compVersion: "1.23.17-eks-8ccc7ba", + nodeName: "eks-vmss000000", + want: "eks", + }, + { + name: "Rancher", + compVersion: "1.24.11+rke2r1", + nodeName: "ip-10-0-5-23", + want: "kubernetes", + }, + { + name: "OCP", + compVersion: "1.26.7+c7ee51f", + nodeName: "ocp413vpool14000-p8vnm-master-2", + want: "ocp", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := k8sNamespace(tt.compVersion, tt.nodeName) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRuntimeVersion(t *testing.T) { + tests := []struct { + name string + runtimeVersion string + wantName string + wantVersion string + }{ + { + name: "containerd", + runtimeVersion: "containerd://1.5.2", + wantName: "github.com/containerd/containerd", + wantVersion: "v1.5.2", + }, + { + name: "cri-o", + runtimeVersion: "cri-o://1.5.2", + wantName: "github.com/cri-o/cri-o", + wantVersion: "v1.5.2", + }, + { + name: "cri-dockerd", + runtimeVersion: "cri-dockerd://1.5.2", + wantName: "github.com/Mirantis/cri-dockerd", + wantVersion: "v1.5.2", + }, + { + name: "na runtime", + runtimeVersion: "cri:1.5.2", + wantName: "", + wantVersion: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotName, gotVersion := runtimeNameVersion(tt.runtimeVersion) + assert.Equal(t, tt.wantName, gotName) + assert.Equal(t, tt.wantVersion, gotVersion) + }) + } +} + +func TestFindNodeName(t *testing.T) { + tests := []struct { + name string + artifacts []*artifacts.Artifact + want string + }{ + { + name: "find node name", + artifacts: []*artifacts.Artifact{ + { + Namespace: "kube-system", + Kind: "Cluster", + Name: "k8s.io/kubernetes", + RawResource: make(map[string]any), + }, + { + Namespace: "kube-system", + Kind: "ControlPlaneComponents", + Name: "k8s.io/apiserver", + RawResource: make(map[string]any), + }, + { + Kind: "NodeComponents", + Name: "kind-control-plane", + RawResource: make(map[string]any), + }, + }, + want: "kind-control-plane", + }, + { + name: "didn't find node name", + artifacts: []*artifacts.Artifact{ + { + Namespace: "kube-system", + Kind: "Cluster", + Name: "k8s.io/kubernetes", + RawResource: make(map[string]any), + }, + { + Namespace: "kube-system", + Kind: "ControlPlaneComponents", + Name: "k8s.io/apiserver", + RawResource: make(map[string]any), + }, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := Scanner{} + got := s.findNodeName(tt.artifacts) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/k8s/writer.go b/pkg/k8s/writer.go new file mode 100644 index 000000000000..abc5bb381b64 --- /dev/null +++ b/pkg/k8s/writer.go @@ -0,0 +1,52 @@ +package k8s + +import ( + "context" + "fmt" + + cdx "github.com/CycloneDX/cyclonedx-go" + + "github.com/aquasecurity/trivy/pkg/k8s/report" + "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Write writes the results in the give format +func Write(ctx context.Context, k8sreport report.Report, option report.Option) error { + k8sreport.PrintErrors() + + switch option.Format { + case types.FormatJSON: + jwriter := report.JSONWriter{ + Output: option.Output, + Report: option.Report, + } + return jwriter.Write(k8sreport) + case types.FormatTable: + separatedReports := report.SeparateMisconfigReports(k8sreport, option.Scanners) + + if option.Report == report.SummaryReport { + target := fmt.Sprintf("Summary Report for %s", k8sreport.ClusterName) + table.RenderTarget(option.Output, target, table.IsOutputToTerminal(option.Output)) + } + + for _, r := range separatedReports { + writer := &report.TableWriter{ + Output: option.Output, + Report: option.Report, + Severities: option.Severities, + ColumnHeading: report.ColumnHeading(option.Scanners, r.Columns), + } + + if err := writer.Write(ctx, r.Report); err != nil { + return err + } + } + + return nil + case types.FormatCycloneDX: + w := report.NewCycloneDXWriter(option.Output, cdx.BOMFileFormatJSON, option.APIVersion) + return w.Write(ctx, k8sreport.BOM) + } + return nil +} diff --git a/pkg/k8s/writer_test.go b/pkg/k8s/writer_test.go new file mode 100644 index 000000000000..0f2c942a3544 --- /dev/null +++ b/pkg/k8s/writer_test.go @@ -0,0 +1,597 @@ +package k8s + +import ( + "bytes" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/k8s/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + roleWithMisconfig = report.Resource{ + Namespace: "default", + Kind: "Role", + Name: "system::leader-locking-kube-controller-manager", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "ID100", + Status: types.MisconfStatusFailure, + Severity: "MEDIUM", + }, + }, + }, + }, + } + apiseverPodWithMisconfigAndInfra = report.Resource{ + Namespace: "kube-system", + Kind: "Pod", + Name: "kube-apiserver", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "KSV-ID100", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "KSV-ID101", + Status: types.MisconfStatusFailure, + Severity: "MEDIUM", + }, + { + ID: "KSV-ID102", + Status: types.MisconfStatusFailure, + Severity: "HIGH", + }, + { + ID: "KSV-ID103", + Status: types.MisconfStatusPassed, + Severity: "HIGH", + }, + + { + ID: "KCV-ID100", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "KCV-ID101", + Status: types.MisconfStatusFailure, + Severity: "MEDIUM", + }, + }, + }, + }, + } + deployLuaWithSecrets = report.Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "lua", + Results: types.Results{ + { + Secrets: []types.DetectedSecret{ + { + RuleID: "secret1", + Severity: "CRITICAL", + }, + { + RuleID: "secret2", + Severity: "MEDIUM", + }, + }, + }, + }, + } + deployOrionWithMisconfigs = report.Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "orion", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "ID100", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "ID101", + Status: types.MisconfStatusFailure, + Severity: "MEDIUM", + }, + { + ID: "ID102", + Status: types.MisconfStatusFailure, + Severity: "HIGH", + }, + { + ID: "ID103", + Status: types.MisconfStatusFailure, + Severity: "CRITICAL", + }, + { + ID: "ID104", + Status: types.MisconfStatusFailure, + Severity: "UNKNOWN", + }, + { + ID: "ID105", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + { + ID: "ID106", + Status: types.MisconfStatusFailure, + Severity: "HIGH", + }, + }, + }, + }, + } + deployOrionWithSingleMisconfig = report.Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "orion", + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "ID100", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + }, + }, + }, + Report: types.Report{ + Results: types.Results{ + { + Class: types.ClassConfig, + MisconfSummary: &types.MisconfSummary{ + Successes: 0, + Failures: 1, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "ID100", + Title: "Config file is bad", + Description: "Your config file is not good.", + Message: "Oh no, a bad config.", + PrimaryURL: "https://google.com/search?q=bad%20config", + Status: types.MisconfStatusFailure, + Severity: "LOW", + }, + }, + }, + }, + }, + } + deployOrionWithVulns = report.Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "orion", + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-1111", + Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}, + }, + { + VulnerabilityID: "CVE-2022-2222", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + { + VulnerabilityID: "CVE-2022-3333", + Vulnerability: dbTypes.Vulnerability{Severity: "HIGH"}, + }, + { + VulnerabilityID: "CVE-2022-4444", + Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}, + }, + { + VulnerabilityID: "CVE-2022-5555", + Vulnerability: dbTypes.Vulnerability{Severity: "UNKNOWN"}, + }, + { + VulnerabilityID: "CVE-2022-6666", + Vulnerability: dbTypes.Vulnerability{Severity: "CRITICAL"}, + }, + { + VulnerabilityID: "CVE-2022-7777", + Vulnerability: dbTypes.Vulnerability{Severity: "MEDIUM"}, + }, + }, + }, + }, + } + + deployOrionWithSingleVuln = report.Resource{ + Namespace: "default", + Kind: "Deploy", + Name: "orion", + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgID: "foo/bar@v0.0.1", + VulnerabilityID: "CVE-2022-1111", + Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}, + }, + }, + }, + }, + Report: types.Report{ + Results: types.Results{ + { + Class: types.ClassLangPkg, + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgName: "foo/bar", + VulnerabilityID: "CVE-2022-1111", + InstalledVersion: "v0.0.1", + FixedVersion: "v0.0.2", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2022-1111", + Vulnerability: dbTypes.Vulnerability{Severity: "LOW"}, + }, + }, + }, + }, + }, + } +) + +func TestReportWrite_Table(t *testing.T) { + allSeverities := []dbTypes.Severity{ + dbTypes.SeverityUnknown, + dbTypes.SeverityLow, + dbTypes.SeverityMedium, + dbTypes.SeverityHigh, + dbTypes.SeverityCritical, + } + + tests := []struct { + name string + report report.Report + opt report.Option + scanners types.Scanners + severities []dbTypes.Severity + reportType string + expectedOutput string + }{ + { + name: "Only config, all severities", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{deployOrionWithMisconfigs}, + }, + scanners: types.Scanners{types.MisconfigScanner}, + severities: allSeverities, + reportType: report.SummaryReport, + expectedOutput: `Summary Report for test +======================= + +Workload Assessment +┌───────────┬──────────────┬───────────────────┐ +│ Namespace │ Resource │ Misconfigurations │ +│ │ ├───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ +├───────────┼──────────────┼───┼───┼───┼───┼───┤ +│ default │ Deploy/orion │ 1 │ 2 │ 1 │ 2 │ 1 │ +└───────────┴──────────────┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN + + +Infra Assessment +┌───────────┬──────────┬───────────────────┐ +│ Namespace │ Resource │ Misconfigurations │ +│ │ ├───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ +└───────────┴──────────┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`, + }, + { + name: "Single misconfig with `--report all`", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{deployOrionWithSingleMisconfig}, + }, + scanners: types.Scanners{types.MisconfigScanner}, + severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, + }, + reportType: report.AllReport, + expectedOutput: `namespace: default, deploy: orion () +==================================== +Tests: 1 (SUCCESSES: 0, FAILURES: 1) +Failures: 0 (CRITICAL: 0) + +ID100 (LOW): Oh no, a bad config. +════════════════════════════════════════ +Your config file is not good. + +See https://google.com/search?q=bad%20config +────────────────────────────────────────`, + }, + { + name: "vulns and misconfig with `--report all`", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{ + deployOrionWithSingleVuln, + deployOrionWithSingleMisconfig, + }, + }, + scanners: types.Scanners{types.VulnerabilityScanner, types.MisconfigScanner}, + severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, dbTypes.SeverityLow, + }, + reportType: report.AllReport, + expectedOutput: `namespace: default, deploy: orion () +==================================== +Total: 1 (LOW: 1, CRITICAL: 0) + +┌─────────┬───────────────┬──────────┬─────────┬───────────────────┬───────────────┬───────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼───────────────┼──────────┼─────────┼───────────────────┼───────────────┼───────────────────────────────────────────┤ +│ foo/bar │ CVE-2022-1111 │ LOW │ unknown │ v0.0.1 │ v0.0.2 │ https://avd.aquasec.com/nvd/cve-2022-1111 │ +└─────────┴───────────────┴──────────┴─────────┴───────────────────┴───────────────┴───────────────────────────────────────────┘ + +namespace: default, deploy: orion () +==================================== +Tests: 1 (SUCCESSES: 0, FAILURES: 1) +Failures: 1 (LOW: 1, CRITICAL: 0) + +ID100 (LOW): Oh no, a bad config. +════════════════════════════════════════ +Your config file is not good. + +See https://google.com/search?q=bad%20config +────────────────────────────────────────`, + }, + { + name: "Only vuln, all severities", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{deployOrionWithVulns}, + }, + scanners: types.Scanners{types.VulnerabilityScanner}, + severities: allSeverities, + reportType: report.SummaryReport, + expectedOutput: `Summary Report for test +======================= + +Workload Assessment +┌───────────┬──────────────┬───────────────────┐ +│ Namespace │ Resource │ Vulnerabilities │ +│ │ ├───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ +├───────────┼──────────────┼───┼───┼───┼───┼───┤ +│ default │ Deploy/orion │ 2 │ 1 │ 2 │ 1 │ 1 │ +└───────────┴──────────────┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN + + +Infra Assessment +┌───────────┬──────────┬───────────────────┐ +│ Namespace │ Resource │ Vulnerabilities │ +│ │ ├───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ +└───────────┴──────────┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`, + }, + { + name: "Single vuln with `--report all`", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{deployOrionWithSingleVuln}, + }, + scanners: types.Scanners{types.VulnerabilityScanner}, + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + }, + reportType: report.AllReport, + expectedOutput: `namespace: default, deploy: orion () +==================================== +Total: 1 (LOW: 1) + +┌─────────┬───────────────┬──────────┬─────────┬───────────────────┬───────────────┬───────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼───────────────┼──────────┼─────────┼───────────────────┼───────────────┼───────────────────────────────────────────┤ +│ foo/bar │ CVE-2022-1111 │ LOW │ unknown │ v0.0.1 │ v0.0.2 │ https://avd.aquasec.com/nvd/cve-2022-1111 │ +└─────────┴───────────────┴──────────┴─────────┴───────────────────┴───────────────┴───────────────────────────────────────────┘`, + }, + { + name: "Only rbac, all severities", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{roleWithMisconfig}, + }, + scanners: types.Scanners{types.RBACScanner}, + severities: allSeverities, + reportType: report.SummaryReport, + expectedOutput: `Summary Report for test +======================= + +RBAC Assessment +┌───────────┬─────────────────────────────────────────────────────┬───────────────────┐ +│ Namespace │ Resource │ RBAC Assessment │ +│ │ ├───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ +├───────────┼─────────────────────────────────────────────────────┼───┼───┼───┼───┼───┤ +│ default │ Role/system::leader-locking-kube-controller-manager │ │ │ 1 │ │ │ +└───────────┴─────────────────────────────────────────────────────┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`, + }, + { + name: "Only secret, all severities", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{deployLuaWithSecrets}, + }, + scanners: types.Scanners{types.SecretScanner}, + severities: allSeverities, + reportType: report.SummaryReport, + expectedOutput: `Summary Report for test +======================= + +Workload Assessment +┌───────────┬────────────┬───────────────────┐ +│ Namespace │ Resource │ Secrets │ +│ │ ├───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ +├───────────┼────────────┼───┼───┼───┼───┼───┤ +│ default │ Deploy/lua │ 1 │ │ 1 │ │ │ +└───────────┴────────────┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN + + +Infra Assessment +┌───────────┬──────────┬───────────────────┐ +│ Namespace │ Resource │ Secrets │ +│ │ ├───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ +└───────────┴──────────┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`, + }, + { + name: "apiserver, only infra and severities", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{apiseverPodWithMisconfigAndInfra}, + }, + scanners: types.Scanners{types.MisconfigScanner}, + severities: allSeverities, + reportType: report.SummaryReport, + expectedOutput: `Summary Report for test +======================= + +Workload Assessment +┌───────────┬──────────┬───────────────────┐ +│ Namespace │ Resource │ Misconfigurations │ +│ │ ├───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ +└───────────┴──────────┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN + + +Infra Assessment +┌─────────────┬────────────────────┬───────────────────┐ +│ Namespace │ Resource │ Misconfigurations │ +│ │ ├───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ +├─────────────┼────────────────────┼───┼───┼───┼───┼───┤ +│ kube-system │ Pod/kube-apiserver │ │ 1 │ 2 │ 2 │ │ +└─────────────┴────────────────────┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`, + }, + { + name: "apiserver, vuln,config,secret and severities", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{apiseverPodWithMisconfigAndInfra}, + }, + scanners: types.Scanners{ + types.VulnerabilityScanner, + types.MisconfigScanner, + types.SecretScanner, + }, + severities: allSeverities, + reportType: report.SummaryReport, + expectedOutput: `Summary Report for test +======================= + +Workload Assessment +┌───────────┬──────────┬───────────────────┬───────────────────┬───────────────────┐ +│ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ Secrets │ +│ │ ├───┬───┬───┬───┬───┼───┬───┬───┬───┬───┼───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │ +└───────────┴──────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN + + +Infra Assessment +┌─────────────┬────────────────────┬───────────────────┬───────────────────┬───────────────────┐ +│ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ Secrets │ +│ │ ├───┬───┬───┬───┬───┼───┬───┬───┬───┬───┼───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │ +├─────────────┼────────────────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ +│ kube-system │ Pod/kube-apiserver │ │ │ │ │ │ │ 1 │ 2 │ 2 │ │ │ │ │ │ │ +└─────────────┴────────────────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`, + }, + { + name: "apiserver, all misconfig and vuln scanners and severities", + report: report.Report{ + ClusterName: "test", + Resources: []report.Resource{apiseverPodWithMisconfigAndInfra}, + }, + scanners: types.Scanners{ + types.MisconfigScanner, + types.VulnerabilityScanner, + }, + severities: allSeverities, + reportType: report.SummaryReport, + expectedOutput: `Summary Report for test +======================= + +Workload Assessment +┌───────────┬──────────┬───────────────────┬───────────────────┐ +│ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ +│ │ ├───┬───┬───┬───┬───┼───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │ +└───────────┴──────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN + + +Infra Assessment +┌─────────────┬────────────────────┬───────────────────┬───────────────────┐ +│ Namespace │ Resource │ Vulnerabilities │ Misconfigurations │ +│ │ ├───┬───┬───┬───┬───┼───┬───┬───┬───┬───┤ +│ │ │ C │ H │ M │ L │ U │ C │ H │ M │ L │ U │ +├─────────────┼────────────────────┼───┼───┼───┼───┼───┼───┼───┼───┼───┼───┤ +│ kube-system │ Pod/kube-apiserver │ │ │ │ │ │ │ 1 │ 2 │ 2 │ │ +└─────────────┴────────────────────┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ +Severities: C=CRITICAL H=HIGH M=MEDIUM L=LOW U=UNKNOWN`, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Setenv("TRIVY_DISABLE_VEX_NOTICE", "true") + output := bytes.Buffer{} + + opt := report.Option{ + Format: "table", + Report: tc.reportType, + Output: &output, + Scanners: tc.scanners, + Severities: tc.severities, + } + + err := Write(t.Context(), tc.report, opt) + require.NoError(t, err) + got := stripAnsi(output.String()) + got = strings.ReplaceAll(got, "\r\n", "\n") + assert.Equal(t, tc.expectedOutput, got, tc.name) + }) + } +} + +const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))" + +var ansiRegexp = regexp.MustCompile(ansi) + +func stripAnsi(str string) string { + return strings.TrimSpace(ansiRegexp.ReplaceAllString(str, "")) +} diff --git a/pkg/licensing/classifier.go b/pkg/licensing/classifier.go new file mode 100644 index 000000000000..5f230624bcc9 --- /dev/null +++ b/pkg/licensing/classifier.go @@ -0,0 +1,87 @@ +package licensing + +import ( + "fmt" + "io" + "sort" + "sync" + + classifier "github.com/google/licenseclassifier/v2" + "github.com/google/licenseclassifier/v2/assets" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +var ( + cf *classifier.Classifier + classifierOnce sync.Once + m sync.Mutex +) + +func initGoogleClassifier() error { + // Initialize the default classifier once. + // This loading is expensive and should be called only when the license classification is needed. + var err error + classifierOnce.Do(func() { + log.Debug("Loading the default license classifier...") + cf, err = assets.DefaultClassifier() + }) + return err +} + +// Classify detects and classifies the license found in a file +func Classify(filePath string, r io.Reader, confidenceLevel float64) (*types.LicenseFile, error) { + content, err := io.ReadAll(r) + if err != nil { + return nil, xerrors.Errorf("unable to read a license file %q: %w", filePath, err) + } + if err = initGoogleClassifier(); err != nil { + return nil, err + } + + var findings types.LicenseFindings + var matchType types.LicenseType + seen := set.New[string]() + + // cf.Match is not thread safe + m.Lock() + + // Use 'github.com/google/licenseclassifier' to find licenses + result := cf.Match(cf.Normalize(content)) + + m.Unlock() + + for _, match := range result.Matches { + if match.Confidence <= confidenceLevel { + continue + } + if seen.Contains(match.Name) { + continue + } + + seen.Append(match.Name) + + switch match.MatchType { + case "Header": + matchType = types.LicenseTypeHeader + case "License": + matchType = types.LicenseTypeFile + } + licenseLink := fmt.Sprintf("https://spdx.org/licenses/%s.html", match.Name) + + findings = append(findings, types.LicenseFinding{ + Name: match.Name, + Confidence: match.Confidence, + Link: licenseLink, + }) + } + sort.Sort(findings) + return &types.LicenseFile{ + Type: matchType, + FilePath: filePath, + Findings: findings, + }, nil +} diff --git a/pkg/licensing/classifier_test.go b/pkg/licensing/classifier_test.go new file mode 100644 index 000000000000..0a559f89f486 --- /dev/null +++ b/pkg/licensing/classifier_test.go @@ -0,0 +1,78 @@ +package licensing_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" +) + +func TestClassifier_FullClassify(t *testing.T) { + tests := []struct { + name string + filePath string + want *types.LicenseFile + wantErr assert.ErrorAssertionFunc + }{ + { + name: "C file with AGPL-3.0", + filePath: "testdata/licensed.c", + want: &types.LicenseFile{ + Type: types.LicenseTypeHeader, + FilePath: "testdata/licensed.c", + Findings: []types.LicenseFinding{ + { + Name: "AGPL-3.0", + Confidence: 1, + Link: "https://spdx.org/licenses/AGPL-3.0.html", + }, + }, + }, + }, + { + name: "Creative commons License file", + filePath: "testdata/LICENSE_creativecommons", + want: &types.LicenseFile{ + Type: types.LicenseTypeFile, + FilePath: "testdata/LICENSE_creativecommons", + Findings: []types.LicenseFinding{ + { + Name: "Commons-Clause", + Confidence: 1, + Link: "https://spdx.org/licenses/Commons-Clause.html", + }, + }, + }, + }, + { + name: "Apache 2 License file", + filePath: "testdata/LICENSE_apache2", + want: &types.LicenseFile{ + Type: types.LicenseTypeFile, + FilePath: "testdata/LICENSE_apache2", + Findings: []types.LicenseFinding{ + { + Name: "Apache-2.0", + Confidence: 1, + Link: "https://spdx.org/licenses/Apache-2.0.html", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + contentFile, err := os.Open(tt.filePath) + require.NoError(t, err) + defer contentFile.Close() + + got, err := licensing.Classify(tt.filePath, contentFile, 0.9) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/licensing/export_test.go b/pkg/licensing/export_test.go new file mode 100644 index 000000000000..e0c4ea6b7eff --- /dev/null +++ b/pkg/licensing/export_test.go @@ -0,0 +1,16 @@ +package licensing + +import "github.com/aquasecurity/trivy/pkg/licensing/expression" + +// Bridge to expose licensing internals to tests in the licensing_test package. + +// StandardizeKeyAndSuffix exports standardizeKeyAndSuffix for testing. +var StandardizeKeyAndSuffix = standardizeKeyAndSuffix + +// NormalizeLicense exports normalizeLicense for testing. +var NormalizeLicense = normalizeLicense + +// Mapping exports mapping for testing. +func Mapping() map[string]expression.SimpleExpr { + return mapping +} diff --git a/pkg/licensing/expression/category.go b/pkg/licensing/expression/category.go new file mode 100644 index 000000000000..3e1f6d1544f5 --- /dev/null +++ b/pkg/licensing/expression/category.go @@ -0,0 +1,433 @@ +package expression + +import ( + "encoding/json" + "strings" + "sync" + + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + + _ "embed" +) + +// Canonical names of the licenses. +// ported from https://github.com/google/licenseclassifier/blob/7c62d6fe8d3aa2f39c4affb58c9781d9dc951a2d/license_type.go#L24-L177 +const ( + // The names come from the https://spdx.org/licenses website, and are + // also the filenames of the licenses in licenseclassifier/licenses. + AFL11 = "AFL-1.1" + AFL12 = "AFL-1.2" + AFL20 = "AFL-2.0" + AFL21 = "AFL-2.1" + AFL30 = "AFL-3.0" + AGPL10 = "AGPL-1.0" + AGPL30 = "AGPL-3.0" + Apache10 = "Apache-1.0" + Apache11 = "Apache-1.1" + Apache20 = "Apache-2.0" + APSL10 = "APSL-1.0" + APSL11 = "APSL-1.1" + APSL12 = "APSL-1.2" + APSL20 = "APSL-2.0" + Artistic10cl8 = "Artistic-1.0-cl8" + Artistic10Perl = "Artistic-1.0-Perl" + Artistic10 = "Artistic-1.0" + Artistic20 = "Artistic-2.0" + BCL = "BCL" + BSD1Clause = "BSD-1-Clause" + BSD2ClauseFreeBSD = "BSD-2-Clause-FreeBSD" + BSD2ClauseNetBSD = "BSD-2-Clause-NetBSD" + BSD2Clause = "BSD-2-Clause" + BSD3ClauseAttribution = "BSD-3-Clause-Attribution" + BSD3ClauseClear = "BSD-3-Clause-Clear" + BSD3ClauseLBNL = "BSD-3-Clause-LBNL" + BSD3Clause = "BSD-3-Clause" + BSD4Clause = "BSD-4-Clause" + BSD4ClauseUC = "BSD-4-Clause-UC" + BSDProtection = "BSD-Protection" + BSL10 = "BSL-1.0" + CC010 = "CC0-1.0" + CCBY10 = "CC-BY-1.0" + CCBY20 = "CC-BY-2.0" + CCBY25 = "CC-BY-2.5" + CCBY30 = "CC-BY-3.0" + CCBY40 = "CC-BY-4.0" + CCBYNC10 = "CC-BY-NC-1.0" + CCBYNC20 = "CC-BY-NC-2.0" + CCBYNC25 = "CC-BY-NC-2.5" + CCBYNC30 = "CC-BY-NC-3.0" + CCBYNC40 = "CC-BY-NC-4.0" + CCBYNCND10 = "CC-BY-NC-ND-1.0" + CCBYNCND20 = "CC-BY-NC-ND-2.0" + CCBYNCND25 = "CC-BY-NC-ND-2.5" + CCBYNCND30 = "CC-BY-NC-ND-3.0" + CCBYNCND40 = "CC-BY-NC-ND-4.0" + CCBYNCSA10 = "CC-BY-NC-SA-1.0" + CCBYNCSA20 = "CC-BY-NC-SA-2.0" + CCBYNCSA25 = "CC-BY-NC-SA-2.5" + CCBYNCSA30 = "CC-BY-NC-SA-3.0" + CCBYNCSA40 = "CC-BY-NC-SA-4.0" + CCBYND10 = "CC-BY-ND-1.0" + CCBYND20 = "CC-BY-ND-2.0" + CCBYND25 = "CC-BY-ND-2.5" + CCBYND30 = "CC-BY-ND-3.0" + CCBYND40 = "CC-BY-ND-4.0" + CCBYSA10 = "CC-BY-SA-1.0" + CCBYSA20 = "CC-BY-SA-2.0" + CCBYSA25 = "CC-BY-SA-2.5" + CCBYSA30 = "CC-BY-SA-3.0" + CCBYSA40 = "CC-BY-SA-4.0" + CDDL10 = "CDDL-1.0" + CDDL11 = "CDDL-1.1" + CommonsClause = "Commons-Clause" + CPAL10 = "CPAL-1.0" + CPL10 = "CPL-1.0" + EPL10 = "EPL-1.0" + EPL20 = "EPL-2.0" + EUPL10 = "EUPL-1.0" + EUPL11 = "EUPL-1.1" + Facebook2Clause = "Facebook-2-Clause" + Facebook3Clause = "Facebook-3-Clause" + FacebookExamples = "Facebook-Examples" + FreeImage = "FreeImage" + FTL = "FTL" + GFDL11WithInvariants = "GFDL-1.1-invariants" + GFDL11NoInvariants = "GFDL-1.1-no-invariants" + GFDL11 = "GFDL-1.1" + GFDL12WithInvariants = "GFDL-1.2-invariants" + GFDL12NoInvariants = "GFDL-1.2-no-invariants" + GFDL12 = "GFDL-1.2" + GFDL13WithInvariants = "GFDL-1.3-invariants" + GFDL13NoInvariants = "GFDL-1.3-no-invariants" + GFDL13 = "GFDL-1.3" + GPL10 = "GPL-1.0" + GPL20 = "GPL-2.0" + GPL20withautoconfexception = "GPL-2.0-with-autoconf-exception" + GPL20withbisonexception = "GPL-2.0-with-bison-exception" + GPL20withclasspathexception = "GPL-2.0-with-classpath-exception" + GPL20withfontexception = "GPL-2.0-with-font-exception" + GPL20withGCCexception = "GPL-2.0-with-GCC-exception" + GPL30 = "GPL-3.0" + GPL30withautoconfexception = "GPL-3.0-with-autoconf-exception" + GPL30withGCCexception = "GPL-3.0-with-GCC-exception" + GUSTFont = "GUST-Font-License" + ImageMagick = "ImageMagick" + IPL10 = "IPL-1.0" + ISC = "ISC" + LGPL20 = "LGPL-2.0" + LGPL21 = "LGPL-2.1" + LGPL30 = "LGPL-3.0" + Libpng = "Libpng" + Lil10 = "Lil-1.0" + LinuxOpenIB = "Linux-OpenIB" + LPL102 = "LPL-1.02" + LPL10 = "LPL-1.0" + MIT = "MIT" + MPL10 = "MPL-1.0" + MPL11 = "MPL-1.1" + MPL20 = "MPL-2.0" + MSPL = "MS-PL" + NCSA = "NCSA" + NPL10 = "NPL-1.0" + NPL11 = "NPL-1.1" + OFL11 = "OFL-1.1" + OpenSSL = "OpenSSL" + OpenVision = "OpenVision" + OSL10 = "OSL-1.0" + OSL11 = "OSL-1.1" + OSL20 = "OSL-2.0" + OSL21 = "OSL-2.1" + OSL30 = "OSL-3.0" + PHP301 = "PHP-3.01" + PHP30 = "PHP-3.0" + PIL = "PIL" + PostgreSQL = "PostgreSQL" + Python20complete = "Python-2.0-complete" + Python20 = "Python-2.0" + QPL10 = "QPL-1.0" + Ruby = "Ruby" + SGIB10 = "SGI-B-1.0" + SGIB11 = "SGI-B-1.1" + SGIB20 = "SGI-B-2.0" + SISSL = "SISSL" + Sleepycat = "Sleepycat" + UnicodeTOU = "Unicode-TOU" + UnicodeDFS2015 = "Unicode-DFS-2015" + UnicodeDFS2016 = "Unicode-DFS-2016" + Unlicense = "Unlicense" + UPL10 = "UPL-1.0" + W3C19980720 = "W3C-19980720" + W3C20150513 = "W3C-20150513" + W3C = "W3C" + WTFPL = "WTFPL" + X11 = "X11" + Xnet = "Xnet" + Zend20 = "Zend-2.0" + ZeroBSD = "0BSD" + ZlibAcknowledgement = "zlib-acknowledgement" + Zlib = "Zlib" + ZPL11 = "ZPL-1.1" + ZPL20 = "ZPL-2.0" + ZPL21 = "ZPL-2.1" +) + +// GNU licenses always use either the suffix “-only“ or the suffix “-or-later“ +// https://spdx.dev/learn/handling-license-info/ +var GnuLicenses = []string{ + AGPL10, + AGPL30, + GFDL11WithInvariants, + GFDL11NoInvariants, + GFDL11, + GFDL12WithInvariants, + GFDL12NoInvariants, + GFDL12, + GFDL13WithInvariants, + GFDL13NoInvariants, + GFDL13, + GPL10, + GPL20, + GPL30, + LGPL20, + LGPL21, + LGPL30, +} + +var ( + // ForbiddenLicenses - Licenses that are forbidden to be used. + // ported from https://github.com/google/licenseclassifier/blob/7c62d6fe8d3aa2f39c4affb58c9781d9dc951a2d/license_type.go#L340-L364 + ForbiddenLicenses = []string{ + AGPL10, + AGPL30, + CCBYNC10, + CCBYNC20, + CCBYNC25, + CCBYNC30, + CCBYNC40, + CCBYNCND10, + CCBYNCND20, + CCBYNCND25, + CCBYNCND30, + CCBYNCND40, + CCBYNCSA10, + CCBYNCSA20, + CCBYNCSA25, + CCBYNCSA30, + CCBYNCSA40, + CommonsClause, + Facebook2Clause, + Facebook3Clause, + FacebookExamples, + WTFPL, + } + + // RestrictedLicenses - Licenses in this category require mandatory source distribution if we ship a product + // that includes third-party code protected by such a license. + // ported from https://github.com/google/licenseclassifier/blob/7c62d6fe8d3aa2f39c4affb58c9781d9dc951a2d/license_type.go#L182-L219 + RestrictedLicenses = []string{ + BCL, + CCBYND10, + CCBYND20, + CCBYND25, + CCBYND30, + CCBYND40, + CCBYSA10, + CCBYSA20, + CCBYSA25, + CCBYSA30, + CCBYSA40, + GPL10, + GPL20, + GPL20withautoconfexception, + GPL20withbisonexception, + GPL20withclasspathexception, + GPL20withfontexception, + GPL20withGCCexception, + GPL30, + GPL30withautoconfexception, + GPL30withGCCexception, + LGPL20, + LGPL21, + LGPL30, + NPL10, + NPL11, + OSL10, + OSL11, + OSL20, + OSL21, + OSL30, + QPL10, + Sleepycat, + } + + // ReciprocalLicenses - These licenses allow usage of software made available under such licenses freely + // in *unmodified* form. If the third-party source code is modified in any way these modifications to the + // original third-party source code must be made available. + ReciprocalLicenses = []string{ + APSL10, + APSL11, + APSL12, + APSL20, + CDDL10, + CDDL11, + CPL10, + EPL10, + EPL20, + FreeImage, + IPL10, + MPL10, + MPL11, + MPL20, + Ruby, + } + + // NoticeLicenses - These licenses contain few restrictions, allowing original or modified third-party software + // to be shipped in any product without endangering or encumbering our source code. + // All of the licenses in this category do, however, have an "original Copyright notice" or "advertising clause", + // wherein any external distributions must include the notice or clause specified in the license. + NoticeLicenses = []string{ + AFL11, + AFL12, + AFL20, + AFL21, + AFL30, + Apache10, + Apache11, + Apache20, + Artistic10cl8, + Artistic10Perl, + Artistic10, + Artistic20, + BSL10, + BSD1Clause, + BSD2ClauseFreeBSD, + BSD2ClauseNetBSD, + BSD2Clause, + BSD3ClauseAttribution, + BSD3ClauseClear, + BSD3ClauseLBNL, + BSD3Clause, + BSD4Clause, + BSD4ClauseUC, + BSDProtection, + CCBY10, + CCBY20, + CCBY25, + CCBY30, + CCBY40, + FTL, + ISC, + ImageMagick, + Libpng, + Lil10, + LinuxOpenIB, + LPL102, + LPL10, + MSPL, + MIT, + NCSA, + OpenSSL, + PHP301, + PHP30, + PIL, + Python20, + Python20complete, + PostgreSQL, + SGIB10, + SGIB11, + SGIB20, + UnicodeDFS2015, + UnicodeDFS2016, + UnicodeTOU, + UPL10, + W3C19980720, + W3C20150513, + W3C, + X11, + Xnet, + Zend20, + ZlibAcknowledgement, + Zlib, + ZPL11, + ZPL20, + ZPL21, + } + + // PermissiveLicenses - These licenses can be used in (relatively rare) cases where third-party software is + // under a license (not "Public Domain" or "free for any use" like 'unencumbered') that is even more lenient + // than a 'notice' license. Use the 'permissive' license type when even a copyright notice is not required + // for license compliance. + PermissiveLicenses []string + + // UnencumberedLicenses - Licenses that basically declare that the code is "free for any use". + UnencumberedLicenses = []string{ + CC010, + Unlicense, + ZeroBSD, + } +) + +var spdxLicenses = set.NewCaseInsensitive() + +//go:embed licenses.json +var licenses []byte + +var initSpdxLicenses = sync.OnceFunc(func() { + if spdxLicenses.Size() > 0 { + return + } + + var lics []string + if err := json.Unmarshal(licenses, &lics); err != nil { + log.WithPrefix(log.PrefixSPDX).Warn("Unable to parse SPDX license file", log.Err(err)) + return + } + + // SPDX license list is case-insensitive. + spdxLicenses.Append(lics...) +}) + +//go:embed exceptions.json +var exceptions []byte + +var spdxExceptions map[string]SimpleExpr + +var initSpdxExceptions = sync.OnceFunc(func() { + if len(spdxExceptions) > 0 { + return + } + + var exs []string + if err := json.Unmarshal(exceptions, &exs); err != nil { + log.WithPrefix(log.PrefixSPDX).Warn("Unable to parse SPDX exception file", log.Err(err)) + return + } + spdxExceptions = lo.SliceToMap(exs, func(exception string) (string, SimpleExpr) { + return strings.ToUpper(exception), SimpleExpr{License: exception} + }) +}) + +// ValidateSPDXLicense returns true if SPDX license list contain licenseID +func ValidateSPDXLicense(license string) bool { + initSpdxLicenses() + + return spdxLicenses.Contains(license) +} + +// SPDXLicenseID returns the canonical (properly cased) SPDX license ID. +// Returns empty string and false if the license is not in the SPDX list +func SPDXLicenseID(license string) (string, bool) { + initSpdxLicenses() + return spdxLicenses.Find(license) +} + +// ValidateSPDXException returns true if SPDX exception list contain exceptionID +func ValidateSPDXException(exception string) bool { + initSpdxExceptions() + + _, ok := spdxExceptions[strings.ToUpper(exception)] + return ok +} diff --git a/pkg/licensing/expression/exceptions.json b/pkg/licensing/expression/exceptions.json new file mode 100644 index 000000000000..3e8b6bb7ba70 --- /dev/null +++ b/pkg/licensing/expression/exceptions.json @@ -0,0 +1 @@ +["389-exception","Asterisk-exception","Asterisk-linking-protocols-exception","Autoconf-exception-2.0","Autoconf-exception-3.0","Autoconf-exception-generic","Autoconf-exception-generic-3.0","Autoconf-exception-macro","Bison-exception-1.24","Bison-exception-2.2","Bootloader-exception","CGAL-linking-exception","CLISP-exception-2.0","Classpath-exception-2.0","DigiRule-FOSS-exception","Digia-Qt-LGPL-exception-1.1","FLTK-exception","Fawkes-Runtime-exception","Font-exception-2.0","GCC-exception-2.0","GCC-exception-2.0-note","GCC-exception-3.1","GNAT-exception","GNOME-examples-exception","GNU-compiler-exception","GPL-3.0-389-ds-base-exception","GPL-3.0-interface-exception","GPL-3.0-linking-exception","GPL-3.0-linking-source-exception","GPL-CC-1.0","GStreamer-exception-2005","GStreamer-exception-2008","Gmsh-exception","Independent-modules-exception","KiCad-libraries-exception","LGPL-3.0-linking-exception","LLGPL","LLVM-exception","LZMA-exception","Libtool-exception","Linux-syscall-note","Nokia-Qt-exception-1.1","OCCT-exception-1.0","OCaml-LGPL-linking-exception","OpenJDK-assembly-exception-1.0","PCRE2-exception","PS-or-PDF-font-exception-20170817","QPL-1.0-INRIA-2004-exception","Qt-GPL-exception-1.0","Qt-LGPL-exception-1.1","Qwt-exception-1.0","RRDtool-FLOSS-exception-2.0","SANE-exception","SHL-2.0","SHL-2.1","SWI-exception","Swift-exception","Texinfo-exception","UBDL-exception","Universal-FOSS-exception-1.0","WxWindows-exception-3.1","cryptsetup-OpenSSL-exception","eCos-exception-2.0","erlang-otp-linking-exception","fmt-exception","freertos-exception-2.0","gnu-javamail-exception","harbour-exception","i2p-gpl-java-exception","libpri-OpenH323-exception","mif-exception","mxml-exception","openvpn-openssl-exception","polyparse-exception","romic-exception","stunnel-exception","u-boot-exception-2.0","vsftpd-openssl-exception","x11vnc-openssl-exception"] \ No newline at end of file diff --git a/pkg/licensing/expression/expression.go b/pkg/licensing/expression/expression.go new file mode 100644 index 000000000000..48d944515948 --- /dev/null +++ b/pkg/licensing/expression/expression.go @@ -0,0 +1,96 @@ +package expression + +import ( + "strings" + "unicode" + + "golang.org/x/xerrors" +) + +var ( + ErrInvalidExpression = xerrors.New("invalid expression error") +) + +type NormalizeFunc func(license Expression) Expression + +func parse(license string) (Expression, error) { + l := NewLexer(strings.NewReader(license)) + if yyParse(l) != 0 { + return nil, xerrors.Errorf("license parse error: %w", l.Err()) + } else if err := l.Err(); err != nil { + return nil, err + } + + return l.result, nil +} + +func Normalize(license string, funcs ...NormalizeFunc) (Expression, error) { + expr, err := parse(license) + if err != nil { + return nil, xerrors.Errorf("license (%s) parse error: %w", license, err) + } + for _, fn := range funcs { + expr = normalize(expr, fn) + } + + return expr, nil +} + +func normalize(expr Expression, fn NormalizeFunc) Expression { + // Apply normalization function first + normalized := fn(expr) + + switch e := normalized.(type) { + case SimpleExpr: + // No further normalization for SimpleExpr + case CompoundExpr: + // Only recursively process if the result is a CompoundExpr + e.left = normalize(e.left, fn) + e.right = normalize(e.right, fn) + e.conjunction.literal = strings.ToUpper(e.conjunction.literal) // e.g. "and" => "AND" + return e + } + + return normalized +} + +// NormalizeForSPDX replaces ' ' to '-' in license-id. +// SPDX license MUST NOT have white space between a license-id. +// There MUST be white space on either side of the operator "WITH". +// ref: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions +func NormalizeForSPDX(expr Expression) Expression { + switch e := expr.(type) { + case SimpleExpr: + var b strings.Builder + for _, c := range e.License { + switch { + // spec: idstring = 1*(ALPHA / DIGIT / "-" / "." ) + case isAlphabet(c) || unicode.IsNumber(c) || c == '-' || c == '.': + _, _ = b.WriteRune(c) + case c == ':': + // TODO: Support DocumentRef + _, _ = b.WriteRune(c) + default: + // Replace invalid characters with '-' + _, _ = b.WriteRune('-') + } + } + return SimpleExpr{License: b.String(), HasPlus: e.HasPlus} + case CompoundExpr: + if e.Conjunction() == TokenWith { + initSpdxExceptions() + // Use correct SPDX exceptionID + if exc, ok := spdxExceptions[strings.ToUpper(e.Right().String())]; ok { + return NewCompoundExpr(e.Left(), e.Conjunction(), exc) + } + } + } + return expr +} + +func isAlphabet(r rune) bool { + if (r < 'a' || r > 'z') && (r < 'A' || r > 'Z') { + return false + } + return true +} diff --git a/pkg/licensing/expression/expression_test.go b/pkg/licensing/expression/expression_test.go new file mode 100644 index 000000000000..ae36f5185db7 --- /dev/null +++ b/pkg/licensing/expression/expression_test.go @@ -0,0 +1,56 @@ +package expression + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNormalize(t *testing.T) { + tests := []struct { + name string + license string + fn NormalizeFunc + want string + wantErr string + }{ + { + name: "SPDX, space", + license: "AFL 2.0", + fn: NormalizeForSPDX, + want: "AFL-2.0", + }, + { + name: "SPDX, exception", + license: "AFL 2.0 with Linux-syscall-note exception", + fn: NormalizeForSPDX, + want: "AFL-2.0 WITH Linux-syscall-note-exception", + }, + { + name: "SPDX, invalid chars", + license: "LGPL_2.1_only or MIT OR BSD-3>Clause", + fn: NormalizeForSPDX, + want: "LGPL-2.1-only OR MIT OR BSD-3-Clause", + }, + { + name: "upper", + license: "LGPL-2.1-only OR MIT", + fn: func(license Expression) Expression { return SimpleExpr{strings.ToUpper(license.String()), false} }, + want: "LGPL-2.1-ONLY OR MIT", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := Normalize(tt.license, tt.fn) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + assert.Equalf(t, tt.want, got.String(), "NormalizeWithExpression(%v)", tt.license) + }) + } +} diff --git a/pkg/licensing/expression/lexer.go b/pkg/licensing/expression/lexer.go new file mode 100644 index 000000000000..64b8d17ef405 --- /dev/null +++ b/pkg/licensing/expression/lexer.go @@ -0,0 +1,119 @@ +package expression + +import ( + "bufio" + "errors" + "io" + "strings" + "unicode" + "unicode/utf8" + + multierror "github.com/hashicorp/go-multierror" +) + +type Lexer struct { + s *bufio.Scanner + result Expression + errs error +} + +func NewLexer(reader io.Reader) *Lexer { + scanner := bufio.NewScanner(reader) + scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { + // The implementation references bufio.ScanWords() + + // Skip leading spaces. + start := 0 + for width := 0; start < len(data); start += width { + var r rune + r, width = utf8.DecodeRune(data[start:]) + if !unicode.IsSpace(r) { + break + } + } + // Process terminal symbols + if len(data) > start && (data[start] == '(' || data[start] == ')' || data[start] == '+') { + return start + 1, data[start : start+1], nil + } + + // Scan until space or token, marking end of word. + for width, i := 0, start; i < len(data); i += width { + var r rune + r, width = utf8.DecodeRune(data[i:]) + switch r { + case '(', ')': + return i, data[start:i], nil + case '+': + // Peek the next rune + if len(data) > i+width { + adv := i + i += width + r, width = utf8.DecodeRune(data[i:]) + if unicode.IsSpace(r) || r == '(' || r == ')' { + return adv, data[start:adv], nil + } + } else if atEOF { + return i, data[start:i], nil + } + default: + if unicode.IsSpace(r) { + return i + width, data[start:i], nil + } + } + } + // If we're at EOF, we have a final, non-empty, non-terminated word. Return it. + if atEOF && len(data) > start { + return len(data), data[start:], nil + } + // Request more data. + return start, nil, nil + }) + + return &Lexer{ + s: scanner, + } +} + +func (l *Lexer) Lex(lval *yySymType) int { + if !l.s.Scan() { + return 0 + } + + var token int + literal := l.s.Text() + switch literal { + case "(", ")", "+": + token = int(literal[0]) + default: + token = lookup(literal) + } + + lval.token = Token{ + token: token, + literal: literal, + } + + if err := l.s.Err(); err != nil { + l.errs = multierror.Append(l.errs, l.s.Err()) + } + + return lval.token.token +} + +func (l *Lexer) Error(e string) { + l.errs = multierror.Append(l.errs, errors.New(e)) +} + +func (l *Lexer) Err() error { + return l.errs +} + +func lookup(t string) int { + t = strings.ToUpper(t) + for i, name := range yyToknames { + if t == name { + return yyPrivate + (i - 1) + } + } + return IDENT +} diff --git a/pkg/licensing/expression/lexer_test.go b/pkg/licensing/expression/lexer_test.go new file mode 100644 index 000000000000..d9492fb80465 --- /dev/null +++ b/pkg/licensing/expression/lexer_test.go @@ -0,0 +1,239 @@ +package expression + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestLexer_Lex(t *testing.T) { + tests := []struct { + name string + input string + want []Token + }{ + { + name: "simple", + input: "GPL-2.0-only", + want: []Token{ + { + token: IDENT, + literal: "GPL-2.0-only", + }, + }, + }, + { + name: "with space", + input: "Public Domain", + want: []Token{ + { + token: IDENT, + literal: "Public", + }, + { + token: IDENT, + literal: "Domain", + }, + }, + }, + { + name: "and", + input: "Public Domain AND MIT", + want: []Token{ + { + token: IDENT, + literal: "Public", + }, + { + token: IDENT, + literal: "Domain", + }, + { + token: AND, + literal: "AND", + }, + { + token: IDENT, + literal: "MIT", + }, + }, + }, + { + name: "or", + input: "LGPL-2.1-only OR MIT OR BSD-3-Clause", + want: []Token{ + { + token: IDENT, + literal: "LGPL-2.1-only", + }, + { + token: OR, + literal: "OR", + }, + { + token: IDENT, + literal: "MIT", + }, + { + token: OR, + literal: "OR", + }, + { + token: IDENT, + literal: "BSD-3-Clause", + }, + }, + }, + { + name: "parenthesis", + input: "LGPL-2.1-only AND (MIT OR BSD-3-Clause)", + want: []Token{ + { + token: IDENT, + literal: "LGPL-2.1-only", + }, + { + token: AND, + literal: "AND", + }, + { + token: int('('), + literal: "(", + }, + { + token: IDENT, + literal: "MIT", + }, + { + token: OR, + literal: "OR", + }, + { + token: IDENT, + literal: "BSD-3-Clause", + }, + { + token: int(')'), + literal: ")", + }, + }, + }, + { + name: "exception", + input: "LGPL-2.1-only AND GPL-2.0-or-later WITH Bison-exception-2.2", + want: []Token{ + { + token: IDENT, + literal: "LGPL-2.1-only", + }, + { + token: AND, + literal: "AND", + }, + { + token: IDENT, + literal: "GPL-2.0-or-later", + }, + { + token: WITH, + literal: "WITH", + }, + { + token: IDENT, + literal: "Bison-exception-2.2", + }, + }, + }, + { + name: "plus", + input: "Public Domain+", + want: []Token{ + { + token: IDENT, + literal: "Public", + }, + { + token: IDENT, + literal: "Domain", + }, + { + token: int('+'), + literal: "+", + }, + }, + }, + { + name: "plus in the middle", + input: "ISC+IBM", + want: []Token{ + { + token: IDENT, + literal: "ISC+IBM", + }, + }, + }, + { + name: "plus with the parenthesis", + input: "(GPL1.0+)", + want: []Token{ + { + token: int('('), + literal: "(", + }, + { + token: IDENT, + literal: "GPL1.0", + }, + { + token: int('+'), + literal: "+", + }, + { + token: int(')'), + literal: ")", + }, + }, + }, + { + name: "utf-8", + input: "GPL1.0+ " + string(byte(0x20)) + "あ🇯🇵" + " and LGPL1.0", + want: []Token{ + { + token: IDENT, + literal: "GPL1.0", + }, + { + token: int('+'), + literal: "+", + }, + { + token: IDENT, + literal: "あ🇯🇵", + }, + { + token: AND, + literal: "and", + }, + { + token: IDENT, + literal: "LGPL1.0", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := NewLexer(strings.NewReader(tt.input)) + var got []Token + var lval yySymType + for l.Lex(&lval) != 0 { + got = append(got, lval.token) + lval = yySymType{} + } + require.NoError(t, l.Err()) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/licensing/expression/licenses.json b/pkg/licensing/expression/licenses.json new file mode 100644 index 000000000000..ad139ecac452 --- /dev/null +++ b/pkg/licensing/expression/licenses.json @@ -0,0 +1 @@ +["0BSD","3D-Slicer-1.0","AAL","ADSL","AFL-1.1","AFL-1.2","AFL-2.0","AFL-2.1","AFL-3.0","AGPL-1.0","AGPL-1.0-only","AGPL-1.0-or-later","AGPL-3.0","AGPL-3.0-only","AGPL-3.0-or-later","AMD-newlib","AMDPLPA","AML","AML-glslang","AMPAS","ANTLR-PD","ANTLR-PD-fallback","APAFML","APL-1.0","APSL-1.0","APSL-1.1","APSL-1.2","APSL-2.0","ASWF-Digital-Assets-1.0","ASWF-Digital-Assets-1.1","Abstyles","AdaCore-doc","Adobe-2006","Adobe-Display-PostScript","Adobe-Glyph","Adobe-Utopia","Afmparse","Aladdin","Apache-1.0","Apache-1.1","Apache-2.0","App-s2p","Arphic-1999","Artistic-1.0","Artistic-1.0-Perl","Artistic-1.0-cl8","Artistic-2.0","Artistic-dist","Aspell-RU","BSD-1-Clause","BSD-2-Clause","BSD-2-Clause-Darwin","BSD-2-Clause-FreeBSD","BSD-2-Clause-NetBSD","BSD-2-Clause-Patent","BSD-2-Clause-Views","BSD-2-Clause-first-lines","BSD-2-Clause-pkgconf-disclaimer","BSD-3-Clause","BSD-3-Clause-Attribution","BSD-3-Clause-Clear","BSD-3-Clause-HP","BSD-3-Clause-LBNL","BSD-3-Clause-Modification","BSD-3-Clause-No-Military-License","BSD-3-Clause-No-Nuclear-License","BSD-3-Clause-No-Nuclear-License-2014","BSD-3-Clause-No-Nuclear-Warranty","BSD-3-Clause-Open-MPI","BSD-3-Clause-Sun","BSD-3-Clause-acpica","BSD-3-Clause-flex","BSD-4-Clause","BSD-4-Clause-Shortened","BSD-4-Clause-UC","BSD-4.3RENO","BSD-4.3TAHOE","BSD-Advertising-Acknowledgement","BSD-Attribution-HPND-disclaimer","BSD-Inferno-Nettverk","BSD-Protection","BSD-Source-Code","BSD-Source-beginning-file","BSD-Systemics","BSD-Systemics-W3Works","BSL-1.0","BUSL-1.1","Baekmuk","Bahyph","Barr","Beerware","BitTorrent-1.0","BitTorrent-1.1","Bitstream-Charter","Bitstream-Vera","BlueOak-1.0.0","Boehm-GC","Boehm-GC-without-fee","Borceux","Brian-Gladman-2-Clause","Brian-Gladman-3-Clause","C-UDA-1.0","CAL-1.0","CAL-1.0-Combined-Work-Exception","CATOSL-1.1","CC-BY-1.0","CC-BY-2.0","CC-BY-2.5","CC-BY-2.5-AU","CC-BY-3.0","CC-BY-3.0-AT","CC-BY-3.0-AU","CC-BY-3.0-DE","CC-BY-3.0-IGO","CC-BY-3.0-NL","CC-BY-3.0-US","CC-BY-4.0","CC-BY-NC-1.0","CC-BY-NC-2.0","CC-BY-NC-2.5","CC-BY-NC-3.0","CC-BY-NC-3.0-DE","CC-BY-NC-4.0","CC-BY-NC-ND-1.0","CC-BY-NC-ND-2.0","CC-BY-NC-ND-2.5","CC-BY-NC-ND-3.0","CC-BY-NC-ND-3.0-DE","CC-BY-NC-ND-3.0-IGO","CC-BY-NC-ND-4.0","CC-BY-NC-SA-1.0","CC-BY-NC-SA-2.0","CC-BY-NC-SA-2.0-DE","CC-BY-NC-SA-2.0-FR","CC-BY-NC-SA-2.0-UK","CC-BY-NC-SA-2.5","CC-BY-NC-SA-3.0","CC-BY-NC-SA-3.0-DE","CC-BY-NC-SA-3.0-IGO","CC-BY-NC-SA-4.0","CC-BY-ND-1.0","CC-BY-ND-2.0","CC-BY-ND-2.5","CC-BY-ND-3.0","CC-BY-ND-3.0-DE","CC-BY-ND-4.0","CC-BY-SA-1.0","CC-BY-SA-2.0","CC-BY-SA-2.0-UK","CC-BY-SA-2.1-JP","CC-BY-SA-2.5","CC-BY-SA-3.0","CC-BY-SA-3.0-AT","CC-BY-SA-3.0-DE","CC-BY-SA-3.0-IGO","CC-BY-SA-4.0","CC-PDDC","CC-PDM-1.0","CC-SA-1.0","CC0-1.0","CDDL-1.0","CDDL-1.1","CDL-1.0","CDLA-Permissive-1.0","CDLA-Permissive-2.0","CDLA-Sharing-1.0","CECILL-1.0","CECILL-1.1","CECILL-2.0","CECILL-2.1","CECILL-B","CECILL-C","CERN-OHL-1.1","CERN-OHL-1.2","CERN-OHL-P-2.0","CERN-OHL-S-2.0","CERN-OHL-W-2.0","CFITSIO","CMU-Mach","CMU-Mach-nodoc","CNRI-Jython","CNRI-Python","CNRI-Python-GPL-Compatible","COIL-1.0","CPAL-1.0","CPL-1.0","CPOL-1.02","CUA-OPL-1.0","Caldera","Caldera-no-preamble","Catharon","ClArtistic","Clips","Community-Spec-1.0","Condor-1.1","Cornell-Lossless-JPEG","Cronyx","Crossword","CryptoSwift","CrystalStacker","Cube","D-FSL-1.0","DEC-3-Clause","DL-DE-BY-2.0","DL-DE-ZERO-2.0","DOC","DRL-1.0","DRL-1.1","DSDP","DocBook-DTD","DocBook-Schema","DocBook-Stylesheet","DocBook-XML","Dotseqn","ECL-1.0","ECL-2.0","EFL-1.0","EFL-2.0","EPICS","EPL-1.0","EPL-2.0","EUDatagrid","EUPL-1.0","EUPL-1.1","EUPL-1.2","Elastic-2.0","Entessa","ErlPL-1.1","Eurosym","FBM","FDK-AAC","FSFAP","FSFAP-no-warranty-disclaimer","FSFUL","FSFULLR","FSFULLRSD","FSFULLRWD","FSL-1.1-ALv2","FSL-1.1-MIT","FTL","Fair","Ferguson-Twofish","Frameworx-1.0","FreeBSD-DOC","FreeImage","Furuseth","GCR-docs","GD","GFDL-1.1","GFDL-1.1-invariants-only","GFDL-1.1-invariants-or-later","GFDL-1.1-no-invariants-only","GFDL-1.1-no-invariants-or-later","GFDL-1.1-only","GFDL-1.1-or-later","GFDL-1.2","GFDL-1.2-invariants-only","GFDL-1.2-invariants-or-later","GFDL-1.2-no-invariants-only","GFDL-1.2-no-invariants-or-later","GFDL-1.2-only","GFDL-1.2-or-later","GFDL-1.3","GFDL-1.3-invariants-only","GFDL-1.3-invariants-or-later","GFDL-1.3-no-invariants-only","GFDL-1.3-no-invariants-or-later","GFDL-1.3-only","GFDL-1.3-or-later","GL2PS","GLWTPL","GPL-1.0","GPL-1.0+","GPL-1.0-only","GPL-1.0-or-later","GPL-2.0","GPL-2.0+","GPL-2.0-only","GPL-2.0-or-later","GPL-2.0-with-GCC-exception","GPL-2.0-with-autoconf-exception","GPL-2.0-with-bison-exception","GPL-2.0-with-classpath-exception","GPL-2.0-with-font-exception","GPL-3.0","GPL-3.0+","GPL-3.0-only","GPL-3.0-or-later","GPL-3.0-with-GCC-exception","GPL-3.0-with-autoconf-exception","Game-Programming-Gems","Giftware","Glide","Glulxe","Graphics-Gems","Gutmann","HDF5","HIDAPI","HP-1986","HP-1989","HPND","HPND-DEC","HPND-Fenneberg-Livingston","HPND-INRIA-IMAG","HPND-Intel","HPND-Kevlin-Henney","HPND-MIT-disclaimer","HPND-Markus-Kuhn","HPND-Netrek","HPND-Pbmplus","HPND-UC","HPND-UC-export-US","HPND-doc","HPND-doc-sell","HPND-export-US","HPND-export-US-acknowledgement","HPND-export-US-modify","HPND-export2-US","HPND-merchantability-variant","HPND-sell-MIT-disclaimer-xserver","HPND-sell-regexpr","HPND-sell-variant","HPND-sell-variant-MIT-disclaimer","HPND-sell-variant-MIT-disclaimer-rev","HTMLTIDY","HaskellReport","Hippocratic-2.1","IBM-pibs","ICU","IEC-Code-Components-EULA","IJG","IJG-short","IPA","IPL-1.0","ISC","ISC-Veillard","ImageMagick","Imlib2","Info-ZIP","Inner-Net-2.0","InnoSetup","Intel","Intel-ACPI","Interbase-1.0","JPL-image","JPNIC","JSON","Jam","JasPer-2.0","Kastrup","Kazlib","Knuth-CTAN","LAL-1.2","LAL-1.3","LGPL-2.0","LGPL-2.0+","LGPL-2.0-only","LGPL-2.0-or-later","LGPL-2.1","LGPL-2.1+","LGPL-2.1-only","LGPL-2.1-or-later","LGPL-3.0","LGPL-3.0+","LGPL-3.0-only","LGPL-3.0-or-later","LGPLLR","LOOP","LPD-document","LPL-1.0","LPL-1.02","LPPL-1.0","LPPL-1.1","LPPL-1.2","LPPL-1.3a","LPPL-1.3c","LZMA-SDK-9.11-to-9.20","LZMA-SDK-9.22","Latex2e","Latex2e-translated-notice","Leptonica","LiLiQ-P-1.1","LiLiQ-R-1.1","LiLiQ-Rplus-1.1","Libpng","Linux-OpenIB","Linux-man-pages-1-para","Linux-man-pages-copyleft","Linux-man-pages-copyleft-2-para","Linux-man-pages-copyleft-var","Lucida-Bitmap-Fonts","MIPS","MIT","MIT-0","MIT-CMU","MIT-Click","MIT-Festival","MIT-Khronos-old","MIT-Modern-Variant","MIT-Wu","MIT-advertising","MIT-enna","MIT-feh","MIT-open-group","MIT-testregex","MITNFA","MMIXware","MPEG-SSG","MPL-1.0","MPL-1.1","MPL-2.0","MPL-2.0-no-copyleft-exception","MS-LPL","MS-PL","MS-RL","MTLL","Mackerras-3-Clause","Mackerras-3-Clause-acknowledgment","MakeIndex","Martin-Birgmeier","McPhee-slideshow","Minpack","MirOS","Motosoto","MulanPSL-1.0","MulanPSL-2.0","Multics","Mup","NAIST-2003","NASA-1.3","NBPL-1.0","NCBI-PD","NCGL-UK-2.0","NCL","NCSA","NGPL","NICTA-1.0","NIST-PD","NIST-PD-fallback","NIST-Software","NLOD-1.0","NLOD-2.0","NLPL","NOSL","NPL-1.0","NPL-1.1","NPOSL-3.0","NRL","NTIA-PD","NTP","NTP-0","Naumen","Net-SNMP","NetCDF","Newsletr","Nokia","Noweb","Nunit","O-UDA-1.0","OAR","OCCT-PL","OCLC-2.0","ODC-By-1.0","ODbL-1.0","OFFIS","OFL-1.0","OFL-1.0-RFN","OFL-1.0-no-RFN","OFL-1.1","OFL-1.1-RFN","OFL-1.1-no-RFN","OGC-1.0","OGDL-Taiwan-1.0","OGL-Canada-2.0","OGL-UK-1.0","OGL-UK-2.0","OGL-UK-3.0","OGTSL","OLDAP-1.1","OLDAP-1.2","OLDAP-1.3","OLDAP-1.4","OLDAP-2.0","OLDAP-2.0.1","OLDAP-2.1","OLDAP-2.2","OLDAP-2.2.1","OLDAP-2.2.2","OLDAP-2.3","OLDAP-2.4","OLDAP-2.5","OLDAP-2.6","OLDAP-2.7","OLDAP-2.8","OLFL-1.3","OML","OPL-1.0","OPL-UK-3.0","OPUBL-1.0","OSET-PL-2.1","OSL-1.0","OSL-1.1","OSL-2.0","OSL-2.1","OSL-3.0","OpenPBS-2.3","OpenSSL","OpenSSL-standalone","OpenVision","PADL","PDDL-1.0","PHP-3.0","PHP-3.01","PPL","PSF-2.0","Parity-6.0.0","Parity-7.0.0","Pixar","Plexus","PolyForm-Noncommercial-1.0.0","PolyForm-Small-Business-1.0.0","PostgreSQL","Python-2.0","Python-2.0.1","QPL-1.0","QPL-1.0-INRIA-2004","Qhull","RHeCos-1.1","RPL-1.1","RPL-1.5","RPSL-1.0","RSA-MD","RSCPL","Rdisc","Ruby","Ruby-pty","SAX-PD","SAX-PD-2.0","SCEA","SGI-B-1.0","SGI-B-1.1","SGI-B-2.0","SGI-OpenGL","SGP4","SHL-0.5","SHL-0.51","SISSL","SISSL-1.2","SL","SMAIL-GPL","SMLNJ","SMPPL","SNIA","SOFA","SPL-1.0","SSH-OpenSSH","SSH-short","SSLeay-standalone","SSPL-1.0","SUL-1.0","SWL","Saxpath","SchemeReport","Sendmail","Sendmail-8.23","Sendmail-Open-Source-1.1","SimPL-2.0","Sleepycat","Soundex","Spencer-86","Spencer-94","Spencer-99","StandardML-NJ","SugarCRM-1.1.3","Sun-PPP","Sun-PPP-2000","SunPro","Symlinks","TAPR-OHL-1.0","TCL","TCP-wrappers","TGPPL-1.0","TMate","TORQUE-1.1","TOSL","TPDL","TPL-1.0","TTWL","TTYP0","TU-Berlin-1.0","TU-Berlin-2.0","TermReadKey","ThirdEye","TrustedQSL","UCAR","UCL-1.0","UMich-Merit","UPL-1.0","URT-RLE","Ubuntu-font-1.0","Unicode-3.0","Unicode-DFS-2015","Unicode-DFS-2016","Unicode-TOU","UnixCrypt","Unlicense","Unlicense-libtelnet","Unlicense-libwhirlpool","VOSTROM","VSL-1.0","Vim","W3C","W3C-19980720","W3C-20150513","WTFPL","Watcom-1.0","Widget-Workshop","Wsuipa","X11","X11-distribute-modifications-variant","X11-swapped","XFree86-1.1","XSkat","Xdebug-1.03","Xerox","Xfig","Xnet","YPL-1.0","YPL-1.1","ZPL-1.1","ZPL-2.0","ZPL-2.1","Zed","Zeeff","Zend-2.0","Zimbra-1.3","Zimbra-1.4","Zlib","any-OSI","any-OSI-perl-modules","bcrypt-Solar-Designer","blessing","bzip2-1.0.5","bzip2-1.0.6","check-cvs","checkmk","copyleft-next-0.3.0","copyleft-next-0.3.1","curl","cve-tou","diffmark","dtoa","dvipdfm","eCos-2.0","eGenix","etalab-2.0","fwlw","gSOAP-1.3b","generic-xts","gnuplot","gtkbook","hdparm","iMatix","jove","libpng-1.6.35","libpng-2.0","libselinux-1.0","libtiff","libutil-David-Nugent","lsof","magaz","mailprio","man2html","metamail","mpi-permissive","mpich2","mplus","ngrep","pkgconf","pnmstitch","psfrag","psutils","python-ldap","radvd","snprintf","softSurfer","ssh-keyscan","swrule","threeparttable","ulem","w3m","wwl","wxWindows","xinetd","xkeyboard-config-Zinoviev","xlock","xpp","xzoom","zlib-acknowledgement"] \ No newline at end of file diff --git a/pkg/licensing/expression/parser.go b/pkg/licensing/expression/parser.go new file mode 100644 index 000000000000..ef80d948f9cc --- /dev/null +++ b/pkg/licensing/expression/parser.go @@ -0,0 +1,2 @@ +//go:generate goyacc -o parser_gen.go parser.go.y +package expression diff --git a/pkg/licensing/expression/parser.go.y b/pkg/licensing/expression/parser.go.y new file mode 100644 index 000000000000..85a90a5a73ce --- /dev/null +++ b/pkg/licensing/expression/parser.go.y @@ -0,0 +1,73 @@ +%{ +package expression +%} + +%union{ + token Token + expr Expression +} + +%type license +%type simple +%type plus +%type compound +%token IDENT OR AND WITH + +%left OR +%left AND +%right WITH +%right '+' + +%% + +license + : compound + { + $$ = $1 + if l, ok := yylex.(*Lexer); ok{ + l.result = $$ + } + } + +simple + : IDENT + { + $$ = SimpleExpr{License: $1.literal} + } + | simple IDENT /* e.g. Public Domain */ + { + $$ = SimpleExpr{License: $1.String() + " " + $2.literal} + } + +plus + : simple '+' + { + $$ = SimpleExpr{License: $1.String(), HasPlus: true} + } + +compound + : simple { + $$ = $1 + } + | plus { + $$ = $1 + } + | compound AND compound /* compound-expression "AND" compound-expression */ + { + $$ = CompoundExpr{left: $1, conjunction: $2, right: $3} + } + | compound OR compound /* compound-expression "OR" compound-expression */ + { + $$ = CompoundExpr{left: $1, conjunction: $2, right: $3} + } + | compound WITH compound /* simple-expression "WITH" license-exception-id */ + { + $$ = CompoundExpr{left: $1, conjunction: $2, right: $3} + } + | '(' compound ')' + { + $$ = $2 + } + + +%% \ No newline at end of file diff --git a/pkg/licensing/expression/parser_gen.go b/pkg/licensing/expression/parser_gen.go new file mode 100644 index 000000000000..57c51b320e5d --- /dev/null +++ b/pkg/licensing/expression/parser_gen.go @@ -0,0 +1,507 @@ +// Code generated by goyacc -o parser_gen.go parser.go.y. DO NOT EDIT. + +//line parser.go.y:2 +package expression + +import __yyfmt__ "fmt" + +//line parser.go.y:2 + +//line parser.go.y:5 +type yySymType struct { + yys int + token Token + expr Expression +} + +const IDENT = 57346 +const OR = 57347 +const AND = 57348 +const WITH = 57349 + +var yyToknames = [...]string{ + "$end", + "error", + "$unk", + "IDENT", + "OR", + "AND", + "WITH", + "'+'", + "'('", + "')'", +} + +var yyStatenames = [...]string{} + +const yyEofCode = 1 +const yyErrCode = 2 +const yyInitialStackSize = 16 + +//line parser.go.y:73 + +//line yacctab:1 +var yyExca = [...]int8{ + -1, 1, + 1, -1, + -2, 0, +} + +const yyPrivate = 57344 + +const yyLast = 22 + +var yyAct = [...]int8{ + 8, 7, 9, 2, 10, 16, 9, 4, 11, 12, + 6, 13, 14, 15, 3, 5, 8, 7, 9, 7, + 9, 1, +} + +var yyPact = [...]int16{ + 6, -1000, 11, 0, -1000, 6, -1000, 6, 6, 6, + -1000, -1000, -5, -1, 13, -1, -1000, +} + +var yyPgo = [...]int8{ + 0, 21, 14, 7, 3, +} + +var yyR1 = [...]int8{ + 0, 1, 2, 2, 3, 4, 4, 4, 4, 4, + 4, +} + +var yyR2 = [...]int8{ + 0, 1, 1, 2, 2, 1, 1, 3, 3, 3, + 3, +} + +var yyChk = [...]int16{ + -1000, -1, -4, -2, -3, 9, 4, 6, 5, 7, + 4, 8, -4, -4, -4, -4, 10, +} + +var yyDef = [...]int8{ + 0, -2, 1, 5, 6, 0, 2, 0, 0, 0, + 3, 4, 0, 7, 8, 9, 10, +} + +var yyTok1 = [...]int8{ + 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, + 9, 10, 3, 8, +} + +var yyTok2 = [...]int8{ + 2, 3, 4, 5, 6, 7, +} + +var yyTok3 = [...]int8{ + 0, +} + +var yyErrorMessages = [...]struct { + state int + token int + msg string +}{} + +//line yaccpar:1 + +/* parser for yacc output */ + +var ( + yyDebug = 0 + yyErrorVerbose = false +) + +type yyLexer interface { + Lex(lval *yySymType) int + Error(s string) +} + +type yyParser interface { + Parse(yyLexer) int + Lookahead() int +} + +type yyParserImpl struct { + lval yySymType + stack [yyInitialStackSize]yySymType + char int +} + +func (p *yyParserImpl) Lookahead() int { + return p.char +} + +func yyNewParser() yyParser { + return &yyParserImpl{} +} + +const yyFlag = -1000 + +func yyTokname(c int) string { + if c >= 1 && c-1 < len(yyToknames) { + if yyToknames[c-1] != "" { + return yyToknames[c-1] + } + } + return __yyfmt__.Sprintf("tok-%v", c) +} + +func yyStatname(s int) string { + if s >= 0 && s < len(yyStatenames) { + if yyStatenames[s] != "" { + return yyStatenames[s] + } + } + return __yyfmt__.Sprintf("state-%v", s) +} + +func yyErrorMessage(state, lookAhead int) string { + const TOKSTART = 4 + + if !yyErrorVerbose { + return "syntax error" + } + + for _, e := range yyErrorMessages { + if e.state == state && e.token == lookAhead { + return "syntax error: " + e.msg + } + } + + res := "syntax error: unexpected " + yyTokname(lookAhead) + + // To match Bison, suggest at most four expected tokens. + expected := make([]int, 0, 4) + + // Look for shiftable tokens. + base := int(yyPact[state]) + for tok := TOKSTART; tok-1 < len(yyToknames); tok++ { + if n := base + tok; n >= 0 && n < yyLast && int(yyChk[int(yyAct[n])]) == tok { + if len(expected) == cap(expected) { + return res + } + expected = append(expected, tok) + } + } + + if yyDef[state] == -2 { + i := 0 + for yyExca[i] != -1 || int(yyExca[i+1]) != state { + i += 2 + } + + // Look for tokens that we accept or reduce. + for i += 2; yyExca[i] >= 0; i += 2 { + tok := int(yyExca[i]) + if tok < TOKSTART || yyExca[i+1] == 0 { + continue + } + if len(expected) == cap(expected) { + return res + } + expected = append(expected, tok) + } + + // If the default action is to accept or reduce, give up. + if yyExca[i+1] != 0 { + return res + } + } + + for i, tok := range expected { + if i == 0 { + res += ", expecting " + } else { + res += " or " + } + res += yyTokname(tok) + } + return res +} + +func yylex1(lex yyLexer, lval *yySymType) (char, token int) { + token = 0 + char = lex.Lex(lval) + if char <= 0 { + token = int(yyTok1[0]) + goto out + } + if char < len(yyTok1) { + token = int(yyTok1[char]) + goto out + } + if char >= yyPrivate { + if char < yyPrivate+len(yyTok2) { + token = int(yyTok2[char-yyPrivate]) + goto out + } + } + for i := 0; i < len(yyTok3); i += 2 { + token = int(yyTok3[i+0]) + if token == char { + token = int(yyTok3[i+1]) + goto out + } + } + +out: + if token == 0 { + token = int(yyTok2[1]) /* unknown char */ + } + if yyDebug >= 3 { + __yyfmt__.Printf("lex %s(%d)\n", yyTokname(token), uint(char)) + } + return char, token +} + +func yyParse(yylex yyLexer) int { + return yyNewParser().Parse(yylex) +} + +func (yyrcvr *yyParserImpl) Parse(yylex yyLexer) int { + var yyn int + var yyVAL yySymType + var yyDollar []yySymType + _ = yyDollar // silence set and not used + yyS := yyrcvr.stack[:] + + Nerrs := 0 /* number of errors */ + Errflag := 0 /* error recovery flag */ + yystate := 0 + yyrcvr.char = -1 + yytoken := -1 // yyrcvr.char translated into internal numbering + defer func() { + // Make sure we report no lookahead when not parsing. + yystate = -1 + yyrcvr.char = -1 + yytoken = -1 + }() + yyp := -1 + goto yystack + +ret0: + return 0 + +ret1: + return 1 + +yystack: + /* put a state and value onto the stack */ + if yyDebug >= 4 { + __yyfmt__.Printf("char %v in %v\n", yyTokname(yytoken), yyStatname(yystate)) + } + + yyp++ + if yyp >= len(yyS) { + nyys := make([]yySymType, len(yyS)*2) + copy(nyys, yyS) + yyS = nyys + } + yyS[yyp] = yyVAL + yyS[yyp].yys = yystate + +yynewstate: + yyn = int(yyPact[yystate]) + if yyn <= yyFlag { + goto yydefault /* simple state */ + } + if yyrcvr.char < 0 { + yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) + } + yyn += yytoken + if yyn < 0 || yyn >= yyLast { + goto yydefault + } + yyn = int(yyAct[yyn]) + if int(yyChk[yyn]) == yytoken { /* valid shift */ + yyrcvr.char = -1 + yytoken = -1 + yyVAL = yyrcvr.lval + yystate = yyn + if Errflag > 0 { + Errflag-- + } + goto yystack + } + +yydefault: + /* default state action */ + yyn = int(yyDef[yystate]) + if yyn == -2 { + if yyrcvr.char < 0 { + yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval) + } + + /* look through exception table */ + xi := 0 + for { + if yyExca[xi+0] == -1 && int(yyExca[xi+1]) == yystate { + break + } + xi += 2 + } + for xi += 2; ; xi += 2 { + yyn = int(yyExca[xi+0]) + if yyn < 0 || yyn == yytoken { + break + } + } + yyn = int(yyExca[xi+1]) + if yyn < 0 { + goto ret0 + } + } + if yyn == 0 { + /* error ... attempt to resume parsing */ + switch Errflag { + case 0: /* brand new error */ + yylex.Error(yyErrorMessage(yystate, yytoken)) + Nerrs++ + if yyDebug >= 1 { + __yyfmt__.Printf("%s", yyStatname(yystate)) + __yyfmt__.Printf(" saw %s\n", yyTokname(yytoken)) + } + fallthrough + + case 1, 2: /* incompletely recovered error ... try again */ + Errflag = 3 + + /* find a state where "error" is a legal shift action */ + for yyp >= 0 { + yyn = int(yyPact[yyS[yyp].yys]) + yyErrCode + if yyn >= 0 && yyn < yyLast { + yystate = int(yyAct[yyn]) /* simulate a shift of "error" */ + if int(yyChk[yystate]) == yyErrCode { + goto yystack + } + } + + /* the current p has no shift on "error", pop stack */ + if yyDebug >= 2 { + __yyfmt__.Printf("error recovery pops state %d\n", yyS[yyp].yys) + } + yyp-- + } + /* there is no state on the stack with an error shift ... abort */ + goto ret1 + + case 3: /* no shift yet; clobber input char */ + if yyDebug >= 2 { + __yyfmt__.Printf("error recovery discards %s\n", yyTokname(yytoken)) + } + if yytoken == yyEofCode { + goto ret1 + } + yyrcvr.char = -1 + yytoken = -1 + goto yynewstate /* try again in the same state */ + } + } + + /* reduction by production yyn */ + if yyDebug >= 2 { + __yyfmt__.Printf("reduce %v in:\n\t%v\n", yyn, yyStatname(yystate)) + } + + yynt := yyn + yypt := yyp + _ = yypt // guard against "declared and not used" + + yyp -= int(yyR2[yyn]) + // yyp is now the index of $0. Perform the default action. Iff the + // reduced production is ε, $1 is possibly out of range. + if yyp+1 >= len(yyS) { + nyys := make([]yySymType, len(yyS)*2) + copy(nyys, yyS) + yyS = nyys + } + yyVAL = yyS[yyp+1] + + /* consult goto table to find next state */ + yyn = int(yyR1[yyn]) + yyg := int(yyPgo[yyn]) + yyj := yyg + yyS[yyp].yys + 1 + + if yyj >= yyLast { + yystate = int(yyAct[yyg]) + } else { + yystate = int(yyAct[yyj]) + if int(yyChk[yystate]) != -yyn { + yystate = int(yyAct[yyg]) + } + } + // dummy call; replaced with literal code + switch yynt { + + case 1: + yyDollar = yyS[yypt-1 : yypt+1] +//line parser.go.y:25 + { + yyVAL.expr = yyDollar[1].expr + if l, ok := yylex.(*Lexer); ok { + l.result = yyVAL.expr + } + } + case 2: + yyDollar = yyS[yypt-1 : yypt+1] +//line parser.go.y:34 + { + yyVAL.expr = SimpleExpr{License: yyDollar[1].token.literal} + } + case 3: + yyDollar = yyS[yypt-2 : yypt+1] +//line parser.go.y:38 + { + yyVAL.expr = SimpleExpr{License: yyDollar[1].expr.String() + " " + yyDollar[2].token.literal} + } + case 4: + yyDollar = yyS[yypt-2 : yypt+1] +//line parser.go.y:44 + { + yyVAL.expr = SimpleExpr{License: yyDollar[1].expr.String(), HasPlus: true} + } + case 5: + yyDollar = yyS[yypt-1 : yypt+1] +//line parser.go.y:49 + { + yyVAL.expr = yyDollar[1].expr + } + case 6: + yyDollar = yyS[yypt-1 : yypt+1] +//line parser.go.y:52 + { + yyVAL.expr = yyDollar[1].expr + } + case 7: + yyDollar = yyS[yypt-3 : yypt+1] +//line parser.go.y:56 + { + yyVAL.expr = CompoundExpr{left: yyDollar[1].expr, conjunction: yyDollar[2].token, right: yyDollar[3].expr} + } + case 8: + yyDollar = yyS[yypt-3 : yypt+1] +//line parser.go.y:60 + { + yyVAL.expr = CompoundExpr{left: yyDollar[1].expr, conjunction: yyDollar[2].token, right: yyDollar[3].expr} + } + case 9: + yyDollar = yyS[yypt-3 : yypt+1] +//line parser.go.y:64 + { + yyVAL.expr = CompoundExpr{left: yyDollar[1].expr, conjunction: yyDollar[2].token, right: yyDollar[3].expr} + } + case 10: + yyDollar = yyS[yypt-3 : yypt+1] +//line parser.go.y:68 + { + yyVAL.expr = yyDollar[2].expr + } + } + goto yystack /* stack new state and value */ +} diff --git a/pkg/licensing/expression/parser_test.go b/pkg/licensing/expression/parser_test.go new file mode 100644 index 000000000000..acbc09a53545 --- /dev/null +++ b/pkg/licensing/expression/parser_test.go @@ -0,0 +1,156 @@ +package expression + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParse(t *testing.T) { + tests := []struct { + name string + input string + want Expression + wantStr string + wantErr string + }{ + { + name: "single license", + input: "Public Domain", + want: SimpleExpr{ + License: "Public Domain", + }, + wantStr: "Public Domain", + }, + { + name: "tag:value license", + input: "DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2", + want: SimpleExpr{ + License: "DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2", + }, + wantStr: "DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2", + }, + { + name: "symbols", + input: "Public ._-+", + want: SimpleExpr{ + License: "Public ._-", + HasPlus: true, + }, + wantStr: "Public ._-+", + }, + { + name: "multi licenses", + input: "Public Domain AND ( GPLv2+ or AFL ) AND LGPLv2+ with distribution exceptions", + want: CompoundExpr{ + left: CompoundExpr{ + left: SimpleExpr{ + License: "Public Domain", + }, + conjunction: Token{ + token: AND, + literal: "AND", + }, + right: CompoundExpr{ + left: SimpleExpr{ + License: "GPLv2", + HasPlus: true, + }, + conjunction: Token{ + token: OR, + literal: "or", + }, + right: SimpleExpr{ + License: "AFL", + }, + }, + }, + conjunction: Token{ + token: AND, + literal: "AND", + }, + right: CompoundExpr{ + left: SimpleExpr{ + License: "LGPLv2", + HasPlus: true, + }, + conjunction: Token{ + token: WITH, + literal: "with", + }, + right: SimpleExpr{ + License: "distribution exceptions", + }, + }, + }, + wantStr: "Public Domain AND (GPLv2+ or AFL) AND LGPLv2+ with distribution exceptions", + }, + { + name: "nested licenses", + input: "Public Domain AND ( GPLv2+ or AFL AND ( CC0 or LGPL1.0) )", + want: CompoundExpr{ + left: SimpleExpr{ + License: "Public Domain", + }, + conjunction: Token{ + token: AND, + literal: "AND", + }, + right: CompoundExpr{ + left: SimpleExpr{ + License: "GPLv2", + HasPlus: true, + }, + conjunction: Token{ + token: OR, + literal: "or", + }, + right: CompoundExpr{ + left: SimpleExpr{ + License: "AFL", + }, + conjunction: Token{ + token: AND, + literal: "AND", + }, + right: CompoundExpr{ + left: SimpleExpr{ + License: "CC0", + }, + conjunction: Token{ + token: OR, + literal: "or", + }, + right: SimpleExpr{ + License: "LGPL1.0", + }, + }, + }, + }, + }, + wantStr: "Public Domain AND (GPLv2+ or AFL AND (CC0 or LGPL1.0))", + }, + { + name: "bad path close bracket not found", + input: "Public Domain AND ( GPLv2+ ", + wantErr: "syntax error", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + l := NewLexer(strings.NewReader(tt.input)) + ret := yyParse(l) + err := l.Err() + if tt.wantErr != "" { + assert.Equal(t, 1, ret) + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, l.result) + assert.Equal(t, tt.wantStr, l.result.String()) + }) + } +} diff --git a/pkg/licensing/expression/types.go b/pkg/licensing/expression/types.go new file mode 100644 index 000000000000..9537a62a9819 --- /dev/null +++ b/pkg/licensing/expression/types.go @@ -0,0 +1,96 @@ +package expression + +import ( + "fmt" + "slices" +) + +var ( + TokenIdent = Token{token: IDENT, literal: "IDENT"} + TokenAnd = Token{token: AND, literal: "AND"} + TokenOR = Token{token: OR, literal: "OR"} + TokenWith = Token{token: WITH, literal: "WITH"} +) + +type Expression interface { + String() string + IsSPDXExpression() bool +} + +type Token struct { + token int + literal string +} + +type SimpleExpr struct { + License string + HasPlus bool +} + +func (s SimpleExpr) String() string { + if slices.Contains(GnuLicenses, s.License) { + if s.HasPlus { + // e.g. AGPL-1.0-or-later + return s.License + "-or-later" + } + // e.g. GPL-1.0-only + return s.License + "-only" + } + + if s.HasPlus { + return s.License + "+" + } + return s.License +} + +func (s SimpleExpr) IsSPDXExpression() bool { + return ValidateSPDXLicense(s.String()) +} + +type CompoundExpr struct { + left Expression + conjunction Token + right Expression +} + +func NewCompoundExpr(left Expression, conjunction Token, right Expression) CompoundExpr { + return CompoundExpr{left: left, conjunction: conjunction, right: right} +} + +func (c CompoundExpr) Conjunction() Token { + return c.conjunction +} + +func (c CompoundExpr) Left() Expression { + return c.left +} + +func (c CompoundExpr) Right() Expression { + return c.right +} + +func (c CompoundExpr) String() string { + left := c.left.String() + if l, ok := c.left.(CompoundExpr); ok { + // e.g. (A OR B) AND C + if c.conjunction.token > l.conjunction.token { + left = fmt.Sprintf("(%s)", left) + } + } + right := c.right.String() + if r, ok := c.right.(CompoundExpr); ok { + // e.g. A AND (B OR C) + if c.conjunction.token > r.conjunction.token { + right = fmt.Sprintf("(%s)", right) + } + } + return fmt.Sprintf("%s %s %s", left, c.conjunction.literal, right) +} + +func (c CompoundExpr) IsSPDXExpression() bool { + if c.conjunction.token == WITH { + // e.g. A WITH B + return c.left.IsSPDXExpression() && ValidateSPDXException(c.right.String()) + } + return c.left.IsSPDXExpression() && c.right.IsSPDXExpression() +} diff --git a/pkg/licensing/expression/y.output b/pkg/licensing/expression/y.output new file mode 100644 index 000000000000..6e39c0c8f4b6 --- /dev/null +++ b/pkg/licensing/expression/y.output @@ -0,0 +1,172 @@ + +state 0 + $accept: .license $end + + IDENT shift 6 + '(' shift 5 + . error + + license goto 1 + simple goto 3 + plus goto 4 + compound goto 2 + +state 1 + $accept: license.$end + + $end accept + . error + + +state 2 + license: compound. (1) + compound: compound.AND compound + compound: compound.OR compound + compound: compound.WITH compound + + OR shift 8 + AND shift 7 + WITH shift 9 + . reduce 1 (src line 23) + + +state 3 + simple: simple.IDENT + plus: simple.'+' + compound: simple. (5) + + IDENT shift 10 + '+' shift 11 + . reduce 5 (src line 48) + + +state 4 + compound: plus. (6) + + . reduce 6 (src line 52) + + +state 5 + compound: '('.compound ')' + + IDENT shift 6 + '(' shift 5 + . error + + simple goto 3 + plus goto 4 + compound goto 12 + +state 6 + simple: IDENT. (2) + + . reduce 2 (src line 32) + + +state 7 + compound: compound AND.compound + + IDENT shift 6 + '(' shift 5 + . error + + simple goto 3 + plus goto 4 + compound goto 13 + +state 8 + compound: compound OR.compound + + IDENT shift 6 + '(' shift 5 + . error + + simple goto 3 + plus goto 4 + compound goto 14 + +state 9 + compound: compound WITH.compound + + IDENT shift 6 + '(' shift 5 + . error + + simple goto 3 + plus goto 4 + compound goto 15 + +state 10 + simple: simple IDENT. (3) + + . reduce 3 (src line 37) + + +state 11 + plus: simple '+'. (4) + + . reduce 4 (src line 42) + + +state 12 + compound: compound.AND compound + compound: compound.OR compound + compound: compound.WITH compound + compound: '(' compound.')' + + OR shift 8 + AND shift 7 + WITH shift 9 + ')' shift 16 + . error + + +state 13 + compound: compound.AND compound + compound: compound AND compound. (7) + compound: compound.OR compound + compound: compound.WITH compound + + WITH shift 9 + . reduce 7 (src line 55) + + +state 14 + compound: compound.AND compound + compound: compound.OR compound + compound: compound OR compound. (8) + compound: compound.WITH compound + + AND shift 7 + WITH shift 9 + . reduce 8 (src line 59) + + +state 15 + compound: compound.AND compound + compound: compound.OR compound + compound: compound.WITH compound + compound: compound WITH compound. (9) + + WITH shift 9 + . reduce 9 (src line 63) + + +state 16 + compound: '(' compound ')'. (10) + + . reduce 10 (src line 67) + + +10 terminals, 5 nonterminals +11 grammar rules, 17/16000 states +0 shift/reduce, 0 reduce/reduce conflicts reported +54 working sets used +memory: parser 15/240000 +13 extra closures +23 shift entries, 1 exceptions +8 goto entries +8 entries saved by goto default +Optimizer space used: output 22/240000 +22 table entries, 0 zero +maximum spread: 10, maximum offset: 9 diff --git a/pkg/licensing/normalize.go b/pkg/licensing/normalize.go new file mode 100644 index 000000000000..3895670584a5 --- /dev/null +++ b/pkg/licensing/normalize.go @@ -0,0 +1,678 @@ +package licensing + +import ( + "regexp" + "slices" + "strings" + + "github.com/samber/lo/it" + + expr "github.com/aquasecurity/trivy/pkg/licensing/expression" +) + +func licence(name string, hasPlus bool) expr.SimpleExpr { + return expr.SimpleExpr{License: name, HasPlus: hasPlus} +} + +var mapping = map[string]expr.SimpleExpr{ + // Simple mappings (i.e. that could be parsed by SpdxExpression.parse, at least without space) + // modified from https://github.com/oss-review-toolkit/ort/blob/fc5389c2cfd9c8b009794c8a11f5c91321b7a730/utils/spdx/src/main/resources/simple-license-mapping.yml + + // Ambiguous simple mappings (mapping reason not obvious without additional information) + "AFL": licence(expr.AFL30, false), + "AGPL": licence(expr.AGPL30, false), + "AL-2": licence(expr.Apache20, false), + "AL-2.0": licence(expr.Apache20, false), + "APACHE": licence(expr.Apache20, false), + "APACHE-STYLE": licence(expr.Apache20, false), + "ARTISTIC": licence(expr.Artistic20, false), + "ASL": licence(expr.Apache20, false), + "BSD": licence(expr.BSD3Clause, false), + "BSD*": licence(expr.BSD3Clause, false), + "BSD-LIKE": licence(expr.BSD3Clause, false), + "BSD-STYLE": licence(expr.BSD3Clause, false), + "BSD-VARIANT": licence(expr.BSD3Clause, false), + "CDDL": licence(expr.CDDL10, false), + "ECLIPSE": licence(expr.EPL10, false), + "EPL": licence(expr.EPL10, false), + "EUPL": licence(expr.EUPL10, false), + "FDL": licence(expr.GFDL13, true), + "GFDL": licence(expr.GFDL13, true), + "GPL": licence(expr.GPL20, true), + "LGPL": licence(expr.LGPL20, true), + "MPL": licence(expr.MPL20, false), + "NETSCAPE": licence(expr.NPL11, false), + "PYTHON": licence(expr.Python20, false), + "ZOPE": licence(expr.ZPL21, false), + + // Non-ambiguous simple mappings + "AFL-2": licence(expr.AFL20, false), + "AGPL-1.0": licence(expr.AGPL10, false), + "AGPL-3.0": licence(expr.AGPL30, false), + "APACHE-1": licence(expr.Apache10, false), + "APACHE-2": licence(expr.Apache20, false), + "APL-2": licence(expr.Apache20, false), + "APL-2.0": licence(expr.Apache20, false), + "ARTISTIC-1.0-CL-8": licence(expr.Artistic10cl8, false), + "ASF-1": licence(expr.Apache10, false), + "ASF-1.0": licence(expr.Apache10, false), + "ASF-1.1": licence(expr.Apache11, false), + "ASF-2": licence(expr.Apache20, false), + "ASF-2.0": licence(expr.Apache20, false), + "ASL-1": licence(expr.Apache10, false), + "ASL-1.0": licence(expr.Apache10, false), + "ASL-1.1": licence(expr.Apache11, false), + "ASL-2": licence(expr.Apache20, false), + "ASL-2.0": licence(expr.Apache20, false), + "BCL": licence(expr.BCL, false), + "BOOST": licence(expr.BSL10, false), + "BOOST-1.0": licence(expr.BSL10, false), + "BOUNCY": licence(expr.MIT, false), + "BSD-1": licence(expr.BSD1Clause, false), + "BSD-2": licence(expr.BSD2Clause, false), + "BSD-3": licence(expr.BSD3Clause, false), + "BSD-3-CLAUSE": licence(expr.BSD3Clause, false), + "BSD-4": licence(expr.BSD4Clause, false), + "BSL": licence(expr.BSL10, false), + "CC0": licence(expr.CC010, false), + "CDDL-1": licence(expr.CDDL10, false), + "COMMONS-CLAUSE": licence(expr.CommonsClause, false), + "CPAL": licence(expr.CPAL10, false), + "CPL": licence(expr.CPL10, false), + "CPL-1.0": licence(expr.CPL10, false), + "ECLIPSE-1.0": licence(expr.EPL10, false), + "ECLIPSE-2.0": licence(expr.EPL20, false), + "EDL-1.0": licence(expr.BSD3Clause, false), + "EXPAT": licence(expr.MIT, false), + "FACEBOOK-2-CLAUSE": licence(expr.Facebook2Clause, false), + "FACEBOOK-3-CLAUSE": licence(expr.Facebook3Clause, false), + "FACEBOOK-EXAMPLES": licence(expr.FacebookExamples, false), + "GFDL-1.1": licence(expr.GFDL11, false), + "GFDL-1.1-INVARIANTS": licence(expr.GFDL11WithInvariants, false), + "GFDL-1.1-NO-INVARIANTS": licence(expr.GFDL11NoInvariants, false), + "GFDL-NIV-1.1": licence(expr.GFDL11NoInvariants, false), + "GFDL-1.2": licence(expr.GFDL12, false), + "GFDL-1.2-INVARIANTS": licence(expr.GFDL12WithInvariants, false), + "GFDL-1.2-NO-INVARIANTS": licence(expr.GFDL12NoInvariants, false), + "GFDL-NIV-1.2": licence(expr.GFDL12NoInvariants, false), + "GFDL-1.3": licence(expr.GFDL13, false), + "GFDL-1.3-INVARIANTS": licence(expr.GFDL13WithInvariants, false), + "GFDL-1.3-NO-INVARIANTS": licence(expr.GFDL13NoInvariants, false), + "GFDL-NIV-1.3": licence(expr.GFDL13NoInvariants, false), + "GO": licence(expr.BSD3Clause, false), + "GPL-1": licence(expr.GPL10, false), + "GPL-1.0": licence(expr.GPL10, false), + "GPL-2": licence(expr.GPL20, false), + "GPL-2+-WITH-BISON-EXCEPTION": licence(expr.GPL20withbisonexception, true), + "GPL-2.0": licence(expr.GPL20, false), + "GPL-2.0-WITH-AUTOCONF-EXCEPTION": licence(expr.GPL20withautoconfexception, false), + "GPL-2.0-WITH-BISON-EXCEPTION": licence(expr.GPL20withbisonexception, false), + "GPL-2.0-WITH-CLASSPATH-EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GPL-2.0-WITH-FONT-EXCEPTION": licence(expr.GPL20withfontexception, false), + "GPL-2.0-WITH-GCC-EXCEPTION": licence(expr.GPL20withGCCexception, false), + "GPL-3": licence(expr.GPL30, false), + "GPL-3+-WITH-BISON-EXCEPTION": licence(expr.GPL20withbisonexception, true), + "GPL-3.0": licence(expr.GPL30, false), + "GPL-3.0-WITH-AUTOCONF-EXCEPTION": licence(expr.GPL30withautoconfexception, false), + "GPL-3.0-WITH-GCC-EXCEPTION": licence(expr.GPL30withGCCexception, false), + "GPLV2+CE": licence(expr.GPL20withclasspathexception, true), + "GUST-FONT": licence(expr.GUSTFont, false), + "HSQLDB": licence(expr.BSD3Clause, false), + "ISCL": licence(expr.ISC, false), + "JQUERY": licence(expr.MIT, false), + "LGPL-2": licence(expr.LGPL20, false), + "LGPL-2.0": licence(expr.LGPL20, false), + "LGPL-2.1": licence(expr.LGPL21, false), + "LGPL-3": licence(expr.LGPL30, false), + "LGPL-3.0": licence(expr.LGPL30, false), + "LIL-1.0": licence(expr.Lil10, false), + "MIT-LIKE": licence(expr.MIT, false), + "MIT-STYLE": licence(expr.MIT, false), + "MPL-1": licence(expr.MPL10, false), + "MPL-2": licence(expr.MPL20, false), + "OFL-1.1": licence(expr.OFL11, false), + "OPENSSL": licence(expr.OpenSSL, false), + "OPENVISION": licence(expr.OpenVision, false), + "OSL-1": licence(expr.OSL10, false), + "OSL-2": licence(expr.OSL20, false), + "OSL-3": licence(expr.OSL30, false), + "PIL": licence(expr.PIL, false), + "PYTHON-2": licence(expr.Python20, false), + "PYTHON-2.0-COMPLETE": licence(expr.Python20complete, false), + "QPL-1": licence(expr.QPL10, false), + "UPL-1": licence(expr.UPL10, false), + "W3CL": licence(expr.W3C, false), + "WTF": licence(expr.WTFPL, false), + "ZEND-2": licence(expr.Zend20, false), + "ZOPE-1.1": licence(expr.ZPL11, false), + "ZOPE-2.0": licence(expr.ZPL20, false), + "ZOPE-2.1": licence(expr.ZPL21, false), + + // Non simple declared mappings + // modified from https://github.com/oss-review-toolkit/ort/blob/fc5389c2cfd9c8b009794c8a11f5c91321b7a730/utils/spdx/src/main/resources/declared-license-mapping.yml + + // Ambiguous declared mappings (mapping reason not obvious without additional information) + "ACADEMIC FREE LICENSE (AFL)": licence(expr.AFL21, false), + "APACHE SOFTWARE LICENSES": licence(expr.Apache20, false), + "APACHE SOFTWARE": licence(expr.Apache20, false), + "APPLE PUBLIC SOURCE": licence(expr.APSL10, false), + "BSD SOFTWARE": licence(expr.BSD2Clause, false), + "BSD STYLE": licence(expr.BSD3Clause, false), + "COMMON DEVELOPMENT AND DISTRIBUTION": licence(expr.CDDL10, false), + "CREATIVE COMMONS - BY": licence(expr.CCBY30, false), + "CREATIVE COMMONS ATTRIBUTION": licence(expr.CCBY30, false), + "CREATIVE COMMONS": licence(expr.CCBY30, false), + "ECLIPSE PUBLIC LICENSE (EPL)": licence(expr.EPL10, false), + "GENERAL PUBLIC LICENSE (GPL)": licence(expr.GPL20, true), + "GNU FREE DOCUMENTATION LICENSE (FDL)": licence(expr.GFDL13, true), + "GNU GENERAL PUBLIC LIBRARY": licence(expr.GPL30, true), + "GNU GENERAL PUBLIC LICENSE (GPL)": licence(expr.GPL30, true), + "GNU GPL": licence(expr.GPL20, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL)": licence(expr.LGPL21, false), + "GNU LESSER GENERAL PUBLIC": licence(expr.LGPL21, false), + "GNU LESSER PUBLIC": licence(expr.LGPL21, false), + "GNU LESSER": licence(expr.LGPL21, false), + "GNU LGPL": licence(expr.LGPL21, false), + "GNU LIBRARY OR LESSER GENERAL PUBLIC LICENSE (LGPL)": licence(expr.LGPL21, false), + "GNU PUBLIC": licence(expr.GPL20, true), + "GPL (WITH DUAL LICENSING OPTION)": licence(expr.GPL20, false), + "GPLV2 WITH EXCEPTIONS": licence(expr.GPL20withclasspathexception, false), + "INDIVIDUAL BSD": licence(expr.BSD3Clause, false), + "LESSER GENERAL PUBLIC LICENSE (LGPL)": licence(expr.LGPL21, true), + "LGPL WITH EXCEPTIONS": licence(expr.LGPL30, false), + "LPGL, SEE LICENSE FILE.": licence(expr.LGPL30, true), + "MOZILLA PUBLIC": licence(expr.MPL20, false), + "ZOPE PUBLIC": licence(expr.ZPL21, false), + + // Non-ambiguous declared mappings + "(NEW) BSD": licence(expr.BSD3Clause, false), + "2-CLAUSE BSD": licence(expr.BSD2Clause, false), + "2-CLAUSE BSDL": licence(expr.BSD2Clause, false), + "3-CLAUSE BDSL": licence(expr.BSD3Clause, false), + "3-CLAUSE BSD": licence(expr.BSD3Clause, false), + "ACADEMIC FREE LICENSE (AFL-2.1": licence(expr.AFL21, false), + "AFFERO GENERAL PUBLIC LICENSE (AGPL-3": licence(expr.AGPL30, false), + "APACHE 2 STYLE": licence(expr.Apache20, false), + "APACHE LICENSE, ASL-2.0": licence(expr.Apache20, false), + "APACHE LICENSE, VERSION 2.0 (HTTP://WWW.APACHE.ORG/LICENSES/LICENSE-2.0": licence(expr.Apache20, false), + "APACHE PUBLIC-1.1": licence(expr.Apache11, false), + "APACHE PUBLIC-2": licence(expr.Apache20, false), + "APACHE PUBLIC-2.0": licence(expr.Apache20, false), + "APACHE SOFTWARE LICENSE (APACHE-2": licence(expr.Apache20, false), + "APACHE SOFTWARE LICENSE (APACHE-2.0": licence(expr.Apache20, false), + "APACHE SOFTWARE-1.1": licence(expr.Apache11, false), + "APACHE SOFTWARE-2": licence(expr.Apache20, false), + "APACHE SOFTWARE-2.0": licence(expr.Apache20, false), + "APACHE VERSION 2.0, JANUARY 2004": licence(expr.Apache20, false), + "APACHE-2.0 */ ' " =END --": licence(expr.Apache20, false), + "BERKELEY SOFTWARE DISTRIBUTION (BSD)": licence(expr.BSD2Clause, false), + "BOOST SOFTWARE LICENSE 1.0 (BSL-1.0": licence(expr.BSL10, false), + "BOOST SOFTWARE": licence(expr.BSL10, false), + "BOUNCY CASTLE": licence(expr.MIT, false), + "BSD (3-CLAUSE)": licence(expr.BSD3Clause, false), + "BSD - SEE NDG/HTTPSCLIENT/LICENSE FILE FOR DETAILS": licence(expr.BSD3Clause, false), + "BSD 2 CLAUSE": licence(expr.BSD2Clause, false), + "BSD 2-CLAUSE": licence(expr.BSD2Clause, false), + "BSD 3 CLAUSE": licence(expr.BSD3Clause, false), + "BSD 3-CLAUSE NEW": licence(expr.BSD3Clause, false), + "BSD 3-CLAUSE \"NEW\" OR \"REVISED\" LICENSE (BSD-3-CLAUSE)": licence(expr.BSD3Clause, false), + "BSD 3-CLAUSE": licence(expr.BSD3Clause, false), + "BSD 4 CLAUSE": licence(expr.BSD4Clause, false), + "BSD 4-CLAUSE": licence(expr.BSD4Clause, false), + "BSD FOUR CLAUSE": licence(expr.BSD4Clause, false), + "BSD LICENSE FOR HSQL": licence(expr.BSD3Clause, false), + "BSD NEW": licence(expr.BSD3Clause, false), + "BSD THREE CLAUSE": licence(expr.BSD3Clause, false), + "BSD TWO CLAUSE": licence(expr.BSD2Clause, false), + "BSD-3 CLAUSE": licence(expr.BSD3Clause, false), + "BSD-STYLE + ATTRIBUTION": licence(expr.BSD3ClauseAttribution, false), + "CC BY-NC-SA-2.0": licence(expr.CCBYNCSA20, false), + "CC BY-NC-SA-2.5": licence(expr.CCBYNCSA25, false), + "CC BY-NC-SA-3.0": licence(expr.CCBYNCSA30, false), + "CC BY-NC-SA-4.0": licence(expr.CCBYNCSA40, false), + "CC BY-SA-2.0": licence(expr.CCBYSA20, false), + "CC BY-SA-2.5": licence(expr.CCBYSA25, false), + "CC BY-SA-3.0": licence(expr.CCBYSA30, false), + "CC BY-SA-4.0": licence(expr.CCBYSA40, false), + "CC0 1.0 UNIVERSAL (CC0 1.0) PUBLIC DOMAIN DEDICATION": licence(expr.CC010, false), + "CC0 1.0 UNIVERSAL": licence(expr.CC010, false), + "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)-1.0": licence(expr.CDDL10, false), + "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)-1.1": licence(expr.CDDL11, false), + "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE 1.0 (CDDL-1.0": licence(expr.CDDL10, false), + "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE 1.1 (CDDL-1.1": licence(expr.CDDL11, false), + "COMMON PUBLIC": licence(expr.CPL10, false), + "COMMON PUBLIC-1.0": licence(expr.CPL10, false), + "CREATIVE COMMONS - ATTRIBUTION 4.0 INTERNATIONAL": licence(expr.CCBY40, false), + "CREATIVE COMMONS 3.0 BY-SA": licence(expr.CCBYSA30, false), + "CREATIVE COMMONS ATTRIBUTION 3.0 UNPORTED (CC BY-3.0": licence(expr.CCBY30, false), + "CREATIVE COMMONS ATTRIBUTION 4.0 INTERNATIONAL (CC BY-4.0": licence(expr.CCBY40, false), + "CREATIVE COMMONS ATTRIBUTION 4.0 INTERNATIONAL PUBLIC": licence(expr.CCBY40, false), + "CREATIVE COMMONS ATTRIBUTION-1.0": licence(expr.CCBY10, false), + "CREATIVE COMMONS ATTRIBUTION-2.5": licence(expr.CCBY25, false), + "CREATIVE COMMONS ATTRIBUTION-3.0": licence(expr.CCBY30, false), + "CREATIVE COMMONS ATTRIBUTION-4.0": licence(expr.CCBY40, false), + "CREATIVE COMMONS ATTRIBUTION-NONCOMMERCIAL 4.0 INTERNATIONAL": licence(expr.CCBYNC40, false), + "CREATIVE COMMONS ATTRIBUTION-NONCOMMERCIAL-NODERIVATIVES 4.0 INTERNATIONAL": licence(expr.CCBYNCND40, false), + "CREATIVE COMMONS ATTRIBUTION-NONCOMMERCIAL-SHAREALIKE 3.0 UNPORTED (CC BY-NC-SA-3.0": licence(expr.CCBYNCSA30, false), + "CREATIVE COMMONS ATTRIBUTION-NONCOMMERCIAL-SHAREALIKE 4.0 INTERNATIONAL PUBLIC": licence(expr.CCBYNCSA40, false), + "CREATIVE COMMONS CC0": licence(expr.CC010, false), + "CREATIVE COMMONS GNU LGPL-2.1": licence(expr.LGPL21, false), + "CREATIVE COMMONS LICENSE ATTRIBUTION-NODERIVS 3.0 UNPORTED": licence(expr.CCBYNCND30, false), + "CREATIVE COMMONS LICENSE ATTRIBUTION-NONCOMMERCIAL-SHAREALIKE 3.0 UNPORTED": licence(expr.CCBYNCSA30, false), + "CREATIVE COMMONS ZERO": licence(expr.CC010, false), + "CREATIVE COMMONS-3.0": licence(expr.CCBY30, false), + "ECLIPSE DISTRIBUTION LICENSE (EDL)-1.0": licence(expr.BSD3Clause, false), + "ECLIPSE DISTRIBUTION LICENSE (NEW BSD LICENSE)": licence(expr.BSD3Clause, false), + "ECLIPSE DISTRIBUTION-1.0": licence(expr.BSD3Clause, false), + "ECLIPSE PUBLIC LICENSE (EPL)-1.0": licence(expr.EPL10, false), + "ECLIPSE PUBLIC LICENSE (EPL)-2.0": licence(expr.EPL20, false), + "ECLIPSE PUBLIC LICENSE 1.0 (EPL-1.0": licence(expr.EPL10, false), + "ECLIPSE PUBLIC LICENSE 2.0 (EPL-2.0": licence(expr.EPL20, false), + "ECLIPSE PUBLIC": licence(expr.EPL10, false), + "ECLIPSE PUBLIC-1.0": licence(expr.EPL10, false), + "ECLIPSE PUBLIC-2.0": licence(expr.EPL20, false), + "ECLIPSE PUBLISH-1.0": licence(expr.EPL10, false), + "EPL (ECLIPSE PUBLIC LICENSE)-1.0": licence(expr.EPL10, false), + "EU PUBLIC LICENSE 1.0 (EUPL-1.0": licence(expr.EUPL10, false), + "EU PUBLIC LICENSE 1.1 (EUPL-1.1": licence(expr.EUPL11, false), + "EUROPEAN UNION PUBLIC LICENSE (EUPL-1.0": licence(expr.EUPL10, false), + "EUROPEAN UNION PUBLIC LICENSE (EUPL-1.1": licence(expr.EUPL11, false), + "EUROPEAN UNION PUBLIC LICENSE 1.0 (EUPL-1.0": licence(expr.EUPL10, false), + "EUROPEAN UNION PUBLIC LICENSE 1.1 (EUPL-1.1": licence(expr.EUPL11, false), + "EUROPEAN UNION PUBLIC-1.0": licence(expr.EUPL10, false), + "EUROPEAN UNION PUBLIC-1.1": licence(expr.EUPL11, false), + "EXPAT (MIT/X11)": licence(expr.MIT, false), + "GENERAL PUBLIC LICENSE 2.0 (GPL)": licence(expr.GPL20, false), + "GNU AFFERO GENERAL PUBLIC LICENSE V3 (AGPL-3": licence(expr.AGPL30, false), + "GNU AFFERO GENERAL PUBLIC LICENSE V3 (AGPL-3.0": licence(expr.AGPL30, false), + "GNU AFFERO GENERAL PUBLIC LICENSE V3 OR LATER (AGPL3+)": licence(expr.AGPL30, true), + "GNU AFFERO GENERAL PUBLIC LICENSE V3 OR LATER (AGPLV3+)": licence(expr.AGPL30, true), + "GNU AFFERO GENERAL PUBLIC-3": licence(expr.AGPL30, false), + "GNU FREE DOCUMENTATION LICENSE (GFDL-1.3": licence(expr.GFDL13, false), + "GNU GENERAL LESSER PUBLIC LICENSE (LGPL)-2.1": licence(expr.LGPL21, false), + "GNU GENERAL LESSER PUBLIC LICENSE (LGPL)-3.0": licence(expr.LGPL30, false), + "GNU GENERAL PUBLIC LICENSE (GPL), VERSION 2, WITH CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GNU GENERAL PUBLIC LICENSE (GPL), VERSION 2, WITH THE CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GNU GENERAL PUBLIC LICENSE (GPL)-2": licence(expr.GPL20, false), + "GNU GENERAL PUBLIC LICENSE (GPL)-3": licence(expr.GPL30, false), + "GNU GENERAL PUBLIC LICENSE V2 (GPL-2": licence(expr.GPL20, false), + "GNU GENERAL PUBLIC LICENSE V2 OR LATER (GPLV2+)": licence(expr.GPL20, true), + "GNU GENERAL PUBLIC LICENSE V2.0 ONLY, WITH CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GNU GENERAL PUBLIC LICENSE V3 (GPL-3": licence(expr.GPL30, false), + "GNU GENERAL PUBLIC LICENSE V3 OR LATER (GPLV3+)": licence(expr.GPL30, true), + "GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL-2": licence(expr.GPL20, false), + "GNU GENERAL PUBLIC LICENSE VERSION 2, JUNE 1991": licence(expr.GPL20, false), + "GNU GENERAL PUBLIC LICENSE VERSION 3 (GPL-3": licence(expr.GPL30, false), + "GNU GENERAL PUBLIC LICENSE, VERSION 2 (GPL2), WITH THE CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GNU GENERAL PUBLIC LICENSE, VERSION 2 WITH THE CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GNU GENERAL PUBLIC LICENSE, VERSION 2 WITH THE GNU CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GNU GENERAL PUBLIC LICENSE, VERSION 2, WITH THE CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GNU GENERAL PUBLIC-2": licence(expr.GPL20, false), + "GNU GENERAL PUBLIC-3": licence(expr.GPL30, false), + "GNU GPL-2": licence(expr.GPL20, false), + "GNU GPL-3": licence(expr.GPL30, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL)-2": licence(expr.LGPL20, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL)-2.0": licence(expr.LGPL20, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL)-2.1": licence(expr.LGPL21, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL)-3": licence(expr.LGPL30, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL)-3.0": licence(expr.LGPL30, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL-2": licence(expr.LGPL20, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL-2.0": licence(expr.LGPL20, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL-2.1": licence(expr.LGPL21, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL-3": licence(expr.LGPL30, false), + "GNU LESSER GENERAL PUBLIC LICENSE (LGPL-3.0": licence(expr.LGPL30, false), + "GNU LESSER GENERAL PUBLIC LICENSE V2 (LGPL-2": licence(expr.LGPL20, false), + "GNU LESSER GENERAL PUBLIC LICENSE V2 OR LATER (LGPLV2+)": licence(expr.LGPL20, true), + "GNU LESSER GENERAL PUBLIC LICENSE V3 (LGPL-3": licence(expr.LGPL30, false), + "GNU LESSER GENERAL PUBLIC LICENSE V3 OR LATER (LGPLV3+)": licence(expr.LGPL30, true), + "GNU LESSER GENERAL PUBLIC LICENSE VERSION 2.1 (LGPL-2.1": licence(expr.LGPL21, false), + "GNU LESSER GENERAL PUBLIC LICENSE VERSION 2.1, FEBRUARY 1999": licence(expr.LGPL21, false), + "GNU LESSER GENERAL PUBLIC LICENSE, VERSION 2.1, FEBRUARY 1999": licence(expr.LGPL21, false), + "GNU LESSER GENERAL PUBLIC-2": licence(expr.LGPL20, false), + "GNU LESSER GENERAL PUBLIC-2.0": licence(expr.LGPL20, false), + "GNU LESSER GENERAL PUBLIC-2.1": licence(expr.LGPL21, false), + "GNU LESSER GENERAL PUBLIC-3": licence(expr.LGPL30, false), + "GNU LESSER GENERAL PUBLIC-3.0": licence(expr.LGPL30, false), + "GNU LGP (GNU GENERAL PUBLIC LICENSE)-2": licence(expr.LGPL20, false), + "GNU LGPL (GNU LESSER GENERAL PUBLIC LICENSE)-2.1": licence(expr.LGPL21, false), + "GNU LGPL-2": licence(expr.LGPL20, false), + "GNU LGPL-2.0": licence(expr.LGPL20, false), + "GNU LGPL-2.1": licence(expr.LGPL21, false), + "GNU LGPL-3": licence(expr.LGPL30, false), + "GNU LGPL-3.0": licence(expr.LGPL30, false), + "GNU LIBRARY GENERAL PUBLIC-2.0": licence(expr.LGPL20, false), + "GNU LIBRARY GENERAL PUBLIC-2.1": licence(expr.LGPL21, false), + "GNU LIBRARY OR LESSER GENERAL PUBLIC LICENSE VERSION 2.0 (LGPL-2": licence(expr.LGPL20, false), + "GNU LIBRARY OR LESSER GENERAL PUBLIC LICENSE VERSION 3.0 (LGPL-3": licence(expr.LGPL30, false), + "GPL (≥ 3)": licence(expr.GPL30, true), + "GPL 2 WITH CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GPL V2 WITH CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GPL-2+ WITH AUTOCONF EXCEPTION": licence(expr.GPL20withautoconfexception, true), + "GPL-3+ WITH AUTOCONF EXCEPTION": licence(expr.GPL30withautoconfexception, true), + "GPL2 W/ CPE": licence(expr.GPL20withclasspathexception, false), + "GPLV2 LICENSE, INCLUDES THE CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "GPLV2 WITH CLASSPATH EXCEPTION": licence(expr.GPL20withclasspathexception, false), + "HSQLDB LICENSE, A BSD OPEN SOURCE": licence(expr.BSD3Clause, false), + "HTTP://ANT-CONTRIB.SOURCEFORGE.NET/TASKS/LICENSE.TXT": licence(expr.Apache11, false), + "HTTP://ASM.OW2.ORG/LICENSE.HTML": licence(expr.BSD3Clause, false), + "HTTP://CREATIVECOMMONS.ORG/PUBLICDOMAIN/ZERO/1.0/LEGALCODE": licence(expr.CC010, false), + "HTTP://EN.WIKIPEDIA.ORG/WIKI/ZLIB_LICENSE": licence(expr.Zlib, false), + "HTTP://JSON.CODEPLEX.COM/LICENSE": licence(expr.MIT, false), + "HTTP://POLYMER.GITHUB.IO/LICENSE.TXT": licence(expr.BSD3Clause, false), + "HTTP://WWW.APACHE.ORG/LICENSES/LICENSE-2.0": licence(expr.Apache20, false), + "HTTP://WWW.APACHE.ORG/LICENSES/LICENSE-2.0.HTML": licence(expr.Apache20, false), + "HTTP://WWW.APACHE.ORG/LICENSES/LICENSE-2.0.TXT": licence(expr.Apache20, false), + "HTTP://WWW.GNU.ORG/COPYLEFT/LESSER.HTML": licence(expr.LGPL30, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-ND/1.0": licence(expr.CCBYNCND10, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-ND/2.0": licence(expr.CCBYNCND20, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-ND/2.5": licence(expr.CCBYNCND25, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-ND/3.0": licence(expr.CCBYNCND30, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-ND/4.0": licence(expr.CCBYNCND40, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-SA/1.0": licence(expr.CCBYNCSA10, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-SA/2.0": licence(expr.CCBYNCSA20, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-SA/2.5": licence(expr.CCBYNCSA25, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-SA/3.0": licence(expr.CCBYNCSA30, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-NC-SA/4.0": licence(expr.CCBYNCSA40, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-ND/1.0": licence(expr.CCBYND10, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-ND/2.0": licence(expr.CCBYND20, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-ND/2.5": licence(expr.CCBYND25, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-ND/3.0": licence(expr.CCBYND30, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-ND/4.0": licence(expr.CCBYND40, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-SA/1.0": licence(expr.CCBYSA10, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-SA/2.0": licence(expr.CCBYSA20, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-SA/2.5": licence(expr.CCBYSA25, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-SA/3.0": licence(expr.CCBYSA30, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY-SA/4.0": licence(expr.CCBYSA40, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY/1.0": licence(expr.CCBY10, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY/2.0": licence(expr.CCBY20, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY/2.5": licence(expr.CCBY25, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY/3.0": licence(expr.CCBY30, false), + "HTTPS://CREATIVECOMMONS.ORG/LICENSES/BY/4.0": licence(expr.CCBY40, false), + "HTTPS://CREATIVECOMMONS.ORG/PUBLICDOMAIN/ZERO/1.0/": licence(expr.CC010, false), + "HTTPS://GITHUB.COM/DOTNET/CORE-SETUP/BLOB/MASTER/LICENSE.TXT": licence(expr.MIT, false), + "HTTPS://GITHUB.COM/DOTNET/COREFX/BLOB/MASTER/LICENSE.TXT": licence(expr.MIT, false), + "HTTPS://RAW.GITHUB.COM/RDFLIB/RDFLIB/MASTER/LICENSE": licence(expr.BSD3Clause, false), + "HTTPS://RAW.GITHUBUSERCONTENT.COM/ASPNET/ASPNETCORE/2.0.0/LICENSE.TXT": licence(expr.Apache20, false), + "HTTPS://RAW.GITHUBUSERCONTENT.COM/ASPNET/HOME/2.0.0/LICENSE.TXT": licence(expr.Apache20, false), + "HTTPS://RAW.GITHUBUSERCONTENT.COM/NUGET/NUGET.CLIENT/DEV/LICENSE.TXT": licence(expr.Apache20, false), + "HTTPS://WWW.APACHE.ORG/LICENSES/LICENSE-2.0": licence(expr.Apache20, false), + "HTTPS://WWW.ECLIPSE.ORG/LEGAL/EPL-V10.HTML": licence(expr.EPL10, false), + "HTTPS://WWW.ECLIPSE.ORG/LEGAL/EPL-V20.HTML": licence(expr.EPL20, false), + "IBM PUBLIC": licence(expr.IPL10, false), + "ISC LICENSE (ISCL)": licence(expr.ISC, false), + "JYTHON SOFTWARE": licence(expr.Python20, false), + "KIRKK.COM BSD": licence(expr.BSD3Clause, false), + "LESSER GENERAL PUBLIC LICENSE, VERSION 3 OR GREATER": licence(expr.LGPL30, true), + "LICENSE AGREEMENT FOR OPEN SOURCE COMPUTER VISION LIBRARY (3-CLAUSE BSD LICENSE)": licence(expr.BSD3Clause, false), + "MIT (HTTP://MOOTOOLS.NET/LICENSE.TXT)": licence(expr.MIT, false), + "MIT / HTTP://REM.MIT-LICENSE.ORG": licence(expr.MIT, false), + "MIT LICENSE (HTTP://OPENSOURCE.ORG/LICENSES/MIT)": licence(expr.MIT, false), + "MIT LICENSE (MIT)": licence(expr.MIT, false), + "MIT LICENSE(MIT)": licence(expr.MIT, false), + "MIT LICENSED. HTTP://WWW.OPENSOURCE.ORG/LICENSES/MIT-LICENSE.PHP": licence(expr.MIT, false), + "MIT/EXPAT": licence(expr.MIT, false), + "MOCKRUNNER LICENSE, BASED ON APACHE SOFTWARE-1.1": licence(expr.Apache11, false), + "MODIFIED BSD": licence(expr.BSD3Clause, false), + "MOZILLA PUBLIC LICENSE 1.0 (MPL)": licence(expr.MPL10, false), + "MOZILLA PUBLIC LICENSE 1.1 (MPL-1.1": licence(expr.MPL11, false), + "MOZILLA PUBLIC LICENSE 2.0 (MPL-2.0": licence(expr.MPL20, false), + "MOZILLA PUBLIC-1.0": licence(expr.MPL10, false), + "MOZILLA PUBLIC-1.1": licence(expr.MPL11, false), + "MOZILLA PUBLIC-2.0": licence(expr.MPL20, false), + "NCSA OPEN SOURCE": licence(expr.NCSA, false), + "NETSCAPE PUBLIC LICENSE (NPL)": licence(expr.NPL10, false), + "NETSCAPE PUBLIC": licence(expr.NPL10, false), + "NEW BSD": licence(expr.BSD3Clause, false), + "OPEN SOFTWARE LICENSE 3.0 (OSL-3.0": licence(expr.OSL30, false), + "OPEN SOFTWARE-3.0": licence(expr.OSL30, false), + "PERL ARTISTIC-2": licence(expr.Artistic10Perl, false), + // Note: public domain without a specific license should not be mapped + // see https://wiki.spdx.org/view/Legal_Team/Decisions/Dealing_with_Public_Domain_within_SPDX_Files + // and https://opensource.google/documentation/reference/thirdparty/licenses#unencumbered + "PUBLIC DOMAIN (CC0-1.0)": licence(expr.CC010, false), + "PUBLIC DOMAIN, PER CREATIVE COMMONS CC0": licence(expr.CC010, false), + "QT PUBLIC LICENSE (QPL)": licence(expr.QPL10, false), + "QT PUBLIC": licence(expr.QPL10, false), + "REVISED BSD": licence(expr.BSD3Clause, false), + "RUBY'S": licence(expr.Ruby, false), + "SEQUENCE LIBRARY LICENSE (BSD-LIKE)": licence(expr.BSD3Clause, false), + "SIL OPEN FONT LICENSE 1.1 (OFL-1.1": licence(expr.OFL11, false), + "SIL OPEN FONT-1.1": licence(expr.OFL11, false), + "SIMPLIFIED BSD LISCENCE": licence(expr.BSD2Clause, false), + "SIMPLIFIED BSD": licence(expr.BSD2Clause, false), + "SUN INDUSTRY STANDARDS SOURCE LICENSE (SISSL)": licence(expr.SISSL, false), + "THREE-CLAUSE BSD-STYLE": licence(expr.BSD3Clause, false), + "TWO-CLAUSE BSD-STYLE": licence(expr.BSD2Clause, false), + "UNIVERSAL PERMISSIVE LICENSE (UPL)": licence(expr.UPL10, false), + "UNIVERSAL PERMISSIVE-1.0": licence(expr.UPL10, false), + "UNLICENSE (UNLICENSE)": licence(expr.Unlicense, false), + "W3C SOFTWARE": licence(expr.W3C, false), + "ZLIB / LIBPNG": licence(expr.ZlibAcknowledgement, false), + "ZLIB/LIBPNG": licence(expr.ZlibAcknowledgement, false), + "['MIT']": licence(expr.MIT, false), +} + +const ( + LicenseTextPrefix = "text://" + LicenseFilePrefix = "file://" + CustomLicensePrefix = "CUSTOM License" +) + +// pythonLicenseExceptions contains licenses that we cannot separate correctly using our logic. +// first word after separator (or/and) => license name +var pythonLicenseExceptions = map[string]string{ + "lesser": "GNU Library or Lesser General Public License (LGPL)", + "distribution": "Common Development and Distribution License 1.0 (CDDL-1.0)", + "disclaimer": "Historical Permission Notice and Disclaimer (HPND)", +} + +// Split licenses without considering "and"/"or" +// examples: +// 'GPL-1+,GPL-2' => {"GPL-1+", "GPL-2"} +// 'GPL-1+ or Artistic or Artistic-dist' => {"GPL-1+", "Artistic", "Artistic-dist"} +// 'LGPLv3+_or_GPLv2+' => {"LGPLv3+", "GPLv2"} +// 'BSD-3-CLAUSE and GPL-2' => {"BSD-3-CLAUSE", "GPL-2"} +// 'GPL-1+ or Artistic, and BSD-4-clause-POWERDOG' => {"GPL-1+", "Artistic", "BSD-4-clause-POWERDOG"} +// 'BSD 3-Clause License or Apache License, Version 2.0' => {"BSD 3-Clause License", "Apache License, Version 2.0"} +var licenseSplitRegexp = regexp.MustCompile("(,?[_ ]+(?:or|and)[_ ]+)|(,[ ]*)") + +// Typical keywords for license texts +var licenseTextKeywords = []string{ + "http://", + "https://", + "(c)", + "as-is", + ";", + "hereby", + "permission to use", + "permission is", + "use in source", + "use, copy, modify", + "using", +} + +func isLicenseText(str string) bool { + for _, keyword := range licenseTextKeywords { + if strings.Contains(str, keyword) { + return true + } + } + return false +} + +func TrimLicenseText(text string) string { + s := strings.Split(text, " ") + n := min(len(s), 3) + return strings.Join(s[:n], " ") + "..." +} + +// version number match +var versionRegexpString = "([A-UW-Z)])( LICENSE)?\\s*[,(-]?\\s*(V|V\\.|VER|VER\\.|VERSION|VERSION-|-)?\\s*([1-9](\\.\\d)*)[)]?" + +// case insensitive version match anywhere in string +var versionRegexp = regexp.MustCompile("(?i)" + versionRegexpString) + +// version suffix match +var versionSuffixRegexp = regexp.MustCompile(versionRegexpString + "$") + +// suffixes from https://spdx.dev/learn/handling-license-info/ +var onlySuffixes = [2]string{"-ONLY", " ONLY"} +var plusSuffixes = [3]string{"+", "-OR-LATER", " OR LATER"} + +func standardizeKeyAndSuffix(name string) expr.SimpleExpr { + // Standardize space, including newline + name = strings.Join(strings.Fields(name), " ") + name = strings.ToUpper(name) + // Do not perform any further normalization for URLs + if strings.HasPrefix(name, "HTTP") { + return expr.SimpleExpr{License: name, HasPlus: false} + } + name = strings.ReplaceAll(name, "LICENCE", "LICENSE") + name = strings.TrimPrefix(name, "THE ") + name = strings.TrimSuffix(name, " LICENSE") + name = strings.TrimSuffix(name, " LICENSED") + name = strings.TrimSuffix(name, "-LICENSE") + name = strings.TrimSuffix(name, "-LICENSED") + // Remove License and Licensed suffixes except for licenses already containing those suffixes such as Unlicense + if name != "UNLICENSE" { + name = strings.TrimSuffix(name, "LICENSE") + } + if name != "UNLICENSED" { + name = strings.TrimSuffix(name, "LICENSED") + } + hasPlus := false + for _, s := range plusSuffixes { + if strings.HasSuffix(name, s) { + name = strings.TrimSuffix(name, s) + hasPlus = true + } + } + for _, s := range onlySuffixes { + name = strings.TrimSuffix(name, s) + } + name = versionSuffixRegexp.ReplaceAllString(name, "$1-$4") + return expr.SimpleExpr{License: name, HasPlus: hasPlus} +} + +func NormalizeLicenses(licenses []string) []string { + seq := it.UniqMap(slices.Values(licenses), normalizeLicense) + seq = it.Compact(seq) + normalized := slices.Collect(seq) + + if len(normalized) == 0 { + return nil + } + + return normalized +} + +func normalizeLicense(name string) string { + return NormalizeLicenseExpression(expr.SimpleExpr{License: name}).String() +} + +func NormalizeLicenseExpression(exp expr.Expression) expr.Expression { + switch e := exp.(type) { + case expr.SimpleExpr: + return normalizeSimpleExpr(e) + case expr.CompoundExpr: + return normalizeCompoundExpr(e) + } + return exp +} + +func normalizeSimpleExpr(e expr.SimpleExpr) expr.Expression { + // Always trim leading and trailing spaces, even if we don't find this license in `mapping`. + name := strings.TrimSpace(e.License) + normalized := standardizeKeyAndSuffix(name) + if found, ok := mapping[normalized.License]; ok { + return expr.SimpleExpr{License: found.License, HasPlus: e.HasPlus || found.HasPlus || normalized.HasPlus} + } + + // If not found in mapping, try to get the canonical SPDX license ID + if canonical, ok := expr.SPDXLicenseID(normalized.License); ok { + return expr.SimpleExpr{License: canonical, HasPlus: e.HasPlus || normalized.HasPlus} + } + + return expr.SimpleExpr{License: name, HasPlus: e.HasPlus} +} + +func normalizeCompoundExpr(e expr.CompoundExpr) expr.Expression { + if e.Conjunction() != expr.TokenWith { + return e // Do not normalize compound expressions other than "WITH" + } + normalized := standardizeKeyAndSuffix(e.String()) + if found, ok := mapping[normalized.License]; ok { + return expr.SimpleExpr{License: found.License, HasPlus: found.HasPlus || normalized.HasPlus} + } + return e // Do not normalize compound expressions that are not found in `mapping` +} + +func SplitLicenses(str string) []string { + if str == "" { + return nil + } + if isLicenseText(strings.ToLower(str)) { + return []string{ + LicenseTextPrefix + str, + } + } + + var licenses []string + for _, maybeLic := range licenseSplitRegexp.Split(str, -1) { + lower := strings.ToLower(maybeLic) + firstWord, _, _ := strings.Cut(lower, " ") + if len(licenses) > 0 { + // e.g. `Apache License, Version 2.0` + if firstWord == "ver" || firstWord == "version" { + licenses[len(licenses)-1] += ", " + maybeLic + continue + // e.g. `GNU Lesser General Public License v2 or later (LGPLv2+)` + } else if firstWord == "later" { + licenses[len(licenses)-1] += " or " + maybeLic + continue + } else if lic, ok := pythonLicenseExceptions[firstWord]; ok { + // Check `or` and `and` separators + if lic == licenses[len(licenses)-1]+" or "+maybeLic || lic == licenses[len(licenses)-1]+" and "+maybeLic { + licenses[len(licenses)-1] = lic + } + continue + } + } + licenses = append(licenses, maybeLic) + } + return licenses +} + +// Split license string considering spaces as separator +// e.g. MPL 2.0 GPL2+ => {"MPL2.0", "GPL2+"} +func LaxSplitLicenses(str string) []string { + if str == "" { + return nil + } + var licenses []string + var afterWith bool + str = versionRegexp.ReplaceAllString(str, "$1-$4") + for s := range strings.FieldsSeq(str) { + s = strings.Trim(s, "()") + switch s { + case "": + continue + case "AND", "OR": + continue + case "WITH": + afterWith = true + continue + default: + normalizedLicense := normalizeLicense(s) + if afterWith && len(licenses) > 0 { + // If we found "WITH" operator, we should not split the license + // e.g. "GPL-2 WITH Autoconf exception" => {"GPL-2 WITH Autoconf exception"} + licenses[len(licenses)-1] += " WITH " + normalizedLicense + afterWith = false + } else { + licenses = append(licenses, normalizedLicense) + } + } + } + return licenses +} diff --git a/pkg/licensing/normalize_test.go b/pkg/licensing/normalize_test.go new file mode 100644 index 000000000000..1125c9a73033 --- /dev/null +++ b/pkg/licensing/normalize_test.go @@ -0,0 +1,408 @@ +package licensing_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/licensing/expression" +) + +func TestSplitLicenses(t *testing.T) { + tests := []struct { + name string + license string + licenses []string + }{ + { + "simple list comma-separated", + "GPL-1+,GPL-2", + []string{ + "GPL-1+", + "GPL-2", + }, + }, + { + "simple list comma-separated", + "GPL-1+,GPL-2,GPL-3", + []string{ + "GPL-1+", + "GPL-2", + "GPL-3", + }, + }, + { + "3 licenses 'or'-separated", + "GPL-1+ or Artistic or Artistic-dist", + []string{ + "GPL-1+", + "Artistic", + "Artistic-dist", + }, + }, + { + "two licenses _or_ separated", + "LGPLv3+_or_GPLv2+", + []string{ + "LGPLv3+", + "GPLv2+", + }, + }, + { + "licenses `and`-separated", + "BSD-3-CLAUSE and GPL-2", + []string{ + "BSD-3-CLAUSE", + "GPL-2", + }, + }, + { + "three licenses and/or separated", + "GPL-1+ or Artistic, and BSD-4-clause-POWERDOG", + []string{ + "GPL-1+", + "Artistic", + "BSD-4-clause-POWERDOG", + }, + }, + { + "two licenses with version", + "Apache License,Version 2.0, OSET Public License version 2.1", + []string{ + "Apache License, Version 2.0", + "OSET Public License version 2.1", + }, + }, + { + "the license starts with `ver`", + "verbatim and BSD-4-clause", + []string{ + "verbatim", + "BSD-4-clause", + }, + }, + { + "the license with `or later`", + "GNU Affero General Public License v3 or later (AGPLv3+)", + []string{ + "GNU Affero General Public License v3 or later (AGPLv3+)", + }, + }, + { + "Python license exceptions", + "GNU Library or Lesser General Public License (LGPL), Common Development and Distribution License 1.0 (CDDL-1.0), Historical Permission Notice and Disclaimer (HPND)", + []string{ + "GNU Library or Lesser General Public License (LGPL)", + "Common Development and Distribution License 1.0 (CDDL-1.0)", + "Historical Permission Notice and Disclaimer (HPND)", + }, + }, + { + name: "License text", + license: "* Permission to use this software in any way is granted without", + licenses: []string{ + "text://* Permission to use this software in any way is granted without", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := licensing.SplitLicenses(tt.license) + assert.Equal(t, tt.licenses, res) + }) + } +} + +func TestLaxSplitLicense(t *testing.T) { + var tests = []struct { + license string + wantLicenses []string + }{ + { + license: "ASL 2.0", + wantLicenses: []string{"Apache-2.0"}, + }, + { + license: "MPL 2.0 GPL2+", + wantLicenses: []string{ + "MPL-2.0", + "GPL-2.0-or-later", + }, + }, + { + license: "GPL-2.0-only WITH Classpath-exception-2.0", + wantLicenses: []string{ + "GPL-2.0-only WITH Classpath-exception-2.0", + }, + }, + { + license: "BSD-3-CLAUSE OR GPL-2.0-only WITH Classpath-exception-2.0 AND MPL-2.0 ASL 2.0", + wantLicenses: []string{ + "BSD-3-Clause", + "GPL-2.0-only WITH Classpath-exception-2.0", + "MPL-2.0", + "Apache-2.0", + }, + }, + } + for _, tt := range tests { + t.Run(tt.license, func(t *testing.T) { + parsed := licensing.LaxSplitLicenses(tt.license) + assert.Equal(t, tt.wantLicenses, parsed) + }) + } +} + +// All map keys must be standardized to be matched +// (uppercase, no common suffixes, standardized version, etc.) +func TestMap(t *testing.T) { + for key := range licensing.Mapping() { + t.Run(key, func(t *testing.T) { + standardized := licensing.StandardizeKeyAndSuffix(key) + assert.Equal(t, standardized.License, key) + }) + } +} + +func TestNormalize(t *testing.T) { + tests := []struct { + licenses []expression.Expression + want string + wantLicense expression.Expression + }{ + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: " the apache license "}, + expression.SimpleExpr{License: " the\tapache \r\nlicense \r\n "}, + expression.SimpleExpr{License: " apache "}, + expression.SimpleExpr{License: "ApacheLicence"}, + expression.SimpleExpr{License: "ApacheLicense"}, + expression.SimpleExpr{License: "al-2"}, + expression.SimpleExpr{License: "al-v2"}, + expression.SimpleExpr{License: "al2"}, + expression.SimpleExpr{License: "alv2"}, + expression.SimpleExpr{License: "apache - v 2.0"}, + expression.SimpleExpr{License: "apache - v. 2.0"}, + expression.SimpleExpr{License: "apache - ver 2.0"}, + expression.SimpleExpr{License: "apache - version 2.0"}, + expression.SimpleExpr{License: "apache 2"}, + expression.SimpleExpr{License: "apache 2.0"}, + expression.SimpleExpr{License: "apache license (2.0)"}, + expression.SimpleExpr{License: "apache license (v. 2)"}, + expression.SimpleExpr{License: "apache license (v. 2.0)"}, + expression.SimpleExpr{License: "apache license (v2)"}, + expression.SimpleExpr{License: "apache license (v2.0)"}, + expression.SimpleExpr{License: "apache license (version 2.0)"}, + expression.SimpleExpr{License: "apache license 2"}, + expression.SimpleExpr{License: "apache license 2.0"}, + expression.SimpleExpr{License: "apache license v2"}, + expression.SimpleExpr{License: "apache license v2.0"}, + expression.SimpleExpr{License: "apache license version 2"}, + expression.SimpleExpr{License: "apache license version 2.0"}, + expression.SimpleExpr{License: "apache license"}, + expression.SimpleExpr{License: "apache license, 2.0"}, + expression.SimpleExpr{License: "apache license, asl version 2.0"}, + expression.SimpleExpr{License: "apache license, version 2"}, + expression.SimpleExpr{License: "apache license, version 2.0 (http://www.apache.org/licenses/license-2.0)"}, + expression.SimpleExpr{License: "apache license, version 2.0"}, + expression.SimpleExpr{License: "apache license,version 2.0"}, + expression.SimpleExpr{License: "apache license,version-2.0"}, + expression.SimpleExpr{License: "apache license-2.0"}, + expression.SimpleExpr{License: "apache public 2.0"}, + expression.SimpleExpr{License: "apache public license 2.0"}, + expression.SimpleExpr{License: "apache public license-2.0"}, + expression.SimpleExpr{License: "apache public-2"}, + expression.SimpleExpr{License: "apache public-2.0"}, + expression.SimpleExpr{License: "apache software license (apache-2.0)"}, + expression.SimpleExpr{License: "apache software license - version 2.0"}, + expression.SimpleExpr{License: "apache software license 2.0"}, + expression.SimpleExpr{License: "apache software license, version 2"}, + expression.SimpleExpr{License: "apache software license, version 2.0"}, + expression.SimpleExpr{License: "apache software-2.0"}, + expression.SimpleExpr{License: "apache v 2.0"}, + expression.SimpleExpr{License: "apache v. 2.0"}, + expression.SimpleExpr{License: "apache v2"}, + expression.SimpleExpr{License: "apache v2.0"}, + expression.SimpleExpr{License: "apache ver 2.0"}, + expression.SimpleExpr{License: "apache ver. 2.0"}, + expression.SimpleExpr{License: "apache version 2.0"}, + expression.SimpleExpr{License: "apache version 2.0, january 2004"}, + expression.SimpleExpr{License: "apache version-2"}, + expression.SimpleExpr{License: "apache version-2.0"}, + expression.SimpleExpr{License: "apache"}, + expression.SimpleExpr{License: "apache, 2"}, + expression.SimpleExpr{License: "apache, v2.0"}, + expression.SimpleExpr{License: "apache, version 2"}, + expression.SimpleExpr{License: "apache, version 2.0"}, + expression.SimpleExpr{License: "apache-2"}, + expression.SimpleExpr{License: "apache-2.0"}, + expression.SimpleExpr{License: "apache-licence"}, + expression.SimpleExpr{License: "apache-license"}, + expression.SimpleExpr{License: "apache-licensed"}, + expression.SimpleExpr{License: "apache-licensed"}, + expression.SimpleExpr{License: "asf 2.0"}, + expression.SimpleExpr{License: "asl 2"}, + expression.SimpleExpr{License: "asl, version 2"}, + expression.SimpleExpr{License: "asl2.0"}, + expression.SimpleExpr{License: "the apache license"}, + expression.SimpleExpr{License: "the apache license"}, + }, + want: "Apache-2.0", + wantLicense: expression.SimpleExpr{License: "Apache-2.0"}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "Apache+"}, + }, + want: "Apache-2.0+", + wantLicense: expression.SimpleExpr{License: "Apache-2.0", HasPlus: true}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) V1.1"}, + expression.SimpleExpr{License: "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) VERSION 1.1"}, + expression.SimpleExpr{License: "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL), VERSION 1.1"}, + expression.SimpleExpr{License: "COMMON DEVELOPMENT AND DISTRIBUTION LICENSE 1.1 (CDDL-1.1)"}, + }, + want: "CDDL-1.1", + wantLicense: expression.SimpleExpr{License: "CDDL-1.1"}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE (EPL) 1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE (EPL), VERSION 1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE - V 1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE - V1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE - VERSION 1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE 1.0 (EPL-1.0)"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE 1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE V. 1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE V1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE VERSION 1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC LICENSE, VERSION 1.0"}, + expression.SimpleExpr{License: "ECLIPSE PUBLIC"}, + }, + want: "EPL-1.0", + wantLicense: expression.SimpleExpr{License: "EPL-1.0"}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "EUROPEAN UNION PUBLIC LICENSE (EUPL V.1.1)"}, + expression.SimpleExpr{License: "EUROPEAN UNION PUBLIC LICENSE 1.1 (EUPL 1.1)"}, + expression.SimpleExpr{License: "EUROPEAN UNION PUBLIC LICENSE 1.1"}, + expression.SimpleExpr{License: "EUROPEAN UNION PUBLIC LICENSE, VERSION 1.1"}, + }, + want: "EUPL-1.1", + wantLicense: expression.SimpleExpr{License: "EUPL-1.1"}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "GPL-or-later"}, + expression.SimpleExpr{License: "GPL+"}, + expression.SimpleExpr{License: "GPL-2.0-only+"}, + }, + want: "GPL-2.0-or-later", + wantLicense: expression.SimpleExpr{License: "GPL-2.0", HasPlus: true}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "GPL (≥ 3)"}, + expression.SimpleExpr{License: "GPL3+"}, + expression.SimpleExpr{License: "GPL3-or-later"}, + expression.SimpleExpr{License: "GPL3 or later licence"}, + }, + want: "GPL-3.0-or-later", + wantLicense: expression.SimpleExpr{License: "GPL-3.0", HasPlus: true}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "GNU GENERAL PUBLIC LICENSE 3"}, + expression.SimpleExpr{License: "GNU GENERAL PUBLIC LICENSE (GPL) V. 3"}, + expression.SimpleExpr{License: "GNU GENERAL PUBLIC LICENSE VERSION 3 (GPL V3)"}, + }, + want: "GPL-3.0-only", + wantLicense: expression.SimpleExpr{License: "GPL-3.0"}, + }, + + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "LGPL LICENSE-3"}, + expression.SimpleExpr{License: "GNU LESSER GENERAL PUBLIC LICENSE V3"}, + expression.SimpleExpr{License: "GNU LESSER GENERAL PUBLIC LICENSE V3.0"}, + expression.SimpleExpr{License: "GNU LESSER GENERAL PUBLIC LICENSE VERSION 3"}, + expression.SimpleExpr{License: "GNU LESSER GENERAL PUBLIC LICENSE VERSION 3.0"}, + expression.SimpleExpr{License: "GNU LESSER GENERAL PUBLIC LICENSE, VERSION 3.0"}, + expression.SimpleExpr{License: "GNU LIBRARY OR LESSER GENERAL PUBLIC LICENSE VERSION 3.0 (LGPLV3)"}, + expression.SimpleExpr{License: "GNU GENERAL LESSER PUBLIC LICENSE (LGPL) VERSION 3.0"}, + expression.SimpleExpr{License: "GNU LESSER GENERAL PUBLIC LICENSE (LGPL), VERSION 3"}, + }, + want: "LGPL-3.0-only", + wantLicense: expression.SimpleExpr{License: "LGPL-3.0"}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "The Unlicense"}, + expression.SimpleExpr{License: "Unlicense"}, + expression.SimpleExpr{License: "UNLICENSE"}, + }, + want: "Unlicense", + wantLicense: expression.SimpleExpr{License: "Unlicense"}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "MIT License"}, + expression.SimpleExpr{License: "http://json.codeplex.com/license"}, + }, + want: "MIT", + wantLicense: expression.SimpleExpr{License: "MIT"}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: " The unmapped license "}, + }, + want: "The unmapped license", + wantLicense: expression.SimpleExpr{License: "The unmapped license"}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "Universal Permissive License, Version 1.0"}, + }, + want: "UPL-1.0", + wantLicense: expression.SimpleExpr{License: "UPL-1.0"}, + }, + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "GPLv2 WITH EXCEPTIONS"}, + expression.NewCompoundExpr( // "GPLv2 WITH EXCEPTIONS" + expression.SimpleExpr{License: "GPLv2"}, + expression.TokenWith, + expression.SimpleExpr{License: "EXCEPTIONS"}, + ), + }, + want: "GPL-2.0-with-classpath-exception", + wantLicense: expression.SimpleExpr{License: "GPL-2.0-with-classpath-exception"}, + }, + // Test canonical SPDX license casing + { + licenses: []expression.Expression{ + expression.SimpleExpr{License: "Tcl"}, + expression.SimpleExpr{License: "tcl"}, + expression.SimpleExpr{License: "TCL"}, + }, + want: "TCL", + wantLicense: expression.SimpleExpr{License: "TCL"}, + }, + } + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + for _, ll := range tt.licenses { + got := licensing.NormalizeLicense(ll.String()) + gotLicense := licensing.NormalizeLicenseExpression(ll) + assert.Equal(t, tt.want, got) + assert.Equal(t, tt.wantLicense, gotLicense) + } + }) + } +} diff --git a/pkg/licensing/scanner.go b/pkg/licensing/scanner.go new file mode 100644 index 000000000000..1831319b9755 --- /dev/null +++ b/pkg/licensing/scanner.go @@ -0,0 +1,124 @@ +package licensing + +import ( + "regexp" + "strings" + + "github.com/samber/lo" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing/expression" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" +) + +type Scanner struct { + categories map[types.LicenseCategory]set.Set[string] +} + +func NewScanner(categories map[types.LicenseCategory][]string) Scanner { + return Scanner{ + categories: lo.MapValues(categories, func(v []string, _ types.LicenseCategory) set.Set[string] { + return set.NewCaseInsensitive(v...) + }), + } +} + +func (s *Scanner) Scan(licenseName string) (types.LicenseCategory, string) { + expr, err := expression.Normalize(licenseName, NormalizeLicenseExpression) + if err != nil { + return types.CategoryUnknown, "" + } + category := s.detectCategory(expr) + + return category, categoryToSeverity(category).String() +} + +// detectCategory recursively parses license expression to detect correct license category: +// For the simple expression - use category of license +// For the compound expression: +// - `AND` operator - use category with maximum severity +// - `OR` operator - use category with minimum severity +// - one of expression has `UNKNOWN` category - use `UNKNOWN` category +func (s *Scanner) detectCategory(license expression.Expression) types.LicenseCategory { + var category types.LicenseCategory + + switch e := license.(type) { + case expression.SimpleExpr: + category = s.licenseToCategory(e) + case expression.CompoundExpr: + // Detect license category for `WITH` operator as a simple expression + if e.Conjunction() == expression.TokenWith { + category = s.licenseToCategory(expression.SimpleExpr{ + License: e.String(), + }) + break + } + + left := s.detectCategory(e.Left()) + right := s.detectCategory(e.Right()) + if left == types.CategoryUnknown || right == types.CategoryUnknown { + category = types.CategoryUnknown + break + } + comparison := func(a, b types.LicenseCategory) bool { + if e.Conjunction() == expression.TokenAnd { + return int(categoryToSeverity(a)) > int(categoryToSeverity(b)) // Take the maximum severity for `AND` operator + } + return int(categoryToSeverity(a)) < int(categoryToSeverity(b)) // Take the minimum severity for `OR` operator + } + category = lo.MaxBy([]types.LicenseCategory{left, right}, comparison) + } + + return category +} + +// ScanTextLicense checks license names from `categories` as glob patterns and matches licenseText against those patterns. +// If a match is found, it returns `unknown` category and severity. +func (s *Scanner) ScanTextLicense(licenseText string) (types.LicenseCategory, string) { + for cat, names := range s.categories { + for name := range names.Iter() { + n, ok := strings.CutPrefix(name, LicenseTextPrefix) + if !ok { + continue + } + + match, err := regexp.MatchString(n, licenseText) + if err != nil { + log.WithPrefix("license").Debug("Failed to match license text", log.String("license_text", licenseText), log.Err(err)) + continue + } else if match { + return cat, categoryToSeverity(cat).String() + } + } + } + return types.CategoryUnknown, dbTypes.SeverityUnknown.String() +} + +func categoryToSeverity(category types.LicenseCategory) dbTypes.Severity { + switch category { + case types.CategoryForbidden: + return dbTypes.SeverityCritical + case types.CategoryRestricted: + return dbTypes.SeverityHigh + case types.CategoryReciprocal: + return dbTypes.SeverityMedium + case types.CategoryNotice, types.CategoryPermissive, types.CategoryUnencumbered: + return dbTypes.SeverityLow + } + return dbTypes.SeverityUnknown +} + +func (s *Scanner) licenseToCategory(se expression.SimpleExpr) types.LicenseCategory { + normalizedNames := set.NewCaseInsensitive(se.String()) // The license name with suffix (e.g. AGPL-1.0-or-later) + normalizedNames.Append(se.License) // Also accept the license name without suffix (e.g. AGPL-1.0) + + for category, names := range s.categories { + if normalizedNames.Intersection(names).Size() > 0 { + return category + } + } + + return types.CategoryUnknown +} diff --git a/pkg/licensing/scanner_test.go b/pkg/licensing/scanner_test.go new file mode 100644 index 000000000000..4e0d36706283 --- /dev/null +++ b/pkg/licensing/scanner_test.go @@ -0,0 +1,248 @@ +package licensing_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/licensing/expression" +) + +func TestScanner_Scan(t *testing.T) { + tests := []struct { + name string + categories map[types.LicenseCategory][]string + licenseName string + wantCategory types.LicenseCategory + wantSeverity string + }{ + { + name: "forbidden", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: { + expression.BSD3Clause, + expression.Apache20, + }, + }, + licenseName: expression.Apache20, + wantCategory: types.CategoryForbidden, + wantSeverity: "CRITICAL", + }, + { + name: "has plus", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: { + expression.BSD3Clause, + expression.Apache20, + }, + }, + licenseName: "Apache-2.0+", + wantCategory: types.CategoryForbidden, + wantSeverity: "CRITICAL", + }, + { + name: "`categories` contains license with suffix", + categories: map[types.LicenseCategory][]string{ + types.CategoryNotice: { + "LGPL-2.0-only", + }, + }, + licenseName: "LGPL-2.0-only", + wantCategory: types.CategoryNotice, + wantSeverity: "LOW", + }, + { + name: "restricted", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: { + expression.GPL30, + }, + types.CategoryRestricted: { + expression.BSD3Clause, + expression.Apache20, + }, + }, + licenseName: expression.BSD3Clause, + wantCategory: types.CategoryRestricted, + wantSeverity: "HIGH", + }, + { + name: "unnormalized license", + categories: map[types.LicenseCategory][]string{ + types.CategoryRestricted: { + expression.BSD3Clause, + expression.MIT, + }, + }, + licenseName: "MIT License", + wantCategory: types.CategoryRestricted, + wantSeverity: "HIGH", + }, + { + name: "compound OR license", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: { + expression.GPL30, + }, + types.CategoryRestricted: { + expression.Apache20, + }, + }, + licenseName: expression.GPL30 + " OR " + expression.Apache20, + wantCategory: types.CategoryRestricted, + wantSeverity: "HIGH", + }, + { + name: "compound AND license", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: { + expression.GPL30, + }, + types.CategoryRestricted: { + expression.Apache20, + }, + }, + licenseName: expression.GPL30 + " AND " + expression.Apache20, + wantCategory: types.CategoryForbidden, + wantSeverity: "CRITICAL", + }, + { + name: "license with exception", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: { + "GPL-2.0-only WITH Classpath-exception-2.0", + expression.GPL30, + expression.Apache20, + }, + }, + licenseName: "GPL-3.0-only OR GPL-2.0-only WITH Classpath-exception-2.0 AND Apache-2.0", + wantCategory: types.CategoryForbidden, + wantSeverity: "CRITICAL", + }, + { + name: "with separator in lowercase", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: { + "Similar to Apache License but with the acknowledgment clause removed.", + }, + }, + licenseName: "Similar to Apache License but with the acknowledgment clause removed.", + wantCategory: types.CategoryForbidden, + wantSeverity: "CRITICAL", + }, + { + name: "compound unknown license", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: { + expression.GPL30, + }, + }, + licenseName: expression.GPL30 + " AND " + expression.Apache20, + wantCategory: types.CategoryUnknown, + wantSeverity: "UNKNOWN", + }, + { + name: "compound long license, recursive", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: { + expression.GPL30, + }, + types.CategoryRestricted: { + expression.BSD3Clause, + }, + types.CategoryNotice: { + expression.Apache20, + }, + }, + licenseName: "(" + expression.BSD3Clause + " OR " + expression.GPL30 + ")" + " AND (" + expression.GPL30 + " OR " + expression.Apache20 + ")", + wantCategory: types.CategoryRestricted, + wantSeverity: "HIGH", + }, + { + name: "unknown", + categories: make(map[types.LicenseCategory][]string), + licenseName: expression.BSD3Clause, + wantCategory: types.CategoryUnknown, + wantSeverity: "UNKNOWN", + }, + { + // `Unlicensed` is a special license name in npm. + // It means the developer does not grant anyone the right to use the private or unpublished package under any circumstances. + name: "'unlicensed' npm license as unknown", + categories: map[types.LicenseCategory][]string{ + types.CategoryUnencumbered: { + expression.Unlicense, + }, + }, + licenseName: "UNLICENSED", + wantCategory: types.CategoryUnknown, + wantSeverity: "UNKNOWN", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := licensing.NewScanner(tt.categories) + gotCategory, gotSeverity := s.Scan(tt.licenseName) + assert.Equalf(t, tt.wantCategory, gotCategory, "Scan(%v)", tt.licenseName) + assert.Equalf(t, tt.wantSeverity, gotSeverity, "Scan(%v)", tt.licenseName) + }) + } +} + +func TestScanner_ScanTextLicense(t *testing.T) { + tests := []struct { + name string + categories map[types.LicenseCategory][]string + licenseText string + wantCategory types.LicenseCategory + wantSeverity string + }{ + { + name: "match license text pattern", + categories: map[types.LicenseCategory][]string{ + types.CategoryForbidden: {"text://Apache.*License"}, + }, + licenseText: "Apache Software Foundation License", + wantCategory: types.CategoryForbidden, + wantSeverity: "CRITICAL", + }, + { + name: "no match returns unknown", + categories: map[types.LicenseCategory][]string{ + types.CategoryNotice: {"text://MIT.*"}, + }, + licenseText: "Some other license text", + wantCategory: types.CategoryUnknown, + wantSeverity: "UNKNOWN", + }, + { + name: "invalid regexp is ignored", + categories: map[types.LicenseCategory][]string{ + types.CategoryRestricted: {"text://("}, + }, + licenseText: "MIT License", + wantCategory: types.CategoryUnknown, + wantSeverity: "UNKNOWN", + }, + { + name: "category without text prefix is ignored", + categories: map[types.LicenseCategory][]string{ + types.CategoryNotice: {"MIT"}, + }, + licenseText: "MIT License", + wantCategory: types.CategoryUnknown, + wantSeverity: "UNKNOWN", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := licensing.NewScanner(tt.categories) + gotCategory, gotSeverity := s.ScanTextLicense(tt.licenseText) + assert.Equal(t, tt.wantCategory, gotCategory) + assert.Equal(t, tt.wantSeverity, gotSeverity) + }) + } +} diff --git a/pkg/licensing/testdata/LICENSE_apache2 b/pkg/licensing/testdata/LICENSE_apache2 new file mode 100644 index 000000000000..261eeb9e9f8b --- /dev/null +++ b/pkg/licensing/testdata/LICENSE_apache2 @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/pkg/licensing/testdata/LICENSE_creativecommons b/pkg/licensing/testdata/LICENSE_creativecommons new file mode 100644 index 000000000000..85097f820dd2 --- /dev/null +++ b/pkg/licensing/testdata/LICENSE_creativecommons @@ -0,0 +1,13 @@ +Commons Clause Restriction + +The Software is provided to you by the Licensor under the License, as defined below, subject to +the following condition. + +Without limiting other conditions in the License, the grant of rights under the License will not +include, and the License does not grant to you, the right to Sell the Software. +For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you +under the License to provide to third parties, for a fee or other consideration (including without +limitation fees for hosting or consulting/ support services related to the Software), a product or +service whose value derives, entirely or substantially, from the functionality of the Software. +Any license notice or attribution required by the License must also include this Commons Cause +License Condition notice. diff --git a/pkg/licensing/testdata/licensed.c b/pkg/licensing/testdata/licensed.c new file mode 100644 index 000000000000..f66b373b7e68 --- /dev/null +++ b/pkg/licensing/testdata/licensed.c @@ -0,0 +1,24 @@ +/* +* This program is free software: you can redistribute it and/or modify +* it under the terms of the GNU Affero General Public License as published by +* the Free Software Foundation, either version 3 of the License, or +* (at your option) any later version. + +* This program is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +* GNU Affero General Public License for more details. + +* You should have received a copy of the GNU Affero General Public License +* along with this program. If not, see . +*/ + +int func1() { + /* implementation */ + return 0; +} + +int func2() { + /* implementation */ + return 0; +} diff --git a/pkg/licensing/testdata/styles.css b/pkg/licensing/testdata/styles.css new file mode 100644 index 000000000000..9e842cbc841c --- /dev/null +++ b/pkg/licensing/testdata/styles.css @@ -0,0 +1,409 @@ +/*! + * Datepicker for Bootstrap + * + * Copyright 2012 Stefan Petre + * Improvements by Andrew Rowls + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * +*/ + +.datepicker { + padding: 4px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + direction: ltr; + /*.dow { + border-top: 1px solid #ddd !important; + }*/ + +} +.datepicker-inline { + width: 220px; +} +.datepicker.datepicker-rtl { + direction: rtl; +} +.datepicker.datepicker-rtl table tr td span { + float: right; +} +.datepicker-dropdown { + top: 0; + left: 0; +} +.datepicker-dropdown:before { + content: ''; + display: inline-block; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid #ccc; + border-bottom-color: rgba(0, 0, 0, 0.2); + position: absolute; + top: -7px; + left: 6px; +} +.datepicker-dropdown:after { + content: ''; + display: inline-block; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid #ffffff; + position: absolute; + top: -6px; + left: 7px; +} +.datepicker > div { + display: none; +} +.datepicker.days div.datepicker-days { + display: block; +} +.datepicker.months div.datepicker-months { + display: block; +} +.datepicker.years div.datepicker-years { + display: block; +} +.datepicker table { + margin: 0; +} +.datepicker td, +.datepicker th { + text-align: center; + width: 20px; + height: 20px; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + border: none; +} +.table-striped .datepicker table tr td, +.table-striped .datepicker table tr th { + background-color: transparent; +} +.datepicker table tr td.day:hover { + background: #eeeeee; + cursor: pointer; +} +.datepicker table tr td.old, +.datepicker table tr td.new { + color: #999999; +} +.datepicker table tr td.disabled, +.datepicker table tr td.disabled:hover { + background: none; + color: #999999; + cursor: default; +} +.datepicker table tr td.today, +.datepicker table tr td.today:hover, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today.disabled:hover { + background-color: #fde19a; + background-image: linear-gradient(to top, #fdd49a, #fdf59a); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fdd49a', endColorstr='#fdf59a', GradientType=0); + border-color: #fdf59a #fdf59a #fbed50; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #000 !important; +} +.datepicker table tr td.today:hover, +.datepicker table tr td.today:hover:hover, +.datepicker table tr td.today.disabled:hover, +.datepicker table tr td.today.disabled:hover:hover, +.datepicker table tr td.today:active, +.datepicker table tr td.today:hover:active, +.datepicker table tr td.today.disabled:active, +.datepicker table tr td.today.disabled:hover:active, +.datepicker table tr td.today.active, +.datepicker table tr td.today:hover.active, +.datepicker table tr td.today.disabled.active, +.datepicker table tr td.today.disabled:hover.active, +.datepicker table tr td.today.disabled, +.datepicker table tr td.today:hover.disabled, +.datepicker table tr td.today.disabled.disabled, +.datepicker table tr td.today.disabled:hover.disabled, +.datepicker table tr td.today[disabled], +.datepicker table tr td.today:hover[disabled], +.datepicker table tr td.today.disabled[disabled], +.datepicker table tr td.today.disabled:hover[disabled] { + background-color: #fdf59a; +} +.datepicker table tr td.today:active, +.datepicker table tr td.today:hover:active, +.datepicker table tr td.today.disabled:active, +.datepicker table tr td.today.disabled:hover:active, +.datepicker table tr td.today.active, +.datepicker table tr td.today:hover.active, +.datepicker table tr td.today.disabled.active, +.datepicker table tr td.today.disabled:hover.active { + background-color: #fbf069 \9; +} +.datepicker table tr td.active, +.datepicker table tr td.active:hover, +.datepicker table tr td.active.disabled, +.datepicker table tr td.active.disabled:hover { + background-color: #006dcc; + background-image: linear-gradient(to top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td.active:hover, +.datepicker table tr td.active:hover:hover, +.datepicker table tr td.active.disabled:hover, +.datepicker table tr td.active.disabled:hover:hover, +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active, +.datepicker table tr td.active.disabled, +.datepicker table tr td.active:hover.disabled, +.datepicker table tr td.active.disabled.disabled, +.datepicker table tr td.active.disabled:hover.disabled, +.datepicker table tr td.active[disabled], +.datepicker table tr td.active:hover[disabled], +.datepicker table tr td.active.disabled[disabled], +.datepicker table tr td.active.disabled:hover[disabled] { + background-color: #0044cc; +} +.datepicker table tr td.active:active, +.datepicker table tr td.active:hover:active, +.datepicker table tr td.active.disabled:active, +.datepicker table tr td.active.disabled:hover:active, +.datepicker table tr td.active.active, +.datepicker table tr td.active:hover.active, +.datepicker table tr td.active.disabled.active, +.datepicker table tr td.active.disabled:hover.active { + background-color: #003399 \9; +} +.datepicker table tr td span { + display: block; + width: 23%; + height: 54px; + line-height: 54px; + float: left; + margin: 1%; + cursor: pointer; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.datepicker table tr td span:hover { + background: #eeeeee; +} +.datepicker table tr td span.disabled, +.datepicker table tr td span.disabled:hover { + background: none; + color: #999999; + cursor: default; +} +.datepicker table tr td span.active, +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active.disabled, +.datepicker table tr td span.active.disabled:hover { + background-color: #006dcc; + background-image: linear-gradient(to top, #0088cc, #0044cc); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0088cc', endColorstr='#0044cc', GradientType=0); + border-color: #0044cc #0044cc #002a80; + border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25); + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); + color: #fff; + text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25); +} +.datepicker table tr td span.active:hover, +.datepicker table tr td span.active:hover:hover, +.datepicker table tr td span.active.disabled:hover, +.datepicker table tr td span.active.disabled:hover:hover, +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active, +.datepicker table tr td span.active.disabled, +.datepicker table tr td span.active:hover.disabled, +.datepicker table tr td span.active.disabled.disabled, +.datepicker table tr td span.active.disabled:hover.disabled, +.datepicker table tr td span.active[disabled], +.datepicker table tr td span.active:hover[disabled], +.datepicker table tr td span.active.disabled[disabled], +.datepicker table tr td span.active.disabled:hover[disabled] { + background-color: #0044cc; +} +.datepicker table tr td span.active:active, +.datepicker table tr td span.active:hover:active, +.datepicker table tr td span.active.disabled:active, +.datepicker table tr td span.active.disabled:hover:active, +.datepicker table tr td span.active.active, +.datepicker table tr td span.active:hover.active, +.datepicker table tr td span.active.disabled.active, +.datepicker table tr td span.active.disabled:hover.active { + background-color: #003399 \9; +} +.datepicker table tr td span.old { + color: #999999; +} +.datepicker th.switch { + width: 145px; +} +.datepicker thead tr:first-child th, +.datepicker tfoot tr:first-child th { + cursor: pointer; +} +.datepicker thead tr:first-child th:hover, +.datepicker tfoot tr:first-child th:hover { + background: #eeeeee; +} +.datepicker .cw { + font-size: 10px; + width: 12px; + padding: 0 2px 0 5px; + vertical-align: middle; +} +.datepicker thead tr:first-child th.cw { + cursor: default; + background-color: transparent; +} +.input-append.date .add-on i, +.input-prepend.date .add-on i { + display: block; + cursor: pointer; + width: 16px; + height: 16px; +} +/*! + * Timepicker Component for Twitter Bootstrap + * + * Copyright 2013 Joris de Wit + * + * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +.bootstrap-timepicker { + position: relative; +} +.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu { + left: auto; + right: 0; +} +.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:before { + left: auto; + right: 12px; +} +.bootstrap-timepicker.pull-right .bootstrap-timepicker-widget.dropdown-menu:after { + left: auto; + right: 13px; +} +.bootstrap-timepicker .add-on { + cursor: pointer; +} +.bootstrap-timepicker .add-on i { + display: inline-block; + width: 16px; + height: 16px; +} +.bootstrap-timepicker-widget.dropdown-menu { + padding: 2px 3px 2px 2px; +} +.bootstrap-timepicker-widget.dropdown-menu.open { + display: inline-block; +} +.bootstrap-timepicker-widget.dropdown-menu:before { + border-bottom: 7px solid rgba(0, 0, 0, 0.2); + border-left: 7px solid transparent; + border-right: 7px solid transparent; + content: ""; + display: inline-block; + left: 9px; + position: absolute; + top: -7px; +} +.bootstrap-timepicker-widget.dropdown-menu:after { + border-bottom: 6px solid #FFFFFF; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + content: ""; + display: inline-block; + left: 10px; + position: absolute; + top: -6px; +} +.bootstrap-timepicker-widget a.btn, +.bootstrap-timepicker-widget input { + border-radius: 4px; +} +.bootstrap-timepicker-widget table { + width: 100%; + margin: 0; +} +.bootstrap-timepicker-widget table td { + text-align: center; + height: 30px; + margin: 0; + padding: 2px; +} +.bootstrap-timepicker-widget table td:not(.separator) { + min-width: 30px; +} +.bootstrap-timepicker-widget table td span { + width: 100%; +} +.bootstrap-timepicker-widget table td a { + border: 1px transparent solid; + width: 100%; + display: inline-block; + margin: 0; + padding: 8px 0; + outline: 0; + color: #333; +} +.bootstrap-timepicker-widget table td a:hover { + text-decoration: none; + background-color: #eee; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + border-color: #ddd; +} +.bootstrap-timepicker-widget table td a i { + margin-top: 2px; +} +.bootstrap-timepicker-widget table td input { + width: 25px; + margin: 0; + text-align: center; +} +.bootstrap-timepicker-widget .modal-content { + padding: 4px; +} +@media (min-width: 767px) { + .bootstrap-timepicker-widget.modal { + width: 200px; + margin-left: -100px; + } +} +@media (max-width: 767px) { + .bootstrap-timepicker { + width: 100%; + } + + .bootstrap-timepicker .dropdown-menu { + width: 100%; + } +} \ No newline at end of file diff --git a/pkg/licensing/testdata/unlicensed.c b/pkg/licensing/testdata/unlicensed.c new file mode 100644 index 000000000000..30a3abaf63b6 --- /dev/null +++ b/pkg/licensing/testdata/unlicensed.c @@ -0,0 +1,10 @@ + +int func1() { + /* implementation */ + return 0; +} + +int func2() { + /* implementation */ + return 0; +} diff --git a/pkg/log/context.go b/pkg/log/context.go new file mode 100644 index 000000000000..67a161875d0f --- /dev/null +++ b/pkg/log/context.go @@ -0,0 +1,47 @@ +package log + +import ( + "context" + "log/slog" +) + +// prefixContextKey is the context key for logger. +// It is unexported to prevent collisions with context keys defined in other packages. +type prefixContextKey struct{} + +// WithContextPrefix returns a new context with the given prefix. +func WithContextPrefix(ctx context.Context, prefix string) context.Context { + if prefix == "" { + return ctx + } + return context.WithValue(ctx, prefixContextKey{}, "["+prefix+"] ") +} + +func contextualPrefix(ctx context.Context) string { + if prefix, ok := ctx.Value(prefixContextKey{}).(string); ok { + return prefix + } + return "" +} + +// attrContextKey is the context key for logger. +// It is unexported to prevent collisions with context keys defined in other packages. +type attrContextKey struct{} + +// WithContextAttrs returns a new context with the given attrs. +func WithContextAttrs(ctx context.Context, attrs ...slog.Attr) context.Context { + if len(attrs) == 0 { + return ctx + } + if ctxAttrs := contextualAttrs(ctx); ctxAttrs != nil { + attrs = append(ctxAttrs, attrs...) + } + return context.WithValue(ctx, attrContextKey{}, attrs) +} + +func contextualAttrs(ctx context.Context) []slog.Attr { + if attrs, ok := ctx.Value(attrContextKey{}).([]slog.Attr); ok { + return attrs + } + return nil +} diff --git a/pkg/log/deferred.go b/pkg/log/deferred.go new file mode 100644 index 000000000000..9fbf81652f4d --- /dev/null +++ b/pkg/log/deferred.go @@ -0,0 +1,62 @@ +package log + +import ( + "context" + "log/slog" + "slices" +) + +func init() { + // Set the default logger so that logs are buffered until the logger is initialized. + slog.SetDefault(New(&DeferredHandler{records: new([]deferredRecord)})) +} + +// DeferredHandler is needed to save logs and print them after calling `PrintLogs()` command. +// For example, this may be necessary when the logger is not yet initialized, but messages need to be transmitted. +// In this case, the messages are saved and printed when the logger is initialized. +type DeferredHandler struct { + attrs []slog.Attr + + // Shared with all instances of the handler. + // NOTE: non-thread safe + records *[]deferredRecord +} + +type deferredRecord struct { + ctx context.Context + slog.Record +} + +func (*DeferredHandler) Enabled(context.Context, slog.Level) bool { + return true +} + +func (d *DeferredHandler) Handle(ctx context.Context, record slog.Record) error { + record.AddAttrs(d.attrs...) + *d.records = append(*d.records, deferredRecord{ + ctx: ctx, + Record: record, + }) + return nil +} + +func (d *DeferredHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + h := *d + h.attrs = slices.Clip(d.attrs) + h.attrs = append(h.attrs, attrs...) + return &h +} + +func (*DeferredHandler) WithGroup(_ string) slog.Handler { + panic("WithGroup is not implemented") +} + +func (d *DeferredHandler) Flush(h slog.Handler) { + for _, record := range *d.records { + if !h.Enabled(record.ctx, record.Level) { + continue + } + _ = h.Handle(record.ctx, record.Record) + } + d.records = nil +} diff --git a/pkg/log/handler.go b/pkg/log/handler.go new file mode 100644 index 000000000000..9bbfd8060e3f --- /dev/null +++ b/pkg/log/handler.go @@ -0,0 +1,317 @@ +package log + +import ( + "bytes" + "context" + "fmt" + "io" + "log/slog" + "slices" + "strconv" + "sync" + "time" + + "github.com/fatih/color" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/clock" +) + +const ( + errKey = "err" + prefixKey = "prefix" +) + +type ColorHandler struct { + opts Options + prefix string + preformatted []byte // data from WithGroup and WithAttrs + groups []string // groups from WithGroup + mu *sync.Mutex + out io.Writer +} + +type Options struct { + // Level reports the minimum level to log. + // Levels with lower levels are discarded. + // If nil, the Handler uses [slog.LevelInfo]. + Level slog.Leveler +} + +func NewHandler(out io.Writer, opts *Options) *ColorHandler { + h := &ColorHandler{ + out: out, + mu: &sync.Mutex{}, + } + if opts != nil { + h.opts = *opts + } + if h.opts.Level == nil { + h.opts.Level = slog.LevelInfo + } + return h +} + +func (h *ColorHandler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.opts.Level.Level() +} + +func (h *ColorHandler) WithGroup(name string) slog.Handler { + if name == "" { + return h + } + h2 := *h + // Add an unopened group to h2 without modifying h. + h2.groups = make([]string, len(h.groups)+1) + copy(h2.groups, h.groups) + h2.groups[len(h2.groups)-1] = name + return &h2 +} + +func (h *ColorHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + if len(attrs) == 0 { + return h + } + h2 := *h + + // Force an append to copy the underlying array. + h2.preformatted = slices.Clip(h.preformatted) + + // Pre-format the attributes. + for _, a := range attrs { + if isLogPrefix(a) { + h2.prefix = string(a.Value.Any().(logPrefix)) + continue + } + h2.preformatted = h2.appendAttr(h2.preformatted, a, h.groups) + } + return &h2 +} + +func (h *ColorHandler) appendAttr(buf []byte, a slog.Attr, groups []string) []byte { + // Resolve the Attr's value before doing anything else. + a.Value = a.Value.Resolve() + // Ignore empty Attrs and log prefixes. + if a.Equal(slog.Attr{}) || isLogPrefix(a) { + return buf + } + + var key string + for _, g := range groups { + key += g + "." + } + key += a.Key + + switch a.Value.Kind() { + case slog.KindString: + // Quote string values, to make them easy to parse. + buf = append(buf, key...) + buf = append(buf, '=') + buf = strconv.AppendQuote(buf, a.Value.String()) + case slog.KindTime: + // Write times in a standard way, without the monotonic time. + buf = append(buf, key...) + buf = append(buf, '=') + buf = a.Value.Time().AppendFormat(buf, time.RFC3339Nano) + case slog.KindGroup: + attrs := a.Value.Group() + // Ignore empty groups. + if len(attrs) == 0 { + return buf + } + if a.Key != "" { + groups = append(groups, a.Key) + } + for _, ga := range attrs { + buf = h.appendAttr(buf, ga, groups) + } + buf = bytes.TrimRight(buf, " ") // Trim the trailing space. + default: + buf = append(buf, key...) + buf = append(buf, '=') + if err, ok := a.Value.Any().(error); ok { + buf = append(buf, color.HiRedString(strconv.Quote(err.Error()))...) + } else { + buf = append(buf, a.Value.String()...) + } + } + return append(buf, ' ') +} + +func (h *ColorHandler) Handle(ctx context.Context, r slog.Record) error { + bufp := allocBuf() + buf := *bufp + defer func() { + *bufp = buf + freeBuf(bufp) + }() + + // For tests, use the fake clock's time. + if c, ok := clock.Clock(ctx).(*clock.FakeClock); ok { + r.Time = c.Now() + } + + buf = h.handle(ctx, buf, r) + + h.mu.Lock() + defer h.mu.Unlock() + + if _, err := h.out.Write(buf); err != nil { + return xerrors.Errorf("failed to write log: %w", err) + } + + return nil +} + +func (h *ColorHandler) handle(ctx context.Context, buf []byte, r slog.Record) []byte { + colorize := color.New() + switch r.Level { + case slog.LevelDebug: + colorize = colorize.Add(color.FgHiBlack) + case slog.LevelInfo: + colorize = colorize.Add(color.FgHiBlue) + case slog.LevelWarn: + colorize = colorize.Add(color.FgHiYellow) + case slog.LevelError: + colorize = colorize.Add(color.FgHiRed) + case LevelFatal: + colorize = colorize.Add(color.FgRed) + } + + // Timestamp + if !r.Time.IsZero() { + buf = append(buf, r.Time.Format(time.RFC3339)...) + buf = append(buf, '\t') + } + + // Level + buf = append(buf, colorize.Sprint(levelString(r.Level))...) + buf = append(buf, '\t') + + // Message + buf = append(buf, h.Prefix(ctx, r)+r.Message...) + if r.Level == LevelFatal { + // Show the error and return early. + format := lo.Ternary(h.opts.Level == slog.LevelDebug, "\n - %+v\n", "\t%v\n") + return fmt.Appendf(buf, format, h.Err(r)) + } + + // Attrs + var preformatted []byte + for _, a := range contextualAttrs(ctx) { + preformatted = h.appendAttr(preformatted, a, h.groups) + } + preformatted = append(preformatted, h.preformatted...) + + if len(preformatted) > 0 || r.NumAttrs() > 0 { + buf = append(buf, '\t') + } + + if len(preformatted) > 0 { + buf = append(buf, preformatted...) + } + r.Attrs(func(a slog.Attr) bool { + buf = h.appendAttr(buf, a, h.groups) + return true + }) + + // Trim the trailing space. + buf = bytes.TrimRight(buf, " ") + buf = append(buf, '\n') + + return buf +} + +// Err returns the error from the attrs, if any. +func (h *ColorHandler) Err(r slog.Record) error { + return findKey[error](errKey, r) +} + +// Prefix returns the prefix from the attrs, if any. +func (h *ColorHandler) Prefix(ctx context.Context, r slog.Record) string { + if attrPrefix := string(findKey[logPrefix](prefixKey, r)); attrPrefix != "" { + return attrPrefix + } + if ctxPrefix := contextualPrefix(ctx); ctxPrefix != "" { + return ctxPrefix + } + return h.prefix +} + +func findKey[T any](key string, r slog.Record) T { + var v T + r.Attrs(func(a slog.Attr) bool { + if a.Key != key { + return true + } + + var ok bool + if v, ok = a.Value.Any().(T); !ok { + return true + } + return false + }) + return v +} + +var ( + String = slog.String + Int64 = slog.Int64 + Int = slog.Int + Bool = slog.Bool + Time = slog.Time + Duration = slog.Duration + Group = slog.Group + Any = slog.Any + Nil = slog.Attr{Key: "", Value: slog.AnyValue(nil)} // Nil will not be logged +) + +// Err returns an Attr that represents an error. +func Err(err error) slog.Attr { + return slog.Any(errKey, err) +} + +type logPrefix string + +// Prefix returns an Attr that represents a prefix. +func Prefix(prefix string) slog.Attr { + return slog.Any(prefixKey, logPrefix("["+prefix+"] ")) +} + +// FilePath returns an Attr that represents a filePath. +func FilePath(filePath string) slog.Attr { + return String("file_path", filePath) +} + +func isLogPrefix(a slog.Attr) bool { + _, ok := a.Value.Any().(logPrefix) + return ok +} + +func levelString(level slog.Level) string { + if level == LevelFatal { + return "FATAL" + } + return level.String() +} + +var bufPool = sync.Pool{ + New: func() any { + b := make([]byte, 0, 1024) + return &b + }, +} + +func allocBuf() *[]byte { + return bufPool.Get().(*[]byte) +} + +func freeBuf(b *[]byte) { + // To reduce peak allocation, return only smaller buffers to the pool. + const maxBufferSize = 16 << 10 + if cap(*b) <= maxBufferSize { + *b = (*b)[:0] + bufPool.Put(b) + } +} diff --git a/pkg/log/handler_test.go b/pkg/log/handler_test.go new file mode 100644 index 000000000000..40ac1df7a426 --- /dev/null +++ b/pkg/log/handler_test.go @@ -0,0 +1,245 @@ +package log_test + +import ( + "bytes" + "errors" + "fmt" + "log/slog" + "strings" + "testing" + "testing/slogtest" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/log" +) + +func TestColorHandler(t *testing.T) { + var buf bytes.Buffer + logger := slog.New(log.NewHandler(&buf, &log.Options{Level: slog.LevelDebug})) + + // Test logging with different levels, attributes, and groups. + logger.Debug("debug message", "key1", "value1", "key2", 2) + logger.Info("info message", "key3", true) + logger.Warn("warn message", slog.Group("group1", slog.Int("key4", 42))) + logger.Error("error message", slog.Group("group2", slog.String("key5", "value5"))) + + got := buf.String() + + wantLines := []string{ + `DEBUG debug message key1="value1" key2=2`, + `INFO info message key3=true`, + `WARN warn message group1.key4=42`, + `ERROR error message group2.key5="value5"`, + } + compareLines(t, got, wantLines) +} + +func TestWithAttrsAndWithGroup(t *testing.T) { + t.Run("single group", func(t *testing.T) { + var buf bytes.Buffer + baseLogger := log.New(log.NewHandler(&buf, &log.Options{Level: slog.LevelWarn})) + + // Test logging with WithContextAttrs and WithGroup. + logger := baseLogger. + With("key1", "value1"). + WithGroup("group1"). + With("key2", "value2") + + logger.Debug("debug message") + logger.Info("info message", "key3", true) + logger.Warn("warn message", log.Err(errors.New("error"))) + logger.Error("error message", slog.Group("group2", slog.Int("key4", 4))) + + got := buf.String() + wantLines := []string{ + `WARN warn message key1="value1" group1.key2="value2" group1.err="error"`, + `ERROR error message key1="value1" group1.key2="value2" group1.group2.key4=4`, + } + compareLines(t, got, wantLines) + }) + + t.Run("multiple groups", func(t *testing.T) { + var buf bytes.Buffer + baseLogger := log.New(log.NewHandler(&buf, &log.Options{Level: slog.LevelWarn})) + + // Test logging with WithContextAttrs and WithGroup. + logger := baseLogger. + WithGroup("group1"). + With("key1", "value1"). + WithGroup("group2") + + logger.Error("error message", slog.Group("group3", slog.Int("key2", 2))) + + got := buf.String() + wantLines := []string{ + `ERROR error message group1.key1="value1" group1.group2.group3.key2=2`, + } + compareLines(t, got, wantLines) + }) + + t.Run("prefix", func(t *testing.T) { + var buf bytes.Buffer + logger := log.New(log.NewHandler(&buf, &log.Options{Level: slog.LevelWarn})) + logger.Error("error message", log.Prefix("prefix1"), log.String("key1", "value1")) + + wantLines := []string{ + `ERROR [prefix1] error message key1="value1"`, + } + compareLines(t, buf.String(), wantLines) + + buf.Reset() + log.SetDefault(logger) + log.WithPrefix("prefix2").Error("error message", log.String("key1", "value1")) + + wantLines = []string{ + `ERROR [prefix2] error message key1="value1"`, + } + compareLines(t, buf.String(), wantLines) + }) +} + +func TestContext(t *testing.T) { + t.Run("with context prefix", func(t *testing.T) { + var buf bytes.Buffer + baseLogger := log.New(log.NewHandler(&buf, &log.Options{Level: slog.LevelInfo})) + + // Test logging with WithContextPrefix + ctx := t.Context() + ctx = log.WithContextPrefix(ctx, "prefix1") + + logger := baseLogger.With("key1", "value1").WithGroup("group1") + logger.InfoContext(ctx, "info message", "key2", true) + + got := buf.String() + wantLines := []string{ + `INFO [prefix1] info message key1="value1" group1.key2=true`, + } + compareLines(t, got, wantLines) + }) + + t.Run("with context attrs", func(t *testing.T) { + var buf bytes.Buffer + baseLogger := log.New(log.NewHandler(&buf, &log.Options{Level: slog.LevelInfo})) + + // Test logging with WithContextAttrs + ctx := t.Context() + ctx = log.WithContextAttrs(ctx, log.String("key1", "value1")) + + logger := baseLogger.WithGroup("group1") + logger.InfoContext(ctx, "info message", "key2", true) + + got := buf.String() + wantLines := []string{ + `INFO info message group1.key1="value1" group1.key2=true`, + } + compareLines(t, got, wantLines) + }) +} + +func compareLines(t *testing.T, got string, wantLines []string) { + // Strip color codes from the output. + got = stripColorCodes(got) + + // Split the output into lines. + gotLines := strings.Split(got, "\n") + + assert.Len(t, gotLines, len(wantLines)+1) // Expecting log lines and an empty line. + + for i, wantLine := range wantLines { + if i >= len(gotLines) { + break + } + + ss := strings.Split(gotLines[i], "\t") + gotLevel, gotMessage, gotAttrs := ss[1], ss[2], ss[3] + + ss = strings.Split(wantLine, "\t") + wantLevel, wantMessage, wantAttrs := ss[0], ss[1], ss[2] + + assert.Equal(t, wantLevel, gotLevel) + assert.Equal(t, wantMessage, gotMessage) + assert.Equal(t, wantAttrs, gotAttrs) + } + assert.Empty(t, strings.TrimSpace(gotLines[len(gotLines)-1])) // Last line should be empty. +} + +func stripColorCodes(s string) string { + // This is a simplified version that only handles the color codes used in ColorHandler. + s = strings.ReplaceAll(s, "\x1b[90m", "") // FgHiBlack + s = strings.ReplaceAll(s, "\x1b[94m", "") // FgHiBlue + s = strings.ReplaceAll(s, "\x1b[93m", "") // FgHiYellow + s = strings.ReplaceAll(s, "\x1b[91m", "") // FgHiRed + s = strings.ReplaceAll(s, "\x1b[96m", "") // FgHiCyan + s = strings.ReplaceAll(s, "\x1b[95m", "") // FgHiMagenta + s = strings.ReplaceAll(s, "\x1b[97m", "") // FgWhite + s = strings.ReplaceAll(s, "\x1b[0m", "") // Reset + return s +} + +// TODO: slogtest.Run was added in Go 1.22. Waiting for https://github.com/aquasecurity/trivy/pull/6075. +func TestSlogtest(t *testing.T) { + var buf bytes.Buffer + newHandler := func(*testing.T) slog.Handler { + buf.Reset() + return log.NewHandler(&buf, &log.Options{Level: slog.LevelDebug}) + } + + results := func(*testing.T) map[string]any { + for line := range strings.SplitSeq(buf.String(), "\n") { + if line == "" { + continue + } + m, err := parseLogLine(line) + if err != nil { + t.Fatalf("Failed to parse log line: %v", err) + } + return m + } + return nil + } + + slogtest.Run(t, newHandler, results) +} + +func parseLogLine(line string) (map[string]any, error) { + parts := strings.SplitN(line, "\t", 4) + if len(parts) < 2 { + return nil, fmt.Errorf("invalid log line format: %s", line) + } + + m := make(map[string]any) + if t, err := time.Parse(time.RFC3339, parts[0]); err == nil { + m["time"] = t + parts = parts[1:] + } + m["level"] = parts[0] + m["msg"] = parts[1] + + if len(parts) == 3 { + for attr := range strings.SplitSeq(parts[2], " ") { + kv := strings.SplitN(attr, "=", 2) + if len(kv) == 2 { + parseAttr(m, kv[0], kv[1]) + } + } + } + + return m, nil +} + +func parseAttr(attrs map[string]any, key, value string) { + parts := strings.Split(key, ".") + currentMap := attrs + for i, part := range parts { + if i == len(parts)-1 { + currentMap[part] = strings.Trim(value, `"`) + } else { + if _, ok := currentMap[part]; !ok { + currentMap[part] = make(map[string]any) + } + currentMap = currentMap[part].(map[string]any) + } + } +} diff --git a/pkg/log/logger.go b/pkg/log/logger.go index 7e305056ce79..a6165545363b 100644 --- a/pkg/log/logger.go +++ b/pkg/log/logger.go @@ -1,55 +1,95 @@ package log import ( - "go.uber.org/zap" - "go.uber.org/zap/zapcore" - "golang.org/x/xerrors" + "context" + "fmt" + "io" + "log/slog" + "os" + + "github.com/samber/lo" ) -var Logger *zap.SugaredLogger +const ( + LevelDebug = slog.LevelDebug + LevelInfo = slog.LevelInfo + LevelWarn = slog.LevelWarn + LevelError = slog.LevelError + LevelFatal = slog.Level(12) -func InitLogger(debug bool) (err error) { - Logger, err = newLogger(debug) - if err != nil { - return xerrors.Errorf("error in new logger: %w", err) - } - return nil + PrefixContainerImage = "image" + PrefixPackage = "pkg" + PrefixVulnerability = "vuln" + PrefixMisconfiguration = "misconfig" + PrefixSecret = "secret" + PrefixLicense = "license" + PrefixVulnerabilityDB = "vulndb" + PrefixJavaDB = "javadb" + PrefixSPDX = "spdx" + PrefixCycloneDX = "cyclonedx" +) +// Logger is an alias of slog.Logger +type Logger = slog.Logger + +// New creates a new Logger with the given non-nil Handler. +func New(h slog.Handler) *Logger { + return slog.New(h) } -func newLogger(debug bool) (*zap.SugaredLogger, error) { - level := zap.NewAtomicLevel() - if debug { - level.SetLevel(zapcore.DebugLevel) - } else { - level.SetLevel(zapcore.InfoLevel) - } +// InitLogger initializes the logger variable and flushes the buffered logs if needed. +func InitLogger(debug, disable bool) { + level := lo.Ternary(debug, slog.LevelDebug, slog.LevelInfo) + out := lo.Ternary(disable, io.Discard, io.Writer(os.Stderr)) + h := NewHandler(out, &Options{Level: level}) - myConfig := zap.Config{ - Level: level, - Encoding: "console", - Development: debug, - DisableStacktrace: !debug, - DisableCaller: !debug, - EncoderConfig: zapcore.EncoderConfig{ - TimeKey: "Time", - LevelKey: "Level", - NameKey: "Name", - CallerKey: "Caller", - MessageKey: "Msg", - StacktraceKey: "St", - EncodeLevel: zapcore.CapitalColorLevelEncoder, - EncodeTime: zapcore.ISO8601TimeEncoder, - EncodeDuration: zapcore.StringDurationEncoder, - EncodeCaller: zapcore.ShortCallerEncoder, - }, - OutputPaths: []string{"stdout"}, - ErrorOutputPaths: []string{"stderr"}, - } - logger, err := myConfig.Build() - if err != nil { - return nil, xerrors.Errorf("failed to build zap config: %w", err) + // Flush the buffered logs if needed. + if d, ok := slog.Default().Handler().(*DeferredHandler); ok { + d.Flush(h) } - return logger.Sugar(), nil + slog.SetDefault(New(h)) +} + +var ( + // With calls [Logger.With] on the default logger. + With = slog.With + + SetDefault = slog.SetDefault + + Debug = slog.Debug + DebugContext = slog.DebugContext + Info = slog.Info + InfoContext = slog.InfoContext + Warn = slog.Warn + WarnContext = slog.WarnContext + Error = slog.Error + ErrorContext = slog.ErrorContext +) + +// WithPrefix calls [Logger.With] with the prefix on the default logger. +// +// Note: If WithPrefix is called within init() or during global variable +// initialization, it will use the default logger of log/slog package +// before Trivy's logger is set up. In such cases, it's recommended to pass the prefix +// via WithContextPrefix to ensure the correct logger is used. +func WithPrefix(prefix string) *Logger { + return slog.Default().With(Prefix(prefix)) +} + +func Debugf(format string, args ...any) { slog.Default().Debug(fmt.Sprintf(format, args...)) } +func Infof(format string, args ...any) { slog.Default().Info(fmt.Sprintf(format, args...)) } +func Warnf(format string, args ...any) { slog.Default().Warn(fmt.Sprintf(format, args...)) } +func Errorf(format string, args ...any) { slog.Default().Error(fmt.Sprintf(format, args...)) } + +// Fatal for logging fatal errors +func Fatal(msg string, args ...any) { + // Fatal errors should be logged to stderr even if the logger is disabled. + if h, ok := slog.Default().Handler().(*ColorHandler); ok { + h.out = os.Stderr + } else { + slog.SetDefault(New(NewHandler(os.Stderr, &Options{}))) + } + slog.Default().Log(context.Background(), LevelFatal, msg, args...) + os.Exit(1) } diff --git a/pkg/mapfs/file.go b/pkg/mapfs/file.go new file mode 100644 index 000000000000..0e0b2593b38a --- /dev/null +++ b/pkg/mapfs/file.go @@ -0,0 +1,380 @@ +package mapfs + +import ( + "io" + "io/fs" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "golang.org/x/xerrors" + + xsync "github.com/aquasecurity/trivy/pkg/x/sync" +) + +var separator = "/" + +// file represents one of them: +// - an actual file +// - a virtual file +// - a virtual dir +type file struct { + underlyingPath string // underlying file path + data []byte // virtual file, only either of 'path' or 'data' has a value. + stat fileStat + files xsync.Map[string, *file] +} + +func (f *file) isVirtual() bool { + return f.underlyingPath == "" +} + +func (f *file) Open(name string) (fs.File, error) { + if name == "" || name == "." { + return f.open() + } + + if sub, err := f.getFile(name); err == nil { + return sub.open() + } + + return nil, &fs.PathError{ + Op: "open", + Path: name, + Err: fs.ErrNotExist, + } +} + +func (f *file) open() (fs.File, error) { + switch { + case f.stat.IsDir(): // Directory + entries, err := f.ReadDir(".") + if err != nil { + return nil, xerrors.Errorf("read dir error: %w", err) + } + return &mapDir{ + path: f.underlyingPath, + fileStat: f.stat, + entry: entries, + }, nil + case f.isVirtual(): // Virtual file + return &openMapFile{ + path: f.stat.name, + file: f, + offset: 0, + }, nil + default: // Real file + return os.Open(f.underlyingPath) + } +} + +func (f *file) Remove(name string) error { + if name == "" || name == "." { + return nil + } + + return f.removePath(name, false) +} + +func (f *file) RemoveAll(name string) error { + if name == "" || name == "." { + return nil + } + + return f.removePath(name, true) +} + +func (f *file) removePath(name string, recursive bool) error { + parts := strings.Split(name, separator) + if len(parts) == 1 { + sub, ok := f.files.Load(name) + if !ok { + return fs.ErrNotExist + } + if sub.files.Len() != 0 && !recursive { + return fs.ErrInvalid + } + f.files.Delete(name) + return nil + } + + sub, err := f.getFile(parts[0]) + if err != nil { + return err + } else if !sub.stat.IsDir() { + return fs.ErrNotExist + } + + return sub.removePath(strings.Join(parts[1:], separator), recursive) +} + +func (f *file) getFile(name string) (*file, error) { + if name == "" || name == "." { + return f, nil + } + parts := strings.Split(name, separator) + if len(parts) == 1 { + f, ok := f.files.Load(name) + if ok { + return f, nil + } + return nil, fs.ErrNotExist + } + + sub, ok := f.files.Load(parts[0]) + if !ok || !sub.stat.IsDir() { + return nil, fs.ErrNotExist + } + + return sub.getFile(strings.Join(parts[1:], separator)) +} + +func (f *file) ReadDir(name string) ([]fs.DirEntry, error) { + if name == "" || name == "." { + var entries []fs.DirEntry + var err error + f.files.Range(func(name string, value *file) bool { + if value.isVirtual() { + entries = append(entries, &value.stat) + } else { + var fi os.FileInfo + fi, err = os.Stat(value.underlyingPath) + if err != nil { + return false + } + entries = append(entries, &fileStat{ + name: name, + size: fi.Size(), + mode: fi.Mode(), + modTime: fi.ModTime(), + sys: fi.Sys(), + }) + } + return true + }) + if err != nil { + return nil, xerrors.Errorf("range error: %w", err) + } + sort.Slice(entries, func(i, j int) bool { return entries[i].Name() < entries[j].Name() }) + return entries, nil + } + + parts := strings.Split(name, separator) + dir, ok := f.files.Load(parts[0]) + if !ok || !dir.stat.IsDir() { + return nil, fs.ErrNotExist + } + return dir.ReadDir(strings.Join(parts[1:], separator)) +} + +func (f *file) MkdirAll(path string, perm fs.FileMode) error { + parts := strings.Split(path, separator) + + if path == "" || path == "." { + return nil + } + + if perm&fs.ModeDir == 0 { + perm |= fs.ModeDir + } + + sub := &file{ + stat: fileStat{ + name: parts[0], + size: 0x100, + modTime: time.Now(), + mode: perm, + }, + files: xsync.Map[string, *file]{}, + } + + // Create the directory when the key is not present + sub, loaded := f.files.LoadOrStore(parts[0], sub) + if loaded && !sub.stat.IsDir() { + return fs.ErrExist + } + + if len(parts) == 1 { + return nil + } + + return sub.MkdirAll(strings.Join(parts[1:], separator), perm) +} + +func (f *file) WriteFile(path, underlyingPath string) error { + parts := strings.Split(path, separator) + + if len(parts) == 1 { + f.files.Store(parts[0], &file{ + underlyingPath: underlyingPath, + }) + return nil + } + + dir, ok := f.files.Load(parts[0]) + if !ok || !dir.stat.IsDir() { + return fs.ErrNotExist + } + + return dir.WriteFile(strings.Join(parts[1:], separator), underlyingPath) +} + +func (f *file) WriteVirtualFile(path string, data []byte, mode fs.FileMode) error { + if mode&fs.ModeDir != 0 { + return xerrors.Errorf("invalid perm: %v", mode) + } + parts := strings.Split(path, separator) + + if len(parts) == 1 { + f.files.Store(parts[0], &file{ + data: data, + stat: fileStat{ + name: parts[0], + size: int64(len(data)), + mode: mode, + modTime: time.Now(), + }, + }) + return nil + } + + dir, ok := f.files.Load(parts[0]) + if !ok || !dir.stat.IsDir() { + return fs.ErrNotExist + } + + return dir.WriteVirtualFile(strings.Join(parts[1:], separator), data, mode) +} + +func (f *file) glob(pattern string) ([]string, error) { + var entries []string + parts := strings.Split(pattern, separator) + + var err error + f.files.Range(func(name string, sub *file) bool { + var ok bool + ok, err = filepath.Match(parts[0], name) + if err != nil { + return false + } else if ok { + if len(parts) == 1 { + entries = append(entries, name) + } else { + var subEntries []string + subEntries, err = sub.glob(strings.Join(parts[1:], separator)) + if err != nil { + return false + } + for _, sub := range subEntries { + entries = append(entries, name+separator+sub) + } + } + } + return true + }) + if err != nil { + return nil, xerrors.Errorf("range error: %w", err) + } + + sort.Strings(entries) + return entries, nil +} + +// An openMapFile is a regular (non-directory) fs.File open for reading. +// ported from https://github.com/golang/go/blob/99bc53f5e819c2d2d49f2a56c488898085be3982/src/testing/fstest/mapfs.go +type openMapFile struct { + path string + *file + offset int64 +} + +func (f *openMapFile) Stat() (fs.FileInfo, error) { return &f.file.stat, nil } + +func (f *openMapFile) Close() error { return nil } + +func (f *openMapFile) Read(b []byte) (int, error) { + if f.offset >= int64(len(f.file.data)) { + return 0, io.EOF + } + if f.offset < 0 { + return 0, &fs.PathError{ + Op: "read", + Path: f.path, + Err: fs.ErrInvalid, + } + } + n := copy(b, f.file.data[f.offset:]) + f.offset += int64(n) + return n, nil +} + +func (f *openMapFile) Seek(offset int64, whence int) (int64, error) { + switch whence { + case 0: + // offset += 0 + case 1: + offset += f.offset + case 2: + offset += int64(len(f.file.data)) + } + if offset < 0 || offset > int64(len(f.file.data)) { + return 0, &fs.PathError{ + Op: "seek", + Path: f.path, + Err: fs.ErrInvalid, + } + } + f.offset = offset + return offset, nil +} + +func (f *openMapFile) ReadAt(b []byte, offset int64) (int, error) { + if offset < 0 || offset > int64(len(f.file.data)) { + return 0, &fs.PathError{ + Op: "read", + Path: f.path, + Err: fs.ErrInvalid, + } + } + n := copy(b, f.file.data[offset:]) + if n < len(b) { + return n, io.EOF + } + return n, nil +} + +// A mapDir is a directory fs.File (so also fs.ReadDirFile) open for reading. +type mapDir struct { + path string + fileStat + entry []fs.DirEntry + offset int +} + +func (d *mapDir) Stat() (fs.FileInfo, error) { return &d.fileStat, nil } +func (d *mapDir) Close() error { return nil } +func (d *mapDir) Read(_ []byte) (int, error) { + return 0, &fs.PathError{ + Op: "read", + Path: d.path, + Err: fs.ErrInvalid, + } +} + +func (d *mapDir) ReadDir(count int) ([]fs.DirEntry, error) { + n := len(d.entry) - d.offset + if n == 0 && count > 0 { + return nil, io.EOF + } + if count > 0 && n > count { + n = count + } + list := make([]fs.DirEntry, n) + for i := range list { + list[i] = d.entry[d.offset+i] + } + d.offset += n + return list, nil +} diff --git a/pkg/mapfs/fs.go b/pkg/mapfs/fs.go new file mode 100644 index 000000000000..839053b8c2b2 --- /dev/null +++ b/pkg/mapfs/fs.go @@ -0,0 +1,291 @@ +package mapfs + +import ( + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "slices" + "strings" + "time" + + "golang.org/x/xerrors" + + xsync "github.com/aquasecurity/trivy/pkg/x/sync" +) + +type allFS interface { + fs.ReadFileFS + fs.ReadDirFS + fs.StatFS + fs.GlobFS + fs.SubFS +} + +// Make sure FS implements all the interfaces +var _ allFS = &FS{} + +// FS is an in-memory filesystem +type FS struct { + root *file + + // When the underlyingRoot has a value, it allows access to the local filesystem outside of this in-memory filesystem. + // The set path is used as the starting point when accessing the local filesystem. + // In other words, although mapfs.Open("../foo") would normally result in an error, if this option is enabled, + // it will be executed as os.Open(filepath.Join(underlyingRoot, "../foo")). + underlyingRoot string +} + +type Option func(*FS) + +// WithUnderlyingRoot returns an option to set the underlying root path for the in-memory filesystem. +func WithUnderlyingRoot(root string) Option { + return func(fsys *FS) { + fsys.underlyingRoot = root + } +} + +// New creates a new filesystem +func New(opts ...Option) *FS { + fsys := &FS{ + root: &file{ + stat: fileStat{ + name: ".", + size: 0x100, + modTime: time.Now(), + mode: 0o0700 | fs.ModeDir, + }, + files: xsync.Map[string, *file]{}, + }, + } + for _, opt := range opts { + opt(fsys) + } + return fsys +} + +// Filter removes the specified skippedFiles and returns a new FS +func (m *FS) Filter(skippedFiles []string) (*FS, error) { + if len(skippedFiles) == 0 { + return m, nil + } + filter := func(path string, _ fs.DirEntry) (bool, error) { + return slices.Contains(skippedFiles, path), nil + } + return m.FilterFunc(filter) +} + +func (m *FS) FilterFunc(fn func(path string, d fs.DirEntry) (bool, error)) (*FS, error) { + newFS := New(WithUnderlyingRoot(m.underlyingRoot)) + err := fs.WalkDir(m, ".", func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if d.IsDir() { + return newFS.MkdirAll(path, d.Type().Perm()) + } + + if filtered, err := fn(path, d); err != nil { + return err + } else if filtered { + return nil + } + + f, err := m.root.getFile(path) + if err != nil { + return xerrors.Errorf("unable to get %s: %w", path, err) + } + // Virtual file + if f.isVirtual() { + return newFS.WriteVirtualFile(path, f.data, f.stat.mode) + } + return newFS.WriteFile(path, f.underlyingPath) + }) + if err != nil { + return nil, xerrors.Errorf("walk error %w", err) + } + + return newFS, nil +} + +// CopyDir copies a directory from the local filesystem into the in-memory filesystem. +// This function works similarly to the Unix command "cp -r src dst", recursively copying +// the entire directory structure from the source to the destination. +// +// The function: +// 1. Walks through all files and subdirectories in the source directory +// 2. Recreates the directory structure in the in-memory filesystem +// 3. Copies all files while preserving their relative paths +// +// Parameters: +// - src: The source directory path in the local filesystem +// - dst: The destination directory path in the in-memory filesystem +// +// For example, if src is "/tmp/data" and dst is "app/", then: +// - A file at "/tmp/data/settings.json" will be copied to "app/data/settings.json" +// - A file at "/tmp/data/subdir/config.yaml" will be copied to "app/data/subdir/config.yaml" +func (m *FS) CopyDir(src, dst string) error { + base := filepath.Base(src) + return filepath.WalkDir(src, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + rel, err := filepath.Rel(src, path) + if err != nil { + return err + } + dstPath := filepath.Join(dst, base, rel) + if d.IsDir() { + return m.MkdirAll(dstPath, d.Type()) + } + return m.WriteFile(dstPath, path) + }) +} + +// TODO(knqyf263): Remove this method and replace with CopyDir +func (m *FS) CopyFilesUnder(dir string) error { + return filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } else if d.IsDir() { + return m.MkdirAll(path, d.Type()) + } + return m.WriteFile(path, path) + }) +} + +// Stat returns a FileInfo describing the file. +func (m *FS) Stat(name string) (fs.FileInfo, error) { + if m.isPathAboveRoot(name) { + return os.Stat(filepath.Join(m.underlyingRoot, name)) + } + + name = cleanPath(name) + f, err := m.root.getFile(name) + if err != nil { + return nil, &fs.PathError{ + Op: "stat", + Path: name, + Err: err, + } + } + if f.isVirtual() { + return &f.stat, nil + } + return os.Stat(f.underlyingPath) +} + +// ReadDir reads the named directory +// and returns a list of directory entries sorted by filename. +func (m *FS) ReadDir(name string) ([]fs.DirEntry, error) { + if m.isPathAboveRoot(name) { + return os.ReadDir(filepath.Join(m.underlyingRoot, name)) + } + return m.root.ReadDir(cleanPath(name)) +} + +// Open opens the named file for reading. +func (m *FS) Open(name string) (fs.File, error) { + if m.isPathAboveRoot(name) { + return os.Open(filepath.Join(m.underlyingRoot, name)) + } + return m.root.Open(cleanPath(name)) +} + +// WriteFile creates a mapping between path and underlyingPath. +func (m *FS) WriteFile(path, underlyingPath string) error { + if underlyingPath == "" { + return errors.New("underlying path must not be empty") + } + return m.root.WriteFile(cleanPath(path), underlyingPath) +} + +// WriteVirtualFile writes the specified bytes to the named file. If the file exists, it will be overwritten. +func (m *FS) WriteVirtualFile(path string, data []byte, mode fs.FileMode) error { + return m.root.WriteVirtualFile(cleanPath(path), data, mode) +} + +// MkdirAll creates a directory named path, +// along with any necessary parents, and returns nil, +// or else returns an error. +// The permission bits perm (before umask) are used for all +// directories that MkdirAll creates. +// If path is already a directory, MkdirAll does nothing +// and returns nil. +func (m *FS) MkdirAll(path string, perm fs.FileMode) error { + return m.root.MkdirAll(cleanPath(path), perm) +} + +// ReadFile reads the named file and returns its contents. +// A successful call returns a nil error, not io.EOF. +// (Because ReadFile reads the whole file, the expected EOF +// from the final Read is not treated as an error to be reported.) +// +// The caller is permitted to modify the returned byte slice. +// This method should return a copy of the underlying data. +func (m *FS) ReadFile(name string) ([]byte, error) { + if m.isPathAboveRoot(name) { + return os.ReadFile(filepath.Join(m.underlyingRoot, name)) + } + + f, err := m.root.Open(cleanPath(name)) + if err != nil { + return nil, err + } + defer func() { _ = f.Close() }() + return io.ReadAll(f) +} + +// Sub returns an FS corresponding to the subtree rooted at dir. +func (m *FS) Sub(dir string) (fs.FS, error) { + d, err := m.root.getFile(cleanPath(dir)) + if err != nil { + return nil, err + } + return &FS{ + root: d, + }, nil +} + +// Glob returns the names of all files matching pattern or nil +// if there is no matching file. The syntax of patterns is the same +// as in Match. The pattern may describe hierarchical names such as +// /usr/*/bin/ed (assuming the Separator is '/'). +// +// Glob ignores file system errors such as I/O errors reading directories. +// The only possible returned error is ErrBadPattern, when pattern +// is malformed. +func (m *FS) Glob(pattern string) ([]string, error) { + return m.root.glob(pattern) +} + +// Remove deletes a file or directory from the filesystem +func (m *FS) Remove(path string) error { + return m.root.Remove(cleanPath(path)) +} + +// RemoveAll deletes a file or directory and any children if present from the filesystem +func (m *FS) RemoveAll(path string) error { + return m.root.RemoveAll(cleanPath(path)) +} + +func cleanPath(path string) string { + // Convert the volume name like 'C:' into dir like 'C\' + if vol := filepath.VolumeName(path); vol != "" { + newVol := strings.TrimSuffix(vol, ":") + newVol = fmt.Sprintf("%s%c", newVol, filepath.Separator) + path = strings.Replace(path, vol, newVol, 1) + } + path = filepath.Clean(path) + path = filepath.ToSlash(path) + path = strings.TrimLeft(path, "/") // Remove the leading slash + return path +} + +func (m *FS) isPathAboveRoot(name string) bool { + return (name == ".." || strings.HasPrefix(name, "../")) && m.underlyingRoot != "" +} diff --git a/pkg/mapfs/fs_test.go b/pkg/mapfs/fs_test.go new file mode 100644 index 000000000000..4d190d7bb367 --- /dev/null +++ b/pkg/mapfs/fs_test.go @@ -0,0 +1,655 @@ +package mapfs_test + +import ( + "io" + "io/fs" + "runtime" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/mapfs" +) + +type fileInfo struct { + name string + fileMode fs.FileMode + isDir bool + size int64 +} + +var ( + filePerm = lo.Ternary(runtime.GOOS == "windows", fs.FileMode(0o666), fs.FileMode(0o644)) + helloFileInfo = fileInfo{ + name: "hello.txt", + fileMode: filePerm, + isDir: false, + size: 11, + } + btxtFileInfo = fileInfo{ + name: "b.txt", + fileMode: filePerm, + isDir: false, + size: 3, + } + virtualFileInfo = fileInfo{ + name: "virtual.txt", + fileMode: 0o600, + isDir: false, + size: 7, + } + emptyVirtualFileInfo = fileInfo{ + name: "empty-virtual.txt", + fileMode: 0o600, + isDir: false, + size: 0, + } + cdirFileInfo = fileInfo{ + name: "c", + fileMode: fs.FileMode(0o700) | fs.ModeDir, + isDir: true, + size: 256, + } +) + +func initFS(t *testing.T) *mapfs.FS { + fsys := mapfs.New() + require.NoError(t, fsys.MkdirAll("a/b/c", 0o700)) + require.NoError(t, fsys.MkdirAll("a/b/empty", 0o700)) + require.NoError(t, fsys.WriteFile("hello.txt", "testdata/hello.txt")) + require.NoError(t, fsys.WriteFile("a/b/b.txt", "testdata/b.txt")) + require.NoError(t, fsys.WriteFile("a/b/c/c.txt", "testdata/c.txt")) + require.NoError(t, fsys.WriteFile("a/b/c/.dotfile", "testdata/dotfile")) + require.NoError(t, fsys.WriteVirtualFile("a/b/c/virtual.txt", []byte("virtual"), 0o600)) + require.NoError(t, fsys.WriteVirtualFile("a/b/c/empty-virtual.txt", []byte{}, 0o600)) + return fsys +} + +func assertFileInfo(t *testing.T, want fileInfo, got fs.FileInfo) { + if got == nil { + return + } + assert.Equal(t, want.name, got.Name()) + assert.Equal(t, want.fileMode, got.Mode()) + assert.Equal(t, want.isDir, got.Mode().IsDir()) + assert.Equal(t, want.isDir, got.IsDir()) + assert.Equal(t, want.size, got.Size()) +} + +func TestFS_Filter(t *testing.T) { + fsys := initFS(t) + t.Run("empty files", func(t *testing.T) { + newFS, err := fsys.Filter(nil) + require.NoError(t, err) + assert.Equal(t, fsys, newFS) + }) + t.Run("happy", func(t *testing.T) { + newFS, err := fsys.Filter([]string{ + "hello.txt", + "a/b/c/.dotfile", + }) + require.NoError(t, err) + _, err = newFS.Stat("hello.txt") + require.ErrorIs(t, err, fs.ErrNotExist) + _, err = newFS.Stat("a/b/c/.dotfile") + require.ErrorIs(t, err, fs.ErrNotExist) + fi, err := newFS.Stat("a/b/c/c.txt") + require.NoError(t, err) + assert.Equal(t, "c.txt", fi.Name()) + }) +} + +func TestFS_Stat(t *testing.T) { + tests := []struct { + name string + filePath string + want fileInfo + wantErr assert.ErrorAssertionFunc + }{ + { + name: "ordinary file", + filePath: "hello.txt", + want: helloFileInfo, + wantErr: assert.NoError, + }, + { + name: "nested file", + filePath: "a/b/b.txt", + want: btxtFileInfo, + wantErr: assert.NoError, + }, + { + name: "virtual file", + filePath: "a/b/c/virtual.txt", + want: virtualFileInfo, + wantErr: assert.NoError, + }, + { + name: "empty virtual file", + filePath: "a/b/c/empty-virtual.txt", + want: emptyVirtualFileInfo, + wantErr: assert.NoError, + }, + { + name: "dir", + filePath: "a/b/c", + want: cdirFileInfo, + wantErr: assert.NoError, + }, + { + name: "no such file", + filePath: "nosuch.txt", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + fsys := initFS(t) + t.Run(tt.name, func(t *testing.T) { + got, err := fsys.Stat(tt.filePath) + tt.wantErr(t, err) + assertFileInfo(t, tt.want, got) + }) + } +} + +func TestFS_WriteFile(t *testing.T) { + tests := []struct { + name string + dstPath string + underlyingPath string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "write file", + dstPath: "hello.txt", + underlyingPath: "testdata/hello.txt", + wantErr: assert.NoError, + }, + { + name: "write file to non-existent dir", + dstPath: "test/hello.txt", + underlyingPath: "testdata/hello.txt", + wantErr: assert.Error, + }, + { + name: "write file to existent dir", + dstPath: "subdir/hello.txt", + underlyingPath: "testdata/hello.txt", + wantErr: assert.NoError, + }, + { + name: "empty underlying path", + dstPath: "copy/empty.txt", + underlyingPath: "", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := mapfs.New() + require.NoError(t, fsys.MkdirAll("subdir", 0o700)) + + err := fsys.WriteFile(tt.dstPath, tt.underlyingPath) + tt.wantErr(t, err) + + if err == nil { + _, statErr := fsys.Stat(tt.dstPath) + assert.NoError(t, statErr) + } + }) + } +} + +func TestFS_ReadDir(t *testing.T) { + type dirEntry struct { + name string + fileMode fs.FileMode + isDir bool + size int64 + fileInfo fileInfo + } + + tests := []struct { + name string + filePath string + want []dirEntry + wantErr assert.ErrorAssertionFunc + }{ + { + name: "at root", + filePath: ".", + want: []dirEntry{ + { + name: "a", + fileMode: fs.FileMode(0o700) | fs.ModeDir, + isDir: true, + size: 0x100, + fileInfo: fileInfo{ + name: "a", + fileMode: fs.FileMode(0o700) | fs.ModeDir, + isDir: true, + size: 0x100, + }, + }, + { + name: "hello.txt", + fileMode: filePerm, + isDir: false, + size: 11, + fileInfo: helloFileInfo, + }, + }, + wantErr: assert.NoError, + }, + { + name: "multiple files", + filePath: "a/b/c", + want: []dirEntry{ + { + name: ".dotfile", + fileMode: filePerm, + isDir: false, + size: 7, + fileInfo: fileInfo{ + name: ".dotfile", + fileMode: filePerm, + isDir: false, + size: 7, + }, + }, + { + name: "c.txt", + fileMode: filePerm, + isDir: false, + size: 0, + fileInfo: fileInfo{ + name: "c.txt", + fileMode: filePerm, + isDir: false, + size: 0, + }, + }, + { + name: "empty-virtual.txt", + fileMode: 0o600, + isDir: false, + size: 0, + fileInfo: emptyVirtualFileInfo, + }, + { + name: "virtual.txt", + fileMode: 0o600, + isDir: false, + size: 0, + fileInfo: virtualFileInfo, + }, + }, + wantErr: assert.NoError, + }, + { + name: "no such dir", + filePath: "nosuch/", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + fsys := initFS(t) + t.Run(tt.name, func(t *testing.T) { + entries, err := fsys.ReadDir(tt.filePath) + tt.wantErr(t, err) + + for _, z := range lo.Zip2(entries, tt.want) { + got, want := z.A, z.B + assert.Equal(t, want.name, got.Name()) + assert.Equal(t, want.fileMode, got.Type(), want.name) + assert.Equal(t, want.isDir, got.IsDir(), want.name) + + fi, err := got.Info() + require.NoError(t, err) + assertFileInfo(t, want.fileInfo, fi) + } + }) + } +} + +func TestFS_Open(t *testing.T) { + type file struct { + fileInfo fileInfo + body string + } + + tests := []struct { + name string + filePath string + want file + wantErr assert.ErrorAssertionFunc + }{ + { + name: "ordinary file", + filePath: "hello.txt", + want: file{ + fileInfo: helloFileInfo, + body: "hello world", + }, + wantErr: assert.NoError, + }, + { + name: "virtual file", + filePath: "a/b/c/virtual.txt", + want: file{ + fileInfo: virtualFileInfo, + body: "virtual", + }, + wantErr: assert.NoError, + }, + { + name: "empty virtual file", + filePath: "a/b/c/empty-virtual.txt", + want: file{ + fileInfo: emptyVirtualFileInfo, + body: "", + }, + wantErr: assert.NoError, + }, + { + name: "dir", + filePath: "a/b/c", + want: file{ + fileInfo: cdirFileInfo, + }, + wantErr: assert.NoError, + }, + { + name: "no such file", + filePath: "nosuch.txt", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + fsys := initFS(t) + t.Run(tt.name, func(t *testing.T) { + f, err := fsys.Open(tt.filePath) + tt.wantErr(t, err) + if f == nil { + return + } + defer func() { + require.NoError(t, f.Close()) + }() + + fi, err := f.Stat() + require.NoError(t, err) + assertFileInfo(t, tt.want.fileInfo, fi) + + if tt.want.body != "" { + b, err := io.ReadAll(f) + require.NoError(t, err) + assert.Equal(t, tt.want.body, string(b)) + } + }) + } +} + +func TestFS_ReadFile(t *testing.T) { + tests := []struct { + name string + filePath string + want string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "ordinary file", + filePath: "hello.txt", + want: "hello world", + wantErr: assert.NoError, + }, + { + name: "virtual file", + filePath: "a/b/c/virtual.txt", + want: "virtual", + wantErr: assert.NoError, + }, + { + name: "empty virtual file", + filePath: "a/b/c/empty-virtual.txt", + want: "", + wantErr: assert.NoError, + }, + { + name: "no such file", + filePath: "nosuch.txt", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + fsys := initFS(t) + t.Run(tt.name, func(t *testing.T) { + b, err := fsys.ReadFile(tt.filePath) + tt.wantErr(t, err) + assert.Equal(t, tt.want, string(b)) + }) + } +} + +func TestFS_Sub(t *testing.T) { + fsys := initFS(t) + sub, err := fsys.Sub("a/b") + require.NoError(t, err) + + data, err := sub.(fs.ReadFileFS).ReadFile("c/.dotfile") + require.NoError(t, err) + assert.Equal(t, "dotfile", string(data)) +} + +func TestFS_Glob(t *testing.T) { + tests := []struct { + name string + pattern string + want []string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "root", + pattern: "*", + want: []string{ + "a", + "hello.txt", + }, + wantErr: assert.NoError, + }, + { + name: "pattern", + pattern: "*/b/c/*.txt", + want: []string{ + "a/b/c/c.txt", + "a/b/c/empty-virtual.txt", + "a/b/c/virtual.txt", + }, + wantErr: assert.NoError, + }, + { + name: "no such", + pattern: "nosuch", + wantErr: assert.NoError, + }, + { + name: "invalid pattern", + pattern: "[abc", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + fsys := initFS(t) + t.Run(tt.name, func(t *testing.T) { + results, err := fsys.Glob(tt.pattern) + tt.wantErr(t, err) + assert.Equal(t, tt.want, results) + }) + } +} + +func TestFS_Remove(t *testing.T) { + tests := []struct { + name string + path string + wantErr assert.ErrorAssertionFunc + }{ + { + name: "ordinary file", + path: "hello.txt", + wantErr: assert.NoError, + }, + { + name: "nested file", + path: "a/b/b.txt", + wantErr: assert.NoError, + }, + { + name: "virtual file", + path: "a/b/c/virtual.txt", + wantErr: assert.NoError, + }, + { + name: "empty virtual file", + path: "a/b/c/empty-virtual.txt", + wantErr: assert.NoError, + }, + { + name: "empty dir", + path: "a/b/empty", + wantErr: assert.NoError, + }, + { + name: "empty path", + path: "", + wantErr: assert.NoError, + }, + { + name: "non-empty dir", + path: "a/b/c", + wantErr: assert.Error, + }, + } + + for _, tt := range tests { + fsys := initFS(t) + t.Run(tt.name, func(t *testing.T) { + err := fsys.Remove(tt.path) + tt.wantErr(t, err) + if err != nil || tt.path == "" { + return + } + + _, err = fsys.Stat(tt.path) + require.ErrorIs(t, err, fs.ErrNotExist) + }) + } +} + +func TestFS_RemoveAll(t *testing.T) { + fsys := initFS(t) + t.Run("ordinary file", func(t *testing.T) { + err := fsys.RemoveAll("hello.txt") + require.NoError(t, err) + _, err = fsys.Stat("hello.txt") + require.ErrorIs(t, err, fs.ErrNotExist) + }) + t.Run("non-empty dir", func(t *testing.T) { + err := fsys.RemoveAll("a/b") + require.NoError(t, err) + _, err = fsys.Stat("a/b/c/c.txt") + require.ErrorIs(t, err, fs.ErrNotExist) + _, err = fsys.Stat("a/b/c/.dotfile") + require.ErrorIs(t, err, fs.ErrNotExist) + _, err = fsys.Stat("a/b/c/virtual.txt") + require.ErrorIs(t, err, fs.ErrNotExist) + _, err = fsys.Stat("a/b/c/empty-virtual.txt") + require.ErrorIs(t, err, fs.ErrNotExist) + }) +} + +func TestFS_WithUnderlyingRoot(t *testing.T) { + root := "testdata/subdir" + fsys := mapfs.New(mapfs.WithUnderlyingRoot(root)) + require.NoError(t, fsys.WriteFile("foo.txt", root+"/foo.txt")) + require.NoError(t, fsys.WriteFile("..foo.txt", root+"/..foo.txt")) + + fi, err := fsys.Stat("..") + require.NoError(t, err) + assert.True(t, fi.IsDir()) + + fi, err = fsys.Stat("../hello.txt") + require.NoError(t, err) + assert.False(t, fi.IsDir()) + + fi, err = fsys.Stat("foo.txt") + require.NoError(t, err) + assert.False(t, fi.IsDir()) + + fi, err = fsys.Stat("..foo.txt") + require.NoError(t, err) + assert.False(t, fi.IsDir()) +} + +func TestFS_CopyDir(t *testing.T) { + tmpDst := "dst" + tests := []struct { + name string + src string + dst string + wantFiles map[string]string // path in dst: want content + wantErr require.ErrorAssertionFunc + }{ + { + name: "copy testdata to dst/", + src: "testdata", + dst: tmpDst, + wantFiles: map[string]string{ + tmpDst + "/testdata/hello.txt": "hello world", + tmpDst + "/testdata/b.txt": "bbb", + tmpDst + "/testdata/c.txt": "", + tmpDst + "/testdata/dotfile": "dotfile", + tmpDst + "/testdata/subdir/foo.txt": "", + tmpDst + "/testdata/subdir/..foo.txt": "", + }, + wantErr: require.NoError, + }, + { + name: "copy subdir only", + src: "testdata/subdir", + dst: tmpDst + "/subdir2", + wantFiles: map[string]string{ + tmpDst + "/subdir2/subdir/foo.txt": "", + tmpDst + "/subdir2/subdir/..foo.txt": "", + }, + wantErr: require.NoError, + }, + { + name: "non-existent src", + src: "testdata/no_such_dir", + dst: tmpDst + "/no_such", + wantErr: require.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fsys := mapfs.New() + err := fsys.CopyDir(tt.src, tt.dst) + tt.wantErr(t, err) + for path, want := range tt.wantFiles { + got, err := fsys.ReadFile(path) + require.NoError(t, err, path) + assert.Equal(t, want, string(got), path) + } + }) + } +} diff --git a/pkg/mapfs/testdata/b.txt b/pkg/mapfs/testdata/b.txt new file mode 100644 index 000000000000..01f02e32ce8a --- /dev/null +++ b/pkg/mapfs/testdata/b.txt @@ -0,0 +1 @@ +bbb \ No newline at end of file diff --git a/pkg/mapfs/testdata/c.txt b/pkg/mapfs/testdata/c.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/mapfs/testdata/dotfile b/pkg/mapfs/testdata/dotfile new file mode 100644 index 000000000000..a21a8f598711 --- /dev/null +++ b/pkg/mapfs/testdata/dotfile @@ -0,0 +1 @@ +dotfile \ No newline at end of file diff --git a/pkg/mapfs/testdata/hello.txt b/pkg/mapfs/testdata/hello.txt new file mode 100644 index 000000000000..95d09f2b1015 --- /dev/null +++ b/pkg/mapfs/testdata/hello.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/pkg/mapfs/testdata/subdir/..foo.txt b/pkg/mapfs/testdata/subdir/..foo.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/mapfs/testdata/subdir/foo.txt b/pkg/mapfs/testdata/subdir/foo.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/mapfs/types.go b/pkg/mapfs/types.go new file mode 100644 index 000000000000..545ab822429b --- /dev/null +++ b/pkg/mapfs/types.go @@ -0,0 +1,26 @@ +package mapfs + +import ( + "io/fs" + "time" +) + +// A fileStat is the implementation of FileInfo returned by Stat and Lstat. +// Ported from https://github.com/golang/go/blob/518889b35cb07f3e71963f2ccfc0f96ee26a51ce/src/os/types_unix.go +type fileStat struct { + name string + size int64 + mode fs.FileMode + modTime time.Time + sys any +} + +func (fstat *fileStat) Name() string { return fstat.name } +func (fstat *fileStat) Size() int64 { return fstat.size } +func (fstat *fileStat) Mode() fs.FileMode { return fstat.mode } +func (fstat *fileStat) IsDir() bool { return fstat.mode.IsDir() } +func (fstat *fileStat) ModTime() time.Time { return fstat.modTime } +func (fstat *fileStat) Sys() any { return &fstat.sys } + +func (fstat *fileStat) Info() (fs.FileInfo, error) { return fstat, nil } +func (fstat *fileStat) Type() fs.FileMode { return fstat.mode } diff --git a/pkg/misconf/config_schema.go b/pkg/misconf/config_schema.go new file mode 100644 index 000000000000..a728bfa01e3b --- /dev/null +++ b/pkg/misconf/config_schema.go @@ -0,0 +1,74 @@ +package misconf + +import ( + "bytes" + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/xeipuuv/gojsonschema" + "golang.org/x/xerrors" +) + +type ConfigFileSchema struct { + path string + name string + source []byte + schema *gojsonschema.Schema +} + +func LoadConfigSchemas(paths []string) ([]*ConfigFileSchema, error) { + var configSchemas []*ConfigFileSchema + for _, path := range paths { + walkFn := func(path string, info fs.DirEntry, err error) error { + if err != nil { + return err + } + if info.IsDir() || !strings.HasSuffix(info.Name(), ".json") { + return nil + } + + schema, err := newConfigFileSchema(path) + if err != nil { + return xerrors.Errorf("load config file schema: %w", err) + } + + configSchemas = append(configSchemas, schema) + return nil + } + if err := filepath.WalkDir(path, walkFn); err != nil { + return nil, xerrors.Errorf("walk error: %w", err) + } + } + + return configSchemas, nil +} + +func newConfigFileSchema(path string) (*ConfigFileSchema, error) { + b, err := os.ReadFile(path) + if err != nil { + return nil, xerrors.Errorf("read config schema error: %w", err) + } + + // Go's regular expression engine does not support \Z + b = bytes.ReplaceAll(b, []byte(`\\Z`), []byte(`$`)) + + // Go's regular expression engine does not support negative lookahead + b = regexp.MustCompile(`\(\?\!.*\)`).ReplaceAll(b, []byte{}) + schema, err := gojsonschema.NewSchema(gojsonschema.NewBytesLoader(b)) + if err != nil { + return nil, xerrors.Errorf("compile config schema %s error: %w", path, err) + } + + fileName := filepath.Base(path) + schemaName := strings.TrimSuffix(fileName, filepath.Ext(fileName)) + + return &ConfigFileSchema{ + path: path, + name: schemaName, + schema: schema, + source: b, + }, nil +} diff --git a/pkg/misconf/config_schema_test.go b/pkg/misconf/config_schema_test.go new file mode 100644 index 000000000000..8290dbdfaf7c --- /dev/null +++ b/pkg/misconf/config_schema_test.go @@ -0,0 +1,41 @@ +package misconf_test + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/misconf" +) + +func Test_LoadConfigSchemas(t *testing.T) { + tests := []struct { + name string + paths []string + want int + }{ + { + name: "load one schema", + paths: []string{ + filepath.Join("testdata", "schemas", "schema1.json"), + }, + want: 1, + }, + { + name: "load dir with schemas", + paths: []string{ + filepath.Join("testdata", "schemas"), + }, + want: 3, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := misconf.LoadConfigSchemas(tt.paths) + require.NoError(t, err) + assert.Len(t, got, tt.want) + }) + } +} diff --git a/pkg/misconf/scanner.go b/pkg/misconf/scanner.go new file mode 100644 index 000000000000..fc9a12e16cbe --- /dev/null +++ b/pkg/misconf/scanner.go @@ -0,0 +1,596 @@ +package misconf + +import ( + "context" + "errors" + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "slices" + "sort" + "strings" + + "github.com/samber/lo" + "github.com/xeipuuv/gojsonschema" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/detection" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/iac/scan" + "github.com/aquasecurity/trivy/pkg/iac/scanners" + "github.com/aquasecurity/trivy/pkg/iac/scanners/ansible" + "github.com/aquasecurity/trivy/pkg/iac/scanners/azure/arm" + cfscanner "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation" + cfparser "github.com/aquasecurity/trivy/pkg/iac/scanners/cloudformation/parser" + dfscanner "github.com/aquasecurity/trivy/pkg/iac/scanners/dockerfile" + "github.com/aquasecurity/trivy/pkg/iac/scanners/generic" + "github.com/aquasecurity/trivy/pkg/iac/scanners/helm" + k8sscanner "github.com/aquasecurity/trivy/pkg/iac/scanners/kubernetes" + "github.com/aquasecurity/trivy/pkg/iac/scanners/options" + "github.com/aquasecurity/trivy/pkg/iac/scanners/terraform" + tfprawscanner "github.com/aquasecurity/trivy/pkg/iac/scanners/terraformplan/snapshot" + tfpjsonscanner "github.com/aquasecurity/trivy/pkg/iac/scanners/terraformplan/tfjson" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/mapfs" + "github.com/aquasecurity/trivy/pkg/version/app" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" + + _ "embed" +) + +var enablediacTypes = map[detection.FileType]types.ConfigType{ + detection.FileTypeAzureARM: types.AzureARM, + detection.FileTypeCloudFormation: types.CloudFormation, + detection.FileTypeTerraform: types.Terraform, + detection.FileTypeDockerfile: types.Dockerfile, + detection.FileTypeKubernetes: types.Kubernetes, + detection.FileTypeHelm: types.Helm, + detection.FileTypeTerraformPlanJSON: types.TerraformPlanJSON, + detection.FileTypeTerraformPlanSnapshot: types.TerraformPlanSnapshot, + detection.FileTypeJSON: types.JSON, + detection.FileTypeYAML: types.YAML, + detection.FileTypeAnsible: types.Ansible, +} + +type ScannerOption struct { + Trace bool + Namespaces []string + PolicyPaths []string + DataPaths []string + DisableEmbeddedPolicies bool + DisableEmbeddedLibraries bool + IncludeDeprecatedChecks bool + RegoErrorLimit int + + HelmValues []string + HelmValueFiles []string + HelmFileValues []string + HelmStringValues []string + HelmAPIVersions []string + HelmKubeVersion string + TerraformTFVars []string + CloudFormationParamVars []string + TfExcludeDownloaded bool + RawConfigScanners []types.ConfigType + K8sVersion string + + FilePatterns []string + ConfigFileSchemas []*ConfigFileSchema + + AnsiblePlaybooks []string + AnsibleInventories []string + AnsibleExtraVars map[string]any + + SkipFiles []string + SkipDirs []string + + RegoScanner *rego.Scanner +} + +func (o *ScannerOption) Sort() { + sort.Strings(o.Namespaces) + sort.Strings(o.PolicyPaths) + sort.Strings(o.DataPaths) +} + +type Scanner struct { + fileType detection.FileType + scanner scanners.FSScanner + hasFilePattern bool + configFileSchemas []*ConfigFileSchema +} + +func NewScanner(t detection.FileType, opt ScannerOption) (*Scanner, error) { + opts, err := scannerOptions(t, opt) + if err != nil { + return nil, err + } + + var scanner scanners.FSScanner + switch t { + case detection.FileTypeAzureARM: + scanner = arm.New(opts...) + case detection.FileTypeCloudFormation: + scanner = cfscanner.New(opts...) + case detection.FileTypeDockerfile: + scanner = dfscanner.NewScanner(opts...) + case detection.FileTypeHelm: + scanner = helm.New(opts...) + case detection.FileTypeKubernetes: + scanner = k8sscanner.NewScanner(opts...) + case detection.FileTypeTerraform: + scanner = terraform.New(opts...) + case detection.FileTypeTerraformPlanJSON: + scanner = tfpjsonscanner.New(opts...) + case detection.FileTypeTerraformPlanSnapshot: + scanner = tfprawscanner.New(opts...) + case detection.FileTypeYAML: + scanner = generic.NewYamlScanner(opts...) + case detection.FileTypeJSON: + scanner = generic.NewJsonScanner(opts...) + case detection.FileTypeAnsible: + scanner = ansible.New(opts...) + default: + return nil, xerrors.Errorf("unknown file type: %s", t) + } + + return &Scanner{ + fileType: t, + scanner: scanner, + hasFilePattern: hasFilePattern(t, opt.FilePatterns), + configFileSchemas: opt.ConfigFileSchemas, + }, nil +} + +func (s *Scanner) Scan(ctx context.Context, fsys fs.FS) ([]types.Misconfiguration, error) { + ctx = log.WithContextPrefix(ctx, log.PrefixMisconfiguration) + newfs, err := s.filterFS(fsys) + if err != nil { + return nil, xerrors.Errorf("fs filter error: %w", err) + } else if newfs == nil { + // Skip scanning if no relevant files are found + return nil, nil + } + + log.DebugContext(ctx, "Scanning files for misconfigurations...", log.String("scanner", s.scanner.Name())) + results, err := s.scanner.ScanFS(ctx, newfs, ".") + if err != nil { + var invalidContentError *cfparser.InvalidContentError + if errors.As(err, &invalidContentError) { + log.ErrorContext(ctx, "scan was broken with InvalidContentError", s.scanner.Name(), log.Err(err)) + return nil, nil + } + return nil, xerrors.Errorf("scan config error: %w", err) + } + + configType := enablediacTypes[s.fileType] + misconfs := ResultsToMisconf(configType, s.scanner.Name(), results) + + // Sort misconfigurations + for _, misconf := range misconfs { + sort.Sort(misconf.Successes) + sort.Sort(misconf.Warnings) + sort.Sort(misconf.Failures) + } + + return misconfs, nil +} + +func (s *Scanner) filterFS(fsys fs.FS) (fs.FS, error) { + mfs, ok := fsys.(*mapfs.FS) + if !ok { + // Unable to filter this filesystem + return fsys, nil + } + + schemas := lo.SliceToMap(s.configFileSchemas, func(schema *ConfigFileSchema) (string, *gojsonschema.Schema) { + return schema.path, schema.schema + }) + + var foundRelevantFile bool + filter := func(path string, _ fs.DirEntry) (bool, error) { + file, err := fsys.Open(path) + if err != nil { + return false, err + } + defer file.Close() + + rs, ok := file.(io.ReadSeeker) + if !ok { + return false, xerrors.Errorf("type assertion error: %w", err) + } + + if len(schemas) > 0 && + (s.fileType == detection.FileTypeYAML || s.fileType == detection.FileTypeJSON) && + !detection.IsFileMatchesSchemas(schemas, s.fileType, path, rs) { + return true, nil + } else if !s.hasFilePattern && !detection.IsType(path, rs, s.fileType) { + return true, nil + } + + foundRelevantFile = true + return false, nil + } + newfs, err := mfs.FilterFunc(filter) + if err != nil { + return nil, xerrors.Errorf("fs filter error: %w", err) + } + if !foundRelevantFile { + return nil, nil + } + return newfs, nil +} + +func InitRegoScanner(opt ScannerOption) (*rego.Scanner, error) { + regoOpts, err := initRegoOptions(opt) + if err != nil { + return nil, xerrors.Errorf("init rego options: %w", err) + } + regoScanner := rego.NewScanner(regoOpts...) + // note: it is safe to pass nil as fsys, since checks and data files will be loaded + // from the filesystems passed through the options. + if err := regoScanner.LoadPolicies(nil); err != nil { + return nil, xerrors.Errorf("load checks: %w", err) + } + return regoScanner, nil +} + +func initRegoOptions(opt ScannerOption) ([]options.ScannerOption, error) { + opts := []options.ScannerOption{ + rego.WithEmbeddedPolicies(!opt.DisableEmbeddedPolicies), + rego.WithEmbeddedLibraries(!opt.DisableEmbeddedLibraries), + rego.WithIncludeDeprecatedChecks(opt.IncludeDeprecatedChecks), + rego.WithTrivyVersion(app.Version()), + } + + if opt.RegoErrorLimit >= 0 { + opts = append(opts, rego.WithMaxAllowedErrors(opt.RegoErrorLimit)) + } + + policyFS, policyPaths, err := CreatePolicyFS(opt.PolicyPaths) + if err != nil { + return nil, err + } + if policyFS != nil { + opts = append(opts, rego.WithPolicyFilesystem(policyFS)) + } + + dataFS, dataPaths, err := CreateDataFS(opt.DataPaths, opt.K8sVersion) + if err != nil { + return nil, err + } + + schemas := lo.SliceToMap(opt.ConfigFileSchemas, func(schema *ConfigFileSchema) (string, []byte) { + return schema.name, schema.source + }) + + opts = append(opts, + rego.WithDataDirs(dataPaths...), + rego.WithDataFilesystem(dataFS), + rego.WithCustomSchemas(schemas), + ) + + if opt.Trace { + opts = append(opts, rego.WithPerResultTracing(true)) + } + + if len(policyPaths) > 0 { + opts = append(opts, rego.WithPolicyDirs(policyPaths...)) + } + + if len(opt.DataPaths) > 0 { + opts = append(opts, rego.WithDataDirs(opt.DataPaths...)) + } + + if len(opt.Namespaces) > 0 { + opts = append(opts, rego.WithPolicyNamespaces(opt.Namespaces...)) + } + return opts, nil +} + +func scannerOptions(t detection.FileType, opt ScannerOption) ([]options.ScannerOption, error) { + var opts []options.ScannerOption + + if opt.RegoScanner != nil { + opts = append(opts, rego.WithRegoScanner(opt.RegoScanner)) + } else { + // If RegoScanner is not provided, pass the Rego options to IaC scanners + // so that they can initialize the Rego scanner themselves + regoOpts, err := initRegoOptions(opt) + if err != nil { + return nil, xerrors.Errorf("init rego opts: %w", err) + } + opts = append(opts, regoOpts...) + } + + opts = append(opts, options.WithScanRawConfig( + slices.Contains(opt.RawConfigScanners, enablediacTypes[t])), + ) + + switch t { + case detection.FileTypeHelm: + return addHelmOpts(opts, opt), nil + case detection.FileTypeTerraform, detection.FileTypeTerraformPlanSnapshot: + return addTFOpts(opts, opt) + case detection.FileTypeCloudFormation: + return addCFOpts(opts, opt) + case detection.FileTypeAnsible: + return addAnsibleOpts(opts, opt), nil + default: + return opts, nil + } +} + +func hasFilePattern(t detection.FileType, filePatterns []string) bool { + for _, pattern := range filePatterns { + if strings.HasPrefix(pattern, fmt.Sprintf("%s:", t)) { + return true + } + } + return false +} + +func addTFOpts(opts []options.ScannerOption, scannerOption ScannerOption) ([]options.ScannerOption, error) { + if len(scannerOption.TerraformTFVars) > 0 { + configFS, err := createConfigFS(scannerOption.TerraformTFVars) + if err != nil { + return nil, xerrors.Errorf("failed to create Terraform config FS: %w", err) + } + opts = append( + opts, + terraform.ScannerWithTFVarsPaths(scannerOption.TerraformTFVars...), + terraform.ScannerWithConfigsFileSystem(configFS), + ) + } + + opts = append(opts, + terraform.ScannerWithAllDirectories(true), + terraform.ScannerWithSkipDownloaded(scannerOption.TfExcludeDownloaded), + terraform.ScannerWithSkipFiles(scannerOption.SkipFiles), + terraform.ScannerWithSkipDirs(scannerOption.SkipDirs), + ) + + return opts, nil +} + +func addCFOpts(opts []options.ScannerOption, scannerOption ScannerOption) ([]options.ScannerOption, error) { + if len(scannerOption.CloudFormationParamVars) > 0 { + configFS, err := createConfigFS(scannerOption.CloudFormationParamVars) + if err != nil { + return nil, xerrors.Errorf("failed to create CloudFormation config FS: %w", err) + } + opts = append( + opts, + cfscanner.WithParameterFiles(scannerOption.CloudFormationParamVars...), + cfscanner.WithConfigsFS(configFS), + ) + } + return opts, nil +} + +func addHelmOpts(opts []options.ScannerOption, scannerOption ScannerOption) []options.ScannerOption { + if len(scannerOption.HelmValueFiles) > 0 { + opts = append(opts, helm.ScannerWithValuesFile(scannerOption.HelmValueFiles...)) + } + + if len(scannerOption.HelmValues) > 0 { + opts = append(opts, helm.ScannerWithValues(scannerOption.HelmValues...)) + } + + if len(scannerOption.HelmFileValues) > 0 { + opts = append(opts, helm.ScannerWithFileValues(scannerOption.HelmFileValues...)) + } + + if len(scannerOption.HelmStringValues) > 0 { + opts = append(opts, helm.ScannerWithStringValues(scannerOption.HelmStringValues...)) + } + + if len(scannerOption.HelmAPIVersions) > 0 { + opts = append(opts, helm.ScannerWithAPIVersions(scannerOption.HelmAPIVersions...)) + } + + if scannerOption.HelmKubeVersion != "" { + opts = append(opts, helm.ScannerWithKubeVersion(scannerOption.HelmKubeVersion)) + } + + return opts +} + +func addAnsibleOpts(opts []options.ScannerOption, scannerOpt ScannerOption) []options.ScannerOption { + return append(opts, + ansible.WithPlaybooks(scannerOpt.AnsiblePlaybooks), + ansible.WithInventories(scannerOpt.AnsibleInventories), + ansible.WithExtraVars(scannerOpt.AnsibleExtraVars), + ) +} + +func createConfigFS(paths []string) (fs.FS, error) { + mfs := mapfs.New() + for _, path := range paths { + if err := mfs.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil && !errors.Is(err, fs.ErrExist) { + return nil, xerrors.Errorf("create dir error: %w", err) + } + if err := mfs.WriteFile(path, path); err != nil { + return nil, xerrors.Errorf("write file error: %w", err) + } + } + return mfs, nil +} + +func CheckPathExists(path string) (fs.FileInfo, string, error) { + abs, err := filepath.Abs(path) + if err != nil { + return nil, "", xerrors.Errorf("failed to derive absolute path from '%s': %w", path, err) + } + fi, err := os.Stat(abs) + if errors.Is(err, os.ErrNotExist) { + return nil, "", xerrors.Errorf("cache does not exist at %q", abs) + } else if err != nil { + return nil, "", xerrors.Errorf("file %q stat error: %w", abs, err) + } + return fi, abs, nil +} + +func CreatePolicyFS(policyPaths []string) (fs.FS, []string, error) { + if len(policyPaths) == 0 { + return nil, nil, nil + } + + mfs := mapfs.New() + for _, p := range policyPaths { + fi, abs, err := CheckPathExists(p) + if err != nil { + return nil, nil, err + } + + if fi.IsDir() { + if err = mfs.CopyFilesUnder(abs); err != nil { + return nil, nil, xerrors.Errorf("mapfs file copy error: %w", err) + } + } else { + if err := mfs.MkdirAll(filepath.Dir(abs), os.ModePerm); err != nil && !errors.Is(err, fs.ErrExist) { + return nil, nil, xerrors.Errorf("mapfs mkdir error: %w", err) + } + if err := mfs.WriteFile(abs, abs); err != nil { + return nil, nil, xerrors.Errorf("mapfs write error: %w", err) + } + } + } + + // check paths are no longer needed as fs.FS contains only needed files now. + policyPaths = []string{"."} + + return mfs, policyPaths, nil +} + +func CreateDataFS(dataPaths []string, opts ...string) (fs.FS, []string, error) { + fsys := mapfs.New() + + // Check if k8sVersion is provided + if len(opts) > 0 { + k8sVersion := opts[0] + if err := fsys.MkdirAll("system", 0o700); err != nil { + return nil, nil, err + } + data := fmt.Appendf(nil, `{"k8s": {"version": %q}}`, k8sVersion) + if err := fsys.WriteVirtualFile("system/k8s-version.json", data, 0o600); err != nil { + return nil, nil, err + } + } + + for _, path := range dataPaths { + if err := fsys.CopyFilesUnder(path); err != nil { + return nil, nil, err + } + } + + // dataPaths are no longer needed as fs.FS contains only needed files now. + dataPaths = []string{"."} + + return fsys, dataPaths, nil +} + +// ResultsToMisconf is exported for trivy-plugin-aqua purposes only +func ResultsToMisconf(configType types.ConfigType, scannerName string, results scan.Results) []types.Misconfiguration { + misconfs := make(map[string]types.Misconfiguration) + + for _, result := range results { + flattened := result.Flatten() + + query := fmt.Sprintf("data.%s.%s", result.RegoNamespace(), result.RegoRule()) + cause := NewCauseWithCode(result, flattened) + + misconfResult := types.MisconfResult{ + Namespace: result.RegoNamespace(), + Query: query, + Message: flattened.Description, + PolicyMetadata: types.PolicyMetadata{ + ID: result.Rule().ID, + AVDID: result.Rule().AVDID, + Aliases: result.Rule().Aliases, + Type: fmt.Sprintf("%s Security Check", scannerName), + Title: result.Rule().Summary, + Description: result.Rule().Explanation, + Severity: string(flattened.Severity), + RecommendedActions: flattened.Resolution, + References: flattened.Links, + }, + CauseMetadata: cause, + Traces: result.Traces(), + } + + filePath := flattened.Location.Filename + misconf, ok := misconfs[filePath] + if !ok { + misconf = types.Misconfiguration{ + FileType: configType, + FilePath: filepath.ToSlash(filePath), // defsec return OS-aware path + } + } + + switch flattened.Status { + case scan.StatusPassed: + misconf.Successes = append(misconf.Successes, misconfResult) + case scan.StatusFailed: + misconf.Failures = append(misconf.Failures, misconfResult) + } + + misconfs[filePath] = misconf + } + + return types.ToMisconfigurations(misconfs) +} + +func NewCauseWithCode(underlying scan.Result, flat scan.FlatResult) types.CauseMetadata { + cause := types.CauseMetadata{ + Resource: flat.Resource, + Provider: flat.RuleProvider.DisplayName(), + Service: flat.RuleService, + StartLine: flat.Location.StartLine, + EndLine: flat.Location.EndLine, + } + for _, o := range flat.Occurrences { + cause.Occurrences = append(cause.Occurrences, types.Occurrence{ + Resource: o.Resource, + Filename: o.Filename, + Location: types.Location{ + StartLine: o.StartLine, + EndLine: o.EndLine, + }, + }) + } + + // only failures have a code cause + // failures can happen either due to lack of + // OR misconfiguration of something + if underlying.Status() == scan.StatusFailed { + if flat.RenderedCause.Raw != "" { + highlighted, _ := scan.Highlight(flat.Location.Filename, flat.RenderedCause.Raw, scan.DarkTheme) + cause.RenderedCause = types.RenderedCause{ + Raw: flat.RenderedCause.Raw, + Highlighted: highlighted, + } + } + + if code, err := underlying.GetCode(); err == nil { + cause.Code = types.Code{ + Lines: xslices.Map(code.Lines, func(l scan.Line) types.Line { + return types.Line{ + Number: l.Number, + Content: l.Content, + IsCause: l.IsCause, + Annotation: l.Annotation, + Truncated: l.Truncated, + Highlighted: l.Highlighted, + FirstCause: l.FirstCause, + LastCause: l.LastCause, + } + }), + } + } + } + return cause +} diff --git a/pkg/misconf/scanner_test.go b/pkg/misconf/scanner_test.go new file mode 100644 index 000000000000..45dd65528450 --- /dev/null +++ b/pkg/misconf/scanner_test.go @@ -0,0 +1,201 @@ +package misconf + +import ( + "io/fs" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/detection" + "github.com/aquasecurity/trivy/pkg/mapfs" +) + +func TestScannerOption_Sort(t *testing.T) { + type fields struct { + Namespaces []string + PolicyPaths []string + DataPaths []string + } + tests := []struct { + name string + fields fields + want ScannerOption + }{ + { + name: "happy path", + fields: fields{ + Namespaces: []string{ + "main", + "custom", + "default", + }, + PolicyPaths: []string{"policy"}, + DataPaths: []string{ + "data/b", + "data/c", + "data/a", + }, + }, + want: ScannerOption{ + Namespaces: []string{ + "custom", + "default", + "main", + }, + PolicyPaths: []string{"policy"}, + DataPaths: []string{ + "data/a", + "data/b", + "data/c", + }, + }, + }, + { + name: "missing some fields", + fields: fields{ + Namespaces: []string{"main"}, + PolicyPaths: nil, + DataPaths: nil, + }, + want: ScannerOption{ + Namespaces: []string{"main"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + o := ScannerOption{ + Namespaces: tt.fields.Namespaces, + PolicyPaths: tt.fields.PolicyPaths, + DataPaths: tt.fields.DataPaths, + } + o.Sort() + + assert.Equal(t, tt.want, o) + }) + } +} + +func TestScanner_Scan(t *testing.T) { + type fields struct { + opt ScannerOption + } + type file struct { + path string + content []byte + } + tests := []struct { + name string + fileType detection.FileType + fields fields + files []file + wantFilePath string + wantFileType types.ConfigType + misconfsExpected int + }{ + { + name: "happy path. Dockerfile", + fileType: detection.FileTypeDockerfile, + fields: fields{ + opt: ScannerOption{}, + }, + files: []file{ + { + path: "Dockerfile", + content: []byte(`FROM alpine`), + }, + }, + wantFilePath: "Dockerfile", + wantFileType: types.Dockerfile, + misconfsExpected: 1, + }, + { + name: "happy path. Dockerfile with custom file name", + fileType: detection.FileTypeDockerfile, + fields: fields{ + opt: ScannerOption{ + FilePatterns: []string{"dockerfile:dockerf"}, + }, + }, + files: []file{ + { + path: "dockerf", + content: []byte(`FROM alpine`), + }, + }, + wantFilePath: "dockerf", + wantFileType: types.Dockerfile, + misconfsExpected: 1, + }, + { + name: "happy path. terraform plan file", + fileType: detection.FileTypeTerraformPlanJSON, + files: []file{ + { + path: "main.tfplan.json", + content: []byte(`{"format_version":"1.1","terraform_version":"1.4.6","planned_values":{"root_module":{"resources":[{"address":"aws_s3_bucket.my-bucket","mode":"managed","type":"aws_s3_bucket","name":"my-bucket","provider_name":"registry.terraform.io/hashicorp/aws","schema_version":0,"values":{"bucket":"evil","force_destroy":false,"tags":null,"timeouts":null},"sensitive_values":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[],"website":[]}}]}},"resource_changes":[{"address":"aws_s3_bucket.my-bucket","mode":"managed","type":"aws_s3_bucket","name":"my-bucket","provider_name":"registry.terraform.io/hashicorp/aws","change":{"actions":["create"],"before":null,"after":{"bucket":"evil","force_destroy":false,"tags":null,"timeouts":null},"after_unknown":{"acceleration_status":true,"acl":true,"arn":true,"bucket_domain_name":true,"bucket_prefix":true,"bucket_regional_domain_name":true,"cors_rule":true,"grant":true,"hosted_zone_id":true,"id":true,"lifecycle_rule":true,"logging":true,"object_lock_configuration":true,"object_lock_enabled":true,"policy":true,"region":true,"replication_configuration":true,"request_payer":true,"server_side_encryption_configuration":true,"tags_all":true,"versioning":true,"website":true,"website_domain":true,"website_endpoint":true},"before_sensitive":false,"after_sensitive":{"cors_rule":[],"grant":[],"lifecycle_rule":[],"logging":[],"object_lock_configuration":[],"replication_configuration":[],"server_side_encryption_configuration":[],"tags_all":{},"versioning":[],"website":[]}}}],"configuration":{"provider_config":{"aws":{"name":"aws","full_name":"registry.terraform.io/hashicorp/aws","expressions":{"profile":{"constant_value":"foo-bar-123123123"},"region":{"constant_value":"us-west-1"}}}},"root_module":{"resources":[{"address":"aws_s3_bucket.my-bucket","mode":"managed","type":"aws_s3_bucket","name":"my-bucket","provider_config_key":"aws","expressions":{"bucket":{"constant_value":"evil"}},"schema_version":0}]}}}`), + }, + }, + wantFilePath: "main.tf", + wantFileType: types.TerraformPlanJSON, + misconfsExpected: 2, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a virtual filesystem for testing + fsys := mapfs.New() + for _, f := range tt.files { + err := fsys.WriteVirtualFile(f.path, f.content, 0o666) + require.NoError(t, err) + } + + // s, err := tt.scannerFunc(tt.fields.filePatterns, tt.fields.opt) + s, err := NewScanner(tt.fileType, tt.fields.opt) + require.NoError(t, err) + + misconfs, err := s.Scan(t.Context(), fsys) + require.NoError(t, err) + require.Len(t, misconfs, tt.misconfsExpected, "wrong number of misconfigurations found") + if tt.misconfsExpected == 1 { + assert.Equal(t, tt.wantFilePath, misconfs[0].FilePath, "filePaths don't equal") + assert.Equal(t, tt.wantFileType, misconfs[0].FileType, "fileTypes don't equal") + } + }) + } +} + +func Test_createPolicyFS(t *testing.T) { + t.Run("outside pwd", func(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "subdir", "testdir"), 0o750)) + f, got, err := CreatePolicyFS([]string{filepath.Join(tmpDir, "subdir", "testdir")}) + assertFS(t, tmpDir, f, got, err) + }) +} + +func Test_CreateDataFS(t *testing.T) { + t.Run("outside pwd", func(t *testing.T) { + tmpDir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(tmpDir, "subdir", "testdir"), 0o750)) + f, got, err := CreateDataFS([]string{filepath.Join(tmpDir, "subdir", "testdir")}) + assertFS(t, tmpDir, f, got, err) + }) +} + +func assertFS(t *testing.T, tmpDir string, f fs.FS, got []string, err error) { + t.Helper() + + require.NoError(t, err) + assert.Equal(t, []string{"."}, got) + + d, err := f.Open(tmpDir) + require.NoError(t, err) + stat, err := d.Stat() + require.NoError(t, err) + assert.True(t, stat.IsDir()) +} diff --git a/pkg/misconf/testdata/schemas/no-schema.file b/pkg/misconf/testdata/schemas/no-schema.file new file mode 100644 index 000000000000..7b4d68d70fca --- /dev/null +++ b/pkg/misconf/testdata/schemas/no-schema.file @@ -0,0 +1 @@ +empty \ No newline at end of file diff --git a/pkg/misconf/testdata/schemas/schema-with-bad-regex.json b/pkg/misconf/testdata/schemas/schema-with-bad-regex.json new file mode 100644 index 000000000000..522a1ccb46e3 --- /dev/null +++ b/pkg/misconf/testdata/schemas/schema-with-bad-regex.json @@ -0,0 +1,9 @@ +{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "patternProperties": { + "^(?![\\.0-9]).": { "type": "string" }, + "test\\Z": { "type": "string" } + } +} diff --git a/pkg/misconf/testdata/schemas/schema1.json b/pkg/misconf/testdata/schemas/schema1.json new file mode 100644 index 000000000000..f72cdb0d2016 --- /dev/null +++ b/pkg/misconf/testdata/schemas/schema1.json @@ -0,0 +1,9 @@ +{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "service": { "type": "string" } + }, + "required": ["service"] +} \ No newline at end of file diff --git a/pkg/misconf/testdata/schemas/schema2.json b/pkg/misconf/testdata/schemas/schema2.json new file mode 100644 index 000000000000..f72cdb0d2016 --- /dev/null +++ b/pkg/misconf/testdata/schemas/schema2.json @@ -0,0 +1,9 @@ +{ + "$id": "https://example.com/test.schema.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "service": { "type": "string" } + }, + "required": ["service"] +} \ No newline at end of file diff --git a/pkg/module/api/api.go b/pkg/module/api/api.go new file mode 100644 index 000000000000..9a7337ed8899 --- /dev/null +++ b/pkg/module/api/api.go @@ -0,0 +1,29 @@ +package api + +import ( + "github.com/aquasecurity/trivy/pkg/module/serialize" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + Version = 1 + + ActionInsert serialize.PostScanAction = "INSERT" + ActionUpdate serialize.PostScanAction = "UPDATE" + ActionDelete serialize.PostScanAction = "DELETE" +) + +type Module interface { + Version() int + Name() string +} + +type Analyzer interface { + RequiredFiles() []string + Analyze(filePath string) (*serialize.AnalysisResult, error) +} + +type PostScanner interface { + PostScanSpec() serialize.PostScanSpec + PostScan(results types.Results) (types.Results, error) +} diff --git a/pkg/module/command.go b/pkg/module/command.go new file mode 100644 index 000000000000..a74da8384c3f --- /dev/null +++ b/pkg/module/command.go @@ -0,0 +1,51 @@ +package module + +import ( + "context" + "os" + "path/filepath" + + "github.com/google/go-containerregistry/pkg/name" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/oci" +) + +const mediaType = "application/vnd.module.wasm.content.layer.v1+wasm" + +// Install installs a module +func Install(ctx context.Context, dir, repo string, quiet bool, opt types.RegistryOptions) error { + ref, err := name.ParseReference(repo) + if err != nil { + return xerrors.Errorf("repository parse error: %w", err) + } + + log.Info("Installing the module from the repository...", log.String("repo", repo)) + art := oci.NewArtifact(repo, opt) + + dst := filepath.Join(dir, ref.Context().Name()) + log.Debug("Installing the module...", log.String("dst", dst)) + if err = art.Download(ctx, dst, oci.DownloadOption{MediaType: mediaType, Quiet: quiet}); err != nil { + return xerrors.Errorf("module download error: %w", err) + } + + return nil +} + +// Uninstall uninstalls a module +func Uninstall(_ context.Context, dir, repo string) error { + ref, err := name.ParseReference(repo) + if err != nil { + return xerrors.Errorf("repository parse error: %w", err) + } + + log.Info("Uninstalling the module ...", log.String("module", repo)) + dst := filepath.Join(dir, ref.Context().Name()) + if err = os.RemoveAll(dst); err != nil { + return xerrors.Errorf("remove error: %w", err) + } + + return nil +} diff --git a/pkg/module/memfs.go b/pkg/module/memfs.go new file mode 100644 index 000000000000..a62ae01ca5cb --- /dev/null +++ b/pkg/module/memfs.go @@ -0,0 +1,71 @@ +package module + +import ( + "io" + "io/fs" + "path/filepath" + "time" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/mapfs" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +// memFS is a wrapper of mapfs.FS and can change its underlying file system +// at runtime. This implements fs.FS. +type memFS struct { + current *mapfs.FS +} + +// Open implements fs.FS. +func (m *memFS) Open(name string) (fs.File, error) { + if m.current != nil { + return m.current.Open(name) + } + // memFS is always a directory. + if name == "." { + return &emptyDir{}, nil + } + return nil, fs.ErrNotExist +} + +// initialize changes the underlying memory file system with the given file path and contents. +// +// Note: it is always to safe swap the underlying FS with this API since this is called only at the beginning of +// Analyze interface call, which is not concurrently called per module instance. +func (m *memFS) initialize(filePath string, content xio.ReadSeekerAt) error { + mfs := mapfs.New() + if err := mfs.MkdirAll(filepath.Dir(filePath), fs.ModePerm); err != nil { + return xerrors.Errorf("mapfs mkdir error: %w", err) + } + b, err := io.ReadAll(content) + if err != nil { + return xerrors.Errorf("read error: %w", err) + } + err = mfs.WriteVirtualFile(filePath, b, fs.ModePerm) + if err != nil { + return xerrors.Errorf("mapfs write error: %w", err) + } + + m.current = mfs + return nil +} + +type emptyDir struct{} + +func (emptyDir) Close() (err error) { return } +func (emptyDir) Stat() (fs.FileInfo, error) { return fakeRootDirInfo{}, nil } +func (emptyDir) Read([]byte) (int, error) { + return 0, &fs.PathError{Op: "read", Path: "/", Err: fs.ErrInvalid} +} + +type fakeRootDirInfo struct{} + +func (fakeRootDirInfo) Name() string { return "/" } +func (fakeRootDirInfo) Size() int64 { return 0 } +func (fakeRootDirInfo) Mode() fs.FileMode { return fs.ModeDir | 0o500 } +func (fakeRootDirInfo) ModTime() time.Time { return time.Unix(0, 0) } +func (fakeRootDirInfo) IsDir() bool { return true } +func (fakeRootDirInfo) Sys() any { return nil } +func (emptyDir) ReadDir(int) (dirents []fs.DirEntry, err error) { return } diff --git a/pkg/module/memfs_test.go b/pkg/module/memfs_test.go new file mode 100644 index 000000000000..bac354087ae5 --- /dev/null +++ b/pkg/module/memfs_test.go @@ -0,0 +1,58 @@ +package module + +import ( + "io" + "io/fs" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMemFS(t *testing.T) { + m := &memFS{} + require.Nil(t, m.current) + + const path, content = "/usr/foo/bar.txt", "my-content" + err := m.initialize(path, strings.NewReader(content)) + require.NoError(t, err) + require.NotNil(t, m.current) + + t.Run("happy", func(t *testing.T) { + f, err := m.Open(path) + require.NoError(t, err) + actual, err := io.ReadAll(f) + require.NoError(t, err) + require.Equal(t, content, string(actual)) + }) + + t.Run("not found", func(t *testing.T) { + _, err = m.Open(path + "tmp") + require.ErrorIs(t, err, os.ErrNotExist) + }) +} + +func TestMemFS_NilIsDirectory(t *testing.T) { + // Wasm module initializes before an FS has been + // associated to this memFS. We handle nil + // so that the guest knows that the mount will map + // to a directory in the future. + m := &memFS{} + require.Nil(t, m.current) + + f, err := m.Open(".") + require.NoError(t, err) + + t.Run("stat is dir", func(t *testing.T) { + st, err := f.Stat() + require.NoError(t, err) + require.True(t, st.IsDir()) + }) + + t.Run("read invalid", func(t *testing.T) { + buf := make([]byte, 4) + _, err = f.Read(buf) + require.ErrorIs(t, err, fs.ErrInvalid) + }) +} diff --git a/pkg/module/module.go b/pkg/module/module.go new file mode 100644 index 000000000000..0a0750a97ff6 --- /dev/null +++ b/pkg/module/module.go @@ -0,0 +1,772 @@ +package module + +import ( + "context" + "encoding/json" + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "slices" + "sync" + + "github.com/samber/lo" + "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/api" + wasi "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/extension" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/log" + tapi "github.com/aquasecurity/trivy/pkg/module/api" + "github.com/aquasecurity/trivy/pkg/module/serialize" + "github.com/aquasecurity/trivy/pkg/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +var ( + logFunctions = map[string]api.GoModuleFunc{ + "debug": logDebug, + "info": logInfo, + "warn": logWarn, + "error": logError, + } +) + +// logDebug is defined as an api.GoModuleFunc for lower overhead vs reflection. +func logDebug(_ context.Context, mod api.Module, params []uint64) { + offset, size := uint32(params[0]), uint32(params[1]) + buf := readMemory(mod.Memory(), offset, size) + if buf != nil { + log.Debug(string(buf)) + } +} + +// logInfo is defined as an api.GoModuleFunc for lower overhead vs reflection. +func logInfo(_ context.Context, mod api.Module, params []uint64) { + offset, size := uint32(params[0]), uint32(params[1]) + buf := readMemory(mod.Memory(), offset, size) + if buf != nil { + log.Info(string(buf)) + } +} + +// logWarn is defined as an api.GoModuleFunc for lower overhead vs reflection. +func logWarn(_ context.Context, mod api.Module, params []uint64) { + offset, size := uint32(params[0]), uint32(params[1]) + buf := readMemory(mod.Memory(), offset, size) + if buf != nil { + log.Warn(string(buf)) + } +} + +// logError is defined as an api.GoModuleFunc for lower overhead vs reflection. +func logError(_ context.Context, mod api.Module, params []uint64) { + offset, size := uint32(params[0]), uint32(params[1]) + buf := readMemory(mod.Memory(), offset, size) + if buf != nil { + log.Error(string(buf)) + } +} + +func readMemory(mem api.Memory, offset, size uint32) []byte { + buf, ok := mem.Read(offset, size) + if !ok { + log.Error("Memory.Read() out of range", + log.Int("offset", int(offset)), log.Int("size", int(size))) + return nil + } + return buf +} + +type Options struct { + Dir string + EnabledModules []string +} + +type Manager struct { + cache wazero.CompilationCache + modules []*wasmModule + dir string + enabledModules []string +} + +func NewManager(ctx context.Context, opts Options) (*Manager, error) { + m := &Manager{ + dir: opts.Dir, + enabledModules: opts.EnabledModules, + } + + // Create a new WebAssembly Runtime. + m.cache = wazero.NewCompilationCache() + + // Load WASM modules in local + if err := m.loadModules(ctx); err != nil { + return nil, xerrors.Errorf("module load error: %w", err) + } + + return m, nil +} + +func (m *Manager) loadModules(ctx context.Context) error { + _, err := os.Stat(m.dir) + if os.IsNotExist(err) { + return nil + } + log.Debug("Module dir", log.String("dir", m.dir)) + + err = filepath.Walk(m.dir, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } else if info.IsDir() || filepath.Ext(info.Name()) != ".wasm" { + return nil + } + + rel, err := filepath.Rel(m.dir, path) + if err != nil { + return xerrors.Errorf("failed to get a relative path: %w", err) + } + + log.Info("Reading a module...", log.String("path", rel)) + wasmCode, err := os.ReadFile(path) + if err != nil { + return xerrors.Errorf("file read error: %w", err) + } + + p, err := newWASMPlugin(ctx, m.cache, wasmCode) + if err != nil { + return xerrors.Errorf("WASM module init error %s: %w", rel, err) + } else if p == nil { + // Skip if nil => mismatch of API version etc. + return nil + } + + // Skip Loading WASM modules if not in the list of enable modules flag. + if len(m.enabledModules) > 0 && !slices.Contains(m.enabledModules, p.Name()) { + return nil + } + + log.Info("Module loaded", log.String("path", rel)) + m.modules = append(m.modules, p) + return nil + }) + if err != nil { + return xerrors.Errorf("module walk error: %w", err) + } + + return nil +} + +func (m *Manager) Register() { + for _, mod := range m.modules { + mod.Register() + } +} + +func (m *Manager) Deregister() { + for _, mod := range m.modules { + analyzer.DeregisterAnalyzer(analyzer.Type(mod.Name())) + extension.DeregisterHook(mod.Name()) + } +} + +func (m *Manager) Close(ctx context.Context) error { + return m.cache.Close(ctx) +} + +func splitPtrSize(u uint64) (uint32, uint32) { + ptr := uint32(u >> 32) + size := uint32(u) + return ptr, size +} + +// ptrSizeToString reads the memory pointed by ptrSize, converting it to string. +// IMPORTANT: If the WASM function allocated this string, the caller must free it (using freePtr). +func ptrSizeToString(mem api.Memory, ptrSize uint64) (string, error) { + ptr, size := splitPtrSize(ptrSize) + buf := readMemory(mem, ptr, size) + if buf == nil { + return "", xerrors.New("unable to read memory") + } + return string(buf), nil +} + +// stringToPtrSize returns a pointer and size pair for the given string in a way compatible with WebAssembly numeric types. +// The caller is responsible for calling free at some point if needed. +func stringToPtrSize(ctx context.Context, s string, mod api.Module, malloc api.Function) (uint64, uint64, error) { + size := uint64(len(s)) + results, err := malloc.Call(ctx, size) + if err != nil { + return 0, 0, xerrors.Errorf("malloc error: %w", err) + } + + // The pointer is a linear memory offset, which is where we write the string. + ptr := results[0] + if !mod.Memory().Write(uint32(ptr), []byte(s)) { + return 0, 0, xerrors.Errorf("Memory.Write(%d, %d) out of range of memory size %d", + ptr, size, mod.Memory().Size()) + } + return ptr, size, nil +} + +func freePtr(ctx context.Context, freeFn api.Function, ptrSize uint64) { + if ptrSize == 0 || freeFn == nil { + return + } + ptr, size := splitPtrSize(ptrSize) + if ptr == 0 { + return + } + // We're ignoring the error result to avoid overshadowing any preceding error. + _, _ = freeFn.Call(ctx, uint64(ptr), uint64(size)) +} + +// unmarshal reads memory at ptrSize, unmarshals JSON into v, but does not free automatically. +// The caller must ensure the pointer is freed if needed. +func unmarshal(mem api.Memory, ptrSize uint64, v any) error { + ptr, size := splitPtrSize(ptrSize) + buf := readMemory(mem, ptr, size) + if buf == nil { + return xerrors.New("unable to read memory") + } + if err := json.Unmarshal(buf, v); err != nil { + return xerrors.Errorf("unmarshal error: %w", err) + } + return nil +} + +// marshal JSON-encodes v, calls malloc, writes the data into memory, and returns ptr+size in 64-bit. +// The caller must free that pointer if the WASM side expects it freed. +func marshal(ctx context.Context, m api.Module, malloc api.Function, v any) (uint64, uint64, error) { + b, err := json.Marshal(v) + if err != nil { + return 0, 0, xerrors.Errorf("marshal error: %w", err) + } + + size := uint64(len(b)) + results, err := malloc.Call(ctx, size) + if err != nil { + return 0, 0, xerrors.Errorf("malloc error: %w", err) + } + ptr := results[0] + if !m.Memory().Write(uint32(ptr), b) { + return 0, 0, xerrors.Errorf("Memory.Write(%d, %d) out of range of memory size %d", + ptr, size, m.Memory().Size()) + } + return ptr, size, nil +} + +var _ extension.ScanHook = (*wasmModule)(nil) + +type wasmModule struct { + mod api.Module + memFS *memFS + mux sync.Mutex + + name string + version int + requiredFiles []*regexp.Regexp + + isAnalyzer bool + isPostScanner bool + postScanSpec serialize.PostScanSpec + + // Exported functions + analyze api.Function + postScan api.Function + malloc api.Function // Exported by Trivy Wasm SDK + free api.Function // Exported by Trivy Wasm SDK +} + +func newWASMPlugin(ctx context.Context, ccache wazero.CompilationCache, code []byte) (*wasmModule, error) { + mf := &memFS{} + config := wazero.NewModuleConfig().WithStdout(os.Stdout).WithFS(mf).WithStartFunctions("_initialize") + + // Create an empty namespace so that multiple modules will not conflict + r := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig().WithCompilationCache(ccache)) + + // Instantiate a Go-defined module named "env" that exports functions. + envBuilder := r.NewHostModuleBuilder("env") + + // Avoid reflection for logging as it implies an overhead of >1us per call. + for n, f := range logFunctions { + envBuilder.NewFunctionBuilder(). + WithGoModuleFunction(f, []api.ValueType{ + api.ValueTypeI32, + api.ValueTypeI32, + }, []api.ValueType{}). + WithParameterNames("offset", "size"). + Export(n) + } + + if _, err := envBuilder.Instantiate(ctx); err != nil { + return nil, xerrors.Errorf("wasm module build error: %w", err) + } + + if _, err := wasi.NewBuilder(r).Instantiate(ctx); err != nil { + return nil, xerrors.Errorf("WASI init error: %w", err) + } + + // Compile the WebAssembly module using the default configuration. + compiled, err := r.CompileModule(ctx, code) + if err != nil { + return nil, xerrors.Errorf("module compile error: %w", err) + } + + // InstantiateModule runs the "_initialize" function + mod, err := r.InstantiateModule(ctx, compiled, config) + if err != nil { + return nil, xerrors.Errorf("module init error: %w", err) + } + + malloc := mod.ExportedFunction("malloc") + free := mod.ExportedFunction("free") + + // Get a module name + name, err := moduleName(ctx, mod, free) + if err != nil { + return nil, xerrors.Errorf("failed to get a module name: %w", err) + } + + // Get a module version + version, err := moduleVersion(ctx, mod) + if err != nil { + return nil, xerrors.Errorf("failed to get a module version: %w", err) + } + + // Get a module API version + apiVersion, err := moduleAPIVersion(ctx, mod) + if err != nil { + return nil, xerrors.Errorf("failed to get a module version: %w", err) + } + + if apiVersion != tapi.Version { + log.Info("Ignore the module due to API version mismatch", + log.String("module", fmt.Sprintf("%s@v%d", name, version)), + log.Int("got", apiVersion), log.Int("want", tapi.Version)) + return nil, nil + } + + isAnalyzer, err := moduleIsAnalyzer(ctx, mod) + if err != nil { + return nil, xerrors.Errorf("failed to check if the module is an analyzer: %w", err) + } + + isPostScanner, err := moduleIsPostScanner(ctx, mod) + if err != nil { + return nil, xerrors.Errorf("failed to check if the module is a post scanner: %w", err) + } + + // Get exported functions by WASM module + analyzeFunc := mod.ExportedFunction("analyze") + if analyzeFunc == nil { + return nil, xerrors.New("analyze() must be exported") + } + postScanFunc := mod.ExportedFunction("post_scan") + if postScanFunc == nil { + return nil, xerrors.New("post_scan() must be exported") + } + + var requiredFiles []*regexp.Regexp + if isAnalyzer { + // Get required files + requiredFiles, err = moduleRequiredFiles(ctx, mod, free) + if err != nil { + return nil, xerrors.Errorf("failed to get required files: %w", err) + } + } + + var postScanSpec serialize.PostScanSpec + if isPostScanner { + // This spec defines how the module works in post scanning like INSERT, UPDATE and DELETE. + postScanSpec, err = modulePostScanSpec(ctx, mod, free) + if err != nil { + return nil, xerrors.Errorf("failed to get a post scan spec: %w", err) + } + } + + return &wasmModule{ + mod: mod, + memFS: mf, + name: name, + version: version, + requiredFiles: requiredFiles, + + isAnalyzer: isAnalyzer, + isPostScanner: isPostScanner, + postScanSpec: postScanSpec, + + analyze: analyzeFunc, + postScan: postScanFunc, + malloc: malloc, + free: free, + }, nil +} + +func (m *wasmModule) Register() { + logger := log.With(log.String("name", m.name), log.Int("version", m.version)) + logger.Info("Registering WASM module") + if m.isAnalyzer { + logger.Debug("Registering custom analyzer") + analyzer.RegisterAnalyzer(m) + } + if m.isPostScanner { + logger.Debug("Registering custom post scanner") + extension.RegisterHook(m) + } +} + +func (m *wasmModule) Close(ctx context.Context) error { + return m.mod.Close(ctx) +} + +func (m *wasmModule) Type() analyzer.Type { + return analyzer.Type(m.name) +} + +func (m *wasmModule) Name() string { + return m.name +} + +func (m *wasmModule) Version() int { + return m.version +} + +func (m *wasmModule) Required(filePath string, _ os.FileInfo) bool { + for _, r := range m.requiredFiles { + if r.MatchString(filePath) { + return true + } + } + return false +} + +func (m *wasmModule) Analyze(ctx context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) { + filePath := "/" + filepath.ToSlash(input.FilePath) + log.Debug("Module analyzing...", log.String("module", m.name), log.FilePath(filePath)) + + // Wasm module instances are not Goroutine safe, so we take look here since Analyze might be called concurrently. + // TODO: This is temporary solution and we could improve the Analyze performance by having module instance pool. + m.mux.Lock() + defer m.mux.Unlock() + + if err := m.memFS.initialize(filePath, input.Content); err != nil { + return nil, err + } + + // 1. Convert filePath -> WASM memory + inputPtr, inputSize, err := stringToPtrSize(ctx, filePath, m.mod, m.malloc) + if err != nil { + return nil, xerrors.Errorf("failed to write string to memory: %w", err) + } + defer m.free.Call(ctx, inputPtr) // nolint: errcheck + + // 2. Call analyze + analyzeRes, err := m.analyze.Call(ctx, inputPtr, inputSize) + if err != nil { + return nil, xerrors.Errorf("analyze error: %w", err) + } else if len(analyzeRes) != 1 { + return nil, xerrors.New("invalid signature: analyze") + } + + // 3. The returned pointer/size from analyze must be freed after reading + resultPtrSize := analyzeRes[0] + defer freePtr(ctx, m.free, resultPtrSize) + + // 4. Unmarshal the returned data + var result analyzer.AnalysisResult + if err = unmarshal(m.mod.Memory(), resultPtrSize, &result); err != nil { + return nil, xerrors.Errorf("invalid return value: %w", err) + } + + return &result, nil +} + +func (m *wasmModule) PreScan(_ context.Context, _ *types.ScanTarget, _ types.ScanOptions) error { + // TODO: Implement + return nil +} + +func (m *wasmModule) PostScan(ctx context.Context, results types.Results) (types.Results, error) { + // Find custom resources + var custom types.Result + for _, result := range results { + if result.Class == types.ClassCustom { + custom = result + break + } + } + + arg := types.Results{custom} + switch m.postScanSpec.Action { + case tapi.ActionUpdate, tapi.ActionDelete: + // Pass the relevant results to the module + arg = append(arg, findIDs(m.postScanSpec.IDs, results)...) + } + + // Marshal the argument into WASM memory so that the WASM module can read it. + inputPtr, inputSize, err := marshal(ctx, m.mod, m.malloc, arg) + if err != nil { + return nil, xerrors.Errorf("post scan marshal error: %w", err) + } + defer m.free.Call(ctx, inputPtr) //nolint: errcheck + + analyzeRes, err := m.postScan.Call(ctx, inputPtr, inputSize) + if err != nil { + return nil, xerrors.Errorf("post scan invocation error: %w", err) + } else if len(analyzeRes) != 1 { + return nil, xerrors.New("invalid signature: post_scan") + } + + // The returned pointer/size from post_scan must be freed after reading + postScanPtrSize := analyzeRes[0] + defer freePtr(ctx, m.free, postScanPtrSize) + + // Unmarshal the result + var got types.Results + if err = unmarshal(m.mod.Memory(), postScanPtrSize, &got); err != nil { + return nil, xerrors.Errorf("post scan unmarshal error: %w", err) + } + + switch m.postScanSpec.Action { + case tapi.ActionInsert: + results = append(results, lo.Filter(got, func(r types.Result, _ int) bool { + return r.Class != types.ClassCustom && r.Class != "" + })...) + case tapi.ActionUpdate: + updateResults(got, results) + case tapi.ActionDelete: + deleteResults(got, results) + } + + return results, nil +} + +func findIDs(ids []string, results types.Results) types.Results { + var filtered types.Results + for _, result := range results { + if result.Class == types.ClassCustom { + continue + } + vulns := lo.Filter(result.Vulnerabilities, func(v types.DetectedVulnerability, _ int) bool { + return slices.Contains(ids, v.VulnerabilityID) + }) + misconfs := lo.Filter(result.Misconfigurations, func(m types.DetectedMisconfiguration, _ int) bool { + return slices.Contains(ids, m.ID) + }) + if len(vulns) > 0 || len(misconfs) > 0 { + filtered = append(filtered, types.Result{ + Target: result.Target, + Class: result.Class, + Type: result.Type, + Vulnerabilities: vulns, + Misconfigurations: misconfs, + }) + } + } + return filtered +} + +func updateResults(gotResults, results types.Results) { + for _, g := range gotResults { + for i, result := range results { + if g.Target == result.Target && g.Class == result.Class && g.Type == result.Type { + results[i].Vulnerabilities = xslices.Map(result.Vulnerabilities, func(v types.DetectedVulnerability) types.DetectedVulnerability { + // Update vulnerabilities in the existing result + for _, got := range g.Vulnerabilities { + if got.VulnerabilityID == v.VulnerabilityID && got.PkgName == v.PkgName && + got.PkgPath == v.PkgPath && got.InstalledVersion == v.InstalledVersion { + + // Override vulnerability details + v.SeveritySource = got.SeveritySource + v.Vulnerability = got.Vulnerability + } + } + return v + }) + + results[i].Misconfigurations = xslices.Map(result.Misconfigurations, func(m types.DetectedMisconfiguration) types.DetectedMisconfiguration { + // Update misconfigurations in the existing result + for _, got := range g.Misconfigurations { + if got.ID == m.ID && + got.CauseMetadata.StartLine == m.CauseMetadata.StartLine && + got.CauseMetadata.EndLine == m.CauseMetadata.EndLine { + + // Override misconfiguration details + m.CauseMetadata = got.CauseMetadata + m.Severity = got.Severity + m.Status = got.Status + } + } + return m + }) + } + } + } +} + +func deleteResults(gotResults, results types.Results) { + for _, gotResult := range gotResults { + for i, result := range results { + // Remove vulnerabilities and misconfigurations from the existing result + if gotResult.Target == result.Target && gotResult.Class == result.Class && gotResult.Type == result.Type { + results[i].Vulnerabilities = lo.Reject(result.Vulnerabilities, func(v types.DetectedVulnerability, _ int) bool { + for _, got := range gotResult.Vulnerabilities { + if got.VulnerabilityID == v.VulnerabilityID && got.PkgName == v.PkgName && + got.PkgPath == v.PkgPath && got.InstalledVersion == v.InstalledVersion { + return true + } + } + return false + }) + results[i].Misconfigurations = lo.Reject(result.Misconfigurations, func(v types.DetectedMisconfiguration, _ int) bool { + for _, got := range gotResult.Misconfigurations { + if got.ID == v.ID && got.Status == v.Status && + got.CauseMetadata.StartLine == v.CauseMetadata.StartLine && + got.CauseMetadata.EndLine == v.CauseMetadata.EndLine { + return true + } + } + return false + }) + } + } + } +} + +func moduleName(ctx context.Context, mod api.Module, freeFn api.Function) (string, error) { + nameFunc := mod.ExportedFunction("name") + if nameFunc == nil { + return "", xerrors.New("name() must be exported") + } + + nameRes, err := nameFunc.Call(ctx) + if err != nil { + return "", xerrors.Errorf("wasm function name() invocation error: %w", err) + } else if len(nameRes) != 1 { + return "", xerrors.New("invalid signature: name()") + } + + ptrSize := nameRes[0] + defer freePtr(ctx, freeFn, ptrSize) + + name, err := ptrSizeToString(mod.Memory(), ptrSize) + if err != nil { + return "", xerrors.Errorf("invalid return value: %w", err) + } + return name, nil +} + +func moduleVersion(ctx context.Context, mod api.Module) (int, error) { + versionFunc := mod.ExportedFunction("version") + if versionFunc == nil { + return 0, xerrors.New("version() must be exported") + } + + versionRes, err := versionFunc.Call(ctx) + if err != nil { + return 0, xerrors.Errorf("wasm function version() invocation error: %w", err) + } else if len(versionRes) != 1 { + return 0, xerrors.New("invalid signature: version") + } + // version is an int, not a pointer + return int(uint32(versionRes[0])), nil +} + +func moduleAPIVersion(ctx context.Context, mod api.Module) (int, error) { + versionFunc := mod.ExportedFunction("api_version") + if versionFunc == nil { + return 0, xerrors.New("api_version() must be exported") + } + + versionRes, err := versionFunc.Call(ctx) + if err != nil { + return 0, xerrors.Errorf("wasm function api_version() invocation error: %w", err) + } else if len(versionRes) != 1 { + return 0, xerrors.New("invalid signature: api_version") + } + // not a pointer + return int(uint32(versionRes[0])), nil +} + +func moduleRequiredFiles(ctx context.Context, mod api.Module, freeFn api.Function) ([]*regexp.Regexp, error) { + requiredFilesFunc := mod.ExportedFunction("required") + if requiredFilesFunc == nil { + return nil, xerrors.New("required() must be exported") + } + + requiredFilesRes, err := requiredFilesFunc.Call(ctx) + if err != nil { + return nil, xerrors.Errorf("wasm function required() invocation error: %w", err) + } else if len(requiredFilesRes) != 1 { + return nil, xerrors.New("invalid signature: required") + } + + ptrSize := requiredFilesRes[0] + defer freePtr(ctx, freeFn, ptrSize) + + var fileRegexps []string + if err = unmarshal(mod.Memory(), ptrSize, &fileRegexps); err != nil { + return nil, xerrors.Errorf("invalid return value: %w", err) + } + + var requiredFiles []*regexp.Regexp + for _, file := range fileRegexps { + re, err := regexp.Compile(file) + if err != nil { + return nil, xerrors.Errorf("regexp compile error: %w", err) + } + requiredFiles = append(requiredFiles, re) + } + + return requiredFiles, nil +} + +func moduleIsAnalyzer(ctx context.Context, mod api.Module) (bool, error) { + return isType(ctx, mod, "is_analyzer") +} + +func moduleIsPostScanner(ctx context.Context, mod api.Module) (bool, error) { + return isType(ctx, mod, "is_post_scanner") +} + +func isType(ctx context.Context, mod api.Module, name string) (bool, error) { + isFunc := mod.ExportedFunction(name) + if isFunc == nil { + return false, xerrors.Errorf("%s() must be exported", name) + } + isRes, err := isFunc.Call(ctx) + if err != nil { + return false, xerrors.Errorf("wasm function %s() invocation error: %w", name, err) + } else if len(isRes) != 1 { + return false, xerrors.Errorf("invalid signature: %s", name) + } + + return isRes[0] > 0, nil +} + +func modulePostScanSpec(ctx context.Context, mod api.Module, freeFn api.Function) (serialize.PostScanSpec, error) { + postScanSpecFunc := mod.ExportedFunction("post_scan_spec") + if postScanSpecFunc == nil { + return serialize.PostScanSpec{}, xerrors.New("post_scan_spec() must be exported") + } + + postScanSpecRes, err := postScanSpecFunc.Call(ctx) + if err != nil { + return serialize.PostScanSpec{}, xerrors.Errorf("wasm function post_scan_spec() invocation error: %w", err) + } else if len(postScanSpecRes) != 1 { + return serialize.PostScanSpec{}, xerrors.New("invalid signature: post_scan_spec") + } + + ptrSize := postScanSpecRes[0] + defer freePtr(ctx, freeFn, ptrSize) + + var spec serialize.PostScanSpec + if err = unmarshal(mod.Memory(), ptrSize, &spec); err != nil { + return serialize.PostScanSpec{}, xerrors.Errorf("invalid return value: %w", err) + } + + return spec, nil +} diff --git a/pkg/module/module_test.go b/pkg/module/module_test.go new file mode 100644 index 000000000000..9704d688b3c1 --- /dev/null +++ b/pkg/module/module_test.go @@ -0,0 +1,134 @@ +package module_test + +import ( + "io/fs" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/extension" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/module" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +func TestManager_Register(t *testing.T) { + if runtime.GOOS == "windows" { + // WASM tests difficult on Windows + t.Skip("Test satisfied adequately by Linux tests") + } + tests := []struct { + name string + moduleDir string + enabledModules []string + wantAnalyzerVersions analyzer.Versions + wantExtentions []string + wantErr bool + }{ + { + name: "happy path", + moduleDir: "testdata/happy", + wantAnalyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "happy": 1, + }, + PostAnalyzers: make(map[string]int), + }, + wantExtentions: []string{ + "happy", + }, + }, + { + name: "only analyzer", + moduleDir: "testdata/analyzer", + wantAnalyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "analyzer": 1, + }, + PostAnalyzers: make(map[string]int), + }, + wantExtentions: nil, + }, + { + name: "only post scanner", + moduleDir: "testdata/scanner", + wantAnalyzerVersions: analyzer.Versions{ + Analyzers: make(map[string]int), + PostAnalyzers: make(map[string]int), + }, + wantExtentions: []string{ + "scanner", + }, + }, + { + name: "no module dir", + moduleDir: "no-such-dir", + wantAnalyzerVersions: analyzer.Versions{ + Analyzers: make(map[string]int), + PostAnalyzers: make(map[string]int), + }, + wantExtentions: nil, + }, + { + name: "pass enabled modules", + moduleDir: "testdata", + enabledModules: []string{ + "happy", + "analyzer", + }, + wantAnalyzerVersions: analyzer.Versions{ + Analyzers: map[string]int{ + "happy": 1, + "analyzer": 1, + }, + PostAnalyzers: make(map[string]int), + }, + wantExtentions: []string{ + "happy", + }, + }, + } + + // Confirm that wasm modules are generated beforehand + var count int + err := filepath.WalkDir("testdata", func(path string, _ fs.DirEntry, _ error) error { + if filepath.Ext(path) == ".wasm" { + count++ + } + return nil + }) + require.NoError(t, err) + // WASM modules must be generated before running the tests. + require.Equal(t, 3, count, "missing WASM modules, try 'mage test:unit' or 'mage test:generateModules'") + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m, err := module.NewManager(t.Context(), module.Options{ + Dir: tt.moduleDir, + EnabledModules: tt.enabledModules, + }) + require.NoError(t, err) + + // Register analyzer and post scanner from WASM module + m.Register() + + // Remove registered analyzers and post scanners so that it will not affect other tests. + defer m.Deregister() + + // Confirm the analyzer is registered + a, err := analyzer.NewAnalyzerGroup(analyzer.AnalyzerOptions{}) + require.NoError(t, err) + + got := a.AnalyzerVersions() + assert.Equal(t, tt.wantAnalyzerVersions, got) + + hookNames := xslices.Map(extension.Hooks(), func(hook extension.Hook) string { + return hook.Name() + }) + assert.Equal(t, tt.wantExtentions, hookNames) + }) + } +} diff --git a/pkg/module/serialize/types.go b/pkg/module/serialize/types.go new file mode 100644 index 000000000000..a8fe92b4620b --- /dev/null +++ b/pkg/module/serialize/types.go @@ -0,0 +1,31 @@ +package serialize + +import ( + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +// TODO: replace with analyzer.AnalysisResult +type AnalysisResult struct { + // TODO: support other fields as well + // OS *types.OS + // Repository *types.Repository + // PackageInfos []types.PackageInfo + // Applications []types.Application + // Secrets []types.Secret + // SystemInstalledFiles []string // A list of files installed by OS package manager + + // Currently it supports custom resources only + CustomResources []ftypes.CustomResource +} + +type PostScanAction string + +type PostScanSpec struct { + // What action the module will do in post scanning. + // value: INSERT, UPDATE and DELETE + Action PostScanAction + + // IDs represent which vulnerability and misconfiguration ID will be updated or deleted in post scanning. + // When the action is UPDATE or DELETE, the matched result will be passed to the module. + IDs []string +} diff --git a/pkg/module/testdata/analyzer/analyzer.go b/pkg/module/testdata/analyzer/analyzer.go new file mode 100644 index 000000000000..07baa2784d14 --- /dev/null +++ b/pkg/module/testdata/analyzer/analyzer.go @@ -0,0 +1,40 @@ +//go:generate go build -o analyzer.wasm -buildmode=c-shared analyzer.go +//go:build wasip1 + +package main + +import ( + "github.com/aquasecurity/trivy/pkg/module/serialize" + "github.com/aquasecurity/trivy/pkg/module/wasm" +) + +const ( + moduleVersion = 1 + moduleName = "analyzer" +) + +func main() {} + +func init() { + wasm.RegisterModule(AnalyzerModule{}) +} + +type AnalyzerModule struct{} + +func (AnalyzerModule) Version() int { + return moduleVersion +} + +func (AnalyzerModule) Name() string { + return moduleName +} + +func (AnalyzerModule) RequiredFiles() []string { + return []string{ + `foo(.?)`, + } +} + +func (s AnalyzerModule) Analyze(_ string) (*serialize.AnalysisResult, error) { + return nil, nil +} diff --git a/pkg/module/testdata/happy/happy.go b/pkg/module/testdata/happy/happy.go new file mode 100644 index 000000000000..fc0f91c27887 --- /dev/null +++ b/pkg/module/testdata/happy/happy.go @@ -0,0 +1,50 @@ +//go:generate go build -o happy.wasm -buildmode=c-shared happy.go +//go:build wasip1 + +package main + +import ( + "github.com/aquasecurity/trivy/pkg/module/api" + "github.com/aquasecurity/trivy/pkg/module/serialize" + "github.com/aquasecurity/trivy/pkg/module/wasm" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + moduleVersion = 1 + moduleName = "happy" +) + +func main() {} + +func init() { + wasm.RegisterModule(HappyModule{}) +} + +type HappyModule struct{} + +func (HappyModule) Version() int { + return moduleVersion +} + +func (HappyModule) Name() string { + return moduleName +} + +func (HappyModule) RequiredFiles() []string { + return []string{} +} + +func (s HappyModule) Analyze(_ string) (*serialize.AnalysisResult, error) { + return nil, nil +} + +func (HappyModule) PostScanSpec() serialize.PostScanSpec { + return serialize.PostScanSpec{ + Action: api.ActionInsert, // Add new vulnerabilities + } +} + +func (HappyModule) PostScan(_ types.Results) (types.Results, error) { + return nil, nil +} diff --git a/pkg/module/testdata/scanner/scanner.go b/pkg/module/testdata/scanner/scanner.go new file mode 100644 index 000000000000..1e6bcb0a5b7f --- /dev/null +++ b/pkg/module/testdata/scanner/scanner.go @@ -0,0 +1,42 @@ +//go:generate go build -o scanner.wasm -buildmode=c-shared scanner.go +//go:build wasip1 + +package main + +import ( + "github.com/aquasecurity/trivy/pkg/module/api" + "github.com/aquasecurity/trivy/pkg/module/serialize" + "github.com/aquasecurity/trivy/pkg/module/wasm" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + moduleVersion = 2 + moduleName = "scanner" +) + +func main() {} + +func init() { + wasm.RegisterModule(PostScannerModule{}) +} + +type PostScannerModule struct{} + +func (PostScannerModule) Version() int { + return moduleVersion +} + +func (PostScannerModule) Name() string { + return moduleName +} + +func (PostScannerModule) PostScanSpec() serialize.PostScanSpec { + return serialize.PostScanSpec{ + Action: api.ActionInsert, // Add new vulnerabilities + } +} + +func (PostScannerModule) PostScan(_ types.Results) (types.Results, error) { + return nil, nil +} diff --git a/pkg/module/wasm/sdk.go b/pkg/module/wasm/sdk.go new file mode 100644 index 000000000000..d6e6cdb6c3c2 --- /dev/null +++ b/pkg/module/wasm/sdk.go @@ -0,0 +1,204 @@ +//go:build wasip1 + +package wasm + +import ( + "encoding/json" + "fmt" + "unsafe" + + "github.com/aquasecurity/trivy/pkg/module/api" + "github.com/aquasecurity/trivy/pkg/types" +) + +// allocations holds byte slices keyed by their 32-bit pointers (offsets in WASM memory). +// This map ensures that the allocated slices are not garbage-collected as long as we need them. +var allocations = make(map[uint32][]byte) + +// allocate creates a byte slice on the Go heap, which resides in WASM linear memory when compiled for WASI. +// It returns a 32-bit pointer (offset) that can be used to access this memory. +func allocate(size uint32) uint32 { + if size == 0 { + return 0 + } + buf := make([]byte, size) + ptr := uint32(uintptr(unsafe.Pointer(&buf[0]))) + allocations[ptr] = buf + return ptr +} + +// malloc exposes a C-style malloc to the host. +// It returns an offset in WASM linear memory where the requested size is allocated. +// +//go:wasmexport malloc +func _malloc(size uint32) uint32 { + return allocate(size) +} + +// free exposes a C-style free to the host. +// It deletes the slice from the allocations map so the memory can be reclaimed by the GC. +// +//go:wasmexport free +func _free(ptr uint32, size uint32) { + delete(allocations, ptr) +} + +// Debug, Info, Warn, Error functions ----------------------------------------- + +func Debug(message string) { + message = fmt.Sprintf("Module %s: %s", module.Name(), message) + ptr, size := stringToPtr(message) + _debug(ptr, size) +} + +func Info(message string) { + message = fmt.Sprintf("Module %s: %s", module.Name(), message) + ptr, size := stringToPtr(message) + _info(ptr, size) +} + +func Warn(message string) { + message = fmt.Sprintf("Module %s: %s", module.Name(), message) + ptr, size := stringToPtr(message) + _warn(ptr, size) +} + +func Error(message string) { + message = fmt.Sprintf("Module %s: %s", module.Name(), message) + ptr, size := stringToPtr(message) + _error(ptr, size) +} + +// Imported host functions --------------------------------------------------- + +//go:wasmimport env debug +func _debug(ptr uint32, size uint32) + +//go:wasmimport env info +func _info(ptr uint32, size uint32) + +//go:wasmimport env warn +func _warn(ptr uint32, size uint32) + +//go:wasmimport env error +func _error(ptr uint32, size uint32) + +var module api.Module + +func RegisterModule(p api.Module) { + module = p +} + +// Exported functions -------------------------------------------------------- + +//go:wasmexport name +func _name() uint64 { + name := module.Name() + ptr, size := stringToPtr(name) + return (uint64(ptr) << 32) | uint64(size) +} + +//go:wasmexport api_version +func _apiVersion() uint32 { + return api.Version +} + +//go:wasmexport version +func _version() uint32 { + return uint32(module.Version()) +} + +//go:wasmexport is_analyzer +func _isAnalyzer() uint64 { + if _, ok := module.(api.Analyzer); !ok { + return 0 + } + return 1 +} + +//go:wasmexport required +func _required() uint64 { + files := module.(api.Analyzer).RequiredFiles() + return marshal(files) +} + +//go:wasmexport analyze +func _analyze(ptr, size uint32) uint64 { + filePath := ptrToString(ptr, size) + custom, err := module.(api.Analyzer).Analyze(filePath) + if err != nil { + Error(fmt.Sprintf("analyze error: %s", err)) + return 0 + } + return marshal(custom) +} + +//go:wasmexport is_post_scanner +func _isPostScanner() uint64 { + if _, ok := module.(api.PostScanner); !ok { + return 0 + } + return 1 +} + +//go:wasmexport post_scan_spec +func _post_scan_spec() uint64 { + return marshal(module.(api.PostScanner).PostScanSpec()) +} + +//go:wasmexport post_scan +func _post_scan(ptr, size uint32) uint64 { + var results types.Results + if err := unmarshal(ptr, size, &results); err != nil { + Error(fmt.Sprintf("post scan error: %s", err)) + return 0 + } + + results, err := module.(api.PostScanner).PostScan(results) + if err != nil { + Error(fmt.Sprintf("post scan error: %s", err)) + return 0 + } + return marshal(results) +} + +// marshal converts the given value to JSON and allocates memory for it in WASM, +// returning a 64-bit packed pointer and size (high 32 bits = pointer, low 32 bits = length). +func marshal(v any) uint64 { + b, err := json.Marshal(v) + if err != nil { + Error(fmt.Sprintf("marshal error: %s", err)) + return 0 + } + // Allocate space in WASM for the JSON-encoded data + ptr := allocate(uint32(len(b))) + // Copy the JSON bytes into the allocated slice + copy(allocations[ptr], b) + + // Pack the pointer and length into a single uint64 + return (uint64(ptr) << 32) | uint64(len(b)) +} + +// unmarshal reads the data from WASM memory and unmarshals JSON into v. +func unmarshal(ptr, size uint32, v any) error { + s := ptrToString(ptr, size) + if err := json.Unmarshal([]byte(s), v); err != nil { + return fmt.Errorf("unmarshal error: %s", err) + } + return nil +} + +// ptrToString constructs a Go string from a pointer and size in WASM memory. +// This uses unsafe.Slice to wrap the memory, then builds a string without an extra copy. +func ptrToString(ptr uint32, size uint32) string { + b := unsafe.Slice((*byte)(unsafe.Pointer(uintptr(ptr))), size) + return *(*string)(unsafe.Pointer(&b)) +} + +// stringToPtr converts a Go string into a pointer and size so that we can return +// them as numeric values in WASM-compatible form. +func stringToPtr(s string) (uint32, uint32) { + buf := []byte(s) + p := uintptr(unsafe.Pointer(&buf[0])) + return uint32(p), uint32(len(buf)) +} diff --git a/pkg/notification/identifier.go b/pkg/notification/identifier.go new file mode 100644 index 000000000000..d91bac1ea8a5 --- /dev/null +++ b/pkg/notification/identifier.go @@ -0,0 +1,47 @@ +package notification + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "net" + "os" + "strings" +) + +func getMachineIdentifier() (string, error) { + hostname, err := os.Hostname() + if err != nil { + return "", err + } + interfaces, err := net.Interfaces() + if err != nil { + return "", err + } + + var macAddr string + for _, iface := range interfaces { + if iface.HardwareAddr.String() != "" { + macAddr = iface.HardwareAddr.String() + break + } + } + identifier := fmt.Sprintf("%s-%s-%s", hostname, macAddr, strings.ToLower(hostname)) + + return identifier, nil +} + +func generateMachineHash(identifier string) string { + hash := sha256.New() + hash.Write([]byte(identifier)) + return hex.EncodeToString(hash.Sum(nil)) +} + +func uniqueIdentifier() string { + identifier, err := getMachineIdentifier() + if err != nil { + return "" + } + + return generateMachineHash(fmt.Sprintf("trivy-%s", identifier)) +} diff --git a/pkg/notification/identifier_test.go b/pkg/notification/identifier_test.go new file mode 100644 index 000000000000..ac71c70917b7 --- /dev/null +++ b/pkg/notification/identifier_test.go @@ -0,0 +1,28 @@ +package notification + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateMachineHash(t *testing.T) { + // Test with known input + identifier := "test-identifier" + hash := generateMachineHash(identifier) + + // Known hash for "test-identifier" + expectedHash := "115ae872eb1d3e23f9de03f7ab344193b21068812ee52eb37e8169e6d093c7ae" + assert.Equal(t, expectedHash, hash) +} + +// This test requires some modification to the original code to make it testable +// by injecting mocked network interfaces +func TestGetMachineIdentifier(t *testing.T) { + // This is a basic test that at least ensures the function returns without error + // A more complete test would mock os.Hostname and net.Interfaces + identifier, err := getMachineIdentifier() + require.NoError(t, err) + require.NotEmpty(t, identifier) +} diff --git a/pkg/notification/notice.go b/pkg/notification/notice.go new file mode 100644 index 000000000000..b8a71077f0c4 --- /dev/null +++ b/pkg/notification/notice.go @@ -0,0 +1,245 @@ +package notification + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "runtime" + "strconv" + "strings" + "time" + + "github.com/samber/lo" + + "github.com/aquasecurity/go-version/pkg/semver" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/version/app" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" +) + +type VersionChecker struct { + updatesApi string + commandName string + cliOptions *flag.Options + + done bool + responseReceived bool + currentVersion string + latestVersion updateResponse +} + +// NewVersionChecker creates a new VersionChecker with the default +// updates API URL. +func NewVersionChecker(commandName string, cliOptions *flag.Options) *VersionChecker { + v := &VersionChecker{ + updatesApi: "https://check.trivy.dev/updates", + currentVersion: app.Version(), + commandName: commandName, + cliOptions: cliOptions, + } + + return v +} + +// RunUpdateCheck makes a best efforts request to determine the +// latest version and any announcements +// Logic: +// 1. if skipUpdateCheck is true AND telemetryDisabled are both true, skip the request +// 2. if skipUpdateCheck is true AND telemetryDisabled is false, run check with metric details but suppress output +// 3. if skipUpdateCheck is false AND telemetryDisabled is true, run update check but don't send any metric identifiers +func (v *VersionChecker) RunUpdateCheck(ctx context.Context) { + logger := log.WithPrefix("notification") + + if v.cliOptions.SkipVersionCheck && v.cliOptions.DisableTelemetry { + logger.Debug("Skipping update check and metric ping") + return + } + + go func() { + logger.Debug("Running version check") + commandParts := v.getFlags() + client := xhttp.ClientWithContext(ctx, xhttp.WithTimeout(3*time.Second)) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, v.updatesApi, http.NoBody) + if err != nil { + logger.Warn("Failed to create a request for Trivy api", log.Err(err)) + return + } + + // if the user hasn't disabled metrics, send the anonymous information as headers + if !v.cliOptions.DisableTelemetry { + req.Header.Set("Trivy-Identifier", uniqueIdentifier()) + req.Header.Set("Trivy-Command", v.commandName) + req.Header.Set("Trivy-Flags", commandParts) + req.Header.Set("Trivy-OS", runtime.GOOS) + req.Header.Set("Trivy-Arch", runtime.GOARCH) + } + + req.Header.Set("User-Agent", fmt.Sprintf("trivy/%s", v.currentVersion)) + resp, err := client.Do(req) + if err != nil { + if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { + logger.Debug("Failed getting response from Trivy api", log.Err(err)) + } + return + } else if resp.StatusCode != http.StatusOK { + logger.Debug("Unexpected status code from Trivy api", log.Int("status_code", resp.StatusCode)) + return + } + + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(&v.latestVersion); err != nil { + logger.Debug("Failed to decode the Trivy response", log.Err(err)) + return + } + + // enable priting if update allowed and quiet mode is not set + if !v.cliOptions.SkipVersionCheck && !v.cliOptions.Quiet { + v.responseReceived = true + } + logger.Debug("Version check completed", log.String("latest_version", v.latestVersion.Trivy.LatestVersion)) + v.done = true + }() +} + +// PrintNotices prints any announcements or warnings +// to the output writer, most likely stderr +func (v *VersionChecker) PrintNotices(ctx context.Context, output io.Writer) { + if !v.responseReceived { + return + } + + logger := log.WithPrefix("notification") + var notices []string + + cv, err := v.CurrentVersion() + if err != nil { + return + } + + lv, err := v.LatestVersion() + if err != nil { + return + } + + notices = append(notices, v.Warnings()...) + for _, announcement := range v.Announcements() { + if announcement.shouldDisplay(ctx, cv) { + notices = append(notices, announcement.Announcement) + } + } + + if cv.LessThan(lv) { + notices = append(notices, fmt.Sprintf("Version %s of Trivy is now available, current version is %s", lv, cv)) + } + + if len(notices) > 0 { + logger.Debug("Printing notices") + fmt.Fprintf(output, "\n📣 \x1b[34mNotices:\x1b[0m\n") + for _, notice := range notices { + fmt.Fprintf(output, " - %s\n", notice) + } + fmt.Fprintln(output) + fmt.Fprintln(output, "To suppress version checks, run Trivy scans with the --skip-version-check flag") + fmt.Fprintln(output) + } +} + +func (v *VersionChecker) CurrentVersion() (semver.Version, error) { + current, err := semver.Parse(strings.TrimPrefix(v.currentVersion, "v")) + if err != nil { + return semver.Version{}, fmt.Errorf("failed to parse current version: %w", err) + } + return current, nil +} + +func (v *VersionChecker) LatestVersion() (semver.Version, error) { + if v.responseReceived { + latest, err := semver.Parse(strings.TrimPrefix(v.latestVersion.Trivy.LatestVersion, "v")) + if err != nil { + return semver.Version{}, fmt.Errorf("failed to parse latest version: %w", err) + } + return latest, nil + } + return semver.Version{}, errors.New("no response received from version check") +} + +func (v *VersionChecker) Announcements() []announcement { + if v.responseReceived { + return v.latestVersion.Announcements + } + return nil +} + +func (v *VersionChecker) Warnings() []string { + if v.responseReceived { + return v.latestVersion.Warnings + } + return nil +} + +func (fd *flexibleTime) UnmarshalJSON(b []byte) error { + s := strings.Trim(string(b), `"`) + if s == "" { + return nil + } + + // Try parsing with time component + layouts := []string{ + time.RFC3339, + "2006-01-02", + time.RFC1123, + } + + var err error + for _, layout := range layouts { + var t time.Time + t, err = time.Parse(layout, s) + if err == nil { + fd.Time = t + return nil + } + } + + return fmt.Errorf("unable to parse date: %s", s) +} + +func (v *VersionChecker) getFlags() string { + var flags []string + for _, f := range v.cliOptions.GetUsedFlags() { + name := f.GetName() + if name == "" { + continue // Skip flags without a name + } + value := lo.Ternary(!f.IsTelemetrySafe(), "***", getFlagValue(f)) + + flags = append(flags, fmt.Sprintf("--%s=%s", name, value)) + } + return strings.Join(flags, " ") +} + +func getFlagValue(f flag.Flagger) string { + type flagger[T flag.FlagType] interface { + Value() T + } + switch ff := f.(type) { + case flagger[string]: + return ff.Value() + case flagger[int]: + return strconv.Itoa(ff.Value()) + case flagger[float64]: + return fmt.Sprintf("%f", ff.Value()) + case flagger[bool]: + return strconv.FormatBool(ff.Value()) + case flagger[time.Duration]: + return ff.Value().String() + case flagger[[]string]: + return strings.Join(ff.Value(), ",") + default: + return "***" // Default case for unsupported types + } +} diff --git a/pkg/notification/notice_test.go b/pkg/notification/notice_test.go new file mode 100644 index 000000000000..1711348a4775 --- /dev/null +++ b/pkg/notification/notice_test.go @@ -0,0 +1,479 @@ +package notification + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/flag" +) + +func TestPrintNotices(t *testing.T) { + tests := []struct { + name string + skipVersionCheck bool + quiet bool + disableTelemetry bool + + currentVersion string + latestVersion string + announcements []announcement + responseExpected bool + expectedOutput string + }{ + { + name: "New version with no announcements", + currentVersion: "0.58.0", + latestVersion: "0.60.0", + responseExpected: true, + expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - Version 0.60.0 of Trivy is now available, current version is 0.58.0\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n", + }, + { + name: "New version available but includes a prefixed version number", + currentVersion: "0.58.0", + latestVersion: "v0.60.0", + responseExpected: true, + expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - Version 0.60.0 of Trivy is now available, current version is 0.58.0\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n", + }, + { + name: "new version available but --quiet mode enabled", + quiet: true, + currentVersion: "0.58.0", + latestVersion: "0.60.0", + responseExpected: false, + expectedOutput: "", + }, + { + name: "new version available but --skip-version-check mode enabled", + skipVersionCheck: true, + currentVersion: "0.58.0", + latestVersion: "0.60.0", + responseExpected: false, + expectedOutput: "", + }, + { + name: "New version with announcements", + currentVersion: "0.58.0", + latestVersion: "0.60.0", + announcements: []announcement{ + { + FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC), + ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC), + Announcement: "There are some amazing things happening right now!", + }, + }, + responseExpected: true, + expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - There are some amazing things happening right now!\n - Version 0.60.0 of Trivy is now available, current version is 0.58.0\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n", + }, + { + name: "No new version with announcements", + currentVersion: "0.60.0", + latestVersion: "0.60.0", + announcements: []announcement{ + { + FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC), + ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC), + Announcement: "There are some amazing things happening right now!", + }, + }, + responseExpected: true, + expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - There are some amazing things happening right now!\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n", + }, + { + name: "No new version with announcements and zero time", + currentVersion: "0.60.0", + latestVersion: "0.60.0", + announcements: []announcement{ + { + FromDate: time.Time{}, + ToDate: time.Time{}, + Announcement: "There are some amazing things happening right now!", + }, + }, + responseExpected: true, + expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - There are some amazing things happening right now!\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n", + }, + { + name: "No new version with announcement that fails announcement version constraints", + currentVersion: "0.60.0", + latestVersion: "0.60.0", + announcements: []announcement{ + { + FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC), + ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC), + FromVersion: "0.61.0", + Announcement: "There are some amazing things happening right now!", + }, + }, + responseExpected: true, + expectedOutput: "", + }, + { + name: "No new version with announcement where current version is greater than to_version", + currentVersion: "0.60.0", + latestVersion: "0.60.0", + announcements: []announcement{ + { + FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC), + ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC), + ToVersion: "0.59.0", + Announcement: "There are some amazing things happening right now!", + }, + }, + responseExpected: true, + expectedOutput: "", + }, + { + name: "No new version with announcement that satisfies version constraint but outside date range", + currentVersion: "0.60.0", + latestVersion: "0.60.0", + announcements: []announcement{ + { + FromDate: time.Date(2024, 2, 2, 12, 0, 0, 0, time.UTC), + ToDate: time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC), + FromVersion: "0.60.0", + Announcement: "There are some amazing things happening right now!", + }, + }, + responseExpected: true, + expectedOutput: "", + }, + { + name: "No new version with multiple announcements, one of which is valid", + currentVersion: "0.60.0", + latestVersion: "0.60.0", + announcements: []announcement{ + { + FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC), + ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC), + Announcement: "There are some amazing things happening right now!", + }, + { + FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC), + ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC), + FromVersion: "0.61.0", + Announcement: "This announcement should not be displayed", + }, + }, + responseExpected: true, + expectedOutput: "\n📣 \x1b[34mNotices:\x1b[0m\n - There are some amazing things happening right now!\n\nTo suppress version checks, run Trivy scans with the --skip-version-check flag\n\n", + }, + { + name: "No new version with no announcements and quiet mode", + quiet: true, + currentVersion: "0.60.0", + latestVersion: "0.60.0", + announcements: []announcement{}, + responseExpected: false, + expectedOutput: "", + }, + { + name: "No new version with no announcements", + currentVersion: "0.60.0", + latestVersion: "0.60.0", + announcements: []announcement{}, + responseExpected: true, + expectedOutput: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + updates := newUpdatesServer(t, tt.latestVersion, tt.announcements) + server := httptest.NewServer(http.HandlerFunc(updates.handler)) + + cliOpts := &flag.Options{ + GlobalOptions: flag.GlobalOptions{ + Quiet: tt.quiet, + }, + ScanOptions: flag.ScanOptions{ + SkipVersionCheck: tt.skipVersionCheck, + DisableTelemetry: tt.disableTelemetry, + }, + } + + v := NewVersionChecker("testCommand", cliOpts) + v.updatesApi = server.URL + v.currentVersion = tt.currentVersion + + v.RunUpdateCheck(t.Context()) + require.Eventually(t, func() bool { return v.done }, time.Second*5, 500) + require.Eventually(t, func() bool { return v.responseReceived == tt.responseExpected }, time.Second*5, 500) + + sb := bytes.NewBufferString("") + v.PrintNotices(t.Context(), sb) + assert.Equal(t, tt.expectedOutput, sb.String()) + + // check metrics are sent + require.NotNil(t, updates.lastRequest) + require.NotEmpty(t, updates.lastRequest.Header.Get("Trivy-Identifier")) + }) + } +} + +func TestCheckForNotices(t *testing.T) { + tests := []struct { + name string + skipVersionCheck bool + disableTelemetry bool + quiet bool + currentVersion string + expectedVersion string + expectedAnnouncements []announcement + expectNoMetrics bool + }{ + { + name: "new version with no announcements", + currentVersion: "0.58.0", + expectedVersion: "0.60.0", + }, + { + name: "new version with disabled metrics", + disableTelemetry: true, + currentVersion: "0.58.0", + expectedVersion: "0.60.0", + expectNoMetrics: true, + }, + { + name: "new version and a new announcement", + currentVersion: "0.58.0", + expectedVersion: "0.60.0", + expectedAnnouncements: []announcement{ + { + FromDate: time.Date(2025, 2, 2, 12, 0, 0, 0, time.UTC), + ToDate: time.Date(2999, 1, 1, 0, 0, 0, 0, time.UTC), + Announcement: "There are some amazing things happening right now!", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + updates := newUpdatesServer(t, tt.expectedVersion, tt.expectedAnnouncements) + server := httptest.NewServer(http.HandlerFunc(updates.handler)) + defer server.Close() + + cliOpts := &flag.Options{ + GlobalOptions: flag.GlobalOptions{ + Quiet: tt.quiet, + }, + ScanOptions: flag.ScanOptions{ + SkipVersionCheck: tt.skipVersionCheck, + DisableTelemetry: tt.disableTelemetry, + }, + } + + v := NewVersionChecker("testCommand", cliOpts) + v.updatesApi = server.URL + + v.RunUpdateCheck(t.Context()) + require.Eventually(t, func() bool { return v.done }, time.Second*5, 500) + require.Eventually(t, func() bool { return v.responseReceived }, time.Second*5, 500) + latestVersion, err := v.LatestVersion() + require.NoError(t, err) + assert.Equal(t, tt.expectedVersion, latestVersion.String()) + assert.ElementsMatch(t, tt.expectedAnnouncements, v.Announcements()) + + if tt.expectNoMetrics { + require.NotNil(t, updates.lastRequest) + assert.Empty(t, updates.lastRequest.Header.Get("Trivy-Identifier")) + } else { + require.NotNil(t, updates.lastRequest) + assert.NotEmpty(t, updates.lastRequest.Header.Get("Trivy-Identifier")) + } + + }) + } +} + +type updatesServer struct { + t *testing.T + lastRequest *http.Request + expectedVersion string + expectedAnnouncements []announcement +} + +func newUpdatesServer(t *testing.T, expectedVersion string, expectedAnnouncements []announcement) *updatesServer { + return &updatesServer{ + t: t, + expectedVersion: expectedVersion, + expectedAnnouncements: expectedAnnouncements, + } +} + +func (u *updatesServer) handler(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.Header.Get("User-Agent"), "trivy") { + w.WriteHeader(http.StatusForbidden) + } + + u.lastRequest = r + + response := updateResponse{ + Trivy: versionInfo{ + LatestVersion: u.expectedVersion, + LatestDate: flexibleTime{Time: time.Now()}, + }, + Announcements: u.expectedAnnouncements, + } + + out, err := json.Marshal(response) + if err != nil { + u.t.Fail() + } + w.Write(out) +} + +func TestFlexibleDate(t *testing.T) { + tests := []struct { + name string + dateStr string + expected time.Time + }{ + { + name: "RFC3339 date format", + dateStr: `"2023-10-01T12:00:00Z"`, + expected: time.Date(2023, 10, 1, 12, 0, 0, 0, time.UTC), + }, + { + name: "RFC1123 date format", + dateStr: `"Sun, 01 Oct 2023 12:00:00 GMT"`, + expected: time.Date(2023, 10, 1, 12, 0, 0, 0, time.UTC), + }, + { + name: "RFC3339 date only format", + dateStr: `"2023-10-01"`, + expected: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ft flexibleTime + err := json.Unmarshal([]byte(tt.dateStr), &ft) + require.NoError(t, err) + assert.Equal(t, tt.expected.Unix(), ft.Unix()) + }) + } +} + +func TestCheckCommandHeaders(t *testing.T) { + tests := []struct { + name string + command string + commandArgs []string + env map[string]string + ignoreParseError bool + expectedCommandHeader string + expectedCommandArgsHeader string + }{ + { + name: "image command with no flags", + command: "image", + commandArgs: []string{"nginx"}, + expectedCommandHeader: "image", + }, + { + name: "image command with flags", + command: "image", + commandArgs: []string{"--severity", "CRITICAL", "--scanners", "vuln,misconfig", "--pkg-types", "library", "nginx", "--include-dev-deps"}, + expectedCommandHeader: "image", + expectedCommandArgsHeader: "--include-dev-deps=true --pkg-types=library --severity=CRITICAL --scanners=vuln,misconfig", + }, + { + name: "image command with multiple flags", + command: "image", + commandArgs: []string{"--severity", "MEDIUM", "-s", "CRITICAL", "--scanners", "misconfig", "nginx"}, + expectedCommandHeader: "image", + expectedCommandArgsHeader: "--severity=MEDIUM,CRITICAL --scanners=misconfig", + }, + { + name: "filesystem command with flags", + command: "fs", + commandArgs: []string{"--severity=HIGH", "--vex", "repo", "--vuln-severity-source", "nvd,debian", "../trivy-ci-test"}, + expectedCommandHeader: "fs", + expectedCommandArgsHeader: "--severity=HIGH --vex=*** --vuln-severity-source=nvd,debian", + }, + { + name: "filesystem command with flags including an invalid flag", + command: "fs", + commandArgs: []string{"--severity=HIGH", "--vex", "repo", "--vuln-severity-source", "nvd,debian", "--invalid-flag", "../trivy-ci-test"}, + ignoreParseError: true, + expectedCommandHeader: "fs", + expectedCommandArgsHeader: "--severity=HIGH --vex=*** --vuln-severity-source=nvd,debian", + }, + { + name: "filesystem with environment variables", + command: "fs", + commandArgs: []string{"--severity", "HIGH", "--vex", "repo", "/home/user/code"}, + env: map[string]string{ + "TRIVY_SCANNERS": "secret,misconfig", + }, + expectedCommandHeader: "fs", + expectedCommandArgsHeader: "--severity=HIGH --scanners=secret,misconfig --vex=***", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + updates := newUpdatesServer(t, "0.60.0", nil) + server := httptest.NewServer(http.HandlerFunc(updates.handler)) + defer server.Close() + + for key, value := range tt.env { + t.Setenv(key, value) + } + + // clean up the env + defer func() { + server.Close() + }() + + opts := getOptionsForArgs(t, tt.commandArgs, tt.ignoreParseError) + + v := NewVersionChecker(tt.command, opts) + v.updatesApi = server.URL + v.RunUpdateCheck(t.Context()) + + require.Eventually(t, func() bool { return v.done }, time.Second*5, 500) + require.NotNil(t, updates.lastRequest) + assert.Equal(t, tt.expectedCommandHeader, updates.lastRequest.Header.Get("Trivy-Command")) + assert.Equal(t, tt.expectedCommandArgsHeader, updates.lastRequest.Header.Get("Trivy-Flags")) + }) + } +} + +// getOptionsForArgs uses a basic command to parse the flags so we can generate +// an options object from it +func getOptionsForArgs(t *testing.T, commandArgs []string, ignoreParseError bool) *flag.Options { + flags := flag.Flags{ + flag.NewGlobalFlagGroup(), + flag.NewImageFlagGroup(), + flag.NewMisconfFlagGroup(), + flag.NewPackageFlagGroup(), + flag.NewReportFlagGroup(), + flag.NewScanFlagGroup(), + flag.NewVulnerabilityFlagGroup(), + } + + // simple command to facilitate flag parsing + cmd := &cobra.Command{} + flags.AddFlags(cmd) + err := cmd.ParseFlags(commandArgs) + if !ignoreParseError { + require.NoError(t, err) + } + + require.NoError(t, flags.Bind(cmd)) + opts, err := flags.ToOptions(commandArgs) + require.NoError(t, err) + return &opts +} diff --git a/pkg/notification/response.go b/pkg/notification/response.go new file mode 100644 index 000000000000..fcd49d51fd62 --- /dev/null +++ b/pkg/notification/response.go @@ -0,0 +1,58 @@ +package notification + +import ( + "context" + "time" + + "github.com/aquasecurity/go-version/pkg/semver" + "github.com/aquasecurity/trivy/pkg/clock" +) + +// flexibleTime is a custom time type that can handle +// different date formats in JSON. It implements the +// UnmarshalJSON method to parse the date string into a time.Time object. +type flexibleTime struct { + time.Time +} + +type versionInfo struct { + LatestVersion string `json:"latest_version"` + LatestDate flexibleTime `json:"latest_date"` +} + +type announcement struct { + FromDate time.Time `json:"from_date"` + ToDate time.Time `json:"to_date"` + FromVersion string `json:"from_version"` + ToVersion string `json:"to_version"` + Announcement string `json:"announcement"` +} + +type updateResponse struct { + Trivy versionInfo `json:"trivy"` + Announcements []announcement `json:"announcements"` + Warnings []string `json:"warnings"` +} + +// shouldDisplay checks if the announcement should be displayed +// based on the current time and version. If version and date constraints are provided +// they are checked against the current time and version. +func (a *announcement) shouldDisplay(ctx context.Context, currentVersion semver.Version) bool { + if !a.FromDate.IsZero() && clock.Now(ctx).Before(a.FromDate) { + return false + } + if !a.ToDate.IsZero() && clock.Now(ctx).After(a.ToDate) { + return false + } + if a.FromVersion != "" { + if fromVersion, err := semver.Parse(a.FromVersion); err == nil && currentVersion.LessThan(fromVersion) { + return false + } + } + if a.ToVersion != "" { + if toVersion, err := semver.Parse(a.ToVersion); err == nil && currentVersion.GreaterThanOrEqual(toVersion) { + return false + } + } + return true +} diff --git a/pkg/notification/response_test.go b/pkg/notification/response_test.go new file mode 100644 index 000000000000..461bb331586b --- /dev/null +++ b/pkg/notification/response_test.go @@ -0,0 +1,180 @@ +package notification + +import ( + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/go-version/pkg/semver" + "github.com/aquasecurity/trivy/pkg/clock" +) + +func TestAnnouncementShouldDisplay(t *testing.T) { + tests := []struct { + name string + announcement announcement + now time.Time + currentVersion string + expected bool + }{ + { + name: "Announcement with valid from_date and current date before it", + announcement: announcement{ + FromDate: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), + Announcement: "Upcoming feature", + }, + now: time.Date(2023, 9, 30, 0, 0, 0, 0, time.UTC), + currentVersion: "1.0.0", + expected: false, + }, + { + name: "Announcement with valid to_date and current date after it", + announcement: announcement{ + ToDate: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), + Announcement: "Past feature", + }, + now: time.Date(2023, 10, 2, 0, 0, 0, 0, time.UTC), + currentVersion: "1.0.0", + expected: false, + }, + { + name: "Announcement with valid from_date and current date after it and before to_date", + announcement: announcement{ + FromDate: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), + ToDate: time.Date(2023, 10, 31, 0, 0, 0, 0, time.UTC), + Announcement: "Ongoing feature", + }, + now: time.Date(2023, 10, 15, 0, 0, 0, 0, time.UTC), + currentVersion: "1.0.0", + expected: true, + }, + { + name: "Announcement with valid from_version and current version greater than it", + announcement: announcement{ + FromVersion: "1.1.0", + Announcement: "New feature", + }, + now: time.Now(), + currentVersion: "1.2.0", + expected: true, + }, + { + name: "Announcement with valid from_version and current version equal to it", + announcement: announcement{ + FromVersion: "1.0.0", + Announcement: "New feature", + }, + now: time.Now(), + currentVersion: "1.0.0", + expected: true, + }, + { + name: "Announcement with valid to_version and current version less than it", + announcement: announcement{ + ToVersion: "1.2.0", + Announcement: "Upcoming feature", + }, + now: time.Now(), + currentVersion: "1.0.0", + expected: true, + }, + { + name: "Announcement with valid to_version and current version equal to it", + announcement: announcement{ + ToVersion: "1.0.0", + Announcement: "Upcoming feature", + }, + now: time.Now(), + currentVersion: "1.0.0", + expected: false, + }, + { + name: "Announcement with valid from_version and valid to_version", + announcement: announcement{ + FromVersion: "1.0.0", + ToVersion: "1.2.0", + Announcement: "Feature announcement", + }, + now: time.Date(2023, 10, 15, 0, 0, 0, 0, time.UTC), + currentVersion: "1.1.0", + expected: true, + }, + { + name: "Announcement with no date or version constraints", + announcement: announcement{ + Announcement: "General announcement", + }, + now: time.Now(), + currentVersion: "1.0.0", + expected: true, + }, + { + name: "Announcement with all constraints but current version meets them", + announcement: announcement{ + FromDate: time.Date(2023, 10, 1, 0, 0, 0, 0, time.UTC), + ToDate: time.Date(2023, 10, 31, 0, 0, 0, 0, time.UTC), + FromVersion: "1.0.0", + ToVersion: "1.2.0", + Announcement: "Feature announcement", + }, + now: time.Date(2023, 10, 15, 0, 0, 0, 0, time.UTC), + currentVersion: "1.1.0", + expected: true, + }, + { + name: "Announcement with version having 'v' prefix", + announcement: announcement{ + FromVersion: "v1.0.0", + Announcement: "Version prefix handling", + }, + now: time.Now(), + currentVersion: "1.0.0", + expected: true, + }, + { + name: "Current version with 'v' prefix", + announcement: announcement{ + FromVersion: "1.0.0", + Announcement: "Version prefix handling", + }, + now: time.Now(), + currentVersion: "v1.0.0", + expected: true, + }, + { + name: "Pre-release version comparison", + announcement: announcement{ + FromVersion: "1.0.0", + ToVersion: "1.2.0", + Announcement: "Pre-release handling", + }, + now: time.Now(), + currentVersion: "1.1.0-beta.1", + expected: true, + }, + { + name: "Build metadata in version", + announcement: announcement{ + FromVersion: "1.0.0", + Announcement: "Build metadata handling", + }, + now: time.Now(), + currentVersion: "1.0.0+build.1", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + currentVersion, err := semver.Parse(strings.TrimPrefix(tt.currentVersion, "v")) + require.NoError(t, err) + + fakeCtx := clock.With(t.Context(), tt.now) + got := tt.announcement.shouldDisplay(fakeCtx, currentVersion) + assert.Equal(t, tt.expected, got) + }) + } +} diff --git a/pkg/oci/artifact.go b/pkg/oci/artifact.go new file mode 100644 index 000000000000..f021f9be2227 --- /dev/null +++ b/pkg/oci/artifact.go @@ -0,0 +1,299 @@ +package oci + +import ( + "context" + "errors" + "io" + "os" + "path/filepath" + "sync" + + "github.com/cheggaaa/pb/v3" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote/transport" + "github.com/hashicorp/go-multierror" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/downloader" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/remote" + "github.com/aquasecurity/trivy/pkg/version/doc" + xio "github.com/aquasecurity/trivy/pkg/x/io" + xos "github.com/aquasecurity/trivy/pkg/x/os" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +const ( + // Artifact types + CycloneDXArtifactType = "application/vnd.cyclonedx+json" + SPDXArtifactType = "application/spdx+json" + + // Sigstore bundle artifact type for Cosign attestations in "new bundle format" > 2.5.0 + SigstoreBundleArtifactType = "application/vnd.dev.sigstore.bundle.v0.3+json" + + // Media types + OCIImageManifest = "application/vnd.oci.image.manifest.v1+json" + + // Annotations + titleAnnotation = "org.opencontainers.image.title" +) + +var SupportedSBOMArtifactTypes = []string{ + CycloneDXArtifactType, + SPDXArtifactType, + SigstoreBundleArtifactType, +} + +// Option is a functional option +type Option func(*Artifact) + +// WithImage takes an OCI v1 Image +func WithImage(img v1.Image) Option { + return func(a *Artifact) { + a.image = img + } +} + +// Artifact is used to download artifacts such as vulnerability database and policies from OCI registries. +type Artifact struct { + m sync.Mutex + repository string + + // For OCI registries + types.RegistryOptions + + image v1.Image // For testing +} + +// NewArtifact returns a new artifact +func NewArtifact(repo string, registryOpt types.RegistryOptions, opts ...Option) *Artifact { + art := &Artifact{ + repository: repo, + RegistryOptions: registryOpt, + } + + for _, o := range opts { + o(art) + } + return art +} + +func (a *Artifact) populate(ctx context.Context, opt types.RegistryOptions) error { + if a.image != nil { + return nil + } + + a.m.Lock() + defer a.m.Unlock() + + var nameOpts []name.Option + if opt.Insecure { + nameOpts = append(nameOpts, name.Insecure) + } + + ref, err := name.ParseReference(a.repository, nameOpts...) + if err != nil { + return xerrors.Errorf("repository name error (%s): %w", a.repository, err) + } + + a.image, err = remote.Image(ctx, ref, opt) + if err != nil { + return xerrors.Errorf("OCI repository error: %w", err) + } + return nil +} + +type DownloadOption struct { + MediaType string // Accept any media type if not specified + Filename string // Use the annotation if not specified + Quiet bool +} + +func (a *Artifact) Download(ctx context.Context, dir string, opt DownloadOption) error { + if err := a.populate(ctx, a.RegistryOptions); err != nil { + return err + } + + layers, err := a.image.Layers() + if err != nil { + return xerrors.Errorf("OCI layer error: %w", err) + } + + manifest, err := a.image.Manifest() + if err != nil { + return xerrors.Errorf("OCI manifest error: %w", err) + } + + // A single layer is only supported now. + if len(layers) != 1 || len(manifest.Layers) != 1 { + return xerrors.Errorf("OCI artifact must be a single layer") + } + + // Take the first layer + layer := layers[0] + + // Take the file name of the first layer if not specified + fileName := opt.Filename + if fileName == "" { + v, ok := manifest.Layers[0].Annotations[titleAnnotation] + if !ok { + return xerrors.Errorf("annotation %s is missing", titleAnnotation) + } + fileName = v + } + + layerMediaType, err := layer.MediaType() + if err != nil { + return xerrors.Errorf("media type error: %w", err) + } else if opt.MediaType != "" && opt.MediaType != string(layerMediaType) { + return xerrors.Errorf("unacceptable media type: %s", string(layerMediaType)) + } + + if err = a.download(ctx, layer, fileName, dir, opt.Quiet); err != nil { + return xerrors.Errorf("oci download error: %w", err) + } + + return nil +} + +func (a *Artifact) download(ctx context.Context, layer v1.Layer, fileName, dir string, quiet bool) error { + size, err := layer.Size() + if err != nil { + return xerrors.Errorf("size error: %w", err) + } + + rc, err := layer.Compressed() + if err != nil { + return xerrors.Errorf("failed to fetch the layer: %w", err) + } + defer rc.Close() + + // Show progress bar + bar := pb.Full.Start64(size) + if quiet { + bar.SetWriter(io.Discard) + } + pr := bar.NewProxyReader(rc) + defer bar.Finish() + + // https://github.com/hashicorp/go-getter/issues/326 + tempDir, err := xos.MkdirTemp("", "oci-download-") + if err != nil { + return xerrors.Errorf("failed to create a temp dir: %w", err) + } + + f, err := os.Create(filepath.Join(tempDir, fileName)) + if err != nil { + return xerrors.Errorf("failed to create a temp file: %w", err) + } + defer func() { + _ = f.Close() + _ = os.RemoveAll(tempDir) + }() + + // Download the layer content into a temporal file + if _, err = xio.Copy(ctx, f, pr); err != nil { + return xerrors.Errorf("copy error: %w", err) + } + + // Decompress the downloaded file if it is compressed and copy it into the dst + // NOTE: it's local copying, the insecure option doesn't matter. + if _, err = downloader.Download(ctx, f.Name(), dir, dir, downloader.Options{}); err != nil { + return xerrors.Errorf("download error: %w", err) + } + + return nil +} + +func (a *Artifact) Digest(ctx context.Context) (string, error) { + if err := a.populate(ctx, a.RegistryOptions); err != nil { + return "", err + } + + digest, err := a.image.Digest() + if err != nil { + return "", xerrors.Errorf("digest error: %w", err) + } + return digest.String(), nil +} + +func (a *Artifact) Manifest(ctx context.Context) (*v1.Manifest, error) { + if err := a.populate(ctx, a.RegistryOptions); err != nil { + return nil, err + } + + manifest, err := a.image.Manifest() + if err != nil { + return nil, xerrors.Errorf("get manifest: %w", err) + } + + return manifest, nil +} + +type Artifacts []*Artifact + +// NewArtifacts returns a slice of artifacts. +func NewArtifacts(repos []name.Reference, opt types.RegistryOptions, opts ...Option) Artifacts { + return xslices.Map(repos, func(r name.Reference) *Artifact { + return NewArtifact(r.String(), opt, opts...) + }) +} + +// Download downloads artifacts until one of them succeeds. +// Attempts to download next artifact if the first one fails due to a temporary error. +func (a Artifacts) Download(ctx context.Context, dst string, opt DownloadOption) error { + var errs error + for i, art := range a { + log.InfoContext(ctx, "Downloading artifact...", log.String("repo", art.repository)) + err := art.Download(ctx, dst, opt) + if err == nil { + log.InfoContext(ctx, "Artifact successfully downloaded", log.String("repo", art.repository)) + return nil + } + + if !shouldTryOtherRepo(err) { + return xerrors.Errorf("failed to download artifact from %s: %w", art.repository, err) + } + log.ErrorContext(ctx, "Failed to download artifact", log.String("repo", art.repository), log.Err(err)) + if i < len(a)-1 { + log.InfoContext(ctx, "Trying to download artifact from other repository...") + } + errs = multierror.Append(errs, err) + } + + return xerrors.Errorf("failed to download artifact from any source: %w", errs) +} + +func shouldTryOtherRepo(err error) bool { + var terr *transport.Error + if !errors.As(err, &terr) { + return false + } + + for _, diagnostic := range terr.Errors { + // For better user experience + if diagnostic.Code == transport.DeniedErrorCode || diagnostic.Code == transport.UnauthorizedErrorCode { + // e.g. https://trivy.dev/docs/latest/guide/references/troubleshooting/#db + log.Warnf("See %s", doc.URL("guide/references/troubleshooting/", "db")) + break + } + } + + // try the following artifact if a temporary error occurs + if terr.Temporary() { + return true + } + + // `GCR` periodically returns `BLOB_UNKNOWN` error. + // cf. https://github.com/aquasecurity/trivy/discussions/8020 + // In this case we need to check other repositories. + for _, e := range terr.Errors { + if e.Code == transport.BlobUnknownErrorCode { + return true + } + } + + return false +} diff --git a/pkg/oci/artifact_test.go b/pkg/oci/artifact_test.go new file mode 100644 index 000000000000..7faf17e9bea9 --- /dev/null +++ b/pkg/oci/artifact_test.go @@ -0,0 +1,136 @@ +package oci_test + +import ( + "errors" + "os" + "path/filepath" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + fakei "github.com/google/go-containerregistry/pkg/v1/fake" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/oci" +) + +type fakeLayer struct { + v1.Layer +} + +func (f fakeLayer) MediaType() (types.MediaType, error) { + return "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", nil +} + +func TestArtifact_Download(t *testing.T) { + layer, err := tarball.LayerFromFile("testdata/test.tar.gz") + require.NoError(t, err) + + txtLayer, err := tarball.LayerFromFile("testdata/test.txt") + require.NoError(t, err) + + flayer := fakeLayer{layer} + + type layersReturns struct { + layers []v1.Layer + err error + } + tests := []struct { + name string + input string + mediaType string + layersReturns layersReturns + want string + wantErr string + }{ + { + name: "happy path", + input: "testdata/test.tar.gz", + mediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", + layersReturns: layersReturns{ + layers: []v1.Layer{flayer}, + }, + want: "Hello, world", + }, + { + name: "sad: two layers", + mediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", + layersReturns: layersReturns{ + layers: []v1.Layer{ + layer, + layer, + }, + }, + wantErr: "OCI artifact must be a single layer", + }, + { + name: "sad: Layers returns an error", + mediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", + layersReturns: layersReturns{ + err: errors.New("error"), + }, + wantErr: "OCI layer error", + }, + { + name: "invalid gzip", + input: "testdata/test.txt", + layersReturns: layersReturns{ + layers: []v1.Layer{txtLayer}, + }, + wantErr: "unexpected EOF", + }, + { + name: "sad: media type doesn't match", + mediaType: "unknown", + layersReturns: layersReturns{ + layers: []v1.Layer{layer}, + }, + wantErr: "unacceptable media type", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + + // Mock image + img := new(fakei.FakeImage) + img.LayersReturns(tt.layersReturns.layers, tt.layersReturns.err) + img.ManifestReturns(&v1.Manifest{ + Layers: []v1.Descriptor{ + { + MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", + Size: 100, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8", + }, + Annotations: map[string]string{ + "org.opencontainers.image.title": "bundle.tar.gz", + }, + }, + }, + }, nil) + + artifact := oci.NewArtifact("repo", ftypes.RegistryOptions{}, oci.WithImage(img)) + err = artifact.Download(t.Context(), tempDir, oci.DownloadOption{ + MediaType: tt.mediaType, + Quiet: true, + }) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + // Assert + got, err := os.ReadFile(filepath.Join(tempDir, "test.txt")) + require.NoError(t, err) + + assert.Equal(t, tt.want, string(got)) + }) + } +} diff --git a/pkg/oci/testdata/test.tar.gz b/pkg/oci/testdata/test.tar.gz new file mode 100644 index 000000000000..8fc713487111 Binary files /dev/null and b/pkg/oci/testdata/test.tar.gz differ diff --git a/pkg/oci/testdata/test.txt b/pkg/oci/testdata/test.txt new file mode 100644 index 000000000000..dbe9dba55ea8 --- /dev/null +++ b/pkg/oci/testdata/test.txt @@ -0,0 +1 @@ +Hello, world \ No newline at end of file diff --git a/pkg/parallel/pipeline.go b/pkg/parallel/pipeline.go new file mode 100644 index 000000000000..75530c9e7e15 --- /dev/null +++ b/pkg/parallel/pipeline.go @@ -0,0 +1,112 @@ +package parallel + +import ( + "context" + + "github.com/cheggaaa/pb/v3" + "golang.org/x/sync/errgroup" +) + +const defaultWorkers = 5 + +// Pipeline represents a structure for performing parallel processing. +// T represents the input element type and U represents the output element type. +type Pipeline[T, U any] struct { + numWorkers int + items []T + onItem onItem[T, U] + onResult onResult[U] + progress bool +} + +// onItem represents a function type that takes an input element and returns an output element. +type onItem[T, U any] func(context.Context, T) (U, error) + +// onResult represents a function type that takes an output element. +type onResult[U any] func(U) error + +func NewPipeline[T, U any](numWorkers int, progress bool, items []T, + fn1 onItem[T, U], fn2 onResult[U]) Pipeline[T, U] { + if fn2 == nil { + // In case where there is no need to process the return values + fn2 = func(_ U) error { return nil } + } + if numWorkers == 0 { + numWorkers = defaultWorkers + } + return Pipeline[T, U]{ + numWorkers: numWorkers, + progress: progress, + items: items, + onItem: fn1, + onResult: fn2, + } +} + +// Do executes pipeline processing. +// It exits when any error occurs. +func (p *Pipeline[T, U]) Do(ctx context.Context) error { + // progress bar + var bar *pb.ProgressBar + if p.progress { + bar = pb.StartNew(len(p.items)) + defer bar.Finish() + } + + g, ctx := errgroup.WithContext(ctx) + itemCh := make(chan T) + + // Start a goroutine to send input data + g.Go(func() error { + defer close(itemCh) + for _, item := range p.items { + if p.progress { + bar.Increment() + } + select { + case itemCh <- item: + case <-ctx.Done(): + return ctx.Err() + } + } + return nil + }) + + // Generate a channel for sending output data + results := make(chan U) + + // Start a fixed number of goroutines to process items. + for i := 0; i < p.numWorkers; i++ { + g.Go(func() error { + for item := range itemCh { + res, err := p.onItem(ctx, item) + if err != nil { + return err + } + select { + case results <- res: + case <-ctx.Done(): + return ctx.Err() + } + } + return nil + }) + } + + go func() { + _ = g.Wait() + close(results) + }() + + // Process output data received from the channel + for res := range results { + if err := p.onResult(res); err != nil { + return err + } + } + + // Check whether any of the goroutines failed. Since g is accumulating the + // errors, we don't need to send them (or check for them) in the individual + // results sent on the channel. + return g.Wait() +} diff --git a/pkg/parallel/pipeline_test.go b/pkg/parallel/pipeline_test.go new file mode 100644 index 000000000000..1226011e018d --- /dev/null +++ b/pkg/parallel/pipeline_test.go @@ -0,0 +1,113 @@ +package parallel_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/parallel" +) + +func TestPipeline_Do(t *testing.T) { + type field struct { + numWorkers int + items []int + onItem func(context.Context, int) (int, error) + } + type testCase struct { + name string + field field + want int + wantErr require.ErrorAssertionFunc + } + tests := []testCase{ + { + name: "pow", + field: field{ + numWorkers: 5, + items: []int{ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + }, + onItem: func(_ context.Context, i int) (int, error) { + return i * i, nil + }, + }, + want: 385, + wantErr: require.NoError, + }, + { + name: "double", + field: field{ + numWorkers: 3, + items: []int{ + 1, + 2, + 3, + 4, + 5, + -1, + -2, + -3, + }, + onItem: func(_ context.Context, i int) (int, error) { + return i * 2, nil + }, + }, + want: 18, + wantErr: require.NoError, + }, + { + name: "error in series", + field: field{ + numWorkers: 1, + items: []int{ + 1, + 2, + 3, + }, + onItem: func(_ context.Context, _ int) (int, error) { + return 0, errors.New("error") + }, + }, + wantErr: require.Error, + }, + { + name: "error in parallel", + field: field{ + numWorkers: 3, + items: []int{ + 1, + 2, + }, + onItem: func(_ context.Context, _ int) (int, error) { + return 0, errors.New("error") + }, + }, + wantErr: require.Error, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var got int + p := parallel.NewPipeline(tt.field.numWorkers, false, tt.field.items, tt.field.onItem, func(f int) error { + got += f + return nil + }) + err := p.Do(t.Context()) + tt.wantErr(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/parallel/walk.go b/pkg/parallel/walk.go new file mode 100644 index 000000000000..3fe65c40ad7a --- /dev/null +++ b/pkg/parallel/walk.go @@ -0,0 +1,115 @@ +package parallel + +import ( + "context" + "io/fs" + + "golang.org/x/sync/errgroup" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" + xio "github.com/aquasecurity/trivy/pkg/x/io" +) + +const defaultParallel = 5 + +type onFile[T any] func(string, fs.FileInfo, xio.ReadSeekerAt) (T, error) +type onWalkResult[T any] func(T) error + +func WalkDir[T any](ctx context.Context, fsys fs.FS, root string, parallel int, + onFile onFile[T], onResult onWalkResult[T]) error { + if parallel == 0 { + parallel = defaultParallel // Set the default value + } + + g, ctx := errgroup.WithContext(ctx) + paths := make(chan string) + + g.Go(func() error { + defer close(paths) + err := fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } else if !d.Type().IsRegular() { + return nil + } + + // check if file is empty + info, err := d.Info() + if err != nil { + return err + } else if info.Size() == 0 { + log.Debug("Skip the empty file", log.FilePath(path)) + return nil + } + + select { + case paths <- path: + case <-ctx.Done(): + return ctx.Err() + } + return nil + }) + if err != nil { + return xerrors.Errorf("walk error: %w", err) + } + return nil + }) + + // Start a fixed number of goroutines to read and digest files. + c := make(chan T) + for i := 0; i < parallel; i++ { + g.Go(func() error { + for path := range paths { + if err := walk(ctx, fsys, path, c, onFile); err != nil { + return err + } + } + return nil + }) + } + go func() { + _ = g.Wait() + close(c) + }() + + for res := range c { + if err := onResult(res); err != nil { + return err + } + } + // Check whether any of the goroutines failed. Since g is accumulating the + // errors, we don't need to send them (or check for them) in the individual + // results sent on the channel. + return g.Wait() +} + +func walk[T any](ctx context.Context, fsys fs.FS, path string, c chan T, onFile onFile[T]) error { + f, err := fsys.Open(path) + if err != nil { + return xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + info, err := f.Stat() + if err != nil { + return xerrors.Errorf("stat error: %w", err) + } + + rsa, ok := f.(xio.ReadSeekerAt) + if !ok { + return xerrors.New("type assertion failed") + } + res, err := onFile(path, info, rsa) + if err != nil { + log.Debug("Walk error", log.FilePath(path), log.Err(err)) + return nil + } + + select { + case c <- res: + case <-ctx.Done(): + return ctx.Err() + } + return nil +} diff --git a/pkg/plugin/index.go b/pkg/plugin/index.go new file mode 100644 index 000000000000..2ea5b37eae1b --- /dev/null +++ b/pkg/plugin/index.go @@ -0,0 +1,133 @@ +package plugin + +import ( + "bytes" + "context" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/downloader" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +const indexURL = "https://aquasecurity.github.io/trivy-plugin-index/v1/index.yaml" + +type Index struct { + Version int `yaml:"version"` + Plugins []struct { + Name string `yaml:"name"` + Version string `yaml:"version"` + Maintainer string `yaml:"maintainer"` + Summary string `yaml:"summary"` + Repository string `yaml:"repository"` + Output bool `yaml:"output"` + } `yaml:"plugins"` +} + +func (m *Manager) Update(ctx context.Context, opts Options) error { + m.logger.InfoContext(ctx, "Updating the plugin index...", log.String("url", m.indexURL)) + + // Download the index file to a temporary directory and copy it to the plugins directory, + // to avoid removing installed plugins. + tempDir, err := downloader.DownloadToTempDir(ctx, m.indexURL, downloader.Options{Insecure: opts.Insecure}) + if err != nil { + return xerrors.Errorf("unable to download the plugin index: %w", err) + } + + err = os.MkdirAll(filepath.Dir(m.indexPath), fs.ModePerm) + if err != nil { + return xerrors.Errorf("failed to create plugin index dir: %w", err) + } + + _, err = fsutils.CopyFile(filepath.Join(tempDir, "index.yaml"), m.indexPath) + if err != nil { + return xerrors.Errorf("unable to copy the plugin index file: %w", err) + } + return nil +} + +func (m *Manager) Search(ctx context.Context, keyword string) error { + index, err := m.loadIndex() + if errors.Is(err, os.ErrNotExist) { + m.logger.ErrorContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.") + return xerrors.Errorf("plugin index not found: %w", err) + } else if err != nil { + return xerrors.Errorf("unable to load the plugin index: %w", err) + } + + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("%-20s %-60s %-20s %s\n", "NAME", "DESCRIPTION", "MAINTAINER", "OUTPUT")) + for _, p := range index.Plugins { + if keyword == "" || strings.Contains(p.Name, keyword) || strings.Contains(p.Summary, keyword) { + s := fmt.Sprintf("%-20s %-60s %-20s %s\n", truncateString(p.Name, 20), + truncateString(p.Summary, 60), truncateString(p.Maintainer, 20), + lo.Ternary(p.Output, " ✓", "")) + buf.WriteString(s) + } + } + + if _, err = fmt.Fprint(m.w, buf.String()); err != nil { + return err + } + + return nil +} + +// tryIndex returns the repository URL if the plugin name is found in the index. +// Otherwise, it returns the input name. +func (m *Manager) tryIndex(ctx context.Context, name string, opts Options) string { + // If the index file does not exist, download it first. + if !fsutils.FileExists(m.indexPath) { + if err := m.Update(ctx, opts); err != nil { + m.logger.ErrorContext(ctx, "Failed to update the plugin index", log.Err(err)) + return name + } + } + + index, err := m.loadIndex() + if errors.Is(err, os.ErrNotExist) { + m.logger.WarnContext(ctx, "The plugin index is not found. Please run 'trivy plugin update' to download the index.") + return name + } else if err != nil { + m.logger.ErrorContext(ctx, "Unable to load the plugin index", log.Err(err)) + return name + } + + for _, p := range index.Plugins { + if p.Name == name { + return p.Repository + } + } + return name +} + +func (m *Manager) loadIndex() (*Index, error) { + f, err := os.Open(m.indexPath) + if err != nil { + return nil, xerrors.Errorf("unable to open the index file: %w", err) + } + defer f.Close() + + var index Index + if err = yaml.NewDecoder(f).Decode(&index); err != nil { + return nil, xerrors.Errorf("unable to decode the index file: %w", err) + } + + return &index, nil +} + +func truncateString(str string, num int) string { + if len(str) <= num { + return str + } + return str[:num-3] + "..." +} diff --git a/pkg/plugin/index_test.go b/pkg/plugin/index_test.go new file mode 100644 index 000000000000..1c5c6745a277 --- /dev/null +++ b/pkg/plugin/index_test.go @@ -0,0 +1,87 @@ +package plugin_test + +import ( + "bytes" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/plugin" +) + +func TestManager_Update(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tempDir) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, err := w.Write([]byte(`this is index`)) + assert.NoError(t, err) + })) + t.Cleanup(ts.Close) + + manager := plugin.NewManager(plugin.WithIndexURL(ts.URL + "/index.yaml")) + err := manager.Update(t.Context(), plugin.Options{}) + require.NoError(t, err) + + indexPath := filepath.Join(tempDir, ".trivy", "plugins", "index.yaml") + assert.FileExists(t, indexPath) + + b, err := os.ReadFile(indexPath) + require.NoError(t, err) + assert.Equal(t, "this is index", string(b)) +} + +func TestManager_Search(t *testing.T) { + tests := []struct { + name string + keyword string + dir string + want string + wantErr string + }{ + { + name: "all plugins", + keyword: "", + dir: "testdata", + want: `NAME DESCRIPTION MAINTAINER OUTPUT +foo A foo plugin aquasecurity ✓ +bar A bar plugin aquasecurity +test_plugin A test plugin aquasecurity +`, + }, + { + name: "keyword", + keyword: "bar", + dir: "testdata", + want: `NAME DESCRIPTION MAINTAINER OUTPUT +bar A bar plugin aquasecurity +`, + }, + { + name: "no index", + keyword: "", + dir: "unknown", + wantErr: "plugin index not found", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("XDG_DATA_HOME", tt.dir) + + var got bytes.Buffer + m := plugin.NewManager(plugin.WithWriter(&got)) + err := m.Search(t.Context(), tt.keyword) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got.String()) + }) + } +} diff --git a/pkg/plugin/manager.go b/pkg/plugin/manager.go new file mode 100644 index 000000000000..b94a2f6cb219 --- /dev/null +++ b/pkg/plugin/manager.go @@ -0,0 +1,402 @@ +package plugin + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/go-version/pkg/semver" + "github.com/aquasecurity/trivy/pkg/downloader" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +const configFile = "plugin.yaml" + +var ( + pluginsDir = "plugins" + + _defaultManager *Manager +) + +type ManagerOption func(indexer *Manager) + +func WithWriter(w io.Writer) ManagerOption { + return func(manager *Manager) { + manager.w = w + } +} + +func WithLogger(logger *log.Logger) ManagerOption { + return func(manager *Manager) { + manager.logger = logger + } +} + +func WithIndexURL(indexURL string) ManagerOption { + return func(manager *Manager) { + manager.indexURL = indexURL + } +} + +// Manager manages the plugins +type Manager struct { + w io.Writer + indexURL string + logger *log.Logger + pluginRoot string + indexPath string +} + +func NewManager(opts ...ManagerOption) *Manager { + root := filepath.Join(fsutils.TrivyHomeDir(), pluginsDir) + m := &Manager{ + w: os.Stdout, + indexURL: indexURL, + logger: log.WithPrefix("plugin"), + pluginRoot: root, + indexPath: filepath.Join(root, "index.yaml"), + } + for _, opt := range opts { + opt(m) + } + return m +} + +func defaultManager() *Manager { + if _defaultManager == nil { + _defaultManager = NewManager() + } + return _defaultManager +} + +func Install(ctx context.Context, name string, opts Options) (Plugin, error) { + return defaultManager().Install(ctx, name, opts) +} +func Start(ctx context.Context, name string, opts Options) (Wait, error) { + return defaultManager().Start(ctx, name, opts) +} +func Run(ctx context.Context, name string, opts Options) error { + return defaultManager().Run(ctx, name, opts) +} +func Upgrade(ctx context.Context, names []string) error { return defaultManager().Upgrade(ctx, names) } +func Uninstall(ctx context.Context, name string) error { return defaultManager().Uninstall(ctx, name) } +func Information(name string) error { return defaultManager().Information(name) } +func List(ctx context.Context) error { return defaultManager().List(ctx) } +func Update(ctx context.Context, opts Options) error { return defaultManager().Update(ctx, opts) } +func Search(ctx context.Context, keyword string) error { return defaultManager().Search(ctx, keyword) } + +// Install installs a plugin +func (m *Manager) Install(ctx context.Context, arg string, opts Options) (Plugin, error) { + input := m.parseArg(ctx, arg) + input.name = m.tryIndex(ctx, input.name, opts) + + // If the plugin is already installed, it skips installing the plugin. + if p, installed := m.isInstalled(ctx, input.name, input.version); installed { + m.logger.InfoContext(ctx, "The plugin is already installed", log.String("name", p.Name)) + return p, nil + } + + m.logger.InfoContext(ctx, "Installing the plugin...", log.String("src", input.name)) + return m.install(ctx, input.String(), opts) +} + +func (m *Manager) install(ctx context.Context, src string, opts Options) (Plugin, error) { + tempDir, err := downloader.DownloadToTempDir(ctx, src, downloader.Options{Insecure: opts.Insecure}) + if err != nil { + return Plugin{}, xerrors.Errorf("download failed: %w", err) + } + defer os.RemoveAll(tempDir) + + if entries, err := os.ReadDir(tempDir); err != nil { + return Plugin{}, xerrors.Errorf("failed to read %s: %w", tempDir, err) + } else if len(entries) == 1 && entries[0].IsDir() { + // A single directory may be contained within an archive file. + // e.g. https://github.com/aquasecurity/trivy-plugin-referrer/archive/refs/heads/main.zip + tempDir = filepath.Join(tempDir, entries[0].Name()) + } + + m.logger.DebugContext(ctx, "Loading the plugin metadata...") + plugin, err := m.loadMetadata(tempDir) + if err != nil { + return Plugin{}, xerrors.Errorf("failed to load the plugin metadata: %w", err) + } + + if err = plugin.install(ctx, plugin.Dir(), tempDir, opts); err != nil { + return Plugin{}, xerrors.Errorf("failed to install the plugin: %w", err) + } + + // Copy plugin.yaml into the plugin dir + f, err := os.Create(filepath.Join(plugin.Dir(), configFile)) + if err != nil { + return Plugin{}, xerrors.Errorf("failed to create plugin.yaml: %w", err) + } + defer f.Close() + + if err = yaml.NewEncoder(f).Encode(plugin); err != nil { + return Plugin{}, xerrors.Errorf("yaml encode error: %w", err) + } + + m.logger.InfoContext(ctx, "Plugin successfully installed", + log.String("name", plugin.Name), log.String("version", plugin.Version)) + + return plugin, nil +} + +// Uninstall installs the plugin +func (m *Manager) Uninstall(ctx context.Context, name string) error { + pluginDir := filepath.Join(m.pluginRoot, name) + if !fsutils.DirExists(pluginDir) { + m.logger.ErrorContext(ctx, "No such plugin") + return nil + } + if err := os.RemoveAll(pluginDir); err != nil { + return xerrors.Errorf("failed to uninstall the plugin: %w", err) + } + m.logger.InfoContext(ctx, "Plugin successfully uninstalled", log.String("name", name)) + return nil +} + +// Information gets the information about an installed plugin +func (m *Manager) Information(name string) error { + plugin, err := m.load(name) + if err != nil { + return xerrors.Errorf("plugin load error: %w", err) + } + + _, err = fmt.Fprintf(m.w, ` +Plugin: %s + Version: %s + Summary: %s + Description: %s +`, plugin.Name, plugin.Version, plugin.Summary, plugin.Description) + + return err +} + +// List gets a list of all installed plugins +func (m *Manager) List(ctx context.Context) error { + s, err := m.list(ctx) + if err != nil { + return xerrors.Errorf("unable to list plugins: %w", err) + } + _, err = fmt.Fprintf(m.w, "%s\n", s) + return err +} + +func (m *Manager) list(ctx context.Context) (string, error) { + if _, err := os.Stat(m.pluginRoot); err != nil { + if os.IsNotExist(err) { + return "No Installed Plugins", nil + } + return "", xerrors.Errorf("stat error: %w", err) + } + plugins, err := m.LoadAll(ctx) + if err != nil { + return "", xerrors.Errorf("unable to load plugins: %w", err) + } else if len(plugins) == 0 { + return "No Installed Plugins", nil + } + pluginList := []string{"Installed Plugins:"} + for _, plugin := range plugins { + pluginList = append(pluginList, fmt.Sprintf(" Name: %s\n Version: %s\n", plugin.Name, plugin.Version)) + } + + return strings.Join(pluginList, "\n"), nil +} + +// Upgrade upgrades an existing plugins +func (m *Manager) Upgrade(ctx context.Context, names []string) error { + if len(names) == 0 { + plugins, err := m.LoadAll(ctx) + if err != nil { + return xerrors.Errorf("unable to load plugins: %w", err) + } else if len(plugins) == 0 { + m.logger.InfoContext(ctx, "No installed plugins") + return nil + } + names = xslices.Map(plugins, func(p Plugin) string { return p.Name }) + } + for _, name := range names { + if err := m.upgrade(ctx, name); err != nil { + return xerrors.Errorf("unable to upgrade '%s' plugin: %w", name, err) + } + } + return nil +} + +func (m *Manager) upgrade(ctx context.Context, name string) error { + plugin, err := m.load(name) + if err != nil { + return xerrors.Errorf("plugin load error: %w", err) + } + + logger := m.logger.With("name", name) + logger.InfoContext(ctx, "Upgrading plugin...") + updated, err := m.install(ctx, plugin.Repository, Options{ + // Use the current installed platform + Platform: ftypes.Platform{ + Platform: &v1.Platform{ + OS: plugin.Installed.Platform.OS, + Architecture: plugin.Installed.Platform.Arch, + }, + }, + }) + if err != nil { + return xerrors.Errorf("unable to perform an upgrade installation: %w", err) + } + + if plugin.Version == updated.Version { + logger.InfoContext(ctx, "The plugin is up-to-date", log.String("version", plugin.Version)) + } else { + logger.InfoContext(ctx, "Plugin upgraded", + log.String("from", plugin.Version), log.String("to", updated.Version)) + } + return nil +} + +// LoadAll loads all plugins +func (m *Manager) LoadAll(ctx context.Context) ([]Plugin, error) { + dirs, err := os.ReadDir(m.pluginRoot) + if err != nil { + return nil, xerrors.Errorf("failed to read %s: %w", m.pluginRoot, err) + } + + var plugins []Plugin + for _, d := range dirs { + if !d.IsDir() { + continue + } + plugin, err := m.loadMetadata(filepath.Join(m.pluginRoot, d.Name())) + if err != nil { + m.logger.WarnContext(ctx, "Plugin load error", log.Err(err)) + continue + } + plugins = append(plugins, plugin) + } + return plugins, nil +} + +// Start starts the plugin +func (m *Manager) Start(ctx context.Context, name string, opts Options) (Wait, error) { + plugin, err := m.load(name) + if err != nil { + return nil, xerrors.Errorf("plugin load error: %w", err) + } + + wait, err := plugin.Start(ctx, opts) + if err != nil { + return nil, xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err) + } + return wait, nil +} + +// Run installs and runs the plugin +func (m *Manager) Run(ctx context.Context, name string, opts Options) error { + plugin, err := m.Install(ctx, name, opts) + if err != nil { + return xerrors.Errorf("plugin install error: %w", err) + } + + if err = plugin.Run(ctx, opts); err != nil { + return xerrors.Errorf("unable to run %s plugin: %w", plugin.Name, err) + } + return nil +} + +func (m *Manager) load(name string) (Plugin, error) { + pluginDir := filepath.Join(m.pluginRoot, name) + if _, err := os.Stat(pluginDir); err != nil { + if os.IsNotExist(err) { + return Plugin{}, xerrors.Errorf("could not find a plugin called '%s', did you install it?", name) + } + return Plugin{}, xerrors.Errorf("plugin stat error: %w", err) + } + + plugin, err := m.loadMetadata(pluginDir) + if err != nil { + return Plugin{}, xerrors.Errorf("unable to load plugin metadata: %w", err) + } + + return plugin, nil +} + +func (m *Manager) loadMetadata(dir string) (Plugin, error) { + filePath := filepath.Join(dir, configFile) + f, err := os.Open(filePath) + if err != nil { + return Plugin{}, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + var plugin Plugin + if err = yaml.NewDecoder(f).Decode(&plugin); err != nil { + return Plugin{}, xerrors.Errorf("yaml decode error: %w", err) + } + + if plugin.Name == "" { + return Plugin{}, xerrors.Errorf("'name' is empty") + } + + // e.g. ~/.trivy/plugins/kubectl + plugin.dir = filepath.Join(m.pluginRoot, plugin.Name) + + if plugin.Summary == "" && plugin.Usage != "" { + plugin.Summary = plugin.Usage // For backward compatibility + plugin.Usage = "" + } + + return plugin, nil +} + +func (m *Manager) isInstalled(ctx context.Context, url, version string) (Plugin, bool) { + installedPlugins, err := m.LoadAll(ctx) + if err != nil { + return Plugin{}, false + } + + for _, plugin := range installedPlugins { + if plugin.Repository == url && (version == "" || plugin.Version == version) { + return plugin, true + } + } + return Plugin{}, false +} + +// Input represents the user-specified Input. +type Input struct { + name string + version string +} + +func (i *Input) String() string { + if i.version != "" { + // cf. https://github.com/hashicorp/go-getter/blob/268c11cae8cf0d9374783e06572679796abe9ce9/README.md#git-git + return i.name + "?ref=v" + i.version + } + return i.name +} + +func (m *Manager) parseArg(ctx context.Context, arg string) Input { + before, after, found := strings.Cut(arg, "@v") + if !found { + return Input{name: arg} + } else if _, err := semver.Parse(after); err != nil { + m.logger.DebugContext(ctx, "Unable to identify the plugin version", log.String("name", arg), log.Err(err)) + return Input{name: arg} + } + // cf. https://github.com/hashicorp/go-getter/blob/268c11cae8cf0d9374783e06572679796abe9ce9/README.md#git-git + return Input{ + name: before, + version: after, + } +} diff --git a/pkg/plugin/manager_test.go b/pkg/plugin/manager_test.go new file mode 100644 index 000000000000..610f361fd034 --- /dev/null +++ b/pkg/plugin/manager_test.go @@ -0,0 +1,382 @@ +//go:build unix + +package plugin_test + +import ( + "bytes" + "context" + "fmt" + "log/slog" + "os" + "path/filepath" + "runtime" + "testing" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/plugin" +) + +func setupInstalledPlugin(t *testing.T, homeDir string, p plugin.Plugin) { + pluginDir := filepath.Join(homeDir, ".trivy", "plugins", p.Name) + + // Create the test plugin directory + err := os.MkdirAll(pluginDir, os.ModePerm) + require.NoError(t, err) + + // write the plugin name + pluginMetadata := fmt.Sprintf(`name: "%s" +repository: "%s" +version: "%s" +usage: test +description: test +platforms: + - selector: + os: linux + arch: amd64 + uri: ./test.sh + bin: ./test.sh +installed: + platform: + os: linux + arch: amd64`, p.Name, p.Repository, p.Version) + + err = os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginMetadata), os.ModePerm) + require.NoError(t, err) +} + +func TestManager_Run(t *testing.T) { + if runtime.GOOS == "windows" { + // the test.sh script can't be run on windows so skipping + t.Skip("Test satisfied adequately by Linux tests") + } + + type fields struct { + Name string + Repository string + Version string + Summary string + Description string + Platforms []plugin.Platform + GOOS string + GOARCH string + } + tests := []struct { + name string + fields fields + opts plugin.Options + wantErr string + }{ + { + name: "happy path", + fields: fields{ + Name: "test_plugin", + Repository: "github.com/aquasecurity/trivy-plugin-test", + Version: "0.1.0", + Summary: "test", + Description: "test", + Platforms: []plugin.Platform{ + { + Selector: &plugin.Selector{ + OS: "linux", + Arch: "amd64", + }, + URI: "github.com/aquasecurity/trivy-plugin-test", + Bin: "test.sh", + }, + }, + GOOS: "linux", + GOARCH: "amd64", + }, + }, + { + name: "no selector", + fields: fields{ + Name: "test_plugin", + Repository: "github.com/aquasecurity/trivy-plugin-test", + Version: "0.1.0", + Summary: "test", + Description: "test", + Platforms: []plugin.Platform{ + { + URI: "github.com/aquasecurity/trivy-plugin-test", + Bin: "test.sh", + }, + }, + }, + }, + { + name: "no matched platform", + fields: fields{ + Name: "test_plugin", + Repository: "github.com/aquasecurity/trivy-plugin-test", + Version: "0.1.0", + Summary: "test", + Description: "test", + Platforms: []plugin.Platform{ + { + Selector: &plugin.Selector{ + OS: "darwin", + Arch: "amd64", + }, + URI: "github.com/aquasecurity/trivy-plugin-test", + Bin: "test.sh", + }, + }, + GOOS: "linux", + GOARCH: "amd64", + }, + wantErr: "platform not found", + }, + { + name: "no execution file", + fields: fields{ + Name: "test_plugin", + Repository: "github.com/aquasecurity/trivy-plugin-test", + Version: "0.1.0", + Summary: "test", + Description: "test", + Platforms: []plugin.Platform{ + { + Selector: &plugin.Selector{ + OS: "linux", + Arch: "amd64", + }, + URI: "github.com/aquasecurity/trivy-plugin-test", + Bin: "nonexistence.sh", + }, + }, + GOOS: "linux", + GOARCH: "amd64", + }, + wantErr: "no such file or directory", + }, + { + name: "plugin exec error", + fields: fields{ + Name: "error_plugin", + Repository: "github.com/aquasecurity/trivy-plugin-error", + Version: "0.1.0", + Summary: "test", + Description: "test", + Platforms: []plugin.Platform{ + { + Selector: &plugin.Selector{ + OS: "linux", + Arch: "amd64", + }, + URI: "github.com/aquasecurity/trivy-plugin-test", + Bin: "test.sh", + }, + }, + GOOS: "linux", + GOARCH: "amd64", + }, + wantErr: "exit status 1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("XDG_DATA_HOME", "testdata") + + p := plugin.Plugin{ + Name: tt.fields.Name, + Repository: tt.fields.Repository, + Version: tt.fields.Version, + Summary: tt.fields.Summary, + Description: tt.fields.Description, + Platforms: tt.fields.Platforms, + } + + err := p.Run(t.Context(), plugin.Options{ + Platform: ftypes.Platform{ + Platform: &v1.Platform{ + OS: "linux", + Architecture: "amd64", + }, + }, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestManager_Uninstall(t *testing.T) { + ctx := clock.With(t.Context(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + pluginName := "test_plugin" + + tempDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tempDir) + pluginDir := filepath.Join(tempDir, ".trivy", "plugins", pluginName) + + t.Run("plugin found", func(t *testing.T) { + // Create the test plugin directory + err := os.MkdirAll(pluginDir, os.ModePerm) + require.NoError(t, err) + + // Create the test file + err = os.WriteFile(filepath.Join(pluginDir, "test.sh"), []byte(`foo`), os.ModePerm) + require.NoError(t, err) + + // Uninstall the plugin + err = plugin.NewManager().Uninstall(ctx, pluginName) + require.NoError(t, err) + assert.NoDirExists(t, pluginDir) + }) + + t.Run("plugin not found", func(t *testing.T) { + t.Setenv("NO_COLOR", tempDir) + buf := bytes.NewBuffer(nil) + slog.SetDefault(slog.New(log.NewHandler(buf, &log.Options{Level: log.LevelInfo}))) + + err := plugin.NewManager().Uninstall(ctx, pluginName) + require.NoError(t, err) + assert.Equal(t, "2021-08-25T12:20:30Z\tERROR\t[plugin] No such plugin\n", buf.String()) + }) +} + +func TestManager_Information(t *testing.T) { + pluginName := "test_plugin" + + tempDir := t.TempDir() + pluginDir := filepath.Join(tempDir, ".trivy", "plugins", pluginName) + + t.Setenv("XDG_DATA_HOME", tempDir) + + // Create the test plugin directory + err := os.MkdirAll(pluginDir, os.ModePerm) + require.NoError(t, err) + + // write the plugin name + pluginMetadata := `name: "test_plugin" +repository: github.com/aquasecurity/trivy-plugin-test +version: "0.1.0" +usage: test +description: A simple test plugin` + + err = os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(pluginMetadata), os.ModePerm) + require.NoError(t, err) + + var got bytes.Buffer + manager := plugin.NewManager(plugin.WithWriter(&got)) + + // Get Information for the plugin + err = manager.Information(pluginName) + require.NoError(t, err) + assert.Equal(t, ` +Plugin: test_plugin + Version: 0.1.0 + Summary: test + Description: A simple test plugin +`, got.String()) + got.Reset() + + // Get Information for unknown plugin + err = manager.Information("unknown") + require.Error(t, err) + assert.ErrorContains(t, err, "could not find a plugin called 'unknown', did you install it?") +} + +func TestManager_LoadAll(t *testing.T) { + tests := []struct { + name string + dir string + want []plugin.Plugin + wantErr string + }{ + { + name: "happy path", + dir: "testdata", + want: []plugin.Plugin{ + { + Name: "test_plugin", + Repository: "github.com/aquasecurity/trivy-plugin-test", + Version: "0.1.0", + Summary: "test", + Description: "test", + Platforms: []plugin.Platform{ + { + Selector: &plugin.Selector{ + OS: "linux", + Arch: "amd64", + }, + URI: "./test.sh", + Bin: "./test.sh", + }, + }, + }, + }, + }, + { + name: "sad path", + dir: "sad", + wantErr: "failed to read", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("XDG_DATA_HOME", tt.dir) + + got, err := plugin.NewManager().LoadAll(t.Context()) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + require.Len(t, got, len(tt.want)) + for i := range tt.want { + assert.EqualExportedValues(t, tt.want[i], got[i]) + } + }) + } +} + +func TestManager_Upgrade(t *testing.T) { + if runtime.GOOS == "windows" { + // the test.sh script can't be run on windows so skipping + t.Skip("Test satisfied adequately by Linux tests") + } + pluginName := "test_plugin" + pluginVersion := "0.0.5" + + tempDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tempDir) + setupInstalledPlugin(t, tempDir, plugin.Plugin{ + Name: pluginName, + Version: pluginVersion, + Repository: "testdata/test_plugin", + }) + + ctx := t.Context() + m := plugin.NewManager() + + // verify initial version + verifyVersion(t, ctx, m, pluginName, pluginVersion) + + // Upgrade the existing plugin + err := m.Upgrade(ctx, nil) + require.NoError(t, err) + + // verify plugin updated + verifyVersion(t, ctx, m, pluginName, "0.2.0") +} + +func verifyVersion(t *testing.T, ctx context.Context, m *plugin.Manager, pluginName, expectedVersion string) { + plugins, err := m.LoadAll(ctx) + require.NoError(t, err) + for _, p := range plugins { + if p.Name == pluginName { + assert.Equal(t, expectedVersion, p.Version) + } + } +} diff --git a/pkg/plugin/manager_unix_test.go b/pkg/plugin/manager_unix_test.go new file mode 100644 index 000000000000..98a8baa01ab6 --- /dev/null +++ b/pkg/plugin/manager_unix_test.go @@ -0,0 +1,238 @@ +//go:build unix + +package plugin_test + +import ( + "archive/zip" + "bytes" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/go-git/go-git/v5" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/gittest" + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/plugin" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +func setupGitRepository(t *testing.T, repo, dir string) *httptest.Server { + gs := gittest.NewServer(t, repo, dir, gittest.Options{}) + + worktree := t.TempDir() + r := gittest.Clone(t, gs, repo, worktree, gittest.Options{}) + + // git tag + gittest.SetTag(t, r, "v0.2.0") + + // git commit + modifyManifest(t, worktree, "0.3.0") + gittest.CommitAll(t, r, "bump up to 0.3.0") + + err := r.Push(&git.PushOptions{}) + require.NoError(t, err) + + // git tag + gittest.SetTag(t, r, "v0.3.0") + + // git push --tags + gittest.PushTags(t, r) + + return gs +} + +func modifyManifest(t *testing.T, worktree, version string) { + manifestPath := filepath.Join(worktree, "plugin.yaml") + b, err := os.ReadFile(manifestPath) + require.NoError(t, err) + + b = bytes.ReplaceAll(b, []byte("0.2.0"), []byte(version)) + err = os.WriteFile(manifestPath, b, 0o644) + require.NoError(t, err) +} + +func TestManager_Install(t *testing.T) { + gs := setupGitRepository(t, "test_plugin", "testdata/test_plugin/test_plugin") + t.Cleanup(gs.Close) + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + zr := zip.NewWriter(w) + switch r.URL.Path { + case "/test_plugin.zip": + assert.NoError(t, zr.AddFS(os.DirFS("testdata/test_plugin/test_plugin"))) + case "/test_nested.zip": + assert.NoError(t, zr.AddFS(os.DirFS("testdata/test_plugin"))) + } + assert.NoError(t, zr.Close()) + })) + t.Cleanup(ts.Close) + + wantPlugin := plugin.Plugin{ + Name: "test_plugin", + Repository: "testdata/test_plugin", + Version: "0.2.0", + Summary: "test", + Description: "test", + Platforms: []plugin.Platform{ + { + Selector: &plugin.Selector{ + OS: "linux", + Arch: "amd64", + }, + URI: "./test.sh", + Bin: "./test.sh", + }, + }, + Installed: plugin.Installed{ + Platform: plugin.Selector{ + OS: "linux", + Arch: "amd64", + }, + }, + } + wantPluginWithGit := wantPlugin + wantPluginWithGit.Version = "0.3.0" + + wantLogs := `2021-08-25T12:20:30Z INFO Installing the plugin... src="%s" +2021-08-25T12:20:30Z INFO Plugin successfully installed name="test_plugin" version="%s" +` + + tests := []struct { + name string + pluginName string + installed *plugin.Plugin + want plugin.Plugin + wantFile string + wantLogs string + wantErr string + }{ + { + name: "http", + pluginName: ts.URL + "/test_plugin.zip", + want: wantPlugin, + wantFile: ".trivy/plugins/test_plugin/test.sh", + wantLogs: fmt.Sprintf(wantLogs, ts.URL+"/test_plugin.zip", "0.2.0"), + }, + { + name: "nested archive", + pluginName: ts.URL + "/test_nested.zip", + want: wantPlugin, + wantFile: ".trivy/plugins/test_plugin/test.sh", + wantLogs: fmt.Sprintf(wantLogs, ts.URL+"/test_nested.zip", "0.2.0"), + }, + { + name: "local path", + pluginName: "testdata/test_plugin", + want: wantPlugin, + wantFile: ".trivy/plugins/test_plugin/test.sh", + wantLogs: fmt.Sprintf(wantLogs, "testdata/test_plugin", "0.2.0"), + }, + { + name: "git", + pluginName: "git::" + gs.URL + "/test_plugin.git", + want: wantPluginWithGit, + wantFile: ".trivy/plugins/test_plugin/test.sh", + wantLogs: fmt.Sprintf(wantLogs, "git::"+gs.URL+"/test_plugin.git", "0.3.0"), + }, + { + name: "with version", + pluginName: "git::" + gs.URL + "/test_plugin.git@v0.2.0", + want: wantPlugin, + wantFile: ".trivy/plugins/test_plugin/test.sh", + wantLogs: fmt.Sprintf(wantLogs, "git::"+gs.URL+"/test_plugin.git", "0.2.0"), + }, + { + name: "via index", + pluginName: "test_plugin", + want: wantPlugin, + wantFile: ".trivy/plugins/test_plugin/test.sh", + wantLogs: fmt.Sprintf(wantLogs, "testdata/test_plugin", "0.2.0"), + }, + { + name: "installed", + pluginName: "test_plugin", + installed: &plugin.Plugin{ + Name: "test_plugin", + Repository: "testdata/test_plugin", + Version: "0.2.0", + }, + want: wantPlugin, + wantLogs: "2021-08-25T12:20:30Z INFO The plugin is already installed name=\"test_plugin\"\n", + }, + { + name: "different version installed", + pluginName: "test_plugin@v0.2.0", + installed: &plugin.Plugin{ + Name: "test_plugin", + Repository: "testdata/test_plugin", + Version: "0.1.0", + }, + want: wantPlugin, + wantLogs: fmt.Sprintf(wantLogs, "testdata/test_plugin", "0.2.0"), + }, + { + name: "plugin not found", + pluginName: "testdata/not_found", + wantErr: "no such file or directory", + }, + { + name: "no plugin.yaml", + pluginName: "testdata/no_yaml", + wantErr: "file open error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // The test plugin will be installed here + dst := t.TempDir() + t.Setenv("XDG_DATA_HOME", dst) + + // For plugin index + pluginDir := filepath.Join(dst, ".trivy", "plugins") + err := os.MkdirAll(pluginDir, 0o755) + require.NoError(t, err) + _, err = fsutils.CopyFile("testdata/.trivy/plugins/index.yaml", filepath.Join(pluginDir, "index.yaml")) + require.NoError(t, err) + + if tt.installed != nil { + setupInstalledPlugin(t, dst, *tt.installed) + } + + var gotLogs bytes.Buffer + logger := log.New(log.NewHandler(&gotLogs, nil)) + + ctx := clock.With(t.Context(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + + got, err := plugin.NewManager(plugin.WithLogger(logger)).Install(ctx, tt.pluginName, plugin.Options{ + Platform: ftypes.Platform{ + Platform: &v1.Platform{ + Architecture: "amd64", + OS: "linux", + }, + }, + }) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + assert.EqualExportedValues(t, tt.want, got) + if tt.wantFile != "" { + assert.FileExists(t, filepath.Join(dst, tt.wantFile)) + } + assert.Equal(t, tt.wantLogs, gotLogs.String()) + }) + } +} diff --git a/pkg/plugin/plugin.go b/pkg/plugin/plugin.go new file mode 100644 index 000000000000..7af8edec0388 --- /dev/null +++ b/pkg/plugin/plugin.go @@ -0,0 +1,169 @@ +package plugin + +import ( + "context" + "errors" + "io" + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/downloader" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +// Plugin represents a plugin. +type Plugin struct { + Name string `yaml:"name"` + Repository string `yaml:"repository"` + Version string `yaml:"version"` + Summary string `yaml:"summary"` + Usage string `yaml:"usage"` // Deprecated: Use summary instead + Description string `yaml:"description"` + Platforms []Platform `yaml:"platforms"` + + // Installed holds the metadata about installation + Installed Installed `yaml:"installed"` + + // dir points to the directory where the plugin is installed + dir string +} + +type Installed struct { + Platform Selector `yaml:"platform"` +} + +// Platform represents where the execution file exists per platform. +type Platform struct { + Selector *Selector + URI string + Bin string +} + +// Selector represents the environment. +type Selector struct { + OS string `yaml:"os"` + Arch string `yaml:"arch"` +} + +type Options struct { + Args []string + Stdin io.Reader // For output plugin + Platform ftypes.Platform + Insecure bool +} + +func (p *Plugin) Cmd(ctx context.Context, opts Options) (*exec.Cmd, error) { + platform, err := p.selectPlatform(ctx, opts) + if err != nil { + return nil, xerrors.Errorf("platform selection error: %w", err) + } + + execFile := filepath.Join(p.Dir(), platform.Bin) + + cmd := exec.CommandContext(ctx, execFile, opts.Args...) + cmd.Stdin = os.Stdin + if opts.Stdin != nil { + cmd.Stdin = opts.Stdin + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Env = os.Environ() + + return cmd, nil +} + +type Wait func() error + +// Start starts the plugin +// +// After a successful call to Start the Wait method must be called. +func (p *Plugin) Start(ctx context.Context, opts Options) (Wait, error) { + cmd, err := p.Cmd(ctx, opts) + if err != nil { + return nil, xerrors.Errorf("cmd: %w", err) + } + + if err = cmd.Start(); err != nil { + return nil, xerrors.Errorf("plugin start: %w", err) + } + return cmd.Wait, nil +} + +// Run runs the plugin +func (p *Plugin) Run(ctx context.Context, opts Options) error { + cmd, err := p.Cmd(ctx, opts) + if err != nil { + return xerrors.Errorf("cmd: %w", err) + } + + // If an error is found during the execution of the plugin, figure + // out if the error was from not being able to execute the plugin or + // an error set by the plugin itself. + if err = cmd.Run(); err != nil { + var execError *exec.ExitError + if errors.As(err, &execError) { + return &types.ExitError{ + Code: execError.ExitCode(), + } + } + return xerrors.Errorf("plugin exec: %w", err) + } + return nil +} + +func (p *Plugin) selectPlatform(ctx context.Context, opts Options) (Platform, error) { + // These values are only filled in during unit tests. + goos := runtime.GOOS + if opts.Platform.Platform != nil && opts.Platform.OS != "" { + goos = opts.Platform.OS + } + goarch := runtime.GOARCH + if opts.Platform.Platform != nil && opts.Platform.Architecture != "" { + goarch = opts.Platform.Architecture + } + + for _, platform := range p.Platforms { + if platform.Selector == nil { + return platform, nil + } + + selector := platform.Selector + if (selector.OS == "" || goos == selector.OS) && + (selector.Arch == "" || goarch == selector.Arch) { + log.DebugContext(ctx, "Platform found", + log.String("os", selector.OS), log.String("arch", selector.Arch)) + return platform, nil + } + } + return Platform{}, xerrors.New("platform not found") +} + +func (p *Plugin) install(ctx context.Context, dst, pwd string, opts Options) error { + log.DebugContext(ctx, "Installing the plugin...", log.String("path", dst)) + platform, err := p.selectPlatform(ctx, opts) + if err != nil { + return xerrors.Errorf("platform selection error: %w", err) + } + p.Installed.Platform = lo.FromPtr(platform.Selector) + + log.DebugContext(ctx, "Downloading the execution file...", log.String("uri", platform.URI)) + if _, err = downloader.Download(ctx, platform.URI, dst, pwd, downloader.Options{Insecure: opts.Insecure}); err != nil { + return xerrors.Errorf("unable to download the execution file (%s): %w", platform.URI, err) + } + return nil +} + +func (p *Plugin) Dir() string { + if p.dir != "" { + return p.dir + } + return filepath.Join(fsutils.TrivyHomeDir(), pluginsDir, p.Name) +} diff --git a/pkg/plugin/testdata/.trivy/plugins/dummy.txt b/pkg/plugin/testdata/.trivy/plugins/dummy.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/plugin/testdata/.trivy/plugins/error_plugin/test.sh b/pkg/plugin/testdata/.trivy/plugins/error_plugin/test.sh new file mode 100755 index 000000000000..91c5ffc57bca --- /dev/null +++ b/pkg/plugin/testdata/.trivy/plugins/error_plugin/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +exit 1 \ No newline at end of file diff --git a/pkg/plugin/testdata/.trivy/plugins/index.yaml b/pkg/plugin/testdata/.trivy/plugins/index.yaml new file mode 100644 index 000000000000..b37b86e176a0 --- /dev/null +++ b/pkg/plugin/testdata/.trivy/plugins/index.yaml @@ -0,0 +1,15 @@ +version: 1 +plugins: + - name: foo + output: true + maintainer: aquasecurity + summary: A foo plugin + repository: github.com/aquasecurity/trivy-plugin-foo + - name: bar + maintainer: aquasecurity + summary: A bar plugin + repository: github.com/aquasecurity/trivy-plugin-bar + - name: test_plugin + maintainer: aquasecurity + summary: A test plugin + repository: testdata/test_plugin \ No newline at end of file diff --git a/pkg/plugin/testdata/.trivy/plugins/test_plugin/plugin.yaml b/pkg/plugin/testdata/.trivy/plugins/test_plugin/plugin.yaml new file mode 100644 index 000000000000..7c2021b29196 --- /dev/null +++ b/pkg/plugin/testdata/.trivy/plugins/test_plugin/plugin.yaml @@ -0,0 +1,14 @@ +name: "test_plugin" +repository: github.com/aquasecurity/trivy-plugin-test +version: "0.1.0" +usage: test +description: test +platforms: + - selector: + os: linux + arch: amd64 + uri: ./test.sh + bin: ./test.sh +# for testing +_goos: linux +_goarch: amd64 \ No newline at end of file diff --git a/pkg/plugin/testdata/.trivy/plugins/test_plugin/test.sh b/pkg/plugin/testdata/.trivy/plugins/test_plugin/test.sh new file mode 100755 index 000000000000..490535e047e7 --- /dev/null +++ b/pkg/plugin/testdata/.trivy/plugins/test_plugin/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "foo" \ No newline at end of file diff --git a/pkg/plugin/testdata/no_yaml/.keep b/pkg/plugin/testdata/no_yaml/.keep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/pkg/plugin/testdata/test_plugin/test_plugin/plugin.yaml b/pkg/plugin/testdata/test_plugin/test_plugin/plugin.yaml new file mode 100644 index 000000000000..086aefb548a1 --- /dev/null +++ b/pkg/plugin/testdata/test_plugin/test_plugin/plugin.yaml @@ -0,0 +1,11 @@ +name: "test_plugin" +repository: testdata/test_plugin +version: "0.2.0" +summary: test +description: test +platforms: + - selector: + os: linux + arch: amd64 + uri: ./test.sh + bin: ./test.sh diff --git a/pkg/plugin/testdata/test_plugin/test_plugin/test.sh b/pkg/plugin/testdata/test_plugin/test_plugin/test.sh new file mode 100755 index 000000000000..490535e047e7 --- /dev/null +++ b/pkg/plugin/testdata/test_plugin/test_plugin/test.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "foo" \ No newline at end of file diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go new file mode 100644 index 000000000000..f5e54988d4f4 --- /dev/null +++ b/pkg/policy/policy.go @@ -0,0 +1,313 @@ +package policy + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "time" + + "golang.org/x/xerrors" + "k8s.io/utils/clock" + + "github.com/aquasecurity/go-version/pkg/part" + "github.com/aquasecurity/go-version/pkg/semver" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/oci" +) + +const ( + BundleVersion = 2 // Latest released MAJOR version for trivy-checks + BundleRepository = "mirror.gcr.io/aquasec/trivy-checks" + policyMediaType = "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip" + updateInterval = 24 * time.Hour + + VersionAnnotationKey = "org.opencontainers.image.version" +) + +type options struct { + artifact *oci.Artifact + clock clock.Clock +} + +// WithOCIArtifact takes an OCI artifact +func WithOCIArtifact(art *oci.Artifact) Option { + return func(opts *options) { + opts.artifact = art + } +} + +// WithClock takes a clock +func WithClock(c clock.Clock) Option { + return func(opts *options) { + opts.clock = c + } +} + +// Option is a functional option +type Option func(*options) + +// Client implements check operations +type Client struct { + *options + policyDir string + checkBundleRepo string + quiet bool +} + +// Metadata holds default check metadata +type Metadata struct { + Digest string + DownloadedAt time.Time + + // MajorVersion indicates the major version of the bundle. + // Used to invalidate cache when the major version increases. + // Nil for old cache entries. Set to 0 for custom builds. + MajorVersion *int `json:",omitempty"` + + // CustomBuild is true if the bundle was built manually and did not go + // through the official build process that enriches the manifest with additional data. + // For custom builds, MajorVersion is not used for cache invalidation. + CustomBuild bool `json:",omitempty"` +} + +func (m Metadata) String() string { + return fmt.Sprintf(`Check Bundle: + Digest: %s + DownloadedAt: %s +`, m.Digest, m.DownloadedAt.UTC()) +} + +// NewClient is the factory method for check client +func NewClient(cacheDir string, quiet bool, checkBundleRepo string, opts ...Option) (*Client, error) { + o := &options{ + clock: clock.RealClock{}, + } + + for _, opt := range opts { + opt(o) + } + + if checkBundleRepo == "" { + checkBundleRepo = fmt.Sprintf("%s:%d", BundleRepository, BundleVersion) + } + + return &Client{ + options: o, + policyDir: filepath.Join(cacheDir, "policy"), + checkBundleRepo: checkBundleRepo, + quiet: quiet, + }, nil +} + +func (c *Client) initOCIArtifact(ctx context.Context, registryOpts types.RegistryOptions) { + if c.artifact == nil { + log.DebugContext(ctx, "Initializing OCI checks bundle artifact", + log.String("repository", c.checkBundleRepo)) + c.artifact = oci.NewArtifact(c.checkBundleRepo, registryOpts) + } +} + +// DownloadBuiltinChecks download default policies from GitHub Pages +func (c *Client) DownloadBuiltinChecks(ctx context.Context, registryOpts types.RegistryOptions) error { + c.initOCIArtifact(ctx, registryOpts) + + dst := c.contentDir() + if err := c.artifact.Download(ctx, dst, oci.DownloadOption{ + MediaType: policyMediaType, + Quiet: c.quiet, + }, + ); err != nil { + return xerrors.Errorf("download error: %w", err) + } + + digest, err := c.artifact.Digest(ctx) + if err != nil { + return xerrors.Errorf("digest error: %w", err) + } + + ver, err := c.getBundleMajorVersion(ctx) + if err != nil { + return xerrors.Errorf("get bundle version: %w", err) + } + + isCustomBundle := ver == 0 + if isCustomBundle { + log.DebugContext(ctx, "Built-in checks (custom build)", + log.String("digest", digest)) + } else { + log.DebugContext(ctx, "Built-in checks", + log.String("digest", digest), log.Int("major_version", ver)) + } + + // Update metadata.json with the new digest and the current date + if err = c.updateMetadata(Metadata{ + Digest: digest, + DownloadedAt: c.clock.Now(), + MajorVersion: &ver, + CustomBuild: isCustomBundle, + }); err != nil { + return xerrors.Errorf("unable to update the check metadata: %w", err) + } + + return nil +} + +// BuiltinChecksPath returns default policies +func (c *Client) BuiltinChecksPath() string { + return c.contentDir() +} + +func (c *Client) getBundleMajorVersion(ctx context.Context) (ver int, err error) { + manifest, err := c.artifact.Manifest(ctx) + if err != nil { + return 0, err + } + + // No annotations → treat as custom build + if manifest.Annotations == nil { + return 0, nil + } + + v, ok := manifest.Annotations[VersionAnnotationKey] + if !ok || v == "" { + return 0, nil + } + + version, err := semver.Parse(v) + if err != nil { + // Invalid version → treat as custom build + return 0, nil + } + + majorPart, ok := version.Major().(part.Uint64) + if !ok { + // Could not extract major part → treat as custom build + return 0, nil + } + + return int(majorPart), nil +} + +// NeedsUpdate returns if the default check should be updated +func (c *Client) NeedsUpdate(ctx context.Context, registryOpts types.RegistryOptions) (bool, error) { + meta, err := c.GetMetadata(ctx) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + log.DebugContext(ctx, "Cache does not exist, will be created") + } else { + log.DebugContext(ctx, + "Invalidating cache: failed to get metadata", log.Err(err), + ) + } + return true, nil + } + + // For official builds, check the major version for cache invalidation. + // Custom builds may not have a version annotation, so MajorVersion is ignored. + // Old cache entries without a version will be invalidated once to enrich metadata. + if !meta.CustomBuild { + + // Invalidate the old cache if it does not store the version. + if meta.MajorVersion == nil { + log.DebugContext(ctx, + "Invalidating cache: missing major version", + log.String("digest", meta.Digest), + log.Time("downloaded_at", meta.DownloadedAt), + ) + return true, nil + } + + // Invalidate the old cache if its version does not match. + if *meta.MajorVersion != BundleVersion { + log.DebugContext(ctx, "Invalidating cache: version mismatch", + log.Int("cached_major_version", *meta.MajorVersion), + log.Int("current_major_version", BundleVersion), + ) + return true, nil + } + } + + // No need to update if it's been within a day since the last update. + if c.clock.Now().Before(meta.DownloadedAt.Add(updateInterval)) { + return false, nil + } + + c.initOCIArtifact(ctx, registryOpts) + digest, err := c.artifact.Digest(ctx) + if err != nil { + return false, xerrors.Errorf("digest error: %w", err) + } + + if meta.Digest != digest { + log.DebugContext(ctx, "Invalidating cache: digest mismatch", + log.String("cached_digest", meta.Digest), + log.String("current_digest", digest), + ) + return true, nil + } + + // Update DownloadedAt with the current time. + // Otherwise, if there are no updates in the remote registry, + // the digest will be fetched every time even after this. + if err = c.updateMetadata(Metadata{ + Digest: meta.Digest, + DownloadedAt: c.clock.Now(), + MajorVersion: meta.MajorVersion, + CustomBuild: meta.CustomBuild, + }); err != nil { + return false, xerrors.Errorf("unable to update the check metadata: %w", err) + } + + return false, nil +} + +func (c *Client) contentDir() string { + return filepath.Join(c.policyDir, "content") +} + +func (c *Client) metadataPath() string { + return filepath.Join(c.policyDir, "metadata.json") +} + +func (c *Client) updateMetadata(meta Metadata) error { + f, err := os.Create(c.metadataPath()) + if err != nil { + return xerrors.Errorf("failed to open checks bundle metadata: %w", err) + } + defer f.Close() + + if err = json.NewEncoder(f).Encode(meta); err != nil { + return xerrors.Errorf("json encode error: %w", err) + } + + return nil +} + +func (c *Client) GetMetadata(ctx context.Context) (*Metadata, error) { + f, err := os.Open(c.metadataPath()) + if err != nil { + log.DebugContext(ctx, "Failed to open the check metadata", log.Err(err)) + return nil, err + } + defer f.Close() + + var meta Metadata + if err = json.NewDecoder(f).Decode(&meta); err != nil { + log.WarnContext(ctx, "Check metadata decode error", log.Err(err)) + return nil, err + } + + return &meta, nil +} + +func (c *Client) Clear() error { + if err := os.RemoveAll(c.policyDir); err != nil { + return xerrors.Errorf("failed to remove check bundle: %w", err) + } + return nil +} diff --git a/pkg/policy/policy_test.go b/pkg/policy/policy_test.go new file mode 100644 index 000000000000..c54c4a46193e --- /dev/null +++ b/pkg/policy/policy_test.go @@ -0,0 +1,515 @@ +package policy_test + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "maps" + "os" + "path/filepath" + "testing" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + fakei "github.com/google/go-containerregistry/pkg/v1/fake" + "github.com/google/go-containerregistry/pkg/v1/tarball" + "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/utils/clock" + fake "k8s.io/utils/clock/testing" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/oci" + "github.com/aquasecurity/trivy/pkg/policy" +) + +type fakeLayer struct { + v1.Layer +} + +func (f fakeLayer) MediaType() (types.MediaType, error) { + return "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", nil +} + +func newFakeLayer(t *testing.T) v1.Layer { + layer, err := tarball.LayerFromFile("testdata/bundle.tar.gz") + require.NoError(t, err) + require.NotNil(t, layer) + + return fakeLayer{layer} +} + +type brokenLayer struct { + v1.Layer +} + +func (b brokenLayer) MediaType() (types.MediaType, error) { + return "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", nil +} + +func (b brokenLayer) Compressed() (io.ReadCloser, error) { + return nil, errors.New("compressed error") +} + +func newBrokenLayer(t *testing.T) v1.Layer { + layer, err := tarball.LayerFromFile("testdata/bundle.tar.gz") + require.NoError(t, err) + + return brokenLayer{layer} +} + +func TestClient_LoadBuiltinChecks(t *testing.T) { + tests := []struct { + name string + cacheDir string + want string + wantErr string + }{ + { + name: "happy path", + cacheDir: "testdata/happy", + want: filepath.Join("testdata", "happy", "policy", "content"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Mock image + img := new(fakei.FakeImage) + img.LayersReturns([]v1.Layer{newFakeLayer(t)}, nil) + img.ManifestReturns(&v1.Manifest{ + Layers: []v1.Descriptor{ + { + MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", + Size: 100, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8", + }, + Annotations: map[string]string{ + "org.opencontainers.image.title": "bundle.tar.gz", + }, + }, + }, + }, nil) + + // Mock OCI artifact + art := oci.NewArtifact("repo", ftypes.RegistryOptions{}, oci.WithImage(img)) + c, err := policy.NewClient(tt.cacheDir, true, "", policy.WithOCIArtifact(art)) + require.NoError(t, err) + + got := c.BuiltinChecksPath() + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +type annotations = map[string]string + +func TestClient_NeedsUpdate(t *testing.T) { + type digestReturns struct { + h v1.Hash + err error + } + + imageDigest := digestReturns{ + h: v1.Hash{ + Algorithm: "sha256", + Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + }, + } + + usedBundleVersion := fmt.Sprintf("%d.0.0", policy.BundleVersion) + + tests := []struct { + name string + clock clock.Clock + digestReturns digestReturns + metadata any + want bool + wantMetadata *policy.Metadata + wantErr bool + }{ + { + name: "recent download", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + digestReturns: imageDigest, + metadata: policy.Metadata{ + Digest: `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(policy.BundleVersion), + }, + want: false, + }, + { + name: "same digest", + clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)), + digestReturns: imageDigest, + metadata: policy.Metadata{ + Digest: `sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(policy.BundleVersion), + }, + want: false, + wantMetadata: &policy.Metadata{ + Digest: `sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d`, + DownloadedAt: time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(policy.BundleVersion), + }, + }, + { + name: "different digest", + clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)), + digestReturns: imageDigest, + metadata: policy.Metadata{ + Digest: `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(policy.BundleVersion), + }, + want: true, + }, + { + name: "sad: Digest returns an error", + clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)), + digestReturns: digestReturns{ + err: errors.New("error"), + }, + metadata: policy.Metadata{ + Digest: `sha256:922e50f14ab484f11ae65540c3d2d76009020213f1027d4331d31141575e5414`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(policy.BundleVersion), + }, + want: false, + wantErr: true, + }, + { + name: "sad: non-existent metadata", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + want: true, + }, + { + name: "sad: broken metadata", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + metadata: `"foo"`, + want: true, + }, + { + name: "old metadata without version", + clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)), + digestReturns: imageDigest, + metadata: policy.Metadata{ + Digest: `sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + }, + want: true, + }, + { + name: "version mismatched", + clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)), + digestReturns: imageDigest, + metadata: policy.Metadata{ + Digest: `sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(1), + }, + want: true, + }, + { + name: "version mismatched but custom build", + clock: fake.NewFakeClock(time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC)), + digestReturns: imageDigest, + metadata: policy.Metadata{ + Digest: `sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d`, + DownloadedAt: time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC), + CustomBuild: true, + MajorVersion: lo.ToPtr(1), + }, + want: false, + wantMetadata: &policy.Metadata{ + Digest: `sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d`, + DownloadedAt: time.Date(2021, 1, 2, 1, 0, 0, 0, time.UTC), + CustomBuild: true, + MajorVersion: lo.ToPtr(1), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up a temporary directory + tmpDir := t.TempDir() + + // Mock image + img := new(fakei.FakeImage) + img.LayersReturns([]v1.Layer{newFakeLayer(t)}, nil) + img.DigestReturns(tt.digestReturns.h, tt.digestReturns.err) + img.ManifestReturns(&v1.Manifest{ + Layers: []v1.Descriptor{ + { + MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", + Size: 100, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8", + }, + Annotations: map[string]string{ + "org.opencontainers.image.title": "bundle.tar.gz", + }, + }, + }, + Annotations: annotations{ + policy.VersionAnnotationKey: usedBundleVersion, + }, + }, nil) + + // Create a check directory + err := os.MkdirAll(filepath.Join(tmpDir, "policy"), os.ModePerm) + require.NoError(t, err) + + if tt.metadata != nil { + b, err := json.Marshal(tt.metadata) + require.NoError(t, err) + + // Write a metadata file + metadataPath := filepath.Join(tmpDir, "policy", "metadata.json") + err = os.WriteFile(metadataPath, b, os.ModePerm) + require.NoError(t, err) + } + + art := oci.NewArtifact("repo", ftypes.RegistryOptions{}, oci.WithImage(img)) + c, err := policy.NewClient(tmpDir, true, "", policy.WithOCIArtifact(art), policy.WithClock(tt.clock)) + require.NoError(t, err) + + // Assert results + got, err := c.NeedsUpdate(t.Context(), ftypes.RegistryOptions{}) + if tt.wantErr { + assert.Error(t, err) + return + } + assert.Equal(t, tt.want, got) + + // Verify that metadata has been updated correctly + if metadata, ok := tt.metadata.(policy.Metadata); ok { + metadataPath := filepath.Join(tmpDir, "policy", "metadata.json") + b, err := os.ReadFile(metadataPath) + require.NoError(t, err) + + var want policy.Metadata + err = json.Unmarshal(b, &want) + require.NoError(t, err) + + if tt.wantMetadata == nil { + // Metadata has not been changed + tt.wantMetadata = lo.ToPtr(metadata) + } + + assert.Equal(t, tt.wantMetadata, &want) + } + }) + } +} + +func TestClient_DownloadBuiltinChecks(t *testing.T) { + type digestReturns struct { + h v1.Hash + err error + } + type layersReturns struct { + layers []v1.Layer + err error + } + tests := []struct { + name string + clock clock.Clock + layersReturns layersReturns + digestReturns digestReturns + annotations annotations + want *policy.Metadata + wantErr string + }{ + { + name: "happy path", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + layersReturns: layersReturns{ + layers: []v1.Layer{newFakeLayer(t)}, + }, + digestReturns: digestReturns{ + h: v1.Hash{ + Algorithm: "sha256", + Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + }, + }, + want: &policy.Metadata{ + Digest: "sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + DownloadedAt: time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(policy.BundleVersion), + }, + }, + { + name: "sad: broken layer", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + layersReturns: layersReturns{ + layers: []v1.Layer{newBrokenLayer(t)}, + }, + digestReturns: digestReturns{ + h: v1.Hash{ + Algorithm: "sha256", + Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + }, + }, + wantErr: "compressed error", + }, + { + name: "sad: Digest returns an error", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + layersReturns: layersReturns{ + layers: []v1.Layer{newFakeLayer(t)}, + }, + digestReturns: digestReturns{ + err: errors.New("error"), + }, + wantErr: "digest error", + }, + { + name: "custom bundle", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + layersReturns: layersReturns{ + layers: []v1.Layer{newFakeLayer(t)}, + }, + digestReturns: digestReturns{ + h: v1.Hash{ + Algorithm: "sha256", + Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + }, + }, + annotations: annotations{}, + want: &policy.Metadata{ + Digest: "sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + DownloadedAt: time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(0), + CustomBuild: true, + }, + }, + { + name: "invalid version is treated as a custom build", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + layersReturns: layersReturns{ + layers: []v1.Layer{newFakeLayer(t)}, + }, + digestReturns: digestReturns{ + h: v1.Hash{ + Algorithm: "sha256", + Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + }, + }, + annotations: annotations{ + policy.VersionAnnotationKey: "dev", + }, + want: &policy.Metadata{ + Digest: "sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + DownloadedAt: time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(0), + CustomBuild: true, + }, + }, + { + name: "nightly build", + clock: fake.NewFakeClock(time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC)), + layersReturns: layersReturns{ + layers: []v1.Layer{newFakeLayer(t)}, + }, + digestReturns: digestReturns{ + h: v1.Hash{ + Algorithm: "sha256", + Hex: "01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + }, + }, + annotations: annotations{ + policy.VersionAnnotationKey: "2.0.1-nightly.20260129", + }, + want: &policy.Metadata{ + Digest: "sha256:01e033e78bd8a59fa4f4577215e7da06c05e1152526094d8d79d2aa06e98cb9d", + DownloadedAt: time.Date(2021, 1, 1, 1, 0, 0, 0, time.UTC), + MajorVersion: lo.ToPtr(2), + CustomBuild: false, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + + // Mock image + img := new(fakei.FakeImage) + img.DigestReturns(tt.digestReturns.h, tt.digestReturns.err) + img.LayersReturns(tt.layersReturns.layers, tt.layersReturns.err) + + manifest := &v1.Manifest{ + Layers: []v1.Descriptor{ + { + MediaType: "application/vnd.cncf.openpolicyagent.layer.v1.tar+gzip", + Size: 100, + Digest: v1.Hash{ + Algorithm: "sha256", + Hex: "cba33656188782852f58993f45b68bfb8577f64cdcf02a604e3fc2afbeb5f2d8", + }, + Annotations: map[string]string{ + "org.opencontainers.image.title": "bundle.tar.gz", + }, + }, + }, + Annotations: make(map[string]string), + } + + if tt.annotations != nil { + maps.Copy(manifest.Annotations, tt.annotations) + } else { + // Set default version + manifest.Annotations[policy.VersionAnnotationKey] = fmt.Sprintf("%d.0.0", policy.BundleVersion) + } + + img.ManifestReturns(manifest, nil) + + // Mock OCI artifact + art := oci.NewArtifact("repo", ftypes.RegistryOptions{}, oci.WithImage(img)) + c, err := policy.NewClient(tempDir, true, "", policy.WithClock(tt.clock), policy.WithOCIArtifact(art)) + require.NoError(t, err) + + err = c.DownloadBuiltinChecks(t.Context(), ftypes.RegistryOptions{}) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + // Assert metadata.json + metadata := filepath.Join(tempDir, "policy", "metadata.json") + b, err := os.ReadFile(metadata) + require.NoError(t, err) + + got := new(policy.Metadata) + err = json.Unmarshal(b, got) + require.NoError(t, err) + + assert.Equal(t, tt.want, got) + }) + } +} + +func TestClient_Clear(t *testing.T) { + cacheDir := t.TempDir() + err := os.MkdirAll(filepath.Join(cacheDir, "policy"), 0o755) + require.NoError(t, err) + + c, err := policy.NewClient(cacheDir, true, "") + require.NoError(t, err) + require.NoError(t, c.Clear()) +} diff --git a/pkg/policy/testdata/broken/policy/content/.manifest b/pkg/policy/testdata/broken/policy/content/.manifest new file mode 100644 index 000000000000..c7759bfead42 --- /dev/null +++ b/pkg/policy/testdata/broken/policy/content/.manifest @@ -0,0 +1,3 @@ +{ + "revision": "1", + } diff --git a/pkg/policy/testdata/bundle.tar.gz b/pkg/policy/testdata/bundle.tar.gz new file mode 100644 index 000000000000..83fd553a7aa2 Binary files /dev/null and b/pkg/policy/testdata/bundle.tar.gz differ diff --git a/pkg/policy/testdata/empty/policy/content/.manifest b/pkg/policy/testdata/empty/policy/content/.manifest new file mode 100644 index 000000000000..68cfbeb68d24 --- /dev/null +++ b/pkg/policy/testdata/empty/policy/content/.manifest @@ -0,0 +1,3 @@ +{ + "revision": "1" +} \ No newline at end of file diff --git a/pkg/policy/testdata/happy/policy/content/.manifest b/pkg/policy/testdata/happy/policy/content/.manifest new file mode 100644 index 000000000000..a5a92a163963 --- /dev/null +++ b/pkg/policy/testdata/happy/policy/content/.manifest @@ -0,0 +1,4 @@ +{ + "revision": "1", + "roots": ["kubernetes", "docker"] + } \ No newline at end of file diff --git a/pkg/purl/purl.go b/pkg/purl/purl.go new file mode 100644 index 000000000000..0f657650ecc2 --- /dev/null +++ b/pkg/purl/purl.go @@ -0,0 +1,530 @@ +package purl + +import ( + "fmt" + "strconv" + "strings" + + cn "github.com/google/go-containerregistry/pkg/name" + version "github.com/knqyf263/go-rpm-version" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + TypeOCI = "oci" + + // TypeK8s is a custom type for Kubernetes components in PURL. + // - namespace: The service provider such as EKS or GKE. It is not case sensitive and must be lowercased. + // Known namespaces: + // - empty (upstream) + // - eks (AWS) + // - aks (GCP) + // - gke (Azure) + // - rke (Rancher) + // - name: The k8s component name and is case sensitive. + // - version: The combined version and release of a component. + // + // Examples: + // - pkg:k8s/upstream/k8s.io%2Fapiserver@1.24.1 + // - pkg:k8s/eks/k8s.io%2Fkube-proxy@1.26.2-eksbuild.1 + TypeK8s = "k8s" + + NamespaceEKS = "eks" + NamespaceAKS = "aks" + NamespaceGKE = "gke" + NamespaceOCP = "ocp" + + TypeUnknown = "unknown" + + // Temporary type before being added in github.com/package-url/packageurl-go + // cf. https://github.com/package-url/purl-spec/issues/454 + packageurlTypeBottlerocket = "bottlerocket" +) + +type PackageURL packageurl.PackageURL + +func FromString(s string) (*PackageURL, error) { + p, err := packageurl.FromString(s) + if err != nil { + return nil, xerrors.Errorf("failed to parse purl(%s): %w", s, err) + } + + if len(p.Qualifiers) == 0 { + p.Qualifiers = nil + } + + return lo.ToPtr(PackageURL(p)), nil +} + +// nolint: gocyclo +func New(t ftypes.TargetType, metadata types.Metadata, pkg ftypes.Package) (*PackageURL, error) { + qualifiers := parseQualifier(pkg) + pkg.Epoch = 0 // we moved Epoch to qualifiers so we don't need it in version + + ptype := purlType(t) + name := pkg.Name + ver := utils.FormatVersion(pkg) + namespace := "" + subpath := "" + + switch ptype { + case packageurl.TypeRPM: + ns, qs := parseRPM(metadata.OS, pkg.Modularitylabel) + namespace = ns + qualifiers = append(qualifiers, qs...) + case packageurl.TypeDebian: + qualifiers = append(qualifiers, parseDeb(metadata.OS)...) + if metadata.OS != nil { + namespace = string(metadata.OS.Family) + } + case packageurlTypeBottlerocket: + qualifiers = append(qualifiers, packageurl.Qualifiers{ + packageurl.Qualifier{ + Key: "distro", Value: fmt.Sprintf("bottlerocket-%s", metadata.OS.Name), + }, + }...) + case packageurl.TypeCoreos: + qualifiers = append(qualifiers, packageurl.Qualifiers{ + packageurl.Qualifier{ + Key: "distro", Value: fmt.Sprintf("coreos-%s", metadata.OS.Name), + }, + }...) + case packageurl.TypeApk: + var qs packageurl.Qualifiers + name, namespace, qs = parseApk(name, metadata.OS) + qualifiers = append(qualifiers, qs...) + case packageurl.TypeMaven, packageurl.TypeGradle: + namespace, name = parseMaven(name) + case packageurl.TypePyPi: + name = parsePyPI(name) + case packageurl.TypeComposer: + namespace, name = parseComposer(name) + case packageurl.TypeGolang: + namespace, name = parseGolang(name) + if name == "" { + return nil, nil + } + case packageurl.TypeNPM: + namespace, name = parseNpm(name) + case packageurl.TypeSwift: + namespace, name = parseSwift(name) + case packageurl.TypeCocoapods: + name, subpath = parseCocoapods(name) + case packageurl.TypeOCI: + purl, err := parseOCI(metadata) + if err != nil { + return nil, err + } else if purl == nil { + return nil, nil + } + return (*PackageURL)(purl), nil + case packageurl.TypeJulia: + var qs packageurl.Qualifiers + namespace, name, qs = parseJulia(name, pkg.ID) // for Julia, the ID is set to the package UUID + qualifiers = append(qualifiers, qs...) + } + + return (*PackageURL)(packageurl.NewPackageURL(ptype, namespace, name, ver, qualifiers, subpath)), nil +} + +func (p *PackageURL) Unwrap() *packageurl.PackageURL { + if p == nil { + return nil + } + purl := (*packageurl.PackageURL)(p) + if len(purl.Qualifiers) == 0 { + purl.Qualifiers = nil + } + return purl +} + +// LangType returns an application type in Trivy +// nolint: gocyclo +func (p *PackageURL) LangType() ftypes.LangType { + switch p.Type { + case packageurl.TypeComposer: + return ftypes.Composer + case packageurl.TypeMaven: + return ftypes.Jar + case packageurl.TypeGem: + return ftypes.GemSpec + case packageurl.TypeConda: + return ftypes.CondaPkg + case packageurl.TypePyPi: + return ftypes.PythonPkg + case packageurl.TypeGolang: + return ftypes.GoBinary + case packageurl.TypeNPM: + return ftypes.NodePkg + case packageurl.TypeCargo: + return ftypes.Cargo + case packageurl.TypeNuget: + return ftypes.NuGet + case packageurl.TypeSwift: + return ftypes.Swift + case packageurl.TypeCocoapods: + return ftypes.Cocoapods + case packageurl.TypeHex: + return ftypes.Hex + case packageurl.TypeConan: + return ftypes.Conan + case packageurl.TypePub: + return ftypes.Pub + case packageurl.TypeBitnami: + return ftypes.Bitnami + case TypeK8s: + switch p.Namespace { + case NamespaceEKS: + return ftypes.EKS + case NamespaceGKE: + return ftypes.GKE + case NamespaceAKS: + return ftypes.AKS + case NamespaceOCP: + return ftypes.OCP + case "": + return ftypes.K8sUpstream + } + return TypeUnknown + default: + return TypeUnknown + } +} + +func (p *PackageURL) Class() types.ResultClass { + switch p.Type { + case packageurl.TypeApk, packageurl.TypeDebian, packageurl.TypeRPM: + // OS packages + return types.ClassOSPkg + default: + if p.LangType() == TypeUnknown { + return types.ClassUnknown + } + // Language-specific packages + return types.ClassLangPkg + } +} + +func (p *PackageURL) Package() *ftypes.Package { + pkgName := p.Name + if p.Namespace != "" && p.Class() != types.ClassOSPkg { + if p.Type == packageurl.TypeMaven || p.Type == packageurl.TypeGradle { + // Maven and Gradle packages separate ":" + // e.g. org.springframework:spring-core + pkgName = p.Namespace + ":" + p.Name + } else { + pkgName = p.Namespace + "/" + p.Name + } + } + + // CocoaPods purl has no namespace, but has subpath + // https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#cocoapods + if p.Subpath != "" && p.Type == packageurl.TypeCocoapods { + // CocoaPods uses / format for package name + // e.g. `pkg:cocoapods/GoogleUtilities@7.5.2#NSData+zlib` => `GoogleUtilities/NSData+zlib` + pkgName = p.Name + "/" + p.Subpath + } + + pkg := &ftypes.Package{ + ID: dependency.ID(p.LangType(), pkgName, p.Version), + Name: pkgName, + Version: p.Version, + Identifier: ftypes.PkgIdentifier{ + PURL: p.Unwrap(), + }, + } + for _, q := range p.Qualifiers { + switch q.Key { + case "arch": + pkg.Arch = q.Value + case "modularitylabel": + pkg.Modularitylabel = q.Value + case "epoch": + epoch, err := strconv.Atoi(q.Value) + if err == nil { + pkg.Epoch = epoch + } + } + } + + if p.Type == packageurl.TypeRPM { + rpmVer := version.NewVersion(p.Version) + pkg.Release = rpmVer.Release() + pkg.Version = rpmVer.Version() + } + + return pkg +} + +// Match returns true if the given PURL "target" satisfies the constraint PURL "p". +// - If the constraint does not have a version, it will match any version in the target. +// - If the constraint has qualifiers, the target must have the same set of qualifiers to match. +func (p *PackageURL) Match(target *packageurl.PackageURL) bool { + if target == nil { + return false + } + switch { + case p.Type != target.Type: + return false + case p.Namespace != target.Namespace: + return false + case p.Name != target.Name: + return false + case p.Version != "" && p.Version != target.Version: + return false + case p.Subpath != "" && p.Subpath != target.Subpath: + return false + } + + // All qualifiers in the constraint must be in the target to match + q := target.Qualifiers.Map() + for k, v1 := range p.Qualifiers.Map() { + if v2, ok := q[k]; !ok || v1 != v2 { + return false + } + } + return true +} + +func (p *PackageURL) String() string { + return p.Unwrap().String() +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#oci +func parseOCI(metadata types.Metadata) (*packageurl.PackageURL, error) { + if len(metadata.RepoDigests) == 0 { + return nil, nil + } + + digest, err := cn.NewDigest(metadata.RepoDigests[0]) + if err != nil { + return nil, xerrors.Errorf("failed to parse digest: %w", err) + } + + name := strings.ToLower(digest.RepositoryStr()) + index := strings.LastIndex(name, "/") + if index != -1 { + name = name[index+1:] + } + + var qualifiers packageurl.Qualifiers + if repoURL := digest.Repository.Name(); repoURL != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "repository_url", + Value: repoURL, + }) + } + if arch := metadata.ImageConfig.Architecture; arch != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "arch", + Value: metadata.ImageConfig.Architecture, + }) + } + + return packageurl.NewPackageURL(packageurl.TypeOCI, "", name, digest.DigestStr(), qualifiers, ""), nil +} + +// ref. https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#apk +func parseApk(pkgName string, fos *ftypes.OS) (string, string, packageurl.Qualifiers) { + // the name must be lowercase + pkgName = strings.ToLower(pkgName) + + if fos == nil { + return pkgName, "", nil + } + + // the namespace must be lowercase + ns := strings.ToLower(string(fos.Family)) + qs := packageurl.Qualifiers{ + { + Key: "distro", + Value: fos.Name, + }, + } + + return pkgName, ns, qs +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#deb +func parseDeb(fos *ftypes.OS) packageurl.Qualifiers { + + if fos == nil { + return packageurl.Qualifiers{} + } + + distro := fmt.Sprintf("%s-%s", fos.Family, fos.Name) + return packageurl.Qualifiers{ + { + Key: "distro", + Value: distro, + }, + } +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#rpm +func parseRPM(fos *ftypes.OS, modularityLabel string) (string, packageurl.Qualifiers) { + if fos == nil { + return "", packageurl.Qualifiers{} + } + + qualifiers := packageurl.Qualifiers{ + { + Key: "distro", + Value: fmt.Sprintf("%s-%s", fos.Family, fos.Name), + }, + } + + if modularityLabel != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "modularitylabel", + Value: modularityLabel, + }) + } + return fos.Family.PurlNamespace(), qualifiers +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#maven +func parseMaven(pkgName string) (string, string) { + // The group id is the "namespace" and the artifact id is the "name". + name := strings.ReplaceAll(pkgName, ":", "/") + return parsePkgName(name) +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#golang +func parseGolang(pkgName string) (string, string) { + // The PURL will be skipped when the package name is a local path, since it can't identify a software package. + if strings.HasPrefix(pkgName, "./") || strings.HasPrefix(pkgName, "../") { + return "", "" + } + name := strings.ToLower(pkgName) + return parsePkgName(name) +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#pypi +func parsePyPI(pkgName string) string { + // PyPi treats - and _ as the same character and is not case-sensitive. + // Therefore a Pypi package name must be lowercased and underscore "_" replaced with a dash "-". + return strings.ToLower(strings.ReplaceAll(pkgName, "_", "-")) +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#composer +func parseComposer(pkgName string) (string, string) { + return parsePkgName(pkgName) +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#swift +func parseSwift(pkgName string) (string, string) { + return parsePkgName(pkgName) +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#cocoapods +func parseCocoapods(pkgName string) (string, string) { + var subpath string + pkgName, subpath, _ = strings.Cut(pkgName, "/") + return pkgName, subpath +} + +// ref. https://github.com/package-url/purl-spec/blob/a748c36ad415c8aeffe2b8a4a5d8a50d16d6d85f/PURL-TYPES.rst#npm +func parseNpm(pkgName string) (string, string) { + // the name must be lowercased + name := strings.ToLower(pkgName) + return parsePkgName(name) +} + +// ref. https://github.com/package-url/purl-spec/blob/7759d1cf81629267742eeeb0cdfccf5ebd624cc5/PURL-TYPES.rst#julia +func parseJulia(pkgName, pkgUUID string) (string, string, packageurl.Qualifiers) { + namespace, name := parsePkgName(pkgName) + qualifiers := packageurl.Qualifiers{ + { + Key: "uuid", + Value: pkgUUID, + }, + } + return namespace, name, qualifiers +} + +// nolint: gocyclo +func purlType(t ftypes.TargetType) string { + switch t { + case ftypes.Jar, ftypes.Pom, ftypes.Gradle, ftypes.Sbt: + return packageurl.TypeMaven + case ftypes.Bundler, ftypes.GemSpec: + return packageurl.TypeGem + case ftypes.NuGet, ftypes.DotNetCore, ftypes.PackagesProps: + return packageurl.TypeNuget + case ftypes.Composer, ftypes.ComposerVendor: + return packageurl.TypeComposer + case ftypes.CondaPkg, ftypes.CondaEnv: + return packageurl.TypeConda + case ftypes.PythonPkg, ftypes.Pip, ftypes.Pipenv, ftypes.Poetry, ftypes.Uv: + return packageurl.TypePyPi + case ftypes.GoBinary, ftypes.GoModule: + return packageurl.TypeGolang + case ftypes.Npm, ftypes.NodePkg, ftypes.Yarn, ftypes.Pnpm, ftypes.Bun: + return packageurl.TypeNPM + case ftypes.Cocoapods: + return packageurl.TypeCocoapods + case ftypes.Swift: + return packageurl.TypeSwift + case ftypes.Hex: + return packageurl.TypeHex + case ftypes.Conan: + return packageurl.TypeConan + case ftypes.Pub: + return packageurl.TypePub + case ftypes.RustBinary, ftypes.Cargo: + return packageurl.TypeCargo + case ftypes.Alpine, ftypes.Chainguard, ftypes.Wolfi, ftypes.MinimOS: + return packageurl.TypeApk + case ftypes.Debian, ftypes.Ubuntu, ftypes.Echo: + return packageurl.TypeDebian + case ftypes.RedHat, ftypes.CentOS, ftypes.Rocky, ftypes.Alma, + ftypes.Amazon, ftypes.Fedora, ftypes.Oracle, ftypes.OpenSUSE, + ftypes.OpenSUSELeap, ftypes.OpenSUSETumbleweed, ftypes.SLES, ftypes.SLEMicro, ftypes.Photon, + ftypes.Azure, ftypes.CBLMariner, ftypes.CoreOS: + return packageurl.TypeRPM + case ftypes.Bottlerocket: + return packageurlTypeBottlerocket + case TypeOCI: + return packageurl.TypeOCI + case ftypes.Julia: + return packageurl.TypeJulia + } + return string(t) +} + +func parseQualifier(pkg ftypes.Package) packageurl.Qualifiers { + var qualifiers packageurl.Qualifiers + if pkg.Arch != "" { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "arch", + Value: pkg.Arch, + }) + } + if pkg.Epoch != 0 { + qualifiers = append(qualifiers, packageurl.Qualifier{ + Key: "epoch", + Value: strconv.Itoa(pkg.Epoch), + }) + } + + return qualifiers +} + +func parsePkgName(name string) (string, string) { + var namespace string + index := strings.LastIndex(name, "/") + if index != -1 { + namespace = name[:index] + name = name[index+1:] + } + return namespace, name + +} diff --git a/pkg/purl/purl_test.go b/pkg/purl/purl_test.go new file mode 100644 index 000000000000..3a888cce2c4b --- /dev/null +++ b/pkg/purl/purl_test.go @@ -0,0 +1,1039 @@ +package purl_test + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestNewPackageURL(t *testing.T) { + testCases := []struct { + name string + typ ftypes.TargetType + pkg ftypes.Package + metadata types.Metadata + want *purl.PackageURL + wantErr string + }{ + { + name: "maven package", + typ: ftypes.Jar, + pkg: ftypes.Package{ + Name: "org.springframework:spring-core", + Version: "5.3.14", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework", + Name: "spring-core", + Version: "5.3.14", + }, + }, + { + name: "gradle package", + typ: ftypes.Gradle, + pkg: ftypes.Package{ + Name: "org.springframework:spring-core", + Version: "5.3.14", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework", + Name: "spring-core", + Version: "5.3.14", + }, + }, + { + name: "sbt package", + typ: ftypes.Sbt, + pkg: ftypes.Package{ + Name: "org.typelevel:cats-core_2.12", + Version: "2.9.0", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.typelevel", + Name: "cats-core_2.12", + Version: "2.9.0", + }, + }, + { + name: "bun package", + typ: ftypes.Bun, + pkg: ftypes.Package{ + Name: "bun-types@1.2.14", + Version: "1.2.14", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "bun-types@1.2.14", + Version: "1.2.14", + }, + }, + { + name: "yarn package", + typ: ftypes.Yarn, + pkg: ftypes.Package{ + Name: "@xtuc/ieee754", + Version: "1.2.0", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeNPM, + Namespace: "@xtuc", + Name: "ieee754", + Version: "1.2.0", + }, + }, + { + name: "yarn package with non-namespace", + typ: ftypes.Yarn, + pkg: ftypes.Package{ + Name: "lodash", + Version: "4.17.21", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "lodash", + Version: "4.17.21", + }, + }, + { + name: "pnpm package", + typ: ftypes.Pnpm, + pkg: ftypes.Package{ + Name: "@xtuc/ieee754", + Version: "1.2.0", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeNPM, + Namespace: "@xtuc", + Name: "ieee754", + Version: "1.2.0", + }, + }, + { + name: "pnpm package with non-namespace", + typ: ftypes.Pnpm, + pkg: ftypes.Package{ + Name: "lodash", + Version: "4.17.21", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "lodash", + Version: "4.17.21", + }, + }, + { + name: "pypi package", + typ: ftypes.PythonPkg, + pkg: ftypes.Package{ + Name: "Django_test", + Version: "1.2.0", + }, + want: &purl.PackageURL{ + Type: packageurl.TypePyPi, + Name: "django-test", + Version: "1.2.0", + }, + }, + { + name: "conda package", + typ: ftypes.CondaPkg, + pkg: ftypes.Package{ + Name: "absl-py", + Version: "0.4.1", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeConda, + Name: "absl-py", + Version: "0.4.1", + }, + }, + { + name: "conda environment.yaml", + typ: ftypes.CondaEnv, + pkg: ftypes.Package{ + Name: "blas", + Version: "1.0", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeConda, + Name: "blas", + Version: "1.0", + }, + }, + { + name: "composer package", + typ: ftypes.Composer, + pkg: ftypes.Package{ + Name: "symfony/contracts", + Version: "v1.0.2", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "symfony", + Name: "contracts", + Version: "v1.0.2", + }, + }, + { + name: "golang package", + typ: ftypes.GoModule, + pkg: ftypes.Package{ + Name: "github.com/go-sql-driver/Mysql", + Version: "v1.5.0", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/go-sql-driver", + Name: "mysql", + Version: "v1.5.0", + }, + }, + { + name: "golang package with a local path", + typ: ftypes.GoModule, + pkg: ftypes.Package{ + Name: "./private_repos/cnrm.googlesource.com/cnrm/", + Version: "", + }, + want: nil, + }, + { + name: "hex package", + typ: ftypes.Hex, + pkg: ftypes.Package{ + ID: "bunt@0.2.0", + Name: "bunt", + Version: "0.2.0", + Locations: []ftypes.Location{ + { + StartLine: 2, + EndLine: 2, + }, + }, + }, + want: &purl.PackageURL{ + Type: packageurl.TypeHex, + Name: "bunt", + Version: "0.2.0", + }, + }, + { + name: "dart package", + typ: ftypes.Pub, + pkg: ftypes.Package{ + Name: "http", + Version: "0.13.2", + }, + want: &purl.PackageURL{ + Type: packageurl.TypePub, + Name: "http", + Version: "0.13.2", + }, + }, + { + name: "swift package", + typ: ftypes.Swift, + pkg: ftypes.Package{ + ID: "github.com/apple/swift-atomics@1.1.0", + Name: "github.com/apple/swift-atomics", + Version: "1.1.0", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeSwift, + Namespace: "github.com/apple", + Name: "swift-atomics", + Version: "1.1.0", + }, + }, + { + name: "cocoapods package", + typ: ftypes.Cocoapods, + pkg: ftypes.Package{ + ID: "GoogleUtilities/NSData+zlib@7.5.2", + Name: "GoogleUtilities/NSData+zlib", + Version: "7.5.2", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeCocoapods, + Name: "GoogleUtilities", + Version: "7.5.2", + Subpath: "NSData+zlib", + }, + }, + { + name: "rust binary", + typ: ftypes.RustBinary, + pkg: ftypes.Package{ + ID: "abomination@0.7.3", + Name: "abomination", + Version: "0.7.3", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeCargo, + Name: "abomination", + Version: "0.7.3", + }, + }, + { + name: "dotnet Packages.props", + typ: ftypes.PackagesProps, + pkg: ftypes.Package{ + ID: "Newtonsoft.Json@9.0.1", + Name: "Newtonsoft.Json", + Version: "9.0.1", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeNuget, + Name: "Newtonsoft.Json", + Version: "9.0.1", + }, + }, + { + name: "os package", + typ: ftypes.RedHat, + pkg: ftypes.Package{ + Name: "acl", + Version: "2.2.53", + Release: "1.el8", + Epoch: 1, + Arch: "aarch64", + SrcName: "acl", + SrcVersion: "2.2.53", + SrcRelease: "1.el8", + SrcEpoch: 1, + Modularitylabel: "", + }, + + metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: ftypes.RedHat, + Name: "8", + }, + }, + want: &purl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "acl", + Version: "2.2.53-1.el8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "aarch64", + }, + { + Key: "epoch", + Value: "1", + }, + { + Key: "distro", + Value: "redhat-8", + }, + }, + }, + }, + { + name: "container", + typ: purl.TypeOCI, + metadata: types.Metadata{ + RepoTags: []string{ + "cblmariner2preview.azurecr.io/base/core:2.0.20220124-amd64", + }, + RepoDigests: []string{ + "cblmariner2preview.azurecr.io/base/core@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + "cblmariner2preview.azurecr.io/base/core@sha256:016bb1f5735e43b2738cd3fd1979b62608fe1727132b2506c17ba0e1f6a6ed8a", + }, + ImageConfig: v1.ConfigFile{ + Architecture: "amd64", + }, + }, + want: &purl.PackageURL{ + Type: packageurl.TypeOCI, + Namespace: "", + Name: "core", + Version: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + Qualifiers: packageurl.Qualifiers{ + { + Key: "repository_url", + Value: "cblmariner2preview.azurecr.io/base/core", + }, + { + Key: "arch", + Value: "amd64", + }, + }, + }, + }, + { + name: "container local", + typ: purl.TypeOCI, + metadata: types.Metadata{ + RepoTags: []string{}, + RepoDigests: []string{}, + ImageConfig: v1.ConfigFile{ + Architecture: "amd64", + }, + ImageID: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + want: nil, + }, + { + name: "container with implicit registry", + typ: purl.TypeOCI, + metadata: types.Metadata{ + RepoTags: []string{ + "alpine:3.14", + "alpine:latest", + }, + RepoDigests: []string{ + "alpine:3.14@sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + "alpine:latest@sha256:016bb1f5735e43b2738cd3fd1979b62608fe1727132b2506c17ba0e1f6a6ed8a", + }, + ImageConfig: v1.ConfigFile{ + Architecture: "amd64", + }, + }, + want: &purl.PackageURL{ + Type: packageurl.TypeOCI, + Namespace: "", + Name: "alpine", + Version: "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + Qualifiers: packageurl.Qualifiers{ + { + Key: "repository_url", + Value: "index.docker.io/library/alpine", + }, + { + Key: "arch", + Value: "amd64", + }, + }, + }, + }, + { + name: "sad path", + typ: purl.TypeOCI, + metadata: types.Metadata{ + RepoTags: []string{ + "cblmariner2preview.azurecr.io/base/core:2.0.20220124-amd64", + }, + RepoDigests: []string{ + "sha256:8fe1727132b2506c17ba0e1f6a6ed8a016bb1f5735e43b2738cd3fd1979b6260", + }, + }, + wantErr: "failed to parse digest", + }, + { + name: "julia project", + typ: ftypes.Julia, + pkg: ftypes.Package{ + ID: "ade2ca70-3891-5945-98fb-dc099432e06a", + Name: "Dates", + Version: "1.9.0", + }, + want: &purl.PackageURL{ + Type: packageurl.TypeJulia, + Name: "Dates", + Version: "1.9.0", + Qualifiers: packageurl.Qualifiers{ + { + Key: "uuid", + Value: "ade2ca70-3891-5945-98fb-dc099432e06a", + }, + }, + }, + }, + { + name: "bottlerocket package", + typ: ftypes.Bottlerocket, + metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: ftypes.Bottlerocket, + Name: "1.34.0", + }, + }, + pkg: ftypes.Package{ + ID: "glibc@2.40", + Name: "glibc", + Version: "2.40", + Epoch: 1, + Arch: "x86_64", + }, + want: &purl.PackageURL{ + Type: "bottlerocket", + Name: "glibc", + Version: "2.40", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "x86_64", + }, + { + Key: "epoch", + Value: "1", + }, + { + Key: "distro", + Value: "bottlerocket-1.34.0", + }, + }, + }, + }, + { + name: "coreos package", + typ: ftypes.CoreOS, + metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: ftypes.CoreOS, + Name: "1.34.0", + }, + }, + pkg: ftypes.Package{ + ID: "glibc@2.40", + Name: "glibc", + Version: "2.40", + Epoch: 1, + Arch: "x86_64", + }, + want: &purl.PackageURL{ + Type: "rpm", + Namespace: "coreos", + Name: "glibc", + Version: "2.40", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "x86_64", + }, + { + Key: "epoch", + Value: "1", + }, + { + Key: "distro", + Value: "coreos-1.34.0", + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + packageURL, err := purl.New(tc.typ, tc.metadata, tc.pkg) + if tc.wantErr != "" { + require.ErrorContains(t, err, tc.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, packageURL, tc.name) + }) + } +} + +func TestFromString(t *testing.T) { + testCases := []struct { + name string + purl string + want purl.PackageURL + wantErr string + }{ + { + name: "happy path for maven", + purl: "pkg:maven/org.springframework/spring-core@5.0.4.RELEASE", + want: purl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework", + Version: "5.0.4.RELEASE", + Name: "spring-core", + }, + }, + { + name: "happy path for npm", + purl: "pkg:npm/bootstrap@5.0.2", + want: purl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "bootstrap", + Version: "5.0.2", + }, + }, + { + name: "happy path for coocapods", + purl: "pkg:cocoapods/GoogleUtilities@7.5.2#NSData+zlib", + want: purl.PackageURL{ + Type: packageurl.TypeCocoapods, + Name: "GoogleUtilities", + Version: "7.5.2", + Subpath: "NSData+zlib", + }, + }, + { + name: "happy path for hex", + purl: "pkg:hex/plug@1.14.0", + want: purl.PackageURL{ + Type: packageurl.TypeHex, + Name: "plug", + Version: "1.14.0", + }, + }, + { + name: "happy path for dart", + purl: "pkg:pub/http@0.13.2", + want: purl.PackageURL{ + Type: packageurl.TypePub, + Name: "http", + Version: "0.13.2", + }, + }, + { + name: "happy path for apk", + purl: "pkg:apk/alpine/alpine-baselayout@3.2.0-r16?distro=3.14.2&epoch=1", + want: purl.PackageURL{ + Type: string(analyzer.TypeApk), + Namespace: "alpine", + Name: "alpine-baselayout", + Version: "3.2.0-r16", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.14.2", + }, + { + Key: "epoch", + Value: "1", + }, + }, + }, + }, + { + name: "happy path for rpm", + purl: "pkg:rpm/redhat/containers-common@0.1.14", + want: purl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "containers-common", + Version: "0.1.14", + }, + }, + { + name: "happy path for conda", + purl: "pkg:conda/absl-py@0.4.1", + want: purl.PackageURL{ + Type: packageurl.TypeConda, + Name: "absl-py", + Version: "0.4.1", + }, + }, + { + name: "bad rpm", + purl: "pkg:rpm/redhat/a--@1.0.0", + want: purl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "a--", + Version: "1.0.0", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + pkg, err := purl.FromString(tc.purl) + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tc.want, *pkg, tc.name) + }) + } +} + +func TestPackageURL_Package(t *testing.T) { + tests := []struct { + name string + pkgURL *purl.PackageURL + wantPkg *ftypes.Package + }{ + { + name: "rpm + Qualifiers", + pkgURL: &purl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "nodejs-full-i18n", + Version: "10.21.0-3.module_el8.2.0+391+8da3adc6", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "x86_64", + }, + { + Key: "epoch", + Value: "1", + }, + { + Key: "modularitylabel", + Value: "nodejs:10:8020020200707141642:6a468ee4", + }, + { + Key: "distro", + Value: "redhat-8", + }, + }, + }, + wantPkg: &ftypes.Package{ + ID: "nodejs-full-i18n@10.21.0-3.module_el8.2.0+391+8da3adc6", + Name: "nodejs-full-i18n", + Version: "10.21.0", + Release: "3.module_el8.2.0+391+8da3adc6", + Arch: "x86_64", + Epoch: 1, + Modularitylabel: "nodejs:10:8020020200707141642:6a468ee4", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "nodejs-full-i18n", + Version: "10.21.0-3.module_el8.2.0+391+8da3adc6", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "x86_64", + }, + { + Key: "epoch", + Value: "1", + }, + { + Key: "modularitylabel", + Value: "nodejs:10:8020020200707141642:6a468ee4", + }, + { + Key: "distro", + Value: "redhat-8", + }, + }, + }, + }, + }, + }, + { + name: "composer with namespace", + pkgURL: &purl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "symfony", + Name: "contracts", + Version: "1.0.2", + }, + + wantPkg: &ftypes.Package{ + ID: "symfony/contracts@1.0.2", + Name: "symfony/contracts", + Version: "1.0.2", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "symfony", + Name: "contracts", + Version: "1.0.2", + }, + }, + }, + }, + { + name: "maven with namespace", + pkgURL: &purl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework", + Name: "spring-core", + Version: "5.0.4.RELEASE", + }, + + wantPkg: &ftypes.Package{ + ID: "org.springframework:spring-core:5.0.4.RELEASE", + Name: "org.springframework:spring-core", + Version: "5.0.4.RELEASE", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework", + Name: "spring-core", + Version: "5.0.4.RELEASE", + }, + }, + }, + }, + { + name: "cocoapods with subpath", + pkgURL: &purl.PackageURL{ + Type: packageurl.TypeCocoapods, + Version: "4.2.0", + Name: "AppCenter", + Subpath: "Analytics", + }, + + wantPkg: &ftypes.Package{ + ID: "AppCenter/Analytics@4.2.0", + Name: "AppCenter/Analytics", + Version: "4.2.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeCocoapods, + Version: "4.2.0", + Name: "AppCenter", + Subpath: "Analytics", + }, + }, + }, + }, + { + name: "bottlerocket with epoch", + pkgURL: &purl.PackageURL{ + Type: "bottlerocket", + Name: "glibc", + Version: "2.40", + Qualifiers: packageurl.Qualifiers{ + { + Key: "epoch", + Value: "1", + }, + { + Key: "distro", + Value: "bottlerocket-1.34.0", + }, + }, + }, + wantPkg: &ftypes.Package{ + ID: "glibc@2.40", + Name: "glibc", + Version: "2.40", + Epoch: 1, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: "bottlerocket", + Name: "glibc", + Version: "2.40", + Qualifiers: packageurl.Qualifiers{ + { + Key: "epoch", + Value: "1", + }, + { + Key: "distro", + Value: "bottlerocket-1.34.0", + }, + }, + }, + }, + }, + }, + { + name: "coreos with epoch", + pkgURL: &purl.PackageURL{ + Type: "rpm", + Namespace: "coreos", + Name: "glibc", + Version: "2.40", + Qualifiers: packageurl.Qualifiers{ + { + Key: "epoch", + Value: "1", + }, + { + Key: "distro", + Value: "coreos-1.34.0", + }, + }, + }, + wantPkg: &ftypes.Package{ + ID: "glibc@2.40", + Name: "glibc", + Version: "2.40", + Epoch: 1, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: "rpm", + Namespace: "coreos", + Name: "glibc", + Version: "2.40", + Qualifiers: packageurl.Qualifiers{ + { + Key: "epoch", + Value: "1", + }, + { + Key: "distro", + Value: "coreos-1.34.0", + }, + }, + }, + }, + }, + }, + { + name: "wrong epoch", + pkgURL: &purl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "acl", + Version: "2.2.53-1.el8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "epoch", + Value: "wrong", + }, + }, + }, + wantPkg: &ftypes.Package{ + ID: "acl@2.2.53-1.el8", + Name: "acl", + Version: "2.2.53", + Release: "1.el8", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "redhat", + Name: "acl", + Version: "2.2.53-1.el8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "epoch", + Value: "wrong", + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.pkgURL.Package() + assert.Equal(t, tt.wantPkg, got) + }) + } +} + +func TestPackageURL_LangType(t *testing.T) { + tests := []struct { + name string + purl packageurl.PackageURL + want ftypes.LangType + }{ + { + name: "maven", + purl: packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework", + Name: "spring-core", + Version: "5.0.4.RELEASE", + }, + want: ftypes.Jar, + }, + { + name: "k8s", + purl: packageurl.PackageURL{ + Type: purl.TypeK8s, + Name: "kubelet", + Version: "1.21.1", + }, + want: ftypes.K8sUpstream, + }, + { + name: "eks", + purl: packageurl.PackageURL{ + Type: purl.TypeK8s, + Namespace: purl.NamespaceEKS, + Name: "kubelet", + Version: "1.21.1", + }, + want: ftypes.EKS, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := purl.PackageURL(tt.purl) + assert.Equalf(t, tt.want, p.LangType(), "LangType()") + }) + } +} + +func TestPackageURL_Match(t *testing.T) { + tests := []struct { + name string + constraint string + target string + want bool + }{ + { + name: "same purl", + constraint: "pkg:golang/github.com/aquasecurity/trivy@v0.49.0", + target: "pkg:golang/github.com/aquasecurity/trivy@v0.49.0", + want: true, + }, + { + name: "different type", + constraint: "pkg:golang/github.com/aquasecurity/trivy@v0.49.0", + target: "pkg:maven/github.com/aquasecurity/trivy@0.49.0", + want: false, + }, + { + name: "different namespace", + constraint: "pkg:golang/github.com/aquasecurity/trivy@v0.49.0", + target: "pkg:golang/github.com/aquasecurity2/trivy@v.49.0", + want: false, + }, + { + name: "different name", + constraint: "pkg:golang/github.com/aquasecurity/trivy@v0.49.0", + target: "pkg:golang/github.com/aquasecurity/tracee@v0.49.0", + want: false, + }, + { + name: "different version", + constraint: "pkg:golang/github.com/aquasecurity/trivy@v0.49.0", + target: "pkg:golang/github.com/aquasecurity/trivy@v0.49.1", + want: false, + }, + { + name: "version wildcard", + constraint: "pkg:golang/github.com/aquasecurity/trivy", + target: "pkg:golang/github.com/aquasecurity/trivy@v0.50.0", + want: true, + }, + { + name: "different qualifier", + constraint: "pkg:bitnami/wordpress@6.2.0?arch=arm64&distro=debian-12", + target: "pkg:bitnami/wordpress@6.2.0?arch=arm64&distro=debian-13", + want: false, + }, + { + name: "target more qualifiers", + constraint: "pkg:bitnami/wordpress@6.2.0?arch=arm64", + target: "pkg:bitnami/wordpress@6.2.0?arch=arm64&distro=debian-13", + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c, err := purl.FromString(tt.constraint) + require.NoError(t, err) + + p, err := purl.FromString(tt.target) + require.NoError(t, err) + + assert.Equalf(t, tt.want, c.Match(p.Unwrap()), "Match()") + }) + } +} diff --git a/pkg/rekor/client.go b/pkg/rekor/client.go new file mode 100644 index 000000000000..c6390f9679db --- /dev/null +++ b/pkg/rekor/client.go @@ -0,0 +1,135 @@ +package rekor + +import ( + "context" + "fmt" + "slices" + + pkgclient "github.com/sigstore/rekor/pkg/client" + "github.com/sigstore/rekor/pkg/generated/client" + eclient "github.com/sigstore/rekor/pkg/generated/client/entries" + "github.com/sigstore/rekor/pkg/generated/client/index" + "github.com/sigstore/rekor/pkg/generated/models" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/version/app" +) + +const ( + MaxGetEntriesLimit = 10 + + treeIDLen = 16 + uuidLen = 64 +) + +var ErrOverGetEntriesLimit = xerrors.Errorf("over get entries limit") + +// EntryID is a hex-format string. The length of the string is 80 or 64. +// If the length is 80, it consists of two elements, the TreeID and the UUID. If the length is 64, +// it consists only of the UUID. +// cf. https://github.com/sigstore/rekor/blob/4923f60f4ae55ccd4baf28d182e8f55c2d8097d3/pkg/sharding/sharding.go#L25-L36 +type EntryID struct { + TreeID string + UUID string +} + +func NewEntryID(entryID string) (EntryID, error) { + switch len(entryID) { + case treeIDLen + uuidLen: + return EntryID{ + TreeID: entryID[:treeIDLen], + UUID: entryID[treeIDLen:], + }, nil + case uuidLen: + return EntryID{ + TreeID: "", + UUID: entryID, + }, nil + default: + return EntryID{}, xerrors.New("invalid Entry ID length") + } +} + +func (e EntryID) String() string { + return e.TreeID + e.UUID +} + +type Entry struct { + Statement []byte +} + +type Client struct { + *client.Rekor +} + +func NewClient(rekorURL string) (*Client, error) { + c, err := pkgclient.GetRekorClient(rekorURL, pkgclient.WithUserAgent(fmt.Sprintf("trivy/%s", app.Version()))) + if err != nil { + return nil, xerrors.Errorf("failed to create rekor client: %w", err) + } + return &Client{Rekor: c}, nil +} + +func (c *Client) Search(ctx context.Context, hash string) ([]EntryID, error) { + log.Debug("Searching index in Rekor", log.String("hash", hash)) + params := index.NewSearchIndexParamsWithContext(ctx).WithQuery(&models.SearchIndex{Hash: hash}) + resp, err := c.Index.SearchIndex(params) + if err != nil { + return nil, xerrors.Errorf("failed to search: %w", err) + } + + ids := make([]EntryID, len(resp.Payload)) + for i, id := range resp.Payload { + ids[i], err = NewEntryID(id) + if err != nil { + return nil, xerrors.Errorf("invalid entry UUID: %w", err) + } + } + + return ids, nil +} + +func (c *Client) GetEntries(ctx context.Context, entryIDs []EntryID) ([]Entry, error) { + if len(entryIDs) > MaxGetEntriesLimit { + return []Entry{}, ErrOverGetEntriesLimit + } + + ids := make([]string, len(entryIDs)) + uuids := make([]string, len(entryIDs)) + + for i, id := range entryIDs { + ids[i] = id.String() + uuids[i] = id.UUID + } + + params := eclient.NewSearchLogQueryParamsWithContext(ctx).WithEntry(&models.SearchLogQuery{ + EntryUUIDs: ids, + }) + + resp, err := c.Entries.SearchLogQuery(params) + if err != nil { + return []Entry{}, xerrors.Errorf("failed to fetch log entries by UUIDs: %w", err) + } + + entries := make([]Entry, 0, len(resp.Payload)) + for _, payload := range resp.Payload { + for id, entry := range payload { + eid, err := NewEntryID(id) + if err != nil { + return []Entry{}, xerrors.Errorf("failed to parse response entryID: %w", err) + } + + if !slices.Contains(uuids, eid.UUID) { + continue + } + + if entry.Attestation == nil { + continue + } + entries = append(entries, Entry{Statement: entry.Attestation.Data}) + } + } + + return entries, nil +} diff --git a/pkg/rekor/client_test.go b/pkg/rekor/client_test.go new file mode 100644 index 000000000000..04299e9bebef --- /dev/null +++ b/pkg/rekor/client_test.go @@ -0,0 +1,170 @@ +package rekor_test + +import ( + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/rekor" +) + +func TestClient_Search(t *testing.T) { + type args struct { + hash string + } + tests := []struct { + name string + mockResponseFile string + args args + want []rekor.EntryID + wantErr string + }{ + { + name: "happy path", + mockResponseFile: "testdata/search-response.json", + args: args{ + hash: "92251458088c638061cda8fd8b403b76d661a4dc6b7ee71b6affcf1872557b2b", + }, + want: []rekor.EntryID{ + { + TreeID: "392f8ecba72f4326", + UUID: "eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55", + }, + { + TreeID: "392f8ecba72f4326", + UUID: "414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523", + }, + { + TreeID: "", + UUID: "414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a012345", + }, + }, + }, + { + name: "invalid UUID", + mockResponseFile: "testdata/search-invalid-response.json", + args: args{ + hash: "92251458088c638061cda8fd8b403b76d661a4dc6b7ee71b6affcf1872557b2b", + }, + wantErr: "invalid entry UUID", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.UserAgent(), "trivy/") { + t.Fatalf("User-Agent header was not specified") + } + http.ServeFile(w, r, tt.mockResponseFile) + return + })) + defer ts.Close() + + c, err := rekor.NewClient(ts.URL) + require.NoError(t, err) + + got, err := c.Search(t.Context(), tt.args.hash) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestClient_GetEntries(t *testing.T) { + type args struct { + uuids []rekor.EntryID + } + tests := []struct { + name string + mockResponseFile string + args args + want []rekor.Entry + wantErr error + }{ + { + name: "happy path", + mockResponseFile: "testdata/log-entries.json", + args: args{ + uuids: []rekor.EntryID{ + { + TreeID: "392f8ecba72f4326", + UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b1e", + }, + { + TreeID: "392f8ecba72f4326", + UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a741a2f", + }, + }, + }, + want: []rekor.Entry{ + { + Statement: []byte(`{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"cosign.sigstore.dev/attestation/v1","subject":[{"name":"ghcr.io/aquasecurity/trivy-test-images","digest":{"sha256":"20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062"}}],"predicate":{"Data":"\"foo\\n\"\n","Timestamp":"2022-08-26T01:17:17Z"}}`), + }, + { + Statement: []byte(`{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"cosign.sigstore.dev/attestation/v1","subject":[{"name":"ghcr.io/aquasecurity/trivy-test-images","digest":{"sha256":"20d3f693dcffa44d6b24eae88783324d25cc132c22089f70e4fbfb858625b062"}}],"predicate":{"Data":"\"bar\\n\"\n","Timestamp":"2022-08-26T01:17:17Z"}}`), + }, + }, + }, + { + name: "no attestation", + mockResponseFile: "testdata/log-entries-no-attestation.json", + args: args{ + uuids: []rekor.EntryID{ + { + TreeID: "392f8ecba72f4326", + UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b1e", + }, + }, + }, + want: []rekor.Entry{}, + }, + { + name: "over get entries limit", + args: args{ + uuids: []rekor.EntryID{ + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b10"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b11"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b12"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b13"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b14"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b15"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b16"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b17"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b18"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b19"}, + {TreeID: "392f8ecba72f4326", UUID: "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b1a"}, + }, + }, + want: []rekor.Entry{}, + wantErr: rekor.ErrOverGetEntriesLimit, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.UserAgent(), "trivy/") { + t.Fatalf("User-Agent header was not specified") + } + http.ServeFile(w, r, tt.mockResponseFile) + return + })) + defer ts.Close() + + client, err := rekor.NewClient(ts.URL) + require.NoError(t, err) + + got, err := client.GetEntries(t.Context(), tt.args.uuids) + require.Equal(t, tt.wantErr, err) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/rekor/testdata/log-entries-no-attestation.json b/pkg/rekor/testdata/log-entries-no-attestation.json new file mode 100644 index 000000000000..d35a5d97eb45 --- /dev/null +++ b/pkg/rekor/testdata/log-entries-no-attestation.json @@ -0,0 +1,32 @@ +[ + { + "8b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b1e": { + "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", + "integratedTime": 1661476639, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + "logIndex": 3280165, + "verification": { + "inclusionProof": { + "hashes": [ + "da70e43d33aadff047c7aa4542a1c2c0f039555b4ebb75773659b246a096d983", + "8e6876b02a01bc1e0491802b967f8b490e46fc3fb5e48986c092fe648377801b", + "eaf1241ffc88cfa3bc51697a678122d9425258967d2975ddd43bd720aa693a42", + "9420c625e610223867a58f840505674b0b3d741c24c432505c6738f2ac4f688d", + "be1b7b409a68ebcdc48c8ab773e72008454203fa4412344f25f6b1a13cb49773", + "5950c17122cae78ec19ea5f531887b7b7aad3ce14beeac68b91f115b388725df", + "664dc6a32f46aaa70be3e2206606890bebc928d4e876c405eade9a244778626e", + "48d515eeab9e86cbab194944afbc744e4c589c7b6701f1d635b70d180e0cfa3d", + "296ac93f733e66cf78544a1412c7724f6fd32ad1aeeff6359c89a5047d0093bc", + "ba20fa75d6d10494608f7716384ae46d62968d7a2ac0d1d49101e3b949d38c90", + "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b", + "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b" + ], + "logIndex": 3280165, + "rootHash": "d714a81604a4a6ea1a6a485296b112b559eea0b6b93580afcb7d382a5944f03f", + "treeSize": 3280179 + }, + "signedEntryTimestamp": "MEQCICXqUEWZzu0q2tk89u7hEBIKCxmRTQGmH+DRcwvdZoPkAiBJEbBsMdLtTWxxg8XNrJ6bRH2QskAJsKnzsgBjAsAo9A==" + } + } + } +] diff --git a/pkg/rekor/testdata/log-entries.json b/pkg/rekor/testdata/log-entries.json new file mode 100644 index 000000000000..45ec42a175ad --- /dev/null +++ b/pkg/rekor/testdata/log-entries.json @@ -0,0 +1,68 @@ +[ + { + "392f8ecba72f43268b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a742b1e": { + "attestation": { + "data": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjb3NpZ24uc2lnc3RvcmUuZGV2L2F0dGVzdGF0aW9uL3YxIiwic3ViamVjdCI6W3sibmFtZSI6ImdoY3IuaW8vYXF1YXNlY3VyaXR5L3RyaXZ5LXRlc3QtaW1hZ2VzIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjIwZDNmNjkzZGNmZmE0NGQ2YjI0ZWFlODg3ODMzMjRkMjVjYzEzMmMyMjA4OWY3MGU0ZmJmYjg1ODYyNWIwNjIifX1dLCJwcmVkaWNhdGUiOnsiRGF0YSI6IlwiZm9vXFxuXCJcbiIsIlRpbWVzdGFtcCI6IjIwMjItMDgtMjZUMDE6MTc6MTdaIn19" + }, + "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", + "integratedTime": 1661476639, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + "logIndex": 3280165, + "verification": { + "inclusionProof": { + "hashes": [ + "da70e43d33aadff047c7aa4542a1c2c0f039555b4ebb75773659b246a096d983", + "8e6876b02a01bc1e0491802b967f8b490e46fc3fb5e48986c092fe648377801b", + "eaf1241ffc88cfa3bc51697a678122d9425258967d2975ddd43bd720aa693a42", + "9420c625e610223867a58f840505674b0b3d741c24c432505c6738f2ac4f688d", + "be1b7b409a68ebcdc48c8ab773e72008454203fa4412344f25f6b1a13cb49773", + "5950c17122cae78ec19ea5f531887b7b7aad3ce14beeac68b91f115b388725df", + "664dc6a32f46aaa70be3e2206606890bebc928d4e876c405eade9a244778626e", + "48d515eeab9e86cbab194944afbc744e4c589c7b6701f1d635b70d180e0cfa3d", + "296ac93f733e66cf78544a1412c7724f6fd32ad1aeeff6359c89a5047d0093bc", + "ba20fa75d6d10494608f7716384ae46d62968d7a2ac0d1d49101e3b949d38c90", + "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b", + "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b" + ], + "logIndex": 3280165, + "rootHash": "d714a81604a4a6ea1a6a485296b112b559eea0b6b93580afcb7d382a5944f03f", + "treeSize": 3280179 + }, + "signedEntryTimestamp": "MEQCICXqUEWZzu0q2tk89u7hEBIKCxmRTQGmH+DRcwvdZoPkAiBJEbBsMdLtTWxxg8XNrJ6bRH2QskAJsKnzsgBjAsAo9A==" + } + } + }, + { + "392f8ecba72f43268b5b2debb565fd5cb05ae0d3935351fa3faabce558bede72e197b5722a741a2f": { + "attestation": { + "data": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjb3NpZ24uc2lnc3RvcmUuZGV2L2F0dGVzdGF0aW9uL3YxIiwic3ViamVjdCI6W3sibmFtZSI6ImdoY3IuaW8vYXF1YXNlY3VyaXR5L3RyaXZ5LXRlc3QtaW1hZ2VzIiwiZGlnZXN0Ijp7InNoYTI1NiI6IjIwZDNmNjkzZGNmZmE0NGQ2YjI0ZWFlODg3ODMzMjRkMjVjYzEzMmMyMjA4OWY3MGU0ZmJmYjg1ODYyNWIwNjIifX1dLCJwcmVkaWNhdGUiOnsiRGF0YSI6IlwiYmFyXFxuXCJcbiIsIlRpbWVzdGFtcCI6IjIwMjItMDgtMjZUMDE6MTc6MTdaIn19" + }, + "body": "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", + "integratedTime": 1661476639, + "logID": "c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d", + "logIndex": 3280165, + "verification": { + "inclusionProof": { + "hashes": [ + "da70e43d33aadff047c7aa4542a1c2c0f039555b4ebb75773659b246a096d983", + "8e6876b02a01bc1e0491802b967f8b490e46fc3fb5e48986c092fe648377801b", + "eaf1241ffc88cfa3bc51697a678122d9425258967d2975ddd43bd720aa693a42", + "9420c625e610223867a58f840505674b0b3d741c24c432505c6738f2ac4f688d", + "be1b7b409a68ebcdc48c8ab773e72008454203fa4412344f25f6b1a13cb49773", + "5950c17122cae78ec19ea5f531887b7b7aad3ce14beeac68b91f115b388725df", + "664dc6a32f46aaa70be3e2206606890bebc928d4e876c405eade9a244778626e", + "48d515eeab9e86cbab194944afbc744e4c589c7b6701f1d635b70d180e0cfa3d", + "296ac93f733e66cf78544a1412c7724f6fd32ad1aeeff6359c89a5047d0093bc", + "ba20fa75d6d10494608f7716384ae46d62968d7a2ac0d1d49101e3b949d38c90", + "6d494b237648126525b08f975c736a55d1f7a64472fcc2782bbc16733c608d7b", + "efb36cfc54705d8cd921a621a9389ffa03956b15d68bfabadac2b4853852079b" + ], + "logIndex": 3280165, + "rootHash": "d714a81604a4a6ea1a6a485296b112b559eea0b6b93580afcb7d382a5944f03f", + "treeSize": 3280179 + }, + "signedEntryTimestamp": "MEQCICXqUEWZzu0q2tk89u7hEBIKCxmRTQGmH+DRcwvdZoPkAiBJEbBsMdLtTWxxg8XNrJ6bRH2QskAJsKnzsgBjAsAo9A==" + } + } + } +] diff --git a/pkg/rekor/testdata/search-invalid-response.json b/pkg/rekor/testdata/search-invalid-response.json new file mode 100644 index 000000000000..18ae148b6f1c --- /dev/null +++ b/pkg/rekor/testdata/search-invalid-response.json @@ -0,0 +1,4 @@ +[ + "foo", + "bar" +] diff --git a/pkg/rekor/testdata/search-response.json b/pkg/rekor/testdata/search-response.json new file mode 100644 index 000000000000..3607ce13df56 --- /dev/null +++ b/pkg/rekor/testdata/search-response.json @@ -0,0 +1,5 @@ +[ + "392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55", + "392f8ecba72f4326414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523", + "414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a012345" +] diff --git a/pkg/rekortest/server.go b/pkg/rekortest/server.go new file mode 100644 index 000000000000..3c818a7a72fc --- /dev/null +++ b/pkg/rekortest/server.go @@ -0,0 +1,366 @@ +package rekortest + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/in-toto/in-toto-golang/in_toto" + slsa "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" + "github.com/samber/lo" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/attestation" +) + +var ( + indexRes = map[string][]string{ + // Contain a SBOM attestation for a container image + "sha256:782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02": { + "392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55", + "392f8ecba72f4326414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523", + }, + // Contain a SBOM attestation for go.mod + "sha256:23f4e10c43c7654e33a3c9570913c8c9c528292762f1a5c4a97253e9e4e4b238": { + "24296fb24b8ad77aa715cdfd264ce34c4d544375d7bd7cd029bf5a48ef25217a13fdba562e0889ca", + }, + // Contain an empty SBOM attestation + "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03": { + "24296fb24b8ad77a8d47be2e40bfe910f0ffc842e86b5685dd85d1c903ef78bb6362125816426fe9", + }, + } + + imageSBOMAttestation = in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: "https://in-toto.io/Statement/v0.1", + PredicateType: "https://cyclonedx.org/bom", + Subject: []in_toto.Subject{ + { + Name: "index.docker.io/knqyf263/cosign-test", + Digest: slsa.DigestSet{ + "sha256": "a777c9c66ba177ccfea23f2a216ff6721e78a662cd17019488c417135299cd89", + }, + }, + }, + }, + Predicate: &attestation.CosignPredicate{ + Data: &cyclonedx.BOM{ + BOMFormat: cyclonedx.BOMFormat, + SerialNumber: "urn:uuid:6453fd82-71f4-47c8-ad12-01775619c443", + SpecVersion: cyclonedx.SpecVersion1_5, + Version: 1, + Metadata: &cyclonedx.Metadata{ + Timestamp: "2022-09-15T13:53:49+00:00", + Tools: &cyclonedx.ToolsChoice{ + Components: &[]cyclonedx.Component{ + { + Type: cyclonedx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + }, + }, + }, + Component: &cyclonedx.Component{ + BOMRef: "pkg:oci/alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad?repository_url=index.docker.io%2Flibrary%2Falpine\u0026arch=amd64", + Type: cyclonedx.ComponentTypeContainer, + Name: "alpine:3.16", + PackageURL: "pkg:oci/alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad?repository_url=index.docker.io%2Flibrary%2Falpine\u0026arch=amd64", + Properties: &[]cyclonedx.Property{ + { + Name: "aquasecurity:trivy:SchemaVersion", + Value: "2", + }, + { + Name: "aquasecurity:trivy:ImageID", + Value: "sha256:9c6f0724472873bb50a2ae67a9e7adcb57673a183cea8b06eb778dca859181b5", + }, + { + Name: "aquasecurity:trivy:RepoDigest", + Value: "alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad", + }, + { + Name: "aquasecurity:trivy:DiffID", + Value: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7", + }, + { + Name: "aquasecurity:trivy:RepoTag", + Value: "alpine:3.16", + }, + }, + }, + }, + Components: &[]cyclonedx.Component{ + { + BOMRef: "fad4eb97-3d2a-4499-ace7-2c94444148a7", + Type: cyclonedx.ComponentTypeOS, + Name: "alpine", + Version: "3.16.2", + Properties: &[]cyclonedx.Property{ + { + Name: "aquasecurity:trivy:Type", + Value: "alpine", + }, + { + Name: "aquasecurity:trivy:Class", + Value: "os-pkgs", + }, + }, + }, + { + BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2", + Type: cyclonedx.ComponentTypeLibrary, + Name: "musl", + Version: "1.2.3-r0", + Licenses: &cyclonedx.Licenses{ + {Expression: "MIT"}, + }, + PackageURL: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2", + Properties: &[]cyclonedx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "alpine", + }, + { + Name: "aquasecurity:trivy:SrcName", + Value: "musl", + }, + { + Name: "aquasecurity:trivy:SrcVersion", + Value: "1.2.3-r0", + }, + { + Name: "aquasecurity:trivy:LayerDiffID", + Value: "sha256:994393dc58e7931862558d06e46aa2bb17487044f670f310dffe1d24e4d1eec7", + }, + }, + }, + }, + Dependencies: &[]cyclonedx.Dependency{ + { + Ref: "pkg:oci/alpine@sha256:bc41182d7ef5ffc53a40b044e725193bc10142a1243f395ee852a8d9730fc2ad?repository_url=index.docker.io%2Flibrary%2Falpine&arch=amd64", + Dependencies: &[]string{ + "fad4eb97-3d2a-4499-ace7-2c94444148a7", + }, + }, + { + Ref: "fad4eb97-3d2a-4499-ace7-2c94444148a7", + Dependencies: &[]string{ + "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.2", + }, + }, + }, + }, + }, + } + + gomodSBOMAttestation = in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: "https://in-toto.io/Statement/v0.1", + PredicateType: "https://cyclonedx.org/bom", + Subject: []in_toto.Subject{ + { + Name: "go.mod", + Digest: slsa.DigestSet{ + "sha256": "23f4e10c43c7654e33a3c9570913c8c9c528292762f1a5c4a97253e9e4e4b238", + }, + }, + }, + }, + Predicate: &attestation.CosignPredicate{ + Data: &cyclonedx.BOM{ + BOMFormat: cyclonedx.BOMFormat, + SerialNumber: "urn:uuid:8b16c9a3-e957-4c85-b43d-7dd05ea0421c", + SpecVersion: cyclonedx.SpecVersion1_5, + Version: 1, + Metadata: &cyclonedx.Metadata{ + Timestamp: "2022-10-21T09:50:08+00:00", + Tools: &cyclonedx.ToolsChoice{ + Components: &[]cyclonedx.Component{ + { + Type: cyclonedx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + }, + }, + }, + Component: &cyclonedx.Component{ + BOMRef: "ef8385d7-a56f-495a-a220-7b0a2e940d39", + Type: cyclonedx.ComponentTypeApplication, + Name: "go.mod", + Properties: &[]cyclonedx.Property{ + { + Name: "aquasecurity:trivy:SchemaVersion", + Value: "2", + }, + }, + }, + }, + Components: &[]cyclonedx.Component{ + { + BOMRef: "bb8b7541-2b08-4692-9363-8f79da5c1a31", + Type: cyclonedx.ComponentTypeApplication, + Name: "go.mod", + Properties: &[]cyclonedx.Property{ + { + Name: "aquasecurity:trivy:Type", + Value: "gomod", + }, + { + Name: "aquasecurity:trivy:Class", + Value: "lang-pkgs", + }, + }, + }, + { + BOMRef: "pkg:golang/github.com/spf13/cobra@1.5.0", + Type: cyclonedx.ComponentTypeLibrary, + Name: "github.com/spf13/cobra", + Version: "1.5.0", + PackageURL: "pkg:golang/github.com/spf13/cobra@1.5.0", + Properties: &[]cyclonedx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "gomod", + }, + }, + }, + }, + Dependencies: &[]cyclonedx.Dependency{ + { + Ref: "ef8385d7-a56f-495a-a220-7b0a2e940d39", + Dependencies: &[]string{ + "bb8b7541-2b08-4692-9363-8f79da5c1a31", + }, + }, + { + Ref: "bb8b7541-2b08-4692-9363-8f79da5c1a31", + Dependencies: &[]string{ + "pkg:golang/github.com/spf13/cobra@1.5.0", + }, + }, + }, + }, + }, + } + + emptySBOMAttestation = in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: "https://in-toto.io/Statement/v0.1", + PredicateType: "https://cyclonedx.org/bom", + }, + Predicate: &attestation.CosignPredicate{ + Data: &cyclonedx.BOM{ + BOMFormat: cyclonedx.BOMFormat, + SpecVersion: cyclonedx.SpecVersion1_5, + Version: 2, + }, + }, + } + + entries = map[string]models.LogEntryAnon{ + "392f8ecba72f4326414eaca77bd19bf5f378725d7fd79309605a81b69cc0101f5cd3119d0a216523": { + Attestation: &models.LogEntryAnonAttestation{ + Data: mustMarshal(imageSBOMAttestation), + }, + Body: "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", + IntegratedTime: lo.ToPtr(int64(1661476639)), + LogID: lo.ToPtr("c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"), + LogIndex: lo.ToPtr(int64(3280165)), + Verification: nil, + }, + "392f8ecba72f4326eb624a7403756250b5f2ad58842a99d1653cd6f147f4ce9eda2da350bd908a55": { + Attestation: &models.LogEntryAnonAttestation{ + Data: []byte(`{"apiVersion":"0.0.1","kind":"intoto","spec":{"content":{"hash":{"algorithm":"sha256","value":"782143e39f1e7a04e3f6da2d88b1c057e5657363c4f90679f3e8a071b7619e02"},"payloadHash":{"algorithm":"sha256","value":"ebbfddda6277af199e93c5bb5cf5998a79311de238e49bcc8ac24102698761bb"}},"publicKey":"LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNwRENDQWlxZ0F3SUJBZ0lVYWhsOEFRd1lZV05ZbnV6dlFvOEVrN1dMTURvd0NnWUlLb1pJemowRUF3TXcKTnpFVk1CTUdBMVVFQ2hNTWMybG5jM1J2Y21VdVpHVjJNUjR3SEFZRFZRUURFeFZ6YVdkemRHOXlaUzFwYm5SbApjbTFsWkdsaGRHVXdIaGNOTWpJd09ESTJNREV4TnpFM1doY05Nakl3T0RJMk1ERXlOekUzV2pBQU1Ga3dFd1lICktvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVLWmZEQzlpalVyclpBWE9jWFYrQXFHRUlTSlEzVHRqSndJdEEKdTE3Rml2aWpnSk1hYUhGNDcrT3Z2OVR1ekFDQ3lpSUV5UDUyZXI2ZmF5bmZKYVVqOEtPQ0FVa3dnZ0ZGTUE0RwpBMVVkRHdFQi93UUVBd0lIZ0RBVEJnTlZIU1VFRERBS0JnZ3JCZ0VGQlFjREF6QWRCZ05WSFE0RUZnUVVHQldUCkMwdUU3dTRQcURVRjFYV0c0QlVWVUpBd0h3WURWUjBqQkJnd0ZvQVUzOVBwejFZa0VaYjVxTmpwS0ZXaXhpNFkKWkQ4d0pRWURWUjBSQVFIL0JCc3dHWUVYYzJGemIyRnJhWEpoTmpFeE5FQm5iV0ZwYkM1amIyMHdLUVlLS3dZQgpCQUdEdnpBQkFRUWJhSFIwY0hNNkx5OWhZMk52ZFc1MGN5NW5iMjluYkdVdVkyOXRNSUdMQmdvckJnRUVBZFo1CkFnUUNCSDBFZXdCNUFIY0FDR0NTOENoUy8yaEYwZEZySjRTY1JXY1lyQlk5d3pqU2JlYThJZ1kyYjNJQUFBR0MKMTdtSmhnQUFCQU1BU0RCR0FpRUFoS09BSkdWVlhCb1cxTDR4alk5eWJWOGZUUXN5TStvUEpIeDk5S29LYUpVQwpJUURCZDllc1Q0Mk1STng3Vm9BM1paKzV4akhNZWR6amVxQ2ZoZTcvd1pxYTlUQUtCZ2dxaGtqT1BRUURBd05vCkFEQmxBakVBcnBkeXlFRjc3b2JyTENMUXpzYmIxM2lsNjd3dzM4Y050amdNQml6Y2VUakRiY2VLeVFSN1RKNHMKZENsclkxY1BBakE4aXB6SUQ4VU1CaGxkSmUvZXJGcGdtN2swNWFic2lPN3V5dVZuS29VNk0rTXJ6VVUrZTlGdwpJRGhCanVRa1dRYz0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo="}}`), + }, + Body: "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", + IntegratedTime: lo.ToPtr(int64(1661476639)), + LogID: lo.ToPtr("c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"), + LogIndex: lo.ToPtr(int64(3280165)), + Verification: nil, + }, + "24296fb24b8ad77aa715cdfd264ce34c4d544375d7bd7cd029bf5a48ef25217a13fdba562e0889ca": { + Attestation: &models.LogEntryAnonAttestation{ + Data: mustMarshal(gomodSBOMAttestation), + }, + Body: nil, // not used at the moment + IntegratedTime: lo.ToPtr(int64(1664451604)), + LogID: lo.ToPtr("c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"), + LogIndex: lo.ToPtr(int64(4215471)), + Verification: nil, + }, + "24296fb24b8ad77a8d47be2e40bfe910f0ffc842e86b5685dd85d1c903ef78bb6362125816426fe9": { + Attestation: &models.LogEntryAnonAttestation{ + Data: mustMarshal(emptySBOMAttestation), + }, + Body: "eyJhcGlWZXJzaW9uIjoiMC4wLjEiLCJraW5kIjoiaW50b3RvIiwic3BlYyI6eyJjb250ZW50Ijp7Imhhc2giOnsiYWxnb3JpdGhtIjoic2hhMjU2IiwidmFsdWUiOiI3ODIxNDNlMzlmMWU3YTA0ZTNmNmRhMmQ4OGIxYzA1N2U1NjU3MzYzYzRmOTA2NzlmM2U4YTA3MWI3NjE5ZTAyIn0sInBheWxvYWRIYXNoIjp7ImFsZ29yaXRobSI6InNoYTI1NiIsInZhbHVlIjoiZWJiZmRkZGE2Mjc3YWYxOTllOTNjNWJiNWNmNTk5OGE3OTMxMWRlMjM4ZTQ5YmNjOGFjMjQxMDI2OTg3NjFiYiJ9fSwicHVibGljS2V5IjoiTFMwdExTMUNSVWRKVGlCRFJWSlVTVVpKUTBGVVJTMHRMUzB0Q2sxSlNVTndSRU5EUVdseFowRjNTVUpCWjBsVllXaHNPRUZSZDFsWlYwNVpiblY2ZGxGdk9FVnJOMWRNVFVSdmQwTm5XVWxMYjFwSmVtb3dSVUYzVFhjS1RucEZWazFDVFVkQk1WVkZRMmhOVFdNeWJHNWpNMUoyWTIxVmRWcEhWakpOVWpSM1NFRlpSRlpSVVVSRmVGWjZZVmRrZW1SSE9YbGFVekZ3WW01U2JBcGpiVEZzV2tkc2FHUkhWWGRJYUdOT1RXcEpkMDlFU1RKTlJFVjRUbnBGTTFkb1kwNU5ha2wzVDBSSk1rMUVSWGxPZWtVelYycEJRVTFHYTNkRmQxbElDa3R2V2tsNmFqQkRRVkZaU1V0dldrbDZhakJFUVZGalJGRm5RVVZMV21aRVF6bHBhbFZ5Y2xwQldFOWpXRllyUVhGSFJVbFRTbEV6VkhScVNuZEpkRUVLZFRFM1JtbDJhV3BuU2sxaFlVaEdORGNyVDNaMk9WUjFla0ZEUTNscFNVVjVVRFV5WlhJMlptRjVibVpLWVZWcU9FdFBRMEZWYTNkblowWkdUVUUwUndwQk1WVmtSSGRGUWk5M1VVVkJkMGxJWjBSQlZFSm5UbFpJVTFWRlJFUkJTMEpuWjNKQ1owVkdRbEZqUkVGNlFXUkNaMDVXU0ZFMFJVWm5VVlZIUWxkVUNrTXdkVVUzZFRSUWNVUlZSakZZVjBjMFFsVldWVXBCZDBoM1dVUldVakJxUWtKbmQwWnZRVlV6T1ZCd2VqRlphMFZhWWpWeFRtcHdTMFpYYVhocE5Ga0tXa1E0ZDBwUldVUldVakJTUVZGSUwwSkNjM2RIV1VWWVl6SkdlbUl5Um5KaFdFcG9UbXBGZUU1RlFtNWlWMFp3WWtNMWFtSXlNSGRMVVZsTFMzZFpRZ3BDUVVkRWRucEJRa0ZSVVdKaFNGSXdZMGhOTmt4NU9XaFpNazUyWkZjMU1HTjVOVzVpTWpsdVlrZFZkVmt5T1hSTlNVZE1RbWR2Y2tKblJVVkJaRm8xQ2tGblVVTkNTREJGWlhkQ05VRklZMEZEUjBOVE9FTm9VeTh5YUVZd1pFWnlTalJUWTFKWFkxbHlRbGs1ZDNwcVUySmxZVGhKWjFreVlqTkpRVUZCUjBNS01UZHRTbWhuUVVGQ1FVMUJVMFJDUjBGcFJVRm9TMDlCU2tkV1ZsaENiMWN4VERSNGFsazVlV0pXT0daVVVYTjVUU3R2VUVwSWVEazVTMjlMWVVwVlF3cEpVVVJDWkRsbGMxUTBNazFTVG5nM1ZtOUJNMXBhS3pWNGFraE5aV1I2YW1WeFEyWm9aVGN2ZDFweFlUbFVRVXRDWjJkeGFHdHFUMUJSVVVSQmQwNXZDa0ZFUW14QmFrVkJjbkJrZVhsRlJqYzNiMkp5VEVOTVVYcHpZbUl4TTJsc05qZDNkek00WTA1MGFtZE5RbWw2WTJWVWFrUmlZMlZMZVZGU04xUktOSE1LWkVOc2Nsa3hZMUJCYWtFNGFYQjZTVVE0VlUxQ2FHeGtTbVV2WlhKR2NHZHROMnN3TldGaWMybFBOM1Y1ZFZadVMyOVZOazByVFhKNlZWVXJaVGxHZHdwSlJHaENhblZSYTFkUll6MEtMUzB0TFMxRlRrUWdRMFZTVkVsR1NVTkJWRVV0TFMwdExRbz0ifX0=", + IntegratedTime: lo.ToPtr(int64(1661476639)), + LogID: lo.ToPtr("c0d23d6ad406973f9559f3ba2d1ca01f84147d8ffc5b8445c224f98b9591801d"), + LogIndex: lo.ToPtr(int64(3280165)), + Verification: nil, + }, + } +) + +type Server struct { + ts *httptest.Server +} + +func NewServer(t *testing.T) *Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v1/index/retrieve": + var params models.SearchIndex + err := json.NewDecoder(r.Body).Decode(¶ms) + assert.NoError(t, err) + + if res, ok := indexRes[params.Hash]; ok { + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(res) + assert.NoError(t, err) + } else { + http.Error(w, "something wrong", http.StatusNotFound) + } + case "/api/v1/log/entries/retrieve": + var params models.SearchLogQuery + err := json.NewDecoder(r.Body).Decode(¶ms) + assert.NoError(t, err) + + resEntries := models.LogEntry{} + for _, uuid := range params.EntryUUIDs { + e, ok := entries[uuid] + if !ok { + http.Error(w, "no such uuid", http.StatusNotFound) + return + } + resEntries[uuid] = e + } + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode([]models.LogEntry{resEntries}) + assert.NoError(t, err) + } + return + })) + + return &Server{ts: ts} +} + +func (s *Server) URL() string { + return s.ts.URL +} + +func (s *Server) Close() { + s.ts.Close() +} + +func mustMarshal(v any) []byte { + b, err := json.Marshal(v) + if err != nil { + panic(err) + } + return b +} diff --git a/pkg/remic/run.go b/pkg/remic/run.go deleted file mode 100644 index ea3eac0576c1..000000000000 --- a/pkg/remic/run.go +++ /dev/null @@ -1,85 +0,0 @@ -package remic - -import ( - l "log" - "os" - "strings" - - "github.com/knqyf263/trivy/pkg/scanner" - - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/vulnsrc" - - "github.com/urfave/cli" - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/db" - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/report" -) - -func Run(c *cli.Context) (err error) { - debug := c.Bool("debug") - if err = log.InitLogger(debug); err != nil { - l.Fatal(err) - } - - args := c.Args() - if len(args) == 0 { - return xerrors.New(`remic" requires at least 1 argument.`) - } - - o := c.String("output") - output := os.Stdout - if o != "" { - if output, err = os.Create(o); err != nil { - return err - } - } - - var severities []vulnerability.Severity - for _, s := range strings.Split(c.String("severity"), ",") { - severity, err := vulnerability.NewSeverity(s) - if err != nil { - return err - } - severities = append(severities, severity) - } - - if err = db.Init(); err != nil { - return err - } - - if err = vulnsrc.Update(); err != nil { - return err - } - - fileName := args[0] - f, err := os.Open(fileName) - if err != nil { - return xerrors.Errorf("failed to open a file: %w", err) - } - defer f.Close() - - result, err := scanner.ScanFile(f, severities) - if err != nil { - return xerrors.Errorf("failed to scan a file: %w", err) - } - - var writer report.Writer - switch c.String("format") { - case "table": - writer = &report.TableWriter{Output: output} - case "json": - writer = &report.JsonWriter{Output: output} - default: - return xerrors.New("unknown format") - } - - if err = writer.Write([]report.Result{result}); err != nil { - return xerrors.Errorf("failed to write results: %w", err) - } - - return nil -} diff --git a/pkg/remote/remote.go b/pkg/remote/remote.go new file mode 100644 index 000000000000..c46f7078a96e --- /dev/null +++ b/pkg/remote/remote.go @@ -0,0 +1,271 @@ +package remote + +import ( + "context" + "errors" + "net/http" + "strings" + + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/authn/github" + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/remote" + v1types "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/hashicorp/go-multierror" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/image/registry" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" +) + +type Descriptor = remote.Descriptor + +// Get is a wrapper of google/go-containerregistry/pkg/v1/remote.Get +// so that it can try multiple authentication methods. +func Get(ctx context.Context, ref name.Reference, option types.RegistryOptions) (*Descriptor, error) { + return tryWithMirrors(ref, option, func(r name.Reference) (*Descriptor, error) { + return tryGet(ctx, xhttp.RoundTripper(ctx), r, option) + }) +} + +// tryGet checks all auth options and tries to get Descriptor. +func tryGet(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (*Descriptor, error) { + var errs error + for _, authOpt := range authOptions(ctx, ref, option) { + remoteOpts := []remote.Option{ + remote.WithTransport(tr), + authOpt, + } + + if option.Platform.Platform != nil { + p, err := resolvePlatform(ref, option.Platform, remoteOpts) + if err != nil { + return nil, xerrors.Errorf("platform error: %w", err) + } + // Don't pass platform when the specified image is single-arch. + if p.Platform != nil { + remoteOpts = append(remoteOpts, remote.WithPlatform(*p.Platform)) + } + } + + desc, err := remote.Get(ref, remoteOpts...) + if err != nil { + errs = multierror.Append(errs, err) + continue + } + + if option.Platform.Force { + if err = satisfyPlatform(desc, lo.FromPtr(option.Platform.Platform)); err != nil { + return nil, err + } + } + return desc, nil + } + return nil, errs +} + +// Image is a wrapper of google/go-containerregistry/pkg/v1/remote.Image +// so that it can try multiple authentication methods. +func Image(ctx context.Context, ref name.Reference, option types.RegistryOptions) (v1.Image, error) { + return tryWithMirrors(ref, option, func(r name.Reference) (v1.Image, error) { + return tryImage(ctx, xhttp.RoundTripper(ctx), r, option) + }) +} + +// tryWithMirrors handles common mirror logic for Get and Image functions +func tryWithMirrors[T any](ref name.Reference, option types.RegistryOptions, fn func(name.Reference) (T, error)) (T, error) { + var zero T + mirrors, err := registryMirrors(ref, option) + if err != nil { + return zero, xerrors.Errorf("unable to parse mirrors: %w", err) + } + + // Try each mirrors/host until it succeeds + var errs error + for _, r := range append(mirrors, ref) { + result, err := fn(r) + if err != nil { + var multiErr *multierror.Error + // All auth options failed, try the next mirror/host + if errors.As(err, &multiErr) { + errs = multierror.Append(errs, multiErr.Errors...) + continue + } + // Other errors + return zero, err + } + + if ref.Context().RegistryStr() != r.Context().RegistryStr() { + log.WithPrefix("remote").Info("Using the mirror registry to get the image", + log.String("image", ref.String()), log.String("mirror", r.Context().RegistryStr())) + } + return result, nil + } + + // No authentication for mirrors/host succeeded + return zero, errs +} + +// tryImage checks all auth options and tries to get v1.Image. +// If none of the auth options work - function returns multierrors for each auth option. +func tryImage(ctx context.Context, tr http.RoundTripper, ref name.Reference, option types.RegistryOptions) (v1.Image, error) { + var errs error + for _, authOpt := range authOptions(ctx, ref, option) { + remoteOpts := []remote.Option{ + remote.WithTransport(tr), + authOpt, + } + index, err := remote.Image(ref, remoteOpts...) + if err != nil { + errs = multierror.Append(errs, err) + continue + } + + return index, nil + } + return nil, errs +} + +// Referrers is a wrapper of google/go-containerregistry/pkg/v1/remote.Referrers +// so that it can try multiple authentication methods. +func Referrers(ctx context.Context, d name.Digest, option types.RegistryOptions) (v1.ImageIndex, error) { + var errs error + // Try each authentication method until it succeeds + for _, authOpt := range authOptions(ctx, d, option) { + remoteOpts := []remote.Option{ + remote.WithTransport(xhttp.RoundTripper(ctx)), + authOpt, + } + index, err := remote.Referrers(d, remoteOpts...) + if err != nil { + errs = multierror.Append(errs, err) + continue + } + return index, nil + } + + // No authentication succeeded + return nil, errs +} + +// registryMirrors returns a list of mirrors for ref, obtained from options.RegistryMirrors +// `go-containerregistry` doesn't support mirrors, so we need to handle them ourselves. +// TODO: use `WithMirror` when `go-containerregistry` will support mirrors. +// cf. https://github.com/google/go-containerregistry/pull/2010 +func registryMirrors(hostRef name.Reference, option types.RegistryOptions) ([]name.Reference, error) { + var mirrors []name.Reference + + reg := hostRef.Context().RegistryStr() + if ms, ok := option.RegistryMirrors[reg]; ok { + for _, m := range ms { + var nameOpts []name.Option + if option.Insecure { + nameOpts = append(nameOpts, name.Insecure) + } + mirrorImageName := strings.Replace(hostRef.Name(), reg, m, 1) + ref, err := name.ParseReference(mirrorImageName, nameOpts...) + if err != nil { + return nil, xerrors.Errorf("unable to parse image from mirror registry: %w", err) + } + mirrors = append(mirrors, ref) + } + } + return mirrors, nil +} + +func authOptions(ctx context.Context, ref name.Reference, option types.RegistryOptions) []remote.Option { + var opts []remote.Option + for _, cred := range option.Credentials { + opts = append(opts, remote.WithAuth(&authn.Basic{ + Username: cred.Username, + Password: cred.Password, + })) + } + + domain := ref.Context().RegistryStr() + token := registry.GetToken(ctx, domain, option) + if !lo.IsEmpty(token) { + opts = append(opts, remote.WithAuth(&token)) + } + + switch { + case option.RegistryToken != "": + bearer := authn.Bearer{Token: option.RegistryToken} + return []remote.Option{remote.WithAuth(&bearer)} + default: + // Use the keychain anyway at the end + opts = append(opts, remote.WithAuthFromKeychain(authn.NewMultiKeychain(authn.DefaultKeychain, github.Keychain))) + return opts + } +} + +// resolvePlatform resolves the OS platform for a given image reference. +// If the platform has an empty OS, the function will attempt to find the first OS +// in the image's manifest list and return the platform with the detected OS. +// It ignores the specified platform if the image is not multi-arch. +func resolvePlatform(ref name.Reference, p types.Platform, options []remote.Option) (types.Platform, error) { + if p.OS != "" { + return p, nil + } + + // OS wildcard, implicitly pick up the first os found in the image list. + // e.g. */amd64 + d, err := remote.Get(ref, options...) + if err != nil { + return types.Platform{}, xerrors.Errorf("image get error: %w", err) + } + switch d.MediaType { + case v1types.OCIManifestSchema1, v1types.DockerManifestSchema2: + // We want an index but the registry has an image, not multi-arch. We just ignore "--platform". + log.Debug("Ignore `--platform` as the image is not multi-arch") + return types.Platform{}, nil + case v1types.OCIImageIndex, v1types.DockerManifestList: + // These are expected. + } + + index, err := d.ImageIndex() + if err != nil { + return types.Platform{}, xerrors.Errorf("image index error: %w", err) + } + + m, err := index.IndexManifest() + if err != nil { + return types.Platform{}, xerrors.Errorf("remote index manifest error: %w", err) + } + if len(m.Manifests) == 0 { + log.Debug("Ignore '--platform' as the image is not multi-arch") + return types.Platform{}, nil + } + if m.Manifests[0].Platform != nil { + newPlatform := p.DeepCopy() + // Replace with the detected OS + // e.g. */amd64 => linux/amd64 + newPlatform.OS = m.Manifests[0].Platform.OS + + // Return the platform with the found OS + return types.Platform{ + Platform: newPlatform, + Force: p.Force, + }, nil + } + return types.Platform{}, nil +} + +func satisfyPlatform(desc *remote.Descriptor, platform v1.Platform) error { + img, err := desc.Image() + if err != nil { + return err + } + c, err := img.ConfigFile() + if err != nil { + return err + } + if !lo.FromPtr(c.Platform()).Satisfies(platform) { + return xerrors.Errorf("the specified platform not found") + } + return nil +} diff --git a/pkg/remote/remote_test.go b/pkg/remote/remote_test.go new file mode 100644 index 000000000000..be2541f2eb1a --- /dev/null +++ b/pkg/remote/remote_test.go @@ -0,0 +1,379 @@ +package remote + +import ( + "encoding/base64" + "fmt" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "sync" + "testing" + + "github.com/google/go-containerregistry/pkg/name" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/testdocker/auth" + "github.com/aquasecurity/testdocker/registry" + "github.com/aquasecurity/testdocker/tarfile" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/version/app" +) + +func setupPrivateRegistry(t *testing.T) *httptest.Server { + images := map[string]v1.Image{ + "v2/library/alpine:3.10": localImage(t), + } + tr := registry.NewDockerRegistry(registry.Option{ + Images: images, + Auth: auth.Auth{ + User: "test", + Password: "testpass", + Secret: "secret", + }, + }) + + tr.Config.Handler = newUserAgentsTrackingHandler(tr.Config.Handler) + return tr +} + +// setupConfigDir sets up an isolated configDir() for this test. +func setupConfigDir(t *testing.T) string { + p := t.TempDir() + t.Setenv("DOCKER_CONFIG", p) + return p +} + +func setupDockerConfig(t *testing.T, content string) { + cd := setupConfigDir(t) + p := filepath.Join(cd, "config.json") + + err := os.WriteFile(p, []byte(content), 0o600) + require.NoError(t, err) +} + +func encode(user, pass string) string { + delimited := fmt.Sprintf("%s:%s", user, pass) + return base64.StdEncoding.EncodeToString([]byte(delimited)) +} + +func TestGet(t *testing.T) { + tr := setupPrivateRegistry(t) + defer tr.Close() + + serverAddr := tr.Listener.Addr().String() + + type args struct { + imageName string + config string + option types.RegistryOptions + } + tests := []struct { + name string + args args + want *Descriptor + wantErr string + }{ + { + name: "single credential", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + Insecure: true, + }, + }, + }, + { + name: "mirror", + args: args{ + imageName: "foo.bar.io/library/alpine:3.10", + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + RegistryMirrors: map[string][]string{ + "foo.bar.io": { + serverAddr, + }, + }, + Insecure: true, + }, + }, + }, + { + name: "mirror for dockerhub", + args: args{ + imageName: "alpine:3.10", + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + RegistryMirrors: map[string][]string{ + "index.docker.io": { + serverAddr, + }, + }, + Insecure: true, + }, + }, + }, + { + name: "non-existent mirror image - use image from host", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + RegistryMirrors: map[string][]string{ + serverAddr: { + "wrong.repository", + }, + }, + Insecure: true, + }, + }, + }, + { + name: "wrong mirror", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + RegistryMirrors: map[string][]string{ + serverAddr: { + "wrong.repository:tag@digest", + }, + }, + Insecure: true, + }, + }, + wantErr: "could not parse reference: wrong.repository:tag@digest/library/alpine:3.10", + }, + { + name: "multiple credential", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "foo", + Password: "bar", + }, + { + Username: "test", + Password: "testpass", + }, + }, + Insecure: true, + }, + }, + }, + { + name: "keychain", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + config: fmt.Sprintf(`{"auths": {%q: {"auth": %q}}}`, serverAddr, encode("test", "testpass")), + option: types.RegistryOptions{ + Insecure: true, + }, + }, + }, + { + name: "platform", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + Insecure: true, + Platform: types.Platform{ + Platform: &v1.Platform{ + OS: "", + Architecture: "amd64", + }, + }, + }, + }, + }, + { + name: "force platform", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + Insecure: true, + Platform: types.Platform{ + Force: true, + Platform: &v1.Platform{ + OS: "windows", + Architecture: "amd64", + }, + }, + }, + }, + wantErr: "the specified platform not found", + }, + { + name: "bad credential", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "foo", + Password: "bar", + }, + }, + Insecure: true, + }, + }, + wantErr: "invalid username/password", + }, + { + name: "bad credential for multiple mirrors", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + option: types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "foo", + Password: "bar", + }, + }, + Insecure: true, + RegistryMirrors: map[string][]string{ + serverAddr: { + serverAddr, + serverAddr, + }, + }, + }, + }, + wantErr: "6 errors occurred:", // 2 errors for each repository (for 2 mirrors and the original repository) + }, + { + name: "bad keychain", + args: args{ + imageName: fmt.Sprintf("%s/library/alpine:3.10", serverAddr), + config: fmt.Sprintf(`{"auths": {%q: {"auth": %q}}}`, serverAddr, encode("foo", "bar")), + option: types.RegistryOptions{ + Insecure: true, + }, + }, + wantErr: "invalid username/password", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + n, err := name.ParseReference(tt.args.imageName) + require.NoError(t, err) + + if tt.args.config != "" { + setupDockerConfig(t, tt.args.config) + } + + _, err = Get(t.Context(), n, tt.args.option) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr, err) + return + } + require.NoError(t, err) + }) + } +} + +type userAgentsTrackingHandler struct { + hr http.Handler + + mu sync.Mutex + agents set.Set[string] +} + +func newUserAgentsTrackingHandler(hr http.Handler) *userAgentsTrackingHandler { + return &userAgentsTrackingHandler{ + hr: hr, + agents: set.New[string](), + } +} + +func (uh *userAgentsTrackingHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + for _, agent := range r.Header["User-Agent"] { + // Skip test framework user agent + if agent != "Go-http-client/1.1" { + uh.agents.Append(agent) + } + } + uh.hr.ServeHTTP(rw, r) +} + +func setupAgentTrackingRegistry(t *testing.T) (*httptest.Server, *userAgentsTrackingHandler) { + images := map[string]v1.Image{ + "v2/library/alpine:3.10": localImage(t), + } + tr := registry.NewDockerRegistry(registry.Option{ + Images: images, + }) + + tracker := newUserAgentsTrackingHandler(tr.Config.Handler) + tr.Config.Handler = tracker + + return tr, tracker +} + +func TestUserAgents(t *testing.T) { + tr, tracker := setupAgentTrackingRegistry(t) + defer tr.Close() + + serverAddr := tr.Listener.Addr().String() + + n, err := name.ParseReference(fmt.Sprintf("%s/library/alpine:3.10", serverAddr)) + require.NoError(t, err) + + _, err = Get(t.Context(), n, types.RegistryOptions{ + Credentials: []types.Credential{ + { + Username: "test", + Password: "testpass", + }, + }, + Insecure: true, + }) + require.NoError(t, err) + + require.Len(t, tracker.agents, 1) + ok := tracker.agents.Contains(fmt.Sprintf("trivy/%s", app.Version())) + require.True(t, ok, `user-agent header equals to "trivy/dev"`) +} + +func localImage(t *testing.T) v1.Image { + img, err := tarfile.ImageFromPath("../fanal/test/testdata/alpine-310.tar.gz") + require.NoError(t, err) + return img +} diff --git a/pkg/report/cyclonedx/cyclonedx.go b/pkg/report/cyclonedx/cyclonedx.go new file mode 100644 index 000000000000..76cd91d67752 --- /dev/null +++ b/pkg/report/cyclonedx/cyclonedx.go @@ -0,0 +1,44 @@ +package cyclonedx + +import ( + "context" + "io" + + cdx "github.com/CycloneDX/cyclonedx-go" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" + "github.com/aquasecurity/trivy/pkg/types" +) + +// Writer implements types.Writer +type Writer struct { + output io.Writer + format cdx.BOMFileFormat + marshaler cyclonedx.Marshaler +} + +func NewWriter(output io.Writer, appVersion string) Writer { + return Writer{ + output: output, + format: cdx.BOMFileFormatJSON, + marshaler: cyclonedx.NewMarshaler(appVersion), + } +} + +// Write writes the results in CycloneDX format +func (w Writer) Write(ctx context.Context, report types.Report) error { + bom, err := w.marshaler.MarshalReport(ctx, report) + if err != nil { + return xerrors.Errorf("CycloneDX marshal error: %w", err) + } + + encoder := cdx.NewBOMEncoder(w.output, w.format) + encoder.SetPretty(true) + encoder.SetEscapeHTML(false) + if err = encoder.Encode(bom); err != nil { + return xerrors.Errorf("failed to encode bom: %w", err) + } + + return nil +} diff --git a/pkg/report/export_test.go b/pkg/report/export_test.go new file mode 100644 index 000000000000..de92b434061b --- /dev/null +++ b/pkg/report/export_test.go @@ -0,0 +1,12 @@ +package report + +// Bridge to expose report internals to tests in the report_test package. + +// ClearURI exports clearURI for testing. +var ClearURI = clearURI + +// ToProperties exports toProperties for testing. +var ToProperties = toProperties + +// ToUri exports toUri for testing. +var ToUri = toUri diff --git a/pkg/report/github/github.go b/pkg/report/github/github.go new file mode 100644 index 000000000000..72af018e3c2d --- /dev/null +++ b/pkg/report/github/github.go @@ -0,0 +1,194 @@ +package github + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "time" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + DirectRelationship string = "direct" + IndirectRelationship string = "indirect" + RuntimeScope string = "runtime" + DevelopmentScope string = "development" +) + +type Package struct { + PackageUrl string `json:"package_url,omitempty"` + Relationship string `json:"relationship,omitempty"` + Dependencies []string `json:"dependencies,omitempty"` + Scope string `json:"scope,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` +} + +type File struct { + SrcLocation string `json:"source_location,omitempty"` +} + +type Metadata map[string]any + +type Manifest struct { + Name string `json:"name,omitempty"` + File *File `json:"file,omitempty"` + Metadata Metadata `json:"metadata,omitempty"` + Resolved map[string]Package `json:"resolved,omitempty"` +} + +type Job struct { + Correlator string `json:"correlator,omitempty"` + Id string `json:"id,omitempty"` +} +type Detector struct { + Name string `json:"name"` + Version string `json:"version"` + Url string `json:"url"` +} + +type DependencySnapshot struct { + Version int `json:"version"` + Detector Detector `json:"detector"` + Metadata Metadata `json:"metadata,omitempty"` + Ref string `json:"ref,omitempty"` + Sha string `json:"sha,omitempty"` + Job *Job `json:"job,omitempty"` + Scanned string `json:"scanned,omitempty"` + Manifests map[string]Manifest `json:"manifests,omitempty"` +} + +// Writer generates JSON for GitHub Dependency Snapshots +type Writer struct { + Output io.Writer + Version string +} + +func (w Writer) Write(ctx context.Context, report types.Report) error { + snapshot := &DependencySnapshot{} + + // use now() method that can be overwritten while integration tests run + snapshot.Scanned = clock.Now(ctx).Format(time.RFC3339) + snapshot.Detector = Detector{ + Name: "trivy", + Version: w.Version, + Url: "https://github.com/aquasecurity/trivy", + } + snapshot.Version = 0 // The version of the repository snapshot submission. + + snapshot.Ref = os.Getenv("GITHUB_REF") + snapshot.Sha = os.Getenv("GITHUB_SHA") + + snapshot.Job = &Job{ + Correlator: fmt.Sprintf("%s_%s", os.Getenv("GITHUB_WORKFLOW"), os.Getenv("GITHUB_JOB")), + Id: os.Getenv("GITHUB_RUN_ID"), + } + + snapshot.Metadata = getMetadata(report) + + manifests := make(map[string]Manifest) + + for _, result := range report.Results { + if result.Packages == nil { + continue + } + + manifest := Manifest{} + manifest.Name = string(result.Type) + // show path for language-specific packages only + if result.Class == types.ClassLangPkg { + if report.ArtifactType == ftypes.TypeContainerImage { + // `RepoDigests` ~= /@sha256: + // `RepoTag` ~= /: + // By concatenating the hash from `RepoDigests` at the end of `RepoTag` we get all the information + imageReference := strings.Join(report.Metadata.RepoTags, ", ") + imageWithHash := strings.Join(report.Metadata.RepoDigests, ", ") + _, imageHash, found := strings.Cut(imageWithHash, "@") + if found { + imageReference += "@" + imageHash + } + // Replacing `source_location` in manifest by the image name, tag and hash + manifest.File = &File{ + SrcLocation: imageReference, + } + + } else { + manifest.File = &File{ + SrcLocation: result.Target, + } + } + } + + resolved := make(map[string]Package) + + for _, pkg := range result.Packages { + var err error + githubPkg := Package{} + githubPkg.Scope = RuntimeScope + githubPkg.Relationship = getPkgRelationshipType(pkg) + githubPkg.Dependencies = pkg.DependsOn // TODO: replace with PURL + githubPkg.PackageUrl, err = buildPurl(result.Type, report.Metadata, pkg) + if err != nil { + return xerrors.Errorf("unable to build purl for %s: %w", pkg.Name, err) + } + + if pkg.FilePath != "" { + githubPkg.Metadata = Metadata{"source_location": pkg.FilePath} + } + + resolved[pkg.Name] = githubPkg + } + + manifest.Resolved = resolved + manifests[result.Target] = manifest + } + + snapshot.Manifests = manifests + + output, err := json.MarshalIndent(snapshot, "", " ") + if err != nil { + return xerrors.Errorf("failed to marshal github dependency snapshots: %w", err) + } + + if _, err = fmt.Fprint(w.Output, string(output)); err != nil { + return xerrors.Errorf("failed to write github dependency snapshots: %w", err) + } + return nil +} + +func getMetadata(report types.Report) Metadata { + metadata := Metadata{} + if report.Metadata.RepoTags != nil { + metadata["aquasecurity:trivy:RepoTag"] = strings.Join(report.Metadata.RepoTags, ", ") + } + if report.Metadata.RepoDigests != nil { + metadata["aquasecurity:trivy:RepoDigest"] = strings.Join(report.Metadata.RepoDigests, ", ") + } + return metadata +} + +func getPkgRelationshipType(pkg ftypes.Package) string { + if pkg.Relationship == ftypes.RelationshipIndirect { + return IndirectRelationship + } + return DirectRelationship +} + +func buildPurl(t ftypes.TargetType, metadata types.Metadata, pkg ftypes.Package) (string, error) { + packageUrl, err := purl.New(t, metadata, pkg) + if err != nil { + return "", xerrors.Errorf("purl error: %w", err) + } + if packageUrl == nil { + return "", nil + } + return packageUrl.String(), nil +} diff --git a/pkg/report/github/github_test.go b/pkg/report/github/github_test.go new file mode 100644 index 000000000000..de61fc3738fd --- /dev/null +++ b/pkg/report/github/github_test.go @@ -0,0 +1,256 @@ +package github_test + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report/github" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestWriter_Write(t *testing.T) { + tests := []struct { + name string + report types.Report + want map[string]github.Manifest + }{ + { + name: "os packages", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "alpine:3.14", + Metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.14", + Eosl: true, + }, + }, + Results: types.Results{ + { + Target: "yarn.lock", + Class: "lang-pkgs", + Type: "yarn", + Packages: []ftypes.Package{ + { + Name: "@xtuc/ieee754", + Version: "1.2.0", + Relationship: ftypes.RelationshipDirect, + }, + { + Name: "@xtuc/long", + Version: "4.2.2", + Relationship: ftypes.RelationshipDirect, + }, + { + Name: "@xtuc/binaryen", + Version: "1.37.33", + Relationship: ftypes.RelationshipIndirect, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + }, + }, + }, + { + Target: "alpine:3.14 (alpine 3.14.10)", + Class: "os-pkgs", + Type: "alpine", + Packages: []ftypes.Package{ + { + ID: "apk-tools@2.12.7-r0", + Name: "apk-tools", + Version: "2.12.7-r0", + Arch: "x86_64", + SrcName: "apk-tools", + SrcVersion: "2.12.7-r0", + }, + }, + }, + }, + }, + want: map[string]github.Manifest{ + "alpine:3.14 (alpine 3.14.10)": { + Name: "alpine", + Resolved: map[string]github.Package{ + "apk-tools": { + PackageUrl: "pkg:apk/alpine/apk-tools@2.12.7-r0?arch=x86_64&distro=3.14", + Relationship: "direct", + Scope: "runtime", + }, + }, + }, + "yarn.lock": { + Name: "yarn", + File: &github.File{ + SrcLocation: "yarn.lock", + }, + Resolved: map[string]github.Package{ + "@xtuc/ieee754": { + PackageUrl: "pkg:npm/%40xtuc/ieee754@1.2.0", + Relationship: "direct", + Scope: "runtime", + }, + "@xtuc/long": { + PackageUrl: "pkg:npm/%40xtuc/long@4.2.2", + Relationship: "direct", + Scope: "runtime", + }, + "@xtuc/binaryen": { + PackageUrl: "pkg:npm/%40xtuc/binaryen@1.37.33", + Relationship: "indirect", + Scope: "runtime", + }, + }, + }, + }, + }, + { + name: "maven", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "my-java-app", + Results: types.Results{ + { + Target: "pom.xml", + Class: "lang-pkgs", + Type: "pom", + Packages: []ftypes.Package{ + { + Name: "com.google.code.gson:gson", + Version: "2.2.2", + }, + { + Name: "net.sf.opencsv:opencsv", + Version: "2.3", + }, + }, + }, + }, + }, + want: map[string]github.Manifest{ + "pom.xml": { + Name: "pom", + File: &github.File{ + SrcLocation: "pom.xml", + }, + Resolved: map[string]github.Package{ + "com.google.code.gson:gson": { + PackageUrl: "pkg:maven/com.google.code.gson/gson@2.2.2", + Relationship: "direct", + Scope: "runtime", + }, + "net.sf.opencsv:opencsv": { + PackageUrl: "pkg:maven/net.sf.opencsv/opencsv@2.3", + Relationship: "direct", + Scope: "runtime", + }, + }, + }, + }, + }, + { + name: "pypi from image", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "fake_repo.azurecr.io/image_name", + ArtifactType: "container_image", + Metadata: types.Metadata{ + RepoDigests: []string{"fake_repo.azurecr.io/image_name@sha256:a7c92cdcb3d010f6edeb37ddcdbacab14981aa31e7f1140e0097dc1b8e834c49"}, + RepoTags: []string{"fake_repo.azurecr.io/image_name:latest"}, + }, + Results: types.Results{ + { + Target: "Python", + Class: "lang-pkgs", + Type: "python-pkg", + Packages: []ftypes.Package{ + { + Name: "jwcrypto", + Version: "0.7", + Licenses: []string{ + "LGPLv3+", + }, + Layer: ftypes.Layer{ + Digest: "sha256:ddc612ba4e74ea5633a93e19e7c32f61f5f230073b21a070302a61ef5eec5c50", + DiffID: "sha256:12935ef6ce21a266aef8df75d601cebf7e935edd01e9f19fab16ccb78fbb9a5e", + }, + FilePath: "opt/pyenv/versions/3.11.2/lib/python3.11/site-packages/jwcrypto-0.7.dist-info/METADATA", + }, + { + Name: "matplotlib", + Version: "3.5.3", + Licenses: []string{ + "PSF", + }, + Layer: ftypes.Layer{ + Digest: "sha256:ddc612ba4e74ea5633a93e19e7c32f61f5f230073b21a070302a61ef5eec5c50", + DiffID: "sha256:12935ef6ce21a266aef8df75d601cebf7e935edd01e9f19fab16ccb78fbb9a5e", + }, + FilePath: "opt/pyenv/versions/3.11.2/lib/python3.11/site-packages/matplotlib-3.5.3.dist-info/METADATA", + }, + }, + }, + }, + }, + want: map[string]github.Manifest{ + "Python": { + Name: "python-pkg", + File: &github.File{ + SrcLocation: "fake_repo.azurecr.io/image_name:latest@sha256:a7c92cdcb3d010f6edeb37ddcdbacab14981aa31e7f1140e0097dc1b8e834c49", + }, + Resolved: map[string]github.Package{ + "jwcrypto": { + PackageUrl: "pkg:pypi/jwcrypto@0.7", + Relationship: "direct", + Scope: "runtime", + Metadata: github.Metadata{"source_location": "opt/pyenv/versions/3.11.2/lib/python3.11/site-packages/jwcrypto-0.7.dist-info/METADATA"}, + }, + "matplotlib": { + PackageUrl: "pkg:pypi/matplotlib@3.5.3", + Relationship: "direct", + Scope: "runtime", + Metadata: github.Metadata{"source_location": "opt/pyenv/versions/3.11.2/lib/python3.11/site-packages/matplotlib-3.5.3.dist-info/METADATA"}, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + written := bytes.NewBuffer(nil) + w := github.Writer{ + Output: written, + } + + inputResults := tt.report + + err := w.Write(t.Context(), inputResults) + require.NoError(t, err) + + var got github.DependencySnapshot + err = json.Unmarshal(written.Bytes(), &got) + require.NoError(t, err, "invalid github written") + assert.Equal(t, tt.want, got.Manifests, tt.name) + }) + } +} diff --git a/pkg/report/json.go b/pkg/report/json.go new file mode 100644 index 000000000000..e51d524d6509 --- /dev/null +++ b/pkg/report/json.go @@ -0,0 +1,49 @@ +package report + +import ( + "context" + "encoding/json" + "fmt" + "io" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/types" +) + +// JSONWriter implements result Writer +type JSONWriter struct { + Output io.Writer + ListAllPkgs bool + ShowSuppressed bool +} + +// Write writes the results in JSON format +func (jw JSONWriter) Write(_ context.Context, report types.Report) error { + if !jw.ListAllPkgs { + // Delete packages + for i := range report.Results { + report.Results[i].Packages = nil + } + } + if !jw.ShowSuppressed { + // Delete suppressed findings + for i := range report.Results { + report.Results[i].ModifiedFindings = nil + } + } + report.Results = lo.Filter(report.Results, func(r types.Result, _ int) bool { + return r.Target != "" || !r.IsEmpty() + }) + + output, err := json.MarshalIndent(report, "", " ") + if err != nil { + return xerrors.Errorf("failed to marshal json: %w", err) + } + + if _, err = fmt.Fprintln(jw.Output, string(output)); err != nil { + return xerrors.Errorf("failed to write json: %w", err) + } + return nil +} diff --git a/pkg/report/json_test.go b/pkg/report/json_test.go new file mode 100644 index 000000000000..e84c7cb6e47b --- /dev/null +++ b/pkg/report/json_test.go @@ -0,0 +1,99 @@ +package report_test + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestReportWriter_JSON(t *testing.T) { + testCases := []struct { + name string + detectedVulns []types.DetectedVulnerability + want types.Report + }{ + { + name: "happy path", + detectedVulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + vulnerability.NVD: dbTypes.SeverityHigh, + }, + }, + }, + }, + want: types.Report{ + SchemaVersion: 2, + ArtifactName: "alpine:3.14", + Results: types.Results{ + types.Result{ + Target: "foojson", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + vulnerability.NVD: dbTypes.SeverityHigh, + }, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + jsonWritten := bytes.NewBuffer(nil) + jw := report.JSONWriter{ + Output: jsonWritten, + } + + inputResults := types.Report{ + SchemaVersion: 2, + ArtifactName: "alpine:3.14", + Results: types.Results{ + { + Target: "foojson", + Vulnerabilities: tc.detectedVulns, + }, + }, + } + + err := jw.Write(t.Context(), inputResults) + require.NoError(t, err) + + var got types.Report + err = json.Unmarshal(jsonWritten.Bytes(), &got) + require.NoError(t, err, "invalid json written") + + assert.Equal(t, tc.want, got, tc.name) + }) + } +} diff --git a/pkg/report/predicate/vuln.go b/pkg/report/predicate/vuln.go new file mode 100644 index 000000000000..56a42abdc5e5 --- /dev/null +++ b/pkg/report/predicate/vuln.go @@ -0,0 +1,89 @@ +package predicate + +import ( + "context" + "encoding/json" + "fmt" + "io" + "time" + + "github.com/package-url/packageurl-go" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/types" +) + +// CosignVulnPredicate represents the Cosign Vulnerability Scan Record. +// CosignVulnPredicate is based on structures in the Cosign repository. +// We defined them ourselves to reduce our dependence on the repository. +// cf. https://github.com/sigstore/cosign/blob/e0547cff64f98585a837a524ff77ff6b47ff5609/pkg/cosign/attestation/attestation.go#L45-L50 +type CosignVulnPredicate struct { + Invocation Invocation `json:"invocation"` + Scanner Scanner `json:"scanner"` + Metadata Metadata `json:"metadata"` +} + +type Invocation struct { + Parameters any `json:"parameters"` + URI string `json:"uri"` + EventID string `json:"event_id"` + BuilderID string `json:"builder.id"` +} + +type DB struct { + URI string `json:"uri"` + Version string `json:"version"` +} + +type Scanner struct { + URI string `json:"uri"` + Version string `json:"version"` + DB DB `json:"db"` + Result types.Report `json:"result"` +} + +type Metadata struct { + ScanStartedOn time.Time `json:"scanStartedOn"` + ScanFinishedOn time.Time `json:"scanFinishedOn"` +} + +type VulnWriter struct { + output io.Writer + version string +} + +func NewVulnWriter(output io.Writer, version string) VulnWriter { + return VulnWriter{ + output: output, + version: version, + } +} + +func (w VulnWriter) Write(ctx context.Context, report types.Report) error { + predicate := CosignVulnPredicate{} + + purl := packageurl.NewPackageURL("github", "aquasecurity", "trivy", w.version, nil, "") + predicate.Scanner = Scanner{ + URI: purl.ToString(), + Version: w.version, + Result: report, + } + + now := clock.Now(ctx) + predicate.Metadata = Metadata{ + ScanStartedOn: now, + ScanFinishedOn: now, + } + + output, err := json.MarshalIndent(predicate, "", " ") + if err != nil { + return xerrors.Errorf("failed to marshal cosign vulnerability predicate: %w", err) + } + + if _, err = fmt.Fprint(w.output, string(output)); err != nil { + return xerrors.Errorf("failed to write cosign vulnerability predicate: %w", err) + } + return nil + +} diff --git a/pkg/report/predicate/vuln_test.go b/pkg/report/predicate/vuln_test.go new file mode 100644 index 000000000000..1937efc1d9a2 --- /dev/null +++ b/pkg/report/predicate/vuln_test.go @@ -0,0 +1,114 @@ +package predicate_test + +import ( + "bytes" + "encoding/json" + "testing" + "time" + + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report/predicate" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestWriter_Write(t *testing.T) { + tests := []struct { + name string + detectedVulns []types.DetectedVulnerability + want predicate.CosignVulnPredicate + }{ + { + name: "happy path", + detectedVulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + vulnerability.NVD: dbTypes.SeverityHigh, + }, + }, + }, + }, + want: predicate.CosignVulnPredicate{ + Scanner: predicate.Scanner{ + URI: "pkg:github/aquasecurity/trivy@dev", + Version: "dev", + Result: types.Report{ + SchemaVersion: 2, + ArtifactName: "alpine:3.14", + ArtifactType: ftypes.ArtifactType(""), + Metadata: types.Metadata{}, + Results: types.Results{ + { + Target: "foojson", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + vulnerability.NVD: dbTypes.SeverityHigh, + }, + }, + }, + }, + }, + }, + }, + }, + Metadata: predicate.Metadata{ + ScanStartedOn: time.Date(2022, time.July, 22, 12, 20, 30, 5, time.UTC), + ScanFinishedOn: time.Date(2022, time.July, 22, 12, 20, 30, 5, time.UTC), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + inputResults := types.Report{ + SchemaVersion: 2, + ArtifactName: "alpine:3.14", + Results: types.Results{ + { + Target: "foojson", + Vulnerabilities: tt.detectedVulns, + }, + }, + } + + output := bytes.NewBuffer(nil) + + ctx := clock.With(t.Context(), time.Date(2022, 7, 22, 12, 20, 30, 5, time.UTC)) + writer := predicate.NewVulnWriter(output, "dev") + + err := writer.Write(ctx, inputResults) + require.NoError(t, err) + + var got predicate.CosignVulnPredicate + err = json.Unmarshal(output.Bytes(), &got) + require.NoError(t, err, "invalid json written") + + require.Equal(t, tt.want, got, tt.name) + + }) + } +} diff --git a/pkg/report/sarif.go b/pkg/report/sarif.go new file mode 100644 index 000000000000..c9096145975e --- /dev/null +++ b/pkg/report/sarif.go @@ -0,0 +1,477 @@ +package report + +import ( + "context" + "fmt" + "io" + "net/url" + "path/filepath" + "regexp" + "strings" + + containerName "github.com/google/go-containerregistry/pkg/name" + "github.com/owenrumney/go-sarif/v2/sarif" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + sarifOsPackageVulnerability = "OsPackageVulnerability" + sarifLanguageSpecificVulnerability = "LanguageSpecificPackageVulnerability" + sarifConfigFiles = "Misconfiguration" + sarifSecretFiles = "Secret" + sarifLicenseFiles = "License" + sarifUnknownIssue = "UnknownIssue" + + sarifError = "error" + sarifWarning = "warning" + sarifNote = "note" + sarifNone = "none" + + columnKind = "utf16CodeUnits" + + builtinRulesUrl = "https://github.com/aquasecurity/trivy/blob/main/pkg/fanal/secret/builtin-rules.go" // list all secrets +) + +var ( + rootPath = "file:///" + + // pathRegex to extract file path in case string includes (distro:version) + pathRegex = regexp.MustCompile(`(?P.+?)(?:\s*\((?:.*?)\).*?)?$`) +) + +// SarifWriter implements result Writer +type SarifWriter struct { + Output io.Writer + Version string + run *sarif.Run + locationCache map[string][]location + Target string +} + +type sarifData struct { + title string + vulnerabilityId string + shortDescription string + fullDescription string + helpText string + helpMarkdown string + resourceClass types.ResultClass + severity string + url *url.URL + resultIndex int + artifactLocation *url.URL + locationMessage string + message string + cvssScore string + cvssData map[string]any + locations []location +} + +type location struct { + startLine int + endLine int +} + +func (sw *SarifWriter) addSarifRule(data *sarifData) { + r := sw.run.AddRule(data.vulnerabilityId). + WithName(toSarifRuleName(data.resourceClass)). + WithDescription(data.vulnerabilityId). + WithShortDescription(&sarif.MultiformatMessageString{Text: &data.shortDescription}). + WithFullDescription(&sarif.MultiformatMessageString{Text: &data.fullDescription}). + WithHelp(&sarif.MultiformatMessageString{ + Text: &data.helpText, + Markdown: &data.helpMarkdown, + }). + WithDefaultConfiguration(&sarif.ReportingConfiguration{ + Level: toSarifErrorLevel(data.severity), + }). + WithProperties(toProperties(data.title, data.severity, data.cvssScore, data.cvssData)) + if data.url != nil && data.url.String() != "" { + r.WithHelpURI(data.url.String()) + } +} + +func (sw *SarifWriter) addSarifResult(data *sarifData) { + sw.addSarifRule(data) + + result := sarif.NewRuleResult(data.vulnerabilityId). + WithRuleIndex(data.resultIndex). + WithMessage(sarif.NewTextMessage(data.message)). + WithLevel(toSarifErrorLevel(data.severity)). + WithLocations(toSarifLocations(data.locations, data.artifactLocation.String(), data.locationMessage)) + sw.run.AddResult(result) +} + +func getRuleIndex(id string, indexes map[string]int) int { + i, ok := indexes[id] + if ok { + return i + } + l := len(indexes) + indexes[id] = l + return l +} + +func (sw *SarifWriter) Write(_ context.Context, report types.Report) error { + sarifReport, err := sarif.New(sarif.Version210) + if err != nil { + return xerrors.Errorf("error creating a new sarif template: %w", err) + } + sw.run = sarif.NewRunWithInformationURI("Trivy", "https://github.com/aquasecurity/trivy") + sw.run.Tool.Driver.WithVersion(sw.Version) + sw.run.Tool.Driver.WithFullName("Trivy Vulnerability Scanner") + sw.locationCache = make(map[string][]location) + if report.ArtifactType == ftypes.TypeContainerImage { + sw.run.Properties = sarif.Properties{ + "imageName": report.ArtifactName, + "repoTags": report.Metadata.RepoTags, + "repoDigests": report.Metadata.RepoDigests, + "imageID": report.Metadata.ImageID, + } + } + if sw.Target != "" { + absPath, _ := filepath.Abs(sw.Target) + rootPath = fmt.Sprintf("file://%s/", absPath) + } + + ruleIndexes := make(map[string]int) + for _, res := range report.Results { + target := ToPathUri(res.Target, res.Class) + + for _, vuln := range res.Vulnerabilities { + fullDescription := vuln.Description + if fullDescription == "" { + fullDescription = vuln.Title + } + path := target + if vuln.PkgPath != "" { + path = ToPathUri(vuln.PkgPath, res.Class) + } + cvssData, cvssScore := toCVSSData(vuln) + sw.addSarifResult(&sarifData{ + title: "vulnerability", + vulnerabilityId: vuln.VulnerabilityID, + severity: vuln.Severity, + cvssScore: cvssScore, + cvssData: cvssData, + url: toUri(vuln.PrimaryURL), + resourceClass: res.Class, + artifactLocation: toUri(path), + locationMessage: fmt.Sprintf("%v: %v@%v", path, vuln.PkgName, vuln.InstalledVersion), + locations: sw.getLocations(vuln.PkgName, vuln.InstalledVersion, path, res.Packages), + resultIndex: getRuleIndex(vuln.VulnerabilityID, ruleIndexes), + shortDescription: vuln.Title, + fullDescription: fullDescription, + helpText: fmt.Sprintf("Vulnerability %v\nSeverity: %v\nPackage: %v\nFixed Version: %v\nLink: [%v](%v)\n%v", + vuln.VulnerabilityID, vuln.Severity, vuln.PkgName, vuln.FixedVersion, vuln.VulnerabilityID, vuln.PrimaryURL, vuln.Description), + helpMarkdown: fmt.Sprintf("**Vulnerability %v**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|%v|%v|%v|[%v](%v)|\n\n%v", + vuln.VulnerabilityID, vuln.Severity, vuln.PkgName, vuln.FixedVersion, vuln.VulnerabilityID, vuln.PrimaryURL, vuln.Description), + message: fmt.Sprintf("Package: %v\nInstalled Version: %v\nVulnerability %v\nSeverity: %v\nFixed Version: %v\nLink: [%v](%v)", + vuln.PkgName, vuln.InstalledVersion, vuln.VulnerabilityID, vuln.Severity, vuln.FixedVersion, vuln.VulnerabilityID, vuln.PrimaryURL), + }) + } + for _, misconf := range res.Misconfigurations { + locationURI := clearURI(res.Target) + sw.addSarifResult(&sarifData{ + title: "misconfiguration", + vulnerabilityId: misconf.ID, + severity: misconf.Severity, + cvssScore: severityToScore(misconf.Severity), + url: toUri(misconf.PrimaryURL), + resourceClass: res.Class, + artifactLocation: toUri(locationURI), + locationMessage: locationURI, + locations: []location{ + { + startLine: misconf.CauseMetadata.StartLine, + endLine: misconf.CauseMetadata.EndLine, + }, + }, + resultIndex: getRuleIndex(misconf.ID, ruleIndexes), + shortDescription: misconf.Title, + fullDescription: misconf.Description, + helpText: fmt.Sprintf("Misconfiguration %v\nType: %s\nSeverity: %v\nCheck: %v\nMessage: %v\nLink: [%v](%v)\n%s", + misconf.ID, misconf.Type, misconf.Severity, misconf.Title, misconf.Message, misconf.ID, misconf.PrimaryURL, misconf.Description), + helpMarkdown: fmt.Sprintf("**Misconfiguration %v**\n| Type | Severity | Check | Message | Link |\n| --- | --- | --- | --- | --- |\n|%v|%v|%v|%s|[%v](%v)|\n\n%v", + misconf.ID, misconf.Type, misconf.Severity, misconf.Title, misconf.Message, misconf.ID, misconf.PrimaryURL, misconf.Description), + message: fmt.Sprintf("Artifact: %v\nType: %v\nVulnerability %v\nSeverity: %v\nMessage: %v\nLink: [%v](%v)", + locationURI, res.Type, misconf.ID, misconf.Severity, misconf.Message, misconf.ID, misconf.PrimaryURL), + }) + } + for _, secret := range res.Secrets { + sw.addSarifResult(&sarifData{ + title: "secret", + vulnerabilityId: secret.RuleID, + severity: secret.Severity, + cvssScore: severityToScore(secret.Severity), + url: toUri(builtinRulesUrl), + resourceClass: res.Class, + artifactLocation: toUri(target), + locationMessage: target, + locations: []location{ + { + startLine: secret.StartLine, + endLine: secret.EndLine, + }, + }, + resultIndex: getRuleIndex(secret.RuleID, ruleIndexes), + shortDescription: secret.Title, + fullDescription: secret.Match, + helpText: fmt.Sprintf("Secret %v\nSeverity: %v\nMatch: %s", + secret.Title, secret.Severity, secret.Match), + helpMarkdown: fmt.Sprintf("**Secret %v**\n| Severity | Match |\n| --- | --- |\n|%v|%v|", + secret.Title, secret.Severity, secret.Match), + message: fmt.Sprintf("Artifact: %v\nType: %v\nSecret %v\nSeverity: %v\nMatch: %v", + res.Target, res.Type, secret.Title, secret.Severity, secret.Match), + }) + } + for _, license := range res.Licenses { + id := fmt.Sprintf("%s:%s", license.PkgName, license.Name) + desc := fmt.Sprintf("%s in %s", license.Name, license.PkgName) + sw.addSarifResult(&sarifData{ + title: "license", + vulnerabilityId: id, + severity: license.Severity, + cvssScore: severityToScore(license.Severity), + url: toUri(license.Link), + resourceClass: res.Class, + artifactLocation: toUri(target), + resultIndex: getRuleIndex(id, ruleIndexes), + shortDescription: desc, + fullDescription: desc, + helpText: fmt.Sprintf("License %s\nClassification: %s\nPkgName: %s\nPath: %s", + license.Name, license.Category, license.PkgName, license.FilePath), + helpMarkdown: fmt.Sprintf("**License %s**\n| PkgName | Classification | Path |\n| --- | --- | --- |\n|%s|%s|%s|", + license.Name, license.PkgName, license.Category, license.FilePath), + message: fmt.Sprintf("Artifact: %s\nLicense %s\nPkgName: %s\n Classification: %s\n Path: %s", + res.Target, license.Name, license.PkgName, license.Category, license.FilePath), + }) + } + + } + sw.run.ColumnKind = columnKind + sw.run.OriginalUriBaseIDs = map[string]*sarif.ArtifactLocation{ + "ROOTPATH": {URI: &rootPath}, + } + sarifReport.AddRun(sw.run) + return sarifReport.PrettyWrite(sw.Output) +} + +func toSarifLocations(locations []location, artifactLocation, locationMessage string) []*sarif.Location { + var sarifLocs []*sarif.Location + // add default (hardcoded) location for vulnerabilities that don't support locations + if len(locations) == 0 { + locations = append(locations, location{ + startLine: 1, + endLine: 1, + }) + } + + // some dependencies can be placed in multiple places. + // e.g.https://github.com/aquasecurity/go-dep-parser/pull/134#discussion_r985353240 + // create locations for each place. + + for _, l := range locations { + // location is missed. Use default (hardcoded) value (misconfigurations have this case) + if l.startLine == 0 && l.endLine == 0 { + l.startLine = 1 + l.endLine = 1 + } + region := sarif.NewRegion().WithStartLine(l.startLine).WithEndLine(l.endLine).WithStartColumn(1).WithEndColumn(1) + loc := sarif.NewPhysicalLocation(). + WithArtifactLocation(sarif.NewSimpleArtifactLocation(artifactLocation).WithUriBaseId("ROOTPATH")). + WithRegion(region) + sarifLocs = append(sarifLocs, sarif.NewLocation().WithMessage(sarif.NewTextMessage(locationMessage)).WithPhysicalLocation(loc)) + } + + return sarifLocs +} + +func toSarifRuleName(class types.ResultClass) string { + switch class { + case types.ClassOSPkg: + return sarifOsPackageVulnerability + case types.ClassLangPkg: + return sarifLanguageSpecificVulnerability + case types.ClassConfig: + return sarifConfigFiles + case types.ClassSecret: + return sarifSecretFiles + case types.ClassLicense, types.ClassLicenseFile: + return sarifLicenseFiles + default: + return sarifUnknownIssue + } +} + +func toSarifErrorLevel(severity string) string { + switch severity { + case "CRITICAL", "HIGH": + return sarifError + case "MEDIUM": + return sarifWarning + case "LOW", "UNKNOWN": + return sarifNote + default: + return sarifNone + } +} + +func ToPathUri(input string, resultClass types.ResultClass) string { + // we only need to convert OS input + // e.g. image names, digests, etc... + if resultClass != types.ClassOSPkg { + return input + } + matches := pathRegex.FindStringSubmatch(input) + if matches != nil { + input = matches[pathRegex.SubexpIndex("path")] + } + ref, err := containerName.ParseReference(input) + if err == nil { + input = ref.Context().RepositoryStr() + } + + return clearURI(input) +} + +// clearURI clears URI for misconfigs +func clearURI(s string) string { + s = strings.ReplaceAll(s, "\\", "/") + // cf. https://developer.hashicorp.com/terraform/language/modules/sources + switch { + case strings.HasPrefix(s, "git@github.com:"): + // build GitHub url format + // e.g. `git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git?ref=v4.2.0/main.tf` -> `github.com/terraform-aws-modules/terraform-aws-s3-bucket/tree/v4.2.0/main.tf` + // cf. https://github.com/aquasecurity/trivy/issues/7897 + s = strings.ReplaceAll(s, "git@github.com:", "github.com/") + s = strings.ReplaceAll(s, ".git", "") + s = strings.ReplaceAll(s, "?ref=", "/tree/") + case strings.HasPrefix(s, "git::https:/") && !strings.HasPrefix(s, "git::https://"): + s = strings.TrimPrefix(s, "git::https:/") + s = strings.ReplaceAll(s, ".git", "") + case strings.HasPrefix(s, "git::ssh://"): + // `"`git::ssh://username@example.com/storage.git` -> `example.com/storage.git` + if _, u, ok := strings.Cut(s, "@"); ok { + s = u + } + s = strings.ReplaceAll(s, ".git", "") + case strings.HasPrefix(s, "git::"): + // `git::https://example.com/vpc.git` -> `https://example.com/vpc` + s = strings.TrimPrefix(s, "git::") + s = strings.ReplaceAll(s, ".git", "") + case strings.HasPrefix(s, "hg::"): + // `hg::http://example.com/vpc.hg` -> `http://example.com/vpc` + s = strings.TrimPrefix(s, "hg::") + s = strings.ReplaceAll(s, ".hg", "") + case strings.HasPrefix(s, "s3::"): + // `s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip` -> `https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip` + s = strings.TrimPrefix(s, "s3::") + case strings.HasPrefix(s, "gcs::"): + // `gcs::https://www.googleapis.com/storage/v1/modules/foomodule.zipp` -> `https://www.googleapis.com/storage/v1/modules/foomodule.zip` + s = strings.TrimPrefix(s, "gcs::") + } + + return s +} + +func toUri(str string) *url.URL { + uri, err := url.Parse(str) + if err != nil { + logger := log.WithPrefix("sarif") + logger.Error("Unable to parse URI", log.String("URI", str), log.Err(err)) + } + return uri +} + +func (sw *SarifWriter) getLocations(name, version, path string, pkgs []ftypes.Package) []location { + id := fmt.Sprintf("%s@%s@%s", path, name, version) + locs, ok := sw.locationCache[id] + if !ok { + for _, pkg := range pkgs { + if name == pkg.Name && version == pkg.Version { + for _, l := range pkg.Locations { + loc := location{ + startLine: l.StartLine, + endLine: l.EndLine, + } + locs = append(locs, loc) + } + sw.locationCache[id] = locs + return locs + } + } + } + return locs +} + +// toCVSSData extracts CVSS data from the vulnerability and returns it along with the score. +// If CVSS V3 Score is not available, it returns an empty CVSSData struct and a score based on severity. +func toCVSSData(vuln types.DetectedVulnerability) (map[string]any, string) { + score := severityToScore(vuln.Severity) + var data = make(map[string]any) + + // Note: 'cvssv3_baseScore' uses a hybrid naming convention (snake_case + camelCase) + // Reference: https://docs.aws.amazon.com/codecatalyst/latest/userguide/test.sarif.html + if cvss, ok := vuln.CVSS[vuln.SeveritySource]; ok { + data["cvssv2_vector"] = cvss.V2Vector + data["cvssv2_score"] = cvss.V2Score + data["cvssv3_vector"] = cvss.V3Vector + data["cvssv3_baseScore"] = cvss.V3Score + data["cvssv40_vector"] = cvss.V40Vector + data["cvssv40_baseScore"] = cvss.V40Score + + if cvss.V3Score != 0 { + score = fmt.Sprintf("%.1f", cvss.V3Score) + } + } + + return data, score +} + +func severityToScore(severity string) string { + switch severity { + case "CRITICAL": + return "9.5" + case "HIGH": + return "8.0" + case "MEDIUM": + return "5.5" + case "LOW": + return "2.0" + default: + return "0.0" + } +} + +func toProperties(title, severity, cvssScore string, cvssData map[string]any) sarif.Properties { + properties := sarif.Properties{ + "tags": []string{ + title, + "security", + severity, + }, + "precision": "very-high", + "security-severity": cvssScore, + } + + for key, value := range cvssData { + switch v := value.(type) { + case string: + if v == "" { + continue + } + case float64: + if v == 0 { + continue + } + } + properties[key] = value + } + + return properties +} diff --git a/pkg/report/sarif_test.go b/pkg/report/sarif_test.go new file mode 100644 index 000000000000..ca3b4eaf67ad --- /dev/null +++ b/pkg/report/sarif_test.go @@ -0,0 +1,957 @@ +package report_test + +import ( + "bytes" + "encoding/json" + "testing" + + "github.com/owenrumney/go-sarif/v2/sarif" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestReportWriter_Sarif(t *testing.T) { + tests := []struct { + name string + input types.Report + want *sarif.Report + }{ + { + name: "report with vulnerabilities", + input: types.Report{ + ArtifactName: "debian:9", + ArtifactType: ftypes.TypeContainerImage, + Metadata: types.Metadata{ + ImageID: "sha256:7640c3f9e75002deb419d5e32738eeff82cf2b3edca3781b4fe1f1f626d11b20", + RepoTags: []string{ + "debian:9", + }, + RepoDigests: []string{ + "debian@sha256:a8cc1744bbdd5266678e3e8b3e6387e45c053218438897e86876f2eb104e5534", + }, + }, + Results: types.Results{ + { + Target: "library/test 1", + Class: types.ClassOSPkg, + Packages: []ftypes.Package{ + { + Name: "foo", + Version: "1.2.3", + Locations: []ftypes.Location{ + { + StartLine: 5, + EndLine: 10, + }, + { + StartLine: 15, + EndLine: 20, + }, + }, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + SeveritySource: "redhat", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + vulnerability.NVD: dbTypes.SeverityCritical, + vulnerability.RedHat: dbTypes.SeverityHigh, + }, + CVSS: map[dbTypes.SourceID]dbTypes.CVSS{ + vulnerability.NVD: { + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + V3Score: 9.8, + }, + vulnerability.RedHat: { + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + V3Score: 7.5, + }, + }, + }, + }, + }, + }, + }, + }, + want: &sarif.Report{ + Version: "2.1.0", + Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + Runs: []*sarif.Run{ + { + Tool: sarif.Tool{ + Driver: &sarif.ToolComponent{ + FullName: lo.ToPtr("Trivy Vulnerability Scanner"), + Name: "Trivy", + Version: lo.ToPtr(""), + InformationURI: lo.ToPtr("https://github.com/aquasecurity/trivy"), + Rules: []*sarif.ReportingDescriptor{ + { + ID: "CVE-2020-0001", + Name: lo.ToPtr("OsPackageVulnerability"), + ShortDescription: &sarif.MultiformatMessageString{Text: lo.ToPtr("foobar")}, + FullDescription: &sarif.MultiformatMessageString{Text: lo.ToPtr("baz")}, + DefaultConfiguration: &sarif.ReportingConfiguration{ + Level: "error", + }, + HelpURI: lo.ToPtr("https://avd.aquasec.com/nvd/cve-2020-0001"), + Properties: map[string]any{ + "tags": []any{ + "vulnerability", + "security", + "HIGH", + }, + "precision": "very-high", + "security-severity": "7.5", + "cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + "cvssv3_baseScore": 7.5, + }, + Help: &sarif.MultiformatMessageString{ + Text: lo.ToPtr("Vulnerability CVE-2020-0001\nSeverity: HIGH\nPackage: foo\nFixed Version: 3.4.5\nLink: [CVE-2020-0001](https://avd.aquasec.com/nvd/cve-2020-0001)\nbaz"), + Markdown: lo.ToPtr("**Vulnerability CVE-2020-0001**\n| Severity | Package | Fixed Version | Link |\n| --- | --- | --- | --- |\n|HIGH|foo|3.4.5|[CVE-2020-0001](https://avd.aquasec.com/nvd/cve-2020-0001)|\n\nbaz"), + }, + }, + }, + }, + }, + Results: []*sarif.Result{ + { + RuleID: lo.ToPtr("CVE-2020-0001"), + RuleIndex: lo.ToPtr[uint](0), + Level: lo.ToPtr("error"), + Message: sarif.Message{Text: lo.ToPtr("Package: foo\nInstalled Version: 1.2.3\nVulnerability CVE-2020-0001\nSeverity: HIGH\nFixed Version: 3.4.5\nLink: [CVE-2020-0001](https://avd.aquasec.com/nvd/cve-2020-0001)")}, + Locations: []*sarif.Location{ + { + Message: &sarif.Message{Text: lo.ToPtr("library/test 1: foo@1.2.3")}, + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{ + URI: lo.ToPtr("library/test%201"), + URIBaseId: lo.ToPtr("ROOTPATH"), + }, + Region: &sarif.Region{ + StartLine: lo.ToPtr(5), + EndLine: lo.ToPtr(10), + StartColumn: lo.ToPtr(1), + EndColumn: lo.ToPtr(1), + }, + }, + }, + { + Message: &sarif.Message{Text: lo.ToPtr("library/test 1: foo@1.2.3")}, + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{ + URI: lo.ToPtr("library/test%201"), + URIBaseId: lo.ToPtr("ROOTPATH"), + }, + Region: &sarif.Region{ + StartLine: lo.ToPtr(15), + EndLine: lo.ToPtr(20), + StartColumn: lo.ToPtr(1), + EndColumn: lo.ToPtr(1), + }, + }, + }, + }, + }, + }, + ColumnKind: "utf16CodeUnits", + OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{ + "ROOTPATH": { + URI: lo.ToPtr("file:///"), + }, + }, + PropertyBag: sarif.PropertyBag{ + Properties: map[string]any{ + "imageName": "debian:9", + "imageID": "sha256:7640c3f9e75002deb419d5e32738eeff82cf2b3edca3781b4fe1f1f626d11b20", + "repoDigests": []any{"debian@sha256:a8cc1744bbdd5266678e3e8b3e6387e45c053218438897e86876f2eb104e5534"}, + "repoTags": []any{"debian:9"}, + }, + }, + }, + }, + }, + }, + { + name: "report with misconfigurations", + input: types.Report{ + Results: types.Results{ + { + Target: "library/test 1", + Class: types.ClassConfig, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Kubernetes Security Check", + ID: "KSV001", + Title: "Image tag ':latest' used", + Message: "Message", + Severity: "HIGH", + PrimaryURL: "https://avd.aquasec.com/appshield/ksv001", + Status: types.MisconfStatusFailure, + }, + { + Type: "Kubernetes Security Check", + ID: "KSV002", + Title: "SYS_ADMIN capability added", + Message: "Message", + Severity: "CRITICAL", + PrimaryURL: "https://avd.aquasec.com/appshield/ksv002", + Status: types.MisconfStatusPassed, + }, + }, + }, + }, + }, + want: &sarif.Report{ + Version: "2.1.0", + Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + Runs: []*sarif.Run{ + { + Tool: sarif.Tool{ + Driver: &sarif.ToolComponent{ + FullName: lo.ToPtr("Trivy Vulnerability Scanner"), + Name: "Trivy", + Version: lo.ToPtr(""), + InformationURI: lo.ToPtr("https://github.com/aquasecurity/trivy"), + Rules: []*sarif.ReportingDescriptor{ + { + ID: "KSV001", + Name: lo.ToPtr("Misconfiguration"), + ShortDescription: &sarif.MultiformatMessageString{Text: lo.ToPtr("Image tag ':latest' used")}, + FullDescription: &sarif.MultiformatMessageString{Text: lo.ToPtr("")}, + DefaultConfiguration: &sarif.ReportingConfiguration{ + Level: "error", + }, + HelpURI: lo.ToPtr("https://avd.aquasec.com/appshield/ksv001"), + Properties: map[string]any{ + "tags": []any{ + "misconfiguration", + "security", + "HIGH", + }, + "precision": "very-high", + "security-severity": "8.0", + }, + Help: &sarif.MultiformatMessageString{ + Text: lo.ToPtr("Misconfiguration KSV001\nType: Kubernetes Security Check\nSeverity: HIGH\nCheck: Image tag ':latest' used\nMessage: Message\nLink: [KSV001](https://avd.aquasec.com/appshield/ksv001)\n"), + Markdown: lo.ToPtr("**Misconfiguration KSV001**\n| Type | Severity | Check | Message | Link |\n| --- | --- | --- | --- | --- |\n|Kubernetes Security Check|HIGH|Image tag ':latest' used|Message|[KSV001](https://avd.aquasec.com/appshield/ksv001)|\n\n"), + }, + }, + { + ID: "KSV002", + Name: lo.ToPtr("Misconfiguration"), + ShortDescription: &sarif.MultiformatMessageString{Text: lo.ToPtr("SYS_ADMIN capability added")}, + FullDescription: &sarif.MultiformatMessageString{Text: lo.ToPtr("")}, + DefaultConfiguration: &sarif.ReportingConfiguration{ + Level: "error", + }, + HelpURI: lo.ToPtr("https://avd.aquasec.com/appshield/ksv002"), + Properties: map[string]any{ + "tags": []any{ + "misconfiguration", + "security", + "CRITICAL", + }, + "precision": "very-high", + "security-severity": "9.5", + }, + Help: &sarif.MultiformatMessageString{ + Text: lo.ToPtr("Misconfiguration KSV002\nType: Kubernetes Security Check\nSeverity: CRITICAL\nCheck: SYS_ADMIN capability added\nMessage: Message\nLink: [KSV002](https://avd.aquasec.com/appshield/ksv002)\n"), + Markdown: lo.ToPtr("**Misconfiguration KSV002**\n| Type | Severity | Check | Message | Link |\n| --- | --- | --- | --- | --- |\n|Kubernetes Security Check|CRITICAL|SYS_ADMIN capability added|Message|[KSV002](https://avd.aquasec.com/appshield/ksv002)|\n\n"), + }, + }, + }, + }, + }, + Results: []*sarif.Result{ + { + RuleID: lo.ToPtr("KSV001"), + RuleIndex: lo.ToPtr[uint](0), + Level: lo.ToPtr("error"), + Message: sarif.Message{Text: lo.ToPtr("Artifact: library/test 1\nType: \nVulnerability KSV001\nSeverity: HIGH\nMessage: Message\nLink: [KSV001](https://avd.aquasec.com/appshield/ksv001)")}, + Locations: []*sarif.Location{ + { + Message: &sarif.Message{Text: lo.ToPtr("library/test 1")}, + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{ + URI: lo.ToPtr("library/test%201"), + URIBaseId: lo.ToPtr("ROOTPATH"), + }, + Region: &sarif.Region{ + StartLine: lo.ToPtr(1), + EndLine: lo.ToPtr(1), + StartColumn: lo.ToPtr(1), + EndColumn: lo.ToPtr(1), + }, + }, + }, + }, + }, + { + RuleID: lo.ToPtr("KSV002"), + RuleIndex: lo.ToPtr[uint](1), + Level: lo.ToPtr("error"), + Message: sarif.Message{Text: lo.ToPtr("Artifact: library/test 1\nType: \nVulnerability KSV002\nSeverity: CRITICAL\nMessage: Message\nLink: [KSV002](https://avd.aquasec.com/appshield/ksv002)")}, + Locations: []*sarif.Location{ + { + Message: &sarif.Message{Text: lo.ToPtr("library/test 1")}, + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{ + URI: lo.ToPtr("library/test%201"), + URIBaseId: lo.ToPtr("ROOTPATH"), + }, + Region: &sarif.Region{ + StartLine: lo.ToPtr(1), + EndLine: lo.ToPtr(1), + StartColumn: lo.ToPtr(1), + EndColumn: lo.ToPtr(1), + }, + }, + }, + }, + }, + }, + ColumnKind: "utf16CodeUnits", + OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{ + "ROOTPATH": { + URI: lo.ToPtr("file:///"), + }, + }, + }, + }, + }, + }, + { + name: "report with secrets", + input: types.Report{ + Results: types.Results{ + { + Target: "library/test 1", + Class: types.ClassSecret, + Secrets: []types.DetectedSecret{ + { + RuleID: "aws-secret-access-key", + Category: "AWS", + Severity: "CRITICAL", + Title: "AWS Secret Access Key", + StartLine: 1, + EndLine: 1, + Match: "'AWS_secret_KEY'=\"****************************************\"", + }, + }, + }, + }, + }, + want: &sarif.Report{ + Version: "2.1.0", + Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + Runs: []*sarif.Run{ + { + Tool: sarif.Tool{ + Driver: &sarif.ToolComponent{ + FullName: lo.ToPtr("Trivy Vulnerability Scanner"), + Name: "Trivy", + Version: lo.ToPtr(""), + InformationURI: lo.ToPtr("https://github.com/aquasecurity/trivy"), + Rules: []*sarif.ReportingDescriptor{ + { + ID: "aws-secret-access-key", + Name: lo.ToPtr("Secret"), + ShortDescription: &sarif.MultiformatMessageString{Text: lo.ToPtr("AWS Secret Access Key")}, + FullDescription: &sarif.MultiformatMessageString{Text: lo.ToPtr("'AWS_secret_KEY'=\"****************************************\"")}, + DefaultConfiguration: &sarif.ReportingConfiguration{ + Level: "error", + }, + HelpURI: lo.ToPtr("https://github.com/aquasecurity/trivy/blob/main/pkg/fanal/secret/builtin-rules.go"), + Properties: map[string]any{ + "tags": []any{ + "secret", + "security", + "CRITICAL", + }, + "precision": "very-high", + "security-severity": "9.5", + }, + Help: &sarif.MultiformatMessageString{ + Text: lo.ToPtr("Secret AWS Secret Access Key\nSeverity: CRITICAL\nMatch: 'AWS_secret_KEY'=\"****************************************\""), + Markdown: lo.ToPtr("**Secret AWS Secret Access Key**\n| Severity | Match |\n| --- | --- |\n|CRITICAL|'AWS_secret_KEY'=\"****************************************\"|"), + }, + }, + }, + }, + }, + Results: []*sarif.Result{ + { + RuleID: lo.ToPtr("aws-secret-access-key"), + RuleIndex: lo.ToPtr[uint](0), + Level: lo.ToPtr("error"), + Message: sarif.Message{Text: lo.ToPtr("Artifact: library/test 1\nType: \nSecret AWS Secret Access Key\nSeverity: CRITICAL\nMatch: 'AWS_secret_KEY'=\"****************************************\"")}, + Locations: []*sarif.Location{ + { + Message: &sarif.Message{Text: lo.ToPtr("library/test 1")}, + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{ + URI: lo.ToPtr("library/test%201"), + URIBaseId: lo.ToPtr("ROOTPATH"), + }, + Region: &sarif.Region{ + StartLine: lo.ToPtr(1), + EndLine: lo.ToPtr(1), + StartColumn: lo.ToPtr(1), + EndColumn: lo.ToPtr(1), + }, + }, + }, + }, + }, + }, + ColumnKind: "utf16CodeUnits", + OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{ + "ROOTPATH": { + URI: lo.ToPtr("file:///"), + }, + }, + }, + }, + }, + }, + { + name: "report with licenses", + input: types.Report{ + Results: types.Results{ + { + Target: "OS Packages", + Class: "license", + Licenses: []types.DetectedLicense{ + { + Severity: "HIGH", + Category: "restricted", + PkgName: "alpine-base", + FilePath: "", + Name: "GPL-3.0", + Confidence: 1, + Link: "", + }, + }, + }, + }, + }, + want: &sarif.Report{ + Version: "2.1.0", + Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + Runs: []*sarif.Run{ + { + Tool: sarif.Tool{ + Driver: &sarif.ToolComponent{ + FullName: lo.ToPtr("Trivy Vulnerability Scanner"), + Name: "Trivy", + Version: lo.ToPtr(""), + InformationURI: lo.ToPtr("https://github.com/aquasecurity/trivy"), + Rules: []*sarif.ReportingDescriptor{ + { + ID: "alpine-base:GPL-3.0", + Name: lo.ToPtr("License"), + ShortDescription: sarif.NewMultiformatMessageString("GPL-3.0 in alpine-base"), + FullDescription: sarif.NewMultiformatMessageString("GPL-3.0 in alpine-base"), + DefaultConfiguration: sarif.NewReportingConfiguration().WithLevel("error"), + Help: sarif.NewMultiformatMessageString("License GPL-3.0\nClassification: restricted\nPkgName: alpine-base\nPath: "). + WithMarkdown("**License GPL-3.0**\n| PkgName | Classification | Path |\n| --- | --- | --- |\n|alpine-base|restricted||"), + Properties: map[string]any{ + "tags": []any{ + "license", + "security", + "HIGH", + }, + "precision": "very-high", + "security-severity": "8.0", + }, + }, + }, + }, + }, + Results: []*sarif.Result{ + { + RuleID: lo.ToPtr("alpine-base:GPL-3.0"), + RuleIndex: lo.ToPtr(uint(0)), + Level: lo.ToPtr("error"), + Message: sarif.Message{Text: lo.ToPtr("Artifact: OS Packages\nLicense GPL-3.0\nPkgName: alpine-base\n Classification: restricted\n Path: ")}, + Locations: []*sarif.Location{ + { + Message: sarif.NewTextMessage(""), + PhysicalLocation: &sarif.PhysicalLocation{ + ArtifactLocation: &sarif.ArtifactLocation{ + URI: lo.ToPtr("OS%20Packages"), + URIBaseId: lo.ToPtr("ROOTPATH"), + }, + Region: &sarif.Region{ + StartLine: lo.ToPtr(1), + EndLine: lo.ToPtr(1), + StartColumn: lo.ToPtr(1), + EndColumn: lo.ToPtr(1), + }, + }, + }, + }, + }, + }, + ColumnKind: "utf16CodeUnits", + OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{ + "ROOTPATH": { + URI: lo.ToPtr("file:///"), + }, + }, + }, + }, + }, + }, + { + name: "no vulns", + want: &sarif.Report{ + Version: "2.1.0", + Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + Runs: []*sarif.Run{ + { + Tool: sarif.Tool{ + Driver: &sarif.ToolComponent{ + FullName: lo.ToPtr("Trivy Vulnerability Scanner"), + Name: "Trivy", + Version: lo.ToPtr(""), + InformationURI: lo.ToPtr("https://github.com/aquasecurity/trivy"), + Rules: []*sarif.ReportingDescriptor{}, + }, + }, + Results: []*sarif.Result{}, + ColumnKind: "utf16CodeUnits", + OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{ + "ROOTPATH": { + URI: lo.ToPtr("file:///"), + }, + }, + }, + }, + }, + }, + { + name: "ref to github", + input: types.Report{ + Results: types.Results{ + { + Target: "git::https:/github.com/terraform-google-modules/terraform-google-kubernetes-engine?ref=c4809044b52b91505bfba5ef9f25526aa0361788/modules/workload-identity/main.tf", + Class: types.ClassConfig, + Type: ftypes.Terraform, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Terraform Security Check", + ID: "AVD-GCP-0007", + AVDID: "AVD-GCP-0007", + Title: "Service accounts should not have roles assigned with excessive privileges", + Description: "Service accounts should have a minimal set of permissions assigned in order to do their job. They should never have excessive access as if compromised, an attacker can escalate privileges and take over the entire account.", + Message: "Service account is granted a privileged role.", + Query: "data..", + Resolution: "Limit service account access to minimal required set", + Severity: "HIGH", + PrimaryURL: "https://avd.aquasec.com/misconfig/avd-gcp-0007", + References: []string{ + "https://cloud.google.com/iam/docs/understanding-roles", + "https://avd.aquasec.com/misconfig/avd-gcp-0007", + }, + Status: "Fail", + CauseMetadata: ftypes.CauseMetadata{ + StartLine: 91, + EndLine: 91, + Occurrences: []ftypes.Occurrence{ + { + Resource: "google_project_iam_member.workload_identity_sa_bindings[\"roles/storage.admin\"]", + Filename: "git::https:/github.com/terraform-google-modules/terraform-google-kubernetes-engine?ref=c4809044b52b91505bfba5ef9f25526aa0361788/modules/workload-identity/main.tf", + Location: ftypes.Location{ + StartLine: 87, + EndLine: 93, + }, + }, + }, + }, + }, + }, + }, + { + Target: "git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git?ref=v4.2.0/main.tf", + Class: types.ClassConfig, + Type: ftypes.Terraform, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Terraform Security Check", + ID: "AVD-GCP-0007", + AVDID: "AVD-GCP-0007", + Title: "Service accounts should not have roles assigned with excessive privileges", + Description: "Service accounts should have a minimal set of permissions assigned in order to do their job. They should never have excessive access as if compromised, an attacker can escalate privileges and take over the entire account.", + Message: "Service account is granted a privileged role.", + Query: "data..", + Resolution: "Limit service account access to minimal required set", + Severity: "HIGH", + PrimaryURL: "https://avd.aquasec.com/misconfig/avd-gcp-0007", + References: []string{ + "https://cloud.google.com/iam/docs/understanding-roles", + "https://avd.aquasec.com/misconfig/avd-gcp-0007", + }, + Status: "Fail", + CauseMetadata: ftypes.CauseMetadata{ + StartLine: 91, + EndLine: 91, + Occurrences: []ftypes.Occurrence{ + { + Resource: "google_project_iam_member.workload_identity_sa_bindings[\"roles/storage.admin\"]", + Filename: "git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git?ref=v4.2.0/main.tf", + Location: ftypes.Location{ + StartLine: 87, + EndLine: 93, + }, + }, + }, + }, + }, + }, + }, + }, + }, + want: &sarif.Report{ + Version: "2.1.0", + Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json", + Runs: []*sarif.Run{ + { + Tool: *sarif.NewTool( + &sarif.ToolComponent{ + FullName: lo.ToPtr("Trivy Vulnerability Scanner"), + Name: "Trivy", + Version: lo.ToPtr(""), + InformationURI: lo.ToPtr("https://github.com/aquasecurity/trivy"), + Rules: []*sarif.ReportingDescriptor{ + { + ID: "AVD-GCP-0007", + Name: lo.ToPtr("Misconfiguration"), + ShortDescription: sarif.NewMultiformatMessageString("Service accounts should not have roles assigned with excessive privileges"), + FullDescription: sarif.NewMultiformatMessageString("Service accounts should have a minimal set of permissions assigned in order to do their job. They should never have excessive access as if compromised, an attacker can escalate privileges and take over the entire account."), + DefaultConfiguration: &sarif.ReportingConfiguration{ + Level: "error", + }, + HelpURI: lo.ToPtr("https://avd.aquasec.com/misconfig/avd-gcp-0007"), + Help: &sarif.MultiformatMessageString{ + Text: lo.ToPtr("Misconfiguration AVD-GCP-0007\nType: Terraform Security Check\nSeverity: HIGH\nCheck: Service accounts should not have roles assigned with excessive privileges\nMessage: Service account is granted a privileged role.\nLink: [AVD-GCP-0007](https://avd.aquasec.com/misconfig/avd-gcp-0007)\nService accounts should have a minimal set of permissions assigned in order to do their job. They should never have excessive access as if compromised, an attacker can escalate privileges and take over the entire account."), + Markdown: lo.ToPtr("**Misconfiguration AVD-GCP-0007**\n| Type | Severity | Check | Message | Link |\n| --- | --- | --- | --- | --- |\n|Terraform Security Check|HIGH|Service accounts should not have roles assigned with excessive privileges|Service account is granted a privileged role.|[AVD-GCP-0007](https://avd.aquasec.com/misconfig/avd-gcp-0007)|\n\nService accounts should have a minimal set of permissions assigned in order to do their job. They should never have excessive access as if compromised, an attacker can escalate privileges and take over the entire account."), + }, + Properties: sarif.Properties{ + "tags": []any{ + "misconfiguration", + "security", + "HIGH", + }, + "precision": "very-high", + "security-severity": "8.0", + }, + }, + }, + }, + ), + Results: []*sarif.Result{ + { + RuleID: lo.ToPtr("AVD-GCP-0007"), + RuleIndex: lo.ToPtr(uint(0)), + Level: lo.ToPtr("error"), + Message: *sarif.NewTextMessage("Artifact: github.com/terraform-google-modules/terraform-google-kubernetes-engine?ref=c4809044b52b91505bfba5ef9f25526aa0361788/modules/workload-identity/main.tf\nType: terraform\nVulnerability AVD-GCP-0007\nSeverity: HIGH\nMessage: Service account is granted a privileged role.\nLink: [AVD-GCP-0007](https://avd.aquasec.com/misconfig/avd-gcp-0007)"), + Locations: []*sarif.Location{ + { + PhysicalLocation: sarif.NewPhysicalLocation(). + WithArtifactLocation( + &sarif.ArtifactLocation{ + URI: lo.ToPtr("github.com/terraform-google-modules/terraform-google-kubernetes-engine?ref=c4809044b52b91505bfba5ef9f25526aa0361788/modules/workload-identity/main.tf"), + URIBaseId: lo.ToPtr("ROOTPATH"), + }, + ). + WithRegion( + &sarif.Region{ + StartLine: lo.ToPtr(91), + StartColumn: lo.ToPtr(1), + EndLine: lo.ToPtr(91), + EndColumn: lo.ToPtr(1), + }, + ), + Message: sarif.NewTextMessage("github.com/terraform-google-modules/terraform-google-kubernetes-engine?ref=c4809044b52b91505bfba5ef9f25526aa0361788/modules/workload-identity/main.tf"), + }, + }, + }, + { + RuleID: lo.ToPtr("AVD-GCP-0007"), + RuleIndex: lo.ToPtr(uint(0)), + Level: lo.ToPtr("error"), + Message: *sarif.NewTextMessage("Artifact: github.com/terraform-aws-modules/terraform-aws-s3-bucket/tree/v4.2.0/main.tf\nType: terraform\nVulnerability AVD-GCP-0007\nSeverity: HIGH\nMessage: Service account is granted a privileged role.\nLink: [AVD-GCP-0007](https://avd.aquasec.com/misconfig/avd-gcp-0007)"), + Locations: []*sarif.Location{ + { + PhysicalLocation: sarif.NewPhysicalLocation(). + WithArtifactLocation( + &sarif.ArtifactLocation{ + URI: lo.ToPtr("github.com/terraform-aws-modules/terraform-aws-s3-bucket/tree/v4.2.0/main.tf"), + URIBaseId: lo.ToPtr("ROOTPATH"), + }, + ). + WithRegion( + &sarif.Region{ + StartLine: lo.ToPtr(91), + StartColumn: lo.ToPtr(1), + EndLine: lo.ToPtr(91), + EndColumn: lo.ToPtr(1), + }, + ), + Message: sarif.NewTextMessage("github.com/terraform-aws-modules/terraform-aws-s3-bucket/tree/v4.2.0/main.tf"), + }, + }, + }, + }, + ColumnKind: "utf16CodeUnits", + OriginalUriBaseIDs: map[string]*sarif.ArtifactLocation{ + "ROOTPATH": { + URI: lo.ToPtr("file:///"), + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sarifWritten := bytes.NewBuffer(nil) + w := report.SarifWriter{ + Output: sarifWritten, + } + err := w.Write(t.Context(), tt.input) + require.NoError(t, err) + + result := &sarif.Report{} + err = json.Unmarshal(sarifWritten.Bytes(), result) + require.NoError(t, err) + assert.Equal(t, tt.want, result) + }) + } +} + +func TestToPathUri(t *testing.T) { + tests := []struct { + input string + resultClass types.ResultClass + output string + }{ + { + input: "almalinux@sha256:08042694fffd61e6a0b3a22dadba207c8937977915ff6b1879ad744fd6638837", + resultClass: types.ClassOSPkg, + output: "library/almalinux", + }, + { + input: "alpine:latest (alpine 3.13.4)", + resultClass: types.ClassOSPkg, + output: "library/alpine", + }, + { + input: "docker.io/my-organization/my-app:2c6912aee7bde44b84d810aed106ca84f40e2e29", + resultClass: types.ClassOSPkg, + output: "my-organization/my-app", + }, + { + input: "lib/test", + resultClass: types.ClassLangPkg, + output: "lib/test", + }, + { + input: "lib(2)/test", + resultClass: types.ClassSecret, + output: "lib(2)/test", + }, + } + + for _, test := range tests { + got := report.ToPathUri(test.input, test.resultClass) + if got != test.output { + t.Errorf("toPathUri(%q) got %q, wanted %q", test.input, got, test.output) + } + } +} + +func Test_clearURI(t *testing.T) { + test := []struct { + name string + uri string + want string + }{ + { + name: "https", + uri: "bitbucket.org/hashicorp/terraform-consul-aws", + want: "bitbucket.org/hashicorp/terraform-consul-aws", + }, + { + name: "github", + uri: "git@github.com:terraform-aws-modules/terraform-aws-s3-bucket.git?ref=v4.2.0/main.tf", + want: "github.com/terraform-aws-modules/terraform-aws-s3-bucket/tree/v4.2.0/main.tf", + }, + { + name: "git", + uri: "git::https://example.com/storage.git?ref=51d462976d84fdea54b47d80dcabbf680badcdb8", + want: "https://example.com/storage?ref=51d462976d84fdea54b47d80dcabbf680badcdb8", + }, + { + name: "git ssh", + uri: "git::ssh://username@example.com/storage.git", + want: "example.com/storage", + }, + { + name: "hg", + uri: "hg::http://example.com/vpc.hg?ref=v1.2.0", + want: "http://example.com/vpc?ref=v1.2.0", + }, + { + name: "s3", + uri: "s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", + want: "https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", + }, + { + name: "gcs", + uri: "gcs::https://www.googleapis.com/storage/v1/modules/foomodule.zip", + want: "https://www.googleapis.com/storage/v1/modules/foomodule.zip", + }, + } + + for _, tt := range test { + t.Run(tt.name, func(t *testing.T) { + got := report.ClearURI(tt.uri) + require.Equal(t, tt.want, got) + require.NotNil(t, report.ToUri(got)) + }) + } +} + +func TestMakePropertiesMarshal(t *testing.T) { + tests := []struct { + name string + title string + severity string + cvssScore string + cvssData map[string]any + expected string + }{ + { + name: "no CVSS data", + title: "test", + severity: "HIGH", + cvssScore: "5.0", + cvssData: make(map[string]any), + expected: `{ + "precision": "very-high", + "security-severity": "5.0", + "tags": ["test", "security", "HIGH"] + }`, + }, + { + name: "only CVSS v2", + title: "test", + severity: "CRITICAL", + cvssScore: "4.0", + cvssData: map[string]any{ + "cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "cvssv2_score": 5.0, + }, + expected: `{ + "cvssv2_score": 5, + "cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:N/A:N", + "precision": "very-high", + "security-severity": "4.0", + "tags": ["test", "security", "CRITICAL"] + }`, + }, + { + name: "only CVSS v3", + title: "test", + severity: "CRITICAL", + cvssScore: "9.8", + cvssData: map[string]any{ + "cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "cvssv3_baseScore": 9.8, + }, + expected: `{ + "cvssv3_baseScore": 9.8, + "cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "precision": "very-high", + "security-severity": "9.8", + "tags": ["test", "security", "CRITICAL"] + }`, + }, + { + name: "only CVSS v4", + title: "test", + severity: "LOW", + cvssScore: "3.5", + cvssData: map[string]any{ + "cvssv40_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", + "cvssv40_baseScore": 3.5, + }, + expected: `{ + "cvssv40_baseScore": 3.5, + "cvssv40_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", + "precision": "very-high", + "security-severity": "3.5", + "tags": ["test", "security", "LOW"] + }`, + }, + { + name: "all CVSS versions", + title: "test", + severity: "HIGH", + cvssScore: "8.1", + cvssData: map[string]any{ + "cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "cvssv2_score": 7.5, + "cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "cvssv3_baseScore": 9.8, + "cvssv40_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", + "cvssv40_baseScore": 9.3, + }, + expected: `{ + "cvssv2_score": 7.5, + "cvssv2_vector": "AV:N/AC:L/Au:N/C:P/I:P/A:P", + "cvssv3_baseScore": 9.8, + "cvssv3_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + "cvssv40_baseScore": 9.3, + "cvssv40_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", + "precision": "very-high", + "security-severity": "8.1", + "tags": ["test", "security", "HIGH"] + }`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := report.ToProperties(tt.title, tt.severity, tt.cvssScore, tt.cvssData) + + actualJSON, err := json.Marshal(result) + require.NoError(t, err) + + var expectedJSON bytes.Buffer + err = json.Compact(&expectedJSON, []byte(tt.expected)) + require.NoError(t, err) + assert.JSONEq(t, expectedJSON.String(), string(actualJSON)) + }) + } +} diff --git a/pkg/report/spdx/spdx.go b/pkg/report/spdx/spdx.go new file mode 100644 index 000000000000..a8550ddca0f3 --- /dev/null +++ b/pkg/report/spdx/spdx.go @@ -0,0 +1,65 @@ +package spdx + +import ( + "context" + "encoding/json" + "io" + + "github.com/spdx/tools-golang/spdx/v2/v2_3" + "github.com/spdx/tools-golang/tagvalue" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/sbom/spdx" + "github.com/aquasecurity/trivy/pkg/types" +) + +type Writer struct { + output io.Writer + version string + format types.Format + marshaler *spdx.Marshaler +} + +func NewWriter(output io.Writer, version string, spdxFormat types.Format) Writer { + return Writer{ + output: output, + version: version, + format: spdxFormat, + marshaler: spdx.NewMarshaler(version), + } +} + +func (w Writer) Write(ctx context.Context, report types.Report) error { + spdxDoc, err := w.marshaler.MarshalReport(ctx, report) + if err != nil { + return xerrors.Errorf("failed to marshal spdx: %w", err) + } + + if w.format == "spdx-json" { + if err := writeSPDXJson(spdxDoc, w.output); err != nil { + return xerrors.Errorf("failed to save spdx json: %w", err) + } + } else { + if err := tagvalue.Write(spdxDoc, w.output); err != nil { + return xerrors.Errorf("failed to save spdx tag-value: %w", err) + } + } + + return nil +} + +// writeSPDXJson writes in human-readable format(multiple lines) +// use function from `github.com/spdx/tools-golang` after release https://github.com/spdx/tools-golang/pull/213 +func writeSPDXJson(doc *v2_3.Document, w io.Writer) error { + buf, err := json.MarshalIndent(doc, "", " ") + if err != nil { + return err + } + + _, err = w.Write(buf) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/report/table/licensing.go b/pkg/report/table/licensing.go new file mode 100644 index 000000000000..6c6e95786877 --- /dev/null +++ b/pkg/report/table/licensing.go @@ -0,0 +1,209 @@ +package table + +import ( + "bytes" + "sort" + "strings" + "sync" + + "github.com/fatih/color" + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/aquasecurity/table" + "github.com/aquasecurity/tml" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +type pkgLicenseRenderer struct { + w *bytes.Buffer + tableWriter *table.Table + isTerminal bool + severities []dbTypes.Severity + once *sync.Once +} + +func NewPkgLicenseRenderer(buf *bytes.Buffer, isTerminal bool, severities []dbTypes.Severity) *pkgLicenseRenderer { + return &pkgLicenseRenderer{ + w: buf, + tableWriter: newTableWriter(buf, isTerminal), + isTerminal: isTerminal, + severities: severities, + once: new(sync.Once), + } +} + +func (r *pkgLicenseRenderer) Render(result types.Result) { + // Trivy doesn't currently support showing suppressed licenses + // So just skip this result + if len(result.Licenses) == 0 { + return + } + + r.setHeaders() + r.setRows(result.Licenses) + + total, summaries := summarize(r.severities, r.countSeverities(result.Licenses)) + + target := result.Target + " (license)" + RenderTarget(r.w, target, r.isTerminal) + r.printf("Total: %d (%s)\n\n", total, strings.Join(summaries, ", ")) + + r.tableWriter.Render() +} + +func (r *pkgLicenseRenderer) setHeaders() { + header := []string{ + "Package", + "License", + "Classification", + "Severity", + } + r.tableWriter.SetHeaders(header...) +} + +func (r *pkgLicenseRenderer) setRows(licenses []types.DetectedLicense) { + for _, l := range licenses { + var row []string + if r.isTerminal { + row = []string{ + l.PkgName, + l.Name, + colorizeLicenseCategory(l.Category), + ColorizeSeverity(l.Severity, l.Severity), + } + } else { + row = []string{ + l.PkgName, + l.Name, + string(l.Category), + l.Severity, + } + } + r.tableWriter.AddRow(row...) + } +} + +func (r *pkgLicenseRenderer) countSeverities(licenses []types.DetectedLicense) map[string]int { + severityCount := make(map[string]int) + for _, l := range licenses { + severityCount[l.Severity]++ + } + return severityCount +} + +func (r *pkgLicenseRenderer) printf(format string, args ...any) { + // nolint + _ = tml.Fprintf(r.w, format, args...) +} + +type fileLicenseRenderer struct { + w *bytes.Buffer + tableWriter *table.Table + isTerminal bool + severities []dbTypes.Severity + once *sync.Once +} + +func NewFileLicenseRenderer(buf *bytes.Buffer, isTerminal bool, severities []dbTypes.Severity) *fileLicenseRenderer { + return &fileLicenseRenderer{ + w: buf, + tableWriter: newTableWriter(buf, isTerminal), + isTerminal: isTerminal, + severities: severities, + once: new(sync.Once), + } +} + +func (r *fileLicenseRenderer) Render(result types.Result) { + // Trivy doesn't currently support showing suppressed licenses + // So just skip this result + if len(result.Licenses) == 0 { + return + } + + r.setHeaders() + r.setRows(result.Licenses) + + total, summaries := summarize(r.severities, r.countSeverities(result.Licenses)) + + target := result.Target + " (license)" + RenderTarget(r.w, target, r.isTerminal) + r.printf("Total: %d (%s)\n\n", total, strings.Join(summaries, ", ")) + + r.tableWriter.Render() +} + +func (r *fileLicenseRenderer) setHeaders() { + header := []string{ + "Classification", + "Severity", + "License", + "File Location", + } + r.tableWriter.SetHeaders(header...) +} + +func (r *fileLicenseRenderer) setRows(licenses []types.DetectedLicense) { + sort.Slice(licenses, func(i, j int) bool { + a := licenses[i] + b := licenses[j] + if a.Severity != b.Severity { + return 0 < dbTypes.CompareSeverityString(b.Severity, a.Severity) + } + if a.Category != b.Category { + return a.Category > b.Category + } + if a.Name != b.Name { + return a.Name < b.Name + } + return a.FilePath < b.FilePath + }) + + for _, l := range licenses { + var row []string + if r.isTerminal { + row = []string{ + colorizeLicenseCategory(l.Category), + ColorizeSeverity(l.Severity, l.Severity), + l.Name, + l.FilePath, + } + } else { + row = []string{ + string(l.Category), + l.Severity, + l.Name, + l.FilePath, + } + } + r.tableWriter.AddRow(row...) + } +} + +func (r *fileLicenseRenderer) countSeverities(licenses []types.DetectedLicense) map[string]int { + severityCount := make(map[string]int) + for _, l := range licenses { + severityCount[l.Severity]++ + } + return severityCount +} + +func (r *fileLicenseRenderer) printf(format string, args ...any) { + // nolint + _ = tml.Fprintf(r.w, format, args...) +} + +func colorizeLicenseCategory(category ftypes.LicenseCategory) string { + switch category { + case ftypes.CategoryForbidden: + return color.New(color.FgRed).Sprintf("Forbidden") + case ftypes.CategoryRestricted: + return color.New(color.FgHiRed).Sprintf("Restricted") + case ftypes.CategoryUnknown: + return color.New(color.FgCyan).Sprintf("Non Standard") + } + return cases.Title(language.AmericanEnglish).String(string(category)) +} diff --git a/pkg/report/table/misconfig.go b/pkg/report/table/misconfig.go new file mode 100644 index 000000000000..a03ea4c2d901 --- /dev/null +++ b/pkg/report/table/misconfig.go @@ -0,0 +1,263 @@ +package table + +import ( + "bytes" + "fmt" + "slices" + "strconv" + "strings" + + "github.com/fatih/color" + "golang.org/x/term" + + "github.com/aquasecurity/tml" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +const ( + severityCritical = "CRITICAL" + severityHigh = "HIGH" + severityMedium = "MEDIUM" + severityLow = "LOW" +) + +type misconfigRenderer struct { + w *bytes.Buffer + severities []dbTypes.Severity + trace bool + includeNonFailures bool + width int + ansi bool + renderCause []ftypes.ConfigType +} + +func NewMisconfigRenderer(buf *bytes.Buffer, severities []dbTypes.Severity, trace, includeNonFailures, ansi bool, renderCause []ftypes.ConfigType) *misconfigRenderer { + width, _, err := term.GetSize(0) + if err != nil || width == 0 { + width = 40 + } + if !ansi { + tml.DisableFormatting() + } + + return &misconfigRenderer{ + w: buf, + severities: severities, + trace: trace, + includeNonFailures: includeNonFailures, + width: width, + ansi: ansi, + renderCause: renderCause, + } +} + +func (r *misconfigRenderer) Render(result types.Result) { + // Trivy doesn't currently support showing suppressed misconfigs + // So just skip this result + if len(result.Misconfigurations) == 0 { + return + } + target := fmt.Sprintf("%s (%s)", result.Target, result.Type) + RenderTarget(r.w, target, r.ansi) + + total, summaries := summarize(r.severities, r.countSeverities(result.Misconfigurations)) + + summary := result.MisconfSummary + r.printf("Tests: %d (SUCCESSES: %d, FAILURES: %d)\n", + summary.Successes+summary.Failures, summary.Successes, summary.Failures) + r.printf("Failures: %d (%s)\n\n", total, strings.Join(summaries, ", ")) + + for _, m := range result.Misconfigurations { + r.renderSingle(result.Target, result.Type, m) + } + + // For debugging + if r.trace { + r.outputTrace(result.Target, result.Misconfigurations) + } +} + +func (r *misconfigRenderer) countSeverities(misconfigs []types.DetectedMisconfiguration) map[string]int { + severityCount := make(map[string]int) + for _, misconf := range misconfigs { + if misconf.Status == types.MisconfStatusFailure { + severityCount[misconf.Severity]++ + } + } + return severityCount +} + +func (r *misconfigRenderer) printf(format string, args ...any) { + // nolint + _ = tml.Fprintf(r.w, format, args...) +} + +func (r *misconfigRenderer) println(input string) { + // nolint + tml.Fprintln(r.w, input) +} + +func (r *misconfigRenderer) printDoubleDivider() { + r.printf("%s\r\n", strings.Repeat("═", r.width)) +} + +func (r *misconfigRenderer) printSingleDivider() { + r.printf("%s\r\n", strings.Repeat("─", r.width)) +} + +func (r *misconfigRenderer) renderSingle(target string, typ ftypes.TargetType, misconf types.DetectedMisconfiguration) { + r.renderSummary(misconf) + r.renderCode(target, misconf) + r.renderMisconfCause(typ, misconf) + r.printf("\r\n\r\n") +} + +func (r *misconfigRenderer) renderSummary(misconf types.DetectedMisconfiguration) { + // show pass/fail/exception unless we are only showing failures + if r.includeNonFailures { + switch misconf.Status { + case types.MisconfStatusPassed: + r.printf("%s: ", misconf.Status) + case types.MisconfStatusFailure: + r.printf("%s: ", misconf.Status) + case types.MisconfStatusException: + r.printf("%s: ", misconf.Status) + } + } + + // ID & severity + switch misconf.Severity { + case severityCritical: + r.printf("%s (%s): ", misconf.ID, misconf.Severity) + case severityHigh: + r.printf("%s (%s): ", misconf.ID, misconf.Severity) + case severityMedium: + r.printf("%s (%s): ", misconf.ID, misconf.Severity) + case severityLow: + r.printf("%s (%s): ", misconf.ID, misconf.Severity) + default: + r.printf("%s (%s): ", misconf.ID, misconf.Severity) + } + + // heading + r.printf("%s\r\n", misconf.Message) + r.printDoubleDivider() + + // description + r.printf("%s\r\n", misconf.Description) + + // show link if we have one + if misconf.PrimaryURL != "" { + r.printf("\r\nSee %s\r\n", misconf.PrimaryURL) + } + + r.printSingleDivider() +} + +func (r *misconfigRenderer) renderCode(target string, misconf types.DetectedMisconfiguration) { + // highlight code if we can... + if lines := misconf.CauseMetadata.Code.Lines; len(lines) > 0 { + + var lineInfo string + if misconf.CauseMetadata.StartLine > 0 { + lineInfo = tml.Sprintf(":%d", misconf.CauseMetadata.StartLine) + if misconf.CauseMetadata.EndLine > misconf.CauseMetadata.StartLine { + lineInfo = tml.Sprintf("%s-%d", lineInfo, misconf.CauseMetadata.EndLine) + } + } + r.printf(" %s%s\r\n", target, lineInfo) + for i, occ := range misconf.CauseMetadata.Occurrences { + lineInfo := fmt.Sprintf("%d-%d", occ.Location.StartLine, occ.Location.EndLine) + if occ.Location.StartLine >= occ.Location.EndLine { + lineInfo = strconv.Itoa(occ.Location.StartLine) + } + + r.printf( + " %svia %s:%s (%s)\n", + strings.Repeat(" ", i+2), + occ.Filename, + lineInfo, + occ.Resource, + ) + } + + r.printSingleDivider() + for i, line := range lines { + switch { + case line.Truncated: + r.printf("%4s ", strings.Repeat(".", len(strconv.Itoa(line.Number)))) + case line.IsCause: + r.printf("%4d ", line.Number) + switch { + case (line.FirstCause && line.LastCause) || len(lines) == 1: + r.printf("[ ") + case line.FirstCause || i == 0: + r.printf("┌ ") + case line.LastCause || i == len(lines)-1: + r.printf("└ ") + default: + r.printf("│ ") + } + default: + r.printf("%4d ", line.Number) + } + + if r.ansi { + r.printf("%s\r\n", line.Highlighted) + } else { + r.printf("%s\r\n", line.Content) + } + } + r.printSingleDivider() + } +} + +func (r *misconfigRenderer) renderMisconfCause(typ ftypes.TargetType, misconf types.DetectedMisconfiguration) { + if !slices.Contains(r.renderCause, typ) { + return + } + + cause := misconf.CauseMetadata.RenderedCause + if cause.Raw == "" { + return + } + + content := cause.Raw + if cause.Highlighted != "" { + content = cause.Highlighted + } + + r.printf("%s\r\n", "Rendered cause:") + r.printSingleDivider() + r.println(content) + r.printSingleDivider() +} + +func (r *misconfigRenderer) outputTrace(target string, misconfigs []types.DetectedMisconfiguration) { + blue := color.New(color.FgBlue).SprintFunc() + green := color.New(color.FgGreen).SprintfFunc() + red := color.New(color.FgRed).SprintfFunc() + + for _, misconf := range misconfigs { + if len(misconf.Traces) == 0 { + continue + } + + c := green + if misconf.Status == types.MisconfStatusFailure { + c = red + } + + r.println(c("\nID: %s", misconf.ID)) + r.println(c("File: %s", target)) + r.println(c("Namespace: %s", misconf.Namespace)) + r.println(c("Query: %s", misconf.Query)) + r.println(c("Message: %s", misconf.Message)) + for _, t := range misconf.Traces { + r.println(blue("TRACE ") + t) + } + r.println("") + } +} diff --git a/pkg/report/table/misconfig_test.go b/pkg/report/table/misconfig_test.go new file mode 100644 index 000000000000..dec8d678ad43 --- /dev/null +++ b/pkg/report/table/misconfig_test.go @@ -0,0 +1,467 @@ +package table_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestMisconfigRenderer(t *testing.T) { + type args struct { + includeNonFailures bool + renderCause []ftypes.ConfigType + } + tests := []struct { + name string + input types.Result + args args + want string + }{ + { + name: "single result", + input: types.Result{ + Target: "my-file", + MisconfSummary: &types.MisconfSummary{Successes: 0, Failures: 1}, + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "XYZ-0123", + AVDID: "AVD-XYZ-0123", + Title: "Config file is bad", + Description: "Your config file is not good.", + Message: "Oh no, a bad config.", + Severity: "HIGH", + PrimaryURL: "https://google.com/search?q=bad%20config", + Status: "FAIL", + }, + }, + }, + args: args{ + includeNonFailures: false, + }, + want: ` +my-file () +========== +Tests: 1 (SUCCESSES: 0, FAILURES: 1) +Failures: 1 (LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +XYZ-0123 (HIGH): Oh no, a bad config. +════════════════════════════════════════ +Your config file is not good. + +See https://google.com/search?q=bad%20config +──────────────────────────────────────── + + +`, + }, + { + name: "single result with code", + input: types.Result{ + Target: "my-file", + MisconfSummary: &types.MisconfSummary{Successes: 0, Failures: 1}, + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "XYZ-0123", + AVDID: "AVD-XYZ-0123", + Title: "Config file is bad", + Description: "Your config file is not good.", + Message: "Oh no, a bad config.", + Severity: "HIGH", + PrimaryURL: "https://google.com/search?q=bad%20config", + Status: "FAIL", + CauseMetadata: ftypes.CauseMetadata{ + Resource: "", + Provider: "", + Service: "", + StartLine: 0, + EndLine: 0, + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 1, + }, + { + Number: 2, + Content: "bad: true", + Highlighted: "\x1b[37mbad:\x1b[0m true", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + }, + }, + }, + }, + }, + }, + }, + args: args{ + includeNonFailures: false, + }, + want: ` +my-file () +========== +Tests: 1 (SUCCESSES: 0, FAILURES: 1) +Failures: 1 (LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +XYZ-0123 (HIGH): Oh no, a bad config. +════════════════════════════════════════ +Your config file is not good. + +See https://google.com/search?q=bad%20config +──────────────────────────────────────── + my-file +──────────────────────────────────────── + 1 + 2 [ bad: true + 3 +──────────────────────────────────────── + + +`, + }, + { + name: "multiple results", + input: types.Result{ + Target: "my-file", + MisconfSummary: &types.MisconfSummary{Successes: 1, Failures: 1}, + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "XYZ-0123", + AVDID: "AVD-XYZ-0123", + Title: "Config file is bad", + Description: "Your config file is not good.", + Message: "Oh no, a bad config.", + Severity: "HIGH", + PrimaryURL: "https://google.com/search?q=bad%20config", + Status: "FAIL", + CauseMetadata: ftypes.CauseMetadata{ + StartLine: 2, + EndLine: 2, + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 1, + }, + { + Number: 2, + Content: "bad: true", + Highlighted: "\x1b[37mbad:\x1b[0m true", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + { + Number: 3, + }, + }, + }, + }, + }, + { + ID: "XYZ-0456", + AVDID: "AVD-XYZ-0456", + Title: "Config file is bad again", + Description: "Your config file is still not good.", + Message: "Oh no, a bad config AGAIN.", + Severity: "MEDIUM", + PrimaryURL: "https://google.com/search?q=bad%20config", + Status: "PASS", + }, + }, + }, + args: args{ + includeNonFailures: true, + }, + want: ` +my-file () +========== +Tests: 2 (SUCCESSES: 1, FAILURES: 1) +Failures: 1 (LOW: 0, MEDIUM: 0, HIGH: 1, CRITICAL: 0) + +FAIL: XYZ-0123 (HIGH): Oh no, a bad config. +════════════════════════════════════════ +Your config file is not good. + +See https://google.com/search?q=bad%20config +──────────────────────────────────────── + my-file:2 +──────────────────────────────────────── + 1 + 2 [ bad: true + 3 +──────────────────────────────────────── + + +PASS: XYZ-0456 (MEDIUM): Oh no, a bad config AGAIN. +════════════════════════════════════════ +Your config file is still not good. + +See https://google.com/search?q=bad%20config +──────────────────────────────────────── + + +`, + }, + { + name: "resource name in report", + input: types.Result{ + Target: "terraform-aws-modules/security-group/aws/main.tf", + Class: types.ClassConfig, + Type: ftypes.Terraform, + MisconfSummary: &types.MisconfSummary{ + Successes: 5, + Failures: 1, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Terraform Security Check", + ID: "AVD-AWS-0107", + AVDID: "AVD-AWS-0107", + Title: "An ingress security group rule allows traffic from /0", + Description: "Opening up ports to the public internet is generally to be avoided. You should restrict access to IP addresses or ranges that explicitly require it where possible.", + Message: "Security group rule allows ingress from public internet.", + Query: "data..", + Resolution: "Set a more restrictive cidr range", + Severity: "CRITICAL", + PrimaryURL: "https://avd.aquasec.com/misconfig/avd-aws-0107", + References: []string{ + "https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/security-group-rules-reference.html", + "https://avd.aquasec.com/misconfig/avd-aws-0107", + }, + Status: "FAIL", + CauseMetadata: ftypes.CauseMetadata{ + Resource: "module.aws-security-groups[\"db1\"]", + Provider: "AWS", + Service: "ec2", + StartLine: 197, + EndLine: 204, + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 191, + Content: "resource \"aws_security_group_rule\" \"ingress_with_cidr_blocks\" {", + IsCause: false, + }, + { + Number: 192, + Truncated: true, + }, + { + Number: 197, + Truncated: true, + }, + { + Number: 198, + Content: " \",\",", + IsCause: true, + }, + { + Number: 199, + Content: " lookup(", + IsCause: true, + }, + { + Number: 200, + Content: " var.ingress_with_cidr_blocks[count.index],", + IsCause: true, + }, + { + Number: 201, + Content: " \"cidr_blocks\",", + IsCause: true, + }, + { + Number: 202, + Content: " join(\",\", var.ingress_cidr_blocks),", + IsCause: true, + }, + { + Number: 203, + Content: " ),", + IsCause: true, + LastCause: true, + }, + { + Number: 204, + Truncated: true, + }, + }, + }, + Occurrences: []ftypes.Occurrence{ + { + Resource: "aws_security_group_rule.ingress_with_cidr_blocks[0]", + Filename: "terraform-aws-modules/security-group/aws/main.tf", + Location: ftypes.Location{ + StartLine: 191, + EndLine: 227, + }, + }, + { + Resource: "module.aws-security-groups[\"db1\"]", + Filename: "sg.tf", + Location: ftypes.Location{ + StartLine: 1, + EndLine: 13, + }, + }, + }, + RenderedCause: ftypes.RenderedCause{ + Raw: "resource \"aws_security_group_rule\" \"ingress_with_cidr_blocks\" {\n cidr_blocks = [ \"0.0.0.0/0\" ]\n}", + }, + }, + }, + }, + }, + want: ` +terraform-aws-modules/security-group/aws/main.tf (terraform) +============================================================ +Tests: 6 (SUCCESSES: 5, FAILURES: 1) +Failures: 1 (LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 1) + +AVD-AWS-0107 (CRITICAL): Security group rule allows ingress from public internet. +════════════════════════════════════════ +Opening up ports to the public internet is generally to be avoided. You should restrict access to IP addresses or ranges that explicitly require it where possible. + +See https://avd.aquasec.com/misconfig/avd-aws-0107 +──────────────────────────────────────── + terraform-aws-modules/security-group/aws/main.tf:197-204 + via terraform-aws-modules/security-group/aws/main.tf:191-227 (aws_security_group_rule.ingress_with_cidr_blocks[0]) + via sg.tf:1-13 (module.aws-security-groups["db1"]) +──────────────────────────────────────── + 191 resource "aws_security_group_rule" "ingress_with_cidr_blocks" { + ... + ... + 198 │ ",", + 199 │ lookup( + 200 │ var.ingress_with_cidr_blocks[count.index], + 201 │ "cidr_blocks", + 202 │ join(",", var.ingress_cidr_blocks), + 203 └ ), + ... +──────────────────────────────────────── + + +`, + }, + { + name: "with rendered cause", + args: args{ + renderCause: []ftypes.ConfigType{ftypes.Terraform}, + }, + input: types.Result{ + Target: "main.tf", + Class: types.ClassConfig, + Type: ftypes.Terraform, + MisconfSummary: &types.MisconfSummary{Failures: 1}, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Terraform Security Check", + ID: "AVD-AWS-0320", + AVDID: "AVD-AWS-0320", + Title: "S3 DNS Compliant Bucket Names", + Description: "Ensures that S3 buckets have DNS complaint bucket names.", + Message: "S3 bucket name is not compliant with DNS naming requirements", + Namespace: "builtin.aws.s3.aws0320", + Query: "data.builtin.aws.s3.aws0320.deny", + Resolution: "Recreate S3 bucket to use - instead of . in S3 bucket names", + Severity: "MEDIUM", + PrimaryURL: "https://avd.aquasec.com/misconfig/avd-aws-0320", + References: []string{ + "https://docs.aws.amazon.com/AmazonS3/latest./dev/transfer-acceleration.html", + "https://avd.aquasec.com/misconfig/avd-aws-0320", + }, + Status: "FAIL", + CauseMetadata: ftypes.CauseMetadata{ + Resource: "aws_s3_bucket.this", + Provider: "AWS", + Service: "s3", + StartLine: 6, + EndLine: 6, + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 5, + Content: "resource \"aws_s3_bucket\" \"this\" {", + }, + { + Number: 6, + Content: " bucket = local.bucket", + IsCause: true, + }, + { + Number: 7, + Content: "}", + }, + }, + }, + Occurrences: []ftypes.Occurrence{ + { + Resource: "aws_s3_bucket.this", + Filename: "main.tf", + Location: ftypes.Location{ + StartLine: 5, + EndLine: 7, + }, + }, + }, + RenderedCause: ftypes.RenderedCause{ + Raw: "resource \"aws_s3_bucket\" \"this\" {\n bucket = \"foo.bar\"\n}", + }, + }, + }, + }, + }, + want: ` +main.tf (terraform) +=================== +Tests: 1 (SUCCESSES: 0, FAILURES: 1) +Failures: 1 (LOW: 0, MEDIUM: 1, HIGH: 0, CRITICAL: 0) + +AVD-AWS-0320 (MEDIUM): S3 bucket name is not compliant with DNS naming requirements +════════════════════════════════════════ +Ensures that S3 buckets have DNS complaint bucket names. + +See https://avd.aquasec.com/misconfig/avd-aws-0320 +──────────────────────────────────────── + main.tf:6 + via main.tf:5-7 (aws_s3_bucket.this) +──────────────────────────────────────── + 5 resource "aws_s3_bucket" "this" { + 6 │ bucket = local.bucket + 7 } +──────────────────────────────────────── +Rendered cause: +──────────────────────────────────────── +resource "aws_s3_bucket" "this" { + bucket = "foo.bar" +} +──────────────────────────────────────── + + +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + severities := []dbTypes.Severity{ + dbTypes.SeverityLow, dbTypes.SeverityMedium, dbTypes.SeverityHigh, + dbTypes.SeverityCritical, + } + buf := bytes.NewBuffer([]byte{}) + renderer := table.NewMisconfigRenderer(buf, severities, false, tt.args.includeNonFailures, false, tt.args.renderCause) + renderer.Render(tt.input) + assert.Equal(t, tt.want, strings.ReplaceAll(buf.String(), "\r\n", "\n")) + }) + } +} diff --git a/pkg/report/table/secret.go b/pkg/report/table/secret.go new file mode 100644 index 000000000000..fa99d5629b8d --- /dev/null +++ b/pkg/report/table/secret.go @@ -0,0 +1,169 @@ +package table + +import ( + "bytes" + "fmt" + "strconv" + "strings" + + "golang.org/x/term" + + "github.com/aquasecurity/tml" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +type secretRenderer struct { + w *bytes.Buffer + severities []dbTypes.Severity + width int + ansi bool +} + +func NewSecretRenderer(buf *bytes.Buffer, ansi bool, severities []dbTypes.Severity) *secretRenderer { + width, _, err := term.GetSize(0) + if err != nil || width == 0 { + width = 40 + } + if !ansi { + tml.DisableFormatting() + } + return &secretRenderer{ + w: buf, + severities: severities, + width: width, + ansi: ansi, + } +} + +func (r *secretRenderer) Render(result types.Result) { + // Trivy doesn't currently support showing suppressed secrets + // So just skip this result + if len(result.Secrets) == 0 { + return + } + target := result.Target + " (secrets)" + RenderTarget(r.w, target, r.ansi) + + severityCount := r.countSeverities(result.Secrets) + total, summaries := summarize(r.severities, severityCount) + + r.printf("Total: %d (%s)\n\n", total, strings.Join(summaries, ", ")) + + for _, m := range result.Secrets { + r.renderSingle(result.Target, m) + } +} + +func (r *secretRenderer) countSeverities(secrets []types.DetectedSecret) map[string]int { + severityCount := make(map[string]int) + for _, secret := range secrets { + severity := secret.Severity + severityCount[severity]++ + } + return severityCount +} + +func (r *secretRenderer) printf(format string, args ...any) { + // nolint + _ = tml.Fprintf(r.w, format, args...) +} + +func (r *secretRenderer) printDoubleDivider() { + r.printf("%s\r\n", strings.Repeat("═", r.width)) +} + +func (r *secretRenderer) printSingleDivider() { + r.printf("%s\r\n", strings.Repeat("─", r.width)) +} + +func (r *secretRenderer) renderSingle(target string, secret types.DetectedSecret) { + r.renderSummary(secret) + r.renderCode(target, secret) + r.printf("\r\n\r\n") +} + +func (r *secretRenderer) renderSummary(secret types.DetectedSecret) { + // severity + switch secret.Severity { + case severityCritical: + r.printf("%s: ", secret.Severity) + case severityHigh: + r.printf("%s: ", secret.Severity) + case severityMedium: + r.printf("%s: ", secret.Severity) + case severityLow: + r.printf("%s: ", secret.Severity) + default: + r.printf("%s: ", secret.Severity) + } + + // heading + r.printf("%s (%s)\r\n", secret.Category, secret.RuleID) + r.printDoubleDivider() + + // description + r.printf("%s\r\n", secret.Title) + + r.printSingleDivider() +} + +func (r *secretRenderer) renderCode(target string, secret types.DetectedSecret) { + // highlight code if we can... + if lines := secret.Code.Lines; len(lines) > 0 { + + var lineInfo string + if secret.StartLine > 0 { + lineInfo = tml.Sprintf(":%d", secret.StartLine) + if secret.EndLine > secret.StartLine { + lineInfo = tml.Sprintf("%s-%d", lineInfo, secret.EndLine) + } + } + + // Add offset information + var offsetInfo string + if secret.Offset > 0 { + offsetInfo = tml.Sprintf(" (offset: %d bytes)", secret.Offset) + } + + var note string + if c := secret.Layer.CreatedBy; c != "" { + if len(c) > 40 { + // Too long + c = c[:40] + } + note = fmt.Sprintf(" (added by '%s')", c) + } else if secret.Layer.DiffID != "" { + note = fmt.Sprintf(" (added in layer '%s')", strings.TrimPrefix(secret.Layer.DiffID, "sha256:")[:12]) + } + r.printf(" %s%s%s%s\r\n", target, lineInfo, offsetInfo, note) + r.printSingleDivider() + + for i, line := range lines { + switch { + case line.Truncated: + r.printf("%4s ", strings.Repeat(".", len(strconv.Itoa(line.Number)))) + case line.IsCause: + r.printf("%4d ", line.Number) + switch { + case (line.FirstCause && line.LastCause) || len(lines) == 1: + r.printf("[ ") + case line.FirstCause || i == 0: + r.printf("┌ ") + case line.LastCause || i == len(lines)-1: + r.printf("└ ") + default: + r.printf("│ ") + } + default: + r.printf("%4d ", line.Number) + } + if r.ansi { + r.printf("%s\r\n", line.Highlighted) + } else { + r.printf("%s\r\n", line.Content) + } + } + r.printSingleDivider() + } +} diff --git a/pkg/report/table/secret_test.go b/pkg/report/table/secret_test.go new file mode 100644 index 000000000000..c7eaca4462a7 --- /dev/null +++ b/pkg/report/table/secret_test.go @@ -0,0 +1,154 @@ +package table_test + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestSecretRenderer(t *testing.T) { + tests := []struct { + name string + input types.Result + want string + }{ + { + name: "single line", + input: types.Result{ + Target: "my-file", + Secrets: []types.DetectedSecret{ + { + RuleID: "rule-id", + Category: ftypes.SecretRuleCategory("category"), + Title: "this is a title", + Severity: "HIGH", + Layer: ftypes.Layer{DiffID: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203"}, + StartLine: 1, + EndLine: 1, + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 1, + Content: "password=secret", + IsCause: true, + FirstCause: true, + LastCause: true, + }, + }, + }, + Match: "secret", + }, + }, + }, + want: ` +my-file (secrets) +================= +Total: 1 (MEDIUM: 0, HIGH: 1) + +HIGH: category (rule-id) +════════════════════════════════════════ +this is a title +──────────────────────────────────────── + my-file:1 (added in layer 'beee9f30bc1f') +──────────────────────────────────────── + 1 [ password=secret +──────────────────────────────────────── + + +`, + }, + { + name: "multiple line", + input: types.Result{ + Target: "my-file", + Secrets: []types.DetectedSecret{ + { + RuleID: "rule-id", + Category: ftypes.SecretRuleCategory("category"), + Title: "this is a title", + Severity: "HIGH", + Layer: ftypes.Layer{ + DiffID: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + CreatedBy: "COPY my-file my-file", + }, + StartLine: 3, + EndLine: 4, + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 1, + Content: "#!/bin/bash", + }, + { + Number: 2, + Content: "", + }, + { + Number: 3, + Content: "password=this is a \\", + IsCause: true, + FirstCause: true, + }, + { + Number: 4, + Content: "secret password", + IsCause: true, + LastCause: true, + }, + { + Number: 5, + Content: "some-app --password $password", + }, + { + Number: 6, + Content: "echo all done", + }, + }, + }, + Match: "secret", + }, + }, + }, + want: ` +my-file (secrets) +================= +Total: 1 (MEDIUM: 0, HIGH: 1) + +HIGH: category (rule-id) +════════════════════════════════════════ +this is a title +──────────────────────────────────────── + my-file:3-4 (added by 'COPY my-file my-file') +──────────────────────────────────────── + 1 #!/bin/bash + 2 + 3 ┌ password=this is a \ + 4 └ secret password + 5 some-app --password $password + 6 echo all done +──────────────────────────────────────── + + +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + renderer := table.NewSecretRenderer(buf, false, []dbTypes.Severity{ + dbTypes.SeverityHigh, + dbTypes.SeverityMedium, + }) + renderer.Render(tt.input) + assert.Equal(t, tt.want, strings.ReplaceAll(buf.String(), "\r\n", "\n")) + }) + } +} diff --git a/pkg/report/table/summary.go b/pkg/report/table/summary.go new file mode 100644 index 000000000000..6307ccd00fc4 --- /dev/null +++ b/pkg/report/table/summary.go @@ -0,0 +1,317 @@ +package table + +import ( + "bytes" + "fmt" + "slices" + "sort" + + "github.com/fatih/color" + "github.com/samber/lo" + + "github.com/aquasecurity/table" + "github.com/aquasecurity/tml" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/langpkg" + "github.com/aquasecurity/trivy/pkg/types" +) + +type Scanner interface { + Header() string + Alignment() table.Alignment + + // Count returns the number of findings, but -1 if the scanner is not applicable + Count(result types.Result) int + + String() string // Required to show correct logs +} + +func NewScanner(scanner types.Scanner) Scanner { + switch scanner { + case types.VulnerabilityScanner: + return VulnerabilityScanner{} + case types.MisconfigScanner: + return MisconfigScanner{} + case types.SecretScanner: + return SecretScanner{} + case types.LicenseScanner: + return LicenseScanner{} + } + return nil +} + +type scannerAlignment struct{} + +func (s scannerAlignment) Alignment() table.Alignment { + return table.AlignCenter +} + +type VulnerabilityScanner struct{ scannerAlignment } + +func (s VulnerabilityScanner) Header() string { + return "Vulnerabilities" +} + +func (s VulnerabilityScanner) Count(result types.Result) int { + if result.Class == types.ClassOSPkg || result.Class == types.ClassLangPkg { + return len(result.Vulnerabilities) + } + return -1 +} + +func (s VulnerabilityScanner) String() string { + return string(types.VulnerabilityScanner) +} + +type MisconfigScanner struct{ scannerAlignment } + +func (s MisconfigScanner) Header() string { + return "Misconfigurations" +} + +func (s MisconfigScanner) Count(result types.Result) int { + if result.Class == types.ClassConfig { + return len(result.Misconfigurations) + } + return -1 +} + +func (s MisconfigScanner) String() string { + return string(types.MisconfigScanner) +} + +type SecretScanner struct{ scannerAlignment } + +func (s SecretScanner) Header() string { + return "Secrets" +} + +func (s SecretScanner) Count(result types.Result) int { + if result.Class == types.ClassSecret { + return len(result.Secrets) + } + return -1 +} + +func (s SecretScanner) String() string { + return string(types.SecretScanner) +} + +type LicenseScanner struct{ scannerAlignment } + +func (s LicenseScanner) Header() string { + return "Licenses" +} + +func (s LicenseScanner) Count(result types.Result) int { + if result.Class == types.ClassLicense || result.Class == types.ClassLicenseFile { + return len(result.Licenses) + } + return -1 +} + +func (s LicenseScanner) String() string { + return string(types.LicenseScanner) +} + +type summaryRenderer struct { + w *bytes.Buffer + isTerminal bool + scanners []Scanner + logger *log.Logger +} + +func NewSummaryRenderer(buf *bytes.Buffer, isTerminal bool, scanners types.Scanners) *summaryRenderer { + if !isTerminal { + tml.DisableFormatting() + } + + var ss []Scanner + for _, scanner := range scanners { + s := NewScanner(scanner) + if lo.IsNil(s) { + continue + } + ss = append(ss, s) + } + + return &summaryRenderer{ + w: buf, + isTerminal: isTerminal, + scanners: ss, + logger: log.WithPrefix("report"), + } +} + +func (r *summaryRenderer) Render(report types.Report) { + if len(r.scanners) == 0 { + r.logger.Warn("No enabled scanners found. Summary table will not be displayed.") + return + } + + r.printf("\nReport Summary\n\n") + + t := newTableWriter(r.w, r.isTerminal) + t.SetAutoMerge(false) + t.SetColumnMaxWidth(80) + + headers := []string{ + "Target", + "Type", + } + alignments := []table.Alignment{ + table.AlignLeft, + table.AlignCenter, + } + for _, scanner := range r.scanners { + headers = append(headers, scanner.Header()) + alignments = append(alignments, scanner.Alignment()) + } + t.SetHeaders(headers...) + t.SetAlignment(alignments...) + + for _, result := range r.splitAggregatedPackages(report.Results) { + resultType := string(result.Type) + switch result.Class { + case types.ClassSecret: + resultType = "text" + case types.ClassLicense, types.ClassLicenseFile: + resultType = "-" + } + rows := []string{ + result.Target, + resultType, + } + for _, scanner := range r.scanners { + rows = append(rows, r.colorizeCount(scanner.Count(result))) + } + t.AddRows(rows) + } + + if len(report.Results) == 0 { + r.showEmptyResultsWarning() + alignments[0] = table.AlignCenter + t.SetAlignment(alignments...) + t.AddRows(slices.Repeat([]string{"-"}, len(r.scanners)+2)) + } + + t.Render() + + // Show legend + r.printf("Legend:\n" + + "- '-': Not scanned\n" + + "- '0': Clean (no security findings detected)\n\n") +} + +func (r *summaryRenderer) printf(format string, args ...any) { + // nolint + _ = tml.Fprintf(r.w, format, args...) +} + +// showEmptyResultsWarning shows WARN why the results array is empty based on the enabled scanners. +// We need to separate the vuln/misconfig and secret/license scanners, +// because the results array contains results without findings for vulns/misconfig only. +func (r *summaryRenderer) showEmptyResultsWarning() { + resultByFiles := []Scanner{ + NewScanner(types.VulnerabilityScanner), + NewScanner(types.MisconfigScanner), + } + resultByFindings := []Scanner{ + NewScanner(types.SecretScanner), + NewScanner(types.LicenseScanner), + } + + if scanners := lo.Intersect(resultByFiles, r.scanners); len(scanners) > 0 { + r.logger.Warn("Supported files for scanner(s) not found.", log.Any("scanners", scanners)) + } + if scanners := lo.Intersect(resultByFindings, r.scanners); len(scanners) > 0 { + r.logger.Info("No issues detected with scanner(s).", log.Any("scanners", scanners)) + } +} + +// splitAggregatedPackages splits aggregated packages into different results with path as target. +// Other results will be returned as is. +func (r *summaryRenderer) splitAggregatedPackages(results types.Results) types.Results { + var newResults types.Results + + for _, result := range results { + if !slices.Contains(ftypes.AggregatingTypes, result.Type) && + // License results from applications don't have `Type`. + (result.Class != types.ClassLicense || !slices.Contains(lo.Values(langpkg.PkgTargets), result.Target)) { + newResults = append(newResults, result) + continue + } + + newResults = append(newResults, r.splitAggregatedVulns(result)...) + newResults = append(newResults, splitAggregatedLicenses(result)...) + + } + return newResults +} + +func (r *summaryRenderer) splitAggregatedVulns(result types.Result) types.Results { + // Handle case when result doesn't contain Package + // cf. https://github.com/aquasecurity/trivy/discussions/8537 + if len(result.Packages) == 0 && len(result.Vulnerabilities) > 0 { + r.logger.Warn("Packages not found unexpectedly", log.String("target", result.Target)) + return types.Results{ + result, + } + } + // Save packages to display them in the table even if no vulnerabilities were found + resultMap := lo.SliceToMap(result.Packages, func(pkg ftypes.Package) (string, *types.Result) { + filePath := rootJarFromPath(pkg.FilePath) + return filePath, &types.Result{ + Target: lo.Ternary(filePath != "", filePath, result.Target), + Class: result.Class, + Type: result.Type, + } + }) + for _, vuln := range result.Vulnerabilities { + pkgPath := rootJarFromPath(vuln.PkgPath) + + if res, ok := resultMap[pkgPath]; !ok { + r.logger.Warn("Package not found unexpectedly", log.String("package_path", pkgPath), log.String("vuln_id", vuln.VulnerabilityID)) + } else { + res.Vulnerabilities = append(res.Vulnerabilities, vuln) + } + } + newResults := lo.Values(resultMap) + sort.Slice(newResults, func(i, j int) bool { + return newResults[i].Target < newResults[j].Target + }) + return lo.FromSlicePtr(newResults) +} + +func splitAggregatedLicenses(result types.Result) types.Results { + var newResults types.Results + + licenses := make(map[string][]types.DetectedLicense) + for _, license := range result.Licenses { + licenses[license.FilePath] = append(licenses[license.FilePath], license) + } + for filePath, l := range licenses { + newResult := result + newResult.Target = lo.Ternary(filePath != "", filePath, result.Target) + newResult.Licenses = l + + newResults = append(newResults, newResult) + } + + sort.Slice(newResults, func(i, j int) bool { + return newResults[i].Target < newResults[j].Target + }) + return newResults +} + +func (r *summaryRenderer) colorizeCount(count int) string { + if count < 0 { + return "-" + } + sprintf := fmt.Sprintf + if count != 0 && r.isTerminal { + sprintf = color.New(color.FgHiRed).SprintfFunc() + } + return sprintf("%d", count) +} diff --git a/pkg/report/table/summary_test.go b/pkg/report/table/summary_test.go new file mode 100644 index 000000000000..da74ad045b6b --- /dev/null +++ b/pkg/report/table/summary_test.go @@ -0,0 +1,433 @@ +package table_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + osVuln = types.Result{ + Target: "test (alpine 3.20.3)", + Class: types.ClassOSPkg, + Type: ftypes.Alpine, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2024-9143", + PkgName: "libcrypto3", + }, + { + VulnerabilityID: "CVE-2024-9143", + PkgName: "libssl3", + }, + }, + } + jarVulns = types.Result{ + Target: "Java", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Packages: []ftypes.Package{ + { + Name: "com.fasterxml.jackson.core:jackson-databind", + FilePath: "app/jackson-databind-2.13.4.1.jar", + }, + { + Name: "com.google.code.gson:gson", + FilePath: "app/gson-2.11.0.jar", + }, + { + Name: "org.apache.logging.log4j:log4j-core", + FilePath: "app/jackson-databind-2.13.4.1.jar/nested/app2/log4j-core-2.17.0.jar", + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-42003", + PkgName: "com.fasterxml.jackson.core:jackson-databind", + PkgPath: "app/jackson-databind-2.13.4.1.jar", + }, + { + VulnerabilityID: "CVE-2021-44832", + PkgName: "org.apache.logging.log4j:log4j-core", + PkgPath: "app/jackson-databind-2.13.4.1.jar/nested/app2/log4j-core-2.17.0.jar", + }, + }, + } + + jarVulnsWithoutPackages = types.Result{ + Target: "Java", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-42003", + PkgName: "com.fasterxml.jackson.core:jackson-databind", + PkgPath: "app/jackson-databind-2.13.4.1.jar", + }, + { + VulnerabilityID: "CVE-2021-44832", + PkgName: "org.apache.logging.log4j:log4j-core", + PkgPath: "app/jackson-databind-2.13.4.1.jar/nested/app2/log4j-core-2.17.0.jar", + }, + }, + } + + jarVulnsWithMissedPackage = types.Result{ + Target: "Java", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Packages: []ftypes.Package{ + { + Name: "com.fasterxml.jackson.core:jackson-databind", + Version: "2.13.4.1", + FilePath: "app/jackson-databind-2.13.4.1.jar", + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-42003", + PkgName: "com.fasterxml.jackson.core:jackson-databind", + PkgPath: "app/jackson-databind-2.13.4.1.jar", + }, + { + VulnerabilityID: "CVE-2021-44832", + PkgName: "org.apache.logging.log4j:log4j-core", + PkgPath: "app/log4j-core-2.17.0.jar", + }, + }, + } + + npmVulns = types.Result{ + Target: "Node.js", + Class: types.ClassLangPkg, + Type: ftypes.NodePkg, + Packages: []ftypes.Package{ + { + Name: "loader-utils@2.0.2", + FilePath: "loader-utils/package.json", + }, + { + Name: "nanoid@3.1.25", + FilePath: "nanoid/package.json", + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-37601", + PkgName: "loader-utils", + PkgPath: "loader-utils/package.json", + }, + { + VulnerabilityID: "CVE-2022-37599", + PkgName: "loader-utils", + PkgPath: "loader-utils/package.json", + }, + { + VulnerabilityID: "CVE-2021-23566", + PkgName: "nanoid", + PkgPath: "nanoid/package.json", + }, + { + VulnerabilityID: "CVE-2024-55565", + PkgName: "nanoid", + PkgPath: "nanoid/package.json", + }, + }, + } + + noVuln = types.Result{ + Target: "requirements.txt", + Class: types.ClassLangPkg, + Type: ftypes.Pip, + } + + dockerfileMisconfig = types.Result{ + Target: "app/Dockerfile", + Class: types.ClassConfig, + Type: ftypes.Dockerfile, + Misconfigurations: []types.DetectedMisconfiguration{ + { + ID: "DS002", + }, + { + ID: "DS026", + }, + }, + } + secret = types.Result{ + Target: "requirements.txt", + Class: types.ClassSecret, + Secrets: []types.DetectedSecret{ + { + RuleID: "aws-access-key-id", + }, + }, + } + osLicense = types.Result{ + Target: "OS Packages", + Class: types.ClassLicense, + Licenses: []types.DetectedLicense{ + { + Name: "GPL-2.0-only", + }, + }, + } + + jarLicense = types.Result{ + Target: "Java", + Class: types.ClassLicense, + } + + npmLicenses = types.Result{ + Target: "Node.js", + Class: types.ClassLicense, + Licenses: []types.DetectedLicense{ + { + Name: "MIT", + FilePath: "loader-utils/package.json", + Category: "notice", + }, + { + Name: "MIT", + FilePath: "nanoid/package.json", + Category: "notice", + }, + }, + } + + fileLicense = types.Result{ + Target: "Loose File License(s)", + Class: types.ClassLicenseFile, + Licenses: []types.DetectedLicense{ + { + FilePath: "LICENSE", + }, + }, + } +) + +func Test_renderSummary(t *testing.T) { + tests := []struct { + name string + scanners types.Scanners + report types.Report + want string + }{ + { + name: "happy path all scanners", + scanners: []types.Scanner{ + types.VulnerabilityScanner, + types.MisconfigScanner, + types.SecretScanner, + types.LicenseScanner, + }, + report: types.Report{ + Results: []types.Result{ + osVuln, + jarVulns, + npmVulns, + dockerfileMisconfig, + secret, + osLicense, + jarLicense, + npmLicenses, + fileLicense, + }, + }, + want: ` +Report Summary + +┌───────────────────────────────────┬────────────┬─────────────────┬───────────────────┬─────────┬──────────┐ +│ Target │ Type │ Vulnerabilities │ Misconfigurations │ Secrets │ Licenses │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ test (alpine 3.20.3) │ alpine │ 2 │ - │ - │ - │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ app/gson-2.11.0.jar │ jar │ 0 │ - │ - │ - │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ app/jackson-databind-2.13.4.1.jar │ jar │ 2 │ - │ - │ - │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ loader-utils/package.json │ node-pkg │ 2 │ - │ - │ - │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ nanoid/package.json │ node-pkg │ 2 │ - │ - │ - │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ app/Dockerfile │ dockerfile │ - │ 2 │ - │ - │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ requirements.txt │ text │ - │ - │ 1 │ - │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ OS Packages │ - │ - │ - │ - │ 1 │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ loader-utils/package.json │ - │ - │ - │ - │ 1 │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ nanoid/package.json │ - │ - │ - │ - │ 1 │ +├───────────────────────────────────┼────────────┼─────────────────┼───────────────────┼─────────┼──────────┤ +│ Loose File License(s) │ - │ - │ - │ - │ 1 │ +└───────────────────────────────────┴────────────┴─────────────────┴───────────────────┴─────────┴──────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + +`, + }, + { + name: "happy path vuln scanner only", + scanners: []types.Scanner{ + types.VulnerabilityScanner, + }, + report: types.Report{ + Results: []types.Result{ + osVuln, + jarVulns, + }, + }, + want: ` +Report Summary + +┌───────────────────────────────────┬────────┬─────────────────┐ +│ Target │ Type │ Vulnerabilities │ +├───────────────────────────────────┼────────┼─────────────────┤ +│ test (alpine 3.20.3) │ alpine │ 2 │ +├───────────────────────────────────┼────────┼─────────────────┤ +│ app/gson-2.11.0.jar │ jar │ 0 │ +├───────────────────────────────────┼────────┼─────────────────┤ +│ app/jackson-databind-2.13.4.1.jar │ jar │ 2 │ +└───────────────────────────────────┴────────┴─────────────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + +`, + }, + { + name: "happy path no vulns + secret", + scanners: []types.Scanner{ + types.VulnerabilityScanner, + types.SecretScanner, + }, + report: types.Report{ + Results: []types.Result{ + noVuln, + secret, + }, + }, + want: ` +Report Summary + +┌──────────────────┬──────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├──────────────────┼──────┼─────────────────┼─────────┤ +│ requirements.txt │ pip │ 0 │ - │ +├──────────────────┼──────┼─────────────────┼─────────┤ +│ requirements.txt │ text │ - │ 1 │ +└──────────────────┴──────┴─────────────────┴─────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + +`, + }, + { + name: "happy path when Result doesn't include Packages", + scanners: []types.Scanner{ + types.VulnerabilityScanner, + }, + report: types.Report{ + Results: []types.Result{ + jarVulnsWithoutPackages, + }, + }, + want: ` +Report Summary + +┌────────┬──────┬─────────────────┐ +│ Target │ Type │ Vulnerabilities │ +├────────┼──────┼─────────────────┤ +│ Java │ jar │ 2 │ +└────────┴──────┴─────────────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + +`, + }, + { + name: "happy path when Result doesn't include all required Packages", + scanners: []types.Scanner{ + types.VulnerabilityScanner, + }, + report: types.Report{ + Results: []types.Result{ + jarVulnsWithMissedPackage, + }, + }, + want: ` +Report Summary + +┌───────────────────────────────────┬──────┬─────────────────┐ +│ Target │ Type │ Vulnerabilities │ +├───────────────────────────────────┼──────┼─────────────────┤ +│ app/jackson-databind-2.13.4.1.jar │ jar │ 1 │ +└───────────────────────────────────┴──────┴─────────────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + +`, + }, + { + name: "happy path without supported files for vulns + secret", + scanners: []types.Scanner{ + types.VulnerabilityScanner, + types.SecretScanner, + }, + want: ` +Report Summary + +┌────────┬──────┬─────────────────┬─────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ +├────────┼──────┼─────────────────┼─────────┤ +│ - │ - │ - │ - │ +└────────┴──────┴─────────────────┴─────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + +`, + }, + { + name: "happy path without supported files for all scanners", + scanners: []types.Scanner{ + types.VulnerabilityScanner, + types.SecretScanner, + types.MisconfigScanner, + types.LicenseScanner, + }, + want: ` +Report Summary + +┌────────┬──────┬─────────────────┬─────────┬───────────────────┬──────────┐ +│ Target │ Type │ Vulnerabilities │ Secrets │ Misconfigurations │ Licenses │ +├────────┼──────┼─────────────────┼─────────┼───────────────────┼──────────┤ +│ - │ - │ - │ - │ - │ - │ +└────────┴──────┴─────────────────┴─────────┴───────────────────┴──────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + r := table.NewSummaryRenderer(buf, false, tt.scanners) + r.Render(tt.report) + require.Equal(t, tt.want, buf.String()) + }) + } +} diff --git a/pkg/report/table/table.go b/pkg/report/table/table.go new file mode 100644 index 000000000000..f0f81b44dce2 --- /dev/null +++ b/pkg/report/table/table.go @@ -0,0 +1,209 @@ +package table + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "runtime" + "slices" + "strings" + + "github.com/fatih/color" + + "github.com/aquasecurity/table" + "github.com/aquasecurity/tml" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + SeverityColor = []func(a ...any) string{ + color.New(color.FgCyan).SprintFunc(), // UNKNOWN + color.New(color.FgBlue).SprintFunc(), // LOW + color.New(color.FgYellow).SprintFunc(), // MEDIUM + color.New(color.FgHiRed).SprintFunc(), // HIGH + color.New(color.FgRed).SprintFunc(), // CRITICAL + } +) + +// Writer implements Writer and output in tabular form +type Writer struct { + // Use one buffer for all renderers + buf *bytes.Buffer + + summaryRenderer *summaryRenderer + vulnerabilityRenderer Renderer + misconfigRenderer Renderer + secretRenderer Renderer + pkgLicenseRenderer Renderer + fileLicenseRenderer Renderer + + options Options +} + +type Options struct { + Scanners types.Scanners + Severities []dbTypes.Severity + Output io.Writer + + // Show dependency origin tree + Tree bool + + // Show suppressed findings + ShowSuppressed bool + + // Show/hide summary/detailed tables + TableModes []types.TableMode + + // For misconfigurations + IncludeNonFailures bool + Trace bool + RenderCause []ftypes.ConfigType + + // For licenses + LicenseRiskThreshold int + IgnoredLicenses []string +} + +func NewWriter(options Options) *Writer { + buf := bytes.NewBuffer([]byte{}) + isTerminal := IsOutputToTerminal(options.Output) + return &Writer{ + buf: buf, + + summaryRenderer: NewSummaryRenderer(buf, isTerminal, options.Scanners), + vulnerabilityRenderer: NewVulnerabilityRenderer(buf, isTerminal, options.Tree, options.ShowSuppressed, options.Severities), + misconfigRenderer: NewMisconfigRenderer(buf, options.Severities, options.Trace, options.IncludeNonFailures, isTerminal, options.RenderCause), + secretRenderer: NewSecretRenderer(buf, isTerminal, options.Severities), + pkgLicenseRenderer: NewPkgLicenseRenderer(buf, isTerminal, options.Severities), + fileLicenseRenderer: NewFileLicenseRenderer(buf, isTerminal, options.Severities), + options: options, + } +} + +type Renderer interface { + Render(result types.Result) +} + +// Write writes the result on standard output +func (tw *Writer) Write(_ context.Context, report types.Report) error { + if slices.Contains(tw.options.TableModes, types.Summary) { + tw.summaryRenderer.Render(report) + } + + if slices.Contains(tw.options.TableModes, types.Detailed) { + for _, result := range report.Results { + // Not display a table of custom resources + if result.Class == types.ClassCustom { + continue + } + tw.render(result) + } + } + + tw.flush() + return nil +} + +func (tw *Writer) flush() { + _, _ = fmt.Fprint(tw.options.Output, tw.buf.String()) + tw.buf.Reset() +} + +func (tw *Writer) render(result types.Result) { + if result.IsEmpty() && result.Class != types.ClassOSPkg { + return + } + + switch result.Class { + // vulnerability + case types.ClassOSPkg, types.ClassLangPkg: + tw.vulnerabilityRenderer.Render(result) + // misconfiguration + case types.ClassConfig: + tw.misconfigRenderer.Render(result) + // secret + case types.ClassSecret: + tw.secretRenderer.Render(result) + // package license + case types.ClassLicense: + tw.pkgLicenseRenderer.Render(result) + // file license + case types.ClassLicenseFile: + tw.fileLicenseRenderer.Render(result) + default: + return + } +} + +func newTableWriter(output io.Writer, isTerminal bool) *table.Table { + tableWriter := table.New(output) + if isTerminal { // use ansi output if we're not piping elsewhere + tableWriter.SetHeaderStyle(table.StyleBold) + tableWriter.SetLineStyle(table.StyleDim) + } + tableWriter.SetBorders(true) + tableWriter.SetAutoMerge(true) + tableWriter.SetRowLines(true) + + return tableWriter +} + +func summarize(specifiedSeverities []dbTypes.Severity, severityCount map[string]int) (int, []string) { + var total int + var severities []string + for _, sev := range specifiedSeverities { + severities = append(severities, sev.String()) + } + + var summaries []string + for _, severity := range dbTypes.SeverityNames { + if !slices.Contains(severities, severity) { + continue + } + count := severityCount[severity] + r := fmt.Sprintf("%s: %d", severity, count) + summaries = append(summaries, r) + total += count + } + + return total, summaries +} + +func IsOutputToTerminal(output io.Writer) bool { + if runtime.GOOS == "windows" { + // if its windows, we don't support formatting + return false + } + + if output != os.Stdout { + return false + } + o, err := os.Stdout.Stat() + if err != nil { + return false + } + return (o.Mode() & os.ModeCharDevice) == os.ModeCharDevice +} + +func RenderTarget(w io.Writer, target string, isTerminal bool) { + if isTerminal { + // nolint + _ = tml.Fprintf(w, "\n%s\n\n", target) + } else { + _, _ = fmt.Fprintf(w, "\n%s\n", target) + _, _ = fmt.Fprintf(w, "%s\n", strings.Repeat("=", len(target))) + } +} + +func ColorizeSeverity(value, severity string) string { + for i, name := range dbTypes.SeverityNames { + if severity == name { + return SeverityColor[i](value) + } + } + return color.New(color.FgBlue).SprintFunc()(severity) +} diff --git a/pkg/report/table/table_test.go b/pkg/report/table/table_test.go new file mode 100644 index 000000000000..380ca802047f --- /dev/null +++ b/pkg/report/table/table_test.go @@ -0,0 +1,253 @@ +package table_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestWriter_Write(t *testing.T) { + testCases := []struct { + name string + scanners types.Scanners + tableModes []types.TableMode + results types.Results + wantOutput string + includeNonFailures bool + }{ + { + name: "vulnerability and custom resource", + scanners: types.Scanners{ + types.VulnerabilityScanner, + }, + tableModes: types.SupportedTableModes, + results: types.Results{ + { + Target: "test", + Type: ftypes.Jar, + Class: types.ClassLangPkg, + Packages: []ftypes.Package{ + { + Name: "foo", + Version: "1.2.3", + FilePath: "test.jar", + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Status: dbTypes.StatusWillNotFix, + PkgPath: "test.jar", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + }, + }, + CustomResources: []ftypes.CustomResource{ + { + Type: "test", + Data: "test", + }, + }, + }, + }, + wantOutput: ` +Report Summary + +┌──────────┬──────┬─────────────────┐ +│ Target │ Type │ Vulnerabilities │ +├──────────┼──────┼─────────────────┤ +│ test.jar │ jar │ 1 │ +└──────────┴──────┴─────────────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + + +test (jar) +========== +Total: 1 (MEDIUM: 0, HIGH: 1) + +┌────────────────┬───────────────┬──────────┬──────────────┬───────────────────┬───────────────┬───────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├────────────────┼───────────────┼──────────┼──────────────┼───────────────────┼───────────────┼───────────────────────────────────────────┤ +│ foo (test.jar) │ CVE-2020-0001 │ HIGH │ will_not_fix │ 1.2.3 │ │ foobar │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-0001 │ +└────────────────┴───────────────┴──────────┴──────────────┴───────────────────┴───────────────┴───────────────────────────────────────────┘ +`, + }, + { + name: "no vulns", + scanners: types.Scanners{ + types.VulnerabilityScanner, + }, + tableModes: types.SupportedTableModes, + results: types.Results{ + { + Target: "test", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Packages: []ftypes.Package{ + { + Name: "foo", + Version: "1.2.3", + FilePath: "test.jar", + }, + }, + }, + }, + wantOutput: ` +Report Summary + +┌──────────┬──────┬─────────────────┐ +│ Target │ Type │ Vulnerabilities │ +├──────────┼──────┼─────────────────┤ +│ test.jar │ jar │ 0 │ +└──────────┴──────┴─────────────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + +`, + }, + { + name: "no summary", + scanners: types.Scanners{ + types.VulnerabilityScanner, + }, + tableModes: []types.TableMode{ + types.Detailed, + }, + results: types.Results{ + { + Target: "test", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Packages: []ftypes.Package{ + { + Name: "foo", + Version: "1.2.3", + FilePath: "test.jar", + }, + }, + }, + }, + wantOutput: ``, + }, + { + name: "no detailed", + scanners: types.Scanners{ + types.VulnerabilityScanner, + }, + tableModes: []types.TableMode{ + types.Summary, + }, + results: types.Results{ + { + Target: "test", + Type: ftypes.Jar, + Class: types.ClassLangPkg, + Packages: []ftypes.Package{ + { + Name: "foo", + Version: "1.2.3", + FilePath: "test.jar", + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Status: dbTypes.StatusWillNotFix, + PkgPath: "test.jar", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + }, + }, + }, + }, + wantOutput: ` +Report Summary + +┌──────────┬──────┬─────────────────┐ +│ Target │ Type │ Vulnerabilities │ +├──────────┼──────┼─────────────────┤ +│ test.jar │ jar │ 1 │ +└──────────┴──────┴─────────────────┘ +Legend: +- '-': Not scanned +- '0': Clean (no security findings detected) + +`, + }, + { + name: "no tables", + scanners: types.Scanners{ + types.VulnerabilityScanner, + }, + tableModes: []types.TableMode{}, + results: types.Results{ + { + Target: "test", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Packages: []ftypes.Package{ + { + Name: "foo", + Version: "1.2.3", + FilePath: "test.jar", + }, + }, + }, + }, + wantOutput: ``, + }, + { + name: "no scanners", + results: types.Results{ + { + Target: "test", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + }, + }, + wantOutput: ``, + }, + } + + t.Setenv("TRIVY_DISABLE_VEX_NOTICE", "1") + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tableWritten := bytes.Buffer{} + writer := table.NewWriter(table.Options{ + Output: &tableWritten, + Tree: true, + IncludeNonFailures: tc.includeNonFailures, + Severities: []dbTypes.Severity{ + dbTypes.SeverityHigh, + dbTypes.SeverityMedium, + }, + Scanners: tc.scanners, + TableModes: tc.tableModes, + }) + _ = writer.Write(t.Context(), types.Report{Results: tc.results}) + assert.Equal(t, tc.wantOutput, tableWritten.String(), tc.name) + }) + } +} diff --git a/pkg/report/table/vulnerability.go b/pkg/report/table/vulnerability.go new file mode 100644 index 000000000000..acb81e198277 --- /dev/null +++ b/pkg/report/table/vulnerability.go @@ -0,0 +1,379 @@ +package table + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "slices" + "sort" + "strings" + "sync" + + "github.com/fatih/color" + "github.com/samber/lo" + "github.com/xlab/treeprint" + + "github.com/aquasecurity/table" + "github.com/aquasecurity/tml" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/version/doc" +) + +const ( + vexNotice = ` +For OSS Maintainers: VEX Notice +-------------------------------- +If you're an OSS maintainer and Trivy has detected vulnerabilities in your project that you believe are not actually exploitable, consider issuing a VEX (Vulnerability Exploitability eXchange) statement. +VEX allows you to communicate the actual status of vulnerabilities in your project, improving security transparency and reducing false positives for your users. +Learn more and start using VEX: %s + +To disable this notice, set the TRIVY_DISABLE_VEX_NOTICE environment variable. + +` + envDisableNotice = "TRIVY_DISABLE_VEX_NOTICE" +) + +var ( + showVEXNoticeOnce = &sync.Once{} + showSuppressedOnce = sync.OnceFunc(func() { + log.Info(`Some vulnerabilities have been ignored/suppressed. Use the "--show-suppressed" flag to display them.`) + }) +) + +type vulnerabilityRenderer struct { + w *bytes.Buffer + isTerminal bool + tree bool // Show dependency tree + showSuppressed bool // Show suppressed vulnerabilities + severities []dbTypes.Severity + once *sync.Once +} + +func NewVulnerabilityRenderer(buf *bytes.Buffer, isTerminal, tree, suppressed bool, severities []dbTypes.Severity) *vulnerabilityRenderer { + if !isTerminal { + tml.DisableFormatting() + } + return &vulnerabilityRenderer{ + w: buf, + isTerminal: isTerminal, + tree: tree, + showSuppressed: suppressed, + severities: severities, + once: new(sync.Once), + } +} + +func (r *vulnerabilityRenderer) Render(result types.Result) { + // There are 3 cases when we show the vulnerability table (or only target and `Total: 0...`): + // When Result contains vulnerabilities; + // When we show non-empty `Suppressed Vulnerabilities` table. + if len(result.Vulnerabilities) > 0 || (r.showSuppressed && len(result.ModifiedFindings) > 0) { + r.renderDetectedVulnerabilities(result) + + if r.tree { + r.renderDependencyTree(result) + } + } + + if r.showSuppressed { + r.renderModifiedVulnerabilities(result.ModifiedFindings) + } else if len(result.ModifiedFindings) > 0 { + showSuppressedOnce() + } +} + +func (r *vulnerabilityRenderer) renderDetectedVulnerabilities(result types.Result) { + // Show VEX notice only on CI + showVEXNoticeOnce.Do(func() { + if os.Getenv(envDisableNotice) != "" || os.Getenv("CI") == "" { + return + } + _, _ = color.New(color.FgCyan).Fprintf(r.w, vexNotice, doc.URL("guide/supply-chain/vex/repo", "publishing-vex-documents")) + }) + + tw := newTableWriter(r.w, r.isTerminal) + r.setHeaders(tw, result.Vulnerabilities) + r.setVulnerabilityRows(tw, result.Vulnerabilities) + + severityCount := r.countSeverities(result.Vulnerabilities) + total, summaries := summarize(r.severities, severityCount) + + target := result.Target + if result.Class == types.ClassLangPkg { + target += fmt.Sprintf(" (%s)", result.Type) + } + RenderTarget(r.w, target, r.isTerminal) + r.printf("Total: %d (%s)\n\n", total, strings.Join(summaries, ", ")) + + tw.Render() +} + +func (r *vulnerabilityRenderer) setHeaders(tw *table.Table, vulns []types.DetectedVulnerability) { + if len(vulns) == 0 { + return + } + header := []string{ + "Library", + "Vulnerability", + "Severity", + "Status", + "Installed Version", + "Fixed Version", + "Title", + } + tw.SetHeaders(header...) +} + +func (r *vulnerabilityRenderer) setVulnerabilityRows(tw *table.Table, vulns []types.DetectedVulnerability) { + for _, v := range vulns { + lib := v.PkgName + if v.PkgPath != "" { + pkgPath := rootJarFromPath(v.PkgPath) + fileName := filepath.Base(pkgPath) + lib = fmt.Sprintf("%s (%s)", v.PkgName, fileName) + r.once.Do(func() { + log.Info("Table result includes only package filenames. Use '--format json' option to get the full path to the package file.") + }) + } + + title := v.Title + if title == "" { + title = v.Description + } + splitTitle := strings.Split(title, " ") + if len(splitTitle) >= 12 { + title = strings.Join(splitTitle[:12], " ") + "..." + } + + if v.PrimaryURL != "" { + if r.isTerminal { + title = tml.Sprintf("%s\n%s", title, v.PrimaryURL) + } else { + title = fmt.Sprintf("%s\n%s", title, v.PrimaryURL) + } + } + + var row []string + if r.isTerminal { + row = []string{ + lib, + v.VulnerabilityID, + ColorizeSeverity(v.Severity, v.Severity), + v.Status.String(), + v.InstalledVersion, + v.FixedVersion, + strings.TrimSpace(title), + } + } else { + row = []string{ + lib, + v.VulnerabilityID, + v.Severity, + v.Status.String(), + v.InstalledVersion, + v.FixedVersion, + strings.TrimSpace(title), + } + } + + tw.AddRow(row...) + } +} + +func (r *vulnerabilityRenderer) countSeverities(vulns []types.DetectedVulnerability) map[string]int { + severityCount := make(map[string]int) + for _, v := range vulns { + severityCount[v.Severity]++ + } + return severityCount +} + +func (r *vulnerabilityRenderer) renderModifiedVulnerabilities(modifiedFindings []types.ModifiedFinding) { + tw := newTableWriter(r.w, r.isTerminal) + header := []string{ + "Library", + "Vulnerability", + "Severity", + "Status", + "Statement", + "Source", + } + tw.SetHeaders(header...) + + var total int + for _, m := range modifiedFindings { + if m.Type != types.FindingTypeVulnerability { + continue + } + vuln := m.Finding.(types.DetectedVulnerability) + total++ + + stmt := lo.Ternary(m.Statement != "", m.Statement, "N/A") + tw.AddRow(vuln.PkgName, vuln.VulnerabilityID, vuln.Severity, string(m.Status), stmt, m.Source) + } + + if total == 0 { + return + } + + title := fmt.Sprintf("Suppressed Vulnerabilities (Total: %d)", total) + if r.isTerminal { + // nolint + _ = tml.Fprintf(r.w, "\n%s\n\n", title) + } else { + _, _ = fmt.Fprintf(r.w, "\n%s\n", title) + _, _ = fmt.Fprintf(r.w, "%s\n", strings.Repeat("=", len(title))) + } + + tw.Render() +} + +func (r *vulnerabilityRenderer) renderDependencyTree(result types.Result) { + // Get parents of each dependency + parents := ftypes.Packages(result.Packages).ParentDeps() + if len(parents) == 0 { + return + } + ancestors := traverseAncestors(result.Packages, parents) + + root := treeprint.NewWithRoot(fmt.Sprintf(` +Dependency Origin Tree (Reversed) +================================= +%s`, result.Target)) + + // This count is next to the package ID. + // e.g. node-fetch@1.7.3 (MEDIUM: 2, HIGH: 1, CRITICAL: 3) + pkgSeverityCount := make(map[string]map[string]int) + for _, vuln := range result.Vulnerabilities { + cnts, ok := pkgSeverityCount[vuln.PkgID] + if !ok { + cnts = make(map[string]int) + } + + cnts[vuln.Severity]++ + pkgSeverityCount[vuln.PkgID] = cnts + } + + // Extract vulnerable packages + vulnPkgs := lo.Filter(result.Packages, func(pkg ftypes.Package, _ int) bool { + return lo.ContainsBy(result.Vulnerabilities, func(vuln types.DetectedVulnerability) bool { + return pkg.ID == vuln.PkgID + }) + }) + + // Render tree + for _, vulnPkg := range vulnPkgs { + _, summaries := summarize(r.severities, pkgSeverityCount[vulnPkg.ID]) + topLvlID := tml.Sprintf("%s, (%s)", vulnPkg.ID, strings.Join(summaries, ", ")) + + branch := root.AddBranch(topLvlID) + addParents(branch, vulnPkg, parents, ancestors, set.New(vulnPkg.ID), 1) + + } + r.printf(root.String()) +} + +func (r *vulnerabilityRenderer) printf(format string, args ...any) { + // nolint + _ = tml.Fprintf(r.w, format, args...) +} + +func addParents(topItem treeprint.Tree, pkg ftypes.Package, parentMap map[string]ftypes.Packages, ancestors map[string][]string, + seen set.Set[string], depth int) { + if pkg.Relationship == ftypes.RelationshipDirect { + return + } + + roots := set.New[string]() + for _, parent := range parentMap[pkg.ID] { + if seen.Contains(parent.ID) { + continue + } + seen.Append(parent.ID) // to avoid infinite loops + + if depth == 1 && parent.Relationship == ftypes.RelationshipDirect { + topItem.AddBranch(parent.ID) + } else { + // We omit intermediate dependencies and show only direct dependencies + // as this could make the dependency tree huge. + for _, ancestor := range ancestors[parent.ID] { + roots.Append(ancestor) + } + } + } + + // Omitted + rootIDs := roots.Difference(seen).Items() + sort.Strings(rootIDs) + if len(rootIDs) > 0 { + branch := topItem.AddBranch("...(omitted)...") + for _, rootID := range rootIDs { + branch.AddBranch(rootID) + } + } +} + +func traverseAncestors(pkgs []ftypes.Package, parentMap map[string]ftypes.Packages) map[string][]string { + ancestors := make(map[string][]string) + for _, pkg := range pkgs { + ancestors[pkg.ID] = findAncestor(pkg.ID, parentMap, set.New[string]()) + } + return ancestors +} + +func findAncestor(pkgID string, parentMap map[string]ftypes.Packages, seen set.Set[string]) []string { + ancestors := set.New[string]() + seen.Append(pkgID) + for _, parent := range parentMap[pkgID] { + if seen.Contains(parent.ID) { + continue + } + switch { + case parent.Relationship == ftypes.RelationshipDirect: + ancestors.Append(parent.ID) + case len(parentMap[parent.ID]) == 0: + // Some package managers, such as "package-lock.json" v1, can retrieve package dependencies but not relationships. + // We try to guess direct dependencies in this case. A dependency with no parents must be a direct dependency. + // + // e.g. + // -> styled-components + // -> fbjs + // -> isomorphic-fetch + // -> node-fetch + // + // Even if `styled-components` is not marked as a direct dependency, it must be a direct dependency + // as it has no parents. Note that it doesn't mean `fbjs` is an indirect dependency. + ancestors.Append(parent.ID) + default: + for _, a := range findAncestor(parent.ID, parentMap, seen) { + ancestors.Append(a) + } + } + } + return ancestors.Items() +} + +var jarExtensions = []string{ + ".jar", + ".war", + ".par", + ".ear", +} + +// rootJarFromPath returns path to root jar. +// For other languages return unchanged path +func rootJarFromPath(path string) string { + // File paths are always forward-slashed in Trivy + paths := strings.Split(path, "/") + for i, p := range paths { + if slices.Contains(jarExtensions, filepath.Ext(p)) { + return strings.Join(paths[:i+1], "/") + } + } + return path +} diff --git a/pkg/report/table/vulnerability_test.go b/pkg/report/table/vulnerability_test.go new file mode 100644 index 000000000000..c69a029e8a21 --- /dev/null +++ b/pkg/report/table/vulnerability_test.go @@ -0,0 +1,665 @@ +package table_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" +) + +func Test_vulnerabilityRenderer_Render(t *testing.T) { + tests := []struct { + name string + result types.Result + want string + includeNonFailures bool + showSuppressed bool + }{ + { + name: "happy path full", + result: types.Result{ + Target: "test", + Class: types.ClassLangPkg, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Status: dbTypes.StatusWillNotFix, + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + }, + }, + // It won't be shown as `showSuppressed` is false. + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: ".trivyignore", + Finding: types.DetectedVulnerability{ + VulnerabilityID: "CVE-2020-0002", + }, + }, + }, + }, + want: ` +test () +======= +Total: 1 (MEDIUM: 0, HIGH: 1) + +┌─────────┬───────────────┬──────────┬──────────────┬───────────────────┬───────────────┬───────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼───────────────┼──────────┼──────────────┼───────────────────┼───────────────┼───────────────────────────────────────────┤ +│ foo │ CVE-2020-0001 │ HIGH │ will_not_fix │ 1.2.3 │ │ foobar │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-0001 │ +└─────────┴───────────────┴──────────┴──────────────┴───────────────────┴───────────────┴───────────────────────────────────────────┘ +`, + }, + { + name: "happy path with filePath in result", + result: types.Result{ + Target: "test", + Class: types.ClassLangPkg, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + PkgPath: "foo/bar", + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Status: dbTypes.StatusFixed, + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + }, + }, + }, + want: ` +test () +======= +Total: 1 (MEDIUM: 0, HIGH: 1) + +┌───────────┬───────────────┬──────────┬────────┬───────────────────┬───────────────┬───────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├───────────┼───────────────┼──────────┼────────┼───────────────────┼───────────────┼───────────────────────────────────────────┤ +│ foo (bar) │ CVE-2020-0001 │ HIGH │ fixed │ 1.2.3 │ 3.4.5 │ foobar │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-0001 │ +└───────────┴───────────────┴──────────┴────────┴───────────────────┴───────────────┴───────────────────────────────────────────┘ +`, + }, + { + name: "no title for vuln and missing primary link", + result: types.Result{ + Target: "test", + Class: types.ClassLangPkg, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + Status: dbTypes.StatusFixed, + Vulnerability: dbTypes.Vulnerability{ + Description: "foobar", + Severity: "HIGH", + }, + }, + }, + }, + want: ` +test () +======= +Total: 1 (MEDIUM: 0, HIGH: 1) + +┌─────────┬───────────────┬──────────┬────────┬───────────────────┬───────────────┬────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼───────────────┼──────────┼────────┼───────────────────┼───────────────┼────────┤ +│ foo │ CVE-2020-0001 │ HIGH │ fixed │ 1.2.3 │ 3.4.5 │ foobar │ +└─────────┴───────────────┴──────────┴────────┴───────────────────┴───────────────┴────────┘ +`, + }, + { + name: "long title for vuln", + result: types.Result{ + Target: "test", + Class: types.ClassLangPkg, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-1234", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-1234", + Status: dbTypes.StatusFixed, + Vulnerability: dbTypes.Vulnerability{ + Title: "a b c d e f g h i j k l m n o p q r s t u v", + Description: "foobar", + Severity: "HIGH", + }, + }, + }, + }, + want: ` +test () +======= +Total: 1 (MEDIUM: 0, HIGH: 1) + +┌─────────┬───────────────┬──────────┬────────┬───────────────────┬───────────────┬───────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼───────────────┼──────────┼────────┼───────────────────┼───────────────┼───────────────────────────────────────────┤ +│ foo │ CVE-2020-1234 │ HIGH │ fixed │ 1.2.3 │ 3.4.5 │ a b c d e f g h i j k l... │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-1234 │ +└─────────┴───────────────┴──────────┴────────┴───────────────────┴───────────────┴───────────────────────────────────────────┘ +`, + }, + { + name: "happy path with vulnerability origin graph with direct dependency info", + result: types.Result{ + Target: "package-lock.json", + Class: types.ClassLangPkg, + Type: "npm", + Packages: []ftypes.Package{ + { + ID: "node-fetch@1.7.3", + Name: "node-fetch", + Version: "1.7.3", + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "isomorphic-fetch@2.2.1", + Name: "isomorphic-fetch", + Version: "2.2.1", + Relationship: ftypes.RelationshipIndirect, + DependsOn: []string{ + "node-fetch@1.7.3", + }, + }, + { + ID: "fbjs@0.8.18", + Name: "fbjs", + Version: "0.8.18", + Relationship: ftypes.RelationshipIndirect, + DependsOn: []string{ + "isomorphic-fetch@2.2.1", + }, + }, + { + ID: "sanitize-html@1.20.0", + Name: "sanitize-html", + Version: "1.20.0", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "styled-components@3.1.3", + Name: "styled-components", + Version: "3.1.3", + Relationship: ftypes.RelationshipDirect, + DependsOn: []string{ + "fbjs@0.8.18", + }, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0235", + PkgID: "node-fetch@1.7.3", + PkgName: "node-fetch", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + InstalledVersion: "1.7.3", + FixedVersion: "2.6.7, 3.1.1", + Status: dbTypes.StatusFixed, + }, + { + VulnerabilityID: "CVE-2021-26539", + PkgID: "sanitize-html@1.20.0", + PkgName: "sanitize-html", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "MEDIUM", + }, + InstalledVersion: "1.20.0", + FixedVersion: "2.3.1", + Status: dbTypes.StatusFixed, + }, + }, + }, + want: ` +package-lock.json (npm) +======================= +Total: 2 (MEDIUM: 1, HIGH: 1) + +┌───────────────┬────────────────┬──────────┬────────┬───────────────────┬───────────────┬────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├───────────────┼────────────────┼──────────┼────────┼───────────────────┼───────────────┼────────┤ +│ node-fetch │ CVE-2022-0235 │ HIGH │ fixed │ 1.7.3 │ 2.6.7, 3.1.1 │ foobar │ +├───────────────┼────────────────┼──────────┤ ├───────────────────┼───────────────┤ │ +│ sanitize-html │ CVE-2021-26539 │ MEDIUM │ │ 1.20.0 │ 2.3.1 │ │ +└───────────────┴────────────────┴──────────┴────────┴───────────────────┴───────────────┴────────┘ + +Dependency Origin Tree (Reversed) +================================= +package-lock.json +├── node-fetch@1.7.3, (MEDIUM: 0, HIGH: 1) +│ └── ...(omitted)... +│ └── styled-components@3.1.3 +└── sanitize-html@1.20.0, (MEDIUM: 1, HIGH: 0) +`, + }, + { + name: "happy path with vulnerability origin graph with unknown relationships", + result: types.Result{ + Target: "package-lock.json", + Class: types.ClassLangPkg, + Type: "npm", + Packages: []ftypes.Package{ + { + ID: "node-fetch@1.7.3", + Name: "node-fetch", + Version: "1.7.3", + Relationship: ftypes.RelationshipUnknown, + }, + { + ID: "isomorphic-fetch@2.2.1", + Name: "isomorphic-fetch", + Version: "2.2.1", + Relationship: ftypes.RelationshipUnknown, + DependsOn: []string{ + "node-fetch@1.7.3", + }, + }, + { + ID: "fbjs@0.8.18", + Name: "fbjs", + Version: "0.8.18", + Relationship: ftypes.RelationshipUnknown, + DependsOn: []string{ + "isomorphic-fetch@2.2.1", + }, + }, + { + ID: "styled-components@3.1.3", + Name: "styled-components", + Version: "3.1.3", + Relationship: ftypes.RelationshipUnknown, + DependsOn: []string{ + "fbjs@0.8.18", + }, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0235", + PkgID: "node-fetch@1.7.3", + PkgName: "node-fetch", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + InstalledVersion: "1.7.3", + FixedVersion: "2.6.7, 3.1.1", + Status: dbTypes.StatusFixed, + }, + }, + }, + want: ` +package-lock.json (npm) +======================= +Total: 1 (MEDIUM: 0, HIGH: 1) + +┌────────────┬───────────────┬──────────┬────────┬───────────────────┬───────────────┬────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├────────────┼───────────────┼──────────┼────────┼───────────────────┼───────────────┼────────┤ +│ node-fetch │ CVE-2022-0235 │ HIGH │ fixed │ 1.7.3 │ 2.6.7, 3.1.1 │ foobar │ +└────────────┴───────────────┴──────────┴────────┴───────────────────┴───────────────┴────────┘ + +Dependency Origin Tree (Reversed) +================================= +package-lock.json +└── node-fetch@1.7.3, (MEDIUM: 0, HIGH: 1) + └── ...(omitted)... + └── styled-components@3.1.3 +`, + }, + { + name: "show suppressed vulnerabilities", + result: types.Result{ + Target: "test", + Class: types.ClassLangPkg, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-0001", + Status: dbTypes.StatusWillNotFix, + Vulnerability: dbTypes.Vulnerability{ + Title: "title1", + Description: "desc1", + Severity: "HIGH", + }, + }, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Statement: "Not exploitable", + Source: ".trivyignore.yaml", + Finding: types.DetectedVulnerability{ + VulnerabilityID: "CVE-2020-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + Status: dbTypes.StatusWillNotFix, + Vulnerability: dbTypes.Vulnerability{ + Title: "title2", + Description: "desc2", + Severity: "MEDIUM", + }, + }, + }, + }, + }, + showSuppressed: true, + want: ` +test () +======= +Total: 1 (MEDIUM: 0, HIGH: 1) + +┌─────────┬───────────────┬──────────┬──────────────┬───────────────────┬───────────────┬───────────────────────────────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────┼───────────────┼──────────┼──────────────┼───────────────────┼───────────────┼───────────────────────────────────────────┤ +│ foo │ CVE-2020-0001 │ HIGH │ will_not_fix │ 1.2.3 │ │ title1 │ +│ │ │ │ │ │ │ https://avd.aquasec.com/nvd/cve-2020-0001 │ +└─────────┴───────────────┴──────────┴──────────────┴───────────────────┴───────────────┴───────────────────────────────────────────┘ + +Suppressed Vulnerabilities (Total: 1) +===================================== +┌─────────┬───────────────┬──────────┬─────────┬─────────────────┬───────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Statement │ Source │ +├─────────┼───────────────┼──────────┼─────────┼─────────────────┼───────────────────┤ +│ bar │ CVE-2020-0002 │ MEDIUM │ ignored │ Not exploitable │ .trivyignore.yaml │ +└─────────┴───────────────┴──────────┴─────────┴─────────────────┴───────────────────┘ +`, + }, + { + name: "suppressed all OS package vulnerabilities without `showSuppressed` flag", + result: types.Result{ + Target: "test", + Class: types.ClassOSPkg, + Type: ftypes.Alpine, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Statement: "Not exploitable", + Source: ".trivyignore.yaml", + Finding: types.DetectedVulnerability{ + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + Status: dbTypes.StatusWillNotFix, + Vulnerability: dbTypes.Vulnerability{ + Title: "title1", + Description: "desc1", + Severity: "MEDIUM", + }, + }, + }, + }, + }, + showSuppressed: false, + want: "", + }, + { + name: "suppressed all language package vulnerabilities without `showSuppressed` flag", + result: types.Result{ + Target: "test", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Statement: "Not exploitable", + Source: ".trivyignore.yaml", + Finding: types.DetectedVulnerability{ + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + Status: dbTypes.StatusWillNotFix, + Vulnerability: dbTypes.Vulnerability{ + Title: "title1", + Description: "desc1", + Severity: "MEDIUM", + }, + }, + }, + }, + }, + showSuppressed: false, + want: ``, + }, + { + name: "suppressed all language package vulnerabilities with `showSuppressed` flag", + result: types.Result{ + Target: "test", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Statement: "Not exploitable", + Source: ".trivyignore.yaml", + Finding: types.DetectedVulnerability{ + VulnerabilityID: "CVE-2020-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + Status: dbTypes.StatusWillNotFix, + Vulnerability: dbTypes.Vulnerability{ + Title: "title1", + Description: "desc1", + Severity: "MEDIUM", + }, + }, + }, + }, + }, + showSuppressed: true, + want: ` +test (jar) +========== +Total: 0 (MEDIUM: 0, HIGH: 0) + + +Suppressed Vulnerabilities (Total: 1) +===================================== +┌─────────┬───────────────┬──────────┬─────────┬─────────────────┬───────────────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Statement │ Source │ +├─────────┼───────────────┼──────────┼─────────┼─────────────────┼───────────────────┤ +│ foo │ CVE-2020-0001 │ MEDIUM │ ignored │ Not exploitable │ .trivyignore.yaml │ +└─────────┴───────────────┴──────────┴─────────┴─────────────────┴───────────────────┘ +`, + }, + { + name: "happy path with vulnerability origin graph for pom.xml file with modules", + result: types.Result{ + Target: "pom.xml", + Class: types.ClassLangPkg, + Type: "pom", + Packages: []ftypes.Package{ + { + ID: "com.example:root:0.0.1::abcdef1234567001", + Name: "com.example:root", + Version: "0.0.1", + Relationship: ftypes.RelationshipRoot, + DependsOn: []string{ + "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567003", + }, + }, + { + ID: "com.example:module1:1.0.0::abcdef1234567002", + Name: "com.example:module1", + Version: "1.0.0", + Relationship: ftypes.RelationshipWorkspace, + DependsOn: []string{ + "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567003", + }, + }, + { + ID: "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567003", + Name: "org.apache.logging.log4j:log4j-core", + Version: "2.6.1", + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "com.example:module2:2.0.0::abcdef1234567004", + Name: "com.example:module2", + Version: "2.0.0", + Relationship: ftypes.RelationshipRoot, + DependsOn: []string{ + "com.example:example-dependency:2.0.0::abcdef1234567005", + }, + }, + { + ID: "com.example:example-dependency:2.0.0::abcdef1234567005", + Name: "com.example:example-dependency", + Version: "2.0.0", + Relationship: ftypes.RelationshipDirect, + DependsOn: []string{ + "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567006", + }, + }, + { + ID: "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567006", + Name: "org.apache.logging.log4j:log4j-core", + Version: "2.6.1", + Relationship: ftypes.RelationshipIndirect, + }, + // Root pkg + { + ID: "com.example:example-nested-dependency:2.0.0::abcdef1234567007", + Name: "com.example:example-nested-dependency", + Version: "2.0.0", + Relationship: ftypes.RelationshipDirect, + DependsOn: []string{ + "com.example:example-dependency:2.0.0::abcdef1234567008", + }, + }, + { + ID: "com.example:example-dependency:2.0.0::abcdef1234567008", + Name: "com.example:example-dependency", + Version: "2.0.0", + Relationship: ftypes.RelationshipIndirect, + DependsOn: []string{ + "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567009", + }, + }, + + { + ID: "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567009", + Name: "org.apache.logging.log4j:log4j-core", + Version: "2.6.1", + Relationship: ftypes.RelationshipIndirect, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2017-5645", + PkgID: "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567003", + PkgName: "org.apache.logging.log4j:log4j-core", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + InstalledVersion: "2.6.1", + FixedVersion: "2.8.2", + Status: dbTypes.StatusFixed, + }, + { + VulnerabilityID: "CVE-2017-5645", + PkgID: "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567006", + PkgName: "org.apache.logging.log4j:log4j-core", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + InstalledVersion: "2.6.1", + FixedVersion: "2.8.2", + Status: dbTypes.StatusFixed, + }, + { + VulnerabilityID: "CVE-2017-5645", + PkgID: "org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567009", + PkgName: "org.apache.logging.log4j:log4j-core", + Vulnerability: dbTypes.Vulnerability{ + Title: "foobar", + Description: "baz", + Severity: "HIGH", + }, + InstalledVersion: "2.6.1", + FixedVersion: "2.8.2", + Status: dbTypes.StatusFixed, + }, + }, + }, + want: ` +pom.xml (pom) +============= +Total: 3 (MEDIUM: 0, HIGH: 3) + +┌─────────────────────────────────────┬───────────────┬──────────┬────────┬───────────────────┬───────────────┬────────┐ +│ Library │ Vulnerability │ Severity │ Status │ Installed Version │ Fixed Version │ Title │ +├─────────────────────────────────────┼───────────────┼──────────┼────────┼───────────────────┼───────────────┼────────┤ +│ org.apache.logging.log4j:log4j-core │ CVE-2017-5645 │ HIGH │ fixed │ 2.6.1 │ 2.8.2 │ foobar │ +│ │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ +│ │ │ │ │ │ │ │ +└─────────────────────────────────────┴───────────────┴──────────┴────────┴───────────────────┴───────────────┴────────┘ + +Dependency Origin Tree (Reversed) +================================= +pom.xml +├── org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567003, (MEDIUM: 0, HIGH: 1) +├── org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567006, (MEDIUM: 0, HIGH: 1) +│ └── com.example:example-dependency:2.0.0::abcdef1234567005 +└── org.apache.logging.log4j:log4j-core:2.6.1::abcdef1234567009, (MEDIUM: 0, HIGH: 1) + └── ...(omitted)... + └── com.example:example-nested-dependency:2.0.0::abcdef1234567007 +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := bytes.NewBuffer([]byte{}) + r := table.NewVulnerabilityRenderer(buf, false, true, tt.showSuppressed, []dbTypes.Severity{ + dbTypes.SeverityHigh, + dbTypes.SeverityMedium, + }) + r.Render(tt.result) + assert.Equal(t, tt.want, buf.String(), tt.name) + }) + } +} diff --git a/pkg/report/template.go b/pkg/report/template.go new file mode 100644 index 000000000000..a2b4407fc0ba --- /dev/null +++ b/pkg/report/template.go @@ -0,0 +1,83 @@ +package report + +import ( + "bytes" + "context" + "encoding/xml" + "html" + "io" + "maps" + "os" + "strings" + "text/template" + + "github.com/Masterminds/sprig/v3" + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +// CustomTemplateFuncMap is used to overwrite existing functions for testing. +var CustomTemplateFuncMap = make(map[string]any) + +// TemplateWriter write result in custom format defined by user's template +type TemplateWriter struct { + Output io.Writer + Template *template.Template +} + +// NewTemplateWriter is the factory method to return TemplateWriter object +func NewTemplateWriter(output io.Writer, outputTemplate, appVersion string) (*TemplateWriter, error) { + if after, ok := strings.CutPrefix(outputTemplate, "@"); ok { + buf, err := os.ReadFile(after) + if err != nil { + return nil, xerrors.Errorf("error retrieving template from path: %w", err) + } + outputTemplate = string(buf) + } + var templateFuncMap template.FuncMap = sprig.GenericFuncMap() + templateFuncMap["escapeXML"] = func(input string) string { + escaped := &bytes.Buffer{} + if err := xml.EscapeText(escaped, []byte(input)); err != nil { + log.Error("Error while escapeString to XML", log.Err(err)) + return input + } + return escaped.String() + } + templateFuncMap["endWithPeriod"] = func(input string) string { + if !strings.HasSuffix(input, ".") { + input += "." + } + return input + } + templateFuncMap["escapeString"] = html.EscapeString + templateFuncMap["sourceID"] = func(input string) dbTypes.SourceID { + return dbTypes.SourceID(input) + } + templateFuncMap["appVersion"] = func() string { + return appVersion + } + + // Overwrite functions + maps.Copy(templateFuncMap, CustomTemplateFuncMap) + + tmpl, err := template.New("output template").Funcs(templateFuncMap).Parse(outputTemplate) + if err != nil { + return nil, xerrors.Errorf("error parsing template: %w", err) + } + return &TemplateWriter{ + Output: output, + Template: tmpl, + }, nil +} + +// Write writes result +func (tw TemplateWriter) Write(_ context.Context, report types.Report) error { + err := tw.Template.Execute(tw.Output, report.Results) + if err != nil { + return xerrors.Errorf("failed to write with template: %w", err) + } + return nil +} diff --git a/pkg/report/template_test.go b/pkg/report/template_test.go new file mode 100644 index 000000000000..07efd1327226 --- /dev/null +++ b/pkg/report/template_test.go @@ -0,0 +1,216 @@ +package report_test + +import ( + "bytes" + "testing" + "time" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestReportWriter_Template(t *testing.T) { + testCases := []struct { + name string + detectedVulns []types.DetectedVulnerability + template string + expected string + }{ + { + name: "happy path", + detectedVulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "foo", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + "nvd": 1, + }, + }, + }, + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "bar", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "baz", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + }, + template: "{{ range . }}{{ range .Vulnerabilities}}{{ println .VulnerabilityID .Severity }}{{ end }}{{ end }}", + expected: "CVE-2019-0000 HIGH\nCVE-2019-0000 HIGH\nCVE-2019-0001 CRITICAL\n", + }, + { + name: "happy path", + detectedVulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "123", + PkgName: `foo \ test`, + InstalledVersion: "1.2.3", + FixedVersion: "3.4.5", + Vulnerability: dbTypes.Vulnerability{ + Title: `gcc: POWER9 "DARN" RNG intrinsic produces repeated output`, + Description: `curl version curl \X 7.20.0 to and including curl 7.59.0 contains a CWE-126: Buffer Over-read vulnerability in denial of service that can result in curl can be tricked into reading data beyond the end of a heap based buffer used to store downloaded RTSP content.. This vulnerability appears to have been fixed in curl < 7.20.0 and curl >= 7.60.0.`, + Severity: "HIGH", + }, + }, + }, + template: ` +{{- range . -}} +{{- $failures := len .Vulnerabilities }} + + {{- if not (eq .Type "") }} + + + + {{- end -}} + {{ range .Vulnerabilities }} + + {{ escapeXML .Description }} + + {{- end }} + +{{- end }} +`, + + expected: ` + + + + + + curl version curl \X 7.20.0 to and including curl 7.59.0 contains a CWE-126: Buffer Over-read vulnerability in denial of service that can result in curl can be tricked into reading data beyond the end of a heap based buffer used to store downloaded RTSP content.. This vulnerability appears to have been fixed in curl < 7.20.0 and curl >= 7.60.0. + + +`, + }, + { + name: "happy path with/without period description should return with period", + detectedVulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "foo", + Vulnerability: dbTypes.Vulnerability{ + Description: "without period", + }, + }, + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "bar", + Vulnerability: dbTypes.Vulnerability{ + Description: "with period.", + }, + }, + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "bar", + Vulnerability: dbTypes.Vulnerability{ + Description: `with period and unescaped string curl: Use-after-free when closing 'easy' handle in Curl_close().`, + }, + }, + }, + template: `{{ range . }}{{ range .Vulnerabilities}}{{.VulnerabilityID}} {{ endWithPeriod (escapeString .Description) | printf "%q" }}{{ end }}{{ end }}`, + expected: `CVE-2019-0000 "without period."CVE-2019-0000 "with period."CVE-2019-0000 "with period and unescaped string curl: Use-after-free when closing 'easy' handle in Curl_close()."`, + }, + { + name: "Calculate using sprig", + detectedVulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "foo", + Vulnerability: dbTypes.Vulnerability{ + Description: "without period", + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "bar", + Vulnerability: dbTypes.Vulnerability{ + Description: "with period.", + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "bar", + Vulnerability: dbTypes.Vulnerability{ + Description: `with period and unescaped string curl: Use-after-free when closing 'easy' handle in Curl_close().`, + Severity: dbTypes.SeverityHigh.String(), + }, + }, + }, + template: `{{ $high := 0 }}{{ $critical := 0 }}{{ range . }}{{ range .Vulnerabilities}}{{ if eq .Severity "HIGH" }}{{ $high = add $high 1 }}{{ end }}{{ if eq .Severity "CRITICAL" }}{{ $critical = add $critical 1 }}{{ end }}{{ end }}Critical: {{ $critical }}, High: {{ $high }}{{ end }}`, + expected: `Critical: 2, High: 1`, + }, + { + name: "custom JSON marshaler", + detectedVulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0000", + PkgName: "foo", + Status: dbTypes.StatusAffected, + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "foobar", + Version: "1.2.3", + }, + }, + }, + }, + template: `{{ range . }}{{ range .Vulnerabilities}}{{ toPrettyJson . }}{{ end }}{{ end }}`, + expected: `{ + "VulnerabilityID": "CVE-2019-0000", + "PkgName": "foo", + "PkgIdentifier": { + "PURL": "pkg:npm/foobar@1.2.3" + }, + "Status": "affected" +}`, + }, + { + name: "happy path: env var parsing", + detectedVulns: []types.DetectedVulnerability{}, + template: `{{ lower (env "AWS_ACCOUNT_ID") }}`, + expected: `123456789012`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := clock.With(t.Context(), time.Date(2020, 8, 10, 7, 28, 17, 958601, time.UTC)) + + t.Setenv("AWS_ACCOUNT_ID", "123456789012") + got := bytes.Buffer{} + inputReport := types.Report{ + Results: types.Results{ + { + Target: "foojunit", + Type: "test", + Vulnerabilities: tc.detectedVulns, + }, + }, + } + + w, err := report.NewTemplateWriter(&got, tc.template, "dev") + require.NoError(t, err) + err = w.Write(ctx, inputReport) + require.NoError(t, err) + assert.Equal(t, tc.expected, got.String()) + }) + } +} diff --git a/pkg/report/testdata/new.json.golden b/pkg/report/testdata/new.json.golden new file mode 100644 index 000000000000..dd8f29506a63 --- /dev/null +++ b/pkg/report/testdata/new.json.golden @@ -0,0 +1,27 @@ +{ + "ImageID": "sha256:5c534be56eca62e756ef2ef51523feda0f19cd7c15bb0c015e3d6e3ae090bf6e", + "RepoTags": [ + "test:latest" + ], + "RepoDigests": [ + "test@sha256:0bd0e9e03a022c3b0226667621da84fc9bf562a9056130424b5bfbd8bcb0397f" + ], + "Results": [ + { + "Target": "foojson", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-0001", + "PkgName": "foo", + "InstalledVersion": "1.2.3", + "FixedVersion": "3.4.5", + "Layer": {}, + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-0001", + "Title": "foobar", + "Description": "baz", + "Severity": "HIGH" + } + ] + } + ] +} \ No newline at end of file diff --git a/pkg/report/testdata/old.json.golden b/pkg/report/testdata/old.json.golden new file mode 100644 index 000000000000..78c2f6760f63 --- /dev/null +++ b/pkg/report/testdata/old.json.golden @@ -0,0 +1,18 @@ +[ + { + "Target": "foojson", + "Vulnerabilities": [ + { + "VulnerabilityID": "CVE-2020-0001", + "PkgName": "foo", + "InstalledVersion": "1.2.3", + "FixedVersion": "3.4.5", + "Layer": {}, + "PrimaryURL": "https://avd.aquasec.com/nvd/cve-2020-0001", + "Title": "foobar", + "Description": "baz", + "Severity": "HIGH" + } + ] + } +] \ No newline at end of file diff --git a/pkg/report/writer.go b/pkg/report/writer.go index 7b5797cc6841..11cc47a45301 100644 --- a/pkg/report/writer.go +++ b/pkg/report/writer.go @@ -1,96 +1,139 @@ package report import ( - "encoding/json" - "fmt" + "context" "io" "strings" + "github.com/hashicorp/go-multierror" "golang.org/x/xerrors" - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/types" - "github.com/olekukonko/tablewriter" + cr "github.com/aquasecurity/trivy/pkg/compliance/report" + "github.com/aquasecurity/trivy/pkg/extension" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/report/cyclonedx" + "github.com/aquasecurity/trivy/pkg/report/github" + "github.com/aquasecurity/trivy/pkg/report/predicate" + "github.com/aquasecurity/trivy/pkg/report/spdx" + "github.com/aquasecurity/trivy/pkg/report/table" + "github.com/aquasecurity/trivy/pkg/types" ) -type Results []Result - -type Result struct { - FileName string `json:"file"` - Vulnerabilities []types.Vulnerability -} - -type Writer interface { - Write(Results) error -} +const ( + SchemaVersion = 2 +) -type TableWriter struct { - Output io.Writer -} +// Write writes the result to output, format as passed in argument +func Write(ctx context.Context, report types.Report, option flag.Options) (err error) { + // Call pre-report hooks + if err := extension.PreReport(ctx, &report, option); err != nil { + return xerrors.Errorf("pre report error: %w", err) + } -func (tw TableWriter) Write(results Results) error { - for _, result := range results { - tw.write(result) + output, cleanup, err := option.OutputWriter(ctx) + if err != nil { + return xerrors.Errorf("failed to create a file: %w", err) } - return nil -} -func (tw TableWriter) write(result Result) { - table := tablewriter.NewWriter(tw.Output) - table.SetHeader([]string{"Library", "Vulnerability ID", "Severity", "Installed Version", "Fixed Version", "Title"}) + defer func() { + if cerr := cleanup(); cerr != nil { + err = multierror.Append(err, cerr) + } + }() - severityCount := map[string]int{} - for _, v := range result.Vulnerabilities { - severityCount[v.Severity]++ + // Compliance report + if option.Compliance.Spec.ID != "" { + return complianceWrite(ctx, report, option, output) + } - title := v.Title - if title == "" { - title = v.Description + var writer Writer + switch option.Format { + case types.FormatTable: + writer = table.NewWriter(table.Options{ + Scanners: option.Scanners, + Output: output, + Severities: option.Severities, + Tree: option.DependencyTree, + ShowSuppressed: option.ShowSuppressed, + IncludeNonFailures: option.IncludeNonFailures, + Trace: option.RegoOptions.Trace, + RenderCause: option.RenderCause, + LicenseRiskThreshold: option.LicenseRiskThreshold, + IgnoredLicenses: option.IgnoredLicenses, + TableModes: option.TableModes, + }) + case types.FormatJSON: + writer = &JSONWriter{ + Output: output, + ListAllPkgs: option.ListAllPkgs, + ShowSuppressed: option.ShowSuppressed, } - splittedTitle := strings.Split(title, " ") - if len(splittedTitle) >= 12 { - title = strings.Join(splittedTitle[:12], " ") + "..." + case types.FormatGitHub: + writer = &github.Writer{ + Output: output, + Version: option.AppVersion, } - table.Append([]string{v.PkgName, v.VulnerabilityID, vulnerability.ColorizeSeverity(v.Severity), - v.InstalledVersion, v.FixedVersion, title}) + case types.FormatCycloneDX: + // TODO: support xml format option with cyclonedx writer + writer = cyclonedx.NewWriter(output, option.AppVersion) + case types.FormatSPDX, types.FormatSPDXJSON: + writer = spdx.NewWriter(output, option.AppVersion, option.Format) + case types.FormatTemplate: + // We keep `sarif.tpl` template working for backward compatibility for a while. + if strings.HasPrefix(option.Template, "@") && strings.HasSuffix(option.Template, "sarif.tpl") { + log.Warn("Using `--template sarif.tpl` is deprecated. Please migrate to `--format sarif`. See https://github.com/aquasecurity/trivy/discussions/1571") + writer = &SarifWriter{ + Output: output, + Version: option.AppVersion, + } + break + } + if writer, err = NewTemplateWriter(output, option.Template, option.AppVersion); err != nil { + return xerrors.Errorf("failed to initialize template writer: %w", err) + } + case types.FormatSarif: + target := "" + if report.ArtifactType == ftypes.TypeFilesystem { + target = option.Target + } + writer = &SarifWriter{ + Output: output, + Version: option.AppVersion, + Target: target, + } + case types.FormatCosignVuln: + writer = predicate.NewVulnWriter(output, option.AppVersion) + default: + return xerrors.Errorf("unknown format: %v", option.Format) } - var results []string - for _, severity := range vulnerability.SeverityNames { - r := fmt.Sprintf("%s: %d", severity, severityCount[severity]) - results = append(results, r) + if err = writer.Write(ctx, report); err != nil { + return xerrors.Errorf("failed to write results: %w", err) } - fmt.Printf("\n%s\n", result.FileName) - fmt.Println(strings.Repeat("=", len(result.FileName))) - fmt.Printf("Total: %d (%s)\n\n", len(result.Vulnerabilities), strings.Join(results, ", ")) - - if len(result.Vulnerabilities) == 0 { - return + // Call post-report hooks + if err := extension.PostReport(ctx, &report, option); err != nil { + return xerrors.Errorf("post report error: %w", err) } - table.SetAutoMergeCells(true) - table.SetRowLine(true) - table.Render() - return -} - -type JsonWriter struct { - Output io.Writer + return nil } -func (jw JsonWriter) Write(results Results) error { - out := map[string][]types.Vulnerability{} - for _, result := range results { - out[result.FileName] = result.Vulnerabilities - } - output, err := json.MarshalIndent(out, "", " ") +func complianceWrite(ctx context.Context, report types.Report, opt flag.Options, output io.Writer) error { + complianceReport, err := cr.BuildComplianceReport([]types.Results{report.Results}, opt.Compliance) if err != nil { - return xerrors.Errorf("failed to marshal json: %w", err) + return xerrors.Errorf("compliance report build error: %w", err) } + return cr.Write(ctx, complianceReport, cr.Option{ + Format: opt.Format, + Report: opt.ReportFormat, + Output: output, + Severities: opt.Severities, + }) +} - if _, err = fmt.Fprint(jw.Output, string(output)); err != nil { - return xerrors.Errorf("failed to write json: %w", err) - } - return nil +// Writer defines the result write operation +type Writer interface { + Write(context.Context, types.Report) error } diff --git a/pkg/report/writer_internal_test.go b/pkg/report/writer_internal_test.go new file mode 100644 index 000000000000..28a916d81549 --- /dev/null +++ b/pkg/report/writer_internal_test.go @@ -0,0 +1,44 @@ +package report + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReportWriter_toSarifErrorLevel(t *testing.T) { + tests := []struct { + severity string + sarifErrorLevel string + }{ + { + severity: "CRITICAL", + sarifErrorLevel: "error", + }, + { + severity: "HIGH", + sarifErrorLevel: "error", + }, + { + severity: "MEDIUM", + sarifErrorLevel: "warning", + }, + { + severity: "LOW", + sarifErrorLevel: "note", + }, + { + severity: "UNKNOWN", + sarifErrorLevel: "note", + }, + { + severity: "OTHER", + sarifErrorLevel: "none", + }, + } + for _, tc := range tests { + t.Run(tc.severity, func(t *testing.T) { + assert.Equal(t, tc.sarifErrorLevel, toSarifErrorLevel(tc.severity), tc.severity) + }) + } +} diff --git a/pkg/report/writer_test.go b/pkg/report/writer_test.go new file mode 100644 index 000000000000..3433ab387ea1 --- /dev/null +++ b/pkg/report/writer_test.go @@ -0,0 +1,180 @@ +package report_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/internal/hooktest" + "github.com/aquasecurity/trivy/pkg/flag" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestResults_Failed(t *testing.T) { + tests := []struct { + name string + results types.Results + want bool + }{ + { + name: "no vulnerabilities and misconfigurations", + results: types.Results{ + { + Target: "test", + Type: "test", + }, + }, + want: false, + }, + { + name: "vulnerabilities found", + results: types.Results{ + { + Target: "test", + Type: "test", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2021-0001", + PkgName: "test", + }, + }, + }, + }, + want: true, + }, + { + name: "failed misconfigurations", + results: types.Results{ + { + Target: "test", + Type: "test", + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Docker Security Check", + ID: "ID-001", + Status: types.MisconfStatusFailure, + }, + }, + }, + }, + want: true, + }, + { + name: "passed misconfigurations", + results: types.Results{ + { + Target: "test", + Type: "test", + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Docker Security Check", + ID: "ID-001", + Status: types.MisconfStatusPassed, + }, + }, + }, + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.results.Failed() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestWrite(t *testing.T) { + testReport := types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "test-artifact", + Results: types.Results{ + { + Target: "test-target", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2021-0001", + PkgName: "test-pkg", + Vulnerability: dbTypes.Vulnerability{ + Title: "Test Vulnerability Title", + Description: "This is a test description of a vulnerability", + }, + }, + }, + }, + }, + } + testTemplate := "{{ range . }}{{ range .Vulnerabilities }}- {{ .VulnerabilityID }}: {{ .Title }}\n {{ .Description }}\n{{ end }}{{ end }}" + + tests := []struct { + name string + setUpHook bool + report types.Report + options flag.Options + wantOutput string + wantTitle string // Expected title after function call + wantDesc string // Expected description after function call + }{ + { + name: "template with title and description", + report: testReport, + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: types.FormatTemplate, + Template: testTemplate, + }, + }, + wantOutput: "- CVE-2021-0001: Test Vulnerability Title\n This is a test description of a vulnerability\n", + wantTitle: "Test Vulnerability Title", // Should remain unchanged + wantDesc: "This is a test description of a vulnerability", // Should remain unchanged + }, + { + name: "report modified by hooks", + setUpHook: true, + report: testReport, + options: flag.Options{ + ReportOptions: flag.ReportOptions{ + Format: types.FormatTemplate, + Template: testTemplate, + }, + }, + // The template output only reflects the pre-report hook changes because + // the post-report hook runs AFTER the output is written. + // However, the report object itself is modified by both pre and post hooks. + wantOutput: "- CVE-2021-0001: Modified by pre-report hook\n This is a test description of a vulnerability\n", + wantTitle: "Modified by pre-report hook", + wantDesc: "Modified by post-report hook", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.setUpHook { + hooktest.Init(t) + } + + // Create a buffer to capture the output + output := new(bytes.Buffer) + tt.options.SetOutputWriter(output) + + // Execute the Write function + err := report.Write(t.Context(), tt.report, tt.options) + require.NoError(t, err) + + // Verify the output matches the expected template rendering + got := output.String() + assert.Equal(t, tt.wantOutput, got, "Template output does not match wanted value") + + // Verify that the title and description in the report match the expected values + require.Len(t, tt.report.Results, 1) + require.Len(t, tt.report.Results[0].Vulnerabilities, 1) + assert.Equal(t, tt.wantTitle, tt.report.Results[0].Vulnerabilities[0].Title) + assert.Equal(t, tt.wantDesc, tt.report.Results[0].Vulnerabilities[0].Description) + }) + } +} diff --git a/pkg/result/filter.go b/pkg/result/filter.go new file mode 100644 index 000000000000..4b31446e5067 --- /dev/null +++ b/pkg/result/filter.go @@ -0,0 +1,334 @@ +package result + +import ( + "context" + "fmt" + "os" + "path/filepath" + "slices" + "sort" + + "github.com/open-policy-agent/opa/v1/ast" + "github.com/open-policy-agent/opa/v1/rego" + "github.com/samber/lo" + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vex" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +const ( + // DefaultIgnoreFile is the file name to be evaluated + DefaultIgnoreFile = ".trivyignore" +) + +type FilterOptions struct { + Severities []dbTypes.Severity + IgnoreStatuses []dbTypes.Status + IncludeNonFailures bool + IgnoreFile string + PolicyFile string + IgnoreLicenses []string + CacheDir string + VEXSources []vex.Source +} + +// Filter filters out the report +func Filter(ctx context.Context, report types.Report, opts FilterOptions) error { + ignoreConf, err := ParseIgnoreFile(ctx, opts.IgnoreFile) + if err != nil { + return xerrors.Errorf("%s error: %w", opts.IgnoreFile, err) + } + + for i := range report.Results { + if err = FilterResult(ctx, &report.Results[i], ignoreConf, opts); err != nil { + return xerrors.Errorf("unable to filter vulnerabilities: %w", err) + } + } + + // Filter out vulnerabilities based on the given VEX document. + if err = vex.Filter(ctx, &report, vex.Options{ + CacheDir: opts.CacheDir, + Sources: opts.VEXSources, + }); err != nil { + return xerrors.Errorf("VEX error: %w", err) + } + + return nil +} + +// FilterResult filters out the result +func FilterResult(ctx context.Context, result *types.Result, ignoreConf IgnoreConfig, opt FilterOptions) error { + // Convert dbTypes.Severity to string + severities := xslices.Map(opt.Severities, dbTypes.Severity.String) + + filterVulnerabilities(result, severities, opt.IgnoreStatuses, ignoreConf) + filterMisconfigurations(result, severities, opt.IncludeNonFailures, ignoreConf) + filterSecrets(result, severities, ignoreConf) + filterLicenses(result, severities, opt.IgnoreLicenses, ignoreConf) + + if opt.PolicyFile != "" { + policyFile := filepath.ToSlash(filepath.Clean(opt.PolicyFile)) + if err := applyPolicy(ctx, result, policyFile); err != nil { + return xerrors.Errorf("failed to apply the policy: %w", err) + } + } + sort.Sort(types.BySeverity(result.Vulnerabilities)) + + return nil +} + +func filterVulnerabilities(result *types.Result, severities []string, ignoreStatuses []dbTypes.Status, ignoreConfig IgnoreConfig) { + uniqVulns := make(map[string]types.DetectedVulnerability) + for _, vuln := range result.Vulnerabilities { + if vuln.Severity == "" { + vuln.Severity = dbTypes.SeverityUnknown.String() + } + + switch { + // Filter by severity + case !slices.Contains(severities, vuln.Severity): + continue + // Filter by status + case slices.Contains(ignoreStatuses, vuln.Status): + continue + } + + // Filter by ignore file + if f := ignoreConfig.MatchVulnerability(vuln.VulnerabilityID, result.Target, vuln.PkgPath, vuln.PkgIdentifier.PURL); f != nil { + result.ModifiedFindings = append(result.ModifiedFindings, + types.NewModifiedFinding(vuln, types.FindingStatusIgnored, f.Statement, ignoreConfig.FilePath)) + continue + } + + // Check if there is a duplicate vulnerability + key := fmt.Sprintf("%s/%s/%s/%s/%s", vuln.VulnerabilityID, vuln.PkgID, vuln.PkgName, vuln.InstalledVersion, vuln.PkgPath) + if old, ok := uniqVulns[key]; ok && !shouldOverwrite(old, vuln) { + continue + } + uniqVulns[key] = vuln + } + + // Override the detected vulnerabilities + result.Vulnerabilities = lo.Values(uniqVulns) + if len(result.Vulnerabilities) == 0 { + result.Vulnerabilities = nil + } +} + +func filterMisconfigurations(result *types.Result, severities []string, includeNonFailures bool, + ignoreConfig IgnoreConfig) { + var filtered []types.DetectedMisconfiguration + result.MisconfSummary = new(types.MisconfSummary) + + for _, misconf := range result.Misconfigurations { + // Filter by severity + if !slices.Contains(severities, misconf.Severity) { + continue + } + + // Filter by ignore file + ids := append([]string{ + misconf.ID, + misconf.AVDID, + }, misconf.Aliases...) + if f := ignoreConfig.MatchMisconfiguration(ids, result.Target); f != nil { + result.ModifiedFindings = append(result.ModifiedFindings, + types.NewModifiedFinding(misconf, types.FindingStatusIgnored, f.Statement, ignoreConfig.FilePath)) + continue + } + + // Count successes and failures + updateMisconfSummary(misconf.Status, result.MisconfSummary) + + if misconf.Status != types.MisconfStatusFailure && !includeNonFailures { + continue + } + filtered = append(filtered, misconf) + } + + result.Misconfigurations = filtered + if result.MisconfSummary.Empty() { + result.Misconfigurations = nil + result.MisconfSummary = nil + } +} + +func filterSecrets(result *types.Result, severities []string, ignoreConfig IgnoreConfig) { + var filtered []types.DetectedSecret + for _, secret := range result.Secrets { + if !slices.Contains(severities, secret.Severity) { + // Filter by severity + continue + } else if f := ignoreConfig.MatchSecret(secret.RuleID, result.Target); f != nil { + // Filter by ignore file + result.ModifiedFindings = append(result.ModifiedFindings, + types.NewModifiedFinding(secret, types.FindingStatusIgnored, f.Statement, ignoreConfig.FilePath)) + continue + } + filtered = append(filtered, secret) + } + result.Secrets = filtered +} + +func filterLicenses(result *types.Result, severities, ignoreLicenseNames []string, ignoreConfig IgnoreConfig) { + // Merge ignore license names into ignored findings + var ignoreLicenses IgnoreConfig + for _, licenseName := range ignoreLicenseNames { + ignoreLicenses.Licenses = append(ignoreLicenses.Licenses, IgnoreFinding{ + ID: licenseName, + }) + } + + var filtered []types.DetectedLicense + for _, l := range result.Licenses { + // Filter by severity + if !slices.Contains(severities, l.Severity) { + continue + } + + // Filter by `--ignored-licenses` + if f := ignoreLicenses.MatchLicense(l.Name, l.FilePath); f != nil { + result.ModifiedFindings = append(result.ModifiedFindings, + types.NewModifiedFinding(l, types.FindingStatusIgnored, "", "--ignored-licenses")) + continue + } + + // Filter by ignore file + if f := ignoreConfig.MatchLicense(l.Name, l.FilePath); f != nil { + result.ModifiedFindings = append(result.ModifiedFindings, + types.NewModifiedFinding(l, types.FindingStatusIgnored, f.Statement, ignoreConfig.FilePath)) + continue + } + + filtered = append(filtered, l) + } + result.Licenses = filtered +} + +func updateMisconfSummary(status types.MisconfStatus, summary *types.MisconfSummary) { + switch status { + case types.MisconfStatusFailure: + summary.Failures++ + case types.MisconfStatusPassed: + summary.Successes++ + } +} + +func applyPolicy(ctx context.Context, result *types.Result, policyFile string) error { + policy, err := os.ReadFile(policyFile) + if err != nil { + return xerrors.Errorf("unable to read the policy file: %w", err) + } + + query, err := rego.New( + rego.Query("data.trivy.ignore"), + rego.Module("lib.rego", module), + rego.Module("trivy.rego", string(policy)), + rego.SetRegoVersion(ast.RegoV0), + ).PrepareForEval(ctx) + if err != nil { + return xerrors.Errorf("unable to prepare for eval: %w", err) + } + + // Vulnerabilities + filteredVulns, modifiedVulns, err := filterFindingsByRego(ctx, query, result.Vulnerabilities, policyFile) + if err != nil { + return err + } + result.Vulnerabilities = filteredVulns + result.ModifiedFindings = append(result.ModifiedFindings, modifiedVulns...) + + // Misconfigurations + filteredMisconfs, modifiedMisconfs, err := filterFindingsByRego(ctx, query, result.Misconfigurations, policyFile) + if err != nil { + return err + } + + for _, m := range modifiedMisconfs { + misconf, ok := m.Finding.(types.DetectedMisconfiguration) + if !ok { + continue + } + switch misconf.Status { + case types.MisconfStatusFailure: + result.MisconfSummary.Failures-- + case types.MisconfStatusPassed: + result.MisconfSummary.Successes-- + } + } + + result.Misconfigurations = filteredMisconfs + result.ModifiedFindings = append(result.ModifiedFindings, modifiedMisconfs...) + + // Secrets + filteredSecrets, modifiedSecrets, err := filterFindingsByRego(ctx, query, result.Secrets, policyFile) + if err != nil { + return err + } + result.Secrets = filteredSecrets + result.ModifiedFindings = append(result.ModifiedFindings, modifiedSecrets...) + + // Licenses + filteredLicenses, modifiedLicenses, err := filterFindingsByRego(ctx, query, result.Licenses, policyFile) + if err != nil { + return err + } + result.Licenses = filteredLicenses + result.ModifiedFindings = append(result.ModifiedFindings, modifiedLicenses...) + return nil +} + +func filterFindingsByRego[T types.Finding]( + ctx context.Context, query rego.PreparedEvalQuery, findings []T, policyFile string, +) ([]T, []types.ModifiedFinding, error) { + var filtered []T + var modified []types.ModifiedFinding + + for _, finding := range findings { + ignored, err := evaluate(ctx, query, finding) + if err != nil { + return nil, nil, err + } + if ignored { + modified = append(modified, + types.NewModifiedFinding(finding, types.FindingStatusIgnored, "Filtered by Rego", policyFile)) + continue + } + filtered = append(filtered, finding) + } + return filtered, modified, nil +} + +func evaluate[T types.Finding](ctx context.Context, query rego.PreparedEvalQuery, finding T) (bool, error) { + type regoInput struct { + Data T `json:",inline"` + Type string `json:"Type"` + } + + ri := regoInput{ + Data: finding, + Type: string(finding.FindingType()), + } + + results, err := query.Eval(ctx, rego.EvalInput(ri)) + if err != nil { + return false, xerrors.Errorf("unable to evaluate the policy: %w", err) + } else if len(results) == 0 { + // Handle undefined result. + return false, nil + } + ignore, ok := results[0].Expressions[0].Value.(bool) + if !ok { + // Handle unexpected result type. + return false, xerrors.New("the policy must return boolean") + } + return ignore, nil +} + +func shouldOverwrite(oldVuln, newVuln types.DetectedVulnerability) bool { + // The same vulnerability must be picked always. + return oldVuln.FixedVersion < newVuln.FixedVersion +} diff --git a/pkg/result/filter_test.go b/pkg/result/filter_test.go new file mode 100644 index 000000000000..be32565637b9 --- /dev/null +++ b/pkg/result/filter_test.go @@ -0,0 +1,1265 @@ +package result_test + +import ( + "testing" + "time" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/result" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vex" +) + +func TestFilter(t *testing.T) { + var ( + pkg1 = ftypes.Package{ + ID: "foo@v1.2.3", + Name: "foo", + Version: "v1.2.3", + Identifier: ftypes.PkgIdentifier{ + UID: "01", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "foo", + Version: "v1.2.3", + }, + }, + } + vuln1 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2019-0001", + PkgName: pkg1.Name, + InstalledVersion: pkg1.Version, + FixedVersion: "1.2.4", + PkgIdentifier: ftypes.PkgIdentifier{ + UID: pkg1.Identifier.UID, + PURL: pkg1.Identifier.PURL, + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + } + vuln2 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2019-0002", + PkgName: pkg1.Name, + InstalledVersion: pkg1.Version, + FixedVersion: "1.2.4", + PkgIdentifier: ftypes.PkgIdentifier{ + UID: pkg1.Identifier.UID, + PURL: pkg1.Identifier.PURL, + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + } + vuln3 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2019-0003", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + } + vuln4 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2019-0004", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + } + vuln5 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2019-0005", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + } + vuln6 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2019-0006", + PkgName: "foo", + InstalledVersion: "v1.2.3", + FixedVersion: "1.2.4", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "foo", + Version: "v1.2.3", + }, + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + } + vuln7 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2019-0007", + PkgName: "bar", + InstalledVersion: "v2.3.4", + FixedVersion: "2.3.5", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "bar", + Version: "v2.3.4", + }, + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + } + misconf1 = types.DetectedMisconfiguration{ + Type: "Kubernetes Security Check", + ID: "ID100", + AVDID: "AVD-ID100", + Title: "Bad Deployment", + Message: "something bad", + Severity: dbTypes.SeverityHigh.String(), + Status: types.MisconfStatusFailure, + } + misconf2 = types.DetectedMisconfiguration{ + Type: "Kubernetes Security Check", + ID: "ID200", + AVDID: "AVD-ID200", + Title: "Bad Pod", + Message: "something bad", + Severity: dbTypes.SeverityLow.String(), + Status: types.MisconfStatusPassed, + } + misconf3 = types.DetectedMisconfiguration{ + Type: "Kubernetes Security Check", + ID: "ID300", + AVDID: "AVD-ID300", + Title: "Bad Job", + Message: "something bad", + Severity: dbTypes.SeverityLow.String(), + Status: types.MisconfStatusFailure, + } + misconf4 = types.DetectedMisconfiguration{ + Type: "Kubernetes Security Check", + ID: "ID400", + Aliases: []string{"AVD-ID400"}, + Title: "Bad Job", + Message: "something bad", + Severity: dbTypes.SeverityLow.String(), + Status: types.MisconfStatusFailure, + } + secret1 = types.DetectedSecret{ + RuleID: "generic-wanted-rule", + Severity: dbTypes.SeverityHigh.String(), + Title: "Secret that should pass filter on rule id", + StartLine: 1, + EndLine: 2, + Match: "*****", + } + secret2 = types.DetectedSecret{ + RuleID: "generic-unwanted-rule", + Severity: dbTypes.SeverityLow.String(), + Title: "Secret that should not pass filter on rule id", + StartLine: 3, + EndLine: 4, + Match: "*****", + } + secret3 = types.DetectedSecret{ + RuleID: "generic-unwanted-rule2", + Severity: dbTypes.SeverityLow.String(), + Title: "Secret that should not pass filter on rule id", + StartLine: 5, + EndLine: 6, + Match: "*****", + } + license1 = types.DetectedLicense{ + Name: "GPL-3.0", + Severity: dbTypes.SeverityLow.String(), + PkgName: "foo", + FilePath: "usr/share/gcc/python/libstdcxx/v6/__init__.py", + Category: "restricted", + Confidence: 1, + } + license2 = types.DetectedLicense{ + Name: "GPL-3.0", + PkgName: "bar", + Severity: dbTypes.SeverityLow.String(), + FilePath: "usr/share/gcc/python/libstdcxx/v6/printers.py", + Category: "restricted", + Confidence: 1, + } + license3 = types.DetectedLicense{ + Name: "mit AND GPL-2.0-or-later", + Severity: dbTypes.SeverityLow.String(), + FilePath: "usr/share/gcc/python/libstdcxx/v6/__init__.py", + Category: "restricted", + Confidence: 1, + } + license4 = types.DetectedLicense{ + Name: "Apache-2.0 WITH LLVM-exception", + Severity: dbTypes.SeverityLow.String(), + FilePath: "usr/share/llvm/LICENSE.txt", + Category: "restricted", + Confidence: 1, + } + license5 = types.DetectedLicense{ + Name: "GPL-3.0 WITH GCC-exception-3.1", + Severity: dbTypes.SeverityLow.String(), + FilePath: "usr/share/gcc/LICENSE.txt", + Category: "restricted", + Confidence: 1, + } + ) + type args struct { + report types.Report + severities []dbTypes.Severity + ignoreStatuses []dbTypes.Status + ignoreFile string + policyFile string + vexPath string + } + tests := []struct { + name string + args args + want types.Report + }{ + { + name: "severities", + args: args{ + report: types.Report{ + Results: []types.Result{ + { + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, // filtered + vuln2, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + misconf1, + misconf2, // filtered + }, + Secrets: []types.DetectedSecret{ + secret1, + secret2, // filtered + }, + }, + }, + }, + severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, + dbTypes.SeverityHigh, + }, + }, + want: types.Report{ + Results: []types.Result{ + { + Vulnerabilities: []types.DetectedVulnerability{ + vuln2, + }, + MisconfSummary: &types.MisconfSummary{ + Successes: 0, + Failures: 1, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + misconf1, + }, + Secrets: []types.DetectedSecret{ + secret1, + }, + }, + }, + }, + }, + { + name: "filter by VEX", + args: args{ + report: types.Report{ + ArtifactName: ".", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{ + types.Result{ + Target: "gobinary", + Class: types.ClassLangPkg, + Type: ftypes.GoBinary, + Packages: []ftypes.Package{pkg1}, + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, + vuln2, + }, + }, + }, + }, + severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, + dbTypes.SeverityHigh, + dbTypes.SeverityMedium, + dbTypes.SeverityLow, + dbTypes.SeverityUnknown, + }, + vexPath: "testdata/openvex.json", + }, + want: types.Report{ + ArtifactName: ".", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{ + types.Result{ + Target: "gobinary", + Class: types.ClassLangPkg, + Type: ftypes.GoBinary, + Packages: []ftypes.Package{pkg1}, + Vulnerabilities: []types.DetectedVulnerability{ + vuln2, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusNotAffected, + Statement: "vulnerable_code_not_in_execute_path", + Source: "testdata/openvex.json", + Finding: vuln1, + }, + }, + }, + }, + }, + }, + { + name: "ignore unfixed", + args: args{ + report: types.Report{ + Results: types.Results{ + types.Result{ + Target: "debian:11 (debian 11)", + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, + vuln2, + }, + }, + }, + }, + severities: []dbTypes.Severity{dbTypes.SeverityHigh}, + ignoreStatuses: []dbTypes.Status{ + dbTypes.StatusWillNotFix, + dbTypes.StatusEndOfLife, + }, + }, + want: types.Report{ + Results: types.Results{ + { + Target: "debian:11 (debian 11)", + }, + }, + }, + }, + { + name: "ignore file", + args: args{ + report: types.Report{ + Results: types.Results{ + { + Target: "package-lock.json", + Class: types.ClassLangPkg, + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, // ignored + vuln2, // filtered by severity + vuln3, + vuln4, + vuln5, // ignored + vuln6, // ignored + }, + }, + { + Target: "deployment.yaml", + Class: types.ClassConfig, + Misconfigurations: []types.DetectedMisconfiguration{ + misconf1, + misconf2, + misconf3, // ignored + misconf4, // ignored + }, + }, + { + Target: "config.yaml", + Secrets: []types.DetectedSecret{ + secret1, + secret2, + }, + }, + { + Target: "LICENSE.txt", + Licenses: []types.DetectedLicense{ + license1, // ignored + license3, + }, + }, + }, + }, + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + dbTypes.SeverityHigh, + }, + ignoreFile: "testdata/.trivyignore", + }, + want: types.Report{ + Results: types.Results{ + { + Target: "package-lock.json", + Class: types.ClassLangPkg, + Vulnerabilities: []types.DetectedVulnerability{ + vuln3, + vuln4, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore", + Finding: vuln1, + }, + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore", + Finding: vuln5, + }, + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore", + Finding: vuln6, + }, + }, + }, + { + Target: "deployment.yaml", + Class: types.ClassConfig, + MisconfSummary: &types.MisconfSummary{ + Successes: 1, + Failures: 1, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + misconf1, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeMisconfiguration, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore", + Finding: misconf3, + }, + { + Type: types.FindingTypeMisconfiguration, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore", + Finding: misconf4, + }, + }, + }, + { + Target: "config.yaml", + Secrets: []types.DetectedSecret{ + secret1, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeSecret, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore", + Finding: secret2, + }, + }, + }, + { + Target: "LICENSE.txt", + Licenses: []types.DetectedLicense{ + license3, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeLicense, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore", + Finding: license1, + }, + }, + }, + }, + }, + }, + { + name: "ignore yaml", + args: args{ + report: types.Report{ + Results: types.Results{ + { + Target: "foo/package-lock.json", + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, // ignored + vuln2, // filtered by severity + vuln3, // ignored + vuln4, + vuln5, // ignored + vuln6, + vuln7, // filtered by PURL + }, + }, + { + Target: "app/Dockerfile", + Misconfigurations: []types.DetectedMisconfiguration{ + misconf1, // ignored + misconf2, // ignored + misconf3, + misconf4, // ignored + }, + }, + { + Target: "config.yaml", + Secrets: []types.DetectedSecret{ + secret1, + secret2, // ignored + secret3, // ignored + }, + }, + { + Target: "LICENSE.txt", + Licenses: []types.DetectedLicense{ + license1, // ignored + license2, + license3, // ignored by combination for 2 licenses + license4, // ignored by WITH operator + license5, // not ignored (different exception) + }, + }, + }, + }, + ignoreFile: "testdata/.trivyignore.yaml", + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + dbTypes.SeverityHigh, + }, + }, + want: types.Report{ + Results: types.Results{ + { + Target: "foo/package-lock.json", + Vulnerabilities: []types.DetectedVulnerability{ + vuln4, + vuln6, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: vuln1, + }, + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: vuln3, + }, + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: vuln5, + }, + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: vuln7, + }, + }, + }, + { + Target: "app/Dockerfile", + MisconfSummary: &types.MisconfSummary{ + Successes: 0, + Failures: 1, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + misconf3, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeMisconfiguration, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: misconf1, + }, + { + Type: types.FindingTypeMisconfiguration, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: misconf2, + }, + { + Type: types.FindingTypeMisconfiguration, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: misconf4, + }, + }, + }, + { + Target: "config.yaml", + Secrets: []types.DetectedSecret{ + secret1, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeSecret, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: secret2, + }, + { + Type: types.FindingTypeSecret, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: secret3, + }, + }, + }, + { + Target: "LICENSE.txt", + Licenses: []types.DetectedLicense{ + license2, + license5, // not ignored (different exception) + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeLicense, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: license1, + }, + { + Type: types.FindingTypeLicense, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Statement: "All license components are individually ignored", + Finding: license3, + }, + { + Type: types.FindingTypeLicense, + Status: types.FindingStatusIgnored, + Source: "testdata/.trivyignore.yaml", + Finding: license4, + }, + }, + }, + }, + }, + }, + { + name: "policy file for vulnerabilities", + args: args{ + report: types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, + vuln2, // ignored by severity + vuln3, // ignored by check + }, + }, + }, + }, + severities: []dbTypes.Severity{dbTypes.SeverityLow}, + policyFile: "./testdata/ignore-vuln.rego", + }, + want: types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Statement: "Filtered by Rego", + Source: "testdata/ignore-vuln.rego", + Finding: vuln3, + }, + }, + }, + }, + }, + }, + { + name: "policy file for misconfigurations", + args: args{ + report: types.Report{ + Results: types.Results{ + { + Misconfigurations: []types.DetectedMisconfiguration{ + misconf1, + misconf2, // passed + misconf3, // ignored by check + }, + }, + }, + }, + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + dbTypes.SeverityHigh, + }, + policyFile: "./testdata/ignore-misconf.rego", + }, + want: types.Report{ + Results: types.Results{ + { + MisconfSummary: &types.MisconfSummary{ + Successes: 1, + Failures: 1, + }, + Misconfigurations: []types.DetectedMisconfiguration{ + misconf1, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeMisconfiguration, + Status: types.FindingStatusIgnored, + Statement: "Filtered by Rego", + Source: "testdata/ignore-misconf.rego", + Finding: misconf3, + }, + }, + }, + }, + }, + }, + { + name: "ignore file for licenses and secrets", + args: args{ + report: types.Report{ + Results: types.Results{ + { + Licenses: []types.DetectedLicense{ + license1, + license2, + }, + Secrets: []types.DetectedSecret{ + secret1, + secret2, + }, + }, + }, + }, + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + dbTypes.SeverityHigh, + }, + policyFile: "./testdata/test-ignore-policy-licenses-and-secrets.rego", + }, + want: types.Report{ + Results: types.Results{ + { + Licenses: []types.DetectedLicense{ + license1, + }, + Secrets: []types.DetectedSecret{ + secret1, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeSecret, + Status: types.FindingStatusIgnored, + Statement: "Filtered by Rego", + Source: "testdata/test-ignore-policy-licenses-and-secrets.rego", + Finding: secret2, + }, + { + Type: types.FindingTypeLicense, + Status: types.FindingStatusIgnored, + Statement: "Filtered by Rego", + Source: "testdata/test-ignore-policy-licenses-and-secrets.rego", + Finding: license2, + }, + }, + }, + }, + }, + }, + { + name: "happy path with duplicates, one with empty fixed version", + args: args{ + report: types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.5", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0001", + PkgName: "baz", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0001", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: "", + }, + }, + { + VulnerabilityID: "CVE-2018-0002", + PkgName: "bar", + InstalledVersion: "2.0.0", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: "", + }, + }, + }, + }, + }, + }, + severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, + dbTypes.SeverityHigh, + dbTypes.SeverityUnknown, + }, + }, + want: types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2018-0001", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.5", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityUnknown.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0002", + PkgName: "bar", + InstalledVersion: "2.0.0", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityUnknown.String(), + }, + }, + { + VulnerabilityID: "CVE-2018-0001", + PkgName: "baz", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with duplicates and different package paths", + args: args{ + report: types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgPath: "some/path/a.jar", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0001", + PkgPath: "some/other/path/a.jar", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgName: "baz", + PkgPath: "some/path/b.jar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgPath: "some/path/b.jar", + PkgName: "baz", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0003", + PkgPath: "some/path/c.jar", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: "", + }, + }, + { + VulnerabilityID: "CVE-2019-0003", + PkgName: "bar", + PkgPath: "some/path/c.jar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: "", + }, + }, + { + VulnerabilityID: "CVE-2019-0003", + PkgName: "bar", + PkgPath: "some/other/path/c.jar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: "", + }, + }, + }, + }, + }, + }, + severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, + dbTypes.SeverityHigh, + dbTypes.SeverityUnknown, + }, + }, + want: types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgPath: "some/other/path/a.jar", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0001", + PkgPath: "some/path/a.jar", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0003", + PkgName: "bar", + PkgPath: "some/other/path/c.jar", + InstalledVersion: "1.2.3", + FixedVersion: "", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityUnknown.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0003", + PkgName: "bar", + PkgPath: "some/path/c.jar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityUnknown.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgPath: "some/path/b.jar", + PkgName: "baz", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityHigh.String(), + }, + }, + }, + }, + }, + }, + }, + { + name: "ignore findings by type in policy file", + args: args{ + report: types.Report{ + Results: types.Results{ + { + Target: "foo/package-lock.json", + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, + vuln7, // filtered by PkgName and Type + }, + }, + { + Target: "LICENSE.txt", + Licenses: []types.DetectedLicense{ + license1, // filtered by PkgName and Type + license2, + }, + }, + }, + }, + policyFile: "testdata/ignore-by-type.rego", + severities: []dbTypes.Severity{ + dbTypes.SeverityLow, + }, + }, + want: types.Report{ + Results: types.Results{ + { + Target: "foo/package-lock.json", + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusIgnored, + Source: "testdata/ignore-by-type.rego", + Statement: "Filtered by Rego", + Finding: vuln7, + }, + }, + }, + { + Target: "LICENSE.txt", + Licenses: []types.DetectedLicense{ + license2, + }, + ModifiedFindings: []types.ModifiedFinding{ + { + Type: types.FindingTypeLicense, + Status: types.FindingStatusIgnored, + Source: "testdata/ignore-by-type.rego", + Statement: "Filtered by Rego", + Finding: license1, + }, + }, + }, + }, + }, + }, + { + name: "happy path with duplicates and different package IDs", + args: args{ + report: types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgID: "foo:1.2.3::abcdef1234567001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0001", + PkgID: "foo:1.2.3::abcdef1234567002", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgID: "bar@1.2.3", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0002", + PkgID: "bar@1.2.3", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + }, + }, + }, + }, + severities: []dbTypes.Severity{ + dbTypes.SeverityCritical, + dbTypes.SeverityHigh, + dbTypes.SeverityUnknown, + }, + }, + want: types.Report{ + Results: types.Results{ + { + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0002", + PkgID: "bar@1.2.3", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0001", + PkgID: "foo:1.2.3::abcdef1234567001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + { + VulnerabilityID: "CVE-2019-0001", + PkgID: "foo:1.2.3::abcdef1234567002", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fakeTime := time.Date(2020, 8, 10, 7, 28, 17, 958601, time.UTC) + ctx := clock.With(t.Context(), fakeTime) + + var vexSources []vex.Source + if tt.args.vexPath != "" { + vexSources = append(vexSources, vex.Source{ + Type: vex.TypeFile, + FilePath: tt.args.vexPath, + }) + } + + err := result.Filter(ctx, tt.args.report, result.FilterOptions{ + Severities: tt.args.severities, + VEXSources: vexSources, + IgnoreStatuses: tt.args.ignoreStatuses, + IgnoreFile: tt.args.ignoreFile, + PolicyFile: tt.args.policyFile, + }) + require.NoError(t, err) + assert.Equal(t, tt.want, tt.args.report) + }) + } +} diff --git a/pkg/result/ignore.go b/pkg/result/ignore.go new file mode 100644 index 000000000000..35926536873a --- /dev/null +++ b/pkg/result/ignore.go @@ -0,0 +1,314 @@ +package result + +import ( + "bufio" + "context" + "errors" + "io/fs" + "os" + "path/filepath" + "strings" + "time" + + "github.com/bmatcuk/doublestar/v4" + "github.com/package-url/packageurl-go" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/licensing/expression" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/purl" +) + +// IgnoreFinding represents an item to be ignored. +type IgnoreFinding struct { + // ID is the identifier of the vulnerability, misconfiguration, secret, or license. + // e.g. CVE-2019-8331, AVD-AWS-0175, etc. + // required: true + ID string `yaml:"id"` + + // Paths is the list of file paths to ignore. + // If Paths is not set, the ignore finding is applied to all files. + // required: false + Paths []string `yaml:"paths"` + + // PURLs is the list of packages to ignore. + // If PURLs is not set, the ignore finding is applied to packages. + // The field is currently available only for vulnerabilities. + // required: false + PURLs []*purl.PackageURL `yaml:"-"` // Filled in UnmarshalYAML + + // ExpiredAt is the expiration date of the ignore finding. + // If ExpiredAt is not set, the ignore finding is always valid. + // required: false + ExpiredAt time.Time `yaml:"expired_at"` + + // Statement describes the reason for ignoring the finding. + // required: false + Statement string `yaml:"statement"` +} + +// UnmarshalYAML is a custom unmarshaler for IgnoreFinding that handles +// the conversion of PURLs from strings to purl.PackageURL objects. +func (i *IgnoreFinding) UnmarshalYAML(value *yaml.Node) error { + // Define a shadow type to prevent infinite recursion + type plain IgnoreFinding + var tmp struct { + plain `yaml:",inline"` + PURLs []string `yaml:"purls"` + } + if err := value.Decode(&tmp); err != nil { + return err + } + + *i = IgnoreFinding(tmp.plain) + + for _, pattern := range i.Paths { + if !doublestar.ValidatePattern(pattern) { + return xerrors.Errorf("invalid path pattern in the ignore file, id: %s, path: %s", i.ID, pattern) + } + } + + // Convert string PURLs to purl.PackageURL objects + for _, purlStr := range tmp.PURLs { + parsedPURL, err := purl.FromString(purlStr) + if err != nil { + return xerrors.Errorf("purl error in the ignore file: %w", err) + } + i.PURLs = append(i.PURLs, parsedPURL) + } + + return nil +} + +type IgnoreFindings []IgnoreFinding + +func (f *IgnoreFindings) Match(id, path string, pkg *packageurl.PackageURL) *IgnoreFinding { + for _, finding := range *f { + if id != finding.ID { + continue + } + if !matchPath(path, finding.Paths) || !matchPURL(pkg, finding.PURLs) { + continue + } + + log.Debug("Ignored", log.String("id", id), log.String("target", path)) + return &finding + + } + return nil +} + +func matchPath(path string, patterns []string) bool { + if len(patterns) == 0 { + return true + } + + for _, pattern := range patterns { + // Patterns are already validated, so we ignore errors here + if matched, _ := doublestar.Match(pattern, path); matched { + return true + } + } + return false +} + +func matchPURL(target *packageurl.PackageURL, purls []*purl.PackageURL) bool { + if target == nil || len(purls) == 0 { + return true + } + + for _, p := range purls { + if p.Match(target) { + return true + } + } + return false +} + +func (f *IgnoreFindings) Prune(ctx context.Context) { + var findings IgnoreFindings + for _, finding := range *f { + // Filter out expired ignore findings + if !finding.ExpiredAt.IsZero() && finding.ExpiredAt.Before(clock.Now(ctx)) { + continue + } + findings = append(findings, finding) + } + *f = findings +} + +// IgnoreConfig represents the structure of .trivyignore.yaml. +type IgnoreConfig struct { + FilePath string + Vulnerabilities IgnoreFindings `yaml:"vulnerabilities"` + Misconfigurations IgnoreFindings `yaml:"misconfigurations"` + Secrets IgnoreFindings `yaml:"secrets"` + Licenses IgnoreFindings `yaml:"licenses"` +} + +func (c *IgnoreConfig) MatchVulnerability(vulnID, filePath, pkgPath string, pkg *packageurl.PackageURL) *IgnoreFinding { + paths := []string{ + filePath, + pkgPath, + } + for _, p := range paths { + if f := c.Vulnerabilities.Match(vulnID, p, pkg); f != nil { + return f + } + } + return nil +} + +func (c *IgnoreConfig) MatchMisconfiguration(ids []string, filePath string) *IgnoreFinding { + for _, id := range ids { + if f := c.Misconfigurations.Match(id, filePath, nil); f != nil { + return f + } + } + return nil +} + +func (c *IgnoreConfig) MatchSecret(secretID, filePath string) *IgnoreFinding { + return c.Secrets.Match(secretID, filePath, nil) +} + +func (c *IgnoreConfig) MatchLicense(licenseID, filePath string) *IgnoreFinding { + if f := c.Licenses.Match(licenseID, filePath, nil); f != nil { + return f + } + + var licenseNotMatch bool + matchLicenses := func(expr expression.Expression) expression.Expression { + // If one of parts of the expression doesn't match - skip check for the rest of the expression + if licenseNotMatch { + return expr + } + + if e, ok := expr.(expression.CompoundExpr); ok && e.Conjunction() != expression.TokenWith { + // Check only license with `WITH` operator as single license + return e + } + + if !expr.IsSPDXExpression() || c.Licenses.Match(expr.String(), filePath, nil) == nil { + licenseNotMatch = true + } + return expr + } + + _, err := expression.Normalize(licenseID, licensing.NormalizeLicenseExpression, matchLicenses) + if err != nil { + log.WithPrefix("ignore").Debug("Unable to normalize license expression", log.String("license", licenseID), log.Err(err)) + return nil + } + + if !licenseNotMatch { + return &IgnoreFinding{ + ID: licenseID, + Statement: "All license components are individually ignored", + } + } + + return nil +} + +func ParseIgnoreFile(ctx context.Context, ignoreFile string) (IgnoreConfig, error) { + var conf IgnoreConfig + if _, err := os.Stat(ignoreFile); errors.Is(err, fs.ErrNotExist) { + // .trivyignore doesn't necessarily exist + log.Debug("Specified ignore file does not exist", log.String("file", ignoreFile)) + return IgnoreConfig{}, nil + } else if filepath.Ext(ignoreFile) == ".yml" || filepath.Ext(ignoreFile) == ".yaml" { + conf, err = parseIgnoreYAML(ignoreFile) + if err != nil { + return IgnoreConfig{}, xerrors.Errorf("%s parse error: %w", ignoreFile, err) + } + } else { + ignoredFindings, err := parseIgnore(ignoreFile) + if err != nil { + return IgnoreConfig{}, xerrors.Errorf("%s parse error: %w", ignoreFile, err) + } + + // IDs in .trivyignore are treated as IDs for all scanners + // as it is unclear which type of security issue they are + conf = IgnoreConfig{ + Vulnerabilities: ignoredFindings, + Misconfigurations: ignoredFindings, + Secrets: ignoredFindings, + Licenses: ignoredFindings, + } + } + + conf.Vulnerabilities.Prune(ctx) + conf.Misconfigurations.Prune(ctx) + conf.Secrets.Prune(ctx) + conf.Licenses.Prune(ctx) + conf.FilePath = filepath.ToSlash(filepath.Clean(ignoreFile)) + + return conf, nil +} + +func parseIgnoreYAML(ignoreFile string) (IgnoreConfig, error) { + // Read .trivyignore.yaml + b, err := os.ReadFile(ignoreFile) + if err != nil { + return IgnoreConfig{}, xerrors.Errorf("file open error: %w", err) + } + log.Debug("Found an ignore yaml", log.FilePath(ignoreFile)) + + // Parse the YAML content + // We have to use Unmarshal() due to go-yaml returning an error with Decode() + // ref: https://github.com/go-yaml/yaml/issues/805 + var ignoreConfig IgnoreConfig + if err = yaml.Unmarshal(b, &ignoreConfig); err != nil { + return IgnoreConfig{}, xerrors.Errorf("yaml decode error: %w", err) + } + return ignoreConfig, nil +} + +func parseIgnore(ignoreFile string) (IgnoreFindings, error) { + f, err := os.Open(ignoreFile) + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + log.Debug("Found an ignore file", log.FilePath(ignoreFile)) + + var ignoredFindings IgnoreFindings + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "#") || line == "" { + continue + } + // Process all fields + var exp time.Time + fields := strings.Fields(line) + if len(fields) > 1 { + exp, err = getExpirationDate(fields) + if err != nil { + log.Warn("Error while parsing expiration date in .trivyignore file", log.Err(err)) + continue + } + } + ignoredFindings = append(ignoredFindings, IgnoreFinding{ + ID: fields[0], + ExpiredAt: exp, + }) + } + + return ignoredFindings, nil +} + +func getExpirationDate(fields []string) (time.Time, error) { + for _, field := range fields { + if after, ok := strings.CutPrefix(field, "exp:"); ok { + return time.Parse("2006-01-02", after) + } + } + + return time.Time{}, nil +} diff --git a/pkg/result/ignore_test.go b/pkg/result/ignore_test.go new file mode 100644 index 000000000000..b8739858c4fc --- /dev/null +++ b/pkg/result/ignore_test.go @@ -0,0 +1,75 @@ +package result + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseIgnoreFile(t *testing.T) { + t.Run("happy path valid config file", func(t *testing.T) { + got, err := ParseIgnoreFile(t.Context(), "testdata/.trivyignore") + require.NoError(t, err) + assert.Equal(t, "testdata/.trivyignore", got.FilePath) + + // IDs in .trivyignore are treated as IDs for all scanners + // as it is unclear which type of security issue they are + assert.Len(t, got.Vulnerabilities, 8) + assert.Len(t, got.Misconfigurations, 8) + assert.Len(t, got.Secrets, 8) + assert.Len(t, got.Licenses, 8) + }) + + t.Run("happy path valid YAML config file", func(t *testing.T) { + got, err := ParseIgnoreFile(t.Context(), "testdata/.trivyignore.yaml") + require.NoError(t, err) + assert.Equal(t, "testdata/.trivyignore.yaml", got.FilePath) + assert.Len(t, got.Vulnerabilities, 5) + assert.Len(t, got.Misconfigurations, 4) + assert.Len(t, got.Secrets, 3) + assert.Len(t, got.Licenses, 5) + }) + + t.Run("empty YAML file passed", func(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "TestParseIgnoreFile-*.yaml") + require.NoError(t, err) + defer f.Close() + + _, err = ParseIgnoreFile(t.Context(), f.Name()) + require.NoError(t, err) + }) + + t.Run("invalid YAML file passed", func(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "TestParseIgnoreFile-*.yaml") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString("this file is not a yaml file") + require.NoError(t, err) + + got, err := ParseIgnoreFile(t.Context(), f.Name()) + require.ErrorContains(t, err, "yaml decode error") + assert.Empty(t, got) + }) + + t.Run("invalid file passed", func(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "TestParseIgnoreFile-*") + require.NoError(t, err) + defer f.Close() + + _, err = f.WriteString("this file is not a valid trivyignore file") + require.NoError(t, err) + + _, err = ParseIgnoreFile(t.Context(), f.Name()) + require.NoError(t, err) // TODO(simar7): We don't verify correctness, should we? + }) + + t.Run("non existing file passed", func(t *testing.T) { + got, err := ParseIgnoreFile(t.Context(), "does-not-exist.yaml") + require.NoError(t, err) + assert.Empty(t, got) + }) + +} diff --git a/pkg/result/module.go b/pkg/result/module.go new file mode 100644 index 000000000000..9dcfe3275b52 --- /dev/null +++ b/pkg/result/module.go @@ -0,0 +1,67 @@ +package result + +const ( + module = ` +package lib.trivy + +parse_cvss_vector_v3(cvss) = vector { + s := split(cvss, "/") + vector := { + "AttackVector": attack_vector[s[1]], + "AttackComplexity": attack_complexity[s[2]], + "PrivilegesRequired": privileges_required[s[3]], + "UserInteraction": user_interaction[s[4]], + "Scope": scope[s[5]], + "Confidentiality": confidentiality[s[6]], + "Integrity": integrity[s[7]], + "Availability": availability[s[8]], + } +} + +attack_vector := { + "AV:N": "Network", + "AV:A": "Adjacent", + "AV:L": "Local", + "AV:P": "Physical", +} + +attack_complexity := { + "AC:L": "Low", + "AC:H": "High", +} + +privileges_required := { + "PR:N": "None", + "PR:L": "Low", + "PR:H": "High", +} + +user_interaction := { + "UI:N": "None", + "UI:R": "Required", +} + +scope := { + "S:U": "Unchanged", + "S:C": "Changed", +} + +confidentiality := { + "C:N": "None", + "C:L": "Low", + "C:H": "High", +} + +integrity := { + "I:N": "None", + "I:L": "Low", + "I:H": "High", +} + +availability := { + "A:N": "None", + "A:L": "Low", + "A:H": "High", +} +` +) diff --git a/pkg/result/testdata/.trivyignore b/pkg/result/testdata/.trivyignore new file mode 100644 index 000000000000..f5e2ea3276a9 --- /dev/null +++ b/pkg/result/testdata/.trivyignore @@ -0,0 +1,17 @@ +# vulnerabilities +CVE-2019-0001 +CVE-2019-0002 +CVE-2019-0004 exp:2020-01-01 +CVE-2019-0005 exp:9999-01-01 +CVE-2019-0006 exp:9999-01-01 key2:value2 + +# misconfigurations +ID300 +## alias +AVD-ID400 + +# secrets +generic-unwanted-rule + +# license +GPL-3.0 \ No newline at end of file diff --git a/pkg/result/testdata/.trivyignore.yaml b/pkg/result/testdata/.trivyignore.yaml new file mode 100644 index 000000000000..a9433e3966c8 --- /dev/null +++ b/pkg/result/testdata/.trivyignore.yaml @@ -0,0 +1,45 @@ +vulnerabilities: + - id: CVE-2019-0001 + paths: + - "**/*-lock.json" + - id: CVE-2019-0002 + paths: + - "bar/package.json" + - id: CVE-2019-0003 + - id: CVE-2019-0004 + paths: + - "bar/package.json" + - id: CVE-2019-0005 + expired_at: 2023-01-01 + - id: CVE-2019-0006 + expired_at: 2020-01-01 + - id: CVE-2019-0007 + purls: + - "pkg:golang/github.com/aquasecurity/aaa@9.9.9" + - "pkg:golang/github.com/aquasecurity/bar" # Match + +misconfigurations: + - id: ID100 + - id: ID200 + paths: + - "app/Dockerfile" + - id: ID300 + paths: + - "docs/Dockerfile" + - id: AVD-ID400 # alias +secrets: + - id: generic-unwanted-rule + - id: generic-unwanted-rule2 + paths: + - "**/config.yaml" + - id: generic-wanted-rule + paths: + - "foo/secret.txt" +licenses: + - id: GPL-3.0 + paths: + - "usr/share/gcc/python/libstdcxx/v6/__init__.py" + - id: MIT + - id: GPL-2.0-or-later + - id: Apache-2.0 WITH LLVM-exception + - id: LLVM-exception diff --git a/pkg/result/testdata/fixtures/sad.yaml b/pkg/result/testdata/fixtures/sad.yaml new file mode 100644 index 000000000000..f7d55cb4b458 --- /dev/null +++ b/pkg/result/testdata/fixtures/sad.yaml @@ -0,0 +1,6 @@ +- bucket: vulnerability + pairs: + - key: CVE-2019-0001 + value: + Title: + - aaa diff --git a/pkg/result/testdata/fixtures/vulnerability.yaml b/pkg/result/testdata/fixtures/vulnerability.yaml new file mode 100644 index 000000000000..623c14a24612 --- /dev/null +++ b/pkg/result/testdata/fixtures/vulnerability.yaml @@ -0,0 +1,59 @@ +- bucket: vulnerability + pairs: + - key: CVE-2019-0001 + value: + Title: dos + Description: dos vulnerability + Severity: MEDIUM + References: + - http://example.com + LastModifiedDate: "2020-01-01T01:01:00Z" + PublishedDate: "2001-01-01T01:01:00Z" + - key: CVE-2019-0002 + value: + Title: dos + Description: dos vulnerability + Severity: UNKNOWN + VendorSeverity: + nvd: 1 + References: + - http://example.com + LastModifiedDate: "2020-01-01T01:01:00Z" + PublishedDate: "2001-01-01T01:01:00Z" + - key: CVE-2019-0003 + value: + Title: dos + Description: dos vulnerability + References: + - http://example.com + - key: CVE-2019-0004 + value: + Title: dos + Description: dos vulnerability + Severity: MEDIUM + VendorSeverity: + redhat: 1 + CVSS: + nvd: + V2Vector: AV:N/AC:L/Au:N/C:P/I:P/A:P + V2Score: 4.5 + V3Vector: CVSS:3.0/PR:N/UI:N/S:U/C:H/I:H/A:H + V3Score: 5.6 + redhat: + V2Vector: AV:N/AC:M/Au:N/C:N/I:P/A:N + V2Score: 7.8 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + V3Score: 9.8 + References: + - http://example.com + CweIDs: + - CWE-311 + - key: CVE-2019-0005 + value: + Title: COVID-19 + Description: a nasty virus vulnerability for humans + Severity: MEDIUM + VendorSeverity: + ghsa: 4 + References: + - "https://www.who.int/emergencies/diseases/novel-coronavirus-2019" diff --git a/pkg/result/testdata/ignore-by-type.rego b/pkg/result/testdata/ignore-by-type.rego new file mode 100644 index 000000000000..5a8c7aa331e7 --- /dev/null +++ b/pkg/result/testdata/ignore-by-type.rego @@ -0,0 +1,11 @@ +package trivy + +ignore { + input.Type == "license" + input.PkgName == "foo" +} + +ignore { + input.Type == "vulnerability" + input.PkgName == "bar" +} \ No newline at end of file diff --git a/pkg/result/testdata/ignore-misconf.rego b/pkg/result/testdata/ignore-misconf.rego new file mode 100644 index 000000000000..1aa5062ddf24 --- /dev/null +++ b/pkg/result/testdata/ignore-misconf.rego @@ -0,0 +1,9 @@ +package trivy + +import data.lib.trivy + +default ignore=false + +ignore { + input.AVDID != "AVD-ID100" +} diff --git a/pkg/result/testdata/ignore-vuln.rego b/pkg/result/testdata/ignore-vuln.rego new file mode 100644 index 000000000000..12c2621ec2da --- /dev/null +++ b/pkg/result/testdata/ignore-vuln.rego @@ -0,0 +1,5 @@ +package trivy + +ignore { + input.VulnerabilityID != "CVE-2019-0001" +} diff --git a/pkg/result/testdata/openvex.json b/pkg/result/testdata/openvex.json new file mode 100644 index 000000000000..385f22e47deb --- /dev/null +++ b/pkg/result/testdata/openvex.json @@ -0,0 +1,17 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "author": "Aqua Security", + "role": "Project Release Bot", + "timestamp": "2023-01-16T19:07:16.853479631-06:00", + "version": 1, + "statements": [ + { + "vulnerability": {"name": "CVE-2019-0001"}, + "products": [ + {"@id": "pkg:golang/github.com/aquasecurity/foo@v1.2.3"} + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} diff --git a/pkg/result/testdata/test-ignore-policy-licenses-and-secrets.rego b/pkg/result/testdata/test-ignore-policy-licenses-and-secrets.rego new file mode 100644 index 000000000000..59cae4b8ca5b --- /dev/null +++ b/pkg/result/testdata/test-ignore-policy-licenses-and-secrets.rego @@ -0,0 +1,15 @@ +package trivy + +import data.lib.trivy + +default ignore=false + +ignore { + input.Name == "GPL-3.0" + input.FilePath == "usr/share/gcc/python/libstdcxx/v6/printers.py" +} + +ignore { + input.RuleID == "generic-unwanted-rule" + input.Title == "Secret that should not pass filter on rule id" +} diff --git a/pkg/rpc/client/client.go b/pkg/rpc/client/client.go new file mode 100644 index 000000000000..0dcb32d09d7a --- /dev/null +++ b/pkg/rpc/client/client.go @@ -0,0 +1,164 @@ +package client + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "sync" + + "github.com/samber/lo" + "github.com/twitchtv/twirp" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + r "github.com/aquasecurity/trivy/pkg/rpc" + "github.com/aquasecurity/trivy/pkg/types" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" + "github.com/aquasecurity/trivy/pkg/x/slices" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" + "github.com/aquasecurity/trivy/rpc/common" + rpc "github.com/aquasecurity/trivy/rpc/scanner" +) + +// ServiceOption holds options for RPC client +type ServiceOption struct { + RemoteURL string + CustomHeaders http.Header + PathPrefix string +} + +// Service implements the RPC client for remote scanning +type Service struct { + remoteURL string + customHeaders http.Header + client rpc.Scanner + httpClient *http.Client +} + +// NewService is the factory method to return RPC Service +func NewService(scannerOptions ServiceOption) Service { + httpClient := xhttp.Client() + + var twirpOpts []twirp.ClientOption + if scannerOptions.PathPrefix != "" { + twirpOpts = append(twirpOpts, twirp.WithClientPathPrefix(scannerOptions.PathPrefix)) + } + + return Service{ + remoteURL: scannerOptions.RemoteURL, + customHeaders: scannerOptions.CustomHeaders, + client: rpc.NewScannerProtobufClient(scannerOptions.RemoteURL, httpClient, twirpOpts...), + httpClient: httpClient, + } +} + +// Scan scans the image +func (s Service) Scan(ctx context.Context, target, artifactKey string, blobKeys []string, opts types.ScanOptions) (types.ScanResponse, error) { + ctx = WithCustomHeaders(ctx, s.customHeaders) + + // Fetch server version info in background + var serverInfo types.VersionInfo + var wg sync.WaitGroup + wg.Go(func() { + info, err := s.serverVersion(ctx) + if err != nil { + log.Warn("Failed to fetch server version", log.Err(err)) + return + } + serverInfo = info + }) + + // Convert to the rpc struct + licenseCategories := make(map[string]*rpc.Licenses) + for category, names := range opts.LicenseCategories { + licenseCategories[string(category)] = &rpc.Licenses{Names: names} + } + + var distro *common.OS + if !lo.IsEmpty(opts.Distro) { + distro = &common.OS{ + Family: string(opts.Distro.Family), + Name: opts.Distro.Name, + } + } + + var res *rpc.ScanResponse + err := r.Retry(func() error { + var err error + res, err = s.client.Scan(ctx, &rpc.ScanRequest{ + Target: target, + ArtifactId: artifactKey, + BlobIds: blobKeys, + Options: &rpc.ScanOptions{ + PkgTypes: opts.PkgTypes, + PkgRelationships: xstrings.ToStringSlice(opts.PkgRelationships), + Scanners: xstrings.ToStringSlice(opts.Scanners), + LicenseCategories: licenseCategories, + LicenseFull: opts.LicenseFull, + IncludeDevDeps: opts.IncludeDevDeps, + Distro: distro, + VulnSeveritySources: xstrings.ToStringSlice(opts.VulnSeveritySources), + }, + }) + return err + }) + if err != nil { + return types.ScanResponse{}, xerrors.Errorf("failed to detect vulnerabilities via RPC: %w", err) + } + + // Wait for server version fetch to complete + wg.Wait() + + return types.ScanResponse{ + Results: r.ConvertFromRPCResults(res.Results), + OS: r.ConvertFromRPCOS(res.Os), + Layers: slices.ZeroToNil(lo.FilterMap(res.Layers, func(layer *common.Layer, _ int) (ftypes.Layer, bool) { + if layer == nil { + return ftypes.Layer{}, false + } + return r.ConvertFromRPCLayer(layer), true + })), + ServerInfo: serverInfo, + }, nil +} + +// serverVersion fetches version information from the Trivy server. +// TODO: Consider migrating to RPC in the future for consistency with other server communication. +func (s Service) serverVersion(ctx context.Context) (types.VersionInfo, error) { + baseURL, err := url.Parse(s.remoteURL) + if err != nil { + return types.VersionInfo{}, xerrors.Errorf("failed to parse remote URL: %w", err) + } + versionURL := baseURL.ResolveReference(&url.URL{Path: "version"}) + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, versionURL.String(), http.NoBody) + if err != nil { + return types.VersionInfo{}, xerrors.Errorf("failed to create request: %w", err) + } + + // Add custom headers + for key, values := range s.customHeaders { + for _, value := range values { + req.Header.Add(key, value) + } + } + + resp, err := s.httpClient.Do(req) + if err != nil { + return types.VersionInfo{}, xerrors.Errorf("failed to fetch server version: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return types.VersionInfo{}, xerrors.Errorf("server returned status %d", resp.StatusCode) + } + + var versionInfo types.VersionInfo + if err := json.NewDecoder(resp.Body).Decode(&versionInfo); err != nil { + return types.VersionInfo{}, xerrors.Errorf("failed to decode version info: %w", err) + } + + return versionInfo, nil +} diff --git a/pkg/rpc/client/client_test.go b/pkg/rpc/client/client_test.go new file mode 100644 index 000000000000..f70b4488dbb2 --- /dev/null +++ b/pkg/rpc/client/client_test.go @@ -0,0 +1,356 @@ +package client_test + +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/aquasecurity/trivy-db/pkg/metadata" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/utils" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/rpc/client" + "github.com/aquasecurity/trivy/pkg/types" + xhttp "github.com/aquasecurity/trivy/pkg/x/http" + "github.com/aquasecurity/trivy/rpc/common" + rpc "github.com/aquasecurity/trivy/rpc/scanner" +) + +func TestScanner_Scan(t *testing.T) { + type args struct { + target string + imageID string + layerIDs []string + options types.ScanOptions + } + + versionInfo := types.VersionInfo{ + Version: "0.50.0", + VulnerabilityDB: &metadata.Metadata{ + Version: 2, + UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + NextUpdate: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC), + DownloadedAt: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + + tests := []struct { + name string + customHeaders http.Header + args args + expectation *rpc.ScanResponse + versionInfo types.VersionInfo + want types.ScanResponse + wantEosl bool + wantErr string + }{ + { + name: "happy path", + customHeaders: http.Header{ + "Trivy-Token": []string{"foo"}, + }, + args: args{ + target: "alpine:3.11", + imageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{"os"}, + }, + }, + expectation: &rpc.ScanResponse{ + Os: &common.OS{ + Family: "alpine", + Name: "3.11", + Eosl: true, + }, + Results: []*rpc.Result{ + { + Target: "alpine:3.11", + Vulnerabilities: []*common.Vulnerability{ + { + VulnerabilityId: "CVE-2020-0001", + PkgName: "musl", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "DoS", + Description: "Denial os Service", + Severity: common.Severity_CRITICAL, + References: []string{"http://example.com"}, + SeveritySource: "nvd", + VendorSeverity: map[string]common.Severity{ + string(vulnerability.NVD): common.Severity_MEDIUM, + string(vulnerability.RedHat): common.Severity_MEDIUM, + }, + Cvss: map[string]*common.CVSS{ + "nvd": { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 7.2, + V3Score: 7.8, + }, + "redhat": { + V2Vector: "AV:H/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 4.2, + V3Score: 2.8, + }, + }, + CweIds: []string{"CWE-78"}, + Layer: &common.Layer{ + DiffId: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + LastModifiedDate: ×tamppb.Timestamp{ + Seconds: 1577840460, + }, + PublishedDate: ×tamppb.Timestamp{ + Seconds: 978310860, + }, + }, + }, + }, + }, + }, + versionInfo: versionInfo, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "alpine:3.11", + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-0001", + PkgName: "musl", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Title: "DoS", + Description: "Denial os Service", + Severity: "CRITICAL", + References: []string{"http://example.com"}, + VendorSeverity: dbTypes.VendorSeverity{ + vulnerability.NVD: dbTypes.SeverityMedium, + vulnerability.RedHat: dbTypes.SeverityMedium, + }, + CVSS: dbTypes.VendorCVSS{ + "nvd": { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 7.2, + V3Score: 7.8, + }, + "redhat": { + V2Vector: "AV:H/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 4.2, + V3Score: 2.8, + }, + }, + CweIDs: []string{"CWE-78"}, + LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"), + PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"), + Custom: nil, + }, + SeveritySource: "nvd", + Layer: ftypes.Layer{ + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + Custom: nil, + }, + }, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: true, + }, + ServerInfo: versionInfo, + }, + }, + { + name: "sad path: Scan returns an error", + customHeaders: http.Header{ + "Trivy-Token": []string{"foo"}, + }, + args: args{ + target: "alpine:3.11", + imageID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{"os"}, + }, + }, + wantErr: "failed to detect vulnerabilities via RPC", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Handle /version endpoint + if strings.HasSuffix(r.URL.Path, "/version") { + w.Header().Set("Content-Type", "application/json") + if lo.IsEmpty(tt.versionInfo) { + w.WriteHeader(http.StatusNotFound) + } else { + _ = json.NewEncoder(w).Encode(tt.versionInfo) + } + return + } + + // Handle RPC scan endpoint + if tt.expectation == nil { + e := map[string]any{ + "code": "not_found", + "msg": "expectation is empty", + } + b, _ := json.Marshal(e) + w.WriteHeader(http.StatusBadGateway) + _, _ = w.Write(b) + return + } + b, err := protojson.Marshal(tt.expectation) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + fmt.Fprintf(w, "json marshaling error: %v", err) + return + } + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write(b) + })) + defer ts.Close() + rpcClient := rpc.NewScannerJSONClient(ts.URL, ts.Client()) + + s := client.NewTestService(ts.URL, tt.customHeaders, rpcClient, ts.Client()) + + gotResponse, err := s.Scan(t.Context(), tt.args.target, tt.args.imageID, tt.args.layerIDs, tt.args.options) + + if tt.wantErr != "" { + require.Error(t, err, tt.name) + require.Contains(t, err.Error(), tt.wantErr, tt.name) + return + } + + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, gotResponse) + }) + } +} + +func TestScanner_ScanServerInsecure(t *testing.T) { + ts := httptest.NewTLSServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {})) + defer ts.Close() + + tests := []struct { + name string + insecure bool + wantErr string + }{ + { + name: "happy path", + insecure: true, + }, + { + name: "sad path", + insecure: false, + wantErr: "failed to do request", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + httpClient := &http.Client{ + Transport: xhttp.NewTransport(xhttp.Options{Insecure: tt.insecure}).Build(), + } + rpcClient := rpc.NewScannerProtobufClient(ts.URL, httpClient) + s := client.NewTestService(ts.URL, nil, rpcClient, httpClient) + _, err := s.Scan(t.Context(), "dummy", "", nil, types.ScanOptions{}) + + if tt.wantErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.wantErr) + return + } + require.NoError(t, err) + }) + } +} + +func TestService_ServerVersion(t *testing.T) { + versionInfo := types.VersionInfo{ + Version: "0.50.0", + VulnerabilityDB: &metadata.Metadata{ + Version: 2, + UpdatedAt: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC), + NextUpdate: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC), + DownloadedAt: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), + }, + } + + tests := []struct { + name string + customHeaders http.Header + serverHandler func(w http.ResponseWriter, r *http.Request) + want types.VersionInfo + }{ + { + name: "happy path", + customHeaders: http.Header{ + "Authorization": []string{"Bearer token"}, + }, + serverHandler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "Bearer token", r.Header.Get("Authorization")) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(versionInfo) + }, + want: versionInfo, + }, + { + name: "server returns 404", + serverHandler: func(w http.ResponseWriter, _ *http.Request) { + w.WriteHeader(http.StatusNotFound) + }, + }, + { + name: "server returns invalid JSON", + serverHandler: func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte("invalid json")) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if strings.HasSuffix(r.URL.Path, "/version") { + tt.serverHandler(w, r) + return + } + // Handle RPC endpoint with empty response for scan + w.Header().Set("Content-Type", "application/json") + b, _ := protojson.Marshal(&rpc.ScanResponse{}) + _, _ = w.Write(b) + })) + defer ts.Close() + + rpcClient := rpc.NewScannerJSONClient(ts.URL, ts.Client()) + s := client.NewTestService(ts.URL, tt.customHeaders, rpcClient, ts.Client()) + + // Call Scan which internally calls serverVersion + resp, err := s.Scan(t.Context(), "test", "", nil, types.ScanOptions{}) + + // When server version fetch fails, it's logged but not returned as an error + require.NoError(t, err) + assert.Equal(t, tt.want, resp.ServerInfo) + }) + } +} diff --git a/pkg/rpc/client/export_test.go b/pkg/rpc/client/export_test.go new file mode 100644 index 000000000000..7fb007ccf461 --- /dev/null +++ b/pkg/rpc/client/export_test.go @@ -0,0 +1,17 @@ +package client + +import ( + "net/http" + + rpc "github.com/aquasecurity/trivy/rpc/scanner" +) + +// NewTestService creates a Service for testing with injected dependencies. +func NewTestService(remoteURL string, customHeaders http.Header, rpcClient rpc.Scanner, httpClient *http.Client) Service { + return Service{ + remoteURL: remoteURL, + customHeaders: customHeaders, + client: rpcClient, + httpClient: httpClient, + } +} diff --git a/pkg/rpc/client/headers.go b/pkg/rpc/client/headers.go new file mode 100644 index 000000000000..0ebac7bf046e --- /dev/null +++ b/pkg/rpc/client/headers.go @@ -0,0 +1,21 @@ +package client + +import ( + "context" + "net/http" + + "github.com/twitchtv/twirp" + + "github.com/aquasecurity/trivy/pkg/log" +) + +// WithCustomHeaders adds custom headers to request headers +func WithCustomHeaders(ctx context.Context, customHeaders http.Header) context.Context { + // Attach the headers to a context + ctxWithToken, err := twirp.WithHTTPRequestHeaders(ctx, customHeaders) + if err != nil { + log.Warn("twirp error setting headers", log.Err(err)) + return ctx + } + return ctxWithToken +} diff --git a/pkg/rpc/client/headers_test.go b/pkg/rpc/client/headers_test.go new file mode 100644 index 000000000000..b61c3cfd722c --- /dev/null +++ b/pkg/rpc/client/headers_test.go @@ -0,0 +1,50 @@ +package client + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/twitchtv/twirp" +) + +func TestWithCustomHeaders(t *testing.T) { + type args struct { + ctx context.Context + customHeaders http.Header + } + tests := []struct { + name string + args args + want http.Header + }{ + { + name: "happy path", + args: args{ + ctx: t.Context(), + customHeaders: http.Header{ + "Trivy-Token": []string{"token"}, + }, + }, + want: http.Header{ + "Trivy-Token": []string{"token"}, + }, + }, + { + name: "sad path, invalid headers passed in", + args: args{ + ctx: t.Context(), + customHeaders: http.Header{ + "Content-Type": []string{"token"}, + }, + }, + want: http.Header(nil), + }, + } + for _, tt := range tests { + gotCtx := WithCustomHeaders(tt.args.ctx, tt.args.customHeaders) + header, _ := twirp.HTTPRequestHeaders(gotCtx) + assert.Equal(t, tt.want, header, tt.name) + } +} diff --git a/pkg/rpc/convert.go b/pkg/rpc/convert.go new file mode 100644 index 000000000000..4b644ead7247 --- /dev/null +++ b/pkg/rpc/convert.go @@ -0,0 +1,1108 @@ +package rpc + +import ( + jsonv2 "encoding/json/v2" + "strings" + "time" + + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/digest" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" + "github.com/aquasecurity/trivy/rpc/cache" + "github.com/aquasecurity/trivy/rpc/common" + "github.com/aquasecurity/trivy/rpc/scanner" +) + +var LicenseCategoryMap = map[common.LicenseCategory_Enum]ftypes.LicenseCategory{ + common.LicenseCategory_UNSPECIFIED: "", + common.LicenseCategory_FORBIDDEN: ftypes.CategoryForbidden, + common.LicenseCategory_RESTRICTED: ftypes.CategoryRestricted, + common.LicenseCategory_RECIPROCAL: ftypes.CategoryReciprocal, + common.LicenseCategory_NOTICE: ftypes.CategoryNotice, + common.LicenseCategory_PERMISSIVE: ftypes.CategoryPermissive, + common.LicenseCategory_UNENCUMBERED: ftypes.CategoryUnencumbered, + common.LicenseCategory_UNKNOWN: ftypes.CategoryUnknown, +} + +var LicenseTypeMap = map[common.LicenseType_Enum]ftypes.LicenseType{ + common.LicenseType_UNSPECIFIED: "", + common.LicenseType_DPKG: ftypes.LicenseTypeDpkg, + common.LicenseType_HEADER: ftypes.LicenseTypeHeader, + common.LicenseType_LICENSE_FILE: ftypes.LicenseTypeFile, +} + +// ByValueOr returns the key from the map of the first matched value, +// or default key if the value is not present. +func ByValueOr[K, V comparable](m map[K]V, val V, d K) K { + for k, v := range m { + if v == val { + return k + } + } + return d +} + +// ConvertToRPCPkgs returns the list of RPC package objects +func ConvertToRPCPkgs(pkgs []ftypes.Package) []*common.Package { + var rpcPkgs []*common.Package + for _, pkg := range pkgs { + rpcPkgs = append(rpcPkgs, &common.Package{ + Id: pkg.ID, + Name: pkg.Name, + Version: pkg.Version, + Release: pkg.Release, + Epoch: int32(pkg.Epoch), + Arch: pkg.Arch, + Identifier: ConvertToRPCPkgIdentifier(pkg.Identifier), + Dev: pkg.Dev, + SrcName: pkg.SrcName, + SrcVersion: pkg.SrcVersion, + SrcRelease: pkg.SrcRelease, + SrcEpoch: int32(pkg.SrcEpoch), + Licenses: pkg.Licenses, + Locations: ConvertToRPCLocations(pkg.Locations), + Layer: ConvertToRPCLayer(pkg.Layer), + FilePath: pkg.FilePath, + DependsOn: pkg.DependsOn, + Digest: pkg.Digest.String(), + Relationship: int32(pkg.Relationship), + Indirect: pkg.Indirect, + Maintainer: pkg.Maintainer, + AnalyzedBy: string(pkg.AnalyzedBy), + }) + } + return rpcPkgs +} + +func ConvertToRPCPkgIdentifier(pkg ftypes.PkgIdentifier) *common.PkgIdentifier { + if pkg.Empty() { + return nil + } + + var p string + if pkg.PURL != nil { + p = pkg.PURL.String() + } + return &common.PkgIdentifier{ + Uid: pkg.UID, + Purl: p, + BomRef: pkg.BOMRef, + } +} + +func ConvertToRPCLocations(pkgLocs []ftypes.Location) []*common.Location { + var locations []*common.Location + for _, pkgLoc := range pkgLocs { + locations = append(locations, &common.Location{ + StartLine: int32(pkgLoc.StartLine), + EndLine: int32(pkgLoc.EndLine), + }) + } + return locations +} + +func ConvertToRPCCustomResources(resources []ftypes.CustomResource) []*common.CustomResource { + var rpcResources []*common.CustomResource + for _, r := range resources { + data, err := structpb.NewValue(r.Data) + if err != nil { + log.Warn("Custom resource conversion error", log.Err(err)) + } + rpcResources = append(rpcResources, &common.CustomResource{ + Type: r.Type, + FilePath: r.FilePath, + Layer: &common.Layer{ + Digest: r.Layer.Digest, + DiffId: r.Layer.DiffID, + }, + Data: data, + }) + } + return rpcResources +} + +func ConvertToRPCCode(code ftypes.Code) *common.Code { + var rpcLines []*common.Line + for _, line := range code.Lines { + rpcLines = append(rpcLines, &common.Line{ + Number: int32(line.Number), + Content: line.Content, + IsCause: line.IsCause, + Annotation: line.Annotation, + Truncated: line.Truncated, + Highlighted: line.Highlighted, + FirstCause: line.FirstCause, + LastCause: line.LastCause, + }) + } + return &common.Code{ + Lines: rpcLines, + } +} + +func ConvertToRPCSecrets(secrets []ftypes.Secret) []*common.Secret { + var rpcSecrets []*common.Secret + for _, s := range secrets { + rpcSecrets = append(rpcSecrets, ConvertToRPCSecret(&s)) + } + return rpcSecrets +} + +func ConvertToRPCSecretFindings(findings []ftypes.SecretFinding) []*common.SecretFinding { + var rpcFindings []*common.SecretFinding + for _, f := range findings { + rpcFindings = append(rpcFindings, &common.SecretFinding{ + RuleId: f.RuleID, + Category: string(f.Category), + Severity: f.Severity, + Title: f.Title, + EndLine: int32(f.EndLine), + StartLine: int32(f.StartLine), + Code: ConvertToRPCCode(f.Code), + Match: f.Match, + Layer: ConvertToRPCLayer(f.Layer), + Offset: int32(f.Offset), + }) + } + return rpcFindings +} + +func ConvertToRPCLicenseFiles(licenses []ftypes.LicenseFile) []*common.LicenseFile { + var rpcLicenses []*common.LicenseFile + + for _, lic := range licenses { + rpcLicenses = append(rpcLicenses, &common.LicenseFile{ + LicenseType: ConvertToRPCLicenseType(lic.Type), + FilePath: lic.FilePath, + PkgName: lic.PkgName, + Fingings: ConvertToRPCLicenseFindings(lic.Findings), + Layer: ConvertToRPCLayer(lic.Layer), + }) + } + + return rpcLicenses +} + +func ConvertToRPCLicenseFindings(findings ftypes.LicenseFindings) []*common.LicenseFinding { + var rpcFindings []*common.LicenseFinding + + for _, f := range findings { + rpcFindings = append(rpcFindings, &common.LicenseFinding{ + Category: ConvertToRPCLicenseCategory(f.Category), + Name: f.Name, + Confidence: float32(f.Confidence), + Link: f.Link, + }) + } + + return rpcFindings +} + +// ConvertFromRPCPkgs returns list of Fanal package objects +func ConvertFromRPCPkgs(rpcPkgs []*common.Package) []ftypes.Package { + var pkgs []ftypes.Package + for _, pkg := range rpcPkgs { + pkgs = append(pkgs, ftypes.Package{ + ID: pkg.Id, + Name: pkg.Name, + Version: pkg.Version, + Release: pkg.Release, + Epoch: int(pkg.Epoch), + Arch: pkg.Arch, + Identifier: ConvertFromRPCPkgIdentifier(pkg.Identifier), + Dev: pkg.Dev, + SrcName: pkg.SrcName, + SrcVersion: pkg.SrcVersion, + SrcRelease: pkg.SrcRelease, + SrcEpoch: int(pkg.SrcEpoch), + Licenses: pkg.Licenses, + Locations: ConvertFromRPCLocation(pkg.Locations), + Layer: ConvertFromRPCLayer(pkg.Layer), + FilePath: pkg.FilePath, + DependsOn: pkg.DependsOn, + Digest: digest.Digest(pkg.Digest), + Relationship: ftypes.Relationship(pkg.Relationship), + Indirect: pkg.Indirect, + Maintainer: pkg.Maintainer, + AnalyzedBy: ftypes.AnalyzerType(pkg.AnalyzedBy), + }) + } + return pkgs +} + +func ConvertFromRPCPkgIdentifier(pkg *common.PkgIdentifier) ftypes.PkgIdentifier { + if pkg == nil { + return ftypes.PkgIdentifier{} + } + + pkgID := ftypes.PkgIdentifier{ + UID: pkg.GetUid(), + BOMRef: pkg.GetBomRef(), + } + + if pkg.Purl != "" { + pu, err := packageurl.FromString(pkg.Purl) + if err != nil { + log.Error("Failed to parse PURL", log.String("purl", pkg.Purl), log.Err(err)) + } + pkgID.PURL = &pu + } + + return pkgID +} + +func ConvertFromRPCLocation(locs []*common.Location) []ftypes.Location { + var pkgLocs []ftypes.Location + for _, loc := range locs { + pkgLocs = append(pkgLocs, ftypes.Location{ + StartLine: int(loc.StartLine), + EndLine: int(loc.EndLine), + }) + } + return pkgLocs +} + +// ConvertToRPCVulns returns common.Vulnerability +func ConvertToRPCVulns(vulns []types.DetectedVulnerability) []*common.Vulnerability { + var rpcVulns []*common.Vulnerability + for _, vuln := range vulns { + severity, err := dbTypes.NewSeverity(vuln.Severity) + if err != nil { + log.Warn("Severity error", log.Err(err)) + } + cvssMap := make(map[string]*common.CVSS) // This is needed because protobuf generates a map[string]*CVSS type + for vendor, vendorSeverity := range vuln.CVSS { + cvssMap[string(vendor)] = &common.CVSS{ + V2Vector: vendorSeverity.V2Vector, + V3Vector: vendorSeverity.V3Vector, + V40Vector: vendorSeverity.V40Vector, + V2Score: vendorSeverity.V2Score, + V3Score: vendorSeverity.V3Score, + V40Score: vendorSeverity.V40Score, + } + } + vendorSeverityMap := make(map[string]common.Severity) + for vendor, vendorSeverity := range vuln.VendorSeverity { + vendorSeverityMap[string(vendor)] = common.Severity(vendorSeverity) + } + + var lastModifiedDate, publishedDate *timestamppb.Timestamp + if vuln.LastModifiedDate != nil { + lastModifiedDate = timestamppb.New(*vuln.LastModifiedDate) // nolint: errcheck + } + + if vuln.PublishedDate != nil { + publishedDate = timestamppb.New(*vuln.PublishedDate) // nolint: errcheck + } + + var customAdvisoryData, customVulnData *structpb.Value + var builder strings.Builder + if vuln.Custom != nil { + builder.Reset() + _ = jsonv2.MarshalWrite(&builder, vuln.Custom) // nolint: errcheck + customAdvisoryData = structpb.NewStringValue(builder.String()) + } + if vuln.Vulnerability.Custom != nil { + builder.Reset() + _ = jsonv2.MarshalWrite(&builder, vuln.Vulnerability.Custom) // nolint: errcheck + customVulnData = structpb.NewStringValue(builder.String()) + } + + rpcVulns = append(rpcVulns, &common.Vulnerability{ + VulnerabilityId: vuln.VulnerabilityID, + VendorIds: vuln.VendorIDs, + PkgId: vuln.PkgID, + PkgName: vuln.PkgName, + PkgPath: vuln.PkgPath, + InstalledVersion: vuln.InstalledVersion, + FixedVersion: vuln.FixedVersion, + PkgIdentifier: ConvertToRPCPkgIdentifier(vuln.PkgIdentifier), + Status: int32(vuln.Status), + Title: vuln.Title, + Description: vuln.Description, + Severity: common.Severity(severity), + VendorSeverity: vendorSeverityMap, + References: vuln.References, + Layer: ConvertToRPCLayer(vuln.Layer), + Cvss: cvssMap, + SeveritySource: string(vuln.SeveritySource), + CweIds: vuln.CweIDs, + PrimaryUrl: vuln.PrimaryURL, + LastModifiedDate: lastModifiedDate, + PublishedDate: publishedDate, + CustomAdvisoryData: customAdvisoryData, + CustomVulnData: customVulnData, + DataSource: ConvertToRPCDataSource(vuln.DataSource), + }) + } + return rpcVulns +} + +// ConvertToRPCMisconfs returns common.DetectedMisconfigurations +func ConvertToRPCMisconfs(misconfs []types.DetectedMisconfiguration) []*common.DetectedMisconfiguration { + var rpcMisconfs []*common.DetectedMisconfiguration + for _, m := range misconfs { + severity, err := dbTypes.NewSeverity(m.Severity) + if err != nil { + log.Warn("Severity conversion error", log.Err(err)) + } + + rpcMisconfs = append(rpcMisconfs, &common.DetectedMisconfiguration{ + Type: m.Type, + Id: m.ID, + AvdId: m.AVDID, + Aliases: m.Aliases, + Title: m.Title, + Description: m.Description, + Message: m.Message, + Namespace: m.Namespace, + Query: m.Query, + Resolution: m.Resolution, + Severity: common.Severity(severity), + PrimaryUrl: m.PrimaryURL, + References: m.References, + Status: string(m.Status), + Layer: ConvertToRPCLayer(m.Layer), + CauseMetadata: ConvertToRPCCauseMetadata(m.CauseMetadata), + }) + } + return rpcMisconfs +} + +// ConvertToRPCLayer returns common.Layer +func ConvertToRPCLayer(layer ftypes.Layer) *common.Layer { + return &common.Layer{ + Size: layer.Size, + Digest: layer.Digest, + DiffId: layer.DiffID, + CreatedBy: layer.CreatedBy, + } +} + +func ConvertToRPCPolicyMetadata(policy ftypes.PolicyMetadata) *common.PolicyMetadata { + return &common.PolicyMetadata{ + Id: policy.ID, + AdvId: policy.AVDID, + Type: policy.Type, + Title: policy.Title, + Description: policy.Description, + Severity: policy.Severity, + RecommendedActions: policy.RecommendedActions, + References: policy.References, + } +} + +func ConvertToRPCCauseMetadata(cause ftypes.CauseMetadata) *common.CauseMetadata { + return &common.CauseMetadata{ + Resource: cause.Resource, + Provider: cause.Provider, + Service: cause.Service, + StartLine: int32(cause.StartLine), + EndLine: int32(cause.EndLine), + Code: ConvertToRPCCode(cause.Code), + RenderedCause: &common.RenderedCause{ + Raw: cause.RenderedCause.Raw, + Highlighted: cause.RenderedCause.Highlighted, + }, + } +} + +// ConvertToRPCDataSource returns common.DataSource +func ConvertToRPCDataSource(ds *dbTypes.DataSource) *common.DataSource { + if ds == nil { + return nil + } + return &common.DataSource{ + Id: string(ds.ID), + Name: ds.Name, + Url: ds.URL, + } +} + +// ConvertFromRPCResults converts scanner.Result to types.Result +func ConvertFromRPCResults(rpcResults []*scanner.Result) []types.Result { + var results []types.Result + for _, result := range rpcResults { + results = append(results, types.Result{ + Target: result.Target, + Vulnerabilities: ConvertFromRPCVulns(result.Vulnerabilities), + Misconfigurations: ConvertFromRPCMisconfs(result.Misconfigurations), + Class: types.ResultClass(result.Class), + Type: ftypes.TargetType(result.Type), + Packages: ConvertFromRPCPkgs(result.Packages), + CustomResources: ConvertFromRPCCustomResources(result.CustomResources), + Secrets: ConvertFromRPCDetectedSecrets(result.Secrets), + Licenses: ConvertFromRPCDetectedLicenses(result.Licenses), + }) + } + return results +} + +func ConvertFromRPCDetectedLicenses(rpcLicenses []*common.DetectedLicense) []types.DetectedLicense { + var licenses []types.DetectedLicense + for _, l := range rpcLicenses { + severity := dbTypes.Severity(l.Severity) + licenses = append(licenses, types.DetectedLicense{ + Severity: severity.String(), + Category: ConvertFromRPCLicenseCategory(l.Category), + PkgName: l.PkgName, + FilePath: l.FilePath, + Name: l.Name, + Text: l.Text, + Confidence: float64(l.Confidence), + Link: l.Link, + }) + } + return licenses +} + +func ConvertFromRPCLicenseCategory(rpcCategory common.LicenseCategory_Enum) ftypes.LicenseCategory { + return lo.ValueOr(LicenseCategoryMap, rpcCategory, "") +} + +func ConvertFromRPCLicenseType(rpcLicenseType common.LicenseType_Enum) ftypes.LicenseType { + return lo.ValueOr(LicenseTypeMap, rpcLicenseType, "") +} + +// ConvertFromRPCCustomResources converts array of cache.CustomResource to fanal.CustomResource +func ConvertFromRPCCustomResources(rpcCustomResources []*common.CustomResource) []ftypes.CustomResource { + var resources []ftypes.CustomResource + for _, res := range rpcCustomResources { + resources = append(resources, ftypes.CustomResource{ + Type: res.Type, + FilePath: res.FilePath, + Layer: ftypes.Layer{ + Digest: res.Layer.Digest, + DiffID: res.Layer.DiffId, + }, + Data: res.Data, + }) + } + return resources +} + +func ConvertFromRPCCode(rpcCode *common.Code) ftypes.Code { + var lines []ftypes.Line + for _, line := range rpcCode.Lines { + lines = append(lines, ftypes.Line{ + Number: int(line.Number), + Content: line.Content, + IsCause: line.IsCause, + Annotation: line.Annotation, + Truncated: line.Truncated, + Highlighted: line.Highlighted, + FirstCause: line.FirstCause, + LastCause: line.LastCause, + }) + } + return ftypes.Code{ + Lines: lines, + } +} + +func ConvertFromRPCDetectedSecrets(rpcFindings []*common.SecretFinding) []types.DetectedSecret { + if len(rpcFindings) == 0 { + return nil + } + return xslices.Map(ConvertFromRPCSecretFindings(rpcFindings), func(s ftypes.SecretFinding) types.DetectedSecret { + return types.DetectedSecret(s) + }) +} + +func ConvertFromRPCSecretFindings(rpcFindings []*common.SecretFinding) []ftypes.SecretFinding { + var findings []ftypes.SecretFinding + for _, finding := range rpcFindings { + findings = append(findings, ftypes.SecretFinding{ + RuleID: finding.RuleId, + Category: ftypes.SecretRuleCategory(finding.Category), + Severity: finding.Severity, + Title: finding.Title, + StartLine: int(finding.StartLine), + EndLine: int(finding.EndLine), + Code: ConvertFromRPCCode(finding.Code), + Match: finding.Match, + Offset: int(finding.Offset), + Layer: ftypes.Layer{ + Digest: finding.Layer.Digest, + DiffID: finding.Layer.DiffId, + CreatedBy: finding.Layer.CreatedBy, + }, + }) + } + return findings +} + +func ConvertFromRPCSecrets(recSecrets []*common.Secret) []ftypes.Secret { + var secrets []ftypes.Secret + for _, recSecret := range recSecrets { + secrets = append(secrets, *ConvertFromRPCSecret(recSecret)) + } + return secrets +} + +func ConvertFromRPCLicenseFiles(rpcLicenses []*common.LicenseFile) []ftypes.LicenseFile { + var licenses []ftypes.LicenseFile + + for _, lic := range rpcLicenses { + licenses = append(licenses, ftypes.LicenseFile{ + Type: ConvertFromRPCLicenseType(lic.LicenseType), + FilePath: lic.FilePath, + PkgName: lic.PkgName, + Findings: ConvertFromRPCLicenseFindings(lic.Fingings), + Layer: ConvertFromRPCLayer(lic.Layer), + }) + } + + return licenses +} + +func ConvertFromRPCLicenseFindings(rpcFindings []*common.LicenseFinding) ftypes.LicenseFindings { + var findings ftypes.LicenseFindings + + for _, finding := range rpcFindings { + findings = append(findings, ftypes.LicenseFinding{ + Category: ConvertFromRPCLicenseCategory(finding.Category), + Name: finding.Name, + Confidence: float64(finding.Confidence), + Link: finding.Link, + }) + } + + return findings +} + +// ConvertFromRPCVulns converts []*common.Vulnerability to []types.DetectedVulnerability +func ConvertFromRPCVulns(rpcVulns []*common.Vulnerability) []types.DetectedVulnerability { + var vulns []types.DetectedVulnerability + for _, vuln := range rpcVulns { + severity := dbTypes.Severity(vuln.Severity) + cvssMap := make(dbTypes.VendorCVSS) // This is needed because protobuf generates a map[string]*CVSS type + for vendor, vendorSeverity := range vuln.Cvss { + cvssMap[dbTypes.SourceID(vendor)] = dbTypes.CVSS{ + V2Vector: vendorSeverity.V2Vector, + V3Vector: vendorSeverity.V3Vector, + V40Vector: vendorSeverity.V40Vector, + V2Score: vendorSeverity.V2Score, + V3Score: vendorSeverity.V3Score, + V40Score: vendorSeverity.V40Score, + } + } + vendorSeverityMap := make(dbTypes.VendorSeverity) + for vendor, vendorSeverity := range vuln.VendorSeverity { + vendorSeverityMap[dbTypes.SourceID(vendor)] = dbTypes.Severity(vendorSeverity) + } + + var lastModifiedDate, publishedDate *time.Time + if vuln.LastModifiedDate != nil { + lastModifiedDate = lo.ToPtr(vuln.LastModifiedDate.AsTime()) + } + if vuln.PublishedDate != nil { + publishedDate = lo.ToPtr(vuln.PublishedDate.AsTime()) + } + + // Handle custom data conversion from protobuf.Value + var customVulnData, customAdvisoryData any + if vuln.CustomVulnData != nil { + customVulnData = vuln.CustomVulnData.AsInterface() + } + if vuln.CustomAdvisoryData != nil { + customAdvisoryData = vuln.CustomAdvisoryData.AsInterface() + } + + vulns = append(vulns, types.DetectedVulnerability{ + VulnerabilityID: vuln.VulnerabilityId, + VendorIDs: vuln.VendorIds, + PkgID: vuln.PkgId, + PkgName: vuln.PkgName, + PkgPath: vuln.PkgPath, + InstalledVersion: vuln.InstalledVersion, + FixedVersion: vuln.FixedVersion, + PkgIdentifier: ConvertFromRPCPkgIdentifier(vuln.PkgIdentifier), + Status: dbTypes.Status(vuln.Status), + Vulnerability: dbTypes.Vulnerability{ + Title: vuln.Title, + Description: vuln.Description, + Severity: severity.String(), + CVSS: cvssMap, + References: vuln.References, + CweIDs: vuln.CweIds, + LastModifiedDate: lastModifiedDate, + PublishedDate: publishedDate, + Custom: customVulnData, + VendorSeverity: vendorSeverityMap, + }, + Layer: ConvertFromRPCLayer(vuln.Layer), + SeveritySource: dbTypes.SourceID(vuln.SeveritySource), + PrimaryURL: vuln.PrimaryUrl, + Custom: customAdvisoryData, + DataSource: ConvertFromRPCDataSource(vuln.DataSource), + }) + } + return vulns +} + +// ConvertFromRPCMisconfs converts []*common.DetectedMisconfigurations to []types.DetectedMisconfiguration +func ConvertFromRPCMisconfs(rpcMisconfs []*common.DetectedMisconfiguration) []types.DetectedMisconfiguration { + var misconfs []types.DetectedMisconfiguration + for _, rpcMisconf := range rpcMisconfs { + misconfs = append(misconfs, types.DetectedMisconfiguration{ + Type: rpcMisconf.Type, + ID: rpcMisconf.Id, + AVDID: rpcMisconf.AvdId, + Aliases: rpcMisconf.Aliases, + Title: rpcMisconf.Title, + Description: rpcMisconf.Description, + Message: rpcMisconf.Message, + Namespace: rpcMisconf.Namespace, + Query: rpcMisconf.Query, + Resolution: rpcMisconf.Resolution, + Severity: rpcMisconf.Severity.String(), + PrimaryURL: rpcMisconf.PrimaryUrl, + References: rpcMisconf.References, + Status: types.MisconfStatus(rpcMisconf.Status), + Layer: ConvertFromRPCLayer(rpcMisconf.Layer), + CauseMetadata: ConvertFromRPCCauseMetadata(rpcMisconf.CauseMetadata), + }) + } + return misconfs +} + +// ConvertFromRPCLayer converts *common.Layer to ftypes.Layer +func ConvertFromRPCLayer(rpcLayer *common.Layer) ftypes.Layer { + if rpcLayer == nil { + return ftypes.Layer{} + } + return ftypes.Layer{ + Size: rpcLayer.Size, + Digest: rpcLayer.Digest, + DiffID: rpcLayer.DiffId, + CreatedBy: rpcLayer.CreatedBy, + } +} + +func ConvertFromRPCPolicyMetadata(rpcPolicy *common.PolicyMetadata) ftypes.PolicyMetadata { + if rpcPolicy == nil { + return ftypes.PolicyMetadata{} + } + + return ftypes.PolicyMetadata{ + ID: rpcPolicy.Id, + AVDID: rpcPolicy.AdvId, + Type: rpcPolicy.Type, + Title: rpcPolicy.Title, + Description: rpcPolicy.Description, + Severity: rpcPolicy.Severity, + RecommendedActions: rpcPolicy.RecommendedActions, + References: rpcPolicy.References, + } +} + +func ConvertFromRPCCauseMetadata(rpcCause *common.CauseMetadata) ftypes.CauseMetadata { + if rpcCause == nil { + return ftypes.CauseMetadata{} + } + return ftypes.CauseMetadata{ + Resource: rpcCause.Resource, + Provider: rpcCause.Provider, + Service: rpcCause.Service, + StartLine: int(rpcCause.StartLine), + EndLine: int(rpcCause.EndLine), + Code: ConvertFromRPCCode(rpcCause.Code), + RenderedCause: ConvertFromRPCRenderedCause(rpcCause.RenderedCause), + } +} + +func ConvertFromRPCRenderedCause(rendered *common.RenderedCause) ftypes.RenderedCause { + if rendered == nil { + return ftypes.RenderedCause{} + } + return ftypes.RenderedCause{ + Raw: rendered.Raw, + Highlighted: rendered.Highlighted, + } +} + +// ConvertFromRPCOS converts common.OS to fanal.OS +func ConvertFromRPCOS(rpcOS *common.OS) ftypes.OS { + if rpcOS == nil { + return ftypes.OS{} + } + return ftypes.OS{ + Family: ftypes.OSType(rpcOS.Family), + Name: rpcOS.Name, + Eosl: rpcOS.Eosl, + Extended: rpcOS.Extended, + } +} + +// ConvertFromRPCRepository converts common.Repository to fanal.Repository +func ConvertFromRPCRepository(rpcRepo *common.Repository) *ftypes.Repository { + if rpcRepo == nil { + return nil + } + return &ftypes.Repository{ + Family: ftypes.OSType(rpcRepo.Family), + Release: rpcRepo.Release, + } +} + +// ConvertFromRPCDataSource converts *common.DataSource to *dbTypes.DataSource +func ConvertFromRPCDataSource(ds *common.DataSource) *dbTypes.DataSource { + if ds == nil { + return nil + } + return &dbTypes.DataSource{ + ID: dbTypes.SourceID(ds.Id), + Name: ds.Name, + URL: ds.Url, + } +} + +// ConvertFromRPCPackageInfos converts common.PackageInfo to fanal.PackageInfo +func ConvertFromRPCPackageInfos(rpcPkgInfos []*common.PackageInfo) []ftypes.PackageInfo { + var pkgInfos []ftypes.PackageInfo + for _, rpcPkgInfo := range rpcPkgInfos { + pkgInfos = append(pkgInfos, ftypes.PackageInfo{ + FilePath: rpcPkgInfo.FilePath, + Packages: ConvertFromRPCPkgs(rpcPkgInfo.Packages), + }) + } + return pkgInfos +} + +// ConvertFromRPCApplications converts common.Application to fanal.Application +func ConvertFromRPCApplications(rpcApps []*common.Application) []ftypes.Application { + var apps []ftypes.Application + for _, rpcApp := range rpcApps { + apps = append(apps, ftypes.Application{ + Type: ftypes.LangType(rpcApp.Type), + FilePath: rpcApp.FilePath, + Packages: ConvertFromRPCPkgs(rpcApp.Packages), + }) + } + return apps +} + +// ConvertFromRPCMisconfigurations converts common.Misconfiguration to fanal.Misconfiguration +func ConvertFromRPCMisconfigurations(rpcMisconfs []*common.Misconfiguration) []ftypes.Misconfiguration { + var misconfs []ftypes.Misconfiguration + for _, rpcMisconf := range rpcMisconfs { + misconfs = append(misconfs, ftypes.Misconfiguration{ + FileType: ftypes.ConfigType(rpcMisconf.FileType), + FilePath: rpcMisconf.FilePath, + Successes: ConvertFromRPCMisconfResults(rpcMisconf.Successes), + Warnings: ConvertFromRPCMisconfResults(rpcMisconf.Warnings), + Failures: ConvertFromRPCMisconfResults(rpcMisconf.Failures), + Layer: ftypes.Layer{}, + }) + } + return misconfs +} + +// ConvertFromRPCMisconfResults converts common.MisconfResult to fanal.MisconfResult +func ConvertFromRPCMisconfResults(rpcResults []*common.MisconfResult) []ftypes.MisconfResult { + var results []ftypes.MisconfResult + for _, r := range rpcResults { + results = append(results, ftypes.MisconfResult{ + Namespace: r.Namespace, + Message: r.Message, + PolicyMetadata: ConvertFromRPCPolicyMetadata(r.PolicyMetadata), + CauseMetadata: ConvertFromRPCCauseMetadata(r.CauseMetadata), + }) + } + return results +} + +// ConvertFromRPCPutArtifactRequest converts cache.PutArtifactRequest to fanal.PutArtifactRequest +func ConvertFromRPCPutArtifactRequest(req *cache.PutArtifactRequest) ftypes.ArtifactInfo { + return ftypes.ArtifactInfo{ + SchemaVersion: int(req.ArtifactInfo.SchemaVersion), + Architecture: req.ArtifactInfo.Architecture, + Created: req.ArtifactInfo.Created.AsTime(), + DockerVersion: req.ArtifactInfo.DockerVersion, + OS: req.ArtifactInfo.Os, + HistoryPackages: ConvertFromRPCPkgs(req.ArtifactInfo.HistoryPackages), + Secret: ConvertFromRPCSecret(req.ArtifactInfo.Secret), + } +} + +// ConvertFromRPCPutBlobRequest returns ftypes.BlobInfo +func ConvertFromRPCPutBlobRequest(req *cache.PutBlobRequest) ftypes.BlobInfo { + return ftypes.BlobInfo{ + SchemaVersion: int(req.BlobInfo.SchemaVersion), + Size: req.BlobInfo.Size, + Digest: req.BlobInfo.Digest, + DiffID: req.BlobInfo.DiffId, + OS: ConvertFromRPCOS(req.BlobInfo.Os), + Repository: ConvertFromRPCRepository(req.BlobInfo.Repository), + PackageInfos: ConvertFromRPCPackageInfos(req.BlobInfo.PackageInfos), + Applications: ConvertFromRPCApplications(req.BlobInfo.Applications), + Misconfigurations: ConvertFromRPCMisconfigurations(req.BlobInfo.Misconfigurations), + CustomResources: ConvertFromRPCCustomResources(req.BlobInfo.CustomResources), + Secrets: ConvertFromRPCSecrets(req.BlobInfo.Secrets), + Licenses: ConvertFromRPCLicenseFiles(req.BlobInfo.Licenses), + CreatedBy: req.BlobInfo.CreatedBy, + OpaqueDirs: req.BlobInfo.OpaqueDirs, + WhiteoutFiles: req.BlobInfo.WhiteoutFiles, + BuildInfo: ConvertFromRPCBuildInfo(req.BlobInfo.BuildInfo), + } +} + +// ConvertToRPCOS returns common.OS +func ConvertToRPCOS(fos ftypes.OS) *common.OS { + return &common.OS{ + Family: string(fos.Family), + Name: fos.Name, + Eosl: fos.Eosl, + Extended: fos.Extended, + } +} + +// ConvertToRPCRepository returns common.Repository +func ConvertToRPCRepository(repo *ftypes.Repository) *common.Repository { + if repo == nil { + return nil + } + return &common.Repository{ + Family: string(repo.Family), + Release: repo.Release, + } +} + +// ConvertFromRPCBuildInfo converts *common.BuildInfo to *ftypes.BuildInfo +func ConvertFromRPCBuildInfo(buildInfo *common.BuildInfo) *ftypes.BuildInfo { + if buildInfo == nil { + return nil + } + return &ftypes.BuildInfo{ + ContentSets: buildInfo.ContentSets, + Nvr: buildInfo.Nvr, + Arch: buildInfo.Arch, + } +} + +// ConvertToRPCArtifactInfo returns PutArtifactRequest +func ConvertToRPCArtifactInfo(imageID string, imageInfo ftypes.ArtifactInfo) *cache.PutArtifactRequest { + + t := timestamppb.New(imageInfo.Created) + if err := t.CheckValid(); err != nil { + log.Warn("Invalid timestamp", log.Err(err)) + } + + return &cache.PutArtifactRequest{ + ArtifactId: imageID, + ArtifactInfo: &cache.ArtifactInfo{ + SchemaVersion: int32(imageInfo.SchemaVersion), + Architecture: imageInfo.Architecture, + Created: t, + DockerVersion: imageInfo.DockerVersion, + Os: imageInfo.OS, + HistoryPackages: ConvertToRPCPkgs(imageInfo.HistoryPackages), + Secret: ConvertToRPCSecret(imageInfo.Secret), + }, + } +} + +// ConvertToRPCPutBlobRequest returns PutBlobRequest +func ConvertToRPCPutBlobRequest(diffID string, blobInfo ftypes.BlobInfo) *cache.PutBlobRequest { + var packageInfos []*common.PackageInfo + for _, pkgInfo := range blobInfo.PackageInfos { + packageInfos = append(packageInfos, &common.PackageInfo{ + FilePath: pkgInfo.FilePath, + Packages: ConvertToRPCPkgs(pkgInfo.Packages), + }) + } + + var applications []*common.Application + for _, app := range blobInfo.Applications { + applications = append(applications, &common.Application{ + Type: string(app.Type), + FilePath: app.FilePath, + Packages: ConvertToRPCPkgs(app.Packages), + }) + } + + var misconfigurations []*common.Misconfiguration + for _, m := range blobInfo.Misconfigurations { + misconfigurations = append(misconfigurations, &common.Misconfiguration{ + FileType: string(m.FileType), + FilePath: m.FilePath, + Successes: ConvertToMisconfResults(m.Successes), + Warnings: ConvertToMisconfResults(m.Warnings), + Failures: ConvertToMisconfResults(m.Failures), + }) + + } + + var customResources []*common.CustomResource + for _, res := range blobInfo.CustomResources { + data, err := structpb.NewValue(res.Data) + if err != nil { + + } else { + customResources = append(customResources, &common.CustomResource{ + Type: res.Type, + FilePath: res.FilePath, + Layer: &common.Layer{ + Digest: res.Layer.Digest, + DiffId: res.Layer.DiffID, + }, + Data: data, + }) + } + } + + return &cache.PutBlobRequest{ + DiffId: diffID, + BlobInfo: &cache.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: blobInfo.Size, + Digest: blobInfo.Digest, + DiffId: blobInfo.DiffID, + Os: ConvertToRPCOS(blobInfo.OS), + Repository: ConvertToRPCRepository(blobInfo.Repository), + PackageInfos: packageInfos, + Applications: applications, + Misconfigurations: misconfigurations, + CustomResources: customResources, + Secrets: ConvertToRPCSecrets(blobInfo.Secrets), + Licenses: ConvertToRPCLicenseFiles(blobInfo.Licenses), + CreatedBy: blobInfo.CreatedBy, + OpaqueDirs: blobInfo.OpaqueDirs, + WhiteoutFiles: blobInfo.WhiteoutFiles, + BuildInfo: ConvertToRPCBuildInfo(blobInfo.BuildInfo), + }, + } +} + +// ConvertToRPCBuildInfo converts *ftypes.BuildInfo to *common.BuildInfo +func ConvertToRPCBuildInfo(buildInfo *ftypes.BuildInfo) *common.BuildInfo { + if buildInfo == nil { + return nil + } + return &common.BuildInfo{ + ContentSets: buildInfo.ContentSets, + Nvr: buildInfo.Nvr, + Arch: buildInfo.Arch, + } +} + +// ConvertToMisconfResults returns common.MisconfResult +func ConvertToMisconfResults(results []ftypes.MisconfResult) []*common.MisconfResult { + var rpcResults []*common.MisconfResult + for _, r := range results { + rpcResults = append(rpcResults, &common.MisconfResult{ + Namespace: r.Namespace, + Message: r.Message, + PolicyMetadata: ConvertToRPCPolicyMetadata(r.PolicyMetadata), + CauseMetadata: ConvertToRPCCauseMetadata(r.CauseMetadata), + }) + } + return rpcResults +} + +// ConvertToMissingBlobsRequest returns MissingBlobsRequest object +func ConvertToMissingBlobsRequest(imageID string, layerIDs []string) *cache.MissingBlobsRequest { + return &cache.MissingBlobsRequest{ + ArtifactId: imageID, + BlobIds: layerIDs, + } +} + +// ConvertToRPCScanResponse converts types.Result to ScanResponse +func ConvertToRPCScanResponse(response types.ScanResponse) *scanner.ScanResponse { + var rpcResults []*scanner.Result + for _, result := range response.Results { + secretFindings := xslices.Map(result.Secrets, func(s types.DetectedSecret) ftypes.SecretFinding { + return ftypes.SecretFinding(s) + }) + rpcResults = append(rpcResults, &scanner.Result{ + Target: result.Target, + Class: string(result.Class), + Type: string(result.Type), + Packages: ConvertToRPCPkgs(result.Packages), + Vulnerabilities: ConvertToRPCVulns(result.Vulnerabilities), + Misconfigurations: ConvertToRPCMisconfs(result.Misconfigurations), + Secrets: ConvertToRPCSecretFindings(secretFindings), + Licenses: ConvertToRPCLicenses(result.Licenses), + CustomResources: ConvertToRPCCustomResources(result.CustomResources), + }) + } + + return &scanner.ScanResponse{ + Os: ConvertToRPCOS(response.OS), + Results: rpcResults, + Layers: xslices.Map(response.Layers, ConvertToRPCLayer), + } +} + +func ConvertToRPCLicenses(licenses []types.DetectedLicense) []*common.DetectedLicense { + var rpcLicenses []*common.DetectedLicense + for _, l := range licenses { + severity, err := dbTypes.NewSeverity(l.Severity) + if err != nil { + log.Warn("Severity conversion error", log.Err(err)) + } + rpcLicenses = append(rpcLicenses, &common.DetectedLicense{ + Severity: common.Severity(severity), + Category: ConvertToRPCLicenseCategory(l.Category), + PkgName: l.PkgName, + FilePath: l.FilePath, + Name: l.Name, + Text: l.Text, + Confidence: float32(l.Confidence), + Link: l.Link, + }) + } + + return rpcLicenses +} + +func ConvertToRPCLicenseCategory(category ftypes.LicenseCategory) common.LicenseCategory_Enum { + return ByValueOr(LicenseCategoryMap, category, common.LicenseCategory_UNSPECIFIED) +} + +func ConvertToRPCLicenseType(ty ftypes.LicenseType) common.LicenseType_Enum { + return ByValueOr(LicenseTypeMap, ty, common.LicenseType_UNSPECIFIED) +} + +func ConvertToDeleteBlobsRequest(blobIDs []string) *cache.DeleteBlobsRequest { + return &cache.DeleteBlobsRequest{BlobIds: blobIDs} +} + +func ConvertFromDeleteBlobsRequest(deleteBlobsRequest *cache.DeleteBlobsRequest) []string { + if deleteBlobsRequest == nil { + return []string{} + } + return deleteBlobsRequest.GetBlobIds() +} + +// ConvertFromRPCSecret converts common.Secret to fanal.Secret +func ConvertFromRPCSecret(rpcSecret *common.Secret) *ftypes.Secret { + if rpcSecret == nil { + return nil + } + return &ftypes.Secret{ + FilePath: rpcSecret.Filepath, + Findings: ConvertFromRPCSecretFindings(rpcSecret.Findings), + } +} + +// ConvertToRPCSecret converts fanal.Secret to common.Secret +func ConvertToRPCSecret(secret *ftypes.Secret) *common.Secret { + if secret == nil { + return nil + } + return &common.Secret{ + Filepath: secret.FilePath, + Findings: ConvertToRPCSecretFindings(secret.Findings), + } +} diff --git a/pkg/rpc/convert_test.go b/pkg/rpc/convert_test.go new file mode 100644 index 000000000000..56a3ee4be7ad --- /dev/null +++ b/pkg/rpc/convert_test.go @@ -0,0 +1,1179 @@ +package rpc + +import ( + jsonv2 "encoding/json/v2" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/rpc/common" + "github.com/aquasecurity/trivy/rpc/scanner" +) + +func TestConvertToRpcPkgs(t *testing.T) { + type args struct { + pkgs []ftypes.Package + } + tests := []struct { + name string + args args + want []*common.Package + }{ + { + name: "happy path", + args: args{ + pkgs: []ftypes.Package{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 10, + EndLine: 20, + }, + { + StartLine: 22, + EndLine: 32, + }, + }, + Layer: ftypes.Layer{ + Digest: "sha256:6a428f9f83b0a29f1fdd2ccccca19a9bab805a925b8eddf432a5a3d3da04afbc", + DiffID: "sha256:39982b2a789afc156fff00c707d0ff1c6ab4af8f1666a8df4787714059ce24e7", + }, + Digest: "SHA1:901a7b55410321c4d35543506cff2a8613ef5aa2", + Indirect: true, + Relationship: ftypes.RelationshipIndirect, + Identifier: ftypes.PkgIdentifier{ + UID: "01", + }, + }, + }, + }, + want: []*common.Package{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + Licenses: []string{"MIT"}, + Locations: []*common.Location{ + { + StartLine: 10, + EndLine: 20, + }, + { + StartLine: 22, + EndLine: 32, + }, + }, + Layer: &common.Layer{ + Digest: "sha256:6a428f9f83b0a29f1fdd2ccccca19a9bab805a925b8eddf432a5a3d3da04afbc", + DiffId: "sha256:39982b2a789afc156fff00c707d0ff1c6ab4af8f1666a8df4787714059ce24e7", + }, + Digest: "SHA1:901a7b55410321c4d35543506cff2a8613ef5aa2", + Indirect: true, + Relationship: 4, + Identifier: &common.PkgIdentifier{ + Uid: "01", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertToRPCPkgs(tt.args.pkgs) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConvertFromRpcPkgs(t *testing.T) { + type args struct { + rpcPkgs []*common.Package + } + tests := []struct { + name string + args args + want []ftypes.Package + }{ + { + args: args{ + rpcPkgs: []*common.Package{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + Licenses: []string{"MIT"}, + Locations: []*common.Location{ + { + StartLine: 10, + EndLine: 20, + }, + { + StartLine: 22, + EndLine: 32, + }, + }, + Layer: &common.Layer{ + Digest: "sha256:6a428f9f83b0a29f1fdd2ccccca19a9bab805a925b8eddf432a5a3d3da04afbc", + DiffId: "sha256:39982b2a789afc156fff00c707d0ff1c6ab4af8f1666a8df4787714059ce24e7", + }, + Digest: "SHA1:901a7b55410321c4d35543506cff2a8613ef5aa2", + Relationship: 4, + Indirect: true, + Identifier: &common.PkgIdentifier{ + Uid: "01", + }, + }, + }, + }, + want: []ftypes.Package{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + Licenses: []string{"MIT"}, + Locations: []ftypes.Location{ + { + StartLine: 10, + EndLine: 20, + }, + { + StartLine: 22, + EndLine: 32, + }, + }, + Layer: ftypes.Layer{ + Digest: "sha256:6a428f9f83b0a29f1fdd2ccccca19a9bab805a925b8eddf432a5a3d3da04afbc", + DiffID: "sha256:39982b2a789afc156fff00c707d0ff1c6ab4af8f1666a8df4787714059ce24e7", + }, + Digest: "SHA1:901a7b55410321c4d35543506cff2a8613ef5aa2", + Relationship: ftypes.RelationshipIndirect, + Indirect: true, + Identifier: ftypes.PkgIdentifier{ + UID: "01", + }, + }, + }, + }, + { + args: args{ + rpcPkgs: []*common.Package{ + { + Name: "binary", + Version: "4.2+dfsg", + Release: "0.1+deb7u4", + Epoch: 0, + Arch: "amd64", + SrcName: "bash", + SrcVersion: "4.2+dfsg", + SrcRelease: "0.1+deb7u4", + SrcEpoch: 0, + Licenses: []string{"GPL-3.0"}, + Locations: []*common.Location{ + { + StartLine: 10, + EndLine: 20, + }, + { + StartLine: 22, + EndLine: 32, + }, + }, + Layer: &common.Layer{ + Digest: "sha256:8d42b73fc1ddc2e9e66c954966f144665825e69f4ed10c66342ae7c26b38d4e4", + DiffId: "sha256:745d171eb8c3d69f788da3a1b053056231ad140b80be71d6869229846a1f3a77", + }, + Digest: "SHA1:901a7b55410321c4d35543506cff2a8613ef5aa2", + Indirect: false, + Relationship: 3, + Identifier: &common.PkgIdentifier{ + Uid: "63f8bef824b960e3", + }, + Maintainer: "alice@example.com", + }, + }, + }, + want: []ftypes.Package{ + { + Name: "binary", + Version: "4.2+dfsg", + Release: "0.1+deb7u4", + Epoch: 0, + Arch: "amd64", + SrcName: "bash", + SrcVersion: "4.2+dfsg", + SrcRelease: "0.1+deb7u4", + SrcEpoch: 0, + Licenses: []string{"GPL-3.0"}, + Locations: []ftypes.Location{ + { + StartLine: 10, + EndLine: 20, + }, + { + StartLine: 22, + EndLine: 32, + }, + }, + Layer: ftypes.Layer{ + Digest: "sha256:8d42b73fc1ddc2e9e66c954966f144665825e69f4ed10c66342ae7c26b38d4e4", + DiffID: "sha256:745d171eb8c3d69f788da3a1b053056231ad140b80be71d6869229846a1f3a77", + }, + Digest: "SHA1:901a7b55410321c4d35543506cff2a8613ef5aa2", + Relationship: ftypes.RelationshipDirect, + Indirect: false, + Identifier: ftypes.PkgIdentifier{ + UID: "63f8bef824b960e3", + }, + Maintainer: "alice@example.com", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertFromRPCPkgs(tt.args.rpcPkgs) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConvertToRpcVulns(t *testing.T) { + fixedPublishedDate := time.Unix(1257894000, 0) + fixedLastModifiedDate := time.Unix(1257894010, 0) + type customStruct struct { + Field string + Number int + } + customData := customStruct{Field: "value", Number: 1} + customJSONBytes, err := jsonv2.Marshal(customData) + require.NoError(t, err) + customJSON := string(customJSONBytes) + + type args struct { + vulns []types.DetectedVulnerability + } + tests := []struct { + name string + args args + want []*common.Vulnerability + }{ + { + name: "happy path", + args: args{ + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Title: "DoS", + Description: "Denial of Service", + Severity: "MEDIUM", + Custom: customData, + VendorSeverity: dbTypes.VendorSeverity{ + vulnerability.RedHat: dbTypes.SeverityMedium, + }, + CVSS: dbTypes.VendorCVSS{ + vulnerability.RedHat: { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 7.2, + V3Score: 7.8, + }, + vulnerability.NVD: { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V40Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/U:Green", + V2Score: 7.2, + V3Score: 7.8, + V40Score: 8.7, + }, + }, + References: []string{"http://example.com"}, + PublishedDate: &fixedPublishedDate, + LastModifiedDate: &fixedLastModifiedDate, + }, + Layer: ftypes.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/CVE-2019-0001", + DataSource: &dbTypes.DataSource{ + Name: "GitHub Security Advisory Maven", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + Custom: customData, + }, + }, + }, + want: []*common.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0001", + PkgName: "foo", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "DoS", + Description: "Denial of Service", + Severity: common.Severity_MEDIUM, + VendorSeverity: map[string]common.Severity{ + string(vulnerability.RedHat): common.Severity_MEDIUM, + }, + Cvss: map[string]*common.CVSS{ + "redhat": { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 7.2, + V3Score: 7.8, + }, + "nvd": { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V40Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/U:Green", + V2Score: 7.2, + V3Score: 7.8, + V40Score: 8.7, + }, + }, + References: []string{"http://example.com"}, + Layer: &common.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + CustomVulnData: structpb.NewStringValue(customJSON), + CustomAdvisoryData: structpb.NewStringValue(customJSON), + PrimaryUrl: "https://avd.aquasec.com/nvd/CVE-2019-0001", + PublishedDate: timestamppb.New(fixedPublishedDate), + LastModifiedDate: timestamppb.New(fixedLastModifiedDate), + DataSource: &common.DataSource{ + Name: "GitHub Security Advisory Maven", + Url: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + }, + }, + }, + { + name: "invalid severity", + args: args{ + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Vulnerability: dbTypes.Vulnerability{ + Title: "DoS", + Description: "Denial of Service", + Severity: "INVALID", + References: []string{"http://example.com"}, + }, + Layer: ftypes.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + DataSource: &dbTypes.DataSource{ + Name: "GitHub Security Advisory Maven", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + }, + }, + }, + want: []*common.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0002", + PkgName: "bar", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "DoS", + Description: "Denial of Service", + Severity: common.Severity_UNKNOWN, + VendorSeverity: make(map[string]common.Severity), + Cvss: make(map[string]*common.CVSS), + References: []string{"http://example.com"}, + Layer: &common.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + DataSource: &common.DataSource{ + Name: "GitHub Security Advisory Maven", + Url: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertToRPCVulns(tt.args.vulns) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConvertFromRPCResults(t *testing.T) { + fixedPublishedDate := time.Date(2009, 11, 10, 23, 0, 0, 0, time.UTC) + fixedLastModifiedDate := time.Date(2009, 11, 10, 23, 0, 10, 0, time.UTC) + customJSON := `{"Field":"value","Number":1}` + + type args struct { + rpcResults []*scanner.Result + } + tests := []struct { + name string + args args + want []types.Result + }{ + { + name: "happy path", + args: args{ + rpcResults: []*scanner.Result{ + { + Target: "alpine:3.10", + Type: string(ftypes.Alpine), + Vulnerabilities: []*common.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0001", + PkgName: "musl", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "DoS", + Description: "Denial of Service", + Severity: common.Severity_MEDIUM, + SeveritySource: string(vulnerability.NVD), + CweIds: []string{ + "CWE-123", + "CWE-456", + }, + VendorSeverity: map[string]common.Severity{ + string(vulnerability.RedHat): common.Severity_MEDIUM, + }, + Cvss: map[string]*common.CVSS{ + string(vulnerability.RedHat): { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 7.2, + V3Score: 7.8, + }, + }, + References: []string{"http://example.com"}, + Layer: &common.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + CustomVulnData: structpb.NewStringValue(customJSON), + CustomAdvisoryData: structpb.NewStringValue(customJSON), + PrimaryUrl: "https://avd.aquasec.com/nvd/CVE-2019-0001", + PublishedDate: timestamppb.New(fixedPublishedDate), + LastModifiedDate: timestamppb.New(fixedLastModifiedDate), + DataSource: &common.DataSource{ + Name: "GitHub Security Advisory Maven", + Url: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + }, + }, + }, + }, + }, + want: []types.Result{ + { + Target: "alpine:3.10", + Type: ftypes.Alpine, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "musl", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Layer: ftypes.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + SeveritySource: vulnerability.NVD, + PrimaryURL: "https://avd.aquasec.com/nvd/CVE-2019-0001", + Vulnerability: dbTypes.Vulnerability{ + Title: "DoS", + Description: "Denial of Service", + Severity: common.Severity_MEDIUM.String(), + VendorSeverity: dbTypes.VendorSeverity{ + vulnerability.RedHat: dbTypes.SeverityMedium, + }, + CweIDs: []string{ + "CWE-123", + "CWE-456", + }, + CVSS: dbTypes.VendorCVSS{ + vulnerability.RedHat: { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 7.2, + V3Score: 7.8, + }, + }, + References: []string{"http://example.com"}, + PublishedDate: &fixedPublishedDate, + LastModifiedDate: &fixedLastModifiedDate, + Custom: customJSON, + }, + DataSource: &dbTypes.DataSource{ + Name: "GitHub Security Advisory Maven", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + Custom: customJSON, + }, + }, + }, + }, + }, + { + name: "happy path - with nil dates", + args: args{ + rpcResults: []*scanner.Result{ + { + Target: "alpine:3.10", + Type: string(ftypes.Alpine), + Vulnerabilities: []*common.Vulnerability{ + { + VulnerabilityId: "CVE-2019-0001", + PkgName: "musl", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Title: "DoS", + Description: "Denial of Service", + Severity: common.Severity_MEDIUM, + SeveritySource: string(vulnerability.NVD), + CweIds: []string{ + "CWE-123", + "CWE-456", + }, + Cvss: map[string]*common.CVSS{ + "redhat": { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 7.2, + V3Score: 7.8, + }, + }, + References: []string{"http://example.com"}, + Layer: &common.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + PrimaryUrl: "https://avd.aquasec.com/nvd/CVE-2019-0001", + PublishedDate: nil, + LastModifiedDate: nil, + }, + }, + }, + }, + }, + want: []types.Result{ + { + Target: "alpine:3.10", + Type: ftypes.Alpine, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + PkgName: "musl", + InstalledVersion: "1.2.3", + FixedVersion: "1.2.4", + Layer: ftypes.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + SeveritySource: vulnerability.NVD, + PrimaryURL: "https://avd.aquasec.com/nvd/CVE-2019-0001", + Vulnerability: dbTypes.Vulnerability{ + Title: "DoS", + Description: "Denial of Service", + Severity: common.Severity_MEDIUM.String(), + CweIDs: []string{ + "CWE-123", + "CWE-456", + }, + VendorSeverity: make(dbTypes.VendorSeverity), + CVSS: dbTypes.VendorCVSS{ + vulnerability.RedHat: { + V2Vector: "AV:L/AC:L/Au:N/C:C/I:C/A:C", + V3Vector: "CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V2Score: 7.2, + V3Score: 7.8, + }, + }, + References: []string{"http://example.com"}, + Custom: any(nil), + }, + Custom: any(nil), + }, + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertFromRPCResults(tt.args.rpcResults) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConvertFromRPCMisconfs(t *testing.T) { + type args struct { + misconfs []*common.DetectedMisconfiguration + } + tests := []struct { + name string + args args + want []types.DetectedMisconfiguration + }{ + { + name: "happy path misconf", + args: args{ + misconfs: []*common.DetectedMisconfiguration{ + { + Type: "Dockerfile Security Check", + Id: "DS005", + AvdId: "AVD-DS-0005", + Aliases: []string{"use-copy-over-add"}, + Title: "ADD instead of COPY", + Description: "You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.", + Message: "Consider using 'COPY . /app' command instead of 'ADD . /app'", + Namespace: "builtin.dockerfile.DS005", + Query: "data.builtin.dockerfile.DS005.deny", + Resolution: "Use COPY instead of ADD", + Severity: common.Severity_LOW, + PrimaryUrl: "https://avd.aquasec.com/misconfig/ds005", + References: []string{ + "https://docs.docker.com/engine/reference/builder/#add", + "https://avd.aquasec.com/misconfig/ds005", + }, + Status: "FAIL", + Layer: &common.Layer{}, + CauseMetadata: &common.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + StartLine: 3, + EndLine: 3, + Code: &common.Code{ + Lines: []*common.Line{ + { + Number: 3, + Content: "ADD . /app", + IsCause: true, + Annotation: "", + Truncated: false, + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + }, + }, + }, + want: []types.DetectedMisconfiguration{ + { + Type: "Dockerfile Security Check", + ID: "DS005", + AVDID: "AVD-DS-0005", + Aliases: []string{"use-copy-over-add"}, + Title: "ADD instead of COPY", + Description: "You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.", + Message: "Consider using 'COPY . /app' command instead of 'ADD . /app'", + Namespace: "builtin.dockerfile.DS005", + Query: "data.builtin.dockerfile.DS005.deny", + Resolution: "Use COPY instead of ADD", + Severity: "LOW", + PrimaryURL: "https://avd.aquasec.com/misconfig/ds005", + References: []string{ + "https://docs.docker.com/engine/reference/builder/#add", + "https://avd.aquasec.com/misconfig/ds005", + }, + Status: "FAIL", + Layer: ftypes.Layer{}, + CauseMetadata: ftypes.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + StartLine: 3, + EndLine: 3, + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 3, + Content: "ADD . /app", + IsCause: true, + Annotation: "", + Truncated: false, + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertFromRPCMisconfs(tt.args.misconfs) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConvertToRPCMiconfs(t *testing.T) { + type args struct { + misconfs []types.DetectedMisconfiguration + } + tests := []struct { + name string + args args + want []*common.DetectedMisconfiguration + }{ + { + name: "happy path misconf", + args: args{ + misconfs: []types.DetectedMisconfiguration{ + { + Type: "Dockerfile Security Check", + ID: "DS005", + AVDID: "AVD-DS-0005", + Aliases: []string{"use-copy-over-add"}, + Title: "ADD instead of COPY", + Description: "You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.", + Message: "Consider using 'COPY . /app' command instead of 'ADD . /app'", + Namespace: "builtin.dockerfile.DS005", + Query: "data.builtin.dockerfile.DS005.deny", + Resolution: "Use COPY instead of ADD", + Severity: "LOW", + PrimaryURL: "https://avd.aquasec.com/misconfig/ds005", + References: []string{ + "https://docs.docker.com/engine/reference/builder/#add", + "https://avd.aquasec.com/misconfig/ds005", + }, + Status: "FAIL", + Layer: ftypes.Layer{}, + CauseMetadata: ftypes.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + StartLine: 3, + EndLine: 3, + Code: ftypes.Code{ + Lines: []ftypes.Line{ + { + Number: 3, + Content: "ADD . /app", + IsCause: true, + Annotation: "", + Truncated: false, + FirstCause: true, + LastCause: true, + }, + }, + }, + }, + }, + }, + }, + want: []*common.DetectedMisconfiguration{ + { + Type: "Dockerfile Security Check", + Id: "DS005", + AvdId: "AVD-DS-0005", + Aliases: []string{"use-copy-over-add"}, + Title: "ADD instead of COPY", + Description: "You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.", + Message: "Consider using 'COPY . /app' command instead of 'ADD . /app'", + Namespace: "builtin.dockerfile.DS005", + Query: "data.builtin.dockerfile.DS005.deny", + Resolution: "Use COPY instead of ADD", + Severity: common.Severity_LOW, + PrimaryUrl: "https://avd.aquasec.com/misconfig/ds005", + References: []string{ + "https://docs.docker.com/engine/reference/builder/#add", + "https://avd.aquasec.com/misconfig/ds005", + }, + Status: "FAIL", + Layer: &common.Layer{}, + CauseMetadata: &common.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + StartLine: 3, + EndLine: 3, + Code: &common.Code{ + Lines: []*common.Line{ + { + Number: 3, + Content: "ADD . /app", + IsCause: true, + Annotation: "", + Truncated: false, + FirstCause: true, + LastCause: true, + }, + }, + }, + RenderedCause: &common.RenderedCause{}, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertToRPCMisconfs(tt.args.misconfs) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestConvertFromRPCLicenses(t *testing.T) { + tests := []struct { + name string + rpcLicenses []*common.DetectedLicense + want []types.DetectedLicense + }{ + { + name: "happy", + rpcLicenses: []*common.DetectedLicense{ + { + Severity: common.Severity_HIGH, + Category: common.LicenseCategory_RESTRICTED, + PkgName: "alpine-baselayout", + FilePath: "some-path", + Name: "GPL-2.0", + Text: "text://* Permission to use this software in any way is granted without", + Confidence: 1, + Link: "https://some-link", + }, + }, + want: []types.DetectedLicense{ + { + Severity: "HIGH", + Category: "restricted", + PkgName: "alpine-baselayout", + FilePath: "some-path", + Name: "GPL-2.0", + Text: "text://* Permission to use this software in any way is granted without", + Confidence: 1, + Link: "https://some-link", + }, + }, + }, + { + name: "no licenses", + rpcLicenses: nil, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertFromRPCDetectedLicenses(tt.rpcLicenses) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestConvertToRPCLicenses(t *testing.T) { + tests := []struct { + name string + licenses []types.DetectedLicense + want []*common.DetectedLicense + }{ + { + name: "happy", + licenses: []types.DetectedLicense{ + { + Severity: "HIGH", + Category: "restricted", + PkgName: "alpine-baselayout", + FilePath: "some-path", + Name: "GPL-2.0", + Text: "text://* Permission to use this software in any way is granted without", + Confidence: 1, + Link: "https://some-link", + }, + }, + want: []*common.DetectedLicense{ + { + Severity: common.Severity_HIGH, + Category: common.LicenseCategory_RESTRICTED, + PkgName: "alpine-baselayout", + FilePath: "some-path", + Name: "GPL-2.0", + Text: "text://* Permission to use this software in any way is granted without", + Confidence: 1, + Link: "https://some-link", + }, + }, + }, + { + name: "no licenses", + licenses: nil, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertToRPCLicenses(tt.licenses) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestConvertToRPCLicenseCategory(t *testing.T) { + tests := []struct { + name string + category ftypes.LicenseCategory + want common.LicenseCategory_Enum + }{ + { + name: "happy", + category: ftypes.CategoryNotice, + want: common.LicenseCategory_NOTICE, + }, + { + name: "unspecified", + category: "", + want: common.LicenseCategory_UNSPECIFIED, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertToRPCLicenseCategory(tt.category) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestConvertFromRPCLicenseCategory(t *testing.T) { + tests := []struct { + name string + rpcCategory common.LicenseCategory_Enum + want ftypes.LicenseCategory + }{ + { + name: "happy", + rpcCategory: common.LicenseCategory_RESTRICTED, + want: ftypes.CategoryRestricted, + }, + { + name: "unspecified", + rpcCategory: common.LicenseCategory_UNSPECIFIED, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertFromRPCLicenseCategory(tt.rpcCategory) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestConvertToRPCLicenseType(t *testing.T) { + tests := []struct { + name string + ty ftypes.LicenseType + want common.LicenseType_Enum + }{ + { + name: "happy", + ty: ftypes.LicenseTypeFile, + want: common.LicenseType_LICENSE_FILE, + }, + { + name: "unspecified", + ty: "", + want: common.LicenseType_UNSPECIFIED, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertToRPCLicenseType(tt.ty) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestConvertFromRPCLicenseType(t *testing.T) { + tests := []struct { + name string + rpcType common.LicenseType_Enum + want ftypes.LicenseType + }{ + { + name: "happy", + rpcType: common.LicenseType_LICENSE_FILE, + want: ftypes.LicenseTypeFile, + }, + { + name: "unspecified", + rpcType: common.LicenseType_UNSPECIFIED, + want: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ConvertFromRPCLicenseType(tt.rpcType) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestConvertToRPCLicenseFiles(t *testing.T) { + tests := []struct { + name string + licenseFiles []ftypes.LicenseFile + want []*common.LicenseFile + }{ + { + name: "happy", + licenseFiles: []ftypes.LicenseFile{ + { + Type: ftypes.LicenseTypeFile, + PkgName: "alpine-baselayout", + FilePath: "some-path", + Findings: ftypes.LicenseFindings{ + { + Category: ftypes.CategoryRestricted, + Name: "GPL-2.0", + Confidence: 1, + Link: "https://some-link", + }, + }, + Layer: ftypes.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + }, + }, + want: []*common.LicenseFile{ + { + LicenseType: common.LicenseType_LICENSE_FILE, + PkgName: "alpine-baselayout", + FilePath: "some-path", + Fingings: []*common.LicenseFinding{ + { + Category: common.LicenseCategory_RESTRICTED, + Name: "GPL-2.0", + Confidence: 1, + Link: "https://some-link", + }, + }, + Layer: &common.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, ConvertToRPCLicenseFiles(tt.licenseFiles)) + }) + } +} + +func TestConvertFromRPCLicenseFiles(t *testing.T) { + tests := []struct { + name string + licenseFiles []*common.LicenseFile + want []ftypes.LicenseFile + }{ + { + name: "happy", + licenseFiles: []*common.LicenseFile{ + { + LicenseType: common.LicenseType_LICENSE_FILE, + PkgName: "alpine-baselayout", + FilePath: "some-path", + Fingings: []*common.LicenseFinding{ + { + Category: common.LicenseCategory_RESTRICTED, + Name: "GPL-2.0", + Confidence: 1, + Link: "https://some-link", + }, + }, + Layer: &common.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + }, + }, + want: []ftypes.LicenseFile{ + { + Type: ftypes.LicenseTypeFile, + PkgName: "alpine-baselayout", + FilePath: "some-path", + Findings: ftypes.LicenseFindings{ + { + Category: ftypes.CategoryRestricted, + Name: "GPL-2.0", + Confidence: 1, + Link: "https://some-link", + }, + }, + Layer: ftypes.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.want, ConvertFromRPCLicenseFiles(tt.licenseFiles)) + }) + } +} diff --git a/pkg/rpc/retry.go b/pkg/rpc/retry.go new file mode 100644 index 000000000000..01fe13788ef4 --- /dev/null +++ b/pkg/rpc/retry.go @@ -0,0 +1,42 @@ +package rpc + +import ( + "time" + + "github.com/cenkalti/backoff/v4" + "github.com/twitchtv/twirp" + + "github.com/aquasecurity/trivy/pkg/log" +) + +const ( + maxRetries = 10 +) + +// Retry executes the function again using backoff until maxRetries or success +func Retry(f func() error) error { + operation := func() error { + err := f() + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + return backoff.Permanent(err) + } + if twerr.Code() == twirp.Unavailable { + return err + } + return backoff.Permanent(err) + } + return nil + } + + b := backoff.WithMaxRetries(backoff.NewExponentialBackOff(), maxRetries) + err := backoff.RetryNotify(operation, b, func(err error, _ time.Duration) { + log.Warn("HTTP error", log.Err(err)) + log.Info("Retrying HTTP request...") + }) + if err != nil { + return err + } + return nil +} diff --git a/pkg/rpc/server/listen.go b/pkg/rpc/server/listen.go new file mode 100644 index 000000000000..05af55e02dd3 --- /dev/null +++ b/pkg/rpc/server/listen.go @@ -0,0 +1,230 @@ +package server + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "os" + "strings" + "sync" + "time" + + "github.com/NYTimes/gziphandler" + "github.com/google/go-containerregistry/pkg/name" + "github.com/twitchtv/twirp" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy-db/pkg/metadata" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" + "github.com/aquasecurity/trivy/pkg/version" + xos "github.com/aquasecurity/trivy/pkg/x/os" + rpcCache "github.com/aquasecurity/trivy/rpc/cache" + rpcScanner "github.com/aquasecurity/trivy/rpc/scanner" +) + +const updateInterval = 1 * time.Hour + +// Server represents Trivy server +type Server struct { + appVersion string + addr string + cacheDir string + dbDir string + token string + tokenHeader string + pathPrefix string + dbRepositories []name.Reference + + // For OCI registries + types.RegistryOptions +} + +// NewServer returns an instance of Server +func NewServer(appVersion, addr, cacheDir, token, tokenHeader, pathPrefix string, dbRepositories []name.Reference, opt types.RegistryOptions) Server { + return Server{ + appVersion: appVersion, + addr: addr, + cacheDir: cacheDir, + dbDir: db.Dir(cacheDir), + token: token, + tokenHeader: tokenHeader, + pathPrefix: pathPrefix, + dbRepositories: dbRepositories, + RegistryOptions: opt, + } +} + +// ListenAndServe starts Trivy server +func (s Server) ListenAndServe(ctx context.Context, serverCache cache.Cache, skipDBUpdate bool) error { + requestWg := &sync.WaitGroup{} + dbUpdateWg := &sync.WaitGroup{} + + server := &http.Server{ + Addr: s.addr, + Handler: s.NewServeMux(ctx, serverCache, dbUpdateWg, requestWg), + ReadHeaderTimeout: 10 * time.Second, + } + + // Start DB update worker + go func() { + worker := newDBWorker(db.NewClient(s.dbDir, true, db.WithDBRepository(s.dbRepositories))) + ticker := time.NewTicker(updateInterval) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + log.Debug("Server shutting down gracefully...") + + // Give active requests time to complete + shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + if err := server.Shutdown(shutdownCtx); err != nil { + log.Errorf("Server shutdown error: %v", err) + } + cancel() + return + case <-ticker.C: + if err := worker.update(ctx, s.appVersion, s.dbDir, skipDBUpdate, dbUpdateWg, requestWg, s.RegistryOptions); err != nil { + log.Errorf("%+v\n", err) + } + } + } + }() + + log.Infof("Listening %s...", s.addr) + + // This will block until Shutdown is called + if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return xerrors.Errorf("server error: %w", err) + } + + return nil +} + +func (s Server) NewServeMux(ctx context.Context, serverCache cache.Cache, dbUpdateWg, requestWg *sync.WaitGroup) *http.ServeMux { + withWaitGroup := func(base http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Stop processing requests during DB update + dbUpdateWg.Wait() + + // Wait for all requests to be processed before DB update + requestWg.Add(1) + defer requestWg.Done() + + base.ServeHTTP(w, r.WithContext(ctx)) + + }) + } + + mux := http.NewServeMux() + + var twirpOpts []any + if s.pathPrefix != "" { + pathPrefix := "/" + strings.TrimPrefix(s.pathPrefix, "/") // Twirp requires the leading slash + twirpOpts = append(twirpOpts, twirp.WithServerPathPrefix(pathPrefix)) + } + + scanServer := rpcScanner.NewScannerServer(initializeScanServer(serverCache), twirpOpts...) + scanHandler := withToken(withWaitGroup(scanServer), s.token, s.tokenHeader) + mux.Handle(scanServer.PathPrefix(), gziphandler.GzipHandler(scanHandler)) + + cacheServer := rpcCache.NewCacheServer(NewCacheServer(serverCache), twirpOpts...) + layerHandler := withToken(withWaitGroup(cacheServer), s.token, s.tokenHeader) + mux.Handle(cacheServer.PathPrefix(), gziphandler.GzipHandler(layerHandler)) + + mux.HandleFunc("/healthz", func(rw http.ResponseWriter, _ *http.Request) { + if _, err := rw.Write([]byte("ok")); err != nil { + log.Error("Health check error", log.Err(err)) + } + }) + + mux.HandleFunc("/version", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Add("Content-Type", "application/json") + + if err := json.NewEncoder(w).Encode(version.NewVersionInfo(s.cacheDir, version.Server())); err != nil { + log.Error("Version error", log.Err(err)) + } + }) + + return mux +} + +func withToken(base http.Handler, token, tokenHeader string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if token != "" && token != r.Header.Get(tokenHeader) { + rpcScanner.WriteError(w, twirp.NewError(twirp.Unauthenticated, "invalid token")) + return + } + base.ServeHTTP(w, r) + }) +} + +type dbWorker struct { + dbClient *db.Client +} + +func newDBWorker(dbClient *db.Client) dbWorker { + return dbWorker{dbClient: dbClient} +} + +func (w dbWorker) update(ctx context.Context, appVersion, dbDir string, + skipDBUpdate bool, dbUpdateWg, requestWg *sync.WaitGroup, opt types.RegistryOptions) error { + log.Debug("Check for DB update...") + needsUpdate, err := w.dbClient.NeedsUpdate(ctx, appVersion, skipDBUpdate) + if err != nil { + return xerrors.Errorf("failed to check if db needs an update") + } else if !needsUpdate { + return nil + } + + log.Info("Updating DB...") + if err = w.hotUpdate(ctx, dbDir, dbUpdateWg, requestWg, opt); err != nil { + return xerrors.Errorf("failed DB hot update: %w", err) + } + return nil +} + +func (w dbWorker) hotUpdate(ctx context.Context, dbDir string, dbUpdateWg, requestWg *sync.WaitGroup, opt types.RegistryOptions) error { + tmpDir, err := xos.MkdirTemp("", "db-worker-") + if err != nil { + return xerrors.Errorf("failed to create a temp dir: %w", err) + } + defer os.RemoveAll(tmpDir) + + if err = w.dbClient.Download(ctx, tmpDir, opt); err != nil { + return xerrors.Errorf("failed to download vulnerability DB: %w", err) + } + + log.Info("Suspending all requests during DB update") + dbUpdateWg.Add(1) + defer dbUpdateWg.Done() + + log.Info("Waiting for all requests to be processed before DB update...") + requestWg.Wait() + + if err = db.Close(); err != nil { + return xerrors.Errorf("failed to close DB: %w", err) + } + + // Copy trivy.db + if _, err = fsutils.CopyFile(db.Path(tmpDir), db.Path(dbDir)); err != nil { + return xerrors.Errorf("failed to copy the database file: %w", err) + } + + // Copy metadata.json + if _, err = fsutils.CopyFile(metadata.Path(tmpDir), metadata.Path(dbDir)); err != nil { + return xerrors.Errorf("failed to copy the metadata file: %w", err) + } + + log.Info("Reopening DB...") + if err = db.Init(dbDir); err != nil { + return xerrors.Errorf("failed to open DB: %w", err) + } + + return nil +} diff --git a/pkg/rpc/server/listen_test.go b/pkg/rpc/server/listen_test.go new file mode 100644 index 000000000000..93aef2c6e5e9 --- /dev/null +++ b/pkg/rpc/server/listen_test.go @@ -0,0 +1,231 @@ +package server + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "path" + "sync" + "testing" + "time" + + ggcrtypes "github.com/google/go-containerregistry/pkg/v1/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/metadata" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/db" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/types" + rpcCache "github.com/aquasecurity/trivy/rpc/cache" +) + +func Test_dbWorker_update(t *testing.T) { + cachedMetadata := metadata.Metadata{ + Version: db.SchemaVersion, + NextUpdate: time.Date(2020, 10, 2, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(2020, 10, 1, 0, 0, 0, 0, time.UTC), + DownloadedAt: time.Date(2020, 10, 1, 1, 0, 0, 0, time.UTC), + } + + tests := []struct { + name string + now time.Time + skipUpdate bool + layerMediaType ggcrtypes.MediaType + want metadata.Metadata + wantErr string + }{ + { + name: "update needed", + now: time.Date(2021, 10, 1, 0, 0, 0, 0, time.UTC), + skipUpdate: false, + want: metadata.Metadata{ + Version: db.SchemaVersion, + NextUpdate: time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC), + UpdatedAt: time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC), + DownloadedAt: time.Date(2021, 10, 1, 0, 0, 0, 0, time.UTC), + }, + }, + { + name: "not update needed", + now: time.Date(2019, 10, 1, 0, 0, 0, 0, time.UTC), + skipUpdate: false, + want: cachedMetadata, + }, + { + name: "skip update", + now: time.Date(2021, 10, 1, 0, 0, 0, 0, time.UTC), + skipUpdate: true, + want: cachedMetadata, + }, + { + name: "Download returns an error", + now: time.Date(2021, 10, 1, 0, 0, 0, 0, time.UTC), + skipUpdate: false, + layerMediaType: ggcrtypes.MediaType("unknown"), + wantErr: "failed DB hot update", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize DB with metadata + dbDir := dbtest.InitWithMetadata(t, cachedMetadata, true) + t.Cleanup(func() { + require.NoError(t, db.Close()) + }) + + // Set a fake time + ctx := clock.With(t.Context(), tt.now) + + // Set a fake DB + dbPath := dbtest.ArchiveDir(t, "testdata/newdb") + art := dbtest.NewFakeDB(t, dbPath, dbtest.FakeDBOptions{ + MediaType: tt.layerMediaType, + }) + client := db.NewClient(dbDir, true, db.WithOCIArtifact(art)) + w := newDBWorker(client) + + var dbUpdateWg, requestWg sync.WaitGroup + err := w.update(ctx, "1.2.3", dbDir, + tt.skipUpdate, &dbUpdateWg, &requestWg, ftypes.RegistryOptions{}) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err, tt.name) + + mc := metadata.NewClient(dbDir) + got, err := mc.Get() + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got, tt.name) + }) + } +} + +func TestServer_newServeMux(t *testing.T) { + type args struct { + token string + tokenHeader string + } + tests := []struct { + name string + args args + path string + header http.Header + want int + }{ + { + name: "health check", + path: "/healthz", + want: http.StatusOK, + }, + { + name: "cache endpoint", + path: path.Join(rpcCache.CachePathPrefix, "MissingBlobs"), + header: http.Header{ + "Content-Type": []string{"application/protobuf"}, + }, + want: http.StatusOK, + }, + { + name: "with token", + args: args{ + token: "test", + tokenHeader: "Authorization", + }, + path: path.Join(rpcCache.CachePathPrefix, "MissingBlobs"), + header: http.Header{ + "Authorization": []string{"test"}, + "Content-Type": []string{"application/protobuf"}, + }, + want: http.StatusOK, + }, + { + name: "sad path: no handler", + path: "/sad", + header: http.Header{ + "Content-Type": []string{"application/protobuf"}, + }, + want: http.StatusNotFound, + }, + { + name: "sad path: invalid token", + args: args{ + token: "test", + tokenHeader: "Authorization", + }, + path: path.Join(rpcCache.CachePathPrefix, "MissingBlobs"), + header: http.Header{ + "Content-Type": []string{"application/protobuf"}, + }, + want: http.StatusUnauthorized, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dbUpdateWg, requestWg := &sync.WaitGroup{}, &sync.WaitGroup{} + + c, err := cache.NewFSCache(t.TempDir()) + require.NoError(t, err) + defer func() { _ = c.Close() }() + + s := NewServer("", "", "", tt.args.token, tt.args.tokenHeader, "", nil, ftypes.RegistryOptions{}) + ts := httptest.NewServer(s.NewServeMux(t.Context(), c, dbUpdateWg, requestWg)) + defer ts.Close() + + var resp *http.Response + url := ts.URL + tt.path + if tt.header == nil { + resp, err = http.Get(url) + require.NoError(t, err) + defer resp.Body.Close() + } else { + req, err := http.NewRequest(http.MethodPost, url, http.NoBody) + require.NoError(t, err) + + req.Header = tt.header + client := new(http.Client) + resp, err = client.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + } + assert.Equal(t, tt.want, resp.StatusCode) + }) + } +} + +func Test_VersionEndpoint(t *testing.T) { + dbUpdateWg, requestWg := &sync.WaitGroup{}, &sync.WaitGroup{} + c, err := cache.NewFSCache(t.TempDir()) + require.NoError(t, err) + defer func() { _ = c.Close() }() + + s := NewServer("", "", "testdata/testcache", "", "", "", nil, ftypes.RegistryOptions{}) + ts := httptest.NewServer(s.NewServeMux(t.Context(), c, dbUpdateWg, requestWg)) + defer ts.Close() + + resp, err := http.Get(ts.URL + "/version") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(t, http.StatusOK, resp.StatusCode) + + var versionInfo types.VersionInfo + require.NoError(t, json.NewDecoder(resp.Body).Decode(&versionInfo)) + + // Server mode excludes JavaDB and CheckBundle as they are managed on the client side + expected := types.VersionInfo{ + Version: "dev", + VulnerabilityDB: &metadata.Metadata{ + Version: 2, + NextUpdate: time.Date(2023, 7, 20, 18, 11, 37, 696263532, time.UTC), + UpdatedAt: time.Date(2023, 7, 20, 12, 11, 37, 696263932, time.UTC), + DownloadedAt: time.Date(2023, 7, 25, 7, 1, 41, 239158000, time.UTC), + }, + } + assert.Equal(t, expected, versionInfo) +} diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go new file mode 100644 index 000000000000..37fc48c07663 --- /dev/null +++ b/pkg/rpc/server/server.go @@ -0,0 +1,169 @@ +package server + +import ( + "context" + + "github.com/samber/lo" + "golang.org/x/xerrors" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/rpc" + "github.com/aquasecurity/trivy/pkg/scan" + "github.com/aquasecurity/trivy/pkg/scan/langpkg" + "github.com/aquasecurity/trivy/pkg/scan/local" + "github.com/aquasecurity/trivy/pkg/scan/ospkg" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vulnerability" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" + xstrings "github.com/aquasecurity/trivy/pkg/x/strings" + rpcCache "github.com/aquasecurity/trivy/rpc/cache" + rpcScanner "github.com/aquasecurity/trivy/rpc/scanner" +) + +// ScanServer implements the scanner service. +// It uses local.Service as its backend to perform various types of security scanning. +type ScanServer struct { + local scan.Backend +} + +// NewScanServer creates a new ScanServer instance with the specified backend implementation. +func NewScanServer(s scan.Backend) *ScanServer { + return &ScanServer{local: s} +} + +// initializeScanServer creates a new RPC scan server with the provided cache. +func initializeScanServer(localArtifactCache cache.LocalArtifactCache) *ScanServer { + applier := applier.NewApplier(localArtifactCache) + osScanner := ospkg.NewScanner() + langScanner := langpkg.NewScanner() + vulnClient := vulnerability.NewClient(db.Config{}) + + localService := local.NewService(applier, osScanner, langScanner, vulnClient) + return NewScanServer(localService) +} + +// Log and return an error +func teeError(err error) error { + log.Errorf("%+v", err) + return err +} + +// Scan scans and return response +func (s *ScanServer) Scan(ctx context.Context, in *rpcScanner.ScanRequest) (*rpcScanner.ScanResponse, error) { + options := s.ToOptions(in.Options) + scanResponse, err := s.local.Scan(ctx, in.Target, in.ArtifactId, in.BlobIds, options) + if err != nil { + return nil, teeError(xerrors.Errorf("failed scan, %s: %w", in.Target, err)) + } + + return rpc.ConvertToRPCScanResponse(scanResponse), nil +} + +func (s *ScanServer) ToOptions(in *rpcScanner.ScanOptions) types.ScanOptions { + pkgRelationships := lo.FilterMap(in.PkgRelationships, func(r string, _ int) (ftypes.Relationship, bool) { + rel, err := ftypes.NewRelationship(r) + if err != nil { + log.Warnf("Invalid relationship: %s", r) + return ftypes.RelationshipUnknown, false + } + return rel, true + }) + if len(pkgRelationships) == 0 { + pkgRelationships = ftypes.Relationships // For backward compatibility + } + + scanners := xslices.Map(in.Scanners, func(s string) types.Scanner { + return types.Scanner(s) + }) + + licenseCategories := lo.MapEntries(in.LicenseCategories, + func(k string, v *rpcScanner.Licenses) (ftypes.LicenseCategory, []string) { + return ftypes.LicenseCategory(k), v.Names + }) + + var distro ftypes.OS + if in.Distro != nil { + distro.Family = ftypes.OSType(in.Distro.Family) + distro.Name = in.Distro.Name + } + + vulnSeveritySources := xstrings.ToTSlice[dbTypes.SourceID](in.VulnSeveritySources) + if len(vulnSeveritySources) == 0 { + vulnSeveritySources = []dbTypes.SourceID{ + "auto", // For backward compatibility + } + } + + return types.ScanOptions{ + PkgTypes: in.PkgTypes, + PkgRelationships: pkgRelationships, + Scanners: scanners, + IncludeDevDeps: in.IncludeDevDeps, + LicenseCategories: licenseCategories, + LicenseFull: in.LicenseFull, + Distro: distro, + VulnSeveritySources: vulnSeveritySources, + } +} + +// CacheServer implements the cache +type CacheServer struct { + cache cache.Cache +} + +// NewCacheServer is the factory method for cacheServer +func NewCacheServer(c cache.Cache) *CacheServer { + return &CacheServer{cache: c} +} + +// PutArtifact puts the artifacts in cache +func (s *CacheServer) PutArtifact(ctx context.Context, in *rpcCache.PutArtifactRequest) (*emptypb.Empty, error) { + if in.ArtifactInfo == nil { + return nil, teeError(xerrors.Errorf("empty image info")) + } + imageInfo := rpc.ConvertFromRPCPutArtifactRequest(in) + if err := s.cache.PutArtifact(ctx, in.ArtifactId, imageInfo); err != nil { + return nil, teeError(xerrors.Errorf("unable to store image info in cache: %w", err)) + } + return &emptypb.Empty{}, nil +} + +// PutBlob puts the blobs in cache +func (s *CacheServer) PutBlob(ctx context.Context, in *rpcCache.PutBlobRequest) (*emptypb.Empty, error) { + if in.BlobInfo == nil { + return nil, teeError(xerrors.Errorf("empty layer info")) + } + layerInfo := rpc.ConvertFromRPCPutBlobRequest(in) + if err := s.cache.PutBlob(ctx, in.DiffId, layerInfo); err != nil { + return nil, teeError(xerrors.Errorf("unable to store layer info in cache: %w", err)) + } + return &emptypb.Empty{}, nil +} + +// MissingBlobs returns missing blobs from cache +func (s *CacheServer) MissingBlobs(ctx context.Context, in *rpcCache.MissingBlobsRequest) (*rpcCache.MissingBlobsResponse, error) { + missingArtifact, blobIDs, err := s.cache.MissingBlobs(ctx, in.ArtifactId, in.BlobIds) + + if err != nil { + return nil, teeError(xerrors.Errorf("failed to get missing blobs: %w", err)) + } + return &rpcCache.MissingBlobsResponse{ + MissingArtifact: missingArtifact, + MissingBlobIds: blobIDs, + }, nil +} + +// DeleteBlobs removes blobs by IDs +func (s *CacheServer) DeleteBlobs(ctx context.Context, in *rpcCache.DeleteBlobsRequest) (*emptypb.Empty, error) { + blobIDs := rpc.ConvertFromDeleteBlobsRequest(in) + if err := s.cache.DeleteBlobs(ctx, blobIDs); err != nil { + return nil, teeError(xerrors.Errorf("failed to remove a blobs: %w", err)) + } + return &emptypb.Empty{}, nil +} diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go new file mode 100644 index 000000000000..6e66141d0756 --- /dev/null +++ b/pkg/rpc/server/server_test.go @@ -0,0 +1,587 @@ +package server + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/aquasecurity/trivy-db/pkg/db" + "github.com/aquasecurity/trivy/internal/cachetest" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/scan/langpkg" + "github.com/aquasecurity/trivy/pkg/scan/local" + "github.com/aquasecurity/trivy/pkg/scan/ospkg" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vulnerability" + rpcCache "github.com/aquasecurity/trivy/rpc/cache" + "github.com/aquasecurity/trivy/rpc/common" + rpcScanner "github.com/aquasecurity/trivy/rpc/scanner" +) + +func TestScanServer_Scan(t *testing.T) { + type args struct { + in *rpcScanner.ScanRequest + } + tests := []struct { + name string + args args + fixtures []string + setUpCache func(t *testing.T) cache.Cache + want *rpcScanner.ScanResponse + wantErr string + }{ + { + name: "happy path", + args: args{ + in: &rpcScanner.ScanRequest{ + Target: "alpine:3.11", + ArtifactId: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + BlobIds: []string{"sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203"}, + Options: &rpcScanner.ScanOptions{ + PkgTypes: []string{types.PkgTypeOS}, + Scanners: []string{string(types.VulnerabilityScanner)}, + PkgRelationships: []string{ftypes.RelationshipUnknown.String()}, + VulnSeveritySources: []string{"auto"}, + }, + }, + }, + fixtures: []string{"../../scan/local/testdata/fixtures/happy.yaml"}, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", ftypes.ArtifactInfo{ + SchemaVersion: 1, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", ftypes.BlobInfo{ + SchemaVersion: 1, + Size: 1000, + DiffID: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11.5", + }, + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: ftypes.Packages{ + { + Name: "musl", + Version: "1.1.24-r2", + SrcName: "musl", + SrcVersion: "1.1.24-r2", + }, + }, + }, + }, + })) + + return c + }, + want: &rpcScanner.ScanResponse{ + Os: &common.OS{ + Family: "alpine", + Name: "3.11.5", + Eosl: true, + }, + Results: []*rpcScanner.Result{ + { + Target: "alpine:3.11 (alpine 3.11.5)", + Vulnerabilities: []*common.Vulnerability{ + { + VulnerabilityId: "CVE-2020-9999", + PkgName: "musl", + InstalledVersion: "1.1.24-r2", + FixedVersion: "1.2.4", + Severity: common.Severity_HIGH, + Layer: &common.Layer{ + DiffId: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + PrimaryUrl: "https://avd.aquasec.com/nvd/cve-2020-9999", + Title: "dos", + Description: "dos vulnerability", + Status: 3, + PkgIdentifier: &common.PkgIdentifier{ + Purl: "pkg:apk/alpine/musl@1.1.24-r2?distro=3.11.5", + Uid: "852936e86971b22e", + }, + Cvss: make(map[string]*common.CVSS), + VendorSeverity: make(map[string]common.Severity), + }, + }, + Type: "alpine", + Class: "os-pkgs", + Packages: []*common.Package{ + { + Name: "musl", + Version: "1.1.24-r2", + SrcName: "musl", + SrcVersion: "1.1.24-r2", + Layer: &common.Layer{ + DiffId: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + Identifier: &common.PkgIdentifier{ + Purl: "pkg:apk/alpine/musl@1.1.24-r2?distro=3.11.5", + Uid: "852936e86971b22e", + }, + }, + }, + }, + }, + Layers: []*common.Layer{ + { + Size: 1000, + DiffId: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + }, + }, + }, + { + name: "sad path: broken database", + args: args{ + in: &rpcScanner.ScanRequest{ + Target: "alpine:3.11", + ArtifactId: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + BlobIds: []string{"sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203"}, + Options: &rpcScanner.ScanOptions{ + PkgTypes: []string{types.PkgTypeOS}, + Scanners: []string{string(types.VulnerabilityScanner)}, + PkgRelationships: []string{ftypes.RelationshipUnknown.String()}, + }, + }, + }, + fixtures: []string{"../../scan/local/testdata/fixtures/sad.yaml"}, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", ftypes.ArtifactInfo{ + SchemaVersion: 1, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", ftypes.BlobInfo{ + SchemaVersion: 1, + Digest: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + DiffID: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11.5", + }, + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: ftypes.Packages{ + { + Name: "musl", + Version: "1.1.24-r2", + SrcName: "musl", + SrcVersion: "1.1.24-r2", + }, + }, + }, + }, + })) + + return c + }, + wantErr: "failed to detect vulnerabilities", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Initialize DB + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + // Create artifact + c := cachetest.NewCache(t, tt.setUpCache) + + // Create scanner + applier := applier.NewApplier(c) + scanner := local.NewService(applier, ospkg.NewScanner(), langpkg.NewScanner(), vulnerability.NewClient(db.Config{})) + s := NewScanServer(scanner) + + got, err := s.Scan(t.Context(), tt.args.in) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestCacheServer_PutArtifact(t *testing.T) { + type args struct { + in *rpcCache.PutArtifactRequest + } + tests := []struct { + name string + args args + setUpCache func(t *testing.T) cache.Cache + wantArtifact cachetest.WantArtifact + want *emptypb.Empty + wantErr string + }{ + { + name: "happy path", + args: args{ + in: &rpcCache.PutArtifactRequest{ + ArtifactId: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + ArtifactInfo: &rpcCache.ArtifactInfo{ + SchemaVersion: 1, + Architecture: "amd64", + Created: func() *timestamppb.Timestamp { + d := time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC) + t := timestamppb.New(d) + return t + }(), + DockerVersion: "18.09", + Os: "linux", + }, + }, + }, + wantArtifact: cachetest.WantArtifact{ + ID: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + ArtifactInfo: ftypes.ArtifactInfo{ + SchemaVersion: 1, + Architecture: "amd64", + Created: time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC), + DockerVersion: "18.09", + OS: "linux", + }, + }, + want: &emptypb.Empty{}, + }, + { + name: "sad path", + args: args{ + in: &rpcCache.PutArtifactRequest{ + ArtifactId: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + ArtifactInfo: &rpcCache.ArtifactInfo{ + SchemaVersion: 1, + Created: func() *timestamppb.Timestamp { + d := time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC) + t := timestamppb.New(d) + return t + }(), + }, + }, + }, + setUpCache: func(_ *testing.T) cache.Cache { + return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{ + PutArtifact: true, + }) + }, + wantErr: "unable to store image info in cache", + }, + { + name: "sad path: empty image info", + args: args{ + in: &rpcCache.PutArtifactRequest{}, + }, + wantErr: "empty image info", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cachetest.NewCache(t, tt.setUpCache) + + s := NewCacheServer(c) + got, err := s.PutArtifact(t.Context(), tt.args.in) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got) + cachetest.AssertArtifact(t, c, tt.wantArtifact) + }) + } +} + +func TestCacheServer_PutBlob(t *testing.T) { + type args struct { + in *rpcCache.PutBlobRequest + } + tests := []struct { + name string + args args + setUpCache func(t *testing.T) cache.Cache + wantBlobs []cachetest.WantBlob + want *emptypb.Empty + wantErr string + }{ + { + name: "happy path", + args: args{ + in: &rpcCache.PutBlobRequest{ + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + BlobInfo: &rpcCache.BlobInfo{ + SchemaVersion: 1, + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + OpaqueDirs: []string{"etc/"}, + WhiteoutFiles: []string{"etc/hostname"}, + Os: &common.OS{ + Family: "alpine", + Name: "3.11", + }, + PackageInfos: []*common.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []*common.Package{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + Layer: &common.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + }, + { + Name: "vim-minimal", + Version: "7.4.160", + Release: "5.el7", + Epoch: 2, + Arch: "x86_64", + SrcName: "vim", + SrcVersion: "7.4.160", + SrcRelease: "5.el7", + SrcEpoch: 2, + Layer: &common.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffId: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + }, + { + Name: "node-minimal", + Version: "17.1.0", + Release: "5.el7", + Epoch: 2, + Arch: "x86_64", + SrcName: "node", + SrcVersion: "17.1.0", + SrcRelease: "5.el7", + SrcEpoch: 2, + Layer: nil, + }, + }, + }, + }, + Applications: []*common.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: []*common.Package{ + { + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + }, + { + Name: "guzzlehttp/promises", + Version: "v1.3.1", + }, + }, + }, + }, + }, + }, + }, + wantBlobs: []cachetest.WantBlob{ + { + ID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + BlobInfo: ftypes.BlobInfo{ + SchemaVersion: 1, + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + OpaqueDirs: []string{"etc/"}, + WhiteoutFiles: []string{"etc/hostname"}, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + }, + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: ftypes.Packages{ + { + Name: "binary", + Version: "1.2.3", + Release: "1", + Epoch: 2, + Arch: "x86_64", + SrcName: "src", + SrcVersion: "1.2.3", + SrcRelease: "1", + SrcEpoch: 2, + Layer: ftypes.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + }, + { + Name: "vim-minimal", + Version: "7.4.160", + Release: "5.el7", + Epoch: 2, + Arch: "x86_64", + SrcName: "vim", + SrcVersion: "7.4.160", + SrcRelease: "5.el7", + SrcEpoch: 2, + Layer: ftypes.Layer{ + Digest: "sha256:154ad0735c360b212b167f424d33a62305770a1fcfb6363882f5c436cfbd9812", + DiffID: "sha256:b2a1a2d80bf0c747a4f6b0ca6af5eef23f043fcdb1ed4f3a3e750aef2dc68079", + }, + }, + { + Name: "node-minimal", + Version: "17.1.0", + Release: "5.el7", + Epoch: 2, + Arch: "x86_64", + SrcName: "node", + SrcVersion: "17.1.0", + SrcRelease: "5.el7", + SrcEpoch: 2, + Layer: ftypes.Layer{}, + }, + }, + }, + }, + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "php-app/composer.lock", + Packages: ftypes.Packages{ + { + Name: "guzzlehttp/guzzle", + Version: "6.2.0", + }, + { + Name: "guzzlehttp/promises", + Version: "v1.3.1", + }, + }, + }, + }, + }, + }, + }, + want: &emptypb.Empty{}, + }, + { + name: "sad path", + args: args{ + in: &rpcCache.PutBlobRequest{ + BlobInfo: &rpcCache.BlobInfo{ + SchemaVersion: 1, + }, + }, + }, + setUpCache: func(_ *testing.T) cache.Cache { + return cachetest.NewErrorCache(cachetest.ErrorCacheOptions{ + PutBlob: true, + }) + }, + wantErr: "unable to store layer info in cache", + }, + { + name: "sad path: empty layer info", + args: args{ + in: &rpcCache.PutBlobRequest{}, + }, + wantErr: "empty layer info", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cachetest.NewCache(t, tt.setUpCache) + + s := NewCacheServer(c) + got, err := s.PutBlob(t.Context(), tt.args.in) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got) + cachetest.AssertBlobs(t, c, tt.wantBlobs) + }) + } +} + +func TestCacheServer_MissingBlobs(t *testing.T) { + type args struct { + ctx context.Context + in *rpcCache.MissingBlobsRequest + } + tests := []struct { + name string + args args + setUpCache func(t *testing.T) cache.Cache + want *rpcCache.MissingBlobsResponse + wantErr string + }{ + { + name: "happy path", + args: args{ + in: &rpcCache.MissingBlobsRequest{ + ArtifactId: "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", + BlobIds: []string{ + "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", + "sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5", + }, + }, + }, + setUpCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutArtifact(t.Context(), "sha256:e7d92cdc71feacf90708cb59182d0df1b911f8ae022d29e8e95d75ca6a99776a", ftypes.ArtifactInfo{ + SchemaVersion: ftypes.ArtifactJSONSchemaVersion, + })) + + require.NoError(t, c.PutBlob(t.Context(), "sha256:932da51564135c98a49a34a193d6cd363d8fa4184d957fde16c9d8527b3f3b02", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + })) + + return c + }, + want: &rpcCache.MissingBlobsResponse{ + MissingArtifact: false, + MissingBlobIds: []string{"sha256:dffd9992ca398466a663c87c92cfea2a2db0ae0cf33fcb99da60eec52addbfc5"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := cachetest.NewCache(t, tt.setUpCache) + + s := NewCacheServer(c) + got, err := s.MissingBlobs(tt.args.ctx, tt.args.in) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/rpc/server/testdata/newdb/metadata.json b/pkg/rpc/server/testdata/newdb/metadata.json new file mode 100644 index 000000000000..bb57f21428f4 --- /dev/null +++ b/pkg/rpc/server/testdata/newdb/metadata.json @@ -0,0 +1 @@ +{"Version":2,"NextUpdate":"3000-01-01T0:00:00.0Z","UpdatedAt":"3000-01-01T0:00:00.0Z"} \ No newline at end of file diff --git a/pkg/rpc/server/testdata/newdb/trivy.db b/pkg/rpc/server/testdata/newdb/trivy.db new file mode 100644 index 000000000000..4ee279a13d3a Binary files /dev/null and b/pkg/rpc/server/testdata/newdb/trivy.db differ diff --git a/pkg/rpc/server/testdata/testcache/db/metadata.json b/pkg/rpc/server/testdata/testcache/db/metadata.json new file mode 100644 index 000000000000..e9a4157ed5c5 --- /dev/null +++ b/pkg/rpc/server/testdata/testcache/db/metadata.json @@ -0,0 +1 @@ +{"Version":2,"NextUpdate":"2023-07-20T18:11:37.696263532Z","UpdatedAt":"2023-07-20T12:11:37.696263932Z","DownloadedAt":"2023-07-25T07:01:41.239158Z"} \ No newline at end of file diff --git a/pkg/rpc/server/testdata/testcache/policy/metadata.json b/pkg/rpc/server/testdata/testcache/policy/metadata.json new file mode 100644 index 000000000000..18f6f4801597 --- /dev/null +++ b/pkg/rpc/server/testdata/testcache/policy/metadata.json @@ -0,0 +1 @@ +{"Digest":"sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43","DownloadedAt":"2023-07-23T17:40:33.122462+01:00"} \ No newline at end of file diff --git a/pkg/run.go b/pkg/run.go deleted file mode 100644 index 87d83326a8eb..000000000000 --- a/pkg/run.go +++ /dev/null @@ -1,134 +0,0 @@ -package pkg - -import ( - l "log" - "os" - "strings" - - "github.com/knqyf263/fanal/cache" - - "github.com/knqyf263/trivy/pkg/utils" - - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/report" - "github.com/knqyf263/trivy/pkg/scanner" - "github.com/knqyf263/trivy/pkg/vulnsrc" - - "github.com/urfave/cli" - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/db" - "github.com/knqyf263/trivy/pkg/log" -) - -func Run(c *cli.Context) (err error) { - cliVersion := c.App.Version - - debug := c.Bool("debug") - if err = log.InitLogger(debug); err != nil { - l.Fatal(err) - } - log.Logger.Debugf("cache dir: %s", utils.CacheDir()) - - clean := c.Bool("clean") - if clean { - log.Logger.Info("Cleaning caches...") - if err = cache.Clear(); err != nil { - return xerrors.New("failed to remove image layer cache") - } - if err = os.RemoveAll(utils.CacheDir()); err != nil { - return xerrors.New("failed to remove cache") - } - return nil - } - - args := c.Args() - filePath := c.String("input") - if filePath == "" && len(args) == 0 { - log.Logger.Info(`trivy" requires at least 1 argument or --input option.`) - cli.ShowAppHelpAndExit(c, 1) - } - - utils.Quiet = c.Bool("quiet") - - o := c.String("output") - output := os.Stdout - if o != "" { - if output, err = os.Create(o); err != nil { - return xerrors.Errorf("failed to create an output file: %w", err) - } - } - - var severities []vulnerability.Severity - for _, s := range strings.Split(c.String("severity"), ",") { - severity, err := vulnerability.NewSeverity(s) - if err != nil { - log.Logger.Infof("error in severity option: %s", err) - cli.ShowAppHelpAndExit(c, 1) - } - severities = append(severities, severity) - } - - if c.Bool("refresh") { - log.Logger.Info("Resetting DB...") - if err = db.Reset(); err != nil { - return xerrors.Errorf("error in refresh DB: %w", err) - } - } - - if err = db.Init(); err != nil { - return xerrors.Errorf("error in vulnerability DB initialize: %w", err) - } - - dbVersion := db.GetVersion() - if dbVersion != "" && dbVersion != cliVersion { - log.Logger.Fatal("Detected version update of trivy. Please try again with --refresh option") - } - - if !c.Bool("skip-update") { - if err = vulnsrc.Update(); err != nil { - return xerrors.Errorf("error in vulnerability DB update: %w", err) - } - } - - ignoreUnfixed := c.Bool("ignore-unfixed") - - var imageName string - if filePath == "" { - imageName = args[0] - } - results, err := scanner.ScanImage(imageName, filePath, severities, ignoreUnfixed) - if err != nil { - return xerrors.Errorf("error in image scan: %w", err) - } - - var writer report.Writer - switch c.String("format") { - case "table": - writer = &report.TableWriter{Output: output} - case "json": - writer = &report.JsonWriter{Output: output} - default: - xerrors.New("unknown format") - } - - if err = writer.Write(results); err != nil { - return xerrors.Errorf("failed to write results: %w", err) - } - - if err = db.SetVersion(cliVersion); err != nil { - return xerrors.Errorf("unexpected error: %w", err) - } - - exitCode := c.Int("exit-code") - if exitCode != 0 { - for _, result := range results { - if len(result.Vulnerabilities) > 0 { - os.Exit(exitCode) - } - } - } - - return nil -} diff --git a/pkg/sbom/core/bom.go b/pkg/sbom/core/bom.go new file mode 100644 index 000000000000..6003dfddd061 --- /dev/null +++ b/pkg/sbom/core/bom.go @@ -0,0 +1,419 @@ +package core + +import ( + "slices" + "sort" + + "github.com/samber/lo" + + dtypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/digest" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/uuid" +) + +const ( + TypeFilesystem ComponentType = "filesystem" + TypeRepository ComponentType = "repository" + TypeContainerImage ComponentType = "container_image" + TypeVM ComponentType = "vm" + TypeApplication ComponentType = "application" + TypeLibrary ComponentType = "library" + TypeOS ComponentType = "os" + TypePlatform ComponentType = "platform" + + // Metadata properties + PropertySchemaVersion = "SchemaVersion" + PropertyType = "Type" + PropertyClass = "Class" + + // Image properties + PropertySize = "Size" + PropertyImageID = "ImageID" + PropertyRepoDigest = "RepoDigest" + PropertyDiffID = "DiffID" + PropertyRepoTag = "RepoTag" + PropertyReference = "Reference" + PropertyLabelsPrefix = "Labels" + + // Package properties + PropertyPkgID = "PkgID" + PropertyPkgType = "PkgType" + PropertySrcName = "SrcName" + PropertySrcVersion = "SrcVersion" + PropertySrcRelease = "SrcRelease" + PropertySrcEpoch = "SrcEpoch" + PropertyModularitylabel = "Modularitylabel" + PropertyFilePath = "FilePath" + PropertyLayerDigest = "LayerDigest" + PropertyLayerDiffID = "LayerDiffID" + + // Red Hat packages only. + // A package can use multiple NVR + Arch content sets or fields. + PropertyContentSet = "ContentSet" + PropertyNVR = "NVR" + PropertyArch = "Arch" + + // Relationships + RelationshipDescribes RelationshipType = "describes" + RelationshipContains RelationshipType = "contains" + RelationshipDependsOn RelationshipType = "depends_on" + + ExternalReferenceVEX ExternalReferenceType = "external_reference_vex" +) + +type ComponentType string +type RelationshipType string +type ExternalReferenceType string + +// BOM represents an intermediate representation of a component for SBOM. +type BOM struct { + SerialNumber string + Version int + + rootID uuid.UUID + components map[uuid.UUID]*Component + relationships map[uuid.UUID][]Relationship + + // externalReferences is a list of documents that are referenced from this BOM but hosted elsewhere. + // They are currently used to look for linked VEX documents + externalReferences []ExternalReference + + // Vulnerabilities is a list of vulnerabilities that affect the component. + // CycloneDX: vulnerabilities + // SPDX: N/A + vulnerabilities map[uuid.UUID][]Vulnerability + + // purls is a map of package URLs to UUIDs + // This is used to ensure that each package URL is only represented once in the BOM. + purls map[string][]uuid.UUID + + // parents is a map of parent components to their children + // This field is populated when Options.Parents is set to true. + parents map[uuid.UUID][]uuid.UUID + + // opts is a set of options for the BOM. + opts Options +} + +type Component struct { + // id is the unique identifier of the component for internal use. + // It's transparently generated by UUIDv4 + id uuid.UUID + + // Type is the type of the component + // CycloneDX: component.type + Type ComponentType + + // Root represents the root of the BOM + // Only one root is allowed in a BOM. + // CycloneDX: metadata.component + Root bool + + // Name is the name of the component + // CycloneDX: component.name + // SPDX: package.name + Name string + + // Group is the group of the component + // CycloneDX: component.group + // SPDX: N/A + Group string + + // Version is the version of the component + // CycloneDX: component.version + // SPDX: package.versionInfo + Version string + + // SrcName is the name of the source component + // CycloneDX: N/A + // SPDX: package.sourceInfo + SrcName string + + // SrcVersion is the version of the source component + // CycloneDX: N/A + // SPDX: package.sourceInfo + SrcVersion string + + // SrcFile is the file path where the component is found. + // CycloneDX: N/A + // SPDX: package.sourceInfo + SrcFile string + + // Licenses is a list of licenses that apply to the component + // CycloneDX: component.licenses + // SPDX: package.licenseConcluded, package.licenseDeclared + Licenses []string + + // PkgIdentifier has PURL and BOMRef for the component + // PURL: + // CycloneDX: component.purl + // SPDX: package.externalRefs.referenceLocator + // BOMRef: + // CycloneDX: component.bom-ref + // SPDX: N/A + PkgIdentifier ftypes.PkgIdentifier + + // Supplier is the name of the supplier of the component + // CycloneDX: component.supplier + // SPDX: package.supplier + Supplier string + + // Files is a list of files that are part of the component. + // CycloneDX: component.properties + // SPDX: files + Files []File + + // Properties is a list of key-value pairs that provide additional information about the component + // CycloneDX: component.properties + // SPDX: package.attributionTexts + Properties Properties `hash:"set"` +} + +func (c *Component) ID() uuid.UUID { + return c.id +} + +// Clone creates a deep copy of the Component +func (c *Component) Clone() *Component { + clone := *c // Shallow copy + + // Deep copy slices using slices.Clone + clone.Licenses = slices.Clone(c.Licenses) + clone.Files = slices.Clone(c.Files) + clone.Properties = slices.Clone(c.Properties) + + return &clone +} + +type File struct { + // Path is a path of the file. + // CycloneDX: N/A + // SPDX: package.files[].fileName + Path string + + // Hash is a hash that uniquely identify the component. + // A file can have several digests with different algorithms, like SHA1, SHA256, etc. + // CycloneDX: component.hashes + // SPDX: package.files[].checksums + Digests []digest.Digest +} + +type Property struct { + Name string + Value string + Namespace string + // External indicates if this property came from external source (not generated by Trivy) + // When false (default), Trivy namespace prefix will be applied during marshaling + External bool +} + +type Properties []Property + +func (p Properties) Len() int { return len(p) } +func (p Properties) Less(i, j int) bool { + if p[i].Name != p[j].Name { + return p[i].Name < p[j].Name + } + return p[i].Value < p[j].Value +} +func (p Properties) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +type Relationship struct { + Dependency uuid.UUID + Type RelationshipType +} + +type ExternalReference struct { + URL string + Type ExternalReferenceType +} + +type Vulnerability struct { + dtypes.Vulnerability + ID string + PkgName string + InstalledVersion string + FixedVersion string + PrimaryURL string + DataSource *dtypes.DataSource +} + +type Options struct { + GenerateBOMRef bool // Generate BOMRef for CycloneDX + Parents bool // Hold parent maps +} + +func NewBOM(opts Options) *BOM { + return &BOM{ + components: make(map[uuid.UUID]*Component), + relationships: make(map[uuid.UUID][]Relationship), + vulnerabilities: make(map[uuid.UUID][]Vulnerability), + purls: make(map[string][]uuid.UUID), + parents: make(map[uuid.UUID][]uuid.UUID), + externalReferences: make([]ExternalReference, 0), + opts: opts, + } +} + +func (b *BOM) setupComponent(c *Component) { + if c.id == uuid.Nil { + c.id = uuid.New() + } + if c.PkgIdentifier.PURL != nil { + p := c.PkgIdentifier.PURL.String() + b.purls[p] = append(b.purls[p], c.id) + } + sort.Sort(c.Properties) +} + +func (b *BOM) AddComponent(c *Component) { + b.setupComponent(c) + if c.Root { + b.rootID = c.id + } + b.components[c.id] = c +} + +func (b *BOM) AddRelationship(parent, child *Component, relationshipType RelationshipType) { + // Check the wrong parent to avoid `panic` + if parent == nil { + return + } + if parent.id == uuid.Nil { + b.AddComponent(parent) + } + + if child == nil { + // It is possible that `relationships` already contains this parent. + // Check this to avoid overwriting. + if _, ok := b.relationships[parent.id]; !ok { + b.relationships[parent.id] = nil // Meaning no dependencies + } + return + } + + if child.id == uuid.Nil { + b.AddComponent(child) + } + + b.relationships[parent.id] = append(b.relationships[parent.id], Relationship{ + Type: relationshipType, + Dependency: child.id, + }) + + if b.opts.Parents { + b.parents[child.id] = append(b.parents[child.id], parent.id) + } +} + +func (b *BOM) AddVulnerabilities(c *Component, vulns []Vulnerability) { + if c.id == uuid.Nil { + b.AddComponent(c) + } + if _, ok := b.vulnerabilities[c.id]; ok { + return + } + b.vulnerabilities[c.id] = vulns +} + +func (b *BOM) AddExternalReferences(refs []ExternalReference) { + b.externalReferences = append(b.externalReferences, refs...) +} + +func (b *BOM) Root() *Component { + root, ok := b.components[b.rootID] + if !ok { + return nil + } + if b.opts.GenerateBOMRef { + root.PkgIdentifier.BOMRef = b.bomRef(root) + } + return root +} + +func (b *BOM) Components() map[uuid.UUID]*Component { + // Fill in BOMRefs for components + if b.opts.GenerateBOMRef { + for id, c := range b.components { + b.components[id].PkgIdentifier.BOMRef = b.bomRef(c) + } + } + return b.components +} + +func (b *BOM) Relationships() map[uuid.UUID][]Relationship { + return b.relationships +} + +func (b *BOM) Vulnerabilities() map[uuid.UUID][]Vulnerability { + return b.vulnerabilities +} + +func (b *BOM) ExternalReferences() []ExternalReference { + return b.externalReferences +} + +func (b *BOM) Parents() map[uuid.UUID][]uuid.UUID { + return b.parents +} + +// bomRef returns BOMRef for CycloneDX +// When multiple lock files have the same dependency with the same name and version, PURL in the BOM can conflict. +// In that case, PURL cannot be used as a unique identifier, and UUIDv4 be used for BOMRef. +func (b *BOM) bomRef(c *Component) string { + if c.PkgIdentifier.BOMRef != "" { + return c.PkgIdentifier.BOMRef + } + // Return the UUID of the component if the PURL is not present. + if c.PkgIdentifier.PURL == nil { + return c.id.String() + } + p := c.PkgIdentifier.PURL.String() + + // Return the UUID of the component if the PURL is not unique in the BOM. + if len(b.purls[p]) > 1 { + return c.id.String() + } + return p +} + +// Clone creates a deep copy of the BOM, including all components, relationships, and vulnerabilities. +// This ensures that modifications to the cloned BOM do not affect the original BOM. +func (b *BOM) Clone() *BOM { + return &BOM{ + SerialNumber: b.SerialNumber, + Version: b.Version, + rootID: b.rootID, + + // Deep copy components + components: lo.MapValues(b.components, func(c *Component, _ uuid.UUID) *Component { + return c.Clone() + }), + + // Deep copy relationships + relationships: lo.MapValues(b.relationships, func(rels []Relationship, _ uuid.UUID) []Relationship { + return slices.Clone(rels) + }), + + // Deep copy vulnerabilities + vulnerabilities: lo.MapValues(b.vulnerabilities, func(vulns []Vulnerability, _ uuid.UUID) []Vulnerability { + return slices.Clone(vulns) + }), + + // Deep copy external references + externalReferences: slices.Clone(b.externalReferences), + + // Deep copy purls + purls: lo.MapValues(b.purls, func(ids []uuid.UUID, _ string) []uuid.UUID { + return slices.Clone(ids) + }), + + // Deep copy parents + parents: lo.MapValues(b.parents, func(parentIds []uuid.UUID, _ uuid.UUID) []uuid.UUID { + return slices.Clone(parentIds) + }), + + opts: b.opts, + } +} diff --git a/pkg/sbom/cyclonedx/marshal.go b/pkg/sbom/cyclonedx/marshal.go new file mode 100644 index 000000000000..d8f49039d06f --- /dev/null +++ b/pkg/sbom/cyclonedx/marshal.go @@ -0,0 +1,580 @@ +package cyclonedx + +import ( + "cmp" + "context" + "fmt" + "net/url" + "slices" + "sort" + "strconv" + "strings" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "golang.org/x/xerrors" + + dtypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/licensing/expression" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom/core" + sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +const ( + ToolVendor = "aquasecurity" + ToolName = "trivy" + ToolManufacturer = "Aqua Security Software Ltd." + Namespace = ToolVendor + ":" + ToolName + ":" + + // https://json-schema.org/understanding-json-schema/reference/string.html#dates-and-times + timeLayout = "2006-01-02T15:04:05+00:00" +) + +type Marshaler struct { + appVersion string // Trivy version + bom *core.BOM + componentIDs map[uuid.UUID]string + + logger *log.Logger +} + +func NewMarshaler(version string) Marshaler { + return Marshaler{ + appVersion: version, + logger: log.WithPrefix(log.PrefixCycloneDX), + } +} + +// MarshalReport converts the Trivy report to the CycloneDX format +func (m *Marshaler) MarshalReport(ctx context.Context, report types.Report) (*cdx.BOM, error) { + // Convert into an intermediate representation + bom, err := sbomio.NewEncoder(sbomio.WithBOMRef()).Encode(report) + if err != nil { + return nil, xerrors.Errorf("failed to marshal report: %w", err) + } + + return m.Marshal(ctx, bom) +} + +// Marshal converts the Trivy component to the CycloneDX format +func (m *Marshaler) Marshal(ctx context.Context, bom *core.BOM) (*cdx.BOM, error) { + m.bom = bom + m.componentIDs = make(map[uuid.UUID]string, len(m.bom.Components())) + + cdxBOM := cdx.NewBOM() + cdxBOM.SerialNumber = uuid.New().URN() + cdxBOM.Metadata = m.Metadata(ctx) + + var err error + if cdxBOM.Metadata.Component, err = m.MarshalRoot(); err != nil { + return nil, xerrors.Errorf("failed to marshal component: %w", err) + } + + if cdxBOM.Components, err = m.marshalComponents(); err != nil { + return nil, xerrors.Errorf("failed to marshal components: %w", err) + } + + cdxBOM.Dependencies = m.marshalDependencies() + cdxBOM.Vulnerabilities = m.marshalVulnerabilities() + + return cdxBOM, nil +} + +func (m *Marshaler) Metadata(ctx context.Context) *cdx.Metadata { + return &cdx.Metadata{ + Timestamp: clock.Now(ctx).UTC().Format(timeLayout), + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{ + { + Type: cdx.ComponentTypeApplication, + Group: ToolVendor, + Name: ToolName, + Version: m.appVersion, + Manufacturer: &cdx.OrganizationalEntity{Name: ToolManufacturer}, + }, + }, + }, + } +} + +func (m *Marshaler) MarshalRoot() (*cdx.Component, error) { + root := m.bom.Root() + // Since we reuse the scanned SBOM, the root component (metadata.component) can be empty. + if root == nil { + m.logger.Debug("Root component not found") + return nil, nil + } + return m.MarshalComponent(root) +} + +func (m *Marshaler) MarshalComponent(component *core.Component) (*cdx.Component, error) { + componentType, err := m.componentType(component.Type) + if err != nil { + return nil, xerrors.Errorf("failed to get cdx component type: %w", err) + } + + cdxComponent := &cdx.Component{ + BOMRef: component.PkgIdentifier.BOMRef, + Type: componentType, + Name: component.Name, + Group: component.Group, + Version: component.Version, + PackageURL: m.PackageURL(component.PkgIdentifier.PURL), + Supplier: m.Supplier(component.Supplier), + Hashes: m.Hashes(component.Files), + Licenses: m.Licenses(component.Licenses), + Properties: m.Properties(component.Properties), + } + m.componentIDs[component.ID()] = cdxComponent.BOMRef + + return cdxComponent, nil +} + +func (m *Marshaler) marshalComponents() (*[]cdx.Component, error) { + var cdxComponents []cdx.Component + for _, component := range m.bom.Components() { + if component.Root { + continue + } + c, err := m.MarshalComponent(component) + if err != nil { + return nil, xerrors.Errorf("failed to marshal component: %w", err) + } + cdxComponents = append(cdxComponents, *c) + } + + // CycloneDX requires an empty slice rather than a nil slice + if len(cdxComponents) == 0 { + return &[]cdx.Component{}, nil + } + + // Sort components by BOM-Ref + sort.Slice(cdxComponents, func(i, j int) bool { + return cdxComponents[i].BOMRef < cdxComponents[j].BOMRef + }) + return &cdxComponents, nil +} + +func (m *Marshaler) marshalDependencies() *[]cdx.Dependency { + var dependencies []cdx.Dependency + for key, rels := range m.bom.Relationships() { + ref, ok := m.componentIDs[key] + if !ok { + continue + } + + deps := lo.FilterMap(rels, func(rel core.Relationship, _ int) (string, bool) { + d, ok := m.componentIDs[rel.Dependency] + return d, ok + }) + sort.Strings(deps) + + dependencies = append(dependencies, cdx.Dependency{ + Ref: ref, + Dependencies: &deps, + }) + } + + // Sort dependencies by BOM-Ref + sort.Slice(dependencies, func(i, j int) bool { + return dependencies[i].Ref < dependencies[j].Ref + }) + return &dependencies +} + +func (m *Marshaler) marshalVulnerabilities() *[]cdx.Vulnerability { + vulns := make(map[string]*cdx.Vulnerability) + for id, vv := range m.bom.Vulnerabilities() { + bomRef := m.componentIDs[id] + for _, v := range vv { + // If the same vulnerability affects multiple packages, those packages will be aggregated into one vulnerability. + // Vulnerability component (CVE-2020-26247) + // -> Library component (nokogiri /srv/app1/vendor/bundle/ruby/3.0.0/specifications/nokogiri-1.10.0.gemspec) + // -> Library component (nokogiri /srv/app2/vendor/bundle/ruby/3.0.0/specifications/nokogiri-1.10.0.gemspec) + if vuln, ok := vulns[v.ID]; ok { + *vuln.Affects = append(*vuln.Affects, m.affects(bomRef, v.InstalledVersion)) + if v.FixedVersion != "" { + // new recommendation + rec := fmt.Sprintf("Upgrade %s to version %s", v.PkgName, v.FixedVersion) + // previous recommendations + recs := strings.Split(vuln.Recommendation, "; ") + if !slices.Contains(recs, rec) { + recs = append(recs, rec) + slices.Sort(recs) + vuln.Recommendation = strings.Join(recs, "; ") + } + } + } else { + vulns[v.ID] = m.marshalVulnerability(bomRef, v) + } + } + } + + vulnList := lo.MapToSlice(vulns, func(_ string, value *cdx.Vulnerability) cdx.Vulnerability { + sort.Slice(*value.Affects, func(i, j int) bool { + return (*value.Affects)[i].Ref < (*value.Affects)[j].Ref + }) + return *value + }) + sort.Slice(vulnList, func(i, j int) bool { + return vulnList[i].ID < vulnList[j].ID + }) + return &vulnList +} + +// componentType converts the Trivy component type to the CycloneDX component type +func (*Marshaler) componentType(t core.ComponentType) (cdx.ComponentType, error) { + switch t { + case core.TypeContainerImage, core.TypeVM: + return cdx.ComponentTypeContainer, nil + case core.TypeApplication, core.TypeFilesystem, core.TypeRepository: + return cdx.ComponentTypeApplication, nil + case core.TypeLibrary: + return cdx.ComponentTypeLibrary, nil + case core.TypeOS: + return cdx.ComponentTypeOS, nil + case core.TypePlatform: + return cdx.ComponentTypePlatform, nil + } + return "", xerrors.Errorf("unknown component type: %s", t) +} + +func (*Marshaler) PackageURL(p *packageurl.PackageURL) string { + if p == nil { + return "" + } + return p.String() +} + +func (*Marshaler) Supplier(supplier string) *cdx.OrganizationalEntity { + if supplier == "" { + return nil + } + return &cdx.OrganizationalEntity{ + Name: supplier, + } +} + +func (m *Marshaler) Hashes(files []core.File) *[]cdx.Hash { + digests := lo.FlatMap(files, func(file core.File, _ int) []digest.Digest { + return file.Digests + }) + if len(digests) == 0 { + return nil + } + + var cdxHashes []cdx.Hash + for _, d := range digests { + var alg cdx.HashAlgorithm + switch d.Algorithm() { + case digest.SHA1: + alg = cdx.HashAlgoSHA1 + case digest.SHA256: + alg = cdx.HashAlgoSHA256 + case digest.SHA512: + alg = cdx.HashAlgoSHA512 + case digest.MD5: + alg = cdx.HashAlgoMD5 + default: + m.logger.Debug("Unable to convert algorithm to CycloneDX format", log.Any("alg", d.Algorithm())) + continue + } + + cdxHashes = append(cdxHashes, cdx.Hash{ + Algorithm: alg, + Value: d.Encoded(), + }) + } + return &cdxHashes +} + +func (m *Marshaler) Licenses(licenses []string) *cdx.Licenses { + licenses = lo.Compact(licenses) + if len(licenses) == 0 { + return nil + } + return m.normalizeLicenses(licenses) +} + +func (m *Marshaler) normalizeLicenses(licenses []string) *cdx.Licenses { + expressions := xslices.Map(licenses, m.normalizeLicense) + // Check if all licenses are valid SPDX expressions + allValidSPDX := lo.EveryBy(expressions, func(expr expression.Expression) bool { + return expr.IsSPDXExpression() + }) + + // Check if at least one is a CompoundExpr + hasCompoundExpr := lo.ContainsBy(expressions, func(expr expression.Expression) bool { + _, isCompound := expr.(expression.CompoundExpr) + return isCompound + }) + + // If all are valid SPDX AND at least one contains CompoundExpr, combine into single Expression + if allValidSPDX && hasCompoundExpr { + exprStrs := xslices.Map(expressions, expression.Expression.String) + return &cdx.Licenses{{Expression: strings.Join(exprStrs, " AND ")}} + } + + // Otherwise use individual LicenseChoice entries with license.id or license.name + choices := xslices.Map(expressions, func(expr expression.Expression) cdx.LicenseChoice { + if s, ok := expr.(expression.SimpleExpr); ok && s.IsSPDXExpression() { + // Use license.id for valid SPDX ID (e.g., "MIT", "Apache-2.0") + return cdx.LicenseChoice{License: &cdx.License{ID: s.String()}} + } + // Use license.name for everything else (invalid SPDX ID, SPDX expression, etc.) + return cdx.LicenseChoice{License: &cdx.License{Name: expr.String()}} + }) + return lo.ToPtr(cdx.Licenses(choices)) +} + +func (m *Marshaler) normalizeLicense(license string) expression.Expression { + // Save text license as licenseChoice.license.name + if after, ok := strings.CutPrefix(license, licensing.LicenseTextPrefix); ok { + return expression.SimpleExpr{ + License: after, + } + } + + // e.g. GPL-3.0-with-autoconf-exception + license = strings.ReplaceAll(license, "-with-", " WITH ") + license = strings.ReplaceAll(license, "-WITH-", " WITH ") + + normalizedLicenses, err := expression.Normalize(license, licensing.NormalizeLicenseExpression, expression.NormalizeForSPDX) + if err != nil { + // Not fail on the invalid license + m.logger.Warn("Unable to marshal SPDX licenses", log.String("license", license)) + return expression.SimpleExpr{License: license} + } + + return normalizedLicenses +} + +func (*Marshaler) Properties(properties []core.Property) *[]cdx.Property { + cdxProps := make([]cdx.Property, 0, len(properties)) + for _, property := range properties { + namespace := cmp.Or(property.Namespace, Namespace) + + // External property preserves original name, Trivy property gets namespace prefix + name := lo.Ternary(property.External, property.Name, namespace+property.Name) + + cdxProps = append(cdxProps, cdx.Property{ + Name: name, + Value: property.Value, + }) + } + sort.Slice(cdxProps, func(i, j int) bool { + if cdxProps[i].Name != cdxProps[j].Name { + return cdxProps[i].Name < cdxProps[j].Name + } + return cdxProps[i].Value < cdxProps[j].Value + }) + return &cdxProps +} + +func (*Marshaler) affects(ref, version string) cdx.Affects { + return cdx.Affects{ + Ref: ref, + Range: &[]cdx.AffectedVersions{ + { + Version: version, + Status: cdx.VulnerabilityStatusAffected, + // "AffectedVersions.Range" is not included, because it does not exist in DetectedVulnerability. + }, + }, + } +} + +func (*Marshaler) advisories(refs []string) *[]cdx.Advisory { + refs = lo.Uniq(refs) + advs := lo.FilterMap(refs, func(ref string, _ int) (cdx.Advisory, bool) { + // There are cases when `ref` contains extra info + // But we need to use only URL. + // cf. https://github.com/aquasecurity/trivy/issues/6801 + ref = trimNonUrlInfo(ref) + return cdx.Advisory{URL: ref}, ref != "" + }) + + // cyclonedx converts link to empty `[]cdx.Advisory` to `null` + // `bom-1.5.schema.json` doesn't support this - `Invalid type. Expected: array, given: null` + // we need to explicitly set `nil` for empty `refs` slice + if len(advs) == 0 { + return nil + } + + return &advs +} + +// trimNonUrlInfo returns first valid URL. +func trimNonUrlInfo(ref string) string { + ss := strings.SplitSeq(ref, " ") + for s := range ss { + if u, err := url.Parse(s); err == nil && u.Scheme != "" && u.Host != "" { + return s + } + } + return "" +} + +func (m *Marshaler) marshalVulnerability(bomRef string, vuln core.Vulnerability) *cdx.Vulnerability { + v := &cdx.Vulnerability{ + ID: vuln.ID, + Source: m.source(vuln.DataSource), + Ratings: m.ratings(vuln), + CWEs: m.cwes(vuln.CweIDs), + Description: vuln.Description, + Advisories: m.advisories(append([]string{vuln.PrimaryURL}, vuln.References...)), + } + if vuln.FixedVersion != "" { + v.Recommendation = fmt.Sprintf("Upgrade %s to version %s", vuln.PkgName, vuln.FixedVersion) + } + if vuln.PublishedDate != nil { + v.Published = vuln.PublishedDate.Format(timeLayout) + } + if vuln.LastModifiedDate != nil { + v.Updated = vuln.LastModifiedDate.Format(timeLayout) + } + + v.Affects = &[]cdx.Affects{m.affects(bomRef, vuln.InstalledVersion)} + + return v +} + +func (*Marshaler) source(source *dtypes.DataSource) *cdx.Source { + if source == nil { + return nil + } + + return &cdx.Source{ + Name: string(source.ID), + URL: source.URL, + } +} + +func (m *Marshaler) cwes(cweIDs []string) *[]int { + // to skip cdx.Vulnerability.CWEs when generating json + // we should return 'clear' nil without 'type' interface part + if cweIDs == nil { + return nil + } + var ret []int + for _, cweID := range cweIDs { + number, err := strconv.Atoi(strings.TrimPrefix(strings.ToLower(cweID), "cwe-")) + if err != nil { + m.logger.Debug("CWE-ID parse error", log.Err(err)) + continue + } + ret = append(ret, number) + } + return &ret +} + +func (m *Marshaler) ratings(vuln core.Vulnerability) *[]cdx.VulnerabilityRating { + rates := make([]cdx.VulnerabilityRating, 0) // nolint:gocritic // To export an empty array in JSON + for sourceID, severity := range vuln.VendorSeverity { + // When the vendor also provides CVSS score/vector + if cvss, ok := vuln.CVSS[sourceID]; ok { + if cvss.V2Score != 0 || cvss.V2Vector != "" { + rates = append(rates, m.ratingV2(sourceID, severity, cvss)) + } + if cvss.V3Score != 0 || cvss.V3Vector != "" { + rates = append(rates, m.ratingV3(sourceID, severity, cvss)) + } + } else { // When the vendor provides only severity + rate := cdx.VulnerabilityRating{ + Source: &cdx.Source{ + Name: string(sourceID), + }, + Severity: m.severity(severity), + } + rates = append(rates, rate) + } + } + + // For consistency + sort.Slice(rates, func(i, j int) bool { + if rates[i].Source.Name != rates[j].Source.Name { + return rates[i].Source.Name < rates[j].Source.Name + } + if rates[i].Method != rates[j].Method { + return rates[i].Method < rates[j].Method + } + if rates[i].Score != nil && rates[j].Score != nil { + return *rates[i].Score < *rates[j].Score + } + return rates[i].Vector < rates[j].Vector + }) + return &rates +} + +func (m *Marshaler) ratingV2(sourceID dtypes.SourceID, severity dtypes.Severity, cvss dtypes.CVSS) cdx.VulnerabilityRating { + cdxSeverity := m.severity(severity) + + // Trivy keeps only CVSSv3 severity for NVD. + // The CVSSv2 severity must be calculated according to CVSSv2 score. + if sourceID == vulnerability.NVD { + cdxSeverity = m.nvdSeverityV2(cvss.V2Score) + } + return cdx.VulnerabilityRating{ + Source: &cdx.Source{ + Name: string(sourceID), + }, + Score: &cvss.V2Score, + Method: cdx.ScoringMethodCVSSv2, + Severity: cdxSeverity, + Vector: cvss.V2Vector, + } +} + +func (m *Marshaler) ratingV3(sourceID dtypes.SourceID, severity dtypes.Severity, cvss dtypes.CVSS) cdx.VulnerabilityRating { + rate := cdx.VulnerabilityRating{ + Source: &cdx.Source{ + Name: string(sourceID), + }, + Score: &cvss.V3Score, + Method: cdx.ScoringMethodCVSSv3, + Severity: m.severity(severity), + Vector: cvss.V3Vector, + } + if strings.HasPrefix(cvss.V3Vector, "CVSS:3.1") { + rate.Method = cdx.ScoringMethodCVSSv31 + } + return rate +} + +// severity converts the Trivy severity to the CycloneDX severity +func (*Marshaler) severity(s dtypes.Severity) cdx.Severity { + switch s { + case dtypes.SeverityLow: + return cdx.SeverityLow + case dtypes.SeverityMedium: + return cdx.SeverityMedium + case dtypes.SeverityHigh: + return cdx.SeverityHigh + case dtypes.SeverityCritical: + return cdx.SeverityCritical + default: + return cdx.SeverityUnknown + } +} + +func (*Marshaler) nvdSeverityV2(score float64) cdx.Severity { + // cf. https://nvd.nist.gov/vuln-metrics/cvss + switch { + case score < 4.0: + return cdx.SeverityInfo + case 4.0 <= score && score < 7.0: + return cdx.SeverityMedium + case 7.0 <= score: + return cdx.SeverityHigh + } + return cdx.SeverityUnknown +} diff --git a/pkg/sbom/cyclonedx/marshal_test.go b/pkg/sbom/cyclonedx/marshal_test.go new file mode 100644 index 000000000000..11105e07e698 --- /dev/null +++ b/pkg/sbom/cyclonedx/marshal_test.go @@ -0,0 +1,2430 @@ +package cyclonedx_test + +import ( + "testing" + "time" + + cdx "github.com/CycloneDX/cyclonedx-go" + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dtypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" +) + +var ( + binutilsIdentifier = ftypes.PkgIdentifier{ + UID: "7CC457C23685235A", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "centos", + Name: "binutils", + Version: "2.30-93.el8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "aarch64", + }, + { + Key: "distro", + Value: "centos-8.3.2011", + }, + }, + }, + } + + actionpack700Identifier = ftypes.PkgIdentifier{ + UID: "DFF5FF40889105B2", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actionpack", + Version: "7.0.0", + }, + } + + actionpack701Identifier = ftypes.PkgIdentifier{ + UID: "6B0A6392BAA7D584", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actionpack", + Version: "7.0.1", + }, + } +) + +var ( + // Add root component + rootComponent = &core.Component{ + Root: true, + Type: core.TypeApplication, + Name: "jackson-databind-2.13.4.1.jar", + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "aff65b54-6009-4c32-968d-748949ef46e8", + }, + Properties: []core.Property{ + { + Name: "SchemaVersion", + Value: "2", + }, + }, + } + + jacksonComponent = &core.Component{ + Type: core.TypeLibrary, + Name: "jackson-databind", + Group: "com.fasterxml.jackson.core", + Version: "2.13.4.1", + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4.1", + }, + }, + Properties: []core.Property{ + { + Name: core.PropertyPkgType, + Value: "jar", + }, + { + Name: core.PropertyFilePath, + Value: "jackson-databind-2.13.4.1.jar", + }, + }, + } +) + +func testSBOM() *core.BOM { + sbom := core.NewBOM(core.Options{GenerateBOMRef: true}) + + // Add root component + sbom.AddComponent(rootComponent) + + // Add the jackson-databind component that matches scan results + sbom.AddComponent(jacksonComponent) + + // Establish relationships + sbom.AddRelationship(rootComponent, jacksonComponent, core.RelationshipContains) + sbom.AddRelationship(jacksonComponent, nil, core.RelationshipDependsOn) + return sbom +} + +func testSBOMWithoutRoot() *core.BOM { + sbom := core.NewBOM(core.Options{GenerateBOMRef: true}) + + // Add the jackson-databind component that matches scan results + sbom.AddComponent(jacksonComponent) + + // Establish relationships + sbom.AddRelationship(jacksonComponent, nil, core.RelationshipDependsOn) + return sbom +} + +func TestMarshaler_MarshalReport(t *testing.T) { + + tests := []struct { + name string + inputReport types.Report + want *cdx.BOM + }{ + { + name: "happy path for container scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "rails:latest", + ArtifactType: ftypes.TypeContainerImage, + Metadata: types.Metadata{ + Size: 1024, + OS: &ftypes.OS{ + Family: ftypes.CentOS, + Name: "8.3.2011", + Eosl: true, + }, + ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + RepoTags: []string{"rails:latest"}, + DiffIDs: []string{"sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a"}, + RepoDigests: []string{"rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177"}, + ImageConfig: v1.ConfigFile{ + Architecture: "arm64", + Config: v1.Config{ + Labels: map[string]string{ + "vendor": "aquasecurity", + }, + }, + }, + }, + Results: types.Results{ + { + Target: "rails:latest (centos 8.3.2011)", + Class: types.ClassOSPkg, + Type: ftypes.CentOS, + Packages: []ftypes.Package{ + { + ID: "binutils@2.30-93.el8", + Name: "binutils", + Version: "2.30", + Release: "93.el8", + Epoch: 0, + Arch: "aarch64", + Identifier: binutilsIdentifier, + SrcName: "binutils", + SrcVersion: "2.30", + SrcRelease: "93.el8", + SrcEpoch: 0, + Modularitylabel: "", + Licenses: []string{"GPLv3+"}, + Maintainer: "CentOS", + Digest: "md5:7459cec61bb4d1b0ca8107e25e0dd005", + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2018-20623", + PkgID: "binutils@2.30-93.el8", + PkgName: "binutils", + InstalledVersion: "2.30-93.el8", + Layer: ftypes.Layer{ + DiffID: "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a", + }, + SeveritySource: vulnerability.RedHatOVAL, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2018-20623", + DataSource: &dtypes.DataSource{ + ID: vulnerability.RedHatOVAL, + Name: "Red Hat OVAL v2", + URL: "https://www.redhat.com/security/data/oval/v2/", + }, + PkgIdentifier: binutilsIdentifier, + Vulnerability: dtypes.Vulnerability{ + Title: "binutils: Use-after-free in the error function", + Description: "In GNU Binutils 2.31.1, there is a use-after-free in the error function in elfcomm.c when called from the process_archive function in readelf.c via a crafted ELF file.", + Severity: dtypes.SeverityMedium.String(), + VendorSeverity: dtypes.VendorSeverity{ + vulnerability.NVD: dtypes.SeverityMedium, + vulnerability.RedHatOVAL: dtypes.SeverityMedium, + }, + CweIDs: []string{"CWE-416"}, + CVSS: dtypes.VendorCVSS{ + vulnerability.NVD: dtypes.CVSS{ + V2Vector: "AV:N/AC:M/Au:N/C:N/I:N/A:P", + V3Vector: "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H", + V2Score: 4.3, + V3Score: 5.5, + }, + vulnerability.RedHatOVAL: dtypes.CVSS{ + V3Vector: "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L", + V3Score: 5.3, + }, + }, + PublishedDate: lo.ToPtr(time.Date(2018, 12, 31, 19, 29, 0, 0, time.UTC)), + LastModifiedDate: lo.ToPtr(time.Date(2019, 10, 31, 1, 15, 0, 0, time.UTC)), + }, + }, + }, + }, + { + Target: "app/subproject/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + // This package conflicts + ID: "actionpack@7.0.0", + Name: "actionpack", + Version: "7.0.0", + Identifier: actionpack700Identifier, + Indirect: false, + }, + { + ID: "actioncontroller@7.0.0", + Name: "actioncontroller", + Version: "7.0.0", + Identifier: ftypes.PkgIdentifier{ + UID: "41ED2619CA718170", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actioncontroller", + Version: "7.0.0", + }, + }, + Indirect: false, + DependsOn: []string{ + "actionpack@7.0.0", + }, + }, + }, + }, + { + Target: "app/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + // This package conflicts + ID: "actionpack@7.0.0", + Name: "actionpack", + Version: "7.0.0", + Identifier: actionpack700Identifier, + }, + }, + }, + { + Target: "app/datacollector.deps.json", + Class: types.ClassLangPkg, + Type: ftypes.DotNetCore, + Packages: []ftypes.Package{ + { + ID: "Newtonsoft.Json@9.0.1", + Name: "Newtonsoft.Json", + Version: "9.0.1", + Identifier: ftypes.PkgIdentifier{ + UID: "94AB97F672F97AFB", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNuget, + Name: "Newtonsoft.Json", + Version: "9.0.1", + }, + }, + }, + }, + }, + { + Target: "usr/local/bin/tfsec", + Class: types.ClassLangPkg, + Type: ftypes.GoBinary, + Packages: []ftypes.Package{ + { + Name: "golang.org/x/crypto", + Version: "v0.0.0-20210421170649-83a5a9bb288b", + Identifier: ftypes.PkgIdentifier{ + UID: "B7183ED2CF7EB470", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "golang.org/x", + Name: "crypto", + Version: "v0.0.0-20210421170649-83a5a9bb288b", + }, + }, + }, + // dependency has been replaced with local directory + { + Name: "./api", + Version: "", + }, + }, + }, + }, + }, + want: &cdx.BOM{ + XMLNS: "http://cyclonedx.org/schema/bom/1.6", + BOMFormat: "CycloneDX", + SpecVersion: cdx.SpecVersion1_6, + JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json", + SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000014", + Version: 1, + Metadata: &cdx.Metadata{ + Timestamp: "2021-08-25T12:20:30+00:00", + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{ + { + Type: cdx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + Manufacturer: &cdx.OrganizationalEntity{ + Name: "Aqua Security Software Ltd.", + }, + }, + }, + }, + Component: &cdx.Component{ + Type: cdx.ComponentTypeContainer, + BOMRef: "pkg:oci/rails@sha256%3Aa27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?arch=arm64&repository_url=index.docker.io%2Flibrary%2Frails", + PackageURL: "pkg:oci/rails@sha256%3Aa27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?arch=arm64&repository_url=index.docker.io%2Flibrary%2Frails", + Name: "rails:latest", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:DiffID", + Value: "sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a", + }, + { + Name: "aquasecurity:trivy:ImageID", + Value: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + }, + { + Name: "aquasecurity:trivy:Labels:vendor", + Value: "aquasecurity", + }, + { + Name: "aquasecurity:trivy:RepoDigest", + Value: "rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177", + }, + { + Name: "aquasecurity:trivy:RepoTag", + Value: "rails:latest", + }, + { + Name: "aquasecurity:trivy:SchemaVersion", + Value: "2", + }, + { + Name: "aquasecurity:trivy:Size", + Value: "1024", + }, + }, + }, + }, + Components: &[]cdx.Component{ + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000002", + Type: cdx.ComponentTypeOS, + Name: "centos", + Version: "8.3.2011", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:Class", + Value: "os-pkgs", + }, + { + Name: "aquasecurity:trivy:Type", + Value: "centos", + }, + }, + }, + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000004", + Type: cdx.ComponentTypeApplication, + Name: "app/subproject/Gemfile.lock", + Version: "", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:Class", + Value: "lang-pkgs", + }, + { + Name: "aquasecurity:trivy:Type", + Value: "bundler", + }, + }, + }, + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000005", + Type: cdx.ComponentTypeLibrary, + Name: "actionpack", + Version: "7.0.0", + PackageURL: "pkg:gem/actionpack@7.0.0", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgID", + Value: "actionpack@7.0.0", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "bundler", + }, + }, + }, + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000007", + Type: cdx.ComponentTypeApplication, + Name: "app/Gemfile.lock", + Version: "", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:Class", + Value: "lang-pkgs", + }, + { + Name: "aquasecurity:trivy:Type", + Value: "bundler", + }, + }, + }, + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000008", + Type: cdx.ComponentTypeLibrary, + Name: "actionpack", + Version: "7.0.0", + PackageURL: "pkg:gem/actionpack@7.0.0", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgID", + Value: "actionpack@7.0.0", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "bundler", + }, + }, + }, + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000009", + Type: cdx.ComponentTypeApplication, + Name: "app/datacollector.deps.json", + Version: "", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:Class", + Value: "lang-pkgs", + }, + { + Name: "aquasecurity:trivy:Type", + Value: "dotnet-core", + }, + }, + }, + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000011", + Type: cdx.ComponentTypeApplication, + Name: "usr/local/bin/tfsec", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:Class", + Value: "lang-pkgs", + }, + { + Name: "aquasecurity:trivy:Type", + Value: "gobinary", + }, + }, + }, + { + // Use UUID for local Go packages + BOMRef: "3ff14136-e09f-4df9-80ea-000000000013", + Type: cdx.ComponentTypeLibrary, + Name: "./api", + Version: "", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "gobinary", + }, + }, + }, + { + BOMRef: "pkg:gem/actioncontroller@7.0.0", + Type: cdx.ComponentTypeLibrary, + Name: "actioncontroller", + Version: "7.0.0", + PackageURL: "pkg:gem/actioncontroller@7.0.0", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgID", + Value: "actioncontroller@7.0.0", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "bundler", + }, + }, + }, + { + BOMRef: "pkg:golang/golang.org/x/crypto@v0.0.0-20210421170649-83a5a9bb288b", + Type: cdx.ComponentTypeLibrary, + Name: "golang.org/x/crypto", + Version: "v0.0.0-20210421170649-83a5a9bb288b", + PackageURL: "pkg:golang/golang.org/x/crypto@v0.0.0-20210421170649-83a5a9bb288b", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "gobinary", + }, + }, + }, + { + BOMRef: "pkg:nuget/Newtonsoft.Json@9.0.1", + Type: cdx.ComponentTypeLibrary, + Name: "Newtonsoft.Json", + Version: "9.0.1", + PackageURL: "pkg:nuget/Newtonsoft.Json@9.0.1", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgID", + Value: "Newtonsoft.Json@9.0.1", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "dotnet-core", + }, + }, + }, + { + BOMRef: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + Type: cdx.ComponentTypeLibrary, + Name: "binutils", + Version: "2.30-93.el8", + Licenses: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + ID: "GPL-3.0-or-later", + }, + }, + }, + PackageURL: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + Supplier: &cdx.OrganizationalEntity{ + Name: "CentOS", + }, + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgID", + Value: "binutils@2.30-93.el8", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "centos", + }, + { + Name: "aquasecurity:trivy:SrcName", + Value: "binutils", + }, + { + Name: "aquasecurity:trivy:SrcRelease", + Value: "93.el8", + }, + { + Name: "aquasecurity:trivy:SrcVersion", + Value: "2.30", + }, + }, + Hashes: &[]cdx.Hash{ + { + Algorithm: cdx.HashAlgoMD5, + Value: "7459cec61bb4d1b0ca8107e25e0dd005", + }, + }, + }, + }, + Dependencies: &[]cdx.Dependency{ + { + Ref: "3ff14136-e09f-4df9-80ea-000000000002", + Dependencies: &[]string{ + "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + }, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000004", + Dependencies: &[]string{ + "pkg:gem/actioncontroller@7.0.0", + }, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000005", + Dependencies: &[]string{}, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000007", + Dependencies: &[]string{ + "3ff14136-e09f-4df9-80ea-000000000008", + }, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000008", + Dependencies: &[]string{}, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000009", + Dependencies: &[]string{ + "pkg:nuget/Newtonsoft.Json@9.0.1", + }, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000011", + Dependencies: &[]string{ + "3ff14136-e09f-4df9-80ea-000000000013", + "pkg:golang/golang.org/x/crypto@v0.0.0-20210421170649-83a5a9bb288b", + }, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000013", + Dependencies: lo.ToPtr([]string{}), + }, + { + Ref: "pkg:gem/actioncontroller@7.0.0", + Dependencies: &[]string{ + "3ff14136-e09f-4df9-80ea-000000000005", + }, + }, + { + Ref: "pkg:golang/golang.org/x/crypto@v0.0.0-20210421170649-83a5a9bb288b", + Dependencies: lo.ToPtr([]string{}), + }, + { + Ref: "pkg:nuget/Newtonsoft.Json@9.0.1", + Dependencies: lo.ToPtr([]string{}), + }, + { + Ref: "pkg:oci/rails@sha256%3Aa27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?arch=arm64&repository_url=index.docker.io%2Flibrary%2Frails", + Dependencies: &[]string{ + "3ff14136-e09f-4df9-80ea-000000000002", + "3ff14136-e09f-4df9-80ea-000000000004", + "3ff14136-e09f-4df9-80ea-000000000007", + "3ff14136-e09f-4df9-80ea-000000000009", + "3ff14136-e09f-4df9-80ea-000000000011", + }, + }, + { + Ref: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + Dependencies: lo.ToPtr([]string{}), + }, + }, + Vulnerabilities: &[]cdx.Vulnerability{ + { + ID: "CVE-2018-20623", + Source: &cdx.Source{ + Name: string(vulnerability.RedHatOVAL), + URL: "https://www.redhat.com/security/data/oval/v2/", + }, + Ratings: &[]cdx.VulnerabilityRating{ + { + Source: &cdx.Source{ + Name: string(vulnerability.NVD), + URL: "", + }, + Score: lo.ToPtr(4.3), + Severity: cdx.SeverityMedium, + Method: cdx.ScoringMethodCVSSv2, + Vector: "AV:N/AC:M/Au:N/C:N/I:N/A:P", + }, + { + Source: &cdx.Source{ + Name: string(vulnerability.NVD), + URL: "", + }, + Score: lo.ToPtr(5.5), + Severity: cdx.SeverityMedium, + Method: cdx.ScoringMethodCVSSv3, + Vector: "CVSS:3.0/AV:L/AC:L/PR:N/UI:R/S:U/C:N/I:N/A:H", + }, + { + Source: &cdx.Source{ + Name: string(vulnerability.RedHatOVAL), + URL: "", + }, + Score: lo.ToPtr(5.3), + Severity: cdx.SeverityMedium, + Method: cdx.ScoringMethodCVSSv3, + Vector: "CVSS:3.0/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:L", + }, + }, + CWEs: &[]int{ + 416, + }, + Description: "In GNU Binutils 2.31.1, there is a use-after-free in the error function in elfcomm.c when called from the process_archive function in readelf.c via a crafted ELF file.", + Published: "2018-12-31T19:29:00+00:00", + Updated: "2019-10-31T01:15:00+00:00", + Advisories: &[]cdx.Advisory{ + { + URL: "https://avd.aquasec.com/nvd/cve-2018-20623", + }, + }, + Affects: &[]cdx.Affects{ + { + Ref: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + Range: &[]cdx.AffectedVersions{ + { + Version: "2.30-93.el8", + Status: cdx.VulnerabilityStatusAffected, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for local container scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "centos:latest", + ArtifactType: ftypes.TypeContainerImage, + Metadata: types.Metadata{ + Size: 1024, + OS: &ftypes.OS{ + Family: ftypes.CentOS, + Name: "8.3.2011", + Eosl: true, + }, + ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + RepoTags: []string{"centos:latest"}, + RepoDigests: []string{}, + ImageConfig: v1.ConfigFile{ + Architecture: "arm64", + }, + }, + Results: types.Results{ + { + Target: "centos:latest (centos 8.3.2011)", + Class: types.ClassOSPkg, + Type: ftypes.CentOS, + Packages: []ftypes.Package{ + { + ID: "acl@2.2.53-1.el8", + Name: "acl", + Version: "2.2.53", + Release: "1.el8", + Epoch: 1, + Arch: "aarch64", + Identifier: ftypes.PkgIdentifier{ + UID: "2FF7A09FA4E6AA2E", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "centos", + Name: "acl", + Version: "2.2.53-1.el8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "aarch64", + }, + { + Key: "distro", + Value: "centos-8.3.2011", + }, + { + Key: "epoch", + Value: "1", + }, + }, + }, + }, + SrcName: "acl", + SrcVersion: "2.2.53", + SrcRelease: "1.el8", + SrcEpoch: 1, + Modularitylabel: "", + Licenses: []string{"GPLv2+"}, + DependsOn: []string{ + "glibc@2.28-151.el8", + }, + Digest: "md5:483792b8b5f9eb8be7dc4407733118d0", + }, + { + ID: "glibc@2.28-151.el8", + Name: "glibc", + Version: "2.28", + Release: "151.el8", + Epoch: 0, + Arch: "aarch64", + Identifier: ftypes.PkgIdentifier{ + UID: "2DCAB94016E57F8E", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "centos", + Name: "glibc", + Version: "2.28-151.el8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "aarch64", + }, + { + Key: "distro", + Value: "centos-8.3.2011", + }, + }, + }, + }, + SrcName: "glibc", + SrcVersion: "2.28", + SrcRelease: "151.el8", + SrcEpoch: 0, + Modularitylabel: "", + Licenses: []string{"GPLv2+"}, + Digest: "md5:969b3c9231627022f8bf7ac70de807a1", + }, + }, + }, + { + Target: "Ruby", + Class: types.ClassLangPkg, + Type: ftypes.GemSpec, + Packages: []ftypes.Package{ + { + ID: "actionpack@7.0.0", + Name: "actionpack", + Version: "7.0.0", + Identifier: actionpack700Identifier, + Layer: ftypes.Layer{ + DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + FilePath: "tools/project-john/specifications/actionpack.gemspec", + }, + { + ID: "actionpack@7.0.1", + Name: "actionpack", + Version: "7.0.1", + Identifier: actionpack701Identifier, + Layer: ftypes.Layer{ + DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + FilePath: "tools/project-doe/specifications/actionpack.gemspec", + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-23633", + PkgID: "actionpack@7.0.0", + PkgName: "actionpack", + PkgPath: "tools/project-john/specifications/actionpack.gemspec", + PkgIdentifier: actionpack700Identifier, + InstalledVersion: "7.0.0", + FixedVersion: "~> 5.2.6, >= 5.2.6.2, ~> 6.0.4, >= 6.0.4.6, ~> 6.1.4, >= 6.1.4.6, >= 7.0.2.2", + SeveritySource: vulnerability.RubySec, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2022-23633", + DataSource: &dtypes.DataSource{ + ID: vulnerability.RubySec, + Name: "Ruby Advisory Database", + URL: "https://github.com/rubysec/ruby-advisory-db", + }, + Vulnerability: dtypes.Vulnerability{ + Title: "rubygem-actionpack: information leak between requests", + Description: "Action Pack is a framework for handling and responding to web requests. Under certain circumstances response bodies will not be closed. In the event a response is *not* notified of a `close`, `ActionDispatch::Executor` will not know to reset thread local state for the next request. This can lead to data being leaked to subsequent requests.This has been fixed in Rails 7.0.2.1, 6.1.4.5, 6.0.4.5, and 5.2.6.1. Upgrading is highly recommended, but to work around this problem a middleware described in GHSA-wh98-p28r-vrc9 can be used.", + Severity: dtypes.SeverityMedium.String(), + VendorSeverity: dtypes.VendorSeverity{ + vulnerability.NVD: dtypes.SeverityMedium, + vulnerability.RedHat: dtypes.SeverityLow, + vulnerability.RubySec: dtypes.SeverityHigh, + }, + CVSS: dtypes.VendorCVSS{ + vulnerability.NVD: dtypes.CVSS{ + V2Vector: "AV:N/AC:L/Au:N/C:C/I:P/A:C", + V3Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + V2Score: 9.7, + V3Score: 5.9, + }, + vulnerability.RedHat: dtypes.CVSS{ + V3Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + V3Score: 5.9, + }, + }, + References: []string{ + " extraPrefix http://www.openwall.com/lists/oss-security/2022/02/11/5", + "https://access.redhat.com/security/cve/CVE-2022-23633 (extra suffix)", + }, + PublishedDate: lo.ToPtr(time.Date(2022, 2, 11, 21, 15, 0, 0, time.UTC)), + LastModifiedDate: lo.ToPtr(time.Date(2022, 2, 22, 21, 47, 0, 0, time.UTC)), + }, + }, + { + VulnerabilityID: "CVE-2022-23633", + PkgID: "actionpack@7.0.1", + PkgName: "actionpack", + PkgPath: "tools/project-doe/specifications/actionpack.gemspec", + PkgIdentifier: actionpack701Identifier, + InstalledVersion: "7.0.1", + FixedVersion: "~> 5.2.6, >= 5.2.6.2, ~> 6.0.4, >= 6.0.4.6, ~> 6.1.4, >= 6.1.4.6, >= 7.0.2.2", + SeveritySource: vulnerability.RubySec, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2022-23633", + DataSource: &dtypes.DataSource{ + ID: vulnerability.RubySec, + Name: "Ruby Advisory Database", + URL: "https://github.com/rubysec/ruby-advisory-db", + }, + Vulnerability: dtypes.Vulnerability{ + Title: "rubygem-actionpack: information leak between requests", + Description: "Action Pack is a framework for handling and responding to web requests. Under certain circumstances response bodies will not be closed. In the event a response is *not* notified of a `close`, `ActionDispatch::Executor` will not know to reset thread local state for the next request. This can lead to data being leaked to subsequent requests.This has been fixed in Rails 7.0.2.1, 6.1.4.5, 6.0.4.5, and 5.2.6.1. Upgrading is highly recommended, but to work around this problem a middleware described in GHSA-wh98-p28r-vrc9 can be used.", + Severity: dtypes.SeverityMedium.String(), + VendorSeverity: dtypes.VendorSeverity{ + vulnerability.NVD: dtypes.SeverityMedium, + vulnerability.RedHat: dtypes.SeverityLow, + vulnerability.RubySec: dtypes.SeverityHigh, + }, + CVSS: dtypes.VendorCVSS{ + vulnerability.NVD: dtypes.CVSS{ + V2Vector: "AV:N/AC:L/Au:N/C:C/I:P/A:C", + V3Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + V2Score: 9.7, + V3Score: 5.9, + }, + vulnerability.RedHat: dtypes.CVSS{ + V3Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + V3Score: 5.9, + }, + }, + References: []string{ + "http://www.openwall.com/lists/oss-security/2022/02/11/5", + "https://access.redhat.com/security/cve/CVE-2022-23633", + }, + PublishedDate: lo.ToPtr(time.Date(2022, 2, 11, 21, 15, 0, 0, time.UTC)), + LastModifiedDate: lo.ToPtr(time.Date(2022, 2, 22, 21, 47, 0, 0, time.UTC)), + }, + }, + }, + }, + }, + }, + want: &cdx.BOM{ + XMLNS: "http://cyclonedx.org/schema/bom/1.6", + BOMFormat: "CycloneDX", + SpecVersion: cdx.SpecVersion1_6, + JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json", + SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000007", + Version: 1, + Metadata: &cdx.Metadata{ + Timestamp: "2021-08-25T12:20:30+00:00", + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{ + { + Type: cdx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + Manufacturer: &cdx.OrganizationalEntity{ + Name: "Aqua Security Software Ltd.", + }, + }, + }, + }, + Component: &cdx.Component{ + Type: cdx.ComponentTypeContainer, + BOMRef: "3ff14136-e09f-4df9-80ea-000000000001", + PackageURL: "", + Name: "centos:latest", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:ImageID", + Value: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + }, + { + Name: "aquasecurity:trivy:RepoTag", + Value: "centos:latest", + }, + { + Name: "aquasecurity:trivy:SchemaVersion", + Value: "2", + }, + { + Name: "aquasecurity:trivy:Size", + Value: "1024", + }, + }, + }, + }, + Components: &[]cdx.Component{ + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000002", + Type: cdx.ComponentTypeOS, + Name: string(ftypes.CentOS), + Version: "8.3.2011", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:Class", + Value: "os-pkgs", + }, + { + Name: "aquasecurity:trivy:Type", + Value: "centos", + }, + }, + }, + { + BOMRef: "pkg:gem/actionpack@7.0.0", + Type: cdx.ComponentTypeLibrary, + Name: "actionpack", + Version: "7.0.0", + PackageURL: "pkg:gem/actionpack@7.0.0", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:FilePath", + Value: "tools/project-john/specifications/actionpack.gemspec", + }, + { + Name: "aquasecurity:trivy:LayerDiffID", + Value: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + { + Name: "aquasecurity:trivy:PkgID", + Value: "actionpack@7.0.0", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "gemspec", + }, + }, + }, + { + BOMRef: "pkg:gem/actionpack@7.0.1", + Type: cdx.ComponentTypeLibrary, + Name: "actionpack", + Version: "7.0.1", + PackageURL: "pkg:gem/actionpack@7.0.1", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:FilePath", + Value: "tools/project-doe/specifications/actionpack.gemspec", + }, + { + Name: "aquasecurity:trivy:LayerDiffID", + Value: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + { + Name: "aquasecurity:trivy:PkgID", + Value: "actionpack@7.0.1", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "gemspec", + }, + }, + }, + { + BOMRef: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011&epoch=1", + Type: cdx.ComponentTypeLibrary, + Name: "acl", + Version: "1:2.2.53-1.el8", + Licenses: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + ID: "GPL-2.0-or-later", + }, + }, + }, + PackageURL: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011&epoch=1", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgID", + Value: "acl@2.2.53-1.el8", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "centos", + }, + { + Name: "aquasecurity:trivy:SrcEpoch", + Value: "1", + }, + { + Name: "aquasecurity:trivy:SrcName", + Value: "acl", + }, + { + Name: "aquasecurity:trivy:SrcRelease", + Value: "1.el8", + }, + { + Name: "aquasecurity:trivy:SrcVersion", + Value: "2.2.53", + }, + }, + Hashes: &[]cdx.Hash{ + { + Algorithm: cdx.HashAlgoMD5, + Value: "483792b8b5f9eb8be7dc4407733118d0", + }, + }, + }, + { + BOMRef: "pkg:rpm/centos/glibc@2.28-151.el8?arch=aarch64&distro=centos-8.3.2011", + Type: cdx.ComponentTypeLibrary, + Name: "glibc", + Version: "2.28-151.el8", + Licenses: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + ID: "GPL-2.0-or-later", + }, + }, + }, + PackageURL: "pkg:rpm/centos/glibc@2.28-151.el8?arch=aarch64&distro=centos-8.3.2011", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgID", + Value: "glibc@2.28-151.el8", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "centos", + }, + { + Name: "aquasecurity:trivy:SrcName", + Value: "glibc", + }, + { + Name: "aquasecurity:trivy:SrcRelease", + Value: "151.el8", + }, + { + Name: "aquasecurity:trivy:SrcVersion", + Value: "2.28", + }, + }, + Hashes: &[]cdx.Hash{ + { + Algorithm: cdx.HashAlgoMD5, + Value: "969b3c9231627022f8bf7ac70de807a1", + }, + }, + }, + }, + Dependencies: &[]cdx.Dependency{ + { + Ref: "3ff14136-e09f-4df9-80ea-000000000001", + Dependencies: &[]string{ + "3ff14136-e09f-4df9-80ea-000000000002", + "pkg:gem/actionpack@7.0.0", + "pkg:gem/actionpack@7.0.1", + }, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000002", + Dependencies: &[]string{ + "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011&epoch=1", + }, + }, + { + Ref: "pkg:gem/actionpack@7.0.0", + Dependencies: lo.ToPtr([]string{}), + }, + { + Ref: "pkg:gem/actionpack@7.0.1", + Dependencies: lo.ToPtr([]string{}), + }, + { + Ref: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011&epoch=1", + Dependencies: &[]string{ + "pkg:rpm/centos/glibc@2.28-151.el8?arch=aarch64&distro=centos-8.3.2011", + }, + }, + { + Ref: "pkg:rpm/centos/glibc@2.28-151.el8?arch=aarch64&distro=centos-8.3.2011", + Dependencies: lo.ToPtr([]string{}), + }, + }, + Vulnerabilities: &[]cdx.Vulnerability{ + { + ID: "CVE-2022-23633", + Source: &cdx.Source{ + Name: string(vulnerability.RubySec), + URL: "https://github.com/rubysec/ruby-advisory-db", + }, + Recommendation: "Upgrade actionpack to version ~> 5.2.6, >= 5.2.6.2, ~> 6.0.4, >= 6.0.4.6, ~> 6.1.4, >= 6.1.4.6, >= 7.0.2.2", + Ratings: &[]cdx.VulnerabilityRating{ + { + Source: &cdx.Source{ + Name: string(vulnerability.NVD), + }, + Score: lo.ToPtr(9.7), + Severity: cdx.SeverityHigh, + Method: cdx.ScoringMethodCVSSv2, + Vector: "AV:N/AC:L/Au:N/C:C/I:P/A:C", + }, + { + Source: &cdx.Source{ + Name: string(vulnerability.NVD), + }, + Score: lo.ToPtr(5.9), + Severity: cdx.SeverityMedium, + Method: cdx.ScoringMethodCVSSv31, + Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + }, + { + Source: &cdx.Source{ + Name: string(vulnerability.RedHat), + }, + Score: lo.ToPtr(5.9), + Severity: cdx.SeverityLow, + Method: cdx.ScoringMethodCVSSv31, + Vector: "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:N/A:N", + }, + { + Source: &cdx.Source{ + Name: string(vulnerability.RubySec), + }, + Severity: cdx.SeverityHigh, + }, + }, + Description: "Action Pack is a framework for handling and responding to web requests. Under certain circumstances response bodies will not be closed. In the event a response is *not* notified of a `close`, `ActionDispatch::Executor` will not know to reset thread local state for the next request. This can lead to data being leaked to subsequent requests.This has been fixed in Rails 7.0.2.1, 6.1.4.5, 6.0.4.5, and 5.2.6.1. Upgrading is highly recommended, but to work around this problem a middleware described in GHSA-wh98-p28r-vrc9 can be used.", + Advisories: &[]cdx.Advisory{ + { + URL: "https://avd.aquasec.com/nvd/cve-2022-23633", + }, + { + URL: "http://www.openwall.com/lists/oss-security/2022/02/11/5", + }, + { + URL: "https://access.redhat.com/security/cve/CVE-2022-23633", + }, + }, + Published: "2022-02-11T21:15:00+00:00", + Updated: "2022-02-22T21:47:00+00:00", + Affects: &[]cdx.Affects{ + { + Ref: "pkg:gem/actionpack@7.0.0", + Range: &[]cdx.AffectedVersions{ + { + Version: "7.0.0", + Status: cdx.VulnerabilityStatusAffected, + }, + }, + }, + { + Ref: "pkg:gem/actionpack@7.0.1", + Range: &[]cdx.AffectedVersions{ + { + Version: "7.0.1", + Status: cdx.VulnerabilityStatusAffected, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for fs scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "masahiro331/CVE-2021-41098", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{ + { + Target: "Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + Name: "actioncable", + Version: "6.1.4.1", + Identifier: ftypes.PkgIdentifier{ + UID: "2E6CF0E3CD6949BD", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actioncable", + Version: "6.1.4.1", + }, + }, + }, + }, + }, + { + Target: "Java", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Packages: []ftypes.Package{ + { + Name: "org.springframework:spring-web", + Version: "5.3.22", + Identifier: ftypes.PkgIdentifier{ + UID: "38DDCC9B589D3124", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework", + Name: "spring-web", + Version: "5.3.22", + }, + }, + FilePath: "spring-web-5.3.22.jar", + }, + }, + }, + { + Target: "yarn.lock", + Class: types.ClassLangPkg, + Type: ftypes.Yarn, + Packages: []ftypes.Package{ + { + ID: "@babel/helper-string-parser@7.23.4", + Name: "@babel/helper-string-parser", + Version: "7.23.4", + Identifier: ftypes.PkgIdentifier{ + UID: "F4C833D7F3FD9ECF", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Namespace: "@babel", + Name: "helper-string-parser", + Version: "7.23.4", + }, + }, + }, + }, + }, + }, + }, + want: &cdx.BOM{ + XMLNS: "http://cyclonedx.org/schema/bom/1.6", + BOMFormat: "CycloneDX", + SpecVersion: cdx.SpecVersion1_6, + JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json", + SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000007", + Version: 1, + Metadata: &cdx.Metadata{ + Timestamp: "2021-08-25T12:20:30+00:00", + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{ + { + Type: cdx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + Manufacturer: &cdx.OrganizationalEntity{ + Name: "Aqua Security Software Ltd.", + }, + }, + }, + }, + Component: &cdx.Component{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000001", + Type: cdx.ComponentTypeApplication, + Name: "masahiro331/CVE-2021-41098", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:SchemaVersion", + Value: "2", + }, + }, + }, + }, + Components: &[]cdx.Component{ + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000002", + Type: cdx.ComponentTypeApplication, + Name: "Gemfile.lock", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:Class", + Value: "lang-pkgs", + }, + { + Name: "aquasecurity:trivy:Type", + Value: "bundler", + }, + }, + }, + { + BOMRef: "3ff14136-e09f-4df9-80ea-000000000005", + Type: cdx.ComponentTypeApplication, + Name: "yarn.lock", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:Class", + Value: "lang-pkgs", + }, + { + Name: "aquasecurity:trivy:Type", + Value: "yarn", + }, + }, + }, + { + BOMRef: "pkg:gem/actioncable@6.1.4.1", + Type: "library", + Name: "actioncable", + Version: "6.1.4.1", + PackageURL: "pkg:gem/actioncable@6.1.4.1", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgType", + Value: "bundler", + }, + }, + }, + { + BOMRef: "pkg:maven/org.springframework/spring-web@5.3.22", + Type: "library", + Name: "spring-web", + Group: "org.springframework", + Version: "5.3.22", + PackageURL: "pkg:maven/org.springframework/spring-web@5.3.22", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:FilePath", + Value: "spring-web-5.3.22.jar", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "jar", + }, + }, + }, + { + BOMRef: "pkg:npm/%40babel/helper-string-parser@7.23.4", + Type: "library", + Name: "helper-string-parser", + Group: "@babel", + Version: "7.23.4", + PackageURL: "pkg:npm/%40babel/helper-string-parser@7.23.4", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:PkgID", + Value: "@babel/helper-string-parser@7.23.4", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "yarn", + }, + }, + }, + }, + Vulnerabilities: &[]cdx.Vulnerability{}, + Dependencies: &[]cdx.Dependency{ + { + Ref: "3ff14136-e09f-4df9-80ea-000000000001", + Dependencies: &[]string{ + "3ff14136-e09f-4df9-80ea-000000000002", + "3ff14136-e09f-4df9-80ea-000000000005", + "pkg:maven/org.springframework/spring-web@5.3.22", + }, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000002", + Dependencies: &[]string{ + "pkg:gem/actioncable@6.1.4.1", + }, + }, + { + Ref: "3ff14136-e09f-4df9-80ea-000000000005", + Dependencies: &[]string{ + "pkg:npm/%40babel/helper-string-parser@7.23.4", + }, + }, + { + Ref: "pkg:gem/actioncable@6.1.4.1", + Dependencies: lo.ToPtr([]string{}), + }, + { + Ref: "pkg:maven/org.springframework/spring-web@5.3.22", + Dependencies: lo.ToPtr([]string{}), + }, + { + Ref: "pkg:npm/%40babel/helper-string-parser@7.23.4", + Dependencies: lo.ToPtr([]string{}), + }, + }, + }, + }, + { + name: "happy path for sbom (cyclonedx) scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "./report.cdx.json", + ArtifactType: ftypes.TypeCycloneDX, + Results: types.Results{ + { + Target: "Java", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Packages: []ftypes.Package{ + { + Name: "com.fasterxml.jackson.core:jackson-databind", + Version: "2.13.4.1", + Identifier: ftypes.PkgIdentifier{ + BOMRef: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + UID: "9A5066570222D04C", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4.1", + }, + }, + FilePath: "jackson-databind-2.13.4.1.jar", + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-42003", + PkgName: "com.fasterxml.jackson.core:jackson-databind", + PkgPath: "jackson-databind-2.13.4.1.jar", + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + UID: "9A5066570222D04C", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4.1", + }, + }, + InstalledVersion: "2.13.4.1", + FixedVersion: "2.12.7.1, 2.13.4.2", + Status: dtypes.StatusFixed, + SeveritySource: "ghsa", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2022-42003", + DataSource: &dtypes.DataSource{ + ID: vulnerability.GHSA, + Name: "GitHub Security Advisory Maven", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + Vulnerability: dtypes.Vulnerability{ + Title: "jackson-databind: deep wrapper array nesting wrt UNWRAP_SINGLE_VALUE_ARRAYS", + Description: "In FasterXML jackson-databind before versions 2.13.4.1 and 2.12.17.1, resource exhaustion can occur because of a lack of a check in primitive value deserializers to avoid deep wrapper array nesting, when the UNWRAP_SINGLE_VALUE_ARRAYS feature is enabled.", + Severity: dtypes.SeverityHigh.String(), + VendorSeverity: dtypes.VendorSeverity{ + vulnerability.GHSA: dtypes.SeverityHigh, + }, + CVSS: dtypes.VendorCVSS{ + vulnerability.GHSA: dtypes.CVSS{ + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + V3Score: 7.5, + }, + }, + References: []string{ + "https://access.redhat.com/security/cve/CVE-2022-42003", + }, + PublishedDate: lo.ToPtr(time.Date(2022, 10, 2, 5, 15, 0, 0, time.UTC)), + LastModifiedDate: lo.ToPtr(time.Date(2022, 12, 20, 10, 15, 0, 0, time.UTC)), + }, + }, + }, + }, + }, + BOM: testSBOM(), + }, + want: &cdx.BOM{ + XMLNS: "http://cyclonedx.org/schema/bom/1.6", + BOMFormat: "CycloneDX", + SpecVersion: cdx.SpecVersion1_6, + JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json", + SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000001", + Version: 1, + Metadata: &cdx.Metadata{ + Timestamp: "2021-08-25T12:20:30+00:00", + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{ + { + Type: cdx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + Manufacturer: &cdx.OrganizationalEntity{ + Name: "Aqua Security Software Ltd.", + }, + }, + }, + }, + Component: &cdx.Component{ + BOMRef: "aff65b54-6009-4c32-968d-748949ef46e8", // The original bom-ref is used + Type: cdx.ComponentTypeApplication, + Name: "jackson-databind-2.13.4.1.jar", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:SchemaVersion", + Value: "2", + }, + }, + }, + }, + Components: &[]cdx.Component{ + { + BOMRef: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + Type: cdx.ComponentTypeLibrary, + Group: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4.1", + PackageURL: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:FilePath", + Value: "jackson-databind-2.13.4.1.jar", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "jar", + }, + }, + }, + }, + Vulnerabilities: &[]cdx.Vulnerability{ + { + ID: "CVE-2022-42003", + Source: &cdx.Source{ + Name: string(vulnerability.GHSA), + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + Recommendation: "Upgrade com.fasterxml.jackson.core:jackson-databind to version 2.12.7.1, 2.13.4.2", + Ratings: &[]cdx.VulnerabilityRating{ + { + Source: &cdx.Source{ + Name: string(vulnerability.GHSA), + }, + Score: lo.ToPtr(7.5), + Severity: cdx.SeverityHigh, + Method: cdx.ScoringMethodCVSSv31, + Vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H", + }, + }, + Description: "In FasterXML jackson-databind before versions 2.13.4.1 and 2.12.17.1, resource exhaustion can occur because of a lack of a check in primitive value deserializers to avoid deep wrapper array nesting, when the UNWRAP_SINGLE_VALUE_ARRAYS feature is enabled.", + Advisories: &[]cdx.Advisory{ + { + URL: "https://avd.aquasec.com/nvd/cve-2022-42003", + }, + { + URL: "https://access.redhat.com/security/cve/CVE-2022-42003", + }, + }, + Published: "2022-10-02T05:15:00+00:00", + Updated: "2022-12-20T10:15:00+00:00", + Affects: &[]cdx.Affects{ + { + Ref: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + Range: &[]cdx.AffectedVersions{ + { + Version: "2.13.4.1", + Status: cdx.VulnerabilityStatusAffected, + }, + }, + }, + }, + }, + }, + Dependencies: &[]cdx.Dependency{ + { + Ref: "aff65b54-6009-4c32-968d-748949ef46e8", + Dependencies: &[]string{ + "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + }, + }, + { + Ref: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + Dependencies: lo.ToPtr([]string{}), + }, + }, + }, + }, + { + name: "happy path. 2 packages for 1 CVE", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "CVE-2023-34468", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{ + { + Target: "Java", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Packages: []ftypes.Package{ + { + Name: "org.apache.nifi:nifi-dbcp-base", + Version: "1.20.0", + Identifier: ftypes.PkgIdentifier{ + UID: "6F266C79E57ADC38", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.apache.nifi", + Name: "nifi-dbcp-base", + Version: "1.20.0", + }, + }, + FilePath: "nifi-dbcp-base-1.20.0.jar", + }, + { + Name: "org.apache.nifi:nifi-hikari-dbcp-service", + Version: "1.20.0", + Identifier: ftypes.PkgIdentifier{ + UID: "3EA16F0A4CAB50F9", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.apache.nifi", + Name: "nifi-hikari-dbcp-service", + Version: "1.20.0", + }, + }, + FilePath: "nifi-hikari-dbcp-service-1.20.0.jar", + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2023-34468", + PkgName: "org.apache.nifi:nifi-dbcp-base", + PkgPath: "nifi-dbcp-base-1.20.0.jar", + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "6F266C79E57ADC38", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.apache.nifi", + Name: "nifi-dbcp-base", + Version: "1.20.0", + }, + }, + InstalledVersion: "1.20.0", + FixedVersion: "1.22.0", + SeveritySource: vulnerability.GHSA, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2023-34468", + DataSource: &dtypes.DataSource{ + ID: vulnerability.GHSA, + Name: "GitHub Security Advisory Maven", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + Vulnerability: dtypes.Vulnerability{ + Title: "Apache NiFi vulnerable to Code Injection", + Description: "The DBCPConnectionPool and HikariCPConnectionPool Controller Services in Apache NiFi 0.0.2 through 1.21.0...", + Severity: dtypes.SeverityHigh.String(), + CweIDs: []string{ + "CWE-94", + }, + VendorSeverity: dtypes.VendorSeverity{ + vulnerability.GHSA: dtypes.SeverityHigh, + vulnerability.NVD: dtypes.SeverityHigh, + }, + CVSS: dtypes.VendorCVSS{ + vulnerability.GHSA: dtypes.CVSS{ + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V3Score: 8.8, + }, + vulnerability.NVD: dtypes.CVSS{ + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V3Score: 8.8, + }, + }, + References: []string{ + "http://www.openwall.com/lists/oss-security/2023/06/12/3", + "https://github.com/advisories/GHSA-xm2m-2q6h-22jw", + }, + PublishedDate: lo.ToPtr(time.Date(2023, 6, 12, 16, 15, 0, 0, time.UTC)), + LastModifiedDate: lo.ToPtr(time.Date(2023, 6, 21, 2, 20, 0, 0, time.UTC)), + }, + }, + { + VulnerabilityID: "CVE-2023-34468", + PkgName: "org.apache.nifi:nifi-hikari-dbcp-service", + PkgPath: "nifi-hikari-dbcp-service-1.20.0.jar", + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "3EA16F0A4CAB50F9", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.apache.nifi", + Name: "nifi-hikari-dbcp-service", + Version: "1.20.0", + }, + }, + InstalledVersion: "1.20.0", + FixedVersion: "1.22.0", + SeveritySource: vulnerability.GHSA, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2023-34468", + DataSource: &dtypes.DataSource{ + ID: vulnerability.GHSA, + Name: "GitHub Security Advisory Maven", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + Vulnerability: dtypes.Vulnerability{ + Title: "Apache NiFi vulnerable to Code Injection", + Description: "The DBCPConnectionPool and HikariCPConnectionPool Controller Services in Apache NiFi 0.0.2 through 1.21.0...", + Severity: dtypes.SeverityHigh.String(), + CweIDs: []string{ + "CWE-94", + }, + VendorSeverity: dtypes.VendorSeverity{ + vulnerability.GHSA: dtypes.SeverityHigh, + vulnerability.NVD: dtypes.SeverityHigh, + }, + CVSS: dtypes.VendorCVSS{ + vulnerability.GHSA: dtypes.CVSS{ + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V3Score: 8.8, + }, + vulnerability.NVD: dtypes.CVSS{ + V3Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + V3Score: 8.8, + }, + }, + References: []string{ + "http://www.openwall.com/lists/oss-security/2023/06/12/3", + "https://github.com/advisories/GHSA-xm2m-2q6h-22jw", + }, + PublishedDate: lo.ToPtr(time.Date(2023, 6, 12, 16, 15, 0, 0, time.UTC)), + LastModifiedDate: lo.ToPtr(time.Date(2023, 6, 21, 2, 20, 0, 0, time.UTC)), + }, + }, + }, + }, + }, + }, + want: &cdx.BOM{ + XMLNS: "http://cyclonedx.org/schema/bom/1.6", + BOMFormat: "CycloneDX", + SpecVersion: cdx.SpecVersion1_6, + JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json", + SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000004", + Version: 1, + Metadata: &cdx.Metadata{ + Timestamp: "2021-08-25T12:20:30+00:00", + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{ + { + Type: cdx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + Manufacturer: &cdx.OrganizationalEntity{ + Name: "Aqua Security Software Ltd.", + }, + }, + }, + }, + Component: &cdx.Component{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000001", + Type: cdx.ComponentTypeApplication, + Name: "CVE-2023-34468", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:SchemaVersion", + Value: "2", + }, + }, + }, + }, + Components: &[]cdx.Component{ + { + BOMRef: "pkg:maven/org.apache.nifi/nifi-dbcp-base@1.20.0", + Type: "library", + Name: "nifi-dbcp-base", + Group: "org.apache.nifi", + Version: "1.20.0", + PackageURL: "pkg:maven/org.apache.nifi/nifi-dbcp-base@1.20.0", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:FilePath", + Value: "nifi-dbcp-base-1.20.0.jar", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "jar", + }, + }, + }, + { + BOMRef: "pkg:maven/org.apache.nifi/nifi-hikari-dbcp-service@1.20.0", + Type: "library", + Name: "nifi-hikari-dbcp-service", + Group: "org.apache.nifi", + Version: "1.20.0", + PackageURL: "pkg:maven/org.apache.nifi/nifi-hikari-dbcp-service@1.20.0", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:FilePath", + Value: "nifi-hikari-dbcp-service-1.20.0.jar", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "jar", + }, + }, + }, + }, + Dependencies: &[]cdx.Dependency{ + { + Ref: "3ff14136-e09f-4df9-80ea-000000000001", + Dependencies: &[]string{ + "pkg:maven/org.apache.nifi/nifi-dbcp-base@1.20.0", + "pkg:maven/org.apache.nifi/nifi-hikari-dbcp-service@1.20.0", + }, + }, + { + Ref: "pkg:maven/org.apache.nifi/nifi-dbcp-base@1.20.0", + Dependencies: lo.ToPtr([]string{}), + }, + { + Ref: "pkg:maven/org.apache.nifi/nifi-hikari-dbcp-service@1.20.0", + Dependencies: lo.ToPtr([]string{}), + }, + }, + Vulnerabilities: &[]cdx.Vulnerability{ + { + ID: "CVE-2023-34468", + Source: &cdx.Source{ + Name: string(vulnerability.GHSA), + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Amaven", + }, + Recommendation: "Upgrade org.apache.nifi:nifi-dbcp-base to version 1.22.0; Upgrade org.apache.nifi:nifi-hikari-dbcp-service to version 1.22.0", + Ratings: &[]cdx.VulnerabilityRating{ + { + Source: &cdx.Source{ + Name: string(vulnerability.GHSA), + }, + Score: lo.ToPtr(8.8), + Severity: cdx.SeverityHigh, + Method: cdx.ScoringMethodCVSSv31, + Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + }, + { + Source: &cdx.Source{ + Name: string(vulnerability.NVD), + }, + Score: lo.ToPtr(8.8), + Severity: cdx.SeverityHigh, + Method: cdx.ScoringMethodCVSSv31, + Vector: "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H", + }, + }, + CWEs: lo.ToPtr([]int{94}), + Description: "The DBCPConnectionPool and HikariCPConnectionPool Controller Services in Apache NiFi 0.0.2 through 1.21.0...", + Advisories: &[]cdx.Advisory{ + { + URL: "https://avd.aquasec.com/nvd/cve-2023-34468", + }, + { + URL: "http://www.openwall.com/lists/oss-security/2023/06/12/3", + }, + { + URL: "https://github.com/advisories/GHSA-xm2m-2q6h-22jw", + }, + }, + Published: "2023-06-12T16:15:00+00:00", + Updated: "2023-06-21T02:20:00+00:00", + Affects: &[]cdx.Affects{ + { + Ref: "pkg:maven/org.apache.nifi/nifi-dbcp-base@1.20.0", + Range: &[]cdx.AffectedVersions{ + { + Version: "1.20.0", + Status: cdx.VulnerabilityStatusAffected, + }, + }, + }, + { + Ref: "pkg:maven/org.apache.nifi/nifi-hikari-dbcp-service@1.20.0", + Range: &[]cdx.AffectedVersions{ + { + Version: "1.20.0", + Status: cdx.VulnerabilityStatusAffected, + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path aggregate results", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "test-aggregate", + ArtifactType: ftypes.TypeRepository, + Results: types.Results{ + { + Target: "Node.js", + Class: types.ClassLangPkg, + Type: ftypes.NodePkg, + Packages: []ftypes.Package{ + { + ID: "ruby-typeprof@0.20.1", + Name: "ruby-typeprof", + Version: "0.20.1", + Identifier: ftypes.PkgIdentifier{ + UID: "C861FD5FC7AC663F", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "ruby-typeprof", + Version: "0.20.1", + }, + }, + Licenses: []string{"MIT"}, + Layer: ftypes.Layer{ + DiffID: "sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e", + }, + FilePath: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json", + }, + }, + }, + }, + }, + want: &cdx.BOM{ + XMLNS: "http://cyclonedx.org/schema/bom/1.6", + BOMFormat: "CycloneDX", + SpecVersion: cdx.SpecVersion1_6, + JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json", + SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000003", + Version: 1, + Metadata: &cdx.Metadata{ + Timestamp: "2021-08-25T12:20:30+00:00", + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{ + { + Type: cdx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + Manufacturer: &cdx.OrganizationalEntity{ + Name: "Aqua Security Software Ltd.", + }, + }, + }, + }, + Component: &cdx.Component{ + Type: cdx.ComponentTypeApplication, + Name: "test-aggregate", + BOMRef: "3ff14136-e09f-4df9-80ea-000000000001", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:SchemaVersion", + Value: "2", + }, + }, + }, + }, + Components: &[]cdx.Component{ + { + BOMRef: "pkg:npm/ruby-typeprof@0.20.1", + Type: "library", + Name: "ruby-typeprof", + Version: "0.20.1", + PackageURL: "pkg:npm/ruby-typeprof@0.20.1", + Licenses: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + ID: "MIT", + }, + }, + }, + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:FilePath", + Value: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json", + }, + { + Name: "aquasecurity:trivy:LayerDiffID", + Value: "sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e", + }, + { + Name: "aquasecurity:trivy:PkgID", + Value: "ruby-typeprof@0.20.1", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "node-pkg", + }, + }, + }, + }, + Vulnerabilities: &[]cdx.Vulnerability{}, + Dependencies: &[]cdx.Dependency{ + { + Ref: "3ff14136-e09f-4df9-80ea-000000000001", + Dependencies: &[]string{ + "pkg:npm/ruby-typeprof@0.20.1", + }, + }, + { + Ref: "pkg:npm/ruby-typeprof@0.20.1", + Dependencies: lo.ToPtr([]string{}), + }, + }, + }, + }, + { + name: "happy path empty", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "empty/path", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{}, + }, + want: &cdx.BOM{ + XMLNS: "http://cyclonedx.org/schema/bom/1.6", + BOMFormat: "CycloneDX", + SpecVersion: cdx.SpecVersion1_6, + JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json", + SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000002", + Version: 1, + Metadata: &cdx.Metadata{ + Timestamp: "2021-08-25T12:20:30+00:00", + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{ + { + Type: cdx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + Manufacturer: &cdx.OrganizationalEntity{ + Name: "Aqua Security Software Ltd.", + }, + }, + }, + }, + Component: &cdx.Component{ + Type: cdx.ComponentTypeApplication, + Name: "empty/path", + BOMRef: "3ff14136-e09f-4df9-80ea-000000000001", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:SchemaVersion", + Value: "2", + }, + }, + }, + }, + Components: &[]cdx.Component{}, + Vulnerabilities: &[]cdx.Vulnerability{}, + Dependencies: &[]cdx.Dependency{ + { + Ref: "3ff14136-e09f-4df9-80ea-000000000001", + Dependencies: lo.ToPtr([]string{}), + }, + }, + }, + }, + { + name: "happy path. Root component doesn't exist", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "empty/path", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{}, + BOM: testSBOMWithoutRoot(), + }, + want: &cdx.BOM{ + XMLNS: "http://cyclonedx.org/schema/bom/1.6", + BOMFormat: "CycloneDX", + SpecVersion: cdx.SpecVersion1_6, + JSONSchema: "http://cyclonedx.org/schema/bom-1.6.schema.json", + SerialNumber: "urn:uuid:3ff14136-e09f-4df9-80ea-000000000001", + Version: 1, + Metadata: &cdx.Metadata{ + Timestamp: "2021-08-25T12:20:30+00:00", + Tools: &cdx.ToolsChoice{ + Components: &[]cdx.Component{ + { + Type: cdx.ComponentTypeApplication, + Name: "trivy", + Group: "aquasecurity", + Version: "dev", + Manufacturer: &cdx.OrganizationalEntity{ + Name: "Aqua Security Software Ltd.", + }, + }, + }, + }, + }, + Components: &[]cdx.Component{ + { + BOMRef: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + Type: cdx.ComponentTypeLibrary, + Group: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4.1", + PackageURL: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + Properties: &[]cdx.Property{ + { + Name: "aquasecurity:trivy:FilePath", + Value: "jackson-databind-2.13.4.1.jar", + }, + { + Name: "aquasecurity:trivy:PkgType", + Value: "jar", + }, + }, + }, + }, + Vulnerabilities: &[]cdx.Vulnerability{}, + Dependencies: &[]cdx.Dependency{ + { + Ref: "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.4.1", + Dependencies: lo.ToPtr([]string{}), + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := clock.With(t.Context(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + marshaler := cyclonedx.NewMarshaler("dev") + got, err := marshaler.MarshalReport(ctx, tt.inputReport) + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestMarshaler_Licenses(t *testing.T) { + tests := []struct { + name string + licenses []string + want *cdx.Licenses + }{ + { + name: "SPDX ID", + licenses: []string{ + "MIT", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + ID: "MIT", + }, + }, + }, + }, + { + name: "Unknown SPDX ID", + licenses: []string{ + "no-spdx-id-license", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + Name: "no-spdx-id-license", + }, + }, + }, + }, + { + name: "text license", + licenses: []string{ + "text://text of license", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + Name: "text of license", + }, + }, + }, + }, + { + name: "SPDX license with exception", + licenses: []string{ + "AFL 2.0 with Linux-syscall-note", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + Expression: "AFL-2.0 WITH Linux-syscall-note", + }, + }, + }, + { + name: "SPDX license with wrong exception", + licenses: []string{ + "GPL-2.0-with-autoconf-exception+", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + Name: "GPL-2.0-only WITH autoconf-exception+", + }, + }, + }, + }, + { + name: "SPDX expression", + licenses: []string{ + "GPL-3.0-only OR AFL 2.0 with Linux-syscall-note AND GPL-3.0-only", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + Expression: "GPL-3.0-only OR AFL-2.0 WITH Linux-syscall-note AND GPL-3.0-only", + }, + }, + }, + { + name: "invalid SPDX expression", + licenses: []string{ + "wrong-spdx-id OR GPL-3.0-only", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + Name: "wrong-spdx-id OR GPL-3.0-only", + }, + }, + }, + }, + { + name: "multiple SPDX IDs", + licenses: []string{ + "AFL 2.0 with Linux-syscall-note", + "GPL-3.0-only OR MIT", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + Expression: "AFL-2.0 WITH Linux-syscall-note AND GPL-3.0-only OR MIT", + }, + }, + }, + { + name: "multiple SPDX expressions", + licenses: []string{ + "MIT", + "AFL 2.0", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + ID: "MIT", + }, + }, + cdx.LicenseChoice{ + License: &cdx.License{ + ID: "AFL-2.0", + }, + }, + }, + }, + { + name: "SPDX ID + license name", + licenses: []string{ + "MIT", + "license-name", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + ID: "MIT", + }, + }, + cdx.LicenseChoice{ + License: &cdx.License{ + Name: "license-name", + }, + }, + }, + }, + { + name: "SPDX ID + SPDX exception", + licenses: []string{ + "MIT", + "AFL 2.0 with Linux-Syscall-Note", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + Expression: "MIT AND AFL-2.0 WITH Linux-syscall-note", + }, + }, + }, + { + name: "license normalization error", + licenses: []string{ + "Copyright (c) 2000, 2025, Oracle and/or its affiliates. Under GPLv2 license as shown in the Description field.", + }, + want: &cdx.Licenses{ + cdx.LicenseChoice{ + License: &cdx.License{ + Name: "Copyright (c) 2000, 2025, Oracle and/or its affiliates. Under GPLv2 license as shown in the Description field.", + }, + }, + }, + }, + { + name: "empty license", + licenses: []string{ + "", + }, + want: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + marshaler := cyclonedx.NewMarshaler("dev") + got := marshaler.Licenses(tt.licenses) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/sbom/cyclonedx/testdata/happy/bom.json b/pkg/sbom/cyclonedx/testdata/happy/bom.json new file mode 100644 index 000000000000..e2d68e96b38b --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/bom.json @@ -0,0 +1,279 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "type": "library", + "name": "musl", + "version": "1.2.3-r0", + "licenses": [ + { + "license": { + "name": "MIT" + } + } + ], + "purl": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:SrcName", + "value": "musl" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "1.2.3-r0" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + } + ] + }, + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + }, + { + "bom-ref": "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + "type": "library", + "name": "child-project", + "group": "org.codehaus.mojo", + "version": "1.0", + "purl": "pkg:maven/org.codehaus.mojo/child-project@1.0", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "app/maven/target/child-project-1.0.jar" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "jar" + } + ] + }, + { + "bom-ref": "pkg:maven/com.example/example@0.0.1", + "type": "library", + "name": "com.example:example", + "version": "0.0.1", + "purl": "pkg:maven/com.example/example@0.0.1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "pkg:npm/@example/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "type": "library", + "group": "@example", + "name": "bootstrap", + "version": "5.0.2", + "licenses": [ + { + "license": { + "name": "MIT" + } + } + ], + "purl": "pkg:npm/@example/bootstrap@5.0.2", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "app/app/package.json" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "node-pkg" + } + ] + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "pkg:composer/pear/pear_exception@v1.0.0", + "type": "library", + "name": "pear/pear_exception", + "version": "v1.0.0", + "purl": "pkg:composer/pear/pear_exception@v1.0.0", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "type": "application", + "name": "app/composer/composer.lock", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "composer" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "type": "library", + "name": "github.com/package-url/packageurl-go", + "version": "v0.1.1-0.20220203205134-d70459300c8a", + "purl": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "properties": [ + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ] + }, + { + "bom-ref": "1a111e6b-a682-470e-8b0e-aaa49d93cd39", + "type": "application", + "name": "app/gobinary/gobinary", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + } + ] + }, + { + "bom-ref": "43b87f73-4b96-25ab-845b-489321cea146", + "type": "application", + "name": "app/gradle/target/gradle.lockfile", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "gradle" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "dependsOn": [ + "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0" + ] + }, + { + "ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1", + "pkg:composer/pear/pear_exception@v1.0.0" + ] + }, + { + "ref": "1a111e6b-a682-470e-8b0e-aaa49d93cd39", + "dependsOn": [ + "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a" + ] + }, + { + "ref": "43b87f73-4b96-25ab-845b-489321cea146", + "dependsOn": [ + "pkg:maven/com.example/example@0.0.1" + ] + }, + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "dependsOn": [ + "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + "pkg:maven/com.example/example@0.0.1", + "pkg:npm/@example/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + "100925ff-7c0a-470f-a725-8fb973b40e7b", + "1a111e6b-a682-470e-8b0e-aaa49d93cd39" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/empty-bom.json b/pkg/sbom/cyclonedx/testdata/happy/empty-bom.json new file mode 100644 index 000000000000..b61f7bef7b5f --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/empty-bom.json @@ -0,0 +1,48 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + } + ] + } + }, + "dependencies": [ + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a" + } + ] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/empty-metadata-component-bom.json b/pkg/sbom/cyclonedx/testdata/happy/empty-metadata-component-bom.json new file mode 100644 index 000000000000..2fb29d2647af --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/empty-metadata-component-bom.json @@ -0,0 +1,20 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + } + }, + "dependencies": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/group-in-name.json b/pkg/sbom/cyclonedx/testdata/happy/group-in-name.json new file mode 100644 index 000000000000..f7e7e44dc437 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/group-in-name.json @@ -0,0 +1,62 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:8366a7c8-229c-4518-b86c-8a1bcf69af01", + "version": 1, + "metadata": { + "timestamp": "2023-06-20T04:32:10+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "b0ae8323-eb7b-4be5-bc5c-4849fd795ec0", + "type": "application", + "name": "spring-web-5.3.22.jar", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:maven/org.springframework/spring-web@5.3.22?file_path=spring-web-5.3.22.jar", + "type": "library", + "name": "org.springframework:spring-web", + "version": "5.3.22", + "purl": "pkg:maven/org.springframework/spring-web@5.3.22", + "properties": [ + { + "name": "aquasecurity:trivy:FilePath", + "value": "spring-web-5.3.22.jar" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "jar" + } + ] + } + ], + "dependencies": [ + { + "ref": "b0ae8323-eb7b-4be5-bc5c-4849fd795ec0", + "dependsOn": [ + "pkg:maven/org.springframework/spring-web@5.3.22?file_path=spring-web-5.3.22.jar" + ] + }, + { + "ref": "pkg:maven/org.springframework/spring-web@5.3.22?file_path=spring-web-5.3.22.jar", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/independent-library-bom.json b/pkg/sbom/cyclonedx/testdata/happy/independent-library-bom.json new file mode 100644 index 000000000000..164eed844166 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/independent-library-bom.json @@ -0,0 +1,63 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "application", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:composer/pear/core@1.13.1", + "type": "library", + "name": "pear/core", + "version": "1.13.1", + "purl": "pkg:composer/pear/core@1.13.1" + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1" + }, + { + "bom-ref": "pkg:composer/pear/pear_exception@v1.0.0", + "type": "library", + "name": "pear/pear_exception", + "version": "v1.0.0", + "purl": "pkg:composer/pear/pear_exception@v1.0.0" + } + ], + "dependencies": [ + { + "ref": "pkg:composer/pear/core@1.13.1", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1", + "pkg:composer/pear/pear_exception@v1.0.0" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/infinite-loop-bom.json b/pkg/sbom/cyclonedx/testdata/happy/infinite-loop-bom.json new file mode 100644 index 000000000000..d1080b4de92a --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/infinite-loop-bom.json @@ -0,0 +1,182 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:a085f5e7-f5c1-4bc0-96be-ffa4d235ebc8", + "version": 1, + "metadata": { + "timestamp": "2023-04-06T05:41:44+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "pkg:oci/ubuntu@sha256:67211c14fa74f070d27cc59d69a7fa9aeff8e28ea118ef3babc295a0428a6d21?repository_url=index.docker.io%2Flibrary%2Fubuntu\u0026arch=amd64", + "type": "container", + "name": "ubuntu", + "purl": "pkg:oci/ubuntu@sha256:67211c14fa74f070d27cc59d69a7fa9aeff8e28ea118ef3babc295a0428a6d21?repository_url=index.docker.io%2Flibrary%2Fubuntu\u0026arch=amd64", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:08d22c0ceb150ddeb2237c5fa3129c0183f3cc6f5eeb2e7aa4016da3ad02140a" + }, + { + "name": "aquasecurity:trivy:RepoDigest", + "value": "ubuntu@sha256:67211c14fa74f070d27cc59d69a7fa9aeff8e28ea118ef3babc295a0428a6d21" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:b93c1bd012ab8fda60f5b4f5906bf244586e0e3292d84571d3abb56472248466" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "ubuntu:latest" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:deb/ubuntu/libc6@2.35-0ubuntu3.1?distro=ubuntu-22.04", + "type": "library", + "name": "libc6", + "version": "2.35-0ubuntu3.1", + "licenses": [ + { + "license": { + "name": "LGPL-2.1" + } + }, + { + "license": { + "name": "GPL-2.0" + } + }, + { + "license": { + "name": "GFDL-1.3" + } + } + ], + "purl": "pkg:deb/ubuntu/libc6@2.35-0ubuntu3.1?distro=ubuntu-22.04", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "libc6@2.35-0ubuntu3.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "ubuntu" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "glibc" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "2.35" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "0ubuntu3.1" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:74ac377868f863e123f24c409f79709f7563fa464557c36a09cf6f85c8b92b7f" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b93c1bd012ab8fda60f5b4f5906bf244586e0e3292d84571d3abb56472248466" + } + ] + }, + { + "bom-ref": "pkg:deb/ubuntu/libcrypt1@4.4.27-1?epoch=1\u0026distro=ubuntu-22.04", + "type": "library", + "name": "libcrypt1", + "version": "4.4.27-1", + "purl": "pkg:deb/ubuntu/libcrypt1@4.4.27-1?epoch=1\u0026distro=ubuntu-22.04", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "libcrypt1@1:4.4.27-1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "ubuntu" + }, + { + "name": "aquasecurity:trivy:SrcName", + "value": "libxcrypt" + }, + { + "name": "aquasecurity:trivy:SrcVersion", + "value": "4.4.27" + }, + { + "name": "aquasecurity:trivy:SrcRelease", + "value": "1" + }, + { + "name": "aquasecurity:trivy:SrcEpoch", + "value": "1" + }, + { + "name": "aquasecurity:trivy:LayerDigest", + "value": "sha256:74ac377868f863e123f24c409f79709f7563fa464557c36a09cf6f85c8b92b7f" + }, + { + "name": "aquasecurity:trivy:LayerDiffID", + "value": "sha256:b93c1bd012ab8fda60f5b4f5906bf244586e0e3292d84571d3abb56472248466" + } + ] + }, + { + "bom-ref": "71577c3e-dbc6-41be-ba9e-c5268bc2e5d4", + "type": "operating-system", + "name": "ubuntu", + "version": "22.04", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "ubuntu" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "71577c3e-dbc6-41be-ba9e-c5268bc2e5d4", + "dependsOn": [ + "pkg:deb/ubuntu/libc6@2.35-0ubuntu3.1?distro=ubuntu-22.04", + "pkg:deb/ubuntu/libcrypt1@4.4.27-1?epoch=1\u0026distro=ubuntu-22.04" + ] + }, + { + "ref": "pkg:deb/ubuntu/libc6@2.35-0ubuntu3.1?distro=ubuntu-22.04", + "dependsOn": [ + "pkg:deb/ubuntu/libcrypt1@4.4.27-1?epoch=1\u0026distro=ubuntu-22.04" + ] + }, + { + "ref": "pkg:deb/ubuntu/libcrypt1@4.4.27-1?epoch=1\u0026distro=ubuntu-22.04", + "dependsOn": [ + "pkg:deb/ubuntu/libc6@2.35-0ubuntu3.1?distro=ubuntu-22.04" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/invalid-purl.json b/pkg/sbom/cyclonedx/testdata/happy/invalid-purl.json new file mode 100644 index 000000000000..070da15fbb05 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/invalid-purl.json @@ -0,0 +1,55 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "application", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:composer/pear/core@1.13.1", + "type": "library", + "name": "pear/core", + "version": "1.13.1", + "purl": "pkg:composer/pear/core@1.13.1" + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "invalid-purl-format" + } + ], + "dependencies": [ + { + "ref": "pkg:composer/pear/core@1.13.1", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/kbom.json b/pkg/sbom/cyclonedx/testdata/happy/kbom.json new file mode 100644 index 000000000000..7d4a5b82b65e --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/kbom.json @@ -0,0 +1,427 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:e2daaea6-d96f-4b84-960c-0d72c348cd23", + "version": 1, + "metadata": { + "timestamp": "2023-09-29T06:25:00+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "pkg:k8s/k8s.io%2Fkubernetes@1.27.4", + "type": "platform", + "name": "k8s.io/kubernetes", + "version": "1.27.4", + "purl": "pkg:k8s/k8s.io%2Fkubernetes@1.27.4", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "cluster" + } + ] + } + }, + "components": [ + { + "bom-ref": "5262e708-f1a3-4fca-a1c3-0a8384f7f4a5", + "type": "operating-system", + "name": "ubuntu", + "version": "22.04.2", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "ubuntu" + } + ] + }, + { + "bom-ref": "a62abb1f-cb38-4fde-90f3-2bda3b87ddb2", + "type": "application", + "name": "node-core-components" + }, + { + "bom-ref": "a6350ac3-52f6-4c5f-a3e3-184b9a634bef", + "type": "platform", + "name": "minikube", + "properties": [ + { + "name": "aquasecurity:trivy:Architecture", + "value": "arm64" + }, + { + "name": "aquasecurity:trivy:HostName", + "value": "minikube" + }, + { + "name": "aquasecurity:trivy:KernelVersion", + "value": "5.15.49-linuxkit-pr" + }, + { + "name": "aquasecurity:trivy:NodeRole", + "value": "master" + }, + { + "name": "aquasecurity:trivy:OperatingSystem", + "value": "linux" + }, + { + "name": "aquasecurity:trivy:resource:Name", + "value": "minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "node" + } + ] + }, + { + "bom-ref": "b19a88a3-017d-4e70-a73a-75f48696ec0f", + "type": "application", + "name": "kube-dns", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "coredns-5d78c9869d-nd92n" + } + ] + }, + { + "bom-ref": "b1c502c9-3c6e-43af-822b-1cb55c6c6ff3", + "type": "application", + "name": "go.etcd.io/etcd/v3", + "version": "3.5.7-0", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "etcd-minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "controlPlane" + } + ] + }, + { + "bom-ref": "pkg:golang/docker@24.0.4", + "type": "application", + "name": "docker", + "version": "24.0.4", + "purl": "pkg:golang/docker@24.0.4", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "docker" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "node" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fapiserver@1.27.4", + "type": "application", + "name": "k8s.io/apiserver", + "version": "1.27.4", + "purl": "pkg:k8s/k8s.io%2Fapiserver@1.27.4", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "kube-apiserver-minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "controlPlane" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.4", + "type": "application", + "name": "k8s.io/controller-manager", + "version": "1.27.4", + "purl": "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.4", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "kube-controller-manager-minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "controlPlane" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fkube-proxy@1.27.4", + "type": "application", + "name": "k8s.io/kube-proxy", + "version": "1.27.4", + "purl": "pkg:k8s/k8s.io%2Fkube-proxy@1.27.4", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "kube-proxy-4wftc" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "node" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.4", + "type": "application", + "name": "k8s.io/kube-scheduler", + "version": "1.27.4", + "purl": "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.4", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "kube-scheduler-minikube" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "controlPlane" + } + ] + }, + { + "bom-ref": "pkg:k8s/k8s.io%2Fkubelet@1.27.4", + "type": "application", + "name": "k8s.io/kubelet", + "version": "1.27.4", + "purl": "pkg:k8s/k8s.io%2Fkubelet@1.27.4", + "properties": [ + { + "name": "aquasecurity:trivy:resource:Name", + "value": "k8s.io/kubelet" + }, + { + "name": "aquasecurity:trivy:resource:Type", + "value": "node" + } + ] + }, + { + "bom-ref": "pkg:oci/coredns@sha256%3Aa0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e?repository_url=registry.k8s.io%2Fcoredns%2Fcoredns", + "type": "container", + "name": "registry.k8s.io/coredns/coredns", + "version": "sha256:a0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e", + "purl": "pkg:oci/coredns@sha256%3Aa0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e?repository_url=registry.k8s.io%2Fcoredns%2Fcoredns", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/coredns/coredns:1.10.1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/etcd@sha256%3A51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83?repository_url=registry.k8s.io%2Fetcd", + "type": "container", + "name": "registry.k8s.io/etcd", + "version": "sha256:51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83", + "purl": "pkg:oci/etcd@sha256%3A51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83?repository_url=registry.k8s.io%2Fetcd", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/etcd:3.5.7-0" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/kube-apiserver@sha256%3A697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d?repository_url=registry.k8s.io%2Fkube-apiserver", + "type": "container", + "name": "registry.k8s.io/kube-apiserver", + "version": "sha256:697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d", + "purl": "pkg:oci/kube-apiserver@sha256%3A697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d?repository_url=registry.k8s.io%2Fkube-apiserver", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/kube-apiserver:1.27.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/kube-controller-manager@sha256%3A6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265?repository_url=registry.k8s.io%2Fkube-controller-manager", + "type": "container", + "name": "registry.k8s.io/kube-controller-manager", + "version": "sha256:6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265", + "purl": "pkg:oci/kube-controller-manager@sha256%3A6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265?repository_url=registry.k8s.io%2Fkube-controller-manager", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/kube-controller-manager:1.27.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/kube-proxy@sha256%3A4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf?repository_url=registry.k8s.io%2Fkube-proxy", + "type": "container", + "name": "registry.k8s.io/kube-proxy", + "version": "sha256:4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf", + "purl": "pkg:oci/kube-proxy@sha256%3A4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf?repository_url=registry.k8s.io%2Fkube-proxy", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/kube-proxy:1.27.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + }, + { + "bom-ref": "pkg:oci/kube-scheduler@sha256%3A5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af?repository_url=registry.k8s.io%2Fkube-scheduler", + "type": "container", + "name": "registry.k8s.io/kube-scheduler", + "version": "sha256:5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af", + "purl": "pkg:oci/kube-scheduler@sha256%3A5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af?repository_url=registry.k8s.io%2Fkube-scheduler", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "registry.k8s.io/kube-scheduler:1.27.4" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "oci" + } + ] + } + ], + "dependencies": [ + { + "ref": "5262e708-f1a3-4fca-a1c3-0a8384f7f4a5", + "dependsOn": [] + }, + { + "ref": "a62abb1f-cb38-4fde-90f3-2bda3b87ddb2", + "dependsOn": [ + "pkg:golang/docker@24.0.4", + "pkg:k8s/k8s.io%2Fkubelet@1.27.4" + ] + }, + { + "ref": "a6350ac3-52f6-4c5f-a3e3-184b9a634bef", + "dependsOn": [ + "5262e708-f1a3-4fca-a1c3-0a8384f7f4a5", + "a62abb1f-cb38-4fde-90f3-2bda3b87ddb2" + ] + }, + { + "ref": "b19a88a3-017d-4e70-a73a-75f48696ec0f", + "dependsOn": [ + "pkg:oci/coredns@sha256%3Aa0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e?repository_url=registry.k8s.io%2Fcoredns%2Fcoredns" + ] + }, + { + "ref": "b1c502c9-3c6e-43af-822b-1cb55c6c6ff3", + "dependsOn": [ + "pkg:oci/etcd@sha256%3A51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83?repository_url=registry.k8s.io%2Fetcd" + ] + }, + { + "ref": "pkg:golang/docker@24.0.4", + "dependsOn": [] + }, + { + "ref": "pkg:k8s/k8s.io%2Fapiserver@1.27.4", + "dependsOn": [ + "pkg:oci/kube-apiserver@sha256%3A697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d?repository_url=registry.k8s.io%2Fkube-apiserver" + ] + }, + { + "ref": "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.4", + "dependsOn": [ + "pkg:oci/kube-controller-manager@sha256%3A6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265?repository_url=registry.k8s.io%2Fkube-controller-manager" + ] + }, + { + "ref": "pkg:k8s/k8s.io%2Fkube-proxy@1.27.4", + "dependsOn": [ + "pkg:oci/kube-proxy@sha256%3A4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf?repository_url=registry.k8s.io%2Fkube-proxy" + ] + }, + { + "ref": "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.4", + "dependsOn": [ + "pkg:oci/kube-scheduler@sha256%3A5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af?repository_url=registry.k8s.io%2Fkube-scheduler" + ] + }, + { + "ref": "pkg:k8s/k8s.io%2Fkubelet@1.27.4", + "dependsOn": [] + }, + { + "ref": "pkg:k8s/k8s.io%2Fkubernetes@1.27.4", + "dependsOn": [ + "a6350ac3-52f6-4c5f-a3e3-184b9a634bef", + "b19a88a3-017d-4e70-a73a-75f48696ec0f", + "b1c502c9-3c6e-43af-822b-1cb55c6c6ff3", + "pkg:k8s/k8s.io%2Fapiserver@1.27.4", + "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.4", + "pkg:k8s/k8s.io%2Fkube-proxy@1.27.4", + "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.4" + ] + }, + { + "ref": "pkg:oci/coredns@sha256%3Aa0ead06651cf580044aeb0a0feba63591858fb2e43ade8c9dea45a6a89ae7e5e?repository_url=registry.k8s.io%2Fcoredns%2Fcoredns", + "dependsOn": [] + }, + { + "ref": "pkg:oci/etcd@sha256%3A51eae8381dcb1078289fa7b4f3df2630cdc18d09fb56f8e56b41c40e191d6c83?repository_url=registry.k8s.io%2Fetcd", + "dependsOn": [] + }, + { + "ref": "pkg:oci/kube-apiserver@sha256%3A697cd88d94f7f2ef42144cb3072b016dcb2e9251f0e7d41a7fede557e555452d?repository_url=registry.k8s.io%2Fkube-apiserver", + "dependsOn": [] + }, + { + "ref": "pkg:oci/kube-controller-manager@sha256%3A6286e500782ad6d0b37a1b8be57fc73f597dc931dfc73ff18ce534059803b265?repository_url=registry.k8s.io%2Fkube-controller-manager", + "dependsOn": [] + }, + { + "ref": "pkg:oci/kube-proxy@sha256%3A4bcb707da9898d2625f5d4edc6d0c96519a24f16db914fc673aa8f97e41dbabf?repository_url=registry.k8s.io%2Fkube-proxy", + "dependsOn": [] + }, + { + "ref": "pkg:oci/kube-scheduler@sha256%3A5897d7a97d23dce25cbf36fcd6e919180a8ef904bf5156583ffdb6a733ab04af?repository_url=registry.k8s.io%2Fkube-scheduler", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/pkg/sbom/cyclonedx/testdata/happy/nested-packages-bom.json b/pkg/sbom/cyclonedx/testdata/happy/nested-packages-bom.json new file mode 100644 index 000000000000..fd7623c73028 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/nested-packages-bom.json @@ -0,0 +1,172 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:12346a7a-5819-43bf-9411-5c146304f023", + "version": 1, + "metadata": { + "timestamp": "2024-12-20T10:57:13+00:00", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "0.38.7-764-g30c7cb137" + } + ] + }, + "component": { + "bom-ref": "1cb40520-a22c-481f-ad77-6bc6960430c5", + "type": "application", + "name": "/test", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "4021d631-e242-4e69-8a93-928665810a27", + "type": "application", + "name": "foo/bar/test.elf", + "properties": [ + { + "name": "aquasecurity:trivy:Class", + "value": "lang-pkgs" + }, + { + "name": "aquasecurity:trivy:Type", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "type": "library", + "name": "github.com/aquasecurity/go-pep440-version", + "version": "v0.0.0-20210121094942-22b2f8951d46", + "purl": "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "type": "library", + "name": "github.com/aquasecurity/go-version", + "version": "v0.0.0-20210121072130-637058cfe492", + "purl": "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/github.com/aquasecurity/test", + "type": "library", + "name": "github.com/aquasecurity/test", + "purl": "pkg:golang/github.com/aquasecurity/test", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "github.com/aquasecurity/test" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "type": "library", + "name": "golang.org/x/xerrors", + "version": "v0.0.0-20200804184101-5ec99f83aff1", + "purl": "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + }, + { + "bom-ref": "pkg:golang/stdlib@v1.15.2", + "type": "library", + "name": "stdlib", + "version": "v1.15.2", + "purl": "pkg:golang/stdlib@v1.15.2", + "properties": [ + { + "name": "aquasecurity:trivy:PkgID", + "value": "stdlib@v1.15.2" + }, + { + "name": "aquasecurity:trivy:PkgType", + "value": "gobinary" + } + ] + } + ], + "dependencies": [ + { + "ref": "1cb40520-a22c-481f-ad77-6bc6960430c5", + "dependsOn": [ + "4021d631-e242-4e69-8a93-928665810a27" + ] + }, + { + "ref": "4021d631-e242-4e69-8a93-928665810a27", + "dependsOn": [ + "pkg:golang/github.com/aquasecurity/test" + ] + }, + { + "ref": "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "dependsOn": [] + }, + { + "ref": "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "dependsOn": [] + }, + { + "ref": "pkg:golang/github.com/aquasecurity/test", + "dependsOn": [ + "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "pkg:golang/stdlib@v1.15.2" + ] + }, + { + "ref": "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "dependsOn": [] + }, + { + "ref": "pkg:golang/stdlib@v1.15.2", + "dependsOn": [] + } + ], + "vulnerabilities": [] +} diff --git a/pkg/sbom/cyclonedx/testdata/happy/os-only-bom.json b/pkg/sbom/cyclonedx/testdata/happy/os-only-bom.json new file mode 100644 index 000000000000..837c16754211 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/os-only-bom.json @@ -0,0 +1,77 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + }, + { + "name": "aquasecurity:trivy:ImageID", + "value": "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "name": "aquasecurity:trivy:DiffID", + "value": "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "name": "aquasecurity:trivy:RepoTag", + "value": "maven-test-project:latest" + } + ] + } + }, + "components": [ + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0", + "properties": [ + { + "name": "aquasecurity:trivy:Type", + "value": "alpine" + }, + { + "name": "aquasecurity:trivy:Class", + "value": "os-pkgs" + } + ] + } + ], + "dependencies": [ + { + "ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "dependsOn": [] + }, + { + "ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "dependsOn": [ + "60e9f57b-d4a6-4f71-ad14-0893ac609182" + ] + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/package-hashes.json b/pkg/sbom/cyclonedx/testdata/happy/package-hashes.json new file mode 100644 index 000000000000..67a2967f15c0 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/package-hashes.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:379ddfdb-306b-44e4-bff3-9bfb9bbc5fa5", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": [ + { + "vendor": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ], + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "maven-test-project" + } + }, + "components": [ + { + "bom-ref": "@angular/animations@19.2.10", + "type": "library", + "name": "@angular/animations", + "version": "19.2.10", + "scope": "required", + "author": "angular", + "description": "Angular - animations integration with web-animations", + "purl": "pkg:npm/%40angular/animations@19.2.10", + "hashes": [ + { + "alg": "SHA-512", + "content": "2e51fa9add03f3e308d0b57c40dc7dfeba8b2efd1609f60f4bfe625d21a92327ec7e52e83b97511a1b52e297506eee60aa69cb75ff62eebe257512637fbc1bfa" + } + ] + } + ] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/third-party-bom-no-os.json b/pkg/sbom/cyclonedx/testdata/happy/third-party-bom-no-os.json new file mode 100644 index 000000000000..436b7dcbf892 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/third-party-bom-no-os.json @@ -0,0 +1,38 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "test-project" + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "type": "library", + "name": "musl", + "version": "1.2.3-r0", + "licenses": [ + { + "license": { + "name": "MIT" + } + } + ], + "purl": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0" + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1" + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/third-party-bom.json b/pkg/sbom/cyclonedx/testdata/happy/third-party-bom.json new file mode 100644 index 000000000000..281d3c4fcc30 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/third-party-bom.json @@ -0,0 +1,56 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "container", + "name": "test-project" + } + }, + "components": [ + { + "bom-ref": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "type": "library", + "name": "musl", + "version": "1.2.3-r0", + "licenses": [ + { + "expression": "MIT" + } + ], + "purl": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0" + }, + { + "bom-ref": "60e9f57b-d4a6-4f71-ad14-0893ac609182", + "type": "operating-system", + "name": "alpine", + "version": "3.16.0" + }, + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1", + "licenses": [ + { + "license": { + "id": "MIT" + } + } + ] + }, + { + "bom-ref": "pkg:composer/pear/pear_exception@v1.0.0", + "type": "framework", + "name": "pear/pear_exception", + "version": "v1.0.0", + "purl": "pkg:composer/pear/pear_exception@v1.0.0" + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/third-party-scan-file-component.json b/pkg/sbom/cyclonedx/testdata/happy/third-party-scan-file-component.json new file mode 100644 index 000000000000..fdc4cc85f347 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/third-party-scan-file-component.json @@ -0,0 +1,106 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:ff7e2292-989f-486b-a18e-29495b085c81", + "version": 1, + "metadata": { + "timestamp": "2025-08-22T09:38:06+02:00", + "tools": { + "components": [ + { + "type": "application", + "author": "anchore", + "name": "third-party", + "version": "[not provided]" + } + ] + }, + "component": { + "bom-ref": "64c458de142e8a93", + "type": "file", + "name": "C:\\dev\\IdeaProjects\\sample-spring-boot\\target\\demo-0.0.1-SNAPSHOT.jar", + "version": "sha256:7444fb30fbf253f431f9b80712ae6bb6fa52f86bae8b0f1b835e124b2baa8203" + } + }, + "components": [ + { + "bom-ref": "pkg:maven/commons-io/commons-io@2.13.0?package-id=6033c6b2f2d3cf10", + "type": "file", + "group": "commons-io", + "name": "commons-io", + "version": "2.13.0", + "licenses": [ + { + "license": { + "id": "Apache-2.0" + } + } + ], + "cpe": "cpe:2.3:a:apache:commons-io:2.13.0:*:*:*:*:*:*:*", + "purl": "pkg:maven/commons-io/commons-io@2.13.0", + "externalReferences": [ + { + "url": "", + "hashes": [ + { + "alg": "SHA-1", + "content": "8bb2bc9b4df17e2411533a0708a69f983bf5e83b" + } + ], + "type": "build-meta" + } + ], + "properties": [ + { + "name": "third-party:package:foundBy", + "value": "java-archive-cataloger" + }, + { + "name": "third-party:package:language", + "value": "java" + }, + { + "name": "third-party:package:type", + "value": "java-archive" + }, + { + "name": "third-party:package:metadataType", + "value": "java-archive" + }, + { + "name": "third-party:cpe23", + "value": "cpe:2.3:a:apache:commons_io:2.13.0:*:*:*:*:*:*:*" + }, + { + "name": "third-party:cpe23", + "value": "cpe:2.3:a:apache:commons:2.13.0:*:*:*:*:*:*:*" + }, + { + "name": "third-party:location:0:path", + "value": "\\demo-0.0.1-SNAPSHOT.jar" + }, + { + "name": "third-party:metadata:-:artifactID", + "value": "commons-io" + }, + { + "name": "third-party:metadata:-:groupID", + "value": "commons-io" + }, + { + "name": "third-party:metadata:virtualPath", + "value": "\\demo-0.0.1-SNAPSHOT.jar:BOOT-INF/lib/commons-io-2.13.0.jar" + } + ] + } + ], + "dependencies": [ + { + "ref": "pkg:maven/com.example/demo@0.0.1-SNAPSHOT?package-id=4ba90badb338f3a9", + "dependsOn": [ + "pkg:maven/commons-io/commons-io@2.13.0?package-id=6033c6b2f2d3cf10" + ] + } + ] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/happy/unrelated-bom.json b/pkg/sbom/cyclonedx/testdata/happy/unrelated-bom.json new file mode 100644 index 000000000000..aecf8e05abfb --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/happy/unrelated-bom.json @@ -0,0 +1,67 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:c986ba94-e37d-49c8-9e30-96daccd0415b", + "version": 1, + "metadata": { + "timestamp": "2022-05-28T10:20:03.79527Z", + "tools": { + "components": [ + { + "type": "application", + "group": "aquasecurity", + "name": "trivy", + "version": "dev" + } + ] + }, + "component": { + "bom-ref": "0f585d64-4815-4b72-92c5-97dae191fa4a", + "type": "application", + "name": "maven-test-project", + "properties": [ + { + "name": "aquasecurity:trivy:SchemaVersion", + "value": "2" + } + ] + } + }, + "components": [ + { + "bom-ref": "pkg:composer/pear/log@1.13.1", + "type": "library", + "name": "pear/log", + "version": "1.13.1", + "purl": "pkg:composer/pear/log@1.13.1" + }, + { + "bom-ref": "pkg:composer/pear/pear_exception@v1.0.0", + "type": "library", + "name": "pear/pear_exception", + "version": "v1.0.0", + "purl": "pkg:composer/pear/pear_exception@v1.0.0" + }, + { + "bom-ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "type": "application", + "name": "app/composer/composer.lock" + } + ], + "dependencies": [ + { + "ref": "100925ff-7c0a-470f-a725-8fb973b40e7b", + "dependsOn": [ + "pkg:composer/pear/log@1.13.1", + "pkg:composer/pear/pear_exception@v1.0.0" + ] + }, + { + "ref": "pkg:composer/pear/log@1.13.1" + }, + { + "ref": "pkg:composer/pear/pear_exception@v1.0.0" + } + ], + "vulnerabilities": [] +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/testdata/sad/invalid-serial.json b/pkg/sbom/cyclonedx/testdata/sad/invalid-serial.json new file mode 100644 index 000000000000..4edeb3f3ec13 --- /dev/null +++ b/pkg/sbom/cyclonedx/testdata/sad/invalid-serial.json @@ -0,0 +1,6 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": 1000000, + "version": 1 +} \ No newline at end of file diff --git a/pkg/sbom/cyclonedx/unmarshal.go b/pkg/sbom/cyclonedx/unmarshal.go new file mode 100644 index 000000000000..ff3e1e1f5025 --- /dev/null +++ b/pkg/sbom/cyclonedx/unmarshal.go @@ -0,0 +1,330 @@ +package cyclonedx + +import ( + "bytes" + "errors" + "fmt" + "io" + "strings" + + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/digest" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom/core" +) + +var ( + ErrUnsupportedType = errors.New("unsupported type") +) + +type BOM struct { + *core.BOM +} + +func DecodeJSON(r io.Reader) (*cdx.BOM, error) { + bom := cdx.NewBOM() + decoder := cdx.NewBOMDecoder(r, cdx.BOMFileFormatJSON) + if err := decoder.Decode(bom); err != nil { + return nil, xerrors.Errorf("CycloneDX decode error: %w", err) + } + return bom, nil +} + +func (b *BOM) UnmarshalJSON(data []byte) error { + log.Debug("Unmarshalling CycloneDX JSON...") + if b.BOM == nil { + b.BOM = core.NewBOM(core.Options{GenerateBOMRef: true}) + } + + cdxBOM, err := DecodeJSON(bytes.NewReader(data)) + if err != nil { + return xerrors.Errorf("CycloneDX decode error: %w", err) + } + + if !IsTrivySBOM(cdxBOM) { + log.Warn("Third-party SBOM may lead to inaccurate vulnerability detection") + log.Warn("Recommend using Trivy to generate SBOMs") + } + + if err = b.parseBOM(cdxBOM); err != nil { + return xerrors.Errorf("failed to parse sbom: %w", err) + } + + // Store the original metadata + b.BOM.SerialNumber = cdxBOM.SerialNumber + b.BOM.Version = cdxBOM.Version + + return nil +} + +func (b *BOM) parseBOM(bom *cdx.BOM) error { + // Convert all CycloneDX components into Trivy components + components := b.parseComponents(bom.Components) + + // Convert the metadata component into Trivy component + mComponent, err := b.parseMetadataComponent(bom) + if err != nil { + return xerrors.Errorf("failed to parse root component: %w", err) + } else if mComponent != nil { + components[mComponent.PkgIdentifier.BOMRef] = mComponent + } + + // Parse dependencies and build relationships + for _, dep := range lo.FromPtr(bom.Dependencies) { + ref, ok := components[dep.Ref] + if !ok { + continue + } + + dependencies := lo.FromPtr(dep.Dependencies) + if len(dependencies) == 0 { + // Empty dependsOn array - create empty relationship to preserve this information + b.BOM.AddRelationship(ref, nil, core.RelationshipDependsOn) + } else { + // Process actual dependencies + for _, depRef := range dependencies { + dependency, ok := components[depRef] + if !ok { + continue + } + b.BOM.AddRelationship(ref, dependency, core.RelationshipDependsOn) + } + } + } + + if refs := b.parseExternalReferences(bom); refs != nil { + b.BOM.AddExternalReferences(refs) + } + + return nil +} + +func (b *BOM) parseMetadataComponent(bom *cdx.BOM) (*core.Component, error) { + if bom.Metadata == nil || bom.Metadata.Component == nil { + return nil, nil + } + root, err := b.parseComponent(*bom.Metadata.Component) + if err != nil { + return nil, xerrors.Errorf("failed to parse metadata component: %w", err) + } + root.Root = true + b.BOM.AddComponent(root) + return root, nil +} + +func (b *BOM) parseExternalReferences(bom *cdx.BOM) []core.ExternalReference { + if bom.ExternalReferences == nil { + return nil + } + var refs = make([]core.ExternalReference, 0) + + for _, ref := range *bom.ExternalReferences { + t, err := b.unmarshalReferenceType(ref.Type) + if err != nil { + continue + } + + externalReference := core.ExternalReference{ + Type: t, + URL: ref.URL, + } + + refs = append(refs, externalReference) + } + return refs +} + +func (b *BOM) unmarshalReferenceType(t cdx.ExternalReferenceType) (core.ExternalReferenceType, error) { + var referenceType core.ExternalReferenceType + switch t { + case cdx.ERTypeExploitabilityStatement: + referenceType = core.ExternalReferenceVEX + default: + return "", fmt.Errorf("unsupported external reference type: %s", t) + } + return referenceType, nil +} + +func (b *BOM) parseComponents(cdxComponents *[]cdx.Component) map[string]*core.Component { + components := make(map[string]*core.Component) + for _, component := range lo.FromPtr(cdxComponents) { + c, err := b.parseComponent(component) + if errors.Is(err, ErrUnsupportedType) { + log.Info("Skipping the component with the unsupported type", + log.String("bom-ref", component.BOMRef), log.String("type", string(component.Type))) + continue + } else if err != nil { + log.Warn("Failed to parse component", log.Err(err)) + continue + } + + b.BOM.AddComponent(c) + components[component.BOMRef] = c + } + return components +} + +func (b *BOM) parseComponent(c cdx.Component) (*core.Component, error) { + componentType, err := b.unmarshalType(c.Type) + if err != nil { + return nil, xerrors.Errorf("failed to unmarshal component type: %w", err) + } + + identifier := ftypes.PkgIdentifier{ + BOMRef: c.BOMRef, + } + + // Parse PURL + if c.PackageURL != "" { + purl, err := packageurl.FromString(c.PackageURL) + if err != nil { + return nil, xerrors.Errorf("failed to parse PURL: %w", err) + } + identifier.PURL = &purl + } + + component := &core.Component{ + Type: componentType, + Name: c.Name, + Group: c.Group, + Version: c.Version, + Licenses: b.unmarshalLicenses(c.Licenses), + Files: []core.File{ + { + Digests: b.unmarshalHashes(c.Hashes), + }, + }, + PkgIdentifier: identifier, + Supplier: b.unmarshalSupplier(c.Supplier), + Properties: b.unmarshalProperties(c.Properties), + } + + return component, nil +} + +func (b *BOM) unmarshalType(t cdx.ComponentType) (core.ComponentType, error) { + var ctype core.ComponentType + switch t { + case cdx.ComponentTypeContainer: + ctype = core.TypeContainerImage + case cdx.ComponentTypeApplication: + ctype = core.TypeApplication + // There are not many differences between a `library` and a `framework` components, and sometimes it is difficult to choose the right type. + // That is why some users choose `framework` type. + // So we should parse and scan `framework` components as libraries. + case cdx.ComponentTypeLibrary, cdx.ComponentTypeFramework: + ctype = core.TypeLibrary + case cdx.ComponentTypeOS: + ctype = core.TypeOS + case cdx.ComponentTypePlatform: + ctype = core.TypePlatform + case cdx.ComponentTypeFile: + ctype = core.TypeFilesystem + default: + return "", ErrUnsupportedType + } + return ctype, nil +} + +// parsePackageLicenses checks all supported license fields and returns a list of licenses. +// https://cyclonedx.org/docs/1.5/json/#components_items_licenses +func (b *BOM) unmarshalLicenses(l *cdx.Licenses) []string { + var licenses []string + for _, license := range lo.FromPtr(l) { + if license.License != nil { + // Trivy uses `Name` field to marshal licenses + if license.License.Name != "" { + licenses = append(licenses, license.License.Name) + continue + } + + if license.License.ID != "" { + licenses = append(licenses, license.License.ID) + continue + } + } + + if license.Expression != "" { + licenses = append(licenses, license.Expression) + continue + } + + } + return licenses +} + +func (b *BOM) unmarshalHashes(hashes *[]cdx.Hash) []digest.Digest { + var digests []digest.Digest + for _, h := range lo.FromPtr(hashes) { + var alg digest.Algorithm + switch h.Algorithm { + case cdx.HashAlgoSHA1: + alg = digest.SHA1 + case cdx.HashAlgoSHA256: + alg = digest.SHA256 + case cdx.HashAlgoSHA512: + alg = digest.SHA512 + case cdx.HashAlgoMD5: + alg = digest.MD5 + default: + log.Warn("Unsupported hash algorithm", log.String("algorithm", string(h.Algorithm))) + } + digests = append(digests, digest.NewDigestFromString(alg, h.Value)) + } + return digests +} + +func (b *BOM) unmarshalSupplier(supplier *cdx.OrganizationalEntity) string { + if supplier == nil { + return "" + } + return supplier.Name +} + +func (b *BOM) unmarshalProperties(properties *[]cdx.Property) []core.Property { + var props []core.Property + for _, p := range lo.FromPtr(properties) { + prop := core.Property{ + Value: p.Value, + } + + // If the property has the Trivy namespace prefix, it's a Trivy property + if name, found := strings.CutPrefix(p.Name, Namespace); found { + prop.Name = name + prop.External = false // Trivy property (default) + } else { + // External property - preserve the original name and mark as external + prop.Name = p.Name + prop.External = true + } + + props = append(props, prop) + } + return props +} + +func IsTrivySBOM(c *cdx.BOM) bool { + if c == nil || c.Metadata == nil || c.Metadata.Tools == nil { + return false + } + + for _, component := range lo.FromPtr(c.Metadata.Tools.Components) { + if component.Group == ToolVendor && component.Name == ToolName { + return true + } + } + + // Metadata.Tools array is deprecated (as of CycloneDX v1.5). We check this field for backward compatibility. + // cf. https://github.com/CycloneDX/cyclonedx-go/blob/b9654ae9b4705645152d20eb9872b5f3d73eac49/cyclonedx.go#L988 + for _, tool := range lo.FromPtr(c.Metadata.Tools.Tools) { + if tool.Vendor == ToolVendor && tool.Name == ToolName { + return true + } + } + return false +} diff --git a/pkg/sbom/cyclonedx/unmarshal_test.go b/pkg/sbom/cyclonedx/unmarshal_test.go new file mode 100644 index 000000000000..b99db8b8f09d --- /dev/null +++ b/pkg/sbom/cyclonedx/unmarshal_test.go @@ -0,0 +1,965 @@ +package cyclonedx_test + +import ( + "encoding/json" + "os" + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" + sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestUnmarshaler_Unmarshal(t *testing.T) { + tests := []struct { + name string + inputFile string + want types.SBOM + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/happy/bom.json", + want: types.SBOM{ + Metadata: types.Metadata{ + ImageID: "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5", + DiffIDs: []string{ + "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + RepoTags: []string{ + "maven-test-project:latest", + }, + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.16.0", + }, + }, + Packages: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "musl@1.2.3-r0", + Name: "musl", + Version: "1.2.3-r0", + SrcName: "musl", + SrcVersion: "1.2.3-r0", + Licenses: []string{"MIT"}, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.2.3-r0", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.16.0", + }, + }, + }, + BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + }, + Layer: ftypes.Layer{ + DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + }, + }, + }, + }, + Applications: []ftypes.Application{ + { + Type: ftypes.Composer, + FilePath: "app/composer/composer.lock", + Packages: ftypes.Packages{ + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + BOMRef: "pkg:composer/pear/log@1.13.1", + }, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + { + ID: "pear/pear_exception@v1.0.0", + Name: "pear/pear_exception", + Version: "v1.0.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "pear_exception", + Version: "v1.0.0", + }, + BOMRef: "pkg:composer/pear/pear_exception@v1.0.0", + }, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: ftypes.GoBinary, + FilePath: "app/gobinary/gobinary", + Packages: ftypes.Packages{ + { + ID: "github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + Name: "github.com/package-url/packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/package-url", + Name: "packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + }, + BOMRef: "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + }, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: ftypes.Gradle, + FilePath: "app/gradle/target/gradle.lockfile", + Packages: ftypes.Packages{ + { + ID: "com.example:example:0.0.1", + Name: "com.example:example", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.example", + Name: "example", + Version: "0.0.1", + }, + BOMRef: "pkg:maven/com.example/example@0.0.1", + }, + Version: "0.0.1", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: ftypes.Jar, + Packages: ftypes.Packages{ + { + ID: "org.codehaus.mojo:child-project:1.0", + Name: "org.codehaus.mojo:child-project", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.codehaus.mojo", + Name: "child-project", + Version: "1.0", + }, + BOMRef: "pkg:maven/org.codehaus.mojo/child-project@1.0?file_path=app%2Fmaven%2Ftarget%2Fchild-project-1.0.jar", + }, + Version: "1.0", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + FilePath: "app/maven/target/child-project-1.0.jar", + }, + }, + }, + { + Type: ftypes.NodePkg, + FilePath: "", + Packages: ftypes.Packages{ + { + ID: "@example/bootstrap@5.0.2", + Name: "@example/bootstrap", + Version: "5.0.2", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Namespace: "@example", + Name: "bootstrap", + Version: "5.0.2", + }, + BOMRef: "pkg:npm/@example/bootstrap@5.0.2?file_path=app%2Fapp%2Fpackage.json", + }, + Licenses: []string{"MIT"}, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + FilePath: "app/app/package.json", + }, + }, + }, + }, + }, + }, + { + name: "happy path KBOM", + inputFile: "testdata/happy/kbom.json", + want: types.SBOM{ + Metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: "ubuntu", + Name: "22.04.2", + }, + }, + Applications: []ftypes.Application{ + { + Type: ftypes.GoBinary, + Packages: ftypes.Packages{ + { + ID: "docker@v24.0.4", + Name: "docker", + Version: "24.0.4", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Name: "docker", + Version: "24.0.4", + }, + BOMRef: "pkg:golang/docker@24.0.4", + }, + }, + }, + }, + { + Type: ftypes.K8sUpstream, + Packages: ftypes.Packages{ + { + ID: "k8s.io/apiserver@1.27.4", + Name: "k8s.io/apiserver", + Version: "1.27.4", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: purl.TypeK8s, + Name: "k8s.io/apiserver", + Version: "1.27.4", + }, + BOMRef: "pkg:k8s/k8s.io%2Fapiserver@1.27.4", + }, + }, + { + ID: "k8s.io/controller-manager@1.27.4", + Name: "k8s.io/controller-manager", + Version: "1.27.4", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: purl.TypeK8s, + Name: "k8s.io/controller-manager", + Version: "1.27.4", + }, + BOMRef: "pkg:k8s/k8s.io%2Fcontroller-manager@1.27.4", + }, + }, + { + ID: "k8s.io/kube-proxy@1.27.4", + Name: "k8s.io/kube-proxy", + Version: "1.27.4", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: purl.TypeK8s, + Name: "k8s.io/kube-proxy", + Version: "1.27.4", + }, + BOMRef: "pkg:k8s/k8s.io%2Fkube-proxy@1.27.4", + }, + }, + { + ID: "k8s.io/kube-scheduler@1.27.4", + Name: "k8s.io/kube-scheduler", + Version: "1.27.4", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: purl.TypeK8s, + Name: "k8s.io/kube-scheduler", + Version: "1.27.4", + }, + BOMRef: "pkg:k8s/k8s.io%2Fkube-scheduler@1.27.4", + }, + }, + { + ID: "k8s.io/kubelet@1.27.4", + Name: "k8s.io/kubelet", + Version: "1.27.4", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: purl.TypeK8s, + Name: "k8s.io/kubelet", + Version: "1.27.4", + }, + BOMRef: "pkg:k8s/k8s.io%2Fkubelet@1.27.4", + }, + }, + { + ID: "k8s.io/kubernetes@1.27.4", + Name: "k8s.io/kubernetes", + Version: "1.27.4", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: purl.TypeK8s, + Name: "k8s.io/kubernetes", + Version: "1.27.4", + }, + BOMRef: "pkg:k8s/k8s.io%2Fkubernetes@1.27.4", + }, + DependsOn: []string{ + "k8s.io/apiserver@1.27.4", + "k8s.io/controller-manager@1.27.4", + "k8s.io/kube-proxy@1.27.4", + "k8s.io/kube-scheduler@1.27.4", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with infinity loop", + inputFile: "testdata/happy/infinite-loop-bom.json", + want: types.SBOM{ + Metadata: types.Metadata{ + ImageID: "sha256:08d22c0ceb150ddeb2237c5fa3129c0183f3cc6f5eeb2e7aa4016da3ad02140a", + DiffIDs: []string{ + "sha256:b93c1bd012ab8fda60f5b4f5906bf244586e0e3292d84571d3abb56472248466", + }, + RepoTags: []string{ + "ubuntu:latest", + }, + RepoDigests: []string{ + "ubuntu@sha256:67211c14fa74f070d27cc59d69a7fa9aeff8e28ea118ef3babc295a0428a6d21", + }, + OS: &ftypes.OS{ + Family: "ubuntu", + Name: "22.04", + }, + }, + Packages: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "libc6@2.35-0ubuntu3.1", + Name: "libc6", + Version: "2.35-0ubuntu3.1", + SrcName: "glibc", + SrcVersion: "2.35", + SrcRelease: "0ubuntu3.1", + Licenses: []string{ + "LGPL-2.1", + "GPL-2.0", + "GFDL-1.3", + }, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "ubuntu", + Name: "libc6", + Version: "2.35-0ubuntu3.1", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "ubuntu-22.04", + }, + }, + }, + BOMRef: "pkg:deb/ubuntu/libc6@2.35-0ubuntu3.1?distro=ubuntu-22.04", + }, + DependsOn: []string{ + "libcrypt1@1:4.4.27-1", + }, + Layer: ftypes.Layer{ + Digest: "sha256:74ac377868f863e123f24c409f79709f7563fa464557c36a09cf6f85c8b92b7f", + DiffID: "sha256:b93c1bd012ab8fda60f5b4f5906bf244586e0e3292d84571d3abb56472248466", + }, + }, + { + ID: "libcrypt1@1:4.4.27-1", + Name: "libcrypt1", + Version: "4.4.27-1", + Epoch: 1, + SrcName: "libxcrypt", + SrcVersion: "4.4.27", + SrcRelease: "1", + SrcEpoch: 1, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "ubuntu", + Name: "libcrypt1", + Version: "4.4.27-1", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "ubuntu-22.04", + }, + { + Key: "epoch", + Value: "1", + }, + }, + }, + BOMRef: "pkg:deb/ubuntu/libcrypt1@4.4.27-1?epoch=1&distro=ubuntu-22.04", + }, + DependsOn: []string{ + "libc6@2.35-0ubuntu3.1", + }, + Layer: ftypes.Layer{ + Digest: "sha256:74ac377868f863e123f24c409f79709f7563fa464557c36a09cf6f85c8b92b7f", + DiffID: "sha256:b93c1bd012ab8fda60f5b4f5906bf244586e0e3292d84571d3abb56472248466", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for third party sbom", + inputFile: "testdata/happy/third-party-bom.json", + want: types.SBOM{ + Metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.16.0", + }, + }, + Packages: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "musl@1.2.3-r0", + Name: "musl", + Version: "1.2.3-r0", + SrcName: "musl", + SrcVersion: "1.2.3-r0", + Licenses: []string{"MIT"}, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.2.3-r0", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.16.0", + }, + }, + }, + BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + }, + }, + }, + }, + }, + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "", + Packages: ftypes.Packages{ + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + BOMRef: "pkg:composer/pear/log@1.13.1", + }, + Licenses: []string{"MIT"}, + }, + { + ID: "pear/pear_exception@v1.0.0", + Name: "pear/pear_exception", + Version: "v1.0.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "pear_exception", + Version: "v1.0.0", + }, + BOMRef: "pkg:composer/pear/pear_exception@v1.0.0", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for third party sbom, no operation-system component", + inputFile: "testdata/happy/third-party-bom-no-os.json", + want: types.SBOM{ + Packages: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "musl@1.2.3-r0", + Name: "musl", + Version: "1.2.3-r0", + SrcName: "musl", + SrcVersion: "1.2.3-r0", + Licenses: []string{"MIT"}, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.2.3-r0", + Qualifiers: packageurl.Qualifiers{ + {Key: "distro", Value: "3.16.0"}, + }, + }, + BOMRef: "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + }, + }, + }, + }, + }, + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "", + Packages: ftypes.Packages{ + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + BOMRef: "pkg:composer/pear/log@1.13.1", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for unrelated bom", + inputFile: "testdata/happy/unrelated-bom.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "", + Packages: ftypes.Packages{ + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + BOMRef: "pkg:composer/pear/log@1.13.1", + }, + }, + { + ID: "pear/pear_exception@v1.0.0", + Name: "pear/pear_exception", + Version: "v1.0.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "pear_exception", + Version: "v1.0.0", + }, + BOMRef: "pkg:composer/pear/pear_exception@v1.0.0", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for independent library bom", + inputFile: "testdata/happy/independent-library-bom.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "", + Packages: ftypes.Packages{ + { + ID: "pear/core@1.13.1", + Name: "pear/core", + Version: "1.13.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "core", + Version: "1.13.1", + }, + BOMRef: "pkg:composer/pear/core@1.13.1", + }, + DependsOn: []string{ + "pear/log@1.13.1", + "pear/pear_exception@v1.0.0", + }, + }, + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + BOMRef: "pkg:composer/pear/log@1.13.1", + }, + }, + { + ID: "pear/pear_exception@v1.0.0", + Name: "pear/pear_exception", + Version: "v1.0.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "pear_exception", + Version: "v1.0.0", + }, + BOMRef: "pkg:composer/pear/pear_exception@v1.0.0", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for jar where name is GroupID and ArtifactID", + inputFile: "testdata/happy/group-in-name.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: "jar", + Packages: ftypes.Packages{ + { + ID: "org.springframework:spring-web:5.3.22", + Name: "org.springframework:spring-web", + Version: "5.3.22", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework", + Name: "spring-web", + Version: "5.3.22", + }, + BOMRef: "pkg:maven/org.springframework/spring-web@5.3.22?file_path=spring-web-5.3.22.jar", + }, + FilePath: "spring-web-5.3.22.jar", + }, + }, + }, + }, + }, + }, + { + name: "happy path for BOM with nested packages", + inputFile: "testdata/happy/nested-packages-bom.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: "gobinary", + FilePath: "foo/bar/test.elf", + Packages: ftypes.Packages{ + { + ID: "github.com/aquasecurity/test", + Name: "github.com/aquasecurity/test", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "test", + }, + BOMRef: "pkg:golang/github.com/aquasecurity/test", + }, + DependsOn: []string{ + "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + "stdlib@v1.15.2", + }, + }, + { + ID: "github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + Name: "github.com/aquasecurity/go-pep440-version", + Version: "v0.0.0-20210121094942-22b2f8951d46", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-pep440-version", + Version: "v0.0.0-20210121094942-22b2f8951d46", + }, + BOMRef: "pkg:golang/github.com/aquasecurity/go-pep440-version@v0.0.0-20210121094942-22b2f8951d46", + }, + }, + { + ID: "github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + Name: "github.com/aquasecurity/go-version", + Version: "v0.0.0-20210121072130-637058cfe492", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-version", + Version: "v0.0.0-20210121072130-637058cfe492", + }, + BOMRef: "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20210121072130-637058cfe492", + }, + }, + { + ID: "golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + Name: "golang.org/x/xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "golang.org/x", + Name: "xerrors", + Version: "v0.0.0-20200804184101-5ec99f83aff1", + }, + BOMRef: "pkg:golang/golang.org/x/xerrors@v0.0.0-20200804184101-5ec99f83aff1", + }, + }, + { + ID: "stdlib@v1.15.2", + Name: "stdlib", + Version: "v1.15.2", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Name: "stdlib", + Version: "v1.15.2", + }, + BOMRef: "pkg:golang/stdlib@v1.15.2", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path only os component", + inputFile: "testdata/happy/os-only-bom.json", + want: types.SBOM{ + Metadata: types.Metadata{ + ImageID: "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5", + DiffIDs: []string{ + "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + RepoTags: []string{ + "maven-test-project:latest", + }, + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.16.0", + }, + }, + }, + }, + { + name: "happy path empty component", + inputFile: "testdata/happy/empty-bom.json", + want: types.SBOM{ + Metadata: types.Metadata{ + ImageID: "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5", + DiffIDs: []string{ + "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + RepoTags: []string{ + "maven-test-project:latest", + }, + }, + }, + }, + { + name: "happy path empty metadata component", + inputFile: "testdata/happy/empty-metadata-component-bom.json", + want: types.SBOM{}, + }, + { + name: "invalid purl", + inputFile: "testdata/happy/invalid-purl.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: ftypes.Composer, + Packages: ftypes.Packages{ + { + ID: "pear/core@1.13.1", + Name: "pear/core", + Version: "1.13.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "core", + Version: "1.13.1", + }, + BOMRef: "pkg:composer/pear/core@1.13.1", + }, + }, + }, + }, + }, + }, + }, + { + name: "SHA-512", + inputFile: "testdata/happy/package-hashes.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: ftypes.NodePkg, + Packages: ftypes.Packages{ + { + ID: "@angular/animations@19.2.10", + Name: "@angular/animations", + Version: "19.2.10", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Namespace: "@angular", + Name: "animations", + Version: "19.2.10", + }, + BOMRef: "@angular/animations@19.2.10", + }, + Digest: "sha512:2e51fa9add03f3e308d0b57c40dc7dfeba8b2efd1609f60f4bfe625d21a92327ec7e52e83b97511a1b52e297506eee60aa69cb75ff62eebe257512637fbc1bfa", + }, + }, + }, + }, + }, + }, + { + name: "happy path - third-party SBOM scan of `file` root component", + inputFile: "testdata/happy/third-party-scan-file-component.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: "jar", + FilePath: "", + Packages: ftypes.Packages{ + ftypes.Package{ + ID: "commons-io:commons-io:2.13.0", + Name: "commons-io:commons-io", + Identifier: ftypes.PkgIdentifier{ + UID: "", + PURL: &packageurl.PackageURL{ + Type: "maven", + Namespace: "commons-io", + Name: "commons-io", + Version: "2.13.0", + Qualifiers: nil, + Subpath: "", + }, + BOMRef: "pkg:maven/commons-io/commons-io@2.13.0?package-id=6033c6b2f2d3cf10", + }, + Version: "2.13.0", + Release: "", + Epoch: 0, + Arch: "", + Dev: false, + SrcName: "", + SrcVersion: "", + SrcRelease: "", + SrcEpoch: 0, + Licenses: []string{"Apache-2.0"}, + Maintainer: "", + ExternalReferences: nil, + Modularitylabel: "", + BuildInfo: nil, + Indirect: false, + Relationship: 0, + DependsOn: nil, + Layer: ftypes.Layer{ + Size: 0, + Digest: "", + DiffID: "", + CreatedBy: "", + }, + FilePath: "", + Digest: "", + Locations: nil, + InstalledFiles: nil, + }, + }, + }, + }, + }, + }, + { + name: "invalid serial", + inputFile: "testdata/sad/invalid-serial.json", + wantErr: "CycloneDX decode error", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + var cdx cyclonedx.BOM + err = json.NewDecoder(f).Decode(&cdx) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + var got types.SBOM + err = sbomio.NewDecoder(cdx.BOM).Decode(t.Context(), &got) + require.NoError(t, err) + + got.BOM = nil + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/sbom/io/decode.go b/pkg/sbom/io/decode.go new file mode 100644 index 000000000000..7265c18b6598 --- /dev/null +++ b/pkg/sbom/io/decode.go @@ -0,0 +1,438 @@ +package io + +import ( + "context" + "errors" + "slices" + "sort" + "strconv" + "sync" + + debver "github.com/knqyf263/go-deb-version" + rpmver "github.com/knqyf263/go-rpm-version" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/dependency" + "github.com/aquasecurity/trivy/pkg/fanal/image/name" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" +) + +var ( + ErrPURLEmpty = errors.New("purl empty error") + ErrUnsupportedType = errors.New("unsupported type") +) + +type Decoder struct { + bom *core.BOM + + osID uuid.UUID + pkgs map[uuid.UUID]*ftypes.Package + apps map[uuid.UUID]*ftypes.Application + + logger *log.Logger +} + +func NewDecoder(bom *core.BOM) *Decoder { + return &Decoder{ + bom: bom, + pkgs: make(map[uuid.UUID]*ftypes.Package), + apps: make(map[uuid.UUID]*ftypes.Application), + logger: log.WithPrefix("sbom"), + } +} + +func (m *Decoder) Decode(ctx context.Context, sbom *types.SBOM) error { + // Parse the root component + if err := m.decodeRoot(sbom); err != nil { + return xerrors.Errorf("failed to decode root component: %w", err) + } + + // Parse all components + if err := m.decodeComponents(ctx, sbom); err != nil { + return xerrors.Errorf("failed to decode components: %w", err) + } + + // Build dependency graph between packages + m.buildDependencyGraph() + + // Add OS packages + m.addOSPkgs(sbom) + + // Add language-specific packages + m.addLangPkgs(sbom) + + // Add remaining packages + if err := m.addOrphanPkgs(sbom); err != nil { + return xerrors.Errorf("failed to aggregate packages: %w", err) + } + + sort.Slice(sbom.Applications, func(i, j int) bool { + if sbom.Applications[i].Type != sbom.Applications[j].Type { + return sbom.Applications[i].Type < sbom.Applications[j].Type + } + return sbom.Applications[i].FilePath < sbom.Applications[j].FilePath + }) + + sbom.BOM = m.bom + + return nil +} + +func (m *Decoder) decodeRoot(s *types.SBOM) error { + root := m.bom.Root() + if root == nil { + return nil // No root found + } + + var err error + for _, prop := range root.Properties { + switch prop.Name { + case core.PropertyImageID: + s.Metadata.ImageID = prop.Value + case core.PropertySize: + if s.Metadata.Size, err = strconv.ParseInt(prop.Value, 10, 64); err != nil { + return xerrors.Errorf("failed to convert size: %w", err) + } + case core.PropertyRepoDigest: + s.Metadata.RepoDigests = append(s.Metadata.RepoDigests, prop.Value) + case core.PropertyDiffID: + s.Metadata.DiffIDs = append(s.Metadata.DiffIDs, prop.Value) + case core.PropertyRepoTag: + s.Metadata.RepoTags = append(s.Metadata.RepoTags, prop.Value) + case core.PropertyReference: + if s.Metadata.Reference, err = name.ParseReference(prop.Value); err != nil { + m.logger.Warn("Failed to parse image reference", log.String("value", prop.Value), log.Err(err)) + } + } + } + return nil +} + +func (m *Decoder) decodeComponents(ctx context.Context, sbom *types.SBOM) error { + onceMultiOSWarn := sync.OnceFunc(func() { + m.logger.WarnContext(ctx, "Multiple OS components are not supported, taking the first one and ignoring the rest") + }) + + for id, c := range m.bom.Components() { + switch c.Type { + case core.TypeOS: + if m.osID != uuid.Nil { + onceMultiOSWarn() + continue + } + m.osID = id + sbom.Metadata.OS = &ftypes.OS{ + Family: ftypes.OSType(c.Name), + Name: c.Version, + } + continue + case core.TypeApplication: + if app := m.decodeApplication(c); app.Type != "" { + m.apps[id] = app + continue + } + } + + // Third-party SBOMs may contain packages in types other than "Library" + if c.Type == core.TypeLibrary || c.PkgIdentifier.PURL != nil { + pkg, err := m.decodePackage(ctx, c) + if errors.Is(err, ErrUnsupportedType) || errors.Is(err, ErrPURLEmpty) { + continue + } else if err != nil { + return xerrors.Errorf("failed to decode package: %w", err) + } + m.pkgs[id] = pkg + } + } + + return nil +} + +// buildDependencyGraph builds a dependency graph between packages +func (m *Decoder) buildDependencyGraph() { + for id, rels := range m.bom.Relationships() { + pkg, ok := m.pkgs[id] + if !ok { + continue + } + for _, rel := range rels { + dep, ok := m.pkgs[rel.Dependency] + if !ok { + continue + } + pkg.DependsOn = append(pkg.DependsOn, dep.ID) + } + continue + } +} + +func (m *Decoder) decodeApplication(c *core.Component) *ftypes.Application { + var app ftypes.Application + for _, prop := range c.Properties { + if prop.Name == core.PropertyType { + app.Type = ftypes.LangType(prop.Value) + } + } + + // Aggregation Types use the name of the language (e.g. `Java`, `Python`, etc.) as the component name. + // Other language files use the file path as their name. + if !slices.Contains(ftypes.AggregatingTypes, app.Type) { + app.FilePath = c.Name + } + return &app +} + +func (m *Decoder) decodePackage(ctx context.Context, c *core.Component) (*ftypes.Package, error) { + p := (*purl.PackageURL)(c.PkgIdentifier.PURL) + if p == nil { + m.logger.DebugContext(ctx, "Skipping a component without PURL", + log.String("name", c.Name), log.String("version", c.Version)) + return nil, ErrPURLEmpty + } + + pkg := p.Package() + if p.Class() == types.ClassUnknown { + m.logger.DebugContext(ctx, "Skipping a component with an unsupported type", + log.String("name", c.Name), log.String("version", c.Version), log.String("type", p.Type)) + return nil, ErrUnsupportedType + } + pkg.Name = m.pkgName(pkg, c) + pkg.ID = dependency.ID(p.LangType(), pkg.Name, p.Version) // Re-generate ID with the updated name + + if err := fillPkgFieldsFromComponentProps(c.Properties, pkg); err != nil { + return nil, xerrors.Errorf("failed to fill package properties: %w", err) + } + + pkg.Identifier.BOMRef = c.PkgIdentifier.BOMRef + pkg.Licenses = c.Licenses + + for _, f := range c.Files { + if f.Path != "" && pkg.FilePath == "" { + pkg.FilePath = f.Path + } + // An empty path represents a package digest + if f.Path == "" && len(f.Digests) > 0 { + pkg.Digest = f.Digests[0] + } + } + + if p.Class() == types.ClassOSPkg { + m.fillSrcPkg(ctx, c, pkg) + } + + return pkg, nil +} + +// pkgName returns the package name. +// PURL loses case-sensitivity (e.g. Go, Npm, PyPI), so we have to use an original package name. +func (m *Decoder) pkgName(pkg *ftypes.Package, c *core.Component) string { + p := c.PkgIdentifier.PURL + + // A name from PURL takes precedence for CocoaPods since it has subpath. + if c.PkgIdentifier.PURL.Type == packageurl.TypeCocoapods { + return pkg.Name + } + + // `maven purl type` has no restrictions on using lowercase letters. + // Also, `spdx-maven-plugin` uses `name` instead of `artifactId` for the `package name` field. + // So we need to use `purl` for maven/gradle packages + // See https://github.com/aquasecurity/trivy/issues/7007 for more information. + if p.Type == packageurl.TypeMaven || p.Type == packageurl.TypeGradle { + return pkg.Name + } + + // TODO(backward compatibility): Remove after 03/2025 + // Bitnami used different pkg.Name and the name from PURL. + // For backwards compatibility - we need to use PURL. + // cf. https://github.com/aquasecurity/trivy/issues/6981 + if c.PkgIdentifier.PURL.Type == packageurl.TypeBitnami { + return pkg.Name + } + + if c.Group != "" { + return c.Group + "/" + c.Name + } + return c.Name +} + +func (m *Decoder) fillSrcPkg(ctx context.Context, c *core.Component, pkg *ftypes.Package) { + if c.SrcName != "" && pkg.SrcName == "" { + pkg.SrcName = c.SrcName + } + m.parseSrcVersion(ctx, pkg, c.SrcVersion) + + // Source info was added from component or properties + if pkg.SrcName != "" && pkg.SrcVersion != "" { + return + } + + // Fill source package information for components in third-party SBOMs . + if pkg.SrcName == "" { + pkg.SrcName = pkg.Name + } + if pkg.SrcVersion == "" { + pkg.SrcVersion = pkg.Version + } + if pkg.SrcRelease == "" { + pkg.SrcRelease = pkg.Release + } + if pkg.SrcEpoch == 0 { + pkg.SrcEpoch = pkg.Epoch + } +} + +// parseSrcVersion parses the version of the source package. +func (m *Decoder) parseSrcVersion(ctx context.Context, pkg *ftypes.Package, ver string) { + if ver == "" { + return + } + switch pkg.Identifier.PURL.Type { + case packageurl.TypeRPM: + v := rpmver.NewVersion(ver) + pkg.SrcEpoch = v.Epoch() + pkg.SrcVersion = v.Version() + pkg.SrcRelease = v.Release() + case packageurl.TypeDebian: + v, err := debver.NewVersion(ver) + if err != nil { + m.logger.DebugContext(ctx, "Failed to parse Debian version", log.Err(err)) + return + } + pkg.SrcEpoch = v.Epoch() + pkg.SrcVersion = v.Version() + pkg.SrcRelease = v.Revision() + } +} + +// addOSPkgs traverses relationships and adds OS packages +func (m *Decoder) addOSPkgs(sbom *types.SBOM) { + pkgs := m.traverseDependencies(m.osID) + if len(pkgs) == 0 { + return + } + sbom.Packages = []ftypes.PackageInfo{{Packages: pkgs}} +} + +// addLangPkgs traverses relationships and adds language-specific packages +func (m *Decoder) addLangPkgs(sbom *types.SBOM) { + for id, app := range m.apps { + app.Packages = append(app.Packages, m.traverseDependencies(id)...) + sbom.Applications = append(sbom.Applications, *app) + } +} + +// traverseDependencies recursively retrieves all packages that the specified component depends on. +// It starts from the given component ID and traverses the dependency tree, collecting all +// dependent packages. The collected packages are removed from m.pkgs to prevent duplicate +// processing. This ensures that all dependencies, including transitive ones, are properly +// captured and associated with their parent component. +func (m *Decoder) traverseDependencies(id uuid.UUID) ftypes.Packages { + var pkgs ftypes.Packages + for _, rel := range m.bom.Relationships()[id] { + pkg, ok := m.pkgs[rel.Dependency] + if !ok { + continue + } + // Add the current package + pkgs = append(pkgs, *pkg) + delete(m.pkgs, rel.Dependency) // Delete the added package + + // Add the nested packages + pkgs = append(pkgs, m.traverseDependencies(rel.Dependency)...) + } + return pkgs +} + +// addOrphanPkgs adds orphan packages. +// Orphan packages are packages that are not related to any components. +func (m *Decoder) addOrphanPkgs(sbom *types.SBOM) error { + osPkgMap := make(map[string]ftypes.Packages) + langPkgMap := make(map[ftypes.LangType]ftypes.Packages) + for _, pkg := range m.pkgs { + p := (*purl.PackageURL)(pkg.Identifier.PURL) + switch p.Class() { + case types.ClassOSPkg: + osPkgMap[p.Type] = append(osPkgMap[p.Type], *pkg) + case types.ClassLangPkg: + langType := p.LangType() + langPkgMap[langType] = append(langPkgMap[langType], *pkg) + } + } + + if len(osPkgMap) > 1 { + return xerrors.Errorf("multiple types of OS packages in SBOM are not supported (%q)", lo.Keys(osPkgMap)) + } + + // Add OS packages + for _, pkgs := range osPkgMap { + // TODO: mismatch between the OS and the packages should be rejected. + // e.g. OS: debian, Packages: rpm + + // addOSPkgs creates exactly one PackageInfo with empty FilePath, so we can merge directly + if len(sbom.Packages) == 0 { + sbom.Packages = append(sbom.Packages, ftypes.PackageInfo{}) + } + // Merge with existing PackageInfo (always sbom.Packages[0]) + sbom.Packages[0].Packages = append(sbom.Packages[0].Packages, pkgs...) + sort.Sort(sbom.Packages[0].Packages) + + break // Just take the first element + } + + // Add language-specific packages + for pkgType, pkgs := range langPkgMap { + sort.Sort(pkgs) + sbom.Applications = append(sbom.Applications, ftypes.Application{ + Type: pkgType, + Packages: pkgs, + }) + } + return nil +} + +func fillPkgFieldsFromComponentProps(props []core.Property, pkg *ftypes.Package) error { + buildInfo := &ftypes.BuildInfo{} + var err error + for _, prop := range props { + switch prop.Name { + case core.PropertyPkgID: + pkg.ID = prop.Value + case core.PropertyFilePath: + pkg.FilePath = prop.Value + case core.PropertySrcName: + pkg.SrcName = prop.Value + case core.PropertySrcVersion: + pkg.SrcVersion = prop.Value + case core.PropertySrcRelease: + pkg.SrcRelease = prop.Value + case core.PropertySrcEpoch: + if pkg.SrcEpoch, err = strconv.Atoi(prop.Value); err != nil { + return xerrors.Errorf("invalid src epoch: %w", err) + } + case core.PropertyModularitylabel: + pkg.Modularitylabel = prop.Value + case core.PropertyLayerDigest: + pkg.Layer.Digest = prop.Value + case core.PropertyLayerDiffID: + pkg.Layer.DiffID = prop.Value + case core.PropertyContentSet: + buildInfo.ContentSets = append(buildInfo.ContentSets, prop.Value) + case core.PropertyNVR: + buildInfo.Nvr = prop.Value + case core.PropertyArch: + buildInfo.Arch = prop.Value + } + } + + if len(buildInfo.ContentSets) > 0 || buildInfo.Nvr != "" { + pkg.BuildInfo = buildInfo + } + return nil +} diff --git a/pkg/sbom/io/decode_test.go b/pkg/sbom/io/decode_test.go new file mode 100644 index 000000000000..d79092b36fa8 --- /dev/null +++ b/pkg/sbom/io/decode_test.go @@ -0,0 +1,408 @@ +package io_test + +import ( + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom/core" + sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + apkToolsComponent = &core.Component{ + Type: core.TypeLibrary, + Name: "apk-tools", + Version: "2.14.10-r4", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Name: "apk-tools", + Version: "2.14.10-r4", + Qualifiers: packageurl.Qualifiers{ + {Key: "arch", Value: "aarch64"}, + {Key: "distro", Value: "wolfi-20230201"}, + }, + }, + }, + Licenses: []string{"GPL-2.0-only"}, + } + + busyboxComponent = &core.Component{ + Type: core.TypeLibrary, + Name: "busybox", + Version: "1.37.0-r42", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Name: "busybox", + Version: "1.37.0-r42", + Qualifiers: packageurl.Qualifiers{ + {Key: "arch", Value: "aarch64"}, + {Key: "distro", Value: "wolfi-20230201"}, + }, + }, + }, + Licenses: []string{"GPL-2.0-only"}, + } + + caCertificatesComponent = &core.Component{ + Type: core.TypeLibrary, + Name: "ca-certificates-bundle", + Version: "20241121-r42", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Name: "ca-certificates-bundle", + Version: "20241121-r42", + Qualifiers: packageurl.Qualifiers{ + {Key: "arch", Value: "aarch64"}, + {Key: "distro", Value: "wolfi-20230201"}, + }, + }, + }, + Licenses: []string{"MPL-2.0"}, + } + + wolfiOSComponent = &core.Component{ + Type: core.TypeOS, + Name: "wolfi", + Version: "20230201", + } + + rpmTestComponent = &core.Component{ + Type: core.TypeLibrary, + Name: "rpm-package", + Version: "2.0.0", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Name: "rpm-package", + Version: "2.0.0", + }, + }, + Properties: core.Properties{ + { + Name: core.PropertyContentSet, + Value: "rhel-9-for-aarch64-appstream-rpms", + }, + { + Name: core.PropertyContentSet, + Value: "rhel-9-for-aarch64-appstream-source-rpms", + }, + { + Name: core.PropertySrcName, + Value: "rpm-package", + }, + { + Name: core.PropertySrcVersion, + Value: "2.0.0", + }, + }, + Licenses: []string{"GPL-2.0"}, + } + + orphanedApkComponent = &core.Component{ + Type: core.TypeLibrary, + Name: "orphaned-package", + Version: "1.0.0", + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Name: "orphaned-package", + Version: "1.0.0", + Qualifiers: packageurl.Qualifiers{ + {Key: "arch", Value: "aarch64"}, + {Key: "distro", Value: "wolfi-20230201"}, + }, + }, + }, + Licenses: []string{"MIT"}, + } +) + +func TestDecoder_Decode_OSPackages(t *testing.T) { + tests := []struct { + name string + setupBOM func() *core.BOM + wantSBOM types.SBOM + wantErr string + }{ + { + name: "OS packages with OS metadata should be included", + setupBOM: func() *core.BOM { + bom := core.NewBOM(core.Options{}) + bom.SerialNumber = "test-with-os" + bom.Version = 1 + + // Add OS component + bom.AddComponent(wolfiOSComponent) + + // Add APK package + bom.AddComponent(busyboxComponent) + + // Create relationship between OS and package + bom.AddRelationship(wolfiOSComponent, busyboxComponent, core.RelationshipContains) + return bom + }, + wantSBOM: types.SBOM{ + Metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: ftypes.Wolfi, + Name: "20230201", + }, + }, + Packages: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "busybox@1.37.0-r42", + Name: "busybox", + Version: "1.37.0-r42", + Arch: "aarch64", + SrcName: "busybox", + SrcVersion: "1.37.0-r42", + Licenses: []string{"GPL-2.0-only"}, + Identifier: ftypes.PkgIdentifier{ + PURL: busyboxComponent.PkgIdentifier.PURL, + }, + }, + }, + }, + }, + }, + }, + { + name: "multiple OS packages without OS metadata should be included and merged", + setupBOM: func() *core.BOM { + bom := core.NewBOM(core.Options{}) + bom.SerialNumber = "test-multiple-no-os" + bom.Version = 1 + + // Add multiple APK packages including orphaned package + bom.AddComponent(apkToolsComponent) + bom.AddComponent(busyboxComponent) + bom.AddComponent(caCertificatesComponent) + bom.AddComponent(orphanedApkComponent) + + return bom + }, + wantSBOM: types.SBOM{ + Metadata: types.Metadata{ + OS: nil, // No OS detected + }, + Packages: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "apk-tools@2.14.10-r4", + Name: "apk-tools", + Version: "2.14.10-r4", + Arch: "aarch64", + SrcName: "apk-tools", + SrcVersion: "2.14.10-r4", + Licenses: []string{"GPL-2.0-only"}, + Identifier: ftypes.PkgIdentifier{ + PURL: apkToolsComponent.PkgIdentifier.PURL, + }, + }, + { + ID: "busybox@1.37.0-r42", + Name: "busybox", + Version: "1.37.0-r42", + Arch: "aarch64", + SrcName: "busybox", + SrcVersion: "1.37.0-r42", + Licenses: []string{"GPL-2.0-only"}, + Identifier: ftypes.PkgIdentifier{ + PURL: busyboxComponent.PkgIdentifier.PURL, + }, + }, + { + ID: "ca-certificates-bundle@20241121-r42", + Name: "ca-certificates-bundle", + Version: "20241121-r42", + Arch: "aarch64", + SrcName: "ca-certificates-bundle", + SrcVersion: "20241121-r42", + Licenses: []string{"MPL-2.0"}, + Identifier: ftypes.PkgIdentifier{ + PURL: caCertificatesComponent.PkgIdentifier.PURL, + }, + }, + { + ID: "orphaned-package@1.0.0", + Name: "orphaned-package", + Version: "1.0.0", + Arch: "aarch64", + SrcName: "orphaned-package", + SrcVersion: "1.0.0", + Licenses: []string{"MIT"}, + Identifier: ftypes.PkgIdentifier{ + PURL: orphanedApkComponent.PkgIdentifier.PURL, + }, + }, + }, + }, + }, + }, + }, + { + name: "rpm package", + setupBOM: func() *core.BOM { + bom := core.NewBOM(core.Options{}) + bom.SerialNumber = "test-multiple-os-types" + bom.Version = 1 + + // Add RPM package + bom.AddComponent(rpmTestComponent) + + return bom + }, + wantSBOM: types.SBOM{ + Metadata: types.Metadata{ + OS: nil, // No OS detected + }, + Packages: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "rpm-package@2.0.0", + Name: "rpm-package", + Version: "2.0.0", + SrcName: "rpm-package", + SrcVersion: "2.0.0", + Licenses: []string{"GPL-2.0"}, + Identifier: ftypes.PkgIdentifier{ + PURL: rpmTestComponent.PkgIdentifier.PURL, + }, + BuildInfo: &ftypes.BuildInfo{ + ContentSets: []string{ + "rhel-9-for-aarch64-appstream-rpms", + "rhel-9-for-aarch64-appstream-source-rpms", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "multiple OS package types should return error", + setupBOM: func() *core.BOM { + bom := core.NewBOM(core.Options{}) + bom.SerialNumber = "test-multiple-os-types" + bom.Version = 1 + + // Add APK package + bom.AddComponent(apkToolsComponent) + + // Add RPM package + bom.AddComponent(rpmTestComponent) + + return bom + }, + wantErr: "multiple types of OS packages in SBOM are not supported", + }, + { + name: "empty BOM should have no packages", + setupBOM: func() *core.BOM { + bom := core.NewBOM(core.Options{}) + bom.SerialNumber = "test-empty" + bom.Version = 1 + return bom + }, + wantSBOM: types.SBOM{ + Metadata: types.Metadata{ + OS: nil, + }, + }, + }, + { + name: "mixed OS packages (in-graph and out-of-graph) should be merged", + setupBOM: func() *core.BOM { + bom := core.NewBOM(core.Options{}) + bom.SerialNumber = "test-mixed-os-packages" + bom.Version = 1 + + // Add OS component + bom.AddComponent(wolfiOSComponent) + + // Add OS packages - busybox is connected to OS (in-graph) + bom.AddComponent(busyboxComponent) + // Add orphaned package not connected to OS (out-of-graph) + bom.AddComponent(orphanedApkComponent) + + // Create relationship between OS and busybox only + bom.AddRelationship(wolfiOSComponent, busyboxComponent, core.RelationshipContains) + // orphanedApkComponent is not connected to OS component + + return bom + }, + wantSBOM: types.SBOM{ + Metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: ftypes.Wolfi, + Name: "20230201", + }, + }, + Packages: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "busybox@1.37.0-r42", + Name: "busybox", + Version: "1.37.0-r42", + Arch: "aarch64", + SrcName: "busybox", + SrcVersion: "1.37.0-r42", + Licenses: []string{"GPL-2.0-only"}, + Identifier: ftypes.PkgIdentifier{ + PURL: busyboxComponent.PkgIdentifier.PURL, + }, + }, + { + ID: "orphaned-package@1.0.0", + Name: "orphaned-package", + Version: "1.0.0", + Arch: "aarch64", + SrcName: "orphaned-package", + SrcVersion: "1.0.0", + Licenses: []string{"MIT"}, + Identifier: ftypes.PkgIdentifier{ + PURL: orphanedApkComponent.PkgIdentifier.PURL, + }, + }, + }, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bom := tt.setupBOM() + decoder := sbomio.NewDecoder(bom) + var gotSBOM types.SBOM + + err := decoder.Decode(t.Context(), &gotSBOM) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + // Set BOM for comparison (it's set by the decoder) + tt.wantSBOM.BOM = gotSBOM.BOM + + // Compare the entire SBOM structure + assert.EqualExportedValues(t, tt.wantSBOM, gotSBOM) + }) + } +} diff --git a/pkg/sbom/io/encode.go b/pkg/sbom/io/encode.go new file mode 100644 index 000000000000..a26cd2fbd856 --- /dev/null +++ b/pkg/sbom/io/encode.go @@ -0,0 +1,531 @@ +package io + +import ( + "fmt" + "slices" + "strconv" + + "github.com/google/uuid" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/digest" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/scan/utils" + "github.com/aquasecurity/trivy/pkg/types" +) + +type EncoderOption func(*Encoder) + +// WithBOMRef enables BOM-Ref generation for CycloneDX components +func WithBOMRef() EncoderOption { + return func(e *Encoder) { + e.bomOpts.GenerateBOMRef = true + } +} + +// WithParents enables holding parent maps in the BOM structure +func WithParents() EncoderOption { + return func(e *Encoder) { + e.bomOpts.Parents = true + } +} + +// ForceRegenerate forces regeneration of BOM instead of reusing existing one +func ForceRegenerate() EncoderOption { + return func(e *Encoder) { + e.forceRegenerate = true + } +} + +type Encoder struct { + bom *core.BOM + bomOpts core.Options + forceRegenerate bool +} + +func NewEncoder(opts ...EncoderOption) *Encoder { + e := &Encoder{} + for _, opt := range opts { + opt(e) + } + return e +} + +func (e *Encoder) Encode(report types.Report) (*core.BOM, error) { + // When report.BOM is not nil, reuse the existing BOM structure unless ForceRegenerate is set. + // This happens in two scenarios: + // 1. SBOM scanning: When scanning an existing SBOM file to refresh vulnerabilities + // 2. Library usage: When using Trivy as a library with a custom BOM in the report + if report.BOM != nil && !e.forceRegenerate { + return e.reuseExistingBOM(report) + } + // Metadata component + root, err := e.rootComponent(report) + if err != nil { + return nil, xerrors.Errorf("failed to create root component: %w", err) + } + + e.bom = core.NewBOM(e.bomOpts) + if report.BOM != nil { + e.bom.SerialNumber = report.BOM.SerialNumber + e.bom.Version = report.BOM.Version + } + e.bom.AddComponent(root) + + for _, result := range report.Results { + e.encodeResult(root, report.Metadata, result) + } + + // Components that do not have their own dependencies MUST be declared as empty elements within the graph. + if _, ok := e.bom.Relationships()[root.ID()]; !ok { + e.bom.AddRelationship(root, nil, "") + } + return e.bom, nil +} + +func (e *Encoder) rootComponent(r types.Report) (*core.Component, error) { + root := &core.Component{ + Root: true, + Name: r.ArtifactName, + } + + props := []core.Property{ + { + Name: core.PropertySchemaVersion, + Value: strconv.Itoa(r.SchemaVersion), + }, + } + + switch r.ArtifactType { + case ftypes.TypeContainerImage: + root.Type = core.TypeContainerImage + props = append(props, core.Property{ + Name: core.PropertyImageID, + Value: r.Metadata.ImageID, + }) + + // Save image labels as properties with `Labels:` prefix. + // e.g. `LABEL vendor="aquasecurity"` => `Labels:vendor` -> `aquasecurity` + for label, value := range r.Metadata.ImageConfig.Config.Labels { + props = append(props, core.Property{ + Name: core.PropertyLabelsPrefix + ":" + label, + Value: value, + }) + } + + p, err := purl.New(purl.TypeOCI, r.Metadata, ftypes.Package{}) + if err != nil { + return nil, xerrors.Errorf("failed to new package url for oci: %w", err) + } + if p != nil { + root.PkgIdentifier.PURL = p.Unwrap() + } + + case ftypes.TypeVM: + root.Type = core.TypeVM + case ftypes.TypeFilesystem: + root.Type = core.TypeFilesystem + case ftypes.TypeRepository: + root.Type = core.TypeRepository + case ftypes.TypeCycloneDX, ftypes.TypeSPDX: + // When we scan a `json` file (meaning a file in `json` format) which was created from the SBOM file. + // e.g. for use in `convert` mode. + // See https://github.com/aquasecurity/trivy/issues/6780 + root.Type = core.TypeFilesystem + } + + if r.Metadata.Size != 0 { + props = append(props, core.Property{ + Name: core.PropertySize, + Value: strconv.FormatInt(r.Metadata.Size, 10), + }) + } + + for _, d := range r.Metadata.RepoDigests { + props = append(props, core.Property{ + Name: core.PropertyRepoDigest, + Value: d, + }) + } + + for _, id := range r.Metadata.DiffIDs { + props = append(props, core.Property{ + Name: core.PropertyDiffID, + Value: id, + }) + } + + for _, tag := range r.Metadata.RepoTags { + props = append(props, core.Property{ + Name: core.PropertyRepoTag, + Value: tag, + }) + } + + if !r.Metadata.Reference.IsZero() { + props = append(props, core.Property{ + Name: core.PropertyReference, + Value: r.Metadata.Reference.String(), + }) + } + + root.Properties = filterProperties(props) + + return root, nil +} + +func (e *Encoder) encodeResult(root *core.Component, metadata types.Metadata, result types.Result) { + if slices.Contains(ftypes.AggregatingTypes, result.Type) { + // If a package is language-specific package that isn't associated with a lock file, + // it will be a dependency of a component under "metadata". + // e.g. + // Container component (alpine:3.15) ----------------------- #1 + // -> Library component (npm package, express-4.17.3) ---- #2 + // -> Library component (python package, django-4.0.2) --- #2 + // -> etc. + // ref. https://cyclonedx.org/use-cases/#inventory + + // Dependency graph from #1 to #2 + e.encodePackages(root, result) + } else if result.Class == types.ClassOSPkg || result.Class == types.ClassLangPkg { + // If a package is OS package, it will be a dependency of "Operating System" component. + // e.g. + // Container component (alpine:3.15) --------------------- #1 + // -> Operating System Component (Alpine Linux 3.15) --- #2 + // -> Library component (bash-4.12) ------------------ #3 + // -> Library component (vim-8.2) ------------------ #3 + // -> etc. + // + // Else if a package is language-specific package associated with a lock file, + // it will be a dependency of "Application" component. + // e.g. + // Container component (alpine:3.15) ------------------------ #1 + // -> Application component (/app/package-lock.json) ------ #2 + // -> Library component (npm package, express-4.17.3) --- #3 + // -> Library component (npm package, lodash-4.17.21) --- #3 + // -> etc. + + // #2 + appComponent := e.resultComponent(root, result, metadata.OS) + + // #3 + e.encodePackages(appComponent, result) + } +} + +func (e *Encoder) encodePackages(parent *core.Component, result types.Result) { + // Get dependency parents first + parents := ftypes.Packages(result.Packages).ParentDeps() + + // Group vulnerabilities by package ID + vulns := make(map[string][]core.Vulnerability) + for _, vuln := range result.Vulnerabilities { + v := e.vulnerability(vuln) + vulns[vuln.PkgIdentifier.UID] = append(vulns[vuln.PkgIdentifier.UID], v) + } + + // UID => Package Component + components := make(map[string]*core.Component, len(result.Packages)) + // PkgID => Package Component + dependencies := make(map[string]*core.Component, len(result.Packages)) + var hasRoot bool + for i, pkg := range result.Packages { + pkgID := lo.Ternary(pkg.ID == "", fmt.Sprintf("%s@%s", pkg.Name, pkg.Version), pkg.ID) + result.Packages[i].ID = pkgID + + // Check if the project has a root dependency + // TODO: Ideally, all projects should have a root dependency. + if pkg.Relationship == ftypes.RelationshipRoot { + hasRoot = true + } + + // Convert packages to components + c := e.component(result, pkg) + components[pkg.Identifier.UID] = c + + // For dependencies: the key "pkgID" might be duplicated in aggregated packages, + // but it doesn't matter as they don't have "DependsOn". + dependencies[pkgID] = c + + // Add a component + e.bom.AddComponent(c) + + // Add vulnerabilities + if vv := vulns[pkg.Identifier.UID]; vv != nil { + e.bom.AddVulnerabilities(c, vv) + } + } + + // Build a dependency graph between packages + for _, pkg := range result.Packages { + c := components[pkg.Identifier.UID] + + // Add a relationship between the parent and the package if needed + if e.belongToParent(pkg, parents, hasRoot) { + e.bom.AddRelationship(parent, c, core.RelationshipContains) + } + + // Add relationships between the package and its dependencies + for _, dep := range pkg.DependsOn { + dependsOn, ok := dependencies[dep] + if !ok { + continue + } + e.bom.AddRelationship(c, dependsOn, core.RelationshipDependsOn) + } + + // Components that do not have their own dependencies MUST be declared as empty elements within the graph. + // TODO: Should check if the component has actually no dependencies or the dependency graph is not supported. + if len(pkg.DependsOn) == 0 { + e.bom.AddRelationship(c, nil, "") + } + } +} + +// reuseExistingBOM preserves the original SBOM structure and only updates the vulnerabilities section +// with newly detected vulnerabilities. This method handles two use cases: +// 1. SBOM scanning (CycloneDX): When scanning an existing SBOM file to refresh vulnerability data while +// preserving the original structure, components, and relationships +// e.g. $ trivy sbom sbom.cdx.json --scanners vuln --format cyclonedx +// 2. Library usage: When using Trivy as a library with a pre-existing custom BOM that needs +// to be enriched with vulnerability information +// +// For SBOM scanning (case 1), this approach is CycloneDX-specific +// because: SPDX 2.3 does not include vulnerabilities in the SBOM specification. +// Therefore, the method uses BOM-Ref for component-vulnerability lookup rather than SPDX-ID. +func (e *Encoder) reuseExistingBOM(report types.Report) (*core.BOM, error) { + bom := report.BOM.Clone() + + // Create a lookup map from BOM-Ref to component for efficient vulnerability assignment + // BOM-Ref is used as the key because it's the standard identifier in CycloneDX format + // and is guaranteed to be present in components from CycloneDX SBOMs + components := lo.MapKeys(report.BOM.Components(), func(v *core.Component, _ uuid.UUID) string { + return v.PkgIdentifier.BOMRef + }) + + for _, result := range report.Results { + // Group newly detected vulnerabilities by their component's BOM-Ref + vulns := make(map[string][]core.Vulnerability) + for _, vuln := range result.Vulnerabilities { + vulns[vuln.PkgIdentifier.BOMRef] = append(vulns[vuln.PkgIdentifier.BOMRef], e.vulnerability(vuln)) + } + + // Associate vulnerabilities with their corresponding components in the SBOM + for bomRef, componentVulns := range vulns { + c, ok := components[bomRef] + if !ok { + // This should never happen in proper SBOM rescanning because vulnerabilities + // should only be detected for components that exist in the original SBOM + log.Warn("Skipping vulnerabilities for component not found in SBOM", + log.String("bom-ref", bomRef), + log.Int("vulnerabilities", len(componentVulns))) + continue + } + bom.AddVulnerabilities(c, componentVulns) + } + } + + return bom, nil +} + +func (e *Encoder) resultComponent(root *core.Component, r types.Result, osFound *ftypes.OS) *core.Component { + component := &core.Component{ + Name: r.Target, + Properties: []core.Property{ + { + Name: core.PropertyType, + Value: string(r.Type), + }, + { + Name: core.PropertyClass, + Value: string(r.Class), + }, + }, + } + + switch r.Class { + case types.ClassOSPkg: + if osFound != nil { + component.Name = string(osFound.Family) + component.Version = osFound.Name + } + component.Type = core.TypeOS + case types.ClassLangPkg: + component.Type = core.TypeApplication + } + + e.bom.AddRelationship(root, component, core.RelationshipContains) + return component +} + +func (*Encoder) component(result types.Result, pkg ftypes.Package) *core.Component { + name := pkg.Name + version := utils.FormatVersion(pkg) + var group string + // there are cases when we can't build purl + // e.g. local Go packages + if pu := pkg.Identifier.PURL; pu != nil { + version = pu.Version + for _, q := range pu.Qualifiers { + if q.Key == "epoch" && q.Value != "0" { + version = fmt.Sprintf("%s:%s", q.Value, version) + } + } + + // Use `group` field for GroupID and `name` for ArtifactID for java files + // https://github.com/aquasecurity/trivy/issues/4675 + // Use `group` field for npm scopes + // https://github.com/aquasecurity/trivy/issues/5908 + if pu.Type == packageurl.TypeMaven || pu.Type == packageurl.TypeNPM { + name = pu.Name + group = pu.Namespace + } + } + + properties := []core.Property{ + { + Name: core.PropertyPkgID, + Value: pkg.ID, + }, + { + Name: core.PropertyPkgType, + Value: string(result.Type), + }, + { + Name: core.PropertyFilePath, + Value: pkg.FilePath, + }, + { + Name: core.PropertySrcName, + Value: pkg.SrcName, + }, + { + Name: core.PropertySrcVersion, + Value: pkg.SrcVersion, + }, + { + Name: core.PropertySrcRelease, + Value: pkg.SrcRelease, + }, + { + Name: core.PropertySrcEpoch, + Value: strconv.Itoa(pkg.SrcEpoch), + }, + { + Name: core.PropertyModularitylabel, + Value: pkg.Modularitylabel, + }, + { + Name: core.PropertyLayerDigest, + Value: pkg.Layer.Digest, + }, + { + Name: core.PropertyLayerDiffID, + Value: pkg.Layer.DiffID, + }, + } + + // Fill Red Hat specific properties + if pkg.BuildInfo != nil { + for _, cs := range pkg.BuildInfo.ContentSets { + properties = append(properties, core.Property{ + Name: core.PropertyContentSet, + Value: cs, + }) + } + + if pkg.BuildInfo.Nvr != "" { + properties = append(properties, core.Property{ + Name: core.PropertyNVR, + Value: pkg.BuildInfo.Nvr, + }) + } + if pkg.BuildInfo.Arch != "" { + properties = append(properties, core.Property{ + Name: core.PropertyArch, + Value: pkg.BuildInfo.Arch, + }) + } + } + + var files []core.File + if pkg.FilePath != "" || pkg.Digest != "" { + files = append(files, core.File{ + Path: pkg.FilePath, + Digests: lo.Ternary(pkg.Digest != "", []digest.Digest{pkg.Digest}, nil), + }) + } + + // TODO(refactor): simplify the list of conditions + var srcFile string + if result.Class == types.ClassLangPkg && !slices.Contains(ftypes.AggregatingTypes, result.Type) { + srcFile = result.Target + } + + return &core.Component{ + Type: core.TypeLibrary, + Name: name, + Group: group, + Version: version, + SrcName: pkg.SrcName, + SrcVersion: utils.FormatSrcVersion(pkg), + SrcFile: srcFile, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: pkg.Identifier.UID, + PURL: pkg.Identifier.PURL, + BOMRef: pkg.Identifier.BOMRef, + }, + Supplier: pkg.Maintainer, + Licenses: pkg.Licenses, + Files: files, + Properties: filterProperties(properties), + } +} + +func (*Encoder) vulnerability(vuln types.DetectedVulnerability) core.Vulnerability { + return core.Vulnerability{ + Vulnerability: vuln.Vulnerability, + ID: vuln.VulnerabilityID, + PkgName: vuln.PkgName, + InstalledVersion: vuln.InstalledVersion, + FixedVersion: vuln.FixedVersion, + PrimaryURL: vuln.PrimaryURL, + DataSource: vuln.DataSource, + } +} + +// belongToParent determines if a package should be directly included in the parent based on its relationship and dependencies. +func (*Encoder) belongToParent(pkg ftypes.Package, parents map[string]ftypes.Packages, hasRoot bool) bool { + // Case 1: Relationship: known , DependsOn: known + // Packages with no parent are included in the parent + // - Relationship: + // - Root: true (it doesn't have a parent) + // - Workspace: false (it always has a parent) + // - Direct: + // - No root dependency in the project: true (e.g., poetry.lock) + // - Otherwise: false (Direct dependencies should belong to the root/workspace) + // - Indirect: false (it always has a parent) + // Case 2: Relationship: unknown, DependsOn: unknown (e.g., conan lockfile v2) + // All packages are included in the parent + // Case 3: Relationship: known , DependsOn: unknown (e.g., go.mod without $GOPATH) + // All packages are included in the parent + // Case 4: Relationship: unknown, DependsOn: known (e.g., GoBinaries, OS packages) + // - Packages with parents: false. These packages are included in the packages from `parents` (e.g. GoBinaries deps and root package). + // - Packages without parents: true. These packages are included in the parent (e.g. OS packages without parents). + if pkg.Relationship == ftypes.RelationshipDirect { + return !hasRoot + } + + return len(parents[pkg.ID]) == 0 +} + +func filterProperties(props []core.Property) []core.Property { + return lo.Filter(props, func(property core.Property, _ int) bool { + return property.Value != "" && (property.Name != core.PropertySrcEpoch || property.Value != "0") + }) +} diff --git a/pkg/sbom/io/encode_test.go b/pkg/sbom/io/encode_test.go new file mode 100644 index 000000000000..38bd9c48104e --- /dev/null +++ b/pkg/sbom/io/encode_test.go @@ -0,0 +1,1774 @@ +package io_test + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + dtypes "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom/core" + sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" +) + +func TestEncoder_Encode(t *testing.T) { + tests := []struct { + name string + report types.Report + wantComponents map[uuid.UUID]*core.Component + wantRels map[uuid.UUID][]core.Relationship + wantVulns map[uuid.UUID][]core.Vulnerability + wantErr string + }{ + { + name: "container image", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "debian:12", + ArtifactType: ftypes.TypeContainerImage, + Metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: ftypes.Debian, + Name: "12", + }, + RepoTags: []string{ + "debian:latest", + "debian:12", + }, + RepoDigests: []string{ + "debian@sha256:4482958b4461ff7d9fabc24b3a9ab1e9a2c85ece07b2db1840c7cbc01d053e90", + }, + ImageConfig: v1.ConfigFile{ + Config: v1.Config{ + Labels: map[string]string{ + "vendor": "aquasecurity", + }, + }, + }, + }, + Results: []types.Result{ + { + Target: "debian:12", + Type: ftypes.Debian, + Class: types.ClassOSPkg, + Packages: []ftypes.Package{ + { + ID: "libc6@2.37-15.1", + Name: "libc6", + Version: "2.37-15.1", + Identifier: ftypes.PkgIdentifier{ + UID: "33654D2C483FC3AD", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Name: "libc6", + Version: "2.37-15.1", + }, + }, + }, + { + ID: "curl@7.50.3-1", + Name: "curl", + Version: "7.50.3-1", + Identifier: ftypes.PkgIdentifier{ + UID: "51BA9E006222819D", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Name: "curl", + Version: "7.50.3-1", + }, + }, + DependsOn: []string{ + "libc6@2.37-15.1", + }, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgName: "curl", + PkgID: "curl@7.50.3-1", + VulnerabilityID: "CVE-2021-22876", + InstalledVersion: "7.50.3-1", + FixedVersion: "7.50.3-1+deb9u1", + Vulnerability: dtypes.Vulnerability{ + Severity: "HIGH", + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "51BA9E006222819D", + }, + }, + }, + }, + { + Target: "Java", + Type: ftypes.Jar, + Class: types.ClassLangPkg, + Packages: []ftypes.Package{ + { + ID: "com.fasterxml.jackson.core:jackson-databind:2.13.4", + Name: "com.fasterxml.jackson.core:jackson-databind", + Version: "2.13.4", + FilePath: "/foo/jackson-databind-2.13.4.jar", + Identifier: ftypes.PkgIdentifier{ + UID: "A6BD5A2FE5C00E10", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4", + }, + }, + }, + { + ID: "com.fasterxml.jackson.core:jackson-databind:2.13.4", + Name: "com.fasterxml.jackson.core:jackson-databind", + Version: "2.13.4", + FilePath: "/bar/jackson-databind-2.13.4.jar", + Identifier: ftypes.PkgIdentifier{ + UID: "64244651208EC759", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4", + }, + }, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + PkgName: "com.fasterxml.jackson.core:jackson-databind", + PkgID: "com.fasterxml.jackson.core:jackson-databind:2.13.4", + VulnerabilityID: "CVE-2022-42003", + InstalledVersion: "2.13.4", + FixedVersion: "2.12.7.1, 2.13.4.2", + PkgPath: "/foo/jackson-databind-2.13.4.jar", + Vulnerability: dtypes.Vulnerability{ + Severity: "HIGH", + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "A6BD5A2FE5C00E10", + }, + }, + { + PkgName: "com.fasterxml.jackson.core:jackson-databind", + PkgID: "com.fasterxml.jackson.core:jackson-databind:2.13.4", + VulnerabilityID: "CVE-2022-42003", + InstalledVersion: "2.13.4", + FixedVersion: "2.12.7.1, 2.13.4.2", + PkgPath: "/bar/jackson-databind-2.13.4.jar", + Vulnerability: dtypes.Vulnerability{ + Severity: "HIGH", + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "64244651208EC759", + }, + }, + }, + }, + { + Target: "trivy", + Type: ftypes.GoBinary, + Class: types.ClassLangPkg, + Packages: []ftypes.Package{ + { + ID: "github.com/aquasecurity/trivy@v0.57.1", + Name: "github.com/aquasecurity/trivy", + Version: "v0.57.1", + Identifier: ftypes.PkgIdentifier{ + UID: "106fee7e57f0b952", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "trivy", + Version: "v0.57.1", + }, + }, + Relationship: ftypes.RelationshipRoot, + DependsOn: []string{ + "github.com/aquasecurity/go-version@v0.0.0-20240603093900-cf8a8d29271d", + "stdlib@v1.22.9", + }, + }, + { + ID: "stdlib@v1.22.9", + Name: "stdlib", + Version: "v1.22.9", + Identifier: ftypes.PkgIdentifier{ + UID: "62e7c8aaebd94b1e", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Name: "stdlib", + Version: "v1.22.9", + }, + }, + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "github.com/aquasecurity/go-version@v0.0.0-20240603093900-cf8a8d29271d", + Name: "github.com/aquasecurity/go-version", + Version: "v0.0.0-20240603093900-cf8a8d29271d", + Identifier: ftypes.PkgIdentifier{ + UID: "350aed171d8ebed5", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-version", + Version: "v0.0.0-20240603093900-cf8a8d29271d", + }, + }, + Relationship: ftypes.RelationshipUnknown, + }, + }, + }, + }, + }, + wantComponents: map[uuid.UUID]*core.Component{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + Type: core.TypeContainerImage, + Name: "debian:12", + Root: true, + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeOCI, + Name: "debian", + Version: "sha256:4482958b4461ff7d9fabc24b3a9ab1e9a2c85ece07b2db1840c7cbc01d053e90", + Qualifiers: packageurl.Qualifiers{ + { + Key: "repository_url", + Value: "index.docker.io/library/debian", + }, + }, + }, + BOMRef: "pkg:oci/debian@sha256%3A4482958b4461ff7d9fabc24b3a9ab1e9a2c85ece07b2db1840c7cbc01d053e90?repository_url=index.docker.io%2Flibrary%2Fdebian", + }, + Properties: []core.Property{ + { + Name: "Labels:vendor", + Value: "aquasecurity", + }, + { + Name: core.PropertyRepoDigest, + Value: "debian@sha256:4482958b4461ff7d9fabc24b3a9ab1e9a2c85ece07b2db1840c7cbc01d053e90", + }, + { + Name: core.PropertyRepoTag, + Value: "debian:12", + }, + { + Name: core.PropertyRepoTag, + Value: "debian:latest", + }, + { + Name: core.PropertySchemaVersion, + Value: "2", + }, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + Type: core.TypeOS, + Name: "debian", + Version: "12", + Properties: []core.Property{ + { + Name: core.PropertyClass, + Value: "os-pkgs", + }, + { + Name: core.PropertyType, + Value: "debian", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000002", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): { + Type: core.TypeLibrary, + Name: "libc6", + Version: "2.37-15.1", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "libc6@2.37-15.1", + }, + { + Name: core.PropertyPkgType, + Value: "debian", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "33654D2C483FC3AD", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Name: "libc6", + Version: "2.37-15.1", + }, + BOMRef: "pkg:deb/libc6@2.37-15.1", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): { + Type: core.TypeLibrary, + Name: "curl", + Version: "7.50.3-1", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "curl@7.50.3-1", + }, + { + Name: core.PropertyPkgType, + Value: "debian", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "51BA9E006222819D", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Name: "curl", + Version: "7.50.3-1", + }, + BOMRef: "pkg:deb/curl@7.50.3-1", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"): { + Type: core.TypeLibrary, + Group: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4", + Files: []core.File{ + { + Path: "/foo/jackson-databind-2.13.4.jar", + }, + }, + Properties: []core.Property{ + { + Name: core.PropertyFilePath, + Value: "/foo/jackson-databind-2.13.4.jar", + }, + { + Name: core.PropertyPkgID, + Value: "com.fasterxml.jackson.core:jackson-databind:2.13.4", + }, + { + Name: core.PropertyPkgType, + Value: "jar", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "A6BD5A2FE5C00E10", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4", + }, + BOMRef: "3ff14136-e09f-4df9-80ea-000000000005", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"): { + Type: core.TypeLibrary, + Group: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4", + Files: []core.File{ + { + Path: "/bar/jackson-databind-2.13.4.jar", + }, + }, + Properties: []core.Property{ + { + Name: core.PropertyFilePath, + Value: "/bar/jackson-databind-2.13.4.jar", + }, + { + Name: core.PropertyPkgID, + Value: "com.fasterxml.jackson.core:jackson-databind:2.13.4", + }, + { + Name: core.PropertyPkgType, + Value: "jar", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "64244651208EC759", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.fasterxml.jackson.core", + Name: "jackson-databind", + Version: "2.13.4", + }, + BOMRef: "3ff14136-e09f-4df9-80ea-000000000006", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000007"): { + Type: core.TypeApplication, + Name: "trivy", + Properties: []core.Property{ + { + Name: core.PropertyClass, + Value: "lang-pkgs", + }, + { + Name: core.PropertyType, + Value: "gobinary", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000007", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000008"): { + Type: core.TypeLibrary, + Name: "github.com/aquasecurity/trivy", + Version: "v0.57.1", + SrcFile: "trivy", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "github.com/aquasecurity/trivy@v0.57.1", + }, + { + Name: core.PropertyPkgType, + Value: "gobinary", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "106fee7e57f0b952", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "trivy", + Version: "v0.57.1", + }, + BOMRef: "pkg:golang/github.com/aquasecurity/trivy@v0.57.1", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000009"): { + Type: core.TypeLibrary, + Name: "stdlib", + Version: "v1.22.9", + SrcFile: "trivy", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "stdlib@v1.22.9", + }, + { + Name: core.PropertyPkgType, + Value: "gobinary", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "62e7c8aaebd94b1e", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Name: "stdlib", + Version: "v1.22.9", + }, + BOMRef: "pkg:golang/stdlib@v1.22.9", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000010"): { + Type: core.TypeLibrary, + Name: "github.com/aquasecurity/go-version", + Version: "v0.0.0-20240603093900-cf8a8d29271d", + SrcFile: "trivy", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "github.com/aquasecurity/go-version@v0.0.0-20240603093900-cf8a8d29271d", + }, + { + Name: core.PropertyPkgType, + Value: "gobinary", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "350aed171d8ebed5", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-version", + Version: "v0.0.0-20240603093900-cf8a8d29271d", + }, + BOMRef: "pkg:golang/github.com/aquasecurity/go-version@v0.0.0-20240603093900-cf8a8d29271d", + }, + }, + }, + wantRels: map[uuid.UUID][]core.Relationship{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"), + Type: core.RelationshipContains, + }, + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"), + Type: core.RelationshipContains, + }, + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"), + Type: core.RelationshipContains, + }, + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000007"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): nil, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"), + Type: core.RelationshipDependsOn, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"): nil, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"): nil, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000007"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000008"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000008"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000010"), + Type: core.RelationshipDependsOn, + }, + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000009"), + Type: core.RelationshipDependsOn, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000009"): nil, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000010"): nil, + }, + wantVulns: map[uuid.UUID][]core.Vulnerability{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): { + { + ID: "CVE-2021-22876", + PkgName: "curl", + InstalledVersion: "7.50.3-1", + FixedVersion: "7.50.3-1+deb9u1", + Vulnerability: dtypes.Vulnerability{ + Severity: "HIGH", + }, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"): { + { + ID: "CVE-2022-42003", + PkgName: "com.fasterxml.jackson.core:jackson-databind", + InstalledVersion: "2.13.4", + FixedVersion: "2.12.7.1, 2.13.4.2", + Vulnerability: dtypes.Vulnerability{ + Severity: "HIGH", + }, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"): { + { + ID: "CVE-2022-42003", + PkgName: "com.fasterxml.jackson.core:jackson-databind", + InstalledVersion: "2.13.4", + FixedVersion: "2.12.7.1, 2.13.4.2", + Vulnerability: dtypes.Vulnerability{ + Severity: "HIGH", + }, + }, + }, + }, + }, + { + name: "Red Hat container image", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "redhat/ubi9-minimal", + ArtifactType: ftypes.TypeContainerImage, + Metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: ftypes.RedHat, + Name: "9.5", + }, + RepoTags: []string{ + "redhat/ubi9:latest", + }, + RepoDigests: []string{ + "redhat/ubi9-minimal@sha256:e1c4703364c5cb58f5462575dc90345bcd934ddc45e6c32f9c162f2b5617681c", + }, + ImageConfig: v1.ConfigFile{ + Config: v1.Config{ + Labels: map[string]string{ + "vendor": "aquasecurity", + }, + }, + }, + }, + Results: []types.Result{ + { + Target: "redhat/ubi9-minimal (redhat 9.5)", + Type: ftypes.RedHat, + Class: types.ClassOSPkg, + Packages: []ftypes.Package{ + { + ID: "glibc@2.34-125.el9_5.8.aarch64", + Name: "glibc", + Version: "2.34", + Release: "125.el9_5.8.aarch64", + Arch: "aarch64", + SrcName: "glibc", + SrcVersion: "2.34", + SrcRelease: "125.el9_5.8", + Maintainer: "Red Hat, Inc.", + BuildInfo: &ftypes.BuildInfo{ + ContentSets: []string{ + "rhel-9-for-aarch64-appstream-rpms", + "rhel-9-for-aarch64-appstream-source-rpms", + }, + }, + Identifier: ftypes.PkgIdentifier{ + UID: "2acbd589f06ebbb8", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Name: "glibc", + Version: "2.34-125.el9_5.8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "aarch64", + }, + { + Key: "distro", + Value: "redhat-9.5", + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantComponents: map[uuid.UUID]*core.Component{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + Type: core.TypeContainerImage, + Name: "redhat/ubi9-minimal", + Root: true, + PkgIdentifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeOCI, + Name: "ubi9-minimal", + Version: "sha256:e1c4703364c5cb58f5462575dc90345bcd934ddc45e6c32f9c162f2b5617681c", + Qualifiers: packageurl.Qualifiers{ + { + Key: "repository_url", + Value: "index.docker.io/redhat/ubi9-minimal", + }, + }, + }, + BOMRef: "pkg:oci/ubi9-minimal@sha256%3Ae1c4703364c5cb58f5462575dc90345bcd934ddc45e6c32f9c162f2b5617681c?repository_url=index.docker.io%2Fredhat%2Fubi9-minimal", + }, + Properties: []core.Property{ + { + Name: "Labels:vendor", + Value: "aquasecurity", + }, + { + Name: core.PropertyRepoDigest, + Value: "redhat/ubi9-minimal@sha256:e1c4703364c5cb58f5462575dc90345bcd934ddc45e6c32f9c162f2b5617681c", + }, + { + Name: core.PropertyRepoTag, + Value: "redhat/ubi9:latest", + }, + { + Name: core.PropertySchemaVersion, + Value: "2", + }, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + Type: core.TypeOS, + Name: "redhat", + Version: "9.5", + Properties: []core.Property{ + { + Name: core.PropertyClass, + Value: "os-pkgs", + }, + { + Name: core.PropertyType, + Value: "redhat", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000002", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): { + Type: core.TypeLibrary, + Name: "glibc", + Version: "2.34-125.el9_5.8", + SrcName: "glibc", + SrcVersion: "2.34-125.el9_5.8", + Supplier: "Red Hat, Inc.", + Properties: []core.Property{ + { + Name: core.PropertyContentSet, + Value: "rhel-9-for-aarch64-appstream-rpms", + }, + { + Name: core.PropertyContentSet, + Value: "rhel-9-for-aarch64-appstream-source-rpms", + }, + { + Name: core.PropertyPkgID, + Value: "glibc@2.34-125.el9_5.8.aarch64", + }, + { + Name: core.PropertyPkgType, + Value: "redhat", + }, + { + Name: core.PropertySrcName, + Value: "glibc", + }, + { + Name: core.PropertySrcRelease, + Value: "125.el9_5.8", + }, + { + Name: core.PropertySrcVersion, + Value: "2.34", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "2acbd589f06ebbb8", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Name: "glibc", + Version: "2.34-125.el9_5.8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "aarch64", + }, + { + Key: "distro", + Value: "redhat-9.5", + }, + }, + }, + BOMRef: "pkg:rpm/glibc@2.34-125.el9_5.8?arch=aarch64&distro=redhat-9.5", + }, + }, + }, + wantRels: map[uuid.UUID][]core.Relationship{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): nil, + }, + wantVulns: make(map[uuid.UUID][]core.Vulnerability), + }, + { + name: "root package", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "gobinary", + ArtifactType: ftypes.TypeFilesystem, + Results: []types.Result{ + { + Target: "test", + Type: ftypes.GoBinary, + Class: types.ClassLangPkg, + Packages: []ftypes.Package{ + { + ID: "github.com/org/root", + Name: "github.com/org/root", + Identifier: ftypes.PkgIdentifier{ + UID: "03D528806D964D22", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/org", + Name: "root", + }, + }, + Relationship: ftypes.RelationshipRoot, + DependsOn: []string{ + "github.com/org/direct@v1.0.0", + "stdlib@v1.22.1", + }, + }, + { + ID: "github.com/org/direct@v1.0.0", + Name: "github.com/org/direct", + Version: "v1.0.0", + Identifier: ftypes.PkgIdentifier{ + UID: "A74CADAD4D9805FF", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/org", + Name: "direct", + Version: "v1.0.0", + }, + }, + Relationship: ftypes.RelationshipDirect, + DependsOn: []string{ + "github.com/org/indirect@v2.0.0", + }, + }, + { + ID: "github.com/org/indirect@v2.0.0", + Name: "github.com/org/indirect", + Version: "v2.0.0", + Identifier: ftypes.PkgIdentifier{ + UID: "955AB4E7E24AC085", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/org", + Name: "indirect", + Version: "v2.0.0", + }, + }, + Relationship: ftypes.RelationshipIndirect, + }, + { + ID: "stdlib@v1.22.1", + Name: "stdlib", + Version: "v1.22.1", + Identifier: ftypes.PkgIdentifier{ + UID: "49728B9674E318A6", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Name: "stdlib", + Version: "v1.22.1", + }, + }, + Relationship: ftypes.RelationshipDirect, + }, + }, + }, + }, + }, + wantComponents: map[uuid.UUID]*core.Component{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + Type: core.TypeFilesystem, + Name: "gobinary", + Root: true, + Properties: []core.Property{ + { + Name: core.PropertySchemaVersion, + Value: "2", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000001", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + Type: core.TypeApplication, + Name: "test", + Properties: []core.Property{ + { + Name: core.PropertyClass, + Value: "lang-pkgs", + }, + { + Name: core.PropertyType, + Value: "gobinary", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000002", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): { + Type: core.TypeLibrary, + Name: "github.com/org/root", + SrcFile: "test", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "github.com/org/root", + }, + { + Name: core.PropertyPkgType, + Value: "gobinary", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "03D528806D964D22", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/org", + Name: "root", + }, + BOMRef: "pkg:golang/github.com/org/root", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): { + Type: core.TypeLibrary, + Name: "github.com/org/direct", + Version: "v1.0.0", + SrcFile: "test", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "github.com/org/direct@v1.0.0", + }, + { + Name: core.PropertyPkgType, + Value: "gobinary", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "A74CADAD4D9805FF", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/org", + Name: "direct", + Version: "v1.0.0", + }, + BOMRef: "pkg:golang/github.com/org/direct@v1.0.0", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"): { + Type: core.TypeLibrary, + Name: "github.com/org/indirect", + Version: "v2.0.0", + SrcFile: "test", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "github.com/org/indirect@v2.0.0", + }, + { + Name: core.PropertyPkgType, + Value: "gobinary", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "955AB4E7E24AC085", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/org", + Name: "indirect", + Version: "v2.0.0", + }, + BOMRef: "pkg:golang/github.com/org/indirect@v2.0.0", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"): { + Type: core.TypeLibrary, + Name: "stdlib", + Version: "v1.22.1", + SrcFile: "test", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "stdlib@v1.22.1", + }, + { + Name: core.PropertyPkgType, + Value: "gobinary", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "49728B9674E318A6", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Name: "stdlib", + Version: "v1.22.1", + }, + BOMRef: "pkg:golang/stdlib@v1.22.1", + }, + }, + }, + wantRels: map[uuid.UUID][]core.Relationship{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"), + Type: core.RelationshipDependsOn, + }, + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"), + Type: core.RelationshipDependsOn, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"), + Type: core.RelationshipDependsOn, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"): nil, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"): nil, + }, + wantVulns: make(map[uuid.UUID][]core.Vulnerability), + }, + { + name: "direct package is also dependency", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "test", + ArtifactType: ftypes.TypeFilesystem, + Results: []types.Result{ + { + Target: "poetry.lock", + Type: ftypes.Poetry, + Class: types.ClassLangPkg, + Packages: []ftypes.Package{ + { + ID: "django@5.1.6", + Name: "django", + Version: "5.1.6", + Identifier: ftypes.PkgIdentifier{ + UID: "69691e87e187021d", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypePyPi, + Name: "django", + Version: "5.1.6", + }, + }, + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "sentry-sdk@2.22.0", + Name: "sentry-sdk", + Version: "2.22.0", + Identifier: ftypes.PkgIdentifier{ + UID: "7e53a15e8bec68ad", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypePyPi, + Name: "sentry-sdk", + Version: "2.22.0", + }, + }, + Relationship: ftypes.RelationshipDirect, + DependsOn: []string{ + "django@5.1.6", + }, + }, + }, + }, + }, + }, + wantComponents: map[uuid.UUID]*core.Component{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + Type: core.TypeFilesystem, + Name: "test", + Root: true, + Properties: []core.Property{ + { + Name: core.PropertySchemaVersion, + Value: "2", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000001", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + Type: core.TypeApplication, + Name: "poetry.lock", + Properties: []core.Property{ + { + Name: core.PropertyClass, + Value: "lang-pkgs", + }, + { + Name: core.PropertyType, + Value: "poetry", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000002", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): { + Type: core.TypeLibrary, + Name: "django", + Version: "5.1.6", + SrcFile: "poetry.lock", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "django@5.1.6", + }, + { + Name: core.PropertyPkgType, + Value: "poetry", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "69691e87e187021d", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypePyPi, + Name: "django", + Version: "5.1.6", + }, + BOMRef: "pkg:pypi/django@5.1.6", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): { + Type: core.TypeLibrary, + Name: "sentry-sdk", + Version: "2.22.0", + SrcFile: "poetry.lock", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "sentry-sdk@2.22.0", + }, + { + Name: core.PropertyPkgType, + Value: "poetry", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "7e53a15e8bec68ad", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypePyPi, + Name: "sentry-sdk", + Version: "2.22.0", + }, + BOMRef: "pkg:pypi/sentry-sdk@2.22.0", + }, + }, + }, + wantRels: map[uuid.UUID][]core.Relationship{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"), + Type: core.RelationshipContains, + }, + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): nil, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"), + Type: core.RelationshipDependsOn, + }, + }, + }, + wantVulns: make(map[uuid.UUID][]core.Vulnerability), + }, + { + name: "SBOM file", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "report.cdx.json", + ArtifactType: ftypes.TypeCycloneDX, + BOM: newTestBOM(t), + }, + wantComponents: map[uuid.UUID]*core.Component{ + uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): appComponent, + uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000002"): libComponent, + }, + wantRels: map[uuid.UUID][]core.Relationship{ + uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): { + { + Dependency: uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000002"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000002"): nil, + }, + wantVulns: make(map[uuid.UUID][]core.Vulnerability), + }, + { + name: "SBOM file without root component", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "report.cdx.json", + ArtifactType: ftypes.TypeCycloneDX, + BOM: newTestBOM2(t), + }, + wantComponents: map[uuid.UUID]*core.Component{ + uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): libComponent, + }, + wantRels: map[uuid.UUID][]core.Relationship{ + uuid.MustParse("2ff14136-e09f-4df9-80ea-000000000001"): nil, + }, + wantVulns: make(map[uuid.UUID][]core.Vulnerability), + }, + { + name: "multimodule maven project", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "pom.xml", + ArtifactType: ftypes.TypeFilesystem, + Results: []types.Result{ + { + Target: "pom.xml", + Type: ftypes.Pom, + Class: types.ClassLangPkg, + Packages: []ftypes.Package{ + { + ID: "com.example:root:1.0.0::abcdef1234567001", + Name: "com.example:root", + Version: "1.0.0", + Identifier: ftypes.PkgIdentifier{ + UID: "f684ec661900abbf", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.example", + Name: "root", + Version: "1.0.0", + }, + }, + Relationship: ftypes.RelationshipRoot, + DependsOn: []string{ + "com.example:module1:1.0.0::abcdef1234567002", + "com.example:module2:2.0.0::abcdef1234567003", + }, + }, + { + ID: "com.example:module1:1.0.0::abcdef1234567002", + Name: "com.example:module1", + Version: "1.0.0", + Identifier: ftypes.PkgIdentifier{ + UID: "ce0d29336874c431", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.example", + Name: "module1", + Version: "1.0.0", + }, + }, + Relationship: ftypes.RelationshipWorkspace, + DependsOn: []string{ + "org.example:example-api:1.1.1::abcdef1234567003", + }, + }, + { + ID: "com.example:module2:2.0.0::abcdef1234567003", + Name: "com.example:module2", + Version: "2.0.0", + Identifier: ftypes.PkgIdentifier{ + UID: "387238ffef6dfa9d", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.example", + Name: "module2", + Version: "2.0.0", + }, + }, + Relationship: ftypes.RelationshipWorkspace, + DependsOn: []string{ + "org.example:example-dependency:1.2.3::abcdef1234567005", + }, + }, + { + ID: "org.example:example-api:1.1.1::abcdef1234567003", + Name: "org.example:example-api", + Version: "1.1.1", + Identifier: ftypes.PkgIdentifier{ + UID: "45cdc62618708bb7", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.example", + Name: "example-api", + Version: "1.1.1", + }, + }, + Relationship: ftypes.RelationshipDirect, + }, + { + ID: "org.example:example-dependency:1.2.3::abcdef1234567005", + Name: "org.example:example-dependency", + Version: "1.2.3", + Identifier: ftypes.PkgIdentifier{ + UID: "52fbe353a46651", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.example", + Name: "example-dependency", + Version: "1.2.3", + }, + }, + Relationship: ftypes.RelationshipDirect, + DependsOn: []string{ + "org.example:example-api:2.0.0::abcdef1234567006", + }, + }, + { + ID: "org.example:example-api:2.0.0::abcdef1234567006", + Name: "org.example:example-api", + Version: "2.0.0", + Identifier: ftypes.PkgIdentifier{ + UID: "f71d14b6d2bd8810", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.example", + Name: "example-api", + Version: "2.0.0", + }, + }, + Relationship: ftypes.RelationshipIndirect, + }, + }, + }, + }, + }, + wantComponents: map[uuid.UUID]*core.Component{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + Type: core.TypeFilesystem, + Name: "pom.xml", + Root: true, + Properties: []core.Property{ + { + Name: core.PropertySchemaVersion, + Value: "2", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000001", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + Type: core.TypeApplication, + Name: "pom.xml", + Properties: []core.Property{ + { + Name: core.PropertyClass, + Value: "lang-pkgs", + }, + { + Name: core.PropertyType, + Value: "pom", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000002", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): { + Type: core.TypeLibrary, + Group: "com.example", + Name: "root", + Version: "1.0.0", + SrcFile: "pom.xml", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "com.example:root:1.0.0::abcdef1234567001", + }, + { + Name: core.PropertyPkgType, + Value: "pom", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "f684ec661900abbf", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.example", + Name: "root", + Version: "1.0.0", + }, + BOMRef: "pkg:maven/com.example/root@1.0.0", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): { + Type: core.TypeLibrary, + Group: "com.example", + Name: "module1", + Version: "1.0.0", + SrcFile: "pom.xml", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "com.example:module1:1.0.0::abcdef1234567002", + }, + { + Name: core.PropertyPkgType, + Value: "pom", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "ce0d29336874c431", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.example", + Name: "module1", + Version: "1.0.0", + }, + BOMRef: "pkg:maven/com.example/module1@1.0.0", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"): { + Type: core.TypeLibrary, + Group: "com.example", + Name: "module2", + Version: "2.0.0", + SrcFile: "pom.xml", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "com.example:module2:2.0.0::abcdef1234567003", + }, + { + Name: core.PropertyPkgType, + Value: "pom", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "387238ffef6dfa9d", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.example", + Name: "module2", + Version: "2.0.0", + }, + BOMRef: "pkg:maven/com.example/module2@2.0.0", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"): { + Type: core.TypeLibrary, + Group: "org.example", + Name: "example-api", + Version: "1.1.1", + SrcFile: "pom.xml", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "org.example:example-api:1.1.1::abcdef1234567003", + }, + { + Name: core.PropertyPkgType, + Value: "pom", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "45cdc62618708bb7", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.example", + Name: "example-api", + Version: "1.1.1", + }, + BOMRef: "pkg:maven/org.example/example-api@1.1.1", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000007"): { + Type: core.TypeLibrary, + Group: "org.example", + Name: "example-dependency", + Version: "1.2.3", + SrcFile: "pom.xml", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "org.example:example-dependency:1.2.3::abcdef1234567005", + }, + { + Name: core.PropertyPkgType, + Value: "pom", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "52fbe353a46651", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.example", + Name: "example-dependency", + Version: "1.2.3", + }, + BOMRef: "pkg:maven/org.example/example-dependency@1.2.3", + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000008"): { + Type: core.TypeLibrary, + Group: "org.example", + Name: "example-api", + Version: "2.0.0", + SrcFile: "pom.xml", + Properties: []core.Property{ + { + Name: core.PropertyPkgID, + Value: "org.example:example-api:2.0.0::abcdef1234567006", + }, + { + Name: core.PropertyPkgType, + Value: "pom", + }, + }, + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "f71d14b6d2bd8810", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.example", + Name: "example-api", + Version: "2.0.0", + }, + BOMRef: "pkg:maven/org.example/example-api@2.0.0", + }, + }, + }, + wantRels: map[uuid.UUID][]core.Relationship{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000003"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"), + Type: core.RelationshipDependsOn, + }, + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"), + Type: core.RelationshipDependsOn, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000004"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"), + Type: core.RelationshipDependsOn, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000005"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000007"), + Type: core.RelationshipDependsOn, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000006"): nil, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000007"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000008"), + Type: core.RelationshipDependsOn, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000008"): nil, + }, + wantVulns: make(map[uuid.UUID][]core.Vulnerability), + }, + { + name: "json file created from SBOM file (BOM is empty)", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "report.cdx.json", + ArtifactType: ftypes.TypeCycloneDX, + Results: []types.Result{ + { + Target: "Java", + Type: ftypes.Jar, + Class: types.ClassLangPkg, + Packages: []ftypes.Package{ + { + ID: "org.apache.logging.log4j:log4j-core:2.23.1", + Name: "org.apache.logging.log4j:log4j-core", + Version: "2.23.1", + Identifier: ftypes.PkgIdentifier{ + UID: "6C0AE96901617503", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.apache.logging.log4j", + Name: "log4j-core", + Version: "2.23.1", + }, + }, + FilePath: "log4j-core-2.23.1.jar", + }, + }, + }, + }, + }, + wantComponents: map[uuid.UUID]*core.Component{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): fsComponent, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): libComponent, + }, + wantRels: map[uuid.UUID][]core.Relationship{ + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000001"): { + { + Dependency: uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"), + Type: core.RelationshipContains, + }, + }, + uuid.MustParse("3ff14136-e09f-4df9-80ea-000000000002"): nil, + }, + wantVulns: make(map[uuid.UUID][]core.Vulnerability), + }, + { + name: "invalid digest", + report: types.Report{ + SchemaVersion: 2, + ArtifactName: "debian:12", + ArtifactType: ftypes.TypeContainerImage, + Metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: ftypes.Debian, + Name: "12", + }, + RepoTags: []string{ + "debian:12", + }, + RepoDigests: []string{ + "debian@sha256:123", + }, + }, + }, + wantErr: "failed to parse digest", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + got, err := sbomio.NewEncoder(sbomio.WithBOMRef()).Encode(tt.report) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + require.Len(t, got.Components(), len(tt.wantComponents)) + for id, want := range tt.wantComponents { + assert.EqualExportedValues(t, *want, *got.Components()[id], id) + } + + assert.Equal(t, tt.wantRels, got.Relationships()) + assert.Equal(t, tt.wantVulns, got.Vulnerabilities()) + }) + } +} + +var ( + appComponent = &core.Component{ + Root: true, + Type: core.TypeApplication, + Name: "log4j-core-2.23.1.jar", + } + fsComponent = &core.Component{ + Root: true, + Type: core.TypeFilesystem, + Name: "report.cdx.json", + PkgIdentifier: ftypes.PkgIdentifier{ + BOMRef: "3ff14136-e09f-4df9-80ea-000000000001", + }, + Properties: core.Properties{ + { + Name: "SchemaVersion", + Value: "2", + }, + }, + } + libComponent = &core.Component{ + Type: core.TypeLibrary, + Name: "log4j-core", + Group: "org.apache.logging.log4j", + Version: "2.23.1", + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "6C0AE96901617503", + BOMRef: "pkg:maven/org.apache.logging.log4j/log4j-core@2.23.1", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.apache.logging.log4j", + Name: "log4j-core", + Version: "2.23.1", + }, + }, + Files: []core.File{ + { + Path: "log4j-core-2.23.1.jar", + }, + }, + Properties: core.Properties{ + { + Name: "FilePath", + Value: "log4j-core-2.23.1.jar", + }, + { + Name: "PkgID", + Value: "org.apache.logging.log4j:log4j-core:2.23.1", + }, + { + Name: "PkgType", + Value: "jar", + }, + }, + } +) + +func newTestBOM(t *testing.T) *core.BOM { + uuid.SetFakeUUID(t, "2ff14136-e09f-4df9-80ea-%012d") + bom := core.NewBOM(core.Options{}) + + // Copy components to avoid UUID conflicts between tests + appComp := appComponent.Clone() + libComp := libComponent.Clone() + + bom.AddComponent(appComp) + bom.AddComponent(libComp) + // Add Contains relationship between appComponent and libComponent + bom.AddRelationship(appComp, libComp, core.RelationshipContains) + // Add empty relationship for libComponent to preserve structure for SBOM rescanning + bom.AddRelationship(libComp, nil, core.RelationshipDependsOn) + return bom +} + +// BOM without root component +func newTestBOM2(t *testing.T) *core.BOM { + uuid.SetFakeUUID(t, "2ff14136-e09f-4df9-80ea-%012d") + bom := core.NewBOM(core.Options{}) + + // Copy component to avoid UUID conflicts between tests + libComp := libComponent.Clone() + + bom.AddComponent(libComp) + // Add empty relationship for libComponent to preserve structure for SBOM rescanning + bom.AddRelationship(libComp, nil, core.RelationshipDependsOn) + return bom +} diff --git a/pkg/sbom/sbom.go b/pkg/sbom/sbom.go new file mode 100644 index 000000000000..44a6868da78a --- /dev/null +++ b/pkg/sbom/sbom.go @@ -0,0 +1,346 @@ +package sbom + +import ( + "bufio" + "context" + "encoding/json" + "encoding/xml" + "io" + "strings" + + "github.com/in-toto/in-toto-golang/in_toto" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/attestation" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" + sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" + "github.com/aquasecurity/trivy/pkg/sbom/spdx" + "github.com/aquasecurity/trivy/pkg/types" +) + +type Format string + +const ( + FormatCycloneDXJSON Format = "cyclonedx-json" + FormatCycloneDXXML Format = "cyclonedx-xml" + FormatSPDXJSON Format = "spdx-json" + FormatSPDXTV Format = "spdx-tv" + FormatSPDXXML Format = "spdx-xml" + FormatAttestCycloneDXJSON Format = "attest-cyclonedx-json" + FormatAttestSPDXJSON Format = "attest-spdx-json" + FormatUnknown Format = "unknown" + + // FormatSigstoreBundleCycloneDXJSON is used for Sigstore bundle format containing CycloneDX SBOM attestation. + // This format is produced by Cosign v3+ with the new bundle format. + // ref. https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md + FormatSigstoreBundleCycloneDXJSON Format = "sigstore-bundle-cyclonedx-json" + + // FormatSigstoreBundleSPDXJSON is used for Sigstore bundle format containing SPDX SBOM attestation. + FormatSigstoreBundleSPDXJSON Format = "sigstore-bundle-spdx-json" + + // FormatLegacyCosignAttestCycloneDXJSON is used to support the older format of CycloneDX JSON Attestation + // produced by the Cosign V1. + // ref. https://github.com/sigstore/cosign/pull/2718 + FormatLegacyCosignAttestCycloneDXJSON Format = "legacy-cosign-attest-cyclonedx-json" + + // PredicateCycloneDXBeforeV05 is the PredicateCycloneDX value defined in in-toto-golang before v0.5.0. + // This is necessary for backward-compatible SBOM detection. + // ref. https://github.com/in-toto/in-toto-golang/pull/188 + PredicateCycloneDXBeforeV05 = "https://cyclonedx.org/schema" + + // SigstoreBundleMediaType is the media type for Sigstore bundles v0.3 + // ref. https://github.com/sigstore/protobuf-specs/blob/main/protos/sigstore_bundle.proto + SigstoreBundleMediaType = "application/vnd.dev.sigstore.bundle.v0.3+json" +) + +var ErrUnknownFormat = xerrors.New("Unknown SBOM format") + +type cdxHeader struct { + // XML specific field + XMLNS string `json:"-" xml:"xmlns,attr"` + + // JSON specific field + BOMFormat string `json:"bomFormat" xml:"-"` +} + +type spdxHeader struct { + SpdxID string `json:"SPDXID"` +} + +// sigstoreBundle represents the structure of a Sigstore bundle +// ref. https://github.com/sigstore/cosign/blob/main/specs/BUNDLE_SPEC.md +type sigstoreBundle struct { + MediaType string `json:"mediaType"` + DSSEEnvelope json.RawMessage `json:"dsseEnvelope"` +} + +func IsCycloneDXJSON(r io.ReadSeeker) (bool, error) { + if _, err := r.Seek(0, io.SeekStart); err != nil { + return false, xerrors.Errorf("seek error: %w", err) + } + + var cdxBom cdxHeader + if err := json.NewDecoder(r).Decode(&cdxBom); err == nil { + if cdxBom.BOMFormat == "CycloneDX" { + return true, nil + } + } + return false, nil +} +func IsCycloneDXXML(r io.ReadSeeker) (bool, error) { + if _, err := r.Seek(0, io.SeekStart); err != nil { + return false, xerrors.Errorf("seek error: %w", err) + } + + var cdxBom cdxHeader + if err := xml.NewDecoder(r).Decode(&cdxBom); err == nil { + if strings.HasPrefix(cdxBom.XMLNS, "http://cyclonedx.org") { + return true, nil + } + } + return false, nil +} + +func IsSPDXJSON(r io.ReadSeeker) (bool, error) { + if _, err := r.Seek(0, io.SeekStart); err != nil { + return false, xerrors.Errorf("seek error: %w", err) + } + + var spdxBom spdxHeader + if err := json.NewDecoder(r).Decode(&spdxBom); err == nil { + if spdxBom.SpdxID == "SPDXRef-DOCUMENT" { + return true, nil + } + } + return false, nil +} + +func IsSPDXTV(r io.ReadSeeker) (bool, error) { + if _, err := r.Seek(0, io.SeekStart); err != nil { + return false, xerrors.Errorf("seek error: %w", err) + } + + if scanner := bufio.NewScanner(r); scanner.Scan() { + if strings.HasPrefix(scanner.Text(), "SPDX") { + return true, nil + } + } + return false, nil +} + +func DetectFormat(r io.ReadSeeker) (Format, error) { + // Rewind the SBOM file at the end + defer r.Seek(0, io.SeekStart) + + // Try CycloneDX JSON + if ok, err := IsCycloneDXJSON(r); err != nil { + return FormatUnknown, err + } else if ok { + return FormatCycloneDXJSON, nil + } + + // Try CycloneDX XML + if ok, err := IsCycloneDXXML(r); err != nil { + return FormatUnknown, err + } else if ok { + return FormatCycloneDXXML, nil + } + + // Try SPDX json + if ok, err := IsSPDXJSON(r); err != nil { + return FormatUnknown, err + } else if ok { + return FormatSPDXJSON, nil + } + + // Try SPDX tag-value + if ok, err := IsSPDXTV(r); err != nil { + return FormatUnknown, err + } else if ok { + return FormatSPDXTV, nil + } + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return FormatUnknown, xerrors.Errorf("seek error: %w", err) + } + + // Try in-toto attestation (CycloneDX or SPDX) + format, ok := decodeAttestationFormat(r) + if ok { + return format, nil + } + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return FormatUnknown, xerrors.Errorf("seek error: %w", err) + } + + // Try Sigstore bundle + format, ok = decodeSigstoreBundleFormat(r) + if ok { + return format, nil + } + + return FormatUnknown, nil +} + +func decodeAttestationFormat(r io.ReadSeeker) (Format, bool) { + var s attestation.Statement + + if err := json.NewDecoder(r).Decode(&s); err != nil { + return "", false + } + + if s.Predicate == nil { + return "", false + } + + m, ok := s.Predicate.(map[string]any) + if !ok { + return "", false + } + + // Check CycloneDX + if s.PredicateType == in_toto.PredicateCycloneDX || s.PredicateType == PredicateCycloneDXBeforeV05 { + if _, ok := m["Data"]; ok { + return FormatLegacyCosignAttestCycloneDXJSON, true + } + return FormatAttestCycloneDXJSON, true + } + + // Check SPDX + if s.PredicateType == in_toto.PredicateSPDX { + if spdxID, ok := m["SPDXID"].(string); ok && spdxID == "SPDXRef-DOCUMENT" { + return FormatAttestSPDXJSON, true + } + } + + return "", false +} + +func decodeSigstoreBundleFormat(r io.ReadSeeker) (Format, bool) { + var bundle sigstoreBundle + if err := json.NewDecoder(r).Decode(&bundle); err != nil { + return "", false + } + + // Check if the media type indicates a Sigstore bundle + if bundle.MediaType != SigstoreBundleMediaType { + return "", false + } + + if bundle.DSSEEnvelope == nil { + return "", false + } + + // Parse the DSSE envelope to determine the SBOM format + var s attestation.Statement + if err := json.Unmarshal(bundle.DSSEEnvelope, &s); err != nil { + return "", false + } + + if s.Predicate == nil { + return "", false + } + + switch s.PredicateType { + case in_toto.PredicateCycloneDX, PredicateCycloneDXBeforeV05: + return FormatSigstoreBundleCycloneDXJSON, true + case in_toto.PredicateSPDX: + return FormatSigstoreBundleSPDXJSON, true + } + + return "", false +} + +func Decode(ctx context.Context, f io.Reader, format Format) (types.SBOM, error) { + var ( + v any + bom *core.BOM + decoder interface{ Decode(any) error } + ) + + switch format { + case FormatCycloneDXJSON: + bom = core.NewBOM(core.Options{GenerateBOMRef: true}) + v = &cyclonedx.BOM{BOM: bom} + decoder = json.NewDecoder(f) + case FormatAttestCycloneDXJSON: + // dsse envelope + // => in-toto attestation + // => CycloneDX JSON + bom = core.NewBOM(core.Options{GenerateBOMRef: true}) + v = &attestation.Statement{ + Predicate: &cyclonedx.BOM{BOM: bom}, + } + decoder = json.NewDecoder(f) + case FormatLegacyCosignAttestCycloneDXJSON: + // dsse envelope + // => in-toto attestation + // => cosign predicate + // => CycloneDX JSON + bom = core.NewBOM(core.Options{GenerateBOMRef: true}) + v = &attestation.Statement{ + Predicate: &attestation.CosignPredicate{ + Data: &cyclonedx.BOM{BOM: bom}, + }, + } + decoder = json.NewDecoder(f) + case FormatAttestSPDXJSON: + // dsse envelope + // => in-toto attestation + // => SPDX JSON + bom = core.NewBOM(core.Options{}) + v = &attestation.Statement{ + Predicate: &spdx.SPDX{BOM: bom}, + } + decoder = json.NewDecoder(f) + case FormatSigstoreBundleCycloneDXJSON: + // Sigstore bundle + // => dsse envelope + // => in-toto attestation + // => CycloneDX JSON + bom = core.NewBOM(core.Options{GenerateBOMRef: true}) + v = &attestation.SigstoreBundle{ + DSSEEnvelope: attestation.Statement{ + Predicate: &cyclonedx.BOM{BOM: bom}, + }, + } + decoder = json.NewDecoder(f) + case FormatSigstoreBundleSPDXJSON: + // Sigstore bundle + // => dsse envelope + // => in-toto attestation + // => SPDX JSON + bom = core.NewBOM(core.Options{}) + v = &attestation.SigstoreBundle{ + DSSEEnvelope: attestation.Statement{ + Predicate: &spdx.SPDX{BOM: bom}, + }, + } + decoder = json.NewDecoder(f) + case FormatSPDXJSON: + bom = core.NewBOM(core.Options{}) + v = &spdx.SPDX{BOM: bom} + decoder = json.NewDecoder(f) + case FormatSPDXTV: + bom = core.NewBOM(core.Options{}) + v = &spdx.SPDX{BOM: bom} + decoder = spdx.NewTVDecoder(f) + default: + return types.SBOM{}, xerrors.Errorf("%s scanning is not yet supported", format) + + } + + // Decode a file content into core.BOM + if err := decoder.Decode(v); err != nil { + return types.SBOM{}, xerrors.Errorf("failed to decode: %w", err) + } + + var sbom types.SBOM + if err := sbomio.NewDecoder(bom).Decode(ctx, &sbom); err != nil { + return types.SBOM{}, xerrors.Errorf("failed to decode: %w", err) + } + + return sbom, nil +} diff --git a/pkg/sbom/sbom_test.go b/pkg/sbom/sbom_test.go new file mode 100644 index 000000000000..d8e89d09a47e --- /dev/null +++ b/pkg/sbom/sbom_test.go @@ -0,0 +1,282 @@ +package sbom_test + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/sbom" +) + +// Test data constants for SBOM format detection and decoding. +// Each constant contains base64-encoded in-toto statement in the payload field. +const ( + // SPDX attestation (DSSE envelope) + // payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"SPDXID":"SPDXRef-DOCUMENT","spdxVersion":"SPDX-2.3","name":"test","dataLicense":"CC0-1.0","documentNamespace":"http://example.invalid/test","creationInfo":{"creators":["Tool: test"],"created":"2025-01-01T00:00:00Z"},"packages":[]}} + spdxAttestation = `{ + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7IlNQRFhJRCI6IlNQRFhSZWYtRE9DVU1FTlQiLCJzcGR4VmVyc2lvbiI6IlNQRFgtMi4zIiwibmFtZSI6InRlc3QiLCJkYXRhTGljZW5zZSI6IkNDMC0xLjAiLCJkb2N1bWVudE5hbWVzcGFjZSI6Imh0dHA6Ly9leGFtcGxlLmludmFsaWQvdGVzdCIsImNyZWF0aW9uSW5mbyI6eyJjcmVhdG9ycyI6WyJUb29sOiB0ZXN0Il0sImNyZWF0ZWQiOiIyMDI1LTAxLTAxVDAwOjAwOjAwWiJ9LCJwYWNrYWdlcyI6W119fQ==", + "signatures": [] + }` + + // SPDX attestation with invalid SPDXID + // payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"SPDXID":"InvalidID","spdxVersion":"SPDX-2.3","name":"test"}} + spdxAttestationInvalidID = `{ + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7IlNQRFhJRCI6IkludmFsaWRJRCIsInNwZHhWZXJzaW9uIjoiU1BEWC0yLjMiLCJuYW1lIjoidGVzdCJ9fQ==", + "signatures": [] + }` + + // SPDX attestation with invalid predicate (string instead of object) + // payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":"invalid"} + spdxAttestationInvalidPredicate = `{ + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjoiaW52YWxpZCJ9", + "signatures": [] + }` + + // CycloneDX attestation (DSSE envelope) + // payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://cyclonedx.org/bom","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"bomFormat":"CycloneDX","specVersion":"1.4","version":1}} + cycloneDXAttestation = `{ + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsInNwZWNWZXJzaW9uIjoiMS40IiwidmVyc2lvbiI6MX19", + "signatures": [] + }` + + // Sigstore bundle with CycloneDX SBOM + // dsseEnvelope.payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://cyclonedx.org/bom","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"bomFormat":"CycloneDX","specVersion":"1.4","version":1}} + sigstoreBundleCycloneDX = `{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "dsseEnvelope": { + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsInNwZWNWZXJzaW9uIjoiMS40IiwidmVyc2lvbiI6MX19", + "signatures": [] + } + }` + + // Sigstore bundle with SPDX SBOM + // dsseEnvelope.payload decoded: {"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://spdx.dev/Document","subject":[{"name":"test","digest":{"sha256":"abc123"}}],"predicate":{"SPDXID":"SPDXRef-DOCUMENT","spdxVersion":"SPDX-2.3","name":"test","dataLicense":"CC0-1.0","documentNamespace":"http://example.invalid/test","creationInfo":{"creators":["Tool: test"],"created":"2025-01-01T00:00:00Z"},"packages":[]}} + sigstoreBundleSPDX = `{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.3+json", + "dsseEnvelope": { + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3NwZHguZGV2L0RvY3VtZW50Iiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7IlNQRFhJRCI6IlNQRFhSZWYtRE9DVU1FTlQiLCJzcGR4VmVyc2lvbiI6IlNQRFgtMi4zIiwibmFtZSI6InRlc3QiLCJkYXRhTGljZW5zZSI6IkNDMC0xLjAiLCJkb2N1bWVudE5hbWVzcGFjZSI6Imh0dHA6Ly9leGFtcGxlLmludmFsaWQvdGVzdCIsImNyZWF0aW9uSW5mbyI6eyJjcmVhdG9ycyI6WyJUb29sOiB0ZXN0Il0sImNyZWF0ZWQiOiIyMDI1LTAxLTAxVDAwOjAwOjAwWiJ9LCJwYWNrYWdlcyI6W119fQ==", + "signatures": [] + } + }` + + // Sigstore bundle with unsupported media type version + sigstoreBundleUnsupportedVersion = `{ + "mediaType": "application/vnd.dev.sigstore.bundle.v0.4+json", + "dsseEnvelope": { + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL2N5Y2xvbmVkeC5vcmcvYm9tIiwic3ViamVjdCI6W3sibmFtZSI6InRlc3QiLCJkaWdlc3QiOnsic2hhMjU2IjoiYWJjMTIzIn19XSwicHJlZGljYXRlIjp7ImJvbUZvcm1hdCI6IkN5Y2xvbmVEWCIsInNwZWNWZXJzaW9uIjoiMS40IiwidmVyc2lvbiI6MX19", + "signatures": [] + } + }` +) + +func TestDetectFormat(t *testing.T) { + tests := []struct { + name string + input string + want sbom.Format + }{ + { + name: "SPDX attestation", + input: spdxAttestation, + want: sbom.FormatAttestSPDXJSON, + }, + { + name: "SPDX attestation with invalid SPDXID", + input: spdxAttestationInvalidID, + want: sbom.FormatUnknown, + }, + { + name: "CycloneDX attestation", + input: cycloneDXAttestation, + want: sbom.FormatAttestCycloneDXJSON, + }, + { + name: "SPDX JSON", + input: `{ + "SPDXID": "SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.3", + "name": "test" + }`, + want: sbom.FormatSPDXJSON, + }, + { + name: "CycloneDX JSON", + input: `{ + "bomFormat": "CycloneDX", + "specVersion": "1.4" + }`, + want: sbom.FormatCycloneDXJSON, + }, + { + name: "Unknown format", + input: `{ + "unknown": "format" + }`, + want: sbom.FormatUnknown, + }, + { + name: "Sigstore bundle with CycloneDX", + input: sigstoreBundleCycloneDX, + want: sbom.FormatSigstoreBundleCycloneDXJSON, + }, + { + name: "Sigstore bundle with SPDX", + input: sigstoreBundleSPDX, + want: sbom.FormatSigstoreBundleSPDXJSON, + }, + { + name: "Sigstore bundle with unsupported version", + input: sigstoreBundleUnsupportedVersion, + want: sbom.FormatUnknown, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := strings.NewReader(tt.input) + got, err := sbom.DetectFormat(r) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestDecode(t *testing.T) { + tests := []struct { + name string + input string + format sbom.Format + wantErr bool + }{ + { + name: "SPDX attestation", + input: spdxAttestation, + format: sbom.FormatAttestSPDXJSON, + wantErr: false, + }, + { + name: "SPDX attestation with invalid predicate", + input: spdxAttestationInvalidPredicate, + format: sbom.FormatAttestSPDXJSON, + wantErr: true, + }, + { + name: "CycloneDX attestation", + input: cycloneDXAttestation, + format: sbom.FormatAttestCycloneDXJSON, + wantErr: false, + }, + { + name: "Sigstore bundle with CycloneDX", + input: sigstoreBundleCycloneDX, + format: sbom.FormatSigstoreBundleCycloneDXJSON, + wantErr: false, + }, + { + name: "Sigstore bundle with SPDX", + input: sigstoreBundleSPDX, + format: sbom.FormatSigstoreBundleSPDXJSON, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := strings.NewReader(tt.input) + _, err := sbom.Decode(t.Context(), r, tt.format) + + if tt.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} + +func TestIsSPDXJSON(t *testing.T) { + tests := []struct { + name string + input string + want bool + }{ + { + name: "Valid SPDX JSON", + input: `{ + "SPDXID": "SPDXRef-DOCUMENT", + "spdxVersion": "SPDX-2.3" + }`, + want: true, + }, + { + name: "Invalid SPDXID", + input: `{ + "SPDXID": "InvalidID", + "spdxVersion": "SPDX-2.3" + }`, + want: false, + }, + { + name: "Not SPDX", + input: `{ + "bomFormat": "CycloneDX" + }`, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := strings.NewReader(tt.input) + got, err := sbom.IsSPDXJSON(r) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestIsCycloneDXJSON(t *testing.T) { + tests := []struct { + name string + input string + want bool + }{ + { + name: "Valid CycloneDX JSON", + input: `{ + "bomFormat": "CycloneDX", + "specVersion": "1.4" + }`, + want: true, + }, + { + name: "Not CycloneDX", + input: `{ + "SPDXID": "SPDXRef-DOCUMENT" + }`, + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := strings.NewReader(tt.input) + got, err := sbom.IsCycloneDXJSON(r) + + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/sbom/spdx/export_test.go b/pkg/sbom/spdx/export_test.go new file mode 100644 index 000000000000..e7ec3c1a6586 --- /dev/null +++ b/pkg/sbom/spdx/export_test.go @@ -0,0 +1,10 @@ +package spdx + +import "github.com/spdx/tools-golang/spdx" + +// Bridge to expose spdx marshaler internals to tests in the spdx_test package. + +// NormalizeLicenses exports normalizeLicenses for testing. +func (m *Marshaler) NormalizeLicenses(licenses []string) (string, []*spdx.OtherLicense) { + return m.normalizeLicenses(licenses) +} diff --git a/pkg/sbom/spdx/marshal.go b/pkg/sbom/spdx/marshal.go new file mode 100644 index 000000000000..b4be226f4230 --- /dev/null +++ b/pkg/sbom/spdx/marshal.go @@ -0,0 +1,665 @@ +package spdx + +import ( + "context" + "fmt" + "slices" + "sort" + "strconv" + "strings" + "time" + + "github.com/mitchellh/hashstructure/v2" + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "github.com/spdx/tools-golang/spdx" + "github.com/spdx/tools-golang/spdx/v2/common" + spdxutils "github.com/spdx/tools-golang/utils" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/digest" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/licensing/expression" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom/core" + sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +const ( + DocumentSPDXIdentifier = "DOCUMENT" + DocumentNamespace = "http://trivy.dev" + CreatorOrganization = "aquasecurity" + CreatorTool = "trivy" + noneField = "NONE" + noAssertionField = "NOASSERTION" +) + +const ( + CategoryPackageManager = "PACKAGE-MANAGER" + RefTypePurl = "purl" + + // Package Purpose fields + PackagePurposeOS = "OPERATING-SYSTEM" + PackagePurposeContainer = "CONTAINER" + PackagePurposeSource = "SOURCE" + PackagePurposeApplication = "APPLICATION" + PackagePurposeLibrary = "LIBRARY" + + PackageSupplierNoAssertion = "NOASSERTION" + PackageSupplierOrganization = "Organization" + + PackageAnnotatorToolField = "Tool" + + RelationShipContains = common.TypeRelationshipContains + RelationShipDescribe = common.TypeRelationshipDescribe + RelationShipDependsOn = common.TypeRelationshipDependsOn + + ElementOperatingSystem = "OperatingSystem" + ElementApplication = "Application" + ElementPackage = "Package" + ElementFile = "File" + + LicenseRefPrefix = "LicenseRef" +) + +var ( + SourcePackagePrefix = "built package from" + SourceFilePrefix = "package found in" +) + +// duplicateProperties contains a list of properties contained in other fields. +var duplicateProperties = []string{ + // `SourceInfo` contains SrcName and SrcVersion (it contains PropertySrcRelease and PropertySrcEpoch) + core.PropertySrcName, + core.PropertySrcRelease, + core.PropertySrcEpoch, + core.PropertySrcVersion, + // `File` contains filePath. + core.PropertyFilePath, +} + +type Marshaler struct { + format spdx.Document + hasher Hash + appVersion string // Trivy version. It needed for `creator` field + logger *log.Logger +} + +type Hash func(v any, format hashstructure.Format, opts *hashstructure.HashOptions) (uint64, error) + +type marshalOption func(*Marshaler) + +func WithHasher(hasher Hash) marshalOption { + return func(opts *Marshaler) { + opts.hasher = hasher + } +} + +func NewMarshaler(version string, opts ...marshalOption) *Marshaler { + m := &Marshaler{ + format: spdx.Document{}, + hasher: hashstructure.Hash, + appVersion: version, + logger: log.WithPrefix(log.PrefixSPDX), + } + + for _, opt := range opts { + opt(m) + } + + return m +} + +func (m *Marshaler) MarshalReport(ctx context.Context, report types.Report) (*spdx.Document, error) { + // Convert into an intermediate representation + bom, err := sbomio.NewEncoder().Encode(report) + if err != nil { + return nil, xerrors.Errorf("failed to marshal report: %w", err) + } + + return m.Marshal(ctx, bom) +} + +func (m *Marshaler) Marshal(ctx context.Context, bom *core.BOM) (*spdx.Document, error) { + var ( + relationShips []*spdx.Relationship + packages []*spdx.Package + ) + + // Lock time to use same time for all spdx fields + timeNow := clock.Now(ctx).UTC().Format(time.RFC3339) + + root := bom.Root() + pkgDownloadLocation := m.packageDownloadLocation(root) + + // Component ID => SPDX ID + packageIDs := make(map[uuid.UUID]spdx.ElementID) + + // Root package contains OS, OS packages, language-specific packages and so on. + rootPkg, err := m.rootSPDXPackage(root, timeNow, pkgDownloadLocation) + if err != nil { + return nil, xerrors.Errorf("failed to generate a root package: %w", err) + } + packages = append(packages, rootPkg) + relationShips = append(relationShips, + m.spdxRelationShip(DocumentSPDXIdentifier, rootPkg.PackageSPDXIdentifier, RelationShipDescribe), + ) + packageIDs[root.ID()] = rootPkg.PackageSPDXIdentifier + + var files []*spdx.File + var otherLicenses []*spdx.OtherLicense + for _, c := range bom.Components() { + if c.Root { + continue + } + spdxPackage, err := m.spdxPackage(c, timeNow, pkgDownloadLocation) + if err != nil { + return nil, xerrors.Errorf("spdx package error: %w", err) + } + + // Add advisories for package + // cf. https://spdx.github.io/spdx-spec/v2.3/how-to-use/#k1-including-security-information-in-a-spdx-document + if vulns, ok := bom.Vulnerabilities()[c.ID()]; ok { + for _, v := range vulns { + spdxPackage.PackageExternalReferences = append(spdxPackage.PackageExternalReferences, m.advisoryExternalReference(v.PrimaryURL)) + } + } + + packages = append(packages, &spdxPackage) + packageIDs[c.ID()] = spdxPackage.PackageSPDXIdentifier + + // Fill licenses + license, others := m.spdxLicense(c) + // The Declared License is what the authors of a project believe govern the package + spdxPackage.PackageLicenseConcluded = license + // The Concluded License field is the license the SPDX file creator believes governs the package + spdxPackage.PackageLicenseDeclared = license + otherLicenses = append(otherLicenses, others...) + + spdxFiles, err := m.spdxFiles(c) + if err != nil { + return nil, xerrors.Errorf("spdx files error: %w", err) + } else if len(spdxFiles) == 0 { + continue + } + + files = append(files, spdxFiles...) + for _, file := range spdxFiles { + relationShips = append(relationShips, + m.spdxRelationShip(spdxPackage.PackageSPDXIdentifier, file.FileSPDXIdentifier, RelationShipContains), + ) + } + verificationCode, err := spdxutils.GetVerificationCode(spdxFiles, "") + if err != nil { + return nil, xerrors.Errorf("package verification error: %w", err) + } + spdxPackage.FilesAnalyzed = true + spdxPackage.PackageVerificationCode = &verificationCode + } + + for id, rels := range bom.Relationships() { + for _, rel := range rels { + refA, ok := packageIDs[id] + if !ok { + continue + } + refB, ok := packageIDs[rel.Dependency] + if !ok { + continue + } + relationShips = append(relationShips, m.spdxRelationShip(refA, refB, m.spdxRelationshipType(rel.Type))) + } + } + + sortPackages(packages) + sortRelationships(relationShips) + sortFiles(files) + otherLicenses = sortOtherLicenses(otherLicenses) + + return &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: DocumentSPDXIdentifier, + DocumentName: root.Name, + DocumentNamespace: getDocumentNamespace(root), + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: CreatorOrganization, + CreatorType: "Organization", + }, + { + Creator: fmt.Sprintf("%s-%s", CreatorTool, m.appVersion), + CreatorType: "Tool", + }, + }, + Created: timeNow, + }, + Packages: packages, + Relationships: relationShips, + Files: files, + OtherLicenses: otherLicenses, + }, nil +} + +func (m *Marshaler) packageDownloadLocation(root *core.Component) string { + location := noneField + // this field is used for git/mercurial/subversion/bazaar: + // https://spdx.github.io/spdx-spec/v2.2.2/package-information/#77-package-download-location-field + if root.Type == core.TypeRepository { + // Trivy currently only supports git repositories. Format examples: + // git+https://git.myproject.org/MyProject.git + // git+http://git.myproject.org/MyProject + location = fmt.Sprintf("git+%s", root.Name) + } + return location +} + +func (m *Marshaler) rootSPDXPackage(root *core.Component, timeNow, pkgDownloadLocation string) (*spdx.Package, error) { + var externalReferences []*spdx.PackageExternalReference + // When the target is a container image, add PURL to the external references of the root package. + if root.PkgIdentifier.PURL != nil { + externalReferences = append(externalReferences, m.purlExternalReference(root.PkgIdentifier.PURL.String())) + } + + pkgID, err := calcSPDXID(m.hasher, fmt.Sprintf("%s-%s", root.Name, root.Type)) + if err != nil { + return nil, xerrors.Errorf("failed to get %s package ID: %w", pkgID, err) + } + + pkgPurpose := PackagePurposeSource + if root.Type == core.TypeContainerImage { + pkgPurpose = PackagePurposeContainer + } + + return &spdx.Package{ + PackageName: root.Name, + PackageSPDXIdentifier: elementID(camelCase(string(root.Type)), pkgID), + PackageDownloadLocation: pkgDownloadLocation, + Annotations: m.spdxAnnotations(root, timeNow), + PackageExternalReferences: externalReferences, + PrimaryPackagePurpose: pkgPurpose, + }, nil +} + +func (m *Marshaler) appendAnnotation(annotations []spdx.Annotation, timeNow, key, value string) []spdx.Annotation { + if value == "" { + return annotations + } + return append(annotations, spdx.Annotation{ + AnnotationDate: timeNow, + AnnotationType: spdx.CategoryOther, + Annotator: spdx.Annotator{ + Annotator: fmt.Sprintf("%s-%s", CreatorTool, m.appVersion), + AnnotatorType: PackageAnnotatorToolField, + }, + AnnotationComment: fmt.Sprintf("%s: %s", key, value), + }) +} + +func (m *Marshaler) purlExternalReference(packageURL string) *spdx.PackageExternalReference { + return &spdx.PackageExternalReference{ + Category: CategoryPackageManager, + RefType: RefTypePurl, + Locator: packageURL, + } +} + +func (m *Marshaler) advisoryExternalReference(primaryURL string) *spdx.PackageExternalReference { + return &spdx.PackageExternalReference{ + Category: common.CategorySecurity, + RefType: common.TypeSecurityAdvisory, + Locator: primaryURL, + } +} + +func (m *Marshaler) spdxPackage(c *core.Component, timeNow, pkgDownloadLocation string) (spdx.Package, error) { + pkgID, err := calcSPDXID(m.hasher, c) + if err != nil { + return spdx.Package{}, xerrors.Errorf("failed to get os metadata package ID: %w", err) + } + + var elementType, purpose, sourceInfo string + var supplier *spdx.Supplier + switch c.Type { + case core.TypeOS: + elementType = ElementOperatingSystem + purpose = PackagePurposeOS + case core.TypeApplication: + elementType = ElementApplication + purpose = PackagePurposeApplication + case core.TypeLibrary: + elementType = ElementPackage + purpose = PackagePurposeLibrary + + // We need to create a new `LicesenRef-*` component for licenses that are not in the SPDX license list + // So we will fill licenses later + + if c.SrcName != "" { + sourceInfo = fmt.Sprintf("%s: %s %s", SourcePackagePrefix, c.SrcName, c.SrcVersion) + } else if c.SrcFile != "" { + sourceInfo = fmt.Sprintf("%s: %s", SourceFilePrefix, c.SrcFile) + } + + supplier = &spdx.Supplier{Supplier: PackageSupplierNoAssertion} + if c.Supplier != "" { + supplier = &spdx.Supplier{ + SupplierType: PackageSupplierOrganization, // Always use "Organization" at the moment as it is difficult to distinguish between "Person" or "Organization". + Supplier: c.Supplier, + } + } + } + + var pkgExtRefs []*spdx.PackageExternalReference + if c.PkgIdentifier.PURL != nil { + pkgExtRefs = []*spdx.PackageExternalReference{m.purlExternalReference(c.PkgIdentifier.PURL.String())} + } + + var digests []digest.Digest + for _, f := range c.Files { + // The file digests are stored separately. + if f.Path != "" { + continue + } + digests = append(digests, f.Digests...) + } + + return spdx.Package{ + PackageSPDXIdentifier: elementID(elementType, pkgID), + PackageName: spdxPkgName(c), + PackageVersion: c.Version, + PrimaryPackagePurpose: purpose, + PackageDownloadLocation: pkgDownloadLocation, + PackageExternalReferences: pkgExtRefs, + Annotations: m.spdxAnnotations(c, timeNow), + PackageSourceInfo: sourceInfo, + PackageSupplier: supplier, + PackageChecksums: m.spdxChecksums(digests), + }, nil +} + +func spdxPkgName(component *core.Component) string { + if p := component.PkgIdentifier.PURL; p != nil && component.Group != "" { + if p.Type == packageurl.TypeMaven || p.Type == packageurl.TypeGradle { + return component.Group + ":" + component.Name + } + return component.Group + "/" + component.Name + } + return component.Name +} +func (m *Marshaler) spdxAnnotations(c *core.Component, timeNow string) []spdx.Annotation { + var annotations []spdx.Annotation + for _, p := range c.Properties { + // Add properties that are not in other fields. + if !slices.Contains(duplicateProperties, p.Name) { + annotations = m.appendAnnotation(annotations, timeNow, p.Name, p.Value) + } + } + return annotations +} + +func (m *Marshaler) spdxLicense(c *core.Component) (string, []*spdx.OtherLicense) { + // Only library components contain licenses + if c.Type != core.TypeLibrary { + return "", nil + } + if len(c.Licenses) == 0 { + return noAssertionField, nil + } + return m.normalizeLicenses(c.Licenses) +} + +func (m *Marshaler) normalizeLicenses(licenses []string) (string, []*spdx.OtherLicense) { + var otherLicenses = make(map[string]*spdx.OtherLicense) // licenseID -> OtherLicense + + license := strings.Join(xslices.Map(licenses, func(license string) string { + // We need to save text licenses before normalization, + // because it is impossible to handle all cases possible in the text. + // as an example, parse a license with 2 consecutive tokens (see https://github.com/aquasecurity/trivy/issues/8465) + if after, ok := strings.CutPrefix(license, licensing.LicenseTextPrefix); ok { + license = after + otherLicense := m.newOtherLicense(license, true) + otherLicenses[otherLicense.LicenseIdentifier] = otherLicense + return otherLicense.LicenseIdentifier + } + + // e.g. GPL-3.0-with-autoconf-exception + license = strings.ReplaceAll(license, "-with-", " WITH ") + license = strings.ReplaceAll(license, "-WITH-", " WITH ") + return fmt.Sprintf("(%s)", license) + }), " AND ") + + replaceOtherLicenses := func(expr expression.Expression) expression.Expression { + var licenseName string + switch e := expr.(type) { + case expression.SimpleExpr: + if strings.HasPrefix(e.License, LicenseRefPrefix) || e.IsSPDXExpression() { + return e + } + + licenseName = e.License + case expression.CompoundExpr: + // Check only CompoundExpr with `WITH` token as one license + if e.Conjunction() != expression.TokenWith { + return expr + } + + // Check that license and exception are valid + if e.IsSPDXExpression() { + // Use SimpleExpr for a valid SPDX license with an exception, + // to avoid parsing the license and exception separately. + return expression.SimpleExpr{License: e.String()} + } + + licenseName = e.String() + } + + l := m.newOtherLicense(licenseName, false) + otherLicenses[l.LicenseIdentifier] = l + return expression.SimpleExpr{License: l.LicenseIdentifier} + } + + normalizedLicense, err := expression.Normalize(license, licensing.NormalizeLicenseExpression, expression.NormalizeForSPDX, replaceOtherLicenses) + if err != nil { + // Not fail on the invalid license + m.logger.Warn("Unable to marshal SPDX licenses", log.String("license", license)) + return "", nil + } + + return normalizedLicense.String(), lo.Ternary(len(otherLicenses) > 0, lo.Values(otherLicenses), nil) +} + +// newOtherLicense create new OtherLicense for license not included in the SPDX license list +func (m *Marshaler) newOtherLicense(license string, text bool) *spdx.OtherLicense { + otherLicense := spdx.OtherLicense{} + if text { + otherLicense.LicenseName = noAssertionField + otherLicense.ExtractedText = license + otherLicense.LicenseComment = "The license text represents text found in package metadata and may not represent the full text of the license" + } else { + otherLicense.LicenseName = license + otherLicense.ExtractedText = fmt.Sprintf("This component is licensed under %q", license) + } + licenseID, err := calcSPDXID(m.hasher, otherLicense) + if err != nil { + // This must be an unattainable case. + m.logger.Warn("Unable to calculate SPDX licenses ID", log.String("license", license), log.Err(err)) + licenseID = license + } + otherLicense.LicenseIdentifier = LicenseRefPrefix + "-" + licenseID + + return &otherLicense +} + +func (m *Marshaler) spdxChecksums(digests []digest.Digest) []common.Checksum { + var checksums []common.Checksum + for _, d := range digests { + var alg spdx.ChecksumAlgorithm + switch d.Algorithm() { + case digest.SHA1: + alg = spdx.SHA1 + case digest.SHA256: + alg = spdx.SHA256 + case digest.MD5: + alg = spdx.MD5 + default: + return nil + } + checksums = append(checksums, spdx.Checksum{ + Algorithm: alg, + Value: d.Encoded(), + }) + } + + return checksums +} + +func (m *Marshaler) spdxFiles(c *core.Component) ([]*spdx.File, error) { + var files []*spdx.File + for _, file := range c.Files { + if file.Path == "" || len(file.Digests) == 0 { + continue + } + spdxFile, err := m.spdxFile(file.Path, file.Digests) + if err != nil { + return nil, xerrors.Errorf("failed to parse file: %w", err) + } + files = append(files, spdxFile) + } + return files, nil +} + +func (m *Marshaler) spdxFile(filePath string, digests []digest.Digest) (*spdx.File, error) { + pkgID, err := calcSPDXID(m.hasher, filePath) + if err != nil { + return nil, xerrors.Errorf("failed to get %s package ID: %w", filePath, err) + } + return &spdx.File{ + FileSPDXIdentifier: spdx.ElementID(fmt.Sprintf("File-%s", pkgID)), + FileName: filePath, + Checksums: m.spdxChecksums(digests), + }, nil +} + +func (m *Marshaler) spdxRelationShip(refA, refB spdx.ElementID, operator string) *spdx.Relationship { + ref := spdx.Relationship{ + RefA: common.MakeDocElementID("", string(refA)), + RefB: common.MakeDocElementID("", string(refB)), + Relationship: operator, + } + return &ref +} + +func (m *Marshaler) spdxRelationshipType(relType core.RelationshipType) string { + switch relType { + case core.RelationshipDependsOn: + return RelationShipDependsOn + case core.RelationshipContains: + return RelationShipContains + case core.RelationshipDescribes: + return RelationShipDescribe + default: + return RelationShipDependsOn + } +} + +func sortPackages(pkgs []*spdx.Package) { + sort.Slice(pkgs, func(i, j int) bool { + switch { + case pkgs[i].PrimaryPackagePurpose != pkgs[j].PrimaryPackagePurpose: + return pkgs[i].PrimaryPackagePurpose < pkgs[j].PrimaryPackagePurpose + case pkgs[i].PackageName != pkgs[j].PackageName: + return pkgs[i].PackageName < pkgs[j].PackageName + default: + return pkgs[i].PackageSPDXIdentifier < pkgs[j].PackageSPDXIdentifier + } + }) +} + +func sortRelationships(rels []*spdx.Relationship) { + sort.Slice(rels, func(i, j int) bool { + switch { + case rels[i].RefA.ElementRefID != rels[j].RefA.ElementRefID: + return rels[i].RefA.ElementRefID < rels[j].RefA.ElementRefID + case rels[i].RefB.ElementRefID != rels[j].RefB.ElementRefID: + return rels[i].RefB.ElementRefID < rels[j].RefB.ElementRefID + default: + return rels[i].Relationship < rels[j].Relationship + } + }) +} + +func sortFiles(files []*spdx.File) { + sort.Slice(files, func(i, j int) bool { + switch { + case files[i].FileName != files[j].FileName: + return files[i].FileName < files[j].FileName + default: + return files[i].FileSPDXIdentifier < files[j].FileSPDXIdentifier + } + }) +} + +// sortOtherLicenses removes duplicates and sorts result slice +func sortOtherLicenses(licenses []*spdx.OtherLicense) []*spdx.OtherLicense { + if len(licenses) == 0 { + return nil + } + licenses = lo.UniqBy(licenses, func(license *spdx.OtherLicense) string { + return license.LicenseIdentifier + }) + sort.Slice(licenses, func(i, j int) bool { + return licenses[i].LicenseIdentifier < licenses[j].LicenseIdentifier + }) + return licenses +} + +func elementID(elementType, pkgID string) spdx.ElementID { + return spdx.ElementID(fmt.Sprintf("%s-%s", elementType, pkgID)) +} + +func getDocumentNamespace(root *core.Component) string { + return fmt.Sprintf("%s/%s/%s-%s", + DocumentNamespace, + string(root.Type), + strings.ReplaceAll(strings.ReplaceAll(root.Name, "https://", ""), "http://", ""), // remove http(s):// prefix when scanning repos + uuid.New().String(), + ) +} + +func calcSPDXID(h Hash, v any) (string, error) { + f, err := h(v, hashstructure.FormatV2, &hashstructure.HashOptions{ + ZeroNil: true, + SlicesAsSets: true, + }) + if err != nil { + return "", xerrors.Errorf("could not build component ID for %+v: %w", v, err) + } + + return strconv.FormatUint(f, 16), nil +} + +func camelCase(inputUnderScoreStr string) (camelCase string) { + isToUpper := false + for k, v := range inputUnderScoreStr { + if k == 0 { + camelCase = strings.ToUpper(string(inputUnderScoreStr[0])) + } else { + if isToUpper { + camelCase += strings.ToUpper(string(v)) + isToUpper = false + } else { + if v == '_' { + isToUpper = true + } else { + camelCase += string(v) + } + } + } + } + return +} diff --git a/pkg/sbom/spdx/marshal_test.go b/pkg/sbom/spdx/marshal_test.go new file mode 100644 index 000000000000..baf638cb370b --- /dev/null +++ b/pkg/sbom/spdx/marshal_test.go @@ -0,0 +1,1635 @@ +package spdx_test + +import ( + "hash/fnv" + "sort" + "testing" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/mitchellh/hashstructure/v2" + "github.com/package-url/packageurl-go" + "github.com/spdx/tools-golang/spdx" + "github.com/spdx/tools-golang/spdx/v2/common" + "github.com/spdx/tools-golang/spdxlib" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/clock" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/sbom/core" + tspdx "github.com/aquasecurity/trivy/pkg/sbom/spdx" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" +) + +func annotation(t *testing.T, comment string) spdx.Annotation { + t.Helper() + + return spdx.Annotation{ + AnnotationDate: "2021-08-25T12:20:30Z", + AnnotationType: spdx.CategoryOther, + Annotator: spdx.Annotator{ + Annotator: "trivy-0.56.2", + AnnotatorType: tspdx.PackageAnnotatorToolField, + }, + AnnotationComment: comment, + } +} + +func TestMarshaler_Marshal(t *testing.T) { + testCases := []struct { + name string + inputReport types.Report + wantSBOM *spdx.Document + }{ + { + name: "happy path for container scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "rails:latest", + ArtifactType: ftypes.TypeContainerImage, + Metadata: types.Metadata{ + Size: 1024, + OS: &ftypes.OS{ + Family: ftypes.CentOS, + Name: "8.3.2011", + Eosl: true, + }, + ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + RepoTags: []string{"rails:latest"}, + DiffIDs: []string{"sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a"}, + RepoDigests: []string{"rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177"}, + ImageConfig: v1.ConfigFile{ + Architecture: "arm64", + Config: v1.Config{ + Labels: map[string]string{ + "vendor": "aquasecurity", + }, + }, + }, + }, + Results: types.Results{ + { + Target: "rails:latest (centos 8.3.2011)", + Class: types.ClassOSPkg, + Type: ftypes.CentOS, + Packages: []ftypes.Package{ + { + Name: "binutils", + Version: "2.30", + Release: "93.el8", + Epoch: 0, + Arch: "aarch64", + Identifier: ftypes.PkgIdentifier{ + UID: "F4C10A4371C93487", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "centos", + Name: "binutils", + Version: "2.30-93.el8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "aarch64", + }, + { + Key: "distro", + Value: "centos-8.3.2011", + }, + }, + }, + }, + SrcName: "binutils", + SrcVersion: "2.30", + SrcRelease: "93.el8", + SrcEpoch: 0, + Modularitylabel: "", + Licenses: []string{"GPLv3+"}, + Maintainer: "CentOS", + Digest: "md5:7459cec61bb4d1b0ca8107e25e0dd005", + }, + }, + }, + { + Target: "app/subproject/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + Name: "actionpack", + Version: "7.0.1", + Identifier: ftypes.PkgIdentifier{ + UID: "B1A9DE534F2737AF", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actionpack", + Version: "7.0.1", + }, + }, + }, + { + Name: "actioncontroller", + Version: "7.0.1", + Identifier: ftypes.PkgIdentifier{ + UID: "1628B51BD543965D", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actioncontroller", + Version: "7.0.1", + }, + }, + }, + }, + }, + { + Target: "app/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + Name: "actionpack", + Version: "7.0.1", + Identifier: ftypes.PkgIdentifier{ + UID: "92D6B6D3FF6F8FF5", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actionpack", + Version: "7.0.1", + }, + }, + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: "DOCUMENT", + DocumentName: "rails:latest", + DocumentNamespace: "http://trivy.dev/container_image/rails:latest-3ff14136-e09f-4df9-80ea-000000000009", + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: "aquasecurity", + CreatorType: "Organization", + }, + { + Creator: "trivy-0.56.2", + CreatorType: "Tool", + }, + }, + Created: "2021-08-25T12:20:30Z", + }, + Packages: []*spdx.Package{ + { + PackageSPDXIdentifier: spdx.ElementID("Application-9f48cdd13858abaf"), + PackageDownloadLocation: "NONE", + PackageName: "app/Gemfile.lock", + PrimaryPackagePurpose: tspdx.PackagePurposeApplication, + Annotations: []spdx.Annotation{ + annotation(t, "Class: lang-pkgs"), + annotation(t, "Type: bundler"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Application-692290f4b2235359"), + PackageDownloadLocation: "NONE", + PackageName: "app/subproject/Gemfile.lock", + PrimaryPackagePurpose: tspdx.PackagePurposeApplication, + Annotations: []spdx.Annotation{ + annotation(t, "Class: lang-pkgs"), + annotation(t, "Type: bundler"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("ContainerImage-9396d894cd0cb6cb"), + PackageDownloadLocation: "NONE", + PackageName: "rails:latest", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:oci/rails@sha256%3Aa27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177?arch=arm64&repository_url=index.docker.io%2Flibrary%2Frails", + }, + }, + Annotations: []spdx.Annotation{ + annotation(t, "DiffID: sha256:d871dadfb37b53ef1ca45be04fc527562b91989991a8f545345ae3be0b93f92a"), + annotation(t, "ImageID: sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6"), + annotation(t, "Labels:vendor: aquasecurity"), + annotation(t, "RepoDigest: rails@sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c473f432b177"), + annotation(t, "RepoTag: rails:latest"), + annotation(t, "SchemaVersion: 2"), + annotation(t, "Size: 1024"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeContainer, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-b8d4663e6d412e7"), + PackageDownloadLocation: "NONE", + PackageName: "actioncontroller", + PackageVersion: "7.0.1", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + Annotations: []spdx.Annotation{ + annotation(t, "PkgType: bundler"), + }, + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actioncontroller@7.0.1", + }, + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + PackageSourceInfo: "package found in: app/subproject/Gemfile.lock", + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-3b51e821f6796568"), + PackageDownloadLocation: "NONE", + PackageName: "actionpack", + PackageVersion: "7.0.1", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + Annotations: []spdx.Annotation{ + annotation(t, "PkgType: bundler"), + }, + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actionpack@7.0.1", + }, + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + PackageSourceInfo: "package found in: app/subproject/Gemfile.lock", + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-fb5630bc7d55a21c"), + PackageDownloadLocation: "NONE", + PackageName: "actionpack", + PackageVersion: "7.0.1", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + Annotations: []spdx.Annotation{ + annotation(t, "PkgType: bundler"), + }, + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actionpack@7.0.1", + }, + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + PackageSourceInfo: "package found in: app/Gemfile.lock", + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-5d43902b18ed2e2c"), + PackageDownloadLocation: "NONE", + PackageName: "binutils", + PackageVersion: "2.30-93.el8", + PackageLicenseConcluded: "GPL-3.0-or-later", + PackageLicenseDeclared: "GPL-3.0-or-later", + Annotations: []spdx.Annotation{ + annotation(t, "PkgType: centos"), + }, + PackageSupplier: &spdx.Supplier{ + SupplierType: tspdx.PackageSupplierOrganization, + Supplier: "CentOS", + }, + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:rpm/centos/binutils@2.30-93.el8?arch=aarch64&distro=centos-8.3.2011", + }, + }, + PackageSourceInfo: "built package from: binutils 2.30-93.el8", + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageChecksums: []common.Checksum{ + { + Algorithm: common.MD5, + Value: "7459cec61bb4d1b0ca8107e25e0dd005", + }, + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("OperatingSystem-20f7fa3049cc748c"), + PackageDownloadLocation: "NONE", + PackageName: "centos", + PackageVersion: "8.3.2011", + PrimaryPackagePurpose: tspdx.PackagePurposeOS, + Annotations: []spdx.Annotation{ + annotation(t, "Class: os-pkgs"), + annotation(t, "Type: centos"), + }, + }, + }, + Relationships: []*spdx.Relationship{ + { + RefA: spdx.DocElementID{ElementRefID: "Application-692290f4b2235359"}, + RefB: spdx.DocElementID{ElementRefID: "Package-3b51e821f6796568"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-692290f4b2235359"}, + RefB: spdx.DocElementID{ElementRefID: "Package-b8d4663e6d412e7"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-9f48cdd13858abaf"}, + RefB: spdx.DocElementID{ElementRefID: "Package-fb5630bc7d55a21c"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, + RefB: spdx.DocElementID{ElementRefID: "Application-692290f4b2235359"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, + RefB: spdx.DocElementID{ElementRefID: "Application-9f48cdd13858abaf"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, + RefB: spdx.DocElementID{ElementRefID: "OperatingSystem-20f7fa3049cc748c"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "ContainerImage-9396d894cd0cb6cb"}, + Relationship: "DESCRIBES", + }, + { + RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-20f7fa3049cc748c"}, + RefB: spdx.DocElementID{ElementRefID: "Package-5d43902b18ed2e2c"}, + Relationship: "CONTAINS", + }, + }, + OtherLicenses: nil, + Annotations: nil, + Reviews: nil, + }, + }, + { + name: "happy path for local container scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "centos:latest", + ArtifactType: ftypes.TypeContainerImage, + Metadata: types.Metadata{ + Size: 1024, + OS: &ftypes.OS{ + Family: ftypes.CentOS, + Name: "8.3.2011", + Eosl: true, + }, + ImageID: "sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6", + RepoTags: []string{"centos:latest"}, + RepoDigests: []string{}, + ImageConfig: v1.ConfigFile{ + Architecture: "arm64", + }, + }, + Results: types.Results{ + { + Target: "centos:latest (centos 8.3.2011)", + Class: types.ClassOSPkg, + Type: ftypes.CentOS, + Packages: []ftypes.Package{ + { + Name: "acl", + Version: "2.2.53", + Release: "1.el8", + Epoch: 1, + Arch: "aarch64", + Identifier: ftypes.PkgIdentifier{ + UID: "740219943F17B1DF", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeRPM, + Namespace: "centos", + Name: "acl", + Version: "2.2.53-1.el8", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "aarch64", + }, + { + Key: "distro", + Value: "centos-8.3.2011", + }, + { + Key: "epoch", + Value: "1", + }, + }, + }, + }, + SrcName: "acl", + SrcVersion: "2.2.53", + SrcRelease: "1.el8", + SrcEpoch: 1, + Modularitylabel: "", + Licenses: []string{"GPLv2+"}, + Digest: "md5:483792b8b5f9eb8be7dc4407733118d0", + }, + }, + }, + { + Target: "Ruby", + Class: types.ClassLangPkg, + Type: ftypes.GemSpec, + Packages: []ftypes.Package{ + { + Name: "actionpack", + Version: "7.0.1", + Identifier: ftypes.PkgIdentifier{ + UID: "E8DB2C6E35F8B990", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actionpack", + Version: "7.0.1", + }, + }, + Layer: ftypes.Layer{ + DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + FilePath: "tools/project-john/specifications/actionpack.gemspec", + Digest: "sha1:d2f9f9aed5161f6e4116a3f9573f41cd832f137c", + }, + { + Name: "actionpack", + Version: "7.0.1", + Identifier: ftypes.PkgIdentifier{ + UID: "B3E70B2159CFAC50", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actionpack", + Version: "7.0.1", + }, + }, + Layer: ftypes.Layer{ + DiffID: "sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488", + }, + FilePath: "tools/project-doe/specifications/actionpack.gemspec", + Digest: "sha1:413f98442c83808042b5d1d2611a346b999bdca5", + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: "DOCUMENT", + DocumentName: "centos:latest", + DocumentNamespace: "http://trivy.dev/container_image/centos:latest-3ff14136-e09f-4df9-80ea-000000000006", + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: "aquasecurity", + CreatorType: "Organization", + }, + { + Creator: "trivy-0.56.2", + CreatorType: "Tool", + }, + }, + Created: "2021-08-25T12:20:30Z", + }, + Packages: []*spdx.Package{ + { + PackageName: "centos:latest", + PackageSPDXIdentifier: "ContainerImage-413bfede37ad01fc", + PackageDownloadLocation: "NONE", + Annotations: []spdx.Annotation{ + annotation(t, "ImageID: sha256:5d0da3dc976460b72c77d94c8a1ad043720b0416bfc16c52c45d4847e53fadb6"), + annotation(t, "RepoTag: centos:latest"), + annotation(t, "SchemaVersion: 2"), + annotation(t, "Size: 1024"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeContainer, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-40c4059fe08523bf"), + PackageDownloadLocation: "NONE", + PackageName: "acl", + PackageVersion: "1:2.2.53-1.el8", + PackageLicenseConcluded: "GPL-2.0-or-later", + PackageLicenseDeclared: "GPL-2.0-or-later", + Annotations: []spdx.Annotation{ + annotation(t, "PkgType: centos"), + }, + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:rpm/centos/acl@2.2.53-1.el8?arch=aarch64&distro=centos-8.3.2011&epoch=1", + }, + }, + PackageSourceInfo: "built package from: acl 1:2.2.53-1.el8", + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + PackageChecksums: []common.Checksum{ + { + Algorithm: common.MD5, + Value: "483792b8b5f9eb8be7dc4407733118d0", + }, + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-69f68dd639314edd"), + PackageDownloadLocation: "NONE", + PackageName: "actionpack", + PackageVersion: "7.0.1", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actionpack@7.0.1", + }, + }, + Annotations: []spdx.Annotation{ + annotation(t, "LayerDiffID: sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488"), + annotation(t, "PkgType: gemspec"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + FilesAnalyzed: true, + PackageVerificationCode: &spdx.PackageVerificationCode{ + Value: "688d98e7e5660b879fd1fc548af8c0df3b7d785a", + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-da2cda24d2ecbfe6"), + PackageDownloadLocation: "NONE", + PackageName: "actionpack", + PackageVersion: "7.0.1", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actionpack@7.0.1", + }, + }, + Annotations: []spdx.Annotation{ + annotation(t, "LayerDiffID: sha256:ccb64cf0b7ba2e50741d0b64cae324eb5de3b1e2f580bbf177e721b67df38488"), + annotation(t, "PkgType: gemspec"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + FilesAnalyzed: true, + PackageVerificationCode: &spdx.PackageVerificationCode{ + Value: "c7526b18eaaeb410e82cb0da9288dd02b38ea171", + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("OperatingSystem-20f7fa3049cc748c"), + PackageDownloadLocation: "NONE", + PackageName: "centos", + PackageVersion: "8.3.2011", + PrimaryPackagePurpose: tspdx.PackagePurposeOS, + Annotations: []spdx.Annotation{ + annotation(t, "Class: os-pkgs"), + annotation(t, "Type: centos"), + }, + }, + }, + Files: []*spdx.File{ + { + FileSPDXIdentifier: "File-fa42187221d0d0a8", + FileName: "tools/project-doe/specifications/actionpack.gemspec", + Checksums: []spdx.Checksum{ + { + Algorithm: spdx.SHA1, + Value: "413f98442c83808042b5d1d2611a346b999bdca5", + }, + }, + }, + { + FileSPDXIdentifier: "File-6a540784b0dc6d55", + FileName: "tools/project-john/specifications/actionpack.gemspec", + Checksums: []spdx.Checksum{ + { + Algorithm: spdx.SHA1, + Value: "d2f9f9aed5161f6e4116a3f9573f41cd832f137c", + }, + }, + }, + }, + Relationships: []*spdx.Relationship{ + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"}, + RefB: spdx.DocElementID{ElementRefID: "OperatingSystem-20f7fa3049cc748c"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"}, + RefB: spdx.DocElementID{ElementRefID: "Package-69f68dd639314edd"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"}, + RefB: spdx.DocElementID{ElementRefID: "Package-da2cda24d2ecbfe6"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "ContainerImage-413bfede37ad01fc"}, + Relationship: "DESCRIBES", + }, + { + RefA: spdx.DocElementID{ElementRefID: "OperatingSystem-20f7fa3049cc748c"}, + RefB: spdx.DocElementID{ElementRefID: "Package-40c4059fe08523bf"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Package-69f68dd639314edd"}, + RefB: spdx.DocElementID{ElementRefID: "File-fa42187221d0d0a8"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Package-da2cda24d2ecbfe6"}, + RefB: spdx.DocElementID{ElementRefID: "File-6a540784b0dc6d55"}, + Relationship: "CONTAINS", + }, + }, + + OtherLicenses: nil, + Annotations: nil, + Reviews: nil, + }, + }, + { + name: "happy path for fs scan", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "masahiro331/CVE-2021-41098", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{ + { + Target: "Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + Name: "actioncable", + Version: "6.1.4.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGem, + Name: "actioncable", + Version: "6.1.4.1", + }, + }, + }, + }, + }, + { + Target: "pom.xml", + Class: types.ClassLangPkg, + Type: ftypes.Pom, + Packages: []ftypes.Package{ + { + ID: "com.example:example:1.0.0::abcdef1234567001", + Name: "com.example:example", + Version: "1.0.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.example", + Name: "example", + Version: "1.0.0", + }, + }, + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: "DOCUMENT", + DocumentName: "masahiro331/CVE-2021-41098", + DocumentNamespace: "http://trivy.dev/filesystem/masahiro331/CVE-2021-41098-3ff14136-e09f-4df9-80ea-000000000006", + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: "aquasecurity", + CreatorType: "Organization", + }, + { + Creator: "trivy-0.56.2", + CreatorType: "Tool", + }, + }, + Created: "2021-08-25T12:20:30Z", + }, + Packages: []*spdx.Package{ + { + PackageSPDXIdentifier: spdx.ElementID("Application-ed046c4a6b4da30f"), + PackageDownloadLocation: "NONE", + PackageName: "Gemfile.lock", + PrimaryPackagePurpose: tspdx.PackagePurposeApplication, + Annotations: []spdx.Annotation{ + annotation(t, "Class: lang-pkgs"), + annotation(t, "Type: bundler"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Application-800d9e6e0f88ab3a"), + PackageDownloadLocation: "NONE", + PackageName: "pom.xml", + PrimaryPackagePurpose: tspdx.PackagePurposeApplication, + Annotations: []spdx.Annotation{ + annotation(t, "Class: lang-pkgs"), + annotation(t, "Type: pom"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-e78eaf94802a53dc"), + PackageDownloadLocation: "NONE", + PackageName: "actioncable", + PackageVersion: "6.1.4.1", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:gem/actioncable@6.1.4.1", + }, + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + PackageSourceInfo: "package found in: Gemfile.lock", + Annotations: []spdx.Annotation{ + annotation(t, "PkgType: bundler"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-69cd7625c68537c7"), + PackageDownloadLocation: "NONE", + PackageName: "com.example:example", + PackageVersion: "1.0.0", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:maven/com.example/example@1.0.0", + }, + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + PackageSourceInfo: "package found in: pom.xml", + Annotations: []spdx.Annotation{ + annotation(t, "PkgID: com.example:example:1.0.0::abcdef1234567001"), + annotation(t, "PkgType: pom"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Filesystem-5af0f1f08c20909a"), + PackageDownloadLocation: "NONE", + PackageName: "masahiro331/CVE-2021-41098", + Annotations: []spdx.Annotation{ + annotation(t, "SchemaVersion: 2"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeSource, + }, + }, + Relationships: []*spdx.Relationship{ + { + RefA: spdx.DocElementID{ElementRefID: "Application-800d9e6e0f88ab3a"}, + RefB: spdx.DocElementID{ElementRefID: "Package-69cd7625c68537c7"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-ed046c4a6b4da30f"}, + RefB: spdx.DocElementID{ElementRefID: "Package-e78eaf94802a53dc"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Filesystem-5af0f1f08c20909a"}, + Relationship: "DESCRIBES", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Filesystem-5af0f1f08c20909a"}, + RefB: spdx.DocElementID{ElementRefID: "Application-800d9e6e0f88ab3a"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Filesystem-5af0f1f08c20909a"}, + RefB: spdx.DocElementID{ElementRefID: "Application-ed046c4a6b4da30f"}, + Relationship: "CONTAINS", + }, + }, + }, + }, + { + name: "happy path with various licenses", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "pom.xml", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{ + { + Target: "pom.xml", + Class: types.ClassLangPkg, + Type: ftypes.Pom, + Packages: []ftypes.Package{ + { + ID: "com.example:example:1.0.0::abcdef1234567001", + Name: "com.example:example", + Version: "1.0.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "com.example", + Name: "example", + Version: "1.0.0", + }, + }, + Licenses: []string{ + "text://BSD-4-clause", + "BSD-4-clause or LGPL-2.0-only", + "AFL 3.0 with wrong-exceptions", + "AFL 3.0 with Autoconf-exception-3.0", + "text://UNKNOWN", + "UNKNOWN", + }, + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: "DOCUMENT", + DocumentName: "pom.xml", + DocumentNamespace: "http://trivy.dev/filesystem/pom.xml-3ff14136-e09f-4df9-80ea-000000000004", + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: "aquasecurity", + CreatorType: "Organization", + }, + { + Creator: "trivy-0.56.2", + CreatorType: "Tool", + }, + }, + Created: "2021-08-25T12:20:30Z", + }, + Packages: []*spdx.Package{ + { + PackageSPDXIdentifier: spdx.ElementID("Application-800d9e6e0f88ab3a"), + PackageDownloadLocation: "NONE", + PackageName: "pom.xml", + PrimaryPackagePurpose: tspdx.PackagePurposeApplication, + Annotations: []spdx.Annotation{ + annotation(t, "Class: lang-pkgs"), + annotation(t, "Type: pom"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-69cd7625c68537c7"), + PackageDownloadLocation: "NONE", + PackageName: "com.example:example", + PackageVersion: "1.0.0", + PackageLicenseConcluded: "LicenseRef-14b1606fb243e2b6 AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-77bdf77d8292ce5b AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-229659393343e160 AND LicenseRef-a8d01765900624d3", + PackageLicenseDeclared: "LicenseRef-14b1606fb243e2b6 AND (BSD-4-Clause OR LGPL-2.0-only) AND LicenseRef-77bdf77d8292ce5b AND AFL-3.0 WITH Autoconf-exception-3.0 AND LicenseRef-229659393343e160 AND LicenseRef-a8d01765900624d3", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:maven/com.example/example@1.0.0", + }, + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + PackageSourceInfo: "package found in: pom.xml", + Annotations: []spdx.Annotation{ + annotation(t, "PkgID: com.example:example:1.0.0::abcdef1234567001"), + annotation(t, "PkgType: pom"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Filesystem-340a6f62df359d6a"), + PackageDownloadLocation: "NONE", + PackageName: "pom.xml", + Annotations: []spdx.Annotation{ + annotation(t, "SchemaVersion: 2"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeSource, + }, + }, + Relationships: []*spdx.Relationship{ + { + RefA: spdx.DocElementID{ElementRefID: "Application-800d9e6e0f88ab3a"}, + RefB: spdx.DocElementID{ElementRefID: "Package-69cd7625c68537c7"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Filesystem-340a6f62df359d6a"}, + Relationship: "DESCRIBES", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Filesystem-340a6f62df359d6a"}, + RefB: spdx.DocElementID{ElementRefID: "Application-800d9e6e0f88ab3a"}, + Relationship: "CONTAINS", + }, + }, + OtherLicenses: []*spdx.OtherLicense{ + { + LicenseIdentifier: "LicenseRef-14b1606fb243e2b6", + LicenseName: "NOASSERTION", + ExtractedText: "BSD-4-clause", + LicenseComment: "The license text represents text found in package metadata and may not represent the full text of the license", + }, + { + LicenseIdentifier: "LicenseRef-229659393343e160", + LicenseName: "NOASSERTION", + ExtractedText: "UNKNOWN", + LicenseComment: "The license text represents text found in package metadata and may not represent the full text of the license", + }, + { + LicenseIdentifier: "LicenseRef-77bdf77d8292ce5b", + LicenseName: "AFL-3.0 WITH wrong-exceptions", + ExtractedText: `This component is licensed under "AFL-3.0 WITH wrong-exceptions"`, + }, + { + LicenseIdentifier: "LicenseRef-a8d01765900624d3", + LicenseName: "UNKNOWN", + ExtractedText: `This component is licensed under "UNKNOWN"`, + }, + }, + }, + }, + { + name: "happy path with vulnerability", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "log4j-core-2.17.0.jar", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{ + { + Target: "Java", + Class: types.ClassLangPkg, + Type: ftypes.Jar, + Packages: []ftypes.Package{ + { + Name: "org.apache.logging.log4j:log4j-core", + Version: "2.17.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.apache.logging.log4j", + Name: "log4j-core", + Version: "2.17.0", + }, + }, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2021-44832", + PkgName: "org.apache.logging.log4j:log4j-core", + InstalledVersion: "2.17.0", + FixedVersion: "2.3.2, 2.12.4, 2.17.1", + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2021-44832", + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: "DOCUMENT", + DocumentName: "log4j-core-2.17.0.jar", + DocumentNamespace: "http://trivy.dev/filesystem/log4j-core-2.17.0.jar-3ff14136-e09f-4df9-80ea-000000000003", + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: "aquasecurity", + CreatorType: "Organization", + }, + { + Creator: "trivy-0.56.2", + CreatorType: "Tool", + }, + }, + Created: "2021-08-25T12:20:30Z", + }, + Packages: []*spdx.Package{ + { + PackageSPDXIdentifier: spdx.ElementID("Package-4ee6f197f4811213"), + PackageDownloadLocation: "NONE", + PackageName: "org.apache.logging.log4j:log4j-core", + PackageVersion: "2.17.0", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:maven/org.apache.logging.log4j/log4j-core@2.17.0", + }, + { + Category: "SECURITY", + RefType: "advisory", + Locator: "https://avd.aquasec.com/nvd/cve-2021-44832", + }, + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + Annotations: []spdx.Annotation{ + annotation(t, "PkgType: jar"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Filesystem-121e7e7a43f02ab"), + PackageDownloadLocation: "NONE", + PackageName: "log4j-core-2.17.0.jar", + Annotations: []spdx.Annotation{ + annotation(t, "SchemaVersion: 2"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeSource, + }, + }, + Relationships: []*spdx.Relationship{ + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Filesystem-121e7e7a43f02ab"}, + Relationship: "DESCRIBES", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Filesystem-121e7e7a43f02ab"}, + RefB: spdx.DocElementID{ElementRefID: "Package-4ee6f197f4811213"}, + Relationship: "CONTAINS", + }, + }, + }, + }, + { + name: "happy path aggregate results", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "http://test-aggregate", + ArtifactType: ftypes.TypeRepository, + Results: types.Results{ + { + Target: "Node.js", + Class: types.ClassLangPkg, + Type: ftypes.NodePkg, + Packages: []ftypes.Package{ + { + Name: "ruby-typeprof", + Version: "0.20.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "ruby-typeprof", + Version: "0.20.1", + }, + }, + Licenses: []string{"MIT"}, + Layer: ftypes.Layer{ + DiffID: "sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e", + }, + Digest: "sha256:a5efa82f08774597165e8c1a102d45d0406913b74c184883ac91f409ae26009d", + FilePath: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json", + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: "DOCUMENT", + DocumentName: "http://test-aggregate", + DocumentNamespace: "http://trivy.dev/repository/test-aggregate-3ff14136-e09f-4df9-80ea-000000000003", + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: "aquasecurity", + CreatorType: "Organization", + }, + { + Creator: "trivy-0.56.2", + CreatorType: "Tool", + }, + }, + Created: "2021-08-25T12:20:30Z", + }, + Packages: []*spdx.Package{ + { + PackageSPDXIdentifier: spdx.ElementID("Package-52b8e939bac2d133"), + PackageDownloadLocation: "git+http://test-aggregate", + PackageName: "ruby-typeprof", + PackageVersion: "0.20.1", + PackageLicenseConcluded: "MIT", + PackageLicenseDeclared: "MIT", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:npm/ruby-typeprof@0.20.1", + }, + }, + Annotations: []spdx.Annotation{ + annotation(t, "LayerDiffID: sha256:661c3fd3cc16b34c070f3620ca6b03b6adac150f9a7e5d0e3c707a159990f88e"), + annotation(t, "PkgType: node-pkg"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + FilesAnalyzed: true, + PackageVerificationCode: &spdx.PackageVerificationCode{ + Value: "da39a3ee5e6b4b0d3255bfef95601890afd80709", + }, + }, + { + PackageSPDXIdentifier: "Repository-1a78857c1a6a759e", + PackageName: "http://test-aggregate", + PackageDownloadLocation: "git+http://test-aggregate", + Annotations: []spdx.Annotation{ + annotation(t, "SchemaVersion: 2"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeSource, + }, + }, + Files: []*spdx.File{ + { + FileName: "usr/local/lib/ruby/gems/3.1.0/gems/typeprof-0.21.1/vscode/package.json", + FileSPDXIdentifier: "File-a52825a3e5bc6dfe", + Checksums: []common.Checksum{ + { + Algorithm: common.SHA256, + Value: "a5efa82f08774597165e8c1a102d45d0406913b74c184883ac91f409ae26009d", + }, + }, + }, + }, + Relationships: []*spdx.Relationship{ + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Repository-1a78857c1a6a759e"}, + Relationship: "DESCRIBES", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Package-52b8e939bac2d133"}, + RefB: spdx.DocElementID{ElementRefID: "File-a52825a3e5bc6dfe"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Repository-1a78857c1a6a759e"}, + RefB: spdx.DocElementID{ElementRefID: "Package-52b8e939bac2d133"}, + Relationship: "CONTAINS", + }, + }, + }, + }, + { + name: "happy path empty", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "empty/path", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{}, + }, + wantSBOM: &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: "DOCUMENT", + DocumentName: "empty/path", + DocumentNamespace: "http://trivy.dev/filesystem/empty/path-3ff14136-e09f-4df9-80ea-000000000002", + + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: "aquasecurity", + CreatorType: "Organization", + }, + { + Creator: "trivy-0.56.2", + CreatorType: "Tool", + }, + }, + Created: "2021-08-25T12:20:30Z", + }, + Packages: []*spdx.Package{ + { + PackageName: "empty/path", + PackageSPDXIdentifier: "Filesystem-70f34983067dba86", + PackageDownloadLocation: "NONE", + Annotations: []spdx.Annotation{ + annotation(t, "SchemaVersion: 2"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeSource, + }, + }, + Relationships: []*spdx.Relationship{ + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Filesystem-70f34983067dba86"}, + Relationship: "DESCRIBES", + }, + }, + }, + }, + { + name: "happy path secret", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "secret", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{ + { + Target: "key.pem", + Class: types.ClassSecret, + Secrets: []types.DetectedSecret{ + { + RuleID: "private-key", + Category: "AsymmetricPrivateKey", + Severity: "HIGH", + Title: "Asymmetric Private Key", + StartLine: 1, + EndLine: 1, + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: "DOCUMENT", + DocumentName: "secret", + DocumentNamespace: "http://trivy.dev/filesystem/secret-3ff14136-e09f-4df9-80ea-000000000002", + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: "aquasecurity", + CreatorType: "Organization", + }, + { + Creator: "trivy-0.56.2", + CreatorType: "Tool", + }, + }, + Created: "2021-08-25T12:20:30Z", + }, + Packages: []*spdx.Package{ + { + PackageName: "secret", + PackageSPDXIdentifier: "Filesystem-5c08d34162a2c5d3", + PackageDownloadLocation: "NONE", + Annotations: []spdx.Annotation{ + annotation(t, "SchemaVersion: 2"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeSource, + }, + }, + Relationships: []*spdx.Relationship{ + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Filesystem-5c08d34162a2c5d3"}, + Relationship: "DESCRIBES", + }, + }, + }, + }, + { + name: "go library local", + inputReport: types.Report{ + SchemaVersion: report.SchemaVersion, + ArtifactName: "go-artifact", + ArtifactType: ftypes.TypeFilesystem, + Results: types.Results{ + { + Target: "/usr/local/bin/test", + Class: types.ClassLangPkg, + Type: ftypes.GoBinary, + Packages: []ftypes.Package{ + { + Name: "./private_repos/cnrm.googlesource.com/cnrm/", + Version: "", + }, + { + Name: "golang.org/x/crypto", + Version: "v0.0.1", + Identifier: ftypes.PkgIdentifier{ + UID: "161541A259EF014B", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "golang.org/x", + Name: "crypto", + Version: "v0.0.1", + }, + }, + }, + }, + }, + }, + }, + wantSBOM: &spdx.Document{ + SPDXVersion: spdx.Version, + DataLicense: spdx.DataLicense, + SPDXIdentifier: "DOCUMENT", + DocumentName: "go-artifact", + DocumentNamespace: "http://trivy.dev/filesystem/go-artifact-3ff14136-e09f-4df9-80ea-000000000005", + CreationInfo: &spdx.CreationInfo{ + Creators: []common.Creator{ + { + Creator: "aquasecurity", + CreatorType: "Organization", + }, + { + Creator: "trivy-0.56.2", + CreatorType: "Tool", + }, + }, + Created: "2021-08-25T12:20:30Z", + }, + Packages: []*spdx.Package{ + { + PackageSPDXIdentifier: spdx.ElementID("Application-aab0f4e8cf174c67"), + PackageDownloadLocation: "NONE", + PackageName: "/usr/local/bin/test", + PrimaryPackagePurpose: tspdx.PackagePurposeApplication, + Annotations: []spdx.Annotation{ + annotation(t, "Class: lang-pkgs"), + annotation(t, "Type: gobinary"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-b1c3b9e2363f5ff7"), + PackageDownloadLocation: "NONE", + PackageName: "./private_repos/cnrm.googlesource.com/cnrm/", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + PackageSourceInfo: "package found in: /usr/local/bin/test", + Annotations: []spdx.Annotation{ + annotation(t, "PkgType: gobinary"), + }, + }, + { + PackageSPDXIdentifier: spdx.ElementID("Package-b9b7ae633941e083"), + PackageDownloadLocation: "NONE", + PackageName: "golang.org/x/crypto", + PackageVersion: "v0.0.1", + PackageLicenseConcluded: "NOASSERTION", + PackageLicenseDeclared: "NOASSERTION", + PackageExternalReferences: []*spdx.PackageExternalReference{ + { + Category: tspdx.CategoryPackageManager, + RefType: tspdx.RefTypePurl, + Locator: "pkg:golang/golang.org/x/crypto@v0.0.1", + }, + }, + PrimaryPackagePurpose: tspdx.PackagePurposeLibrary, + PackageSupplier: &spdx.Supplier{Supplier: tspdx.PackageSupplierNoAssertion}, + PackageSourceInfo: "package found in: /usr/local/bin/test", + Annotations: []spdx.Annotation{ + annotation(t, "PkgType: gobinary"), + }, + }, + { + PackageName: "go-artifact", + PackageSPDXIdentifier: "Filesystem-e340f27468b382be", + PackageDownloadLocation: "NONE", + Annotations: []spdx.Annotation{ + annotation(t, "SchemaVersion: 2"), + }, + PrimaryPackagePurpose: tspdx.PackagePurposeSource, + }, + }, + Relationships: []*spdx.Relationship{ + { + RefA: spdx.DocElementID{ElementRefID: "Application-aab0f4e8cf174c67"}, + RefB: spdx.DocElementID{ElementRefID: "Package-b1c3b9e2363f5ff7"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Application-aab0f4e8cf174c67"}, + RefB: spdx.DocElementID{ElementRefID: "Package-b9b7ae633941e083"}, + Relationship: "CONTAINS", + }, + { + RefA: spdx.DocElementID{ElementRefID: "DOCUMENT"}, + RefB: spdx.DocElementID{ElementRefID: "Filesystem-e340f27468b382be"}, + Relationship: "DESCRIBES", + }, + { + RefA: spdx.DocElementID{ElementRefID: "Filesystem-e340f27468b382be"}, + RefB: spdx.DocElementID{ElementRefID: "Application-aab0f4e8cf174c67"}, + Relationship: "CONTAINS", + }, + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Fake function calculating the hash value + h := fnv.New64() + hasher := func(v any, _ hashstructure.Format, _ *hashstructure.HashOptions) (uint64, error) { + h.Reset() + + var str string + switch vv := v.(type) { + case *core.Component: + str = vv.Name + vv.Version + vv.SrcFile + for _, f := range vv.Files { + str += f.Path + } + case spdx.OtherLicense: + str = vv.ExtractedText + vv.LicenseName + case string: + str = vv + default: + require.Failf(t, "unknown type", "%T", v) + } + + if _, err := h.Write([]byte(str)); err != nil { + return 0, err + } + + return h.Sum64(), nil + } + + ctx := clock.With(t.Context(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + + marshaler := tspdx.NewMarshaler("0.56.2", tspdx.WithHasher(hasher)) + spdxDoc, err := marshaler.MarshalReport(ctx, tc.inputReport) + require.NoError(t, err) + + assert.NoError(t, spdxlib.ValidateDocument(spdxDoc)) + assert.Equal(t, tc.wantSBOM, spdxDoc) + }) + } +} + +func TestMarshaler_normalizeLicenses(t *testing.T) { + tests := []struct { + name string + input []string + wantLicenseName string + wantOtherLicenses []*spdx.OtherLicense + }{ + { + name: "happy path", + input: []string{ + "GPLv2+", + }, + wantLicenseName: "GPL-2.0-or-later", + }, + { + name: "happy path with multi license", + input: []string{ + "GPLv2+", + "GPLv3+", + "BSD-4-Clause", + }, + wantLicenseName: "GPL-2.0-or-later AND GPL-3.0-or-later AND BSD-4-Clause", + }, + { + name: "happy path with OR operator", + input: []string{ + "GPLv2+", + "LGPL 2.0 or GNU LESSER", + }, + wantLicenseName: "GPL-2.0-or-later AND (LGPL-2.0-only OR LGPL-2.1-only)", + }, + { + name: "happy path with OR operator with non-SPDX license", + input: []string{ + "GPLv2+", + "wrong-license or unknown-license", + }, + wantLicenseName: "GPL-2.0-or-later AND (LicenseRef-c581e42fe705aa48 OR LicenseRef-a0bb0951a6dfbdbe)", + wantOtherLicenses: []*spdx.OtherLicense{ + { + LicenseIdentifier: "LicenseRef-a0bb0951a6dfbdbe", + LicenseName: "unknown-license", + ExtractedText: `This component is licensed under "unknown-license"`, + }, + { + LicenseIdentifier: "LicenseRef-c581e42fe705aa48", + LicenseName: "wrong-license", + ExtractedText: `This component is licensed under "wrong-license"`, + }, + }, + }, + { + name: "happy path with AND operator", + input: []string{ + "GPLv2+", + "LGPL 2.0 and GNU LESSER", + }, + wantLicenseName: "GPL-2.0-or-later AND LGPL-2.0-only AND LGPL-2.1-only", + }, + { + name: "happy path with WITH operator", + input: []string{ + "GPLv2", + "AFL 3.0 with wrong-exceptions", + "LGPL 2.0 and unknown-license and GNU LESSER", + "AFL 3.0 with Autoconf-exception-3.0", + }, + wantLicenseName: "GPL-2.0-only AND LicenseRef-51373b28fab165e9 AND LGPL-2.0-only AND LicenseRef-a0bb0951a6dfbdbe AND LGPL-2.1-only AND AFL-3.0 WITH Autoconf-exception-3.0", + wantOtherLicenses: []*spdx.OtherLicense{ + { + LicenseIdentifier: "LicenseRef-51373b28fab165e9", + LicenseName: "AFL-3.0 WITH wrong-exceptions", + ExtractedText: `This component is licensed under "AFL-3.0 WITH wrong-exceptions"`, + }, + { + LicenseIdentifier: "LicenseRef-a0bb0951a6dfbdbe", + LicenseName: "unknown-license", + ExtractedText: `This component is licensed under "unknown-license"`, + }, + }, + }, + { + name: "happy path with non-SPDX exception", + input: []string{ + "AFL 2.0", + "AFL 3.0 with wrong-exceptions", + }, + wantLicenseName: "AFL-2.0 AND LicenseRef-51373b28fab165e9", + wantOtherLicenses: []*spdx.OtherLicense{ + { + LicenseIdentifier: "LicenseRef-51373b28fab165e9", + LicenseName: "AFL-3.0 WITH wrong-exceptions", + ExtractedText: `This component is licensed under "AFL-3.0 WITH wrong-exceptions"`, + }, + }, + }, + { + name: "happy path with incorrect cases for license and exception", + input: []string{ + "afl 3.0 with autoCONF-exception-3.0", + }, + wantLicenseName: "AFL-3.0 WITH Autoconf-exception-3.0", + }, + { + name: "happy path with text of license", + input: []string{ + "text://Redistribution and use in source and binary forms, with or without", + "AFL 2.0", + "unknown-license", + }, + wantLicenseName: "LicenseRef-b5b4cc09bc5f0e16 AND AFL-2.0 AND LicenseRef-a0bb0951a6dfbdbe", + wantOtherLicenses: []*spdx.OtherLicense{ + { + LicenseIdentifier: "LicenseRef-a0bb0951a6dfbdbe", + LicenseName: "unknown-license", + ExtractedText: `This component is licensed under "unknown-license"`, + }, + { + LicenseIdentifier: "LicenseRef-b5b4cc09bc5f0e16", + LicenseName: "NOASSERTION", + ExtractedText: "Redistribution and use in source and binary forms, with or without", + LicenseComment: "The license text represents text found in package metadata and may not represent the full text of the license", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := tspdx.NewMarshaler("") + gotLicenseName, gotOtherLicenses := m.NormalizeLicenses(tt.input) + // We will sort all OtherLicenses for SPDX document + // So we need to sort OtherLicenses for this test + sort.Slice(gotOtherLicenses, func(i, j int) bool { + return gotOtherLicenses[i].LicenseIdentifier < gotOtherLicenses[j].LicenseIdentifier + }) + assert.Equal(t, tt.wantLicenseName, gotLicenseName) + assert.Equal(t, tt.wantOtherLicenses, gotOtherLicenses) + }) + } +} diff --git a/pkg/sbom/spdx/testdata/happy/bom.json b/pkg/sbom/spdx/testdata/happy/bom.json new file mode 100644 index 000000000000..604c3b1f0468 --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/bom.json @@ -0,0 +1,290 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:02:46.826609Z", + "creators": [ + "Tool: trivy-dev", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://trivy.dev/container/meven-test-project-eb7a0384-b04a-4fc6-8afb-1662fe59ca79", + "name": "maven-test-projecct", + "packages": [ + { + "SPDXID": "SPDXRef-Application-150e605f5f17224d", + "filesAnalyzed": false, + "name": "jar", + "sourceInfo": "Java" + }, + { + "SPDXID": "SPDXRef-Application-24f8a80152e2c0fc", + "filesAnalyzed": false, + "name": "node-pkg", + "sourceInfo": "Node.js" + }, + { + "SPDXID": "SPDXRef-Application-36324ee492e03f0a", + "filesAnalyzed": false, + "name": "gobinary", + "sourceInfo": "app/gobinary/gobinary" + }, + { + "SPDXID": "SPDXRef-Application-4af197c15114fb0e", + "filesAnalyzed": false, + "name": "composer", + "sourceInfo": "app/composer/composer.lock" + }, + { + "SPDXID": "SPDXRef-ContainerImage-b5d81cde5f95c8fc", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "SchemaVersion: 2" + }, + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "ImageID: sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5" + }, + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "DiffID: sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + }, + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "DiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + }, + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "RepoTag: maven-test-project:latest" + }, + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "RepoTag: tmp-test:latest" + } + ], + "filesAnalyzed": false, + "name": "meven-test-project" + }, + { + "SPDXID": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "filesAnalyzed": false, + "name": "alpine", + "versionInfo": "3.16.0" + }, + { + "SPDXID": "SPDXRef-Package-2906575950df652b", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:composer/pear/log@1.13.1", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "pear/log", + "versionInfo": "1.13.1" + }, + { + "SPDXID": "SPDXRef-Package-2a53baa495b9ddaf", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:maven/org.codehaus.mojo/child-project@1.0", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "org.codehaus.mojo:child-project", + "versionInfo": "1.0" + }, + { + "SPDXID": "SPDXRef-Package-5e2e255ac76747ef", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:composer/pear/pear_exception@v1.0.0", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "pear/pear_exception", + "versionInfo": "v1.0.0" + }, + { + "SPDXID": "SPDXRef-Package-5f1dbaff8de5eb06", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:npm/bootstrap@5.0.2", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "name": "bootstrap", + "versionInfo": "5.0.2" + }, + { + "SPDXID": "SPDXRef-Package-84ebffe38343d949", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1" + } + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:golang/github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "github.com/package-url/packageurl-go", + "versionInfo": "v0.1.1-0.20220203205134-d70459300c8a" + }, + { + "SPDXID": "SPDXRef-Package-b7ebaf0233f1ef7b", + "annotations": [ + { + "annotator": "Tool: trivy-dev", + "annotationDate": "2024-10-29T06:50:38Z", + "annotationType": "OTHER", + "comment": "LayerDiffID: sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3" + } + ], + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:apk/alpine/musl@1.2.3-r0?distro=3.16.0", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "MIT", + "licenseDeclared": "MIT", + "name": "musl", + "sourceInfo": "built package from: musl 1.2.3-r0", + "versionInfo": "1.2.3-r0" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-b5d81cde5f95c8fc", + "relationshipType": "DESCRIBES", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-b7ebaf0233f1ef7b", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-OperatingSystem-bd17bf9010aa612c" + }, + { + "relatedSpdxElement": "SPDXRef-Application-150e605f5f17224d", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-2a53baa495b9ddaf", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-150e605f5f17224d" + }, + { + "relatedSpdxElement": "SPDXRef-Application-24f8a80152e2c0fc", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-5f1dbaff8de5eb06", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-24f8a80152e2c0fc" + }, + { + "relatedSpdxElement": "SPDXRef-Application-4af197c15114fb0e", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-2906575950df652b", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-4af197c15114fb0e" + }, + { + "relatedSpdxElement": "SPDXRef-Package-5e2e255ac76747ef", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-4af197c15114fb0e" + }, + { + "relatedSpdxElement": "SPDXRef-Application-36324ee492e03f0a", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-84ebffe38343d949", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-36324ee492e03f0a" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/empty-bom.json b/pkg/sbom/spdx/testdata/happy/empty-bom.json new file mode 100644 index 000000000000..07020a41fb74 --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/empty-bom.json @@ -0,0 +1,34 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:03:35.840861Z", + "creators": [ + "Tool: trivy-dev", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentDescribes": [ + "SPDXRef-ContainerImage-7f428bd075b13fe8" + ], + "documentNamespace": "http://trivy.dev/container/maven-test-project-3e87878b-eac3-4baa-af11-bdf2c5eab8ea", + "name": "maven-test-project", + "packages": [ + { + "SPDXID": "SPDXRef-ContainerImage-7f428bd075b13fe8", + "attributionTexts": [ + "SchemaVersion: 2" + ], + "filesAnalyzed": false, + "name": "maven-test-project" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-7f428bd075b13fe8", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/no-relationship.json b/pkg/sbom/spdx/testdata/happy/no-relationship.json new file mode 100644 index 000000000000..16495c54e1df --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/no-relationship.json @@ -0,0 +1,97 @@ +{ + "SPDXID": "SPDXRef-elasticsearch", + "spdxVersion": "SPDX-2.3", + "creationInfo": { + "created": "2023-05-17T15:59:30.511Z", + "creators": [ + "Organization: VMware, Inc." + ] + }, + "name": "SPDX document for Elasticsearch 8.7.1", + "dataLicense": "CC0-1.0", + "documentDescribes": [ + "SPDXRef-elasticsearch" + ], + "documentNamespace": "elasticsearch-8.7.1", + "packages": [ + { + "SPDXID": "SPDXRef-elasticsearch", + "name": "Elasticsearch", + "versionInfo": "8.7.1", + "downloadLocation": "https://github.com/elastic/elasticsearch/archive/v8.7.1.tar.gz", + "licenseConcluded": "Elastic-2.0", + "licenseDeclared": "Elastic-2.0", + "filesAnalyzed": false, + "externalRefs": [ + { + "referenceCategory": "SECURITY", + "referenceType": "cpe23Type", + "referenceLocator": "cpe:2.3:*:elasticsearch:elasticsearch:8.7.1:*:*:*:*:*:*:*" + } + ] + }, + { + "name": "co.elastic.apm:apm-agent", + "SPDXID": "SPDXRef-Package-d6465ccdd5385c16", + "versionInfo": "1.36.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "copyrightText": "", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:maven/co.elastic.apm/apm-agent@1.36.0" + } + ], + "primaryPackagePurpose": "LIBRARY", + "files": [ + { + "fileName":"modules/apm/elastic-apm-agent-1.36.0.jar", + "SPDXID": "SPDXRef-File-4d457bf4ff3526ea", + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "d2a9ad9b159eb650d25add9395c4f4198f200066" + } + ], + "copyrightText": "" + } + ] + }, + { + "name": "co.elastic.apm:apm-agent-cached-lookup-key", + "SPDXID": "SPDXRef-Package-8e3a2cf58d7bd790", + "versionInfo": "1.36.0", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "copyrightText": "", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:maven/co.elastic.apm/apm-agent-cached-lookup-key@1.36.0" + } + ], + "primaryPackagePurpose": "LIBRARY", + "files": [ + { + "fileName": "modules/apm/elastic-apm-agent-1.36.0.jar", + "SPDXID": "SPDXRef-File-4d457bf4ff3526ea", + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "d2a9ad9b159eb650d25add9395c4f4198f200066" + } + ], + "copyrightText": "" + } + ] + } + ], + "files": [] +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/os-only-bom.json b/pkg/sbom/spdx/testdata/happy/os-only-bom.json new file mode 100644 index 000000000000..148a5f71ddf1 --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/os-only-bom.json @@ -0,0 +1,42 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:04:09.262672Z", + "creators": [ + "Tool: trivy-dev", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://trivy.dev/container/maven-test-project-1a255568-e896-498f-93de-7975a5cb0212", + "name": "maven-test-project", + "packages": [ + { + "SPDXID": "SPDXRef-ContainerImage-fbbee9b988766322", + "attributionTexts": [ + "SchemaVersion: 2" + ], + "filesAnalyzed": false, + "name": "maven-test-project" + }, + { + "SPDXID": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "filesAnalyzed": false, + "name": "alpine", + "versionInfo": "3.16.0" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-fbbee9b988766322", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-fbbee9b988766322" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/unrelated-bom.json b/pkg/sbom/spdx/testdata/happy/unrelated-bom.json new file mode 100644 index 000000000000..35d63541d847 --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/unrelated-bom.json @@ -0,0 +1,82 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:04:28.43059Z", + "creators": [ + "Tool: trivy-dev", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://trivy.dev/application/maven-test-project-8aa3b6db-bae7-4755-b43e-00b4bcf46410", + "name": "maven-test-project", + "packages": [ + { + "SPDXID": "SPDXRef-Application-193be12e25033404", + "filesAnalyzed": false, + "name": "composer", + "sourceInfo": "app/composer/composer.lock" + }, + { + "SPDXID": "SPDXRef-Filesystem-12d960c003a8275b", + "attributionTexts": [ + "SchemaVersion: 2" + ], + "filesAnalyzed": false, + "name": "maven-test-project" + }, + { + "SPDXID": "SPDXRef-Package-2906575950df652b", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:composer/pear/log@1.13.1", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "pear/log", + "versionInfo": "1.13.1" + }, + { + "SPDXID": "SPDXRef-Package-5e2e255ac76747ef", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:composer/pear/pear_exception@v1.0.0", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "licenseConcluded": "NONE", + "licenseDeclared": "NONE", + "name": "pear/pear_exception", + "versionInfo": "v1.0.0" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-Filesystem-12d960c003a8275b", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-Application-193be12e25033404", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-Filesystem-12d960c003a8275b" + }, + { + "relatedSpdxElement": "SPDXRef-Package-2906575950df652b", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-193be12e25033404" + }, + { + "relatedSpdxElement": "SPDXRef-Package-5e2e255ac76747ef", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-Application-193be12e25033404" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/with-file-as-relationship-parent.json b/pkg/sbom/spdx/testdata/happy/with-file-as-relationship-parent.json new file mode 100644 index 000000000000..798e75d0a52a --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/with-file-as-relationship-parent.json @@ -0,0 +1,54 @@ +{ + "files": [ + { + "fileName": "./Modules/Microsoft.PowerShell.PSResourceGet/_manifest/spdx_2.2/manifest.spdx.json", + "SPDXID": "SPDXRef-File--Modules-Microsoft.PowerShell.PSResourceGet--manifest-spdx-2.2-manifest.spdx.json-2B9FB98F5CA97DC84FD382A8F8E68F663C003362", + "checksums": [ + { + "algorithm": "SHA256", + "checksumValue": "4201b0989938842ef8c11a006184e0b1466bd7f9bb2af61d89a4c8318d43466e" + }, + { + "algorithm": "SHA1", + "checksumValue": "2b9fb98f5ca97dc84fd382a8f8e68f663c003362" + } + ], + "licenseConcluded": "NOASSERTION", + "licenseInfoInFiles": [ + "NOASSERTION" + ], + "copyrightText": "NOASSERTION", + "fileTypes": [ + "SPDX" + ] + } + ], + "externalDocumentRefs": [], + "relationships": [ + { + "relationshipType": "DESCRIBES", + "relatedSpdxElement": "SPDXRef-RootPackage", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relationshipType": "DESCRIBED_BY", + "relatedSpdxElement": "SPDXRef-DOCUMENT", + "spdxElementId": "SPDXRef-File--Modules-Microsoft.PowerShell.PSResourceGet--manifest-spdx-2.2-manifest.spdx.json-2B9FB98F5CA97DC84FD382A8F8E68F663C003362" + } + ], + "spdxVersion": "SPDX-2.2", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "PowerShell Linux Arm32 7.5.0-preview.2", + "documentNamespace": "https://sbom.microsoft/1:2QSF7qZlbE-F7QrUJlEo7g:pHp_nUFvDUijZ4LrJ4RhoQ/696:458654/PowerShell%20Linux%20Arm32:7.5.0-preview.2:pDkyTHXmgUOdzSXIq9CiqA", + "creationInfo": { + "created": "2024-02-22T00:43:53Z", + "creators": [ + "Organization: Microsoft", + "Tool: Microsoft.SBOMTool-2.2.3" + ] + }, + "documentDescribes": [ + "SPDXRef-RootPackage" + ] +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/with-files-in-relationships-bom.json b/pkg/sbom/spdx/testdata/happy/with-files-in-relationships-bom.json new file mode 100644 index 000000000000..596f3d10b289 --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/with-files-in-relationships-bom.json @@ -0,0 +1,91 @@ +{ + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": "app", + "documentNamespace": "http://trivy.dev/filesystem/app-8e571278-2221-4dcd-bc56-0b256210fa91", + "creationInfo": { + "licenseListVersion": "", + "creators": [ + "Organization: aquasecurity", + "Tool: trivy-dev" + ], + "created": "2023-05-31T05:58:45Z" + }, + "packages": [ + { + "name": "app", + "SPDXID": "SPDXRef-Filesystem-13b142ca391a006e", + "downloadLocation": "NONE", + "copyrightText": "", + "attributionTexts": [ + "SchemaVersion: 2" + ], + "primaryPackagePurpose": "SOURCE" + }, + { + "name": "node-pkg", + "SPDXID": "SPDXRef-Application-24f8a80152e2c0fc", + "downloadLocation": "NONE", + "sourceInfo": "Node.js", + "copyrightText": "", + "primaryPackagePurpose": "APPLICATION" + }, + { + "name": "yargs-parser", + "SPDXID": "SPDXRef-Package-c3508825bf3861d8", + "versionInfo": "21.1.1", + "supplier": "NOASSERTION", + "downloadLocation": "NONE", + "licenseConcluded": "ISC", + "licenseDeclared": "ISC", + "copyrightText": "", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": "pkg:npm/yargs-parser@21.1.1" + } + ], + "attributionTexts": [ + "PkgID: yargs-parser@21.1.1" + ], + "primaryPackagePurpose": "LIBRARY" + } + ], + "files": [ + { + "fileName": "node_modules/yargs-parser/package.json", + "SPDXID": "SPDXRef-File-51bb5f929ef68877", + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "69e70ec702f9df4ff64024b5fdea4644f1ce6c97" + } + ], + "copyrightText": "" + } + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relatedSpdxElement": "SPDXRef-Filesystem-13b142ca391a006e", + "relationshipType": "DESCRIBES" + }, + { + "spdxElementId": "SPDXRef-Filesystem-13b142ca391a006e", + "relatedSpdxElement": "SPDXRef-Application-24f8a80152e2c0fc", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Application-24f8a80152e2c0fc", + "relatedSpdxElement": "SPDXRef-Package-c3508825bf3861d8", + "relationshipType": "CONTAINS" + }, + { + "spdxElementId": "SPDXRef-Package-c3508825bf3861d8", + "relatedSpdxElement": "SPDXRef-File-51bb5f929ef68877", + "relationshipType": "CONTAINS" + } + ] +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/happy/with-hasfiles-bom.json b/pkg/sbom/spdx/testdata/happy/with-hasfiles-bom.json new file mode 100644 index 000000000000..5bc6c03e4ea8 --- /dev/null +++ b/pkg/sbom/spdx/testdata/happy/with-hasfiles-bom.json @@ -0,0 +1,86 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2023-05-31T04:56:43Z", + "creators": [ + "Tool: trivy-dev", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentDescribes": [ + "SPDXRef-Filesystem-13b142ca391a006e" + ], + "documentNamespace": "http://trivy.dev/filesystem/app-ab9e166e-7229-4d03-947e-8ffc9c60cf6f", + "files": [ + { + "SPDXID": "SPDXRef-File-51bb5f929ef68877", + "checksums": [ + { + "algorithm": "SHA1", + "checksumValue": "69e70ec702f9df4ff64024b5fdea4644f1ce6c97" + } + ], + "fileName": "node_modules/yargs-parser/package.json" + } + ], + "name": "app", + "packages": [ + { + "SPDXID": "SPDXRef-Application-24f8a80152e2c0fc", + "downloadLocation": "NONE", + "filesAnalyzed": false, + "name": "node-pkg", + "sourceInfo": "Node.js" + }, + { + "SPDXID": "SPDXRef-Filesystem-13b142ca391a006e", + "attributionTexts": [ + "SchemaVersion: 2" + ], + "downloadLocation": "NONE", + "filesAnalyzed": false, + "name": "app" + }, + { + "SPDXID": "SPDXRef-Package-c3508825bf3861d8", + "attributionTexts": [ + "PkgID: yargs-parser@21.1.1" + ], + "downloadLocation": "NONE", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:npm/yargs-parser@21.1.1", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "hasFiles": [ + "SPDXRef-File-51bb5f929ef68877" + ], + "licenseConcluded": "ISC", + "licenseDeclared": "ISC", + "name": "yargs-parser", + "versionInfo": "21.1.1" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-Filesystem-13b142ca391a006e", + "relationshipType": "DESCRIBES", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-Application-24f8a80152e2c0fc", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-Filesystem-13b142ca391a006e" + }, + { + "relatedSpdxElement": "SPDXRef-Package-c3508825bf3861d8", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-Application-24f8a80152e2c0fc" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/testdata/sad/invalid-purl.json b/pkg/sbom/spdx/testdata/sad/invalid-purl.json new file mode 100644 index 000000000000..bbf5291b0e08 --- /dev/null +++ b/pkg/sbom/spdx/testdata/sad/invalid-purl.json @@ -0,0 +1,58 @@ +{ + "SPDXID": "SPDXRef-DOCUMENT", + "creationInfo": { + "created": "2022-09-12T17:02:46.826609Z", + "creators": [ + "Tool: trivy-dev", + "Organization: aquasecurity" + ] + }, + "dataLicense": "CC0-1.0", + "documentNamespace": "http://trivy.dev/container/meven-test-project-eb7a0384-b04a-4fc6-8afb-1662fe59ca79", + "name": "maven-test-projecct", + "packages": [ + { + "SPDXID": "SPDXRef-ContainerImage-b5d81cde5f95c8fc", + "filesAnalyzed": false, + "name": "meven-test-project" + }, + { + "SPDXID": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "filesAnalyzed": false, + "name": "alpine", + "versionInfo": "3.16.0" + }, + { + "SPDXID": "SPDXRef-Package-b7ebaf0233f1ef7b", + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceLocator": "pkg:invalid", + "referenceType": "purl" + } + ], + "filesAnalyzed": false, + "name": "musl", + "sourceInfo": "built package from: musl", + "versionInfo": "1.2.3-r0" + } + ], + "relationships": [ + { + "relatedSpdxElement": "SPDXRef-ContainerImage-b5d81cde5f95c8fc", + "relationshipType": "DESCRIBE", + "spdxElementId": "SPDXRef-DOCUMENT" + }, + { + "relatedSpdxElement": "SPDXRef-OperatingSystem-bd17bf9010aa612c", + "relationshipType": "CONTAINS", + "spdxElementId": "SPDXRef-ContainerImage-b5d81cde5f95c8fc" + }, + { + "relatedSpdxElement": "SPDXRef-Package-b7ebaf0233f1ef7b", + "relationshipType": "DEPENDS_ON", + "spdxElementId": "SPDXRef-OperatingSystem-bd17bf9010aa612c" + } + ], + "spdxVersion": "SPDX-2.2" +} \ No newline at end of file diff --git a/pkg/sbom/spdx/unmarshal.go b/pkg/sbom/spdx/unmarshal.go new file mode 100644 index 000000000000..1766bdea3042 --- /dev/null +++ b/pkg/sbom/spdx/unmarshal.go @@ -0,0 +1,293 @@ +package spdx + +import ( + "bytes" + "fmt" + "io" + "strings" + + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "github.com/spdx/tools-golang/json" + "github.com/spdx/tools-golang/spdx" + "github.com/spdx/tools-golang/spdx/v2/common" + "github.com/spdx/tools-golang/tagvalue" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/sbom/core" +) + +type SPDX struct { + *core.BOM + + trivySBOM bool + pkgFilePaths map[common.ElementID]string +} + +func NewTVDecoder(r io.Reader) *TVDecoder { + return &TVDecoder{r: r} +} + +type TVDecoder struct { + r io.Reader +} + +func (tv *TVDecoder) Decode(v any) error { + spdxDocument, err := tagvalue.Read(tv.r) + if err != nil { + return xerrors.Errorf("failed to load tag-value spdx: %w", err) + } + + a, ok := v.(*SPDX) + if !ok { + return xerrors.Errorf("invalid struct type tag-value decoder needed SPDX struct") + } + if err = a.unmarshal(spdxDocument); err != nil { + return xerrors.Errorf("failed to unmarshal spdx: %w", err) + } + + return nil +} + +func (s *SPDX) UnmarshalJSON(b []byte) error { + if s.BOM == nil { + s.BOM = core.NewBOM(core.Options{}) + } + + spdxDocument, err := json.Read(bytes.NewReader(b)) + if err != nil { + return xerrors.Errorf("failed to load spdx json: %w", err) + } + + if err = s.unmarshal(spdxDocument); err != nil { + return xerrors.Errorf("failed to unmarshal spdx: %w", err) + } + return nil +} + +func (s *SPDX) unmarshal(spdxDocument *spdx.Document) error { + s.trivySBOM = s.isTrivySBOM(spdxDocument) + + if s.pkgFilePaths == nil { + s.pkgFilePaths = make(map[common.ElementID]string) + } + + // Parse files and find file paths for packages + s.parseFiles(spdxDocument) + + // Convert all SPDX packages into Trivy components + components, err := s.parsePackages(spdxDocument) + if err != nil { + return xerrors.Errorf("package parse error: %w", err) + } + + // Parse relationships and build the dependency graph + for _, rel := range spdxDocument.Relationships { + // Skip the DESCRIBES relationship. + if rel.Relationship == common.TypeRelationshipDescribe || rel.Relationship == "DESCRIBE" { + continue + } + + compA, ok := components[rel.RefA.ElementRefID] + if !ok { // Skip if parent is not Package + continue + } + + compB, ok := components[rel.RefB.ElementRefID] + if !ok { // Skip if child is not Package + continue + } + + s.BOM.AddRelationship(compA, compB, s.parseRelationshipType(rel.Relationship)) + } + + return nil +} + +// parseFiles parses Relationships and finds filepaths for packages +func (s *SPDX) parseFiles(spdxDocument *spdx.Document) { + fileSPDXIdentifierMap := lo.SliceToMap(spdxDocument.Files, func(file *spdx.File) (common.ElementID, *spdx.File) { + return file.FileSPDXIdentifier, file + }) + + for _, rel := range spdxDocument.Relationships { + if rel.Relationship != common.TypeRelationshipContains && rel.Relationship != "CONTAIN" { + // Skip the DESCRIBES relationship. + continue + } + + // hasFiles field is deprecated + // https://github.com/spdx/tools-golang/issues/171 + // hasFiles values converted in Relationships + // https://github.com/spdx/tools-golang/pull/201 + if isFile(rel.RefB.ElementRefID) { + file, ok := fileSPDXIdentifierMap[rel.RefB.ElementRefID] + if ok { + // Save filePaths for packages + // Insert filepath will be later + s.pkgFilePaths[rel.RefA.ElementRefID] = file.FileName + } + continue + } + } +} + +func (s *SPDX) parsePackages(spdxDocument *spdx.Document) (map[common.ElementID]*core.Component, error) { + // Find a root package + var rootID common.ElementID + for _, rel := range spdxDocument.Relationships { + if rel.RefA.ElementRefID == DocumentSPDXIdentifier && rel.Relationship == RelationShipDescribe { + rootID = rel.RefB.ElementRefID + break + } + } + + // Convert packages into components + components := make(map[common.ElementID]*core.Component) + for _, pkg := range spdxDocument.Packages { + component, err := s.parsePackage(*pkg) + if err != nil { + return nil, xerrors.Errorf("failed to parse package: %w", err) + } + components[pkg.PackageSPDXIdentifier] = component + + if pkg.PackageSPDXIdentifier == rootID { + component.Root = true + } + s.BOM.AddComponent(component) + } + return components, nil +} + +func (s *SPDX) parsePackage(spdxPkg spdx.Package) (*core.Component, error) { + var err error + component := &core.Component{ + Type: s.parseType(spdxPkg), + Name: spdxPkg.PackageName, + Version: spdxPkg.PackageVersion, + } + + // PURL + if component.PkgIdentifier.PURL, err = s.parseExternalReferences(spdxPkg.PackageExternalReferences); err != nil { + return nil, xerrors.Errorf("external references error: %w", err) + } + + // License + if spdxPkg.PackageLicenseDeclared != "NONE" { + component.Licenses = strings.Split(spdxPkg.PackageLicenseDeclared, ",") + } + + // Source package + if strings.HasPrefix(spdxPkg.PackageSourceInfo, SourcePackagePrefix) { + srcPkgName := strings.TrimPrefix(spdxPkg.PackageSourceInfo, fmt.Sprintf("%s: ", SourcePackagePrefix)) + component.SrcName, component.SrcVersion, _ = strings.Cut(srcPkgName, " ") + } + + // Files + // TODO: handle checksums as well + if path, ok := s.pkgFilePaths[spdxPkg.PackageSPDXIdentifier]; ok { + component.Files = []core.File{ + {Path: path}, + } + } else if len(spdxPkg.Files) > 0 { + component.Files = []core.File{ + {Path: spdxPkg.Files[0].FileName}, // Take the first file name + } + } + + // Trivy stores properties in Annotations + // But previous versions stored properties in AttributionTexts + // So we need to check both cases to maintain backward compatibility + var props []string + if len(spdxPkg.Annotations) > 0 { + for _, annotation := range spdxPkg.Annotations { + props = append(props, annotation.AnnotationComment) + } + } else if len(spdxPkg.PackageAttributionTexts) > 0 { + for _, attr := range spdxPkg.PackageAttributionTexts { + props = append(props, attr) + } + } + for _, prop := range props { + k, v, ok := strings.Cut(prop, ": ") + if !ok { + continue + } + component.Properties = append(component.Properties, core.Property{ + Name: k, + Value: v, + }) + } + + // For backward-compatibility + // Older Trivy versions put the file path in "sourceInfo" and the package type in "name". + if s.trivySBOM && component.Type == core.TypeApplication && spdxPkg.PackageSourceInfo != "" { + component.Name = spdxPkg.PackageSourceInfo + component.Properties = append(component.Properties, core.Property{ + Name: core.PropertyType, + Value: spdxPkg.PackageName, + }) + } + + return component, nil +} + +func (s *SPDX) parseType(pkg spdx.Package) core.ComponentType { + id := string(pkg.PackageSPDXIdentifier) + switch { + case strings.HasPrefix(id, ElementOperatingSystem): + return core.TypeOS + case strings.HasPrefix(id, ElementApplication): + return core.TypeApplication + case strings.HasPrefix(id, ElementPackage): + return core.TypeLibrary + default: + return core.TypeLibrary // unknown is handled as a library + } +} + +func (s *SPDX) parseRelationshipType(rel string) core.RelationshipType { + switch rel { + case common.TypeRelationshipDescribe: + return core.RelationshipDescribes + case common.TypeRelationshipContains, "CONTAIN": + return core.RelationshipContains + case common.TypeRelationshipDependsOn: + return core.RelationshipDependsOn + default: + return core.RelationshipContains + } +} + +func (s *SPDX) parseExternalReferences(refs []*spdx.PackageExternalReference) (*packageurl.PackageURL, error) { + for _, ref := range refs { + // Extract the package information from PURL + if ref.RefType != RefTypePurl || ref.Category != CategoryPackageManager { + continue + } + + packageURL, err := packageurl.FromString(ref.Locator) + if err != nil { + return nil, xerrors.Errorf("failed to parse purl from string: %w", err) + } + return &packageURL, nil + } + return nil, nil +} + +func (s *SPDX) isTrivySBOM(spdxDocument *spdx.Document) bool { + if spdxDocument == nil || spdxDocument.CreationInfo == nil || spdxDocument.CreationInfo.Creators == nil { + return false + } + + for _, c := range spdxDocument.CreationInfo.Creators { + if c.CreatorType == "Tool" && strings.HasPrefix(c.Creator, "trivy") { + return true + } + } + return false +} + +func isFile(elementID spdx.ElementID) bool { + return strings.HasPrefix(string(elementID), ElementFile) +} diff --git a/pkg/sbom/spdx/unmarshal_test.go b/pkg/sbom/spdx/unmarshal_test.go new file mode 100644 index 000000000000..50bfcb11190b --- /dev/null +++ b/pkg/sbom/spdx/unmarshal_test.go @@ -0,0 +1,372 @@ +package spdx_test + +import ( + "encoding/json" + "os" + "sort" + "testing" + + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" + "github.com/aquasecurity/trivy/pkg/sbom/spdx" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestUnmarshaler_Unmarshal(t *testing.T) { + tests := []struct { + name string + inputFile string + want types.SBOM + wantErr string + }{ + { + name: "happy path", + inputFile: "testdata/happy/bom.json", + want: types.SBOM{ + Metadata: types.Metadata{ + ImageID: "sha256:49193a2310dbad4c02382da87ac624a80a92387a4f7536235f9ba590e5bcd7b5", + DiffIDs: []string{ + "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + RepoTags: []string{ + "maven-test-project:latest", + "tmp-test:latest", + }, + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.16.0", + }, + }, + Packages: []ftypes.PackageInfo{ + { + Packages: ftypes.Packages{ + { + ID: "musl@1.2.3-r0", + Name: "musl", + Version: "1.2.3-r0", + SrcName: "musl", + SrcVersion: "1.2.3-r0", + Licenses: []string{"MIT"}, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeApk, + Namespace: "alpine", + Name: "musl", + Version: "1.2.3-r0", + Qualifiers: packageurl.Qualifiers{ + { + Key: "distro", + Value: "3.16.0", + }, + }, + }, + }, + Layer: ftypes.Layer{ + DiffID: "sha256:dd565ff850e7003356e2b252758f9bdc1ff2803f61e995e24c7844f6297f8fc3", + }, + }, + }, + }, + }, + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "app/composer/composer.lock", + Packages: ftypes.Packages{ + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + }, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + { + ID: "pear/pear_exception@v1.0.0", + Name: "pear/pear_exception", + Version: "v1.0.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "pear_exception", + Version: "v1.0.0", + }, + }, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "gobinary", + FilePath: "app/gobinary/gobinary", + Packages: ftypes.Packages{ + { + ID: "github.com/package-url/packageurl-go@v0.1.1-0.20220203205134-d70459300c8a", + Name: "github.com/package-url/packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/package-url", + Name: "packageurl-go", + Version: "v0.1.1-0.20220203205134-d70459300c8a", + }, + }, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "jar", + Packages: ftypes.Packages{ + { + ID: "org.codehaus.mojo:child-project:1.0", + Name: "org.codehaus.mojo:child-project", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.codehaus.mojo", + Name: "child-project", + Version: "1.0", + }, + }, + Version: "1.0", + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + { + Type: "node-pkg", + Packages: ftypes.Packages{ + { + ID: "bootstrap@5.0.2", + Name: "bootstrap", + Version: "5.0.2", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "bootstrap", + Version: "5.0.2", + }, + }, + Licenses: []string{"MIT"}, + Layer: ftypes.Layer{ + DiffID: "sha256:3c79e832b1b4891a1cb4a326ef8524e0bd14a2537150ac0e203a5677176c1ca1", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path for bom with hasFiles field", + inputFile: "testdata/happy/with-hasfiles-bom.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: ftypes.NodePkg, + Packages: ftypes.Packages{ + { + ID: "yargs-parser@21.1.1", + Name: "yargs-parser", + Version: "21.1.1", + Licenses: []string{"ISC"}, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "yargs-parser", + Version: "21.1.1", + }, + }, + FilePath: "node_modules/yargs-parser/package.json", + }, + }, + }, + }, + }, + }, + { + name: "happy path for bom files in relationships", + inputFile: "testdata/happy/with-files-in-relationships-bom.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: "node-pkg", + Packages: ftypes.Packages{ + { + ID: "yargs-parser@21.1.1", + Name: "yargs-parser", + Version: "21.1.1", + Licenses: []string{"ISC"}, + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeNPM, + Name: "yargs-parser", + Version: "21.1.1", + }, + }, + FilePath: "node_modules/yargs-parser/package.json", + }, + }, + }, + }, + }, + }, + { + name: "happy path for unrelated bom", + inputFile: "testdata/happy/unrelated-bom.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: "composer", + FilePath: "app/composer/composer.lock", + Packages: ftypes.Packages{ + { + ID: "pear/log@1.13.1", + Name: "pear/log", + Version: "1.13.1", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "log", + Version: "1.13.1", + }, + }, + }, + { + ID: "pear/pear_exception@v1.0.0", + Name: "pear/pear_exception", + Version: "v1.0.0", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeComposer, + Namespace: "pear", + Name: "pear_exception", + Version: "v1.0.0", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with no relationship", + inputFile: "testdata/happy/no-relationship.json", + want: types.SBOM{ + Applications: []ftypes.Application{ + { + Type: ftypes.Jar, + Packages: ftypes.Packages{ + { + ID: "co.elastic.apm:apm-agent:1.36.0", + Name: "co.elastic.apm:apm-agent", + Version: "1.36.0", + FilePath: "modules/apm/elastic-apm-agent-1.36.0.jar", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "co.elastic.apm", + Name: "apm-agent", + Version: "1.36.0", + }, + }, + }, + { + ID: "co.elastic.apm:apm-agent-cached-lookup-key:1.36.0", + Name: "co.elastic.apm:apm-agent-cached-lookup-key", + Version: "1.36.0", + FilePath: "modules/apm/elastic-apm-agent-1.36.0.jar", + Identifier: ftypes.PkgIdentifier{ + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "co.elastic.apm", + Name: "apm-agent-cached-lookup-key", + Version: "1.36.0", + }, + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with file as parent of relationship", + inputFile: "testdata/happy/with-file-as-relationship-parent.json", + want: types.SBOM{}, + }, + { + name: "happy path only os component", + inputFile: "testdata/happy/os-only-bom.json", + want: types.SBOM{ + Metadata: types.Metadata{ + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.16.0", + }, + }, + }, + }, + { + name: "happy path empty component", + inputFile: "testdata/happy/empty-bom.json", + want: types.SBOM{}, + }, + { + name: "sad path invalid purl", + inputFile: "testdata/sad/invalid-purl.json", + wantErr: "purl is missing type or name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f, err := os.Open(tt.inputFile) + require.NoError(t, err) + defer f.Close() + + var v spdx.SPDX + err = json.NewDecoder(f).Decode(&v) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + var got types.SBOM + err = sbomio.NewDecoder(v.BOM).Decode(t.Context(), &got) + require.NoError(t, err) + + // Not compare BOM + got.BOM = nil + + sort.Slice(got.Applications, func(i, j int) bool { + return got.Applications[i].Type < got.Applications[j].Type + }) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/scan/export_test.go b/pkg/scan/export_test.go new file mode 100644 index 000000000000..dcc0f86c3b9b --- /dev/null +++ b/pkg/scan/export_test.go @@ -0,0 +1,12 @@ +package scan + +import ( + "github.com/aquasecurity/trivy/pkg/fanal/artifact" +) + +// Bridge to expose scan internals to tests in the scan_test package. + +// GenerateArtifactID exports generateArtifactID for testing. +func (s Service) GenerateArtifactID(artifactInfo artifact.Reference) string { + return s.generateArtifactID(artifactInfo) +} diff --git a/pkg/scan/langpkg/scan.go b/pkg/scan/langpkg/scan.go new file mode 100644 index 000000000000..565c3b2c07c1 --- /dev/null +++ b/pkg/scan/langpkg/scan.go @@ -0,0 +1,103 @@ +package langpkg + +import ( + "context" + "sort" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/detector/library" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/types" +) + +var ( + PkgTargets = map[ftypes.LangType]string{ + ftypes.PythonPkg: "Python", + ftypes.CondaPkg: "Conda", + ftypes.GemSpec: "Ruby", + ftypes.NodePkg: "Node.js", + ftypes.Jar: "Java", + ftypes.K8sUpstream: "Kubernetes", + } +) + +type Scanner interface { + Scan(ctx context.Context, target types.ScanTarget, options types.ScanOptions) (types.Results, error) +} + +type scanner struct{} + +func NewScanner() Scanner { + return &scanner{} +} + +func (s *scanner) Scan(ctx context.Context, target types.ScanTarget, opts types.ScanOptions) (types.Results, error) { + apps := target.Applications + log.Info("Number of language-specific files", log.Int("num", len(apps))) + if len(apps) == 0 { + return nil, nil + } + + var results types.Results + printedTypes := set.New[ftypes.LangType]() + for _, app := range apps { + if len(app.Packages) == 0 { + continue + } + + ctx = log.WithContextPrefix(ctx, string(app.Type)) + result := types.Result{ + Target: targetName(app.Type, app.FilePath), + Class: types.ClassLangPkg, + Type: app.Type, + } + + sort.Sort(app.Packages) + result.Packages = app.Packages + + if opts.Scanners.Enabled(types.VulnerabilityScanner) { + var err error + result.Vulnerabilities, err = s.scanVulnerabilities(ctx, app, printedTypes) + if err != nil { + return nil, err + } + } + + if len(result.Packages) == 0 && len(result.Vulnerabilities) == 0 { + continue + } + results = append(results, result) + } + sort.Slice(results, func(i, j int) bool { + return results[i].Target < results[j].Target + }) + return results, nil +} + +func (s *scanner) scanVulnerabilities(ctx context.Context, app ftypes.Application, printedTypes set.Set[ftypes.LangType]) ( + []types.DetectedVulnerability, error) { + + // Prevent the same log messages from being displayed many times for the same type. + if !printedTypes.Contains(app.Type) { + log.InfoContext(ctx, "Detecting vulnerabilities...") + printedTypes.Append(app.Type) + } + + log.DebugContext(ctx, "Scanning packages for vulnerabilities", log.FilePath(app.FilePath)) + vulns, err := library.Detect(ctx, app.Type, app.Packages) + if err != nil { + return nil, xerrors.Errorf("failed vulnerability detection of libraries: %w", err) + } + return vulns, err +} + +func targetName(appType ftypes.LangType, filePath string) string { + if t, ok := PkgTargets[appType]; ok && filePath == "" { + // When the file path is empty, we will overwrite it with the pre-defined value. + return t + } + return filePath +} diff --git a/pkg/scan/local/service.go b/pkg/scan/local/service.go new file mode 100644 index 000000000000..6bb74fa20828 --- /dev/null +++ b/pkg/scan/local/service.go @@ -0,0 +1,552 @@ +package local + +import ( + "context" + "errors" + "fmt" + "slices" + "sort" + "strings" + "sync" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + ospkgDetector "github.com/aquasecurity/trivy/pkg/detector/ospkg" + "github.com/aquasecurity/trivy/pkg/extension" + "github.com/aquasecurity/trivy/pkg/fanal/analyzer" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/iac/rego" + "github.com/aquasecurity/trivy/pkg/licensing" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/scan/langpkg" + "github.com/aquasecurity/trivy/pkg/scan/ospkg" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vulnerability" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" + + _ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/all" + _ "github.com/aquasecurity/trivy/pkg/fanal/handler/all" +) + +// Service implements the OspkgDetector and LibraryDetector +type Service struct { + applier applier.Applier + osPkgScanner ospkg.Scanner + langPkgScanner langpkg.Scanner + vulnClient vulnerability.Client +} + +// NewService is the factory method for scan service +func NewService(a applier.Applier, osPkgScanner ospkg.Scanner, langPkgScanner langpkg.Scanner, + vulnClient vulnerability.Client) Service { + return Service{ + applier: a, + osPkgScanner: osPkgScanner, + langPkgScanner: langPkgScanner, + vulnClient: vulnClient, + } +} + +// Scan scans the artifact and return results. +func (s Service) Scan(ctx context.Context, targetName, artifactKey string, blobKeys []string, options types.ScanOptions) ( + types.ScanResponse, error) { + detail, err := s.applier.ApplyLayers(ctx, artifactKey, blobKeys) + switch { + case errors.Is(err, analyzer.ErrUnknownOS): + log.Debug("OS is not detected.") + + // Packages may contain OS-independent binary information even though OS is not detected. + if len(detail.Packages) != 0 { + detail.OS = ftypes.OS{Family: "none"} + } + + // If OS is not detected and repositories are detected, we'll try to use repositories as OS. + if detail.Repository != nil { + log.Debug("Package repository", log.String("family", string(detail.Repository.Family)), + log.String("version", detail.Repository.Release)) + log.Debug("Assuming OS", log.String("family", string(detail.Repository.Family)), + log.String("version", detail.Repository.Release)) + detail.OS = ftypes.OS{ + Family: detail.Repository.Family, + Name: detail.Repository.Release, + } + } + case !detail.OS.Family.HasOSPackages(): + // Some OS types like ActiveState don't have OS packages, only language-specific packages. + // No warning needed. + case errors.Is(err, analyzer.ErrNoPkgsDetected): + log.Warn("No OS package is detected. Make sure you haven't deleted any files that contain information about the installed packages.") + log.Warn(`e.g. files under "/lib/apk/db/", "/var/lib/dpkg/" and "/var/lib/rpm"`) + case err != nil: + return types.ScanResponse{}, xerrors.Errorf("failed to apply layers: %w", err) + } + + if !lo.IsEmpty(options.Distro) && !lo.IsEmpty(detail.OS) { + log.Info("Overriding detected OS with provided distro", log.String("detected", detail.OS.String()), + log.String("provided", options.Distro.String())) + detail.OS = options.Distro + } + + target := types.ScanTarget{ + Name: targetName, + OS: detail.OS, + Repository: lo.Ternary(lo.IsEmpty(options.Distro), detail.Repository, nil), + Packages: mergePkgs(detail.Packages, detail.ImageConfig.Packages, options), + Applications: detail.Applications, + Misconfigurations: mergeMisconfigurations(targetName, detail), + Secrets: mergeSecrets(targetName, detail), + Licenses: detail.Licenses, + CustomResources: detail.CustomResources, + } + + results, os, err := s.ScanTarget(ctx, target, options) + if err != nil { + return types.ScanResponse{}, err + } + return types.ScanResponse{ + Results: results, + OS: os, + Layers: detail.Layers, + }, nil +} + +func (s Service) ScanTarget(ctx context.Context, target types.ScanTarget, options types.ScanOptions) (types.Results, ftypes.OS, error) { + // Call pre-scan hooks + if err := extension.PreScan(ctx, &target, options); err != nil { + return nil, ftypes.OS{}, xerrors.Errorf("pre scan error: %w", err) + } + + var results types.Results + + // Filter packages according to the options + excludePackages(&target, options) + + // Add packages if needed and scan packages for vulnerabilities + vulnResults, eosl, err := s.scanVulnerabilities(ctx, target, options) + if err != nil { + return nil, ftypes.OS{}, xerrors.Errorf("failed to detect vulnerabilities: %w", err) + } + target.OS.Eosl = eosl + results = append(results, vulnResults...) + + // Store misconfigurations + results = append(results, s.misconfsToResults(target.Misconfigurations, options)...) + + // Store secrets + results = append(results, s.secretsToResults(target.Secrets, options)...) + + // Scan licenses + results = append(results, s.scanLicenses(target, options)...) + + // For WASM plugins and custom analyzers + if len(target.CustomResources) != 0 { + results = append(results, types.Result{ + Class: types.ClassCustom, + CustomResources: target.CustomResources, + }) + } + + for i := range results { + // Fill vulnerability details + s.vulnClient.FillInfo(results[i].Vulnerabilities, options.VulnSeveritySources) + } + + // Call post-scan hooks + if results, err = extension.PostScan(ctx, results); err != nil { + return nil, ftypes.OS{}, xerrors.Errorf("post scan error: %w", err) + } + + return results, target.OS, nil +} + +func (s Service) scanVulnerabilities(ctx context.Context, target types.ScanTarget, options types.ScanOptions) ( + types.Results, bool, error) { + if !options.Scanners.AnyEnabled(types.SBOMScanner, types.VulnerabilityScanner) { + return nil, false, nil + } + + var eosl bool + var results types.Results + + if slices.Contains(options.PkgTypes, types.PkgTypeOS) { + vuln, detectedEOSL, err := s.osPkgScanner.Scan(ctx, target, options) + switch { + case errors.Is(err, ospkgDetector.ErrUnsupportedOS): + // do nothing + case err != nil: + return nil, false, xerrors.Errorf("unable to scan OS packages: %w", err) + case vuln.Target != "": + results = append(results, vuln) + eosl = detectedEOSL + } + } + + if slices.Contains(options.PkgTypes, types.PkgTypeLibrary) { + vulns, err := s.langPkgScanner.Scan(ctx, target, options) + if err != nil { + return nil, false, xerrors.Errorf("failed to scan application libraries: %w", err) + } + results = append(results, vulns...) + } + + return results, eosl, nil +} + +func (s Service) misconfsToResults(misconfs []ftypes.Misconfiguration, options types.ScanOptions) types.Results { + if !ShouldScanMisconfigOrRbac(options.Scanners) && + !options.ImageConfigScanners.Enabled(types.MisconfigScanner) { + return nil + } + + return s.MisconfsToResults(misconfs) +} + +// MisconfsToResults is exported for trivy-plugin-aqua purposes only +func (s Service) MisconfsToResults(misconfs []ftypes.Misconfiguration) types.Results { + log.Info("Detected config files", log.Int("num", len(misconfs))) + var results types.Results + for _, misconf := range misconfs { + log.Debug("Scanned config file", log.FilePath(misconf.FilePath)) + + var detected []types.DetectedMisconfiguration + + for _, f := range misconf.Failures { + detected = append(detected, toDetectedMisconfiguration(f, dbTypes.SeverityCritical, types.MisconfStatusFailure, misconf.Layer)) + } + for _, w := range misconf.Warnings { + detected = append(detected, toDetectedMisconfiguration(w, dbTypes.SeverityMedium, types.MisconfStatusFailure, misconf.Layer)) + } + for _, w := range misconf.Successes { + detected = append(detected, toDetectedMisconfiguration(w, dbTypes.SeverityUnknown, types.MisconfStatusPassed, misconf.Layer)) + } + + results = append(results, types.Result{ + Target: misconf.FilePath, + Class: types.ClassConfig, + Type: misconf.FileType, + Misconfigurations: detected, + }) + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Target < results[j].Target + }) + + return results +} + +func (s Service) secretsToResults(secrets []ftypes.Secret, options types.ScanOptions) types.Results { + if !options.Scanners.Enabled(types.SecretScanner) { + return nil + } + + var results types.Results + for _, secret := range secrets { + log.Debug("Secret file", log.FilePath(secret.FilePath)) + + results = append(results, types.Result{ + Target: secret.FilePath, + Class: types.ClassSecret, + Secrets: xslices.Map(secret.Findings, func(secret ftypes.SecretFinding) types.DetectedSecret { + return types.DetectedSecret(secret) + }), + }) + } + return results +} + +func (s Service) scanLicenses(target types.ScanTarget, options types.ScanOptions) types.Results { + if !options.Scanners.Enabled(types.LicenseScanner) { + return nil + } + + var results types.Results + scanner := licensing.NewScanner(options.LicenseCategories) + + if slices.Contains(options.PkgTypes, types.PkgTypeOS) { + // Scan licenses for OS packages + if result := s.scanOSPackageLicenses(target.Packages, scanner); result != nil { + results = append(results, *result) + } + } + + if slices.Contains(options.PkgTypes, types.PkgTypeLibrary) { + // Scan licenses for language-specific packages + results = append(results, s.scanApplicationLicenses(target.Applications, scanner)...) + } + + // Scan licenses in file headers or license files + if result := s.scanFileLicenses(target.Licenses, scanner, options); result != nil { + results = append(results, *result) + } + + return results +} + +func (s Service) scanOSPackageLicenses(packages []ftypes.Package, scanner licensing.Scanner) *types.Result { + if len(packages) == 0 { + return nil + } + + var licenses []types.DetectedLicense + for _, pkg := range packages { + for _, license := range pkg.Licenses { + licenses = append(licenses, toDetectedLicense(scanner, license, pkg.Name, "")) + } + } + return &types.Result{ + Target: "OS Packages", + Class: types.ClassLicense, + Licenses: licenses, + } +} + +func (s Service) scanApplicationLicenses(apps []ftypes.Application, scanner licensing.Scanner) []types.Result { + var results []types.Result + + for _, app := range apps { + var langLicenses []types.DetectedLicense + for _, lib := range app.Packages { + for _, license := range lib.Licenses { + // Lock files use app.FilePath - https://github.com/aquasecurity/trivy/blob/6ccc0a554b07b05fd049f882a1825a0e1e0aabe1/pkg/fanal/types/artifact.go#L245-L246 + // Applications use lib.FilePath - https://github.com/aquasecurity/trivy/blob/6ccc0a554b07b05fd049f882a1825a0e1e0aabe1/pkg/fanal/types/artifact.go#L93-L94 + filePath := lo.Ternary(lib.FilePath != "", lib.FilePath, app.FilePath) + + langLicenses = append(langLicenses, toDetectedLicense(scanner, license, lib.Name, filePath)) + } + } + + targetName := app.FilePath + if t, ok := langpkg.PkgTargets[app.Type]; ok && targetName == "" { + // When the file path is empty, we will overwrite it with the pre-defined value. + targetName = t + } + + if len(langLicenses) != 0 { + results = append(results, types.Result{ + Target: targetName, + Class: types.ClassLicense, + Licenses: langLicenses, + }) + } + } + + return results +} + +func (s Service) scanFileLicenses(licenses []ftypes.LicenseFile, scanner licensing.Scanner, options types.ScanOptions) *types.Result { + if !options.LicenseFull { + return nil + } + var detectedLicenses []types.DetectedLicense + for _, license := range licenses { + for _, finding := range license.Findings { + category, severity := scanner.Scan(finding.Name) + detectedLicenses = append(detectedLicenses, types.DetectedLicense{ + Severity: severity, + Category: category, + FilePath: license.FilePath, + Name: finding.Name, + Confidence: finding.Confidence, + Link: finding.Link, + }) + } + } + + return &types.Result{ + Target: "Loose File License(s)", + Class: types.ClassLicenseFile, + Licenses: detectedLicenses, + } +} + +func toDetectedMisconfiguration(res ftypes.MisconfResult, defaultSeverity dbTypes.Severity, + status types.MisconfStatus, layer ftypes.Layer) types.DetectedMisconfiguration { + + severity := defaultSeverity + sev, err := dbTypes.NewSeverity(res.Severity) + if err != nil { + log.Warn("Unsupported severity", log.String("severity", res.Severity)) + } else { + severity = sev + } + + msg := strings.TrimSpace(res.Message) + if msg == "" { + msg = "No issues found" + } + + var primaryURL string + + // empty namespace implies a go rule from defsec, "builtin" refers to a built-in rego rule + // this ensures we don't generate bad links for custom policies + if res.Namespace == "" || rego.IsBuiltinNamespace(res.Namespace) { + primaryURL = fmt.Sprintf("https://avd.aquasec.com/misconfig/%s", strings.ToLower(res.ID)) + res.References = append(res.References, primaryURL) + } + + if primaryURL == "" && len(res.References) > 0 { + primaryURL = res.References[0] + } + + return types.DetectedMisconfiguration{ + ID: res.ID, + AVDID: res.AVDID, + Aliases: res.Aliases, + Type: res.Type, + Title: res.Title, + Description: res.Description, + Message: msg, + Resolution: res.RecommendedActions, + Namespace: res.Namespace, + Query: res.Query, + Severity: severity.String(), + PrimaryURL: primaryURL, + References: res.References, + Status: status, + Layer: layer, + Traces: res.Traces, + CauseMetadata: ftypes.CauseMetadata{ + Resource: res.Resource, + Provider: res.Provider, + Service: res.Service, + StartLine: res.StartLine, + EndLine: res.EndLine, + Code: res.Code, + Occurrences: res.Occurrences, + RenderedCause: res.RenderedCause, + }, + } +} + +func toDetectedLicense(scanner licensing.Scanner, license, pkgName, filePath string) types.DetectedLicense { + var category ftypes.LicenseCategory + var severity, licenseText string + if after, ok := strings.CutPrefix(license, licensing.LicenseTextPrefix); ok { // License text + licenseText = after + category, severity = scanner.ScanTextLicense(licenseText) + license = licensing.CustomLicensePrefix + ": " + licensing.TrimLicenseText(licenseText) // Use `CUSTOM LICENSE: *...` format for text licenses + } else { // License name + category, severity = scanner.Scan(license) + } + + return types.DetectedLicense{ + Severity: severity, + Category: category, + PkgName: pkgName, + FilePath: filePath, + Name: license, + Text: licenseText, + Confidence: 1.0, + } +} + +func ShouldScanMisconfigOrRbac(scanners types.Scanners) bool { + return scanners.AnyEnabled(types.MisconfigScanner, types.RBACScanner) +} + +func excludePackages(target *types.ScanTarget, options types.ScanOptions) { + // Filter packages by relationship + filterPkgByRelationship(target, options) + + // By default, development packages are removed from the result + // '--include-dev-deps' option allows including them + excludeDevDeps(target.Applications, options.IncludeDevDeps) +} + +func filterPkgByRelationship(target *types.ScanTarget, options types.ScanOptions) { + if slices.Compare(options.PkgRelationships, ftypes.Relationships) == 0 { + return // No need to filter + } + + filter := func(pkgs []ftypes.Package) []ftypes.Package { + return lo.Filter(pkgs, func(pkg ftypes.Package, _ int) bool { + return slices.Contains(options.PkgRelationships, pkg.Relationship) + }) + } + + target.Packages = filter(target.Packages) + for i, app := range target.Applications { + target.Applications[i].Packages = filter(app.Packages) + } +} + +// excludeDevDeps removes development dependencies from the list of applications +func excludeDevDeps(apps []ftypes.Application, include bool) { + if include { + return + } + + onceInfo := sync.OnceFunc(func() { + log.Info("Suppressing dependencies for development and testing. To display them, try the '--include-dev-deps' flag.") + }) + + for i := range apps { + devDeps := set.New[string]() + apps[i].Packages = lo.Filter(apps[i].Packages, func(pkg ftypes.Package, _ int) bool { + if pkg.Dev { + onceInfo() + devDeps.Append(pkg.ID) + } + return !pkg.Dev + }) + + // Remove development dependencies from dependencies of root and workspace packages + for j, pkg := range apps[i].Packages { + if pkg.Relationship != ftypes.RelationshipRoot && pkg.Relationship != ftypes.RelationshipWorkspace { + continue + } + apps[i].Packages[j].DependsOn = lo.Filter(apps[i].Packages[j].DependsOn, func(dep string, _ int) bool { + return !devDeps.Contains(dep) + }) + + } + } +} + +func mergePkgs(pkgs, pkgsFromCommands []ftypes.Package, options types.ScanOptions) []ftypes.Package { + if !options.ScanRemovedPackages || len(pkgsFromCommands) == 0 { + return pkgs + } + + // pkg has priority over pkgsFromCommands + uniqPkgs := set.New[string]() + for _, pkg := range pkgs { + uniqPkgs.Append(pkg.Name) + } + for _, pkg := range pkgsFromCommands { + if uniqPkgs.Contains(pkg.Name) { + continue + } + pkgs = append(pkgs, pkg) + } + return pkgs +} + +// mergeMisconfigurations merges misconfigurations on container image config +func mergeMisconfigurations(targetName string, detail ftypes.ArtifactDetail) []ftypes.Misconfiguration { + if detail.ImageConfig.Misconfiguration == nil { + return detail.Misconfigurations + } + + // Append misconfigurations on container image config + misconf := detail.ImageConfig.Misconfiguration + misconf.FilePath = targetName // Set the target name to the file path as container image config is not a real file. + return append(detail.Misconfigurations, *misconf) +} + +// mergeSecrets merges secrets on container image config. +func mergeSecrets(targetName string, detail ftypes.ArtifactDetail) []ftypes.Secret { + if detail.ImageConfig.Secret == nil { + return detail.Secrets + } + + // Append secrets on container image config + secret := detail.ImageConfig.Secret + secret.FilePath = targetName // Set the target name to the file path as container image config is not a real file. + return append(detail.Secrets, *secret) +} diff --git a/pkg/scan/local/service_test.go b/pkg/scan/local/service_test.go new file mode 100644 index 000000000000..b8ed8698908c --- /dev/null +++ b/pkg/scan/local/service_test.go @@ -0,0 +1,1542 @@ +package local + +import ( + "testing" + "time" + + "github.com/package-url/packageurl-go" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/internal/hooktest" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/scan/langpkg" + "github.com/aquasecurity/trivy/pkg/scan/ospkg" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vulnerability" +) + +var ( + muslPkg = ftypes.Package{ + Name: "musl", + Version: "1.2.3", + SrcName: "musl", + SrcVersion: "1.2.3", + Licenses: []string{"MIT"}, + Layer: ftypes.Layer{ + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + Identifier: ftypes.PkgIdentifier{ + UID: "d9a73c7459d27809", + PURL: &packageurl.PackageURL{ + Type: "apk", + Namespace: "alpine", + Name: "musl", + Version: "1.2.3", + Qualifiers: packageurl.Qualifiers{ + packageurl.Qualifier{ + Key: "distro", + Value: "3.11", + }, + }, + }, + }, + } + libunistring5Pkg = ftypes.Package{ + Name: "libunistring5", + Version: "1.1-2build1.1", + SrcName: "libunistring5", + SrcVersion: "1.1-2build1.1", + Licenses: []string{"GFDL-NIV-1.2+"}, + } + railsPkg = ftypes.Package{ + Name: "rails", + Version: "4.0.2", + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + Identifier: ftypes.PkgIdentifier{ + UID: "49be2edc1596dd5d", + PURL: &packageurl.PackageURL{ + Type: "gem", + Name: "rails", + Version: "4.0.2", + }, + }, + } + innocentPkg = ftypes.Package{ + Name: "innocent", + Version: "1.2.3", + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + Identifier: ftypes.PkgIdentifier{ + UID: "50b49e415e6a2f59", + PURL: &packageurl.PackageURL{ + Type: "gem", + Name: "innocent", + Version: "1.2.3", + }, + }, + } + uuidPkg = ftypes.Package{ + Name: "github.com/google/uuid", + Version: "1.6.0", + FilePath: "", + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + Licenses: []string{"LGPL"}, + } + urllib3Pkg = ftypes.Package{ + Name: "urllib3", + Version: "3.2.1", + FilePath: "/usr/lib/python/site-packages/urllib3-3.2.1/METADATA", + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + Licenses: []string{"text://(c) 2015 Continuum Analytics, Inc."}, + } + menuinstPkg = ftypes.Package{ + Name: "menuinst", + Version: "2.0.2", + FilePath: "opt/conda/lib/python3.11/site-packages/menuinst-2.0.2.dist-info/METADATA", + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + Licenses: []string{"text://(c) 2016 Continuum Analytics, Inc. / http://continuum.io All Rights Reserved"}, + } + + laravelPkg = ftypes.Package{ + Name: "laravel/framework", + Version: "6.0.0", + Relationship: ftypes.RelationshipDirect, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + Identifier: ftypes.PkgIdentifier{ + UID: "ba565db6c74968e3", + PURL: &packageurl.PackageURL{ + Type: "composer", + Namespace: "laravel", + Name: "framework", + Version: "6.0.0", + }, + }, + } + guzzlePkg = ftypes.Package{ + Name: "guzzlehttp/guzzle", + Version: "7.9.2", + Relationship: ftypes.RelationshipIndirect, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + Identifier: ftypes.PkgIdentifier{ + UID: "791b71e6f31e53a5", + PURL: &packageurl.PackageURL{ + Type: "composer", + Namespace: "guzzlehttp", + Name: "guzzle", + Version: "7.9.2", + }, + }, + } +) + +func TestScanner_Scan(t *testing.T) { + type args struct { + target string + layerIDs []string + options types.ScanOptions + } + tests := []struct { + name string + args args + fixtures []string + setUpHook bool + setupCache func(t *testing.T) cache.Cache + want types.ScanResponse + wantErr string + }{ + { + name: "happy path", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{ + types.PkgTypeOS, + types.PkgTypeLibrary, + }, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + OS: ftypes.OS{ + Family: ftypes.Alpine, + Name: "3.11", + }, + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []ftypes.Package{muslPkg}, + }, + }, + Applications: []ftypes.Application{ + { + Type: ftypes.Bundler, + FilePath: "/app/Gemfile.lock", + Packages: []ftypes.Package{railsPkg}, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "alpine:latest (alpine 3.11)", + Class: types.ClassOSPkg, + Type: ftypes.Alpine, + Packages: ftypes.Packages{ + muslPkg, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-9999", + PkgName: muslPkg.Name, + PkgIdentifier: muslPkg.Identifier, + InstalledVersion: muslPkg.Version, + FixedVersion: "1.2.4", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-9999", + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: "HIGH", + }, + }, + }, + }, + { + Target: "/app/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: ftypes.Packages{ + railsPkg, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2014-0081", + PkgName: railsPkg.Name, + PkgIdentifier: railsPkg.Identifier, + InstalledVersion: railsPkg.Version, + FixedVersion: "4.0.3, 3.2.17", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081", + Vulnerability: dbTypes.Vulnerability{ + Title: "xss", + Description: "xss vulnerability", + Severity: "MEDIUM", + References: []string{ + "http://example.com", + }, + LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)), + PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)), + }, + }, + }, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: true, + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + { + name: "happy path with OS rewriting", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{ + types.PkgTypeOS, + types.PkgTypeLibrary, + }, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + Distro: ftypes.OS{ + Family: "alpine", + Name: "3.11", + }, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + OS: ftypes.OS{ + Family: ftypes.Alpine, + Name: "3.11", + }, + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []ftypes.Package{muslPkg}, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "alpine:latest (alpine 3.11)", + Class: types.ClassOSPkg, + Type: ftypes.Alpine, + Packages: ftypes.Packages{ + muslPkg, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-9999", + PkgName: muslPkg.Name, + PkgIdentifier: muslPkg.Identifier, + InstalledVersion: muslPkg.Version, + FixedVersion: "1.2.4", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-9999", + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: "HIGH", + }, + }, + }, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: true, + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + { + name: "happy path license scanner (exclude language-specific packages)", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.LicenseScanner}, + LicenseFull: true, + PkgTypes: []string{ + types.PkgTypeOS, + }, + LicenseCategories: map[ftypes.LicenseCategory][]string{ + ftypes.CategoryNotice: { + "MIT", + "GFDL-1.2-no-invariants", // License before normalization + // "GFDL-1.2-no-invariants-or-later", // License after normalization + }, + }, + }, + }, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + OS: ftypes.OS{ + Family: ftypes.Alpine, + Name: "3.11", + }, + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []ftypes.Package{ + muslPkg, + libunistring5Pkg, + }, + }, + }, + Applications: []ftypes.Application{ + { + Type: ftypes.PythonPkg, + FilePath: "", + Packages: []ftypes.Package{ + urllib3Pkg, + menuinstPkg, + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "OS Packages", + Class: types.ClassLicense, + Licenses: []types.DetectedLicense{ + { + Severity: "LOW", + Category: "notice", + PkgName: libunistring5Pkg.Name, + Name: "GFDL-NIV-1.2+", + Confidence: 1, + }, + { + Severity: "LOW", + Category: "notice", + PkgName: muslPkg.Name, + Name: "MIT", + Confidence: 1, + }, + }, + }, + { + Target: "Loose File License(s)", + Class: types.ClassLicenseFile, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: false, + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + { + name: "happy path license scanner (exclude OS packages)", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.LicenseScanner}, + LicenseFull: true, + PkgTypes: []string{ + types.PkgTypeLibrary, + }, + LicenseCategories: map[ftypes.LicenseCategory][]string{ + ftypes.CategoryNotice: { + "MIT", + "text://\\(c\\) 2015.*", + "text://.* 2016 Continuum.*", + }, + }, + }, + }, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + OS: ftypes.OS{ + Family: ftypes.Alpine, + Name: "3.11", + }, + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []ftypes.Package{ + muslPkg, + libunistring5Pkg, + }, + }, + }, + Applications: []ftypes.Application{ + { + Type: ftypes.GoModule, + FilePath: "/app/go.mod", + Packages: []ftypes.Package{ + uuidPkg, + }, + }, + { + Type: ftypes.PythonPkg, + FilePath: "", + Packages: []ftypes.Package{ + urllib3Pkg, + menuinstPkg, + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "/app/go.mod", + Class: types.ClassLicense, + Licenses: []types.DetectedLicense{ + { + Severity: "UNKNOWN", + Category: "unknown", + PkgName: uuidPkg.Name, + FilePath: "/app/go.mod", + Name: "LGPL", + Confidence: 1, + Link: "", + }, + }, + }, + { + Target: "Python", + Class: types.ClassLicense, + Licenses: []types.DetectedLicense{ + { + Severity: "LOW", + Category: "notice", + PkgName: urllib3Pkg.Name, + FilePath: "/usr/lib/python/site-packages/urllib3-3.2.1/METADATA", + Name: "CUSTOM License: (c) 2015 Continuum...", + Text: "(c) 2015 Continuum Analytics, Inc.", + Confidence: 1, + }, + { + Severity: "LOW", + Category: "notice", + PkgName: menuinstPkg.Name, + FilePath: "opt/conda/lib/python3.11/site-packages/menuinst-2.0.2.dist-info/METADATA", + Name: "CUSTOM License: (c) 2016 Continuum...", + Text: "(c) 2016 Continuum Analytics, Inc. / http://continuum.io All Rights Reserved", + Confidence: 1, + }, + }, + }, + { + Target: "Loose File License(s)", + Class: types.ClassLicenseFile, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: false, + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + { + name: "happy path with empty os", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{ + types.PkgTypeOS, + types.PkgTypeLibrary, + }, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + Applications: []ftypes.Application{ + { + Type: ftypes.Bundler, + FilePath: "/app1/Gemfile.lock", + Packages: []ftypes.Package{ + innocentPkg, // no vulnerability + }, + }, + { + Type: ftypes.Bundler, + FilePath: "/app2/Gemfile.lock", + Packages: []ftypes.Package{ + railsPkg, // one vulnerability + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "/app1/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: ftypes.Packages{ + innocentPkg, + }, + }, + { + Target: "/app2/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: ftypes.Packages{ + railsPkg, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2014-0081", + PkgName: railsPkg.Name, + PkgIdentifier: railsPkg.Identifier, + InstalledVersion: railsPkg.Version, + FixedVersion: "4.0.3, 3.2.17", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081", + Vulnerability: dbTypes.Vulnerability{ + Title: "xss", + Description: "xss vulnerability", + Severity: "MEDIUM", + References: []string{ + "http://example.com", + }, + LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)), + PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)), + }, + }, + }, + }, + }, + OS: ftypes.OS{}, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + { + name: "happy path, empty file paths (e.g. Scanned SBOM)", + args: args{ + target: "./result.cdx", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{types.PkgTypeLibrary}, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Applications: []ftypes.Application{ + { + Type: ftypes.Bundler, + FilePath: "", + Packages: []ftypes.Package{ + railsPkg, + }, + }, + { + Type: ftypes.Composer, + FilePath: "", + Packages: []ftypes.Package{ + laravelPkg, + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: []ftypes.Package{ + { + Name: railsPkg.Name, + Version: railsPkg.Version, + Identifier: railsPkg.Identifier, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2014-0081", + PkgName: railsPkg.Name, + PkgIdentifier: railsPkg.Identifier, + InstalledVersion: railsPkg.Version, + FixedVersion: "4.0.3, 3.2.17", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081", + Vulnerability: dbTypes.Vulnerability{ + Title: "xss", + Description: "xss vulnerability", + Severity: "MEDIUM", + References: []string{ + "http://example.com", + }, + LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)), + PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)), + }, + }, + }, + }, + { + Target: "", + Class: types.ClassLangPkg, + Type: ftypes.Composer, + Packages: []ftypes.Package{ + { + Name: laravelPkg.Name, + Version: laravelPkg.Version, + Identifier: laravelPkg.Identifier, + Relationship: ftypes.RelationshipDirect, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + }, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2021-21263", + PkgName: laravelPkg.Name, + PkgIdentifier: laravelPkg.Identifier, + InstalledVersion: laravelPkg.Version, + FixedVersion: "8.22.1, 7.30.3, 6.20.12", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + }, + }, + }, + }, + }, + }, + { + name: "happy path with no package", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{ + types.PkgTypeOS, + types.PkgTypeLibrary, + }, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + }, + Applications: []ftypes.Application{ + { + Type: "bundler", + FilePath: "/app/Gemfile.lock", + Packages: []ftypes.Package{ + railsPkg, + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "alpine:latest (alpine 3.11)", + Class: types.ClassOSPkg, + Type: ftypes.Alpine, + }, + { + Target: "/app/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: ftypes.Packages{ + railsPkg, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2014-0081", + PkgName: "rails", + PkgIdentifier: railsPkg.Identifier, + InstalledVersion: railsPkg.Version, + FixedVersion: "4.0.3, 3.2.17", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081", + Vulnerability: dbTypes.Vulnerability{ + Title: "xss", + Description: "xss vulnerability", + Severity: "MEDIUM", + References: []string{ + "http://example.com", + }, + LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)), + PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)), + }, + }, + }, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: true, + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + { + name: "happy path with unsupported os", + args: args{ + target: "fedora:27", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{ + types.PkgTypeOS, + types.PkgTypeLibrary, + }, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + OS: ftypes.OS{ + Family: "fedora", + Name: "27", + }, + Applications: []ftypes.Application{ + { + Type: ftypes.Bundler, + FilePath: "/app/Gemfile.lock", + Packages: []ftypes.Package{ + railsPkg, + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "/app/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: ftypes.Packages{railsPkg}, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2014-0081", + PkgName: railsPkg.Name, + PkgIdentifier: railsPkg.Identifier, + InstalledVersion: railsPkg.Version, + FixedVersion: "4.0.3, 3.2.17", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081", + Vulnerability: dbTypes.Vulnerability{ + Title: "xss", + Description: "xss vulnerability", + Severity: "MEDIUM", + References: []string{ + "http://example.com", + }, + LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)), + PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)), + }, + }, + }, + }, + }, + OS: ftypes.OS{ + Family: "fedora", + Name: "27", + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + { + name: "happy path with a scratch image", + args: args{ + target: "busybox:latest", + layerIDs: []string{"sha256:a6d503001157aedc826853f9b67f26d35966221b158bff03849868ae4a821116"}, + options: types.ScanOptions{ + PkgTypes: []string{ + types.PkgTypeOS, + types.PkgTypeLibrary, + }, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:a6d503001157aedc826853f9b67f26d35966221b158bff03849868ae4a821116", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + OS: ftypes.OS{}, + })) + return c + }, + want: types.ScanResponse{ + Results: nil, + }, + }, + { + name: "happy path with only language-specific package detection, excluding direct packages", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33"}, + options: types.ScanOptions{ + PkgTypes: []string{types.PkgTypeLibrary}, + PkgRelationships: []ftypes.Relationship{ + ftypes.RelationshipUnknown, + ftypes.RelationshipRoot, + ftypes.RelationshipIndirect, + }, + Scanners: types.Scanners{types.VulnerabilityScanner}, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + }, + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []ftypes.Package{muslPkg}, + }, + }, + Applications: []ftypes.Application{ + { + Type: "bundler", + FilePath: "/app/Gemfile.lock", + Packages: []ftypes.Package{ + railsPkg, + }, + }, + { + Type: "composer", + FilePath: "/app/composer-lock.json", + Packages: []ftypes.Package{ + laravelPkg, // will be excluded + guzzlePkg, + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "/app/Gemfile.lock", + Class: types.ClassLangPkg, + Type: ftypes.Bundler, + Packages: ftypes.Packages{railsPkg}, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2014-0081", + PkgName: railsPkg.Name, + PkgIdentifier: railsPkg.Identifier, + InstalledVersion: railsPkg.Version, + FixedVersion: "4.0.3, 3.2.17", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2014-0081", + Vulnerability: dbTypes.Vulnerability{ + Title: "xss", + Description: "xss vulnerability", + Severity: "MEDIUM", + References: []string{ + "http://example.com", + }, + LastModifiedDate: lo.ToPtr(time.Date(2020, 2, 1, 1, 1, 0, 0, time.UTC)), + PublishedDate: lo.ToPtr(time.Date(2020, 1, 1, 1, 1, 0, 0, time.UTC)), + }, + }, + }, + }, + { + Target: "/app/composer-lock.json", + Class: types.ClassLangPkg, + Type: ftypes.Composer, + Packages: ftypes.Packages{guzzlePkg}, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:0ea33a93585cf1917ba522b2304634c3073654062d5282c1346322967790ef33", + }, + }, + }, + }, + { + name: "happy path with hooks", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{types.PkgTypeOS}, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setUpHook: true, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + OS: ftypes.OS{ + Family: ftypes.Alpine, + Name: "3.11", + }, + PackageInfos: []ftypes.PackageInfo{ + { + FilePath: "lib/apk/db/installed", + Packages: []ftypes.Package{muslPkg}, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "alpine:latest (pre-scan) (alpine 3.11)", + Class: types.ClassOSPkg, + Type: ftypes.Alpine, + Packages: ftypes.Packages{ + muslPkg, + }, + Vulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-9999", + PkgName: muslPkg.Name, + PkgIdentifier: muslPkg.Identifier, + InstalledVersion: muslPkg.Version, + FixedVersion: "1.2.4", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-9999", + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: "HIGH", + References: []string{ + "https://example.com/post-scan", // modified by post-scan hook + }, + }, + }, + }, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: true, + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + { + name: "happy path with misconfigurations", + args: args{ + target: "/app/configs", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + Scanners: types.Scanners{types.MisconfigScanner}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303", + OS: ftypes.OS{ + Family: ftypes.Alpine, + Name: "3.11", + }, + Misconfigurations: []ftypes.Misconfiguration{ + { + FileType: ftypes.Kubernetes, + FilePath: "/app/configs/pod.yaml", + Warnings: []ftypes.MisconfResult{ + { + Namespace: "main.kubernetes.id300", + PolicyMetadata: ftypes.PolicyMetadata{ + ID: "ID300", + Type: "Kubernetes Security Check", + Title: "Bad Deployment", + Severity: "DUMMY", + }, + }, + }, + }, + { + FileType: ftypes.Kubernetes, + FilePath: "/app/configs/deployment.yaml", + Successes: []ftypes.MisconfResult{ + { + Namespace: "builtin.kubernetes.id200", + PolicyMetadata: ftypes.PolicyMetadata{ + ID: "ID200", + Type: "Kubernetes Security Check", + Title: "Bad Deployment", + Severity: "MEDIUM", + }, + }, + }, + Failures: ftypes.MisconfResults{ + { + Namespace: "main.kubernetes.id100", + Message: "something bad", + PolicyMetadata: ftypes.PolicyMetadata{ + ID: "ID100", + Type: "Kubernetes Security Check", + Title: "Bad Deployment", + Severity: "HIGH", + }, + }, + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "/app/configs/deployment.yaml", + Class: types.ClassConfig, + Type: ftypes.Kubernetes, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Kubernetes Security Check", + ID: "ID100", + Title: "Bad Deployment", + Message: "something bad", + Namespace: "main.kubernetes.id100", + Severity: "HIGH", + Status: types.MisconfStatusFailure, + Layer: ftypes.Layer{ + DiffID: "sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303", + }, + CauseMetadata: ftypes.CauseMetadata{ + Provider: "", + Service: "", + Code: ftypes.Code{}, + }, + }, + { + Type: "Kubernetes Security Check", + ID: "ID200", + Title: "Bad Deployment", + Message: "No issues found", + Namespace: "builtin.kubernetes.id200", + Severity: "MEDIUM", + PrimaryURL: "https://avd.aquasec.com/misconfig/id200", + References: []string{ + "https://avd.aquasec.com/misconfig/id200", + }, + Status: types.MisconfStatusPassed, + Layer: ftypes.Layer{ + DiffID: "sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303", + }, + CauseMetadata: ftypes.CauseMetadata{ + Provider: "", + Service: "", + Code: ftypes.Code{}, + }, + }, + }, + }, + { + Target: "/app/configs/pod.yaml", + Class: types.ClassConfig, + Type: ftypes.Kubernetes, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Type: "Kubernetes Security Check", + ID: "ID300", + Title: "Bad Deployment", + Message: "No issues found", + Namespace: "main.kubernetes.id300", + Severity: "MEDIUM", + Status: types.MisconfStatusFailure, + Layer: ftypes.Layer{ + DiffID: "sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303", + }, + }, + }, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: false, + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:9922bc15eeefe1637b803ef2106f178152ce19a391f24aec838cbe2e48e73303", + }, + }, + }, + }, + { + name: "sad path: ApplyLayers returns an error", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{ + types.PkgTypeOS, + types.PkgTypeLibrary, + }, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: 0, + })) + return c + }, + wantErr: "failed to apply layers", + }, + { + name: "sad path: library.Detect returns an error", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + PkgTypes: []string{types.PkgTypeLibrary}, + PkgRelationships: ftypes.Relationships, + Scanners: types.Scanners{types.VulnerabilityScanner}, + }, + }, + fixtures: []string{"testdata/fixtures/sad.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + }, + Applications: []ftypes.Application{ + { + Type: ftypes.Bundler, + FilePath: "/app/Gemfile.lock", + Packages: []ftypes.Package{ + { + Name: "rails", + Version: "6.0", + Layer: ftypes.Layer{ + DiffID: "sha256:9bdb2c849099a99c8ab35f6fd7469c623635e8f4479a0a5a3df61e22bae509f6", + }, + }, + }, + }, + }, + })) + return c + }, + wantErr: "failed to scan application libraries", + }, + { + name: "scan image history", + args: args{ + target: "alpine:latest", + layerIDs: []string{"sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10"}, + options: types.ScanOptions{ + ImageConfigScanners: types.Scanners{types.MisconfigScanner}, + }, + }, + fixtures: []string{"testdata/fixtures/happy.yaml"}, + setupCache: func(t *testing.T) cache.Cache { + c := cache.NewMemoryCache() + require.NoError(t, c.PutBlob(t.Context(), "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", ftypes.BlobInfo{ + SchemaVersion: ftypes.BlobJSONSchemaVersion, + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + OS: ftypes.OS{ + Family: ftypes.Alpine, + Name: "3.11", + }, + Misconfigurations: []ftypes.Misconfiguration{ + { + FileType: ftypes.Dockerfile, + FilePath: "Dockerfile", + Successes: ftypes.MisconfResults{ + { + Namespace: "builtin.dockerfile.DS001", + Query: "data.builtin.dockerfile.DS001.deny", + Message: "", + PolicyMetadata: ftypes.PolicyMetadata{ + ID: "DS001", + AVDID: "AVD-DS-0001", + Type: "Dockerfile Security Check", + Title: "':latest' tag used", + Description: "When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.", + Severity: "MEDIUM", + RecommendedActions: "Add a tag to the image in the 'FROM' statement", + }, + CauseMetadata: ftypes.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + Code: ftypes.Code{}, + }, + }, + }, + Failures: ftypes.MisconfResults{ + { + Namespace: "builtin.dockerfile.DS002", + Query: "data.builtin.dockerfile.DS002.deny", + Message: "Specify at least 1 USER command in Dockerfile with non-root user as argument", + PolicyMetadata: ftypes.PolicyMetadata{ + ID: "DS002", + AVDID: "AVD-DS-0002", + Type: "Dockerfile Security Check", + Title: "Image user should not be 'root'", + Description: "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", + Severity: "HIGH", + RecommendedActions: "Add 'USER ' line to the Dockerfile", + }, + CauseMetadata: ftypes.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + Code: ftypes.Code{}, + }, + }, + }, + }, + }, + })) + return c + }, + want: types.ScanResponse{ + Results: types.Results{ + { + Target: "Dockerfile", + Class: types.ClassConfig, + Type: ftypes.Dockerfile, + Misconfigurations: []types.DetectedMisconfiguration{ + { + Namespace: "builtin.dockerfile.DS002", + Query: "data.builtin.dockerfile.DS002.deny", + Message: "Specify at least 1 USER command in Dockerfile with non-root user as argument", + Type: "Dockerfile Security Check", + ID: "DS002", + AVDID: "AVD-DS-0002", + Title: "Image user should not be 'root'", + Description: "Running containers with 'root' user can lead to a container escape situation. It is a best practice to run containers as non-root users, which can be done by adding a 'USER' statement to the Dockerfile.", + Severity: "HIGH", + Resolution: "Add 'USER ' line to the Dockerfile", + Status: types.MisconfStatusFailure, + PrimaryURL: "https://avd.aquasec.com/misconfig/ds002", + References: []string{"https://avd.aquasec.com/misconfig/ds002"}, + CauseMetadata: ftypes.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + Code: ftypes.Code{}, + }, + Layer: ftypes.Layer{ + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + { + Namespace: "builtin.dockerfile.DS001", + Query: "data.builtin.dockerfile.DS001.deny", + Message: "No issues found", + Type: "Dockerfile Security Check", + ID: "DS001", + AVDID: "AVD-DS-0001", + Title: "':latest' tag used", + Description: "When using a 'FROM' statement you should use a specific tag to avoid uncontrolled behavior when the image is updated.", + Severity: "MEDIUM", + Resolution: "Add a tag to the image in the 'FROM' statement", + Status: types.MisconfStatusPassed, + CauseMetadata: ftypes.CauseMetadata{ + Provider: "Dockerfile", + Service: "general", + Code: ftypes.Code{}, + }, + PrimaryURL: "https://avd.aquasec.com/misconfig/ds001", + References: []string{"https://avd.aquasec.com/misconfig/ds001"}, + Layer: ftypes.Layer{ + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + }, + OS: ftypes.OS{ + Family: "alpine", + Name: "3.11", + Eosl: false, + }, + Layers: ftypes.Layers{ + { + Size: 1000, + DiffID: "sha256:5216338b40a7b96416b8b9858974bbe4acc3096ee60acbc4dfb1ee02aecceb10", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + if tt.setUpHook { + hooktest.Init(t) + } + + c := tt.setupCache(t) + a := applier.NewApplier(c) + s := NewService(a, ospkg.NewScanner(), langpkg.NewScanner(), vulnerability.NewClient(db.Config{})) + + gotResponse, err := s.Scan(t.Context(), tt.args.target, "", tt.args.layerIDs, tt.args.options) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr, tt.name) + return + } + + require.NoError(t, err, tt.name) + assert.Equal(t, tt.want, gotResponse) + }) + } +} diff --git a/pkg/scan/local/testdata/fixtures/happy.yaml b/pkg/scan/local/testdata/fixtures/happy.yaml new file mode 100644 index 000000000000..3b4d81b8e703 --- /dev/null +++ b/pkg/scan/local/testdata/fixtures/happy.yaml @@ -0,0 +1,50 @@ +- bucket: alpine 3.11 + pairs: + - bucket: musl + pairs: + - key: CVE-2020-9999 + value: + FixedVersion: 1.2.4 +- bucket: "rubygems::GitHub Security Advisory RubyGems" + pairs: + - bucket: rails + pairs: + - key: CVE-2014-0081 + value: + PatchedVersions: + - "4.0.3" + - "3.2.17" + VulnerableVersions: + - ">= 4.0.0, < 4.0.3" + - ">= 3.0.0, < 3.2.17" + +- bucket: "composer::GitHub Security Advisory Composer" + pairs: + - bucket: laravel/framework + pairs: + - key: CVE-2021-21263 + value: + PatchedVersions: + - 8.22.1 + - 7.30.3 + - 6.20.12 + VulnerableVersions: + - ">= 8.0.0, < 8.22.1" + - ">= 7.0.0, < 7.30.3" + - "< 6.20.12" +- bucket: vulnerability + pairs: + - key: CVE-2020-9999 + value: + Title: dos + Description: dos vulnerability + Severity: HIGH + - key: CVE-2014-0081 + value: + Title: xss + Description: xss vulnerability + Severity: MEDIUM + References: + - http://example.com + LastModifiedDate: "2020-02-01T01:01:00Z" + PublishedDate: "2020-01-01T01:01:00Z" \ No newline at end of file diff --git a/pkg/scan/local/testdata/fixtures/sad.yaml b/pkg/scan/local/testdata/fixtures/sad.yaml new file mode 100644 index 000000000000..61eb12aaf428 --- /dev/null +++ b/pkg/scan/local/testdata/fixtures/sad.yaml @@ -0,0 +1,15 @@ +- bucket: alpine 3.11 + pairs: + - bucket: musl + pairs: + - key: CVE-2020-9999 + value: + FixedVersion: + - "invalid" +- bucket: "rubygems::uby-advisory-db" + pairs: + - bucket: rails + pairs: + - key: CVE-2014-0081 + value: + PatchedVersions: "invalid" diff --git a/pkg/scan/ospkg/scan.go b/pkg/scan/ospkg/scan.go new file mode 100644 index 000000000000..060a4c230256 --- /dev/null +++ b/pkg/scan/ospkg/scan.go @@ -0,0 +1,71 @@ +package ospkg + +import ( + "context" + "fmt" + "sort" + + "golang.org/x/xerrors" + + ospkgDetector "github.com/aquasecurity/trivy/pkg/detector/ospkg" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" +) + +type Scanner interface { + Scan(ctx context.Context, target types.ScanTarget, options types.ScanOptions) (types.Result, bool, error) +} + +type scanner struct{} + +func NewScanner() Scanner { + return &scanner{} +} + +func (s *scanner) Scan(ctx context.Context, target types.ScanTarget, opts types.ScanOptions) (types.Result, bool, error) { + if !target.OS.Detected() { + log.Debug("Detected OS: unknown") + return types.Result{}, false, nil + } + log.Info("Detected OS", log.String("family", + string(target.OS.Family)), log.String("version", target.OS.Name)) + + // Skip OS package scanning if the target is expected not to have OS packages + if !target.OS.Family.HasOSPackages() { + log.Debug("Skipping OS package scanning", log.String("family", string(target.OS.Family))) + return types.Result{}, false, nil + } + + if target.OS.Extended { + // TODO: move the logic to each detector + target.OS.Name += "-ESM" + } + + result := types.Result{ + Target: fmt.Sprintf("%s (%s %s)", target.Name, target.OS.Family, target.OS.Name), + Class: types.ClassOSPkg, + Type: target.OS.Family, + } + + sort.Sort(target.Packages) + result.Packages = target.Packages + + if !opts.Scanners.Enabled(types.VulnerabilityScanner) { + // Return packages only + return result, false, nil + } + + detector, err := ospkgDetector.NewDetector(target) + if err != nil { + return result, false, xerrors.Errorf("unable to initialize Detector for OS packages: %w", err) + } + + vulns, eosl, err := detector.Detect(ctx) + if err != nil { + // Return a result for those who want to override the error handling. + return result, false, xerrors.Errorf("failed vulnerability detection of OS packages: %w", err) + } + result.Vulnerabilities = vulns + + return result, eosl, nil +} diff --git a/pkg/scan/service.go b/pkg/scan/service.go new file mode 100644 index 000000000000..cbfb58ca3e26 --- /dev/null +++ b/pkg/scan/service.go @@ -0,0 +1,186 @@ +package scan + +import ( + "context" + "crypto/sha256" + "fmt" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/fingerprint" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/report" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" + "github.com/aquasecurity/trivy/pkg/version/app" +) + +// Service is the main service that coordinates security scanning operations. +// It uses either local.Service or remote.Service as its backend implementation. +type Service struct { + backend Backend + artifact artifact.Artifact +} + +// Backend defines the interface for security scanning implementations. +// It can be either local.Service for standalone scanning or remote.Service +// for client/server mode scanning. The backend handles various types of +// security scanning including vulnerability, misconfiguration, secret, +// and license scanning. +type Backend interface { + Scan(ctx context.Context, target, artifactKey string, blobKeys []string, options types.ScanOptions) ( + response types.ScanResponse, err error) +} + +// NewService creates a new Service instance with the specified backend implementation +// and artifact handler. +func NewService(backend Backend, ar artifact.Artifact) Service { + return Service{ + backend: backend, + artifact: ar, + } +} + +// ScanArtifact performs security scanning on the specified artifact. +// It first inspects the artifact to gather necessary information, +// then delegates the actual scanning to the configured backend implementation. +func (s Service) ScanArtifact(ctx context.Context, options types.ScanOptions) (types.Report, error) { + artifactInfo, err := s.artifact.Inspect(ctx) + if err != nil { + return types.Report{}, xerrors.Errorf("failed analysis: %w", err) + } + defer func() { + if err := s.artifact.Clean(artifactInfo); err != nil { + log.Warn("Failed to clean the artifact", + log.String("artifact", artifactInfo.Name), log.Err(err)) + } + }() + + scanResponse, err := s.backend.Scan(ctx, artifactInfo.Name, artifactInfo.ID, artifactInfo.BlobIDs, options) + if err != nil { + return types.Report{}, xerrors.Errorf("scan failed: %w", err) + } + + ptros := &scanResponse.OS + if scanResponse.OS.Detected() && scanResponse.OS.Eosl { + log.Warn("This OS version is no longer supported by the distribution", + log.String("family", string(scanResponse.OS.Family)), log.String("version", scanResponse.OS.Name)) + log.Warn("The vulnerability detection may be insufficient because security updates are not provided") + } else if !scanResponse.OS.Detected() { + ptros = nil + } + + // We don't need to include CreatedBy into Report + for i := range scanResponse.Layers { + scanResponse.Layers[i].CreatedBy = "" + } + + reportID, err := uuid.NewV7() + if err != nil { + return types.Report{}, xerrors.Errorf("failed to generate ReportID: %w", err) + } + + r := types.Report{ + SchemaVersion: report.SchemaVersion, + Trivy: types.TrivyInfo{ + Version: app.Version(), + Server: scanResponse.ServerInfo, + }, + ReportID: reportID.String(), + CreatedAt: clock.Now(ctx), + ArtifactID: s.generateArtifactID(artifactInfo), + ArtifactName: artifactInfo.Name, + ArtifactType: artifactInfo.Type, + Metadata: types.Metadata{ + OS: ptros, + + // Container image + ImageID: artifactInfo.ImageMetadata.ID, + DiffIDs: artifactInfo.ImageMetadata.DiffIDs, + RepoTags: artifactInfo.ImageMetadata.RepoTags, + RepoDigests: artifactInfo.ImageMetadata.RepoDigests, + Reference: artifactInfo.ImageMetadata.Reference, + ImageConfig: artifactInfo.ImageMetadata.ConfigFile, + Size: scanResponse.Layers.TotalSize(), + Layers: lo.Ternary(len(scanResponse.Layers) > 0, scanResponse.Layers, nil), + + // Git repository + RepoURL: artifactInfo.RepoMetadata.RepoURL, + Branch: artifactInfo.RepoMetadata.Branch, + Tags: artifactInfo.RepoMetadata.Tags, + Commit: artifactInfo.RepoMetadata.Commit, + CommitMsg: artifactInfo.RepoMetadata.CommitMsg, + Author: artifactInfo.RepoMetadata.Author, + Committer: artifactInfo.RepoMetadata.Committer, + }, + Results: scanResponse.Results, + BOM: artifactInfo.BOM, + } + + // Fill fingerprints for all findings + fingerprint.Fill(&r) + + return r, nil +} + +// generateArtifactID generates a unique ID for the artifact based on its type +func (s Service) generateArtifactID(artifactInfo artifact.Reference) string { + switch artifactInfo.Type { + case ftypes.TypeContainerImage: + // For container images, calculate hash(ImageID + Registry + Repository) + // to ensure same images in different repos/registries have different IDs. + // Note: The artifact ID does NOT include the tag or digest, only registry/repository, + // so the same image with different tags will have the same artifact ID. + imageID := artifactInfo.ImageMetadata.ID + if imageID == "" { + return "" + } + + // Use the Reference field if available + ref := artifactInfo.ImageMetadata.Reference + if ref.IsZero() { + // Reference is empty when RepoTags and RepoDigests are both empty. + // This happens in the following cases: + // 1. Images built without tags (e.g., "docker build ." without -t flag) + // 2. Images saved by ID (e.g., "docker save " or "docker save sha256:xxx") + // In these cases, fall back to using the image ID directly. + log.Debug("No image reference available for artifact ID calculation, using image ID directly", + log.String("image", artifactInfo.Name)) + return imageID + } + + // ref.Context() returns registry/repository (e.g., "index.docker.io/library/alpine") + data := fmt.Sprintf("%s:%s", imageID, ref.Context()) + hash := sha256.Sum256([]byte(data)) + return fmt.Sprintf("sha256:%x", hash) + + case ftypes.TypeRepository: + // Generate ID from repository URL and commit hash combination + if artifactInfo.RepoMetadata.RepoURL != "" && artifactInfo.RepoMetadata.Commit != "" { + // Calculate SHA256 of URL + commit hash + data := artifactInfo.RepoMetadata.RepoURL + "@" + artifactInfo.RepoMetadata.Commit + hash := sha256.Sum256([]byte(data)) + return fmt.Sprintf("sha256:%x", hash) + } + // For local repositories without URL, use path and commit hash + if artifactInfo.RepoMetadata.Commit != "" { + data := artifactInfo.Name + "@" + artifactInfo.RepoMetadata.Commit + hash := sha256.Sum256([]byte(data)) + return fmt.Sprintf("sha256:%x", hash) + } + // Empty string for non-Git directories + return "" + + case ftypes.TypeFilesystem: + // Empty string for filesystem scans (as per requirement) + return "" + + default: + // Empty string for other types + return "" + } +} diff --git a/pkg/scan/service_test.go b/pkg/scan/service_test.go new file mode 100644 index 000000000000..388c52b5586a --- /dev/null +++ b/pkg/scan/service_test.go @@ -0,0 +1,466 @@ +package scan_test + +import ( + "testing" + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/cache" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/fanal/applier" + "github.com/aquasecurity/trivy/pkg/fanal/artifact" + image2 "github.com/aquasecurity/trivy/pkg/fanal/artifact/image" + "github.com/aquasecurity/trivy/pkg/fanal/image" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/scan" + "github.com/aquasecurity/trivy/pkg/scan/langpkg" + "github.com/aquasecurity/trivy/pkg/scan/local" + "github.com/aquasecurity/trivy/pkg/scan/ospkg" + tTypes "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" + "github.com/aquasecurity/trivy/pkg/vulnerability" +) + +func TestScanner_ScanArtifact(t *testing.T) { + type args struct { + options tTypes.ScanOptions + } + tests := []struct { + name string + args args + imagePath string + fixtures []string + want tTypes.Report + wantErr string + }{ + { + name: "happy path", + args: args{ + options: tTypes.ScanOptions{ + PkgTypes: []string{"os"}, + Scanners: tTypes.Scanners{tTypes.VulnerabilityScanner}, + PkgRelationships: ftypes.Relationships, + VulnSeveritySources: []dbTypes.SourceID{"auto"}, + }, + }, + imagePath: "../fanal/test/testdata/alpine-311.tar.gz", + fixtures: []string{"local/testdata/fixtures/happy.yaml"}, + want: tTypes.Report{ + SchemaVersion: 2, + Trivy: tTypes.TrivyInfo{Version: "dev"}, + CreatedAt: time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC), + ArtifactID: "sha256:574abdaf07824449b1277ec1e7e67659cc869bbf97fd95447812b55644350a21", // hash(ImageID:index.docker.io/library/alpine) from RepoTag alpine:3.11 + ArtifactName: "../fanal/test/testdata/alpine-311.tar.gz", + ArtifactType: ftypes.TypeContainerImage, + ReportID: "017b7d41-e09f-7000-80ea-000000000001", + Metadata: tTypes.Metadata{ + Size: 5861888, + OS: &ftypes.OS{ + Family: "alpine", + Name: "3.11.5", + Eosl: false, + Extended: false, + }, + ImageID: "sha256:a187dde48cd289ac374ad8539930628314bc581a481cdb41409c9289419ddb72", + DiffIDs: []string{ + "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + RepoTags: []string{"alpine:3.11"}, + Layers: ftypes.Layers{ + { + Size: 5861888, + DiffID: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + }, + Reference: testutil.MustParseReference(t, "alpine:3.11"), + ImageConfig: v1.ConfigFile{ + Architecture: "amd64", + Container: "fb71ddde5f6411a82eb056a9190f0cc1c80d7f77a8509ee90a2054428edb0024", + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)}, + DockerVersion: "18.09.7", + History: []v1.History{ + { + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 27725872, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) ADD file:0c4555f363c2672e350001f1293e689875a3760afe7b3f9146886afe67121cba in / ", + EmptyLayer: false, + }, + { + Created: v1.Time{Time: time.Date(2020, 3, 23, 21, 19, 34, 196162891, time.UTC)}, + CreatedBy: "/bin/sh -c #(nop) CMD [\"/bin/sh\"]", + EmptyLayer: true, + }, + }, + OS: "linux", + RootFS: v1.RootFS{ + Type: "layers", + DiffIDs: []v1.Hash{ + { + Algorithm: "sha256", + Hex: "beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + }, + }, + Config: v1.Config{ + Cmd: []string{"/bin/sh"}, + Env: []string{"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, + Image: "sha256:74df73bb19fbfc7fb5ab9a8234b3d98ee2fb92df5b824496679802685205ab8c", + ArgsEscaped: true, + }, + }, + }, + Results: tTypes.Results{ + { + Target: "../fanal/test/testdata/alpine-311.tar.gz (alpine 3.11.5)", + Class: tTypes.ClassOSPkg, + Type: "alpine", + Vulnerabilities: []tTypes.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2020-9999", + PkgName: "musl", + PkgID: "musl@1.1.24-r2", + InstalledVersion: "1.1.24-r2", + FixedVersion: "1.2.4", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-9999", + Fingerprint: "sha256:36d448cc18b4acd7ccc868fc1865f7dc97694d1e7e4fa55cfabec91990866926", // hash(sha256:574abdaf07824449b1277ec1e7e67659cc869bbf97fd95447812b55644350a21:../fanal/test/testdata/alpine-311.tar.gz (alpine 3.11.5):musl@1.1.24-r2:CVE-2020-9999) + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "4cdbcc57baa49752", + PURL: &packageurl.PackageURL{ + Type: "apk", + Namespace: "alpine", + Name: "musl", + Version: "1.1.24-r2", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "x86_64", + }, + { + Key: "distro", + Value: "3.11.5", + }, + }, + }, + }, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: "HIGH", + }, + }, + { + VulnerabilityID: "CVE-2020-9999", + PkgName: "musl-utils", + PkgID: "musl-utils@1.1.24-r2", + InstalledVersion: "1.1.24-r2", + FixedVersion: "1.2.4", + Status: dbTypes.StatusFixed, + Layer: ftypes.Layer{ + DiffID: "sha256:beee9f30bc1f711043e78d4a2be0668955d4b761d587d6f60c2c8dc081efb203", + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2020-9999", + Fingerprint: "sha256:5b28a2608ccc60c031066a4809cdb5c4ed7eb331e1136b413883c562a7e7aa55", // hash(sha256:574abdaf07824449b1277ec1e7e67659cc869bbf97fd95447812b55644350a21:../fanal/test/testdata/alpine-311.tar.gz (alpine 3.11.5):musl-utils@1.1.24-r2:CVE-2020-9999) + PkgIdentifier: ftypes.PkgIdentifier{ + UID: "9cb69455d0f6ae6a", + PURL: &packageurl.PackageURL{ + Type: "apk", + Namespace: "alpine", + Name: "musl-utils", + Version: "1.1.24-r2", + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "x86_64", + }, + { + Key: "distro", + Value: "3.11.5", + }, + }, + }, + }, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: "HIGH", + }, + }, + }, + }, + }, + }, + }, + { + name: "sad path: broken database", + args: args{ + options: tTypes.ScanOptions{ + PkgTypes: []string{"os"}, + Scanners: tTypes.Scanners{tTypes.VulnerabilityScanner}, + PkgRelationships: ftypes.Relationships, + }, + }, + imagePath: "../fanal/test/testdata/alpine-311.tar.gz", + fixtures: []string{"local/testdata/fixtures/sad.yaml"}, + wantErr: "failed to detect vulnerabilities", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set fake UUID v7 for testing + uuid.SetFakeUUIDV7(t, "017b7d41-e09f-7000-80ea-%012d") + + // Initialize DB + _ = dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + // Load test image + img, err := image.NewArchiveImage(tt.imagePath) + require.NoError(t, err) + + // Create artifact + c := cache.NewMemoryCache() + artifact, err := image2.NewArtifact(img, c, artifact.Option{}) + require.NoError(t, err) + + // Create scanner + applier := applier.NewApplier(c) + scanner := local.NewService(applier, ospkg.NewScanner(), langpkg.NewScanner(), vulnerability.NewClient(db.Config{})) + s := scan.NewService(scanner, artifact) + + ctx := clock.With(t.Context(), time.Date(2021, 8, 25, 12, 20, 30, 5, time.UTC)) + got, err := s.ScanArtifact(ctx, tt.args.options) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + for i := range got.Results { + got.Results[i].Packages = nil + } + assert.Equal(t, tt.want, got) + }) + } +} + +func TestService_generateArtifactID(t *testing.T) { + tests := []struct { + name string + artifactInfo artifact.Reference + want string + }{ + { + name: "container image with valid reference", + artifactInfo: artifact.Reference{ + Name: "ghcr.io/aquasecurity/trivy:latest", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:abc123", + Reference: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:latest"), + }, + }, + want: "sha256:58a3381def2cec86309c94be4fbeaca4b6c0231743ed1df9b0bea883a33cdebb", + }, + { + name: "same image with different tag should have same artifact ID", + artifactInfo: artifact.Reference{ + Name: "ghcr.io/aquasecurity/trivy:v0.65.0", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:abc123", + Reference: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy:v0.65.0"), + }, + }, + want: "sha256:58a3381def2cec86309c94be4fbeaca4b6c0231743ed1df9b0bea883a33cdebb", + }, + { + name: "different repository should have different artifact ID", + artifactInfo: artifact.Reference{ + Name: "ghcr.io/aqua-sec/trivy:v0.65.0", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:abc123", + Reference: testutil.MustParseReference(t, "ghcr.io/aqua-sec/trivy:v0.65.0"), + }, + }, + want: "sha256:bf73a838ae6a9d9c3018fbc7b628741f3be920b75c011a49c0b192736eb789b1", + }, + { + name: "different registry should have different artifact ID", + artifactInfo: artifact.Reference{ + Name: "docker.io/aquasecurity/trivy:v0.65.0", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:abc123", + Reference: testutil.MustParseReference(t, "docker.io/aquasecurity/trivy:v0.65.0"), + }, + }, + want: "sha256:dcba426e1fbd6e7fda125be3b9a2507ce3da2c7954c2edbf0e06e34d7f0ca22f", + }, + { + name: "docker.io implicit (no registry)", + artifactInfo: artifact.Reference{ + Name: "aquasecurity/trivy:latest", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:abc123", + Reference: testutil.MustParseReference(t, "aquasecurity/trivy:latest"), + }, + }, + want: "sha256:dcba426e1fbd6e7fda125be3b9a2507ce3da2c7954c2edbf0e06e34d7f0ca22f", + }, + { + name: "docker.io official image", + artifactInfo: artifact.Reference{ + Name: "alpine:3.10", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:alpine123", + Reference: testutil.MustParseReference(t, "alpine:3.10"), + }, + }, + want: "sha256:56de33d7ec6a1f832c9a7b2a26b1870efe79198e1c13ac645d43798c90954bb5", + }, + { + name: "localhost with port", + artifactInfo: artifact.Reference{ + Name: "localhost:5000/myapp:latest", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:local123", + Reference: testutil.MustParseReference(t, "localhost:5000/myapp:latest"), + }, + }, + want: "sha256:7cbf1bbde2285bac7c810fb76da5b0476d284f320f50b913987d6fc9226dc3e3", + }, + { + name: "multi-level repository", + artifactInfo: artifact.Reference{ + Name: "gcr.io/my-org/my-team/my-app:v1.0.0", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:gcr123", + Reference: testutil.MustParseReference(t, "gcr.io/my-org/my-team/my-app:v1.0.0"), + }, + }, + want: "sha256:edb01f579a800df17687439f1115bf4ced7bb977aa6afd468675ec56145a530c", + }, + { + name: "same image with different digest should have same artifact ID", + artifactInfo: artifact.Reference{ + Name: "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:abc123", + Reference: testutil.MustParseReference(t, "ghcr.io/aquasecurity/trivy@sha256:0000000000000000000000000000000000000000000000000000000000000000"), + }, + }, + want: "sha256:58a3381def2cec86309c94be4fbeaca4b6c0231743ed1df9b0bea883a33cdebb", + }, + { + name: "image with digest (no reference)", + artifactInfo: artifact.Reference{ + Name: "ghcr.io/aquasecurity/trivy@sha256:abc123", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:abc123", + // No reference for digest case (empty) + }, + }, + want: "sha256:abc123", + }, + { + name: "container image with no image ID", + artifactInfo: artifact.Reference{ + Name: "ghcr.io/aquasecurity/trivy:latest", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "", + // No reference + }, + }, + want: "", + }, + { + name: "container image with tar archive (uses RepoTag)", + artifactInfo: artifact.Reference{ + Name: "../fanal/test/testdata/alpine-311.tar.gz", + Type: ftypes.TypeContainerImage, + ImageMetadata: artifact.ImageMetadata{ + ID: "sha256:fallback123", + Reference: testutil.MustParseReference(t, "alpine:3.11"), + }, + }, + want: "sha256:a840c3e6bbadd213fee8cf6e4c32082f06541b8792a929fd373a57e5af0e8fa5", + }, + { + name: "repository with URL and commit", + artifactInfo: artifact.Reference{ + Name: "myrepo", + Type: ftypes.TypeRepository, + RepoMetadata: artifact.RepoMetadata{ + RepoURL: "https://github.com/aquasecurity/trivy", + Commit: "abc123def456", + }, + }, + want: "sha256:e23a8c4bae6c00f26ebf52d59e70ddfbbf5b2916d089239c3224f7f06371af98", + }, + { + name: "repository with only commit", + artifactInfo: artifact.Reference{ + Name: "/path/to/local/repo", + Type: ftypes.TypeRepository, + RepoMetadata: artifact.RepoMetadata{ + Commit: "abc123def456", + }, + }, + want: "sha256:9183de2823d60a525ed7aeabdb2cda775cba82dd5da0e94bb2fbba779ad399a7", + }, + { + name: "repository without commit", + artifactInfo: artifact.Reference{ + Name: "myrepo", + Type: ftypes.TypeRepository, + RepoMetadata: artifact.RepoMetadata{ + RepoURL: "https://github.com/aquasecurity/trivy", + }, + }, + want: "", + }, + { + name: "filesystem scan", + artifactInfo: artifact.Reference{ + Name: "/some/path", + Type: ftypes.TypeFilesystem, + }, + want: "", + }, + { + name: "unknown type", + artifactInfo: artifact.Reference{ + Name: "something", + Type: "unknown", + }, + want: "", + }, + } + + s := scan.Service{} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := s.GenerateArtifactID(tt.artifactInfo) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/scan/utils/utils.go b/pkg/scan/utils/utils.go new file mode 100644 index 000000000000..74b5397525b1 --- /dev/null +++ b/pkg/scan/utils/utils.go @@ -0,0 +1,29 @@ +package utils + +import ( + "fmt" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +// FormatVersion formats the package version based on epoch, version & release +func FormatVersion(pkg types.Package) string { + return formatVersion(pkg.Epoch, pkg.Version, pkg.Release) +} + +// FormatSrcVersion formats the package version based on source epoch, version & release +func FormatSrcVersion(pkg types.Package) string { + return formatVersion(pkg.SrcEpoch, pkg.SrcVersion, pkg.SrcRelease) +} + +func formatVersion(epoch int, version, release string) string { + v := version + if release != "" { + v = fmt.Sprintf("%s-%s", v, release) + } + if epoch != 0 { + v = fmt.Sprintf("%d:%s", epoch, v) + } + return v + +} diff --git a/pkg/scan/utils/utils_test.go b/pkg/scan/utils/utils_test.go new file mode 100644 index 000000000000..e4c28cebf811 --- /dev/null +++ b/pkg/scan/utils/utils_test.go @@ -0,0 +1,73 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +func TestFormatSrcVersion(t *testing.T) { + tests := []struct { + name string + pkg types.Package + want string + }{ + { + name: "happy path", + pkg: types.Package{ + SrcVersion: "1.2.3", + SrcRelease: "1", + }, + want: "1.2.3-1", + }, + { + name: "with epoch", + pkg: types.Package{ + SrcEpoch: 2, + SrcVersion: "1.2.3", + SrcRelease: "alpha", + }, + want: "2:1.2.3-alpha", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FormatSrcVersion(tt.pkg) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestFormatVersion(t *testing.T) { + tests := []struct { + name string + pkg types.Package + want string + }{ + { + name: "happy path", + pkg: types.Package{ + Version: "1.2.3", + Release: "1", + }, + want: "1.2.3-1", + }, + { + name: "with epoch", + pkg: types.Package{ + Epoch: 2, + Version: "1.2.3", + Release: "alpha", + }, + want: "2:1.2.3-alpha", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := FormatVersion(tt.pkg) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/scanner/library/bundler/advisory.go b/pkg/scanner/library/bundler/advisory.go deleted file mode 100644 index b93cae358272..000000000000 --- a/pkg/scanner/library/bundler/advisory.go +++ /dev/null @@ -1,122 +0,0 @@ -package bundler - -import ( - "fmt" - "io/ioutil" - "os" - "path/filepath" - - "github.com/etcd-io/bbolt" - - "github.com/knqyf263/trivy/pkg/db" - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/git" - "github.com/knqyf263/trivy/pkg/utils" - "gopkg.in/yaml.v2" -) - -const ( - dbURL = "https://github.com/rubysec/ruby-advisory-db.git" -) - -var ( - repoPath = filepath.Join(utils.CacheDir(), "ruby-advisory-db") -) - -type AdvisoryDB map[string][]Advisory - -type Advisory struct { - Gem string - Cve string - Osvdb string - Title string - Url string - Description string - CvssV2 float64 `yaml:"cvss_v2"` - CvssV3 float64 `yaml:"cvss_v3"` - PatchedVersions []string `yaml:"patched_versions"` - UnaffectedVersions []string `yaml:"unaffected_versions"` - Related Related -} - -type Related struct { - Cve []string - Url []string -} - -func (s *Scanner) UpdateDB() (err error) { - if _, err := git.CloneOrPull(dbURL, repoPath); err != nil { - return xerrors.Errorf("error in %s security DB update: %w", s.Type(), err) - } - s.db, err = s.walk() - return err -} - -func (s *Scanner) walk() (AdvisoryDB, error) { - advisoryDB := AdvisoryDB{} - root := filepath.Join(repoPath, "gems") - - var vulns []vulnerability.Vulnerability - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - return nil - } - buf, err := ioutil.ReadFile(path) - if err != nil { - return xerrors.Errorf("failed to read a file: %w", err) - } - - advisory := Advisory{} - err = yaml.Unmarshal(buf, &advisory) - if err != nil { - return xerrors.Errorf("failed to unmarshal YAML: %w", err) - } - - // for detecting vulnerabilities - advisories, ok := advisoryDB[advisory.Gem] - if !ok { - advisories = []Advisory{} - } - advisoryDB[advisory.Gem] = append(advisories, advisory) - - // for displaying vulnerability detail - var vulnerabilityID string - if advisory.Cve != "" { - vulnerabilityID = fmt.Sprintf("CVE-%s", advisory.Cve) - } else if advisory.Osvdb != "" { - vulnerabilityID = fmt.Sprintf("OSVDB-%s", advisory.Osvdb) - } - vulns = append(vulns, vulnerability.Vulnerability{ - ID: vulnerabilityID, - CvssScore: advisory.CvssV2, - CvssScoreV3: advisory.CvssV3, - References: append([]string{advisory.Url}, advisory.Related.Url...), - Title: advisory.Title, - Description: advisory.Description, - }) - - return nil - }) - if err != nil { - return nil, xerrors.Errorf("error in file walk: %w", err) - } - - if err = s.saveVulnerabilities(vulns); err != nil { - return nil, err - } - return advisoryDB, nil -} - -func (s *Scanner) saveVulnerabilities(vulns []vulnerability.Vulnerability) error { - return vulnerability.BatchUpdate(func(b *bbolt.Bucket) error { - for _, vuln := range vulns { - if err := db.Put(b, vuln.ID, vulnerability.RubySec, vuln); err != nil { - return xerrors.Errorf("failed to save %s vulnerability: %w", s.Type(), err) - } - } - return nil - }) -} diff --git a/pkg/scanner/library/bundler/scan.go b/pkg/scanner/library/bundler/scan.go deleted file mode 100644 index 1f7265056048..000000000000 --- a/pkg/scanner/library/bundler/scan.go +++ /dev/null @@ -1,67 +0,0 @@ -package bundler - -import ( - "fmt" - "os" - "strings" - - "github.com/knqyf263/go-dep-parser/pkg/bundler" - ptypes "github.com/knqyf263/go-dep-parser/pkg/types" - "github.com/knqyf263/go-version" - "github.com/knqyf263/trivy/pkg/scanner/utils" - "github.com/knqyf263/trivy/pkg/types" - "golang.org/x/xerrors" -) - -const ( - scannerType = "bundler" -) - -type Scanner struct { - db AdvisoryDB -} - -func NewScanner() *Scanner { - return &Scanner{} -} - -func (s *Scanner) Detect(pkgName string, pkgVer *version.Version) ([]types.Vulnerability, error) { - var vulns []types.Vulnerability - for _, advisory := range s.db[pkgName] { - if utils.MatchVersions(pkgVer, advisory.PatchedVersions) { - continue - } - if utils.MatchVersions(pkgVer, advisory.UnaffectedVersions) { - continue - } - - var vulnerabilityID string - if advisory.Cve != "" { - vulnerabilityID = fmt.Sprintf("CVE-%s", advisory.Cve) - } else if advisory.Osvdb != "" { - vulnerabilityID = fmt.Sprintf("OSVDB-%s", advisory.Osvdb) - } - - vuln := types.Vulnerability{ - VulnerabilityID: vulnerabilityID, - PkgName: strings.TrimSpace(advisory.Gem), - Title: strings.TrimSpace(advisory.Title), - InstalledVersion: pkgVer.String(), - FixedVersion: strings.Join(advisory.PatchedVersions, ", "), - } - vulns = append(vulns, vuln) - } - return vulns, nil -} - -func (s *Scanner) ParseLockfile(f *os.File) ([]ptypes.Library, error) { - libs, err := bundler.Parse(f) - if err != nil { - return nil, xerrors.Errorf("invalid Gemfile.lock format: %w", err) - } - return libs, nil -} - -func (s *Scanner) Type() string { - return scannerType -} diff --git a/pkg/scanner/library/composer/advisory.go b/pkg/scanner/library/composer/advisory.go deleted file mode 100644 index 39047942a42c..000000000000 --- a/pkg/scanner/library/composer/advisory.go +++ /dev/null @@ -1,103 +0,0 @@ -package composer - -import ( - "io/ioutil" - "os" - "path/filepath" - "strings" - - "github.com/etcd-io/bbolt" - - "github.com/knqyf263/trivy/pkg/db" - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/utils" - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/git" - "gopkg.in/yaml.v2" -) - -const ( - dbURL = "https://github.com/FriendsOfPHP/security-advisories" -) - -var ( - repoPath = filepath.Join(utils.CacheDir(), "php-security-advisories") -) - -type AdvisoryDB map[string][]Advisory - -type Advisory struct { - Cve string - Title string - Link string - Reference string - Branches map[string]Branch -} - -type Branch struct { - Versions []string -} - -func (s *Scanner) UpdateDB() (err error) { - if _, err := git.CloneOrPull(dbURL, repoPath); err != nil { - return err - } - s.db, err = s.walk() - return err -} - -func (s *Scanner) walk() (AdvisoryDB, error) { - advisoryDB := AdvisoryDB{} - var vulns []vulnerability.Vulnerability - err := filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error { - if info.IsDir() || !strings.HasPrefix(info.Name(), "CVE-") { - return nil - } - buf, err := ioutil.ReadFile(path) - if err != nil { - return err - } - - advisory := Advisory{} - err = yaml.Unmarshal(buf, &advisory) - if err != nil { - return err - } - - // for detecting vulnerabilities - advisories, ok := advisoryDB[advisory.Reference] - if !ok { - advisories = []Advisory{} - } - advisoryDB[advisory.Reference] = append(advisories, advisory) - - // for displaying vulnerability detail - vulns = append(vulns, vulnerability.Vulnerability{ - ID: advisory.Cve, - References: []string{advisory.Link}, - Title: advisory.Title, - }) - - return nil - }) - if err != nil { - return nil, xerrors.Errorf("error in file walk: %w", err) - } - if err = s.saveVulnerabilities(vulns); err != nil { - return nil, err - } - return advisoryDB, nil -} - -func (s Scanner) saveVulnerabilities(vulns []vulnerability.Vulnerability) error { - return vulnerability.BatchUpdate(func(b *bbolt.Bucket) error { - for _, vuln := range vulns { - if err := db.Put(b, vuln.ID, vulnerability.PhpSecurityAdvisories, vuln); err != nil { - return xerrors.Errorf("failed to save %s vulnerability: %w", s.Type(), err) - } - } - return nil - }) -} diff --git a/pkg/scanner/library/composer/scan.go b/pkg/scanner/library/composer/scan.go deleted file mode 100644 index 9658c4f78f84..000000000000 --- a/pkg/scanner/library/composer/scan.go +++ /dev/null @@ -1,69 +0,0 @@ -package composer - -import ( - "fmt" - "os" - "strings" - - "golang.org/x/xerrors" - - "github.com/knqyf263/go-dep-parser/pkg/composer" - ptypes "github.com/knqyf263/go-dep-parser/pkg/types" - "github.com/knqyf263/go-version" - "github.com/knqyf263/trivy/pkg/scanner/utils" - "github.com/knqyf263/trivy/pkg/types" -) - -const ( - scannerType = "composer" -) - -type Scanner struct { - db AdvisoryDB -} - -func NewScanner() *Scanner { - return &Scanner{} -} - -func (s *Scanner) Detect(pkgName string, pkgVer *version.Version) ([]types.Vulnerability, error) { - var vulns []types.Vulnerability - ref := fmt.Sprintf("composer://%s", pkgName) - for _, advisory := range s.db[ref] { - var affectedVersions []string - var patchedVersions []string - for _, branch := range advisory.Branches { - for _, version := range branch.Versions { - if !strings.HasPrefix(version, "<=") && strings.HasPrefix(version, "<") { - patchedVersions = append(patchedVersions, strings.Trim(version, "<")) - } - } - affectedVersions = append(affectedVersions, strings.Join(branch.Versions, ", ")) - } - - if !utils.MatchVersions(pkgVer, affectedVersions) { - continue - } - - vuln := types.Vulnerability{ - VulnerabilityID: advisory.Cve, - PkgName: pkgName, - Title: strings.TrimSpace(advisory.Title), - InstalledVersion: pkgVer.String(), - FixedVersion: strings.Join(patchedVersions, ", "), - } - vulns = append(vulns, vuln) - } - return vulns, nil -} - -func (s *Scanner) ParseLockfile(f *os.File) ([]ptypes.Library, error) { - libs, err := composer.Parse(f) - if err != nil { - return nil, xerrors.Errorf("invalid composer.lock format: %w", err) - } - return libs, nil -} -func (s *Scanner) Type() string { - return scannerType -} diff --git a/pkg/scanner/library/npm/advisory.go b/pkg/scanner/library/npm/advisory.go deleted file mode 100644 index 81807526600f..000000000000 --- a/pkg/scanner/library/npm/advisory.go +++ /dev/null @@ -1,122 +0,0 @@ -package npm - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/etcd-io/bbolt" - - "github.com/knqyf263/trivy/pkg/db" - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/utils" - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/git" -) - -const ( - dbURL = "https://github.com/nodejs/security-wg.git" -) - -var ( - repoPath = filepath.Join(utils.CacheDir(), "nodejs-security-wg") -) - -type AdvisoryDB map[string][]Advisory - -type Advisory struct { - ID int - Title string - ModuleName string `json:"module_name""` - Cves []string - VulnerableVersions string `json:"vulnerable_versions"` - PatchedVersions string `json:"patched_versions"` - Overview string - Recommendation string - References []string - CvssScoreNumber json.Number `json:"cvss_score"` - CvssScore float64 -} - -func (s *Scanner) UpdateDB() (err error) { - if _, err := git.CloneOrPull(dbURL, repoPath); err != nil { - return err - } - s.db, err = s.walk() - return err -} - -func (s *Scanner) walk() (AdvisoryDB, error) { - advisoryDB := AdvisoryDB{} - var vulns []vulnerability.Vulnerability - err := filepath.Walk(filepath.Join(repoPath, "vuln"), func(path string, info os.FileInfo, err error) error { - if info.IsDir() || !strings.HasSuffix(info.Name(), ".json") { - return nil - } - f, err := os.Open(path) - if err != nil { - return err - } - defer f.Close() - - advisory := Advisory{} - if err = json.NewDecoder(f).Decode(&advisory); err != nil { - return err - } - advisory.ModuleName = strings.ToLower(advisory.ModuleName) - - // `cvss_score` returns float or string like "4.8 (MEDIUM)" - s := strings.Split(advisory.CvssScoreNumber.String(), " ") - advisory.CvssScore, err = strconv.ParseFloat(s[0], 64) - if err != nil { - advisory.CvssScore = -1 - } - - // for detecting vulnerabilities - advisories, ok := advisoryDB[advisory.ModuleName] - if !ok { - advisories = []Advisory{} - } - advisoryDB[advisory.ModuleName] = append(advisories, advisory) - - // for displaying vulnerability detail - vulnerabilityIDs := advisory.Cves - if len(vulnerabilityIDs) == 0 { - vulnerabilityIDs = []string{fmt.Sprintf("NSWG-ECO-%d", advisory.ID)} - } - for _, vulnID := range vulnerabilityIDs { - vulns = append(vulns, vulnerability.Vulnerability{ - ID: vulnID, - CvssScore: advisory.CvssScore, - References: advisory.References, - Title: advisory.Title, - Description: advisory.Overview, - }) - } - - return nil - }) - if err != nil { - return nil, xerrors.Errorf("error in file walk: %w", err) - } - if err = s.saveVulnerabilities(vulns); err != nil { - return nil, err - } - return advisoryDB, nil -} - -func (s Scanner) saveVulnerabilities(vulns []vulnerability.Vulnerability) error { - return vulnerability.BatchUpdate(func(b *bbolt.Bucket) error { - for _, vuln := range vulns { - if err := db.Put(b, vuln.ID, vulnerability.NodejsSecurityWg, vuln); err != nil { - return xerrors.Errorf("failed to save %s vulnerability: %w", s.Type(), err) - } - } - return nil - }) -} diff --git a/pkg/scanner/library/npm/scan.go b/pkg/scanner/library/npm/scan.go deleted file mode 100644 index fbf50ec12fa7..000000000000 --- a/pkg/scanner/library/npm/scan.go +++ /dev/null @@ -1,82 +0,0 @@ -package npm - -import ( - "fmt" - "os" - "strings" - - "golang.org/x/xerrors" - - "github.com/knqyf263/go-dep-parser/pkg/npm" - ptypes "github.com/knqyf263/go-dep-parser/pkg/types" - "github.com/knqyf263/go-version" - "github.com/knqyf263/trivy/pkg/scanner/utils" - "github.com/knqyf263/trivy/pkg/types" -) - -const ( - scannerType = "npm" -) - -type Scanner struct { - db AdvisoryDB -} - -func NewScanner() *Scanner { - return &Scanner{} -} - -func (s *Scanner) Detect(pkgName string, pkgVer *version.Version) ([]types.Vulnerability, error) { - replacer := strings.NewReplacer(".alpha", "-alpha", ".beta", "-beta", ".rc", "-rc", " <", ", <", " >", ", >") - var vulns []types.Vulnerability - for _, advisory := range s.db[pkgName] { - // e.g. <= 2.15.0 || >= 3.0.0 <= 3.8.2 - // => {"<=2.15.0", ">= 3.0.0, <= 3.8.2"} - var vulnerableVersions []string - for _, version := range strings.Split(advisory.VulnerableVersions, " || ") { - version = strings.TrimSpace(version) - vulnerableVersions = append(vulnerableVersions, replacer.Replace(version)) - } - - if !utils.MatchVersions(pkgVer, vulnerableVersions) { - continue - } - - var patchedVersions []string - for _, version := range strings.Split(advisory.PatchedVersions, " || ") { - version = strings.TrimSpace(version) - patchedVersions = append(patchedVersions, replacer.Replace(version)) - } - - if utils.MatchVersions(pkgVer, patchedVersions) { - continue - } - - if len(advisory.Cves) == 0 { - advisory.Cves = []string{fmt.Sprintf("NSWG-ECO-%d", advisory.ID)} - } - - for _, cveID := range advisory.Cves { - vuln := types.Vulnerability{ - VulnerabilityID: cveID, - PkgName: pkgName, - Title: strings.TrimSpace(advisory.Title), - InstalledVersion: pkgVer.String(), - FixedVersion: strings.Join(patchedVersions, ", "), - } - vulns = append(vulns, vuln) - } - } - return vulns, nil -} - -func (s *Scanner) ParseLockfile(f *os.File) ([]ptypes.Library, error) { - libs, err := npm.Parse(f) - if err != nil { - return nil, xerrors.Errorf("invalid package-lock.json format: %w", err) - } - return libs, nil -} -func (s *Scanner) Type() string { - return scannerType -} diff --git a/pkg/scanner/library/pipenv/advisory.go b/pkg/scanner/library/pipenv/advisory.go deleted file mode 100644 index 52391e6839b8..000000000000 --- a/pkg/scanner/library/pipenv/advisory.go +++ /dev/null @@ -1,92 +0,0 @@ -package pipenv - -import ( - "encoding/json" - "os" - "path/filepath" - - "github.com/etcd-io/bbolt" - - "github.com/knqyf263/trivy/pkg/db" - - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/utils" - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/git" -) - -const ( - dbURL = "https://github.com/pyupio/safety-db.git" -) - -var ( - repoPath = filepath.Join(utils.CacheDir(), "python-safety-db") -) - -type AdvisoryDB map[string][]Advisory - -type Advisory struct { - ID string - Advisory string - Cve string - Specs []string - Version string `json:"v"` -} - -func (s *Scanner) UpdateDB() (err error) { - if _, err := git.CloneOrPull(dbURL, repoPath); err != nil { - return err - } - s.db, err = s.parse() - if err != nil { - return xerrors.Errorf("failed to parse python safety-db: %w", err) - } - return nil -} - -func (s *Scanner) parse() (AdvisoryDB, error) { - advisoryDB := AdvisoryDB{} - f, err := os.Open(filepath.Join(repoPath, "data", "insecure_full.json")) - if err != nil { - return nil, err - } - defer f.Close() - - // for detecting vulnerabilities - if err = json.NewDecoder(f).Decode(&advisoryDB); err != nil { - return nil, err - } - - // for displaying vulnerability detail - var vulns []vulnerability.Vulnerability - for _, advisories := range advisoryDB { - for _, advisory := range advisories { - vulnerabilityID := advisory.Cve - if vulnerabilityID == "" { - vulnerabilityID = advisory.ID - } - vulns = append(vulns, vulnerability.Vulnerability{ - ID: vulnerabilityID, - Title: advisory.Advisory, - }) - } - } - if err = s.saveVulnerabilities(vulns); err != nil { - return nil, err - } - - return advisoryDB, nil -} - -func (s Scanner) saveVulnerabilities(vulns []vulnerability.Vulnerability) error { - return vulnerability.BatchUpdate(func(b *bbolt.Bucket) error { - for _, vuln := range vulns { - if err := db.Put(b, vuln.ID, vulnerability.PythonSafetyDB, vuln); err != nil { - return xerrors.Errorf("failed to save %s vulnerability: %w", s.Type(), err) - } - } - return nil - }) -} diff --git a/pkg/scanner/library/pipenv/scan.go b/pkg/scanner/library/pipenv/scan.go deleted file mode 100644 index 78cc16bbcd8c..000000000000 --- a/pkg/scanner/library/pipenv/scan.go +++ /dev/null @@ -1,73 +0,0 @@ -package pipenv - -import ( - "os" - "strings" - - "golang.org/x/xerrors" - - "github.com/knqyf263/go-dep-parser/pkg/pipenv" - ptypes "github.com/knqyf263/go-dep-parser/pkg/types" - "github.com/knqyf263/go-version" - "github.com/knqyf263/trivy/pkg/scanner/utils" - "github.com/knqyf263/trivy/pkg/types" -) - -const ( - scannerType = "pipenv" -) - -type Scanner struct { - db AdvisoryDB -} - -func NewScanner() *Scanner { - return &Scanner{} -} - -func (s *Scanner) Detect(pkgName string, pkgVer *version.Version) ([]types.Vulnerability, error) { - var vulns []types.Vulnerability - for _, advisory := range s.db[pkgName] { - if !utils.MatchVersions(pkgVer, advisory.Specs) { - continue - } - - vulnerabilityID := advisory.Cve - if vulnerabilityID == "" { - vulnerabilityID = advisory.ID - } - - vuln := types.Vulnerability{ - VulnerabilityID: vulnerabilityID, - PkgName: pkgName, - Title: strings.TrimSpace(advisory.Advisory), - InstalledVersion: pkgVer.String(), - FixedVersion: createFixedVersions(advisory.Specs), - } - vulns = append(vulns, vuln) - } - return vulns, nil -} - -func createFixedVersions(specs []string) string { - var fixedVersions []string - for _, spec := range specs { - for _, s := range strings.Split(spec, ",") { - if !strings.HasPrefix(s, "<=") && strings.HasPrefix(s, "<") { - fixedVersions = append(fixedVersions, strings.TrimPrefix(s, "<")) - } - } - } - return strings.Join(fixedVersions, ", ") -} - -func (s *Scanner) ParseLockfile(f *os.File) ([]ptypes.Library, error) { - libs, err := pipenv.Parse(f) - if err != nil { - return nil, xerrors.Errorf("invalid Pipfile.lock format: %w", err) - } - return libs, nil -} -func (s *Scanner) Type() string { - return scannerType -} diff --git a/pkg/scanner/library/scan.go b/pkg/scanner/library/scan.go deleted file mode 100644 index 5f1e8b7d7fcb..000000000000 --- a/pkg/scanner/library/scan.go +++ /dev/null @@ -1,116 +0,0 @@ -package library - -import ( - "os" - "path/filepath" - - "github.com/knqyf263/fanal/analyzer" - _ "github.com/knqyf263/fanal/analyzer/library/bundler" - _ "github.com/knqyf263/fanal/analyzer/library/composer" - _ "github.com/knqyf263/fanal/analyzer/library/npm" - _ "github.com/knqyf263/fanal/analyzer/library/pipenv" - "github.com/knqyf263/fanal/extractor" - ptypes "github.com/knqyf263/go-dep-parser/pkg/types" - "github.com/knqyf263/go-version" - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/scanner/library/bundler" - "github.com/knqyf263/trivy/pkg/scanner/library/composer" - "github.com/knqyf263/trivy/pkg/scanner/library/npm" - "github.com/knqyf263/trivy/pkg/scanner/library/pipenv" - "github.com/knqyf263/trivy/pkg/types" - "golang.org/x/xerrors" -) - -type Scanner interface { - UpdateDB() error - ParseLockfile(*os.File) ([]ptypes.Library, error) - Detect(string, *version.Version) ([]types.Vulnerability, error) - Type() string -} - -func NewScanner(filename string) Scanner { - var scanner Scanner - switch filename { - case "Gemfile.lock": - scanner = bundler.NewScanner() - case "composer.lock": - scanner = composer.NewScanner() - case "package-lock.json": - scanner = npm.NewScanner() - case "Pipfile.lock": - scanner = pipenv.NewScanner() - default: - return nil - } - return scanner -} - -func Scan(files extractor.FileMap) (map[string][]types.Vulnerability, error) { - results, err := analyzer.GetLibraries(files) - if err != nil { - return nil, xerrors.Errorf("failed to analyze libraries: %w", err) - } - - vulnerabilities := map[string][]types.Vulnerability{} - for path, pkgs := range results { - log.Logger.Debugf("Detecting library vulnerabilities, path: %s", path) - scanner := NewScanner(filepath.Base(string(path))) - if scanner == nil { - return nil, xerrors.New("unknown file type") - } - - vulns, err := scan(scanner, pkgs) - if err != nil { - return nil, xerrors.Errorf("failed to scan %s vulnerabilities: %w", scanner.Type(), err) - } - - if len(vulns) != 0 { - vulnerabilities[string(path)] = vulns - } - } - return vulnerabilities, nil -} - -func ScanFile(f *os.File) ([]types.Vulnerability, error) { - scanner := NewScanner(f.Name()) - if scanner == nil { - return nil, xerrors.New("unknown file type") - } - - pkgs, err := scanner.ParseLockfile(f) - if err != nil { - return nil, err - } - - vulns, err := scan(scanner, pkgs) - if err != nil { - return nil, err - } - return vulns, nil -} - -func scan(scanner Scanner, pkgs []ptypes.Library) ([]types.Vulnerability, error) { - log.Logger.Infof("Updating %s Security DB...", scanner.Type()) - err := scanner.UpdateDB() - if err != nil { - return nil, xerrors.Errorf("failed to update %s advisories: %w", scanner.Type(), err) - } - - log.Logger.Infof("Detecting %s vulnerabilities...", scanner.Type()) - var vulnerabilities []types.Vulnerability - for _, pkg := range pkgs { - v, err := version.NewVersion(pkg.Version) - if err != nil { - log.Logger.Debug(err) - continue - } - - vulns, err := scanner.Detect(pkg.Name, v) - if err != nil { - return nil, xerrors.Errorf("failed to detect %s vulnerabilities: %w", scanner.Type(), err) - } - vulnerabilities = append(vulnerabilities, vulns...) - } - - return vulnerabilities, nil -} diff --git a/pkg/scanner/ospkg/alpine/alpine.go b/pkg/scanner/ospkg/alpine/alpine.go deleted file mode 100644 index 1785ae3f18f2..000000000000 --- a/pkg/scanner/ospkg/alpine/alpine.go +++ /dev/null @@ -1,57 +0,0 @@ -package alpine - -import ( - "strings" - - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/scanner/utils" - - "github.com/knqyf263/go-rpm-version" - - "github.com/knqyf263/trivy/pkg/vulnsrc/alpine" - - "github.com/knqyf263/fanal/analyzer" - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/types" -) - -type Scanner struct{} - -func NewScanner() *Scanner { - return &Scanner{} -} - -func (s *Scanner) Detect(osVer string, pkgs []analyzer.Package) ([]types.Vulnerability, error) { - log.Logger.Info("Detecting Alpine vulnerabilities...") - if strings.Count(osVer, ".") > 1 { - osVer = osVer[:strings.LastIndex(osVer, ".")] - } - log.Logger.Debugf("alpine: os version: %s", osVer) - log.Logger.Debugf("alpine: the number of packages: %s", len(pkgs)) - - var vulns []types.Vulnerability - for _, pkg := range pkgs { - advisories, err := alpine.Get(osVer, pkg.Name) - if err != nil { - return nil, xerrors.Errorf("failed to get alpine advisories: %w", err) - } - - installed := utils.FormatVersion(pkg) - installedVersion := version.NewVersion(installed) - - for _, adv := range advisories { - fixedVersion := version.NewVersion(adv.FixedVersion) - if installedVersion.LessThan(fixedVersion) { - vuln := types.Vulnerability{ - VulnerabilityID: adv.VulnerabilityID, - PkgName: pkg.Name, - InstalledVersion: installed, - FixedVersion: adv.FixedVersion, - } - vulns = append(vulns, vuln) - } - } - } - return vulns, nil -} diff --git a/pkg/scanner/ospkg/debian/debian.go b/pkg/scanner/ospkg/debian/debian.go deleted file mode 100644 index 1a075733afb1..000000000000 --- a/pkg/scanner/ospkg/debian/debian.go +++ /dev/null @@ -1,81 +0,0 @@ -package debian - -import ( - "strings" - - "golang.org/x/xerrors" - - "github.com/knqyf263/go-deb-version" - "github.com/knqyf263/trivy/pkg/scanner/utils" - - "github.com/knqyf263/fanal/analyzer" - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/types" - "github.com/knqyf263/trivy/pkg/vulnsrc/debian" - debianoval "github.com/knqyf263/trivy/pkg/vulnsrc/debian-oval" -) - -type Scanner struct{} - -func NewScanner() *Scanner { - return &Scanner{} -} - -func (s *Scanner) Detect(osVer string, pkgs []analyzer.Package) ([]types.Vulnerability, error) { - log.Logger.Info("Detecting Debian vulnerabilities...") - - if strings.Count(osVer, ".") > 0 { - osVer = osVer[:strings.Index(osVer, ".")] - } - log.Logger.Debugf("debian: os version: %s", osVer) - log.Logger.Debugf("debian: the number of packages: %s", len(pkgs)) - - var vulns []types.Vulnerability - for _, pkg := range pkgs { - if pkg.Type != analyzer.TypeSource { - continue - } - advisories, err := debianoval.Get(osVer, pkg.Name) - if err != nil { - return nil, xerrors.Errorf("failed to get debian OVAL: %w", err) - } - - installed := utils.FormatVersion(pkg) - installedVersion, err := version.NewVersion(installed) - if err != nil { - log.Logger.Debugf("failed to parse Debian installed package version: %w", err) - continue - } - - for _, adv := range advisories { - fixedVersion, err := version.NewVersion(adv.FixedVersion) - if err != nil { - log.Logger.Debugf("failed to parse Debian package version: %w", err) - continue - } - - if installedVersion.LessThan(fixedVersion) { - vuln := types.Vulnerability{ - VulnerabilityID: adv.VulnerabilityID, - PkgName: pkg.Name, - InstalledVersion: installed, - FixedVersion: adv.FixedVersion, - } - vulns = append(vulns, vuln) - } - } - advisories, err = debian.Get(osVer, pkg.Name) - if err != nil { - return nil, xerrors.Errorf("failed to get debian advisory: %w", err) - } - for _, adv := range advisories { - vuln := types.Vulnerability{ - VulnerabilityID: adv.VulnerabilityID, - PkgName: pkg.Name, - InstalledVersion: installed, - } - vulns = append(vulns, vuln) - } - } - return vulns, nil -} diff --git a/pkg/scanner/ospkg/redhat/redhat.go b/pkg/scanner/ospkg/redhat/redhat.go deleted file mode 100644 index f1e958ebc7ce..000000000000 --- a/pkg/scanner/ospkg/redhat/redhat.go +++ /dev/null @@ -1,56 +0,0 @@ -package redhat - -import ( - "strings" - - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/scanner/utils" - - "github.com/knqyf263/go-rpm-version" - - "github.com/knqyf263/fanal/analyzer" - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/types" - "github.com/knqyf263/trivy/pkg/vulnsrc/redhat" -) - -type Scanner struct{} - -func NewScanner() *Scanner { - return &Scanner{} -} - -func (s *Scanner) Detect(osVer string, pkgs []analyzer.Package) ([]types.Vulnerability, error) { - log.Logger.Info("Detecting RHEL/CentOS vulnerabilities...") - if strings.Count(osVer, ".") > 0 { - osVer = osVer[:strings.Index(osVer, ".")] - } - log.Logger.Debugf("redhat: os version: %s", osVer) - log.Logger.Debugf("redhat: the number of packages: %s", len(pkgs)) - - var vulns []types.Vulnerability - for _, pkg := range pkgs { - advisories, err := redhat.Get(osVer, pkg.Name) - if err != nil { - return nil, xerrors.Errorf("failed to get Red Hat advisories: %w", err) - } - - installed := utils.FormatVersion(pkg) - installedVersion := version.NewVersion(installed) - for _, adv := range advisories { - fixedVersion := version.NewVersion(adv.FixedVersion) - - if installedVersion.LessThan(fixedVersion) { - vuln := types.Vulnerability{ - VulnerabilityID: adv.VulnerabilityID, - PkgName: pkg.Name, - InstalledVersion: installed, - FixedVersion: adv.FixedVersion, - } - vulns = append(vulns, vuln) - } - } - } - return vulns, nil -} diff --git a/pkg/scanner/ospkg/scan.go b/pkg/scanner/ospkg/scan.go deleted file mode 100644 index 69a848da5d39..000000000000 --- a/pkg/scanner/ospkg/scan.go +++ /dev/null @@ -1,57 +0,0 @@ -package ospkg - -import ( - "github.com/knqyf263/fanal/analyzer" - fos "github.com/knqyf263/fanal/analyzer/os" - _ "github.com/knqyf263/fanal/analyzer/os/alpine" - _ "github.com/knqyf263/fanal/analyzer/os/debianbase" - _ "github.com/knqyf263/fanal/analyzer/os/redhatbase" - _ "github.com/knqyf263/fanal/analyzer/pkg/apk" - _ "github.com/knqyf263/fanal/analyzer/pkg/dpkg" - "github.com/knqyf263/fanal/extractor" - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/scanner/ospkg/alpine" - "github.com/knqyf263/trivy/pkg/scanner/ospkg/debian" - "github.com/knqyf263/trivy/pkg/scanner/ospkg/redhat" - "github.com/knqyf263/trivy/pkg/scanner/ospkg/ubuntu" - "github.com/knqyf263/trivy/pkg/types" - "golang.org/x/xerrors" -) - -type Scanner interface { - Detect(string, []analyzer.Package) ([]types.Vulnerability, error) -} - -func Scan(files extractor.FileMap) (string, string, []types.Vulnerability, error) { - os, err := analyzer.GetOS(files) - if err != nil { - return "", "", nil, xerrors.Errorf("failed to analyze OS: %w", err) - } - log.Logger.Debugf("OS family: %s, OS version: %s", os.Family, os.Name) - - var s Scanner - switch os.Family { - case fos.Alpine: - s = alpine.NewScanner() - case fos.Debian: - s = debian.NewScanner() - case fos.Ubuntu: - s = ubuntu.NewScanner() - case fos.RedHat, fos.CentOS: - s = redhat.NewScanner() - default: - return "", "", nil, xerrors.New("unsupported os") - } - pkgs, err := analyzer.GetPackages(files) - if err != nil { - return "", "", nil, xerrors.Errorf("failed to analyze OS packages: %w", err) - } - log.Logger.Debugf("the number of packages: %d", len(pkgs)) - - vulns, err := s.Detect(os.Name, pkgs) - if err != nil { - return "", "", nil, xerrors.Errorf("failed to detect vulnerabilities: %w", err) - } - - return os.Family, os.Name, vulns, nil -} diff --git a/pkg/scanner/ospkg/scan_bsd.go b/pkg/scanner/ospkg/scan_bsd.go deleted file mode 100644 index 69230da143b8..000000000000 --- a/pkg/scanner/ospkg/scan_bsd.go +++ /dev/null @@ -1,7 +0,0 @@ -// +build freebsd netbsd openbsd - -package ospkg - -import ( - _ "github.com/knqyf263/fanal/analyzer/pkg/rpmcmd" -) diff --git a/pkg/scanner/ospkg/scan_unix.go b/pkg/scanner/ospkg/scan_unix.go deleted file mode 100644 index a9172665617f..000000000000 --- a/pkg/scanner/ospkg/scan_unix.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build linux darwin - -package ospkg - -import ( - _ "github.com/knqyf263/fanal/analyzer/pkg/rpmcmd" - // TODO: Eliminate the dependency on "rpm" command - // _ "github.com/knqyf263/fanal/analyzer/pkg/rpm" -) diff --git a/pkg/scanner/ospkg/ubuntu/ubuntu.go b/pkg/scanner/ospkg/ubuntu/ubuntu.go deleted file mode 100644 index 0cf3a8ffe3ad..000000000000 --- a/pkg/scanner/ospkg/ubuntu/ubuntu.go +++ /dev/null @@ -1,64 +0,0 @@ -package ubuntu - -import ( - "github.com/knqyf263/go-deb-version" - "github.com/knqyf263/trivy/pkg/scanner/utils" - "golang.org/x/xerrors" - - "github.com/knqyf263/fanal/analyzer" - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/types" - "github.com/knqyf263/trivy/pkg/vulnsrc/ubuntu" -) - -type Scanner struct{} - -func NewScanner() *Scanner { - return &Scanner{} -} - -func (s *Scanner) Detect(osVer string, pkgs []analyzer.Package) ([]types.Vulnerability, error) { - log.Logger.Info("Detecting Ubuntu vulnerabilities...") - log.Logger.Debugf("ubuntu: os version: %s", osVer) - log.Logger.Debugf("ubuntu: the number of packages: %s", len(pkgs)) - - var vulns []types.Vulnerability - for _, pkg := range pkgs { - advisories, err := ubuntu.Get(osVer, pkg.Name) - if err != nil { - return nil, xerrors.Errorf("failed to get Ubuntu advisories: %w", err) - } - - installed := utils.FormatVersion(pkg) - installedVersion, err := version.NewVersion(installed) - if err != nil { - log.Logger.Debugf("failed to parse Ubuntu installed package version: %w", err) - continue - } - - for _, adv := range advisories { - vuln := types.Vulnerability{ - VulnerabilityID: adv.VulnerabilityID, - PkgName: pkg.Name, - InstalledVersion: installed, - FixedVersion: adv.FixedVersion, - } - - if adv.FixedVersion == "" { - vulns = append(vulns, vuln) - continue - } - - fixedVersion, err := version.NewVersion(adv.FixedVersion) - if err != nil { - log.Logger.Debugf("failed to parse Ubuntu package version: %w", err) - continue - } - - if installedVersion.LessThan(fixedVersion) { - vulns = append(vulns, vuln) - } - } - } - return vulns, nil -} diff --git a/pkg/scanner/scan.go b/pkg/scanner/scan.go deleted file mode 100644 index 8d186d533717..000000000000 --- a/pkg/scanner/scan.go +++ /dev/null @@ -1,238 +0,0 @@ -package scanner - -import ( - "context" - "flag" - "fmt" - "os" - "sort" - - "github.com/genuinetools/reg/registry" - - "github.com/knqyf263/trivy/pkg/log" - - "github.com/knqyf263/trivy/pkg/report" - - "github.com/knqyf263/trivy/pkg/scanner/library" - - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/types" - - "github.com/knqyf263/trivy/pkg/scanner/ospkg" - - "golang.org/x/crypto/ssh/terminal" - "golang.org/x/xerrors" - - "github.com/knqyf263/fanal/analyzer" - "github.com/knqyf263/fanal/extractor" -) - -var ( - sources = []string{vulnerability.Nvd, vulnerability.RedHat, vulnerability.Debian, - vulnerability.DebianOVAL, vulnerability.Alpine, vulnerability.RubySec, vulnerability.PhpSecurityAdvisories, - vulnerability.NodejsSecurityWg, vulnerability.PythonSafetyDB} -) - -func ScanImage(imageName, filePath string, severities []vulnerability.Severity, ignoreUnfixed bool) (report.Results, error) { - var results report.Results - var err error - ctx := context.Background() - - image, err := registry.ParseImage(imageName) - if err != nil { - return nil, xerrors.Errorf("invalid image: %w", err) - } - if image.Tag == "latest" { - log.Logger.Warn("You should avoid using the :latest tag as it is cached. You need to specify '--clean' option when :latest image is changed") - } - - var target string - var files extractor.FileMap - if imageName != "" { - target = imageName - files, err = analyzer.Analyze(ctx, imageName) - if err != nil { - return nil, xerrors.Errorf("failed to analyze image: %w", err) - } - } else if filePath != "" { - target = filePath - rc, err := openStream(filePath) - if err != nil { - return nil, xerrors.Errorf("failed to open stream: %w", err) - } - - files, err = analyzer.AnalyzeFromFile(ctx, rc) - if err != nil { - return nil, err - } - } else { - return nil, xerrors.New("image name or image file must be specified") - } - - osFamily, osVersion, osVulns, err := ospkg.Scan(files) - if err != nil { - return nil, xerrors.New("failed to scan image") - - } - - results = append(results, report.Result{ - FileName: fmt.Sprintf("%s (%s %s)", target, osFamily, osVersion), - Vulnerabilities: processVulnerabilties(osVulns, severities, ignoreUnfixed), - }) - - libVulns, err := library.Scan(files) - if err != nil { - return nil, xerrors.New("failed to scan libraries") - } - for path, vulns := range libVulns { - results = append(results, report.Result{ - FileName: path, - Vulnerabilities: processVulnerabilties(vulns, severities, ignoreUnfixed), - }) - } - - return results, nil -} - -func ScanFile(f *os.File, severities []vulnerability.Severity) (report.Result, error) { - vulns, err := library.ScanFile(f) - if err != nil { - return report.Result{}, xerrors.New("failed to scan libraries in file") - } - result := report.Result{ - FileName: f.Name(), - Vulnerabilities: processVulnerabilties(vulns, severities, false), - } - return result, nil -} - -func processVulnerabilties(vulns []types.Vulnerability, severities []vulnerability.Severity, ignoreUnfixed bool) []types.Vulnerability { - var vulnerabilities []types.Vulnerability - for _, vuln := range vulns { - sev, title, description, references := getDetail(vuln.VulnerabilityID) - - // Filter vulnerabilities by severity - for _, s := range severities { - if s == sev { - vuln.Severity = fmt.Sprint(sev) - vuln.Title = title - vuln.Description = description - vuln.References = references - - // Ignore unfixed vulnerabilities - if ignoreUnfixed && vuln.FixedVersion == "" { - continue - } - vulnerabilities = append(vulnerabilities, vuln) - break - } - } - } - sort.Slice(vulnerabilities, func(i, j int) bool { - if vulnerabilities[i].PkgName != vulnerabilities[j].PkgName { - return vulnerabilities[i].PkgName < vulnerabilities[j].PkgName - } - return vulnerability.CompareSeverityString(vulnerabilities[j].Severity, vulnerabilities[i].Severity) - }) - return vulnerabilities -} - -func openStream(path string) (*os.File, error) { - if path == "-" { - if terminal.IsTerminal(0) { - flag.Usage() - os.Exit(64) - } else { - return os.Stdin, nil - } - } - return os.Open(path) -} - -func getDetail(vulnID string) (vulnerability.Severity, string, string, []string) { - details, err := vulnerability.Get(vulnID) - if err != nil { - log.Logger.Debug(err) - return vulnerability.SeverityUnknown, "", "", nil - } else if len(details) == 0 { - return vulnerability.SeverityUnknown, "", "", nil - } - return getSeverity(details), getTitle(details), getDescription(details), getReferences(details) -} - -func getSeverity(details map[string]vulnerability.Vulnerability) vulnerability.Severity { - for _, source := range sources { - d, ok := details[source] - if !ok { - continue - } - if d.Severity != 0 { - return d.Severity - } else if d.SeverityV3 != 0 { - return d.SeverityV3 - } else if d.CvssScore > 0 { - return scoreToSeverity(d.CvssScore) - } else if d.CvssScoreV3 > 0 { - return scoreToSeverity(d.CvssScoreV3) - } - } - return vulnerability.SeverityUnknown -} - -func getTitle(details map[string]vulnerability.Vulnerability) string { - for _, source := range sources { - d, ok := details[source] - if !ok { - continue - } - if d.Title != "" { - return d.Title - } - } - return "" -} - -func getDescription(details map[string]vulnerability.Vulnerability) string { - for _, source := range sources { - d, ok := details[source] - if !ok { - continue - } - if d.Description != "" { - return d.Description - } - } - return "" -} - -func getReferences(details map[string]vulnerability.Vulnerability) []string { - references := map[string]struct{}{} - for _, source := range sources { - d, ok := details[source] - if !ok { - continue - } - for _, ref := range d.References { - references[ref] = struct{}{} - } - } - var refs []string - for ref := range references { - refs = append(refs, ref) - } - return refs -} - -func scoreToSeverity(score float64) vulnerability.Severity { - if score >= 9.0 { - return vulnerability.SeverityCritical - } else if score >= 7.0 { - return vulnerability.SeverityHigh - } else if score >= 4.0 { - return vulnerability.SeverityMedium - } else if score > 0.0 { - return vulnerability.SeverityLow - } - return vulnerability.SeverityUnknown -} diff --git a/pkg/scanner/utils/utils.go b/pkg/scanner/utils/utils.go deleted file mode 100644 index e11f430bc654..000000000000 --- a/pkg/scanner/utils/utils.go +++ /dev/null @@ -1,40 +0,0 @@ -package utils - -import ( - "fmt" - "strings" - - "github.com/knqyf263/fanal/analyzer" - - "github.com/knqyf263/go-version" - "github.com/knqyf263/trivy/pkg/log" -) - -var ( - replacer = strings.NewReplacer(".alpha", "-alpha", ".beta", "-beta", ".rc", "-rc") -) - -func MatchVersions(currentVersion *version.Version, rangeVersions []string) bool { - for _, p := range rangeVersions { - c, err := version.NewConstraint(replacer.Replace(p)) - if err != nil { - log.Logger.Debug("NewConstraint", "error", err) - return false - } - if c.Check(currentVersion) { - return true - } - } - return false -} - -func FormatVersion(pkg analyzer.Package) string { - v := pkg.Version - if pkg.Release != "" { - v = fmt.Sprintf("%s-%s", v, pkg.Release) - } - if pkg.Epoch != 0 { - v = fmt.Sprintf("%d:%s", pkg.Epoch, v) - } - return v -} diff --git a/pkg/semaphore/semaphore.go b/pkg/semaphore/semaphore.go new file mode 100644 index 000000000000..4f5a1e7459af --- /dev/null +++ b/pkg/semaphore/semaphore.go @@ -0,0 +1,14 @@ +package semaphore + +import "golang.org/x/sync/semaphore" + +const defaultSize = 5 + +type Weighted = semaphore.Weighted + +func New(parallel int) *Weighted { + if parallel == 0 { + parallel = defaultSize + } + return semaphore.NewWeighted(int64(parallel)) +} diff --git a/pkg/set/case_insensitive.go b/pkg/set/case_insensitive.go new file mode 100644 index 000000000000..8ac26b17cbc7 --- /dev/null +++ b/pkg/set/case_insensitive.go @@ -0,0 +1,112 @@ +package set + +import ( + "iter" + "maps" + "slices" + "strings" +) + +// caseInsensitiveStringSet represents a case-insensitive string set implementation +// It stores strings with case-insensitive comparison while preserving the original casing +// of the first occurrence of each unique string (case-insensitive). +// The map key is the lowercase version, and the value is the original string. +// WARNING: This implementation is not thread-safe +type caseInsensitiveStringSet map[string]string //nolint: gocritic + +// NewCaseInsensitive creates a new empty case-insensitive string set with optional initial values +// The first occurrence of each unique string (case-insensitive) will be preserved. +// For example: NewCaseInsensitive("Hello", "HELLO", "world") will contain "Hello" and "world" +func NewCaseInsensitive(values ...string) Set[string] { + s := make(caseInsensitiveStringSet, len(values)) + s.Append(values...) + return s +} + +// Append adds multiple items to the set and returns the new size +// If an item already exists (case-insensitive), it will not be added again +// and the original casing is preserved +func (s caseInsensitiveStringSet) Append(values ...string) int { + for _, v := range values { + key := strings.ToLower(v) + if _, exists := s[key]; !exists { + s[key] = v + } + } + return len(s) +} + +// Remove removes an item from the set (case-insensitive) +func (s caseInsensitiveStringSet) Remove(item string) { + delete(s, strings.ToLower(item)) +} + +// Contains checks if an item exists in the set (case-insensitive) +func (s caseInsensitiveStringSet) Contains(item string) bool { + _, exists := s[strings.ToLower(item)] + return exists +} + +// Find returns the stored value with its original casing +func (s caseInsensitiveStringSet) Find(item string) (string, bool) { + value, exists := s[strings.ToLower(item)] + return value, exists +} + +// Size returns the number of items in the set +func (s caseInsensitiveStringSet) Size() int { + return len(s) +} + +// Clear removes all items from the set +func (s caseInsensitiveStringSet) Clear() { + clear(s) +} + +// Clone returns a new set with a copy of all items +func (s caseInsensitiveStringSet) Clone() Set[string] { + return maps.Clone(s) +} + +// Items returns all items in the set as a slice with their original casing +func (s caseInsensitiveStringSet) Items() []string { + return slices.Collect(s.Iter()) +} + +// Iter returns an iterator over the set values with their original casing +func (s caseInsensitiveStringSet) Iter() iter.Seq[string] { + return maps.Values(s) +} + +// Union returns a new case-insensitive set containing all items from both sets +// If the same item (case-insensitive) exists in both sets, the casing from this set is preserved +func (s caseInsensitiveStringSet) Union(other Set[string]) Set[string] { + result := make(caseInsensitiveStringSet, s.Size()+other.Size()) + maps.Copy(result, s) + result.Append(other.Items()...) + return result +} + +// Intersection returns a new case-insensitive set containing items present in both sets +// The casing from this set is preserved for matching items +func (s caseInsensitiveStringSet) Intersection(other Set[string]) Set[string] { + result := make(caseInsensitiveStringSet) + for _, v := range s { + if other.Contains(v) { + result.Append(v) + } + } + return result +} + +// Difference returns a new case-insensitive set containing items present in this set but not in the other +// The casing from this set is preserved +func (s caseInsensitiveStringSet) Difference(other Set[string]) Set[string] { + result := make(caseInsensitiveStringSet) + for _, v := range s { + if !other.Contains(v) { + result.Append(v) + } + } + return result +} diff --git a/pkg/set/case_insensitive_test.go b/pkg/set/case_insensitive_test.go new file mode 100644 index 000000000000..851b9fda6414 --- /dev/null +++ b/pkg/set/case_insensitive_test.go @@ -0,0 +1,500 @@ +package set_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/set" +) + +func TestNewCaseInsensitive(t *testing.T) { + tests := []struct { + name string + values []string + want []string + desc string + }{ + { + name: "empty set", + values: []string{}, + want: []string{}, + desc: "should create empty set when no values provided", + }, + { + name: "single value", + values: []string{"Hello"}, + want: []string{"Hello"}, + desc: "should create set with single value", + }, + { + name: "multiple values", + values: []string{"Hello", "World", "Test"}, + want: []string{"Hello", "World", "Test"}, + desc: "should create set with multiple values", + }, + { + name: "case insensitive duplicates", + values: []string{"Hello", "HELLO", "hello", "HeLLo"}, + want: []string{"Hello"}, + desc: "should treat case variations as duplicates and preserve first occurrence", + }, + { + name: "mixed case duplicates", + values: []string{"Test", "TEST", "test", "World", "WORLD"}, + want: []string{"Test", "World"}, + desc: "should treat case variations as duplicates across multiple strings and preserve first occurrences", + }, + { + name: "empty strings", + values: []string{"", "test", ""}, + want: []string{"", "test"}, + desc: "should handle empty strings and treat duplicates correctly", + }, + { + name: "unicode strings", + values: []string{"こんにちは", "世界", "こんにちは"}, + want: []string{"こんにちは", "世界"}, + desc: "should handle unicode strings correctly", + }, + { + name: "strings with spaces", + values: []string{"Hello World", "hello world", "HELLO WORLD"}, + want: []string{"Hello World"}, + desc: "should handle strings with spaces case-insensitively and preserve original spacing", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.NewCaseInsensitive(tt.values...) + assert.ElementsMatch(t, tt.want, s.Items(), "unexpected set contents") + }) + } +} + +func TestCaseInsensitiveSet_Append(t *testing.T) { + tests := []struct { + name string + initial []string + append []string + want []string + }{ + { + name: "append to empty set", + initial: []string{}, + append: []string{"Hello", "World"}, + want: []string{"Hello", "World"}, + }, + { + name: "append case variations", + initial: []string{"Hello"}, + append: []string{"HELLO", "hello"}, + want: []string{"Hello"}, + }, + { + name: "append new and existing", + initial: []string{"Hello"}, + append: []string{"HELLO", "World"}, + want: []string{"Hello", "World"}, + }, + { + name: "append empty slice", + initial: []string{"Hello"}, + append: []string{}, + want: []string{"Hello"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.NewCaseInsensitive(tt.initial...) + got := s.Append(tt.append...) + + assert.Equal(t, len(tt.want), got, "unexpected returned size") + assert.ElementsMatch(t, tt.want, s.Items(), "unexpected set contents") + }) + } +} + +func TestCaseInsensitiveSet_Contains(t *testing.T) { + tests := []struct { + name string + initial []string + check string + want bool + }{ + { + name: "exact match", + initial: []string{"Hello"}, + check: "Hello", + want: true, + }, + { + name: "lowercase match", + initial: []string{"Hello"}, + check: "hello", + want: true, + }, + { + name: "uppercase match", + initial: []string{"Hello"}, + check: "HELLO", + want: true, + }, + { + name: "mixed case match", + initial: []string{"Hello"}, + check: "HeLLo", + want: true, + }, + { + name: "not found", + initial: []string{"Hello"}, + check: "World", + want: false, + }, + { + name: "empty string exists", + initial: []string{""}, + check: "", + want: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.NewCaseInsensitive(tt.initial...) + got := s.Contains(tt.check) + assert.Equal(t, tt.want, got, "unexpected contains result") + }) + } +} + +func TestCaseInsensitiveSet_Remove(t *testing.T) { + tests := []struct { + name string + initial []string + remove string + wantSize int + }{ + { + name: "remove exact match", + initial: []string{"Hello", "World"}, + remove: "Hello", + wantSize: 1, + }, + { + name: "remove with different case", + initial: []string{"Hello", "World"}, + remove: "hello", + wantSize: 1, + }, + { + name: "remove uppercase", + initial: []string{"Hello", "World"}, + remove: "WORLD", + wantSize: 1, + }, + { + name: "remove non-existing", + initial: []string{"Hello"}, + remove: "World", + wantSize: 1, + }, + { + name: "remove from empty set", + initial: []string{}, + remove: "Hello", + wantSize: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.NewCaseInsensitive(tt.initial...) + s.Remove(tt.remove) + got := s.Size() + assert.Equal(t, tt.wantSize, got, "unexpected set size after remove") + assert.False(t, s.Contains(tt.remove), "set should not contain removed item") + }) + } +} + +func TestCaseInsensitiveSet_Clear(t *testing.T) { + tests := []struct { + name string + initial []string + }{ + { + name: "clear non-empty set", + initial: []string{"Hello", "World", "Test"}, + }, + { + name: "clear empty set", + initial: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.NewCaseInsensitive(tt.initial...) + s.Clear() + + assert.Zero(t, s.Size(), "set should be empty after clear") + assert.Empty(t, s.Items(), "items should be empty after clear") + }) + } +} + +func TestCaseInsensitiveSet_Clone(t *testing.T) { + t.Run("empty set", func(t *testing.T) { + original := set.NewCaseInsensitive() + cloned := original.Clone() + + assert.Equal(t, 0, cloned.Size(), "cloned set should be empty") + + // Verify independence + original.Append("test") + assert.False(t, cloned.Contains("test"), "cloned set should not be affected by original") + }) + + t.Run("non-empty set", func(t *testing.T) { + original := set.NewCaseInsensitive("Hello", "World") + cloned := original.Clone() + + assert.Equal(t, original.Size(), cloned.Size(), "sizes should match") + assert.True(t, cloned.Contains("hello"), "should contain hello (case insensitive)") + assert.True(t, cloned.Contains("WORLD"), "should contain world (case insensitive)") + + // Verify independence + original.Append("new") + assert.False(t, cloned.Contains("new"), "cloned set should not be affected by original") + cloned.Append("another") + assert.False(t, original.Contains("another"), "original set should not be affected by clone") + }) + + t.Run("preserves casing", func(t *testing.T) { + original := set.NewCaseInsensitive("Hello", "WORLD") + cloned := original.Clone() + + assert.ElementsMatch(t, original.Items(), cloned.Items(), "cloned set should preserve original casing") + }) +} + +func TestCaseInsensitiveSet_Union(t *testing.T) { + tests := []struct { + name string + set1 []string + set2 []string + want []string + }{ + { + name: "non-overlapping sets", + set1: []string{"Hello", "World"}, + set2: []string{"Test", "Data"}, + want: []string{"Hello", "World", "Test", "Data"}, + }, + { + name: "overlapping sets with same case", + set1: []string{"Hello", "World"}, + set2: []string{"World", "Test"}, + want: []string{"Hello", "World", "Test"}, + }, + { + name: "overlapping sets with different case", + set1: []string{"Hello", "World"}, + set2: []string{"HELLO", "test"}, + want: []string{"Hello", "World", "test"}, + }, + { + name: "union with empty set", + set1: []string{"Hello"}, + set2: []string{}, + want: []string{"Hello"}, + }, + { + name: "empty sets union", + set1: []string{}, + set2: []string{}, + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s1 := set.NewCaseInsensitive(tt.set1...) + s2 := set.NewCaseInsensitive(tt.set2...) + + result := s1.Union(s2) + got := result.Items() + + assert.ElementsMatch(t, tt.want, got, "unexpected union result") + }) + } +} + +func TestCaseInsensitiveSet_Intersection(t *testing.T) { + tests := []struct { + name string + set1 []string + set2 []string + want []string + }{ + { + name: "overlapping sets with same case", + set1: []string{"Hello", "World", "Test"}, + set2: []string{"World", "Test", "Data"}, + want: []string{"World", "Test"}, + }, + { + name: "overlapping sets with different case", + set1: []string{"Hello", "World"}, + set2: []string{"hello", "WORLD"}, + want: []string{"Hello", "World"}, + }, + { + name: "non-overlapping sets", + set1: []string{"Hello"}, + set2: []string{"World"}, + want: []string{}, + }, + { + name: "intersection with empty set", + set1: []string{"Hello"}, + set2: []string{}, + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s1 := set.NewCaseInsensitive(tt.set1...) + s2 := set.NewCaseInsensitive(tt.set2...) + + result := s1.Intersection(s2) + got := result.Items() + + assert.ElementsMatch(t, tt.want, got, "unexpected intersection result") + }) + } +} + +func TestCaseInsensitiveSet_Difference(t *testing.T) { + tests := []struct { + name string + set1 []string + set2 []string + want []string + }{ + { + name: "difference with same case", + set1: []string{"Hello", "World", "Test"}, + set2: []string{"World", "Data"}, + want: []string{"Hello", "Test"}, + }, + { + name: "difference with different case", + set1: []string{"Hello", "World", "Test"}, + set2: []string{"hello", "WORLD"}, + want: []string{"Test"}, + }, + { + name: "difference with non-overlapping set", + set1: []string{"Hello", "World"}, + set2: []string{"Test", "Data"}, + want: []string{"Hello", "World"}, + }, + { + name: "difference with empty set", + set1: []string{"Hello", "World"}, + set2: []string{}, + want: []string{"Hello", "World"}, + }, + { + name: "difference of empty set", + set1: []string{}, + set2: []string{"Hello"}, + want: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s1 := set.NewCaseInsensitive(tt.set1...) + s2 := set.NewCaseInsensitive(tt.set2...) + + result := s1.Difference(s2) + got := result.Items() + + assert.ElementsMatch(t, tt.want, got, "unexpected difference result") + }) + } +} + +func TestCaseInsensitiveSet_Find(t *testing.T) { + tests := []struct { + name string + set set.Set[string] + lookup string + wantValue string + wantFound bool + }{ + { + name: "exact match", + set: set.NewCaseInsensitive("Hello", "World"), + lookup: "Hello", + wantValue: "Hello", + wantFound: true, + }, + { + name: "lowercase lookup", + set: set.NewCaseInsensitive("Hello", "World"), + lookup: "hello", + wantValue: "Hello", + wantFound: true, + }, + { + name: "uppercase lookup", + set: set.NewCaseInsensitive("Hello", "World"), + lookup: "HELLO", + wantValue: "Hello", + wantFound: true, + }, + { + name: "mixed case lookup", + set: set.NewCaseInsensitive("Hello", "World"), + lookup: "hElLo", + wantValue: "Hello", + wantFound: true, + }, + { + name: "not found", + set: set.NewCaseInsensitive("Hello", "World"), + lookup: "Foo", + wantValue: "", + wantFound: false, + }, + { + name: "preserves first casing", + set: set.NewCaseInsensitive("TCL", "Tcl"), + lookup: "tcl", + wantValue: "TCL", + wantFound: true, + }, + { + name: "empty set", + set: set.NewCaseInsensitive(), + lookup: "test", + wantValue: "", + wantFound: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValue, gotFound := tt.set.Find(tt.lookup) + assert.Equal(t, tt.wantValue, gotValue) + assert.Equal(t, tt.wantFound, gotFound) + }) + } +} diff --git a/pkg/set/set.go b/pkg/set/set.go new file mode 100644 index 000000000000..37f8d1fc97d4 --- /dev/null +++ b/pkg/set/set.go @@ -0,0 +1,43 @@ +package set + +import "iter" + +// Set defines the interface for set operations +type Set[T comparable] interface { + // Append adds multiple items to the set and returns the new size + Append(val ...T) int + + // Remove removes an item from the set + Remove(item T) + + // Contains checks if an item exists in the set + Contains(item T) bool + + // Find returns the stored value for the given item if it exists + // For case-insensitive sets, this returns the canonical (original) casing + Find(item T) (T, bool) + + // Size returns the number of items in the set + Size() int + + // Clear removes all items from the set + Clear() + + // Clone returns a new set with a copy of all items + Clone() Set[T] + + // Items returns all items in the set as a slice + Items() []T + + // Iter returns an iterator over the set + Iter() iter.Seq[T] + + // Union returns a new set containing all items from both sets + Union(other Set[T]) Set[T] + + // Intersection returns a new set containing items present in both sets + Intersection(other Set[T]) Set[T] + + // Difference returns a new set containing items present in this set but not in the other + Difference(other Set[T]) Set[T] +} diff --git a/pkg/set/unsafe.go b/pkg/set/unsafe.go new file mode 100644 index 000000000000..9ebd5fcf69b5 --- /dev/null +++ b/pkg/set/unsafe.go @@ -0,0 +1,106 @@ +package set + +import ( + "iter" + "maps" + "slices" +) + +// unsafeSet represents a non-thread-safe set implementation +// WARNING: This implementation is not thread-safe +type unsafeSet[T comparable] map[T]struct{} //nolint: gocritic + +// New creates a new empty non-thread-safe set with optional initial values +func New[T comparable](values ...T) Set[T] { + s := make(unsafeSet[T]) + for _, v := range values { + s[v] = struct{}{} + } + return s +} + +// Append adds multiple items to the set and returns the new size +func (s unsafeSet[T]) Append(val ...T) int { + for _, item := range val { + s[item] = struct{}{} + } + return len(s) +} + +// Remove removes an item from the set +func (s unsafeSet[T]) Remove(item T) { + delete(s, item) +} + +// Contains checks if an item exists in the set +func (s unsafeSet[T]) Contains(item T) bool { + _, exists := s[item] + return exists +} + +// Find returns the stored value for the given item if it exists +func (s unsafeSet[T]) Find(item T) (T, bool) { + _, exists := s[item] + return item, exists +} + +// Size returns the number of items in the set +func (s unsafeSet[T]) Size() int { + return len(s) +} + +// Clear removes all items from the set +func (s unsafeSet[T]) Clear() { + for k := range s { + delete(s, k) + } +} + +// Clone returns a new set with a copy of all items +func (s unsafeSet[T]) Clone() Set[T] { + return maps.Clone(s) +} + +// Items returns all items in the set as a slice +func (s unsafeSet[T]) Items() []T { + return slices.Collect(s.Iter()) +} + +// Iter returns an iterator over the set +func (s unsafeSet[T]) Iter() iter.Seq[T] { + return maps.Keys(s) +} + +// Union returns a new set containing all items from both sets +func (s unsafeSet[T]) Union(other Set[T]) Set[T] { + result := make(unsafeSet[T]) + for k := range s { + result[k] = struct{}{} + } + for _, item := range other.Items() { + result[item] = struct{}{} + } + return result +} + +// Intersection returns a new set containing items present in both sets +func (s unsafeSet[T]) Intersection(other Set[T]) Set[T] { + result := make(unsafeSet[T]) + for k := range s { + if other.Contains(k) { + result[k] = struct{}{} + } + } + return result +} + +// Difference returns a new set containing items present in this set but not in the other +func (s unsafeSet[T]) Difference(other Set[T]) Set[T] { + result := make(unsafeSet[T]) + for k := range s { + if !other.Contains(k) { + result[k] = struct{}{} + } + } + return result +} diff --git a/pkg/set/unsafe_test.go b/pkg/set/unsafe_test.go new file mode 100644 index 000000000000..8f42c01666ea --- /dev/null +++ b/pkg/set/unsafe_test.go @@ -0,0 +1,583 @@ +package set_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy/pkg/set" +) + +func Test_New(t *testing.T) { + tests := []struct { + name string + values []int + wantSize int + wantAll bool + desc string + }{ + { + name: "new empty set", + values: []int{}, + wantSize: 0, + wantAll: true, + desc: "should create empty set when no values provided", + }, + { + name: "new set with single value", + values: []int{1}, + wantSize: 1, + wantAll: true, + desc: "should create set with single value", + }, + { + name: "new set with multiple values", + values: []int{ + 1, + 2, + 3, + }, + wantSize: 3, + wantAll: true, + desc: "should create set with multiple values", + }, + { + name: "new set with duplicate values", + values: []int{ + 1, + 2, + 2, + 3, + 3, + 3, + }, + wantSize: 3, + wantAll: true, + desc: "should create set with unique values only", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.New(tt.values...) + assert.Equal(t, tt.wantSize, s.Size(), "unexpected set size") + }) + } +} + +func Test_unsafeSet_Add(t *testing.T) { + // Define custom type for struct test cases + type custom struct { + id int + name string + } + + tests := []struct { + name string + prepare func(s set.Set[any]) + input any + wantSize int + }{ + { + name: "add integer", + prepare: nil, + input: 1, + wantSize: 1, + }, + { + name: "add duplicate integer", + prepare: func(s set.Set[any]) { + s.Append(1) + }, + input: 1, + wantSize: 1, + }, + { + name: "add string", + prepare: nil, + input: "test", + wantSize: 1, + }, + { + name: "add empty string", + prepare: nil, + input: "", + wantSize: 1, + }, + { + name: "add custom struct", + prepare: nil, + input: custom{ + id: 1, + name: "test1", + }, + wantSize: 1, + }, + { + name: "add nil pointer", + prepare: nil, + input: (*int)(nil), + wantSize: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.New[any]() + if tt.prepare != nil { + tt.prepare(s) + } + s.Append(tt.input) + + got := s.Size() + assert.Equal(t, tt.wantSize, got, "unexpected set size") + assert.True(t, s.Contains(tt.input), "unexpected contains result for value: %v", tt.input) + }) + } +} + +func Test_unsafeSet_Append(t *testing.T) { + tests := []struct { + name string + prepare func(s set.Set[int]) + input []int + wantSize int + }{ + { + name: "append to empty set", + prepare: nil, + input: []int{ + 1, + 2, + 3, + }, + wantSize: 3, + }, + { + name: "append with duplicates", + prepare: func(s set.Set[int]) { + s.Append(1) + }, + input: []int{ + 1, + 2, + 1, + 3, + 2, + }, + wantSize: 3, + }, + { + name: "append empty slice", + prepare: func(s set.Set[int]) { + s.Append(1) + }, + input: []int{}, + wantSize: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.New[int]() + if tt.prepare != nil { + tt.prepare(s) + } + got := s.Append(tt.input...) + + assert.Equal(t, tt.wantSize, got, "unexpected returned size") + assert.Equal(t, tt.wantSize, s.Size(), "unexpected actual size") + + for _, item := range tt.input { + assert.True(t, s.Contains(item), "set should contain appended item: %v", item) + } + }) + } +} + +func Test_unsafeSet_Remove(t *testing.T) { + tests := []struct { + name string + prepare func(s set.Set[int]) + input int + wantSize int + }{ + { + name: "remove existing element", + prepare: func(s set.Set[int]) { + s.Append(1) + }, + input: 1, + wantSize: 0, + }, + { + name: "remove non-existing element", + prepare: func(s set.Set[int]) { + s.Append(1) + }, + input: 2, + wantSize: 1, + }, + { + name: "remove from empty set", + prepare: nil, + input: 1, + wantSize: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.New[int]() + if tt.prepare != nil { + tt.prepare(s) + } + s.Remove(tt.input) + + got := s.Size() + assert.Equal(t, tt.wantSize, got, "unexpected set size") + assert.False(t, s.Contains(tt.input), "unexpected contains result for value: %v", tt.input) + }) + } +} + +func Test_unsafeSet_Clear(t *testing.T) { + tests := []struct { + name string + prepare func(s set.Set[int]) + }{ + { + name: "clear non-empty set", + prepare: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + s.Append(3) + }, + }, + { + name: "clear empty set", + prepare: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.New[int]() + if tt.prepare != nil { + tt.prepare(s) + } + s.Clear() + + got := s.Size() + assert.Zero(t, got, "unexpected set size") + assert.Empty(t, s.Items(), "items should be empty") + }) + } +} + +func Test_unsafeSet_Clone(t *testing.T) { + t.Run("empty set", func(t *testing.T) { + original := set.New[string]() + cloned := original.Clone() + + assert.Equal(t, 0, cloned.Size(), "cloned set should be empty") + + // Verify independence + original.Append("test") + assert.False(t, cloned.Contains("test"), "cloned set should not be affected by original") + }) + + t.Run("basic types", func(t *testing.T) { + original := set.New[any](1, "test", true) + cloned := original.Clone() + + assert.Equal(t, original.Size(), cloned.Size(), "sizes should match") + assert.True(t, cloned.Contains(1), "should contain integer") + assert.True(t, cloned.Contains("test"), "should contain string") + assert.True(t, cloned.Contains(true), "should contain boolean") + + // Verify independence + original.Append("new") + assert.False(t, cloned.Contains("new"), "cloned set should not be affected by original") + cloned.Append("another") + assert.False(t, original.Contains("another"), "original set should not be affected by clone") + }) + + // Test nil pointer + t.Run("nil pointer", func(t *testing.T) { + original := set.New[*int]() + original.Append(nil) + + cloned := original.Clone() + + assert.Equal(t, original.Size(), cloned.Size(), "sizes should match") + assert.True(t, cloned.Contains((*int)(nil)), "should contain nil pointer") + }) +} + +func Test_unsafeSet_Items(t *testing.T) { + tests := []struct { + name string + prepare func(s set.Set[int]) + want []int + }{ + { + name: "get items from non-empty set", + prepare: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + s.Append(3) + }, + want: []int{ + 1, + 2, + 3, + }, + }, + { + name: "get items from empty set", + prepare: nil, + want: []int{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := set.New[int]() + if tt.prepare != nil { + tt.prepare(s) + } + got := s.Items() + + assert.ElementsMatch(t, tt.want, got, "unexpected items in set") + }) + } +} + +func Test_unsafeSet_Union(t *testing.T) { + tests := []struct { + name string + prepare1 func(s set.Set[int]) + prepare2 func(s set.Set[int]) + want []int + }{ + { + name: "union of non-overlapping sets", + prepare1: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + }, + prepare2: func(s set.Set[int]) { + s.Append(3) + s.Append(4) + }, + want: []int{ + 1, + 2, + 3, + 4, + }, + }, + { + name: "union of overlapping sets", + prepare1: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + s.Append(3) + }, + prepare2: func(s set.Set[int]) { + s.Append(2) + s.Append(3) + s.Append(4) + }, + want: []int{ + 1, + 2, + 3, + 4, + }, + }, + { + name: "union with empty set", + prepare1: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + }, + prepare2: nil, + want: []int{ + 1, + 2, + }, + }, + { + name: "union of empty sets", + prepare1: nil, + prepare2: nil, + want: []int{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s1 := set.New[int]() + s2 := set.New[int]() + + if tt.prepare1 != nil { + tt.prepare1(s1) + } + if tt.prepare2 != nil { + tt.prepare2(s2) + } + + result := s1.Union(s2) + got := result.Items() + + assert.ElementsMatch(t, tt.want, got, "unexpected union result") + }) + } +} + +func Test_unsafeSet_Intersection(t *testing.T) { + tests := []struct { + name string + prepare1 func(s set.Set[int]) + prepare2 func(s set.Set[int]) + want []int + }{ + { + name: "intersection of overlapping sets", + prepare1: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + s.Append(3) + }, + prepare2: func(s set.Set[int]) { + s.Append(2) + s.Append(3) + s.Append(4) + }, + want: []int{ + 2, + 3, + }, + }, + { + name: "intersection of non-overlapping sets", + prepare1: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + }, + prepare2: func(s set.Set[int]) { + s.Append(3) + s.Append(4) + }, + want: []int{}, + }, + { + name: "intersection with empty set", + prepare1: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + }, + prepare2: nil, + want: []int{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s1 := set.New[int]() + s2 := set.New[int]() + + if tt.prepare1 != nil { + tt.prepare1(s1) + } + if tt.prepare2 != nil { + tt.prepare2(s2) + } + + result := s1.Intersection(s2) + got := result.Items() + + assert.ElementsMatch(t, tt.want, got, "unexpected intersection result") + }) + } +} + +func Test_unsafeSet_Difference(t *testing.T) { + tests := []struct { + name string + prepare1 func(s set.Set[int]) + prepare2 func(s set.Set[int]) + want []int + }{ + { + name: "difference of overlapping sets", + prepare1: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + s.Append(3) + }, + prepare2: func(s set.Set[int]) { + s.Append(2) + s.Append(3) + s.Append(4) + }, + want: []int{1}, + }, + { + name: "difference with non-overlapping set", + prepare1: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + }, + prepare2: func(s set.Set[int]) { + s.Append(3) + s.Append(4) + }, + want: []int{ + 1, + 2, + }, + }, + { + name: "difference with empty set", + prepare1: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + }, + prepare2: nil, + want: []int{ + 1, + 2, + }, + }, + { + name: "difference of empty set", + prepare1: nil, + prepare2: func(s set.Set[int]) { + s.Append(1) + s.Append(2) + }, + want: []int{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s1 := set.New[int]() + s2 := set.New[int]() + if tt.prepare1 != nil { + tt.prepare1(s1) + } + if tt.prepare2 != nil { + tt.prepare2(s2) + } + + result := s1.Difference(s2) + got := result.Items() + + assert.ElementsMatch(t, tt.want, got, "unexpected difference result") + }) + } +} diff --git a/pkg/types/error.go b/pkg/types/error.go new file mode 100644 index 000000000000..6ea8ced4e34a --- /dev/null +++ b/pkg/types/error.go @@ -0,0 +1,22 @@ +package types + +import ( + "fmt" +) + +type ExitError struct { + Code int +} + +func (e *ExitError) Error() string { + return fmt.Sprintf("exit status %d", e.Code) +} + +// UserError represents an error with a user-friendly message. +type UserError struct { + Message string +} + +func (e *UserError) Error() string { + return e.Message +} diff --git a/pkg/types/finding.go b/pkg/types/finding.go new file mode 100644 index 000000000000..ddc840a197b9 --- /dev/null +++ b/pkg/types/finding.go @@ -0,0 +1,94 @@ +package types + +import ( + "encoding/json" + + "golang.org/x/xerrors" +) + +type FindingType string +type FindingStatus string + +const ( + FindingTypeVulnerability FindingType = "vulnerability" + FindingTypeMisconfiguration FindingType = "misconfiguration" + FindingTypeSecret FindingType = "secret" + FindingTypeLicense FindingType = "license" + + FindingStatusIgnored FindingStatus = "ignored" // Trivy + FindingStatusUnknown FindingStatus = "unknown" // Trivy + FindingStatusNotAffected FindingStatus = "not_affected" // VEX + FindingStatusAffected FindingStatus = "affected" // VEX + FindingStatusFixed FindingStatus = "fixed" // VEX + FindingStatusUnderInvestigation FindingStatus = "under_investigation" // VEX +) + +// Finding represents one of the findings that Trivy can detect, +// such as vulnerabilities, misconfigurations, secrets, and licenses. +type Finding interface { + FindingType() FindingType +} + +// ModifiedFinding represents a security finding that has been modified by an external source, +// such as .trivyignore and VEX. Currently, it is primarily used to account for vulnerabilities +// that are ignored via .trivyignore or identified as not impactful through VEX. +// However, it is planned to also store vulnerabilities whose severity has been adjusted by VEX, +// or that have been detected through Wasm modules in the future. +type ModifiedFinding struct { + Type FindingType + Status FindingStatus + Statement string + Source string + Finding Finding // one of findings +} + +func NewModifiedFinding(f Finding, status FindingStatus, statement, source string) ModifiedFinding { + return ModifiedFinding{ + Type: f.FindingType(), + Status: status, + Statement: statement, + Source: source, + Finding: f, + } +} + +// UnmarshalJSON unmarshals ModifiedFinding given the type and `UnmarshalJSON` functions of struct fields +func (m *ModifiedFinding) UnmarshalJSON(data []byte) error { + type Alias ModifiedFinding + aux := &struct { + Finding json.RawMessage `json:"Finding"` + *Alias + }{ + Alias: (*Alias)(m), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + // Select struct by m.Type to avoid errors with Unmarshal + var err error + switch m.Type { + case FindingTypeVulnerability: + m.Finding, err = unmarshalFinding[DetectedVulnerability](aux.Finding) + case FindingTypeMisconfiguration: + m.Finding, err = unmarshalFinding[DetectedMisconfiguration](aux.Finding) + case FindingTypeSecret: + m.Finding, err = unmarshalFinding[DetectedSecret](aux.Finding) + case FindingTypeLicense: + m.Finding, err = unmarshalFinding[DetectedLicense](aux.Finding) + default: + return xerrors.Errorf("invalid Finding type: %s", m.Type) + } + + if err != nil { + return xerrors.Errorf("unable to unmarshal %q type: %w", m.Type, err) + } + return nil +} + +func unmarshalFinding[T Finding](data []byte) (T, error) { + var f T + err := json.Unmarshal(data, &f) + return f, err +} diff --git a/pkg/types/library.go b/pkg/types/library.go index fc4b5eb9719c..02dfc8a64839 100644 --- a/pkg/types/library.go +++ b/pkg/types/library.go @@ -1,6 +1,7 @@ package types -type Library struct{ - Name string +// Library holds the attribute of a package library +type Library struct { + Name string Version string } diff --git a/pkg/types/license.go b/pkg/types/license.go new file mode 100644 index 000000000000..60e3b375567e --- /dev/null +++ b/pkg/types/license.go @@ -0,0 +1,36 @@ +package types + +import ( + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type DetectedLicense struct { + // Severity is the consistent parameter indicating how severe the issue is + Severity string + + // Category holds the license category such as "forbidden" + Category types.LicenseCategory + + // PkgName holds a package name of the license. + // It will be empty if FilePath is filled. + PkgName string + + // PkgName holds a file path of the license. + // It will be empty if PkgName is filled. + FilePath string // for file license + + // Name holds a detected license name + Name string + + // Text holds a long license text if Trivy detects a license name as a license text + Text string + + // Confidence is level of the match. The confidence level is between 0.0 and 1.0, with 1.0 indicating an + // exact match and 0.0 indicating a complete mismatch + Confidence float64 + + // Link is a SPDX link of the license + Link string +} + +func (DetectedLicense) FindingType() FindingType { return FindingTypeLicense } diff --git a/pkg/types/misconfiguration.go b/pkg/types/misconfiguration.go new file mode 100644 index 000000000000..69d8f74b455e --- /dev/null +++ b/pkg/types/misconfiguration.go @@ -0,0 +1,43 @@ +package types + +import ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + +// DetectedMisconfiguration holds detected misconfigurations +type DetectedMisconfiguration struct { + Type string `json:",omitempty"` + ID string `json:",omitempty"` + // Deprecated: Use the ID field instead. + AVDID string `json:",omitempty"` + Aliases []string `json:"-"` + Title string `json:",omitempty"` + Description string `json:",omitempty"` + Message string `json:",omitempty"` + Namespace string `json:",omitempty"` + Query string `json:",omitempty"` + Resolution string `json:",omitempty"` + Severity string `json:",omitempty"` + PrimaryURL string `json:",omitempty"` + References []string `json:",omitempty"` + Status MisconfStatus `json:",omitempty"` + Layer ftypes.Layer `json:",omitzero"` + CauseMetadata ftypes.CauseMetadata `json:",omitzero"` + + // For debugging + Traces []string `json:",omitempty"` +} + +// MisconfStatus represents a status of misconfiguration +type MisconfStatus string + +const ( + // MisconfStatusPassed represents successful status + MisconfStatusPassed MisconfStatus = "PASS" + + // MisconfStatusFailure represents failure status + MisconfStatusFailure MisconfStatus = "FAIL" + + // MisconfStatusException Passed represents the status of exception + MisconfStatusException MisconfStatus = "EXCEPTION" +) + +func (DetectedMisconfiguration) FindingType() FindingType { return FindingTypeMisconfiguration } diff --git a/pkg/types/report.go b/pkg/types/report.go new file mode 100644 index 000000000000..0c65ed3b4137 --- /dev/null +++ b/pkg/types/report.go @@ -0,0 +1,208 @@ +package types + +import ( + "time" + + v1 "github.com/google/go-containerregistry/pkg/v1" // nolint: goimports + + "github.com/aquasecurity/trivy/pkg/fanal/image/name" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom/core" +) + +// TrivyInfo contains Trivy-specific information +type TrivyInfo struct { + Version string `json:",omitempty"` // Client version + Server VersionInfo `json:",omitzero"` // Server info (client/server mode only) +} + +// Report represents a scan result +type Report struct { + SchemaVersion int `json:",omitempty"` + Trivy TrivyInfo `json:",omitzero"` + ReportID string `json:",omitempty"` // Unique identifier for this scan report + CreatedAt time.Time `json:",omitzero"` + + // ArtifactID uniquely identifies the scanned artifact. + // For container images: hash(ImageID + Registry + Repository) - ensures same image in different repos have different IDs + // For repositories: hash(RepoURL + Commit) or hash(Path + Commit) for local repos + // For filesystems: empty string + // For other artifact types: empty string + ArtifactID string `json:",omitempty"` + + ArtifactName string `json:",omitempty"` + ArtifactType ftypes.ArtifactType `json:",omitempty"` + Metadata Metadata `json:",omitzero"` + Results Results `json:",omitempty"` + + // parsed SBOM + BOM *core.BOM `json:"-"` // Just for internal usage, not exported in JSON +} + +// Metadata represents a metadata of artifact +type Metadata struct { + Size int64 `json:",omitempty"` + OS *ftypes.OS `json:",omitempty"` + + // Container image + ImageID string `json:",omitempty"` + DiffIDs []string `json:",omitempty"` + RepoTags []string `json:",omitempty"` + RepoDigests []string `json:",omitempty"` + Reference name.Reference `json:",omitzero"` + ImageConfig v1.ConfigFile `json:",omitzero"` + Layers ftypes.Layers `json:",omitzero"` + + // Git repository + RepoURL string `json:",omitzero"` + Branch string `json:",omitzero"` + Tags []string `json:",omitzero"` + Commit string `json:",omitzero"` + CommitMsg string `json:",omitzero"` + Author string `json:",omitzero"` + Committer string `json:",omitzero"` +} + +// Results to hold list of Result +type Results []Result + +type ResultClass string +type Compliance = string +type Format string + +const ( + ClassUnknown ResultClass = "unknown" + ClassOSPkg ResultClass = "os-pkgs" // For detected packages and vulnerabilities in OS packages + ClassLangPkg ResultClass = "lang-pkgs" // For detected packages and vulnerabilities in language-specific packages + ClassConfig ResultClass = "config" // For detected misconfigurations + ClassSecret ResultClass = "secret" // For detected secrets + ClassLicense ResultClass = "license" // For detected package licenses + ClassLicenseFile ResultClass = "license-file" // For detected licenses in files + ClassCustom ResultClass = "custom" + + ComplianceK8sNsa10 = Compliance("k8s-nsa-1.0") + ComplianceK8sCIS123 = Compliance("k8s-cis-1.23") + ComplianceK8sPSSBaseline01 = Compliance("k8s-pss-baseline-0.1") + ComplianceK8sPSSRestricted01 = Compliance("k8s-pss-restricted-0.1") + ComplianceAWSCIS12 = Compliance("aws-cis-1.2") + ComplianceAWSCIS14 = Compliance("aws-cis-1.4") + ComplianceDockerCIS160 = Compliance("docker-cis-1.6.0") + ComplianceEksCIS14 = Compliance("eks-cis-1.4") + ComplianceRke2CIS124 = Compliance("rke2-cis-1.24") + + FormatTable Format = "table" + FormatJSON Format = "json" + FormatTemplate Format = "template" + FormatSarif Format = "sarif" + FormatCycloneDX Format = "cyclonedx" + FormatSPDX Format = "spdx" + FormatSPDXJSON Format = "spdx-json" + FormatGitHub Format = "github" + FormatCosignVuln Format = "cosign-vuln" +) + +var BuiltInK8sCompliances = []string{ + ComplianceK8sNsa10, + ComplianceK8sCIS123, + ComplianceEksCIS14, + ComplianceRke2CIS124, + ComplianceK8sPSSBaseline01, + ComplianceK8sPSSRestricted01, +} + +var ( + SupportedFormats = []Format{ + FormatTable, + FormatJSON, + FormatTemplate, + FormatSarif, + FormatCycloneDX, + FormatSPDX, + FormatSPDXJSON, + FormatGitHub, + FormatCosignVuln, + } + SupportedSBOMFormats = []Format{ + FormatCycloneDX, + FormatSPDX, + FormatSPDXJSON, + FormatGitHub, + } + SupportedCompliances = []string{ + ComplianceK8sNsa10, + ComplianceK8sCIS123, + ComplianceK8sPSSBaseline01, + ComplianceK8sPSSRestricted01, + ComplianceAWSCIS12, + ComplianceAWSCIS14, + ComplianceDockerCIS160, + ComplianceEksCIS14, + ComplianceRke2CIS124, + } +) + +type TableMode string + +const ( + Summary TableMode = "summary" + Detailed TableMode = "detailed" +) + +var SupportedTableModes = []TableMode{ + Summary, + Detailed, +} + +// Result holds a target and detected vulnerabilities +type Result struct { + Target string `json:"Target"` + Class ResultClass `json:"Class,omitempty"` + Type ftypes.TargetType `json:"Type,omitempty"` + Packages []ftypes.Package `json:"Packages,omitempty"` + Vulnerabilities []DetectedVulnerability `json:"Vulnerabilities,omitempty"` + MisconfSummary *MisconfSummary `json:"MisconfSummary,omitempty"` + Misconfigurations []DetectedMisconfiguration `json:"Misconfigurations,omitempty"` + Secrets []DetectedSecret `json:"Secrets,omitempty"` + Licenses []DetectedLicense `json:"Licenses,omitempty"` + CustomResources []ftypes.CustomResource `json:"CustomResources,omitempty"` + + // ModifiedFindings holds a list of findings that have been modified from their original state. + // This can include vulnerabilities that have been marked as ignored, not affected, or have had + // their severity adjusted. It's still in an experimental stage and may change in the future. + ModifiedFindings []ModifiedFinding `json:"ExperimentalModifiedFindings,omitempty"` +} + +func (r *Result) IsEmpty() bool { + return len(r.Packages) == 0 && len(r.Vulnerabilities) == 0 && len(r.Misconfigurations) == 0 && + len(r.Secrets) == 0 && len(r.Licenses) == 0 && len(r.CustomResources) == 0 && len(r.ModifiedFindings) == 0 +} + +type MisconfSummary struct { + Successes int + Failures int +} + +func (s MisconfSummary) Empty() bool { + return s.Successes == 0 && s.Failures == 0 +} + +// Failed returns whether the result includes any vulnerabilities, misconfigurations or secrets +func (results Results) Failed() bool { + for _, r := range results { + if len(r.Vulnerabilities) > 0 { + return true + } + for _, m := range r.Misconfigurations { + if m.Status == MisconfStatusFailure { + return true + } + } + if len(r.Secrets) > 0 { + return true + } + if len(r.Licenses) > 0 { + return true + } + } + return false +} diff --git a/pkg/types/sbom.go b/pkg/types/sbom.go new file mode 100644 index 000000000000..82f6139e48f7 --- /dev/null +++ b/pkg/types/sbom.go @@ -0,0 +1,29 @@ +package types + +import ( + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom/core" +) + +type SBOMSource = string + +type SBOM struct { + Metadata Metadata + + Packages []ftypes.PackageInfo + Applications []ftypes.Application + + BOM *core.BOM +} + +const ( + SBOMSourceOCI = SBOMSource("oci") + SBOMSourceRekor = SBOMSource("rekor") +) + +var ( + SBOMSources = []string{ + SBOMSourceOCI, + SBOMSourceRekor, + } +) diff --git a/pkg/types/scan.go b/pkg/types/scan.go new file mode 100644 index 000000000000..faae1eba3c7e --- /dev/null +++ b/pkg/types/scan.go @@ -0,0 +1,131 @@ +package types + +import ( + "slices" + + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +// PkgType represents package type +type PkgType = string + +// Scanner represents the type of security scanning +type Scanner string + +// Scanners is a slice of scanners +type Scanners []Scanner + +const ( + // PkgTypeUnknown is a package type of unknown + PkgTypeUnknown PkgType = "unknown" + + // PkgTypeOS is a package type of OS packages + PkgTypeOS PkgType = "os" + + // PkgTypeLibrary is a package type of programming language dependencies + PkgTypeLibrary PkgType = "library" + + // UnknownScanner is the scanner of unknown + UnknownScanner Scanner = "unknown" + + // NoneScanner is the scanner of none + NoneScanner Scanner = "none" + + // SBOMScanner is the virtual scanner of SBOM, which cannot be enabled by the user + SBOMScanner Scanner = "sbom" + + // VulnerabilityScanner is the scanner of vulnerabilities + VulnerabilityScanner Scanner = "vuln" + + // MisconfigScanner is the scanner of misconfigurations + MisconfigScanner Scanner = "misconfig" + + // SecretScanner is the scanner of secrets + SecretScanner Scanner = "secret" + + // RBACScanner is the scanner of rbac assessment + RBACScanner Scanner = "rbac" + + // LicenseScanner is the scanner of licenses + LicenseScanner Scanner = "license" +) + +var ( + PkgTypes = []string{ + PkgTypeOS, + PkgTypeLibrary, + } + + AllScanners = Scanners{ + VulnerabilityScanner, + MisconfigScanner, + RBACScanner, + SecretScanner, + LicenseScanner, + NoneScanner, + } + + // AllImageConfigScanners has a list of available scanners on container image config. + // The container image in container registries consists of manifest, config and layers. + // Trivy is also able to detect security issues on the image config. + AllImageConfigScanners = Scanners{ + MisconfigScanner, + SecretScanner, + NoneScanner, + } +) + +func (scanners *Scanners) Enable(s Scanner) { + if !scanners.Enabled(s) { + *scanners = append(*scanners, s) + } +} + +func (scanners *Scanners) Enabled(s Scanner) bool { + return slices.Contains(*scanners, s) +} + +// AnyEnabled returns true if any of the passed scanners is included. +func (scanners *Scanners) AnyEnabled(ss ...Scanner) bool { + return slices.ContainsFunc(ss, scanners.Enabled) +} + +// ScanTarget holds the attributes for scanning. +type ScanTarget struct { + Name string // container image name, file path, etc + OS types.OS + Repository *types.Repository + Packages types.Packages + Applications []types.Application + Misconfigurations []types.Misconfiguration + Secrets []types.Secret + Licenses []types.LicenseFile + + // CustomResources hold analysis results from custom analyzers. + // It is for extensibility and not used in OSS. + CustomResources []types.CustomResource +} + +// ScanOptions holds the attributes for scanning vulnerabilities/licenses +type ScanOptions struct { + PkgTypes []string + PkgRelationships []types.Relationship + Scanners Scanners + ImageConfigScanners Scanners // Scanners for container image configuration + ScanRemovedPackages bool + LicenseCategories map[types.LicenseCategory][]string + LicenseFull bool + FilePatterns []string + IncludeDevDeps bool + Distro types.OS // Forced OS + VulnSeveritySources []dbTypes.SourceID +} + +// ScanResponse represents the response from the scan service +type ScanResponse struct { + Results Results + OS types.OS + Layers types.Layers + ServerInfo VersionInfo // Server info (client/server mode only) +} diff --git a/pkg/types/secret.go b/pkg/types/secret.go new file mode 100644 index 000000000000..a18c438e2e14 --- /dev/null +++ b/pkg/types/secret.go @@ -0,0 +1,9 @@ +package types + +import ( + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +type DetectedSecret ftypes.SecretFinding + +func (DetectedSecret) FindingType() FindingType { return FindingTypeSecret } diff --git a/pkg/types/version.go b/pkg/types/version.go new file mode 100644 index 000000000000..4643cf7b73d6 --- /dev/null +++ b/pkg/types/version.go @@ -0,0 +1,54 @@ +package types + +import ( + "fmt" + "time" + + "github.com/aquasecurity/trivy-db/pkg/metadata" +) + +// BundleMetadata holds policy bundle metadata. +// This is a lightweight alternative to policy.Metadata to avoid importing +// pkg/policy which has dependencies incompatible with wasip1/wasm. +type BundleMetadata struct { + Digest string + DownloadedAt time.Time +} + +func (m BundleMetadata) String() string { + return fmt.Sprintf(`Check Bundle: + Digest: %s + DownloadedAt: %s +`, m.Digest, m.DownloadedAt.UTC()) +} + +// VersionInfo holds version information for Trivy and its databases. +type VersionInfo struct { + Version string `json:",omitempty"` + VulnerabilityDB *metadata.Metadata `json:",omitempty"` + JavaDB *metadata.Metadata `json:",omitempty"` + CheckBundle *BundleMetadata `json:",omitempty"` +} + +func (v VersionInfo) String() string { + output := fmt.Sprintf("Version: %s\n", v.Version) + if v.VulnerabilityDB != nil { + output += formatDBMetadata("Vulnerability DB", *v.VulnerabilityDB) + } + if v.JavaDB != nil { + output += formatDBMetadata("Java DB", *v.JavaDB) + } + if v.CheckBundle != nil { + output += v.CheckBundle.String() + } + return output +} + +func formatDBMetadata(title string, meta metadata.Metadata) string { + return fmt.Sprintf(`%s: + Version: %d + UpdatedAt: %s + NextUpdate: %s + DownloadedAt: %s +`, title, meta.Version, meta.UpdatedAt.UTC(), meta.NextUpdate.UTC(), meta.DownloadedAt.UTC()) +} diff --git a/pkg/types/vulnerability.go b/pkg/types/vulnerability.go index 6413d9477e85..341dbc1f617d 100644 --- a/pkg/types/vulnerability.go +++ b/pkg/types/vulnerability.go @@ -1,13 +1,71 @@ package types -type Vulnerability struct { - VulnerabilityID string - PkgName string - InstalledVersion string - FixedVersion string - - Title string - Description string - Severity string - References []string +import ( + "github.com/aquasecurity/trivy-db/pkg/types" + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" +) + +// DetectedVulnerability holds the information of detected vulnerabilities +type DetectedVulnerability struct { + VulnerabilityID string `json:",omitempty"` + VendorIDs []string `json:",omitempty"` + PkgID string `json:",omitempty"` // It is used to construct dependency graph. + PkgName string `json:",omitempty"` + PkgPath string `json:",omitempty"` // This field is populated in the case of language-specific packages such as egg/wheel and gemspec + PkgIdentifier ftypes.PkgIdentifier `json:",omitzero"` + InstalledVersion string `json:",omitempty"` + FixedVersion string `json:",omitempty"` + Status types.Status `json:",omitempty"` + Layer ftypes.Layer `json:",omitzero"` + SeveritySource types.SourceID `json:",omitempty"` + PrimaryURL string `json:",omitempty"` + + // DataSource holds where the advisory comes from + DataSource *types.DataSource `json:",omitempty"` + + // Fingerprint is a unique identifier for the vulnerability based on + // ArtifactID, Target, PkgID, and VulnerabilityID + Fingerprint string `json:",omitempty"` + + // Custom is for extensibility and not supposed to be used in OSS + Custom any `json:",omitempty"` + + // Embed vulnerability details + types.Vulnerability +} + +func (DetectedVulnerability) FindingType() FindingType { return FindingTypeVulnerability } + +// BySeverity implements sort.Interface based on the Severity field. +type BySeverity []DetectedVulnerability + +// Len returns the length of DetectedVulnerabilities +func (v BySeverity) Len() int { return len(v) } + +// Less compares 2 DetectedVulnerabilities based on package name, ID, severity, vulnerabilityID and package path +func (v BySeverity) Less(i, j int) bool { + switch { + case v[i].PkgName != v[j].PkgName: + return v[i].PkgName < v[j].PkgName + case v[i].PkgID != v[j].PkgID: + // For cases where there are different pkgIDs but the same pkgName. + // e.g. same packages from different maven module. + return v[i].PkgID < v[j].PkgID + case v[i].InstalledVersion != v[j].InstalledVersion: + return v[i].InstalledVersion < v[j].InstalledVersion + } + + ret := types.CompareSeverityString( + v[j].Severity, v[i].Severity, + ) + if ret != 0 { + return ret > 0 + } + if v[i].VulnerabilityID != v[j].VulnerabilityID { + return v[i].VulnerabilityID < v[j].VulnerabilityID + } + return v[i].PkgPath < v[j].PkgPath } + +// Swap swaps 2 vulnerability +func (v BySeverity) Swap(i, j int) { v[i], v[j] = v[j], v[i] } diff --git a/pkg/utils/fsutils/fs.go b/pkg/utils/fsutils/fs.go new file mode 100644 index 000000000000..b2aea51dda0a --- /dev/null +++ b/pkg/utils/fsutils/fs.go @@ -0,0 +1,105 @@ +package fsutils + +import ( + "fmt" + "io" + "io/fs" + "os" + "path/filepath" + "slices" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" +) + +const ( + xdgDataHome = "XDG_DATA_HOME" +) + +func HomeDir() string { + dataHome := os.Getenv(xdgDataHome) + if dataHome != "" { + return dataHome + } + + homeDir, _ := os.UserHomeDir() + return homeDir +} + +func TrivyHomeDir() string { + return filepath.Join(HomeDir(), ".trivy") +} + +// CopyFile copies the file content from scr to dst +func CopyFile(src, dst string) (int64, error) { + sourceFileStat, err := os.Stat(src) + if err != nil { + return 0, xerrors.Errorf("file (%s) stat error: %w", src, err) + } + + if !sourceFileStat.Mode().IsRegular() { + return 0, fmt.Errorf("%s is not a regular file", src) + } + + source, err := os.Open(src) + if err != nil { + return 0, err + } + defer source.Close() + + destination, err := os.Create(dst) + if err != nil { + return 0, err + } + defer destination.Close() + n, err := io.Copy(destination, source) + return n, err +} + +func DirExists(path string) bool { + f, err := os.Stat(path) + return err == nil && f.IsDir() +} + +func FileExists(filename string) bool { + f, err := os.Stat(filename) + return err == nil && !f.IsDir() +} + +type WalkDirRequiredFunc func(path string, d fs.DirEntry) bool + +type WalkDirFunc func(path string, d fs.DirEntry, r io.Reader) error + +func WalkDir(fsys fs.FS, root string, required WalkDirRequiredFunc, fn WalkDirFunc) error { + return fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } else if !d.Type().IsRegular() || !required(path, d) { + return nil + } + + f, err := fsys.Open(path) + if err != nil { + return xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + if err = fn(path, d, f); err != nil { + log.Debug("Walk error", log.FilePath(path), log.Err(err)) + } + return nil + }) +} + +func RequiredExt(exts ...string) WalkDirRequiredFunc { + return func(filePath string, _ fs.DirEntry) bool { + return slices.Contains(exts, filepath.Ext(filePath)) + } +} + +func RequiredFile(fileNames ...string) WalkDirRequiredFunc { + return func(filePath string, _ fs.DirEntry) bool { + return slices.Contains(fileNames, filepath.Base(filePath)) + } +} diff --git a/pkg/utils/fsutils/fs_test.go b/pkg/utils/fsutils/fs_test.go new file mode 100644 index 000000000000..0b6133394b23 --- /dev/null +++ b/pkg/utils/fsutils/fs_test.go @@ -0,0 +1,103 @@ +package fsutils + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCopyFile(t *testing.T) { + type args struct { + src string + dst string + } + tests := []struct { + name string + args args + content []byte + want string + wantErr string + }{ + { + name: "happy path", + content: []byte("this is a content"), + args: args{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + src := tt.args.src + if tt.args.src == "" { + s, err := os.CreateTemp(t.TempDir(), "src") + require.NoError(t, err, tt.name) + _, err = s.Write(tt.content) + require.NoError(t, err, tt.name) + src = s.Name() + require.NoError(t, s.Close()) + } + + dst := tt.args.dst + if tt.args.dst == "" { + d, err := os.CreateTemp(t.TempDir(), "dst") + require.NoError(t, err, tt.name) + dst = d.Name() + require.NoError(t, d.Close(), tt.name) + } + + _, err := CopyFile(src, dst) + if tt.wantErr != "" { + require.Error(t, err, tt.name) + assert.Equal(t, tt.wantErr, err.Error(), tt.name) + } else { + require.NoError(t, err, tt.name) + } + }) + } +} + +func TestDirExists(t *testing.T) { + t.Run("invalid path", func(t *testing.T) { + assert.False(t, DirExists("\000invalid:path")) + }) + + t.Run("valid path", func(t *testing.T) { + assert.True(t, DirExists(t.TempDir())) + }) + + t.Run("dir not exist", func(t *testing.T) { + assert.False(t, DirExists(filepath.Join(t.TempDir(), "tmp"))) + }) + + t.Run("file path", func(t *testing.T) { + filePath := filepath.Join(t.TempDir(), "tmp") + f, err := os.Create(filePath) + require.NoError(t, f.Close()) + require.NoError(t, err) + assert.False(t, DirExists(filePath)) + }) +} + +func TestFileExists(t *testing.T) { + t.Run("invalid path", func(t *testing.T) { + assert.False(t, FileExists("\000invalid:path")) + }) + + t.Run("valid path", func(t *testing.T) { + filePath := filepath.Join(t.TempDir(), "tmp") + f, err := os.Create(filePath) + require.NoError(t, f.Close()) + require.NoError(t, err) + assert.True(t, FileExists(filePath)) + }) + + t.Run("file not exist", func(t *testing.T) { + assert.False(t, FileExists(filepath.Join(t.TempDir(), "tmp"))) + }) + + t.Run("dir path", func(t *testing.T) { + assert.False(t, FileExists(t.TempDir())) + }) +} diff --git a/pkg/utils/progress.go b/pkg/utils/progress.go deleted file mode 100644 index b01bd9dc19a3..000000000000 --- a/pkg/utils/progress.go +++ /dev/null @@ -1,63 +0,0 @@ -package utils - -import ( - "time" - - "github.com/briandowns/spinner" - pb "gopkg.in/cheggaaa/pb.v1" -) - -var ( - Quiet = false -) - -type Spinner struct { - client *spinner.Spinner -} - -func NewSpinner(suffix string) *Spinner { - if Quiet { - return &Spinner{} - } - s := spinner.New(spinner.CharSets[36], 100*time.Millisecond) - s.Suffix = suffix - return &Spinner{client: s} -} - -func (s *Spinner) Start() { - if s.client == nil { - return - } - s.client.Start() -} -func (s *Spinner) Stop() { - if s.client == nil { - return - } - s.client.Stop() -} - -type ProgressBar struct { - client *pb.ProgressBar -} - -func PbStartNew(total int) *ProgressBar { - if Quiet { - return &ProgressBar{} - } - bar := pb.StartNew(total) - return &ProgressBar{client: bar} -} - -func (p *ProgressBar) Increment() { - if p.client == nil { - return - } - p.client.Increment() -} -func (p *ProgressBar) Finish() { - if p.client == nil { - return - } - p.client.Finish() -} diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go deleted file mode 100644 index 0ff3d57f84d8..000000000000 --- a/pkg/utils/utils.go +++ /dev/null @@ -1,113 +0,0 @@ -package utils - -import ( - "bytes" - "io" - "os" - "os/exec" - "path/filepath" - "strings" - - "github.com/knqyf263/trivy/pkg/log" - "golang.org/x/xerrors" -) - -func CacheDir() string { - cacheDir, err := os.UserCacheDir() - if err != nil { - cacheDir = os.TempDir() - } - dir := filepath.Join(cacheDir, "trivy") - return dir -} - -func FileWalk(root string, targetFiles map[string]struct{}, walkFn func(r io.Reader, path string) error) error { - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if info.IsDir() { - return nil - } - - rel, err := filepath.Rel(root, path) - if err != nil { - return xerrors.Errorf("error in filepath rel: %w", err) - } - - if _, ok := targetFiles[rel]; !ok { - return nil - } - - if info.Size() == 0 { - log.Logger.Debugf("invalid size: %s", path) - return nil - } - - f, err := os.Open(path) - defer f.Close() - if err != nil { - return xerrors.Errorf("failed to open file: %w", err) - } - - if err = walkFn(f, path); err != nil { - return err - } - return nil - }) - if err != nil { - return xerrors.Errorf("error in file walk: %w", err) - } - return nil -} - -func IsCommandAvailable(name string) bool { - cmd := exec.Command(name, "--help") - if err := cmd.Run(); err != nil { - return false - } - return true -} - -func Exists(path string) (bool, error) { - _, err := os.Stat(path) - if err == nil { - return true, nil - } - if os.IsNotExist(err) { - return false, nil - } - return true, err -} - -func StringInSlice(a string, list []string) bool { - for _, b := range list { - if b == a { - return true - } - } - return false -} - -func Exec(command string, args []string) (string, error) { - cmd := exec.Command(command, args...) - var stdoutBuf, stderrBuf bytes.Buffer - cmd.Stdout = &stdoutBuf - cmd.Stderr = &stderrBuf - if err := cmd.Run(); err != nil { - log.Logger.Debug(stderrBuf.String()) - return "", xerrors.Errorf("failed to exec: %w", err) - } - return stdoutBuf.String(), nil -} - -func FilterTargets(prefixPath string, targets map[string]struct{}) (map[string]struct{}, error) { - filtered := map[string]struct{}{} - for filename := range targets { - if strings.HasPrefix(filename, prefixPath) { - rel, err := filepath.Rel(prefixPath, filename) - if err != nil { - return nil, xerrors.Errorf("error in filepath rel: %w", err) - } - filtered[rel] = struct{}{} - } - } - return filtered, nil -} diff --git a/pkg/uuid/uuid.go b/pkg/uuid/uuid.go new file mode 100644 index 000000000000..8ee0ebf3cccc --- /dev/null +++ b/pkg/uuid/uuid.go @@ -0,0 +1,56 @@ +package uuid + +import ( + "fmt" + "testing" + + "github.com/google/uuid" +) + +type UUID = uuid.UUID + +var ( + newUUID func() uuid.UUID = uuid.New + newUUIDV7 func() (uuid.UUID, error) = uuid.NewV7 + Nil = uuid.Nil + MustParse = uuid.MustParse +) + +// SetFakeUUID sets a fake UUID for testing. +// The 'format' is used to generate a fake UUID and +// must contain a single '%d' which will be replaced with a counter. +func SetFakeUUID(t *testing.T, format string) { + var count int + newUUID = func() uuid.UUID { + count++ + return uuid.Must(uuid.Parse(fmt.Sprintf(format, count))) + } + t.Cleanup(func() { + newUUID = uuid.New + }) +} + +// SetFakeUUIDV7 sets a fake UUID v7 for testing. +// The 'format' is used to generate a fake UUID and +// must contain a single '%d' which will be replaced with a counter. +func SetFakeUUIDV7(t *testing.T, format string) { + var count int + newUUIDV7 = func() (uuid.UUID, error) { + count++ + return uuid.Must(uuid.Parse(fmt.Sprintf(format, count))), nil + } + t.Cleanup(func() { + newUUIDV7 = uuid.NewV7 + }) +} + +func New() UUID { + return newUUID() +} + +// NewV7 generates a new UUID version 7. +// UUIDv7 is time-ordered, combining a timestamp with random bits. +// This makes it suitable for use cases where ordering and database performance are important. +func NewV7() (UUID, error) { + return newUUIDV7() +} diff --git a/pkg/version/app/version.go b/pkg/version/app/version.go new file mode 100644 index 000000000000..d1c7bdbe7d3d --- /dev/null +++ b/pkg/version/app/version.go @@ -0,0 +1,7 @@ +package app + +var ver = "dev" + +func Version() string { + return ver +} diff --git a/pkg/version/doc/doc.go b/pkg/version/doc/doc.go new file mode 100644 index 000000000000..e292f004a6a9 --- /dev/null +++ b/pkg/version/doc/doc.go @@ -0,0 +1,50 @@ +package doc + +import ( + "fmt" + "net/url" + "path" + "strings" + + "github.com/aquasecurity/go-version/pkg/semver" + "github.com/aquasecurity/trivy/pkg/version/app" +) + +const devVersion = "dev" + +// BaseURL returns the base URL for the versioned documentation +func BaseURL(ver string) *url.URL { + ver = canonicalVersion(ver) + path := path.Join("docs", ver) + return &url.URL{ + Scheme: "https", + Host: "trivy.dev", + Path: path, + } +} + +// URL returns the URL for the versioned documentation with the given path +func URL(rawPath, fragment string) string { + base := BaseURL(app.Version()) + base.Path = path.Join(base.Path, rawPath) + base.Fragment = fragment + return base.String() +} + +func canonicalVersion(ver string) string { + if ver == devVersion { + return ver + } + ver = strings.TrimPrefix(ver, "v") + v, err := semver.Parse(ver) + if err != nil { + return devVersion + } + // Replace pre-release with "dev" + // e.g. v0.34.0-beta1+snapshot-1 + if v.IsPreRelease() || v.Metadata() != "" { + return devVersion + } + // Add "v" prefix and cut a patch number, "0.34.0" => "v0.34" for the URL + return fmt.Sprintf("v%d.%d", v.Major(), v.Minor()) +} diff --git a/pkg/version/doc/doc_test.go b/pkg/version/doc/doc_test.go new file mode 100644 index 000000000000..7bae83c4477e --- /dev/null +++ b/pkg/version/doc/doc_test.go @@ -0,0 +1,96 @@ +package doc_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/version/doc" +) + +func TestBaseURL(t *testing.T) { + tests := []struct { + name string + ver string + want string + }{ + { + name: "dev", + ver: "dev", + want: "https://trivy.dev/docs/dev", + }, + { + name: "semver", + ver: "0.52.0", + want: "https://trivy.dev/docs/v0.52", + }, + { + name: "with v prefix", + ver: "v0.52.0", + want: "https://trivy.dev/docs/v0.52", + }, + { + name: "pre-release", + ver: "0.52.0-beta1", + want: "https://trivy.dev/docs/dev", + }, + { + name: "non-semver", + ver: "1", + want: "https://trivy.dev/docs/dev", + }, + { + name: "empty", + ver: "", + want: "https://trivy.dev/docs/dev", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := doc.BaseURL(tt.ver) + require.Equal(t, tt.want, got.String()) + }) + } +} + +func TestURL(t *testing.T) { + tests := []struct { + name string + rawPath string + fragment string + want string + }{ + { + name: "path without slash", + rawPath: "foo", + want: "https://trivy.dev/docs/dev/foo", + }, + { + name: "path with leading slash", + rawPath: "/foo", + want: "https://trivy.dev/docs/dev/foo", + }, + { + name: "path with slash", + rawPath: "foo/bar", + want: "https://trivy.dev/docs/dev/foo/bar", + }, + { + name: "path with fragment", + rawPath: "foo", + fragment: "bar", + want: "https://trivy.dev/docs/dev/foo#bar", + }, + { + name: "empty", + rawPath: "", + want: "https://trivy.dev/docs/dev", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := doc.URL(tt.rawPath, tt.fragment) + require.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/version/testdata/testcache/db/metadata.json b/pkg/version/testdata/testcache/db/metadata.json new file mode 100644 index 000000000000..e9a4157ed5c5 --- /dev/null +++ b/pkg/version/testdata/testcache/db/metadata.json @@ -0,0 +1 @@ +{"Version":2,"NextUpdate":"2023-07-20T18:11:37.696263532Z","UpdatedAt":"2023-07-20T12:11:37.696263932Z","DownloadedAt":"2023-07-25T07:01:41.239158Z"} \ No newline at end of file diff --git a/pkg/version/testdata/testcache/java-db/metadata.json b/pkg/version/testdata/testcache/java-db/metadata.json new file mode 100644 index 000000000000..b8c43fa523c6 --- /dev/null +++ b/pkg/version/testdata/testcache/java-db/metadata.json @@ -0,0 +1 @@ +{"Version":1,"NextUpdate":"2023-07-28T01:03:52.169192565Z","UpdatedAt":"2023-07-25T01:03:52.169192765Z","DownloadedAt":"2023-07-25T09:37:48.906152Z"} diff --git a/pkg/version/testdata/testcache/policy/metadata.json b/pkg/version/testdata/testcache/policy/metadata.json new file mode 100644 index 000000000000..18f6f4801597 --- /dev/null +++ b/pkg/version/testdata/testcache/policy/metadata.json @@ -0,0 +1 @@ +{"Digest":"sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43","DownloadedAt":"2023-07-23T17:40:33.122462+01:00"} \ No newline at end of file diff --git a/pkg/version/version.go b/pkg/version/version.go new file mode 100644 index 000000000000..31ec688a5c06 --- /dev/null +++ b/pkg/version/version.go @@ -0,0 +1,96 @@ +package version + +import ( + "context" + "path/filepath" + + "github.com/aquasecurity/trivy-db/pkg/metadata" + javadb "github.com/aquasecurity/trivy-java-db/pkg/db" + "github.com/aquasecurity/trivy/pkg/db" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/policy" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/version/app" +) + +// VersionOption is a functional option for NewVersionInfo +type VersionOption func(*versionOptions) + +type versionOptions struct { + forServer bool +} + +// Server returns a VersionOption that excludes JavaDB and CheckBundle +// from version info, as these are managed on the client side in client/server mode. +func Server() VersionOption { + return func(o *versionOptions) { + o.forServer = true + } +} + +func NewVersionInfo(cacheDir string, opts ...VersionOption) types.VersionInfo { + var options versionOptions + for _, opt := range opts { + opt(&options) + } + + var dbMeta *metadata.Metadata + var javadbMeta *metadata.Metadata + var pbMeta *types.BundleMetadata + + mc := metadata.NewClient(db.Dir(cacheDir)) + meta, err := mc.Get() + if err != nil { + log.Debug("Failed to get DB metadata", log.Err(err)) + } + if !meta.UpdatedAt.IsZero() && !meta.NextUpdate.IsZero() && meta.Version != 0 { + dbMeta = &metadata.Metadata{ + Version: meta.Version, + NextUpdate: meta.NextUpdate.UTC(), + UpdatedAt: meta.UpdatedAt.UTC(), + DownloadedAt: meta.DownloadedAt.UTC(), + } + } + + // Skip JavaDB and CheckBundle for server mode as they are managed on the client side + if !options.forServer { + mcJava := javadb.NewMetadata(filepath.Join(cacheDir, "java-db")) + metaJava, err := mcJava.Get() + if err != nil { + log.Debug("Failed to get Java DB metadata", log.Err(err)) + } + if !metaJava.UpdatedAt.IsZero() && !metaJava.NextUpdate.IsZero() && metaJava.Version != 0 { + javadbMeta = &metadata.Metadata{ + Version: metaJava.Version, + NextUpdate: metaJava.NextUpdate.UTC(), + UpdatedAt: metaJava.UpdatedAt.UTC(), + DownloadedAt: metaJava.DownloadedAt.UTC(), + } + } + + pc, err := policy.NewClient(cacheDir, false, "") + if err != nil { + log.Debug("Failed to instantiate policy client", log.Err(err)) + } + if pc != nil && err == nil { + ctx := log.WithContextPrefix(context.TODO(), log.PrefixMisconfiguration) + pbMetaRaw, err := pc.GetMetadata(ctx) + + if err != nil { + log.Debug("Failed to get policy metadata", log.Err(err)) + } else { + pbMeta = &types.BundleMetadata{ + Digest: pbMetaRaw.Digest, + DownloadedAt: pbMetaRaw.DownloadedAt.UTC(), + } + } + } + } + + return types.VersionInfo{ + Version: app.Version(), + VulnerabilityDB: dbMeta, + JavaDB: javadbMeta, + CheckBundle: pbMeta, + } +} diff --git a/pkg/version/version_test.go b/pkg/version/version_test.go new file mode 100644 index 000000000000..94efab7cc20a --- /dev/null +++ b/pkg/version/version_test.go @@ -0,0 +1,85 @@ +package version + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy-db/pkg/metadata" + "github.com/aquasecurity/trivy/pkg/types" +) + +func TestNewVersionInfo(t *testing.T) { + tests := []struct { + name string + opts []VersionOption + want types.VersionInfo + }{ + { + name: "default", + opts: nil, + want: types.VersionInfo{ + Version: "dev", + VulnerabilityDB: &metadata.Metadata{ + Version: 2, + NextUpdate: time.Date(2023, 7, 20, 18, 11, 37, 696263532, time.UTC), + UpdatedAt: time.Date(2023, 7, 20, 12, 11, 37, 696263932, time.UTC), + DownloadedAt: time.Date(2023, 7, 25, 7, 1, 41, 239158000, time.UTC), + }, + JavaDB: &metadata.Metadata{ + Version: 1, + NextUpdate: time.Date(2023, 7, 28, 1, 3, 52, 169192565, time.UTC), + UpdatedAt: time.Date(2023, 7, 25, 1, 3, 52, 169192765, time.UTC), + DownloadedAt: time.Date(2023, 7, 25, 9, 37, 48, 906152000, time.UTC), + }, + CheckBundle: &types.BundleMetadata{ + Digest: "sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43", + DownloadedAt: time.Date(2023, 7, 23, 16, 40, 33, 122462000, time.UTC), + }, + }, + }, + { + name: "server mode excludes JavaDB and CheckBundle", + opts: []VersionOption{Server()}, + want: types.VersionInfo{ + Version: "dev", + VulnerabilityDB: &metadata.Metadata{ + Version: 2, + NextUpdate: time.Date(2023, 7, 20, 18, 11, 37, 696263532, time.UTC), + UpdatedAt: time.Date(2023, 7, 20, 12, 11, 37, 696263932, time.UTC), + DownloadedAt: time.Date(2023, 7, 25, 7, 1, 41, 239158000, time.UTC), + }, + JavaDB: nil, + CheckBundle: nil, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := NewVersionInfo("testdata/testcache", tt.opts...) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestVersionInfo_String(t *testing.T) { + want := `Version: dev +Vulnerability DB: + Version: 2 + UpdatedAt: 2023-07-20 12:11:37.696263932 +0000 UTC + NextUpdate: 2023-07-20 18:11:37.696263532 +0000 UTC + DownloadedAt: 2023-07-25 07:01:41.239158 +0000 UTC +Java DB: + Version: 1 + UpdatedAt: 2023-07-25 01:03:52.169192765 +0000 UTC + NextUpdate: 2023-07-28 01:03:52.169192565 +0000 UTC + DownloadedAt: 2023-07-25 09:37:48.906152 +0000 UTC +Check Bundle: + Digest: sha256:829832357626da2677955e3b427191212978ba20012b6eaa03229ca28569ae43 + DownloadedAt: 2023-07-23 16:40:33.122462 +0000 UTC +` + got := NewVersionInfo("testdata/testcache") + assert.Equal(t, want, got.String()) +} diff --git a/pkg/vex/csaf.go b/pkg/vex/csaf.go new file mode 100644 index 000000000000..0c0f994db168 --- /dev/null +++ b/pkg/vex/csaf.go @@ -0,0 +1,177 @@ +package vex + +import ( + "github.com/gocsaf/csaf/v3/csaf" + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" +) + +type CSAF struct { + advisory csaf.Advisory + source string + logger *log.Logger +} + +type relationship struct { + Product *purl.PackageURL + SubProducts []*purl.PackageURL +} + +func newCSAF(advisory csaf.Advisory, source string) *CSAF { + return &CSAF{ + advisory: advisory, + source: source, + logger: log.WithPrefix("vex").With(log.String("format", "CSAF")), + } +} + +func (v *CSAF) Filter(result *types.Result, bom *core.BOM) { + filterVulnerabilities(result, bom, v.NotAffected) +} + +func (v *CSAF) NotAffected(vuln types.DetectedVulnerability, product, subProduct *core.Component) (types.ModifiedFinding, bool) { + found, ok := lo.Find(v.advisory.Vulnerabilities, func(item *csaf.Vulnerability) bool { + return string(*item.CVE) == vuln.VulnerabilityID + }) + if !ok { + return types.ModifiedFinding{}, false + } + + status := v.match(found, product, subProduct) + if status == "" { + return types.ModifiedFinding{}, false + } + return types.NewModifiedFinding(vuln, status, v.statement(found), v.source), true +} + +func (v *CSAF) match(vuln *csaf.Vulnerability, product, subProduct *core.Component) types.FindingStatus { + if product == nil || product.PkgIdentifier.PURL == nil || vuln.ProductStatus == nil { + return "" + } + + productStatusMap := map[types.FindingStatus]csaf.Products{ + types.FindingStatusNotAffected: lo.FromPtr(vuln.ProductStatus.KnownNotAffected), + types.FindingStatusFixed: lo.FromPtr(vuln.ProductStatus.Fixed), + } + for status, productRange := range productStatusMap { + for _, p := range productRange { + productID := lo.FromPtr(p) + logger := v.logger.With(log.String("vulnerability-id", string(*vuln.CVE)), + log.String("product-id", string(productID)), log.String("status", string(status))) + + // Check if the product is affected + if v.matchProduct(productID, product) { + logger.Info("Filtered out the detected vulnerability") + return status + } + + // Check if the relationship between the product and the subcomponent is affected + if category, match := v.matchRelationship(productID, product, subProduct); match { + logger.Info("Filtered out the detected vulnerability", + log.String("relationship", string(category))) + return status + } + } + } + return "" +} + +func (v *CSAF) matchProduct(productID csaf.ProductID, product *core.Component) bool { + for _, productPURL := range v.productPURLs(productID) { + if productPURL.Match(product.PkgIdentifier.PURL) { + return true + } + } + return false +} + +func (v *CSAF) matchRelationship(fullProductID csaf.ProductID, product, subProduct *core.Component) ( + csaf.RelationshipCategory, bool) { + + for category, relationships := range v.inspectProductRelationships(fullProductID) { + for _, rel := range relationships { + if !rel.Product.Match(product.PkgIdentifier.PURL) { + continue + } + for _, subProductPURL := range rel.SubProducts { + if subProductPURL.Match(subProduct.PkgIdentifier.PURL) { + return category, true + } + } + } + } + return "", false +} + +// productPURLs returns a slice of PackageURLs associated to a given product +func (v *CSAF) productPURLs(product csaf.ProductID) []*purl.PackageURL { + return v.purlsFromProductIdentificationHelpers(v.advisory.ProductTree.CollectProductIdentificationHelpers(product)) +} + +// inspectProductRelationships returns a map of PackageURLs associated to each relationship category +// iterating over relationships looking for sub-products that might be part of the original product +func (v *CSAF) inspectProductRelationships(fullProductID csaf.ProductID) map[csaf.RelationshipCategory][]relationship { + if v.advisory.ProductTree.RelationShips == nil { + return nil + } + + relationships := make(map[csaf.RelationshipCategory][]relationship) + for _, rel := range lo.FromPtr(v.advisory.ProductTree.RelationShips) { + if rel == nil || rel.FullProductName == nil { + continue + } else if lo.FromPtr(rel.FullProductName.ProductID) != fullProductID { + continue + } + + category := lo.FromPtr(rel.Category) + switch category { + case csaf.CSAFRelationshipCategoryDefaultComponentOf, + csaf.CSAFRelationshipCategoryInstalledOn, + csaf.CSAFRelationshipCategoryInstalledWith: + + productID := lo.FromPtr(rel.RelatesToProductReference) + productPURLs := v.productPURLs(productID) + + subProductID := lo.FromPtr(rel.ProductReference) + subProductPURLs := v.productPURLs(subProductID) + + for _, productPURL := range productPURLs { + relationships[category] = append(relationships[category], relationship{ + Product: productPURL, + SubProducts: subProductPURLs, + }) + } + } + } + + return relationships +} + +// purlsFromProductIdentificationHelpers returns a slice of PURLs given a slice of ProductIdentificationHelpers. +func (v *CSAF) purlsFromProductIdentificationHelpers(helpers []*csaf.ProductIdentificationHelper) []*purl.PackageURL { + return lo.FilterMap(helpers, func(helper *csaf.ProductIdentificationHelper, _ int) (*purl.PackageURL, bool) { + if helper == nil || helper.PURL == nil { + return nil, false + } + p, err := purl.FromString(string(*helper.PURL)) + if err != nil { + v.logger.Error("Invalid PURL", log.String("purl", string(*helper.PURL)), log.Err(err)) + return nil, false + } + return p, true + }) +} + +func (v *CSAF) statement(vuln *csaf.Vulnerability) string { + threat, ok := lo.Find(vuln.Threats, func(threat *csaf.Threat) bool { + return lo.FromPtr(threat.Category) == csaf.CSAFThreatCategoryImpact + }) + if !ok { + return "" + } + return lo.FromPtr(threat.Details) +} diff --git a/pkg/vex/cyclonedx.go b/pkg/vex/cyclonedx.go new file mode 100644 index 000000000000..a4fb61f695e5 --- /dev/null +++ b/pkg/vex/cyclonedx.go @@ -0,0 +1,88 @@ +package vex + +import ( + cdx "github.com/CycloneDX/cyclonedx-go" + "github.com/samber/lo" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +type CycloneDX struct { + sbom *core.BOM + statements map[string]Statement + logger *log.Logger +} + +type Statement struct { + Affects []string + Status types.FindingStatus + Justification string +} + +func newCycloneDX(sbom *core.BOM, vex *cdx.BOM) *CycloneDX { + statements := make(map[string]Statement) + for _, vuln := range lo.FromPtr(vex.Vulnerabilities) { + affects := xslices.Map(lo.FromPtr(vuln.Affects), func(item cdx.Affects) string { + return item.Ref + }) + + analysis := lo.FromPtr(vuln.Analysis) + statements[vuln.ID] = Statement{ + Affects: affects, + Status: cdxStatus(analysis.State), + Justification: string(analysis.Justification), + } + } + return &CycloneDX{ + sbom: sbom, + statements: statements, + logger: log.WithPrefix("vex").With(log.String("format", "CycloneDX")), + } +} + +func (v *CycloneDX) NotAffected(vuln types.DetectedVulnerability, product, _ *core.Component) (types.ModifiedFinding, bool) { + stmt, ok := v.statements[vuln.VulnerabilityID] + if !ok { + return types.ModifiedFinding{}, false + } + + for _, affect := range stmt.Affects { + if stmt.Status != types.FindingStatusNotAffected && stmt.Status != types.FindingStatusFixed { + continue + } + + // Affect must be BOM-Link at the moment + link, err := cdx.ParseBOMLink(affect) + if err != nil { + v.logger.Warn("Unable to parse BOM-Link", log.String("affect", affect)) + continue + } + if v.sbom.SerialNumber != link.SerialNumber() || v.sbom.Version != link.Version() { + v.logger.Warn("URN doesn't match with SBOM", + log.String("serial number", link.SerialNumber()), + log.Int("version", link.Version())) + continue + } + if product.PkgIdentifier.Match(link.Reference()) { + return types.NewModifiedFinding(vuln, stmt.Status, stmt.Justification, "CycloneDX VEX"), true + } + } + return types.ModifiedFinding{}, false +} + +func cdxStatus(s cdx.ImpactAnalysisState) types.FindingStatus { + switch s { + case cdx.IASResolved, cdx.IASResolvedWithPedigree: + return types.FindingStatusFixed + case cdx.IASExploitable: + return types.FindingStatusAffected + case cdx.IASInTriage: + return types.FindingStatusUnderInvestigation + case cdx.IASFalsePositive, cdx.IASNotAffected: + return types.FindingStatusNotAffected + } + return types.FindingStatusUnknown +} diff --git a/pkg/vex/document.go b/pkg/vex/document.go new file mode 100644 index 000000000000..4019ed7eb274 --- /dev/null +++ b/pkg/vex/document.go @@ -0,0 +1,106 @@ +package vex + +import ( + "encoding/json" + "io" + "os" + + "github.com/gocsaf/csaf/v3/csaf" + "github.com/hashicorp/go-multierror" + openvex "github.com/openvex/go-vex/pkg/vex" + "github.com/sirupsen/logrus" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom" + "github.com/aquasecurity/trivy/pkg/sbom/cyclonedx" + "github.com/aquasecurity/trivy/pkg/types" +) + +func NewDocument(filePath string, report *types.Report) (VEX, error) { + if filePath == "" { + return nil, xerrors.New("VEX file path is empty") + } + f, err := os.Open(filePath) + if err != nil { + return nil, xerrors.Errorf("file open error: %w", err) + } + defer f.Close() + + v, errs := decodeVEX(f, filePath, report) + if errs != nil { + return nil, xerrors.Errorf("unable to load VEX from file: %w", errs) + } + return v, nil +} + +func decodeVEX(r io.ReadSeeker, source string, report *types.Report) (VEX, error) { + var errs error + // Try CycloneDX JSON + if ok, err := sbom.IsCycloneDXJSON(r); err != nil { + errs = multierror.Append(errs, err) + } else if ok { + return decodeCycloneDXJSON(r, report) + } + + // Try OpenVEX + if v, err := decodeOpenVEX(r, source); err != nil { + errs = multierror.Append(errs, err) + } else if v != nil { + return v, nil + } + + // Try CSAF + if v, err := decodeCSAF(r, source); err != nil { + errs = multierror.Append(errs, err) + } else if v != nil { + return v, nil + } + + return nil, xerrors.Errorf("unable to decode VEX: %w", errs) +} + +func decodeCycloneDXJSON(r io.ReadSeeker, report *types.Report) (*CycloneDX, error) { + if _, err := r.Seek(0, io.SeekStart); err != nil { + return nil, xerrors.Errorf("seek error: %w", err) + } + vex, err := cyclonedx.DecodeJSON(r) + if err != nil { + return nil, xerrors.Errorf("json decode error: %w", err) + } + if report.ArtifactType != ftypes.TypeCycloneDX { + return nil, xerrors.New("CycloneDX VEX can be used with CycloneDX SBOM") + } + return newCycloneDX(report.BOM, vex), nil +} + +func decodeOpenVEX(r io.ReadSeeker, source string) (*OpenVEX, error) { + // openvex/go-vex outputs log messages by default + logrus.SetOutput(io.Discard) + + if _, err := r.Seek(0, io.SeekStart); err != nil { + return nil, xerrors.Errorf("seek error: %w", err) + } + var openVEX openvex.VEX + if err := json.NewDecoder(r).Decode(&openVEX); err != nil { + return nil, err + } + if openVEX.Context == "" { + return nil, nil + } + return newOpenVEX(openVEX, source), nil +} + +func decodeCSAF(r io.ReadSeeker, source string) (*CSAF, error) { + if _, err := r.Seek(0, io.SeekStart); err != nil { + return nil, xerrors.Errorf("seek error: %w", err) + } + var adv csaf.Advisory + if err := json.NewDecoder(r).Decode(&adv); err != nil { + return nil, err + } + if adv.Vulnerabilities == nil { + return nil, nil + } + return newCSAF(adv, source), nil +} diff --git a/pkg/vex/oci.go b/pkg/vex/oci.go new file mode 100644 index 000000000000..9640339b3c86 --- /dev/null +++ b/pkg/vex/oci.go @@ -0,0 +1,54 @@ +package vex + +import ( + "fmt" + + "github.com/openvex/discovery/pkg/discovery" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/types" +) + +type OCI struct{} + +func NewOCI(report *types.Report) (*OpenVEX, error) { + if report.ArtifactType != ftypes.TypeContainerImage || len(report.Metadata.RepoDigests) == 0 { + return nil, xerrors.New("'--vex oci' can be used only when scanning OCI artifacts stored in registries") + } + + // TODO(knqyf263): Add the PURL field to Report.Metadata + p, err := purl.New(purl.TypeOCI, report.Metadata, ftypes.Package{}) + if err != nil { + return nil, xerrors.Errorf("failed to create a package URL: %w", err) + } + + v, err := RetrieveVEXAttestation(p) + if err != nil { + return nil, xerrors.Errorf("failed to retrieve VEX attestation: %w", err) + } + return v, nil +} + +func RetrieveVEXAttestation(p *purl.PackageURL) (*OpenVEX, error) { + logger := log.WithPrefix("vex").With(log.String("type", "oci"), + log.String("purl", p.String())) + + // Probe the OCI artifact and retrieve VEX documents + vexDocuments, err := discovery.NewAgent().ProbePurl(p.String()) + if err != nil { + return nil, xerrors.Errorf("failed to probe the package URL: %w", err) + } + if len(vexDocuments) == 0 { + logger.Info("No VEX attestations found") + return nil, nil + } + + logger.Debug("VEX attestation found, taking the first one") + return &OpenVEX{ + vex: *vexDocuments[0], + source: fmt.Sprintf("VEX attestation in OCI registry (%s)", p.String()), + }, nil +} diff --git a/pkg/vex/oci_test.go b/pkg/vex/oci_test.go new file mode 100644 index 000000000000..e803d6ab20aa --- /dev/null +++ b/pkg/vex/oci_test.go @@ -0,0 +1,146 @@ +package vex_test + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "math/rand" + "net/http/httptest" + "strings" + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/empty" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/random" + "github.com/google/go-containerregistry/pkg/v1/static" + "github.com/in-toto/in-toto-golang/in_toto" + openvex "github.com/openvex/go-vex/pkg/vex" + "github.com/package-url/packageurl-go" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/testdocker/auth" + "github.com/aquasecurity/testdocker/registry" + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/purl" + "github.com/aquasecurity/trivy/pkg/vex" +) + +func setUpRegistry(t *testing.T) (*httptest.Server, v1.Hash) { + imgWithVEX := setUpImage(t) + d, err := imgWithVEX.Digest() + require.NoError(t, err) + + images := map[string]v1.Image{ + "v2/debian:latest": imgWithVEX, + "v2/debian:no-vex": setUpImage(t), + fmt.Sprintf("v2/debian@%s", d.String()): imgWithVEX, + fmt.Sprintf("v2/debian:sha256-%s.att", d.Hex): setUpVEXAttestation(t), // VEX attestation + } + + tr := registry.NewDockerRegistry(registry.Option{ + Images: images, + Auth: auth.Auth{}, + }) + + return tr, d +} + +func setUpImage(t *testing.T) v1.Image { + img, err := random.Image(100, 1, random.WithSource(rand.NewSource(0))) + require.NoError(t, err) + + return img +} + +func setUpVEXAttestation(t *testing.T) v1.Image { + envelope := createVEXAttestation(t) + b, err := json.Marshal(envelope) + require.NoError(t, err) + + layer := static.NewLayer(b, "application/vnd.dsse.envelope.v1+json") + newImage, err := mutate.AppendLayers(empty.Image, layer) + require.NoError(t, err) + + return newImage +} + +func createVEXAttestation(t *testing.T) dsse.Envelope { + var v openvex.VEX + testutil.MustReadJSON(t, "testdata/openvex-oci.json", &v) + + // in-toto Statement + statement := in_toto.Statement{ + StatementHeader: in_toto.StatementHeader{ + Type: "https://in-toto.io/Statement/v0.1", + PredicateType: "https://openvex.dev/ns", + Subject: []in_toto.Subject{ + { + Name: "example", + Digest: map[string]string{ + "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + }, + }, + }, + }, + Predicate: v, + } + + attestationJSON, err := json.Marshal(statement) + require.NoError(t, err) + + // Base64 encode + encodedAttestation := base64.StdEncoding.EncodeToString(attestationJSON) + + // Create a DSSE envelope + return dsse.Envelope{ + PayloadType: "application/vnd.in-toto+json", + Payload: encodedAttestation, + Signatures: nil, + } +} + +func TestRetrieveVEXAttestation(t *testing.T) { + tr, _ := setUpRegistry(t) + t.Cleanup(tr.Close) + + tests := []struct { + name string + url string + wantErr require.ErrorAssertionFunc + }{ + { + name: "vex found", + url: strings.TrimPrefix(tr.URL, "http://") + "/debian:latest", + wantErr: require.NoError, + }, + { + name: "vex not found", + url: strings.TrimPrefix(tr.URL, "http://") + "/debian:no-vex", + wantErr: require.NoError, + }, + { + name: "image not found", + url: strings.TrimPrefix(tr.URL, "http://") + "/debian:no-such-image", + wantErr: require.Error, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &purl.PackageURL{ + Type: packageurl.TypeOCI, + Name: "debian", + Qualifiers: packageurl.Qualifiers{ + { + Key: "repository_url", + Value: tt.url, + }, + }, + } + _, err := vex.RetrieveVEXAttestation(p) + tt.wantErr(t, err) + }) + } +} diff --git a/pkg/vex/openvex.go b/pkg/vex/openvex.go new file mode 100644 index 000000000000..d300de66ff61 --- /dev/null +++ b/pkg/vex/openvex.go @@ -0,0 +1,66 @@ +package vex + +import ( + openvex "github.com/openvex/go-vex/pkg/vex" + + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" +) + +type OpenVEX struct { + vex openvex.VEX + source string +} + +func newOpenVEX(vex openvex.VEX, source string) *OpenVEX { + return &OpenVEX{ + vex: vex, + source: source, + } +} + +func (v *OpenVEX) Filter(result *types.Result, bom *core.BOM) { + filterVulnerabilities(result, bom, v.NotAffected) +} + +func (v *OpenVEX) NotAffected(vuln types.DetectedVulnerability, product, subComponent *core.Component) (types.ModifiedFinding, bool) { + stmts := v.Matches(vuln, product, subComponent) + if len(stmts) == 0 { + return types.ModifiedFinding{}, false + } + + // Take the latest statement for a given vulnerability and product + // as a sequence of statements can be overridden by the newer one. + // cf. https://github.com/openvex/spec/blob/fa5ba0c0afedb008dc5ebad418548cacf16a3ca7/OPENVEX-SPEC.md#the-vex-statement + stmt := stmts[len(stmts)-1] + if stmt.Status == openvex.StatusNotAffected || stmt.Status == openvex.StatusFixed { + modifiedFindings := types.NewModifiedFinding(vuln, findingStatus(stmt.Status), string(stmt.Justification), v.source) + return modifiedFindings, true + } + return types.ModifiedFinding{}, false +} + +func (v *OpenVEX) Matches(vuln types.DetectedVulnerability, product, subComponent *core.Component) []openvex.Statement { + if product == nil || product.PkgIdentifier.PURL == nil { + return nil + } + + var subComponentPURL string + if subComponent != nil && subComponent.PkgIdentifier.PURL != nil { + subComponentPURL = subComponent.PkgIdentifier.PURL.String() + } + return v.vex.Matches(vuln.VulnerabilityID, product.PkgIdentifier.PURL.String(), []string{subComponentPURL}) +} + +func findingStatus(status openvex.Status) types.FindingStatus { + switch status { + case openvex.StatusNotAffected: + return types.FindingStatusNotAffected + case openvex.StatusFixed: + return types.FindingStatusFixed + case openvex.StatusUnderInvestigation: + return types.FindingStatusUnderInvestigation + default: + return types.FindingStatusUnknown + } +} diff --git a/pkg/vex/repo.go b/pkg/vex/repo.go new file mode 100644 index 000000000000..d7c6717b8e52 --- /dev/null +++ b/pkg/vex/repo.go @@ -0,0 +1,143 @@ +package vex + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/package-url/packageurl-go" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vex/repo" + xsync "github.com/aquasecurity/trivy/pkg/x/sync" +) + +var errNoRepository = errors.New("no available VEX repository found") + +// RepositoryIndex wraps the repository index +type RepositoryIndex struct { + Name string + URL string + repo.Index +} + +type RepositorySet struct { + indexes []RepositoryIndex + logOnce *xsync.Map[string, *sync.Once] + logger *log.Logger +} + +func NewRepositorySet(ctx context.Context, cacheDir string) (*RepositorySet, error) { + conf, err := repo.NewManager(cacheDir).Config(ctx) + if err != nil { + return nil, xerrors.Errorf("failed to get VEX repository config: %w", err) + } + + logger := log.WithPrefix("vex") + var indexes []RepositoryIndex + for _, r := range conf.EnabledRepositories() { + index, err := r.Index(ctx) + if errors.Is(err, os.ErrNotExist) { + logger.Warn("VEX repository not found locally, skipping this repository", log.String("repo", r.Name)) + continue + } else if err != nil { + return nil, xerrors.Errorf("failed to get VEX repository index: %w", err) + } + indexes = append(indexes, RepositoryIndex{ + Name: r.Name, + URL: r.URL, + Index: index, + }) + } + if len(indexes) == 0 { + logger.Warn("No available VEX repository found locally") + return nil, errNoRepository + } + + return &RepositorySet{ + indexes: indexes, // In precedence order + logOnce: new(xsync.Map[string, *sync.Once]), + logger: logger, + }, nil +} + +func (rs *RepositorySet) NotAffected(vuln types.DetectedVulnerability, product, subComponent *core.Component) (types.ModifiedFinding, bool) { + if product == nil || product.PkgIdentifier.PURL == nil { + return types.ModifiedFinding{}, false + } + p := *product.PkgIdentifier.PURL + + // Exclude version, qualifiers, and subpath from the package URL except for OCI + // cf. https://github.com/aquasecurity/vex-repo-spec?tab=readme-ov-file#32-indexjson + p.Version = "" + p.Qualifiers = nil + p.Subpath = "" + + if p.Type == packageurl.TypeOCI { + // For OCI artifacts, we consider "repository_url" is part of name. + for _, q := range product.PkgIdentifier.PURL.Qualifiers { + if q.Key == "repository_url" { + p.Qualifiers = packageurl.Qualifiers{q} + break + } + } + } + + pkgID := p.String() // PURL without version, qualifiers, and subpath + for _, index := range rs.indexes { + entry, ok := index.Packages[pkgID] + if !ok { + continue + } + rs.logVEXFound(pkgID, index.Name, index.URL, entry.Location) + + source := fmt.Sprintf("VEX Repository: %s (%s)", index.Name, index.URL) + doc, err := rs.OpenDocument(source, filepath.Dir(index.Path), entry) + if err != nil { + rs.logger.Warn("Failed to open the VEX document", log.String("location", entry.Location), log.Err(err)) + return types.ModifiedFinding{}, false + } + + if m, notAffected := doc.NotAffected(vuln, product, subComponent); notAffected { + return m, notAffected + } + + break // Stop searching for the next VEX document as this repository has higher precedence. + } + return types.ModifiedFinding{}, false +} + +func (rs *RepositorySet) OpenDocument(source, dir string, entry repo.PackageEntry) (VEX, error) { + f, err := os.Open(filepath.Join(dir, entry.Location)) + if err != nil { + return nil, xerrors.Errorf("failed to open the VEX document: %w", err) + } + defer f.Close() + + switch entry.Format { + case "openvex", "": + return decodeOpenVEX(f, source) + case "csaf": + return decodeCSAF(f, source) + default: + return nil, xerrors.Errorf("unsupported VEX format: %s", entry.Format) + } +} + +func (rs *RepositorySet) logVEXFound(pkgID, repoName, repoURL, filePath string) { + once, _ := rs.logOnce.LoadOrStore(pkgID, &sync.Once{}) + once.Do(func() { + rs.logger.Debug("VEX found in the repository", + log.String("package", pkgID), + log.String("repo", repoName), + log.String("repo_url", repoURL), + log.FilePath(filePath), + ) + }) +} diff --git a/pkg/vex/repo/manager.go b/pkg/vex/repo/manager.go new file mode 100644 index 000000000000..bcbce9bedb33 --- /dev/null +++ b/pkg/vex/repo/manager.go @@ -0,0 +1,193 @@ +package repo + +import ( + "context" + "fmt" + "io" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/samber/lo" + "golang.org/x/xerrors" + "gopkg.in/yaml.v3" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +const ( + defaultVEXHubURL = "https://github.com/aquasecurity/vexhub" + vexDir = "vex" + repoDir = "repositories" +) + +type ManagerOption func(indexer *Manager) + +func WithWriter(w io.Writer) ManagerOption { + return func(manager *Manager) { + manager.w = w + } +} + +type Config struct { + Repositories []Repository `json:"repositories"` +} + +func (c *Config) EnabledRepositories() []Repository { + return lo.Filter(c.Repositories, func(r Repository, _ int) bool { + return r.Enabled + }) +} + +type Options struct { + Insecure bool +} + +// Manager manages the repositories +type Manager struct { + w io.Writer + configFile string + cacheDir string +} + +func NewManager(cacheRoot string, opts ...ManagerOption) *Manager { + m := &Manager{ + w: os.Stdout, + configFile: filepath.Join(fsutils.TrivyHomeDir(), vexDir, "repository.yaml"), + cacheDir: filepath.Join(cacheRoot, vexDir), + } + for _, opt := range opts { + opt(m) + } + + return m +} + +func (m *Manager) writeConfig(conf Config) error { + if err := os.MkdirAll(filepath.Dir(m.configFile), 0o700); err != nil { + return xerrors.Errorf("failed to mkdir: %w", err) + } + f, err := os.Create(m.configFile) + if err != nil { + return xerrors.Errorf("failed to create a file: %w", err) + } + defer f.Close() + + e := yaml.NewEncoder(f) + e.SetIndent(2) + if err = e.Encode(conf); err != nil { + return xerrors.Errorf("JSON encode error: %w", err) + } + + return nil +} + +func (m *Manager) Config(ctx context.Context) (Config, error) { + if !fsutils.FileExists(m.configFile) { + log.DebugContext(ctx, "No repository config found", log.String("path", m.configFile)) + if err := m.Init(ctx); err != nil { + return Config{}, xerrors.Errorf("unable to initialize the VEX repository config: %w", err) + } + } + + f, err := os.Open(m.configFile) + if err != nil { + return Config{}, xerrors.Errorf("unable to open a file: %w", err) + } + defer f.Close() + + var conf Config + if err = yaml.NewDecoder(f).Decode(&conf); err != nil { + return conf, xerrors.Errorf("unable to decode metadata: %w", err) + } + + for i, repo := range conf.Repositories { + conf.Repositories[i].dir = filepath.Join(m.cacheDir, repoDir, repo.Name) + } + + return conf, nil +} + +func (m *Manager) Init(ctx context.Context) error { + if fsutils.FileExists(m.configFile) { + log.InfoContext(ctx, "The configuration file already exists", log.String("path", m.configFile)) + return nil + } + + err := m.writeConfig(Config{ + Repositories: []Repository{ + { + Name: "default", + URL: defaultVEXHubURL, + Enabled: true, + }, + }, + }) + if err != nil { + return xerrors.Errorf("failed to write the default config: %w", err) + } + log.InfoContext(ctx, "The default repository config has been created", log.FilePath(m.configFile)) + return nil +} + +func (m *Manager) DownloadRepositories(ctx context.Context, names []string, opts Options) error { + conf, err := m.Config(ctx) + if err != nil { + return xerrors.Errorf("unable to read config: %w", err) + } + + repos := lo.Filter(conf.EnabledRepositories(), func(r Repository, _ int) bool { + return len(names) == 0 || slices.Contains(names, r.Name) + }) + if len(repos) == 0 { + log.WarnContext(ctx, "No enabled repositories found in config", log.String("path", m.configFile)) + return nil + } + + for _, repo := range repos { + if err = repo.Update(ctx, opts); err != nil { + return xerrors.Errorf("failed to update the repository: %w", err) + } + } + return nil +} + +// List returns a list of all repositories in the configuration +func (m *Manager) List(ctx context.Context) error { + conf, err := m.Config(ctx) + if err != nil { + return xerrors.Errorf("unable to read config: %w", err) + } + + var output strings.Builder + + output.WriteString(fmt.Sprintf("VEX Repositories (config: %s)\n\n", m.configFile)) + + if len(conf.Repositories) == 0 { + output.WriteString("No repositories configured.\n") + } else { + for _, repo := range conf.Repositories { + status := "Enabled" + if !repo.Enabled { + status = "Disabled" + } + tlsVerify := "" + if repo.Insecure { + tlsVerify = "\n TLS Verify: No" + } + output.WriteString(fmt.Sprintf("- Name: %s\n URL: %s\n Status: %s%s\n\n", repo.Name, repo.URL, status, tlsVerify)) + } + } + + if _, err = io.WriteString(m.w, output.String()); err != nil { + return xerrors.Errorf("failed to write output: %w", err) + } + + return nil +} + +func (m *Manager) Clear() error { + return os.RemoveAll(m.cacheDir) +} diff --git a/pkg/vex/repo/manager_test.go b/pkg/vex/repo/manager_test.go new file mode 100644 index 000000000000..97faf4cfb7b5 --- /dev/null +++ b/pkg/vex/repo/manager_test.go @@ -0,0 +1,354 @@ +package repo_test + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/vex/repo" +) + +func TestManager_Config(t *testing.T) { + tests := []struct { + name string + setup func(*testing.T, string) + want repo.Config + wantErr string + }{ + { + name: "config file exists", + setup: func(t *testing.T, dir string) { + config := repo.Config{ + Repositories: []repo.Repository{ + { + Name: "test-repo", + URL: "https://example.com/repo", + Enabled: true, + }, + }, + } + configPath := filepath.Join(dir, ".trivy", "vex", "repository.yaml") + testutil.MustWriteYAML(t, configPath, config) + }, + want: repo.Config{ + Repositories: []repo.Repository{ + { + Name: "test-repo", + URL: "https://example.com/repo", + Enabled: true, + }, + }, + }, + }, + { + name: "config file does not exist", + setup: func(_ *testing.T, _ string) {}, + want: repo.Config{ + Repositories: []repo.Repository{ + { + Name: "default", + URL: "https://github.com/aquasecurity/vexhub", + Enabled: true, + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tempDir) + m := repo.NewManager(tempDir) + + tt.setup(t, tempDir) + + got, err := m.Config(t.Context()) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.EqualExportedValues(t, tt.want, got) + }) + } +} + +func TestManager_Init(t *testing.T) { + tests := []struct { + name string + setup func(*testing.T, string) + want repo.Config + wantErr string + }{ + { + name: "successful init", + setup: func(_ *testing.T, _ string) {}, + want: repo.Config{ + Repositories: []repo.Repository{ + { + Name: "default", + URL: "https://github.com/aquasecurity/vexhub", + Enabled: true, + }, + }, + }, + }, + { + name: "config already exists", + setup: func(t *testing.T, dir string) { + configPath := filepath.Join(dir, ".trivy", "vex", "repository.yaml") + testutil.MustWriteYAML(t, configPath, repo.Config{}) + }, + want: repo.Config{ + Repositories: []repo.Repository{}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tempDir) + m := repo.NewManager(tempDir) + + tt.setup(t, tempDir) + + err := m.Init(t.Context()) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + configPath := filepath.Join(tempDir, ".trivy", "vex", "repository.yaml") + assert.FileExists(t, configPath) + + var got repo.Config + testutil.MustReadYAML(t, configPath, &got) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestManager_DownloadRepositories(t *testing.T) { + ts := setUpRepository(t) + defer ts.Close() + + tests := []struct { + name string + config repo.Config + location string + names []string + wantErr string + wantDownload bool + }{ + { + name: "successful download", + config: repo.Config{ + Repositories: []repo.Repository{ + { + Name: "test-repo", + URL: ts.URL, + Enabled: true, + Insecure: true, + }, + }, + }, + location: ts.URL + "/archive.zip", + wantDownload: true, + }, + { + name: "no enabled repositories", + config: repo.Config{ + Repositories: []repo.Repository{ + { + Name: "test-repo", + URL: "https://localhost:10000", // Will not be reached + Enabled: false, + }, + }, + }, + location: ts.URL + "/archive.zip", + wantDownload: false, + }, + { + name: "download specific repository", + config: repo.Config{ + Repositories: []repo.Repository{ + { + Name: "another-repo", + URL: "https://example.com/repo", + Enabled: true, + }, + { + Name: "test-repo", + URL: ts.URL, + Enabled: true, + Insecure: true, + }, + }, + }, + location: ts.URL + "/archive.zip", + names: []string{"test-repo"}, + wantDownload: true, + }, + { + name: "download error", + config: repo.Config{ + Repositories: []repo.Repository{ + { + Name: "test-repo", + URL: ts.URL, + Enabled: true, + }, + }, + }, + location: ts.URL + "/error", + wantErr: "failed to download the repository", + wantDownload: false, + }, + { + name: "download error insecure flag false", + config: repo.Config{ + Repositories: []repo.Repository{ + { + Name: "test-repo", + URL: ts.URL, + Enabled: true, + Insecure: false, + }, + }, + }, + location: ts.URL + "/archive.zip", + wantErr: "failed to download the repository", + wantDownload: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tempDir) + m := repo.NewManager(tempDir) + + configPath := filepath.Join(tempDir, ".trivy", "vex", "repository.yaml") + testutil.MustWriteYAML(t, configPath, tt.config) + + manifestPath := filepath.Join(tempDir, "vex", "repositories", "test-repo", "vex-repository.json") + manifest.Versions[0].Locations[0].URL = tt.location + testutil.MustWriteJSON(t, manifestPath, manifest) + + err := m.DownloadRepositories(t.Context(), tt.names, repo.Options{}) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + + // Check if the repository was downloaded + if tt.wantDownload { + repoDir := filepath.Join(tempDir, "vex", "repositories", "test-repo") + assert.DirExists(t, repoDir) + assert.FileExists(t, filepath.Join(repoDir, "vex-repository.json")) + assert.FileExists(t, filepath.Join(repoDir, "0.1", "index.json")) + } + }) + } +} + +func TestManager_List(t *testing.T) { + tests := []struct { + name string + config repo.Config + want string + wantErr string + }{ + { + name: "list repositories", + config: repo.Config{ + Repositories: []repo.Repository{ + { + Name: "default", + URL: "https://github.com/aquasecurity/vexhub", + Enabled: true, + }, + { + Name: "custom", + URL: "https://example.com/custom-vex-repo", + Enabled: false, + Insecure: true, + }, + }, + }, + want: `VEX Repositories (config: %s) + +- Name: default + URL: https://github.com/aquasecurity/vexhub + Status: Enabled + +- Name: custom + URL: https://example.com/custom-vex-repo + Status: Disabled + TLS Verify: No + +`, + }, + { + name: "no repositories", + config: repo.Config{ + Repositories: []repo.Repository{}, + }, + want: `VEX Repositories (config: %s) + +No repositories configured. +`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tempDir) + configPath := filepath.Join(tempDir, ".trivy", "vex", "repository.yaml") + testutil.MustWriteYAML(t, configPath, tt.config) + + var buf bytes.Buffer + m := repo.NewManager(tempDir, repo.WithWriter(&buf)) + + err := m.List(t.Context()) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + want := fmt.Sprintf(tt.want, configPath) + require.NoError(t, err) + assert.Equal(t, want, buf.String()) + }) + } +} + +func TestManager_Clear(t *testing.T) { + tempDir := t.TempDir() + m := repo.NewManager(tempDir) + + // Create some dummy files + cacheDir := filepath.Join(tempDir, "vex") + require.NoError(t, os.MkdirAll(cacheDir, 0o755)) + dummyFile := filepath.Join(cacheDir, "dummy.txt") + require.NoError(t, os.WriteFile(dummyFile, []byte("dummy"), 0o644)) + + err := m.Clear() + require.NoError(t, err) + + // Check if the cache directory was removed + _, err = os.Stat(cacheDir) + assert.True(t, os.IsNotExist(err)) +} diff --git a/pkg/vex/repo/repo.go b/pkg/vex/repo/repo.go new file mode 100644 index 000000000000..d8442aec1b48 --- /dev/null +++ b/pkg/vex/repo/repo.go @@ -0,0 +1,331 @@ +package repo + +import ( + "context" + "encoding/json" + "errors" + "net/url" + "os" + "path" + "path/filepath" + "time" + + "github.com/hashicorp/go-getter" + "github.com/hashicorp/go-multierror" + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/downloader" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/utils/fsutils" +) + +const ( + SchemaVersion = "0.1" + + manifestFile = "vex-repository.json" + indexFile = "index.json" + cacheMetadataFile = "cache.json" +) + +type Manifest struct { + Name string `json:"name"` + Description string `json:"description"` + Versions []Version `json:"versions"` +} + +type Version struct { + SpecVersion string `json:"spec_version"` + Locations []Location `json:"locations"` + UpdateInterval Duration `json:"update_interval"` +} + +// Duration is a wrapper around time.Duration that implements UnmarshalJSON +type Duration struct { + time.Duration +} + +func (d Duration) MarshalJSON() ([]byte, error) { + return json.Marshal(d.String()) +} + +// UnmarshalJSON implements the json.Unmarshaler interface +func (d *Duration) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return xerrors.Errorf("duration unmarshal error: %w", err) + } + + var err error + d.Duration, err = time.ParseDuration(s) + if err != nil { + return xerrors.Errorf("duration parse error: %w", err) + } + return nil +} + +type Location struct { + URL string `json:"url"` +} + +type Index struct { + Path string // Path to the index file + UpdatedAt time.Time + Packages map[string]PackageEntry +} + +type PackageEntry struct { + ID string `json:"id"` + Location string `json:"location"` + Format string `json:"format"` +} + +type RawIndex struct { + UpdatedAt time.Time `json:"updated_at"` + Packages []PackageEntry `json:"packages"` +} + +type Repository struct { + Name string + URL string + Enabled bool + Username string + Password string + Token string // For Bearer + Insecure bool + + dir string // Root directory for this VEX repository, $CACHE_DIR/vex/repositories/$REPO_NAME/ +} + +type CacheMetadata struct { + UpdatedAt time.Time // Last updated time + ETags map[string]string // Last ETag for each URL +} + +func (r *Repository) Manifest(ctx context.Context, opts Options) (Manifest, error) { + filePath := filepath.Join(r.dir, manifestFile) + if !fsutils.FileExists(filePath) { + if err := r.downloadManifest(ctx, opts); err != nil { + return Manifest{}, xerrors.Errorf("failed to download the repository metadata: %w", err) + } + } + + log.DebugContext(ctx, "Reading the repository metadata...", log.String("repo", r.Name), log.FilePath(filePath)) + f, err := os.Open(filePath) + if err != nil { + return Manifest{}, xerrors.Errorf("failed to open the file: %w", err) + } + defer f.Close() + + var manifest Manifest + if err = json.NewDecoder(f).Decode(&manifest); err != nil { + return Manifest{}, xerrors.Errorf("failed to decode the metadata: %w", err) + } + return manifest, nil +} + +func (r *Repository) Index(ctx context.Context) (Index, error) { + filePath := filepath.Join(r.dir, SchemaVersion, indexFile) + log.DebugContext(ctx, "Reading the repository index...", log.String("repo", r.Name), log.FilePath(filePath)) + + f, err := os.Open(filePath) + if err != nil { + return Index{}, xerrors.Errorf("failed to open the file: %w", err) + } + defer f.Close() + + var raw RawIndex + if err = json.NewDecoder(f).Decode(&raw); err != nil { + return Index{}, xerrors.Errorf("failed to decode the index: %w", err) + } + + return Index{ + Path: filePath, + UpdatedAt: raw.UpdatedAt, + Packages: lo.KeyBy(raw.Packages, func(p PackageEntry) string { return p.ID }), + }, nil +} + +func (r *Repository) downloadManifest(ctx context.Context, opts Options) error { + if err := os.MkdirAll(r.dir, 0o700); err != nil { + return xerrors.Errorf("failed to mkdir: %w", err) + } + + u, err := url.Parse(r.URL) + if err != nil { + return xerrors.Errorf("failed to parse the URL: %w", err) + } + + if u.Host == "github.com" { + u.Path = path.Join(u.Path, manifestFile) + } else { + u.Path = path.Join(u.Path, ".well-known", manifestFile) + } + + log.DebugContext(ctx, "Downloading the repository metadata...", log.String("url", u.String()), log.String("dst", r.dir)) + _, err = downloader.Download(ctx, u.String(), filepath.Join(r.dir, manifestFile), ".", downloader.Options{ + // if one between global and per-repo insecure option is set, + // we set it to true accordingly + Insecure: opts.Insecure || r.Insecure, + Auth: downloader.Auth{ + Username: r.Username, + Password: r.Password, + Token: r.Token, + }, + ClientMode: getter.ClientModeFile, + }) + if err != nil { + _ = os.RemoveAll(r.dir) + return xerrors.Errorf("failed to download the repository: %w", err) + } + return nil +} + +func (r *Repository) Update(ctx context.Context, opts Options) error { + manifest, err := r.Manifest(ctx, opts) + if err != nil { + return xerrors.Errorf("failed to get the repository metadata: %w", err) + } + + ver, err := r.selectSupportedVersion(manifest.Versions) + if err != nil { + return xerrors.Errorf("version %s not found", SchemaVersion) + } + + versionDir := filepath.Join(r.dir, SchemaVersion) + if !r.needUpdate(ctx, ver, versionDir) { + log.InfoContext(ctx, "No need to check repository updates", log.String("repo", r.Name)) + return nil + } + + log.InfoContext(ctx, "Updating repository...", log.String("repo", r.Name), log.String("url", r.URL)) + if err = r.download(ctx, ver, versionDir, opts); err != nil { + return xerrors.Errorf("failed to download the repository: %w", err) + } + return err +} + +func (r *Repository) needUpdate(ctx context.Context, ver Version, versionDir string) bool { + if !fsutils.DirExists(versionDir) { + return true + } + + m, err := r.cacheMetadata() + if err != nil { + log.DebugContext(ctx, "Failed to get repository cache metadata", log.String("repo", r.Name), log.Err(err)) + return true + } + + now := clock.Clock(ctx).Now() + log.DebugContext(ctx, "Checking if the repository needs to be updated...", log.String("repo", r.Name), + log.Time("last_update", m.UpdatedAt), log.Duration("update_interval", ver.UpdateInterval.Duration)) + return now.After(m.UpdatedAt.Add(ver.UpdateInterval.Duration)) +} + +func (r *Repository) download(ctx context.Context, ver Version, dst string, opts Options) error { + if len(ver.Locations) == 0 { + return xerrors.Errorf("no locations found for version %s", ver.SpecVersion) + } + if err := os.MkdirAll(dst, 0o700); err != nil { + return xerrors.Errorf("failed to mkdir: %w", err) + } + + m, err := r.cacheMetadata() + if err != nil { + return xerrors.Errorf("failed to get the repository cache metadata: %w", err) + } + etags := lo.Ternary(m.ETags == nil, make(map[string]string), m.ETags) + + var errs error + for _, loc := range ver.Locations { + logger := log.With(log.String("repo", r.Name)) + logger.DebugContext(ctx, "Downloading repository to cache dir...", log.String("url", loc.URL), + log.String("dir", dst), log.String("etag", etags[loc.URL])) + + etag, err := downloader.Download(ctx, loc.URL, dst, ".", downloader.Options{ + // if one between global and per-repo insecure option is set, + // we set it to true accordingly + Insecure: opts.Insecure || r.Insecure, + Auth: downloader.Auth{ + Username: r.Username, + Password: r.Password, + Token: r.Token, + }, + ETag: etags[loc.URL], + }) + switch { + case errors.Is(err, downloader.ErrSkipDownload): + logger.DebugContext(ctx, "No updates in the repository", log.String("url", r.URL)) + etag = etags[loc.URL] // Keep the old ETag + // Update last updated time so that Trivy will not try to download the same URL soon + case err != nil: + errs = multierror.Append(errs, err) + continue // Try the next location + default: + // Successfully downloaded + } + + // Update the cache metadata + etags[loc.URL] = etag + now := clock.Clock(ctx).Now() + if err = r.updateCacheMetadata(ctx, CacheMetadata{ + UpdatedAt: now, + ETags: etags, + }); err != nil { + return xerrors.Errorf("failed to update the repository cache metadata: %w", err) + } + logger.DebugContext(ctx, "Updated repository cache metadata", log.String("etag", etag), + log.Time("updated_at", now)) + return nil + } + if errs != nil { + return xerrors.Errorf("failed to download the repository: %w", errs) + } + return nil +} + +func (r *Repository) cacheMetadata() (CacheMetadata, error) { + filePath := filepath.Join(r.dir, cacheMetadataFile) + if !fsutils.FileExists(filePath) { + return CacheMetadata{}, nil + } + f, err := os.Open(filePath) + if err != nil { + return CacheMetadata{}, xerrors.Errorf("failed to open the file: %w", err) + } + defer f.Close() + + var metadata CacheMetadata + if err = json.NewDecoder(f).Decode(&metadata); err != nil { + return CacheMetadata{}, xerrors.Errorf("failed to decode the cache metadata: %w", err) + } + return metadata, nil +} + +func (r *Repository) selectSupportedVersion(versions []Version) (Version, error) { + for _, ver := range versions { + // Versions should exactly match until the spec version reaches 1.0. + // After reaching 1.0, we can select the latest version that has the same major version. + if ver.SpecVersion == SchemaVersion { + return ver, nil + } + } + return Version{}, xerrors.New("no supported version found") +} + +func (r *Repository) updateCacheMetadata(ctx context.Context, metadata CacheMetadata) error { + filePath := filepath.Join(r.dir, cacheMetadataFile) + log.DebugContext(ctx, "Updating repository cache metadata...", log.FilePath(filePath)) + + f, err := os.Create(filePath) + if err != nil { + return xerrors.Errorf("failed to create the file: %w", err) + } + defer f.Close() + + if err = json.NewEncoder(f).Encode(metadata); err != nil { + return xerrors.Errorf("failed to encode the metadata: %w", err) + } + return nil +} diff --git a/pkg/vex/repo/repo_test.go b/pkg/vex/repo/repo_test.go new file mode 100644 index 000000000000..24cc8929f4bb --- /dev/null +++ b/pkg/vex/repo/repo_test.go @@ -0,0 +1,367 @@ +package repo_test + +import ( + "archive/zip" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/internal/testutil" + "github.com/aquasecurity/trivy/pkg/clock" + "github.com/aquasecurity/trivy/pkg/vex/repo" +) + +var manifest = repo.Manifest{ + Name: "test-repo", + Description: "test repository", + Versions: []repo.Version{ + { + SpecVersion: "0.1", + Locations: []repo.Location{ + { + URL: "https://localhost", + }, + }, + UpdateInterval: repo.Duration{Duration: time.Hour * 24}, + }, + }, +} + +func TestRepository_Manifest(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/.well-known/vex-repository.json" { + err := json.NewEncoder(w).Encode(manifest) + assert.NoError(t, err) + } + http.Error(w, "error", http.StatusInternalServerError) + })) + t.Cleanup(ts.Close) + + tests := []struct { + name string + setup func(*testing.T, string, *repo.Repository) + want repo.Manifest + wantErr string + }{ + { + name: "local manifest exists", + setup: func(t *testing.T, dir string, _ *repo.Repository) { + manifestFile := filepath.Join(dir, "vex", "repositories", "test-repo", "vex-repository.json") + testutil.MustWriteJSON(t, manifestFile, manifest) + }, + want: manifest, + }, + { + name: "fetch from remote", + setup: func(_ *testing.T, _ string, r *repo.Repository) { + r.URL = ts.URL + }, + want: manifest, + }, + { + name: "http error", + setup: func(_ *testing.T, _ string, r *repo.Repository) { + r.URL = ts.URL + "/error" + }, + wantErr: "failed to download the repository metadata", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir, m := setupManager(t) + conf, err := m.Config(t.Context()) + require.NoError(t, err) + + r := conf.Repositories[0] + tt.setup(t, tempDir, &r) + + got, err := r.Manifest(t.Context(), repo.Options{}) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRepository_Index(t *testing.T) { + tests := []struct { + name string + setup func(*testing.T, string, *repo.Repository) + want repo.Index + wantErr string + }{ + { + name: "local index exists", + setup: func(t *testing.T, cacheDir string, r *repo.Repository) { + indexData := repo.RawIndex{ + UpdatedAt: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + Packages: []repo.PackageEntry{ + { + ID: "pkg1", + Location: "location1", + Format: "format1", + }, + { + ID: "pkg2", + Location: "location2", + Format: "format2", + }, + }, + } + + indexPath := filepath.Join(cacheDir, "vex", "repositories", r.Name, "0.1", "index.json") + testutil.MustWriteJSON(t, indexPath, indexData) + }, + want: repo.Index{ + UpdatedAt: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + Packages: map[string]repo.PackageEntry{ + "pkg1": { + ID: "pkg1", + Location: "location1", + Format: "format1", + }, + "pkg2": { + ID: "pkg2", + Location: "location2", + Format: "format2", + }, + }, + }, + }, + { + name: "index file not found", + setup: func(*testing.T, string, *repo.Repository) {}, + wantErr: "failed to open the file", + }, + { + name: "invalid JSON in index file", + setup: func(t *testing.T, cacheDir string, r *repo.Repository) { + indexPath := filepath.Join(cacheDir, "vex", "repositories", r.Name, "0.1", "index.json") + testutil.MustWriteFile(t, indexPath, []byte("invalid JSON")) + }, + wantErr: "failed to decode the index", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir, m := setupManager(t) + conf, err := m.Config(t.Context()) + require.NoError(t, err) + + r := conf.Repositories[0] + tt.setup(t, tempDir, &r) + + got, err := r.Index(t.Context()) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + tt.want.Path = filepath.Join(tempDir, "vex", "repositories", r.Name, "0.1", "index.json") + assert.Equal(t, tt.want, got) + }) + } +} + +func TestRepository_Update(t *testing.T) { + ts := setUpRepository(t) + defer ts.Close() + + tests := []struct { + name string + setup func(*testing.T, string, *repo.Repository) + clockTime time.Time + wantErr string + wantCache repo.CacheMetadata + }{ + { + name: "successful update", + setup: func(t *testing.T, cacheDir string, _ *repo.Repository) { + setUpManifest(t, cacheDir, ts.URL+"/archive.zip") + }, + clockTime: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + wantCache: repo.CacheMetadata{ + UpdatedAt: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + ETags: map[string]string{ts.URL + "/archive.zip": "new-etag"}, + }, + }, + { + name: "no update needed (within update interval)", + setup: func(t *testing.T, cacheDir string, r *repo.Repository) { + setUpManifest(t, cacheDir, "") // No location as the test server is not used + + repoDir := filepath.Join(cacheDir, "vex", "repositories", r.Name) + testutil.MustMkdirAll(t, filepath.Join(repoDir, "0.1")) + + cacheMetadata := repo.CacheMetadata{ + UpdatedAt: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + ETags: map[string]string{ts.URL + "/archive.zip": "current-etag"}, + } + testutil.MustWriteJSON(t, filepath.Join(repoDir, "cache.json"), cacheMetadata) + }, + clockTime: time.Date(2023, 1, 1, 1, 30, 0, 0, time.UTC), + wantCache: repo.CacheMetadata{ + UpdatedAt: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + ETags: map[string]string{ts.URL + "/archive.zip": "current-etag"}, + }, + }, + { + name: "update needed (update interval passed)", + setup: func(t *testing.T, cacheDir string, r *repo.Repository) { + setUpManifest(t, cacheDir, ts.URL+"/archive.zip") + + repoDir := filepath.Join(cacheDir, "vex", "repositories", r.Name) + testutil.MustMkdirAll(t, filepath.Join(repoDir, "0.1")) + + cacheMetadata := repo.CacheMetadata{ + UpdatedAt: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + ETags: map[string]string{ts.URL + "/archive.zip": "old-etag"}, + } + testutil.MustWriteJSON(t, filepath.Join(repoDir, "cache.json"), cacheMetadata) + }, + clockTime: time.Date(2023, 1, 2, 3, 0, 0, 0, time.UTC), + wantCache: repo.CacheMetadata{ + UpdatedAt: time.Date(2023, 1, 2, 3, 0, 0, 0, time.UTC), + ETags: map[string]string{ts.URL + "/archive.zip": "new-etag"}, + }, + }, + { + name: "no update needed (304 Not Modified)", + setup: func(t *testing.T, cacheDir string, r *repo.Repository) { + setUpManifest(t, cacheDir, ts.URL+"/archive.zip") + + repoDir := filepath.Join(cacheDir, "vex", "repositories", r.Name) + testutil.MustMkdirAll(t, filepath.Join(repoDir, "0.1")) + + cacheMetadata := repo.CacheMetadata{ + UpdatedAt: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + ETags: map[string]string{ts.URL + "/archive.zip": "current-etag"}, + } + testutil.MustWriteJSON(t, filepath.Join(repoDir, "cache.json"), cacheMetadata) + }, + clockTime: time.Date(2023, 1, 2, 3, 0, 0, 0, time.UTC), + wantCache: repo.CacheMetadata{ + UpdatedAt: time.Date(2023, 1, 2, 3, 0, 0, 0, time.UTC), + ETags: map[string]string{ts.URL + "/archive.zip": "current-etag"}, + }, + }, + { + name: "update with no existing cache.json", + setup: func(t *testing.T, cacheDir string, r *repo.Repository) { + setUpManifest(t, cacheDir, ts.URL+"/archive.zip") + + repoDir := filepath.Join(cacheDir, "vex", "repositories", r.Name) + testutil.MustMkdirAll(t, filepath.Join(repoDir, "0.1")) + }, + clockTime: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + wantCache: repo.CacheMetadata{ + UpdatedAt: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + ETags: map[string]string{ts.URL + "/archive.zip": "new-etag"}, + }, + }, + { + name: "manifest not found", + setup: func(*testing.T, string, *repo.Repository) {}, + clockTime: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + wantErr: "failed to get the repository metadata", + }, + { + name: "download error", + setup: func(t *testing.T, cacheDir string, r *repo.Repository) { + setUpManifest(t, cacheDir, ts.URL+"/error") + + repoDir := filepath.Join(cacheDir, "vex", "repositories", r.Name) + testutil.MustMkdirAll(t, filepath.Join(repoDir, "0.1")) + }, + clockTime: time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC), + wantErr: "failed to download the repository", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tempDir, m := setupManager(t) + conf, err := m.Config(t.Context()) + require.NoError(t, err) + + r := conf.Repositories[0] + r.URL = ts.URL + "/vex-repository.json" + tt.setup(t, tempDir, &r) + + ctx := clock.With(t.Context(), tt.clockTime) + err = r.Update(ctx, repo.Options{ + Insecure: true, + }) + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + + cacheFile := filepath.Join(tempDir, "vex", "repositories", r.Name, "cache.json") + var gotCache repo.CacheMetadata + testutil.MustReadJSON(t, cacheFile, &gotCache) + assert.Equal(t, tt.wantCache, gotCache) + }) + } +} + +func setupManager(t *testing.T) (string, *repo.Manager) { + tempDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", "testdata") + return tempDir, repo.NewManager(tempDir) +} + +func setUpManifest(t *testing.T, dir, url string) { + manifest := repo.Manifest{ + Name: "test-repo", + Description: "test repository", + Versions: []repo.Version{ + { + SpecVersion: "0.1", + Locations: []repo.Location{ + { + URL: url, + }, + }, + UpdateInterval: repo.Duration{Duration: time.Hour * 24}, + }, + }, + } + manifestPath := filepath.Join(dir, "vex", "repositories", "test-repo", "vex-repository.json") + testutil.MustWriteJSON(t, manifestPath, manifest) +} + +func setUpRepository(t *testing.T) *httptest.Server { + return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/archive.zip": + if r.Header.Get("If-None-Match") == "current-etag" { + w.WriteHeader(http.StatusNotModified) + return + } + w.Header().Set("Content-Type", "application/zip") + w.Header().Set("ETag", "new-etag") + zw := zip.NewWriter(w) + assert.NoError(t, zw.AddFS(os.DirFS("testdata/test-repo"))) + assert.NoError(t, zw.Close()) + case "/error": + w.WriteHeader(http.StatusInternalServerError) + default: + w.WriteHeader(http.StatusNotFound) + } + })) +} diff --git a/pkg/vex/repo/testdata/.trivy/vex/repository.yaml b/pkg/vex/repo/testdata/.trivy/vex/repository.yaml new file mode 100644 index 000000000000..92f1c4b7538d --- /dev/null +++ b/pkg/vex/repo/testdata/.trivy/vex/repository.yaml @@ -0,0 +1,4 @@ +repositories: + - name: "test-repo" + url: "https://localhost" + enabled: true \ No newline at end of file diff --git a/pkg/vex/repo/testdata/test-repo/index.json b/pkg/vex/repo/testdata/test-repo/index.json new file mode 100644 index 000000000000..a2b346441c8a --- /dev/null +++ b/pkg/vex/repo/testdata/test-repo/index.json @@ -0,0 +1,9 @@ +{ + "version": 1, + "packages": [ + { + "ID": "pkg:golang/github.com/aquasecurity/trivy", + "Location": "test" + } + ] +} \ No newline at end of file diff --git a/pkg/vex/repo/testdata/test-repo/vex-repository.json b/pkg/vex/repo/testdata/test-repo/vex-repository.json new file mode 100644 index 000000000000..132e960e553d --- /dev/null +++ b/pkg/vex/repo/testdata/test-repo/vex-repository.json @@ -0,0 +1,16 @@ +{ + "name": "Test Repository", + "description": "Test Repository", + "versions": { + "v0": { + "spec_version": "v0.1", + "locations": [ + { + "url": "Must be filled in tests" + } + ], + "update_interval": "24h" + } + }, + "latest_version": "v0" +} \ No newline at end of file diff --git a/pkg/vex/repo_test.go b/pkg/vex/repo_test.go new file mode 100644 index 000000000000..5316b718eb68 --- /dev/null +++ b/pkg/vex/repo_test.go @@ -0,0 +1,112 @@ +package vex_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vex" +) + +var bashComponent = core.Component{ + Name: bashPackage.Name, + Version: bashPackage.Version, + PkgIdentifier: bashPackage.Identifier, +} + +func TestRepositorySet_NotAffected(t *testing.T) { + tests := []struct { + name string + cacheDir string + configContent string + vuln types.DetectedVulnerability + product core.Component + wantModified types.ModifiedFinding + wantNotAffected bool + }{ + { + name: "single repository - not affected", + cacheDir: "testdata/single-repo", + configContent: ` +repositories: + - name: default + url: https://example.com/vex/default + enabled: true +`, + vuln: vuln3, + product: bashComponent, + wantModified: types.ModifiedFinding{ + Type: types.FindingTypeVulnerability, + Finding: vuln3, + Status: types.FindingStatusNotAffected, + Statement: "vulnerable_code_not_in_execute_path", + Source: "VEX Repository: default (https://example.com/vex/default)", + }, + wantNotAffected: true, + }, + { + name: "multiple repositories - high priority affected", + cacheDir: "testdata/multi-repos", + configContent: ` +repositories: + - name: high-priority + url: https://example.com/vex/high-priority + enabled: true + - name: default + url: https://example.com/vex/default + enabled: true +`, + vuln: vuln3, + product: bashComponent, + wantNotAffected: false, + }, + { + name: "no matching VEX data", + cacheDir: "testdata/single-repo", + configContent: ` +repositories: + - name: default + url: https://example.com/vex/default + enabled: true +`, + vuln: vuln4, + product: bashComponent, + wantNotAffected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary directory for each test + tmpDir := t.TempDir() + + // Set XDG_DATA_HOME to the temporary directory + t.Setenv("XDG_DATA_HOME", tmpDir) + + // Create the vex directory in the temporary directory + vexDir := filepath.Join(tmpDir, ".trivy", "vex") + err := os.MkdirAll(vexDir, 0o755) + require.NoError(t, err) + + // Write the config file + configPath := filepath.Join(vexDir, "repository.yaml") + err = os.WriteFile(configPath, []byte(tt.configContent), 0o644) + require.NoError(t, err) + + ctx := t.Context() + rs, err := vex.NewRepositorySet(ctx, tt.cacheDir) + require.NoError(t, err) + + modified, notAffected := rs.NotAffected(tt.vuln, &tt.product, nil) + assert.Equal(t, tt.wantNotAffected, notAffected) + if tt.wantNotAffected { + assert.Equal(t, tt.wantModified, modified) + } + }) + } +} diff --git a/pkg/vex/sbomref.go b/pkg/vex/sbomref.go new file mode 100644 index 000000000000..098f1d7261b0 --- /dev/null +++ b/pkg/vex/sbomref.go @@ -0,0 +1,109 @@ +package vex + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" +) + +type SBOMReferenceSet struct { + VEXes []VEX +} + +func NewSBOMReferenceSet(report *types.Report) (*SBOMReferenceSet, error) { + if report.ArtifactType != ftypes.TypeCycloneDX { + return nil, xerrors.Errorf("externalReferences can only be used when scanning CycloneDX SBOMs: %w", report.ArtifactType) + } + + ctx := log.WithContextPrefix(context.Background(), "vex") + ctx = log.WithContextAttrs(ctx, log.String("type", "sbom_reference")) + + externalRefs := report.BOM.ExternalReferences() + urls := parseToURLs(externalRefs) + + v, err := retrieveExternalVEXDocuments(ctx, urls, report) + if err != nil { + return nil, xerrors.Errorf("failed to fetch external VEX documents: %w", err) + } else if v == nil { + return nil, nil + } + + return &SBOMReferenceSet{VEXes: v}, nil +} + +func parseToURLs(refs []core.ExternalReference) []*url.URL { + return lo.FilterMap(refs, func(ref core.ExternalReference, _ int) (*url.URL, bool) { + if ref.Type != core.ExternalReferenceVEX { + return nil, false + } + val, err := url.Parse(ref.URL) + if err != nil || (val.Scheme != "https" && val.Scheme != "http") { + // do not concern ourselves with relative URLs at this point + return nil, false + } + return val, true + }) +} + +func retrieveExternalVEXDocuments(ctx context.Context, refs []*url.URL, report *types.Report) ([]VEX, error) { + var docs []VEX + for _, ref := range refs { + doc, err := retrieveExternalVEXDocument(ctx, ref, report) + if err != nil { + return nil, xerrors.Errorf("failed to retrieve external VEX document: %w", err) + } + docs = append(docs, doc) + } + log.DebugContext(ctx, "Retrieved external VEX documents", log.Int("count", len(docs))) + + if len(docs) == 0 { + log.DebugContext(ctx, "No external VEX documents found") + return nil, nil + } + return docs, nil + +} + +func retrieveExternalVEXDocument(ctx context.Context, vexUrl *url.URL, report *types.Report) (VEX, error) { + log.DebugContext(ctx, "Retrieving external VEX document", log.String("url", vexUrl.String())) + + res, err := http.Get(vexUrl.String()) + if err != nil { + return nil, xerrors.Errorf("unable to fetch file via HTTP: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, xerrors.Errorf("did not receive 2xx status code: %w", res.StatusCode) + } + + val, err := io.ReadAll(res.Body) + if err != nil { + return nil, xerrors.Errorf("unable to read response into memory: %w", err) + } + + v, err := decodeVEX(bytes.NewReader(val), vexUrl.String(), report) + if err != nil { + return nil, xerrors.Errorf("unable to load VEX from external reference: %w", err) + } + return v, nil +} + +func (set *SBOMReferenceSet) NotAffected(vuln types.DetectedVulnerability, product, subComponent *core.Component) (types.ModifiedFinding, bool) { + for _, vex := range set.VEXes { + if m, notAffected := vex.NotAffected(vuln, product, subComponent); notAffected { + return m, notAffected + } + } + return types.ModifiedFinding{}, false +} diff --git a/pkg/vex/sbomref_test.go b/pkg/vex/sbomref_test.go new file mode 100644 index 000000000000..582d95072c54 --- /dev/null +++ b/pkg/vex/sbomref_test.go @@ -0,0 +1,122 @@ +package vex_test + +import ( + "io" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/samber/lo" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/vex" +) + +const ( + vexExternalRef = "/openvex" + vexUnknown = "/unknown" + vexNotFound = "/not-found" +) + +func setUpServer(t *testing.T) *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case vexExternalRef: + f, err := os.Open("testdata/" + vexExternalRef + ".json") + if err != nil { + t.Fatalf("failed to open testdata: %s", err) + } + defer f.Close() + + if _, err = io.Copy(w, f); err != nil { + t.Fatalf("failed to copy testdata: %s", err) + } + case vexUnknown: + f, err := os.Open("testdata/" + vexUnknown + ".json") + if err != nil { + t.Fatalf("failed to open testdata: %s", err) + } + defer f.Close() + + if _, err = io.Copy(w, f); err != nil { + t.Fatalf("failed to copy testdata: %s", err) + } + default: + http.NotFound(w, r) + } + })) +} + +func setupTestReport(s *httptest.Server, path string) *types.Report { + r := types.Report{ + ArtifactType: ftypes.TypeCycloneDX, + BOM: &core.BOM{}, + } + r.BOM.AddExternalReferences([]core.ExternalReference{ + { + URL: s.URL + path, + Type: core.ExternalReferenceVEX, + }, + }) + + return &r +} + +func setupEmptyTestReport() *types.Report { + r := types.Report{ + ArtifactType: ftypes.TypeCycloneDX, + BOM: &core.BOM{}, + } + return &r +} + +func TestRetrieveExternalVEXDocuments(t *testing.T) { + s := setUpServer(t) + t.Cleanup(s.Close) + + tests := []struct { + name string + input *types.Report + wantVEXes int + wantErr bool + }{ + { + name: "external vex retrieval", + input: setupTestReport(s, vexExternalRef), + wantVEXes: 1, + wantErr: false, + }, + { + name: "incompatible external vex", + input: setupTestReport(s, vexUnknown), + wantErr: true, + }, + { + name: "vex not found", + input: setupTestReport(s, vexNotFound), + wantErr: true, + }, + { + name: "no external reference", + input: setupEmptyTestReport(), + wantVEXes: 0, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := vex.NewSBOMReferenceSet(tt.input) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Len(t, lo.FromPtr(got).VEXes, tt.wantVEXes) + }) + } +} diff --git a/pkg/vex/testdata/csaf-relationships.json b/pkg/vex/testdata/csaf-relationships.json new file mode 100644 index 000000000000..18c311228206 --- /dev/null +++ b/pkg/vex/testdata/csaf-relationships.json @@ -0,0 +1,126 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "publisher": { + "category": "vendor", + "name": "VMWare, Inc.", + "namespace": "https://tanzu.vmware.com/application-catalog" + }, + "title": "ArgoCD 2.9.3-2 Amd64 Debian12 Advisory", + "tracking": { + "current_release_date": "2024-01-04T17:17:25+01:00", + "generator": { + "engine": { + "name": "Bitnami VEX CLI", + "version": "1.0.0" + } + }, + "id": "fcf5bd33-41c3-45f9-885a-c2ee812f49c9", + "initial_release_date": "2024-01-04T17:17:25+01:00", + "revision_history": [ + { + "date": "2024-01-04T17:17:25+01:00", + "number": "1", + "summary": "Initial version." + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "branches": [ + { + "branches": [ + { + "category": "product_version", + "name": "2.0.0", + "product": { + "name": "go-direct1 v2.0.0", + "product_id": "go-direct1-v2.0.0", + "product_identification_helper": { + "purl": "pkg:golang/github.com/aquasecurity/go-direct1@v2.0.0" + } + } + } + ], + "category": "product_name", + "name": "go-direct1" + } + ], + "category": "vendor", + "name": "bar" + }, + { + "branches": [ + { + "branches": [ + { + "category": "product_version", + "name": "v4.0.0", + "product": { + "name": "go-transitive v4.0.0", + "product_id": "go-transitive-v4.0.0", + "product_identification_helper": { + "purl": "pkg:golang/github.com/aquasecurity/go-transitive@v4.0.0" + } + } + } + ], + "category": "product_name", + "name": "go-transitive" + } + ], + "category": "vendor", + "name": "foo" + } + ], + "relationships": [ + { + "product_reference": "go-transitive-v4.0.0", + "category": "default_component_of", + "relates_to_product_reference": "go-direct1-v2.0.0", + "full_product_name": { + "product_id": "go-direct1-v2.0.0-go-transitive-v4.0.0", + "name": "go-direct1 uses go-transitive" + } + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2024-0001", + "flags": [ + { + "date": "2024-01-04T17:17:25+01:00", + "label": "vulnerable_code_cannot_be_controlled_by_adversary", + "product_ids": [ + "go-direct1-v2.0.0-go-transitive-v4.0.0" + ] + } + ], + "notes": [ + { + "category": "description", + "text": "Users may be able to launch containers using images that are restricted by ImagePolicyWebhook when using ephemeral containers. Kubernetes clusters are only affected if the ImagePolicyWebhook admission plugin is used together with ephemeral containers.", + "title": "CVE description" + } + ], + "product_status": { + "known_not_affected": [ + "go-direct1-v2.0.0-go-transitive-v4.0.0" + ] + }, + "threats": [ + { + "category": "impact", + "date": "2024-01-04T17:17:25+01:00", + "details": "vulnerable_code_not_in_execute_path" + } + ] + } + ] +} diff --git a/pkg/vex/testdata/csaf.json b/pkg/vex/testdata/csaf.json new file mode 100644 index 000000000000..182856f96a51 --- /dev/null +++ b/pkg/vex/testdata/csaf.json @@ -0,0 +1,90 @@ +{ + "document": { + "category": "csaf_vex", + "csaf_version": "2.0", + "notes": [ + { + "category": "summary", + "text": "Example Company VEX document. Unofficial content for demonstration purposes only.", + "title": "Author comment" + } + ], + "publisher": { + "category": "vendor", + "name": "Example Company ProductCERT", + "namespace": "https://psirt.example.com" + }, + "title": "Aqua Security example VEX document", + "tracking": { + "current_release_date": "2022-03-03T11:00:00.000Z", + "generator": { + "date": "2022-03-03T11:00:00.000Z", + "engine": { + "name": "Secvisogram", + "version": "1.11.0" + } + }, + "id": "2022-EVD-UC-01-A-001", + "initial_release_date": "2022-03-03T11:00:00.000Z", + "revision_history": [ + { + "date": "2022-03-03T11:00:00.000Z", + "number": "1", + "summary": "Initial version." + } + ], + "status": "final", + "version": "1" + } + }, + "product_tree": { + "branches": [ + { + "branches": [ + { + "branches": [ + { + "category": "product_version", + "name": "v4.0.0", + "product": { + "name": "go-transitive v4.0.0", + "product_id": "go-transitive-v4.0.0", + "product_identification_helper": { + "purl": "pkg:golang/github.com/aquasecurity/go-transitive@v4.0.0" + } + } + } + ], + "category": "product_name", + "name": "go-transitive" + } + ], + "category": "vendor", + "name": "foo" + } + ] + }, + "vulnerabilities": [ + { + "cve": "CVE-2024-0001", + "notes": [ + { + "category": "description", + "text": "Users may be able to launch containers using images that are restricted by ImagePolicyWebhook when using ephemeral containers. Kubernetes clusters are only affected if the ImagePolicyWebhook admission plugin is used together with ephemeral containers.", + "title": "CVE description" + } + ], + "product_status": { + "known_not_affected": [ + "go-transitive-v4.0.0" + ] + }, + "threats": [ + { + "category": "impact", + "details": "vulnerable_code_not_in_execute_path" + } + ] + } + ] +} diff --git a/pkg/vex/testdata/cyclonedx.json b/pkg/vex/testdata/cyclonedx.json new file mode 100644 index 000000000000..fb6463600c65 --- /dev/null +++ b/pkg/vex/testdata/cyclonedx.json @@ -0,0 +1,44 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "version": 1, + "vulnerabilities": [ + { + "id": "CVE-2021-44228", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-44228" + }, + "analysis": { + "state": "not_affected", + "justification": "code_not_reachable" + }, + "affects": [ + { + "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:maven/org.springframework.boot/spring-boot@2.6.0" + } + ] + }, + { + "id": "CVE-2022-27943", + "source": { + "name": "ubuntu", + "url": "https://git.launchpad.net/ubuntu-cve-tracker" + }, + "affects": [ + { + "ref": "urn:cdx:3e671687-395b-41f5-a30f-a58921a69b79/1#pkg:deb/ubuntu/libstdc%2B%2B6@12.3.0-1ubuntu1~22.04?arch=amd64&distro=ubuntu-22.04", + "versions": [ + { + "version": "12.3.0-1ubuntu1~22.04", + "status": "affected" + } + ] + } + ], + "analysis": { + "state": "not_affected" + } + } + ] +} diff --git a/pkg/vex/testdata/multi-repos/vex/repositories/default/0.1/bash-vex.json b/pkg/vex/testdata/multi-repos/vex/repositories/default/0.1/bash-vex.json new file mode 100644 index 000000000000..c06426bac376 --- /dev/null +++ b/pkg/vex/testdata/multi-repos/vex/repositories/default/0.1/bash-vex.json @@ -0,0 +1,22 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-5d6e2706", + "author": "Example Author", + "role": "Document Creator", + "timestamp": "2023-07-01T00:00:00Z", + "version": 1, + "statements": [ + { + "vulnerability": { + "@id": "CVE-2022-3715" + }, + "products": [ + { + "@id": "pkg:deb/debian/bash@5.3" + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} \ No newline at end of file diff --git a/pkg/vex/testdata/multi-repos/vex/repositories/default/0.1/index.json b/pkg/vex/testdata/multi-repos/vex/repositories/default/0.1/index.json new file mode 100644 index 000000000000..4a746cca5382 --- /dev/null +++ b/pkg/vex/testdata/multi-repos/vex/repositories/default/0.1/index.json @@ -0,0 +1,10 @@ +{ + "updated_at": "2024-07-01T00:00:00Z", + "packages": [ + { + "id": "pkg:deb/debian/bash", + "location": "bash-vex.json", + "format": "openvex" + } + ] +} \ No newline at end of file diff --git a/pkg/vex/testdata/multi-repos/vex/repositories/high-priority/0.1/bash-vex.json b/pkg/vex/testdata/multi-repos/vex/repositories/high-priority/0.1/bash-vex.json new file mode 100644 index 000000000000..300ecc8e7c3a --- /dev/null +++ b/pkg/vex/testdata/multi-repos/vex/repositories/high-priority/0.1/bash-vex.json @@ -0,0 +1,21 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-5d6e2706", + "author": "Example Author", + "role": "Document Creator", + "timestamp": "2023-07-01T00:00:00Z", + "version": 1, + "statements": [ + { + "vulnerability": { + "@id": "CVE-2022-3715" + }, + "products": [ + { + "@id": "pkg:deb/debian/bash@5.3" + } + ], + "status": "affected" + } + ] +} \ No newline at end of file diff --git a/pkg/vex/testdata/multi-repos/vex/repositories/high-priority/0.1/index.json b/pkg/vex/testdata/multi-repos/vex/repositories/high-priority/0.1/index.json new file mode 100644 index 000000000000..4a746cca5382 --- /dev/null +++ b/pkg/vex/testdata/multi-repos/vex/repositories/high-priority/0.1/index.json @@ -0,0 +1,10 @@ +{ + "updated_at": "2024-07-01T00:00:00Z", + "packages": [ + { + "id": "pkg:deb/debian/bash", + "location": "bash-vex.json", + "format": "openvex" + } + ] +} \ No newline at end of file diff --git a/pkg/vex/testdata/openvex-multiple.json b/pkg/vex/testdata/openvex-multiple.json new file mode 100644 index 000000000000..11c9ac5d192e --- /dev/null +++ b/pkg/vex/testdata/openvex-multiple.json @@ -0,0 +1,33 @@ +{ + "@context": "https://openvex.dev/ns", + "author": "Aqua Security", + "role": "Project Release Bot", + "timestamp": "2023-01-16T19:07:16.853479631-06:00", + "version": 1, + "statements": [ + { + "timestamp": "2023-01-15T01:02:03.853479631-06:00", + "vulnerability": { + "name": "CVE-2021-44228" + }, + "products": [ + { + "@id": "pkg:maven/org.springframework.boot/spring-boot@2.6.0" + } + ], + "status": "affected" + }, + { + "vulnerability": { + "name": "CVE-2021-44228" + }, + "products": [ + { + "@id": "pkg:maven/org.springframework.boot/spring-boot@2.6.0" + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} diff --git a/pkg/vex/testdata/openvex-nested.json b/pkg/vex/testdata/openvex-nested.json new file mode 100644 index 000000000000..637603d9e48c --- /dev/null +++ b/pkg/vex/testdata/openvex-nested.json @@ -0,0 +1,26 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "author": "Aqua Security", + "role": "Project Release Bot", + "timestamp": "2023-01-16T19:07:16.853479631-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2024-0001" + }, + "products": [ + { + "@id": "pkg:golang/github.com/aquasecurity/go-direct1@v2.0.0", + "subcomponents": [ + { + "@id": "pkg:golang/github.com/aquasecurity/go-transitive@v4.0.0" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} diff --git a/pkg/vex/testdata/openvex-oci-mismatch.json b/pkg/vex/testdata/openvex-oci-mismatch.json new file mode 100644 index 000000000000..e022d4854abb --- /dev/null +++ b/pkg/vex/testdata/openvex-oci-mismatch.json @@ -0,0 +1,26 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "author": "Aqua Security", + "role": "Project Release Bot", + "timestamp": "2023-01-16T19:07:16.853479631-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2022-3715" + }, + "products": [ + { + "@id": "pkg:oci/mismatch", + "subcomponents": [ + { + "@id": "pkg:deb/debian/bash" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} diff --git a/pkg/vex/testdata/openvex-oci.json b/pkg/vex/testdata/openvex-oci.json new file mode 100644 index 000000000000..667ca5e3d049 --- /dev/null +++ b/pkg/vex/testdata/openvex-oci.json @@ -0,0 +1,26 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "author": "Aqua Security", + "role": "Project Release Bot", + "timestamp": "2023-01-16T19:07:16.853479631-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2022-3715" + }, + "products": [ + { + "@id": "pkg:oci/debian", + "subcomponents": [ + { + "@id": "pkg:deb/debian/bash" + } + ] + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} diff --git a/pkg/vex/testdata/openvex.json b/pkg/vex/testdata/openvex.json new file mode 100644 index 000000000000..4acaf3aff95e --- /dev/null +++ b/pkg/vex/testdata/openvex.json @@ -0,0 +1,21 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "author": "Aqua Security", + "role": "Project Release Bot", + "timestamp": "2023-01-16T19:07:16.853479631-06:00", + "version": 1, + "statements": [ + { + "vulnerability": { + "name": "CVE-2021-44228" + }, + "products": [ + { + "@id": "pkg:maven/org.springframework.boot/spring-boot@2.6.0" + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} diff --git a/pkg/vex/testdata/single-repo/vex/repositories/default/0.1/bash-vex.json b/pkg/vex/testdata/single-repo/vex/repositories/default/0.1/bash-vex.json new file mode 100644 index 000000000000..c06426bac376 --- /dev/null +++ b/pkg/vex/testdata/single-repo/vex/repositories/default/0.1/bash-vex.json @@ -0,0 +1,22 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://openvex.dev/docs/public/vex-5d6e2706", + "author": "Example Author", + "role": "Document Creator", + "timestamp": "2023-07-01T00:00:00Z", + "version": 1, + "statements": [ + { + "vulnerability": { + "@id": "CVE-2022-3715" + }, + "products": [ + { + "@id": "pkg:deb/debian/bash@5.3" + } + ], + "status": "not_affected", + "justification": "vulnerable_code_not_in_execute_path" + } + ] +} \ No newline at end of file diff --git a/pkg/vex/testdata/single-repo/vex/repositories/default/0.1/index.json b/pkg/vex/testdata/single-repo/vex/repositories/default/0.1/index.json new file mode 100644 index 000000000000..4a746cca5382 --- /dev/null +++ b/pkg/vex/testdata/single-repo/vex/repositories/default/0.1/index.json @@ -0,0 +1,10 @@ +{ + "updated_at": "2024-07-01T00:00:00Z", + "packages": [ + { + "id": "pkg:deb/debian/bash", + "location": "bash-vex.json", + "format": "openvex" + } + ] +} \ No newline at end of file diff --git a/pkg/vex/testdata/single-repo/vex/repositories/default/vex-repository.json b/pkg/vex/testdata/single-repo/vex/repositories/default/vex-repository.json new file mode 100644 index 000000000000..e064c0e1b3cf --- /dev/null +++ b/pkg/vex/testdata/single-repo/vex/repositories/default/vex-repository.json @@ -0,0 +1,15 @@ +{ + "name": "Test VEX Repository", + "description": "VEX Repository for Testing", + "versions": [ + { + "spec_version": "0.1", + "locations": [ + { + "url": "never used" + } + ], + "update_interval": "24h" + } + ] +} \ No newline at end of file diff --git a/pkg/vex/testdata/unknown.json b/pkg/vex/testdata/unknown.json new file mode 100644 index 000000000000..9e26dfeeb6e6 --- /dev/null +++ b/pkg/vex/testdata/unknown.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/pkg/vex/vex.go b/pkg/vex/vex.go new file mode 100644 index 000000000000..83ebd70f0765 --- /dev/null +++ b/pkg/vex/vex.go @@ -0,0 +1,219 @@ +package vex + +import ( + "context" + "errors" + + "github.com/samber/lo" + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom/core" + sbomio "github.com/aquasecurity/trivy/pkg/sbom/io" + "github.com/aquasecurity/trivy/pkg/set" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" +) + +const ( + TypeFile SourceType = "file" + TypeRepository SourceType = "repo" + TypeOCI SourceType = "oci" + TypeSBOMReference SourceType = "sbom-ref" +) + +// VEX represents Vulnerability Exploitability eXchange. It abstracts multiple VEX formats. +// Note: This is in the experimental stage and does not yet support many specifications. +// The implementation may change significantly. +type VEX interface { + NotAffected(vuln types.DetectedVulnerability, product, subComponent *core.Component) (types.ModifiedFinding, bool) +} + +type Client struct { + VEXes []VEX +} + +type Options struct { + CacheDir string + Sources []Source +} + +type SourceType string + +type Source struct { + Type SourceType + FilePath string // Used only for the file type +} + +func NewSource(src string) Source { + switch src { + case "repository", "repo": + return Source{Type: TypeRepository} + case "oci": + return Source{Type: TypeOCI} + case "sbom-ref": + return Source{Type: TypeSBOMReference} + default: + return Source{ + Type: TypeFile, + FilePath: src, + } + } +} + +type NotAffected func(vuln types.DetectedVulnerability, product, subComponent *core.Component) (types.ModifiedFinding, bool) + +// Filter determines whether a detected vulnerability should be filtered out based on the provided VEX document. +// If the VEX document is passed and the vulnerability is either not affected or fixed according to the VEX statement, +// the vulnerability is filtered out. +func Filter(ctx context.Context, report *types.Report, opts Options) error { + ctx = log.WithContextPrefix(ctx, "vex") + client, err := New(ctx, report, opts) + if err != nil { + return xerrors.Errorf("VEX error: %w", err) + } else if client == nil { + return nil + } + + // NOTE: This method call has a side effect on the report + bom, err := sbomio.NewEncoder(sbomio.WithParents(), sbomio.ForceRegenerate()).Encode(*report) + if err != nil { + return xerrors.Errorf("unable to encode the SBOM: %w", err) + } + + for i, result := range report.Results { + if len(result.Vulnerabilities) == 0 { + continue + } + filterVulnerabilities(&report.Results[i], bom, client.NotAffected) + } + return nil +} + +func New(ctx context.Context, report *types.Report, opts Options) (*Client, error) { + var vexes []VEX + for _, src := range opts.Sources { + var v VEX + var err error + switch src.Type { + case TypeFile: + v, err = NewDocument(src.FilePath, report) + if err != nil { + return nil, xerrors.Errorf("unable to load VEX: %w", err) + } + case TypeRepository: + v, err = NewRepositorySet(ctx, opts.CacheDir) + if errors.Is(err, errNoRepository) { + continue + } else if err != nil { + return nil, xerrors.Errorf("failed to create a vex repository set: %w", err) + } + case TypeOCI: + v, err = NewOCI(report) + if err != nil { + return nil, xerrors.Errorf("VEX OCI error: %w", err) + } else if lo.IsNil(v) { + continue + } + case TypeSBOMReference: + v, err = NewSBOMReferenceSet(report) + if err != nil { + return nil, xerrors.Errorf("failed to create set of external VEX documents: %w", err) + } else if v == nil { + continue + } + default: + log.Warn("Unsupported VEX source", log.String("type", string(src.Type))) + continue + } + vexes = append(vexes, v) + } + + if len(vexes) == 0 { + log.DebugContext(ctx, "VEX filtering is disabled") + return nil, nil + } + return &Client{VEXes: vexes}, nil +} + +func (c *Client) NotAffected(vuln types.DetectedVulnerability, product, subComponent *core.Component) (types.ModifiedFinding, bool) { + for _, v := range c.VEXes { + if m, notAffected := v.NotAffected(vuln, product, subComponent); notAffected { + return m, true + } + } + return types.ModifiedFinding{}, false +} + +func filterVulnerabilities(result *types.Result, bom *core.BOM, fn NotAffected) { + components := lo.MapEntries(bom.Components(), func(_ uuid.UUID, component *core.Component) (string, *core.Component) { + return component.PkgIdentifier.UID, component + }) + + result.Vulnerabilities = lo.Filter(result.Vulnerabilities, func(vuln types.DetectedVulnerability, _ int) bool { + c, ok := components[vuln.PkgIdentifier.UID] + if !ok { + log.Error("Component not found", log.String("uid", vuln.PkgIdentifier.UID)) + return true // Should never reach here + } + + var modified types.ModifiedFinding + notAffectedFn := func(c, leaf *core.Component) bool { + m, notAffected := fn(vuln, c, leaf) + if notAffected { + modified = m // Take the last modified finding if multiple VEX states "not affected" + } + return notAffected + } + + if !reachRoot(c, bom.Components(), bom.Parents(), notAffectedFn) { + result.ModifiedFindings = append(result.ModifiedFindings, modified) + return false + } + return true + }) +} + +// reachRoot traverses the component tree from the leaf to the root and returns true if the leaf reaches the root. +func reachRoot(leaf *core.Component, components map[uuid.UUID]*core.Component, parents map[uuid.UUID][]uuid.UUID, + notAffected func(c, leaf *core.Component) bool, +) bool { + if notAffected(leaf, nil) { + return false + } + + // Use Depth First Search (DFS) + var dfs func(c *core.Component, visited set.Set[uuid.UUID]) bool + dfs = func(c *core.Component, visited set.Set[uuid.UUID]) bool { + + // Call the function with the current component and the leaf component + switch { + case notAffected(c, leaf): + return false + case c.Root: + return true + case set.New[uuid.UUID](parents[c.ID()]...).Difference(visited).Size() == 0: + // Should never go here, since all components except the root must have at least one parent and be related to the root component. + // If it does, it means the component tree is not connected due to a bug in the SBOM generation. + // In this case, so as not to filter out all the vulnerabilities accidentally, return true for fail-safe. + return true + } + + visited.Append(c.ID()) + for _, parent := range parents[c.ID()] { + if visited.Contains(parent) { + continue + } + + // Each DFS path needs its own visited set, + // to avoid false positives in other paths + newVisited := visited.Clone() + if dfs(components[parent], newVisited) { + return true + } + } + return false + } + + return dfs(leaf, set.New[uuid.UUID]()) +} diff --git a/pkg/vex/vex_test.go b/pkg/vex/vex_test.go new file mode 100644 index 000000000000..ec5864b1e7ba --- /dev/null +++ b/pkg/vex/vex_test.go @@ -0,0 +1,855 @@ +package vex_test + +import ( + "fmt" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/google/go-containerregistry/pkg/v1" + "github.com/package-url/packageurl-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/sbom/core" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/uuid" + "github.com/aquasecurity/trivy/pkg/vex" +) + +const ( + vulnerableCodeNotInExecutePath = "vulnerable_code_not_in_execute_path" + codeNotReachable = "code_not_reachable" +) + +var ( + springPackage = ftypes.Package{ + ID: "org.springframework.boot:spring-boot:2.6.0", + Name: "org.springframework.boot:spring-boot", + Version: "2.6.0", + Identifier: ftypes.PkgIdentifier{ + UID: "01", + BOMRef: "pkg:maven/org.springframework.boot/spring-boot@2.6.0", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeMaven, + Namespace: "org.springframework.boot", + Name: "spring-boot", + Version: "2.6.0", + }, + }, + } + bashPackage = ftypes.Package{ + ID: "bash@5.3", + Name: "bash", + Version: "5.3", + Identifier: ftypes.PkgIdentifier{ + UID: "02", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "bash", + Version: "5.3", + }, + }, + } + baseFilesPackage = ftypes.Package{ + ID: "base-files@5.3", + Name: "base-files", + Version: "5.3", + Identifier: ftypes.PkgIdentifier{ + UID: "07", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "base-files", + Version: "5.3", + }, + }, + } + baseFiles2Package = ftypes.Package{ + ID: "base-files2@5.3", + Name: "base-files2", + Version: "5.3", + Identifier: ftypes.PkgIdentifier{ + UID: "08", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeDebian, + Namespace: "debian", + Name: "base-files2", + Version: "5.3", + }, + }, + } + goModulePackage = ftypes.Package{ + ID: "github.com/aquasecurity/go-module@v1.0.0", + Name: "github.com/aquasecurity/go-module", + Version: "v1.0.0", + Relationship: ftypes.RelationshipRoot, + Identifier: ftypes.PkgIdentifier{ + UID: "03", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-module", + Version: "v1.0.0", + }, + }, + } + goDirectPackage1 = ftypes.Package{ + ID: "github.com/aquasecurity/go-direct1@v2.0.0", + Name: "github.com/aquasecurity/go-direct1", + Version: "v2.0.0", + Relationship: ftypes.RelationshipDirect, + Identifier: ftypes.PkgIdentifier{ + UID: "04", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-direct1", + Version: "v2.0.0", + }, + }, + } + goDirectPackage2 = ftypes.Package{ + ID: "github.com/aquasecurity/go-direct2@v3.0.0", + Name: "github.com/aquasecurity/go-direct2", + Version: "v3.0.0", + Relationship: ftypes.RelationshipDirect, + Identifier: ftypes.PkgIdentifier{ + UID: "05", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-direct2", + Version: "v3.0.0", + }, + }, + } + goTransitivePackage = ftypes.Package{ + ID: "github.com/aquasecurity/go-transitive@v4.0.0", + Name: "github.com/aquasecurity/go-transitive", + Version: "v4.0.0", + Relationship: ftypes.RelationshipIndirect, + Identifier: ftypes.PkgIdentifier{ + UID: "06", + PURL: &packageurl.PackageURL{ + Type: packageurl.TypeGolang, + Namespace: "github.com/aquasecurity", + Name: "go-transitive", + Version: "v4.0.0", + }, + }, + } + vuln1 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2021-44228", + PkgName: springPackage.Name, + InstalledVersion: springPackage.Version, + PkgIdentifier: springPackage.Identifier, + } + vuln2 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2021-0001", + PkgName: springPackage.Name, + InstalledVersion: springPackage.Version, + PkgIdentifier: springPackage.Identifier, + } + vuln3 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2022-3715", + PkgName: bashPackage.Name, + InstalledVersion: bashPackage.Version, + PkgIdentifier: bashPackage.Identifier, + } + vuln4 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2024-10000", + PkgName: bashPackage.Name, + InstalledVersion: bashPackage.Version, + PkgIdentifier: bashPackage.Identifier, + } + vuln5 = types.DetectedVulnerability{ + VulnerabilityID: "CVE-2024-0001", + PkgName: goTransitivePackage.Name, + InstalledVersion: goTransitivePackage.Version, + PkgIdentifier: goTransitivePackage.Identifier, + } +) + +func TestMain(m *testing.M) { + log.InitLogger(false, true) + os.Exit(m.Run()) +} + +func TestFilter(t *testing.T) { + // Set up the OCI registry + tr, d := setUpRegistry(t) + + uuid.SetFakeUUID(t, "3ff14136-e09f-4df9-80ea-%012d") + testCycloneDXSBOM := createCycloneDXBOMWithSpringComponent() + + type args struct { + report *types.Report + opts vex.Options + } + tests := []struct { + name string + setup func(t *testing.T, tmpDir string) + args args + want *types.Report + wantErr string + }{ + { + name: "OpenVEX", + args: args{ + // - oci:debian?tag=12 + // - pkg:maven/org.springframework.boot/spring-boot@2.6.0 + report: imageReport([]types.Result{ + springResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{vuln1}, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/openvex.json", + }, + }, + }, + }, + want: imageReport([]types.Result{ + springResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{}, + ModifiedFindings: []types.ModifiedFinding{modifiedFinding(vuln1, vulnerableCodeNotInExecutePath, "testdata/openvex.json")}, + }), + }), + }, + { + name: "OpenVEX, multiple statements", + args: args{ + // - oci:debian?tag=12 + // - pkg:maven/org.springframework.boot/spring-boot@2.6.0 + report: imageReport([]types.Result{ + springResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln1, // filtered by VEX + vuln2, + }, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/openvex-multiple.json", + }, + }, + }, + }, + want: imageReport([]types.Result{ + springResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{vuln2}, + ModifiedFindings: []types.ModifiedFinding{modifiedFinding(vuln1, vulnerableCodeNotInExecutePath, "testdata/openvex-multiple.json")}, + }), + }), + }, + { + name: "OpenVEX, subcomponents, oci image", + args: args{ + // - oci:debian?tag=12 + // - pkg:deb/debian/bash@5.3 + report: imageReport([]types.Result{ + bashResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln3, // filtered by VEX + }, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/openvex-oci.json", + }, + }, + }, + }, + want: imageReport([]types.Result{ + bashResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{}, + ModifiedFindings: []types.ModifiedFinding{modifiedFinding(vuln3, vulnerableCodeNotInExecutePath, "testdata/openvex-oci.json")}, + }), + }), + }, + { + name: "OpenVEX, subcomponents, mismatched oci image", + args: args{ + report: imageReport(types.Results{ + bashResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{vuln3}, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/openvex-oci-mismatch.json", + }, + }, + }, + }, + want: imageReport([]types.Result{ + bashResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{vuln3}, + }), + }), + }, + { + name: "OpenVEX, single path between product and subcomponent", + args: args{ + report: fsReport([]types.Result{ + goSinglePathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln5, // filtered by VEX + }, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/openvex-nested.json", + }, + }, + }, + }, + want: fsReport([]types.Result{ + goSinglePathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{}, + ModifiedFindings: []types.ModifiedFinding{modifiedFinding(vuln5, vulnerableCodeNotInExecutePath, "testdata/openvex-nested.json")}, + }), + }), + }, + { + name: "OpenVEX, multi paths between product and subcomponent", + args: args{ + report: fsReport([]types.Result{ + goMultiPathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln5, + }, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/openvex-nested.json", + }, + }, + }, + }, + want: fsReport([]types.Result{ + goMultiPathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{vuln5}, // Will not be filtered because of multi paths + }), + }), + }, + { + name: "CycloneDX SBOM with CycloneDX VEX", + args: args{ + report: &types.Report{ + ArtifactType: ftypes.TypeCycloneDX, + BOM: testCycloneDXSBOM, + Results: []types.Result{ + springResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{vuln1}, + }), + }, + }, + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/cyclonedx.json", + }, + }, + }, + }, + want: &types.Report{ + ArtifactType: ftypes.TypeCycloneDX, + BOM: testCycloneDXSBOM, + Results: []types.Result{ + springResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{}, + ModifiedFindings: []types.ModifiedFinding{modifiedFinding(vuln1, codeNotReachable, "CycloneDX VEX")}, + }), + }, + }, + }, + { + name: "CycloneDX VEX wrong URN", + args: args{ + report: &types.Report{ + ArtifactType: ftypes.TypeCycloneDX, + BOM: &core.BOM{ + SerialNumber: "urn:uuid:wrong", + Version: 1, + }, + Results: []types.Result{ + springResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{vuln1}, + }), + }, + }, + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/cyclonedx.json", + }, + }, + }, + }, + want: &types.Report{ + ArtifactType: ftypes.TypeCycloneDX, + BOM: &core.BOM{ + SerialNumber: "urn:uuid:wrong", + Version: 1, + }, + Results: []types.Result{ + springResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{vuln1}, + }), + }, + }, + }, + { + name: "CSAF, not affected", + args: args{ + report: imageReport([]types.Result{ + goSinglePathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln5, // filtered by VEX + }, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/csaf.json", + }, + }, + }, + }, + want: imageReport([]types.Result{ + goSinglePathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{}, + ModifiedFindings: []types.ModifiedFinding{modifiedFinding(vuln5, vulnerableCodeNotInExecutePath, "testdata/csaf.json")}, + }), + }), + }, + { + name: "CSAF with relationships, not affected", + args: args{ + report: imageReport([]types.Result{ + goSinglePathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln5, // filtered by VEX + }, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/csaf-relationships.json", + }, + }, + }, + }, + want: imageReport([]types.Result{ + goSinglePathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{}, + ModifiedFindings: []types.ModifiedFinding{modifiedFinding(vuln5, vulnerableCodeNotInExecutePath, "testdata/csaf-relationships.json")}, + }), + }), + }, + { + name: "CSAF with relationships, affected", + args: args{ + report: imageReport([]types.Result{ + goMultiPathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln5, + }, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/csaf-relationships.json", + }, + }, + }, + }, + want: imageReport([]types.Result{ + goMultiPathResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{vuln5}, // Will not be filtered because of multi paths + }), + }), + }, + { + name: "VEX Repository", + setup: func(t *testing.T, tmpDir string) { + // Create repository.yaml + vexDir := filepath.Join(tmpDir, ".trivy", "vex") + require.NoError(t, os.MkdirAll(vexDir, 0o755)) + + configPath := filepath.Join(vexDir, "repository.yaml") + configContent := ` +repositories: + - name: default + url: https://example.com/vex/default + enabled: true` + require.NoError(t, os.WriteFile(configPath, []byte(configContent), 0o644)) + }, + args: args{ + report: imageReport([]types.Result{ + bashResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln3, // filtered by VEX + }, + }), + }), + opts: vex.Options{ + CacheDir: "testdata/single-repo", + Sources: []vex.Source{{Type: vex.TypeRepository}}, + }, + }, + want: imageReport([]types.Result{ + bashResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{}, + ModifiedFindings: []types.ModifiedFinding{ + modifiedFinding(vuln3, "vulnerable_code_not_in_execute_path", "VEX Repository: default (https://example.com/vex/default)"), + }, + }), + }), + }, + { + name: "VEX attestation from OCI registry", + args: args{ + // - oci:debian?tag=12 + // - pkg:deb/debian/bash@5.3 + report: imageReportWithAttestation([]types.Result{ + bashResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln3, // filtered by VEX + }, + }), + }, fmt.Sprintf("%s/debian@%s", strings.TrimPrefix(tr.URL, "http://"), d.String())), + opts: vex.Options{ + Sources: []vex.Source{ + {Type: vex.TypeOCI}, + }, + }, + }, + want: imageReportWithAttestation([]types.Result{ + bashResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{}, + ModifiedFindings: []types.ModifiedFinding{ + modifiedFinding(vuln3, vulnerableCodeNotInExecutePath, + fmt.Sprintf("VEX attestation in OCI registry (%s)", ociPURLString(tr, d))), + }, + }), + }, fmt.Sprintf("%s/debian@%s", strings.TrimPrefix(tr.URL, "http://"), d.String())), + }, + { + name: "infinity loop for OS packages", + args: args{ + // - oci:debian?tag=12 + // - pkg:deb/debian/bash@5.3 + // - pkg:deb/debian/base-files@5.3 + // - pkg:deb/debian/base-files@5.3 + // - pkg:deb/debian/bash@5.3 + report: imageReport([]types.Result{ + infinityLoopOSPackagesResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln3, + }, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/openvex-oci.json", + }, + }, + }, + }, + want: imageReport([]types.Result{ + infinityLoopOSPackagesResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln3, + }, + }), + }), + }, + { + name: "check one parent from multiple dependency paths", + args: args{ + // - oci:debian?tag=12 + // - pkg:deb/debian/base-files@5.3 + // - pkg:deb/debian/bash@5.3 + // - pkg:deb/debian/base-files2@5.3 + // - pkg:deb/debian/bash@5.3 + report: imageReport([]types.Result{ + bashPackagesResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{ + vuln3, + }, + }), + }), + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/openvex-oci.json", + }, + }, + }, + }, + want: imageReport([]types.Result{ + bashPackagesResult(types.Result{ + Vulnerabilities: []types.DetectedVulnerability{}, + ModifiedFindings: []types.ModifiedFinding{modifiedFinding(vuln3, vulnerableCodeNotInExecutePath, "testdata/openvex-oci.json")}, + }), + }), + }, + { + name: "unknown format", + args: args{ + report: &types.Report{}, + opts: vex.Options{ + Sources: []vex.Source{ + { + Type: vex.TypeFile, + FilePath: "testdata/unknown.json", + }, + }, + }, + }, + wantErr: "unable to load VEX", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir := t.TempDir() + t.Setenv("XDG_DATA_HOME", tmpDir) + if tt.setup != nil { + tt.setup(t, tmpDir) + } + err := vex.Filter(t.Context(), tt.args.report, tt.args.opts) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + require.NoError(t, err) + assert.Equal(t, tt.want, tt.args.report) + }) + } +} + +func imageReport(results types.Results) *types.Report { + return &types.Report{ + ArtifactName: "debian:12", + ArtifactType: ftypes.TypeContainerImage, + Metadata: types.Metadata{ + RepoDigests: []string{ + "debian@sha256:4482958b4461ff7d9fabc24b3a9ab1e9a2c85ece07b2db1840c7cbc01d053e90", + }, + ImageConfig: v1.ConfigFile{ + Architecture: "amd64", + }, + }, + Results: results, + } +} + +func imageReportWithAttestation(results types.Results, repoDigest string) *types.Report { + report := imageReport(results) + report.Metadata.RepoDigests[0] = repoDigest + return report +} + +func ociPURLString(ts *httptest.Server, d v1.Hash) string { + p := &packageurl.PackageURL{ + Type: packageurl.TypeOCI, + Name: "debian", + Version: d.String(), + Qualifiers: packageurl.Qualifiers{ + { + Key: "arch", + Value: "amd64", + }, + { + Key: "repository_url", + Value: strings.TrimPrefix(ts.URL, "http://") + "/debian", + }, + }, + } + return p.String() +} + +func createCycloneDXBOMWithSpringComponent() *core.BOM { + bom := core.NewBOM(core.Options{}) + bom.SerialNumber = "urn:uuid:3e671687-395b-41f5-a30f-a58921a69b79" + bom.Version = 1 + pkgIdentifier := ftypes.PkgIdentifier{ + // Components got from scanned SBOM files don't have UID + BOMRef: springPackage.Identifier.BOMRef, + PURL: springPackage.Identifier.PURL, + } + // Add the spring component to match vuln1's BOM-Ref + springComponent := &core.Component{ + Type: core.TypeLibrary, + Name: springPackage.Identifier.PURL.Name, + Group: springPackage.Identifier.PURL.Namespace, + Version: springPackage.Version, + PkgIdentifier: pkgIdentifier, + } + bom.AddComponent(springComponent) + return bom +} + +func fsReport(results types.Results) *types.Report { + return &types.Report{ + ArtifactName: ".", + ArtifactType: ftypes.TypeFilesystem, + Results: results, + } +} + +func springResult(result types.Result) types.Result { + result.Type = ftypes.Jar + result.Class = types.ClassLangPkg + result.Packages = []ftypes.Package{springPackage} + return result +} + +// bashResult wraps the result with the bash package +func bashResult(result types.Result) types.Result { + result.Type = ftypes.Debian + result.Class = types.ClassOSPkg + result.Packages = []ftypes.Package{bashPackage} + return result +} + +func bashPackagesResult(result types.Result) types.Result { + result.Type = ftypes.Debian + result.Class = types.ClassOSPkg + + bashPkg := clonePackage(bashPackage) + baseFilesPkg := clonePackage(baseFilesPackage) + baseFiles2Pkg := clonePackage(baseFiles2Package) + + baseFilesPkg.DependsOn = []string{bashPkg.ID} + baseFiles2Pkg.DependsOn = []string{bashPkg.ID} + + result.Packages = []ftypes.Package{ + bashPkg, + baseFilesPkg, + baseFiles2Pkg, + } + + return result +} + +func infinityLoopOSPackagesResult(result types.Result) types.Result { + result.Type = ftypes.Debian + result.Class = types.ClassOSPkg + + bashPkg := clonePackage(bashPackage) + baseFilesPkg := clonePackage(baseFilesPackage) + + bashPkg.DependsOn = []string{baseFilesPkg.ID} + baseFilesPkg.DependsOn = []string{bashPkg.ID} + + result.Packages = []ftypes.Package{ + bashPkg, + baseFilesPkg, + } + + return result +} + +func goSinglePathResult(result types.Result) types.Result { + result.Type = ftypes.GoModule + result.Class = types.ClassLangPkg + + // - pkg:golang/github.com/aquasecurity/go-module@v1.0.0 + // - pkg:golang/github.com/aquasecurity/go-direct1@v2.0.0 + // - pkg:golang/github.com/aquasecurity/go-transitive@v4.0.0 + goModule := clonePackage(goModulePackage) + goDirect1 := clonePackage(goDirectPackage1) + goTransitive := clonePackage(goTransitivePackage) + + goModule.DependsOn = []string{goDirect1.ID} + goDirect1.DependsOn = []string{goTransitive.ID} + result.Packages = []ftypes.Package{ + goModule, + goDirect1, + goTransitive, + } + return result +} + +func goMultiPathResult(result types.Result) types.Result { + result.Type = ftypes.GoModule + result.Class = types.ClassLangPkg + + // - pkg:golang/github.com/aquasecurity/go-module@v1.0.0 + // - pkg:golang/github.com/aquasecurity/go-direct1@v2.0.0 + // - pkg:golang/github.com/aquasecurity/go-transitive@v5.0.0 + // - pkg:golang/github.com/aquasecurity/go-direct2@v3.0.0 + // - pkg:golang/github.com/aquasecurity/go-transitive@v4.0.0 + goModule := clonePackage(goModulePackage) + goDirect1 := clonePackage(goDirectPackage1) + goDirect2 := clonePackage(goDirectPackage2) + goTransitive := clonePackage(goTransitivePackage) + + goModule.DependsOn = []string{ + goDirect1.ID, + goDirect2.ID, + } + goDirect1.DependsOn = []string{goTransitive.ID} + goDirect2.DependsOn = []string{goTransitive.ID} + result.Packages = []ftypes.Package{ + goModule, + goDirect1, + goDirect2, + goTransitive, + } + return result +} + +func modifiedFinding(vuln types.DetectedVulnerability, statement, source string) types.ModifiedFinding { + return types.ModifiedFinding{ + Type: types.FindingTypeVulnerability, + Status: types.FindingStatusNotAffected, + Statement: statement, + Source: source, + Finding: vuln, + } +} + +func clonePackage(p ftypes.Package) ftypes.Package { + n := p + n.DependsOn = []string{} + return n +} diff --git a/pkg/vulnerability/testdata/fixtures/sad.yaml b/pkg/vulnerability/testdata/fixtures/sad.yaml new file mode 100644 index 000000000000..f7d55cb4b458 --- /dev/null +++ b/pkg/vulnerability/testdata/fixtures/sad.yaml @@ -0,0 +1,6 @@ +- bucket: vulnerability + pairs: + - key: CVE-2019-0001 + value: + Title: + - aaa diff --git a/pkg/vulnerability/testdata/fixtures/vulnerability.yaml b/pkg/vulnerability/testdata/fixtures/vulnerability.yaml new file mode 100644 index 000000000000..1e9f8b404356 --- /dev/null +++ b/pkg/vulnerability/testdata/fixtures/vulnerability.yaml @@ -0,0 +1,85 @@ +- bucket: vulnerability + pairs: + - key: CVE-2019-0001 + value: + Title: dos + Description: dos vulnerability + Severity: MEDIUM + References: + - http://example.com + LastModifiedDate: "2020-01-01T01:01:00Z" + PublishedDate: "2001-01-01T01:01:00Z" + - key: CVE-2019-0004 + value: + Title: dos + Description: dos vulnerability + Severity: MEDIUM + VendorSeverity: + redhat: 1 + CVSS: + nvd: + V2Vector: AV:N/AC:L/Au:N/C:P/I:P/A:P + V2Score: 4.5 + V3Vector: CVSS:3.0/PR:N/UI:N/S:U/C:H/I:H/A:H + V3Score: 5.6 + redhat: + V2Vector: AV:N/AC:M/Au:N/C:N/I:P/A:N + V2Score: 7.8 + V3Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H + V3Score: 9.8 + References: + - http://example.com + CweIDs: + - CWE-311 + - key: CVE-2019-0005 + value: + Title: COVID-19 + Description: a nasty virus vulnerability for humans + Severity: MEDIUM + VendorSeverity: + ghsa: 4 + References: + - "https://www.who.int/emergencies/diseases/novel-coronavirus-2019" + - key: RUSTSEC-2018-0017 + value: + Title: dos + Description: dos vulnerability + Severity: UNKNOWN + VendorSeverity: + nvd: 1 + LastModifiedDate: "2020-01-01T01:01:00Z" + PublishedDate: "2001-01-01T01:01:00Z" + - key: GHSA-28fw-88hq-6jmm + value: + Title: dos + Description: dos vulnerability + References: + - http://example.com + - key: CVE-2022-0001 + value: + Title: dos + Description: dos vulnerability + VendorSeverity: + nvd: 1 + ghsa: 3 + References: + - http://example.com + - key: CVE-2023-0001 + value: + Title: dos + Description: dos vulnerability + VendorSeverity: + nvd: 1 + alpine: 2 + ubuntu: 3 + References: + - http://example.com + - key: GHSA-0000-aaaa-1111 + value: + Title: dos + Description: dos vulnerability + VendorSeverity: + nvd: 1 + ghsa: 3 + References: + - http://example.com \ No newline at end of file diff --git a/pkg/vulnerability/vulnerability.go b/pkg/vulnerability/vulnerability.go new file mode 100644 index 000000000000..9a6538446bb6 --- /dev/null +++ b/pkg/vulnerability/vulnerability.go @@ -0,0 +1,194 @@ +package vulnerability + +import ( + "cmp" + "slices" + "strings" + "sync" + + "github.com/samber/lo" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/pkg/log" + "github.com/aquasecurity/trivy/pkg/types" + "github.com/aquasecurity/trivy/pkg/version/doc" +) + +var ( + primaryURLPrefixes = map[dbTypes.SourceID][]string{ + vulnerability.Debian: { + "http://www.debian.org", + "https://www.debian.org", + }, + vulnerability.Ubuntu: { + "http://www.ubuntu.com", + "https://usn.ubuntu.com", + }, + vulnerability.RedHat: {"https://access.redhat.com"}, + vulnerability.SuseCVRF: { + "http://lists.opensuse.org", + "https://lists.opensuse.org", + }, + vulnerability.OracleOVAL: { + "http://linux.oracle.com/errata", + "https://linux.oracle.com/errata", + }, + vulnerability.NodejsSecurityWg: { + "https://www.npmjs.com", + "https://hackerone.com", + }, + vulnerability.RubySec: {"https://groups.google.com"}, + } +) + +// Show warning if we use severity from another vendor +// cf. https://github.com/aquasecurity/trivy/issues/6714 +var onceWarn = sync.OnceFunc(func() { + // e.g. https://trivy.dev/docs/latest/scanner/vulnerability/#severity-selection + log.Warnf("Using severities from other vendors for some vulnerabilities. Read %s for details.", doc.URL("guide/scanner/vulnerability/", "severity-selection")) +}) + +// Client manipulates vulnerabilities +type Client struct { + dbc db.Operation +} + +// NewClient is the factory method for Client +func NewClient(dbc db.Operation) Client { + return Client{dbc: dbc} +} + +// FillInfo fills extra info in vulnerability objects +func (c Client) FillInfo(vulns []types.DetectedVulnerability, severitySources []dbTypes.SourceID) { + var noSeverityIDs []string + for i := range vulns { + // Add the vulnerability status + // Some vendors such as Red Hat have their own vulnerability status, and we use it. + // Otherwise, we put "fixed" or "affected" according to the fixed version. + if vulns[i].FixedVersion != "" { + vulns[i].Status = dbTypes.StatusFixed + } else if vulns[i].Status == dbTypes.StatusUnknown { + vulns[i].Status = dbTypes.StatusAffected + } + + // Get the vulnerability detail + vulnID := vulns[i].VulnerabilityID + vuln, err := c.dbc.GetVulnerability(vulnID) + if err != nil { + log.Warn("Unable to get vulnerability details (CVE may be rejected)", log.Err(err)) + continue + } + + // Detect the data source + dataSource := lo.FromPtr(vulns[i].DataSource) + + // To determine severity, use BaseID if available, otherwise use ID. + // e.g. `debian` severity for root.io advisories. + dataSourceID := cmp.Or(dataSource.BaseID, dataSource.ID) + + // Select the severity according to the detected sourceID. + severity, severitySource := c.getSeverity(vulnID, &vuln, dataSourceID, severitySources) + if severity == dbTypes.SeverityUnknown.String() { + noSeverityIDs = append(noSeverityIDs, vulnID) + } + + // The vendor might provide package-specific severity like Debian. + // For example, CVE-2015-2328 in Debian has "unimportant" for mongodb and "low" for pcre3. + // In that case, we keep the severity as is. + if vulns[i].SeveritySource != "" { + severity = vulns[i].Severity + severitySource = vulns[i].SeveritySource + + // Store package-specific severity in vendor severities + if vuln.VendorSeverity == nil { + vuln.VendorSeverity = make(dbTypes.VendorSeverity) + } + s, _ := dbTypes.NewSeverity(severity) // skip error handling because `SeverityUnknown` will be returned in case of error + vuln.VendorSeverity[severitySource] = s + } + + // Add the vulnerability detail + vulns[i].Vulnerability = vuln + + vulns[i].Severity = severity + vulns[i].SeveritySource = severitySource + vulns[i].PrimaryURL = c.getPrimaryURL(vulnID, vuln.References, dataSource.ID) + } + + if !slices.Contains(severitySources, "auto") && len(noSeverityIDs) > 0 { + log.Warn("No severity found in specified sources", + log.Any("vulnerability-ids", noSeverityIDs), log.Any("severity-sources", severitySources)) + } +} + +func (c Client) getSeverity(vulnID string, vuln *dbTypes.Vulnerability, dataSourceID dbTypes.SourceID, severitySources []dbTypes.SourceID) (string, dbTypes.SourceID) { + for _, source := range severitySources { + if source == "auto" { + return c.autoDetectSeverity(vulnID, vuln, dataSourceID) + } + + if severity, ok := vuln.VendorSeverity[source]; ok { + return severity.String(), source + } + } + + return dbTypes.SeverityUnknown.String(), "" +} + +// autoDetectSeverity detects the severity from the vulnerability ID and data source. +// +// The severity is determined in the following order: +// 1. If the vulnerability is from a specific data source (e.g., Red Hat advisories for Red Hat distributions), +// use the severity from that data source. +// 2. For GHSA-IDs, also consider the severity from GitHub Advisory Database. +// 3. Use the severity from NVD as a fallback. +// 4. Try severities from other data sources (e.g., Debian severity for Red Hat distributions). +// 5. If no severity is found from any data source, return "UNKNOWN". +func (c Client) autoDetectSeverity(vulnID string, vuln *dbTypes.Vulnerability, dataSourceID dbTypes.SourceID) (string, dbTypes.SourceID) { + autoSeveritySrcs := []dbTypes.SourceID{dataSourceID, vulnerability.NVD} + if vs, ok := vuln.VendorSeverity[dataSourceID]; ok { + return vs.String(), dataSourceID + } + + // use severity from GitHub for all GHSA-xxx vulnerabilities + if strings.HasPrefix(vulnID, "GHSA-") { + // use severity from GitHub for all GHSA-IDs + autoSeveritySrcs = []dbTypes.SourceID{dataSourceID, vulnerability.GHSA, vulnerability.NVD} + } + + if severity, severitySource := c.getSeverity(vulnID, vuln, dataSourceID, autoSeveritySrcs); severity != dbTypes.SeverityUnknown.String() { + return severity, severitySource + } + + if vuln.Severity == "" { + return dbTypes.SeverityUnknown.String(), "" + } + + onceWarn() + return vuln.Severity, "" +} + +func (c Client) getPrimaryURL(vulnID string, refs []string, source dbTypes.SourceID) string { + switch { + case strings.HasPrefix(vulnID, "CVE-"): + return "https://avd.aquasec.com/nvd/" + strings.ToLower(vulnID) + case strings.HasPrefix(vulnID, "RUSTSEC-"): + return "https://osv.dev/vulnerability/" + vulnID + case strings.HasPrefix(vulnID, "GHSA-"): + return "https://github.com/advisories/" + vulnID + case strings.HasPrefix(vulnID, "TEMP-"): + return "https://security-tracker.debian.org/tracker/" + vulnID + } + + prefixes := primaryURLPrefixes[source] + for _, pre := range prefixes { + for _, ref := range refs { + if strings.HasPrefix(ref, pre) { + return ref + } + } + } + return "" +} diff --git a/pkg/vulnerability/vulnerability_test.go b/pkg/vulnerability/vulnerability_test.go new file mode 100644 index 000000000000..858a38af4678 --- /dev/null +++ b/pkg/vulnerability/vulnerability_test.go @@ -0,0 +1,493 @@ +package vulnerability_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/trivy-db/pkg/db" + dbTypes "github.com/aquasecurity/trivy-db/pkg/types" + "github.com/aquasecurity/trivy-db/pkg/utils" + "github.com/aquasecurity/trivy-db/pkg/vulnsrc/vulnerability" + "github.com/aquasecurity/trivy/internal/dbtest" + "github.com/aquasecurity/trivy/pkg/types" + vuln "github.com/aquasecurity/trivy/pkg/vulnerability" +) + +func TestClient_FillInfo(t *testing.T) { + tests := []struct { + name string + vulnSeveritySources []dbTypes.SourceID + fixtures []string + vulns []types.DetectedVulnerability + expectedVulnerabilities []types.DetectedVulnerability + }{ + { + name: "happy path, with only OS vulnerability but no vendor severity, no NVD", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2019-0001"}, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + Status: dbTypes.StatusAffected, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityMedium.String(), + References: []string{"http://example.com"}, + LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"), + PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"), + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0001", + }, + }, + }, + { + name: "happy path, with only OS vulnerability but no vendor severity, yes NVD", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "RUSTSEC-2018-0017", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.OSV, + Name: "RustSec Advisory Database", + URL: "https://github.com/RustSec/advisory-db", + }, + }, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "RUSTSEC-2018-0017", + Status: dbTypes.StatusAffected, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityLow.String(), + VendorSeverity: dbTypes.VendorSeverity{ + vulnerability.NVD: dbTypes.SeverityLow, + }, + LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"), + PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"), + }, + SeveritySource: vulnerability.NVD, + PrimaryURL: "https://osv.dev/vulnerability/RUSTSEC-2018-0017", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.OSV, + Name: "RustSec Advisory Database", + URL: "https://github.com/RustSec/advisory-db", + }, + }, + }, + }, + { + name: "happy path, with only OS vulnerability but no severity, no vendor severity, no NVD", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "GHSA-28fw-88hq-6jmm", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.GHSA, + Name: "GitHub Security Advisory Pip", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip", + }, + }, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "GHSA-28fw-88hq-6jmm", + Status: dbTypes.StatusAffected, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityUnknown.String(), + References: []string{"http://example.com"}, + }, + PrimaryURL: "https://github.com/advisories/GHSA-28fw-88hq-6jmm", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.GHSA, + Name: "GitHub Security Advisory Pip", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip", + }, + }, + }, + }, + { + name: "happy path, with only OS vulnerability, yes vendor severity, with both NVD and CVSS info", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0004", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RedHat, + Name: "Red Hat OVAL v2", + URL: "https://www.redhat.com/security/data/oval/v2/", + }, + }, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0004", + Status: dbTypes.StatusAffected, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityLow.String(), + VendorSeverity: dbTypes.VendorSeverity{ + vulnerability.RedHat: dbTypes.SeverityLow, + }, + CweIDs: []string{"CWE-311"}, + References: []string{"http://example.com"}, + CVSS: map[dbTypes.SourceID]dbTypes.CVSS{ + vulnerability.NVD: { + V2Vector: "AV:N/AC:L/Au:N/C:P/I:P/A:P", + V2Score: 4.5, + V3Vector: "CVSS:3.0/PR:N/UI:N/S:U/C:H/I:H/A:H", + V3Score: 5.6, + }, + vulnerability.RedHat: { + V2Vector: "AV:N/AC:M/Au:N/C:N/I:P/A:N", + V2Score: 7.8, + V3Vector: "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", + V3Score: 9.8, + }, + }, + }, + SeveritySource: vulnerability.RedHat, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0004", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RedHat, + Name: "Red Hat OVAL v2", + URL: "https://www.redhat.com/security/data/oval/v2/", + }, + }, + }, + }, + { + name: "happy path, with only library vulnerability", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0005", + Status: dbTypes.StatusAffected, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.GHSA, + Name: "GitHub Security Advisory Pip", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip", + }, + }, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0005", + Status: dbTypes.StatusAffected, + Vulnerability: dbTypes.Vulnerability{ + Title: "COVID-19", + Description: "a nasty virus vulnerability for humans", + Severity: dbTypes.SeverityCritical.String(), + VendorSeverity: dbTypes.VendorSeverity{ + vulnerability.GHSA: dbTypes.SeverityCritical, + }, + References: []string{"https://www.who.int/emergencies/diseases/novel-coronavirus-2019"}, + }, + SeveritySource: vulnerability.GHSA, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0005", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.GHSA, + Name: "GitHub Security Advisory Pip", + URL: "https://github.com/advisories?query=type%3Areviewed+ecosystem%3Apip", + }, + }, + }, + }, + { + name: "happy path, with package-specific severity", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + SeveritySource: vulnerability.Debian, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityLow.String(), + }, + }, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0001", + Status: dbTypes.StatusAffected, + SeveritySource: vulnerability.Debian, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityLow.String(), + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + vulnerability.Debian: dbTypes.SeverityLow, + }, + References: []string{"http://example.com"}, + LastModifiedDate: utils.MustTimeParse("2020-01-01T01:01:00Z"), + PublishedDate: utils.MustTimeParse("2001-01-01T01:01:00Z"), + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2019-0001", + }, + }, + }, + { + name: "happy path. GHSA-xxx. Severity gets from ghsa", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "GHSA-0000-aaaa-1111"}, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "GHSA-0000-aaaa-1111", + Status: dbTypes.StatusAffected, + SeveritySource: vulnerability.GHSA, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityHigh.String(), + References: []string{"http://example.com"}, + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + "nvd": dbTypes.SeverityLow, + "ghsa": dbTypes.SeverityHigh, + }, + }, + PrimaryURL: "https://github.com/advisories/GHSA-0000-aaaa-1111", + }, + }, + }, + { + name: "happy path. CVE-xxx. Severity gets from nvd", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2022-0001"}, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0001", + Status: dbTypes.StatusAffected, + SeveritySource: vulnerability.NVD, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityLow.String(), + References: []string{"http://example.com"}, + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + "nvd": dbTypes.SeverityLow, + "ghsa": dbTypes.SeverityHigh, + }, + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2022-0001", + }, + }, + }, + { + name: "happy path. Severity got from 'nvd' from VulnSeveritySources", + vulnSeveritySources: []dbTypes.SourceID{ + "alma", + "alpine", + "nvd", + }, + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2022-0001"}, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0001", + Status: dbTypes.StatusAffected, + SeveritySource: vulnerability.NVD, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityLow.String(), + References: []string{"http://example.com"}, + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + "nvd": dbTypes.SeverityLow, + "ghsa": dbTypes.SeverityHigh, + }, + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2022-0001", + }, + }, + }, + { + name: "happy path. Severity got from 'auto' from VulnSeveritySources", + vulnSeveritySources: []dbTypes.SourceID{ + "alma", + "alpine", + "auto", + }, + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2022-0001"}, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0001", + Status: dbTypes.StatusAffected, + SeveritySource: vulnerability.NVD, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityLow.String(), + References: []string{"http://example.com"}, + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + "nvd": dbTypes.SeverityLow, + "ghsa": dbTypes.SeverityHigh, + }, + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2022-0001", + }, + }, + }, + { + name: "happy path. Severity didn't find from VulnSeveritySources", + vulnSeveritySources: []dbTypes.SourceID{ + "alma", + "alpine", + }, + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + {VulnerabilityID: "CVE-2022-0001"}, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2022-0001", + Status: dbTypes.StatusAffected, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityUnknown.String(), + References: []string{"http://example.com"}, + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + "nvd": dbTypes.SeverityLow, + "ghsa": dbTypes.SeverityHigh, + }, + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2022-0001", + }, + }, + }, + { + name: "happy path. Use alpine severity for root.io vulnerability", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2023-0001", + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RootIO, + BaseID: vulnerability.Alpine, + Name: "Root.io Security Patches (alpine)", + URL: "https://api.root.io/external/patch_feed", + }, + }, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2023-0001", + Status: dbTypes.StatusAffected, + SeveritySource: vulnerability.Alpine, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RootIO, + BaseID: vulnerability.Alpine, + Name: "Root.io Security Patches (alpine)", + URL: "https://api.root.io/external/patch_feed", + }, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityMedium.String(), + References: []string{"http://example.com"}, + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + "nvd": dbTypes.SeverityLow, + "alpine": dbTypes.SeverityMedium, + "ubuntu": dbTypes.SeverityHigh, + }, + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2023-0001", + }, + }, + }, + { + name: "happy path. Use debian severity for root.io vulnerability", + fixtures: []string{"testdata/fixtures/vulnerability.yaml"}, + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2023-0001", + SeveritySource: vulnerability.Debian, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RootIO, + BaseID: vulnerability.Debian, + Name: "Root.io Security Patches (debian)", + URL: "https://api.root.io/external/patch_feed", + }, + Vulnerability: dbTypes.Vulnerability{ + Severity: dbTypes.SeverityCritical.String(), + }, + }, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2023-0001", + Status: dbTypes.StatusAffected, + SeveritySource: vulnerability.Debian, + DataSource: &dbTypes.DataSource{ + ID: vulnerability.RootIO, + BaseID: vulnerability.Debian, + Name: "Root.io Security Patches (debian)", + URL: "https://api.root.io/external/patch_feed", + }, + Vulnerability: dbTypes.Vulnerability{ + Title: "dos", + Description: "dos vulnerability", + Severity: dbTypes.SeverityCritical.String(), + References: []string{"http://example.com"}, + VendorSeverity: map[dbTypes.SourceID]dbTypes.Severity{ + "nvd": dbTypes.SeverityLow, + "alpine": dbTypes.SeverityMedium, + "ubuntu": dbTypes.SeverityHigh, + "debian": dbTypes.SeverityCritical, + }, + }, + PrimaryURL: "https://avd.aquasec.com/nvd/cve-2023-0001", + }, + }, + }, + { + name: "GetVulnerability returns an error", + fixtures: []string{"testdata/fixtures/sad.yaml"}, + vulns: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0004", + Status: dbTypes.StatusAffected, + }, + }, + expectedVulnerabilities: []types.DetectedVulnerability{ + { + VulnerabilityID: "CVE-2019-0004", + Status: dbTypes.StatusAffected, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dbtest.InitDB(t, tt.fixtures) + defer db.Close() + + c := vuln.NewClient(db.Config{}) + vulnSeveritySources := []dbTypes.SourceID{ + "auto", + } + if len(tt.vulnSeveritySources) > 0 { + vulnSeveritySources = tt.vulnSeveritySources + } + c.FillInfo(tt.vulns, vulnSeveritySources) + assert.Equal(t, tt.expectedVulnerabilities, tt.vulns, tt.name) + }) + } +} diff --git a/pkg/vulnsrc/alpine/alpine.go b/pkg/vulnsrc/alpine/alpine.go deleted file mode 100644 index f69db0aa49a8..000000000000 --- a/pkg/vulnsrc/alpine/alpine.go +++ /dev/null @@ -1,111 +0,0 @@ -package alpine - -import ( - "encoding/json" - "fmt" - "io" - "path/filepath" - - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/db" - "github.com/knqyf263/trivy/pkg/log" - - "golang.org/x/xerrors" - - bolt "github.com/etcd-io/bbolt" - "github.com/knqyf263/trivy/pkg/utils" -) - -const ( - alpineDir = "alpine" -) - -var ( - platformFormat = "alpine %s" -) - -func Update(dir string, updatedFiles map[string]struct{}) error { - rootDir := filepath.Join(dir, alpineDir) - targets, err := utils.FilterTargets(alpineDir, updatedFiles) - if err != nil { - return xerrors.Errorf("failed to filter target files: %w", err) - } else if len(targets) == 0 { - log.Logger.Debug("Alpine: no updated file") - return nil - } - log.Logger.Debugf("Alpine updated files: %d", len(targets)) - - bar := utils.PbStartNew(len(targets)) - defer bar.Finish() - - var cves []AlpineCVE - err = utils.FileWalk(rootDir, targets, func(r io.Reader, path string) error { - var cve AlpineCVE - if err = json.NewDecoder(r).Decode(&cve); err != nil { - return xerrors.Errorf("failed to decode Alpine JSON: %w", err) - } - cves = append(cves, cve) - bar.Increment() - return nil - }) - if err != nil { - return xerrors.Errorf("error in Alpine walk: %w", err) - } - - if err = save(cves); err != nil { - return xerrors.Errorf("error in Alpine save: %w", err) - } - - return nil -} - -func save(cves []AlpineCVE) error { - log.Logger.Debug("Saving Alpine DB") - - err := db.BatchUpdate(func(tx *bolt.Tx) error { - for _, cve := range cves { - platformName := fmt.Sprintf(platformFormat, cve.Release) - pkgName := cve.Package - advisory := Advisory{ - VulnerabilityID: cve.VulnerabilityID, - FixedVersion: cve.FixedVersion, - Repository: cve.Repository, - } - if err := db.PutNestedBucket(tx, platformName, pkgName, cve.VulnerabilityID, advisory); err != nil { - return xerrors.Errorf("failed to save alpine advisory: %w", err) - } - - vuln := vulnerability.Vulnerability{ - Title: cve.Subject, - Description: cve.Description, - } - if err := vulnerability.Put(tx, cve.VulnerabilityID, vulnerability.Alpine, vuln); err != nil { - return xerrors.Errorf("failed to save alpine vulnerability: %w", err) - } - } - return nil - }) - if err != nil { - return xerrors.Errorf("error in db batch update: %w", err) - } - return nil -} - -func Get(release string, pkgName string) ([]Advisory, error) { - bucket := fmt.Sprintf(platformFormat, release) - advisories, err := db.ForEach(bucket, pkgName) - if err != nil { - return nil, xerrors.Errorf("error in Alpine foreach: %w", err) - } - - var results []Advisory - for _, v := range advisories { - var advisory Advisory - if err = json.Unmarshal(v, &advisory); err != nil { - return nil, xerrors.Errorf("failed to unmarshal Alpine JSON: %w", err) - } - results = append(results, advisory) - } - return results, nil -} diff --git a/pkg/vulnsrc/alpine/types.go b/pkg/vulnsrc/alpine/types.go deleted file mode 100644 index 4c5ecb6eb79d..000000000000 --- a/pkg/vulnsrc/alpine/types.go +++ /dev/null @@ -1,17 +0,0 @@ -package alpine - -type AlpineCVE struct { - VulnerabilityID string - Release string - Package string - Repository string - FixedVersion string - Subject string - Description string -} - -type Advisory struct { - VulnerabilityID string - FixedVersion string - Repository string -} diff --git a/pkg/vulnsrc/debian-oval/debian-oval.go b/pkg/vulnsrc/debian-oval/debian-oval.go deleted file mode 100644 index 27b9a8b01c10..000000000000 --- a/pkg/vulnsrc/debian-oval/debian-oval.go +++ /dev/null @@ -1,164 +0,0 @@ -package debianoval - -import ( - "encoding/json" - "fmt" - "io" - "os" - "path/filepath" - "strings" - - "github.com/knqyf263/trivy/pkg/vulnsrc/debian" - - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - bolt "github.com/etcd-io/bbolt" - "github.com/knqyf263/trivy/pkg/db" - "github.com/knqyf263/trivy/pkg/log" - - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/utils" -) - -var ( - debianDir = filepath.Join("oval", "debian") - // e.g. debian oval 8 - platformFormat = "debian oval %s" -) - -func Update(dir string, updatedFiles map[string]struct{}) error { - rootDir := filepath.Join(dir, debianDir) - targets, err := utils.FilterTargets(debianDir, updatedFiles) - if err != nil { - return xerrors.Errorf("failed to filter target files: %w", err) - } else if len(targets) == 0 { - log.Logger.Debug("Debian OVAL: no updated file") - return nil - } - log.Logger.Debugf("Debian OVAL updated files: %d", len(targets)) - - bar := utils.PbStartNew(len(targets)) - defer bar.Finish() - - var cves []DebianOVAL - err = utils.FileWalk(rootDir, targets, func(r io.Reader, path string) error { - var cve DebianOVAL - if err = json.NewDecoder(r).Decode(&cve); err != nil { - return xerrors.Errorf("failed to decode Debian OVAL JSON: %w", err) - } - - dirs := strings.Split(path, string(os.PathSeparator)) - if len(dirs) < 3 { - log.Logger.Debugf("invalid path: %s", path) - return nil - } - cve.Release = dirs[len(dirs)-3] - cves = append(cves, cve) - bar.Increment() - return nil - }) - if err != nil { - return xerrors.Errorf("error in Debian OVAL walk: %w", err) - } - - if err = save(cves); err != nil { - return xerrors.Errorf("error in Debian OVAL save: %w", err) - } - - return nil -} - -// from https://github.com/kotakanbe/goval-dictionary/blob/c462c07a5cd0b6de52f167e9aa4298083edfc356/models/debian.go#L53 -func walkDebian(cri Criteria, pkgs []Package) []Package { - for _, c := range cri.Criterions { - ss := strings.Split(c.Comment, " DPKG is earlier than ") - if len(ss) != 2 { - continue - } - - // "0" means notyetfixed or erroneous information. - // Not available because "0" includes erroneous info... - if ss[1] == "0" { - continue - } - pkgs = append(pkgs, Package{ - Name: ss[0], - FixedVersion: strings.Split(ss[1], " ")[0], - }) - } - - if len(cri.Criterias) == 0 { - return pkgs - } - for _, c := range cri.Criterias { - pkgs = walkDebian(c, pkgs) - } - return pkgs -} - -func save(cves []DebianOVAL) error { - log.Logger.Debug("Saving Debian OVAL") - err := db.BatchUpdate(func(tx *bolt.Tx) error { - for _, cve := range cves { - affectedPkgs := walkDebian(cve.Criteria, []Package{}) - for _, affectedPkg := range affectedPkgs { - // stretch => 9 - majorVersion, ok := debian.DebianReleasesMapping[cve.Release] - if !ok { - continue - } - platformName := fmt.Sprintf(platformFormat, majorVersion) - cveID := cve.Metadata.Title - advisory := vulnerability.Advisory{ - VulnerabilityID: cveID, - FixedVersion: affectedPkg.FixedVersion, - } - if err := db.PutNestedBucket(tx, platformName, affectedPkg.Name, cveID, advisory); err != nil { - return xerrors.Errorf("failed to save Debian OVAL advisory: %w", err) - } - - var references []string - for _, ref := range cve.Metadata.References { - references = append(references, ref.RefURL) - } - - vuln := vulnerability.Vulnerability{ - Description: cve.Metadata.Description, - References: references, - } - - if err := vulnerability.Put(tx, cveID, vulnerability.DebianOVAL, vuln); err != nil { - return xerrors.Errorf("failed to save Debian OVAL vulnerability: %w", err) - } - } - } - return nil - }) - if err != nil { - return xerrors.Errorf("error in batch update: %w", err) - } - - return nil -} - -func Get(release string, pkgName string) ([]vulnerability.Advisory, error) { - bucket := fmt.Sprintf(platformFormat, release) - advisories, err := db.ForEach(bucket, pkgName) - if err != nil { - return nil, xerrors.Errorf("error in Debian OVAL foreach: %w", err) - } - if len(advisories) == 0 { - return nil, nil - } - - var results []vulnerability.Advisory - for _, v := range advisories { - var advisory vulnerability.Advisory - if err = json.Unmarshal(v, &advisory); err != nil { - return nil, xerrors.Errorf("failed to unmarshal Debian OVAL JSON: %w", err) - } - results = append(results, advisory) - } - return results, nil -} diff --git a/pkg/vulnsrc/debian-oval/types.go b/pkg/vulnsrc/debian-oval/types.go deleted file mode 100644 index 1f8cf2c15313..000000000000 --- a/pkg/vulnsrc/debian-oval/types.go +++ /dev/null @@ -1,43 +0,0 @@ -package debianoval - -type DebianOVAL struct { - Metadata Metadata - Criteria Criteria - Release string -} - -type Metadata struct { - Title string - AffectedList []Affected - Description string - References []Reference -} - -type Affected struct { - Family string - Platform string - Product string -} - -type Criteria struct { - Operator string - Criterias []Criteria - Criterions []Criterion -} - -type Reference struct { - Source string - RefID string - RefURL string -} - -type Criterion struct { - Negate bool - TestRef string - Comment string -} - -type Package struct { - Name string - FixedVersion string -} diff --git a/pkg/vulnsrc/debian/debian.go b/pkg/vulnsrc/debian/debian.go deleted file mode 100644 index 92abdf70e9db..000000000000 --- a/pkg/vulnsrc/debian/debian.go +++ /dev/null @@ -1,154 +0,0 @@ -package debian - -import ( - "encoding/json" - "fmt" - "io" - "path/filepath" - "strings" - - bolt "github.com/etcd-io/bbolt" - "github.com/knqyf263/trivy/pkg/db" - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/utils" - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - "golang.org/x/xerrors" -) - -const ( - debianDir = "debian" -) - -var ( - // e.g. debian 8 - platformFormat = "debian %s" - DebianReleasesMapping = map[string]string{ - // Code names - "squeeze": "6", - "wheezy": "7", - "jessie": "8", - "stretch": "9", - "buster": "10", - "sid": "unstable", - } -) - -func Update(dir string, updatedFiles map[string]struct{}) error { - rootDir := filepath.Join(dir, debianDir) - targets, err := utils.FilterTargets(debianDir, updatedFiles) - if err != nil { - return xerrors.Errorf("failed to filter target files: %w", err) - } else if len(targets) == 0 { - log.Logger.Debug("Debian: no updated file") - return nil - } - log.Logger.Debugf("Debian updated files: %d", len(targets)) - - bar := utils.PbStartNew(len(targets)) - defer bar.Finish() - - var cves []DebianCVE - err = utils.FileWalk(rootDir, targets, func(r io.Reader, path string) error { - var cve DebianCVE - if err = json.NewDecoder(r).Decode(&cve); err != nil { - return xerrors.Errorf("failed to decode Debian JSON: %w", err) - } - - cve.VulnerabilityID = strings.TrimSuffix(filepath.Base(path), ".json") - cve.Package = filepath.Base(filepath.Dir(path)) - cves = append(cves, cve) - - bar.Increment() - return nil - }) - if err != nil { - return xerrors.Errorf("error in Debian walk: %w", err) - } - - if err = save(cves); err != nil { - return xerrors.Errorf("error in Debian save: %w", err) - } - - return nil -} - -func save(cves []DebianCVE) error { - log.Logger.Debug("Saving Debian DB") - err := db.BatchUpdate(func(tx *bolt.Tx) error { - for _, cve := range cves { - for _, release := range cve.Releases { - for releaseStr := range release.Repositories { - majorVersion, ok := DebianReleasesMapping[releaseStr] - if !ok { - continue - } - platformName := fmt.Sprintf(platformFormat, majorVersion) - if release.Status != "open" { - continue - } - advisory := vulnerability.Advisory{ - VulnerabilityID: cve.VulnerabilityID, - //Severity: severityFromUrgency(release.Urgency), - } - if err := db.PutNestedBucket(tx, platformName, cve.Package, cve.VulnerabilityID, advisory); err != nil { - return xerrors.Errorf("failed to save Debian advisory: %w", err) - } - - vuln := vulnerability.Vulnerability{ - Severity: severityFromUrgency(release.Urgency), - Description: cve.Description, - } - - if err := vulnerability.Put(tx, cve.VulnerabilityID, vulnerability.Debian, vuln); err != nil { - return xerrors.Errorf("failed to save Debian vulnerability: %w", err) - } - } - } - } - return nil - }) - if err != nil { - return xerrors.Errorf("error in batch update: %w", err) - } - - return nil -} - -func Get(release string, pkgName string) ([]vulnerability.Advisory, error) { - bucket := fmt.Sprintf(platformFormat, release) - advisories, err := db.ForEach(bucket, pkgName) - if err != nil { - return nil, xerrors.Errorf("error in Debian foreach: %w", err) - } - if len(advisories) == 0 { - return nil, nil - } - - var results []vulnerability.Advisory - for _, v := range advisories { - var advisory vulnerability.Advisory - if err = json.Unmarshal(v, &advisory); err != nil { - return nil, xerrors.Errorf("failed to unmarshal Debian JSON: %w", err) - } - results = append(results, advisory) - } - return results, nil -} - -func severityFromUrgency(urgency string) vulnerability.Severity { - switch urgency { - case "not yet assigned": - return vulnerability.SeverityUnknown - - case "end-of-life", "unimportant", "low", "low*", "low**": - return vulnerability.SeverityLow - - case "medium", "medium*", "medium**": - return vulnerability.SeverityMedium - - case "high", "high*", "high**": - return vulnerability.SeverityHigh - default: - return vulnerability.SeverityUnknown - } -} diff --git a/pkg/vulnsrc/debian/types.go b/pkg/vulnsrc/debian/types.go deleted file mode 100644 index 5e726aaa494c..000000000000 --- a/pkg/vulnsrc/debian/types.go +++ /dev/null @@ -1,15 +0,0 @@ -package debian - -type DebianCVE struct { - Description string `json:"description"` - Releases map[string]Release `json:"releases"` - Scope string `json:"scope"` - Package string - VulnerabilityID string -} - -type Release struct { - Repositories map[string]string `json:"repositories"` - Status string `json:"status"` - Urgency string `json:"urgency"` -} diff --git a/pkg/vulnsrc/nvd/nvd.go b/pkg/vulnsrc/nvd/nvd.go deleted file mode 100644 index 07f9412d9d6f..000000000000 --- a/pkg/vulnsrc/nvd/nvd.go +++ /dev/null @@ -1,97 +0,0 @@ -package nvd - -import ( - "encoding/json" - "io" - "path/filepath" - - "github.com/knqyf263/trivy/pkg/log" - - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/utils" - - bolt "github.com/etcd-io/bbolt" - "github.com/knqyf263/trivy/pkg/db" -) - -const ( - nvdDir = "nvd" -) - -func Update(dir string, updatedFiles map[string]struct{}) error { - rootDir := filepath.Join(dir, nvdDir) - targets, err := utils.FilterTargets(nvdDir, updatedFiles) - if err != nil { - return xerrors.Errorf("failed to filter target files: %w", err) - } else if len(targets) == 0 { - log.Logger.Debug("NVD: no updated file") - return nil - } - log.Logger.Debugf("NVD updated files: %d", len(targets)) - - bar := utils.PbStartNew(len(targets)) - defer bar.Finish() - var items []Item - err = utils.FileWalk(rootDir, targets, func(r io.Reader, _ string) error { - item := Item{} - if err := json.NewDecoder(r).Decode(&item); err != nil { - return xerrors.Errorf("failed to decode NVD JSON: %w", err) - } - items = append(items, item) - bar.Increment() - return nil - }) - if err != nil { - return xerrors.Errorf("error in NVD walk: %w", err) - } - - if err = save(items); err != nil { - return xerrors.Errorf("error in NVD save: %w", err) - } - - return nil -} - -func save(items []Item) error { - log.Logger.Debug("NVD batch update") - err := vulnerability.BatchUpdate(func(b *bolt.Bucket) error { - for _, item := range items { - cveID := item.Cve.Meta.ID - severity, _ := vulnerability.NewSeverity(item.Impact.BaseMetricV2.Severity) - severityV3, _ := vulnerability.NewSeverity(item.Impact.BaseMetricV3.CvssV3.BaseSeverity) - - var references []string - for _, ref := range item.Cve.References.ReferenceDataList { - references = append(references, ref.URL) - } - - var description string - for _, d := range item.Cve.Description.DescriptionDataList { - if d.Value != "" { - description = d.Value - break - } - } - - vuln := vulnerability.Vulnerability{ - Severity: severity, - SeverityV3: severityV3, - References: references, - Title: "", - Description: description, - } - - if err := db.Put(b, cveID, vulnerability.Nvd, vuln); err != nil { - return err - } - } - return nil - }) - if err != nil { - return xerrors.Errorf("error in batch update: %w", err) - } - return nil -} diff --git a/pkg/vulnsrc/nvd/types.go b/pkg/vulnsrc/nvd/types.go deleted file mode 100644 index 8d4892f46b70..000000000000 --- a/pkg/vulnsrc/nvd/types.go +++ /dev/null @@ -1,55 +0,0 @@ -package nvd - -type NVD struct { - CVEItems []Item `json:"CVE_Items"` -} - -type Item struct { - Cve Cve - Impact Impact -} - -type Cve struct { - Meta Meta `json:"CVE_data_meta"` - References References - Description Description -} - -type Meta struct { - ID string -} - -type Impact struct { - BaseMetricV2 BaseMetricV2 - BaseMetricV3 BaseMetricV3 -} - -type BaseMetricV2 struct { - Severity string -} - -type BaseMetricV3 struct { - CvssV3 CvssV3 -} - -type CvssV3 struct { - BaseSeverity string -} - -type References struct { - ReferenceDataList []ReferenceData `json:"reference_data"` -} -type ReferenceData struct { - Name string - Refsource string - URL string -} - -type Description struct { - DescriptionDataList []DescriptionData `json:"description_data"` -} - -type DescriptionData struct { - Lang string - Value string -} diff --git a/pkg/vulnsrc/redhat/redhat.go b/pkg/vulnsrc/redhat/redhat.go deleted file mode 100644 index b9b04ab3ed95..000000000000 --- a/pkg/vulnsrc/redhat/redhat.go +++ /dev/null @@ -1,247 +0,0 @@ -package redhat - -import ( - "encoding/json" - "fmt" - "io" - "io/ioutil" - "path/filepath" - "strconv" - "strings" - - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/log" - - bolt "github.com/etcd-io/bbolt" - "github.com/knqyf263/trivy/pkg/db" - "github.com/knqyf263/trivy/pkg/utils" - "golang.org/x/xerrors" -) - -const ( - redhatDir = "redhat" - platformFormat = "Red Hat Enterprise Linux %s" -) - -var ( - targetPlatforms = []string{"Red Hat Enterprise Linux 5", "Red Hat Enterprise Linux 6", "Red Hat Enterprise Linux 7"} - targetStatus = []string{"Affected", "Fix deferred", "Will not fix"} -) - -func Update(dir string, updatedFiles map[string]struct{}) error { - rootDir := filepath.Join(dir, redhatDir) - targets, err := utils.FilterTargets(redhatDir, updatedFiles) - if err != nil { - return xerrors.Errorf("failed to filter target files: %w", err) - } else if len(targets) == 0 { - log.Logger.Debug("Red Hat: no updated file") - return nil - } - log.Logger.Debugf("Red Hat updated files: %d", len(targets)) - - bar := utils.PbStartNew(len(targets)) - defer bar.Finish() - - var cves []RedhatCVE - err = utils.FileWalk(rootDir, targets, func(r io.Reader, _ string) error { - content, err := ioutil.ReadAll(r) - if err != nil { - return err - } - cve := RedhatCVE{} - if err = json.Unmarshal(content, &cve); err != nil { - return xerrors.Errorf("failed to decode RedHat JSON: %w", err) - } - switch cve.TempAffectedRelease.(type) { - case []interface{}: - var ar RedhatCVEAffectedReleaseArray - if err = json.Unmarshal(content, &ar); err != nil { - return xerrors.Errorf("unknown affected_release type: %w", err) - } - cve.AffectedRelease = ar.AffectedRelease - case map[string]interface{}: - var ar RedhatCVEAffectedReleaseObject - if err = json.Unmarshal(content, &ar); err != nil { - return xerrors.Errorf("unknown affected_release type: %w", err) - } - cve.AffectedRelease = []RedhatAffectedRelease{ar.AffectedRelease} - case nil: - default: - return xerrors.New("unknown affected_release type") - } - - switch cve.TempPackageState.(type) { - case []interface{}: - var ps RedhatCVEPackageStateArray - if err = json.Unmarshal(content, &ps); err != nil { - return xerrors.Errorf("unknown package_state type: %w", err) - } - cve.PackageState = ps.PackageState - case map[string]interface{}: - var ps RedhatCVEPackageStateObject - if err = json.Unmarshal(content, &ps); err != nil { - return xerrors.Errorf("unknown package_state type: %w", err) - } - cve.PackageState = []RedhatPackageState{ps.PackageState} - case nil: - default: - return xerrors.New("unknown package_state type") - } - cves = append(cves, cve) - bar.Increment() - return nil - }) - if err != nil { - return xerrors.Errorf("error in RedHat walk: %w", err) - } - - if err = save(cves); err != nil { - return xerrors.Errorf("error in RedHat save: %w", err) - } - - return nil -} - -// platformName: pkgStatus -type platform map[string]pkg - -// pkgName: advisoryStatus -type pkg map[string]advisory - -// cveID: version -type advisory map[string]interface{} - -func save(cves []RedhatCVE) error { - log.Logger.Debug("Saving RedHat DB") - err := db.BatchUpdate(func(tx *bolt.Tx) error { - for _, cve := range cves { - for _, affected := range cve.AffectedRelease { - if affected.Package == "" { - continue - } - // e.g. Red Hat Enterprise Linux 7 - platformName := affected.ProductName - if !utils.StringInSlice(affected.ProductName, targetPlatforms) { - continue - } - - pkgName, version := splitPkgName(affected.Package) - advisory := vulnerability.Advisory{ - VulnerabilityID: cve.Name, - FixedVersion: version, - } - if err := db.PutNestedBucket(tx, platformName, pkgName, cve.Name, advisory); err != nil { - return xerrors.Errorf("failed to save Red Hat advisory: %w", err) - } - - } - - for _, pkgState := range cve.PackageState { - pkgName := pkgState.PackageName - if pkgName == "" { - continue - } - // e.g. Red Hat Enterprise Linux 7 - platformName := pkgState.ProductName - if !utils.StringInSlice(platformName, targetPlatforms) { - continue - } - if !utils.StringInSlice(pkgState.FixState, targetStatus) { - continue - } - - advisory := vulnerability.Advisory{ - // this means all versions - FixedVersion: "", - VulnerabilityID: cve.Name, - } - if err := db.PutNestedBucket(tx, platformName, pkgName, cve.Name, advisory); err != nil { - return xerrors.Errorf("failed to save Red Hat advisory: %w", err) - } - - } - - cvssScore, _ := strconv.ParseFloat(cve.Cvss.CvssBaseScore, 64) - cvss3Score, _ := strconv.ParseFloat(cve.Cvss3.Cvss3BaseScore, 64) - - title := strings.TrimPrefix(strings.TrimSpace(cve.Bugzilla.Description), cve.Name) - - vuln := vulnerability.Vulnerability{ - CvssScore: cvssScore, - CvssScoreV3: cvss3Score, - Severity: severityFromThreat(cve.ThreatSeverity), - References: cve.References, - Title: strings.TrimSpace(title), - Description: strings.TrimSpace(strings.Join(cve.Details, "")), - } - if err := vulnerability.Put(tx, cve.Name, vulnerability.RedHat, vuln); err != nil { - return xerrors.Errorf("failed to save Red Hat vulnerability: %w", err) - } - } - return nil - }) - if err != nil { - return err - } - - return nil -} - -func Get(majorVersion string, pkgName string) ([]vulnerability.Advisory, error) { - bucket := fmt.Sprintf(platformFormat, majorVersion) - advisories, err := db.ForEach(bucket, pkgName) - if err != nil { - return nil, xerrors.Errorf("error in Red Hat foreach: %w", err) - } - if len(advisories) == 0 { - return nil, nil - } - - var results []vulnerability.Advisory - for _, v := range advisories { - var advisory vulnerability.Advisory - if err = json.Unmarshal(v, &advisory); err != nil { - return nil, xerrors.Errorf("failed to unmarshal Red Hat JSON: %w", err) - } - results = append(results, advisory) - } - return results, nil -} - -// ref. https://github.com/rpm-software-management/yum/blob/043e869b08126c1b24e392f809c9f6871344c60d/rpmUtils/miscutils.py#L301 -func splitPkgName(pkgName string) (string, string) { - var version string - - // Trim release - index := strings.LastIndex(pkgName, "-") - if index == -1 { - return "", "" - } - version = pkgName[index:] - pkgName = pkgName[:index] - - // Trim version - index = strings.LastIndex(pkgName, "-") - if index == -1 { - return "", "" - } - version = pkgName[index+1:] + version - pkgName = pkgName[:index] - - return pkgName, version -} - -func severityFromThreat(sev string) vulnerability.Severity { - switch strings.Title(sev) { - case "Low": - return vulnerability.SeverityLow - case "Moderate": - return vulnerability.SeverityMedium - case "Important": - return vulnerability.SeverityHigh - case "Critical": - return vulnerability.SeverityCritical - } - return vulnerability.SeverityUnknown -} diff --git a/pkg/vulnsrc/redhat/types.go b/pkg/vulnsrc/redhat/types.go deleted file mode 100644 index 1c2493b0739d..000000000000 --- a/pkg/vulnsrc/redhat/types.go +++ /dev/null @@ -1,80 +0,0 @@ -package redhat - -type RedhatCVE struct { - ThreatSeverity string `json:"threat_severity"` - PublicDate string `json:"public_date"` - Bugzilla RedhatBugzilla `json:"bugzilla"` - Cvss RedhatCvss `json:"cvss"` - Cvss3 RedhatCvss3 `json:"cvss3"` - Iava string `json:"iava"` - Cwe string `json:"cwe"` - Statement string `json:"statement"` - Acknowledgement string `json:"acknowledgement"` - Mitigation string `json:"mitigation"` - TempAffectedRelease interface{} `json:"affected_release"` // affected_release is array or object - AffectedRelease []RedhatAffectedRelease - TempPackageState interface{} `json:"package_state"` // package_state is array or object - PackageState []RedhatPackageState - Name string `json:"name"` - DocumentDistribution string `json:"document_distribution"` - - Details []string `json:"details" gorm:"-"` - References []string `json:"references" gorm:"-"` -} - -type RedhatCVEAffectedReleaseArray struct { - AffectedRelease []RedhatAffectedRelease `json:"affected_release"` -} - -type RedhatCVEAffectedReleaseObject struct { - AffectedRelease RedhatAffectedRelease `json:"affected_release"` -} - -type RedhatCVEPackageStateArray struct { - PackageState []RedhatPackageState `json:"package_state"` -} - -type RedhatCVEPackageStateObject struct { - PackageState RedhatPackageState `json:"package_state"` -} - -type RedhatDetail struct { - Detail string `sql:"type:text"` -} - -type RedhatReference struct { - Reference string `sql:"type:text"` -} - -type RedhatBugzilla struct { - Description string `json:"description" sql:"type:text"` - BugzillaID string `json:"id"` - URL string `json:"url"` -} - -type RedhatCvss struct { - CvssBaseScore string `json:"cvss_base_score"` - CvssScoringVector string `json:"cvss_scoring_vector"` - Status string `json:"status"` -} - -type RedhatCvss3 struct { - Cvss3BaseScore string `json:"cvss3_base_score"` - Cvss3ScoringVector string `json:"cvss3_scoring_vector"` - Status string `json:"status"` -} - -type RedhatAffectedRelease struct { - ProductName string `json:"product_name"` - ReleaseDate string `json:"release_date"` - Advisory string `json:"advisory"` - Package string `json:"package"` - Cpe string `json:"cpe"` -} - -type RedhatPackageState struct { - ProductName string `json:"product_name"` - FixState string `json:"fix_state"` - PackageName string `json:"package_name"` - Cpe string `json:"cpe"` -} diff --git a/pkg/vulnsrc/ubuntu/types.go b/pkg/vulnsrc/ubuntu/types.go deleted file mode 100644 index 4e74600c1448..000000000000 --- a/pkg/vulnsrc/ubuntu/types.go +++ /dev/null @@ -1,18 +0,0 @@ -package ubuntu - -type UbuntuCVE struct { - Description string `json:"description"` - Candidate string - Priority string - Patches map[PackageName]Patch - References []string -} - -type PackageName string -type Release string -type Patch map[Release]Status - -type Status struct { - Status string - Note string -} diff --git a/pkg/vulnsrc/ubuntu/ubuntu.go b/pkg/vulnsrc/ubuntu/ubuntu.go deleted file mode 100644 index f5aba032aeed..000000000000 --- a/pkg/vulnsrc/ubuntu/ubuntu.go +++ /dev/null @@ -1,164 +0,0 @@ -package ubuntu - -import ( - "encoding/json" - "fmt" - "io" - "path/filepath" - - "github.com/knqyf263/trivy/pkg/vulnsrc/vulnerability" - - "github.com/knqyf263/trivy/pkg/log" - - bolt "github.com/etcd-io/bbolt" - - "github.com/knqyf263/trivy/pkg/db" - "golang.org/x/xerrors" - - "github.com/knqyf263/trivy/pkg/utils" -) - -const ( - ubuntuDir = "ubuntu" - platformFormat = "ubuntu %s" - t -) - -var ( - targetStatus = []string{"needed", "deferred", "released"} - UbuntuReleasesMapping = map[string]string{ - "precise": "12.04", - "quantal": "12.10", - "raring": "13.04", - "trusty": "14.04", - "utopic": "14.10", - "vivid": "15.04", - "wily": "15.10", - "xenial": "16.04", - "yakkety": "16.10", - "zesty": "17.04", - "artful": "17.10", - "bionic": "18.04", - "cosmic": "18.10", - "disco": "19.04", - } -) - -func Update(dir string, updatedFiles map[string]struct{}) error { - rootDir := filepath.Join(dir, ubuntuDir) - targets, err := utils.FilterTargets(ubuntuDir, updatedFiles) - if err != nil { - return xerrors.Errorf("failed to filter target files: %w", err) - } else if len(targets) == 0 { - log.Logger.Debug("Ubuntu: no updated file") - return nil - } - log.Logger.Debugf("Ubuntu OVAL updated files: %d", len(targets)) - - bar := utils.PbStartNew(len(targets)) - defer bar.Finish() - - var cves []UbuntuCVE - err = utils.FileWalk(rootDir, targets, func(r io.Reader, path string) error { - var cve UbuntuCVE - if err = json.NewDecoder(r).Decode(&cve); err != nil { - return xerrors.Errorf("failed to decode Ubuntu JSON: %w", err) - } - cves = append(cves, cve) - bar.Increment() - return nil - }) - if err != nil { - return xerrors.Errorf("error in Ubuntu walk: %w", err) - } - - if err = save(cves); err != nil { - return xerrors.Errorf("error in Ubuntu save: %w", err) - } - - return nil -} - -func save(cves []UbuntuCVE) error { - log.Logger.Debug("Saving Ubuntu DB") - err := db.BatchUpdate(func(tx *bolt.Tx) error { - for _, cve := range cves { - for packageName, patch := range cve.Patches { - pkgName := string(packageName) - for release, status := range patch { - if !utils.StringInSlice(status.Status, targetStatus) { - continue - } - osVersion, ok := UbuntuReleasesMapping[string(release)] - if !ok { - continue - } - platformName := fmt.Sprintf(platformFormat, osVersion) - advisory := vulnerability.Advisory{ - VulnerabilityID: cve.Candidate, - } - if status.Status == "released" { - advisory.FixedVersion = status.Note - } - if err := db.PutNestedBucket(tx, platformName, pkgName, cve.Candidate, advisory); err != nil { - return xerrors.Errorf("failed to save Ubuntu advisory: %w", err) - } - - vuln := vulnerability.Vulnerability{ - Severity: severityFromPriority(cve.Priority), - References: cve.References, - Description: cve.Description, - // TODO - Title: "", - } - if err := vulnerability.Put(tx, cve.Candidate, vulnerability.Ubuntu, vuln); err != nil { - return xerrors.Errorf("failed to save Ubuntu vulnerability: %w", err) - } - } - } - } - return nil - }) - if err != nil { - return xerrors.Errorf("error in batch update: %w", err) - } - return nil -} - -func Get(release string, pkgName string) ([]vulnerability.Advisory, error) { - bucket := fmt.Sprintf(platformFormat, release) - advisories, err := db.ForEach(bucket, pkgName) - if err != nil { - return nil, xerrors.Errorf("error in Ubuntu foreach: %w", err) - } - if len(advisories) == 0 { - return nil, nil - } - - var results []vulnerability.Advisory - for _, v := range advisories { - var advisory vulnerability.Advisory - if err = json.Unmarshal(v, &advisory); err != nil { - return nil, xerrors.Errorf("failed to unmarshal Ubuntu JSON: %w", err) - } - results = append(results, advisory) - } - return results, nil -} - -func severityFromPriority(priority string) vulnerability.Severity { - switch priority { - case "untriaged": - return vulnerability.SeverityUnknown - case "negligible", "low": - return vulnerability.SeverityLow - case "medium": - return vulnerability.SeverityMedium - case "high": - return vulnerability.SeverityHigh - case "critical": - return vulnerability.SeverityCritical - default: - return vulnerability.SeverityUnknown - } -} diff --git a/pkg/vulnsrc/vulnerability/const.go b/pkg/vulnsrc/vulnerability/const.go deleted file mode 100644 index eeb5c3e2f743..000000000000 --- a/pkg/vulnsrc/vulnerability/const.go +++ /dev/null @@ -1,18 +0,0 @@ -package vulnerability - -const ( - // Data source - Nvd = "nvd" - RedHat = "redhat" - Debian = "debian" - DebianOVAL = "debian-oval" - Ubuntu = "ubuntu" - CentOS = "centos" - Fedora = "fedora" - Amazon = "amazon" - Alpine = "alpine" - RubySec = "ruby-advisory-db" - PhpSecurityAdvisories = "php-security-advisories" - NodejsSecurityWg = "nodejs-security-wg" - PythonSafetyDB = "python-safety-db" -) diff --git a/pkg/vulnsrc/vulnerability/types.go b/pkg/vulnsrc/vulnerability/types.go deleted file mode 100644 index 29c5eb75e4f2..000000000000 --- a/pkg/vulnsrc/vulnerability/types.go +++ /dev/null @@ -1,67 +0,0 @@ -package vulnerability - -import ( - "fmt" - "time" - - "github.com/fatih/color" -) - -type Severity int - -const ( - SeverityUnknown Severity = iota - SeverityLow - SeverityMedium - SeverityHigh - SeverityCritical -) - -var ( - SeverityNames = []string{ - "UNKNOWN", - "LOW", - "MEDIUM", - "HIGH", - "CRITICAL", - } - SeverityColor = []func(a ...interface{}) string{ - color.New(color.FgCyan).SprintFunc(), - color.New(color.FgBlue).SprintFunc(), - color.New(color.FgYellow).SprintFunc(), - color.New(color.FgHiRed).SprintFunc(), - color.New(color.FgRed).SprintFunc(), - } -) - -func NewSeverity(severity string) (Severity, error) { - for i, name := range SeverityNames { - if severity == name { - return Severity(i), nil - } - } - return SeverityUnknown, fmt.Errorf("unknown severity: %s", severity) -} - -func CompareSeverityString(sev1, sev2 string) bool { - s1, _ := NewSeverity(sev1) - s2, _ := NewSeverity(sev2) - return s1 < s2 -} - -func ColorizeSeverity(severity string) string { - for i, name := range SeverityNames { - if severity == name { - return SeverityColor[i](severity) - } - } - return color.New(color.FgBlue).SprintFunc()(severity) -} - -func (s Severity) String() string { - return SeverityNames[s] -} - -type LastUpdated struct { - Date time.Time -} diff --git a/pkg/vulnsrc/vulnerability/vulnerability.go b/pkg/vulnsrc/vulnerability/vulnerability.go deleted file mode 100644 index 626027b7efc0..000000000000 --- a/pkg/vulnsrc/vulnerability/vulnerability.go +++ /dev/null @@ -1,71 +0,0 @@ -package vulnerability - -import ( - "encoding/json" - - bolt "github.com/etcd-io/bbolt" - "github.com/knqyf263/trivy/pkg/db" - "golang.org/x/xerrors" -) - -const ( - rootBucket = "vulnerability" -) - -type Vulnerability struct { - ID string // e.g. CVE-2019-8331, OSVDB-104365 - CvssScore float64 - CvssScoreV3 float64 - Severity Severity - SeverityV3 Severity - References []string - Title string - Description string -} - -type Advisory struct { - VulnerabilityID string - FixedVersion string -} - -func Put(tx *bolt.Tx, cveID, source string, vuln Vulnerability) error { - root, err := tx.CreateBucketIfNotExists([]byte(rootBucket)) - if err != nil { - return err - } - return db.Put(root, cveID, source, vuln) -} - -func Update(cveID, source string, vuln Vulnerability) error { - return db.Update(rootBucket, cveID, source, vuln) -} - -func BatchUpdate(fn func(b *bolt.Bucket) error) error { - return db.BatchUpdate(func(tx *bolt.Tx) error { - root, err := tx.CreateBucketIfNotExists([]byte(rootBucket)) - if err != nil { - return err - } - return fn(root) - }) -} - -func Get(cveID string) (map[string]Vulnerability, error) { - values, err := db.ForEach(rootBucket, cveID) - if err != nil { - return nil, xerrors.Errorf("error in NVD get: %w", err) - } - if len(values) == 0 { - return nil, nil - } - - vulns := map[string]Vulnerability{} - for source, value := range values { - var vuln Vulnerability - if err = json.Unmarshal(value, &vuln); err != nil { - return nil, xerrors.Errorf("failed to unmarshal Vulnerability JSON: %w", err) - } - vulns[source] = vuln - } - return vulns, nil -} diff --git a/pkg/vulnsrc/vulnsrc.go b/pkg/vulnsrc/vulnsrc.go deleted file mode 100644 index 19541db534a2..000000000000 --- a/pkg/vulnsrc/vulnsrc.go +++ /dev/null @@ -1,75 +0,0 @@ -package vulnsrc - -import ( - "path/filepath" - - "github.com/knqyf263/trivy/pkg/git" - "github.com/knqyf263/trivy/pkg/log" - "github.com/knqyf263/trivy/pkg/utils" - "github.com/knqyf263/trivy/pkg/vulnsrc/alpine" - "github.com/knqyf263/trivy/pkg/vulnsrc/debian" - debianoval "github.com/knqyf263/trivy/pkg/vulnsrc/debian-oval" - "github.com/knqyf263/trivy/pkg/vulnsrc/nvd" - "github.com/knqyf263/trivy/pkg/vulnsrc/redhat" - "github.com/knqyf263/trivy/pkg/vulnsrc/ubuntu" - "golang.org/x/xerrors" -) - -const ( - repoURL = "https://github.com/knqyf263/vuln-list.git" -) - -func Update() (err error) { - log.Logger.Info("Updating vulnerability database...") - - // Clone vuln-list repository - dir := filepath.Join(utils.CacheDir(), "vuln-list") - updatedFiles, err := git.CloneOrPull(repoURL, dir) - if err != nil { - return xerrors.Errorf("error in vulnsrc clone or pull: %w", err) - } - log.Logger.Debugf("total updated files: %d", len(updatedFiles)) - - // Only last_updated.json - if len(updatedFiles) <= 1 { - return nil - } - - // Update NVD - log.Logger.Info("Updating NVD data...") - if err = nvd.Update(dir, updatedFiles); err != nil { - return xerrors.Errorf("error in NVD update: %w", err) - } - - // Update Alpine OVAL - log.Logger.Info("Updating Alpine data...") - if err = alpine.Update(dir, updatedFiles); err != nil { - return xerrors.Errorf("error in Alpine OVAL update: %w", err) - } - - //Update RedHat - log.Logger.Info("Updating RedHat data...") - if err = redhat.Update(dir, updatedFiles); err != nil { - return xerrors.Errorf("error in RedHat update: %w", err) - } - - // Update Debian - log.Logger.Info("Updating Debian data...") - if err = debian.Update(dir, updatedFiles); err != nil { - return xerrors.Errorf("error in Debian update: %w", err) - } - - // Update Debian OVAL - log.Logger.Info("Updating Debian OVAL data...") - if err = debianoval.Update(dir, updatedFiles); err != nil { - return xerrors.Errorf("error in Debian OVAL update: %w", err) - } - - //Update Ubuntu - log.Logger.Info("Updating Ubuntu data...") - if err = ubuntu.Update(dir, updatedFiles); err != nil { - return xerrors.Errorf("error in Ubuntu update: %w", err) - } - - return nil -} diff --git a/pkg/x/http/client.go b/pkg/x/http/client.go new file mode 100644 index 000000000000..62c5879ef1fa --- /dev/null +++ b/pkg/x/http/client.go @@ -0,0 +1,30 @@ +package http + +import ( + "context" + "net/http" + "time" +) + +type ClientOption func(client *http.Client) + +func WithTimeout(timeout time.Duration) ClientOption { + return func(client *http.Client) { + client.Timeout = timeout + } +} + +func Client(opts ...ClientOption) *http.Client { + return ClientWithContext(context.Background(), opts...) +} + +// ClientWithContext returns an HTTP client with the specified context and options. +func ClientWithContext(ctx context.Context, opts ...ClientOption) *http.Client { + c := &http.Client{ + Transport: RoundTripper(ctx), + } + for _, opt := range opts { + opt(c) + } + return c +} diff --git a/pkg/x/http/recorder_test.go b/pkg/x/http/recorder_test.go new file mode 100644 index 000000000000..9862a3b2cb3e --- /dev/null +++ b/pkg/x/http/recorder_test.go @@ -0,0 +1,73 @@ +package http_test + +import ( + "bytes" + "io" + "net/http" +) + +// RequestRecorder is a test helper that records a single HTTP request sent through a RoundTripper +type RequestRecorder struct { + request *http.Request + response *http.Response + err error +} + +// RequestRecorderOption is a functional option for RequestRecorder +type RequestRecorderOption func(*RequestRecorder) + +// WithResponse sets the response to return +func WithResponse(resp *http.Response) RequestRecorderOption { + return func(rr *RequestRecorder) { + rr.response = resp + } +} + +// WithError sets the error to return +func WithError(err error) RequestRecorderOption { + return func(rr *RequestRecorder) { + rr.err = err + } +} + +// NewRequestRecorder creates a new RequestRecorder with optional configuration +func NewRequestRecorder(opts ...RequestRecorderOption) *RequestRecorder { + rr := &RequestRecorder{ + response: &http.Response{ + StatusCode: http.StatusOK, + Status: "200 OK", + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Body: io.NopCloser(bytes.NewReader(nil)), + }, + } + + for _, opt := range opts { + opt(rr) + } + + return rr +} + +// RoundTrip implements http.RoundTripper and records the request +func (rr *RequestRecorder) RoundTrip(req *http.Request) (*http.Response, error) { + // Record the request + rr.request = req + + if rr.err != nil { + return nil, rr.err + } + + if rr.response != nil { + rr.response.Request = req + } + + return rr.response, nil +} + +// Request returns the recorded request +func (rr *RequestRecorder) Request() *http.Request { + return rr.request +} diff --git a/pkg/x/http/trace.go b/pkg/x/http/trace.go new file mode 100644 index 000000000000..e472a7da6ae3 --- /dev/null +++ b/pkg/x/http/trace.go @@ -0,0 +1,519 @@ +package http + +import ( + "bytes" + "fmt" + "io" + "net/http" + "net/http/httputil" + "net/url" + "os" + "regexp" + "strings" + + "github.com/fatih/color" + "github.com/google/go-containerregistry/pkg/v1/types" + + "github.com/aquasecurity/trivy/pkg/fanal/secret" + "github.com/aquasecurity/trivy/pkg/fanal/utils" + "github.com/aquasecurity/trivy/pkg/log" +) + +const ( + redactedText = "" + binaryRedactText = "" + maxBodySize = 1024 * 1024 // 1MB + + // MIME types + mimeApplicationJSON = "application/json" + mimeApplicationXML = "application/xml" + mimeApplicationFormURLEncoded = "application/x-www-form-urlencoded" + mimeApplicationJavaScript = "application/javascript" + mimeApplicationOctetStream = "application/octet-stream" + mimeApplicationPDF = "application/pdf" + mimeApplicationZip = "application/zip" + mimeApplicationGzip = "application/gzip" + mimeApplicationXTar = "application/x-tar" + mimeApplicationXRar = "application/x-rar" + mimeMultipartFormData = "multipart/form-data" + mimeTextPrefix = "text/" + mimeImagePrefix = "image/" + mimeVideoPrefix = "video/" + mimeAudioPrefix = "audio/" + mimeApplicationVndPrefix = "application/vnd." +) + +var ( + // Colors for HTTP trace output + requestHeaderColor = color.New(color.FgCyan, color.Bold).SprintFunc() + responseHeaderColor = color.New(color.FgGreen, color.Bold).SprintFunc() + errorHeaderColor = color.New(color.FgRed, color.Bold).SprintFunc() + redactedColor = color.New(color.FgYellow).SprintFunc() + + // HTTP method colors + methodColor = color.New(color.FgMagenta, color.Bold).SprintFunc() + urlColor = color.New(color.FgBlue).SprintFunc() + + // Header colors + headerKeyColor = color.New(color.FgCyan).SprintFunc() + headerValueColor = color.New(color.FgWhite).SprintFunc() + + // Status code colors + statusSuccessColor = color.New(color.FgGreen).SprintFunc() // 2xx + statusRedirectColor = color.New(color.FgYellow).SprintFunc() // 3xx + statusClientError = color.New(color.FgRed).SprintFunc() // 4xx + statusServerError = color.New(color.FgRed, color.Bold).SprintFunc() // 5xx + + // Sensitive headers that should be redacted + sensitiveHeaders = []string{ + "Authorization", + "Cookie", + "Set-Cookie", + "X-Auth-Token", + "X-API-Key", + "X-API-Secret", + "X-Access-Token", + "X-Secret-Key", + "API-Key", + "Access-Token", + "Proxy-Authorization", + "WWW-Authenticate", + "X-CSRF-Token", + "X-CSRFToken", + } + + // Sensitive query parameters that should be redacted + sensitiveQueryParams = []string{ + "token", + "api_key", + "apikey", + "access_token", + "client_secret", + "secret", + "password", + "auth", + "key", + "session", + "signature", + "oauth_token", + } + + // Regex patterns for redaction + asteriskPattern = regexp.MustCompile(`\*+`) +) + +type traceTransport struct { + inner http.RoundTripper + writer io.Writer + secretScanner secret.Scanner +} + +// TraceOption is a functional option for traceTransport +type TraceOption func(*traceTransport) + +// WithWriter sets the writer for trace output +func WithWriter(w io.Writer) TraceOption { + return func(tt *traceTransport) { + tt.writer = w + } +} + +// NewTraceTransport returns an http.RoundTripper that logs HTTP requests and responses +func NewTraceTransport(inner http.RoundTripper, opts ...TraceOption) http.RoundTripper { + tt := &traceTransport{ + inner: inner, + writer: os.Stderr, // default + secretScanner: secret.NewScanner(nil), // Use built-in rules + } + + for _, opt := range opts { + opt(tt) + } + + return tt +} + +// RoundTrip implements http.RoundTripper with HTTP tracing +func (tt *traceTransport) RoundTrip(req *http.Request) (*http.Response, error) { + // Dump and redact request + reqDump, err := tt.dumpRequest(req) + if err != nil { + log.Debug("Failed to dump HTTP request", log.Err(err)) + } + + coloredDump := colorizeHTTPDump(reqDump, true) + fmt.Fprintf(tt.writer, "\n%s\n%s\n", requestHeaderColor("--- HTTP REQUEST ---"), coloredDump) + + // Make the request + resp, err := tt.inner.RoundTrip(req) + if err != nil { + fmt.Fprintf(tt.writer, "\n%s\n%v\n", errorHeaderColor("--- HTTP ERROR ---"), err) + return nil, err + } else if resp == nil { + return nil, nil + } + + // Dump and redact response + respDump, err := tt.dumpResponse(resp) + if err != nil { + log.Debug("Failed to dump HTTP response", log.Err(err)) + } else { + coloredDump = colorizeHTTPDump(respDump, false) + fmt.Fprintf(tt.writer, "\n%s\n%s\n", responseHeaderColor("--- HTTP RESPONSE ---"), coloredDump) + } + + return resp, nil +} + +// dumpRequest dumps and redacts sensitive information from the request +func (tt *traceTransport) dumpRequest(req *http.Request) (string, error) { + // Clone request to avoid modifying the original + reqClone := req.Clone(req.Context()) + + // Redact sensitive headers + redactHeaders(reqClone.Header) + + // Redact sensitive query parameters + if reqClone.URL != nil { + reqClone.URL = redactQueryParams(reqClone.URL) + } + + // Handle body + if req.Body != nil && req.Body != http.NoBody { + bodyBytes, err := io.ReadAll(req.Body) + if err != nil { + return "", err + } + // Restore original body + req.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + + // Set redacted body on clone + redactedBody := tt.redactBody(bodyBytes, req.Header.Get("Content-Type")) + reqClone.Body = io.NopCloser(bytes.NewReader(redactedBody)) + reqClone.ContentLength = int64(len(redactedBody)) + } + + // Dump the redacted request + dump, err := httputil.DumpRequestOut(reqClone, true) + if err != nil { + return "", err + } + + return string(dump), nil +} + +// dumpResponse dumps and redacts sensitive information from the response +func (tt *traceTransport) dumpResponse(resp *http.Response) (string, error) { + // Read response body + var bodyBytes []byte + if resp.Body != nil { + var err error + bodyBytes, err = io.ReadAll(resp.Body) + if err != nil { + return "", err + } + // Restore original body + resp.Body = io.NopCloser(bytes.NewReader(bodyBytes)) + } + + // Clone response for redaction + respClone := &http.Response{ + Status: resp.Status, + StatusCode: resp.StatusCode, + Proto: resp.Proto, + ProtoMajor: resp.ProtoMajor, + ProtoMinor: resp.ProtoMinor, + Header: resp.Header.Clone(), + ContentLength: resp.ContentLength, + TransferEncoding: resp.TransferEncoding, + Close: resp.Close, + Uncompressed: resp.Uncompressed, + Trailer: resp.Trailer, + Request: resp.Request, + } + + // Redact sensitive headers + redactHeaders(respClone.Header) + + // Set redacted body + if len(bodyBytes) > 0 { + redactedBody := tt.redactBody(bodyBytes, resp.Header.Get("Content-Type")) + respClone.Body = io.NopCloser(bytes.NewReader(redactedBody)) + respClone.ContentLength = int64(len(redactedBody)) + } + + // Dump the redacted response + dump, err := httputil.DumpResponse(respClone, true) + if err != nil { + return "", err + } + + return string(dump), nil +} + +// redactHeaders redacts sensitive headers +func redactHeaders(headers http.Header) { + for _, header := range sensitiveHeaders { + for k := range headers { + if strings.EqualFold(k, header) { + headers[k] = []string{redactedColor(redactedText)} + } + } + } +} + +// redactQueryParams redacts sensitive query parameters +func redactQueryParams(u *url.URL) *url.URL { + // Clone URL to avoid modifying the original + cloned, _ := url.Parse(u.String()) + + values := cloned.Query() + for _, param := range sensitiveQueryParams { + for k := range values { + if strings.EqualFold(k, param) { + values[k] = []string{redactedColor(redactedText)} + } + } + } + cloned.RawQuery = values.Encode() + + return cloned +} + +// redactBody redacts sensitive information from request/response bodies using various methods including Trivy's secret scanner +func (tt *traceTransport) redactBody(body []byte, contentType string) []byte { + // Check if body is too large + if len(body) > maxBodySize { + return fmt.Appendf(nil, "", len(body)) + } + + // Check if body is binary + if isBinaryContent(contentType) || isBinaryData(body) { + return []byte(redactedColor(binaryRedactText)) + } + + // Start with the original content + redacted := string(body) + + // First, use Trivy's secret scanner for detection + scanResult := tt.secretScanner.Scan(secret.ScanArgs{ + FilePath: "http-body.txt", + Content: bytes.NewReader(body), + Binary: false, + }) + + // If scanner found secrets, redact them + if len(scanResult.Findings) > 0 { + lines := strings.Split(redacted, "\n") + for _, finding := range scanResult.Findings { + for _, line := range finding.Code.Lines { + if line.IsCause && line.Number > 0 && line.Number <= len(lines) { + // Replace one or more * with using regex + redactedLine := asteriskPattern.ReplaceAllString(line.Content, redactedColor(redactedText)) + lines[line.Number-1] = redactedLine + } + } + } + redacted = strings.Join(lines, "\n") + } + + // Apply additional pattern-based redactions + coloredRedactedText := redactedColor(redactedText) + + // Handle JSON patterns + jsonPattern := regexp.MustCompile(`(?i)"(password|passwd|pwd|secret|token|api_key|apikey|access_token|client_secret|auth_token|private_key)"\s*:\s*"[^"]*"`) + redacted = jsonPattern.ReplaceAllStringFunc(redacted, func(match string) string { + colonIndex := strings.Index(match, ":") + if colonIndex != -1 { + key := match[:colonIndex+1] + return key + ` "` + coloredRedactedText + `"` + } + return coloredRedactedText + }) + + // Handle form data patterns + formPattern := regexp.MustCompile(`(?i)(password|passwd|pwd|secret|token|api_key|apikey|access_token|client_secret|auth_token|private_key)=[^&\s]+`) + redacted = formPattern.ReplaceAllStringFunc(redacted, func(match string) string { + equalIndex := strings.Index(match, "=") + if equalIndex != -1 { + key := match[:equalIndex+1] + return key + coloredRedactedText + } + return coloredRedactedText + }) + + // Handle Bearer tokens + bearerPattern := regexp.MustCompile(`(?i)Bearer\s+[A-Za-z0-9\-._~+/]+=*`) + redacted = bearerPattern.ReplaceAllString(redacted, coloredRedactedText) + + return []byte(redacted) +} + +// isBinaryContent checks if the content type indicates binary data +func isBinaryContent(contentType string) bool { + if contentType == "" { + return false + } + + contentType = strings.ToLower(contentType) + + // Text-like content types (explicitly not binary) + textTypes := []string{ + mimeTextPrefix, + mimeApplicationJSON, + mimeApplicationXML, + mimeApplicationFormURLEncoded, + mimeApplicationJavaScript, + string(types.OCIContentDescriptor), + string(types.OCIImageIndex), + string(types.OCIManifestSchema1), + string(types.OCIConfigJSON), + string(types.DockerManifestSchema1), + string(types.DockerManifestSchema1Signed), + string(types.DockerManifestSchema2), + string(types.DockerManifestList), + string(types.DockerConfigJSON), + string(types.DockerPluginConfig), + } + + for _, textType := range textTypes { + if strings.HasPrefix(contentType, textType) { + return false + } + } + + // Common binary content types + binaryTypes := []string{ + mimeApplicationOctetStream, + mimeApplicationPDF, + mimeApplicationZip, + mimeApplicationGzip, + mimeApplicationXTar, + mimeApplicationXRar, + mimeImagePrefix, + mimeVideoPrefix, + mimeAudioPrefix, + mimeApplicationVndPrefix, + mimeMultipartFormData, + } + + for _, bType := range binaryTypes { + if strings.HasPrefix(contentType, bType) { + return true + } + } + + return false +} + +// isBinaryData checks if the data appears to be binary using utils.IsBinary +func isBinaryData(data []byte) bool { + if len(data) == 0 { + return false + } + + // Use bytes.Reader to implement ReadSeekerAt interface + reader := bytes.NewReader(data) + + isBinary, _ := utils.IsBinary(reader, int64(len(data))) + return isBinary +} + +// colorizeHTTPDump adds colors to HTTP request/response dumps +func colorizeHTTPDump(dump string, isRequest bool) string { + lines := strings.Split(dump, "\n") + if len(lines) == 0 { + return dump + } + + var result []string + isBody := false + + for i, line := range lines { + // Empty line indicates start of body + if line == "" || line == "\r" { + isBody = true + result = append(result, line) + continue + } + + // First line processing + if i == 0 { + if isRequest { + // Colorize request line: METHOD URL HTTP/1.1 + parts := strings.Fields(line) + if len(parts) >= 3 { + coloredLine := fmt.Sprintf("%s %s %s", + methodColor(parts[0]), + urlColor(parts[1]), + strings.Join(parts[2:], " ")) + result = append(result, coloredLine) + } else { + result = append(result, line) + } + } else { + // Colorize response line: HTTP/1.1 STATUS_CODE STATUS_TEXT + parts := strings.SplitN(line, " ", 3) + if len(parts) >= 2 { + statusCode := parts[1] + statusColor := getStatusCodeColor(statusCode) + coloredLine := fmt.Sprintf("%s %s", + parts[0], + statusColor(strings.Join(parts[1:], " "))) + result = append(result, coloredLine) + } else { + result = append(result, line) + } + } + continue + } + + // Header processing + if !isBody && strings.Contains(line, ":") { + parts := strings.SplitN(line, ":", 2) + if len(parts) == 2 { + key := parts[0] + value := strings.TrimSpace(parts[1]) + // Don't color the value if it's already colored (e.g., redacted text) + if strings.Contains(value, "\x1b[") { + coloredLine := fmt.Sprintf("%s: %s", + headerKeyColor(key), + value) + result = append(result, coloredLine) + } else { + coloredLine := fmt.Sprintf("%s: %s", + headerKeyColor(key), + headerValueColor(value)) + result = append(result, coloredLine) + } + } else { + result = append(result, line) + } + } else { + // Body content - no additional coloring + result = append(result, line) + } + } + + return strings.Join(result, "\n") +} + +// getStatusCodeColor returns the appropriate color function for a status code +func getStatusCodeColor(statusCode string) func(...any) string { + if statusCode == "" { + return headerValueColor + } + + switch statusCode[0] { + case '2': + return statusSuccessColor + case '3': + return statusRedirectColor + case '4': + return statusClientError + case '5': + return statusServerError + default: + return headerValueColor + } +} diff --git a/pkg/x/http/trace_test.go b/pkg/x/http/trace_test.go new file mode 100644 index 000000000000..6310551f719e --- /dev/null +++ b/pkg/x/http/trace_test.go @@ -0,0 +1,287 @@ +package http_test + +import ( + "bytes" + "cmp" + "io" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + xhttp "github.com/aquasecurity/trivy/pkg/x/http" +) + +func TestTraceTransport_RoundTrip(t *testing.T) { + // Disable colors for consistent testing + t.Setenv("NO_COLOR", "1") + + tests := []struct { + name string + method string + url string + headers map[string]string + body io.Reader + wantOutput string + responseStatus int + responseBody string + responseHeaders map[string]string + wantError error + }{ + { + name: "traces GET request", + method: "GET", + url: "http://example.com/test", + headers: map[string]string{ + "Accept": "application/json", + }, + wantOutput: ` +--- HTTP REQUEST --- +GET /test HTTP/1.1 +Host: example.com +User-Agent: test-agent/1.0 +Accept: application/json +Accept-Encoding: gzip + + + +--- HTTP RESPONSE --- +HTTP/1.1 200 OK +Content-Length: 0 + + +`, + }, + { + name: "redacts sensitive headers", + method: "POST", + url: "http://api.example.com/auth?token=secret123", + headers: map[string]string{ + "Authorization": "Bearer my-secret-token", + "Content-Type": "application/json", + }, + body: strings.NewReader(`{"password": "secret-password"}`), + wantOutput: ` +--- HTTP REQUEST --- +POST /auth?token=%3Credacted%3E HTTP/1.1 +Host: api.example.com +User-Agent: test-agent/1.0 +Content-Length: 26 +Authorization: +Content-Type: application/json +Accept-Encoding: gzip + +{"password": ""} + +--- HTTP RESPONSE --- +HTTP/1.1 200 OK +Content-Length: 0 + + +`, + }, + { + name: "traces error responses", + method: "GET", + url: "http://example.com/error", + responseStatus: 404, + responseBody: "Not Found", + wantOutput: ` +--- HTTP REQUEST --- +GET /error HTTP/1.1 +Host: example.com +User-Agent: test-agent/1.0 +Accept-Encoding: gzip + + + +--- HTTP RESPONSE --- +HTTP/1.1 404 Not Found +Content-Length: 9 + +Not Found +`, + }, + { + name: "redacts sensitive response headers", + method: "GET", + url: "http://example.com/login", + responseStatus: 200, + responseHeaders: map[string]string{ + "Set-Cookie": "session=abc123; HttpOnly", + "Content-Type": "text/plain", + }, + responseBody: "OK", + wantOutput: ` +--- HTTP REQUEST --- +GET /login HTTP/1.1 +Host: example.com +User-Agent: test-agent/1.0 +Accept-Encoding: gzip + + + +--- HTTP RESPONSE --- +HTTP/1.1 200 OK +Content-Length: 2 +Content-Type: text/plain +Set-Cookie: + +OK +`, + }, + { + name: "handles binary content", + method: "POST", + url: "http://example.com/upload", + headers: map[string]string{ + "Content-Type": "application/octet-stream", + }, + body: bytes.NewReader([]byte{ + 0x00, + 0x01, + 0x02, + 0x03, + }), + wantOutput: ` +--- HTTP REQUEST --- +POST /upload HTTP/1.1 +Host: example.com +User-Agent: test-agent/1.0 +Content-Length: 22 +Content-Type: application/octet-stream +Accept-Encoding: gzip + + + +--- HTTP RESPONSE --- +HTTP/1.1 200 OK +Content-Length: 0 + + +`, + }, + { + name: "redacts private keys detected by scanner", + method: "POST", + url: "http://api.company.com/keys", + headers: map[string]string{ + "Content-Type": "text/plain", + }, + body: strings.NewReader(`-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA7WXkqw8EQOLGWVzsUJ7tAwVAV5zTW8V+O1KoZFpENWKDZlPU +VwEiQxaZq8pQJbm2O9zKlMSU7J6PZpuBvb8ZUhxWz9SLKmzqZjIqI4Vg5UUrUpOo +V9aVzqf8ZoMxLQGjCa8iGPp6r6qF5a+FGQ5z9b5x8y1dF3cJNmzGVqw6XlHzpQZJ +z7YwWJmQyOsQKSGQUfZMSgNQs4qT8dOZqYq9iOzGWt1s3Q6QZO4vA4iJoXl8MgU2 +EyG0ZxO7zBqQGKzXzMKJQP4Z+4qQ9Q1iNiQsUz8PzQgU8MzfzMGDQCJOJQZzX1zK +EwQwQ7CQJQJ8qQMqJ9Q2MQMqJ8qQKJQZMQOKJQZ4zX1zKEwQwQ7CQJQJ8 +-----END RSA PRIVATE KEY----- +Normal text here`), + wantOutput: ` +--- HTTP REQUEST --- +POST /keys HTTP/1.1 +Host: api.company.com +User-Agent: test-agent/1.0 +Content-Length: 144 +Content-Type: text/plain +Accept-Encoding: gzip + +-----BEGIN RSA PRIVATE KEY----- + + + + + + +-----END RSA PRIVATE KEY----- +Normal text here + +--- HTTP RESPONSE --- +HTTP/1.1 200 OK +Content-Length: 0 + + +`, + }, + { + name: "handles transport errors", + method: "GET", + url: "http://example.com/test", + wantOutput: ` +--- HTTP REQUEST --- +GET /test HTTP/1.1 +Host: example.com + +--- HTTP ERROR --- +http: use last response +`, + wantError: http.ErrUseLastResponse, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a buffer to capture output + var output bytes.Buffer + + // Create request recorder + var recorder *RequestRecorder + switch { + case tt.wantError != nil: + recorder = NewRequestRecorder(WithError(tt.wantError)) + case tt.responseStatus != 0 || tt.responseBody != "" || len(tt.responseHeaders) > 0: + status := cmp.Or(tt.responseStatus, http.StatusOK) + mockResponse := &http.Response{ + StatusCode: status, + Status: http.StatusText(status), + Proto: "HTTP/1.1", + ProtoMajor: 1, + ProtoMinor: 1, + Header: make(http.Header), + Body: io.NopCloser(strings.NewReader(tt.responseBody)), + } + for k, v := range tt.responseHeaders { + mockResponse.Header.Set(k, v) + } + recorder = NewRequestRecorder(WithResponse(mockResponse)) + default: + recorder = NewRequestRecorder() + } + + // Create trace transport with output writer + transport := xhttp.NewTraceTransport(recorder, xhttp.WithWriter(&output)) + // Wrap with user agent transport for consistent testing + transport = xhttp.NewUserAgent(transport, "test-agent/1.0") + + // Create request + req, err := http.NewRequest(tt.method, tt.url, tt.body) + require.NoError(t, err) + + // Set headers + for k, v := range tt.headers { + req.Header.Set(k, v) + } + + // Make request + resp, err := transport.RoundTrip(req) + if tt.wantError != nil { + assert.Equal(t, tt.wantError, err) + return + } + require.NoError(t, err) + defer resp.Body.Close() + + // Normalize CRLF to LF for consistent testing + got := strings.ReplaceAll(output.String(), "\r\n", "\n") + assert.Equal(t, tt.wantOutput, got) + + // Verify request was recorded + recorded := recorder.Request() + require.NotNil(t, recorded) + assert.Equal(t, tt.method, recorded.Method) + assert.Equal(t, tt.url, recorded.URL.String()) + }) + } +} diff --git a/pkg/x/http/transport.go b/pkg/x/http/transport.go new file mode 100644 index 000000000000..81633c82e6f2 --- /dev/null +++ b/pkg/x/http/transport.go @@ -0,0 +1,145 @@ +package http + +import ( + "cmp" + "context" + "crypto/tls" + "crypto/x509" + "fmt" + "net" + "net/http" + "sync" + "time" + + "github.com/aquasecurity/trivy/pkg/version/app" +) + +var ( + defaultTransport = NewTransport(Options{}) + mu sync.RWMutex +) + +// wrapper wraps an http.RoundTripper to add custom behavior (e.g., retry, logging). +type wrapper func(http.RoundTripper) http.RoundTripper + +// TransportOption modifies an *http.Transport. +type TransportOption func(*http.Transport) + +// Transport is an interface for building an http.RoundTripper. +type Transport interface { + Build(opts ...TransportOption) http.RoundTripper +} + +// transport is the default implementation of Transport. +type transport struct { + base *http.Transport + wrappers []wrapper +} + +// Build returns an http.RoundTripper with TransportOptions applied and all wrappers applied. +func (t *transport) Build(opts ...TransportOption) http.RoundTripper { + base := t.base.Clone() + for _, opt := range opts { + opt(base) + } + var tr http.RoundTripper = base + for _, wrapper := range t.wrappers { + tr = wrapper(tr) + } + return tr +} + +type transportKey struct{} + +// WithTransport returns a new context with the given transport. +// This is mainly for testing when a different HTTP transport needs to be used. +func WithTransport(ctx context.Context, t Transport) context.Context { + return context.WithValue(ctx, transportKey{}, t) +} + +// Options configures the transport settings +type Options struct { + Insecure bool + Timeout time.Duration + CACerts *x509.CertPool + UserAgent string + TraceHTTP bool +} + +// SetDefaultTransport sets the default transport configuration +func SetDefaultTransport(t Transport) { + mu.Lock() + defer mu.Unlock() + defaultTransport = t +} + +// RoundTripper returns the http.RoundTripper from the context, or builds one from the default transport. +// TransportOptions can be used to override the base transport settings for the returned http.RoundTripper only; +// they do not modify the default transport or the transport stored in the context. +func RoundTripper(ctx context.Context, opts ...TransportOption) http.RoundTripper { + var t Transport + if ct, ok := ctx.Value(transportKey{}).(Transport); ok { + t = ct + } else { + mu.RLock() + t = defaultTransport + mu.RUnlock() + } + return t.Build(opts...) +} + +// WithInsecure returns a TransportOption that sets InsecureSkipVerify. +func WithInsecure(insecure bool) TransportOption { + return func(tr *http.Transport) { + if tr.TLSClientConfig == nil { + tr.TLSClientConfig = &tls.Config{} + } + tr.TLSClientConfig.InsecureSkipVerify = insecure + } +} + +// NewTransport creates a new custom Transport with the specified options. +// It should be used to initialize the default transport via SetDefaultTransport. +// In most cases, you should use the `RoundTripper` function to get the http.RoundTripper. +func NewTransport(opts Options) Transport { + tr := http.DefaultTransport.(*http.Transport).Clone() + + // Set timeout (default to 5 minutes) + timeout := cmp.Or(opts.Timeout, 5*time.Minute) + d := &net.Dialer{ + Timeout: timeout, + } + tr.DialContext = d.DialContext + + // Configure TLS only when needed. + if opts.CACerts != nil || opts.Insecure { + tr.TLSClientConfig = &tls.Config{ + InsecureSkipVerify: opts.Insecure, + RootCAs: opts.CACerts, + } + } + + userAgent := cmp.Or(opts.UserAgent, fmt.Sprintf("trivy/%s", app.Version())) + + // Apply trace transport first, then user agent transport + // so that the user agent is set before the request is logged + rt := &transport{base: tr} + if opts.TraceHTTP { + rt.wrappers = append(rt.wrappers, traceWrapper()) + } + + rt.wrappers = append(rt.wrappers, userAgentWrapper(userAgent)) + return rt +} + +func traceWrapper() wrapper { + return func(rt http.RoundTripper) http.RoundTripper { + return NewTraceTransport(rt) + } +} + +func userAgentWrapper(ua string) wrapper { + return func(rt http.RoundTripper) http.RoundTripper { + return NewUserAgent(rt, ua) + } +} diff --git a/pkg/x/http/useragent.go b/pkg/x/http/useragent.go new file mode 100644 index 000000000000..0601682a0116 --- /dev/null +++ b/pkg/x/http/useragent.go @@ -0,0 +1,28 @@ +package http + +import ( + "net/http" +) + +type userAgentTransport struct { + inner http.RoundTripper + ua string +} + +// NewUserAgent returns an http.Roundtripper that sets the user agent +// +// User-Agent: trivy/v0.64.0 +func NewUserAgent(inner http.RoundTripper, ua string) http.RoundTripper { + return &userAgentTransport{ + inner: inner, + ua: ua, + } +} + +// RoundTrip implements http.RoundTripper +func (ut *userAgentTransport) RoundTrip(in *http.Request) (*http.Response, error) { + if ut.ua != "" { + in.Header.Set("User-Agent", ut.ua) + } + return ut.inner.RoundTrip(in) +} diff --git a/pkg/x/http/useragent_test.go b/pkg/x/http/useragent_test.go new file mode 100644 index 000000000000..feb060382e6c --- /dev/null +++ b/pkg/x/http/useragent_test.go @@ -0,0 +1,90 @@ +package http_test + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + xhttp "github.com/aquasecurity/trivy/pkg/x/http" +) + +func TestUserAgentTransport_RoundTrip(t *testing.T) { + tests := []struct { + name string + userAgent string + existingHeaders map[string]string + existingUA string + wantUA string + wantHeaders map[string]string + }{ + { + name: "custom user agent", + userAgent: "custom-scanner/2.1", + wantUA: "custom-scanner/2.1", + }, + { + name: "preserves existing headers", + userAgent: "test-agent/1.0", + existingHeaders: map[string]string{ + "Authorization": "Bearer token123", + "Content-Type": "application/json", + }, + wantUA: "test-agent/1.0", + wantHeaders: map[string]string{ + "Authorization": "Bearer token123", + "Content-Type": "application/json", + }, + }, + { + name: "overwrites existing user agent", + userAgent: "new-agent/2.0", + existingUA: "old-agent/1.0", + wantUA: "new-agent/2.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a request recorder + recorder := NewRequestRecorder() + + // Create transport with user agent + transport := xhttp.NewUserAgent(recorder, tt.userAgent) + + // Create request with an invalid URL to avoid actual network calls + // cf. https://www.rfc-editor.org/rfc/rfc6761 + req, err := http.NewRequest(http.MethodGet, "http://example.invalid/test", http.NoBody) + require.NoError(t, err) + + // Set existing headers + for key, value := range tt.existingHeaders { + req.Header.Set(key, value) + } + + // Set User-Agent + req.Header.Set("User-Agent", tt.existingUA) + + // Make request + resp, _ := transport.RoundTrip(req) + if resp != nil && resp.Body != nil { + resp.Body.Close() + } + + // Check the recorded request + recorded := recorder.Request() + require.NotNil(t, recorded) + + // Check User-Agent + gotUA := recorded.UserAgent() + assert.Equal(t, tt.wantUA, gotUA) + + // Check other headers are preserved + for key, wantValue := range tt.wantHeaders { + gotValue := recorded.Header.Get(key) + assert.Equal(t, wantValue, gotValue, "header %s", key) + } + }) + } +} diff --git a/pkg/x/io/counting_reader.go b/pkg/x/io/counting_reader.go new file mode 100644 index 000000000000..1b140d28bbce --- /dev/null +++ b/pkg/x/io/counting_reader.go @@ -0,0 +1,32 @@ +package io + +import ( + "io" +) + +// CountingReader wraps an io.Reader and counts the number of bytes read. +// Note: This implementation is NOT thread-safe. It should not be used +// concurrently from multiple goroutines. +type CountingReader struct { + r io.Reader + bytesRead int64 +} + +// NewCountingReader creates a new CountingReader that wraps the provided io.Reader. +func NewCountingReader(r io.Reader) *CountingReader { + return &CountingReader{r: r} +} + +// Read reads data from the underlying reader and counts the number of bytes read. +func (c *CountingReader) Read(p []byte) (int, error) { + n, err := c.r.Read(p) + if n > 0 { + c.bytesRead += int64(n) + } + return n, err +} + +// BytesRead returns the number of bytes read. +func (c *CountingReader) BytesRead() int64 { + return c.bytesRead +} diff --git a/pkg/x/io/counting_reader_test.go b/pkg/x/io/counting_reader_test.go new file mode 100644 index 000000000000..6a8584346365 --- /dev/null +++ b/pkg/x/io/counting_reader_test.go @@ -0,0 +1,64 @@ +package io + +import ( + "bytes" + "io" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCountingReader(t *testing.T) { + testCases := []struct { + name string + data []byte + readSize int + want int64 + }{ + { + name: "empty data", + data: []byte{}, + readSize: 10, + want: 0, + }, + { + name: "small data single read", + data: []byte("hello"), + readSize: 10, + want: 5, + }, + { + name: "multiple reads", + data: []byte("hello world"), + readSize: 5, + want: 11, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // Create a reader with test data + reader := bytes.NewReader(tc.data) + + // Wrap with counter + counter := NewCountingReader(reader) + + // Read all data + buf := make([]byte, tc.readSize) + for { + n, err := counter.Read(buf) + if err == io.EOF { + break + } + require.NoError(t, err, "unexpected error during read") + if n == 0 { + break + } + } + + // Verify bytes read + assert.Equal(t, tc.want, counter.BytesRead(), "BytesRead() should return correct count") + }) + } +} diff --git a/pkg/x/io/io.go b/pkg/x/io/io.go new file mode 100644 index 000000000000..8612e53b8b92 --- /dev/null +++ b/pkg/x/io/io.go @@ -0,0 +1,98 @@ +package io + +import ( + "bytes" + "context" + "io" + + "golang.org/x/xerrors" +) + +type ReadSeekerAt interface { + io.ReadSeeker + io.ReaderAt +} + +type ReadSeekCloserAt interface { + io.ReadSeekCloser + io.ReaderAt +} + +func NewReadSeekerAt(r io.Reader) (ReadSeekerAt, error) { + if rr, ok := r.(ReadSeekerAt); ok { + return rr, nil + } + + buff := bytes.NewBuffer([]byte{}) + if _, err := io.Copy(buff, r); err != nil { + return nil, xerrors.Errorf("copy error: %w", err) + } + + return bytes.NewReader(buff.Bytes()), nil +} + +func NewReadSeekerAtWithSize(r io.Reader) (ReadSeekerAt, int64, error) { + rsa, err := NewReadSeekerAt(r) + if err != nil { + return nil, 0, err + } + + br, ok := rsa.(*bytes.Reader) + if ok { + return rsa, br.Size(), nil + } + + size, err := getSeekerSize(rsa) + if err != nil { + return nil, 0, xerrors.Errorf("get size error: %w", err) + } + return rsa, size, nil +} + +func getSeekerSize(s io.Seeker) (int64, error) { + size, err := s.Seek(0, io.SeekEnd) + if err != nil { + return 0, xerrors.Errorf("seek error: %w", err) + } + + if _, err = s.Seek(0, io.SeekStart); err != nil { + return 0, xerrors.Errorf("seek error: %w", err) + } + return size, nil +} + +// NopCloser returns a ReadSeekCloserAt with a no-op Close method wrapping +// the provided Reader r. +func NopCloser(r ReadSeekerAt) ReadSeekCloserAt { + return nopCloser{r} +} + +type nopCloser struct { + ReadSeekerAt +} + +func (nopCloser) Close() error { return nil } + +// readerFunc is a function that implements io.Reader +type readerFunc func([]byte) (int, error) + +func (f readerFunc) Read(p []byte) (int, error) { + return f(p) +} + +// Copy copies from src to dst until either EOF is reached on src or the context is canceled. +// It returns the number of bytes copied and the first error encountered while copying, if any. +// +// Note: This implementation wraps the reader with a context check, which means it won't +// benefit from WriterTo optimization in io.Copy if the source implements it. This is a trade-off +// for being able to cancel the operation on context cancellation. +func Copy(ctx context.Context, dst io.Writer, src io.Reader) (int64, error) { + return io.Copy(dst, readerFunc(func(p []byte) (int, error) { + select { + case <-ctx.Done(): + return 0, ctx.Err() + default: + return src.Read(p) + } + })) +} diff --git a/pkg/x/io/io_test.go b/pkg/x/io/io_test.go new file mode 100644 index 000000000000..b9c5b7f672b0 --- /dev/null +++ b/pkg/x/io/io_test.go @@ -0,0 +1,66 @@ +package io + +import ( + "bytes" + "context" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCopy(t *testing.T) { + t.Run("successful copy", func(t *testing.T) { + ctx := t.Context() + src := strings.NewReader("hello world") + dst := &bytes.Buffer{} + + n, err := Copy(ctx, dst, src) + require.NoError(t, err) + assert.Equal(t, int64(11), n) + assert.Equal(t, "hello world", dst.String()) + }) + + t.Run("context canceled before read", func(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + cancel() // Cancel immediately + + src := strings.NewReader("hello world") + dst := &bytes.Buffer{} + + n, err := Copy(ctx, dst, src) + require.ErrorIs(t, err, context.Canceled) + assert.Equal(t, int64(0), n) + assert.Empty(t, dst.String()) + }) + + t.Run("context canceled during read", func(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + + // Create a reader that will be canceled after first read + reader := &dummyReader{ + cancel: cancel, // Cancel after first read + } + dst := &bytes.Buffer{} + + n, err := Copy(ctx, dst, reader) + require.ErrorIs(t, err, context.Canceled) + // Should have written first chunk before cancellation + assert.Equal(t, int64(5), n) + assert.Equal(t, "dummy", dst.String()) + }) +} + +// dummyReader returns the same data on every Read call +type dummyReader struct { + cancel context.CancelFunc +} + +func (r *dummyReader) Read(p []byte) (int, error) { + n := copy(p, "dummy") + if r.cancel != nil { + r.cancel() // Simulate cancellation after first read + } + return n, nil +} diff --git a/pkg/x/json/json.go b/pkg/x/json/json.go new file mode 100644 index 000000000000..e0d5e7df290a --- /dev/null +++ b/pkg/x/json/json.go @@ -0,0 +1,135 @@ +package json + +import ( + "bytes" + "encoding/json/jsontext" + "encoding/json/v2" + "io" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/set" +) + +// lineReader is a custom reader that tracks line numbers. +type lineReader struct { + r io.Reader + line int +} + +// NewLineReader creates a new line reader. +func NewLineReader(r io.Reader) *lineReader { + return &lineReader{ + r: r, + line: 1, + } +} + +func (lr *lineReader) Read(p []byte) (n int, err error) { + n, err = lr.r.Read(p) + if n > 0 { + // Count the number of newlines in the read buffer + lr.line += bytes.Count(p[:n], []byte("\n")) + } + return n, err +} + +func (lr *lineReader) Line() int { + return lr.line +} + +func Unmarshal(data []byte, v any) error { + return UnmarshalRead(bytes.NewBuffer(data), v) +} + +func UnmarshalRead(r io.Reader, v any) error { + lr := NewLineReader(r) + unmarshalers := UnmarshalerWithLocation[ObjectLocation](lr) + return json.UnmarshalRead(lr, v, json.WithUnmarshalers(unmarshalers)) +} + +// Location is wrap of types.Location. +// This struct is required when you need to detect location of your object from json file. +type Location types.Location + +func (l *Location) SetLocation(location types.Location) { + *l = Location(location) +} + +// ObjectLocation is required when you need to save Location for your struct. +type ObjectLocation interface { + SetLocation(location types.Location) +} + +type DecodeHook struct { + After func(dec *jsontext.Decoder, target any, loc types.Location) +} + +var SetLocationHook = DecodeHook{ + After: func(_ *jsontext.Decoder, target any, location types.Location) { + if loc, ok := target.(ObjectLocation); ok { + loc.SetLocation(location) + } + }, +} + +// UnmarshalerWithLocation returns a [json.Unmarshalers] that captures the source code location +// (start and end lines) of each decoded object and optionally invokes hooks after decoding. +// +// By default, the [SetLocationHook] is used to record source code locations, +// unless other hooks are explicitly provided. +// +// To use UnmarshalerWithLocation for primitive types, you must implement the [json.UnmarshalerFrom] interface for those objects. +// cf. https://pkg.go.dev/github.com/go-json-experiment/json#UnmarshalerFrom +func UnmarshalerWithLocation[T any](r *lineReader, hooks ...DecodeHook) *json.Unmarshalers { + visited := set.New[int]() + if len(hooks) == 0 { + hooks = []DecodeHook{SetLocationHook} + } + return unmarshaler[T](r, visited, hooks...) +} + +func unmarshaler[T any](r *lineReader, visited set.Set[int], hooks ...DecodeHook) *json.Unmarshalers { + return json.UnmarshalFromFunc(func(dec *jsontext.Decoder, target T) error { + // Decoder.InputOffset reports the offset after the last token, + // but we want to record the offset before the next token. + // + // Call Decoder.PeekKind to buffer enough to reach the next token. + // Add the number of leading whitespace, commas, and colons + // to locate the start of the next token. + // cf. https://pkg.go.dev/github.com/go-json-experiment/json@v0.0.0-20250223041408-d3c622f1b874#example-WithUnmarshalers-RecordOffsets + kind := dec.PeekKind() + + unread := bytes.TrimLeft(dec.UnreadBuffer(), " \n\r\t,:") + start := r.Line() - bytes.Count(unread, []byte("\n")) // The decoder buffer may have read more lines. + + // Check visited set to avoid infinity loops + if visited.Contains(start) { + return json.SkipFunc + } + visited.Append(start) + + // Return more detailed error for cases when UnmarshalJSONFrom is not implemented for primitive type. + if _, ok := any(target).(json.UnmarshalerFrom); !ok && kind != '[' && kind != '{' { + return xerrors.Errorf("structures with single primitive type should implement UnmarshalJSONFrom: %T", target) + } + + if err := json.UnmarshalDecode(dec, target, json.WithUnmarshalers(unmarshaler[T](r, visited, hooks...))); err != nil { + return err + } + + location := types.Location{ + StartLine: start, + EndLine: r.Line() - bytes.Count(dec.UnreadBuffer(), []byte("\n")), + } + + for _, h := range hooks { + if h.After != nil { + h.After(dec, target, location) + } + } + + return nil + }) +} diff --git a/pkg/x/json/json_test.go b/pkg/x/json/json_test.go new file mode 100644 index 000000000000..a6ddb3e03865 --- /dev/null +++ b/pkg/x/json/json_test.go @@ -0,0 +1,149 @@ +package json_test + +import ( + "encoding/json/jsontext" + "encoding/json/v2" + "testing" + + "github.com/stretchr/testify/require" + + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +// See npm.LockFile +type nestedStruct struct { + Dependencies map[string]Dependency `json:"dependencies"` +} + +type Dependency struct { + Version string `json:"version"` + Dependencies map[string]Dependency `json:"dependencies"` + xjson.Location +} + +type stringWithLocation struct { + Requires Requires `json:"requires"` +} + +type Requires []Require + +type Require struct { + Dependency string + xjson.Location +} + +func (r *Require) UnmarshalJSONFrom(dec *jsontext.Decoder) error { + return json.UnmarshalDecode(dec, &r.Dependency) +} + +type stringsWithoutUnmarshalerFrom struct { + Strings []StringWithoutUnmarshalerFrom `json:"strings"` +} + +type StringWithoutUnmarshalerFrom struct { + String string + xjson.Location +} + +func TestUnmarshal(t *testing.T) { + tests := []struct { + name string + in []byte + out any + want any + wantErr string + }{ + { + name: "nested LocationObjects", + in: []byte(`{ + "dependencies": { + "body-parser": { + "version": "1.18.3", + "dependencies": { + "debug": { + "version": "2.6.9" + } + } + } + } +}`), + out: nestedStruct{}, + want: nestedStruct{ + Dependencies: map[string]Dependency{ + "body-parser": { + Version: "1.18.3", + Location: xjson.Location{ + StartLine: 3, + EndLine: 10, + }, + Dependencies: map[string]Dependency{ + // UnmarshalerWithObjectLocation doesn't support Location for nested objects + "debug": { + Version: "2.6.9", + Location: xjson.Location{ + StartLine: 6, + EndLine: 8, + }, + }, + }, + }, + }, + }, + }, + { + name: "Location for only string", + in: []byte(`{ + "version": "0.5", + "requires": [ + "sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488", + "matrix/1.3#905c3f0babc520684c84127378fefdd0%1675278900.0103245" + ] +}`), + out: stringWithLocation{}, + want: stringWithLocation{ + Requires: []Require{ + { + Dependency: "sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488", + Location: xjson.Location{ + StartLine: 4, + EndLine: 4, + }, + }, + { + Dependency: "matrix/1.3#905c3f0babc520684c84127378fefdd0%1675278900.0103245", + Location: xjson.Location{ + StartLine: 5, + EndLine: 5, + }, + }, + }, + }, + }, + { + name: "String object without UnmarshalerFrom implementation", + in: []byte(`{ + "strings": [ + "sound32/1.0#83d4b7bf607b3b60a6546f8b58b5cdd7%1675278904.0791488", + "matrix/1.3#905c3f0babc520684c84127378fefdd0%1675278900.0103245" + ] +}`), + out: stringsWithoutUnmarshalerFrom{}, + wantErr: "structures with single primitive type should implement UnmarshalJSONFrom", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := xjson.Unmarshal(tt.in, &tt.out) + + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + return + } + + require.NoError(t, err) + require.Equal(t, tt.want, tt.out) + }) + } + +} diff --git a/pkg/x/json/jsonc.go b/pkg/x/json/jsonc.go new file mode 100644 index 000000000000..764b546538ca --- /dev/null +++ b/pkg/x/json/jsonc.go @@ -0,0 +1,248 @@ +package json + +import ( + "bytes" + "errors" + "io" +) + +// TokenType represents the type of token being processed +type TokenType int + +const ( + TokenNormal TokenType = iota + TokenString + TokenSingleLineComment + TokenMultiLineComment +) + +// jsoncParser manages the state and processing of JSONC content +type jsoncParser struct { + reader *bytes.Reader // Source reader + dst []byte // Destination buffer + pos int // Current position in destination + tokenType TokenType // Current token type being processed + escaped bool // Whether the previous character was an escape character + lastChar byte // Last processed character +} + +// ToRFC8259 converts JSONC (JSON with Comments) to valid JSON following RFC8259. +// It strips out comments and trailing commas while maintaining the exact character +// offsets as the input. This ensures that any JSON parser locations will map +// directly back to the original source file positions. +// +// Both line numbers and character positions are preserved in the output. +// Comments and trailing commas are replaced with spaces without changing line counts. +// +// Comments can be either: +// - Single-line: starting with // and continuing to the end of the line +// - Multi-line: starting with /* and ending with */ +// +// Trailing commas are allowed in JSONC but not in standard JSON, so they are replaced +// with spaces to maintain character offsets. +func ToRFC8259(src []byte) []byte { + dst := make([]byte, len(src)) + copy(dst, src) // Copy input to maintain same length and offsets + + parser := newJSONCParser(src, dst) + parser.process() + + return dst +} + +// UnmarshalJSONC parses JSONC (JSON with Comments) data into the specified value. +// It first converts JSONC to standard JSON following RFC8259 and then unmarshals it. +// This is a convenience function that combines ToRFC8259 and Unmarshal. +// +// The parser preserves line number information, which is essential for reporting +// errors at their correct locations in the original file. +// +// Usage example: +// +// type Config struct { +// Name string `json:"name"` +// Version string `json:"version"` +// xjson.Location // Embed Location to get line number info +// } +// +// var config Config +// if err := xjson.UnmarshalJSONC(data, &config); err != nil { +// return err +// } +func UnmarshalJSONC(data []byte, v any) error { + jsonData := ToRFC8259(data) + return Unmarshal(jsonData, v) +} + +// newJSONCParser creates a new JSONC parser +func newJSONCParser(src, dst []byte) *jsoncParser { + return &jsoncParser{ + reader: bytes.NewReader(src), + dst: dst, + pos: 0, + tokenType: TokenNormal, + } +} + +// process processes the input JSONC content +func (p *jsoncParser) process() { + for { + b, err := p.reader.ReadByte() + if errors.Is(err, io.EOF) { + break + } else if err != nil { + // Ignore other errors (not expected to occur) + break + } + p.processChar(b) + } +} + +// processChar processes a single character based on current state +func (p *jsoncParser) processChar(b byte) { + switch p.tokenType { + case TokenString: + p.processStringToken(b) + case TokenSingleLineComment: + p.processSingleLineComment(b) + case TokenMultiLineComment: + p.processMultiLineComment(b) + default: + p.processNormalToken(b) + } +} + +// processStringToken processes a character within a string literal +func (p *jsoncParser) processStringToken(b byte) { + switch { + case p.escaped: + p.escaped = false + case b == '\\': + p.escaped = true + case b == '"': + p.tokenType = TokenNormal + } + + p.lastChar = b + p.pos++ +} + +// processSingleLineComment processes a character within a single-line comment +func (p *jsoncParser) processSingleLineComment(b byte) { + if b == '\n' { + // End of single-line comment at newline + p.tokenType = TokenNormal + } else if !isPreservedWhitespace(b) { + // Replace non-whitespace characters with spaces + if p.pos < len(p.dst) { + p.dst[p.pos] = ' ' + } + } + + p.lastChar = b + p.pos++ +} + +// processMultiLineComment processes a character within a multi-line comment +func (p *jsoncParser) processMultiLineComment(b byte) { + if p.lastChar == '*' && b == '/' { + // End of multi-line comment + p.tokenType = TokenNormal + if p.pos < len(p.dst) { + p.dst[p.pos] = ' ' // Replace '/' with space + } + } else if !isPreservedWhitespace(b) { + // Replace non-whitespace with space + if p.pos < len(p.dst) { + p.dst[p.pos] = ' ' + } + } + + p.lastChar = b + p.pos++ +} + +// processNormalToken processes a character outside of string literals and comments +func (p *jsoncParser) processNormalToken(b byte) { + switch b { + case '"': + // Start of string literal + p.tokenType = TokenString + case '/': + // Potential start of comment - look ahead + nextByte, err := p.reader.ReadByte() + if err != nil { + // End of file after '/' character + return + } + + switch nextByte { + case '/': + // Start of single-line comment + p.tokenType = TokenSingleLineComment + if p.pos < len(p.dst) { + p.dst[p.pos] = ' ' // Replace '/' with space + } + if p.pos+1 < len(p.dst) { + p.dst[p.pos+1] = ' ' // Replace second '/' with space + } + p.lastChar = nextByte + p.pos += 2 + return + case '*': + // Start of multi-line comment + p.tokenType = TokenMultiLineComment + if p.pos < len(p.dst) { + p.dst[p.pos] = ' ' // Replace '/' with space + } + if p.pos+1 < len(p.dst) { + p.dst[p.pos+1] = ' ' // Replace '*' with space + } + p.lastChar = nextByte + p.pos += 2 + return + } + + // Not a comment, put the byte back + p.reader.UnreadByte() + case ']', '}': + // Handle trailing comma - look backward + p.handleTrailingComma() + } + p.lastChar = b + p.pos++ +} + +// handleTrailingComma handles the trailing comma by looking backward from the current position +func (p *jsoncParser) handleTrailingComma() { + // Start from one position before the current bracket + startPos := p.pos - 1 + if startPos < 0 { + return + } + + // Find the previous significant (non-whitespace) character + for i := startPos; i >= 0; i-- { + if i >= len(p.dst) { + continue + } + + c := p.dst[i] + switch c { + case ' ', '\t', '\n', '\r': + // Skip whitespace + continue + case ',': + // If it's a comma, replace it with a space + p.dst[i] = ' ' + default: + // Stop after finding the first non-whitespace character + return + } + } +} + +// isPreservedWhitespace returns true for whitespace that should be preserved +func isPreservedWhitespace(c byte) bool { + return c == '\n' || c == '\t' || c == '\r' +} diff --git a/pkg/x/json/jsonc_test.go b/pkg/x/json/jsonc_test.go new file mode 100644 index 000000000000..3a1422ca614d --- /dev/null +++ b/pkg/x/json/jsonc_test.go @@ -0,0 +1,190 @@ +package json_test + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + xjson "github.com/aquasecurity/trivy/pkg/x/json" +) + +func TestToRFC8259(t *testing.T) { + tests := []struct { + name string + input string + want string + }{ + { + name: "no comments", + input: `{"a": 1, "b": 2}`, + want: `{"a": 1, "b": 2}`, + }, + { + name: "single-line comment", + input: "{\n \"a\": 1, // This is a comment\n \"b\": 2\n}", + want: "{\n \"a\": 1, \n \"b\": 2\n}", + }, + { + name: "multi-line comment", + input: "{\n \"a\": 1, /* This is\n a multi-line\n comment */ \"b\": 2\n}", + want: "{\n \"a\": 1, \n \n \"b\": 2\n}", + }, + { + name: "comment with forward slash in string", + input: "{\n \"url\": \"http://example.com\", // Comment\n \"value\": 123\n}", + want: "{\n \"url\": \"http://example.com\", \n \"value\": 123\n}", + }, + { + name: "trailing comma in object", + input: `{"a": 1, "b": 2,}`, + want: `{"a": 1, "b": 2 }`, + }, + { + name: "trailing comma in array", + input: `[1, 2, 3,]`, + want: `[1, 2, 3 ]`, + }, + { + name: "nested trailing commas", + input: `{"a": [1, 2,], "b": {"x": 1, "y": 2,},}`, + want: `{"a": [1, 2 ], "b": {"x": 1, "y": 2 } }`, + }, + { + name: "single-line comment at end of file without newline", + input: `{"a": 1} // Comment`, + want: `{"a": 1} `, + }, + { + name: "multi-line comment at end of file", + input: `{"a": 1} /* Comment */`, + want: `{"a": 1} `, + }, + { + name: "comment within string", + input: `{"text": "This string has // comment syntax"}`, + want: `{"text": "This string has // comment syntax"}`, + }, + { + name: "quoted comment markers", + input: `{"a": "//", "b": "/*", "c": "*/"}`, + want: `{"a": "//", "b": "/*", "c": "*/"}`, + }, + { + name: "escaped quotes in string", + input: `{"text": "String with \"escaped quotes\" // not a comment"}`, + want: `{"text": "String with \"escaped quotes\" // not a comment"}`, + }, + { + name: "complex escaped quotes", + input: `{"text": "String with \\\"double escaped\\\" quotes"}`, + want: `{"text": "String with \\\"double escaped\\\" quotes"}`, + }, + { + name: "real world example", + input: `{ + "name": "my-package", // Package name + "version": "1.0.0", /* Version number */ + "dependencies": { + "lodash": "^4.17.21", + "express": "^4.17.1", // Latest express + }, + "scripts": { + "start": "node index.js", + "test": "jest", + } +}`, + want: `{ + "name": "my-package", + "version": "1.0.0", + "dependencies": { + "lodash": "^4.17.21", + "express": "^4.17.1" + }, + "scripts": { + "start": "node index.js", + "test": "jest" + } +}`, + }, + { + name: "preserves newlines in multiline comments", + input: `{ + "name": "test", // Comment + /* + * Multi-line + * comment + */ + "value": 42 +}`, + want: `{ + "name": "test", + + + + + "value": 42 +}`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test ToRFC8259 (allocates new buffer) + got := xjson.ToRFC8259([]byte(tt.input)) + + // Check length preservation + require.Len(t, got, len(tt.input), "output length should match input length") + + // Check content + assert.Equal(t, tt.want, string(got)) + + // Verify newline count is preserved + inputNewlines := bytes.Count([]byte(tt.input), []byte{'\n'}) + outputNewlines := bytes.Count(got, []byte{'\n'}) + assert.Equal(t, inputNewlines, outputNewlines, "number of newlines should be preserved") + + // Make sure the output is valid JSON + var jsonMap any + err := xjson.Unmarshal(got, &jsonMap) + require.NoError(t, err, "result should be valid JSON") + }) + } +} + +func TestUnmarshalJSONC(t *testing.T) { + jsonc := `{ + "name": "test", // This is a comment + "dependencies": { + "lodash": "^4.17.21", /* Another comment */ + "express": "^4.17.1", // Comment + }, // Trailing comment + /* Multi-line + comment */ + "version": "1.0.0" +}` + + type Config struct { + Name string `json:"name"` + Dependencies map[string]string `json:"dependencies"` + Version string `json:"version"` + xjson.Location + } + + var config Config + err := xjson.UnmarshalJSONC([]byte(jsonc), &config) + require.NoError(t, err) + + // Verify the parsed content + assert.Equal(t, "test", config.Name) + assert.Equal(t, "1.0.0", config.Version) + assert.Equal(t, map[string]string{ + "lodash": "^4.17.21", + "express": "^4.17.1", + }, config.Dependencies) + + // Verify location information + assert.Equal(t, 1, config.StartLine) + assert.Equal(t, 10, config.EndLine) +} diff --git a/pkg/x/os/os.go b/pkg/x/os/os.go new file mode 100644 index 000000000000..269fafa50a51 --- /dev/null +++ b/pkg/x/os/os.go @@ -0,0 +1,88 @@ +package os + +import ( + "fmt" + "os" + "path/filepath" + "sync" + "sync/atomic" + + "golang.org/x/xerrors" + + "github.com/aquasecurity/trivy/pkg/log" +) + +var ( + tempDirOnce = sync.OnceValues(initTempDir) + // initialized tracks whether the temp directory has been created. + // This is used by Cleanup() to avoid creating a directory just to delete it. + initialized atomic.Bool +) + +// initTempDir initializes the process-specific temp directory +func initTempDir() (string, error) { + pid := os.Getpid() + tempDir := filepath.Join(os.TempDir(), fmt.Sprintf("trivy-%d", pid)) + + if err := os.MkdirAll(tempDir, 0o755); err != nil { + return "", xerrors.Errorf("failed to create temp dir: %w", err) + } + + log.Debug("Created process-specific temp directory", log.String("path", tempDir)) + initialized.Store(true) + return tempDir, nil +} + +// CreateTemp creates a temporary file, using the process-specific directory when dir is empty +func CreateTemp(dir, pattern string) (*os.File, error) { + // If dir is empty, use our process-specific temp directory + if dir == "" { + tempDir, err := tempDirOnce() + if err != nil { + return nil, err + } + dir = tempDir + } + + return os.CreateTemp(dir, pattern) //nolint: gocritic +} + +// MkdirTemp creates a temporary directory, using the process-specific directory as base when dir is empty +func MkdirTemp(dir, pattern string) (string, error) { + // If dir is empty, use our process-specific temp directory + if dir == "" { + tempDir, err := tempDirOnce() + if err != nil { + return "", err + } + dir = tempDir + } + + return os.MkdirTemp(dir, pattern) //nolint: gocritic +} + +// TempDir returns the process-specific temp directory path +func TempDir() string { + tempDir, err := tempDirOnce() + if err != nil { + log.Debug("Failed to get process-specific temp directory, falling back to system temp", log.Err(err)) + return os.TempDir() // fallback + } + return tempDir +} + +// Cleanup removes the entire process-specific temp directory +// Note: On Windows, directory deletion may fail if files are still open +func Cleanup() error { + // If temp dir was never initialized, nothing to clean up + if !initialized.Load() { + return nil + } + + tempDir, err := tempDirOnce() + if err != nil || tempDir == "" { + return nil + } + log.Debug("Cleaning up temp directory", log.String("path", tempDir)) + return os.RemoveAll(tempDir) +} diff --git a/pkg/x/os/os_test.go b/pkg/x/os/os_test.go new file mode 100644 index 000000000000..236ba323554d --- /dev/null +++ b/pkg/x/os/os_test.go @@ -0,0 +1,175 @@ +package os + +import ( + "os" + "path/filepath" + "strconv" + "strings" + "sync" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// resetForTest resets global variables for testing +func resetForTest() { + tempDirOnce = sync.OnceValues(initTempDir) +} + +func TestTempDir(t *testing.T) { + resetForTest() + t.Cleanup(func() { + _ = Cleanup() + resetForTest() + }) + + got := TempDir() + + // Should contain process ID + pid := os.Getpid() + want := filepath.Join(os.TempDir(), "trivy-"+strconv.Itoa(pid)) + assert.Equal(t, want, got) + + // Directory should exist + _, err := os.Stat(got) + require.NoError(t, err) +} + +func TestCreateTemp(t *testing.T) { + resetForTest() + + tests := []struct { + name string + pattern string + }{ + { + name: "simple pattern", + pattern: "test-", + }, + { + name: "empty pattern", + pattern: "", + }, + { + name: "pattern with extension", + pattern: "test-*.txt", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test with empty dir (should use process-specific dir) + file, err := CreateTemp("", tt.pattern) //nolint: usetesting + require.NoError(t, err) + t.Cleanup(func() { + _ = file.Close() + _ = Cleanup() + resetForTest() + }) + + // File should exist + _, err = os.Stat(file.Name()) + require.NoError(t, err) + + // File should be in our temp directory + pid := os.Getpid() + expectedDir := filepath.Join(os.TempDir(), "trivy-"+strconv.Itoa(pid)) + assert.True(t, strings.HasPrefix(file.Name(), expectedDir)) + + // Test with specific dir + customDir := t.TempDir() + file2, err := CreateTemp(customDir, tt.pattern) + require.NoError(t, err) + defer file2.Close() + + // File should exist and be in custom dir + _, err = os.Stat(file2.Name()) + require.NoError(t, err) + assert.True(t, strings.HasPrefix(file2.Name(), customDir)) + }) + } +} + +func TestMkdirTemp(t *testing.T) { + resetForTest() + + tests := []struct { + name string + pattern string + }{ + { + name: "simple pattern", + pattern: "test-", + }, + { + name: "empty pattern", + pattern: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Cleanup(func() { + Cleanup() + resetForTest() + }) + + // Test with empty dir (should use process-specific dir) + dir, err := MkdirTemp("", tt.pattern) //nolint:usetesting + require.NoError(t, err) + + // Directory should exist + _, err = os.Stat(dir) + require.NoError(t, err) + + // Directory should be in our temp directory + wantParent := filepath.Join(os.TempDir(), "trivy-"+strconv.Itoa(os.Getpid())) + assert.True(t, strings.HasPrefix(dir, wantParent)) + + // Test with specific dir + customParent := t.TempDir() + dir2, err := MkdirTemp(customParent, tt.pattern) //nolint:usetesting + require.NoError(t, err) + + // Directory should exist and be in custom parent + _, err = os.Stat(dir2) + require.NoError(t, err) + assert.True(t, strings.HasPrefix(dir2, customParent)) + }) + } +} + +func TestCleanup(t *testing.T) { + resetForTest() + t.Cleanup(func() { + resetForTest() + }) + + // Create a temp file + file, err := CreateTemp("", "test-") //nolint: usetesting + require.NoError(t, err) + filename := file.Name() + require.NoError(t, file.Close()) + + // Directory should exist + dir := filepath.Join(os.TempDir(), "trivy-"+strconv.Itoa(os.Getpid())) + _, err = os.Stat(dir) + require.NoError(t, err) + + // File should exist + _, err = os.Stat(filename) + require.NoError(t, err) + + // Cleanup + err = Cleanup() + require.NoError(t, err) + + // File should be gone + _, err = os.Stat(filename) + require.ErrorIs(t, err, os.ErrNotExist) + + // Directory should be gone + _, err = os.Stat(dir) + assert.ErrorIs(t, err, os.ErrNotExist) +} diff --git a/pkg/x/path/path.go b/pkg/x/path/path.go new file mode 100644 index 000000000000..d7c66ecadc8b --- /dev/null +++ b/pkg/x/path/path.go @@ -0,0 +1,12 @@ +package path + +import ( + "slices" + "strings" +) + +// Contains reports whether the path contains the subpath. +func Contains(filePath, subpath string) bool { + ss := strings.Split(filePath, "/") + return slices.Contains(ss, subpath) +} diff --git a/pkg/x/path/path_test.go b/pkg/x/path/path_test.go new file mode 100644 index 000000000000..77d6eea9ecfe --- /dev/null +++ b/pkg/x/path/path_test.go @@ -0,0 +1,50 @@ +package path + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestContains(t *testing.T) { + type args struct { + filePath string + subpath string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "file", + args: args{ + filePath: "go.mod", + subpath: "go.mod", + }, + want: true, + }, + { + name: "dir", + args: args{ + filePath: "app/node_modules/express/package.json", + subpath: "node_modules", + }, + want: true, + }, + { + name: "path", + args: args{ + filePath: "app/node_modules/express/package.json", + subpath: "app/node_modules", + }, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Contains(tt.args.filePath, tt.args.subpath) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/pkg/x/slices/slices.go b/pkg/x/slices/slices.go new file mode 100644 index 000000000000..97ea97455cbe --- /dev/null +++ b/pkg/x/slices/slices.go @@ -0,0 +1,22 @@ +package slices + +// Map manipulates a slice and transforms it to a slice of another type. +// Unlike lo.Map, the transform function doesn't receive an index argument. +func Map[T, U any](collection []T, transform func(item T) U) []U { + if len(collection) == 0 { + return nil + } + result := make([]U, len(collection)) + for i := range collection { + result[i] = transform(collection[i]) + } + return result +} + +// ZeroToNil returns nil, if slice is empty +func ZeroToNil[T any](t []T) []T { + if len(t) == 0 { + return nil + } + return t +} diff --git a/pkg/x/strings/strings.go b/pkg/x/strings/strings.go new file mode 100644 index 000000000000..a5ba7c37d6fd --- /dev/null +++ b/pkg/x/strings/strings.go @@ -0,0 +1,36 @@ +package strings + +import ( + "fmt" + + xslices "github.com/aquasecurity/trivy/pkg/x/slices" +) + +type String interface { + ~string +} + +func ToStringSlice[T any](ss []T) []string { + if len(ss) == 0 { + return nil + } + return xslices.Map(ss, func(s T) string { + switch v := any(s).(type) { + case string: + return v + case fmt.Stringer: + return v.String() + default: + return fmt.Sprint(v) + } + }) +} + +func ToTSlice[T String](ss []string) []T { + if ss == nil { + return nil + } + return xslices.Map(ss, func(s string) T { + return T(s) + }) +} diff --git a/pkg/x/sync/sync.go b/pkg/x/sync/sync.go new file mode 100644 index 000000000000..ad833fe062cf --- /dev/null +++ b/pkg/x/sync/sync.go @@ -0,0 +1,46 @@ +package sync + +import "sync" + +type Map[K comparable, V any] struct { + m sync.Map +} + +func (m *Map[K, V]) Delete(key K) { m.m.Delete(key) } + +func (m *Map[K, V]) Load(key K) (value V, ok bool) { + v, ok := m.m.Load(key) + if !ok { + return value, ok + } + return v.(V), ok +} + +func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + v, loaded := m.m.LoadAndDelete(key) + if !loaded { + return value, loaded + } + return v.(V), loaded +} + +func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + a, loaded := m.m.LoadOrStore(key, value) + return a.(V), loaded +} + +func (m *Map[K, V]) Range(f func(key K, value V) bool) { + m.m.Range(func(key, value any) bool { return f(key.(K), value.(V)) }) +} + +func (m *Map[K, V]) Store(key K, value V) { m.m.Store(key, value) } + +// Len returns the length of the map +func (m *Map[K, V]) Len() int { + var i int + m.m.Range(func(_, _ any) bool { + i++ + return true + }) + return i +} diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 000000000000..2d6aaecd71dd --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "release-type": "go", + "include-component-in-tag": false, + "bump-minor-pre-major": true, + "skip-github-release": true, + "pull-request-title-pattern": "release: v${version} [${branch}]", + "packages": { + ".": {} + } +} diff --git a/rpc/cache/service.pb.go b/rpc/cache/service.pb.go new file mode 100644 index 000000000000..471ba4f101d4 --- /dev/null +++ b/rpc/cache/service.pb.go @@ -0,0 +1,934 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.0 +// protoc (unknown) +// source: rpc/cache/service.proto + +package cache + +import ( + common "github.com/aquasecurity/trivy/rpc/common" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ArtifactInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SchemaVersion int32 `protobuf:"varint,1,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` + Architecture string `protobuf:"bytes,2,opt,name=architecture,proto3" json:"architecture,omitempty"` + Created *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=created,proto3" json:"created,omitempty"` + DockerVersion string `protobuf:"bytes,4,opt,name=docker_version,json=dockerVersion,proto3" json:"docker_version,omitempty"` + Os string `protobuf:"bytes,5,opt,name=os,proto3" json:"os,omitempty"` + HistoryPackages []*common.Package `protobuf:"bytes,6,rep,name=history_packages,json=historyPackages,proto3" json:"history_packages,omitempty"` + Secret *common.Secret `protobuf:"bytes,7,opt,name=secret,proto3" json:"secret,omitempty"` +} + +func (x *ArtifactInfo) Reset() { + *x = ArtifactInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_cache_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ArtifactInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ArtifactInfo) ProtoMessage() {} + +func (x *ArtifactInfo) ProtoReflect() protoreflect.Message { + mi := &file_rpc_cache_service_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ArtifactInfo.ProtoReflect.Descriptor instead. +func (*ArtifactInfo) Descriptor() ([]byte, []int) { + return file_rpc_cache_service_proto_rawDescGZIP(), []int{0} +} + +func (x *ArtifactInfo) GetSchemaVersion() int32 { + if x != nil { + return x.SchemaVersion + } + return 0 +} + +func (x *ArtifactInfo) GetArchitecture() string { + if x != nil { + return x.Architecture + } + return "" +} + +func (x *ArtifactInfo) GetCreated() *timestamppb.Timestamp { + if x != nil { + return x.Created + } + return nil +} + +func (x *ArtifactInfo) GetDockerVersion() string { + if x != nil { + return x.DockerVersion + } + return "" +} + +func (x *ArtifactInfo) GetOs() string { + if x != nil { + return x.Os + } + return "" +} + +func (x *ArtifactInfo) GetHistoryPackages() []*common.Package { + if x != nil { + return x.HistoryPackages + } + return nil +} + +func (x *ArtifactInfo) GetSecret() *common.Secret { + if x != nil { + return x.Secret + } + return nil +} + +type PutArtifactRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ArtifactId string `protobuf:"bytes,1,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + ArtifactInfo *ArtifactInfo `protobuf:"bytes,2,opt,name=artifact_info,json=artifactInfo,proto3" json:"artifact_info,omitempty"` +} + +func (x *PutArtifactRequest) Reset() { + *x = PutArtifactRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_cache_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PutArtifactRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutArtifactRequest) ProtoMessage() {} + +func (x *PutArtifactRequest) ProtoReflect() protoreflect.Message { + mi := &file_rpc_cache_service_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutArtifactRequest.ProtoReflect.Descriptor instead. +func (*PutArtifactRequest) Descriptor() ([]byte, []int) { + return file_rpc_cache_service_proto_rawDescGZIP(), []int{1} +} + +func (x *PutArtifactRequest) GetArtifactId() string { + if x != nil { + return x.ArtifactId + } + return "" +} + +func (x *PutArtifactRequest) GetArtifactInfo() *ArtifactInfo { + if x != nil { + return x.ArtifactInfo + } + return nil +} + +type BlobInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + SchemaVersion int32 `protobuf:"varint,1,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` + Os *common.OS `protobuf:"bytes,2,opt,name=os,proto3" json:"os,omitempty"` + Repository *common.Repository `protobuf:"bytes,11,opt,name=repository,proto3" json:"repository,omitempty"` + PackageInfos []*common.PackageInfo `protobuf:"bytes,3,rep,name=package_infos,json=packageInfos,proto3" json:"package_infos,omitempty"` + Applications []*common.Application `protobuf:"bytes,4,rep,name=applications,proto3" json:"applications,omitempty"` + Misconfigurations []*common.Misconfiguration `protobuf:"bytes,9,rep,name=misconfigurations,proto3" json:"misconfigurations,omitempty"` + OpaqueDirs []string `protobuf:"bytes,5,rep,name=opaque_dirs,json=opaqueDirs,proto3" json:"opaque_dirs,omitempty"` + WhiteoutFiles []string `protobuf:"bytes,6,rep,name=whiteout_files,json=whiteoutFiles,proto3" json:"whiteout_files,omitempty"` + Digest string `protobuf:"bytes,7,opt,name=digest,proto3" json:"digest,omitempty"` + DiffId string `protobuf:"bytes,8,opt,name=diff_id,json=diffId,proto3" json:"diff_id,omitempty"` + CustomResources []*common.CustomResource `protobuf:"bytes,10,rep,name=custom_resources,json=customResources,proto3" json:"custom_resources,omitempty"` + Secrets []*common.Secret `protobuf:"bytes,12,rep,name=secrets,proto3" json:"secrets,omitempty"` + Licenses []*common.LicenseFile `protobuf:"bytes,13,rep,name=licenses,proto3" json:"licenses,omitempty"` + Size int64 `protobuf:"varint,14,opt,name=size,proto3" json:"size,omitempty"` + CreatedBy string `protobuf:"bytes,15,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` + BuildInfo *common.BuildInfo `protobuf:"bytes,16,opt,name=build_info,json=buildInfo,proto3" json:"build_info,omitempty"` +} + +func (x *BlobInfo) Reset() { + *x = BlobInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_cache_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BlobInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlobInfo) ProtoMessage() {} + +func (x *BlobInfo) ProtoReflect() protoreflect.Message { + mi := &file_rpc_cache_service_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlobInfo.ProtoReflect.Descriptor instead. +func (*BlobInfo) Descriptor() ([]byte, []int) { + return file_rpc_cache_service_proto_rawDescGZIP(), []int{2} +} + +func (x *BlobInfo) GetSchemaVersion() int32 { + if x != nil { + return x.SchemaVersion + } + return 0 +} + +func (x *BlobInfo) GetOs() *common.OS { + if x != nil { + return x.Os + } + return nil +} + +func (x *BlobInfo) GetRepository() *common.Repository { + if x != nil { + return x.Repository + } + return nil +} + +func (x *BlobInfo) GetPackageInfos() []*common.PackageInfo { + if x != nil { + return x.PackageInfos + } + return nil +} + +func (x *BlobInfo) GetApplications() []*common.Application { + if x != nil { + return x.Applications + } + return nil +} + +func (x *BlobInfo) GetMisconfigurations() []*common.Misconfiguration { + if x != nil { + return x.Misconfigurations + } + return nil +} + +func (x *BlobInfo) GetOpaqueDirs() []string { + if x != nil { + return x.OpaqueDirs + } + return nil +} + +func (x *BlobInfo) GetWhiteoutFiles() []string { + if x != nil { + return x.WhiteoutFiles + } + return nil +} + +func (x *BlobInfo) GetDigest() string { + if x != nil { + return x.Digest + } + return "" +} + +func (x *BlobInfo) GetDiffId() string { + if x != nil { + return x.DiffId + } + return "" +} + +func (x *BlobInfo) GetCustomResources() []*common.CustomResource { + if x != nil { + return x.CustomResources + } + return nil +} + +func (x *BlobInfo) GetSecrets() []*common.Secret { + if x != nil { + return x.Secrets + } + return nil +} + +func (x *BlobInfo) GetLicenses() []*common.LicenseFile { + if x != nil { + return x.Licenses + } + return nil +} + +func (x *BlobInfo) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *BlobInfo) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +func (x *BlobInfo) GetBuildInfo() *common.BuildInfo { + if x != nil { + return x.BuildInfo + } + return nil +} + +type PutBlobRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DiffId string `protobuf:"bytes,1,opt,name=diff_id,json=diffId,proto3" json:"diff_id,omitempty"` + BlobInfo *BlobInfo `protobuf:"bytes,3,opt,name=blob_info,json=blobInfo,proto3" json:"blob_info,omitempty"` +} + +func (x *PutBlobRequest) Reset() { + *x = PutBlobRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_cache_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PutBlobRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutBlobRequest) ProtoMessage() {} + +func (x *PutBlobRequest) ProtoReflect() protoreflect.Message { + mi := &file_rpc_cache_service_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutBlobRequest.ProtoReflect.Descriptor instead. +func (*PutBlobRequest) Descriptor() ([]byte, []int) { + return file_rpc_cache_service_proto_rawDescGZIP(), []int{3} +} + +func (x *PutBlobRequest) GetDiffId() string { + if x != nil { + return x.DiffId + } + return "" +} + +func (x *PutBlobRequest) GetBlobInfo() *BlobInfo { + if x != nil { + return x.BlobInfo + } + return nil +} + +type PutResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Os *common.OS `protobuf:"bytes,1,opt,name=os,proto3" json:"os,omitempty"` + Eosl bool `protobuf:"varint,2,opt,name=eosl,proto3" json:"eosl,omitempty"` +} + +func (x *PutResponse) Reset() { + *x = PutResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_cache_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PutResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PutResponse) ProtoMessage() {} + +func (x *PutResponse) ProtoReflect() protoreflect.Message { + mi := &file_rpc_cache_service_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PutResponse.ProtoReflect.Descriptor instead. +func (*PutResponse) Descriptor() ([]byte, []int) { + return file_rpc_cache_service_proto_rawDescGZIP(), []int{4} +} + +func (x *PutResponse) GetOs() *common.OS { + if x != nil { + return x.Os + } + return nil +} + +func (x *PutResponse) GetEosl() bool { + if x != nil { + return x.Eosl + } + return false +} + +type MissingBlobsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ArtifactId string `protobuf:"bytes,1,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + BlobIds []string `protobuf:"bytes,2,rep,name=blob_ids,json=blobIds,proto3" json:"blob_ids,omitempty"` +} + +func (x *MissingBlobsRequest) Reset() { + *x = MissingBlobsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_cache_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MissingBlobsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MissingBlobsRequest) ProtoMessage() {} + +func (x *MissingBlobsRequest) ProtoReflect() protoreflect.Message { + mi := &file_rpc_cache_service_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MissingBlobsRequest.ProtoReflect.Descriptor instead. +func (*MissingBlobsRequest) Descriptor() ([]byte, []int) { + return file_rpc_cache_service_proto_rawDescGZIP(), []int{5} +} + +func (x *MissingBlobsRequest) GetArtifactId() string { + if x != nil { + return x.ArtifactId + } + return "" +} + +func (x *MissingBlobsRequest) GetBlobIds() []string { + if x != nil { + return x.BlobIds + } + return nil +} + +type MissingBlobsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + MissingArtifact bool `protobuf:"varint,1,opt,name=missing_artifact,json=missingArtifact,proto3" json:"missing_artifact,omitempty"` + MissingBlobIds []string `protobuf:"bytes,2,rep,name=missing_blob_ids,json=missingBlobIds,proto3" json:"missing_blob_ids,omitempty"` +} + +func (x *MissingBlobsResponse) Reset() { + *x = MissingBlobsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_cache_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MissingBlobsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MissingBlobsResponse) ProtoMessage() {} + +func (x *MissingBlobsResponse) ProtoReflect() protoreflect.Message { + mi := &file_rpc_cache_service_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MissingBlobsResponse.ProtoReflect.Descriptor instead. +func (*MissingBlobsResponse) Descriptor() ([]byte, []int) { + return file_rpc_cache_service_proto_rawDescGZIP(), []int{6} +} + +func (x *MissingBlobsResponse) GetMissingArtifact() bool { + if x != nil { + return x.MissingArtifact + } + return false +} + +func (x *MissingBlobsResponse) GetMissingBlobIds() []string { + if x != nil { + return x.MissingBlobIds + } + return nil +} + +type DeleteBlobsRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + BlobIds []string `protobuf:"bytes,1,rep,name=blob_ids,json=blobIds,proto3" json:"blob_ids,omitempty"` +} + +func (x *DeleteBlobsRequest) Reset() { + *x = DeleteBlobsRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_cache_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DeleteBlobsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteBlobsRequest) ProtoMessage() {} + +func (x *DeleteBlobsRequest) ProtoReflect() protoreflect.Message { + mi := &file_rpc_cache_service_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteBlobsRequest.ProtoReflect.Descriptor instead. +func (*DeleteBlobsRequest) Descriptor() ([]byte, []int) { + return file_rpc_cache_service_proto_rawDescGZIP(), []int{7} +} + +func (x *DeleteBlobsRequest) GetBlobIds() []string { + if x != nil { + return x.BlobIds + } + return nil +} + +var File_rpc_cache_service_proto protoreflect.FileDescriptor + +var file_rpc_cache_service_proto_rawDesc = []byte{ + 0x0a, 0x17, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x61, 0x63, 0x68, 0x65, 0x2f, 0x73, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0e, 0x74, 0x72, 0x69, 0x76, 0x79, + 0x2e, 0x63, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x18, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0xb6, 0x02, 0x0a, 0x0c, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x49, 0x6e, + 0x66, 0x6f, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x22, 0x0a, 0x0c, 0x61, 0x72, 0x63, + 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, 0x34, 0x0a, + 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x5f, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x64, 0x6f, 0x63, + 0x6b, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x73, 0x12, 0x40, 0x0a, 0x10, 0x68, 0x69, + 0x73, 0x74, 0x6f, 0x72, 0x79, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x06, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x0f, 0x68, 0x69, 0x73, + 0x74, 0x6f, 0x72, 0x79, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x06, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, + 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x52, 0x06, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x22, 0x78, 0x0a, 0x12, 0x50, 0x75, + 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x49, + 0x64, 0x12, 0x41, 0x0a, 0x0d, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x69, 0x6e, + 0x66, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, + 0x2e, 0x63, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0c, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, + 0x49, 0x6e, 0x66, 0x6f, 0x22, 0xee, 0x05, 0x0a, 0x08, 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, + 0x6f, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x73, 0x63, 0x68, 0x65, 0x6d, + 0x61, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x20, 0x0a, 0x02, 0x6f, 0x73, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4f, 0x53, 0x52, 0x02, 0x6f, 0x73, 0x12, 0x38, 0x0a, 0x0a, 0x72, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, + 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, + 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x0a, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, + 0x74, 0x6f, 0x72, 0x79, 0x12, 0x3e, 0x0a, 0x0d, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, + 0x69, 0x6e, 0x66, 0x6f, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x72, + 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x61, + 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0c, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x49, + 0x6e, 0x66, 0x6f, 0x73, 0x12, 0x3d, 0x0a, 0x0c, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x72, 0x69, + 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x41, 0x70, 0x70, 0x6c, 0x69, 0x63, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x4c, 0x0a, 0x11, 0x6d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, + 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x69, + 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x11, + 0x6d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x6f, 0x70, 0x61, 0x71, 0x75, 0x65, 0x5f, 0x64, 0x69, 0x72, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x6f, 0x70, 0x61, 0x71, 0x75, 0x65, 0x44, 0x69, + 0x72, 0x73, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x68, 0x69, 0x74, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x66, + 0x69, 0x6c, 0x65, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x77, 0x68, 0x69, 0x74, + 0x65, 0x6f, 0x75, 0x74, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, + 0x65, 0x73, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, + 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x69, 0x66, 0x66, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x66, 0x66, 0x49, 0x64, 0x12, 0x47, 0x0a, 0x10, 0x63, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x0a, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x52, 0x0f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, + 0x63, 0x65, 0x73, 0x12, 0x2e, 0x0a, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x0c, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x07, 0x73, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x73, 0x12, 0x35, 0x0a, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x18, + 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x46, 0x69, 0x6c, 0x65, + 0x52, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, + 0x7a, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x1d, + 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, 0x79, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x12, 0x36, 0x0a, + 0x0a, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x10, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x17, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x09, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x60, 0x0a, 0x0e, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x69, 0x66, 0x66, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x66, 0x66, 0x49, 0x64, + 0x12, 0x35, 0x0a, 0x09, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x61, 0x63, 0x68, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x08, 0x62, + 0x6c, 0x6f, 0x62, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x43, 0x0a, 0x0b, 0x50, 0x75, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x02, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4f, 0x53, 0x52, 0x02, 0x6f, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x65, 0x6f, 0x73, 0x6c, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x65, 0x6f, 0x73, 0x6c, 0x22, 0x51, 0x0a, 0x13, + 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x73, + 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x62, 0x6c, 0x6f, 0x62, 0x49, 0x64, 0x73, 0x22, + 0x6b, 0x0a, 0x14, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x6d, 0x69, 0x73, 0x73, 0x69, + 0x6e, 0x67, 0x5f, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x0f, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x41, 0x72, 0x74, 0x69, 0x66, 0x61, + 0x63, 0x74, 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x5f, 0x62, 0x6c, + 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6e, 0x67, 0x42, 0x6c, 0x6f, 0x62, 0x49, 0x64, 0x73, 0x22, 0x2f, 0x0a, 0x12, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x01, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x62, 0x6c, 0x6f, 0x62, 0x49, 0x64, 0x73, 0x32, 0xbb, 0x02, + 0x0a, 0x05, 0x43, 0x61, 0x63, 0x68, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x50, 0x75, 0x74, 0x41, 0x72, + 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x12, 0x22, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, + 0x61, 0x63, 0x68, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x75, 0x74, 0x41, 0x72, 0x74, 0x69, 0x66, + 0x61, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0x41, 0x0a, 0x07, 0x50, 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x12, 0x1e, 0x2e, + 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, + 0x75, 0x74, 0x42, 0x6c, 0x6f, 0x62, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x59, 0x0a, 0x0c, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, + 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, 0x23, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x61, + 0x63, 0x68, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x42, 0x6c, + 0x6f, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x74, 0x72, 0x69, + 0x76, 0x79, 0x2e, 0x63, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x69, 0x73, 0x73, + 0x69, 0x6e, 0x67, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x49, 0x0a, 0x0b, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x12, + 0x22, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x61, 0x63, 0x68, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x6c, 0x6f, 0x62, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x2f, 0x5a, 0x2d, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x71, 0x75, 0x61, 0x73, 0x65, + 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2f, 0x72, 0x70, 0x63, + 0x2f, 0x63, 0x61, 0x63, 0x68, 0x65, 0x3b, 0x63, 0x61, 0x63, 0x68, 0x65, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_rpc_cache_service_proto_rawDescOnce sync.Once + file_rpc_cache_service_proto_rawDescData = file_rpc_cache_service_proto_rawDesc +) + +func file_rpc_cache_service_proto_rawDescGZIP() []byte { + file_rpc_cache_service_proto_rawDescOnce.Do(func() { + file_rpc_cache_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_rpc_cache_service_proto_rawDescData) + }) + return file_rpc_cache_service_proto_rawDescData +} + +var file_rpc_cache_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_rpc_cache_service_proto_goTypes = []interface{}{ + (*ArtifactInfo)(nil), // 0: trivy.cache.v1.ArtifactInfo + (*PutArtifactRequest)(nil), // 1: trivy.cache.v1.PutArtifactRequest + (*BlobInfo)(nil), // 2: trivy.cache.v1.BlobInfo + (*PutBlobRequest)(nil), // 3: trivy.cache.v1.PutBlobRequest + (*PutResponse)(nil), // 4: trivy.cache.v1.PutResponse + (*MissingBlobsRequest)(nil), // 5: trivy.cache.v1.MissingBlobsRequest + (*MissingBlobsResponse)(nil), // 6: trivy.cache.v1.MissingBlobsResponse + (*DeleteBlobsRequest)(nil), // 7: trivy.cache.v1.DeleteBlobsRequest + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*common.Package)(nil), // 9: trivy.common.Package + (*common.Secret)(nil), // 10: trivy.common.Secret + (*common.OS)(nil), // 11: trivy.common.OS + (*common.Repository)(nil), // 12: trivy.common.Repository + (*common.PackageInfo)(nil), // 13: trivy.common.PackageInfo + (*common.Application)(nil), // 14: trivy.common.Application + (*common.Misconfiguration)(nil), // 15: trivy.common.Misconfiguration + (*common.CustomResource)(nil), // 16: trivy.common.CustomResource + (*common.LicenseFile)(nil), // 17: trivy.common.LicenseFile + (*common.BuildInfo)(nil), // 18: trivy.common.BuildInfo + (*emptypb.Empty)(nil), // 19: google.protobuf.Empty +} +var file_rpc_cache_service_proto_depIdxs = []int32{ + 8, // 0: trivy.cache.v1.ArtifactInfo.created:type_name -> google.protobuf.Timestamp + 9, // 1: trivy.cache.v1.ArtifactInfo.history_packages:type_name -> trivy.common.Package + 10, // 2: trivy.cache.v1.ArtifactInfo.secret:type_name -> trivy.common.Secret + 0, // 3: trivy.cache.v1.PutArtifactRequest.artifact_info:type_name -> trivy.cache.v1.ArtifactInfo + 11, // 4: trivy.cache.v1.BlobInfo.os:type_name -> trivy.common.OS + 12, // 5: trivy.cache.v1.BlobInfo.repository:type_name -> trivy.common.Repository + 13, // 6: trivy.cache.v1.BlobInfo.package_infos:type_name -> trivy.common.PackageInfo + 14, // 7: trivy.cache.v1.BlobInfo.applications:type_name -> trivy.common.Application + 15, // 8: trivy.cache.v1.BlobInfo.misconfigurations:type_name -> trivy.common.Misconfiguration + 16, // 9: trivy.cache.v1.BlobInfo.custom_resources:type_name -> trivy.common.CustomResource + 10, // 10: trivy.cache.v1.BlobInfo.secrets:type_name -> trivy.common.Secret + 17, // 11: trivy.cache.v1.BlobInfo.licenses:type_name -> trivy.common.LicenseFile + 18, // 12: trivy.cache.v1.BlobInfo.build_info:type_name -> trivy.common.BuildInfo + 2, // 13: trivy.cache.v1.PutBlobRequest.blob_info:type_name -> trivy.cache.v1.BlobInfo + 11, // 14: trivy.cache.v1.PutResponse.os:type_name -> trivy.common.OS + 1, // 15: trivy.cache.v1.Cache.PutArtifact:input_type -> trivy.cache.v1.PutArtifactRequest + 3, // 16: trivy.cache.v1.Cache.PutBlob:input_type -> trivy.cache.v1.PutBlobRequest + 5, // 17: trivy.cache.v1.Cache.MissingBlobs:input_type -> trivy.cache.v1.MissingBlobsRequest + 7, // 18: trivy.cache.v1.Cache.DeleteBlobs:input_type -> trivy.cache.v1.DeleteBlobsRequest + 19, // 19: trivy.cache.v1.Cache.PutArtifact:output_type -> google.protobuf.Empty + 19, // 20: trivy.cache.v1.Cache.PutBlob:output_type -> google.protobuf.Empty + 6, // 21: trivy.cache.v1.Cache.MissingBlobs:output_type -> trivy.cache.v1.MissingBlobsResponse + 19, // 22: trivy.cache.v1.Cache.DeleteBlobs:output_type -> google.protobuf.Empty + 19, // [19:23] is the sub-list for method output_type + 15, // [15:19] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name +} + +func init() { file_rpc_cache_service_proto_init() } +func file_rpc_cache_service_proto_init() { + if File_rpc_cache_service_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_rpc_cache_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ArtifactInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_cache_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PutArtifactRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_cache_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BlobInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_cache_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PutBlobRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_cache_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PutResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_cache_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MissingBlobsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_cache_service_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MissingBlobsResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_cache_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DeleteBlobsRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_rpc_cache_service_proto_rawDesc, + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_rpc_cache_service_proto_goTypes, + DependencyIndexes: file_rpc_cache_service_proto_depIdxs, + MessageInfos: file_rpc_cache_service_proto_msgTypes, + }.Build() + File_rpc_cache_service_proto = out.File + file_rpc_cache_service_proto_rawDesc = nil + file_rpc_cache_service_proto_goTypes = nil + file_rpc_cache_service_proto_depIdxs = nil +} diff --git a/rpc/cache/service.proto b/rpc/cache/service.proto new file mode 100644 index 000000000000..41067a39aac5 --- /dev/null +++ b/rpc/cache/service.proto @@ -0,0 +1,74 @@ +syntax = "proto3"; + +package trivy.cache.v1; + +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "rpc/common/service.proto"; + +option go_package = "github.com/aquasecurity/trivy/rpc/cache;cache"; + +service Cache { + rpc PutArtifact(PutArtifactRequest) returns (google.protobuf.Empty); + rpc PutBlob(PutBlobRequest) returns (google.protobuf.Empty); + rpc MissingBlobs(MissingBlobsRequest) returns (MissingBlobsResponse); + rpc DeleteBlobs(DeleteBlobsRequest) returns (google.protobuf.Empty); +} + +message ArtifactInfo { + int32 schema_version = 1; + string architecture = 2; + google.protobuf.Timestamp created = 3; + string docker_version = 4; + string os = 5; + repeated common.Package history_packages = 6; + common.Secret secret = 7; +} + +message PutArtifactRequest { + string artifact_id = 1; + ArtifactInfo artifact_info = 2; +} + +message BlobInfo { + int32 schema_version = 1; + common.OS os = 2; + common.Repository repository = 11; + repeated common.PackageInfo package_infos = 3; + repeated common.Application applications = 4; + repeated common.Misconfiguration misconfigurations = 9; + repeated string opaque_dirs = 5; + repeated string whiteout_files = 6; + string digest = 7; + string diff_id = 8; + repeated common.CustomResource custom_resources = 10; + repeated common.Secret secrets = 12; + repeated common.LicenseFile licenses = 13; + int64 size = 14; + string created_by = 15; + common.BuildInfo build_info = 16; +} + +message PutBlobRequest { + string diff_id = 1; + BlobInfo blob_info = 3; +} + +message PutResponse { + common.OS os = 1; + bool eosl = 2; +} + +message MissingBlobsRequest { + string artifact_id = 1; + repeated string blob_ids = 2; +} + +message MissingBlobsResponse { + bool missing_artifact = 1; + repeated string missing_blob_ids = 2; +} + +message DeleteBlobsRequest { + repeated string blob_ids = 1; +} diff --git a/rpc/cache/service.twirp.go b/rpc/cache/service.twirp.go new file mode 100644 index 000000000000..396db062519f --- /dev/null +++ b/rpc/cache/service.twirp.go @@ -0,0 +1,1986 @@ +// Code generated by protoc-gen-twirp v8.1.3, DO NOT EDIT. +// source: rpc/cache/service.proto + +package cache + +import context "context" +import fmt "fmt" +import http "net/http" +import io "io" +import json "encoding/json" +import strconv "strconv" +import strings "strings" + +import protojson "google.golang.org/protobuf/encoding/protojson" +import proto "google.golang.org/protobuf/proto" +import twirp "github.com/twitchtv/twirp" +import ctxsetters "github.com/twitchtv/twirp/ctxsetters" + +import google_protobuf "google.golang.org/protobuf/types/known/emptypb" + +import bytes "bytes" +import errors "errors" +import path "path" +import url "net/url" + +// Version compatibility assertion. +// If the constant is not defined in the package, that likely means +// the package needs to be updated to work with this generated code. +// See https://twitchtv.github.io/twirp/docs/version_matrix.html +const _ = twirp.TwirpPackageMinVersion_8_1_0 + +// =============== +// Cache Interface +// =============== + +type Cache interface { + PutArtifact(context.Context, *PutArtifactRequest) (*google_protobuf.Empty, error) + + PutBlob(context.Context, *PutBlobRequest) (*google_protobuf.Empty, error) + + MissingBlobs(context.Context, *MissingBlobsRequest) (*MissingBlobsResponse, error) + + DeleteBlobs(context.Context, *DeleteBlobsRequest) (*google_protobuf.Empty, error) +} + +// ===================== +// Cache Protobuf Client +// ===================== + +type cacheProtobufClient struct { + client HTTPClient + urls [4]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewCacheProtobufClient creates a Protobuf client that implements the Cache interface. +// It communicates using Protobuf and can be configured with a custom HTTPClient. +func NewCacheProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Cache { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "trivy.cache.v1", "Cache") + urls := [4]string{ + serviceURL + "PutArtifact", + serviceURL + "PutBlob", + serviceURL + "MissingBlobs", + serviceURL + "DeleteBlobs", + } + + return &cacheProtobufClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *cacheProtobufClient) PutArtifact(ctx context.Context, in *PutArtifactRequest) (*google_protobuf.Empty, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.cache.v1") + ctx = ctxsetters.WithServiceName(ctx, "Cache") + ctx = ctxsetters.WithMethodName(ctx, "PutArtifact") + caller := c.callPutArtifact + if c.interceptor != nil { + caller = func(ctx context.Context, req *PutArtifactRequest) (*google_protobuf.Empty, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*PutArtifactRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*PutArtifactRequest) when calling interceptor") + } + return c.callPutArtifact(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *cacheProtobufClient) callPutArtifact(ctx context.Context, in *PutArtifactRequest) (*google_protobuf.Empty, error) { + out := new(google_protobuf.Empty) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *cacheProtobufClient) PutBlob(ctx context.Context, in *PutBlobRequest) (*google_protobuf.Empty, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.cache.v1") + ctx = ctxsetters.WithServiceName(ctx, "Cache") + ctx = ctxsetters.WithMethodName(ctx, "PutBlob") + caller := c.callPutBlob + if c.interceptor != nil { + caller = func(ctx context.Context, req *PutBlobRequest) (*google_protobuf.Empty, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*PutBlobRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*PutBlobRequest) when calling interceptor") + } + return c.callPutBlob(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *cacheProtobufClient) callPutBlob(ctx context.Context, in *PutBlobRequest) (*google_protobuf.Empty, error) { + out := new(google_protobuf.Empty) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[1], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *cacheProtobufClient) MissingBlobs(ctx context.Context, in *MissingBlobsRequest) (*MissingBlobsResponse, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.cache.v1") + ctx = ctxsetters.WithServiceName(ctx, "Cache") + ctx = ctxsetters.WithMethodName(ctx, "MissingBlobs") + caller := c.callMissingBlobs + if c.interceptor != nil { + caller = func(ctx context.Context, req *MissingBlobsRequest) (*MissingBlobsResponse, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*MissingBlobsRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*MissingBlobsRequest) when calling interceptor") + } + return c.callMissingBlobs(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*MissingBlobsResponse) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*MissingBlobsResponse) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *cacheProtobufClient) callMissingBlobs(ctx context.Context, in *MissingBlobsRequest) (*MissingBlobsResponse, error) { + out := new(MissingBlobsResponse) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[2], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *cacheProtobufClient) DeleteBlobs(ctx context.Context, in *DeleteBlobsRequest) (*google_protobuf.Empty, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.cache.v1") + ctx = ctxsetters.WithServiceName(ctx, "Cache") + ctx = ctxsetters.WithMethodName(ctx, "DeleteBlobs") + caller := c.callDeleteBlobs + if c.interceptor != nil { + caller = func(ctx context.Context, req *DeleteBlobsRequest) (*google_protobuf.Empty, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DeleteBlobsRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DeleteBlobsRequest) when calling interceptor") + } + return c.callDeleteBlobs(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *cacheProtobufClient) callDeleteBlobs(ctx context.Context, in *DeleteBlobsRequest) (*google_protobuf.Empty, error) { + out := new(google_protobuf.Empty) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[3], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ================= +// Cache JSON Client +// ================= + +type cacheJSONClient struct { + client HTTPClient + urls [4]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewCacheJSONClient creates a JSON client that implements the Cache interface. +// It communicates using JSON and can be configured with a custom HTTPClient. +func NewCacheJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Cache { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "trivy.cache.v1", "Cache") + urls := [4]string{ + serviceURL + "PutArtifact", + serviceURL + "PutBlob", + serviceURL + "MissingBlobs", + serviceURL + "DeleteBlobs", + } + + return &cacheJSONClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *cacheJSONClient) PutArtifact(ctx context.Context, in *PutArtifactRequest) (*google_protobuf.Empty, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.cache.v1") + ctx = ctxsetters.WithServiceName(ctx, "Cache") + ctx = ctxsetters.WithMethodName(ctx, "PutArtifact") + caller := c.callPutArtifact + if c.interceptor != nil { + caller = func(ctx context.Context, req *PutArtifactRequest) (*google_protobuf.Empty, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*PutArtifactRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*PutArtifactRequest) when calling interceptor") + } + return c.callPutArtifact(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *cacheJSONClient) callPutArtifact(ctx context.Context, in *PutArtifactRequest) (*google_protobuf.Empty, error) { + out := new(google_protobuf.Empty) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *cacheJSONClient) PutBlob(ctx context.Context, in *PutBlobRequest) (*google_protobuf.Empty, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.cache.v1") + ctx = ctxsetters.WithServiceName(ctx, "Cache") + ctx = ctxsetters.WithMethodName(ctx, "PutBlob") + caller := c.callPutBlob + if c.interceptor != nil { + caller = func(ctx context.Context, req *PutBlobRequest) (*google_protobuf.Empty, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*PutBlobRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*PutBlobRequest) when calling interceptor") + } + return c.callPutBlob(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *cacheJSONClient) callPutBlob(ctx context.Context, in *PutBlobRequest) (*google_protobuf.Empty, error) { + out := new(google_protobuf.Empty) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[1], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *cacheJSONClient) MissingBlobs(ctx context.Context, in *MissingBlobsRequest) (*MissingBlobsResponse, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.cache.v1") + ctx = ctxsetters.WithServiceName(ctx, "Cache") + ctx = ctxsetters.WithMethodName(ctx, "MissingBlobs") + caller := c.callMissingBlobs + if c.interceptor != nil { + caller = func(ctx context.Context, req *MissingBlobsRequest) (*MissingBlobsResponse, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*MissingBlobsRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*MissingBlobsRequest) when calling interceptor") + } + return c.callMissingBlobs(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*MissingBlobsResponse) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*MissingBlobsResponse) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *cacheJSONClient) callMissingBlobs(ctx context.Context, in *MissingBlobsRequest) (*MissingBlobsResponse, error) { + out := new(MissingBlobsResponse) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[2], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +func (c *cacheJSONClient) DeleteBlobs(ctx context.Context, in *DeleteBlobsRequest) (*google_protobuf.Empty, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.cache.v1") + ctx = ctxsetters.WithServiceName(ctx, "Cache") + ctx = ctxsetters.WithMethodName(ctx, "DeleteBlobs") + caller := c.callDeleteBlobs + if c.interceptor != nil { + caller = func(ctx context.Context, req *DeleteBlobsRequest) (*google_protobuf.Empty, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DeleteBlobsRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DeleteBlobsRequest) when calling interceptor") + } + return c.callDeleteBlobs(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *cacheJSONClient) callDeleteBlobs(ctx context.Context, in *DeleteBlobsRequest) (*google_protobuf.Empty, error) { + out := new(google_protobuf.Empty) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[3], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ==================== +// Cache Server Handler +// ==================== + +type cacheServer struct { + Cache + interceptor twirp.Interceptor + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response + jsonCamelCase bool // JSON fields are serialized as lowerCamelCase rather than keeping the original proto names +} + +// NewCacheServer builds a TwirpServer that can be used as an http.Handler to handle +// HTTP requests that are routed to the right method in the provided svc implementation. +// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). +func NewCacheServer(svc Cache, opts ...interface{}) TwirpServer { + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + jsonCamelCase := false + _ = serverOpts.ReadOpt("jsonCamelCase", &jsonCamelCase) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + return &cacheServer{ + Cache: svc, + hooks: serverOpts.Hooks, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, + jsonCamelCase: jsonCamelCase, + } +} + +// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func (s *cacheServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { + writeError(ctx, resp, err, s.hooks) +} + +// handleRequestBodyError is used to handle error when the twirp server cannot read request +func (s *cacheServer) handleRequestBodyError(ctx context.Context, resp http.ResponseWriter, msg string, err error) { + if context.Canceled == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.Canceled, "failed to read request: context canceled")) + return + } + if context.DeadlineExceeded == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.DeadlineExceeded, "failed to read request: deadline exceeded")) + return + } + s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) +} + +// CachePathPrefix is a convenience constant that may identify URL paths. +// Should be used with caution, it only matches routes generated by Twirp Go clients, +// with the default "/twirp" prefix and default CamelCase service and method names. +// More info: https://twitchtv.github.io/twirp/docs/routing.html +const CachePathPrefix = "/twirp/trivy.cache.v1.Cache/" + +func (s *cacheServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx = ctxsetters.WithPackageName(ctx, "trivy.cache.v1") + ctx = ctxsetters.WithServiceName(ctx, "Cache") + ctx = ctxsetters.WithResponseWriter(ctx, resp) + + var err error + ctx, err = callRequestReceived(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + if req.Method != "POST" { + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + // Verify path format: []/./ + prefix, pkgService, method := parseTwirpPath(req.URL.Path) + if pkgService != "trivy.cache.v1.Cache" { + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + if prefix != s.pathPrefix { + msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + switch method { + case "PutArtifact": + s.servePutArtifact(ctx, resp, req) + return + case "PutBlob": + s.servePutBlob(ctx, resp, req) + return + case "MissingBlobs": + s.serveMissingBlobs(ctx, resp, req) + return + case "DeleteBlobs": + s.serveDeleteBlobs(ctx, resp, req) + return + default: + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } +} + +func (s *cacheServer) servePutArtifact(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.servePutArtifactJSON(ctx, resp, req) + case "application/protobuf": + s.servePutArtifactProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *cacheServer) servePutArtifactJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "PutArtifact") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(PutArtifactRequest) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.Cache.PutArtifact + if s.interceptor != nil { + handler = func(ctx context.Context, req *PutArtifactRequest) (*google_protobuf.Empty, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*PutArtifactRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*PutArtifactRequest) when calling interceptor") + } + return s.Cache.PutArtifact(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *google_protobuf.Empty + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *google_protobuf.Empty and nil error while calling PutArtifact. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *cacheServer) servePutArtifactProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "PutArtifact") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(PutArtifactRequest) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.Cache.PutArtifact + if s.interceptor != nil { + handler = func(ctx context.Context, req *PutArtifactRequest) (*google_protobuf.Empty, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*PutArtifactRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*PutArtifactRequest) when calling interceptor") + } + return s.Cache.PutArtifact(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *google_protobuf.Empty + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *google_protobuf.Empty and nil error while calling PutArtifact. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *cacheServer) servePutBlob(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.servePutBlobJSON(ctx, resp, req) + case "application/protobuf": + s.servePutBlobProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *cacheServer) servePutBlobJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "PutBlob") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(PutBlobRequest) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.Cache.PutBlob + if s.interceptor != nil { + handler = func(ctx context.Context, req *PutBlobRequest) (*google_protobuf.Empty, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*PutBlobRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*PutBlobRequest) when calling interceptor") + } + return s.Cache.PutBlob(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *google_protobuf.Empty + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *google_protobuf.Empty and nil error while calling PutBlob. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *cacheServer) servePutBlobProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "PutBlob") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(PutBlobRequest) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.Cache.PutBlob + if s.interceptor != nil { + handler = func(ctx context.Context, req *PutBlobRequest) (*google_protobuf.Empty, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*PutBlobRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*PutBlobRequest) when calling interceptor") + } + return s.Cache.PutBlob(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *google_protobuf.Empty + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *google_protobuf.Empty and nil error while calling PutBlob. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *cacheServer) serveMissingBlobs(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveMissingBlobsJSON(ctx, resp, req) + case "application/protobuf": + s.serveMissingBlobsProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *cacheServer) serveMissingBlobsJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "MissingBlobs") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(MissingBlobsRequest) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.Cache.MissingBlobs + if s.interceptor != nil { + handler = func(ctx context.Context, req *MissingBlobsRequest) (*MissingBlobsResponse, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*MissingBlobsRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*MissingBlobsRequest) when calling interceptor") + } + return s.Cache.MissingBlobs(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*MissingBlobsResponse) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*MissingBlobsResponse) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *MissingBlobsResponse + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *MissingBlobsResponse and nil error while calling MissingBlobs. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *cacheServer) serveMissingBlobsProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "MissingBlobs") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(MissingBlobsRequest) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.Cache.MissingBlobs + if s.interceptor != nil { + handler = func(ctx context.Context, req *MissingBlobsRequest) (*MissingBlobsResponse, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*MissingBlobsRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*MissingBlobsRequest) when calling interceptor") + } + return s.Cache.MissingBlobs(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*MissingBlobsResponse) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*MissingBlobsResponse) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *MissingBlobsResponse + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *MissingBlobsResponse and nil error while calling MissingBlobs. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *cacheServer) serveDeleteBlobs(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveDeleteBlobsJSON(ctx, resp, req) + case "application/protobuf": + s.serveDeleteBlobsProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *cacheServer) serveDeleteBlobsJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "DeleteBlobs") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(DeleteBlobsRequest) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.Cache.DeleteBlobs + if s.interceptor != nil { + handler = func(ctx context.Context, req *DeleteBlobsRequest) (*google_protobuf.Empty, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DeleteBlobsRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DeleteBlobsRequest) when calling interceptor") + } + return s.Cache.DeleteBlobs(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *google_protobuf.Empty + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *google_protobuf.Empty and nil error while calling DeleteBlobs. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *cacheServer) serveDeleteBlobsProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "DeleteBlobs") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(DeleteBlobsRequest) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.Cache.DeleteBlobs + if s.interceptor != nil { + handler = func(ctx context.Context, req *DeleteBlobsRequest) (*google_protobuf.Empty, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*DeleteBlobsRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*DeleteBlobsRequest) when calling interceptor") + } + return s.Cache.DeleteBlobs(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*google_protobuf.Empty) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*google_protobuf.Empty) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *google_protobuf.Empty + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *google_protobuf.Empty and nil error while calling DeleteBlobs. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *cacheServer) ServiceDescriptor() ([]byte, int) { + return twirpFileDescriptor0, 0 +} + +func (s *cacheServer) ProtocGenTwirpVersion() string { + return "v8.1.3" +} + +// PathPrefix returns the base service path, in the form: "//./" +// that is everything in a Twirp route except for the . This can be used for routing, +// for example to identify the requests that are targeted to this service in a mux. +func (s *cacheServer) PathPrefix() string { + return baseServicePath(s.pathPrefix, "trivy.cache.v1", "Cache") +} + +// ===== +// Utils +// ===== + +// HTTPClient is the interface used by generated clients to send HTTP requests. +// It is fulfilled by *(net/http).Client, which is sufficient for most users. +// Users can provide their own implementation for special retry policies. +// +// HTTPClient implementations should not follow redirects. Redirects are +// automatically disabled if *(net/http).Client is passed to client +// constructors. See the withoutRedirects function in this file for more +// details. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// TwirpServer is the interface generated server structs will support: they're +// HTTP handlers with additional methods for accessing metadata about the +// service. Those accessors are a low-level API for building reflection tools. +// Most people can think of TwirpServers as just http.Handlers. +type TwirpServer interface { + http.Handler + + // ServiceDescriptor returns gzipped bytes describing the .proto file that + // this service was generated from. Once unzipped, the bytes can be + // unmarshalled as a + // google.golang.org/protobuf/types/descriptorpb.FileDescriptorProto. + // + // The returned integer is the index of this particular service within that + // FileDescriptorProto's 'Service' slice of ServiceDescriptorProtos. This is a + // low-level field, expected to be used for reflection. + ServiceDescriptor() ([]byte, int) + + // ProtocGenTwirpVersion is the semantic version string of the version of + // twirp used to generate this file. + ProtocGenTwirpVersion() string + + // PathPrefix returns the HTTP URL path prefix for all methods handled by this + // service. This can be used with an HTTP mux to route Twirp requests. + // The path prefix is in the form: "//./" + // that is, everything in a Twirp route except for the at the end. + PathPrefix() string +} + +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + +// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). +// Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func WriteError(resp http.ResponseWriter, err error) { + writeError(context.Background(), resp, err, nil) +} + +// writeError writes Twirp errors in the response and triggers hooks. +func writeError(ctx context.Context, resp http.ResponseWriter, err error, hooks *twirp.ServerHooks) { + // Convert to a twirp.Error. Non-twirp errors are converted to internal errors. + var twerr twirp.Error + if !errors.As(err, &twerr) { + twerr = twirp.InternalErrorWith(err) + } + + statusCode := twirp.ServerHTTPStatusFromErrorCode(twerr.Code()) + ctx = ctxsetters.WithStatusCode(ctx, statusCode) + ctx = callError(ctx, hooks, twerr) + + respBody := marshalErrorToJSON(twerr) + + resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON + resp.Header().Set("Content-Length", strconv.Itoa(len(respBody))) + resp.WriteHeader(statusCode) // set HTTP status code and send response + + _, writeErr := resp.Write(respBody) + if writeErr != nil { + // We have three options here. We could log the error, call the Error + // hook, or just silently ignore the error. + // + // Logging is unacceptable because we don't have a user-controlled + // logger; writing out to stderr without permission is too rude. + // + // Calling the Error hook would confuse users: it would mean the Error + // hook got called twice for one request, which is likely to lead to + // duplicated log messages and metrics, no matter how well we document + // the behavior. + // + // Silently ignoring the error is our least-bad option. It's highly + // likely that the connection is broken and the original 'err' says + // so anyway. + _ = writeErr + } + + callResponseSent(ctx, hooks) +} + +// sanitizeBaseURL parses the the baseURL, and adds the "http" scheme if needed. +// If the URL is unparsable, the baseURL is returned unchanged. +func sanitizeBaseURL(baseURL string) string { + u, err := url.Parse(baseURL) + if err != nil { + return baseURL // invalid URL will fail later when making requests + } + if u.Scheme == "" { + u.Scheme = "http" + } + return u.String() +} + +// baseServicePath composes the path prefix for the service (without ). +// e.g.: baseServicePath("/twirp", "my.pkg", "MyService") +// +// returns => "/twirp/my.pkg.MyService/" +// +// e.g.: baseServicePath("", "", "MyService") +// +// returns => "/MyService/" +func baseServicePath(prefix, pkg, service string) string { + fullServiceName := service + if pkg != "" { + fullServiceName = pkg + "." + service + } + return path.Join("/", prefix, fullServiceName) + "/" +} + +// parseTwirpPath extracts path components form a valid Twirp route. +// Expected format: "[]/./" +// e.g.: prefix, pkgService, method := parseTwirpPath("/twirp/pkg.Svc/MakeHat") +func parseTwirpPath(path string) (string, string, string) { + parts := strings.Split(path, "/") + if len(parts) < 2 { + return "", "", "" + } + method := parts[len(parts)-1] + pkgService := parts[len(parts)-2] + prefix := strings.Join(parts[0:len(parts)-2], "/") + return prefix, pkgService, method +} + +// getCustomHTTPReqHeaders retrieves a copy of any headers that are set in +// a context through the twirp.WithHTTPRequestHeaders function. +// If there are no headers set, or if they have the wrong type, nil is returned. +func getCustomHTTPReqHeaders(ctx context.Context) http.Header { + header, ok := twirp.HTTPRequestHeaders(ctx) + if !ok || header == nil { + return nil + } + copied := make(http.Header) + for k, vv := range header { + if vv == nil { + copied[k] = nil + continue + } + copied[k] = make([]string, len(vv)) + copy(copied[k], vv) + } + return copied +} + +// newRequest makes an http.Request from a client, adding common headers. +func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { + req, err := http.NewRequest("POST", url, reqBody) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if customHeader := getCustomHTTPReqHeaders(ctx); customHeader != nil { + req.Header = customHeader + } + req.Header.Set("Accept", contentType) + req.Header.Set("Content-Type", contentType) + req.Header.Set("Twirp-Version", "v8.1.3") + return req, nil +} + +// JSON serialization for errors +type twerrJSON struct { + Code string `json:"code"` + Msg string `json:"msg"` + Meta map[string]string `json:"meta,omitempty"` +} + +// marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body. +// If serialization fails, it will use a descriptive Internal error instead. +func marshalErrorToJSON(twerr twirp.Error) []byte { + // make sure that msg is not too large + msg := twerr.Msg() + if len(msg) > 1e6 { + msg = msg[:1e6] + } + + tj := twerrJSON{ + Code: string(twerr.Code()), + Msg: msg, + Meta: twerr.MetaMap(), + } + + buf, err := json.Marshal(&tj) + if err != nil { + buf = []byte("{\"type\": \"" + twirp.Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback + } + + return buf +} + +// errorFromResponse builds a twirp.Error from a non-200 HTTP response. +// If the response has a valid serialized Twirp error, then it's returned. +// If not, the response status code is used to generate a similar twirp +// error. See twirpErrorFromIntermediary for more info on intermediary errors. +func errorFromResponse(resp *http.Response) twirp.Error { + statusCode := resp.StatusCode + statusText := http.StatusText(statusCode) + + if isHTTPRedirect(statusCode) { + // Unexpected redirect: it must be an error from an intermediary. + // Twirp clients don't follow redirects automatically, Twirp only handles + // POST requests, redirects should only happen on GET and HEAD requests. + location := resp.Header.Get("Location") + msg := fmt.Sprintf("unexpected HTTP status code %d %q received, Location=%q", statusCode, statusText, location) + return twirpErrorFromIntermediary(statusCode, msg, location) + } + + respBodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return wrapInternal(err, "failed to read server error response body") + } + + var tj twerrJSON + dec := json.NewDecoder(bytes.NewReader(respBodyBytes)) + dec.DisallowUnknownFields() + if err := dec.Decode(&tj); err != nil || tj.Code == "" { + // Invalid JSON response; it must be an error from an intermediary. + msg := fmt.Sprintf("Error from intermediary with HTTP status code %d %q", statusCode, statusText) + return twirpErrorFromIntermediary(statusCode, msg, string(respBodyBytes)) + } + + errorCode := twirp.ErrorCode(tj.Code) + if !twirp.IsValidErrorCode(errorCode) { + msg := "invalid type returned from server error response: " + tj.Code + return twirp.InternalError(msg).WithMeta("body", string(respBodyBytes)) + } + + twerr := twirp.NewError(errorCode, tj.Msg) + for k, v := range tj.Meta { + twerr = twerr.WithMeta(k, v) + } + return twerr +} + +// twirpErrorFromIntermediary maps HTTP errors from non-twirp sources to twirp errors. +// The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. +// Returned twirp Errors have some additional metadata for inspection. +func twirpErrorFromIntermediary(status int, msg string, bodyOrLocation string) twirp.Error { + var code twirp.ErrorCode + if isHTTPRedirect(status) { // 3xx + code = twirp.Internal + } else { + switch status { + case 400: // Bad Request + code = twirp.Internal + case 401: // Unauthorized + code = twirp.Unauthenticated + case 403: // Forbidden + code = twirp.PermissionDenied + case 404: // Not Found + code = twirp.BadRoute + case 429: // Too Many Requests + code = twirp.ResourceExhausted + case 502, 503, 504: // Bad Gateway, Service Unavailable, Gateway Timeout + code = twirp.Unavailable + default: // All other codes + code = twirp.Unknown + } + } + + twerr := twirp.NewError(code, msg) + twerr = twerr.WithMeta("http_error_from_intermediary", "true") // to easily know if this error was from intermediary + twerr = twerr.WithMeta("status_code", strconv.Itoa(status)) + if isHTTPRedirect(status) { + twerr = twerr.WithMeta("location", bodyOrLocation) + } else { + twerr = twerr.WithMeta("body", bodyOrLocation) + } + return twerr +} + +func isHTTPRedirect(status int) bool { + return status >= 300 && status <= 399 +} + +// wrapInternal wraps an error with a prefix as an Internal error. +// The original error cause is accessible by github.com/pkg/errors.Cause. +func wrapInternal(err error, prefix string) twirp.Error { + return twirp.InternalErrorWith(&wrappedError{prefix: prefix, cause: err}) +} + +type wrappedError struct { + prefix string + cause error +} + +func (e *wrappedError) Error() string { return e.prefix + ": " + e.cause.Error() } +func (e *wrappedError) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As +func (e *wrappedError) Cause() error { return e.cause } // for github.com/pkg/errors + +// ensurePanicResponses makes sure that rpc methods causing a panic still result in a Twirp Internal +// error response (status 500), and error hooks are properly called with the panic wrapped as an error. +// The panic is re-raised so it can be handled normally with middleware. +func ensurePanicResponses(ctx context.Context, resp http.ResponseWriter, hooks *twirp.ServerHooks) { + if r := recover(); r != nil { + // Wrap the panic as an error so it can be passed to error hooks. + // The original error is accessible from error hooks, but not visible in the response. + err := errFromPanic(r) + twerr := &internalWithCause{msg: "Internal service panic", cause: err} + // Actually write the error + writeError(ctx, resp, twerr, hooks) + // If possible, flush the error to the wire. + f, ok := resp.(http.Flusher) + if ok { + f.Flush() + } + + panic(r) + } +} + +// errFromPanic returns the typed error if the recovered panic is an error, otherwise formats as error. +func errFromPanic(p interface{}) error { + if err, ok := p.(error); ok { + return err + } + return fmt.Errorf("panic: %v", p) +} + +// internalWithCause is a Twirp Internal error wrapping an original error cause, +// but the original error message is not exposed on Msg(). The original error +// can be checked with go1.13+ errors.Is/As, and also by (github.com/pkg/errors).Unwrap +type internalWithCause struct { + msg string + cause error +} + +func (e *internalWithCause) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As +func (e *internalWithCause) Cause() error { return e.cause } // for github.com/pkg/errors +func (e *internalWithCause) Error() string { return e.msg + ": " + e.cause.Error() } +func (e *internalWithCause) Code() twirp.ErrorCode { return twirp.Internal } +func (e *internalWithCause) Msg() string { return e.msg } +func (e *internalWithCause) Meta(key string) string { return "" } +func (e *internalWithCause) MetaMap() map[string]string { return nil } +func (e *internalWithCause) WithMeta(key string, val string) twirp.Error { return e } + +// malformedRequestError is used when the twirp server cannot unmarshal a request +func malformedRequestError(msg string) twirp.Error { + return twirp.NewError(twirp.Malformed, msg) +} + +// badRouteError is used when the twirp server cannot route a request +func badRouteError(msg string, method, url string) twirp.Error { + err := twirp.NewError(twirp.BadRoute, msg) + err = err.WithMeta("twirp_invalid_route", method+" "+url) + return err +} + +// withoutRedirects makes sure that the POST request can not be redirected. +// The standard library will, by default, redirect requests (including POSTs) if it gets a 302 or +// 303 response, and also 301s in go1.8. It redirects by making a second request, changing the +// method to GET and removing the body. This produces very confusing error messages, so instead we +// set a redirect policy that always errors. This stops Go from executing the redirect. +// +// We have to be a little careful in case the user-provided http.Client has its own CheckRedirect +// policy - if so, we'll run through that policy first. +// +// Because this requires modifying the http.Client, we make a new copy of the client and return it. +func withoutRedirects(in *http.Client) *http.Client { + copy := *in + copy.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if in.CheckRedirect != nil { + // Run the input's redirect if it exists, in case it has side effects, but ignore any error it + // returns, since we want to use ErrUseLastResponse. + err := in.CheckRedirect(req, via) + _ = err // Silly, but this makes sure generated code passes errcheck -blank, which some people use. + } + return http.ErrUseLastResponse + } + return © +} + +// doProtobufRequest makes a Protobuf request to the remote Twirp service. +func doProtobufRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { + reqBodyBytes, err := proto.Marshal(in) + if err != nil { + return ctx, wrapInternal(err, "failed to marshal proto request") + } + reqBody := bytes.NewBuffer(reqBodyBytes) + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, reqBody, "application/protobuf") + if err != nil { + return ctx, wrapInternal(err, "could not build request") + } + ctx, err = callClientRequestPrepared(ctx, hooks, req) + if err != nil { + return ctx, err + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return ctx, wrapInternal(err, "failed to do request") + } + defer func() { _ = resp.Body.Close() }() + + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return ctx, errorFromResponse(resp) + } + + respBodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return ctx, wrapInternal(err, "failed to read response body") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if err = proto.Unmarshal(respBodyBytes, out); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal proto response") + } + return ctx, nil +} + +// doJSONRequest makes a JSON request to the remote Twirp service. +func doJSONRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { + marshaler := &protojson.MarshalOptions{UseProtoNames: true} + reqBytes, err := marshaler.Marshal(in) + if err != nil { + return ctx, wrapInternal(err, "failed to marshal json request") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, bytes.NewReader(reqBytes), "application/json") + if err != nil { + return ctx, wrapInternal(err, "could not build request") + } + ctx, err = callClientRequestPrepared(ctx, hooks, req) + if err != nil { + return ctx, err + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return ctx, wrapInternal(err, "failed to do request") + } + + defer func() { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = wrapInternal(cerr, "failed to close response body") + } + }() + + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return ctx, errorFromResponse(resp) + } + + d := json.NewDecoder(resp.Body) + rawRespBody := json.RawMessage{} + if err := d.Decode(&rawRespBody); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal json response") + } + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawRespBody, out); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal json response") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + return ctx, nil +} + +// Call twirp.ServerHooks.RequestReceived if the hook is available +func callRequestReceived(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestReceived == nil { + return ctx, nil + } + return h.RequestReceived(ctx) +} + +// Call twirp.ServerHooks.RequestRouted if the hook is available +func callRequestRouted(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestRouted == nil { + return ctx, nil + } + return h.RequestRouted(ctx) +} + +// Call twirp.ServerHooks.ResponsePrepared if the hook is available +func callResponsePrepared(ctx context.Context, h *twirp.ServerHooks) context.Context { + if h == nil || h.ResponsePrepared == nil { + return ctx + } + return h.ResponsePrepared(ctx) +} + +// Call twirp.ServerHooks.ResponseSent if the hook is available +func callResponseSent(ctx context.Context, h *twirp.ServerHooks) { + if h == nil || h.ResponseSent == nil { + return + } + h.ResponseSent(ctx) +} + +// Call twirp.ServerHooks.Error if the hook is available +func callError(ctx context.Context, h *twirp.ServerHooks, err twirp.Error) context.Context { + if h == nil || h.Error == nil { + return ctx + } + return h.Error(ctx, err) +} + +func callClientResponseReceived(ctx context.Context, h *twirp.ClientHooks) { + if h == nil || h.ResponseReceived == nil { + return + } + h.ResponseReceived(ctx) +} + +func callClientRequestPrepared(ctx context.Context, h *twirp.ClientHooks, req *http.Request) (context.Context, error) { + if h == nil || h.RequestPrepared == nil { + return ctx, nil + } + return h.RequestPrepared(ctx, req) +} + +func callClientError(ctx context.Context, h *twirp.ClientHooks, err twirp.Error) { + if h == nil || h.Error == nil { + return + } + h.Error(ctx, err) +} + +var twirpFileDescriptor0 = []byte{ + // 896 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x56, 0x6d, 0x6f, 0xe3, 0x44, + 0x10, 0x56, 0x92, 0xb6, 0x49, 0x26, 0x2f, 0x0d, 0xcb, 0x71, 0xf5, 0x85, 0xe3, 0x2e, 0x32, 0x20, + 0x05, 0x09, 0x6c, 0x51, 0x38, 0x84, 0x84, 0x40, 0xb4, 0x3d, 0x40, 0x91, 0xee, 0x44, 0xd9, 0x43, + 0x48, 0xf0, 0x25, 0xd8, 0xeb, 0x75, 0xb2, 0xaa, 0xed, 0x75, 0x77, 0xd7, 0x85, 0xf0, 0xc3, 0xf8, + 0xc2, 0x7f, 0xe2, 0x37, 0xa0, 0x5d, 0xaf, 0x93, 0x38, 0xc9, 0x55, 0xdc, 0x97, 0x6a, 0x77, 0xe6, + 0xd9, 0x79, 0x79, 0xe6, 0x19, 0xa7, 0x70, 0x26, 0x72, 0xe2, 0x93, 0x80, 0x2c, 0xa9, 0x2f, 0xa9, + 0xb8, 0x63, 0x84, 0x7a, 0xb9, 0xe0, 0x8a, 0xa3, 0xa1, 0x12, 0xec, 0x6e, 0xe5, 0x19, 0x97, 0x77, + 0xf7, 0xe9, 0xf8, 0xdd, 0x05, 0xe7, 0x8b, 0x84, 0xfa, 0xc6, 0x1b, 0x16, 0xb1, 0x4f, 0xd3, 0x5c, + 0xad, 0x4a, 0xf0, 0xf8, 0xe9, 0xae, 0x53, 0xb1, 0x94, 0x4a, 0x15, 0xa4, 0xb9, 0x05, 0x38, 0x26, + 0x0d, 0x4f, 0x53, 0x9e, 0xd5, 0xf3, 0xb8, 0x7f, 0x37, 0xa1, 0x7f, 0x21, 0x14, 0x8b, 0x03, 0xa2, + 0x66, 0x59, 0xcc, 0xd1, 0x87, 0x30, 0x94, 0x64, 0x49, 0xd3, 0x60, 0x7e, 0x47, 0x85, 0x64, 0x3c, + 0x73, 0x1a, 0x93, 0xc6, 0xf4, 0x18, 0x0f, 0x4a, 0xeb, 0x2f, 0xa5, 0x11, 0xb9, 0xd0, 0x0f, 0x04, + 0x59, 0x32, 0x45, 0x89, 0x2a, 0x04, 0x75, 0x9a, 0x93, 0xc6, 0xb4, 0x8b, 0x6b, 0x36, 0xf4, 0x39, + 0xb4, 0x89, 0xa0, 0x81, 0xa2, 0x91, 0xd3, 0x9a, 0x34, 0xa6, 0xbd, 0xf3, 0xb1, 0x57, 0x16, 0xea, + 0x55, 0x85, 0x7a, 0x3f, 0x57, 0x85, 0xe2, 0x0a, 0xaa, 0x0b, 0x88, 0x38, 0xb9, 0xa1, 0x62, 0x5d, + 0xc0, 0x91, 0x89, 0x3d, 0x28, 0xad, 0x55, 0x01, 0x43, 0x68, 0x72, 0xe9, 0x1c, 0x1b, 0x57, 0x93, + 0x4b, 0xf4, 0x2d, 0x8c, 0x96, 0x4c, 0x2a, 0x2e, 0x56, 0xf3, 0x3c, 0x20, 0x37, 0xc1, 0x82, 0x4a, + 0xe7, 0x64, 0xd2, 0x9a, 0xf6, 0xce, 0xdf, 0xf1, 0x2c, 0x97, 0xa6, 0x7f, 0xef, 0xba, 0xf4, 0xe2, + 0x53, 0x0b, 0xb7, 0x77, 0x89, 0x3e, 0x86, 0x13, 0x49, 0x89, 0xa0, 0xca, 0x69, 0x9b, 0x6a, 0x1f, + 0xd4, 0xdf, 0xbd, 0x32, 0x3e, 0x6c, 0x31, 0xee, 0x9f, 0x80, 0xae, 0x0b, 0x55, 0x51, 0x87, 0xe9, + 0x6d, 0x41, 0xa5, 0x42, 0x4f, 0xa1, 0x17, 0x58, 0xd3, 0x9c, 0x45, 0x86, 0xba, 0x2e, 0x86, 0xca, + 0x34, 0x8b, 0xd0, 0x05, 0x0c, 0x36, 0x80, 0x2c, 0xe6, 0x86, 0xb8, 0xde, 0xf9, 0x63, 0xaf, 0x3e, + 0x6f, 0x6f, 0x7b, 0x26, 0x9a, 0xd6, 0xcd, 0xcd, 0xfd, 0xf7, 0x18, 0x3a, 0x97, 0x09, 0x0f, 0xdf, + 0x64, 0x5c, 0x13, 0xc3, 0x56, 0x99, 0x6b, 0x54, 0xef, 0xeb, 0xc7, 0x57, 0x86, 0xbf, 0x2f, 0x01, + 0x04, 0xcd, 0xb9, 0x64, 0x9a, 0x13, 0xa7, 0x67, 0x90, 0x4e, 0x1d, 0x89, 0xd7, 0x7e, 0xbc, 0x85, + 0x45, 0xdf, 0xc0, 0xc0, 0x32, 0x6e, 0x3a, 0x92, 0x4e, 0xcb, 0xd0, 0xfe, 0xe8, 0x20, 0xed, 0x65, + 0x3f, 0xf9, 0xe6, 0x22, 0xd1, 0xd7, 0xd0, 0x0f, 0xf2, 0x3c, 0x61, 0x24, 0x50, 0x8c, 0x67, 0xd2, + 0x39, 0x3a, 0xf4, 0xfc, 0x62, 0x83, 0xc0, 0x35, 0x38, 0x7a, 0x01, 0x6f, 0xa5, 0x4c, 0x12, 0x9e, + 0xc5, 0x6c, 0x51, 0x08, 0x1b, 0xa3, 0x6b, 0x62, 0x3c, 0xa9, 0xc7, 0x78, 0xb9, 0x03, 0xc3, 0xfb, + 0x0f, 0xf5, 0x00, 0x79, 0x1e, 0xdc, 0x16, 0x74, 0x1e, 0x31, 0xa1, 0xf5, 0xd5, 0xd2, 0x03, 0x2c, + 0x4d, 0xcf, 0x99, 0x90, 0x9a, 0xf0, 0x3f, 0xb4, 0xc4, 0x79, 0xa1, 0xe6, 0x31, 0x4b, 0xac, 0xca, + 0xba, 0x78, 0x50, 0x59, 0xbf, 0xd7, 0x46, 0xf4, 0x10, 0x4e, 0x22, 0xb6, 0xa0, 0xb2, 0x14, 0x53, + 0x17, 0xdb, 0x1b, 0x3a, 0x83, 0x76, 0xc4, 0xe2, 0x58, 0x8b, 0xa3, 0x53, 0x39, 0xe2, 0x78, 0x16, + 0xa1, 0x1f, 0x60, 0x44, 0x0a, 0xa9, 0x78, 0x3a, 0x17, 0x54, 0xf2, 0x42, 0x10, 0x2a, 0x1d, 0x30, + 0x5d, 0x3c, 0xae, 0x77, 0x71, 0x65, 0x50, 0xd8, 0x82, 0xf0, 0x29, 0xa9, 0xdd, 0x25, 0xf2, 0xa0, + 0x5d, 0x4a, 0x54, 0x3a, 0x7d, 0xf3, 0xfe, 0xb0, 0x8e, 0x2b, 0x10, 0x7a, 0x06, 0x9d, 0x84, 0x11, + 0x9a, 0x49, 0x2a, 0x9d, 0xc1, 0x21, 0xea, 0x5f, 0x94, 0x5e, 0xdd, 0x17, 0x5e, 0x43, 0x11, 0x82, + 0x23, 0xc9, 0xfe, 0xa2, 0xce, 0x70, 0xd2, 0x98, 0xb6, 0xb0, 0x39, 0xa3, 0xf7, 0x00, 0xec, 0x16, + 0xcf, 0xc3, 0x95, 0x73, 0x6a, 0xfa, 0xeb, 0x5a, 0xcb, 0xe5, 0x0a, 0x7d, 0x01, 0x10, 0x16, 0x2c, + 0x89, 0x4a, 0xe1, 0x8f, 0x8c, 0xc4, 0xce, 0xea, 0xb9, 0x2e, 0xb5, 0xdf, 0x68, 0xa4, 0x1b, 0x56, + 0x47, 0xf7, 0x77, 0x18, 0x5e, 0x17, 0x4a, 0x4b, 0xbe, 0x5a, 0xb3, 0x2d, 0x16, 0x1b, 0x35, 0x16, + 0x9f, 0x41, 0x37, 0x4c, 0x78, 0x58, 0x66, 0x68, 0xd5, 0x45, 0x5c, 0xad, 0x56, 0xb5, 0x3b, 0xb8, + 0x13, 0xda, 0x93, 0x7b, 0x05, 0xbd, 0xeb, 0x42, 0x61, 0x2a, 0x73, 0x9e, 0x49, 0x6a, 0xb7, 0xa5, + 0x71, 0xcf, 0xb6, 0x20, 0x38, 0xa2, 0x5c, 0x26, 0x66, 0xa3, 0x3a, 0xd8, 0x9c, 0xdd, 0x9f, 0xe0, + 0xed, 0x97, 0x4c, 0x4a, 0x96, 0x2d, 0x74, 0x06, 0xf9, 0xbf, 0x3f, 0x09, 0x8f, 0xa0, 0x53, 0xd6, + 0x1c, 0xe9, 0x0d, 0xd5, 0x5a, 0x6a, 0x9b, 0xc2, 0x22, 0xe9, 0xde, 0xc0, 0x83, 0x7a, 0x48, 0x5b, + 0xe0, 0x47, 0x30, 0x4a, 0x4b, 0xfb, 0xbc, 0x0a, 0x64, 0x02, 0x77, 0xf0, 0xa9, 0xb5, 0x57, 0xdf, + 0x0f, 0x34, 0xdd, 0x40, 0x77, 0xb2, 0x0c, 0xd3, 0x4d, 0x68, 0x9d, 0xcc, 0x07, 0xf4, 0x9c, 0x26, + 0x54, 0xd1, 0x5a, 0xf9, 0xdb, 0xd5, 0x35, 0x6a, 0xd5, 0x9d, 0xff, 0xd3, 0x84, 0xe3, 0x2b, 0xcd, + 0x2a, 0x9a, 0x19, 0xfe, 0xd6, 0x39, 0xdd, 0x5d, 0xca, 0xf7, 0xbf, 0x94, 0xe3, 0x87, 0x7b, 0xbf, + 0x05, 0xdf, 0xe9, 0x5f, 0x34, 0x74, 0x01, 0x6d, 0x3b, 0x6c, 0xf4, 0xe4, 0x40, 0x98, 0x2d, 0x15, + 0xbc, 0x36, 0xc4, 0xaf, 0xd0, 0xdf, 0x66, 0x0d, 0xbd, 0xbf, 0x1b, 0xe7, 0xc0, 0x98, 0xc6, 0x1f, + 0xdc, 0x0f, 0xb2, 0xc4, 0xcf, 0xa0, 0xb7, 0xc5, 0xd1, 0x7e, 0xa3, 0xfb, 0x04, 0xbe, 0xae, 0xca, + 0x4b, 0xff, 0xb7, 0x4f, 0x16, 0x4c, 0x2d, 0x8b, 0x50, 0x4b, 0xcb, 0x0f, 0x6e, 0x8b, 0x40, 0x52, + 0x52, 0x08, 0xa6, 0x56, 0xbe, 0x09, 0xea, 0xaf, 0xff, 0x35, 0xf8, 0xca, 0xfc, 0x0d, 0x4f, 0x4c, + 0x80, 0xcf, 0xfe, 0x0b, 0x00, 0x00, 0xff, 0xff, 0xb5, 0xf0, 0xeb, 0x72, 0x34, 0x08, 0x00, 0x00, +} diff --git a/rpc/common/service.pb.go b/rpc/common/service.pb.go new file mode 100644 index 000000000000..941c591a85bf --- /dev/null +++ b/rpc/common/service.pb.go @@ -0,0 +1,3466 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.0 +// protoc (unknown) +// source: rpc/common/service.proto + +package common + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + structpb "google.golang.org/protobuf/types/known/structpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Severity int32 + +const ( + Severity_UNKNOWN Severity = 0 + Severity_LOW Severity = 1 + Severity_MEDIUM Severity = 2 + Severity_HIGH Severity = 3 + Severity_CRITICAL Severity = 4 +) + +// Enum value maps for Severity. +var ( + Severity_name = map[int32]string{ + 0: "UNKNOWN", + 1: "LOW", + 2: "MEDIUM", + 3: "HIGH", + 4: "CRITICAL", + } + Severity_value = map[string]int32{ + "UNKNOWN": 0, + "LOW": 1, + "MEDIUM": 2, + "HIGH": 3, + "CRITICAL": 4, + } +) + +func (x Severity) Enum() *Severity { + p := new(Severity) + *p = x + return p +} + +func (x Severity) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Severity) Descriptor() protoreflect.EnumDescriptor { + return file_rpc_common_service_proto_enumTypes[0].Descriptor() +} + +func (Severity) Type() protoreflect.EnumType { + return &file_rpc_common_service_proto_enumTypes[0] +} + +func (x Severity) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Severity.Descriptor instead. +func (Severity) EnumDescriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{0} +} + +type LicenseCategory_Enum int32 + +const ( + LicenseCategory_UNSPECIFIED LicenseCategory_Enum = 0 + LicenseCategory_FORBIDDEN LicenseCategory_Enum = 1 + LicenseCategory_RESTRICTED LicenseCategory_Enum = 2 + LicenseCategory_RECIPROCAL LicenseCategory_Enum = 3 + LicenseCategory_NOTICE LicenseCategory_Enum = 4 + LicenseCategory_PERMISSIVE LicenseCategory_Enum = 5 + LicenseCategory_UNENCUMBERED LicenseCategory_Enum = 6 + LicenseCategory_UNKNOWN LicenseCategory_Enum = 7 +) + +// Enum value maps for LicenseCategory_Enum. +var ( + LicenseCategory_Enum_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "FORBIDDEN", + 2: "RESTRICTED", + 3: "RECIPROCAL", + 4: "NOTICE", + 5: "PERMISSIVE", + 6: "UNENCUMBERED", + 7: "UNKNOWN", + } + LicenseCategory_Enum_value = map[string]int32{ + "UNSPECIFIED": 0, + "FORBIDDEN": 1, + "RESTRICTED": 2, + "RECIPROCAL": 3, + "NOTICE": 4, + "PERMISSIVE": 5, + "UNENCUMBERED": 6, + "UNKNOWN": 7, + } +) + +func (x LicenseCategory_Enum) Enum() *LicenseCategory_Enum { + p := new(LicenseCategory_Enum) + *p = x + return p +} + +func (x LicenseCategory_Enum) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (LicenseCategory_Enum) Descriptor() protoreflect.EnumDescriptor { + return file_rpc_common_service_proto_enumTypes[1].Descriptor() +} + +func (LicenseCategory_Enum) Type() protoreflect.EnumType { + return &file_rpc_common_service_proto_enumTypes[1] +} + +func (x LicenseCategory_Enum) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use LicenseCategory_Enum.Descriptor instead. +func (LicenseCategory_Enum) EnumDescriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{26, 0} +} + +type LicenseType_Enum int32 + +const ( + LicenseType_UNSPECIFIED LicenseType_Enum = 0 + LicenseType_DPKG LicenseType_Enum = 1 + LicenseType_HEADER LicenseType_Enum = 2 + LicenseType_LICENSE_FILE LicenseType_Enum = 3 +) + +// Enum value maps for LicenseType_Enum. +var ( + LicenseType_Enum_name = map[int32]string{ + 0: "UNSPECIFIED", + 1: "DPKG", + 2: "HEADER", + 3: "LICENSE_FILE", + } + LicenseType_Enum_value = map[string]int32{ + "UNSPECIFIED": 0, + "DPKG": 1, + "HEADER": 2, + "LICENSE_FILE": 3, + } +) + +func (x LicenseType_Enum) Enum() *LicenseType_Enum { + p := new(LicenseType_Enum) + *p = x + return p +} + +func (x LicenseType_Enum) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (LicenseType_Enum) Descriptor() protoreflect.EnumDescriptor { + return file_rpc_common_service_proto_enumTypes[2].Descriptor() +} + +func (LicenseType_Enum) Type() protoreflect.EnumType { + return &file_rpc_common_service_proto_enumTypes[2] +} + +func (x LicenseType_Enum) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use LicenseType_Enum.Descriptor instead. +func (LicenseType_Enum) EnumDescriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{27, 0} +} + +type OS struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Family string `protobuf:"bytes,1,opt,name=family,proto3" json:"family,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Eosl bool `protobuf:"varint,3,opt,name=eosl,proto3" json:"eosl,omitempty"` + Extended bool `protobuf:"varint,4,opt,name=extended,proto3" json:"extended,omitempty"` +} + +func (x *OS) Reset() { + *x = OS{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OS) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OS) ProtoMessage() {} + +func (x *OS) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OS.ProtoReflect.Descriptor instead. +func (*OS) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{0} +} + +func (x *OS) GetFamily() string { + if x != nil { + return x.Family + } + return "" +} + +func (x *OS) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *OS) GetEosl() bool { + if x != nil { + return x.Eosl + } + return false +} + +func (x *OS) GetExtended() bool { + if x != nil { + return x.Extended + } + return false +} + +type Repository struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Family string `protobuf:"bytes,1,opt,name=family,proto3" json:"family,omitempty"` + Release string `protobuf:"bytes,2,opt,name=release,proto3" json:"release,omitempty"` +} + +func (x *Repository) Reset() { + *x = Repository{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Repository) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Repository) ProtoMessage() {} + +func (x *Repository) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Repository.ProtoReflect.Descriptor instead. +func (*Repository) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{1} +} + +func (x *Repository) GetFamily() string { + if x != nil { + return x.Family + } + return "" +} + +func (x *Repository) GetRelease() string { + if x != nil { + return x.Release + } + return "" +} + +type PackageInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FilePath string `protobuf:"bytes,1,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + Packages []*Package `protobuf:"bytes,2,rep,name=packages,proto3" json:"packages,omitempty"` +} + +func (x *PackageInfo) Reset() { + *x = PackageInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PackageInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PackageInfo) ProtoMessage() {} + +func (x *PackageInfo) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PackageInfo.ProtoReflect.Descriptor instead. +func (*PackageInfo) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{2} +} + +func (x *PackageInfo) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *PackageInfo) GetPackages() []*Package { + if x != nil { + return x.Packages + } + return nil +} + +type Application struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + FilePath string `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + Packages []*Package `protobuf:"bytes,3,rep,name=packages,proto3" json:"packages,omitempty"` +} + +func (x *Application) Reset() { + *x = Application{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Application) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Application) ProtoMessage() {} + +func (x *Application) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Application.ProtoReflect.Descriptor instead. +func (*Application) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{3} +} + +func (x *Application) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Application) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *Application) GetPackages() []*Package { + if x != nil { + return x.Packages + } + return nil +} + +type Package struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // binary package + // e.g. bind-utils + Id string `protobuf:"bytes,13,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + Release string `protobuf:"bytes,3,opt,name=release,proto3" json:"release,omitempty"` + Epoch int32 `protobuf:"varint,4,opt,name=epoch,proto3" json:"epoch,omitempty"` + Identifier *PkgIdentifier `protobuf:"bytes,19,opt,name=identifier,proto3" json:"identifier,omitempty"` + Arch string `protobuf:"bytes,5,opt,name=arch,proto3" json:"arch,omitempty"` + // src package containing some binary packages + // e.g. bind + SrcName string `protobuf:"bytes,6,opt,name=src_name,json=srcName,proto3" json:"src_name,omitempty"` + SrcVersion string `protobuf:"bytes,7,opt,name=src_version,json=srcVersion,proto3" json:"src_version,omitempty"` + SrcRelease string `protobuf:"bytes,8,opt,name=src_release,json=srcRelease,proto3" json:"src_release,omitempty"` + SrcEpoch int32 `protobuf:"varint,9,opt,name=src_epoch,json=srcEpoch,proto3" json:"src_epoch,omitempty"` + Licenses []string `protobuf:"bytes,15,rep,name=licenses,proto3" json:"licenses,omitempty"` + Locations []*Location `protobuf:"bytes,20,rep,name=locations,proto3" json:"locations,omitempty"` + Layer *Layer `protobuf:"bytes,11,opt,name=layer,proto3" json:"layer,omitempty"` + FilePath string `protobuf:"bytes,12,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + DependsOn []string `protobuf:"bytes,14,rep,name=depends_on,json=dependsOn,proto3" json:"depends_on,omitempty"` + Digest string `protobuf:"bytes,16,opt,name=digest,proto3" json:"digest,omitempty"` + Dev bool `protobuf:"varint,17,opt,name=dev,proto3" json:"dev,omitempty"` + Indirect bool `protobuf:"varint,18,opt,name=indirect,proto3" json:"indirect,omitempty"` + Maintainer string `protobuf:"bytes,21,opt,name=maintainer,proto3" json:"maintainer,omitempty"` + Relationship int32 `protobuf:"varint,22,opt,name=relationship,proto3" json:"relationship,omitempty"` + AnalyzedBy string `protobuf:"bytes,23,opt,name=analyzed_by,json=analyzedBy,proto3" json:"analyzed_by,omitempty"` +} + +func (x *Package) Reset() { + *x = Package{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Package) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Package) ProtoMessage() {} + +func (x *Package) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Package.ProtoReflect.Descriptor instead. +func (*Package) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{4} +} + +func (x *Package) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *Package) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Package) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *Package) GetRelease() string { + if x != nil { + return x.Release + } + return "" +} + +func (x *Package) GetEpoch() int32 { + if x != nil { + return x.Epoch + } + return 0 +} + +func (x *Package) GetIdentifier() *PkgIdentifier { + if x != nil { + return x.Identifier + } + return nil +} + +func (x *Package) GetArch() string { + if x != nil { + return x.Arch + } + return "" +} + +func (x *Package) GetSrcName() string { + if x != nil { + return x.SrcName + } + return "" +} + +func (x *Package) GetSrcVersion() string { + if x != nil { + return x.SrcVersion + } + return "" +} + +func (x *Package) GetSrcRelease() string { + if x != nil { + return x.SrcRelease + } + return "" +} + +func (x *Package) GetSrcEpoch() int32 { + if x != nil { + return x.SrcEpoch + } + return 0 +} + +func (x *Package) GetLicenses() []string { + if x != nil { + return x.Licenses + } + return nil +} + +func (x *Package) GetLocations() []*Location { + if x != nil { + return x.Locations + } + return nil +} + +func (x *Package) GetLayer() *Layer { + if x != nil { + return x.Layer + } + return nil +} + +func (x *Package) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *Package) GetDependsOn() []string { + if x != nil { + return x.DependsOn + } + return nil +} + +func (x *Package) GetDigest() string { + if x != nil { + return x.Digest + } + return "" +} + +func (x *Package) GetDev() bool { + if x != nil { + return x.Dev + } + return false +} + +func (x *Package) GetIndirect() bool { + if x != nil { + return x.Indirect + } + return false +} + +func (x *Package) GetMaintainer() string { + if x != nil { + return x.Maintainer + } + return "" +} + +func (x *Package) GetRelationship() int32 { + if x != nil { + return x.Relationship + } + return 0 +} + +func (x *Package) GetAnalyzedBy() string { + if x != nil { + return x.AnalyzedBy + } + return "" +} + +type PkgIdentifier struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Purl string `protobuf:"bytes,1,opt,name=purl,proto3" json:"purl,omitempty"` + BomRef string `protobuf:"bytes,2,opt,name=bom_ref,json=bomRef,proto3" json:"bom_ref,omitempty"` + Uid string `protobuf:"bytes,3,opt,name=uid,proto3" json:"uid,omitempty"` +} + +func (x *PkgIdentifier) Reset() { + *x = PkgIdentifier{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PkgIdentifier) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PkgIdentifier) ProtoMessage() {} + +func (x *PkgIdentifier) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PkgIdentifier.ProtoReflect.Descriptor instead. +func (*PkgIdentifier) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{5} +} + +func (x *PkgIdentifier) GetPurl() string { + if x != nil { + return x.Purl + } + return "" +} + +func (x *PkgIdentifier) GetBomRef() string { + if x != nil { + return x.BomRef + } + return "" +} + +func (x *PkgIdentifier) GetUid() string { + if x != nil { + return x.Uid + } + return "" +} + +type Location struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + StartLine int32 `protobuf:"varint,1,opt,name=start_line,json=startLine,proto3" json:"start_line,omitempty"` + EndLine int32 `protobuf:"varint,2,opt,name=end_line,json=endLine,proto3" json:"end_line,omitempty"` +} + +func (x *Location) Reset() { + *x = Location{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Location) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Location) ProtoMessage() {} + +func (x *Location) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Location.ProtoReflect.Descriptor instead. +func (*Location) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{6} +} + +func (x *Location) GetStartLine() int32 { + if x != nil { + return x.StartLine + } + return 0 +} + +func (x *Location) GetEndLine() int32 { + if x != nil { + return x.EndLine + } + return 0 +} + +type BuildInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + ContentSets []string `protobuf:"bytes,1,rep,name=content_sets,json=contentSets,proto3" json:"content_sets,omitempty"` + Nvr string `protobuf:"bytes,2,opt,name=nvr,proto3" json:"nvr,omitempty"` + Arch string `protobuf:"bytes,3,opt,name=arch,proto3" json:"arch,omitempty"` +} + +func (x *BuildInfo) Reset() { + *x = BuildInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BuildInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuildInfo) ProtoMessage() {} + +func (x *BuildInfo) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[7] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuildInfo.ProtoReflect.Descriptor instead. +func (*BuildInfo) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{7} +} + +func (x *BuildInfo) GetContentSets() []string { + if x != nil { + return x.ContentSets + } + return nil +} + +func (x *BuildInfo) GetNvr() string { + if x != nil { + return x.Nvr + } + return "" +} + +func (x *BuildInfo) GetArch() string { + if x != nil { + return x.Arch + } + return "" +} + +type Misconfiguration struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + FileType string `protobuf:"bytes,1,opt,name=file_type,json=fileType,proto3" json:"file_type,omitempty"` + FilePath string `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + Successes []*MisconfResult `protobuf:"bytes,3,rep,name=successes,proto3" json:"successes,omitempty"` + Warnings []*MisconfResult `protobuf:"bytes,4,rep,name=warnings,proto3" json:"warnings,omitempty"` + Failures []*MisconfResult `protobuf:"bytes,5,rep,name=failures,proto3" json:"failures,omitempty"` +} + +func (x *Misconfiguration) Reset() { + *x = Misconfiguration{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Misconfiguration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Misconfiguration) ProtoMessage() {} + +func (x *Misconfiguration) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Misconfiguration.ProtoReflect.Descriptor instead. +func (*Misconfiguration) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{8} +} + +func (x *Misconfiguration) GetFileType() string { + if x != nil { + return x.FileType + } + return "" +} + +func (x *Misconfiguration) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *Misconfiguration) GetSuccesses() []*MisconfResult { + if x != nil { + return x.Successes + } + return nil +} + +func (x *Misconfiguration) GetWarnings() []*MisconfResult { + if x != nil { + return x.Warnings + } + return nil +} + +func (x *Misconfiguration) GetFailures() []*MisconfResult { + if x != nil { + return x.Failures + } + return nil +} + +type MisconfResult struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` + PolicyMetadata *PolicyMetadata `protobuf:"bytes,7,opt,name=policy_metadata,json=policyMetadata,proto3" json:"policy_metadata,omitempty"` + CauseMetadata *CauseMetadata `protobuf:"bytes,8,opt,name=cause_metadata,json=causeMetadata,proto3" json:"cause_metadata,omitempty"` +} + +func (x *MisconfResult) Reset() { + *x = MisconfResult{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *MisconfResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MisconfResult) ProtoMessage() {} + +func (x *MisconfResult) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[9] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MisconfResult.ProtoReflect.Descriptor instead. +func (*MisconfResult) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{9} +} + +func (x *MisconfResult) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *MisconfResult) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *MisconfResult) GetPolicyMetadata() *PolicyMetadata { + if x != nil { + return x.PolicyMetadata + } + return nil +} + +func (x *MisconfResult) GetCauseMetadata() *CauseMetadata { + if x != nil { + return x.CauseMetadata + } + return nil +} + +type PolicyMetadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + AdvId string `protobuf:"bytes,2,opt,name=adv_id,json=advId,proto3" json:"adv_id,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"` + Severity string `protobuf:"bytes,6,opt,name=severity,proto3" json:"severity,omitempty"` + RecommendedActions string `protobuf:"bytes,7,opt,name=recommended_actions,json=recommendedActions,proto3" json:"recommended_actions,omitempty"` + References []string `protobuf:"bytes,8,rep,name=references,proto3" json:"references,omitempty"` +} + +func (x *PolicyMetadata) Reset() { + *x = PolicyMetadata{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PolicyMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PolicyMetadata) ProtoMessage() {} + +func (x *PolicyMetadata) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[10] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PolicyMetadata.ProtoReflect.Descriptor instead. +func (*PolicyMetadata) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{10} +} + +func (x *PolicyMetadata) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *PolicyMetadata) GetAdvId() string { + if x != nil { + return x.AdvId + } + return "" +} + +func (x *PolicyMetadata) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *PolicyMetadata) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *PolicyMetadata) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *PolicyMetadata) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} + +func (x *PolicyMetadata) GetRecommendedActions() string { + if x != nil { + return x.RecommendedActions + } + return "" +} + +func (x *PolicyMetadata) GetReferences() []string { + if x != nil { + return x.References + } + return nil +} + +type DetectedMisconfiguration struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Id string `protobuf:"bytes,2,opt,name=id,proto3" json:"id,omitempty"` + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,4,opt,name=description,proto3" json:"description,omitempty"` + Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` + Namespace string `protobuf:"bytes,6,opt,name=namespace,proto3" json:"namespace,omitempty"` + Resolution string `protobuf:"bytes,7,opt,name=resolution,proto3" json:"resolution,omitempty"` + Severity Severity `protobuf:"varint,8,opt,name=severity,proto3,enum=trivy.common.Severity" json:"severity,omitempty"` + PrimaryUrl string `protobuf:"bytes,9,opt,name=primary_url,json=primaryUrl,proto3" json:"primary_url,omitempty"` + References []string `protobuf:"bytes,10,rep,name=references,proto3" json:"references,omitempty"` + Status string `protobuf:"bytes,11,opt,name=status,proto3" json:"status,omitempty"` + Layer *Layer `protobuf:"bytes,12,opt,name=layer,proto3" json:"layer,omitempty"` + CauseMetadata *CauseMetadata `protobuf:"bytes,13,opt,name=cause_metadata,json=causeMetadata,proto3" json:"cause_metadata,omitempty"` + // Deprecated: Marked as deprecated in rpc/common/service.proto. + AvdId string `protobuf:"bytes,14,opt,name=avd_id,json=avdId,proto3" json:"avd_id,omitempty"` + Query string `protobuf:"bytes,15,opt,name=query,proto3" json:"query,omitempty"` + Aliases []string `protobuf:"bytes,16,rep,name=aliases,proto3" json:"aliases,omitempty"` +} + +func (x *DetectedMisconfiguration) Reset() { + *x = DetectedMisconfiguration{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DetectedMisconfiguration) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetectedMisconfiguration) ProtoMessage() {} + +func (x *DetectedMisconfiguration) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DetectedMisconfiguration.ProtoReflect.Descriptor instead. +func (*DetectedMisconfiguration) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{11} +} + +func (x *DetectedMisconfiguration) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *DetectedMisconfiguration) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *DetectedMisconfiguration) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *DetectedMisconfiguration) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *DetectedMisconfiguration) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +func (x *DetectedMisconfiguration) GetNamespace() string { + if x != nil { + return x.Namespace + } + return "" +} + +func (x *DetectedMisconfiguration) GetResolution() string { + if x != nil { + return x.Resolution + } + return "" +} + +func (x *DetectedMisconfiguration) GetSeverity() Severity { + if x != nil { + return x.Severity + } + return Severity_UNKNOWN +} + +func (x *DetectedMisconfiguration) GetPrimaryUrl() string { + if x != nil { + return x.PrimaryUrl + } + return "" +} + +func (x *DetectedMisconfiguration) GetReferences() []string { + if x != nil { + return x.References + } + return nil +} + +func (x *DetectedMisconfiguration) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *DetectedMisconfiguration) GetLayer() *Layer { + if x != nil { + return x.Layer + } + return nil +} + +func (x *DetectedMisconfiguration) GetCauseMetadata() *CauseMetadata { + if x != nil { + return x.CauseMetadata + } + return nil +} + +// Deprecated: Marked as deprecated in rpc/common/service.proto. +func (x *DetectedMisconfiguration) GetAvdId() string { + if x != nil { + return x.AvdId + } + return "" +} + +func (x *DetectedMisconfiguration) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *DetectedMisconfiguration) GetAliases() []string { + if x != nil { + return x.Aliases + } + return nil +} + +type Vulnerability struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + VulnerabilityId string `protobuf:"bytes,1,opt,name=vulnerability_id,json=vulnerabilityId,proto3" json:"vulnerability_id,omitempty"` + PkgName string `protobuf:"bytes,2,opt,name=pkg_name,json=pkgName,proto3" json:"pkg_name,omitempty"` + InstalledVersion string `protobuf:"bytes,3,opt,name=installed_version,json=installedVersion,proto3" json:"installed_version,omitempty"` + FixedVersion string `protobuf:"bytes,4,opt,name=fixed_version,json=fixedVersion,proto3" json:"fixed_version,omitempty"` + Title string `protobuf:"bytes,5,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,6,opt,name=description,proto3" json:"description,omitempty"` + Severity Severity `protobuf:"varint,7,opt,name=severity,proto3,enum=trivy.common.Severity" json:"severity,omitempty"` + References []string `protobuf:"bytes,8,rep,name=references,proto3" json:"references,omitempty"` + PkgIdentifier *PkgIdentifier `protobuf:"bytes,25,opt,name=pkg_identifier,json=pkgIdentifier,proto3" json:"pkg_identifier,omitempty"` + Layer *Layer `protobuf:"bytes,10,opt,name=layer,proto3" json:"layer,omitempty"` + SeveritySource string `protobuf:"bytes,11,opt,name=severity_source,json=severitySource,proto3" json:"severity_source,omitempty"` + Cvss map[string]*CVSS `protobuf:"bytes,12,rep,name=cvss,proto3" json:"cvss,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + CweIds []string `protobuf:"bytes,13,rep,name=cwe_ids,json=cweIds,proto3" json:"cwe_ids,omitempty"` + PrimaryUrl string `protobuf:"bytes,14,opt,name=primary_url,json=primaryUrl,proto3" json:"primary_url,omitempty"` + PublishedDate *timestamppb.Timestamp `protobuf:"bytes,15,opt,name=published_date,json=publishedDate,proto3" json:"published_date,omitempty"` + LastModifiedDate *timestamppb.Timestamp `protobuf:"bytes,16,opt,name=last_modified_date,json=lastModifiedDate,proto3" json:"last_modified_date,omitempty"` + CustomAdvisoryData *structpb.Value `protobuf:"bytes,17,opt,name=custom_advisory_data,json=customAdvisoryData,proto3" json:"custom_advisory_data,omitempty"` + CustomVulnData *structpb.Value `protobuf:"bytes,18,opt,name=custom_vuln_data,json=customVulnData,proto3" json:"custom_vuln_data,omitempty"` + VendorIds []string `protobuf:"bytes,19,rep,name=vendor_ids,json=vendorIds,proto3" json:"vendor_ids,omitempty"` + DataSource *DataSource `protobuf:"bytes,20,opt,name=data_source,json=dataSource,proto3" json:"data_source,omitempty"` + VendorSeverity map[string]Severity `protobuf:"bytes,21,rep,name=vendor_severity,json=vendorSeverity,proto3" json:"vendor_severity,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"varint,2,opt,name=value,proto3,enum=trivy.common.Severity"` + PkgPath string `protobuf:"bytes,22,opt,name=pkg_path,json=pkgPath,proto3" json:"pkg_path,omitempty"` + PkgId string `protobuf:"bytes,23,opt,name=pkg_id,json=pkgId,proto3" json:"pkg_id,omitempty"` + Status int32 `protobuf:"varint,24,opt,name=status,proto3" json:"status,omitempty"` +} + +func (x *Vulnerability) Reset() { + *x = Vulnerability{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Vulnerability) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Vulnerability) ProtoMessage() {} + +func (x *Vulnerability) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Vulnerability.ProtoReflect.Descriptor instead. +func (*Vulnerability) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{12} +} + +func (x *Vulnerability) GetVulnerabilityId() string { + if x != nil { + return x.VulnerabilityId + } + return "" +} + +func (x *Vulnerability) GetPkgName() string { + if x != nil { + return x.PkgName + } + return "" +} + +func (x *Vulnerability) GetInstalledVersion() string { + if x != nil { + return x.InstalledVersion + } + return "" +} + +func (x *Vulnerability) GetFixedVersion() string { + if x != nil { + return x.FixedVersion + } + return "" +} + +func (x *Vulnerability) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Vulnerability) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Vulnerability) GetSeverity() Severity { + if x != nil { + return x.Severity + } + return Severity_UNKNOWN +} + +func (x *Vulnerability) GetReferences() []string { + if x != nil { + return x.References + } + return nil +} + +func (x *Vulnerability) GetPkgIdentifier() *PkgIdentifier { + if x != nil { + return x.PkgIdentifier + } + return nil +} + +func (x *Vulnerability) GetLayer() *Layer { + if x != nil { + return x.Layer + } + return nil +} + +func (x *Vulnerability) GetSeveritySource() string { + if x != nil { + return x.SeveritySource + } + return "" +} + +func (x *Vulnerability) GetCvss() map[string]*CVSS { + if x != nil { + return x.Cvss + } + return nil +} + +func (x *Vulnerability) GetCweIds() []string { + if x != nil { + return x.CweIds + } + return nil +} + +func (x *Vulnerability) GetPrimaryUrl() string { + if x != nil { + return x.PrimaryUrl + } + return "" +} + +func (x *Vulnerability) GetPublishedDate() *timestamppb.Timestamp { + if x != nil { + return x.PublishedDate + } + return nil +} + +func (x *Vulnerability) GetLastModifiedDate() *timestamppb.Timestamp { + if x != nil { + return x.LastModifiedDate + } + return nil +} + +func (x *Vulnerability) GetCustomAdvisoryData() *structpb.Value { + if x != nil { + return x.CustomAdvisoryData + } + return nil +} + +func (x *Vulnerability) GetCustomVulnData() *structpb.Value { + if x != nil { + return x.CustomVulnData + } + return nil +} + +func (x *Vulnerability) GetVendorIds() []string { + if x != nil { + return x.VendorIds + } + return nil +} + +func (x *Vulnerability) GetDataSource() *DataSource { + if x != nil { + return x.DataSource + } + return nil +} + +func (x *Vulnerability) GetVendorSeverity() map[string]Severity { + if x != nil { + return x.VendorSeverity + } + return nil +} + +func (x *Vulnerability) GetPkgPath() string { + if x != nil { + return x.PkgPath + } + return "" +} + +func (x *Vulnerability) GetPkgId() string { + if x != nil { + return x.PkgId + } + return "" +} + +func (x *Vulnerability) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +type DataSource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` +} + +func (x *DataSource) Reset() { + *x = DataSource{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DataSource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DataSource) ProtoMessage() {} + +func (x *DataSource) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DataSource.ProtoReflect.Descriptor instead. +func (*DataSource) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{13} +} + +func (x *DataSource) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *DataSource) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DataSource) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +type Layer struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Digest string `protobuf:"bytes,1,opt,name=digest,proto3" json:"digest,omitempty"` + DiffId string `protobuf:"bytes,2,opt,name=diff_id,json=diffId,proto3" json:"diff_id,omitempty"` + CreatedBy string `protobuf:"bytes,3,opt,name=created_by,json=createdBy,proto3" json:"created_by,omitempty"` + Size int64 `protobuf:"varint,4,opt,name=size,proto3" json:"size,omitempty"` +} + +func (x *Layer) Reset() { + *x = Layer{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Layer) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Layer) ProtoMessage() {} + +func (x *Layer) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Layer.ProtoReflect.Descriptor instead. +func (*Layer) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{14} +} + +func (x *Layer) GetDigest() string { + if x != nil { + return x.Digest + } + return "" +} + +func (x *Layer) GetDiffId() string { + if x != nil { + return x.DiffId + } + return "" +} + +func (x *Layer) GetCreatedBy() string { + if x != nil { + return x.CreatedBy + } + return "" +} + +func (x *Layer) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +type CauseMetadata struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Resource string `protobuf:"bytes,1,opt,name=resource,proto3" json:"resource,omitempty"` + Provider string `protobuf:"bytes,2,opt,name=provider,proto3" json:"provider,omitempty"` + Service string `protobuf:"bytes,3,opt,name=service,proto3" json:"service,omitempty"` + StartLine int32 `protobuf:"varint,4,opt,name=start_line,json=startLine,proto3" json:"start_line,omitempty"` + EndLine int32 `protobuf:"varint,5,opt,name=end_line,json=endLine,proto3" json:"end_line,omitempty"` + Code *Code `protobuf:"bytes,6,opt,name=code,proto3" json:"code,omitempty"` + RenderedCause *RenderedCause `protobuf:"bytes,7,opt,name=rendered_cause,json=renderedCause,proto3" json:"rendered_cause,omitempty"` +} + +func (x *CauseMetadata) Reset() { + *x = CauseMetadata{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CauseMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CauseMetadata) ProtoMessage() {} + +func (x *CauseMetadata) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CauseMetadata.ProtoReflect.Descriptor instead. +func (*CauseMetadata) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{15} +} + +func (x *CauseMetadata) GetResource() string { + if x != nil { + return x.Resource + } + return "" +} + +func (x *CauseMetadata) GetProvider() string { + if x != nil { + return x.Provider + } + return "" +} + +func (x *CauseMetadata) GetService() string { + if x != nil { + return x.Service + } + return "" +} + +func (x *CauseMetadata) GetStartLine() int32 { + if x != nil { + return x.StartLine + } + return 0 +} + +func (x *CauseMetadata) GetEndLine() int32 { + if x != nil { + return x.EndLine + } + return 0 +} + +func (x *CauseMetadata) GetCode() *Code { + if x != nil { + return x.Code + } + return nil +} + +func (x *CauseMetadata) GetRenderedCause() *RenderedCause { + if x != nil { + return x.RenderedCause + } + return nil +} + +type CVSS struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + V2Vector string `protobuf:"bytes,1,opt,name=v2_vector,json=v2Vector,proto3" json:"v2_vector,omitempty"` + V3Vector string `protobuf:"bytes,2,opt,name=v3_vector,json=v3Vector,proto3" json:"v3_vector,omitempty"` + V2Score float64 `protobuf:"fixed64,3,opt,name=v2_score,json=v2Score,proto3" json:"v2_score,omitempty"` + V3Score float64 `protobuf:"fixed64,4,opt,name=v3_score,json=v3Score,proto3" json:"v3_score,omitempty"` + V40Vector string `protobuf:"bytes,5,opt,name=v40_vector,json=v40Vector,proto3" json:"v40_vector,omitempty"` + V40Score float64 `protobuf:"fixed64,6,opt,name=v40_score,json=v40Score,proto3" json:"v40_score,omitempty"` +} + +func (x *CVSS) Reset() { + *x = CVSS{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CVSS) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CVSS) ProtoMessage() {} + +func (x *CVSS) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[16] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CVSS.ProtoReflect.Descriptor instead. +func (*CVSS) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{16} +} + +func (x *CVSS) GetV2Vector() string { + if x != nil { + return x.V2Vector + } + return "" +} + +func (x *CVSS) GetV3Vector() string { + if x != nil { + return x.V3Vector + } + return "" +} + +func (x *CVSS) GetV2Score() float64 { + if x != nil { + return x.V2Score + } + return 0 +} + +func (x *CVSS) GetV3Score() float64 { + if x != nil { + return x.V3Score + } + return 0 +} + +func (x *CVSS) GetV40Vector() string { + if x != nil { + return x.V40Vector + } + return "" +} + +func (x *CVSS) GetV40Score() float64 { + if x != nil { + return x.V40Score + } + return 0 +} + +type CustomResource struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + FilePath string `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + Layer *Layer `protobuf:"bytes,3,opt,name=layer,proto3" json:"layer,omitempty"` + Data *structpb.Value `protobuf:"bytes,4,opt,name=data,proto3" json:"data,omitempty"` +} + +func (x *CustomResource) Reset() { + *x = CustomResource{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CustomResource) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CustomResource) ProtoMessage() {} + +func (x *CustomResource) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CustomResource.ProtoReflect.Descriptor instead. +func (*CustomResource) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{17} +} + +func (x *CustomResource) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *CustomResource) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *CustomResource) GetLayer() *Layer { + if x != nil { + return x.Layer + } + return nil +} + +func (x *CustomResource) GetData() *structpb.Value { + if x != nil { + return x.Data + } + return nil +} + +type Line struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Number int32 `protobuf:"varint,1,opt,name=number,proto3" json:"number,omitempty"` + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + IsCause bool `protobuf:"varint,3,opt,name=is_cause,json=isCause,proto3" json:"is_cause,omitempty"` + Annotation string `protobuf:"bytes,4,opt,name=annotation,proto3" json:"annotation,omitempty"` + Truncated bool `protobuf:"varint,5,opt,name=truncated,proto3" json:"truncated,omitempty"` + Highlighted string `protobuf:"bytes,6,opt,name=highlighted,proto3" json:"highlighted,omitempty"` + FirstCause bool `protobuf:"varint,7,opt,name=first_cause,json=firstCause,proto3" json:"first_cause,omitempty"` + LastCause bool `protobuf:"varint,8,opt,name=last_cause,json=lastCause,proto3" json:"last_cause,omitempty"` +} + +func (x *Line) Reset() { + *x = Line{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Line) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Line) ProtoMessage() {} + +func (x *Line) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[18] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Line.ProtoReflect.Descriptor instead. +func (*Line) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{18} +} + +func (x *Line) GetNumber() int32 { + if x != nil { + return x.Number + } + return 0 +} + +func (x *Line) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *Line) GetIsCause() bool { + if x != nil { + return x.IsCause + } + return false +} + +func (x *Line) GetAnnotation() string { + if x != nil { + return x.Annotation + } + return "" +} + +func (x *Line) GetTruncated() bool { + if x != nil { + return x.Truncated + } + return false +} + +func (x *Line) GetHighlighted() string { + if x != nil { + return x.Highlighted + } + return "" +} + +func (x *Line) GetFirstCause() bool { + if x != nil { + return x.FirstCause + } + return false +} + +func (x *Line) GetLastCause() bool { + if x != nil { + return x.LastCause + } + return false +} + +type Code struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Lines []*Line `protobuf:"bytes,1,rep,name=lines,proto3" json:"lines,omitempty"` +} + +func (x *Code) Reset() { + *x = Code{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Code) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Code) ProtoMessage() {} + +func (x *Code) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[19] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Code.ProtoReflect.Descriptor instead. +func (*Code) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{19} +} + +func (x *Code) GetLines() []*Line { + if x != nil { + return x.Lines + } + return nil +} + +type RenderedCause struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Raw string `protobuf:"bytes,1,opt,name=raw,proto3" json:"raw,omitempty"` + Highlighted string `protobuf:"bytes,2,opt,name=highlighted,proto3" json:"highlighted,omitempty"` +} + +func (x *RenderedCause) Reset() { + *x = RenderedCause{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RenderedCause) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenderedCause) ProtoMessage() {} + +func (x *RenderedCause) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenderedCause.ProtoReflect.Descriptor instead. +func (*RenderedCause) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{20} +} + +func (x *RenderedCause) GetRaw() string { + if x != nil { + return x.Raw + } + return "" +} + +func (x *RenderedCause) GetHighlighted() string { + if x != nil { + return x.Highlighted + } + return "" +} + +type SecretFinding struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + RuleId string `protobuf:"bytes,1,opt,name=rule_id,json=ruleId,proto3" json:"rule_id,omitempty"` + Category string `protobuf:"bytes,2,opt,name=category,proto3" json:"category,omitempty"` + Severity string `protobuf:"bytes,3,opt,name=severity,proto3" json:"severity,omitempty"` + Title string `protobuf:"bytes,4,opt,name=title,proto3" json:"title,omitempty"` + StartLine int32 `protobuf:"varint,5,opt,name=start_line,json=startLine,proto3" json:"start_line,omitempty"` + EndLine int32 `protobuf:"varint,6,opt,name=end_line,json=endLine,proto3" json:"end_line,omitempty"` + Code *Code `protobuf:"bytes,7,opt,name=code,proto3" json:"code,omitempty"` + Match string `protobuf:"bytes,8,opt,name=match,proto3" json:"match,omitempty"` + Layer *Layer `protobuf:"bytes,10,opt,name=layer,proto3" json:"layer,omitempty"` + Offset int32 `protobuf:"varint,11,opt,name=offset,proto3" json:"offset,omitempty"` +} + +func (x *SecretFinding) Reset() { + *x = SecretFinding{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SecretFinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SecretFinding) ProtoMessage() {} + +func (x *SecretFinding) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SecretFinding.ProtoReflect.Descriptor instead. +func (*SecretFinding) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{21} +} + +func (x *SecretFinding) GetRuleId() string { + if x != nil { + return x.RuleId + } + return "" +} + +func (x *SecretFinding) GetCategory() string { + if x != nil { + return x.Category + } + return "" +} + +func (x *SecretFinding) GetSeverity() string { + if x != nil { + return x.Severity + } + return "" +} + +func (x *SecretFinding) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *SecretFinding) GetStartLine() int32 { + if x != nil { + return x.StartLine + } + return 0 +} + +func (x *SecretFinding) GetEndLine() int32 { + if x != nil { + return x.EndLine + } + return 0 +} + +func (x *SecretFinding) GetCode() *Code { + if x != nil { + return x.Code + } + return nil +} + +func (x *SecretFinding) GetMatch() string { + if x != nil { + return x.Match + } + return "" +} + +func (x *SecretFinding) GetLayer() *Layer { + if x != nil { + return x.Layer + } + return nil +} + +func (x *SecretFinding) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +type Secret struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Filepath string `protobuf:"bytes,1,opt,name=filepath,proto3" json:"filepath,omitempty"` + Findings []*SecretFinding `protobuf:"bytes,2,rep,name=findings,proto3" json:"findings,omitempty"` +} + +func (x *Secret) Reset() { + *x = Secret{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Secret) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Secret) ProtoMessage() {} + +func (x *Secret) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Secret.ProtoReflect.Descriptor instead. +func (*Secret) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{22} +} + +func (x *Secret) GetFilepath() string { + if x != nil { + return x.Filepath + } + return "" +} + +func (x *Secret) GetFindings() []*SecretFinding { + if x != nil { + return x.Findings + } + return nil +} + +type DetectedLicense struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Severity Severity `protobuf:"varint,1,opt,name=severity,proto3,enum=trivy.common.Severity" json:"severity,omitempty"` + Category LicenseCategory_Enum `protobuf:"varint,2,opt,name=category,proto3,enum=trivy.common.LicenseCategory_Enum" json:"category,omitempty"` + PkgName string `protobuf:"bytes,3,opt,name=pkg_name,json=pkgName,proto3" json:"pkg_name,omitempty"` + FilePath string `protobuf:"bytes,4,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + Name string `protobuf:"bytes,5,opt,name=name,proto3" json:"name,omitempty"` + Confidence float32 `protobuf:"fixed32,6,opt,name=confidence,proto3" json:"confidence,omitempty"` + Link string `protobuf:"bytes,7,opt,name=link,proto3" json:"link,omitempty"` + Text string `protobuf:"bytes,8,opt,name=text,proto3" json:"text,omitempty"` +} + +func (x *DetectedLicense) Reset() { + *x = DetectedLicense{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *DetectedLicense) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DetectedLicense) ProtoMessage() {} + +func (x *DetectedLicense) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DetectedLicense.ProtoReflect.Descriptor instead. +func (*DetectedLicense) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{23} +} + +func (x *DetectedLicense) GetSeverity() Severity { + if x != nil { + return x.Severity + } + return Severity_UNKNOWN +} + +func (x *DetectedLicense) GetCategory() LicenseCategory_Enum { + if x != nil { + return x.Category + } + return LicenseCategory_UNSPECIFIED +} + +func (x *DetectedLicense) GetPkgName() string { + if x != nil { + return x.PkgName + } + return "" +} + +func (x *DetectedLicense) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *DetectedLicense) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DetectedLicense) GetConfidence() float32 { + if x != nil { + return x.Confidence + } + return 0 +} + +func (x *DetectedLicense) GetLink() string { + if x != nil { + return x.Link + } + return "" +} + +func (x *DetectedLicense) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +type LicenseFile struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LicenseType LicenseType_Enum `protobuf:"varint,1,opt,name=license_type,json=licenseType,proto3,enum=trivy.common.LicenseType_Enum" json:"license_type,omitempty"` + FilePath string `protobuf:"bytes,2,opt,name=file_path,json=filePath,proto3" json:"file_path,omitempty"` + PkgName string `protobuf:"bytes,3,opt,name=pkg_name,json=pkgName,proto3" json:"pkg_name,omitempty"` + Fingings []*LicenseFinding `protobuf:"bytes,4,rep,name=fingings,proto3" json:"fingings,omitempty"` + Layer *Layer `protobuf:"bytes,5,opt,name=layer,proto3" json:"layer,omitempty"` +} + +func (x *LicenseFile) Reset() { + *x = LicenseFile{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LicenseFile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LicenseFile) ProtoMessage() {} + +func (x *LicenseFile) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[24] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LicenseFile.ProtoReflect.Descriptor instead. +func (*LicenseFile) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{24} +} + +func (x *LicenseFile) GetLicenseType() LicenseType_Enum { + if x != nil { + return x.LicenseType + } + return LicenseType_UNSPECIFIED +} + +func (x *LicenseFile) GetFilePath() string { + if x != nil { + return x.FilePath + } + return "" +} + +func (x *LicenseFile) GetPkgName() string { + if x != nil { + return x.PkgName + } + return "" +} + +func (x *LicenseFile) GetFingings() []*LicenseFinding { + if x != nil { + return x.Fingings + } + return nil +} + +func (x *LicenseFile) GetLayer() *Layer { + if x != nil { + return x.Layer + } + return nil +} + +type LicenseFinding struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Category LicenseCategory_Enum `protobuf:"varint,1,opt,name=category,proto3,enum=trivy.common.LicenseCategory_Enum" json:"category,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Confidence float32 `protobuf:"fixed32,3,opt,name=confidence,proto3" json:"confidence,omitempty"` + Link string `protobuf:"bytes,4,opt,name=link,proto3" json:"link,omitempty"` +} + +func (x *LicenseFinding) Reset() { + *x = LicenseFinding{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LicenseFinding) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LicenseFinding) ProtoMessage() {} + +func (x *LicenseFinding) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[25] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LicenseFinding.ProtoReflect.Descriptor instead. +func (*LicenseFinding) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{25} +} + +func (x *LicenseFinding) GetCategory() LicenseCategory_Enum { + if x != nil { + return x.Category + } + return LicenseCategory_UNSPECIFIED +} + +func (x *LicenseFinding) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *LicenseFinding) GetConfidence() float32 { + if x != nil { + return x.Confidence + } + return 0 +} + +func (x *LicenseFinding) GetLink() string { + if x != nil { + return x.Link + } + return "" +} + +// Enumerations are wrapped with a message to improve the readability of +// enumerations in generated code and avoid name conflicts. +// https://github.com/golang/protobuf/issues/513 +type LicenseCategory struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *LicenseCategory) Reset() { + *x = LicenseCategory{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LicenseCategory) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LicenseCategory) ProtoMessage() {} + +func (x *LicenseCategory) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[26] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LicenseCategory.ProtoReflect.Descriptor instead. +func (*LicenseCategory) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{26} +} + +type LicenseType struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *LicenseType) Reset() { + *x = LicenseType{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_common_service_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *LicenseType) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LicenseType) ProtoMessage() {} + +func (x *LicenseType) ProtoReflect() protoreflect.Message { + mi := &file_rpc_common_service_proto_msgTypes[27] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LicenseType.ProtoReflect.Descriptor instead. +func (*LicenseType) Descriptor() ([]byte, []int) { + return file_rpc_common_service_proto_rawDescGZIP(), []int{27} +} + +var File_rpc_common_service_proto protoreflect.FileDescriptor + +var file_rpc_common_service_proto_rawDesc = []byte{ + 0x0a, 0x18, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0c, 0x74, 0x72, 0x69, 0x76, + 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x73, 0x74, 0x72, 0x75, 0x63, 0x74, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x60, 0x0a, 0x02, 0x4f, 0x53, 0x12, 0x16, 0x0a, + 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, + 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x65, 0x6f, 0x73, + 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x65, 0x6f, 0x73, 0x6c, 0x12, 0x1a, 0x0a, + 0x08, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x08, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x22, 0x3e, 0x0a, 0x0a, 0x52, 0x65, 0x70, + 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x16, 0x0a, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x66, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, + 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x22, 0x5d, 0x0a, 0x0b, 0x50, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, + 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, + 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x31, 0x0a, 0x08, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x08, + 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x22, 0x71, 0x0a, 0x0b, 0x41, 0x70, 0x70, 0x6c, + 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, + 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, + 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x31, 0x0a, 0x08, 0x70, 0x61, 0x63, 0x6b, + 0x61, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x72, 0x69, + 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, + 0x65, 0x52, 0x08, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x22, 0xa6, 0x05, 0x0a, 0x07, + 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0d, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, + 0x14, 0x0a, 0x05, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, + 0x65, 0x70, 0x6f, 0x63, 0x68, 0x12, 0x3b, 0x0a, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x72, 0x69, 0x76, + 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6b, 0x67, 0x49, 0x64, 0x65, 0x6e, + 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0a, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, + 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x61, 0x72, 0x63, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x61, 0x72, 0x63, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x72, 0x63, 0x5f, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x72, 0x63, 0x4e, 0x61, 0x6d, + 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x72, 0x63, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x72, 0x63, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x72, 0x63, 0x5f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x72, 0x63, 0x52, 0x65, 0x6c, 0x65, + 0x61, 0x73, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x73, 0x72, 0x63, 0x5f, 0x65, 0x70, 0x6f, 0x63, 0x68, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x73, 0x72, 0x63, 0x45, 0x70, 0x6f, 0x63, 0x68, + 0x12, 0x1a, 0x0a, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x0f, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x12, 0x34, 0x0a, 0x09, + 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x14, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, + 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x12, 0x29, 0x0a, 0x05, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x52, 0x05, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x12, 0x1b, 0x0a, + 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x64, 0x65, + 0x70, 0x65, 0x6e, 0x64, 0x73, 0x5f, 0x6f, 0x6e, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, + 0x64, 0x65, 0x70, 0x65, 0x6e, 0x64, 0x73, 0x4f, 0x6e, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, + 0x65, 0x73, 0x74, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, + 0x74, 0x12, 0x10, 0x0a, 0x03, 0x64, 0x65, 0x76, 0x18, 0x11, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, + 0x64, 0x65, 0x76, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x18, + 0x12, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x6e, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x12, + 0x1e, 0x0a, 0x0a, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x15, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x61, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, + 0x22, 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x18, + 0x16, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x68, 0x69, 0x70, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, 0x65, 0x64, 0x5f, + 0x62, 0x79, 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6e, 0x61, 0x6c, 0x79, 0x7a, + 0x65, 0x64, 0x42, 0x79, 0x22, 0x4e, 0x0a, 0x0d, 0x50, 0x6b, 0x67, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x75, 0x72, 0x6c, 0x12, 0x17, 0x0a, 0x07, 0x62, 0x6f, 0x6d, + 0x5f, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x62, 0x6f, 0x6d, 0x52, + 0x65, 0x66, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x75, 0x69, 0x64, 0x22, 0x44, 0x0a, 0x08, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x69, 0x6e, 0x65, 0x12, + 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x05, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x4c, 0x69, 0x6e, 0x65, 0x22, 0x54, 0x0a, 0x09, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, + 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x63, + 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x6e, 0x76, + 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6e, 0x76, 0x72, 0x12, 0x12, 0x0a, 0x04, + 0x61, 0x72, 0x63, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x63, 0x68, + 0x22, 0xff, 0x01, 0x0a, 0x10, 0x4d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x54, 0x79, + 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, + 0x39, 0x0a, 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x4d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, + 0x09, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x37, 0x0a, 0x08, 0x77, 0x61, + 0x72, 0x6e, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, + 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x69, 0x73, 0x63, + 0x6f, 0x6e, 0x66, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x08, 0x77, 0x61, 0x72, 0x6e, 0x69, + 0x6e, 0x67, 0x73, 0x12, 0x37, 0x0a, 0x08, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x18, + 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x52, 0x08, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x4a, 0x04, 0x08, 0x06, + 0x10, 0x07, 0x22, 0xf3, 0x01, 0x0a, 0x0d, 0x4d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x52, 0x65, + 0x73, 0x75, 0x6c, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, + 0x63, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x45, 0x0a, 0x0f, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x52, 0x0e, 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x12, 0x42, 0x0a, 0x0e, 0x63, 0x61, 0x75, 0x73, 0x65, 0x5f, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x72, + 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x75, 0x73, 0x65, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x63, 0x61, 0x75, 0x73, 0x65, 0x4d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x07, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x52, 0x02, 0x69, 0x64, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x52, 0x08, + 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x22, 0xf0, 0x01, 0x0a, 0x0e, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x61, + 0x64, 0x76, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x61, 0x64, 0x76, + 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, + 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, + 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x2f, 0x0a, 0x13, 0x72, 0x65, + 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x72, 0x65, 0x63, 0x6f, 0x6d, 0x6d, 0x65, + 0x6e, 0x64, 0x65, 0x64, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x72, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x95, 0x04, 0x0a, 0x18, + 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x14, 0x0a, 0x05, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, + 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1c, + 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x12, 0x1e, 0x0a, 0x0a, + 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x08, + 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, + 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, + 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, + 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x75, 0x72, 0x6c, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x55, 0x72, + 0x6c, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, + 0x0a, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, + 0x73, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x29, 0x0a, 0x05, 0x6c, 0x61, 0x79, + 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x52, 0x05, 0x6c, + 0x61, 0x79, 0x65, 0x72, 0x12, 0x42, 0x0a, 0x0e, 0x63, 0x61, 0x75, 0x73, 0x65, 0x5f, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, + 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x75, 0x73, + 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x0d, 0x63, 0x61, 0x75, 0x73, 0x65, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x19, 0x0a, 0x06, 0x61, 0x76, 0x64, 0x5f, + 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x05, 0x61, 0x76, + 0x64, 0x49, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x6c, 0x69, + 0x61, 0x73, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x61, 0x6c, 0x69, 0x61, + 0x73, 0x65, 0x73, 0x22, 0xff, 0x09, 0x0a, 0x0d, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, + 0x69, 0x6c, 0x69, 0x74, 0x79, 0x12, 0x29, 0x0a, 0x10, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0f, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x49, 0x64, + 0x12, 0x19, 0x0a, 0x08, 0x70, 0x6b, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x70, 0x6b, 0x67, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, 0x64, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x65, + 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x69, 0x78, 0x65, + 0x64, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x66, 0x69, 0x78, 0x65, 0x64, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, + 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, + 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x72, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0a, 0x72, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x42, 0x0a, 0x0e, 0x70, 0x6b, 0x67, + 0x5f, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x19, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x50, 0x6b, 0x67, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0d, + 0x70, 0x6b, 0x67, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x29, 0x0a, + 0x05, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, + 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x61, 0x79, 0x65, + 0x72, 0x52, 0x05, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x12, 0x27, 0x0a, 0x0f, 0x73, 0x65, 0x76, 0x65, + 0x72, 0x69, 0x74, 0x79, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0e, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x53, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x12, 0x39, 0x0a, 0x04, 0x63, 0x76, 0x73, 0x73, 0x18, 0x0c, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x25, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x56, + 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2e, 0x43, 0x76, 0x73, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x63, 0x76, 0x73, 0x73, 0x12, 0x17, 0x0a, 0x07, + 0x63, 0x77, 0x65, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x63, + 0x77, 0x65, 0x49, 0x64, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, + 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x72, 0x69, 0x6d, + 0x61, 0x72, 0x79, 0x55, 0x72, 0x6c, 0x12, 0x41, 0x0a, 0x0e, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, + 0x68, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0d, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x73, 0x68, 0x65, 0x64, 0x44, 0x61, 0x74, 0x65, 0x12, 0x48, 0x0a, 0x12, 0x6c, 0x61, 0x73, + 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x5f, 0x64, 0x61, 0x74, 0x65, 0x18, + 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x10, 0x6c, 0x61, 0x73, 0x74, 0x4d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x44, + 0x61, 0x74, 0x65, 0x12, 0x48, 0x0a, 0x14, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x61, 0x64, + 0x76, 0x69, 0x73, 0x6f, 0x72, 0x79, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x11, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x12, 0x63, 0x75, 0x73, 0x74, 0x6f, + 0x6d, 0x41, 0x64, 0x76, 0x69, 0x73, 0x6f, 0x72, 0x79, 0x44, 0x61, 0x74, 0x61, 0x12, 0x40, 0x0a, + 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x76, 0x75, 0x6c, 0x6e, 0x5f, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, + 0x0e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x56, 0x75, 0x6c, 0x6e, 0x44, 0x61, 0x74, 0x61, 0x12, + 0x1d, 0x0a, 0x0a, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x13, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x09, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x49, 0x64, 0x73, 0x12, 0x39, + 0x0a, 0x0b, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x14, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0a, 0x64, + 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x58, 0x0a, 0x0f, 0x76, 0x65, 0x6e, + 0x64, 0x6f, 0x72, 0x5f, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x15, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2e, + 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x0e, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x53, 0x65, 0x76, 0x65, 0x72, + 0x69, 0x74, 0x79, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x6b, 0x67, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, + 0x16, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x6b, 0x67, 0x50, 0x61, 0x74, 0x68, 0x12, 0x15, + 0x0a, 0x06, 0x70, 0x6b, 0x67, 0x5f, 0x69, 0x64, 0x18, 0x17, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x70, 0x6b, 0x67, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x18, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x1a, 0x4b, 0x0a, + 0x09, 0x43, 0x76, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x28, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x74, 0x72, + 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x56, 0x53, 0x53, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x59, 0x0a, 0x13, 0x56, 0x65, + 0x6e, 0x64, 0x6f, 0x72, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x2c, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x42, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x53, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x22, 0x6b, 0x0a, 0x05, 0x4c, 0x61, 0x79, + 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x69, + 0x66, 0x66, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x69, 0x66, + 0x66, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x62, + 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x42, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x87, 0x02, 0x0a, 0x0d, 0x43, 0x61, 0x75, 0x73, 0x65, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, + 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x4c, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, + 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x65, 0x6e, 0x64, + 0x4c, 0x69, 0x6e, 0x65, 0x12, 0x26, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x43, 0x6f, 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x42, 0x0a, 0x0e, + 0x72, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x61, 0x75, 0x73, 0x65, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x65, 0x64, 0x43, 0x61, 0x75, 0x73, + 0x65, 0x52, 0x0d, 0x72, 0x65, 0x6e, 0x64, 0x65, 0x72, 0x65, 0x64, 0x43, 0x61, 0x75, 0x73, 0x65, + 0x22, 0xb2, 0x01, 0x0a, 0x04, 0x43, 0x56, 0x53, 0x53, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x32, 0x5f, + 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x32, + 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x33, 0x5f, 0x76, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x76, 0x33, 0x56, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x12, 0x19, 0x0a, 0x08, 0x76, 0x32, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x07, 0x76, 0x32, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x19, + 0x0a, 0x08, 0x76, 0x33, 0x5f, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, + 0x52, 0x07, 0x76, 0x33, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x76, 0x34, 0x30, + 0x5f, 0x76, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x76, + 0x34, 0x30, 0x56, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x34, 0x30, 0x5f, + 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x08, 0x76, 0x34, 0x30, + 0x53, 0x63, 0x6f, 0x72, 0x65, 0x22, 0x98, 0x01, 0x0a, 0x0e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, + 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, + 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x29, 0x0a, 0x05, 0x6c, 0x61, 0x79, + 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x52, 0x05, 0x6c, + 0x61, 0x79, 0x65, 0x72, 0x12, 0x2a, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, + 0x22, 0xf3, 0x01, 0x0a, 0x04, 0x4c, 0x69, 0x6e, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6e, 0x75, 0x6d, + 0x62, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6e, 0x75, 0x6d, 0x62, 0x65, + 0x72, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x69, + 0x73, 0x5f, 0x63, 0x61, 0x75, 0x73, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, + 0x73, 0x43, 0x61, 0x75, 0x73, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x6e, 0x6e, 0x6f, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x72, 0x75, 0x6e, 0x63, 0x61, + 0x74, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x74, 0x72, 0x75, 0x6e, 0x63, + 0x61, 0x74, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, + 0x74, 0x65, 0x64, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x68, 0x69, 0x67, 0x68, 0x6c, + 0x69, 0x67, 0x68, 0x74, 0x65, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, + 0x63, 0x61, 0x75, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x66, 0x69, 0x72, + 0x73, 0x74, 0x43, 0x61, 0x75, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x6c, 0x61, 0x73, 0x74, 0x5f, + 0x63, 0x61, 0x75, 0x73, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x6c, 0x61, 0x73, + 0x74, 0x43, 0x61, 0x75, 0x73, 0x65, 0x22, 0x30, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x28, + 0x0a, 0x05, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, + 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x6e, + 0x65, 0x52, 0x05, 0x6c, 0x69, 0x6e, 0x65, 0x73, 0x22, 0x43, 0x0a, 0x0d, 0x52, 0x65, 0x6e, 0x64, + 0x65, 0x72, 0x65, 0x64, 0x43, 0x61, 0x75, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x72, 0x61, 0x77, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x72, 0x61, 0x77, 0x12, 0x20, 0x0a, 0x0b, 0x68, + 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x65, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x68, 0x69, 0x67, 0x68, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x65, 0x64, 0x22, 0xb7, 0x02, + 0x0a, 0x0d, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x46, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, + 0x17, 0x0a, 0x07, 0x72, 0x75, 0x6c, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x72, 0x75, 0x6c, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, + 0x67, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, + 0x67, 0x6f, 0x72, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, + 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, + 0x6c, 0x69, 0x6e, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x4c, 0x69, 0x6e, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x65, 0x6e, 0x64, 0x5f, 0x6c, 0x69, 0x6e, + 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x4c, 0x69, 0x6e, 0x65, + 0x12, 0x26, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, + 0x64, 0x65, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6d, 0x61, 0x74, 0x63, + 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x29, + 0x0a, 0x05, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, + 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x61, 0x79, + 0x65, 0x72, 0x52, 0x05, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, + 0x73, 0x65, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, + 0x74, 0x4a, 0x04, 0x08, 0x09, 0x10, 0x0a, 0x22, 0x5d, 0x0a, 0x06, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x70, 0x61, 0x74, 0x68, 0x12, 0x37, 0x0a, + 0x08, 0x66, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x46, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x66, 0x69, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x73, 0x22, 0x99, 0x02, 0x0a, 0x0f, 0x44, 0x65, 0x74, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x08, 0x73, 0x65, + 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x74, + 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, 0x65, 0x76, 0x65, + 0x72, 0x69, 0x74, 0x79, 0x52, 0x08, 0x73, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x12, 0x3e, + 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x22, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x2e, + 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x19, + 0x0a, 0x08, 0x70, 0x6b, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x07, 0x70, 0x6b, 0x67, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, + 0x65, 0x5f, 0x70, 0x61, 0x74, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, + 0x6c, 0x65, 0x50, 0x61, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, + 0x6e, 0x66, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x02, 0x52, 0x0a, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, + 0x6e, 0x6b, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x65, + 0x78, 0x74, 0x22, 0xed, 0x01, 0x0a, 0x0b, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x46, 0x69, + 0x6c, 0x65, 0x12, 0x41, 0x0a, 0x0c, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x54, + 0x79, 0x70, 0x65, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x0b, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x70, 0x61, + 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x50, 0x61, + 0x74, 0x68, 0x12, 0x19, 0x0a, 0x08, 0x70, 0x6b, 0x67, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x6b, 0x67, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x38, 0x0a, + 0x08, 0x66, 0x69, 0x6e, 0x67, 0x69, 0x6e, 0x67, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, + 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x46, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x08, 0x66, + 0x69, 0x6e, 0x67, 0x69, 0x6e, 0x67, 0x73, 0x12, 0x29, 0x0a, 0x05, 0x6c, 0x61, 0x79, 0x65, 0x72, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x61, 0x79, 0x65, 0x72, 0x52, 0x05, 0x6c, 0x61, 0x79, + 0x65, 0x72, 0x22, 0x98, 0x01, 0x0a, 0x0e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x46, 0x69, + 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x3e, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x61, + 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x2e, 0x45, 0x6e, 0x75, 0x6d, 0x52, 0x08, 0x63, 0x61, 0x74, + 0x65, 0x67, 0x6f, 0x72, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x02, 0x52, 0x0a, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x64, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x6e, + 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x69, 0x6e, 0x6b, 0x22, 0x95, 0x01, + 0x0a, 0x0f, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, + 0x79, 0x22, 0x81, 0x01, 0x0a, 0x04, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x0f, 0x0a, 0x0b, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x46, + 0x4f, 0x52, 0x42, 0x49, 0x44, 0x44, 0x45, 0x4e, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x45, + 0x53, 0x54, 0x52, 0x49, 0x43, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x52, 0x45, + 0x43, 0x49, 0x50, 0x52, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x4e, 0x4f, + 0x54, 0x49, 0x43, 0x45, 0x10, 0x04, 0x12, 0x0e, 0x0a, 0x0a, 0x50, 0x45, 0x52, 0x4d, 0x49, 0x53, + 0x53, 0x49, 0x56, 0x45, 0x10, 0x05, 0x12, 0x10, 0x0a, 0x0c, 0x55, 0x4e, 0x45, 0x4e, 0x43, 0x55, + 0x4d, 0x42, 0x45, 0x52, 0x45, 0x44, 0x10, 0x06, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, + 0x4f, 0x57, 0x4e, 0x10, 0x07, 0x22, 0x4e, 0x0a, 0x0b, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, + 0x54, 0x79, 0x70, 0x65, 0x22, 0x3f, 0x0a, 0x04, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x0f, 0x0a, 0x0b, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x08, 0x0a, + 0x04, 0x44, 0x50, 0x4b, 0x47, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x48, 0x45, 0x41, 0x44, 0x45, + 0x52, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x5f, 0x46, + 0x49, 0x4c, 0x45, 0x10, 0x03, 0x2a, 0x44, 0x0a, 0x08, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, 0x74, + 0x79, 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x07, + 0x0a, 0x03, 0x4c, 0x4f, 0x57, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x4d, 0x45, 0x44, 0x49, 0x55, + 0x4d, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x48, 0x49, 0x47, 0x48, 0x10, 0x03, 0x12, 0x0c, 0x0a, + 0x08, 0x43, 0x52, 0x49, 0x54, 0x49, 0x43, 0x41, 0x4c, 0x10, 0x04, 0x42, 0x31, 0x5a, 0x2f, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x71, 0x75, 0x61, 0x73, 0x65, + 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2f, 0x72, 0x70, 0x63, + 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x3b, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_rpc_common_service_proto_rawDescOnce sync.Once + file_rpc_common_service_proto_rawDescData = file_rpc_common_service_proto_rawDesc +) + +func file_rpc_common_service_proto_rawDescGZIP() []byte { + file_rpc_common_service_proto_rawDescOnce.Do(func() { + file_rpc_common_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_rpc_common_service_proto_rawDescData) + }) + return file_rpc_common_service_proto_rawDescData +} + +var file_rpc_common_service_proto_enumTypes = make([]protoimpl.EnumInfo, 3) +var file_rpc_common_service_proto_msgTypes = make([]protoimpl.MessageInfo, 30) +var file_rpc_common_service_proto_goTypes = []interface{}{ + (Severity)(0), // 0: trivy.common.Severity + (LicenseCategory_Enum)(0), // 1: trivy.common.LicenseCategory.Enum + (LicenseType_Enum)(0), // 2: trivy.common.LicenseType.Enum + (*OS)(nil), // 3: trivy.common.OS + (*Repository)(nil), // 4: trivy.common.Repository + (*PackageInfo)(nil), // 5: trivy.common.PackageInfo + (*Application)(nil), // 6: trivy.common.Application + (*Package)(nil), // 7: trivy.common.Package + (*PkgIdentifier)(nil), // 8: trivy.common.PkgIdentifier + (*Location)(nil), // 9: trivy.common.Location + (*BuildInfo)(nil), // 10: trivy.common.BuildInfo + (*Misconfiguration)(nil), // 11: trivy.common.Misconfiguration + (*MisconfResult)(nil), // 12: trivy.common.MisconfResult + (*PolicyMetadata)(nil), // 13: trivy.common.PolicyMetadata + (*DetectedMisconfiguration)(nil), // 14: trivy.common.DetectedMisconfiguration + (*Vulnerability)(nil), // 15: trivy.common.Vulnerability + (*DataSource)(nil), // 16: trivy.common.DataSource + (*Layer)(nil), // 17: trivy.common.Layer + (*CauseMetadata)(nil), // 18: trivy.common.CauseMetadata + (*CVSS)(nil), // 19: trivy.common.CVSS + (*CustomResource)(nil), // 20: trivy.common.CustomResource + (*Line)(nil), // 21: trivy.common.Line + (*Code)(nil), // 22: trivy.common.Code + (*RenderedCause)(nil), // 23: trivy.common.RenderedCause + (*SecretFinding)(nil), // 24: trivy.common.SecretFinding + (*Secret)(nil), // 25: trivy.common.Secret + (*DetectedLicense)(nil), // 26: trivy.common.DetectedLicense + (*LicenseFile)(nil), // 27: trivy.common.LicenseFile + (*LicenseFinding)(nil), // 28: trivy.common.LicenseFinding + (*LicenseCategory)(nil), // 29: trivy.common.LicenseCategory + (*LicenseType)(nil), // 30: trivy.common.LicenseType + nil, // 31: trivy.common.Vulnerability.CvssEntry + nil, // 32: trivy.common.Vulnerability.VendorSeverityEntry + (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp + (*structpb.Value)(nil), // 34: google.protobuf.Value +} +var file_rpc_common_service_proto_depIdxs = []int32{ + 7, // 0: trivy.common.PackageInfo.packages:type_name -> trivy.common.Package + 7, // 1: trivy.common.Application.packages:type_name -> trivy.common.Package + 8, // 2: trivy.common.Package.identifier:type_name -> trivy.common.PkgIdentifier + 9, // 3: trivy.common.Package.locations:type_name -> trivy.common.Location + 17, // 4: trivy.common.Package.layer:type_name -> trivy.common.Layer + 12, // 5: trivy.common.Misconfiguration.successes:type_name -> trivy.common.MisconfResult + 12, // 6: trivy.common.Misconfiguration.warnings:type_name -> trivy.common.MisconfResult + 12, // 7: trivy.common.Misconfiguration.failures:type_name -> trivy.common.MisconfResult + 13, // 8: trivy.common.MisconfResult.policy_metadata:type_name -> trivy.common.PolicyMetadata + 18, // 9: trivy.common.MisconfResult.cause_metadata:type_name -> trivy.common.CauseMetadata + 0, // 10: trivy.common.DetectedMisconfiguration.severity:type_name -> trivy.common.Severity + 17, // 11: trivy.common.DetectedMisconfiguration.layer:type_name -> trivy.common.Layer + 18, // 12: trivy.common.DetectedMisconfiguration.cause_metadata:type_name -> trivy.common.CauseMetadata + 0, // 13: trivy.common.Vulnerability.severity:type_name -> trivy.common.Severity + 8, // 14: trivy.common.Vulnerability.pkg_identifier:type_name -> trivy.common.PkgIdentifier + 17, // 15: trivy.common.Vulnerability.layer:type_name -> trivy.common.Layer + 31, // 16: trivy.common.Vulnerability.cvss:type_name -> trivy.common.Vulnerability.CvssEntry + 33, // 17: trivy.common.Vulnerability.published_date:type_name -> google.protobuf.Timestamp + 33, // 18: trivy.common.Vulnerability.last_modified_date:type_name -> google.protobuf.Timestamp + 34, // 19: trivy.common.Vulnerability.custom_advisory_data:type_name -> google.protobuf.Value + 34, // 20: trivy.common.Vulnerability.custom_vuln_data:type_name -> google.protobuf.Value + 16, // 21: trivy.common.Vulnerability.data_source:type_name -> trivy.common.DataSource + 32, // 22: trivy.common.Vulnerability.vendor_severity:type_name -> trivy.common.Vulnerability.VendorSeverityEntry + 22, // 23: trivy.common.CauseMetadata.code:type_name -> trivy.common.Code + 23, // 24: trivy.common.CauseMetadata.rendered_cause:type_name -> trivy.common.RenderedCause + 17, // 25: trivy.common.CustomResource.layer:type_name -> trivy.common.Layer + 34, // 26: trivy.common.CustomResource.data:type_name -> google.protobuf.Value + 21, // 27: trivy.common.Code.lines:type_name -> trivy.common.Line + 22, // 28: trivy.common.SecretFinding.code:type_name -> trivy.common.Code + 17, // 29: trivy.common.SecretFinding.layer:type_name -> trivy.common.Layer + 24, // 30: trivy.common.Secret.findings:type_name -> trivy.common.SecretFinding + 0, // 31: trivy.common.DetectedLicense.severity:type_name -> trivy.common.Severity + 1, // 32: trivy.common.DetectedLicense.category:type_name -> trivy.common.LicenseCategory.Enum + 2, // 33: trivy.common.LicenseFile.license_type:type_name -> trivy.common.LicenseType.Enum + 28, // 34: trivy.common.LicenseFile.fingings:type_name -> trivy.common.LicenseFinding + 17, // 35: trivy.common.LicenseFile.layer:type_name -> trivy.common.Layer + 1, // 36: trivy.common.LicenseFinding.category:type_name -> trivy.common.LicenseCategory.Enum + 19, // 37: trivy.common.Vulnerability.CvssEntry.value:type_name -> trivy.common.CVSS + 0, // 38: trivy.common.Vulnerability.VendorSeverityEntry.value:type_name -> trivy.common.Severity + 39, // [39:39] is the sub-list for method output_type + 39, // [39:39] is the sub-list for method input_type + 39, // [39:39] is the sub-list for extension type_name + 39, // [39:39] is the sub-list for extension extendee + 0, // [0:39] is the sub-list for field type_name +} + +func init() { file_rpc_common_service_proto_init() } +func file_rpc_common_service_proto_init() { + if File_rpc_common_service_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_rpc_common_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OS); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Repository); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PackageInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Application); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Package); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PkgIdentifier); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Location); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BuildInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Misconfiguration); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*MisconfResult); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PolicyMetadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DetectedMisconfiguration); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Vulnerability); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DataSource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Layer); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CauseMetadata); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CVSS); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CustomResource); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Line); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Code); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RenderedCause); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SecretFinding); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Secret); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*DetectedLicense); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LicenseFile); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LicenseFinding); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LicenseCategory); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_common_service_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*LicenseType); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_rpc_common_service_proto_rawDesc, + NumEnums: 3, + NumMessages: 30, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_rpc_common_service_proto_goTypes, + DependencyIndexes: file_rpc_common_service_proto_depIdxs, + EnumInfos: file_rpc_common_service_proto_enumTypes, + MessageInfos: file_rpc_common_service_proto_msgTypes, + }.Build() + File_rpc_common_service_proto = out.File + file_rpc_common_service_proto_rawDesc = nil + file_rpc_common_service_proto_goTypes = nil + file_rpc_common_service_proto_depIdxs = nil +} diff --git a/rpc/common/service.proto b/rpc/common/service.proto new file mode 100644 index 000000000000..edbd12d29b24 --- /dev/null +++ b/rpc/common/service.proto @@ -0,0 +1,291 @@ +syntax = "proto3"; + +package trivy.common; + +import "google/protobuf/struct.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "github.com/aquasecurity/trivy/rpc/common;common"; + +message OS { + string family = 1; + string name = 2; + bool eosl = 3; + bool extended = 4; +} + +message Repository { + string family = 1; + string release = 2; +} + +message PackageInfo { + string file_path = 1; + repeated Package packages = 2; +} + +message Application { + string type = 1; + string file_path = 2; + repeated Package packages = 3; +} + +message Package { + // binary package + // e.g. bind-utils + string id = 13; + string name = 1; + string version = 2; + string release = 3; + int32 epoch = 4; + PkgIdentifier identifier = 19; + string arch = 5; + // src package containing some binary packages + // e.g. bind + string src_name = 6; + string src_version = 7; + string src_release = 8; + int32 src_epoch = 9; + repeated string licenses = 15; + repeated Location locations = 20; + Layer layer = 11; + string file_path = 12; + repeated string depends_on = 14; + string digest = 16; + bool dev = 17; + bool indirect = 18; + string maintainer = 21; + int32 relationship = 22; + string analyzed_by = 23; +} + +message PkgIdentifier { + string purl = 1; + string bom_ref = 2; + string uid = 3; +} + +message Location { + int32 start_line = 1; + int32 end_line = 2; +} + +message BuildInfo { + repeated string content_sets = 1; + string nvr = 2; + string arch = 3; +} + +message Misconfiguration { + string file_type = 1; + string file_path = 2; + repeated MisconfResult successes = 3; + repeated MisconfResult warnings = 4; + repeated MisconfResult failures = 5; + + reserved 6; // deprecated 'exceptions' +} + +message MisconfResult { + string namespace = 1; + string message = 2; + reserved 3 to 6; + reserved "type", "id", "title", "severity"; + PolicyMetadata policy_metadata = 7; + CauseMetadata cause_metadata = 8; +} + +message PolicyMetadata { + string id = 1; + string adv_id = 2; + string type = 3; + string title = 4; + string description = 5; + string severity = 6; + string recommended_actions = 7; + repeated string references = 8; +} + +message DetectedMisconfiguration { + string type = 1; + string id = 2; + string title = 3; + string description = 4; + string message = 5; + string namespace = 6; + string resolution = 7; + Severity severity = 8; + string primary_url = 9; + repeated string references = 10; + string status = 11; + Layer layer = 12; + CauseMetadata cause_metadata = 13; + string avd_id = 14 [deprecated = true]; + string query = 15; + repeated string aliases = 16; +} + +message Vulnerability { + string vulnerability_id = 1; + string pkg_name = 2; + string installed_version = 3; + string fixed_version = 4; + string title = 5; + string description = 6; + Severity severity = 7; + repeated string references = 8; + PkgIdentifier pkg_identifier = 25; + Layer layer = 10; + string severity_source = 11; + map cvss = 12; + repeated string cwe_ids = 13; + string primary_url = 14; + google.protobuf.Timestamp published_date = 15; + google.protobuf.Timestamp last_modified_date = 16; + google.protobuf.Value custom_advisory_data = 17; + google.protobuf.Value custom_vuln_data = 18; + repeated string vendor_ids = 19; + DataSource data_source = 20; + map vendor_severity = 21; + string pkg_path = 22; + string pkg_id = 23; + int32 status = 24; +} + +message DataSource { + string id = 1; + string name = 2; + string url = 3; +} + +message Layer { + string digest = 1; + string diff_id = 2; + string created_by = 3; + int64 size = 4; +} + +message CauseMetadata { + string resource = 1; + string provider = 2; + string service = 3; + int32 start_line = 4; + int32 end_line = 5; + Code code = 6; + RenderedCause rendered_cause = 7; +} + +enum Severity { + UNKNOWN = 0; + LOW = 1; + MEDIUM = 2; + HIGH = 3; + CRITICAL = 4; +} + +message CVSS { + string v2_vector = 1; + string v3_vector = 2; + double v2_score = 3; + double v3_score = 4; + string v40_vector = 5; + double v40_score = 6; +} + +message CustomResource { + string type = 1; + string file_path = 2; + Layer layer = 3; + google.protobuf.Value data = 4; +} + +message Line { + int32 number = 1; + string content = 2; + bool is_cause = 3; + string annotation = 4; + bool truncated = 5; + string highlighted = 6; + bool first_cause = 7; + bool last_cause = 8; +} + +message Code { + repeated Line lines = 1; +} + +message RenderedCause { + string raw = 1; + string highlighted = 2; +} + +message SecretFinding { + string rule_id = 1; + string category = 2; + string severity = 3; + string title = 4; + int32 start_line = 5; + int32 end_line = 6; + Code code = 7; + string match = 8; + Layer layer = 10; + int32 offset = 11; + + reserved 9; // deprecated 'deleted' +} + +message Secret { + string filepath = 1; + repeated SecretFinding findings = 2; +} + +message DetectedLicense { + Severity severity = 1; + LicenseCategory.Enum category = 2; + string pkg_name = 3; + string file_path = 4; + string name = 5; + float confidence = 6; + string link = 7; + string text = 8; +} + +message LicenseFile { + LicenseType.Enum license_type = 1; + string file_path = 2; + string pkg_name = 3; + repeated LicenseFinding fingings = 4; + Layer layer = 5; +} + +message LicenseFinding { + LicenseCategory.Enum category = 1; + string name = 2; + float confidence = 3; + string link = 4; +} + +// Enumerations are wrapped with a message to improve the readability of +// enumerations in generated code and avoid name conflicts. +// https://github.com/golang/protobuf/issues/513 +message LicenseCategory { + enum Enum { + UNSPECIFIED = 0; + FORBIDDEN = 1; + RESTRICTED = 2; + RECIPROCAL = 3; + NOTICE = 4; + PERMISSIVE = 5; + UNENCUMBERED = 6; + UNKNOWN = 7; + } +} + +message LicenseType { + enum Enum { + UNSPECIFIED = 0; + DPKG = 1; + HEADER = 2; + LICENSE_FILE = 3; + } +} diff --git a/rpc/scanner/service.pb.go b/rpc/scanner/service.pb.go new file mode 100644 index 000000000000..ed28691f48e6 --- /dev/null +++ b/rpc/scanner/service.pb.go @@ -0,0 +1,659 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.0 +// protoc (unknown) +// source: rpc/scanner/service.proto + +package scanner + +import ( + common "github.com/aquasecurity/trivy/rpc/common" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ScanRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Target string `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` // image name or tar file path + ArtifactId string `protobuf:"bytes,2,opt,name=artifact_id,json=artifactId,proto3" json:"artifact_id,omitempty"` + BlobIds []string `protobuf:"bytes,3,rep,name=blob_ids,json=blobIds,proto3" json:"blob_ids,omitempty"` + Options *ScanOptions `protobuf:"bytes,4,opt,name=options,proto3" json:"options,omitempty"` +} + +func (x *ScanRequest) Reset() { + *x = ScanRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_scanner_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ScanRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScanRequest) ProtoMessage() {} + +func (x *ScanRequest) ProtoReflect() protoreflect.Message { + mi := &file_rpc_scanner_service_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScanRequest.ProtoReflect.Descriptor instead. +func (*ScanRequest) Descriptor() ([]byte, []int) { + return file_rpc_scanner_service_proto_rawDescGZIP(), []int{0} +} + +func (x *ScanRequest) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +func (x *ScanRequest) GetArtifactId() string { + if x != nil { + return x.ArtifactId + } + return "" +} + +func (x *ScanRequest) GetBlobIds() []string { + if x != nil { + return x.BlobIds + } + return nil +} + +func (x *ScanRequest) GetOptions() *ScanOptions { + if x != nil { + return x.Options + } + return nil +} + +// cf. +// https://stackoverflow.com/questions/38886789/protobuf3-how-to-describe-map-of-repeated-string +type Licenses struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Names []string `protobuf:"bytes,1,rep,name=names,proto3" json:"names,omitempty"` +} + +func (x *Licenses) Reset() { + *x = Licenses{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_scanner_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Licenses) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Licenses) ProtoMessage() {} + +func (x *Licenses) ProtoReflect() protoreflect.Message { + mi := &file_rpc_scanner_service_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Licenses.ProtoReflect.Descriptor instead. +func (*Licenses) Descriptor() ([]byte, []int) { + return file_rpc_scanner_service_proto_rawDescGZIP(), []int{1} +} + +func (x *Licenses) GetNames() []string { + if x != nil { + return x.Names + } + return nil +} + +type ScanOptions struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PkgTypes []string `protobuf:"bytes,1,rep,name=pkg_types,json=pkgTypes,proto3" json:"pkg_types,omitempty"` + Scanners []string `protobuf:"bytes,2,rep,name=scanners,proto3" json:"scanners,omitempty"` + LicenseCategories map[string]*Licenses `protobuf:"bytes,4,rep,name=license_categories,json=licenseCategories,proto3" json:"license_categories,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + IncludeDevDeps bool `protobuf:"varint,5,opt,name=include_dev_deps,json=includeDevDeps,proto3" json:"include_dev_deps,omitempty"` + PkgRelationships []string `protobuf:"bytes,6,rep,name=pkg_relationships,json=pkgRelationships,proto3" json:"pkg_relationships,omitempty"` + Distro *common.OS `protobuf:"bytes,7,opt,name=distro,proto3" json:"distro,omitempty"` + VulnSeveritySources []string `protobuf:"bytes,8,rep,name=vuln_severity_sources,json=vulnSeveritySources,proto3" json:"vuln_severity_sources,omitempty"` + LicenseFull bool `protobuf:"varint,9,opt,name=license_full,json=licenseFull,proto3" json:"license_full,omitempty"` +} + +func (x *ScanOptions) Reset() { + *x = ScanOptions{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_scanner_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ScanOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScanOptions) ProtoMessage() {} + +func (x *ScanOptions) ProtoReflect() protoreflect.Message { + mi := &file_rpc_scanner_service_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScanOptions.ProtoReflect.Descriptor instead. +func (*ScanOptions) Descriptor() ([]byte, []int) { + return file_rpc_scanner_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ScanOptions) GetPkgTypes() []string { + if x != nil { + return x.PkgTypes + } + return nil +} + +func (x *ScanOptions) GetScanners() []string { + if x != nil { + return x.Scanners + } + return nil +} + +func (x *ScanOptions) GetLicenseCategories() map[string]*Licenses { + if x != nil { + return x.LicenseCategories + } + return nil +} + +func (x *ScanOptions) GetIncludeDevDeps() bool { + if x != nil { + return x.IncludeDevDeps + } + return false +} + +func (x *ScanOptions) GetPkgRelationships() []string { + if x != nil { + return x.PkgRelationships + } + return nil +} + +func (x *ScanOptions) GetDistro() *common.OS { + if x != nil { + return x.Distro + } + return nil +} + +func (x *ScanOptions) GetVulnSeveritySources() []string { + if x != nil { + return x.VulnSeveritySources + } + return nil +} + +func (x *ScanOptions) GetLicenseFull() bool { + if x != nil { + return x.LicenseFull + } + return false +} + +type ScanResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Os *common.OS `protobuf:"bytes,1,opt,name=os,proto3" json:"os,omitempty"` + Results []*Result `protobuf:"bytes,3,rep,name=results,proto3" json:"results,omitempty"` + Layers []*common.Layer `protobuf:"bytes,4,rep,name=layers,proto3" json:"layers,omitempty"` +} + +func (x *ScanResponse) Reset() { + *x = ScanResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_scanner_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ScanResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ScanResponse) ProtoMessage() {} + +func (x *ScanResponse) ProtoReflect() protoreflect.Message { + mi := &file_rpc_scanner_service_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ScanResponse.ProtoReflect.Descriptor instead. +func (*ScanResponse) Descriptor() ([]byte, []int) { + return file_rpc_scanner_service_proto_rawDescGZIP(), []int{3} +} + +func (x *ScanResponse) GetOs() *common.OS { + if x != nil { + return x.Os + } + return nil +} + +func (x *ScanResponse) GetResults() []*Result { + if x != nil { + return x.Results + } + return nil +} + +func (x *ScanResponse) GetLayers() []*common.Layer { + if x != nil { + return x.Layers + } + return nil +} + +// Result is the same as github.com/aquasecurity/trivy/pkg/report.Result +type Result struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Target string `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` + Vulnerabilities []*common.Vulnerability `protobuf:"bytes,2,rep,name=vulnerabilities,proto3" json:"vulnerabilities,omitempty"` + Misconfigurations []*common.DetectedMisconfiguration `protobuf:"bytes,4,rep,name=misconfigurations,proto3" json:"misconfigurations,omitempty"` + Class string `protobuf:"bytes,6,opt,name=class,proto3" json:"class,omitempty"` + Type string `protobuf:"bytes,3,opt,name=type,proto3" json:"type,omitempty"` + Packages []*common.Package `protobuf:"bytes,5,rep,name=packages,proto3" json:"packages,omitempty"` + CustomResources []*common.CustomResource `protobuf:"bytes,7,rep,name=custom_resources,json=customResources,proto3" json:"custom_resources,omitempty"` + Secrets []*common.SecretFinding `protobuf:"bytes,8,rep,name=secrets,proto3" json:"secrets,omitempty"` + Licenses []*common.DetectedLicense `protobuf:"bytes,9,rep,name=licenses,proto3" json:"licenses,omitempty"` +} + +func (x *Result) Reset() { + *x = Result{} + if protoimpl.UnsafeEnabled { + mi := &file_rpc_scanner_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Result) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Result) ProtoMessage() {} + +func (x *Result) ProtoReflect() protoreflect.Message { + mi := &file_rpc_scanner_service_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Result.ProtoReflect.Descriptor instead. +func (*Result) Descriptor() ([]byte, []int) { + return file_rpc_scanner_service_proto_rawDescGZIP(), []int{4} +} + +func (x *Result) GetTarget() string { + if x != nil { + return x.Target + } + return "" +} + +func (x *Result) GetVulnerabilities() []*common.Vulnerability { + if x != nil { + return x.Vulnerabilities + } + return nil +} + +func (x *Result) GetMisconfigurations() []*common.DetectedMisconfiguration { + if x != nil { + return x.Misconfigurations + } + return nil +} + +func (x *Result) GetClass() string { + if x != nil { + return x.Class + } + return "" +} + +func (x *Result) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Result) GetPackages() []*common.Package { + if x != nil { + return x.Packages + } + return nil +} + +func (x *Result) GetCustomResources() []*common.CustomResource { + if x != nil { + return x.CustomResources + } + return nil +} + +func (x *Result) GetSecrets() []*common.SecretFinding { + if x != nil { + return x.Secrets + } + return nil +} + +func (x *Result) GetLicenses() []*common.DetectedLicense { + if x != nil { + return x.Licenses + } + return nil +} + +var File_rpc_scanner_service_proto protoreflect.FileDescriptor + +var file_rpc_scanner_service_proto_rawDesc = []byte{ + 0x0a, 0x19, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2f, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x74, 0x72, 0x69, + 0x76, 0x79, 0x2e, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x1a, 0x18, 0x72, + 0x70, 0x63, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9a, 0x01, 0x0a, 0x0b, 0x53, 0x63, 0x61, 0x6e, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, + 0x1f, 0x0a, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x49, 0x64, + 0x12, 0x19, 0x0a, 0x08, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x69, 0x64, 0x73, 0x18, 0x03, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x07, 0x62, 0x6c, 0x6f, 0x62, 0x49, 0x64, 0x73, 0x12, 0x37, 0x0a, 0x07, 0x6f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, + 0x72, 0x69, 0x76, 0x79, 0x2e, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x63, 0x61, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x07, 0x6f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x20, 0x0a, 0x08, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, + 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, + 0x05, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x22, 0xeb, 0x03, 0x0a, 0x0b, 0x53, 0x63, 0x61, 0x6e, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x6b, 0x67, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6b, 0x67, 0x54, 0x79, + 0x70, 0x65, 0x73, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x73, 0x12, + 0x63, 0x0a, 0x12, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, + 0x6f, 0x72, 0x69, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x74, 0x72, + 0x69, 0x76, 0x79, 0x2e, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, + 0x63, 0x61, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x4c, 0x69, 0x63, 0x65, 0x6e, + 0x73, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x11, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, + 0x72, 0x69, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, + 0x64, 0x65, 0x76, 0x5f, 0x64, 0x65, 0x70, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, + 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x44, 0x65, 0x76, 0x44, 0x65, 0x70, 0x73, 0x12, 0x2b, + 0x0a, 0x11, 0x70, 0x6b, 0x67, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, + 0x69, 0x70, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x70, 0x6b, 0x67, 0x52, 0x65, + 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x68, 0x69, 0x70, 0x73, 0x12, 0x28, 0x0a, 0x06, 0x64, + 0x69, 0x73, 0x74, 0x72, 0x6f, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x72, + 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4f, 0x53, 0x52, 0x06, 0x64, + 0x69, 0x73, 0x74, 0x72, 0x6f, 0x12, 0x32, 0x0a, 0x15, 0x76, 0x75, 0x6c, 0x6e, 0x5f, 0x73, 0x65, + 0x76, 0x65, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x08, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x13, 0x76, 0x75, 0x6c, 0x6e, 0x53, 0x65, 0x76, 0x65, 0x72, 0x69, + 0x74, 0x79, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x6c, 0x69, 0x63, + 0x65, 0x6e, 0x73, 0x65, 0x5f, 0x66, 0x75, 0x6c, 0x6c, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x0b, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x46, 0x75, 0x6c, 0x6c, 0x1a, 0x60, 0x0a, 0x16, + 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x69, 0x65, + 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, + 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x63, 0x65, 0x6e, + 0x73, 0x65, 0x73, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x4a, 0x04, + 0x08, 0x03, 0x10, 0x04, 0x22, 0x91, 0x01, 0x0a, 0x0c, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x20, 0x0a, 0x02, 0x6f, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x10, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x4f, 0x53, 0x52, 0x02, 0x6f, 0x73, 0x12, 0x32, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, + 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, + 0x2e, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x73, 0x75, + 0x6c, 0x74, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x2b, 0x0a, 0x06, 0x6c, + 0x61, 0x79, 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x74, 0x72, + 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x61, 0x79, 0x65, 0x72, + 0x52, 0x06, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x22, 0xd5, 0x03, 0x0a, 0x06, 0x52, 0x65, 0x73, + 0x75, 0x6c, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x45, 0x0a, 0x0f, 0x76, + 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x56, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, + 0x79, 0x52, 0x0f, 0x76, 0x75, 0x6c, 0x6e, 0x65, 0x72, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, + 0x65, 0x73, 0x12, 0x54, 0x0a, 0x11, 0x6d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, + 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x74, + 0x65, 0x63, 0x74, 0x65, 0x64, 0x4d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x75, 0x72, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x11, 0x6d, 0x69, 0x73, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6c, 0x61, 0x73, + 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x12, 0x31, 0x0a, 0x08, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x52, 0x08, 0x70, 0x61, 0x63, + 0x6b, 0x61, 0x67, 0x65, 0x73, 0x12, 0x47, 0x0a, 0x10, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0f, 0x63, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x12, 0x35, + 0x0a, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x46, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x07, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x39, 0x0a, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, + 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x74, 0x65, 0x63, 0x74, 0x65, 0x64, 0x4c, + 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x52, 0x08, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, + 0x32, 0x50, 0x0a, 0x07, 0x53, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x12, 0x45, 0x0a, 0x04, 0x53, + 0x63, 0x61, 0x6e, 0x12, 0x1d, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x73, 0x63, 0x61, 0x6e, + 0x6e, 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x74, 0x72, 0x69, 0x76, 0x79, 0x2e, 0x73, 0x63, 0x61, 0x6e, 0x6e, + 0x65, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x61, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x42, 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x61, 0x71, 0x75, 0x61, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x2f, 0x74, 0x72, + 0x69, 0x76, 0x79, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x3b, + 0x73, 0x63, 0x61, 0x6e, 0x6e, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_rpc_scanner_service_proto_rawDescOnce sync.Once + file_rpc_scanner_service_proto_rawDescData = file_rpc_scanner_service_proto_rawDesc +) + +func file_rpc_scanner_service_proto_rawDescGZIP() []byte { + file_rpc_scanner_service_proto_rawDescOnce.Do(func() { + file_rpc_scanner_service_proto_rawDescData = protoimpl.X.CompressGZIP(file_rpc_scanner_service_proto_rawDescData) + }) + return file_rpc_scanner_service_proto_rawDescData +} + +var file_rpc_scanner_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_rpc_scanner_service_proto_goTypes = []interface{}{ + (*ScanRequest)(nil), // 0: trivy.scanner.v1.ScanRequest + (*Licenses)(nil), // 1: trivy.scanner.v1.Licenses + (*ScanOptions)(nil), // 2: trivy.scanner.v1.ScanOptions + (*ScanResponse)(nil), // 3: trivy.scanner.v1.ScanResponse + (*Result)(nil), // 4: trivy.scanner.v1.Result + nil, // 5: trivy.scanner.v1.ScanOptions.LicenseCategoriesEntry + (*common.OS)(nil), // 6: trivy.common.OS + (*common.Layer)(nil), // 7: trivy.common.Layer + (*common.Vulnerability)(nil), // 8: trivy.common.Vulnerability + (*common.DetectedMisconfiguration)(nil), // 9: trivy.common.DetectedMisconfiguration + (*common.Package)(nil), // 10: trivy.common.Package + (*common.CustomResource)(nil), // 11: trivy.common.CustomResource + (*common.SecretFinding)(nil), // 12: trivy.common.SecretFinding + (*common.DetectedLicense)(nil), // 13: trivy.common.DetectedLicense +} +var file_rpc_scanner_service_proto_depIdxs = []int32{ + 2, // 0: trivy.scanner.v1.ScanRequest.options:type_name -> trivy.scanner.v1.ScanOptions + 5, // 1: trivy.scanner.v1.ScanOptions.license_categories:type_name -> trivy.scanner.v1.ScanOptions.LicenseCategoriesEntry + 6, // 2: trivy.scanner.v1.ScanOptions.distro:type_name -> trivy.common.OS + 6, // 3: trivy.scanner.v1.ScanResponse.os:type_name -> trivy.common.OS + 4, // 4: trivy.scanner.v1.ScanResponse.results:type_name -> trivy.scanner.v1.Result + 7, // 5: trivy.scanner.v1.ScanResponse.layers:type_name -> trivy.common.Layer + 8, // 6: trivy.scanner.v1.Result.vulnerabilities:type_name -> trivy.common.Vulnerability + 9, // 7: trivy.scanner.v1.Result.misconfigurations:type_name -> trivy.common.DetectedMisconfiguration + 10, // 8: trivy.scanner.v1.Result.packages:type_name -> trivy.common.Package + 11, // 9: trivy.scanner.v1.Result.custom_resources:type_name -> trivy.common.CustomResource + 12, // 10: trivy.scanner.v1.Result.secrets:type_name -> trivy.common.SecretFinding + 13, // 11: trivy.scanner.v1.Result.licenses:type_name -> trivy.common.DetectedLicense + 1, // 12: trivy.scanner.v1.ScanOptions.LicenseCategoriesEntry.value:type_name -> trivy.scanner.v1.Licenses + 0, // 13: trivy.scanner.v1.Scanner.Scan:input_type -> trivy.scanner.v1.ScanRequest + 3, // 14: trivy.scanner.v1.Scanner.Scan:output_type -> trivy.scanner.v1.ScanResponse + 14, // [14:15] is the sub-list for method output_type + 13, // [13:14] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name +} + +func init() { file_rpc_scanner_service_proto_init() } +func file_rpc_scanner_service_proto_init() { + if File_rpc_scanner_service_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_rpc_scanner_service_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ScanRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_scanner_service_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Licenses); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_scanner_service_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ScanOptions); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_scanner_service_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ScanResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_rpc_scanner_service_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Result); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_rpc_scanner_service_proto_rawDesc, + NumEnums: 0, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_rpc_scanner_service_proto_goTypes, + DependencyIndexes: file_rpc_scanner_service_proto_depIdxs, + MessageInfos: file_rpc_scanner_service_proto_msgTypes, + }.Build() + File_rpc_scanner_service_proto = out.File + file_rpc_scanner_service_proto_rawDesc = nil + file_rpc_scanner_service_proto_goTypes = nil + file_rpc_scanner_service_proto_depIdxs = nil +} diff --git a/rpc/scanner/service.proto b/rpc/scanner/service.proto new file mode 100644 index 000000000000..e91228839fec --- /dev/null +++ b/rpc/scanner/service.proto @@ -0,0 +1,56 @@ +syntax = "proto3"; + +package trivy.scanner.v1; + +import "rpc/common/service.proto"; + +option go_package = "github.com/aquasecurity/trivy/rpc/scanner;scanner"; + +service Scanner { + rpc Scan(ScanRequest) returns (ScanResponse); +} + +message ScanRequest { + string target = 1; // image name or tar file path + string artifact_id = 2; + repeated string blob_ids = 3; + ScanOptions options = 4; +} + +// cf. +// https://stackoverflow.com/questions/38886789/protobuf3-how-to-describe-map-of-repeated-string +message Licenses { + repeated string names = 1; +} + +message ScanOptions { + repeated string pkg_types = 1; + repeated string scanners = 2; + map license_categories = 4; + bool include_dev_deps = 5; + repeated string pkg_relationships = 6; + common.OS distro = 7; + repeated string vuln_severity_sources = 8; + bool license_full = 9; + + reserved 3; // deleted 'list_all_packages' +} + +message ScanResponse { + common.OS os = 1; + repeated Result results = 3; + repeated common.Layer layers = 4; +} + +// Result is the same as github.com/aquasecurity/trivy/pkg/report.Result +message Result { + string target = 1; + repeated common.Vulnerability vulnerabilities = 2; + repeated common.DetectedMisconfiguration misconfigurations = 4; + string class = 6; + string type = 3; + repeated common.Package packages = 5; + repeated common.CustomResource custom_resources = 7; + repeated common.SecretFinding secrets = 8; + repeated common.DetectedLicense licenses = 9; +} diff --git a/rpc/scanner/service.twirp.go b/rpc/scanner/service.twirp.go new file mode 100644 index 000000000000..32db8a23b385 --- /dev/null +++ b/rpc/scanner/service.twirp.go @@ -0,0 +1,1138 @@ +// Code generated by protoc-gen-twirp v8.1.3, DO NOT EDIT. +// source: rpc/scanner/service.proto + +package scanner + +import context "context" +import fmt "fmt" +import http "net/http" +import io "io" +import json "encoding/json" +import strconv "strconv" +import strings "strings" + +import protojson "google.golang.org/protobuf/encoding/protojson" +import proto "google.golang.org/protobuf/proto" +import twirp "github.com/twitchtv/twirp" +import ctxsetters "github.com/twitchtv/twirp/ctxsetters" + +import bytes "bytes" +import errors "errors" +import path "path" +import url "net/url" + +// Version compatibility assertion. +// If the constant is not defined in the package, that likely means +// the package needs to be updated to work with this generated code. +// See https://twitchtv.github.io/twirp/docs/version_matrix.html +const _ = twirp.TwirpPackageMinVersion_8_1_0 + +// ================= +// Scanner Interface +// ================= + +type Scanner interface { + Scan(context.Context, *ScanRequest) (*ScanResponse, error) +} + +// ======================= +// Scanner Protobuf Client +// ======================= + +type scannerProtobufClient struct { + client HTTPClient + urls [1]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewScannerProtobufClient creates a Protobuf client that implements the Scanner interface. +// It communicates using Protobuf and can be configured with a custom HTTPClient. +func NewScannerProtobufClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Scanner { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "trivy.scanner.v1", "Scanner") + urls := [1]string{ + serviceURL + "Scan", + } + + return &scannerProtobufClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *scannerProtobufClient) Scan(ctx context.Context, in *ScanRequest) (*ScanResponse, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.scanner.v1") + ctx = ctxsetters.WithServiceName(ctx, "Scanner") + ctx = ctxsetters.WithMethodName(ctx, "Scan") + caller := c.callScan + if c.interceptor != nil { + caller = func(ctx context.Context, req *ScanRequest) (*ScanResponse, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ScanRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ScanRequest) when calling interceptor") + } + return c.callScan(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ScanResponse) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ScanResponse) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *scannerProtobufClient) callScan(ctx context.Context, in *ScanRequest) (*ScanResponse, error) { + out := new(ScanResponse) + ctx, err := doProtobufRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// =================== +// Scanner JSON Client +// =================== + +type scannerJSONClient struct { + client HTTPClient + urls [1]string + interceptor twirp.Interceptor + opts twirp.ClientOptions +} + +// NewScannerJSONClient creates a JSON client that implements the Scanner interface. +// It communicates using JSON and can be configured with a custom HTTPClient. +func NewScannerJSONClient(baseURL string, client HTTPClient, opts ...twirp.ClientOption) Scanner { + if c, ok := client.(*http.Client); ok { + client = withoutRedirects(c) + } + + clientOpts := twirp.ClientOptions{} + for _, o := range opts { + o(&clientOpts) + } + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + literalURLs := false + _ = clientOpts.ReadOpt("literalURLs", &literalURLs) + var pathPrefix string + if ok := clientOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + // Build method URLs: []/./ + serviceURL := sanitizeBaseURL(baseURL) + serviceURL += baseServicePath(pathPrefix, "trivy.scanner.v1", "Scanner") + urls := [1]string{ + serviceURL + "Scan", + } + + return &scannerJSONClient{ + client: client, + urls: urls, + interceptor: twirp.ChainInterceptors(clientOpts.Interceptors...), + opts: clientOpts, + } +} + +func (c *scannerJSONClient) Scan(ctx context.Context, in *ScanRequest) (*ScanResponse, error) { + ctx = ctxsetters.WithPackageName(ctx, "trivy.scanner.v1") + ctx = ctxsetters.WithServiceName(ctx, "Scanner") + ctx = ctxsetters.WithMethodName(ctx, "Scan") + caller := c.callScan + if c.interceptor != nil { + caller = func(ctx context.Context, req *ScanRequest) (*ScanResponse, error) { + resp, err := c.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ScanRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ScanRequest) when calling interceptor") + } + return c.callScan(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ScanResponse) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ScanResponse) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + return caller(ctx, in) +} + +func (c *scannerJSONClient) callScan(ctx context.Context, in *ScanRequest) (*ScanResponse, error) { + out := new(ScanResponse) + ctx, err := doJSONRequest(ctx, c.client, c.opts.Hooks, c.urls[0], in, out) + if err != nil { + twerr, ok := err.(twirp.Error) + if !ok { + twerr = twirp.InternalErrorWith(err) + } + callClientError(ctx, c.opts.Hooks, twerr) + return nil, err + } + + callClientResponseReceived(ctx, c.opts.Hooks) + + return out, nil +} + +// ====================== +// Scanner Server Handler +// ====================== + +type scannerServer struct { + Scanner + interceptor twirp.Interceptor + hooks *twirp.ServerHooks + pathPrefix string // prefix for routing + jsonSkipDefaults bool // do not include unpopulated fields (default values) in the response + jsonCamelCase bool // JSON fields are serialized as lowerCamelCase rather than keeping the original proto names +} + +// NewScannerServer builds a TwirpServer that can be used as an http.Handler to handle +// HTTP requests that are routed to the right method in the provided svc implementation. +// The opts are twirp.ServerOption modifiers, for example twirp.WithServerHooks(hooks). +func NewScannerServer(svc Scanner, opts ...interface{}) TwirpServer { + serverOpts := newServerOpts(opts) + + // Using ReadOpt allows backwards and forwards compatibility with new options in the future + jsonSkipDefaults := false + _ = serverOpts.ReadOpt("jsonSkipDefaults", &jsonSkipDefaults) + jsonCamelCase := false + _ = serverOpts.ReadOpt("jsonCamelCase", &jsonCamelCase) + var pathPrefix string + if ok := serverOpts.ReadOpt("pathPrefix", &pathPrefix); !ok { + pathPrefix = "/twirp" // default prefix + } + + return &scannerServer{ + Scanner: svc, + hooks: serverOpts.Hooks, + interceptor: twirp.ChainInterceptors(serverOpts.Interceptors...), + pathPrefix: pathPrefix, + jsonSkipDefaults: jsonSkipDefaults, + jsonCamelCase: jsonCamelCase, + } +} + +// writeError writes an HTTP response with a valid Twirp error format, and triggers hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func (s *scannerServer) writeError(ctx context.Context, resp http.ResponseWriter, err error) { + writeError(ctx, resp, err, s.hooks) +} + +// handleRequestBodyError is used to handle error when the twirp server cannot read request +func (s *scannerServer) handleRequestBodyError(ctx context.Context, resp http.ResponseWriter, msg string, err error) { + if context.Canceled == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.Canceled, "failed to read request: context canceled")) + return + } + if context.DeadlineExceeded == ctx.Err() { + s.writeError(ctx, resp, twirp.NewError(twirp.DeadlineExceeded, "failed to read request: deadline exceeded")) + return + } + s.writeError(ctx, resp, twirp.WrapError(malformedRequestError(msg), err)) +} + +// ScannerPathPrefix is a convenience constant that may identify URL paths. +// Should be used with caution, it only matches routes generated by Twirp Go clients, +// with the default "/twirp" prefix and default CamelCase service and method names. +// More info: https://twitchtv.github.io/twirp/docs/routing.html +const ScannerPathPrefix = "/twirp/trivy.scanner.v1.Scanner/" + +func (s *scannerServer) ServeHTTP(resp http.ResponseWriter, req *http.Request) { + ctx := req.Context() + ctx = ctxsetters.WithPackageName(ctx, "trivy.scanner.v1") + ctx = ctxsetters.WithServiceName(ctx, "Scanner") + ctx = ctxsetters.WithResponseWriter(ctx, resp) + + var err error + ctx, err = callRequestReceived(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + if req.Method != "POST" { + msg := fmt.Sprintf("unsupported method %q (only POST is allowed)", req.Method) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + // Verify path format: []/./ + prefix, pkgService, method := parseTwirpPath(req.URL.Path) + if pkgService != "trivy.scanner.v1.Scanner" { + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + if prefix != s.pathPrefix { + msg := fmt.Sprintf("invalid path prefix %q, expected %q, on path %q", prefix, s.pathPrefix, req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } + + switch method { + case "Scan": + s.serveScan(ctx, resp, req) + return + default: + msg := fmt.Sprintf("no handler for path %q", req.URL.Path) + s.writeError(ctx, resp, badRouteError(msg, req.Method, req.URL.Path)) + return + } +} + +func (s *scannerServer) serveScan(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + header := req.Header.Get("Content-Type") + i := strings.Index(header, ";") + if i == -1 { + i = len(header) + } + switch strings.TrimSpace(strings.ToLower(header[:i])) { + case "application/json": + s.serveScanJSON(ctx, resp, req) + case "application/protobuf": + s.serveScanProtobuf(ctx, resp, req) + default: + msg := fmt.Sprintf("unexpected Content-Type: %q", req.Header.Get("Content-Type")) + twerr := badRouteError(msg, req.Method, req.URL.Path) + s.writeError(ctx, resp, twerr) + } +} + +func (s *scannerServer) serveScanJSON(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "Scan") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + d := json.NewDecoder(req.Body) + rawReqBody := json.RawMessage{} + if err := d.Decode(&rawReqBody); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + reqContent := new(ScanRequest) + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawReqBody, reqContent); err != nil { + s.handleRequestBodyError(ctx, resp, "the json request could not be decoded", err) + return + } + + handler := s.Scanner.Scan + if s.interceptor != nil { + handler = func(ctx context.Context, req *ScanRequest) (*ScanResponse, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ScanRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ScanRequest) when calling interceptor") + } + return s.Scanner.Scan(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ScanResponse) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ScanResponse) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *ScanResponse + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *ScanResponse and nil error while calling Scan. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + marshaler := &protojson.MarshalOptions{UseProtoNames: !s.jsonCamelCase, EmitUnpopulated: !s.jsonSkipDefaults} + respBytes, err := marshaler.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal json response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/json") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *scannerServer) serveScanProtobuf(ctx context.Context, resp http.ResponseWriter, req *http.Request) { + var err error + ctx = ctxsetters.WithMethodName(ctx, "Scan") + ctx, err = callRequestRouted(ctx, s.hooks) + if err != nil { + s.writeError(ctx, resp, err) + return + } + + buf, err := io.ReadAll(req.Body) + if err != nil { + s.handleRequestBodyError(ctx, resp, "failed to read request body", err) + return + } + reqContent := new(ScanRequest) + if err = proto.Unmarshal(buf, reqContent); err != nil { + s.writeError(ctx, resp, malformedRequestError("the protobuf request could not be decoded")) + return + } + + handler := s.Scanner.Scan + if s.interceptor != nil { + handler = func(ctx context.Context, req *ScanRequest) (*ScanResponse, error) { + resp, err := s.interceptor( + func(ctx context.Context, req interface{}) (interface{}, error) { + typedReq, ok := req.(*ScanRequest) + if !ok { + return nil, twirp.InternalError("failed type assertion req.(*ScanRequest) when calling interceptor") + } + return s.Scanner.Scan(ctx, typedReq) + }, + )(ctx, req) + if resp != nil { + typedResp, ok := resp.(*ScanResponse) + if !ok { + return nil, twirp.InternalError("failed type assertion resp.(*ScanResponse) when calling interceptor") + } + return typedResp, err + } + return nil, err + } + } + + // Call service method + var respContent *ScanResponse + func() { + defer ensurePanicResponses(ctx, resp, s.hooks) + respContent, err = handler(ctx, reqContent) + }() + + if err != nil { + s.writeError(ctx, resp, err) + return + } + if respContent == nil { + s.writeError(ctx, resp, twirp.InternalError("received a nil *ScanResponse and nil error while calling Scan. nil responses are not supported")) + return + } + + ctx = callResponsePrepared(ctx, s.hooks) + + respBytes, err := proto.Marshal(respContent) + if err != nil { + s.writeError(ctx, resp, wrapInternal(err, "failed to marshal proto response")) + return + } + + ctx = ctxsetters.WithStatusCode(ctx, http.StatusOK) + resp.Header().Set("Content-Type", "application/protobuf") + resp.Header().Set("Content-Length", strconv.Itoa(len(respBytes))) + resp.WriteHeader(http.StatusOK) + if n, err := resp.Write(respBytes); err != nil { + msg := fmt.Sprintf("failed to write response, %d of %d bytes written: %s", n, len(respBytes), err.Error()) + twerr := twirp.NewError(twirp.Unknown, msg) + ctx = callError(ctx, s.hooks, twerr) + } + callResponseSent(ctx, s.hooks) +} + +func (s *scannerServer) ServiceDescriptor() ([]byte, int) { + return twirpFileDescriptor0, 0 +} + +func (s *scannerServer) ProtocGenTwirpVersion() string { + return "v8.1.3" +} + +// PathPrefix returns the base service path, in the form: "//./" +// that is everything in a Twirp route except for the . This can be used for routing, +// for example to identify the requests that are targeted to this service in a mux. +func (s *scannerServer) PathPrefix() string { + return baseServicePath(s.pathPrefix, "trivy.scanner.v1", "Scanner") +} + +// ===== +// Utils +// ===== + +// HTTPClient is the interface used by generated clients to send HTTP requests. +// It is fulfilled by *(net/http).Client, which is sufficient for most users. +// Users can provide their own implementation for special retry policies. +// +// HTTPClient implementations should not follow redirects. Redirects are +// automatically disabled if *(net/http).Client is passed to client +// constructors. See the withoutRedirects function in this file for more +// details. +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// TwirpServer is the interface generated server structs will support: they're +// HTTP handlers with additional methods for accessing metadata about the +// service. Those accessors are a low-level API for building reflection tools. +// Most people can think of TwirpServers as just http.Handlers. +type TwirpServer interface { + http.Handler + + // ServiceDescriptor returns gzipped bytes describing the .proto file that + // this service was generated from. Once unzipped, the bytes can be + // unmarshalled as a + // google.golang.org/protobuf/types/descriptorpb.FileDescriptorProto. + // + // The returned integer is the index of this particular service within that + // FileDescriptorProto's 'Service' slice of ServiceDescriptorProtos. This is a + // low-level field, expected to be used for reflection. + ServiceDescriptor() ([]byte, int) + + // ProtocGenTwirpVersion is the semantic version string of the version of + // twirp used to generate this file. + ProtocGenTwirpVersion() string + + // PathPrefix returns the HTTP URL path prefix for all methods handled by this + // service. This can be used with an HTTP mux to route Twirp requests. + // The path prefix is in the form: "//./" + // that is, everything in a Twirp route except for the at the end. + PathPrefix() string +} + +func newServerOpts(opts []interface{}) *twirp.ServerOptions { + serverOpts := &twirp.ServerOptions{} + for _, opt := range opts { + switch o := opt.(type) { + case twirp.ServerOption: + o(serverOpts) + case *twirp.ServerHooks: // backwards compatibility, allow to specify hooks as an argument + twirp.WithServerHooks(o)(serverOpts) + case nil: // backwards compatibility, allow nil value for the argument + continue + default: + panic(fmt.Sprintf("Invalid option type %T, please use a twirp.ServerOption", o)) + } + } + return serverOpts +} + +// WriteError writes an HTTP response with a valid Twirp error format (code, msg, meta). +// Useful outside of the Twirp server (e.g. http middleware), but does not trigger hooks. +// If err is not a twirp.Error, it will get wrapped with twirp.InternalErrorWith(err) +func WriteError(resp http.ResponseWriter, err error) { + writeError(context.Background(), resp, err, nil) +} + +// writeError writes Twirp errors in the response and triggers hooks. +func writeError(ctx context.Context, resp http.ResponseWriter, err error, hooks *twirp.ServerHooks) { + // Convert to a twirp.Error. Non-twirp errors are converted to internal errors. + var twerr twirp.Error + if !errors.As(err, &twerr) { + twerr = twirp.InternalErrorWith(err) + } + + statusCode := twirp.ServerHTTPStatusFromErrorCode(twerr.Code()) + ctx = ctxsetters.WithStatusCode(ctx, statusCode) + ctx = callError(ctx, hooks, twerr) + + respBody := marshalErrorToJSON(twerr) + + resp.Header().Set("Content-Type", "application/json") // Error responses are always JSON + resp.Header().Set("Content-Length", strconv.Itoa(len(respBody))) + resp.WriteHeader(statusCode) // set HTTP status code and send response + + _, writeErr := resp.Write(respBody) + if writeErr != nil { + // We have three options here. We could log the error, call the Error + // hook, or just silently ignore the error. + // + // Logging is unacceptable because we don't have a user-controlled + // logger; writing out to stderr without permission is too rude. + // + // Calling the Error hook would confuse users: it would mean the Error + // hook got called twice for one request, which is likely to lead to + // duplicated log messages and metrics, no matter how well we document + // the behavior. + // + // Silently ignoring the error is our least-bad option. It's highly + // likely that the connection is broken and the original 'err' says + // so anyway. + _ = writeErr + } + + callResponseSent(ctx, hooks) +} + +// sanitizeBaseURL parses the the baseURL, and adds the "http" scheme if needed. +// If the URL is unparsable, the baseURL is returned unchanged. +func sanitizeBaseURL(baseURL string) string { + u, err := url.Parse(baseURL) + if err != nil { + return baseURL // invalid URL will fail later when making requests + } + if u.Scheme == "" { + u.Scheme = "http" + } + return u.String() +} + +// baseServicePath composes the path prefix for the service (without ). +// e.g.: baseServicePath("/twirp", "my.pkg", "MyService") +// +// returns => "/twirp/my.pkg.MyService/" +// +// e.g.: baseServicePath("", "", "MyService") +// +// returns => "/MyService/" +func baseServicePath(prefix, pkg, service string) string { + fullServiceName := service + if pkg != "" { + fullServiceName = pkg + "." + service + } + return path.Join("/", prefix, fullServiceName) + "/" +} + +// parseTwirpPath extracts path components form a valid Twirp route. +// Expected format: "[]/./" +// e.g.: prefix, pkgService, method := parseTwirpPath("/twirp/pkg.Svc/MakeHat") +func parseTwirpPath(path string) (string, string, string) { + parts := strings.Split(path, "/") + if len(parts) < 2 { + return "", "", "" + } + method := parts[len(parts)-1] + pkgService := parts[len(parts)-2] + prefix := strings.Join(parts[0:len(parts)-2], "/") + return prefix, pkgService, method +} + +// getCustomHTTPReqHeaders retrieves a copy of any headers that are set in +// a context through the twirp.WithHTTPRequestHeaders function. +// If there are no headers set, or if they have the wrong type, nil is returned. +func getCustomHTTPReqHeaders(ctx context.Context) http.Header { + header, ok := twirp.HTTPRequestHeaders(ctx) + if !ok || header == nil { + return nil + } + copied := make(http.Header) + for k, vv := range header { + if vv == nil { + copied[k] = nil + continue + } + copied[k] = make([]string, len(vv)) + copy(copied[k], vv) + } + return copied +} + +// newRequest makes an http.Request from a client, adding common headers. +func newRequest(ctx context.Context, url string, reqBody io.Reader, contentType string) (*http.Request, error) { + req, err := http.NewRequest("POST", url, reqBody) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if customHeader := getCustomHTTPReqHeaders(ctx); customHeader != nil { + req.Header = customHeader + } + req.Header.Set("Accept", contentType) + req.Header.Set("Content-Type", contentType) + req.Header.Set("Twirp-Version", "v8.1.3") + return req, nil +} + +// JSON serialization for errors +type twerrJSON struct { + Code string `json:"code"` + Msg string `json:"msg"` + Meta map[string]string `json:"meta,omitempty"` +} + +// marshalErrorToJSON returns JSON from a twirp.Error, that can be used as HTTP error response body. +// If serialization fails, it will use a descriptive Internal error instead. +func marshalErrorToJSON(twerr twirp.Error) []byte { + // make sure that msg is not too large + msg := twerr.Msg() + if len(msg) > 1e6 { + msg = msg[:1e6] + } + + tj := twerrJSON{ + Code: string(twerr.Code()), + Msg: msg, + Meta: twerr.MetaMap(), + } + + buf, err := json.Marshal(&tj) + if err != nil { + buf = []byte("{\"type\": \"" + twirp.Internal + "\", \"msg\": \"There was an error but it could not be serialized into JSON\"}") // fallback + } + + return buf +} + +// errorFromResponse builds a twirp.Error from a non-200 HTTP response. +// If the response has a valid serialized Twirp error, then it's returned. +// If not, the response status code is used to generate a similar twirp +// error. See twirpErrorFromIntermediary for more info on intermediary errors. +func errorFromResponse(resp *http.Response) twirp.Error { + statusCode := resp.StatusCode + statusText := http.StatusText(statusCode) + + if isHTTPRedirect(statusCode) { + // Unexpected redirect: it must be an error from an intermediary. + // Twirp clients don't follow redirects automatically, Twirp only handles + // POST requests, redirects should only happen on GET and HEAD requests. + location := resp.Header.Get("Location") + msg := fmt.Sprintf("unexpected HTTP status code %d %q received, Location=%q", statusCode, statusText, location) + return twirpErrorFromIntermediary(statusCode, msg, location) + } + + respBodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return wrapInternal(err, "failed to read server error response body") + } + + var tj twerrJSON + dec := json.NewDecoder(bytes.NewReader(respBodyBytes)) + dec.DisallowUnknownFields() + if err := dec.Decode(&tj); err != nil || tj.Code == "" { + // Invalid JSON response; it must be an error from an intermediary. + msg := fmt.Sprintf("Error from intermediary with HTTP status code %d %q", statusCode, statusText) + return twirpErrorFromIntermediary(statusCode, msg, string(respBodyBytes)) + } + + errorCode := twirp.ErrorCode(tj.Code) + if !twirp.IsValidErrorCode(errorCode) { + msg := "invalid type returned from server error response: " + tj.Code + return twirp.InternalError(msg).WithMeta("body", string(respBodyBytes)) + } + + twerr := twirp.NewError(errorCode, tj.Msg) + for k, v := range tj.Meta { + twerr = twerr.WithMeta(k, v) + } + return twerr +} + +// twirpErrorFromIntermediary maps HTTP errors from non-twirp sources to twirp errors. +// The mapping is similar to gRPC: https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md. +// Returned twirp Errors have some additional metadata for inspection. +func twirpErrorFromIntermediary(status int, msg string, bodyOrLocation string) twirp.Error { + var code twirp.ErrorCode + if isHTTPRedirect(status) { // 3xx + code = twirp.Internal + } else { + switch status { + case 400: // Bad Request + code = twirp.Internal + case 401: // Unauthorized + code = twirp.Unauthenticated + case 403: // Forbidden + code = twirp.PermissionDenied + case 404: // Not Found + code = twirp.BadRoute + case 429: // Too Many Requests + code = twirp.ResourceExhausted + case 502, 503, 504: // Bad Gateway, Service Unavailable, Gateway Timeout + code = twirp.Unavailable + default: // All other codes + code = twirp.Unknown + } + } + + twerr := twirp.NewError(code, msg) + twerr = twerr.WithMeta("http_error_from_intermediary", "true") // to easily know if this error was from intermediary + twerr = twerr.WithMeta("status_code", strconv.Itoa(status)) + if isHTTPRedirect(status) { + twerr = twerr.WithMeta("location", bodyOrLocation) + } else { + twerr = twerr.WithMeta("body", bodyOrLocation) + } + return twerr +} + +func isHTTPRedirect(status int) bool { + return status >= 300 && status <= 399 +} + +// wrapInternal wraps an error with a prefix as an Internal error. +// The original error cause is accessible by github.com/pkg/errors.Cause. +func wrapInternal(err error, prefix string) twirp.Error { + return twirp.InternalErrorWith(&wrappedError{prefix: prefix, cause: err}) +} + +type wrappedError struct { + prefix string + cause error +} + +func (e *wrappedError) Error() string { return e.prefix + ": " + e.cause.Error() } +func (e *wrappedError) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As +func (e *wrappedError) Cause() error { return e.cause } // for github.com/pkg/errors + +// ensurePanicResponses makes sure that rpc methods causing a panic still result in a Twirp Internal +// error response (status 500), and error hooks are properly called with the panic wrapped as an error. +// The panic is re-raised so it can be handled normally with middleware. +func ensurePanicResponses(ctx context.Context, resp http.ResponseWriter, hooks *twirp.ServerHooks) { + if r := recover(); r != nil { + // Wrap the panic as an error so it can be passed to error hooks. + // The original error is accessible from error hooks, but not visible in the response. + err := errFromPanic(r) + twerr := &internalWithCause{msg: "Internal service panic", cause: err} + // Actually write the error + writeError(ctx, resp, twerr, hooks) + // If possible, flush the error to the wire. + f, ok := resp.(http.Flusher) + if ok { + f.Flush() + } + + panic(r) + } +} + +// errFromPanic returns the typed error if the recovered panic is an error, otherwise formats as error. +func errFromPanic(p interface{}) error { + if err, ok := p.(error); ok { + return err + } + return fmt.Errorf("panic: %v", p) +} + +// internalWithCause is a Twirp Internal error wrapping an original error cause, +// but the original error message is not exposed on Msg(). The original error +// can be checked with go1.13+ errors.Is/As, and also by (github.com/pkg/errors).Unwrap +type internalWithCause struct { + msg string + cause error +} + +func (e *internalWithCause) Unwrap() error { return e.cause } // for go1.13 + errors.Is/As +func (e *internalWithCause) Cause() error { return e.cause } // for github.com/pkg/errors +func (e *internalWithCause) Error() string { return e.msg + ": " + e.cause.Error() } +func (e *internalWithCause) Code() twirp.ErrorCode { return twirp.Internal } +func (e *internalWithCause) Msg() string { return e.msg } +func (e *internalWithCause) Meta(key string) string { return "" } +func (e *internalWithCause) MetaMap() map[string]string { return nil } +func (e *internalWithCause) WithMeta(key string, val string) twirp.Error { return e } + +// malformedRequestError is used when the twirp server cannot unmarshal a request +func malformedRequestError(msg string) twirp.Error { + return twirp.NewError(twirp.Malformed, msg) +} + +// badRouteError is used when the twirp server cannot route a request +func badRouteError(msg string, method, url string) twirp.Error { + err := twirp.NewError(twirp.BadRoute, msg) + err = err.WithMeta("twirp_invalid_route", method+" "+url) + return err +} + +// withoutRedirects makes sure that the POST request can not be redirected. +// The standard library will, by default, redirect requests (including POSTs) if it gets a 302 or +// 303 response, and also 301s in go1.8. It redirects by making a second request, changing the +// method to GET and removing the body. This produces very confusing error messages, so instead we +// set a redirect policy that always errors. This stops Go from executing the redirect. +// +// We have to be a little careful in case the user-provided http.Client has its own CheckRedirect +// policy - if so, we'll run through that policy first. +// +// Because this requires modifying the http.Client, we make a new copy of the client and return it. +func withoutRedirects(in *http.Client) *http.Client { + copy := *in + copy.CheckRedirect = func(req *http.Request, via []*http.Request) error { + if in.CheckRedirect != nil { + // Run the input's redirect if it exists, in case it has side effects, but ignore any error it + // returns, since we want to use ErrUseLastResponse. + err := in.CheckRedirect(req, via) + _ = err // Silly, but this makes sure generated code passes errcheck -blank, which some people use. + } + return http.ErrUseLastResponse + } + return © +} + +// doProtobufRequest makes a Protobuf request to the remote Twirp service. +func doProtobufRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { + reqBodyBytes, err := proto.Marshal(in) + if err != nil { + return ctx, wrapInternal(err, "failed to marshal proto request") + } + reqBody := bytes.NewBuffer(reqBodyBytes) + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, reqBody, "application/protobuf") + if err != nil { + return ctx, wrapInternal(err, "could not build request") + } + ctx, err = callClientRequestPrepared(ctx, hooks, req) + if err != nil { + return ctx, err + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return ctx, wrapInternal(err, "failed to do request") + } + defer func() { _ = resp.Body.Close() }() + + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return ctx, errorFromResponse(resp) + } + + respBodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return ctx, wrapInternal(err, "failed to read response body") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if err = proto.Unmarshal(respBodyBytes, out); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal proto response") + } + return ctx, nil +} + +// doJSONRequest makes a JSON request to the remote Twirp service. +func doJSONRequest(ctx context.Context, client HTTPClient, hooks *twirp.ClientHooks, url string, in, out proto.Message) (_ context.Context, err error) { + marshaler := &protojson.MarshalOptions{UseProtoNames: true} + reqBytes, err := marshaler.Marshal(in) + if err != nil { + return ctx, wrapInternal(err, "failed to marshal json request") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + req, err := newRequest(ctx, url, bytes.NewReader(reqBytes), "application/json") + if err != nil { + return ctx, wrapInternal(err, "could not build request") + } + ctx, err = callClientRequestPrepared(ctx, hooks, req) + if err != nil { + return ctx, err + } + + req = req.WithContext(ctx) + resp, err := client.Do(req) + if err != nil { + return ctx, wrapInternal(err, "failed to do request") + } + + defer func() { + cerr := resp.Body.Close() + if err == nil && cerr != nil { + err = wrapInternal(cerr, "failed to close response body") + } + }() + + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + + if resp.StatusCode != 200 { + return ctx, errorFromResponse(resp) + } + + d := json.NewDecoder(resp.Body) + rawRespBody := json.RawMessage{} + if err := d.Decode(&rawRespBody); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal json response") + } + unmarshaler := protojson.UnmarshalOptions{DiscardUnknown: true} + if err = unmarshaler.Unmarshal(rawRespBody, out); err != nil { + return ctx, wrapInternal(err, "failed to unmarshal json response") + } + if err = ctx.Err(); err != nil { + return ctx, wrapInternal(err, "aborted because context was done") + } + return ctx, nil +} + +// Call twirp.ServerHooks.RequestReceived if the hook is available +func callRequestReceived(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestReceived == nil { + return ctx, nil + } + return h.RequestReceived(ctx) +} + +// Call twirp.ServerHooks.RequestRouted if the hook is available +func callRequestRouted(ctx context.Context, h *twirp.ServerHooks) (context.Context, error) { + if h == nil || h.RequestRouted == nil { + return ctx, nil + } + return h.RequestRouted(ctx) +} + +// Call twirp.ServerHooks.ResponsePrepared if the hook is available +func callResponsePrepared(ctx context.Context, h *twirp.ServerHooks) context.Context { + if h == nil || h.ResponsePrepared == nil { + return ctx + } + return h.ResponsePrepared(ctx) +} + +// Call twirp.ServerHooks.ResponseSent if the hook is available +func callResponseSent(ctx context.Context, h *twirp.ServerHooks) { + if h == nil || h.ResponseSent == nil { + return + } + h.ResponseSent(ctx) +} + +// Call twirp.ServerHooks.Error if the hook is available +func callError(ctx context.Context, h *twirp.ServerHooks, err twirp.Error) context.Context { + if h == nil || h.Error == nil { + return ctx + } + return h.Error(ctx, err) +} + +func callClientResponseReceived(ctx context.Context, h *twirp.ClientHooks) { + if h == nil || h.ResponseReceived == nil { + return + } + h.ResponseReceived(ctx) +} + +func callClientRequestPrepared(ctx context.Context, h *twirp.ClientHooks, req *http.Request) (context.Context, error) { + if h == nil || h.RequestPrepared == nil { + return ctx, nil + } + return h.RequestPrepared(ctx, req) +} + +func callClientError(ctx context.Context, h *twirp.ClientHooks, err twirp.Error) { + if h == nil || h.Error == nil { + return + } + h.Error(ctx, err) +} + +var twirpFileDescriptor0 = []byte{ + // 749 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x54, 0xcd, 0x8e, 0xdb, 0x36, + 0x10, 0x86, 0x2c, 0xaf, 0x2d, 0x8f, 0x83, 0xc6, 0xcb, 0x34, 0x81, 0xe2, 0x34, 0xad, 0xeb, 0x43, + 0x61, 0x20, 0x80, 0xdd, 0x75, 0x5a, 0xf4, 0xef, 0xd6, 0xec, 0xa6, 0x48, 0x91, 0x22, 0x01, 0x1d, + 0xf4, 0xd0, 0x8b, 0x4a, 0x53, 0xb3, 0x0a, 0x61, 0x59, 0xd2, 0x92, 0x94, 0x00, 0x3f, 0x46, 0xaf, + 0x7d, 0xaf, 0x3e, 0x45, 0x5f, 0xa2, 0xe0, 0x8f, 0x8c, 0xb5, 0xbd, 0xdb, 0x93, 0x38, 0xf3, 0x7d, + 0xf3, 0xa3, 0xe1, 0x37, 0x84, 0xa7, 0xb2, 0xe2, 0x0b, 0xc5, 0x59, 0x51, 0xa0, 0x5c, 0x28, 0x94, + 0x8d, 0xe0, 0x38, 0xaf, 0x64, 0xa9, 0x4b, 0x32, 0xd2, 0x52, 0x34, 0xbb, 0xb9, 0x07, 0xe7, 0xcd, + 0xc5, 0x38, 0x36, 0x64, 0x5e, 0x6e, 0xb7, 0x65, 0x71, 0xc8, 0x9d, 0xfe, 0x1d, 0xc0, 0x70, 0xc5, + 0x59, 0x41, 0xf1, 0xa6, 0x46, 0xa5, 0xc9, 0x13, 0xe8, 0x69, 0x26, 0x33, 0xd4, 0x71, 0x30, 0x09, + 0x66, 0x03, 0xea, 0x2d, 0xf2, 0x05, 0x0c, 0x99, 0xd4, 0xe2, 0x9a, 0x71, 0x9d, 0x88, 0x34, 0xee, + 0x58, 0x10, 0x5a, 0xd7, 0x9b, 0x94, 0x3c, 0x85, 0x68, 0x9d, 0x97, 0xeb, 0x44, 0xa4, 0x2a, 0x0e, + 0x27, 0xe1, 0x6c, 0x40, 0xfb, 0xc6, 0x7e, 0x93, 0x2a, 0xf2, 0x1d, 0xf4, 0xcb, 0x4a, 0x8b, 0xb2, + 0x50, 0x71, 0x77, 0x12, 0xcc, 0x86, 0xcb, 0xe7, 0xf3, 0xe3, 0x0e, 0xe7, 0xa6, 0x87, 0x77, 0x8e, + 0x44, 0x5b, 0xf6, 0x74, 0x02, 0xd1, 0x5b, 0xc1, 0xb1, 0x50, 0xa8, 0xc8, 0xa7, 0x70, 0x56, 0xb0, + 0x2d, 0xaa, 0x38, 0xb0, 0xc9, 0x9d, 0x31, 0xfd, 0x37, 0x74, 0xed, 0xfb, 0x50, 0xf2, 0x0c, 0x06, + 0xd5, 0x26, 0x4b, 0xf4, 0xae, 0xda, 0x33, 0xa3, 0x6a, 0x93, 0x7d, 0x30, 0x36, 0x19, 0x43, 0xe4, + 0x2b, 0xaa, 0xb8, 0xe3, 0xb0, 0xd6, 0x26, 0x1c, 0x48, 0xee, 0x4a, 0x25, 0x9c, 0x69, 0xcc, 0x4a, + 0x29, 0xd0, 0xb4, 0x1b, 0xce, 0x86, 0xcb, 0x6f, 0xfe, 0xb7, 0xdd, 0xb9, 0x6f, 0xf1, 0xd5, 0x3e, + 0xec, 0xaa, 0xd0, 0x72, 0x47, 0xcf, 0xf3, 0x63, 0x3f, 0x99, 0xc1, 0x48, 0x14, 0x3c, 0xaf, 0x53, + 0x4c, 0x52, 0x6c, 0x92, 0x14, 0x2b, 0x15, 0x9f, 0x4d, 0x82, 0x59, 0x44, 0x3f, 0xf1, 0xfe, 0x4b, + 0x6c, 0x2e, 0xb1, 0x52, 0xe4, 0x05, 0x9c, 0x9b, 0xff, 0x90, 0x98, 0x33, 0x5b, 0xe4, 0xa3, 0xa8, + 0x54, 0xdc, 0xb3, 0x3d, 0x8f, 0xaa, 0x4d, 0x46, 0x6f, 0xfb, 0xc9, 0x0c, 0x7a, 0xa9, 0x50, 0x5a, + 0x96, 0x71, 0xdf, 0x8e, 0x77, 0xe4, 0xfb, 0x75, 0x17, 0x3e, 0x7f, 0xb7, 0xa2, 0x1e, 0x27, 0x4b, + 0x78, 0xdc, 0xd4, 0x79, 0x91, 0x28, 0x6c, 0x50, 0x0a, 0xbd, 0x4b, 0x54, 0x59, 0x4b, 0x8e, 0x2a, + 0x8e, 0x6c, 0xea, 0x47, 0x06, 0x5c, 0x79, 0x6c, 0xe5, 0x20, 0xf2, 0x25, 0x3c, 0x68, 0x27, 0x73, + 0x5d, 0xe7, 0x79, 0x3c, 0xb0, 0x0d, 0x0f, 0xbd, 0xef, 0x75, 0x9d, 0xe7, 0xe3, 0x3f, 0xe1, 0xc9, + 0xdd, 0x43, 0x20, 0x23, 0x08, 0x37, 0xb8, 0xf3, 0x5a, 0x32, 0x47, 0xf2, 0x35, 0x9c, 0x35, 0x2c, + 0xaf, 0xd1, 0x4a, 0x68, 0xb8, 0x1c, 0x9f, 0xce, 0xb6, 0xbd, 0x72, 0xea, 0x88, 0x3f, 0x76, 0xbe, + 0x0f, 0x7e, 0xed, 0x46, 0xe1, 0xa8, 0x3b, 0xfd, 0x2b, 0x80, 0x07, 0x4e, 0xac, 0xaa, 0x2a, 0x0b, + 0x85, 0x64, 0x02, 0x9d, 0x52, 0xd9, 0xec, 0x77, 0xfd, 0x75, 0xa7, 0x54, 0x64, 0x09, 0x7d, 0x89, + 0xaa, 0xce, 0xb5, 0x53, 0xe5, 0x70, 0x19, 0x9f, 0x16, 0xa4, 0x96, 0x40, 0x5b, 0x22, 0x79, 0x01, + 0xbd, 0x9c, 0xed, 0x8c, 0x4a, 0xdc, 0xfd, 0x3f, 0x3a, 0xcc, 0xfc, 0xd6, 0x60, 0xd4, 0x53, 0xa6, + 0xff, 0x84, 0xd0, 0x73, 0x09, 0xee, 0xdd, 0x9d, 0x2b, 0x78, 0x68, 0x06, 0x8b, 0x92, 0xad, 0x45, + 0x2e, 0xb4, 0x11, 0x56, 0xc7, 0x26, 0x7e, 0x76, 0x98, 0xf8, 0xf7, 0x5b, 0xa4, 0x1d, 0x3d, 0x8e, + 0x21, 0x1f, 0xe0, 0x7c, 0x2b, 0x14, 0x2f, 0x8b, 0x6b, 0x91, 0xd5, 0x92, 0xb5, 0x0b, 0x65, 0x12, + 0x7d, 0x75, 0x98, 0xe8, 0x12, 0x35, 0x72, 0x8d, 0xe9, 0x6f, 0x47, 0x74, 0x7a, 0x9a, 0xc0, 0xec, + 0x15, 0xcf, 0x99, 0x32, 0xea, 0x32, 0x3d, 0x3b, 0x83, 0x10, 0xe8, 0x9a, 0x1d, 0x8a, 0x43, 0xeb, + 0xb4, 0x67, 0x72, 0x01, 0x51, 0xc5, 0xf8, 0x86, 0x65, 0x68, 0x54, 0x6b, 0xca, 0x3e, 0x3e, 0x2c, + 0xfb, 0xde, 0xa1, 0x74, 0x4f, 0x23, 0xbf, 0xc0, 0x88, 0xd7, 0x4a, 0x97, 0xdb, 0x44, 0x62, 0x2b, + 0xb5, 0xbe, 0x0d, 0xfd, 0xec, 0x30, 0xf4, 0x95, 0x65, 0x51, 0x4f, 0xa2, 0x0f, 0xf9, 0x81, 0xad, + 0xc8, 0xb7, 0xd0, 0x57, 0xc8, 0x25, 0x6a, 0x27, 0xd5, 0x93, 0xd1, 0xad, 0x2c, 0xf8, 0x5a, 0x14, + 0xa9, 0x28, 0x32, 0xda, 0x72, 0xc9, 0x0f, 0x10, 0x79, 0x9d, 0xaa, 0x78, 0x60, 0xe3, 0x9e, 0xdf, + 0x3d, 0x29, 0xaf, 0x39, 0xba, 0xa7, 0x2f, 0xdf, 0x43, 0x7f, 0xe5, 0x24, 0x42, 0xae, 0xa0, 0x6b, + 0x8e, 0xe4, 0x9e, 0x67, 0xcb, 0x3f, 0x9d, 0xe3, 0xcf, 0xef, 0x83, 0x9d, 0x58, 0x7f, 0x7e, 0xf9, + 0xc7, 0x45, 0x26, 0xf4, 0xc7, 0x7a, 0x6d, 0x8a, 0x2f, 0xd8, 0x4d, 0xcd, 0x14, 0xf2, 0xda, 0xac, + 0xda, 0xc2, 0x06, 0x2e, 0x6e, 0xbd, 0xe8, 0x3f, 0xf9, 0xef, 0xba, 0x67, 0x9f, 0xe9, 0x97, 0xff, + 0x05, 0x00, 0x00, 0xff, 0xff, 0x68, 0x51, 0xa0, 0x7d, 0xef, 0x05, 0x00, 0x00, +} diff --git a/schema/trivy-config.json b/schema/trivy-config.json new file mode 100644 index 000000000000..44e279a1e581 --- /dev/null +++ b/schema/trivy-config.json @@ -0,0 +1,933 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "quiet": { + "type": "boolean", + "description": "suppress progress bar and log output" + }, + "debug": { + "type": "boolean", + "description": "debug mode" + }, + "insecure": { + "type": "boolean", + "description": "allow insecure server connections" + }, + "cacert": { + "type": "string", + "description": "Path to PEM-encoded CA certificate file" + }, + "timeout": { + "type": "string", + "description": "timeout" + }, + "cache": { + "properties": { + "dir": { + "type": "string", + "description": "cache directory" + }, + "backend": { + "type": "string", + "description": "[EXPERIMENTAL] cache backend (e.g. redis://localhost:6379)" + }, + "ttl": { + "type": "string", + "description": "cache TTL when using redis as cache backend" + }, + "redis": { + "properties": { + "tls": { + "type": "boolean", + "description": "enable redis TLS with public certificates, if using redis as cache backend" + }, + "ca": { + "type": "string", + "description": "redis ca file location, if using redis as cache backend" + }, + "cert": { + "type": "string", + "description": "redis certificate file location, if using redis as cache backend" + }, + "key": { + "type": "string", + "description": "redis key file location, if using redis as cache backend" + } + }, + "type": "object" + } + }, + "type": "object" + }, + "clean": { + "properties": { + "all": { + "type": "boolean", + "description": "remove all caches" + }, + "scan-cache": { + "type": "boolean", + "description": "remove scan cache (container and VM image analysis results)" + }, + "vuln-db": { + "type": "boolean", + "description": "remove vulnerability database" + }, + "java-db": { + "type": "boolean", + "description": "remove Java database" + }, + "checks-bundle": { + "type": "boolean", + "description": "remove checks bundle" + }, + "vex-repo": { + "type": "boolean", + "description": "remove VEX repositories" + } + }, + "type": "object" + }, + "server": { + "properties": { + "token": { + "type": "string", + "description": "for authentication in client/server mode" + }, + "token-header": { + "type": "string", + "description": "specify a header name for token in client/server mode" + }, + "addr": { + "type": "string", + "description": "server address in client mode" + }, + "custom-headers": { + "items": { + "type": "string" + }, + "type": "array", + "description": "custom headers in client mode" + }, + "listen": { + "type": "string", + "description": "listen address in server mode" + } + }, + "type": "object" + }, + "db": { + "properties": { + "download-only": { + "type": "boolean", + "description": "download/update vulnerability database but don't run a scan" + }, + "skip-update": { + "type": "boolean", + "description": "skip updating vulnerability database" + }, + "download-java-only": { + "type": "boolean", + "description": "download/update Java index database but don't run a scan" + }, + "java-skip-update": { + "type": "boolean", + "description": "skip updating Java index database" + }, + "no-progress": { + "type": "boolean", + "description": "suppress progress bar" + }, + "repository": { + "items": { + "type": "string" + }, + "type": "array", + "description": "OCI repository(ies) to retrieve trivy-db in order of priority" + }, + "java-repository": { + "items": { + "type": "string" + }, + "type": "array", + "description": "OCI repository(ies) to retrieve trivy-java-db in order of priority" + } + }, + "type": "object" + }, + "image": { + "properties": { + "input": { + "type": "string", + "description": "input file path instead of image name" + }, + "image-config-scanners": { + "items": { + "type": "string", + "enum": [ + "misconfig", + "secret" + ] + }, + "type": "array", + "description": "comma-separated list of what security issues to detect on container image configurations" + }, + "removed-pkgs": { + "type": "boolean", + "description": "detect vulnerabilities of removed packages (only for Alpine)" + }, + "platform": { + "type": "string", + "description": "set platform in the form os/arch if image is multi-platform capable" + }, + "docker": { + "properties": { + "host": { + "type": "string", + "description": "unix domain socket path to use for docker scanning" + } + }, + "type": "object" + }, + "podman": { + "properties": { + "host": { + "type": "string", + "description": "unix podman socket path to use for podman scanning" + } + }, + "type": "object" + }, + "source": { + "items": { + "type": "string", + "enum": [ + "docker", + "containerd", + "podman", + "remote" + ] + }, + "type": "array", + "description": "image source(s) to use, in priority order" + }, + "max-size": { + "type": "string", + "description": "[EXPERIMENTAL] maximum image size to process, specified in a human-readable format (e.g., '44kB', '17MB'); an error will be returned if the image exceeds this size" + } + }, + "type": "object" + }, + "kubernetes": { + "properties": { + "kubeconfig": { + "type": "string", + "description": "specify the kubeconfig file path to use" + }, + "k8s-version": { + "type": "string", + "description": "specify k8s version to validate outdated api by it (example: 1.21.0)" + }, + "disableNodeCollector": { + "type": "boolean", + "description": "When the flag is activated, the node-collector job will not be executed, thus skipping misconfiguration findings on the node." + }, + "tolerations": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify node-collector job tolerations (example: key1=value1:NoExecute,key2=value2:NoSchedule)" + }, + "node-collector": { + "properties": { + "namespace": { + "type": "string", + "description": "specify the namespace in which the node-collector job should be deployed" + }, + "imageref": { + "type": "string", + "description": "indicate the image reference for the node-collector scan job" + } + }, + "type": "object" + }, + "exclude": { + "properties": { + "owned": { + "type": "boolean", + "description": "exclude resources that have an owner reference" + }, + "nodes": { + "items": { + "type": "string" + }, + "type": "array", + "description": "indicate the node labels that the node-collector job should exclude from scanning (example: kubernetes.io/arch:arm64,team:dev)" + } + }, + "type": "object" + }, + "excludeKinds": { + "items": { + "type": "string" + }, + "type": "array", + "description": "indicate the kinds exclude from scanning (example: node)" + }, + "includeKinds": { + "items": { + "type": "string" + }, + "type": "array", + "description": "indicate the kinds included in scanning (example: node)" + }, + "excludeNamespaces": { + "items": { + "type": "string" + }, + "type": "array", + "description": "indicate the namespaces excluded from scanning (example: kube-system)" + }, + "includeNamespaces": { + "items": { + "type": "string" + }, + "type": "array", + "description": "indicate the namespaces included in scanning (example: kube-system)" + }, + "qps": { + "type": "number", + "description": "specify the maximum QPS to the master from this client" + }, + "skipImages": { + "type": "boolean", + "description": "skip the downloading and scanning of images (vulnerabilities and secrets) in the cluster resources" + }, + "burst": { + "type": "integer", + "description": "specify the maximum burst for throttle" + } + }, + "type": "object" + }, + "license": { + "properties": { + "full": { + "type": "boolean", + "description": "eagerly look for licenses in source code headers and license files" + }, + "ignored": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify a list of license to ignore" + }, + "forbidden": { + "items": { + "type": "string" + }, + "type": "array", + "description": "forbidden licenses" + }, + "restricted": { + "items": { + "type": "string" + }, + "type": "array", + "description": "restricted licenses" + }, + "reciprocal": { + "items": { + "type": "string" + }, + "type": "array", + "description": "reciprocal licenses" + }, + "notice": { + "items": { + "type": "string" + }, + "type": "array", + "description": "notice licenses" + }, + "permissive": { + "items": { + "type": "string" + }, + "type": "array", + "description": "permissive licenses" + }, + "unencumbered": { + "items": { + "type": "string" + }, + "type": "array", + "description": "unencumbered licenses" + }, + "confidenceLevel": { + "type": "number", + "description": "specify license classifier's confidence level" + } + }, + "type": "object" + }, + "misconfiguration": { + "properties": { + "include-non-failures": { + "type": "boolean", + "description": "include successes, available with '--scanners misconfig'" + }, + "checks-bundle-repository": { + "type": "string", + "description": "OCI registry URL to retrieve checks bundle from" + }, + "helm": { + "properties": { + "set": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify Helm values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)" + }, + "values": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify paths to override the Helm values.yaml files" + }, + "set-file": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify Helm values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)" + }, + "set-string": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify Helm string values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)" + }, + "api-versions": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Available API versions used for Capabilities.APIVersions. This flag is the same as the api-versions flag of the helm template command. (can specify multiple or separate values with commas: policy/v1/PodDisruptionBudget,apps/v1/Deployment)" + }, + "kube-version": { + "type": "string", + "description": "Kubernetes version used for Capabilities.KubeVersion. This flag is the same as the kube-version flag of the helm template command." + } + }, + "type": "object" + }, + "terraform": { + "properties": { + "vars": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify paths to override the Terraform tfvars files" + }, + "exclude-downloaded-modules": { + "type": "boolean", + "description": "exclude misconfigurations for downloaded terraform modules" + } + }, + "type": "object" + }, + "cloudformation": { + "properties": { + "params": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify paths to override the CloudFormation parameters files" + } + }, + "type": "object" + }, + "scanners": { + "items": { + "type": "string" + }, + "type": "array", + "description": "comma-separated list of misconfig scanners to use for misconfiguration scanning" + }, + "config-file-schemas": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify paths to JSON configuration file schemas to determine that a file matches some configuration and pass the schema to Rego checks for type checking" + }, + "render-cause": { + "items": { + "type": "string", + "enum": [ + "terraform", + "ansible" + ] + }, + "type": "array", + "description": "specify configuration types for which the rendered causes will be shown in the table report" + }, + "raw-config-scanners": { + "items": { + "type": "string", + "enum": [ + "terraform" + ] + }, + "type": "array", + "description": "specify the types of scanners that will also scan raw configurations. For example, scanners will scan a non-adapted configuration into a shared state" + } + }, + "type": "object" + }, + "ansible": { + "properties": { + "playbooks": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify playbook file path(s) to scan" + }, + "inventories": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify inventory host path or comma separated host list" + }, + "extra-vars": { + "items": { + "type": "string" + }, + "type": "array", + "description": "set additional variables as key=value or @file (YAML/JSON)" + } + }, + "type": "object" + }, + "module": { + "properties": { + "dir": { + "type": "string", + "description": "specify directory to the wasm modules that will be loaded" + }, + "enable-modules": { + "items": { + "type": "string" + }, + "type": "array", + "description": "[EXPERIMENTAL] module names to enable" + } + }, + "type": "object" + }, + "pkg": { + "properties": { + "include-dev-deps": { + "type": "boolean", + "description": "include development dependencies in the report (supported: npm, yarn, gradle)" + }, + "types": { + "items": { + "type": "string", + "enum": [ + "os", + "library" + ] + }, + "type": "array", + "description": "list of package types" + }, + "relationships": { + "items": { + "type": "string", + "enum": [ + "unknown", + "root", + "workspace", + "direct", + "indirect" + ] + }, + "type": "array", + "description": "list of package relationships" + } + }, + "type": "object" + }, + "registry": { + "properties": { + "username": { + "items": { + "type": "string" + }, + "type": "array", + "description": "username. Comma-separated usernames allowed." + }, + "password": { + "items": { + "type": "string" + }, + "type": "array", + "description": "password. Comma-separated passwords allowed. TRIVY_PASSWORD should be used for security reasons." + }, + "password-stdin": { + "type": "boolean", + "description": "password from stdin. Comma-separated passwords are not supported." + }, + "token": { + "type": "string", + "description": "registry token" + }, + "mirrors": { + "additionalProperties": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": "object", + "description": "map of hosts and registries for them." + } + }, + "type": "object" + }, + "rego": { + "properties": { + "include-deprecated-checks": { + "type": "boolean", + "description": "include deprecated checks" + }, + "skip-check-update": { + "type": "boolean", + "description": "skip fetching rego check updates" + }, + "trace": { + "type": "boolean", + "description": "enable more verbose trace output for custom queries" + }, + "check": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify the paths to the Rego check files or to the directories containing them, applying config files" + }, + "data": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify paths from which data for the Rego checks will be recursively loaded" + }, + "namespaces": { + "items": { + "type": "string" + }, + "type": "array", + "description": "Rego namespaces" + }, + "error-limit": { + "type": "integer", + "description": "maximum number of compile errors allowed during Rego policy evaluation" + } + }, + "type": "object" + }, + "format": { + "type": "string", + "enum": [ + "table", + "json", + "template", + "sarif", + "cyclonedx", + "spdx", + "spdx-json", + "github", + "cosign-vuln" + ], + "description": "format" + }, + "report": { + "type": "string", + "enum": [ + "all", + "summary" + ], + "description": "specify a report format for the output" + }, + "template": { + "type": "string", + "description": "output template" + }, + "dependency-tree": { + "type": "boolean", + "description": "[EXPERIMENTAL] show dependency origin tree of vulnerable packages" + }, + "list-all-pkgs": { + "type": "boolean", + "description": "output all packages in the JSON report regardless of vulnerability" + }, + "ignorefile": { + "type": "string", + "description": "specify .trivyignore file" + }, + "ignore-policy": { + "type": "string", + "description": "specify the Rego file path to evaluate each vulnerability" + }, + "exit-code": { + "type": "integer", + "description": "specify exit code when any security issues are found" + }, + "exit-on-eol": { + "type": "integer", + "description": "exit with the specified code when the OS reaches end of service/life" + }, + "output": { + "type": "string", + "description": "output file name" + }, + "output-plugin-arg": { + "type": "string", + "description": "[EXPERIMENTAL] output plugin arguments" + }, + "severity": { + "items": { + "type": "string", + "enum": [ + "UNKNOWN", + "LOW", + "MEDIUM", + "HIGH", + "CRITICAL" + ] + }, + "type": "array", + "description": "severities of security issues to be displayed" + }, + "scan": { + "properties": { + "compliance": { + "type": "string", + "description": "compliance report to generate" + }, + "show-suppressed": { + "type": "boolean", + "description": "[EXPERIMENTAL] show suppressed vulnerabilities" + }, + "skip-dirs": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify the directories or glob patterns to skip" + }, + "skip-files": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify the files or glob patterns to skip" + }, + "offline": { + "type": "boolean", + "description": "do not issue API requests to identify dependencies" + }, + "scanners": { + "items": { + "type": "string", + "enum": [ + "vuln", + "misconfig", + "secret", + "license" + ] + }, + "type": "array", + "description": "comma-separated list of what security issues to detect" + }, + "file-patterns": { + "items": { + "type": "string" + }, + "type": "array", + "description": "specify config file patterns" + }, + "parallel": { + "type": "integer", + "description": "number of goroutines enabled for parallel scanning, set 0 to auto-detect parallelism" + }, + "sbom-sources": { + "items": { + "type": "string", + "enum": [ + "oci", + "rekor" + ] + }, + "type": "array", + "description": "[EXPERIMENTAL] try to retrieve SBOM from the specified sources" + }, + "rekor-url": { + "type": "string", + "description": "[EXPERIMENTAL] address of rekor STL server" + }, + "detection-priority": { + "type": "string", + "enum": [ + "precise", + "comprehensive" + ], + "description": "specify the detection priority:\n - \"precise\": Prioritizes precise by minimizing false positives.\n - \"comprehensive\": Aims to detect more security findings at the cost of potential false positives.\n" + }, + "distro": { + "type": "string", + "description": "[EXPERIMENTAL] specify a distribution, \u003cfamily\u003e/\u003cversion\u003e" + }, + "skip-version-check": { + "type": "boolean", + "description": "suppress notices about version updates and Trivy announcements" + }, + "disable-telemetry": { + "type": "boolean", + "description": "disable sending anonymous usage data to Aqua" + } + }, + "type": "object" + }, + "table-mode": { + "items": { + "type": "string", + "enum": [ + "summary", + "detailed" + ] + }, + "type": "array", + "description": "[EXPERIMENTAL] tables that will be displayed in 'table' format" + }, + "repository": { + "properties": { + "branch": { + "type": "string", + "description": "pass the branch name to be scanned" + }, + "commit": { + "type": "string", + "description": "pass the commit hash to be scanned" + }, + "tag": { + "type": "string", + "description": "pass the tag name to be scanned" + } + }, + "type": "object" + }, + "secret": { + "properties": { + "config": { + "type": "string", + "description": "specify a path to config file for secret scanning" + } + }, + "type": "object" + }, + "vulnerability": { + "properties": { + "ignore-unfixed": { + "type": "boolean", + "description": "display only fixed vulnerabilities" + }, + "ignore-status": { + "items": { + "type": "string", + "enum": [ + "unknown", + "not_affected", + "affected", + "fixed", + "under_investigation", + "will_not_fix", + "fix_deferred", + "end_of_life" + ] + }, + "type": "array", + "description": "comma-separated list of vulnerability status to ignore" + }, + "vex": { + "items": { + "type": "string" + }, + "type": "array", + "description": "[EXPERIMENTAL] VEX sources (\"repo\", \"oci\" or file path)" + }, + "skip-vex-repo-update": { + "type": "boolean", + "description": "[EXPERIMENTAL] Skip VEX Repository update" + }, + "severity-source": { + "items": { + "type": "string", + "enum": [ + "nvd", + "redhat", + "redhat-oval", + "debian", + "ubuntu", + "alpine", + "amazon", + "oracle-oval", + "suse-cvrf", + "photon", + "arch-linux", + "alma", + "rocky", + "cbl-mariner", + "azure", + "ruby-advisory-db", + "php-security-advisories", + "nodejs-security-wg", + "ghsa", + "glad", + "aqua", + "osv", + "k8s", + "wolfi", + "chainguard", + "bitnami", + "govulndb", + "julia", + "echo", + "minimos", + "rootio", + "auto" + ] + }, + "type": "array", + "description": "order of data sources for selecting vulnerability severity level" + } + }, + "type": "object" + } + }, + "type": "object", + "title": "Trivy Configuration", + "description": "Configuration file for Trivy security scanner (trivy.yaml)" +} \ No newline at end of file